nodejs-audio-visualizer 1.0.6 → 1.3.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/dist/video.js CHANGED
@@ -3,12 +3,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  var child_process_1 = require("child_process");
4
4
  var ffmpeg_static_1 = require("ffmpeg-static");
5
5
  exports.spawnFfmpegVideoWriter = function (config) {
6
+ var crf = config.crf || '23';
7
+ var preset = config.preset || 'medium';
6
8
  var args = [
7
9
  '-y',
8
10
  '-i', config.audioFilename,
9
- '-crf', '18',
11
+ '-crf', crf,
10
12
  '-c:a', 'aac', '-b:a', '384k', '-profile:a', 'aac_low',
11
- '-c:v', 'libx264', '-r', "" + config.fps, '-pix_fmt', 'yuv420p', config.videoFileName,
13
+ '-c:v', 'libx264', '-r', "" + config.fps, '-pix_fmt', 'yuv420p', '-preset', preset, config.videoFileName,
12
14
  '-r', "" + config.fps,
13
15
  '-i', '-'
14
16
  ];
package/dist/video.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"video.js","sourceRoot":"","sources":["../src/video.ts"],"names":[],"mappings":";;AACA,+CAAsC;AACtC,+CAAmD;AAStC,QAAA,sBAAsB,GAAG,UAAC,MAA+B;IACpE,IAAM,IAAI,GAAG;QACX,IAAI;QACJ,IAAI,EAAE,MAAM,CAAC,aAAa;QAC1B,MAAM,EAAE,IAAI;QACZ,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,SAAS;QACtD,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,KAAG,MAAM,CAAC,GAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,CAAC,aAAa;QACrF,IAAI,EAAE,KAAG,MAAM,CAAC,GAAK;QACrB,IAAI,EAAE,GAAG;KACV,CAAC;IACF,IAAM,MAAM,GAAG,qBAAK,CAAC,oBAAU,EAAE,IAAI,CAAC,CAAC;IACvC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAClC,IAAI,MAAM,CAAC,QAAQ,EAAE;QACnB,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;KAC3C;IACD,OAAO,MAAM,CAAC;AAChB,CAAC,CAAC;AAEW,QAAA,WAAW,GAAG,UAAC,UAAyC;IACnE,OAAA,UAAC,YAAoB;QACnB,IAAM,WAAW,GAAG,YAAY,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;QACrE,IAAI,CAAC,WAAW,EAAE;YAChB,OAAO;SACR;QACD,IAAM,YAAY,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;QACrC,IAAI,KAAK,CAAC,YAAY,CAAC,EAAE;YACvB,OAAO;SACR;QACD,UAAU,CAAC,YAAY,CAAC,CAAC;IAC3B,CAAC;AAVD,CAUC,CAAC;AAES,QAAA,iBAAiB,GAAG,UAAC,WAAmB,EAAE,QAAmC;IACxF,OAAA,UAAC,YAAoB;QACnB,OAAA,QAAQ,CACN,CAAC,CAAC,YAAY,GAAG,WAAW,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAC/C;IAFD,CAEC;AAHH,CAGG,CAAC;AAEO,QAAA,SAAS,GAAG,UAAC,MAAgB;IACxC,OAAA,IAAI,OAAO,CAAO,UAAA,OAAO,IAAI,OAAA,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,EAA7B,CAA6B,CAAC;AAA3D,CAA2D,CAAC"}
1
+ {"version":3,"file":"video.js","sourceRoot":"","sources":["../src/video.ts"],"names":[],"mappings":";;AACA,+CAAsC;AACtC,+CAAmD;AAWtC,QAAA,sBAAsB,GAAG,UAAC,MAA+B;IACpE,IAAM,GAAG,GAAG,MAAM,CAAC,GAAG,IAAI,IAAI,CAAC;IAC/B,IAAM,MAAM,GAAG,MAAM,CAAC,MAAM,IAAI,QAAQ,CAAC;IACzC,IAAM,IAAI,GAAG;QACX,IAAI;QACJ,IAAI,EAAE,MAAM,CAAC,aAAa;QAC1B,MAAM,EAAE,GAAG;QACX,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,SAAS;QACtD,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,KAAG,MAAM,CAAC,GAAK,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,aAAa;QACxG,IAAI,EAAE,KAAG,MAAM,CAAC,GAAK;QACrB,IAAI,EAAE,GAAG;KACV,CAAC;IACF,IAAM,MAAM,GAAG,qBAAK,CAAC,oBAAU,EAAE,IAAI,CAAC,CAAC;IACvC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAClC,IAAI,MAAM,CAAC,QAAQ,EAAE;QACnB,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;KAC3C;IACD,OAAO,MAAM,CAAC;AAChB,CAAC,CAAC;AAEW,QAAA,WAAW,GAAG,UAAC,UAAyC;IACnE,OAAA,UAAC,YAAoB;QACnB,IAAM,WAAW,GAAG,YAAY,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;QACrE,IAAI,CAAC,WAAW,EAAE;YAChB,OAAO;SACR;QACD,IAAM,YAAY,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;QACrC,IAAI,KAAK,CAAC,YAAY,CAAC,EAAE;YACvB,OAAO;SACR;QACD,UAAU,CAAC,YAAY,CAAC,CAAC;IAC3B,CAAC;AAVD,CAUC,CAAC;AAES,QAAA,iBAAiB,GAAG,UAAC,WAAmB,EAAE,QAAmC;IACxF,OAAA,UAAC,YAAoB;QACnB,OAAA,QAAQ,CACN,CAAC,CAAC,YAAY,GAAG,WAAW,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAC/C;IAFD,CAEC;AAHH,CAGG,CAAC;AAEO,QAAA,SAAS,GAAG,UAAC,MAAgB;IACxC,OAAA,IAAI,OAAO,CAAO,UAAA,OAAO,IAAI,OAAA,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,EAA7B,CAA6B,CAAC;AAA3D,CAA2D,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodejs-audio-visualizer",
3
- "version": "1.0.6",
3
+ "version": "1.3.0",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -53,7 +53,6 @@
53
53
  "@types/ffmpeg-static": "^2.0.0",
54
54
  "@types/node": "^12.12.3",
55
55
  "@types/sinon": "^7.5.0",
56
- "array-smooth": "^1.0.0",
57
56
  "bmp-js": "^0.1.0",
58
57
  "ffmpeg-static": "^2.6.0",
59
58
  "jimp": "^0.9.3"
package/src/audio.ts CHANGED
@@ -1,68 +1,90 @@
1
- /// <reference path="./vendor-typings/array-smooth.d.ts"/>
2
1
  import { spawn } from 'child_process';
3
2
  import ffmpeg from 'ffmpeg-static';
4
3
  import { getSpectrum } from './dsp';
5
- import smooth from 'array-smooth';
6
4
 
7
5
  export const skipEvery = <T>(skipIndex: number) => (element: T, index: number) =>
8
- (index % skipIndex === 0) ? element : null;
6
+ index % skipIndex === 0;
9
7
 
10
- export const getSmoothBuses = (busValues: number[]) =>
11
- smooth(busValues, 2);
12
-
13
- export const smoothSpectrums = (spectrums: number[][], FPS: number) => {
14
- const scaleValues = (values: number[]) => {
15
- const maxValue = Math.max.apply(null, values);
16
- return values.map(value => value / maxValue);
17
- };
18
- const lengths = spectrums.map(spectrum => spectrum.length);
19
- const maxLength = Math.max.apply(null, lengths);
20
- const buses: number[][] = [];
21
- for (let bus = 0; bus < maxLength; bus++) {
22
- buses[bus] = [];
8
+ export const getPeaks = (spectrums: number[], prevPeaks?: number[]) => {
9
+ if (!prevPeaks) {
10
+ return spectrums;
23
11
  }
12
+ const resultPeaks: number[] = [];
13
+ for (let i = 0; i < spectrums.length; i++) {
14
+ const currValue = spectrums[i];
15
+ const currPrevPeak = prevPeaks[i] || 0;
16
+ resultPeaks.push(
17
+ (currValue > currPrevPeak) ? currValue : currPrevPeak
18
+ );
19
+ }
20
+ return resultPeaks;
21
+ };
24
22
 
25
- spectrums.forEach((spectrum) => {
26
- for (let bus = 0; bus < maxLength; bus++) {
27
- buses[bus].push(spectrum[bus] || 0);
23
+ export const correctPeaks = (spectrums: number[], peaks: number[]) => {
24
+ const resultSpectrum: number[] = [];
25
+ for (let i = 0; i < spectrums.length; i++) {
26
+ const value = spectrums[i];
27
+ const peakValue = peaks[i] || 0;
28
+ if (value < 3) {
29
+ resultSpectrum.push(value / 3);
30
+ } else {
31
+ resultSpectrum.push(value / peakValue);
28
32
  }
29
- });
30
-
31
- const smoothBuses = buses.map(bus => getSmoothBuses(scaleValues(bus)));
32
- const smoothSpectrums: number[][] = [];
33
- for (let spectrumIndex = 0; spectrumIndex < spectrums.length; spectrumIndex++) {
34
- smoothSpectrums[spectrumIndex] = [];
35
33
  }
34
+ return resultSpectrum;
35
+ };
36
36
 
37
- for (let spectrumIndex = 0; spectrumIndex < spectrums.length; spectrumIndex++) {
38
- for (let bus = 0; bus < maxLength; bus++) {
39
- smoothSpectrums[spectrumIndex].push(smoothBuses[bus][spectrumIndex]);
40
- }
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);
41
47
  }
42
- return smoothSpectrums;
48
+ return resultSpectrum;
43
49
  };
44
50
 
45
- export const getSmoothSpectrums = (audioData: number[], FPS: number, audioDataStep: number) => {
46
- const spectrums: number[][] = [];
47
- for (let i = 0; i < audioData.length; i += audioDataStep) {
48
- const normalizedAudioFrame = audioData.slice(i, i + audioDataStep);
49
- const spectrum = getSpectrum(normalizedAudioFrame);
51
+ export const createSpectrumsProcessor = (busesCount: number) => {
52
+ let prevAudioDataNormalized: number[] = [];
53
+ let prevPeaks: number[] = [];
54
+ let prevSpectrums: number[] = [];
55
+ const skipFrameIndex = 2;
50
56
 
51
- spectrums.push(spectrum);
52
- }
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;
53
64
 
54
- return smoothSpectrums(spectrums, FPS);
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;
73
+
74
+ return smoothSpectrum;
75
+ };
55
76
  };
56
77
 
57
- export const bufferToUInt8 = (buffer: Buffer) => {
78
+ export const bufferToUInt8 = (buffer: Buffer, start: number, end: number) => {
58
79
  const numbers = [];
59
- for (let i = 0; i < buffer.length; i += 1) {
80
+ for (let i = start; i < end; i += 1) {
60
81
  numbers.push(buffer.readUInt8(i));
61
82
  }
62
83
  return numbers;
63
84
  };
64
85
 
65
- export const normalizeAudioData = (PCMData: number[]) => PCMData.map(num => (num - 128) / 128);
86
+ export const normalizeAudioData = (PCMData: number[]) =>
87
+ PCMData.map(num => (num - 128) / 128);
66
88
 
67
89
  export const spawnFfmpegAudioReader = (filename: string, format: string) => {
68
90
  const ffmpegProcess = spawn(ffmpeg.path, ['-i', filename, '-f', format, '-ac', '1', '-']);
@@ -90,3 +112,4 @@ export const createAudioBuffer = (filename: string, format: string) =>
90
112
  resolve({ audioBuffer, sampleRate });
91
113
  });
92
114
  });
115
+
package/src/config.ts ADDED
@@ -0,0 +1,177 @@
1
+ import path from 'path';
2
+ import { Config, PositionAliasName } from './index';
3
+
4
+ export const defaults = {
5
+ fps: 60,
6
+ spectrumWidth: 0.4,
7
+ spectrumHeight: 0.1,
8
+ spectrumX: 'center',
9
+ spectrumY: 'top'
10
+ };
11
+
12
+ type RelativePositionValue = 0 | 0.5 | 1;
13
+
14
+ interface PositionAlias {
15
+ name: PositionAliasName;
16
+ value: RelativePositionValue;
17
+ }
18
+
19
+ const postitionAliases: PositionAlias[] = [
20
+ { name: 'left', value: 0 },
21
+ { name: 'center', value: 0.5 },
22
+ { name: 'right', value: 1 },
23
+ { name: 'top', value: 0 },
24
+ { name: 'middle', value: 0.5 },
25
+ { name: 'bottom', value: 1 },
26
+ ];
27
+
28
+ const checkIsInt = (num: number) =>
29
+ num % 1 === 0;
30
+
31
+ export const getAudioFilePath = (config: Config) =>
32
+ path.resolve(config.audio.path);
33
+
34
+ export const getBackgroundImagePath = (config: Config) =>
35
+ path.resolve(config.image.path);
36
+
37
+ export const getOutVideoPath = (config: Config) =>
38
+ path.resolve(config.outVideo.path);
39
+
40
+ export const getFPS = (config: Config) =>
41
+ config.outVideo.fps || defaults.fps;
42
+
43
+ const getSpectrumWidth = (config: Config) =>
44
+ (config.outVideo.spectrum && config.outVideo.spectrum.width) ||
45
+ defaults.spectrumWidth;
46
+
47
+ export const getSpectrumWidthAbsolute = (
48
+ config: Config, backgroundImageWidth: number
49
+ ) => {
50
+ const spectrumWidth = getSpectrumWidth(config);
51
+ const isAbsoluteValue = checkIsInt(spectrumWidth);
52
+ if (isAbsoluteValue) {
53
+ return spectrumWidth;
54
+ }
55
+ return backgroundImageWidth * spectrumWidth;
56
+ };
57
+
58
+ const getSpectrumHeight = (config: Config) =>
59
+ (config.outVideo.spectrum && config.outVideo.spectrum.height) ||
60
+ defaults.spectrumHeight;
61
+
62
+ export const getSpectrumHeightAbsolute = (
63
+ config: Config, backgroundImageHeight: number
64
+ ) => {
65
+ const spectrumHeight = getSpectrumHeight(config);
66
+ const isAbsoluteValue = checkIsInt(spectrumHeight);
67
+ if (isAbsoluteValue) {
68
+ return spectrumHeight;
69
+ }
70
+ return backgroundImageHeight * spectrumHeight;
71
+ };
72
+
73
+ const getSpectrumX = (config: Config) =>
74
+ parseNumberIfPossible(
75
+ (config.outVideo.spectrum && config.outVideo.spectrum.x) ||
76
+ defaults.spectrumX
77
+ );
78
+
79
+ const getSpectrumY = (config: Config) =>
80
+ parseNumberIfPossible(
81
+ (config.outVideo.spectrum && config.outVideo.spectrum.y) ||
82
+ defaults.spectrumY
83
+ );
84
+
85
+ const parseNumberIfPossible = (value: string | number) => {
86
+ const parsed = Number(value);
87
+ if (isNaN(parsed)) {
88
+ return value;
89
+ }
90
+ return parsed;
91
+ };
92
+
93
+ const parsePositionAlias = (aliasName: string): PositionAlias => {
94
+ const nameNormalized = aliasName.trim().toLowerCase();
95
+ const alias = postitionAliases.find(
96
+ als => als.name === nameNormalized
97
+ );
98
+ if (!alias) {
99
+ throw new Error();
100
+ }
101
+ return alias;
102
+ };
103
+
104
+ const getValidPositionAliasValues = () =>
105
+ postitionAliases.map(aliasEl => aliasEl.name).join(', ');
106
+
107
+ const getSpectrumAbsolutePosition = (
108
+ relativePosition: RelativePositionValue,
109
+ spectrumSize: number,
110
+ backgroundImageSize: number
111
+ ) => {
112
+ switch (relativePosition) {
113
+ case 0:
114
+ return 0;
115
+ case 0.5:
116
+ return relativePosition * backgroundImageSize - spectrumSize / 2;
117
+ case 1:
118
+ return relativePosition * backgroundImageSize - spectrumSize;
119
+ default:
120
+ throw new Error();
121
+ }
122
+ };
123
+
124
+ export const getSpectrumXAbsolute = (
125
+ config: Config,
126
+ spectrumWidth: number,
127
+ backgroundImageWidth: number
128
+ ) => {
129
+ const spectrumX = getSpectrumX(config);
130
+ if (typeof spectrumX !== 'string') {
131
+ return spectrumX;
132
+ }
133
+ try {
134
+ const spectrumXParsed = parsePositionAlias(spectrumX);
135
+ return getSpectrumAbsolutePosition(
136
+ spectrumXParsed.value,
137
+ spectrumWidth,
138
+ backgroundImageWidth
139
+ );
140
+ } catch {
141
+ throw new Error(`Invalid spectrum x value: ${spectrumX}. Valid values: ${getValidPositionAliasValues()}.`);
142
+ }
143
+ };
144
+
145
+ export const getSpectrumYAbsolute = (
146
+ config: Config,
147
+ spectrumHeight: number,
148
+ backgroundImageHeight: number
149
+ ) => {
150
+ const spectrumY = getSpectrumY(config);
151
+ if (typeof spectrumY !== 'string') {
152
+ return spectrumY;
153
+ }
154
+ try {
155
+ const spectrumXParsed = parsePositionAlias(spectrumY);
156
+ return getSpectrumAbsolutePosition(
157
+ spectrumXParsed.value,
158
+ spectrumHeight,
159
+ backgroundImageHeight
160
+ );
161
+ } catch {
162
+ throw new Error(`Invalid spectrum y value: ${spectrumY}. Valid values: ${getValidPositionAliasValues()}.`);
163
+ }
164
+ };
165
+
166
+ export const getSpectrumColor = (config: Config) =>
167
+ (config.outVideo.spectrum && config.outVideo.spectrum.color);
168
+
169
+ export const getFfmpeg_cfr = (config: Config) =>
170
+ config.tweaks && config.tweaks.ffmpeg_cfr;
171
+
172
+ export const getFfmpeg_preset = (config: Config) =>
173
+ config.tweaks && config.tweaks.ffmpeg_preset;
174
+
175
+ export const getFrame_processing_delay = (config: Config) =>
176
+ config.tweaks && config.tweaks.frame_processing_delay;
177
+
package/src/image.ts CHANGED
@@ -29,9 +29,10 @@ export const drawRect = (imageDstBuffer: BmpDecoder, position: Position, size: S
29
29
  }
30
30
  };
31
31
 
32
- const drawSpectrum = (imageDstBuffer: BmpDecoder, spectrum: number[], size: Size, color: Color) => {
33
- const paddingLeft = Math.trunc(imageDstBuffer.width / 2 - size.width / 2);
32
+ const drawSpectrum = (imageDstBuffer: BmpDecoder, spectrum: number[], size: Size, position: Position, color: Color) => {
34
33
  const busWidth = Math.trunc(size.width / spectrum.length);
34
+ const left = Math.trunc(position.x);
35
+ const top = Math.trunc(position.y);
35
36
  const margin = 4;
36
37
 
37
38
  for (let spectrumX = 0; spectrumX < spectrum.length; spectrumX++) {
@@ -40,17 +41,17 @@ const drawSpectrum = (imageDstBuffer: BmpDecoder, spectrum: number[], size: Size
40
41
  throw new Error('Spectrum values must be in range from 0 to 1');
41
42
  }
42
43
 
43
- const rectX = paddingLeft + busWidth * spectrumX;
44
+ const rectX = left + busWidth * spectrumX;
44
45
  const rectHeight = size.height * spectrumValue;
45
- drawRect(imageDstBuffer, { x: rectX + margin, y: 0 }, { width: busWidth - margin / 2, height: rectHeight }, color);
46
+ drawRect(imageDstBuffer, { x: rectX + margin, y: top }, { width: busWidth - margin / 2, height: rectHeight }, color);
46
47
  }
47
48
  };
48
49
 
49
- export const createVisualizerFrame = (backgroundImageBuffer: BmpDecoder, spectrum: number[], size: Size, color: Color | string) => {
50
+ export const createVisualizerFrame = (backgroundImageBuffer: BmpDecoder, spectrum: number[], size: Size, position: Position, color: Color | string) => {
50
51
  const image = Object.assign({}, backgroundImageBuffer);
51
52
  image.data = Buffer.from(image.data);
52
53
  const rgbSpectrumColor = (typeof color === 'string') ? hexToRgb(color) : color;
53
- drawSpectrum(image, spectrum, size, rgbSpectrumColor);
54
+ drawSpectrum(image, spectrum, size, position, rgbSpectrumColor);
54
55
  return image;
55
56
  };
56
57
 
package/src/index.ts CHANGED
@@ -1,5 +1,18 @@
1
- import path from 'path';
2
- import { createAudioBuffer, bufferToUInt8, normalizeAudioData, getSmoothSpectrums, skipEvery } from './audio';
1
+ import {
2
+ getAudioFilePath,
3
+ getBackgroundImagePath,
4
+ getOutVideoPath,
5
+ getFPS,
6
+ getSpectrumWidthAbsolute,
7
+ getSpectrumHeightAbsolute,
8
+ getSpectrumXAbsolute,
9
+ getSpectrumYAbsolute,
10
+ getSpectrumColor,
11
+ getFfmpeg_cfr,
12
+ getFfmpeg_preset,
13
+ getFrame_processing_delay
14
+ } from './config';
15
+ import { createAudioBuffer, bufferToUInt8, createSpectrumsProcessor } from './audio';
3
16
  import { parseImage, createVisualizerFrame, createImageBuffer, getImageColor, invertColor, Color, convertToBmp } from './image';
4
17
  import { spawnFfmpegVideoWriter, getProgress, calculateProgress, waitDrain } from './video';
5
18
 
@@ -23,16 +36,34 @@ export interface Config {
23
36
  spectrum?: {
24
37
  width?: number;
25
38
  height?: number;
39
+ x?: number | PositionAliasName;
40
+ y?: number | PositionAliasName;
26
41
  color?: Color | string;
27
42
  }
28
- }
43
+ };
44
+ tweaks?: {
45
+ ffmpeg_cfr?: string;
46
+ ffmpeg_preset?: string;
47
+ frame_processing_delay?: number;
48
+ };
29
49
  }
30
50
 
51
+ export type PositionAliasName =
52
+ 'left' |
53
+ 'center' |
54
+ 'right' |
55
+ 'top' |
56
+ 'middle' |
57
+ 'bottom';
58
+
59
+ const sleep = (timeout: number) =>
60
+ new Promise(resolve => setTimeout(resolve, timeout));
61
+
31
62
  export const renderAudioVisualizer = (config: Config, onProgress?: (progress: number) => any, shouldStop?: () => boolean) =>
32
63
  new Promise<number>(async (resolve) => {
33
- const audioFilePath = path.resolve(config.audio.path);
34
- const backgroundImagePath = path.resolve(config.image.path);
35
- const outVideoPath = path.resolve(config.outVideo.path);
64
+ const audioFilePath = getAudioFilePath(config);
65
+ const backgroundImagePath = getBackgroundImagePath(config);
66
+ const outVideoPath = getOutVideoPath(config);
36
67
 
37
68
  const backgroundImageBmpBuffer = await convertToBmp(backgroundImagePath);
38
69
  const backgroundImage = parseImage(backgroundImageBmpBuffer);
@@ -42,39 +73,48 @@ export const renderAudioVisualizer = (config: Config, onProgress?: (progress: nu
42
73
  if (!sampleRate) {
43
74
  throw new Error('ffmpeg didn\'t show audio sample rate');
44
75
  }
45
- const audioData = PCM_FORMAT.parseFunction(audioBuffer);
46
- const normalizedAudioData = normalizeAudioData(audioData);
47
76
 
48
- const FPS = config.outVideo.fps || 60;
77
+ const FPS = getFPS(config);
49
78
  const spectrumWidth =
50
- (config.outVideo.spectrum && config.outVideo.spectrum.width) ||
51
- backgroundImage.width * 0.4;
79
+ getSpectrumWidthAbsolute(config, backgroundImage.width);
52
80
  const spectrumHeight =
53
- (config.outVideo.spectrum && config.outVideo.spectrum.height) ||
54
- backgroundImage.height * 0.1;
81
+ getSpectrumHeightAbsolute(config, backgroundImage.height);
82
+ const spectrumX =
83
+ getSpectrumXAbsolute(config, spectrumWidth, backgroundImage.width);
84
+ const spectrumY =
85
+ getSpectrumYAbsolute(config, spectrumHeight, backgroundImage.height);
55
86
  const spectrumColor =
56
- (config.outVideo.spectrum && config.outVideo.spectrum.color) ||
87
+ getSpectrumColor(config) ||
57
88
  invertColor(getImageColor(backgroundImage));
89
+ const ffmpeg_cfr = getFfmpeg_cfr(config);
90
+ const ffmpeg_preset = getFfmpeg_preset(config);
91
+ const frame_processing_delay = getFrame_processing_delay(config);
92
+ const spectrumBusesCount = 64;
58
93
 
59
- const audioDuration = audioData.length / sampleRate;
94
+ const audioDuration = audioBuffer.length / sampleRate;
60
95
  const framesCount = Math.trunc(audioDuration * FPS);
61
- const audioDataStep = Math.trunc(audioData.length / framesCount);
62
- const spectrums = getSmoothSpectrums(normalizedAudioData, FPS, audioDataStep);
63
- const spectrumsReduced = spectrums.map(spectrum => spectrum.filter(skipEvery(4)));
96
+ const audioDataStep = Math.trunc(audioBuffer.length / framesCount);
64
97
 
65
98
  const ffmpegVideoWriter = spawnFfmpegVideoWriter({
66
99
  audioFilename: audioFilePath,
67
100
  videoFileName: outVideoPath,
68
101
  fps: FPS,
69
- ...(!!onProgress && { onStderr: getProgress(calculateProgress(framesCount + 1, onProgress)) })
102
+ ...(!!onProgress && { onStderr: getProgress(calculateProgress(framesCount + 1, onProgress)) }),
103
+ ...(ffmpeg_cfr && { crf: ffmpeg_cfr }),
104
+ ...(ffmpeg_preset && { preset: ffmpeg_preset }),
70
105
  });
71
106
  ffmpegVideoWriter.on('exit', (code: number) => resolve(code));
72
107
 
108
+ const processSpectrum = createSpectrumsProcessor(spectrumBusesCount);
73
109
  for (let i = 0; i < framesCount; i++) {
110
+ const audioDataParser = () =>
111
+ PCM_FORMAT.parseFunction(audioBuffer, i * audioDataStep, i * audioDataStep + audioDataStep);
112
+ const spectrum = processSpectrum(i, audioDataParser);
74
113
  const frameImage = createVisualizerFrame(
75
114
  backgroundImage,
76
- spectrumsReduced[i],
115
+ spectrum,
77
116
  { width: spectrumWidth, height: spectrumHeight },
117
+ { x: spectrumX, y: spectrumY },
78
118
  spectrumColor
79
119
  );
80
120
  const frameImageBuffer = createImageBuffer(frameImage);
@@ -85,6 +125,9 @@ export const renderAudioVisualizer = (config: Config, onProgress?: (progress: nu
85
125
  if (shouldStop && shouldStop()) {
86
126
  break;
87
127
  }
128
+ if (frame_processing_delay) {
129
+ await sleep(frame_processing_delay);
130
+ }
88
131
  }
89
132
 
90
133
  ffmpegVideoWriter.stdin.end();
@@ -5,9 +5,11 @@ import {
5
5
  bufferToUInt8,
6
6
  normalizeAudioData,
7
7
  spawnFfmpegAudioReader,
8
- smoothSpectrums,
9
- getSmoothSpectrums,
10
- skipEvery
8
+ skipEvery,
9
+ getPeaks,
10
+ correctPeaks,
11
+ smoothValues,
12
+ createSpectrumsProcessor,
11
13
  } from '../audio';
12
14
  import { createSandbox } from 'sinon';
13
15
 
@@ -31,7 +33,7 @@ describe('audio', function () {
31
33
  it('bufferToUInt8', function () {
32
34
  const expected = [1, 2, 3, 4, 5];
33
35
  const buffer = Buffer.from(new Uint8Array(expected));
34
- const result = bufferToUInt8(buffer);
36
+ const result = bufferToUInt8(buffer, 0, 5);
35
37
  expect(result).deep.equal(expected);
36
38
  });
37
39
 
@@ -58,22 +60,46 @@ describe('audio', function () {
58
60
  childProcessStream.stderr.emit('data', 'some data');
59
61
  });
60
62
 
61
- it('smoothSpectrums', () => {
62
- const result = smoothSpectrums([[1, 0, 3], [40, 50, 60]], 1);
63
- const expected = [[0.5125, 0.5, 0.525], [0.5125, 0.5, 0.525]];
63
+ it('skipEvery', () => {
64
+ const sampleData = [1, 2, 3, 4];
65
+ const result = sampleData.filter(skipEvery(2));
66
+ const expected = [1, 3];
64
67
  expect(result).deep.equal(expected);
65
68
  });
66
69
 
67
- it('getSmoothSpectrums', () => {
68
- const result = getSmoothSpectrums([1, 2, 3, 4, 5, 6, 7, 8], 2, 2);
69
- const expected = [[0.4666666666666666], [0.6], [0.6], [0.7333333333333334]];
70
+ it('getPeaks', () => {
71
+ const sampleData = [1, 2, 3, 4];
72
+ const prevPeaks = [2, 2, 2, 5];
73
+ const resultNoPrevPeaks = getPeaks(sampleData);
74
+ expect(resultNoPrevPeaks).deep.equal(sampleData);
75
+ const resultWithPrevPeaks = getPeaks(sampleData, prevPeaks);
76
+ const expectedWithPrevPeaks = [2, 2, 3, 5];
77
+ expect(resultWithPrevPeaks).deep.equal(expectedWithPrevPeaks);
78
+ });
79
+
80
+ it('correctPeaks', () => {
81
+ const spectrums = [1, 2, 3, 4];
82
+ const peaks = [2, 2, 3, 5];
83
+ const result = correctPeaks(spectrums, peaks);
84
+ const expected = [1 / 3, 2 / 3, 1, 0.8];
70
85
  expect(result).deep.equal(expected);
71
86
  });
72
87
 
73
- it('skipEvery', () => {
74
- const sampleData = [1, 2, 3, 4];
75
- const result = sampleData.filter(skipEvery(2));
76
- const expected = [1, 3];
88
+ it('smoothValues', () => {
89
+ const spectrums = [1, 2, 3, 4];
90
+ const prevSpectrums = [2, 4, 6, 3];
91
+ const result = smoothValues(spectrums, prevSpectrums);
92
+ const expected = [1.5, 3, 4.5, 3.5];
77
93
  expect(result).deep.equal(expected);
78
94
  });
95
+
96
+ it('createSpectrumsProcessor', () => {
97
+ const busesCount = 2;
98
+ const spectrumsProcessor = createSpectrumsProcessor(busesCount);
99
+
100
+ const frame0 = spectrumsProcessor(0, () => ([1, 2, 3, 4]));
101
+ expect(frame0).to.have.length(2);
102
+ const frame1 = spectrumsProcessor(1, () => ([2, 2, 6, 2]));
103
+ expect(frame1).to.have.length(2);
104
+ });
79
105
  });