Safer fold and crush section
This commit is contained in:
@ -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
|
soft(x, k) {
|
||||||
}
|
return Math.tanh(x * (1 + k))
|
||||||
if (folded < -1.0) {
|
}
|
||||||
folded = -2.0 - folded
|
|
||||||
}
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
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 { 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}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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 })
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
}
|
||||||
@ -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>
|
||||||
@ -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`
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user