Safer fold and crush section
This commit is contained in:
@ -2,7 +2,7 @@ class FoldCrushProcessor extends AudioWorkletProcessor {
|
||||
constructor() {
|
||||
super()
|
||||
|
||||
this.clipMode = 'wrap'
|
||||
this.clipMode = 'fold'
|
||||
this.drive = 1
|
||||
this.bitDepth = 16
|
||||
this.crushAmount = 0
|
||||
@ -28,39 +28,63 @@ class FoldCrushProcessor extends AudioWorkletProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
wrap(sample) {
|
||||
const range = 2.0
|
||||
let wrapped = sample
|
||||
while (wrapped > 1.0) wrapped -= range
|
||||
while (wrapped < -1.0) wrapped += range
|
||||
return wrapped
|
||||
clamp(x, min, max) {
|
||||
return Math.max(min, Math.min(max, x))
|
||||
}
|
||||
|
||||
clamp(sample) {
|
||||
return Math.max(-1.0, Math.min(1.0, sample))
|
||||
mod(x, y) {
|
||||
return ((x % y) + y) % y
|
||||
}
|
||||
|
||||
fold(sample) {
|
||||
let folded = sample
|
||||
while (folded > 1.0 || folded < -1.0) {
|
||||
if (folded > 1.0) {
|
||||
folded = 2.0 - folded
|
||||
squash(x) {
|
||||
return x / (1 + Math.abs(x))
|
||||
}
|
||||
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) {
|
||||
switch (this.clipMode) {
|
||||
case 'wrap':
|
||||
return this.wrap(sample)
|
||||
case 'clamp':
|
||||
return this.clamp(sample)
|
||||
case 'soft':
|
||||
return this.soft(sample, this.drive)
|
||||
case 'hard':
|
||||
return this.hard(sample, this.drive)
|
||||
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:
|
||||
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) {
|
||||
const input = inputs[0]
|
||||
const output = outputs[0]
|
||||
@ -95,9 +127,9 @@ class FoldCrushProcessor extends AudioWorkletProcessor {
|
||||
const outputChannel = output[0]
|
||||
|
||||
for (let i = 0; i < inputChannel.length; i++) {
|
||||
const driven = inputChannel[i] * this.drive
|
||||
let processed = this.processWavefolder(driven)
|
||||
let processed = this.processWavefolder(inputChannel[i])
|
||||
processed = this.processBitcrush(processed)
|
||||
processed = this.safetyLimiter(processed)
|
||||
outputChannel[i] = processed
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
if (playbackManagerRef.current) {
|
||||
playbackManagerRef.current.setEffects(effectValues)
|
||||
|
||||
27
src/components/Dropdown.tsx
Normal file
27
src/components/Dropdown.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -1,22 +1,15 @@
|
||||
import { Slider } from './Slider'
|
||||
import { Switch } from './Switch'
|
||||
import { Dropdown } from './Dropdown'
|
||||
import { EFFECTS } from '../config/effects'
|
||||
import { getClipModeLabel } from '../utils/formatters'
|
||||
import type { EffectValues } from '../types/effects'
|
||||
|
||||
interface EffectsBarProps {
|
||||
values: EffectValues
|
||||
onChange: (parameterId: string, value: number | boolean) => void
|
||||
onChange: (parameterId: string, value: number | boolean | string) => void
|
||||
}
|
||||
|
||||
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 filterGroups = [
|
||||
{ prefix: 'hp', label: 'HP' },
|
||||
@ -98,6 +91,18 @@ export function EffectsBar({ values, onChange }: EffectsBarProps) {
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
{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
|
||||
|
||||
if (isSwitch) {
|
||||
@ -121,13 +126,12 @@ export function EffectsBar({ values, onChange }: EffectsBarProps) {
|
||||
<Slider
|
||||
key={param.id}
|
||||
label={param.label}
|
||||
value={values[param.id] as number ?? param.default}
|
||||
value={values[param.id] as number ?? param.default as number}
|
||||
min={param.min}
|
||||
max={param.max}
|
||||
step={param.step}
|
||||
unit={param.unit}
|
||||
onChange={(value) => onChange(param.id, value)}
|
||||
formatValue={param.id === 'clipMode' ? formatValue : undefined}
|
||||
valueId={param.id}
|
||||
/>
|
||||
)
|
||||
|
||||
@ -160,19 +160,26 @@ export const EFFECTS: EffectConfig[] = [
|
||||
id: 'clipMode',
|
||||
label: 'Mode',
|
||||
min: 0,
|
||||
max: 2,
|
||||
default: 0,
|
||||
max: 0,
|
||||
default: 'fold',
|
||||
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',
|
||||
label: 'Drive',
|
||||
min: 1,
|
||||
min: 0.001,
|
||||
max: 10,
|
||||
default: 1,
|
||||
step: 0.1,
|
||||
unit: 'x'
|
||||
unit: ''
|
||||
},
|
||||
{
|
||||
id: 'bitcrushDepth',
|
||||
@ -309,8 +316,8 @@ export const EFFECTS: EffectConfig[] = [
|
||||
}
|
||||
]
|
||||
|
||||
export function getDefaultEffectValues(): Record<string, number | boolean> {
|
||||
const defaults: Record<string, number | boolean> = {}
|
||||
export function getDefaultEffectValues(): Record<string, number | boolean | string> {
|
||||
const defaults: Record<string, number | boolean | string> = {}
|
||||
EFFECTS.forEach(effect => {
|
||||
effect.parameters.forEach(param => {
|
||||
defaults[param.id] = param.default
|
||||
@ -326,7 +333,7 @@ export function getDefaultEngineValues(): Record<string, number> {
|
||||
const defaults: Record<string, number> = {}
|
||||
ENGINE_CONTROLS.forEach(control => {
|
||||
control.parameters.forEach(param => {
|
||||
defaults[param.id] = param.default
|
||||
defaults[param.id] = param.default as number
|
||||
})
|
||||
})
|
||||
return defaults
|
||||
|
||||
@ -45,6 +45,7 @@ export class AudioPlayer {
|
||||
this.dispose()
|
||||
|
||||
this.audioContext = new AudioContext({ sampleRate: this.sampleRate })
|
||||
this.workletRegistered = false
|
||||
await this.registerWorklet(this.audioContext)
|
||||
|
||||
this.effectsChain = new EffectsChain(this.audioContext)
|
||||
@ -176,5 +177,6 @@ export class AudioPlayer {
|
||||
this.audioContext.close()
|
||||
this.audioContext = null
|
||||
}
|
||||
this.workletRegistered = false
|
||||
}
|
||||
}
|
||||
@ -72,12 +72,12 @@ export class BitcrushEffect implements Effect {
|
||||
}
|
||||
}
|
||||
|
||||
updateParams(values: Record<string, number>): void {
|
||||
if (values.bitcrushDepth !== undefined) {
|
||||
updateParams(values: Record<string, number | string>): void {
|
||||
if (values.bitcrushDepth !== undefined && typeof values.bitcrushDepth === 'number') {
|
||||
this.bitDepth = values.bitcrushDepth
|
||||
}
|
||||
|
||||
if (values.bitcrushRate !== undefined) {
|
||||
if (values.bitcrushRate !== undefined && typeof values.bitcrushRate === 'number') {
|
||||
this.crushAmount = values.bitcrushRate
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,8 +92,8 @@ export class DelayEffect implements Effect {
|
||||
}
|
||||
}
|
||||
|
||||
updateParams(values: Record<string, number>): void {
|
||||
if (values.delayTime !== undefined) {
|
||||
updateParams(values: Record<string, number | string>): void {
|
||||
if (values.delayTime !== undefined && typeof values.delayTime === 'number') {
|
||||
const time = values.delayTime / 1000
|
||||
this.delayNode.delayTime.setTargetAtTime(
|
||||
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
|
||||
this.feedbackNode.gain.setTargetAtTime(
|
||||
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
|
||||
this.currentWetValue = 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 freq = 200 + tone * 7800
|
||||
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
|
||||
this.createSaturationCurve(saturation)
|
||||
}
|
||||
|
||||
if (values.delayFlutter !== undefined) {
|
||||
if (values.delayFlutter !== undefined && typeof values.delayFlutter === 'number') {
|
||||
const flutter = values.delayFlutter / 100
|
||||
const baseDelay = this.delayNode.delayTime.value
|
||||
const modDepth = baseDelay * flutter * 0.1
|
||||
|
||||
@ -2,7 +2,7 @@ export interface Effect {
|
||||
readonly id: string
|
||||
getInputNode(): AudioNode
|
||||
getOutputNode(): AudioNode
|
||||
updateParams(values: Record<string, number>): void
|
||||
updateParams(values: Record<string, number | string>): void
|
||||
setBypass(bypass: boolean): void
|
||||
dispose(): void
|
||||
}
|
||||
|
||||
@ -44,7 +44,7 @@ export class EffectsChain {
|
||||
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) {
|
||||
const effectId = effect.id
|
||||
const bypassKey = `${effectId}Bypass`
|
||||
@ -53,13 +53,13 @@ export class EffectsChain {
|
||||
effect.setBypass(Boolean(values[bypassKey]))
|
||||
}
|
||||
|
||||
const numericValues: Record<string, number> = {}
|
||||
const effectValues: Record<string, number | string> = {}
|
||||
for (const [key, value] of Object.entries(values)) {
|
||||
if (typeof value === 'number') {
|
||||
numericValues[key] = value
|
||||
if (typeof value === 'number' || typeof value === 'string') {
|
||||
effectValues[key] = value
|
||||
}
|
||||
}
|
||||
effect.updateParams(numericValues)
|
||||
effect.updateParams(effectValues)
|
||||
}
|
||||
|
||||
if (values.masterVolume !== undefined) {
|
||||
|
||||
@ -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) {
|
||||
this.hpEnabled = values.hpEnable === 1
|
||||
this.updateBypassState()
|
||||
}
|
||||
|
||||
if (values.hpFreq !== undefined) {
|
||||
if (values.hpFreq !== undefined && typeof values.hpFreq === 'number') {
|
||||
this.hpFilter.frequency.cancelScheduledValues(this.audioContext.currentTime)
|
||||
this.hpFilter.frequency.setValueAtTime(
|
||||
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.setValueAtTime(
|
||||
this.hpFilter.Q.value,
|
||||
@ -107,7 +107,7 @@ export class FilterEffect implements Effect {
|
||||
this.updateBypassState()
|
||||
}
|
||||
|
||||
if (values.lpFreq !== undefined) {
|
||||
if (values.lpFreq !== undefined && typeof values.lpFreq === 'number') {
|
||||
this.lpFilter.frequency.cancelScheduledValues(this.audioContext.currentTime)
|
||||
this.lpFilter.frequency.setValueAtTime(
|
||||
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.setValueAtTime(
|
||||
this.lpFilter.Q.value,
|
||||
@ -136,7 +136,7 @@ export class FilterEffect implements Effect {
|
||||
this.updateBypassState()
|
||||
}
|
||||
|
||||
if (values.bpFreq !== undefined) {
|
||||
if (values.bpFreq !== undefined && typeof values.bpFreq === 'number') {
|
||||
this.bpFilter.frequency.cancelScheduledValues(this.audioContext.currentTime)
|
||||
this.bpFilter.frequency.setValueAtTime(
|
||||
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.setValueAtTime(
|
||||
this.bpFilter.Q.value,
|
||||
|
||||
@ -15,8 +15,8 @@ export class FoldCrushEffect implements Effect {
|
||||
this.wetNode = audioContext.createGain()
|
||||
this.dryNode = audioContext.createGain()
|
||||
|
||||
this.wetNode.gain.value = 1
|
||||
this.dryNode.gain.value = 0
|
||||
this.wetNode.gain.value = 0
|
||||
this.dryNode.gain.value = 1
|
||||
|
||||
this.inputNode.connect(this.dryNode)
|
||||
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 (values.clipMode !== undefined) {
|
||||
const modeIndex = values.clipMode
|
||||
const clipMode = ['wrap', 'clamp', 'fold'][modeIndex] || 'wrap'
|
||||
this.processorNode.port.postMessage({ type: 'clipMode', value: clipMode })
|
||||
this.processorNode.port.postMessage({ type: 'clipMode', value: values.clipMode })
|
||||
}
|
||||
if (values.wavefolderDrive !== undefined) {
|
||||
this.processorNode.port.postMessage({ type: 'drive', value: values.wavefolderDrive })
|
||||
|
||||
@ -21,7 +21,7 @@ export class PassThroughEffect implements Effect {
|
||||
setBypass(_bypass: boolean): void {
|
||||
}
|
||||
|
||||
updateParams(_values: Record<string, number>): void {
|
||||
updateParams(_values: Record<string, number | string>): void {
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
|
||||
@ -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
|
||||
|
||||
if (values.reverbDecay !== undefined && values.reverbDecay !== this.currentDecay) {
|
||||
if (values.reverbDecay !== undefined && typeof values.reverbDecay === 'number' && values.reverbDecay !== this.currentDecay) {
|
||||
this.currentDecay = values.reverbDecay
|
||||
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
|
||||
needsRegeneration = true
|
||||
}
|
||||
|
||||
if (values.reverbWetDry !== undefined) {
|
||||
if (values.reverbWetDry !== undefined && typeof values.reverbWetDry === 'number') {
|
||||
const wet = values.reverbWetDry / 100
|
||||
this.currentWetValue = 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
|
||||
this.panLfoNode.frequency.setTargetAtTime(
|
||||
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
|
||||
this.panLfoGainNode.gain.setTargetAtTime(
|
||||
width,
|
||||
|
||||
@ -96,12 +96,11 @@ export class WavefolderEffect implements Effect {
|
||||
}
|
||||
}
|
||||
|
||||
updateParams(values: Record<string, number>): void {
|
||||
if (values.clipMode !== undefined) {
|
||||
const modeIndex = values.clipMode
|
||||
this.mode = ['wrap', 'clamp', 'fold'][modeIndex] as ClipMode || 'wrap'
|
||||
updateParams(values: Record<string, number | string>): void {
|
||||
if (values.clipMode !== undefined && typeof values.clipMode === 'string') {
|
||||
this.mode = values.clipMode as ClipMode
|
||||
}
|
||||
if (values.wavefolderDrive !== undefined) {
|
||||
if (values.wavefolderDrive !== undefined && typeof values.wavefolderDrive === 'number') {
|
||||
this.drive = values.wavefolderDrive
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,15 +1,34 @@
|
||||
import { persistentMap } from '@nanostores/persistent'
|
||||
import { map } from 'nanostores'
|
||||
import { getDefaultEngineValues, getDefaultEffectValues } from '../config/effects'
|
||||
|
||||
export const engineSettings = persistentMap('engine:', getDefaultEngineValues(), {
|
||||
encode: JSON.stringify,
|
||||
decode: JSON.parse
|
||||
})
|
||||
const STORAGE_KEY_ENGINE = 'engine:'
|
||||
const STORAGE_KEY_EFFECTS = 'effects:'
|
||||
|
||||
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(),
|
||||
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)
|
||||
}
|
||||
@ -3,9 +3,10 @@ export interface EffectParameter {
|
||||
label: string
|
||||
min: number
|
||||
max: number
|
||||
default: number
|
||||
default: number | string
|
||||
step: number
|
||||
unit?: string
|
||||
options?: { value: string; label: string }[]
|
||||
}
|
||||
|
||||
export interface EffectConfig {
|
||||
@ -15,4 +16,4 @@ export interface EffectConfig {
|
||||
bypassable?: boolean
|
||||
}
|
||||
|
||||
export type EffectValues = Record<string, number | boolean>
|
||||
export type EffectValues = Record<string, number | boolean | string>
|
||||
@ -10,11 +10,6 @@ export function getBitDepthLabel(index: number): string {
|
||||
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 {
|
||||
return `${SAMPLE_RATES[index]}Hz`
|
||||
}
|
||||
Reference in New Issue
Block a user