nodejs-audio-visualizer 2.0.0 → 2.1.1
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 +2 -1
- package/dist/bpmEncoder.d.ts +10 -0
- package/dist/bpmEncoder.js +76 -0
- package/dist/bpmEncoder.js.map +1 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js +31 -15
- package/dist/config.js.map +1 -1
- package/dist/image.d.ts +28 -3
- package/dist/image.js +80 -21
- package/dist/image.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +19 -6
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/bpmEncoder.ts +71 -0
- package/src/config.ts +39 -13
- package/src/image.ts +153 -30
- package/src/index.ts +21 -11
- package/src/tests/config.test.ts +60 -0
- package/src/tests/image.test.ts +91 -22
- package/src/tests/index.test.ts +2 -2
- package/src/tests/snapshots/frameSnapshotSpectrumDown.json +1 -1
- package/src/tests/snapshots/frameSnapshotSpectrumUp.json +1 -1
- package/src/tests/snapshots/frameSnapshotSpectrumUpOpacity50.json +1 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export interface EncodedBmp {
|
|
2
|
+
shiftPos: number;
|
|
3
|
+
rowBytes: number;
|
|
4
|
+
data: Buffer;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const createBpmEncoder = (imgData: { width: number; height: number; }) => {
|
|
8
|
+
const width = imgData.width;
|
|
9
|
+
const height = imgData.height;
|
|
10
|
+
const extraBytes = width % 4;
|
|
11
|
+
const rgbSize = height * (3 * width + extraBytes);
|
|
12
|
+
const headerInfoSize = 40;
|
|
13
|
+
|
|
14
|
+
/******************header***********************/
|
|
15
|
+
const flag = 'BM';
|
|
16
|
+
const reserved = 0;
|
|
17
|
+
const offset = 54;
|
|
18
|
+
const fileSize = rgbSize + offset;
|
|
19
|
+
const planes = 1;
|
|
20
|
+
const bitPP = 24;
|
|
21
|
+
const compress = 0;
|
|
22
|
+
const hr = 0;
|
|
23
|
+
const vr = 0;
|
|
24
|
+
const colors = 0;
|
|
25
|
+
const importantColors = 0;
|
|
26
|
+
|
|
27
|
+
return (buffer: Buffer): EncodedBmp => {
|
|
28
|
+
const tempBuffer = Buffer.alloc(offset + rgbSize);
|
|
29
|
+
|
|
30
|
+
let pos = 0;
|
|
31
|
+
tempBuffer.write(flag, pos, 2); pos += 2;
|
|
32
|
+
tempBuffer.writeUInt32LE(fileSize, pos); pos += 4;
|
|
33
|
+
tempBuffer.writeUInt32LE(reserved, pos); pos += 4;
|
|
34
|
+
tempBuffer.writeUInt32LE(offset, pos); pos += 4;
|
|
35
|
+
|
|
36
|
+
tempBuffer.writeUInt32LE(headerInfoSize, pos); pos += 4;
|
|
37
|
+
tempBuffer.writeUInt32LE(width, pos); pos += 4;
|
|
38
|
+
tempBuffer.writeInt32LE(-height, pos); pos += 4;
|
|
39
|
+
tempBuffer.writeUInt16LE(planes, pos); pos += 2;
|
|
40
|
+
tempBuffer.writeUInt16LE(bitPP, pos); pos += 2;
|
|
41
|
+
tempBuffer.writeUInt32LE(compress, pos); pos += 4;
|
|
42
|
+
tempBuffer.writeUInt32LE(rgbSize, pos); pos += 4;
|
|
43
|
+
tempBuffer.writeUInt32LE(hr, pos); pos += 4;
|
|
44
|
+
tempBuffer.writeUInt32LE(vr, pos); pos += 4;
|
|
45
|
+
tempBuffer.writeUInt32LE(colors, pos); pos += 4;
|
|
46
|
+
tempBuffer.writeUInt32LE(importantColors, pos); pos += 4;
|
|
47
|
+
|
|
48
|
+
let i = 0;
|
|
49
|
+
const rowBytes = 3 * width + extraBytes;
|
|
50
|
+
|
|
51
|
+
for (let y = 0; y < height; y++) {
|
|
52
|
+
for (let x = 0; x < width; x++) {
|
|
53
|
+
const p = pos + y * rowBytes + x * 3;
|
|
54
|
+
i++; // a
|
|
55
|
+
tempBuffer[p] = buffer[i++]; // b
|
|
56
|
+
tempBuffer[p + 1] = buffer[i++]; // g
|
|
57
|
+
tempBuffer[p + 2] = buffer[i++]; // r
|
|
58
|
+
}
|
|
59
|
+
if (extraBytes > 0) {
|
|
60
|
+
const fillOffset = pos + y * rowBytes + width * 3;
|
|
61
|
+
tempBuffer.fill(0, fillOffset, fillOffset + extraBytes);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
shiftPos: pos,
|
|
67
|
+
rowBytes,
|
|
68
|
+
data: tempBuffer
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
};
|
package/src/config.ts
CHANGED
|
@@ -7,7 +7,10 @@ export const defaults = {
|
|
|
7
7
|
spectrumHeight: '10%',
|
|
8
8
|
spectrumX: 'center',
|
|
9
9
|
spectrumY: 'top',
|
|
10
|
-
spectrumRotation: 'down'
|
|
10
|
+
spectrumRotation: 'down',
|
|
11
|
+
spectrumBusesCount: 64,
|
|
12
|
+
spectrumBusMargin: 4,
|
|
13
|
+
spectrumOpacity: '100%',
|
|
11
14
|
};
|
|
12
15
|
|
|
13
16
|
type RelativePositionValue = 0 | 0.5 | 1;
|
|
@@ -43,19 +46,25 @@ export const getOutVideoPath = (config: Config) =>
|
|
|
43
46
|
export const getFPS = (config: Config) =>
|
|
44
47
|
config.outVideo.fps || defaults.fps;
|
|
45
48
|
|
|
49
|
+
export const getSpectrumBusesCount = () => defaults.spectrumBusesCount;
|
|
50
|
+
|
|
51
|
+
export const getSpectrumBusMargin = () => defaults.spectrumBusMargin;
|
|
52
|
+
|
|
46
53
|
const getSpectrumWidth = (config: Config) =>
|
|
47
54
|
(config.outVideo.spectrum && config.outVideo.spectrum.width) ||
|
|
48
55
|
defaults.spectrumWidth;
|
|
49
56
|
|
|
50
57
|
const getSpectrumSizeAbsolute = (
|
|
51
|
-
spectrumSize: SpectrumSizeValue,
|
|
58
|
+
spectrumSize: SpectrumSizeValue,
|
|
59
|
+
backgroundImageSize: number,
|
|
60
|
+
fieldName: string
|
|
52
61
|
) => {
|
|
53
62
|
if (typeof spectrumSize === 'number') {
|
|
54
63
|
return spectrumSize;
|
|
55
64
|
}
|
|
56
65
|
const isPercentValue = checkIsPercentValue(spectrumSize);
|
|
57
66
|
if (!isPercentValue) {
|
|
58
|
-
throw new Error();
|
|
67
|
+
throw new Error(getSpectrumSizeValueErrorMessage(fieldName, spectrumSize));
|
|
59
68
|
}
|
|
60
69
|
const percentValue = parseInt(spectrumSize) / 100;
|
|
61
70
|
return backgroundImageSize * percentValue;
|
|
@@ -70,11 +79,13 @@ export const getSpectrumWidthAbsolute = (
|
|
|
70
79
|
config: Config, backgroundImageWidth: number
|
|
71
80
|
) => {
|
|
72
81
|
const spectrumWidth = getSpectrumWidth(config);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
82
|
+
const spectrumWidthAbsolute = getSpectrumSizeAbsolute(spectrumWidth, backgroundImageWidth, 'width');
|
|
83
|
+
const spectrumWidthWithoutMargin =
|
|
84
|
+
Math.trunc(spectrumWidthAbsolute / defaults.spectrumBusesCount) - defaults.spectrumBusMargin / 2;
|
|
85
|
+
if (spectrumWidthWithoutMargin <= 0) {
|
|
86
|
+
throw new Error(`Spectrum width '${spectrumWidth}' is too small.`);
|
|
77
87
|
}
|
|
88
|
+
return spectrumWidthAbsolute;
|
|
78
89
|
};
|
|
79
90
|
|
|
80
91
|
const getSpectrumHeight = (config: Config) =>
|
|
@@ -85,11 +96,7 @@ export const getSpectrumHeightAbsolute = (
|
|
|
85
96
|
config: Config, backgroundImageHeight: number
|
|
86
97
|
) => {
|
|
87
98
|
const spectrumHeight = getSpectrumHeight(config);
|
|
88
|
-
|
|
89
|
-
return getSpectrumSizeAbsolute(spectrumHeight, backgroundImageHeight);
|
|
90
|
-
} catch {
|
|
91
|
-
throw new Error(getSpectrumSizeValueErrorMessage('height', spectrumHeight));
|
|
92
|
-
}
|
|
99
|
+
return getSpectrumSizeAbsolute(spectrumHeight, backgroundImageHeight, 'height');
|
|
93
100
|
};
|
|
94
101
|
|
|
95
102
|
const getSpectrumX = (config: Config) =>
|
|
@@ -203,7 +210,26 @@ export const getSpectrumRotation = (config: Config): RotationAliasName => {
|
|
|
203
210
|
};
|
|
204
211
|
|
|
205
212
|
export const getSpectrumColor = (config: Config) =>
|
|
206
|
-
|
|
213
|
+
config.outVideo.spectrum && config.outVideo.spectrum.color;
|
|
214
|
+
|
|
215
|
+
export const getSpectrumOpacity = (config: Config) =>
|
|
216
|
+
(config.outVideo.spectrum && config.outVideo.spectrum.opacity) ||
|
|
217
|
+
defaults.spectrumOpacity;
|
|
218
|
+
|
|
219
|
+
export const getSpectrumOpacityParsed = (config: Config) => {
|
|
220
|
+
const spectrumOpacity = getSpectrumOpacity(config);
|
|
221
|
+
if (!checkIsPercentValue(spectrumOpacity)) {
|
|
222
|
+
throw new Error(`Invalid spectrum opacity value: '${spectrumOpacity}'. Use string percent value, for example '80%'.`);
|
|
223
|
+
}
|
|
224
|
+
const percentValue = parseInt(spectrumOpacity);
|
|
225
|
+
if (
|
|
226
|
+
(percentValue < 0) ||
|
|
227
|
+
(percentValue > 100)
|
|
228
|
+
) {
|
|
229
|
+
throw new Error(`Invalid spectrum opacity value: '${spectrumOpacity}'. Percent values must be in range from '0%' to '100%'.`);
|
|
230
|
+
}
|
|
231
|
+
return percentValue / 100;
|
|
232
|
+
};
|
|
207
233
|
|
|
208
234
|
export const getFfmpeg_cfr = (config: Config) =>
|
|
209
235
|
config.tweaks && config.tweaks.ffmpeg_cfr;
|
package/src/image.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { RotationAliasName } from './index';
|
|
2
|
-
import { decode,
|
|
2
|
+
import { decode, BmpDecoder } from 'bmp-js';
|
|
3
|
+
import { EncodedBmp } from './bpmEncoder';
|
|
3
4
|
import Jimp from 'jimp';
|
|
4
5
|
|
|
5
6
|
export interface Color {
|
|
@@ -18,31 +19,112 @@ interface Size {
|
|
|
18
19
|
height: number;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
interface MixColorProps {
|
|
23
|
+
color: Color;
|
|
24
|
+
colorOpacity: number;
|
|
25
|
+
backgroundColor: Color;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const mixValues =(value1: number, value1Opacity: number, value2: number) =>
|
|
29
|
+
Math.round(value2 * (1 - value1Opacity) + value1 * value1Opacity);
|
|
30
|
+
|
|
31
|
+
export const mixColors = ({
|
|
32
|
+
color,
|
|
33
|
+
colorOpacity,
|
|
34
|
+
backgroundColor,
|
|
35
|
+
}: MixColorProps): Color => {
|
|
36
|
+
return {
|
|
37
|
+
red: mixValues(color.red, colorOpacity, backgroundColor.red),
|
|
38
|
+
green: mixValues(color.green, colorOpacity, backgroundColor.green),
|
|
39
|
+
blue: mixValues(color.blue, colorOpacity, backgroundColor.blue),
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const getRectPixelColor = (params: {
|
|
44
|
+
imageDstBuffer: EncodedBmp;
|
|
45
|
+
pixelIndex: number;
|
|
46
|
+
color: Color;
|
|
47
|
+
opacity: number;
|
|
48
|
+
}) => {
|
|
49
|
+
const {
|
|
50
|
+
imageDstBuffer,
|
|
51
|
+
pixelIndex,
|
|
52
|
+
color,
|
|
53
|
+
opacity,
|
|
54
|
+
} = params;
|
|
55
|
+
if (opacity === 1) {
|
|
56
|
+
return color;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const backgroundPixelColor: Color = {
|
|
60
|
+
blue: imageDstBuffer.data[pixelIndex],
|
|
61
|
+
green: imageDstBuffer.data[pixelIndex + 1],
|
|
62
|
+
red: imageDstBuffer.data[pixelIndex + 2],
|
|
63
|
+
};
|
|
64
|
+
const resultColor = mixColors({
|
|
65
|
+
color,
|
|
66
|
+
colorOpacity: opacity,
|
|
67
|
+
backgroundColor: backgroundPixelColor,
|
|
68
|
+
});
|
|
69
|
+
return resultColor;
|
|
70
|
+
};
|
|
25
71
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
72
|
+
interface DrawRectPorps {
|
|
73
|
+
imageDstBuffer: EncodedBmp;
|
|
74
|
+
position: Position;
|
|
75
|
+
size: Size;
|
|
76
|
+
color: Color;
|
|
77
|
+
opacity: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const drawRect = ({
|
|
81
|
+
imageDstBuffer,
|
|
82
|
+
position,
|
|
83
|
+
size,
|
|
84
|
+
color,
|
|
85
|
+
opacity,
|
|
86
|
+
}: DrawRectPorps) => {
|
|
87
|
+
for (let currY = position.y; currY < position.y + size.height; currY++) {
|
|
88
|
+
for (let currX = position.x; currX < position.x + size.width; currX++) {
|
|
89
|
+
const pixelIndex = imageDstBuffer.shiftPos + currY * imageDstBuffer.rowBytes + currX * 3;
|
|
90
|
+
const pixelColor = getRectPixelColor({
|
|
91
|
+
imageDstBuffer,
|
|
92
|
+
pixelIndex,
|
|
93
|
+
color,
|
|
94
|
+
opacity,
|
|
95
|
+
});
|
|
96
|
+
imageDstBuffer.data[pixelIndex] = pixelColor.blue;
|
|
97
|
+
imageDstBuffer.data[pixelIndex + 1] = pixelColor.green;
|
|
98
|
+
imageDstBuffer.data[pixelIndex + 2] = pixelColor.red;
|
|
29
99
|
}
|
|
30
100
|
}
|
|
31
101
|
};
|
|
32
102
|
|
|
33
|
-
|
|
34
|
-
imageDstBuffer:
|
|
35
|
-
spectrum: number[]
|
|
36
|
-
size: Size
|
|
37
|
-
position: Position
|
|
38
|
-
rotation: RotationAliasName
|
|
39
|
-
|
|
40
|
-
|
|
103
|
+
interface DrawSpectrumProps {
|
|
104
|
+
imageDstBuffer: EncodedBmp;
|
|
105
|
+
spectrum: number[];
|
|
106
|
+
size: Size;
|
|
107
|
+
position: Position;
|
|
108
|
+
rotation: RotationAliasName;
|
|
109
|
+
margin: number;
|
|
110
|
+
color: Color;
|
|
111
|
+
opacity: number;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const drawSpectrum = ({
|
|
115
|
+
imageDstBuffer,
|
|
116
|
+
spectrum,
|
|
117
|
+
size,
|
|
118
|
+
position,
|
|
119
|
+
rotation,
|
|
120
|
+
margin,
|
|
121
|
+
color,
|
|
122
|
+
opacity,
|
|
123
|
+
}: DrawSpectrumProps) => {
|
|
41
124
|
const busWidth = Math.trunc(size.width / spectrum.length);
|
|
42
125
|
const left = Math.trunc(position.x);
|
|
43
126
|
const top = Math.trunc(position.y);
|
|
44
127
|
const height = Math.trunc(size.height);
|
|
45
|
-
const margin = 4;
|
|
46
128
|
|
|
47
129
|
for (let spectrumX = 0; spectrumX < spectrum.length; spectrumX++) {
|
|
48
130
|
const spectrumValue = spectrum[spectrumX];
|
|
@@ -53,30 +135,71 @@ const drawSpectrum = (
|
|
|
53
135
|
const rectX = left + busWidth * spectrumX;
|
|
54
136
|
const rectHeight = Math.trunc(height * spectrumValue);
|
|
55
137
|
const rectY = (rotation === 'up') ? top + height - rectHeight : top;
|
|
56
|
-
drawRect(
|
|
138
|
+
drawRect({
|
|
139
|
+
imageDstBuffer,
|
|
140
|
+
position: { x: rectX + margin, y: rectY },
|
|
141
|
+
size: { width: busWidth - margin / 2, height: rectHeight },
|
|
142
|
+
color,
|
|
143
|
+
opacity,
|
|
144
|
+
});
|
|
57
145
|
}
|
|
58
146
|
};
|
|
59
147
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
148
|
+
interface CreateVisualizerFrameProps {
|
|
149
|
+
backgroundImageBuffer: EncodedBmp;
|
|
150
|
+
spectrum: number[];
|
|
151
|
+
size: Size;
|
|
152
|
+
position: Position;
|
|
153
|
+
rotation: RotationAliasName;
|
|
154
|
+
margin: number;
|
|
155
|
+
color: Color | string;
|
|
156
|
+
opacity: number;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export const createVisualizerFrame = ({
|
|
160
|
+
backgroundImageBuffer,
|
|
161
|
+
spectrum,
|
|
162
|
+
size,
|
|
163
|
+
position,
|
|
164
|
+
rotation,
|
|
165
|
+
margin,
|
|
166
|
+
color,
|
|
167
|
+
opacity,
|
|
168
|
+
}: CreateVisualizerFrameProps) => {
|
|
169
|
+
const imageDstBuffer = Object.assign({}, backgroundImageBuffer);
|
|
170
|
+
imageDstBuffer.data = Buffer.from(backgroundImageBuffer.data);
|
|
171
|
+
|
|
63
172
|
const rgbSpectrumColor = (typeof color === 'string') ? hexToRgb(color) : color;
|
|
64
|
-
drawSpectrum(
|
|
65
|
-
|
|
173
|
+
drawSpectrum({
|
|
174
|
+
imageDstBuffer,
|
|
175
|
+
spectrum,
|
|
176
|
+
size,
|
|
177
|
+
position,
|
|
178
|
+
rotation,
|
|
179
|
+
margin,
|
|
180
|
+
color: rgbSpectrumColor,
|
|
181
|
+
opacity,
|
|
182
|
+
});
|
|
183
|
+
return imageDstBuffer;
|
|
66
184
|
};
|
|
67
185
|
|
|
68
186
|
export const convertToBmp = async (filePath: string) =>
|
|
69
|
-
new Promise<Buffer>(async resolve => {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
187
|
+
new Promise<Buffer>(async (resolve, reject) => {
|
|
188
|
+
try {
|
|
189
|
+
const image = await Jimp.read(filePath);
|
|
190
|
+
image.getBuffer("image/bmp", (err, value) => {
|
|
191
|
+
if (err) {
|
|
192
|
+
reject(err);
|
|
193
|
+
}
|
|
194
|
+
resolve(value);
|
|
195
|
+
});
|
|
196
|
+
} catch {
|
|
197
|
+
throw new Error(`Cannot procces image file from path: ${filePath}`);
|
|
198
|
+
}
|
|
74
199
|
});
|
|
75
200
|
|
|
76
201
|
export const parseImage = (buffer: Buffer) => decode(buffer);
|
|
77
202
|
|
|
78
|
-
export const createImageBuffer = (image: BmpDecoder) => encode(image).data;
|
|
79
|
-
|
|
80
203
|
export const getImageColor = (image: BmpDecoder): Color => {
|
|
81
204
|
let blueSum = 0;
|
|
82
205
|
let greenSum = 0;
|
package/src/index.ts
CHANGED
|
@@ -3,11 +3,14 @@ import {
|
|
|
3
3
|
getBackgroundImagePath,
|
|
4
4
|
getOutVideoPath,
|
|
5
5
|
getFPS,
|
|
6
|
+
getSpectrumBusesCount,
|
|
7
|
+
getSpectrumBusMargin,
|
|
6
8
|
getSpectrumWidthAbsolute,
|
|
7
9
|
getSpectrumHeightAbsolute,
|
|
8
10
|
getSpectrumXAbsolute,
|
|
9
11
|
getSpectrumYAbsolute,
|
|
10
12
|
getSpectrumColor,
|
|
13
|
+
getSpectrumOpacityParsed,
|
|
11
14
|
getFfmpeg_cfr,
|
|
12
15
|
getFfmpeg_preset,
|
|
13
16
|
getFrame_processing_delay,
|
|
@@ -15,8 +18,9 @@ import {
|
|
|
15
18
|
getSpectrumRotation
|
|
16
19
|
} from './config';
|
|
17
20
|
import { createAudioBuffer, bufferToUInt8, createSpectrumsProcessor } from './audio';
|
|
18
|
-
import { parseImage, createVisualizerFrame,
|
|
21
|
+
import { parseImage, createVisualizerFrame, getImageColor, invertColor, Color, convertToBmp } from './image';
|
|
19
22
|
import { spawnFfmpegVideoWriter, getProgress, calculateProgress, waitDrain } from './video';
|
|
23
|
+
import { createBpmEncoder } from './bpmEncoder';
|
|
20
24
|
|
|
21
25
|
export const PCM_FORMAT = {
|
|
22
26
|
bit: 8,
|
|
@@ -42,6 +46,7 @@ export interface Config {
|
|
|
42
46
|
y?: number | PositionAliasName;
|
|
43
47
|
rotation?: RotationAliasName;
|
|
44
48
|
color?: Color | string;
|
|
49
|
+
opacity?: string;
|
|
45
50
|
}
|
|
46
51
|
};
|
|
47
52
|
tweaks?: {
|
|
@@ -81,6 +86,8 @@ export const renderAudioVisualizer = (config: Config, onProgress?: (progress: nu
|
|
|
81
86
|
throw new Error('ffmpeg didn\'t show audio sample rate');
|
|
82
87
|
}
|
|
83
88
|
|
|
89
|
+
const spectrumBusesCount = getSpectrumBusesCount();
|
|
90
|
+
const spectrumBusMargin = getSpectrumBusMargin();
|
|
84
91
|
const FPS = getFPS(config);
|
|
85
92
|
const spectrumWidth =
|
|
86
93
|
getSpectrumWidthAbsolute(config, backgroundImage.width);
|
|
@@ -95,10 +102,10 @@ export const renderAudioVisualizer = (config: Config, onProgress?: (progress: nu
|
|
|
95
102
|
const spectrumColor =
|
|
96
103
|
getSpectrumColor(config) ||
|
|
97
104
|
invertColor(getImageColor(backgroundImage));
|
|
105
|
+
const spectrumOpacity = getSpectrumOpacityParsed(config);
|
|
98
106
|
const ffmpeg_cfr = getFfmpeg_cfr(config);
|
|
99
107
|
const ffmpeg_preset = getFfmpeg_preset(config);
|
|
100
108
|
const frame_processing_delay = getFrame_processing_delay(config);
|
|
101
|
-
const spectrumBusesCount = 64;
|
|
102
109
|
|
|
103
110
|
const audioDuration = audioBuffer.length / sampleRate;
|
|
104
111
|
const framesCount = Math.trunc(audioDuration * FPS);
|
|
@@ -114,21 +121,24 @@ export const renderAudioVisualizer = (config: Config, onProgress?: (progress: nu
|
|
|
114
121
|
});
|
|
115
122
|
ffmpegVideoWriter.on('exit', (code: number) => resolve(code));
|
|
116
123
|
|
|
124
|
+
const bpmEncoder = createBpmEncoder({ width: backgroundImage.width, height: backgroundImage.height });
|
|
125
|
+
const backgroundImageBuffer = bpmEncoder(backgroundImage.data);
|
|
117
126
|
const processSpectrum = createSpectrumsProcessor(spectrumBusesCount);
|
|
118
127
|
for (let i = 0; i < framesCount; i++) {
|
|
119
128
|
const audioDataParser = () =>
|
|
120
129
|
PCM_FORMAT.parseFunction(audioBuffer, i * audioDataStep, i * audioDataStep + audioDataStep);
|
|
121
130
|
const spectrum = processSpectrum(i, audioDataParser);
|
|
122
|
-
const frameImage = createVisualizerFrame(
|
|
123
|
-
|
|
131
|
+
const frameImage = createVisualizerFrame({
|
|
132
|
+
backgroundImageBuffer,
|
|
124
133
|
spectrum,
|
|
125
|
-
{ width: spectrumWidth, height: spectrumHeight },
|
|
126
|
-
{ x: spectrumX, y: spectrumY },
|
|
127
|
-
spectrumRotation,
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
134
|
+
size: { width: spectrumWidth, height: spectrumHeight },
|
|
135
|
+
position: { x: spectrumX, y: spectrumY },
|
|
136
|
+
rotation: spectrumRotation,
|
|
137
|
+
margin: spectrumBusMargin,
|
|
138
|
+
color: spectrumColor,
|
|
139
|
+
opacity: spectrumOpacity,
|
|
140
|
+
});
|
|
141
|
+
const isFrameProcessed = ffmpegVideoWriter.stdin.write(frameImage.data);
|
|
132
142
|
if (!isFrameProcessed) {
|
|
133
143
|
await waitDrain(ffmpegVideoWriter.stdin);
|
|
134
144
|
}
|
package/src/tests/config.test.ts
CHANGED
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
getSpectrumXAbsolute,
|
|
13
13
|
getSpectrumYAbsolute,
|
|
14
14
|
getSpectrumColor,
|
|
15
|
+
getSpectrumOpacity,
|
|
16
|
+
getSpectrumOpacityParsed,
|
|
15
17
|
getFfmpeg_cfr,
|
|
16
18
|
getFfmpeg_preset,
|
|
17
19
|
getFrame_processing_delay,
|
|
@@ -94,6 +96,24 @@ describe('config', function() {
|
|
|
94
96
|
backgroundImageWidth
|
|
95
97
|
);
|
|
96
98
|
expect(result4).to.throw('Invalid spectrum width value: \'69\'. Use number value or percent value in string, for example: \'30%\'.');
|
|
99
|
+
|
|
100
|
+
const result5 = getSpectrumWidthAbsolute.bind(
|
|
101
|
+
undefined,
|
|
102
|
+
{
|
|
103
|
+
outVideo: { spectrum: { width: 1 }}
|
|
104
|
+
} as Config,
|
|
105
|
+
backgroundImageWidth
|
|
106
|
+
);
|
|
107
|
+
expect(result5).to.throw('Spectrum width \'1\' is too small.');
|
|
108
|
+
|
|
109
|
+
const result6 = getSpectrumWidthAbsolute.bind(
|
|
110
|
+
undefined,
|
|
111
|
+
{
|
|
112
|
+
outVideo: { spectrum: { width: '1%' }}
|
|
113
|
+
} as Config,
|
|
114
|
+
backgroundImageWidth
|
|
115
|
+
);
|
|
116
|
+
expect(result6).to.throw('Spectrum width \'1%\' is too small.');
|
|
97
117
|
});
|
|
98
118
|
|
|
99
119
|
it('getSpectrumHeightAbsolute', function() {
|
|
@@ -267,6 +287,46 @@ describe('config', function() {
|
|
|
267
287
|
expect(result2).equal('#cccc99');
|
|
268
288
|
});
|
|
269
289
|
|
|
290
|
+
it('getSpectrumOpacity', function() {
|
|
291
|
+
const result1 = getSpectrumOpacity({
|
|
292
|
+
outVideo: {}
|
|
293
|
+
} as Config);
|
|
294
|
+
expect(result1).equal(defaults.spectrumOpacity);
|
|
295
|
+
|
|
296
|
+
const result2 = getSpectrumOpacity({
|
|
297
|
+
outVideo: { spectrum: { opacity: '69%' } }
|
|
298
|
+
} as Config);
|
|
299
|
+
expect(result2).equal('69%');
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('getSpectrumOpacityParsed', function() {
|
|
303
|
+
const result1 = getSpectrumOpacityParsed({
|
|
304
|
+
outVideo: {}
|
|
305
|
+
} as Config);
|
|
306
|
+
expect(result1).equal(parseInt(defaults.spectrumOpacity) / 100);
|
|
307
|
+
|
|
308
|
+
const result2 = getSpectrumOpacityParsed({
|
|
309
|
+
outVideo: { spectrum: { opacity: '69%' } }
|
|
310
|
+
} as Config);
|
|
311
|
+
expect(result2).equal(0.69);
|
|
312
|
+
|
|
313
|
+
const result3 = getSpectrumOpacityParsed.bind(
|
|
314
|
+
undefined,
|
|
315
|
+
{
|
|
316
|
+
outVideo: { spectrum: { opacity: 'fake value' } }
|
|
317
|
+
} as Config
|
|
318
|
+
);
|
|
319
|
+
expect(result3).to.throw('Invalid spectrum opacity value: \'fake value\'. Use string percent value, for example \'80%\'.');
|
|
320
|
+
|
|
321
|
+
const result4 = getSpectrumOpacityParsed.bind(
|
|
322
|
+
undefined,
|
|
323
|
+
{
|
|
324
|
+
outVideo: { spectrum: { opacity: '103%' } }
|
|
325
|
+
} as Config
|
|
326
|
+
);
|
|
327
|
+
expect(result4).to.throw('Invalid spectrum opacity value: \'103%\'. Percent values must be in range from \'0%\' to \'100%\'.');
|
|
328
|
+
});
|
|
329
|
+
|
|
270
330
|
it('getFfmpeg_cfr', function() {
|
|
271
331
|
const result1 = getFfmpeg_cfr({
|
|
272
332
|
tweaks: {}
|