nodejs-audio-visualizer 4.1.1 → 4.2.2

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.
@@ -0,0 +1,284 @@
1
+ /** Output length per segment when `audio.autoHighlight` is true (seconds). */
2
+
3
+ import { waitForEventLoop } from "./waitForEventLoop";
4
+
5
+ export const HIGHLIGHT_DURATION_SEC = 15;
6
+
7
+ export interface BeatFrameEvent {
8
+ frameIndex: number;
9
+ intensity: number;
10
+ }
11
+
12
+ export interface HighlightAudioSegment {
13
+ seekSeconds: number;
14
+ durationSeconds: number;
15
+ }
16
+
17
+ /** One auto-highlight segment before concatenation (for separate output files). */
18
+ export interface HighlightRun {
19
+ startFrame: number;
20
+ highlightFrames: number;
21
+ spectrums: number[][];
22
+ beatFrameIndices: number[];
23
+ audioSegment: HighlightAudioSegment;
24
+ }
25
+
26
+ function buildHighlightRun(
27
+ fps: number,
28
+ seg: { startFrame: number; highlightFrames: number },
29
+ spectrums: number[][],
30
+ beatEvents: BeatFrameEvent[],
31
+ ): HighlightRun {
32
+ const { startFrame, highlightFrames } = seg;
33
+ const spectrumsSlice = spectrums.slice(startFrame, startFrame + highlightFrames);
34
+ const beatFrameIndices = beatEvents
35
+ .map(b => b.frameIndex)
36
+ .filter(i => i >= startFrame && i < startFrame + highlightFrames)
37
+ .map(i => i - startFrame)
38
+ .sort((a, b) => a - b);
39
+ return {
40
+ startFrame,
41
+ highlightFrames,
42
+ spectrums: spectrumsSlice,
43
+ beatFrameIndices,
44
+ audioSegment: {
45
+ seekSeconds: startFrame / fps,
46
+ durationSeconds: highlightFrames / fps,
47
+ },
48
+ };
49
+ }
50
+
51
+ function frameEnergy(spectrum: number[]): number {
52
+ let s = 0;
53
+ for (let i = 0; i < spectrum.length; i++) {
54
+ const v = spectrum[i];
55
+ s += v * v;
56
+ }
57
+ return s;
58
+ }
59
+
60
+ function rangesOverlap(
61
+ aStart: number,
62
+ aEndExclusive: number,
63
+ ranges: Array<{ start: number; endExclusive: number }>,
64
+ ): boolean {
65
+ for (const r of ranges) {
66
+ if (aStart < r.endExclusive && aEndExclusive > r.start) {
67
+ return true;
68
+ }
69
+ }
70
+ return false;
71
+ }
72
+
73
+ async function buildFrameEnergies(
74
+ spectrums: number[][],
75
+ totalFrames: number,
76
+ ): Promise<Float64Array> {
77
+ const energies = new Float64Array(totalFrames);
78
+ for (let i = 0; i < totalFrames; i++) {
79
+ energies[i] = frameEnergy(spectrums[i] ?? []);
80
+ await waitForEventLoop();
81
+ }
82
+ return energies;
83
+ }
84
+
85
+ /**
86
+ * Chooses the fixed-length window whose frames have the highest summed spectral energy
87
+ * (typical chorus / drop vs a single loud intro transient).
88
+ * Skips windows that overlap any excluded range.
89
+ */
90
+ async function findBestHighlightStart(
91
+ energies: Float64Array,
92
+ totalFrames: number,
93
+ windowFrames: number,
94
+ excludeRanges: Array<{ start: number; endExclusive: number }>,
95
+ ): Promise<number | null> {
96
+ if (totalFrames <= windowFrames) {
97
+ if (!rangesOverlap(0, totalFrames, excludeRanges)) {
98
+ return 0;
99
+ }
100
+ return null;
101
+ }
102
+
103
+ let minE = Infinity;
104
+ let maxE = -Infinity;
105
+ for (let i = 0; i < totalFrames; i++) {
106
+ const e = energies[i];
107
+ if (e < minE) {
108
+ minE = e;
109
+ }
110
+ if (e > maxE) {
111
+ maxE = e;
112
+ }
113
+ await waitForEventLoop();
114
+ }
115
+
116
+ const prefix = new Float64Array(totalFrames + 1);
117
+ for (let i = 0; i < totalFrames; i++) {
118
+ prefix[i + 1] = prefix[i] + energies[i];
119
+ await waitForEventLoop();
120
+ }
121
+
122
+ const maxStart = totalFrames - windowFrames;
123
+ let bestStart: number | null = null;
124
+ let bestScore = -Infinity;
125
+
126
+ for (let start = 0; start <= maxStart; start++) {
127
+ if (rangesOverlap(start, start + windowFrames, excludeRanges)) {
128
+ await waitForEventLoop();
129
+ continue;
130
+ }
131
+ const score = prefix[start + windowFrames] - prefix[start];
132
+ if (score > bestScore) {
133
+ bestScore = score;
134
+ bestStart = start;
135
+ }
136
+ await waitForEventLoop();
137
+ }
138
+
139
+ if (bestStart === null) {
140
+ return null;
141
+ }
142
+
143
+ if (maxE <= minE) {
144
+ const center = Math.floor((totalFrames - windowFrames) / 2);
145
+ let bestDist = Infinity;
146
+ let tieBreak: number | null = null;
147
+ for (let start = 0; start <= maxStart; start++) {
148
+ if (rangesOverlap(start, start + windowFrames, excludeRanges)) {
149
+ await waitForEventLoop();
150
+ continue;
151
+ }
152
+ const dist = Math.abs(start - center);
153
+ if (dist < bestDist) {
154
+ bestDist = dist;
155
+ tieBreak = start;
156
+ }
157
+ await waitForEventLoop();
158
+ }
159
+ return tieBreak;
160
+ }
161
+
162
+ return bestStart;
163
+ }
164
+
165
+
166
+ /**
167
+ * Picks up to `segmentCount` non-overlapping 15s windows with highest summed spectral energy each,
168
+ * sorts them chronologically, concatenates spectrums, and remaps beat indices.
169
+ */
170
+ export async function computeHighlightSlice(
171
+ fps: number,
172
+ totalFrames: number,
173
+ spectrums: number[][],
174
+ beatEvents: BeatFrameEvent[],
175
+ segmentCount = 1,
176
+ ): Promise<{
177
+ startFrame: number;
178
+ highlightFrames: number;
179
+ spectrums: number[][];
180
+ beatFrameIndices: number[];
181
+ audioSeekSeconds: number;
182
+ audioDurationSeconds: number;
183
+ audioSegments: HighlightAudioSegment[];
184
+ runs: HighlightRun[];
185
+ }> {
186
+ const highlightFramesTarget = Math.ceil(HIGHLIGHT_DURATION_SEC * fps);
187
+
188
+ if (totalFrames <= 0) {
189
+ return {
190
+ startFrame: 0,
191
+ highlightFrames: 0,
192
+ spectrums: [],
193
+ beatFrameIndices: [],
194
+ audioSeekSeconds: 0,
195
+ audioDurationSeconds: 0,
196
+ audioSegments: [],
197
+ runs: [],
198
+ };
199
+ }
200
+
201
+ if (totalFrames <= highlightFramesTarget) {
202
+ const audioSegments: HighlightAudioSegment[] = [
203
+ {
204
+ seekSeconds: 0,
205
+ durationSeconds: totalFrames / fps,
206
+ },
207
+ ];
208
+ const fullSeg = { startFrame: 0, highlightFrames: totalFrames };
209
+ return {
210
+ startFrame: 0,
211
+ highlightFrames: totalFrames,
212
+ spectrums: spectrums.slice(),
213
+ beatFrameIndices: beatEvents.map(b => b.frameIndex).sort((a, b) => a - b),
214
+ audioSeekSeconds: 0,
215
+ audioDurationSeconds: totalFrames / fps,
216
+ audioSegments,
217
+ runs: [buildHighlightRun(fps, fullSeg, spectrums, beatEvents)],
218
+ };
219
+ }
220
+
221
+ const highlightFrames = highlightFramesTarget;
222
+ const energies = await buildFrameEnergies(spectrums, totalFrames);
223
+ const excludeRanges: Array<{ start: number; endExclusive: number }> = [];
224
+ const rawSegments: Array<{ startFrame: number; highlightFrames: number }> = [];
225
+
226
+ const n = Math.max(1, Math.floor(segmentCount));
227
+ for (let k = 0; k < n; k++) {
228
+ const startFrame = await findBestHighlightStart(
229
+ energies,
230
+ totalFrames,
231
+ highlightFrames,
232
+ excludeRanges,
233
+ );
234
+ if (startFrame === null) {
235
+ break;
236
+ }
237
+ rawSegments.push({ startFrame, highlightFrames });
238
+ excludeRanges.push({ start: startFrame, endExclusive: startFrame + highlightFrames });
239
+ }
240
+
241
+ rawSegments.sort((a, b) => a.startFrame - b.startFrame);
242
+
243
+ const runs = rawSegments.map(seg =>
244
+ buildHighlightRun(fps, seg, spectrums, beatEvents),
245
+ );
246
+
247
+ const slicedSpectrums: number[][] = [];
248
+ const beatFrameIndices: number[] = [];
249
+ let outOffset = 0;
250
+
251
+ for (const seg of rawSegments) {
252
+ const chunk = spectrums.slice(seg.startFrame, seg.startFrame + seg.highlightFrames);
253
+ slicedSpectrums.push(...chunk);
254
+
255
+ for (const b of beatEvents) {
256
+ const i = b.frameIndex;
257
+ if (i >= seg.startFrame && i < seg.startFrame + seg.highlightFrames) {
258
+ beatFrameIndices.push(i - seg.startFrame + outOffset);
259
+ }
260
+ }
261
+ outOffset += seg.highlightFrames;
262
+ }
263
+
264
+ beatFrameIndices.sort((a, b) => a - b);
265
+
266
+ const totalHighlightFrames = slicedSpectrums.length;
267
+ const audioSegments: HighlightAudioSegment[] = rawSegments.map(seg => ({
268
+ seekSeconds: seg.startFrame / fps,
269
+ durationSeconds: seg.highlightFrames / fps,
270
+ }));
271
+
272
+ const first = audioSegments[0] ?? { seekSeconds: 0, durationSeconds: 0 };
273
+
274
+ return {
275
+ startFrame: rawSegments[0]?.startFrame ?? 0,
276
+ highlightFrames: totalHighlightFrames,
277
+ spectrums: slicedSpectrums,
278
+ beatFrameIndices,
279
+ audioSeekSeconds: first.seekSeconds,
280
+ audioDurationSeconds: first.durationSeconds,
281
+ audioSegments,
282
+ runs,
283
+ };
284
+ }
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import path from 'path';
1
2
  import {
2
3
  getAudioFilePath,
3
4
  getBackgroundImagePath,
@@ -28,13 +29,18 @@ import {
28
29
  getPolarColor,
29
30
  getPolarOpacityParsed,
30
31
  getAutoEditVideo,
32
+ getAudioAutoHighlight,
33
+ getAudioAutoHighlightCount,
31
34
  } from './config';
32
35
  import { createAudioBuffer, bufferToUInt8, createSpectrumsProcessor } from './audio';
33
36
  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';
37
+ import { spawnFfmpegVideoWriter, getProgress, waitDrain, getVideoInfo, spawnVideoFrameReader, readVideoFrame, detectSceneChanges, buildBeatSyncedSegments, writeConcatFile, spawnConcatVideoFrameReader, cleanupConcatFile } from './video';
35
38
  import { createBpmEncoder, createBgrFrameEncoder, EncodedBmp } from './bpmEncoder';
36
39
  import { createBeatDetector } from './beats';
37
40
  export { BeatInfo, BeatDetectorOptions } from './beats';
41
+ import { computeHighlightSlice, HIGHLIGHT_DURATION_SEC } from './highlight';
42
+ import { waitForEventLoop } from './waitForEventLoop';
43
+ export { computeHighlightSlice, HIGHLIGHT_DURATION_SEC, BeatFrameEvent, HighlightAudioSegment, HighlightRun } from './highlight';
38
44
 
39
45
  export const PCM_FORMAT = {
40
46
  bit: 8,
@@ -46,7 +52,10 @@ const PROCESSING_BUFFER_SIZE = Math.pow(2, 12);
46
52
 
47
53
  export interface Config {
48
54
  audio: {
49
- path: string
55
+ path: string;
56
+ autoHighlight?: boolean;
57
+ /** When `autoHighlight` is true, number of non-overlapping 15s windows to stitch (default 1). */
58
+ autoHighlightCount?: number;
50
59
  };
51
60
  image?: {
52
61
  path: string;
@@ -164,9 +173,10 @@ const createVisualizerFrameGenerator = (
164
173
  interface PreProcessedAudio {
165
174
  spectrums: number[][];
166
175
  beatFrameIndices: number[];
176
+ beatEvents: { frameIndex: number; intensity: number }[];
167
177
  }
168
178
 
169
- const PREPROCESS_YIELD_EVERY = 256;
179
+ const PREPROCESS_YIELD_EVERY = 1;
170
180
 
171
181
  const preProcessAudio = async (
172
182
  audioBuffer: Buffer,
@@ -182,6 +192,7 @@ const preProcessAudio = async (
182
192
 
183
193
  const spectrums: number[][] = [];
184
194
  const beatFrameIndices: number[] = [];
195
+ const beatEvents: { frameIndex: number; intensity: number }[] = [];
185
196
 
186
197
  for (let i = 0; i < framesCount; i++) {
187
198
  const currentFrameData = PCM_FORMAT.parseFunction(audioBuffer, i * audioDataStep, i * audioDataStep + audioDataStep);
@@ -195,14 +206,13 @@ const preProcessAudio = async (
195
206
  spectrums.push(spectrum);
196
207
  if (beat.isBeat) {
197
208
  beatFrameIndices.push(i);
209
+ beatEvents.push({ frameIndex: i, intensity: beat.intensity });
198
210
  }
199
211
 
200
- if ((i + 1) % PREPROCESS_YIELD_EVERY === 0) {
201
- await new Promise<void>(resolve => setImmediate(resolve));
202
- }
212
+ await waitForEventLoop();
203
213
  }
204
214
 
205
- return { spectrums, beatFrameIndices };
215
+ return { spectrums, beatFrameIndices, beatEvents };
206
216
  };
207
217
 
208
218
  async function prepareBackgroundForRender(params: {
@@ -338,8 +348,14 @@ async function resolveBackgroundFrameBuffer(params: {
338
348
  return staticBackgroundBuffer;
339
349
  }
340
350
 
351
+ export interface RenderAudioVisualizerResult {
352
+ exitCode: number;
353
+ /** Absolute paths of video files written successfully (exit code 0, full pass, no early stop). */
354
+ outputVideoFiles: string[];
355
+ }
356
+
341
357
  export const renderAudioVisualizer = (config: Config, onProgress?: (progress: number) => any, shouldStop?: () => boolean) =>
342
- new Promise<number>(async (resolve) => {
358
+ new Promise<RenderAudioVisualizerResult>(async (resolve) => {
343
359
  if (config.outVideo.spectrum && config.outVideo.polar) {
344
360
  throw new Error('Cannot use both "spectrum" and "polar" options. Please specify only one visualizer type.');
345
361
  }
@@ -378,74 +394,183 @@ export const renderAudioVisualizer = (config: Config, onProgress?: (progress: nu
378
394
  }
379
395
  const preprocessed = await preProcessAudio(audioBuffer, sampleRate, FPS, framesCount);
380
396
 
381
- const {
382
- backgroundWidth,
383
- backgroundHeight,
384
- defaultColor,
385
- staticBackgroundBuffer,
386
- videoFrameReader,
387
- videoFrameSize,
388
- encodeVideoFrame,
389
- concatFilePath,
390
- } = await prepareBackgroundForRender({
391
- useVideoBackground,
392
- backgroundVideoPath,
393
- backgroundImagePath,
394
- beatFrameIndices: preprocessed.beatFrameIndices,
395
- framesCount,
396
- outputResolution,
397
- fps: FPS,
398
- autoEditVideo: getAutoEditVideo(config),
399
- });
400
-
401
- const createVisualizerFrame = createVisualizerFrameGenerator(
402
- config, backgroundWidth, backgroundHeight, defaultColor, spectrumBusMargin
403
- );
404
-
405
- const ffmpegVideoWriter = spawnFfmpegVideoWriter({
406
- audioFilename: audioFilePath,
407
- videoFileName: outVideoPath,
408
- fps: FPS,
409
- ...(!!onProgress && { onStderr: getProgress(calculateProgress(framesCount + 1, onProgress)) }),
410
- ...(ffmpeg_cfr && { crf: ffmpeg_cfr }),
411
- ...(ffmpeg_preset && { preset: ffmpeg_preset }),
412
- });
413
- ffmpegVideoWriter.on('exit', (code: number) => resolve(code));
397
+ const autoHighlight = getAudioAutoHighlight(config);
398
+ let spectrumsForRender = preprocessed.spectrums;
399
+ let beatIndicesForRender = preprocessed.beatFrameIndices;
400
+ let framesCountForRender = framesCount;
401
+ type HighlightSliceResult = ReturnType<typeof computeHighlightSlice> extends Promise<
402
+ infer R
403
+ >
404
+ ? R
405
+ : never;
406
+ let highlightSlice: HighlightSliceResult | undefined;
407
+
408
+ if (autoHighlight) {
409
+ highlightSlice = await computeHighlightSlice(
410
+ FPS,
411
+ framesCount,
412
+ preprocessed.spectrums,
413
+ preprocessed.beatEvents,
414
+ getAudioAutoHighlightCount(config),
415
+ );
416
+ spectrumsForRender = highlightSlice.spectrums;
417
+ beatIndicesForRender = highlightSlice.beatFrameIndices;
418
+ framesCountForRender = highlightSlice.highlightFrames;
419
+ }
414
420
 
415
- for (let i = 0; i < framesCount; i++) {
416
- const spectrum = preprocessed.spectrums[i];
421
+ const separateHighlightFiles =
422
+ autoHighlight &&
423
+ highlightSlice &&
424
+ highlightSlice.runs.length > 1;
425
+
426
+ type VideoRenderPass = {
427
+ outPath: string;
428
+ spectrums: number[][];
429
+ beatIndices: number[];
430
+ frameCount: number;
431
+ audioSegment: import('./highlight').HighlightAudioSegment | undefined;
432
+ };
417
433
 
418
- const backgroundImageBuffer = await resolveBackgroundFrameBuffer({
419
- frameIndex: i,
420
- useVideoBackground,
434
+ const passes: VideoRenderPass[] =
435
+ separateHighlightFiles && highlightSlice
436
+ ? highlightSlice.runs.map((run, i) => {
437
+ const dir = path.dirname(outVideoPath);
438
+ const ext = path.extname(outVideoPath);
439
+ const base = path.basename(outVideoPath, ext);
440
+ const numberedPath = path.resolve(dir, `${base}-${i + 1}${ext}`);
441
+ return {
442
+ outPath: numberedPath,
443
+ spectrums: run.spectrums,
444
+ beatIndices: run.beatFrameIndices,
445
+ frameCount: run.highlightFrames,
446
+ audioSegment: run.audioSegment,
447
+ };
448
+ })
449
+ : [
450
+ {
451
+ outPath: outVideoPath,
452
+ spectrums: spectrumsForRender,
453
+ beatIndices: beatIndicesForRender,
454
+ frameCount: framesCountForRender,
455
+ audioSegment:
456
+ autoHighlight &&
457
+ highlightSlice &&
458
+ highlightSlice.audioSegments.length > 0
459
+ ? highlightSlice.audioSegments[0]
460
+ : undefined,
461
+ },
462
+ ];
463
+
464
+ const progressDenominator =
465
+ passes.reduce((s, p) => s + p.frameCount, 0) + passes.length;
466
+ let progressFrameBase = 0;
467
+ let lastExitCode = 0;
468
+ const outputVideoFiles: string[] = [];
469
+
470
+ passLoop: for (const pass of passes) {
471
+ const {
472
+ backgroundWidth,
473
+ backgroundHeight,
474
+ defaultColor,
475
+ staticBackgroundBuffer,
421
476
  videoFrameReader,
422
477
  videoFrameSize,
423
478
  encodeVideoFrame,
424
- staticBackgroundBuffer,
479
+ concatFilePath,
480
+ } = await prepareBackgroundForRender({
481
+ useVideoBackground,
482
+ backgroundVideoPath,
483
+ backgroundImagePath,
484
+ beatFrameIndices: pass.beatIndices,
485
+ framesCount: pass.frameCount,
486
+ outputResolution,
487
+ fps: FPS,
488
+ autoEditVideo: getAutoEditVideo(config),
489
+ });
490
+
491
+ const createVisualizerFrame = createVisualizerFrameGenerator(
492
+ config, backgroundWidth, backgroundHeight, defaultColor, spectrumBusMargin
493
+ );
494
+
495
+ const ffmpegVideoWriter = spawnFfmpegVideoWriter({
496
+ audioFilename: audioFilePath,
497
+ videoFileName: pass.outPath,
498
+ fps: FPS,
499
+ ...(pass.audioSegment && { audioSegment: pass.audioSegment }),
500
+ ...(!!onProgress && {
501
+ onStderr: getProgress((currentFrame: number) => {
502
+ const g = progressFrameBase + currentFrame;
503
+ onProgress(
504
+ +(Math.min(100, (g / progressDenominator) * 100)).toFixed(2),
505
+ );
506
+ }),
507
+ }),
508
+ ...(ffmpeg_cfr && { crf: ffmpeg_cfr }),
509
+ ...(ffmpeg_preset && { preset: ffmpeg_preset }),
510
+ });
511
+
512
+ const exitPromise = new Promise<number>(res => {
513
+ let settled = false;
514
+ ffmpegVideoWriter.on('exit', (code: number | null) => {
515
+ if (settled) {
516
+ return;
517
+ }
518
+ settled = true;
519
+ res(code ?? 0);
520
+ });
425
521
  });
426
522
 
427
- const commonVisualizerFrameProps: CommonVisualizerFrameProps = {
428
- backgroundImageBuffer,
429
- spectrum,
430
- };
431
- const frameImage = createVisualizerFrame(commonVisualizerFrameProps);
432
- const isFrameProcessed = ffmpegVideoWriter.stdin.write(frameImage.data);
433
- if (!isFrameProcessed) {
434
- await waitDrain(ffmpegVideoWriter.stdin);
523
+ let stoppedEarly = false;
524
+ for (let i = 0; i < pass.frameCount; i++) {
525
+ const spectrum = pass.spectrums[i];
526
+
527
+ const backgroundImageBuffer = await resolveBackgroundFrameBuffer({
528
+ frameIndex: i,
529
+ useVideoBackground,
530
+ videoFrameReader,
531
+ videoFrameSize,
532
+ encodeVideoFrame,
533
+ staticBackgroundBuffer,
534
+ });
535
+
536
+ const commonVisualizerFrameProps: CommonVisualizerFrameProps = {
537
+ backgroundImageBuffer,
538
+ spectrum,
539
+ };
540
+ const frameImage = createVisualizerFrame(commonVisualizerFrameProps);
541
+ const isFrameProcessed = ffmpegVideoWriter.stdin.write(frameImage.data);
542
+ if (!isFrameProcessed) {
543
+ await waitDrain(ffmpegVideoWriter.stdin);
544
+ }
545
+ if (shouldStop && shouldStop()) {
546
+ stoppedEarly = true;
547
+ break;
548
+ }
549
+ if (frame_processing_delay) {
550
+ await sleep(frame_processing_delay);
551
+ }
552
+ await waitForEventLoop();
435
553
  }
436
- if (shouldStop && shouldStop()) {
437
- break;
554
+
555
+ if (videoFrameReader) {
556
+ videoFrameReader.kill();
438
557
  }
439
- if (frame_processing_delay) {
440
- await sleep(frame_processing_delay);
558
+ if (concatFilePath) {
559
+ cleanupConcatFile(concatFilePath);
441
560
  }
442
- }
561
+ ffmpegVideoWriter.stdin.end();
443
562
 
444
- if (videoFrameReader) {
445
- videoFrameReader.kill();
446
- }
447
- if (concatFilePath) {
448
- cleanupConcatFile(concatFilePath);
563
+ lastExitCode = await exitPromise;
564
+ progressFrameBase += pass.frameCount + 1;
565
+
566
+ if (lastExitCode === 0 && !stoppedEarly) {
567
+ outputVideoFiles.push(pass.outPath);
568
+ }
569
+
570
+ if (stoppedEarly || lastExitCode !== 0) {
571
+ break passLoop;
572
+ }
449
573
  }
450
- ffmpegVideoWriter.stdin.end();
574
+
575
+ resolve({ exitCode: lastExitCode, outputVideoFiles });
451
576
  });
@@ -4,6 +4,8 @@ import { Config } from '../index';
4
4
  import {
5
5
  defaults,
6
6
  getAudioFilePath,
7
+ getAudioAutoHighlight,
8
+ getAudioAutoHighlightCount,
7
9
  getBackgroundImagePath,
8
10
  getOutVideoPath,
9
11
  getFPS,
@@ -30,6 +32,36 @@ describe('config', function() {
30
32
  expect(result).equal(expected);
31
33
  });
32
34
 
35
+ it('getAudioAutoHighlight', function() {
36
+ expect(
37
+ getAudioAutoHighlight({ audio: { path: 'a.wav' } } as Config)
38
+ ).equal(false);
39
+ expect(
40
+ getAudioAutoHighlight({ audio: { path: 'a.wav', autoHighlight: false } } as Config)
41
+ ).equal(false);
42
+ expect(
43
+ getAudioAutoHighlight({ audio: { path: 'a.wav', autoHighlight: true } } as Config)
44
+ ).equal(true);
45
+ });
46
+
47
+ it('getAudioAutoHighlightCount', function() {
48
+ expect(
49
+ getAudioAutoHighlightCount({ audio: { path: 'a.wav' } } as Config)
50
+ ).equal(1);
51
+ expect(
52
+ getAudioAutoHighlightCount({ audio: { path: 'a.wav', autoHighlightCount: 3 } } as Config)
53
+ ).equal(3);
54
+ expect(
55
+ getAudioAutoHighlightCount.bind(undefined, { audio: { path: 'a.wav', autoHighlightCount: 0 } } as Config)
56
+ ).to.throw('Invalid audio.autoHighlightCount');
57
+ expect(
58
+ getAudioAutoHighlightCount.bind(undefined, { audio: { path: 'a.wav', autoHighlightCount: 1.5 } } as Config)
59
+ ).to.throw('Invalid audio.autoHighlightCount');
60
+ expect(
61
+ getAudioAutoHighlightCount.bind(undefined, { audio: { path: 'a.wav', autoHighlightCount: NaN } } as Config)
62
+ ).to.throw('Invalid audio.autoHighlightCount');
63
+ });
64
+
33
65
  it('getBackgroundImagePath', function() {
34
66
  const result = getBackgroundImagePath({
35
67
  image: { path: 'test/path' }