nodejs-audio-visualizer 5.1.0 → 5.2.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.
Files changed (50) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +3 -2
  3. package/dist/audio.d.ts +13 -13
  4. package/dist/audio.js +128 -128
  5. package/dist/beats.d.ts +39 -39
  6. package/dist/beats.js +220 -220
  7. package/dist/bpmEncoder.d.ts +14 -14
  8. package/dist/bpmEncoder.js +97 -97
  9. package/dist/config.d.ts +78 -77
  10. package/dist/config.js +377 -369
  11. package/dist/config.js.map +1 -1
  12. package/dist/dsp.d.ts +16 -16
  13. package/dist/dsp.js +76 -76
  14. package/dist/highlight.d.ts +34 -34
  15. package/dist/highlight.js +334 -334
  16. package/dist/image.d.ts +97 -92
  17. package/dist/image.js +545 -538
  18. package/dist/image.js.map +1 -1
  19. package/dist/index.d.ts +87 -79
  20. package/dist/index.js +597 -566
  21. package/dist/index.js.map +1 -1
  22. package/dist/subtitleConvert.d.ts +3 -3
  23. package/dist/subtitleConvert.js +61 -61
  24. package/dist/video.d.ts +150 -120
  25. package/dist/video.js +677 -582
  26. package/dist/video.js.map +1 -1
  27. package/dist/waitForEventLoop.d.ts +1 -1
  28. package/dist/waitForEventLoop.js +5 -5
  29. package/package.json +62 -62
  30. package/src/audio.ts +129 -129
  31. package/src/beats.ts +279 -279
  32. package/src/bpmEncoder.ts +97 -97
  33. package/src/config.ts +487 -476
  34. package/src/dsp.ts +95 -95
  35. package/src/highlight.ts +291 -291
  36. package/src/image.ts +707 -700
  37. package/src/index.ts +79 -37
  38. package/src/subtitleConvert.ts +63 -63
  39. package/src/tests/audio.test.ts +102 -102
  40. package/src/tests/beats.test.ts +134 -134
  41. package/src/tests/config.test.ts +534 -496
  42. package/src/tests/dsp.test.ts +24 -24
  43. package/src/tests/highlight.test.ts +178 -178
  44. package/src/tests/image.test.ts +319 -308
  45. package/src/tests/index.test.ts +180 -180
  46. package/src/tests/subtitleConvert.test.ts +23 -23
  47. package/src/tests/video.test.ts +535 -447
  48. package/src/typings.d.ts +28 -28
  49. package/src/video.ts +856 -722
  50. package/src/waitForEventLoop.ts +2 -2
package/src/index.ts CHANGED
@@ -3,7 +3,7 @@ import { readFileSync } from 'fs';
3
3
  import {
4
4
  getAudioFilePath,
5
5
  getBackgroundImagePath,
6
- getBackgroundVideoPath,
6
+ getBackgroundVideoPaths,
7
7
  getOutVideoPath,
8
8
  getSubtitleRenderSpec,
9
9
  subtitleAlignmentToAss,
@@ -33,13 +33,14 @@ import {
33
33
  getPolarColor,
34
34
  getPolarOpacityParsed,
35
35
  getAutoEditVideo,
36
+ getCameraShakeEnabled,
36
37
  getAudioAutoHighlight,
37
38
  getAudioAutoHighlightCount,
38
39
  } from './config';
39
40
  import { createAudioBuffer, bufferToUInt8, createSpectrumsProcessor, pcmU8ToFloatSamples } from './audio';
40
- import { parseImage, getImageColor, getVideoFrameColor, invertColor, Color, convertToBmp, createSpectrumVisualizerFrameGenerator, createPolarVisualizerFrameGenerator, CreatePolarVisualizerFrameProps, CreateVisualizerFrameProps, CommonVisualizerFrameProps, applyCameraShake, getCutShakeOffset, buildCutShakeAmplitudes, CAMERA_SHAKE_EVERY_BEATS } from './image';
41
+ import { parseImage, getImageColor, getVideoFrameColor, invertColor, Color, convertToBmp, createSpectrumVisualizerFrameGenerator, createPolarVisualizerFrameGenerator, CreatePolarVisualizerFrameProps, CreateVisualizerFrameProps, CommonVisualizerFrameProps, applyCameraShake, getCutShakeOffset, buildCutShakeAmplitudes, CAMERA_SHAKE_DELAY_SECONDS, CAMERA_SHAKE_EVERY_BEATS } from './image';
41
42
  import { normalizeInlineSubtitlesToSrt, lrcToSrt } from './subtitleConvert';
42
- import { spawnFfmpegVideoWriter, waitDrain, waitForProcessExit, getVideoInfo, spawnVideoFrameReader, readVideoFrame, detectSceneChanges, buildBeatSyncedSegments, getCutFrameIndices, writeConcatFile, writeSubtitlesFile, spawnConcatVideoFrameReader, cleanupConcatFile, cleanupTempFile } from './video';
43
+ import { spawnFfmpegVideoWriter, waitDrain, waitForProcessExit, getVideoInfo, spawnVideoFrameReader, readVideoFrame, detectSceneChanges, buildBeatSyncedSegments, buildSequentialVideoSegments, buildShuffledVideoSegments, getCutFrameIndices, writeConcatFile, writeSubtitlesFile, spawnConcatVideoFrameReader, cleanupConcatFile, cleanupTempFile, VideoSegment } from './video';
43
44
  import { createBpmEncoder, createBgrFrameEncoder, EncodedBmp } from './bpmEncoder';
44
45
  import { createBeatDetector, estimateTempo, TempoEstimate, beatGridFrameIndices, tempoForWindow, MAX_TEMPO_ANALYSIS_SECONDS } from './beats';
45
46
  export { BeatInfo, BeatDetectorOptions, TempoEstimate, estimateTempo, beatGridFrameIndices, shiftTempoPhase, tempoForWindow } from './beats';
@@ -66,8 +67,16 @@ export interface Config {
66
67
  path: string;
67
68
  };
68
69
  video?: {
69
- path: string;
70
+ /**
71
+ * A single video path, or an array of video paths.
72
+ * With an array: when `autoEdit` is false, the videos play one after another (looping the
73
+ * sequence as needed); when `autoEdit` is true, cuts switch (shuffle) between the given
74
+ * videos instead of auto-detecting scene changes within a single video.
75
+ */
76
+ path: string | string[];
70
77
  autoEdit?: boolean;
78
+ /** When `autoEdit` is true, disables the brief camera shake applied on cuts, or every 2 beats when there are no cuts (default true). */
79
+ cameraShake?: boolean;
71
80
  };
72
81
  outVideo: {
73
82
  path: string;
@@ -263,7 +272,7 @@ const preProcessAudio = async (
263
272
 
264
273
  async function prepareBackgroundForRender(params: {
265
274
  useVideoBackground: boolean;
266
- backgroundVideoPath: string | undefined;
275
+ backgroundVideoPaths: string[] | undefined;
267
276
  backgroundImagePath: string | undefined;
268
277
  beatFrameIndices: number[];
269
278
  beatIntensities?: number[];
@@ -273,6 +282,8 @@ async function prepareBackgroundForRender(params: {
273
282
  autoEditVideo: boolean;
274
283
  readVideoFrameTimeout: number;
275
284
  tempo?: TempoEstimate | null;
285
+ /** First video to play for `video.path` arrays without `autoEdit`. */
286
+ startVideoIndex?: number;
276
287
  }): Promise<{
277
288
  backgroundWidth: number;
278
289
  backgroundHeight: number;
@@ -286,7 +297,7 @@ async function prepareBackgroundForRender(params: {
286
297
  }> {
287
298
  const {
288
299
  useVideoBackground,
289
- backgroundVideoPath,
300
+ backgroundVideoPaths,
290
301
  backgroundImagePath,
291
302
  beatFrameIndices,
292
303
  beatIntensities,
@@ -296,32 +307,56 @@ async function prepareBackgroundForRender(params: {
296
307
  autoEditVideo,
297
308
  readVideoFrameTimeout,
298
309
  tempo,
310
+ startVideoIndex = 0,
299
311
  } = params;
300
312
 
301
- if (useVideoBackground && backgroundVideoPath) {
302
- const videoInfo = await getVideoInfo(backgroundVideoPath);
303
- const sceneChanges = autoEditVideo
304
- ? await detectSceneChanges(backgroundVideoPath)
305
- : [];
306
- const backgroundWidth = outputResolution?.width ?? videoInfo.width;
307
- const backgroundHeight = outputResolution?.height ?? videoInfo.height;
313
+ if (useVideoBackground && backgroundVideoPaths && backgroundVideoPaths.length > 0) {
314
+ const videoInfos = await Promise.all(backgroundVideoPaths.map(videoPath => getVideoInfo(videoPath)));
315
+ const primaryVideoInfo = videoInfos[0];
316
+ const backgroundWidth = outputResolution?.width ?? primaryVideoInfo.width;
317
+ const backgroundHeight = outputResolution?.height ?? primaryVideoInfo.height;
308
318
 
309
319
  const videoFrameSize = backgroundWidth * backgroundHeight * 3;
310
320
  const encodeVideoFrame = createBgrFrameEncoder({ width: backgroundWidth, height: backgroundHeight });
311
321
 
312
- const segments = buildBeatSyncedSegments(
313
- beatFrameIndices,
314
- framesCount,
315
- sceneChanges,
316
- videoInfo.duration,
317
- fps,
318
- {
319
- beatIntensities,
320
- ...(autoEditVideo && tempo ? { tempo } : {}),
321
- },
322
- );
322
+ let segments: VideoSegment[];
323
+ if (backgroundVideoPaths.length > 1) {
324
+ segments = autoEditVideo
325
+ ? buildShuffledVideoSegments(
326
+ beatFrameIndices,
327
+ framesCount,
328
+ backgroundVideoPaths.length,
329
+ fps,
330
+ {
331
+ beatIntensities,
332
+ ...(tempo ? { tempo } : {}),
333
+ },
334
+ )
335
+ : buildSequentialVideoSegments(
336
+ framesCount,
337
+ videoInfos.map(info => info.duration),
338
+ fps,
339
+ startVideoIndex % backgroundVideoPaths.length,
340
+ );
341
+ } else {
342
+ const sceneChanges = autoEditVideo
343
+ ? await detectSceneChanges(backgroundVideoPaths[0])
344
+ : [];
345
+ segments = buildBeatSyncedSegments(
346
+ beatFrameIndices,
347
+ framesCount,
348
+ sceneChanges,
349
+ primaryVideoInfo.duration,
350
+ fps,
351
+ {
352
+ beatIntensities,
353
+ ...(autoEditVideo && tempo ? { tempo } : {}),
354
+ },
355
+ );
356
+ }
323
357
 
324
- const concatFilePath = writeConcatFile(segments, backgroundVideoPath, videoInfo.duration, fps);
358
+ const videoSources = videoInfos.map((info, i) => ({ path: backgroundVideoPaths[i], duration: info.duration }));
359
+ const concatFilePath = writeConcatFile(segments, videoSources, fps);
325
360
  const cutFrameIndices = autoEditVideo ? getCutFrameIndices(segments) : [];
326
361
 
327
362
  const videoFrameReader = spawnConcatVideoFrameReader({
@@ -331,8 +366,8 @@ async function prepareBackgroundForRender(params: {
331
366
  ...(outputResolution && {
332
367
  width: backgroundWidth,
333
368
  height: backgroundHeight,
334
- sourceWidth: videoInfo.width,
335
- sourceHeight: videoInfo.height,
369
+ sourceWidth: primaryVideoInfo.width,
370
+ sourceHeight: primaryVideoInfo.height,
336
371
  }),
337
372
  });
338
373
 
@@ -342,7 +377,7 @@ async function prepareBackgroundForRender(params: {
342
377
  readVideoFrameTimeout,
343
378
  );
344
379
  if (!firstFrame) {
345
- throw new Error(`Could not read frames from video: ${backgroundVideoPath}`);
380
+ throw new Error(`Could not read frames from video: ${backgroundVideoPaths.join(', ')}`);
346
381
  }
347
382
  const defaultColor = getVideoFrameColor(firstFrame, backgroundWidth, backgroundHeight);
348
383
  const staticBackgroundBuffer = encodeVideoFrame(firstFrame);
@@ -465,9 +500,9 @@ export const renderAudioVisualizer = (config: Config, onProgress?: (progress: nu
465
500
  subtitleFileIsTemporary = true;
466
501
  }
467
502
  }
468
- const backgroundVideoPath = getBackgroundVideoPath(config);
503
+ const backgroundVideoPaths = getBackgroundVideoPaths(config);
469
504
  const backgroundImagePath = getBackgroundImagePath(config);
470
- const useVideoBackground = !!backgroundVideoPath;
505
+ const useVideoBackground = !!backgroundVideoPaths && backgroundVideoPaths.length > 0;
471
506
 
472
507
  const audioReader = await createAudioBuffer(audioFilePath, FFMPEG_FORMAT);
473
508
  const audioBuffer = audioReader.audioBuffer;
@@ -543,6 +578,8 @@ export const renderAudioVisualizer = (config: Config, onProgress?: (progress: nu
543
578
  }
544
579
 
545
580
  const autoEditVideo = getAutoEditVideo(config);
581
+ const cameraShakeEnabled = getCameraShakeEnabled(config);
582
+ const shakeDelayFrames = Math.round(FPS * CAMERA_SHAKE_DELAY_SECONDS);
546
583
  let trackTempo: TempoEstimate | null = null;
547
584
  if (autoEditVideo) {
548
585
  const sampleRateNum = Number(sampleRate);
@@ -618,7 +655,8 @@ export const renderAudioVisualizer = (config: Config, onProgress?: (progress: nu
618
655
  let exitReason: string | undefined;
619
656
  const outputVideoFiles: string[] = [];
620
657
  try {
621
- passLoop: for (const pass of passes) {
658
+ passLoop: for (let passIndex = 0; passIndex < passes.length; passIndex++) {
659
+ const pass = passes[passIndex];
622
660
  const tempo = trackTempo
623
661
  ? tempoForWindow(trackTempo, pass.startFrame, FPS, pass.frameCount, pass.beatIndices)
624
662
  : null;
@@ -634,9 +672,10 @@ export const renderAudioVisualizer = (config: Config, onProgress?: (progress: nu
634
672
  videoFrameSize,
635
673
  encodeVideoFrame,
636
674
  concatFilePath,
675
+ cutFrameIndices,
637
676
  } = await prepareBackgroundForRender({
638
677
  useVideoBackground,
639
- backgroundVideoPath,
678
+ backgroundVideoPaths,
640
679
  backgroundImagePath,
641
680
  beatFrameIndices: pass.beatIndices,
642
681
  beatIntensities: pass.beatIntensities,
@@ -646,14 +685,17 @@ export const renderAudioVisualizer = (config: Config, onProgress?: (progress: nu
646
685
  autoEditVideo,
647
686
  readVideoFrameTimeout: videoTimeouts.readVideoFrame,
648
687
  ...(tempo ? { tempo } : {}),
688
+ // Each separate highlight output opens with a different video from the array.
689
+ startVideoIndex: passIndex,
649
690
  });
650
691
  reportRenderProgress();
651
692
 
693
+ const beatShakeFrames = () => (tempo
694
+ ? beatGridFrameIndices(tempo, pass.frameCount, CAMERA_SHAKE_EVERY_BEATS)
695
+ : pass.beatIndices.filter(frameIndex => frameIndex > 0).filter((_, i) => i % CAMERA_SHAKE_EVERY_BEATS === 0));
652
696
  const shakeFrames = new Set(
653
- autoEditVideo
654
- ? (tempo
655
- ? beatGridFrameIndices(tempo, pass.frameCount, CAMERA_SHAKE_EVERY_BEATS)
656
- : pass.beatIndices.filter(frameIndex => frameIndex > 0).filter((_, i) => i % CAMERA_SHAKE_EVERY_BEATS === 0))
697
+ autoEditVideo && cameraShakeEnabled
698
+ ? (cutFrameIndices.length > 0 ? cutFrameIndices : beatShakeFrames())
657
699
  : [],
658
700
  );
659
701
  const createVisualizerFrame = createVisualizerFrameGenerator(
@@ -695,7 +737,7 @@ export const renderAudioVisualizer = (config: Config, onProgress?: (progress: nu
695
737
  spectrum,
696
738
  };
697
739
  const frameImage = createVisualizerFrame(commonVisualizerFrameProps);
698
- const shakeOffset = getCutShakeOffset(i, shakeFrames, shakeAmplitudes);
740
+ const shakeOffset = getCutShakeOffset(i, shakeFrames, shakeAmplitudes, shakeDelayFrames);
699
741
  if (shakeOffset.x !== 0 || shakeOffset.y !== 0) {
700
742
  applyCameraShake(frameImage, backgroundWidth, backgroundHeight, shakeOffset.x, shakeOffset.y);
701
743
  }
@@ -1,63 +1,63 @@
1
- /** LRC timestamp line: [mm:ss.xx] text — hundredths optional after dot. */
2
- const LRC_LINE = /^\[(\d+):(\d+)(?:\.(\d+))?\]\s*(.*)$/;
3
-
4
- export const isLikelyLrc = (text: string): boolean => {
5
- const body = text.replace(/^\uFEFF/, '');
6
- for (const line of body.split(/\r?\n/)) {
7
- const t = line.trim();
8
- if (t.length === 0) {
9
- continue;
10
- }
11
- return LRC_LINE.test(t);
12
- }
13
- return false;
14
- };
15
-
16
- const msToSrtTime = (ms: number): string => {
17
- const h = Math.floor(ms / 3600000);
18
- const m = Math.floor((ms % 3600000) / 60000);
19
- const s = Math.floor((ms % 60000) / 1000);
20
- const milli = ms % 1000;
21
- const pad = (n: number, w: number) => String(n).padStart(w, '0');
22
- return `${pad(h, 2)}:${pad(m, 2)}:${pad(s, 2)},${pad(milli, 3)}`;
23
- };
24
-
25
- const DEFAULT_TAIL_MS = 2000;
26
-
27
- export const lrcToSrt = (lrc: string): string => {
28
- const body = lrc.replace(/^\uFEFF/, '');
29
- const entries: { startMs: number; text: string }[] = [];
30
- for (const line of body.split(/\r?\n/)) {
31
- const match = line.trim().match(LRC_LINE);
32
- if (!match) {
33
- continue;
34
- }
35
- const min = parseInt(match[1], 10);
36
- const sec = parseInt(match[2], 10);
37
- const centisec =
38
- match[3] !== undefined
39
- ? parseInt(match[3].padEnd(2, '0').slice(0, 2), 10)
40
- : 0;
41
- const startMs = (min * 60 + sec) * 1000 + centisec * 10;
42
- const text = (match[4] || '').trim();
43
- if (text.length > 0) {
44
- entries.push({ startMs, text });
45
- }
46
- }
47
- entries.sort((a, b) => a.startMs - b.startMs);
48
- let out = '';
49
- for (let i = 0; i < entries.length; i++) {
50
- const start = entries[i].startMs;
51
- const end =
52
- i + 1 < entries.length
53
- ? Math.max(entries[i + 1].startMs, start + 1)
54
- : start + DEFAULT_TAIL_MS;
55
- out += `${i + 1}\n`;
56
- out += `${msToSrtTime(start)} --> ${msToSrtTime(end)}\n`;
57
- out += `${entries[i].text}\n\n`;
58
- }
59
- return out.length > 0 ? out.replace(/\n*$/, '\n') : '';
60
- };
61
-
62
- export const normalizeInlineSubtitlesToSrt = (text: string): string =>
63
- isLikelyLrc(text) ? lrcToSrt(text) : text;
1
+ /** LRC timestamp line: [mm:ss.xx] text — hundredths optional after dot. */
2
+ const LRC_LINE = /^\[(\d+):(\d+)(?:\.(\d+))?\]\s*(.*)$/;
3
+
4
+ export const isLikelyLrc = (text: string): boolean => {
5
+ const body = text.replace(/^\uFEFF/, '');
6
+ for (const line of body.split(/\r?\n/)) {
7
+ const t = line.trim();
8
+ if (t.length === 0) {
9
+ continue;
10
+ }
11
+ return LRC_LINE.test(t);
12
+ }
13
+ return false;
14
+ };
15
+
16
+ const msToSrtTime = (ms: number): string => {
17
+ const h = Math.floor(ms / 3600000);
18
+ const m = Math.floor((ms % 3600000) / 60000);
19
+ const s = Math.floor((ms % 60000) / 1000);
20
+ const milli = ms % 1000;
21
+ const pad = (n: number, w: number) => String(n).padStart(w, '0');
22
+ return `${pad(h, 2)}:${pad(m, 2)}:${pad(s, 2)},${pad(milli, 3)}`;
23
+ };
24
+
25
+ const DEFAULT_TAIL_MS = 2000;
26
+
27
+ export const lrcToSrt = (lrc: string): string => {
28
+ const body = lrc.replace(/^\uFEFF/, '');
29
+ const entries: { startMs: number; text: string }[] = [];
30
+ for (const line of body.split(/\r?\n/)) {
31
+ const match = line.trim().match(LRC_LINE);
32
+ if (!match) {
33
+ continue;
34
+ }
35
+ const min = parseInt(match[1], 10);
36
+ const sec = parseInt(match[2], 10);
37
+ const centisec =
38
+ match[3] !== undefined
39
+ ? parseInt(match[3].padEnd(2, '0').slice(0, 2), 10)
40
+ : 0;
41
+ const startMs = (min * 60 + sec) * 1000 + centisec * 10;
42
+ const text = (match[4] || '').trim();
43
+ if (text.length > 0) {
44
+ entries.push({ startMs, text });
45
+ }
46
+ }
47
+ entries.sort((a, b) => a.startMs - b.startMs);
48
+ let out = '';
49
+ for (let i = 0; i < entries.length; i++) {
50
+ const start = entries[i].startMs;
51
+ const end =
52
+ i + 1 < entries.length
53
+ ? Math.max(entries[i + 1].startMs, start + 1)
54
+ : start + DEFAULT_TAIL_MS;
55
+ out += `${i + 1}\n`;
56
+ out += `${msToSrtTime(start)} --> ${msToSrtTime(end)}\n`;
57
+ out += `${entries[i].text}\n\n`;
58
+ }
59
+ return out.length > 0 ? out.replace(/\n*$/, '\n') : '';
60
+ };
61
+
62
+ export const normalizeInlineSubtitlesToSrt = (text: string): string =>
63
+ isLikelyLrc(text) ? lrcToSrt(text) : text;
@@ -1,102 +1,102 @@
1
- import { expect } from 'chai';
2
- import { Readable, Writable, Pipe } from 'stream';
3
- import child_process, { ChildProcessWithoutNullStreams } from 'child_process';
4
- import {
5
- bufferToUInt8,
6
- normalizeAudioData,
7
- pcmU8ToFloatSamples,
8
- spawnFfmpegAudioReader,
9
- skipEvery,
10
- getPeaks,
11
- correctPeaks,
12
- createSpectrumsProcessor,
13
- } from '../audio';
14
- import { createSandbox } from 'sinon';
15
-
16
- let childProcessStream = {
17
- stdin: new Writable(),
18
- stderr: new Readable(),
19
- };
20
-
21
- const videoSandbox = createSandbox();
22
-
23
- describe('audio', function () {
24
-
25
- this.beforeAll(function () {
26
- videoSandbox.stub(child_process, 'spawn').returns(childProcessStream as ChildProcessWithoutNullStreams);
27
- });
28
-
29
- this.afterAll(function () {
30
- videoSandbox.restore();
31
- });
32
-
33
- it('bufferToUInt8', function () {
34
- const expected = [1, 2, 3, 4, 5];
35
- const buffer = Buffer.from(new Uint8Array(expected));
36
- const result = bufferToUInt8(buffer, 0, 5);
37
- expect(result).deep.equal(expected);
38
- });
39
-
40
- it('normalizeAudioData', function () {
41
- const result = normalizeAudioData([4, 5, 23, 78, 2]);
42
- const expected = [-0.96875, -0.9609375, -0.8203125, -0.390625, -0.984375];
43
- expect(result).deep.equal(expected);
44
- });
45
-
46
- it('pcmU8ToFloatSamples', function () {
47
- const buffer = Buffer.from([128, 255, 0]);
48
- expect(pcmU8ToFloatSamples(buffer)).deep.equal([0, 127 / 128, -1]);
49
- });
50
-
51
- it('spawnFfmpegAudioReader', function (done) {
52
- const childProcessReadableStream = new Readable();
53
- const childProcessWritableStream = new Writable();
54
- childProcessReadableStream._read = () => { done(); };
55
- (<Pipe>childProcessWritableStream.pipe) = () => childProcessWritableStream;
56
-
57
- childProcessStream.stdin = childProcessWritableStream;
58
- childProcessStream.stderr = childProcessReadableStream;
59
-
60
- const ffmpegProcess = spawnFfmpegAudioReader('filename', 'format');
61
- ffmpegProcess.stderr.on('data', (data) => {
62
- expect(data).equal('some data');
63
- });
64
-
65
- childProcessStream.stderr.emit('data', 'some data');
66
- });
67
-
68
- it('skipEvery', () => {
69
- const sampleData = [1, 2, 3, 4];
70
- const result = sampleData.filter(skipEvery(2));
71
- const expected = [1, 3];
72
- expect(result).deep.equal(expected);
73
- });
74
-
75
- it('getPeaks', () => {
76
- const sampleData = [1, 2, 3, 4];
77
- const prevPeaks = [2, 2, 2, 5];
78
- const resultNoPrevPeaks = getPeaks(sampleData);
79
- expect(resultNoPrevPeaks).deep.equal(sampleData);
80
- const resultWithPrevPeaks = getPeaks(sampleData, prevPeaks);
81
- const expectedWithPrevPeaks = [2, 2, 3, 5];
82
- expect(resultWithPrevPeaks).deep.equal(expectedWithPrevPeaks);
83
- });
84
-
85
- it('correctPeaks', () => {
86
- const spectrums = [1, 2, 3, 4];
87
- const peaks = [2, 2, 3, 5];
88
- const result = correctPeaks(spectrums, peaks);
89
- const expected = [1 / 3, 2 / 3, 1, 0.8];
90
- expect(result).deep.equal(expected);
91
- });
92
-
93
-
94
- it('createSpectrumsProcessor', () => {
95
- const spectrumsProcessor = createSpectrumsProcessor(44100);
96
-
97
- const frame0 = spectrumsProcessor(() => ([1, 2, 3, 4]));
98
- expect(frame0).to.have.length(24);
99
- const frame1 = spectrumsProcessor(() => ([2, 2, 6, 2]));
100
- expect(frame1).to.have.length(24);
101
- });
102
- });
1
+ import { expect } from 'chai';
2
+ import { Readable, Writable, Pipe } from 'stream';
3
+ import child_process, { ChildProcessWithoutNullStreams } from 'child_process';
4
+ import {
5
+ bufferToUInt8,
6
+ normalizeAudioData,
7
+ pcmU8ToFloatSamples,
8
+ spawnFfmpegAudioReader,
9
+ skipEvery,
10
+ getPeaks,
11
+ correctPeaks,
12
+ createSpectrumsProcessor,
13
+ } from '../audio';
14
+ import { createSandbox } from 'sinon';
15
+
16
+ let childProcessStream = {
17
+ stdin: new Writable(),
18
+ stderr: new Readable(),
19
+ };
20
+
21
+ const videoSandbox = createSandbox();
22
+
23
+ describe('audio', function () {
24
+
25
+ this.beforeAll(function () {
26
+ videoSandbox.stub(child_process, 'spawn').returns(childProcessStream as ChildProcessWithoutNullStreams);
27
+ });
28
+
29
+ this.afterAll(function () {
30
+ videoSandbox.restore();
31
+ });
32
+
33
+ it('bufferToUInt8', function () {
34
+ const expected = [1, 2, 3, 4, 5];
35
+ const buffer = Buffer.from(new Uint8Array(expected));
36
+ const result = bufferToUInt8(buffer, 0, 5);
37
+ expect(result).deep.equal(expected);
38
+ });
39
+
40
+ it('normalizeAudioData', function () {
41
+ const result = normalizeAudioData([4, 5, 23, 78, 2]);
42
+ const expected = [-0.96875, -0.9609375, -0.8203125, -0.390625, -0.984375];
43
+ expect(result).deep.equal(expected);
44
+ });
45
+
46
+ it('pcmU8ToFloatSamples', function () {
47
+ const buffer = Buffer.from([128, 255, 0]);
48
+ expect(pcmU8ToFloatSamples(buffer)).deep.equal([0, 127 / 128, -1]);
49
+ });
50
+
51
+ it('spawnFfmpegAudioReader', function (done) {
52
+ const childProcessReadableStream = new Readable();
53
+ const childProcessWritableStream = new Writable();
54
+ childProcessReadableStream._read = () => { done(); };
55
+ (<Pipe>childProcessWritableStream.pipe) = () => childProcessWritableStream;
56
+
57
+ childProcessStream.stdin = childProcessWritableStream;
58
+ childProcessStream.stderr = childProcessReadableStream;
59
+
60
+ const ffmpegProcess = spawnFfmpegAudioReader('filename', 'format');
61
+ ffmpegProcess.stderr.on('data', (data) => {
62
+ expect(data).equal('some data');
63
+ });
64
+
65
+ childProcessStream.stderr.emit('data', 'some data');
66
+ });
67
+
68
+ it('skipEvery', () => {
69
+ const sampleData = [1, 2, 3, 4];
70
+ const result = sampleData.filter(skipEvery(2));
71
+ const expected = [1, 3];
72
+ expect(result).deep.equal(expected);
73
+ });
74
+
75
+ it('getPeaks', () => {
76
+ const sampleData = [1, 2, 3, 4];
77
+ const prevPeaks = [2, 2, 2, 5];
78
+ const resultNoPrevPeaks = getPeaks(sampleData);
79
+ expect(resultNoPrevPeaks).deep.equal(sampleData);
80
+ const resultWithPrevPeaks = getPeaks(sampleData, prevPeaks);
81
+ const expectedWithPrevPeaks = [2, 2, 3, 5];
82
+ expect(resultWithPrevPeaks).deep.equal(expectedWithPrevPeaks);
83
+ });
84
+
85
+ it('correctPeaks', () => {
86
+ const spectrums = [1, 2, 3, 4];
87
+ const peaks = [2, 2, 3, 5];
88
+ const result = correctPeaks(spectrums, peaks);
89
+ const expected = [1 / 3, 2 / 3, 1, 0.8];
90
+ expect(result).deep.equal(expected);
91
+ });
92
+
93
+
94
+ it('createSpectrumsProcessor', () => {
95
+ const spectrumsProcessor = createSpectrumsProcessor(44100);
96
+
97
+ const frame0 = spectrumsProcessor(() => ([1, 2, 3, 4]));
98
+ expect(frame0).to.have.length(24);
99
+ const frame1 = spectrumsProcessor(() => ([2, 2, 6, 2]));
100
+ expect(frame1).to.have.length(24);
101
+ });
102
+ });