Adding new FM synthesis mode
This commit is contained in:
391
public/worklets/fm-processor.js
Normal file
391
public/worklets/fm-processor.js
Normal file
@ -0,0 +1,391 @@
|
|||||||
|
class FMProcessor extends AudioWorkletProcessor {
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.phase1 = 0
|
||||||
|
this.phase2 = 0
|
||||||
|
this.phase3 = 0
|
||||||
|
this.phase4 = 0
|
||||||
|
this.baseFreq = 220
|
||||||
|
this.algorithm = 0
|
||||||
|
this.opLevel1 = 128
|
||||||
|
this.opLevel2 = 128
|
||||||
|
this.opLevel3 = 128
|
||||||
|
this.opLevel4 = 128
|
||||||
|
this.feedback = 0
|
||||||
|
this.playbackRate = 1.0
|
||||||
|
this.loopLength = 0
|
||||||
|
this.sampleCount = 0
|
||||||
|
this.feedbackSample = 0
|
||||||
|
|
||||||
|
this.frequencyRatios = [1.0, 1.0, 1.0, 1.0]
|
||||||
|
|
||||||
|
this.lfoPhase1 = 0
|
||||||
|
this.lfoPhase2 = 0
|
||||||
|
this.lfoPhase3 = 0
|
||||||
|
this.lfoPhase4 = 0
|
||||||
|
this.lfoRate1 = 0.37
|
||||||
|
this.lfoRate2 = 0.53
|
||||||
|
this.lfoRate3 = 0.71
|
||||||
|
this.lfoRate4 = 0.43
|
||||||
|
this.lfoDepth = 0.3
|
||||||
|
|
||||||
|
this.port.onmessage = (event) => {
|
||||||
|
const { type, value } = event.data
|
||||||
|
switch (type) {
|
||||||
|
case 'algorithm':
|
||||||
|
this.algorithm = value.id
|
||||||
|
this.frequencyRatios = value.ratios
|
||||||
|
if (value.lfoRates) {
|
||||||
|
this.lfoRate1 = value.lfoRates[0]
|
||||||
|
this.lfoRate2 = value.lfoRates[1]
|
||||||
|
this.lfoRate3 = value.lfoRates[2]
|
||||||
|
this.lfoRate4 = value.lfoRates[3]
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'operatorLevels':
|
||||||
|
this.opLevel1 = value.a
|
||||||
|
this.opLevel2 = value.b
|
||||||
|
this.opLevel3 = value.c
|
||||||
|
this.opLevel4 = value.d
|
||||||
|
break
|
||||||
|
case 'baseFreq':
|
||||||
|
this.baseFreq = value
|
||||||
|
break
|
||||||
|
case 'feedback':
|
||||||
|
this.feedback = value / 100.0
|
||||||
|
break
|
||||||
|
case 'reset':
|
||||||
|
this.phase1 = 0
|
||||||
|
this.phase2 = 0
|
||||||
|
this.phase3 = 0
|
||||||
|
this.phase4 = 0
|
||||||
|
this.sampleCount = 0
|
||||||
|
this.feedbackSample = 0
|
||||||
|
break
|
||||||
|
case 'loopLength':
|
||||||
|
this.loopLength = value
|
||||||
|
break
|
||||||
|
case 'playbackRate':
|
||||||
|
this.playbackRate = value
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
synthesize(algorithm) {
|
||||||
|
const TWO_PI = Math.PI * 2
|
||||||
|
const sampleRate = 44100
|
||||||
|
|
||||||
|
const freq1 = (this.baseFreq * this.frequencyRatios[0] * TWO_PI) / sampleRate
|
||||||
|
const freq2 = (this.baseFreq * this.frequencyRatios[1] * TWO_PI) / sampleRate
|
||||||
|
const freq3 = (this.baseFreq * this.frequencyRatios[2] * TWO_PI) / sampleRate
|
||||||
|
const freq4 = (this.baseFreq * this.frequencyRatios[3] * TWO_PI) / sampleRate
|
||||||
|
|
||||||
|
const lfo1 = Math.sin(this.lfoPhase1)
|
||||||
|
const lfo2 = Math.sin(this.lfoPhase2)
|
||||||
|
const lfo3 = Math.sin(this.lfoPhase3)
|
||||||
|
const lfo4 = Math.sin(this.lfoPhase4)
|
||||||
|
|
||||||
|
const level1 = (this.opLevel1 / 255.0) * (1 + this.lfoDepth * lfo1)
|
||||||
|
const level2 = (this.opLevel2 / 255.0) * (1 + this.lfoDepth * lfo2)
|
||||||
|
const level3 = (this.opLevel3 / 255.0) * (1 + this.lfoDepth * lfo3)
|
||||||
|
const level4 = (this.opLevel4 / 255.0) * (1 + this.lfoDepth * lfo4)
|
||||||
|
|
||||||
|
this.lfoPhase1 += (this.lfoRate1 * TWO_PI) / sampleRate
|
||||||
|
this.lfoPhase2 += (this.lfoRate2 * TWO_PI) / sampleRate
|
||||||
|
this.lfoPhase3 += (this.lfoRate3 * TWO_PI) / sampleRate
|
||||||
|
this.lfoPhase4 += (this.lfoRate4 * TWO_PI) / sampleRate
|
||||||
|
|
||||||
|
if (this.lfoPhase1 > TWO_PI) this.lfoPhase1 -= TWO_PI
|
||||||
|
if (this.lfoPhase2 > TWO_PI) this.lfoPhase2 -= TWO_PI
|
||||||
|
if (this.lfoPhase3 > TWO_PI) this.lfoPhase3 -= TWO_PI
|
||||||
|
if (this.lfoPhase4 > TWO_PI) this.lfoPhase4 -= TWO_PI
|
||||||
|
|
||||||
|
let output = 0
|
||||||
|
|
||||||
|
switch (algorithm) {
|
||||||
|
case 0: {
|
||||||
|
const op1 = Math.sin(this.phase1) * level1
|
||||||
|
const op2 = Math.sin(this.phase2) * level2
|
||||||
|
const op3 = Math.sin(this.phase3) * level3
|
||||||
|
const op4 = Math.sin(this.phase4) * level4
|
||||||
|
output = (op1 + op2 + op3 + op4) * 0.25
|
||||||
|
this.phase1 += freq1 * this.playbackRate
|
||||||
|
this.phase2 += freq2 * this.playbackRate
|
||||||
|
this.phase3 += freq3 * this.playbackRate
|
||||||
|
this.phase4 += freq4 * this.playbackRate
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 1: {
|
||||||
|
const op1 = Math.sin(this.phase1) * level1
|
||||||
|
const mod1 = op1 * 10
|
||||||
|
const op2 = Math.sin(this.phase2 + mod1) * level2
|
||||||
|
const mod2 = op2 * 10
|
||||||
|
const op3 = Math.sin(this.phase3 + mod2) * level3
|
||||||
|
const mod3 = op3 * 10
|
||||||
|
const op4 = Math.sin(this.phase4 + mod3 + this.feedbackSample * this.feedback * 10) * level4
|
||||||
|
output = op4
|
||||||
|
this.feedbackSample = op4
|
||||||
|
this.phase1 += freq1 * this.playbackRate
|
||||||
|
this.phase2 += freq2 * this.playbackRate
|
||||||
|
this.phase3 += freq3 * this.playbackRate
|
||||||
|
this.phase4 += freq4 * this.playbackRate
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 2: {
|
||||||
|
const op1 = Math.sin(this.phase1) * level1
|
||||||
|
const mod1 = op1 * 10
|
||||||
|
const op2 = Math.sin(this.phase2 + mod1) * level2
|
||||||
|
const op3 = Math.sin(this.phase3) * level3
|
||||||
|
const mod3 = op3 * 10
|
||||||
|
const op4 = Math.sin(this.phase4 + mod3) * level4
|
||||||
|
output = (op2 + op4) * 0.5
|
||||||
|
this.phase1 += freq1 * this.playbackRate
|
||||||
|
this.phase2 += freq2 * this.playbackRate
|
||||||
|
this.phase3 += freq3 * this.playbackRate
|
||||||
|
this.phase4 += freq4 * this.playbackRate
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 3: {
|
||||||
|
const op1 = Math.sin(this.phase1) * level1
|
||||||
|
const mod1 = op1 * 10
|
||||||
|
const op2 = Math.sin(this.phase2 + mod1) * level2
|
||||||
|
const mod2 = op2 * 10
|
||||||
|
const op3 = Math.sin(this.phase3 + mod2) * level3
|
||||||
|
const op4 = Math.sin(this.phase4) * level4
|
||||||
|
output = (op3 + op4) * 0.5
|
||||||
|
this.phase1 += freq1 * this.playbackRate
|
||||||
|
this.phase2 += freq2 * this.playbackRate
|
||||||
|
this.phase3 += freq3 * this.playbackRate
|
||||||
|
this.phase4 += freq4 * this.playbackRate
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 4: {
|
||||||
|
const op1 = Math.sin(this.phase1) * level1
|
||||||
|
const mod1 = op1 * 10
|
||||||
|
const op2 = Math.sin(this.phase2) * level2
|
||||||
|
const mod2 = op2 * 10
|
||||||
|
const op3 = Math.sin(this.phase3 + mod1 + mod2) * level3
|
||||||
|
const mod3 = op3 * 10
|
||||||
|
const op4 = Math.sin(this.phase4 + mod3) * level4
|
||||||
|
output = op4
|
||||||
|
this.phase1 += freq1 * this.playbackRate
|
||||||
|
this.phase2 += freq2 * this.playbackRate
|
||||||
|
this.phase3 += freq3 * this.playbackRate
|
||||||
|
this.phase4 += freq4 * this.playbackRate
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 5: {
|
||||||
|
const op1 = Math.sin(this.phase1) * level1
|
||||||
|
const mod1 = op1 * 10
|
||||||
|
const op2 = Math.sin(this.phase2 + mod1) * level2
|
||||||
|
const mod2 = op2 * 10
|
||||||
|
const op3 = Math.sin(this.phase3 + mod1) * level3
|
||||||
|
const mod3 = op3 * 10
|
||||||
|
const op4 = Math.sin(this.phase4 + mod2 + mod3) * level4
|
||||||
|
output = op4
|
||||||
|
this.phase1 += freq1 * this.playbackRate
|
||||||
|
this.phase2 += freq2 * this.playbackRate
|
||||||
|
this.phase3 += freq3 * this.playbackRate
|
||||||
|
this.phase4 += freq4 * this.playbackRate
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 6: {
|
||||||
|
const op1 = Math.sin(this.phase1) * level1
|
||||||
|
const mod1 = op1 * 10
|
||||||
|
const op2 = Math.sin(this.phase2 + mod1) * level2
|
||||||
|
const op3 = Math.sin(this.phase3) * level3
|
||||||
|
const op4 = Math.sin(this.phase4) * level4
|
||||||
|
output = (op2 + op3 + op4) * 0.333
|
||||||
|
this.phase1 += freq1 * this.playbackRate
|
||||||
|
this.phase2 += freq2 * this.playbackRate
|
||||||
|
this.phase3 += freq3 * this.playbackRate
|
||||||
|
this.phase4 += freq4 * this.playbackRate
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 7: {
|
||||||
|
const op1 = Math.sin(this.phase1) * level1
|
||||||
|
const mod1 = op1 * 10
|
||||||
|
const op2 = Math.sin(this.phase2 + mod1) * level2
|
||||||
|
const mod2 = op2 * 1.5
|
||||||
|
const op3 = Math.sin(this.phase3 + mod1) * level3
|
||||||
|
const mod3 = op3 * 1.5
|
||||||
|
const op4 = Math.sin(this.phase4 + mod2 + mod3) * level4
|
||||||
|
output = op4
|
||||||
|
this.phase1 += freq1 * this.playbackRate
|
||||||
|
this.phase2 += freq2 * this.playbackRate
|
||||||
|
this.phase3 += freq3 * this.playbackRate
|
||||||
|
this.phase4 += freq4 * this.playbackRate
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 8: {
|
||||||
|
const op1 = Math.sin(this.phase1) * level1
|
||||||
|
const mod1 = op1 * 10
|
||||||
|
const op3 = Math.sin(this.phase3 + mod1) * level3
|
||||||
|
const op2 = Math.sin(this.phase2) * level2
|
||||||
|
const mod2 = op2 * 10
|
||||||
|
const op4 = Math.sin(this.phase4 + mod2) * level4
|
||||||
|
output = (op3 + op4) * 0.5
|
||||||
|
this.phase1 += freq1 * this.playbackRate
|
||||||
|
this.phase2 += freq2 * this.playbackRate
|
||||||
|
this.phase3 += freq3 * this.playbackRate
|
||||||
|
this.phase4 += freq4 * this.playbackRate
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 9: {
|
||||||
|
const op1 = Math.sin(this.phase1) * level1
|
||||||
|
const mod1 = op1 * 10
|
||||||
|
const op4 = Math.sin(this.phase4 + mod1) * level4
|
||||||
|
const op2 = Math.sin(this.phase2) * level2
|
||||||
|
const mod2 = op2 * 10
|
||||||
|
const op3 = Math.sin(this.phase3 + mod2) * level3
|
||||||
|
output = (op3 + op4) * 0.5
|
||||||
|
this.phase1 += freq1 * this.playbackRate
|
||||||
|
this.phase2 += freq2 * this.playbackRate
|
||||||
|
this.phase3 += freq3 * this.playbackRate
|
||||||
|
this.phase4 += freq4 * this.playbackRate
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 10: {
|
||||||
|
const op1 = Math.sin(this.phase1) * level1
|
||||||
|
const mod1 = op1 * 10
|
||||||
|
const op2 = Math.sin(this.phase2 + mod1) * level2
|
||||||
|
const op3 = Math.sin(this.phase3) * level3
|
||||||
|
const mod3 = op3 * 10
|
||||||
|
const op4 = Math.sin(this.phase4 + mod3) * level4
|
||||||
|
output = (op2 + op4) * 0.5
|
||||||
|
this.phase1 += freq1 * this.playbackRate
|
||||||
|
this.phase2 += freq2 * this.playbackRate
|
||||||
|
this.phase3 += freq3 * this.playbackRate
|
||||||
|
this.phase4 += freq4 * this.playbackRate
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 11: {
|
||||||
|
const op1 = Math.sin(this.phase1) * level1
|
||||||
|
const op2 = Math.sin(this.phase2) * level2
|
||||||
|
const mod2 = op2 * 10
|
||||||
|
const op3 = Math.sin(this.phase3 + mod2) * level3
|
||||||
|
const mod3 = op3 * 10
|
||||||
|
const op4 = Math.sin(this.phase4 + mod3) * level4
|
||||||
|
output = (op1 + op4) * 0.5
|
||||||
|
this.phase1 += freq1 * this.playbackRate
|
||||||
|
this.phase2 += freq2 * this.playbackRate
|
||||||
|
this.phase3 += freq3 * this.playbackRate
|
||||||
|
this.phase4 += freq4 * this.playbackRate
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 12: {
|
||||||
|
const op1 = Math.sin(this.phase1) * level1
|
||||||
|
const mod1 = op1 * 10
|
||||||
|
const op2 = Math.sin(this.phase2 + mod1) * level2
|
||||||
|
const mod2 = op2 * 10
|
||||||
|
const op4 = Math.sin(this.phase4 + mod2) * level4
|
||||||
|
const op3 = Math.sin(this.phase3) * level3
|
||||||
|
output = (op3 + op4) * 0.5
|
||||||
|
this.phase1 += freq1 * this.playbackRate
|
||||||
|
this.phase2 += freq2 * this.playbackRate
|
||||||
|
this.phase3 += freq3 * this.playbackRate
|
||||||
|
this.phase4 += freq4 * this.playbackRate
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 13: {
|
||||||
|
const op1 = Math.sin(this.phase1) * level1
|
||||||
|
const mod1 = op1 * 10
|
||||||
|
const op2 = Math.sin(this.phase2 + mod1) * level2
|
||||||
|
const mod2 = op2 * 10
|
||||||
|
const op3 = Math.sin(this.phase3 + mod1) * level3
|
||||||
|
const mod3 = op3 * 10
|
||||||
|
const op4 = Math.sin(this.phase4 + mod2 + mod3) * level4
|
||||||
|
output = op4
|
||||||
|
this.phase1 += freq1 * this.playbackRate
|
||||||
|
this.phase2 += freq2 * this.playbackRate
|
||||||
|
this.phase3 += freq3 * this.playbackRate
|
||||||
|
this.phase4 += freq4 * this.playbackRate
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 14: {
|
||||||
|
const op1 = Math.sin(this.phase1) * level1
|
||||||
|
const mod1 = op1 * 10
|
||||||
|
const op3 = Math.sin(this.phase3) * level3
|
||||||
|
const mod3 = op3 * 10
|
||||||
|
const op4 = Math.sin(this.phase4 + mod3) * level4
|
||||||
|
const mod4 = op4 * 1.5
|
||||||
|
const op2 = Math.sin(this.phase2 + mod1 + mod4) * level2
|
||||||
|
output = op2
|
||||||
|
this.phase1 += freq1 * this.playbackRate
|
||||||
|
this.phase2 += freq2 * this.playbackRate
|
||||||
|
this.phase3 += freq3 * this.playbackRate
|
||||||
|
this.phase4 += freq4 * this.playbackRate
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 15: {
|
||||||
|
const op1 = Math.sin(this.phase1) * level1
|
||||||
|
const mod1 = op1 * 10
|
||||||
|
const op2 = Math.sin(this.phase2) * level2
|
||||||
|
const mod2 = op2 * 10
|
||||||
|
const op3 = Math.sin(this.phase3) * level3
|
||||||
|
const mod3 = op3 * 10
|
||||||
|
const op4 = Math.sin(this.phase4 + mod1 + mod2 + mod3) * level4
|
||||||
|
output = op4
|
||||||
|
this.phase1 += freq1 * this.playbackRate
|
||||||
|
this.phase2 += freq2 * this.playbackRate
|
||||||
|
this.phase3 += freq3 * this.playbackRate
|
||||||
|
this.phase4 += freq4 * this.playbackRate
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
output = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const TWO_PI_LIMIT = TWO_PI * 10
|
||||||
|
if (this.phase1 > TWO_PI_LIMIT) this.phase1 -= TWO_PI_LIMIT
|
||||||
|
if (this.phase2 > TWO_PI_LIMIT) this.phase2 -= TWO_PI_LIMIT
|
||||||
|
if (this.phase3 > TWO_PI_LIMIT) this.phase3 -= TWO_PI_LIMIT
|
||||||
|
if (this.phase4 > TWO_PI_LIMIT) this.phase4 -= TWO_PI_LIMIT
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
process(inputs, outputs) {
|
||||||
|
const output = outputs[0]
|
||||||
|
|
||||||
|
if (output.length > 0) {
|
||||||
|
const outputChannel = output[0]
|
||||||
|
|
||||||
|
for (let i = 0; i < outputChannel.length; i++) {
|
||||||
|
outputChannel[i] = this.synthesize(this.algorithm)
|
||||||
|
|
||||||
|
this.sampleCount++
|
||||||
|
if (this.loopLength > 0 && this.sampleCount >= this.loopLength) {
|
||||||
|
this.phase1 = 0
|
||||||
|
this.phase2 = 0
|
||||||
|
this.phase3 = 0
|
||||||
|
this.phase4 = 0
|
||||||
|
this.sampleCount = 0
|
||||||
|
this.feedbackSample = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerProcessor('fm-processor', FMProcessor)
|
||||||
203
src/App.tsx
203
src/App.tsx
@ -4,6 +4,7 @@ import { Square, Archive, Dices, Sparkles } from 'lucide-react'
|
|||||||
import { PlaybackManager } from './services/PlaybackManager'
|
import { PlaybackManager } from './services/PlaybackManager'
|
||||||
import { DownloadService } from './services/DownloadService'
|
import { DownloadService } from './services/DownloadService'
|
||||||
import { generateTileGrid, generateRandomFormula } from './utils/bytebeatFormulas'
|
import { generateTileGrid, generateRandomFormula } from './utils/bytebeatFormulas'
|
||||||
|
import { generateFMTileGrid, generateRandomFMPatch, createFMTileState } from './utils/fmPatches'
|
||||||
import { BytebeatTile } from './components/BytebeatTile'
|
import { BytebeatTile } from './components/BytebeatTile'
|
||||||
import { EffectsBar } from './components/EffectsBar'
|
import { EffectsBar } from './components/EffectsBar'
|
||||||
import { EngineControls } from './components/EngineControls'
|
import { EngineControls } from './components/EngineControls'
|
||||||
@ -13,6 +14,7 @@ import { AudioContextWarning } from './components/AudioContextWarning'
|
|||||||
import { HelpModal } from './components/HelpModal'
|
import { HelpModal } from './components/HelpModal'
|
||||||
import { getSampleRateFromIndex } from './config/effects'
|
import { getSampleRateFromIndex } from './config/effects'
|
||||||
import { engineSettings, effectSettings, lfoSettings, type LFOConfig } from './stores/settings'
|
import { engineSettings, effectSettings, lfoSettings, type LFOConfig } from './stores/settings'
|
||||||
|
import { synthesisMode, setSynthesisMode } from './stores/synthesisMode'
|
||||||
import { exitMappingMode } from './stores/mappingMode'
|
import { exitMappingMode } from './stores/mappingMode'
|
||||||
import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts'
|
import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts'
|
||||||
import { useTileParams } from './hooks/useTileParams'
|
import { useTileParams } from './hooks/useTileParams'
|
||||||
@ -24,9 +26,12 @@ import { getTileId, getTileFromGrid, type FocusedTile } from './utils/tileHelper
|
|||||||
function App() {
|
function App() {
|
||||||
const engineValues = useStore(engineSettings)
|
const engineValues = useStore(engineSettings)
|
||||||
const effectValues = useStore(effectSettings)
|
const effectValues = useStore(effectSettings)
|
||||||
|
const mode = useStore(synthesisMode)
|
||||||
|
|
||||||
const [tiles, setTiles] = useState<TileState[][]>(() =>
|
const [tiles, setTiles] = useState<TileState[][]>(() =>
|
||||||
generateTileGrid(TILE_GRID.SIZE, TILE_GRID.COLUMNS, engineValues.complexity)
|
mode === 'fm'
|
||||||
|
? generateFMTileGrid(TILE_GRID.SIZE, TILE_GRID.COLUMNS, engineValues.complexity)
|
||||||
|
: generateTileGrid(TILE_GRID.SIZE, TILE_GRID.COLUMNS, engineValues.complexity)
|
||||||
)
|
)
|
||||||
const [playing, setPlaying] = useState<string | null>(null)
|
const [playing, setPlaying] = useState<string | null>(null)
|
||||||
const [queued, setQueued] = useState<string | null>(null)
|
const [queued, setQueued] = useState<string | null>(null)
|
||||||
@ -81,10 +86,32 @@ function App() {
|
|||||||
|
|
||||||
const handleRandom = () => {
|
const handleRandom = () => {
|
||||||
clearSwitchTimer()
|
clearSwitchTimer()
|
||||||
setTiles(generateTileGrid(TILE_GRID.SIZE, TILE_GRID.COLUMNS, engineValues.complexity))
|
if (mode === 'fm') {
|
||||||
|
setTiles(generateFMTileGrid(TILE_GRID.SIZE, TILE_GRID.COLUMNS, engineValues.complexity))
|
||||||
|
} else {
|
||||||
|
setTiles(generateTileGrid(TILE_GRID.SIZE, TILE_GRID.COLUMNS, engineValues.complexity))
|
||||||
|
}
|
||||||
setQueued(null)
|
setQueued(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleModeToggle = () => {
|
||||||
|
const newMode = mode === 'bytebeat' ? 'fm' : 'bytebeat'
|
||||||
|
handleStop()
|
||||||
|
setSynthesisMode(newMode)
|
||||||
|
|
||||||
|
if (playbackManagerRef.current) {
|
||||||
|
playbackManagerRef.current.setMode(newMode)
|
||||||
|
playbackManagerRef.current.setAlgorithm(engineValues.fmAlgorithm ?? 0)
|
||||||
|
playbackManagerRef.current.setFeedback(engineValues.fmFeedback ?? 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newMode === 'fm') {
|
||||||
|
setTiles(generateFMTileGrid(TILE_GRID.SIZE, TILE_GRID.COLUMNS, engineValues.complexity))
|
||||||
|
} else {
|
||||||
|
setTiles(generateTileGrid(TILE_GRID.SIZE, TILE_GRID.COLUMNS, engineValues.complexity))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleRandomizeAllParams = () => {
|
const handleRandomizeAllParams = () => {
|
||||||
clearSwitchTimer()
|
clearSwitchTimer()
|
||||||
let newRandomized: TileState | null = null
|
let newRandomized: TileState | null = null
|
||||||
@ -145,6 +172,7 @@ function App() {
|
|||||||
|
|
||||||
if (!playbackManagerRef.current) {
|
if (!playbackManagerRef.current) {
|
||||||
playbackManagerRef.current = new PlaybackManager({ sampleRate, duration })
|
playbackManagerRef.current = new PlaybackManager({ sampleRate, duration })
|
||||||
|
playbackManagerRef.current.setMode(mode)
|
||||||
} else {
|
} else {
|
||||||
await playbackManagerRef.current.updateOptions({ sampleRate, duration })
|
await playbackManagerRef.current.updateOptions({ sampleRate, duration })
|
||||||
}
|
}
|
||||||
@ -158,6 +186,10 @@ function App() {
|
|||||||
engineValues.d ?? DEFAULT_VARIABLES.d
|
engineValues.d ?? DEFAULT_VARIABLES.d
|
||||||
)
|
)
|
||||||
playbackManagerRef.current.setPitch(engineValues.pitch ?? 1.0)
|
playbackManagerRef.current.setPitch(engineValues.pitch ?? 1.0)
|
||||||
|
const fmPatch = mode === 'fm' ? JSON.parse(formula) : null
|
||||||
|
const lfoRates = fmPatch?.lfoRates || undefined
|
||||||
|
playbackManagerRef.current.setAlgorithm(engineValues.fmAlgorithm ?? 0, lfoRates)
|
||||||
|
playbackManagerRef.current.setFeedback(engineValues.fmFeedback ?? 0)
|
||||||
|
|
||||||
await playbackManagerRef.current.play(formula)
|
await playbackManagerRef.current.play(formula)
|
||||||
setPlaying(id)
|
setPlaying(id)
|
||||||
@ -206,6 +238,14 @@ function App() {
|
|||||||
playbackManagerRef.current.setPitch(value)
|
playbackManagerRef.current.setPitch(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (parameterId === 'fmAlgorithm' && playbackManagerRef.current) {
|
||||||
|
playbackManagerRef.current.setAlgorithm(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameterId === 'fmFeedback' && playbackManagerRef.current) {
|
||||||
|
playbackManagerRef.current.setFeedback(value)
|
||||||
|
}
|
||||||
|
|
||||||
if (['a', 'b', 'c', 'd'].includes(parameterId) && playbackManagerRef.current && playing) {
|
if (['a', 'b', 'c', 'd'].includes(parameterId) && playbackManagerRef.current && playing) {
|
||||||
const updatedValues = { ...engineValues, [parameterId]: value }
|
const updatedValues = { ...engineValues, [parameterId]: value }
|
||||||
playbackManagerRef.current.setVariables(
|
playbackManagerRef.current.setVariables(
|
||||||
@ -324,8 +364,15 @@ function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleRegenerate = (row: number, col: number) => {
|
const handleRegenerate = (row: number, col: number) => {
|
||||||
const newFormula = generateRandomFormula(engineValues.complexity)
|
let newTile: TileState
|
||||||
const newTile = createTileStateFromCurrent(newFormula)
|
|
||||||
|
if (mode === 'fm') {
|
||||||
|
const patch = generateRandomFMPatch(engineValues.complexity)
|
||||||
|
newTile = createFMTileState(patch)
|
||||||
|
} else {
|
||||||
|
const newFormula = generateRandomFormula(engineValues.complexity)
|
||||||
|
newTile = createTileStateFromCurrent(newFormula)
|
||||||
|
}
|
||||||
|
|
||||||
setTiles(prevTiles => {
|
setTiles(prevTiles => {
|
||||||
const newTiles = [...prevTiles]
|
const newTiles = [...prevTiles]
|
||||||
@ -547,37 +594,61 @@ function App() {
|
|||||||
>
|
>
|
||||||
BRUITISTE
|
BRUITISTE
|
||||||
</h1>
|
</h1>
|
||||||
<div className="flex gap-1 border-2 border-white">
|
<div className="flex gap-1">
|
||||||
<button
|
<div className="flex border-2 border-white">
|
||||||
onClick={() => setMobileHeaderTab('global')}
|
<button
|
||||||
className={`px-2 py-1 font-mono text-[8px] tracking-[0.15em] transition-colors ${
|
onClick={handleModeToggle}
|
||||||
mobileHeaderTab === 'global'
|
className={`px-2 py-1 font-mono text-[7px] tracking-[0.15em] transition-colors ${
|
||||||
? 'bg-white text-black'
|
mode === 'bytebeat'
|
||||||
: 'bg-black text-white hover:bg-white/10'
|
? 'bg-white text-black'
|
||||||
}`}
|
: 'bg-black text-white hover:bg-white/10'
|
||||||
>
|
}`}
|
||||||
GLOBAL
|
>
|
||||||
</button>
|
1-BIT
|
||||||
<button
|
</button>
|
||||||
onClick={() => setMobileHeaderTab('options')}
|
<button
|
||||||
className={`px-2 py-1 font-mono text-[8px] tracking-[0.15em] transition-colors ${
|
onClick={handleModeToggle}
|
||||||
mobileHeaderTab === 'options'
|
className={`px-2 py-1 font-mono text-[7px] tracking-[0.15em] transition-colors ${
|
||||||
? 'bg-white text-black'
|
mode === 'fm'
|
||||||
: 'bg-black text-white hover:bg-white/10'
|
? 'bg-white text-black'
|
||||||
}`}
|
: 'bg-black text-white hover:bg-white/10'
|
||||||
>
|
}`}
|
||||||
OPTIONS
|
>
|
||||||
</button>
|
4OP-FM
|
||||||
<button
|
</button>
|
||||||
onClick={() => setMobileHeaderTab('modulate')}
|
</div>
|
||||||
className={`px-2 py-1 font-mono text-[8px] tracking-[0.15em] transition-colors ${
|
<div className="flex border-2 border-white">
|
||||||
mobileHeaderTab === 'modulate'
|
<button
|
||||||
? 'bg-white text-black'
|
onClick={() => setMobileHeaderTab('global')}
|
||||||
: 'bg-black text-white hover:bg-white/10'
|
className={`px-2 py-1 font-mono text-[8px] tracking-[0.15em] transition-colors ${
|
||||||
}`}
|
mobileHeaderTab === 'global'
|
||||||
>
|
? 'bg-white text-black'
|
||||||
MODULATE
|
: 'bg-black text-white hover:bg-white/10'
|
||||||
</button>
|
}`}
|
||||||
|
>
|
||||||
|
GLOBAL
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setMobileHeaderTab('options')}
|
||||||
|
className={`px-2 py-1 font-mono text-[8px] tracking-[0.15em] transition-colors ${
|
||||||
|
mobileHeaderTab === 'options'
|
||||||
|
? 'bg-white text-black'
|
||||||
|
: 'bg-black text-white hover:bg-white/10'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
OPTIONS
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setMobileHeaderTab('modulate')}
|
||||||
|
className={`px-2 py-1 font-mono text-[8px] tracking-[0.15em] transition-colors ${
|
||||||
|
mobileHeaderTab === 'modulate'
|
||||||
|
? 'bg-white text-black'
|
||||||
|
: 'bg-black text-white hover:bg-white/10'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
MODULATE
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -635,12 +706,36 @@ function App() {
|
|||||||
|
|
||||||
{/* Desktop header */}
|
{/* Desktop header */}
|
||||||
<div className="hidden lg:flex items-center gap-4">
|
<div className="hidden lg:flex items-center gap-4">
|
||||||
<h1
|
<div className="flex flex-col gap-1 flex-shrink-0">
|
||||||
onClick={() => setShowHelp(true)}
|
<h1
|
||||||
className="font-mono text-sm tracking-[0.3em] text-white flex-shrink-0 cursor-pointer hover:opacity-70 transition-opacity"
|
onClick={() => setShowHelp(true)}
|
||||||
>
|
className="font-mono text-sm tracking-[0.3em] text-white cursor-pointer hover:opacity-70 transition-opacity"
|
||||||
BRUITISTE
|
>
|
||||||
</h1>
|
BRUITISTE
|
||||||
|
</h1>
|
||||||
|
<div className="flex border-2 border-white w-fit">
|
||||||
|
<button
|
||||||
|
onClick={handleModeToggle}
|
||||||
|
className={`px-2 py-0.5 font-mono text-[7px] tracking-[0.15em] transition-colors whitespace-nowrap ${
|
||||||
|
mode === 'bytebeat'
|
||||||
|
? 'bg-white text-black'
|
||||||
|
: 'bg-black text-white hover:bg-white/10'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
1-BIT
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleModeToggle}
|
||||||
|
className={`px-2 py-0.5 font-mono text-[7px] tracking-[0.15em] transition-colors whitespace-nowrap ${
|
||||||
|
mode === 'fm'
|
||||||
|
? 'bg-white text-black'
|
||||||
|
: 'bg-black text-white hover:bg-white/10'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
4OP-FM
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<EngineControls
|
<EngineControls
|
||||||
values={engineValues}
|
values={engineValues}
|
||||||
@ -687,19 +782,21 @@ function App() {
|
|||||||
<LFOPanel onChange={handleLFOChange} onUpdateDepth={handleUpdateMappingDepth} onRemoveMapping={handleRemoveMapping} />
|
<LFOPanel onChange={handleLFOChange} onUpdateDepth={handleUpdateMappingDepth} onRemoveMapping={handleRemoveMapping} />
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col overflow-auto bg-white">
|
<div className="flex-1 flex flex-col overflow-auto bg-white">
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-[1px] bg-white p-[1px]">
|
{mode === 'bytebeat' && (
|
||||||
<div className="col-span-1 lg:col-span-4">
|
<div className="grid grid-cols-1 lg:grid-cols-4 gap-[1px] bg-white p-[1px]">
|
||||||
<FormulaEditor
|
<div className="col-span-1 lg:col-span-4">
|
||||||
formula={customTile.formula}
|
<FormulaEditor
|
||||||
isPlaying={playing === PLAYBACK_ID.CUSTOM}
|
formula={customTile.formula}
|
||||||
isFocused={focusedTile === 'custom'}
|
isPlaying={playing === PLAYBACK_ID.CUSTOM}
|
||||||
playbackPosition={playing === PLAYBACK_ID.CUSTOM ? playbackPosition : 0}
|
isFocused={focusedTile === 'custom'}
|
||||||
onEvaluate={handleCustomEvaluate}
|
playbackPosition={playing === PLAYBACK_ID.CUSTOM ? playbackPosition : 0}
|
||||||
onStop={handleCustomStop}
|
onEvaluate={handleCustomEvaluate}
|
||||||
onRandom={handleCustomRandom}
|
onStop={handleCustomStop}
|
||||||
/>
|
onRandom={handleCustomRandom}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
<div className="flex-1 grid grid-cols-1 lg:grid-cols-4 auto-rows-min gap-[1px] bg-white p-[1px]">
|
<div className="flex-1 grid grid-cols-1 lg:grid-cols-4 auto-rows-min gap-[1px] bg-white p-[1px]">
|
||||||
{tiles.map((row, i) =>
|
{tiles.map((row, i) =>
|
||||||
|
|||||||
@ -1,6 +1,11 @@
|
|||||||
import { useRef, useEffect } from 'react'
|
import { useRef, useEffect } from 'react'
|
||||||
|
import { useStore } from '@nanostores/react'
|
||||||
import { Download, Dices } from 'lucide-react'
|
import { Download, Dices } from 'lucide-react'
|
||||||
import { generateWaveformData, drawWaveform } from '../utils/waveformGenerator'
|
import { generateWaveformData, drawWaveform } from '../utils/waveformGenerator'
|
||||||
|
import { generateFMWaveformData } from '../utils/fmWaveformGenerator'
|
||||||
|
import { synthesisMode } from '../stores/synthesisMode'
|
||||||
|
import { parseFMPatch } from '../utils/fmPatches'
|
||||||
|
import { getAlgorithmName } from '../config/fmAlgorithms'
|
||||||
|
|
||||||
interface BytebeatTileProps {
|
interface BytebeatTileProps {
|
||||||
formula: string
|
formula: string
|
||||||
@ -22,6 +27,7 @@ interface BytebeatTileProps {
|
|||||||
|
|
||||||
export function BytebeatTile({ formula, row, col, isPlaying, isQueued, isFocused, playbackPosition, a, b, c, d, onPlay, onDoubleClick, onDownload, onRegenerate }: BytebeatTileProps) {
|
export function BytebeatTile({ formula, row, col, isPlaying, isQueued, isFocused, playbackPosition, a, b, c, d, onPlay, onDoubleClick, onDownload, onRegenerate }: BytebeatTileProps) {
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||||
|
const mode = useStore(synthesisMode)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const canvas = canvasRef.current
|
const canvas = canvasRef.current
|
||||||
@ -31,10 +37,19 @@ export function BytebeatTile({ formula, row, col, isPlaying, isQueued, isFocused
|
|||||||
canvas.width = rect.width * window.devicePixelRatio
|
canvas.width = rect.width * window.devicePixelRatio
|
||||||
canvas.height = rect.height * window.devicePixelRatio
|
canvas.height = rect.height * window.devicePixelRatio
|
||||||
|
|
||||||
const waveformData = generateWaveformData(formula, canvas.width, 8000, 0.5, a, b, c, d)
|
let waveformData: number[]
|
||||||
|
if (mode === 'fm') {
|
||||||
|
const fmPatch = parseFMPatch(formula)
|
||||||
|
const algorithmId = fmPatch?.algorithm ?? 0
|
||||||
|
const feedback = fmPatch?.feedback ?? 0
|
||||||
|
waveformData = generateFMWaveformData(algorithmId, a, b, c, d, feedback, canvas.width, 8000, 0.5)
|
||||||
|
} else {
|
||||||
|
waveformData = generateWaveformData(formula, canvas.width, 8000, 0.5, a, b, c, d)
|
||||||
|
}
|
||||||
|
|
||||||
const color = isPlaying ? 'rgba(0, 0, 0, 0.3)' : 'rgba(255, 255, 255, 0.35)'
|
const color = isPlaying ? 'rgba(0, 0, 0, 0.3)' : 'rgba(255, 255, 255, 0.35)'
|
||||||
drawWaveform(canvas, waveformData, color)
|
drawWaveform(canvas, waveformData, color)
|
||||||
}, [formula, isPlaying, isQueued, a, b, c, d])
|
}, [formula, isPlaying, isQueued, a, b, c, d, mode])
|
||||||
|
|
||||||
const handleDownload = (e: React.MouseEvent) => {
|
const handleDownload = (e: React.MouseEvent) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
@ -66,7 +81,11 @@ export function BytebeatTile({ formula, row, col, isPlaying, isQueued, isFocused
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="text-xs break-all font-light flex-1 relative z-10">
|
<div className="text-xs break-all font-light flex-1 relative z-10">
|
||||||
{formula}
|
{mode === 'fm' ? (() => {
|
||||||
|
const fmPatch = parseFMPatch(formula)
|
||||||
|
const algorithmName = getAlgorithmName(fmPatch?.algorithm ?? 0)
|
||||||
|
return `${algorithmName} [${a},${b},${c},${d}] FB:${fmPatch?.feedback ?? 0}`
|
||||||
|
})() : formula}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1 flex-shrink-0 relative z-10">
|
<div className="flex gap-1 flex-shrink-0 relative z-10">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
|
import { useStore } from '@nanostores/react'
|
||||||
import { ENGINE_CONTROLS } from '../config/effects'
|
import { ENGINE_CONTROLS } from '../config/effects'
|
||||||
import { getComplexityLabel, getBitDepthLabel, getSampleRateLabel } from '../utils/formatters'
|
import { getComplexityLabel, getBitDepthLabel, getSampleRateLabel, getAlgorithmLabel } from '../utils/formatters'
|
||||||
import type { EffectValues } from '../types/effects'
|
import type { EffectValues } from '../types/effects'
|
||||||
import { Knob } from './Knob'
|
import { Knob } from './Knob'
|
||||||
|
import { synthesisMode } from '../stores/synthesisMode'
|
||||||
|
|
||||||
interface EngineControlsProps {
|
interface EngineControlsProps {
|
||||||
values: EffectValues
|
values: EffectValues
|
||||||
@ -15,6 +17,8 @@ interface EngineControlsProps {
|
|||||||
const KNOB_PARAMS = ['masterVolume', 'pitch', 'a', 'b', 'c', 'd']
|
const KNOB_PARAMS = ['masterVolume', 'pitch', 'a', 'b', 'c', 'd']
|
||||||
|
|
||||||
export function EngineControls({ values, onChange, onMapClick, getMappedLFOs, showOnlySliders, showOnlyKnobs }: EngineControlsProps) {
|
export function EngineControls({ values, onChange, onMapClick, getMappedLFOs, showOnlySliders, showOnlyKnobs }: EngineControlsProps) {
|
||||||
|
const mode = useStore(synthesisMode)
|
||||||
|
|
||||||
const formatValue = (id: string, value: number): string => {
|
const formatValue = (id: string, value: number): string => {
|
||||||
switch (id) {
|
switch (id) {
|
||||||
case 'sampleRate':
|
case 'sampleRate':
|
||||||
@ -23,6 +27,8 @@ export function EngineControls({ values, onChange, onMapClick, getMappedLFOs, sh
|
|||||||
return getComplexityLabel(value)
|
return getComplexityLabel(value)
|
||||||
case 'bitDepth':
|
case 'bitDepth':
|
||||||
return getBitDepthLabel(value)
|
return getBitDepthLabel(value)
|
||||||
|
case 'fmAlgorithm':
|
||||||
|
return getAlgorithmLabel(value)
|
||||||
default: {
|
default: {
|
||||||
const param = ENGINE_CONTROLS[0].parameters.find(p => p.id === id)
|
const param = ENGINE_CONTROLS[0].parameters.find(p => p.id === id)
|
||||||
return `${value}${param?.unit || ''}`
|
return `${value}${param?.unit || ''}`
|
||||||
@ -35,6 +41,9 @@ export function EngineControls({ values, onChange, onMapClick, getMappedLFOs, sh
|
|||||||
{ENGINE_CONTROLS[0].parameters.map(param => {
|
{ENGINE_CONTROLS[0].parameters.map(param => {
|
||||||
const useKnob = KNOB_PARAMS.includes(param.id)
|
const useKnob = KNOB_PARAMS.includes(param.id)
|
||||||
|
|
||||||
|
if (mode === 'bytebeat' && (param.id === 'fmAlgorithm' || param.id === 'fmFeedback')) return null
|
||||||
|
if (mode === 'fm' && (param.id === 'complexity' || param.id === 'bitDepth')) return null
|
||||||
|
|
||||||
if (showOnlySliders && useKnob) return null
|
if (showOnlySliders && useKnob) return null
|
||||||
if (showOnlyKnobs && !useKnob) return null
|
if (showOnlyKnobs && !useKnob) return null
|
||||||
|
|
||||||
|
|||||||
@ -94,6 +94,24 @@ export const ENGINE_CONTROLS: EffectConfig[] = [
|
|||||||
default: 64,
|
default: 64,
|
||||||
step: 1,
|
step: 1,
|
||||||
unit: ''
|
unit: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fmAlgorithm',
|
||||||
|
label: 'Algorithm',
|
||||||
|
min: 0,
|
||||||
|
max: 15,
|
||||||
|
default: 0,
|
||||||
|
step: 1,
|
||||||
|
unit: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fmFeedback',
|
||||||
|
label: 'Feedback',
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
default: 0,
|
||||||
|
step: 1,
|
||||||
|
unit: '%'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
97
src/config/fmAlgorithms.ts
Normal file
97
src/config/fmAlgorithms.ts
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
export interface FMAlgorithm {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
frequencyRatios: [number, number, number, number]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FM_ALGORITHMS: FMAlgorithm[] = [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
name: 'Algo 0',
|
||||||
|
frequencyRatios: [1.0, 1.0, 1.0, 1.0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Algo 1',
|
||||||
|
frequencyRatios: [1.0, 1.0, 1.0, 1.0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Algo 2',
|
||||||
|
frequencyRatios: [1.0, 2.0, 1.0, 2.0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'Algo 3',
|
||||||
|
frequencyRatios: [1.0, 2.0, 3.0, 1.0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: 'Algo 4',
|
||||||
|
frequencyRatios: [1.0, 1.414, 2.0, 2.828]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
name: 'Algo 5',
|
||||||
|
frequencyRatios: [1.0, 2.0, 3.0, 4.0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
name: 'Algo 6',
|
||||||
|
frequencyRatios: [1.0, 2.5, 1.0, 1.0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
name: 'Algo 7',
|
||||||
|
frequencyRatios: [1.0, 1.5, 1.5, 2.0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
name: 'Algo 8',
|
||||||
|
frequencyRatios: [1.0, 1.0, 2.0, 2.0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
name: 'Algo 9',
|
||||||
|
frequencyRatios: [1.0, 1.0, 2.0, 1.5]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 10,
|
||||||
|
name: 'Algo 10',
|
||||||
|
frequencyRatios: [1.0, 1.0, 1.0, 2.0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
name: 'Algo 11',
|
||||||
|
frequencyRatios: [1.0, 1.0, 2.0, 3.0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 12,
|
||||||
|
name: 'Algo 12',
|
||||||
|
frequencyRatios: [1.0, 1.5, 1.0, 2.0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 13,
|
||||||
|
name: 'Algo 13',
|
||||||
|
frequencyRatios: [1.0, 2.0, 3.0, 4.0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 14,
|
||||||
|
name: 'Algo 14',
|
||||||
|
frequencyRatios: [1.0, 2.0, 1.5, 2.5]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 15,
|
||||||
|
name: 'Algo 15',
|
||||||
|
frequencyRatios: [1.0, 1.414, 1.732, 2.0]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export function getAlgorithmById(id: number): FMAlgorithm {
|
||||||
|
return FM_ALGORITHMS[id % FM_ALGORITHMS.length] || FM_ALGORITHMS[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAlgorithmName(id: number): string {
|
||||||
|
const alg = getAlgorithmById(id)
|
||||||
|
return `${alg.name}`
|
||||||
|
}
|
||||||
@ -1,8 +1,11 @@
|
|||||||
import { EffectsChain } from './effects/EffectsChain'
|
import { EffectsChain } from './effects/EffectsChain'
|
||||||
import { BytebeatSourceEffect } from './effects/BytebeatSourceEffect'
|
import { BytebeatSourceEffect } from './effects/BytebeatSourceEffect'
|
||||||
|
import { FMSourceEffect } from './effects/FMSourceEffect'
|
||||||
import { ModulationEngine } from '../modulation/ModulationEngine'
|
import { ModulationEngine } from '../modulation/ModulationEngine'
|
||||||
import type { LFOWaveform } from '../modulation/LFO'
|
import type { LFOWaveform } from '../modulation/LFO'
|
||||||
import type { EffectValues } from '../../types/effects'
|
import type { EffectValues } from '../../types/effects'
|
||||||
|
import type { SynthesisMode } from '../../stores/synthesisMode'
|
||||||
|
import { getAlgorithmById } from '../../config/fmAlgorithms'
|
||||||
|
|
||||||
export interface AudioPlayerOptions {
|
export interface AudioPlayerOptions {
|
||||||
sampleRate: number
|
sampleRate: number
|
||||||
@ -12,6 +15,7 @@ export interface AudioPlayerOptions {
|
|||||||
export class AudioPlayer {
|
export class AudioPlayer {
|
||||||
private audioContext: AudioContext | null = null
|
private audioContext: AudioContext | null = null
|
||||||
private bytebeatSource: BytebeatSourceEffect | null = null
|
private bytebeatSource: BytebeatSourceEffect | null = null
|
||||||
|
private fmSource: FMSourceEffect | null = null
|
||||||
private effectsChain: EffectsChain | null = null
|
private effectsChain: EffectsChain | null = null
|
||||||
private modulationEngine: ModulationEngine | null = null
|
private modulationEngine: ModulationEngine | null = null
|
||||||
private effectValues: EffectValues = {}
|
private effectValues: EffectValues = {}
|
||||||
@ -20,6 +24,9 @@ export class AudioPlayer {
|
|||||||
private duration: number
|
private duration: number
|
||||||
private workletRegistered: boolean = false
|
private workletRegistered: boolean = false
|
||||||
private currentPitch: number = 1.0
|
private currentPitch: number = 1.0
|
||||||
|
private currentMode: SynthesisMode = 'bytebeat'
|
||||||
|
private currentAlgorithm: number = 0
|
||||||
|
private currentFeedback: number = 0
|
||||||
|
|
||||||
constructor(options: AudioPlayerOptions) {
|
constructor(options: AudioPlayerOptions) {
|
||||||
this.sampleRate = options.sampleRate
|
this.sampleRate = options.sampleRate
|
||||||
@ -66,6 +73,7 @@ export class AudioPlayer {
|
|||||||
context.audioWorklet.addModule('/worklets/svf-processor.js'),
|
context.audioWorklet.addModule('/worklets/svf-processor.js'),
|
||||||
context.audioWorklet.addModule('/worklets/fold-crush-processor.js'),
|
context.audioWorklet.addModule('/worklets/fold-crush-processor.js'),
|
||||||
context.audioWorklet.addModule('/worklets/bytebeat-processor.js'),
|
context.audioWorklet.addModule('/worklets/bytebeat-processor.js'),
|
||||||
|
context.audioWorklet.addModule('/worklets/fm-processor.js'),
|
||||||
context.audioWorklet.addModule('/worklets/output-limiter.js')
|
context.audioWorklet.addModule('/worklets/output-limiter.js')
|
||||||
])
|
])
|
||||||
this.workletRegistered = true
|
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> {
|
async playRealtime(formula: string, a: number, b: number, c: number, d: number): Promise<void> {
|
||||||
await this.ensureAudioContext()
|
await this.ensureAudioContext()
|
||||||
|
|
||||||
if (!this.bytebeatSource) {
|
if (this.currentMode === 'bytebeat') {
|
||||||
this.bytebeatSource = new BytebeatSourceEffect(this.audioContext!)
|
if (!this.bytebeatSource) {
|
||||||
await this.bytebeatSource.initialize(this.audioContext!)
|
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)
|
this.effectsChain!.getOutputNode().connect(this.audioContext!.destination)
|
||||||
|
|
||||||
if (this.modulationEngine) {
|
if (this.modulationEngine) {
|
||||||
@ -166,14 +214,18 @@ export class AudioPlayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateRealtimeVariables(a: number, b: number, c: number, d: number): void {
|
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)
|
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 {
|
private applyPitch(pitch: number): void {
|
||||||
if (this.bytebeatSource) {
|
if (this.currentMode === 'bytebeat' && this.bytebeatSource) {
|
||||||
this.bytebeatSource.setPlaybackRate(pitch)
|
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) {
|
if (this.bytebeatSource) {
|
||||||
this.bytebeatSource.getOutputNode().disconnect()
|
this.bytebeatSource.getOutputNode().disconnect()
|
||||||
}
|
}
|
||||||
|
if (this.fmSource) {
|
||||||
|
this.fmSource.getOutputNode().disconnect()
|
||||||
|
}
|
||||||
if (this.modulationEngine) {
|
if (this.modulationEngine) {
|
||||||
this.modulationEngine.stop()
|
this.modulationEngine.stop()
|
||||||
}
|
}
|
||||||
@ -209,6 +264,10 @@ export class AudioPlayer {
|
|||||||
this.bytebeatSource.dispose()
|
this.bytebeatSource.dispose()
|
||||||
this.bytebeatSource = null
|
this.bytebeatSource = null
|
||||||
}
|
}
|
||||||
|
if (this.fmSource) {
|
||||||
|
this.fmSource.dispose()
|
||||||
|
this.fmSource = null
|
||||||
|
}
|
||||||
if (this.modulationEngine) {
|
if (this.modulationEngine) {
|
||||||
this.modulationEngine.dispose()
|
this.modulationEngine.dispose()
|
||||||
this.modulationEngine = null
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import { AudioPlayer } from '../domain/audio/AudioPlayer'
|
import { AudioPlayer } from '../domain/audio/AudioPlayer'
|
||||||
import type { LFOWaveform } from '../domain/modulation/LFO'
|
import type { LFOWaveform } from '../domain/modulation/LFO'
|
||||||
import type { EffectValues } from '../types/effects'
|
import type { EffectValues } from '../types/effects'
|
||||||
|
import type { SynthesisMode } from '../stores/synthesisMode'
|
||||||
import { DEFAULT_VARIABLES } from '../constants/defaults'
|
import { DEFAULT_VARIABLES } from '../constants/defaults'
|
||||||
|
|
||||||
export interface PlaybackOptions {
|
export interface PlaybackOptions {
|
||||||
@ -40,6 +41,18 @@ export class PlaybackManager {
|
|||||||
this.player.updatePitch(pitch)
|
this.player.updatePitch(pitch)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setMode(mode: SynthesisMode): void {
|
||||||
|
this.player.setMode(mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
setAlgorithm(algorithmId: number, lfoRates?: number[]): void {
|
||||||
|
this.player.setAlgorithm(algorithmId, lfoRates)
|
||||||
|
}
|
||||||
|
|
||||||
|
setFeedback(feedback: number): void {
|
||||||
|
this.player.setFeedback(feedback)
|
||||||
|
}
|
||||||
|
|
||||||
setPlaybackPositionCallback(callback: (position: number) => void): void {
|
setPlaybackPositionCallback(callback: (position: number) => void): void {
|
||||||
this.playbackPositionCallback = callback
|
this.playbackPositionCallback = callback
|
||||||
}
|
}
|
||||||
|
|||||||
25
src/stores/synthesisMode.ts
Normal file
25
src/stores/synthesisMode.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { atom } from 'nanostores'
|
||||||
|
|
||||||
|
export type SynthesisMode = 'bytebeat' | 'fm'
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'synthesisMode'
|
||||||
|
|
||||||
|
function loadFromStorage(): SynthesisMode {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(STORAGE_KEY)
|
||||||
|
return (stored === 'fm' || stored === 'bytebeat') ? stored : 'bytebeat'
|
||||||
|
} catch {
|
||||||
|
return 'bytebeat'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const synthesisMode = atom<SynthesisMode>(loadFromStorage())
|
||||||
|
|
||||||
|
export function setSynthesisMode(mode: SynthesisMode): void {
|
||||||
|
synthesisMode.set(mode)
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEY, mode)
|
||||||
|
} catch {
|
||||||
|
// Silently fail on storage errors
|
||||||
|
}
|
||||||
|
}
|
||||||
89
src/utils/fmPatches.ts
Normal file
89
src/utils/fmPatches.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import type { TileState } from '../types/tiles'
|
||||||
|
import { getDefaultEngineValues, getDefaultEffectValues } from '../config/effects'
|
||||||
|
import { getDefaultLFOValues } from '../stores/settings'
|
||||||
|
|
||||||
|
export interface FMPatchConfig {
|
||||||
|
algorithm: number
|
||||||
|
feedback: number
|
||||||
|
lfoRates: [number, number, number, number]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateRandomFMPatch(complexity: number = 1): FMPatchConfig {
|
||||||
|
let algorithmRange: number[]
|
||||||
|
|
||||||
|
switch (complexity) {
|
||||||
|
case 0:
|
||||||
|
algorithmRange = [0, 2, 6, 8, 9, 10]
|
||||||
|
break
|
||||||
|
case 2:
|
||||||
|
algorithmRange = [1, 4, 5, 7, 11, 12, 13, 14, 15]
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
algorithmRange = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
|
||||||
|
}
|
||||||
|
|
||||||
|
const algorithm = algorithmRange[Math.floor(Math.random() * algorithmRange.length)]
|
||||||
|
const feedback = Math.floor(Math.random() * 100)
|
||||||
|
|
||||||
|
const lfoRates: [number, number, number, number] = [
|
||||||
|
0.2 + Math.random() * 0.8,
|
||||||
|
0.3 + Math.random() * 1.0,
|
||||||
|
0.4 + Math.random() * 1.2,
|
||||||
|
0.25 + Math.random() * 0.9
|
||||||
|
]
|
||||||
|
|
||||||
|
return { algorithm, feedback, lfoRates }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createFMTileState(patch: FMPatchConfig): TileState {
|
||||||
|
const formula = JSON.stringify(patch)
|
||||||
|
|
||||||
|
const pitchValues = [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 2.0]
|
||||||
|
const randomPitch = pitchValues[Math.floor(Math.random() * pitchValues.length)]
|
||||||
|
|
||||||
|
return {
|
||||||
|
formula,
|
||||||
|
engineParams: {
|
||||||
|
...getDefaultEngineValues(),
|
||||||
|
a: Math.floor(Math.random() * 128) + 64,
|
||||||
|
b: Math.floor(Math.random() * 128) + 64,
|
||||||
|
c: Math.floor(Math.random() * 128) + 64,
|
||||||
|
d: Math.floor(Math.random() * 128) + 64,
|
||||||
|
pitch: randomPitch,
|
||||||
|
fmAlgorithm: patch.algorithm,
|
||||||
|
fmFeedback: patch.feedback
|
||||||
|
},
|
||||||
|
effectParams: getDefaultEffectValues(),
|
||||||
|
lfoConfigs: getDefaultLFOValues()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateFMTileGrid(size: number, columns: number, complexity: number = 1): TileState[][] {
|
||||||
|
const tiles: TileState[][] = []
|
||||||
|
const rows = Math.ceil(size / columns)
|
||||||
|
|
||||||
|
for (let i = 0; i < rows; i++) {
|
||||||
|
const row: TileState[] = []
|
||||||
|
for (let j = 0; j < columns; j++) {
|
||||||
|
if (i * columns + j < size) {
|
||||||
|
const patch = generateRandomFMPatch(complexity)
|
||||||
|
row.push(createFMTileState(patch))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tiles.push(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tiles
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseFMPatch(formula: string): FMPatchConfig | null {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(formula)
|
||||||
|
if (typeof parsed.algorithm === 'number' && typeof parsed.feedback === 'number') {
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Not a valid FM patch
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
252
src/utils/fmWaveformGenerator.ts
Normal file
252
src/utils/fmWaveformGenerator.ts
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
import { getAlgorithmById } from '../config/fmAlgorithms'
|
||||||
|
|
||||||
|
export function generateFMWaveformData(
|
||||||
|
algorithmId: number,
|
||||||
|
a: number,
|
||||||
|
b: number,
|
||||||
|
c: number,
|
||||||
|
d: number,
|
||||||
|
feedback: number,
|
||||||
|
width: number,
|
||||||
|
sampleRate: number,
|
||||||
|
duration: number
|
||||||
|
): number[] {
|
||||||
|
const algorithm = getAlgorithmById(algorithmId)
|
||||||
|
const samples = Math.floor(sampleRate * duration)
|
||||||
|
const data: number[] = []
|
||||||
|
|
||||||
|
const level1 = a / 255.0
|
||||||
|
const level2 = b / 255.0
|
||||||
|
const level3 = c / 255.0
|
||||||
|
const level4 = d / 255.0
|
||||||
|
const feedbackAmount = feedback / 100.0
|
||||||
|
|
||||||
|
const baseFreq = 220
|
||||||
|
const TWO_PI = Math.PI * 2
|
||||||
|
|
||||||
|
const freq1 = (baseFreq * algorithm.frequencyRatios[0] * TWO_PI) / sampleRate
|
||||||
|
const freq2 = (baseFreq * algorithm.frequencyRatios[1] * TWO_PI) / sampleRate
|
||||||
|
const freq3 = (baseFreq * algorithm.frequencyRatios[2] * TWO_PI) / sampleRate
|
||||||
|
const freq4 = (baseFreq * algorithm.frequencyRatios[3] * TWO_PI) / sampleRate
|
||||||
|
|
||||||
|
let phase1 = 0
|
||||||
|
let phase2 = 0
|
||||||
|
let phase3 = 0
|
||||||
|
let phase4 = 0
|
||||||
|
let feedbackSample = 0
|
||||||
|
|
||||||
|
for (let i = 0; i < samples; i++) {
|
||||||
|
let output = 0
|
||||||
|
|
||||||
|
switch (algorithmId) {
|
||||||
|
case 0: {
|
||||||
|
const op1 = Math.sin(phase1) * level1
|
||||||
|
const op2 = Math.sin(phase2) * level2
|
||||||
|
const op3 = Math.sin(phase3) * level3
|
||||||
|
const op4 = Math.sin(phase4) * level4
|
||||||
|
output = (op1 + op2 + op3 + op4) * 0.25
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 1: {
|
||||||
|
const op1 = Math.sin(phase1) * level1
|
||||||
|
const mod1 = op1 * 10
|
||||||
|
const op2 = Math.sin(phase2 + mod1) * level2
|
||||||
|
const mod2 = op2 * 10
|
||||||
|
const op3 = Math.sin(phase3 + mod2) * level3
|
||||||
|
const mod3 = op3 * 10
|
||||||
|
const op4 = Math.sin(phase4 + mod3 + feedbackSample * feedbackAmount) * level4
|
||||||
|
output = op4
|
||||||
|
feedbackSample = op4
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 2: {
|
||||||
|
const op1 = Math.sin(phase1) * level1
|
||||||
|
const mod1 = op1 * 10
|
||||||
|
const op2 = Math.sin(phase2 + mod1) * level2
|
||||||
|
const op3 = Math.sin(phase3) * level3
|
||||||
|
const mod3 = op3 * 10
|
||||||
|
const op4 = Math.sin(phase4 + mod3) * level4
|
||||||
|
output = (op2 + op4) * 0.5
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 3: {
|
||||||
|
const op1 = Math.sin(phase1) * level1
|
||||||
|
const mod1 = op1 * 10
|
||||||
|
const op2 = Math.sin(phase2 + mod1) * level2
|
||||||
|
const mod2 = op2 * 10
|
||||||
|
const op3 = Math.sin(phase3 + mod2) * level3
|
||||||
|
const op4 = Math.sin(phase4) * level4
|
||||||
|
output = (op3 + op4) * 0.5
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 4: {
|
||||||
|
const op1 = Math.sin(phase1) * level1
|
||||||
|
const mod1 = op1 * 10
|
||||||
|
const op2 = Math.sin(phase2) * level2
|
||||||
|
const mod2 = op2 * 10
|
||||||
|
const op3 = Math.sin(phase3 + mod1 + mod2) * level3
|
||||||
|
const mod3 = op3 * 10
|
||||||
|
const op4 = Math.sin(phase4 + mod3) * level4
|
||||||
|
output = op4
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 5: {
|
||||||
|
const op1 = Math.sin(phase1) * level1
|
||||||
|
const mod1 = op1 * 10
|
||||||
|
const op2 = Math.sin(phase2 + mod1) * level2
|
||||||
|
const mod2 = op2 * 10
|
||||||
|
const op3 = Math.sin(phase3 + mod1) * level3
|
||||||
|
const mod3 = op3 * 10
|
||||||
|
const op4 = Math.sin(phase4 + mod2 + mod3) * level4
|
||||||
|
output = op4
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 6: {
|
||||||
|
const op1 = Math.sin(phase1) * level1
|
||||||
|
const mod1 = op1 * 10
|
||||||
|
const op2 = Math.sin(phase2 + mod1) * level2
|
||||||
|
const op3 = Math.sin(phase3) * level3
|
||||||
|
const op4 = Math.sin(phase4) * level4
|
||||||
|
output = (op2 + op3 + op4) * 0.333
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 7: {
|
||||||
|
const op1 = Math.sin(phase1) * level1
|
||||||
|
const mod1 = op1 * 10
|
||||||
|
const op2 = Math.sin(phase2 + mod1) * level2
|
||||||
|
const mod2 = op2 * 1.5
|
||||||
|
const op3 = Math.sin(phase3 + mod1) * level3
|
||||||
|
const mod3 = op3 * 1.5
|
||||||
|
const op4 = Math.sin(phase4 + mod2 + mod3) * level4
|
||||||
|
output = op4
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 8: {
|
||||||
|
const op1 = Math.sin(phase1) * level1
|
||||||
|
const mod1 = op1 * 10
|
||||||
|
const op3 = Math.sin(phase3 + mod1) * level3
|
||||||
|
const op2 = Math.sin(phase2) * level2
|
||||||
|
const mod2 = op2 * 10
|
||||||
|
const op4 = Math.sin(phase4 + mod2) * level4
|
||||||
|
output = (op3 + op4) * 0.5
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 9: {
|
||||||
|
const op1 = Math.sin(phase1) * level1
|
||||||
|
const mod1 = op1 * 10
|
||||||
|
const op4 = Math.sin(phase4 + mod1) * level4
|
||||||
|
const op2 = Math.sin(phase2) * level2
|
||||||
|
const mod2 = op2 * 10
|
||||||
|
const op3 = Math.sin(phase3 + mod2) * level3
|
||||||
|
output = (op3 + op4) * 0.5
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 10: {
|
||||||
|
const op1 = Math.sin(phase1) * level1
|
||||||
|
const mod1 = op1 * 10
|
||||||
|
const op2 = Math.sin(phase2 + mod1) * level2
|
||||||
|
const op3 = Math.sin(phase3) * level3
|
||||||
|
const mod3 = op3 * 10
|
||||||
|
const op4 = Math.sin(phase4 + mod3) * level4
|
||||||
|
output = (op2 + op4) * 0.5
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 11: {
|
||||||
|
const op1 = Math.sin(phase1) * level1
|
||||||
|
const op2 = Math.sin(phase2) * level2
|
||||||
|
const mod2 = op2 * 10
|
||||||
|
const op3 = Math.sin(phase3 + mod2) * level3
|
||||||
|
const mod3 = op3 * 10
|
||||||
|
const op4 = Math.sin(phase4 + mod3) * level4
|
||||||
|
output = (op1 + op4) * 0.5
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 12: {
|
||||||
|
const op1 = Math.sin(phase1) * level1
|
||||||
|
const mod1 = op1 * 10
|
||||||
|
const op2 = Math.sin(phase2 + mod1) * level2
|
||||||
|
const mod2 = op2 * 10
|
||||||
|
const op4 = Math.sin(phase4 + mod2) * level4
|
||||||
|
const op3 = Math.sin(phase3) * level3
|
||||||
|
output = (op3 + op4) * 0.5
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 13: {
|
||||||
|
const op1 = Math.sin(phase1) * level1
|
||||||
|
const mod1 = op1 * 10
|
||||||
|
const op2 = Math.sin(phase2 + mod1) * level2
|
||||||
|
const mod2 = op2 * 10
|
||||||
|
const op3 = Math.sin(phase3 + mod1) * level3
|
||||||
|
const mod3 = op3 * 10
|
||||||
|
const op4 = Math.sin(phase4 + mod2 + mod3) * level4
|
||||||
|
output = op4
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 14: {
|
||||||
|
const op1 = Math.sin(phase1) * level1
|
||||||
|
const mod1 = op1 * 10
|
||||||
|
const op3 = Math.sin(phase3) * level3
|
||||||
|
const mod3 = op3 * 10
|
||||||
|
const op4 = Math.sin(phase4 + mod3) * level4
|
||||||
|
const mod4 = op4 * 1.5
|
||||||
|
const op2 = Math.sin(phase2 + mod1 + mod4) * level2
|
||||||
|
output = op2
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 15: {
|
||||||
|
const op1 = Math.sin(phase1) * level1
|
||||||
|
const mod1 = op1 * 10
|
||||||
|
const op2 = Math.sin(phase2) * level2
|
||||||
|
const mod2 = op2 * 10
|
||||||
|
const op3 = Math.sin(phase3) * level3
|
||||||
|
const mod3 = op3 * 10
|
||||||
|
const op4 = Math.sin(phase4 + mod1 + mod2 + mod3) * level4
|
||||||
|
output = op4
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data.push(output)
|
||||||
|
|
||||||
|
phase1 += freq1
|
||||||
|
phase2 += freq2
|
||||||
|
phase3 += freq3
|
||||||
|
phase4 += freq4
|
||||||
|
}
|
||||||
|
|
||||||
|
const samplesPerPixel = Math.max(1, Math.floor(samples / width))
|
||||||
|
const downsampledData: number[] = []
|
||||||
|
|
||||||
|
for (let i = 0; i < width; i++) {
|
||||||
|
let min = Infinity
|
||||||
|
let max = -Infinity
|
||||||
|
|
||||||
|
for (let s = 0; s < samplesPerPixel; s++) {
|
||||||
|
const index = i * samplesPerPixel + s
|
||||||
|
if (index < data.length) {
|
||||||
|
const value = data[index]
|
||||||
|
min = Math.min(min, value)
|
||||||
|
max = Math.max(max, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
downsampledData.push(min === Infinity ? 0 : min, max === -Infinity ? 0 : max)
|
||||||
|
}
|
||||||
|
|
||||||
|
return downsampledData
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import { SAMPLE_RATES } from '../config/effects'
|
import { SAMPLE_RATES } from '../config/effects'
|
||||||
|
import { getAlgorithmName } from '../config/fmAlgorithms'
|
||||||
|
|
||||||
export function getComplexityLabel(index: number): string {
|
export function getComplexityLabel(index: number): string {
|
||||||
const labels = ['Simple', 'Medium', 'Complex']
|
const labels = ['Simple', 'Medium', 'Complex']
|
||||||
@ -12,4 +13,8 @@ export function getBitDepthLabel(index: number): string {
|
|||||||
|
|
||||||
export function getSampleRateLabel(index: number): string {
|
export function getSampleRateLabel(index: number): string {
|
||||||
return `${SAMPLE_RATES[index]}Hz`
|
return `${SAMPLE_RATES[index]}Hz`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAlgorithmLabel(index: number): string {
|
||||||
|
return getAlgorithmName(index)
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user