Skip to content

Commit

Permalink
Add support for virtual VCs
Browse files Browse the repository at this point in the history
 * This is just a VC with some pre-set initial state that shows up in the list of available VCs
 * Create virtual VC entries for soul reverb and flanger
  • Loading branch information
Ameobea committed Feb 1, 2025
1 parent 70d7ff3 commit 93094f3
Show file tree
Hide file tree
Showing 6 changed files with 68 additions and 13 deletions.
10 changes: 8 additions & 2 deletions engine/engine/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>) {
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)
}

Expand Down
21 changes: 14 additions & 7 deletions engine/engine/src/view_context/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,13 @@ impl ViewContextManager {
name: String,
mut view_context: Box<dyn ViewContext>,
subgraph_id: Uuid,
initial_state: Option<String>,
) -> 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();

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) =
Expand All @@ -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 {
Expand Down
17 changes: 16 additions & 1 deletion src/ViewContextManager/AddModulePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [
Expand Down Expand Up @@ -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 {
Expand Down
20 changes: 20 additions & 0 deletions src/ViewContextManager/virtualVCDefinitions.ts
Original file line number Diff line number Diff line change
@@ -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<bufferSize> 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<bufferSize> 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}',
},
];
2 changes: 1 addition & 1 deletion src/faustEditor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
11 changes: 9 additions & 2 deletions src/graphEditor/GraphEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -531,15 +531,22 @@ const createNode = (nodeType: string, subgraphId: string, params?: Record<string
if (isVc) {
const engine = getEngine();
if (!engine) {
console.error('Engine not initialized when creating a VC from the graph editor');
return;
}

const displayName = ViewContextDescriptors.find(d => 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));
Expand Down

0 comments on commit 93094f3

Please sign in to comment.