reorganization
This commit is contained in:
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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user