reorganization
This commit is contained in:
24
node_modules/.vite/deps/_metadata.json
generated
vendored
24
node_modules/.vite/deps/_metadata.json
generated
vendored
@ -1,61 +1,61 @@
|
||||
{
|
||||
"hash": "43216e8d",
|
||||
"configHash": "0b9ed782",
|
||||
"hash": "480f37ab",
|
||||
"configHash": "e652d8ff",
|
||||
"lockfileHash": "dadb379e",
|
||||
"browserHash": "4f0bb7c4",
|
||||
"browserHash": "55e2ae32",
|
||||
"optimized": {
|
||||
"@nanostores/persistent": {
|
||||
"src": "../../.pnpm/@nanostores+persistent@1.1.0_nanostores@1.0.1/node_modules/@nanostores/persistent/index.js",
|
||||
"file": "@nanostores_persistent.js",
|
||||
"fileHash": "7596f610",
|
||||
"fileHash": "41270c63",
|
||||
"needsInterop": false
|
||||
},
|
||||
"@nanostores/react": {
|
||||
"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",
|
||||
"fileHash": "19e1624e",
|
||||
"fileHash": "125eb5ad",
|
||||
"needsInterop": false
|
||||
},
|
||||
"jszip": {
|
||||
"src": "../../.pnpm/jszip@3.10.1/node_modules/jszip/dist/jszip.min.js",
|
||||
"file": "jszip.js",
|
||||
"fileHash": "e59e7578",
|
||||
"fileHash": "ea14d80a",
|
||||
"needsInterop": true
|
||||
},
|
||||
"lucide-react": {
|
||||
"src": "../../.pnpm/lucide-react@0.544.0_react@19.1.1/node_modules/lucide-react/dist/esm/lucide-react.js",
|
||||
"file": "lucide-react.js",
|
||||
"fileHash": "f256b1cf",
|
||||
"fileHash": "347cd426",
|
||||
"needsInterop": false
|
||||
},
|
||||
"react-dom": {
|
||||
"src": "../../.pnpm/react-dom@19.1.1_react@19.1.1/node_modules/react-dom/index.js",
|
||||
"file": "react-dom.js",
|
||||
"fileHash": "74c66a5f",
|
||||
"fileHash": "67fe1167",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react-dom/client": {
|
||||
"src": "../../.pnpm/react-dom@19.1.1_react@19.1.1/node_modules/react-dom/client.js",
|
||||
"file": "react-dom_client.js",
|
||||
"fileHash": "41baaeaa",
|
||||
"fileHash": "9c5fd5d6",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react": {
|
||||
"src": "../../.pnpm/react@19.1.1/node_modules/react/index.js",
|
||||
"file": "react.js",
|
||||
"fileHash": "37acbd53",
|
||||
"fileHash": "35a8de87",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react/jsx-dev-runtime": {
|
||||
"src": "../../.pnpm/react@19.1.1/node_modules/react/jsx-dev-runtime.js",
|
||||
"file": "react_jsx-dev-runtime.js",
|
||||
"fileHash": "bf9c5ec3",
|
||||
"fileHash": "675ff029",
|
||||
"needsInterop": true
|
||||
},
|
||||
"react/jsx-runtime": {
|
||||
"src": "../../.pnpm/react@19.1.1/node_modules/react/jsx-runtime.js",
|
||||
"file": "react_jsx-runtime.js",
|
||||
"fileHash": "082ac5fc",
|
||||
"fileHash": "4bc2e93d",
|
||||
"needsInterop": true
|
||||
}
|
||||
},
|
||||
|
||||
@ -114,8 +114,8 @@ function App() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleEffectChange = (parameterId: string, value: number) => {
|
||||
effectSettings.setKey(parameterId as keyof typeof effectValues, value)
|
||||
const handleEffectChange = (parameterId: string, value: number | boolean) => {
|
||||
effectSettings.setKey(parameterId as any, value as any)
|
||||
if (playbackManagerRef.current) {
|
||||
playbackManagerRef.current.setEffects(effectValues)
|
||||
}
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import { Slider } from './Slider'
|
||||
import { Switch } from './Switch'
|
||||
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) => void
|
||||
onChange: (parameterId: string, value: number | boolean) => void
|
||||
}
|
||||
|
||||
export function EffectsBar({ values, onChange }: EffectsBarProps) {
|
||||
@ -18,13 +19,27 @@ export function EffectsBar({ values, onChange }: EffectsBarProps) {
|
||||
|
||||
return (
|
||||
<div className="bg-black border-t-2 border-white px-6 py-4">
|
||||
<div className="grid grid-cols-4 gap-6">
|
||||
{EFFECTS.flatMap(effect =>
|
||||
effect.parameters.map(param => (
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
{EFFECTS.map(effect => (
|
||||
<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
|
||||
key={param.id}
|
||||
label={param.label}
|
||||
value={values[param.id] ?? param.default}
|
||||
value={values[param.id] as number ?? param.default}
|
||||
min={param.min}
|
||||
max={param.max}
|
||||
step={param.step}
|
||||
@ -33,8 +48,10 @@ export function EffectsBar({ values, onChange }: EffectsBarProps) {
|
||||
formatValue={param.id === 'clipMode' ? formatValue : undefined}
|
||||
valueId={param.id}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -31,7 +31,7 @@ export function EngineControls({ values, onChange }: EngineControlsProps) {
|
||||
{param.label.toUpperCase()}
|
||||
</label>
|
||||
<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>
|
||||
</div>
|
||||
<input
|
||||
@ -39,7 +39,7 @@ export function EngineControls({ values, onChange }: EngineControlsProps) {
|
||||
min={param.min}
|
||||
max={param.max}
|
||||
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))}
|
||||
className="w-full h-[2px] bg-white appearance-none cursor-pointer"
|
||||
/>
|
||||
|
||||
29
src/components/Switch.tsx
Normal file
29
src/components/Switch.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -56,47 +56,34 @@ export const ENGINE_CONTROLS: EffectConfig[] = [
|
||||
|
||||
export const EFFECTS: EffectConfig[] = [
|
||||
{
|
||||
id: 'reverb',
|
||||
name: 'Reverb',
|
||||
id: 'wavefolder',
|
||||
name: 'Wavefolder',
|
||||
bypassable: true,
|
||||
parameters: [
|
||||
{
|
||||
id: 'reverbWetDry',
|
||||
label: 'Reverb',
|
||||
id: 'clipMode',
|
||||
label: 'Mode',
|
||||
min: 0,
|
||||
max: 100,
|
||||
max: 2,
|
||||
default: 0,
|
||||
step: 1,
|
||||
unit: '%'
|
||||
}
|
||||
]
|
||||
unit: ''
|
||||
},
|
||||
{
|
||||
id: 'delay',
|
||||
name: 'Ping Pong Delay',
|
||||
parameters: [
|
||||
{
|
||||
id: 'delayTime',
|
||||
label: 'Time',
|
||||
min: 0,
|
||||
max: 1000,
|
||||
default: 250,
|
||||
step: 10,
|
||||
unit: 'ms'
|
||||
},
|
||||
{
|
||||
id: 'delayFeedback',
|
||||
label: 'Feedback',
|
||||
min: 0,
|
||||
max: 100,
|
||||
default: 50,
|
||||
step: 1,
|
||||
unit: '%'
|
||||
id: 'wavefolderDrive',
|
||||
label: 'Drive',
|
||||
min: 1,
|
||||
max: 10,
|
||||
default: 1,
|
||||
step: 0.1,
|
||||
unit: 'x'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'bitcrush',
|
||||
name: 'Bitcrush',
|
||||
bypassable: true,
|
||||
parameters: [
|
||||
{
|
||||
id: 'bitcrushDepth',
|
||||
@ -119,28 +106,57 @@ export const EFFECTS: EffectConfig[] = [
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'clipmode',
|
||||
name: 'Clip Mode',
|
||||
id: 'delay',
|
||||
name: 'Delay',
|
||||
bypassable: true,
|
||||
parameters: [
|
||||
{
|
||||
id: 'clipMode',
|
||||
label: 'Mode',
|
||||
id: 'delayTime',
|
||||
label: 'Time',
|
||||
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,
|
||||
step: 1,
|
||||
unit: ''
|
||||
unit: '%'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
export function getDefaultEffectValues(): Record<string, number> {
|
||||
const defaults: Record<string, number> = {}
|
||||
export function getDefaultEffectValues(): Record<string, number | boolean> {
|
||||
const defaults: Record<string, number | boolean> = {}
|
||||
EFFECTS.forEach(effect => {
|
||||
effect.parameters.forEach(param => {
|
||||
defaults[param.id] = param.default
|
||||
})
|
||||
if (effect.bypassable) {
|
||||
defaults[`${effect.id}Bypass`] = true
|
||||
}
|
||||
})
|
||||
return defaults
|
||||
}
|
||||
@ -155,7 +171,7 @@ export function getDefaultEngineValues(): Record<string, number> {
|
||||
return defaults
|
||||
}
|
||||
|
||||
export const SAMPLE_RATES = [4000, 8000, 16000, 22050]
|
||||
export const SAMPLE_RATES = [8000, 11025, 22050, 44100]
|
||||
|
||||
export function getSampleRateFromIndex(index: number): number {
|
||||
return SAMPLE_RATES[index] || 8000
|
||||
|
||||
92
src/domain/audio/effects/BitcrushEffect.ts
Normal file
92
src/domain/audio/effects/BitcrushEffect.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
@ -9,6 +9,9 @@ export class DelayEffect implements Effect {
|
||||
private feedbackNode: GainNode
|
||||
private wetNode: GainNode
|
||||
private dryNode: GainNode
|
||||
private bypassed: boolean = false
|
||||
private currentWetValue: number = 0
|
||||
private currentDryValue: number = 1
|
||||
|
||||
constructor(audioContext: AudioContext) {
|
||||
this.inputNode = audioContext.createGain()
|
||||
@ -39,12 +42,28 @@ export class DelayEffect implements Effect {
|
||||
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 {
|
||||
if (values.delayTime !== undefined) {
|
||||
this.delayNode.delayTime.value = values.delayTime / 1000
|
||||
const delayAmount = Math.min(values.delayTime / 1000, 0.5)
|
||||
this.wetNode.gain.value = delayAmount
|
||||
this.dryNode.gain.value = 1 - delayAmount
|
||||
this.currentWetValue = delayAmount
|
||||
this.currentDryValue = 1 - delayAmount
|
||||
|
||||
if (!this.bypassed) {
|
||||
this.wetNode.gain.value = this.currentWetValue
|
||||
this.dryNode.gain.value = this.currentDryValue
|
||||
}
|
||||
}
|
||||
|
||||
if (values.delayFeedback !== undefined) {
|
||||
|
||||
@ -3,6 +3,7 @@ export interface Effect {
|
||||
getInputNode(): AudioNode
|
||||
getOutputNode(): AudioNode
|
||||
updateParams(values: Record<string, number>): void
|
||||
setBypass(bypass: boolean): void
|
||||
dispose(): void
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import type { Effect } from './Effect.interface'
|
||||
import { DelayEffect } from './DelayEffect'
|
||||
import { ReverbEffect } from './ReverbEffect'
|
||||
import { PassThroughEffect } from './PassThroughEffect'
|
||||
import { BitcrushEffect } from './BitcrushEffect'
|
||||
import { WavefolderEffect } from './WavefolderEffect'
|
||||
|
||||
export class EffectsChain {
|
||||
private inputNode: GainNode
|
||||
@ -15,10 +16,10 @@ export class EffectsChain {
|
||||
this.masterGainNode = audioContext.createGain()
|
||||
|
||||
this.effects = [
|
||||
new WavefolderEffect(audioContext),
|
||||
new BitcrushEffect(audioContext),
|
||||
new DelayEffect(audioContext),
|
||||
new ReverbEffect(audioContext),
|
||||
new PassThroughEffect(audioContext, 'bitcrush'),
|
||||
new PassThroughEffect(audioContext, 'clipmode')
|
||||
new ReverbEffect(audioContext)
|
||||
]
|
||||
|
||||
this.setupChain()
|
||||
@ -36,13 +37,26 @@ export class EffectsChain {
|
||||
this.masterGainNode.connect(this.outputNode)
|
||||
}
|
||||
|
||||
updateEffects(values: Record<string, number>): void {
|
||||
updateEffects(values: Record<string, number | boolean>): void {
|
||||
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) {
|
||||
this.masterGainNode.gain.value = values.masterVolume / 100
|
||||
this.masterGainNode.gain.value = Number(values.masterVolume) / 100
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -18,6 +18,9 @@ export class PassThroughEffect implements Effect {
|
||||
return this.node
|
||||
}
|
||||
|
||||
setBypass(_bypass: boolean): void {
|
||||
}
|
||||
|
||||
updateParams(_values: Record<string, number>): void {
|
||||
}
|
||||
|
||||
|
||||
@ -9,6 +9,9 @@ export class ReverbEffect implements Effect {
|
||||
private convolverNode: ConvolverNode
|
||||
private wetNode: GainNode
|
||||
private dryNode: GainNode
|
||||
private bypassed: boolean = false
|
||||
private currentWetValue: number = 0
|
||||
private currentDryValue: number = 1
|
||||
|
||||
constructor(audioContext: AudioContext) {
|
||||
this.audioContext = audioContext
|
||||
@ -52,11 +55,27 @@ export class ReverbEffect implements Effect {
|
||||
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 {
|
||||
if (values.reverbWetDry !== undefined) {
|
||||
const wet = values.reverbWetDry / 100
|
||||
this.wetNode.gain.value = wet
|
||||
this.dryNode.gain.value = 1 - wet
|
||||
this.currentWetValue = wet
|
||||
this.currentDryValue = 1 - wet
|
||||
|
||||
if (!this.bypassed) {
|
||||
this.wetNode.gain.value = this.currentWetValue
|
||||
this.dryNode.gain.value = this.currentDryValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
116
src/domain/audio/effects/WavefolderEffect.ts
Normal file
116
src/domain/audio/effects/WavefolderEffect.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
@ -78,18 +78,24 @@ export class EffectsChain {
|
||||
}
|
||||
|
||||
updateEffects(values: EffectValues): void {
|
||||
if (typeof values.reverbWetDry === 'number') {
|
||||
const reverbWet = values.reverbWetDry / 100
|
||||
this.reverbWetNode.gain.value = reverbWet
|
||||
this.reverbDryNode.gain.value = 1 - reverbWet
|
||||
}
|
||||
|
||||
if (typeof values.delayTime === 'number') {
|
||||
this.delayNode.delayTime.value = values.delayTime / 1000
|
||||
this.delayFeedbackNode.gain.value = values.delayFeedback / 100
|
||||
|
||||
const delayAmount = Math.min(values.delayTime / 1000, 0.5)
|
||||
this.delayWetNode.gain.value = 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
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,6 +12,7 @@ export interface EffectConfig {
|
||||
id: string
|
||||
name: string
|
||||
parameters: EffectParameter[]
|
||||
bypassable?: boolean
|
||||
}
|
||||
|
||||
export type EffectValues = Record<string, number>
|
||||
export type EffectValues = Record<string, number | boolean>
|
||||
Reference in New Issue
Block a user