Fixing more bugs

This commit is contained in:
2025-09-30 22:21:53 +02:00
parent d1ff3daae1
commit 79ad69275f
10 changed files with 398 additions and 201 deletions

View File

@ -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 {

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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()