Adding new FM synthesis mode

This commit is contained in:
2025-10-06 13:08:59 +02:00
parent 0110a9760b
commit 324cf9d2ed
13 changed files with 1233 additions and 69 deletions

View File

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

View 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()
}
}