From f56a398586bc139fab5f8743ba7f188e9646683e Mon Sep 17 00:00:00 2001 From: Benjamin Klum Date: Sun, 2 Jul 2023 10:17:26 +0200 Subject: [PATCH] Playtime: Remove clip engine from repository --- playtime-clip-engine/Cargo.toml | 67 - playtime-clip-engine/src/base/clip.rs | 318 --- .../src/base/clip_edit_session.rs | 52 - .../src/base/clip_manifestation.rs | 111 - playtime-clip-engine/src/base/column.rs | 995 ------- playtime-clip-engine/src/base/history.rs | 351 --- playtime-clip-engine/src/base/matrix.rs | 2006 -------------- playtime-clip-engine/src/base/mod.rs | 14 - playtime-clip-engine/src/base/row.rs | 37 - playtime-clip-engine/src/base/slot.rs | 1373 ---------- playtime-clip-engine/src/conversion_util.rs | 62 - playtime-clip-engine/src/file_util.rs | 15 - playtime-clip-engine/src/lib.rs | 50 - playtime-clip-engine/src/metrics_util.rs | 70 - playtime-clip-engine/src/midi_util.rs | 5 - playtime-clip-engine/src/mutex_util.rs | 37 - playtime-clip-engine/src/proto/clip_engine.rs | 2433 ----------------- .../src/proto/clip_engine_ext.rs | 286 -- playtime-clip-engine/src/proto/hub.rs | 502 ---- playtime-clip-engine/src/proto/mod.rs | 13 - playtime-clip-engine/src/proto/senders.rs | 53 - playtime-clip-engine/src/proto/service.rs | 906 ------ playtime-clip-engine/src/rt/audio_hook.rs | 222 -- playtime-clip-engine/src/rt/buffer.rs | 325 --- playtime-clip-engine/src/rt/fx_hook.rs | 91 - playtime-clip-engine/src/rt/mod.rs | 18 - playtime-clip-engine/src/rt/rt_clip.rs | 2295 ---------------- playtime-clip-engine/src/rt/rt_column.rs | 1359 --------- playtime-clip-engine/src/rt/rt_matrix.rs | 366 --- playtime-clip-engine/src/rt/rt_slot.rs | 717 ----- playtime-clip-engine/src/rt/schedule_util.rs | 107 - playtime-clip-engine/src/rt/source_util.rs | 30 - .../src/rt/supplier/amplifier.rs | 102 - playtime-clip-engine/src/rt/supplier/api.rs | 516 ---- .../src/rt/supplier/audio_util.rs | 127 - playtime-clip-engine/src/rt/supplier/cache.rs | 271 -- playtime-clip-engine/src/rt/supplier/chain.rs | 706 ----- .../src/rt/supplier/clip_source.rs | 49 - .../src/rt/supplier/downbeat.rs | 166 -- .../src/rt/supplier/fade_util.rs | 131 - .../src/rt/supplier/interaction_handler.rs | 401 --- .../src/rt/supplier/log_util.rs | 89 - .../src/rt/supplier/looper.rs | 333 --- .../src/rt/supplier/midi_note_tracker.rs | 253 -- .../src/rt/supplier/midi_sequence.rs | 584 ---- .../src/rt/supplier/midi_util.rs | 136 - playtime-clip-engine/src/rt/supplier/mod.rs | 60 - .../src/rt/supplier/pre_buffer.rs | 1037 ------- .../src/rt/supplier/reaper_clip_source.rs | 217 -- .../src/rt/supplier/recorder.rs | 1772 ------------ .../src/rt/supplier/resampler.rs | 273 -- .../src/rt/supplier/section.rs | 393 --- .../src/rt/supplier/start_end_handler.rs | 125 - .../src/rt/supplier/time_series.rs | 126 - .../src/rt/supplier/time_stretcher.rs | 204 -- playtime-clip-engine/src/rt/tempo_util.rs | 51 - playtime-clip-engine/src/source_util.rs | 196 -- playtime-clip-engine/src/timeline.rs | 628 ----- playtime-clip-engine/src/tracing_util.rs | 7 - 59 files changed, 24239 deletions(-) delete mode 100644 playtime-clip-engine/Cargo.toml delete mode 100644 playtime-clip-engine/src/base/clip.rs delete mode 100644 playtime-clip-engine/src/base/clip_edit_session.rs delete mode 100644 playtime-clip-engine/src/base/clip_manifestation.rs delete mode 100644 playtime-clip-engine/src/base/column.rs delete mode 100644 playtime-clip-engine/src/base/history.rs delete mode 100644 playtime-clip-engine/src/base/matrix.rs delete mode 100644 playtime-clip-engine/src/base/mod.rs delete mode 100644 playtime-clip-engine/src/base/row.rs delete mode 100644 playtime-clip-engine/src/base/slot.rs delete mode 100644 playtime-clip-engine/src/conversion_util.rs delete mode 100644 playtime-clip-engine/src/file_util.rs delete mode 100644 playtime-clip-engine/src/lib.rs delete mode 100644 playtime-clip-engine/src/metrics_util.rs delete mode 100644 playtime-clip-engine/src/midi_util.rs delete mode 100644 playtime-clip-engine/src/mutex_util.rs delete mode 100644 playtime-clip-engine/src/proto/clip_engine.rs delete mode 100644 playtime-clip-engine/src/proto/clip_engine_ext.rs delete mode 100644 playtime-clip-engine/src/proto/hub.rs delete mode 100644 playtime-clip-engine/src/proto/mod.rs delete mode 100644 playtime-clip-engine/src/proto/senders.rs delete mode 100644 playtime-clip-engine/src/proto/service.rs delete mode 100644 playtime-clip-engine/src/rt/audio_hook.rs delete mode 100644 playtime-clip-engine/src/rt/buffer.rs delete mode 100644 playtime-clip-engine/src/rt/fx_hook.rs delete mode 100644 playtime-clip-engine/src/rt/mod.rs delete mode 100644 playtime-clip-engine/src/rt/rt_clip.rs delete mode 100644 playtime-clip-engine/src/rt/rt_column.rs delete mode 100644 playtime-clip-engine/src/rt/rt_matrix.rs delete mode 100644 playtime-clip-engine/src/rt/rt_slot.rs delete mode 100644 playtime-clip-engine/src/rt/schedule_util.rs delete mode 100644 playtime-clip-engine/src/rt/source_util.rs delete mode 100644 playtime-clip-engine/src/rt/supplier/amplifier.rs delete mode 100644 playtime-clip-engine/src/rt/supplier/api.rs delete mode 100644 playtime-clip-engine/src/rt/supplier/audio_util.rs delete mode 100644 playtime-clip-engine/src/rt/supplier/cache.rs delete mode 100644 playtime-clip-engine/src/rt/supplier/chain.rs delete mode 100644 playtime-clip-engine/src/rt/supplier/clip_source.rs delete mode 100644 playtime-clip-engine/src/rt/supplier/downbeat.rs delete mode 100644 playtime-clip-engine/src/rt/supplier/fade_util.rs delete mode 100644 playtime-clip-engine/src/rt/supplier/interaction_handler.rs delete mode 100644 playtime-clip-engine/src/rt/supplier/log_util.rs delete mode 100644 playtime-clip-engine/src/rt/supplier/looper.rs delete mode 100644 playtime-clip-engine/src/rt/supplier/midi_note_tracker.rs delete mode 100644 playtime-clip-engine/src/rt/supplier/midi_sequence.rs delete mode 100644 playtime-clip-engine/src/rt/supplier/midi_util.rs delete mode 100644 playtime-clip-engine/src/rt/supplier/mod.rs delete mode 100644 playtime-clip-engine/src/rt/supplier/pre_buffer.rs delete mode 100644 playtime-clip-engine/src/rt/supplier/reaper_clip_source.rs delete mode 100644 playtime-clip-engine/src/rt/supplier/recorder.rs delete mode 100644 playtime-clip-engine/src/rt/supplier/resampler.rs delete mode 100644 playtime-clip-engine/src/rt/supplier/section.rs delete mode 100644 playtime-clip-engine/src/rt/supplier/start_end_handler.rs delete mode 100644 playtime-clip-engine/src/rt/supplier/time_series.rs delete mode 100644 playtime-clip-engine/src/rt/supplier/time_stretcher.rs delete mode 100644 playtime-clip-engine/src/rt/tempo_util.rs delete mode 100644 playtime-clip-engine/src/source_util.rs delete mode 100644 playtime-clip-engine/src/timeline.rs delete mode 100644 playtime-clip-engine/src/tracing_util.rs diff --git a/playtime-clip-engine/Cargo.toml b/playtime-clip-engine/Cargo.toml deleted file mode 100644 index e538ad80e..000000000 --- a/playtime-clip-engine/Cargo.toml +++ /dev/null @@ -1,67 +0,0 @@ -[package] -name = "playtime-clip-engine" -version = "0.1.0" -authors = ["Benjamin Klum "] -edition = "2021" - -[dependencies] -# Own -reaper-high.workspace = true -reaper-medium.workspace = true -reaper-low.workspace = true -playtime-api.workspace = true -helgoboss-midi.workspace = true -helgoboss-learn.workspace = true -base.workspace = true -swell-ui.workspace = true - -#3rd-party -# Ring buffer for clip stretching -rtrb = "0.2.1" -crossbeam-channel.workspace = true -# For our own timeline -atomic = "0.5.1" -# For making sure that we can have a cheap atomic f64 -static_assertions = "1.1.0" -# For detecting undesired (de)allocation in real-time threads. -assert_no_alloc.workspace = true -# For using bit flags in the reaper-rs API. -enumflags2.workspace = true -serde.workspace = true -# For generating random file names -nanoid.workspace = true -# For deriving file names -slug.workspace = true -num_enum.workspace = true -# For being able to exclude fields from the derived Debug implementation -derivative.workspace = true -# For fast non-cryptographic hashing -xxhash-rust.workspace = true -# For easily deriving Display trait -derive_more.workspace = true -# For profiling -metrics.workspace = true -# For lazy-loading whether metrics enabled -once_cell.workspace = true -# For logging -tracing.workspace = true -# For gRPC server -tonic.workspace = true -# For gRPC server -prost.workspace = true -# For random clip IDs -ulid = "1.0.0" -# For being able to return iterators of different types -either.workspace = true -# For being able to serialize the persistent matrix data to JSON -serde_json.workspace = true -# For being able to quickly looking up slots and clips by ID while still maintaining order -indexmap.workspace = true -# For joining undo history labels -itertools.workspace = true -# For gRPC stream implementation -tokio-stream.workspace = true -# For gRPC implementation -futures.workspace = true -# For gRPC implementation -tokio.workspace = true \ No newline at end of file diff --git a/playtime-clip-engine/src/base/clip.rs b/playtime-clip-engine/src/base/clip.rs deleted file mode 100644 index 3256f25c1..000000000 --- a/playtime-clip-engine/src/base/clip.rs +++ /dev/null @@ -1,318 +0,0 @@ -use crate::rt::supplier::{ - ChainEquipment, KindSpecificRecordingOutcome, MidiOverdubOutcome, MidiSequence, - ReaperClipSource, RecorderRequest, RtClipSource, -}; -use crate::rt::tempo_util::{calc_tempo_factor, determine_tempo_from_time_base}; -use crate::rt::{OverridableMatrixSettings, RtClipId, RtClipSettings}; -use crate::source_util::{ - create_file_api_source, create_pcm_source_from_api_source, create_pcm_source_from_media_file, - make_media_file_path_absolute, -}; -use crate::{rt, ClipEngineResult}; -use crossbeam_channel::Sender; -use playtime_api::persistence as api; -use playtime_api::persistence::{ - ClipColor, ClipId, ClipTimeBase, Db, MidiChunkSource, Section, SourceOrigin, -}; -use reaper_high::{Project, Reaper, Track}; -use reaper_medium::{Bpm, PeakFileMode}; -use std::error::Error; -use std::fs; -use std::future::Future; -use std::path::{Path, PathBuf}; - -/// Describes a clip. -/// -/// Not loaded yet. -#[derive(Clone, PartialEq, Debug)] -pub struct Clip { - id: ClipId, - rt_id: RtClipId, - name: Option, - color: api::ClipColor, - source: api::Source, - frozen_source: Option, - active_source: SourceOrigin, - rt_settings: RtClipSettings, -} - -impl Clip { - pub fn load(api_clip: api::Clip) -> Self { - Self { - rt_settings: RtClipSettings::from_api(&api_clip), - rt_id: RtClipId::from_clip_id(&api_clip.id), - id: api_clip.id, - name: api_clip.name, - color: api_clip.color, - source: api_clip.source, - frozen_source: api_clip.frozen_source, - active_source: api_clip.active_source, - } - } - - pub fn from_recording( - id: ClipId, - kind_specific_outcome: KindSpecificRecordingOutcome, - clip_settings: RtClipSettings, - temporary_project: Option, - recording_track: &Track, - ) -> ClipEngineResult { - use KindSpecificRecordingOutcome::*; - let api_source = match kind_specific_outcome { - Midi { midi_sequence } => create_api_source_from_recorded_midi_sequence(&midi_sequence), - Audio { path, .. } => create_file_api_source(temporary_project, &path), - }; - let clip = Self { - rt_id: RtClipId::from_clip_id(&id), - id, - name: recording_track.name().map(|n| n.into_string()), - color: ClipColor::PlayTrackColor, - source: api_source, - frozen_source: None, - active_source: SourceOrigin::Normal, - rt_settings: clip_settings, - }; - Ok(clip) - } - - pub fn duplicate(&self) -> Self { - let new_id = ClipId::random(); - Self { - rt_id: RtClipId::from_clip_id(&new_id), - id: new_id, - name: self.name.clone(), - color: self.color.clone(), - source: self.source.clone(), - frozen_source: self.frozen_source.clone(), - active_source: self.active_source, - rt_settings: self.rt_settings, - } - } - - pub fn rt_settings(&self) -> &RtClipSettings { - &self.rt_settings - } - - pub fn set_data(&mut self, clip: Clip) { - self.color = clip.color; - self.name = clip.name; - self.rt_settings = clip.rt_settings; - } - - pub fn name(&self) -> Option<&str> { - self.name.as_deref() - } - - /// Creates an API clip. - /// - /// If the MIDI source is given, it will create the API source by inspecting the contents of - /// this MIDI source (instead of just cloning the API source field). With this, changes that - /// have been made to the source via MIDI editor are correctly saved. - pub fn save(&self) -> ClipEngineResult { - let clip = api::Clip { - id: self.id.clone(), - name: self.name.clone(), - source: self.source.clone(), - frozen_source: self.frozen_source.clone(), - active_source: self.active_source, - time_base: self.rt_settings.time_base, - start_timing: self.rt_settings.start_timing, - stop_timing: self.rt_settings.stop_timing, - looped: self.rt_settings.looped, - volume: self.rt_settings.volume, - color: self.color.clone(), - section: self.rt_settings.section, - audio_settings: self.rt_settings.audio_settings, - midi_settings: self.rt_settings.midi_settings, - }; - Ok(clip) - } - - pub fn activate_frozen_source(&mut self, frozen_source: api::Source, tempo: Option) { - self.frozen_source = Some(frozen_source); - self.active_source = SourceOrigin::Frozen; - if let ClipTimeBase::Beat(tb) = &mut self.rt_settings.time_base { - let tempo = tempo.expect("tempo not given although beat time base"); - tb.audio_tempo = Some(api::Bpm::new(tempo.get()).unwrap()) - } - } - - pub fn notify_midi_overdub_finished(&mut self, outcome: MidiOverdubOutcome) { - self.source = create_api_source_from_recorded_midi_sequence(&outcome.midi_sequence); - } - - pub fn set_source(&mut self, source: api::Source) { - self.source = source; - } - - pub fn api_source(&self) -> &api::Source { - &self.source - } - - pub fn peak_file_contents( - &self, - permanent_project: Option, - ) -> ClipEngineResult>> + 'static> { - let media_file_path = self - .absolute_media_file_path(permanent_project) - .ok_or("no audio clip or path not found")?; - let future = async move { - let peak_file = get_peak_file_path(&media_file_path); - tracing::debug!("Trying to read peak file {:?}", &peak_file); - match fs::read(&peak_file) { - Ok(content) => Ok(content), - Err(_) => { - // Peak file doesn't exist yet. Build it. - let pcm_source = create_pcm_source_from_media_file(&media_file_path, false)?; - // Begin building - if !pcm_source.as_raw().peaks_build_begin() { - // REAPER sometimes seems to have some peaks cached in memory or so? Then - // it refuses to begin peak building. Make sure all peaks are cleared and - // try starting another build. - pcm_source.as_raw().peaks_clear(true); - if !pcm_source.as_raw().peaks_build_begin() { - return Err( - "peaks not available and source says that building not necessary", - ); - } - } - // Do the actual building - tracing::debug!("Building peak file"); - while pcm_source.as_raw().peaks_build_run() { - base::future_util::millis(10).await; - } - // Finish building - pcm_source.as_raw().peaks_build_finish(); - // We need to query the peak file path again. It's weird but it can be a - // different one now! - let peak_file = get_peak_file_path(&media_file_path); - fs::read(peak_file).map_err(|_| "peaks built but couldn't be found") - } - } - }; - Ok(future) - } - - fn absolute_media_file_path(&self, permanent_project: Option) -> Option { - let api::Source::File(s) = &self.source else { - return None; - }; - make_media_file_path_absolute(permanent_project, &s.path).ok() - } - - pub fn create_pcm_source( - &self, - temporary_project: Option, - ) -> ClipEngineResult { - create_pcm_source_from_api_source(&self.source, temporary_project) - } - - pub(crate) fn create_real_time_clip( - &mut self, - equipment: CreateRtClipEquipment, - ) -> Result> { - let api_source = match self.active_source { - SourceOrigin::Normal => &self.source, - SourceOrigin::Frozen => self - .frozen_source - .as_ref() - .ok_or("no frozen source given")?, - }; - let clip_source = if let api::Source::MidiChunk(s) = &self.source { - let midi_sequence = MidiSequence::parse_from_reaper_midi_chunk(&s.chunk)?; - RtClipSource::Midi(midi_sequence) - } else { - let pcm_source = - create_pcm_source_from_api_source(api_source, equipment.permanent_project)?; - RtClipSource::Reaper(pcm_source) - }; - let rt_clip = rt::RtClip::ready( - self.rt_id, - clip_source, - equipment.matrix_settings, - equipment.column_settings, - self.rt_settings, - equipment.permanent_project, - equipment.chain_equipment, - equipment.recorder_request_sender, - )?; - Ok(rt_clip) - } - - pub fn looped(&self) -> bool { - self.rt_settings.looped - } - - pub fn set_looped(&mut self, looped: bool) { - self.rt_settings.looped = looped; - } - - pub fn set_volume(&mut self, volume: Db) { - self.rt_settings.volume = volume; - } - - pub fn set_name(&mut self, name: Option) { - self.name = name; - } - - pub fn id(&self) -> &ClipId { - &self.id - } - - pub fn rt_id(&self) -> RtClipId { - self.rt_id - } - - pub fn volume(&self) -> Db { - self.rt_settings.volume - } - - pub fn tempo_factor(&self, timeline_tempo: Bpm, is_midi: bool) -> f64 { - if let Some(tempo) = self.tempo(is_midi) { - calc_tempo_factor(tempo, timeline_tempo) - } else { - 1.0 - } - } - - pub fn time_base(&self) -> &ClipTimeBase { - &self.rt_settings.time_base - } - - pub fn section(&self) -> Section { - self.rt_settings.section - } - - pub fn set_section(&mut self, section: Section) { - self.rt_settings.section = section; - } - - /// Returns `None` if time base is not "Beat". - fn tempo(&self, is_midi: bool) -> Option { - determine_tempo_from_time_base(&self.rt_settings.time_base, is_midi) - } -} - -pub fn create_api_source_from_recorded_midi_sequence(midi_sequence: &MidiSequence) -> api::Source { - api::Source::MidiChunk(MidiChunkSource { - chunk: midi_sequence.format_as_reaper_midi_chunk(), - }) -} - -fn get_peak_file_path(media_file_path: &Path) -> PathBuf { - Reaper::get().medium_reaper().get_peak_file_name_ex_2( - media_file_path, - 2000, - PeakFileMode::Read, - ".reapeaks", - ) -} - -#[derive(Copy, Clone)] -pub struct CreateRtClipEquipment<'a> { - pub chain_equipment: &'a ChainEquipment, - pub recorder_request_sender: &'a Sender, - pub matrix_settings: &'a OverridableMatrixSettings, - pub permanent_project: Option, - pub column_settings: &'a rt::RtColumnSettings, -} diff --git a/playtime-clip-engine/src/base/clip_edit_session.rs b/playtime-clip-engine/src/base/clip_edit_session.rs deleted file mode 100644 index 7701e08ca..000000000 --- a/playtime-clip-engine/src/base/clip_edit_session.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::base::clip_manifestation::ClipOnTrackManifestation; - -use reaper_medium::Hwnd; - -use swell_ui::Window; - -#[derive(Clone, Debug)] -pub enum ClipEditSession { - Audio(AudioClipEditSession), - Midi(MidiClipEditSession), -} - -#[derive(Clone, Debug)] -pub struct AudioClipEditSession { - pub clip_manifestation: ClipOnTrackManifestation, -} - -#[derive(Clone, Debug)] -pub struct MidiClipEditSession { - clip_manifestation: ClipOnTrackManifestation, - midi_editor_window: Window, - previous_content_hash: Option, -} - -impl MidiClipEditSession { - pub fn new(clip_manifestation: ClipOnTrackManifestation, midi_editor_window: Hwnd) -> Self { - Self { - midi_editor_window: Window::from_non_null(midi_editor_window), - previous_content_hash: clip_manifestation.source_hash().ok(), - clip_manifestation, - } - } - - pub fn midi_editor_window(&self) -> Window { - self.midi_editor_window - } - - pub fn clip_manifestation(&self) -> &ClipOnTrackManifestation { - &self.clip_manifestation - } - - /// Returns `true` if hash has changed. - pub fn update_source_hash(&mut self) -> bool { - let new_hash = self.clip_manifestation.source_hash().ok(); - if new_hash != self.previous_content_hash { - self.previous_content_hash = new_hash; - true - } else { - false - } - } -} diff --git a/playtime-clip-engine/src/base/clip_manifestation.rs b/playtime-clip-engine/src/base/clip_manifestation.rs deleted file mode 100644 index 15f528e4e..000000000 --- a/playtime-clip-engine/src/base/clip_manifestation.rs +++ /dev/null @@ -1,111 +0,0 @@ -use crate::base::{Clip, OnlineData}; -use crate::{clip_timeline, ClipEngineResult, QuantizedPosition, Timeline}; -use playtime_api::persistence::ClipTimeBase; -use reaper_high::{Item, OwnedSource, Reaper, ReaperSource, Take, Track}; -use reaper_medium::{Bpm, DurationInSeconds, PositionInSeconds, UiRefreshBehavior}; - -pub fn manifest_clip_on_track( - clip: &Clip, - online_data: &OnlineData, - track: &Track, -) -> ClipEngineResult { - let temporary_project = track.project(); - // TODO-medium Make sure time-based MIDI clips are treated correctly (pretty rare). - let item = track.add_item().map_err(|e| e.message())?; - let timeline = clip_timeline(Some(track.project()), true); - // We must put the item exactly how we would play it so the grid is correct (important - // for MIDI editor). - let item_length = online_data.effective_length_in_seconds(clip, &timeline)?; - let section_start_pos = DurationInSeconds::new(clip.section().start_pos.get()); - let (item_pos, take_offset, tempo) = match clip.time_base() { - // Place section start exactly on start of project. - ClipTimeBase::Time => ( - PositionInSeconds::ZERO, - PositionInSeconds::from(section_start_pos), - None, - ), - ClipTimeBase::Beat(t) => { - // Place downbeat exactly on start of 2nd bar of project. - let second_bar_pos = timeline.pos_of_quantized_pos(QuantizedPosition::bar(1)); - let bpm = timeline.tempo_at(second_bar_pos); - let bps = bpm.get() / 60.0; - let downbeat_pos = t.downbeat.get() / bps; - ( - second_bar_pos - downbeat_pos, - PositionInSeconds::from(section_start_pos), - Some(bpm), - ) - } - }; - // TODO-high Implement "Open in REAPER MIDI editor" again by creating a PCM source ad-hoc - // let source = if let Some(s) = online_data.pooled_midi_source.as_ref() { - // Reaper::get().with_pref_pool_midi_when_duplicating(true, || s.clone()) - // } else - let source = clip.create_pcm_source(Some(temporary_project))?; - if online_data.runtime_data.material_info.is_midi() { - // Because we set a constant preview tempo for our MIDI sources (which is - // important for our internal processing), IGNTEMPO is set to 1, which means the source - // is considered as time-based by REAPER. That makes it appear incorrect in the MIDI - // editor because in reality they are beat-based. The following sets IGNTEMPO to 0 - // for recent REAPER versions. Hopefully this is then only valid for this particular - // pooled copy. - // TODO-low This problem might disappear though as soon as we can use - // "Source beats" MIDI editor time base (which we can't use at the moment because we rely - // on sections). - let _ = source.reaper_source().ext_set_preview_tempo(None); - } - let take = item.add_take().map_err(|e| e.message())?; - let source = OwnedSource::new(source.into_reaper_source()); - take.set_source(source); - take.set_start_offset(take_offset).unwrap(); - item.set_position(item_pos, UiRefreshBehavior::NoRefresh) - .unwrap(); - item.set_length(item_length, UiRefreshBehavior::NoRefresh) - .unwrap(); - let manifestation = ClipOnTrackManifestation { - track: track.clone(), - item, - take, - tempo, - }; - Ok(manifestation) -} - -#[derive(Clone, Debug)] -pub struct ClipOnTrackManifestation { - pub track: Track, - pub item: Item, - pub take: Take, - /// Always set if beat-based. - pub tempo: Option, -} - -impl ClipOnTrackManifestation { - pub fn source(&self) -> ClipEngineResult { - if !self.item.is_available() { - return Err("item not available anymore"); - } - self.take.source().ok_or("take lost source") - } - - pub fn source_hash(&self) -> ClipEngineResult { - let source = self.source()?; - let hash = source.as_raw().ext_get_midi_data_hash()?; - Ok(hash) - } - - pub fn state_chunk(&self) -> ClipEngineResult { - Ok(self.source()?.state_chunk()) - } -} - -impl Drop for ClipOnTrackManifestation { - fn drop(&mut self) { - let _ = unsafe { - Reaper::get() - .medium_reaper() - .delete_track_media_item(self.track.raw(), self.item.raw()) - }; - Reaper::get().medium_reaper().update_timeline(); - } -} diff --git a/playtime-clip-engine/src/base/column.rs b/playtime-clip-engine/src/base/column.rs deleted file mode 100644 index 6dc776d9b..000000000 --- a/playtime-clip-engine/src/base/column.rs +++ /dev/null @@ -1,995 +0,0 @@ -use crate::base::{ - Clip, ClipMatrixHandler, CreateRtClipEquipment, EssentialSlotRecordClipArgs, IdMode, - MatrixSettings, RelevantContent, Slot, -}; -use crate::rt::supplier::{ChainEquipment, RecorderRequest}; -use crate::rt::{ - ClipChangeEvent, ColumnCommandSender, ColumnHandle, ColumnLoadArgs, ColumnMoveSlotContentsArgs, - ColumnPlayRowArgs, ColumnPlaySlotArgs, ColumnReorderSlotsArgs, ColumnStopArgs, - ColumnStopSlotArgs, FillSlotMode, OverridableMatrixSettings, RtColumnEvent, RtColumnSettings, - RtSlotId, RtSlots, SharedRtColumn, SlotChangeEvent, -}; -use crate::{rt, source_util, ClipEngineResult}; -use crossbeam_channel::{Receiver, Sender}; -use either::Either; -use enumflags2::BitFlags; -use helgoboss_learn::UnitValue; -use indexmap::IndexMap; -use playtime_api::persistence as api; -use playtime_api::persistence::{ - preferred_clip_midi_settings, BeatTimeBase, ClipAudioSettings, ClipColor, ClipTimeBase, - ColumnClipPlayAudioSettings, ColumnClipPlaySettings, ColumnClipRecordSettings, ColumnId, - ColumnPlayMode, Db, MatrixClipRecordSettings, PositiveBeat, PositiveSecond, Section, SlotId, - TimeSignature, TrackId, -}; -use reaper_high::{Guid, OrCurrentProject, Project, Reaper, Track}; -use reaper_low::raw::preview_register_t; -use reaper_medium::{ - create_custom_owned_pcm_source, Bpm, CustomPcmSource, FlexibleOwnedPcmSource, HelpMode, - MeasureAlignment, OwnedPreviewRegister, ReaperMutex, ReaperVolumeValue, -}; -use std::collections::HashMap; -use std::iter; -use std::ptr::NonNull; -use std::sync::Arc; -use xxhash_rust::xxh3::Xxh3Builder; - -pub type SharedRegister = Arc>; - -#[derive(Clone, Debug)] -pub struct Column { - id: ColumnId, - name: Option, - settings: ColumnSettings, - rt_settings: rt::RtColumnSettings, - rt_command_sender: ColumnCommandSender, - rt_column: SharedRtColumn, - preview_register: Option, - slots: Slots, - event_receiver: Receiver, - project: Option, -} - -type Slots = IndexMap; - -#[derive(Clone, Debug, Default)] -pub struct ColumnSettings { - pub clip_record_settings: ColumnClipRecordSettings, -} - -impl ColumnSettings { - pub fn from_api(api_column: &api::Column) -> Self { - Self { - clip_record_settings: api_column.clip_record_settings.clone(), - } - } -} - -#[derive(Clone, Debug)] -struct PlayingPreviewRegister { - _preview_register: SharedRegister, - play_handle: NonNull, - track: Track, -} - -impl Column { - /// Creates a column with default settings, all slots empty and without a track. - /// - /// In order to make the column fully functional, a playback track must be set at a later stage. - pub fn new( - id: ColumnId, - permanent_project: Option, - initial_slot_count: usize, - ) -> Self { - let (command_sender, command_receiver) = crossbeam_channel::bounded(500); - let (event_sender, event_receiver) = crossbeam_channel::bounded(500); - let source = rt::RtColumn::new(permanent_project, command_receiver, event_sender); - let shared_source = SharedRtColumn::new(source); - Self { - id, - name: None, - settings: Default::default(), - rt_settings: Default::default(), - preview_register: None, - rt_column: shared_source, - rt_command_sender: ColumnCommandSender::new(command_sender), - slots: (0..initial_slot_count) - .map(|i| { - let slot = Slot::new(SlotId::random(), i); - (slot.rt_id(), slot) - }) - .collect(), - event_receiver, - project: permanent_project, - } - } - - pub fn id(&self) -> &ColumnId { - &self.id - } - - pub fn all_column_settings_combined(&self) -> api::ColumnSettings { - api::ColumnSettings { - clip_play_settings: self.save_play_settings(), - clip_record_settings: self.settings.clip_record_settings.clone(), - } - } - - pub fn set_settings(&mut self, settings: api::ColumnSettings) { - self.settings.clip_record_settings = settings.clip_record_settings; - self.rt_settings = RtColumnSettings::from_api(&settings.clip_play_settings); - self.sync_column_settings_to_rt_column(); - } - - pub fn duplicate(&self, rt_equipment: ColumnRtEquipment) -> Column { - let mut duplicate = Self::new(ColumnId::random(), self.project, self.slots.len()); - duplicate.name = self.name.clone(); - duplicate.settings = self.settings.clone(); - duplicate.rt_settings = self.rt_settings.clone(); - duplicate.set_playback_track_internal(self.playback_track().ok().cloned()); - duplicate.slots = self - .slots - .values() - .map(|slot| { - let duplicate_slot = slot.duplicate(slot.index()); - (duplicate_slot.rt_id(), duplicate_slot) - }) - .collect(); - duplicate.sync_everything_to_rt_column(rt_equipment); - duplicate - } - - pub(crate) fn start_editing_clip( - &mut self, - slot_index: usize, - clip_index: usize, - ) -> ClipEngineResult<()> { - let playback_track = self.playback_track()?.clone(); - get_slot_mut(&mut self.slots, slot_index)?.start_editing_clip(clip_index, &playback_track) - } - - pub(crate) fn stop_editing_clip( - &mut self, - slot_index: usize, - clip_index: usize, - ) -> ClipEngineResult<()> { - get_slot_mut(&mut self.slots, slot_index)?.stop_editing_clip(clip_index) - } - - pub(crate) fn set_clip_data( - &mut self, - slot_index: usize, - clip_index: usize, - api_clip: api::Clip, - ) -> ClipEngineResult<()> { - let slot = get_slot_mut(&mut self.slots, slot_index)?; - slot.set_clip_data(clip_index, api_clip, &self.rt_command_sender)?; - Ok(()) - } - - pub fn move_slot_contents( - &mut self, - source_index: usize, - dest_index: usize, - ) -> ClipEngineResult<()> { - if source_index >= self.slots.len() { - return Err("source index out of bounds"); - } - if dest_index >= self.slots.len() { - return Err("destination index out of bounds"); - } - if source_index == dest_index { - return Ok(()); - } - // This will get more complicated in future as soon as we support moving on non-empty slots. - self.slots.swap_indices(source_index, dest_index); - self.reindex_slots(); - self.rt_command_sender - .move_slot_contents(ColumnMoveSlotContentsArgs { - source_index, - dest_index, - }); - Ok(()) - } - - pub(crate) fn reorder_slots( - &mut self, - source_index: usize, - dest_index: usize, - ) -> ClipEngineResult<()> { - if source_index >= self.slots.len() { - return Err("source slot doesn't exist"); - } - if dest_index >= self.slots.len() { - return Err("destination slot doesn't exist"); - } - self.slots.move_index(source_index, dest_index); - self.reindex_slots(); - self.rt_command_sender - .reorder_slots(ColumnReorderSlotsArgs { - source_index, - dest_index, - }); - Ok(()) - } - - pub fn set_play_mode(&mut self, play_mode: ColumnPlayMode) { - self.rt_settings.play_mode = play_mode; - } - - /// Returns the sender for sending commands to the corresponding real-time column. - pub fn rt_command_sender(&self) -> &ColumnCommandSender { - &self.rt_command_sender - } - - fn resolve_track_from_id(&self, track_id: &TrackId) -> ClipEngineResult { - let guid = Guid::from_string_without_braces(track_id.get())?; - let track = self.project.or_current_project().track_by_guid(&guid)?; - if !track.is_available() { - return Err("track not available"); - } - Ok(track) - } - - pub fn load( - &mut self, - api_column: api::Column, - necessary_row_count: usize, - rt_equipment: ColumnRtEquipment, - ) -> ClipEngineResult<()> { - // Track - let track = api_column - .clip_play_settings - .track - .as_ref() - .and_then(|id| self.resolve_track_from_id(id).ok()); - self.set_playback_track_internal(track); - // Settings - self.settings = ColumnSettings::from_api(&api_column); - self.rt_settings = rt::RtColumnSettings::from_api(&api_column.clip_play_settings); - self.name = api_column.name; - // Create slots for all rows - let api_slots = api_column.slots.unwrap_or_default(); - let mut api_slots_map: HashMap<_, _> = api_slots - .into_iter() - .map(|api_slot| (api_slot.row, api_slot)) - .collect(); - self.slots = (0..necessary_row_count) - .map(|row_index| { - let slot = if let Some(api_slot) = api_slots_map.remove(&row_index) { - let mut slot = Slot::new(api_slot.id.clone(), row_index); - slot.load(api_slot.into_clips()); - slot - } else { - Slot::new(SlotId::random(), row_index) - }; - (slot.rt_id(), slot) - }) - .collect(); - // Sync to rt - self.sync_everything_to_rt_column(rt_equipment); - Ok(()) - } - - /// Resynchronizes all slots to the real-time column. - /// - /// Although this recreates all PCM sources and sends them to the real-time column, the - /// real-time column should take core to not do more than necessary and ensure a smooth - /// transition into the new state. This is the code that's also used for undo/redo (restoring - /// history states). - /// - /// So this can also be used for small changed when too lazy to create a real-time column - /// command. It's a bit heavier on resources though, so it shouldn't be used for column changes - /// that can happen very frequently. - fn sync_slots_to_rt_column(&mut self, rt_equipment: ColumnRtEquipment) { - let create_rt_clip_equipment = CreateRtClipEquipment { - permanent_project: self.project, - chain_equipment: rt_equipment.chain_equipment, - recorder_request_sender: rt_equipment.recorder_request_sender, - matrix_settings: &rt_equipment.matrix_settings.overridable, - column_settings: &self.rt_settings, - }; - let rt_slots: RtSlots = self - .slots - .values_mut() - .map(|slot| { - let rt_slot = slot.bring_online(create_rt_clip_equipment); - (rt_slot.id(), rt_slot) - }) - .collect(); - self.rt_command_sender.load(ColumnLoadArgs { - new_slots: rt_slots, - }); - } - - /// Sets the playback track, recreating the preview register if necessary and auto-naming - /// the column according to the track name. - /// - /// Also resyncs everything to the real-time column in case no track was assigned before. - pub fn set_playback_track(&mut self, track: Option, rt_equipment: ColumnRtEquipment) { - // Update name - if let Some(t) = &track { - self.name = t.name().map(|n| n.into_string()); - } - // Set track - let was_online_before = self.preview_register.is_some(); - self.set_playback_track_internal(track); - let is_online_now = self.preview_register.is_some(); - // Sync if necessary - if !was_online_before && is_online_now { - self.sync_everything_to_rt_column(rt_equipment); - } - } - - /// Sets the playback track, recreating the preview register if necessary. - fn set_playback_track_internal(&mut self, track: Option) { - // Check if recreation of preview register is necessary. Unnecessary recreation would - // interrupt playing. - if let (Some(existing_register), Some(track)) = (&self.preview_register, track.as_ref()) { - if &existing_register.track == track { - // No need to recreate. Column already uses a preview register for that track. - return; - } - } - // Update preview register - self.preview_register = - track.map(|t| PlayingPreviewRegister::new(self.rt_column.clone(), t)); - // Enable/disable the sender as necessary - self.rt_command_sender - .set_enabled(self.preview_register.is_some()); - } - - fn sync_everything_to_rt_column(&mut self, rt_equipment: ColumnRtEquipment) { - self.sync_matrix_settings_to_rt_column(rt_equipment.matrix_settings); - self.sync_column_settings_to_rt_column(); - self.sync_slots_to_rt_column(rt_equipment); - } - - fn sync_column_settings_to_rt_column(&self) { - self.rt_command_sender - .update_settings(self.rt_settings.clone()); - } - - pub fn sync_matrix_settings_to_rt_column(&self, matrix_settings: &MatrixSettings) { - self.rt_command_sender - .update_matrix_settings(matrix_settings.overridable.clone()); - } - - /// Returns all clips that are currently playing (along with slot index) . - pub(crate) fn playing_clips(&self) -> impl Iterator + '_ { - // TODO-high-clip-engine This is used for building a scene from the currently playing clips. - // If multiple clips are currently playing in one column, we shouldn't add new columns - // but put the clips into one slot! This is a new possibility and this is a good use case! - self.slots.values().enumerate().flat_map(|(i, s)| { - let is_playing = s.play_state().is_as_good_as_playing(); - if is_playing { - Either::Left(s.clips().map(move |c| (i, c))) - } else { - Either::Right(iter::empty()) - } - }) - } - - pub fn clear_slots(&mut self) { - self.slots.clear(); - self.rt_command_sender.clear_slots(); - } - - pub fn get_slot(&self, index: usize) -> ClipEngineResult<&Slot> { - Ok(self.slots.get_index(index).ok_or(SLOT_DOESNT_EXIST)?.1) - } - - pub(crate) fn get_slot_mut(&mut self, index: usize) -> ClipEngineResult<&mut Slot> { - get_slot_mut(&mut self.slots, index) - } - - pub(crate) fn get_slot_kit_mut(&mut self, index: usize) -> ClipEngineResult { - let kit = SlotKit { - sender: &self.rt_command_sender, - slot: get_slot_mut(&mut self.slots, index)?, - }; - Ok(kit) - } - - /// Returns whether the slot at the given index is empty. - pub(crate) fn slot_is_empty(&self, index: usize) -> bool { - match self.slots.get_index(index) { - None => true, - Some((_, s)) => s.is_empty(), - } - } - - pub fn save(&self) -> api::Column { - api::Column { - id: self.id.clone(), - name: self.name.clone(), - clip_play_settings: self.save_play_settings(), - clip_record_settings: self.settings.clip_record_settings.clone(), - slots: { - let slots = self.slots.values().filter_map(|slot| slot.save()).collect(); - Some(slots) - }, - } - } - - fn save_play_settings(&self) -> api::ColumnClipPlaySettings { - let track_id = self - .playback_track() - .ok() - .map(|t| TrackId::new(t.guid().to_string_without_braces())); - ColumnClipPlaySettings { - mode: Some(self.rt_settings.play_mode), - track: track_id, - start_timing: self.rt_settings.clip_play_start_timing, - stop_timing: self.rt_settings.clip_play_stop_timing, - audio_settings: ColumnClipPlayAudioSettings { - resample_mode: self.rt_settings.audio_resample_mode, - time_stretch_mode: self.rt_settings.audio_time_stretch_mode, - cache_behavior: self.rt_settings.audio_cache_behavior, - }, - } - } - - pub fn create_handle(&self) -> ColumnHandle { - ColumnHandle { - pointer: self.rt_column.downgrade(), - command_sender: self.rt_command_sender.clone(), - } - } - - pub fn poll(&mut self, timeline_tempo: Bpm) -> Vec<(usize, SlotChangeEvent)> { - // Process source events and generate clip change events - let mut change_events = vec![]; - while let Ok(evt) = self.event_receiver.try_recv() { - use RtColumnEvent::*; - let change_event = match evt { - SlotPlayStateChanged { - slot_id, - play_state, - } => { - if let Some(slot) = self.slots.get_mut(&slot_id) { - slot.update_play_state(play_state); - Some((slot.index(), SlotChangeEvent::PlayState(play_state))) - } else { - None - } - } - ClipMaterialInfoChanged { - slot_id, - clip_id, - material_info, - } => { - if let Some(slot) = self.slots.get_mut(&slot_id) { - let _ = slot.update_material_info(clip_id, material_info); - } - None - } - Dispose(_) => None, - RecordRequestAcknowledged { - slot_id, result, .. - } => { - if let Some(slot) = self.slots.get_mut(&slot_id) { - slot.notify_recording_request_acknowledged(result).unwrap(); - } - None - } - MidiOverdubFinished { - slot_id, - clip_id, - outcome, - } => { - if let Some(slot) = self.slots.get_mut(&slot_id) { - if slot.notify_midi_overdub_finished(clip_id, outcome).is_ok() { - Some(( - slot.index(), - SlotChangeEvent::Clips("MIDI overdub finished"), - )) - } else { - None - } - } else { - None - } - } - NormalRecordingFinished { slot_id, outcome } => { - let recording_track = &self.effective_recording_track().unwrap(); - if let Some(slot) = self.slots.get_mut(&slot_id) { - let event = slot - .notify_normal_recording_finished( - outcome, - self.project, - recording_track, - ) - .unwrap(); - Some((slot.index(), event)) - } else { - None - } - } - InteractionFailed(failure) => { - let formatted = format!("Playtime: Interaction failed ({})", failure.message); - Reaper::get() - .medium_reaper() - .help_set(formatted, HelpMode::Temporary); - None - } - SlotCleared { slot_id, .. } => { - if let Some(slot) = self.slots.get_mut(&slot_id) { - slot.slot_cleared().map(|e| (slot.index(), e)) - } else { - None - } - } - }; - if let Some(evt) = change_event { - change_events.push(evt); - } - } - // Add position updates - let continuous_clip_events = self.slots.values().enumerate().flat_map(|(row, slot)| { - if !slot.play_state().is_advancing() { - return Either::Right(iter::empty()); - } - let _temp_project = self.project.or_current_project(); - let iter = match slot.relevant_contents() { - RelevantContent::Normal(contents) => { - let iter = contents.filter_map(move |content| { - let online_data = content.online_data.as_ref()?; - let seconds = - online_data.position_in_seconds(&content.clip, timeline_tempo); - let event = SlotChangeEvent::Continuous { - proportional: online_data.proportional_position().unwrap_or_default(), - seconds, - peak: online_data.peak(), - }; - let _ = content.notify_pos_changed(timeline_tempo, seconds); - Some((row, event)) - }); - Either::Left(iter) - } - RelevantContent::Recording(runtime_data) => { - let event = SlotChangeEvent::Continuous { - proportional: runtime_data.proportional_position().unwrap_or_default(), - seconds: runtime_data.position_in_seconds_during_recording(timeline_tempo), - peak: runtime_data.peak(), - }; - Either::Right(iter::once((row, event))) - } - }; - Either::Left(iter) - }); - change_events.extend(continuous_clip_events); - change_events - } - - /// Clears the given slot. - /// - /// # Errors - /// - /// Returns an error if the slot doesn't exist. - pub fn clear_slot(&mut self, slot_index: usize) -> ClipEngineResult<()> { - self.get_slot_mut(slot_index)?.clear(); - self.rt_command_sender.clear_slot(slot_index); - Ok(()) - } - - /// Freezes the complete column. - pub async fn freeze(&mut self, _column_index: usize) -> ClipEngineResult<()> { - let playback_track = self.playback_track()?.clone(); - for slot in self.slots.values_mut() { - // TODO-high-clip-matrix implement - let _ = slot.freeze(&playback_track).await; - } - Ok(()) - } - - pub fn apply_edited_contents_if_necessary( - &mut self, - chain_equipment: &ChainEquipment, - recorder_request_sender: &Sender, - matrix_settings: &OverridableMatrixSettings, - ) { - for slot in self.slots.values_mut() { - let equipment = CreateRtClipEquipment { - permanent_project: self.project, - chain_equipment, - recorder_request_sender, - matrix_settings, - column_settings: &self.rt_settings, - }; - slot.apply_edited_contents_if_necessary(equipment, &self.rt_command_sender) - } - } - - /// Adds the given clips to the slot or replaces all existing ones. - /// - /// Immediately syncs to real-time column. - #[allow(clippy::too_many_arguments)] - pub(crate) fn fill_slot( - &mut self, - slot_index: usize, - api_clips: Vec, - chain_equipment: &ChainEquipment, - recorder_request_sender: &Sender, - matrix_settings: &MatrixSettings, - fill_mode: FillSlotMode, - id_mode: IdMode, - ) -> ClipEngineResult<()> { - let slot = get_slot_mut(&mut self.slots, slot_index)?; - let equipment = CreateRtClipEquipment { - permanent_project: self.project, - chain_equipment, - recorder_request_sender, - matrix_settings: &matrix_settings.overridable, - column_settings: &self.rt_settings, - }; - slot.fill( - api_clips, - equipment, - &self.rt_command_sender, - fill_mode, - id_mode, - ); - Ok(()) - } - - pub(crate) fn replace_slot_clips_with_selected_item( - &mut self, - slot_index: usize, - chain_equipment: &ChainEquipment, - recorder_request_sender: &Sender, - matrix_settings: &MatrixSettings, - ) -> ClipEngineResult<()> { - let project = self.project.or_current_project(); - let item = project.first_selected_item().ok_or("no item selected")?; - let source = source_util::create_api_source_from_item(item, false) - .map_err(|_| "couldn't create source from item")?; - let clip = api::Clip { - id: Default::default(), - name: None, - source, - frozen_source: None, - active_source: Default::default(), - // TODO-high-clip-engine Derive whether time or beat from item/track/project - time_base: ClipTimeBase::Beat(BeatTimeBase { - // TODO-high-clip-engine Correctly determine audio tempo, only if audio - audio_tempo: Some(api::Bpm::new(project.tempo().bpm().get())?), - // TODO-high-clip-engine Correctly determine time signature at item position - time_signature: TimeSignature { - numerator: 4, - denominator: 4, - }, - // TODO-high-clip-engine Correctly determine by looking at snap offset - downbeat: PositiveBeat::default(), - }), - start_timing: None, - stop_timing: None, - // TODO-high-clip-engine Check if item itself is looped or not - looped: true, - // TODO-high-clip-engine Derive from item take volume - volume: api::Db::ZERO, - // TODO-high-clip-engine Derive from item color - color: ClipColor::PlayTrackColor, - // TODO-high-clip-engine Derive from item cut - section: Section { - start_pos: PositiveSecond::default(), - length: None, - }, - audio_settings: ClipAudioSettings { - apply_source_fades: true, - // TODO-high-clip-engine Derive from item time stretch mode - time_stretch_mode: None, - // TODO-high-clip-engine Derive from item resample mode - resample_mode: None, - cache_behavior: None, - }, - midi_settings: preferred_clip_midi_settings(), - }; - self.fill_slot( - slot_index, - vec![clip], - chain_equipment, - recorder_request_sender, - matrix_settings, - FillSlotMode::Replace, - IdMode::KeepIds, - ) - } - - pub(crate) fn play_scene(&self, args: ColumnPlayRowArgs) { - self.rt_command_sender.play_row(args); - } - - pub(crate) fn play_slot(&self, args: ColumnPlaySlotArgs) { - self.rt_command_sender.play_slot(args); - } - - pub(crate) fn stop_slot(&self, args: ColumnStopSlotArgs) { - self.rt_command_sender.stop_slot(args); - } - - pub(crate) fn duplicate_slot( - &mut self, - slot_index: usize, - rt_equipment: ColumnRtEquipment, - ) -> ClipEngineResult<()> { - if slot_index >= self.slots.len() { - return Err("slot to be removed doesn't exist"); - } - let (_, slot) = self - .slots - .get_index(slot_index) - .ok_or("slot doesn't exist")?; - let duplicate_slot = slot.duplicate(slot_index + 1); - let new_index = duplicate_slot.index(); - self.slots.insert(duplicate_slot.rt_id(), duplicate_slot); - self.slots.move_index(self.slots.len() - 1, new_index); - self.reindex_slots(); - self.sync_slots_to_rt_column(rt_equipment); - Ok(()) - } - - pub(crate) fn insert_slot( - &mut self, - slot_index: usize, - rt_equipment: ColumnRtEquipment, - ) -> ClipEngineResult<()> { - if slot_index > self.slots.len() { - return Err("slot index too large"); - } - let new_slot = Slot::new(SlotId::random(), slot_index); - self.slots.insert(new_slot.rt_id(), new_slot); - self.slots.move_index(self.slots.len() - 1, slot_index); - self.reindex_slots(); - self.sync_slots_to_rt_column(rt_equipment); - Ok(()) - } - - pub(crate) fn remove_slot(&mut self, slot_index: usize) -> ClipEngineResult<()> { - if slot_index >= self.slots.len() { - return Err("slot to be removed doesn't exist"); - } - self.slots.shift_remove_index(slot_index); - self.reindex_slots(); - self.rt_command_sender.remove_slot(slot_index); - Ok(()) - } - - fn reindex_slots(&mut self) { - for (i, slot) in self.slots.values_mut().enumerate() { - slot.set_index(i); - } - } - - pub(crate) fn stop(&self, args: ColumnStopArgs) { - self.rt_command_sender.stop(args); - } - - pub fn panic(&self) { - self.rt_command_sender.panic(); - } - - pub fn panic_slot(&self, slot_index: usize) { - self.rt_command_sender.panic_slot(slot_index); - } - - pub(crate) fn pause_slot(&self, slot_index: usize) { - self.rt_command_sender.pause_slot(slot_index); - } - - pub(crate) fn seek_slot(&self, slot_index: usize, desired_pos: UnitValue) { - self.rt_command_sender.seek_slot(slot_index, desired_pos); - } - - pub fn set_clip_volume( - &mut self, - slot_index: usize, - volume: Db, - ) -> ClipEngineResult { - let slot = get_slot_mut(&mut self.slots, slot_index)?; - slot.set_volume(volume, &self.rt_command_sender) - } - - pub fn slots(&self) -> impl Iterator + '_ { - self.slots.values() - } - - /// Returns whether some slots in this column are currently playing/recording. - pub fn is_stoppable(&self) -> bool { - self.slots.values().any(|slot| slot.is_stoppable()) - } - - pub fn is_armed_for_recording(&self) -> bool { - self.effective_recording_track() - .map(|t| t.is_armed(true)) - .unwrap_or(false) - } - - pub fn effective_recording_track(&self) -> ClipEngineResult { - let playback_track = self.playback_track()?; - resolve_recording_track(&self.settings.clip_record_settings, playback_track) - } - - /// Sets the playback track of this column. - pub fn set_playback_track_from_id( - &mut self, - track_id: Option<&TrackId>, - rt_equipment: ColumnRtEquipment, - ) -> ClipEngineResult<()> { - let track = if let Some(id) = track_id { - Some(self.resolve_track_from_id(id)?) - } else { - None - }; - self.set_playback_track(track, rt_equipment); - Ok(()) - } - - pub fn set_name(&mut self, name: String) { - self.name = Some(name); - } - - /// Returns the playback track of this column. - pub fn playback_track(&self) -> ClipEngineResult<&Track> { - let preview_register = self.preview_register.as_ref().ok_or("column inactive")?; - Ok(&preview_register.track) - } - - pub fn uses_track(&self, track: &Track) -> bool { - self.playback_track().ok() == Some(track) - } - - pub fn follows_scene(&self) -> bool { - self.rt_settings.play_mode.follows_scene() - } - - pub fn is_recording(&self) -> bool { - self.slots.values().any(|s| s.is_recording()) - } - - pub(crate) fn midi_overdub_clip( - &mut self, - slot_index: usize, - clip_index: usize, - args: EssentialColumnRecordClipArgs, - ) -> ClipEngineResult<()> { - let recording_track = &self.effective_recording_track()?; - let slot = get_slot_mut(&mut self.slots, slot_index)?; - let args = EssentialSlotRecordClipArgs { - column_args: args, - column_record_settings: &self.settings.clip_record_settings, - rt_column_settings: &self.rt_settings, - recording_track, - rt_column: &self.rt_column, - column_command_sender: &self.rt_command_sender, - }; - slot.midi_overdub_clip(clip_index, args) - } - - pub(crate) fn record_slot( - &mut self, - slot_index: usize, - args: EssentialColumnRecordClipArgs, - ) -> ClipEngineResult<()> { - let recording_track = &self.effective_recording_track()?; - // Insert slot if it doesn't exist already. - let slot = get_slot_mut_insert(&mut self.slots, slot_index); - let args = EssentialSlotRecordClipArgs { - column_args: args, - column_record_settings: &self.settings.clip_record_settings, - rt_column_settings: &self.rt_settings, - recording_track, - rt_column: &self.rt_column, - column_command_sender: &self.rt_command_sender, - }; - slot.record_clip(args) - } -} - -impl Drop for PlayingPreviewRegister { - fn drop(&mut self) { - self.stop_playing_preview(); - } -} - -impl PlayingPreviewRegister { - pub fn new(source: impl CustomPcmSource + 'static, track: Track) -> Self { - let mut register = OwnedPreviewRegister::default(); - register.set_volume(ReaperVolumeValue::ZERO_DB); - register.set_out_chan(-1); - register.set_preview_track(Some(track.raw())); - let source = create_custom_owned_pcm_source(source); - register.set_src(Some(FlexibleOwnedPcmSource::Custom(source))); - let preview_register = Arc::new(ReaperMutex::new(register)); - let play_handle = start_playing_preview(&preview_register, &track); - Self { - _preview_register: preview_register, - play_handle, - track, - } - } - - fn stop_playing_preview(&mut self) { - // Check prevents error message on project close. - let project = self.track.project(); - // If not successful this probably means it was stopped already, so okay. - let _ = Reaper::get() - .medium_session() - .stop_track_preview_2(project.context(), self.play_handle); - } -} - -fn start_playing_preview(reg: &SharedRegister, track: &Track) -> NonNull { - debug!("Starting preview on track {:?}", &track); - let buffering_behavior = BitFlags::empty(); - let measure_alignment = MeasureAlignment::PlayImmediately; - Reaper::get() - .medium_session() - .play_track_preview_2_ex( - track.project().context(), - reg.clone(), - buffering_behavior, - measure_alignment, - ) - .expect("starting column preview failed") -} - -fn get_slot_mut(slots: &mut Slots, index: usize) -> ClipEngineResult<&mut Slot> { - Ok(slots.get_index_mut(index).ok_or(SLOT_DOESNT_EXIST)?.1) -} - -fn get_slot_mut_insert(slots: &mut Slots, slot_index: usize) -> &mut Slot { - upsize_if_necessary(slots, slot_index + 1); - slots.get_index_mut(slot_index).unwrap().1 -} - -fn upsize_if_necessary(slots: &mut Slots, row_count: usize) { - let current_row_count = slots.len(); - if current_row_count < row_count { - let missing_rows = (current_row_count..row_count).map(|i| { - let slot = Slot::new(SlotId::random(), i); - (slot.rt_id(), slot) - }); - slots.extend(missing_rows); - } -} - -const SLOT_DOESNT_EXIST: &str = "slot doesn't exist"; - -fn resolve_recording_track( - column_settings: &ColumnClipRecordSettings, - playback_track: &Track, -) -> ClipEngineResult { - if let Some(track_id) = &column_settings.track { - let track_guid = Guid::from_string_without_braces(track_id.get())?; - let track = playback_track.project().track_by_guid(&track_guid)?; - if track.is_available() { - Ok(track) - } else { - Err("track not available") - } - } else { - Ok(playback_track.clone()) - } -} - -pub(crate) struct SlotKit<'a> { - pub slot: &'a mut Slot, - pub sender: &'a ColumnCommandSender, -} - -#[derive(Copy, Clone)] -pub struct ColumnRtEquipment<'a> { - pub chain_equipment: &'a ChainEquipment, - pub recorder_request_sender: &'a Sender, - pub matrix_settings: &'a MatrixSettings, -} - -pub struct EssentialColumnRecordClipArgs<'a> { - pub matrix_record_settings: &'a MatrixClipRecordSettings, - pub chain_equipment: &'a ChainEquipment, - pub recorder_request_sender: &'a Sender, - pub handler: &'a dyn ClipMatrixHandler, - pub containing_track: Option<&'a Track>, - pub overridable_matrix_settings: &'a OverridableMatrixSettings, -} diff --git a/playtime-clip-engine/src/base/history.rs b/playtime-clip-engine/src/base/history.rs deleted file mode 100644 index 37af68ca1..000000000 --- a/playtime-clip-engine/src/base/history.rs +++ /dev/null @@ -1,351 +0,0 @@ -use crate::base::MatrixContent; -use crate::ClipEngineResult; -use itertools::Itertools; -use playtime_api::persistence as api; -use playtime_api::persistence::TrackId; -use reaper_high::{Guid, Project, Reaper, Track}; -use reaper_medium::ReorderTracksBehavior; -use std::collections::BTreeSet; -use std::error::Error; -use std::fmt::Debug; -use std::mem; -use std::time::{Duration, Instant}; - -/// Data structure holding the undo history. -#[derive(Debug)] -pub struct History { - history_entry_buffer: HistoryEntryBuffer, - undo_stack: Vec, - redo_stack: Vec, -} - -#[derive(Debug)] -struct HistoryEntryBuffer { - time_of_last_addition: Instant, - content: HistoryEntryBufferContent, -} - -#[derive(Debug, Default)] -struct HistoryEntryBufferContent { - labels: BTreeSet, - reaper_changes: Vec, -} - -impl HistoryEntryBufferContent { - pub fn summary_label(&self) -> String { - self.labels.iter().join(", ") - } -} - -impl HistoryEntryBuffer { - pub fn add(&mut self, label: String, reaper_changes: Vec) { - self.time_of_last_addition = Instant::now(); - self.content.labels.insert(label); - self.content.reaper_changes.extend(reaper_changes); - } - - pub fn its_flush_time(&self) -> bool { - const DEBOUNCE_DURATION: Duration = Duration::from_millis(1000); - !self.is_empty() && self.time_of_last_addition.elapsed() > DEBOUNCE_DURATION - } - - pub fn is_empty(&self) -> bool { - self.content.labels.is_empty() - } - - pub fn flush(&mut self) -> HistoryEntryBufferContent { - HistoryEntryBufferContent { - labels: mem::take(&mut self.content.labels), - reaper_changes: mem::take(&mut self.content.reaper_changes), - } - } -} - -impl Default for HistoryEntryBuffer { - fn default() -> Self { - Self { - time_of_last_addition: Instant::now(), - content: Default::default(), - } - } -} - -impl History { - pub fn new(initial_matrix: api::Matrix) -> Self { - Self { - history_entry_buffer: Default::default(), - undo_stack: vec![State::new("Initial".to_string(), initial_matrix, vec![])], - redo_stack: vec![], - } - } - - /// Returns the label of the next undoable action if there is one. - pub fn next_undo_label(&self) -> Option<&str> { - if !self.can_undo() { - return None; - } - let state = self.undo_stack.last()?; - Some(&state.label) - } - - /// Returns the label of the next redoable action if there is one. - pub fn next_redo_label(&self) -> Option<&str> { - let state = self.redo_stack.last()?; - Some(&state.label) - } - - /// Returns if undo is possible. - pub fn can_undo(&self) -> bool { - self.undo_stack.len() > 1 - } - - /// Returns if redo is possible. - pub fn can_redo(&self) -> bool { - !self.redo_stack.is_empty() - } - - pub fn its_flush_time(&self) -> bool { - self.history_entry_buffer.its_flush_time() - } - - /// Doesn't add a history event immediately but buffers it until no change has happened for - /// some time. - pub(crate) fn add_buffered(&mut self, label: String, reaper_changes: Vec) { - self.history_entry_buffer.add(label, reaper_changes); - } - - /// Adds the buffered history entries if the matrix is different from the one in the previous - /// undo point. - pub fn flush_buffer(&mut self, new_matrix: api::Matrix) { - let content = self.history_entry_buffer.flush(); - let label = content.summary_label(); - self.add_internal(label, new_matrix, content.reaper_changes); - } - - /// Adds the given history entry if the matrix is different from the one in the previous - /// undo point. - pub(crate) fn add( - &mut self, - label: String, - new_matrix: api::Matrix, - reaper_changes: Vec, - ) { - let label = if self.history_entry_buffer.is_empty() { - label - } else { - let mut content = self.history_entry_buffer.flush(); - content.labels.insert(label); - content.summary_label() - }; - self.add_internal(label, new_matrix, reaper_changes); - } - - fn add_internal( - &mut self, - label: String, - new_matrix: api::Matrix, - reaper_changes: Vec, - ) { - if let Some(prev_state) = self.undo_stack.last() { - if new_matrix == prev_state.matrix { - return; - } - }; - self.redo_stack.clear(); - let new_state = State::new(label, new_matrix, reaper_changes); - self.undo_stack.push(new_state); - } - - /// Marks the last action as undone and returns instructions to restore the previous state. - pub(crate) fn undo(&mut self) -> ClipEngineResult { - if self.undo_stack.len() <= 1 { - return Err("nothing to undo"); - } - // Put current state on the redo stack - let current_state = self.undo_stack.pop().unwrap(); - self.redo_stack.push(current_state); - // Return instruction - let previous_state = self.undo_stack.last().unwrap(); - let current_state = self.redo_stack.last_mut().unwrap(); - let instruction = RestorationInstruction { - // We need to restore the matrix revision of the previous state - matrix: &previous_state.matrix, - // And we need to undo the REAPER changes associated with the *current state* - reaper_changes: &mut current_state.reaper_changes, - }; - Ok(instruction) - } - - /// Marks the last undone action as redone and returns the matrix state to be loaded. - pub(crate) fn redo(&mut self) -> ClipEngineResult { - // Put next state on the undo stack - let next_state = self.redo_stack.pop().ok_or("nothing to redo")?; - self.undo_stack.push(next_state); - // Return instruction - let next_state = self.undo_stack.last_mut().unwrap(); - let instruction = RestorationInstruction { - // We need to restore the matrix revision of the next state - matrix: &next_state.matrix, - // And we need to redo the REAPER changes associated with the *next state* - reaper_changes: &mut next_state.reaper_changes, - }; - Ok(instruction) - } -} - -#[derive(Debug)] -struct State { - pub label: String, - pub matrix: api::Matrix, - /// What needed to be done *within REAPER* in order to arrive at that matrix. - pub reaper_changes: Vec, -} - -pub(crate) struct RestorationInstruction<'a> { - pub matrix: &'a api::Matrix, - pub reaper_changes: &'a mut [BoxedReaperChange], -} - -pub(crate) struct ReaperChangeContext<'a> { - pub matrix: &'a mut MatrixContent, -} - -pub(crate) type BoxedReaperChange = Box; - -pub(crate) trait ReaperChange: Debug { - /// Executed before undoing the clip matrix change. - fn pre_undo(&mut self, context: ReaperChangeContext) -> Result<(), Box> { - let _ = context; - Ok(()) - } - - /// Executed after undoing the clip matrix change. - fn post_undo(&mut self, context: ReaperChangeContext) -> Result<(), Box> { - let _ = context; - Ok(()) - } - - /// Executed before redoing the clip matrix change. - fn pre_redo(&mut self, context: ReaperChangeContext) -> Result<(), Box> { - let _ = context; - Ok(()) - } - - /// Executed after redoing the clip matrix change. - fn post_redo(&mut self, context: ReaperChangeContext) -> Result<(), Box> { - let _ = context; - Ok(()) - } -} - -impl State { - fn new(label: String, matrix: api::Matrix, reaper_changes: Vec) -> Self { - Self { - label, - matrix, - reaper_changes, - } - } -} - -#[derive(Clone, Debug)] -pub struct TrackAdditionReaperChange { - project: Project, - guid: Guid, - index: u32, - associated_column_index: usize, -} - -impl TrackAdditionReaperChange { - pub fn new(track: &Track, associated_column_index: usize) -> Self { - Self { - project: track.project(), - guid: *track.guid(), - index: track.index().unwrap(), - associated_column_index, - } - } -} - -impl ReaperChange for TrackAdditionReaperChange { - fn post_undo(&mut self, _: ReaperChangeContext) -> Result<(), Box> { - let track = self.project.track_by_guid(&self.guid)?; - if !track.is_available() { - // Track doesn't exist anymore, so no need to remove it - return Ok(()); - } - self.project.remove_track(&track); - Ok(()) - } - - fn pre_redo(&mut self, _: ReaperChangeContext) -> Result<(), Box> { - let track = self.project.insert_track_at(self.index)?; - self.guid = *track.guid(); - Ok(()) - } - - fn post_redo(&mut self, context: ReaperChangeContext) -> Result<(), Box> { - let id = TrackId::new(self.guid.to_string_without_braces()); - context - .matrix - .set_column_playback_track(self.associated_column_index, Some(&id))?; - Ok(()) - } -} - -#[derive(Clone, Debug)] -pub struct TrackReorderingReaperChange { - project: Project, - guid: Guid, - source_index: u32, - dest_index: u32, -} - -impl TrackReorderingReaperChange { - pub fn new(track: &Track, dest_index: u32) -> Self { - Self { - project: track.project(), - guid: *track.guid(), - source_index: track.index().unwrap(), - dest_index, - } - } -} - -impl ReaperChange for TrackReorderingReaperChange { - fn post_undo(&mut self, _: ReaperChangeContext) -> Result<(), Box> { - let track = self.project.track_by_guid(&self.guid)?; - reorder_tracks(&track, self.source_index)?; - Ok(()) - } - - fn post_redo(&mut self, _: ReaperChangeContext) -> Result<(), Box> { - let track = self.project.track_by_guid(&self.guid)?; - reorder_tracks(&track, self.dest_index)?; - Ok(()) - } -} - -pub fn reorder_tracks( - source_track: &Track, - dest_track_index: u32, -) -> ClipEngineResult { - if !source_track.is_available() { - return Err("track doesn't exist"); - } - let source_track_index = source_track - .index() - .ok_or("source track doesn't have index")?; - source_track.select_exclusively(); - let reorder_index = if source_track_index < dest_track_index { - dest_track_index + 1 - } else { - dest_track_index - }; - let reaper_change = TrackReorderingReaperChange::new(source_track, dest_track_index); - Reaper::get() - .medium_reaper() - .reorder_selected_tracks(reorder_index, ReorderTracksBehavior::ExtendFolder)?; - Reaper::get().medium_reaper().update_arrange(); - Ok(reaper_change) -} diff --git a/playtime-clip-engine/src/base/matrix.rs b/playtime-clip-engine/src/base/matrix.rs deleted file mode 100644 index 5bbfe7eca..000000000 --- a/playtime-clip-engine/src/base/matrix.rs +++ /dev/null @@ -1,2006 +0,0 @@ -use crate::base::history::History; -use crate::base::row::Row; -use crate::base::{ - reorder_tracks, BoxedReaperChange, Clip, Column, ColumnRtEquipment, - EssentialColumnRecordClipArgs, IdMode, ReaperChange, ReaperChangeContext, - RestorationInstruction, Slot, SlotKit, TrackAdditionReaperChange, TrackReorderingReaperChange, -}; -use crate::rt::audio_hook::{FxInputClipRecordTask, HardwareInputClipRecordTask}; -use crate::rt::supplier::{ - keep_processing_cache_requests, keep_processing_pre_buffer_requests, - keep_processing_recorder_requests, AudioRecordingEquipment, ChainEquipment, - ChainPreBufferCommandProcessor, MidiRecordingEquipment, QuantizationSettings, RecorderRequest, - RecordingEquipment, -}; -use crate::rt::{ - ClipChangeEvent, ColumnHandle, ColumnPlayRowArgs, ColumnPlaySlotArgs, ColumnPlaySlotOptions, - ColumnStopArgs, ColumnStopSlotArgs, FillSlotMode, OverridableMatrixSettings, - QualifiedClipChangeEvent, QualifiedSlotChangeEvent, RtMatrixCommandSender, SlotChangeEvent, - WeakRtColumn, -}; -use crate::timeline::clip_timeline; -use crate::{rt, ClipEngineResult, HybridTimeline, Timeline}; -use base::validation_util::{ensure_no_duplicate, ValidationError}; -use crossbeam_channel::{Receiver, Sender}; -use derivative::Derivative; -use helgoboss_learn::UnitValue; -use helgoboss_midi::Channel; -use playtime_api::persistence as api; -use playtime_api::persistence::{ - ChannelRange, ClipPlayStopTiming, ColumnId, Db, MatrixClipPlayAudioSettings, - MatrixClipPlaySettings, MatrixClipRecordSettings, RecordLength, RowId, TempoRange, TrackId, -}; -use reaper_high::{ChangeEvent, OrCurrentProject, Project, Reaper, Track}; -use reaper_medium::{Bpm, MidiInputDeviceId}; -use std::collections::HashMap; -use std::error::Error; -use std::thread::JoinHandle; -use std::time::{Duration, Instant}; -use std::{mem, thread}; - -#[derive(Debug)] -pub struct Matrix { - history: History, - content: MatrixContent, -} - -#[derive(Derivative)] -#[derivative(Debug)] -pub(crate) struct MatrixContent { - /// Don't lock this from the main thread, only from real-time threads! - rt_matrix: rt::SharedRtMatrix, - settings: MatrixSettings, - #[derivative(Debug = "ignore")] - handler: Box, - chain_equipment: ChainEquipment, - recorder_request_sender: Sender, - columns: Vec, - /// Contains columns that are not in use anymore since the last column removal operation. - /// - /// They are still kept around in order to allow for a smooth fadeout of the clips on the - /// removed column (instead of abruptly interrupt playing and therefore ending up with an - /// annoying click). - retired_columns: Vec, - rows: Vec, - containing_track: Option, - command_receiver: Receiver, - rt_command_sender: Sender, - clipboard: MatrixClipboard, - poll_count: u64, - // We use this just for RAII (joining worker threads when dropped) - _worker_pool: WorkerPool, -} - -impl MatrixContent { - pub fn sync_column_handles_to_rt_matrix(&mut self) { - let column_handles = self.columns.iter().map(|c| c.create_handle()).collect(); - self.rt_command_sender.set_column_handles(column_handles); - } - - pub fn permanent_project(&self) -> Option { - self.containing_track.as_ref().map(|t| t.project()) - } - - /// Used by preset loading and by undo/redo. - pub fn load_internal(&mut self, api_matrix: api::Matrix) -> ClipEngineResult<()> { - let permanent_project = self.permanent_project(); - let necessary_row_count = api_matrix.necessary_row_count(); - // Settings - self.settings = MatrixSettings::from_api(&api_matrix); - // Columns - let mut old_columns: HashMap<_, _> = mem::take(&mut self.columns) - .into_iter() - .map(|c| (c.id().clone(), c)) - .collect(); - for api_column in api_matrix.columns.unwrap_or_default().into_iter() { - let mut column = old_columns.remove(&api_column.id).unwrap_or_else(|| { - Column::new( - api_column.id.clone(), - permanent_project, - necessary_row_count, - ) - }); - column.load( - api_column, - necessary_row_count, - ColumnRtEquipment { - chain_equipment: &self.chain_equipment, - recorder_request_sender: &self.recorder_request_sender, - matrix_settings: &self.settings, - }, - )?; - self.columns.push(column); - } - // Rows - self.rows = api_matrix - .rows - .unwrap_or_default() - .into_iter() - .map(Row::from_api_row) - .collect(); - self.rows - .resize_with(necessary_row_count, || Row::new(RowId::random())); - // Sync to real-time matrix - self.sync_column_handles_to_rt_matrix(); - // Retire old and now unused columns - self.retired_columns - .extend(old_columns.into_values().map(RetiredColumn::new)); - // Notify listeners - self.notify_everything_changed(); - Ok(()) - } - - pub fn set_column_playback_track( - &mut self, - column_index: usize, - track_id: Option<&TrackId>, - ) -> ClipEngineResult<()> { - let column = self.columns.get_mut(column_index).ok_or(NO_SUCH_COLUMN)?; - column.set_playback_track_from_id( - track_id, - ColumnRtEquipment { - chain_equipment: &self.chain_equipment, - recorder_request_sender: &self.recorder_request_sender, - matrix_settings: &self.settings, - }, - )?; - self.notify_everything_changed(); - Ok(()) - } - - pub fn notify_everything_changed(&self) { - self.emit(ClipMatrixEvent::EverythingChanged); - } - - pub fn emit(&self, event: ClipMatrixEvent) { - self.handler.emit_event(event); - } -} - -#[derive(Debug)] -struct RetiredColumn { - time_of_retirement: Instant, - _column: Column, -} - -impl RetiredColumn { - pub fn new(column: Column) -> Self { - Self { - time_of_retirement: Instant::now(), - _column: column, - } - } - - pub fn is_still_alive(&self) -> bool { - const MAX_RETIRED_DURATION: Duration = Duration::from_millis(1000); - self.time_of_retirement.elapsed() < MAX_RETIRED_DURATION - } -} - -#[derive(Debug, Default)] -struct MatrixClipboard { - content: Option, -} - -#[derive(Debug)] -enum MatrixClipboardContent { - Slot(Vec), - Scene(Vec), -} - -#[derive(Debug, Default)] -pub struct MatrixSettings { - pub common_tempo_range: TempoRange, - pub clip_record_settings: MatrixClipRecordSettings, - pub overridable: OverridableMatrixSettings, -} - -impl MatrixSettings { - pub fn from_api(matrix: &api::Matrix) -> Self { - Self { - common_tempo_range: matrix.common_tempo_range, - clip_record_settings: matrix.clip_record_settings, - overridable: OverridableMatrixSettings::from_api(&matrix.clip_play_settings), - } - } -} - -#[derive(Debug)] -pub enum MatrixCommand { - ThrowAway(MatrixGarbage), -} - -#[derive(Debug)] -pub enum MatrixGarbage { - ColumnHandles(Vec), -} - -pub trait MainMatrixCommandSender { - fn throw_away(&self, garbage: MatrixGarbage); - fn send_command(&self, command: MatrixCommand); -} - -impl MainMatrixCommandSender for Sender { - fn throw_away(&self, garbage: MatrixGarbage) { - self.send_command(MatrixCommand::ThrowAway(garbage)); - } - - fn send_command(&self, command: MatrixCommand) { - self.try_send(command).unwrap(); - } -} - -#[derive(Debug, Default)] -struct WorkerPool { - workers: Vec, -} - -#[derive(Debug)] -struct Worker { - join_handle: Option>, -} - -impl WorkerPool { - pub fn add_worker(&mut self, name: &str, f: impl FnOnce() + Send + 'static) { - let join_handle = thread::Builder::new() - .name(String::from(name)) - .spawn(f) - .unwrap(); - let worker = Worker { - join_handle: Some(join_handle), - }; - self.workers.push(worker); - } -} - -impl Drop for Worker { - fn drop(&mut self) { - if let Some(join_handle) = self.join_handle.take() { - let name = join_handle.thread().name().unwrap(); - debug!("Shutting down clip matrix worker \"{}\"...", name); - join_handle.join().unwrap(); - } - } -} - -impl Matrix { - pub fn save(&self) -> api::Matrix { - api::Matrix { - columns: Some( - self.content - .columns - .iter() - .map(|column| column.save()) - .collect(), - ), - rows: Some(self.content.rows.iter().map(|row| row.save()).collect()), - clip_play_settings: self.save_play_settings(), - clip_record_settings: self.content.settings.clip_record_settings, - common_tempo_range: self.content.settings.common_tempo_range, - } - } - - fn save_play_settings(&self) -> api::MatrixClipPlaySettings { - MatrixClipPlaySettings { - start_timing: self.content.settings.overridable.clip_play_start_timing, - stop_timing: self.content.settings.overridable.clip_play_stop_timing, - audio_settings: MatrixClipPlayAudioSettings { - resample_mode: self.content.settings.overridable.audio_resample_mode, - time_stretch_mode: self.content.settings.overridable.audio_time_stretch_mode, - cache_behavior: self.content.settings.overridable.audio_cache_behavior, - }, - } - } - - pub fn history(&self) -> &History { - &self.history - } - - /// Returns the project which contains this matrix, unless the matrix is project-less - /// (monitoring FX chain). - pub fn permanent_project(&self) -> Option { - self.content.permanent_project() - } - - /// Returns the permanent project. If the matrix is project-less, the current project. - pub fn temporary_project(&self) -> Project { - self.permanent_project().or_current_project() - } - - /// Creates an empty matrix with no columns and no rows. - pub fn new(handler: Box, containing_track: Option) -> Self { - let (recorder_request_sender, recorder_request_receiver) = crossbeam_channel::bounded(500); - let (cache_request_sender, cache_request_receiver) = crossbeam_channel::bounded(500); - let (pre_buffer_request_sender, pre_buffer_request_receiver) = - crossbeam_channel::bounded(500); - let (rt_command_sender, rt_command_receiver) = crossbeam_channel::bounded(500); - let (main_command_sender, main_command_receiver) = crossbeam_channel::bounded(500); - let mut worker_pool = WorkerPool::default(); - worker_pool.add_worker("Playtime recording worker", move || { - keep_processing_recorder_requests(recorder_request_receiver); - }); - worker_pool.add_worker("Playtime cache worker", move || { - keep_processing_cache_requests(cache_request_receiver); - }); - worker_pool.add_worker("Playtime pre-buffer worker", move || { - keep_processing_pre_buffer_requests( - pre_buffer_request_receiver, - ChainPreBufferCommandProcessor, - ); - }); - let project = containing_track.as_ref().map(|t| t.project()); - let rt_matrix = rt::RtMatrix::new(rt_command_receiver, main_command_sender, project); - Self { - history: History::new(Default::default()), - content: MatrixContent { - rt_matrix: { - let m = rt::SharedRtMatrix::new(rt_matrix); - // This is necessary since Rust 1.62.0 (or 1.63.0, not sure). Since those versions, - // locking a mutex the first time apparently allocates. If we don't lock the - // mutex now for the first time but do it in the real-time thread, assert_no_alloc will - // complain in debug builds. - drop(m.lock()); - m - }, - settings: Default::default(), - handler, - chain_equipment: ChainEquipment { - cache_request_sender, - pre_buffer_request_sender, - }, - recorder_request_sender, - columns: vec![], - retired_columns: vec![], - rows: vec![], - containing_track, - command_receiver: main_command_receiver, - rt_command_sender, - clipboard: Default::default(), - poll_count: 0, - _worker_pool: worker_pool, - }, - } - } - - pub fn real_time_matrix(&self) -> rt::WeakRtMatrix { - self.content.rt_matrix.downgrade() - } - - pub fn load(&mut self, api_matrix: api::Matrix) -> Result<(), Box> { - // We make a fresh start by throwing away all existing columns (with preview registers). - // In theory, we don't need to do this because our core loading logic (the same that we also - // use for undo/redo) should ideally be solid enough to react correctly when loading - // something completely different. But who knows, maybe we have bugs in there and with the - // following simple line we can effectively prevent those from having an effect here. - self.content - .retired_columns - .extend(self.content.columns.drain(..).map(RetiredColumn::new)); - // This is a public method so we need to expect the worst, also invalid data! Do checks. - validate_api_matrix(&api_matrix)?; - // Core loading logic - self.content.load_internal(api_matrix)?; - // We want to reset undo/redo history - self.history = History::new(self.save()); - Ok(()) - } - - pub fn next_undo_label(&self) -> Option<&str> { - self.history.next_undo_label() - } - - pub fn next_redo_label(&self) -> Option<&str> { - self.history.next_redo_label() - } - - pub fn can_undo(&self) -> bool { - self.history.can_undo() - } - - pub fn can_redo(&self) -> bool { - self.history.can_redo() - } - - pub fn undo(&mut self) -> ClipEngineResult<()> { - self.restore_history_state( - |history| history.undo(), - |ch, context| ch.pre_undo(context), - |ch, context| ch.post_undo(context), - ) - } - - pub fn redo(&mut self) -> ClipEngineResult<()> { - self.restore_history_state( - |history| history.redo(), - |ch, context| ch.pre_redo(context), - |ch, context| ch.post_redo(context), - ) - } - - fn restore_history_state( - &mut self, - pop_state: impl FnOnce(&mut History) -> ClipEngineResult, - pre_load: impl Fn(&mut dyn ReaperChange, ReaperChangeContext) -> Result<(), Box>, - post_load: impl Fn(&mut dyn ReaperChange, ReaperChangeContext) -> Result<(), Box>, - ) -> ClipEngineResult<()> { - let instruction = pop_state(&mut self.history)?; - // Apply pre-load REAPER changes - // - // # Example 1: Insert column - // - // Undo => - - // Redo => Insert REAPER track - // - // # Example 2: Remove column - // - // Undo => Insert REAPER track - // Redo => - - for change in instruction.reaper_changes.iter_mut() { - let context = ReaperChangeContext { - matrix: &mut self.content, - }; - let _ = pre_load(&mut **change, context); - } - // Restore actual matrix state - self.content.load_internal(instruction.matrix.clone())?; - // Apply post-load REAPER changes - // - // # Example 1: Insert column - // - // Undo => Remove REAPER track - // Redo => - - // - // # Example 2: Remove column - // - // Undo => - - // Redo => Remove REAPER track - for change in instruction.reaper_changes.iter_mut() { - let context = ReaperChangeContext { - matrix: &mut self.content, - }; - let _ = post_load(&mut **change, context); - } - // Emit change notification - self.emit(ClipMatrixEvent::HistoryChanged); - Ok(()) - } - - fn undoable( - &mut self, - label: impl Into, - f: impl FnOnce(&mut Self) -> ClipEngineResult>, - ) -> ClipEngineResult<()> { - let reaper_changes = f(self)?; - self.history.add_buffered(label.into(), reaper_changes); - Ok(()) - } - - /// Freezes the complete matrix. - pub async fn freeze(&mut self) { - for (i, column) in self.content.columns.iter_mut().enumerate() { - let _ = column.freeze(i).await; - } - } - - /// Takes the current effective matrix dimensions into account, so even if a slot doesn't exist - /// yet physically in the column, it returns `true` if it *should* exist. - pub fn slot_exists(&self, coordinates: ClipSlotAddress) -> bool { - coordinates.column < self.content.columns.len() && coordinates.row < self.row_count() - } - - /// Finds the column at the given index. - pub fn find_column(&self, index: usize) -> Option<&Column> { - self.get_column(index).ok() - } - - /// Finds the slot at the given address. - pub fn find_slot(&self, address: ClipSlotAddress) -> Option<&Slot> { - self.get_slot(address).ok() - } - - /// Finds the clip at the given address. - pub fn find_clip(&self, address: ClipAddress) -> Option<&Clip> { - self.get_clip(address).ok() - } - - /// Returns an iterator over all slots in each column, including the column indexes. - pub fn all_slots(&self) -> impl Iterator + '_ { - self.content - .columns - .iter() - .enumerate() - .flat_map(|(column_index, column)| { - column - .slots() - .map(move |slot| SlotWithColumn::new(column_index, slot)) - }) - } - - /// Returns an iterator over all columns. - pub fn all_columns(&self) -> impl Iterator + '_ { - self.content.columns.iter() - } - - /// Returns an iterator over all clips in a row whose column is a scene follower. - fn all_clips_in_scene( - &self, - row_index: usize, - ) -> impl Iterator + '_ { - let _project = self.permanent_project(); - self.content - .columns - .iter() - .enumerate() - .filter(|(_, c)| c.follows_scene()) - .filter_map(move |(i, c)| Some((i, c.get_slot(row_index).ok()?))) - .map(move |(i, s)| { - let api_clips = s.clips().filter_map(move |clip| clip.save().ok()); - SlotContentsWithColumn::new(i, api_clips.collect()) - }) - } - - /// Cuts the given scene's clips to the matrix clipboard. - pub fn cut_scene(&mut self, row_index: usize) -> ClipEngineResult<()> { - self.copy_scene(row_index)?; - self.clear_scene(row_index)?; - Ok(()) - } - - /// Copies the given scene's clips to the matrix clipboard. - pub fn copy_scene(&mut self, row_index: usize) -> ClipEngineResult<()> { - let clips = self.all_clips_in_scene(row_index).collect(); - self.content.clipboard.content = Some(MatrixClipboardContent::Scene(clips)); - Ok(()) - } - - /// Pastes the clips stored in the matrix clipboard into the given scene. - pub fn paste_scene(&mut self, row_index: usize) -> ClipEngineResult<()> { - let content = self - .content - .clipboard - .content - .as_ref() - .ok_or("clipboard empty")?; - let MatrixClipboardContent::Scene(clips) = content else { - return Err("clipboard doesn't contain scene contents"); - }; - let cloned_clips = clips.clone(); - self.undoable("Fill row with clips", |matrix| { - matrix.replace_row_with_clips(row_index, cloned_clips, IdMode::AssignNewIds)?; - matrix.notify_everything_changed(); - Ok(vec![]) - }) - } - - pub fn remove_column(&mut self, column_index: usize) -> ClipEngineResult<()> { - if column_index >= self.content.columns.len() { - return Err("column doesn't exist"); - } - self.undoable("Remove column", |matrix| { - let column = matrix.content.columns.remove(column_index); - column.panic(); - matrix - .content - .retired_columns - .push(RetiredColumn::new(column)); - matrix.content.sync_column_handles_to_rt_matrix(); - matrix.notify_everything_changed(); - Ok(vec![]) - }) - } - - pub fn duplicate_column(&mut self, column_index: usize) -> ClipEngineResult<()> { - self.undoable("Duplicate column", |matrix| { - let column = matrix - .content - .columns - .get(column_index) - .ok_or("column doesn't exist")?; - let duplicate_column = column.duplicate(ColumnRtEquipment { - chain_equipment: &matrix.content.chain_equipment, - recorder_request_sender: &matrix.content.recorder_request_sender, - matrix_settings: &matrix.content.settings, - }); - matrix - .content - .columns - .insert(column_index + 1, duplicate_column); - matrix.content.sync_column_handles_to_rt_matrix(); - matrix.notify_everything_changed(); - Ok(vec![]) - }) - } - - /// Determines the index of a new REAPER track to be added togeter with the given new column. - /// - /// Only works if the existing column next to it has a playback track assigned. - fn determine_index_of_new_track(&self, new_column_index: usize) -> Option { - if self.content.columns.is_empty() { - // We add the first column. Add track at end. - return Some(self.temporary_project().track_count()); - } - let reference_column_index = if new_column_index < self.content.columns.len() { - // Add column in the middle. Add track above playback track of column which was - // previously at that position. - new_column_index - } else { - // Add column at end. Add track below playback track of last column. - new_column_index - 1 - }; - let reference_column = self.content.columns.get(reference_column_index)?; - reference_column.playback_track().ok()?.index() - } - - pub fn insert_column(&mut self, column_index: usize) -> ClipEngineResult<()> { - // Try to add new column track in REAPER - let new_track_index = self.determine_index_of_new_track(column_index); - let new_track = - new_track_index.and_then(|i| self.temporary_project().insert_track_at(i).ok()); - let reaper_changes: Vec = if let Some(t) = &new_track { - let reaper_change = TrackAdditionReaperChange::new(t, column_index); - vec![Box::new(reaper_change)] - } else { - vec![] - }; - // Add actual column to matrix, with or without new track - self.undoable("Insert column", |matrix| { - let mut new_column = Column::new( - ColumnId::random(), - matrix.permanent_project(), - matrix.content.rows.len(), - ); - new_column.set_playback_track(new_track, matrix.column_rt_equipment()); - matrix.content.columns.insert(column_index, new_column); - matrix.content.sync_column_handles_to_rt_matrix(); - matrix.notify_everything_changed(); - Ok(reaper_changes) - }) - } - - pub fn duplicate_row(&mut self, row_index: usize) -> ClipEngineResult<()> { - self.undoable("Duplicate row", |matrix| { - let row = matrix - .content - .rows - .get(row_index) - .ok_or("row doesn't exist")?; - matrix.content.rows.insert(row_index + 1, row.duplicate()); - for column in &mut matrix.content.columns { - column.duplicate_slot( - row_index, - ColumnRtEquipment { - chain_equipment: &matrix.content.chain_equipment, - recorder_request_sender: &matrix.content.recorder_request_sender, - matrix_settings: &matrix.content.settings, - }, - )?; - } - matrix.notify_everything_changed(); - Ok(vec![]) - }) - } - - pub fn insert_row(&mut self, row_index: usize) -> ClipEngineResult<()> { - self.undoable("Insert row", |matrix| { - if row_index > matrix.content.rows.len() { - return Err("row index too large"); - } - matrix - .content - .rows - .insert(row_index, Row::new(RowId::random())); - for column in &mut matrix.content.columns { - column.insert_slot( - row_index, - ColumnRtEquipment { - chain_equipment: &matrix.content.chain_equipment, - recorder_request_sender: &matrix.content.recorder_request_sender, - matrix_settings: &matrix.content.settings, - }, - )?; - } - matrix.notify_everything_changed(); - Ok(vec![]) - }) - } - - pub fn remove_row(&mut self, row_index: usize) -> ClipEngineResult<()> { - if row_index >= self.row_count() { - return Err("row doesn't exist"); - } - self.undoable("Remove row", |matrix| { - matrix.content.rows.remove(row_index); - for column in &mut matrix.content.columns { - // It's possible that the slot index doesn't exist in that column because slots - // are added lazily. - column.remove_slot(row_index)?; - } - matrix.notify_everything_changed(); - Ok(vec![]) - }) - } - - /// Clears the slots of all scene-following columns. - pub fn clear_scene(&mut self, row_index: usize) -> ClipEngineResult<()> { - self.undoable("Clear scene", |matrix| { - matrix.clear_scene_internal(row_index)?; - matrix.notify_everything_changed(); - Ok(vec![]) - }) - } - - fn clear_scene_internal(&mut self, row_index: usize) -> ClipEngineResult<()> { - if row_index >= self.row_count() { - return Err("row doesn't exist"); - } - for column in self.scene_columns_mut() { - // If the slot doesn't exist in that column, it's okay. - let _ = column.clear_slot(row_index); - } - Ok(()) - } - - /// Adds a history entry immediately and emits the appropriate event. - fn add_history_entry_immediately( - &mut self, - label: String, - reaper_changes: Vec, - ) { - self.history.add(label, self.save(), reaper_changes); - self.emit(ClipMatrixEvent::HistoryChanged); - } - - /// Returns an iterator over all scene-following columns. - fn scene_columns(&self) -> impl Iterator { - self.content.columns.iter().filter(|c| c.follows_scene()) - } - - /// Returns a mutable iterator over all scene-following columns. - fn scene_columns_mut(&mut self) -> impl Iterator { - self.content - .columns - .iter_mut() - .filter(|c| c.follows_scene()) - } - - /// Cuts the given slot's clips to the matrix clipboard. - pub fn cut_slot(&mut self, address: ClipSlotAddress) -> ClipEngineResult<()> { - self.copy_slot(address)?; - self.clear_slot(address)?; - Ok(()) - } - - /// Copies the given slot's clips to the matrix clipboard. - pub fn copy_slot(&mut self, address: ClipSlotAddress) -> ClipEngineResult<()> { - let clips_in_slot = self.get_slot(address)?.api_clips(self.permanent_project()); - self.content.clipboard.content = Some(MatrixClipboardContent::Slot(clips_in_slot)); - Ok(()) - } - - /// Sets the name of the column and the track. - /// - /// If the name is `None`, it resets the column name to the name of the track. - pub fn set_column_name(&mut self, column_index: usize, name: String) -> ClipEngineResult<()> { - if let Ok(t) = self.get_column(column_index)?.playback_track() { - // This will cause the matrix to rename the columns as well (uni-directional flow) - t.set_name(name); - Ok(()) - } else { - // Column isn't associated with a track. Rename the column itself. - self.undoable("Set column name", move |matrix| { - let column = matrix.get_column_mut(column_index)?; - column.set_name(name); - Ok(vec![]) - }) - } - } - - /// Sets the playback track of the given column. - pub fn set_column_playback_track( - &mut self, - column_index: usize, - track_id: Option<&TrackId>, - ) -> ClipEngineResult<()> { - self.undoable("Set column playback track", move |matrix| { - matrix - .content - .set_column_playback_track(column_index, track_id)?; - Ok(vec![]) - }) - } - - fn column_rt_equipment(&self) -> ColumnRtEquipment { - ColumnRtEquipment { - chain_equipment: &self.content.chain_equipment, - recorder_request_sender: &self.content.recorder_request_sender, - matrix_settings: &self.content.settings, - } - } - - /// Pastes the clips stored in the matrix clipboard into the given slot. - // TODO-high-clip-engine In all copy scenarios, we must take care to create new unique IDs! - pub fn paste_slot(&mut self, address: ClipSlotAddress) -> ClipEngineResult<()> { - let content = self - .content - .clipboard - .content - .as_ref() - .ok_or("clipboard empty")?; - let MatrixClipboardContent::Slot(clips) = content else { - return Err("clipboard doesn't contain slot contents"); - }; - let cloned_clips = clips.clone(); - self.undoable("Paste slot", move |matrix| { - matrix.replace_clips_in_slot(address, cloned_clips, IdMode::AssignNewIds)?; - let event = SlotChangeEvent::Clips("Added clips to slot"); - matrix.emit(ClipMatrixEvent::slot_changed(address, event)); - Ok(vec![]) - }) - } - - /// Clears the given slot. - pub fn clear_slot(&mut self, address: ClipSlotAddress) -> ClipEngineResult<()> { - self.undoable("Clear slot", move |matrix| { - matrix.clear_slot_internal(address)?; - let event = SlotChangeEvent::Clips(""); - matrix.emit(ClipMatrixEvent::slot_changed(address, event)); - Ok(vec![]) - }) - } - - fn clear_slot_internal(&mut self, address: ClipSlotAddress) -> ClipEngineResult<()> { - self.get_column_mut(address.column)? - .clear_slot(address.row)?; - Ok(()) - } - - /// Adjusts the section lengths of all clips in the given slot. - pub fn adjust_slot_section_length( - &mut self, - address: ClipSlotAddress, - factor: f64, - ) -> ClipEngineResult<()> { - let kit = self.get_slot_kit(address)?; - kit.slot.adjust_section_length(factor, kit.sender) - } - - /// Opens the editor for the given clip. - pub fn start_editing_clip(&mut self, address: ClipAddress) -> ClipEngineResult<()> { - self.get_column_mut(address.slot_address.column)? - .start_editing_clip(address.slot_address.row, address.clip_index) - } - - /// Closes the editor for the given clip. - pub fn stop_editing_clip(&mut self, address: ClipAddress) -> ClipEngineResult<()> { - self.get_column_mut(address.slot_address.column)? - .stop_editing_clip(address.slot_address.row, address.clip_index) - } - - /// Returns if the editor for the given slot is open. - pub fn is_editing_clip(&self, address: ClipAddress) -> bool { - let Ok(slot) = self.get_slot(address.slot_address) else { - return false; - }; - slot.is_editing_clip(address.clip_index) - } - - /// Replaces the given row with the given clips. - fn replace_row_with_clips( - &mut self, - row_index: usize, - slot_contents: Vec, - id_mode: IdMode, - ) -> ClipEngineResult<()> { - for slot_content in slot_contents { - let column = - match get_column_mut(&mut self.content.columns, slot_content.column_index).ok() { - None => break, - Some(c) => c, - }; - column.fill_slot( - row_index, - slot_content.value, - &self.content.chain_equipment, - &self.content.recorder_request_sender, - &self.content.settings, - FillSlotMode::Replace, - id_mode, - )?; - } - Ok(()) - } - - fn replace_clips_in_slot( - &mut self, - address: ClipSlotAddress, - api_clips: Vec, - id_mode: IdMode, - ) -> ClipEngineResult<()> { - let column = get_column_mut(&mut self.content.columns, address.column)?; - column.fill_slot( - address.row, - api_clips, - &self.content.chain_equipment, - &self.content.recorder_request_sender, - &self.content.settings, - FillSlotMode::Replace, - id_mode, - )?; - Ok(()) - } - - /// Replaces the slot contents with the currently selected REAPER item. - pub fn replace_slot_clips_with_selected_item( - &mut self, - address: ClipSlotAddress, - ) -> ClipEngineResult<()> { - self.undoable("Fill slot with selected item", |matrix| { - let column = get_column_mut(&mut matrix.content.columns, address.column)?; - column.replace_slot_clips_with_selected_item( - address.row, - &matrix.content.chain_equipment, - &matrix.content.recorder_request_sender, - &matrix.content.settings, - )?; - matrix.emit(ClipMatrixEvent::slot_changed( - address, - SlotChangeEvent::Clips(""), - )); - Ok(vec![]) - }) - } - - /// Plays the given slot. - pub fn play_slot( - &self, - address: ClipSlotAddress, - options: ColumnPlaySlotOptions, - ) -> ClipEngineResult<()> { - let timeline = self.timeline(); - let column = get_column(&self.content.columns, address.column)?; - let args = ColumnPlaySlotArgs { - slot_index: address.row, - timeline, - ref_pos: None, - options, - }; - column.play_slot(args); - Ok(()) - } - - pub fn move_slot_to( - &mut self, - source_address: ClipSlotAddress, - dest_address: ClipSlotAddress, - ) -> ClipEngineResult<()> { - if source_address == dest_address { - return Ok(()); - } - self.undoable("Move slot", |matrix| { - if source_address.column == dest_address.column { - // Special handling. We can easily move within the same column. - let column = matrix.get_column_mut(source_address.column)?; - column.move_slot_contents(source_address.row, dest_address.row)?; - matrix.notify_everything_changed(); - Ok(vec![]) - } else { - let clips_in_slot = matrix - .get_slot(source_address)? - .api_clips(matrix.permanent_project()); - matrix.clear_slot_internal(source_address)?; - matrix.replace_clips_in_slot(dest_address, clips_in_slot, IdMode::KeepIds)?; - matrix.notify_everything_changed(); - Ok(vec![]) - } - }) - } - - pub fn copy_slot_to( - &mut self, - source_address: ClipSlotAddress, - dest_address: ClipSlotAddress, - ) -> ClipEngineResult<()> { - if source_address == dest_address { - return Ok(()); - } - let clips_in_slot = self - .get_slot(source_address)? - .api_clips(self.permanent_project()); - self.undoable("Copy slot to", |matrix| { - matrix.replace_clips_in_slot(dest_address, clips_in_slot, IdMode::AssignNewIds)?; - let event = SlotChangeEvent::Clips("Copied clips to slot"); - matrix.emit(ClipMatrixEvent::slot_changed(dest_address, event)); - Ok(vec![]) - })?; - Ok(()) - } - - pub fn move_scene_content_to( - &mut self, - source_row_index: usize, - dest_row_index: usize, - ) -> ClipEngineResult<()> { - if source_row_index == dest_row_index { - return Ok(()); - } - let clips_in_scene = self.all_clips_in_scene(source_row_index).collect(); - self.undoable("Move scene content to", |matrix| { - matrix.replace_row_with_clips(dest_row_index, clips_in_scene, IdMode::KeepIds)?; - matrix.clear_scene_internal(source_row_index)?; - matrix.notify_everything_changed(); - Ok(vec![]) - }) - } - - pub fn reorder_columns( - &mut self, - source_column_index: usize, - dest_column_index: usize, - ) -> ClipEngineResult<()> { - if source_column_index == dest_column_index { - return Ok(()); - } - self.undoable("Reorder columns", |matrix| { - let source_column = matrix.get_column(source_column_index)?; - let dest_column = matrix.get_column(dest_column_index)?; - let reaper_changes: Vec = - if let Ok(change) = matrix.reorder_column_tracks(source_column, dest_column) { - vec![Box::new(change)] - } else { - vec![] - }; - let source_column = matrix.content.columns.remove(source_column_index); - matrix - .content - .columns - .insert(dest_column_index, source_column); - matrix.notify_everything_changed(); - Ok(reaper_changes) - }) - } - - fn reorder_column_tracks( - &self, - source_column: &Column, - dest_column: &Column, - ) -> ClipEngineResult { - let source_track = source_column.playback_track()?; - let dest_track = dest_column.playback_track()?; - let dest_track_index = dest_track - .index() - .ok_or("destination track doesn't have index")?; - reorder_tracks(source_track, dest_track_index) - } - - pub fn reorder_rows( - &mut self, - source_row_index: usize, - dest_row_index: usize, - ) -> ClipEngineResult<()> { - if source_row_index >= self.content.rows.len() { - return Err("source row doesn't exist"); - } - if dest_row_index >= self.content.rows.len() { - return Err("destination row doesn't exist"); - } - if source_row_index == dest_row_index { - return Ok(()); - } - self.undoable("Reorder rows", |matrix| { - let source_row = matrix.content.rows.remove(source_row_index); - matrix.content.rows.insert(dest_row_index, source_row); - for column in &mut matrix.content.columns { - column.reorder_slots(source_row_index, dest_row_index)?; - } - matrix.notify_everything_changed(); - Ok(vec![]) - }) - } - - pub fn copy_scene_content_to( - &mut self, - source_row_index: usize, - dest_row_index: usize, - ) -> ClipEngineResult<()> { - if source_row_index == dest_row_index { - return Ok(()); - } - let clips_in_scene = self.all_clips_in_scene(source_row_index).collect(); - self.undoable("Copy scene content to", |matrix| { - matrix.replace_row_with_clips(dest_row_index, clips_in_scene, IdMode::AssignNewIds)?; - matrix.notify_everything_changed(); - Ok(vec![]) - }) - } - - /// Stops the given slot. - pub fn stop_slot( - &self, - address: ClipSlotAddress, - stop_timing: Option, - ) -> ClipEngineResult<()> { - let timeline = self.timeline(); - let column = get_column(&self.content.columns, address.column)?; - let args = ColumnStopSlotArgs { - slot_index: address.row, - timeline, - ref_pos: None, - stop_timing, - }; - column.stop_slot(args); - Ok(()) - } - - /// Stops all slots in all columns. - pub fn stop(&self) { - let timeline = self.timeline(); - let args = ColumnStopArgs { - ref_pos: Some(timeline.cursor_pos()), - timeline, - stop_timing: None, - }; - for c in &self.content.columns { - c.stop(args.clone()); - } - } - - /// Stops all slots in all columns immediately. - pub fn panic(&self) { - for c in &self.content.columns { - c.panic(); - } - } - - /// Stops column immediately. - pub fn panic_column(&self, column_index: usize) -> ClipEngineResult<()> { - self.get_column(column_index)?.panic(); - Ok(()) - } - - /// Stops slot immediately. - pub fn panic_slot(&self, address: ClipSlotAddress) -> ClipEngineResult<()> { - self.get_column(address.column)?.panic_slot(address.row); - Ok(()) - } - - /// Stops row immediately. - pub fn panic_row(&self, row_index: usize) -> ClipEngineResult<()> { - for col in &self.content.columns { - col.panic_slot(row_index); - } - Ok(()) - } - - /// Plays all slots of scene-following columns in the given row. - pub fn play_scene(&self, index: usize) { - let timeline = self.timeline(); - let timeline_cursor_pos = timeline.cursor_pos(); - let args = ColumnPlayRowArgs { - slot_index: index, - timeline, - ref_pos: timeline_cursor_pos, - }; - for c in &self.content.columns { - c.play_scene(args.clone()); - } - } - - /// Returns the basic settings of this matrix. - pub fn settings(&self) -> &MatrixSettings { - &self.content.settings - } - - pub fn all_matrix_settings_combined(&self) -> api::MatrixSettings { - api::MatrixSettings { - clip_play_settings: self.save_play_settings(), - clip_record_settings: self.content.settings.clip_record_settings, - common_tempo_range: self.content.settings.common_tempo_range, - } - } - - /// Sets the record duration for new clip recordings. - pub fn set_record_duration(&mut self, record_length: RecordLength) { - self.content.settings.clip_record_settings.duration = record_length; - self.emit(ClipMatrixEvent::RecordDurationChanged); - } - - /// Builds a scene of all currently playing clips, in the first empty row. - pub fn build_scene_in_first_empty_row(&mut self) -> ClipEngineResult<()> { - let empty_row_index = (0usize..) - .find(|row_index| self.scene_is_empty(*row_index)) - .expect("there's always an empty row"); - self.build_scene_internal(empty_row_index) - } - - /// Builds a scene of all currently playing clips, in the given row. - pub fn build_scene(&mut self, row_index: usize) -> ClipEngineResult<()> { - if !self.scene_is_empty(row_index) { - return Err("row is not empty"); - } - self.build_scene_internal(row_index) - } - - fn build_scene_internal(&mut self, row_index: usize) -> ClipEngineResult<()> { - self.undoable("Build scene", |matrix| { - let playing_clips = matrix.capture_playing_clips(); - matrix.distribute_clips_to_scene(playing_clips, row_index)?; - matrix.notify_everything_changed(); - Ok(vec![]) - }) - } - - fn notify_everything_changed(&self) { - self.content.notify_everything_changed(); - } - - fn emit(&self, event: ClipMatrixEvent) { - self.content.emit(event); - } - - fn distribute_clips_to_scene( - &mut self, - slot_contents: Vec, - row_index: usize, - ) -> ClipEngineResult<()> { - let need_handle_sync = false; - for slot_content in slot_contents { - // First try to put it within same column as clip itself - let original_column = - get_column_mut(&mut self.content.columns, slot_content.column_index)?; - let dest_column = - if original_column.follows_scene() && original_column.slot_is_empty(row_index) { - // We have space in that column, good. - original_column - } else { - // We need to find another appropriate column. - // TODO-high We shouldn't do that but use the multi-clip-per-slot feature! - // let original_column_track = original_column.playback_track().ok().cloned(); - // let existing_column = self.columns.iter_mut().find(|c| { - // c.follows_scene() - // && c.slot_is_empty(row_index) - // && c.playback_track().ok() == original_column_track.as_ref() - // }); - // if let Some(c) = existing_column { - // // Found. - // c - // } else { - // // Not found. Create a new one. - // let same_column = self.columns.get(slot_content.column_index).unwrap(); - // let mut duplicate = same_column.duplicate_without_contents(); - // duplicate.set_play_mode(ColumnPlayMode::ExclusiveFollowingScene); - // duplicate.sync_matrix_and_column_settings_to_rt_column(&self.settings); - // self.columns.push(duplicate); - // need_handle_sync = true; - // self.columns.last_mut().unwrap() - // } - todo!() - }; - dest_column.fill_slot( - row_index, - slot_content.value, - &self.content.chain_equipment, - &self.content.recorder_request_sender, - &self.content.settings, - FillSlotMode::Replace, - IdMode::AssignNewIds, - )?; - } - if need_handle_sync { - self.content.sync_column_handles_to_rt_matrix(); - } - Ok(()) - } - - /// Returns whether the given slot is empty. - pub fn slot_is_empty(&self, address: ClipSlotAddress) -> bool { - let Some(column) = self.content.columns.get(address.column) else { - return false; - }; - column.slot_is_empty(address.row) - } - - /// Returns whether the scene of the given row is empty. - pub fn scene_is_empty(&self, row_index: usize) -> bool { - self.scene_columns().all(|c| c.slot_is_empty(row_index)) - } - - fn capture_playing_clips(&self) -> Vec { - let _project = self.permanent_project(); - self.content - .columns - .iter() - .enumerate() - .map(|(col_index, col)| { - let api_clips = col - .playing_clips() - .filter_map(move |(_, clip)| clip.save().ok()); - SlotContentsWithColumn::new(col_index, api_clips.collect()) - }) - .collect() - } - - /// Stops all slots in the given column. - pub fn stop_column( - &self, - index: usize, - stop_timing: Option, - ) -> ClipEngineResult<()> { - let timeline = self.timeline(); - let column = self.get_column(index)?; - let args = ColumnStopArgs { - timeline, - ref_pos: None, - stop_timing, - }; - column.stop(args); - Ok(()) - } - - /// Returns a clip timeline for this matrix. - pub fn timeline(&self) -> HybridTimeline { - clip_timeline(self.permanent_project(), false) - } - - fn process_commands(&mut self) { - while let Ok(task) = self.content.command_receiver.try_recv() { - match task { - MatrixCommand::ThrowAway(_) => {} - } - } - } - - fn remove_obsolete_retired_columns(&mut self) { - self.content.retired_columns.retain(|c| c.is_still_alive()); - } - - /// Polls this matrix and returns a list of gathered events. - /// - /// Polling is absolutely essential, e.g. to detect changes or finish recordings. - pub fn poll(&mut self, project: Option) -> Vec { - let timeline = clip_timeline(project, false); - let timeline_cursor_pos = timeline.cursor_pos(); - let timeline_tempo = timeline.tempo_at(timeline_cursor_pos); - self.poll_internal(timeline_tempo) - } - - fn poll_internal(&mut self, timeline_tempo: Bpm) -> Vec { - self.poll_history(); - self.remove_obsolete_retired_columns(); - self.process_commands(); - let events: Vec<_> = self - .content - .columns - .iter_mut() - .enumerate() - .flat_map(|(column_index, column)| { - column - .poll(timeline_tempo) - .into_iter() - .map(move |(row_index, event)| { - ClipMatrixEvent::slot_changed( - ClipSlotAddress::new(column_index, row_index), - event, - ) - }) - }) - .collect(); - let undo_point_label = events.iter().find_map(|evt| evt.undo_point_for_polling()); - if let Some(l) = undo_point_label { - // TODO-high-clip-engine Not sure if we should also add this buffered? - self.add_history_entry_immediately(l.into(), vec![]); - } - // Do occasional checks (roughly two times a second) - if self.content.poll_count % 15 == 0 { - self.apply_edited_contents_if_necessary(); - } - self.content.poll_count += 1; - // Return polled events - events - } - - fn apply_edited_contents_if_necessary(&mut self) { - for column in &mut self.content.columns { - column.apply_edited_contents_if_necessary( - &self.content.chain_equipment, - &self.content.recorder_request_sender, - &self.content.settings.overridable, - ); - } - } - - pub fn process_reaper_change_events(&mut self, events: &[ChangeEvent]) { - for event in events { - match event { - ChangeEvent::TrackRemoved(e) => { - self.remove_all_columns_associated_with_track(&e.track); - } - ChangeEvent::TrackNameChanged(e) => { - self.rename_all_columns_associated_with_track(&e.track) - } - _ => {} - } - } - } - - fn remove_all_columns_associated_with_track(&mut self, track: &Track) { - let _ = self.undoable("Remove columns due to track removal", |matrix| { - let mut removed_track = false; - matrix.content.columns.retain(|col| { - let keep = !col.uses_track(track); - if !keep { - removed_track = true; - } - keep - }); - if !removed_track { - // This can happen when the track removal was caused by the matrix itself. - // In this case, we don't want to create yet another undo point. - return Err("track removed already"); - } - matrix.notify_everything_changed(); - Ok(vec![]) - }); - } - - fn rename_all_columns_associated_with_track(&mut self, track: &Track) { - let _ = self.undoable("Rename columns due to track renaming", |matrix| { - let track_name = track.name().ok_or("track has no name")?.into_string(); - for col in matrix - .content - .columns - .iter_mut() - .filter(|c| c.uses_track(track)) - { - col.set_name(track_name.clone()); - } - matrix.notify_everything_changed(); - Ok(vec![]) - }); - } - - fn poll_history(&mut self) { - if !self.history.its_flush_time() { - return; - } - self.history.flush_buffer(self.save()); - self.emit(ClipMatrixEvent::HistoryChanged); - } - - /// Toggles the loop setting of the given slot. - pub fn toggle_looped(&mut self, address: ClipSlotAddress) -> ClipEngineResult<()> { - self.undoable("Toggle looped", |matrix| { - let kit = matrix.get_slot_kit(address)?; - let event = kit.slot.toggle_looped(kit.sender)?; - matrix.emit(ClipMatrixEvent::clip_changed( - ClipAddress::legacy(address), - event, - )); - Ok(vec![]) - }) - } - - /// Returns whether some slots in this matrix are currently playing/recording. - pub fn is_stoppable(&self) -> bool { - self.content.columns.iter().any(|c| c.is_stoppable()) - } - - /// Returns whether the given column is currently playing/recording. - pub fn column_is_stoppable(&self, index: usize) -> bool { - self.content - .columns - .get(index) - .map(|c| c.is_stoppable()) - .unwrap_or(false) - } - - /// Returns whether the given column is armed for recording. - pub fn column_is_armed_for_recording(&self, index: usize) -> bool { - self.content - .columns - .get(index) - .map(|c| c.is_armed_for_recording()) - .unwrap_or(false) - } - - /// Returns if the given track is a playback track in one of the matrix columns. - pub fn uses_playback_track(&self, track: &Track) -> bool { - self.content - .columns - .iter() - .any(|c| c.playback_track() == Ok(track)) - } - - /// Returns the number of columns in this matrix. - pub fn column_count(&self) -> usize { - self.content.columns.len() - } - - /// Returns the number of rows in this matrix. - pub fn row_count(&self) -> usize { - self.content.rows.len() - } - - /// Starts MIDI overdubbing the given clip. - pub fn midi_overdub_clip(&mut self, address: ClipAddress) -> ClipEngineResult<()> { - let args = EssentialColumnRecordClipArgs { - matrix_record_settings: &self.content.settings.clip_record_settings, - chain_equipment: &self.content.chain_equipment, - recorder_request_sender: &self.content.recorder_request_sender, - handler: &*self.content.handler, - containing_track: self.content.containing_track.as_ref(), - overridable_matrix_settings: &self.content.settings.overridable, - }; - get_column_mut(&mut self.content.columns, address.slot_address.column)?.midi_overdub_clip( - address.slot_address.row, - address.clip_index, - args, - ) - } - - /// Starts recording in the given slot. - pub fn record_slot(&mut self, address: ClipSlotAddress) -> ClipEngineResult<()> { - if self.is_recording() { - return Err("recording already"); - } - let args = EssentialColumnRecordClipArgs { - matrix_record_settings: &self.content.settings.clip_record_settings, - chain_equipment: &self.content.chain_equipment, - recorder_request_sender: &self.content.recorder_request_sender, - handler: &*self.content.handler, - containing_track: self.content.containing_track.as_ref(), - overridable_matrix_settings: &self.content.settings.overridable, - }; - get_column_mut(&mut self.content.columns, address.column())? - .record_slot(address.row(), args) - } - - /// Returns whether any column in this matrix is recording. - pub fn is_recording(&self) -> bool { - self.content.columns.iter().any(|c| c.is_recording()) - } - - /// Pauses all clips in the given slot. - pub fn pause_clip(&self, address: ClipSlotAddress) -> ClipEngineResult<()> { - get_column(&self.content.columns, address.column())?.pause_slot(address.row()); - Ok(()) - } - - /// Seeks the given slot. - pub fn seek_slot(&self, address: ClipSlotAddress, position: UnitValue) -> ClipEngineResult<()> { - get_column(&self.content.columns, address.column())?.seek_slot(address.row(), position); - Ok(()) - } - - /// Sets the volume of the given slot. - pub fn set_slot_volume( - &mut self, - address: ClipSlotAddress, - volume: Db, - ) -> ClipEngineResult<()> { - let kit = self.get_slot_kit(address)?; - let event = kit.slot.set_volume(volume, kit.sender)?; - self.emit(ClipMatrixEvent::clip_changed( - ClipAddress::legacy(address), - event, - )); - Ok(()) - } - - /// Sets the name of the given clip. - pub fn set_clip_name( - &mut self, - address: ClipAddress, - name: Option, - ) -> ClipEngineResult<()> { - self.undoable("Set clip name", |matrix| { - matrix.get_clip_mut(address)?.set_name(name); - matrix.emit(ClipMatrixEvent::clip_changed( - address, - ClipChangeEvent::Everything, - )); - Ok(vec![]) - }) - } - - pub fn set_settings(&mut self, settings: api::MatrixSettings) -> ClipEngineResult<()> { - self.undoable("Change matrix settings", |matrix| { - matrix.content.settings.overridable = - OverridableMatrixSettings::from_api(&settings.clip_play_settings); - matrix.content.settings.clip_record_settings = settings.clip_record_settings; - matrix.content.settings.common_tempo_range = settings.common_tempo_range; - for column in &mut matrix.content.columns { - column.sync_matrix_settings_to_rt_column(&matrix.content.settings); - } - matrix.emit(ClipMatrixEvent::MatrixSettingsChanged); - Ok(vec![]) - }) - } - - pub fn set_column_settings( - &mut self, - column_index: usize, - settings: api::ColumnSettings, - ) -> ClipEngineResult<()> { - self.undoable("Change column settings", |matrix| { - let column = matrix.get_column_mut(column_index)?; - column.set_settings(settings); - matrix.emit(ClipMatrixEvent::ColumnSettingsChanged(column_index)); - Ok(vec![]) - }) - } - - pub fn set_row_data(&mut self, row_index: usize, api_row: api::Row) -> ClipEngineResult<()> { - self.undoable("Change row data", |matrix| { - let row = matrix.get_row_mut(row_index)?; - *row = Row::from_api_row(api_row); - matrix.emit(ClipMatrixEvent::RowChanged(row_index)); - Ok(vec![]) - }) - } - - /// Applies most properties of the given clip to the clip at the given address. - /// - /// The following clip properties will not be changed: - /// - /// - ID - /// - Source(s) - pub fn set_clip_data( - &mut self, - address: ClipAddress, - api_clip: api::Clip, - ) -> ClipEngineResult<()> { - self.undoable("Change clip data", |matrix| { - let column = matrix.get_column_mut(address.slot_address.column)?; - column.set_clip_data(address.slot_address.row, address.clip_index, api_clip)?; - matrix.emit(ClipMatrixEvent::clip_changed( - address, - ClipChangeEvent::Everything, - )); - Ok(vec![]) - }) - } - - /// Returns the clip at the given address. - pub fn get_clip(&self, address: ClipAddress) -> ClipEngineResult<&Clip> { - self.get_slot(address.slot_address)? - .get_clip(address.clip_index) - } - - /// Returns the slot at the given address. - pub fn get_slot(&self, address: ClipSlotAddress) -> ClipEngineResult<&Slot> { - self.get_column(address.column)?.get_slot(address.row) - } - - fn get_slot_kit(&mut self, address: ClipSlotAddress) -> ClipEngineResult { - self.get_column_mut(address.column)? - .get_slot_kit_mut(address.row) - } - - fn get_slot_mut(&mut self, address: ClipSlotAddress) -> ClipEngineResult<&mut Slot> { - self.get_column_mut(address.column)? - .get_slot_mut(address.row) - } - - /// Returns the column at the given index. - pub fn get_column(&self, index: usize) -> ClipEngineResult<&Column> { - get_column(&self.content.columns, index) - } - - /// Returns the row at the given index. - pub fn get_row(&self, index: usize) -> ClipEngineResult<&Row> { - get_row(&self.content.rows, index) - } - - fn get_row_mut(&mut self, index: usize) -> ClipEngineResult<&mut Row> { - get_row_mut(&mut self.content.rows, index) - } - - fn get_column_mut(&mut self, index: usize) -> ClipEngineResult<&mut Column> { - get_column_mut(&mut self.content.columns, index) - } - - fn get_clip_mut(&mut self, address: ClipAddress) -> ClipEngineResult<&mut Clip> { - self.get_slot_mut(address.slot_address)? - .get_clip_mut(address.clip_index) - } -} - -fn get_column(columns: &[Column], index: usize) -> ClipEngineResult<&Column> { - columns.get(index).ok_or(NO_SUCH_COLUMN) -} - -fn get_row(rows: &[Row], index: usize) -> ClipEngineResult<&Row> { - rows.get(index).ok_or(NO_SUCH_ROW) -} - -fn get_row_mut(rows: &mut [Row], index: usize) -> ClipEngineResult<&mut Row> { - rows.get_mut(index).ok_or(NO_SUCH_ROW) -} - -fn get_column_mut(columns: &mut [Column], index: usize) -> ClipEngineResult<&mut Column> { - columns.get_mut(index).ok_or(NO_SUCH_COLUMN) -} - -const NO_SUCH_COLUMN: &str = "no such column"; -const NO_SUCH_ROW: &str = "no such row"; - -#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)] -pub struct ClipSlotAddress { - pub column: usize, - pub row: usize, -} - -impl ClipSlotAddress { - pub fn new(column: usize, row: usize) -> Self { - Self { column, row } - } - - pub fn column(&self) -> usize { - self.column - } - - pub fn row(&self) -> usize { - self.row - } -} - -#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)] -pub struct ClipAddress { - pub slot_address: ClipSlotAddress, - pub clip_index: usize, -} - -impl ClipAddress { - pub fn new(slot_address: ClipSlotAddress, clip_index: usize) -> Self { - Self { - slot_address, - clip_index, - } - } - - pub fn legacy(slot_address: ClipSlotAddress) -> Self { - ClipAddress { - slot_address, - clip_index: 0, - } - } -} - -#[derive(Debug)] -pub struct ClipRecordTask { - pub input: ClipRecordInput, - pub destination: ClipRecordDestination, -} - -#[derive(Debug)] -pub enum SpecificClipRecordTask { - HardwareInput(HardwareInputClipRecordTask), - FxInput(FxInputClipRecordTask), -} - -impl ClipRecordTask { - pub fn create_specific_task(self) -> SpecificClipRecordTask { - match self.input { - ClipRecordInput::HardwareInput(input) => { - let hw_task = HardwareInputClipRecordTask { - input, - destination: self.destination, - }; - SpecificClipRecordTask::HardwareInput(hw_task) - } - ClipRecordInput::FxInput(input) => { - let fx_task = FxInputClipRecordTask { - input, - destination: self.destination, - }; - SpecificClipRecordTask::FxInput(fx_task) - } - } - } -} - -#[derive(Debug)] -pub struct ClipRecordDestination { - pub column_source: WeakRtColumn, - pub slot_index: usize, - /// If this is set, it's important to write the MIDI events during the *post* phase of the audio - /// callback, otherwise the written MIDI events would be played back a moment later, which - /// would result in duplicated note playback during recording. - /// - /// If this is not set, it's important to write it in the *pre* phase because we don't want - /// to miss playing back any material when we change back from recording to ready. - pub is_midi_overdub: bool, -} - -#[derive(Debug)] -pub enum ClipRecordInput { - HardwareInput(ClipRecordHardwareInput), - FxInput(VirtualClipRecordAudioInput), -} - -impl ClipRecordInput { - /// Project is necessary to create an audio sink. - pub fn create_recording_equipment( - &self, - project: Option, - auto_quantize_midi: bool, - ) -> ClipEngineResult { - use ClipRecordInput::*; - match &self { - HardwareInput(ClipRecordHardwareInput::Midi(_)) => { - let quantization_settings = if auto_quantize_midi { - // TODO-high-clip-engine Use project quantization settings - Some(QuantizationSettings {}) - } else { - None - }; - let equipment = MidiRecordingEquipment::new(quantization_settings); - Ok(RecordingEquipment::Midi(equipment)) - } - HardwareInput(ClipRecordHardwareInput::Audio(virtual_input)) - | FxInput(virtual_input) => { - let channel_count = match virtual_input { - VirtualClipRecordAudioInput::Specific(range) => range.channel_count, - VirtualClipRecordAudioInput::Detect { channel_count } => *channel_count, - }; - let sample_rate = Reaper::get().audio_device_sample_rate()?; - let equipment = - AudioRecordingEquipment::new(project, channel_count as _, sample_rate); - Ok(RecordingEquipment::Audio(equipment)) - } - } - } -} - -#[derive(Debug)] -pub enum ClipRecordHardwareInput { - Midi(VirtualClipRecordHardwareMidiInput), - Audio(VirtualClipRecordAudioInput), -} - -#[derive(Debug)] -pub enum VirtualClipRecordHardwareMidiInput { - Specific(ClipRecordHardwareMidiInput), - Detect, -} - -#[derive(Copy, Clone, Debug)] -pub struct ClipRecordHardwareMidiInput { - pub device_id: Option, - pub channel: Option, -} - -#[derive(Debug)] -pub enum VirtualClipRecordAudioInput { - Specific(ChannelRange), - Detect { channel_count: u32 }, -} - -impl VirtualClipRecordAudioInput { - pub fn channel_offset(&self) -> ClipEngineResult { - use VirtualClipRecordAudioInput::*; - match self { - Specific(channel_range) => Ok(channel_range.first_channel_index), - Detect { .. } => Err("audio input detection not yet implemented"), - } - } -} - -pub trait ClipMatrixHandler { - fn request_recording_input(&self, task: ClipRecordTask); - fn emit_event(&self, event: ClipMatrixEvent); -} - -#[derive(Debug)] -pub enum ClipMatrixEvent { - MatrixSettingsChanged, - ColumnSettingsChanged(usize), - RowChanged(usize), - EverythingChanged, - RecordDurationChanged, - HistoryChanged, - SlotChanged(QualifiedSlotChangeEvent), - ClipChanged(QualifiedClipChangeEvent), -} - -impl ClipMatrixEvent { - pub fn slot_changed(slot_address: ClipSlotAddress, event: SlotChangeEvent) -> Self { - Self::SlotChanged(QualifiedSlotChangeEvent { - slot_address, - event, - }) - } - - pub fn clip_changed(clip_address: ClipAddress, event: ClipChangeEvent) -> Self { - Self::ClipChanged(QualifiedClipChangeEvent { - clip_address, - event, - }) - } - - pub fn undo_point_for_polling(&self) -> Option<&'static str> { - match self { - ClipMatrixEvent::SlotChanged(QualifiedSlotChangeEvent { - event: SlotChangeEvent::Clips(desc), - .. - }) => Some(desc), - _ => None, - } - } -} - -#[derive(Copy, Clone, Debug)] -pub enum ClipRecordTiming { - StartImmediatelyStopOnDemand, - StartOnBarStopOnDemand { start_bar: i32 }, - StartOnBarStopOnBar { start_bar: i32, bar_count: u32 }, -} - -#[derive(Copy, Clone)] -pub struct RecordArgs { - pub kind: RecordKind, -} - -#[derive(Copy, Clone)] -pub enum RecordKind { - Normal { - looped: bool, - timing: ClipRecordTiming, - detect_downbeat: bool, - }, - MidiOverdub, -} - -#[derive(Clone, Debug)] -pub struct WithColumn { - column_index: usize, - value: T, -} - -impl WithColumn { - fn new(column_index: usize, value: T) -> Self { - Self { - column_index, - value, - } - } - - pub fn column_index(&self) -> usize { - self.column_index - } - - pub fn value(&self) -> &T { - &self.value - } -} - -pub type SlotWithColumn<'a> = WithColumn<&'a Slot>; - -pub type SlotContentsWithColumn = WithColumn>; - -fn validate_api_matrix(matrix: &api::Matrix) -> Result<(), ValidationError> { - ensure_no_duplicate( - "column IDs", - matrix.columns.iter().flatten().map(|col| &col.id), - )?; - ensure_no_duplicate("row IDs", matrix.rows.iter().flatten().map(|row| &row.id))?; - ensure_no_duplicate( - "slot IDs", - matrix - .columns - .iter() - .flatten() - .flat_map(|col| col.slots.iter().flatten()) - .map(|slot| &slot.id), - )?; - ensure_no_duplicate( - "clip IDs", - matrix - .columns - .iter() - .flatten() - .flat_map(|col| col.slots.iter().flatten()) - .flat_map(|slot| slot.clips.iter().flatten()) - .map(|clip| &clip.id), - )?; - Ok(()) -} diff --git a/playtime-clip-engine/src/base/mod.rs b/playtime-clip-engine/src/base/mod.rs deleted file mode 100644 index 5ab7802c5..000000000 --- a/playtime-clip-engine/src/base/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -mod clip; -mod clip_edit_session; -mod clip_manifestation; -mod column; -mod history; -mod matrix; -mod row; -mod slot; - -pub use clip::*; -pub use column::*; -pub use history::*; -pub use matrix::*; -pub use slot::*; diff --git a/playtime-clip-engine/src/base/row.rs b/playtime-clip-engine/src/base/row.rs deleted file mode 100644 index 1cf2cb46c..000000000 --- a/playtime-clip-engine/src/base/row.rs +++ /dev/null @@ -1,37 +0,0 @@ -use playtime_api::persistence as api; -use playtime_api::persistence::RowId; - -#[derive(Clone, Debug)] -pub struct Row { - id: RowId, - name: Option, -} - -impl Row { - pub fn from_api_row(api_row: api::Row) -> Self { - Self { - id: api_row.id, - name: api_row.name, - } - } - - pub fn new(id: RowId) -> Self { - Self { id, name: None } - } - - pub fn save(&self) -> api::Row { - api::Row { - id: self.id.clone(), - name: self.name.clone(), - tempo: None, - time_signature: None, - } - } - - pub fn duplicate(&self) -> Self { - Self { - id: RowId::random(), - name: self.name.clone(), - } - } -} diff --git a/playtime-clip-engine/src/base/slot.rs b/playtime-clip-engine/src/base/slot.rs deleted file mode 100644 index 9849b1bcf..000000000 --- a/playtime-clip-engine/src/base/slot.rs +++ /dev/null @@ -1,1373 +0,0 @@ -use crate::base::clip_edit_session::{AudioClipEditSession, ClipEditSession, MidiClipEditSession}; -use crate::base::clip_manifestation::manifest_clip_on_track; -use crate::base::{ - Clip, ClipMatrixHandler, ClipRecordDestination, ClipRecordHardwareInput, - ClipRecordHardwareMidiInput, ClipRecordInput, ClipRecordTask, CreateRtClipEquipment, - EssentialColumnRecordClipArgs, VirtualClipRecordAudioInput, VirtualClipRecordHardwareMidiInput, -}; -use crate::conversion_util::adjust_duration_in_secs_anti_proportionally; -use crate::rt::supplier::{ - MaterialInfo, MidiOverdubOutcome, MidiOverdubSettings, MidiSequence, QuantizationSettings, - Recorder, RecordingArgs, RecordingEquipment, SupplierChain, -}; - -use crate::rt::{ - ClipChangeEvent, ClipRecordArgs, ColumnCommandSender, ColumnLoadClipArgs, ColumnLoadSlotArgs, - ColumnSetClipLoopedArgs, ColumnSetClipSettingsArgs, FillSlotMode, InternalClipPlayState, - MidiOverdubInstruction, NormalRecordingOutcome, RecordNewClipInstruction, RtClipId, RtSlot, - RtSlotId, SharedRtColumn, SlotChangeEvent, SlotRecordInstruction, SlotRuntimeData, -}; -use crate::source_util::{create_file_api_source, create_pcm_source_from_file_based_api_source}; -use crate::{clip_timeline, rt, ClipEngineResult, HybridTimeline, Timeline}; - -use helgoboss_learn::UnitValue; -use indexmap::IndexMap; -use playtime_api::persistence as api; -use playtime_api::persistence::{ - ChannelRange, ClipId, ColumnClipRecordSettings, Db, MatrixClipRecordSettings, MidiChunkSource, - MidiClipRecordMode, PositiveSecond, RecordOrigin, SlotId, -}; -use playtime_api::runtime::ClipPlayState; -use reaper_high::{Item, Project, Reaper, Take, Track, TrackRoute}; -use reaper_medium::{ - Bpm, CommandId, DurationInSeconds, Hwnd, PositionInSeconds, RecordingInput, RequiredViewMode, -}; -use std::mem; -use std::ptr::null_mut; -use xxhash_rust::xxh3::Xxh3Builder; - -#[derive(Clone, Debug)] -pub struct Slot { - id: SlotId, - rt_id: RtSlotId, - index: usize, - play_state: InternalClipPlayState, - /// If this is set, the slot contains a clip. - /// - /// This means one of the following things: - /// - /// - The clip is active and can be playing, stopped etc. - /// - The clip is active and is currently being MIDI-overdubbed. - /// - The clip is inactive, which means it's about to be replaced with different clip content - /// that's in the process of being recorded right now. - contents: Contents, - state: SlotState, - /// Route which was created temporarily for recording. - temporary_route: Option, -} - -type Contents = IndexMap; - -#[derive(Clone, Debug)] -pub struct Content { - pub clip: Clip, - pub online_data: Option, -} - -#[derive(Clone, Debug)] -pub struct OnlineData { - /// The frame count in the material info is supposed to take the section bounds into account. - pub runtime_data: SlotRuntimeData, - pub edit_session: Option, -} - -impl OnlineData { - pub fn new(rt_clip: &rt::RtClip) -> ClipEngineResult { - let data = Self { - runtime_data: SlotRuntimeData::new(rt_clip, false)?, - edit_session: None, - }; - Ok(data) - } - - pub fn midi_edit_session_mut(&mut self) -> ClipEngineResult<&mut MidiClipEditSession> { - let edit_session = self.edit_session.as_mut().ok_or("no edit session")?; - let ClipEditSession::Midi(midi_edit_session) = edit_session else { - return Err("no MIDI edit session"); - }; - Ok(midi_edit_session) - } - - /// Returns the effective length (tempo adjusted and taking the section into account). - pub fn effective_length_in_seconds( - &self, - clip: &Clip, - timeline: &HybridTimeline, - ) -> ClipEngineResult { - let timeline_tempo = timeline.tempo_at(timeline.cursor_pos()); - let tempo_factor = self.tempo_factor(clip, timeline_tempo); - let tempo_adjusted_secs = adjust_duration_in_secs_anti_proportionally( - self.runtime_data.material_info.duration(), - tempo_factor, - ); - Ok(tempo_adjusted_secs) - } - - pub fn tempo_factor(&self, clip: &Clip, timeline_tempo: Bpm) -> f64 { - let is_midi = self.runtime_data.material_info.is_midi(); - clip.tempo_factor(timeline_tempo, is_midi) - } - - pub fn proportional_position(&self) -> ClipEngineResult { - self.runtime_data.proportional_position() - } - - pub fn position_in_seconds(&self, clip: &Clip, timeline_tempo: Bpm) -> PositionInSeconds { - let tempo_factor = self.tempo_factor(clip, timeline_tempo); - self.runtime_data.position_in_seconds(tempo_factor) - } - - pub fn peak(&self) -> UnitValue { - self.runtime_data.peak() - } - - pub fn is_freezable(&self) -> bool { - // At the moment, we only freeze MIDI to audio. - self.runtime_data.material_info.is_midi() - } -} - -impl Content { - pub fn new(clip: Clip) -> Self { - Content { - clip, - online_data: None, - } - } - - pub fn duplicate(&self) -> Self { - Self { - clip: self.clip.duplicate(), - online_data: None, - } - } - - pub fn is_freezable(&self) -> bool { - if let Some(od) = self.online_data.as_ref() { - od.is_freezable() - } else { - false - } - } - - fn midi_edit_session(&self) -> ClipEngineResult<&MidiClipEditSession> { - let online_data = self.online_data.as_ref().ok_or("clip offline")?; - let edit_session = online_data.edit_session.as_ref().ok_or("no edit session")?; - let ClipEditSession::Midi(midi_edit_session) = edit_session else { - return Err("no MIDI edit session"); - }; - Ok(midi_edit_session) - } - - pub async fn freeze(&mut self, playback_track: &Track) -> ClipEngineResult<()> { - // TODO-high-clip-engine CONTINUE Get the clip-to-item layout 100% right. - // TODO-high-clip-engine CONTINUE Sync the frozen clips to the real-time thread when finished. - // TODO-high-clip-engine CONTINUE Provide a header panel action to go back to unfrozen version. - // TODO-high-clip-engine CONTINUE Provide a header panel action to go back to frozen version. - // TODO-high-clip-engine CONTINUE Don't freeze tracks whose FX chain contains ReaLearn FX only. - // TODO-high-clip-engine CONTINUE Take relevant FX offline/online when freezing/unfreezing. - let project = playback_track.project(); - let online_data = self.online_data.as_ref().ok_or("clip not online")?; - let clip = &self.clip; - let manifestation = manifest_clip_on_track(clip, online_data, playback_track)?; - project.select_item_exclusively(manifestation.item); - // Item: Apply track/take FX to items - let apply_fx_id = CommandId::new(40209); - Reaper::get() - .main_section() - .action_by_command_id(apply_fx_id) - .invoke_as_trigger(Some(project))?; - let frozen_take = manifestation - .item - .active_take() - .expect("frozen item doesn't have frozen take"); - let frozen_pcm_source = frozen_take - .source() - .expect("frozen take doesn't have a source"); - let file_name = frozen_pcm_source - .file_name() - .expect("frozen source doesn't have file name"); - let frozen_api_source = create_file_api_source(Some(project), &file_name); - self.clip - .activate_frozen_source(frozen_api_source, manifestation.tempo); - Ok(()) - } - - /// Moves cursor in REAPER's MIDI editor. - pub(crate) fn notify_pos_changed( - &self, - bpm: Bpm, - seconds: PositionInSeconds, - ) -> ClipEngineResult<()> { - let midi_edit_session = self.midi_edit_session()?; - let source = midi_edit_session.clip_manifestation().source()?; - let bps = bpm.get() / 60.0; - let beats = seconds.get() * bps; - // TODO-medium Read PPQ from MIDI file. The best thing is if we use the MidiSequence as - // source representation in the main thread and read from it. - let ppq = 960.0; - let ticks = beats * ppq; - const PCM_SOURCE_EXT_SET_PREVIEW_POS_OVERRIDE: i32 = 0xC0101; - // REAPER v6.73+dev1230 - unsafe { - source.as_raw().extended( - PCM_SOURCE_EXT_SET_PREVIEW_POS_OVERRIDE, - &ticks as *const _ as *mut _, - null_mut(), - null_mut(), - ) - }; - Ok(()) - } -} - -impl Slot { - pub fn new(id: SlotId, index: usize) -> Self { - Self { - rt_id: RtSlotId::from_slot_id(&id), - id, - index, - play_state: Default::default(), - contents: Default::default(), - state: Default::default(), - temporary_route: None, - } - } - - pub fn id(&self) -> &SlotId { - &self.id - } - - pub fn rt_id(&self) -> RtSlotId { - self.rt_id - } - - pub fn duplicate(&self, new_index: usize) -> Self { - let new_id = SlotId::random(); - Self { - rt_id: RtSlotId::from_slot_id(&new_id), - id: new_id, - index: new_index, - play_state: Default::default(), - contents: self - .contents - .values() - .map(|content| { - let duplicate_content = content.duplicate(); - (duplicate_content.clip.rt_id(), duplicate_content) - }) - .collect(), - state: Default::default(), - temporary_route: None, - } - } - - pub fn is_empty(&self) -> bool { - self.contents.is_empty() && !self.state.is_pretty_much_recording() - } - - pub fn index(&self) -> usize { - self.index - } - - pub(crate) fn set_index(&mut self, new_index: usize) { - self.index = new_index; - } - - /// Returns `None` if this slot doesn't need to be saved (because it's empty). - pub fn save(&self) -> Option { - let clips: Vec<_> = self - .contents - .values() - .filter_map(|content| content.clip.save().ok()) - .collect(); - if clips.is_empty() { - return None; - } - let api_slot = api::Slot { - id: self.id.clone(), - row: self.index, - clip_old: None, - clips: Some(clips), - }; - Some(api_slot) - } - - /// Loads the given clips into the slot but doesn't bring them online yet. - /// - /// Keeps the clip IDs, doesn't generate new ones. - pub fn load(&mut self, api_clips: Vec) { - self.contents = load_api_clips(api_clips, IdMode::KeepIds).collect(); - } - - /// Brings the previously loaded clips online. - pub fn bring_online(&mut self, equipment: CreateRtClipEquipment) -> RtSlot { - let rt_clips = self.contents.values_mut().filter_map(|content| { - let rt_clip = content.clip.create_real_time_clip(equipment).ok()?; - let online_data = OnlineData::new(&rt_clip).ok()?; - content.online_data = Some(online_data); - Some((rt_clip.id(), rt_clip)) - }); - RtSlot::new(self.rt_id, rt_clips.collect()) - } - - pub fn apply_edited_contents_if_necessary( - &mut self, - equipment: CreateRtClipEquipment, - column_command_sender: &ColumnCommandSender, - ) { - for (i, content) in self.contents.values_mut().enumerate() { - let Some(online_data) = content.online_data.as_mut() else { - continue; - }; - if apply_edited_content_if_necessary(online_data, &mut content.clip) == Ok(true) { - let Ok(rt_clip) = content.clip.create_real_time_clip(equipment) else { - continue; - }; - let Ok(runtime_data) = SlotRuntimeData::new(&rt_clip, false) else { - continue; - }; - online_data.runtime_data = runtime_data; - let args = ColumnLoadClipArgs { - slot_index: self.index, - clip_index: i, - clip: rt_clip, - }; - column_command_sender.load_clip(args); - } - } - } - - /// Immediately syncs to real-time column. - #[allow(clippy::too_many_arguments)] - pub fn fill( - &mut self, - api_clips: Vec, - equipment: CreateRtClipEquipment, - rt_command_sender: &ColumnCommandSender, - fill_mode: FillSlotMode, - id_mode: IdMode, - ) { - // Load clips - let contents = load_api_clips(api_clips, id_mode); - match fill_mode { - FillSlotMode::Add => { - self.contents.extend(contents); - } - FillSlotMode::Replace => { - self.contents = contents.collect(); - } - } - // Bring slot online - let rt_slot = self.bring_online(equipment); - // Send real-time slot to the real-time column - let args = ColumnLoadSlotArgs { - slot_index: self.index, - clips: rt_slot.clips, - }; - rt_command_sender.load_slot(Box::new(Some(args))); - } - - /// Immediately syncs to real-time column. - pub fn set_clip_data( - &mut self, - clip_index: usize, - api_clip: api::Clip, - rt_command_sender: &ColumnCommandSender, - ) -> ClipEngineResult<()> { - // Apply data to existing clip - let content = get_content_mut(&mut self.contents, clip_index)?; - let clip = &mut content.clip; - let new_clip = Clip::load(api_clip); - clip.set_data(new_clip); - // Sync to real-time column - let args = ColumnSetClipSettingsArgs { - slot_index: self.index, - clip_index, - settings: *clip.rt_settings(), - }; - rt_command_sender.set_clip_settings(args); - Ok(()) - } - - pub fn is_recording(&self) -> bool { - self.state.is_pretty_much_recording() - } - - pub(crate) fn midi_overdub_clip( - &mut self, - clip_index: usize, - args: EssentialSlotRecordClipArgs, - ) -> ClipEngineResult<()> { - let content = self.get_content(clip_index)?; - let overdub_instruction = self.create_midi_overdub_instruction_internal( - content, - args.column_args.matrix_record_settings, - args.recording_track.project(), - )?; - self.record_or_overdub_internal(args, Some(overdub_instruction)) - } - - /// Chooses dynamically whether to do normal recording or overdub. - pub(crate) fn record_clip( - &mut self, - args: EssentialSlotRecordClipArgs, - ) -> ClipEngineResult<()> { - let overdub_instruction = self.create_midi_overdub_instruction_if_applicable( - args.column_args.matrix_record_settings, - args.recording_track.project(), - ); - self.record_or_overdub_internal(args, overdub_instruction) - } - - /// Decides whether to use MIDI overdub recording and if yes, returns an instruction. - /// - /// Decides for MIDI overdub under the following conditions: - /// - /// - MIDI recording mode in matrix record settings is set to overdub/replace - /// - Slot has exactly one clip - fn create_midi_overdub_instruction_if_applicable( - &self, - matrix_record_settings: &MatrixClipRecordSettings, - project: Project, - ) -> Option { - if matrix_record_settings.midi_settings.record_mode == MidiClipRecordMode::Normal { - return None; - } - if self.contents.len() > 1 { - return None; - } - let (_, content) = self.contents.first()?; - self.create_midi_overdub_instruction_internal(content, matrix_record_settings, project) - .ok() - } - - fn create_midi_overdub_instruction_internal( - &self, - content: &Content, - matrix_record_settings: &MatrixClipRecordSettings, - project: Project, - ) -> ClipEngineResult { - let Some(online_data) = &content.online_data else { - return Err("clip not online"); - }; - if !online_data.runtime_data.material_info.is_midi() { - return Err("not a MIDI clip"); - } - let instruction = create_midi_overdub_instruction( - 0, - matrix_record_settings.midi_settings.record_mode, - matrix_record_settings.midi_settings.auto_quantize, - content.clip.api_source(), - Some(project), - )?; - Ok(instruction) - } - - #[allow(clippy::too_many_arguments)] - fn record_or_overdub_internal( - &mut self, - args: EssentialSlotRecordClipArgs, - desired_midi_overdub_instruction: Option, - ) -> ClipEngineResult<()> { - if self.state.is_pretty_much_recording() { - return Err("recording already"); - } - if self.contents.len() > 1 { - return Err("recording on slots with multiple clips is not supported"); - } - // Check preconditions and prepare stuff. - let project = args.recording_track.project(); - if self.play_state.is_somehow_recording() { - return Err("recording already according to play state"); - } - let (common_stuff, mode_specific_stuff) = create_record_stuff( - self.index, - args.column_args.containing_track, - args.column_args.matrix_record_settings, - args.column_record_settings, - args.recording_track, - args.rt_column, - desired_midi_overdub_instruction, - )?; - match mode_specific_stuff { - ModeSpecificRecordStuff::FromScratch(from_scratch_stuff) => { - self.record_from_scratch(args, project, common_stuff, from_scratch_stuff) - } - ModeSpecificRecordStuff::MidiOverdub(midi_overdub_stuff) => self - .record_as_midi_overdub( - args.column_command_sender, - args.column_args.handler, - common_stuff, - midi_overdub_stuff, - ), - } - } - - fn record_from_scratch( - &mut self, - args: EssentialSlotRecordClipArgs, - project: Project, - common_stuff: CommonRecordStuff, - specific_stuff: FromScratchRecordStuff, - ) -> ClipEngineResult<()> { - // Build slot instruction - let clip_args = ClipRecordArgs { - recording_equipment: specific_stuff.recording_equipment, - settings: *args.column_args.matrix_record_settings, - }; - let (clip_id, instruction) = if let Some((_, content)) = self.contents.first() { - // There's a clip already. That makes it easy because we have the clip struct - // already, including the complete clip supplier chain, and can reuse it. - ( - content.clip.id().clone(), - SlotRecordInstruction::ExistingClip(clip_args), - ) - } else { - // There's no clip yet so we need to create the clip including the complete supplier - // chain from scratch. We need to do create much of the stuff here already because - // we must not allocate in the real-time thread. However, we can't create the - // complete clip because we don't have enough information (block length, timeline - // frame rate) available at this point to resolve the initial recording position. - let recording_args = RecordingArgs::from_stuff( - Some(project), - args.rt_column_settings, - args.column_args.overridable_matrix_settings, - &clip_args.settings, - clip_args.recording_equipment, - ); - let timeline = clip_timeline(Some(project), false); - let timeline_cursor_pos = timeline.cursor_pos(); - let recorder = Recorder::recording( - recording_args, - args.column_args.recorder_request_sender.clone(), - ); - let supplier_chain = - SupplierChain::new(recorder, args.column_args.chain_equipment.clone())?; - let clip_id = ClipId::random(); - let new_clip_instruction = RecordNewClipInstruction { - clip_id: RtClipId::from_clip_id(&clip_id), - supplier_chain, - project: Some(project), - shared_pos: Default::default(), - shared_peak: Default::default(), - timeline, - timeline_cursor_pos, - settings: *args.column_args.matrix_record_settings, - }; - ( - clip_id, - SlotRecordInstruction::NewClip(new_clip_instruction), - ) - }; - let next_state = SlotState::RequestedRecording(RequestedRecordingState { clip_id }); - // Above code was only for checking preconditions and preparing stuff. - // Here we can't fail anymore, do the actual state changes and distribute tasks. - self.initiate_recording( - args.column_command_sender, - args.column_args.handler, - next_state, - instruction, - common_stuff.temporary_route, - common_stuff.task, - ); - Ok(()) - } - - fn record_as_midi_overdub( - &mut self, - column_command_sender: &ColumnCommandSender, - handler: &dyn ClipMatrixHandler, - common_stuff: CommonRecordStuff, - specific_stuff: MidiOverdubRecordStuff, - ) -> ClipEngineResult<()> { - let content = get_content_mut(&mut self.contents, specific_stuff.instruction.clip_index)?; - content - .online_data - .as_ref() - .ok_or("clip to be overdubbed is offline")?; - self.initiate_recording( - column_command_sender, - handler, - SlotState::RequestedOverdubbing, - SlotRecordInstruction::MidiOverdub(specific_stuff.instruction), - common_stuff.temporary_route, - common_stuff.task, - ); - Ok(()) - } - - fn initiate_recording( - &mut self, - column_command_sender: &ColumnCommandSender, - handler: &dyn ClipMatrixHandler, - next_state: SlotState, - instruction: SlotRecordInstruction, - temporary_route: Option, - task: ClipRecordTask, - ) { - // 1. The main slot needs to know what's going on. - self.state = next_state; - // 2. The real-time slot needs to be prepared. - column_command_sender.record_clip(self.index, instruction); - // 3. The context needs to deliver our input. - handler.request_recording_input(task); - // 4. When recording track output, we must set up a send. - // TODO-medium For reasons of clean rollback, we should create the route here, not above. - self.temporary_route = temporary_route; - } - - fn remove_temporary_route(&mut self) { - if let Some(route) = self.temporary_route.take() { - route.delete().unwrap(); - } - } - - /// Adjusts the section length of all contained clips that are online. - /// - /// # Errors - /// - /// Returns an error if this slot doesn't contain any clip. - pub fn adjust_section_length( - &mut self, - factor: f64, - column_command_sender: &ColumnCommandSender, - ) -> ClipEngineResult<()> { - for (i, content) in get_contents_mut(&mut self.contents)? - .values_mut() - .enumerate() - { - let Some(online_data) = content.online_data.as_mut() else { - continue; - }; - let current_section = content.clip.section(); - let current_length = if let Some(current_length) = current_section.length { - current_length.get() - } else { - online_data.runtime_data.material_info.duration().get() - }; - let new_section = api::Section { - start_pos: current_section.start_pos, - length: Some(PositiveSecond::new(current_length * factor)?), - }; - content.clip.set_section(new_section); - // TODO-high-multiclips CONTINUE Pass clip index - column_command_sender.set_clip_section(self.index, i, new_section); - } - Ok(()) - } - - /// Returns whether this slot contains freezable clips. - pub fn is_freezeable(&self) -> bool { - self.contents.values().any(|content| content.is_freezable()) - } - - /// Freezes all clips in this slot. - /// - /// Doesn't error if the slot is empty. - pub async fn freeze(&mut self, playback_track: &Track) -> ClipEngineResult<()> { - for content in self.contents.values_mut() { - if !content.is_freezable() { - continue; - } - content.freeze(playback_track).await?; - } - Ok(()) - } - - /// Starts editing of all online clips contained in this slot. - pub fn start_editing_clip( - &mut self, - clip_index: usize, - playback_track: &Track, - ) -> ClipEngineResult<()> { - let content = get_content_mut(&mut self.contents, clip_index)?; - let online_data = content.online_data.as_mut().ok_or("clip not online")?; - let is_midi = online_data.runtime_data.material_info.is_midi(); - let clip_manifestation = - manifest_clip_on_track(&content.clip, online_data, playback_track)?; - let edit_session = if is_midi { - // open_midi_editor_via_action(temporary_project, item); - let hwnd = open_midi_editor_directly(playback_track, clip_manifestation.take)?; - ClipEditSession::Midi(MidiClipEditSession::new(clip_manifestation, hwnd)) - } else { - open_audio_editor(playback_track.project(), clip_manifestation.item)?; - ClipEditSession::Audio(AudioClipEditSession { clip_manifestation }) - }; - online_data.edit_session = Some(edit_session); - Ok(()) - } - - /// Stops editing of all online clips contained in this slot. - pub fn stop_editing_clip(&mut self, clip_index: usize) -> ClipEngineResult<()> { - let content = get_content_mut(&mut self.contents, clip_index)?; - let online_data = content.online_data.as_mut().ok_or("clip not online")?; - online_data.edit_session = None; - Ok(()) - } - - pub fn is_editing_clip(&self, clip_index: usize) -> bool { - self.get_content(clip_index) - .ok() - .and_then(|c| c.online_data.as_ref()) - .map(|d| d.edit_session.is_some()) - .unwrap_or(false) - } - - /// Returns all clips in this slot. Can be empty. - pub fn clips(&self) -> impl Iterator { - self.contents.values().map(|c| &c.clip) - } - - /// Returns all clips in this slot, converted to standalone API clips. Can be empty. - pub fn api_clips(&self, _permanent_project: Option) -> Vec { - self.contents - .values() - .filter_map(|c| c.clip.save().ok()) - .collect() - } - - /// Returns clip at the given index. - /// - /// # Errors - /// - /// Returns an error if there's no clip at that index. - pub fn get_clip(&self, index: usize) -> ClipEngineResult<&Clip> { - Ok(&self.get_content(index)?.clip) - } - - /// Returns the content at the given clip index. - fn get_content(&self, index: usize) -> ClipEngineResult<&Content> { - Ok(self.contents.get_index(index).ok_or(CLIP_DOESNT_EXIST)?.1) - } - - /// Returns the clip at the given index, mutable. - pub fn get_clip_mut(&mut self, index: usize) -> ClipEngineResult<&mut Clip> { - let content = get_content_mut(&mut self.contents, index)?; - Ok(&mut content.clip) - } - - /// Returns volume of the first clip. - /// - /// # Errors - /// - /// Returns an error if this slot is empty. - pub fn volume(&self) -> ClipEngineResult { - Ok(self.get_content(0)?.clip.volume()) - } - - /// Returns looped setting of the first clip. - /// - /// # Errors - /// - /// Returns an error if this slot is empty. - pub fn looped(&self) -> ClipEngineResult { - Ok(self.get_content(0)?.clip.looped()) - } - - /// Sets volume of all clips. - /// - /// # Errors - /// - /// Returns an error if this slot is empty. - pub fn set_volume( - &mut self, - volume: Db, - column_command_sender: &ColumnCommandSender, - ) -> ClipEngineResult { - for (i, content) in get_contents_mut(&mut self.contents)? - .values_mut() - .enumerate() - { - content.clip.set_volume(volume); - column_command_sender.set_clip_volume(self.index, i, volume); - } - Ok(ClipChangeEvent::Volume(volume)) - } - - /// Toggles the looped setting of all clips, using the setting of the first one as reference. - /// - /// # Errors - /// - /// Returns an error if this slot is empty. - pub fn toggle_looped( - &mut self, - column_command_sender: &ColumnCommandSender, - ) -> ClipEngineResult { - let new_looped_value = !self.get_content(0)?.clip.looped(); - for (i, content) in self.contents.values_mut().enumerate() { - content.clip.set_looped(new_looped_value); - let args = ColumnSetClipLoopedArgs { - slot_index: self.index, - clip_index: i, - looped: new_looped_value, - }; - column_command_sender.set_clip_looped(args); - } - Ok(ClipChangeEvent::Looped(new_looped_value)) - } - - pub fn play_state(&self) -> InternalClipPlayState { - use SlotState::*; - match &self.state { - Normal => self.play_state, - RequestedOverdubbing | RequestedRecording(_) => { - ClipPlayState::ScheduledForRecordingStart.into() - } - // TODO-high-clip-engine Couldn't we use the slot play state here, too? - Recording(s) => s.runtime_data.play_state, - } - } - - /// Returns whether this slot is playing/recording something at the moment and therefore can be - /// stopped. - pub fn is_stoppable(&self) -> bool { - self.play_state().is_stoppable() - } - - pub fn update_play_state(&mut self, play_state: InternalClipPlayState) { - self.play_state = play_state; - } - - pub fn update_material_info( - &mut self, - clip_id: RtClipId, - material_info: MaterialInfo, - ) -> ClipEngineResult<()> { - let content = self - .contents - .get_mut(&clip_id) - .ok_or("clip doesn't exist")?; - let online_data = content.online_data.as_mut().ok_or("clip is not online")?; - online_data.runtime_data.material_info = material_info; - Ok(()) - } - - /// Returns the currently relevant content. - /// - /// If the slot is recording, that's the runtime data of the recording. Otherwise, it's - /// the content for each clip. Doesn't error if there's no clip. - pub fn relevant_contents( - &self, - ) -> RelevantContent + ExactSizeIterator> { - if let SlotState::Recording(s) = &self.state { - RelevantContent::Recording(&s.runtime_data) - } else { - RelevantContent::Normal(self.contents.values()) - } - } - - pub fn notify_recording_request_acknowledged( - &mut self, - result: Result, SlotRecordInstruction>, - ) -> ClipEngineResult<()> { - let runtime_data = match result { - Ok(r) => r, - Err(_) => { - debug!("Recording request acknowledged with negative result"); - self.remove_temporary_route(); - self.state = SlotState::Normal; - return Ok(()); - } - }; - use SlotState::*; - match mem::replace(&mut self.state, Normal) { - Normal => Err("recording was not requested"), - RequestedOverdubbing => { - debug!("Acknowledged overdubbing"); - Ok(()) - } - RequestedRecording(s) => { - debug!("Acknowledged real recording"); - let runtime_data = runtime_data.expect("no runtime data sent back"); - self.state = { - // This must be a real recording, not overdub. - let recording_state = RecordingState { - clip_id: s.clip_id, - runtime_data, - }; - SlotState::Recording(recording_state) - }; - Ok(()) - } - Recording(_) => Err("recording already"), - } - } - - pub fn notify_midi_overdub_finished( - &mut self, - clip_id: RtClipId, - outcome: MidiOverdubOutcome, - ) -> ClipEngineResult<()> { - self.remove_temporary_route(); - get_content_mut_by_id(&mut self.contents, clip_id)? - .clip - .notify_midi_overdub_finished(outcome); - Ok(()) - } - - pub fn clear(&mut self) { - self.contents.clear(); - } - - pub fn slot_cleared(&mut self) -> Option { - if self.is_empty() { - return None; - } - self.contents.clear(); - Some(SlotChangeEvent::Clips("clip removed")) - } - - pub fn notify_normal_recording_finished( - &mut self, - outcome: NormalRecordingOutcome, - temporary_project: Option, - recording_track: &Track, - ) -> ClipEngineResult { - self.remove_temporary_route(); - match outcome { - NormalRecordingOutcome::Committed(recording) => match mem::take(&mut self.state) { - SlotState::Normal => Err("slot was not recording"), - SlotState::RequestedOverdubbing => Err("requested overdubbing"), - SlotState::RequestedRecording(_) => Err("clip recording was not yet acknowledged"), - SlotState::Recording(mut s) => { - let clip = Clip::from_recording( - s.clip_id, - recording.kind_specific, - recording.clip_settings, - temporary_project, - recording_track, - )?; - s.runtime_data.material_info = recording.material_info; - debug!("Record slot with clip: {:#?}", &clip); - let content = Content { - clip, - online_data: Some(OnlineData { - runtime_data: s.runtime_data, - edit_session: None, - }), - }; - self.contents.clear(); - self.contents.insert(content.clip.rt_id(), content); - self.state = SlotState::Normal; - Ok(SlotChangeEvent::Clips("clip recording finished")) - } - }, - NormalRecordingOutcome::Canceled => { - debug!("Recording canceled"); - self.state = SlotState::Normal; - Ok(SlotChangeEvent::Clips("recording canceled")) - } - } - } -} - -#[derive(Clone, Debug, Default)] -enum SlotState { - /// Either empty or filled. - /// - /// Can be overdubbing (check play state). - #[default] - Normal, - /// Used to prevent double invocation during overdubbing acknowledgement phase. - RequestedOverdubbing, - /// Used to prevent double invocation during recording acknowledgement phase. - RequestedRecording(RequestedRecordingState), - /// Recording (not overdubbing). - Recording(RecordingState), -} - -#[derive(Clone, Debug)] -struct RequestedRecordingState { - clip_id: ClipId, -} - -#[derive(Clone, Debug)] -struct RecordingState { - clip_id: ClipId, - runtime_data: SlotRuntimeData, -} - -impl SlotState { - pub fn is_pretty_much_recording(&self) -> bool { - !matches!(self, Self::Normal) - } -} - -fn get_contents_mut(contents: &mut Contents) -> ClipEngineResult<&mut Contents> { - if contents.is_empty() { - return Err(SLOT_NOT_FILLED); - } - Ok(contents) -} - -fn get_content_mut(contents: &mut Contents, clip_index: usize) -> ClipEngineResult<&mut Content> { - let content = contents - .get_index_mut(clip_index) - .ok_or(CLIP_DOESNT_EXIST)? - .1; - Ok(content) -} - -fn get_content_mut_by_id( - contents: &mut Contents, - clip_id: RtClipId, -) -> ClipEngineResult<&mut Content> { - contents.get_mut(&clip_id).ok_or(CLIP_DOESNT_EXIST) -} - -struct CommonRecordStuff { - task: ClipRecordTask, - temporary_route: Option, -} - -// TODO-high-clip-engine Maybe fix the clippy warning -#[allow(clippy::large_enum_variant)] -enum ModeSpecificRecordStuff { - FromScratch(FromScratchRecordStuff), - MidiOverdub(MidiOverdubRecordStuff), -} - -struct FromScratchRecordStuff { - recording_equipment: RecordingEquipment, -} - -struct MidiOverdubRecordStuff { - instruction: MidiOverdubInstruction, -} - -fn create_record_stuff( - slot_index: usize, - containing_track: Option<&Track>, - matrix_record_settings: &MatrixClipRecordSettings, - column_settings: &ColumnClipRecordSettings, - recording_track: &Track, - column_source: &SharedRtColumn, - desired_midi_overdub_instruction: Option, -) -> ClipEngineResult<(CommonRecordStuff, ModeSpecificRecordStuff)> { - let (input, temporary_route) = { - use RecordOrigin::*; - match &column_settings.origin { - TrackInput => { - debug!("Input: track input"); - let track_input = recording_track - .recording_input() - .ok_or("track doesn't have any recording input")?; - let hw_input = translate_track_input_to_hw_input(track_input)?; - (ClipRecordInput::HardwareInput(hw_input), None) - } - TrackAudioOutput => { - debug!("Input: track audio output"); - let containing_track = containing_track.ok_or( - "can't recording track audio output if Playtime runs in monitoring FX chain", - )?; - let route = recording_track.add_send_to(containing_track); - // TODO-medium At the moment, we support stereo routes only. In order to support - // multi-channel routes, the user must increase the ReaLearn track channel count. - // And we have to: - // 1. Create multi-channel sends (I_SRCCHAN, I_DSTCHAN) - // 2. Make sure our ReaLearn instance has enough input pins. Roughly like this: - // // In VST plug-in - // let low_context = reaper_low::VstPluginContext::new(self.host.raw_callback().unwrap()); - // let context = VstPluginContext::new(&low_context); - // let channel_count = unsafe { - // context.request_containing_track_channel_count( - // NonNull::new(self.host.raw_effect()).unwrap(), - // ) - // }; - // unsafe { - // (*self.host.raw_effect()).numInputs = channel_count; - // } - let channel_range = ChannelRange { - first_channel_index: 0, - channel_count: recording_track.channel_count(), - }; - let fx_input = VirtualClipRecordAudioInput::Specific(channel_range); - (ClipRecordInput::FxInput(fx_input), Some(route)) - } - FxAudioInput(range) => { - debug!("Input: FX audio input"); - let fx_input = VirtualClipRecordAudioInput::Specific(*range); - (ClipRecordInput::FxInput(fx_input), None) - } - } - }; - let recording_equipment = input.create_recording_equipment( - Some(recording_track.project()), - matrix_record_settings.midi_settings.auto_quantize, - )?; - let final_midi_overdub_instruction = if recording_equipment.is_midi() { - desired_midi_overdub_instruction - } else { - // Want overdub but we have a audio input, so don't use overdub mode after all. - None - }; - let task = ClipRecordTask { - input, - destination: ClipRecordDestination { - column_source: column_source.downgrade(), - slot_index, - is_midi_overdub: final_midi_overdub_instruction.is_some(), - }, - }; - let mode_specific_stuff = if let Some(instruction) = final_midi_overdub_instruction { - ModeSpecificRecordStuff::MidiOverdub(MidiOverdubRecordStuff { instruction }) - } else { - ModeSpecificRecordStuff::FromScratch(FromScratchRecordStuff { - recording_equipment, - }) - }; - let common_stuff = CommonRecordStuff { - task, - temporary_route, - }; - Ok((common_stuff, mode_specific_stuff)) -} - -const SLOT_NOT_FILLED: &str = "slot not filled"; - -fn translate_track_input_to_hw_input( - track_input: RecordingInput, -) -> ClipEngineResult { - let hw_input = match track_input { - RecordingInput::Mono(i) => { - let range = ChannelRange { - first_channel_index: i, - channel_count: 1, - }; - ClipRecordHardwareInput::Audio(VirtualClipRecordAudioInput::Specific(range)) - } - RecordingInput::Stereo(i) => { - let range = ChannelRange { - first_channel_index: i, - channel_count: 2, - }; - ClipRecordHardwareInput::Audio(VirtualClipRecordAudioInput::Specific(range)) - } - RecordingInput::Midi { device_id, channel } => { - let input = ClipRecordHardwareMidiInput { device_id, channel }; - ClipRecordHardwareInput::Midi(VirtualClipRecordHardwareMidiInput::Specific(input)) - } - _ => return Err(""), - }; - Ok(hw_input) -} - -pub fn create_midi_overdub_instruction( - clip_index: usize, - mode: MidiClipRecordMode, - auto_quantize: bool, - api_source: &api::Source, - temporary_project: Option, -) -> ClipEngineResult { - let quantization_settings = if auto_quantize { - // TODO-high-clip-engine Use project quantization settings - Some(QuantizationSettings {}) - } else { - None - }; - let (mirror_source, source_replacement) = match api_source { - api::Source::File(file_based_api_source) => { - // We have a file-based MIDI source only. In the real-time clip, we need to replace - // it with an equivalent in-project MIDI source first. Create it! - let in_project_source = create_pcm_source_from_file_based_api_source( - temporary_project, - file_based_api_source, - true, - )?; - let chunk = in_project_source.state_chunk(); - let midi_sequence = MidiSequence::parse_from_reaper_midi_chunk(&chunk) - .map_err(|_| "couldn't parse MidiSequence from API MIDI file")?; - (midi_sequence.clone(), Some(midi_sequence)) - } - api::Source::MidiChunk(s) => { - // We have an in-project MIDI source already. Great! - let midi_sequence = MidiSequence::parse_from_reaper_midi_chunk(&s.chunk) - .map_err(|_| "couldn't parse MidiSequence from API chunk")?; - (midi_sequence, None) - } - }; - // TODO-high We need to enlarge capacity of the MidiSequences in the recorder in order to avoid - // allocation. That also means we should probably ALWAYS send a source replacement! - let instruction = MidiOverdubInstruction { - clip_index, - source_replacement, - settings: MidiOverdubSettings { - mode, - quantization_settings, - mirror_source, - }, - }; - Ok(instruction) -} - -fn open_midi_editor_directly(editor_track: &Track, take: Take) -> ClipEngineResult { - let source = take.source().ok_or("take has no source")?; - unsafe { - source - .as_raw() - .ext_open_editor(Reaper::get().main_window(), editor_track.index().unwrap()) - .unwrap(); - } - configure_midi_editor(); - let hwnd = Reaper::get() - .medium_reaper() - .midi_editor_get_active() - .ok_or("couldn't find focused MIDI editor")?; - Ok(hwnd) -} - -#[allow(dead_code)] -fn open_midi_editor_via_action(project: Project, item: Item) -> ClipEngineResult<()> { - project.select_item_exclusively(item); - // Open built-in MIDI editor - let open_midi_editor_command_id = CommandId::new(40153); - // Open items in primary external editor - // let open_midi_editor_command_id = CommandId::new(40109); - Reaper::get() - .main_section() - .action_by_command_id(open_midi_editor_command_id) - .invoke_as_trigger(item.project())?; - configure_midi_editor(); - Ok(()) -} - -fn open_audio_editor(project: Project, item: Item) -> ClipEngineResult<()> { - project.select_item_exclusively(item); - // Toggle zoom to selected items - let open_midi_editor_command_id = CommandId::new(41622); - Reaper::get() - .main_section() - .action_by_command_id(open_midi_editor_command_id) - .invoke_as_trigger(item.project())?; - Ok(()) -} - -fn configure_midi_editor() { - let reaper = Reaper::get().medium_reaper(); - let required_view_mode = RequiredViewMode::Normal; - // // Switch piano roll time base to "Source beats" if not already happened. - // let midi_editor_section_id = SectionId::new(32060); - // let source_beats_command_id = CommandId::new(40470); - // if reaper.get_toggle_command_state_ex(midi_editor_section_id, source_beats_command_id) - // != Some(true) - // { - // let _ = - // reaper.midi_editor_last_focused_on_command(source_beats_command_id, required_view_mode); - // } - // Zoom to content - let zoom_command_id = CommandId::new(40466); - let _ = reaper.midi_editor_last_focused_on_command(zoom_command_id, required_view_mode); -} - -const CLIP_DOESNT_EXIST: &str = "clip doesn't exist"; - -pub enum RelevantContent<'a, T> { - /// Contains one content per clip. - Normal(T), - /// Contains runtime data of the recording. - Recording(&'a SlotRuntimeData), -} - -impl<'a, T: Iterator> RelevantContent<'a, T> { - /// Returns the proportional position of the recording or first clip. - pub fn primary_proportional_position(self) -> ClipEngineResult { - match self { - RelevantContent::Normal(mut contents) => { - // We use the position of the first clip only. - contents - .next() - .ok_or("slot empty")? - .online_data - .as_ref() - .ok_or("clip offline")? - .proportional_position() - } - RelevantContent::Recording(runtime_data) => runtime_data.proportional_position(), - } - } - - /// Returns the position of the recording or first clip. - pub fn primary_position_in_seconds( - self, - timeline_tempo: Bpm, - ) -> ClipEngineResult { - let res = match self { - RelevantContent::Normal(mut contents) => { - // We use the position of the first clip only. - let content = contents.next().ok_or("slot empty")?; - content - .online_data - .as_ref() - .ok_or("clip offline")? - .position_in_seconds(&content.clip, timeline_tempo) - } - RelevantContent::Recording(runtime_data) => { - runtime_data.position_in_seconds_during_recording(timeline_tempo) - } - }; - Ok(res) - } -} - -fn load_api_clips( - api_clips: Vec, - id_mode: IdMode, -) -> impl Iterator { - api_clips.into_iter().map(move |mut api_clip| { - if id_mode == IdMode::AssignNewIds { - api_clip.id = ClipId::random(); - } - let clip = Clip::load(api_clip); - (clip.rt_id(), Content::new(clip)) - }) -} - -#[derive(Copy, Clone, Eq, PartialEq, Debug)] -pub enum IdMode { - /// Keep object IDs. - /// - /// This should be used if it's important to track the "journey" of objects. This is then - /// used in the real-time column to decide whether a change affects an existing clip or is - /// something new. This in turn makes interruption-free playing possible in many cases, e.g. - /// undo/redo. - KeepIds, - /// Assign new objects IDs. - /// - /// This should be used whenever there's the danger of duplicate IDs. The rule is: - /// IDs must be unique across the whole matrix. E.g. clip IDs should not just be unique within - /// one slot but across all columns! - AssignNewIds, -} - -pub struct EssentialSlotRecordClipArgs<'a> { - pub column_args: EssentialColumnRecordClipArgs<'a>, - pub column_record_settings: &'a ColumnClipRecordSettings, - pub rt_column_settings: &'a rt::RtColumnSettings, - pub recording_track: &'a Track, - pub rt_column: &'a SharedRtColumn, - pub column_command_sender: &'a ColumnCommandSender, -} - -/// Returns `true` if the clip source has changed and needs to be synced to the real-time -/// column. -pub fn apply_edited_content_if_necessary( - online_data: &mut OnlineData, - clip: &mut Clip, -) -> ClipEngineResult { - // Check if content changed - let midi_edit_session = online_data.midi_edit_session_mut()?; - let changed = if midi_edit_session.update_source_hash() { - let chunk = midi_edit_session.clip_manifestation().state_chunk()?; - let updated_source = api::Source::MidiChunk(MidiChunkSource { chunk }); - clip.set_source(updated_source); - true - } else { - false - }; - // Remove edit session if MIDI editor closed - if !midi_edit_session.midi_editor_window().is_open() { - online_data.edit_session = None; - } - Ok(changed) -} diff --git a/playtime-clip-engine/src/conversion_util.rs b/playtime-clip-engine/src/conversion_util.rs deleted file mode 100644 index fb5c1e5e0..000000000 --- a/playtime-clip-engine/src/conversion_util.rs +++ /dev/null @@ -1,62 +0,0 @@ -use reaper_medium::{DurationInSeconds, Hz, PositionInSeconds}; - -pub fn convert_duration_in_seconds_to_frames(seconds: DurationInSeconds, sample_rate: Hz) -> usize { - adjust_proportionally_positive(seconds.get(), sample_rate.get()) -} - -pub fn convert_position_in_seconds_to_frames(seconds: PositionInSeconds, sample_rate: Hz) -> isize { - adjust_proportionally(seconds.get(), sample_rate.get()) -} - -pub fn adjust_proportionally_positive(value: f64, factor: f64) -> usize { - adjust_proportionally(value, factor) as usize -} - -pub fn adjust_proportionally(value: f64, factor: f64) -> isize { - (value * factor).round() as isize -} - -pub fn adjust_pos_in_secs_anti_proportionally( - pos: PositionInSeconds, - factor: f64, -) -> PositionInSeconds { - PositionInSeconds::new(pos.get() / factor) -} - -pub fn adjust_duration_in_secs_anti_proportionally( - pos: DurationInSeconds, - factor: f64, -) -> DurationInSeconds { - DurationInSeconds::new(pos.get() / factor) -} - -#[allow(dead_code)] -pub fn adjust_duration_in_secs_proportionally( - pos: DurationInSeconds, - factor: f64, -) -> DurationInSeconds { - DurationInSeconds::new(pos.get() * factor) -} - -pub fn convert_duration_in_frames_to_seconds( - frame_count: usize, - sample_rate: Hz, -) -> DurationInSeconds { - DurationInSeconds::new(frame_count as f64 / sample_rate.get()) -} - -pub fn convert_position_in_frames_to_seconds( - frame_count: isize, - sample_rate: Hz, -) -> PositionInSeconds { - PositionInSeconds::new(frame_count as f64 / sample_rate.get()) -} - -pub fn convert_duration_in_frames_to_other_frame_rate( - frame_count: usize, - in_sample_rate: Hz, - out_sample_rate: Hz, -) -> usize { - let ratio = out_sample_rate.get() / in_sample_rate.get(); - (ratio * frame_count as f64).round() as usize -} diff --git a/playtime-clip-engine/src/file_util.rs b/playtime-clip-engine/src/file_util.rs deleted file mode 100644 index ac03a6d02..000000000 --- a/playtime-clip-engine/src/file_util.rs +++ /dev/null @@ -1,15 +0,0 @@ -use reaper_high::{Project, Reaper}; -use std::path::PathBuf; - -pub fn get_path_for_new_media_file( - file_base_name: &str, - file_extension_without_dot: &str, - project: Option, -) -> PathBuf { - let project = project.unwrap_or_else(|| Reaper::get().current_project()); - let recording_path = project.recording_path(); - let name_slug = slug::slugify(file_base_name); - let unique_id = nanoid::nanoid!(8); - let file_name = format!("{name_slug}-{unique_id}.{file_extension_without_dot}"); - recording_path.join(file_name) -} diff --git a/playtime-clip-engine/src/lib.rs b/playtime-clip-engine/src/lib.rs deleted file mode 100644 index 0ea63d336..000000000 --- a/playtime-clip-engine/src/lib.rs +++ /dev/null @@ -1,50 +0,0 @@ -#[macro_use] -mod tracing_util; - -pub mod base; -pub mod proto; -pub mod rt; - -mod metrics_util; - -mod timeline; -pub use timeline::*; - -mod source_util; - -mod file_util; - -mod conversion_util; - -mod mutex_util; - -pub mod midi_util; - -type ClipEngineResult = Result; - -#[derive(Clone)] -pub struct ErrorWithPayload { - pub message: &'static str, - pub payload: T, -} - -impl ErrorWithPayload { - pub const fn new(message: &'static str, payload: T) -> Self { - Self { message, payload } - } - - pub fn map_payload(self, f: impl FnOnce(T) -> R) -> ErrorWithPayload { - ErrorWithPayload { - payload: f(self.payload), - message: self.message, - } - } -} - -/// Must be called as early as possible. -/// -/// - before creating a matrix -/// - preferably in the main thread -pub fn init() { - metrics_util::init_metrics(); -} diff --git a/playtime-clip-engine/src/metrics_util.rs b/playtime-clip-engine/src/metrics_util.rs deleted file mode 100644 index b79ae89b7..000000000 --- a/playtime-clip-engine/src/metrics_util.rs +++ /dev/null @@ -1,70 +0,0 @@ -#![allow(dead_code)] - -use crossbeam_channel::{Receiver, Sender}; -use once_cell::sync::Lazy; -use std::thread; -use std::time::{Duration, Instant}; - -static METRICS_ENABLED: Lazy = Lazy::new(|| std::env::var("CLIP_ENGINE_METRICS").is_ok()); -static METRICS_CHANNEL: Lazy = Lazy::new(Default::default); - -/// Initializes the metrics channel. -pub fn init_metrics() { - let _ = *METRICS_ENABLED; - if !*METRICS_ENABLED { - return; - } - let _ = *METRICS_CHANNEL; - // We record metrics async because we are mostly in real-time threads when recording metrics. - // The metrics and metrics-exporter-prometheus crates sometimes do allocations. If this would - // just provoke audio dropouts, then fine ... users shouldn't collect metrics anyway under - // normal circumstances, in live scenarios certainly never! But it could also distort results. - thread::Builder::new() - .name(String::from("Playtime metrics")) - .spawn(move || { - keep_recording_metrics(METRICS_CHANNEL.receiver.clone()); - }) - .unwrap(); -} - -pub fn measure_time(id: &'static str, f: impl FnOnce() -> R) -> R { - if !*METRICS_ENABLED { - return f(); - } - let start = Instant::now(); - let result = f(); - let task = MetricsTask::Histogram { - id, - delta: start.elapsed(), - }; - if METRICS_CHANNEL.sender.try_send(task).is_err() { - debug!("Clip Engine metrics channel is full"); - } - result -} - -struct MetricsChannel { - sender: Sender, - receiver: Receiver, -} - -impl Default for MetricsChannel { - fn default() -> Self { - let (sender, receiver) = crossbeam_channel::bounded(5000); - Self { sender, receiver } - } -} - -enum MetricsTask { - Histogram { id: &'static str, delta: Duration }, -} - -fn keep_recording_metrics(receiver: Receiver) { - while let Ok(task) = receiver.recv() { - match task { - MetricsTask::Histogram { id, delta } => { - metrics::histogram!(id, delta); - } - } - } -} diff --git a/playtime-clip-engine/src/midi_util.rs b/playtime-clip-engine/src/midi_util.rs deleted file mode 100644 index 6398fe45c..000000000 --- a/playtime-clip-engine/src/midi_util.rs +++ /dev/null @@ -1,5 +0,0 @@ -use helgoboss_midi::ShortMessage; - -pub fn is_play_message(msg: &impl ShortMessage) -> bool { - msg.is_note_on() -} diff --git a/playtime-clip-engine/src/mutex_util.rs b/playtime-clip-engine/src/mutex_util.rs deleted file mode 100644 index f6370a0d2..000000000 --- a/playtime-clip-engine/src/mutex_util.rs +++ /dev/null @@ -1,37 +0,0 @@ -use std::sync::{Mutex, MutexGuard}; - -/// Attempts to lock the given mutex. -/// -/// Returns the guard even if mutex is poisoned. -/// -/// # Panics -/// -/// Panics in debug builds if already locked (blocks in release builds). -pub fn non_blocking_lock<'a, T>( - mutex: &'a Mutex, - description: &'static str, -) -> MutexGuard<'a, T> { - #[cfg(debug_assertions)] - match mutex.try_lock() { - Ok(g) => g, - Err(std::sync::TryLockError::Poisoned(e)) => e.into_inner(), - Err(std::sync::TryLockError::WouldBlock) => { - panic!("locking mutex would block: {description}") - } - } - #[cfg(not(debug_assertions))] - match mutex.lock() { - Ok(g) => g, - Err(e) => e.into_inner(), - } -} - -/// Locks the given mutex, potentially blocking. -/// -/// Returns the guard even if mutex is poisoned. -pub fn blocking_lock(mutex: &Mutex) -> MutexGuard { - match mutex.lock() { - Ok(g) => g, - Err(e) => e.into_inner(), - } -} diff --git a/playtime-clip-engine/src/proto/clip_engine.rs b/playtime-clip-engine/src/proto/clip_engine.rs deleted file mode 100644 index 801425e67..000000000 --- a/playtime-clip-engine/src/proto/clip_engine.rs +++ /dev/null @@ -1,2433 +0,0 @@ -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct FullColumnAddress { - #[prost(string, tag = "1")] - pub matrix_id: ::prost::alloc::string::String, - #[prost(uint32, tag = "2")] - pub column_index: u32, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct FullRowAddress { - #[prost(string, tag = "1")] - pub matrix_id: ::prost::alloc::string::String, - #[prost(uint32, tag = "2")] - pub row_index: u32, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct FullSlotAddress { - #[prost(string, tag = "1")] - pub matrix_id: ::prost::alloc::string::String, - #[prost(message, optional, tag = "2")] - pub slot_address: ::core::option::Option, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct FullClipAddress { - #[prost(string, tag = "1")] - pub matrix_id: ::prost::alloc::string::String, - #[prost(message, optional, tag = "2")] - pub clip_address: ::core::option::Option, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct ClipAddress { - #[prost(message, optional, tag = "1")] - pub slot_address: ::core::option::Option, - #[prost(uint32, tag = "2")] - pub clip_index: u32, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct SlotAddress { - #[prost(uint32, tag = "1")] - pub column_index: u32, - #[prost(uint32, tag = "2")] - pub row_index: u32, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct SetMatrixTempoRequest { - #[prost(string, tag = "1")] - pub matrix_id: ::prost::alloc::string::String, - #[prost(double, tag = "2")] - pub bpm: f64, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct SetMatrixVolumeRequest { - #[prost(string, tag = "1")] - pub matrix_id: ::prost::alloc::string::String, - #[prost(double, tag = "2")] - pub db: f64, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct SetMatrixPanRequest { - #[prost(string, tag = "1")] - pub matrix_id: ::prost::alloc::string::String, - #[prost(double, tag = "2")] - pub pan: f64, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct SetColumnVolumeRequest { - #[prost(message, optional, tag = "1")] - pub column_address: ::core::option::Option, - #[prost(double, tag = "2")] - pub db: f64, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct SetColumnNameRequest { - #[prost(message, optional, tag = "1")] - pub column_address: ::core::option::Option, - #[prost(string, tag = "2")] - pub name: ::prost::alloc::string::String, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct SetColumnPanRequest { - #[prost(message, optional, tag = "1")] - pub column_address: ::core::option::Option, - #[prost(double, tag = "2")] - pub pan: f64, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct SetColumnTrackRequest { - #[prost(message, optional, tag = "1")] - pub column_address: ::core::option::Option, - #[prost(string, optional, tag = "2")] - pub track_id: ::core::option::Option<::prost::alloc::string::String>, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct Empty {} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct TriggerMatrixRequest { - #[prost(string, tag = "1")] - pub matrix_id: ::prost::alloc::string::String, - #[prost(enumeration = "TriggerMatrixAction", tag = "2")] - pub action: i32, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct SetMatrixSettingsRequest { - #[prost(string, tag = "1")] - pub matrix_id: ::prost::alloc::string::String, - /// Matrix settings as JSON - #[prost(string, tag = "2")] - pub settings: ::prost::alloc::string::String, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GetAllTracksRequest { - #[prost(string, tag = "1")] - pub matrix_id: ::prost::alloc::string::String, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GetAllTracksReply { - #[prost(message, repeated, tag = "1")] - pub track: ::prost::alloc::vec::Vec, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct TriggerColumnRequest { - #[prost(message, optional, tag = "1")] - pub column_address: ::core::option::Option, - #[prost(enumeration = "TriggerColumnAction", tag = "2")] - pub action: i32, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct SetColumnSettingsRequest { - #[prost(message, optional, tag = "1")] - pub column_address: ::core::option::Option, - /// Column settings as JSON - #[prost(string, tag = "2")] - pub settings: ::prost::alloc::string::String, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct TriggerRowRequest { - #[prost(message, optional, tag = "1")] - pub row_address: ::core::option::Option, - #[prost(enumeration = "TriggerRowAction", tag = "2")] - pub action: i32, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct SetRowDataRequest { - #[prost(message, optional, tag = "1")] - pub row_address: ::core::option::Option, - /// Row data as JSON - #[prost(string, tag = "2")] - pub data: ::prost::alloc::string::String, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct DragColumnRequest { - #[prost(string, tag = "1")] - pub matrix_id: ::prost::alloc::string::String, - #[prost(uint32, tag = "2")] - pub source_column_index: u32, - #[prost(uint32, tag = "3")] - pub destination_column_index: u32, - #[prost(enumeration = "DragColumnAction", tag = "4")] - pub action: i32, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct DragRowRequest { - #[prost(string, tag = "1")] - pub matrix_id: ::prost::alloc::string::String, - #[prost(uint32, tag = "2")] - pub source_row_index: u32, - #[prost(uint32, tag = "3")] - pub destination_row_index: u32, - #[prost(enumeration = "DragRowAction", tag = "4")] - pub action: i32, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct TriggerSlotRequest { - #[prost(message, optional, tag = "1")] - pub slot_address: ::core::option::Option, - #[prost(enumeration = "TriggerSlotAction", tag = "2")] - pub action: i32, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct TriggerClipRequest { - #[prost(message, optional, tag = "1")] - pub clip_address: ::core::option::Option, - #[prost(enumeration = "TriggerClipAction", tag = "2")] - pub action: i32, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct DragSlotRequest { - #[prost(string, tag = "1")] - pub matrix_id: ::prost::alloc::string::String, - #[prost(message, optional, tag = "2")] - pub source_slot_address: ::core::option::Option, - #[prost(message, optional, tag = "3")] - pub destination_slot_address: ::core::option::Option, - #[prost(enumeration = "DragSlotAction", tag = "4")] - pub action: i32, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct SetClipNameRequest { - #[prost(message, optional, tag = "1")] - pub clip_address: ::core::option::Option, - #[prost(string, optional, tag = "2")] - pub name: ::core::option::Option<::prost::alloc::string::String>, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct SetClipDataRequest { - #[prost(message, optional, tag = "1")] - pub clip_address: ::core::option::Option, - /// Clip data as JSON - #[prost(string, tag = "2")] - pub data: ::prost::alloc::string::String, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GetClipDetailRequest { - #[prost(message, optional, tag = "1")] - pub clip_address: ::core::option::Option, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GetClipDetailReply { - #[prost(bytes = "vec", optional, tag = "1")] - pub rea_peaks: ::core::option::Option<::prost::alloc::vec::Vec>, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GetOccasionalMatrixUpdatesRequest { - #[prost(string, tag = "1")] - pub matrix_id: ::prost::alloc::string::String, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GetOccasionalTrackUpdatesRequest { - #[prost(string, tag = "1")] - pub matrix_id: ::prost::alloc::string::String, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GetOccasionalSlotUpdatesRequest { - #[prost(string, tag = "1")] - pub matrix_id: ::prost::alloc::string::String, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GetOccasionalClipUpdatesRequest { - #[prost(string, tag = "1")] - pub matrix_id: ::prost::alloc::string::String, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GetContinuousMatrixUpdatesRequest { - #[prost(string, tag = "1")] - pub matrix_id: ::prost::alloc::string::String, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GetOccasionalColumnUpdatesRequest { - #[prost(string, tag = "1")] - pub matrix_id: ::prost::alloc::string::String, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GetOccasionalRowUpdatesRequest { - #[prost(string, tag = "1")] - pub matrix_id: ::prost::alloc::string::String, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GetContinuousColumnUpdatesRequest { - #[prost(string, tag = "1")] - pub matrix_id: ::prost::alloc::string::String, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GetContinuousSlotUpdatesRequest { - #[prost(string, tag = "1")] - pub matrix_id: ::prost::alloc::string::String, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GetOccasionalMatrixUpdatesReply { - /// For each updated matrix property - #[prost(message, repeated, tag = "1")] - pub matrix_updates: ::prost::alloc::vec::Vec, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct QualifiedOccasionalColumnUpdate { - #[prost(uint32, tag = "1")] - pub column_index: u32, - #[prost(oneof = "qualified_occasional_column_update::Update", tags = "2")] - pub update: ::core::option::Option, -} -/// Nested message and enum types in `QualifiedOccasionalColumnUpdate`. -pub mod qualified_occasional_column_update { - #[allow(clippy::derive_partial_eq_without_eq)] - #[derive(Clone, PartialEq, ::prost::Oneof)] - pub enum Update { - /// Column settings as JSON - #[prost(string, tag = "2")] - Settings(::prost::alloc::string::String), - } -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct QualifiedOccasionalRowUpdate { - #[prost(uint32, tag = "1")] - pub row_index: u32, - #[prost(oneof = "qualified_occasional_row_update::Update", tags = "2")] - pub update: ::core::option::Option, -} -/// Nested message and enum types in `QualifiedOccasionalRowUpdate`. -pub mod qualified_occasional_row_update { - #[allow(clippy::derive_partial_eq_without_eq)] - #[derive(Clone, PartialEq, ::prost::Oneof)] - pub enum Update { - /// Row data as JSON - #[prost(string, tag = "2")] - Data(::prost::alloc::string::String), - } -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GetOccasionalTrackUpdatesReply { - /// For each updated column track - #[prost(message, repeated, tag = "1")] - pub track_updates: ::prost::alloc::vec::Vec, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GetOccasionalSlotUpdatesReply { - /// For each updated slot AND slot property - #[prost(message, repeated, tag = "1")] - pub slot_updates: ::prost::alloc::vec::Vec, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GetOccasionalClipUpdatesReply { - /// For each updated clip AND clip property - #[prost(message, repeated, tag = "1")] - pub clip_updates: ::prost::alloc::vec::Vec, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GetContinuousMatrixUpdatesReply { - #[prost(message, optional, tag = "1")] - pub matrix_update: ::core::option::Option, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GetOccasionalColumnUpdatesReply { - /// For each updated column property - #[prost(message, repeated, tag = "1")] - pub column_updates: ::prost::alloc::vec::Vec, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GetOccasionalRowUpdatesReply { - /// For each updated row property - #[prost(message, repeated, tag = "1")] - pub row_updates: ::prost::alloc::vec::Vec, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GetContinuousColumnUpdatesReply { - /// For each column - #[prost(message, repeated, tag = "1")] - pub column_updates: ::prost::alloc::vec::Vec, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct GetContinuousSlotUpdatesReply { - /// For each updated slot - #[prost(message, repeated, tag = "1")] - pub slot_updates: ::prost::alloc::vec::Vec, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct ContinuousMatrixUpdate { - #[prost(double, tag = "1")] - pub second: f64, - #[prost(sint32, tag = "2")] - pub bar: i32, - #[prost(double, tag = "3")] - pub beat: f64, - #[prost(double, repeated, tag = "4")] - pub peaks: ::prost::alloc::vec::Vec, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct ContinuousColumnUpdate { - #[prost(double, repeated, tag = "1")] - pub peaks: ::prost::alloc::vec::Vec, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct QualifiedContinuousSlotUpdate { - #[prost(message, optional, tag = "1")] - pub slot_address: ::core::option::Option, - #[prost(message, optional, tag = "2")] - pub update: ::core::option::Option, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct QualifiedOccasionalTrackUpdate { - #[prost(string, tag = "1")] - pub track_id: ::prost::alloc::string::String, - /// For each updated track property - #[prost(message, repeated, tag = "2")] - pub track_updates: ::prost::alloc::vec::Vec, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct OccasionalMatrixUpdate { - #[prost( - oneof = "occasional_matrix_update::Update", - tags = "1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11" - )] - pub update: ::core::option::Option, -} -/// Nested message and enum types in `OccasionalMatrixUpdate`. -pub mod occasional_matrix_update { - #[allow(clippy::derive_partial_eq_without_eq)] - #[derive(Clone, PartialEq, ::prost::Oneof)] - pub enum Update { - /// Matrix volume (= REAPER master track volume) - #[prost(double, tag = "1")] - Volume(f64), - /// Matrix pan (= REAPER master track pan) - #[prost(double, tag = "2")] - Pan(f64), - /// Matrix tempo (= REAPER master tempo) - #[prost(double, tag = "3")] - Tempo(f64), - /// Arrangement play state (= REAPER transport play state) - #[prost(enumeration = "super::ArrangementPlayState", tag = "4")] - ArrangementPlayState(i32), - /// MIDI input devices (= REAPER MIDI input devices) - #[prost(message, tag = "5")] - MidiInputDevices(super::MidiInputDevices), - /// Audio input channels (= REAPER hardware input channels) - #[prost(message, tag = "6")] - AudioInputChannels(super::AudioInputChannels), - /// Complete persistent data of the matrix has changed, including topology and other settings! - /// This contains the complete matrix as JSON. - #[prost(string, tag = "7")] - CompletePersistentData(::prost::alloc::string::String), - /// Clip matrix history state - #[prost(message, tag = "8")] - HistoryState(super::HistoryState), - /// Click on/off (= REAPER metronome state, at the moment) - #[prost(bool, tag = "9")] - ClickEnabled(bool), - /// Time signature (= REAPER master time signature) - #[prost(message, tag = "10")] - TimeSignature(super::TimeSignature), - /// Settings data as JSON. - #[prost(string, tag = "11")] - Settings(::prost::alloc::string::String), - } -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct HistoryState { - #[prost(string, tag = "1")] - pub undo_label: ::prost::alloc::string::String, - #[prost(string, tag = "2")] - pub redo_label: ::prost::alloc::string::String, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct TimeSignature { - #[prost(uint32, tag = "1")] - pub numerator: u32, - #[prost(uint32, tag = "2")] - pub denominator: u32, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct OccasionalTrackUpdate { - #[prost( - oneof = "occasional_track_update::Update", - tags = "1, 2, 3, 4, 5, 6, 7, 8, 9, 10" - )] - pub update: ::core::option::Option, -} -/// Nested message and enum types in `OccasionalTrackUpdate`. -pub mod occasional_track_update { - #[allow(clippy::derive_partial_eq_without_eq)] - #[derive(Clone, PartialEq, ::prost::Oneof)] - pub enum Update { - /// Track name - #[prost(string, tag = "1")] - Name(::prost::alloc::string::String), - /// Track color - #[prost(message, tag = "2")] - Color(super::TrackColor), - /// Track recording input - #[prost(message, tag = "3")] - Input(super::TrackInput), - /// Track record-arm on/off - #[prost(bool, tag = "4")] - Armed(bool), - /// Track recording input monitoring setting - #[prost(enumeration = "super::TrackInputMonitoring", tag = "5")] - InputMonitoring(i32), - /// Track mute on/off - #[prost(bool, tag = "6")] - Mute(bool), - /// Track solo on/off - #[prost(bool, tag = "7")] - Solo(bool), - /// Track selected or not - #[prost(bool, tag = "8")] - Selected(bool), - /// Track volume - #[prost(double, tag = "9")] - Volume(f64), - /// Track pan - #[prost(double, tag = "10")] - Pan(f64), - } -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct TrackColor { - #[prost(int32, optional, tag = "1")] - pub color: ::core::option::Option, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct TrackInput { - #[prost(oneof = "track_input::Input", tags = "1, 2, 3")] - pub input: ::core::option::Option, -} -/// Nested message and enum types in `TrackInput`. -pub mod track_input { - #[allow(clippy::derive_partial_eq_without_eq)] - #[derive(Clone, PartialEq, ::prost::Oneof)] - pub enum Input { - #[prost(uint32, tag = "1")] - Mono(u32), - #[prost(uint32, tag = "2")] - Stereo(u32), - #[prost(message, tag = "3")] - Midi(super::TrackMidiInput), - } -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct TrackMidiInput { - #[prost(uint32, optional, tag = "1")] - pub device: ::core::option::Option, - #[prost(uint32, optional, tag = "2")] - pub channel: ::core::option::Option, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct MidiInputDevices { - #[prost(message, repeated, tag = "1")] - pub devices: ::prost::alloc::vec::Vec, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct MidiInputDevice { - #[prost(uint32, tag = "1")] - pub id: u32, - #[prost(string, tag = "2")] - pub name: ::prost::alloc::string::String, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct TrackInList { - #[prost(string, tag = "1")] - pub id: ::prost::alloc::string::String, - #[prost(string, tag = "2")] - pub name: ::prost::alloc::string::String, - #[prost(uint32, tag = "3")] - pub level: u32, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct AudioInputChannels { - #[prost(message, repeated, tag = "1")] - pub channels: ::prost::alloc::vec::Vec, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct AudioInputChannel { - #[prost(uint32, tag = "1")] - pub index: u32, - #[prost(string, tag = "2")] - pub name: ::prost::alloc::string::String, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct QualifiedOccasionalSlotUpdate { - #[prost(message, optional, tag = "1")] - pub slot_address: ::core::option::Option, - #[prost(oneof = "qualified_occasional_slot_update::Update", tags = "2, 3")] - pub update: ::core::option::Option, -} -/// Nested message and enum types in `QualifiedOccasionalSlotUpdate`. -pub mod qualified_occasional_slot_update { - #[allow(clippy::derive_partial_eq_without_eq)] - #[derive(Clone, PartialEq, ::prost::Oneof)] - pub enum Update { - /// Slot play state - #[prost(enumeration = "super::SlotPlayState", tag = "2")] - PlayState(i32), - /// The complete persistent data of this slot has changed, that's mainly the - /// list of clips and their contents. This contains the complete slot as JSON. - #[prost(string, tag = "3")] - CompletePersistentData(::prost::alloc::string::String), - } -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct QualifiedOccasionalClipUpdate { - #[prost(message, optional, tag = "1")] - pub clip_address: ::core::option::Option, - #[prost(oneof = "qualified_occasional_clip_update::Update", tags = "2")] - pub update: ::core::option::Option, -} -/// Nested message and enum types in `QualifiedOccasionalClipUpdate`. -pub mod qualified_occasional_clip_update { - #[allow(clippy::derive_partial_eq_without_eq)] - #[derive(Clone, PartialEq, ::prost::Oneof)] - pub enum Update { - /// The complete persistent data of this clip has changed, e.g. its name. - /// This contains the complete clip as JSON. - #[prost(string, tag = "2")] - CompletePersistentData(::prost::alloc::string::String), - } -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct ContinuousSlotUpdate { - /// For each clip in the slot - #[prost(message, repeated, tag = "1")] - pub clip_update: ::prost::alloc::vec::Vec, -} -#[allow(clippy::derive_partial_eq_without_eq)] -#[derive(Clone, PartialEq, ::prost::Message)] -pub struct ContinuousClipUpdate { - /// Number between 0 and 1, interpretable as percentage. - #[prost(double, tag = "1")] - pub proportional_position: f64, - #[prost(double, tag = "2")] - pub position_in_seconds: f64, - #[prost(double, tag = "3")] - pub peak: f64, -} -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] -#[repr(i32)] -pub enum TriggerMatrixAction { - ArrangementTogglePlayStop = 0, - StopAllClips = 1, - ArrangementPlay = 2, - ArrangementStop = 3, - ArrangementPause = 4, - ArrangementStartRecording = 5, - ArrangementStopRecording = 6, - Undo = 7, - Redo = 8, - ToggleClick = 9, - Panic = 10, -} -impl TriggerMatrixAction { - /// String value of the enum field names used in the ProtoBuf definition. - /// - /// The values are not transformed in any way and thus are considered stable - /// (if the ProtoBuf definition does not change) and safe for programmatic use. - pub fn as_str_name(&self) -> &'static str { - match self { - TriggerMatrixAction::ArrangementTogglePlayStop => { - "TRIGGER_MATRIX_ACTION_ARRANGEMENT_TOGGLE_PLAY_STOP" - } - TriggerMatrixAction::StopAllClips => "TRIGGER_MATRIX_ACTION_STOP_ALL_CLIPS", - TriggerMatrixAction::ArrangementPlay => "TRIGGER_MATRIX_ACTION_ARRANGEMENT_PLAY", - TriggerMatrixAction::ArrangementStop => "TRIGGER_MATRIX_ACTION_ARRANGEMENT_STOP", - TriggerMatrixAction::ArrangementPause => "TRIGGER_MATRIX_ACTION_ARRANGEMENT_PAUSE", - TriggerMatrixAction::ArrangementStartRecording => { - "TRIGGER_MATRIX_ACTION_ARRANGEMENT_START_RECORDING" - } - TriggerMatrixAction::ArrangementStopRecording => { - "TRIGGER_MATRIX_ACTION_ARRANGEMENT_STOP_RECORDING" - } - TriggerMatrixAction::Undo => "TRIGGER_MATRIX_ACTION_UNDO", - TriggerMatrixAction::Redo => "TRIGGER_MATRIX_ACTION_REDO", - TriggerMatrixAction::ToggleClick => "TRIGGER_MATRIX_ACTION_TOGGLE_CLICK", - TriggerMatrixAction::Panic => "TRIGGER_MATRIX_ACTION_PANIC", - } - } - /// Creates an enum from field names used in the ProtoBuf definition. - pub fn from_str_name(value: &str) -> ::core::option::Option { - match value { - "TRIGGER_MATRIX_ACTION_ARRANGEMENT_TOGGLE_PLAY_STOP" => { - Some(Self::ArrangementTogglePlayStop) - } - "TRIGGER_MATRIX_ACTION_STOP_ALL_CLIPS" => Some(Self::StopAllClips), - "TRIGGER_MATRIX_ACTION_ARRANGEMENT_PLAY" => Some(Self::ArrangementPlay), - "TRIGGER_MATRIX_ACTION_ARRANGEMENT_STOP" => Some(Self::ArrangementStop), - "TRIGGER_MATRIX_ACTION_ARRANGEMENT_PAUSE" => Some(Self::ArrangementPause), - "TRIGGER_MATRIX_ACTION_ARRANGEMENT_START_RECORDING" => { - Some(Self::ArrangementStartRecording) - } - "TRIGGER_MATRIX_ACTION_ARRANGEMENT_STOP_RECORDING" => { - Some(Self::ArrangementStopRecording) - } - "TRIGGER_MATRIX_ACTION_UNDO" => Some(Self::Undo), - "TRIGGER_MATRIX_ACTION_REDO" => Some(Self::Redo), - "TRIGGER_MATRIX_ACTION_TOGGLE_CLICK" => Some(Self::ToggleClick), - "TRIGGER_MATRIX_ACTION_PANIC" => Some(Self::Panic), - _ => None, - } - } -} -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] -#[repr(i32)] -pub enum TriggerColumnAction { - Stop = 0, - ToggleMute = 1, - ToggleSolo = 2, - ToggleArm = 3, - Remove = 4, - Duplicate = 5, - Insert = 6, - Panic = 7, -} -impl TriggerColumnAction { - /// String value of the enum field names used in the ProtoBuf definition. - /// - /// The values are not transformed in any way and thus are considered stable - /// (if the ProtoBuf definition does not change) and safe for programmatic use. - pub fn as_str_name(&self) -> &'static str { - match self { - TriggerColumnAction::Stop => "TRIGGER_COLUMN_ACTION_STOP", - TriggerColumnAction::ToggleMute => "TRIGGER_COLUMN_ACTION_TOGGLE_MUTE", - TriggerColumnAction::ToggleSolo => "TRIGGER_COLUMN_ACTION_TOGGLE_SOLO", - TriggerColumnAction::ToggleArm => "TRIGGER_COLUMN_ACTION_TOGGLE_ARM", - TriggerColumnAction::Remove => "TRIGGER_COLUMN_ACTION_REMOVE", - TriggerColumnAction::Duplicate => "TRIGGER_COLUMN_ACTION_DUPLICATE", - TriggerColumnAction::Insert => "TRIGGER_COLUMN_ACTION_INSERT", - TriggerColumnAction::Panic => "TRIGGER_COLUMN_ACTION_PANIC", - } - } - /// Creates an enum from field names used in the ProtoBuf definition. - pub fn from_str_name(value: &str) -> ::core::option::Option { - match value { - "TRIGGER_COLUMN_ACTION_STOP" => Some(Self::Stop), - "TRIGGER_COLUMN_ACTION_TOGGLE_MUTE" => Some(Self::ToggleMute), - "TRIGGER_COLUMN_ACTION_TOGGLE_SOLO" => Some(Self::ToggleSolo), - "TRIGGER_COLUMN_ACTION_TOGGLE_ARM" => Some(Self::ToggleArm), - "TRIGGER_COLUMN_ACTION_REMOVE" => Some(Self::Remove), - "TRIGGER_COLUMN_ACTION_DUPLICATE" => Some(Self::Duplicate), - "TRIGGER_COLUMN_ACTION_INSERT" => Some(Self::Insert), - "TRIGGER_COLUMN_ACTION_PANIC" => Some(Self::Panic), - _ => None, - } - } -} -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] -#[repr(i32)] -pub enum TriggerRowAction { - Play = 0, - Clear = 1, - Copy = 2, - Cut = 3, - Paste = 4, - Remove = 5, - Duplicate = 6, - Insert = 7, - Panic = 8, -} -impl TriggerRowAction { - /// String value of the enum field names used in the ProtoBuf definition. - /// - /// The values are not transformed in any way and thus are considered stable - /// (if the ProtoBuf definition does not change) and safe for programmatic use. - pub fn as_str_name(&self) -> &'static str { - match self { - TriggerRowAction::Play => "TRIGGER_ROW_ACTION_PLAY", - TriggerRowAction::Clear => "TRIGGER_ROW_ACTION_CLEAR", - TriggerRowAction::Copy => "TRIGGER_ROW_ACTION_COPY", - TriggerRowAction::Cut => "TRIGGER_ROW_ACTION_CUT", - TriggerRowAction::Paste => "TRIGGER_ROW_ACTION_PASTE", - TriggerRowAction::Remove => "TRIGGER_ROW_ACTION_REMOVE", - TriggerRowAction::Duplicate => "TRIGGER_ROW_ACTION_DUPLICATE", - TriggerRowAction::Insert => "TRIGGER_ROW_ACTION_INSERT", - TriggerRowAction::Panic => "TRIGGER_ROW_ACTION_PANIC", - } - } - /// Creates an enum from field names used in the ProtoBuf definition. - pub fn from_str_name(value: &str) -> ::core::option::Option { - match value { - "TRIGGER_ROW_ACTION_PLAY" => Some(Self::Play), - "TRIGGER_ROW_ACTION_CLEAR" => Some(Self::Clear), - "TRIGGER_ROW_ACTION_COPY" => Some(Self::Copy), - "TRIGGER_ROW_ACTION_CUT" => Some(Self::Cut), - "TRIGGER_ROW_ACTION_PASTE" => Some(Self::Paste), - "TRIGGER_ROW_ACTION_REMOVE" => Some(Self::Remove), - "TRIGGER_ROW_ACTION_DUPLICATE" => Some(Self::Duplicate), - "TRIGGER_ROW_ACTION_INSERT" => Some(Self::Insert), - "TRIGGER_ROW_ACTION_PANIC" => Some(Self::Panic), - _ => None, - } - } -} -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] -#[repr(i32)] -pub enum DragColumnAction { - Reorder = 0, -} -impl DragColumnAction { - /// String value of the enum field names used in the ProtoBuf definition. - /// - /// The values are not transformed in any way and thus are considered stable - /// (if the ProtoBuf definition does not change) and safe for programmatic use. - pub fn as_str_name(&self) -> &'static str { - match self { - DragColumnAction::Reorder => "DRAG_COLUMN_ACTION_REORDER", - } - } - /// Creates an enum from field names used in the ProtoBuf definition. - pub fn from_str_name(value: &str) -> ::core::option::Option { - match value { - "DRAG_COLUMN_ACTION_REORDER" => Some(Self::Reorder), - _ => None, - } - } -} -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] -#[repr(i32)] -pub enum DragRowAction { - MoveContent = 0, - CopyContent = 1, - Reorder = 2, -} -impl DragRowAction { - /// String value of the enum field names used in the ProtoBuf definition. - /// - /// The values are not transformed in any way and thus are considered stable - /// (if the ProtoBuf definition does not change) and safe for programmatic use. - pub fn as_str_name(&self) -> &'static str { - match self { - DragRowAction::MoveContent => "DRAG_ROW_ACTION_MOVE_CONTENT", - DragRowAction::CopyContent => "DRAG_ROW_ACTION_COPY_CONTENT", - DragRowAction::Reorder => "DRAG_ROW_ACTION_REORDER", - } - } - /// Creates an enum from field names used in the ProtoBuf definition. - pub fn from_str_name(value: &str) -> ::core::option::Option { - match value { - "DRAG_ROW_ACTION_MOVE_CONTENT" => Some(Self::MoveContent), - "DRAG_ROW_ACTION_COPY_CONTENT" => Some(Self::CopyContent), - "DRAG_ROW_ACTION_REORDER" => Some(Self::Reorder), - _ => None, - } - } -} -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] -#[repr(i32)] -pub enum TriggerSlotAction { - Play = 0, - Stop = 1, - Record = 2, - Clear = 4, - Copy = 5, - Cut = 6, - Paste = 7, - FillWithSelectedItem = 8, - Panic = 9, -} -impl TriggerSlotAction { - /// String value of the enum field names used in the ProtoBuf definition. - /// - /// The values are not transformed in any way and thus are considered stable - /// (if the ProtoBuf definition does not change) and safe for programmatic use. - pub fn as_str_name(&self) -> &'static str { - match self { - TriggerSlotAction::Play => "TRIGGER_SLOT_ACTION_PLAY", - TriggerSlotAction::Stop => "TRIGGER_SLOT_ACTION_STOP", - TriggerSlotAction::Record => "TRIGGER_SLOT_ACTION_RECORD", - TriggerSlotAction::Clear => "TRIGGER_SLOT_ACTION_CLEAR", - TriggerSlotAction::Copy => "TRIGGER_SLOT_ACTION_COPY", - TriggerSlotAction::Cut => "TRIGGER_SLOT_ACTION_CUT", - TriggerSlotAction::Paste => "TRIGGER_SLOT_ACTION_PASTE", - TriggerSlotAction::FillWithSelectedItem => { - "TRIGGER_SLOT_ACTION_FILL_WITH_SELECTED_ITEM" - } - TriggerSlotAction::Panic => "TRIGGER_SLOT_ACTION_PANIC", - } - } - /// Creates an enum from field names used in the ProtoBuf definition. - pub fn from_str_name(value: &str) -> ::core::option::Option { - match value { - "TRIGGER_SLOT_ACTION_PLAY" => Some(Self::Play), - "TRIGGER_SLOT_ACTION_STOP" => Some(Self::Stop), - "TRIGGER_SLOT_ACTION_RECORD" => Some(Self::Record), - "TRIGGER_SLOT_ACTION_CLEAR" => Some(Self::Clear), - "TRIGGER_SLOT_ACTION_COPY" => Some(Self::Copy), - "TRIGGER_SLOT_ACTION_CUT" => Some(Self::Cut), - "TRIGGER_SLOT_ACTION_PASTE" => Some(Self::Paste), - "TRIGGER_SLOT_ACTION_FILL_WITH_SELECTED_ITEM" => Some(Self::FillWithSelectedItem), - "TRIGGER_SLOT_ACTION_PANIC" => Some(Self::Panic), - _ => None, - } - } -} -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] -#[repr(i32)] -pub enum TriggerClipAction { - MidiOverdub = 0, - Edit = 1, -} -impl TriggerClipAction { - /// String value of the enum field names used in the ProtoBuf definition. - /// - /// The values are not transformed in any way and thus are considered stable - /// (if the ProtoBuf definition does not change) and safe for programmatic use. - pub fn as_str_name(&self) -> &'static str { - match self { - TriggerClipAction::MidiOverdub => "TRIGGER_CLIP_ACTION_MIDI_OVERDUB", - TriggerClipAction::Edit => "TRIGGER_CLIP_ACTION_EDIT", - } - } - /// Creates an enum from field names used in the ProtoBuf definition. - pub fn from_str_name(value: &str) -> ::core::option::Option { - match value { - "TRIGGER_CLIP_ACTION_MIDI_OVERDUB" => Some(Self::MidiOverdub), - "TRIGGER_CLIP_ACTION_EDIT" => Some(Self::Edit), - _ => None, - } - } -} -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] -#[repr(i32)] -pub enum DragSlotAction { - Move = 0, - Copy = 1, -} -impl DragSlotAction { - /// String value of the enum field names used in the ProtoBuf definition. - /// - /// The values are not transformed in any way and thus are considered stable - /// (if the ProtoBuf definition does not change) and safe for programmatic use. - pub fn as_str_name(&self) -> &'static str { - match self { - DragSlotAction::Move => "DRAG_SLOT_ACTION_MOVE", - DragSlotAction::Copy => "DRAG_SLOT_ACTION_COPY", - } - } - /// Creates an enum from field names used in the ProtoBuf definition. - pub fn from_str_name(value: &str) -> ::core::option::Option { - match value { - "DRAG_SLOT_ACTION_MOVE" => Some(Self::Move), - "DRAG_SLOT_ACTION_COPY" => Some(Self::Copy), - _ => None, - } - } -} -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] -#[repr(i32)] -pub enum TrackInputMonitoring { - Unknown = 0, - Off = 1, - Normal = 2, - TapeStyle = 3, -} -impl TrackInputMonitoring { - /// String value of the enum field names used in the ProtoBuf definition. - /// - /// The values are not transformed in any way and thus are considered stable - /// (if the ProtoBuf definition does not change) and safe for programmatic use. - pub fn as_str_name(&self) -> &'static str { - match self { - TrackInputMonitoring::Unknown => "TRACK_INPUT_MONITORING_UNKNOWN", - TrackInputMonitoring::Off => "TRACK_INPUT_MONITORING_OFF", - TrackInputMonitoring::Normal => "TRACK_INPUT_MONITORING_NORMAL", - TrackInputMonitoring::TapeStyle => "TRACK_INPUT_MONITORING_TAPE_STYLE", - } - } - /// Creates an enum from field names used in the ProtoBuf definition. - pub fn from_str_name(value: &str) -> ::core::option::Option { - match value { - "TRACK_INPUT_MONITORING_UNKNOWN" => Some(Self::Unknown), - "TRACK_INPUT_MONITORING_OFF" => Some(Self::Off), - "TRACK_INPUT_MONITORING_NORMAL" => Some(Self::Normal), - "TRACK_INPUT_MONITORING_TAPE_STYLE" => Some(Self::TapeStyle), - _ => None, - } - } -} -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] -#[repr(i32)] -pub enum SlotPlayState { - Unknown = 0, - Stopped = 1, - ScheduledForPlayStart = 2, - Playing = 3, - Paused = 4, - ScheduledForPlayStop = 5, - ScheduledForRecordingStart = 6, - Recording = 7, - ScheduledForRecordingStop = 8, -} -impl SlotPlayState { - /// String value of the enum field names used in the ProtoBuf definition. - /// - /// The values are not transformed in any way and thus are considered stable - /// (if the ProtoBuf definition does not change) and safe for programmatic use. - pub fn as_str_name(&self) -> &'static str { - match self { - SlotPlayState::Unknown => "SLOT_PLAY_STATE_UNKNOWN", - SlotPlayState::Stopped => "SLOT_PLAY_STATE_STOPPED", - SlotPlayState::ScheduledForPlayStart => "SLOT_PLAY_STATE_SCHEDULED_FOR_PLAY_START", - SlotPlayState::Playing => "SLOT_PLAY_STATE_PLAYING", - SlotPlayState::Paused => "SLOT_PLAY_STATE_PAUSED", - SlotPlayState::ScheduledForPlayStop => "SLOT_PLAY_STATE_SCHEDULED_FOR_PLAY_STOP", - SlotPlayState::ScheduledForRecordingStart => { - "SLOT_PLAY_STATE_SCHEDULED_FOR_RECORDING_START" - } - SlotPlayState::Recording => "SLOT_PLAY_STATE_RECORDING", - SlotPlayState::ScheduledForRecordingStop => { - "SLOT_PLAY_STATE_SCHEDULED_FOR_RECORDING_STOP" - } - } - } - /// Creates an enum from field names used in the ProtoBuf definition. - pub fn from_str_name(value: &str) -> ::core::option::Option { - match value { - "SLOT_PLAY_STATE_UNKNOWN" => Some(Self::Unknown), - "SLOT_PLAY_STATE_STOPPED" => Some(Self::Stopped), - "SLOT_PLAY_STATE_SCHEDULED_FOR_PLAY_START" => Some(Self::ScheduledForPlayStart), - "SLOT_PLAY_STATE_PLAYING" => Some(Self::Playing), - "SLOT_PLAY_STATE_PAUSED" => Some(Self::Paused), - "SLOT_PLAY_STATE_SCHEDULED_FOR_PLAY_STOP" => Some(Self::ScheduledForPlayStop), - "SLOT_PLAY_STATE_SCHEDULED_FOR_RECORDING_START" => { - Some(Self::ScheduledForRecordingStart) - } - "SLOT_PLAY_STATE_RECORDING" => Some(Self::Recording), - "SLOT_PLAY_STATE_SCHEDULED_FOR_RECORDING_STOP" => Some(Self::ScheduledForRecordingStop), - _ => None, - } - } -} -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] -#[repr(i32)] -pub enum ArrangementPlayState { - Unknown = 0, - Stopped = 1, - Playing = 2, - PlayingPaused = 3, - Recording = 4, - RecordingPaused = 5, -} -impl ArrangementPlayState { - /// String value of the enum field names used in the ProtoBuf definition. - /// - /// The values are not transformed in any way and thus are considered stable - /// (if the ProtoBuf definition does not change) and safe for programmatic use. - pub fn as_str_name(&self) -> &'static str { - match self { - ArrangementPlayState::Unknown => "ARRANGEMENT_PLAY_STATE_UNKNOWN", - ArrangementPlayState::Stopped => "ARRANGEMENT_PLAY_STATE_STOPPED", - ArrangementPlayState::Playing => "ARRANGEMENT_PLAY_STATE_PLAYING", - ArrangementPlayState::PlayingPaused => "ARRANGEMENT_PLAY_STATE_PLAYING_PAUSED", - ArrangementPlayState::Recording => "ARRANGEMENT_PLAY_STATE_RECORDING", - ArrangementPlayState::RecordingPaused => "ARRANGEMENT_PLAY_STATE_RECORDING_PAUSED", - } - } - /// Creates an enum from field names used in the ProtoBuf definition. - pub fn from_str_name(value: &str) -> ::core::option::Option { - match value { - "ARRANGEMENT_PLAY_STATE_UNKNOWN" => Some(Self::Unknown), - "ARRANGEMENT_PLAY_STATE_STOPPED" => Some(Self::Stopped), - "ARRANGEMENT_PLAY_STATE_PLAYING" => Some(Self::Playing), - "ARRANGEMENT_PLAY_STATE_PLAYING_PAUSED" => Some(Self::PlayingPaused), - "ARRANGEMENT_PLAY_STATE_RECORDING" => Some(Self::Recording), - "ARRANGEMENT_PLAY_STATE_RECORDING_PAUSED" => Some(Self::RecordingPaused), - _ => None, - } - } -} -/// Generated server implementations. -pub mod clip_engine_server { - #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] - use tonic::codegen::*; - /// Generated trait containing gRPC methods that should be implemented for use with ClipEngineServer. - #[async_trait] - pub trait ClipEngine: Send + Sync + 'static { - /// Global queries - async fn get_all_tracks( - &self, - request: tonic::Request, - ) -> Result, tonic::Status>; - /// Matrix commands - async fn trigger_matrix( - &self, - request: tonic::Request, - ) -> Result, tonic::Status>; - async fn set_matrix_settings( - &self, - request: tonic::Request, - ) -> Result, tonic::Status>; - async fn set_matrix_tempo( - &self, - request: tonic::Request, - ) -> Result, tonic::Status>; - async fn set_matrix_volume( - &self, - request: tonic::Request, - ) -> Result, tonic::Status>; - async fn set_matrix_pan( - &self, - request: tonic::Request, - ) -> Result, tonic::Status>; - /// Column commands - async fn trigger_column( - &self, - request: tonic::Request, - ) -> Result, tonic::Status>; - async fn set_column_settings( - &self, - request: tonic::Request, - ) -> Result, tonic::Status>; - async fn set_column_name( - &self, - request: tonic::Request, - ) -> Result, tonic::Status>; - async fn set_column_volume( - &self, - request: tonic::Request, - ) -> Result, tonic::Status>; - async fn set_column_pan( - &self, - request: tonic::Request, - ) -> Result, tonic::Status>; - async fn set_column_track( - &self, - request: tonic::Request, - ) -> Result, tonic::Status>; - async fn drag_column( - &self, - request: tonic::Request, - ) -> Result, tonic::Status>; - /// Row commands - async fn trigger_row( - &self, - request: tonic::Request, - ) -> Result, tonic::Status>; - async fn set_row_data( - &self, - request: tonic::Request, - ) -> Result, tonic::Status>; - async fn drag_row( - &self, - request: tonic::Request, - ) -> Result, tonic::Status>; - /// Slot commands - async fn trigger_slot( - &self, - request: tonic::Request, - ) -> Result, tonic::Status>; - async fn drag_slot( - &self, - request: tonic::Request, - ) -> Result, tonic::Status>; - /// Clip commands - async fn trigger_clip( - &self, - request: tonic::Request, - ) -> Result, tonic::Status>; - async fn set_clip_name( - &self, - request: tonic::Request, - ) -> Result, tonic::Status>; - async fn set_clip_data( - &self, - request: tonic::Request, - ) -> Result, tonic::Status>; - /// Clip queries - async fn get_clip_detail( - &self, - request: tonic::Request, - ) -> Result, tonic::Status>; - /// Server streaming response type for the GetOccasionalMatrixUpdates method. - type GetOccasionalMatrixUpdatesStream: futures_core::Stream< - Item = Result, - > + Send - + 'static; - /// Matrix events - async fn get_occasional_matrix_updates( - &self, - request: tonic::Request, - ) -> Result, tonic::Status>; - /// Server streaming response type for the GetContinuousMatrixUpdates method. - type GetContinuousMatrixUpdatesStream: futures_core::Stream< - Item = Result, - > + Send - + 'static; - async fn get_continuous_matrix_updates( - &self, - request: tonic::Request, - ) -> Result, tonic::Status>; - /// Server streaming response type for the GetOccasionalColumnUpdates method. - type GetOccasionalColumnUpdatesStream: futures_core::Stream< - Item = Result, - > + Send - + 'static; - /// Column events - async fn get_occasional_column_updates( - &self, - request: tonic::Request, - ) -> Result, tonic::Status>; - /// Server streaming response type for the GetContinuousColumnUpdates method. - type GetContinuousColumnUpdatesStream: futures_core::Stream< - Item = Result, - > + Send - + 'static; - async fn get_continuous_column_updates( - &self, - request: tonic::Request, - ) -> Result, tonic::Status>; - /// Server streaming response type for the GetOccasionalRowUpdates method. - type GetOccasionalRowUpdatesStream: futures_core::Stream> - + Send - + 'static; - /// Row events - async fn get_occasional_row_updates( - &self, - request: tonic::Request, - ) -> Result, tonic::Status>; - /// Server streaming response type for the GetOccasionalSlotUpdates method. - type GetOccasionalSlotUpdatesStream: futures_core::Stream> - + Send - + 'static; - /// Slot events - async fn get_occasional_slot_updates( - &self, - request: tonic::Request, - ) -> Result, tonic::Status>; - /// Server streaming response type for the GetContinuousSlotUpdates method. - type GetContinuousSlotUpdatesStream: futures_core::Stream> - + Send - + 'static; - async fn get_continuous_slot_updates( - &self, - request: tonic::Request, - ) -> Result, tonic::Status>; - /// Server streaming response type for the GetOccasionalClipUpdates method. - type GetOccasionalClipUpdatesStream: futures_core::Stream> - + Send - + 'static; - /// Clip events - async fn get_occasional_clip_updates( - &self, - request: tonic::Request, - ) -> Result, tonic::Status>; - /// Server streaming response type for the GetOccasionalTrackUpdates method. - type GetOccasionalTrackUpdatesStream: futures_core::Stream< - Item = Result, - > + Send - + 'static; - /// Track events - async fn get_occasional_track_updates( - &self, - request: tonic::Request, - ) -> Result, tonic::Status>; - } - #[derive(Debug)] - pub struct ClipEngineServer { - inner: _Inner, - accept_compression_encodings: EnabledCompressionEncodings, - send_compression_encodings: EnabledCompressionEncodings, - } - struct _Inner(Arc); - impl ClipEngineServer { - pub fn new(inner: T) -> Self { - Self::from_arc(Arc::new(inner)) - } - pub fn from_arc(inner: Arc) -> Self { - let inner = _Inner(inner); - Self { - inner, - accept_compression_encodings: Default::default(), - send_compression_encodings: Default::default(), - } - } - pub fn with_interceptor(inner: T, interceptor: F) -> InterceptedService - where - F: tonic::service::Interceptor, - { - InterceptedService::new(Self::new(inner), interceptor) - } - /// Enable decompressing requests with the given encoding. - #[must_use] - pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { - self.accept_compression_encodings.enable(encoding); - self - } - /// Compress responses with the given encoding, if the client supports it. - #[must_use] - pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { - self.send_compression_encodings.enable(encoding); - self - } - } - impl tonic::codegen::Service> for ClipEngineServer - where - T: ClipEngine, - B: Body + Send + 'static, - B::Error: Into + Send + 'static, - { - type Response = http::Response; - type Error = std::convert::Infallible; - type Future = BoxFuture; - fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - fn call(&mut self, req: http::Request) -> Self::Future { - let inner = self.inner.clone(); - match req.uri().path() { - "/playtime.clip_engine.ClipEngine/GetAllTracks" => { - #[allow(non_camel_case_types)] - struct GetAllTracksSvc(pub Arc); - impl tonic::server::UnaryService for GetAllTracksSvc { - type Response = super::GetAllTracksReply; - type Future = BoxFuture, tonic::Status>; - fn call( - &mut self, - request: tonic::Request, - ) -> Self::Future { - let inner = self.0.clone(); - let fut = async move { (*inner).get_all_tracks(request).await }; - Box::pin(fut) - } - } - let accept_compression_encodings = self.accept_compression_encodings; - let send_compression_encodings = self.send_compression_encodings; - let inner = self.inner.clone(); - let fut = async move { - let inner = inner.0; - let method = GetAllTracksSvc(inner); - let codec = tonic::codec::ProstCodec::default(); - let mut grpc = tonic::server::Grpc::new(codec).apply_compression_config( - accept_compression_encodings, - send_compression_encodings, - ); - let res = grpc.unary(method, req).await; - Ok(res) - }; - Box::pin(fut) - } - "/playtime.clip_engine.ClipEngine/TriggerMatrix" => { - #[allow(non_camel_case_types)] - struct TriggerMatrixSvc(pub Arc); - impl tonic::server::UnaryService - for TriggerMatrixSvc - { - type Response = super::Empty; - type Future = BoxFuture, tonic::Status>; - fn call( - &mut self, - request: tonic::Request, - ) -> Self::Future { - let inner = self.0.clone(); - let fut = async move { (*inner).trigger_matrix(request).await }; - Box::pin(fut) - } - } - let accept_compression_encodings = self.accept_compression_encodings; - let send_compression_encodings = self.send_compression_encodings; - let inner = self.inner.clone(); - let fut = async move { - let inner = inner.0; - let method = TriggerMatrixSvc(inner); - let codec = tonic::codec::ProstCodec::default(); - let mut grpc = tonic::server::Grpc::new(codec).apply_compression_config( - accept_compression_encodings, - send_compression_encodings, - ); - let res = grpc.unary(method, req).await; - Ok(res) - }; - Box::pin(fut) - } - "/playtime.clip_engine.ClipEngine/SetMatrixSettings" => { - #[allow(non_camel_case_types)] - struct SetMatrixSettingsSvc(pub Arc); - impl tonic::server::UnaryService - for SetMatrixSettingsSvc - { - type Response = super::Empty; - type Future = BoxFuture, tonic::Status>; - fn call( - &mut self, - request: tonic::Request, - ) -> Self::Future { - let inner = self.0.clone(); - let fut = async move { (*inner).set_matrix_settings(request).await }; - Box::pin(fut) - } - } - let accept_compression_encodings = self.accept_compression_encodings; - let send_compression_encodings = self.send_compression_encodings; - let inner = self.inner.clone(); - let fut = async move { - let inner = inner.0; - let method = SetMatrixSettingsSvc(inner); - let codec = tonic::codec::ProstCodec::default(); - let mut grpc = tonic::server::Grpc::new(codec).apply_compression_config( - accept_compression_encodings, - send_compression_encodings, - ); - let res = grpc.unary(method, req).await; - Ok(res) - }; - Box::pin(fut) - } - "/playtime.clip_engine.ClipEngine/SetMatrixTempo" => { - #[allow(non_camel_case_types)] - struct SetMatrixTempoSvc(pub Arc); - impl tonic::server::UnaryService - for SetMatrixTempoSvc - { - type Response = super::Empty; - type Future = BoxFuture, tonic::Status>; - fn call( - &mut self, - request: tonic::Request, - ) -> Self::Future { - let inner = self.0.clone(); - let fut = async move { (*inner).set_matrix_tempo(request).await }; - Box::pin(fut) - } - } - let accept_compression_encodings = self.accept_compression_encodings; - let send_compression_encodings = self.send_compression_encodings; - let inner = self.inner.clone(); - let fut = async move { - let inner = inner.0; - let method = SetMatrixTempoSvc(inner); - let codec = tonic::codec::ProstCodec::default(); - let mut grpc = tonic::server::Grpc::new(codec).apply_compression_config( - accept_compression_encodings, - send_compression_encodings, - ); - let res = grpc.unary(method, req).await; - Ok(res) - }; - Box::pin(fut) - } - "/playtime.clip_engine.ClipEngine/SetMatrixVolume" => { - #[allow(non_camel_case_types)] - struct SetMatrixVolumeSvc(pub Arc); - impl tonic::server::UnaryService - for SetMatrixVolumeSvc - { - type Response = super::Empty; - type Future = BoxFuture, tonic::Status>; - fn call( - &mut self, - request: tonic::Request, - ) -> Self::Future { - let inner = self.0.clone(); - let fut = async move { (*inner).set_matrix_volume(request).await }; - Box::pin(fut) - } - } - let accept_compression_encodings = self.accept_compression_encodings; - let send_compression_encodings = self.send_compression_encodings; - let inner = self.inner.clone(); - let fut = async move { - let inner = inner.0; - let method = SetMatrixVolumeSvc(inner); - let codec = tonic::codec::ProstCodec::default(); - let mut grpc = tonic::server::Grpc::new(codec).apply_compression_config( - accept_compression_encodings, - send_compression_encodings, - ); - let res = grpc.unary(method, req).await; - Ok(res) - }; - Box::pin(fut) - } - "/playtime.clip_engine.ClipEngine/SetMatrixPan" => { - #[allow(non_camel_case_types)] - struct SetMatrixPanSvc(pub Arc); - impl tonic::server::UnaryService for SetMatrixPanSvc { - type Response = super::Empty; - type Future = BoxFuture, tonic::Status>; - fn call( - &mut self, - request: tonic::Request, - ) -> Self::Future { - let inner = self.0.clone(); - let fut = async move { (*inner).set_matrix_pan(request).await }; - Box::pin(fut) - } - } - let accept_compression_encodings = self.accept_compression_encodings; - let send_compression_encodings = self.send_compression_encodings; - let inner = self.inner.clone(); - let fut = async move { - let inner = inner.0; - let method = SetMatrixPanSvc(inner); - let codec = tonic::codec::ProstCodec::default(); - let mut grpc = tonic::server::Grpc::new(codec).apply_compression_config( - accept_compression_encodings, - send_compression_encodings, - ); - let res = grpc.unary(method, req).await; - Ok(res) - }; - Box::pin(fut) - } - "/playtime.clip_engine.ClipEngine/TriggerColumn" => { - #[allow(non_camel_case_types)] - struct TriggerColumnSvc(pub Arc); - impl tonic::server::UnaryService - for TriggerColumnSvc - { - type Response = super::Empty; - type Future = BoxFuture, tonic::Status>; - fn call( - &mut self, - request: tonic::Request, - ) -> Self::Future { - let inner = self.0.clone(); - let fut = async move { (*inner).trigger_column(request).await }; - Box::pin(fut) - } - } - let accept_compression_encodings = self.accept_compression_encodings; - let send_compression_encodings = self.send_compression_encodings; - let inner = self.inner.clone(); - let fut = async move { - let inner = inner.0; - let method = TriggerColumnSvc(inner); - let codec = tonic::codec::ProstCodec::default(); - let mut grpc = tonic::server::Grpc::new(codec).apply_compression_config( - accept_compression_encodings, - send_compression_encodings, - ); - let res = grpc.unary(method, req).await; - Ok(res) - }; - Box::pin(fut) - } - "/playtime.clip_engine.ClipEngine/SetColumnSettings" => { - #[allow(non_camel_case_types)] - struct SetColumnSettingsSvc(pub Arc); - impl tonic::server::UnaryService - for SetColumnSettingsSvc - { - type Response = super::Empty; - type Future = BoxFuture, tonic::Status>; - fn call( - &mut self, - request: tonic::Request, - ) -> Self::Future { - let inner = self.0.clone(); - let fut = async move { (*inner).set_column_settings(request).await }; - Box::pin(fut) - } - } - let accept_compression_encodings = self.accept_compression_encodings; - let send_compression_encodings = self.send_compression_encodings; - let inner = self.inner.clone(); - let fut = async move { - let inner = inner.0; - let method = SetColumnSettingsSvc(inner); - let codec = tonic::codec::ProstCodec::default(); - let mut grpc = tonic::server::Grpc::new(codec).apply_compression_config( - accept_compression_encodings, - send_compression_encodings, - ); - let res = grpc.unary(method, req).await; - Ok(res) - }; - Box::pin(fut) - } - "/playtime.clip_engine.ClipEngine/SetColumnName" => { - #[allow(non_camel_case_types)] - struct SetColumnNameSvc(pub Arc); - impl tonic::server::UnaryService - for SetColumnNameSvc - { - type Response = super::Empty; - type Future = BoxFuture, tonic::Status>; - fn call( - &mut self, - request: tonic::Request, - ) -> Self::Future { - let inner = self.0.clone(); - let fut = async move { (*inner).set_column_name(request).await }; - Box::pin(fut) - } - } - let accept_compression_encodings = self.accept_compression_encodings; - let send_compression_encodings = self.send_compression_encodings; - let inner = self.inner.clone(); - let fut = async move { - let inner = inner.0; - let method = SetColumnNameSvc(inner); - let codec = tonic::codec::ProstCodec::default(); - let mut grpc = tonic::server::Grpc::new(codec).apply_compression_config( - accept_compression_encodings, - send_compression_encodings, - ); - let res = grpc.unary(method, req).await; - Ok(res) - }; - Box::pin(fut) - } - "/playtime.clip_engine.ClipEngine/SetColumnVolume" => { - #[allow(non_camel_case_types)] - struct SetColumnVolumeSvc(pub Arc); - impl tonic::server::UnaryService - for SetColumnVolumeSvc - { - type Response = super::Empty; - type Future = BoxFuture, tonic::Status>; - fn call( - &mut self, - request: tonic::Request, - ) -> Self::Future { - let inner = self.0.clone(); - let fut = async move { (*inner).set_column_volume(request).await }; - Box::pin(fut) - } - } - let accept_compression_encodings = self.accept_compression_encodings; - let send_compression_encodings = self.send_compression_encodings; - let inner = self.inner.clone(); - let fut = async move { - let inner = inner.0; - let method = SetColumnVolumeSvc(inner); - let codec = tonic::codec::ProstCodec::default(); - let mut grpc = tonic::server::Grpc::new(codec).apply_compression_config( - accept_compression_encodings, - send_compression_encodings, - ); - let res = grpc.unary(method, req).await; - Ok(res) - }; - Box::pin(fut) - } - "/playtime.clip_engine.ClipEngine/SetColumnPan" => { - #[allow(non_camel_case_types)] - struct SetColumnPanSvc(pub Arc); - impl tonic::server::UnaryService for SetColumnPanSvc { - type Response = super::Empty; - type Future = BoxFuture, tonic::Status>; - fn call( - &mut self, - request: tonic::Request, - ) -> Self::Future { - let inner = self.0.clone(); - let fut = async move { (*inner).set_column_pan(request).await }; - Box::pin(fut) - } - } - let accept_compression_encodings = self.accept_compression_encodings; - let send_compression_encodings = self.send_compression_encodings; - let inner = self.inner.clone(); - let fut = async move { - let inner = inner.0; - let method = SetColumnPanSvc(inner); - let codec = tonic::codec::ProstCodec::default(); - let mut grpc = tonic::server::Grpc::new(codec).apply_compression_config( - accept_compression_encodings, - send_compression_encodings, - ); - let res = grpc.unary(method, req).await; - Ok(res) - }; - Box::pin(fut) - } - "/playtime.clip_engine.ClipEngine/SetColumnTrack" => { - #[allow(non_camel_case_types)] - struct SetColumnTrackSvc(pub Arc); - impl tonic::server::UnaryService - for SetColumnTrackSvc - { - type Response = super::Empty; - type Future = BoxFuture, tonic::Status>; - fn call( - &mut self, - request: tonic::Request, - ) -> Self::Future { - let inner = self.0.clone(); - let fut = async move { (*inner).set_column_track(request).await }; - Box::pin(fut) - } - } - let accept_compression_encodings = self.accept_compression_encodings; - let send_compression_encodings = self.send_compression_encodings; - let inner = self.inner.clone(); - let fut = async move { - let inner = inner.0; - let method = SetColumnTrackSvc(inner); - let codec = tonic::codec::ProstCodec::default(); - let mut grpc = tonic::server::Grpc::new(codec).apply_compression_config( - accept_compression_encodings, - send_compression_encodings, - ); - let res = grpc.unary(method, req).await; - Ok(res) - }; - Box::pin(fut) - } - "/playtime.clip_engine.ClipEngine/DragColumn" => { - #[allow(non_camel_case_types)] - struct DragColumnSvc(pub Arc); - impl tonic::server::UnaryService for DragColumnSvc { - type Response = super::Empty; - type Future = BoxFuture, tonic::Status>; - fn call( - &mut self, - request: tonic::Request, - ) -> Self::Future { - let inner = self.0.clone(); - let fut = async move { (*inner).drag_column(request).await }; - Box::pin(fut) - } - } - let accept_compression_encodings = self.accept_compression_encodings; - let send_compression_encodings = self.send_compression_encodings; - let inner = self.inner.clone(); - let fut = async move { - let inner = inner.0; - let method = DragColumnSvc(inner); - let codec = tonic::codec::ProstCodec::default(); - let mut grpc = tonic::server::Grpc::new(codec).apply_compression_config( - accept_compression_encodings, - send_compression_encodings, - ); - let res = grpc.unary(method, req).await; - Ok(res) - }; - Box::pin(fut) - } - "/playtime.clip_engine.ClipEngine/TriggerRow" => { - #[allow(non_camel_case_types)] - struct TriggerRowSvc(pub Arc); - impl tonic::server::UnaryService for TriggerRowSvc { - type Response = super::Empty; - type Future = BoxFuture, tonic::Status>; - fn call( - &mut self, - request: tonic::Request, - ) -> Self::Future { - let inner = self.0.clone(); - let fut = async move { (*inner).trigger_row(request).await }; - Box::pin(fut) - } - } - let accept_compression_encodings = self.accept_compression_encodings; - let send_compression_encodings = self.send_compression_encodings; - let inner = self.inner.clone(); - let fut = async move { - let inner = inner.0; - let method = TriggerRowSvc(inner); - let codec = tonic::codec::ProstCodec::default(); - let mut grpc = tonic::server::Grpc::new(codec).apply_compression_config( - accept_compression_encodings, - send_compression_encodings, - ); - let res = grpc.unary(method, req).await; - Ok(res) - }; - Box::pin(fut) - } - "/playtime.clip_engine.ClipEngine/SetRowData" => { - #[allow(non_camel_case_types)] - struct SetRowDataSvc(pub Arc); - impl tonic::server::UnaryService for SetRowDataSvc { - type Response = super::Empty; - type Future = BoxFuture, tonic::Status>; - fn call( - &mut self, - request: tonic::Request, - ) -> Self::Future { - let inner = self.0.clone(); - let fut = async move { (*inner).set_row_data(request).await }; - Box::pin(fut) - } - } - let accept_compression_encodings = self.accept_compression_encodings; - let send_compression_encodings = self.send_compression_encodings; - let inner = self.inner.clone(); - let fut = async move { - let inner = inner.0; - let method = SetRowDataSvc(inner); - let codec = tonic::codec::ProstCodec::default(); - let mut grpc = tonic::server::Grpc::new(codec).apply_compression_config( - accept_compression_encodings, - send_compression_encodings, - ); - let res = grpc.unary(method, req).await; - Ok(res) - }; - Box::pin(fut) - } - "/playtime.clip_engine.ClipEngine/DragRow" => { - #[allow(non_camel_case_types)] - struct DragRowSvc(pub Arc); - impl tonic::server::UnaryService for DragRowSvc { - type Response = super::Empty; - type Future = BoxFuture, tonic::Status>; - fn call( - &mut self, - request: tonic::Request, - ) -> Self::Future { - let inner = self.0.clone(); - let fut = async move { (*inner).drag_row(request).await }; - Box::pin(fut) - } - } - let accept_compression_encodings = self.accept_compression_encodings; - let send_compression_encodings = self.send_compression_encodings; - let inner = self.inner.clone(); - let fut = async move { - let inner = inner.0; - let method = DragRowSvc(inner); - let codec = tonic::codec::ProstCodec::default(); - let mut grpc = tonic::server::Grpc::new(codec).apply_compression_config( - accept_compression_encodings, - send_compression_encodings, - ); - let res = grpc.unary(method, req).await; - Ok(res) - }; - Box::pin(fut) - } - "/playtime.clip_engine.ClipEngine/TriggerSlot" => { - #[allow(non_camel_case_types)] - struct TriggerSlotSvc(pub Arc); - impl tonic::server::UnaryService for TriggerSlotSvc { - type Response = super::Empty; - type Future = BoxFuture, tonic::Status>; - fn call( - &mut self, - request: tonic::Request, - ) -> Self::Future { - let inner = self.0.clone(); - let fut = async move { (*inner).trigger_slot(request).await }; - Box::pin(fut) - } - } - let accept_compression_encodings = self.accept_compression_encodings; - let send_compression_encodings = self.send_compression_encodings; - let inner = self.inner.clone(); - let fut = async move { - let inner = inner.0; - let method = TriggerSlotSvc(inner); - let codec = tonic::codec::ProstCodec::default(); - let mut grpc = tonic::server::Grpc::new(codec).apply_compression_config( - accept_compression_encodings, - send_compression_encodings, - ); - let res = grpc.unary(method, req).await; - Ok(res) - }; - Box::pin(fut) - } - "/playtime.clip_engine.ClipEngine/DragSlot" => { - #[allow(non_camel_case_types)] - struct DragSlotSvc(pub Arc); - impl tonic::server::UnaryService for DragSlotSvc { - type Response = super::Empty; - type Future = BoxFuture, tonic::Status>; - fn call( - &mut self, - request: tonic::Request, - ) -> Self::Future { - let inner = self.0.clone(); - let fut = async move { (*inner).drag_slot(request).await }; - Box::pin(fut) - } - } - let accept_compression_encodings = self.accept_compression_encodings; - let send_compression_encodings = self.send_compression_encodings; - let inner = self.inner.clone(); - let fut = async move { - let inner = inner.0; - let method = DragSlotSvc(inner); - let codec = tonic::codec::ProstCodec::default(); - let mut grpc = tonic::server::Grpc::new(codec).apply_compression_config( - accept_compression_encodings, - send_compression_encodings, - ); - let res = grpc.unary(method, req).await; - Ok(res) - }; - Box::pin(fut) - } - "/playtime.clip_engine.ClipEngine/TriggerClip" => { - #[allow(non_camel_case_types)] - struct TriggerClipSvc(pub Arc); - impl tonic::server::UnaryService for TriggerClipSvc { - type Response = super::Empty; - type Future = BoxFuture, tonic::Status>; - fn call( - &mut self, - request: tonic::Request, - ) -> Self::Future { - let inner = self.0.clone(); - let fut = async move { (*inner).trigger_clip(request).await }; - Box::pin(fut) - } - } - let accept_compression_encodings = self.accept_compression_encodings; - let send_compression_encodings = self.send_compression_encodings; - let inner = self.inner.clone(); - let fut = async move { - let inner = inner.0; - let method = TriggerClipSvc(inner); - let codec = tonic::codec::ProstCodec::default(); - let mut grpc = tonic::server::Grpc::new(codec).apply_compression_config( - accept_compression_encodings, - send_compression_encodings, - ); - let res = grpc.unary(method, req).await; - Ok(res) - }; - Box::pin(fut) - } - "/playtime.clip_engine.ClipEngine/SetClipName" => { - #[allow(non_camel_case_types)] - struct SetClipNameSvc(pub Arc); - impl tonic::server::UnaryService for SetClipNameSvc { - type Response = super::Empty; - type Future = BoxFuture, tonic::Status>; - fn call( - &mut self, - request: tonic::Request, - ) -> Self::Future { - let inner = self.0.clone(); - let fut = async move { (*inner).set_clip_name(request).await }; - Box::pin(fut) - } - } - let accept_compression_encodings = self.accept_compression_encodings; - let send_compression_encodings = self.send_compression_encodings; - let inner = self.inner.clone(); - let fut = async move { - let inner = inner.0; - let method = SetClipNameSvc(inner); - let codec = tonic::codec::ProstCodec::default(); - let mut grpc = tonic::server::Grpc::new(codec).apply_compression_config( - accept_compression_encodings, - send_compression_encodings, - ); - let res = grpc.unary(method, req).await; - Ok(res) - }; - Box::pin(fut) - } - "/playtime.clip_engine.ClipEngine/SetClipData" => { - #[allow(non_camel_case_types)] - struct SetClipDataSvc(pub Arc); - impl tonic::server::UnaryService for SetClipDataSvc { - type Response = super::Empty; - type Future = BoxFuture, tonic::Status>; - fn call( - &mut self, - request: tonic::Request, - ) -> Self::Future { - let inner = self.0.clone(); - let fut = async move { (*inner).set_clip_data(request).await }; - Box::pin(fut) - } - } - let accept_compression_encodings = self.accept_compression_encodings; - let send_compression_encodings = self.send_compression_encodings; - let inner = self.inner.clone(); - let fut = async move { - let inner = inner.0; - let method = SetClipDataSvc(inner); - let codec = tonic::codec::ProstCodec::default(); - let mut grpc = tonic::server::Grpc::new(codec).apply_compression_config( - accept_compression_encodings, - send_compression_encodings, - ); - let res = grpc.unary(method, req).await; - Ok(res) - }; - Box::pin(fut) - } - "/playtime.clip_engine.ClipEngine/GetClipDetail" => { - #[allow(non_camel_case_types)] - struct GetClipDetailSvc(pub Arc); - impl tonic::server::UnaryService - for GetClipDetailSvc - { - type Response = super::GetClipDetailReply; - type Future = BoxFuture, tonic::Status>; - fn call( - &mut self, - request: tonic::Request, - ) -> Self::Future { - let inner = self.0.clone(); - let fut = async move { (*inner).get_clip_detail(request).await }; - Box::pin(fut) - } - } - let accept_compression_encodings = self.accept_compression_encodings; - let send_compression_encodings = self.send_compression_encodings; - let inner = self.inner.clone(); - let fut = async move { - let inner = inner.0; - let method = GetClipDetailSvc(inner); - let codec = tonic::codec::ProstCodec::default(); - let mut grpc = tonic::server::Grpc::new(codec).apply_compression_config( - accept_compression_encodings, - send_compression_encodings, - ); - let res = grpc.unary(method, req).await; - Ok(res) - }; - Box::pin(fut) - } - "/playtime.clip_engine.ClipEngine/GetOccasionalMatrixUpdates" => { - #[allow(non_camel_case_types)] - struct GetOccasionalMatrixUpdatesSvc(pub Arc); - impl - tonic::server::ServerStreamingService< - super::GetOccasionalMatrixUpdatesRequest, - > for GetOccasionalMatrixUpdatesSvc - { - type Response = super::GetOccasionalMatrixUpdatesReply; - type ResponseStream = T::GetOccasionalMatrixUpdatesStream; - type Future = - BoxFuture, tonic::Status>; - fn call( - &mut self, - request: tonic::Request, - ) -> Self::Future { - let inner = self.0.clone(); - let fut = async move { - (*inner).get_occasional_matrix_updates(request).await - }; - Box::pin(fut) - } - } - let accept_compression_encodings = self.accept_compression_encodings; - let send_compression_encodings = self.send_compression_encodings; - let inner = self.inner.clone(); - let fut = async move { - let inner = inner.0; - let method = GetOccasionalMatrixUpdatesSvc(inner); - let codec = tonic::codec::ProstCodec::default(); - let mut grpc = tonic::server::Grpc::new(codec).apply_compression_config( - accept_compression_encodings, - send_compression_encodings, - ); - let res = grpc.server_streaming(method, req).await; - Ok(res) - }; - Box::pin(fut) - } - "/playtime.clip_engine.ClipEngine/GetContinuousMatrixUpdates" => { - #[allow(non_camel_case_types)] - struct GetContinuousMatrixUpdatesSvc(pub Arc); - impl - tonic::server::ServerStreamingService< - super::GetContinuousMatrixUpdatesRequest, - > for GetContinuousMatrixUpdatesSvc - { - type Response = super::GetContinuousMatrixUpdatesReply; - type ResponseStream = T::GetContinuousMatrixUpdatesStream; - type Future = - BoxFuture, tonic::Status>; - fn call( - &mut self, - request: tonic::Request, - ) -> Self::Future { - let inner = self.0.clone(); - let fut = async move { - (*inner).get_continuous_matrix_updates(request).await - }; - Box::pin(fut) - } - } - let accept_compression_encodings = self.accept_compression_encodings; - let send_compression_encodings = self.send_compression_encodings; - let inner = self.inner.clone(); - let fut = async move { - let inner = inner.0; - let method = GetContinuousMatrixUpdatesSvc(inner); - let codec = tonic::codec::ProstCodec::default(); - let mut grpc = tonic::server::Grpc::new(codec).apply_compression_config( - accept_compression_encodings, - send_compression_encodings, - ); - let res = grpc.server_streaming(method, req).await; - Ok(res) - }; - Box::pin(fut) - } - "/playtime.clip_engine.ClipEngine/GetOccasionalColumnUpdates" => { - #[allow(non_camel_case_types)] - struct GetOccasionalColumnUpdatesSvc(pub Arc); - impl - tonic::server::ServerStreamingService< - super::GetOccasionalColumnUpdatesRequest, - > for GetOccasionalColumnUpdatesSvc - { - type Response = super::GetOccasionalColumnUpdatesReply; - type ResponseStream = T::GetOccasionalColumnUpdatesStream; - type Future = - BoxFuture, tonic::Status>; - fn call( - &mut self, - request: tonic::Request, - ) -> Self::Future { - let inner = self.0.clone(); - let fut = async move { - (*inner).get_occasional_column_updates(request).await - }; - Box::pin(fut) - } - } - let accept_compression_encodings = self.accept_compression_encodings; - let send_compression_encodings = self.send_compression_encodings; - let inner = self.inner.clone(); - let fut = async move { - let inner = inner.0; - let method = GetOccasionalColumnUpdatesSvc(inner); - let codec = tonic::codec::ProstCodec::default(); - let mut grpc = tonic::server::Grpc::new(codec).apply_compression_config( - accept_compression_encodings, - send_compression_encodings, - ); - let res = grpc.server_streaming(method, req).await; - Ok(res) - }; - Box::pin(fut) - } - "/playtime.clip_engine.ClipEngine/GetContinuousColumnUpdates" => { - #[allow(non_camel_case_types)] - struct GetContinuousColumnUpdatesSvc(pub Arc); - impl - tonic::server::ServerStreamingService< - super::GetContinuousColumnUpdatesRequest, - > for GetContinuousColumnUpdatesSvc - { - type Response = super::GetContinuousColumnUpdatesReply; - type ResponseStream = T::GetContinuousColumnUpdatesStream; - type Future = - BoxFuture, tonic::Status>; - fn call( - &mut self, - request: tonic::Request, - ) -> Self::Future { - let inner = self.0.clone(); - let fut = async move { - (*inner).get_continuous_column_updates(request).await - }; - Box::pin(fut) - } - } - let accept_compression_encodings = self.accept_compression_encodings; - let send_compression_encodings = self.send_compression_encodings; - let inner = self.inner.clone(); - let fut = async move { - let inner = inner.0; - let method = GetContinuousColumnUpdatesSvc(inner); - let codec = tonic::codec::ProstCodec::default(); - let mut grpc = tonic::server::Grpc::new(codec).apply_compression_config( - accept_compression_encodings, - send_compression_encodings, - ); - let res = grpc.server_streaming(method, req).await; - Ok(res) - }; - Box::pin(fut) - } - "/playtime.clip_engine.ClipEngine/GetOccasionalRowUpdates" => { - #[allow(non_camel_case_types)] - struct GetOccasionalRowUpdatesSvc(pub Arc); - impl - tonic::server::ServerStreamingService - for GetOccasionalRowUpdatesSvc - { - type Response = super::GetOccasionalRowUpdatesReply; - type ResponseStream = T::GetOccasionalRowUpdatesStream; - type Future = - BoxFuture, tonic::Status>; - fn call( - &mut self, - request: tonic::Request, - ) -> Self::Future { - let inner = self.0.clone(); - let fut = - async move { (*inner).get_occasional_row_updates(request).await }; - Box::pin(fut) - } - } - let accept_compression_encodings = self.accept_compression_encodings; - let send_compression_encodings = self.send_compression_encodings; - let inner = self.inner.clone(); - let fut = async move { - let inner = inner.0; - let method = GetOccasionalRowUpdatesSvc(inner); - let codec = tonic::codec::ProstCodec::default(); - let mut grpc = tonic::server::Grpc::new(codec).apply_compression_config( - accept_compression_encodings, - send_compression_encodings, - ); - let res = grpc.server_streaming(method, req).await; - Ok(res) - }; - Box::pin(fut) - } - "/playtime.clip_engine.ClipEngine/GetOccasionalSlotUpdates" => { - #[allow(non_camel_case_types)] - struct GetOccasionalSlotUpdatesSvc(pub Arc); - impl - tonic::server::ServerStreamingService< - super::GetOccasionalSlotUpdatesRequest, - > for GetOccasionalSlotUpdatesSvc - { - type Response = super::GetOccasionalSlotUpdatesReply; - type ResponseStream = T::GetOccasionalSlotUpdatesStream; - type Future = - BoxFuture, tonic::Status>; - fn call( - &mut self, - request: tonic::Request, - ) -> Self::Future { - let inner = self.0.clone(); - let fut = - async move { (*inner).get_occasional_slot_updates(request).await }; - Box::pin(fut) - } - } - let accept_compression_encodings = self.accept_compression_encodings; - let send_compression_encodings = self.send_compression_encodings; - let inner = self.inner.clone(); - let fut = async move { - let inner = inner.0; - let method = GetOccasionalSlotUpdatesSvc(inner); - let codec = tonic::codec::ProstCodec::default(); - let mut grpc = tonic::server::Grpc::new(codec).apply_compression_config( - accept_compression_encodings, - send_compression_encodings, - ); - let res = grpc.server_streaming(method, req).await; - Ok(res) - }; - Box::pin(fut) - } - "/playtime.clip_engine.ClipEngine/GetContinuousSlotUpdates" => { - #[allow(non_camel_case_types)] - struct GetContinuousSlotUpdatesSvc(pub Arc); - impl - tonic::server::ServerStreamingService< - super::GetContinuousSlotUpdatesRequest, - > for GetContinuousSlotUpdatesSvc - { - type Response = super::GetContinuousSlotUpdatesReply; - type ResponseStream = T::GetContinuousSlotUpdatesStream; - type Future = - BoxFuture, tonic::Status>; - fn call( - &mut self, - request: tonic::Request, - ) -> Self::Future { - let inner = self.0.clone(); - let fut = - async move { (*inner).get_continuous_slot_updates(request).await }; - Box::pin(fut) - } - } - let accept_compression_encodings = self.accept_compression_encodings; - let send_compression_encodings = self.send_compression_encodings; - let inner = self.inner.clone(); - let fut = async move { - let inner = inner.0; - let method = GetContinuousSlotUpdatesSvc(inner); - let codec = tonic::codec::ProstCodec::default(); - let mut grpc = tonic::server::Grpc::new(codec).apply_compression_config( - accept_compression_encodings, - send_compression_encodings, - ); - let res = grpc.server_streaming(method, req).await; - Ok(res) - }; - Box::pin(fut) - } - "/playtime.clip_engine.ClipEngine/GetOccasionalClipUpdates" => { - #[allow(non_camel_case_types)] - struct GetOccasionalClipUpdatesSvc(pub Arc); - impl - tonic::server::ServerStreamingService< - super::GetOccasionalClipUpdatesRequest, - > for GetOccasionalClipUpdatesSvc - { - type Response = super::GetOccasionalClipUpdatesReply; - type ResponseStream = T::GetOccasionalClipUpdatesStream; - type Future = - BoxFuture, tonic::Status>; - fn call( - &mut self, - request: tonic::Request, - ) -> Self::Future { - let inner = self.0.clone(); - let fut = - async move { (*inner).get_occasional_clip_updates(request).await }; - Box::pin(fut) - } - } - let accept_compression_encodings = self.accept_compression_encodings; - let send_compression_encodings = self.send_compression_encodings; - let inner = self.inner.clone(); - let fut = async move { - let inner = inner.0; - let method = GetOccasionalClipUpdatesSvc(inner); - let codec = tonic::codec::ProstCodec::default(); - let mut grpc = tonic::server::Grpc::new(codec).apply_compression_config( - accept_compression_encodings, - send_compression_encodings, - ); - let res = grpc.server_streaming(method, req).await; - Ok(res) - }; - Box::pin(fut) - } - "/playtime.clip_engine.ClipEngine/GetOccasionalTrackUpdates" => { - #[allow(non_camel_case_types)] - struct GetOccasionalTrackUpdatesSvc(pub Arc); - impl - tonic::server::ServerStreamingService< - super::GetOccasionalTrackUpdatesRequest, - > for GetOccasionalTrackUpdatesSvc - { - type Response = super::GetOccasionalTrackUpdatesReply; - type ResponseStream = T::GetOccasionalTrackUpdatesStream; - type Future = - BoxFuture, tonic::Status>; - fn call( - &mut self, - request: tonic::Request, - ) -> Self::Future { - let inner = self.0.clone(); - let fut = - async move { (*inner).get_occasional_track_updates(request).await }; - Box::pin(fut) - } - } - let accept_compression_encodings = self.accept_compression_encodings; - let send_compression_encodings = self.send_compression_encodings; - let inner = self.inner.clone(); - let fut = async move { - let inner = inner.0; - let method = GetOccasionalTrackUpdatesSvc(inner); - let codec = tonic::codec::ProstCodec::default(); - let mut grpc = tonic::server::Grpc::new(codec).apply_compression_config( - accept_compression_encodings, - send_compression_encodings, - ); - let res = grpc.server_streaming(method, req).await; - Ok(res) - }; - Box::pin(fut) - } - _ => Box::pin(async move { - Ok(http::Response::builder() - .status(200) - .header("grpc-status", "12") - .header("content-type", "application/grpc") - .body(empty_body()) - .unwrap()) - }), - } - } - } - impl Clone for ClipEngineServer { - fn clone(&self) -> Self { - let inner = self.inner.clone(); - Self { - inner, - accept_compression_encodings: self.accept_compression_encodings, - send_compression_encodings: self.send_compression_encodings, - } - } - } - impl Clone for _Inner { - fn clone(&self) -> Self { - Self(self.0.clone()) - } - } - impl std::fmt::Debug for _Inner { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self.0) - } - } - impl tonic::server::NamedService for ClipEngineServer { - const NAME: &'static str = "playtime.clip_engine.ClipEngine"; - } -} diff --git a/playtime-clip-engine/src/proto/clip_engine_ext.rs b/playtime-clip-engine/src/proto/clip_engine_ext.rs deleted file mode 100644 index 5c5f92c5e..000000000 --- a/playtime-clip-engine/src/proto/clip_engine_ext.rs +++ /dev/null @@ -1,286 +0,0 @@ -use playtime_api::runtime::ClipPlayState; -use reaper_high::Project; -use reaper_medium::{ - Bpm, Db, InputMonitoringMode, PlayState, ReaperPanValue, RecordingInput, RgbColor, -}; - -use crate::base::{Clip, ClipSlotAddress, History, Matrix, Slot}; -use crate::proto::track_input::Input; -use crate::proto::{ - clip_engine, occasional_matrix_update, occasional_track_update, - qualified_occasional_clip_update, qualified_occasional_column_update, - qualified_occasional_row_update, qualified_occasional_slot_update, ArrangementPlayState, - ClipAddress, HistoryState, SlotAddress, SlotPlayState, TimeSignature, TrackColor, TrackInput, - TrackInputMonitoring, TrackMidiInput, -}; -use crate::rt::InternalClipPlayState; -use crate::{base, clip_timeline, ClipEngineResult, Timeline}; - -impl occasional_matrix_update::Update { - pub fn volume(db: Db) -> Self { - Self::Volume(db.get()) - } - - pub fn pan(pan: ReaperPanValue) -> Self { - Self::Pan(pan.get()) - } - - pub fn tempo(bpm: Bpm) -> Self { - Self::Tempo(bpm.get()) - } - - pub fn arrangement_play_state(play_state: PlayState) -> Self { - Self::ArrangementPlayState(ArrangementPlayState::from_engine(play_state).into()) - } - - pub fn complete_persistent_data(matrix: &Matrix) -> Self { - let matrix_json = - serde_json::to_string(&matrix.save()).expect("couldn't represent matrix as JSON"); - Self::CompletePersistentData(matrix_json) - } - - pub fn settings(matrix: &Matrix) -> Self { - let settings_json = serde_json::to_string(&matrix.all_matrix_settings_combined()) - .expect("couldn't represent matrix settings as JSON"); - Self::Settings(settings_json) - } - - pub fn history_state(matrix: &Matrix) -> Self { - Self::HistoryState(HistoryState::from_engine(matrix.history())) - } - - pub fn time_signature(project: Project) -> Self { - Self::TimeSignature(TimeSignature::from_engine(project)) - } -} - -impl clip_engine::TrackInList { - pub fn from_engine(track: reaper_high::Track, level: u32) -> Self { - Self { - id: track.guid().to_string_without_braces(), - name: track.name().unwrap_or_default().into_string(), - level, - } - } -} - -impl TimeSignature { - pub fn from_engine(project: Project) -> Self { - let timeline = clip_timeline(Some(project), true); - let time_signature = timeline.time_signature_at(timeline.cursor_pos()); - TimeSignature { - numerator: time_signature.numerator.get(), - denominator: time_signature.denominator.get(), - } - } -} - -impl occasional_track_update::Update { - pub fn name(track: &reaper_high::Track) -> Self { - Self::Name(track.name().unwrap_or_default().into_string()) - } - - pub fn color(track: &reaper_high::Track) -> Self { - Self::Color(TrackColor::from_engine(track.custom_color())) - } - - pub fn input(input: Option) -> Self { - Self::Input(TrackInput::from_engine(input)) - } - - pub fn armed(value: bool) -> Self { - Self::Armed(value) - } - - pub fn input_monitoring(mode: InputMonitoringMode) -> Self { - Self::InputMonitoring(TrackInputMonitoring::from_engine(mode).into()) - } - - pub fn mute(value: bool) -> Self { - Self::Mute(value) - } - - pub fn solo(value: bool) -> Self { - Self::Solo(value) - } - - pub fn selected(value: bool) -> Self { - Self::Selected(value) - } - - pub fn volume(db: Db) -> Self { - Self::Volume(db.get()) - } - - pub fn pan(pan: ReaperPanValue) -> Self { - Self::Pan(pan.get()) - } -} - -impl qualified_occasional_slot_update::Update { - pub fn play_state(play_state: InternalClipPlayState) -> Self { - Self::PlayState(SlotPlayState::from_engine(play_state.get()).into()) - } - - pub fn complete_persistent_data(_matrix: &Matrix, slot: &Slot) -> Self { - let api_slot = slot.save().unwrap_or(playtime_api::persistence::Slot { - id: slot.id().clone(), - row: slot.index(), - clip_old: None, - clips: None, - }); - let json = serde_json::to_string(&api_slot).expect("couldn't represent slot as JSON"); - Self::CompletePersistentData(json) - } -} - -impl qualified_occasional_column_update::Update { - pub fn settings(matrix: &Matrix, column_index: usize) -> ClipEngineResult { - let column = matrix.get_column(column_index)?; - let json = serde_json::to_string(&column.all_column_settings_combined()) - .expect("couldn't represent slot as JSON"); - Ok(Self::Settings(json)) - } -} - -impl qualified_occasional_row_update::Update { - pub fn data(matrix: &Matrix, row_index: usize) -> ClipEngineResult { - let row = matrix.get_row(row_index)?; - let json = serde_json::to_string(&row.save()).expect("couldn't represent row as JSON"); - Ok(Self::Data(json)) - } -} - -impl qualified_occasional_clip_update::Update { - pub fn complete_persistent_data(_matrix: &Matrix, clip: &Clip) -> ClipEngineResult { - let api_clip = clip.save()?; - let json = serde_json::to_string(&api_clip).expect("couldn't represent clip as JSON"); - Ok(Self::CompletePersistentData(json)) - } -} - -impl HistoryState { - pub fn from_engine(history: &History) -> Self { - Self { - undo_label: history - .next_undo_label() - .map(|l| l.to_string()) - .unwrap_or_default(), - redo_label: history - .next_redo_label() - .map(|l| l.to_string()) - .unwrap_or_default(), - } - } -} - -impl SlotAddress { - pub fn from_engine(address: ClipSlotAddress) -> Self { - Self { - column_index: address.column() as _, - row_index: address.row() as _, - } - } - - pub fn to_engine(&self) -> ClipSlotAddress { - ClipSlotAddress::new(self.column_index as _, self.row_index as _) - } -} - -impl ClipAddress { - pub fn from_engine(address: base::ClipAddress) -> Self { - Self { - slot_address: Some(SlotAddress::from_engine(address.slot_address)), - clip_index: address.clip_index as _, - } - } - - pub fn to_engine(&self) -> Result { - let addr = base::ClipAddress { - slot_address: self - .slot_address - .as_ref() - .ok_or("slot address missing")? - .to_engine(), - clip_index: self.clip_index as usize, - }; - Ok(addr) - } -} - -impl SlotPlayState { - pub fn from_engine(play_state: ClipPlayState) -> Self { - use ClipPlayState::*; - match play_state { - Stopped => Self::Stopped, - ScheduledForPlayStart => Self::ScheduledForPlayStart, - Playing => Self::Playing, - Paused => Self::Paused, - ScheduledForPlayStop => Self::ScheduledForPlayStop, - ScheduledForRecordingStart => Self::ScheduledForRecordingStart, - Recording => Self::Recording, - ScheduledForRecordingStop => Self::ScheduledForRecordingStop, - } - } -} - -impl TrackColor { - pub fn from_engine(color: Option) -> Self { - Self { - color: color - .map(|c| (((c.r as u32) << 16) + ((c.g as u32) << 8) + (c.b as u32)) as i32), - } - } -} - -impl TrackInput { - pub fn from_engine(input: Option) -> Self { - use RecordingInput::*; - let input = match input { - Some(Mono(ch)) => Some(Input::Mono(ch)), - Some(Stereo(ch)) => Some(Input::Stereo(ch)), - Some(Midi { device_id, channel }) => { - let midi_input = TrackMidiInput { - device: device_id.map(|id| id.get() as _), - channel: channel.map(|ch| ch.get() as _), - }; - Some(Input::Midi(midi_input)) - } - _ => None, - }; - Self { input } - } -} - -impl TrackInputMonitoring { - pub fn from_engine(mode: InputMonitoringMode) -> Self { - match mode { - InputMonitoringMode::Off => Self::Off, - InputMonitoringMode::Normal => Self::Normal, - InputMonitoringMode::NotWhenPlaying => Self::TapeStyle, - InputMonitoringMode::Unknown(_) => Self::Unknown, - } - } -} - -impl ArrangementPlayState { - pub fn from_engine(play_state: reaper_medium::PlayState) -> Self { - if play_state.is_recording { - if play_state.is_paused { - Self::RecordingPaused - } else { - Self::Recording - } - } else if play_state.is_playing { - if play_state.is_paused { - Self::PlayingPaused - } else { - Self::Playing - } - } else if play_state.is_paused { - Self::PlayingPaused - } else { - Self::Stopped - } - } -} diff --git a/playtime-clip-engine/src/proto/hub.rs b/playtime-clip-engine/src/proto/hub.rs deleted file mode 100644 index 7f1a13343..000000000 --- a/playtime-clip-engine/src/proto/hub.rs +++ /dev/null @@ -1,502 +0,0 @@ -use crate::base::{ClipMatrixEvent, Matrix}; -use crate::proto::clip_engine_server::ClipEngineServer; -use crate::proto::senders::{ - ClipEngineSenders, ContinuousColumnUpdateBatch, ContinuousMatrixUpdateBatch, - ContinuousSlotUpdateBatch, OccasionalClipUpdateBatch, OccasionalColumnUpdateBatch, - OccasionalMatrixUpdateBatch, OccasionalRowUpdateBatch, OccasionalSlotUpdateBatch, - OccasionalTrackUpdateBatch, -}; -use crate::proto::{ - occasional_matrix_update, occasional_track_update, qualified_occasional_clip_update, - qualified_occasional_column_update, qualified_occasional_row_update, - qualified_occasional_slot_update, ClipEngineService, ContinuousClipUpdate, - ContinuousColumnUpdate, ContinuousMatrixUpdate, ContinuousSlotUpdate, MatrixProvider, - OccasionalMatrixUpdate, OccasionalTrackUpdate, QualifiedContinuousSlotUpdate, - QualifiedOccasionalClipUpdate, QualifiedOccasionalColumnUpdate, QualifiedOccasionalRowUpdate, - QualifiedOccasionalSlotUpdate, QualifiedOccasionalTrackUpdate, SlotAddress, -}; -use crate::rt::{ - ClipChangeEvent, QualifiedClipChangeEvent, QualifiedSlotChangeEvent, SlotChangeEvent, -}; -use crate::{clip_timeline, proto, Laziness, Timeline}; -use playtime_api::persistence::EvenQuantization; -use reaper_high::{ - AvailablePanValue, ChangeEvent, Guid, OrCurrentProject, PanExt, Project, Reaper, Track, Volume, -}; -use reaper_medium::TrackAttributeKey; -use std::collections::HashMap; - -#[derive(Debug)] -pub struct ClipEngineHub { - senders: ClipEngineSenders, -} - -impl Default for ClipEngineHub { - fn default() -> Self { - Self::new() - } -} - -impl ClipEngineHub { - pub fn new() -> Self { - Self { - senders: ClipEngineSenders::new(), - } - } - - pub fn create_service( - &self, - matrix_provider: P, - ) -> ClipEngineServer> { - ClipEngineServer::new(ClipEngineService::new( - matrix_provider, - self.senders.clone(), - )) - } - - pub fn clip_matrix_changed( - &self, - matrix_id: &str, - matrix: &Matrix, - events: &[ClipMatrixEvent], - is_poll: bool, - project: Option, - ) { - self.send_occasional_matrix_updates_caused_by_matrix(matrix_id, matrix, events); - self.send_occasional_column_updates(matrix_id, matrix, events); - self.send_occasional_row_updates(matrix_id, matrix, events); - self.send_occasional_slot_updates(matrix_id, matrix, events); - self.send_occasional_clip_updates(matrix_id, matrix, events); - self.send_continuous_slot_updates(matrix_id, events); - if is_poll { - self.send_continuous_matrix_updates(matrix_id, project); - self.send_continuous_column_updates(matrix_id, matrix); - } - } - - fn send_occasional_matrix_updates_caused_by_matrix( - &self, - matrix_id: &str, - matrix: &Matrix, - events: &[ClipMatrixEvent], - ) { - let sender = &self.senders.occasional_matrix_update_sender; - if sender.receiver_count() == 0 { - return; - } - // TODO-high-clip-engine-performance Push persistent matrix state only once (even if many events) - let updates: Vec<_> = events - .iter() - .filter_map(|event| match event { - ClipMatrixEvent::EverythingChanged => Some(OccasionalMatrixUpdate { - update: Some(occasional_matrix_update::Update::complete_persistent_data( - matrix, - )), - }), - ClipMatrixEvent::MatrixSettingsChanged => Some(OccasionalMatrixUpdate { - update: Some(occasional_matrix_update::Update::settings(matrix)), - }), - ClipMatrixEvent::HistoryChanged => Some(OccasionalMatrixUpdate { - update: Some(occasional_matrix_update::Update::history_state(matrix)), - }), - _ => None, - }) - .collect(); - if !updates.is_empty() { - let batch_event = OccasionalMatrixUpdateBatch { - session_id: matrix_id.to_string(), - value: updates, - }; - let _ = sender.send(batch_event); - } - } - - fn send_occasional_column_updates( - &self, - matrix_id: &str, - matrix: &Matrix, - events: &[ClipMatrixEvent], - ) { - let sender = &self.senders.occasional_column_update_sender; - if sender.receiver_count() == 0 { - return; - } - let updates: Vec<_> = events - .iter() - .filter_map(|event| match event { - ClipMatrixEvent::ColumnSettingsChanged(column_index) => { - let update = - qualified_occasional_column_update::Update::settings(matrix, *column_index) - .ok()?; - Some(QualifiedOccasionalColumnUpdate { - column_index: *column_index as _, - update: Some(update), - }) - } - _ => None, - }) - .collect(); - if !updates.is_empty() { - let batch_event = OccasionalColumnUpdateBatch { - session_id: matrix_id.to_string(), - value: updates, - }; - let _ = sender.send(batch_event); - } - } - fn send_occasional_row_updates( - &self, - matrix_id: &str, - matrix: &Matrix, - events: &[ClipMatrixEvent], - ) { - let sender = &self.senders.occasional_row_update_sender; - if sender.receiver_count() == 0 { - return; - } - let updates: Vec<_> = events - .iter() - .filter_map(|event| match event { - ClipMatrixEvent::RowChanged(row_index) => { - let update = - qualified_occasional_row_update::Update::data(matrix, *row_index).ok()?; - Some(QualifiedOccasionalRowUpdate { - row_index: *row_index as _, - update: Some(update), - }) - } - _ => None, - }) - .collect(); - if !updates.is_empty() { - let batch_event = OccasionalRowUpdateBatch { - session_id: matrix_id.to_string(), - value: updates, - }; - let _ = sender.send(batch_event); - } - } - - fn send_occasional_slot_updates( - &self, - matrix_id: &str, - matrix: &Matrix, - events: &[ClipMatrixEvent], - ) { - let sender = &self.senders.occasional_slot_update_sender; - if sender.receiver_count() == 0 { - return; - } - let updates: Vec<_> = events - .iter() - .filter_map(|event| match event { - ClipMatrixEvent::SlotChanged(QualifiedSlotChangeEvent { - slot_address: slot_coordinates, - event, - }) => { - use SlotChangeEvent::*; - let update = match event { - PlayState(play_state) => { - qualified_occasional_slot_update::Update::play_state(*play_state) - } - Clips(_) => { - let slot = matrix.find_slot(*slot_coordinates)?; - qualified_occasional_slot_update::Update::complete_persistent_data( - matrix, slot, - ) - } - Continuous { .. } => return None, - }; - Some(QualifiedOccasionalSlotUpdate { - slot_address: Some(SlotAddress::from_engine(*slot_coordinates)), - update: Some(update), - }) - } - _ => None, - }) - .collect(); - if !updates.is_empty() { - let batch_event = OccasionalSlotUpdateBatch { - session_id: matrix_id.to_string(), - value: updates, - }; - let _ = sender.send(batch_event); - } - } - - fn send_occasional_clip_updates( - &self, - matrix_id: &str, - matrix: &Matrix, - events: &[ClipMatrixEvent], - ) { - let sender = &self.senders.occasional_clip_update_sender; - if sender.receiver_count() == 0 { - return; - } - let updates: Vec<_> = events - .iter() - .filter_map(|event| match event { - ClipMatrixEvent::ClipChanged(QualifiedClipChangeEvent { - clip_address, - event, - }) => { - use ClipChangeEvent::*; - let update = match event { - Everything | Volume(_) | Looped(_) => { - let clip = matrix.find_clip(*clip_address)?; - qualified_occasional_clip_update::Update::complete_persistent_data( - matrix, clip, - ) - .ok()? - } - }; - Some(QualifiedOccasionalClipUpdate { - clip_address: Some(proto::ClipAddress::from_engine(*clip_address)), - update: Some(update), - }) - } - _ => None, - }) - .collect(); - if !updates.is_empty() { - let batch_event = OccasionalClipUpdateBatch { - session_id: matrix_id.to_string(), - value: updates, - }; - let _ = sender.send(batch_event); - } - } - - pub fn send_occasional_matrix_updates_caused_by_reaper( - &self, - matrix_id: &str, - matrix: &Matrix, - event: &ChangeEvent, - ) { - use occasional_track_update::Update; - enum R { - Matrix(occasional_matrix_update::Update), - Track(QualifiedOccasionalTrackUpdate), - } - let matrix_update_sender = &self.senders.occasional_matrix_update_sender; - let track_update_sender = &self.senders.occasional_track_update_sender; - if matrix_update_sender.receiver_count() == 0 && track_update_sender.receiver_count() == 0 { - return; - } - fn track_update(track: &Track, create_update: impl FnOnce() -> Update) -> R { - R::Track(QualifiedOccasionalTrackUpdate { - track_id: track.guid().to_string_without_braces(), - track_updates: vec![OccasionalTrackUpdate { - update: Some(create_update()), - }], - }) - } - fn column_track_update( - matrix: &Matrix, - track: &Track, - create_update: impl FnOnce() -> Update, - ) -> Option { - if matrix.uses_playback_track(track) { - Some(track_update(track, create_update)) - } else { - None - } - } - let update: Option = match event { - ChangeEvent::TrackVolumeChanged(e) => { - let db = Volume::from_reaper_value(e.new_value).db(); - if e.track.is_master_track() { - Some(R::Matrix(occasional_matrix_update::Update::volume(db))) - } else { - column_track_update(matrix, &e.track, || Update::volume(db)) - } - } - ChangeEvent::TrackPanChanged(e) => { - let val = match e.new_value { - AvailablePanValue::Complete(v) => v.main_pan(), - AvailablePanValue::Incomplete(v) => v, - }; - if e.track.is_master_track() { - Some(R::Matrix(occasional_matrix_update::Update::pan(val))) - } else { - column_track_update(matrix, &e.track, || Update::pan(val)) - } - } - ChangeEvent::TrackNameChanged(e) => { - Some(track_update(&e.track, || Update::name(&e.track))) - } - ChangeEvent::TrackInputChanged(e) => { - column_track_update(matrix, &e.track, || Update::input(e.new_value)) - } - ChangeEvent::TrackInputMonitoringChanged(e) => { - column_track_update(matrix, &e.track, || Update::input_monitoring(e.new_value)) - } - ChangeEvent::TrackArmChanged(e) => { - column_track_update(matrix, &e.track, || Update::armed(e.new_value)) - } - ChangeEvent::TrackMuteChanged(e) => { - column_track_update(matrix, &e.track, || Update::mute(e.new_value)) - } - ChangeEvent::TrackSoloChanged(e) => { - column_track_update(matrix, &e.track, || Update::solo(e.new_value)) - } - ChangeEvent::TrackSelectedChanged(e) => { - column_track_update(matrix, &e.track, || Update::selected(e.new_value)) - } - ChangeEvent::MasterTempoChanged(e) => { - // TODO-high-clip-engine Also notify correctly about time signature changes. Looks like - // MasterTempoChanged event doesn't fire in that case :( - Some(R::Matrix(occasional_matrix_update::Update::tempo( - e.new_value, - ))) - } - ChangeEvent::PlayStateChanged(e) => Some(R::Matrix( - occasional_matrix_update::Update::arrangement_play_state(e.new_value), - )), - _ => None, - }; - if let Some(update) = update { - match update { - R::Matrix(u) => { - let _ = matrix_update_sender.send(OccasionalMatrixUpdateBatch { - session_id: matrix_id.to_string(), - value: vec![OccasionalMatrixUpdate { update: Some(u) }], - }); - } - R::Track(u) => { - let _ = track_update_sender.send(OccasionalTrackUpdateBatch { - session_id: matrix_id.to_string(), - value: vec![u], - }); - } - } - } - } - - fn send_continuous_slot_updates(&self, matrix_id: &str, events: &[ClipMatrixEvent]) { - let sender = &self.senders.continuous_slot_update_sender; - if sender.receiver_count() == 0 { - return; - } - let updates: Vec<_> = events - .iter() - .filter_map(|event| { - if let ClipMatrixEvent::SlotChanged(QualifiedSlotChangeEvent { - slot_address: slot_coordinates, - event: - SlotChangeEvent::Continuous { - proportional, - seconds, - peak, - }, - }) = event - { - Some(QualifiedContinuousSlotUpdate { - slot_address: Some(SlotAddress::from_engine(*slot_coordinates)), - update: Some(ContinuousSlotUpdate { - // TODO-high-clip-engine Send for each clip - clip_update: vec![ - (ContinuousClipUpdate { - proportional_position: proportional.get(), - position_in_seconds: seconds.get(), - peak: peak.get(), - }), - ], - }), - }) - } else { - None - } - }) - .collect(); - if !updates.is_empty() { - let batch_event = ContinuousSlotUpdateBatch { - session_id: matrix_id.to_string(), - value: updates, - }; - let _ = sender.send(batch_event); - } - } - - fn send_continuous_matrix_updates(&self, matrix_id: &str, project: Option) { - let sender = &self.senders.continuous_matrix_update_sender; - if sender.receiver_count() == 0 { - return; - } - let timeline = clip_timeline(project, false); - let pos = timeline.cursor_pos(); - let bar_quantization = EvenQuantization::ONE_BAR; - let next_bar = - timeline.next_quantized_pos_at(pos, bar_quantization, Laziness::EagerForNextPos); - // TODO-high-clip-engine CONTINUE We are mainly interested in beats relative to the bar in order to get a - // typical position display and a useful visual metronome! - let full_beats = timeline.full_beats_at_pos(pos); - let batch_event = ContinuousMatrixUpdateBatch { - session_id: matrix_id.to_string(), - value: ContinuousMatrixUpdate { - second: pos.get(), - bar: (next_bar.position() - 1) as i32, - beat: full_beats.get(), - peaks: project - .or_current_project() - .master_track() - .map(|t| get_track_peaks(&t)) - .unwrap_or_default(), - }, - }; - let _ = sender.send(batch_event); - } - - fn send_continuous_column_updates(&self, matrix_id: &str, matrix: &Matrix) { - let sender = &self.senders.continuous_column_update_sender; - if sender.receiver_count() == 0 { - return; - } - let column_update_by_track_guid: HashMap = - HashMap::with_capacity(matrix.column_count()); - let column_updates: Vec<_> = matrix - .all_columns() - .map(|column| { - if let Ok(track) = column.playback_track() { - if let Some(existing_update) = column_update_by_track_guid.get(track.guid()) { - // We have already collected the update for this column's playback track. - existing_update.clone() - } else { - // We haven't yet collected the update for this column's playback track. - ContinuousColumnUpdate { - peaks: get_track_peaks(track), - } - } - } else { - ContinuousColumnUpdate { peaks: vec![] } - } - }) - .collect(); - if !column_updates.is_empty() { - let batch_event = ContinuousColumnUpdateBatch { - session_id: matrix_id.to_string(), - value: column_updates, - }; - let _ = sender.send(batch_event); - } - } -} - -fn get_track_peaks(track: &Track) -> Vec { - let reaper = Reaper::get().medium_reaper(); - let track = track.raw(); - let channel_count = - unsafe { reaper.get_media_track_info_value(track, TrackAttributeKey::Nchan) as i32 }; - if channel_count <= 0 { - return vec![]; - } - // TODO-high-clip-engine CONTINUE Apply same fix as in #560 (check I_VUMODE to know whether to query volume or peaks) - // TODO-high-clip-engine CONTINUE Respect solo (same as a recent ReaLearn issue) - (0..channel_count) - .map(|ch| { - let volume = unsafe { reaper.track_get_peak_info(track, ch as u32) }; - volume.get() - }) - .collect() -} diff --git a/playtime-clip-engine/src/proto/mod.rs b/playtime-clip-engine/src/proto/mod.rs deleted file mode 100644 index 566e06598..000000000 --- a/playtime-clip-engine/src/proto/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -mod clip_engine; -pub use clip_engine::*; - -mod clip_engine_ext; -pub use clip_engine_ext::*; - -mod service; -pub use service::*; - -mod hub; -pub use hub::*; - -mod senders; diff --git a/playtime-clip-engine/src/proto/senders.rs b/playtime-clip-engine/src/proto/senders.rs deleted file mode 100644 index e4af3449b..000000000 --- a/playtime-clip-engine/src/proto/senders.rs +++ /dev/null @@ -1,53 +0,0 @@ -use crate::proto::{ - ContinuousColumnUpdate, ContinuousMatrixUpdate, OccasionalMatrixUpdate, - QualifiedContinuousSlotUpdate, QualifiedOccasionalClipUpdate, QualifiedOccasionalColumnUpdate, - QualifiedOccasionalRowUpdate, QualifiedOccasionalSlotUpdate, QualifiedOccasionalTrackUpdate, -}; -use tokio::sync::broadcast::Sender; - -/// This must be a global object because it's responsible for supplying one gRPC endpoint with -/// streaming data and we have only one endpoint for all matrices. -#[derive(Clone, Debug)] -pub struct ClipEngineSenders { - pub occasional_matrix_update_sender: Sender, - pub occasional_track_update_sender: Sender, - pub occasional_column_update_sender: Sender, - pub occasional_row_update_sender: Sender, - pub occasional_slot_update_sender: Sender, - pub occasional_clip_update_sender: Sender, - pub continuous_matrix_update_sender: Sender, - pub continuous_column_update_sender: Sender, - pub continuous_slot_update_sender: Sender, -} - -impl ClipEngineSenders { - pub fn new() -> Self { - Self { - occasional_matrix_update_sender: tokio::sync::broadcast::channel(100).0, - occasional_track_update_sender: tokio::sync::broadcast::channel(100).0, - occasional_column_update_sender: tokio::sync::broadcast::channel(100).0, - occasional_row_update_sender: tokio::sync::broadcast::channel(100).0, - occasional_slot_update_sender: tokio::sync::broadcast::channel(100).0, - occasional_clip_update_sender: tokio::sync::broadcast::channel(100).0, - continuous_slot_update_sender: tokio::sync::broadcast::channel(1000).0, - continuous_column_update_sender: tokio::sync::broadcast::channel(500).0, - continuous_matrix_update_sender: tokio::sync::broadcast::channel(500).0, - } - } -} - -#[derive(Clone)] -pub struct WithSessionId { - pub session_id: String, - pub value: T, -} - -pub type OccasionalMatrixUpdateBatch = WithSessionId>; -pub type OccasionalTrackUpdateBatch = WithSessionId>; -pub type OccasionalColumnUpdateBatch = WithSessionId>; -pub type OccasionalRowUpdateBatch = WithSessionId>; -pub type OccasionalSlotUpdateBatch = WithSessionId>; -pub type OccasionalClipUpdateBatch = WithSessionId>; -pub type ContinuousMatrixUpdateBatch = WithSessionId; -pub type ContinuousColumnUpdateBatch = WithSessionId>; -pub type ContinuousSlotUpdateBatch = WithSessionId>; diff --git a/playtime-clip-engine/src/proto/service.rs b/playtime-clip-engine/src/proto/service.rs deleted file mode 100644 index 53111f0a6..000000000 --- a/playtime-clip-engine/src/proto/service.rs +++ /dev/null @@ -1,906 +0,0 @@ -use crate::base::Matrix; -use crate::base::{ClipAddress, ClipSlotAddress}; -use crate::proto; -use crate::proto::senders::{ClipEngineSenders, WithSessionId}; -use crate::proto::{ - clip_engine_server, occasional_matrix_update, occasional_track_update, - qualified_occasional_slot_update, DragColumnAction, DragColumnRequest, DragRowAction, - DragRowRequest, DragSlotAction, DragSlotRequest, Empty, FullClipAddress, FullColumnAddress, - FullRowAddress, FullSlotAddress, GetAllTracksReply, GetAllTracksRequest, GetClipDetailReply, - GetClipDetailRequest, GetContinuousColumnUpdatesReply, GetContinuousColumnUpdatesRequest, - GetContinuousMatrixUpdatesReply, GetContinuousMatrixUpdatesRequest, - GetContinuousSlotUpdatesReply, GetContinuousSlotUpdatesRequest, GetOccasionalClipUpdatesReply, - GetOccasionalClipUpdatesRequest, GetOccasionalColumnUpdatesReply, - GetOccasionalColumnUpdatesRequest, GetOccasionalMatrixUpdatesReply, - GetOccasionalMatrixUpdatesRequest, GetOccasionalRowUpdatesReply, - GetOccasionalRowUpdatesRequest, GetOccasionalSlotUpdatesReply, GetOccasionalSlotUpdatesRequest, - GetOccasionalTrackUpdatesReply, GetOccasionalTrackUpdatesRequest, OccasionalMatrixUpdate, - OccasionalTrackUpdate, QualifiedOccasionalSlotUpdate, QualifiedOccasionalTrackUpdate, - SetClipDataRequest, SetClipNameRequest, SetColumnNameRequest, SetColumnPanRequest, - SetColumnSettingsRequest, SetColumnTrackRequest, SetColumnVolumeRequest, SetMatrixPanRequest, - SetMatrixSettingsRequest, SetMatrixTempoRequest, SetMatrixVolumeRequest, SetRowDataRequest, - SlotAddress, TriggerClipAction, TriggerClipRequest, TriggerColumnAction, TriggerColumnRequest, - TriggerMatrixAction, TriggerMatrixRequest, TriggerRowAction, TriggerRowRequest, - TriggerSlotAction, TriggerSlotRequest, -}; -use crate::rt::ColumnPlaySlotOptions; -use base::future_util; -use base::tracing_util::ok_or_log_as_warn; -use futures::{FutureExt, Stream, StreamExt}; -use playtime_api::persistence::TrackId; -use reaper_high::{ - GroupingBehavior, Guid, OrCurrentProject, Pan, Project, Reaper, Tempo, Track, Volume, -}; -use reaper_medium::{Bpm, CommandId, Db, GangBehavior, ReaperPanValue, SoloMode, UndoBehavior}; -use std::collections::HashMap; -use std::pin::Pin; -use std::{future, iter}; -use tokio::sync::broadcast::Receiver; -use tokio_stream::wrappers::BroadcastStream; -use tonic::{Request, Response, Status}; - -#[derive(Debug)] -pub struct ClipEngineService

{ - matrix_provider: P, - senders: ClipEngineSenders, -} - -impl ClipEngineService

{ - pub(crate) fn new(matrix_provider: P, senders: ClipEngineSenders) -> Self { - Self { - matrix_provider, - senders, - } - } -} - -pub trait MatrixProvider: Send + Sync + 'static { - fn with_matrix( - &self, - clip_matrix_id: &str, - f: impl FnOnce(&Matrix) -> R, - ) -> Result; - - fn with_matrix_mut( - &self, - clip_matrix_id: &str, - f: impl FnOnce(&mut Matrix) -> R, - ) -> Result; -} - -impl ClipEngineService

{ - fn handle_matrix_command( - &self, - matrix_id: &str, - handler: impl FnOnce(&mut Matrix) -> Result<(), &'static str>, - ) -> Result, Status> { - self.handle_matrix_internal(matrix_id, handler)?; - Ok(Response::new(Empty {})) - } - - fn handle_matrix_internal( - &self, - matrix_id: &str, - handler: impl FnOnce(&mut Matrix) -> Result, - ) -> Result { - let r = self - .matrix_provider - .with_matrix_mut(matrix_id, handler) - .map_err(Status::unknown)? - .map_err(Status::not_found)?; - Ok(r) - } - - fn handle_column_command( - &self, - full_column_id: &Option, - handler: impl FnOnce(&mut Matrix, usize) -> Result<(), &'static str>, - ) -> Result, Status> { - self.handle_column_internal(full_column_id, handler)?; - Ok(Response::new(Empty {})) - } - - fn handle_column_internal( - &self, - full_column_id: &Option, - handler: impl FnOnce(&mut Matrix, usize) -> Result, - ) -> Result { - let full_column_id = full_column_id - .as_ref() - .ok_or_else(|| Status::invalid_argument("need full column address"))?; - let column_index = full_column_id.column_index as usize; - self.handle_matrix_internal(&full_column_id.matrix_id, |matrix| { - handler(matrix, column_index) - }) - } - - fn handle_row_command( - &self, - full_row_id: &Option, - handler: impl FnOnce(&mut Matrix, usize) -> Result<(), &'static str>, - ) -> Result, Status> { - let full_row_id = full_row_id - .as_ref() - .ok_or_else(|| Status::invalid_argument("need full row address"))?; - let row_index = full_row_id.row_index as usize; - self.handle_matrix_command(&full_row_id.matrix_id, |matrix| handler(matrix, row_index)) - } - - fn handle_slot_command( - &self, - full_slot_address: &Option, - handler: impl FnOnce(&mut Matrix, ClipSlotAddress) -> Result<(), &'static str>, - ) -> Result, Status> { - let full_slot_address = full_slot_address - .as_ref() - .ok_or_else(|| Status::invalid_argument("need full slot address"))?; - let slot_addr = convert_slot_address_to_engine(&full_slot_address.slot_address)?; - self.handle_matrix_command(&full_slot_address.matrix_id, |matrix| { - handler(matrix, slot_addr) - }) - } - - fn handle_clip_command( - &self, - full_clip_address: &Option, - handler: impl FnOnce(&mut Matrix, ClipAddress) -> Result<(), &'static str>, - ) -> Result, Status> { - self.handle_clip_internal(full_clip_address, handler)?; - Ok(Response::new(Empty {})) - } - - fn handle_clip_internal( - &self, - full_clip_address: &Option, - handler: impl FnOnce(&mut Matrix, ClipAddress) -> Result, - ) -> Result { - let full_clip_address = full_clip_address - .as_ref() - .ok_or_else(|| Status::invalid_argument("need full clip address"))?; - let clip_addr = convert_clip_address_to_engine(&full_clip_address.clip_address)?; - self.handle_matrix_internal(&full_clip_address.matrix_id, |matrix| { - handler(matrix, clip_addr) - }) - } -} - -#[tonic::async_trait] -impl clip_engine_server::ClipEngine for ClipEngineService

{ - type GetContinuousMatrixUpdatesStream = - SyncBoxStream<'static, Result>; - - async fn get_continuous_matrix_updates( - &self, - request: Request, - ) -> Result, Status> { - let receiver = self.senders.continuous_matrix_update_sender.subscribe(); - stream_by_session_id( - request.into_inner().matrix_id, - receiver, - |matrix_update| GetContinuousMatrixUpdatesReply { - matrix_update: Some(matrix_update), - }, - iter::empty(), - ) - } - type GetContinuousColumnUpdatesStream = - SyncBoxStream<'static, Result>; - async fn get_continuous_column_updates( - &self, - request: Request, - ) -> Result, Status> { - let receiver = self.senders.continuous_column_update_sender.subscribe(); - stream_by_session_id( - request.into_inner().matrix_id, - receiver, - |column_updates| GetContinuousColumnUpdatesReply { column_updates }, - iter::empty(), - ) - } - - type GetOccasionalColumnUpdatesStream = - SyncBoxStream<'static, Result>; - - type GetOccasionalRowUpdatesStream = - SyncBoxStream<'static, Result>; - - type GetOccasionalSlotUpdatesStream = - SyncBoxStream<'static, Result>; - - async fn get_occasional_slot_updates( - &self, - request: Request, - ) -> Result, Status> { - // Initial - let initial_slot_updates = self - .matrix_provider - .with_matrix(&request.get_ref().matrix_id, |matrix| { - matrix - .all_slots() - .map(|slot| { - let play_state = slot.value().play_state(); - let address = SlotAddress { - column_index: slot.column_index() as u32, - row_index: slot.value().index() as u32, - }; - QualifiedOccasionalSlotUpdate { - slot_address: Some(address), - update: Some(qualified_occasional_slot_update::Update::play_state( - play_state, - )), - } - }) - .collect() - }) - .map_err(Status::not_found)?; - let initial_reply = GetOccasionalSlotUpdatesReply { - slot_updates: initial_slot_updates, - }; - // On change - let receiver = self.senders.occasional_slot_update_sender.subscribe(); - stream_by_session_id( - request.into_inner().matrix_id, - receiver, - |slot_updates| GetOccasionalSlotUpdatesReply { slot_updates }, - Some(initial_reply).into_iter(), - ) - } - - async fn get_occasional_column_updates( - &self, - request: Request, - ) -> Result, Status> { - // On change - let receiver = self.senders.occasional_column_update_sender.subscribe(); - stream_by_session_id( - request.into_inner().matrix_id, - receiver, - |column_updates| GetOccasionalColumnUpdatesReply { column_updates }, - iter::empty(), - ) - } - - async fn get_occasional_row_updates( - &self, - request: Request, - ) -> Result, Status> { - // On change - let receiver = self.senders.occasional_row_update_sender.subscribe(); - stream_by_session_id( - request.into_inner().matrix_id, - receiver, - |row_updates| GetOccasionalRowUpdatesReply { row_updates }, - iter::empty(), - ) - } - - type GetOccasionalClipUpdatesStream = - SyncBoxStream<'static, Result>; - - async fn get_occasional_clip_updates( - &self, - request: Request, - ) -> Result, Status> { - let receiver = self.senders.occasional_clip_update_sender.subscribe(); - stream_by_session_id( - request.into_inner().matrix_id, - receiver, - |clip_updates| GetOccasionalClipUpdatesReply { clip_updates }, - iter::empty(), - ) - } - - type GetContinuousSlotUpdatesStream = - SyncBoxStream<'static, Result>; - - async fn get_continuous_slot_updates( - &self, - request: Request, - ) -> Result, Status> { - let receiver = self.senders.continuous_slot_update_sender.subscribe(); - stream_by_session_id( - request.into_inner().matrix_id, - receiver, - |slot_updates| GetContinuousSlotUpdatesReply { slot_updates }, - iter::empty(), - ) - } - - type GetOccasionalMatrixUpdatesStream = - SyncBoxStream<'static, Result>; - - async fn get_occasional_matrix_updates( - &self, - request: Request, - ) -> Result, Status> { - use occasional_matrix_update::Update; - let initial_matrix_updates = self - .matrix_provider - .with_matrix( - &request.get_ref().matrix_id, - |matrix| -> Result<_, &'static str> { - let project = matrix.permanent_project().or_current_project(); - let master_track = project.master_track()?; - let updates = [ - Update::volume(master_track.volume().db()), - Update::pan(master_track.pan().reaper_value()), - Update::tempo(project.tempo().bpm()), - Update::arrangement_play_state(project.play_state()), - // TODO MIDI input devices - // TODO audio input channels - Update::complete_persistent_data(matrix), - Update::history_state(matrix), - // TODO click enabled - Update::time_signature(project), - ]; - let updates: Vec<_> = updates - .into_iter() - .map(|u| OccasionalMatrixUpdate { update: Some(u) }) - .collect(); - Ok(updates) - }, - ) - .map_err(Status::not_found)? - .map_err(Status::unknown)?; - let initial_reply = GetOccasionalMatrixUpdatesReply { - matrix_updates: initial_matrix_updates, - }; - let receiver = self.senders.occasional_matrix_update_sender.subscribe(); - stream_by_session_id( - request.into_inner().matrix_id, - receiver, - |matrix_updates| GetOccasionalMatrixUpdatesReply { matrix_updates }, - Some(initial_reply).into_iter(), - ) - } - - type GetOccasionalTrackUpdatesStream = - SyncBoxStream<'static, Result>; - - async fn get_occasional_track_updates( - &self, - request: Request, - ) -> Result, Status> { - let initial_track_updates = self - .matrix_provider - .with_matrix(&request.get_ref().matrix_id, |matrix| { - let track_by_guid: HashMap = matrix - .all_columns() - .flat_map(|column| { - column - .playback_track() - .into_iter() - .cloned() - .chain(column.effective_recording_track().into_iter()) - }) - .map(|track| (*track.guid(), track)) - .collect(); - track_by_guid - .into_iter() - .map(|(guid, track)| { - use occasional_track_update::Update; - QualifiedOccasionalTrackUpdate { - track_id: guid.to_string_without_braces(), - track_updates: [ - Update::name(&track), - Update::color(&track), - Update::input(track.recording_input()), - Update::armed(track.is_armed(false)), - Update::input_monitoring(track.input_monitoring_mode()), - Update::mute(track.is_muted()), - Update::solo(track.is_solo()), - Update::selected(track.is_selected()), - Update::volume(track.volume().db()), - Update::pan(track.pan().reaper_value()), - ] - .into_iter() - .map(|update| OccasionalTrackUpdate { - update: Some(update), - }) - .collect(), - } - }) - .collect() - }) - .map_err(Status::not_found)?; - let initial_reply = GetOccasionalTrackUpdatesReply { - track_updates: initial_track_updates, - }; - let receiver = self.senders.occasional_track_update_sender.subscribe(); - stream_by_session_id( - request.into_inner().matrix_id, - receiver, - |track_updates| GetOccasionalTrackUpdatesReply { track_updates }, - Some(initial_reply).into_iter(), - ) - } - - async fn trigger_slot( - &self, - request: Request, - ) -> Result, Status> { - let req = request.into_inner(); - let action = TriggerSlotAction::from_i32(req.action) - .ok_or_else(|| Status::invalid_argument("unknown trigger slot action"))?; - self.handle_slot_command(&req.slot_address, |matrix, slot_address| match action { - TriggerSlotAction::Play => { - matrix.play_slot(slot_address, ColumnPlaySlotOptions::default()) - } - TriggerSlotAction::Stop => matrix.stop_slot(slot_address, None), - TriggerSlotAction::Record => matrix.record_slot(slot_address), - TriggerSlotAction::Clear => matrix.clear_slot(slot_address), - TriggerSlotAction::Copy => matrix.copy_slot(slot_address), - TriggerSlotAction::Cut => matrix.cut_slot(slot_address), - TriggerSlotAction::Paste => matrix.paste_slot(slot_address), - TriggerSlotAction::FillWithSelectedItem => { - matrix.replace_slot_clips_with_selected_item(slot_address) - } - TriggerSlotAction::Panic => matrix.panic_slot(slot_address), - }) - } - - async fn trigger_clip( - &self, - request: Request, - ) -> Result, Status> { - let req = request.into_inner(); - let action = TriggerClipAction::from_i32(req.action) - .ok_or_else(|| Status::invalid_argument("unknown trigger clip action"))?; - self.handle_clip_command(&req.clip_address, |matrix, clip_address| match action { - TriggerClipAction::MidiOverdub => matrix.midi_overdub_clip(clip_address), - TriggerClipAction::Edit => matrix.start_editing_clip(clip_address), - }) - } - - async fn drag_slot( - &self, - request: Request, - ) -> Result, Status> { - let req = request.into_inner(); - let action = DragSlotAction::from_i32(req.action) - .ok_or_else(|| Status::invalid_argument("unknown drag slot action"))?; - let source_slot_address = convert_slot_address_to_engine(&req.source_slot_address)?; - let dest_slot_address = convert_slot_address_to_engine(&req.destination_slot_address)?; - self.handle_matrix_command(&req.matrix_id, |matrix| match action { - DragSlotAction::Move => matrix.move_slot_to(source_slot_address, dest_slot_address), - DragSlotAction::Copy => matrix.copy_slot_to(source_slot_address, dest_slot_address), - }) - } - - async fn drag_row(&self, request: Request) -> Result, Status> { - let req = request.into_inner(); - let action = DragRowAction::from_i32(req.action) - .ok_or_else(|| Status::invalid_argument("unknown drag row action"))?; - self.handle_matrix_command(&req.matrix_id, |matrix| match action { - DragRowAction::MoveContent => matrix - .move_scene_content_to(req.source_row_index as _, req.destination_row_index as _), - DragRowAction::CopyContent => matrix - .copy_scene_content_to(req.source_row_index as _, req.destination_row_index as _), - DragRowAction::Reorder => { - matrix.reorder_rows(req.source_row_index as _, req.destination_row_index as _) - } - }) - } - - async fn drag_column( - &self, - request: Request, - ) -> Result, Status> { - let req = request.into_inner(); - let action = DragColumnAction::from_i32(req.action) - .ok_or_else(|| Status::invalid_argument("unknown drag column action"))?; - self.handle_matrix_command(&req.matrix_id, |matrix| match action { - DragColumnAction::Reorder => matrix.reorder_columns( - req.source_column_index as _, - req.destination_column_index as _, - ), - }) - } - - async fn set_clip_name( - &self, - request: Request, - ) -> Result, Status> { - let req = request.into_inner(); - self.handle_clip_command(&req.clip_address, |matrix, clip_address| { - matrix.set_clip_name(clip_address, req.name) - }) - } - - async fn set_clip_data( - &self, - request: Request, - ) -> Result, Status> { - let req = request.into_inner(); - let clip = - serde_json::from_str(&req.data).map_err(|e| Status::invalid_argument(e.to_string()))?; - self.handle_clip_command(&req.clip_address, |matrix, clip_address| { - matrix.set_clip_data(clip_address, clip) - }) - } - - async fn trigger_matrix( - &self, - request: Request, - ) -> Result, Status> { - let req = request.into_inner(); - let action: TriggerMatrixAction = TriggerMatrixAction::from_i32(req.action) - .ok_or_else(|| Status::invalid_argument("unknown trigger matrix action"))?; - self.handle_matrix_command(&req.matrix_id, |matrix| { - let project = matrix.permanent_project().or_current_project(); - match action { - TriggerMatrixAction::ArrangementTogglePlayStop => { - if project.is_playing() { - project.stop(); - } else { - project.play(); - } - Ok(()) - } - TriggerMatrixAction::StopAllClips => { - matrix.stop(); - Ok(()) - } - TriggerMatrixAction::ArrangementPlay => { - project.play(); - Ok(()) - } - TriggerMatrixAction::ArrangementStop => { - project.stop(); - Ok(()) - } - TriggerMatrixAction::ArrangementPause => { - project.pause(); - Ok(()) - } - TriggerMatrixAction::ArrangementStartRecording => { - Reaper::get().enable_record_in_current_project(); - Ok(()) - } - TriggerMatrixAction::ArrangementStopRecording => { - Reaper::get().disable_record_in_current_project(); - Ok(()) - } - TriggerMatrixAction::Undo => matrix.undo(), - TriggerMatrixAction::Redo => matrix.redo(), - TriggerMatrixAction::ToggleClick => Reaper::get() - .main_section() - .action_by_command_id(CommandId::new(40364)) - .invoke_as_trigger(Some(project)) - .map_err(|e| e.message()), - TriggerMatrixAction::Panic => { - matrix.panic(); - Ok(()) - } - } - }) - } - - async fn set_matrix_settings( - &self, - request: Request, - ) -> Result, Status> { - let req = request.into_inner(); - let matrix_settings = serde_json::from_str(&req.settings) - .map_err(|e| Status::invalid_argument(e.to_string()))?; - self.handle_matrix_command(&req.matrix_id, |matrix| { - matrix.set_settings(matrix_settings) - }) - } - - async fn trigger_column( - &self, - request: Request, - ) -> Result, Status> { - let req = request.into_inner(); - let action = TriggerColumnAction::from_i32(req.action) - .ok_or_else(|| Status::invalid_argument("unknown trigger column action"))?; - self.handle_column_command(&req.column_address, |matrix, column_index| match action { - TriggerColumnAction::Stop => matrix.stop_column(column_index, None), - TriggerColumnAction::ToggleMute => { - let column = matrix.get_column(column_index)?; - let track = column.playback_track()?; - track.set_mute( - !track.is_muted(), - GangBehavior::DenyGang, - GroupingBehavior::PreventGrouping, - ); - Ok(()) - } - TriggerColumnAction::ToggleSolo => { - let column = matrix.get_column(column_index)?; - let track = column.playback_track()?; - let new_solo_mode = if track.is_solo() { - SoloMode::Off - } else { - SoloMode::SoloInPlace - }; - track.set_solo_mode(new_solo_mode); - Ok(()) - } - TriggerColumnAction::ToggleArm => { - let column = matrix.get_column(column_index)?; - let track = column.playback_track()?; - track.set_armed( - !track.is_armed(false), - GangBehavior::DenyGang, - GroupingBehavior::PreventGrouping, - ); - Ok(()) - } - TriggerColumnAction::Remove => matrix.remove_column(column_index), - TriggerColumnAction::Duplicate => matrix.duplicate_column(column_index), - TriggerColumnAction::Insert => matrix.insert_column(column_index), - TriggerColumnAction::Panic => matrix.panic_column(column_index), - }) - } - - async fn set_column_settings( - &self, - request: Request, - ) -> Result, Status> { - let req = request.into_inner(); - let column_settings = serde_json::from_str(&req.settings) - .map_err(|e| Status::invalid_argument(e.to_string()))?; - self.handle_column_command(&req.column_address, |matrix, column_index| { - matrix.set_column_settings(column_index, column_settings) - }) - } - - async fn trigger_row( - &self, - request: Request, - ) -> Result, Status> { - let req = request.into_inner(); - let action = TriggerRowAction::from_i32(req.action) - .ok_or_else(|| Status::invalid_argument("unknown trigger row action"))?; - self.handle_row_command(&req.row_address, |matrix, row_index| match action { - TriggerRowAction::Play => { - matrix.play_scene(row_index); - Ok(()) - } - TriggerRowAction::Clear => matrix.clear_scene(row_index), - TriggerRowAction::Copy => matrix.copy_scene(row_index), - TriggerRowAction::Cut => matrix.cut_scene(row_index), - TriggerRowAction::Paste => matrix.paste_scene(row_index), - TriggerRowAction::Remove => matrix.remove_row(row_index), - TriggerRowAction::Duplicate => matrix.duplicate_row(row_index), - TriggerRowAction::Insert => matrix.insert_row(row_index), - TriggerRowAction::Panic => matrix.panic_row(row_index), - }) - } - - async fn set_row_data( - &self, - request: Request, - ) -> Result, Status> { - let req = request.into_inner(); - let row_data = - serde_json::from_str(&req.data).map_err(|e| Status::invalid_argument(e.to_string()))?; - self.handle_row_command(&req.row_address, |matrix, row_index| { - matrix.set_row_data(row_index, row_data) - }) - } - - async fn set_matrix_tempo( - &self, - request: Request, - ) -> Result, Status> { - let req = request.into_inner(); - let bpm = Bpm::try_from(req.bpm).map_err(|e| Status::invalid_argument(e.as_ref()))?; - self.handle_matrix_command(&req.matrix_id, |matrix| { - let project = matrix.permanent_project().or_current_project(); - project - .set_tempo(Tempo::from_bpm(bpm), UndoBehavior::OmitUndoPoint) - .map_err(|e| e.message()) - }) - } - - async fn set_matrix_volume( - &self, - request: Request, - ) -> Result, Status> { - let req = request.into_inner(); - let db = Db::try_from(req.db).map_err(|e| Status::invalid_argument(e.as_ref()))?; - self.handle_matrix_command(&req.matrix_id, |matrix| { - let project = matrix.permanent_project().or_current_project(); - project.master_track()?.set_volume( - Volume::from_db(db), - GangBehavior::DenyGang, - GroupingBehavior::PreventGrouping, - ); - Ok(()) - }) - } - - async fn set_matrix_pan( - &self, - request: Request, - ) -> Result, Status> { - let req = request.into_inner(); - let pan = - ReaperPanValue::try_from(req.pan).map_err(|e| Status::invalid_argument(e.as_ref()))?; - self.handle_matrix_command(&req.matrix_id, |matrix| { - let project = matrix.permanent_project().or_current_project(); - project.master_track()?.set_pan( - Pan::from_reaper_value(pan), - GangBehavior::DenyGang, - GroupingBehavior::PreventGrouping, - ); - Ok(()) - }) - } - - async fn set_column_volume( - &self, - request: Request, - ) -> Result, Status> { - let req = request.into_inner(); - let db = Db::try_from(req.db).map_err(|e| Status::invalid_argument(e.as_ref()))?; - self.handle_column_command(&req.column_address, |matrix, column_index| { - let column = matrix.get_column(column_index)?; - let track = column.playback_track()?; - track.set_volume( - Volume::from_db(db), - GangBehavior::DenyGang, - GroupingBehavior::PreventGrouping, - ); - Ok(()) - }) - } - - async fn set_column_pan( - &self, - request: Request, - ) -> Result, Status> { - let req = request.into_inner(); - let pan = ReaperPanValue::new(req.pan.clamp(-1.0, 1.0)); - self.handle_column_command(&req.column_address, |matrix, column_index| { - let column = matrix.get_column(column_index)?; - let track = column.playback_track()?; - track.set_pan( - Pan::from_reaper_value(pan), - GangBehavior::DenyGang, - GroupingBehavior::PreventGrouping, - ); - Ok(()) - }) - } - - async fn set_column_track( - &self, - request: Request, - ) -> Result, Status> { - // We shouldn't just change the column track directly, otherwise we get abrupt clicks - // (audio) and hanging notes (MIDI). The following is a dirty but efficient solution to - // prevent this. - // Immediately stop everything in that column (gracefully) - let req = request.into_inner(); - self.handle_column_internal(&req.column_address, |matrix, column_index| { - matrix.get_column(column_index)?.panic(); - Ok(()) - })?; - // Make sure to wait long enough until fade outs and stuff finished - future_util::millis(50).await; - // Finally change column track - self.handle_column_command(&req.column_address, |matrix, column_index| { - let track_id = req.track_id.map(TrackId::new); - matrix.set_column_playback_track(column_index, track_id.as_ref())?; - Ok(()) - }) - } - - async fn get_clip_detail( - &self, - request: Request, - ) -> Result, Status> { - let req = request.into_inner(); - let peak_file_future = - self.handle_clip_internal(&req.clip_address, |matrix, clip_address| { - let clip = matrix.get_clip(clip_address)?; - let peak_file_future = clip.peak_file_contents(matrix.permanent_project())?; - Ok(peak_file_future) - })?; - let reply = GetClipDetailReply { - rea_peaks: ok_or_log_as_warn(peak_file_future.await), - }; - Ok(Response::new(reply)) - } - - async fn get_all_tracks( - &self, - request: Request, - ) -> Result, Status> { - let req = request.into_inner(); - let reply = self.handle_matrix_internal(&req.matrix_id, |matrix| { - let project = matrix.temporary_project(); - Ok(get_all_tracks(project)) - })?; - Ok(Response::new(reply)) - } - - async fn set_column_name( - &self, - request: Request, - ) -> Result, Status> { - let req = request.into_inner(); - self.handle_column_command(&req.column_address, |matrix, column_index| { - matrix.set_column_name(column_index, req.name) - }) - } -} - -type SyncBoxStream<'a, T> = Pin + Send + Sync + 'a>>; - -fn stream_by_session_id( - requested_clip_matrix_id: String, - receiver: Receiver>, - create_result: F, - initial: I, -) -> Result>>, Status> -where - T: Clone + Send + 'static, - R: Send + Sync + 'static, - F: Fn(T) -> R + Send + Sync + 'static, - I: Iterator + Send + Sync + 'static, -{ - // Stream that waits 1 millisecond and emits nothing - // This is done to (hopefully) prevent the following client-side Dart error, which otherwise - // would occur sporadically when attempting to connect: - // [ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: gRPC Error (code: 2, codeName: UNKNOWN, message: HTTP/2 error: Connection error: Connection is being forcefully terminated. (errorCode: 1), details: null, rawResponse: null, trailers: {}) - let wait_one_milli = future_util::millis(1) - .map(|_| Err(Status::unknown("skipped"))) - .into_stream() - .skip(1); - // Stream for sending the initial state - let initial_stream = futures::stream::iter(initial.map(|r| Ok(r))); - // Stream for sending occasional updates - let receiver_stream = BroadcastStream::new(receiver).filter_map(move |value| { - let res = match value { - // Error - Err(e) => Some(Err(Status::unknown(e.to_string()))), - // Clip matrix ID matches - Ok(WithSessionId { session_id, value }) if session_id == requested_clip_matrix_id => { - Some(Ok(create_result(value))) - } - // Clip matrix ID doesn't match - _ => None, - }; - future::ready(res) - }); - Ok(Response::new(Box::pin( - wait_one_milli.chain(initial_stream).chain(receiver_stream), - ))) -} - -fn convert_slot_address_to_engine(addr: &Option) -> Result { - let addr = addr - .as_ref() - .ok_or_else(|| Status::invalid_argument("need slot address"))? - .to_engine(); - Ok(addr) -} - -fn convert_clip_address_to_engine( - addr: &Option, -) -> Result { - let addr = addr - .as_ref() - .ok_or_else(|| Status::invalid_argument("need clip address"))? - .to_engine() - .map_err(Status::invalid_argument)?; - Ok(addr) -} - -fn get_all_tracks(project: Project) -> GetAllTracksReply { - let mut level = 0i32; - let tracks = project.tracks().map(|t| { - let folder_depth_change = t.folder_depth_change(); - let track = proto::TrackInList::from_engine(t, level.unsigned_abs()); - level += folder_depth_change; - track - }); - GetAllTracksReply { - track: tracks.collect(), - } -} diff --git a/playtime-clip-engine/src/rt/audio_hook.rs b/playtime-clip-engine/src/rt/audio_hook.rs deleted file mode 100644 index 16112d48e..000000000 --- a/playtime-clip-engine/src/rt/audio_hook.rs +++ /dev/null @@ -1,222 +0,0 @@ -use crate::base::{ - ClipRecordDestination, ClipRecordHardwareInput, ClipRecordHardwareMidiInput, - VirtualClipRecordAudioInput, VirtualClipRecordHardwareMidiInput, -}; -use crate::rt::supplier::{WriteAudioRequest, WriteMidiRequest}; -use crate::rt::{AudioBuf, BasicAudioRequestProps, RtColumn}; -use crate::{global_steady_timeline_state, midi_util}; -use helgoboss_midi::Channel; -use reaper_high::{MidiInputDevice, Reaper}; -use reaper_medium::{AudioHookRegister, MidiInputDeviceId}; -use std::sync::MutexGuard; - -#[derive(Debug, Default)] -pub struct ClipEngineAudioHook { - clip_record_task: Option, -} - -impl ClipEngineAudioHook { - pub fn new() -> Self { - Self::default() - } -} - -#[derive(Debug)] -pub struct HardwareInputClipRecordTask { - pub input: ClipRecordHardwareInput, - pub destination: ClipRecordDestination, -} - -#[derive(Debug)] -pub struct FxInputClipRecordTask { - pub input: VirtualClipRecordAudioInput, - pub destination: ClipRecordDestination, -} - -impl ClipEngineAudioHook { - pub fn start_clip_recording(&mut self, task: HardwareInputClipRecordTask) { - debug!("Audio hook received clip record task"); - self.clip_record_task = Some(task); - } - - /// Call very early in audio hook and only if `is_post == false`. - pub fn poll_advance_timeline(&mut self, block_props: BasicAudioRequestProps) { - global_steady_timeline_state().on_audio_buffer(block_props); - } - - /// Call a bit later in audio hook. - pub fn poll_process_clip_record_tasks( - &mut self, - is_post: bool, - block_props: BasicAudioRequestProps, - audio_hook_register: &AudioHookRegister, - ) { - if let Some(t) = &mut self.clip_record_task { - let its_our_turn = (t.destination.is_midi_overdub && is_post) - || (!t.destination.is_midi_overdub && !is_post); - if its_our_turn && !process_clip_record_task(block_props, audio_hook_register, t) { - debug!("Clearing clip record task from audio hook"); - self.clip_record_task = None; - } - } - } -} - -/// Returns whether task still relevant. -fn process_clip_record_task( - block_props: BasicAudioRequestProps, - audio_hook_register: &AudioHookRegister, - record_task: &mut HardwareInputClipRecordTask, -) -> bool { - let column_source = match record_task.destination.column_source.upgrade() { - None => return false, - Some(s) => s, - }; - let mut src = column_source.lock(); - if !src.recording_poll(record_task.destination.slot_index, block_props) { - return false; - } - match &mut record_task.input { - ClipRecordHardwareInput::Midi(input) => { - use VirtualClipRecordHardwareMidiInput::*; - let specific_input = match input { - Specific(s) => *s, - Detect => { - // Detect - match find_first_dev_with_play_msg() { - None => { - // No play message detected so far in any input device. - return true; - } - Some(dev_id) => { - // Found first play message in this device. Leave "Detect" mode and - // capture from this specific device from now on. - let specific_input = ClipRecordHardwareMidiInput { - device_id: Some(dev_id), - channel: None, - }; - *input = Specific(specific_input); - specific_input - } - } - } - }; - if let Some(dev_id) = specific_input.device_id { - // Read from specific MIDI input device - let dev = Reaper::get().midi_input_device_by_id(dev_id); - write_midi_to_clip_slot( - block_props, - &mut src, - record_task.destination.slot_index, - dev, - specific_input.channel, - ); - } else { - // Read from all open MIDI input devices - for dev in Reaper::get().midi_input_devices() { - write_midi_to_clip_slot( - block_props, - &mut src, - record_task.destination.slot_index, - dev, - specific_input.channel, - ); - } - } - } - ClipRecordHardwareInput::Audio(input) => { - let channel_offset = input.channel_offset().unwrap(); - let write_audio_request = AudioHookWriteAudioRequest::new( - audio_hook_register, - block_props, - channel_offset as _, - ); - src.write_clip_audio(record_task.destination.slot_index, write_audio_request) - .unwrap(); - } - } - true -} - -fn find_first_dev_with_play_msg() -> Option { - for dev in Reaper::get().midi_input_devices() { - let contains_play_msg = dev.with_midi_input(|mi| match mi { - None => false, - Some(mi) => mi - .get_read_buf() - .into_iter() - .any(|e| midi_util::is_play_message(e.message())), - }); - if contains_play_msg { - return Some(dev.id()); - } - } - None -} - -fn write_midi_to_clip_slot( - block_props: BasicAudioRequestProps, - src: &mut MutexGuard, - slot_index: usize, - dev: MidiInputDevice, - channel_filter: Option, -) { - dev.with_midi_input(|mi| { - let mi = match mi { - None => return, - Some(m) => m, - }; - let events = mi.get_read_buf(); - if events.get_size() == 0 { - return; - } - let req = WriteMidiRequest { - audio_request_props: block_props, - events, - channel_filter, - }; - src.write_clip_midi(slot_index, req).unwrap(); - }); -} - -#[derive(Copy, Clone)] -struct AudioHookWriteAudioRequest<'a> { - channel_offset: usize, - register: &'a AudioHookRegister, - block_props: BasicAudioRequestProps, -} - -impl<'a> AudioHookWriteAudioRequest<'a> { - pub fn new( - register: &'a AudioHookRegister, - block_props: BasicAudioRequestProps, - channel_offset: usize, - ) -> Self { - Self { - channel_offset, - register, - block_props, - } - } -} - -impl<'a> WriteAudioRequest for AudioHookWriteAudioRequest<'a> { - fn audio_request_props(&self) -> BasicAudioRequestProps { - self.block_props - } - - fn get_channel_buffer(&self, channel_index: usize) -> Option { - let reg = unsafe { self.register.get().as_ref() }; - let get_buffer = match reg.GetBuffer { - None => return None, - Some(f) => f, - }; - let effective_channel_index = self.channel_offset + channel_index; - let buf = unsafe { (get_buffer)(false, effective_channel_index as _) }; - if buf.is_null() { - return None; - } - let buf = unsafe { AudioBuf::from_raw(buf, 1, self.block_props.block_length) }; - Some(buf) - } -} diff --git a/playtime-clip-engine/src/rt/buffer.rs b/playtime-clip-engine/src/rt/buffer.rs deleted file mode 100644 index 623514b6d..000000000 --- a/playtime-clip-engine/src/rt/buffer.rs +++ /dev/null @@ -1,325 +0,0 @@ -use crate::ClipEngineResult; -use derivative::Derivative; -use reaper_medium::PcmSourceTransfer; -use std::collections::Bound; -use std::fmt::Debug; -use std::ops::RangeBounds; - -#[derive(Derivative)] -#[derivative(Debug)] -pub struct OwnedAudioBuffer { - #[derivative(Debug = "ignore")] - data: Vec, - channel_count: usize, - frame_count: usize, -} - -impl OwnedAudioBuffer { - /// Creates an owned audio buffer with the given topology. - pub fn new(channel_count: usize, frame_count: usize) -> Self { - Self { - data: vec![0.0; channel_count * frame_count], - channel_count, - frame_count, - } - } - - pub fn to_buf(&self) -> AudioBuf { - AudioBuf { - data: self.data.as_slice(), - frame_count: self.frame_count, - channel_count: self.channel_count, - } - } - - pub fn to_buf_mut(&mut self) -> AudioBufMut { - AudioBufMut { - data: self.data.as_mut_slice(), - frame_count: self.frame_count, - channel_count: self.channel_count, - } - } - - /// Attempts to create an owned audio buffer with the given topology by reusing the given vec. - /// - /// Returns an error if the given vec is not large enough. - pub fn try_recycle( - mut data: Vec, - channel_count: usize, - frame_count: usize, - ) -> ClipEngineResult { - let min_capacity = channel_count * frame_count; - if data.capacity() < min_capacity { - return Err("given vector doesn't have enough capacity"); - } - data.resize(min_capacity, 0.0); - let buffer = Self { - data, - channel_count, - frame_count, - }; - Ok(buffer) - } - - pub fn into_inner(self) -> Vec { - self.data - } -} - -// TODO-medium Replace this with one of the audio buffer types in the Rust ecosystem -// (dasp_slice, audio, fon, ...) -#[derive(Copy, Clone, Debug)] -pub struct AbstractAudioBuf> { - data: T, - frame_count: usize, - channel_count: usize, -} - -pub type AudioBuf<'a> = AbstractAudioBuf<&'a [f64]>; -pub type AudioBufMut<'a> = AbstractAudioBuf<&'a mut [f64]>; - -impl<'a> AudioBuf<'a> { - /// # Safety - /// - /// REAPER can crash if you pass an invalid pointer. - pub unsafe fn from_raw(data: *mut f64, channel_count: usize, frame_count: usize) -> Self { - AudioBuf { - data: std::slice::from_raw_parts(data, (channel_count * frame_count) as _), - frame_count, - channel_count, - } - } - - /// # Panics - /// - /// Panics if requested frame count is zero. - /// - /// # Errors - /// - /// Returns an error if the size of the given data chunk isn't large enough. - pub fn from_slice( - chunk: &'a [f64], - channel_count: usize, - frame_count: usize, - ) -> Result { - let required_slice_length = required_slice_length(chunk.len(), channel_count, frame_count)?; - let buf = AudioBuf { - data: &chunk[0..required_slice_length], - frame_count, - channel_count, - }; - Ok(buf) - } -} - -fn required_slice_length( - chunk_len: usize, - channel_count: usize, - frame_count: usize, -) -> ClipEngineResult { - if frame_count == 0 { - panic!("attempt to create buffer from sliced data with a frame count of zero"); - } - let required_slice_length = channel_count * frame_count; - if chunk_len < required_slice_length { - return Err("given slice not large enough"); - } - Ok(required_slice_length) -} - -impl<'a> AudioBufMut<'a> { - /// # Safety - /// - /// REAPER can crash if you pass an invalid pointer. - pub unsafe fn from_pcm_source_transfer(transfer: &mut PcmSourceTransfer) -> Self { - Self::from_raw( - transfer.samples(), - transfer.nch() as _, - transfer.length() as _, - ) - } - - /// # Panics - /// - /// Panics if requested frame count is zero. - /// - /// # Errors - /// - /// Returns an error if the size of the given data chunk isn't large enough. - pub fn from_slice( - chunk: &'a mut [f64], - channel_count: usize, - frame_count: usize, - ) -> Result { - let required_slice_length = required_slice_length(chunk.len(), channel_count, frame_count)?; - let buf = AudioBufMut { - data: &mut chunk[0..required_slice_length], - frame_count, - channel_count, - }; - Ok(buf) - } - - /// # Panics - /// - /// Panics if requested frame count is zero. - /// - /// # Safety - /// - /// REAPER can crash if you pass an invalid pointer. - pub unsafe fn from_raw(data: *mut f64, channel_count: usize, frame_count: usize) -> Self { - if frame_count == 0 { - panic!("attempt to create buffer from raw data with a frame count of zero"); - } - AudioBufMut { - data: std::slice::from_raw_parts_mut(data, (channel_count * frame_count) as _), - frame_count, - channel_count, - } - } -} - -impl> AbstractAudioBuf { - /// Destination buffer must have the same number of channels and frames. - pub fn copy_to(&self, dest: &mut AudioBufMut) { - if dest.channel_count() != self.channel_count() { - panic!("different channel counts"); - } - if dest.frame_count() != self.frame_count() { - panic!("different frame counts"); - } - dest.data_as_mut_slice().copy_from_slice(self.data.as_ref()); - } - - pub fn channel_count(&self) -> usize { - self.channel_count - } - - pub fn frame_count(&self) -> usize { - self.frame_count - } - - pub fn data_as_slice(&self) -> &[f64] { - self.data.as_ref() - } - - pub fn data_as_mut_ptr(&self) -> *mut f64 { - self.data.as_ref().as_ptr() as *mut _ - } - - pub fn sample_value_at(&self, index: SampleIndex) -> Option { - self.data - .as_ref() - .get(index.frame * self.channel_count + index.channel) - .copied() - } - - /// Works pretty much like indexing slices with a range. - /// - /// - Ranges that yield an empty buffer are allowed. - /// - Panics when out of bounds. - pub fn slice(&self, bounds: impl RangeBounds) -> AudioBuf { - let desc = self.prepare_slice(bounds); - AudioBuf { - data: &self.data.as_ref()[desc.data_start_index..desc.data_end_index], - frame_count: desc.new_frame_count, - channel_count: desc.channel_count, - } - } - - fn prepare_slice(&self, bounds: impl RangeBounds) -> SliceDescriptor { - use Bound::*; - let start_frame = match bounds.start_bound() { - Included(i) => *i, - Excluded(i) => *i + 1, - Unbounded => 0, - }; - let end_frame = match bounds.end_bound() { - Included(i) => *i - 1, - Excluded(i) => *i, - Unbounded => self.frame_count, - }; - if start_frame > self.frame_count || end_frame > self.frame_count { - panic!("slice range out of bounds"); - } - if start_frame > end_frame { - panic!("slice start greater than end"); - } - SliceDescriptor { - new_frame_count: end_frame - start_frame, - data_start_index: start_frame * self.channel_count, - data_end_index: end_frame * self.channel_count, - channel_count: self.channel_count, - } - } -} - -struct SliceDescriptor { - new_frame_count: usize, - data_start_index: usize, - data_end_index: usize, - channel_count: usize, -} - -impl + AsMut<[f64]>> AbstractAudioBuf { - pub fn data_as_mut_slice(&mut self) -> &mut [f64] { - self.data.as_mut() - } - - /// Works pretty much like indexing slices with a range. - /// - /// - Ranges that yield an empty buffer are allowed. - /// - Panics when out of bounds. - pub fn slice_mut(&mut self, bounds: impl RangeBounds) -> AudioBufMut { - let desc = self.prepare_slice(bounds); - AudioBufMut { - data: &mut self.data.as_mut()[desc.data_start_index..desc.data_end_index], - frame_count: desc.new_frame_count, - channel_count: desc.channel_count, - } - } - - pub fn modify_frames(&mut self, mut f: impl FnMut(SampleDescriptor) -> f64) { - for frame_index in 0..self.frame_count { - for ch in 0..self.channel_count { - // TODO-high-performance For performance we might want to skip the bound checks. This is - // very hot code. - let sample_value = &mut self.data.as_mut()[frame_index * self.channel_count + ch]; - let descriptor = SampleDescriptor { - index: SampleIndex { - frame: frame_index, - channel: ch, - }, - value: *sample_value, - }; - *sample_value = f(descriptor); - } - } - } - - /// Fills the buffer with zero samples. - /// - /// This is not always necessary, it depends on the situation. The preview register pre-zeroes - /// buffers but the time stretcher and resampler doesn't, which results in beeps if we don't - /// clear it. - pub fn clear(&mut self) { - self.data.as_mut().fill(0.0); - } -} - -pub struct SampleDescriptor { - pub index: SampleIndex, - pub value: f64, -} - -#[derive(Copy, Clone)] -pub struct SampleIndex { - pub channel: usize, - pub frame: usize, -} - -impl SampleIndex { - pub fn new(channel: usize, frame: usize) -> Self { - Self { channel, frame } - } -} diff --git a/playtime-clip-engine/src/rt/fx_hook.rs b/playtime-clip-engine/src/rt/fx_hook.rs deleted file mode 100644 index 6472565a0..000000000 --- a/playtime-clip-engine/src/rt/fx_hook.rs +++ /dev/null @@ -1,91 +0,0 @@ -use crate::rt::audio_hook::FxInputClipRecordTask; -use crate::rt::supplier::WriteAudioRequest; -use crate::rt::{AudioBuf, BasicAudioRequestProps}; - -#[derive(Debug, Default)] -pub struct ClipEngineFxHook { - clip_record_task: Option, -} - -impl ClipEngineFxHook { - pub fn new() -> Self { - Self::default() - } - - pub fn start_clip_recording(&mut self, task: FxInputClipRecordTask) { - debug!("Real-time processor received clip record task"); - self.clip_record_task = Some(task); - } - - pub fn process_clip_record_task( - &mut self, - inputs: &impl ChannelInputs, - block_props: BasicAudioRequestProps, - ) { - if let Some(t) = &mut self.clip_record_task { - if !process_clip_record_task(t, inputs, block_props) { - debug!("Clearing clip record task from real-time processor"); - self.clip_record_task = None; - } - } - } -} - -pub trait ChannelInputs { - fn channel_count(&self) -> usize; - fn get_channel_data(&self, channel_index: usize) -> &[f64]; -} - -/// Returns whether task still relevant. -fn process_clip_record_task( - record_task: &mut FxInputClipRecordTask, - inputs: &impl ChannelInputs, - block_props: BasicAudioRequestProps, -) -> bool { - let column_source = match record_task.destination.column_source.upgrade() { - None => return false, - Some(s) => s, - }; - let mut src = column_source.lock(); - if !src.recording_poll(record_task.destination.slot_index, block_props) { - return false; - } - let channel_offset = record_task.input.channel_offset().unwrap(); - let write_audio_request = - RealTimeProcessorWriteAudioRequest::new(inputs, block_props, channel_offset as _); - src.write_clip_audio(record_task.destination.slot_index, write_audio_request) - .unwrap(); - true -} - -#[derive(Copy, Clone)] -struct RealTimeProcessorWriteAudioRequest<'a, I> { - channel_offset: usize, - inputs: &'a I, - block_props: BasicAudioRequestProps, -} - -impl<'a, I: ChannelInputs> RealTimeProcessorWriteAudioRequest<'a, I> { - pub fn new(inputs: &'a I, block_props: BasicAudioRequestProps, channel_offset: usize) -> Self { - Self { - channel_offset, - inputs, - block_props, - } - } -} - -impl<'a, I: ChannelInputs> WriteAudioRequest for RealTimeProcessorWriteAudioRequest<'a, I> { - fn audio_request_props(&self) -> BasicAudioRequestProps { - self.block_props - } - - fn get_channel_buffer(&self, channel_index: usize) -> Option { - let effective_channel_index = self.channel_offset + channel_index; - if effective_channel_index >= self.inputs.channel_count() { - return None; - } - let slice = self.inputs.get_channel_data(effective_channel_index); - AudioBuf::from_slice(slice, 1, self.block_props.block_length).ok() - } -} diff --git a/playtime-clip-engine/src/rt/mod.rs b/playtime-clip-engine/src/rt/mod.rs deleted file mode 100644 index a0f859603..000000000 --- a/playtime-clip-engine/src/rt/mod.rs +++ /dev/null @@ -1,18 +0,0 @@ -pub mod audio_hook; -mod buffer; -pub mod fx_hook; -mod rt_clip; -mod rt_column; -mod rt_matrix; -mod rt_slot; -mod schedule_util; -pub mod source_util; -pub mod tempo_util; - -pub mod supplier; - -pub use buffer::*; -pub use rt_clip::*; -pub use rt_column::*; -pub use rt_matrix::*; -pub use rt_slot::*; diff --git a/playtime-clip-engine/src/rt/rt_clip.rs b/playtime-clip-engine/src/rt/rt_clip.rs deleted file mode 100644 index 527512f84..000000000 --- a/playtime-clip-engine/src/rt/rt_clip.rs +++ /dev/null @@ -1,2295 +0,0 @@ -use crate::base::{ClipAddress, ClipSlotAddress}; -use crate::conversion_util::{ - adjust_proportionally_positive, convert_duration_in_frames_to_other_frame_rate, - convert_duration_in_frames_to_seconds, convert_duration_in_seconds_to_frames, -}; -use crate::rt::buffer::AudioBufMut; -use crate::rt::schedule_util::calc_distance_from_quantized_pos; -use crate::rt::supplier::{ - AudioSupplier, ChainEquipment, ChainSettings, CompleteRecordingData, - KindSpecificRecordingOutcome, MaterialInfo, MidiOverdubOutcome, MidiOverdubSettings, - MidiSequence, MidiSupplier, PollRecordingOutcome, RecordState, Recorder, RecorderRequest, - RecordingArgs, RecordingEquipment, RecordingOutcome, RtClipSource, StopRecordingOutcome, - SupplierChain, SupplyAudioRequest, SupplyMidiRequest, SupplyRequestGeneralInfo, - SupplyRequestInfo, SupplyResponse, SupplyResponseStatus, WithMaterialInfo, WriteAudioRequest, - WriteMidiRequest, MIDI_BASE_BPM, MIDI_FRAME_RATE, -}; -use crate::rt::tempo_util::{calc_tempo_factor, determine_tempo_from_time_base}; -use crate::rt::{OverridableMatrixSettings, RtClips, RtColumnEvent, RtColumnSettings}; -use crate::timeline::{HybridTimeline, Timeline}; -use crate::{ClipEngineResult, ErrorWithPayload, Laziness, QuantizedPosition}; -use atomic::Atomic; -use crossbeam_channel::Sender; -use helgoboss_learn::UnitValue; -use helgoboss_midi::ShortMessage; -use playtime_api::persistence as api; -use playtime_api::persistence::{ - ClipAudioSettings, ClipId, ClipPlayStartTiming, ClipPlayStopTiming, ClipTimeBase, Db, - EvenQuantization, MatrixClipRecordSettings, PositiveSecond, -}; -use playtime_api::runtime::ClipPlayState; -use reaper_high::Project; -use reaper_medium::{ - BorrowedMidiEventList, Bpm, DurationInSeconds, Hz, OnAudioBufferArgs, PcmSourceTransfer, - PositionInSeconds, -}; -use std::sync::atomic::{AtomicIsize, Ordering}; -use std::sync::Arc; - -#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] -pub struct RtClipId(u64); - -impl RtClipId { - pub fn from_clip_id(clip_id: &ClipId) -> Self { - Self(base::hash_util::calculate_non_crypto_hash(clip_id)) - } -} - -#[derive(Debug)] -pub struct RtClip { - id: RtClipId, - supplier_chain: SupplierChain, - state: ClipState, - project: Option, - shared_pos: SharedPos, - shared_peak: SharedPeak, -} - -fn calculate_beat_count(tempo: Bpm, duration: DurationInSeconds) -> u32 { - let beats_per_sec = tempo.get() / 60.0; - (duration.get() * beats_per_sec).round() as u32 -} - -#[derive(Copy, Clone, Debug)] -enum ClipState { - Ready(ReadyState), - /// Recording from scratch, not MIDI overdub. - Recording(RecordingState), -} - -#[derive(Copy, Clone, Debug)] -struct ReadyState { - state: ReadySubState, - settings: RtClipSettings, -} - -#[derive(Copy, Clone, Debug)] -enum ReadySubState { - /// At this state, the clip is stopped. No fade-in, no fade-out ... nothing. - Stopped, - Playing(PlayingState), - /// Very short transition for fade outs or sending all-notes-off before entering another state. - Suspending(SuspendingState), - Paused(PausedState), -} - -#[derive(Copy, Clone, Debug, Default)] -struct PlayingState { - pub virtual_pos: VirtualPosition, - /// Position within material, not a timeline position. - pub pos: Option, - pub stop_request: Option, - pub overdubbing: bool, - pub seek_pos: Option, -} - -#[derive(Copy, Clone, Debug)] -enum StopRequest { - AtEndOfClip, - Quantized(QuantizedPosition), -} - -#[derive(Copy, Clone, Debug)] -struct SuspendingState { - pub next_state: StateAfterSuspension, - pub pos: MaterialPos, -} - -#[derive(Copy, Clone, Debug, Default)] -struct PausedState { - pub pos: MaterialPos, -} - -//region Description -/// At the time `get_samples` is called, this contains the position in the inner source that -/// should be played next. -/// -/// - The frames relate to the source sample rate. -/// - The position can be after the source content, in which case one needs to modulo native -/// source length to get the position *within* the inner source. -/// - If this position is negative, we are in the count-in phase. -/// - On each call of `get_samples()`, the position is advanced and set *exactly* to the end of -/// the previous block, so that the source is played continuously under any circumstance, -/// without skipping material - because skipping material sounds bad. -/// - Before introducing this field, we were instead memorizing the absolute timeline position -/// at which the clip started playing. Then we always played the source at the position that -/// corresponds to the current absolute timeline position - which is basically the analog to -/// putting items in the arrange view. It works flawlessly ... until you interact with the -/// timeline and/or make on-the-fly tempo changes. Read on! -/// - First issue: The REAPER project timeline is -/// non-steady. It resets its position when we change the cursor position - even when the -/// project is not playing and therefore no sync is desired from ReaLearn's perspective. -/// The same happens when we change the tempo and the project is playing: The speed of the -/// timeline doesn't change (which is fine) but its position resets! -/// - Second issue: While we could solve the first issue by consulting a steady timeline (e.g. -/// the preview register timeline), there's a second one that is about on-the-fly tempo -/// changes only. When increasing or decreasing the tempo, we really want the clip to play -/// continuously, with every sample block continuing at the position where it left off in the -/// previous block. That is the absolute basis for a smooth tempo changing experience. If we -/// calculate the position that should be played based on some distance-to-start logic using -/// a linear timeline, we will have a hard time achieving this. Because this logic assumes -/// that the tempo was always the same since the clip started playing. -/// - For these reasons, we use this relative-to-previous-block logic. It guarantees that the -/// clip is played continuously, no matter what. Simple and effective. -//endregion -type MaterialPos = isize; - -#[derive(Clone, Debug, Default)] -pub struct SharedPos(Arc); - -impl SharedPos { - pub fn get(&self) -> MaterialPos { - self.0.load(Ordering::Relaxed) - } - - fn set(&self, pos: isize) { - self.0.store(pos, Ordering::Relaxed); - } -} - -#[derive(Clone, Debug, Default)] -pub struct SharedPeak(Arc>); - -impl SharedPeak { - /// Returns the last detected peak value. Plus, if a MIDI note-on was encountered - /// (= peak is MAX), resets value to MIN in order to acknowledge receipt of the note-on event. - pub fn reset(&self) -> UnitValue { - let res = self.0.compare_exchange( - UnitValue::MAX, - UnitValue::MIN, - Ordering::Relaxed, - Ordering::Relaxed, - ); - match res { - Ok(v) => v, - Err(v) => v, - } - } - - fn set(&self, peak: UnitValue) { - self.0.store(peak, Ordering::Relaxed); - } -} - -#[derive(Copy, Clone, Debug)] -enum StateAfterSuspension { - /// Play was suspended for initiating a retriggering, so the next state will be - /// [`ClipState::ScheduledOrPlaying`] again. - Playing(PlayingState), - /// Play was suspended for initiating a pause, so the next state will be [`ClipState::Paused`]. - Paused, - /// Play was suspended for initiating a stop, so the next state will be [`ClipState::Stopped`]. - Stopped, - /// Play was suspended for initiating recording. - Recording(RecordingState), -} - -#[derive(Copy, Clone, Debug)] -struct RecordingState { - rollback_data: Option, - settings: MatrixClipRecordSettings, -} - -#[derive(Copy, Clone, Debug)] -struct RollbackData { - clip_settings: RtClipSettings, -} - -impl RtClip { - /// Must not call in real-time thread! - #[allow(clippy::too_many_arguments)] - pub fn ready( - id: RtClipId, - pcm_source: RtClipSource, - matrix_settings: &OverridableMatrixSettings, - column_settings: &RtColumnSettings, - clip_settings: RtClipSettings, - permanent_project: Option, - chain_equipment: &ChainEquipment, - recorder_request_sender: &Sender, - ) -> ClipEngineResult { - let mut supplier_chain = SupplierChain::new( - Recorder::ready(pcm_source, recorder_request_sender.clone()), - chain_equipment.clone(), - )?; - let chain_settings = clip_settings.create_chain_settings(matrix_settings, column_settings); - supplier_chain.configure_complete_chain(chain_settings)?; - supplier_chain.pre_buffer_simple(0); - let ready_state = ReadyState { - state: ReadySubState::Stopped, - settings: clip_settings, - }; - let clip = Self { - id, - supplier_chain, - state: ClipState::Ready(ready_state), - project: permanent_project, - shared_pos: Default::default(), - shared_peak: Default::default(), - }; - Ok(clip) - } - - pub fn recording(instruction: RecordNewClipInstruction) -> Self { - let recording_state = RecordingState { - rollback_data: None, - settings: instruction.settings, - }; - instruction.supplier_chain.emit_audio_recording_task(); - Self { - id: instruction.clip_id, - supplier_chain: instruction.supplier_chain, - state: ClipState::Recording(recording_state), - project: instruction.project, - shared_pos: instruction.shared_pos, - shared_peak: instruction.shared_peak, - } - } - - pub fn id(&self) -> RtClipId { - self.id - } - - /// Applies properties of the given clip to this clip. - /// - /// This gets called when the slot load logic detects an existing clip with the same ID. - /// Instead of replacing the slot's clip with the new clip, it lets the old clip (this one!) - /// apply the properties of the new clip to itself. This makes it possible to keep the clip - /// playing, applying just the differences. - /// - /// # Errors - /// - /// If this clip or the given clip is recording or material doesn't deliver info. - pub fn apply(&mut self, args: ApplyClipArgs) -> ClipEngineResult<()> { - let (ClipState::Ready(this_clip), ClipState::Ready(new_clip))= (&mut self.state, &mut args.other_clip.state) else { - return Err("can't apply if this or given clip is in recording state"); - }; - let setting_args = SetClipSettingsArgs { - clip_settings: new_clip.settings, - matrix_settings: args.matrix_settings, - column_settings: args.column_settings, - }; - this_clip.set_settings(setting_args, &mut self.supplier_chain)?; - // Really important to reconnect the shared position and peak info variables, otherwise the - // UI will not display them anymore. - self.shared_pos = args.other_clip.shared_pos.clone(); - self.shared_peak = args.other_clip.shared_peak.clone(); - args.other_clip - .supplier_chain - .with_source_mut(|other_source| self.supplier_chain.swap_source(other_source))??; - Ok(()) - } - - /// If recording, delivers material info of the material that's being recorded. - pub fn recording_material_info(&self) -> ClipEngineResult { - self.supplier_chain.recording_material_info() - } - - /// Plays the clip if it's not recording. - pub fn play(&mut self, args: SlotPlayArgs) -> ClipEngineResult { - use ClipState::*; - match &mut self.state { - Ready(s) => Ok(s.play(args, &mut self.supplier_chain)), - Recording(_) => Err("recording"), - } - } - - /// Stops the clip immediately, initiating fade-outs if necessary. - /// - /// Also stops clip recording. Consumer should just wait for the clip to be stopped and then not - /// use it anymore. - pub fn initiate_removal(&mut self) { - match &mut self.state { - ClipState::Ready(s) => s.panic(&mut self.supplier_chain), - ClipState::Recording(_) => { - self.state = ClipState::Ready(ReadyState { - state: ReadySubState::Stopped, - settings: Default::default(), - }) - } - } - } - - /// Stops the clip immediately, initiating fade-outs if necessary. - /// - /// Doesn't stop a clip recording. - pub fn panic(&mut self) { - match &mut self.state { - ClipState::Ready(s) => s.panic(&mut self.supplier_chain), - ClipState::Recording(_) => {} - } - } - - /// Stops the clip playing or recording. - pub fn stop( - &mut self, - args: SlotStopArgs, - event_handler: &H, - ) -> ClipEngineResult> { - use ClipState::*; - let instruction = match &mut self.state { - Ready(s) => { - if let Some(outcome) = s.stop(args, &mut self.supplier_chain) { - event_handler.midi_overdub_finished(self.id, outcome); - } - None - } - Recording(s) => { - use ClipRecordingStopOutcome::*; - match s.stop(args, &mut self.supplier_chain, event_handler)? { - KeepState => None, - TransitionToReady(ready_state) => { - self.state = Ready(ready_state); - None - } - ClearSlot => Some(SlotInstruction::ClearSlot), - } - } - }; - Ok(instruction) - } - - pub fn set_settings(&mut self, args: SetClipSettingsArgs) -> ClipEngineResult<()> { - use ClipState::*; - match &mut self.state { - Ready(s) => s.set_settings(args, &mut self.supplier_chain), - Recording(_) => Err("can't set settings while recording"), - } - } - - pub fn set_looped(&mut self, looped: bool) -> ClipEngineResult<()> { - use ClipState::*; - match &mut self.state { - Ready(s) => { - s.set_looped(looped, &mut self.supplier_chain); - Ok(()) - } - Recording(_) => Err("can't set looped while recording"), - } - } - - pub fn set_section(&mut self, section: api::Section) -> ClipEngineResult<()> { - use ClipState::*; - match &mut self.state { - Ready(s) => { - s.set_section(section, &mut self.supplier_chain); - Ok(()) - } - Recording(_) => Err("can't set section while recording"), - } - } - - pub fn looped(&self) -> bool { - use ClipState::*; - match self.state { - Ready(s) => s.settings.looped, - Recording(_) => false, - } - } - - // TODO-high-clip-engine The error type is too large! - #[allow(clippy::result_large_err)] - pub fn midi_overdub( - &mut self, - args: MidiOverdubInstruction, - ) -> Result<(), ErrorWithPayload> { - use ClipState::*; - match &mut self.state { - Ready(s) => s.midi_overdub(args, &mut self.supplier_chain), - Recording(_) => Err(ErrorWithPayload::new("clip is recording", args)), - } - } - - // TODO-high-clip-engine The error type is too large! - #[allow(clippy::result_large_err)] - pub fn record( - &mut self, - args: ClipRecordArgs, - matrix_settings: &OverridableMatrixSettings, - column_settings: &RtColumnSettings, - ) -> Result<(), ErrorWithPayload> { - use ClipState::*; - match &mut self.state { - Ready(s) => { - let new_state = s.record( - args, - self.project, - &mut self.supplier_chain, - matrix_settings, - column_settings, - ); - self.supplier_chain.emit_audio_recording_task(); - if let Some(recording_state) = new_state { - self.state = Recording(recording_state); - } - Ok(()) - } - Recording(_) => Err(ErrorWithPayload::new("already recording", args)), - } - } - - pub fn pause(&mut self) { - use ClipState::*; - match &mut self.state { - Ready(s) => s.pause(&self.supplier_chain), - Recording(_) => {} - } - } - - pub fn seek(&mut self, desired_pos: UnitValue) -> ClipEngineResult<()> { - use ClipState::*; - match &mut self.state { - Ready(s) => s.seek(desired_pos, &self.supplier_chain), - Recording(_) => Err("recording"), - } - } - - /// Should be called exactly once per block when recording and before writing material, - /// in order to drive various record-related processing and also to know when to stop polling - /// and writing material. - /// - /// Returns `false` if not necessary to poll and write material anymore. - pub fn recording_poll( - &mut self, - args: ClipRecordingPollArgs, - event_handler: &H, - ) -> bool { - use ClipState::*; - match &mut self.state { - Ready(s) => match &s.state { - ReadySubState::Playing(s) => s.overdubbing, - _ => false, - }, - Recording(s) => { - use PollRecordingOutcome::*; - match self.supplier_chain.poll_recording(args.audio_request_props) { - PleaseStopPolling => false, - CommittedRecording(outcome) => { - let ready_state = s.finish_recording( - outcome, - &mut self.supplier_chain, - event_handler, - args.matrix_settings, - args.column_settings, - ); - self.state = Ready(ready_state); - false - } - PleaseContinuePolling { pos } => { - self.shared_pos.set(pos); - // TODO-high-clip-engine Set recording peak somewhere - true - } - } - } - } - } - - /// Writes the events in the given request into the currently recording MIDI source. - pub fn write_midi(&mut self, request: WriteMidiRequest) { - use ClipState::*; - let play_pos = match &self.state { - Ready(s) => match s.state { - ReadySubState::Playing(PlayingState { - overdubbing: true, - pos: Some(pos), - .. - }) => Some(pos), - _ => return, - }, - Recording(_) => None, - }; - self.supplier_chain.write_midi(request, play_pos).unwrap(); - } - - /// Writes the samples in the given request into the currently recording audio source. - /// - /// Also drives processing during recording because it's called exactly once per audio block - /// anyway. - pub fn write_audio(&mut self, request: impl WriteAudioRequest) { - self.supplier_chain.write_audio(request); - } - - pub fn set_volume(&mut self, volume: Db) { - self.supplier_chain.set_volume(volume); - } - - pub fn shared_pos(&self) -> SharedPos { - self.shared_pos.clone() - } - - pub fn shared_peak(&self) -> SharedPeak { - self.shared_peak.clone() - } - - /// Attention: If this returns some info while in the middle of recording, this returns - /// information about the previous clip's material! Use [`Self::recording_material_info`] - /// instead if you need to query information about the material that's being recorded. - pub fn material_info(&self) -> ClipEngineResult { - self.supplier_chain.material_info() - } - - /// Returns the current clip play state. - /// - /// Attention: If the clip is being suspended (e.g. fading out), this will return the state - /// after suspension, e.g. "Stopped". So don't use this to check whether processing is still - /// necessary. - pub fn play_state(&self) -> InternalClipPlayState { - match &self.state { - ClipState::Ready(s) => s.play_state(), - ClipState::Recording(_) => { - use RecordState::*; - let api_state = match self - .supplier_chain - .record_state() - .expect("recorder not recording while clip recording") - { - ScheduledForStart => ClipPlayState::ScheduledForRecordingStart, - Recording => ClipPlayState::Recording, - ScheduledForStop => ClipPlayState::ScheduledForRecordingStop, - }; - api_state.into() - } - } - } - - pub fn process(&mut self, args: &mut ClipProcessArgs) -> ClipProcessingOutcome { - use ClipState::*; - match &mut self.state { - Ready(s) => { - let (outcome, changed_state) = s.process( - args, - &mut self.supplier_chain, - &mut self.shared_pos, - &mut self.shared_peak, - ); - if let Some(s) = changed_state { - debug!("Changing to recording state {:?}", &s); - self.state = Recording(s); - } - outcome - } - Recording(_) => { - // Recording is not driven by the preview register processing but uses a separate - // record polling which is driven by the code that provides the input material. - ClipProcessingOutcome::default() - } - } - } -} - -impl ReadyState { - /// Stops the clip immediately, initiating fade-outs if necessary. - pub fn panic(&mut self, supplier_chain: &mut SupplierChain) { - use ReadySubState::*; - self.state = match self.state { - Playing(PlayingState { pos: Some(pos), .. }) => { - // Processing will automatically install an immediate stop interaction - // when entering the suspending state and there's no stop interaction yet. - // However, it will not do this when a stop interaction is installed already, e.g. - // a scheduled one (clip has scheduled stop). So we need to enforce an immediate - // one. - supplier_chain.install_immediate_stop_interaction(pos); - Suspending(SuspendingState { - next_state: StateAfterSuspension::Stopped, - pos, - }) - } - Suspending(s) => Suspending(SuspendingState { - next_state: StateAfterSuspension::Stopped, - ..s - }), - _ => Stopped, - }; - } - - /// Returns `None` if time base is not "Beat". - fn tempo(&self, is_midi: bool) -> Option { - determine_tempo_from_time_base(&self.settings.time_base, is_midi) - } - - pub fn set_settings( - &mut self, - args: SetClipSettingsArgs, - supplier_chain: &mut SupplierChain, - ) -> ClipEngineResult<()> { - self.settings = args.clip_settings; - let material_info = supplier_chain.material_info()?; - let chain_settings = args - .clip_settings - .create_chain_settings(args.matrix_settings, args.column_settings); - self.set_time_base(chain_settings.time_base, supplier_chain, &material_info); - self.set_looped(chain_settings.looped, supplier_chain); - self.set_section(chain_settings.section, supplier_chain); - self.set_start_timing(args.clip_settings.start_timing); - self.set_stop_timing(args.clip_settings.stop_timing); - supplier_chain.set_volume(chain_settings.volume); - supplier_chain.set_audio_fades_enabled_for_source(chain_settings.audio_apply_source_fades); - supplier_chain.set_audio_time_stretch_mode(chain_settings.audio_time_stretch_mode); - supplier_chain.set_audio_resample_mode(chain_settings.audio_resample_mode); - supplier_chain.set_audio_cache_behavior(chain_settings.cache_behavior); - supplier_chain.set_midi_settings(args.clip_settings.midi_settings); - Ok(()) - } - - fn set_time_base( - &mut self, - time_base: ClipTimeBase, - supplier_chain: &mut SupplierChain, - material_info: &MaterialInfo, - ) { - self.settings.time_base = time_base; - supplier_chain.set_time_base(&time_base, material_info); - } - - fn set_start_timing(&mut self, start_timing: Option) { - self.settings.start_timing = start_timing; - } - - fn set_stop_timing(&mut self, stop_timing: Option) { - self.settings.stop_timing = stop_timing; - } - - pub fn set_looped(&mut self, looped: bool, supplier_chain: &mut SupplierChain) { - self.settings.looped = looped; - if !looped { - if let ReadySubState::Playing(PlayingState { pos: Some(pos), .. }) = self.state { - supplier_chain.keep_playing_until_end_of_current_cycle(pos); - return; - } - } - supplier_chain.set_looped(self.settings.looped); - } - - pub fn set_section(&mut self, section: api::Section, supplier_chain: &mut SupplierChain) { - supplier_chain.set_section(section.start_pos, section.length); - } - - pub fn play(&mut self, args: SlotPlayArgs, supplier_chain: &mut SupplierChain) -> PlayOutcome { - let virtual_pos = self.calculate_virtual_play_pos(&args); - use ReadySubState::*; - match self.state { - // Not yet running. - Stopped => self.schedule_play_internal(virtual_pos), - Playing(s) => { - if s.stop_request.is_some() { - // Scheduled for stop. Backpedal! - // We can only schedule for stop when repeated, so we can set this - // back to Infinitely. - supplier_chain.set_looped(true); - // If we have a quantized stop, the interaction handler is active. Clear! - supplier_chain.reset_interactions(); - self.state = Playing(PlayingState { - stop_request: None, - ..s - }); - } else { - // Scheduled for play or playing already. - if let Some(pos) = s.pos { - if supplier_chain.is_playing_already(pos) { - // Already playing. Retrigger! - supplier_chain.pre_buffer_simple(0); - self.state = Suspending(SuspendingState { - next_state: StateAfterSuspension::Playing(PlayingState { - virtual_pos, - ..Default::default() - }), - pos, - }); - } else { - // Not yet playing. Reschedule! - self.schedule_play_internal(virtual_pos); - } - } else { - // Not yet playing. Reschedule! - self.schedule_play_internal(virtual_pos); - } - } - } - Suspending(s) => { - // It's important to handle this, otherwise some play actions simply have no effect, - // which is especially annoying when using transport sync because then it's like - // forgetting that clip ... the next time the transport is stopped and started, - // that clip won't play again. - self.state = ReadySubState::Suspending(SuspendingState { - next_state: StateAfterSuspension::Playing(PlayingState { - virtual_pos, - ..Default::default() - }), - ..s - }); - } - Paused(s) => { - // Resume - let pos = s.pos; - supplier_chain.install_immediate_start_interaction(pos); - self.state = ReadySubState::Playing(PlayingState { - pos: Some(pos), - ..Default::default() - }); - } - } - PlayOutcome { virtual_pos } - } - - fn resolve_stop_timing(&self, stop_args: &SlotStopArgs) -> ConcreteClipPlayStopTiming { - let start_timing = stop_args.resolve_start_timing(self.settings.start_timing); - let stop_timing = stop_args.resolve_stop_timing(self.settings.stop_timing); - ConcreteClipPlayStopTiming::resolve(start_timing, stop_timing) - } - - fn calculate_virtual_play_pos(&self, play_args: &SlotPlayArgs) -> VirtualPosition { - let start_timing = play_args.resolve_start_timing(self.settings.start_timing); - use ClipPlayStartTiming::*; - match start_timing { - Immediately => VirtualPosition::Now, - Quantized(q) => { - let ref_pos = play_args - .ref_pos - .unwrap_or_else(|| play_args.timeline.cursor_pos()); - let quantized_pos = play_args.timeline.next_quantized_pos_at( - ref_pos, - q, - Laziness::DwellingOnCurrentPos, - ); - VirtualPosition::Quantized(quantized_pos) - } - } - } - - /// Stops the clip. - /// - /// By default, if it's overdubbing, it just stops the overdubbing (a second call will make - /// it stop playing). - pub fn stop( - &mut self, - args: SlotStopArgs, - supplier_chain: &mut SupplierChain, - ) -> Option { - use ReadySubState::*; - match self.state { - Stopped => None, - Playing(s) => { - let overdub_outcome = if s.overdubbing { - // Currently recording overdub. Stop recording. - self.state = Playing(PlayingState { - overdubbing: false, - ..s - }); - let outcome = supplier_chain.stop_midi_overdubbing().ok(); - if !args.enforce_play_stop { - // Continue playing - return outcome; - } - outcome - } else { - None - }; - // Just playing, not recording. - if let Some(pos) = s.pos { - if s.stop_request.is_none() { - // Not yet scheduled for stop. - self.state = if supplier_chain.is_playing_already(pos) { - // Playing - let resolved_stop_timing = self.resolve_stop_timing(&args); - use ConcreteClipPlayStopTiming::*; - match resolved_stop_timing { - Immediately => { - // Immediately. Transition to stop. - Suspending(SuspendingState { - next_state: StateAfterSuspension::Stopped, - pos, - }) - } - Quantized(q) => { - let ref_pos = - args.ref_pos.unwrap_or_else(|| args.timeline.cursor_pos()); - let quantized_pos = args.timeline.next_quantized_pos_at( - ref_pos, - q, - Laziness::DwellingOnCurrentPos, - ); - Playing(PlayingState { - stop_request: Some(StopRequest::Quantized(quantized_pos)), - ..s - }) - } - UntilEndOfClip => { - if self.settings.looped { - // Schedule - supplier_chain.keep_playing_until_end_of_current_cycle(pos); - Playing(PlayingState { - stop_request: Some(StopRequest::AtEndOfClip), - ..s - }) - } else { - // Scheduling stop of a non-repeated clip doesn't make - // sense. - self.state - } - } - } - } else { - // Not yet playing. Backpedal. - Stopped - }; - } - } else { - // Not yet playing. Backpedal. - self.state = Stopped; - } - overdub_outcome - } - Paused(_) => { - self.state = Stopped; - None - } - Suspending(s) => { - let resolved_stop_timing = self.resolve_stop_timing(&args); - if resolved_stop_timing == ConcreteClipPlayStopTiming::Immediately { - // We are in another transition already. Simply change it to stop. - self.state = Suspending(SuspendingState { - next_state: StateAfterSuspension::Stopped, - ..s - }); - } - None - } - } - } - - pub fn process( - &mut self, - args: &mut ClipProcessArgs, - supplier_chain: &mut SupplierChain, - shared_pos: &mut SharedPos, - shared_peak: &mut SharedPeak, - ) -> (ClipProcessingOutcome, Option) { - use ReadySubState::*; - let (outcome, changed_state, pos) = match self.state { - Stopped | Paused(_) => return (Default::default(), None), - Playing(s) => { - let outcome = self.process_playing(s, args, supplier_chain, shared_peak); - (outcome, None, s.pos.unwrap_or_default()) - } - Suspending(s) => { - let (outcome, changed_state) = - self.process_suspending(s, args, supplier_chain, shared_peak); - (outcome, changed_state, s.pos) - } - }; - shared_pos.set(pos); - (outcome, changed_state) - } - - fn process_playing( - &mut self, - s: PlayingState, - args: &mut ClipProcessArgs, - supplier_chain: &mut SupplierChain, - shared_peak: &mut SharedPeak, - ) -> ClipProcessingOutcome { - let material_info = match supplier_chain.material_info() { - Ok(i) => i, - Err(_) => return Default::default(), - }; - let general_info = self.prepare_playing(args, supplier_chain, material_info.is_midi()); - let go = if let Some(pos) = s.pos { - // Already counting in or playing. - if let Some(seek_pos) = s.seek_pos { - // Seek requested - self.calculate_seek_go(supplier_chain, pos, seek_pos, &material_info) - } else if args.resync { - // Resync requested - debug!("Resync"); - let go = self.go( - s, - args, - supplier_chain, - general_info.clip_tempo_factor, - &material_info, - ); - supplier_chain.pre_buffer_simple(go.pos); - go - } else { - // Normal situation: Continue playing - // Check if the resolve step would still arrive at the same result as our - // frame-advancing counter. - let compare_pos = resolve_virtual_pos( - s.virtual_pos, - args, - general_info.clip_tempo_factor, - false, - &material_info, - None, - ); - if material_info.is_midi() && compare_pos != pos { - // This happened a lot when the MIDI_FRAME_RATE wasn't a multiple of the sample - // rate and PPQ. - // debug!("ATTENTION: compare pos {} != pos {}", compare_pos, pos); - } - Go { - pos, - ..Go::default() - } - } - } else { - // Not counting in or playing yet. - self.go( - s, - args, - supplier_chain, - general_info.clip_tempo_factor, - &material_info, - ) - }; - // Resolve potential quantized stop position if not yet done. - if let Some(StopRequest::Quantized(quantized_pos)) = s.stop_request { - if !supplier_chain.stop_interaction_is_installed_already() { - // We have a quantized stop request. Calculate distance from quantized position. - // This should be a negative position because we should be left of the stop. - let distance_from_quantized_stop_pos = resolve_virtual_pos( - VirtualPosition::Quantized(quantized_pos), - args, - general_info.clip_tempo_factor, - false, - &material_info, - None, - ); - // Derive stop position within material. - let stop_pos = go.pos - distance_from_quantized_stop_pos; - let mod_stop_pos = modulo_frame(stop_pos, material_info.frame_count()); - debug!( - "Calculated stop position {} (mod_stop_pos = {}, go pos = {}, distance = {}, quantized pos = {:?}, tempo factor = {:?})", - stop_pos, mod_stop_pos, go.pos, distance_from_quantized_stop_pos, quantized_pos, general_info.clip_tempo_factor - ); - supplier_chain.schedule_stop_interaction_at(stop_pos); - } - } - let fill_samples_outcome = self.fill_samples( - args, - go.pos, - &general_info, - go.sample_rate_factor, - supplier_chain, - &material_info, - shared_peak, - ); - self.state = if let Some(next_frame) = fill_samples_outcome.next_frame { - // There's still something to play. - ReadySubState::Playing(PlayingState { - pos: Some(next_frame), - seek_pos: go.new_seek_pos.and_then(|new_seek_pos| { - // Check if we reached our desired position. - if next_frame >= new_seek_pos as isize { - // Reached - None - } else { - // Not reached yet. - Some(new_seek_pos) - } - }), - ..s - }) - } else { - // We have reached the natural or scheduled-stop (at end of clip) end. Everything that - // needed to be played has been played in previous blocks. Audio fade outs have been - // applied as well, so no need to go to suspending state first. Go right to stop! - supplier_chain.pre_buffer_simple(0); - self.reset_for_play(supplier_chain); - ReadySubState::Stopped - }; - ClipProcessingOutcome { - num_audio_frames_written: fill_samples_outcome.num_audio_frames_written, - } - } - - fn go( - &mut self, - playing_state: PlayingState, - args: &ClipProcessArgs, - supplier_chain: &mut SupplierChain, - clip_tempo_factor: f64, - material_info: &MaterialInfo, - ) -> Go { - let tempo = self.tempo(material_info.is_midi()); - let pos = resolve_virtual_pos( - playing_state.virtual_pos, - args, - clip_tempo_factor, - true, - material_info, - tempo, - ); - if supplier_chain.is_playing_already(pos) { - debug!("Install immediate start interaction because material playing already"); - supplier_chain.install_immediate_start_interaction(pos); - } - Go { - pos, - ..Go::default() - } - } - - fn calculate_seek_go( - &mut self, - supplier_chain: &mut SupplierChain, - pos: MaterialPos, - seek_pos: usize, - material_info: &MaterialInfo, - ) -> Go { - // Seek requested. - if material_info.is_midi() { - // MIDI. Let's jump to the position directly. - Go { - pos: seek_pos as isize, - sample_rate_factor: 1.0, - new_seek_pos: None, - } - } else { - // Audio. Let's fast-forward if possible. - let (sample_rate_factor, new_seek_pos) = if supplier_chain.is_playing_already(pos) { - // Playing. - let pos = pos as usize; - let seek_pos = if pos < seek_pos { - seek_pos - } else { - seek_pos + material_info.frame_count() - }; - // We might need to fast-forward. - let real_distance = seek_pos - pos; - let desired_distance_in_secs = DurationInSeconds::new(0.300); - let source_frame_rate = material_info.frame_rate(); - let desired_distance = convert_duration_in_seconds_to_frames( - desired_distance_in_secs, - source_frame_rate, - ); - if desired_distance < real_distance { - // We need to fast-forward. - let playback_speed_factor = - 16.0f64.min(real_distance as f64 / desired_distance as f64); - let sample_rate_factor = 1.0 / playback_speed_factor; - (sample_rate_factor, Some(seek_pos)) - } else { - // We are almost there anyway, so no. - (1.0, None) - } - } else { - // Counting in. - // We prevent seek during count-in but just in case, we reject it here. - (1.0, None) - }; - Go { - pos, - sample_rate_factor, - new_seek_pos, - } - } - } - - /// Returns the next frame to be queried. - /// - /// Returns `None` if end of material. - #[allow(clippy::too_many_arguments)] - fn fill_samples( - &mut self, - args: &mut ClipProcessArgs, - start_frame: isize, - info: &SupplyRequestGeneralInfo, - sample_rate_factor: f64, - supplier_chain: &mut SupplierChain, - material_info: &MaterialInfo, - shared_peak: &mut SharedPeak, - ) -> FillSamplesOutcome { - let dest_sample_rate = Hz::new(args.dest_sample_rate.get() * sample_rate_factor); - let is_midi = material_info.is_midi(); - let response = if is_midi { - let resp = - self.fill_samples_midi(args, start_frame, info, dest_sample_rate, supplier_chain); - let has_note_on = args - .midi_event_list - .iter() - .any(|evt| evt.message().is_note_on()); - if has_note_on { - // Main thread is responsible for setting it back to MIN (acknowledges reading). - shared_peak.set(UnitValue::MAX); - } - resp - } else { - self.fill_samples_audio(args, start_frame, info, dest_sample_rate, supplier_chain) - }; - let (num_frames_written, next_frame) = match response.status { - SupplyResponseStatus::PleaseContinue => ( - args.dest_buffer.frame_count(), - Some(start_frame + response.num_frames_consumed as isize), - ), - SupplyResponseStatus::ReachedEnd { num_frames_written } => (num_frames_written, None), - }; - FillSamplesOutcome { - num_audio_frames_written: if is_midi { 0 } else { num_frames_written }, - next_frame, - } - } - - fn fill_samples_audio( - &mut self, - args: &mut ClipProcessArgs, - start_frame: isize, - info: &SupplyRequestGeneralInfo, - dest_sample_rate: Hz, - supplier_chain: &mut SupplierChain, - ) -> SupplyResponse { - let request = SupplyAudioRequest { - start_frame, - dest_sample_rate: Some(dest_sample_rate), - info: SupplyRequestInfo { - audio_block_frame_offset: 0, - requester: "root-audio", - note: "", - is_realtime: true, - }, - parent_request: None, - general_info: info, - }; - supplier_chain.supply_audio(&request, args.dest_buffer) - } - - fn fill_samples_midi( - &mut self, - args: &mut ClipProcessArgs, - start_frame: isize, - info: &SupplyRequestGeneralInfo, - dest_sample_rate: Hz, - supplier_chain: &mut SupplierChain, - ) -> SupplyResponse { - let request = SupplyMidiRequest { - start_frame, - dest_frame_count: args.dest_buffer.frame_count(), - dest_sample_rate, - info: SupplyRequestInfo { - audio_block_frame_offset: 0, - requester: "root-midi", - note: "", - is_realtime: true, - }, - parent_request: None, - general_info: info, - }; - supplier_chain.supply_midi(&request, args.midi_event_list) - } - - fn prepare_playing( - &mut self, - args: &ClipProcessArgs, - supplier_chain: &mut SupplierChain, - is_midi: bool, - ) -> SupplyRequestGeneralInfo { - let tempo_factor = self.calc_tempo_factor(args.timeline_tempo, is_midi); - let general_info = SupplyRequestGeneralInfo { - audio_block_timeline_cursor_pos: args.timeline_cursor_pos, - audio_block_length: args.dest_buffer.frame_count(), - output_frame_rate: args.dest_sample_rate, - timeline_tempo: args.timeline_tempo, - clip_tempo_factor: tempo_factor, - }; - supplier_chain.set_tempo_factor(tempo_factor); - general_info - } - - fn calc_tempo_factor(&self, timeline_tempo: Bpm, is_midi: bool) -> f64 { - if let Some(clip_tempo) = self.tempo(is_midi) { - calc_tempo_factor(clip_tempo, timeline_tempo) - } else { - 1.0 - } - } - - fn process_suspending( - &mut self, - s: SuspendingState, - args: &mut ClipProcessArgs, - supplier_chain: &mut SupplierChain, - shared_peak: &mut SharedPeak, - ) -> (ClipProcessingOutcome, Option) { - let material_info = match supplier_chain.material_info() { - Ok(i) => i, - Err(_) => return (Default::default(), None), - }; - let general_info = self.prepare_playing(args, supplier_chain, material_info.is_midi()); - // TODO-medium We could do that already when changing to suspended. That saves us the - // check if a stop interaction is installed already. - if !supplier_chain.stop_interaction_is_installed_already() { - supplier_chain.install_immediate_stop_interaction(s.pos); - } - let fill_samples_outcome = self.fill_samples( - args, - s.pos, - &general_info, - 1.0, - supplier_chain, - &material_info, - shared_peak, - ); - let (next_state, recording_state) = - if let Some(next_frame) = fill_samples_outcome.next_frame { - // Suspension not finished yet. - let next_state = ReadySubState::Suspending(SuspendingState { - pos: next_frame, - ..s - }); - (next_state, None) - } else { - // Suspension finished. - use StateAfterSuspension::*; - self.reset_for_play(supplier_chain); - match s.next_state { - Playing(s) => (ReadySubState::Playing(s), None), - Paused => (ReadySubState::Paused(PausedState { pos: s.pos }), None), - Stopped => { - supplier_chain.pre_buffer_simple(0); - (ReadySubState::Stopped, None) - } - Recording(s) => (self.state, Some(s)), - } - }; - self.state = next_state; - let outcome = ClipProcessingOutcome { - num_audio_frames_written: fill_samples_outcome.num_audio_frames_written, - }; - (outcome, recording_state) - } - - fn reset_for_play(&mut self, supplier_chain: &mut SupplierChain) { - supplier_chain.reset_for_play(self.settings.looped); - } - - // TODO-high-clip-engine The error type is too large! - #[allow(clippy::result_large_err)] - pub fn midi_overdub( - &mut self, - args: MidiOverdubInstruction, - supplier_chain: &mut SupplierChain, - ) -> Result<(), ErrorWithPayload> { - use ReadySubState::*; - // TODO-medium Maybe we should start to play if not yet playing - if let Playing(s) = self.state { - supplier_chain.start_midi_overdub(args.source_replacement, args.settings); - self.state = Playing(PlayingState { - overdubbing: true, - ..s - }); - Ok(()) - } else { - Err(ErrorWithPayload::new("clip not playing", args)) - } - } - - pub fn record( - &mut self, - args: ClipRecordArgs, - project: Option, - supplier_chain: &mut SupplierChain, - matrix_settings: &OverridableMatrixSettings, - column_settings: &RtColumnSettings, - ) -> Option { - let recording_args = RecordingArgs::from_stuff( - project, - column_settings, - matrix_settings, - &args.settings, - args.recording_equipment, - ); - supplier_chain.prepare_recording(recording_args); - let recording_state = RecordingState { - rollback_data: { - let data = RollbackData { - clip_settings: self.settings, - }; - Some(data) - }, - settings: args.settings, - }; - use ReadySubState::*; - match self.state { - Stopped => Some(recording_state), - Playing(s) => { - if let Some(pos) = s.pos { - if supplier_chain.is_playing_already(pos) { - debug!("Suspending play in order to start recording"); - self.state = Suspending(SuspendingState { - next_state: StateAfterSuspension::Recording(recording_state), - pos, - }); - None - } else { - Some(recording_state) - } - } else { - Some(recording_state) - } - } - Suspending(s) => { - self.state = Suspending(SuspendingState { - next_state: StateAfterSuspension::Recording(recording_state), - ..s - }); - None - } - Paused(_) => Some(recording_state), - } - } - - pub fn pause(&mut self, supplier_chain: &SupplierChain) { - use ReadySubState::*; - match self.state { - Stopped | Paused(_) => {} - Playing(s) => { - if let Some(pos) = s.pos { - if supplier_chain.is_playing_already(pos) { - // Playing. Pause! - self.state = Suspending(SuspendingState { - next_state: StateAfterSuspension::Paused, - pos, - }); - } - } - // If not yet playing, we don't do anything at the moment. - // TODO-medium In future, we could defer the clip scheduling to the future. I think - // that would feel natural. - } - Suspending(s) => { - self.state = Suspending(SuspendingState { - next_state: StateAfterSuspension::Paused, - ..s - }); - } - } - } - - pub fn seek( - &mut self, - desired_pos: UnitValue, - supplier_chain: &SupplierChain, - ) -> ClipEngineResult<()> { - let material_info = supplier_chain.material_info()?; - let frame_count = material_info.frame_count(); - let desired_frame = adjust_proportionally_positive(frame_count as f64, desired_pos.get()); - use ReadySubState::*; - match self.state { - Stopped | Suspending(_) => {} - Playing(s) => { - if let Some(pos) = s.pos { - if supplier_chain.is_playing_already(pos) { - let up_cycled_frame = - self.up_cycle_frame(desired_frame, pos, frame_count, &material_info); - self.state = Playing(PlayingState { - seek_pos: Some(up_cycled_frame), - ..s - }); - } - } - } - Paused(s) => { - let up_cycled_frame = - self.up_cycle_frame(desired_frame, s.pos, frame_count, &material_info); - self.state = Paused(PausedState { - pos: up_cycled_frame as isize, - }); - } - } - Ok(()) - } - - fn up_cycle_frame( - &self, - frame: usize, - offset_pos: isize, - frame_count: usize, - material_info: &MaterialInfo, - ) -> usize { - let current_cycle = material_info.get_cycle_at_frame(offset_pos); - current_cycle * frame_count + frame - } - - pub fn play_state(&self) -> InternalClipPlayState { - use ReadySubState::*; - let api_state = match self.state { - Stopped => ClipPlayState::Stopped, - Playing(s) => { - if s.overdubbing { - ClipPlayState::Recording - } else if s.stop_request.is_some() { - ClipPlayState::ScheduledForPlayStop - } else if let Some(pos) = s.pos { - // It's correct that we don't consider the downbeat here. We want to expose - // the count-in phase as count-in phase, even some pickup beats are playing - // already. - if pos < 0 { - ClipPlayState::ScheduledForPlayStart - } else { - ClipPlayState::Playing - } - } else { - ClipPlayState::ScheduledForPlayStart - } - } - Suspending(s) => match s.next_state { - StateAfterSuspension::Playing(_) => ClipPlayState::Playing, - StateAfterSuspension::Paused => ClipPlayState::Paused, - StateAfterSuspension::Stopped => ClipPlayState::Stopped, - StateAfterSuspension::Recording(_) => ClipPlayState::Recording, - }, - Paused(_) => ClipPlayState::Paused, - }; - api_state.into() - } - - fn schedule_play_internal(&mut self, virtual_pos: VirtualPosition) { - self.state = ReadySubState::Playing(PlayingState { - virtual_pos, - ..Default::default() - }); - } -} - -impl RecordingState { - pub fn stop( - &mut self, - args: SlotStopArgs, - supplier_chain: &mut SupplierChain, - event_handler: &H, - ) -> ClipEngineResult { - let ref_pos = args.ref_pos.unwrap_or_else(|| args.timeline.cursor_pos()); - let outcome = match supplier_chain.stop_recording( - args.timeline, - ref_pos, - args.audio_request_props, - )? { - StopRecordingOutcome::Committed(outcome) => { - let ready_state = self.finish_recording( - outcome, - supplier_chain, - event_handler, - args.matrix_settings, - args.column_settings, - ); - ClipRecordingStopOutcome::TransitionToReady(ready_state) - } - StopRecordingOutcome::Canceled => { - event_handler.normal_recording_finished(NormalRecordingOutcome::Canceled); - if let Some(rollback_data) = &self.rollback_data { - let ready_state = ReadyState { - state: ReadySubState::Stopped, - settings: rollback_data.clip_settings, - }; - debug!("Rolling back to old clip"); - ClipRecordingStopOutcome::TransitionToReady(ready_state) - } else { - debug!("Clearing slot after recording canceled"); - ClipRecordingStopOutcome::ClearSlot - } - } - StopRecordingOutcome::EndScheduled => ClipRecordingStopOutcome::KeepState, - }; - Ok(outcome) - } - - fn finish_recording( - self, - outcome: RecordingOutcome, - supplier_chain: &mut SupplierChain, - event_handler: &H, - matrix_settings: &OverridableMatrixSettings, - column_settings: &RtColumnSettings, - ) -> ReadyState { - debug!("Finishing recording"); - let clip_settings = RtClipSettings::derive_from_recording( - &self.settings, - &outcome.data, - matrix_settings, - column_settings, - ); - let clip_settings = clip_settings.unwrap(); - let chain_settings = clip_settings.create_chain_settings(matrix_settings, column_settings); - supplier_chain - .configure_complete_chain(chain_settings) - .unwrap(); - // Prepare ready state - let ready_state = ReadyState { - state: if clip_settings.looped { - ReadySubState::Playing(PlayingState { - virtual_pos: match outcome.data.section_and_downbeat_data.quantized_end_pos { - None => VirtualPosition::Now, - Some(qp) => VirtualPosition::Quantized(qp), - }, - ..Default::default() - }) - } else { - ReadySubState::Stopped - }, - settings: clip_settings, - }; - // Send event - let material_info = outcome.material_info(); - let committed_recording = CommittedRecording { - kind_specific: outcome.kind_specific, - clip_settings, - material_info, - }; - event_handler - .normal_recording_finished(NormalRecordingOutcome::Committed(committed_recording)); - // Return ready state - // Finishing recording happens in the call stack of either record polling or stopping. - // Both of these things happen *before* get_samples() is called by the preview register. - // So get_samples() for the same block as the one we are in now will be called a moment - // later. That's what guarantees us that we don't miss any samples. - ready_state - } -} - -pub enum SlotInstruction { - ClearSlot, -} - -#[allow(clippy::large_enum_variant)] -enum ClipRecordingStopOutcome { - KeepState, - TransitionToReady(ReadyState), - ClearSlot, -} - -#[derive(Copy, Clone, Debug)] -pub struct SlotPlayArgs<'a> { - pub timeline: &'a HybridTimeline, - /// Set this if you already have the current timeline position or want to play a batch of clips. - pub ref_pos: Option, - pub matrix_settings: &'a OverridableMatrixSettings, - pub column_settings: &'a RtColumnSettings, - pub start_timing: Option, -} - -#[derive(Debug)] -pub struct SlotLoadArgs<'a> { - pub new_clips: RtClips, - pub event_sender: &'a Sender, - pub matrix_settings: &'a OverridableMatrixSettings, - pub column_settings: &'a RtColumnSettings, -} - -#[derive(Debug)] -pub struct SlotLoadClipArgs<'a> { - pub clip_index: usize, - pub new_clip: &'a mut RtClip, - pub event_sender: &'a Sender, - pub matrix_settings: &'a OverridableMatrixSettings, - pub column_settings: &'a RtColumnSettings, -} - -impl<'a> SlotPlayArgs<'a> { - pub fn resolve_start_timing( - &self, - clip_start_timing: Option, - ) -> ClipPlayStartTiming { - self.start_timing - .or(clip_start_timing) - .or(self.column_settings.clip_play_start_timing) - .unwrap_or(self.matrix_settings.clip_play_start_timing) - } -} - -#[derive(Copy, Clone, Debug)] -pub struct SlotStopArgs<'a> { - pub stop_timing: Option, - pub timeline: &'a HybridTimeline, - /// Set this if you already have the current timeline position or want to stop a batch of clips. - pub ref_pos: Option, - /// If this is `true` and the clip is overdubbing, it not just stops overdubbing but also - /// playing the clip. - pub enforce_play_stop: bool, - pub matrix_settings: &'a OverridableMatrixSettings, - pub column_settings: &'a RtColumnSettings, - pub audio_request_props: BasicAudioRequestProps, -} - -#[derive(Debug)] -pub struct ClipRecordingPollArgs<'a> { - pub matrix_settings: &'a OverridableMatrixSettings, - pub column_settings: &'a RtColumnSettings, - pub audio_request_props: BasicAudioRequestProps, -} - -impl<'a> SlotStopArgs<'a> { - pub fn resolve_start_timing( - &self, - clip_start_timing: Option, - ) -> ClipPlayStartTiming { - clip_start_timing - .or(self.column_settings.clip_play_start_timing) - .unwrap_or(self.matrix_settings.clip_play_start_timing) - } - - pub fn resolve_stop_timing( - &self, - clip_stop_timing: Option, - ) -> ClipPlayStopTiming { - self.stop_timing - .or(clip_stop_timing) - .or(self.column_settings.clip_play_stop_timing) - .unwrap_or(self.matrix_settings.clip_play_stop_timing) - } -} - -#[derive(Copy, Clone, Debug)] -pub enum VirtualPosition { - Now, - Quantized(QuantizedPosition), -} - -impl Default for VirtualPosition { - fn default() -> Self { - Self::Now - } -} - -impl VirtualPosition { - pub fn is_quantized(&self) -> bool { - matches!(self, VirtualPosition::Quantized(_)) - } -} - -#[derive(Debug)] -pub enum SlotRecordInstruction { - NewClip(RecordNewClipInstruction), - ExistingClip(ClipRecordArgs), - MidiOverdub(MidiOverdubInstruction), -} - -#[derive(Debug)] -pub struct RecordNewClipInstruction { - pub clip_id: RtClipId, - pub supplier_chain: SupplierChain, - pub project: Option, - pub shared_pos: SharedPos, - pub shared_peak: SharedPeak, - pub timeline: HybridTimeline, - pub timeline_cursor_pos: PositionInSeconds, - pub settings: MatrixClipRecordSettings, -} - -#[derive(Debug)] -pub struct MidiOverdubInstruction { - pub clip_index: usize, - /// We can't overdub on a file-based MIDI source. If the current MIDI source is a file-based - /// one, this field will contain a MidiSequence. The current real-time source needs - /// to be replaced with this one before overdubbing can work. - pub source_replacement: Option, - pub settings: MidiOverdubSettings, -} - -#[derive(Debug)] -pub struct ClipRecordArgs { - pub recording_equipment: RecordingEquipment, - pub settings: MatrixClipRecordSettings, -} - -#[derive(Debug)] -pub struct ApplyClipArgs<'a> { - pub other_clip: &'a mut RtClip, - pub matrix_settings: &'a OverridableMatrixSettings, - pub column_settings: &'a RtColumnSettings, -} - -#[derive(Debug)] -pub struct SetClipSettingsArgs<'a> { - pub clip_settings: RtClipSettings, - pub matrix_settings: &'a OverridableMatrixSettings, - pub column_settings: &'a RtColumnSettings, -} - -#[derive(Eq, PartialEq, Debug)] -pub enum ClipStopBehavior { - Immediately, - EndOfClip, -} - -pub struct ClipProcessArgs<'a, 'b> { - /// The destination buffer dictates the desired output frame count but it doesn't dictate the - /// channel count! Its channel count should always match the channel count of the clip itself. - pub dest_buffer: &'a mut AudioBufMut<'b>, - pub dest_sample_rate: Hz, - pub midi_event_list: &'a mut BorrowedMidiEventList, - pub timeline: &'a HybridTimeline, - pub timeline_cursor_pos: PositionInSeconds, - pub timeline_tempo: Bpm, - /// Tells the clip to re-calculate its ideal play position (set when doing resume-from-pause). - pub resync: bool, - pub matrix_settings: &'a OverridableMatrixSettings, - pub column_settings: &'a RtColumnSettings, -} - -impl<'a, 'b> ClipProcessArgs<'a, 'b> { - pub fn basic_audio_request_props(&self) -> BasicAudioRequestProps { - BasicAudioRequestProps { - block_length: self.dest_buffer.frame_count(), - frame_rate: self.dest_sample_rate, - } - } -} - -struct LogNaturalDeviationArgs { - quantized_pos: QuantizedPosition, - block_length: usize, - timeline: T, - timeline_cursor_pos: PositionInSeconds, - // timeline_tempo: Bpm, - clip_tempo_factor: f64, - timeline_frame_rate: Hz, - clip_tempo: Bpm, -} - -/// Play state of a slot. -#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)] -pub struct InternalClipPlayState(pub ClipPlayState); - -impl From for InternalClipPlayState { - fn from(inner: ClipPlayState) -> Self { - Self(inner) - } -} - -impl InternalClipPlayState { - pub fn get(&self) -> ClipPlayState { - self.0 - } - - pub fn id_string(&self) -> &'static str { - use ClipPlayState::*; - match self.0 { - Stopped => "stopped", - ScheduledForPlayStart => "scheduled_for_play_start", - Playing => "playing", - Paused => "paused", - ScheduledForPlayStop => "scheduled_for_play_stop", - ScheduledForRecordingStart => "scheduled_for_record_start", - Recording => "recording", - ScheduledForRecordingStop => "scheduled_for_record_stop", - } - } - - /// Translates this play state into a feedback value. - pub fn feedback_value(self) -> UnitValue { - use ClipPlayState::*; - match self.0 { - Stopped => UnitValue::new(0.1), - ScheduledForPlayStart => UnitValue::new(0.75), - Playing => UnitValue::MAX, - Paused => UnitValue::new(0.5), - ScheduledForPlayStop => UnitValue::new(0.25), - Recording => UnitValue::new(0.60), - ScheduledForRecordingStart => UnitValue::new(0.9), - ScheduledForRecordingStop => UnitValue::new(0.4), - } - } - - /// If you want to know if it's worth to push out position updates. - /// - /// Attention: This will return `false` if the clip is being suspended (e.g. fading out), so - /// don't use this to check whether processing is still necessary. - pub fn is_advancing(&self) -> bool { - use ClipPlayState::*; - matches!( - self.0, - ScheduledForPlayStart - | Playing - | ScheduledForPlayStop - | ScheduledForRecordingStart - | Recording - | ScheduledForRecordingStop - ) - } - - pub fn is_somehow_recording(&self) -> bool { - use ClipPlayState::*; - matches!( - self.0, - ScheduledForRecordingStart | Recording | ScheduledForRecordingStop - ) - } - - pub fn is_as_good_as_playing(&self) -> bool { - use ClipPlayState::*; - matches!(self.0, ScheduledForPlayStart | Playing) - } - - pub fn is_as_good_as_recording(&self) -> bool { - use ClipPlayState::*; - matches!(self.0, ScheduledForRecordingStart | Recording) - } - - pub fn is_stoppable(&self) -> bool { - self.is_as_good_as_playing() || self.is_as_good_as_recording() - } -} - -impl Default for InternalClipPlayState { - fn default() -> Self { - Self(ClipPlayState::Stopped) - } -} - -#[derive(Debug)] -pub enum SlotChangeEvent { - PlayState(InternalClipPlayState), - Clips(&'static str), - Continuous { - proportional: UnitValue, - seconds: PositionInSeconds, - peak: UnitValue, - }, -} - -#[derive(Debug)] -pub struct QualifiedClipChangeEvent { - pub clip_address: ClipAddress, - pub event: ClipChangeEvent, -} - -#[derive(Debug)] -pub enum ClipChangeEvent { - /// Everything within the clip has potentially changed. - Everything, - // TODO-high-clip-engine Is special handling for volume and looped necessary? - Volume(Db), - Looped(bool), -} - -#[derive(Debug)] -pub struct QualifiedSlotChangeEvent { - pub slot_address: ClipSlotAddress, - pub event: SlotChangeEvent, -} - -pub struct PlayOutcome { - pub virtual_pos: VirtualPosition, -} - -#[derive(PartialEq)] -enum ConcreteClipPlayStopTiming { - Immediately, - Quantized(EvenQuantization), - UntilEndOfClip, -} - -impl ConcreteClipPlayStopTiming { - pub fn resolve(start_timing: ClipPlayStartTiming, stop_timing: ClipPlayStopTiming) -> Self { - use ClipPlayStopTiming::*; - match stop_timing { - LikeClipStartTiming => match start_timing { - ClipPlayStartTiming::Immediately => Self::Immediately, - ClipPlayStartTiming::Quantized(q) => Self::Quantized(q), - }, - Immediately => Self::Immediately, - Quantized(q) => Self::Quantized(q), - UntilEndOfClip => Self::UntilEndOfClip, - } - } -} - -struct Go { - pos: isize, - sample_rate_factor: f64, - new_seek_pos: Option, -} - -impl Default for Go { - fn default() -> Self { - Go { - pos: 0, - sample_rate_factor: 1.0, - new_seek_pos: None, - } - } -} - -#[derive(Default)] -pub struct ClipProcessingOutcome { - pub num_audio_frames_written: usize, -} - -struct FillSamplesOutcome { - num_audio_frames_written: usize, - next_frame: Option, -} - -pub trait HandleSlotEvent { - fn midi_overdub_finished(&self, clip_id: RtClipId, outcome: MidiOverdubOutcome); - fn normal_recording_finished(&self, outcome: NormalRecordingOutcome); - fn slot_cleared(&self, clips: RtClips); -} - -/// Holds the result of a normal (non-overdub) recording. -/// -/// Can also be cancelled. -#[allow(clippy::large_enum_variant)] -#[derive(Clone, Debug)] -pub enum NormalRecordingOutcome { - Committed(CommittedRecording), - Canceled, -} - -/// Holds the data of a successful recording (material and settings). -#[derive(Clone, Debug)] -pub struct CommittedRecording { - pub kind_specific: KindSpecificRecordingOutcome, - pub clip_settings: RtClipSettings, - pub material_info: MaterialInfo, -} - -/// All settings of a clip that affect processing. -/// -/// To be sent back to the main thread to update the main thread clip. -#[derive(Copy, Clone, PartialEq, Default, Debug)] -pub struct RtClipSettings { - pub time_base: api::ClipTimeBase, - pub looped: bool, - pub volume: api::Db, - pub section: api::Section, - pub start_timing: Option, - pub stop_timing: Option, - pub audio_settings: api::ClipAudioSettings, - pub midi_settings: api::ClipMidiSettings, -} - -impl RtClipSettings { - pub fn from_api(clip: &api::Clip) -> Self { - Self { - time_base: clip.time_base, - looped: clip.looped, - volume: clip.volume, - section: clip.section, - start_timing: clip.start_timing, - stop_timing: clip.stop_timing, - audio_settings: clip.audio_settings, - midi_settings: clip.midi_settings, - } - } - - pub fn derive_from_recording( - record_settings: &MatrixClipRecordSettings, - data: &CompleteRecordingData, - matrix_settings: &OverridableMatrixSettings, - column_settings: &RtColumnSettings, - ) -> ClipEngineResult { - let current_play_start_timing = column_settings - .clip_play_start_timing - .unwrap_or(matrix_settings.clip_play_start_timing); - let settings = Self { - start_timing: record_settings.effective_play_start_timing( - data.initial_play_start_timing, - current_play_start_timing, - ), - stop_timing: record_settings.effective_play_stop_timing( - data.initial_play_start_timing, - current_play_start_timing, - ), - looped: record_settings.looped, - time_base: { - let audio_tempo = if data.is_midi { - None - } else { - Some(api::Bpm::new(data.tempo.get())?) - }; - record_settings.effective_play_time_base( - data.initial_play_start_timing, - audio_tempo, - api::TimeSignature { - numerator: data.time_signature.numerator.get(), - denominator: data.time_signature.denominator.get(), - }, - api::PositiveBeat::new(data.downbeat_in_beats().get())?, - ) - }, - volume: api::Db::ZERO, - section: api::Section { - start_pos: PositiveSecond::new(data.section_start_pos_in_seconds().get())?, - length: data - .section_length_in_seconds() - .map(|l| PositiveSecond::new(l.get()).unwrap()), - }, - audio_settings: ClipAudioSettings { - // In general, a recording won't be automatically cut correctly, so we apply fades. - apply_source_fades: true, - time_stretch_mode: None, - resample_mode: None, - cache_behavior: None, - }, - midi_settings: record_settings.midi_settings.clip_settings, - }; - Ok(settings) - } - - fn create_chain_settings( - &self, - matrix_settings: &OverridableMatrixSettings, - column_settings: &RtColumnSettings, - ) -> ChainSettings { - ChainSettings { - looped: self.looped, - time_base: self.time_base, - volume: self.volume, - section: self.section, - audio_apply_source_fades: self.audio_settings.apply_source_fades, - midi_settings: self.midi_settings, - audio_time_stretch_mode: self - .audio_settings - .time_stretch_mode - .or(column_settings.audio_time_stretch_mode) - .unwrap_or(matrix_settings.audio_time_stretch_mode), - audio_resample_mode: self - .audio_settings - .resample_mode - .or(column_settings.audio_resample_mode) - .unwrap_or(matrix_settings.audio_resample_mode), - cache_behavior: self - .audio_settings - .cache_behavior - .or(column_settings.audio_cache_behavior) - .unwrap_or(matrix_settings.audio_cache_behavior), - } - } -} - -fn log_natural_deviation( - args: LogNaturalDeviationArgs, - material_info: &MaterialInfo, -) { - if args.quantized_pos.denominator() != 1 { - // This is not a quantization to a single bar. Never tested with that. - return; - } - let start_bar = args.quantized_pos.position() as i32; - let start_bar_timeline_pos = args.timeline.pos_of_quantized_pos(args.quantized_pos); - // Assuming a constant tempo and time signature during one cycle - let clip_duration = material_info.duration(); - let source_frame_rate = material_info.frame_rate(); - let beat_count = calculate_beat_count(args.clip_tempo, clip_duration); - let bar_count = (beat_count as f64 / 4.0).ceil() as u32; - let end_bar = start_bar + bar_count as i32; - let end_bar_timeline_pos = args - .timeline - .pos_of_quantized_pos(QuantizedPosition::bar(end_bar as i64)); - debug_assert!( - end_bar_timeline_pos > start_bar_timeline_pos, - "end_bar_timeline_pos {end_bar_timeline_pos} <= start_bar_timeline_pos {start_bar_timeline_pos}", - ); - // Timeline cycle length - let timeline_cycle_length_in_secs = (end_bar_timeline_pos - start_bar_timeline_pos).abs(); - let timeline_cycle_length_in_timeline_frames = convert_duration_in_seconds_to_frames( - timeline_cycle_length_in_secs, - args.timeline_frame_rate, - ); - let timeline_cycle_length_in_source_frames = - convert_duration_in_seconds_to_frames(timeline_cycle_length_in_secs, source_frame_rate); - // Source cycle length - let source_cycle_length_in_secs = clip_duration; - let source_cycle_length_in_timeline_frames = convert_duration_in_seconds_to_frames( - source_cycle_length_in_secs, - args.timeline_frame_rate, - ); - let source_cycle_length_in_source_frames = material_info.frame_count(); - // Block length - let block_length_in_timeline_frames = args.block_length; - let block_length_in_secs = convert_duration_in_frames_to_seconds( - block_length_in_timeline_frames, - args.timeline_frame_rate, - ); - let block_length_in_source_frames = convert_duration_in_frames_to_other_frame_rate( - block_length_in_timeline_frames, - args.timeline_frame_rate, - source_frame_rate, - ); - // Block count and remainder - let num_full_blocks = source_cycle_length_in_source_frames / block_length_in_source_frames; - let remainder_in_source_frames = - source_cycle_length_in_source_frames % block_length_in_source_frames; - // Tempo-adjusted - let adjusted_block_length_in_source_frames = adjust_proportionally_positive( - block_length_in_source_frames as f64, - args.clip_tempo_factor, - ); - let adjusted_block_length_in_timeline_frames = convert_duration_in_frames_to_other_frame_rate( - adjusted_block_length_in_source_frames, - source_frame_rate, - args.timeline_frame_rate, - ); - let adjusted_block_length_in_secs = convert_duration_in_frames_to_seconds( - adjusted_block_length_in_source_frames, - source_frame_rate, - ); - let adjusted_remainder_in_source_frames = - adjust_proportionally_positive(remainder_in_source_frames as f64, args.clip_tempo_factor); - // Source cycle remainder - let adjusted_remainder_in_timeline_frames = convert_duration_in_frames_to_other_frame_rate( - adjusted_remainder_in_source_frames, - source_frame_rate, - args.timeline_frame_rate, - ); - let adjusted_remainder_in_secs = convert_duration_in_frames_to_seconds( - adjusted_remainder_in_source_frames, - source_frame_rate, - ); - let block_index = (args.timeline_cursor_pos.get() / block_length_in_secs.get()) as isize; - debug!( - "\n\ - # Natural deviation report\n\ - Block index: {},\n\ - Tempo factor: {:.3}\n\ - Bars: {} ({} - {})\n\ - Start bar position: {:.3}s\n\ - Source cycle length: {:.3}ms (= {} timeline frames = {} source frames)\n\ - Timeline cycle length: {:.3}ms (= {} timeline frames = {} source frames)\n\ - Block length: {:.3}ms (= {} timeline frames = {} source frames)\n\ - Tempo-adjusted block length: {:.3}ms (= {} timeline frames = {} source frames)\n\ - Number of full blocks: {}\n\ - Tempo-adjusted remainder per cycle: {:.3}ms (= {} timeline frames = {} source frames)\n\ - ", - block_index, - args.clip_tempo_factor, - bar_count, - start_bar, - end_bar, - start_bar_timeline_pos.get(), - source_cycle_length_in_secs.get() * 1000.0, - source_cycle_length_in_timeline_frames, - source_cycle_length_in_source_frames, - timeline_cycle_length_in_secs.get() * 1000.0, - timeline_cycle_length_in_timeline_frames, - timeline_cycle_length_in_source_frames, - block_length_in_secs.get() * 1000.0, - block_length_in_timeline_frames, - block_length_in_source_frames, - adjusted_block_length_in_secs.get() * 1000.0, - adjusted_block_length_in_timeline_frames, - adjusted_block_length_in_source_frames, - num_full_blocks, - adjusted_remainder_in_secs.get() * 1000.0, - adjusted_remainder_in_timeline_frames, - adjusted_remainder_in_source_frames, - ); -} - -fn resolve_virtual_pos( - virtual_pos: VirtualPosition, - process_args: &ClipProcessArgs, - clip_tempo_factor: f64, - log_natural_deviation_enabled: bool, - material_info: &MaterialInfo, - // Used for logging natural deviation. - clip_tempo: Option, -) -> isize { - use VirtualPosition::*; - match virtual_pos { - Now => 0, - Quantized(qp) => { - let equipment = QuantizedPosCalcEquipment { - audio_request_props: process_args.basic_audio_request_props(), - timeline: process_args.timeline, - timeline_cursor_pos: process_args.timeline_cursor_pos, - clip_tempo_factor, - source_frame_rate: material_info.frame_rate(), - }; - let pos = calc_distance_from_quantized_pos(qp, equipment); - if log_natural_deviation_enabled { - // Quantization to bar - if let Some(clip_tempo) = clip_tempo { - // Plus, we react to tempo changes. - let args = LogNaturalDeviationArgs { - quantized_pos: qp, - block_length: process_args.dest_buffer.frame_count(), - timeline: process_args.timeline, - timeline_cursor_pos: process_args.timeline_cursor_pos, - clip_tempo_factor, - timeline_frame_rate: process_args.dest_sample_rate, - clip_tempo, - }; - log_natural_deviation(args, material_info); - } - } - pos - } - } -} - -#[derive(Copy, Clone, PartialEq, Debug)] -pub struct BasicAudioRequestProps { - pub block_length: usize, - pub frame_rate: Hz, -} - -impl BasicAudioRequestProps { - pub fn from_on_audio_buffer_args(args: &OnAudioBufferArgs) -> Self { - Self { - block_length: args.len as _, - frame_rate: args.srate, - } - } - - pub fn from_transfer(transfer: &PcmSourceTransfer) -> Self { - Self { - block_length: transfer.length() as _, - frame_rate: transfer.sample_rate(), - } - } -} - -pub struct QuantizedPosCalcEquipment<'a> { - pub audio_request_props: BasicAudioRequestProps, - pub timeline: &'a HybridTimeline, - pub timeline_cursor_pos: PositionInSeconds, - pub clip_tempo_factor: f64, - pub source_frame_rate: Hz, -} - -impl<'a> QuantizedPosCalcEquipment<'a> { - pub fn new_with_unmodified_tempo( - timeline: &'a HybridTimeline, - timeline_cursor_pos: PositionInSeconds, - timeline_tempo: Bpm, - audio_request_props: BasicAudioRequestProps, - is_midi: bool, - ) -> Self { - QuantizedPosCalcEquipment { - audio_request_props, - timeline, - timeline_cursor_pos, - clip_tempo_factor: if is_midi { - timeline_tempo.get() / MIDI_BASE_BPM.get() - } else { - 1.0 - }, - source_frame_rate: if is_midi { - MIDI_FRAME_RATE - } else { - audio_request_props.frame_rate - }, - } - } -} - -fn modulo_frame(frame: isize, frame_count: usize) -> isize { - if frame < 0 { - frame - } else { - frame % frame_count as isize - } -} diff --git a/playtime-clip-engine/src/rt/rt_column.rs b/playtime-clip-engine/src/rt/rt_column.rs deleted file mode 100644 index 292ca673e..000000000 --- a/playtime-clip-engine/src/rt/rt_column.rs +++ /dev/null @@ -1,1359 +0,0 @@ -use crate::mutex_util::{blocking_lock, non_blocking_lock}; -use crate::rt::supplier::{MaterialInfo, MidiOverdubOutcome, WriteAudioRequest, WriteMidiRequest}; -use crate::rt::{ - BasicAudioRequestProps, ClipRecordingPollArgs, HandleSlotEvent, InternalClipPlayState, - NormalRecordingOutcome, OwnedAudioBuffer, RtClip, RtClipId, RtClipSettings, RtClips, RtSlot, - RtSlotId, SetClipSettingsArgs, SlotLoadArgs, SlotLoadClipArgs, SlotPlayArgs, SlotProcessArgs, - SlotProcessTransportChangeArgs, SlotRecordInstruction, SlotRuntimeData, SlotStopArgs, - TransportChange, -}; -use crate::timeline::{clip_timeline, HybridTimeline, Timeline}; -use crate::ClipEngineResult; -use assert_no_alloc::assert_no_alloc; -use crossbeam_channel::{Receiver, Sender}; -use helgoboss_learn::UnitValue; -use indexmap::IndexMap; -use playtime_api::persistence as api; -use playtime_api::persistence::{ - AudioCacheBehavior, AudioTimeStretchMode, ClipPlayStartTiming, ClipPlayStopTiming, - ColumnClipPlaySettings, ColumnPlayMode, Db, MatrixClipPlaySettings, VirtualResampleMode, -}; -use reaper_high::Project; -use reaper_medium::{ - reaper_str, CustomPcmSource, DurationInBeats, DurationInSeconds, ExtendedArgs, GetPeakInfoArgs, - GetSamplesArgs, Hz, LoadStateArgs, OwnedPcmSource, PcmSource, PeaksClearArgs, - PositionInSeconds, PropertiesWindowArgs, ReaperStr, SaveStateArgs, SetAvailableArgs, - SetFileNameArgs, SetSourceArgs, -}; -use std::error::Error; -use std::mem; -use std::sync::{Arc, Mutex, MutexGuard, Weak}; -use xxhash_rust::xxh3::Xxh3Builder; - -/// Only such methods are public which are allowed to use from real-time threads. Other ones -/// are private and called from the method that processes the incoming commands. -#[derive(Debug)] -pub struct RtColumn { - matrix_settings: OverridableMatrixSettings, - settings: RtColumnSettings, - slots: RtSlots, - /// Slots end up here when removed. - /// - /// They stay there until they have faded out (prevents abrupt stops). - retired_slots: Vec, - /// Should be set to the project of the ReaLearn instance or `None` if on monitoring FX. - project: Option, - command_receiver: Receiver, - event_sender: Sender, - /// Enough reserved memory to hold one audio block of an arbitrary size. - mix_buffer_chunk: Vec, - timeline_was_paused_in_last_block: bool, -} - -#[derive(Clone, Debug)] -pub struct SharedRtColumn(Arc>); - -#[derive(Clone, Debug)] -pub struct WeakRtColumn(Weak>); - -impl SharedRtColumn { - pub fn new(column_source: RtColumn) -> Self { - Self(Arc::new(Mutex::new(column_source))) - } - - pub fn lock(&self) -> MutexGuard { - non_blocking_lock(&self.0, "real-time column") - } - - pub fn lock_allow_blocking(&self) -> MutexGuard { - blocking_lock(&self.0) - } - - pub fn downgrade(&self) -> WeakRtColumn { - WeakRtColumn(Arc::downgrade(&self.0)) - } - - pub fn strong_count(&self) -> usize { - Arc::strong_count(&self.0) - } -} - -impl WeakRtColumn { - pub fn upgrade(&self) -> Option { - self.0.upgrade().map(SharedRtColumn) - } -} - -#[derive(Clone, Debug)] -pub struct ColumnCommandSender { - /// Whether the sender should indeed send the commands or discard them. - /// - /// If the column doesn't have an associated track, then it doesn't have a preview register. - /// If no preview is running, real-time column commands can't be processed on the real-time - /// side. Sending them anyway would make the tasks accumulate and get out-of-date. That's why - /// we should discard them in this case. A full resync will be done as soon as a track is - /// associated again. - is_enabled: bool, - command_sender: Sender, -} - -impl ColumnCommandSender { - pub fn new(command_sender: Sender) -> Self { - Self { - is_enabled: false, - command_sender, - } - } - - pub fn set_enabled(&mut self, enabled: bool) { - self.is_enabled = enabled; - } - - pub fn move_slot_contents(&self, args: ColumnMoveSlotContentsArgs) { - self.send_task(RtColumnCommand::MoveSlotContents(args)); - } - - pub fn reorder_slots(&self, args: ColumnReorderSlotsArgs) { - self.send_task(RtColumnCommand::ReorderSlots(args)); - } - - pub fn clear_slots(&self) { - self.send_task(RtColumnCommand::ClearSlots); - } - - pub fn load(&self, args: ColumnLoadArgs) { - self.send_task(RtColumnCommand::Load(args)); - } - - pub fn update_settings(&self, settings: RtColumnSettings) { - self.send_task(RtColumnCommand::UpdateColumnSettings(settings)); - } - - pub fn update_matrix_settings(&self, settings: OverridableMatrixSettings) { - self.send_task(RtColumnCommand::UpdateMatrixSettings(settings)); - } - - pub fn load_slot(&self, args: Box>) { - self.send_task(RtColumnCommand::LoadSlot(args)); - } - - pub fn load_clip(&self, args: ColumnLoadClipArgs) { - self.send_task(RtColumnCommand::LoadClip(Box::new(args))); - } - - pub fn process_transport_change(&self, args: ColumnProcessTransportChangeArgs) { - self.send_task(RtColumnCommand::ProcessTransportChange(args)); - } - - pub fn clear_slot(&self, slot_index: usize) { - self.send_task(RtColumnCommand::ClearSlot(slot_index)); - } - - pub fn play_slot(&self, args: ColumnPlaySlotArgs) { - self.send_task(RtColumnCommand::PlaySlot(args)); - } - - pub fn play_row(&self, args: ColumnPlayRowArgs) { - self.send_task(RtColumnCommand::PlayRow(args)); - } - - pub fn stop_slot(&self, args: ColumnStopSlotArgs) { - self.send_task(RtColumnCommand::StopSlot(args)); - } - - pub fn remove_slot(&self, index: usize) { - self.send_task(RtColumnCommand::RemoveSlot(index)); - } - - pub fn stop(&self, args: ColumnStopArgs) { - self.send_task(RtColumnCommand::Stop(args)); - } - - pub fn panic(&self) { - self.send_task(RtColumnCommand::Panic); - } - - pub fn panic_slot(&self, slot_index: usize) { - self.send_task(RtColumnCommand::PanicSlot(slot_index)); - } - - pub fn set_clip_settings(&self, args: ColumnSetClipSettingsArgs) { - self.send_task(RtColumnCommand::SetClipSettings(args)); - } - - pub fn set_clip_looped(&self, args: ColumnSetClipLoopedArgs) { - self.send_task(RtColumnCommand::SetClipLooped(args)); - } - - pub fn pause_slot(&self, index: usize) { - let args = ColumnPauseSlotArgs { index }; - self.send_task(RtColumnCommand::PauseSlot(args)); - } - - pub fn seek_slot(&self, index: usize, desired_pos: UnitValue) { - let args = ColumnSeekSlotArgs { index, desired_pos }; - self.send_task(RtColumnCommand::SeekSlot(args)); - } - - pub fn set_clip_volume(&self, slot_index: usize, clip_index: usize, volume: Db) { - let args = ColumnSetClipVolumeArgs { - slot_index, - clip_index, - volume, - }; - self.send_task(RtColumnCommand::SetClipVolume(args)); - } - - pub fn set_clip_section(&self, slot_index: usize, clip_index: usize, section: api::Section) { - let args = ColumnSetClipSectionArgs { - slot_index, - clip_index, - section, - }; - self.send_task(RtColumnCommand::SetClipSection(args)); - } - - pub fn record_clip(&self, slot_index: usize, instruction: SlotRecordInstruction) { - let args = ColumnRecordClipArgs { - slot_index, - instruction, - }; - self.send_task(RtColumnCommand::RecordClip(Box::new(Some(args)))); - } - - fn send_task(&self, task: RtColumnCommand) { - if !self.is_enabled { - return; - } - self.command_sender.try_send(task).unwrap(); - } -} - -#[derive(Debug)] -pub enum RtColumnCommand { - ClearSlots, - Panic, - PanicSlot(usize), - Load(ColumnLoadArgs), - MoveSlotContents(ColumnMoveSlotContentsArgs), - ReorderSlots(ColumnReorderSlotsArgs), - ClearSlot(usize), - RemoveSlot(usize), - UpdateColumnSettings(RtColumnSettings), - UpdateMatrixSettings(OverridableMatrixSettings), - // Boxed because comparatively large. - LoadSlot(Box>), - LoadClip(Box), - ProcessTransportChange(ColumnProcessTransportChangeArgs), - PlaySlot(ColumnPlaySlotArgs), - PlayRow(ColumnPlayRowArgs), - StopSlot(ColumnStopSlotArgs), - Stop(ColumnStopArgs), - PauseSlot(ColumnPauseSlotArgs), - SeekSlot(ColumnSeekSlotArgs), - SetClipSettings(ColumnSetClipSettingsArgs), - SetClipVolume(ColumnSetClipVolumeArgs), - SetClipLooped(ColumnSetClipLoopedArgs), - SetClipSection(ColumnSetClipSectionArgs), - RecordClip(Box>), -} - -pub trait RtColumnEventSender { - fn slot_play_state_changed(&self, slot_id: RtSlotId, play_state: InternalClipPlayState); - - fn clip_material_info_changed( - &self, - slot_id: RtSlotId, - clip_id: RtClipId, - material_info: MaterialInfo, - ); - - fn slot_cleared(&self, slot_id: RtSlotId, clips: RtClips); - - fn record_request_acknowledged( - &self, - slot_id: RtSlotId, - result: Result, SlotRecordInstruction>, - ); - - fn midi_overdub_finished( - &self, - slot_id: RtSlotId, - clip_id: RtClipId, - outcome: MidiOverdubOutcome, - ); - - fn normal_recording_finished(&self, slot_id: RtSlotId, outcome: NormalRecordingOutcome); - - fn interaction_failed(&self, failure: InteractionFailure); - - fn dispose(&self, garbage: RtColumnGarbage); - - fn send_event(&self, event: RtColumnEvent); -} - -impl RtColumnEventSender for Sender { - fn slot_play_state_changed(&self, slot_id: RtSlotId, play_state: InternalClipPlayState) { - let event = RtColumnEvent::SlotPlayStateChanged { - slot_id, - play_state, - }; - self.send_event(event); - } - - fn clip_material_info_changed( - &self, - slot_id: RtSlotId, - clip_id: RtClipId, - material_info: MaterialInfo, - ) { - let event = RtColumnEvent::ClipMaterialInfoChanged { - slot_id, - clip_id, - material_info, - }; - self.send_event(event); - } - - fn slot_cleared(&self, slot_id: RtSlotId, clips: RtClips) { - let event = RtColumnEvent::SlotCleared { slot_id, clips }; - self.send_event(event); - } - - fn record_request_acknowledged( - &self, - slot_id: RtSlotId, - result: Result, SlotRecordInstruction>, - ) { - let event = RtColumnEvent::RecordRequestAcknowledged { slot_id, result }; - self.send_event(event); - } - - fn midi_overdub_finished( - &self, - slot_id: RtSlotId, - clip_id: RtClipId, - outcome: MidiOverdubOutcome, - ) { - let event = RtColumnEvent::MidiOverdubFinished { - slot_id, - clip_id, - outcome, - }; - self.send_event(event); - } - - fn normal_recording_finished(&self, slot_id: RtSlotId, outcome: NormalRecordingOutcome) { - let event = RtColumnEvent::NormalRecordingFinished { slot_id, outcome }; - self.send_event(event); - } - - fn dispose(&self, garbage: RtColumnGarbage) { - self.send_event(RtColumnEvent::Dispose(garbage)); - } - - fn interaction_failed(&self, failure: InteractionFailure) { - self.send_event(RtColumnEvent::InteractionFailed(failure)); - } - - fn send_event(&self, event: RtColumnEvent) { - self.try_send(event).unwrap(); - } -} - -#[derive(Clone, Debug, Default)] -pub struct RtColumnSettings { - pub clip_play_start_timing: Option, - pub clip_play_stop_timing: Option, - pub audio_time_stretch_mode: Option, - pub audio_resample_mode: Option, - pub audio_cache_behavior: Option, - pub play_mode: ColumnPlayMode, -} - -impl RtColumnSettings { - pub fn from_api(settings: &ColumnClipPlaySettings) -> Self { - Self { - clip_play_start_timing: settings.start_timing, - clip_play_stop_timing: settings.stop_timing, - audio_time_stretch_mode: settings.audio_settings.time_stretch_mode, - audio_resample_mode: settings.audio_settings.resample_mode, - audio_cache_behavior: settings.audio_settings.cache_behavior, - play_mode: settings.mode.unwrap_or_default(), - } - } -} - -#[derive(Clone, Debug, Default)] -pub struct OverridableMatrixSettings { - pub clip_play_start_timing: ClipPlayStartTiming, - pub clip_play_stop_timing: ClipPlayStopTiming, - pub audio_time_stretch_mode: AudioTimeStretchMode, - pub audio_resample_mode: VirtualResampleMode, - pub audio_cache_behavior: AudioCacheBehavior, -} - -impl OverridableMatrixSettings { - pub fn from_api(clip_play_settings: &MatrixClipPlaySettings) -> Self { - OverridableMatrixSettings { - clip_play_start_timing: clip_play_settings.start_timing, - clip_play_stop_timing: clip_play_settings.stop_timing, - audio_time_stretch_mode: clip_play_settings.audio_settings.time_stretch_mode, - audio_resample_mode: clip_play_settings.audio_settings.resample_mode, - audio_cache_behavior: clip_play_settings.audio_settings.cache_behavior, - } - } -} - -const MAX_AUDIO_CHANNEL_COUNT: usize = 64; -const MAX_BLOCK_SIZE: usize = 2048; - -/// At the time of this writing, a slot is just around 900 byte, so 100 slots take roughly 90 kB. -/// TODO-high-clip-engine 100 slots in one column is a lot ... but don't we want to guarantee -/// "no allocation" even for thousands of slots? -const MAX_SLOT_COUNT_WITHOUT_REALLOCATION: usize = 100; - -impl RtColumn { - pub fn new( - permanent_project: Option, - command_receiver: Receiver, - event_sender: Sender, - ) -> Self { - debug!("Slot size: {}", std::mem::size_of::()); - let hash_builder = base::hash_util::create_non_crypto_hash_builder(); - Self { - matrix_settings: Default::default(), - settings: Default::default(), - slots: RtSlots::with_capacity_and_hasher( - MAX_SLOT_COUNT_WITHOUT_REALLOCATION, - hash_builder, - ), - retired_slots: Vec::with_capacity(MAX_SLOT_COUNT_WITHOUT_REALLOCATION), - project: permanent_project, - command_receiver, - event_sender, - // Sized to hold pretty any audio block imaginable. Vastly oversized for the majority - // of use cases but 1 MB memory per column ... okay for now, on the safe side. - mix_buffer_chunk: OwnedAudioBuffer::new(MAX_AUDIO_CHANNEL_COUNT, MAX_BLOCK_SIZE) - .into_inner(), - timeline_was_paused_in_last_block: false, - } - } - - fn load_slot(&mut self, args: ColumnLoadSlotArgs) -> ClipEngineResult<()> { - let slot = get_slot_mut(&mut self.slots, args.slot_index)?; - let slot_args = SlotLoadArgs { - new_clips: args.clips, - event_sender: &self.event_sender, - matrix_settings: &self.matrix_settings, - column_settings: &self.settings, - }; - slot.load(slot_args); - Ok(()) - } - - fn load_clip(&mut self, args: &mut ColumnLoadClipArgs) -> ClipEngineResult<()> { - let slot = get_slot_mut(&mut self.slots, args.slot_index)?; - let slot_args = SlotLoadClipArgs { - clip_index: args.clip_index, - new_clip: &mut args.clip, - event_sender: &self.event_sender, - matrix_settings: &self.matrix_settings, - column_settings: &self.settings, - }; - slot.load_clip(slot_args)?; - Ok(()) - } - - pub fn slot(&self, index: usize) -> ClipEngineResult<&RtSlot> { - get_slot(&self.slots, index) - } - - /// # Errors - /// - /// Returns an error if the slot doesn't exist, doesn't have any clip or is currently recording. - pub fn play_slot( - &mut self, - args: ColumnPlaySlotArgs, - audio_request_props: BasicAudioRequestProps, - ) -> ClipEngineResult<()> { - let ref_pos = args.ref_pos.unwrap_or_else(|| args.timeline.cursor_pos()); - let slot_args = SlotPlayArgs { - timeline: &args.timeline, - ref_pos: Some(ref_pos), - matrix_settings: &self.matrix_settings, - column_settings: &self.settings, - start_timing: args.options.start_timing, - }; - let slot = get_slot_mut(&mut self.slots, args.slot_index)?; - if slot.is_filled() { - slot.play(slot_args)?; - if self.settings.play_mode.is_exclusive() { - self.stop_all_clips( - audio_request_props, - ref_pos, - &args.timeline, - Some(args.slot_index), - None, - ); - } - Ok(()) - } else if args.options.stop_column_if_slot_empty { - self.stop_all_clips(audio_request_props, ref_pos, &args.timeline, None, None); - Ok(()) - } else { - Err("slot is empty") - } - } - - /// # Errors - /// - /// Returns an error if the row doesn't exist. - pub fn play_row( - &mut self, - args: ColumnPlayRowArgs, - audio_request_props: BasicAudioRequestProps, - ) -> ClipEngineResult<()> { - if !self.settings.play_mode.follows_scene() { - return Ok(()); - } - if !self.settings.play_mode.is_exclusive() { - // When in column play mode "NonExclusiveFollowingScene", playing the clip itself - // doesn't take care of stopping the other clips. But when playing scenes, we want - // other clips to stop (otherwise they would accumulate). Do it manually. - self.stop_all_clips( - audio_request_props, - args.ref_pos, - &args.timeline, - Some(args.slot_index), - None, - ); - } - let play_args = ColumnPlaySlotArgs { - slot_index: args.slot_index, - timeline: args.timeline, - ref_pos: Some(args.ref_pos), - options: ColumnPlaySlotOptions { - stop_column_if_slot_empty: true, - start_timing: None, - }, - }; - self.play_slot(play_args, audio_request_props) - } - - pub fn stop(&mut self, args: ColumnStopArgs, audio_request_props: BasicAudioRequestProps) { - let ref_pos = args.ref_pos.unwrap_or_else(|| args.timeline.cursor_pos()); - self.stop_all_clips( - audio_request_props, - ref_pos, - &args.timeline, - None, - args.stop_timing, - ); - } - - fn stop_all_clips( - &mut self, - audio_request_props: BasicAudioRequestProps, - ref_pos: PositionInSeconds, - timeline: &HybridTimeline, - except: Option, - stop_timing: Option, - ) { - for (_, slot) in self - .slots - .values_mut() - .enumerate() - .filter(|(i, _)| except.map(|e| e != *i).unwrap_or(true)) - { - let stop_args = SlotStopArgs { - stop_timing, - timeline, - ref_pos: Some(ref_pos), - enforce_play_stop: true, - matrix_settings: &self.matrix_settings, - column_settings: &self.settings, - audio_request_props, - }; - let event_handler = SlotEventHandler::new(&self.event_sender, slot.id()); - let _ = slot.stop(stop_args, &event_handler); - } - } - - /// # Errors - /// - /// Returns an error if the slot doesn't exist or doesn't have any clip. - pub fn stop_slot( - &mut self, - args: ColumnStopSlotArgs, - audio_request_props: BasicAudioRequestProps, - ) -> ClipEngineResult<()> { - let clip_args = SlotStopArgs { - stop_timing: args.stop_timing, - timeline: &args.timeline, - ref_pos: args.ref_pos, - enforce_play_stop: false, - matrix_settings: &self.matrix_settings, - column_settings: &self.settings, - audio_request_props, - }; - let slot = get_slot_mut(&mut self.slots, args.slot_index)?; - let event_handler = SlotEventHandler::new(&self.event_sender, slot.id()); - slot.stop(clip_args, &event_handler) - } - - pub fn panic(&mut self) { - for slot in self.slots.values_mut() { - slot.panic(); - } - } - - pub fn panic_slot(&mut self, index: usize) -> ClipEngineResult<()> { - get_slot_mut(&mut self.slots, index)?.panic(); - Ok(()) - } - - pub fn remove_slot(&mut self, index: usize) -> ClipEngineResult<()> { - let (_, slot) = self - .slots - .shift_remove_index(index) - .ok_or("slot to be removed doesn't exist")?; - self.retire_slot(slot); - Ok(()) - } - - fn retire_slot(&mut self, mut slot: RtSlot) { - slot.initiate_removal(); - self.retired_slots.push(slot); - } - - pub fn set_clip_settings(&mut self, args: ColumnSetClipSettingsArgs) -> ClipEngineResult<()> { - let clip_args = SetClipSettingsArgs { - clip_settings: args.settings, - matrix_settings: &self.matrix_settings, - column_settings: &self.settings, - }; - get_slot_mut(&mut self.slots, args.slot_index)? - .get_clip_mut(args.clip_index)? - .set_settings(clip_args) - } - - pub fn set_clip_looped(&mut self, args: ColumnSetClipLoopedArgs) -> ClipEngineResult<()> { - get_slot_mut(&mut self.slots, args.slot_index)? - .get_clip_mut(args.clip_index)? - .set_looped(args.looped) - } - - pub fn set_clip_section(&mut self, args: ColumnSetClipSectionArgs) -> ClipEngineResult<()> { - get_slot_mut(&mut self.slots, args.slot_index)? - .get_clip_mut(args.clip_index)? - .set_section(args.section) - } - - /// See [`RtClip::recording_poll`]. - pub fn recording_poll( - &mut self, - slot_index: usize, - audio_request_props: BasicAudioRequestProps, - ) -> bool { - match get_slot_mut(&mut self.slots, slot_index) { - Ok(slot) => { - let args = ClipRecordingPollArgs { - matrix_settings: &self.matrix_settings, - column_settings: &self.settings, - audio_request_props, - }; - let event_handler = SlotEventHandler::new(&self.event_sender, slot.id()); - slot.recording_poll(args, &event_handler) - } - Err(_) => false, - } - } - - fn record_clip( - &mut self, - slot_index: usize, - instruction: SlotRecordInstruction, - audio_request_props: BasicAudioRequestProps, - ) -> ClipEngineResult<()> { - let slot = get_slot_mut(&mut self.slots, slot_index)?; - let slot_id = slot.id(); - let result = slot.record_clip(instruction, &self.matrix_settings, &self.settings); - let (informative_result, ack_result) = match result { - Ok(slot_runtime_data) => { - if self.settings.play_mode.is_exclusive() { - let timeline = clip_timeline(self.project, false); - let ref_pos = timeline.cursor_pos(); - self.stop_all_clips( - audio_request_props, - ref_pos, - &timeline, - Some(slot_index), - None, - ); - } - (Ok(()), Ok(slot_runtime_data)) - } - Err(e) => (Err(e.message), Err(e.payload)), - }; - self.event_sender - .record_request_acknowledged(slot_id, ack_result); - informative_result - } - - pub fn is_stoppable(&self) -> bool { - self.slots.values().any(|slot| slot.is_stoppable()) - } - - pub fn pause_slot(&mut self, args: ColumnPauseSlotArgs) -> ClipEngineResult<()> { - get_slot_mut(&mut self.slots, args.index)?.pause() - } - - fn seek_clip(&mut self, args: ColumnSeekSlotArgs) -> ClipEngineResult<()> { - get_slot_mut(&mut self.slots, args.index)?.seek(args.desired_pos) - } - - pub fn write_clip_midi( - &mut self, - index: usize, - request: WriteMidiRequest, - ) -> ClipEngineResult<()> { - get_slot_mut(&mut self.slots, index)?.write_clip_midi(request) - } - - pub fn write_clip_audio( - &mut self, - slot_index: usize, - request: impl WriteAudioRequest, - ) -> ClipEngineResult<()> { - get_slot_mut(&mut self.slots, slot_index)?.write_clip_audio(request) - } - - fn set_clip_volume(&mut self, args: ColumnSetClipVolumeArgs) -> ClipEngineResult<()> { - get_slot_mut(&mut self.slots, args.slot_index)? - .get_clip_mut(args.clip_index)? - .set_volume(args.volume); - Ok(()) - } - - fn process_transport_change(&mut self, args: ColumnProcessTransportChangeArgs) { - let args = SlotProcessTransportChangeArgs { - column_args: &args, - matrix_settings: &self.matrix_settings, - column_settings: &self.settings, - }; - for slot in self.slots.values_mut() { - let event_handler = SlotEventHandler::new(&self.event_sender, slot.id()); - let _ = slot.process_transport_change(&args, &event_handler); - } - } - - /// The duration of this column source is infinite. - fn duration(&self) -> DurationInSeconds { - DurationInSeconds::MAX - } - - /// Clears all the slots in this column, fading out still playing clips. - pub fn clear_slots(&mut self) { - for slot in self.slots.values_mut() { - slot.clear(); - } - } - - /// Replaces the slots in this column with the given ones but keeps unchanged slots playing - /// if possible and fades out still playing old slots. - pub fn load(&mut self, mut args: ColumnLoadArgs) { - // Take old slots out - let mut old_slots = mem::take(&mut self.slots); - // For each new slot, check if there's a corresponding old slot. In that case, update - // the old slot instead of completely replacing it with the new one. This keeps unchanged - // playing slots playing. - for new_slot in args.new_slots.values_mut() { - if let Some(mut old_slot) = old_slots.remove(&new_slot.id()) { - // We have an old slot with the same ID. Reuse it for smooth transition! - // Load the new slot's clips into the old clip by the slot's terms. After this, the - // new slot doesn't have clips and should not be used anymore. - let slot_args = SlotLoadArgs { - new_clips: mem::take(&mut new_slot.clips), - event_sender: &self.event_sender, - matrix_settings: &self.matrix_settings, - column_settings: &self.settings, - }; - old_slot.load(slot_args); - // Declare the old slot to be the new slot - let obsolete_slot = mem::replace(new_slot, old_slot); - // Dispose the obsolete slot - self.event_sender - .dispose(RtColumnGarbage::Slot(obsolete_slot)); - } - } - // Declare the mixture of updated and new slots as the new slot collection! - self.slots = args.new_slots; - // Retire old and now unused slots - for (_, slot) in old_slots.drain(..) { - self.retire_slot(slot); - } - // Dispose old and now empty slot collection - self.event_sender.dispose(RtColumnGarbage::Slots(old_slots)); - } - - /// Clears the clips in the given slot, fading out still playing clips. - pub fn clear_slot(&mut self, index: usize) -> ClipEngineResult<()> { - let slot = get_slot_mut(&mut self.slots, index)?; - slot.clear(); - Ok(()) - } - - pub fn move_slot_contents(&mut self, args: ColumnMoveSlotContentsArgs) -> ClipEngineResult<()> { - if args.source_index >= self.slots.len() { - return Err("source index out of bounds"); - } - if args.dest_index >= self.slots.len() { - return Err("destination index out of bounds"); - } - self.slots.swap_indices(args.source_index, args.dest_index); - Ok(()) - } - - pub fn reorder_slots(&mut self, args: ColumnReorderSlotsArgs) -> ClipEngineResult<()> { - if args.source_index >= self.slots.len() { - return Err("source index out of bounds"); - } - if args.dest_index >= self.slots.len() { - return Err("destination index out of bounds"); - } - self.slots.move_index(args.source_index, args.dest_index); - Ok(()) - } - - fn process_commands(&mut self, audio_request_props: BasicAudioRequestProps) { - while let Ok(task) = self.command_receiver.try_recv() { - use RtColumnCommand::*; - match task { - ClearSlots => { - self.clear_slots(); - } - Load(args) => { - self.load(args); - } - ClearSlot(slot_index) => { - let result = self.clear_slot(slot_index); - self.notify_user_about_failed_interaction(result); - } - MoveSlotContents(args) => { - self.move_slot_contents(args).unwrap(); - } - ReorderSlots(args) => { - self.reorder_slots(args).unwrap(); - } - UpdateColumnSettings(s) => { - self.settings = s; - } - UpdateMatrixSettings(s) => { - self.matrix_settings = s; - } - LoadSlot(mut boxed_args) => { - let args = boxed_args.take().unwrap(); - self.load_slot(args).unwrap(); - self.event_sender - .dispose(RtColumnGarbage::LoadSlotArgs(boxed_args)); - } - LoadClip(mut boxed_args) => { - self.load_clip(&mut boxed_args).unwrap(); - self.event_sender - .dispose(RtColumnGarbage::LoadClipArgs(boxed_args)); - } - PlaySlot(args) => { - let result = self.play_slot(args, audio_request_props); - self.notify_user_about_failed_interaction(result); - } - PlayRow(args) => { - let result = self.play_row(args, audio_request_props); - self.notify_user_about_failed_interaction(result); - } - ProcessTransportChange(args) => { - self.process_transport_change(args); - } - StopSlot(args) => { - let result = self.stop_slot(args, audio_request_props); - self.notify_user_about_failed_interaction(result); - } - RemoveSlot(index) => { - self.remove_slot(index).unwrap(); - } - Stop(args) => { - self.stop(args, audio_request_props); - } - Panic => { - self.panic(); - } - PanicSlot(slot_index) => { - self.panic_slot(slot_index).unwrap(); - } - PauseSlot(args) => { - self.pause_slot(args).unwrap(); - } - SetClipVolume(args) => { - self.set_clip_volume(args).unwrap(); - } - SeekSlot(args) => { - self.seek_clip(args).unwrap(); - } - SetClipSettings(args) => { - self.set_clip_settings(args).unwrap(); - } - SetClipLooped(args) => { - self.set_clip_looped(args).unwrap(); - } - SetClipSection(args) => { - self.set_clip_section(args).unwrap(); - } - RecordClip(mut boxed_args) => { - let args = boxed_args.take().unwrap(); - let result = - self.record_clip(args.slot_index, args.instruction, audio_request_props); - self.notify_user_about_failed_interaction(result); - self.event_sender - .dispose(RtColumnGarbage::RecordClipArgs(boxed_args)); - } - } - } - } - - fn notify_user_about_failed_interaction(&self, result: ClipEngineResult) { - if let Err(message) = result { - let failure = InteractionFailure { message }; - debug!("Failed clip interaction: {}", message); - self.event_sender.interaction_failed(failure); - } - } - - fn get_samples(&mut self, args: GetSamplesArgs) { - // We have code, e.g. triggered by crossbeam_channel that requests the ID of the - // current thread. This operation needs an allocation at the first time it's executed - // on a specific thread. If Live FX multi-processing is enabled, get_samples() will be - // called from a non-sticky worker thread instead of the audio interface thread (for which - // we already do this), so we execute this here again in order to initialize the current - // thread outside of assert_no_alloc. - let _ = std::thread::current().id(); - assert_no_alloc(|| { - let request_props = BasicAudioRequestProps::from_transfer(args.block); - // Super important that commands are processed before getting samples from clips. - // That's what guarantees that we act immediately to changes and also don't miss any - // samples after finishing recording. - self.process_commands(request_props); - // Make sure that in any case, we are only queried once per time, without retries. - // TODO-medium This mechanism of advancing the position on every call by - // the block duration relies on the fact that the preview - // register timeline calls us continuously and never twice per block. - // It would be better not to make that assumption and make this more - // stable by actually looking at the diff between the currently requested - // time_s and the previously requested time_s. If this diff is zero or - // doesn't correspond to the non-tempo-adjusted block duration, we know - // something is wrong. - unsafe { - args.block.set_samples_out(args.block.length()); - } - // Get main timeline info - let timeline = clip_timeline(self.project, false); - // Handle sync to project pause - if !timeline.is_running() { - // Main timeline is paused. - self.timeline_was_paused_in_last_block = true; - return; - } - let resync = if self.timeline_was_paused_in_last_block { - self.timeline_was_paused_in_last_block = false; - true - } else { - false - }; - // Get samples - let timeline_cursor_pos = timeline.cursor_pos(); - let timeline_tempo = timeline.tempo_at(timeline_cursor_pos); - // rt_debug!("block sr = {}, block length = {}, block time = {}, timeline cursor pos = {}, timeline cursor frame = {}", - // sample_rate, args.block.length(), args.block.time_s(), timeline_cursor_pos, timeline_cursor_frame); - let mut slot_args = SlotProcessArgs { - block: &mut *args.block, - mix_buffer_chunk: &mut self.mix_buffer_chunk, - timeline: &timeline, - timeline_cursor_pos, - timeline_tempo, - resync, - matrix_settings: &self.matrix_settings, - column_settings: &self.settings, - event_sender: &self.event_sender, - }; - // Fade out retired slots - self.retired_slots.retain_mut(|slot| { - let outcome = slot.process(&mut slot_args); - // As long as the slot still wrote audio frames, we keep it in memory. But as soon - // as no audio frames are written anymore, we can safely assume it's stopped and - // drop it. - let keep = outcome.num_audio_frames_written > 0; - // If done, dispose the slot in order to avoid deallocation in real-time thread - if !keep { - self.event_sender - .dispose(RtColumnGarbage::Slot(mem::take(slot))); - } - keep - }); - // Play current slots - for slot in self.slots.values_mut() { - let outcome = slot.process(&mut slot_args); - if let Some(changed_play_state) = outcome.changed_play_state { - self.event_sender - .slot_play_state_changed(slot.id(), changed_play_state); - } - } - }); - debug_assert_eq!(args.block.samples_out(), args.block.length()); - } - - fn extended(&mut self, _args: ExtendedArgs) -> i32 { - // TODO-medium Maybe implement PCM_SOURCE_EXT_NOTIFYPREVIEWPLAYPOS. This is the only - // extended call done by the preview register, at least for type WAVE. - 0 - } -} - -impl CustomPcmSource for SharedRtColumn { - fn duplicate(&mut self) -> Option { - unimplemented!() - } - - fn is_available(&mut self) -> bool { - unimplemented!() - } - - fn set_available(&mut self, _: SetAvailableArgs) { - unimplemented!() - } - - fn get_type(&mut self) -> &ReaperStr { - // This is not relevant for usage in preview registers, but it will be called. - // TODO-medium Return something less misleading here. - reaper_str!("WAVE") - } - - fn get_file_name(&mut self) -> Option<&ReaperStr> { - unimplemented!() - } - - fn set_file_name(&mut self, _: SetFileNameArgs) -> bool { - unimplemented!() - } - - fn get_source(&mut self) -> Option { - unimplemented!() - } - - fn set_source(&mut self, _: SetSourceArgs) { - unimplemented!() - } - - fn get_num_channels(&mut self) -> Option { - // This will only be called if the preview register is played without track. - unimplemented!("track-less columns not yet supported") - } - - fn get_sample_rate(&mut self) -> Option { - unimplemented!() - } - - fn get_length(&mut self) -> DurationInSeconds { - self.lock().duration() - } - - fn get_length_beats(&mut self) -> Option { - unimplemented!() - } - - fn get_bits_per_sample(&mut self) -> u32 { - unimplemented!() - } - - fn get_preferred_position(&mut self) -> Option { - unimplemented!() - } - - fn properties_window(&mut self, _: PropertiesWindowArgs) -> i32 { - unimplemented!() - } - - fn get_samples(&mut self, args: GetSamplesArgs) { - self.lock().get_samples(args) - } - - fn get_peak_info(&mut self, _: GetPeakInfoArgs) { - unimplemented!() - } - - fn save_state(&mut self, _: SaveStateArgs) { - unimplemented!() - } - - fn load_state(&mut self, _: LoadStateArgs) -> Result<(), Box> { - unimplemented!() - } - - fn peaks_clear(&mut self, _: PeaksClearArgs) { - unimplemented!() - } - - fn peaks_build_begin(&mut self) -> bool { - unimplemented!() - } - - fn peaks_build_run(&mut self) -> bool { - unimplemented!() - } - - fn peaks_build_finish(&mut self) { - unimplemented!() - } - - unsafe fn extended(&mut self, args: ExtendedArgs) -> i32 { - self.lock().extended(args) - } -} - -pub type RtSlots = IndexMap; - -#[derive(Debug)] -pub struct ColumnLoadArgs { - pub new_slots: RtSlots, -} - -#[derive(Debug)] -pub struct ColumnMoveSlotContentsArgs { - pub source_index: usize, - pub dest_index: usize, -} - -#[derive(Debug)] -pub struct ColumnReorderSlotsArgs { - pub source_index: usize, - pub dest_index: usize, -} - -#[derive(Debug)] -pub struct ColumnLoadSlotArgs { - pub slot_index: usize, - pub clips: RtClips, -} - -#[derive(Debug)] -pub struct ColumnLoadClipArgs { - pub slot_index: usize, - pub clip_index: usize, - pub clip: RtClip, -} - -#[derive(Copy, Clone, Eq, PartialEq, Debug)] -pub enum FillSlotMode { - Add, - Replace, -} - -#[derive(Debug)] -pub struct ColumnSetClipAudioResampleModeArgs { - pub slot_index: usize, - pub mode: VirtualResampleMode, -} - -#[derive(Debug)] -pub struct ColumnSetClipAudioTimeStretchModeArgs { - pub slot_index: usize, - pub mode: AudioTimeStretchMode, -} - -#[derive(Clone, Debug)] -pub struct ColumnPlaySlotArgs { - pub slot_index: usize, - pub timeline: HybridTimeline, - /// Set this if you already have the current timeline position or want to play a batch of clips. - pub ref_pos: Option, - pub options: ColumnPlaySlotOptions, -} - -#[derive(Clone, Debug)] -pub struct ColumnPlayRowArgs { - pub slot_index: usize, - pub timeline: HybridTimeline, - pub ref_pos: PositionInSeconds, -} - -#[derive(Clone, Debug, Default)] -pub struct ColumnPlaySlotOptions { - /// If the slot to be played is empty and this is `false`, nothing happens. If it's `true`, - /// it acts like a column stop button (good for matrix controllers without column stop button). - pub stop_column_if_slot_empty: bool, - pub start_timing: Option, -} - -#[derive(Debug)] -pub struct ColumnStopSlotArgs { - pub slot_index: usize, - pub timeline: HybridTimeline, - /// Set this if you already have the current timeline position or want to stop a batch of clips. - pub ref_pos: Option, - pub stop_timing: Option, -} - -#[derive(Clone, Debug)] -pub struct ColumnStopArgs { - pub timeline: HybridTimeline, - /// Set this if you already have the current timeline position or want to stop a batch of columns. - pub ref_pos: Option, - pub stop_timing: Option, -} - -#[derive(Debug)] -pub struct ColumnPauseSlotArgs { - pub index: usize, -} - -#[derive(Debug)] -pub struct ColumnSeekSlotArgs { - pub index: usize, - pub desired_pos: UnitValue, -} - -#[derive(Debug)] -pub struct ColumnSetClipSettingsArgs { - pub slot_index: usize, - pub clip_index: usize, - pub settings: RtClipSettings, -} - -#[derive(Debug)] -pub struct ColumnSetClipVolumeArgs { - pub slot_index: usize, - pub clip_index: usize, - pub volume: Db, -} - -#[derive(Debug)] -pub struct ColumnRecordClipArgs { - pub slot_index: usize, - pub instruction: SlotRecordInstruction, -} - -#[derive(Debug)] -pub struct ColumnSetClipLoopedArgs { - pub slot_index: usize, - pub clip_index: usize, - pub looped: bool, -} - -#[derive(Debug)] -pub struct ColumnSetClipSectionArgs { - pub slot_index: usize, - pub clip_index: usize, - pub section: api::Section, -} - -pub struct ColumnWithSlotArgs<'a> { - pub index: usize, - pub use_slot: &'a dyn Fn(), -} - -fn get_slot(slots: &RtSlots, index: usize) -> ClipEngineResult<&RtSlot> { - Ok(slots.get_index(index).ok_or(SLOT_DOESNT_EXIST)?.1) -} - -fn get_slot_mut(slots: &mut RtSlots, index: usize) -> ClipEngineResult<&mut RtSlot> { - Ok(slots.get_index_mut(index).ok_or(SLOT_DOESNT_EXIST)?.1) -} - -const SLOT_DOESNT_EXIST: &str = "slot doesn't exist"; - -#[derive(Debug)] -pub enum RtColumnEvent { - SlotPlayStateChanged { - slot_id: RtSlotId, - play_state: InternalClipPlayState, - }, - ClipMaterialInfoChanged { - slot_id: RtSlotId, - clip_id: RtClipId, - material_info: MaterialInfo, - }, - SlotCleared { - slot_id: RtSlotId, - clips: RtClips, - }, - RecordRequestAcknowledged { - slot_id: RtSlotId, - /// Slot runtime data is returned only if it's a recording from scratch (slot was not - /// filled before). - result: Result, SlotRecordInstruction>, - }, - MidiOverdubFinished { - slot_id: RtSlotId, - clip_id: RtClipId, - outcome: MidiOverdubOutcome, - }, - NormalRecordingFinished { - slot_id: RtSlotId, - outcome: NormalRecordingOutcome, - }, - Dispose(RtColumnGarbage), - InteractionFailed(InteractionFailure), -} - -#[derive(Debug)] -pub struct InteractionFailure { - pub message: &'static str, -} - -#[derive(Debug)] -#[allow(clippy::large_enum_variant)] -pub enum RtColumnGarbage { - Slot(RtSlot), - LoadSlotArgs(Box>), - LoadClipArgs(Box), - Clip(Option), - RecordClipArgs(Box>), - Slots(RtSlots), - Clips(RtClips), -} - -struct SlotEventHandler<'a> { - event_sender: &'a Sender, - slot_id: RtSlotId, -} - -impl<'a> SlotEventHandler<'a> { - pub fn new(event_sender: &'a Sender, slot_id: RtSlotId) -> Self { - Self { - slot_id, - event_sender, - } - } -} - -impl<'a> HandleSlotEvent for SlotEventHandler<'a> { - fn midi_overdub_finished(&self, clip_id: RtClipId, outcome: MidiOverdubOutcome) { - self.event_sender - .midi_overdub_finished(self.slot_id, clip_id, outcome); - } - - fn normal_recording_finished(&self, outcome: NormalRecordingOutcome) { - self.event_sender - .normal_recording_finished(self.slot_id, outcome); - } - - fn slot_cleared(&self, clips: RtClips) { - self.event_sender.slot_cleared(self.slot_id, clips); - } -} - -#[derive(Clone, Debug)] -pub struct ColumnProcessTransportChangeArgs { - pub change: TransportChange, - pub timeline: HybridTimeline, - pub timeline_cursor_pos: PositionInSeconds, - pub audio_request_props: BasicAudioRequestProps, -} diff --git a/playtime-clip-engine/src/rt/rt_matrix.rs b/playtime-clip-engine/src/rt/rt_matrix.rs deleted file mode 100644 index ab95ca956..000000000 --- a/playtime-clip-engine/src/rt/rt_matrix.rs +++ /dev/null @@ -1,366 +0,0 @@ -use crate::base::{ClipSlotAddress, MainMatrixCommandSender, MatrixGarbage}; -use crate::mutex_util::non_blocking_lock; -use crate::rt::{ - BasicAudioRequestProps, ColumnCommandSender, ColumnPlayRowArgs, ColumnPlaySlotArgs, - ColumnPlaySlotOptions, ColumnProcessTransportChangeArgs, ColumnStopArgs, ColumnStopSlotArgs, - RelevantPlayStateChange, SharedRtColumn, TransportChange, WeakRtColumn, -}; -use crate::{base, clip_timeline, ClipEngineResult, HybridTimeline, Timeline}; -use crossbeam_channel::{Receiver, Sender}; -use playtime_api::persistence::ClipPlayStopTiming; -use reaper_high::{Project, Reaper}; -use reaper_medium::{PlayState, ProjectContext, ReaperPointer}; -use std::mem; -use std::sync::{Arc, Mutex, MutexGuard, Weak}; - -/// A column here is just a weak pointer. 1000 pointers take just around 8 kB in total. -const MAX_COLUMN_COUNT_WITHOUT_REALLOCATION: usize = 1000; - -/// The real-time matrix is supposed to be used from real-time threads. -/// -/// It locks the column sources because it can be sure that there's no contention. Well, at least -/// if "Live FX multi-processing" is disabled - because then both the lock code and the preview -/// register playing are driven by the same (audio interface) thread and one thread can't do -/// multiple things in parallel. -/// -/// If "Live FX multi-processing" is enabled, the preview register playing will be driven by -/// different worker threads. Still, according to Justin, the locking should be okay even then. As -/// long as the worker threads don't hold the lock for too long. Which probably means that there's -/// no high risk of priority inversion because the worker threads have a higher priority as well. -/// -/// In theory, we could replace locking in favor of channels even here. But there's one significant -/// catch: It means the calling code can't "look into" the matrix and query current state before -/// executing a play, stop or whatever ... a sender is only uni-directional (bi-directional async -/// communication would also be possible but it's more complex to get it right in particular with -/// multiple similar requests coming in in sequence that depend on the result of the previous -/// request). So simply toggling play/stop would already need a channel message (= ReaLearn target) -/// that does just this. Which would be a deviation of ReaLearn's concept where the "glue" section -/// can decide what to do based on the current target value. But in case we need it, that would be -/// the way to go. -#[derive(Debug)] -pub struct RtMatrix { - column_handles: Vec, - command_receiver: Receiver, - main_command_sender: Sender, - project: Option, - last_project_play_state: PlayState, - play_position_jump_detector: PlayPositionJumpDetector, -} - -#[derive(Debug)] -pub struct ColumnHandle { - pub pointer: WeakRtColumn, - pub command_sender: ColumnCommandSender, -} - -#[derive(Clone, Debug)] -pub struct SharedRtMatrix(Arc>); - -#[derive(Clone, Debug)] -pub struct WeakRtMatrix(Weak>); - -impl SharedRtMatrix { - pub fn new(matrix: RtMatrix) -> Self { - Self(Arc::new(Mutex::new(matrix))) - } - - /// The real-time matrix should be locked only from real-time threads. - /// - /// Then we have no contention and this is super fast. - pub fn lock(&self) -> MutexGuard { - non_blocking_lock(&self.0, "real-time matrix") - } - - pub fn downgrade(&self) -> WeakRtMatrix { - WeakRtMatrix(Arc::downgrade(&self.0)) - } -} - -impl WeakRtMatrix { - pub fn upgrade(&self) -> Option { - self.0.upgrade().map(SharedRtMatrix) - } -} - -impl RtMatrix { - pub fn new( - command_receiver: Receiver, - command_sender: Sender, - project: Option, - ) -> Self { - Self { - column_handles: Vec::with_capacity(MAX_COLUMN_COUNT_WITHOUT_REALLOCATION), - command_receiver, - main_command_sender: command_sender, - project, - last_project_play_state: get_project_play_state(project), - play_position_jump_detector: PlayPositionJumpDetector::new(project), - } - } - - pub fn poll(&mut self, audio_request_props: BasicAudioRequestProps) { - if let Some(p) = self.project { - if !p.is_available() { - return; - } - } - let relevant_transport_change_detected = - self.detect_and_process_transport_change(audio_request_props); - if !relevant_transport_change_detected { - self.detect_and_process_play_position_jump(audio_request_props); - } - while let Ok(command) = self.command_receiver.try_recv() { - use RtMatrixCommand::*; - match command { - SetColumnHandles(handles) => { - let old_handles = mem::replace(&mut self.column_handles, handles); - self.main_command_sender - .throw_away(MatrixGarbage::ColumnHandles(old_handles)); - } - } - } - } - - fn detect_and_process_transport_change( - &mut self, - audio_request_props: BasicAudioRequestProps, - ) -> bool { - let timeline = clip_timeline(self.project, false); - let new_play_state = get_project_play_state(self.project); - let last_play_state = mem::replace(&mut self.last_project_play_state, new_play_state); - if let Some(relevant) = - RelevantPlayStateChange::from_play_state_change(last_play_state, new_play_state) - { - let args = ColumnProcessTransportChangeArgs { - change: TransportChange::PlayState(relevant), - timeline: timeline.clone(), - timeline_cursor_pos: timeline.cursor_pos(), - audio_request_props, - }; - for handle in &self.column_handles { - handle.command_sender.process_transport_change(args.clone()); - } - true - } else { - false - } - } - - fn detect_and_process_play_position_jump( - &mut self, - audio_request_props: BasicAudioRequestProps, - ) { - if !self.play_position_jump_detector.detect_play_jump() { - return; - } - let timeline = clip_timeline(self.project, true); - let args = ColumnProcessTransportChangeArgs { - change: TransportChange::PlayCursorJump, - timeline: timeline.clone(), - timeline_cursor_pos: timeline.cursor_pos(), - audio_request_props, - }; - for handle in &self.column_handles { - handle.command_sender.process_transport_change(args.clone()); - } - } - - pub fn play_clip( - &self, - coordinates: ClipSlotAddress, - options: ColumnPlaySlotOptions, - ) -> ClipEngineResult<()> { - let handle = self.column_handle(coordinates.column())?; - let args = ColumnPlaySlotArgs { - slot_index: coordinates.row(), - // TODO-medium This could be optimized. In real-time context, getting the timeline only - // once per block could save some resources. Sample with clip stop. - timeline: self.timeline(), - // TODO-medium We could even take the frame offset of the MIDI - // event into account and from that calculate the exact timeline position (within the - // block). That amount of accuracy is probably not necessary, but it's almost too easy - // to implement to not do it ... same with clip stop. - ref_pos: None, - options, - }; - handle.command_sender.play_slot(args); - Ok(()) - } - - pub fn stop_clip( - &self, - coordinates: ClipSlotAddress, - stop_timing: Option, - ) -> ClipEngineResult<()> { - let handle = self.column_handle(coordinates.column())?; - let args = ColumnStopSlotArgs { - slot_index: coordinates.row(), - timeline: self.timeline(), - ref_pos: None, - stop_timing, - }; - handle.command_sender.stop_slot(args); - Ok(()) - } - - pub fn is_stoppable(&self) -> bool { - self.columns_internal().any(|h| h.lock().is_stoppable()) - } - - pub fn column_is_stoppable(&self, index: usize) -> bool { - self.column_internal(index) - .map(|c| c.lock().is_stoppable()) - .unwrap_or(false) - } - - pub fn stop(&self, stop_timing: Option) { - let timeline = self.timeline(); - let args = ColumnStopArgs { - ref_pos: Some(timeline.cursor_pos()), - timeline, - stop_timing, - }; - for handle in &self.column_handles { - handle.command_sender.stop(args.clone()); - } - } - - pub fn stop_column( - &self, - index: usize, - stop_timing: Option, - ) -> ClipEngineResult<()> { - let handle = self.column_handle(index)?; - let args = ColumnStopArgs { - timeline: self.timeline(), - ref_pos: None, - stop_timing, - }; - handle.command_sender.stop(args); - Ok(()) - } - - pub fn play_row(&self, index: usize) { - let timeline = self.timeline(); - let timeline_cursor_pos = timeline.cursor_pos(); - let args = ColumnPlayRowArgs { - slot_index: index, - timeline, - ref_pos: timeline_cursor_pos, - }; - for handle in &self.column_handles { - handle.command_sender.play_row(args.clone()); - } - } - - fn timeline(&self) -> HybridTimeline { - clip_timeline(self.project, false) - } - - pub fn pause_slot(&self, coordinates: ClipSlotAddress) -> ClipEngineResult<()> { - let handle = self.column_handle(coordinates.column())?; - handle.command_sender.pause_slot(coordinates.row()); - Ok(()) - } - - pub fn column(&self, index: usize) -> ClipEngineResult { - self.column_internal(index) - } - - fn column_internal(&self, index: usize) -> ClipEngineResult { - let handle = self.column_handle(index)?; - handle - .pointer - .upgrade() - .ok_or("column doesn't exist anymore") - } - - fn columns_internal(&self) -> impl Iterator + '_ { - self.column_handles.iter().flat_map(|h| h.pointer.upgrade()) - } - - fn column_handle(&self, index: usize) -> ClipEngineResult<&ColumnHandle> { - self.column_handles.get(index).ok_or("column doesn't exist") - } -} - -pub enum RtMatrixCommand { - SetColumnHandles(Vec), -} - -pub trait RtMatrixCommandSender { - fn set_column_handles(&self, handles: Vec); - fn send_command(&self, command: RtMatrixCommand); -} - -impl RtMatrixCommandSender for Sender { - fn set_column_handles(&self, handles: Vec) { - self.send_command(RtMatrixCommand::SetColumnHandles(handles)); - } - - fn send_command(&self, command: RtMatrixCommand) { - self.try_send(command).unwrap(); - } -} - -fn get_project_play_state(project: Option) -> PlayState { - let project_context = get_project_context(project); - Reaper::get() - .medium_reaper() - .get_play_state_ex(project_context) -} - -fn get_project_context(project: Option) -> ProjectContext { - if let Some(p) = project { - p.context() - } else { - ProjectContext::CurrentProject - } -} - -/// Detects play position discontinuity while the project is playing, ignoring tempo changes. -#[derive(Debug)] -struct PlayPositionJumpDetector { - project_context: ProjectContext, - previous_beat: Option, -} - -impl PlayPositionJumpDetector { - pub fn new(project: Option) -> Self { - Self { - project_context: project - .map(|p| p.context()) - .unwrap_or(ProjectContext::CurrentProject), - previous_beat: None, - } - } - - /// Returns `true` if a jump has been detected. - /// - /// To be called in each audio block. - pub fn detect_play_jump(&mut self) -> bool { - let reaper = Reaper::get().medium_reaper(); - if let ProjectContext::Proj(p) = self.project_context { - if !reaper.validate_ptr(ReaperPointer::ReaProject(p)) { - // Project doesn't exist anymore. Happens when closing it. - return false; - } - } - let play_state = reaper.get_play_state_ex(self.project_context); - if !play_state.is_playing { - return false; - } - let play_pos = reaper.get_play_position_2_ex(self.project_context); - let res = reaper.time_map_2_time_to_beats(self.project_context, play_pos); - // TODO-high-later If we skip slightly forward within the beat or just to the next beat, the - // detector won't detect it as a jump. Destroys the synchronization. - let beat = res.full_beats.get() as isize; - if let Some(previous_beat) = self.previous_beat.replace(beat) { - let beat_diff = beat - previous_beat; - !(0..=1).contains(&beat_diff) - } else { - // Don't count initial change as jump. - false - } - } -} diff --git a/playtime-clip-engine/src/rt/rt_slot.rs b/playtime-clip-engine/src/rt/rt_slot.rs deleted file mode 100644 index 6455020e5..000000000 --- a/playtime-clip-engine/src/rt/rt_slot.rs +++ /dev/null @@ -1,717 +0,0 @@ -use crate::conversion_util::{ - adjust_pos_in_secs_anti_proportionally, convert_position_in_frames_to_seconds, -}; -use crate::rt::supplier::{MaterialInfo, WriteAudioRequest, WriteMidiRequest}; -use crate::rt::{ - ApplyClipArgs, AudioBufMut, ClipProcessArgs, ClipProcessingOutcome, ClipRecordingPollArgs, - ColumnProcessTransportChangeArgs, FillSlotMode, HandleSlotEvent, InternalClipPlayState, - OverridableMatrixSettings, RtClip, RtClipId, RtColumnEvent, RtColumnEventSender, - RtColumnGarbage, RtColumnSettings, SharedPeak, SharedPos, SlotInstruction, SlotLoadArgs, - SlotLoadClipArgs, SlotPlayArgs, SlotRecordInstruction, SlotStopArgs, -}; -use crate::{ClipEngineResult, ErrorWithPayload, HybridTimeline}; -use crossbeam_channel::Sender; -use helgoboss_learn::UnitValue; -use indexmap::IndexMap; -use playtime_api::persistence::{ClipPlayStopTiming, SlotId}; -use playtime_api::runtime::ClipPlayState; -use reaper_medium::{Bpm, PcmSourceTransfer, PlayState, PositionInSeconds}; -use std::{cmp, mem}; -use xxhash_rust::xxh3::Xxh3Builder; - -#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Default)] -pub struct RtSlotId(u64); - -impl RtSlotId { - pub fn from_slot_id(slot_id: &SlotId) -> Self { - Self(base::hash_util::calculate_non_crypto_hash(slot_id)) - } -} - -pub type RtClips = IndexMap; - -#[derive(Debug, Default)] -pub struct RtSlot { - id: RtSlotId, - pub(crate) clips: RtClips, - /// This is a vec of an option because RtClip doesn't implement Default. - retired_clips: Vec>, - runtime_data: InternalRuntimeData, -} - -#[derive(Debug, Default)] -struct InternalRuntimeData { - last_play_state: InternalClipPlayState, - stop_was_caused_by_transport_change: bool, -} - -impl RtSlot { - pub fn new(id: RtSlotId, clips: RtClips) -> Self { - Self { - id, - retired_clips: Vec::with_capacity(clips.len()), - clips, - runtime_data: Default::default(), - } - } - - /// Replaces the clips in this slot with the given ones but keeps unchanged clips playing if - /// possible and fades out still playing old clips. - /// - /// Exploits the given clips and replaces them with trash! They should not be used anymore. - pub fn load(&mut self, mut args: SlotLoadArgs) { - // Take old clips out - let mut old_clips = mem::take(&mut self.clips); - // For each new clip, check if there's a corresponding old clip. In that case, update - // the old clip instead of completely replacing it with the new one. This keeps unchanged - // playing clips playing. - for (_, new_clip) in &mut args.new_clips { - if let Some(mut old_clip) = old_clips.remove(&new_clip.id()) { - // We have an old clip with the same ID. Reuse it for smooth transition! - // Apply the new clip's settings to the old clip. - let apply_args = ApplyClipArgs { - other_clip: new_clip, - matrix_settings: args.matrix_settings, - column_settings: args.column_settings, - }; - let _ = old_clip.apply(apply_args); - // Declare the old clip to be the new clip - let obsolete_clip = mem::replace(new_clip, old_clip); - // Dispose the obsolete clip - args.event_sender - .dispose(RtColumnGarbage::Clip(Some(obsolete_clip))); - } - } - // Declare the mixture of updated and new clips as the new clip collection! - self.clips = args.new_clips; - // Retire old and now unused clips - retire_clips(&mut old_clips, &mut self.retired_clips); - // The old slot might actually still be playing. Notify listeners. - args.event_sender - .slot_play_state_changed(self.id, self.runtime_data.last_play_state); - // Dispose old and now empty clip collection - args.event_sender.dispose(RtColumnGarbage::Clips(old_clips)); - } - - /// Replaces the given clip in this slot with the given one but keeps the clip playing if - /// possible and fades out still playing old clips. - /// - /// Exploits the given clip and replaces it with trash! It should not be used anymore. - pub fn load_clip(&mut self, args: SlotLoadClipArgs) -> ClipEngineResult<()> { - let old_clip = self.get_clip_mut(args.clip_index)?; - // Apply the new clip's settings to the old clip. - let apply_args = ApplyClipArgs { - other_clip: args.new_clip, - matrix_settings: args.matrix_settings, - column_settings: args.column_settings, - }; - - old_clip.apply(apply_args)?; - Ok(()) - } - - pub fn id(&self) -> RtSlotId { - self.id - } - - pub fn last_play_state(&self) -> InternalClipPlayState { - self.runtime_data.last_play_state - } - - /// Returns the index at which the clip landed. - pub fn fill(&mut self, clip: RtClip, mode: FillSlotMode) -> usize { - // TODO-medium Suspend previous clip if playing. - match mode { - FillSlotMode::Add => { - self.clips.insert(clip.id(), clip); - self.clips.len() - 1 - } - FillSlotMode::Replace => { - self.clips.clear(); - self.clips.insert(clip.id(), clip); - 0 - } - } - } - - pub fn is_filled(&self) -> bool { - !self.clips.is_empty() - } - - pub fn find_clip(&self, index: usize) -> Option<&RtClip> { - Some(self.clips.get_index(index)?.1) - } - - pub fn clip_count(&self) -> usize { - self.clips.len() - } - - pub fn first_clip(&self) -> Option<&RtClip> { - Some(self.clips.first()?.1) - } - - /// See [`RtClip::recording_poll`]. - pub fn recording_poll( - &mut self, - args: ClipRecordingPollArgs, - event_handler: &H, - ) -> bool { - match self.get_clip_mut(0) { - Ok(clip) => clip.recording_poll(args, event_handler), - Err(_) => false, - } - } - - /// Plays all clips in this slot. - pub fn play(&mut self, args: SlotPlayArgs) -> ClipEngineResult<()> { - for clip in self.get_clips_mut()? { - clip.play(args)?; - } - Ok(()) - } - - /// Stops the slot immediately, initiating fade-outs if necessary. - /// - /// Also stops recording. Consumer should just wait for the slot to be stopped and then not use - /// it anymore. - pub fn initiate_removal(&mut self) { - for clip in self.clips.values_mut() { - clip.initiate_removal() - } - } - - /// Stops the slot immediately, initiating fade-outs if necessary. - /// - /// Doesn't touch recording. - pub fn panic(&mut self) { - self.runtime_data.stop_was_caused_by_transport_change = false; - for clip in self.clips.values_mut() { - clip.panic(); - } - } - - /// Stops all clips in this slot. - pub fn stop( - &mut self, - args: SlotStopArgs, - event_handler: &H, - ) -> ClipEngineResult<()> { - self.runtime_data.stop_was_caused_by_transport_change = false; - let mut instruction = None; - for clip in self.get_clips_mut()? { - let inst = clip.stop(args, event_handler)?; - if let Some(inst) = inst { - instruction = Some(inst); - } - } - if let Some(instruction) = instruction { - self.process_instruction(instruction, event_handler); - } - Ok(()) - } - - fn process_instruction( - &mut self, - instruction: SlotInstruction, - event_handler: &H, - ) { - use SlotInstruction::*; - match instruction { - ClearSlot => { - self.clear_internal(event_handler); - } - } - } - - /// Removes all the clips in this slot. - pub fn clear(&mut self) { - retire_clips(&mut self.clips, &mut self.retired_clips); - } - - fn clear_internal(&mut self, event_handler: &H) { - debug!("Clearing real-time slot"); - if self.clips.is_empty() { - return; - } - let old_clips = mem::take(&mut self.clips); - event_handler.slot_cleared(old_clips); - self.runtime_data = InternalRuntimeData::default(); - } - - /// # Errors - /// - /// Returns an error either if the instruction is to record on the given new clip but the slot - /// is not empty, or if the instruction is to record on an existing clip but the slot is empty. - /// - /// In both cases, it returns the instruction itself so it can be disposed appropriately. - pub fn record_clip( - &mut self, - instruction: SlotRecordInstruction, - matrix_settings: &OverridableMatrixSettings, - column_settings: &RtColumnSettings, - ) -> Result, ErrorWithPayload> { - use SlotRecordInstruction::*; - match instruction { - NewClip(instruction) => { - debug!("Record new clip"); - if !self.clips.is_empty() { - return Err(ErrorWithPayload::new( - "slot not empty", - NewClip(instruction), - )); - } - let clip = RtClip::recording(instruction); - let runtime_data = - SlotRuntimeData::new(&clip, true).expect("no material info in record_clip"); - self.clips.insert(clip.id(), clip); - Ok(Some(runtime_data)) - } - ExistingClip(args) => { - debug!("Record with existing clip"); - let clip = match self.clips.first_mut() { - None => { - return Err(ErrorWithPayload::new("slot empty", ExistingClip(args))); - } - Some(c) => c.1, - }; - match clip.record(args, matrix_settings, column_settings) { - Ok(_) => { - let runtime_data = SlotRuntimeData::new(clip, true) - .expect("no material info in record_clip"); - Ok(Some(runtime_data)) - } - Err(e) => Err(e.map_payload(ExistingClip)), - } - } - MidiOverdub(instruction) => { - debug!("MIDI overdub"); - match self.get_clip_mut(instruction.clip_index) { - Ok(clip) => match clip.midi_overdub(instruction) { - Ok(_) => Ok(None), - Err(e) => Err(e.map_payload(MidiOverdub)), - }, - Err(e) => Err(ErrorWithPayload::new(e, MidiOverdub(instruction))), - } - } - } - } - - pub fn pause(&mut self) -> ClipEngineResult<()> { - for clip in self.get_clips_mut()? { - clip.pause(); - } - Ok(()) - } - - pub fn seek(&mut self, desired_pos: UnitValue) -> ClipEngineResult<()> { - for clip in self.get_clips_mut()? { - clip.seek(desired_pos)?; - } - Ok(()) - } - - pub fn write_clip_midi(&mut self, request: WriteMidiRequest) -> ClipEngineResult<()> { - self.get_clip_mut(0)?.write_midi(request); - Ok(()) - } - - pub fn write_clip_audio(&mut self, request: impl WriteAudioRequest) -> ClipEngineResult<()> { - self.get_clip_mut(0)?.write_audio(request); - Ok(()) - } - - pub fn get_clip_mut(&mut self, index: usize) -> ClipEngineResult<&mut RtClip> { - Ok(self.clips.get_index_mut(index).ok_or(CLIP_DOESNT_EXIST)?.1) - } - - pub fn process_transport_change( - &mut self, - args: &SlotProcessTransportChangeArgs, - event_handler: &H, - ) -> ClipEngineResult<()> { - let mut instruction = None; - { - for clip in self.clips.values_mut() { - let inst = match args.column_args.change { - TransportChange::PlayState(rel_change) => { - // We have a relevant transport change. - let state = clip.play_state(); - use ClipPlayState::*; - use RelevantPlayStateChange::*; - match rel_change { - PlayAfterStop => { - match state.get() { - Stopped - if self - .runtime_data - .stop_was_caused_by_transport_change => - { - // REAPER transport was started from stopped state. Clip is stopped - // as well and was put in that state due to a previous transport - // stop. Play the clip! - play_clip_by_transport(clip, args) - } - ScheduledForPlayStart | Playing | ScheduledForPlayStop => { - // Retrigger (timeline switch) - play_clip_by_transport(clip, args) - } - Stopped - | Paused - | Recording - | ScheduledForRecordingStart - | ScheduledForRecordingStop => { - // Stop and forget. - self.runtime_data.stop_clip_by_transport( - clip, - args, - false, - event_handler, - )? - } - } - } - StopAfterPlay => match state.get() { - ScheduledForPlayStart - | Playing - | ScheduledForPlayStop - | Recording - | ScheduledForRecordingStart - | ScheduledForRecordingStop => { - // Stop and memorize - self.runtime_data.stop_clip_by_transport( - clip, - args, - true, - event_handler, - )? - } - - Stopped | Paused => { - // Stop and forget - self.runtime_data.stop_clip_by_transport( - clip, - args, - false, - event_handler, - )? - } - }, - StopAfterPause => self.runtime_data.stop_clip_by_transport( - clip, - args, - false, - event_handler, - )?, - } - } - TransportChange::PlayCursorJump => { - // The play cursor was repositioned. - let play_state = clip.play_state(); - use ClipPlayState::*; - if !matches!( - play_state.get(), - ScheduledForPlayStart | Playing | ScheduledForPlayStop - ) { - return Ok(()); - } - play_clip_by_transport(clip, args) - } - }; - if let Some(inst) = inst { - // Right now there's only one instruction we can have, so this is okay. - instruction = Some(inst); - } - } - }; - if let Some(instruction) = instruction { - self.process_instruction(instruction, event_handler); - } - Ok(()) - } - - pub fn process(&mut self, args: &mut SlotProcessArgs) -> SlotProcessingOutcome { - // Our strategy is to always write all available source channels into the mix - // buffer. From a performance perspective, it would actually be enough to take - // only as many channels as we need (= track channel count). However, always using - // the source channel count as reference is much simpler, in particular when it - // comes to caching and pre-buffering. Also, in practice this is rarely an issue. - // Most samples out there used in typical stereo track setups have no more than 2 - // channels. And if they do, the user can always down-mix to the desired channel - // count up-front. - let mut num_audio_frames_written = 0; - // Fade out retired clips - self.retired_clips.retain_mut(|clip| { - let keep = if let Some(clip) = clip.as_mut() { - let outcome = process_clip(clip, args, &mut num_audio_frames_written); - // As long as the clip still wrote audio frames, we keep it in memory. But as soon - // as no audio frames are written anymore, we can safely assume it's stopped and - // drop it. - outcome.num_audio_frames_written > 0 - } else { - false - }; - // If done, dispose the slot in order to avoid deallocation in real-time thread - if !keep { - args.event_sender - .dispose(RtColumnGarbage::Clip(clip.take())); - } - keep - }); - // Play current clips - let mut new_slot_play_state = InternalClipPlayState::default(); - for clip in self.clips.values_mut() { - process_clip(clip, args, &mut num_audio_frames_written); - // Aggregate clip play states into slot play state - new_slot_play_state = cmp::max(new_slot_play_state, clip.play_state()); - } - let last_play_state = - mem::replace(&mut self.runtime_data.last_play_state, new_slot_play_state); - SlotProcessingOutcome { - changed_play_state: if new_slot_play_state != last_play_state { - Some(new_slot_play_state) - } else { - None - }, - num_audio_frames_written, - } - } - - pub fn is_stoppable(&self) -> bool { - self.clips.values().any(|c| c.play_state().is_stoppable()) - } - - fn get_clips_mut(&mut self) -> ClipEngineResult> { - if self.clips.is_empty() { - return Err(SLOT_NOT_FILLED); - } - Ok(self.clips.values_mut()) - } -} - -impl InternalRuntimeData { - fn stop_clip_by_transport( - &mut self, - clip: &mut RtClip, - args: &SlotProcessTransportChangeArgs, - keep_starting_with_transport: bool, - event_handler: &H, - ) -> ClipEngineResult> { - self.stop_was_caused_by_transport_change = keep_starting_with_transport; - let args = SlotStopArgs { - stop_timing: Some(ClipPlayStopTiming::Immediately), - timeline: &args.column_args.timeline, - ref_pos: Some(args.column_args.timeline_cursor_pos), - enforce_play_stop: true, - matrix_settings: args.matrix_settings, - column_settings: args.column_settings, - audio_request_props: args.column_args.audio_request_props, - }; - clip.stop(args, event_handler) - } -} - -#[derive(Clone, Debug)] -pub struct SlotProcessTransportChangeArgs<'a> { - pub column_args: &'a ColumnProcessTransportChangeArgs, - pub matrix_settings: &'a OverridableMatrixSettings, - pub column_settings: &'a RtColumnSettings, -} - -const SLOT_NOT_FILLED: &str = "slot not filled"; -const CLIP_DOESNT_EXIST: &str = "clip doesn't exist"; - -#[derive(Copy, Clone, Debug)] -pub enum TransportChange { - PlayState(RelevantPlayStateChange), - PlayCursorJump, -} -#[derive(Copy, Clone, Debug)] -pub enum RelevantPlayStateChange { - PlayAfterStop, - StopAfterPlay, - StopAfterPause, -} - -impl RelevantPlayStateChange { - pub fn from_play_state_change(old: PlayState, new: PlayState) -> Option { - use RelevantPlayStateChange::*; - let change = if !old.is_paused && !old.is_playing && new.is_playing { - PlayAfterStop - } else if old.is_playing && !new.is_playing && !new.is_paused { - StopAfterPlay - } else if old.is_paused && !new.is_playing && !new.is_paused { - StopAfterPause - } else { - return None; - }; - Some(change) - } -} - -pub struct SlotProcessingOutcome { - pub changed_play_state: Option, - pub num_audio_frames_written: usize, -} - -fn play_clip_by_transport( - clip: &mut RtClip, - args: &SlotProcessTransportChangeArgs, -) -> Option { - let args = SlotPlayArgs { - timeline: &args.column_args.timeline, - ref_pos: Some(args.column_args.timeline_cursor_pos), - matrix_settings: args.matrix_settings, - column_settings: args.column_settings, - start_timing: None, - }; - clip.play(args).unwrap(); - None -} - -#[derive(Clone, Debug)] -pub struct SlotRuntimeData { - pub play_state: InternalClipPlayState, - pub pos: SharedPos, - pub peak: SharedPeak, - /// The frame count in this material info is supposed to take the section bounds into account. - pub material_info: MaterialInfo, -} - -impl SlotRuntimeData { - pub fn new(clip: &RtClip, use_recording_material_info: bool) -> ClipEngineResult { - let data = Self { - play_state: clip.play_state(), - pos: clip.shared_pos(), - peak: clip.shared_peak(), - material_info: if use_recording_material_info { - clip.recording_material_info()? - } else { - clip.material_info()? - }, - }; - Ok(data) - } - - pub fn mod_frame(&self) -> isize { - let frame = self.pos.get(); - if frame < 0 { - frame - } else if self.material_info.frame_count() > 0 { - frame % self.material_info.frame_count() as isize - } else { - 0 - } - } - - pub fn proportional_position(&self) -> ClipEngineResult { - let pos = self.pos.get(); - if pos < 0 { - return Err("count-in phase"); - } - let frame_count = self.material_info.frame_count(); - if frame_count == 0 { - return Err("frame count is zero"); - } - let mod_pos = pos as usize % frame_count; - let proportional = UnitValue::new_clamped(mod_pos as f64 / frame_count as f64); - Ok(proportional) - } - - pub fn position_in_seconds_during_recording(&self, timeline_tempo: Bpm) -> PositionInSeconds { - let tempo_factor = self - .material_info - .tempo_factor_during_recording(timeline_tempo); - self.position_in_seconds(tempo_factor) - } - - pub fn position_in_seconds(&self, tempo_factor: f64) -> PositionInSeconds { - let pos_in_source_frames = self.mod_frame(); - let pos_in_secs = convert_position_in_frames_to_seconds( - pos_in_source_frames, - self.material_info.frame_rate(), - ); - adjust_pos_in_secs_anti_proportionally(pos_in_secs, tempo_factor) - } - - pub fn peak(&self) -> UnitValue { - self.peak.reset() - } -} - -fn process_clip( - clip: &mut RtClip, - args: &mut SlotProcessArgs, - total_num_audio_frames_written: &mut usize, -) -> ClipProcessingOutcome { - let clip_channel_count = { - match clip.material_info() { - Ok(info) => info.channel_count(), - // If the clip doesn't have material, it's probably recording. We still - // allow the slot to process because it could propagate some play state - // changes. With a channel count of zero though. - Err(_) => 0, - } - }; - let mut mix_buffer = AudioBufMut::from_slice( - args.mix_buffer_chunk, - clip_channel_count, - args.block.length() as _, - ) - .unwrap(); - let mut inner_args = ClipProcessArgs { - dest_buffer: &mut mix_buffer, - dest_sample_rate: args.block.sample_rate(), - midi_event_list: args - .block - .midi_event_list_mut() - .expect("no MIDI event list available"), - timeline: args.timeline, - timeline_cursor_pos: args.timeline_cursor_pos, - timeline_tempo: args.timeline_tempo, - resync: args.resync, - matrix_settings: args.matrix_settings, - column_settings: args.column_settings, - }; - let outcome = clip.process(&mut inner_args); - // Aggregate number of written audio frames - *total_num_audio_frames_written = cmp::max( - *total_num_audio_frames_written, - outcome.num_audio_frames_written, - ); - // Write from mix buffer to destination buffer - if outcome.num_audio_frames_written > 0 { - let mut output_buffer = unsafe { AudioBufMut::from_pcm_source_transfer(args.block) }; - output_buffer - .slice_mut(0..outcome.num_audio_frames_written) - .modify_frames(|sample| { - // TODO-high-performance This is a hot code path. We might want to skip bound checks - // in sample_value_at(). - if sample.index.channel < clip_channel_count { - sample.value + mix_buffer.sample_value_at(sample.index).unwrap() - } else { - // Clip doesn't have material on this channel. - 0.0 - } - }) - } - outcome -} - -pub struct SlotProcessArgs<'a> { - pub block: &'a mut PcmSourceTransfer, - pub mix_buffer_chunk: &'a mut [f64], - pub timeline: &'a HybridTimeline, - pub timeline_cursor_pos: PositionInSeconds, - pub timeline_tempo: Bpm, - pub resync: bool, - pub matrix_settings: &'a OverridableMatrixSettings, - pub column_settings: &'a RtColumnSettings, - pub event_sender: &'a Sender, -} - -fn retire_clips(clips: &mut RtClips, retired_clips: &mut Vec>) { - for (_, mut clip) in clips.drain(..) { - clip.initiate_removal(); - retired_clips.push(Some(clip)) - } -} diff --git a/playtime-clip-engine/src/rt/schedule_util.rs b/playtime-clip-engine/src/rt/schedule_util.rs deleted file mode 100644 index 1d8756337..000000000 --- a/playtime-clip-engine/src/rt/schedule_util.rs +++ /dev/null @@ -1,107 +0,0 @@ -use crate::conversion_util::{ - adjust_proportionally_positive, convert_duration_in_frames_to_other_frame_rate, - convert_position_in_seconds_to_frames, -}; -use crate::rt::QuantizedPosCalcEquipment; -use crate::{QuantizedPosition, Timeline}; -use reaper_medium::PositionInSeconds; - -/// So, this is how we do play scheduling. Whenever the preview register -/// calls get_samples() and we are in a fresh ScheduledOrPlaying state, the -/// relative number of count-in frames will be determined. Based on the given -/// absolute bar for which the clip is scheduled. -/// -/// 1. We use a *relative* count-in (instead of just -/// using the absolute scheduled-play position and check if we reached it) -/// in order to respect arbitrary tempo changes during the count-in phase and -/// still end up starting on the correct point in time. Okay, we could reach -/// the same goal also by regularly checking whether we finally reached the -/// start of the bar. But first, we need the relative count-in anyway for pickup beats, -/// which start to play during count-in time. And second, just counting is cheaper -/// than repeatedly doing time/beat mapping. -/// -/// 2. We resolve the count-in length here, not at the time the play is requested. -/// Reason: Here we have block information such as block length and frame rate available. -/// That's not an urgent reason ... we could always cache this information and thus make it -/// available in the play request itself. Or we make sure that play/stop is always triggered -/// via receiving in get_samples()! That's good! TODO-medium Implement it. -/// In the past there were more urgent reasons but they are gone. I'll document them here -/// because they might remove doubt in case of possible future refactorings: -/// -/// 2a) The play request didn't happen in a real-time thread but in the main thread. -/// At that time it was important to resolve in get_samples() because the start time of the -/// next bar at play-request time was not necessarily the same as the one in the get_samples() -/// call, which would lead to wrong results. However, today, play requests always happen in -/// the real-time thread (a change introduced in favor of a lock-free design). -/// -/// 2b) I still thought that it would be better to do it here in case "Live FX multiprocessing" -/// is enabled. If this is enabled, it means get_samples() will in most situations be called in -/// a different real-time thread (some REAPER worker thread) than the play-request code -/// (audio interface thread). I worried that GetPlayPosition2Ex() in the worker thread would -/// return a different position as the audio interface thread would do. However, Justin -/// assured that the worker threads are designed to be synchronous with the audio interface -/// thread and they return the same values. So this is not a reason anymore. -pub fn calc_distance_from_quantized_pos( - quantized_pos: QuantizedPosition, - equipment: QuantizedPosCalcEquipment, -) -> isize { - // Essential calculation - let quantized_timeline_pos = equipment.timeline.pos_of_quantized_pos(quantized_pos); - calc_distance_from_pos(quantized_timeline_pos, equipment) -} - -pub fn calc_distance_from_pos( - quantized_timeline_pos: PositionInSeconds, - equipment: QuantizedPosCalcEquipment, -) -> isize { - // Essential calculation - let rel_pos_from_quant_in_secs = equipment.timeline_cursor_pos - quantized_timeline_pos; - let rel_pos_from_quant_in_source_frames = convert_position_in_seconds_to_frames( - rel_pos_from_quant_in_secs, - equipment.source_frame_rate, - ); - //region Description - // Now we have a countdown/position in source frames, but it doesn't yet - // take the tempo adjustment of the source into account. - // Once we have initialized the countdown with the first value, each - // get_samples() call - including this one - will advance it by a frame - // count that ideally = block length in source frames * tempo factor. - // We use this countdown approach for two reasons. - // - // 1. In order to allow tempo changes during count-in time. - // 2. If the downbeat is > 0, the count-in phase plays source material already. - // - // Especially (2) means that the count-in phase will not always have that - // ideal length which makes the source frame ZERO be perfectly aligned with - // the ZERO of the timeline bar. I think this is unavoidable when dealing - // with material that needs sample-rate conversion and/or time - // stretching. So if one of this is involved, this is just an estimation. - // However, in real-world scenarios this usually results in slight start - // deviations around 0-5ms, so it still makes sense musically. - //endregion - let block_length_in_source_frames = convert_duration_in_frames_to_other_frame_rate( - equipment.audio_request_props.block_length, - equipment.audio_request_props.frame_rate, - equipment.source_frame_rate, - ); - adjust_proportionally_in_blocks( - rel_pos_from_quant_in_source_frames, - equipment.clip_tempo_factor, - block_length_in_source_frames, - ) -} - -/// It can make a difference if we apply a factor once on a large integer x and then round or -/// n times on x/n and round each time. Latter is what happens in practice because we advance -/// frames step by step in n blocks. -fn adjust_proportionally_in_blocks(value: isize, factor: f64, block_length: usize) -> isize { - let abs_value = value.unsigned_abs(); - let block_count = abs_value / block_length; - let remainder = abs_value % block_length; - let adjusted_block_length = adjust_proportionally_positive(block_length as f64, factor); - let adjusted_remainder = adjust_proportionally_positive(remainder as f64, factor); - let total_without_remainder = block_count * adjusted_block_length; - let total = total_without_remainder + adjusted_remainder; - // dbg!(abs_value, adjusted_block_length, block_count, remainder, adjusted_remainder, total_without_remainder, total); - total as isize * value.signum() -} diff --git a/playtime-clip-engine/src/rt/source_util.rs b/playtime-clip-engine/src/rt/source_util.rs deleted file mode 100644 index f2c566e27..000000000 --- a/playtime-clip-engine/src/rt/source_util.rs +++ /dev/null @@ -1,30 +0,0 @@ -use reaper_medium::BorrowedPcmSource; - -pub fn pcm_source_is_midi(src: &BorrowedPcmSource) -> bool { - get_pcm_source_type(src).is_midi() -} - -pub fn get_pcm_source_type(src: &BorrowedPcmSource) -> PcmSourceType { - use PcmSourceType::*; - src.get_type(|t| match t.to_str() { - "MIDI" => NormalMidi, - "MIDIPOOL" => PooledMidi, - "WAVE" => Wave, - _ => Unknown, - }) -} - -#[derive(Copy, Clone, Eq, PartialEq, Debug)] -pub enum PcmSourceType { - NormalMidi, - PooledMidi, - Wave, - Unknown, -} - -impl PcmSourceType { - pub fn is_midi(&self) -> bool { - use PcmSourceType::*; - matches!(self, NormalMidi | PooledMidi) - } -} diff --git a/playtime-clip-engine/src/rt/supplier/amplifier.rs b/playtime-clip-engine/src/rt/supplier/amplifier.rs deleted file mode 100644 index 0b1cd33f4..000000000 --- a/playtime-clip-engine/src/rt/supplier/amplifier.rs +++ /dev/null @@ -1,102 +0,0 @@ -use crate::rt::buffer::AudioBufMut; -use crate::rt::supplier::{ - AudioSupplier, AutoDelegatingPositionTranslationSkill, AutoDelegatingPreBufferSourceSkill, - AutoDelegatingWithMaterialInfo, MidiSupplier, SupplyAudioRequest, SupplyMidiRequest, - SupplyResponse, WithSupplier, -}; - -use helgoboss_midi::{ - RawShortMessage, ShortMessage, ShortMessageFactory, StructuredShortMessage, U7, -}; -use reaper_high::Reaper; -use reaper_medium::{BorrowedMidiEventList, Db, VolumeSliderValue}; -use std::cmp; - -#[derive(Debug)] -pub struct Amplifier { - supplier: S, - volume: Db, - derived_volume_factor: f64, -} - -impl Amplifier { - pub fn new(supplier: S) -> Self { - Self { - supplier, - volume: Db::ZERO_DB, - derived_volume_factor: 1.0, - } - } - - pub fn volume(&self) -> Db { - self.volume - } - - pub fn set_volume(&mut self, volume: Db) { - self.volume = volume; - // TODO-medium Maybe improve the volume factor - self.derived_volume_factor = Reaper::get().medium_reaper().db2slider(volume).get() - / VolumeSliderValue::ZERO_DB.get(); - } -} - -impl WithSupplier for Amplifier { - type Supplier = S; - - fn supplier(&self) -> &Self::Supplier { - &self.supplier - } - - fn supplier_mut(&mut self) -> &mut Self::Supplier { - &mut self.supplier - } -} - -impl AudioSupplier for Amplifier { - fn supply_audio( - &mut self, - request: &SupplyAudioRequest, - dest_buffer: &mut AudioBufMut, - ) -> SupplyResponse { - let response = self.supplier.supply_audio(request, dest_buffer); - if self.volume != Db::ZERO_DB { - // TODO-medium Maybe improve the volume factor - dest_buffer.modify_frames(|sample| sample.value * self.derived_volume_factor); - } - response - } -} - -impl MidiSupplier for Amplifier { - fn supply_midi( - &mut self, - request: &SupplyMidiRequest, - event_list: &mut BorrowedMidiEventList, - ) -> SupplyResponse { - let response = self.supplier.supply_midi(request, event_list); - if self.volume != Db::ZERO_DB { - for event in event_list.iter_mut() { - if let StructuredShortMessage::NoteOn { - channel, - key_number, - velocity, - } = event.message().to_structured() - { - let adjusted_velocity = - (self.derived_volume_factor * velocity.get() as f64).round() as u8; - let amplified_msg = RawShortMessage::note_on( - channel, - key_number, - U7::new(cmp::min(127u8, adjusted_velocity)), - ); - event.set_message(amplified_msg); - } - } - } - response - } -} - -impl AutoDelegatingWithMaterialInfo for Amplifier {} -impl AutoDelegatingPreBufferSourceSkill for Amplifier {} -impl AutoDelegatingPositionTranslationSkill for Amplifier {} diff --git a/playtime-clip-engine/src/rt/supplier/api.rs b/playtime-clip-engine/src/rt/supplier/api.rs deleted file mode 100644 index cea4ae437..000000000 --- a/playtime-clip-engine/src/rt/supplier/api.rs +++ /dev/null @@ -1,516 +0,0 @@ -use crate::conversion_util::convert_duration_in_frames_to_seconds; -use crate::mutex_util::non_blocking_lock; -use crate::rt::buffer::AudioBufMut; -use crate::rt::supplier::get_cycle_at_frame; -use crate::rt::tempo_util::calc_tempo_factor; -use crate::ClipEngineResult; -use reaper_medium::{ - BorrowedMidiEventList, Bpm, DurationInSeconds, Hz, MidiFrameOffset, PositionInSeconds, -}; -use std::fmt::Debug; -use std::path::Path; -use std::sync::{Arc, Mutex}; - -/// We could use just any unit to represent a position within a MIDI source, but we choose frames -/// with regard to the following frame rate. Choosing frames allows us to treat MIDI similar to -/// audio, which results in fewer special cases. The frame rate of 169,344,000 is a multiple of -/// all common sample rates and PPQs. This prevents rounding issues (advice from Justin). -/// Initially I wanted to take 1,024,000 because it is the unit which is used in REAPER's MIDI -/// events, but it's not a multiple of common sample rates and PPQs. -pub const MIDI_FRAME_RATE: Hz = unsafe { Hz::new_unchecked(169_344_000.0) }; - -/// MIDI data is tempo-less. But pretending that all MIDI clips have a fixed tempo allows us to -/// treat MIDI similar to audio. E.g. if we want it to play faster, we just lower the output sample -/// rate. Plus, we can use the same time stretching supplier. Fewer special cases, nice! -pub const MIDI_BASE_BPM: Bpm = unsafe { Bpm::new_unchecked(120.0) }; - -// TODO-medium We can remove the WithMaterialInfo because we don't box anymore. -pub trait AudioSupplier: Debug + WithMaterialInfo { - /// Writes a portion of audio material into the given destination buffer so that it completely - /// fills that buffer. - fn supply_audio( - &mut self, - request: &SupplyAudioRequest, - dest_buffer: &mut AudioBufMut, - ) -> SupplyResponse; -} - -pub trait AutoDelegatingAudioSupplier {} - -impl AudioSupplier for T -where - T: WithSupplier + AutoDelegatingAudioSupplier + Debug + WithMaterialInfo, - S: AudioSupplier, -{ - fn supply_audio( - &mut self, - request: &SupplyAudioRequest, - dest_buffer: &mut AudioBufMut, - ) -> SupplyResponse { - self.supplier_mut().supply_audio(request, dest_buffer) - } -} - -pub trait PreBufferSourceSkill: Debug { - /// Does its best to make sure that the next source block is pre-buffered with the given - /// criteria. - /// - /// It must be asynchronous and cheap enough to call from a real-time thread. - fn pre_buffer(&mut self, request: PreBufferFillRequest); -} - -pub trait AutoDelegatingPreBufferSourceSkill {} - -impl PreBufferSourceSkill for T -where - T: WithSupplier + AutoDelegatingPreBufferSourceSkill + Debug, - S: PreBufferSourceSkill, -{ - fn pre_buffer(&mut self, request: PreBufferFillRequest) { - self.supplier_mut().pre_buffer(request); - } -} - -pub trait PositionTranslationSkill: Debug { - fn translate_play_pos_to_source_pos(&self, play_pos: isize) -> isize; -} - -pub trait AutoDelegatingPositionTranslationSkill {} - -impl PositionTranslationSkill for T -where - T: WithSupplier + AutoDelegatingPositionTranslationSkill + Debug, - S: PositionTranslationSkill, -{ - fn translate_play_pos_to_source_pos(&self, play_pos: isize) -> isize { - self.supplier().translate_play_pos_to_source_pos(play_pos) - } -} - -pub trait MidiSupplier: Debug { - /// Writes a portion of MIDI material into the given destination buffer so that it completely - /// fills that buffer. - fn supply_midi( - &mut self, - request: &SupplyMidiRequest, - event_list: &mut BorrowedMidiEventList, - ) -> SupplyResponse; -} - -pub trait AutoDelegatingMidiSupplier {} - -impl MidiSupplier for T -where - T: WithSupplier + AutoDelegatingMidiSupplier + Debug, - S: MidiSupplier, -{ - fn supply_midi( - &mut self, - request: &SupplyMidiRequest, - event_list: &mut BorrowedMidiEventList, - ) -> SupplyResponse { - self.supplier_mut().supply_midi(request, event_list) - } -} - -pub trait MidiSilencer: Debug { - /// Releases all currently playing notes. - fn release_notes( - &mut self, - frame_offset: MidiFrameOffset, - event_list: &mut BorrowedMidiEventList, - ); -} - -pub trait WithSupplier { - type Supplier; - - fn supplier(&self) -> &Self::Supplier; - fn supplier_mut(&mut self) -> &mut Self::Supplier; -} - -pub trait AutoDelegatingMidiSilencer {} - -impl MidiSilencer for T -where - T: WithSupplier + AutoDelegatingMidiSilencer + Debug, - S: MidiSilencer, -{ - fn release_notes( - &mut self, - frame_offset: MidiFrameOffset, - event_list: &mut BorrowedMidiEventList, - ) { - self.supplier_mut().release_notes(frame_offset, event_list); - } -} - -pub trait CacheableSource: AudioSupplier + WithMaterialInfo + Send { - fn file_name(&self) -> Option<&Path>; - fn duplicate(&self) -> Box; -} - -pub trait WithCacheableSource { - type Source: CacheableSource; - - fn cacheable_source(&self) -> Option<&Self::Source>; -} - -pub trait SupplyRequest { - fn start_frame(&self) -> isize; - fn info(&self) -> &SupplyRequestInfo; - fn general_info(&self) -> &SupplyRequestGeneralInfo; - fn parent_request(&self) -> Option<&Self>; -} - -pub trait WithMaterialInfo { - /// Returns an error if no material available. - fn material_info(&self) -> ClipEngineResult; -} - -pub trait AutoDelegatingWithMaterialInfo {} - -impl WithMaterialInfo for T -where - T: WithSupplier + AutoDelegatingWithMaterialInfo, - S: WithMaterialInfo, -{ - fn material_info(&self) -> ClipEngineResult { - self.supplier().material_info() - } -} - -/// Contains information about the material. -/// -/// "Material" here usually means the inner-most material (the source). However, there's -/// one exception: If a section is defined, that section will change the frame count. -#[derive(Clone, Debug)] -pub enum MaterialInfo { - Audio(AudioMaterialInfo), - Midi(MidiMaterialInfo), -} - -impl MaterialInfo { - pub fn is_midi(&self) -> bool { - matches!(self, MaterialInfo::Midi(_)) - } - - pub fn channel_count(&self) -> usize { - match self { - MaterialInfo::Audio(i) => i.channel_count, - MaterialInfo::Midi(_) => 0, - } - } - - pub fn frame_rate(&self) -> Hz { - match self { - MaterialInfo::Audio(i) => i.frame_rate, - MaterialInfo::Midi(_) => MIDI_FRAME_RATE, - } - } - - pub fn frame_count(&self) -> usize { - match self { - MaterialInfo::Audio(i) => i.frame_count, - MaterialInfo::Midi(i) => i.frame_count, - } - } - - /// Returns the duration assuming native source tempo. - pub fn duration(&self) -> DurationInSeconds { - match self { - MaterialInfo::Audio(i) => i.duration(), - MaterialInfo::Midi(i) => i.duration(), - } - } - - pub fn get_cycle_at_frame(&self, frame: isize) -> usize { - get_cycle_at_frame(frame, self.frame_count()) - } - - pub fn tempo_factor_during_recording(&self, timeline_tempo: Bpm) -> f64 { - if self.is_midi() { - calc_tempo_factor(MIDI_BASE_BPM, timeline_tempo) - } else { - // When recording audio, we have tempo factor 1.0 (original recording tempo). - 1.0 - } - } -} - -#[derive(Clone, Debug)] -pub struct AudioMaterialInfo { - pub channel_count: usize, - pub frame_count: usize, - pub frame_rate: Hz, -} - -impl AudioMaterialInfo { - /// Returns the duration assuming native source tempo. - pub fn duration(&self) -> DurationInSeconds { - convert_duration_in_frames_to_seconds(self.frame_count, self.frame_rate) - } -} - -#[derive(Clone, Debug)] -pub struct MidiMaterialInfo { - // TODO-high I think we should use u64 instead in many places - /// This should be provided normalized to [`MIDI_FRAME_RATE`]! - /// - /// - **NOT** pulses (depends on MIDI clip resolution)! - /// - **NOT** normalized to 1/1_024_000 (as REAPER's [`MidiFrameOffset`]) - pub frame_count: usize, -} - -impl MidiMaterialInfo { - /// Returns the duration assuming native source tempo. - pub fn duration(&self) -> DurationInSeconds { - convert_duration_in_frames_to_seconds(self.frame_count, MIDI_FRAME_RATE) - } -} - -#[derive(Clone, Debug)] -pub struct SupplyAudioRequest<'a> { - /// Position within the most inner material that marks the start of the desired portion. - /// - /// It's important to know that we are talking about the position within the most inner audio - /// supplier (usually the source) because this one provides the continuity that we rely on for - /// smooth tempo changes etc. - /// - /// The frame always relates to the preferred sample rate of the audio supplier, not to - /// `dest_sample_rate`. - pub start_frame: isize, - /// Desired sample rate of the requested material (uses material's native sample rate if not - /// set). - /// - /// The supplier might employ resampling to fulfill this sample rate demand. - pub dest_sample_rate: Option, - /// Just for analysis and debugging purposes. - pub info: SupplyRequestInfo, - pub parent_request: Option<&'a SupplyAudioRequest<'a>>, - pub general_info: &'a SupplyRequestGeneralInfo, -} - -impl<'a> SupplyAudioRequest<'a> { - /// Can be used by code that's built upon the assumption that in/out frame rates equal and - /// therefore number of consumed frames == number of written frames. - /// - /// In our supplier chain, this assumption is for most suppliers true because we don't let the - /// PCM source or our buffers do the resampling itself. The higher-level resample supplier - /// takes care of that. - pub fn assert_wants_source_frame_rate(&self, source_frame_rate: Hz) { - if let Some(dest_sample_rate) = self.dest_sample_rate { - assert_eq!(dest_sample_rate, source_frame_rate); - } - } -} - -impl<'a> SupplyRequest for SupplyAudioRequest<'a> { - fn start_frame(&self) -> isize { - self.start_frame - } - - fn info(&self) -> &SupplyRequestInfo { - &self.info - } - - fn general_info(&self) -> &SupplyRequestGeneralInfo { - self.general_info - } - - fn parent_request(&self) -> Option<&Self> { - self.parent_request - } -} - -#[derive(Clone, Debug, Default)] -pub struct SupplyRequestGeneralInfo { - /// Timeline cursor position of the start of the currently requested audio block. - pub audio_block_timeline_cursor_pos: PositionInSeconds, - /// Audio block length in frames. - pub audio_block_length: usize, - /// The device frame rate. - pub output_frame_rate: Hz, - /// Current tempo on the timeline. - pub timeline_tempo: Bpm, - /// Current tempo factor for the clip. - pub clip_tempo_factor: f64, -} - -/// Only for analysis and debugging, shouldn't influence behavior. -#[derive(Clone, Debug, Default)] -pub struct SupplyRequestInfo { - /// Frame offset within the currently requested audio block - /// - /// At the top of the chain, there's one request per audio block, so this number is usually 0. - /// - /// Some suppliers divide this top request into smaller ones (e.g. the looper when - /// it reaches the start or end of the source within one block). In that case, this number will - /// be greater than 0 for the second request. The number should be accumulated if multiple - /// nested suppliers divide requests. - pub audio_block_frame_offset: usize, - /// A little label identifying which supplier and which sub request in the chain - /// produced/modified this request. - pub requester: &'static str, - /// An optional note. - pub note: &'static str, - pub is_realtime: bool, -} - -#[derive(Clone, Eq, PartialEq, Debug)] -pub struct PreBufferFillRequest { - pub start_frame: isize, -} - -#[derive(Clone, Debug)] -pub struct SupplyMidiRequest<'a> { - /// Position within the most inner material that marks the start of the desired portion. - /// - /// A MIDI frame, that is 1/1024000 of a second. - pub start_frame: isize, - /// Number of requested frames. - pub dest_frame_count: usize, - /// Device sample rate. - pub dest_sample_rate: Hz, - /// Just for analysis and debugging purposes. - pub info: SupplyRequestInfo, - pub parent_request: Option<&'a SupplyMidiRequest<'a>>, - pub general_info: &'a SupplyRequestGeneralInfo, -} - -impl<'a> SupplyRequest for SupplyMidiRequest<'a> { - fn start_frame(&self) -> isize { - self.start_frame - } - - fn info(&self) -> &SupplyRequestInfo { - &self.info - } - - fn general_info(&self) -> &SupplyRequestGeneralInfo { - self.general_info - } - - fn parent_request(&self) -> Option<&Self> { - self.parent_request - } -} - -#[derive(Copy, Clone, Debug, Default)] -pub struct SupplyResponse { - /// The number of frames that were actually consumed from the source. - /// - /// Can be less than requested if the end of the source has been reached. - /// If the start of the source has not been reached yet, still fill it with the ideal - /// amount of consumed frames. - pub num_frames_consumed: usize, - pub status: SupplyResponseStatus, -} - -#[derive(Copy, Clone, Debug)] -pub enum SupplyResponseStatus { - PleaseContinue, - ReachedEnd { - /// The number of frames that were actually written to the destination block. - num_frames_written: usize, - }, -} - -impl Default for SupplyResponseStatus { - fn default() -> Self { - Self::ReachedEnd { - num_frames_written: 0, - } - } -} - -impl SupplyResponseStatus { - pub fn reached_end(&self) -> bool { - matches!(self, SupplyResponseStatus::ReachedEnd { .. }) - } -} - -impl SupplyResponse { - pub fn reached_end(num_frames_consumed: usize, num_frames_written: usize) -> Self { - Self { - num_frames_consumed, - status: SupplyResponseStatus::ReachedEnd { num_frames_written }, - } - } - - pub fn exceeded_end() -> Self { - Self::reached_end(0, 0) - } - - pub fn please_continue(num_frames_consumed: usize) -> Self { - Self { - num_frames_consumed, - status: SupplyResponseStatus::PleaseContinue, - } - } - - pub fn limited_by_total_frame_count( - num_frames_consumed: usize, - num_frames_written: usize, - start_frame: isize, - total_frame_count: usize, - ) -> Self { - let next_frame = start_frame + num_frames_consumed as isize; - Self { - num_frames_consumed, - status: if next_frame < total_frame_count as isize { - SupplyResponseStatus::PleaseContinue - } else { - SupplyResponseStatus::ReachedEnd { num_frames_written } - }, - } - } -} - -impl WithMaterialInfo for Arc> { - fn material_info(&self) -> ClipEngineResult { - non_blocking_lock(self, "material info").material_info() - } -} - -impl AudioSupplier for Arc> { - fn supply_audio( - &mut self, - request: &SupplyAudioRequest, - dest_buffer: &mut AudioBufMut, - ) -> SupplyResponse { - non_blocking_lock(&*self, "supply audio").supply_audio(request, dest_buffer) - } -} - -impl MidiSupplier for Arc> { - fn supply_midi( - &mut self, - request: &SupplyMidiRequest, - event_list: &mut BorrowedMidiEventList, - ) -> SupplyResponse { - non_blocking_lock(&*self, "supply MIDI").supply_midi(request, event_list) - } -} - -impl MidiSilencer for Arc> { - fn release_notes( - &mut self, - frame_offset: MidiFrameOffset, - event_list: &mut BorrowedMidiEventList, - ) { - non_blocking_lock(&*self, "release notes").release_notes(frame_offset, event_list); - } -} - -impl PreBufferSourceSkill for Arc> { - fn pre_buffer(&mut self, request: PreBufferFillRequest) { - non_blocking_lock(&*self, "pre-buffer").pre_buffer(request); - } -} - -impl PositionTranslationSkill for Arc> { - fn translate_play_pos_to_source_pos(&self, play_pos: isize) -> isize { - non_blocking_lock(self, "position translation").translate_play_pos_to_source_pos(play_pos) - } -} diff --git a/playtime-clip-engine/src/rt/supplier/audio_util.rs b/playtime-clip-engine/src/rt/supplier/audio_util.rs deleted file mode 100644 index d8647e88c..000000000 --- a/playtime-clip-engine/src/rt/supplier/audio_util.rs +++ /dev/null @@ -1,127 +0,0 @@ -use crate::conversion_util::adjust_proportionally_positive; -use crate::rt::buffer::{AudioBuf, AudioBufMut}; -use crate::rt::supplier::log_util::print_distance_from_beat_start_at; -use crate::rt::supplier::{SupplyAudioRequest, SupplyResponse, SupplyResponseStatus}; -use reaper_medium::Hz; -use std::cmp; - -/// Helper function for audio suppliers that read from sources and don't want to deal with -/// negative start frames themselves. -pub fn supply_audio_material( - request: &SupplyAudioRequest, - dest_buffer: &mut AudioBufMut, - source_frame_rate: Hz, - supply_inner: impl FnOnce(AudioSourceMaterialRequest) -> SupplyResponse, -) -> SupplyResponse { - #[cfg(debug_assertions)] - { - request.assert_wants_source_frame_rate(source_frame_rate); - } - let ideal_num_consumed_frames = dest_buffer.frame_count(); - let ideal_end_frame = request.start_frame + ideal_num_consumed_frames as isize; - if ideal_end_frame <= 0 { - // Requested portion is located entirely before the actual source material. - // rt_debug!( - // "ideal end frame {} ({})", - // ideal_end_frame, ideal_num_consumed_frames - // ); - dest_buffer.clear(); - // We haven't reached the end of the source, so still tell the caller that we - // wrote all frames. And advance the count-in phase. - return SupplyResponse::please_continue(ideal_num_consumed_frames); - } - // Requested portion contains playable material. - if request.start_frame < 0 { - // Portion overlaps start of material. - // rt_debug!( - // "overlap: start_frame = {}, ideal_end_frame = {}", - // request.start_frame, ideal_end_frame - // ); - let num_skipped_frames_in_source = -request.start_frame as usize; - let proportion_skipped = - num_skipped_frames_in_source as f64 / ideal_num_consumed_frames as f64; - let num_skipped_frames_in_dest = - adjust_proportionally_positive(ideal_num_consumed_frames as f64, proportion_skipped); - // We need to zero the portion of the output buffer that precedes start of material. - dest_buffer.slice_mut(..num_skipped_frames_in_dest).clear(); - if request.info.is_realtime { - print_distance_from_beat_start_at( - request, - num_skipped_frames_in_dest, - "audio, start_frame < 0", - ); - } - let mut shifted_dest_buffer = dest_buffer.slice_mut(num_skipped_frames_in_dest..); - let req = AudioSourceMaterialRequest { - start_frame: 0, - dest_buffer: &mut shifted_dest_buffer, - }; - // rt_debug!( - // "Before source: start = {}, source sr = {}, dest sr = {}", - // req.start_frame, req.source_sample_rate, req.dest_sample_rate - // ); - let res = supply_inner(req); - return SupplyResponse { - num_frames_consumed: num_skipped_frames_in_source + res.num_frames_consumed, - status: match res.status { - SupplyResponseStatus::PleaseContinue => SupplyResponseStatus::PleaseContinue, - SupplyResponseStatus::ReachedEnd { num_frames_written } => { - // Oh, that's short material. - shifted_dest_buffer.slice_mut(num_frames_written..).clear(); - SupplyResponseStatus::ReachedEnd { - num_frames_written: num_skipped_frames_in_dest + num_frames_written, - } - } - }, - }; - } - // Requested portion is located on or after start of the actual source material. - if request.start_frame == 0 && request.info.is_realtime { - print_distance_from_beat_start_at(request, 0, "audio, start_frame == 0"); - } - let req = AudioSourceMaterialRequest { - start_frame: request.start_frame as usize, - dest_buffer, - }; - // rt_debug!( - // "In source: start = {}, source sr = {}, dest sr = {}", - // req.start_frame, req.source_sample_rate, req.dest_sample_rate - // ); - let inner_response = supply_inner(req); - // Because neither resampler nor time stretcher pre-zero buffers, we need to zero - // the unwritten part of the buffer ourselves, otherwise we get some nice saw-like - // tone at the end. This is particularly audible if we have a section that exceeds - // the source end. Then the last buffer content will get played repeatedly. - if let SupplyResponseStatus::ReachedEnd { num_frames_written } = inner_response.status { - dest_buffer.slice_mut(num_frames_written..).clear(); - } - inner_response -} - -pub struct AudioSourceMaterialRequest<'a, 'b> { - pub start_frame: usize, - pub dest_buffer: &'a mut AudioBufMut<'b>, -} - -pub fn transfer_samples_from_buffer( - buf: AudioBuf, - req: AudioSourceMaterialRequest, -) -> SupplyResponse { - let num_remaining_frames_in_source = buf.frame_count() - req.start_frame; - let num_frames_written = cmp::min( - num_remaining_frames_in_source, - req.dest_buffer.frame_count(), - ); - if num_frames_written == 0 { - return SupplyResponse::exceeded_end(); - } - let end_frame = req.start_frame + num_frames_written; - buf.slice(req.start_frame..end_frame) - .copy_to(&mut req.dest_buffer.slice_mut(0..num_frames_written)); - SupplyResponse::limited_by_total_frame_count( - num_frames_written, - num_frames_written, - req.start_frame as isize, - buf.frame_count(), - ) -} diff --git a/playtime-clip-engine/src/rt/supplier/cache.rs b/playtime-clip-engine/src/rt/supplier/cache.rs deleted file mode 100644 index d47cb448b..000000000 --- a/playtime-clip-engine/src/rt/supplier/cache.rs +++ /dev/null @@ -1,271 +0,0 @@ -use std::fmt::Debug; -use std::path::{Path, PathBuf}; - -use crossbeam_channel::{Receiver, Sender}; -use playtime_api::persistence::AudioCacheBehavior; - -use crate::rt::buffer::{AudioBufMut, OwnedAudioBuffer}; -use crate::rt::supplier::audio_util::{supply_audio_material, transfer_samples_from_buffer}; -use crate::rt::supplier::{ - AudioMaterialInfo, AudioSupplier, AutoDelegatingMidiSupplier, - AutoDelegatingPositionTranslationSkill, CacheableSource, MaterialInfo, SupplyAudioRequest, - SupplyRequestInfo, SupplyResponse, WithCacheableSource, WithMaterialInfo, WithSupplier, -}; -use crate::ClipEngineResult; - -#[derive(Debug)] -pub struct Cache { - cached_data: Option, - request_sender: Sender, - response_channel: CacheResponseChannel, - supplier: S, -} - -#[derive(Debug)] -pub struct CacheResponseChannel { - sender: Sender, - receiver: Receiver, -} - -impl Default for CacheResponseChannel { - fn default() -> Self { - Self::new() - } -} - -impl CacheResponseChannel { - pub fn new() -> Self { - let (sender, receiver) = crossbeam_channel::bounded(10); - Self { sender, receiver } - } -} - -#[derive(Debug)] -pub enum CacheRequest { - CacheSource(CacheSourceRequest), - DiscardCachedData(CachedData), -} - -#[derive(Debug)] -pub struct CacheSourceRequest { - audio_material_info: AudioMaterialInfo, - source_file: PathBuf, - source: Box, - response_sender: Sender, -} - -#[derive(Debug)] -pub enum CacheResponse { - CachedSource(CachedData), -} - -#[derive(Debug)] -pub struct CachedData { - audio_material_info: AudioMaterialInfo, - source_file: PathBuf, - content: OwnedAudioBuffer, -} - -impl CachedData { - fn is_still_valid(&self, source_file: &Path) -> bool { - self.source_file == source_file - } -} - -impl WithSupplier for Cache { - type Supplier = S; - - fn supplier(&self) -> &Self::Supplier { - &self.supplier - } - - fn supplier_mut(&mut self) -> &mut Self::Supplier { - &mut self.supplier - } -} - -impl Cache { - pub fn new(supplier: S, request_sender: Sender) -> Self { - Self { - cached_data: None, - request_sender, - response_channel: CacheResponseChannel::new(), - supplier, - } - } - - pub fn set_audio_cache_behavior( - &mut self, - cache_behavior: AudioCacheBehavior, - ) -> ClipEngineResult<()> { - use AudioCacheBehavior::*; - let cache_enabled = match cache_behavior { - DirectFromDisk => false, - CacheInMemory => true, - }; - if cache_enabled { - self.enable()?; - } else { - self.disable(); - } - Ok(()) - } - - /// If not cached already, triggers building the cache asynchronously, caching all supplied - /// audio data in memory. - /// - /// Don't call in real-time thread. If this is necessary one day, no problem: Clone the source - /// in advance. - fn enable(&mut self) -> ClipEngineResult<()> { - // A cacheable source might not be available at all times. - let source = self - .supplier - .cacheable_source() - .ok_or("no cacheable source available")?; - let material_info = source.material_info()?; - // Reject MIDI - let MaterialInfo::Audio(audio_material_info) = material_info else { - return Err("source can't be cached because it's MIDI"); - }; - // If source has no file name, we can't check if the cache is still valid. - let source_file = source - .file_name() - .ok_or("source doesn't have any file name")?; - // Look at currently cached data - if let Some(cached_data) = self.cached_data.take() { - if cached_data.is_still_valid(source_file) { - self.cached_data = Some(cached_data); - return Ok(()); - } - self.request_sender.discard_cached_data(cached_data); - } - // Cache - let req = CacheSourceRequest { - audio_material_info, - source_file: source_file.to_path_buf(), - source: source.duplicate(), - response_sender: self.response_channel.sender.clone(), - }; - self.request_sender.cache_source(req); - Ok(()) - } - - /// Disables the cache and clears it, releasing the consumed memory. - fn disable(&mut self) { - if let Some(cached_data) = self.cached_data.take() { - self.request_sender.discard_cached_data(cached_data); - } - } - - fn process_worker_response(&mut self) { - let response = match self.response_channel.receiver.try_recv() { - Ok(r) => r, - Err(_) => return, - }; - match response { - CacheResponse::CachedSource(cache_data) => { - debug!("Cached audio material completely in memory"); - self.cached_data = Some(cache_data); - } - } - } -} - -pub fn keep_processing_cache_requests(receiver: Receiver) { - while let Ok(request) = receiver.recv() { - use CacheRequest::*; - match request { - CacheSource(req) => { - let _ = cache_source(req); - } - DiscardCachedData(_) => {} - } - } -} - -fn cache_source(mut req: CacheSourceRequest) -> ClipEngineResult<()> { - let mut content = OwnedAudioBuffer::new( - req.audio_material_info.channel_count, - req.audio_material_info.frame_count, - ); - let request = SupplyAudioRequest { - start_frame: 0, - dest_sample_rate: None, - info: SupplyRequestInfo { - audio_block_frame_offset: 0, - requester: "cache", - note: "", - is_realtime: false, - }, - parent_request: None, - general_info: &Default::default(), - }; - req.source.supply_audio(&request, &mut content.to_buf_mut()); - let cached_data = CachedData { - audio_material_info: req.audio_material_info, - source_file: req.source_file, - content, - }; - req.response_sender - .try_send(CacheResponse::CachedSource(cached_data)) - .map_err(|_| "clip not interested in cached data anymore")?; - Ok(()) -} - -trait CacheRequestSender { - fn cache_source(&self, req: CacheSourceRequest); - - fn discard_cached_data(&self, data: CachedData); - - fn send_request(&self, request: CacheRequest); -} - -impl CacheRequestSender for Sender { - fn cache_source(&self, req: CacheSourceRequest) { - let request = CacheRequest::CacheSource(req); - self.send_request(request); - } - - fn discard_cached_data(&self, data: CachedData) { - let request = CacheRequest::DiscardCachedData(data); - self.send_request(request); - } - - fn send_request(&self, request: CacheRequest) { - self.try_send(request).unwrap(); - } -} - -impl AudioSupplier for Cache { - fn supply_audio( - &mut self, - request: &SupplyAudioRequest, - dest_buffer: &mut AudioBufMut, - ) -> SupplyResponse { - self.process_worker_response(); - let d = match &self.cached_data { - None => return self.supplier.supply_audio(request, dest_buffer), - Some(d) => d, - }; - let buf = d.content.to_buf(); - supply_audio_material( - request, - dest_buffer, - d.audio_material_info.frame_rate, - |input| transfer_samples_from_buffer(buf, input), - ) - } -} - -impl WithMaterialInfo for Cache { - fn material_info(&self) -> ClipEngineResult { - if let Some(d) = &self.cached_data { - Ok(MaterialInfo::Audio(d.audio_material_info.clone())) - } else { - self.supplier.material_info() - } - } -} - -impl AutoDelegatingMidiSupplier for Cache {} -impl AutoDelegatingPositionTranslationSkill for Cache {} diff --git a/playtime-clip-engine/src/rt/supplier/chain.rs b/playtime-clip-engine/src/rt/supplier/chain.rs deleted file mode 100644 index 8ad5ac20c..000000000 --- a/playtime-clip-engine/src/rt/supplier/chain.rs +++ /dev/null @@ -1,706 +0,0 @@ -use crate::mutex_util::non_blocking_lock; -use crate::rt::supplier::{ - Amplifier, AutoDelegatingAudioSupplier, AutoDelegatingMidiSupplier, - AutoDelegatingPositionTranslationSkill, AutoDelegatingPreBufferSourceSkill, - AutoDelegatingWithMaterialInfo, Cache, CacheRequest, CommandProcessor, Downbeat, - InteractionHandler, LoopBehavior, Looper, MaterialInfo, MidiNoteTracker, MidiOverdubOutcome, - MidiOverdubSettings, MidiSequence, PollRecordingOutcome, PositionTranslationSkill, PreBuffer, - PreBufferCacheMissBehavior, PreBufferFillRequest, PreBufferOptions, PreBufferRequest, - PreBufferSourceSkill, RecordState, Recorder, RecordingArgs, Resampler, RtClipSource, Section, - SectionBounds, StartEndHandler, StopRecordingOutcome, TimeStretcher, WithMaterialInfo, - WithSupplier, WriteAudioRequest, WriteMidiRequest, -}; -use crate::rt::tempo_util::determine_tempo_from_beat_time_base; -use crate::rt::BasicAudioRequestProps; -use crate::{ClipEngineResult, HybridTimeline}; -use crossbeam_channel::Sender; -use playtime_api::persistence as api; -use playtime_api::persistence::{ - AudioCacheBehavior, AudioTimeStretchMode, ClipTimeBase, Db, MidiResetMessageRange, - PositiveBeat, PositiveSecond, VirtualResampleMode, -}; -use reaper_medium::{Bpm, PositionInSeconds}; -use std::sync::{Arc, Mutex, MutexGuard}; - -/// The head of the supplier chain (just an alias). -type Head = AmplifierTail; - -/// Responsible for changing the volume. -/// -/// It sits on top of everything because volume changes are fast and shouldn't be cached because -/// they can happen very suddenly (e.g. in response to different velocity values). -type AmplifierTail = Amplifier; - -/// Resampler takes care of converting between the requested destination (= output) frame rate -/// and the frame rate of the inner material. It's also responsible for changing the tempo of MIDI -/// material and optionally even audio material (VariSpeed = not preserving pitch). -/// -/// We have the resampler on top of the interaction handler because at the moment the interaction -/// handler logic is based on the assumption that input frame rate == output frame rate. If we want -/// to put the interaction handler above the resampler one day (e.g. for caching reasons), we first -/// must change the logic accordingly (doing some frame rate conversions). -/// -/// At the moment, I think resampling results don't need to be pre-buffered. However, as soon as -/// we decide that they do, the interaction handler should definitely move above the resampler. -type ResamplerTail = Resampler; - -/// Interaction handler handles sudden interactions, introducing proper fades and reset messages. -/// -/// It sits on top of almost everything because it's fast and shouldn't be cached (because -/// interactions are by definition very sudden events). -type InteractionHandlerTail = InteractionHandler; - -/// Time stretcher is responsible for stretching audio material while preserving its pitch. -/// -/// It sits on top of the (downbeat-shifted) looper (not deeper) because it greedily grabs -/// material from its supplier - a kind of internal look-ahead/pre-buffering. If it would reach the -/// end of material and we want to start the next loop cycle, it wouldn't have material ready and -/// would need to start pre-buffering from scratch. Not good. -/// -/// It sits above the pre-buffer at the moment although time-stretching results also should be -/// pre-buffered (because time stretching is slow). Pre-buffering time-stretching results probably -/// has some special needs that we don't handle yet. Let's see. -type TimeStretcherTail = TimeStretcher; - -/// Downbeat handler sits on top of the looper because it moves the complete loop to the left -/// (it helps imagining the material as items in the arrangement view). -/// -/// It even sits on top of the pre-buffer because it just moves the material, which is easy to -/// express by cheaply converting pre-buffer requests. In general, we don't want to pre-buffer -/// more non-destructive changes than necessary, especially not the sudden changes (because that -/// would introduce a latency). -/// -/// Also, the pre-buffer can apply an optimization if it can be sure that there's no material -/// in the count-in phase, which is true for all material below the downbeat handler. -type DownbeatTail = Downbeat; - -/// Pre-buffer asynchronously loads a small amount of audio source material into memory before it's -/// being played. That's important because the inner-most source usually reads audio material -/// directly from disk and disk access can be slow. -/// -/// It sits above the looper (not just above the inner-most source), because it needs to grab -/// material in advance. The looper knows best which material comes next. If it would sit below -/// the looper and it would reach end of material, it doesn't have anything in hand to decide what -/// needs to be pre-buffered next. -type PreBufferTail = - PreBuffer; - -/// Everything below the pre-buffer is shared because we must access it from the pre-buffer worker -/// thread as well. We make sure of having no contention when locking the mutex. -type SharedLooperTail = Arc>; - -/// Looper optionally repeats the material. -/// -/// It sits above the section because the section needs to be looped, not the full source. -type LooperTail = Looper; - -/// Section handler optionally plays just a certain portion of the material. It can also be used to -/// add silence after end of material. -/// -/// It sits above the start-end handler because it has its own non-optional section -/// start-end handling. It could probably also sit below the start-end handler and the start-end -/// handler could be configured to handle section start-end as well, but at the moment it's fine as -/// it is. -type SectionTail = Section; - -/// Start-end handler introduces fades and reset messages to "fix" the source material and make it -/// ready for being looped. -/// -/// It sits on top of the recorder (representing the inner-most source) because it's -/// optional and intended to really affect only the inner-most source, not the section or loop -/// (which have their own start-end handling). -type StartEndHandlerTail = StartEndHandler; - -/// The MIDI note tracker provides a method for silencing currently playing MIDI. -type MidiNoteTrackerTail = MidiNoteTracker; - -/// Cache handler optionally caches the complete original source material in memory. -/// -/// It sits on top of recorder so that recorder doesn't have to deal with swapping caches (it has -/// to do enough already). -/// -/// It sits below the other suppliers because if we cache a big chunk in memory, we want to be sure -/// we can reuse it in lots of different ways. -type CacheTail = Cache; - -/// Recorder takes care of recording and swapping sources. -/// -/// When it comes to playing (not recording), it basically represents the source = the inner-most -/// material. -/// -/// It's hard-coded to sit on top of clip source because it's responsible for swapping an old -/// source with a newly recorded source. -type RecorderTail = Recorder; - -#[derive(Debug)] -pub struct SupplierChain { - head: Head, -} - -impl SupplierChain { - pub fn new(recorder: Recorder, equipment: ChainEquipment) -> ClipEngineResult { - let pre_buffer_options = PreBufferOptions { - // We know we sit below the downbeat handler, so the underlying suppliers won't deliver - // material in the count-in phase. - skip_count_in_phase_material: true, - cache_miss_behavior: PreBufferCacheMissBehavior::OutputSilence, - recalibrate_on_cache_miss: false, - }; - let mut looper = Looper::new(Section::new(StartEndHandler::new(MidiNoteTracker::new( - Cache::new(recorder, equipment.cache_request_sender), - )))); - looper.set_enabled(true); - let mut chain = Self { - head: { - Amplifier::new(Resampler::new(InteractionHandler::new(TimeStretcher::new( - Downbeat::new(PreBuffer::new( - Arc::new(Mutex::new(looper)), - equipment.pre_buffer_request_sender, - pre_buffer_options, - ChainPreBufferCommandProcessor, - )), - )))) - }, - }; - // Configure resampler - let resampler = chain.resampler_mut(); - resampler.set_enabled(true); - // Configure time stretcher - let time_stretcher = chain.time_stretcher_mut(); - time_stretcher.set_enabled(true); - // Configure downbeat - let downbeat = chain.downbeat_mut(); - downbeat.set_enabled(true); - // Configure pre-buffer - let pre_buffer = chain.pre_buffer_mut(); - pre_buffer.set_enabled(true); - Ok(chain) - } - - /// Not suitable for applying while playing (e.g. because no special handling for looped). - /// Use `RtClip::set_settings` instead. - pub fn configure_complete_chain(&mut self, settings: ChainSettings) -> ClipEngineResult<()> { - let material_info = self.material_info()?; - self.set_looped(settings.looped); - self.set_time_base(&settings.time_base, &material_info); - self.set_volume(settings.volume); - self.set_section(settings.section.start_pos, settings.section.length); - self.set_audio_fades_enabled_for_source(settings.audio_apply_source_fades); - self.set_audio_time_stretch_mode(settings.audio_time_stretch_mode); - self.set_audio_resample_mode(settings.audio_resample_mode); - self.set_audio_cache_behavior(settings.cache_behavior); - self.set_midi_settings(settings.midi_settings); - Ok(()) - } - - pub fn emit_audio_recording_task(&self) { - // When recording, there's no contention. - self.pre_buffer_wormhole() - .recorder() - .emit_audio_recording_task() - } - - pub fn pre_buffer_simple(&mut self, next_expected_pos: isize) { - if self.material_info().map(|i| i.is_midi()).unwrap_or(false) { - // MIDI doesn't need pre-buffering - return; - } - let req = PreBufferFillRequest { - start_frame: next_expected_pos, - }; - self.pre_buffer(req); - } - - pub fn set_time_base(&mut self, time_base: &ClipTimeBase, material_info: &MaterialInfo) { - match time_base { - ClipTimeBase::Time => { - debug!("Disable tempo adjustments"); - self.time_stretcher_mut().set_active(false); - self.resampler_mut().set_tempo_adjustments_enabled(false); - self.clear_downbeat(); - } - ClipTimeBase::Beat(b) => { - debug!("Enable tempo adjustments"); - self.time_stretcher_mut().set_active(true); - self.resampler_mut().set_tempo_adjustments_enabled(true); - let tempo = determine_tempo_from_beat_time_base(b, material_info.is_midi()); - self.set_downbeat_in_beats(b.downbeat, tempo, material_info); - } - } - } - - pub fn is_playing_already(&self, pos: isize) -> bool { - let downbeat_correct_pos = pos + self.downbeat().downbeat_frame() as isize; - downbeat_correct_pos >= 0 - } - - fn clear_downbeat(&mut self) { - self.downbeat_mut().set_downbeat_frame(0); - } - - pub fn with_source_mut( - &mut self, - f: impl FnOnce(&mut RtClipSource) -> R, - ) -> ClipEngineResult { - let mut wormhole = self.pre_buffer_wormhole(); - let source = wormhole.recorder().source_mut()?; - Ok(f(source)) - } - - pub fn swap_source(&mut self, source: &mut RtClipSource) -> ClipEngineResult<()> { - self.pre_buffer_wormhole().recorder().swap_source(source) - } - - pub fn set_audio_fades_enabled_for_source(&mut self, enabled: bool) { - let command = ChainPreBufferCommand::SetAudioFadesEnabledForSource(enabled); - self.pre_buffer_supplier().send_command(command); - } - - pub fn set_midi_settings(&mut self, settings: api::ClipMidiSettings) { - self.set_midi_reset_msg_range_for_interaction(settings.interaction_reset_settings); - self.set_midi_reset_msg_range_for_source(settings.source_reset_settings); - self.set_midi_reset_msg_range_for_section(settings.section_reset_settings); - self.set_midi_reset_msg_range_for_loop(settings.loop_reset_settings); - } - - fn set_midi_reset_msg_range_for_section(&mut self, range: MidiResetMessageRange) { - let command = ChainPreBufferCommand::SetMidiResetMsgRangeForSection(range); - self.pre_buffer_supplier().send_command(command); - } - - fn set_midi_reset_msg_range_for_interaction(&mut self, range: MidiResetMessageRange) { - self.interaction_handler_mut() - .set_midi_reset_msg_range(range); - } - - fn set_midi_reset_msg_range_for_loop(&mut self, range: MidiResetMessageRange) { - let command = ChainPreBufferCommand::SetMidiResetMsgRangeForLoop(range); - self.pre_buffer_supplier().send_command(command); - } - - fn set_midi_reset_msg_range_for_source(&mut self, range: MidiResetMessageRange) { - let command = ChainPreBufferCommand::SetMidiResetMsgRangeForSource(range); - self.pre_buffer_supplier().send_command(command); - } - - pub fn set_volume(&mut self, volume: Db) { - self.amplifier_mut() - .set_volume(reaper_medium::Db::new(volume.get())); - } - - fn set_downbeat_in_beats( - &mut self, - beat: PositiveBeat, - tempo: Bpm, - material_info: &MaterialInfo, - ) { - self.downbeat_mut() - .set_downbeat_in_beats(beat, tempo, material_info); - } - - pub fn set_audio_resample_mode(&mut self, mode: VirtualResampleMode) { - self.resampler_mut().set_mode(mode); - } - - pub fn recording_material_info(&self) -> ClipEngineResult { - // With MIDI, there's no contention. - self.pre_buffer_wormhole() - .recorder() - .recording_material_info() - } - - pub fn start_midi_overdub( - &mut self, - source_replacment: Option, - settings: MidiOverdubSettings, - ) { - // With MIDI, there's no contention. - self.pre_buffer_wormhole() - .recorder() - .start_midi_overdub(source_replacment, settings) - .unwrap(); - } - - /// If we are in MIDI overdub mode, the play position parameter must be set. - pub fn write_midi( - &mut self, - request: WriteMidiRequest, - play_pos: Option, - ) -> ClipEngineResult<()> { - // When recording, there's no contention. - let translated_play_pos = match play_pos { - None => None, - Some(play_pos) => { - let translated = self.translate_play_pos_to_source_pos(play_pos); - if translated < 0 { - return Err("translated play position is not within source bounds"); - } - Some(translated as usize) - } - }; - self.pre_buffer_wormhole() - .recorder() - .write_midi(request, translated_play_pos) - } - - pub fn write_audio(&mut self, request: impl WriteAudioRequest) { - // When recording, there's no contention. - self.pre_buffer_wormhole() - .recorder() - .write_audio(request) - .unwrap(); - } - - pub fn record_state(&self) -> Option { - self.pre_buffer_wormhole().recorder().record_state() - } - - pub fn poll_recording( - &mut self, - audio_request_props: BasicAudioRequestProps, - ) -> PollRecordingOutcome { - self.pre_buffer_wormhole() - .recorder() - .poll_recording(audio_request_props) - } - - pub fn stop_midi_overdubbing(&mut self) -> ClipEngineResult { - self.pre_buffer_wormhole().recorder().stop_midi_overdub() - } - - pub fn stop_recording( - &mut self, - timeline: &HybridTimeline, - timeline_cursor_pos: PositionInSeconds, - audio_request_props: BasicAudioRequestProps, - ) -> ClipEngineResult { - self.pre_buffer_wormhole().recorder().stop_recording( - timeline, - timeline_cursor_pos, - audio_request_props, - ) - } - - pub fn prepare_recording(&mut self, args: RecordingArgs) { - // When recording, there's no contention. - self.pre_buffer_wormhole() - .recorder() - .prepare_recording(args) - .unwrap(); - } - - pub fn set_audio_cache_behavior(&mut self, cache_behavior: AudioCacheBehavior) { - use AudioCacheBehavior::*; - let pre_buffer_enabled = match &cache_behavior { - DirectFromDisk => true, - CacheInMemory => false, - }; - let command = ChainPreBufferCommand::SetAudioCacheBehavior(cache_behavior); - self.pre_buffer_supplier().send_command(command); - // Enable/disable pre-buffer accordingly (pre-buffering not necessary if we have the - // complete source material in memory already). - let pre_buffer = self.pre_buffer_mut(); - if pre_buffer_enabled { - let _ = pre_buffer.activate(); - } else { - pre_buffer.deactivate(); - } - } - - pub fn set_audio_time_stretch_mode(&mut self, mode: AudioTimeStretchMode) { - use AudioTimeStretchMode::*; - let use_vari_speed = match mode { - VariSpeed => true, - KeepingPitch(m) => { - self.time_stretcher_mut().set_mode(m.mode); - false - } - }; - self.resampler_mut() - .set_responsible_for_audio_tempo_adjustments(use_vari_speed); - self.time_stretcher_mut() - .set_responsible_for_audio_time_stretching(!use_vari_speed); - } - - pub fn set_looped(&mut self, looped: bool) { - let command = ChainPreBufferCommand::SetLooped(looped); - self.pre_buffer_supplier().send_command(command); - } - - pub fn set_tempo_factor(&mut self, tempo_factor: f64) { - self.resampler_mut().set_tempo_factor(tempo_factor); - self.time_stretcher_mut().set_tempo_factor(tempo_factor); - } - - pub fn install_immediate_start_interaction(&mut self, current_frame: isize) { - self.interaction_handler_mut() - .start_immediately(current_frame) - .unwrap(); - } - - pub fn stop_interaction_is_installed_already(&self) -> bool { - self.interaction_handler().has_stop_interaction() - } - - pub fn install_immediate_stop_interaction(&mut self, current_frame: isize) { - self.interaction_handler_mut() - .stop_immediately(current_frame) - .unwrap(); - } - - pub fn schedule_stop_interaction_at(&mut self, frame: isize) { - self.interaction_handler_mut().schedule_stop_at(frame); - } - - pub fn reset_interactions(&mut self) { - self.interaction_handler_mut().reset(); - } - - pub fn reset_for_play(&mut self, looped: bool) { - self.interaction_handler_mut().reset(); - self.resampler_mut().reset_buffers_and_latency(); - self.time_stretcher_mut().reset_buffers_and_latency(); - self.set_looped(looped); - } - - pub fn keep_playing_until_end_of_current_cycle(&mut self, pos: isize) { - let command = ChainPreBufferCommand::KeepPlayingUntilEndOfCurrentCycle { pos }; - self.pre_buffer_supplier().send_command(command); - } - - pub fn set_section(&mut self, start: PositiveSecond, length: Option) { - let command = ChainPreBufferCommand::SetSectionBoundsInSeconds { start, length }; - self.pre_buffer_supplier().send_command(command); - } - - fn amplifier(&self) -> &AmplifierTail { - &self.head - } - - fn amplifier_mut(&mut self) -> &mut AmplifierTail { - &mut self.head - } - - fn interaction_handler(&self) -> &InteractionHandlerTail { - self.resampler().supplier() - } - - fn interaction_handler_mut(&mut self) -> &mut InteractionHandlerTail { - self.resampler_mut().supplier_mut() - } - - fn resampler(&self) -> &ResamplerTail { - self.amplifier().supplier() - } - - fn resampler_mut(&mut self) -> &mut ResamplerTail { - self.amplifier_mut().supplier_mut() - } - - fn time_stretcher(&self) -> &TimeStretcherTail { - self.interaction_handler().supplier() - } - - fn time_stretcher_mut(&mut self) -> &mut TimeStretcherTail { - self.interaction_handler_mut().supplier_mut() - } - - fn downbeat(&self) -> &DownbeatTail { - self.time_stretcher().supplier() - } - - fn downbeat_mut(&mut self) -> &mut DownbeatTail { - self.time_stretcher_mut().supplier_mut() - } - - fn pre_buffer_supplier(&self) -> &PreBufferTail { - self.downbeat().supplier() - } - - fn pre_buffer_mut(&mut self) -> &mut PreBufferTail { - self.downbeat_mut().supplier_mut() - } - - /// Allows accessing the suppliers below the pre-buffer. - /// - /// Attention: This attempts to lock a mutex and panics if it's locked already. Therefore it can - /// be used only if one is sure that there can't be any contention! - /// - /// # Panics - /// - /// This method panics if the mutex is locked! - fn pre_buffer_wormhole(&self) -> MutexGuard { - non_blocking_lock( - self.pre_buffer_supplier().supplier(), - "attempt to access pre-buffer wormhole from chain while locked", - ) - } -} - -trait Entrance { - fn looper(&mut self) -> &mut LooperTail; - - fn section(&mut self) -> &mut SectionTail; - - fn start_end_handler(&mut self) -> &mut StartEndHandlerTail; - - fn midi_note_tracker(&mut self) -> &mut MidiNoteTrackerTail; - - fn cache(&mut self) -> &mut CacheTail; - - fn recorder(&mut self) -> &mut RecorderTail; -} - -impl<'a> Entrance for MutexGuard<'a, LooperTail> { - fn looper(&mut self) -> &mut LooperTail { - self - } - - fn section(&mut self) -> &mut SectionTail { - self.supplier_mut() - } - - fn start_end_handler(&mut self) -> &mut StartEndHandlerTail { - self.section().supplier_mut() - } - - fn midi_note_tracker(&mut self) -> &mut MidiNoteTrackerTail { - self.start_end_handler().supplier_mut() - } - - fn cache(&mut self) -> &mut CacheTail { - self.midi_note_tracker().supplier_mut() - } - - fn recorder(&mut self) -> &mut RecorderTail { - self.cache().supplier_mut() - } -} - -impl WithSupplier for SupplierChain { - type Supplier = Head; - - fn supplier(&self) -> &Self::Supplier { - &self.head - } - - fn supplier_mut(&mut self) -> &mut Self::Supplier { - &mut self.head - } -} - -impl AutoDelegatingAudioSupplier for SupplierChain {} -impl AutoDelegatingMidiSupplier for SupplierChain {} -impl AutoDelegatingWithMaterialInfo for SupplierChain {} -impl AutoDelegatingPreBufferSourceSkill for SupplierChain {} -impl AutoDelegatingPositionTranslationSkill for SupplierChain {} - -pub type ChainPreBufferRequest = PreBufferRequest; - -#[derive(Debug)] -pub enum ChainPreBufferCommand { - SetAudioFadesEnabledForSource(bool), - SetMidiResetMsgRangeForSection(MidiResetMessageRange), - SetMidiResetMsgRangeForLoop(MidiResetMessageRange), - SetMidiResetMsgRangeForSource(MidiResetMessageRange), - SetAudioCacheBehavior(AudioCacheBehavior), - SetLooped(bool), - KeepPlayingUntilEndOfCurrentCycle { - pos: isize, - }, - SetSectionBoundsInSeconds { - start: PositiveSecond, - length: Option, - }, -} - -#[derive(Debug)] -pub struct ChainPreBufferCommandProcessor; - -impl CommandProcessor for ChainPreBufferCommandProcessor { - type Supplier = SharedLooperTail; - type Command = ChainPreBufferCommand; - - fn process_command(&self, command: ChainPreBufferCommand, supplier: &SharedLooperTail) { - let mut entrance = non_blocking_lock(supplier, "command processing"); - use ChainPreBufferCommand::*; - match command { - SetAudioFadesEnabledForSource(enabled) => { - entrance - .start_end_handler() - .set_audio_fades_enabled(enabled); - } - SetMidiResetMsgRangeForSection(range) => { - entrance.section().set_midi_reset_msg_range(range); - } - SetMidiResetMsgRangeForLoop(range) => { - entrance.looper().set_midi_reset_msg_range(range); - } - SetMidiResetMsgRangeForSource(range) => { - entrance.start_end_handler().set_midi_reset_msg_range(range); - } - SetAudioCacheBehavior(behavior) => { - let _ = entrance.cache().set_audio_cache_behavior(behavior); - } - SetLooped(looped) => entrance - .looper() - .set_loop_behavior(LoopBehavior::from_bool(looped)), - KeepPlayingUntilEndOfCurrentCycle { pos } => { - entrance - .looper() - .keep_playing_until_end_of_current_cycle(pos) - .unwrap(); - } - SetSectionBoundsInSeconds { start, length } => { - let source_material_info = entrance.recorder().material_info().unwrap(); - let section_handler = entrance.section(); - section_handler - .set_bounds_in_seconds(start, length, &source_material_info) - .unwrap(); - let bounds = section_handler.bounds(); - configure_start_end_handler_on_section_change( - entrance.start_end_handler(), - bounds, - source_material_info.frame_count(), - ); - } - } - } -} - -fn configure_start_end_handler_on_section_change( - start_end_handler: &mut StartEndHandlerTail, - bounds: SectionBounds, - source_length: usize, -) { - // Let the section handle the start fade if appropriate (in order to not have overlaps). - start_end_handler.set_enabled_for_start(bounds.start_frame() == 0); - // However, it's important that we still have an end fade if the section exceeds the source! - // Otherwise we get a nice click. - let enabled_for_end = if let Some(l) = bounds.length() { - let end_in_source = bounds.start_frame() + l; - end_in_source > source_length - } else { - true - }; - start_end_handler.set_enabled_for_end(enabled_for_end); -} - -#[derive(Clone, Debug)] -pub struct ChainEquipment { - pub pre_buffer_request_sender: Sender, - pub cache_request_sender: Sender, -} -/// Everything necessary to configure the clip supply chain. -#[derive(Copy, Clone, Debug)] -pub struct ChainSettings { - pub time_base: api::ClipTimeBase, - pub midi_settings: api::ClipMidiSettings, - pub looped: bool, - pub volume: api::Db, - pub section: api::Section, - pub audio_apply_source_fades: bool, - pub audio_time_stretch_mode: AudioTimeStretchMode, - pub audio_resample_mode: VirtualResampleMode, - pub cache_behavior: AudioCacheBehavior, -} diff --git a/playtime-clip-engine/src/rt/supplier/clip_source.rs b/playtime-clip-engine/src/rt/supplier/clip_source.rs deleted file mode 100644 index abd9a51cf..000000000 --- a/playtime-clip-engine/src/rt/supplier/clip_source.rs +++ /dev/null @@ -1,49 +0,0 @@ -use crate::rt::supplier::midi_sequence::MidiSequence; -use crate::rt::supplier::{ - AudioSupplier, MaterialInfo, MidiSupplier, ReaperClipSource, SupplyAudioRequest, - SupplyMidiRequest, SupplyResponse, WithMaterialInfo, -}; -use crate::rt::AudioBufMut; -use crate::ClipEngineResult; -use reaper_medium::BorrowedMidiEventList; - -#[derive(Clone, Debug)] -pub enum RtClipSource { - Reaper(ReaperClipSource), - Midi(MidiSequence), -} - -impl AudioSupplier for RtClipSource { - fn supply_audio( - &mut self, - request: &SupplyAudioRequest, - dest_buffer: &mut AudioBufMut, - ) -> SupplyResponse { - match self { - RtClipSource::Reaper(s) => s.supply_audio(request, dest_buffer), - RtClipSource::Midi(_) => SupplyResponse::default(), - } - } -} - -impl MidiSupplier for RtClipSource { - fn supply_midi( - &mut self, - request: &SupplyMidiRequest, - event_list: &mut BorrowedMidiEventList, - ) -> SupplyResponse { - match self { - RtClipSource::Reaper(s) => s.supply_midi(request, event_list), - RtClipSource::Midi(s) => s.supply_midi(request, event_list), - } - } -} - -impl WithMaterialInfo for RtClipSource { - fn material_info(&self) -> ClipEngineResult { - match self { - RtClipSource::Reaper(s) => s.material_info(), - RtClipSource::Midi(s) => s.material_info(), - } - } -} diff --git a/playtime-clip-engine/src/rt/supplier/downbeat.rs b/playtime-clip-engine/src/rt/supplier/downbeat.rs deleted file mode 100644 index 30731c0ac..000000000 --- a/playtime-clip-engine/src/rt/supplier/downbeat.rs +++ /dev/null @@ -1,166 +0,0 @@ -use crate::conversion_util::convert_duration_in_seconds_to_frames; -use crate::rt::buffer::AudioBufMut; -use crate::rt::supplier::{ - AudioSupplier, AutoDelegatingMidiSilencer, AutoDelegatingWithMaterialInfo, MaterialInfo, - MidiSupplier, PositionTranslationSkill, PreBufferFillRequest, PreBufferSourceSkill, - SupplyAudioRequest, SupplyMidiRequest, SupplyRequest, SupplyRequestInfo, SupplyResponse, - WithMaterialInfo, WithSupplier, -}; - -use playtime_api::persistence::PositiveBeat; -use reaper_medium::{BorrowedMidiEventList, Bpm, DurationInSeconds}; - -#[derive(Debug)] -pub struct Downbeat { - supplier: S, - enabled: bool, - downbeat_frame: usize, -} - -impl WithSupplier for Downbeat { - type Supplier = S; - - fn supplier(&self) -> &Self::Supplier { - &self.supplier - } - - fn supplier_mut(&mut self) -> &mut Self::Supplier { - &mut self.supplier - } -} - -impl Downbeat { - pub fn new(supplier: S) -> Self { - Self { - supplier, - enabled: false, - downbeat_frame: 0, - // downbeat_frame: 1_024_000 / 2, - } - } - - pub fn set_enabled(&mut self, enabled: bool) { - self.enabled = enabled; - } - - pub fn downbeat_frame(&self) -> usize { - self.downbeat_frame - } - - pub fn set_downbeat_in_beats( - &mut self, - beat: PositiveBeat, - tempo: Bpm, - material_info: &MaterialInfo, - ) where - S: WithMaterialInfo, - { - let source_frame_frate = material_info.frame_rate(); - let bps = tempo.get() / 60.0; - let second = beat.get() / bps; - let frame = convert_duration_in_seconds_to_frames( - DurationInSeconds::new(second), - source_frame_frate, - ); - self.set_downbeat_frame(frame); - } - - pub fn set_downbeat_frame(&mut self, frame: usize) { - self.downbeat_frame = frame; - } - - fn get_data(&self, request: &impl SupplyRequest) -> Option { - if !self.enabled || self.downbeat_frame == 0 { - return None; - } - let data = DownbeatRequestData { - start_frame: request.start_frame() + self.downbeat_frame as isize, - info: SupplyRequestInfo { - audio_block_frame_offset: request.info().audio_block_frame_offset, - requester: "downbeat-request", - note: "", - is_realtime: request.info().is_realtime, - }, - }; - Some(data) - } -} - -impl AudioSupplier for Downbeat { - fn supply_audio( - &mut self, - request: &SupplyAudioRequest, - dest_buffer: &mut AudioBufMut, - ) -> SupplyResponse { - let data = match self.get_data(request) { - None => { - return self.supplier.supply_audio(request, dest_buffer); - } - Some(d) => d, - }; - let inner_request = SupplyAudioRequest { - start_frame: data.start_frame, - info: data.info, - dest_sample_rate: request.dest_sample_rate, - parent_request: Some(request), - general_info: request.general_info, - }; - self.supplier.supply_audio(&inner_request, dest_buffer) - } -} - -impl MidiSupplier for Downbeat { - fn supply_midi( - &mut self, - request: &SupplyMidiRequest, - event_list: &mut BorrowedMidiEventList, - ) -> SupplyResponse { - let data = match self.get_data(request) { - None => { - return self.supplier.supply_midi(request, event_list); - } - Some(d) => d, - }; - let inner_request = SupplyMidiRequest { - start_frame: data.start_frame, - info: data.info, - dest_frame_count: request.dest_frame_count, - dest_sample_rate: request.dest_sample_rate, - parent_request: Some(request), - general_info: request.general_info, - }; - self.supplier.supply_midi(&inner_request, event_list) - } -} - -impl PreBufferSourceSkill for Downbeat { - fn pre_buffer(&mut self, request: PreBufferFillRequest) { - if !self.enabled || self.downbeat_frame == 0 { - return self.supplier.pre_buffer(request); - } - let inner_request = PreBufferFillRequest { - start_frame: request.start_frame + self.downbeat_frame as isize, - }; - self.supplier.pre_buffer(inner_request); - } -} - -impl PositionTranslationSkill for Downbeat { - fn translate_play_pos_to_source_pos(&self, play_pos: isize) -> isize { - let effective_play_pos = if self.enabled { - play_pos + self.downbeat_frame as isize - } else { - play_pos - }; - self.supplier - .translate_play_pos_to_source_pos(effective_play_pos) - } -} - -struct DownbeatRequestData { - start_frame: isize, - info: SupplyRequestInfo, -} - -impl AutoDelegatingWithMaterialInfo for Downbeat {} -impl AutoDelegatingMidiSilencer for Downbeat {} diff --git a/playtime-clip-engine/src/rt/supplier/fade_util.rs b/playtime-clip-engine/src/rt/supplier/fade_util.rs deleted file mode 100644 index af46c6c31..000000000 --- a/playtime-clip-engine/src/rt/supplier/fade_util.rs +++ /dev/null @@ -1,131 +0,0 @@ -use crate::rt::buffer::AudioBufMut; - -/// Takes care of applying a fade-in starting at frame zero. -/// -/// The portion left of it will be muted. -/// -/// The `block_start_frame` parameter indicates the position of the block within the larger audio -/// portion. -pub fn apply_fade_in_starting_at_zero( - block: &mut AudioBufMut, - block_start_frame: isize, - fade_length: usize, -) { - use BlockLocation::*; - match block_location(block_start_frame, block.frame_count(), fade_length) { - ContainingFadePortion => { - block.modify_frames(|sample| { - let factor = calc_fade_in_volume_factor_at( - block_start_frame + sample.index.frame as isize, - fade_length, - ); - sample.value * factor - }); - } - LeftOfFade => { - block.clear(); - } - RightOfFade => {} - } -} - -/// Takes care of applying a fade-out to the last few frames of a larger audio portion. -/// -/// The portion right of it will be muted. -/// -/// The `block_start_frame` parameter indicates the position of the block within the larger audio -/// portion. -pub fn apply_fade_out_ending_at( - block: &mut AudioBufMut, - block_start_frame: isize, - frame_count: usize, - fade_length: usize, -) { - let adjusted_block_start_frame = - block_start_frame - frame_count as isize + fade_length as isize; - apply_fade_out_starting_at_zero(block, adjusted_block_start_frame, fade_length); -} - -/// Takes care of applying a fade-out starting at frame zero. -/// -/// The portion right of it will be muted. -/// -/// The `block_start_frame` parameter indicates the position of the block within the larger audio -/// portion. -pub fn apply_fade_out_starting_at_zero( - block: &mut AudioBufMut, - block_start_frame: isize, - fade_length: usize, -) { - use BlockLocation::*; - match block_location(block_start_frame, block.frame_count(), fade_length) { - ContainingFadePortion => { - block.modify_frames(|sample| { - let factor = calc_fade_out_volume_factor_at( - block_start_frame + sample.index.frame as isize, - fade_length, - ); - sample.value * factor - }); - } - LeftOfFade => {} - RightOfFade => { - block.clear(); - } - } -} - -fn calc_fade_in_volume_factor_at(frame: isize, fade_length: usize) -> f64 { - if frame < 0 { - // Left of fade - return 0.0; - } - if frame >= fade_length as isize { - // Right of fade - return 1.0; - } - frame as f64 / fade_length as f64 -} - -fn calc_fade_out_volume_factor_at(frame: isize, fade_length: usize) -> f64 { - if frame < 0 { - // Left of fade - return 1.0; - } - if frame >= fade_length as isize { - // Right of fade - return 0.0; - } - (frame - fade_length as isize).abs() as f64 / fade_length as f64 -} - -fn block_location( - block_start_frame: isize, - block_frame_count: usize, - fade_length: usize, -) -> BlockLocation { - if block_start_frame > fade_length as isize { - return BlockLocation::RightOfFade; - } - let block_end_frame = block_start_frame + block_frame_count as isize; - if block_end_frame < 0 { - return BlockLocation::LeftOfFade; - } - BlockLocation::ContainingFadePortion -} - -enum BlockLocation { - ContainingFadePortion, - LeftOfFade, - RightOfFade, -} - -/// 2400 frames = 50ms at 48 kHz -/// -/// I tested 5ms, that's not enough. Take some pad/organ sound and it will click! -/// And this, gentlemen, is why the stop process needs to be asynchronous = needs to cover -/// multiple audio callback cycles. -const FADE_LENGTH: usize = 2400; -pub const SECTION_FADE_LENGTH: usize = FADE_LENGTH; -pub const INTERACTION_FADE_LENGTH: usize = FADE_LENGTH; -pub const START_END_FADE_LENGTH: usize = FADE_LENGTH; diff --git a/playtime-clip-engine/src/rt/supplier/interaction_handler.rs b/playtime-clip-engine/src/rt/supplier/interaction_handler.rs deleted file mode 100644 index 76a965faf..000000000 --- a/playtime-clip-engine/src/rt/supplier/interaction_handler.rs +++ /dev/null @@ -1,401 +0,0 @@ -use crate::rt::buffer::AudioBufMut; -use crate::rt::supplier::fade_util::{ - apply_fade_in_starting_at_zero, apply_fade_out_starting_at_zero, INTERACTION_FADE_LENGTH, -}; -use crate::rt::supplier::midi_util::SilenceMidiBlockMode; -use crate::rt::supplier::{ - midi_util, AudioSupplier, AutoDelegatingPositionTranslationSkill, - AutoDelegatingPreBufferSourceSkill, AutoDelegatingWithMaterialInfo, MidiSilencer, MidiSupplier, - SupplyAudioRequest, SupplyMidiRequest, SupplyRequestInfo, SupplyResponse, SupplyResponseStatus, - WithMaterialInfo, WithSupplier, -}; -use crate::ClipEngineResult; -use playtime_api::persistence::MidiResetMessageRange; -use reaper_medium::BorrowedMidiEventList; -use std::cmp; -use std::fmt::Debug; - -#[derive(Debug)] -pub struct InteractionHandler { - supplier: S, - interaction: Option, - midi_reset_msg_range: MidiResetMessageRange, -} - -#[derive(Clone, Copy, Debug)] -struct Interaction { - kind: InteractionKind, - /// Reference frame. - /// - /// This is the frame on which the material should start (start interaction) - /// or stop (stop interaction). - /// - /// For audio material, fades are inserted. For a start interaction, this frame marks the fade - /// beginning. For a stop interaction, it marks the fade end. - frame: isize, -} - -impl Interaction { - pub fn new(kind: InteractionKind, frame: isize) -> Self { - Interaction { kind, frame } - } - - pub fn immediate(kind: InteractionKind, current_frame: isize, is_midi: bool) -> Self { - if is_midi { - Self::new(kind, current_frame) - } else { - use InteractionKind::*; - match kind { - Start => Self::new(kind, current_frame), - Stop => Self::new(kind, current_frame + INTERACTION_FADE_LENGTH as isize), - } - } - } - - pub fn fade_begin_frame(&self) -> isize { - use InteractionKind::*; - match self.kind { - Start => self.frame, - Stop => self.frame - INTERACTION_FADE_LENGTH as isize, - } - } - - pub fn fade_end_frame(&self) -> isize { - use InteractionKind::*; - match self.kind { - Start => self.frame + INTERACTION_FADE_LENGTH as isize, - Stop => self.frame, - } - } -} - -#[derive(Clone, Copy, Eq, PartialEq, Debug)] -enum InteractionKind { - Start, - Stop, -} - -impl WithSupplier for InteractionHandler { - type Supplier = S; - - fn supplier(&self) -> &Self::Supplier { - &self.supplier - } - - fn supplier_mut(&mut self) -> &mut Self::Supplier { - &mut self.supplier - } -} - -impl InteractionHandler { - pub fn new(supplier: S) -> Self { - Self { - interaction: None, - supplier, - midi_reset_msg_range: Default::default(), - } - } - - pub fn set_midi_reset_msg_range(&mut self, range: MidiResetMessageRange) { - self.midi_reset_msg_range = range; - } - - pub fn has_stop_interaction(&self) -> bool { - self.interaction - .map(|f| f.kind == InteractionKind::Stop) - .unwrap_or(false) - } - - pub fn reset(&mut self) { - self.interaction = None; - } - - /// Invokes a start interaction. - /// - /// Audio: - /// - Installs a fade-in starting at the given frame. - /// - Handles an already happening fade-out correctly. - /// - /// MIDI: - /// - Installs some stop interaction reset messages. - pub fn start_immediately(&mut self, current_frame: isize) -> ClipEngineResult<()> - where - S: WithMaterialInfo, - { - self.install_immediate_interaction(InteractionKind::Start, current_frame) - } - - /// Invokes a stop interaction. - /// - /// Audio: - /// - Installs a fade-out starting at the given frame. - /// - Handles an already happening fade-in correctly. - /// - /// MIDI: - /// - Installs some stop interaction reset messages. - pub fn stop_immediately(&mut self, current_frame: isize) -> ClipEngineResult<()> - where - S: WithMaterialInfo, - { - self.install_immediate_interaction(InteractionKind::Stop, current_frame) - } - - /// Schedules a stop interaction at the given position. - /// - /// Audio: - /// - Installs a fade-out ending at the given frame (and starting some frames before that). - /// - Doesn't do anything to handle an already happening fade-in correctly! - /// - /// MIDI: - /// - Installs some stop interaction reset messages at the given frame. - pub fn schedule_stop_at(&mut self, end_frame: isize) { - self.interaction = Some(Interaction::new(InteractionKind::Stop, end_frame)) - } - - fn install_immediate_interaction( - &mut self, - kind: InteractionKind, - current_frame: isize, - ) -> ClipEngineResult<()> - where - S: WithMaterialInfo, - { - let is_midi = self.material_info()?.is_midi(); - let new_interaction = Interaction::immediate(kind, current_frame, is_midi); - let new_interaction = if is_midi { - Some(new_interaction) - } else { - self.fix_new_interaction_respecting_overlapping_fades(new_interaction) - }; - if let Some(i) = new_interaction { - self.interaction = Some(i) - } - Ok(()) - } - - /// Shifts the frame of the given interaction in case there's a fade happening already in order - /// to ensure continuity of the volume envelope (e.g. a fade-in when it's already fading out). - /// - /// Returns `None` if no interaction change is necessary, in particular if there's already - /// an ongoing fade in the right direction. - /// - /// Attention: This logic assumes that the given frame is the current timeline frame! It - /// can't be used with scheduled interactions. - fn fix_new_interaction_respecting_overlapping_fades( - &self, - new_interaction: Interaction, - ) -> Option { - let ongoing_interaction = match self.interaction { - // No fade at the moment, no fix necessary. - None => return Some(new_interaction), - Some(i) => i, - }; - if ongoing_interaction.kind == new_interaction.kind { - // Already fading into same direction. But the fade-out end position might have changed. - // So we replace it with the new interaction. This is especially important when - // requesting immediate clip stop / panic while the clip has already been scheduled - // for stop. - return Some(new_interaction); - } - let begin_frame_of_new_fade = new_interaction.fade_begin_frame(); - let begin_frame_of_ongoing_fade = ongoing_interaction.fade_begin_frame(); - let current_pos_in_fade = begin_frame_of_new_fade - begin_frame_of_ongoing_fade; - // If current_pos_in_fade is zero, we should skip the fade (move it completely to left). - // If it's FADE_LENGTH, we should apply the complete fade. - let adjustment = current_pos_in_fade - INTERACTION_FADE_LENGTH as isize; - let fixed_interaction = - Interaction::new(new_interaction.kind, new_interaction.frame + adjustment); - Some(fixed_interaction) - } - - fn silence_midi_at_stop_interaction(&mut self, event_list: &mut BorrowedMidiEventList) - where - S: MidiSilencer, - { - debug!("Silence MIDI at stop interaction"); - midi_util::silence_midi( - event_list, - self.midi_reset_msg_range.right, - SilenceMidiBlockMode::Append, - &mut self.supplier, - ); - self.interaction = None; - } -} - -impl AudioSupplier for InteractionHandler { - fn supply_audio( - &mut self, - request: &SupplyAudioRequest, - dest_buffer: &mut AudioBufMut, - ) -> SupplyResponse { - let interaction = match self.interaction { - None => { - // No interaction installed. - return self.supplier.supply_audio(request, dest_buffer); - } - Some(i) => i, - }; - #[cfg(debug_assertions)] - { - let source_frame_rate = self.supplier.material_info().unwrap().frame_rate(); - request.assert_wants_source_frame_rate(source_frame_rate); - } - use InteractionKind::*; - let distance_from_fade_begin = request.start_frame - interaction.fade_begin_frame(); - match interaction.kind { - Start => { - if distance_from_fade_begin < 0 { - unreachable!("there shouldn't be any scheduled start interactions"); - } - let inner_response = self.supplier.supply_audio(request, dest_buffer); - // The following function returns early if fade not yet started. - apply_fade_in_starting_at_zero( - dest_buffer, - distance_from_fade_begin, - INTERACTION_FADE_LENGTH, - ); - let end_frame = request.start_frame + inner_response.num_frames_consumed as isize; - if end_frame >= interaction.fade_end_frame() || inner_response.status.reached_end() - { - // Fade-in over or end-of-material reached. We can uninstall the interaction. - self.interaction = None; - } - inner_response - } - Stop => { - let distance_to_fade_end = interaction.fade_end_frame() - request.start_frame; - if distance_to_fade_end <= 0 { - // Exceeded end. Shouldn't usually happen because playback is continuous, but - // let's handle this gracefully. - self.interaction = None; - return SupplyResponse::exceeded_end(); - } - let num_frames_to_write = - cmp::min(dest_buffer.frame_count(), distance_to_fade_end as usize); - let mut sliced_dest_buffer = dest_buffer.slice_mut(0..num_frames_to_write); - let inner_response = self.supplier.supply_audio(request, &mut sliced_dest_buffer); - match inner_response.status { - SupplyResponseStatus::PleaseContinue => { - // The following function returns early if fade not yet started. - apply_fade_out_starting_at_zero( - dest_buffer, - distance_from_fade_begin, - INTERACTION_FADE_LENGTH, - ); - let end_frame = - request.start_frame + inner_response.num_frames_consumed as isize; - if end_frame < interaction.fade_end_frame() { - // Fade-out end not reached yet. - inner_response - } else { - // Fade-out over. We can uninstall the interaction. - self.interaction = None; - SupplyResponse::reached_end( - inner_response.num_frames_consumed, - num_frames_to_write, - ) - } - } - SupplyResponseStatus::ReachedEnd { .. } => { - // If no more material, it's not our responsibility to apply a fade. - // Also, there's no need to continue the fade. - self.interaction = None; - inner_response - } - } - } - } - } -} - -impl MidiSupplier for InteractionHandler { - fn supply_midi( - &mut self, - request: &SupplyMidiRequest, - event_list: &mut BorrowedMidiEventList, - ) -> SupplyResponse { - // With MIDI it's simple. No fade necessary, just a plain "Shut up!". - let interaction = match self.interaction { - None => { - // No interaction installed. - return self.supplier.supply_midi(request, event_list); - } - Some(i) => i, - }; - use InteractionKind::*; - match interaction.kind { - Start => { - // We know that start interactions are always immediate and that they are cleared - // immediately as well (MIDI only). - assert_eq!(request.start_frame, interaction.frame); - let inner_response = self.supplier.supply_midi(request, event_list); - debug!("Silence MIDI at start interaction"); - midi_util::silence_midi( - event_list, - self.midi_reset_msg_range.left, - SilenceMidiBlockMode::Prepend, - &mut self.supplier, - ); - self.interaction = None; - inner_response - } - Stop => { - let distance_to_interaction = interaction.frame - request.start_frame; - if distance_to_interaction <= 0 { - // Exceeded end. - // Usually this is caused by an instant stop interaction. In this case, the - // distance is exactly 0. - self.silence_midi_at_stop_interaction(event_list); - return SupplyResponse::exceeded_end(); - } - // This logic assumes that the destination frame rate is comparable to the - // source frame rate. The resampler (which currently sits on top of this supplier) - // takes care of that. - let num_frames_to_write = - cmp::min(request.dest_frame_count, distance_to_interaction as usize); - let inner_request = SupplyMidiRequest { - start_frame: request.start_frame, - dest_frame_count: num_frames_to_write, - dest_sample_rate: request.dest_sample_rate, - info: SupplyRequestInfo { - audio_block_frame_offset: request.info.audio_block_frame_offset, - requester: "interaction-handler-midi-stop", - note: "", - is_realtime: true, - }, - parent_request: Some(request), - general_info: request.general_info, - }; - let inner_response = self.supplier.supply_midi(&inner_request, event_list); - match inner_response.status { - SupplyResponseStatus::PleaseContinue => { - let end_frame = - request.start_frame + inner_response.num_frames_consumed as isize; - if end_frame < interaction.frame { - // Not yet time to reset. - inner_response - } else { - // Time to reset. Also, we can uninstall the interaction. - self.silence_midi_at_stop_interaction(event_list); - SupplyResponse::reached_end( - inner_response.num_frames_consumed, - num_frames_to_write, - ) - } - } - SupplyResponseStatus::ReachedEnd { .. } => { - // If no more material, it's not our responsibility to apply a fade. - // Also, there's no need to continue the fade. - self.interaction = None; - inner_response - } - } - } - } - } -} - -impl AutoDelegatingWithMaterialInfo for InteractionHandler {} -impl AutoDelegatingPreBufferSourceSkill for InteractionHandler {} -impl AutoDelegatingPositionTranslationSkill for InteractionHandler {} diff --git a/playtime-clip-engine/src/rt/supplier/log_util.rs b/playtime-clip-engine/src/rt/supplier/log_util.rs deleted file mode 100644 index 1e22cbf81..000000000 --- a/playtime-clip-engine/src/rt/supplier/log_util.rs +++ /dev/null @@ -1,89 +0,0 @@ -use crate::conversion_util::{ - convert_duration_in_frames_to_seconds, convert_position_in_seconds_to_frames, -}; -use crate::rt::supplier::SupplyRequest; -use crate::timeline::{clip_timeline, Timeline}; -use crate::{Laziness, QuantizedPosition}; -use playtime_api::persistence::EvenQuantization; -use reaper_medium::PositionInSeconds; -use std::cmp; - -/// This deals with timeline units only. -pub fn print_distance_from_beat_start_at( - request: &impl SupplyRequest, - additional_block_offset: usize, - comment: &str, -) { - let effective_block_offset = request.info().audio_block_frame_offset + additional_block_offset; - let offset_in_timeline_secs = convert_duration_in_frames_to_seconds( - effective_block_offset, - request.general_info().output_frame_rate, - ); - let ref_pos = request.general_info().audio_block_timeline_cursor_pos + offset_in_timeline_secs; - let timeline = clip_timeline(None, false); - let next_bar = timeline - .next_quantized_pos_at( - ref_pos, - EvenQuantization::ONE_BAR, - Laziness::DwellingOnCurrentPos, - ) - .position() as i32; - struct BarInfo { - bar: i32, - pos: PositionInSeconds, - rel_pos: PositionInSeconds, - } - let create_bar_info = |bar| { - let bar_pos = timeline.pos_of_quantized_pos(QuantizedPosition::bar(bar as i64)); - BarInfo { - bar, - pos: bar_pos, - rel_pos: ref_pos - bar_pos, - } - }; - let current_bar_info = create_bar_info(next_bar - 1); - let next_bar_info = create_bar_info(next_bar); - let closest = cmp::min_by_key(¤t_bar_info, &next_bar_info, |v| v.rel_pos.abs()); - let rel_pos_from_closest_bar_in_timeline_frames = convert_position_in_seconds_to_frames( - closest.rel_pos, - request.general_info().output_frame_rate, - ); - let block_duration = convert_duration_in_frames_to_seconds( - request.general_info().audio_block_length, - request.general_info().output_frame_rate, - ); - let block_index = (request.general_info().audio_block_timeline_cursor_pos.get() - / block_duration.get()) as isize; - debug!( - "\n\ - # New loop cycle\n\ - Block index: {}\n\ - Block start position: {:.3}s\n\ - Closest bar: {}\n\ - Closest bar timeline position: {:.3}s\n\ - Relative position from closest bar: {:.3}ms (= {} timeline frames)\n\ - Effective block offset: {},\n\ - Requester: {}\n\ - Note: {}\n\ - Comment: {}\n\ - Clip tempo factor: {}\n\ - Timeline tempo: {}\n\ - Parent requester: {:?}\n\ - Parent note: {:?}\n\ - ", - block_index, - request.general_info().audio_block_timeline_cursor_pos, - closest.bar, - closest.pos.get(), - closest.rel_pos.get() * 1000.0, - rel_pos_from_closest_bar_in_timeline_frames, - effective_block_offset, - request.info().requester, - request.info().note, - comment, - request.general_info().clip_tempo_factor, - request.general_info().timeline_tempo, - request.parent_request().map(|r| r.info().requester), - request.parent_request().map(|r| r.info().note) - ); -} diff --git a/playtime-clip-engine/src/rt/supplier/looper.rs b/playtime-clip-engine/src/rt/supplier/looper.rs deleted file mode 100644 index 313804d31..000000000 --- a/playtime-clip-engine/src/rt/supplier/looper.rs +++ /dev/null @@ -1,333 +0,0 @@ -use crate::rt::buffer::AudioBufMut; -use crate::rt::supplier::midi_util::SilenceMidiBlockMode; -use crate::rt::supplier::{ - midi_util, AudioSupplier, AutoDelegatingMidiSilencer, AutoDelegatingWithMaterialInfo, - MidiSilencer, MidiSupplier, PositionTranslationSkill, SupplyAudioRequest, SupplyMidiRequest, - SupplyRequest, SupplyRequestInfo, SupplyResponse, SupplyResponseStatus, WithMaterialInfo, - WithSupplier, -}; -use crate::ClipEngineResult; -use playtime_api::persistence::MidiResetMessageRange; -use reaper_medium::BorrowedMidiEventList; - -#[derive(Debug)] -pub struct Looper { - loop_behavior: LoopBehavior, - enabled: bool, - supplier: S, - midi_reset_msg_range: MidiResetMessageRange, -} - -#[derive(Debug)] -pub enum LoopBehavior { - Infinitely, - UntilEndOfCycle(usize), -} - -impl Default for LoopBehavior { - fn default() -> Self { - Self::UntilEndOfCycle(0) - } -} - -#[derive(Copy, Clone, Eq, PartialEq, Debug)] -pub enum Repetition { - Infinitely, - Once, -} - -impl Repetition { - pub fn from_bool(repeated: bool) -> Self { - if repeated { - Repetition::Infinitely - } else { - Repetition::Once - } - } -} - -impl LoopBehavior { - pub fn from_repetition(repetition: Repetition) -> Self { - use Repetition::*; - match repetition { - Infinitely => Self::Infinitely, - Once => Self::UntilEndOfCycle(0), - } - } - - pub fn from_bool(repeated: bool) -> Self { - if repeated { - Self::Infinitely - } else { - Self::UntilEndOfCycle(0) - } - } - - /// Returns the index of the last cycle to be played. - fn last_cycle_to_be_played(&self) -> Option { - use LoopBehavior::*; - match self { - Infinitely => None, - UntilEndOfCycle(n) => Some(*n), - } - } -} - -impl WithSupplier for Looper { - type Supplier = S; - - fn supplier(&self) -> &Self::Supplier { - &self.supplier - } - - fn supplier_mut(&mut self) -> &mut Self::Supplier { - &mut self.supplier - } -} - -impl Looper { - pub fn new(supplier: S) -> Self { - Self { - loop_behavior: Default::default(), - enabled: false, - supplier, - midi_reset_msg_range: Default::default(), - } - } - - pub fn set_enabled(&mut self, enabled: bool) { - self.enabled = enabled; - } - - pub fn set_midi_reset_msg_range(&mut self, range: MidiResetMessageRange) { - self.midi_reset_msg_range = range; - } - - pub fn set_loop_behavior(&mut self, loop_behavior: LoopBehavior) { - self.loop_behavior = loop_behavior; - } - - pub fn keep_playing_until_end_of_current_cycle(&mut self, pos: isize) -> ClipEngineResult<()> { - let last_cycle = get_cycle_at_frame(pos, self.supplier.material_info()?.frame_count()); - self.loop_behavior = LoopBehavior::UntilEndOfCycle(last_cycle); - Ok(()) - } - - fn check_relevance(&self, start_frame: isize) -> Option { - if !self.enabled { - return None; - } - let frame_count = self.supplier.material_info().unwrap().frame_count(); - let current_cycle = get_cycle_at_frame(start_frame, frame_count); - let cycle_in_scope = self - .loop_behavior - .last_cycle_to_be_played() - .map(|last_cycle| current_cycle <= last_cycle) - .unwrap_or(true); - if !cycle_in_scope { - return None; - } - let data = RelevantData { - start_frame, - current_cycle, - frame_count, - }; - Some(data) - } - - fn is_last_cycle(&self, cycle: usize) -> bool { - self.loop_behavior - .last_cycle_to_be_played() - .map(|last_cycle| cycle == last_cycle) - .unwrap_or(false) - } -} - -struct RelevantData { - start_frame: isize, - current_cycle: usize, - frame_count: usize, -} - -impl RelevantData { - /// Start from beginning if we encounter a start frame after the end (modulo). - fn modulo_start_frame(&self) -> isize { - if self.start_frame < 0 { - self.start_frame - } else { - self.start_frame % self.frame_count as isize - } - } -} - -impl AudioSupplier for Looper { - fn supply_audio( - &mut self, - request: &SupplyAudioRequest, - dest_buffer: &mut AudioBufMut, - ) -> SupplyResponse { - let data = match self.check_relevance(request.start_frame) { - None => { - return self.supplier.supply_audio(request, dest_buffer); - } - Some(d) => d, - }; - let modulo_start_frame = data.modulo_start_frame(); - let modulo_request = SupplyAudioRequest { - start_frame: modulo_start_frame, - dest_sample_rate: request.dest_sample_rate, - info: SupplyRequestInfo { - audio_block_frame_offset: request.info.audio_block_frame_offset, - requester: "looper-audio-modulo-request", - note: "", - is_realtime: request.info().is_realtime, - }, - parent_request: Some(request), - general_info: request.general_info, - }; - let modulo_response = self.supplier.supply_audio(&modulo_request, dest_buffer); - match modulo_response.status { - SupplyResponseStatus::PleaseContinue => modulo_response, - SupplyResponseStatus::ReachedEnd { num_frames_written } => { - if self.is_last_cycle(data.current_cycle) { - // Time to stop. - modulo_response - } else if num_frames_written == dest_buffer.frame_count() { - // Perfect landing, source completely consumed. Start next cycle. - SupplyResponse::please_continue(modulo_response.num_frames_consumed) - } else { - // Exceeded end of source. - // We need to fill the rest with material from the beginning of the source. - let start_request = SupplyAudioRequest { - start_frame: 0, - dest_sample_rate: request.dest_sample_rate, - info: SupplyRequestInfo { - audio_block_frame_offset: request.info.audio_block_frame_offset - + num_frames_written, - requester: "looper-audio-start-request", - note: "", - is_realtime: request.info().is_realtime, - }, - parent_request: Some(request), - general_info: request.general_info, - }; - let start_response = self.supplier.supply_audio( - &start_request, - &mut dest_buffer.slice_mut(num_frames_written..), - ); - SupplyResponse::please_continue( - modulo_response.num_frames_consumed + start_response.num_frames_consumed, - ) - } - } - } - } -} - -impl MidiSupplier for Looper { - fn supply_midi( - &mut self, - request: &SupplyMidiRequest, - event_list: &mut BorrowedMidiEventList, - ) -> SupplyResponse { - let data = match self.check_relevance(request.start_frame) { - None => { - return self.supplier.supply_midi(request, event_list); - } - Some(d) => d, - }; - let modulo_start_frame = data.modulo_start_frame(); - let modulo_request = SupplyMidiRequest { - start_frame: modulo_start_frame, - dest_frame_count: request.dest_frame_count, - dest_sample_rate: request.dest_sample_rate, - info: SupplyRequestInfo { - audio_block_frame_offset: request.info.audio_block_frame_offset, - requester: "looper-midi-modulo-request", - note: "", - is_realtime: request.info().is_realtime, - }, - parent_request: Some(request), - general_info: request.general_info, - }; - let modulo_response = self.supplier.supply_midi(&modulo_request, event_list); - if data.start_frame <= 0 { - let end_frame = data.start_frame + modulo_response.num_frames_consumed as isize; - if end_frame > 0 { - debug!("Silence MIDI at loop start"); - midi_util::silence_midi( - event_list, - self.midi_reset_msg_range.left, - SilenceMidiBlockMode::Prepend, - &mut self.supplier, - ); - } - } - match modulo_response.status { - SupplyResponseStatus::PleaseContinue => modulo_response, - SupplyResponseStatus::ReachedEnd { num_frames_written } => { - if self.is_last_cycle(data.current_cycle) { - // Time to stop. - debug!("Silence MIDI at loop end"); - midi_util::silence_midi( - event_list, - self.midi_reset_msg_range.right, - SilenceMidiBlockMode::Append, - &mut self.supplier, - ); - modulo_response - } else if num_frames_written == request.dest_frame_count { - // Perfect landing, source completely consumed. Start next cycle. - SupplyResponse::please_continue(modulo_response.num_frames_consumed) - } else { - // We need to fill the rest with material from the beginning of the source. - // Repeat. Fill rest of buffer with beginning of source. - // We need to start from negative position so the frame - // offset of the *added* MIDI events is correctly written. - // The negative position should be as long as the duration of - // samples already written. - let start_request = SupplyMidiRequest { - start_frame: -(modulo_response.num_frames_consumed as isize), - dest_sample_rate: request.dest_sample_rate, - dest_frame_count: request.dest_frame_count, - info: SupplyRequestInfo { - audio_block_frame_offset: request.info.audio_block_frame_offset - + num_frames_written, - requester: "looper-midi-start-request", - note: "", - is_realtime: request.info().is_realtime, - }, - parent_request: Some(request), - general_info: request.general_info, - }; - let start_response = self.supplier.supply_midi(&start_request, event_list); - // We don't add modulo_response.num_frames_consumed because that number of - // consumed frames is already contained in the number returned in the start - // response (because we started at a negative start position). - SupplyResponse::please_continue(start_response.num_frames_consumed) - } - } - } - } -} - -pub fn get_cycle_at_frame(frame: isize, frame_count: usize) -> usize { - if frame < 0 { - return 0; - } - frame as usize / frame_count -} - -impl PositionTranslationSkill for Looper { - fn translate_play_pos_to_source_pos(&self, play_pos: isize) -> isize { - let effective_play_pos = match self.check_relevance(play_pos) { - None => play_pos, - Some(d) => d.modulo_start_frame(), - }; - self.supplier - .translate_play_pos_to_source_pos(effective_play_pos) - } -} - -impl AutoDelegatingWithMaterialInfo for Looper {} -impl AutoDelegatingMidiSilencer for Looper {} diff --git a/playtime-clip-engine/src/rt/supplier/midi_note_tracker.rs b/playtime-clip-engine/src/rt/supplier/midi_note_tracker.rs deleted file mode 100644 index 6ad01fd41..000000000 --- a/playtime-clip-engine/src/rt/supplier/midi_note_tracker.rs +++ /dev/null @@ -1,253 +0,0 @@ -use crate::rt::supplier::{ - AutoDelegatingAudioSupplier, AutoDelegatingPositionTranslationSkill, - AutoDelegatingWithMaterialInfo, MidiSilencer, MidiSupplier, SupplyMidiRequest, SupplyResponse, - WithSupplier, -}; - -use helgoboss_midi::{ - Channel, KeyNumber, RawShortMessage, ShortMessage, ShortMessageFactory, StructuredShortMessage, - U7, -}; -use reaper_medium::{BorrowedMidiEventList, MidiEvent, MidiFrameOffset}; -use std::fmt::Debug; - -#[derive(Clone, Debug)] -pub struct MidiNoteTracker { - midi_state: MidiState, - supplier: S, -} - -#[derive(Clone, Debug, Default)] -struct MidiState { - note_states_by_channel: [NoteState; 16], -} - -impl MidiState { - pub fn reset(&mut self) { - for state in &mut self.note_states_by_channel { - state.reset(); - } - } - - pub fn update(&mut self, msg: &impl ShortMessage) { - match msg.to_structured() { - StructuredShortMessage::NoteOn { - channel, - key_number, - velocity, - } => { - let state = &mut self.note_states_by_channel[channel.get() as usize]; - if velocity.get() > 0 { - state.add_note(key_number); - } else { - state.remove_note(key_number); - } - } - StructuredShortMessage::NoteOff { - channel, - key_number, - .. - } => { - self.note_states_by_channel[channel.get() as usize].remove_note(key_number); - } - _ => {} - } - } - - pub fn on_notes(&self) -> impl Iterator + '_ { - self.note_states_by_channel - .iter() - .enumerate() - .flat_map(|(ch, note_state)| { - let ch = Channel::new(ch as _); - note_state.on_notes().map(move |note| (ch, note)) - }) - } -} - -#[derive(Copy, Clone, Eq, PartialEq, Default, Debug)] -struct NoteState(u128); - -impl NoteState { - pub fn reset(&mut self) { - self.0 = 0; - } - - pub fn add_note(&mut self, note: KeyNumber) { - self.0 |= 1 << (note.get() as u128); - } - - pub fn remove_note(&mut self, note: KeyNumber) { - self.0 &= !(1 << (note.get() as u128)); - } - - pub fn on_notes(&self) -> impl Iterator + '_ { - (0u8..128u8) - .filter(|note| self.note_is_on(*note)) - .map(KeyNumber::new) - } - - fn note_is_on(&self, note: u8) -> bool { - (self.0 & (1 << note)) > 0 - } -} - -impl MidiNoteTracker { - pub fn new(supplier: S) -> Self { - Self { - supplier, - midi_state: MidiState::default(), - } - } -} - -impl WithSupplier for MidiNoteTracker { - type Supplier = S; - - fn supplier(&self) -> &Self::Supplier { - &self.supplier - } - - fn supplier_mut(&mut self) -> &mut Self::Supplier { - &mut self.supplier - } -} - -impl MidiSupplier for MidiNoteTracker { - fn supply_midi( - &mut self, - request: &SupplyMidiRequest, - event_list: &mut BorrowedMidiEventList, - ) -> SupplyResponse { - let response = self.supplier.supply_midi(request, event_list); - // Track playing notes - for evt in event_list { - self.midi_state.update(evt.message()) - } - response - } -} - -impl MidiSilencer for MidiNoteTracker { - fn release_notes( - &mut self, - frame_offset: MidiFrameOffset, - event_list: &mut BorrowedMidiEventList, - ) { - for (ch, note) in self.midi_state.on_notes() { - let msg = RawShortMessage::note_off(ch, note, U7::MIN); - let mut event = MidiEvent::default(); - event.set_frame_offset(frame_offset); - event.set_message(msg); - event_list.add_item(&event); - } - self.midi_state.reset(); - } -} - -impl AutoDelegatingAudioSupplier for MidiNoteTracker {} -impl AutoDelegatingWithMaterialInfo for MidiNoteTracker {} -impl AutoDelegatingPositionTranslationSkill for MidiNoteTracker {} - -#[cfg(test)] -mod tests { - use super::*; - use helgoboss_midi::test_util::*; - - #[test] - fn note_state_basics() { - // Given - let mut note_state = NoteState::default(); - // When - note_state.add_note(KeyNumber::new(5)); - note_state.add_note(KeyNumber::new(7)); - note_state.add_note(KeyNumber::new(105)); - // Then - assert!(!note_state.note_is_on(0)); - assert!(note_state.note_is_on(5)); - assert!(note_state.note_is_on(7)); - assert!(!note_state.note_is_on(100)); - assert!(note_state.note_is_on(105)); - let on_notes: Vec<_> = note_state.on_notes().collect(); - assert_eq!( - on_notes, - vec![KeyNumber::new(5), KeyNumber::new(7), KeyNumber::new(105)] - ); - } - - #[test] - fn note_state_remove() { - // Given - let mut note_state = NoteState::default(); - // When - note_state.add_note(key_number(5)); - note_state.add_note(key_number(7)); - note_state.remove_note(key_number(5)); - note_state.add_note(key_number(105)); - note_state.remove_note(key_number(105)); - // Then - assert!(!note_state.note_is_on(0)); - assert!(!note_state.note_is_on(5)); - assert!(note_state.note_is_on(7)); - assert!(!note_state.note_is_on(100)); - let on_notes: Vec<_> = note_state.on_notes().collect(); - assert_eq!(on_notes, vec![key_number(7)]); - } - - #[test] - fn midi_state_update() { - // Given - let mut midi_state = MidiState::default(); - // When - midi_state.update(¬e_on(0, 7, 100)); - midi_state.update(¬e_on(0, 120, 120)); - midi_state.update(¬e_on(0, 5, 120)); - midi_state.update(¬e_on(0, 7, 0)); - midi_state.update(¬e_on(0, 120, 1)); - midi_state.update(¬e_off(0, 5, 20)); - // Then - let on_notes: Vec<_> = midi_state.on_notes().collect(); - assert_eq!(on_notes, vec![(channel(0), key_number(120))]); - } - - #[test] - fn midi_state_reset() { - // Given - let mut midi_state = MidiState::default(); - // When - midi_state.update(¬e_on(0, 7, 100)); - midi_state.update(¬e_on(0, 120, 120)); - midi_state.update(¬e_on(0, 5, 120)); - midi_state.update(¬e_on(0, 7, 0)); - midi_state.update(¬e_on(5, 120, 1)); - midi_state.update(¬e_off(7, 5, 20)); - midi_state.reset(); - // Then - let on_notes: Vec<_> = midi_state.on_notes().collect(); - assert_eq!(on_notes, vec![]); - } - - #[test] - fn midi_state_update_with_channels() { - // Given - let mut midi_state = MidiState::default(); - // When - midi_state.update(¬e_on(0, 7, 100)); - midi_state.update(¬e_on(0, 120, 120)); - midi_state.update(¬e_on(0, 5, 120)); - midi_state.update(¬e_on(1, 7, 0)); - midi_state.update(¬e_on(3, 7, 50)); - midi_state.update(¬e_on(0, 120, 1)); - midi_state.update(¬e_off(0, 5, 20)); - // Then - let on_notes: Vec<_> = midi_state.on_notes().collect(); - assert_eq!( - on_notes, - vec![ - (channel(0), key_number(7)), - (channel(0), key_number(120)), - (channel(3), key_number(7)) - ] - ); - } -} diff --git a/playtime-clip-engine/src/rt/supplier/midi_sequence.rs b/playtime-clip-engine/src/rt/supplier/midi_sequence.rs deleted file mode 100644 index 3d4dd7454..000000000 --- a/playtime-clip-engine/src/rt/supplier/midi_sequence.rs +++ /dev/null @@ -1,584 +0,0 @@ -#![allow(dead_code)] - -use crate::conversion_util::{ - adjust_proportionally_positive, convert_duration_in_frames_to_seconds, -}; -use crate::rt::supplier::midi_util::supply_midi_material; -use crate::rt::supplier::time_series::{TimeSeries, TimeSeriesEvent}; -use crate::rt::supplier::{ - MaterialInfo, MidiMaterialInfo, MidiSupplier, SupplyMidiRequest, SupplyResponse, - WithMaterialInfo, MIDI_BASE_BPM, MIDI_FRAME_RATE, -}; -use crate::ClipEngineResult; -use helgoboss_midi::{RawShortMessage, ShortMessage, ShortMessageFactory}; -use playtime_api::persistence::{Bpm, TimeSignature}; -use reaper_medium::{BorrowedMidiEventList, DurationInSeconds, MidiFrameOffset}; -use std::cmp; -use std::error::Error; -use std::fmt::{Display, Formatter, Write}; - -#[derive(Clone, Debug)] -pub struct MidiSequence { - ppq: u64, - time_series: TimeSeries, - time_info: MidiTimeInfo, - normalized_second_to_pulse_factor: f64, -} - -#[derive(Copy, Clone, Debug, PartialEq)] -pub struct MidiTimeInfo { - igntempo: bool, - tempo: Bpm, - time_signature: TimeSignature, -} - -impl Default for MidiTimeInfo { - fn default() -> Self { - Self { - igntempo: true, - tempo: Bpm::new(MIDI_BASE_BPM.get()).unwrap(), - time_signature: TimeSignature { - numerator: 4, - denominator: 4, - }, - } - } -} - -pub type MidiEvent = TimeSeriesEvent; - -impl MidiEvent { - pub fn format_for_reaper_chunk( - &self, - f: &mut Formatter, - last_frame: &mut u64, - ) -> std::fmt::Result { - let selected_char = if self.payload.selected { 'e' } else { 'E' }; - let mute_str = if self.payload.mute { "m" } else { "" }; - let frame_diff = self.frame - *last_frame; - *last_frame = self.frame; - let msg = self.payload.msg; - let b1 = msg.status_byte(); - let b2 = msg.data_byte_1().get(); - let b3 = msg.data_byte_2().get(); - let negative_quantization_shift = -self.payload.quantization_shift; - write!( - f, - "{selected_char}{mute_str} {frame_diff} {b1:02x} {b2:02x} {b3:02x} {negative_quantization_shift}" - ) - } -} - -#[derive(Copy, Clone, Eq, PartialEq, Debug)] -pub struct MidiEventPayload { - pub selected: bool, - pub mute: bool, - pub msg: RawShortMessage, - /// Positive means it has been shifted to the right, negative to the left. - pub quantization_shift: i32, -} - -enum ReaperMidiChunkEntry<'a> { - HasData(HasDataEntry), - Event(MidiEvent), - IgnTempo(MidiTimeInfo), - Unhandled(&'a str), -} - -struct HasDataEntry { - ppq: u64, -} - -const CONSTANT_BPM: Bpm = unsafe { Bpm::new_unchecked(MIDI_BASE_BPM.get()) }; -const CONSTANT_DENOM: u32 = 4; - -impl MidiSequence { - pub fn new( - ppq: u64, - time_series: TimeSeries, - time_info: MidiTimeInfo, - ) -> Self { - Self { - ppq, - time_series, - time_info, - normalized_second_to_pulse_factor: calculate_normalized_second_to_pulse_factor(ppq), - } - } - - pub fn empty(ppq: u64, capacity: usize, time_info: MidiTimeInfo) -> Self { - Self::new( - ppq, - TimeSeries::new(Vec::with_capacity(capacity)), - time_info, - ) - } - - /// Parses a MIDI sequence from the given in-project MIDI REAPER chunk. - pub fn parse_from_reaper_midi_chunk(chunk: &str) -> Result> { - let mut ppq = 960; - let mut time_info = MidiTimeInfo::default(); - let mut last_frame = 0; - let mut events = Vec::new(); - for line in chunk.lines() { - let trimmed = line.trim(); - if trimmed.is_empty() { - continue; - } - match parse_entry(trimmed, &mut last_frame)? { - ReaperMidiChunkEntry::Event(e) => { - events.push(e); - } - ReaperMidiChunkEntry::HasData(e) => { - ppq = e.ppq; - } - ReaperMidiChunkEntry::IgnTempo(d) => { - time_info = d; - } - ReaperMidiChunkEntry::Unhandled(_) => {} - } - } - let sequence = MidiSequence::new(ppq, TimeSeries::new(events), time_info); - Ok(sequence) - } - - pub fn ppq(&self) -> u64 { - self.ppq - } - - pub fn time_series(&self) -> &TimeSeries { - &self.time_series - } - - pub fn time_info(&self) -> &MidiTimeInfo { - &self.time_info - } - - pub fn insert_event_at_normalized_midi_frame( - &mut self, - frame: usize, - payload: MidiEventPayload, - ) { - let second = convert_duration_in_frames_to_seconds(frame, MIDI_FRAME_RATE); - let pulse = self.convert_duration_in_seconds_to_pulse_normalized(second); - self.insert_event_at_pulse(pulse, payload); - } - - pub fn insert_event_at_pulse(&mut self, pulse_index: u64, payload: MidiEventPayload) { - self.time_series.insert(pulse_index, payload); - } - - /// Formats the sequence as REAPER in-project chunk. - pub fn format_as_reaper_midi_chunk(&self) -> String { - MidiSequenceAsReaperChunk(self).to_string() - } - - /// Returns the total number of pulses of this sequence, in other words, the tempo-independent - /// length of the sequence. - /// - /// This is not the number of events! - pub fn total_pulse_count(&self) -> u64 { - // The last pulse frame index marks the *exclusive* end, so we don't need + 1 here I guess. - self.time_series.events.last().map(|e| e.frame).unwrap_or(0) - } - - /// Calculates the number of MIDI frames according to [`MIDI_FRAME_RATE`] using a normalized - /// tempo and time signature. - fn calculate_midi_frame_count_normalized(&self, pulse_count: u64) -> usize { - let seconds = pulse_count as f64 / self.normalized_second_to_pulse_factor; - adjust_proportionally_positive(seconds, MIDI_FRAME_RATE.get()) - } - - fn convert_duration_in_seconds_to_pulse_normalized(&self, duration: DurationInSeconds) -> u64 { - (duration.get() * self.normalized_second_to_pulse_factor).round() as u64 - } - - /// Calculates the length of this sequence given a particular tempo and time signature. - pub fn calculate_length(&self, bpm: Bpm, time_sig_denominator: u32) -> DurationInSeconds { - let second_to_pulse_factor = - self.calculate_second_to_pulse_factor(bpm, time_sig_denominator); - let length = self.total_pulse_count() as f64 / second_to_pulse_factor; - DurationInSeconds::new(length) - } - - fn calculate_second_to_pulse_factor(&self, bpm: Bpm, time_sig_denominator: u32) -> f64 { - convert_second_to_pulse_flexible(1.0, bpm, time_sig_denominator, self.ppq) - } -} - -fn convert_pulse_to_second_flexible( - pulse: u64, - bpm: Bpm, - time_sig_denominator: u32, - ppq: u64, -) -> f64 { - let bps = bpm.get() / 60.0; - let duration_of_one_beat_in_secs = 1.0 / bps; - let quarter_notes = pulse as f64 / ppq as f64; - let beat = quarter_notes * get_qn_to_beat_factor(time_sig_denominator); - beat * duration_of_one_beat_in_secs -} - -fn calculate_normalized_second_to_pulse_factor(ppq: u64) -> f64 { - convert_second_to_pulse_flexible(1.0, CONSTANT_BPM, CONSTANT_DENOM, ppq) -} - -fn convert_second_to_pulse_flexible( - second: f64, - bpm: Bpm, - time_sig_denominator: u32, - ppq: u64, -) -> f64 { - let bps = bpm.get() / 60.0; - let duration_of_one_beat_in_secs = 1.0 / bps; - let beat = second / duration_of_one_beat_in_secs; - let quarter_notes = beat / get_qn_to_beat_factor(time_sig_denominator); - quarter_notes * ppq as f64 -} - -/// Calculates the factor for converting from quarter notes to beats. -/// -/// 1 quarter note = how many beats? -/// - x/4 => 1 beat -/// - x/8 => 2 beats -/// - x/2 => 0.5 beats -fn get_qn_to_beat_factor(time_sig_denominator: u32) -> f64 { - time_sig_denominator as f64 / 4.0 -} - -impl MidiSupplier for MidiSequence { - // Below logic assumes that the destination frame rate is comparable to the source frame - // rate. The resampler makes sure of it. However, it's not necessarily equal since we use - // frame rate changes for tempo changes. It's only equal if the clip is played in - // MIDI_BASE_BPM. That's fine! - fn supply_midi( - &mut self, - request: &SupplyMidiRequest, - event_list: &mut BorrowedMidiEventList, - ) -> SupplyResponse { - supply_midi_material(request, |request| { - // Create aliases for brevity - let frame_rate = request.dest_sample_rate; - let start_frame = request.start_frame; - let num_frames_to_be_consumed = request.dest_frame_count; - // If sequence empty, stop right here - let total_num_pulses = self.total_pulse_count(); - if total_num_pulses == 0 { - return SupplyResponse::exceeded_end(); - } - // If start frame behind end, stop right here - let total_num_frames = self.calculate_midi_frame_count_normalized(total_num_pulses); - if start_frame >= total_num_frames { - return SupplyResponse::exceeded_end(); - }; - // Reduce number of requested frames according to that's still available - let num_remaining_frames = total_num_frames - start_frame; - let actual_num_frames_to_be_consumed = - cmp::min(num_frames_to_be_consumed, num_remaining_frames); - // Convert to pulses - let start_time = convert_duration_in_frames_to_seconds(start_frame, frame_rate); - let start_pulse = self.convert_duration_in_seconds_to_pulse_normalized(start_time); - let actual_seconds_to_be_consumed = - convert_duration_in_frames_to_seconds(actual_num_frames_to_be_consumed, frame_rate); - let actual_num_pulses_to_be_consumed = - self.convert_duration_in_seconds_to_pulse_normalized(actual_seconds_to_be_consumed); - // Write events to event list - let mut reaper_evt = reaper_medium::MidiEvent::default(); - let relevant_events = self - .time_series - .find_events_in_range(start_pulse, actual_num_pulses_to_be_consumed); - let pulse_to_frame_factor = frame_rate.get() / self.normalized_second_to_pulse_factor; - for evt in relevant_events { - let pulse_offset = (evt.frame - start_pulse) as f64; - let frame_offset = (pulse_offset * pulse_to_frame_factor).round() as u32; - reaper_evt.set_frame_offset(MidiFrameOffset::new(frame_offset)); - reaper_evt.set_message(evt.payload.msg); - event_list.add_item(&reaper_evt) - } - // TODO We don't include the last frame when it comes to reporting the pulse/frame count. - // Should we manually add this notes-off event? Or is it already transmitted? Or should we - // handle this via StartEndHandler? - // The lower the sample rate, the higher the tempo, the more inner source material we - // effectively grabbed. - // TODO-high We sometimes have missing/hanging notes, probably because some events - // are sometimes skipped, probably missing continuity. We should log whenever we - // detect a missing continuity and work on it. - SupplyResponse::limited_by_total_frame_count( - actual_num_frames_to_be_consumed, - actual_num_frames_to_be_consumed, - start_frame as isize, - total_num_frames, - ) - }) - } -} - -impl WithMaterialInfo for MidiSequence { - fn material_info(&self) -> ClipEngineResult { - let info = MidiMaterialInfo { - frame_count: self.calculate_midi_frame_count_normalized(self.total_pulse_count()), - }; - Ok(MaterialInfo::Midi(info)) - } -} - -struct MidiSequenceAsReaperChunk<'a>(&'a MidiSequence); - -impl<'a> Display for MidiSequenceAsReaperChunk<'a> { - fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { - writeln!(f, "HASDATA 1 {} QN", self.0.ppq)?; - let mut last_frame = 0; - for e in &self.0.time_series.events { - e.format_for_reaper_chunk(f, &mut last_frame)?; - f.write_char('\n')?; - } - let info = &self.0.time_info; - writeln!( - f, - "IGNTEMPO 1 {} {} {}", - info.tempo.get(), - info.time_signature.numerator, - info.time_signature.denominator - )?; - Ok(()) - } -} - -fn parse_entry<'a>( - line: &'a str, - last_frame: &mut u64, -) -> Result, Box> { - let mut iter = line.split(' '); - let directive = iter.next().ok_or("missing directive")?; - match directive { - // Example: "em 240 81 37 00 -90" - "e" | "E" | "em" | "Em" => { - let mut prefix_chars = directive.chars(); - let selected = prefix_chars.next().ok_or("missing selected char")? == 'e'; - let mute = prefix_chars.next() == Some('m'); - let frame_diff: u64 = iter.next().ok_or("missing frame diff")?.parse()?; - let frame = *last_frame + frame_diff; - *last_frame = frame; - let msg = RawShortMessage::from_bytes(( - parse_hex_byte(&mut iter)?, - parse_hex_byte(&mut iter)?, - parse_hex_byte(&mut iter)?, - ))?; - let quantization_shift = match iter.next() { - None => 0, - Some(s) => { - let negative_shift: i32 = s.parse()?; - -negative_shift - } - }; - let event = MidiEvent::new( - frame, - MidiEventPayload { - selected, - mute, - msg, - quantization_shift, - }, - ); - Ok(ReaperMidiChunkEntry::Event(event)) - } - // Example: "HASDATA 1 960 QN" - "HASDATA" => { - iter.next().ok_or("no HASDATA flag")?; - let ppq: u64 = iter.next().ok_or("no PPQ")?.parse()?; - let unit = iter.next().ok_or("no PPQ unit")?; - if unit != "QN" { - return Err("no QN unit".into()); - } - let entry = HasDataEntry { ppq }; - Ok(ReaperMidiChunkEntry::HasData(entry)) - } - // Example: "IGNTEMPO 1 120 4 4" - "IGNTEMPO" => { - let igntempo = iter.next().ok_or("no IGNTEMPO flag")? == "1"; - let tempo: f64 = iter.next().ok_or("no custom tempo")?.parse()?; - let numerator: u32 = iter.next().ok_or("no custom numerator")?.parse()?; - let denominator: u32 = iter.next().ok_or("no custom denominator")?.parse()?; - let data = MidiTimeInfo { - igntempo, - tempo: Bpm::new(tempo)?, - time_signature: TimeSignature { - numerator, - denominator, - }, - }; - Ok(ReaperMidiChunkEntry::IgnTempo(data)) - } - _ => Ok(ReaperMidiChunkEntry::Unhandled(line)), - } -} - -fn parse_hex_byte<'a, T: TryFrom, E: Error + 'static>( - iter: &mut impl Iterator, -) -> Result> { - let hex_string = iter.next().ok_or("byte missing")?; - let byte = u8::from_str_radix(hex_string, 16)?; - Ok(byte.try_into()?) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::rt::supplier::time_series::TimeSeriesEvent; - use helgoboss_midi::test_util::*; - - #[test] - fn basics() { - // Given - let events = vec![ - // e 0 91 30 31 - e(0, true, false, (0x91, 0x30, 0x31), 0), - // E 1 b0 7b 00 - e(1, false, false, (0xb0, 0x7b, 0x00), 0), - // e 239 81 30 00 -90 - e(240, true, false, (0x81, 0x30, 0x00), 90), - // e 240 91 37 27 - e(480, true, false, (0x91, 0x37, 0x27), 0), - // em 240 81 37 00 -90 - e(720, true, true, (0x81, 0x37, 0x00), 90), - // E 240 b0 7b 00 - e(960, false, false, (0xb0, 0x7b, 0x00), 0), - ]; - let midi_sequence = MidiSequence::new( - 960, - TimeSeries::new(events), - MidiTimeInfo { - igntempo: true, - tempo: Bpm::new(120.0).unwrap(), - time_signature: TimeSignature { - numerator: 4, - denominator: 4, - }, - }, - ); - // When - assert_eq!(midi_sequence.total_pulse_count(), 960); - let length_with_normal_tempo = midi_sequence.calculate_length(Bpm::new(120.0).unwrap(), 4); - assert_eq!(length_with_normal_tempo.get(), 0.5); - let length_with_different_time_sig = - midi_sequence.calculate_length(Bpm::new(120.0).unwrap(), 8); - assert_eq!(length_with_different_time_sig.get(), 1.0); - let length_with_double_tempo = midi_sequence.calculate_length(Bpm::new(240.0).unwrap(), 4); - assert_eq!(length_with_double_tempo.get(), 0.25); - let length_with_half_tempo = midi_sequence.calculate_length(Bpm::new(60.0).unwrap(), 4); - assert_eq!(length_with_half_tempo.get(), 1.0); - } - - #[test] - fn parse_from_reaper_midi_chunk() { - // Given - let chunk = r#" -HASDATA 1 960 QN -CCINTERP 32 -POOLEDEVTS {2B7731B1-2DE0-534E-A08F-DBFB0B3205DC} -e 0 91 30 31 -E 1 b0 7b 00 -e 239 81 30 00 -90 -e 240 91 37 27 -em 240 81 37 00 -90 -E 240 b0 7b 00 -CCINTERP 32 -GUID {ACC4D7CA-2E56-0248-AD95-8B027F12FD09} -IGNTEMPO 1 120 4 4 -SRCCOLOR 8197 -"#; - // When - let sequence = MidiSequence::parse_from_reaper_midi_chunk(chunk).unwrap(); - // Then - assert_eq!(sequence.ppq, 960); - assert_eq!( - sequence.time_info, - MidiTimeInfo { - igntempo: true, - tempo: Bpm::new(120.0).unwrap(), - time_signature: TimeSignature { - numerator: 4, - denominator: 4, - }, - } - ); - assert_eq!( - sequence.time_series.events, - vec![ - // e 0 91 30 31 - e(0, true, false, (0x91, 0x30, 0x31), 0), - // E 1 b0 7b 00 - e(1, false, false, (0xb0, 0x7b, 0x00), 0), - // e 239 81 30 00 -90 - e(240, true, false, (0x81, 0x30, 0x00), 90), - // e 240 91 37 27 - e(480, true, false, (0x91, 0x37, 0x27), 0), - // em 240 81 37 00 -90 - e(720, true, true, (0x81, 0x37, 0x00), 90), - // E 240 b0 7b 00 - e(960, false, false, (0xb0, 0x7b, 0x00), 0), - ] - ); - } - - #[test] - fn format_as_reaper_midi_chunk() { - // Given - let events = vec![ - // e 0 91 30 31 - e(0, true, false, (0x91, 0x30, 0x31), 0), - // E 1 b0 7b 00 - e(1, false, false, (0xb0, 0x7b, 0x00), 0), - // e 239 81 30 00 -90 - e(240, true, false, (0x81, 0x30, 0x00), 90), - // e 240 91 37 27 - e(480, true, false, (0x91, 0x37, 0x27), 0), - // em 240 81 37 00 -90 - e(720, true, true, (0x81, 0x37, 0x00), 90), - // E 240 b0 7b 00 - e(960, false, false, (0xb0, 0x7b, 0x00), 0), - ]; - let midi_sequence = MidiSequence::new( - 960, - TimeSeries::new(events), - MidiTimeInfo { - igntempo: true, - tempo: Bpm::new(120.0).unwrap(), - time_signature: TimeSignature { - numerator: 4, - denominator: 4, - }, - }, - ); - // When - let actual_chunk = midi_sequence.format_as_reaper_midi_chunk(); - // Then - let expected_chunk = r#"HASDATA 1 960 QN -e 0 91 30 31 0 -E 1 b0 7b 00 0 -e 239 81 30 00 -90 -e 240 91 37 27 0 -em 240 81 37 00 -90 -E 240 b0 7b 00 0 -IGNTEMPO 1 120 4 4 -"#; - // Then - assert_eq!(actual_chunk.as_str(), expected_chunk); - } - - fn e( - frame: u64, - selected: bool, - mute: bool, - (byte1, byte2, byte3): (u8, u8, u8), - quantization_shift: i32, - ) -> TimeSeriesEvent { - let evt = MidiEventPayload { - selected, - mute, - msg: short(byte1, byte2, byte3), - quantization_shift, - }; - TimeSeriesEvent::new(frame, evt) - } -} diff --git a/playtime-clip-engine/src/rt/supplier/midi_util.rs b/playtime-clip-engine/src/rt/supplier/midi_util.rs deleted file mode 100644 index 62622cfb6..000000000 --- a/playtime-clip-engine/src/rt/supplier/midi_util.rs +++ /dev/null @@ -1,136 +0,0 @@ -use crate::conversion_util::adjust_proportionally_positive; -use crate::rt::supplier::{MidiSilencer, SupplyMidiRequest, SupplyResponse, SupplyResponseStatus}; -use helgoboss_midi::{controller_numbers, Channel, RawShortMessage, ShortMessageFactory, U7}; -use playtime_api::persistence::MidiResetMessages; -use reaper_medium::{BorrowedMidiEventList, Hz, MidiEvent, MidiFrameOffset}; - -/// Helper function for MIDI suppliers that read from sources and don't want to deal with -/// negative start frames themselves. -/// -/// Below logic assumes that the destination frame rate is comparable to the source frame -/// rate. The resampler makes sure of it. However, it's not necessarily equal since we use -/// frame rate changes for tempo changes. It's only equal if the clip is played in -/// MIDI_BASE_BPM. That's fine! -pub fn supply_midi_material( - request: &SupplyMidiRequest, - supply_inner: impl FnOnce(MidiSourceMaterialRequest) -> SupplyResponse, -) -> SupplyResponse { - let ideal_num_consumed_frames = request.dest_frame_count; - let ideal_end_frame = request.start_frame + ideal_num_consumed_frames as isize; - if ideal_end_frame <= 0 { - // Requested portion is located entirely before the actual source material. - // We haven't reached the end of the source, so still tell the caller that we - // wrote all frames. And advance the count-in phase. - return SupplyResponse::please_continue(ideal_num_consumed_frames); - } - // Requested portion contains playable material. - if request.start_frame < 0 { - // Portion overlaps start of material. - let num_skipped_frames_in_source = -request.start_frame as usize; - let proportion_skipped = - num_skipped_frames_in_source as f64 / ideal_num_consumed_frames as f64; - let num_skipped_frames_in_dest = - adjust_proportionally_positive(ideal_num_consumed_frames as f64, proportion_skipped); - let req = MidiSourceMaterialRequest { - start_frame: 0, - dest_frame_count: ideal_num_consumed_frames - num_skipped_frames_in_dest, - dest_sample_rate: request.dest_sample_rate, - }; - let res = supply_inner(req); - return SupplyResponse { - num_frames_consumed: num_skipped_frames_in_source + res.num_frames_consumed, - status: match res.status { - SupplyResponseStatus::PleaseContinue => SupplyResponseStatus::PleaseContinue, - SupplyResponseStatus::ReachedEnd { num_frames_written } => { - // Oh, that's short material. - SupplyResponseStatus::ReachedEnd { - num_frames_written: num_skipped_frames_in_dest + num_frames_written, - } - } - }, - }; - } - // Requested portion is located on or after start of the actual source material. - let req = MidiSourceMaterialRequest { - start_frame: request.start_frame as usize, - dest_frame_count: ideal_num_consumed_frames, - dest_sample_rate: request.dest_sample_rate, - }; - supply_inner(req) -} - -pub struct MidiSourceMaterialRequest { - pub start_frame: usize, - pub dest_frame_count: usize, - pub dest_sample_rate: Hz, -} - -pub fn silence_midi( - event_list: &mut BorrowedMidiEventList, - reset_messages: MidiResetMessages, - block_mode: SilenceMidiBlockMode, - silencer: &mut dyn MidiSilencer, -) { - if !reset_messages.at_least_one_enabled() { - return; - } - use SilenceMidiBlockMode::*; - let frame_offset = match block_mode { - Prepend => { - for evt in event_list.iter_mut() { - if evt.frame_offset() == MidiFrameOffset::MIN { - evt.set_frame_offset(MidiFrameOffset::new(1)); - } - } - MidiFrameOffset::MIN - } - Append => event_list - .iter() - .map(|evt| evt.frame_offset()) - .max() - .map(|o| MidiFrameOffset::new(o.get() + 1)) - .unwrap_or(MidiFrameOffset::MIN), - }; - // At the moment, resetting on-notes is only supported as append (= at the end). If we would ask - // to release notes when prepending (at the start), we would need to make sure that the - // to-be-released notes are captured *before* filling the event list. But this function is - // usually called *after* filling the event list. - if reset_messages.on_notes_off && block_mode == Append { - silencer.release_notes(frame_offset, event_list); - } - for ch in 0..16 { - let mut append_reset = |cc| { - let msg = RawShortMessage::control_change(Channel::new(ch), cc, U7::MIN); - add_midi_event(event_list, frame_offset, msg); - }; - if reset_messages.all_notes_off { - append_reset(controller_numbers::ALL_NOTES_OFF); - } - if reset_messages.all_sound_off { - append_reset(controller_numbers::ALL_SOUND_OFF); - } - if reset_messages.reset_all_controllers { - append_reset(controller_numbers::RESET_ALL_CONTROLLERS); - } - if reset_messages.damper_pedal_off { - append_reset(controller_numbers::DAMPER_PEDAL_ON_OFF); - } - } -} - -#[derive(Eq, PartialEq)] -pub enum SilenceMidiBlockMode { - Prepend, - Append, -} - -fn add_midi_event( - event_list: &mut BorrowedMidiEventList, - frame_offset: MidiFrameOffset, - msg: RawShortMessage, -) { - let mut event = MidiEvent::default(); - event.set_frame_offset(frame_offset); - event.set_message(msg); - event_list.add_item(&event); -} diff --git a/playtime-clip-engine/src/rt/supplier/mod.rs b/playtime-clip-engine/src/rt/supplier/mod.rs deleted file mode 100644 index fd6f8f647..000000000 --- a/playtime-clip-engine/src/rt/supplier/mod.rs +++ /dev/null @@ -1,60 +0,0 @@ -mod reaper_clip_source; -pub use reaper_clip_source::*; - -mod clip_source; -pub use clip_source::*; - -mod cache; -pub use cache::*; - -mod pre_buffer; -pub use pre_buffer::*; - -mod looper; -pub use looper::*; - -mod recorder; -pub use recorder::*; - -pub mod time_stretcher; -pub use time_stretcher::*; - -pub mod resampler; -pub use resampler::*; - -mod chain; -pub use chain::*; - -mod interaction_handler; -pub use interaction_handler::*; - -mod start_end_handler; -pub use start_end_handler::*; - -mod amplifier; -pub use amplifier::*; - -mod section; -pub use section::*; - -mod downbeat; -pub use downbeat::*; - -mod midi_note_tracker; -pub use midi_note_tracker::*; - -mod fade_util; - -mod midi_util; - -mod api; -pub use api::*; - -mod audio_util; - -mod log_util; - -mod time_series; - -mod midi_sequence; -pub use midi_sequence::*; diff --git a/playtime-clip-engine/src/rt/supplier/pre_buffer.rs b/playtime-clip-engine/src/rt/supplier/pre_buffer.rs deleted file mode 100644 index 11ffbabc3..000000000 --- a/playtime-clip-engine/src/rt/supplier/pre_buffer.rs +++ /dev/null @@ -1,1037 +0,0 @@ -use crate::rt::buffer::{AudioBufMut, OwnedAudioBuffer}; -use crate::rt::supplier::{ - AudioMaterialInfo, AudioSupplier, AutoDelegatingMidiSilencer, AutoDelegatingMidiSupplier, - AutoDelegatingPositionTranslationSkill, MaterialInfo, PreBufferFillRequest, - PreBufferSourceSkill, SupplyAudioRequest, SupplyRequestInfo, SupplyResponse, - SupplyResponseStatus, WithMaterialInfo, WithSupplier, -}; -use crate::ClipEngineResult; -use core::cmp; -use crossbeam_channel::{Receiver, Sender, TryRecvError}; -use derive_more::Display; - -use rtrb::{Consumer, Producer, RingBuffer}; -use std::collections::HashMap; -use std::fmt::Debug; -use std::hash::BuildHasherDefault; -use std::marker::PhantomData; -use std::ops::Range; -use std::sync::atomic; -use std::sync::atomic::AtomicUsize; -use std::time::Duration; -use std::{iter, thread}; - -#[derive(Debug)] -pub struct PreBuffer { - id: PreBufferInstanceId, - enabled: bool, - state: State, - request_sender: Sender>, - supplier: S, - options: PreBufferOptions, - command_processor: F, -} - -pub trait CommandProcessor { - type Supplier; - type Command; - - fn process_command(&self, command: Self::Command, supplier: &Self::Supplier); -} - -#[derive(Debug)] -enum State { - Inactive, - Active(ActiveState), -} - -impl State { - pub fn is_active(&self) -> bool { - matches!(self, State::Active(_)) - } -} - -#[derive(Debug)] -struct ActiveState { - consumer: Consumer, - cached_material_info: AudioMaterialInfo, -} - -impl ActiveState { - /// A successful result means that the complete request could be satisfied using pre-buffered - /// blocks. An error means that some frames are left to be filled. It contains the frame offset. - pub fn use_pre_buffers_as_far_as_possible( - &mut self, - request: &SupplyAudioRequest, - dest_buffer: &mut AudioBufMut, - initial_frame_offset: usize, - request_sender: &Sender>, - ) -> Result { - let mut frame_offset = initial_frame_offset; - loop { - let outcome = self.step(request, dest_buffer, frame_offset, request_sender)?; - use StepSuccess::*; - match outcome { - Finished(response) => { - return Ok(response); - } - ContinueWithFrameOffset(new_offset) => { - frame_offset = new_offset; - } - } - } - } - - /// A successful result means that a matching pre-buffered block was found and fulfilled at - /// least a part of the request. - pub fn step( - &mut self, - request: &SupplyAudioRequest, - dest_buffer: &mut AudioBufMut, - frame_offset: usize, - request_sender: &Sender>, - ) -> Result { - let mut remaining_dest_buffer = dest_buffer.slice_mut(frame_offset..); - let criteria = MatchCriteria { - start_frame: request.start_frame + frame_offset as isize, - desired_frame_count: remaining_dest_buffer.frame_count(), - }; - // Try to fill at least the beginning of the remaining portion of the requested material - // with the next available pre-buffered block. - let block = self.consumer.peek().map_err(|_| StepFailure { - frame_offset, - non_matching_block_count: 0, - })?; - let apply_result = block.try_apply_to(&mut remaining_dest_buffer, &criteria); - // Evaluate peek result - match apply_result { - Ok(apply_outcome) => { - // Consume block if exhausted. - if apply_outcome.block_exhausted { - let block = self.consumer.pop().unwrap(); - request_sender.recycle_block(block); - } - let success = process_pre_buffered_response( - dest_buffer, - apply_outcome.partial_response, - frame_offset, - ); - Ok(success) - } - Err(_) => { - // We just left the super happy path. - // Let's check not just the next available block but all available blocks. - // Don't consume immediately! We might run into the situation that no block matches - // and consuming immediately would make the producer produce further probably - // unnecessary blocks. We defer consumption until we know what's going on. - let slots = self.consumer.slots(); - let read_chunk = self.consumer.read_chunk(slots).unwrap(); - let (slice_one, slice_two) = read_chunk.as_slices(); - let outcome = slice_one - .iter() - .chain(slice_two.iter()) - .enumerate() - // We already checked the first block. - // Important to have the skip after the enumerate because then i starts at 1. - .skip(1) - .find_map(|(i, b)| { - let outcome = b.try_apply_to(&mut remaining_dest_buffer, &criteria).ok()?; - Some((i, outcome)) - }); - match outcome { - None => { - // No block matched. Sad path. - debug!("No block matched."); - let failure = StepFailure { - frame_offset, - non_matching_block_count: slots, - }; - Err(failure) - } - Some((i, outcome)) => { - // Found a matching block and applied it! - debug!( - "Found matching block after searching {} of {} additional slot(s).", - i, - slots - 1 - ); - // At first recycle blocks. - let num_blocks_to_be_consumed = if outcome.block_exhausted { - // Including the matched one - i + 1 - } else { - // Not including the matched one - i - }; - self.recycle_next_n_blocks(num_blocks_to_be_consumed, request_sender); - let success = process_pre_buffered_response( - dest_buffer, - outcome.partial_response, - frame_offset, - ); - Ok(success) - } - } - } - } - } - - pub fn pre_buffer( - &mut self, - args: PreBufferFillRequest, - instance_id: PreBufferInstanceId, - request_sender: &Sender>, - ) { - // Not sufficiently thought about what to do if consumer wants to pre-buffer from a negative - // start frame. Probably normalization to 0 because we know we the downbeat handler is - // above us. Let's see. - debug_assert!(args.start_frame >= 0); - request_sender.keep_filling(instance_id, args); - self.recycle_next_n_blocks(self.consumer.slots(), request_sender); - } - - pub fn recycle_next_n_blocks( - &mut self, - count: usize, - request_sender: &Sender>, - ) { - for block in self.consumer.read_chunk(count).unwrap().into_iter() { - request_sender.recycle_block(block); - } - } -} - -#[derive(Debug)] -pub struct PreBufferOptions { - /// If we know the underlying supplier doesn't deliver count-in material, we should set this to - /// `true`. An important optimization that prevents unnecessary supplier queries. - pub skip_count_in_phase_material: bool, - pub cache_miss_behavior: PreBufferCacheMissBehavior, - /// Doesn't seem to work well. - pub recalibrate_on_cache_miss: bool, -} - -/// Decides what to do if the pre-buffer doesn't contain usable data. -#[derive(Copy, Clone, Debug)] -pub enum PreBufferCacheMissBehavior { - /// Simply outputs silence. - /// - /// Safest but also the most silent option ;) - OutputSilence, - /// Falls back to querying the supplier directly. - /// - /// It's risky: - /// - /// - Might block due to mutex contention (if the underlying supplier is a mutex) - /// - Might block due to file system access - QuerySupplierEvenIfContended, - /// Falls back to querying the supplier only if it's uncontended. - /// - /// Still risky: - /// - /// - Might block due to file system access - QuerySupplierIfUncontended, -} - -trait PreBufferSender { - type Supplier; - type Command; - - fn register_instance( - &self, - id: PreBufferInstanceId, - producer: Producer, - supplier: Self::Supplier, - ); - - fn unregister_instance(&self, id: PreBufferInstanceId); - - fn recycle_block(&self, block: PreBufferedBlock); - - fn keep_filling(&self, id: PreBufferInstanceId, args: PreBufferFillRequest); - - fn send_command(&self, id: PreBufferInstanceId, command: Self::Command); - - fn send_request(&self, request: PreBufferRequest); -} - -#[derive(Debug)] -pub struct PreBufferedBlock { - start_frame: isize, - buffer: OwnedAudioBuffer, - response: SupplyResponse, -} - -struct MatchCriteria { - start_frame: isize, - /// This is just a wish. We are also satisfied if the block offers less frames. - desired_frame_count: usize, -} - -#[derive(Copy, Clone, Debug)] -#[allow(clippy::enum_variant_names)] -enum MatchError { - /// Start frame of the pre-buffered block is in the future but all of its material would - /// belong into the requested block. - BlockContainsOnlyRelevantMaterialButStartFrameIsInFuture, - /// Start frame of the pre-buffered block is in the future and it contains material that - /// doesn't belong into the requested block. - BlockContainsFutureMaterial, - /// All material in the pre-buffered block is in the past. - BlockContainsOnlyPastMaterial, -} - -#[derive(Debug)] -pub enum PreBufferRequest { - RegisterInstance { - id: PreBufferInstanceId, - producer: Producer, - supplier: S, - }, - UnregisterInstance(PreBufferInstanceId), - Recycle(PreBufferedBlock), - KeepFillingFrom { - id: PreBufferInstanceId, - args: crate::rt::supplier::PreBufferFillRequest, - }, - SendCommand(PreBufferInstanceId, C), -} - -#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Display)] -pub struct PreBufferInstanceId(usize); - -impl PreBufferInstanceId { - pub fn next() -> Self { - static COUNTER: AtomicUsize = AtomicUsize::new(0); - Self(COUNTER.fetch_add(1, atomic::Ordering::SeqCst)) - } -} - -impl WithSupplier for PreBuffer { - type Supplier = S; - - fn supplier(&self) -> &Self::Supplier { - &self.supplier - } - - fn supplier_mut(&mut self) -> &mut Self::Supplier { - &mut self.supplier - } -} - -impl PreBuffer -where - S: AudioSupplier + Clone + Send + 'static, - F: CommandProcessor, -{ - /// Don't call in real-time thread. - pub fn new( - supplier: S, - request_sender: Sender>, - options: PreBufferOptions, - command_processor: F, - ) -> Self { - Self { - id: PreBufferInstanceId::next(), - enabled: false, - state: State::Inactive, - request_sender, - supplier, - options, - command_processor, - } - } - - pub fn send_command(&self, command: C) { - if !self.enabled { - self.command_processor - .process_command(command, &self.supplier); - return; - } - match &self.state { - State::Inactive => { - // When inactive, we process the command synchronously. Fast and straightforward. - self.command_processor - .process_command(command, &self.supplier); - } - State::Active(_) => { - // When enabled, we let a worker thread to the work because accessing the supplier - // might take too long for doing it in a real-time thread (either because the - // operation itself is expensive or because we might need to obtain a lock in a - // blocking way because the worker is currently using the supplier as well). - self.request_sender.send_command(self.id, command); - } - } - } - - pub fn set_enabled(&mut self, enabled: bool) { - self.enabled = enabled; - } - - /// # Errors - /// - /// Returns an error if the material can't or doesn't need to be buffered. In that case - /// it just leaves the pre-buffer disabled. - pub fn activate(&mut self) -> ClipEngineResult<()> { - if !self.enabled { - return Err("disabled"); - } - if !self.state.is_active() { - return Err("inactive"); - } - let audio_material_info = require_audio_material_info(self.supplier.material_info()?)?; - let (producer, consumer) = RingBuffer::new(RING_BUFFER_BLOCK_COUNT); - self.request_sender - .register_instance(self.id, producer, self.supplier.clone()); - let enabled_state = ActiveState { - consumer, - cached_material_info: audio_material_info, - }; - self.state = State::Active(enabled_state); - Ok(()) - } - - pub fn deactivate(&mut self) { - if !self.enabled || !self.state.is_active() { - return; - } - self.request_sender.unregister_instance(self.id); - self.state = State::Inactive; - } - - /// Invalidates the material info cache. - /// - /// This should be called whenever the underlying material info might change. However, it - /// accesses the supplier and therefore should be used with care (especially if the supplier - /// is a mutex). - pub fn invalidate_material_info_cache(&mut self) -> ClipEngineResult<()> { - if !self.enabled { - return Err("disabled"); - } - match &mut self.state { - State::Inactive => Err("inactive"), - State::Active(s) => { - let audio_material_info = - require_audio_material_info(self.supplier.material_info()?)?; - s.cached_material_info = audio_material_info; - Ok(()) - } - } - } -} - -enum StepSuccess { - Finished(SupplyResponse), - ContinueWithFrameOffset(usize), -} - -struct StepFailure { - /// Position in the block until which material was filled so far. - frame_offset: usize, - /// If this > 0, this means that blocks were available but none of them matched. - non_matching_block_count: usize, -} - -enum SkipCountInPhaseOutcome { - PureCountIn(SupplyResponse), - /// Partial count-in or no count-in (in latter case the frame offset is 0). - StartWithFrameOffset(usize), -} - -struct ApplyOutcome { - partial_response: SupplyResponse, - /// If not exhausted, the pre-buffered block still holds future material. - block_exhausted: bool, -} - -impl PreBufferSourceSkill for PreBuffer -where - S: AudioSupplier + Clone + Send + 'static, - F: Debug + CommandProcessor, - C: Debug, -{ - fn pre_buffer(&mut self, args: PreBufferFillRequest) { - if !self.enabled { - return; - } - match &mut self.state { - State::Inactive => {} - State::Active(s) => { - s.pre_buffer(args, self.id, &self.request_sender); - } - } - } -} - -impl AudioSupplier for PreBuffer -where - S: AudioSupplier + Clone + Send + 'static, - F: Debug + CommandProcessor, - C: Debug, -{ - fn supply_audio( - &mut self, - request: &SupplyAudioRequest, - dest_buffer: &mut AudioBufMut, - ) -> SupplyResponse { - if !self.enabled { - return self.supplier.supply_audio(request, dest_buffer); - } - let state = match &mut self.state { - State::Inactive => { - // Inactive means we may access the supplier directly. - return self.supplier.supply_audio(request, dest_buffer); - } - State::Active(s) => s, - }; - #[cfg(debug_assertions)] - { - request.assert_wants_source_frame_rate(state.cached_material_info.frame_rate); - } - let initial_frame_offset = if self.options.skip_count_in_phase_material { - // Return silence until frame 0 reached, if allowed. - use SkipCountInPhaseOutcome::*; - match skip_count_in_phase(request.start_frame, dest_buffer) { - PureCountIn(response) => return response, - StartWithFrameOffset(initial_frame_offset) => initial_frame_offset, - } - } else { - 0 - }; - // Get the material. - // - Happy path: The next pre-buffered blocks match and we consume them if fully exhausted. - // - Almost happy path: There are some non-matching pre-buffered blocks in the way and we - // need to get rid of them until we finally reach matching blocks again that satisfy our - // request. - // - Sad path: Not enough matching pre-buffered blocks are there to satisfy our request. - // When we realize it, we fill up the remaining material by querying the supplier. - // In addition, we send a new pre-buffer request to hopefully can go the happy path next - // time. Also, we consume all blocks because they are useless. - match state.use_pre_buffers_as_far_as_possible( - request, - dest_buffer, - initial_frame_offset, - &self.request_sender, - ) { - Ok(response) => response, - Err(step_failure) => { - use PreBufferCacheMissBehavior::*; - let response = match self.options.cache_miss_behavior { - OutputSilence => { - let mut remaining_dest_buffer = - dest_buffer.slice_mut(step_failure.frame_offset..); - remaining_dest_buffer.clear(); - SupplyResponse::please_continue( - step_failure.frame_offset + remaining_dest_buffer.frame_count(), - ) - } - QuerySupplierEvenIfContended => query_supplier_for_remaining_portion( - request, - dest_buffer, - step_failure.frame_offset, - &mut self.supplier, - ), - QuerySupplierIfUncontended => unimplemented!(), - }; - if step_failure.non_matching_block_count > 0 { - // We found non-matching blocks. - // First, we can assume that the pre-buffer worker somehow is somehow on the - // wrong track. "Recalibrate" it. - if self.options.recalibrate_on_cache_miss { - let fill_request = PreBufferFillRequest { - start_frame: calculate_next_reasonable_frame( - request.start_frame, - &response, - ), - }; - state.pre_buffer(fill_request, self.id, &self.request_sender); - } - // Second, let's drain all non-matching blocks. Not useful! - state.recycle_next_n_blocks( - step_failure.non_matching_block_count, - &self.request_sender, - ); - } - response - } - } - } -} - -impl WithMaterialInfo for PreBuffer { - fn material_info(&self) -> ClipEngineResult { - if !self.enabled { - return self.supplier.material_info(); - } - match &self.state { - State::Inactive => self.supplier.material_info(), - State::Active(s) => Ok(MaterialInfo::Audio(s.cached_material_info.clone())), - } - } -} - -struct PreBufferWorker { - instances: - HashMap, BuildHasherDefault>, - spare_buffer_chunks: Vec>, - command_processor: F, - phantom: PhantomData, -} - -impl PreBufferWorker -where - S: AudioSupplier + WithMaterialInfo, - F: CommandProcessor, -{ - pub fn process_request(&mut self, request: PreBufferRequest) { - use PreBufferRequest::*; - match request { - RegisterInstance { - id, - producer, - supplier, - } => { - let instance = Instance { - producer, - supplier, - state: InstanceState::Initialized, - }; - self.register_instance(id, instance); - } - UnregisterInstance(id) => { - self.unregister_instance(id); - } - Recycle(block) => { - self.recycle(block); - } - KeepFillingFrom { id, args } => { - let _ = self.keep_filling_from(id, args); - } - SendCommand(id, command) => { - let instance = match self.instances.get(&id) { - None => return, - Some(i) => i, - }; - self.command_processor - .process_command(command, &instance.supplier); - } - } - } - - pub fn fill_all(&mut self) { - let spare_buffer_chunks = &mut self.spare_buffer_chunks; - let mut get_spare_buffer = |channel_count: usize| { - iter::repeat_with(|| spare_buffer_chunks.pop()) - .take_while(|buffer| buffer.is_some()) - .flatten() - .find_map(|chunk| { - OwnedAudioBuffer::try_recycle(chunk, channel_count, PRE_BUFFERED_BLOCK_LENGTH) - .ok() - }) - .unwrap_or_else(|| OwnedAudioBuffer::new(channel_count, PRE_BUFFERED_BLOCK_LENGTH)) - }; - self.instances.retain(|_, instance| { - let outcome = instance.fill(&mut get_spare_buffer); - // Unregister instance if consumer gone. - !matches!(outcome, Err(FillError::ConsumerGone)) - }); - } - - fn register_instance(&mut self, id: PreBufferInstanceId, instance: Instance) { - self.instances.insert(id, instance); - } - - fn unregister_instance(&mut self, id: PreBufferInstanceId) { - self.instances.remove(&id); - } - - fn recycle(&mut self, block: PreBufferedBlock) { - self.spare_buffer_chunks.push(block.buffer.into_inner()); - } - - fn keep_filling_from( - &mut self, - id: PreBufferInstanceId, - args: PreBufferFillRequest, - ) -> ClipEngineResult<()> { - debug!("Pre-buffer request for instance {}: {:?}", id, &args); - let instance = self - .instances - .get_mut(&id) - .ok_or("instance doesn't exist")?; - let filling_state = FillingState { - next_start_frame: args.start_frame, - }; - instance.state = InstanceState::Filling(filling_state); - Ok(()) - } -} - -struct Instance { - producer: Producer, - supplier: S, - state: InstanceState, -} - -impl Instance { - pub fn fill( - &mut self, - mut get_spare_buffer: impl FnMut(usize) -> OwnedAudioBuffer, - ) -> Result<(), FillError> { - if self.producer.is_abandoned() { - return Err(FillError::ConsumerGone); - } - if self.producer.is_full() { - return Err(FillError::Full); - } - use InstanceState::*; - let state = match &mut self.state { - Initialized => return Err(FillError::NotFilling), - Filling(s) => s, - }; - let material_info = self - .supplier - .material_info() - .map_err(|_| FillError::MaterialUnavailable)?; - let source_channel_count = material_info.channel_count(); - let mut buffer = get_spare_buffer(source_channel_count); - let request = SupplyAudioRequest { - start_frame: state.next_start_frame, - dest_sample_rate: None, - info: SupplyRequestInfo { - audio_block_frame_offset: 0, - requester: "pre-buffer", - note: "", - is_realtime: false, - }, - parent_request: None, - general_info: &Default::default(), - }; - let response = self - .supplier - .supply_audio(&request, &mut buffer.to_buf_mut()); - let block = PreBufferedBlock { - start_frame: state.next_start_frame, - buffer, - response, - }; - state.next_start_frame = calculate_next_reasonable_frame(state.next_start_frame, &response); - // dbg!(&block); - self.producer - .push(block) - .expect("ring buffer should not be full"); - Ok(()) - } -} - -fn calculate_next_reasonable_frame(current_frame: isize, response: &SupplyResponse) -> isize { - use SupplyResponseStatus::*; - match response.status { - PleaseContinue => current_frame + response.num_frames_consumed as isize, - // Starting over pre-buffering the start is a good default. - ReachedEnd { .. } => 0, - } -} - -enum FillError { - NotFilling, - ConsumerGone, - Full, - MaterialUnavailable, -} - -enum InstanceState { - Initialized, - Filling(FillingState), -} - -struct FillingState { - next_start_frame: isize, -} - -pub fn keep_processing_pre_buffer_requests( - receiver: Receiver>, - command_processor: impl CommandProcessor, -) where - S: AudioSupplier, -{ - let mut worker = PreBufferWorker { - instances: Default::default(), - spare_buffer_chunks: vec![], - command_processor, - phantom: PhantomData, - }; - loop { - // At first take every incoming request serious so we can fill based on up-to-date demands. - loop { - match receiver.try_recv() { - Ok(request) => { - worker.process_request(request); - } - Err(e) => { - use TryRecvError::*; - match e { - Empty => break, - Disconnected => return, - } - } - } - } - // Then write more audio into ring buffers. - worker.fill_all(); - // Don't spin like crazy - thread::sleep(Duration::from_millis(1)); - } -} - -fn process_pre_buffered_response( - dest_buffer: &mut AudioBufMut, - step_response: SupplyResponse, - frame_offset: usize, -) -> StepSuccess { - use SupplyResponseStatus::*; - let next_frame_offset = frame_offset + step_response.num_frames_consumed; - // Finish if end of source material reached. - if step_response.status.reached_end() { - debug!("pre-buffer: COMPLETE end!"); - let finished_response = SupplyResponse { - num_frames_consumed: next_frame_offset, - status: ReachedEnd { - num_frames_written: next_frame_offset, - }, - }; - return StepSuccess::Finished(finished_response); - } - // Finish if block filled to satisfaction. - debug_assert!(next_frame_offset <= dest_buffer.frame_count()); - if next_frame_offset == dest_buffer.frame_count() { - // println!( - // "Finished with frame offset = frame count = num_frames_consumed {}", - // frame_offset - // ); - // Satisfied complete block and supplier still has material. - let finished_response = SupplyResponse { - num_frames_consumed: dest_buffer.frame_count(), - status: PleaseContinue, - }; - return StepSuccess::Finished(finished_response); - } - StepSuccess::ContinueWithFrameOffset(next_frame_offset) -} - -// Unusable. Misses start at 1x 150 bpm or so. -// const PRE_BUFFERED_BLOCK_LENGTH: usize = 128; -// const RING_BUFFER_BLOCK_COUNT: usize = 375; - -// Quite stable. Misses start at 2x 960 bpm. -// Double block count doesn't help. -// const PRE_BUFFERED_BLOCK_LENGTH: usize = 2048; -// const RING_BUFFER_BLOCK_COUNT: usize = 4; - -// Quite stable. No misses. -// const PRE_BUFFERED_BLOCK_LENGTH: usize = 4096; -// const RING_BUFFER_BLOCK_COUNT: usize = 4; - -// Quite stable. No misses. THIS IS THE BEST! -const PRE_BUFFERED_BLOCK_LENGTH: usize = 4096; -const RING_BUFFER_BLOCK_COUNT: usize = 2; - -// Quite stable. Misses start at 3x 960 bpm. -// const PRE_BUFFERED_BLOCK_LENGTH: usize = 4096; -// const RING_BUFFER_BLOCK_COUNT: usize = 1; - -impl PreBufferSender for Sender> { - type Supplier = S; - type Command = C; - - fn register_instance( - &self, - id: PreBufferInstanceId, - producer: Producer, - supplier: Self::Supplier, - ) { - let request = PreBufferRequest::RegisterInstance { - id, - producer, - supplier, - }; - self.send_request(request); - } - - fn unregister_instance(&self, id: PreBufferInstanceId) { - let request = PreBufferRequest::UnregisterInstance(id); - self.send_request(request); - } - - fn recycle_block(&self, block: PreBufferedBlock) { - let request = PreBufferRequest::Recycle(block); - self.send_request(request); - } - - fn keep_filling(&self, id: PreBufferInstanceId, args: PreBufferFillRequest) { - let request = PreBufferRequest::KeepFillingFrom { id, args }; - self.send_request(request); - } - - fn send_command(&self, id: PreBufferInstanceId, command: Self::Command) { - self.send_request(PreBufferRequest::SendCommand(id, command)); - } - - fn send_request(&self, request: PreBufferRequest) { - self.try_send(request).unwrap(); - } -} -impl PreBufferedBlock { - fn try_apply_to( - &self, - remaining_dest_buffer: &mut AudioBufMut, - criteria: &MatchCriteria, - ) -> Result { - let range = self.matches(criteria)?; - // Pre-buffered block available and matches. - Ok(self.copy_range_to(remaining_dest_buffer, range)) - } - - /// Checks whether this block contains at least the given start position and hopefully more. - /// - /// It returns the range of this block which can be used to fill the - fn matches(&self, criteria: &MatchCriteria) -> Result, MatchError> { - let self_buffer = self.buffer.to_buf(); - // At this point we know the channel count is correct. - let offset = criteria.start_frame - self.start_frame; - if offset < 0 { - // Start frame is in the future. - let block_end = offset + criteria.desired_frame_count as isize; - let err = if block_end < self_buffer.frame_count() as isize { - MatchError::BlockContainsFutureMaterial - } else { - MatchError::BlockContainsOnlyRelevantMaterialButStartFrameIsInFuture - }; - return Err(err); - } - let offset = offset as usize; - // At this point we know the start frame is in the past or spot-on. - let num_available_frames = self_buffer.frame_count() as isize - offset as isize; - if num_available_frames <= 0 { - return Err(MatchError::BlockContainsOnlyPastMaterial); - } - let num_available_frames = num_available_frames as usize; - // At this point we know we have usable material. - let length = cmp::min(criteria.desired_frame_count, num_available_frames); - Ok(offset..(offset + length)) - } - - fn copy_range_to( - &self, - remaining_dest_buffer: &mut AudioBufMut, - range: Range, - ) -> ApplyOutcome { - let pre_buf = self.buffer.to_buf(); - debug_assert!(range.end <= pre_buf.frame_count()); - // Check if we reached end. - use SupplyResponseStatus::*; - let (clamped_range_end, reached_end) = match self.response.status { - PleaseContinue => { - // Pre-buffered block doesn't contain end. - (range.end, false) - } - ReachedEnd { num_frames_written } => { - // Pre-buffered block contains end. - if range.end < num_frames_written { - // But requested block is not there yet. - (range.end, false) - } else { - // Requested block reached end. - (num_frames_written, true) - } - } - }; - // Copy material from pre-buffered block to destination buffer - let range = range.start..clamped_range_end; - let sliced_src_buffer = pre_buf.slice(range.clone()); - let mut sliced_dest_buffer = remaining_dest_buffer.slice_mut(0..range.len()); - sliced_src_buffer.copy_to(&mut sliced_dest_buffer); - // Express outcome - ApplyOutcome { - partial_response: SupplyResponse { - num_frames_consumed: range.len(), - status: if reached_end { - ReachedEnd { - num_frames_written: range.len(), - } - } else { - PleaseContinue - }, - }, - block_exhausted: reached_end || range.end == pre_buf.frame_count(), - } - } -} - -fn require_audio_material_info(material_info: MaterialInfo) -> ClipEngineResult { - match material_info { - MaterialInfo::Audio(i) => Ok(i), - MaterialInfo::Midi(_) => { - Err("supplier provides MIDI material which doesn't need to be pre-buffered") - } - } -} - -/// This is an optimization we *can* (and should) apply only because we know we sit right -/// above the source, which by definition doesn't have any material in the count-in phase. -fn skip_count_in_phase( - start_frame: isize, - dest_buffer: &mut AudioBufMut, -) -> SkipCountInPhaseOutcome { - if start_frame >= 0 { - // Not in count-in phase. - return SkipCountInPhaseOutcome::StartWithFrameOffset(0); - } - let num_frames_until_zero = -start_frame as usize; - let num_frames_to_be_silenced = cmp::min(num_frames_until_zero, dest_buffer.frame_count()); - dest_buffer.slice_mut(0..num_frames_to_be_silenced).clear(); - if num_frames_to_be_silenced == dest_buffer.frame_count() { - // Pure count-in. - let response = SupplyResponse::please_continue(num_frames_to_be_silenced); - SkipCountInPhaseOutcome::PureCountIn(response) - } else { - SkipCountInPhaseOutcome::StartWithFrameOffset(num_frames_to_be_silenced) - } -} -fn query_supplier_for_remaining_portion( - request: &SupplyAudioRequest, - dest_buffer: &mut AudioBufMut, - frame_offset: usize, - supplier: &mut S, -) -> SupplyResponse { - let inner_request = SupplyAudioRequest { - start_frame: request.start_frame + frame_offset as isize, - dest_sample_rate: request.dest_sample_rate, - info: SupplyRequestInfo { - audio_block_frame_offset: request.info.audio_block_frame_offset + frame_offset, - requester: "pre-buffer-fallback", - note: "", - is_realtime: false, - }, - parent_request: Some(request), - general_info: request.general_info, - }; - let mut remaining_dest_buffer = dest_buffer.slice_mut(frame_offset..); - let inner_response = supplier.supply_audio(&inner_request, &mut remaining_dest_buffer); - use SupplyResponseStatus::*; - let num_frames_consumed = frame_offset + inner_response.num_frames_consumed; - // rt_debug!("pre-buffer: fallback"); - SupplyResponse { - num_frames_consumed, - status: match inner_response.status { - PleaseContinue => PleaseContinue, - ReachedEnd { .. } => ReachedEnd { - num_frames_written: num_frames_consumed, - }, - }, - } -} - -impl AutoDelegatingMidiSupplier for PreBuffer {} -impl AutoDelegatingPositionTranslationSkill for PreBuffer {} -impl AutoDelegatingMidiSilencer for PreBuffer {} diff --git a/playtime-clip-engine/src/rt/supplier/reaper_clip_source.rs b/playtime-clip-engine/src/rt/supplier/reaper_clip_source.rs deleted file mode 100644 index 480be7580..000000000 --- a/playtime-clip-engine/src/rt/supplier/reaper_clip_source.rs +++ /dev/null @@ -1,217 +0,0 @@ -use crate::conversion_util::{ - adjust_proportionally_positive, convert_duration_in_frames_to_seconds, - convert_duration_in_seconds_to_frames, convert_position_in_frames_to_seconds, -}; -use crate::rt::buffer::AudioBufMut; -use crate::rt::source_util::pcm_source_is_midi; -use crate::rt::supplier::audio_util::{supply_audio_material, AudioSourceMaterialRequest}; -use crate::rt::supplier::log_util::print_distance_from_beat_start_at; -use crate::rt::supplier::{ - AudioMaterialInfo, AudioSupplier, CacheableSource, MaterialInfo, MidiMaterialInfo, - MidiSupplier, SupplyAudioRequest, SupplyMidiRequest, SupplyResponse, WithCacheableSource, - WithMaterialInfo, MIDI_BASE_BPM, MIDI_FRAME_RATE, -}; -use crate::ClipEngineResult; -use reaper_medium::{ - BorrowedMidiEventList, BorrowedPcmSource, DurationInSeconds, Hz, OwnedPcmSource, - PcmSourceTransfer, -}; -use std::path::{Path, PathBuf}; -use tracing::Level; - -#[derive(Clone, Debug)] -pub struct ReaperClipSource { - source_file: Option, - source: OwnedPcmSource, -} - -impl ReaperClipSource { - pub fn new(reaper_source: OwnedPcmSource) -> Self { - Self { - source_file: reaper_source.get_file_name(|p| Some(p?.to_path_buf())), - source: reaper_source, - } - } - - pub fn reaper_source(&self) -> &BorrowedPcmSource { - &self.source - } - - pub fn into_reaper_source(self) -> OwnedPcmSource { - self.source - } - - fn get_audio_source_frame_rate(&self) -> Hz { - self.source - .get_sample_rate() - .expect("audio source should expose frame rate") - } - - fn transfer_audio(&self, req: AudioSourceMaterialRequest) -> SupplyResponse { - let source_sample_rate = self.source.get_sample_rate().unwrap(); - let time_s = convert_duration_in_frames_to_seconds(req.start_frame, source_sample_rate); - let num_frames_written = unsafe { - let mut transfer = PcmSourceTransfer::default(); - // Both channel count and sample rate should be the one from the source itself! - transfer.set_nch(self.get_audio_source_channel_count() as _); - transfer.set_sample_rate(source_sample_rate); - // The rest depends on the given parameters - transfer.set_length(req.dest_buffer.frame_count() as _); - transfer.set_samples(req.dest_buffer.data_as_mut_ptr()); - transfer.set_time_s(time_s.into()); - self.source.get_samples(&transfer); - transfer.samples_out() as usize - }; - // The lower the sample rate, the higher the tempo, the more inner source material we - // effectively grabbed. - SupplyResponse::limited_by_total_frame_count( - num_frames_written, - num_frames_written, - req.start_frame as isize, - self.calculate_audio_frame_count(source_sample_rate), - ) - } - - fn get_audio_source_channel_count(&self) -> usize { - self.source - .get_num_channels() - .expect("audio source should report channel count") as usize - } - - fn calculate_audio_frame_count(&self, sample_rate: Hz) -> usize { - let length_in_seconds = self.source.get_length().unwrap_or(DurationInSeconds::ZERO); - convert_duration_in_seconds_to_frames(length_in_seconds, sample_rate) - } - - fn calculate_midi_frame_count(&self) -> usize { - let length_in_seconds = if let Some(length_in_beats) = self.source.get_length_beats() { - // For MIDI, get_length() takes the current project tempo in account ... which is not - // what we want because we want to do all the tempo calculations ourselves and treat - // MIDI/audio the same wherever possible. - let beats_per_minute = MIDI_BASE_BPM; - let beats_per_second = beats_per_minute.get() / 60.0; - DurationInSeconds::new(length_in_beats.get() / beats_per_second) - } else { - // If we don't get a length in beats, this either means we have set a preview tempo - // on the source or the source has IGNTEMPO set to 1. Either way we will take the - // reported length. Usually we end up here because we set a preview tempo. - self.source.get_length().unwrap() - }; - convert_duration_in_seconds_to_frames(length_in_seconds, MIDI_FRAME_RATE) - } -} - -impl AudioSupplier for ReaperClipSource { - fn supply_audio( - &mut self, - request: &SupplyAudioRequest, - dest_buffer: &mut AudioBufMut, - ) -> SupplyResponse { - let source_frame_rate = self.get_audio_source_frame_rate(); - supply_audio_material(request, dest_buffer, source_frame_rate, |req| { - self.transfer_audio(req) - }) - } -} - -impl WithMaterialInfo for ReaperClipSource { - fn material_info(&self) -> ClipEngineResult { - let info = if pcm_source_is_midi(&self.source) { - let info = MidiMaterialInfo { - frame_count: self.calculate_midi_frame_count(), - }; - MaterialInfo::Midi(info) - } else { - let sample_rate = self.get_audio_source_frame_rate(); - let info = AudioMaterialInfo { - channel_count: self.get_audio_source_channel_count(), - frame_count: self.calculate_audio_frame_count(sample_rate), - frame_rate: sample_rate, - }; - MaterialInfo::Audio(info) - }; - Ok(info) - } -} - -impl MidiSupplier for ReaperClipSource { - // Below logic assumes that the destination frame rate is comparable to the source frame - // rate. The resampler makes sure of it. However, it's not necessarily equal since we use - // frame rate changes for tempo changes. It's only equal if the clip is played in - // MIDI_BASE_BPM. That's fine! - fn supply_midi( - &mut self, - request: &SupplyMidiRequest, - event_list: &mut BorrowedMidiEventList, - ) -> SupplyResponse { - let frame_rate = request.dest_sample_rate; - let num_frames_to_be_consumed = request.dest_frame_count; - // Do some logging - if tracing::enabled!(Level::DEBUG) { - if request.start_frame == 0 { - print_distance_from_beat_start_at(request, 0, "(MIDI, start_frame = 0)"); - } else if request.start_frame < 0 - && (request.start_frame + num_frames_to_be_consumed as isize) >= 0 - { - let distance_to_zero_in_midi_frames = (-request.start_frame) as usize; - let ratio = request.dest_frame_count as f64 / num_frames_to_be_consumed as f64; - let distance_to_zero_in_dest_frames = - adjust_proportionally_positive(distance_to_zero_in_midi_frames as f64, ratio); - print_distance_from_beat_start_at( - request, - distance_to_zero_in_dest_frames, - "(MIDI, start_frame < 0)", - ); - } - } - // For MIDI it seems to be okay to start at a negative position. The source - // will ignore positions < 0.0 and add events >= 0.0 with the correct frame - // offset. - let time_s = convert_position_in_frames_to_seconds(request.start_frame, frame_rate); - let num_midi_frames_consumed = unsafe { - let mut transfer = PcmSourceTransfer::default(); - transfer.set_sample_rate(frame_rate); - transfer.set_length(num_frames_to_be_consumed as i32); - transfer.set_time_s(time_s); - transfer.set_midi_event_list(event_list); - self.source.get_samples(&transfer); - // In the past, we did the following in order to deal with on-the-fly tempo changes that - // occur while playing instead of REAPER letting use its generic mechanism that leads - // to repeated notes, probably through internal position changes. - // - // transfer.set_force_bpm(MIDI_BASE_BPM); - // transfer.set_absolute_time_s(PositionInSeconds::ZERO); - // - // However, now we set the constant preview tempo at source creation time, which makes - // the source completely project tempo/pos-independent, also when doing recording via - // midi_realtime_write_struct_t. So that's not necessary anymore - transfer.samples_out() as usize - }; - // The lower the sample rate, the higher the tempo, the more inner source material we - // effectively grabbed. - SupplyResponse::limited_by_total_frame_count( - num_midi_frames_consumed, - num_midi_frames_consumed, - request.start_frame, - self.calculate_midi_frame_count(), - ) - } -} - -impl CacheableSource for ReaperClipSource { - fn file_name(&self) -> Option<&Path> { - self.source_file.as_deref() - } - - fn duplicate(&self) -> Box { - Box::new(self.clone()) - } -} - -impl WithCacheableSource for ReaperClipSource { - type Source = ReaperClipSource; - - fn cacheable_source(&self) -> Option<&Self::Source> { - Some(self) - } -} diff --git a/playtime-clip-engine/src/rt/supplier/recorder.rs b/playtime-clip-engine/src/rt/supplier/recorder.rs deleted file mode 100644 index c0981f390..000000000 --- a/playtime-clip-engine/src/rt/supplier/recorder.rs +++ /dev/null @@ -1,1772 +0,0 @@ -use crate::conversion_util::{ - adjust_proportionally_positive, convert_duration_in_frames_to_other_frame_rate, - convert_duration_in_frames_to_seconds, -}; -use crate::file_util::get_path_for_new_media_file; -use crate::rt::buffer::{AudioBuf, AudioBufMut, OwnedAudioBuffer}; -use crate::rt::schedule_util::{calc_distance_from_pos, calc_distance_from_quantized_pos}; -use crate::rt::supplier::audio_util::{supply_audio_material, transfer_samples_from_buffer}; -use crate::rt::supplier::{ - AudioMaterialInfo, AudioSupplier, MaterialInfo, MidiEventPayload, MidiMaterialInfo, - MidiSequence, MidiSupplier, MidiTimeInfo, PositionTranslationSkill, ReaperClipSource, - RtClipSource, SectionBounds, SupplyAudioRequest, SupplyMidiRequest, SupplyResponse, - WithCacheableSource, WithMaterialInfo, MIDI_BASE_BPM, MIDI_FRAME_RATE, -}; -use crate::rt::{ - BasicAudioRequestProps, OverridableMatrixSettings, QuantizedPosCalcEquipment, RtColumnSettings, -}; - -use crate::timeline::{clip_timeline, Timeline}; -use crate::{ClipEngineResult, HybridTimeline, Laziness, QuantizedPosition}; -use base::tracing_error; -use crossbeam_channel::{Receiver, Sender}; -use helgoboss_midi::{Channel, RawShortMessage, ShortMessage, ShortMessageFactory, U7}; -use playtime_api::persistence::{ - ClipPlayStartTiming, ClipRecordStartTiming, ClipRecordStopTiming, EvenQuantization, - MatrixClipRecordSettings, MidiClipRecordMode, RecordLength, -}; -use reaper_high::{OwnedSource, Project, Reaper}; -use reaper_low::raw::PCM_sink; -use reaper_medium::{ - BorrowedMidiEventList, Bpm, DurationInBeats, DurationInSeconds, Hz, MidiFrameOffset, - MidiImportBehavior, OwnedPcmSink, PositionInSeconds, TimeSignature, -}; -use std::ffi::CString; -use std::path::{Path, PathBuf}; -use std::ptr::{null, null_mut, NonNull}; -use std::time::Duration; -use std::{cmp, mem, thread}; - -// TODO-high-prebuffer In addition we should deploy a start-buffer that always keeps the start completely in -// memory. Because sudden restarts (e.g. retriggers) are the main reason why we could still run -// into a cache miss. That start-buffer should take the downbeat setting into account. It must -// cache everything up to the downbeat + the usual start-buffer samples. It should probably sit -// on top of the pre-buffer and serve samples at the beginning by itself, leaving the pre-buffer -// out of the equation. It should forward pre-buffer requests to the pre-buffer but modify them -// by using the end of the start-buffer cache as the minimum pre-buffer position. - -#[derive(Debug)] -pub struct Recorder { - state: Option, - request_sender: Sender, - response_channel: ResponseChannel, -} - -#[derive(Debug)] -struct ResponseChannel { - sender: Sender, - receiver: Receiver, -} - -impl ResponseChannel { - fn new() -> Self { - let (sender, receiver) = crossbeam_channel::bounded(10); - Self { sender, receiver } - } -} - -#[derive(Debug)] -pub enum RecorderRequest { - RecordAudio(AudioRecordingTask), - DiscardSource(RtClipSource), - DiscardAudioRecordingFinishingData { - temporary_audio_buffer: OwnedAudioBuffer, - file: PathBuf, - old_source: Option, - }, -} - -#[derive(Debug)] -struct AudioRecordingFinishedResponse { - pub source: Result, -} - -#[derive(Debug)] -enum RecorderResponse { - AudioRecordingFinished(AudioRecordingFinishedResponse), -} - -/// State of the recorder. -/// -/// This state is not necessarily synchronous with the clip state. In particular, after a recording, -/// the clip can already be in the Ready state and play the clip while the recorder is still in -/// the Recording state. In that state, the recorder delivers playable material from an in-memory -/// buffer. Until the PCM source is ready. Then it moves to the Ready state. -#[derive(Debug)] -#[allow(clippy::large_enum_variant)] -enum State { - Ready(ReadyState), - Recording(RecordingState), -} - -#[derive(Debug)] -struct ReadyState { - source: RtClipSource, - midi_overdub_settings: Option, -} - -#[derive(Debug)] -pub struct MidiOverdubSettings { - pub mode: MidiClipRecordMode, - pub quantization_settings: Option, - pub mirror_source: MidiSequence, -} - -#[derive(Debug)] -struct RecordingState { - kind_state: KindState, - old_source: Option, - project: Option, - detect_downbeat: bool, - tempo: Bpm, - time_signature: TimeSignature, - start_timing: RecordInteractionTiming, - stop_timing: RecordInteractionTiming, - recording: Option, - length: RecordLength, - committed: bool, - initial_play_start_timing: ClipPlayStartTiming, -} - -#[derive(Clone, Copy, Debug)] -struct Recording { - total_frame_offset: usize, - /// Number of count-in frames. - num_count_in_frames: usize, - frame_rate: Hz, - first_play_frame: Option, - scheduled_end: Option, - latency: usize, -} - -impl Recording { - pub fn is_still_in_count_in_phase(&self) -> bool { - self.total_frame_offset < self.num_count_in_frames - } - - pub fn effective_pos(&self) -> isize { - self.total_frame_offset as isize - self.num_count_in_frames as isize - } - - /// Returns the current frame count or even the final one if an end is scheduled already. - /// - /// Doesn't count the count-in phase frames. - pub fn effective_frame_count(&self) -> usize { - let total_frame_count = if let Some(end) = self.scheduled_end { - end.complete_length - } else { - self.total_frame_offset - }; - total_frame_count.saturating_sub(self.num_count_in_frames) - } - - pub fn calculate_downbeat_frame(&self) -> usize { - let first_play_frame = match self.first_play_frame { - None => return 0, - Some(f) => f, - }; - // In most cases, first_play_frame will be < num_count_in_frames because we stop - // looking for the first play frame as soon as the count-in phase is exceeded. - // However, that's just a performance optimization and we shouldn't rely on it. - // Also, when doing that optimization, we just check if the *start* of the block - // is < num_count_in_frames, but the actual first play frame within that block could - // be > num_count_in_frames! So we must check here again. - if first_play_frame >= self.num_count_in_frames { - return 0; - } - // We detected material that should play at count-in phase - // (also called pick-up beat or anacrusis). So the position of the downbeat in - // the material is greater than zero. - let downbeat_frame = self.num_count_in_frames - first_play_frame; - if !self.downbeat_frame_is_large_enough(downbeat_frame) { - return 0; - } - downbeat_frame - } - - fn downbeat_frame_is_large_enough(&self, downbeat_frame: usize) -> bool { - // TODO-low Maybe better to base this on current tempo and time signature? - let seconds = convert_duration_in_frames_to_seconds(downbeat_frame, self.frame_rate); - // This would be roughly a 8th note with tempo 120. - seconds.get() >= 0.1 - } -} - -// TODO-high-clip-engine The size difference is too high! -#[allow(clippy::large_enum_variant)] -#[derive(Debug)] -enum KindState { - Audio(RecordingAudioState), - Midi(RecordingMidiState), -} - -#[derive(Debug)] -enum RecordingAudioState { - Active(RecordingAudioActiveState), - Finishing(RecordingAudioFinishingState), -} - -impl RecordingAudioState { - pub fn temporary_audio_buffer(&self) -> &OwnedAudioBuffer { - match self { - RecordingAudioState::Active(s) => &s.temporary_audio_buffer, - RecordingAudioState::Finishing(s) => &s.temporary_audio_buffer, - } - } -} - -#[derive(Debug)] -struct RecordingAudioActiveState { - file_clone: PathBuf, - file_clone_2: PathBuf, - producer: rtrb::Producer, - temporary_audio_buffer: OwnedAudioBuffer, - task: Option, -} - -#[derive(Debug)] -struct RecordingAudioFinishingState { - temporary_audio_buffer: OwnedAudioBuffer, - file: PathBuf, -} - -impl RecordingAudioFinishingState { - /// Produces a material info that *doesn't*. Appropriate for producing the final info that will - /// go through the whole chain! - pub fn material_info(&self, recording: &Option) -> AudioMaterialInfo { - let recording = recording - .as_ref() - .expect("recording data must be available if audio recording is finishing"); - AudioMaterialInfo { - channel_count: self.temporary_audio_buffer.to_buf().channel_count(), - frame_count: recording.total_frame_offset, - frame_rate: recording.frame_rate, - } - } -} - -#[derive(Debug)] -struct RecordingMidiState { - equipment: MidiRecordingEquipment, -} - -struct MidiRecordingSourcePair<'a> { - source: &'a mut MidiSequence, - mirror_source: &'a mut MidiSequence, -} - -impl KindState { - fn new(equipment: RecordingEquipment, response_sender: &Sender) -> Self { - use RecordingEquipment::*; - match equipment { - Midi(equipment) => { - let recording_midi_state = RecordingMidiState { equipment }; - Self::Midi(recording_midi_state) - } - Audio(equipment) => { - let active_state = RecordingAudioActiveState { - task: Some(AudioRecordingTask { - pcm_sink: equipment.pcm_sink, - consumer: equipment.consumer, - channel_count: equipment.temporary_audio_buffer.to_buf().channel_count(), - file: equipment.file, - response_sender: response_sender.clone(), - }), - file_clone: equipment.file_clone, - file_clone_2: equipment.file_clone_2, - temporary_audio_buffer: equipment.temporary_audio_buffer, - producer: equipment.producer, - }; - let recording_audio_state = RecordingAudioState::Active(active_state); - Self::Audio(recording_audio_state) - } - } - } - - pub fn is_midi(&self) -> bool { - matches!(self, KindState::Midi(_)) - } -} - -#[derive(Copy, Clone)] -pub struct WriteMidiRequest<'a> { - pub audio_request_props: BasicAudioRequestProps, - pub events: &'a BorrowedMidiEventList, - // TODO-medium Filtering to one channel not supported at the moment. - pub channel_filter: Option, -} - -pub trait WriteAudioRequest { - /// Returns the basic request properties. - fn audio_request_props(&self) -> BasicAudioRequestProps; - - /// Returns the input samples on the given channel. - fn get_channel_buffer(&self, channel_index: usize) -> Option; -} - -impl Drop for Recorder { - fn drop(&mut self) { - debug!("Dropping recorder..."); - } -} - -impl Recorder { - /// Okay to call in real-time thread. - pub fn ready(source: RtClipSource, request_sender: Sender) -> Self { - let ready_state = ReadyState { - source, - midi_overdub_settings: None, - }; - Self { - state: Some(State::Ready(ready_state)), - request_sender, - response_channel: ResponseChannel::new(), - } - } - - pub fn recording(args: RecordingArgs, request_sender: Sender) -> Self { - let response_channel = ResponseChannel::new(); - let kind_state = KindState::new(args.equipment, &response_channel.sender); - let recording_state = RecordingState { - kind_state, - old_source: None, - project: args.project, - detect_downbeat: args.detect_downbeat, - tempo: args.tempo, - time_signature: args.time_signature, - start_timing: args.start_timing, - stop_timing: args.stop_timing, - recording: None, - length: args.length, - committed: false, - initial_play_start_timing: args.initial_play_start_timing, - }; - Self { - state: Some(State::Recording(recording_state)), - request_sender, - response_channel, - } - } - - /// Swaps the old source with the given one. - pub fn swap_source(&mut self, source: &mut RtClipSource) -> ClipEngineResult<()> { - match get_state_mut(&mut self.state)? { - State::Ready(s) => { - mem::swap(&mut s.source, source); - Ok(()) - } - State::Recording(_) => Err("recording"), - } - } - - pub fn source_mut(&mut self) -> ClipEngineResult<&mut RtClipSource> { - match get_state_mut(&mut self.state)? { - State::Ready(s) => Ok(&mut s.source), - State::Recording(_) => Err("recording"), - } - } - - pub fn emit_audio_recording_task(&mut self) { - if let State::Recording(RecordingState { - kind_state: - KindState::Audio(RecordingAudioState::Active(RecordingAudioActiveState { - task, .. - })), - .. - }) = self.state.as_mut().unwrap() - { - if let Some(task) = task.take() { - self.request_sender.start_audio_recording(task); - } - } - } - - pub fn recording_material_info(&self) -> ClipEngineResult { - match get_state(&self.state)? { - State::Ready(_) => Err("not recording"), - State::Recording(s) => Ok(s.recording_material_info()), - } - } - - pub fn record_state(&self) -> Option { - match self.state.as_ref().unwrap() { - State::Ready(_) => None, - State::Recording(s) => { - use RecordState::*; - let state = match s.recording { - None => ScheduledForStart, - Some(r) => { - if r.is_still_in_count_in_phase() { - ScheduledForStart - } else if let Some(end) = r.scheduled_end { - if end.is_predefined { - Recording - } else { - ScheduledForStop - } - } else { - Recording - } - } - }; - Some(state) - } - } - } - - pub fn start_midi_overdub( - &mut self, - source_replacement: Option, - settings: MidiOverdubSettings, - ) -> ClipEngineResult<()> { - match get_state_mut(&mut self.state)? { - State::Ready(s) => { - if let Some(source_replacement) = source_replacement { - // We can only record with an in-project MIDI source, so before overdubbing - // we need to replace the current file-based one with the given in-project one. - let obsolete_source = - mem::replace(&mut s.source, RtClipSource::Midi(source_replacement)); - self.request_sender.discard_source(obsolete_source); - } - s.midi_overdub_settings = Some(settings); - Ok(()) - } - State::Recording(_) => Err("recorder can't start overdubbing because it's recording"), - } - } - - /// Can be called in a real-time thread (doesn't allocate). - pub fn prepare_recording(&mut self, args: RecordingArgs) -> ClipEngineResult<()> { - use State::*; - let (res, next_state) = match take_state(&mut self.state)? { - Ready(s) => { - let recording_state = RecordingState { - kind_state: KindState::new(args.equipment, &self.response_channel.sender), - old_source: Some(s.source), - project: args.project, - detect_downbeat: args.detect_downbeat, - tempo: args.tempo, - time_signature: args.time_signature, - start_timing: args.start_timing, - stop_timing: args.stop_timing, - recording: None, - length: args.length, - committed: false, - initial_play_start_timing: args.initial_play_start_timing, - }; - (Ok(()), Recording(recording_state)) - } - Recording(s) => (Err("already recording"), Recording(s)), - }; - self.state = Some(next_state); - res - } - - pub fn stop_midi_overdub(&mut self) -> ClipEngineResult { - match get_state_mut(&mut self.state)? { - State::Ready(s) => { - let settings = s - .midi_overdub_settings - .take() - .ok_or("not MIDI overdubbung")?; - let outcome = MidiOverdubOutcome { - midi_sequence: settings.mirror_source, - }; - Ok(outcome) - } - State::Recording(_) => Err("we are recording, not MIDI overdubbing"), - } - } - - pub fn stop_recording( - &mut self, - timeline: &HybridTimeline, - timeline_cursor_pos: PositionInSeconds, - audio_request_props: BasicAudioRequestProps, - ) -> ClipEngineResult { - let (res, next_state) = match take_state(&mut self.state)? { - State::Ready(s) => (Err("was not recording"), State::Ready(s)), - State::Recording(s) => { - s.stop_recording(timeline, timeline_cursor_pos, audio_request_props) - } - }; - self.state = Some(next_state); - res - } - - /// Should be called once per block while in recording mode, before writing any material. - /// - /// Takes care of: - /// - /// - Creating the initial recording data and position. - /// - Advancing the recording position for the next material. - /// - Committing the recording as soon as the scheduled end is reached. - pub fn poll_recording( - &mut self, - audio_request_props: BasicAudioRequestProps, - ) -> PollRecordingOutcome { - use State::*; - let Some(current_state) = self.state.take() else { - tracing_error!("Previous state change failed. Record polling stopped."); - return PollRecordingOutcome::PleaseStopPolling; - }; - let (outcome, next_state) = match current_state { - Ready(s) => (PollRecordingOutcome::PleaseStopPolling, Ready(s)), - Recording(s) => s.poll_recording(audio_request_props), - }; - self.state = Some(next_state); - outcome - } - - pub fn write_audio(&mut self, request: impl WriteAudioRequest) -> ClipEngineResult<()> { - match self.state.as_mut().unwrap() { - State::Ready(_) => Err("not recording"), - State::Recording(s) => { - if s.committed { - return Err("already committed"); - } - match &mut s.kind_state { - KindState::Midi(_) => Err("recording MIDI, not audio"), - KindState::Audio(audio_state) => { - match audio_state { - RecordingAudioState::Active(active_state) => { - let recording = s - .recording - .ok_or("recording not started yet ... not polling?")?; - let mut temp_buf = active_state.temporary_audio_buffer.to_buf_mut(); - let channel_count = temp_buf.channel_count(); - let block_length = request.audio_request_props().block_length; - // TODO-high-record-audio Write only part of the block until scheduled end - // Write into ring buffer - let mut write_chunk = active_state - .producer - .write_chunk(channel_count * block_length) - .map_err(|_| "ring buffer too small for writing block")?; - let (slice_one, slice_two) = write_chunk.as_mut_slices(); - for ch in 0..channel_count { - let offset = ch * block_length; - let channel_slice = if offset < slice_one.len() { - &mut slice_one[offset..offset + block_length] - } else { - let slice_two_offset = offset - slice_one.len(); - &mut slice_two - [slice_two_offset..slice_two_offset + block_length] - }; - if let Some(channel_buf) = request.get_channel_buffer(ch) { - channel_slice[..block_length].clone_from_slice( - &channel_buf.data_as_slice()[..block_length], - ); - } else { - channel_slice.fill(0.0); - } - } - write_chunk.commit_all(); - // Write into temporary buffer - let start_frame = recording.total_frame_offset; - if start_frame < temp_buf.frame_count() { - let ideal_end_frame = start_frame + block_length; - let end_frame = - cmp::min(ideal_end_frame, temp_buf.frame_count()); - let num_frames_writable = end_frame - start_frame; - let temp_buf_slice = temp_buf.data_as_mut_slice(); - for ch in 0..channel_count { - if let Some(channel_buf) = request.get_channel_buffer(ch) { - let offset = start_frame * channel_count; - for i in 0..num_frames_writable { - let temp_index = offset + i * channel_count + ch; - temp_buf_slice[temp_index] = - channel_buf.data_as_slice()[i]; - } - } - } - } - Ok(()) - } - RecordingAudioState::Finishing(_) => { - unreachable!("audio can only be finishing if already committed") - } - } - } - } - } - } - } - - pub fn write_midi( - &mut self, - request: WriteMidiRequest, - overdub_frame: Option, - ) -> ClipEngineResult<()> { - match self.state.as_mut().unwrap() { - State::Ready(s) => match s.midi_overdub_settings.as_mut() { - None => Err("neither recording nor overdubbing"), - Some(overdub_settings) => { - let RtClipSource::Midi(midi_sequence) = &mut s.source else { - return Err("source should have been replaced with MidiSequence but has not"); - }; - write_midi( - request, - MidiRecordingSourcePair { - source: midi_sequence, - mirror_source: &mut overdub_settings.mirror_source, - }, - overdub_frame.expect("no MIDI overdub frame given"), - overdub_settings.mode, - overdub_settings.quantization_settings.as_ref(), - ); - Ok(()) - } - }, - State::Recording(s) => { - assert!(!s.committed, "MIDI doesn't use the committed state"); - match &mut s.kind_state { - KindState::Audio(_) => Err("recording audio, not MIDI"), - KindState::Midi(midi_state) => { - let recording = s - .recording - .as_mut() - .ok_or("recording not started yet ... not polling?")?; - // Detect first play frame if downbeat detection enabled - if s.detect_downbeat - && recording.first_play_frame.is_none() - && recording.is_still_in_count_in_phase() - { - if let Some(evt) = request - .events - .into_iter() - .find(|e| crate::midi_util::is_play_message(e.message())) - { - let block_start_frame = recording.total_frame_offset; - let block_offset = convert_duration_in_frames_to_other_frame_rate( - evt.frame_offset().get() as usize, - MidiFrameOffset::REFERENCE_FRAME_RATE, - MIDI_FRAME_RATE, - ); - let event_frame = block_start_frame + block_offset; - debug!( - "Detected first-play frame during count-in phase: {} with block offset {}", - event_frame, block_offset - ); - recording.first_play_frame = Some(event_frame); - } - } - let frame_offset_within_source = if let Some(f) = recording.first_play_frame - { - recording.total_frame_offset - f - } else { - let effective_pos = recording.effective_pos(); - if effective_pos < 0 { - return Ok(()); - } - effective_pos as usize - }; - write_midi( - request, - MidiRecordingSourcePair { - source: &mut midi_state.equipment.new_midi_source, - mirror_source: &mut midi_state.equipment.mirror_source, - }, - frame_offset_within_source, - MidiClipRecordMode::Normal, - midi_state.equipment.quantization_settings.as_ref(), - ); - Ok(()) - } - } - } - } - } - - fn process_worker_response(&mut self) { - let response = match self.response_channel.receiver.try_recv() { - Ok(r) => r, - Err(_) => return, - }; - dbg!(&self.state); - match response { - RecorderResponse::AudioRecordingFinished(r) => { - use State::*; - let next_state = match self.state.take().unwrap() { - Recording(RecordingState { - kind_state: KindState::Audio(RecordingAudioState::Finishing(s)), - old_source, - .. - }) => match r.source { - Ok(source) => { - self.request_sender.discard_audio_recording_finishing_data( - s.temporary_audio_buffer, - s.file, - old_source, - ); - let ready_state = ReadyState { - source, - midi_overdub_settings: None, - }; - Ready(ready_state) - } - Err(msg) => { - // TODO-high-record-audio We should handle this more gracefully, not just let it - // stuck in Finishing state. First by trying to roll back to the old - // clip. If there's no old clip, either by making it possible to return - // an instruction to clear the slot or by letting the worker not just - // return an error message but an alternative empty source. - panic!("recording didn't finish successfully: {msg}") - } - }, - s => { - if let Ok(source) = r.source { - self.request_sender.discard_source(source); - } - s - } - }; - self.state = Some(next_state); - } - } - } -} -impl RecordingState { - pub fn recording_material_info(&self) -> MaterialInfo { - let (frame_rate, frame_count) = if let Some(r) = self.recording { - (r.frame_rate, r.effective_frame_count()) - } else { - (Default::default(), 0) - }; - match &self.kind_state { - KindState::Audio(s) => { - let audio_material_info = AudioMaterialInfo { - channel_count: { s.temporary_audio_buffer().to_buf().channel_count() }, - frame_count, - frame_rate, - }; - MaterialInfo::Audio(audio_material_info) - } - KindState::Midi(_) => MaterialInfo::Midi(MidiMaterialInfo { frame_count }), - } - } - - pub fn stop_recording( - self, - timeline: &HybridTimeline, - timeline_cursor_pos: PositionInSeconds, - audio_request_props: BasicAudioRequestProps, - ) -> (ClipEngineResult, State) { - match self.stop_timing { - RecordInteractionTiming::Immediately => { - // Commit immediately - let (commit_result, next_state) = self.commit_recording(); - ( - commit_result.map(StopRecordingOutcome::Committed), - next_state, - ) - } - RecordInteractionTiming::Quantized(quantization) => { - let rollback = match self.recording { - None => true, - Some(r) => { - if r.scheduled_end.is_some() { - return (Err("end scheduled already"), State::Recording(self)); - } - r.is_still_in_count_in_phase() - } - }; - if rollback { - // Zero point of recording hasn't even been reached yet. Cancel. - if let Some(old_source) = self.old_source { - // There's an old source to roll back to. - let ready_state = ReadyState { - source: old_source, - midi_overdub_settings: None, - }; - ( - Ok(StopRecordingOutcome::Canceled), - State::Ready(ready_state), - ) - } else { - // Nothing to roll back to. This whole chain will be removed in one moment. - (Ok(StopRecordingOutcome::Canceled), State::Recording(self)) - } - } else { - // Schedule end - self.schedule_end( - timeline, - timeline_cursor_pos, - audio_request_props, - quantization, - ) - } - } - } - } - - pub fn schedule_end( - mut self, - timeline: &HybridTimeline, - timeline_cursor_pos: PositionInSeconds, - audio_request_props: BasicAudioRequestProps, - quantization: EvenQuantization, - ) -> (ClipEngineResult, State) { - let r = match &mut self.recording { - None => return (Err("no material arrived yet"), State::Recording(self)), - Some(r) => r, - }; - let scheduled_end = calculate_scheduled_end( - timeline, - timeline_cursor_pos, - audio_request_props, - quantization, - r.total_frame_offset, - self.kind_state.is_midi(), - false, - ); - r.scheduled_end = Some(scheduled_end); - ( - Ok(StopRecordingOutcome::EndScheduled), - State::Recording(self), - ) - } - - pub fn poll_recording( - mut self, - audio_request_props: BasicAudioRequestProps, - ) -> (PollRecordingOutcome, State) { - if self.committed { - return ( - PollRecordingOutcome::PleaseStopPolling, - State::Recording(self), - ); - } - if let Some(recording) = self.recording.as_mut() { - // Recording started already. Advancing position. - // Advance recording position (for MIDI mainly) - let num_source_frames = if self.kind_state.is_midi() { - let num_midi_frames = convert_duration_in_frames_to_other_frame_rate( - audio_request_props.block_length, - audio_request_props.frame_rate, - MIDI_FRAME_RATE, - ); - let timeline = clip_timeline(self.project, false); - let timeline_tempo = timeline.tempo_at(timeline.cursor_pos()); - let tempo_factor = timeline_tempo.get() / MIDI_BASE_BPM.get(); - adjust_proportionally_positive(num_midi_frames as f64, tempo_factor) - } else { - audio_request_props.block_length - }; - let next_frame_offset = recording.total_frame_offset + num_source_frames; - recording.total_frame_offset = next_frame_offset; - // Commit recording if end exceeded - if let Some(scheduled_end) = recording.scheduled_end { - let end_frame = - scheduled_end.complete_length - recording.calculate_downbeat_frame(); - if next_frame_offset > end_frame { - // Exceeded scheduled end. - let recording = *recording; - let (recording_outcome, next_state) = self.commit_recording_internal(recording); - return ( - PollRecordingOutcome::CommittedRecording(recording_outcome), - next_state, - ); - } - } - ( - PollRecordingOutcome::PleaseContinuePolling { - pos: recording.effective_pos(), - }, - State::Recording(self), - ) - } else { - // Recording not started yet. Do it now. - let timeline = clip_timeline(self.project, false); - let timeline_cursor_pos = timeline.cursor_pos(); - let timeline_tempo = timeline.tempo_at(timeline_cursor_pos); - let (start_pos, frames_to_start_pos) = match self.start_timing { - RecordInteractionTiming::Immediately => (timeline_cursor_pos, 0), - RecordInteractionTiming::Quantized(quantization) => { - let equipment = QuantizedPosCalcEquipment::new_with_unmodified_tempo( - &timeline, - timeline_cursor_pos, - timeline_tempo, - audio_request_props, - self.kind_state.is_midi(), - ); - let quantized_start_pos = timeline.next_quantized_pos_at( - timeline_cursor_pos, - quantization, - Laziness::EagerForNextPos, - ); - debug!("Calculated quantized start pos {:?}", quantized_start_pos); - let start_pos = timeline.pos_of_quantized_pos(quantized_start_pos); - let frames_from_start_pos = calc_distance_from_pos(start_pos, equipment); - assert!(frames_from_start_pos < 0); - let frames_to_start_pos = (-frames_from_start_pos) as usize; - (start_pos, frames_to_start_pos) - } - }; - let device_latency_info = Reaper::get().medium_reaper().get_input_output_latency(); - let recording = Recording { - total_frame_offset: 0, - num_count_in_frames: frames_to_start_pos, - frame_rate: if self.kind_state.is_midi() { - MIDI_FRAME_RATE - } else { - audio_request_props.frame_rate - }, - first_play_frame: None, - scheduled_end: self.calculate_predefined_scheduled_end( - &timeline, - audio_request_props, - start_pos, - frames_to_start_pos, - ), - latency: device_latency_info.input_latency as usize - + device_latency_info.output_latency as usize, - }; - self.recording = Some(recording); - ( - PollRecordingOutcome::PleaseContinuePolling { - pos: recording.effective_pos(), - }, - State::Recording(self), - ) - } - } - - // May be called in real-time thread. - pub fn commit_recording(self) -> (ClipEngineResult, State) { - if self.committed { - return (Err("already committed"), State::Recording(self)); - } - let recording = match self.recording { - None => return (Err("no input arrived yet"), State::Recording(self)), - Some(r) => r, - }; - let (recording_outcome, new_state) = self.commit_recording_internal(recording); - (Ok(recording_outcome), new_state) - } - - fn commit_recording_internal(self, recording: Recording) -> (RecordingOutcome, State) { - let is_midi = self.kind_state.is_midi(); - let downbeat_frame = recording.calculate_downbeat_frame(); - let (final_frame_count, section_bounds, kind_specific_outcome, new_state) = match self - .kind_state - { - KindState::Audio(audio_state) => { - let active_state = match audio_state { - RecordingAudioState::Active(s) => s, - RecordingAudioState::Finishing(_) => { - unreachable!( - "if recording not committed yet, audio state can't be finishing" - ); - } - }; - // TODO-high-clip-engine We've got a problem here! The resulting section is shifted exactly - // by l samples (l = latency) to the right in order to make up for the latency - // and get accurate timing. However, that also means that we have a gap at the - // end. The section will exceed the recorded source by exactly l samples. This - // results in a little click at the end (which we can only hear if we switch off - // audio source fades ... good for testing). We need to continue recording the l - // samples (and maybe even a bit more, just to be safe and to have more - // possibilities in future, e.g. creating crossfades). - let outcome = KindSpecificRecordingOutcome::Audio { - path: active_state.file_clone, - channel_count: active_state.temporary_audio_buffer.to_buf().channel_count(), - }; - let recording_state = RecordingState { - kind_state: { - let finishing_state = RecordingAudioFinishingState { - temporary_audio_buffer: active_state.temporary_audio_buffer, - file: active_state.file_clone_2, - }; - KindState::Audio(RecordingAudioState::Finishing(finishing_state)) - }, - committed: true, - ..self - }; - // We always use sections when doing scheduled audio recording because we - // record audio into the file before scheduled start. - let section_bounds = { - let start = recording.num_count_in_frames - downbeat_frame; - let length = recording.scheduled_end.map(|end| { - assert!(recording.num_count_in_frames < end.complete_length); - end.complete_length - recording.num_count_in_frames - }); - SectionBounds::new(start + recording.latency, length) - }; - ( - recording.total_frame_offset, - section_bounds, - outcome, - State::Recording(recording_state), - ) - } - KindState::Midi(mut midi_state) => { - // Write end event - // Example: "E 240 b0 7b 00" - let final_frame_count = recording.effective_frame_count(); - for source in [ - &mut midi_state.equipment.new_midi_source, - &mut midi_state.equipment.mirror_source, - ] { - source.insert_event_at_normalized_midi_frame( - final_frame_count, - MidiEventPayload { - selected: false, - mute: false, - msg: RawShortMessage::from_bytes((0xb0, U7::new(0x7b), U7::new(0x00))) - .unwrap(), - quantization_shift: 0, - }, - ); - } - // Build outcome and next state - let outcome = KindSpecificRecordingOutcome::Midi { - midi_sequence: midi_state.equipment.mirror_source, - }; - let ready_state = ReadyState { - source: RtClipSource::Midi(midi_state.equipment.new_midi_source), - midi_overdub_settings: None, - }; - // Not section with MIDI. We want clean, pre-tailored MIDI sequences! That's no - // problem with MIDI. - let section_bounds = SectionBounds::default(); - ( - final_frame_count, - section_bounds, - outcome, - State::Ready(ready_state), - ) - } - }; - let section_and_downbeat_data = SectionAndDownbeatData { - section_bounds, - quantized_end_pos: recording.scheduled_end.map(|end| end.quantized_end_pos), - downbeat_frame, - }; - let recording_outcome = RecordingOutcome { - data: CompleteRecordingData { - frame_rate: recording.frame_rate, - total_frame_count: final_frame_count, - tempo: self.tempo, - time_signature: self.time_signature, - is_midi, - section_and_downbeat_data, - initial_play_start_timing: self.initial_play_start_timing, - }, - kind_specific: kind_specific_outcome, - }; - (recording_outcome, new_state) - } - - fn calculate_predefined_scheduled_end( - &self, - timeline: &HybridTimeline, - audio_request_props: BasicAudioRequestProps, - start_pos: PositionInSeconds, - frames_to_start_pos: usize, - ) -> Option { - match self.length { - RecordLength::OpenEnd => None, - RecordLength::Quantized(q) => { - let end = calculate_scheduled_end( - timeline, - start_pos, - audio_request_props, - q, - frames_to_start_pos, - self.kind_state.is_midi(), - true, - ); - Some(end) - } - } - } -} - -#[derive(Debug)] -pub enum RecordingEquipment { - Midi(MidiRecordingEquipment), - Audio(AudioRecordingEquipment), -} - -impl RecordingEquipment { - pub fn is_midi(&self) -> bool { - matches!(self, Self::Midi(_)) - } -} - -#[derive(Clone, Debug)] -pub struct MidiRecordingEquipment { - new_midi_source: MidiSequence, - mirror_source: MidiSequence, - quantization_settings: Option, -} - -/// Default number of pulses per quarter note for new MIDI sequence recordings. -const DEFAULT_PPQ: u64 = 960; - -/// Maximum expected number of MIDI events when doing a clip recording. -/// -/// This shouldn't be too high because it increases memory consumption. It also shouldn't be too low -/// because if the size is exceeded, allocation will happen in real-time thread! -const DEFAULT_EVENT_LIST_CAPACITY: usize = 10_000; - -impl MidiRecordingEquipment { - pub fn new(quantization_settings: Option) -> Self { - let new_midi_source = MidiSequence::empty( - DEFAULT_PPQ, - DEFAULT_EVENT_LIST_CAPACITY, - // TODO-high Although not relevant for playback and recording, this should be - // set to the current project tempo in order to be able to check later in which - // speed this was recorded. Might come in useful! - MidiTimeInfo::default(), - ); - Self { - // empty_midi_source: RtClipSource::Reaper(ReaperClipSource::new( - // create_empty_reaper_midi_source().into_raw(), - // )), - mirror_source: new_midi_source.clone(), - new_midi_source, - quantization_settings, - } - } -} - -#[derive(Debug)] -pub struct AudioRecordingEquipment { - pcm_sink: OwnedPcmSink, - producer: rtrb::Producer, - consumer: rtrb::Consumer, - temporary_audio_buffer: OwnedAudioBuffer, - file: PathBuf, - file_clone: PathBuf, - file_clone_2: PathBuf, -} - -// This combination requires ~3 MB for stereo. With 64 channels it would be ~100 MB. -const TEMP_BUF_MAX_FRAME_RATE: usize = 96_000; -const TEMP_BUF_SECONDS: usize = 2; - -const RING_BUF_MAX_BLOCK_SIZE: usize = 2048; -const RING_BUF_MAX_BLOCK_COUNT: usize = 100; - -impl AudioRecordingEquipment { - pub fn new(project: Option, channel_count: usize, sample_rate: Hz) -> Self { - let sink_outcome = create_audio_sink(project, channel_count, sample_rate); - let (producer, consumer) = rtrb::RingBuffer::new( - RING_BUF_MAX_BLOCK_COUNT * channel_count * RING_BUF_MAX_BLOCK_SIZE, - ); - Self { - pcm_sink: sink_outcome.sink, - producer, - consumer, - temporary_audio_buffer: OwnedAudioBuffer::new( - channel_count, - TEMP_BUF_MAX_FRAME_RATE * TEMP_BUF_SECONDS, - ), - file: sink_outcome.file.clone(), - file_clone: sink_outcome.file.clone(), - file_clone_2: sink_outcome.file, - } - } -} - -/// Project is necessary to create the sink. -fn create_audio_sink( - project: Option, - channel_count: usize, - sample_rate: Hz, -) -> AudioSinkOutcome { - let proj_ptr = project.map(|p| p.raw().as_ptr()).unwrap_or(null_mut()); - let file_name = get_path_for_new_media_file("clip-audio", "wav", project); - let file_name_str = file_name.to_str().unwrap(); - let file_name_c_string = CString::new(file_name_str).unwrap(); - let sink = unsafe { - let sink = Reaper::get().medium_reaper().low().PCM_Sink_CreateEx( - proj_ptr, - file_name_c_string.as_ptr(), - null(), - 0, - channel_count as _, - sample_rate.get() as _, - false, - ); - let sink = NonNull::new(sink).expect("PCM_Sink_CreateEx returned null"); - OwnedPcmSink::from_raw(sink) - }; - AudioSinkOutcome { - sink, - file: file_name, - } -} - -struct AudioSinkOutcome { - sink: OwnedPcmSink, - file: PathBuf, -} - -impl AudioSupplier for Recorder { - fn supply_audio( - &mut self, - request: &SupplyAudioRequest, - dest_buffer: &mut AudioBufMut, - ) -> SupplyResponse { - self.process_worker_response(); - match self.state.as_mut().unwrap() { - State::Ready(s) => s.source.supply_audio(request, dest_buffer), - State::Recording(s) => { - match &s.kind_state { - KindState::Audio(RecordingAudioState::Finishing(finishing_state)) => { - // The source is not ready yet but we have a temporary audio buffer that - // gives us the material we need. - // We know that the frame rates should be equal because this is audio and we - // do resampling in upper layers. - debug!("Using temporary buffer"); - let recording = s - .recording - .expect("recording data must be set when audio recording finishing"); - supply_audio_material( - request, - dest_buffer, - recording.frame_rate, - |input| { - transfer_samples_from_buffer( - finishing_state.temporary_audio_buffer.to_buf(), - input, - ) - }, - ); - // Under the assumption that the frame rates are equal (which we asserted), - // the number of consumed frames is the number of written frames. - SupplyResponse::please_continue(dest_buffer.frame_count()) - } - _ => { - if let Some(s) = &mut s.old_source { - // Particularly important if the clip is suspending to switch to recording. - debug!("Querying old source audio"); - s.supply_audio(request, dest_buffer) - } else { - panic!("attempt to play back audio while recording with no previous source") - } - } - } - } - } - } -} - -impl MidiSupplier for Recorder { - fn supply_midi( - &mut self, - request: &SupplyMidiRequest, - event_list: &mut BorrowedMidiEventList, - ) -> SupplyResponse { - match self.state.as_mut().unwrap() { - State::Ready(s) => s.source.supply_midi(request, event_list), - State::Recording(s) => { - if let Some(old_source) = &mut s.old_source { - // Particularly important if the clip is suspending to switch to recording. - debug!("Querying old source MIDI"); - old_source.supply_midi(request, event_list) - } else { - panic!("attempt to play back MIDI while recording without previous source"); - } - } - } - } -} - -impl WithMaterialInfo for Recorder { - fn material_info(&self) -> ClipEngineResult { - match get_state(&self.state)? { - State::Ready(s) => s.source.material_info(), - State::Recording(s) => match &s.kind_state { - KindState::Audio(RecordingAudioState::Finishing(finishing_state)) => { - // Audio recording is being finished. In that case we prefer playing the first - // blocks of the new material (from temporary audio buffer). - let info = finishing_state.material_info(&s.recording); - Ok(MaterialInfo::Audio(info)) - } - _ => { - // In any other case we see if we have an old source to be played. - if let Some(s) = &s.old_source { - // Particularly important if the clip is suspending to switch to recording. - s.material_info() - } else { - Err("attempt to query material info while recording with no previous source") - } - } - }, - } - } -} - -#[derive(Copy, Clone, Debug)] -struct ScheduledEnd { - quantized_end_pos: QuantizedPosition, - /// This is the length from start of material, not from the scheduled start point. - complete_length: usize, - is_predefined: bool, -} - -#[derive(Clone, Debug)] -pub struct RecordingOutcome { - pub data: CompleteRecordingData, - pub kind_specific: KindSpecificRecordingOutcome, -} - -impl RecordingOutcome { - pub fn material_info(&self) -> MaterialInfo { - use KindSpecificRecordingOutcome::*; - match &self.kind_specific { - Midi { .. } => MaterialInfo::Midi(MidiMaterialInfo { - frame_count: self.data.effective_frame_count(), - }), - Audio { channel_count, .. } => MaterialInfo::Audio(AudioMaterialInfo { - channel_count: *channel_count, - frame_count: self.data.effective_frame_count(), - frame_rate: self.data.frame_rate, - }), - } - } -} - -#[derive(Clone, Debug)] -pub enum KindSpecificRecordingOutcome { - Midi { midi_sequence: MidiSequence }, - Audio { path: PathBuf, channel_count: usize }, -} - -#[derive(Clone, Debug)] -pub struct CompleteRecordingData { - pub frame_rate: Hz, - /// Doesn't take section bounds into account. - pub total_frame_count: usize, - pub tempo: Bpm, - pub time_signature: TimeSignature, - pub is_midi: bool, - pub section_and_downbeat_data: SectionAndDownbeatData, - pub initial_play_start_timing: ClipPlayStartTiming, -} - -#[derive(Clone, Debug)] -pub struct SectionAndDownbeatData { - pub section_bounds: SectionBounds, - pub quantized_end_pos: Option, - pub downbeat_frame: usize, -} - -impl CompleteRecordingData { - pub fn effective_frame_count(&self) -> usize { - self.section_and_downbeat_data - .section_bounds - .calculate_frame_count(self.total_frame_count) - } - - pub fn section_start_pos_in_seconds(&self) -> DurationInSeconds { - convert_duration_in_frames_to_seconds( - self.section_and_downbeat_data.section_bounds.start_frame(), - self.frame_rate, - ) - } - - pub fn section_length_in_seconds(&self) -> Option { - let section_frame_count = self.section_and_downbeat_data.section_bounds.length()?; - Some(convert_duration_in_frames_to_seconds( - section_frame_count, - self.frame_rate, - )) - } - - pub fn downbeat_in_beats(&self) -> DurationInBeats { - let downbeat_in_secs = convert_duration_in_frames_to_seconds( - self.section_and_downbeat_data.downbeat_frame, - self.frame_rate, - ); - let bps = self.tempo.get() / 60.0; - DurationInBeats::new(downbeat_in_secs.get() * bps) - } -} - -impl WithCacheableSource for Recorder { - type Source = ReaperClipSource; - - fn cacheable_source(&self) -> Option<&Self::Source> { - match self.state.as_ref().unwrap() { - State::Ready(s) => match &s.source { - RtClipSource::Reaper(s) => s.cacheable_source(), - RtClipSource::Midi(_) => None, - }, - State::Recording(_) => { - // The "current source" during recording state can change quickly. We don't want - // any caching be based on this. - None - } - } - } -} - -pub struct RecordingArgs { - pub equipment: RecordingEquipment, - pub project: Option, - pub timeline_cursor_pos: PositionInSeconds, - pub tempo: Bpm, - pub time_signature: TimeSignature, - pub detect_downbeat: bool, - pub start_timing: RecordInteractionTiming, - pub stop_timing: RecordInteractionTiming, - pub length: RecordLength, - pub initial_play_start_timing: ClipPlayStartTiming, -} - -impl RecordingArgs { - pub fn from_stuff( - project: Option, - column_settings: &RtColumnSettings, - overridable_matrix_settings: &OverridableMatrixSettings, - matrix_record_settings: &MatrixClipRecordSettings, - recording_equipment: RecordingEquipment, - ) -> Self { - let timeline = clip_timeline(project, false); - let timeline_cursor_pos = timeline.cursor_pos(); - let tempo = timeline.tempo_at(timeline_cursor_pos); - let initial_play_start_timing = column_settings - .clip_play_start_timing - .unwrap_or(overridable_matrix_settings.clip_play_start_timing); - let is_midi = recording_equipment.is_midi(); - RecordingArgs { - equipment: recording_equipment, - project, - timeline_cursor_pos, - tempo, - time_signature: timeline.time_signature_at(timeline_cursor_pos), - detect_downbeat: matrix_record_settings.downbeat_detection_enabled(is_midi), - start_timing: RecordInteractionTiming::from_record_start_timing( - matrix_record_settings.start_timing, - initial_play_start_timing, - ), - stop_timing: RecordInteractionTiming::from_record_stop_timing( - matrix_record_settings.stop_timing, - matrix_record_settings.start_timing, - initial_play_start_timing, - ), - length: matrix_record_settings.duration, - initial_play_start_timing, - } - } -} - -#[derive(Copy, Clone, Debug)] -pub enum RecordInteractionTiming { - Immediately, - Quantized(EvenQuantization), -} - -impl RecordInteractionTiming { - pub fn from_record_start_timing( - timing: ClipRecordStartTiming, - play_start_timing: ClipPlayStartTiming, - ) -> Self { - use ClipRecordStartTiming::*; - match timing { - LikeClipPlayStartTiming => match play_start_timing { - ClipPlayStartTiming::Immediately => Self::Immediately, - ClipPlayStartTiming::Quantized(q) => Self::Quantized(q), - }, - Immediately => Self::Immediately, - Quantized(q) => Self::Quantized(q), - } - } - - pub fn from_record_stop_timing( - timing: ClipRecordStopTiming, - record_start_timing: ClipRecordStartTiming, - play_start_timing: ClipPlayStartTiming, - ) -> Self { - use ClipRecordStopTiming::*; - match timing { - LikeClipRecordStartTiming => { - Self::from_record_start_timing(record_start_timing, play_start_timing) - } - Immediately => Self::Immediately, - Quantized(q) => Self::Quantized(q), - } - } -} - -trait RecorderRequestSender { - fn start_audio_recording(&self, task: AudioRecordingTask); - - fn discard_source(&self, source: RtClipSource); - - fn discard_audio_recording_finishing_data( - &self, - temporary_audio_buffer: OwnedAudioBuffer, - file: PathBuf, - old_source: Option, - ); - - fn send_request(&self, request: RecorderRequest); -} - -impl RecorderRequestSender for Sender { - fn start_audio_recording(&self, task: AudioRecordingTask) { - let request = RecorderRequest::RecordAudio(task); - self.send_request(request); - } - fn discard_source(&self, source: RtClipSource) { - let request = RecorderRequest::DiscardSource(source); - self.send_request(request); - } - - fn discard_audio_recording_finishing_data( - &self, - temporary_audio_buffer: OwnedAudioBuffer, - file: PathBuf, - old_source: Option, - ) { - let request = RecorderRequest::DiscardAudioRecordingFinishingData { - temporary_audio_buffer, - file, - old_source, - }; - let _ = self.try_send(request); - } - - fn send_request(&self, request: RecorderRequest) { - self.try_send(request).unwrap(); - } -} - -struct RecorderWorker; - -#[derive(Debug)] -pub struct AudioRecordingTask { - pcm_sink: OwnedPcmSink, - consumer: rtrb::Consumer, - channel_count: usize, - file: PathBuf, - response_sender: Sender, -} - -impl AudioRecordingTask { - fn write_audio(&mut self) { - let slot_count = self.consumer.slots(); - if slot_count == 0 { - return; - } - let read_chunk = match self.consumer.read_chunk(slot_count) { - Ok(c) => c, - Err(_) => return, - }; - let sink = self.pcm_sink.as_mut().as_mut(); - let (slice_one, slice_two) = read_chunk.as_slices(); - write_slice_to_sink(slice_one, sink, self.channel_count); - write_slice_to_sink(slice_two, sink, self.channel_count); - read_chunk.commit_all(); - } -} - -impl RecorderWorker { - pub fn process_request(&mut self, request: RecorderRequest) { - use RecorderRequest::*; - match request { - RecordAudio(task) => { - self.record_audio(task); - } - DiscardSource(_) => {} - DiscardAudioRecordingFinishingData { .. } => {} - } - } - - fn record_audio(&mut self, mut task: AudioRecordingTask) { - loop { - if task.consumer.is_abandoned() { - let response = finish_audio_recording(task.pcm_sink, &task.file); - // If the clip is not interested in the recording anymore, so what. - let _ = task - .response_sender - .try_send(RecorderResponse::AudioRecordingFinished(response)); - break; - } - task.write_audio(); - // Don't spin like crazy - thread::sleep(Duration::from_millis(1)); - } - } -} - -/// Writes the given slice to the sink. -/// -/// The slice must contain all channels in sequence. -fn write_slice_to_sink(slice: &[f64], sink: &mut PCM_sink, channel_count: usize) { - if slice.is_empty() { - return; - } - debug_assert!(slice.len() % channel_count == 0); - let block_length = slice.len() / channel_count; - let mut channel_pointers: [*mut f64; MAX_AUDIO_CHANNEL_COUNT] = - [null_mut(); MAX_AUDIO_CHANNEL_COUNT]; - for (ch, channel_pointer) in channel_pointers.iter_mut().enumerate().take(channel_count) { - let offset = ch * block_length; - let channel_slice = &slice[offset..offset + block_length]; - *channel_pointer = channel_slice.as_ptr() as *mut _; - } - unsafe { - sink.WriteDoubles( - &mut channel_pointers as *mut _, - block_length as _, - channel_count as _, - 0, - 1, - ); - } -} - -pub fn keep_processing_recorder_requests(receiver: Receiver) { - let mut worker = RecorderWorker; - while let Ok(request) = receiver.recv() { - worker.process_request(request); - } -} - -fn finish_audio_recording(sink: OwnedPcmSink, file: &Path) -> AudioRecordingFinishedResponse { - std::mem::drop(sink); - let source = OwnedSource::from_file(file, MidiImportBehavior::ForceNoMidiImport); - AudioRecordingFinishedResponse { - source: source - .map(|s| ReaperClipSource::new(s.into_raw())) - .map(RtClipSource::Reaper), - } -} - -fn write_midi( - request: WriteMidiRequest, - pair: MidiRecordingSourcePair, - frame_offset_within_source: usize, - record_mode: MidiClipRecordMode, - quantization_settings: Option<&QuantizationSettings>, -) { - // Write same data into both sources so we can send the mirror source back to the - // main thread on commit. - // TODO-high Check if this is okay. Alternative: Arc> ... but - // then we must take great care to not block the audio thread accidentally from the - // main thread (= never access from main thread during recording!). - for s in [pair.source, pair.mirror_source] { - write_midi_to_midi_sequence( - request, - s, - frame_offset_within_source, - record_mode, - quantization_settings, - ); - } -} - -fn write_midi_to_midi_sequence( - request: WriteMidiRequest, - sequence: &mut MidiSequence, - frame_offset_within_source: usize, - _record_mode: MidiClipRecordMode, - _quantization_settings: Option<&QuantizationSettings>, -) { - // TODO-high Respect other parameters - // TODO-high Insert multiple events as batch so MIDI sequence doesn't have to shift so often - // in case it's MIDI overdub - for event in request.events { - let frame_offset_within_block = (event.frame_offset().get() as f64 - * REAPER_MIDI_FRAME_OFFSET_NORMALIZATION_FACTOR) - .round() as usize; - let final_frame = frame_offset_within_source + frame_offset_within_block; - let payload = MidiEventPayload { - selected: false, - mute: false, - msg: event.message().to_other(), - quantization_shift: 0, - }; - sequence.insert_event_at_normalized_midi_frame(final_frame, payload); - } -} - -const REAPER_MIDI_FRAME_OFFSET_NORMALIZATION_FACTOR: f64 = - MIDI_FRAME_RATE.get() / MidiFrameOffset::REFERENCE_FRAME_RATE.get(); - -pub enum StopRecordingOutcome { - Committed(RecordingOutcome), - Canceled, - EndScheduled, -} - -pub enum PollRecordingOutcome { - PleaseStopPolling, - CommittedRecording(RecordingOutcome), - PleaseContinuePolling { pos: isize }, -} - -impl PositionTranslationSkill for Recorder { - fn translate_play_pos_to_source_pos(&self, play_pos: isize) -> isize { - play_pos - } -} - -fn calculate_scheduled_end( - timeline: &HybridTimeline, - timeline_cursor_pos: PositionInSeconds, - audio_request_props: BasicAudioRequestProps, - quantization: EvenQuantization, - total_frame_offset: usize, - is_midi: bool, - is_predefined: bool, -) -> ScheduledEnd { - let quantized_end_pos = timeline.next_quantized_pos_at( - timeline_cursor_pos, - quantization, - Laziness::EagerForNextPos, - ); - debug!("Calculated quantized end pos {:?}", quantized_end_pos); - let equipment = QuantizedPosCalcEquipment::new_with_unmodified_tempo( - timeline, - timeline_cursor_pos, - timeline.tempo_at(timeline_cursor_pos), - audio_request_props, - is_midi, - ); - let distance_from_end = calc_distance_from_quantized_pos(quantized_end_pos, equipment); - assert!(distance_from_end < 0, "scheduled end before now"); - let distance_to_end = (-distance_from_end) as usize; - let complete_length = total_frame_offset + distance_to_end; - ScheduledEnd { - quantized_end_pos, - complete_length, - is_predefined, - } -} - -pub enum RecordState { - ScheduledForStart, - Recording, - ScheduledForStop, -} - -const MAX_AUDIO_CHANNEL_COUNT: usize = 64; - -#[derive(Clone, Debug)] -pub struct QuantizationSettings {} - -#[derive(Debug)] -pub struct MidiOverdubOutcome { - pub midi_sequence: MidiSequence, -} - -const STATE_DESTROYED: &str = "state destroyed"; - -fn take_state(state: &mut Option) -> ClipEngineResult { - state.take().ok_or(STATE_DESTROYED) -} - -fn get_state(state: &Option) -> ClipEngineResult<&State> { - state.as_ref().ok_or(STATE_DESTROYED) -} - -fn get_state_mut(state: &mut Option) -> ClipEngineResult<&mut State> { - state.as_mut().ok_or(STATE_DESTROYED) -} diff --git a/playtime-clip-engine/src/rt/supplier/resampler.rs b/playtime-clip-engine/src/rt/supplier/resampler.rs deleted file mode 100644 index cbf760023..000000000 --- a/playtime-clip-engine/src/rt/supplier/resampler.rs +++ /dev/null @@ -1,273 +0,0 @@ -use crate::conversion_util::adjust_proportionally_positive; -use crate::rt::buffer::AudioBufMut; -use crate::rt::supplier::{ - AudioSupplier, AutoDelegatingPositionTranslationSkill, AutoDelegatingPreBufferSourceSkill, - AutoDelegatingWithMaterialInfo, SupplyAudioRequest, SupplyResponse, SupplyResponseStatus, - WithMaterialInfo, WithSupplier, MIDI_FRAME_RATE, -}; -use crate::rt::supplier::{MidiSupplier, SupplyMidiRequest, SupplyRequestInfo}; - -use playtime_api::persistence::VirtualResampleMode; -use reaper_high::Reaper; -use reaper_low::raw; -use reaper_medium::{BorrowedMidiEventList, Hz, OwnedReaperResample}; -use std::ffi::c_void; -use std::ptr::null_mut; - -#[derive(Debug)] -pub struct Resampler { - /// If enabled in general. - enabled: bool, - tempo_adjustments_enabled: bool, - responsible_for_audio_tempo_adjustments: bool, - supplier: S, - api: OwnedReaperResample, - tempo_factor: f64, -} - -impl WithSupplier for Resampler { - type Supplier = S; - - fn supplier(&self) -> &Self::Supplier { - &self.supplier - } - - fn supplier_mut(&mut self) -> &mut Self::Supplier { - &mut self.supplier - } -} - -impl Resampler { - pub fn new(supplier: S) -> Self { - let api = Reaper::get().medium_reaper().resampler_create(); - Self { - enabled: false, - tempo_adjustments_enabled: false, - responsible_for_audio_tempo_adjustments: false, - supplier, - api, - tempo_factor: 1.0, - } - } - - pub fn reset_buffers_and_latency(&mut self) { - self.api.as_mut().as_mut().Reset(); - } - - pub fn set_enabled(&mut self, enabled: bool) { - self.enabled = enabled; - } - - pub fn set_mode(&mut self, mode: VirtualResampleMode) { - use VirtualResampleMode::*; - let raw_mode = match mode { - ProjectDefault => -1, - ReaperMode(m) => m.mode as i32, - }; - unsafe { - self.api.as_mut().as_mut().Extended( - raw::RESAMPLE_EXT_SETRSMODE, - raw_mode as *const c_void as *mut _, - null_mut(), - null_mut(), - ); - } - } - - /// If the part of the resampler is enabled that modifies the tempo of the material. - pub fn set_tempo_adjustments_enabled(&mut self, enabled: bool) { - self.tempo_adjustments_enabled = enabled; - } - - /// Decides whether the resampler should also take the tempo factor into account for audio - /// (VariSpeed). - /// - /// Usually, it only takes care of adjusting the MIDI tempo. - /// - /// This only has an effect if tempo adjustments are enabled in general. - pub fn set_responsible_for_audio_tempo_adjustments(&mut self, responsible: bool) { - self.responsible_for_audio_tempo_adjustments = responsible; - } - - /// Only has an effect if tempo changing enabled. - pub fn set_tempo_factor(&mut self, tempo_factor: f64) { - self.tempo_factor = tempo_factor; - } -} - -impl AudioSupplier for Resampler { - fn supply_audio( - &mut self, - request: &SupplyAudioRequest, - dest_buffer: &mut AudioBufMut, - ) -> SupplyResponse { - if !self.enabled { - return self.supplier.supply_audio(request, dest_buffer); - } - let material_info = self.supplier.material_info().unwrap(); - let source_frame_rate = material_info.frame_rate(); - let dest_frame_rate = request.dest_sample_rate.unwrap_or(source_frame_rate); - let dest_frame_rate = - if self.tempo_adjustments_enabled && self.responsible_for_audio_tempo_adjustments { - Hz::new(dest_frame_rate.get() / self.tempo_factor) - } else { - dest_frame_rate - }; - if source_frame_rate == dest_frame_rate { - return self.supplier.supply_audio(request, dest_buffer); - } - let mut total_num_frames_consumed = 0usize; - let mut total_num_frames_written = 0usize; - let source_channel_count = material_info.channel_count(); - let api = self.api.as_mut().as_mut(); - api.SetRates(source_frame_rate.get(), dest_frame_rate.get()); - // Set ResamplePrepare's out_samples to refer to request a specific number of input samples. - // const RESAMPLE_EXT_SETFEEDMODE: i32 = 0x1001; - // let ext_result = unsafe { - // self.mode.api.Extended( - // RESAMPLE_EXT_SETFEEDMODE, - // 1 as *mut _, - // null_mut(), - // null_mut(), - // ) - // }; - let reached_end = loop { - // Get resampler buffer. - let buffer_frame_count = 128usize; - let mut resample_buffer: *mut f64 = null_mut(); - let num_source_frames_to_write = unsafe { - api.ResamplePrepare( - buffer_frame_count as _, - source_channel_count as i32, - &mut resample_buffer, - ) - }; - if num_source_frames_to_write == 0 { - // We are probably responsible for tempo adjustment and the tempo is super low. - break false; - } - let mut resample_buffer = unsafe { - AudioBufMut::from_raw( - resample_buffer, - source_channel_count, - num_source_frames_to_write as _, - ) - }; - // Feed resampler buffer with source material. - let inner_request = SupplyAudioRequest { - start_frame: request.start_frame + total_num_frames_consumed as isize, - dest_sample_rate: None, - info: SupplyRequestInfo { - audio_block_frame_offset: request.info.audio_block_frame_offset - + total_num_frames_written, - requester: "resampler-audio", - note: "", - is_realtime: false, - }, - parent_request: Some(request), - general_info: request.general_info, - }; - let inner_response = self - .supplier - .supply_audio(&inner_request, &mut resample_buffer); - if inner_response.status.reached_end() { - break true; - } - total_num_frames_consumed += inner_response.num_frames_consumed; - // Get output material. - let offset_buffer = dest_buffer.slice_mut(total_num_frames_written..); - let num_frames_written = unsafe { - api.ResampleOut( - offset_buffer.data_as_mut_ptr(), - num_source_frames_to_write, - offset_buffer.frame_count() as _, - source_channel_count as _, - ) - }; - total_num_frames_written += num_frames_written as usize; - if total_num_frames_written >= dest_buffer.frame_count() { - // We have enough resampled material. - break false; - } - }; - SupplyResponse { - num_frames_consumed: total_num_frames_consumed, - status: if reached_end { - SupplyResponseStatus::ReachedEnd { - num_frames_written: total_num_frames_written, - } - } else { - SupplyResponseStatus::PleaseContinue - }, - } - } -} - -impl MidiSupplier for Resampler { - fn supply_midi( - &mut self, - request: &SupplyMidiRequest, - event_list: &mut BorrowedMidiEventList, - ) -> SupplyResponse { - if !self.enabled { - return self.supplier.supply_midi(request, event_list); - } - let source_frame_rate = MIDI_FRAME_RATE; - if request.dest_sample_rate == source_frame_rate { - // Should never be the case because we have an artificial fixed MIDI frame rate that - // is unlike any realistic sample rate. - return self.supplier.supply_midi(request, event_list); - } - let num_frames_to_be_written = request.dest_frame_count; - let request_ratio = num_frames_to_be_written as f64 / request.dest_sample_rate.get(); - let final_ratio = if self.tempo_adjustments_enabled { - request_ratio * self.tempo_factor - } else { - request_ratio - }; - let num_frames_to_be_consumed = - adjust_proportionally_positive(source_frame_rate.get(), final_ratio); - let inner_request = SupplyMidiRequest { - start_frame: request.start_frame, - dest_frame_count: num_frames_to_be_consumed, - dest_sample_rate: source_frame_rate, - info: SupplyRequestInfo { - audio_block_frame_offset: request.info.audio_block_frame_offset, - requester: "resampler-midi", - note: "", - is_realtime: true, - }, - parent_request: Some(request), - general_info: request.general_info, - }; - let inner_response = self.supplier.supply_midi(&inner_request, event_list); - SupplyResponse { - num_frames_consumed: inner_response.num_frames_consumed, - status: { - use SupplyResponseStatus::*; - match inner_response.status { - PleaseContinue => PleaseContinue, - ReachedEnd { num_frames_written } => { - let response_ratio = - num_frames_to_be_written as f64 / num_frames_to_be_consumed as f64; - ReachedEnd { - num_frames_written: adjust_proportionally_positive( - num_frames_written as f64, - response_ratio, - ), - } - } - } - }, - } - } -} - -impl AutoDelegatingWithMaterialInfo for Resampler {} -impl AutoDelegatingPreBufferSourceSkill for Resampler {} - -// There's no translation because the resampler doesn't actually change the scale -// in which positions are measured, not even if the resampler is responsible for -// tempo adjustments. E.g. if the tempo is higher, the play position will just do larger -// steps forward. -impl AutoDelegatingPositionTranslationSkill for Resampler {} diff --git a/playtime-clip-engine/src/rt/supplier/section.rs b/playtime-clip-engine/src/rt/supplier/section.rs deleted file mode 100644 index cd7b75de8..000000000 --- a/playtime-clip-engine/src/rt/supplier/section.rs +++ /dev/null @@ -1,393 +0,0 @@ -use crate::conversion_util::convert_duration_in_seconds_to_frames; -use crate::rt::buffer::AudioBufMut; -use crate::rt::supplier::fade_util::{ - apply_fade_in_starting_at_zero, apply_fade_out_ending_at, SECTION_FADE_LENGTH, -}; -use crate::rt::supplier::midi_util::SilenceMidiBlockMode; -use crate::rt::supplier::{ - midi_util, AudioMaterialInfo, AudioSupplier, AutoDelegatingMidiSilencer, MaterialInfo, - MidiMaterialInfo, MidiSilencer, MidiSupplier, PositionTranslationSkill, SupplyAudioRequest, - SupplyMidiRequest, SupplyRequest, SupplyRequestInfo, SupplyResponse, SupplyResponseStatus, - WithMaterialInfo, WithSupplier, -}; -use crate::ClipEngineResult; -use playtime_api::persistence::{MidiResetMessageRange, PositiveSecond}; -use reaper_medium::{BorrowedMidiEventList, DurationInSeconds}; - -#[derive(Debug)] -pub struct Section { - supplier: S, - bounds: SectionBounds, - midi_reset_msg_range: MidiResetMessageRange, -} - -#[derive(Clone, Copy, Eq, PartialEq, Debug, Default)] -pub struct SectionBounds { - start_frame: usize, - length: Option, -} - -impl SectionBounds { - pub fn new(start_frame: usize, length: Option) -> Self { - Self { - start_frame, - length, - } - } - - pub fn is_default(&self) -> bool { - self == &Default::default() - } - - pub fn calculate_frame_count(&self, supplier_frame_count: usize) -> usize { - if let Some(length) = self.length { - length - } else { - supplier_frame_count.saturating_sub(self.start_frame) - } - } - - pub fn start_frame(&self) -> usize { - self.start_frame - } - - pub fn length(&self) -> Option { - self.length - } -} - -impl WithSupplier for Section { - type Supplier = S; - - fn supplier(&self) -> &Self::Supplier { - &self.supplier - } - - fn supplier_mut(&mut self) -> &mut Self::Supplier { - &mut self.supplier - } -} - -impl Section { - pub fn new(supplier: S) -> Self { - Self { - supplier, - bounds: Default::default(), - midi_reset_msg_range: Default::default(), - } - } - - pub fn set_midi_reset_msg_range(&mut self, range: MidiResetMessageRange) { - self.midi_reset_msg_range = range; - } - - pub fn bounds(&self) -> SectionBounds { - self.bounds - } - - pub fn set_bounds(&mut self, start_frame: usize, length: Option) { - self.bounds.start_frame = start_frame; - self.bounds.length = length; - } - - pub fn reset(&mut self) { - self.bounds = Default::default(); - } - - pub fn set_bounds_in_seconds( - &mut self, - start: PositiveSecond, - length: Option, - material_info: &MaterialInfo, - ) -> ClipEngineResult<()> - where - S: WithMaterialInfo, - { - let source_frame_rate = material_info.frame_rate(); - let start_frame = convert_duration_in_seconds_to_frames( - DurationInSeconds::new(start.get()), - source_frame_rate, - ); - let frame_count = length.map(|l| { - convert_duration_in_seconds_to_frames( - DurationInSeconds::new(l.get()), - source_frame_rate, - ) - }); - self.set_bounds(start_frame, frame_count); - Ok(()) - } - - fn get_instruction( - &mut self, - request: &impl SupplyRequest, - dest_frame_count: usize, - ) -> Instruction { - if self.bounds.is_default() { - return Instruction::Bypass; - } - // Section is set (start and/or length). - // This logic assumes that the destination frame rate is comparable to the - // source frame rate. The resampler (which sits on top of this supplier) - // takes care of that. - let ideal_num_frames_to_be_consumed = dest_frame_count; - let ideal_end_frame_in_section = - request.start_frame() + ideal_num_frames_to_be_consumed as isize; - if ideal_end_frame_in_section <= 0 { - // Pure count-in phase. Pass through for now. - return Instruction::Bypass; - } - // TODO-high-downbeat If the start frame is < 0 and the end frame is > 0, we currently play - // some material which is shortly before the section start. I think one effect of this is - // that the MIDI piano clip sometimes plays the F note when using this boundary: - // boundary: Boundary { - // start_frame: 1_024_000, - // length: Some(1_024_000), - // } - // Determine source range - let start_frame_in_source = self.bounds.start_frame as isize + request.start_frame(); - let (phase_two, num_frames_to_be_written) = match self.bounds.length { - None => { - // Section doesn't have right bound (easy). - (PhaseTwo::Unbounded, dest_frame_count) - } - Some(length) => { - // Section has right bound. - if request.start_frame() >= length as isize { - // We exceeded the section boundary. Return silence. - return Instruction::Return(SupplyResponse::exceeded_end()); - } - // We are still within the section. - let right_bound_in_source = self.bounds.start_frame + length; - let ideal_end_frame_in_source = - start_frame_in_source + ideal_num_frames_to_be_consumed as isize; - let (reached_bound, effective_end_frame_in_source) = - if ideal_end_frame_in_source <= right_bound_in_source as isize { - // End of block is located before or on end of section end - (false, ideal_end_frame_in_source) - } else { - // End of block is located behind section end - (true, right_bound_in_source as isize) - }; - let bounded_num_frames_to_be_consumed = - (effective_end_frame_in_source - start_frame_in_source) as usize; - let bounded_num_frames_to_be_written = bounded_num_frames_to_be_consumed; - let phase_two = PhaseTwo::Bounded { - reached_bound, - bounded_num_frames_to_be_consumed, - bounded_num_frames_to_be_written, - ideal_num_frames_to_be_consumed, - }; - (phase_two, bounded_num_frames_to_be_written) - } - }; - let data = SectionRequestData { - phase_one: PhaseOne { - start_frame: start_frame_in_source, - info: SupplyRequestInfo { - audio_block_frame_offset: request.info().audio_block_frame_offset, - requester: "section-request", - note: "", - is_realtime: request.info().is_realtime, - }, - num_frames_to_be_written, - }, - phase_two, - }; - Instruction::ApplySection(data) - } - - fn generate_outer_response( - &self, - inner_response: SupplyResponse, - phase_two: PhaseTwo, - ) -> SupplyResponse { - use PhaseTwo::*; - match phase_two { - Unbounded => { - // Section has open end. In that case the inner response is valid. - inner_response - } - Bounded { - reached_bound, - bounded_num_frames_to_be_consumed, - bounded_num_frames_to_be_written, - ideal_num_frames_to_be_consumed, - } => { - // Section has right bound. - if reached_bound { - // Bound reached. - SupplyResponse::reached_end( - bounded_num_frames_to_be_consumed, - bounded_num_frames_to_be_written, - ) - } else { - // Bound not yet reached. - use SupplyResponseStatus::*; - match inner_response.status { - PleaseContinue => { - // Source has more material. - SupplyResponse::please_continue(bounded_num_frames_to_be_consumed) - } - ReachedEnd { .. } => { - // Source has reached its end (but the boundary is not reached yet). - SupplyResponse::please_continue(ideal_num_frames_to_be_consumed) - } - } - } - } - } - } -} - -impl AudioSupplier for Section { - fn supply_audio( - &mut self, - request: &SupplyAudioRequest, - dest_buffer: &mut AudioBufMut, - ) -> SupplyResponse { - let data = match self.get_instruction(request, dest_buffer.frame_count()) { - Instruction::Bypass => { - return self.supplier.supply_audio(request, dest_buffer); - } - Instruction::Return(r) => return r, - Instruction::ApplySection(d) => d, - }; - let inner_request = SupplyAudioRequest { - start_frame: data.phase_one.start_frame, - dest_sample_rate: request.dest_sample_rate, - info: data.phase_one.info, - parent_request: Some(request), - general_info: request.general_info, - }; - let mut inner_dest_buffer = - dest_buffer.slice_mut(0..data.phase_one.num_frames_to_be_written); - let inner_response = self - .supplier - .supply_audio(&inner_request, &mut inner_dest_buffer); - if self.bounds.start_frame > 0 { - apply_fade_in_starting_at_zero(dest_buffer, request.start_frame, SECTION_FADE_LENGTH); - } - if let Some(length) = self.bounds.length { - apply_fade_out_ending_at( - dest_buffer, - request.start_frame, - length, - SECTION_FADE_LENGTH, - ); - } - self.generate_outer_response(inner_response, data.phase_two) - } -} - -impl MidiSupplier for Section { - fn supply_midi( - &mut self, - request: &SupplyMidiRequest, - event_list: &mut BorrowedMidiEventList, - ) -> SupplyResponse { - let data = match self.get_instruction(request, request.dest_frame_count) { - Instruction::Bypass => { - return self.supplier.supply_midi(request, event_list); - } - Instruction::Return(r) => return r, - Instruction::ApplySection(d) => d, - }; - let inner_request = SupplyMidiRequest { - start_frame: data.phase_one.start_frame, - dest_frame_count: data.phase_one.num_frames_to_be_written, - info: data.phase_one.info.clone(), - dest_sample_rate: request.dest_sample_rate, - parent_request: request.parent_request, - general_info: request.general_info, - }; - let inner_response = self.supplier.supply_midi(&inner_request, event_list); - // Reset MIDI at start if necessary - if request.start_frame <= 0 { - debug!("Silence MIDI at section start"); - midi_util::silence_midi( - event_list, - self.midi_reset_msg_range.left, - SilenceMidiBlockMode::Prepend, - &mut self.supplier, - ); - } - // Reset MIDI at end if necessary - if let PhaseTwo::Bounded { - reached_bound: true, - .. - } = &data.phase_two - { - debug!("Silence MIDI at section end"); - midi_util::silence_midi( - event_list, - self.midi_reset_msg_range.right, - SilenceMidiBlockMode::Append, - &mut self.supplier, - ); - } - self.generate_outer_response(inner_response, data.phase_two) - } -} - -impl WithMaterialInfo for Section { - fn material_info(&self) -> ClipEngineResult { - let inner_material_info = self.supplier.material_info()?; - if self.bounds.is_default() { - return Ok(inner_material_info); - } - let material_info = match inner_material_info { - MaterialInfo::Audio(i) => { - let i = AudioMaterialInfo { - frame_count: self.bounds.calculate_frame_count(i.frame_count), - ..i - }; - MaterialInfo::Audio(i) - } - MaterialInfo::Midi(i) => { - let i = MidiMaterialInfo { - frame_count: self.bounds.calculate_frame_count(i.frame_count), - }; - MaterialInfo::Midi(i) - } - }; - Ok(material_info) - } -} - -enum Instruction { - Bypass, - ApplySection(SectionRequestData), - Return(SupplyResponse), -} - -struct SectionRequestData { - phase_one: PhaseOne, - phase_two: PhaseTwo, -} - -struct PhaseOne { - start_frame: isize, - info: SupplyRequestInfo, - num_frames_to_be_written: usize, -} - -enum PhaseTwo { - Unbounded, - Bounded { - reached_bound: bool, - bounded_num_frames_to_be_consumed: usize, - bounded_num_frames_to_be_written: usize, - ideal_num_frames_to_be_consumed: usize, - }, -} - -impl PositionTranslationSkill for Section { - fn translate_play_pos_to_source_pos(&self, play_pos: isize) -> isize { - let effective_play_pos = self.bounds.start_frame as isize + play_pos; - self.supplier - .translate_play_pos_to_source_pos(effective_play_pos) - } -} - -impl AutoDelegatingMidiSilencer for Section {} diff --git a/playtime-clip-engine/src/rt/supplier/start_end_handler.rs b/playtime-clip-engine/src/rt/supplier/start_end_handler.rs deleted file mode 100644 index d4b0ab42d..000000000 --- a/playtime-clip-engine/src/rt/supplier/start_end_handler.rs +++ /dev/null @@ -1,125 +0,0 @@ -use crate::rt::buffer::AudioBufMut; -use crate::rt::supplier::fade_util::{ - apply_fade_in_starting_at_zero, apply_fade_out_ending_at, START_END_FADE_LENGTH, -}; -use crate::rt::supplier::midi_util::SilenceMidiBlockMode; -use crate::rt::supplier::{ - midi_util, AudioSupplier, AutoDelegatingMidiSilencer, AutoDelegatingPositionTranslationSkill, - AutoDelegatingWithMaterialInfo, MidiSilencer, MidiSupplier, SupplyAudioRequest, - SupplyMidiRequest, SupplyResponse, WithMaterialInfo, WithSupplier, -}; - -use playtime_api::persistence::MidiResetMessageRange; -use reaper_medium::BorrowedMidiEventList; - -#[derive(Debug)] -pub struct StartEndHandler { - supplier: S, - audio_fades_enabled: bool, - enabled_for_start: bool, - enabled_for_end: bool, - midi_reset_msg_range: MidiResetMessageRange, -} - -impl StartEndHandler { - pub fn new(supplier: S) -> Self { - Self { - supplier, - audio_fades_enabled: false, - enabled_for_start: false, - enabled_for_end: false, - midi_reset_msg_range: Default::default(), - } - } - - pub fn set_audio_fades_enabled(&mut self, enabled: bool) { - self.audio_fades_enabled = enabled; - } - - pub fn set_midi_reset_msg_range(&mut self, range: MidiResetMessageRange) { - self.midi_reset_msg_range = range; - } - - pub fn set_enabled_for_start(&mut self, enabled: bool) { - self.enabled_for_start = enabled; - } - - pub fn set_enabled_for_end(&mut self, enabled: bool) { - self.enabled_for_end = enabled; - } -} - -impl WithSupplier for StartEndHandler { - type Supplier = S; - - fn supplier(&self) -> &Self::Supplier { - &self.supplier - } - - fn supplier_mut(&mut self) -> &mut Self::Supplier { - &mut self.supplier - } -} - -impl AudioSupplier for StartEndHandler { - fn supply_audio( - &mut self, - request: &SupplyAudioRequest, - dest_buffer: &mut AudioBufMut, - ) -> SupplyResponse { - let response = self.supplier.supply_audio(request, dest_buffer); - if !self.audio_fades_enabled { - return response; - } - if self.enabled_for_start { - apply_fade_in_starting_at_zero(dest_buffer, request.start_frame, START_END_FADE_LENGTH); - } - let frame_count = self.supplier.material_info().unwrap().frame_count(); - if self.enabled_for_end { - apply_fade_out_ending_at( - dest_buffer, - request.start_frame, - frame_count, - START_END_FADE_LENGTH, - ); - } - response - } -} - -impl MidiSupplier for StartEndHandler { - fn supply_midi( - &mut self, - request: &SupplyMidiRequest, - event_list: &mut BorrowedMidiEventList, - ) -> SupplyResponse { - let response = self.supplier.supply_midi(request, event_list); - if self.enabled_for_start && request.start_frame <= 0 { - let end_frame = request.start_frame + response.num_frames_consumed as isize; - if end_frame > 0 { - debug!("Silence MIDI at source start"); - midi_util::silence_midi( - event_list, - self.midi_reset_msg_range.left, - SilenceMidiBlockMode::Prepend, - &mut self.supplier, - ); - } - } - if self.enabled_for_end && response.status.reached_end() { - // TODO-high-clip-engine This is sent repeatedly when the section exceeds the source length! - debug!("Silence MIDI at source end"); - midi_util::silence_midi( - event_list, - self.midi_reset_msg_range.right, - SilenceMidiBlockMode::Append, - &mut self.supplier, - ); - } - response - } -} - -impl AutoDelegatingWithMaterialInfo for StartEndHandler {} -impl AutoDelegatingPositionTranslationSkill for StartEndHandler {} -impl AutoDelegatingMidiSilencer for StartEndHandler {} diff --git a/playtime-clip-engine/src/rt/supplier/time_series.rs b/playtime-clip-engine/src/rt/supplier/time_series.rs deleted file mode 100644 index dba8ff14e..000000000 --- a/playtime-clip-engine/src/rt/supplier/time_series.rs +++ /dev/null @@ -1,126 +0,0 @@ -#![allow(dead_code)] - -#[derive(Clone, Debug)] -pub struct TimeSeries { - pub events: Vec>, -} - -impl Default for TimeSeries { - fn default() -> Self { - Self::new(vec![]) - } -} - -impl TimeSeries { - pub fn new(events: Vec>) -> Self { - Self { events } - } -} - -#[derive(Copy, Clone, Eq, PartialEq, Debug)] -pub struct TimeSeriesEvent { - pub frame: u64, - pub payload: T, -} - -impl TimeSeriesEvent { - pub fn new(frame: u64, payload: T) -> Self { - Self { frame, payload } - } -} - -impl TimeSeries { - pub fn insert(&mut self, frame: u64, payload: T) { - let event = TimeSeriesEvent::new(frame, payload); - // Optimization: If frame is larger or equal than last frame, just push. - let add = self.events.last().map(|l| frame >= l.frame).unwrap_or(true); - if add { - self.events.push(event); - return; - } - // In all other cases, insert at correct position in order to maintain sort order - let insertion_index = self.events.partition_point(|e| e.frame < frame); - self.events.insert(insertion_index, event); - } - - pub fn find_events_in_range( - &self, - start_frame: u64, - frame_count: u64, - ) -> &[TimeSeriesEvent] { - if frame_count == 0 { - return &[]; - } - let exclusive_end_frame = start_frame + frame_count; - // Determine inclusive start index - let start_index = self.events.partition_point(|e| e.frame < start_frame); - // Determine exclusive end index - let exclusive_end_index = self - .events - .partition_point(|e| e.frame < exclusive_end_frame); - // Return slice - &self.events[start_index..exclusive_end_index] - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn find_events_1() { - // Given - let time_series = TimeSeries::new(vec![ - e(5, 'a'), - e(6, 'b'), - e(15, 'c'), - e(15, 'd'), - e(17, 'd'), - e(50, 'e'), - ]); - // When - // Then - assert_eq!( - time_series.find_events_in_range(5, 10), - &[e(5, 'a'), e(6, 'b'),] - ); - assert_eq!(time_series.find_events_in_range(0, 0), &[]); - assert_eq!( - time_series.find_events_in_range(0, 1000), - &time_series.events - ); - assert_eq!(time_series.find_events_in_range(1000, 5000), &[]); - assert_eq!( - time_series.find_events_in_range(15, 20), - &[e(15, 'c'), e(15, 'd'), e(17, 'd'),] - ); - } - - #[test] - fn find_events_2() { - // Given - let time_series = TimeSeries::new(vec![ - e(5, 'a'), - e(6, 'b'), - e(14, 'c'), - e(15, 'd'), - e(15, 'e'), - e(17, 'f'), - e(50, 'g'), - ]); - // When - // Then - assert_eq!( - time_series.find_events_in_range(5, 10), - &[e(5, 'a'), e(6, 'b'), e(14, 'c')] - ); - assert_eq!( - time_series.find_events_in_range(15, 20), - &[e(15, 'd'), e(15, 'e'), e(17, 'f'),] - ); - } - - fn e(frame: u64, payload: T) -> TimeSeriesEvent { - TimeSeriesEvent::new(frame, payload) - } -} diff --git a/playtime-clip-engine/src/rt/supplier/time_stretcher.rs b/playtime-clip-engine/src/rt/supplier/time_stretcher.rs deleted file mode 100644 index 5d97128d9..000000000 --- a/playtime-clip-engine/src/rt/supplier/time_stretcher.rs +++ /dev/null @@ -1,204 +0,0 @@ -use crate::rt::buffer::AudioBufMut; -use crate::rt::supplier::SupplyRequestInfo; -use crate::rt::supplier::{ - AudioSupplier, AutoDelegatingMidiSilencer, AutoDelegatingMidiSupplier, - AutoDelegatingPositionTranslationSkill, AutoDelegatingPreBufferSourceSkill, - AutoDelegatingWithMaterialInfo, SupplyAudioRequest, SupplyResponse, SupplyResponseStatus, - WithMaterialInfo, WithSupplier, -}; - -use playtime_api::persistence::VirtualTimeStretchMode; -use reaper_high::Reaper; -use reaper_low::raw::REAPER_PITCHSHIFT_API_VER; -use reaper_medium::OwnedReaperPitchShift; - -#[derive(Debug)] -pub struct TimeStretcher { - api: OwnedReaperPitchShift, - supplier: S, - enabled: bool, - active: bool, - responsible_for_audio_time_stretching: bool, - tempo_factor: f64, -} - -impl WithSupplier for TimeStretcher { - type Supplier = S; - - fn supplier(&self) -> &Self::Supplier { - &self.supplier - } - - fn supplier_mut(&mut self) -> &mut Self::Supplier { - &mut self.supplier - } -} - -impl TimeStretcher { - pub fn new(supplier: S) -> Self { - let api = Reaper::get() - .medium_reaper() - .reaper_get_pitch_shift_api(REAPER_PITCHSHIFT_API_VER) - .expect("couldn't get pitch shift API in correct version"); - Self { - api, - supplier, - enabled: false, - active: false, - responsible_for_audio_time_stretching: false, - tempo_factor: 1.0, - } - } - - /// Decides whether the time stretcher should take the tempo factor into account for audio. - /// Usually it does. - pub fn set_responsible_for_audio_time_stretching(&mut self, responsible: bool) { - self.responsible_for_audio_time_stretching = responsible; - } - - pub fn set_enabled(&mut self, enabled: bool) { - self.enabled = enabled; - } - - pub fn set_active(&mut self, active: bool) { - self.active = active; - } - - pub fn set_mode(&mut self, mode: VirtualTimeStretchMode) { - use VirtualTimeStretchMode::*; - let raw_quality_param = match mode { - ProjectDefault => -1i32, - ReaperMode(m) => (m.mode << (16 + m.sub_mode)) as i32, - }; - self.api - .as_mut() - .as_mut() - .SetQualityParameter(raw_quality_param); - } - - pub fn set_tempo_factor(&mut self, tempo_factor: f64) { - self.tempo_factor = tempo_factor; - } - - pub fn reset_buffers_and_latency(&mut self) { - self.api.as_mut().as_mut().Reset(); - } -} - -impl AudioSupplier for TimeStretcher { - fn supply_audio( - &mut self, - request: &SupplyAudioRequest, - dest_buffer: &mut AudioBufMut, - ) -> SupplyResponse { - if !self.enabled || !self.active || !self.responsible_for_audio_time_stretching { - return self.supplier.supply_audio(request, dest_buffer); - } - let material_info = self.supplier.material_info().unwrap(); - let source_frame_rate = material_info.frame_rate(); - #[cfg(debug_assertions)] - { - request.assert_wants_source_frame_rate(source_frame_rate); - } - let mut total_num_frames_consumed = 0usize; - let mut total_num_frames_written = 0usize; - // I think it makes sense to set both the output and the input sample rate to the sample - // rate of the source. Then the result could be even cached and sample rate & play-rate - // changes don't need to invalidate the cache. - // TODO-medium Setting this right at the beginning should be enough. - let api = self.api.as_mut().as_mut(); - api.set_srate(source_frame_rate.get()); - let source_channel_count = material_info.channel_count(); - api.set_nch(source_channel_count as _); - api.set_tempo(self.tempo_factor); - let reached_end = loop { - // Get time stretcher buffer. - let buffer_frame_count = 128usize; - let stretch_buffer = api.GetBuffer(buffer_frame_count as _); - let mut stretch_buffer = unsafe { - AudioBufMut::from_raw(stretch_buffer, source_channel_count, buffer_frame_count) - }; - // Fill buffer with a minimum amount of source data (so that we never consume more than - // necessary). - let inner_request = SupplyAudioRequest { - start_frame: request.start_frame + total_num_frames_consumed as isize, - dest_sample_rate: None, - info: SupplyRequestInfo { - // Here we should not add total_num_frames_written because it doesn't grow - // proportionally to the number of consumed source frames. It yields 0 in the - // beginning and then grows fast at the end. - // However, we also can't pass anti-proportionally adjusted consumed source - // frames because the time stretcher may consume lots of source frames in - // advance. Even those that will end up being spit out stretched in the next - // block or the one after that (= input buffering). - // Verdict: At the time this request is made, we have nothing which lets us map - // the currently consumed block of source frames to a frame in the destination - // block. So our best bet is still total_num_frames_written. So better use - // resampling if we want to have accurate bar deviation reporting. - audio_block_frame_offset: request.info.audio_block_frame_offset - + total_num_frames_written, - requester: "time-stretcher-audio", - note: "Attention: Using serious time stretching. Analysis results usually have a negative offset (due to input buffering).", - is_realtime: false - }, - parent_request: Some(request), - general_info: request.general_info, - }; - let inner_response = self - .supplier - .supply_audio(&inner_request, &mut stretch_buffer); - if inner_response.status.reached_end() { - break true; - } - total_num_frames_consumed += inner_response.num_frames_consumed; - use SupplyResponseStatus::*; - let num_inner_frames_written = match inner_response.status { - PleaseContinue => stretch_buffer.frame_count(), - ReachedEnd { num_frames_written } => num_frames_written, - }; - api.BufferDone(num_inner_frames_written as _); - // Get output material. - let offset_buffer = dest_buffer.slice_mut(total_num_frames_written..); - let num_frames_written = unsafe { - api.GetSamples( - offset_buffer.frame_count() as _, - offset_buffer.data_as_mut_ptr(), - ) - }; - total_num_frames_written += num_frames_written as usize; - // println!( - // "num_frames_read: {}, total_num_frames_read: {}, num_frames_written: {}, total_num_frames_written: {}", - // response.num_frames_written, total_num_frames_read, num_frames_written, total_num_frames_written - // ); - if total_num_frames_written >= dest_buffer.frame_count() { - // We have enough stretched material. - break false; - } - }; - SupplyResponse { - num_frames_consumed: total_num_frames_consumed, - status: if reached_end { - SupplyResponseStatus::ReachedEnd { - num_frames_written: total_num_frames_written, - } - } else { - SupplyResponseStatus::PleaseContinue - }, - } - } -} - -// With MIDI, the resampler takes care of adjusting the tempo (since it needs to adjust -// the frame rate anyway). -impl AutoDelegatingMidiSupplier for TimeStretcher {} -impl AutoDelegatingPreBufferSourceSkill for TimeStretcher {} -// There's no translation because the time stretcher doesn't actually change the scale -// in which positions are measured. E.g. if the tempo is higher, the play position will -// just do larger steps forward. -impl AutoDelegatingPositionTranslationSkill for TimeStretcher {} -impl AutoDelegatingWithMaterialInfo for TimeStretcher {} -impl AutoDelegatingMidiSilencer for TimeStretcher {} - -pub enum StretchWorkerRequest { - Stretch, -} diff --git a/playtime-clip-engine/src/rt/tempo_util.rs b/playtime-clip-engine/src/rt/tempo_util.rs deleted file mode 100644 index 6e7dfee0f..000000000 --- a/playtime-clip-engine/src/rt/tempo_util.rs +++ /dev/null @@ -1,51 +0,0 @@ -use crate::rt::supplier::MIDI_BASE_BPM; -use playtime_api::persistence::{BeatTimeBase, ClipTimeBase, TempoRange}; -use reaper_high::{Project, Reaper}; -use reaper_medium::{Bpm, DurationInSeconds, PositionInSeconds}; - -#[allow(dead_code)] -pub fn detect_tempo( - duration: DurationInSeconds, - project: Project, - common_tempo_range: TempoRange, -) -> Bpm { - let result = Reaper::get() - .medium_reaper() - .time_map_2_time_to_beats(project.context(), PositionInSeconds::ZERO); - let numerator = result.time_signature.numerator; - let mut bpm = numerator.get() as f64 * 60.0 / duration.get(); - while bpm < common_tempo_range.min().get() { - bpm *= 2.0; - } - while bpm > common_tempo_range.max().get() { - bpm /= 2.0; - } - Bpm::new(bpm) -} - -/// Returns `None` if time base is not "Beat". -pub fn determine_tempo_from_time_base(time_base: &ClipTimeBase, is_midi: bool) -> Option { - use ClipTimeBase::*; - match time_base { - Time => None, - Beat(b) => Some(determine_tempo_from_beat_time_base(b, is_midi)), - } -} - -pub fn determine_tempo_from_beat_time_base(beat_time_base: &BeatTimeBase, is_midi: bool) -> Bpm { - if is_midi { - MIDI_BASE_BPM - } else { - let tempo = beat_time_base - .audio_tempo - .expect("audio material has time base 'beat' but no tempo"); - Bpm::new(tempo.get()) - } -} - -pub fn calc_tempo_factor(clip_tempo: Bpm, timeline_tempo: Bpm) -> f64 { - let timeline_tempo_factor = timeline_tempo.get() / clip_tempo.get(); - timeline_tempo_factor.max(MIN_TEMPO_FACTOR) -} - -const MIN_TEMPO_FACTOR: f64 = 0.0000000001; diff --git a/playtime-clip-engine/src/source_util.rs b/playtime-clip-engine/src/source_util.rs deleted file mode 100644 index ee92940fb..000000000 --- a/playtime-clip-engine/src/source_util.rs +++ /dev/null @@ -1,196 +0,0 @@ -use crate::file_util::get_path_for_new_media_file; -use crate::rt::source_util::{get_pcm_source_type, PcmSourceType}; -use crate::rt::supplier::{ReaperClipSource, MIDI_BASE_BPM}; -use crate::{rt, ClipEngineResult}; -use playtime_api::persistence as api; -use playtime_api::persistence::{FileSource, MidiChunkSource}; -use reaper_high::{BorrowedSource, Item, OwnedSource, Project, Reaper, ReaperSource}; -use reaper_medium::{BorrowedPcmSource, MidiImportBehavior}; -use std::borrow::Cow; -use std::error::Error; -use std::path::{Path, PathBuf}; - -/// Creates slot content based on the audio/MIDI file used by the given item. -/// -/// If the item uses pooled MIDI instead of a file, this method exports the MIDI data to a new -/// file in the recording directory and uses that one. -#[allow(dead_code)] -pub fn create_api_source_from_item( - item: Item, - force_export_to_file: bool, -) -> Result> { - let active_take = item.active_take().ok_or("item has no active take")?; - let root_pcm_source = active_take - .source() - .ok_or("take has no source")? - .root_source(); - let root_pcm_source = ReaperSource::new(root_pcm_source); - let mode = if force_export_to_file { - CreateApiSourceMode::ForceExportToFile { - file_base_name: active_take.name(), - } - } else { - CreateApiSourceMode::AllowEmbeddedData - }; - create_api_source_from_pcm_source(root_pcm_source.as_raw(), mode, item.project()) -} - -/// Determines how to handle MIDI PCM sources. -#[allow(dead_code)] -pub enum CreateApiSourceMode { - AllowEmbeddedData, - ForceExportToFile { file_base_name: String }, -} - -/// Project is used for making a file path relative and/or for determining the directory of a file -/// to be exported. -pub fn create_api_source_from_pcm_source( - pcm_source: &BorrowedPcmSource, - mode: CreateApiSourceMode, - project: Option, -) -> Result> { - let pcm_source = BorrowedSource::from_raw(pcm_source); - if let Some(source_file) = pcm_source.file_name() { - Ok(create_file_api_source(project, &source_file)) - } else { - let pcm_source_type = get_pcm_source_type(pcm_source.as_raw()); - if pcm_source_type.is_midi() { - use CreateApiSourceMode::*; - let api_source = match mode { - AllowEmbeddedData => { - let ref_source = if pcm_source_type == PcmSourceType::PooledMidi { - let cloned_pcm_source = Reaper::get() - .with_pref_pool_midi_when_duplicating(false, || pcm_source.to_owned()); - Cow::Owned(cloned_pcm_source) - } else { - Cow::Borrowed(pcm_source) - }; - create_midi_chunk_source(ref_source.state_chunk()) - } - ForceExportToFile { file_base_name } => { - let file_name = get_path_for_new_media_file(&file_base_name, "mid", project); - pcm_source - .export_to_file(&file_name) - .map_err(|_| "couldn't export MIDI source to file")?; - create_file_api_source(project, &file_name) - } - }; - Ok(api_source) - } else { - Err(format!("item source incompatible (type {pcm_source_type:?})").into()) - } - } -} - -/// Takes care of making the path project-relative (if a project is given). -pub fn create_file_api_source(project: Option, file: &Path) -> api::Source { - api::Source::File(api::FileSource { - path: make_relative(project, file), - }) -} - -fn create_midi_chunk_source(chunk: String) -> api::Source { - api::Source::MidiChunk(api::MidiChunkSource { chunk }) -} - -/// Creates a REAPER PCM source from the given API source. -/// -/// - Keeps MIDI files as reference, doesn't convert to in-project MIDI. -/// - If no project is given, the path will not be relative. -pub fn create_pcm_source_from_api_source( - api_source: &api::Source, - project_for_relative_path: Option, -) -> ClipEngineResult { - use api::Source::*; - let pcm_source = match api_source { - File(s) => { - // We don't import MIDI as in-project MIDI, otherwise we would end up with a MIDI chunk - // source on save, which would be unexpected. It's worth to point out that MIDI overdub - // is not possible with file-based MIDI sources. So as soon as the user does MIDI - // overdub, we need to go "MIDI chunk". - create_pcm_source_from_file_based_api_source(project_for_relative_path, s, false)? - } - MidiChunk(s) => create_pcm_source_from_midi_chunk_based_api_source(s.clone())?, - }; - Ok(ReaperClipSource::new(pcm_source.into_raw())) -} - -fn create_pcm_source_from_midi_chunk_based_api_source( - mut pcm_source: MidiChunkSource, -) -> ClipEngineResult { - let mut source = OwnedSource::from_type("MIDI").unwrap(); - pcm_source.chunk += ">\n"; - source.set_state_chunk(", - path: &Path, -) -> ClipEngineResult { - if path.is_relative() { - project_for_relative_path - .ok_or("slot source given as relative file but without project")? - .make_path_absolute(path) - .ok_or("couldn't make clip source path absolute") - } else { - Ok(path.to_path_buf()) - } -} - -pub fn create_pcm_source_from_file_based_api_source( - project_for_relative_path: Option, - source: &FileSource, - import_midi_as_in_project_midi: bool, -) -> ClipEngineResult { - let absolute_file = make_media_file_path_absolute(project_for_relative_path, &source.path)?; - if !absolute_file.exists() { - return Err("clip file doesn't exist"); - } - create_pcm_source_from_media_file(&absolute_file, import_midi_as_in_project_midi) -} - -pub fn create_pcm_source_from_media_file( - absolute_file: &Path, - import_midi_as_in_project_midi: bool, -) -> ClipEngineResult { - let source = if import_midi_as_in_project_midi { - Reaper::get().with_pref_import_as_mid_file_reference(false, || { - OwnedSource::from_file(absolute_file, MidiImportBehavior::UsePreference) - }) - } else { - OwnedSource::from_file(absolute_file, MidiImportBehavior::ForceNoMidiImport) - }; - let source = source?; - if rt::source_util::pcm_source_is_midi(source.as_ref().as_raw()) { - post_process_midi_source(&source); - } - Ok(source) -} - -fn post_process_midi_source(source: &BorrowedSource) { - // Setting the source preview tempo is absolutely essential. It decouples the playing - // of the source from REAPER's project position and tempo. We set it to a constant tempo - // because we control the tempo on-the-fly by modifying the frame rate when requesting - // material. - // There are alternatives to setting the preview tempo, in particular doing the - // following when playing the material: - // - // transfer.set_force_bpm(MIDI_BASE_BPM); - // transfer.set_absolute_time_s(PositionInSeconds::ZERO); - // - // However, now we set the constant preview tempo at source creation time, which makes - // the source completely project tempo/pos-independent, also when doing recording via - // midi_realtime_write_struct_t. So that's not necessary anymore. - source.set_preview_tempo(Some(MIDI_BASE_BPM)).unwrap(); -} - -fn make_relative(project: Option, file: &Path) -> PathBuf { - project - .and_then(|p| p.make_path_relative_if_in_project_directory(file)) - .unwrap_or_else(|| file.to_owned()) -} diff --git a/playtime-clip-engine/src/timeline.rs b/playtime-clip-engine/src/timeline.rs deleted file mode 100644 index 29740e5f5..000000000 --- a/playtime-clip-engine/src/timeline.rs +++ /dev/null @@ -1,628 +0,0 @@ -use crate::conversion_util::{ - convert_duration_in_frames_to_seconds, convert_position_in_seconds_to_frames, -}; -use crate::rt::BasicAudioRequestProps; -use crate::ClipEngineResult; -use atomic::Atomic; -use helgoboss_learn::BASE_EPSILON; -use playtime_api::persistence::EvenQuantization; -use reaper_high::{Project, Reaper}; -use reaper_medium::{ - Bpm, Hz, PlayState, PositionInBeats, PositionInQuarterNotes, PositionInSeconds, ProjectContext, - TimeSignature, -}; -use static_assertions::const_assert; -use std::sync::atomic::{AtomicU64, Ordering}; - -/// Delivers the timeline to be used for clips. -pub fn clip_timeline(project: Option, force_reaper_timeline: bool) -> HybridTimeline { - let reaper_timeline = ReaperTimeline::new(project); - if force_reaper_timeline || reaper_timeline.is_playing_or_paused() { - HybridTimeline::ReaperProject(reaper_timeline) - } else { - let steady_timeline = SteadyTimeline::new(global_steady_timeline_state()); - HybridTimeline::GlobalSteady(steady_timeline) - } -} - -pub fn clip_timeline_cursor_pos(project: Option) -> PositionInSeconds { - clip_timeline(project, false).cursor_pos() -} - -/// This represents the timeline of a REAPER project. -/// -/// Characteristics: -/// -/// - The cursor position (seconds) moves forward in real-time and independent from the current -/// tempo. -/// - If the project is paused, all positions freeze. -/// - The cursor position is reset whenever the user relocates the cursor in the project. -/// - This is okay for clip playing when the project is playing because in that case we want -/// to interrupt the clips and re-align to the changed situation. -/// - It's not okay for clip playing when the project is stopped because it would come as a -/// surprise for the user that clips are interrupted since they appear to be running -/// disconnected from the project timeline but actually aren't. -/// - If the project is playing (not stopped, not paused), the cursor position even resets when -/// changing the tempo. However, it leaves the bar/beat structure intact. -/// - The timeline cursor position doesn't influence the position within our clip, so we are -/// immune against these resets. -/// - If the project is not playing, tempo changes don't affect the cursor position but they reset -/// the bar/beat structure. -/// - It's understandable that the bar/beat structure is affected because REAPER has no tempo -/// envelope to look at, so it just does the most simple thing: Distributing the bars/beats -/// in a linear way. -/// - It's problematic for us that the bar/beat structure is affected because we use bars/beats -/// to keep the clips in sync (by scheduling them on start of bar). We need the bars/beats -/// structure to be fluent and adjust to tempo changes dynamically, much as a playing project -/// would do. -#[derive(Clone, Debug)] -pub struct ReaperTimeline { - project_context: ProjectContext, -} - -impl ReaperTimeline { - pub fn new(project: Option) -> Self { - Self { - project_context: project - .map(|p| p.context()) - .unwrap_or(ProjectContext::CurrentProject), - } - } - - fn is_playing_or_paused(&self) -> bool { - let play_state = self.play_state(); - play_state.is_playing || play_state.is_paused - } - - fn play_state(&self) -> PlayState { - Reaper::get() - .medium_reaper() - .get_play_state_ex(self.project_context) - } -} - -/// Defines how the next quantized position is determined. -pub enum Laziness { - EagerForNextPos, - DwellingOnCurrentPos, -} - -impl Timeline for ReaperTimeline { - fn cursor_pos(&self) -> PositionInSeconds { - Reaper::get() - .medium_reaper() - .get_play_position_2_ex(self.project_context) - } - - fn full_beats_at_pos(&self, timeline_pos: PositionInSeconds) -> PositionInBeats { - let res = Reaper::get() - .medium_reaper() - .time_map_2_time_to_beats(self.project_context, timeline_pos); - res.full_beats - } - - fn next_quantized_pos_at( - &self, - timeline_pos: PositionInSeconds, - quantization: EvenQuantization, - laziness: Laziness, - ) -> QuantizedPosition { - // TODO-medium Handle in-measure tempo changes correctly (also for pos_of_quantized_pos). - // Time signature changes (always start a new measure) and on-measure tempo changes are - // handled correctly already. - get_next_quantized_pos_at(timeline_pos, quantization, self.project_context, laziness) - } - - fn pos_of_quantized_pos(&self, quantized_pos: QuantizedPosition) -> PositionInSeconds { - get_pos_of_quantized_pos(quantized_pos, self.project_context) - } - - fn is_running(&self) -> bool { - !self.play_state().is_paused - } - - fn tempo_at(&self, timeline_pos: PositionInSeconds) -> Bpm { - Reaper::get() - .medium_reaper() - .time_map_2_get_divided_bpm_at_time(self.project_context, timeline_pos) - } - - fn time_signature_at(&self, timeline_pos: PositionInSeconds) -> TimeSignature { - Reaper::get() - .medium_reaper() - .time_map_2_time_to_beats(self.project_context, timeline_pos) - .time_signature - } -} - -pub trait Timeline { - fn cursor_pos(&self) -> PositionInSeconds; - - fn full_beats_at_pos(&self, timeline_pos: PositionInSeconds) -> PositionInBeats; - - fn next_quantized_pos_at( - &self, - timeline_pos: PositionInSeconds, - quantization: EvenQuantization, - laziness: Laziness, - ) -> QuantizedPosition; - - fn pos_of_quantized_pos(&self, quantized_pos: QuantizedPosition) -> PositionInSeconds; - - fn is_running(&self) -> bool; - - fn tempo_at(&self, timeline_pos: PositionInSeconds) -> Bpm; - - fn time_signature_at(&self, timeline_pos: PositionInSeconds) -> TimeSignature; -} - -/// Self-made timeline state that is driven by the global audio hook. -/// -/// Characteristics: -/// -/// - The cursor position (seconds) moves forward in real-time and independent from the current -/// tempo. -/// - The tempo is synchronized with the tempo of the current project at the current edit cursor -/// position. -/// - Uses the time signature of the current project at the current edit cursor position for -/// quantization purposes. -#[derive(Clone, Debug)] -pub struct SteadyTimeline<'a> { - state: &'a SteadyTimelineState, -} - -impl<'a> SteadyTimeline<'a> { - pub fn new(state: &'a SteadyTimelineState) -> Self { - Self { state } - } - - fn time_signature(&self) -> TimeSignature { - // We could take the time signature from a particular project here (instead of from the - // current project) but that wouldn't be consequent because the global - // (project-independent) timeline state takes the tempo information always from the current - // project. - Reaper::get() - .medium_reaper() - .time_map_2_time_to_beats( - ProjectContext::CurrentProject, - SteadyTimelineState::tempo_and_time_sig_ref_pos(), - ) - .time_signature - } -} - -#[derive(Debug)] -pub struct SteadyTimelineState { - sample_counter: AtomicU64, - sample_rate: Atomic, - tempo: Atomic, - beat_at_last_tempo_change: Atomic, - sample_count_at_last_tempo_change: AtomicU64, -} - -impl SteadyTimelineState { - pub const fn new() -> Self { - const_assert!(Atomic::::is_lock_free()); - Self { - sample_counter: AtomicU64::new(0), - sample_rate: Atomic::new(Hz::MIN), - tempo: Atomic::new(Bpm::MIN), - beat_at_last_tempo_change: Atomic::new(0.0), - sample_count_at_last_tempo_change: AtomicU64::new(0), - } - } - - /// Supposed to be called once per audio callback. - pub fn on_audio_buffer(&self, audio_request_props: BasicAudioRequestProps) { - let tempo = Reaper::get() - .medium_reaper() - .time_map_2_get_divided_bpm_at_time( - ProjectContext::CurrentProject, - Self::tempo_and_time_sig_ref_pos(), - ); - let prev_tempo = self.tempo(); - let prev_sample_count = self - .sample_counter - .fetch_add(audio_request_props.block_length as _, Ordering::SeqCst); - if tempo != prev_tempo { - let prev_sample_count_at_last_tempo_change = self.sample_count_at_last_tempo_change(); - let prev_beat_at_last_tempo_change = self.beat_at_last_tempo_change(); - let prev_sample_rate = self.sample_rate(); - let beat = calc_beat_at( - prev_sample_count, - prev_sample_count_at_last_tempo_change, - prev_beat_at_last_tempo_change, - prev_tempo, - prev_sample_rate, - ); - self.sample_count_at_last_tempo_change - .store(prev_sample_count, Ordering::SeqCst); - self.beat_at_last_tempo_change.store(beat, Ordering::SeqCst); - } - self.tempo.store(tempo, Ordering::SeqCst); - self.sample_rate - .store(audio_request_props.frame_rate, Ordering::SeqCst); - } - - fn tempo_and_time_sig_ref_pos() -> PositionInSeconds { - Reaper::get() - .medium_reaper() - .get_cursor_position_ex(ProjectContext::CurrentProject) - } - - fn sample_count(&self) -> u64 { - self.sample_counter.load(Ordering::SeqCst) - } - - fn sample_rate(&self) -> Hz { - self.sample_rate.load(Ordering::SeqCst) - } - - fn tempo(&self) -> Bpm { - self.tempo.load(Ordering::SeqCst) - } - - fn beat_at_last_tempo_change(&self) -> f64 { - self.beat_at_last_tempo_change.load(Ordering::SeqCst) - } - - fn sample_count_at_last_tempo_change(&self) -> u64 { - self.sample_count_at_last_tempo_change - .load(Ordering::SeqCst) - } - - fn cursor_pos(&self) -> PositionInSeconds { - PositionInSeconds::new(self.sample_count() as f64 / self.sample_rate().get()) - } - - fn next_quantized_pos_at( - &self, - timeline_pos: PositionInSeconds, - quantization: EvenQuantization, - time_sig_denominator: u32, - laziness: Laziness, - ) -> QuantizedPosition { - let accurate_beat = self.accurate_beat(timeline_pos); - // The time signature denominator defines what one beat "means" (e.g. a quarter note). - let ratio = quantization.denominator() as f64 / time_sig_denominator as f64; - let accurate_pos = accurate_beat * ratio; - calc_quantized_pos_from_accurate_pos(accurate_pos, quantization, laziness) - } - - fn accurate_beat(&self, timeline_pos: PositionInSeconds) -> f64 { - let sample_rate = self.sample_rate(); - let timeline_frame = convert_position_in_seconds_to_frames(timeline_pos, sample_rate); - let sample_count_at_last_tempo_change = self.sample_count_at_last_tempo_change(); - let sample_count = timeline_frame as u64; - if sample_count < sample_count_at_last_tempo_change { - panic!("attempt to query next quantized position from a position in the past"); - } - calc_beat_at( - sample_count, - sample_count_at_last_tempo_change, - self.beat_at_last_tempo_change(), - self.tempo(), - sample_rate, - ) - } - - fn pos_of_quantized_pos( - &self, - quantized_pos: QuantizedPosition, - time_sig_denominator: u32, - ) -> PositionInSeconds { - let ratio = time_sig_denominator as f64 / quantized_pos.denominator() as f64; - let beat = quantized_pos.position() as f64 * ratio; - calc_pos_of_beat( - beat, - self.tempo(), - self.beat_at_last_tempo_change(), - self.sample_count_at_last_tempo_change(), - self.sample_rate(), - ) - } -} - -fn calc_beat_at( - sample_count: u64, - sample_count_at_last_tempo_change: u64, - beat_at_last_tempo_change: f64, - current_tempo: Bpm, - current_sample_rate: Hz, -) -> f64 { - debug_assert!(sample_count >= sample_count_at_last_tempo_change); - let beats_per_sec = current_tempo.get() / 60.0; - let samples_since_last_tempo_change = sample_count - sample_count_at_last_tempo_change; - let secs_since_last_tempo_change = convert_duration_in_frames_to_seconds( - samples_since_last_tempo_change as usize, - current_sample_rate, - ); - beat_at_last_tempo_change + secs_since_last_tempo_change.get() * beats_per_sec -} - -fn calc_pos_of_beat( - beat: f64, - current_tempo: Bpm, - beat_at_last_tempo_change: f64, - sample_count_at_last_tempo_change: u64, - current_sample_rate: Hz, -) -> PositionInSeconds { - let beats_per_sec = current_tempo.get() / 60.0; - let secs_since_last_tempo_change = (beat - beat_at_last_tempo_change) / beats_per_sec; - let secs_at_last_tempo_change = convert_duration_in_frames_to_seconds( - sample_count_at_last_tempo_change as usize, - current_sample_rate, - ); - PositionInSeconds::new(secs_at_last_tempo_change.get() + secs_since_last_tempo_change) -} - -impl<'a> Timeline for SteadyTimeline<'a> { - fn cursor_pos(&self) -> PositionInSeconds { - self.state.cursor_pos() - } - - fn full_beats_at_pos(&self, timeline_pos: PositionInSeconds) -> PositionInBeats { - PositionInBeats::new(self.state.accurate_beat(timeline_pos)) - } - - fn next_quantized_pos_at( - &self, - timeline_pos: PositionInSeconds, - quantization: EvenQuantization, - laziness: Laziness, - ) -> QuantizedPosition { - self.state.next_quantized_pos_at( - timeline_pos, - quantization, - self.time_signature().denominator.get(), - laziness, - ) - } - - fn pos_of_quantized_pos(&self, quantized_pos: QuantizedPosition) -> PositionInSeconds { - self.state - .pos_of_quantized_pos(quantized_pos, self.time_signature().denominator.get()) - } - - fn is_running(&self) -> bool { - true - } - - fn tempo_at(&self, _timeline_pos: PositionInSeconds) -> Bpm { - self.state.tempo() - } - - fn time_signature_at(&self, _timeline_pos: PositionInSeconds) -> TimeSignature { - self.time_signature() - } -} - -fn get_next_quantized_pos_at( - cursor_pos: PositionInSeconds, - quantization: EvenQuantization, - proj_context: ProjectContext, - laziness: Laziness, -) -> QuantizedPosition { - let reaper = Reaper::get().medium_reaper(); - if quantization.denominator() == 1 { - // We are looking for one of the next bars. - let res = reaper.time_map_2_time_to_beats(proj_context, cursor_pos); - let next_position = next_quantized_pos( - res.measure_index as i64, - res.beats_since_measure.get(), - quantization.numerator(), - laziness, - ); - QuantizedPosition::new(next_position, 1).unwrap() - } else { - // We are looking for the next fraction of a bar (e.g. the next 16th note). - let qn = reaper.time_map_2_time_to_qn_abs(proj_context, cursor_pos); - // Calculate ratio between our desired target unit (16th) and a quarter note (4th) = 4 - let ratio = quantization.denominator() as f64 / 4.0; - // Current position in desired target unit (158.4 16th's). - let accurate_pos = qn.get() * ratio; - calc_quantized_pos_from_accurate_pos(accurate_pos, quantization, laziness) - } -} - -fn calc_quantized_pos_from_accurate_pos( - accurate_pos: f64, - quantization: EvenQuantization, - laziness: Laziness, -) -> QuantizedPosition { - // Current position quantized (e.g. 158 16th's). - let quantized_pos = accurate_pos.floor() as i64; - // Difference (0.4 16th's). - let within = accurate_pos - quantized_pos as f64; - let next_position = - next_quantized_pos(quantized_pos, within, quantization.numerator(), laziness); - QuantizedPosition::new(next_position, quantization.denominator()).unwrap() -} - -fn next_quantized_pos( - current_quantized_pos: i64, - within: f64, - numerator: u32, - laziness: Laziness, -) -> i64 { - match laziness { - Laziness::EagerForNextPos => current_quantized_pos + numerator as i64, - Laziness::DwellingOnCurrentPos => { - if within < BASE_EPSILON { - // Just a tiny bit away from quantized position. Pretty sure the user meant to start now. - return current_quantized_pos; - } - // Enough distance from quantized position. - current_quantized_pos + numerator as i64 - } - } -} - -fn get_pos_of_quantized_pos( - quantized_pos: QuantizedPosition, - proj_context: ProjectContext, -) -> PositionInSeconds { - let reaper = Reaper::get().medium_reaper(); - let qn = if quantized_pos.denominator() == 1 { - // We are looking for the position of a bar. - let res = reaper.time_map_get_measure_info(proj_context, quantized_pos.position as _); - res.start_qn - } else { - // We are looking for the position of a fraction of a bar (e.g. a 16th note). - // Calculate ratio between a quarter note (4th) and our desired target unit (16th) = 0.25 - let ratio = 4.0 / quantized_pos.denominator() as f64; - PositionInQuarterNotes::new(quantized_pos.position as f64 * ratio) - }; - reaper.time_map_2_qn_to_time_abs(proj_context, qn) -} - -impl Timeline for &T { - fn cursor_pos(&self) -> PositionInSeconds { - (*self).cursor_pos() - } - - fn full_beats_at_pos(&self, timeline_pos: PositionInSeconds) -> PositionInBeats { - (*self).full_beats_at_pos(timeline_pos) - } - - fn next_quantized_pos_at( - &self, - timeline_pos: PositionInSeconds, - quantization: EvenQuantization, - laziness: Laziness, - ) -> QuantizedPosition { - (*self).next_quantized_pos_at(timeline_pos, quantization, laziness) - } - - fn pos_of_quantized_pos(&self, quantized_pos: QuantizedPosition) -> PositionInSeconds { - (*self).pos_of_quantized_pos(quantized_pos) - } - - fn is_running(&self) -> bool { - (*self).is_running() - } - - fn tempo_at(&self, timeline_pos: PositionInSeconds) -> Bpm { - (*self).tempo_at(timeline_pos) - } - - fn time_signature_at(&self, timeline_pos: PositionInSeconds) -> TimeSignature { - (*self).time_signature_at(timeline_pos) - } -} - -static GLOBAL_STEADY_TIMELINE_STATE: SteadyTimelineState = SteadyTimelineState::new(); - -/// Returns the state for a global timeline that is ever-increasing and not influenced by REAPER's -/// transport. -pub fn global_steady_timeline_state() -> &'static SteadyTimelineState { - &GLOBAL_STEADY_TIMELINE_STATE -} - -#[derive(Clone, Debug)] -pub enum HybridTimeline { - ReaperProject(ReaperTimeline), - GlobalSteady(SteadyTimeline<'static>), -} - -impl Timeline for HybridTimeline { - fn cursor_pos(&self) -> PositionInSeconds { - match self { - HybridTimeline::ReaperProject(t) => t.cursor_pos(), - HybridTimeline::GlobalSteady(t) => t.cursor_pos(), - } - } - - fn next_quantized_pos_at( - &self, - timeline_pos: PositionInSeconds, - quantization: EvenQuantization, - laziness: Laziness, - ) -> QuantizedPosition { - match self { - HybridTimeline::ReaperProject(t) => { - t.next_quantized_pos_at(timeline_pos, quantization, laziness) - } - HybridTimeline::GlobalSteady(t) => { - t.next_quantized_pos_at(timeline_pos, quantization, laziness) - } - } - } - - fn full_beats_at_pos(&self, timeline_pos: PositionInSeconds) -> PositionInBeats { - match self { - HybridTimeline::ReaperProject(t) => t.full_beats_at_pos(timeline_pos), - HybridTimeline::GlobalSteady(t) => t.full_beats_at_pos(timeline_pos), - } - } - - fn pos_of_quantized_pos(&self, quantized_pos: QuantizedPosition) -> PositionInSeconds { - match self { - HybridTimeline::ReaperProject(t) => t.pos_of_quantized_pos(quantized_pos), - HybridTimeline::GlobalSteady(t) => t.pos_of_quantized_pos(quantized_pos), - } - } - - fn is_running(&self) -> bool { - match self { - HybridTimeline::ReaperProject(t) => t.is_running(), - HybridTimeline::GlobalSteady(t) => t.is_running(), - } - } - - fn tempo_at(&self, timeline_pos: PositionInSeconds) -> Bpm { - match self { - HybridTimeline::ReaperProject(t) => t.tempo_at(timeline_pos), - HybridTimeline::GlobalSteady(t) => t.tempo_at(timeline_pos), - } - } - - fn time_signature_at(&self, timeline_pos: PositionInSeconds) -> TimeSignature { - match self { - HybridTimeline::ReaperProject(t) => t.time_signature_at(timeline_pos), - HybridTimeline::GlobalSteady(t) => t.time_signature_at(timeline_pos), - } - } -} - -#[derive(Copy, Clone, Debug)] -pub struct QuantizedPosition { - position: i64, - denominator: u32, -} - -impl QuantizedPosition { - pub fn bar(position: i64) -> Self { - Self { - position, - denominator: 1, - } - } - - pub fn new(position: i64, denominator: u32) -> ClipEngineResult { - if denominator == 0 { - return Err("denominator must be > 0"); - } - let p = Self { - position, - denominator, - }; - Ok(p) - } - - /// The position, that is the number of intervals from timeline zero. - pub fn position(&self) -> i64 { - self.position - } - - pub fn quantization(&self) -> EvenQuantization { - EvenQuantization::new(1, self.denominator).unwrap() - } - - /// The quotient that divides the bar into multiple equally-sized portions. - /// - /// E.g. 16 if it's a sixteenth note or 1 if it's a whole bar. - pub fn denominator(&self) -> u32 { - self.denominator - } -} diff --git a/playtime-clip-engine/src/tracing_util.rs b/playtime-clip-engine/src/tracing_util.rs deleted file mode 100644 index a7afbdd69..000000000 --- a/playtime-clip-engine/src/tracing_util.rs +++ /dev/null @@ -1,7 +0,0 @@ -macro_rules! debug { - ($($tts:tt)*) => { - assert_no_alloc::permit_alloc(|| { - tracing::debug!($($tts)*); - }); - } -}