love it
This commit is contained in:
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "bytesample",
|
||||
"name": "bruitiste",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
|
||||
@ -45,6 +45,7 @@ function App() {
|
||||
|
||||
playbackManagerRef.current.stop()
|
||||
playbackManagerRef.current.setEffects(effectValues)
|
||||
playbackManagerRef.current.setPitch(engineValues.pitch ?? 1)
|
||||
|
||||
const success = playbackManagerRef.current.play(formula, sampleRate, duration)
|
||||
|
||||
@ -112,6 +113,10 @@ function App() {
|
||||
if (parameterId === 'masterVolume' && playbackManagerRef.current) {
|
||||
playbackManagerRef.current.setEffects(effectValues)
|
||||
}
|
||||
|
||||
if (parameterId === 'pitch' && playbackManagerRef.current) {
|
||||
playbackManagerRef.current.setPitch(value)
|
||||
}
|
||||
}
|
||||
|
||||
const handleEffectChange = (parameterId: string, value: number | boolean) => {
|
||||
|
||||
@ -49,6 +49,15 @@ export const ENGINE_CONTROLS: EffectConfig[] = [
|
||||
default: 75,
|
||||
step: 1,
|
||||
unit: '%'
|
||||
},
|
||||
{
|
||||
id: 'pitch',
|
||||
label: 'Pitch',
|
||||
min: 0.1,
|
||||
max: 4,
|
||||
default: 1,
|
||||
step: 0.01,
|
||||
unit: 'x'
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -113,8 +122,8 @@ export const EFFECTS: EffectConfig[] = [
|
||||
{
|
||||
id: 'delayTime',
|
||||
label: 'Time',
|
||||
min: 0,
|
||||
max: 1000,
|
||||
min: 10,
|
||||
max: 2000,
|
||||
default: 250,
|
||||
step: 10,
|
||||
unit: 'ms'
|
||||
@ -127,6 +136,42 @@ export const EFFECTS: EffectConfig[] = [
|
||||
default: 50,
|
||||
step: 1,
|
||||
unit: '%'
|
||||
},
|
||||
{
|
||||
id: 'delayWetDry',
|
||||
label: 'Mix',
|
||||
min: 0,
|
||||
max: 100,
|
||||
default: 50,
|
||||
step: 1,
|
||||
unit: '%'
|
||||
},
|
||||
{
|
||||
id: 'delayTone',
|
||||
label: 'Tone',
|
||||
min: 0,
|
||||
max: 100,
|
||||
default: 70,
|
||||
step: 1,
|
||||
unit: '%'
|
||||
},
|
||||
{
|
||||
id: 'delaySaturation',
|
||||
label: 'Saturation',
|
||||
min: 0,
|
||||
max: 100,
|
||||
default: 20,
|
||||
step: 1,
|
||||
unit: '%'
|
||||
},
|
||||
{
|
||||
id: 'delayFlutter',
|
||||
label: 'Flutter',
|
||||
min: 0,
|
||||
max: 100,
|
||||
default: 15,
|
||||
step: 1,
|
||||
unit: '%'
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -143,6 +188,42 @@ export const EFFECTS: EffectConfig[] = [
|
||||
default: 0,
|
||||
step: 1,
|
||||
unit: '%'
|
||||
},
|
||||
{
|
||||
id: 'reverbDecay',
|
||||
label: 'Decay',
|
||||
min: 0.1,
|
||||
max: 5,
|
||||
default: 2,
|
||||
step: 0.1,
|
||||
unit: 's'
|
||||
},
|
||||
{
|
||||
id: 'reverbDamping',
|
||||
label: 'Damping',
|
||||
min: 0,
|
||||
max: 100,
|
||||
default: 50,
|
||||
step: 1,
|
||||
unit: '%'
|
||||
},
|
||||
{
|
||||
id: 'reverbPanRate',
|
||||
label: 'Pan Rate',
|
||||
min: 0,
|
||||
max: 10,
|
||||
default: 0,
|
||||
step: 0.1,
|
||||
unit: 'Hz'
|
||||
},
|
||||
{
|
||||
id: 'reverbPanWidth',
|
||||
label: 'Pan Width',
|
||||
min: 0,
|
||||
max: 100,
|
||||
default: 50,
|
||||
step: 1,
|
||||
unit: '%'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -16,6 +16,7 @@ export class AudioPlayer {
|
||||
private isLooping: boolean = true
|
||||
private sampleRate: number
|
||||
private duration: number
|
||||
private pitch: number = 1
|
||||
|
||||
constructor(options: AudioPlayerOptions) {
|
||||
this.sampleRate = options.sampleRate
|
||||
@ -38,6 +39,17 @@ export class AudioPlayer {
|
||||
}
|
||||
}
|
||||
|
||||
setPitch(pitch: number): void {
|
||||
this.pitch = pitch
|
||||
if (this.sourceNode && this.audioContext) {
|
||||
this.sourceNode.playbackRate.setTargetAtTime(
|
||||
pitch,
|
||||
this.audioContext.currentTime,
|
||||
0.015
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
play(buffer: Float32Array, onEnded?: () => void): void {
|
||||
if (!this.audioContext) {
|
||||
this.audioContext = new AudioContext({ sampleRate: this.sampleRate })
|
||||
@ -58,6 +70,7 @@ export class AudioPlayer {
|
||||
this.sourceNode = this.audioContext.createBufferSource()
|
||||
this.sourceNode.buffer = audioBuffer
|
||||
this.sourceNode.loop = this.isLooping
|
||||
this.sourceNode.playbackRate.value = this.pitch
|
||||
|
||||
if (onEnded) {
|
||||
this.sourceNode.onended = onEnded
|
||||
|
||||
@ -3,37 +3,76 @@ import type { Effect } from './Effect.interface'
|
||||
export class DelayEffect implements Effect {
|
||||
readonly id = 'delay'
|
||||
|
||||
private audioContext: AudioContext
|
||||
private inputNode: GainNode
|
||||
private outputNode: GainNode
|
||||
private delayNode: DelayNode
|
||||
private feedbackNode: GainNode
|
||||
private wetNode: GainNode
|
||||
private dryNode: GainNode
|
||||
private filterNode: BiquadFilterNode
|
||||
private saturatorNode: WaveShaperNode
|
||||
private lfoNode: OscillatorNode
|
||||
private lfoGainNode: GainNode
|
||||
private bypassed: boolean = false
|
||||
private currentWetValue: number = 0
|
||||
private currentDryValue: number = 1
|
||||
private currentWetValue: number = 0.5
|
||||
private currentDryValue: number = 0.5
|
||||
|
||||
constructor(audioContext: AudioContext) {
|
||||
this.audioContext = audioContext
|
||||
this.inputNode = audioContext.createGain()
|
||||
this.outputNode = audioContext.createGain()
|
||||
this.delayNode = audioContext.createDelay(2.0)
|
||||
this.feedbackNode = audioContext.createGain()
|
||||
this.wetNode = audioContext.createGain()
|
||||
this.dryNode = audioContext.createGain()
|
||||
this.filterNode = audioContext.createBiquadFilter()
|
||||
this.saturatorNode = audioContext.createWaveShaper()
|
||||
|
||||
this.dryNode.gain.value = 1
|
||||
this.wetNode.gain.value = 0
|
||||
this.delayNode.delayTime.value = 0.25
|
||||
this.dryNode.gain.value = 0.5
|
||||
this.wetNode.gain.value = 0.5
|
||||
this.feedbackNode.gain.value = 0.5
|
||||
|
||||
this.filterNode.type = 'lowpass'
|
||||
this.filterNode.frequency.value = 5000
|
||||
this.filterNode.Q.value = 0.7
|
||||
|
||||
this.createSaturationCurve(0.2)
|
||||
|
||||
this.lfoNode = audioContext.createOscillator()
|
||||
this.lfoGainNode = audioContext.createGain()
|
||||
this.lfoNode.frequency.value = 2.5
|
||||
this.lfoGainNode.gain.value = 0.0015
|
||||
this.lfoNode.connect(this.lfoGainNode)
|
||||
this.lfoGainNode.connect(this.delayNode.delayTime)
|
||||
this.lfoNode.start()
|
||||
|
||||
this.inputNode.connect(this.dryNode)
|
||||
this.inputNode.connect(this.delayNode)
|
||||
this.delayNode.connect(this.feedbackNode)
|
||||
this.delayNode.connect(this.saturatorNode)
|
||||
this.saturatorNode.connect(this.filterNode)
|
||||
this.filterNode.connect(this.feedbackNode)
|
||||
this.feedbackNode.connect(this.delayNode)
|
||||
this.delayNode.connect(this.wetNode)
|
||||
this.dryNode.connect(this.outputNode)
|
||||
this.wetNode.connect(this.outputNode)
|
||||
}
|
||||
|
||||
private createSaturationCurve(amount: number): void {
|
||||
const samples = 2048
|
||||
const curve = new Float32Array(samples)
|
||||
const drive = 1 + amount * 9
|
||||
|
||||
for (let i = 0; i < samples; i++) {
|
||||
const x = (i * 2) / samples - 1
|
||||
curve[i] = Math.tanh(x * drive) / Math.tanh(drive)
|
||||
}
|
||||
|
||||
this.saturatorNode.curve = curve
|
||||
this.saturatorNode.oversample = '2x'
|
||||
}
|
||||
|
||||
getInputNode(): AudioNode {
|
||||
return this.inputNode
|
||||
}
|
||||
@ -55,28 +94,80 @@ export class DelayEffect implements Effect {
|
||||
|
||||
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.currentWetValue = delayAmount
|
||||
this.currentDryValue = 1 - delayAmount
|
||||
|
||||
if (!this.bypassed) {
|
||||
this.wetNode.gain.value = this.currentWetValue
|
||||
this.dryNode.gain.value = this.currentDryValue
|
||||
}
|
||||
const time = values.delayTime / 1000
|
||||
this.delayNode.delayTime.setTargetAtTime(
|
||||
time,
|
||||
this.audioContext.currentTime,
|
||||
0.01
|
||||
)
|
||||
}
|
||||
|
||||
if (values.delayFeedback !== undefined) {
|
||||
this.feedbackNode.gain.value = values.delayFeedback / 100
|
||||
const feedback = values.delayFeedback / 100
|
||||
this.feedbackNode.gain.setTargetAtTime(
|
||||
feedback * 0.95,
|
||||
this.audioContext.currentTime,
|
||||
0.01
|
||||
)
|
||||
}
|
||||
|
||||
if (values.delayWetDry !== undefined) {
|
||||
const wet = values.delayWetDry / 100
|
||||
this.currentWetValue = wet
|
||||
this.currentDryValue = 1 - wet
|
||||
|
||||
if (!this.bypassed) {
|
||||
this.wetNode.gain.setTargetAtTime(
|
||||
this.currentWetValue,
|
||||
this.audioContext.currentTime,
|
||||
0.01
|
||||
)
|
||||
this.dryNode.gain.setTargetAtTime(
|
||||
this.currentDryValue,
|
||||
this.audioContext.currentTime,
|
||||
0.01
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (values.delayTone !== undefined) {
|
||||
const tone = values.delayTone / 100
|
||||
const freq = 200 + tone * 7800
|
||||
this.filterNode.frequency.setTargetAtTime(
|
||||
freq,
|
||||
this.audioContext.currentTime,
|
||||
0.01
|
||||
)
|
||||
}
|
||||
|
||||
if (values.delaySaturation !== undefined) {
|
||||
const saturation = values.delaySaturation / 100
|
||||
this.createSaturationCurve(saturation)
|
||||
}
|
||||
|
||||
if (values.delayFlutter !== undefined) {
|
||||
const flutter = values.delayFlutter / 100
|
||||
const baseDelay = this.delayNode.delayTime.value
|
||||
const modDepth = baseDelay * flutter * 0.1
|
||||
this.lfoGainNode.gain.setTargetAtTime(
|
||||
modDepth,
|
||||
this.audioContext.currentTime,
|
||||
0.01
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.lfoNode.stop()
|
||||
this.lfoNode.disconnect()
|
||||
this.lfoGainNode.disconnect()
|
||||
this.inputNode.disconnect()
|
||||
this.outputNode.disconnect()
|
||||
this.delayNode.disconnect()
|
||||
this.feedbackNode.disconnect()
|
||||
this.wetNode.disconnect()
|
||||
this.dryNode.disconnect()
|
||||
this.filterNode.disconnect()
|
||||
this.saturatorNode.disconnect()
|
||||
}
|
||||
}
|
||||
@ -6,45 +6,120 @@ 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 pannerNode: StereoPannerNode
|
||||
private panLfoNode: OscillatorNode
|
||||
private panLfoGainNode: GainNode
|
||||
private bypassed: boolean = false
|
||||
private currentWetValue: number = 0
|
||||
private currentDryValue: number = 1
|
||||
private currentDecay: number = 2
|
||||
private currentDamping: number = 50
|
||||
|
||||
constructor(audioContext: AudioContext) {
|
||||
this.audioContext = audioContext
|
||||
this.inputNode = audioContext.createGain()
|
||||
this.outputNode = audioContext.createGain()
|
||||
this.finalOutputNode = audioContext.createGain()
|
||||
this.convolverNode = audioContext.createConvolver()
|
||||
this.wetNode = audioContext.createGain()
|
||||
this.dryNode = audioContext.createGain()
|
||||
this.pannerNode = audioContext.createStereoPanner()
|
||||
this.panLfoNode = audioContext.createOscillator()
|
||||
this.panLfoGainNode = audioContext.createGain()
|
||||
|
||||
this.wetNode.gain.value = 0
|
||||
this.dryNode.gain.value = 1
|
||||
|
||||
this.panLfoNode.frequency.value = 0
|
||||
this.panLfoGainNode.gain.value = 0
|
||||
this.panLfoNode.connect(this.panLfoGainNode)
|
||||
this.panLfoGainNode.connect(this.pannerNode.pan)
|
||||
this.panLfoNode.start()
|
||||
|
||||
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.generateImpulseResponse()
|
||||
this.generateReverb(this.currentDecay, this.currentDamping)
|
||||
}
|
||||
|
||||
private generateImpulseResponse(): void {
|
||||
const length = this.audioContext.sampleRate * 2
|
||||
const impulse = this.audioContext.createBuffer(2, length, this.audioContext.sampleRate)
|
||||
const left = impulse.getChannelData(0)
|
||||
const right = impulse.getChannelData(1)
|
||||
private generateReverb(decayTime: number, damping: number): void {
|
||||
const sampleRate = this.audioContext.sampleRate
|
||||
const numChannels = 2
|
||||
const totalTime = decayTime * 1.5
|
||||
const decaySampleFrames = Math.round(decayTime * sampleRate)
|
||||
const numSampleFrames = Math.round(totalTime * sampleRate)
|
||||
const fadeInTime = 0.05
|
||||
const fadeInSampleFrames = Math.round(fadeInTime * sampleRate)
|
||||
const decayBase = Math.pow(1 / 1000, 1 / decaySampleFrames)
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
left[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / length, 2)
|
||||
right[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / length, 2)
|
||||
const reverbIR = this.audioContext.createBuffer(numChannels, numSampleFrames, sampleRate)
|
||||
|
||||
for (let i = 0; i < numChannels; i++) {
|
||||
const chan = reverbIR.getChannelData(i)
|
||||
for (let j = 0; j < numSampleFrames; j++) {
|
||||
chan[j] = (Math.random() * 2 - 1) * Math.pow(decayBase, j)
|
||||
}
|
||||
for (let j = 0; j < fadeInSampleFrames; j++) {
|
||||
chan[j] *= j / fadeInSampleFrames
|
||||
}
|
||||
}
|
||||
|
||||
this.convolverNode.buffer = impulse
|
||||
const lpFreqStart = 10000
|
||||
const lpFreqEnd = 200 + (damping / 100) * 7800
|
||||
|
||||
this.applyGradualLowpass(reverbIR, lpFreqStart, lpFreqEnd, decayTime, (buffer) => {
|
||||
this.convolverNode.buffer = buffer
|
||||
})
|
||||
}
|
||||
|
||||
private applyGradualLowpass(
|
||||
input: AudioBuffer,
|
||||
lpFreqStart: number,
|
||||
lpFreqEnd: number,
|
||||
lpFreqEndAt: number,
|
||||
callback: (buffer: AudioBuffer) => void
|
||||
): void {
|
||||
if (lpFreqStart === 0) {
|
||||
callback(input)
|
||||
return
|
||||
}
|
||||
|
||||
const context = new OfflineAudioContext(
|
||||
input.numberOfChannels,
|
||||
input.length,
|
||||
input.sampleRate
|
||||
)
|
||||
|
||||
const player = context.createBufferSource()
|
||||
player.buffer = input
|
||||
|
||||
const filter = context.createBiquadFilter()
|
||||
lpFreqStart = Math.min(lpFreqStart, input.sampleRate / 2)
|
||||
lpFreqEnd = Math.min(lpFreqEnd, input.sampleRate / 2)
|
||||
|
||||
filter.type = 'lowpass'
|
||||
filter.Q.value = 0.0001
|
||||
filter.frequency.setValueAtTime(lpFreqStart, 0)
|
||||
filter.frequency.linearRampToValueAtTime(lpFreqEnd, lpFreqEndAt)
|
||||
|
||||
player.connect(filter)
|
||||
filter.connect(context.destination)
|
||||
player.start()
|
||||
|
||||
context.oncomplete = (event) => {
|
||||
callback(event.renderedBuffer)
|
||||
}
|
||||
|
||||
context.startRendering()
|
||||
}
|
||||
|
||||
getInputNode(): AudioNode {
|
||||
@ -52,7 +127,7 @@ export class ReverbEffect implements Effect {
|
||||
}
|
||||
|
||||
getOutputNode(): AudioNode {
|
||||
return this.outputNode
|
||||
return this.finalOutputNode
|
||||
}
|
||||
|
||||
setBypass(bypass: boolean): void {
|
||||
@ -67,6 +142,18 @@ export class ReverbEffect implements Effect {
|
||||
}
|
||||
|
||||
updateParams(values: Record<string, number>): void {
|
||||
let needsRegeneration = false
|
||||
|
||||
if (values.reverbDecay !== undefined && values.reverbDecay !== this.currentDecay) {
|
||||
this.currentDecay = values.reverbDecay
|
||||
needsRegeneration = true
|
||||
}
|
||||
|
||||
if (values.reverbDamping !== undefined && values.reverbDamping !== this.currentDamping) {
|
||||
this.currentDamping = values.reverbDamping
|
||||
needsRegeneration = true
|
||||
}
|
||||
|
||||
if (values.reverbWetDry !== undefined) {
|
||||
const wet = values.reverbWetDry / 100
|
||||
this.currentWetValue = wet
|
||||
@ -77,13 +164,40 @@ export class ReverbEffect implements Effect {
|
||||
this.dryNode.gain.value = this.currentDryValue
|
||||
}
|
||||
}
|
||||
|
||||
if (values.reverbPanRate !== undefined) {
|
||||
const rate = values.reverbPanRate
|
||||
this.panLfoNode.frequency.setTargetAtTime(
|
||||
rate,
|
||||
this.audioContext.currentTime,
|
||||
0.01
|
||||
)
|
||||
}
|
||||
|
||||
if (values.reverbPanWidth !== undefined) {
|
||||
const width = values.reverbPanWidth / 100
|
||||
this.panLfoGainNode.gain.setTargetAtTime(
|
||||
width,
|
||||
this.audioContext.currentTime,
|
||||
0.01
|
||||
)
|
||||
}
|
||||
|
||||
if (needsRegeneration) {
|
||||
this.generateReverb(this.currentDecay, this.currentDamping)
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.panLfoNode.stop()
|
||||
this.panLfoNode.disconnect()
|
||||
this.panLfoGainNode.disconnect()
|
||||
this.inputNode.disconnect()
|
||||
this.outputNode.disconnect()
|
||||
this.finalOutputNode.disconnect()
|
||||
this.convolverNode.disconnect()
|
||||
this.wetNode.disconnect()
|
||||
this.dryNode.disconnect()
|
||||
this.pannerNode.disconnect()
|
||||
}
|
||||
}
|
||||
@ -27,6 +27,10 @@ export class PlaybackManager {
|
||||
this.player.setEffects(values)
|
||||
}
|
||||
|
||||
setPitch(pitch: number): void {
|
||||
this.player.setPitch(pitch)
|
||||
}
|
||||
|
||||
play(formula: string, sampleRate: number, duration: number): boolean {
|
||||
const result = compileFormula(formula)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user