diff --git a/public/worklets/fm-processor.js b/public/worklets/fm-processor.js new file mode 100644 index 00000000..70afd974 --- /dev/null +++ b/public/worklets/fm-processor.js @@ -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) diff --git a/src/App.tsx b/src/App.tsx index 1231c614..d1131ade 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,6 +4,7 @@ import { Square, Archive, Dices, Sparkles } from 'lucide-react' import { PlaybackManager } from './services/PlaybackManager' import { DownloadService } from './services/DownloadService' import { generateTileGrid, generateRandomFormula } from './utils/bytebeatFormulas' +import { generateFMTileGrid, generateRandomFMPatch, createFMTileState } from './utils/fmPatches' import { BytebeatTile } from './components/BytebeatTile' import { EffectsBar } from './components/EffectsBar' import { EngineControls } from './components/EngineControls' @@ -13,6 +14,7 @@ import { AudioContextWarning } from './components/AudioContextWarning' import { HelpModal } from './components/HelpModal' import { getSampleRateFromIndex } from './config/effects' import { engineSettings, effectSettings, lfoSettings, type LFOConfig } from './stores/settings' +import { synthesisMode, setSynthesisMode } from './stores/synthesisMode' import { exitMappingMode } from './stores/mappingMode' import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts' import { useTileParams } from './hooks/useTileParams' @@ -24,9 +26,12 @@ import { getTileId, getTileFromGrid, type FocusedTile } from './utils/tileHelper function App() { const engineValues = useStore(engineSettings) const effectValues = useStore(effectSettings) + const mode = useStore(synthesisMode) const [tiles, setTiles] = useState(() => - 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(null) const [queued, setQueued] = useState(null) @@ -81,10 +86,32 @@ function App() { const handleRandom = () => { 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) } + 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 = () => { clearSwitchTimer() let newRandomized: TileState | null = null @@ -145,6 +172,7 @@ function App() { if (!playbackManagerRef.current) { playbackManagerRef.current = new PlaybackManager({ sampleRate, duration }) + playbackManagerRef.current.setMode(mode) } else { await playbackManagerRef.current.updateOptions({ sampleRate, duration }) } @@ -158,6 +186,10 @@ function App() { engineValues.d ?? DEFAULT_VARIABLES.d ) 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) setPlaying(id) @@ -206,6 +238,14 @@ function App() { 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) { const updatedValues = { ...engineValues, [parameterId]: value } playbackManagerRef.current.setVariables( @@ -324,8 +364,15 @@ function App() { } const handleRegenerate = (row: number, col: number) => { - const newFormula = generateRandomFormula(engineValues.complexity) - const newTile = createTileStateFromCurrent(newFormula) + let newTile: TileState + + if (mode === 'fm') { + const patch = generateRandomFMPatch(engineValues.complexity) + newTile = createFMTileState(patch) + } else { + const newFormula = generateRandomFormula(engineValues.complexity) + newTile = createTileStateFromCurrent(newFormula) + } setTiles(prevTiles => { const newTiles = [...prevTiles] @@ -547,37 +594,61 @@ function App() { > BRUITISTE -
- - - +
+
+ + +
+
+ + + +
@@ -635,12 +706,36 @@ function App() { {/* Desktop header */}
-

setShowHelp(true)} - className="font-mono text-sm tracking-[0.3em] text-white flex-shrink-0 cursor-pointer hover:opacity-70 transition-opacity" - > - BRUITISTE -

+
+

setShowHelp(true)} + className="font-mono text-sm tracking-[0.3em] text-white cursor-pointer hover:opacity-70 transition-opacity" + > + BRUITISTE +

+
+ + +
+
-
-
- + {mode === 'bytebeat' && ( +
+
+ +
-
+ )}
{tiles.map((row, i) => diff --git a/src/components/BytebeatTile.tsx b/src/components/BytebeatTile.tsx index bef8417a..7670f851 100644 --- a/src/components/BytebeatTile.tsx +++ b/src/components/BytebeatTile.tsx @@ -1,6 +1,11 @@ import { useRef, useEffect } from 'react' +import { useStore } from '@nanostores/react' import { Download, Dices } from 'lucide-react' 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 { 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) { const canvasRef = useRef(null) + const mode = useStore(synthesisMode) useEffect(() => { const canvas = canvasRef.current @@ -31,10 +37,19 @@ export function BytebeatTile({ formula, row, col, isPlaying, isQueued, isFocused canvas.width = rect.width * 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)' drawWaveform(canvas, waveformData, color) - }, [formula, isPlaying, isQueued, a, b, c, d]) + }, [formula, isPlaying, isQueued, a, b, c, d, mode]) const handleDownload = (e: React.MouseEvent) => { e.stopPropagation() @@ -66,7 +81,11 @@ export function BytebeatTile({ formula, row, col, isPlaying, isQueued, isFocused /> )}
- {formula} + {mode === 'fm' ? (() => { + const fmPatch = parseFMPatch(formula) + const algorithmName = getAlgorithmName(fmPatch?.algorithm ?? 0) + return `${algorithmName} [${a},${b},${c},${d}] FB:${fmPatch?.feedback ?? 0}` + })() : formula}
{ switch (id) { case 'sampleRate': @@ -23,6 +27,8 @@ export function EngineControls({ values, onChange, onMapClick, getMappedLFOs, sh return getComplexityLabel(value) case 'bitDepth': return getBitDepthLabel(value) + case 'fmAlgorithm': + return getAlgorithmLabel(value) default: { const param = ENGINE_CONTROLS[0].parameters.find(p => p.id === id) return `${value}${param?.unit || ''}` @@ -35,6 +41,9 @@ export function EngineControls({ values, onChange, onMapClick, getMappedLFOs, sh {ENGINE_CONTROLS[0].parameters.map(param => { 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 (showOnlyKnobs && !useKnob) return null diff --git a/src/config/effects.ts b/src/config/effects.ts index 30d28a5d..ebd7c7c3 100644 --- a/src/config/effects.ts +++ b/src/config/effects.ts @@ -94,6 +94,24 @@ export const ENGINE_CONTROLS: EffectConfig[] = [ default: 64, step: 1, 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: '%' } ] } diff --git a/src/config/fmAlgorithms.ts b/src/config/fmAlgorithms.ts new file mode 100644 index 00000000..2eb787a6 --- /dev/null +++ b/src/config/fmAlgorithms.ts @@ -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}` +} diff --git a/src/domain/audio/AudioPlayer.ts b/src/domain/audio/AudioPlayer.ts index 0458bc62..49bce661 100644 --- a/src/domain/audio/AudioPlayer.ts +++ b/src/domain/audio/AudioPlayer.ts @@ -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 { 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 diff --git a/src/domain/audio/effects/FMSourceEffect.ts b/src/domain/audio/effects/FMSourceEffect.ts new file mode 100644 index 00000000..c9b8c25e --- /dev/null +++ b/src/domain/audio/effects/FMSourceEffect.ts @@ -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 { + 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() + } +} diff --git a/src/services/PlaybackManager.ts b/src/services/PlaybackManager.ts index b9d1c2fb..363a99dc 100644 --- a/src/services/PlaybackManager.ts +++ b/src/services/PlaybackManager.ts @@ -1,6 +1,7 @@ import { AudioPlayer } from '../domain/audio/AudioPlayer' import type { LFOWaveform } from '../domain/modulation/LFO' import type { EffectValues } from '../types/effects' +import type { SynthesisMode } from '../stores/synthesisMode' import { DEFAULT_VARIABLES } from '../constants/defaults' export interface PlaybackOptions { @@ -40,6 +41,18 @@ export class PlaybackManager { 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 { this.playbackPositionCallback = callback } diff --git a/src/stores/synthesisMode.ts b/src/stores/synthesisMode.ts new file mode 100644 index 00000000..2d0c641e --- /dev/null +++ b/src/stores/synthesisMode.ts @@ -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(loadFromStorage()) + +export function setSynthesisMode(mode: SynthesisMode): void { + synthesisMode.set(mode) + try { + localStorage.setItem(STORAGE_KEY, mode) + } catch { + // Silently fail on storage errors + } +} diff --git a/src/utils/fmPatches.ts b/src/utils/fmPatches.ts new file mode 100644 index 00000000..394b70e9 --- /dev/null +++ b/src/utils/fmPatches.ts @@ -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 +} diff --git a/src/utils/fmWaveformGenerator.ts b/src/utils/fmWaveformGenerator.ts new file mode 100644 index 00000000..ef85fbfc --- /dev/null +++ b/src/utils/fmWaveformGenerator.ts @@ -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 +} diff --git a/src/utils/formatters.ts b/src/utils/formatters.ts index 39c85822..9a8e1536 100644 --- a/src/utils/formatters.ts +++ b/src/utils/formatters.ts @@ -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) } \ No newline at end of file