hyperframes 0.8.160 → 0.8.162

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 (103) hide show
  1. package/dist/{add-OGBTJCU4.js → add-UW7GCPQF.js} +5 -5
  2. package/dist/{asset-DKQFFGPP.js → asset-TWBPYQOL.js} +2 -2
  3. package/dist/audio-DJIE4YLH.js +1153 -0
  4. package/dist/{auth-AQMRAFXN.js → auth-VTCEZYBT.js} +1 -1
  5. package/dist/{autoUpdate-NFQB7V2X.js → autoUpdate-JA3SV472.js} +2 -2
  6. package/dist/{backgroundChecks-7JKE2FIU.js → backgroundChecks-J3WJDPQJ.js} +3 -3
  7. package/dist/backgroundChecksWorker.js +3 -3
  8. package/dist/beats-QVJVXZV5.js +239 -0
  9. package/dist/{benchmark-RRU67WEY.js → benchmark-WNBAKOXS.js} +5 -5
  10. package/dist/{browser-K4TP4ZM2.js → browser-VJOTRIXL.js} +3 -3
  11. package/dist/{capture-T2TBROMS.js → capture-GUO23PVN.js} +1 -1
  12. package/dist/{catalog-IMIHQ4KX.js → catalog-SAC4GFIN.js} +5 -5
  13. package/dist/{catch-up-UY5MJWOG.js → catch-up-T5RIRFJF.js} +4 -4
  14. package/dist/{check-6NAJSJHQ.js → check-IRETF27J.js} +18 -18
  15. package/dist/{checkBrowser-JWNQFYTY.js → checkBrowser-5IXR4QCK.js} +14 -14
  16. package/dist/{chunk-VZ6LCTOZ.js → chunk-3LQEZ4EI.js} +1 -1
  17. package/dist/{chunk-TU2CQJO3.js → chunk-3NSVHCOH.js} +2 -2
  18. package/dist/{chunk-D2WGLWIK.js → chunk-5K34RPE5.js} +2 -2
  19. package/dist/{chunk-IYLHJV34.js → chunk-5UBZPG74.js} +1 -143
  20. package/dist/{chunk-VD7WQYTO.js → chunk-5YSHR4XC.js} +5 -5
  21. package/dist/{chunk-D46G5IQH.js → chunk-AAUK5GDI.js} +7 -7
  22. package/dist/{chunk-GOASXWDD.js → chunk-FF5TAEPT.js} +1 -1
  23. package/dist/chunk-FFJ5TDRD.js +1181 -0
  24. package/dist/{chunk-PINFPWRV.js → chunk-GIPNECWL.js} +12 -10
  25. package/dist/{chunk-KWRBEDJI.js → chunk-GNFWKGSQ.js} +2 -2
  26. package/dist/{chunk-4VYNMVMC.js → chunk-GS5EXZBV.js} +4 -4
  27. package/dist/chunk-J4L5TPLS.js +101 -0
  28. package/dist/{chunk-V2FQQILJ.js → chunk-JJYFRAI4.js} +1 -1
  29. package/dist/{chunk-IGTGLGML.js → chunk-KAKBRDU4.js} +2 -2
  30. package/dist/{chunk-TTFDAWCS.js → chunk-KIJO75L4.js} +1 -1
  31. package/dist/{chunk-3XPVZLAR.js → chunk-NQR5SBXX.js} +1 -1
  32. package/dist/{chunk-6MCASYIS.js → chunk-ON2HYNGB.js} +4 -4
  33. package/dist/{chunk-GOTCGKX6.js → chunk-RGYEL7QD.js} +1 -1
  34. package/dist/{chunk-7OFNPEKR.js → chunk-S6OLD4SL.js} +2 -2
  35. package/dist/{chunk-FRGYHRP6.js → chunk-T5BG7PYZ.js} +1 -1
  36. package/dist/{chunk-MYGXKD7A.js → chunk-T65MIV36.js} +2 -2
  37. package/dist/chunk-TDPVEMBS.js +156 -0
  38. package/dist/{chunk-SHVDJON6.js → chunk-XWWYGXJ5.js} +1 -1
  39. package/dist/{chunk-37LNY2EC.js → chunk-YJKUE3LV.js} +1 -1
  40. package/dist/{chunk-KRE55WLI.js → chunk-YLBV2PXM.js} +1 -1
  41. package/dist/{chunk-ICZVCVD3.js → chunk-ZEWX3GHP.js} +1 -1
  42. package/dist/{clean-RRE6PMKZ.js → clean-WB62QULT.js} +6 -6
  43. package/dist/cli.js +49 -49
  44. package/dist/{cloud-OIR6K7TS.js → cloud-E5WB4DWZ.js} +4 -4
  45. package/dist/{compare-E57W737I.js → compare-GHFVHZ5A.js} +11 -11
  46. package/dist/{component-I2O4AX56.js → component-ZGCC3MHX.js} +3 -3
  47. package/dist/{compositions-B6NKZHIN.js → compositions-THPEDDIX.js} +5 -5
  48. package/dist/{delete-DTUNBC6K.js → delete-6FWT7DZX.js} +4 -3
  49. package/dist/{doctor-SCNB73Z3.js → doctor-RDGDOUC4.js} +3 -3
  50. package/dist/{events-6LQNRKHZ.js → events-CU6FDXHC.js} +3 -3
  51. package/dist/{events-Q37UJL36.js → events-VHXICDA2.js} +2 -2
  52. package/dist/{feedback-FOEBE2OW.js → feedback-PUPN6EKP.js} +5 -5
  53. package/dist/{figma-54MRTBKH.js → figma-ZKWBF7QH.js} +3 -3
  54. package/dist/fontLocalizeCli.js +1 -1
  55. package/dist/{get-EPGHM2SA.js → get-JHIBQRPM.js} +4 -3
  56. package/dist/{grade-compare-LOJWX6SK.js → grade-compare-GPLUXDPW.js} +11 -11
  57. package/dist/{help-6FPZI3WP.js → help-GO6RGM37.js} +1 -1
  58. package/dist/{history-GRXSUA5R.js → history-RUTC4SH5.js} +6 -6
  59. package/dist/hyperframes-player.global.js +1 -1
  60. package/dist/{info-LV7DFCLD.js → info-773VXW37.js} +5 -5
  61. package/dist/{init-3J7BSAC3.js → init-YXSIRYZI.js} +6 -6
  62. package/dist/{inspect-5RON554C.js → inspect-L4RRJEAA.js} +13 -13
  63. package/dist/{keyframes-KHXTHI5C.js → keyframes-P5IOQ6AM.js} +6 -6
  64. package/dist/{layout-DVWWT2TR.js → layout-OH2VTN5E.js} +13 -13
  65. package/dist/{lint-Q5WFB5ST.js → lint-YEEPVE6Q.js} +6 -6
  66. package/dist/{list-RFEUTW6N.js → list-QURRZTVN.js} +4 -3
  67. package/dist/{login-WBHWA4M2.js → login-DFT4L5YY.js} +4 -4
  68. package/dist/{media-treatment-WRHVNWLS.js → media-treatment-ERQ2PBIN.js} +6 -6
  69. package/dist/{motionShot-BZLRN3SS.js → motionShot-C2YDYRNB.js} +5 -5
  70. package/dist/{normalize-audio-4YXE3RIF.js → normalize-audio-R23VODOA.js} +4 -4
  71. package/dist/{open-MCGYEFML.js → open-GEOB4L3S.js} +4 -4
  72. package/dist/{play-WXOV3BD7.js → play-BYUS37GA.js} +9 -9
  73. package/dist/{present-RVG4J6CJ.js → present-BZLRNHDY.js} +9 -9
  74. package/dist/{preview-PDUQR2JQ.js → preview-TXML6PQ7.js} +5 -5
  75. package/dist/publish-VX5OH4ST.js +1 -1
  76. package/dist/{render-5BGU3FXU.js → render-57SXH6NS.js} +7 -7
  77. package/dist/{render-JNJVFBFF.js → render-JTY7XQFQ.js} +11 -86
  78. package/dist/skills/hyperframes-cli/references/beats.md +2 -6
  79. package/dist/{skills-LKJA27ZN.js → skills-2LBVMKVY.js} +5 -5
  80. package/dist/{skillsUpdateCheck-RFZJKZC4.js → skillsUpdateCheck-5DRXL565.js} +3 -3
  81. package/dist/{snapshot-EOYA4MLG.js → snapshot-V2L77FCF.js} +13 -13
  82. package/dist/{staticProjectServer-S3NK3V4S.js → staticProjectServer-4CQ3KDQK.js} +5 -5
  83. package/dist/studio/assets/{hyperframes-player-CXL23NEv.js → hyperframes-player-CAclPB6q.js} +1 -1
  84. package/dist/studio/assets/{index-D7BvLHtZ.js → index-B82aOvgB.js} +118 -118
  85. package/dist/studio/assets/{index-CejKvBNw.js → index-DCwGKghK.js} +1 -1
  86. package/dist/studio/index.html +1 -1
  87. package/dist/{studio-api-YM5HLXUS.js → studio-api-RYCDLEOR.js} +3 -3
  88. package/dist/{studioServer-CEXJK5TC.js → studioServer-B7H7EQ4U.js} +9 -9
  89. package/dist/{telemetry-7AWYECPL.js → telemetry-WVHWY3DU.js} +3 -3
  90. package/dist/{timeline-G7H373XX.js → timeline-EDXJ5L45.js} +8 -8
  91. package/dist/{tokens-GYH7NXXA.js → tokens-UNJONAXT.js} +2 -2
  92. package/dist/{transcribe-CIC7WAT5.js → transcribe-VX7ZE2IZ.js} +3 -3
  93. package/dist/{updateCheck-MQ44KGUR.js → updateCheck-NLH5E44Q.js} +2 -2
  94. package/dist/{upgrade-OL47SID3.js → upgrade-RGQJA2B4.js} +2 -2
  95. package/dist/{validate-TDCH6X3C.js → validate-4AN4PDV7.js} +11 -11
  96. package/package.json +2 -3
  97. package/dist/audio-GK4OVVTH.js +0 -2086
  98. package/dist/beat-analyzer.global.js +0 -341
  99. package/dist/beats-C3ZSK2R2.js +0 -322
  100. package/dist/lib-TPG2ZMAX.js +0 -128
  101. package/dist/studio/assets/index-Bl_xrA5T.js +0 -1
  102. /package/dist/{capture-ZPRK3TOZ.js → capture-M5ZAD3BI.js} +0 -0
  103. /package/dist/{lambda-4KCSMIM6.js → lambda-RAJA6ZNA.js} +0 -0
@@ -1,2086 +0,0 @@
1
- import { createRequire as __hf_createRequire } from "node:module";
2
- import { fileURLToPath as __hf_fileURLToPath } from "node:url";
3
- import { dirname as __hf_dirname } from "node:path";
4
- var require = __hf_createRequire(import.meta.url);
5
- var __filename = __hf_fileURLToPath(import.meta.url);
6
- var __dirname = __hf_dirname(__filename);
7
- import {
8
- describeProject
9
- } from "./chunk-GJQHHAAX.js";
10
- import "./chunk-WPNE5NEP.js";
11
- import {
12
- resolveProject
13
- } from "./chunk-37LNY2EC.js";
14
- import "./chunk-K2FLNJVB.js";
15
- import {
16
- ensureDOMParser
17
- } from "./chunk-442JNXRM.js";
18
- import "./chunk-6JR2XUEE.js";
19
- import "./chunk-KRE55WLI.js";
20
- import "./chunk-ICZVCVD3.js";
21
- import "./chunk-VWG7OQKU.js";
22
- import "./chunk-QWQ6GQTI.js";
23
- import {
24
- assertKnownFlags
25
- } from "./chunk-UPFUC4DB.js";
26
- import {
27
- defineCommand
28
- } from "./chunk-S3S4TGEH.js";
29
- import "./chunk-54HJ63Q2.js";
30
- import {
31
- CliUsageError
32
- } from "./chunk-PBN6GBJE.js";
33
- import "./chunk-ZTZA4UWZ.js";
34
- import {
35
- withMeta
36
- } from "./chunk-FRGYHRP6.js";
37
- import "./chunk-K4WIOZFP.js";
38
- import "./chunk-XUXGIA7H.js";
39
- import "./chunk-WERLJGIC.js";
40
- import "./chunk-ISN5GRYI.js";
41
- import "./chunk-TU2CQJO3.js";
42
- import "./chunk-XT6MXC7A.js";
43
- import "./chunk-PLEMSN3E.js";
44
- import "./chunk-LYIG2GZ5.js";
45
- import "./chunk-KCF6GHJP.js";
46
- import "./chunk-JYVGYKXG.js";
47
- import "./chunk-7KDQAD2W.js";
48
- import "./chunk-Q3TFX4OZ.js";
49
- import "./chunk-P47XYVEQ.js";
50
- import "./chunk-VC76R6W2.js";
51
- import "./chunk-UJKQWGFF.js";
52
- import "./chunk-ALLKNYC3.js";
53
- import "./chunk-OFQWTKPW.js";
54
- import "./chunk-DPVXBVLL.js";
55
- import "./chunk-T76PLPD6.js";
56
- import "./chunk-FXMKHHPO.js";
57
- import "./chunk-RQVDTPGD.js";
58
- import "./chunk-TWWSYAKJ.js";
59
- import "./chunk-2742O5DO.js";
60
- import "./chunk-IWFLMU2N.js";
61
- import "./chunk-3VGU2SH7.js";
62
- import {
63
- findFfBinary
64
- } from "./chunk-VAS6HQX3.js";
65
- import "./chunk-3RM2ETQT.js";
66
- import "./chunk-BSQ7XANM.js";
67
- import "./chunk-SDYDNHEE.js";
68
- import "./chunk-TYXH3EKP.js";
69
- import "./chunk-IXLUTE6N.js";
70
- import "./chunk-KNU5COPB.js";
71
- import "./chunk-3GXQPCJC.js";
72
- import "./chunk-M2WBOQ2Q.js";
73
-
74
- // src/commands/audio.ts
75
- import { resolve as resolve2 } from "path";
76
-
77
- // ../core/dist/fft.js
78
- function fft(re, im) {
79
- const n = re.length;
80
- for (let i = 1, j = 0; i < n; i++) {
81
- let bit = n >> 1;
82
- for (; j & bit; bit >>= 1)
83
- j ^= bit;
84
- j ^= bit;
85
- if (i < j) {
86
- [re[i], re[j]] = [re[j], re[i]];
87
- [im[i], im[j]] = [im[j], im[i]];
88
- }
89
- }
90
- for (let len = 2; len <= n; len <<= 1) {
91
- const ang = -2 * Math.PI / len;
92
- const wr = Math.cos(ang);
93
- const wi = Math.sin(ang);
94
- for (let i = 0; i < n; i += len) {
95
- let cr = 1;
96
- let ci = 0;
97
- for (let k = 0; k < len / 2; k++) {
98
- const ur = re[i + k];
99
- const ui = im[i + k];
100
- const vr = re[i + k + len / 2] * cr - im[i + k + len / 2] * ci;
101
- const vi = re[i + k + len / 2] * ci + im[i + k + len / 2] * cr;
102
- re[i + k] = ur + vr;
103
- im[i + k] = ui + vi;
104
- re[i + k + len / 2] = ur - vr;
105
- im[i + k + len / 2] = ui - vi;
106
- const ncr = cr * wr - ci * wi;
107
- ci = cr * wi + ci * wr;
108
- cr = ncr;
109
- }
110
- }
111
- }
112
- }
113
-
114
- // ../core/dist/audioRhythm.js
115
- var FRAMES_PER_SECOND = 100;
116
- var LOW_BAND_HZ = 150;
117
- var SLOWEST_BPM = 40;
118
- var FASTEST_BPM = 240;
119
- var TIGHTNESS = 100;
120
- var round = (t) => Math.round(t * 1e3) / 1e3;
121
- function bandEdges(sampleRate, size) {
122
- const binHz = sampleRate / size;
123
- const edges = [];
124
- for (let hz = 30; hz < Math.min(9e3, sampleRate / 2); hz *= 2 ** (1 / 5))
125
- edges.push(Math.max(1, Math.round(hz / binHz)));
126
- return edges;
127
- }
128
- function addFramePower(samples, center, window, edges, into) {
129
- const size = window.length;
130
- const re = new Float64Array(size);
131
- const im = new Float64Array(size);
132
- const first = center - size / 2;
133
- for (let i = 0; i < size; i++) {
134
- const at = first + i;
135
- re[i] = at >= 0 && at < samples.length ? samples[at] * window[i] : 0;
136
- }
137
- fft(re, im);
138
- for (let b = 0; b + 1 < edges.length; b++) {
139
- const end = Math.max(edges[b] + 1, edges[b + 1]);
140
- let power = 0;
141
- for (let k = edges[b]; k < end; k++)
142
- power += re[k] * re[k] + im[k] * im[k];
143
- into[b] += power / (size * size * (end - edges[b]));
144
- }
145
- }
146
- function bandFrames(signal) {
147
- const { sampleRate, channels } = signal;
148
- const hop = Math.max(1, Math.round(sampleRate / FRAMES_PER_SECOND));
149
- const size = 2 ** Math.round(Math.log2(sampleRate * 0.046));
150
- const window = Float64Array.from({ length: size }, (_, i) => 0.5 - 0.5 * Math.cos(2 * Math.PI * i / size));
151
- const edges = bandEdges(sampleRate, size);
152
- const length = channels[0]?.length ?? 0;
153
- const bands = [];
154
- for (let center = 0; center < length; center += hop) {
155
- const power = new Float64Array(Math.max(0, edges.length - 1));
156
- for (const channel of channels)
157
- addFramePower(channel, center, window, edges, power);
158
- bands.push(power.map((p) => Math.log1p(1e9 * p)));
159
- }
160
- return { fps: sampleRate / hop, bands };
161
- }
162
- function aboveLocalMean(curve, radius) {
163
- const sums = new Float64Array(curve.length + 1);
164
- for (let i = 0; i < curve.length; i++)
165
- sums[i + 1] = sums[i] + curve[i];
166
- return curve.map((value, i) => {
167
- const from = Math.max(0, i - radius);
168
- const to = Math.min(curve.length, i + radius + 1);
169
- return Math.max(0, value - (sums[to] - sums[from]) / (to - from));
170
- });
171
- }
172
- function standardized(curve) {
173
- const mean = curve.reduce((a, b) => a + b, 0) / (curve.length || 1);
174
- const spread = Math.sqrt(curve.reduce((a, v) => a + (v - mean) ** 2, 0) / (curve.length || 1));
175
- return spread > 0 ? curve.map((v) => v / spread) : curve.map(() => 0);
176
- }
177
- function onsetCurves({ fps, bands }, sampleRate) {
178
- const size = 2 ** Math.round(Math.log2(sampleRate * 0.046));
179
- const lowBands = bandEdges(sampleRate, size).filter((bin) => bin * sampleRate / size < LOW_BAND_HZ).length;
180
- const broad = new Float64Array(bands.length);
181
- const low = new Float64Array(bands.length);
182
- for (let f = 1; f < bands.length; f++) {
183
- const frame2 = bands[f];
184
- const previous = bands[f - 1];
185
- for (let b = 0; b < frame2.length; b++) {
186
- const rise = Math.max(0, frame2[b] - previous[b]);
187
- broad[f] += rise;
188
- if (b < lowBands)
189
- low[f] += rise;
190
- }
191
- }
192
- const radius = Math.round(0.4 * fps);
193
- return {
194
- fps,
195
- broad: standardized(aboveLocalMean(broad, radius)),
196
- low: standardized(aboveLocalMean(low, radius))
197
- };
198
- }
199
- function autocorrelation(curve, maxLag) {
200
- const mean = curve.reduce((a, b) => a + b, 0) / (curve.length || 1);
201
- const centred = curve.map((v) => v - mean);
202
- const ac = new Float64Array(maxLag + 1);
203
- for (let lag = 0; lag <= maxLag && lag < centred.length; lag++) {
204
- let sum = 0;
205
- for (let i = lag; i < centred.length; i++)
206
- sum += centred[i] * centred[i - lag];
207
- ac[lag] = sum;
208
- }
209
- const zero = ac[0] || 1;
210
- return ac.map((v) => v / zero);
211
- }
212
- var tempoPrior = (lag, fps) => Math.exp(-0.5 * Math.log2(60 / (lag / fps) / 120) ** 2);
213
- var fullRange = (fps) => ({
214
- shortest: Math.max(1, Math.floor(60 / FASTEST_BPM * fps)),
215
- longest: Math.ceil(60 / SLOWEST_BPM * fps)
216
- });
217
- function beatPeriod(curve, fps, { shortest, longest } = fullRange(fps)) {
218
- const ac = autocorrelation(curve, 2 * longest + 1);
219
- const at = (lag) => ac[Math.min(ac.length - 1, Math.max(0, lag))];
220
- const score = (lag) => tempoPrior(lag, fps) * (at(lag) + 0.5 * at(2 * lag) + 0.25 * (at(2 * lag - 1) + at(2 * lag + 1)));
221
- let best = shortest;
222
- for (let lag = shortest; lag <= longest; lag++)
223
- if (score(lag) > score(best))
224
- best = lag;
225
- const [a, b, c] = [score(best - 1), score(best), score(best + 1)];
226
- const curvature = a - 2 * b + c;
227
- const shift = curvature ? Math.max(-0.5, Math.min(0.5, 0.5 * (a - c) / curvature)) : 0;
228
- return { period: best + shift, clarity: Math.max(0, Math.min(1, at(best))) };
229
- }
230
- function smoothed(curve, period) {
231
- const half = Math.max(1, Math.round(period / 8));
232
- const kernel = Array.from({ length: 2 * half + 1 }, (_, i) => Math.exp(-0.5 * ((i - half) * 32 / period) ** 2));
233
- return curve.map((_, i) => {
234
- let sum = 0;
235
- for (let j = 0; j < kernel.length; j++)
236
- sum += kernel[j] * (curve[i + j - half] ?? 0);
237
- return sum;
238
- });
239
- }
240
- function cumulativeScore(local, periodAt) {
241
- const score = new Float64Array(local.length);
242
- const back = new Int32Array(local.length).fill(-1);
243
- for (let i = 0; i < local.length; i++) {
244
- const period = periodAt(i);
245
- let best = -Infinity;
246
- let from = -1;
247
- for (let j = Math.max(0, Math.round(i - 2 * period)); j <= i - Math.round(period / 2); j++) {
248
- const candidate = score[j] - TIGHTNESS * Math.log((i - j) / period) ** 2;
249
- if (candidate > best)
250
- [best, from] = [candidate, j];
251
- }
252
- score[i] = local[i] + Math.max(0, best);
253
- back[i] = best > 0 ? from : -1;
254
- }
255
- return { score, back };
256
- }
257
- function lastBeat(score) {
258
- const maxima = [];
259
- for (let i = 1; i + 1 < score.length; i++)
260
- if (score[i] > score[i - 1] && score[i] >= score[i + 1])
261
- maxima.push(i);
262
- if (!maxima.length)
263
- return score.length - 1;
264
- const median = maxima.map((i) => score[i]).sort((a, b) => a - b)[maxima.length >> 1];
265
- for (let k = maxima.length - 1; k >= 0; k--)
266
- if (score[maxima[k]] >= 0.5 * median)
267
- return maxima[k];
268
- return maxima[maxima.length - 1];
269
- }
270
- function trimmed(beats, local) {
271
- const rms = Math.sqrt(beats.reduce((a, b) => a + local[b] ** 2, 0) / (beats.length || 1));
272
- let first = 0;
273
- let end = beats.length;
274
- while (first < end && local[beats[first]] < 0.5 * rms)
275
- first++;
276
- while (end > first && local[beats[end - 1]] < 0.5 * rms)
277
- end--;
278
- return beats.slice(first, end);
279
- }
280
- var LOCAL_WINDOW_SECONDS = 8;
281
- function localPeriods(curve, fps, period) {
282
- const window = Math.round(LOCAL_WINDOW_SECONDS * fps);
283
- const hop = Math.round(fps);
284
- const range = {
285
- shortest: Math.max(fullRange(fps).shortest, Math.floor(period * 0.7)),
286
- longest: Math.min(fullRange(fps).longest, Math.ceil(period * 1.4))
287
- };
288
- const centres = [];
289
- const periods = [];
290
- for (let start = 0; start === 0 || start + window <= curve.length; start += hop) {
291
- const end = Math.min(curve.length, start + window);
292
- periods.push(beatPeriod(curve.subarray(start, end), fps, range).period);
293
- centres.push((start + end) / 2);
294
- }
295
- const median = periods.map((_, i) => {
296
- const near = periods.slice(Math.max(0, i - 2), i + 3).sort((a, b) => a - b);
297
- return near[near.length >> 1];
298
- });
299
- return (frame2) => interpolate(centres, median, frame2);
300
- }
301
- function interpolate(xs, ys, x) {
302
- if (x <= xs[0])
303
- return ys[0];
304
- for (let i = 1; i < xs.length; i++)
305
- if (x <= xs[i]) {
306
- const t = (x - xs[i - 1]) / (xs[i] - xs[i - 1]);
307
- return ys[i - 1] * (1 - t) + ys[i] * t;
308
- }
309
- return ys[ys.length - 1];
310
- }
311
- function beatFrames(curve, fps, period) {
312
- const local = smoothed(curve, period);
313
- const { score, back } = cumulativeScore(local, localPeriods(curve, fps, period));
314
- const beats = [];
315
- for (let i = lastBeat(score); i >= 0; i = back[i])
316
- beats.push(i);
317
- return { frames: trimmed(beats.reverse(), local), local };
318
- }
319
- function onsetLift(frames, local) {
320
- const overall = local.reduce((a, b) => a + b, 0) / (local.length || 1);
321
- const onBeats = frames.reduce((a, f) => a + local[f], 0) / (frames.length || 1);
322
- return overall > 0 ? onBeats / overall - 1 : 0;
323
- }
324
- function barPhase(beats, curves) {
325
- const strength = (f) => Math.max(...[-1, 0, 1, 2].map((d) => (curves.low[f + d] ?? 0) + 0.3 * (curves.broad[f + d] ?? 0)));
326
- const levels = beats.map(strength);
327
- let best = { meter: 4, phase: 0, contrast: -Infinity };
328
- for (const meter of [4, 3])
329
- for (let phase = 0; phase < meter; phase++) {
330
- const on = levels.filter((_, k) => (k - phase) % meter === 0);
331
- const off = levels.filter((_, k) => (k - phase) % meter !== 0);
332
- const mean = (v) => v.reduce((a, b) => a + b, 0) / (v.length || 1);
333
- const contrast = (mean(on) - mean(off)) * (meter === 3 ? 0.85 : 1);
334
- if (contrast > best.contrast)
335
- best = { meter, phase, contrast };
336
- }
337
- return best;
338
- }
339
- function medianInterval(times) {
340
- const gaps = times.slice(1).map((t, i) => t - times[i]).sort((a, b) => a - b);
341
- return gaps.length ? gaps[gaps.length >> 1] : null;
342
- }
343
- var noRhythm = (confidence, reason = "no steady beat") => ({
344
- beats: { times: [], confidence, reason },
345
- downbeats: { times: [], meter: null, confidence: "low" }
346
- });
347
- var SHORTEST_SECONDS = 6;
348
- var STEADY_BEAT_CONFIDENCE = 0.05;
349
- var beatConfidence = (clarity, lift) => round(Math.max(0, Math.min(1, clarity)) * Math.max(0, Math.min(1, lift / 3)));
350
- function rhythmOf(frames, sampleRate) {
351
- const curves = onsetCurves(frames, sampleRate);
352
- const { period, clarity } = beatPeriod(curves.broad, curves.fps);
353
- if (curves.broad.length < SHORTEST_SECONDS * curves.fps)
354
- return noRhythm(0, "too short for a steady beat");
355
- if (!curves.broad.some((v) => v > 0) || curves.broad.length < 2 * period)
356
- return noRhythm(0);
357
- const { frames: beatList, local } = beatFrames(curves.broad, curves.fps, period);
358
- const confidence = beatConfidence(clarity, onsetLift(beatList, local));
359
- if (beatList.length < 4 || confidence < STEADY_BEAT_CONFIDENCE)
360
- return noRhythm(confidence);
361
- const times = beatList.map((f) => round(f / curves.fps));
362
- const bar = beatList.length >= 6 ? barPhase(beatList, curves) : null;
363
- return {
364
- beats: { times, confidence },
365
- downbeats: {
366
- times: bar ? times.filter((_, k) => (k - bar.phase) % bar.meter === 0) : [],
367
- meter: bar?.meter ?? null,
368
- confidence: "low"
369
- }
370
- };
371
- }
372
- var TEMPO_STEP_SECONDS = 4;
373
- function tempoOf(times) {
374
- const interval = medianInterval(times);
375
- if (!interval)
376
- return { value: null, curve: [] };
377
- const bpm = (gap) => Math.round(600 / gap) / 10;
378
- const curve = [];
379
- const last = times[times.length - 1];
380
- for (let t = times[0]; t <= last; t += TEMPO_STEP_SECONDS) {
381
- const gap = medianInterval(times.filter((b) => Math.abs(b - t) <= TEMPO_STEP_SECONDS));
382
- if (gap)
383
- curve.push({ t: round(t), bpm: bpm(gap) });
384
- }
385
- return { value: bpm(interval), curve };
386
- }
387
- var measured = /* @__PURE__ */ new WeakMap();
388
- function bandFramesOf(signal) {
389
- let entry = measured.get(signal);
390
- if (!entry)
391
- measured.set(signal, entry = { frames: bandFrames(signal) });
392
- return entry.frames;
393
- }
394
- function rhythm(signal) {
395
- const frames = bandFramesOf(signal);
396
- const entry = measured.get(signal);
397
- return entry.rhythm ??= rhythmOf(frames, signal.sampleRate);
398
- }
399
-
400
- // ../core/dist/audioSnap.js
401
- function nearestIndex(sorted, t) {
402
- let low = 0;
403
- let high = sorted.length - 1;
404
- while (low < high) {
405
- const mid = low + high >> 1;
406
- if (sorted[mid] < t)
407
- low = mid + 1;
408
- else
409
- high = mid;
410
- }
411
- return low > 0 && t - sorted[low - 1] <= sorted[low] - t ? low - 1 : low;
412
- }
413
- function snapper(measured3, toleranceSeconds, reference) {
414
- const shifts = [];
415
- const unsnapped = [];
416
- return {
417
- snap(t) {
418
- const nearest = measured3.length ? measured3[nearestIndex(measured3, t)] : null;
419
- if (nearest === null || Math.abs(nearest - t) > toleranceSeconds) {
420
- unsnapped.push(t);
421
- return t;
422
- }
423
- shifts.push(Math.abs(nearest - t));
424
- return nearest;
425
- },
426
- report() {
427
- const sorted = [...shifts].sort((a, b) => a - b);
428
- const median = sorted.length ? sorted[sorted.length >> 1] : null;
429
- return {
430
- reference,
431
- toleranceMs: Math.round(toleranceSeconds * 1e3),
432
- snapped: shifts.length,
433
- unsnapped: [...unsnapped],
434
- medianShiftMs: median === null ? null : Math.round(median * 1e3)
435
- };
436
- }
437
- };
438
- }
439
-
440
- // ../core/dist/audioSections.js
441
- var BLOCK_SECONDS = 0.5;
442
- var KERNEL_BLOCKS = 16;
443
- var SAME_SECTION = 0.9;
444
- var round2 = (t) => Math.round(t * 1e3) / 1e3;
445
- function meanOf(frames) {
446
- const sum = new Array(frames[0]?.length ?? 0).fill(0);
447
- for (const frame2 of frames)
448
- for (let b = 0; b < sum.length; b++)
449
- sum[b] += frame2[b];
450
- return sum.map((v) => v / (frames.length || 1));
451
- }
452
- function standardizeBands(blocks) {
453
- for (let b = 0; b < (blocks[0]?.length ?? 0); b++) {
454
- const column = blocks.map((block) => block[b]);
455
- const mean = column.reduce((a, v) => a + v, 0) / column.length;
456
- const spread = Math.sqrt(column.reduce((a, v) => a + (v - mean) ** 2, 0) / column.length);
457
- for (const block of blocks)
458
- block[b] = (block[b] - mean) / (spread || 1);
459
- }
460
- }
461
- function blockFeatures({ fps, bands }) {
462
- const per = Math.max(1, Math.round(BLOCK_SECONDS * fps));
463
- const blocks = [];
464
- for (let start = 0; start + per <= bands.length; start += per)
465
- blocks.push(meanOf(bands.slice(start, start + per)));
466
- standardizeBands(blocks);
467
- return blocks.map(unit);
468
- }
469
- function unit(vector) {
470
- const length = Math.hypot(...vector) || 1;
471
- return vector.map((v) => v / length);
472
- }
473
- var dot = (a, b) => a.reduce((sum, v, i) => sum + v * b[i], 0);
474
- var CHECKERBOARD = (() => {
475
- const offsets = Array.from({ length: 2 * KERNEL_BLOCKS }, (_, k) => k - KERNEL_BLOCKS);
476
- const taper = (k) => Math.exp(-0.5 * (((k < 0 ? -k - 1 : k) + 0.5) / (KERNEL_BLOCKS / 2)) ** 2);
477
- return offsets.map((a) => offsets.map((b) => (a < 0 === b < 0 ? 1 : -1) * taper(a) * taper(b)));
478
- })();
479
- function noveltyAt(blocks, i) {
480
- let sum = 0;
481
- CHECKERBOARD.forEach((row, a) => {
482
- const x = blocks[i + a - KERNEL_BLOCKS];
483
- if (!x)
484
- return;
485
- row.forEach((weight, b) => {
486
- const y = blocks[i + b - KERNEL_BLOCKS];
487
- if (y)
488
- sum += weight * dot(x, y);
489
- });
490
- });
491
- return sum;
492
- }
493
- var novelty = (blocks) => blocks.map((_, i) => noveltyAt(blocks, i));
494
- function boundaryBlocks(curve, minBlocks) {
495
- const mean = curve.reduce((a, b) => a + b, 0) / (curve.length || 1);
496
- const spread = Math.sqrt(curve.reduce((a, v) => a + (v - mean) ** 2, 0) / (curve.length || 1));
497
- const peaks = [];
498
- for (let i = minBlocks; i <= curve.length - minBlocks; i++) {
499
- const neighbourhood = curve.slice(Math.max(0, i - minBlocks), i + minBlocks + 1);
500
- if (curve[i] < Math.max(...neighbourhood) || curve[i] <= mean + spread)
501
- continue;
502
- if (!peaks.length || i - peaks[peaks.length - 1] >= minBlocks)
503
- peaks.push(i);
504
- }
505
- return peaks;
506
- }
507
- function labels(blocks, edges) {
508
- const means = [];
509
- const letters = [];
510
- for (let s2 = 0; s2 + 1 < edges.length; s2++) {
511
- const part = blocks.slice(edges[s2], edges[s2 + 1]);
512
- const mean = unit(part[0].map((_, b) => part.reduce((a, block) => a + block[b], 0)));
513
- const match = means.findIndex((other) => dot(other, mean) >= SAME_SECTION);
514
- letters.push(match >= 0 ? letters[match] : String.fromCharCode(65 + new Set(letters).size % 26));
515
- means.push(mean);
516
- }
517
- return letters;
518
- }
519
- var beatTolerance = (beats) => (medianInterval(beats) ?? 0) / 2;
520
- function sections(signal, options = {}) {
521
- const duration = round2((signal.channels[0]?.length ?? 0) / signal.sampleRate);
522
- const blocks = blockFeatures(bandFramesOf(signal));
523
- const minBlocks = Math.max(1, Math.round((options.minSeconds ?? 4) / BLOCK_SECONDS));
524
- if (blocks.length < 2 * minBlocks)
525
- return { spans: [{ start: 0, end: duration, label: "A" }] };
526
- const edges = [0, ...boundaryBlocks(novelty(blocks), minBlocks), blocks.length];
527
- const letters = labels(blocks, edges);
528
- const beats = rhythm(signal).beats.times;
529
- const onBeat = snapper(beats, beatTolerance(beats), "signal beats");
530
- const times = edges.map((e, i) => i === 0 ? 0 : i === edges.length - 1 ? duration : round2(onBeat.snap(e * BLOCK_SECONDS)));
531
- return {
532
- spans: letters.map((label, s2) => ({ start: times[s2], end: times[s2 + 1], label })),
533
- snap: onBeat.report()
534
- };
535
- }
536
-
537
- // ../core/dist/audioAnalysisLadder.js
538
- function defaultLadder(capability, providers) {
539
- return providers.filter((p) => p.capabilities.includes(capability)).map((p) => p.id);
540
- }
541
- function exclusion(provider, quality) {
542
- if (quality === "fast" && !provider.signal)
543
- return "quality fast allows signal processing only";
544
- if (quality === "local" && !provider.local)
545
- return "quality local keeps audio on this machine";
546
- return null;
547
- }
548
- var message = (error) => error instanceof Error ? error.message : String(error);
549
- async function availabilityOf(provider) {
550
- try {
551
- return provider.available ? await provider.available() : { ok: true };
552
- } catch (error) {
553
- return { ok: false, reason: message(error) };
554
- }
555
- }
556
- async function blocked(id, candidate, capability, quality) {
557
- if (!candidate?.capabilities.includes(capability))
558
- return { provider: id, outcome: "unavailable", reason: `no provider ${id} for ${capability}` };
559
- const excluded = exclusion(candidate, quality);
560
- if (excluded)
561
- return { provider: id, outcome: "excluded", reason: excluded };
562
- const availability = await availabilityOf(candidate);
563
- if (!availability.ok)
564
- return { provider: id, outcome: "unavailable", reason: availability.reason };
565
- return null;
566
- }
567
- function passedOver(capability, steps, answered) {
568
- const skipped = steps.filter((s2) => s2.outcome === "unavailable" || s2.outcome === "failed");
569
- if (!skipped.length)
570
- return null;
571
- const why = skipped.map((s2) => `${s2.provider} ${s2.outcome}${s2.reason ? ` (${s2.reason})` : ""}`).join("; ");
572
- return answered ? `${capability}: used ${answered}; ${why}` : `${capability}: ${why}`;
573
- }
574
- async function runLadder(capability, input, providers, { quality = "best", ladder, provider, options = {} } = {}) {
575
- const byId = new Map(providers.map((p) => [p.id, p]));
576
- const order = provider ? [provider] : ladder ?? defaultLadder(capability, providers);
577
- const steps = [];
578
- for (const id of order) {
579
- const candidate = byId.get(id);
580
- const skip = await blocked(id, candidate, capability, quality);
581
- if (skip) {
582
- steps.push(skip);
583
- continue;
584
- }
585
- try {
586
- const value = await candidate.analyze(capability, input, options);
587
- const warning = passedOver(capability, steps, id);
588
- steps.push({ provider: id, outcome: "answered" });
589
- return { value, provenance: id, steps, warnings: warning ? [warning] : [] };
590
- } catch (error) {
591
- steps.push({ provider: id, outcome: "failed", reason: message(error) });
592
- }
593
- }
594
- const none = order.length ? "" : " (none offers it)";
595
- const why = passedOver(capability, steps, null)?.slice(capability.length + 2);
596
- return {
597
- value: null,
598
- provenance: null,
599
- steps,
600
- warnings: [`${capability}: no provider answered${none}${why ? `; ${why}` : ""}`]
601
- };
602
- }
603
-
604
- // ../core/dist/audioAnalysis.js
605
- var DB_FLOOR = -120;
606
- var signalDuration = (signal) => (signal.channels[0]?.length ?? 0) / signal.sampleRate;
607
- function energyMix(signal) {
608
- const [first, ...rest] = signal.channels;
609
- if (!first)
610
- return new Float32Array(0);
611
- if (!rest.length)
612
- return first;
613
- const mixed = new Float32Array(first.length);
614
- for (const channel of signal.channels)
615
- for (let i = 0; i < mixed.length; i++)
616
- mixed[i] += channel[i] * channel[i];
617
- const scale = 1 / signal.channels.length;
618
- for (let i = 0; i < mixed.length; i++)
619
- mixed[i] = Math.sqrt(mixed[i] * scale);
620
- return mixed;
621
- }
622
- var toDb = (amplitude) => amplitude > 0 ? Math.max(DB_FLOOR, 20 * Math.log10(amplitude)) : DB_FLOOR;
623
- var hopSamples = (sampleRate, hopMs) => Math.max(1, Math.round(sampleRate * hopMs / 1e3));
624
- var round3 = (t) => Math.round(t * 1e4) / 1e4;
625
- function rmsFrames(samples, sampleRate, hopMs, windowMs) {
626
- const hop = hopSamples(sampleRate, hopMs);
627
- const half = Math.max(1, Math.round(sampleRate * windowMs / 2e3));
628
- const frames = [];
629
- for (let center = 0; center < samples.length; center += hop) {
630
- const from = Math.max(0, center - half);
631
- const to = Math.min(samples.length, center + half);
632
- let sum = 0;
633
- for (let i = from; i < to; i++)
634
- sum += samples[i] * samples[i];
635
- frames.push(to > from ? Math.sqrt(sum / (to - from)) : 0);
636
- }
637
- return frames;
638
- }
639
- var fftSizeFor = (sampleRate) => 2 ** Math.round(Math.log2(sampleRate * 0.023));
640
- function bandBins(sampleRate, size, band) {
641
- const binHz = sampleRate / size;
642
- const low = Math.max(1, Math.floor((band?.[0] ?? 0) / binHz));
643
- return [low, Math.max(low, Math.min(size / 2, Math.ceil((band?.[1] ?? sampleRate / 2) / binHz)))];
644
- }
645
- function loadFrame(samples, center, window, re, im) {
646
- const first = center - window.length / 2;
647
- for (let i = 0; i < window.length; i++) {
648
- const at = first + i;
649
- re[i] = at >= 0 && at < samples.length ? samples[at] * window[i] : 0;
650
- im[i] = 0;
651
- }
652
- }
653
- function spectra(samples, sampleRate, hopMs, band) {
654
- const size = fftSizeFor(sampleRate);
655
- const hop = hopSamples(sampleRate, hopMs);
656
- const window = Float64Array.from({ length: size }, (_, i) => 0.5 - 0.5 * Math.cos(2 * Math.PI * i / size));
657
- const [low, high] = bandBins(sampleRate, size, band);
658
- const frames = [];
659
- const re = new Float64Array(size);
660
- const im = new Float64Array(size);
661
- for (let center = 0; center < samples.length; center += hop) {
662
- loadFrame(samples, center, window, re, im);
663
- fft(re, im);
664
- const magnitudes = new Float64Array(high - low);
665
- for (let k = low; k < high; k++)
666
- magnitudes[k - low] = Math.hypot(re[k], im[k]);
667
- frames.push(magnitudes);
668
- }
669
- return frames;
670
- }
671
- function averageMagnitudes(frames) {
672
- const mixed = new Float64Array(frames[0]?.length ?? 0);
673
- for (const frame2 of frames)
674
- for (let bin = 0; bin < mixed.length; bin++)
675
- mixed[bin] += frame2[bin];
676
- const scale = frames.length || 1;
677
- for (let bin = 0; bin < mixed.length; bin++)
678
- mixed[bin] /= scale;
679
- return mixed;
680
- }
681
- function meanSpectra(signal, hopMs, band) {
682
- const stacked = signal.channels.map((channel) => spectra(channel, signal.sampleRate, hopMs, band));
683
- const count = stacked[0]?.length ?? 0;
684
- const mixed = [];
685
- for (let frame2 = 0; frame2 < count; frame2++)
686
- mixed.push(averageMagnitudes(stacked.map((channel) => channel[frame2])));
687
- return mixed;
688
- }
689
- function envelope(signal, options = {}) {
690
- const { hopMs = 5, scale = "db", band } = options;
691
- const windowMs = options.windowMs ?? hopMs * 2;
692
- const size = fftSizeFor(signal.sampleRate);
693
- const amplitudes = band ? meanSpectra(signal, hopMs, band).map((frame2) => {
694
- let sum = 0;
695
- for (const magnitude of frame2)
696
- sum += magnitude * magnitude;
697
- return Math.sqrt(2 * sum / (size * size * 0.375));
698
- }) : rmsFrames(energyMix(signal), signal.sampleRate, hopMs, windowMs);
699
- return { hopMs, scale, values: amplitudes.map((a) => scale === "db" ? toDb(a) : a) };
700
- }
701
- function noveltyCurve(signal, method, hopMs, band) {
702
- const samples = energyMix(signal);
703
- if (method === "energy") {
704
- const rms = rmsFrames(samples, signal.sampleRate, hopMs, hopMs * 2);
705
- return rms.map((value, i) => Math.max(0, value - (rms[i - 1] ?? 0)));
706
- }
707
- const frames = meanSpectra(signal, hopMs, band).map((frame2) => frame2.map((m) => Math.log1p(100 * m)));
708
- const lastWhole = Math.floor((samples.length - fftSizeFor(signal.sampleRate) / 2) / hopSamples(signal.sampleRate, hopMs));
709
- return frames.map((frame2, i) => {
710
- const previous = frames[i - 1];
711
- if (!previous || i > lastWhole)
712
- return 0;
713
- let flux = 0;
714
- for (let k = 0; k < frame2.length; k++)
715
- flux += Math.max(0, frame2[k] - previous[k]);
716
- return flux;
717
- });
718
- }
719
- function isOnsetPeak(curve, i, gap, context, top) {
720
- const value = curve[i];
721
- let sum = 0;
722
- let count = 0;
723
- for (let j = Math.max(0, i - context); j <= Math.min(curve.length - 1, i + context); j++) {
724
- if (Math.abs(j - i) <= gap && curve[j] > value)
725
- return false;
726
- sum += curve[j];
727
- count++;
728
- }
729
- return value >= sum / count + 0.05 * top;
730
- }
731
- function onsets(signal, options = {}) {
732
- const { method = "spectral-flux", band, threshold = 0.1, minGapMs = 30, hopMs = 5 } = options;
733
- const curve = noveltyCurve(signal, method, hopMs, band);
734
- const top = Math.max(0, ...curve);
735
- if (top <= 0)
736
- return { method, events: [] };
737
- const gap = Math.max(1, Math.round(minGapMs / hopMs));
738
- const context = Math.max(gap, Math.round(100 / hopMs));
739
- const hopSeconds = hopSamples(signal.sampleRate, hopMs) / signal.sampleRate;
740
- const events = [];
741
- for (let i = 0; i < curve.length; i++) {
742
- const strength = curve[i] / top;
743
- if (strength < threshold || !isOnsetPeak(curve, i, gap, context, top))
744
- continue;
745
- const onset = { t: round3(i * hopSeconds), strength: round3(strength) };
746
- const last = events[events.length - 1];
747
- if (!last || onset.t - last.t >= minGapMs / 1e3)
748
- events.push(onset);
749
- else if (onset.strength > last.strength)
750
- events[events.length - 1] = onset;
751
- }
752
- return { method, events };
753
- }
754
- function moments(signal, options = {}) {
755
- const { onsetRatio = 0.2, tailDb = -40, hopMs = 1 } = options;
756
- const rms = rmsFrames(energyMix(signal), signal.sampleRate, hopMs, Math.max(4, hopMs * 4));
757
- const hopSeconds = hopSamples(signal.sampleRate, hopMs) / signal.sampleRate;
758
- let peakIndex = 0;
759
- for (let i = 1; i < rms.length; i++)
760
- if (rms[i] > rms[peakIndex])
761
- peakIndex = i;
762
- const peak = rms[peakIndex] ?? 0;
763
- if (peak <= 0)
764
- return null;
765
- let onsetIndex = rms.findIndex((value) => value >= peak * onsetRatio);
766
- if (onsetIndex < 0)
767
- onsetIndex = peakIndex;
768
- let steepestIndex = onsetIndex;
769
- let steepestRise = -Infinity;
770
- for (let i = 1; i <= peakIndex; i++) {
771
- const rise = rms[i] - rms[i - 1];
772
- if (rise > steepestRise)
773
- [steepestRise, steepestIndex] = [rise, i];
774
- }
775
- const floor = peak * 10 ** (tailDb / 20);
776
- let tailIndex = peakIndex;
777
- for (let i = rms.length - 1; i > peakIndex; i--)
778
- if (rms[i] >= floor) {
779
- tailIndex = i;
780
- break;
781
- }
782
- return {
783
- onset: round3(onsetIndex * hopSeconds),
784
- steepest: round3(steepestIndex * hopSeconds),
785
- peak: round3(peakIndex * hopSeconds),
786
- tailEnd: round3(tailIndex * hopSeconds)
787
- };
788
- }
789
- function kWeighting(sampleRate) {
790
- let k = Math.tan(Math.PI * 1681.974450955533 / sampleRate);
791
- const q = 0.7071752369554196;
792
- const vh = 10 ** (3.999843853973347 / 20);
793
- const vb = vh ** 0.4996667741545416;
794
- let a0 = 1 + k / q + k * k;
795
- const shelf = [
796
- (vh + vb * k / q + k * k) / a0,
797
- 2 * (k * k - vh) / a0,
798
- (vh - vb * k / q + k * k) / a0,
799
- 2 * (k * k - 1) / a0,
800
- (1 - k / q + k * k) / a0
801
- ];
802
- k = Math.tan(Math.PI * 38.13547087602444 / sampleRate);
803
- const q2 = 0.5003270373238773;
804
- a0 = 1 + k / q2 + k * k;
805
- const highPass = [1, -2, 1, 2 * (k * k - 1) / a0, (1 - k / q2 + k * k) / a0];
806
- return [shelf, highPass];
807
- }
808
- function filtered(samples, [b0, b1, b2, a1, a2]) {
809
- const out = new Float64Array(samples.length);
810
- let x1 = 0, x2 = 0, y1 = 0, y2 = 0;
811
- for (let i = 0; i < samples.length; i++) {
812
- const x = samples[i];
813
- const y = b0 * x + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2;
814
- x2 = x1;
815
- x1 = x;
816
- y2 = y1;
817
- y1 = y;
818
- out[i] = y;
819
- }
820
- return out;
821
- }
822
- var blockLoudness = (meanSquare) => meanSquare > 0 ? -0.691 + 10 * Math.log10(meanSquare) : -Infinity;
823
- function squareSums(channel) {
824
- const sums = new Float64Array(channel.length + 1);
825
- for (let i = 0; i < channel.length; i++)
826
- sums[i + 1] = sums[i] + channel[i] * channel[i];
827
- return sums;
828
- }
829
- function blockPowers(sums, sampleRate, blockSeconds, stepSeconds) {
830
- const length = (sums[0]?.length ?? 1) - 1;
831
- if (!length)
832
- return [];
833
- const block = Math.min(length, Math.round(blockSeconds * sampleRate));
834
- const step = Math.round(stepSeconds * sampleRate);
835
- const powers = [];
836
- for (let start = 0; start + block <= length; start += step)
837
- powers.push(sums.reduce((power, sum) => power + (sum[start + block] - sum[start]) / block, 0));
838
- return powers;
839
- }
840
- var TRUE_PEAK_OVERSAMPLE = 4;
841
- var TRUE_PEAK_TAPS = 12;
842
- var TRUE_PEAK_KERNELS = Array.from({ length: TRUE_PEAK_OVERSAMPLE - 1 }, (_, phase) => {
843
- const offset = (phase + 1) / TRUE_PEAK_OVERSAMPLE;
844
- return Float64Array.from({ length: 2 * TRUE_PEAK_TAPS }, (_2, tap) => {
845
- const x = tap - TRUE_PEAK_TAPS + 1 - offset;
846
- const sinc = x === 0 ? 1 : Math.sin(Math.PI * x) / (Math.PI * x);
847
- return sinc * (0.5 + 0.5 * Math.cos(Math.PI * x / TRUE_PEAK_TAPS));
848
- });
849
- });
850
- function truePeak(channel) {
851
- let samplePeak = 0;
852
- for (let i = 0; i < channel.length; i++)
853
- samplePeak = Math.max(samplePeak, Math.abs(channel[i]));
854
- const floor = samplePeak / 2;
855
- const taps = 2 * TRUE_PEAK_TAPS;
856
- let peak = samplePeak;
857
- for (let n = 0; n + taps <= channel.length; n++) {
858
- if (Math.abs(channel[n + TRUE_PEAK_TAPS - 1]) < floor && Math.abs(channel[n + TRUE_PEAK_TAPS]) < floor)
859
- continue;
860
- for (const kernel of TRUE_PEAK_KERNELS) {
861
- let y = 0;
862
- for (let tap = 0; tap < taps; tap++)
863
- y += channel[n + tap] * kernel[tap];
864
- if (y > peak)
865
- peak = y;
866
- else if (-y > peak)
867
- peak = -y;
868
- }
869
- }
870
- return peak;
871
- }
872
- function loudness(signal) {
873
- const [shelf, highPass] = kWeighting(signal.sampleRate);
874
- const sums = signal.channels.map((channel) => squareSums(filtered(filtered(channel, shelf), highPass)));
875
- const momentary = blockPowers(sums, signal.sampleRate, 0.4, 0.1);
876
- const audible = momentary.filter((power) => blockLoudness(power) > -70);
877
- let lufsIntegrated = null;
878
- if (audible.length) {
879
- const relative = blockLoudness(audible.reduce((a, b) => a + b, 0) / audible.length) - 10;
880
- const gated = audible.filter((power) => blockLoudness(power) > relative);
881
- if (gated.length)
882
- lufsIntegrated = Math.round(blockLoudness(gated.reduce((a, b) => a + b, 0) / gated.length) * 10) / 10;
883
- }
884
- const shortTerm = blockPowers(sums, signal.sampleRate, 3, 0.1).map(blockLoudness).filter(Number.isFinite);
885
- let samplePeak = 0;
886
- let count = 0;
887
- let firstAt = null;
888
- for (const channel of signal.channels)
889
- for (let i = 0; i < channel.length; i++) {
890
- const magnitude = Math.abs(channel[i]);
891
- samplePeak = Math.max(samplePeak, magnitude);
892
- if (magnitude >= 0.999) {
893
- count++;
894
- const t = i / signal.sampleRate;
895
- if (firstAt === null || t < firstAt)
896
- firstAt = round3(t);
897
- }
898
- }
899
- return {
900
- lufsIntegrated,
901
- lufsShortTermMax: shortTerm.length ? Math.round(Math.max(...shortTerm) * 10) / 10 : null,
902
- samplePeakDb: Math.round(toDb(samplePeak) * 10) / 10,
903
- truePeakDb: Math.round(toDb(Math.max(0, ...signal.channels.map(truePeak))) * 10) / 10,
904
- clipping: { count, firstAt }
905
- };
906
- }
907
- function silence(signal, options = {}) {
908
- const { thresholdDb = -50, minMs = 200, hopMs = 5 } = options;
909
- const db = rmsFrames(energyMix(signal), signal.sampleRate, hopMs, hopMs * 2).map(toDb);
910
- const hopSeconds = hopSamples(signal.sampleRate, hopMs) / signal.sampleRate;
911
- const spans = [];
912
- let from = null;
913
- for (let i = 0; i <= db.length; i++) {
914
- const quiet = i < db.length && db[i] < thresholdDb;
915
- if (quiet && from === null)
916
- from = i;
917
- if (!quiet && from !== null) {
918
- if ((i - from) * hopSeconds * 1e3 >= minMs)
919
- spans.push({
920
- start: round3(from * hopSeconds),
921
- end: round3(Math.min(i * hopSeconds, signalDuration(signal)))
922
- });
923
- from = null;
924
- }
925
- }
926
- return spans;
927
- }
928
- function periodicity(curve, hopMs) {
929
- const mean = curve.reduce((a, b) => a + b, 0) / (curve.length || 1);
930
- const centred = curve.map((v) => v - mean);
931
- const energy = centred.reduce((a, v) => a + v * v, 0);
932
- if (!energy)
933
- return 0;
934
- let best = 0;
935
- for (let lag = Math.round(300 / hopMs); lag <= Math.round(1500 / hopMs) && lag < centred.length; lag++) {
936
- let sum = 0;
937
- for (let i = lag; i < centred.length; i++)
938
- sum += centred[i] * centred[i - lag];
939
- best = Math.max(best, sum / energy);
940
- }
941
- return best;
942
- }
943
- function syllableModulation(signal) {
944
- const hopMs = 10;
945
- const rms = rmsFrames(energyMix(signal), signal.sampleRate, hopMs, 25);
946
- const size = 2 ** Math.floor(Math.log2(Math.min(rms.length, 1024)));
947
- if (size < 64)
948
- return 0;
949
- const re = Float64Array.from(rms.slice(0, size));
950
- const mean = re.reduce((a, b) => a + b, 0) / size;
951
- for (let i = 0; i < size; i++)
952
- re[i] = re[i] - mean;
953
- const im = new Float64Array(size);
954
- fft(re, im);
955
- const rate = 1e3 / hopMs;
956
- let inBand = 0;
957
- let total = 0;
958
- for (let k = 1; k < size / 2; k++) {
959
- const power = re[k] * re[k] + im[k] * im[k];
960
- const hz = k * rate / size;
961
- total += power;
962
- if (hz >= 3 && hz <= 8)
963
- inBand += power;
964
- }
965
- return total ? inBand / total : 0;
966
- }
967
- function sustainShare(signal) {
968
- const levels = rmsFrames(energyMix(signal), signal.sampleRate, 50, 100).map(toDb);
969
- if (!levels.length)
970
- return 0;
971
- const loud = [...levels].sort((a, b) => a - b)[Math.floor((levels.length - 1) * 0.95)];
972
- return levels.filter((level) => level > loud - 20).length / levels.length;
973
- }
974
- function kindFeatures(signal) {
975
- return {
976
- duration: round3(signalDuration(signal)),
977
- regularity: round3(periodicity(noveltyCurve(signal, "spectral-flux", 10), 10)),
978
- syllables: round3(syllableModulation(signal)),
979
- sustain: round3(sustainShare(signal))
980
- };
981
- }
982
- function classify(signal) {
983
- const features = kindFeatures(signal);
984
- const { duration, regularity, syllables, sustain } = features;
985
- const speechLike = syllables >= 0.35 && regularity < 0.35;
986
- if (duration >= 8) {
987
- if (sustain < 0.4)
988
- return { value: "sfx", confidence: 0.6, features };
989
- if (speechLike)
990
- return { value: "voice", confidence: round3(Math.min(0.9, 0.4 + syllables)), features };
991
- return { value: "music", confidence: round3(Math.min(0.9, 0.5 + regularity)), features };
992
- }
993
- if (duration >= 1.5 && syllables >= 0.17 && regularity < 0.3 && sustain >= 0.7)
994
- return { value: "voice", confidence: speechLike ? 0.8 : 0.6, features };
995
- return { value: "sfx", confidence: duration <= 2 ? 0.85 : 0.65, features };
996
- }
997
- var SIGNAL_MEASURES = {
998
- beats: (input) => rhythm(input).beats,
999
- downbeats: (input) => rhythm(input).downbeats,
1000
- sections: (input, options) => sections(input, options),
1001
- envelope: (input, options) => envelope(input, options),
1002
- onsets: (input, options) => onsets(input, options),
1003
- moments: (input, options) => moments(input, options),
1004
- loudness: (input) => loudness(input),
1005
- silence: (input, options) => silence(input, options),
1006
- kind: (input) => classify(input)
1007
- };
1008
- var signalProvider = {
1009
- id: "signal",
1010
- local: true,
1011
- signal: true,
1012
- capabilities: [
1013
- "envelope",
1014
- "onsets",
1015
- "moments",
1016
- "loudness",
1017
- "silence",
1018
- "kind",
1019
- "beats",
1020
- "downbeats",
1021
- "sections"
1022
- ],
1023
- async analyze(capability, input, options) {
1024
- const measure = SIGNAL_MEASURES[capability];
1025
- if (!measure)
1026
- throw new Error(`signal processing does not measure ${capability}`);
1027
- return measure(input, options);
1028
- }
1029
- };
1030
-
1031
- // src/audio/analysis/providers/shared.ts
1032
- import { execFile } from "child_process";
1033
- import { mkdtemp, rm, writeFile } from "fs/promises";
1034
- import { tmpdir } from "os";
1035
- import { join } from "path";
1036
- import { promisify } from "util";
1037
- var runFile = promisify(execFile);
1038
- var unavailable = (reason) => ({ ok: false, reason });
1039
- var AVAILABLE = { ok: true };
1040
- function encodeWav(signal) {
1041
- const channels = signal.channels.length;
1042
- const frames = signal.channels[0]?.length ?? 0;
1043
- const data = frames * channels * 2;
1044
- const wav = Buffer.alloc(44 + data);
1045
- wav.write("RIFF", 0);
1046
- wav.writeUInt32LE(36 + data, 4);
1047
- wav.write("WAVEfmt ", 8);
1048
- wav.writeUInt32LE(16, 16);
1049
- wav.writeUInt16LE(1, 20);
1050
- wav.writeUInt16LE(channels, 22);
1051
- wav.writeUInt32LE(signal.sampleRate, 24);
1052
- wav.writeUInt32LE(signal.sampleRate * channels * 2, 28);
1053
- wav.writeUInt16LE(channels * 2, 32);
1054
- wav.writeUInt16LE(16, 34);
1055
- wav.write("data", 36);
1056
- wav.writeUInt32LE(data, 40);
1057
- let offset = 44;
1058
- for (let i = 0; i < frames; i++)
1059
- for (const channel of signal.channels) {
1060
- const sample = Math.max(-1, Math.min(1, channel[i]));
1061
- wav.writeInt16LE(Math.round(sample * 32767), offset);
1062
- offset += 2;
1063
- }
1064
- return wav;
1065
- }
1066
- async function withWavFile(signal, use) {
1067
- const dir = await mkdtemp(join(tmpdir(), "hf-audio-"));
1068
- try {
1069
- const path = join(dir, "audio.wav");
1070
- await writeFile(path, encodeWav(signal));
1071
- return await use(path);
1072
- } finally {
1073
- await rm(dir, { recursive: true, force: true });
1074
- }
1075
- }
1076
- function perSignal(compute) {
1077
- const answers = /* @__PURE__ */ new WeakMap();
1078
- return (signal) => {
1079
- let answer = answers.get(signal);
1080
- if (!answer) answers.set(signal, answer = compute(signal));
1081
- return answer;
1082
- };
1083
- }
1084
- function downbeatsWithMeter(beats, downbeats) {
1085
- const bar = medianInterval(downbeats);
1086
- const beat = medianInterval(beats);
1087
- const meter = bar && beat ? Math.round(bar / beat) : null;
1088
- return { times: downbeats, meter: meter && meter > 0 ? meter : null };
1089
- }
1090
- var round4 = (t) => Math.round(t * 1e3) / 1e3;
1091
- var rounded = (times) => times.map(round4);
1092
-
1093
- // src/audio/analysis/providers/api.ts
1094
- var URL_ENV = "HYPERFRAMES_AUDIO_API_URL";
1095
- var KEY_ENV = "HYPERFRAMES_AUDIO_API_KEY";
1096
- var TIMEOUT_ENV = "HYPERFRAMES_AUDIO_API_TIMEOUT_MS";
1097
- var DEFAULT_TIMEOUT_MS = 12e4;
1098
- var askedTogether = (capability) => capability === "beats" || capability === "downbeats" ? ["beats", "downbeats"] : [capability];
1099
- function config() {
1100
- const url = process.env[URL_ENV]?.trim();
1101
- const key = process.env[KEY_ENV]?.trim();
1102
- const timeout = Number(process.env[TIMEOUT_ENV]);
1103
- return { url, key, timeoutMs: timeout > 0 ? timeout : DEFAULT_TIMEOUT_MS };
1104
- }
1105
- var isNumberList = (value) => Array.isArray(value) && value.every((t) => typeof t === "number" && Number.isFinite(t));
1106
- function checked(capability, body) {
1107
- const value = body[capability];
1108
- if (!value || typeof value !== "object")
1109
- throw new Error(`the service did not answer ${capability}`);
1110
- const valid = {
1111
- beats: () => isNumberList(value.times),
1112
- downbeats: () => isNumberList(value.times),
1113
- sections: () => Array.isArray(value.spans) && value.spans.every((s2) => isNumberList([s2.start, s2.end])),
1114
- describe: () => Array.isArray(value.tags) && value.tags.every(
1115
- (t) => typeof t.label === "string" && typeof t.score === "number"
1116
- )
1117
- }[capability];
1118
- if (!valid())
1119
- throw new Error(`the service answered ${capability} in a shape this CLI cannot read`);
1120
- }
1121
- async function failure(response) {
1122
- const text = await response.text().catch(() => "");
1123
- let detail = text.slice(0, 200);
1124
- try {
1125
- detail = JSON.parse(text).error?.message ?? detail;
1126
- } catch {
1127
- }
1128
- return new Error(`the service answered ${response.status}${detail ? `: ${detail}` : ""}`);
1129
- }
1130
- async function ask(signal, capabilities2) {
1131
- const { url, key, timeoutMs } = config();
1132
- if (!url || !key) throw new Error(`${URL_ENV} and ${KEY_ENV} must both be set`);
1133
- const request = {
1134
- version: 1,
1135
- capabilities: capabilities2,
1136
- audio: {
1137
- sampleRate: signal.sampleRate,
1138
- channels: signal.channels.length,
1139
- duration: (signal.channels[0]?.length ?? 0) / signal.sampleRate
1140
- }
1141
- };
1142
- const form = new FormData();
1143
- form.append("request", new Blob([JSON.stringify(request)], { type: "application/json" }));
1144
- form.append(
1145
- "audio",
1146
- new Blob([new Uint8Array(encodeWav(signal))], { type: "audio/wav" }),
1147
- "audio.wav"
1148
- );
1149
- const response = await fetch(url, {
1150
- method: "POST",
1151
- headers: { authorization: `Bearer ${key}` },
1152
- body: form,
1153
- signal: AbortSignal.timeout(timeoutMs)
1154
- });
1155
- if (!response.ok) throw await failure(response);
1156
- return await response.json();
1157
- }
1158
- var requests = /* @__PURE__ */ new Map();
1159
- function answerFor(signal, capability) {
1160
- const together = askedTogether(capability);
1161
- const key = together.join(",");
1162
- let request = requests.get(key);
1163
- if (!request) requests.set(key, request = perSignal((s2) => ask(s2, together)));
1164
- return request(signal);
1165
- }
1166
- function beatSnapper(signal, beatSeconds) {
1167
- const measured3 = rhythm(signal).beats.times;
1168
- const period = beatSeconds ?? medianInterval(measured3) ?? 0;
1169
- return snapper(measured3, period / 4, "signal beats");
1170
- }
1171
- function snappedBeats(signal, body) {
1172
- const times = body.beats.times;
1173
- const onBeat = beatSnapper(signal, medianInterval(times));
1174
- return { times: rounded(times.map(onBeat.snap)), snap: onBeat.report() };
1175
- }
1176
- function snappedDownbeats(signal, body) {
1177
- const { times, meter = null } = body.downbeats;
1178
- const beat = body.beats ? medianInterval(body.beats.times) : null;
1179
- const bar = medianInterval(times);
1180
- const onBeat = beatSnapper(signal, beat ?? (bar && meter ? bar / meter : null));
1181
- return { times: rounded(times.map(onBeat.snap)), meter, snap: onBeat.report() };
1182
- }
1183
- function snappedSections(signal, body) {
1184
- const onBeat = beatSnapper(signal, null);
1185
- const end = (signal.channels[0]?.length ?? 0) / signal.sampleRate;
1186
- const move = (t) => t <= 0 || t >= end ? t : onBeat.snap(t);
1187
- const spans = body.sections.spans.map((s2) => ({
1188
- ...s2,
1189
- start: rounded([move(s2.start)])[0],
1190
- end: rounded([move(s2.end)])[0]
1191
- }));
1192
- return { spans, snap: onBeat.report() };
1193
- }
1194
- function measured2(capability, signal, body) {
1195
- const value = capability === "beats" ? snappedBeats(signal, body) : capability === "downbeats" ? snappedDownbeats(signal, body) : capability === "sections" ? snappedSections(signal, body) : body.describe;
1196
- return body.source ? { ...value, source: body.source } : value;
1197
- }
1198
- var API_CAPABILITIES = ["beats", "downbeats", "sections", "describe"];
1199
- var apiProvider = {
1200
- id: "api",
1201
- local: false,
1202
- capabilities: API_CAPABILITIES,
1203
- available() {
1204
- const { url, key } = config();
1205
- if (!url) return unavailable(`${URL_ENV} is not set`);
1206
- if (!key) return unavailable(`${KEY_ENV} is not set`);
1207
- return AVAILABLE;
1208
- },
1209
- async analyze(capability, signal) {
1210
- const asked = capability;
1211
- const body = await answerFor(signal, asked);
1212
- checked(asked, body);
1213
- return measured2(asked, signal, body);
1214
- }
1215
- };
1216
-
1217
- // src/audio/analysis/providers/apple.ts
1218
- import { createHash } from "crypto";
1219
- import { existsSync } from "fs";
1220
- import { mkdir, rename, writeFile as writeFile2 } from "fs/promises";
1221
- import { homedir } from "os";
1222
- import { join as join2 } from "path";
1223
- var SOUND_ANALYSIS = "/System/Library/Frameworks/SoundAnalysis.framework";
1224
- var MUSIC_UNDERSTANDING_MACOS = 27;
1225
- var SOUND_TAGGER = `
1226
- import Foundation
1227
- import SoundAnalysis
1228
-
1229
- final class Collector: NSObject, SNResultsObserving {
1230
- var sums: [String: Double] = [:]
1231
- var windows = 0
1232
- var failure: Error?
1233
- func request(_ request: SNRequest, didProduce result: SNResult) {
1234
- guard let result = result as? SNClassificationResult else { return }
1235
- windows += 1
1236
- for c in result.classifications { sums[c.identifier, default: 0] += c.confidence }
1237
- }
1238
- func request(_ request: SNRequest, didFailWithError error: Error) { failure = error }
1239
- }
1240
-
1241
- do {
1242
- let analyzer = try SNAudioFileAnalyzer(url: URL(fileURLWithPath: CommandLine.arguments[1]))
1243
- let collector = Collector()
1244
- try analyzer.add(try SNClassifySoundRequest(classifierIdentifier: .version1), withObserver: collector)
1245
- analyzer.analyze()
1246
- if let failure = collector.failure { throw failure }
1247
- let count = Double(max(1, collector.windows))
1248
- var tags: [[String: Any]] = []
1249
- for (label, sum) in collector.sums.sorted(by: { $0.value > $1.value }).prefix(10) {
1250
- let score: Double = sum / count
1251
- if score >= 0.01 { tags.append(["label": label, "score": (score * 1000).rounded() / 1000]) }
1252
- }
1253
- let json = try JSONSerialization.data(withJSONObject: ["tags": tags, "windows": collector.windows])
1254
- FileHandle.standardOutput.write(json)
1255
- } catch {
1256
- FileHandle.standardError.write(Data("\\(error)".utf8))
1257
- exit(1)
1258
- }
1259
- `;
1260
- var CLASSIFIER_WINDOW_SECONDS = 3;
1261
- function atLeastOneWindow(signal) {
1262
- const length = Math.ceil(CLASSIFIER_WINDOW_SECONDS * signal.sampleRate);
1263
- if ((signal.channels[0]?.length ?? 0) >= length) return signal;
1264
- return {
1265
- ...signal,
1266
- channels: signal.channels.map((channel) => {
1267
- const padded = new Float32Array(length);
1268
- padded.set(channel);
1269
- return padded;
1270
- })
1271
- };
1272
- }
1273
- var helperDir = () => join2(homedir(), ".cache", "hyperframes", "audio-helpers");
1274
- function helperPath(name, source) {
1275
- const hash = createHash("sha256").update(source).digest("hex").slice(0, 12);
1276
- return join2(helperDir(), `${name}-${hash}`);
1277
- }
1278
- async function hasSwiftCompiler() {
1279
- try {
1280
- await runFile("xcrun", ["--find", "swiftc"], { timeout: 3e4 });
1281
- return true;
1282
- } catch {
1283
- return false;
1284
- }
1285
- }
1286
- var firstLine = (error) => (error.stderr || String(error)).trim().split("\n")[0];
1287
- var builds = /* @__PURE__ */ new Map();
1288
- function compiled(name, source) {
1289
- const binary = helperPath(name, source);
1290
- if (existsSync(binary)) return Promise.resolve(binary);
1291
- let build = builds.get(binary);
1292
- if (!build) {
1293
- build = (async () => {
1294
- await mkdir(helperDir(), { recursive: true });
1295
- const file = `${binary}.swift`;
1296
- await writeFile2(file, source);
1297
- const args = [
1298
- "--sdk",
1299
- "macosx",
1300
- "swiftc",
1301
- "-O",
1302
- "-swift-version",
1303
- "5",
1304
- file,
1305
- "-o",
1306
- `${binary}.tmp`
1307
- ];
1308
- await runFile("xcrun", args, { timeout: 5 * 6e4 }).catch((error) => {
1309
- throw new Error(`could not build the ${name} helper: ${firstLine(error)}`);
1310
- });
1311
- await rename(`${binary}.tmp`, binary);
1312
- return binary;
1313
- })();
1314
- builds.set(binary, build);
1315
- }
1316
- return build;
1317
- }
1318
- async function macosMajor() {
1319
- if (process.platform !== "darwin") return null;
1320
- const { stdout } = await runFile("sw_vers", ["-productVersion"], { timeout: 1e4 });
1321
- return Number.parseInt(stdout, 10) || null;
1322
- }
1323
- var appleSoundProvider = {
1324
- id: "apple-sound",
1325
- local: true,
1326
- capabilities: ["describe"],
1327
- async available() {
1328
- if (process.platform !== "darwin") return unavailable("macOS only");
1329
- if (!existsSync(SOUND_ANALYSIS))
1330
- return unavailable("SoundAnalysis.framework is not on this Mac");
1331
- if (existsSync(helperPath("apple-sound", SOUND_TAGGER))) return AVAILABLE;
1332
- if (!await hasSwiftCompiler()) return unavailable("no Swift compiler to build the helper");
1333
- return AVAILABLE;
1334
- },
1335
- async analyze(_capability, signal) {
1336
- const helper = await compiled("apple-sound", SOUND_TAGGER);
1337
- const { stdout } = await withWavFile(
1338
- atLeastOneWindow(signal),
1339
- (wav) => runFile(helper, [wav], { timeout: 5 * 6e4, maxBuffer: 8 * 1024 * 1024 })
1340
- );
1341
- const { tags } = JSON.parse(stdout);
1342
- return {
1343
- tags: tags.map((t) => ({ label: t.label, score: Math.round(t.score * 1e3) / 1e3 }))
1344
- };
1345
- }
1346
- };
1347
- var appleMuProvider = {
1348
- id: "apple-mu",
1349
- local: true,
1350
- capabilities: ["beats", "downbeats", "sections"],
1351
- async available() {
1352
- const major = await macosMajor();
1353
- if (major === null) return unavailable("macOS only");
1354
- if (major < MUSIC_UNDERSTANDING_MACOS)
1355
- return unavailable(
1356
- `Music Understanding needs macOS ${MUSIC_UNDERSTANDING_MACOS}; this Mac runs ${major}`
1357
- );
1358
- return unavailable("no Music Understanding helper yet: it needs the macOS 27 SDK to build");
1359
- },
1360
- analyze() {
1361
- return Promise.reject(new Error("Music Understanding is not available"));
1362
- }
1363
- };
1364
-
1365
- // src/audio/analysis/providers/beatThis.ts
1366
- import { existsSync as existsSync2 } from "fs";
1367
- import { homedir as homedir2 } from "os";
1368
- import { join as join3 } from "path";
1369
- var PYTHON_ENV = "HYPERFRAMES_BEAT_THIS_PYTHON";
1370
- var CHECKPOINT_ENV = "HYPERFRAMES_BEAT_THIS_CHECKPOINT";
1371
- function checkpointPath() {
1372
- const configured = process.env[CHECKPOINT_ENV]?.trim();
1373
- if (configured) return configured;
1374
- const torchHome = process.env.TORCH_HOME ?? join3(homedir2(), ".cache", "torch");
1375
- return join3(torchHome, "hub", "checkpoints", "beat_this-small0.ckpt");
1376
- }
1377
- var FIND_PACKAGE = "import importlib.util,sys; sys.exit(importlib.util.find_spec('beat_this') is None)";
1378
- var TRACK = `
1379
- import json, sys
1380
- from beat_this.inference import File2Beats
1381
- beats, downbeats = File2Beats(checkpoint_path=sys.argv[2], device="cpu", dbn=False)(sys.argv[1])
1382
- print(json.dumps({"beats": [float(t) for t in beats], "downbeats": [float(t) for t in downbeats]}))
1383
- `;
1384
- var installed = /* @__PURE__ */ new Map();
1385
- function hasPackage(python) {
1386
- let found = installed.get(python);
1387
- if (!found) {
1388
- found = runFile(python, ["-c", FIND_PACKAGE], { timeout: 3e4, windowsHide: true }).then(
1389
- () => true,
1390
- () => false
1391
- );
1392
- installed.set(python, found);
1393
- }
1394
- return found;
1395
- }
1396
- var track = perSignal(
1397
- (signal) => withWavFile(signal, async (wav) => {
1398
- const python = process.env[PYTHON_ENV].trim();
1399
- const { stdout } = await runFile(python, ["-c", TRACK, wav, checkpointPath()], {
1400
- maxBuffer: 64 * 1024 * 1024,
1401
- timeout: 10 * 6e4,
1402
- windowsHide: true
1403
- });
1404
- const answer = JSON.parse(stdout.trim().split("\n").pop());
1405
- return { beats: rounded(answer.beats), downbeats: rounded(answer.downbeats) };
1406
- })
1407
- );
1408
- var beatThisProvider = {
1409
- id: "beat-this",
1410
- local: true,
1411
- capabilities: ["beats", "downbeats"],
1412
- async available() {
1413
- const python = process.env[PYTHON_ENV]?.trim();
1414
- if (!python) return unavailable(`${PYTHON_ENV} is not set`);
1415
- if (!existsSync2(python)) return unavailable(`${PYTHON_ENV} names no file`);
1416
- if (!existsSync2(checkpointPath()))
1417
- return unavailable(`no Beat This! weights at ${checkpointPath()}`);
1418
- if (!await hasPackage(python)) return unavailable(`beat_this is not installed for ${python}`);
1419
- return AVAILABLE;
1420
- },
1421
- async analyze(capability, signal) {
1422
- const { beats, downbeats } = await track(signal);
1423
- return capability === "beats" ? { times: beats } : downbeatsWithMeter(beats, downbeats);
1424
- }
1425
- };
1426
-
1427
- // src/audio/analysis/fileTime.ts
1428
- var round5 = (t) => Math.round(t * 1e3) / 1e3;
1429
- function withSnap(value, at) {
1430
- return value.snap ? { ...value, snap: { ...value.snap, unsnapped: value.snap.unsnapped.map(at) } } : value;
1431
- }
1432
- var SHIFTS = {
1433
- moments: (m, at) => Object.fromEntries(Object.entries(m).map(([k, t]) => [k, at(t)])),
1434
- onsets: (o, at) => ({ ...o, events: o.events.map((e) => ({ ...e, t: at(e.t) })) }),
1435
- silence: (spans, at) => spans.map((s2) => ({ start: at(s2.start), end: at(s2.end) })),
1436
- loudness: (l, at) => ({
1437
- ...l,
1438
- clipping: {
1439
- ...l.clipping,
1440
- firstAt: l.clipping.firstAt === null ? null : at(l.clipping.firstAt)
1441
- }
1442
- }),
1443
- envelope: (e, _at, offset) => ({ startsAt: round5(offset), ...e }),
1444
- beats: (b, at) => withSnap({ ...b, times: b.times.map(at) }, at),
1445
- downbeats: (b, at) => withSnap({ ...b, times: b.times.map(at) }, at),
1446
- bpm: (tempo, at) => ({
1447
- ...tempo,
1448
- curve: tempo.curve.map((p) => ({ ...p, t: at(p.t) }))
1449
- }),
1450
- sections: (s2, at) => withSnap(
1451
- {
1452
- ...s2,
1453
- spans: s2.spans.map((span) => ({ ...span, start: at(span.start), end: at(span.end) }))
1454
- },
1455
- at
1456
- )
1457
- };
1458
- function shiftedTimes(capability, value, offset) {
1459
- if (value === null || typeof value !== "object") return value;
1460
- const shift = SHIFTS[capability];
1461
- return shift ? shift(value, (t) => round5(t + offset), offset) : value;
1462
- }
1463
-
1464
- // src/audio/analysis/analyze.ts
1465
- var DEFAULT_CAPABILITIES = [
1466
- "kind",
1467
- "moments",
1468
- "onsets",
1469
- "loudness",
1470
- "silence",
1471
- "envelope"
1472
- ];
1473
- function audioAnalysisProviders() {
1474
- return [apiProvider, appleMuProvider, beatThisProvider, appleSoundProvider, signalProvider];
1475
- }
1476
- function envelopeHopMs(seconds, asked) {
1477
- if (asked !== void 0) return asked;
1478
- return Math.max(5, Math.ceil(seconds * 1e3 / 2e3));
1479
- }
1480
- var round6 = (t) => Math.round(t * 1e3) / 1e3;
1481
- function within(value, from, to) {
1482
- if (!value || typeof value !== "object" || !("events" in value)) return value;
1483
- const onsets2 = value;
1484
- return { ...onsets2, events: onsets2.events.filter((e) => e.t >= from && e.t < to) };
1485
- }
1486
- function frame(signal, context) {
1487
- const before = context?.before ?? 0;
1488
- const skip = Math.round(before * signal.sampleRate);
1489
- const length = signal.channels[0]?.length ?? 0;
1490
- const end = Math.max(skip, length - Math.round((context?.after ?? 0) * signal.sampleRate));
1491
- const cut = skip > 0 || end < length;
1492
- const windowed = cut ? { ...signal, channels: signal.channels.map((c) => c.subarray(skip, end)) } : signal;
1493
- return { windowed, before, end: end / signal.sampleRate, cut };
1494
- }
1495
- function optionsFor(capability, request, seconds) {
1496
- const options = { ...request.options?.[capability] };
1497
- if (capability === "envelope")
1498
- options.hopMs = envelopeHopMs(seconds, options.hopMs);
1499
- return options;
1500
- }
1501
- async function analyzeSignal(signal, request, providers = audioAnalysisProviders()) {
1502
- const { beats, bpm } = request.provider ?? {};
1503
- if (beats && bpm && beats !== bpm) throw new Error("beats and bpm must use the same provider");
1504
- const beatProvider = beats ?? bpm;
1505
- const framing = frame(signal, request.context);
1506
- const analysis = {
1507
- analyzed: round6(signalDuration(framing.windowed)),
1508
- values: {},
1509
- provenance: {},
1510
- ladder: {},
1511
- warnings: []
1512
- };
1513
- const ladders = /* @__PURE__ */ new Map();
1514
- const ladderFor = (capability) => {
1515
- let result = ladders.get(capability);
1516
- if (!result) {
1517
- const withContext = capability === "onsets" && framing.cut;
1518
- result = runLadder(capability, withContext ? signal : framing.windowed, providers, {
1519
- quality: request.quality,
1520
- provider: capability === "beats" ? beatProvider : request.provider?.[capability],
1521
- options: optionsFor(capability, request, analysis.analyzed)
1522
- });
1523
- ladders.set(capability, result);
1524
- }
1525
- return result;
1526
- };
1527
- for (const capability of request.capabilities) {
1528
- const withContext = capability === "onsets" && framing.cut;
1529
- const result = capability === "bpm" ? tempoFromBeats(await ladderFor("beats")) : await ladderFor(capability);
1530
- const value = withContext ? within(result.value, framing.before, framing.end) : result.value;
1531
- const offset = (request.fileOffset ?? 0) - (withContext ? framing.before : 0);
1532
- analysis.values[capability] = shiftedTimes(capability, value, offset);
1533
- analysis.provenance[capability] = result.provenance;
1534
- analysis.ladder[capability] = result.steps;
1535
- const said = new Set(analysis.warnings);
1536
- analysis.warnings.push(...result.warnings.filter((w) => !said.has(w)));
1537
- }
1538
- return analysis;
1539
- }
1540
- function tempoFromBeats(beats) {
1541
- const times = beats.value?.times;
1542
- return { ...beats, value: times ? tempoOf(times) : null };
1543
- }
1544
-
1545
- // src/audio/analysis/targets.ts
1546
- import { dirname, join as join4, resolve } from "path";
1547
- var round7 = (t) => Math.round(t * 1e3) / 1e3;
1548
- function placementOf(row) {
1549
- return {
1550
- start: row.absStart,
1551
- end: row.absEnd,
1552
- dataStart: row.start,
1553
- mediaStart: row.mediaStart ?? 0,
1554
- playbackRate: row.playbackRate ?? 1,
1555
- volume: row.volume ?? 1
1556
- };
1557
- }
1558
- function filmTime(placement, fileTime2) {
1559
- return round7(placement.start + (fileTime2 - placement.mediaStart) / placement.playbackRate);
1560
- }
1561
- function fileTime(placement, filmTime2) {
1562
- return round7(placement.mediaStart + (filmTime2 - placement.start) * placement.playbackRate);
1563
- }
1564
- var soundingRows = (timeline) => timeline.tracks.filter((track2) => track2.kind === "audio" || track2.kind === "video").flatMap((track2) => track2.rows).filter((row) => row.src && row.volume !== 0 && /^(audio|video)$/.test(row.kind)).sort((a, b) => a.absStart - b.absStart);
1565
- function matches(row, query) {
1566
- switch (query.by) {
1567
- case "all":
1568
- case "element":
1569
- return true;
1570
- case "at":
1571
- return row.absStart <= query.t && query.t < row.absEnd;
1572
- case "range":
1573
- return row.absStart < query.end && query.start < row.absEnd;
1574
- }
1575
- }
1576
- function targetOf(projectDir, row, query) {
1577
- const placement = placementOf(row);
1578
- const from = query.by === "range" ? Math.max(placement.start, query.start) : placement.start;
1579
- const to = query.by === "range" ? Math.min(placement.end, query.end) : placement.end;
1580
- const declaredIn = dirname(join4(projectDir, row.file));
1581
- return {
1582
- path: resolve(declaredIn, row.src),
1583
- file: row.src,
1584
- ref: row.ref || null,
1585
- kind: row.kind,
1586
- placement,
1587
- window: {
1588
- from: fileTime(placement, from),
1589
- seconds: round7((to - from) * placement.playbackRate)
1590
- }
1591
- };
1592
- }
1593
- function filmTargets(projectDir, timeline, query) {
1594
- if (query.by === "element") {
1595
- const id = query.ref.startsWith("#") ? query.ref.slice(1) : null;
1596
- const found = timeline.tracks.flatMap((track2) => track2.rows).filter((row2) => row2.ref === query.ref || id !== null && row2.id === id);
1597
- if (found.length !== 1)
1598
- throw new Error(
1599
- `${query.ref} ${found.length ? `matches ${found.length} clips` : "was not found"}; use a ref from hyperframes timeline`
1600
- );
1601
- const row = found[0];
1602
- if (!row.src || !/^(audio|video)$/.test(row.kind))
1603
- throw new Error(`${query.ref} is not an audio or video clip`);
1604
- return [targetOf(projectDir, row, query)];
1605
- }
1606
- return soundingRows(timeline).filter((row) => matches(row, query)).map((row) => targetOf(projectDir, row, query));
1607
- }
1608
-
1609
- // src/audio/analysis/format.ts
1610
- var BARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
1611
- var SPARK_WIDTH = 48;
1612
- var ONSETS_SHOWN = 12;
1613
- var s = (t) => t.toFixed(3);
1614
- function sparkline(envelope2, width = SPARK_WIDTH) {
1615
- const { values } = envelope2;
1616
- if (values.length === 0) return "";
1617
- const per = Math.max(1, Math.ceil(values.length / width));
1618
- const slices = [];
1619
- for (let i = 0; i < values.length; i += per) slices.push(Math.max(...values.slice(i, i + per)));
1620
- const top = Math.max(...slices);
1621
- const floor = Math.max(Math.min(...slices), top - 60);
1622
- if (top - floor < 1e-9) return BARS[0].repeat(slices.length);
1623
- return slices.map(
1624
- (v) => BARS[Math.round((Math.max(v, floor) - floor) / (top - floor) * (BARS.length - 1))]
1625
- ).join("");
1626
- }
1627
- function momentsLine(moments2, at) {
1628
- return ` onset ${at(moments2.onset)} steepest ${at(moments2.steepest)} peak ${at(moments2.peak)} tail end ${at(moments2.tailEnd)}`;
1629
- }
1630
- function hitsLine(onsets2, at) {
1631
- if (!onsets2.events.length) return null;
1632
- const shown = onsets2.events.slice(0, ONSETS_SHOWN).map((e) => `${at(e.t)}(${e.strength.toFixed(2)})`);
1633
- const more = onsets2.events.length - shown.length;
1634
- return ` hits ${shown.join(" ")}${more > 0 ? ` +${more}` : ""}`;
1635
- }
1636
- function loudnessLine(loudness2, at) {
1637
- const lufs = loudness2.lufsIntegrated === null ? "below gate" : `${loudness2.lufsIntegrated} LUFS`;
1638
- const { count, firstAt } = loudness2.clipping;
1639
- const clip = count && firstAt !== null ? `, clips ${count}\xD7 from ${at(firstAt)}` : "";
1640
- return ` loudness ${lufs}, true peak ${loudness2.truePeakDb} dBTP${clip}`;
1641
- }
1642
- function silenceLine(silence2, at) {
1643
- return silence2.length ? ` silent ${silence2.map((g) => `${at(g.start)}\u2013${at(g.end)}`).join(" ")}` : null;
1644
- }
1645
- function snapNote(snap) {
1646
- if (!snap) return "";
1647
- const total = snap.snapped + snap.unsnapped.length;
1648
- const median = snap.medianShiftMs === null ? "" : `, median ${snap.medianShiftMs} ms`;
1649
- return `; ${snap.snapped}/${total} snapped to ${snap.reference}${median}`;
1650
- }
1651
- var listed = (times, at) => {
1652
- const shown = times.slice(0, ONSETS_SHOWN).map(at).join(" ");
1653
- return times.length > ONSETS_SHOWN ? `${shown} +${times.length - ONSETS_SHOWN}` : shown;
1654
- };
1655
- function beatsLine(beats, at) {
1656
- if (!beats.times.length) return ` beats none${beats.reason ? ` (${beats.reason})` : ""}`;
1657
- return ` beats ${beats.times.length}: ${listed(beats.times, at)}${snapNote(beats.snap)}`;
1658
- }
1659
- function downbeatsLine(downbeats, at) {
1660
- if (!downbeats.times.length) return null;
1661
- const meter = downbeats.meter ? ` in ${downbeats.meter}` : "";
1662
- const low = downbeats.confidence === "low" ? ", a guess" : "";
1663
- return ` downbeats${meter}${low}: ${listed(downbeats.times, at)}${snapNote(downbeats.snap)}`;
1664
- }
1665
- function tempoLine(tempo) {
1666
- if (tempo.value === null) return null;
1667
- const bpms = tempo.curve.map((p) => p.bpm);
1668
- const spread = bpms.length && Math.max(...bpms) - Math.min(...bpms) >= 2 ? ` (${Math.min(...bpms)}\u2013${Math.max(...bpms)})` : "";
1669
- return ` tempo ${tempo.value} BPM${spread}`;
1670
- }
1671
- function sectionsLine(sections2, at) {
1672
- const spans = sections2.spans.map((s2) => `${s2.label} ${at(s2.start)}\u2013${at(s2.end)}`).join(" ");
1673
- return ` sections ${spans}${snapNote(sections2.snap)}`;
1674
- }
1675
- function describeLine(description) {
1676
- const tags = description.tags.map((t) => `${t.label} ${t.score.toFixed(2)}`).join(", ");
1677
- return ` sounds like ${[description.text, tags].filter(Boolean).join(" \u2014 ")}`;
1678
- }
1679
- var LINES = [
1680
- ["moments", momentsLine],
1681
- ["onsets", hitsLine],
1682
- ["loudness", loudnessLine],
1683
- ["silence", silenceLine],
1684
- ["envelope", (envelope2) => ` ${sparkline(envelope2)}`],
1685
- ["describe", describeLine],
1686
- ["bpm", tempoLine],
1687
- ["beats", beatsLine],
1688
- ["downbeats", downbeatsLine],
1689
- ["sections", sectionsLine]
1690
- ];
1691
- function headline(item, where) {
1692
- const kind = item.kind;
1693
- const span = item.placement ? `film ${s(item.placement.start)}\u2013${s(item.placement.end)}, file from ${s(item.placement.mediaStart)}` : `${s(item.duration ?? 0)} s`;
1694
- return `${where} ${span}${kind ? ` ${kind.value} (${kind.confidence})` : ""}`;
1695
- }
1696
- function formatItem(item) {
1697
- const where = item.ref ? `${item.file} ${item.ref}` : item.file;
1698
- if (item.error) return [`${where}: ${item.error}`];
1699
- const at = (t) => s(item.placement ? filmTime(item.placement, t) : t);
1700
- const lines = [headline(item, where)];
1701
- for (const [capability, line] of LINES) {
1702
- const value = item[capability];
1703
- const text = value ? line(value, at) : null;
1704
- if (text) lines.push(text);
1705
- }
1706
- for (const warning of item.warnings) lines.push(` ! ${warning}`);
1707
- return lines;
1708
- }
1709
- function formatReport(report) {
1710
- if (report.items.length === 0) return "No audio matched.";
1711
- return report.items.map((item) => formatItem(item).join("\n")).join("\n\n");
1712
- }
1713
-
1714
- // src/audio/analysis/run.ts
1715
- import { existsSync as existsSync3 } from "fs";
1716
-
1717
- // src/audio/analysis/decode.ts
1718
- import { execFile as execFile2 } from "child_process";
1719
- import { promisify as promisify2 } from "util";
1720
- var run = promisify2(execFile2);
1721
- function requiredBinary(name) {
1722
- const path = findFfBinary(name, { configuredMustExist: true });
1723
- if (!path) throw new Error(`${name} is required to analyze audio; run: npx hyperframes doctor`);
1724
- return path;
1725
- }
1726
- async function probeAudio(file) {
1727
- const { stdout } = await run(
1728
- requiredBinary("ffprobe"),
1729
- [
1730
- "-v",
1731
- "error",
1732
- "-select_streams",
1733
- "a:0",
1734
- "-show_streams",
1735
- "-show_format",
1736
- "-of",
1737
- "json",
1738
- "--",
1739
- file
1740
- ],
1741
- { maxBuffer: 8 * 1024 * 1024, windowsHide: true }
1742
- );
1743
- const parsed = JSON.parse(stdout);
1744
- const stream = parsed.streams?.[0];
1745
- if (!stream) return null;
1746
- return {
1747
- sampleRate: Number(stream.sample_rate),
1748
- channels: stream.channels ?? 1,
1749
- duration: Number(stream.duration ?? parsed.format?.duration ?? 0)
1750
- };
1751
- }
1752
- async function decodeAudio(file, window = {}, probed) {
1753
- const info = probed ?? await probeAudio(file);
1754
- if (!info) return null;
1755
- const seek = window.from ? ["-ss", String(window.from)] : [];
1756
- const length = window.seconds === void 0 ? [] : ["-t", String(window.seconds)];
1757
- const { stdout } = await run(
1758
- requiredBinary("ffmpeg"),
1759
- [
1760
- "-v",
1761
- "error",
1762
- ...seek,
1763
- "-i",
1764
- file,
1765
- ...length,
1766
- "-map",
1767
- "0:a:0",
1768
- "-f",
1769
- "f32le",
1770
- "-acodec",
1771
- "pcm_f32le",
1772
- "-"
1773
- ],
1774
- { encoding: "buffer", maxBuffer: 2 * 1024 * 1024 * 1024, windowsHide: true }
1775
- );
1776
- const bytes = stdout.byteLength - stdout.byteLength % 4;
1777
- const interleaved = new Float32Array(
1778
- stdout.buffer.slice(stdout.byteOffset, stdout.byteOffset + bytes)
1779
- );
1780
- const frames = Math.floor(interleaved.length / info.channels);
1781
- const channels = Array.from({ length: info.channels }, (_, c) => {
1782
- const samples = new Float32Array(frames);
1783
- for (let i = 0; i < frames; i++) samples[i] = interleaved[i * info.channels + c];
1784
- return samples;
1785
- });
1786
- return { signal: { sampleRate: info.sampleRate, channels }, stream: info };
1787
- }
1788
-
1789
- // src/audio/analysis/run.ts
1790
- var round8 = (t) => Math.round(t * 1e3) / 1e3;
1791
- var EDGE_CONTEXT_SECONDS = 0.1;
1792
- async function previewTarget(path, file, at, mediaStart = 0) {
1793
- if (at === null) return { path, file, ref: null, kind: "file", placement: null };
1794
- const stream = existsSync3(path) ? await probeAudio(path) : null;
1795
- if (stream && mediaStart >= stream.duration)
1796
- throw new Error(
1797
- `skipping ${mediaStart} s leaves nothing of ${file}, which is ${round8(stream.duration)} s long`
1798
- );
1799
- const length = stream ? stream.duration - mediaStart : 0;
1800
- return {
1801
- path,
1802
- file,
1803
- ref: null,
1804
- kind: "file",
1805
- placement: {
1806
- start: at,
1807
- end: round8(at + length),
1808
- dataStart: at,
1809
- mediaStart,
1810
- playbackRate: 1,
1811
- volume: 1
1812
- },
1813
- window: { from: mediaStart, seconds: length }
1814
- };
1815
- }
1816
- var emptyItem = (target) => ({
1817
- file: target.file,
1818
- ref: target.ref,
1819
- element: target.kind === "file" ? null : target.kind,
1820
- placement: target.placement,
1821
- timebase: "file",
1822
- window: null,
1823
- duration: null,
1824
- sampleRate: null,
1825
- channels: null,
1826
- provenance: {},
1827
- warnings: []
1828
- });
1829
- function unreadable(target) {
1830
- if (/^[a-z][a-z\d+.-]*:\/\//i.test(target.file)) return "remote sources are not analyzed";
1831
- if (!existsSync3(target.path)) return "file not found";
1832
- return null;
1833
- }
1834
- function decodeSpan(target, stream) {
1835
- const from = target.window?.from ?? 0;
1836
- const seconds = target.window?.seconds ?? stream.duration - from;
1837
- const before = Math.min(from, EDGE_CONTEXT_SECONDS);
1838
- const after = Math.max(0, Math.min(EDGE_CONTEXT_SECONDS, stream.duration - from - seconds));
1839
- return {
1840
- from,
1841
- read: { from: from - before, seconds: seconds + before + after },
1842
- context: { before, after }
1843
- };
1844
- }
1845
- var loudest = (signal) => Math.max(0, ...envelope(signal, { hopMs: 10, scale: "linear" }).values);
1846
- function retained(signal, before, after) {
1847
- const skip = Math.round(before * signal.sampleRate);
1848
- const length = signal.channels[0]?.length ?? 0;
1849
- const end = Math.max(skip, length - Math.round(after * signal.sampleRate));
1850
- if (!skip && end >= length) return signal;
1851
- return { ...signal, channels: signal.channels.map((channel) => channel.subarray(skip, end)) };
1852
- }
1853
- async function skippedPeak(target, kept, stream) {
1854
- const skipped = target.kind === "file" ? target.window?.from ?? 0 : 0;
1855
- if (skipped <= 0) return [];
1856
- const head = await decodeAudio(target.path, { from: 0, seconds: skipped }, stream);
1857
- if (!head || loudest(head.signal) <= loudest(kept)) return [];
1858
- return [
1859
- `the loudest part of ${target.file} is in the first ${round8(skipped)} s, which is skipped`
1860
- ];
1861
- }
1862
- async function analyzeTarget(target, request, full) {
1863
- const item = emptyItem(target);
1864
- const problem = unreadable(target);
1865
- if (problem) return { ...item, error: problem };
1866
- const stream = await probeAudio(target.path);
1867
- if (!stream) return { ...item, error: "no audio stream" };
1868
- const { from, read, context } = decodeSpan(target, stream);
1869
- const decoded = await decodeAudio(target.path, read, stream);
1870
- if (!decoded) return { ...item, error: "no audio stream" };
1871
- const analysis = await analyzeSignal(decoded.signal, { ...request, fileOffset: from, context });
1872
- return {
1873
- ...item,
1874
- window: { from: round8(from), to: round8(from + analysis.analyzed) },
1875
- duration: round8(stream.duration),
1876
- sampleRate: stream.sampleRate,
1877
- channels: stream.channels,
1878
- ...analysis.values,
1879
- provenance: analysis.provenance,
1880
- ...full ? { ladder: analysis.ladder } : {},
1881
- warnings: [
1882
- ...analysis.warnings,
1883
- ...await skippedPeak(
1884
- target,
1885
- retained(decoded.signal, context.before, context.after),
1886
- stream
1887
- )
1888
- ]
1889
- };
1890
- }
1891
- async function analyzeTargets(targets, request, full = false) {
1892
- const items = [];
1893
- for (const target of targets) {
1894
- try {
1895
- items.push(await analyzeTarget(target, request, full));
1896
- } catch (error) {
1897
- const reason = error instanceof Error ? error.message.split("\n")[0] : String(error);
1898
- items.push({ ...emptyItem(target), error: `could not read: ${reason}` });
1899
- }
1900
- }
1901
- return { version: 1, items };
1902
- }
1903
-
1904
- // src/commands/audio.ts
1905
- var examples = [
1906
- ["Where a sound effect lands, its hits and loudness", "hyperframes audio sfx/whoosh.mp3"],
1907
- [
1908
- "Where a file not in the film yet would land if it started at 4.2 s",
1909
- "hyperframes audio sfx/hit.wav --place-at 4.2"
1910
- ],
1911
- ["Every sound playing at 12 s in the film", "hyperframes audio --at 12"],
1912
- ["One clip of the film, as JSON", "hyperframes audio --element '#riser' --json"],
1913
- [
1914
- "The beat grid, tempo and sections of a music bed",
1915
- "hyperframes audio music.mp3 --only beats,downbeats,bpm,sections"
1916
- ],
1917
- [
1918
- "Only onsets, from 60\u2013200 Hz, finer",
1919
- "hyperframes audio drums.wav --only onsets --band 60,200 --hop-ms 2"
1920
- ]
1921
- ];
1922
- var CAPABILITIES = /* @__PURE__ */ new Set([
1923
- ...DEFAULT_CAPABILITIES,
1924
- "beats",
1925
- "downbeats",
1926
- "bpm",
1927
- "sections",
1928
- "describe"
1929
- ]);
1930
- var QUALITIES = /* @__PURE__ */ new Set(["best", "local", "fast"]);
1931
- function number(name, raw) {
1932
- if (raw === void 0) return void 0;
1933
- const value = Number(raw);
1934
- if (!Number.isFinite(value)) throw new CliUsageError(`--${name} must be a number, got ${raw}`);
1935
- return value;
1936
- }
1937
- function pair(name, raw) {
1938
- if (raw === void 0) return void 0;
1939
- const parts = raw.split(",").map(Number);
1940
- if (parts.length !== 2 || parts.some((n) => !Number.isFinite(n)) || parts[0] >= parts[1])
1941
- throw new CliUsageError(`--${name} takes two increasing numbers, like 2.5,4`);
1942
- return parts;
1943
- }
1944
- function capabilities(raw) {
1945
- if (!raw) return [...DEFAULT_CAPABILITIES];
1946
- const asked = raw.split(",").map((c) => c.trim()).filter(Boolean);
1947
- if (asked.includes("words"))
1948
- throw new CliUsageError("words come from hyperframes transcribe, not hyperframes audio");
1949
- const unknown = asked.filter((c) => !CAPABILITIES.has(c));
1950
- if (unknown.length)
1951
- throw new CliUsageError(
1952
- `--only does not know ${unknown.join(", ")}; use ${[...CAPABILITIES].join(", ")}`
1953
- );
1954
- return asked;
1955
- }
1956
- function pins(raw) {
1957
- if (!raw) return void 0;
1958
- return Object.fromEntries(
1959
- raw.split(",").map((entry) => {
1960
- const [capability, id] = entry.split("=").map((s2) => s2.trim());
1961
- if (!capability || !id || !CAPABILITIES.has(capability))
1962
- throw new CliUsageError(`--provider takes capability=provider pairs, like onsets=signal`);
1963
- return [capability, id];
1964
- })
1965
- );
1966
- }
1967
- function filmQuery(args) {
1968
- const given = ["element", "at", "range"].filter((k) => args[k] !== void 0);
1969
- if (given.length > 1) throw new CliUsageError(`use one of --element, --at, --range`);
1970
- if (args.element) return { by: "element", ref: args.element };
1971
- if (args.at !== void 0) return { by: "at", t: number("at", args.at) };
1972
- const range = pair("range", args.range);
1973
- return range ? { by: "range", start: range[0], end: range[1] } : null;
1974
- }
1975
- function analyzeRequest(flags) {
1976
- const quality = flags.quality ?? "best";
1977
- if (!QUALITIES.has(quality)) throw new CliUsageError("--quality is best, local or fast");
1978
- const method = flags["onset-method"];
1979
- if (method && method !== "spectral-flux" && method !== "energy")
1980
- throw new CliUsageError("--onset-method is spectral-flux or energy");
1981
- const band = pair("band", flags.band);
1982
- return {
1983
- capabilities: capabilities(flags.only),
1984
- quality,
1985
- provider: pins(flags.provider),
1986
- options: {
1987
- onsets: { method, band, threshold: number("threshold", flags.threshold) },
1988
- envelope: { band, hopMs: number("hop-ms", flags["hop-ms"]) },
1989
- moments: {
1990
- onsetRatio: number("onset-ratio", flags["onset-ratio"]),
1991
- tailDb: number("tail-db", flags["tail-db"])
1992
- },
1993
- silence: { thresholdDb: number("silence-db", flags["silence-db"]) }
1994
- }
1995
- };
1996
- }
1997
- async function fileTargets(files, flags) {
1998
- const start = number("place-at", flags["place-at"]);
1999
- if (start !== void 0 && files.length !== 1)
2000
- throw new CliUsageError("--place-at places exactly one file");
2001
- const skip = number("skip-file-seconds", flags["skip-file-seconds"]);
2002
- if (skip !== void 0 && start === void 0)
2003
- throw new CliUsageError("--skip-file-seconds only works with --place-at");
2004
- const mediaStart = skip ?? 0;
2005
- const cwd = process.cwd();
2006
- return Promise.all(
2007
- files.map((file) => previewTarget(resolve2(cwd, file), file, start ?? null, mediaStart))
2008
- );
2009
- }
2010
- async function clipTargets(flags, query) {
2011
- const project = resolveProject(flags.dir);
2012
- ensureDOMParser();
2013
- const timeline = await describeProject(project.indexPath);
2014
- return filmTargets(project.dir, timeline, query ?? { by: "all" });
2015
- }
2016
- var audio_default = defineCommand({
2017
- meta: {
2018
- name: "audio",
2019
- description: "Measure how sound moves in time: where it lands, its hits, loudness and silence"
2020
- },
2021
- args: {
2022
- files: { type: "positional", description: "Audio or video files to analyze", required: false },
2023
- dir: { type: "string", description: "Project directory, for film queries (default: current)" },
2024
- element: {
2025
- type: "string",
2026
- description: "One audio or video clip of the film: #id, hf:id or audio/N"
2027
- },
2028
- at: { type: "string", description: "Every sound playing at this film second" },
2029
- range: { type: "string", description: "Every sound playing in start,end film seconds" },
2030
- "place-at": {
2031
- type: "string",
2032
- description: "For one file not in the film yet: the film second it would start at"
2033
- },
2034
- "skip-file-seconds": {
2035
- type: "string",
2036
- description: "With --place-at: seconds cut from the start of the file"
2037
- },
2038
- only: {
2039
- type: "string",
2040
- description: `Capabilities to measure (default ${DEFAULT_CAPABILITIES.join(",")}; also beats,downbeats,bpm,sections,describe)`
2041
- },
2042
- quality: {
2043
- type: "string",
2044
- description: "best (any provider), local (this machine), fast (signal only)"
2045
- },
2046
- provider: { type: "string", description: "Pin capabilities to providers: onsets=signal,..." },
2047
- "onset-method": { type: "string", description: "spectral-flux (default) or energy" },
2048
- threshold: { type: "string", description: "Onset strength floor, 0\u20131 (default 0.1)" },
2049
- band: { type: "string", description: "Measure onsets and envelope in lo,hi Hz only" },
2050
- "hop-ms": {
2051
- type: "string",
2052
- description: "Envelope hop in ms (default: about 2,000 values per item)"
2053
- },
2054
- "onset-ratio": {
2055
- type: "string",
2056
- description: "Moments: onset is where RMS first reaches this share of the peak (0.2)"
2057
- },
2058
- "tail-db": {
2059
- type: "string",
2060
- description: "Moments: the tail ends this many dB under the peak (-40)"
2061
- },
2062
- "silence-db": { type: "string", description: "Silence below this level in dBFS (-50)" },
2063
- full: {
2064
- type: "boolean",
2065
- description: "Include each capability's provider ladder",
2066
- default: false
2067
- },
2068
- json: { type: "boolean", description: "Output as JSON", default: false }
2069
- },
2070
- setup({ rawArgs, cmd }) {
2071
- assertKnownFlags({ args: cmd.args }, rawArgs);
2072
- },
2073
- async run({ args }) {
2074
- const flags = args;
2075
- const files = args._.map(String);
2076
- const query = filmQuery(flags);
2077
- if (files.length && query) throw new CliUsageError("give files or a film query, not both");
2078
- const targets = files.length ? await fileTargets(files, flags) : await clipTargets(flags, query);
2079
- const report = await analyzeTargets(targets, analyzeRequest(flags), Boolean(args.full));
2080
- console.log(args.json ? JSON.stringify(withMeta(report)) : formatReport(report));
2081
- }
2082
- });
2083
- export {
2084
- audio_default as default,
2085
- examples
2086
- };