Adding new FM synthesis mode
This commit is contained in:
@ -1,8 +1,11 @@
|
||||
import { EffectsChain } from './effects/EffectsChain'
|
||||
import { BytebeatSourceEffect } from './effects/BytebeatSourceEffect'
|
||||
import { FMSourceEffect } from './effects/FMSourceEffect'
|
||||
import { ModulationEngine } from '../modulation/ModulationEngine'
|
||||
import type { LFOWaveform } from '../modulation/LFO'
|
||||
import type { EffectValues } from '../../types/effects'
|
||||
import type { SynthesisMode } from '../../stores/synthesisMode'
|
||||
import { getAlgorithmById } from '../../config/fmAlgorithms'
|
||||
|
||||
export interface AudioPlayerOptions {
|
||||
sampleRate: number
|
||||
@ -12,6 +15,7 @@ export interface AudioPlayerOptions {
|
||||
export class AudioPlayer {
|
||||
private audioContext: AudioContext | null = null
|
||||
private bytebeatSource: BytebeatSourceEffect | null = null
|
||||
private fmSource: FMSourceEffect | null = null
|
||||
private effectsChain: EffectsChain | null = null
|
||||
private modulationEngine: ModulationEngine | null = null
|
||||
private effectValues: EffectValues = {}
|
||||
@ -20,6 +24,9 @@ export class AudioPlayer {
|
||||
private duration: number
|
||||
private workletRegistered: boolean = false
|
||||
private currentPitch: number = 1.0
|
||||
private currentMode: SynthesisMode = 'bytebeat'
|
||||
private currentAlgorithm: number = 0
|
||||
private currentFeedback: number = 0
|
||||
|
||||
constructor(options: AudioPlayerOptions) {
|
||||
this.sampleRate = options.sampleRate
|
||||
@ -66,6 +73,7 @@ export class AudioPlayer {
|
||||
context.audioWorklet.addModule('/worklets/svf-processor.js'),
|
||||
context.audioWorklet.addModule('/worklets/fold-crush-processor.js'),
|
||||
context.audioWorklet.addModule('/worklets/bytebeat-processor.js'),
|
||||
context.audioWorklet.addModule('/worklets/fm-processor.js'),
|
||||
context.audioWorklet.addModule('/worklets/output-limiter.js')
|
||||
])
|
||||
this.workletRegistered = true
|
||||
@ -141,21 +149,61 @@ export class AudioPlayer {
|
||||
}
|
||||
}
|
||||
|
||||
setMode(mode: SynthesisMode): void {
|
||||
this.currentMode = mode
|
||||
}
|
||||
|
||||
setAlgorithm(algorithmId: number, lfoRates?: number[]): void {
|
||||
this.currentAlgorithm = algorithmId
|
||||
if (this.fmSource) {
|
||||
const algorithm = getAlgorithmById(algorithmId)
|
||||
this.fmSource.setAlgorithm(algorithm, lfoRates)
|
||||
}
|
||||
}
|
||||
|
||||
setFeedback(feedback: number): void {
|
||||
this.currentFeedback = feedback
|
||||
if (this.fmSource) {
|
||||
this.fmSource.setFeedback(feedback)
|
||||
}
|
||||
}
|
||||
|
||||
async playRealtime(formula: string, a: number, b: number, c: number, d: number): Promise<void> {
|
||||
await this.ensureAudioContext()
|
||||
|
||||
if (!this.bytebeatSource) {
|
||||
this.bytebeatSource = new BytebeatSourceEffect(this.audioContext!)
|
||||
await this.bytebeatSource.initialize(this.audioContext!)
|
||||
if (this.currentMode === 'bytebeat') {
|
||||
if (!this.bytebeatSource) {
|
||||
this.bytebeatSource = new BytebeatSourceEffect(this.audioContext!)
|
||||
await this.bytebeatSource.initialize(this.audioContext!)
|
||||
}
|
||||
|
||||
this.bytebeatSource.setLoopLength(this.sampleRate, this.duration)
|
||||
this.bytebeatSource.setFormula(formula)
|
||||
this.bytebeatSource.setVariables(a, b, c, d)
|
||||
this.bytebeatSource.setPlaybackRate(this.currentPitch)
|
||||
this.bytebeatSource.reset()
|
||||
|
||||
this.bytebeatSource.getOutputNode().connect(this.effectsChain!.getInputNode())
|
||||
} else {
|
||||
if (!this.fmSource) {
|
||||
this.fmSource = new FMSourceEffect(this.audioContext!)
|
||||
await this.fmSource.initialize(this.audioContext!)
|
||||
}
|
||||
|
||||
this.fmSource.setLoopLength(this.sampleRate, this.duration)
|
||||
const algorithm = getAlgorithmById(this.currentAlgorithm)
|
||||
const patch = formula ? JSON.parse(formula) : null
|
||||
const lfoRates = patch?.lfoRates || undefined
|
||||
this.fmSource.setAlgorithm(algorithm, lfoRates)
|
||||
this.fmSource.setOperatorLevels(a, b, c, d)
|
||||
this.fmSource.setBaseFrequency(220 * this.currentPitch)
|
||||
this.fmSource.setFeedback(this.currentFeedback)
|
||||
this.fmSource.setPlaybackRate(1.0)
|
||||
this.fmSource.reset()
|
||||
|
||||
this.fmSource.getOutputNode().connect(this.effectsChain!.getInputNode())
|
||||
}
|
||||
|
||||
this.bytebeatSource.setLoopLength(this.sampleRate, this.duration)
|
||||
this.bytebeatSource.setFormula(formula)
|
||||
this.bytebeatSource.setVariables(a, b, c, d)
|
||||
this.bytebeatSource.setPlaybackRate(this.currentPitch)
|
||||
this.bytebeatSource.reset()
|
||||
|
||||
this.bytebeatSource.getOutputNode().connect(this.effectsChain!.getInputNode())
|
||||
this.effectsChain!.getOutputNode().connect(this.audioContext!.destination)
|
||||
|
||||
if (this.modulationEngine) {
|
||||
@ -166,14 +214,18 @@ export class AudioPlayer {
|
||||
}
|
||||
|
||||
updateRealtimeVariables(a: number, b: number, c: number, d: number): void {
|
||||
if (this.bytebeatSource) {
|
||||
if (this.currentMode === 'bytebeat' && this.bytebeatSource) {
|
||||
this.bytebeatSource.setVariables(a, b, c, d)
|
||||
} else if (this.currentMode === 'fm' && this.fmSource) {
|
||||
this.fmSource.setOperatorLevels(a, b, c, d)
|
||||
}
|
||||
}
|
||||
|
||||
private applyPitch(pitch: number): void {
|
||||
if (this.bytebeatSource) {
|
||||
if (this.currentMode === 'bytebeat' && this.bytebeatSource) {
|
||||
this.bytebeatSource.setPlaybackRate(pitch)
|
||||
} else if (this.currentMode === 'fm' && this.fmSource) {
|
||||
this.fmSource.setBaseFrequency(220 * pitch)
|
||||
}
|
||||
}
|
||||
|
||||
@ -197,6 +249,9 @@ export class AudioPlayer {
|
||||
if (this.bytebeatSource) {
|
||||
this.bytebeatSource.getOutputNode().disconnect()
|
||||
}
|
||||
if (this.fmSource) {
|
||||
this.fmSource.getOutputNode().disconnect()
|
||||
}
|
||||
if (this.modulationEngine) {
|
||||
this.modulationEngine.stop()
|
||||
}
|
||||
@ -209,6 +264,10 @@ export class AudioPlayer {
|
||||
this.bytebeatSource.dispose()
|
||||
this.bytebeatSource = null
|
||||
}
|
||||
if (this.fmSource) {
|
||||
this.fmSource.dispose()
|
||||
this.fmSource = null
|
||||
}
|
||||
if (this.modulationEngine) {
|
||||
this.modulationEngine.dispose()
|
||||
this.modulationEngine = null
|
||||
|
||||
90
src/domain/audio/effects/FMSourceEffect.ts
Normal file
90
src/domain/audio/effects/FMSourceEffect.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import type { Effect } from './Effect.interface'
|
||||
import type { FMAlgorithm } from '../../../config/fmAlgorithms'
|
||||
|
||||
export class FMSourceEffect implements Effect {
|
||||
readonly id = 'fm-source'
|
||||
|
||||
private inputNode: GainNode
|
||||
private outputNode: GainNode
|
||||
private processorNode: AudioWorkletNode | null = null
|
||||
|
||||
constructor(audioContext: AudioContext) {
|
||||
this.inputNode = audioContext.createGain()
|
||||
this.outputNode = audioContext.createGain()
|
||||
}
|
||||
|
||||
async initialize(audioContext: AudioContext): Promise<void> {
|
||||
this.processorNode = new AudioWorkletNode(audioContext, 'fm-processor')
|
||||
this.processorNode.connect(this.outputNode)
|
||||
}
|
||||
|
||||
getInputNode(): AudioNode {
|
||||
return this.inputNode
|
||||
}
|
||||
|
||||
getOutputNode(): AudioNode {
|
||||
return this.outputNode
|
||||
}
|
||||
|
||||
setBypass(): void {
|
||||
// Source node doesn't support bypass
|
||||
}
|
||||
|
||||
updateParams(): void {
|
||||
// Parameters handled via specific methods
|
||||
}
|
||||
|
||||
setAlgorithm(algorithm: FMAlgorithm, lfoRates?: number[]): void {
|
||||
if (!this.processorNode) return
|
||||
this.processorNode.port.postMessage({
|
||||
type: 'algorithm',
|
||||
value: {
|
||||
id: algorithm.id,
|
||||
ratios: algorithm.frequencyRatios,
|
||||
lfoRates: lfoRates || [0.37, 0.53, 0.71, 0.43]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
setOperatorLevels(a: number, b: number, c: number, d: number): void {
|
||||
if (!this.processorNode) return
|
||||
this.processorNode.port.postMessage({
|
||||
type: 'operatorLevels',
|
||||
value: { a, b, c, d }
|
||||
})
|
||||
}
|
||||
|
||||
setBaseFrequency(freq: number): void {
|
||||
if (!this.processorNode) return
|
||||
this.processorNode.port.postMessage({ type: 'baseFreq', value: freq })
|
||||
}
|
||||
|
||||
setFeedback(amount: number): void {
|
||||
if (!this.processorNode) return
|
||||
this.processorNode.port.postMessage({ type: 'feedback', value: amount })
|
||||
}
|
||||
|
||||
setLoopLength(sampleRate: number, duration: number): void {
|
||||
if (!this.processorNode) return
|
||||
const loopLength = sampleRate * duration
|
||||
this.processorNode.port.postMessage({ type: 'loopLength', value: loopLength })
|
||||
}
|
||||
|
||||
setPlaybackRate(rate: number): void {
|
||||
if (!this.processorNode) return
|
||||
this.processorNode.port.postMessage({ type: 'playbackRate', value: rate })
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
if (!this.processorNode) return
|
||||
this.processorNode.port.postMessage({ type: 'reset' })
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.processorNode) {
|
||||
this.processorNode.disconnect()
|
||||
}
|
||||
this.inputNode.disconnect()
|
||||
this.outputNode.disconnect()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user