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/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, createVisualizerFrameGenerator } from './image';
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 audioDataStep = Math.trunc(audioBuffer.length / framesCount);
116
- const processingBuffer = new Float32Array(PROCESSING_BUFFER_SIZE).fill(0);
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 currentFrameData = PCM_FORMAT.parseFunction(audioBuffer, i * audioDataStep, i * audioDataStep + audioDataStep);
135
- processingBuffer.copyWithin(0, currentFrameData.length);
136
- processingBuffer.set(currentFrameData, PROCESSING_BUFFER_SIZE - currentFrameData.length);
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 audioDataParser = () => Array.from(processingBuffer);
139
- const spectrum = processSpectrum(audioDataParser);
140
- const frameImage = createVisualizerFrame({
418
+ const commonVisualizerFrameProps: CommonVisualizerFrameProps = {
141
419
  backgroundImageBuffer,
142
420
  spectrum,
143
- size: { width: spectrumWidth, height: spectrumHeight },
144
- position: { x: spectrumX, y: spectrumY },
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
  });
@@ -3,7 +3,8 @@ import path from 'path';
3
3
  import {
4
4
  drawRect,
5
5
  Color,
6
- createVisualizerFrameGenerator,
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('createVisualizerFrame', async function () {
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 = createVisualizerFrameGenerator();
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
  });