diff --git a/.prettierrc b/.prettierrc index 138d666b..0a2aa530 100644 --- a/.prettierrc +++ b/.prettierrc @@ -10,5 +10,6 @@ "importOrderCaseInsensitive": true, "importOrderMergeDuplicateImports": true, "importOrderParserPlugins": ["typescript", "jsx", "decorators-legacy"], - "importOrderBuiltinModulesToTop": true + "importOrderBuiltinModulesToTop": true, + "parser": "babel-ts" } diff --git a/backend/migrations/2024-07-28-050006_composition-versions/down.sql b/backend/migrations/2024-07-28-050006_composition-versions/down.sql new file mode 100644 index 00000000..96af6686 --- /dev/null +++ b/backend/migrations/2024-07-28-050006_composition-versions/down.sql @@ -0,0 +1,2 @@ +ALTER TABLE compositions DROP COLUMN IF EXISTS composition_version; +ALTER TABLE compositions DROP COLUMN IF EXISTS parent_id; diff --git a/backend/migrations/2024-07-28-050006_composition-versions/up.sql b/backend/migrations/2024-07-28-050006_composition-versions/up.sql new file mode 100644 index 00000000..35f08667 --- /dev/null +++ b/backend/migrations/2024-07-28-050006_composition-versions/up.sql @@ -0,0 +1,4 @@ +ALTER TABLE compositions ADD COLUMN IF NOT EXISTS composition_version INT NOT NULL DEFAULT 0; +ALTER TABLE compositions ADD COLUMN IF NOT EXISTS parent_id BIGINT NULL DEFAULT NULL; + +ALTER TABLE compositions ADD CONSTRAINT IF NOT EXISTS parent_id_composition_version UNIQUE (parent_id, composition_version); diff --git a/backend/rustfmt.toml b/backend/rustfmt.toml index 79c171f3..b4a44097 100644 --- a/backend/rustfmt.toml +++ b/backend/rustfmt.toml @@ -16,3 +16,5 @@ match_arm_blocks = false overflow_delimited_expr = true edition = "2018" normalize_doc_attributes = true +# todo: enable \/ and format project +# tab_spaces = 2 diff --git a/backend/src/db_util/mod.rs b/backend/src/db_util/mod.rs index cd53959f..ce85471a 100644 --- a/backend/src/db_util/mod.rs +++ b/backend/src/db_util/mod.rs @@ -16,7 +16,7 @@ pub mod private_sample_libraries; // Facilitate getting the primary key of the last inserted item // // https://github.com/diesel-rs/diesel/issues/1011#issuecomment-315536931 -sql_function! { +define_sql_function! { fn last_insert_id() -> BigInt; } diff --git a/backend/src/models/compositions.rs b/backend/src/models/compositions.rs index ad00f2e5..f023864b 100644 --- a/backend/src/models/compositions.rs +++ b/backend/src/models/compositions.rs @@ -9,6 +9,9 @@ pub struct NewCompositionRequest { pub content: Map, #[serde(default)] pub tags: Vec, + #[serde(default)] + #[serde(rename = "parentID")] + pub parent_id: Option, } #[derive(Insertable)] @@ -18,6 +21,17 @@ pub struct NewComposition { pub description: String, pub content: String, pub user_id: Option, + pub composition_version: i32, + pub parent_id: Option, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CompositionVersion { + pub id: i64, + pub title: String, + pub description: String, + pub composition_version: i32, } #[derive(Serialize)] @@ -29,6 +43,7 @@ pub struct CompositionDescriptor { pub tags: Vec, pub user_id: Option, pub user_name: Option, + pub versions: Vec, } #[derive(Serialize, Queryable)] @@ -39,6 +54,8 @@ pub struct Composition { pub description: String, pub content: String, pub user_id: Option, + pub composition_version: i32, + pub parent_id: Option, } #[derive(Insertable)] diff --git a/backend/src/routes/mod.rs b/backend/src/routes/mod.rs index 26075d13..75f3acaa 100644 --- a/backend/src/routes/mod.rs +++ b/backend/src/routes/mod.rs @@ -10,8 +10,8 @@ use crate::{ }, models::{ compositions::{ - Composition, CompositionDescriptor, NewComposition, NewCompositionRequest, - NewCompositionTag, + Composition, CompositionDescriptor, CompositionVersion, NewComposition, + NewCompositionRequest, NewCompositionTag, }, effects::{Effect, InsertableEffect}, synth_preset::{ @@ -95,6 +95,65 @@ pub async fn save_composition( login_token: MaybeLoginToken, ) -> Result, String> { let user_id = get_logged_in_user_id(&conn, login_token).await; + + // if adding a version to an existing composition, the user must be logged in and own the parent + let (parent_id, composition_version) = if let Some(parent_id) = composition.0.parent_id { + let Some(user_id) = user_id else { + return Err("User must be logged in to save a new version of a composition".to_owned()); + }; + + let (id, parent_id) = match conn + .run( + move |conn| -> QueryResult, Option)>> { + schema::compositions::table + .filter(schema::compositions::dsl::id.eq(parent_id)) + .select(( + schema::compositions::dsl::id, + schema::compositions::dsl::parent_id, + schema::compositions::dsl::user_id, + )) + .order_by(schema::compositions::dsl::composition_version.asc()) + .first(conn) + .optional() + }, + ) + .await + { + Ok(Some((id, parent_id, parent_user_id))) => { + if parent_user_id != Some(user_id) { + return Err("User does not own the parent composition".to_owned()); + } + + (id, parent_id) + }, + Ok(None) => return Err("Parent composition not found".to_owned()), + Err(err) => { + error!("Error querying parent composition: {:?}", err); + return Err("Error querying parent composition from the database".to_owned()); + }, + }; + + let root_parent_id = parent_id.unwrap_or(id); + let latest_version: Option = conn + .run(move |conn| { + schema::compositions::table + .filter(schema::compositions::dsl::parent_id.eq(Some(root_parent_id))) + .select(diesel::dsl::max( + schema::compositions::dsl::composition_version, + )) + .first::>(conn) + }) + .await + .map_err(|err| { + error!("Error querying composition version: {:?}", err); + "Error querying composition version from the database".to_owned() + })?; + + (Some(root_parent_id), latest_version.unwrap_or(0) + 1) + } else { + (None, 0) + }; + let new_composition = NewComposition { title: composition.0.title, description: composition.0.description, @@ -103,8 +162,14 @@ pub async fn save_composition( format!("Failed to serialize composition to JSON string") })?, user_id, + composition_version, + parent_id, + }; + let tags: Vec = if new_composition.parent_id.is_none() { + std::mem::take(&mut composition.0.tags) + } else { + Vec::new() }; - let tags: Vec = std::mem::take(&mut composition.0.tags); let saved_composition_id = conn .run(move |conn| { @@ -148,26 +213,38 @@ pub async fn save_composition( Ok(Json(saved_composition_id)) } +async fn query_composition_by_id( + conn: WebSynthDbConn, + composition_id: i64, +) -> QueryResult> { + use crate::schema::compositions; + + conn.run(move |conn| { + compositions::table + .filter( + compositions::dsl::id + .eq(composition_id) + .or(compositions::dsl::parent_id.eq(composition_id)), + ) + .order_by(compositions::dsl::composition_version.desc()) + .first::(conn) + .optional() + }) + .await +} + #[get("/compositions/")] pub async fn get_composition_by_id( conn: WebSynthDbConn, composition_id: i64, ) -> Result>, String> { - use crate::schema::compositions::dsl::*; - - let composition_opt = match conn - .run(move |conn| compositions.find(composition_id).first::(conn)) + query_composition_by_id(conn, composition_id) .await - { - Ok(composition) => Some(Json(composition)), - Err(diesel::NotFound) => None, - Err(err) => { - error!("Error querying composition by id: {:?}", err); - return Err("Error querying composition by id from the database".to_string()); - }, - }; - - Ok(composition_opt) + .map_err(|err| { + error!("Error querying composition: {:?}", err); + "Error querying composition from the database".to_string() + }) + .map(|comp| comp.map(Json)) } #[get("/compositions")] @@ -178,7 +255,7 @@ pub async fn get_compositions( let (all_compos, all_compos_tags) = conn .run( - |conn| -> QueryResult<(Vec<(_, _, _, _, _)>, Vec)> { + |conn| -> QueryResult<(Vec<(_, _, _, _, Option, i32, _)>, Vec)> { let all_compos = compositions::table .left_join( users::table.on(compositions::dsl::user_id.eq(users::dsl::id.nullable())), @@ -188,8 +265,11 @@ pub async fn get_compositions( compositions::dsl::title, compositions::dsl::description, compositions::dsl::user_id, + compositions::dsl::parent_id, + compositions::dsl::composition_version, users::dsl::username.nullable(), )) + .order_by(compositions::dsl::id.asc()) .load(conn)?; let all_compos_tags: Vec = compositions_tags::table @@ -201,7 +281,7 @@ pub async fn get_compositions( ) .await .map_err(|err| { - error!("Error querying compositions: {:?}", err); + error!("Error querying compositions: {err:?}"); "Error querying compositions from the database".to_string() })?; @@ -209,26 +289,49 @@ pub async fn get_compositions( .into_iter() .into_group_map_by(|tag| tag.entity_id); - let all_compos = all_compos - .into_iter() - .map(|(id, title, description, user_id, user_name)| { - let tags = tags_by_compo_id - .remove(&id) - .unwrap_or_default() - .iter() - .map(|tag| tag.tag.clone()) - .collect_vec(); - - CompositionDescriptor { + let mut compositions_by_root_id: FxHashMap = FxHashMap::default(); + + for (id, title, description, user_id, parent_id, composition_version, user_name) in all_compos { + if let Some(parent_id) = parent_id { + let Some(parent) = compositions_by_root_id.get_mut(&parent_id) else { + error!( + "Composition with parent_id={parent_id} not found; versions should have been \ + ordered after the parent" + ); + continue; + }; + + parent.versions.push(CompositionVersion { id, title, description, - tags, - user_id, - user_name, - } - }) - .collect_vec(); + composition_version, + }); + + continue; + } + + let tags = tags_by_compo_id + .remove(&id) + .unwrap_or_default() + .iter() + .map(|tag| tag.tag.clone()) + .collect_vec(); + + let descriptor = CompositionDescriptor { + id, + title, + description, + tags, + user_id, + user_name, + versions: Vec::new(), + }; + compositions_by_root_id.insert(id, descriptor); + } + + let mut all_compos = compositions_by_root_id.into_values().collect_vec(); + all_compos.sort_unstable_by_key(|comp| comp.id); Ok(Json(all_compos)) } diff --git a/backend/src/schema.rs b/backend/src/schema.rs index 3b443292..1c0cad7a 100644 --- a/backend/src/schema.rs +++ b/backend/src/schema.rs @@ -7,6 +7,8 @@ diesel::table! { description -> Text, content -> Longtext, user_id -> Nullable, + composition_version -> Integer, + parent_id -> Nullable, } } diff --git a/engine/wavetable/src/fm/effects/moog.rs b/engine/wavetable/src/fm/effects/moog.rs index 1119a5f4..ef945bc6 100644 --- a/engine/wavetable/src/fm/effects/moog.rs +++ b/engine/wavetable/src/fm/effects/moog.rs @@ -62,20 +62,9 @@ impl Effect for MoogFilter { let resonance = unsafe { *rendered_params.get_unchecked(1) }; let drive = unsafe { *rendered_params.get_unchecked(2) }; - let mut dV0 = self.dV[0]; - let mut dV1 = self.dV[1]; - let mut dV2 = self.dV[2]; - let mut dV3 = self.dV[3]; - - let mut tV0 = self.tV[0]; - let mut tV1 = self.tV[1]; - let mut tV2 = self.tV[2]; - let mut tV3 = self.tV[3]; - - let mut V0 = self.V[0]; - let mut V1 = self.V[1]; - let mut V2 = self.V[2]; - let mut V3 = self.V[3]; + let [mut dV0, mut dV1, mut dV2, mut dV3] = self.dV; + let [mut tV0, mut tV1, mut tV2, mut tV3] = self.tV; + let [mut V0, mut V1, mut V2, mut V3] = self.V; let mut out_sample = 0.; // 2x oversampling @@ -116,20 +105,9 @@ impl Effect for MoogFilter { } self.last_sample = sample; - self.tV[0] = tV0; - self.tV[1] = tV1; - self.tV[2] = tV2; - self.tV[3] = tV3; - - self.dV[0] = dV0; - self.dV[1] = dV1; - self.dV[2] = dV2; - self.dV[3] = dV3; - - self.V[0] = V0; - self.V[1] = V1; - self.V[2] = V2; - self.V[3] = V3; + self.tV = [tV0, tV1, tV2, tV3]; + self.dV = [dV0, dV1, dV2, dV3]; + self.V = [V0, V1, V2, V3]; out_sample / 2. } @@ -146,25 +124,14 @@ impl Effect for MoogFilter { let resonances = unsafe { rendered_params.get_unchecked(1) }; let drives = unsafe { rendered_params.get_unchecked(2) }; - let mut dV0 = self.dV[0]; - let mut dV1 = self.dV[1]; - let mut dV2 = self.dV[2]; - let mut dV3 = self.dV[3]; - - let mut tV0 = self.tV[0]; - let mut tV1 = self.tV[1]; - let mut tV2 = self.tV[2]; - let mut tV3 = self.tV[3]; - - let mut V0 = self.V[0]; - let mut V1 = self.V[1]; - let mut V2 = self.V[2]; - let mut V3 = self.V[3]; + let [mut dV0, mut dV1, mut dV2, mut dV3] = self.dV; + let [mut tV0, mut tV1, mut tV2, mut tV3] = self.tV; + let [mut V0, mut V1, mut V2, mut V3] = self.V; let mut last_sample = self.last_sample; - for i in 0..samples.len() { + for sample_ix in 0..samples.len() { let mut out_sample = 0.; - let cur_sample = unsafe { *samples.get_unchecked(i) }; + let cur_sample = unsafe { *samples.get_unchecked(sample_ix) }; // 2x oversampling for j in 0..=1 { @@ -174,9 +141,9 @@ impl Effect for MoogFilter { cur_sample }; - let cutoff = dsp::clamp(1., 22_100., cutoffs[i]); - let resonance = dsp::clamp(0., 20., resonances[i]); - let drive = drives[i]; + let cutoff = dsp::clamp(1., 22_100., cutoffs[sample_ix]); + let resonance = dsp::clamp(0., 20., resonances[sample_ix]); + let drive = drives[sample_ix]; let x = (PI * cutoff) / (2 * SAMPLE_RATE) as f32; let g = 4. * PI * VT * cutoff * (1. - x) / (1. + x); @@ -205,23 +172,12 @@ impl Effect for MoogFilter { } last_sample = cur_sample; - unsafe { *samples.get_unchecked_mut(i) = out_sample / 2. }; + unsafe { *samples.get_unchecked_mut(sample_ix) = out_sample / 2. }; } - self.tV[0] = tV0; - self.tV[1] = tV1; - self.tV[2] = tV2; - self.tV[3] = tV3; - - self.dV[0] = dV0; - self.dV[1] = dV1; - self.dV[2] = dV2; - self.dV[3] = dV3; - - self.V[0] = V0; - self.V[1] = V1; - self.V[2] = V2; - self.V[3] = V3; + self.tV = [tV0, tV1, tV2, tV3]; + self.dV = [dV0, dV1, dV2, dV3]; + self.V = [V0, V1, V2, V3]; self.last_sample = last_sample; } diff --git a/engine/wavetable/src/fm/mod.rs b/engine/wavetable/src/fm/mod.rs index 2835e9fc..30377b64 100644 --- a/engine/wavetable/src/fm/mod.rs +++ b/engine/wavetable/src/fm/mod.rs @@ -485,7 +485,7 @@ impl Oscillator for WaveTableHandle { // .get(param_buffers, adsrs, sample_ix_within_frame, base_frequency), // ]; - // Only 1D wavetables are supported for now, so we can make thing a bit simpler + // Only 1D wavetables are supported for now, so we can make this thing a bit simpler let mixes: [f32; 4] = [ self .dim_0_intra_mix diff --git a/engine/wavetable/src/lib.rs b/engine/wavetable/src/lib.rs index 62c20206..854bc879 100644 --- a/engine/wavetable/src/lib.rs +++ b/engine/wavetable/src/lib.rs @@ -21,6 +21,7 @@ pub struct WaveTableSettings { } impl WaveTableSettings { + #[inline(always)] pub fn get_samples_per_dimension(&self) -> usize { self.waveforms_per_dimension * self.waveform_length } @@ -74,7 +75,7 @@ impl WaveTable { (sample_ix.ceil() as usize).min(self.samples.len() - 1), ); - if waveform_offset_samples + sample_hi_ix >= self.samples.len() { + if cfg!(debug_assertions) && waveform_offset_samples + sample_hi_ix >= self.samples.len() { panic!( "sample_hi_ix: {}, waveform_offset_samples: {}, samples.len(): {}, waveform_ix: {}, \ dimension_ix: {}, sample_ix: {}", @@ -110,6 +111,10 @@ impl WaveTable { fn sample_dimension(&self, dimension_ix: usize, waveform_ix: f32, sample_ix: f32) -> f32 { let waveform_mix = waveform_ix.fract(); + if waveform_mix == 0. { + return self.sample_waveform(dimension_ix, waveform_ix as usize, sample_ix); + } + let (waveform_low_ix, waveform_hi_ix) = (waveform_ix.floor() as usize, waveform_ix.ceil() as usize); @@ -120,7 +125,6 @@ impl WaveTable { } pub fn get_sample(&self, sample_ix: f32, mixes: &[f32]) -> f32 { - // debug_assert!(sample_ix < (self.settings.waveform_length - 1) as f32); if cfg!(debug_assertions) { if sample_ix < 0.0 || sample_ix >= (self.settings.waveform_length - 1) as f32 { panic!( @@ -130,8 +134,12 @@ impl WaveTable { } } - let waveform_ix = mixes[0] * ((self.settings.waveforms_per_dimension - 1) as f32); - let base_sample = self.sample_dimension(0, waveform_ix, sample_ix); + let base_sample = if self.settings.waveforms_per_dimension == 1 { + self.sample_waveform(0, 0, sample_ix) + } else { + let waveform_ix = mixes[0] * ((self.settings.waveforms_per_dimension - 1) as f32); + self.sample_dimension(0, waveform_ix, sample_ix) + }; // For each higher dimension, mix the base sample from the lowest dimension with the output // of the next dimension until a final sample is produced @@ -139,7 +147,11 @@ impl WaveTable { for dimension_ix in 1..self.settings.dimension_count { let waveform_ix = mixes[dimension_ix * 2] * ((self.settings.waveforms_per_dimension - 1) as f32); - let sample_for_dimension = self.sample_dimension(dimension_ix, waveform_ix, sample_ix); + let sample_for_dimension = if self.settings.waveforms_per_dimension == 1 { + self.sample_waveform(dimension_ix, 0, sample_ix) + } else { + self.sample_dimension(dimension_ix, waveform_ix, sample_ix) + }; sample = mix(mixes[dimension_ix * 2 + 1], sample, sample_for_dimension); } diff --git a/src/api.ts b/src/api.ts index 9308d14a..af17e3c0 100644 --- a/src/api.ts +++ b/src/api.ts @@ -84,11 +84,12 @@ export const saveComposition = async ( title: string, description: string, serializedComposition: { [key: string]: string }, - tags: string[] + tags: string[], + parentID?: number | null ): Promise => fetch(`${BACKEND_BASE_URL}/compositions`, { method: 'POST', - body: JSON.stringify({ title, description, content: serializedComposition, tags }), + body: JSON.stringify({ title, description, content: serializedComposition, tags, parentID }), headers: { 'Content-Type': 'application/json', Authorization: await getLoginToken(), diff --git a/src/compositionSharing/CompositionSharing.tsx b/src/compositionSharing/CompositionSharing.tsx index 9ceabd11..5edd9837 100644 --- a/src/compositionSharing/CompositionSharing.tsx +++ b/src/compositionSharing/CompositionSharing.tsx @@ -1,7 +1,11 @@ import * as R from 'ramda'; -import React, { useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; -import { onBeforeUnload, reinitializeWithComposition } from '../persistance'; +import { + getCurLoadedCompositionId, + onBeforeUnload, + reinitializeWithComposition, +} from '../persistance'; import './CompositionSharing.scss'; import { fetchAllSharedCompositions, @@ -24,12 +28,21 @@ import { getSentry } from 'src/sentry'; import { NIL_UUID, getEngine } from 'src/util'; import { saveSubgraphPreset } from 'src/graphEditor/GraphEditor'; +export interface CompositionVersion { + id: number; + title: string; + description: string; + compositionVersion: number; +} + export interface CompositionDefinition { id: number; title: string; description: string; content: string; + tags: string[]; userId: number | null | undefined; + versions: CompositionVersion[]; } const mkLocalSamplesConfirmation = (localSamples: SampleDescriptor[]) => { @@ -138,7 +151,7 @@ const updateSamplesInSave = ( ); }; -const removeCompositionSharingFromVCMState = (serializedVcmState: any) => { +const removeCompositionSharingFromVCMState = (serializedVcmState: string) => { const vcmState = JSON.parse(serializedVcmState); // Select control panel, graph editor, synth designer in that order if exist as the default active view @@ -184,15 +197,19 @@ const removeCompositionSharingFromVCMState = (serializedVcmState: any) => { }); }; +interface SerializeAndSaveCompositionArgs { + title: string; + description: string; + tags: string[]; + parentID?: number | null; +} + const serializeAndSaveComposition = async ({ title, description, tags, -}: { - title: string; - description: string; - tags: string[]; -}): Promise => { + parentID, +}: SerializeAndSaveCompositionArgs): Promise => { // Check to see if any local compositions are in use by the composition. If so, check confirm with // the user whether or not they should be uploaded and handle converting them to remote samples. const samples = await checkForLocalSamples(); @@ -227,7 +244,7 @@ const serializeAndSaveComposition = async ({ let compositionID: number | null = null; let saveCompositionError: any = null; try { - compositionID = await saveComposition(title, description, compositionData, tags); + compositionID = await saveComposition(title, description, compositionData, tags, parentID); } catch (err) { console.error('Error saving composition: ', err); saveCompositionError = err; @@ -242,49 +259,67 @@ const serializeAndSaveComposition = async ({ throw saveCompositionError; }; +const handleSave = async (parentID: number | null): Promise => { + try { + const { + name: title, + description, + tags, + } = await renderGenericPresetSaverWithModal({ + description: true, + tags: !parentID, + getExistingTags: getExistingCompositionTags, + }); + + getSentry()?.captureMessage('Saving composition', { + tags: { + title, + description, + tags: tags?.join(','), + parentID, + }, + }); + const savedCompositionID = await serializeAndSaveComposition({ + title, + description: description ?? '', + tags: tags ?? [], + parentID, + }); + toastSuccess(`Successfully saved as composition ${savedCompositionID}`); + return savedCompositionID; + } catch (err) { + if (!err) { + return null; + } + + getSentry()?.captureException(err); + alert('Error saving composition: ' + err); + + return null; + } +}; + const ShareComposition: React.FC = () => { const [savedCompositionID, setSavedCompositionID] = useState(null); + const [curEditingCompositionID, setCurEditingCompositionID] = useState(null); + useEffect(() => void getCurLoadedCompositionId().then(setCurEditingCompositionID), []); + + const handleSaveInner = useCallback( + async (parentID: number | null) => { + const savedCompositionID = await handleSave(parentID); + setSavedCompositionID(savedCompositionID); + }, + [setSavedCompositionID] + ); return ( <> - + + {curEditingCompositionID !== null ? ( + + ) : null} {savedCompositionID !== null ? ( Composition {savedCompositionID} saved successfully! ) : null} @@ -335,7 +370,7 @@ const CompositionSharing: React.FC = () => ( R.prop('uuid') ); reinitializeWithComposition( - { type: 'serialized', value: composition.content }, + { type: 'serialized', value: composition.content, id: +compID }, getEngine()!, allViewContextIds ); diff --git a/src/controls/GenericPresetPicker/GenericPresetSaver.tsx b/src/controls/GenericPresetPicker/GenericPresetSaver.tsx index bbe81e43..d6d0fcb8 100644 --- a/src/controls/GenericPresetPicker/GenericPresetSaver.tsx +++ b/src/controls/GenericPresetPicker/GenericPresetSaver.tsx @@ -13,6 +13,10 @@ import './GenericPresetPicker.scss'; interface GenericPresetSaverArgs { getExistingTags?: () => Promise<{ name: string; count?: number }[]>; description?: boolean; + /** + * default: true + */ + tags?: boolean; } interface TagProps { @@ -147,7 +151,7 @@ const mkGenericPresetSaver = (args: GenericPresetSaverArgs) => { ) : null} - {args.getExistingTags ? ( + {args.tags !== false && args.getExistingTags ? ( { */ export const reinitializeWithComposition = ( compositionBody: - | { type: 'serialized'; value: string } - | { type: 'parsed'; value: { [key: string]: string } }, + | { type: 'serialized'; value: string; id?: number | null } + | { type: 'parsed'; value: { [key: string]: string }; id?: number | null }, engine: typeof import('./engine'), allViewContextIds: string[] ): Either => { @@ -55,6 +56,10 @@ export const reinitializeWithComposition = ( setGlobalBpm(+deserialized.globalTempo); } + if (!R.isNil(compositionBody.id)) { + setCurLoadedCompositionId(compositionBody.id); + } + // Trigger applicaion to refresh using the newly set `localStorage` content engine.init(); @@ -132,6 +137,18 @@ export const loadSharedComposition = async ( } }; +export const getCurLoadedCompositionId = async (): Promise => { + const [id] = await currentLoadedCompositionIdTable.toArray(); + return id ? +id : null; +}; + +export const setCurLoadedCompositionId = async (id: number | null) => { + await currentLoadedCompositionIdTable.clear(); + if (id !== null) { + await currentLoadedCompositionIdTable.add(id, ['']); + } +}; + export const maybeRestoreLocalComposition = async () => { const hasSavedLocalComposition = (await localCompositionTable.count()) > 0; if (!hasSavedLocalComposition) { diff --git a/src/redux/modules/vcmUtils.ts b/src/redux/modules/vcmUtils.ts index b34cdd29..f5781574 100644 --- a/src/redux/modules/vcmUtils.ts +++ b/src/redux/modules/vcmUtils.ts @@ -16,7 +16,7 @@ import type { PatchNetwork, } from 'src/patchNetwork'; import type { MIDINode } from 'src/patchNetwork/midiNode'; -import { reinitializeWithComposition } from 'src/persistance'; +import { reinitializeWithComposition, setCurLoadedCompositionId } from 'src/persistance'; import { getState, store } from 'src/redux'; import { filterNils, getEngine, UnreachableError } from 'src/util'; import { setGlobalVolume } from 'src/ViewContextManager/GlobalVolumeSlider'; @@ -209,6 +209,7 @@ export const create_empty_audio_connectables = (vcId: string): AudioConnectables export const initializeDefaultVCMState = () => { const engine = getEngine()!; const allViewContextIds = getState().viewContextManager.activeViewContexts.map(R.prop('uuid')); + setCurLoadedCompositionId(null); const res = reinitializeWithComposition( { type: 'parsed', value: DefaultComposition }, engine, diff --git a/src/synthDesigner/SynthModule.tsx b/src/synthDesigner/SynthModule.tsx index ca9a5e9d..85b4a12f 100644 --- a/src/synthDesigner/SynthModule.tsx +++ b/src/synthDesigner/SynthModule.tsx @@ -1,4 +1,3 @@ -import * as R from 'ramda'; import React, { useCallback, useMemo, useState } from 'react'; import ControlPanel from 'react-control-panel'; import { Provider, shallowEqual, useSelector } from 'react-redux'; @@ -12,9 +11,8 @@ import { renderGenericPresetSaverWithModal } from 'src/controls/GenericPresetPic import { ConnectedFMSynthUI } from 'src/fmSynth/FMSynthUI'; import type { Adsr, AdsrParams } from 'src/graphEditor/nodes/CustomAudio/FMSynth/FMSynth'; import { updateConnectables } from 'src/patchNetwork/interface'; -import { getState, store, type ReduxStore } from 'src/redux'; +import { store, type ReduxStore } from 'src/redux'; import { getSynthDesignerReduxInfra, type SynthModule } from 'src/redux/modules/synthDesigner'; -import { getSentry } from 'src/sentry'; import { get_synth_designer_audio_connectables, getVoicePreset } from 'src/synthDesigner'; import { UnreachableError, msToSamples, samplesToMs } from 'src/util'; import { Filter as FilterModule } from './Filter'; @@ -23,11 +21,7 @@ import { type PresetDescriptor, } from 'src/controls/GenericPresetPicker/GenericPresetPicker'; import { renderModalWithControls } from 'src/controls/Modal'; -import { - voicePresetIdsSelector, - type SynthVoicePreset, - type SynthVoicePresetEntry, -} from 'src/redux/modules/presets'; +import { type SynthVoicePreset } from 'src/redux/modules/presets'; const PRESETS_CONTROL_PANEL_STYLE = { height: 97, width: 400 }; @@ -38,18 +32,23 @@ interface PresetsControlPanelProps { const PresetsControlPanel: React.FC = ({ index, stateKey }) => { const { dispatch, actionCreators } = getSynthDesignerReduxInfra(stateKey); - const allVoicePresets = useSelector((state: ReduxStore) => { + const allVoicePresetsRaw = useSelector((state: ReduxStore) => { if (typeof state.presets.voicePresets === 'string') { return null; } - return state.presets.voicePresets.map( - (preset): PresetDescriptor => ({ - ...preset, - name: preset.title, - preset: preset.body, - }) - ); + return state.presets.voicePresets; }, shallowEqual); + const allVoicePresets = useMemo( + () => + allVoicePresetsRaw?.map( + (preset): PresetDescriptor => ({ + ...preset, + name: preset.title, + preset: preset.body, + }) + ), + [allVoicePresetsRaw] + ); const settings = useMemo(() => { if (!allVoicePresets) {