Fixing more bugs
This commit is contained in:
109
public/worklets/fold-crush-processor.js
Normal file
109
public/worklets/fold-crush-processor.js
Normal file
@ -0,0 +1,109 @@
|
||||
class FoldCrushProcessor extends AudioWorkletProcessor {
|
||||
constructor() {
|
||||
super()
|
||||
|
||||
this.clipMode = 'wrap'
|
||||
this.drive = 1
|
||||
this.bitDepth = 16
|
||||
this.crushAmount = 0
|
||||
this.bitcrushPhase = 0
|
||||
this.lastCrushedValue = 0
|
||||
|
||||
this.port.onmessage = (event) => {
|
||||
const { type, value } = event.data
|
||||
switch (type) {
|
||||
case 'clipMode':
|
||||
this.clipMode = value
|
||||
break
|
||||
case 'drive':
|
||||
this.drive = value
|
||||
break
|
||||
case 'bitDepth':
|
||||
this.bitDepth = value
|
||||
break
|
||||
case 'crushAmount':
|
||||
this.crushAmount = value
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wrap(sample) {
|
||||
const range = 2.0
|
||||
let wrapped = sample
|
||||
while (wrapped > 1.0) wrapped -= range
|
||||
while (wrapped < -1.0) wrapped += range
|
||||
return wrapped
|
||||
}
|
||||
|
||||
clamp(sample) {
|
||||
return Math.max(-1.0, Math.min(1.0, sample))
|
||||
}
|
||||
|
||||
fold(sample) {
|
||||
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
|
||||
}
|
||||
|
||||
processWavefolder(sample) {
|
||||
switch (this.clipMode) {
|
||||
case 'wrap':
|
||||
return this.wrap(sample)
|
||||
case 'clamp':
|
||||
return this.clamp(sample)
|
||||
case 'fold':
|
||||
return this.fold(sample)
|
||||
default:
|
||||
return sample
|
||||
}
|
||||
}
|
||||
|
||||
processBitcrush(sample) {
|
||||
if (this.crushAmount === 0 && this.bitDepth === 16) {
|
||||
return sample
|
||||
}
|
||||
|
||||
const step = Math.pow(0.5, this.bitDepth)
|
||||
const phaseIncrement = 1 - (this.crushAmount / 100)
|
||||
|
||||
this.bitcrushPhase += phaseIncrement
|
||||
|
||||
if (this.bitcrushPhase >= 1.0) {
|
||||
this.bitcrushPhase -= 1.0
|
||||
const crushed = Math.floor(sample / step + 0.5) * step
|
||||
this.lastCrushedValue = Math.max(-1, Math.min(1, crushed))
|
||||
return this.lastCrushedValue
|
||||
} else {
|
||||
return this.lastCrushedValue
|
||||
}
|
||||
}
|
||||
|
||||
process(inputs, outputs) {
|
||||
const input = inputs[0]
|
||||
const output = outputs[0]
|
||||
|
||||
if (input.length > 0 && output.length > 0) {
|
||||
const inputChannel = input[0]
|
||||
const outputChannel = output[0]
|
||||
|
||||
for (let i = 0; i < inputChannel.length; i++) {
|
||||
const driven = inputChannel[i] * this.drive
|
||||
let processed = this.processWavefolder(driven)
|
||||
processed = this.processBitcrush(processed)
|
||||
outputChannel[i] = processed
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
registerProcessor('fold-crush-processor', FoldCrushProcessor)
|
||||
35
src/App.tsx
35
src/App.tsx
@ -24,6 +24,11 @@ function App() {
|
||||
const playbackManagerRef = useRef<PlaybackManager | null>(null)
|
||||
const downloadServiceRef = useRef<DownloadService>(new DownloadService())
|
||||
const animationFrameRef = useRef<number | null>(null)
|
||||
const formulasRef = useRef<string[][]>(formulas)
|
||||
|
||||
useEffect(() => {
|
||||
formulasRef.current = formulas
|
||||
}, [formulas])
|
||||
|
||||
useEffect(() => {
|
||||
effectSettings.setKey('masterVolume', engineValues.masterVolume)
|
||||
@ -34,21 +39,21 @@ function App() {
|
||||
setQueued(null)
|
||||
}
|
||||
|
||||
const playFormula = (formula: string, id: string) => {
|
||||
const playFormula = async (formula: string, id: string) => {
|
||||
const sampleRate = getSampleRateFromIndex(engineValues.sampleRate)
|
||||
const duration = engineValues.loopDuration
|
||||
|
||||
if (!playbackManagerRef.current) {
|
||||
playbackManagerRef.current = new PlaybackManager({ sampleRate, duration })
|
||||
} else {
|
||||
playbackManagerRef.current.updateOptions({ sampleRate, duration })
|
||||
await playbackManagerRef.current.updateOptions({ sampleRate, duration })
|
||||
}
|
||||
|
||||
playbackManagerRef.current.stop()
|
||||
playbackManagerRef.current.setEffects(effectValues)
|
||||
playbackManagerRef.current.setPitch(engineValues.pitch ?? 1)
|
||||
|
||||
const success = playbackManagerRef.current.play(formula, sampleRate, duration)
|
||||
const success = await playbackManagerRef.current.play(formula, sampleRate, duration)
|
||||
|
||||
if (success) {
|
||||
setPlaying(id)
|
||||
@ -94,7 +99,7 @@ function App() {
|
||||
setQueued(id)
|
||||
if (playbackManagerRef.current) {
|
||||
playbackManagerRef.current.scheduleNextTrack(() => {
|
||||
const queuedFormula = formulas.flat()[parseInt(id.split('-')[0]) * 2 + parseInt(id.split('-')[1])]
|
||||
const queuedFormula = formulasRef.current.flat()[parseInt(id.split('-')[0]) * 2 + parseInt(id.split('-')[1])]
|
||||
if (queuedFormula) {
|
||||
playFormula(queuedFormula, id)
|
||||
}
|
||||
@ -112,7 +117,7 @@ function App() {
|
||||
engineSettings.setKey(parameterId as keyof typeof engineValues, value)
|
||||
|
||||
if (parameterId === 'masterVolume' && playbackManagerRef.current) {
|
||||
playbackManagerRef.current.setEffects(effectValues)
|
||||
playbackManagerRef.current.setEffects({ ...effectValues, masterVolume: value })
|
||||
}
|
||||
|
||||
if (parameterId === 'pitch' && playbackManagerRef.current) {
|
||||
@ -141,19 +146,25 @@ function App() {
|
||||
const id = `${row}-${col}`
|
||||
const newFormula = generateRandomFormula(engineValues.complexity)
|
||||
|
||||
setFormulas(prevFormulas => {
|
||||
const newFormulas = [...prevFormulas]
|
||||
newFormulas[row] = [...newFormulas[row]]
|
||||
newFormulas[row][col] = newFormula
|
||||
return newFormulas
|
||||
})
|
||||
|
||||
if (playing === id && playbackManagerRef.current) {
|
||||
setRegenerating(id)
|
||||
playbackManagerRef.current.scheduleNextTrack(() => {
|
||||
setFormulas(prevFormulas => {
|
||||
const newFormulas = [...prevFormulas]
|
||||
newFormulas[row] = [...newFormulas[row]]
|
||||
newFormulas[row][col] = newFormula
|
||||
return newFormulas
|
||||
})
|
||||
playFormula(newFormula, id)
|
||||
setRegenerating(null)
|
||||
})
|
||||
} else {
|
||||
setFormulas(prevFormulas => {
|
||||
const newFormulas = [...prevFormulas]
|
||||
newFormulas[row] = [...newFormulas[row]]
|
||||
newFormulas[row][col] = newFormula
|
||||
return newFormulas
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -17,62 +17,125 @@ export function EffectsBar({ values, onChange }: EffectsBarProps) {
|
||||
return value.toString()
|
||||
}
|
||||
|
||||
const renderFilterEffect = (effect: typeof EFFECTS[number]) => {
|
||||
const filterGroups = [
|
||||
{ prefix: 'hp', label: 'HP' },
|
||||
{ prefix: 'lp', label: 'LP' },
|
||||
{ prefix: 'bp', label: 'BP' }
|
||||
]
|
||||
|
||||
return (
|
||||
<div key={effect.id} className="border-2 border-white p-3">
|
||||
<h3 className="font-mono text-[10px] tracking-[0.2em] text-white mb-3">
|
||||
{effect.name.toUpperCase()}
|
||||
</h3>
|
||||
<div className="flex flex-col gap-3">
|
||||
{filterGroups.map(group => {
|
||||
const enableParam = effect.parameters.find(p => p.id === `${group.prefix}Enable`)
|
||||
const freqParam = effect.parameters.find(p => p.id === `${group.prefix}Freq`)
|
||||
const resParam = effect.parameters.find(p => p.id === `${group.prefix}Res`)
|
||||
|
||||
if (!enableParam || !freqParam || !resParam) return null
|
||||
|
||||
return (
|
||||
<div key={group.prefix} className="flex gap-2 items-center">
|
||||
<Switch
|
||||
checked={Boolean(values[enableParam.id])}
|
||||
onChange={(checked) => onChange(enableParam.id, checked ? 1 : 0)}
|
||||
vertical
|
||||
/>
|
||||
<div className="flex-1 flex flex-col gap-2">
|
||||
<Slider
|
||||
label={freqParam.label}
|
||||
value={values[freqParam.id] as number ?? freqParam.default}
|
||||
min={freqParam.min}
|
||||
max={freqParam.max}
|
||||
step={freqParam.step}
|
||||
unit={freqParam.unit}
|
||||
onChange={(value) => onChange(freqParam.id, value)}
|
||||
valueId={freqParam.id}
|
||||
/>
|
||||
<Slider
|
||||
label={resParam.label}
|
||||
value={values[resParam.id] as number ?? resParam.default}
|
||||
min={resParam.min}
|
||||
max={resParam.max}
|
||||
step={resParam.step}
|
||||
unit={resParam.unit}
|
||||
onChange={(value) => onChange(resParam.id, value)}
|
||||
valueId={resParam.id}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-black border-t-2 border-white px-6 py-4">
|
||||
<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 => {
|
||||
const isSwitch = param.min === 0 && param.max === 1 && param.step === 1
|
||||
{EFFECTS.map(effect => {
|
||||
if (effect.id === 'filter') {
|
||||
return renderFilterEffect(effect)
|
||||
}
|
||||
|
||||
if (isSwitch) {
|
||||
return (
|
||||
<div key={param.id} className="flex flex-col gap-1 mt-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-mono text-[9px] tracking-[0.15em] text-white">
|
||||
{param.label.toUpperCase()}
|
||||
</span>
|
||||
<Switch
|
||||
checked={Boolean(values[param.id])}
|
||||
onChange={(checked) => onChange(param.id, checked ? 1 : 0)}
|
||||
label={Boolean(values[param.id]) ? 'ON' : 'OFF'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Slider
|
||||
key={param.id}
|
||||
label={param.label}
|
||||
value={values[param.id] as number ?? param.default}
|
||||
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}
|
||||
return (
|
||||
<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 => {
|
||||
const isSwitch = param.min === 0 && param.max === 1 && param.step === 1
|
||||
|
||||
if (isSwitch) {
|
||||
return (
|
||||
<div key={param.id} className="flex flex-col gap-1 mt-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-mono text-[9px] tracking-[0.15em] text-white">
|
||||
{param.label.toUpperCase()}
|
||||
</span>
|
||||
<Switch
|
||||
checked={Boolean(values[param.id])}
|
||||
onChange={(checked) => onChange(param.id, checked ? 1 : 0)}
|
||||
label={Boolean(values[param.id]) ? 'ON' : 'OFF'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Slider
|
||||
key={param.id}
|
||||
label={param.label}
|
||||
value={values[param.id] as number ?? param.default}
|
||||
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}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -2,9 +2,29 @@ interface SwitchProps {
|
||||
checked: boolean
|
||||
onChange: (checked: boolean) => void
|
||||
label?: string
|
||||
vertical?: boolean
|
||||
}
|
||||
|
||||
export function Switch({ checked, onChange, label }: SwitchProps) {
|
||||
export function Switch({ checked, onChange, label, vertical = false }: SwitchProps) {
|
||||
if (vertical) {
|
||||
return (
|
||||
<label className="flex flex-col items-center gap-1 cursor-pointer">
|
||||
<div
|
||||
className={`relative w-4 h-12 border-2 transition-colors ${
|
||||
checked ? 'bg-white border-white' : 'bg-black border-white'
|
||||
}`}
|
||||
onClick={() => onChange(!checked)}
|
||||
>
|
||||
<div
|
||||
className={`absolute left-0 w-3 h-3 transition-transform ${
|
||||
checked ? 'translate-y-8 bg-black' : 'translate-y-0 bg-white'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<div
|
||||
|
||||
@ -17,19 +17,54 @@ export class AudioPlayer {
|
||||
private sampleRate: number
|
||||
private duration: number
|
||||
private pitch: number = 1
|
||||
private workletRegistered: boolean = false
|
||||
|
||||
constructor(options: AudioPlayerOptions) {
|
||||
this.sampleRate = options.sampleRate
|
||||
this.duration = options.duration
|
||||
}
|
||||
|
||||
updateOptions(options: Partial<AudioPlayerOptions>): void {
|
||||
async updateOptions(options: Partial<AudioPlayerOptions>): Promise<void> {
|
||||
const sampleRateChanged = options.sampleRate !== undefined && options.sampleRate !== this.sampleRate
|
||||
|
||||
if (options.sampleRate !== undefined) {
|
||||
this.sampleRate = options.sampleRate
|
||||
}
|
||||
if (options.duration !== undefined) {
|
||||
this.duration = options.duration
|
||||
}
|
||||
|
||||
if (sampleRateChanged && this.audioContext) {
|
||||
await this.recreateAudioContext()
|
||||
}
|
||||
}
|
||||
|
||||
private async recreateAudioContext(): Promise<void> {
|
||||
const wasPlaying = this.sourceNode !== null
|
||||
|
||||
this.dispose()
|
||||
|
||||
this.audioContext = new AudioContext({ sampleRate: this.sampleRate })
|
||||
await this.registerWorklet(this.audioContext)
|
||||
|
||||
this.effectsChain = new EffectsChain(this.audioContext)
|
||||
await this.effectsChain.initialize(this.audioContext)
|
||||
this.effectsChain.updateEffects(this.effectValues)
|
||||
|
||||
if (wasPlaying) {
|
||||
console.warn('Audio context recreated due to sample rate change. Playback stopped.')
|
||||
}
|
||||
}
|
||||
|
||||
private async registerWorklet(context: AudioContext): Promise<void> {
|
||||
if (this.workletRegistered) return
|
||||
|
||||
try {
|
||||
await context.audioWorklet.addModule('/worklets/fold-crush-processor.js')
|
||||
this.workletRegistered = true
|
||||
} catch (error) {
|
||||
console.error('Failed to register AudioWorklet:', error)
|
||||
}
|
||||
}
|
||||
|
||||
setEffects(values: EffectValues): void {
|
||||
@ -50,13 +85,15 @@ export class AudioPlayer {
|
||||
}
|
||||
}
|
||||
|
||||
play(buffer: Float32Array, onEnded?: () => void): void {
|
||||
async play(buffer: Float32Array, onEnded?: () => void): Promise<void> {
|
||||
if (!this.audioContext) {
|
||||
this.audioContext = new AudioContext({ sampleRate: this.sampleRate })
|
||||
await this.registerWorklet(this.audioContext)
|
||||
}
|
||||
|
||||
if (!this.effectsChain) {
|
||||
this.effectsChain = new EffectsChain(this.audioContext)
|
||||
await this.effectsChain.initialize(this.audioContext)
|
||||
this.effectsChain.updateEffects(this.effectValues)
|
||||
}
|
||||
|
||||
@ -108,7 +145,8 @@ export class AudioPlayer {
|
||||
return 0
|
||||
}
|
||||
const elapsed = this.audioContext.currentTime - this.startTime
|
||||
return (elapsed % this.duration) / this.duration
|
||||
const actualDuration = this.duration / this.pitch
|
||||
return (elapsed % actualDuration) / actualDuration
|
||||
}
|
||||
|
||||
pause(): void {
|
||||
|
||||
@ -9,15 +9,18 @@ export class EffectsChain {
|
||||
private outputNode: GainNode
|
||||
private masterGainNode: GainNode
|
||||
private effects: Effect[]
|
||||
private foldCrushEffect: FoldCrushEffect
|
||||
|
||||
constructor(audioContext: AudioContext) {
|
||||
this.inputNode = audioContext.createGain()
|
||||
this.outputNode = audioContext.createGain()
|
||||
this.masterGainNode = audioContext.createGain()
|
||||
|
||||
this.foldCrushEffect = new FoldCrushEffect(audioContext)
|
||||
|
||||
this.effects = [
|
||||
new FilterEffect(audioContext),
|
||||
new FoldCrushEffect(audioContext),
|
||||
this.foldCrushEffect,
|
||||
new DelayEffect(audioContext),
|
||||
new ReverbEffect(audioContext)
|
||||
]
|
||||
@ -25,6 +28,10 @@ export class EffectsChain {
|
||||
this.setupChain()
|
||||
}
|
||||
|
||||
async initialize(audioContext: AudioContext): Promise<void> {
|
||||
await this.foldCrushEffect.initialize(audioContext)
|
||||
}
|
||||
|
||||
private setupChain(): void {
|
||||
let currentInput: AudioNode = this.inputNode
|
||||
|
||||
|
||||
@ -6,6 +6,8 @@ export class FilterEffect implements Effect {
|
||||
private audioContext: AudioContext
|
||||
private inputNode: GainNode
|
||||
private outputNode: GainNode
|
||||
private wetNode: GainNode
|
||||
private dryNode: GainNode
|
||||
private hpFilter: BiquadFilterNode
|
||||
private lpFilter: BiquadFilterNode
|
||||
private bpFilter: BiquadFilterNode
|
||||
@ -17,26 +19,34 @@ export class FilterEffect implements Effect {
|
||||
this.audioContext = audioContext
|
||||
this.inputNode = audioContext.createGain()
|
||||
this.outputNode = audioContext.createGain()
|
||||
this.wetNode = audioContext.createGain()
|
||||
this.dryNode = audioContext.createGain()
|
||||
|
||||
this.wetNode.gain.value = 0
|
||||
this.dryNode.gain.value = 1
|
||||
|
||||
this.hpFilter = audioContext.createBiquadFilter()
|
||||
this.hpFilter.type = 'highpass'
|
||||
this.hpFilter.frequency.value = 20
|
||||
this.hpFilter.frequency.value = 1000
|
||||
this.hpFilter.Q.value = 1
|
||||
|
||||
this.lpFilter = audioContext.createBiquadFilter()
|
||||
this.lpFilter.type = 'lowpass'
|
||||
this.lpFilter.frequency.value = 20000
|
||||
this.lpFilter.frequency.value = 5000
|
||||
this.lpFilter.Q.value = 1
|
||||
|
||||
this.bpFilter = audioContext.createBiquadFilter()
|
||||
this.bpFilter.type = 'allpass'
|
||||
this.bpFilter.type = 'bandpass'
|
||||
this.bpFilter.frequency.value = 1000
|
||||
this.bpFilter.Q.value = 1
|
||||
|
||||
this.inputNode.connect(this.dryNode)
|
||||
this.inputNode.connect(this.hpFilter)
|
||||
this.hpFilter.connect(this.lpFilter)
|
||||
this.lpFilter.connect(this.bpFilter)
|
||||
this.bpFilter.connect(this.outputNode)
|
||||
this.bpFilter.connect(this.wetNode)
|
||||
this.dryNode.connect(this.outputNode)
|
||||
this.wetNode.connect(this.outputNode)
|
||||
}
|
||||
|
||||
getInputNode(): AudioNode {
|
||||
@ -47,24 +57,28 @@ export class FilterEffect implements Effect {
|
||||
return this.outputNode
|
||||
}
|
||||
|
||||
setBypass(bypass: boolean): void {
|
||||
setBypass(_bypass: boolean): void {
|
||||
// No global bypass for filters - each filter has individual enable switch
|
||||
}
|
||||
|
||||
updateParams(values: Record<string, number>): void {
|
||||
private updateBypassState(): void {
|
||||
const anyEnabled = this.hpEnabled || this.lpEnabled || this.bpEnabled
|
||||
if (anyEnabled) {
|
||||
this.wetNode.gain.value = 1
|
||||
this.dryNode.gain.value = 0
|
||||
} else {
|
||||
this.wetNode.gain.value = 0
|
||||
this.dryNode.gain.value = 1
|
||||
}
|
||||
}
|
||||
|
||||
updateParams(values: Record<string, number>): void {
|
||||
if (values.hpEnable !== undefined) {
|
||||
const enable = values.hpEnable === 1
|
||||
if (enable && !this.hpEnabled) {
|
||||
this.hpFilter.type = 'highpass'
|
||||
this.hpEnabled = true
|
||||
} else if (!enable && this.hpEnabled) {
|
||||
this.hpFilter.type = 'allpass'
|
||||
this.hpEnabled = false
|
||||
}
|
||||
this.hpEnabled = values.hpEnable === 1
|
||||
this.updateBypassState()
|
||||
}
|
||||
|
||||
if (values.hpFreq !== undefined && this.hpEnabled) {
|
||||
if (values.hpFreq !== undefined) {
|
||||
this.hpFilter.frequency.cancelScheduledValues(this.audioContext.currentTime)
|
||||
this.hpFilter.frequency.setValueAtTime(
|
||||
this.hpFilter.frequency.value,
|
||||
@ -76,7 +90,7 @@ export class FilterEffect implements Effect {
|
||||
)
|
||||
}
|
||||
|
||||
if (values.hpRes !== undefined && this.hpEnabled) {
|
||||
if (values.hpRes !== undefined) {
|
||||
this.hpFilter.Q.cancelScheduledValues(this.audioContext.currentTime)
|
||||
this.hpFilter.Q.setValueAtTime(
|
||||
this.hpFilter.Q.value,
|
||||
@ -89,17 +103,11 @@ export class FilterEffect implements Effect {
|
||||
}
|
||||
|
||||
if (values.lpEnable !== undefined) {
|
||||
const enable = values.lpEnable === 1
|
||||
if (enable && !this.lpEnabled) {
|
||||
this.lpFilter.type = 'lowpass'
|
||||
this.lpEnabled = true
|
||||
} else if (!enable && this.lpEnabled) {
|
||||
this.lpFilter.type = 'allpass'
|
||||
this.lpEnabled = false
|
||||
}
|
||||
this.lpEnabled = values.lpEnable === 1
|
||||
this.updateBypassState()
|
||||
}
|
||||
|
||||
if (values.lpFreq !== undefined && this.lpEnabled) {
|
||||
if (values.lpFreq !== undefined) {
|
||||
this.lpFilter.frequency.cancelScheduledValues(this.audioContext.currentTime)
|
||||
this.lpFilter.frequency.setValueAtTime(
|
||||
this.lpFilter.frequency.value,
|
||||
@ -111,7 +119,7 @@ export class FilterEffect implements Effect {
|
||||
)
|
||||
}
|
||||
|
||||
if (values.lpRes !== undefined && this.lpEnabled) {
|
||||
if (values.lpRes !== undefined) {
|
||||
this.lpFilter.Q.cancelScheduledValues(this.audioContext.currentTime)
|
||||
this.lpFilter.Q.setValueAtTime(
|
||||
this.lpFilter.Q.value,
|
||||
@ -124,17 +132,11 @@ export class FilterEffect implements Effect {
|
||||
}
|
||||
|
||||
if (values.bpEnable !== undefined) {
|
||||
const enable = values.bpEnable === 1
|
||||
if (enable && !this.bpEnabled) {
|
||||
this.bpFilter.type = 'bandpass'
|
||||
this.bpEnabled = true
|
||||
} else if (!enable && this.bpEnabled) {
|
||||
this.bpFilter.type = 'allpass'
|
||||
this.bpEnabled = false
|
||||
}
|
||||
this.bpEnabled = values.bpEnable === 1
|
||||
this.updateBypassState()
|
||||
}
|
||||
|
||||
if (values.bpFreq !== undefined && this.bpEnabled) {
|
||||
if (values.bpFreq !== undefined) {
|
||||
this.bpFilter.frequency.cancelScheduledValues(this.audioContext.currentTime)
|
||||
this.bpFilter.frequency.setValueAtTime(
|
||||
this.bpFilter.frequency.value,
|
||||
@ -146,7 +148,7 @@ export class FilterEffect implements Effect {
|
||||
)
|
||||
}
|
||||
|
||||
if (values.bpRes !== undefined && this.bpEnabled) {
|
||||
if (values.bpRes !== undefined) {
|
||||
this.bpFilter.Q.cancelScheduledValues(this.audioContext.currentTime)
|
||||
this.bpFilter.Q.setValueAtTime(
|
||||
this.bpFilter.Q.value,
|
||||
@ -162,6 +164,8 @@ export class FilterEffect implements Effect {
|
||||
dispose(): void {
|
||||
this.inputNode.disconnect()
|
||||
this.outputNode.disconnect()
|
||||
this.wetNode.disconnect()
|
||||
this.dryNode.disconnect()
|
||||
this.hpFilter.disconnect()
|
||||
this.lpFilter.disconnect()
|
||||
this.bpFilter.disconnect()
|
||||
|
||||
@ -1,107 +1,35 @@
|
||||
import type { Effect } from './Effect.interface'
|
||||
|
||||
type ClipMode = 'wrap' | 'clamp' | 'fold'
|
||||
|
||||
export class FoldCrushEffect implements Effect {
|
||||
readonly id = 'foldcrush'
|
||||
|
||||
private inputNode: GainNode
|
||||
private outputNode: GainNode
|
||||
private processorNode: ScriptProcessorNode
|
||||
private processorNode: AudioWorkletNode | null = null
|
||||
private wetNode: GainNode
|
||||
private dryNode: GainNode
|
||||
private clipMode: ClipMode = 'wrap'
|
||||
private drive: number = 1
|
||||
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)
|
||||
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
const driven = input[i] * this.drive
|
||||
let processed = this.processWavefolder(driven)
|
||||
processed = this.processBitcrush(processed, i, output)
|
||||
output[i] = processed
|
||||
}
|
||||
}
|
||||
|
||||
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 processWavefolder(sample: number): number {
|
||||
switch (this.clipMode) {
|
||||
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
|
||||
}
|
||||
|
||||
private bitcrushPhase: number = 0
|
||||
private lastCrushedValue: number = 0
|
||||
|
||||
private processBitcrush(sample: number, index: number, output: Float32Array): number {
|
||||
if (this.crushAmount === 0 && this.bitDepth === 16) {
|
||||
return sample
|
||||
}
|
||||
|
||||
const step = Math.pow(0.5, this.bitDepth)
|
||||
const phaseIncrement = 1 - (this.crushAmount / 100)
|
||||
|
||||
this.bitcrushPhase += phaseIncrement
|
||||
|
||||
if (this.bitcrushPhase >= 1.0) {
|
||||
this.bitcrushPhase -= 1.0
|
||||
const crushed = Math.floor(sample / step + 0.5) * step
|
||||
this.lastCrushedValue = Math.max(-1, Math.min(1, crushed))
|
||||
return this.lastCrushedValue
|
||||
} else {
|
||||
return this.lastCrushedValue
|
||||
async initialize(audioContext: AudioContext): Promise<void> {
|
||||
try {
|
||||
this.processorNode = new AudioWorkletNode(audioContext, 'fold-crush-processor')
|
||||
this.inputNode.connect(this.processorNode)
|
||||
this.processorNode.connect(this.wetNode)
|
||||
this.wetNode.connect(this.outputNode)
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize FoldCrushEffect worklet:', error)
|
||||
}
|
||||
}
|
||||
|
||||
@ -124,23 +52,28 @@ export class FoldCrushEffect implements Effect {
|
||||
}
|
||||
|
||||
updateParams(values: Record<string, number>): void {
|
||||
if (!this.processorNode) return
|
||||
|
||||
if (values.clipMode !== undefined) {
|
||||
const modeIndex = values.clipMode
|
||||
this.clipMode = ['wrap', 'clamp', 'fold'][modeIndex] as ClipMode || 'wrap'
|
||||
const clipMode = ['wrap', 'clamp', 'fold'][modeIndex] || 'wrap'
|
||||
this.processorNode.port.postMessage({ type: 'clipMode', value: clipMode })
|
||||
}
|
||||
if (values.wavefolderDrive !== undefined) {
|
||||
this.drive = values.wavefolderDrive
|
||||
this.processorNode.port.postMessage({ type: 'drive', value: values.wavefolderDrive })
|
||||
}
|
||||
if (values.bitcrushDepth !== undefined) {
|
||||
this.bitDepth = values.bitcrushDepth
|
||||
this.processorNode.port.postMessage({ type: 'bitDepth', value: values.bitcrushDepth })
|
||||
}
|
||||
if (values.bitcrushRate !== undefined) {
|
||||
this.crushAmount = values.bitcrushRate
|
||||
this.processorNode.port.postMessage({ type: 'crushAmount', value: values.bitcrushRate })
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.processorNode.disconnect()
|
||||
if (this.processorNode) {
|
||||
this.processorNode.disconnect()
|
||||
}
|
||||
this.wetNode.disconnect()
|
||||
this.dryNode.disconnect()
|
||||
this.inputNode.disconnect()
|
||||
|
||||
@ -6,10 +6,10 @@ export class ReverbEffect implements Effect {
|
||||
private audioContext: AudioContext
|
||||
private inputNode: GainNode
|
||||
private outputNode: GainNode
|
||||
private finalOutputNode: GainNode
|
||||
private convolverNode: ConvolverNode
|
||||
private wetNode: GainNode
|
||||
private dryNode: GainNode
|
||||
private mixNode: GainNode
|
||||
private pannerNode: StereoPannerNode
|
||||
private panLfoNode: OscillatorNode
|
||||
private panLfoGainNode: GainNode
|
||||
@ -23,7 +23,7 @@ export class ReverbEffect implements Effect {
|
||||
this.audioContext = audioContext
|
||||
this.inputNode = audioContext.createGain()
|
||||
this.outputNode = audioContext.createGain()
|
||||
this.finalOutputNode = audioContext.createGain()
|
||||
this.mixNode = audioContext.createGain()
|
||||
this.convolverNode = audioContext.createConvolver()
|
||||
this.wetNode = audioContext.createGain()
|
||||
this.dryNode = audioContext.createGain()
|
||||
@ -43,14 +43,26 @@ export class ReverbEffect implements Effect {
|
||||
this.inputNode.connect(this.dryNode)
|
||||
this.inputNode.connect(this.convolverNode)
|
||||
this.convolverNode.connect(this.wetNode)
|
||||
this.dryNode.connect(this.outputNode)
|
||||
this.wetNode.connect(this.outputNode)
|
||||
this.outputNode.connect(this.pannerNode)
|
||||
this.pannerNode.connect(this.finalOutputNode)
|
||||
this.dryNode.connect(this.mixNode)
|
||||
this.wetNode.connect(this.mixNode)
|
||||
this.mixNode.connect(this.pannerNode)
|
||||
this.pannerNode.connect(this.outputNode)
|
||||
|
||||
this.convolverNode.buffer = this.createDummyBuffer()
|
||||
this.generateReverb(this.currentDecay, this.currentDamping)
|
||||
}
|
||||
|
||||
private createDummyBuffer(): AudioBuffer {
|
||||
const buffer = this.audioContext.createBuffer(2, this.audioContext.sampleRate * 0.1, this.audioContext.sampleRate)
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const data = buffer.getChannelData(i)
|
||||
for (let j = 0; j < data.length; j++) {
|
||||
data[j] = (Math.random() * 2 - 1) * Math.exp(-j / (this.audioContext.sampleRate * 0.05))
|
||||
}
|
||||
}
|
||||
return buffer
|
||||
}
|
||||
|
||||
private generateReverb(decayTime: number, damping: number): void {
|
||||
const sampleRate = this.audioContext.sampleRate
|
||||
const numChannels = 2
|
||||
@ -127,7 +139,7 @@ export class ReverbEffect implements Effect {
|
||||
}
|
||||
|
||||
getOutputNode(): AudioNode {
|
||||
return this.finalOutputNode
|
||||
return this.outputNode
|
||||
}
|
||||
|
||||
setBypass(bypass: boolean): void {
|
||||
@ -194,7 +206,7 @@ export class ReverbEffect implements Effect {
|
||||
this.panLfoGainNode.disconnect()
|
||||
this.inputNode.disconnect()
|
||||
this.outputNode.disconnect()
|
||||
this.finalOutputNode.disconnect()
|
||||
this.mixNode.disconnect()
|
||||
this.convolverNode.disconnect()
|
||||
this.wetNode.disconnect()
|
||||
this.dryNode.disconnect()
|
||||
|
||||
@ -18,8 +18,8 @@ export class PlaybackManager {
|
||||
this.player = new AudioPlayer(options)
|
||||
}
|
||||
|
||||
updateOptions(options: Partial<PlaybackOptions>): void {
|
||||
this.player.updateOptions(options)
|
||||
async updateOptions(options: Partial<PlaybackOptions>): Promise<void> {
|
||||
await this.player.updateOptions(options)
|
||||
this.currentBuffer = null
|
||||
}
|
||||
|
||||
@ -31,7 +31,7 @@ export class PlaybackManager {
|
||||
this.player.setPitch(pitch)
|
||||
}
|
||||
|
||||
play(formula: string, sampleRate: number, duration: number): boolean {
|
||||
async play(formula: string, sampleRate: number, duration: number): Promise<boolean> {
|
||||
const result = compileFormula(formula)
|
||||
|
||||
if (!result.success || !result.compiledFormula) {
|
||||
@ -43,7 +43,7 @@ export class PlaybackManager {
|
||||
this.currentBuffer = generateSamples(result.compiledFormula, { sampleRate, duration })
|
||||
this.currentFormula = formula
|
||||
this.player.setLooping(true)
|
||||
this.player.play(this.currentBuffer)
|
||||
await this.player.play(this.currentBuffer)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Failed to generate samples:', error)
|
||||
|
||||
Reference in New Issue
Block a user