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/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
- export const smoothValues = (spectrums: number[], prevSpectrums?: number[]) => {
38
- if (!prevSpectrums) {
39
- return spectrums;
40
- }
41
- const resultSpectrum: number[] = [];
42
- for(let i = 0; i < spectrums.length; i++) {
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 = (busesCount: number) => {
45
+ export const createSpectrumsProcessor = (sampleRate: number, skipFramesCount: number) => {
52
46
  let prevAudioDataNormalized: number[] = [];
53
47
  let prevPeaks: number[] = [];
54
- let prevSpectrums: number[] = [];
55
- const skipFrameIndex = 2;
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
- const spectrum = getSpectrum(audioDataNomrmalized);
66
- const skipIndex = Math.trunc(spectrum.length / busesCount);
67
- const spectrumReduced = spectrum.filter(skipEvery(skipIndex));
68
- const peaks = getPeaks(spectrumReduced, prevPeaks);
69
- const correctedSpectrum = correctPeaks(spectrumReduced, peaks);
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
- return smoothSpectrum;
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: 60,
6
- spectrumWidth: '40%',
7
- spectrumHeight: '10%',
6
+ fps: 30,
7
+ spectrumWidth: '33%',
8
+ spectrumHeight: 160,
8
9
  spectrumX: 'center',
9
- spectrumY: 'top',
10
- spectrumRotation: 'down',
11
- spectrumBusesCount: 64,
12
- spectrumBusMargin: 4,
13
- spectrumOpacity: '100%',
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 / defaults.spectrumBusesCount) - defaults.spectrumBusMargin / 2;
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 { createSpectrumAnalyzer } from './spectrum';
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
- const spectrumAnalyzer = createSpectrumAnalyzer(signalLength);
20
- const spectrum = spectrumAnalyzer(signal);
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
- // attenuates low freqs and boosts highs
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
- return spectrum;
92
+
93
+ // Combine frequencies into buses
94
+ return combineToBuses(spectrum);
26
95
  }