diff --git a/engine/engine/src/lib.rs b/engine/engine/src/lib.rs index 6b803230..60f4ec2a 100644 --- a/engine/engine/src/lib.rs +++ b/engine/engine/src/lib.rs @@ -57,12 +57,18 @@ pub fn init() { /// Creates a new view context from the provided name in the active subgraph and sets it as the main /// view context for that subgraph. #[wasm_bindgen] -pub fn create_view_context(vc_name: String, display_name: String) { +pub fn create_view_context(vc_name: String, display_name: String, initial_state: Option) { let uuid = uuid_v4(); debug!("Creating VC with name {} with vcId {}", vc_name, uuid); let view_context = build_view(&vc_name, uuid); let vcm = get_vcm(); - vcm.add_view_context(uuid, vc_name, view_context, vcm.active_subgraph_id); + vcm.add_view_context( + uuid, + vc_name, + view_context, + vcm.active_subgraph_id, + initial_state, + ); set_vc_title(uuid.to_string(), display_name) } diff --git a/engine/engine/src/view_context/manager.rs b/engine/engine/src/view_context/manager.rs index 44311253..f95b42a2 100644 --- a/engine/engine/src/view_context/manager.rs +++ b/engine/engine/src/view_context/manager.rs @@ -201,7 +201,13 @@ impl ViewContextManager { name: String, mut view_context: Box, subgraph_id: Uuid, + initial_state: Option, ) -> usize { + if let Some(initial_state) = initial_state { + let new_localstorage_key = view_context.get_state_key(); + js::set_localstorage_key(&new_localstorage_key, &initial_state); + } + view_context.init(); view_context.hide(); @@ -428,6 +434,7 @@ impl ViewContextManager { "graph_editor".to_string(), mk_graph_editor(new_graph_editor_vc_id), new_subgraph_id, + None, ); // Add subgraph portals to and from the subgraph so the user can navigate between them @@ -919,9 +926,6 @@ impl ViewContextManager { }; } if let Some(serde_json::Value::Array(nodes)) = state.get_mut("nodes") { - // for node in nodes { - - // } nodes.retain_mut(|node| { if let Some(serde_json::Value::String(vc_id)) = node.get_mut("id") { if let Some(mapped_vc_id) = @@ -945,12 +949,15 @@ impl ViewContextManager { *val = serde_json::to_string(&state).unwrap(); } - - let new_localstorage_key = ctx.get_state_key(); - js::set_localstorage_key(&new_localstorage_key, val); } - self.add_view_context(vc.def.uuid, vc.def.name.clone(), ctx, vc.def.subgraph_id); + self.add_view_context( + vc.def.uuid, + vc.def.name.clone(), + ctx, + vc.def.subgraph_id, + vc.localstorage_val.clone(), + ); } for (id, mut desc) in serialized.subgraphs { diff --git a/src/ViewContextManager/AddModulePicker.tsx b/src/ViewContextManager/AddModulePicker.tsx index 9aae8fb2..67ca9f99 100644 --- a/src/ViewContextManager/AddModulePicker.tsx +++ b/src/ViewContextManager/AddModulePicker.tsx @@ -5,11 +5,24 @@ import ControlPanel from 'react-control-panel'; import './GlobalVolume.css'; import { getEngine } from 'src/util'; import { createPortal } from 'react-dom'; +import { VirtualVCDefinitions } from 'src/ViewContextManager/virtualVCDefinitions'; -interface ViewContextDescriptor { +export interface ViewContextDescriptor { name: string; + /** + * If set, this will be the VC type used under the hood. Used for virtual VCs so they can have unique + * names but share the underlying implementation. + */ + nameAlias?: string; displayName: string; description?: string; + /** + * This will be set into localStorage for the VC's state key if provided to override the default + * initial state. + * + * It can be used to implement virtual VCs like reverb that use the code editor under the hood. + */ + initialState?: string; } export const ViewContextDescriptors: ViewContextDescriptor[] = [ @@ -105,6 +118,8 @@ export const ViewContextDescriptors: ViewContextDescriptor[] = [ description: 'Chop up, manipulate, and play back pieces of samples in response to incoming MIDI events. Useful for creating vocal chops and stuff like that.', }, + // -- virtual VCs -- + ...VirtualVCDefinitions, ]; interface AddModulePickerProps { diff --git a/src/ViewContextManager/virtualVCDefinitions.ts b/src/ViewContextManager/virtualVCDefinitions.ts new file mode 100644 index 00000000..c1fab6c2 --- /dev/null +++ b/src/ViewContextManager/virtualVCDefinitions.ts @@ -0,0 +1,20 @@ +import type { ViewContextDescriptor } from 'src/ViewContextManager/AddModulePicker'; + +export const VirtualVCDefinitions: ViewContextDescriptor[] = [ + { + name: 'reverb', + nameAlias: 'faust_editor', + displayName: 'Reverb', + description: 'A basic reverb effect based on Freeverb', + initialState: + '{"editorContent":"/*\\n == SOUL example code ==\\n == Author: Jules via the class Freeverb algorithm ==\\n*/\\n\\n/// Title: An implementation of the classic Freeverb reverb algorithm.\\n\\ngraph Reverb [[ main ]]\\n{\\n input stream float audioIn;\\n output stream float<2> audioOut;\\n\\n input\\n {\\n ReverbParameterProcessorParam.roomSize [[ name: \\"Room Size\\", min: 0, max: 100, init: 80, text: \\"tiny|small|medium|large|hall\\" ]];\\n ReverbParameterProcessorParam.damping [[ name: \\"Damping Factor\\", min: 0, max: 100, init: 50, unit: \\"%\\", step: 1 ]];\\n ReverbParameterProcessorParam.wetLevel [[ name: \\"Wet Level\\", min: 0, max: 100, init: 33, unit: \\"%\\", step: 1 ]];\\n ReverbParameterProcessorParam.dryLevel [[ name: \\"Dry Level\\", min: 0, max: 100, init: 40, unit: \\"%\\", step: 1 ]];\\n ReverbParameterProcessorParam.width [[ name: \\"Width\\", min: 0, max: 100, init: 100, unit: \\"%\\", step: 1 ]];\\n }\\n\\n let\\n {\\n dryGainParameterRamp = ParameterRamp (20.0f);\\n wetGain1ParameterRamp = ParameterRamp (20.0f);\\n wetGain2ParameterRamp = ParameterRamp (20.0f);\\n dampingParameterRamp = ParameterRamp (20.0f);\\n feedbackParameterRamp = ParameterRamp (20.0f);\\n\\n reverbChannelLeft = ReverbChannel (0);\\n reverbChannelRight = ReverbChannel (23);\\n }\\n\\n connection\\n {\\n // Parameter outputs to smoothing processors\\n ReverbParameterProcessorParam.dryGainOut -> dryGainParameterRamp.updateParameter;\\n ReverbParameterProcessorParam.wetGain1Out -> wetGain1ParameterRamp.updateParameter;\\n ReverbParameterProcessorParam.wetGain2Out -> wetGain2ParameterRamp.updateParameter;\\n ReverbParameterProcessorParam.dampingOut -> dampingParameterRamp.updateParameter;\\n ReverbParameterProcessorParam.feedbackOut -> feedbackParameterRamp.updateParameter;\\n\\n // Sum the audio\\n audioIn -> Mixer.audioInDry;\\n\\n // Left channel\\n audioIn -> reverbChannelLeft.audioIn;\\n dampingParameterRamp.parameterOut -> reverbChannelLeft.damping;\\n feedbackParameterRamp.parameterOut -> reverbChannelLeft.feedback;\\n reverbChannelLeft -> Mixer.audioInLeftWet;\\n\\n // Right channel\\n audioIn -> reverbChannelRight.audioIn;\\n dampingParameterRamp.parameterOut -> reverbChannelRight.damping;\\n feedbackParameterRamp.parameterOut -> reverbChannelRight.feedback;\\n reverbChannelRight.audioOut -> Mixer.audioInRightWet;\\n\\n // Mix parameters to the mixer\\n dryGainParameterRamp.parameterOut -> Mixer.dryIn;\\n wetGain1ParameterRamp.parameterOut -> Mixer.wet1In;\\n wetGain2ParameterRamp.parameterOut -> Mixer.wet2In;\\n\\n // Write the mixed values to the output\\n Mixer -> audioOut;\\n }\\n}\\n\\n//==============================================================================\\nprocessor AllpassFilter (int bufferSize)\\n{\\n output stream float audioOut;\\n input stream float audioIn;\\n\\n float[bufferSize] buffer;\\n\\n void run()\\n {\\n wrap bufferIndex = 0;\\n\\n loop\\n {\\n let in = audioIn;\\n let bufferedValue = buffer[bufferIndex];\\n\\n buffer[bufferIndex] = in + (bufferedValue * 0.5f);\\n\\n bufferIndex++;\\n audioOut << bufferedValue - in;\\n\\n advance();\\n }\\n }\\n}\\n\\n//==============================================================================\\nprocessor CombFilter (int bufferSize)\\n{\\n output stream float audioOut;\\n input stream float audioIn, dampingIn, feedbackLevelIn;\\n\\n float[bufferSize] buffer;\\n\\n void run()\\n {\\n wrap bufferIndex = 0;\\n\\n let gain = 0.015f;\\n float last = 0.0f;\\n\\n loop\\n {\\n let out = buffer[bufferIndex];\\n audioOut << out;\\n\\n last = (out * (1.0f - dampingIn)) + (last * dampingIn);\\n\\n buffer[bufferIndex] = (gain * audioIn) + (last * feedbackLevelIn);\\n ++bufferIndex;\\n\\n advance();\\n }\\n }\\n}\\n\\n//==============================================================================\\nprocessor Mixer\\n{\\n input stream float audioInDry;\\n input stream float dryIn, wet1In, wet2In,\\n audioInLeftWet, audioInRightWet;\\n\\n output stream float<2> audioOut;\\n\\n void run()\\n {\\n loop\\n {\\n let left = (audioInLeftWet * wet1In) + (audioInRightWet * wet2In);\\n let right = (audioInRightWet * wet1In) + (audioInLeftWet * wet2In);\\n\\n audioOut << float<2> (left, right) + audioInDry * dryIn;\\n advance();\\n }\\n }\\n}\\n\\n\\n//==============================================================================\\n// Converts an input value into a stream (limited to the given slewRate)\\nprocessor ParameterRamp (float slewRate)\\n{\\n input event float updateParameter;\\n output stream float parameterOut;\\n\\n event updateParameter (float newTarget)\\n {\\n targetValue = newTarget;\\n\\n let diff = targetValue - currentValue;\\n let rampSeconds = abs (diff) / slewRate;\\n\\n rampSamples = int (processor.frequency * rampSeconds);\\n rampIncrement = diff / float (rampSamples);\\n }\\n\\n float targetValue;\\n float currentValue;\\n float rampIncrement;\\n int rampSamples;\\n\\n void run()\\n {\\n loop\\n {\\n if (rampSamples > 0)\\n {\\n currentValue += rampIncrement;\\n --rampSamples;\\n }\\n\\n parameterOut << currentValue;\\n advance();\\n }\\n }\\n}\\n\\n//==============================================================================\\n// Correctly applies parameter changes to the streams of input to the algorithm\\nprocessor ReverbParameterProcessorParam\\n{\\n input event float roomSize,\\n damping,\\n wetLevel,\\n dryLevel,\\n width;\\n\\n output event float dryGainOut,\\n wetGain1Out,\\n wetGain2Out,\\n dampingOut,\\n feedbackOut;\\n\\n event roomSize (float newValue) { roomSizeScaled = newValue / 100.0f; onUpdate(); }\\n event damping (float newValue) { dampingScaled = newValue / 100.0f; onUpdate(); }\\n event wetLevel (float newValue) { wetLevelScaled = newValue / 100.0f; onUpdate(); }\\n event dryLevel (float newValue) { dryLevelScaled = newValue / 100.0f; onUpdate(); }\\n event width (float newValue) { widthScaled = newValue / 100.0f; onUpdate(); }\\n\\n float roomSizeScaled = 0.5f;\\n float dampingScaled = 0.5f;\\n float wetLevelScaled = 0.33f;\\n float dryLevelScaled = 0.4f;\\n float widthScaled = 1.0f;\\n\\n void onUpdate()\\n {\\n // Various tuning factors for the reverb\\n let wetScaleFactor = 3.0f;\\n let dryScaleFactor = 2.0f;\\n\\n let roomScaleFactor = 0.28f;\\n let roomOffset = 0.7f;\\n let dampScaleFactor = 0.4f;\\n\\n // Write updated values\\n dryGainOut << dryLevelScaled * dryScaleFactor;\\n wetGain1Out << 0.5f * wetLevelScaled * wetScaleFactor * (1.0f + widthScaled);\\n wetGain2Out << 0.5f * wetLevelScaled * wetScaleFactor * (1.0f - widthScaled);\\n dampingOut << dampingScaled * dampScaleFactor;\\n feedbackOut << roomSizeScaled * roomScaleFactor + roomOffset;\\n }\\n}\\n\\n//==============================================================================\\n// Mono freeverb implementation\\ngraph ReverbChannel (int offset)\\n{\\n input stream float audioIn, damping, feedback;\\n output stream float audioOut;\\n\\n let\\n {\\n allpass_1 = AllpassFilter(225 + offset);\\n allpass_2 = AllpassFilter(341 + offset);\\n allpass_3 = AllpassFilter(441 + offset);\\n allpass_4 = AllpassFilter(556 + offset);\\n\\n comb_1 = CombFilter(1116 + offset);\\n comb_2 = CombFilter(1188 + offset);\\n comb_3 = CombFilter(1277 + offset);\\n comb_4 = CombFilter(1356 + offset);\\n comb_5 = CombFilter(1422 + offset);\\n comb_6 = CombFilter(1491 + offset);\\n comb_7 = CombFilter(1557 + offset);\\n comb_8 = CombFilter(1617 + offset);\\n }\\n\\n connection\\n {\\n damping -> comb_1.dampingIn,\\n comb_2.dampingIn,\\n comb_3.dampingIn,\\n comb_4.dampingIn,\\n comb_5.dampingIn,\\n comb_6.dampingIn,\\n comb_7.dampingIn,\\n comb_8.dampingIn;\\n\\n feedback -> comb_1.feedbackLevelIn,\\n comb_2.feedbackLevelIn,\\n comb_3.feedbackLevelIn,\\n comb_4.feedbackLevelIn,\\n comb_5.feedbackLevelIn,\\n comb_6.feedbackLevelIn,\\n comb_7.feedbackLevelIn,\\n comb_8.feedbackLevelIn;\\n\\n audioIn -> comb_1.audioIn,\\n comb_2.audioIn,\\n comb_3.audioIn,\\n comb_4.audioIn,\\n comb_5.audioIn,\\n comb_6.audioIn,\\n comb_7.audioIn,\\n comb_8.audioIn;\\n\\n comb_1,\\n comb_2,\\n comb_3,\\n comb_4,\\n comb_5,\\n comb_6,\\n comb_7,\\n comb_8 -> allpass_1 -> allpass_2 -> allpass_3 -> allpass_4 -> audioOut;\\n }\\n}\\n","cachedInputNames":["roomSize","damping","wetLevel","dryLevel","width"],"polyphonyState":{"polyphonyEnabled":false,"frequencyInputName":null,"gateInputName":null,"voiceCount":8},"paramDefaultValues":{"roomSize":80,"damping":86,"wetLevel":33,"dryLevel":57,"width":100},"isRunning":true,"language":"soul","optimize":true}', + }, + { + name: 'flanger', + nameAlias: 'faust_editor', + displayName: 'Flanger', + description: 'Basic flanger implemented in Faust', + initialState: + '{"editorContent":"// Author: Julius Smith\\r\\n// License: MIT\\r\\n\\r\\nma = library(\\"maths.lib\\");\\r\\nba = library(\\"basics.lib\\");\\r\\nde = library(\\"delays.lib\\");\\r\\nsi = library(\\"signals.lib\\");\\r\\nan = library(\\"analyzers.lib\\");\\r\\nfi = library(\\"filters.lib\\");\\r\\nos = library(\\"oscillators.lib\\");\\r\\nno = library(\\"noises.lib\\");\\r\\nef = library(\\"misceffects.lib\\");\\r\\nco = library(\\"compressors.lib\\");\\r\\nve = library(\\"vaeffects.lib\\");\\r\\npf = library(\\"phaflangers.lib\\");\\r\\nre = library(\\"reverbs.lib\\");\\r\\nen = library(\\"envelopes.lib\\");\\r\\n\\r\\nflanger_demo = ba.bypass2(fbp,flanger_stereo_demo)\\r\\nwith{\\r\\n\\tfbp = checkbox(\\"[0] Bypass\\t[tooltip: When this is checked, the flanger\\r\\n\\t\\thas no effect]\\");\\r\\n\\tinvert = checkbox(\\"[1] Invert Flange Sum\\");\\r\\n\\r\\n\\t// FIXME: This should be an amplitude-response display:\\r\\n\\tflangeview = lfor(freq) + lfol(freq) :hbargraph(\\"[2] Flange LFO\\r\\n\\t\\t[style: led] [tooltip: Display sum of flange delays]\\", -1.5,+1.5);\\r\\n\\r\\n\\tflanger_stereo_demo(x,y) = attach(x,flangeview),y :\\r\\n\\t\\t*(level),*(level) : pf.flanger_stereo(dmax,curdel1,curdel2,depth,fb,invert);\\r\\n\\r\\n\\tlfol = os.oscrs;\\r\\n\\tlfor = os.oscrc;\\r\\n\\r\\n\\tdmax = 2048;\\r\\n\\tdflange = 0.001 * ma.SR *\\r\\n\\t\\thslider(\\"Flange Delay\\", 10, 0, 20, 0.001);\\r\\n\\todflange = 0.001 * ma.SR *\\r\\n\\thslider(\\"[2] Delay Offset\\", 1, 0, 20, 0.001);\\r\\n\\tfreq = hslider(\\"Speed\\", 0.5, 0, 10, 0.01);\\r\\n\\tdepth = hslider(\\"[2] Depth [style:knob]\\", 1, 0, 1, 0.001);\\r\\n\\tfb = hslider(\\"[3] Feedback [style:knob]\\", 0, -0.999, 0.999, 0.001);\\r\\n\\tlevel = hslider(\\"Flanger Output Level [unit:dB]\\", 0, -60, 10, 0.1) :\\r\\n\\t\\tba.db2linear;\\r\\n\\tcurdel1 = odflange+dflange*(1 + lfol(freq))/2;\\r\\n\\tcurdel2 = odflange+dflange*(1 + lfor(freq))/2;\\r\\n};\\r\\n\\r\\nprocess = flanger_demo;","cachedInputNames":["Flange_Delay","Flanger_Output_Level","Speed","Bypass","Invert_Flange_Sum","Delay_Offset","Depth","Feedback"],"polyphonyState":{"polyphonyEnabled":false,"frequencyInputName":null,"gateInputName":null,"voiceCount":8},"paramDefaultValues":{"/faust-code412340597/Flange_Delay":10,"/faust-code412340597/Flanger_Output_Level":0,"/faust-code412340597/Speed":0.5,"/faust-code412340597/Bypass":0,"/faust-code412340597/Invert_Flange_Sum":0,"/faust-code412340597/Delay_Offset":1,"/faust-code412340597/Depth":1,"/faust-code412340597/Feedback":0.1720000058412552},"isRunning":true,"language":"faust","optimize":true}', + }, +]; diff --git a/src/faustEditor/index.tsx b/src/faustEditor/index.tsx index 7b1a074d..8e718e09 100644 --- a/src/faustEditor/index.tsx +++ b/src/faustEditor/index.tsx @@ -64,7 +64,7 @@ export const init_faust_editor = (stateKey: string) => { parsed.polyphonyState = buildDefaultFaustEditorPolyphonyState(); } return Option.of(parsed); - } catch (err) { + } catch (_err) { console.error('Error parsing localstorage content for Faust editor; resetting to scratch.'); return Option.none(); } diff --git a/src/graphEditor/GraphEditor.tsx b/src/graphEditor/GraphEditor.tsx index 4cba5842..780565ab 100644 --- a/src/graphEditor/GraphEditor.tsx +++ b/src/graphEditor/GraphEditor.tsx @@ -531,15 +531,22 @@ const createNode = (nodeType: string, subgraphId: string, params?: Record d.name === nodeType)!.displayName; - engine.create_view_context(nodeType, displayName); + const desc = + ViewContextDescriptors.find(d => d.nameAlias === nodeType) ?? + ViewContextDescriptors.find(d => d.name === nodeType); + if (!desc) { + throw new UnreachableError(`Could not find VC descriptor for ${nodeType}`); + } + engine.create_view_context(desc.nameAlias ?? desc.name, desc.displayName, desc.initialState); return; } const id = buildNewForeignConnectableID().toString(); + // TODO: will also need to handle virtual FCs here const node = new audioNodeGetters[nodeType]!.nodeGetter(ctx, id, params); const connectables = node.buildConnectables(); dispatch(actionCreators.viewContextManager.ADD_PATCH_NETWORK_NODE(id, connectables, subgraphId));