reorganization

This commit is contained in:
2025-09-30 16:56:14 +02:00
parent 16dd4c08bf
commit 84cd1f3603
15 changed files with 425 additions and 92 deletions

View File

@ -1,61 +1,61 @@
{ {
"hash": "43216e8d", "hash": "480f37ab",
"configHash": "0b9ed782", "configHash": "e652d8ff",
"lockfileHash": "dadb379e", "lockfileHash": "dadb379e",
"browserHash": "4f0bb7c4", "browserHash": "55e2ae32",
"optimized": { "optimized": {
"@nanostores/persistent": { "@nanostores/persistent": {
"src": "../../.pnpm/@nanostores+persistent@1.1.0_nanostores@1.0.1/node_modules/@nanostores/persistent/index.js", "src": "../../.pnpm/@nanostores+persistent@1.1.0_nanostores@1.0.1/node_modules/@nanostores/persistent/index.js",
"file": "@nanostores_persistent.js", "file": "@nanostores_persistent.js",
"fileHash": "7596f610", "fileHash": "41270c63",
"needsInterop": false "needsInterop": false
}, },
"@nanostores/react": { "@nanostores/react": {
"src": "../../.pnpm/@nanostores+react@1.0.0_nanostores@1.0.1_react@19.1.1/node_modules/@nanostores/react/index.js", "src": "../../.pnpm/@nanostores+react@1.0.0_nanostores@1.0.1_react@19.1.1/node_modules/@nanostores/react/index.js",
"file": "@nanostores_react.js", "file": "@nanostores_react.js",
"fileHash": "19e1624e", "fileHash": "125eb5ad",
"needsInterop": false "needsInterop": false
}, },
"jszip": { "jszip": {
"src": "../../.pnpm/jszip@3.10.1/node_modules/jszip/dist/jszip.min.js", "src": "../../.pnpm/jszip@3.10.1/node_modules/jszip/dist/jszip.min.js",
"file": "jszip.js", "file": "jszip.js",
"fileHash": "e59e7578", "fileHash": "ea14d80a",
"needsInterop": true "needsInterop": true
}, },
"lucide-react": { "lucide-react": {
"src": "../../.pnpm/lucide-react@0.544.0_react@19.1.1/node_modules/lucide-react/dist/esm/lucide-react.js", "src": "../../.pnpm/lucide-react@0.544.0_react@19.1.1/node_modules/lucide-react/dist/esm/lucide-react.js",
"file": "lucide-react.js", "file": "lucide-react.js",
"fileHash": "f256b1cf", "fileHash": "347cd426",
"needsInterop": false "needsInterop": false
}, },
"react-dom": { "react-dom": {
"src": "../../.pnpm/react-dom@19.1.1_react@19.1.1/node_modules/react-dom/index.js", "src": "../../.pnpm/react-dom@19.1.1_react@19.1.1/node_modules/react-dom/index.js",
"file": "react-dom.js", "file": "react-dom.js",
"fileHash": "74c66a5f", "fileHash": "67fe1167",
"needsInterop": true "needsInterop": true
}, },
"react-dom/client": { "react-dom/client": {
"src": "../../.pnpm/react-dom@19.1.1_react@19.1.1/node_modules/react-dom/client.js", "src": "../../.pnpm/react-dom@19.1.1_react@19.1.1/node_modules/react-dom/client.js",
"file": "react-dom_client.js", "file": "react-dom_client.js",
"fileHash": "41baaeaa", "fileHash": "9c5fd5d6",
"needsInterop": true "needsInterop": true
}, },
"react": { "react": {
"src": "../../.pnpm/react@19.1.1/node_modules/react/index.js", "src": "../../.pnpm/react@19.1.1/node_modules/react/index.js",
"file": "react.js", "file": "react.js",
"fileHash": "37acbd53", "fileHash": "35a8de87",
"needsInterop": true "needsInterop": true
}, },
"react/jsx-dev-runtime": { "react/jsx-dev-runtime": {
"src": "../../.pnpm/react@19.1.1/node_modules/react/jsx-dev-runtime.js", "src": "../../.pnpm/react@19.1.1/node_modules/react/jsx-dev-runtime.js",
"file": "react_jsx-dev-runtime.js", "file": "react_jsx-dev-runtime.js",
"fileHash": "bf9c5ec3", "fileHash": "675ff029",
"needsInterop": true "needsInterop": true
}, },
"react/jsx-runtime": { "react/jsx-runtime": {
"src": "../../.pnpm/react@19.1.1/node_modules/react/jsx-runtime.js", "src": "../../.pnpm/react@19.1.1/node_modules/react/jsx-runtime.js",
"file": "react_jsx-runtime.js", "file": "react_jsx-runtime.js",
"fileHash": "082ac5fc", "fileHash": "4bc2e93d",
"needsInterop": true "needsInterop": true
} }
}, },

View File

@ -114,8 +114,8 @@ function App() {
} }
} }
const handleEffectChange = (parameterId: string, value: number) => { const handleEffectChange = (parameterId: string, value: number | boolean) => {
effectSettings.setKey(parameterId as keyof typeof effectValues, value) effectSettings.setKey(parameterId as any, value as any)
if (playbackManagerRef.current) { if (playbackManagerRef.current) {
playbackManagerRef.current.setEffects(effectValues) playbackManagerRef.current.setEffects(effectValues)
} }

View File

@ -1,11 +1,12 @@
import { Slider } from './Slider' import { Slider } from './Slider'
import { Switch } from './Switch'
import { EFFECTS } from '../config/effects' import { EFFECTS } from '../config/effects'
import { getClipModeLabel } from '../utils/formatters' import { getClipModeLabel } from '../utils/formatters'
import type { EffectValues } from '../types/effects' import type { EffectValues } from '../types/effects'
interface EffectsBarProps { interface EffectsBarProps {
values: EffectValues values: EffectValues
onChange: (parameterId: string, value: number) => void onChange: (parameterId: string, value: number | boolean) => void
} }
export function EffectsBar({ values, onChange }: EffectsBarProps) { export function EffectsBar({ values, onChange }: EffectsBarProps) {
@ -18,13 +19,27 @@ export function EffectsBar({ values, onChange }: EffectsBarProps) {
return ( return (
<div className="bg-black border-t-2 border-white px-6 py-4"> <div className="bg-black border-t-2 border-white px-6 py-4">
<div className="grid grid-cols-4 gap-6"> <div className="grid grid-cols-4 gap-4">
{EFFECTS.flatMap(effect => {EFFECTS.map(effect => (
effect.parameters.map(param => ( <div key={effect.id} className="border-2 border-white p-3">
<div className="flex items-center justify-between mb-3">
<h3 className="font-mono text-[10px] tracking-[0.2em] text-white">
{effect.name.toUpperCase()}
</h3>
{effect.bypassable && (
<Switch
checked={!Boolean(values[`${effect.id}Bypass`])}
onChange={(checked) => onChange(`${effect.id}Bypass`, !checked)}
label={Boolean(values[`${effect.id}Bypass`]) ? 'OFF' : 'ON'}
/>
)}
</div>
<div className="flex flex-col gap-3">
{effect.parameters.map(param => (
<Slider <Slider
key={param.id} key={param.id}
label={param.label} label={param.label}
value={values[param.id] ?? param.default} value={values[param.id] as number ?? param.default}
min={param.min} min={param.min}
max={param.max} max={param.max}
step={param.step} step={param.step}
@ -33,8 +48,10 @@ export function EffectsBar({ values, onChange }: EffectsBarProps) {
formatValue={param.id === 'clipMode' ? formatValue : undefined} formatValue={param.id === 'clipMode' ? formatValue : undefined}
valueId={param.id} valueId={param.id}
/> />
)) ))}
)} </div>
</div>
))}
</div> </div>
</div> </div>
) )

View File

@ -31,7 +31,7 @@ export function EngineControls({ values, onChange }: EngineControlsProps) {
{param.label.toUpperCase()} {param.label.toUpperCase()}
</label> </label>
<span className="font-mono text-[9px] text-white"> <span className="font-mono text-[9px] text-white">
{formatValue(param.id, values[param.id] ?? param.default)} {formatValue(param.id, (values[param.id] as number) ?? param.default)}
</span> </span>
</div> </div>
<input <input
@ -39,7 +39,7 @@ export function EngineControls({ values, onChange }: EngineControlsProps) {
min={param.min} min={param.min}
max={param.max} max={param.max}
step={param.step} step={param.step}
value={values[param.id] ?? param.default} value={(values[param.id] as number) ?? param.default}
onChange={(e) => onChange(param.id, Number(e.target.value))} onChange={(e) => onChange(param.id, Number(e.target.value))}
className="w-full h-[2px] bg-white appearance-none cursor-pointer" className="w-full h-[2px] bg-white appearance-none cursor-pointer"
/> />

29
src/components/Switch.tsx Normal file
View File

@ -0,0 +1,29 @@
interface SwitchProps {
checked: boolean
onChange: (checked: boolean) => void
label?: string
}
export function Switch({ checked, onChange, label }: SwitchProps) {
return (
<label className="flex items-center gap-2 cursor-pointer">
<div
className={`relative w-8 h-4 border-2 transition-colors ${
checked ? 'bg-white border-white' : 'bg-black border-white'
}`}
onClick={() => onChange(!checked)}
>
<div
className={`absolute top-0 w-3 h-3 transition-transform ${
checked ? 'translate-x-4 bg-black' : 'translate-x-0 bg-white'
}`}
/>
</div>
{label && (
<span className="font-mono text-[9px] text-white select-none">
{label}
</span>
)}
</label>
)
}

View File

@ -56,47 +56,34 @@ export const ENGINE_CONTROLS: EffectConfig[] = [
export const EFFECTS: EffectConfig[] = [ export const EFFECTS: EffectConfig[] = [
{ {
id: 'reverb', id: 'wavefolder',
name: 'Reverb', name: 'Wavefolder',
bypassable: true,
parameters: [ parameters: [
{ {
id: 'reverbWetDry', id: 'clipMode',
label: 'Reverb', label: 'Mode',
min: 0, min: 0,
max: 100, max: 2,
default: 0, default: 0,
step: 1, step: 1,
unit: '%' unit: ''
}
]
}, },
{ {
id: 'delay', id: 'wavefolderDrive',
name: 'Ping Pong Delay', label: 'Drive',
parameters: [ min: 1,
{ max: 10,
id: 'delayTime', default: 1,
label: 'Time', step: 0.1,
min: 0, unit: 'x'
max: 1000,
default: 250,
step: 10,
unit: 'ms'
},
{
id: 'delayFeedback',
label: 'Feedback',
min: 0,
max: 100,
default: 50,
step: 1,
unit: '%'
} }
] ]
}, },
{ {
id: 'bitcrush', id: 'bitcrush',
name: 'Bitcrush', name: 'Bitcrush',
bypassable: true,
parameters: [ parameters: [
{ {
id: 'bitcrushDepth', id: 'bitcrushDepth',
@ -119,28 +106,57 @@ export const EFFECTS: EffectConfig[] = [
] ]
}, },
{ {
id: 'clipmode', id: 'delay',
name: 'Clip Mode', name: 'Delay',
bypassable: true,
parameters: [ parameters: [
{ {
id: 'clipMode', id: 'delayTime',
label: 'Mode', label: 'Time',
min: 0, min: 0,
max: 2, max: 1000,
default: 250,
step: 10,
unit: 'ms'
},
{
id: 'delayFeedback',
label: 'Feedback',
min: 0,
max: 100,
default: 50,
step: 1,
unit: '%'
}
]
},
{
id: 'reverb',
name: 'Reverb',
bypassable: true,
parameters: [
{
id: 'reverbWetDry',
label: 'Amount',
min: 0,
max: 100,
default: 0, default: 0,
step: 1, step: 1,
unit: '' unit: '%'
} }
] ]
} }
] ]
export function getDefaultEffectValues(): Record<string, number> { export function getDefaultEffectValues(): Record<string, number | boolean> {
const defaults: Record<string, number> = {} const defaults: Record<string, number | boolean> = {}
EFFECTS.forEach(effect => { EFFECTS.forEach(effect => {
effect.parameters.forEach(param => { effect.parameters.forEach(param => {
defaults[param.id] = param.default defaults[param.id] = param.default
}) })
if (effect.bypassable) {
defaults[`${effect.id}Bypass`] = true
}
}) })
return defaults return defaults
} }
@ -155,7 +171,7 @@ export function getDefaultEngineValues(): Record<string, number> {
return defaults return defaults
} }
export const SAMPLE_RATES = [4000, 8000, 16000, 22050] export const SAMPLE_RATES = [8000, 11025, 22050, 44100]
export function getSampleRateFromIndex(index: number): number { export function getSampleRateFromIndex(index: number): number {
return SAMPLE_RATES[index] || 8000 return SAMPLE_RATES[index] || 8000

View File

@ -0,0 +1,92 @@
import type { Effect } from './Effect.interface'
export class BitcrushEffect implements Effect {
readonly id = 'bitcrush'
private inputNode: GainNode
private outputNode: GainNode
private processorNode: ScriptProcessorNode
private wetNode: GainNode
private dryNode: GainNode
private bitDepth: number = 16
private crushAmount: number = 0
constructor(audioContext: AudioContext) {
this.inputNode = audioContext.createGain()
this.outputNode = audioContext.createGain()
this.processorNode = audioContext.createScriptProcessor(4096, 1, 1)
this.wetNode = audioContext.createGain()
this.dryNode = audioContext.createGain()
this.wetNode.gain.value = 1
this.dryNode.gain.value = 0
this.processorNode.onaudioprocess = (e) => {
const input = e.inputBuffer.getChannelData(0)
const output = e.outputBuffer.getChannelData(0)
if (this.crushAmount === 0 && this.bitDepth === 16) {
output.set(input)
return
}
const step = Math.pow(0.5, this.bitDepth)
const phaseIncrement = 1 - (this.crushAmount / 100)
let phase = 0
for (let i = 0; i < input.length; i++) {
phase += phaseIncrement
if (phase >= 1.0) {
phase -= 1.0
const crushed = Math.floor(input[i] / step + 0.5) * step
output[i] = Math.max(-1, Math.min(1, crushed))
} else {
output[i] = i > 0 ? output[i - 1] : 0
}
}
}
this.inputNode.connect(this.dryNode)
this.inputNode.connect(this.processorNode)
this.processorNode.connect(this.wetNode)
this.dryNode.connect(this.outputNode)
this.wetNode.connect(this.outputNode)
}
getInputNode(): AudioNode {
return this.inputNode
}
getOutputNode(): AudioNode {
return this.outputNode
}
setBypass(bypass: boolean): void {
if (bypass) {
this.wetNode.gain.value = 0
this.dryNode.gain.value = 1
} else {
this.wetNode.gain.value = 1
this.dryNode.gain.value = 0
}
}
updateParams(values: Record<string, number>): void {
if (values.bitcrushDepth !== undefined) {
this.bitDepth = values.bitcrushDepth
}
if (values.bitcrushRate !== undefined) {
this.crushAmount = values.bitcrushRate
}
}
dispose(): void {
this.processorNode.disconnect()
this.wetNode.disconnect()
this.dryNode.disconnect()
this.inputNode.disconnect()
this.outputNode.disconnect()
}
}

View File

@ -9,6 +9,9 @@ export class DelayEffect implements Effect {
private feedbackNode: GainNode private feedbackNode: GainNode
private wetNode: GainNode private wetNode: GainNode
private dryNode: GainNode private dryNode: GainNode
private bypassed: boolean = false
private currentWetValue: number = 0
private currentDryValue: number = 1
constructor(audioContext: AudioContext) { constructor(audioContext: AudioContext) {
this.inputNode = audioContext.createGain() this.inputNode = audioContext.createGain()
@ -39,12 +42,28 @@ export class DelayEffect implements Effect {
return this.outputNode return this.outputNode
} }
setBypass(bypass: boolean): void {
this.bypassed = bypass
if (bypass) {
this.wetNode.gain.value = 0
this.dryNode.gain.value = 1
} else {
this.wetNode.gain.value = this.currentWetValue
this.dryNode.gain.value = this.currentDryValue
}
}
updateParams(values: Record<string, number>): void { updateParams(values: Record<string, number>): void {
if (values.delayTime !== undefined) { if (values.delayTime !== undefined) {
this.delayNode.delayTime.value = values.delayTime / 1000 this.delayNode.delayTime.value = values.delayTime / 1000
const delayAmount = Math.min(values.delayTime / 1000, 0.5) const delayAmount = Math.min(values.delayTime / 1000, 0.5)
this.wetNode.gain.value = delayAmount this.currentWetValue = delayAmount
this.dryNode.gain.value = 1 - delayAmount this.currentDryValue = 1 - delayAmount
if (!this.bypassed) {
this.wetNode.gain.value = this.currentWetValue
this.dryNode.gain.value = this.currentDryValue
}
} }
if (values.delayFeedback !== undefined) { if (values.delayFeedback !== undefined) {

View File

@ -3,6 +3,7 @@ export interface Effect {
getInputNode(): AudioNode getInputNode(): AudioNode
getOutputNode(): AudioNode getOutputNode(): AudioNode
updateParams(values: Record<string, number>): void updateParams(values: Record<string, number>): void
setBypass(bypass: boolean): void
dispose(): void dispose(): void
} }

View File

@ -1,7 +1,8 @@
import type { Effect } from './Effect.interface' import type { Effect } from './Effect.interface'
import { DelayEffect } from './DelayEffect' import { DelayEffect } from './DelayEffect'
import { ReverbEffect } from './ReverbEffect' import { ReverbEffect } from './ReverbEffect'
import { PassThroughEffect } from './PassThroughEffect' import { BitcrushEffect } from './BitcrushEffect'
import { WavefolderEffect } from './WavefolderEffect'
export class EffectsChain { export class EffectsChain {
private inputNode: GainNode private inputNode: GainNode
@ -15,10 +16,10 @@ export class EffectsChain {
this.masterGainNode = audioContext.createGain() this.masterGainNode = audioContext.createGain()
this.effects = [ this.effects = [
new WavefolderEffect(audioContext),
new BitcrushEffect(audioContext),
new DelayEffect(audioContext), new DelayEffect(audioContext),
new ReverbEffect(audioContext), new ReverbEffect(audioContext)
new PassThroughEffect(audioContext, 'bitcrush'),
new PassThroughEffect(audioContext, 'clipmode')
] ]
this.setupChain() this.setupChain()
@ -36,13 +37,26 @@ export class EffectsChain {
this.masterGainNode.connect(this.outputNode) this.masterGainNode.connect(this.outputNode)
} }
updateEffects(values: Record<string, number>): void { updateEffects(values: Record<string, number | boolean>): void {
for (const effect of this.effects) { for (const effect of this.effects) {
effect.updateParams(values) const effectId = effect.id
const bypassKey = `${effectId}Bypass`
if (values[bypassKey] !== undefined) {
effect.setBypass(Boolean(values[bypassKey]))
}
const numericValues: Record<string, number> = {}
for (const [key, value] of Object.entries(values)) {
if (typeof value === 'number') {
numericValues[key] = value
}
}
effect.updateParams(numericValues)
} }
if (values.masterVolume !== undefined) { if (values.masterVolume !== undefined) {
this.masterGainNode.gain.value = values.masterVolume / 100 this.masterGainNode.gain.value = Number(values.masterVolume) / 100
} }
} }

View File

@ -18,6 +18,9 @@ export class PassThroughEffect implements Effect {
return this.node return this.node
} }
setBypass(_bypass: boolean): void {
}
updateParams(_values: Record<string, number>): void { updateParams(_values: Record<string, number>): void {
} }

View File

@ -9,6 +9,9 @@ export class ReverbEffect implements Effect {
private convolverNode: ConvolverNode private convolverNode: ConvolverNode
private wetNode: GainNode private wetNode: GainNode
private dryNode: GainNode private dryNode: GainNode
private bypassed: boolean = false
private currentWetValue: number = 0
private currentDryValue: number = 1
constructor(audioContext: AudioContext) { constructor(audioContext: AudioContext) {
this.audioContext = audioContext this.audioContext = audioContext
@ -52,11 +55,27 @@ export class ReverbEffect implements Effect {
return this.outputNode return this.outputNode
} }
setBypass(bypass: boolean): void {
this.bypassed = bypass
if (bypass) {
this.wetNode.gain.value = 0
this.dryNode.gain.value = 1
} else {
this.wetNode.gain.value = this.currentWetValue
this.dryNode.gain.value = this.currentDryValue
}
}
updateParams(values: Record<string, number>): void { updateParams(values: Record<string, number>): void {
if (values.reverbWetDry !== undefined) { if (values.reverbWetDry !== undefined) {
const wet = values.reverbWetDry / 100 const wet = values.reverbWetDry / 100
this.wetNode.gain.value = wet this.currentWetValue = wet
this.dryNode.gain.value = 1 - wet this.currentDryValue = 1 - wet
if (!this.bypassed) {
this.wetNode.gain.value = this.currentWetValue
this.dryNode.gain.value = this.currentDryValue
}
} }
} }

View File

@ -0,0 +1,116 @@
import type { Effect } from './Effect.interface'
type ClipMode = 'wrap' | 'clamp' | 'fold'
export class WavefolderEffect implements Effect {
readonly id = 'wavefolder'
private inputNode: GainNode
private outputNode: GainNode
private processorNode: ScriptProcessorNode
private wetNode: GainNode
private dryNode: GainNode
private mode: ClipMode = 'wrap'
private drive: number = 1
constructor(audioContext: AudioContext) {
this.inputNode = audioContext.createGain()
this.outputNode = audioContext.createGain()
this.processorNode = audioContext.createScriptProcessor(4096, 1, 1)
this.wetNode = audioContext.createGain()
this.dryNode = audioContext.createGain()
this.wetNode.gain.value = 1
this.dryNode.gain.value = 0
this.processorNode.onaudioprocess = (e) => {
const input = e.inputBuffer.getChannelData(0)
const output = e.outputBuffer.getChannelData(0)
for (let i = 0; i < input.length; i++) {
const driven = input[i] * this.drive
output[i] = this.processSample(driven)
}
}
this.inputNode.connect(this.dryNode)
this.inputNode.connect(this.processorNode)
this.processorNode.connect(this.wetNode)
this.dryNode.connect(this.outputNode)
this.wetNode.connect(this.outputNode)
}
private processSample(sample: number): number {
switch (this.mode) {
case 'wrap':
return this.wrap(sample)
case 'clamp':
return this.clamp(sample)
case 'fold':
return this.fold(sample)
default:
return sample
}
}
private wrap(sample: number): number {
const range = 2.0
let wrapped = sample
while (wrapped > 1.0) wrapped -= range
while (wrapped < -1.0) wrapped += range
return wrapped
}
private clamp(sample: number): number {
return Math.max(-1.0, Math.min(1.0, sample))
}
private fold(sample: number): number {
let folded = sample
while (folded > 1.0 || folded < -1.0) {
if (folded > 1.0) {
folded = 2.0 - folded
}
if (folded < -1.0) {
folded = -2.0 - folded
}
}
return folded
}
getInputNode(): AudioNode {
return this.inputNode
}
getOutputNode(): AudioNode {
return this.outputNode
}
setBypass(bypass: boolean): void {
if (bypass) {
this.wetNode.gain.value = 0
this.dryNode.gain.value = 1
} else {
this.wetNode.gain.value = 1
this.dryNode.gain.value = 0
}
}
updateParams(values: Record<string, number>): void {
if (values.clipMode !== undefined) {
const modeIndex = values.clipMode
this.mode = ['wrap', 'clamp', 'fold'][modeIndex] as ClipMode || 'wrap'
}
if (values.wavefolderDrive !== undefined) {
this.drive = values.wavefolderDrive
}
}
dispose(): void {
this.processorNode.disconnect()
this.wetNode.disconnect()
this.dryNode.disconnect()
this.inputNode.disconnect()
this.outputNode.disconnect()
}
}

View File

@ -78,18 +78,24 @@ export class EffectsChain {
} }
updateEffects(values: EffectValues): void { updateEffects(values: EffectValues): void {
if (typeof values.reverbWetDry === 'number') {
const reverbWet = values.reverbWetDry / 100 const reverbWet = values.reverbWetDry / 100
this.reverbWetNode.gain.value = reverbWet this.reverbWetNode.gain.value = reverbWet
this.reverbDryNode.gain.value = 1 - reverbWet this.reverbDryNode.gain.value = 1 - reverbWet
}
if (typeof values.delayTime === 'number') {
this.delayNode.delayTime.value = values.delayTime / 1000 this.delayNode.delayTime.value = values.delayTime / 1000
this.delayFeedbackNode.gain.value = values.delayFeedback / 100
const delayAmount = Math.min(values.delayTime / 1000, 0.5) const delayAmount = Math.min(values.delayTime / 1000, 0.5)
this.delayWetNode.gain.value = delayAmount this.delayWetNode.gain.value = delayAmount
this.delayDryNode.gain.value = 1 - delayAmount this.delayDryNode.gain.value = 1 - delayAmount
}
if (values.masterVolume !== undefined) { if (typeof values.delayFeedback === 'number') {
this.delayFeedbackNode.gain.value = values.delayFeedback / 100
}
if (typeof values.masterVolume === 'number') {
this.masterGainNode.gain.value = values.masterVolume / 100 this.masterGainNode.gain.value = values.masterVolume / 100
} }
} }

View File

@ -12,6 +12,7 @@ export interface EffectConfig {
id: string id: string
name: string name: string
parameters: EffectParameter[] parameters: EffectParameter[]
bypassable?: boolean
} }
export type EffectValues = Record<string, number> export type EffectValues = Record<string, number | boolean>