nodejs-audio-visualizer 5.1.0 → 5.3.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 (51) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +81 -80
  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 +66 -34
  15. package/dist/highlight.js +471 -334
  16. package/dist/highlight.js.map +1 -1
  17. package/dist/image.d.ts +97 -92
  18. package/dist/image.js +545 -538
  19. package/dist/image.js.map +1 -1
  20. package/dist/index.d.ts +87 -79
  21. package/dist/index.js +598 -566
  22. package/dist/index.js.map +1 -1
  23. package/dist/subtitleConvert.d.ts +3 -3
  24. package/dist/subtitleConvert.js +61 -61
  25. package/dist/video.d.ts +150 -120
  26. package/dist/video.js +677 -582
  27. package/dist/video.js.map +1 -1
  28. package/dist/waitForEventLoop.d.ts +1 -1
  29. package/dist/waitForEventLoop.js +5 -5
  30. package/package.json +62 -62
  31. package/src/audio.ts +129 -129
  32. package/src/beats.ts +279 -279
  33. package/src/bpmEncoder.ts +97 -97
  34. package/src/config.ts +487 -476
  35. package/src/dsp.ts +95 -95
  36. package/src/highlight.ts +451 -291
  37. package/src/image.ts +707 -700
  38. package/src/index.ts +813 -769
  39. package/src/subtitleConvert.ts +63 -63
  40. package/src/tests/audio.test.ts +102 -102
  41. package/src/tests/beats.test.ts +134 -134
  42. package/src/tests/config.test.ts +534 -496
  43. package/src/tests/dsp.test.ts +24 -24
  44. package/src/tests/highlight.test.ts +261 -178
  45. package/src/tests/image.test.ts +319 -308
  46. package/src/tests/index.test.ts +180 -180
  47. package/src/tests/subtitleConvert.test.ts +23 -23
  48. package/src/tests/video.test.ts +535 -447
  49. package/src/typings.d.ts +28 -28
  50. package/src/video.ts +856 -722
  51. package/src/waitForEventLoop.ts +2 -2
package/dist/highlight.js CHANGED
@@ -1,335 +1,472 @@
1
- "use strict";
2
- /** Output length per segment when `audio.autoHighlight` is true (seconds). */
3
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
4
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
5
- return new (P || (P = Promise))(function (resolve, reject) {
6
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
7
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
8
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
9
- step((generator = generator.apply(thisArg, _arguments || [])).next());
10
- });
11
- };
12
- var __generator = (this && this.__generator) || function (thisArg, body) {
13
- var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
14
- return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
15
- function verb(n) { return function (v) { return step([n, v]); }; }
16
- function step(op) {
17
- if (f) throw new TypeError("Generator is already executing.");
18
- while (_) try {
19
- if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
20
- if (y = 0, t) op = [op[0] & 2, t.value];
21
- switch (op[0]) {
22
- case 0: case 1: t = op; break;
23
- case 4: _.label++; return { value: op[1], done: false };
24
- case 5: _.label++; y = op[1]; op = [0]; continue;
25
- case 7: op = _.ops.pop(); _.trys.pop(); continue;
26
- default:
27
- if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
28
- if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
29
- if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
30
- if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
31
- if (t[2]) _.ops.pop();
32
- _.trys.pop(); continue;
33
- }
34
- op = body.call(thisArg, _);
35
- } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
36
- if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
37
- }
38
- };
39
- Object.defineProperty(exports, "__esModule", { value: true });
40
- var waitForEventLoop_1 = require("./waitForEventLoop");
41
- exports.HIGHLIGHT_DURATION_SEC = 15;
42
- function buildHighlightRun(fps, seg, spectrums, beatEvents) {
43
- var startFrame = seg.startFrame, highlightFrames = seg.highlightFrames;
44
- var spectrumsSlice = spectrums.slice(startFrame, startFrame + highlightFrames);
45
- var beatsInWindow = beatEvents
46
- .filter(function (b) { return b.frameIndex >= startFrame && b.frameIndex < startFrame + highlightFrames; })
47
- .sort(function (a, b) { return a.frameIndex - b.frameIndex; });
48
- var beatFrameIndices = beatsInWindow.map(function (b) { return b.frameIndex - startFrame; });
49
- var beatIntensities = beatsInWindow.map(function (b) { return b.intensity; });
50
- return {
51
- startFrame: startFrame,
52
- highlightFrames: highlightFrames,
53
- spectrums: spectrumsSlice,
54
- beatFrameIndices: beatFrameIndices,
55
- beatIntensities: beatIntensities,
56
- audioSegment: {
57
- seekSeconds: startFrame / fps,
58
- durationSeconds: highlightFrames / fps,
59
- },
60
- };
61
- }
62
- function frameEnergy(spectrum) {
63
- var s = 0;
64
- for (var i = 0; i < spectrum.length; i++) {
65
- var v = spectrum[i];
66
- s += v * v;
67
- }
68
- return s;
69
- }
70
- function rangesOverlap(aStart, aEndExclusive, ranges) {
71
- for (var _i = 0, ranges_1 = ranges; _i < ranges_1.length; _i++) {
72
- var r = ranges_1[_i];
73
- if (aStart < r.endExclusive && aEndExclusive > r.start) {
74
- return true;
75
- }
76
- }
77
- return false;
78
- }
79
- function buildFrameEnergies(spectrums, totalFrames) {
80
- var _a;
81
- return __awaiter(this, void 0, void 0, function () {
82
- var energies, i;
83
- return __generator(this, function (_b) {
84
- switch (_b.label) {
85
- case 0:
86
- energies = new Float64Array(totalFrames);
87
- i = 0;
88
- _b.label = 1;
89
- case 1:
90
- if (!(i < totalFrames)) return [3 /*break*/, 4];
91
- energies[i] = frameEnergy((_a = spectrums[i], (_a !== null && _a !== void 0 ? _a : [])));
92
- return [4 /*yield*/, waitForEventLoop_1.waitForEventLoop()];
93
- case 2:
94
- _b.sent();
95
- _b.label = 3;
96
- case 3:
97
- i++;
98
- return [3 /*break*/, 1];
99
- case 4: return [2 /*return*/, energies];
100
- }
101
- });
102
- });
103
- }
104
- /**
105
- * Chooses the fixed-length window whose frames have the highest summed spectral energy
106
- * (typical chorus / drop vs a single loud intro transient).
107
- * Skips windows that overlap any excluded range.
108
- */
109
- function findBestHighlightStart(energies, totalFrames, windowFrames, excludeRanges) {
110
- return __awaiter(this, void 0, void 0, function () {
111
- var minE, maxE, i, e, prefix, i, maxStart, bestStart, bestScore, start, score, center, bestDist, tieBreak, start, dist;
112
- return __generator(this, function (_a) {
113
- switch (_a.label) {
114
- case 0:
115
- if (totalFrames <= windowFrames) {
116
- if (!rangesOverlap(0, totalFrames, excludeRanges)) {
117
- return [2 /*return*/, 0];
118
- }
119
- return [2 /*return*/, null];
120
- }
121
- minE = Infinity;
122
- maxE = -Infinity;
123
- i = 0;
124
- _a.label = 1;
125
- case 1:
126
- if (!(i < totalFrames)) return [3 /*break*/, 4];
127
- e = energies[i];
128
- if (e < minE) {
129
- minE = e;
130
- }
131
- if (e > maxE) {
132
- maxE = e;
133
- }
134
- return [4 /*yield*/, waitForEventLoop_1.waitForEventLoop()];
135
- case 2:
136
- _a.sent();
137
- _a.label = 3;
138
- case 3:
139
- i++;
140
- return [3 /*break*/, 1];
141
- case 4:
142
- prefix = new Float64Array(totalFrames + 1);
143
- i = 0;
144
- _a.label = 5;
145
- case 5:
146
- if (!(i < totalFrames)) return [3 /*break*/, 8];
147
- prefix[i + 1] = prefix[i] + energies[i];
148
- return [4 /*yield*/, waitForEventLoop_1.waitForEventLoop()];
149
- case 6:
150
- _a.sent();
151
- _a.label = 7;
152
- case 7:
153
- i++;
154
- return [3 /*break*/, 5];
155
- case 8:
156
- maxStart = totalFrames - windowFrames;
157
- bestStart = null;
158
- bestScore = -Infinity;
159
- start = 0;
160
- _a.label = 9;
161
- case 9:
162
- if (!(start <= maxStart)) return [3 /*break*/, 14];
163
- if (!rangesOverlap(start, start + windowFrames, excludeRanges)) return [3 /*break*/, 11];
164
- return [4 /*yield*/, waitForEventLoop_1.waitForEventLoop()];
165
- case 10:
166
- _a.sent();
167
- return [3 /*break*/, 13];
168
- case 11:
169
- score = prefix[start + windowFrames] - prefix[start];
170
- if (score > bestScore) {
171
- bestScore = score;
172
- bestStart = start;
173
- }
174
- return [4 /*yield*/, waitForEventLoop_1.waitForEventLoop()];
175
- case 12:
176
- _a.sent();
177
- _a.label = 13;
178
- case 13:
179
- start++;
180
- return [3 /*break*/, 9];
181
- case 14:
182
- if (bestStart === null) {
183
- return [2 /*return*/, null];
184
- }
185
- if (!(maxE <= minE)) return [3 /*break*/, 21];
186
- center = Math.floor((totalFrames - windowFrames) / 2);
187
- bestDist = Infinity;
188
- tieBreak = null;
189
- start = 0;
190
- _a.label = 15;
191
- case 15:
192
- if (!(start <= maxStart)) return [3 /*break*/, 20];
193
- if (!rangesOverlap(start, start + windowFrames, excludeRanges)) return [3 /*break*/, 17];
194
- return [4 /*yield*/, waitForEventLoop_1.waitForEventLoop()];
195
- case 16:
196
- _a.sent();
197
- return [3 /*break*/, 19];
198
- case 17:
199
- dist = Math.abs(start - center);
200
- if (dist < bestDist) {
201
- bestDist = dist;
202
- tieBreak = start;
203
- }
204
- return [4 /*yield*/, waitForEventLoop_1.waitForEventLoop()];
205
- case 18:
206
- _a.sent();
207
- _a.label = 19;
208
- case 19:
209
- start++;
210
- return [3 /*break*/, 15];
211
- case 20: return [2 /*return*/, tieBreak];
212
- case 21: return [2 /*return*/, bestStart];
213
- }
214
- });
215
- });
216
- }
217
- /**
218
- * Picks up to `segmentCount` non-overlapping 15s windows with highest summed spectral energy each,
219
- * sorts them chronologically, concatenates spectrums, and remaps beat indices.
220
- */
221
- function computeHighlightSlice(fps, totalFrames, spectrums, beatEvents, segmentCount) {
222
- if (segmentCount === void 0) { segmentCount = 1; }
223
- var _a, _b, _c;
224
- return __awaiter(this, void 0, void 0, function () {
225
- var highlightFramesTarget, audioSegments_1, fullSeg, fullRun, highlightFrames, energies, excludeRanges, rawSegments, n, k, startFrame, runs, slicedSpectrums, beatFrameIndices, beatIntensities, outOffset, _loop_1, _i, rawSegments_1, seg, totalHighlightFrames, audioSegments, first;
226
- return __generator(this, function (_d) {
227
- switch (_d.label) {
228
- case 0:
229
- highlightFramesTarget = Math.ceil(exports.HIGHLIGHT_DURATION_SEC * fps);
230
- if (totalFrames <= 0) {
231
- return [2 /*return*/, {
232
- startFrame: 0,
233
- highlightFrames: 0,
234
- spectrums: [],
235
- beatFrameIndices: [],
236
- beatIntensities: [],
237
- audioSeekSeconds: 0,
238
- audioDurationSeconds: 0,
239
- audioSegments: [],
240
- runs: [],
241
- }];
242
- }
243
- if (totalFrames <= highlightFramesTarget) {
244
- audioSegments_1 = [
245
- {
246
- seekSeconds: 0,
247
- durationSeconds: totalFrames / fps,
248
- },
249
- ];
250
- fullSeg = { startFrame: 0, highlightFrames: totalFrames };
251
- fullRun = buildHighlightRun(fps, fullSeg, spectrums, beatEvents);
252
- return [2 /*return*/, {
253
- startFrame: 0,
254
- highlightFrames: totalFrames,
255
- spectrums: spectrums.slice(),
256
- beatFrameIndices: fullRun.beatFrameIndices,
257
- beatIntensities: fullRun.beatIntensities,
258
- audioSeekSeconds: 0,
259
- audioDurationSeconds: totalFrames / fps,
260
- audioSegments: audioSegments_1,
261
- runs: [fullRun],
262
- }];
263
- }
264
- highlightFrames = highlightFramesTarget;
265
- return [4 /*yield*/, buildFrameEnergies(spectrums, totalFrames)];
266
- case 1:
267
- energies = _d.sent();
268
- excludeRanges = [];
269
- rawSegments = [];
270
- n = Math.max(1, Math.floor(segmentCount));
271
- k = 0;
272
- _d.label = 2;
273
- case 2:
274
- if (!(k < n)) return [3 /*break*/, 5];
275
- return [4 /*yield*/, findBestHighlightStart(energies, totalFrames, highlightFrames, excludeRanges)];
276
- case 3:
277
- startFrame = _d.sent();
278
- if (startFrame === null) {
279
- return [3 /*break*/, 5];
280
- }
281
- rawSegments.push({ startFrame: startFrame, highlightFrames: highlightFrames });
282
- excludeRanges.push({ start: startFrame, endExclusive: startFrame + highlightFrames });
283
- _d.label = 4;
284
- case 4:
285
- k++;
286
- return [3 /*break*/, 2];
287
- case 5:
288
- rawSegments.sort(function (a, b) { return a.startFrame - b.startFrame; });
289
- runs = rawSegments.map(function (seg) {
290
- return buildHighlightRun(fps, seg, spectrums, beatEvents);
291
- });
292
- slicedSpectrums = [];
293
- beatFrameIndices = [];
294
- beatIntensities = [];
295
- outOffset = 0;
296
- _loop_1 = function (seg) {
297
- var chunk = spectrums.slice(seg.startFrame, seg.startFrame + seg.highlightFrames);
298
- slicedSpectrums.push.apply(slicedSpectrums, chunk);
299
- var beatsInSeg = beatEvents
300
- .filter(function (b) { return b.frameIndex >= seg.startFrame && b.frameIndex < seg.startFrame + seg.highlightFrames; })
301
- .sort(function (a, b) { return a.frameIndex - b.frameIndex; });
302
- for (var _i = 0, beatsInSeg_1 = beatsInSeg; _i < beatsInSeg_1.length; _i++) {
303
- var b = beatsInSeg_1[_i];
304
- beatFrameIndices.push(b.frameIndex - seg.startFrame + outOffset);
305
- beatIntensities.push(b.intensity);
306
- }
307
- outOffset += seg.highlightFrames;
308
- };
309
- for (_i = 0, rawSegments_1 = rawSegments; _i < rawSegments_1.length; _i++) {
310
- seg = rawSegments_1[_i];
311
- _loop_1(seg);
312
- }
313
- totalHighlightFrames = slicedSpectrums.length;
314
- audioSegments = rawSegments.map(function (seg) { return ({
315
- seekSeconds: seg.startFrame / fps,
316
- durationSeconds: seg.highlightFrames / fps,
317
- }); });
318
- first = (_a = audioSegments[0], (_a !== null && _a !== void 0 ? _a : { seekSeconds: 0, durationSeconds: 0 }));
319
- return [2 /*return*/, {
320
- startFrame: (_c = (_b = rawSegments[0]) === null || _b === void 0 ? void 0 : _b.startFrame, (_c !== null && _c !== void 0 ? _c : 0)),
321
- highlightFrames: totalHighlightFrames,
322
- spectrums: slicedSpectrums,
323
- beatFrameIndices: beatFrameIndices,
324
- beatIntensities: beatIntensities,
325
- audioSeekSeconds: first.seekSeconds,
326
- audioDurationSeconds: first.durationSeconds,
327
- audioSegments: audioSegments,
328
- runs: runs,
329
- }];
330
- }
331
- });
332
- });
333
- }
334
- exports.computeHighlightSlice = computeHighlightSlice;
1
+ "use strict";
2
+ /** Output length per segment when `audio.autoHighlight` is true (seconds). */
3
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
4
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
5
+ return new (P || (P = Promise))(function (resolve, reject) {
6
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
7
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
8
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
9
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
10
+ });
11
+ };
12
+ var __generator = (this && this.__generator) || function (thisArg, body) {
13
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
14
+ return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
15
+ function verb(n) { return function (v) { return step([n, v]); }; }
16
+ function step(op) {
17
+ if (f) throw new TypeError("Generator is already executing.");
18
+ while (_) try {
19
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
20
+ if (y = 0, t) op = [op[0] & 2, t.value];
21
+ switch (op[0]) {
22
+ case 0: case 1: t = op; break;
23
+ case 4: _.label++; return { value: op[1], done: false };
24
+ case 5: _.label++; y = op[1]; op = [0]; continue;
25
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
26
+ default:
27
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
28
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
29
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
30
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
31
+ if (t[2]) _.ops.pop();
32
+ _.trys.pop(); continue;
33
+ }
34
+ op = body.call(thisArg, _);
35
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
36
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
37
+ }
38
+ };
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ var waitForEventLoop_1 = require("./waitForEventLoop");
41
+ exports.HIGHLIGHT_DURATION_SEC = 15;
42
+ /**
43
+ * How far (seconds) a highlight boundary may move to land on a beat. The final length can
44
+ * therefore differ from `HIGHLIGHT_DURATION_SEC` by up to twice this value.
45
+ */
46
+ exports.MAX_BEAT_SNAP_SEC = 1.5;
47
+ /** Beats per bar used when preferring highlight lengths made of whole bars. */
48
+ var BEATS_PER_BAR = 4;
49
+ /** Cost (in beats of boundary shift) for each beat the length is away from a whole bar. */
50
+ var OFF_BAR_PENALTY = 3;
51
+ /** Grid beats past the analyzed tempo region snap to a detected onset within this share of a beat. */
52
+ var GRID_ONSET_SNAP_SHARE = 0.2;
53
+ function buildHighlightRun(fps, seg, spectrums, beatEvents) {
54
+ var startFrame = seg.startFrame, highlightFrames = seg.highlightFrames;
55
+ var spectrumsSlice = spectrums.slice(startFrame, startFrame + highlightFrames);
56
+ var beatsInWindow = beatEvents
57
+ .filter(function (b) { return b.frameIndex >= startFrame && b.frameIndex < startFrame + highlightFrames; })
58
+ .sort(function (a, b) { return a.frameIndex - b.frameIndex; });
59
+ var beatFrameIndices = beatsInWindow.map(function (b) { return b.frameIndex - startFrame; });
60
+ var beatIntensities = beatsInWindow.map(function (b) { return b.intensity; });
61
+ return {
62
+ startFrame: startFrame,
63
+ highlightFrames: highlightFrames,
64
+ spectrums: spectrumsSlice,
65
+ beatFrameIndices: beatFrameIndices,
66
+ beatIntensities: beatIntensities,
67
+ audioSegment: {
68
+ seekSeconds: startFrame / fps,
69
+ durationSeconds: highlightFrames / fps,
70
+ },
71
+ };
72
+ }
73
+ function frameEnergy(spectrum) {
74
+ var s = 0;
75
+ for (var i = 0; i < spectrum.length; i++) {
76
+ var v = spectrum[i];
77
+ s += v * v;
78
+ }
79
+ return s;
80
+ }
81
+ function rangesOverlap(aStart, aEndExclusive, ranges) {
82
+ for (var _i = 0, ranges_1 = ranges; _i < ranges_1.length; _i++) {
83
+ var r = ranges_1[_i];
84
+ if (aStart < r.endExclusive && aEndExclusive > r.start) {
85
+ return true;
86
+ }
87
+ }
88
+ return false;
89
+ }
90
+ function buildFrameEnergies(spectrums, totalFrames) {
91
+ var _a;
92
+ return __awaiter(this, void 0, void 0, function () {
93
+ var energies, i;
94
+ return __generator(this, function (_b) {
95
+ switch (_b.label) {
96
+ case 0:
97
+ energies = new Float64Array(totalFrames);
98
+ i = 0;
99
+ _b.label = 1;
100
+ case 1:
101
+ if (!(i < totalFrames)) return [3 /*break*/, 4];
102
+ energies[i] = frameEnergy((_a = spectrums[i], (_a !== null && _a !== void 0 ? _a : [])));
103
+ return [4 /*yield*/, waitForEventLoop_1.waitForEventLoop()];
104
+ case 2:
105
+ _b.sent();
106
+ _b.label = 3;
107
+ case 3:
108
+ i++;
109
+ return [3 /*break*/, 1];
110
+ case 4: return [2 /*return*/, energies];
111
+ }
112
+ });
113
+ });
114
+ }
115
+ /**
116
+ * Chooses the fixed-length window whose frames have the highest summed spectral energy
117
+ * (typical chorus / drop vs a single loud intro transient).
118
+ * Skips windows that overlap any excluded range.
119
+ */
120
+ function findBestHighlightStart(energies, totalFrames, windowFrames, excludeRanges) {
121
+ return __awaiter(this, void 0, void 0, function () {
122
+ var minE, maxE, i, e, prefix, i, maxStart, bestStart, bestScore, start, score, center, bestDist, tieBreak, start, dist;
123
+ return __generator(this, function (_a) {
124
+ switch (_a.label) {
125
+ case 0:
126
+ if (totalFrames <= windowFrames) {
127
+ if (!rangesOverlap(0, totalFrames, excludeRanges)) {
128
+ return [2 /*return*/, 0];
129
+ }
130
+ return [2 /*return*/, null];
131
+ }
132
+ minE = Infinity;
133
+ maxE = -Infinity;
134
+ i = 0;
135
+ _a.label = 1;
136
+ case 1:
137
+ if (!(i < totalFrames)) return [3 /*break*/, 4];
138
+ e = energies[i];
139
+ if (e < minE) {
140
+ minE = e;
141
+ }
142
+ if (e > maxE) {
143
+ maxE = e;
144
+ }
145
+ return [4 /*yield*/, waitForEventLoop_1.waitForEventLoop()];
146
+ case 2:
147
+ _a.sent();
148
+ _a.label = 3;
149
+ case 3:
150
+ i++;
151
+ return [3 /*break*/, 1];
152
+ case 4:
153
+ prefix = new Float64Array(totalFrames + 1);
154
+ i = 0;
155
+ _a.label = 5;
156
+ case 5:
157
+ if (!(i < totalFrames)) return [3 /*break*/, 8];
158
+ prefix[i + 1] = prefix[i] + energies[i];
159
+ return [4 /*yield*/, waitForEventLoop_1.waitForEventLoop()];
160
+ case 6:
161
+ _a.sent();
162
+ _a.label = 7;
163
+ case 7:
164
+ i++;
165
+ return [3 /*break*/, 5];
166
+ case 8:
167
+ maxStart = totalFrames - windowFrames;
168
+ bestStart = null;
169
+ bestScore = -Infinity;
170
+ start = 0;
171
+ _a.label = 9;
172
+ case 9:
173
+ if (!(start <= maxStart)) return [3 /*break*/, 14];
174
+ if (!rangesOverlap(start, start + windowFrames, excludeRanges)) return [3 /*break*/, 11];
175
+ return [4 /*yield*/, waitForEventLoop_1.waitForEventLoop()];
176
+ case 10:
177
+ _a.sent();
178
+ return [3 /*break*/, 13];
179
+ case 11:
180
+ score = prefix[start + windowFrames] - prefix[start];
181
+ if (score > bestScore) {
182
+ bestScore = score;
183
+ bestStart = start;
184
+ }
185
+ return [4 /*yield*/, waitForEventLoop_1.waitForEventLoop()];
186
+ case 12:
187
+ _a.sent();
188
+ _a.label = 13;
189
+ case 13:
190
+ start++;
191
+ return [3 /*break*/, 9];
192
+ case 14:
193
+ if (bestStart === null) {
194
+ return [2 /*return*/, null];
195
+ }
196
+ if (!(maxE <= minE)) return [3 /*break*/, 21];
197
+ center = Math.floor((totalFrames - windowFrames) / 2);
198
+ bestDist = Infinity;
199
+ tieBreak = null;
200
+ start = 0;
201
+ _a.label = 15;
202
+ case 15:
203
+ if (!(start <= maxStart)) return [3 /*break*/, 20];
204
+ if (!rangesOverlap(start, start + windowFrames, excludeRanges)) return [3 /*break*/, 17];
205
+ return [4 /*yield*/, waitForEventLoop_1.waitForEventLoop()];
206
+ case 16:
207
+ _a.sent();
208
+ return [3 /*break*/, 19];
209
+ case 17:
210
+ dist = Math.abs(start - center);
211
+ if (dist < bestDist) {
212
+ bestDist = dist;
213
+ tieBreak = start;
214
+ }
215
+ return [4 /*yield*/, waitForEventLoop_1.waitForEventLoop()];
216
+ case 18:
217
+ _a.sent();
218
+ _a.label = 19;
219
+ case 19:
220
+ start++;
221
+ return [3 /*break*/, 15];
222
+ case 20: return [2 /*return*/, tieBreak];
223
+ case 21: return [2 /*return*/, bestStart];
224
+ }
225
+ });
226
+ });
227
+ }
228
+ function nearestOnset(sortedOnsets, frame) {
229
+ var lo = 0;
230
+ var hi = sortedOnsets.length;
231
+ while (lo < hi) {
232
+ var mid = (lo + hi) >> 1;
233
+ if (sortedOnsets[mid] < frame) {
234
+ lo = mid + 1;
235
+ }
236
+ else {
237
+ hi = mid;
238
+ }
239
+ }
240
+ var best = null;
241
+ for (var _i = 0, _a = [lo - 1, lo]; _i < _a.length; _i++) {
242
+ var i = _a[_i];
243
+ if (i >= 0 && i < sortedOnsets.length) {
244
+ var v = sortedOnsets[i];
245
+ if (best === null || Math.abs(v - frame) < Math.abs(best - frame)) {
246
+ best = v;
247
+ }
248
+ }
249
+ }
250
+ return best;
251
+ }
252
+ /**
253
+ * Beat frames (sorted, unique, within [0, totalFrames]) where a highlight may start or end.
254
+ * With a tempo grid: tracked beats from the analyzed region, then the regular grid beyond it
255
+ * (each grid beat nudged onto a nearby detected onset to follow slight tempo drift).
256
+ * Without one: the detected onset beats.
257
+ */
258
+ function buildBeatCandidates(fps, totalFrames, beatEvents, beatGrid) {
259
+ var _a, _b, _c;
260
+ var onsets = Array.from(new Set(beatEvents.map(function (b) { return b.frameIndex; })))
261
+ .filter(function (f) { return f >= 0 && f <= totalFrames; })
262
+ .sort(function (a, b) { return a - b; });
263
+ var period = (_b = (_a = beatGrid) === null || _a === void 0 ? void 0 : _a.periodFrames, (_b !== null && _b !== void 0 ? _b : 0));
264
+ if (!beatGrid || !(period > 0) || !isFinite(period)) {
265
+ return onsets;
266
+ }
267
+ var frames = new Set();
268
+ var lastTracked = -Infinity;
269
+ for (var _i = 0, _d = (_c = beatGrid.beatsSec, (_c !== null && _c !== void 0 ? _c : [])); _i < _d.length; _i++) {
270
+ var t = _d[_i];
271
+ var f = Math.round(t * fps);
272
+ if (f >= 0 && f <= totalFrames) {
273
+ frames.add(f);
274
+ lastTracked = Math.max(lastTracked, f);
275
+ }
276
+ }
277
+ var maxOnsetShift = period * GRID_ONSET_SNAP_SHARE;
278
+ var firstN = Math.ceil(-beatGrid.phaseFrame / period);
279
+ for (var n = firstN;; n++) {
280
+ var gridFrame = beatGrid.phaseFrame + n * period;
281
+ if (gridFrame > totalFrames) {
282
+ break;
283
+ }
284
+ if (gridFrame <= lastTracked + period / 2) {
285
+ continue;
286
+ }
287
+ var onset = nearestOnset(onsets, gridFrame);
288
+ var f = onset !== null && Math.abs(onset - gridFrame) <= maxOnsetShift
289
+ ? onset
290
+ : Math.round(gridFrame);
291
+ if (f >= 0 && f <= totalFrames) {
292
+ frames.add(f);
293
+ }
294
+ }
295
+ return Array.from(frames).sort(function (a, b) { return a - b; });
296
+ }
297
+ exports.buildBeatCandidates = buildBeatCandidates;
298
+ /**
299
+ * Moves a fixed-length window's start and end onto beats (each by at most `maxShiftFrames`),
300
+ * preferring lengths that span whole bars when the beat period is known.
301
+ * Boundaries without a nearby beat stay where they are.
302
+ */
303
+ function snapSegmentToBeats(seg, totalFrames, beatFrames, maxShiftFrames, periodFrames) {
304
+ var rawStart = seg.startFrame;
305
+ var rawEnd = seg.startFrame + seg.highlightFrames;
306
+ var near = function (target, max) {
307
+ return beatFrames.filter(function (f) { return Math.abs(f - target) <= maxShiftFrames && f >= 0 && f <= max; });
308
+ };
309
+ var startCands = near(rawStart, totalFrames - 1);
310
+ var endCands = near(rawEnd, totalFrames);
311
+ if (startCands.length === 0) {
312
+ startCands.push(rawStart);
313
+ }
314
+ if (endCands.length === 0) {
315
+ endCands.push(Math.min(rawEnd, totalFrames));
316
+ }
317
+ var period = periodFrames && periodFrames > 0 && isFinite(periodFrames) ? periodFrames : 0;
318
+ var shiftUnit = period > 0 ? period : Math.max(1, maxShiftFrames);
319
+ var best = null;
320
+ var bestCost = Infinity;
321
+ for (var _i = 0, startCands_1 = startCands; _i < startCands_1.length; _i++) {
322
+ var s = startCands_1[_i];
323
+ for (var _a = 0, endCands_1 = endCands; _a < endCands_1.length; _a++) {
324
+ var e = endCands_1[_a];
325
+ if (e - s < seg.highlightFrames / 2) {
326
+ continue;
327
+ }
328
+ var cost = (Math.abs(s - rawStart) + Math.abs(e - rawEnd)) / shiftUnit;
329
+ if (period > 0) {
330
+ var beats = Math.round((e - s) / period);
331
+ var offBar = Math.abs(beats - BEATS_PER_BAR * Math.round(beats / BEATS_PER_BAR));
332
+ cost += offBar * OFF_BAR_PENALTY;
333
+ }
334
+ if (cost < bestCost) {
335
+ bestCost = cost;
336
+ best = { startFrame: s, endFrame: e };
337
+ }
338
+ }
339
+ }
340
+ if (!best) {
341
+ return seg;
342
+ }
343
+ return { startFrame: best.startFrame, highlightFrames: best.endFrame - best.startFrame };
344
+ }
345
+ exports.snapSegmentToBeats = snapSegmentToBeats;
346
+ /**
347
+ * Picks up to `segmentCount` non-overlapping ~15s windows with highest summed spectral energy each,
348
+ * moves their boundaries onto beats (see `snapSegmentToBeats`), sorts them chronologically,
349
+ * concatenates spectrums, and remaps beat indices.
350
+ */
351
+ function computeHighlightSlice(fps, totalFrames, spectrums, beatEvents, segmentCount, beatGrid) {
352
+ if (segmentCount === void 0) { segmentCount = 1; }
353
+ var _a, _b, _c, _d;
354
+ return __awaiter(this, void 0, void 0, function () {
355
+ var highlightFramesTarget, audioSegments_1, fullSeg, fullRun, highlightFrames, energies, excludeRanges, rawSegments, beatFrames, maxShiftFrames, n, k, startFrame, snapped, runs, slicedSpectrums, beatFrameIndices, beatIntensities, outOffset, _loop_1, _i, rawSegments_1, seg, totalHighlightFrames, audioSegments, first;
356
+ return __generator(this, function (_e) {
357
+ switch (_e.label) {
358
+ case 0:
359
+ highlightFramesTarget = Math.ceil(exports.HIGHLIGHT_DURATION_SEC * fps);
360
+ if (totalFrames <= 0) {
361
+ return [2 /*return*/, {
362
+ startFrame: 0,
363
+ highlightFrames: 0,
364
+ spectrums: [],
365
+ beatFrameIndices: [],
366
+ beatIntensities: [],
367
+ audioSeekSeconds: 0,
368
+ audioDurationSeconds: 0,
369
+ audioSegments: [],
370
+ runs: [],
371
+ }];
372
+ }
373
+ if (totalFrames <= highlightFramesTarget) {
374
+ audioSegments_1 = [
375
+ {
376
+ seekSeconds: 0,
377
+ durationSeconds: totalFrames / fps,
378
+ },
379
+ ];
380
+ fullSeg = { startFrame: 0, highlightFrames: totalFrames };
381
+ fullRun = buildHighlightRun(fps, fullSeg, spectrums, beatEvents);
382
+ return [2 /*return*/, {
383
+ startFrame: 0,
384
+ highlightFrames: totalFrames,
385
+ spectrums: spectrums.slice(),
386
+ beatFrameIndices: fullRun.beatFrameIndices,
387
+ beatIntensities: fullRun.beatIntensities,
388
+ audioSeekSeconds: 0,
389
+ audioDurationSeconds: totalFrames / fps,
390
+ audioSegments: audioSegments_1,
391
+ runs: [fullRun],
392
+ }];
393
+ }
394
+ highlightFrames = highlightFramesTarget;
395
+ return [4 /*yield*/, buildFrameEnergies(spectrums, totalFrames)];
396
+ case 1:
397
+ energies = _e.sent();
398
+ excludeRanges = [];
399
+ rawSegments = [];
400
+ beatFrames = buildBeatCandidates(fps, totalFrames, beatEvents, beatGrid);
401
+ maxShiftFrames = Math.round(exports.MAX_BEAT_SNAP_SEC * fps);
402
+ n = Math.max(1, Math.floor(segmentCount));
403
+ k = 0;
404
+ _e.label = 2;
405
+ case 2:
406
+ if (!(k < n)) return [3 /*break*/, 5];
407
+ return [4 /*yield*/, findBestHighlightStart(energies, totalFrames, highlightFrames, excludeRanges)];
408
+ case 3:
409
+ startFrame = _e.sent();
410
+ if (startFrame === null) {
411
+ return [3 /*break*/, 5];
412
+ }
413
+ snapped = snapSegmentToBeats({ startFrame: startFrame, highlightFrames: highlightFrames }, totalFrames, beatFrames, maxShiftFrames, (_a = beatGrid) === null || _a === void 0 ? void 0 : _a.periodFrames);
414
+ rawSegments.push(snapped);
415
+ // Padded so the next raw window, once snapped, cannot reach back into this one.
416
+ excludeRanges.push({
417
+ start: snapped.startFrame - maxShiftFrames,
418
+ endExclusive: snapped.startFrame + snapped.highlightFrames + maxShiftFrames,
419
+ });
420
+ _e.label = 4;
421
+ case 4:
422
+ k++;
423
+ return [3 /*break*/, 2];
424
+ case 5:
425
+ rawSegments.sort(function (a, b) { return a.startFrame - b.startFrame; });
426
+ runs = rawSegments.map(function (seg) {
427
+ return buildHighlightRun(fps, seg, spectrums, beatEvents);
428
+ });
429
+ slicedSpectrums = [];
430
+ beatFrameIndices = [];
431
+ beatIntensities = [];
432
+ outOffset = 0;
433
+ _loop_1 = function (seg) {
434
+ var chunk = spectrums.slice(seg.startFrame, seg.startFrame + seg.highlightFrames);
435
+ slicedSpectrums.push.apply(slicedSpectrums, chunk);
436
+ var beatsInSeg = beatEvents
437
+ .filter(function (b) { return b.frameIndex >= seg.startFrame && b.frameIndex < seg.startFrame + seg.highlightFrames; })
438
+ .sort(function (a, b) { return a.frameIndex - b.frameIndex; });
439
+ for (var _i = 0, beatsInSeg_1 = beatsInSeg; _i < beatsInSeg_1.length; _i++) {
440
+ var b = beatsInSeg_1[_i];
441
+ beatFrameIndices.push(b.frameIndex - seg.startFrame + outOffset);
442
+ beatIntensities.push(b.intensity);
443
+ }
444
+ outOffset += seg.highlightFrames;
445
+ };
446
+ for (_i = 0, rawSegments_1 = rawSegments; _i < rawSegments_1.length; _i++) {
447
+ seg = rawSegments_1[_i];
448
+ _loop_1(seg);
449
+ }
450
+ totalHighlightFrames = slicedSpectrums.length;
451
+ audioSegments = rawSegments.map(function (seg) { return ({
452
+ seekSeconds: seg.startFrame / fps,
453
+ durationSeconds: seg.highlightFrames / fps,
454
+ }); });
455
+ first = (_b = audioSegments[0], (_b !== null && _b !== void 0 ? _b : { seekSeconds: 0, durationSeconds: 0 }));
456
+ return [2 /*return*/, {
457
+ startFrame: (_d = (_c = rawSegments[0]) === null || _c === void 0 ? void 0 : _c.startFrame, (_d !== null && _d !== void 0 ? _d : 0)),
458
+ highlightFrames: totalHighlightFrames,
459
+ spectrums: slicedSpectrums,
460
+ beatFrameIndices: beatFrameIndices,
461
+ beatIntensities: beatIntensities,
462
+ audioSeekSeconds: first.seekSeconds,
463
+ audioDurationSeconds: first.durationSeconds,
464
+ audioSegments: audioSegments,
465
+ runs: runs,
466
+ }];
467
+ }
468
+ });
469
+ });
470
+ }
471
+ exports.computeHighlightSlice = computeHighlightSlice;
335
472
  //# sourceMappingURL=highlight.js.map