Adding new FM synthesis mode
This commit is contained in:
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 { 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)
|
||||
}
|
||||
Reference in New Issue
Block a user