diff --git a/Cargo.lock b/Cargo.lock index a588e1367..e301d4d94 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3632,6 +3632,7 @@ checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" name = "playtime-api" version = "0.1.0" dependencies = [ + "nanoid", "realearn-macros", "schemars", "serde", diff --git a/main/src/infrastructure/data/clip_legacy.rs b/main/src/infrastructure/data/clip_legacy.rs index 76fb00d24..ce71ffd5e 100644 --- a/main/src/infrastructure/data/clip_legacy.rs +++ b/main/src/infrastructure/data/clip_legacy.rs @@ -24,6 +24,7 @@ pub(super) fn create_clip_matrix_from_legacy_slots( let output = determine_legacy_clip_track(desc.index, main_mappings, controller_mappings); let api_column = api::Column { + id: Default::default(), clip_play_settings: api::ColumnClipPlaySettings { track: output .resolve_track(containing_track.cloned())? @@ -33,7 +34,7 @@ pub(super) fn create_clip_matrix_from_legacy_slots( clip_record_settings: Default::default(), slots: { let api_clip = api::Clip { - id: None, + id: Default::default(), name: None, source: match desc.descriptor.content.clone() { ClipContent::File { file } => { @@ -59,6 +60,7 @@ pub(super) fn create_clip_matrix_from_legacy_slots( midi_settings: Default::default(), }; let api_slot = api::Slot { + id: Default::default(), // In the previous clip system, we had only one dimension. row: 0, clip_old: None, diff --git a/main/src/infrastructure/ui/main_panel.rs b/main/src/infrastructure/ui/main_panel.rs index 1f642de67..c005608b3 100644 --- a/main/src/infrastructure/ui/main_panel.rs +++ b/main/src/infrastructure/ui/main_panel.rs @@ -931,7 +931,7 @@ fn get_track_peaks(track: &Track) -> Vec { // 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 + 1024) }; + let volume = unsafe { reaper.track_get_peak_info(track, ch as u32) }; volume.get() }) .collect() diff --git a/playtime-api/Cargo.toml b/playtime-api/Cargo.toml index 7d117914e..591ca7300 100644 --- a/playtime-api/Cargo.toml +++ b/playtime-api/Cargo.toml @@ -9,4 +9,6 @@ serde.workspace = true # TODO-medium Actually not necessary anymore because we generate Dart directly now. schemars.workspace = true # For being able to use various attributes that can help in Rust-to-Dart code generation -realearn-macros.workspace = true \ No newline at end of file +realearn-macros.workspace = true +# For generating random IDs +nanoid.workspace = true \ No newline at end of file diff --git a/playtime-api/src/persistence/mod.rs b/playtime-api/src/persistence/mod.rs index 176e53837..139cba319 100644 --- a/playtime-api/src/persistence/mod.rs +++ b/playtime-api/src/persistence/mod.rs @@ -520,8 +520,55 @@ impl EvenQuantization { } } +#[derive(Clone, Eq, PartialEq, Hash, Debug, Serialize, Deserialize, JsonSchema)] +pub struct ColumnId(String); + +impl ColumnId { + pub fn random() -> Self { + Default::default() + } +} + +impl Default for ColumnId { + fn default() -> Self { + Self(nanoid::nanoid!()) + } +} + +#[derive(Clone, Eq, PartialEq, Hash, Debug, Serialize, Deserialize, JsonSchema)] +pub struct SlotId(String); + +impl SlotId { + pub fn random() -> Self { + Default::default() + } +} + +impl Default for SlotId { + fn default() -> Self { + Self(nanoid::nanoid!()) + } +} + +#[derive(Clone, Eq, PartialEq, Hash, Debug, Serialize, Deserialize, JsonSchema)] +pub struct ClipId(String); + +impl ClipId { + pub fn random() -> Self { + Default::default() + } +} + +impl Default for ClipId { + fn default() -> Self { + Self(nanoid::nanoid!()) + } +} + #[derive(Clone, PartialEq, Debug, Serialize, Deserialize, JsonSchema)] pub struct Column { + #[serde(default)] + pub id: ColumnId, pub clip_play_settings: ColumnClipPlaySettings, pub clip_record_settings: ColumnClipRecordSettings, /// Slots in this column. @@ -712,7 +759,7 @@ pub struct ReaperPitchShiftMode { pub sub_mode: u32, } -#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize, JsonSchema)] +#[derive(Copy, Clone, Eq, PartialEq, Debug, Serialize, Deserialize, JsonSchema)] #[serde(tag = "kind")] pub enum RecordOrigin { /// Records using the hardware input set for the track (MIDI or stereo). @@ -751,6 +798,8 @@ impl Default for RecordOrigin { #[derive(Clone, PartialEq, Debug, Serialize, Deserialize, JsonSchema)] pub struct Slot { + #[serde(default)] + pub id: SlotId, /// Slot index within the column (= row), starting at zero. pub row: usize, /// Clip which currently lives in this slot. @@ -773,8 +822,8 @@ impl Slot { #[derive(Clone, PartialEq, Debug, Serialize, Deserialize, JsonSchema)] pub struct Clip { - #[serde(skip_serializing_if = "Option::is_none")] - pub id: Option, + #[serde(default)] + pub id: ClipId, #[serde(skip_serializing_if = "Option::is_none")] pub name: Option, /// Source of the audio/MIDI material of this clip. diff --git a/playtime-clip-engine/src/base/clip.rs b/playtime-clip-engine/src/base/clip.rs index 04d901f6f..3e1ba0dcd 100644 --- a/playtime-clip-engine/src/base/clip.rs +++ b/playtime-clip-engine/src/base/clip.rs @@ -10,20 +10,17 @@ use crate::source_util::{ use crate::{rt, source_util, ClipEngineResult}; use crossbeam_channel::Sender; use playtime_api::persistence as api; -use playtime_api::persistence::{ClipColor, ClipTimeBase, Db, Section, SourceOrigin}; +use playtime_api::persistence::{ClipColor, ClipId, ClipTimeBase, Db, Section, SourceOrigin}; use reaper_high::{Project, Reaper, Track}; use reaper_medium::{Bpm, PeakFileMode}; -use std::fmt::{Display, Formatter}; +use std::fs; use std::future::Future; use std::path::{Path, PathBuf}; -use std::str::FromStr; -use std::{fmt, fs}; -use ulid::Ulid; /// Describes a clip. /// /// Not loaded yet. -#[derive(Clone, Debug)] +#[derive(Clone, PartialEq, Debug)] pub struct Clip { id: ClipId, name: Option, @@ -34,38 +31,11 @@ pub struct Clip { processing_relevant_settings: ProcessingRelevantClipSettings, } -#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug, Hash)] -pub struct ClipId(Ulid); - -impl ClipId { - pub fn random() -> Self { - Self(Ulid::new()) - } -} - -impl Display for ClipId { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - self.0.fmt(f) - } -} - -impl FromStr for ClipId { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - let ulid = Ulid::from_str(s).map_err(|_| "couldn't decode string as ULID")?; - Ok(ClipId(ulid)) - } -} - impl Clip { pub fn load(api_clip: api::Clip) -> Self { Self { processing_relevant_settings: ProcessingRelevantClipSettings::from_api(&api_clip), - id: api_clip - .id - .and_then(|s| ClipId::from_str(&s).ok()) - .unwrap_or_else(ClipId::random), + id: api_clip.id, name: api_clip.name, color: api_clip.color, source: api_clip.source, @@ -91,7 +61,7 @@ impl Clip { Audio { path, .. } => create_file_api_source(temporary_project, &path), }; let clip = Self { - id: ClipId::random(), + id: Default::default(), name: recording_track.name().map(|n| n.into_string()), color: ClipColor::PlayTrackColor, source: api_source, @@ -122,7 +92,7 @@ impl Clip { temporary_project: Option, ) -> ClipEngineResult { let clip = api::Clip { - id: Some(self.id.to_string()), + id: self.id.clone(), name: self.name.clone(), source: { if let Some(midi_source) = midi_source { @@ -290,8 +260,8 @@ impl Clip { ClipChangeEvent::Everything } - pub fn id(&self) -> ClipId { - self.id + pub fn id(&self) -> &ClipId { + &self.id } pub fn volume(&self) -> Db { diff --git a/playtime-clip-engine/src/base/column.rs b/playtime-clip-engine/src/base/column.rs index 9f4b0d113..6d0bc3da3 100644 --- a/playtime-clip-engine/src/base/column.rs +++ b/playtime-clip-engine/src/base/column.rs @@ -1,9 +1,9 @@ use crate::base::{Clip, ClipMatrixHandler, MatrixSettings, RelevantContent, Slot}; use crate::rt::supplier::{ChainEquipment, RecorderRequest}; use crate::rt::{ - ClipChangeEvent, ColumnCommandSender, ColumnEvent, ColumnFillSlotArgs, ColumnPlayRowArgs, + ClipChangeEvent, ColumnCommandSender, ColumnEvent, ColumnHandle, ColumnPlayRowArgs, ColumnPlaySlotArgs, ColumnStopArgs, ColumnStopSlotArgs, FillClipMode, - OverridableMatrixSettings, SharedColumn, SlotChangeEvent, WeakColumn, + OverridableMatrixSettings, SharedColumn, SlotChangeEvent, }; use crate::{rt, source_util, ClipEngineResult}; use crossbeam_channel::{Receiver, Sender}; @@ -13,8 +13,9 @@ use helgoboss_learn::UnitValue; use playtime_api::persistence as api; use playtime_api::persistence::{ preferred_clip_midi_settings, BeatTimeBase, ClipAudioSettings, ClipColor, ClipTimeBase, - ColumnClipPlayAudioSettings, ColumnClipPlaySettings, ColumnClipRecordSettings, ColumnPlayMode, - Db, MatrixClipRecordSettings, PositiveBeat, PositiveSecond, Section, TimeSignature, + ColumnClipPlayAudioSettings, ColumnClipPlaySettings, ColumnClipRecordSettings, ColumnId, + ColumnPlayMode, Db, MatrixClipRecordSettings, PositiveBeat, PositiveSecond, Section, SlotId, + TimeSignature, }; use reaper_high::{Guid, OrCurrentProject, Project, Reaper, Track}; use reaper_low::raw::preview_register_t; @@ -22,14 +23,16 @@ use reaper_medium::{ create_custom_owned_pcm_source, Bpm, CustomPcmSource, FlexibleOwnedPcmSource, HelpMode, MeasureAlignment, OwnedPreviewRegister, ReaperMutex, ReaperVolumeValue, }; -use std::iter; +use std::collections::HashMap; use std::ptr::NonNull; use std::sync::Arc; +use std::{iter, mem}; pub type SharedRegister = Arc>; #[derive(Clone, Debug)] pub struct Column { + id: ColumnId, settings: ColumnSettings, rt_settings: rt::ColumnSettings, rt_command_sender: ColumnCommandSender, @@ -45,6 +48,14 @@ 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, @@ -53,12 +64,13 @@ struct PlayingPreviewRegister { } impl Column { - pub fn new(permanent_project: Option) -> Self { + pub fn new(id: ColumnId, permanent_project: Option) -> Self { let (command_sender, command_receiver) = crossbeam_channel::bounded(500); let (event_sender, event_receiver) = crossbeam_channel::bounded(500); let source = rt::Column::new(permanent_project, command_receiver, event_sender); let shared_source = SharedColumn::new(source); Self { + id, settings: Default::default(), rt_settings: Default::default(), // preview_register: { @@ -73,16 +85,20 @@ impl Column { } } + pub fn id(&self) -> &ColumnId { + &self.id + } + pub fn set_play_mode(&mut self, play_mode: ColumnPlayMode) { self.rt_settings.play_mode = play_mode; } pub fn duplicate_without_contents(&self) -> Self { - let mut duplicate = Self::new(self.project); + let mut duplicate = Self::new(ColumnId::random(), self.project); duplicate.settings = self.settings.clone(); duplicate.rt_settings = self.rt_settings.clone(); if let Some(pr) = &self.preview_register { - duplicate.init_preview_register(pr.track.clone()); + duplicate.init_preview_register_if_necessary(pr.track.clone()); } duplicate } @@ -99,7 +115,6 @@ impl Column { recorder_request_sender: &Sender, matrix_settings: &MatrixSettings, ) -> ClipEngineResult<()> { - self.clear_slots(); // Track let track = if let Some(id) = api_column.clip_play_settings.track.as_ref() { let guid = Guid::from_string_without_braces(id.get())?; @@ -107,47 +122,54 @@ impl Column { } else { None }; - self.init_preview_register(track); + self.init_preview_register_if_necessary(track); // Settings - self.settings.clip_record_settings = api_column.clip_record_settings; - self.rt_settings.audio_resample_mode = - api_column.clip_play_settings.audio_settings.resample_mode; - self.rt_settings.audio_time_stretch_mode = api_column - .clip_play_settings - .audio_settings - .time_stretch_mode; - self.rt_settings.audio_cache_behavior = - api_column.clip_play_settings.audio_settings.cache_behavior; - self.rt_settings.play_mode = api_column.clip_play_settings.mode.unwrap_or_default(); - self.rt_settings.clip_play_start_timing = api_column.clip_play_settings.start_timing; - self.rt_settings.clip_play_stop_timing = api_column.clip_play_settings.stop_timing; + self.settings = ColumnSettings::from_api(&api_column); + self.rt_settings = rt::ColumnSettings::from_api(&api_column); // Slots + let mut old_slots: HashMap<_, _> = mem::take(&mut self.slots) + .into_iter() + .map(|s| (s.id().clone(), s)) + .collect(); for api_slot in api_column.slots.unwrap_or_default() { let row = api_slot.row; - for api_clip in api_slot.into_clips() { - let clip = Clip::load(api_clip); - let slot = get_slot_mut_insert(&mut self.slots, row); - fill_slot_with_clip_internal( - slot, - clip, - chain_equipment, - recorder_request_sender, - matrix_settings, - &self.rt_settings, - &self.rt_command_sender, - self.project, - FillClipMode::Add, - )?; - } + let mut slot = if let Some(mut old_slot) = old_slots.remove(&api_slot.id) { + old_slot.set_index(row); + old_slot + } else { + Slot::new(api_slot.id.clone(), row) + }; + slot.load( + api_slot.into_clips(), + chain_equipment, + recorder_request_sender, + matrix_settings, + &self.rt_settings, + &self.rt_command_sender, + self.project, + )?; + *get_slot_mut_insert(&mut self.slots, row) = slot; } + // Sync + self.sync_matrix_settings_to_rt_internal(matrix_settings); Ok(()) } - fn init_preview_register(&mut self, track: Option) { + fn init_preview_register_if_necessary(&mut self, track: Option) { + if let Some(r) = &self.preview_register { + if r.track == track { + // No need to init. Column already uses a preview register for that track. + return; + } + } self.preview_register = Some(PlayingPreviewRegister::new(self.rt_column.clone(), track)); } - pub fn sync_settings_to_rt(&self, matrix_settings: &MatrixSettings) { + pub fn sync_matrix_settings_to_rt(&self, matrix_settings: &MatrixSettings) { + self.sync_matrix_settings_to_rt_internal(matrix_settings); + } + + fn sync_matrix_settings_to_rt_internal(&self, matrix_settings: &MatrixSettings) { self.rt_command_sender .update_settings(self.rt_settings.clone()); self.rt_command_sender @@ -220,6 +242,7 @@ impl Column { .map(api::TrackId::new) }); api::Column { + id: self.id.clone(), clip_play_settings: ColumnClipPlaySettings { mode: Some(self.rt_settings.play_mode), track: track_id, @@ -243,8 +266,11 @@ impl Column { } } - pub fn rt_column(&self) -> WeakColumn { - self.rt_column.downgrade() + 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)> { @@ -406,8 +432,7 @@ impl Column { return Err("slot is not empty"); } let clip = Clip::load(api_clip); - fill_slot_with_clip_internal( - slot, + slot.fill_slot_with_clip_internal( clip, chain_equipment, recorder_request_sender, @@ -431,7 +456,7 @@ impl Column { let source = source_util::create_api_source_from_item(item, false) .map_err(|_| "couldn't create source from item")?; let clip = api::Clip { - id: None, + id: Default::default(), name: None, source, frozen_source: None, @@ -665,7 +690,7 @@ fn upsize_if_necessary(slots: &mut Vec, row_count: usize) { let mut current_row_count = slots.len(); if current_row_count < row_count { slots.resize_with(row_count, || { - let slot = Slot::new(current_row_count); + let slot = Slot::new(SlotId::random(), current_row_count); current_row_count += 1; slot }); @@ -674,35 +699,6 @@ fn upsize_if_necessary(slots: &mut Vec, row_count: usize) { const SLOT_DOESNT_EXIST: &str = "slot doesn't exist"; -#[allow(clippy::too_many_arguments)] -fn fill_slot_with_clip_internal( - slot: &mut Slot, - mut clip: Clip, - chain_equipment: &ChainEquipment, - recorder_request_sender: &Sender, - matrix_settings: &MatrixSettings, - column_settings: &rt::ColumnSettings, - rt_command_sender: &ColumnCommandSender, - project: Option, - mode: FillClipMode, -) -> ClipEngineResult { - let (rt_clip, pooled_midi_source) = clip.create_real_time_clip( - project, - chain_equipment, - recorder_request_sender, - &matrix_settings.overridable, - column_settings, - )?; - slot.fill_with_clip(clip, &rt_clip, pooled_midi_source, mode); - let args = ColumnFillSlotArgs { - slot_index: slot.index(), - clip: rt_clip, - mode, - }; - rt_command_sender.fill_slot_with_clip(Box::new(Some(args))); - Ok(SlotChangeEvent::Clips("filled slot")) -} - fn resolve_recording_track( column_settings: &ColumnClipRecordSettings, playback_track: &Track, diff --git a/playtime-clip-engine/src/base/history.rs b/playtime-clip-engine/src/base/history.rs index 0bb79d3f7..e1b3f45f9 100644 --- a/playtime-clip-engine/src/base/history.rs +++ b/playtime-clip-engine/src/base/history.rs @@ -2,17 +2,18 @@ use crate::ClipEngineResult; use playtime_api::persistence as api; /// Data structure holding the undo history. -#[derive(Debug, Default)] +#[derive(Debug)] pub struct History { undo_stack: Vec, redo_stack: Vec, } impl History { - /// Clears the complete history. - pub fn clear(&mut self) { - self.redo_stack.clear(); - self.undo_stack.clear(); + pub fn new(initial_matrix: api::Matrix) -> Self { + Self { + undo_stack: vec![State::new("Initial".to_string(), initial_matrix)], + redo_stack: vec![], + } } /// Returns the label of the next undoable action if there is one. diff --git a/playtime-clip-engine/src/base/matrix.rs b/playtime-clip-engine/src/base/matrix.rs index 911973656..ad201dcef 100644 --- a/playtime-clip-engine/src/base/matrix.rs +++ b/playtime-clip-engine/src/base/matrix.rs @@ -20,14 +20,15 @@ use helgoboss_learn::UnitValue; use helgoboss_midi::Channel; use playtime_api::persistence as api; use playtime_api::persistence::{ - ChannelRange, ClipPlayStartTiming, ClipPlayStopTiming, ColumnPlayMode, Db, + ChannelRange, ClipPlayStartTiming, ClipPlayStopTiming, ColumnId, ColumnPlayMode, Db, MatrixClipPlayAudioSettings, MatrixClipPlaySettings, MatrixClipRecordSettings, RecordLength, TempoRange, }; use reaper_high::{OrCurrentProject, Project, Reaper, Track}; use reaper_medium::{Bpm, MidiInputDeviceId}; +use std::collections::HashMap; use std::thread::JoinHandle; -use std::{cmp, thread}; +use std::{cmp, mem, thread}; #[derive(Debug)] pub struct Matrix { @@ -66,19 +67,40 @@ pub struct MatrixSettings { 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 { + clip_play_start_timing: matrix.clip_play_settings.start_timing, + clip_play_stop_timing: matrix.clip_play_settings.stop_timing, + audio_time_stretch_mode: matrix.clip_play_settings.audio_settings.time_stretch_mode, + audio_resample_mode: matrix.clip_play_settings.audio_settings.resample_mode, + audio_cache_behavior: matrix.clip_play_settings.audio_settings.cache_behavior, + }, + } + } +} + #[derive(Debug)] pub enum MatrixCommand { - ThrowAway(ColumnHandle), + ThrowAway(MatrixGarbage), +} + +#[derive(Debug)] +pub enum MatrixGarbage { + ColumnHandles(Vec), } pub trait MainMatrixCommandSender { - fn throw_away(&self, handle: ColumnHandle); + fn throw_away(&self, garbage: MatrixGarbage); fn send_command(&self, command: MatrixCommand); } impl MainMatrixCommandSender for Sender { - fn throw_away(&self, handle: ColumnHandle) { - self.send_command(MatrixCommand::ThrowAway(handle)); + fn throw_away(&self, garbage: MatrixGarbage) { + self.send_command(MatrixCommand::ThrowAway(garbage)); } fn send_command(&self, command: MatrixCommand) { @@ -199,7 +221,7 @@ impl Matrix { containing_track, command_receiver: main_command_receiver, rt_command_sender, - history: History::default(), + history: History::new(Default::default()), clipboard: Default::default(), _worker_pool: worker_pool, } @@ -210,69 +232,45 @@ impl Matrix { } pub fn load(&mut self, api_matrix: api::Matrix) -> ClipEngineResult<()> { + self.columns.clear(); self.load_internal(api_matrix)?; - self.clear_history(); + self.history = History::new(self.save()); Ok(()) } - fn clear_history(&mut self) { - self.history.clear(); - self.emit(ClipMatrixEvent::HistoryChanged); - } - - // TODO-medium We might be able to improve that to take API matrix by reference. This would - // slightly benefit undo/redo performance. fn load_internal(&mut self, api_matrix: api::Matrix) -> ClipEngineResult<()> { - self.clear_columns(); let permanent_project = self.permanent_project(); - // Main settings - self.settings.common_tempo_range = api_matrix.common_tempo_range; - self.settings.overridable.audio_resample_mode = - api_matrix.clip_play_settings.audio_settings.resample_mode; - self.settings.overridable.audio_time_stretch_mode = api_matrix - .clip_play_settings - .audio_settings - .time_stretch_mode; - self.settings.overridable.audio_cache_behavior = - api_matrix.clip_play_settings.audio_settings.cache_behavior; - self.settings.clip_record_settings = api_matrix.clip_record_settings; - // Real-time settings - self.settings.overridable.clip_play_start_timing = - api_matrix.clip_play_settings.start_timing; - self.settings.overridable.clip_play_stop_timing = api_matrix.clip_play_settings.stop_timing; - // Columns - for (i, api_column) in api_matrix - .columns - .unwrap_or_default() + self.settings = MatrixSettings::from_api(&api_matrix); + let mut old_columns: HashMap<_, _> = mem::take(&mut self.columns) .into_iter() - .enumerate() - { - let mut column = Column::new(permanent_project); + .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)); column.load( api_column, &self.chain_equipment, &self.recorder_request_sender, &self.settings, )?; - column.sync_settings_to_rt(&self.settings); - initialize_new_column(i, column, &self.rt_command_sender, &mut self.columns); + self.columns.push(column); } - // Rows self.rows = api_matrix .rows .unwrap_or_default() .into_iter() .map(|_| Row {}) .collect(); - // Emit event self.notify_everything_changed(); + self.sync_column_handles_to_rt(); Ok(()) } - fn clear_columns(&mut self) { - // TODO-medium How about suspension? - self.columns.clear(); - self.rt_command_sender.clear_columns(); + fn sync_column_handles_to_rt(&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 next_undo_label(&self) -> Option<&str> { @@ -292,6 +290,7 @@ impl Matrix { } pub fn undo(&mut self) -> ClipEngineResult<()> { + // TODO-medium-clip-engine We could probably make it work without clone. let api_matrix = self.history.undo()?.clone(); self.load_internal(api_matrix)?; self.emit(ClipMatrixEvent::HistoryChanged); @@ -766,6 +765,7 @@ impl Matrix { clips: Vec, row_index: usize, ) -> ClipEngineResult<()> { + let mut need_handle_sync = false; for clip in clips { // First try to put it within same column as clip itself let original_column = get_column_mut(&mut self.columns, clip.column_index)?; @@ -789,14 +789,9 @@ impl Matrix { let same_column = self.columns.get(clip.column_index).unwrap(); let mut duplicate = same_column.duplicate_without_contents(); duplicate.set_play_mode(ColumnPlayMode::ExclusiveFollowingScene); - duplicate.sync_settings_to_rt(&self.settings); - let duplicate_index = self.columns.len(); - initialize_new_column( - duplicate_index, - duplicate, - &self.rt_command_sender, - &mut self.columns, - ); + duplicate.sync_matrix_settings_to_rt(&self.settings); + self.columns.push(duplicate); + need_handle_sync = true; self.columns.last_mut().unwrap() } }; @@ -809,6 +804,9 @@ impl Matrix { FillClipMode::Replace, )?; } + if need_handle_sync { + self.sync_column_handles_to_rt(); + } Ok(()) } @@ -1297,17 +1295,3 @@ impl WithColumn { pub type SlotWithColumn<'a> = WithColumn<&'a Slot>; pub type ApiClipWithColumn = WithColumn; - -fn initialize_new_column( - column_index: usize, - column: Column, - rt_command_sender: &Sender, - columns: &mut Vec, -) { - let handle = ColumnHandle { - pointer: column.rt_column(), - command_sender: column.rt_command_sender().clone(), - }; - rt_command_sender.insert_column(column_index, handle); - columns.push(column); -} diff --git a/playtime-clip-engine/src/base/slot.rs b/playtime-clip-engine/src/base/slot.rs index 3ebf91023..65812b5ea 100644 --- a/playtime-clip-engine/src/base/slot.rs +++ b/playtime-clip-engine/src/base/slot.rs @@ -1,7 +1,7 @@ use crate::base::{ create_api_source_from_recorded_midi_source, Clip, ClipMatrixHandler, ClipRecordDestination, ClipRecordHardwareInput, ClipRecordHardwareMidiInput, ClipRecordInput, ClipRecordTask, - VirtualClipRecordAudioInput, VirtualClipRecordHardwareMidiInput, + MatrixSettings, VirtualClipRecordAudioInput, VirtualClipRecordHardwareMidiInput, }; use crate::conversion_util::adjust_duration_in_secs_anti_proportionally; use crate::rt::supplier::{ @@ -9,10 +9,10 @@ use crate::rt::supplier::{ RecorderRequest, RecordingArgs, RecordingEquipment, SupplierChain, }; use crate::rt::{ - ClipChangeEvent, ClipRecordArgs, ColumnCommandSender, ColumnSetClipLoopedArgs, FillClipMode, - InternalClipPlayState, MidiOverdubInstruction, NormalRecordingOutcome, - OverridableMatrixSettings, RecordNewClipInstruction, SharedColumn, SlotChangeEvent, - SlotRecordInstruction, SlotRuntimeData, + ClipChangeEvent, ClipRecordArgs, ColumnCommandSender, ColumnFillSlotArgs, + ColumnSetClipLoopedArgs, FillClipMode, InternalClipPlayState, MidiOverdubInstruction, + NormalRecordingOutcome, OverridableMatrixSettings, RecordNewClipInstruction, SharedColumn, + 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, QuantizedPosition, Timeline}; @@ -22,7 +22,7 @@ use helgoboss_learn::UnitValue; use playtime_api::persistence as api; use playtime_api::persistence::{ ChannelRange, ClipTimeBase, ColumnClipRecordSettings, Db, MatrixClipRecordSettings, - MidiClipRecordMode, PositiveSecond, RecordOrigin, + MidiClipRecordMode, PositiveSecond, RecordOrigin, SlotId, }; use playtime_api::runtime::ClipPlayState; use reaper_high::{BorrowedSource, Item, OwnedSource, Project, Reaper, Take, Track, TrackRoute}; @@ -30,11 +30,13 @@ use reaper_medium::{ Bpm, CommandId, DurationInSeconds, PositionInSeconds, RecordingInput, RequiredViewMode, TrackArea, UiRefreshBehavior, }; +use std::collections::HashMap; use std::ptr::null_mut; use std::{iter, mem}; #[derive(Clone, Debug)] pub struct Slot { + id: SlotId, index: usize, /// If this is set, the slot contains a clip. /// @@ -66,6 +68,26 @@ pub struct Content { } impl Content { + pub fn new(clip: Clip, rt_clip: &rt::Clip, pooled_midi_source: Option) -> Self { + Content { + clip, + runtime_data: SlotRuntimeData { + play_state: Default::default(), + pos: rt_clip.shared_pos(), + peak: rt_clip.shared_peak(), + material_info: rt_clip.material_info().unwrap(), + }, + pooled_midi_source, + } + } + + pub fn apply(&mut self, api_clip: api::Clip) { + if self.clip == Clip::load(api_clip) { + return; + } + // TODO-high CONTINUE Actually apply clip differences! + } + /// Returns the effective length (tempo adjusted and taking the section into account). pub fn effective_length_in_seconds( &self, @@ -172,8 +194,9 @@ impl Content { } impl Slot { - pub fn new(index: usize) -> Self { + pub fn new(id: SlotId, index: usize) -> Self { Self { + id, index, contents: vec![], state: Default::default(), @@ -181,6 +204,10 @@ impl Slot { } } + pub fn id(&self) -> &SlotId { + &self.id + } + pub fn is_empty(&self) -> bool { self.contents.is_empty() && !self.state.is_pretty_much_recording() } @@ -189,6 +216,10 @@ impl Slot { 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, temporary_project: Option) -> Option { let slot_is_pretty_much_recording = self.state.is_pretty_much_recording(); @@ -217,6 +248,7 @@ impl Slot { return None; } let api_slot = api::Slot { + id: self.id.clone(), row: self.index, clip_old: None, clips: Some(clips), @@ -224,6 +256,78 @@ impl Slot { Some(api_slot) } + pub fn load( + &mut self, + api_clips: Vec, + chain_equipment: &ChainEquipment, + recorder_request_sender: &Sender, + matrix_settings: &MatrixSettings, + column_settings: &rt::ColumnSettings, + rt_command_sender: &ColumnCommandSender, + project: Option, + ) -> ClipEngineResult<()> { + let mut old_contents: HashMap<_, _> = mem::take(&mut self.contents) + .into_iter() + .map(|c| (c.clip.id().clone(), c)) + .collect(); + for api_clip in api_clips { + let content = if let Some(mut old_content) = old_contents.remove(&api_clip.id) { + // TODO-high CONTINUE Important to update content + old_content.apply(api_clip); + old_content + } else { + let mut clip = Clip::load(api_clip); + let (rt_clip, pooled_midi_source) = clip.create_real_time_clip( + project, + chain_equipment, + recorder_request_sender, + &matrix_settings.overridable, + column_settings, + )?; + let content = Content::new(clip, &rt_clip, pooled_midi_source); + // TODO-high CONTINUE Not correct anymore if this is undo/redo + let args = ColumnFillSlotArgs { + slot_index: self.index(), + clip: rt_clip, + mode: FillClipMode::Add, + }; + rt_command_sender.fill_slot_with_clip(Box::new(Some(args))); + content + }; + self.contents.push(content); + } + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + pub fn fill_slot_with_clip_internal( + &mut self, + mut clip: Clip, + chain_equipment: &ChainEquipment, + recorder_request_sender: &Sender, + matrix_settings: &MatrixSettings, + column_settings: &rt::ColumnSettings, + rt_command_sender: &ColumnCommandSender, + project: Option, + mode: FillClipMode, + ) -> ClipEngineResult { + let (rt_clip, pooled_midi_source) = clip.create_real_time_clip( + project, + chain_equipment, + recorder_request_sender, + &matrix_settings.overridable, + column_settings, + )?; + self.fill_with_clip(clip, &rt_clip, pooled_midi_source, mode); + let args = ColumnFillSlotArgs { + slot_index: self.index(), + clip: rt_clip, + mode, + }; + rt_command_sender.fill_slot_with_clip(Box::new(Some(args))); + Ok(SlotChangeEvent::Clips("filled slot")) + } + pub fn is_recording(&self) -> bool { self.state.is_pretty_much_recording() } @@ -731,16 +835,7 @@ impl Slot { pooled_midi_source: Option, mode: FillClipMode, ) { - let content = Content { - clip, - runtime_data: SlotRuntimeData { - play_state: Default::default(), - pos: rt_clip.shared_pos(), - peak: rt_clip.shared_peak(), - material_info: rt_clip.material_info().unwrap(), - }, - pooled_midi_source, - }; + let content = Content::new(clip, rt_clip, pooled_midi_source); match mode { FillClipMode::Add => { self.contents.push(content); diff --git a/playtime-clip-engine/src/proto/mod.rs b/playtime-clip-engine/src/proto/mod.rs index 30677f9e1..30e407c97 100644 --- a/playtime-clip-engine/src/proto/mod.rs +++ b/playtime-clip-engine/src/proto/mod.rs @@ -104,6 +104,7 @@ impl qualified_occasional_slot_update::Update { let api_slot = slot.save(matrix.permanent_project()) .unwrap_or(playtime_api::persistence::Slot { + id: slot.id().clone(), row: slot.index(), clip_old: None, clips: None, diff --git a/playtime-clip-engine/src/rt/clip.rs b/playtime-clip-engine/src/rt/clip.rs index 89d81f86f..907d62a90 100644 --- a/playtime-clip-engine/src/rt/clip.rs +++ b/playtime-clip-engine/src/rt/clip.rs @@ -1813,7 +1813,7 @@ pub struct CommittedRecording { /// All settings of a clip that affect processing. /// /// To be sent back to the main thread to update the main thread clip. -#[derive(Clone, Debug)] +#[derive(Clone, PartialEq, Debug)] pub struct ProcessingRelevantClipSettings { pub time_base: api::ClipTimeBase, pub looped: bool, diff --git a/playtime-clip-engine/src/rt/column.rs b/playtime-clip-engine/src/rt/column.rs index b3850757d..2588a2446 100644 --- a/playtime-clip-engine/src/rt/column.rs +++ b/playtime-clip-engine/src/rt/column.rs @@ -297,6 +297,22 @@ pub struct ColumnSettings { pub play_mode: ColumnPlayMode, } +impl ColumnSettings { + pub fn from_api(api_column: &api::Column) -> Self { + Self { + clip_play_start_timing: api_column.clip_play_settings.start_timing, + clip_play_stop_timing: api_column.clip_play_settings.stop_timing, + audio_time_stretch_mode: api_column + .clip_play_settings + .audio_settings + .time_stretch_mode, + audio_resample_mode: api_column.clip_play_settings.audio_settings.resample_mode, + audio_cache_behavior: api_column.clip_play_settings.audio_settings.cache_behavior, + play_mode: api_column.clip_play_settings.mode.unwrap_or_default(), + } + } +} + #[derive(Clone, Debug, Default)] pub struct OverridableMatrixSettings { pub clip_play_start_timing: ClipPlayStartTiming, diff --git a/playtime-clip-engine/src/rt/matrix.rs b/playtime-clip-engine/src/rt/matrix.rs index 5c96b5c12..b767ab23a 100644 --- a/playtime-clip-engine/src/rt/matrix.rs +++ b/playtime-clip-engine/src/rt/matrix.rs @@ -1,4 +1,4 @@ -use crate::base::{ClipSlotAddress, MainMatrixCommandSender}; +use crate::base::{ClipSlotAddress, MainMatrixCommandSender, MatrixGarbage}; use crate::mutex_util::non_blocking_lock; use crate::rt::{ BasicAudioRequestProps, ColumnCommandSender, ColumnPlayClipOptions, ColumnPlayRowArgs, @@ -112,17 +112,10 @@ impl Matrix { while let Ok(command) = self.command_receiver.try_recv() { use MatrixCommand::*; match command { - InsertColumn(index, handle) => { - self.column_handles.insert(index, handle); - } - RemoveColumn(index) => { - let handle = self.column_handles.remove(index); - self.main_command_sender.throw_away(handle); - } - ClearColumns => { - for handles in self.column_handles.drain(..) { - self.main_command_sender.throw_away(handles); - } + SetColumnHandles(handles) => { + let old_handles = mem::replace(&mut self.column_handles, handles); + self.main_command_sender + .throw_away(MatrixGarbage::ColumnHandles(old_handles)); } } } @@ -286,29 +279,17 @@ impl Matrix { } pub enum MatrixCommand { - InsertColumn(usize, ColumnHandle), - RemoveColumn(usize), - ClearColumns, + SetColumnHandles(Vec), } pub trait RtMatrixCommandSender { - fn insert_column(&self, index: usize, handle: ColumnHandle); - fn remove_column(&self, index: usize); - fn clear_columns(&self); + fn set_column_handles(&self, handles: Vec); fn send_command(&self, command: MatrixCommand); } impl RtMatrixCommandSender for Sender { - fn insert_column(&self, index: usize, handle: ColumnHandle) { - self.send_command(MatrixCommand::InsertColumn(index, handle)); - } - - fn remove_column(&self, index: usize) { - self.send_command(MatrixCommand::RemoveColumn(index)); - } - - fn clear_columns(&self) { - self.send_command(MatrixCommand::ClearColumns); + fn set_column_handles(&self, handles: Vec) { + self.send_command(MatrixCommand::SetColumnHandles(handles)); } fn send_command(&self, command: MatrixCommand) {