This guide explains how to add new audio effect nodes to the Patchrome Chrome extension. Follow these steps to implement a new effect from start to finish.
Adding a new effect requires changes to multiple files across the extension:
- Type definitions
- Audio processing implementation
- UI components
- Data conversion logic
Add your effect to the type unions:
export type NodeType = 'input' | 'reverb' | 'delay' | 'youreffect' | 'output';
export type EffectNodeType = 'reverb' | 'delay' | 'youreffect';Create the node interface and parameter definitions:
// youreffect
export interface YourEffectNode extends BaseNode {
type: 'youreffect';
data: {
param1: number;
param2: number;
// Add your parameters here
};
deletable: true;
}
export const YourEffectParamDOM = [
{
label: 'Parameter 1',
key: 'param1',
min: 0,
max: 100,
step: 1,
valueType: 'percentage' // or 'number' or 'milliseconds'
},
{
label: 'Parameter 2',
key: 'param2',
min: 0,
max: 1000,
step: 10,
valueType: 'milliseconds'
},
// Add more parameters as needed
];Update the AudioNode union type:
export type AudioNode = InputNode | ReverbNode | DelayNode | YourEffectNode | OutputNode;Add your effect to the NodeData type:
export interface NodeData {
id: string;
type: 'input' | 'reverb' | 'delay' | 'youreffect' | 'output';
params: Record<string, number>;
}Edit src/inject.js to add your effect's audio processing logic.
} else if (node.type === 'youreffect') {
// Create effect chain
const inputGain = audioContext.createGain();
const dryGain = audioContext.createGain();
const wetGain = audioContext.createGain();
// Create your Web Audio API nodes here
// Example: const effectNode = audioContext.createBiquadFilter();
const merger = audioContext.createGain();
// Set up parameters
const param1 = node.params.param1 !== undefined ? node.params.param1 : 50;
const param2 = node.params.param2 !== undefined ? node.params.param2 : 500;
// Apply parameters to your effect nodes
// Example: effectNode.frequency.value = param1;
// Set up wet/dry mix
const wetAmount = (node.params.mix || 0) / 100;
const dryAmount = 1 - wetAmount;
dryGain.gain.value = dryAmount;
wetGain.gain.value = wetAmount;
// Connect internal routing
inputGain.connect(dryGain);
inputGain.connect(effectNode); // Replace with your effect node
nodes.set(node.id, {
type: 'youreffect',
input: inputGain,
output: merger,
inputGain,
dryGain,
wetGain,
// Add your effect nodes here
merger,
params: node.params,
audioContext
});
// Store internal connections
// Example: connections.push({ from: effectNode, to: wetGain });
connections.push({ from: wetGain, to: merger });
connections.push({ from: dryGain, to: merger });Add cases for connecting your effect to other nodes:
} else if (sourceNode.type === 'youreffect') {
if (targetNode.type === 'reverb') {
sourceNode.output.connect(targetNode.inputGain);
} else if (targetNode.type === 'delay') {
sourceNode.output.connect(targetNode.inputGain);
} else if (targetNode.type === 'youreffect') {
sourceNode.output.connect(targetNode.inputGain);
} else if (targetNode.type === 'output') {
sourceNode.output.connect(targetNode.audioNode);
}
}Also add cases for connecting TO your effect in the existing conditions:
if (sourceNode.type === 'input') {
// ... existing cases ...
} else if (targetNode.type === 'youreffect') {
sourceNode.audioNode.connect(targetNode.inputGain);
}} else if (node.type === 'youreffect' && node.wetGain && node.dryGain) {
const wetAmount = (graphNode.params.mix || 0) / 100;
const dryAmount = 1 - wetAmount;
// Use setValueAtTime for immediate parameter changes
const currentTime = audioContexts.get(element)?.currentTime || 0;
node.wetGain.gain.setValueAtTime(wetAmount, currentTime);
node.dryGain.gain.setValueAtTime(dryAmount, currentTime);
// Update your effect-specific parameters
const param1 = graphNode.params.param1 !== undefined ? graphNode.params.param1 : 50;
// Example: node.effectNode.frequency.setValueAtTime(param1, currentTime);
// Always update the stored parameters
node.params = { ...graphNode.params };
console.log(`Patchrome: Updated youreffect ${nodeId} - params:`, node.params);
}Add disconnection for your effect nodes:
if (node.yourEffectNode) {
try { node.yourEffectNode.disconnect(); } catch(e) {}
}
// Add disconnection for any other nodes specific to your effectAdd a button for your effect (around line 76):
<button
className="effect-dropdown-item"
onClick={() => handleAddEffect('youreffect')}
>
<div className="effect-item-content">
<div className="effect-item-icon youreffect">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
{/* Add your custom SVG icon here */}
</svg>
</div>
<span>Your Effect</span>
</div>
</button>Import your parameter definitions:
import { AudioNode, InputParamDOM, ReverbParamDOM, DelayParamDOM, YourEffectParamDOM, OutputParamDOM, ValueType } from '../../../types/nodeGraphStructure';Add your effect icon (around line 28):
youreffect: (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
{/* Add your custom SVG icon here */}
</svg>
),Add your effect header (around line 46):
const nodeHeaders = {
input: 'Audio Input',
reverb: 'Reverb',
delay: 'Delay',
youreffect: 'Your Effect',
output: 'Audio Output'
};Update getParamDOM function (around line 56):
case 'youreffect':
return YourEffectParamDOM;.effect-item-icon.youreffect {
background: rgba(YOUR_COLOR_RGB, 0.1);
color: #YOUR_COLOR_HEX;
}Edit src/sidepanel/components/FlowDiagram.tsx:
} else if (effectType === 'youreffect') {
const viewport = getViewport();
const centerX = (-viewport.x + window.innerWidth / 2) / viewport.zoom;
const centerY = (-viewport.y + window.innerHeight / 2) / viewport.zoom;
const newNodeId = nodeIdCounter.toString();
const newNode: Node = {
id: newNodeId,
type: 'unifiedAudio',
data: {
type: 'youreffect' as const,
param1: 50, // Default value
param2: 500, // Default value
mix: 0, // If your effect has wet/dry mix
deletable: true,
onChange: (key: string, value: number) => {
handleNodeValueChange(newNodeId, key, value);
},
onRemove: () => handleRemoveNode(newNodeId)
},
position: { x: centerX - 110, y: centerY - 75 },
};
setNodes((nds) => {
const updated = [...nds, newNode];
saveToLocalStorage(updated, edges);
return updated;
});
setNodeIdCounter((prev) => prev + 1);
}} else if (node.type === 'youreffect') {
return {
...baseNode,
data: {
type: 'youreffect' as const,
param1: node.params.param1 !== undefined ? node.params.param1 : 50,
param2: node.params.param2 !== undefined ? node.params.param2 : 500,
mix: node.params.mix || 0, // If your effect has wet/dry mix
deletable: true,
onChange: (key: string, value: number) => {
handleNodeValueChange(node.id, key, value);
},
onRemove: () => handleRemoveNode(node.id)
}
};
}} else if (node.data.type === 'youreffect') {
baseNode.params = {
param1: node.data.param1 !== undefined ? node.data.param1 : 50,
param2: node.data.param2 !== undefined ? node.data.param2 : 500,
mix: node.data.mix || 0 // If your effect has wet/dry mix
};
}- Build the extension:
npm run build - Reload the extension in Chrome
- Open the side panel and click the "Add Effect" button
- Select your new effect
- Connect it between input and output nodes
- Adjust parameters and verify audio processing works correctly
Here are some Web Audio API nodes you might use:
- Filter:
audioContext.createBiquadFilter() - Distortion:
audioContext.createWaveShaper() - Compressor:
audioContext.createDynamicsCompressor() - Panner:
audioContext.createStereoPanner() - Gain:
audioContext.createGain() - Oscillator:
audioContext.createOscillator() - Analyser:
audioContext.createAnalyser()
- Always implement wet/dry mixing for effects
- Use
setValueAtTime()for smooth parameter changes - Handle edge cases (undefined parameters, disconnection)
- Add meaningful console.log statements for debugging
- Test with various parameter combinations
- Consider the order of connections in your effect chain
- Remember to disconnect all nodes properly to avoid memory leaks
For a practical example, here's how you might implement a lowpass filter:
} else if (node.type === 'filter') {
const inputGain = audioContext.createGain();
const dryGain = audioContext.createGain();
const wetGain = audioContext.createGain();
const filter = audioContext.createBiquadFilter();
const merger = audioContext.createGain();
// Configure filter
filter.type = 'lowpass';
filter.frequency.value = node.params.frequency || 1000;
filter.Q.value = (node.params.resonance || 0) / 10; // Convert 0-100 to 0-10
// Set up wet/dry mix
const wetAmount = (node.params.mix || 0) / 100;
const dryAmount = 1 - wetAmount;
dryGain.gain.value = dryAmount;
wetGain.gain.value = wetAmount;
// Connect routing
inputGain.connect(dryGain);
inputGain.connect(filter);
nodes.set(node.id, {
type: 'filter',
input: inputGain,
output: merger,
inputGain,
dryGain,
wetGain,
filter,
merger,
params: node.params,
audioContext
});
connections.push({ from: filter, to: wetGain });
connections.push({ from: wetGain, to: merger });
connections.push({ from: dryGain, to: merger });
}This documentation should help you add any new effect to the Patchrome extension!