nodejs-audio-visualizer 2.1.4 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -6
- package/dist/audio.d.ts +1 -2
- package/dist/audio.js +40 -30
- package/dist/audio.js.map +1 -1
- package/dist/config.d.ts +19 -5
- package/dist/config.js +103 -11
- package/dist/config.js.map +1 -1
- package/dist/dsp.d.ts +15 -1
- package/dist/dsp.js +58 -7
- package/dist/dsp.js.map +1 -1
- package/dist/image.d.ts +28 -2
- package/dist/image.js +255 -18
- package/dist/image.js.map +1 -1
- package/dist/index.d.ts +12 -1
- package/dist/index.js +63 -55
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
- package/src/audio.ts +35 -33
- package/src/config.ts +122 -12
- package/src/dsp.ts +76 -7
- package/src/image.ts +347 -22
- package/src/index.ts +100 -31
- package/src/tests/audio.test.ts +5 -14
- package/src/tests/config.test.ts +6 -8
- package/src/tests/dsp.test.ts +3 -4
- package/src/tests/image.test.ts +104 -11
- package/src/tests/index.test.ts +1 -1
- package/src/typings.d.ts +7 -0
- package/dist/spectrum.d.ts +0 -1
- package/dist/spectrum.js +0 -72
- package/dist/spectrum.js.map +0 -1
- package/src/spectrum.ts +0 -91
- package/src/tests/snapshots/frameSnapshotSpectrumDown.json +0 -1
- package/src/tests/snapshots/frameSnapshotSpectrumUp.json +0 -1
- package/src/tests/snapshots/frameSnapshotSpectrumUpOpacity50.json +0 -1
package/src/audio.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
2
|
import ffmpegPath from 'ffmpeg-static';
|
|
3
|
-
import { getSpectrum } from './dsp';
|
|
3
|
+
import { FREQUENCY_BANDS, getSpectrum } from './dsp';
|
|
4
4
|
|
|
5
5
|
export const skipEvery = <T>(skipIndex: number) => (element: T, index: number) =>
|
|
6
6
|
index % skipIndex === 0;
|
|
@@ -34,44 +34,46 @@ export const correctPeaks = (spectrums: number[], peaks: number[]) => {
|
|
|
34
34
|
return resultSpectrum;
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
const currValue = spectrums[i];
|
|
44
|
-
const currPrevValue = prevSpectrums[i] || 0;
|
|
45
|
-
const avgValue = (currValue + currPrevValue) / 2;
|
|
46
|
-
resultSpectrum.push(avgValue);
|
|
47
|
-
}
|
|
48
|
-
return resultSpectrum;
|
|
37
|
+
const oldMax = 1;
|
|
38
|
+
const newMin = 0.6;
|
|
39
|
+
const getInterpolationFactor = (skippedFramesCount: number, skipFramesCount: number) => {
|
|
40
|
+
const interpolationFactor = 1 - (skippedFramesCount / skipFramesCount);
|
|
41
|
+
const interpolationFactorInRange = (interpolationFactor * (oldMax - newMin)) + newMin;
|
|
42
|
+
return interpolationFactorInRange;
|
|
49
43
|
};
|
|
50
44
|
|
|
51
|
-
export const createSpectrumsProcessor = (
|
|
45
|
+
export const createSpectrumsProcessor = (sampleRate: number, skipFramesCount: number) => {
|
|
52
46
|
let prevAudioDataNormalized: number[] = [];
|
|
53
47
|
let prevPeaks: number[] = [];
|
|
54
|
-
let
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return (frameIndex: number, parseAudioData: () => number[]) => {
|
|
58
|
-
const isFrameSkiped =
|
|
59
|
-
frameIndex && (frameIndex % skipFrameIndex === 0);
|
|
60
|
-
const audioDataNomrmalized = isFrameSkiped ?
|
|
61
|
-
prevAudioDataNormalized :
|
|
62
|
-
normalizeAudioData(parseAudioData());
|
|
63
|
-
prevAudioDataNormalized = audioDataNomrmalized;
|
|
48
|
+
let skippedFramesCount = 0;
|
|
49
|
+
let targetSpectrum: number[] = [];
|
|
50
|
+
let previousSpectrum: number[] = [];
|
|
64
51
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const smoothSpectrum = smoothValues(correctedSpectrum, prevSpectrums);
|
|
71
|
-
prevSpectrums = smoothSpectrum;
|
|
72
|
-
prevPeaks = peaks;
|
|
52
|
+
return (parseAudioData: () => number[]) => {
|
|
53
|
+
if (skippedFramesCount === 0) {
|
|
54
|
+
skippedFramesCount = skipFramesCount;
|
|
55
|
+
const audioDataNomrmalized = normalizeAudioData(parseAudioData());
|
|
56
|
+
prevAudioDataNormalized = audioDataNomrmalized;
|
|
73
57
|
|
|
74
|
-
|
|
58
|
+
const spectrum = getSpectrum(audioDataNomrmalized, sampleRate);
|
|
59
|
+
const peaks = getPeaks(spectrum, prevPeaks);
|
|
60
|
+
const correctedSpectrum =
|
|
61
|
+
prevPeaks.length === 0 ?
|
|
62
|
+
Array(FREQUENCY_BANDS.length).fill(0) :
|
|
63
|
+
correctPeaks(spectrum, peaks);
|
|
64
|
+
|
|
65
|
+
prevPeaks = peaks;
|
|
66
|
+
previousSpectrum = [...targetSpectrum];
|
|
67
|
+
targetSpectrum = correctedSpectrum;
|
|
68
|
+
} else {
|
|
69
|
+
skippedFramesCount--;
|
|
70
|
+
}
|
|
71
|
+
const interpolationFactor = getInterpolationFactor(skippedFramesCount, skipFramesCount);
|
|
72
|
+
const interpolatedSpectrum = targetSpectrum.map((target, i) => {
|
|
73
|
+
const prev = previousSpectrum[i] || target;
|
|
74
|
+
return prev + (target - prev) * interpolationFactor;
|
|
75
|
+
});
|
|
76
|
+
return interpolatedSpectrum;
|
|
75
77
|
};
|
|
76
78
|
};
|
|
77
79
|
|
package/src/config.ts
CHANGED
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import { Config, SpectrumSizeValue, PositionAliasName, RotationAliasName } from './index';
|
|
3
|
+
import { FREQUENCY_BANDS } from './dsp';
|
|
3
4
|
|
|
4
5
|
export const defaults = {
|
|
5
|
-
fps:
|
|
6
|
-
spectrumWidth: '
|
|
7
|
-
spectrumHeight:
|
|
6
|
+
fps: 30,
|
|
7
|
+
spectrumWidth: '33%',
|
|
8
|
+
spectrumHeight: 160,
|
|
8
9
|
spectrumX: 'center',
|
|
9
|
-
spectrumY: '
|
|
10
|
-
spectrumRotation: '
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
spectrumY: 'middle',
|
|
11
|
+
spectrumRotation: 'mirror',
|
|
12
|
+
spectrumBusMargin: 12,
|
|
13
|
+
spectrumOpacity: '80%',
|
|
14
|
+
polarX: 'center',
|
|
15
|
+
polarY: 'middle',
|
|
16
|
+
polarInnerRadius: 100,
|
|
17
|
+
polarMaxBarLength: 160,
|
|
18
|
+
polarBarWidth: 15,
|
|
14
19
|
};
|
|
15
20
|
|
|
16
21
|
type RelativePositionValue = 0 | 0.5 | 1;
|
|
17
22
|
|
|
23
|
+
export type SpectrumEffect = 'volume' | 'smooth';
|
|
24
|
+
|
|
18
25
|
interface PositionAlias {
|
|
19
26
|
name: PositionAliasName;
|
|
20
27
|
value: RelativePositionValue;
|
|
@@ -29,7 +36,7 @@ const postitionAliases: PositionAlias[] = [
|
|
|
29
36
|
{ name: 'bottom', value: 1 },
|
|
30
37
|
];
|
|
31
38
|
|
|
32
|
-
export const rotationAliasValues = ['up', 'down'] as const;
|
|
39
|
+
export const rotationAliasValues = ['up', 'down', 'mirror'] as const;
|
|
33
40
|
|
|
34
41
|
export const checkIsPercentValue = (value: string) =>
|
|
35
42
|
RegExp(/\d+%/).test(value)
|
|
@@ -46,14 +53,15 @@ export const getOutVideoPath = (config: Config) =>
|
|
|
46
53
|
export const getFPS = (config: Config) =>
|
|
47
54
|
config.outVideo.fps || defaults.fps;
|
|
48
55
|
|
|
49
|
-
export const getSpectrumBusesCount = () => defaults.spectrumBusesCount;
|
|
50
|
-
|
|
51
56
|
export const getSpectrumBusMargin = () => defaults.spectrumBusMargin;
|
|
52
57
|
|
|
53
58
|
const getSpectrumWidth = (config: Config) =>
|
|
54
59
|
(config.outVideo.spectrum && config.outVideo.spectrum.width) ||
|
|
55
60
|
defaults.spectrumWidth;
|
|
56
61
|
|
|
62
|
+
export const getSpectrumEffect = (config: Config) =>
|
|
63
|
+
config.outVideo.spectrum?.effect;
|
|
64
|
+
|
|
57
65
|
const getSpectrumSizeAbsolute = (
|
|
58
66
|
spectrumSize: SpectrumSizeValue,
|
|
59
67
|
backgroundImageSize: number,
|
|
@@ -81,7 +89,7 @@ export const getSpectrumWidthAbsolute = (
|
|
|
81
89
|
const spectrumWidth = getSpectrumWidth(config);
|
|
82
90
|
const spectrumWidthAbsolute = getSpectrumSizeAbsolute(spectrumWidth, backgroundImageWidth, 'width');
|
|
83
91
|
const spectrumWidthWithoutMargin =
|
|
84
|
-
Math.trunc(spectrumWidthAbsolute /
|
|
92
|
+
Math.trunc(spectrumWidthAbsolute / FREQUENCY_BANDS.length) - defaults.spectrumBusMargin / 2;
|
|
85
93
|
if (spectrumWidthWithoutMargin <= 0) {
|
|
86
94
|
throw new Error(`Spectrum width '${spectrumWidth}' is too small.`);
|
|
87
95
|
}
|
|
@@ -240,3 +248,105 @@ export const getFfmpeg_preset = (config: Config) =>
|
|
|
240
248
|
export const getFrame_processing_delay = (config: Config) =>
|
|
241
249
|
config.tweaks && config.tweaks.frame_processing_delay;
|
|
242
250
|
|
|
251
|
+
// Polar visualizer config getters
|
|
252
|
+
const getPolarX = (config: Config) =>
|
|
253
|
+
parseNumberIfPossible(
|
|
254
|
+
(config.outVideo.polar && config.outVideo.polar.x) ||
|
|
255
|
+
defaults.polarX
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
const getPolarY = (config: Config) =>
|
|
259
|
+
parseNumberIfPossible(
|
|
260
|
+
(config.outVideo.polar && config.outVideo.polar.y) ||
|
|
261
|
+
defaults.polarY
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
export const getPolarXAbsolute = (
|
|
265
|
+
config: Config,
|
|
266
|
+
backgroundImageWidth: number
|
|
267
|
+
) => {
|
|
268
|
+
const polarX = getPolarX(config);
|
|
269
|
+
if (typeof polarX !== 'string') {
|
|
270
|
+
return polarX;
|
|
271
|
+
}
|
|
272
|
+
try {
|
|
273
|
+
const polarXParsed = parsePositionAlias(polarX);
|
|
274
|
+
if (polarXParsed.value === 0) {
|
|
275
|
+
const polarLength = getPolarMaxBarLength(config);
|
|
276
|
+
const polarRadius = getPolarInnerRadius(config);
|
|
277
|
+
return polarRadius + polarLength;
|
|
278
|
+
}
|
|
279
|
+
if (polarXParsed.value === 1) {
|
|
280
|
+
const polarLength = getPolarMaxBarLength(config);
|
|
281
|
+
const polarRadius = getPolarInnerRadius(config);
|
|
282
|
+
return backgroundImageWidth - polarRadius - polarLength;
|
|
283
|
+
}
|
|
284
|
+
return polarXParsed.value * backgroundImageWidth;
|
|
285
|
+
} catch {
|
|
286
|
+
throw new Error(`Invalid polar x value: ${polarX}. Valid values: ${getValidPositionAliasValues()}.`);
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
export const getPolarYAbsolute = (
|
|
291
|
+
config: Config,
|
|
292
|
+
backgroundImageHeight: number
|
|
293
|
+
) => {
|
|
294
|
+
const polarY = getPolarY(config);
|
|
295
|
+
if (typeof polarY !== 'string') {
|
|
296
|
+
return polarY;
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
const polarYParsed = parsePositionAlias(polarY);
|
|
300
|
+
if (polarYParsed.value === 0) {
|
|
301
|
+
const polarLength = getPolarMaxBarLength(config);
|
|
302
|
+
const polarRadius = getPolarInnerRadius(config);
|
|
303
|
+
return polarRadius + polarLength;
|
|
304
|
+
}
|
|
305
|
+
if (polarYParsed.value === 1) {
|
|
306
|
+
const polarLength = getPolarMaxBarLength(config);
|
|
307
|
+
const polarRadius = getPolarInnerRadius(config);
|
|
308
|
+
return backgroundImageHeight - polarRadius - polarLength;
|
|
309
|
+
}
|
|
310
|
+
return polarYParsed.value * backgroundImageHeight;
|
|
311
|
+
} catch {
|
|
312
|
+
throw new Error(`Invalid polar y value: ${polarY}. Valid values: ${getValidPositionAliasValues()}.`);
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
export const getPolarInnerRadius = (config: Config) =>
|
|
317
|
+
(config.outVideo.polar && config.outVideo.polar.innerRadius) ||
|
|
318
|
+
defaults.polarInnerRadius;
|
|
319
|
+
|
|
320
|
+
export const getPolarMaxBarLength = (config: Config) =>
|
|
321
|
+
(config.outVideo.polar && config.outVideo.polar.maxBarLength) ||
|
|
322
|
+
defaults.polarMaxBarLength;
|
|
323
|
+
|
|
324
|
+
export const getPolarBarWidth = (config: Config) =>
|
|
325
|
+
(config.outVideo.polar && config.outVideo.polar.barWidth) ||
|
|
326
|
+
defaults.polarBarWidth;
|
|
327
|
+
|
|
328
|
+
export const getPolarEffect = (config: Config) =>
|
|
329
|
+
config.outVideo.polar?.effect;
|
|
330
|
+
|
|
331
|
+
export const getPolarColor = (config: Config) =>
|
|
332
|
+
config.outVideo.polar && config.outVideo.polar.color;
|
|
333
|
+
|
|
334
|
+
export const getPolarOpacity = (config: Config) =>
|
|
335
|
+
(config.outVideo.polar && config.outVideo.polar.opacity) ||
|
|
336
|
+
defaults.spectrumOpacity;
|
|
337
|
+
|
|
338
|
+
export const getPolarOpacityParsed = (config: Config) => {
|
|
339
|
+
const polarOpacity = getPolarOpacity(config);
|
|
340
|
+
if (!checkIsPercentValue(polarOpacity)) {
|
|
341
|
+
throw new Error(`Invalid polar opacity value: '${polarOpacity}'. Use string percent value, for example '80%'.`);
|
|
342
|
+
}
|
|
343
|
+
const percentValue = parseInt(polarOpacity);
|
|
344
|
+
if (
|
|
345
|
+
(percentValue < 0) ||
|
|
346
|
+
(percentValue > 100)
|
|
347
|
+
) {
|
|
348
|
+
throw new Error(`Invalid polar opacity value: '${polarOpacity}'. Percent values must be in range from '0%' to '100%'.`);
|
|
349
|
+
}
|
|
350
|
+
return percentValue / 100;
|
|
351
|
+
};
|
|
352
|
+
|
package/src/dsp.ts
CHANGED
|
@@ -1,4 +1,62 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { fft, util } from 'fft-js';
|
|
2
|
+
|
|
3
|
+
export interface SpectrumPoint {
|
|
4
|
+
frequency: number;
|
|
5
|
+
magnitude: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface FrequencyBus {
|
|
9
|
+
startFreq: number;
|
|
10
|
+
endFreq: number;
|
|
11
|
+
magnitude: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Common frequency bands (in Hz)
|
|
15
|
+
export const FREQUENCY_BANDS = [
|
|
16
|
+
{ name: 'Sub Bass 1', start: 20, end: 30 },
|
|
17
|
+
{ name: 'Sub Bass 2', start: 30, end: 40 },
|
|
18
|
+
{ name: 'Sub Bass 3', start: 50, end: 60 },
|
|
19
|
+
{ name: 'Sub Bass 4', start: 20, end: 60 },
|
|
20
|
+
{ name: 'Bass 1', start: 60, end: 107.5 },
|
|
21
|
+
{ name: 'Bass 2', start: 107.5, end: 155 },
|
|
22
|
+
{ name: 'Bass 3', start: 155, end: 202.5 },
|
|
23
|
+
{ name: 'Bass 4', start: 202.5, end: 250 },
|
|
24
|
+
{ name: 'Low Mids 1', start: 250, end: 312.5 },
|
|
25
|
+
{ name: 'Low Mids 2', start: 312.5, end: 375 },
|
|
26
|
+
{ name: 'Low Mids 3', start: 375, end: 437.5 },
|
|
27
|
+
{ name: 'Low Mids 4', start: 437.5, end: 500 },
|
|
28
|
+
{ name: 'Mids 1', start: 500, end: 875 },
|
|
29
|
+
{ name: 'Mids 2', start: 875, end: 1250 },
|
|
30
|
+
{ name: 'Mids 3', start: 1250, end: 1625 },
|
|
31
|
+
{ name: 'Mids 4', start: 1625, end: 2000 },
|
|
32
|
+
{ name: 'High Mids 1', start: 2000, end: 2500 },
|
|
33
|
+
{ name: 'High Mids 2', start: 2500, end: 3000 },
|
|
34
|
+
{ name: 'High Mids 3', start: 3000, end: 3500 },
|
|
35
|
+
{ name: 'High Mids 4', start: 3500, end: 4000 },
|
|
36
|
+
{ name: 'Presence 1', start: 4000, end: 5000 },
|
|
37
|
+
{ name: 'Presence 2', start: 5000, end: 6000 },
|
|
38
|
+
{ name: 'Brilliance 1', start: 6000, end: 13000 },
|
|
39
|
+
{ name: 'Brilliance 2', start: 13000, end: 20000 },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const combineToBuses = (spectrum: SpectrumPoint[]): number[] => {
|
|
43
|
+
return FREQUENCY_BANDS.map(band => {
|
|
44
|
+
const pointsInBand = spectrum.filter(point =>
|
|
45
|
+
point.frequency >= band.start && point.frequency < band.end
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// If no points found in this band, return zero magnitude
|
|
49
|
+
if (pointsInBand.length === 0) {
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Average the magnitudes in this frequency band
|
|
54
|
+
const avgMagnitude = pointsInBand.reduce((sum, point) =>
|
|
55
|
+
sum + point.magnitude, 0) / pointsInBand.length;
|
|
56
|
+
|
|
57
|
+
return avgMagnitude;
|
|
58
|
+
});
|
|
59
|
+
};
|
|
2
60
|
|
|
3
61
|
export const brutForceFFTSignalLength = (PCMDataLength: number) => {
|
|
4
62
|
let exponent = 1;
|
|
@@ -11,16 +69,27 @@ export const brutForceFFTSignalLength = (PCMDataLength: number) => {
|
|
|
11
69
|
}
|
|
12
70
|
};
|
|
13
71
|
|
|
14
|
-
export const getSpectrum = (PCMData: number[]) => {
|
|
72
|
+
export const getSpectrum = (PCMData: number[], sampleRate: number): number[] => {
|
|
15
73
|
const signalLength = brutForceFFTSignalLength(PCMData.length);
|
|
16
74
|
const signalStartIndex = Math.trunc(PCMData.length / 2 - signalLength / 2);
|
|
17
75
|
const signal = PCMData.slice(signalStartIndex, signalStartIndex + signalLength);
|
|
18
76
|
|
|
19
|
-
|
|
20
|
-
const
|
|
77
|
+
// Use fft-js to compute the FFT
|
|
78
|
+
const phasors = fft(signal);
|
|
79
|
+
const frequencies = util.fftFreq(phasors, sampleRate);
|
|
80
|
+
const magnitudes = util.fftMag(phasors);
|
|
81
|
+
|
|
82
|
+
// Create combined spectrum array with both frequency and magnitude
|
|
83
|
+
let spectrum: SpectrumPoint[] = frequencies.map((freq: number, i: number) => ({
|
|
84
|
+
frequency: freq,
|
|
85
|
+
magnitude: magnitudes[i]
|
|
86
|
+
}));
|
|
87
|
+
|
|
88
|
+
// Apply frequency-dependent scaling
|
|
21
89
|
for (let i = 0; i < spectrum.length; i++) {
|
|
22
|
-
|
|
23
|
-
spectrum[i] *= -1 * Math.log((signalLength / 2 - i) * (0.5 / signalLength / 2)) * signalLength;
|
|
90
|
+
spectrum[i].magnitude *= -1 * Math.log((signalLength / 2 - i) * (0.5 / signalLength / 2)) * signalLength;
|
|
24
91
|
}
|
|
25
|
-
|
|
92
|
+
|
|
93
|
+
// Combine frequencies into buses
|
|
94
|
+
return combineToBuses(spectrum);
|
|
26
95
|
}
|