Safer fold and crush section

This commit is contained in:
2025-10-03 23:34:45 +02:00
parent 697be5cf65
commit 0fc7ffdee0
18 changed files with 189 additions and 105 deletions

View File

@ -2,7 +2,7 @@ class FoldCrushProcessor extends AudioWorkletProcessor {
constructor() { constructor() {
super() super()
this.clipMode = 'wrap' this.clipMode = 'fold'
this.drive = 1 this.drive = 1
this.bitDepth = 16 this.bitDepth = 16
this.crushAmount = 0 this.crushAmount = 0
@ -28,39 +28,63 @@ class FoldCrushProcessor extends AudioWorkletProcessor {
} }
} }
wrap(sample) { clamp(x, min, max) {
const range = 2.0 return Math.max(min, Math.min(max, x))
let wrapped = sample
while (wrapped > 1.0) wrapped -= range
while (wrapped < -1.0) wrapped += range
return wrapped
} }
clamp(sample) { mod(x, y) {
return Math.max(-1.0, Math.min(1.0, sample)) return ((x % y) + y) % y
} }
fold(sample) { squash(x) {
let folded = sample return x / (1 + Math.abs(x))
while (folded > 1.0 || folded < -1.0) {
if (folded > 1.0) {
folded = 2.0 - folded
} }
if (folded < -1.0) {
folded = -2.0 - folded soft(x, k) {
return Math.tanh(x * (1 + k))
} }
hard(x, k) {
return this.clamp((1 + k) * x, -1, 1)
} }
return folded
fold(x, k) {
let y = (1 + 0.5 * k) * x
const window = this.mod(y + 1, 4)
return 1 - Math.abs(window - 2)
}
cubic(x, k) {
const t = this.squash(Math.log1p(k))
const cubic = (x - (t / 3) * x * x * x) / (1 - t / 3)
return this.soft(cubic, k)
}
diode(x, k) {
const g = 1 + 2 * k
const t = this.squash(Math.log1p(k))
const bias = 0.07 * t
const pos = this.soft(x + bias, 2 * k)
const neg = this.soft(-x + bias, 2 * k)
const y = pos - neg
const sech = 1 / Math.cosh(g * bias)
const sech2 = sech * sech
const denom = Math.max(1e-8, 2 * g * sech2)
return this.soft(y / denom, k)
} }
processWavefolder(sample) { processWavefolder(sample) {
switch (this.clipMode) { switch (this.clipMode) {
case 'wrap': case 'soft':
return this.wrap(sample) return this.soft(sample, this.drive)
case 'clamp': case 'hard':
return this.clamp(sample) return this.hard(sample, this.drive)
case 'fold': case 'fold':
return this.fold(sample) return this.fold(sample, this.drive)
case 'cubic':
return this.cubic(sample, this.drive)
case 'diode':
return this.diode(sample, this.drive)
default: default:
return sample return sample
} }
@ -86,6 +110,14 @@ class FoldCrushProcessor extends AudioWorkletProcessor {
} }
} }
safetyLimiter(sample) {
const threshold = 0.8
if (Math.abs(sample) > threshold) {
return Math.tanh(sample * 0.9) / Math.tanh(0.9)
}
return sample
}
process(inputs, outputs) { process(inputs, outputs) {
const input = inputs[0] const input = inputs[0]
const output = outputs[0] const output = outputs[0]
@ -95,9 +127,9 @@ class FoldCrushProcessor extends AudioWorkletProcessor {
const outputChannel = output[0] const outputChannel = output[0]
for (let i = 0; i < inputChannel.length; i++) { for (let i = 0; i < inputChannel.length; i++) {
const driven = inputChannel[i] * this.drive let processed = this.processWavefolder(inputChannel[i])
let processed = this.processWavefolder(driven)
processed = this.processBitcrush(processed) processed = this.processBitcrush(processed)
processed = this.safetyLimiter(processed)
outputChannel[i] = processed outputChannel[i] = processed
} }
} }

View File

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

View File

@ -0,0 +1,27 @@
interface DropdownProps {
label: string
value: string
options: { value: string; label: string }[]
onChange: (value: string) => void
}
export function Dropdown({ label, value, options, onChange }: DropdownProps) {
return (
<div className="flex flex-col gap-1">
<label className="font-mono text-[9px] tracking-[0.15em] text-white">
{label.toUpperCase()}
</label>
<select
value={value}
onChange={(e) => onChange(e.target.value)}
className="bg-black text-white border-2 border-white font-mono text-[10px] tracking-[0.1em] px-2 py-1 cursor-pointer hover:bg-white hover:text-black transition-colors"
>
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label.toUpperCase()}
</option>
))}
</select>
</div>
)
}

View File

@ -1,22 +1,15 @@
import { Slider } from './Slider' import { Slider } from './Slider'
import { Switch } from './Switch' import { Switch } from './Switch'
import { Dropdown } from './Dropdown'
import { EFFECTS } from '../config/effects' import { EFFECTS } from '../config/effects'
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 | boolean) => void onChange: (parameterId: string, value: number | boolean | string) => void
} }
export function EffectsBar({ values, onChange }: EffectsBarProps) { export function EffectsBar({ values, onChange }: EffectsBarProps) {
const formatValue = (id: string, value: number): string => {
if (id === 'clipMode') {
return getClipModeLabel(value)
}
return value.toString()
}
const renderFilterEffect = (effect: typeof EFFECTS[number]) => { const renderFilterEffect = (effect: typeof EFFECTS[number]) => {
const filterGroups = [ const filterGroups = [
{ prefix: 'hp', label: 'HP' }, { prefix: 'hp', label: 'HP' },
@ -98,6 +91,18 @@ export function EffectsBar({ values, onChange }: EffectsBarProps) {
</div> </div>
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
{effect.parameters.map(param => { {effect.parameters.map(param => {
if (param.options) {
return (
<Dropdown
key={param.id}
label={param.label}
value={values[param.id] as string ?? param.default as string}
options={param.options}
onChange={(value) => onChange(param.id, value)}
/>
)
}
const isSwitch = param.min === 0 && param.max === 1 && param.step === 1 const isSwitch = param.min === 0 && param.max === 1 && param.step === 1
if (isSwitch) { if (isSwitch) {
@ -121,13 +126,12 @@ export function EffectsBar({ values, onChange }: EffectsBarProps) {
<Slider <Slider
key={param.id} key={param.id}
label={param.label} label={param.label}
value={values[param.id] as number ?? param.default} value={values[param.id] as number ?? param.default as number}
min={param.min} min={param.min}
max={param.max} max={param.max}
step={param.step} step={param.step}
unit={param.unit} unit={param.unit}
onChange={(value) => onChange(param.id, value)} onChange={(value) => onChange(param.id, value)}
formatValue={param.id === 'clipMode' ? formatValue : undefined}
valueId={param.id} valueId={param.id}
/> />
) )

View File

@ -160,19 +160,26 @@ export const EFFECTS: EffectConfig[] = [
id: 'clipMode', id: 'clipMode',
label: 'Mode', label: 'Mode',
min: 0, min: 0,
max: 2, max: 0,
default: 0, default: 'fold',
step: 1, step: 1,
unit: '' unit: '',
options: [
{ value: 'fold', label: 'Fold' },
{ value: 'soft', label: 'Soft' },
{ value: 'cubic', label: 'Cubic' },
{ value: 'diode', label: 'Diode' },
{ value: 'hard', label: 'Hard' }
]
}, },
{ {
id: 'wavefolderDrive', id: 'wavefolderDrive',
label: 'Drive', label: 'Drive',
min: 1, min: 0.001,
max: 10, max: 10,
default: 1, default: 1,
step: 0.1, step: 0.1,
unit: 'x' unit: ''
}, },
{ {
id: 'bitcrushDepth', id: 'bitcrushDepth',
@ -309,8 +316,8 @@ export const EFFECTS: EffectConfig[] = [
} }
] ]
export function getDefaultEffectValues(): Record<string, number | boolean> { export function getDefaultEffectValues(): Record<string, number | boolean | string> {
const defaults: Record<string, number | boolean> = {} const defaults: Record<string, number | boolean | string> = {}
EFFECTS.forEach(effect => { EFFECTS.forEach(effect => {
effect.parameters.forEach(param => { effect.parameters.forEach(param => {
defaults[param.id] = param.default defaults[param.id] = param.default
@ -326,7 +333,7 @@ export function getDefaultEngineValues(): Record<string, number> {
const defaults: Record<string, number> = {} const defaults: Record<string, number> = {}
ENGINE_CONTROLS.forEach(control => { ENGINE_CONTROLS.forEach(control => {
control.parameters.forEach(param => { control.parameters.forEach(param => {
defaults[param.id] = param.default defaults[param.id] = param.default as number
}) })
}) })
return defaults return defaults

View File

@ -45,6 +45,7 @@ export class AudioPlayer {
this.dispose() this.dispose()
this.audioContext = new AudioContext({ sampleRate: this.sampleRate }) this.audioContext = new AudioContext({ sampleRate: this.sampleRate })
this.workletRegistered = false
await this.registerWorklet(this.audioContext) await this.registerWorklet(this.audioContext)
this.effectsChain = new EffectsChain(this.audioContext) this.effectsChain = new EffectsChain(this.audioContext)
@ -176,5 +177,6 @@ export class AudioPlayer {
this.audioContext.close() this.audioContext.close()
this.audioContext = null this.audioContext = null
} }
this.workletRegistered = false
} }
} }

View File

@ -72,12 +72,12 @@ export class BitcrushEffect implements Effect {
} }
} }
updateParams(values: Record<string, number>): void { updateParams(values: Record<string, number | string>): void {
if (values.bitcrushDepth !== undefined) { if (values.bitcrushDepth !== undefined && typeof values.bitcrushDepth === 'number') {
this.bitDepth = values.bitcrushDepth this.bitDepth = values.bitcrushDepth
} }
if (values.bitcrushRate !== undefined) { if (values.bitcrushRate !== undefined && typeof values.bitcrushRate === 'number') {
this.crushAmount = values.bitcrushRate this.crushAmount = values.bitcrushRate
} }
} }

View File

@ -92,8 +92,8 @@ export class DelayEffect implements Effect {
} }
} }
updateParams(values: Record<string, number>): void { updateParams(values: Record<string, number | string>): void {
if (values.delayTime !== undefined) { if (values.delayTime !== undefined && typeof values.delayTime === 'number') {
const time = values.delayTime / 1000 const time = values.delayTime / 1000
this.delayNode.delayTime.setTargetAtTime( this.delayNode.delayTime.setTargetAtTime(
time, time,
@ -102,7 +102,7 @@ export class DelayEffect implements Effect {
) )
} }
if (values.delayFeedback !== undefined) { if (values.delayFeedback !== undefined && typeof values.delayFeedback === 'number') {
const feedback = values.delayFeedback / 100 const feedback = values.delayFeedback / 100
this.feedbackNode.gain.setTargetAtTime( this.feedbackNode.gain.setTargetAtTime(
feedback * 0.95, feedback * 0.95,
@ -111,7 +111,7 @@ export class DelayEffect implements Effect {
) )
} }
if (values.delayWetDry !== undefined) { if (values.delayWetDry !== undefined && typeof values.delayWetDry === 'number') {
const wet = values.delayWetDry / 100 const wet = values.delayWetDry / 100
this.currentWetValue = wet this.currentWetValue = wet
this.currentDryValue = 1 - wet this.currentDryValue = 1 - wet
@ -130,7 +130,7 @@ export class DelayEffect implements Effect {
} }
} }
if (values.delayTone !== undefined) { if (values.delayTone !== undefined && typeof values.delayTone === 'number') {
const tone = values.delayTone / 100 const tone = values.delayTone / 100
const freq = 200 + tone * 7800 const freq = 200 + tone * 7800
this.filterNode.frequency.setTargetAtTime( this.filterNode.frequency.setTargetAtTime(
@ -140,12 +140,12 @@ export class DelayEffect implements Effect {
) )
} }
if (values.delaySaturation !== undefined) { if (values.delaySaturation !== undefined && typeof values.delaySaturation === 'number') {
const saturation = values.delaySaturation / 100 const saturation = values.delaySaturation / 100
this.createSaturationCurve(saturation) this.createSaturationCurve(saturation)
} }
if (values.delayFlutter !== undefined) { if (values.delayFlutter !== undefined && typeof values.delayFlutter === 'number') {
const flutter = values.delayFlutter / 100 const flutter = values.delayFlutter / 100
const baseDelay = this.delayNode.delayTime.value const baseDelay = this.delayNode.delayTime.value
const modDepth = baseDelay * flutter * 0.1 const modDepth = baseDelay * flutter * 0.1

View File

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

View File

@ -44,7 +44,7 @@ export class EffectsChain {
this.masterGainNode.connect(this.outputNode) this.masterGainNode.connect(this.outputNode)
} }
updateEffects(values: Record<string, number | boolean>): void { updateEffects(values: Record<string, number | boolean | string>): void {
for (const effect of this.effects) { for (const effect of this.effects) {
const effectId = effect.id const effectId = effect.id
const bypassKey = `${effectId}Bypass` const bypassKey = `${effectId}Bypass`
@ -53,13 +53,13 @@ export class EffectsChain {
effect.setBypass(Boolean(values[bypassKey])) effect.setBypass(Boolean(values[bypassKey]))
} }
const numericValues: Record<string, number> = {} const effectValues: Record<string, number | string> = {}
for (const [key, value] of Object.entries(values)) { for (const [key, value] of Object.entries(values)) {
if (typeof value === 'number') { if (typeof value === 'number' || typeof value === 'string') {
numericValues[key] = value effectValues[key] = value
} }
} }
effect.updateParams(numericValues) effect.updateParams(effectValues)
} }
if (values.masterVolume !== undefined) { if (values.masterVolume !== undefined) {

View File

@ -72,13 +72,13 @@ export class FilterEffect implements Effect {
} }
} }
updateParams(values: Record<string, number>): void { updateParams(values: Record<string, number | string>): void {
if (values.hpEnable !== undefined) { if (values.hpEnable !== undefined) {
this.hpEnabled = values.hpEnable === 1 this.hpEnabled = values.hpEnable === 1
this.updateBypassState() this.updateBypassState()
} }
if (values.hpFreq !== undefined) { if (values.hpFreq !== undefined && typeof values.hpFreq === 'number') {
this.hpFilter.frequency.cancelScheduledValues(this.audioContext.currentTime) this.hpFilter.frequency.cancelScheduledValues(this.audioContext.currentTime)
this.hpFilter.frequency.setValueAtTime( this.hpFilter.frequency.setValueAtTime(
this.hpFilter.frequency.value, this.hpFilter.frequency.value,
@ -90,7 +90,7 @@ export class FilterEffect implements Effect {
) )
} }
if (values.hpRes !== undefined) { if (values.hpRes !== undefined && typeof values.hpRes === 'number') {
this.hpFilter.Q.cancelScheduledValues(this.audioContext.currentTime) this.hpFilter.Q.cancelScheduledValues(this.audioContext.currentTime)
this.hpFilter.Q.setValueAtTime( this.hpFilter.Q.setValueAtTime(
this.hpFilter.Q.value, this.hpFilter.Q.value,
@ -107,7 +107,7 @@ export class FilterEffect implements Effect {
this.updateBypassState() this.updateBypassState()
} }
if (values.lpFreq !== undefined) { if (values.lpFreq !== undefined && typeof values.lpFreq === 'number') {
this.lpFilter.frequency.cancelScheduledValues(this.audioContext.currentTime) this.lpFilter.frequency.cancelScheduledValues(this.audioContext.currentTime)
this.lpFilter.frequency.setValueAtTime( this.lpFilter.frequency.setValueAtTime(
this.lpFilter.frequency.value, this.lpFilter.frequency.value,
@ -119,7 +119,7 @@ export class FilterEffect implements Effect {
) )
} }
if (values.lpRes !== undefined) { if (values.lpRes !== undefined && typeof values.lpRes === 'number') {
this.lpFilter.Q.cancelScheduledValues(this.audioContext.currentTime) this.lpFilter.Q.cancelScheduledValues(this.audioContext.currentTime)
this.lpFilter.Q.setValueAtTime( this.lpFilter.Q.setValueAtTime(
this.lpFilter.Q.value, this.lpFilter.Q.value,
@ -136,7 +136,7 @@ export class FilterEffect implements Effect {
this.updateBypassState() this.updateBypassState()
} }
if (values.bpFreq !== undefined) { if (values.bpFreq !== undefined && typeof values.bpFreq === 'number') {
this.bpFilter.frequency.cancelScheduledValues(this.audioContext.currentTime) this.bpFilter.frequency.cancelScheduledValues(this.audioContext.currentTime)
this.bpFilter.frequency.setValueAtTime( this.bpFilter.frequency.setValueAtTime(
this.bpFilter.frequency.value, this.bpFilter.frequency.value,
@ -148,7 +148,7 @@ export class FilterEffect implements Effect {
) )
} }
if (values.bpRes !== undefined) { if (values.bpRes !== undefined && typeof values.bpRes === 'number') {
this.bpFilter.Q.cancelScheduledValues(this.audioContext.currentTime) this.bpFilter.Q.cancelScheduledValues(this.audioContext.currentTime)
this.bpFilter.Q.setValueAtTime( this.bpFilter.Q.setValueAtTime(
this.bpFilter.Q.value, this.bpFilter.Q.value,

View File

@ -15,8 +15,8 @@ export class FoldCrushEffect implements Effect {
this.wetNode = audioContext.createGain() this.wetNode = audioContext.createGain()
this.dryNode = audioContext.createGain() this.dryNode = audioContext.createGain()
this.wetNode.gain.value = 1 this.wetNode.gain.value = 0
this.dryNode.gain.value = 0 this.dryNode.gain.value = 1
this.inputNode.connect(this.dryNode) this.inputNode.connect(this.dryNode)
this.dryNode.connect(this.outputNode) this.dryNode.connect(this.outputNode)
@ -51,13 +51,11 @@ export class FoldCrushEffect implements Effect {
} }
} }
updateParams(values: Record<string, number>): void { updateParams(values: Record<string, number | string>): void {
if (!this.processorNode) return if (!this.processorNode) return
if (values.clipMode !== undefined) { if (values.clipMode !== undefined) {
const modeIndex = values.clipMode this.processorNode.port.postMessage({ type: 'clipMode', value: values.clipMode })
const clipMode = ['wrap', 'clamp', 'fold'][modeIndex] || 'wrap'
this.processorNode.port.postMessage({ type: 'clipMode', value: clipMode })
} }
if (values.wavefolderDrive !== undefined) { if (values.wavefolderDrive !== undefined) {
this.processorNode.port.postMessage({ type: 'drive', value: values.wavefolderDrive }) this.processorNode.port.postMessage({ type: 'drive', value: values.wavefolderDrive })

View File

@ -21,7 +21,7 @@ export class PassThroughEffect implements Effect {
setBypass(_bypass: boolean): void { setBypass(_bypass: boolean): void {
} }
updateParams(_values: Record<string, number>): void { updateParams(_values: Record<string, number | string>): void {
} }
dispose(): void { dispose(): void {

View File

@ -153,20 +153,20 @@ export class ReverbEffect implements Effect {
} }
} }
updateParams(values: Record<string, number>): void { updateParams(values: Record<string, number | string>): void {
let needsRegeneration = false let needsRegeneration = false
if (values.reverbDecay !== undefined && values.reverbDecay !== this.currentDecay) { if (values.reverbDecay !== undefined && typeof values.reverbDecay === 'number' && values.reverbDecay !== this.currentDecay) {
this.currentDecay = values.reverbDecay this.currentDecay = values.reverbDecay
needsRegeneration = true needsRegeneration = true
} }
if (values.reverbDamping !== undefined && values.reverbDamping !== this.currentDamping) { if (values.reverbDamping !== undefined && typeof values.reverbDamping === 'number' && values.reverbDamping !== this.currentDamping) {
this.currentDamping = values.reverbDamping this.currentDamping = values.reverbDamping
needsRegeneration = true needsRegeneration = true
} }
if (values.reverbWetDry !== undefined) { if (values.reverbWetDry !== undefined && typeof values.reverbWetDry === 'number') {
const wet = values.reverbWetDry / 100 const wet = values.reverbWetDry / 100
this.currentWetValue = wet this.currentWetValue = wet
this.currentDryValue = 1 - wet this.currentDryValue = 1 - wet
@ -177,7 +177,7 @@ export class ReverbEffect implements Effect {
} }
} }
if (values.reverbPanRate !== undefined) { if (values.reverbPanRate !== undefined && typeof values.reverbPanRate === 'number') {
const rate = values.reverbPanRate const rate = values.reverbPanRate
this.panLfoNode.frequency.setTargetAtTime( this.panLfoNode.frequency.setTargetAtTime(
rate, rate,
@ -186,7 +186,7 @@ export class ReverbEffect implements Effect {
) )
} }
if (values.reverbPanWidth !== undefined) { if (values.reverbPanWidth !== undefined && typeof values.reverbPanWidth === 'number') {
const width = values.reverbPanWidth / 100 const width = values.reverbPanWidth / 100
this.panLfoGainNode.gain.setTargetAtTime( this.panLfoGainNode.gain.setTargetAtTime(
width, width,

View File

@ -96,12 +96,11 @@ export class WavefolderEffect implements Effect {
} }
} }
updateParams(values: Record<string, number>): void { updateParams(values: Record<string, number | string>): void {
if (values.clipMode !== undefined) { if (values.clipMode !== undefined && typeof values.clipMode === 'string') {
const modeIndex = values.clipMode this.mode = values.clipMode as ClipMode
this.mode = ['wrap', 'clamp', 'fold'][modeIndex] as ClipMode || 'wrap'
} }
if (values.wavefolderDrive !== undefined) { if (values.wavefolderDrive !== undefined && typeof values.wavefolderDrive === 'number') {
this.drive = values.wavefolderDrive this.drive = values.wavefolderDrive
} }
} }

View File

@ -1,15 +1,34 @@
import { persistentMap } from '@nanostores/persistent' import { map } from 'nanostores'
import { getDefaultEngineValues, getDefaultEffectValues } from '../config/effects' import { getDefaultEngineValues, getDefaultEffectValues } from '../config/effects'
export const engineSettings = persistentMap('engine:', getDefaultEngineValues(), { const STORAGE_KEY_ENGINE = 'engine:'
encode: JSON.stringify, const STORAGE_KEY_EFFECTS = 'effects:'
decode: JSON.parse
})
export const effectSettings = persistentMap('effects:', { function loadFromStorage<T>(key: string, defaults: T): T {
try {
const stored = localStorage.getItem(key)
return stored ? { ...defaults, ...JSON.parse(stored) } : defaults
} catch {
return defaults
}
}
export const engineSettings = map(loadFromStorage(STORAGE_KEY_ENGINE, getDefaultEngineValues()))
export const effectSettings = map(loadFromStorage(STORAGE_KEY_EFFECTS, {
...getDefaultEffectValues(), ...getDefaultEffectValues(),
masterVolume: 75 masterVolume: 75
}, { }))
encode: JSON.stringify,
decode: JSON.parse function saveToStorage() {
}) try {
localStorage.setItem(STORAGE_KEY_ENGINE, JSON.stringify(engineSettings.get()))
localStorage.setItem(STORAGE_KEY_EFFECTS, JSON.stringify(effectSettings.get()))
} catch (e) {
console.error('Failed to save settings:', e)
}
}
if (typeof window !== 'undefined') {
window.addEventListener('beforeunload', saveToStorage)
}

View File

@ -3,9 +3,10 @@ export interface EffectParameter {
label: string label: string
min: number min: number
max: number max: number
default: number default: number | string
step: number step: number
unit?: string unit?: string
options?: { value: string; label: string }[]
} }
export interface EffectConfig { export interface EffectConfig {
@ -15,4 +16,4 @@ export interface EffectConfig {
bypassable?: boolean bypassable?: boolean
} }
export type EffectValues = Record<string, number | boolean> export type EffectValues = Record<string, number | boolean | string>

View File

@ -10,11 +10,6 @@ export function getBitDepthLabel(index: number): string {
return labels[index] || '8bit' return labels[index] || '8bit'
} }
export function getClipModeLabel(index: number): string {
const labels = ['Wrap', 'Clamp', 'Fold']
return labels[index] || 'Wrap'
}
export function getSampleRateLabel(index: number): string { export function getSampleRateLabel(index: number): string {
return `${SAMPLE_RATES[index]}Hz` return `${SAMPLE_RATES[index]}Hz`
} }