diff --git a/src/backend/pattern/fftRipple.ts b/src/backend/pattern/fftRipple.ts new file mode 100644 index 0000000..df5ca29 --- /dev/null +++ b/src/backend/pattern/fftRipple.ts @@ -0,0 +1,73 @@ +import { IColorGetter, IColorMapper, IArrColor } from 'src/typings' +import { callIndexedGetter } from './mappers' +import { pixelsCount, hueToColor } from '../shared' +import { settings } from 'src/settings' +import { audioState } from '../wsAudio' + +interface Ripple { + pos: number + radius: number + brightness: number + hue: number +} + +let ripples: Ripple[] = [] +let lastTime = Date.now() +let averages: number[] = [] +const attenuation = 0.9 +const speed = 60 + +function spawnRipple(bin: number, mag: number) { + const hue = (bin / audioState.bins.length) * 270 + ripples.push({ + pos: (bin / audioState.bins.length) * pixelsCount, + radius: 1, + brightness: Math.min(1, mag), + hue, + }) +} + +function update(time: number) { + const dt = (time - lastTime) * settings.effectSpeed + lastTime = time + const bins = audioState.bins + if (bins && bins.length) { + if (averages.length !== bins.length) averages = bins.slice() + for (let i = 0; i < bins.length; i++) { + const m = bins[i] + averages[i] = averages[i] * 0.9 + m * 0.1 + if (m > averages[i] * 1.5) spawnRipple(i, m) + } + } + ripples.forEach(r => { + r.radius += (speed * dt) / 1000 + r.brightness *= Math.pow(attenuation, dt / 16) + }) + ripples = ripples.filter(r => r.brightness > 0.05) +} + +export const getFftRippleColor: IColorGetter = (index, time) => { + if (index === 0) update(time) + let r = 0 + let g = 0 + let b = 0 + for (const ripple of ripples) { + const dist = Math.abs(index - ripple.pos) + if (dist <= ripple.radius) { + const intensity = ripple.brightness * (1 - dist / ripple.radius) + const { r: rr, g: gg, b: bb } = hueToColor(ripple.hue).rgb() + r += rr * intensity + g += gg * intensity + b += bb * intensity + } + } + return [Math.min(255, Math.round(r)), Math.min(255, Math.round(g)), Math.min(255, Math.round(b))] +} + +export function resetFftRipples() { + ripples = [] + lastTime = Date.now() + averages = [] +} + +export const fftRippleMapper: IColorMapper = () => callIndexedGetter(getFftRippleColor) diff --git a/src/backend/pattern/index.ts b/src/backend/pattern/index.ts index 30a3cf9..7168c85 100644 --- a/src/backend/pattern/index.ts +++ b/src/backend/pattern/index.ts @@ -10,6 +10,7 @@ import { getWaveColor } from './wave' import { getHeartbeatColor, getStrobeColor, getPulseColor, getGradientPulseColor, getMultiPulseColor } from './extra' import { rippleMapper } from './ripple' import { musicRippleMapper } from './musicRipple' +import { fftRippleMapper } from './fftRipple' import { createIndexedMapper, createFlatMapper } from './mappers' const transitionDuration = 250 @@ -39,6 +40,7 @@ const mappers: Record = { [IMode.MultiPulse]: createIndexedMapper(getMultiPulseColor), [IMode.Ripple]: rippleMapper, [IMode.MusicRipple]: musicRippleMapper, + [IMode.FftRipple]: fftRippleMapper, } export function getPixels(mode: IMode): IArrColor[][] { diff --git a/src/backend/wsAudio.ts b/src/backend/wsAudio.ts index a417113..7b5f268 100644 --- a/src/backend/wsAudio.ts +++ b/src/backend/wsAudio.ts @@ -6,9 +6,10 @@ export interface AudioState { hue: number level: number freq: number + bins: number[] } -export const audioState: AudioState = { hue: 0, level: 0, freq: 0 } +export const audioState: AudioState = { hue: 0, level: 0, freq: 0, bins: [] } export function startAudioServer(port = 8081) { const wss = new WebSocketServer({ port }) @@ -24,6 +25,7 @@ export function processAudio(buffer: Buffer, sampleRate = 44100) { const input = Array.from(samples, s => s / 32768) const spectrum = fft(input) const mags = util.fftMag(spectrum) + audioState.bins = mags.slice(0, mags.length / 2) let max = 0 let idx = 0 for (let i = 1; i < mags.length / 2; i++) { diff --git a/src/typings.ts b/src/typings.ts index 598e80a..6f0827b 100644 --- a/src/typings.ts +++ b/src/typings.ts @@ -27,6 +27,7 @@ export enum IMode { MultiPulse, Ripple, MusicRipple, + FftRipple, } export interface ISettings { diff --git a/tests/fftRipple.test.ts b/tests/fftRipple.test.ts new file mode 100644 index 0000000..c15e164 --- /dev/null +++ b/tests/fftRipple.test.ts @@ -0,0 +1,17 @@ +import { getFftRippleColor, resetFftRipples } from '../src/backend/pattern/fftRipple' +import { audioState } from '../src/backend/wsAudio' + +beforeEach(() => { + resetFftRipples() + audioState.bins = Array(8).fill(0) +}) + +describe('fft ripple pattern', () => { + test('spawns ripple on spike', () => { + audioState.bins[0] = 2 + ;(getFftRippleColor as any)(0, 0) + audioState.bins[0] = 10 + const color = (getFftRippleColor as any)(0, 16) + expect(color).not.toEqual([0, 0, 0]) + }) +})