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

89
src/utils/fmPatches.ts Normal file
View 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
}

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

View File

@@ -1,4 +1,5 @@
import { SAMPLE_RATES } from '../config/effects'
import { getAlgorithmName } from '../config/fmAlgorithms'
export function getComplexityLabel(index: number): string {
const labels = ['Simple', 'Medium', 'Complex']
@@ -12,4 +13,8 @@ export function getBitDepthLabel(index: number): string {
export function getSampleRateLabel(index: number): string {
return `${SAMPLE_RATES[index]}Hz`
}
export function getAlgorithmLabel(index: number): string {
return getAlgorithmName(index)
}