Skip to content

Commit 87be290

Browse files
authored
Merge pull request #42 from sliterok/codex/implement-fft-to-ripple-led-visualization
Add FFT ripple mode
2 parents 3fed527 + df32990 commit 87be290

File tree

5 files changed

+96
-1
lines changed

5 files changed

+96
-1
lines changed

src/backend/pattern/fftRipple.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { IColorGetter, IColorMapper, IArrColor } from 'src/typings'
2+
import { callIndexedGetter } from './mappers'
3+
import { pixelsCount, hueToColor } from '../shared'
4+
import { settings } from 'src/settings'
5+
import { audioState } from '../wsAudio'
6+
7+
interface Ripple {
8+
pos: number
9+
radius: number
10+
brightness: number
11+
hue: number
12+
}
13+
14+
let ripples: Ripple[] = []
15+
let lastTime = Date.now()
16+
let averages: number[] = []
17+
const attenuation = 0.9
18+
const speed = 60
19+
20+
function spawnRipple(bin: number, mag: number) {
21+
const hue = (bin / audioState.bins.length) * 270
22+
ripples.push({
23+
pos: (bin / audioState.bins.length) * pixelsCount,
24+
radius: 1,
25+
brightness: Math.min(1, mag),
26+
hue,
27+
})
28+
}
29+
30+
function update(time: number) {
31+
const dt = (time - lastTime) * settings.effectSpeed
32+
lastTime = time
33+
const bins = audioState.bins
34+
if (bins && bins.length) {
35+
if (averages.length !== bins.length) averages = bins.slice()
36+
for (let i = 0; i < bins.length; i++) {
37+
const m = bins[i]
38+
averages[i] = averages[i] * 0.9 + m * 0.1
39+
if (m > averages[i] * 1.5) spawnRipple(i, m)
40+
}
41+
}
42+
ripples.forEach(r => {
43+
r.radius += (speed * dt) / 1000
44+
r.brightness *= Math.pow(attenuation, dt / 16)
45+
})
46+
ripples = ripples.filter(r => r.brightness > 0.05)
47+
}
48+
49+
export const getFftRippleColor: IColorGetter = (index, time) => {
50+
if (index === 0) update(time)
51+
let r = 0
52+
let g = 0
53+
let b = 0
54+
for (const ripple of ripples) {
55+
const dist = Math.abs(index - ripple.pos)
56+
if (dist <= ripple.radius) {
57+
const intensity = ripple.brightness * (1 - dist / ripple.radius)
58+
const { r: rr, g: gg, b: bb } = hueToColor(ripple.hue).rgb()
59+
r += rr * intensity
60+
g += gg * intensity
61+
b += bb * intensity
62+
}
63+
}
64+
return [Math.min(255, Math.round(r)), Math.min(255, Math.round(g)), Math.min(255, Math.round(b))]
65+
}
66+
67+
export function resetFftRipples() {
68+
ripples = []
69+
lastTime = Date.now()
70+
averages = []
71+
}
72+
73+
export const fftRippleMapper: IColorMapper = () => callIndexedGetter(getFftRippleColor)

src/backend/pattern/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { getWaveColor } from './wave'
1010
import { getHeartbeatColor, getStrobeColor, getPulseColor, getGradientPulseColor, getMultiPulseColor } from './extra'
1111
import { rippleMapper } from './ripple'
1212
import { musicRippleMapper } from './musicRipple'
13+
import { fftRippleMapper } from './fftRipple'
1314
import { createIndexedMapper, createFlatMapper } from './mappers'
1415

1516
const transitionDuration = 250
@@ -39,6 +40,7 @@ const mappers: Record<IMode, IColorMapper> = {
3940
[IMode.MultiPulse]: createIndexedMapper(getMultiPulseColor),
4041
[IMode.Ripple]: rippleMapper,
4142
[IMode.MusicRipple]: musicRippleMapper,
43+
[IMode.FftRipple]: fftRippleMapper,
4244
}
4345

4446
export function getPixels(mode: IMode): IArrColor[][] {

src/backend/wsAudio.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ export interface AudioState {
66
hue: number
77
level: number
88
freq: number
9+
bins: number[]
910
}
1011

11-
export const audioState: AudioState = { hue: 0, level: 0, freq: 0 }
12+
export const audioState: AudioState = { hue: 0, level: 0, freq: 0, bins: [] }
1213

1314
export function startAudioServer(port = 8081) {
1415
const wss = new WebSocketServer({ port })
@@ -24,6 +25,7 @@ export function processAudio(buffer: Buffer, sampleRate = 44100) {
2425
const input = Array.from(samples, s => s / 32768)
2526
const spectrum = fft(input)
2627
const mags = util.fftMag(spectrum)
28+
audioState.bins = mags.slice(0, mags.length / 2)
2729
let max = 0
2830
let idx = 0
2931
for (let i = 1; i < mags.length / 2; i++) {

src/typings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export enum IMode {
2727
MultiPulse,
2828
Ripple,
2929
MusicRipple,
30+
FftRipple,
3031
}
3132

3233
export interface ISettings {

tests/fftRipple.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { getFftRippleColor, resetFftRipples } from '../src/backend/pattern/fftRipple'
2+
import { audioState } from '../src/backend/wsAudio'
3+
4+
beforeEach(() => {
5+
resetFftRipples()
6+
audioState.bins = Array(8).fill(0)
7+
})
8+
9+
describe('fft ripple pattern', () => {
10+
test('spawns ripple on spike', () => {
11+
audioState.bins[0] = 2
12+
;(getFftRippleColor as any)(0, 0)
13+
audioState.bins[0] = 10
14+
const color = (getFftRippleColor as any)(0, 16)
15+
expect(color).not.toEqual([0, 0, 0])
16+
})
17+
})

0 commit comments

Comments
 (0)