nodejs-audio-visualizer 3.0.0 → 4.1.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 +19 -3
- package/dist/beats.d.ts +19 -0
- package/dist/beats.js +44 -0
- package/dist/beats.js.map +1 -0
- package/dist/bpmEncoder.d.ts +4 -0
- package/dist/bpmEncoder.js +65 -43
- package/dist/bpmEncoder.js.map +1 -1
- package/dist/config.d.ts +21 -1
- package/dist/config.js +100 -1
- package/dist/config.js.map +1 -1
- package/dist/image.d.ts +28 -3
- package/dist/image.js +231 -3
- package/dist/image.js.map +1 -1
- package/dist/index.d.ts +20 -1
- package/dist/index.js +214 -52
- package/dist/index.js.map +1 -1
- package/dist/video.d.ts +41 -1
- package/dist/video.js +244 -0
- package/dist/video.js.map +1 -1
- package/package.json +2 -2
- package/src/beats.ts +68 -0
- package/src/bpmEncoder.ts +57 -31
- package/src/config.ts +117 -1
- package/src/image.ts +338 -27
- package/src/index.ts +321 -43
- package/src/tests/image.test.ts +86 -3
- package/src/video.ts +302 -1
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getAudioFilePath,
|
|
3
3
|
getBackgroundImagePath,
|
|
4
|
+
getBackgroundVideoPath,
|
|
4
5
|
getOutVideoPath,
|
|
5
6
|
getFPS,
|
|
6
7
|
getSpectrumBusMargin,
|
|
@@ -13,15 +14,27 @@ import {
|
|
|
13
14
|
getFfmpeg_cfr,
|
|
14
15
|
getFfmpeg_preset,
|
|
15
16
|
getFrame_processing_delay,
|
|
17
|
+
getOutputResolution,
|
|
16
18
|
rotationAliasValues,
|
|
17
19
|
getSpectrumRotation,
|
|
18
20
|
getSpectrumEffect,
|
|
19
21
|
SpectrumEffect,
|
|
22
|
+
getPolarXAbsolute,
|
|
23
|
+
getPolarYAbsolute,
|
|
24
|
+
getPolarInnerRadius,
|
|
25
|
+
getPolarMaxBarLength,
|
|
26
|
+
getPolarBarWidth,
|
|
27
|
+
getPolarEffect,
|
|
28
|
+
getPolarColor,
|
|
29
|
+
getPolarOpacityParsed,
|
|
30
|
+
getAutoEditVideo,
|
|
20
31
|
} from './config';
|
|
21
32
|
import { createAudioBuffer, bufferToUInt8, createSpectrumsProcessor } from './audio';
|
|
22
|
-
import { parseImage, getImageColor, invertColor, Color, convertToBmp,
|
|
23
|
-
import { spawnFfmpegVideoWriter, getProgress, calculateProgress, waitDrain } from './video';
|
|
24
|
-
import { createBpmEncoder } from './bpmEncoder';
|
|
33
|
+
import { parseImage, getImageColor, getVideoFrameColor, invertColor, Color, convertToBmp, createSpectrumVisualizerFrameGenerator, createPolarVisualizerFrameGenerator, CreatePolarVisualizerFrameProps, CreateVisualizerFrameProps, CommonVisualizerFrameProps } from './image';
|
|
34
|
+
import { spawnFfmpegVideoWriter, getProgress, calculateProgress, waitDrain, getVideoInfo, spawnVideoFrameReader, readVideoFrame, detectSceneChanges, buildBeatSyncedSegments, writeConcatFile, spawnConcatVideoFrameReader, cleanupConcatFile } from './video';
|
|
35
|
+
import { createBpmEncoder, createBgrFrameEncoder, EncodedBmp } from './bpmEncoder';
|
|
36
|
+
import { createBeatDetector } from './beats';
|
|
37
|
+
export { BeatInfo, BeatDetectorOptions } from './beats';
|
|
25
38
|
|
|
26
39
|
export const PCM_FORMAT = {
|
|
27
40
|
bit: 8,
|
|
@@ -35,12 +48,20 @@ export interface Config {
|
|
|
35
48
|
audio: {
|
|
36
49
|
path: string
|
|
37
50
|
};
|
|
38
|
-
image
|
|
51
|
+
image?: {
|
|
39
52
|
path: string;
|
|
40
53
|
};
|
|
54
|
+
video?: {
|
|
55
|
+
path: string;
|
|
56
|
+
autoEdit?: boolean;
|
|
57
|
+
};
|
|
41
58
|
outVideo: {
|
|
42
59
|
path: string;
|
|
43
60
|
fps?: number;
|
|
61
|
+
resolution?: {
|
|
62
|
+
width: number;
|
|
63
|
+
height: number;
|
|
64
|
+
};
|
|
44
65
|
spectrum?: {
|
|
45
66
|
width?: SpectrumSizeValue;
|
|
46
67
|
height?: SpectrumSizeValue;
|
|
@@ -50,6 +71,16 @@ export interface Config {
|
|
|
50
71
|
effect?: SpectrumEffect;
|
|
51
72
|
color?: Color | string;
|
|
52
73
|
opacity?: string;
|
|
74
|
+
};
|
|
75
|
+
polar?: {
|
|
76
|
+
x?: number | PositionAliasName;
|
|
77
|
+
y?: number | PositionAliasName;
|
|
78
|
+
innerRadius?: number;
|
|
79
|
+
maxBarLength?: number;
|
|
80
|
+
barWidth?: number;
|
|
81
|
+
effect?: SpectrumEffect;
|
|
82
|
+
color?: Color | string;
|
|
83
|
+
opacity?: string;
|
|
53
84
|
}
|
|
54
85
|
};
|
|
55
86
|
tweaks?: {
|
|
@@ -74,14 +105,251 @@ export type RotationAliasName = typeof rotationAliasValues[number];
|
|
|
74
105
|
const sleep = (timeout: number) =>
|
|
75
106
|
new Promise(resolve => setTimeout(resolve, timeout));
|
|
76
107
|
|
|
108
|
+
const createVisualizerFrameGenerator = (
|
|
109
|
+
config: Config,
|
|
110
|
+
backgroundWidth: number,
|
|
111
|
+
backgroundHeight: number,
|
|
112
|
+
defaultColor: Color,
|
|
113
|
+
spectrumBusMargin: number
|
|
114
|
+
): (params: CommonVisualizerFrameProps) => EncodedBmp => {
|
|
115
|
+
if (!config.outVideo.spectrum) {
|
|
116
|
+
const createPolarVisualizerFrame = createPolarVisualizerFrameGenerator();
|
|
117
|
+
const polarX = getPolarXAbsolute(config, backgroundWidth);
|
|
118
|
+
const polarY = getPolarYAbsolute(config, backgroundHeight);
|
|
119
|
+
const polarInnerRadius = getPolarInnerRadius(config);
|
|
120
|
+
const polarMaxBarLength = getPolarMaxBarLength(config);
|
|
121
|
+
const polarBarWidth = getPolarBarWidth(config);
|
|
122
|
+
const polarColor = getPolarColor(config) || invertColor(defaultColor);
|
|
123
|
+
const polarEffect = getPolarEffect(config);
|
|
124
|
+
const polarOpacity = getPolarOpacityParsed(config);
|
|
125
|
+
|
|
126
|
+
return (params: CommonVisualizerFrameProps) => {
|
|
127
|
+
return createPolarVisualizerFrame({
|
|
128
|
+
...params,
|
|
129
|
+
centerX: polarX,
|
|
130
|
+
centerY: polarY,
|
|
131
|
+
innerRadius: polarInnerRadius,
|
|
132
|
+
maxBarLength: polarMaxBarLength,
|
|
133
|
+
barWidth: polarBarWidth,
|
|
134
|
+
color: polarColor,
|
|
135
|
+
opacity: polarOpacity,
|
|
136
|
+
spectrumEffect: polarEffect,
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const createSpectrumVisualizerFrame = createSpectrumVisualizerFrameGenerator();
|
|
141
|
+
const spectrumWidth = getSpectrumWidthAbsolute(config, backgroundWidth);
|
|
142
|
+
const spectrumHeight = getSpectrumHeightAbsolute(config, backgroundHeight);
|
|
143
|
+
const spectrumX = getSpectrumXAbsolute(config, spectrumWidth, backgroundWidth);
|
|
144
|
+
const spectrumY = getSpectrumYAbsolute(config, spectrumHeight, backgroundHeight);
|
|
145
|
+
const spectrumRotation = getSpectrumRotation(config);
|
|
146
|
+
const spectrumColor = getSpectrumColor(config) || invertColor(defaultColor);
|
|
147
|
+
const spectrumEffect = getSpectrumEffect(config);
|
|
148
|
+
const spectrumOpacity = getSpectrumOpacityParsed(config);
|
|
149
|
+
|
|
150
|
+
return (params: CommonVisualizerFrameProps) => {
|
|
151
|
+
return createSpectrumVisualizerFrame({
|
|
152
|
+
...params,
|
|
153
|
+
size: { width: spectrumWidth, height: spectrumHeight },
|
|
154
|
+
position: { x: spectrumX, y: spectrumY },
|
|
155
|
+
rotation: spectrumRotation,
|
|
156
|
+
margin: spectrumBusMargin,
|
|
157
|
+
color: spectrumColor,
|
|
158
|
+
opacity: spectrumOpacity,
|
|
159
|
+
spectrumEffect,
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
interface PreProcessedAudio {
|
|
165
|
+
spectrums: number[][];
|
|
166
|
+
beatFrameIndices: number[];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const preProcessAudio = (
|
|
170
|
+
audioBuffer: Buffer,
|
|
171
|
+
sampleRate: number,
|
|
172
|
+
fps: number,
|
|
173
|
+
framesCount: number,
|
|
174
|
+
): PreProcessedAudio => {
|
|
175
|
+
const audioDataStep = Math.trunc(audioBuffer.length / framesCount);
|
|
176
|
+
const processingBuffer = new Float32Array(PROCESSING_BUFFER_SIZE).fill(0);
|
|
177
|
+
const skipFramesCount = fps < 45 ? 1 : 2;
|
|
178
|
+
const processSpectrum = createSpectrumsProcessor(sampleRate, skipFramesCount);
|
|
179
|
+
const detectBeat = createBeatDetector(fps);
|
|
180
|
+
|
|
181
|
+
const spectrums: number[][] = [];
|
|
182
|
+
const beatFrameIndices: number[] = [];
|
|
183
|
+
|
|
184
|
+
for (let i = 0; i < framesCount; i++) {
|
|
185
|
+
const currentFrameData = PCM_FORMAT.parseFunction(audioBuffer, i * audioDataStep, i * audioDataStep + audioDataStep);
|
|
186
|
+
processingBuffer.copyWithin(0, currentFrameData.length);
|
|
187
|
+
processingBuffer.set(currentFrameData, PROCESSING_BUFFER_SIZE - currentFrameData.length);
|
|
188
|
+
|
|
189
|
+
const audioDataParser = () => Array.from(processingBuffer);
|
|
190
|
+
const spectrum = processSpectrum(audioDataParser);
|
|
191
|
+
const beat = detectBeat(spectrum);
|
|
192
|
+
|
|
193
|
+
spectrums.push(spectrum);
|
|
194
|
+
if (beat.isBeat) {
|
|
195
|
+
beatFrameIndices.push(i);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return { spectrums, beatFrameIndices };
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
async function prepareBackgroundForRender(params: {
|
|
203
|
+
useVideoBackground: boolean;
|
|
204
|
+
backgroundVideoPath: string | undefined;
|
|
205
|
+
backgroundImagePath: string | undefined;
|
|
206
|
+
beatFrameIndices: number[];
|
|
207
|
+
framesCount: number;
|
|
208
|
+
outputResolution: ReturnType<typeof getOutputResolution>;
|
|
209
|
+
fps: number;
|
|
210
|
+
autoEditVideo: boolean;
|
|
211
|
+
}): Promise<{
|
|
212
|
+
backgroundWidth: number;
|
|
213
|
+
backgroundHeight: number;
|
|
214
|
+
defaultColor: Color;
|
|
215
|
+
staticBackgroundBuffer: EncodedBmp;
|
|
216
|
+
videoFrameReader?: ReturnType<typeof spawnVideoFrameReader>;
|
|
217
|
+
videoFrameSize: number;
|
|
218
|
+
encodeVideoFrame?: (bgrBuffer: Buffer) => EncodedBmp;
|
|
219
|
+
concatFilePath?: string;
|
|
220
|
+
}> {
|
|
221
|
+
const {
|
|
222
|
+
useVideoBackground,
|
|
223
|
+
backgroundVideoPath,
|
|
224
|
+
backgroundImagePath,
|
|
225
|
+
beatFrameIndices,
|
|
226
|
+
framesCount,
|
|
227
|
+
outputResolution,
|
|
228
|
+
fps,
|
|
229
|
+
autoEditVideo,
|
|
230
|
+
} = params;
|
|
231
|
+
|
|
232
|
+
if (useVideoBackground && backgroundVideoPath) {
|
|
233
|
+
const videoInfo = await getVideoInfo(backgroundVideoPath);
|
|
234
|
+
const sceneChanges = autoEditVideo
|
|
235
|
+
? await detectSceneChanges(backgroundVideoPath)
|
|
236
|
+
: [];
|
|
237
|
+
const backgroundWidth = outputResolution?.width ?? videoInfo.width;
|
|
238
|
+
const backgroundHeight = outputResolution?.height ?? videoInfo.height;
|
|
239
|
+
|
|
240
|
+
const videoFrameSize = backgroundWidth * backgroundHeight * 3;
|
|
241
|
+
const encodeVideoFrame = createBgrFrameEncoder({ width: backgroundWidth, height: backgroundHeight });
|
|
242
|
+
|
|
243
|
+
const segments = buildBeatSyncedSegments(
|
|
244
|
+
beatFrameIndices,
|
|
245
|
+
framesCount,
|
|
246
|
+
sceneChanges,
|
|
247
|
+
videoInfo.duration,
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
const concatFilePath = writeConcatFile(segments, backgroundVideoPath, videoInfo.duration, fps);
|
|
251
|
+
|
|
252
|
+
const videoFrameReader = spawnConcatVideoFrameReader({
|
|
253
|
+
concatFilePath,
|
|
254
|
+
fps,
|
|
255
|
+
totalFrames: framesCount,
|
|
256
|
+
...(outputResolution && {
|
|
257
|
+
width: backgroundWidth,
|
|
258
|
+
height: backgroundHeight,
|
|
259
|
+
sourceWidth: videoInfo.width,
|
|
260
|
+
sourceHeight: videoInfo.height,
|
|
261
|
+
}),
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const firstFrame = await readVideoFrame(videoFrameReader.stdout, videoFrameSize);
|
|
265
|
+
if (!firstFrame) {
|
|
266
|
+
throw new Error(`Could not read frames from video: ${backgroundVideoPath}`);
|
|
267
|
+
}
|
|
268
|
+
const defaultColor = getVideoFrameColor(firstFrame, backgroundWidth, backgroundHeight);
|
|
269
|
+
const staticBackgroundBuffer = encodeVideoFrame(firstFrame);
|
|
270
|
+
return {
|
|
271
|
+
backgroundWidth,
|
|
272
|
+
backgroundHeight,
|
|
273
|
+
defaultColor,
|
|
274
|
+
staticBackgroundBuffer,
|
|
275
|
+
videoFrameReader,
|
|
276
|
+
videoFrameSize,
|
|
277
|
+
encodeVideoFrame,
|
|
278
|
+
concatFilePath,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (!backgroundImagePath) {
|
|
283
|
+
throw new Error('Background image path is required when not using video background.');
|
|
284
|
+
}
|
|
285
|
+
const backgroundImageBmpBuffer = await convertToBmp(
|
|
286
|
+
backgroundImagePath,
|
|
287
|
+
outputResolution?.width,
|
|
288
|
+
outputResolution?.height,
|
|
289
|
+
);
|
|
290
|
+
const backgroundImage = parseImage(backgroundImageBmpBuffer);
|
|
291
|
+
const backgroundWidth = backgroundImage.width;
|
|
292
|
+
const backgroundHeight = backgroundImage.height;
|
|
293
|
+
const defaultColor = getImageColor(backgroundImage);
|
|
294
|
+
|
|
295
|
+
const bpmEncoder = createBpmEncoder({ width: backgroundWidth, height: backgroundHeight });
|
|
296
|
+
const staticBackgroundBuffer = bpmEncoder(backgroundImage.data);
|
|
297
|
+
return {
|
|
298
|
+
backgroundWidth,
|
|
299
|
+
backgroundHeight,
|
|
300
|
+
defaultColor,
|
|
301
|
+
staticBackgroundBuffer,
|
|
302
|
+
videoFrameSize: 0,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function resolveBackgroundFrameBuffer(params: {
|
|
307
|
+
frameIndex: number;
|
|
308
|
+
useVideoBackground: boolean;
|
|
309
|
+
videoFrameReader?: ReturnType<typeof spawnVideoFrameReader>;
|
|
310
|
+
videoFrameSize: number;
|
|
311
|
+
encodeVideoFrame?: (bgrBuffer: Buffer) => EncodedBmp;
|
|
312
|
+
staticBackgroundBuffer: EncodedBmp;
|
|
313
|
+
}): Promise<EncodedBmp> {
|
|
314
|
+
const {
|
|
315
|
+
frameIndex,
|
|
316
|
+
useVideoBackground,
|
|
317
|
+
videoFrameReader,
|
|
318
|
+
videoFrameSize,
|
|
319
|
+
encodeVideoFrame,
|
|
320
|
+
staticBackgroundBuffer,
|
|
321
|
+
} = params;
|
|
322
|
+
|
|
323
|
+
if (useVideoBackground && videoFrameReader && encodeVideoFrame) {
|
|
324
|
+
if (frameIndex === 0) {
|
|
325
|
+
return staticBackgroundBuffer;
|
|
326
|
+
}
|
|
327
|
+
const videoFrame = await readVideoFrame(videoFrameReader.stdout, videoFrameSize);
|
|
328
|
+
return videoFrame
|
|
329
|
+
? encodeVideoFrame(videoFrame)
|
|
330
|
+
: staticBackgroundBuffer;
|
|
331
|
+
}
|
|
332
|
+
return staticBackgroundBuffer;
|
|
333
|
+
}
|
|
334
|
+
|
|
77
335
|
export const renderAudioVisualizer = (config: Config, onProgress?: (progress: number) => any, shouldStop?: () => boolean) =>
|
|
78
336
|
new Promise<number>(async (resolve) => {
|
|
337
|
+
if (config.outVideo.spectrum && config.outVideo.polar) {
|
|
338
|
+
throw new Error('Cannot use both "spectrum" and "polar" options. Please specify only one visualizer type.');
|
|
339
|
+
}
|
|
340
|
+
if (!config.image && !config.video) {
|
|
341
|
+
throw new Error('Either "image" or "video" must be specified as the background source.');
|
|
342
|
+
}
|
|
343
|
+
if (config.image && config.video) {
|
|
344
|
+
throw new Error('Cannot use both "image" and "video" options. Please specify only one background source.');
|
|
345
|
+
}
|
|
346
|
+
|
|
79
347
|
const audioFilePath = getAudioFilePath(config);
|
|
80
|
-
const backgroundImagePath = getBackgroundImagePath(config);
|
|
81
348
|
const outVideoPath = getOutVideoPath(config);
|
|
349
|
+
const backgroundVideoPath = getBackgroundVideoPath(config);
|
|
350
|
+
const backgroundImagePath = getBackgroundImagePath(config);
|
|
351
|
+
const useVideoBackground = !!backgroundVideoPath;
|
|
82
352
|
|
|
83
|
-
const backgroundImageBmpBuffer = await convertToBmp(backgroundImagePath);
|
|
84
|
-
const backgroundImage = parseImage(backgroundImageBmpBuffer);
|
|
85
353
|
const audioReader = await createAudioBuffer(audioFilePath, FFMPEG_FORMAT);
|
|
86
354
|
const audioBuffer = audioReader.audioBuffer;
|
|
87
355
|
const sampleRate = audioReader.sampleRate;
|
|
@@ -91,29 +359,39 @@ export const renderAudioVisualizer = (config: Config, onProgress?: (progress: nu
|
|
|
91
359
|
|
|
92
360
|
const spectrumBusMargin = getSpectrumBusMargin();
|
|
93
361
|
const FPS = getFPS(config);
|
|
94
|
-
const spectrumWidth =
|
|
95
|
-
getSpectrumWidthAbsolute(config, backgroundImage.width);
|
|
96
|
-
const spectrumHeight =
|
|
97
|
-
getSpectrumHeightAbsolute(config, backgroundImage.height);
|
|
98
|
-
const spectrumX =
|
|
99
|
-
getSpectrumXAbsolute(config, spectrumWidth, backgroundImage.width);
|
|
100
|
-
const spectrumY =
|
|
101
|
-
getSpectrumYAbsolute(config, spectrumHeight, backgroundImage.height);
|
|
102
|
-
const spectrumRotation =
|
|
103
|
-
getSpectrumRotation(config);
|
|
104
|
-
const spectrumColor =
|
|
105
|
-
getSpectrumColor(config) ||
|
|
106
|
-
invertColor(getImageColor(backgroundImage));
|
|
107
|
-
const spectrumEffect = getSpectrumEffect(config);
|
|
108
|
-
const spectrumOpacity = getSpectrumOpacityParsed(config);
|
|
109
362
|
const ffmpeg_cfr = getFfmpeg_cfr(config);
|
|
110
363
|
const ffmpeg_preset = getFfmpeg_preset(config);
|
|
111
364
|
const frame_processing_delay = getFrame_processing_delay(config);
|
|
112
365
|
|
|
113
366
|
const audioDuration = audioBuffer.length / sampleRate;
|
|
114
367
|
const framesCount = Math.trunc(audioDuration * FPS);
|
|
115
|
-
const
|
|
116
|
-
|
|
368
|
+
const outputResolution = getOutputResolution(config);
|
|
369
|
+
|
|
370
|
+
const preprocessed = preProcessAudio(audioBuffer, sampleRate, FPS, framesCount);
|
|
371
|
+
|
|
372
|
+
const {
|
|
373
|
+
backgroundWidth,
|
|
374
|
+
backgroundHeight,
|
|
375
|
+
defaultColor,
|
|
376
|
+
staticBackgroundBuffer,
|
|
377
|
+
videoFrameReader,
|
|
378
|
+
videoFrameSize,
|
|
379
|
+
encodeVideoFrame,
|
|
380
|
+
concatFilePath,
|
|
381
|
+
} = await prepareBackgroundForRender({
|
|
382
|
+
useVideoBackground,
|
|
383
|
+
backgroundVideoPath,
|
|
384
|
+
backgroundImagePath,
|
|
385
|
+
beatFrameIndices: preprocessed.beatFrameIndices,
|
|
386
|
+
framesCount,
|
|
387
|
+
outputResolution,
|
|
388
|
+
fps: FPS,
|
|
389
|
+
autoEditVideo: getAutoEditVideo(config),
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
const createVisualizerFrame = createVisualizerFrameGenerator(
|
|
393
|
+
config, backgroundWidth, backgroundHeight, defaultColor, spectrumBusMargin
|
|
394
|
+
);
|
|
117
395
|
|
|
118
396
|
const ffmpegVideoWriter = spawnFfmpegVideoWriter({
|
|
119
397
|
audioFilename: audioFilePath,
|
|
@@ -125,29 +403,23 @@ export const renderAudioVisualizer = (config: Config, onProgress?: (progress: nu
|
|
|
125
403
|
});
|
|
126
404
|
ffmpegVideoWriter.on('exit', (code: number) => resolve(code));
|
|
127
405
|
|
|
128
|
-
const bpmEncoder = createBpmEncoder({ width: backgroundImage.width, height: backgroundImage.height });
|
|
129
|
-
const backgroundImageBuffer = bpmEncoder(backgroundImage.data);
|
|
130
|
-
const skipFramesCount = FPS < 45 ? 1 : 2;
|
|
131
|
-
const processSpectrum = createSpectrumsProcessor(sampleRate, skipFramesCount);
|
|
132
|
-
const createVisualizerFrame = createVisualizerFrameGenerator();
|
|
133
406
|
for (let i = 0; i < framesCount; i++) {
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
407
|
+
const spectrum = preprocessed.spectrums[i];
|
|
408
|
+
|
|
409
|
+
const backgroundImageBuffer = await resolveBackgroundFrameBuffer({
|
|
410
|
+
frameIndex: i,
|
|
411
|
+
useVideoBackground,
|
|
412
|
+
videoFrameReader,
|
|
413
|
+
videoFrameSize,
|
|
414
|
+
encodeVideoFrame,
|
|
415
|
+
staticBackgroundBuffer,
|
|
416
|
+
});
|
|
137
417
|
|
|
138
|
-
const
|
|
139
|
-
const spectrum = processSpectrum(audioDataParser);
|
|
140
|
-
const frameImage = createVisualizerFrame({
|
|
418
|
+
const commonVisualizerFrameProps: CommonVisualizerFrameProps = {
|
|
141
419
|
backgroundImageBuffer,
|
|
142
420
|
spectrum,
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
rotation: spectrumRotation,
|
|
146
|
-
margin: spectrumBusMargin,
|
|
147
|
-
color: spectrumColor,
|
|
148
|
-
opacity: spectrumOpacity,
|
|
149
|
-
spectrumEffect,
|
|
150
|
-
});
|
|
421
|
+
};
|
|
422
|
+
const frameImage = createVisualizerFrame(commonVisualizerFrameProps);
|
|
151
423
|
const isFrameProcessed = ffmpegVideoWriter.stdin.write(frameImage.data);
|
|
152
424
|
if (!isFrameProcessed) {
|
|
153
425
|
await waitDrain(ffmpegVideoWriter.stdin);
|
|
@@ -160,5 +432,11 @@ export const renderAudioVisualizer = (config: Config, onProgress?: (progress: nu
|
|
|
160
432
|
}
|
|
161
433
|
}
|
|
162
434
|
|
|
435
|
+
if (videoFrameReader) {
|
|
436
|
+
videoFrameReader.kill();
|
|
437
|
+
}
|
|
438
|
+
if (concatFilePath) {
|
|
439
|
+
cleanupConcatFile(concatFilePath);
|
|
440
|
+
}
|
|
163
441
|
ffmpegVideoWriter.stdin.end();
|
|
164
442
|
});
|
package/src/tests/image.test.ts
CHANGED
|
@@ -3,7 +3,8 @@ import path from 'path';
|
|
|
3
3
|
import {
|
|
4
4
|
drawRect,
|
|
5
5
|
Color,
|
|
6
|
-
|
|
6
|
+
createSpectrumVisualizerFrameGenerator,
|
|
7
|
+
createPolarVisualizerFrameGenerator,
|
|
7
8
|
parseImage,
|
|
8
9
|
convertToBmp,
|
|
9
10
|
mixValues,
|
|
@@ -76,14 +77,14 @@ describe('image', function () {
|
|
|
76
77
|
expect(imageDstBuffer.data).deep.equal(expectedImageData);
|
|
77
78
|
});
|
|
78
79
|
|
|
79
|
-
it('
|
|
80
|
+
it('createSpectrumVisualizerFrameGenerator', async function () {
|
|
80
81
|
const expectedImageLength = 6220854;
|
|
81
82
|
const backgroundImagePath = path.resolve('example/media/horses.png');
|
|
82
83
|
const backgroundImageBmpBuffer = await convertToBmp(backgroundImagePath);
|
|
83
84
|
const backgroundImage = parseImage(backgroundImageBmpBuffer);
|
|
84
85
|
const bpmEncoder = createBpmEncoder({ width: backgroundImage.width, height: backgroundImage.height });
|
|
85
86
|
const backgroundImageBuffer = bpmEncoder(backgroundImage.data);
|
|
86
|
-
const createVisualizerFrame =
|
|
87
|
+
const createVisualizerFrame = createSpectrumVisualizerFrameGenerator();
|
|
87
88
|
const frameSpectrumDown = createVisualizerFrame({
|
|
88
89
|
backgroundImageBuffer,
|
|
89
90
|
spectrum: [0.5, 0, 1],
|
|
@@ -136,4 +137,86 @@ describe('image', function () {
|
|
|
136
137
|
const resultSpectrumUpOpacity50 = frameSpectrumUpOpacity50.data.toJSON().data;
|
|
137
138
|
expect(resultSpectrumUpOpacity50).to.have.length(expectedImageLength);
|
|
138
139
|
});
|
|
140
|
+
|
|
141
|
+
it('createPolarVisualizerFrameGenerator', async function () {
|
|
142
|
+
const expectedImageLength = 6220854;
|
|
143
|
+
const backgroundImagePath = path.resolve('example/media/horses.png');
|
|
144
|
+
const backgroundImageBmpBuffer = await convertToBmp(backgroundImagePath);
|
|
145
|
+
const backgroundImage = parseImage(backgroundImageBmpBuffer);
|
|
146
|
+
const bpmEncoder = createBpmEncoder({ width: backgroundImage.width, height: backgroundImage.height });
|
|
147
|
+
const backgroundImageBuffer = bpmEncoder(backgroundImage.data);
|
|
148
|
+
const createPolarVisualizerFrame = createPolarVisualizerFrameGenerator();
|
|
149
|
+
|
|
150
|
+
const framePolarBasic = createPolarVisualizerFrame({
|
|
151
|
+
backgroundImageBuffer,
|
|
152
|
+
spectrum: [0.5, 0.8, 0.3, 0.9, 0.6, 0.4, 0.7, 0.2],
|
|
153
|
+
centerX: 500,
|
|
154
|
+
centerY: 400,
|
|
155
|
+
innerRadius: 100,
|
|
156
|
+
maxBarLength: 150,
|
|
157
|
+
barWidth: 5,
|
|
158
|
+
color: { red: 255, green: 100, blue: 50 },
|
|
159
|
+
opacity: 1,
|
|
160
|
+
});
|
|
161
|
+
const resultPolarBasic = framePolarBasic.data.toJSON().data;
|
|
162
|
+
expect(resultPolarBasic).to.have.length(expectedImageLength);
|
|
163
|
+
|
|
164
|
+
const framePolarVolume = createPolarVisualizerFrame({
|
|
165
|
+
backgroundImageBuffer,
|
|
166
|
+
spectrum: [0.2, 0.5, 0.8, 0.3, 0.6, 0.9, 0.4, 0.7],
|
|
167
|
+
centerX: 500,
|
|
168
|
+
centerY: 400,
|
|
169
|
+
innerRadius: 80,
|
|
170
|
+
maxBarLength: 120,
|
|
171
|
+
barWidth: 4,
|
|
172
|
+
color: { red: 100, green: 200, blue: 255 },
|
|
173
|
+
opacity: 1,
|
|
174
|
+
spectrumEffect: 'volume',
|
|
175
|
+
});
|
|
176
|
+
const resultPolarVolume = framePolarVolume.data.toJSON().data;
|
|
177
|
+
expect(resultPolarVolume).to.have.length(expectedImageLength);
|
|
178
|
+
|
|
179
|
+
const framePolarSmooth = createPolarVisualizerFrame({
|
|
180
|
+
backgroundImageBuffer,
|
|
181
|
+
spectrum: [0.7, 0.4, 0.9, 0.2, 0.5, 0.8, 0.3, 0.6],
|
|
182
|
+
centerX: 500,
|
|
183
|
+
centerY: 400,
|
|
184
|
+
innerRadius: 90,
|
|
185
|
+
maxBarLength: 130,
|
|
186
|
+
barWidth: 6,
|
|
187
|
+
color: { red: 255, green: 255, blue: 0 },
|
|
188
|
+
opacity: 1,
|
|
189
|
+
spectrumEffect: 'smooth',
|
|
190
|
+
});
|
|
191
|
+
const resultPolarSmooth = framePolarSmooth.data.toJSON().data;
|
|
192
|
+
expect(resultPolarSmooth).to.have.length(expectedImageLength);
|
|
193
|
+
|
|
194
|
+
const framePolarOpacity50 = createPolarVisualizerFrame({
|
|
195
|
+
backgroundImageBuffer,
|
|
196
|
+
spectrum: [0.6, 0.3, 0.8, 0.5, 0.4, 0.7, 0.2, 0.9],
|
|
197
|
+
centerX: 500,
|
|
198
|
+
centerY: 400,
|
|
199
|
+
innerRadius: 100,
|
|
200
|
+
maxBarLength: 140,
|
|
201
|
+
barWidth: 5,
|
|
202
|
+
color: { red: 0, green: 255, blue: 128 },
|
|
203
|
+
opacity: 0.5,
|
|
204
|
+
});
|
|
205
|
+
const resultPolarOpacity50 = framePolarOpacity50.data.toJSON().data;
|
|
206
|
+
expect(resultPolarOpacity50).to.have.length(expectedImageLength);
|
|
207
|
+
|
|
208
|
+
const framePolarHexColor = createPolarVisualizerFrame({
|
|
209
|
+
backgroundImageBuffer,
|
|
210
|
+
spectrum: [0.5, 0.6, 0.7, 0.8, 0.9, 0.4, 0.3, 0.2],
|
|
211
|
+
centerX: 500,
|
|
212
|
+
centerY: 400,
|
|
213
|
+
innerRadius: 110,
|
|
214
|
+
maxBarLength: 160,
|
|
215
|
+
barWidth: 7,
|
|
216
|
+
color: '#FF5733',
|
|
217
|
+
opacity: 1,
|
|
218
|
+
});
|
|
219
|
+
const resultPolarHexColor = framePolarHexColor.data.toJSON().data;
|
|
220
|
+
expect(resultPolarHexColor).to.have.length(expectedImageLength);
|
|
221
|
+
});
|
|
139
222
|
});
|