hyperframes 0.8.160 → 0.8.161
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{add-OGBTJCU4.js → add-7U5TQGSQ.js} +5 -5
- package/dist/{asset-DKQFFGPP.js → asset-N3JO5LHY.js} +2 -2
- package/dist/audio-QNPSENQR.js +1131 -0
- package/dist/{auth-AQMRAFXN.js → auth-HINUM4A7.js} +1 -1
- package/dist/{autoUpdate-NFQB7V2X.js → autoUpdate-AAMND4XK.js} +2 -2
- package/dist/{backgroundChecks-7JKE2FIU.js → backgroundChecks-FXIEHPND.js} +3 -3
- package/dist/backgroundChecksWorker.js +3 -3
- package/dist/beats-UZNREIRU.js +239 -0
- package/dist/{benchmark-RRU67WEY.js → benchmark-YM3QXPH6.js} +5 -5
- package/dist/{browser-K4TP4ZM2.js → browser-SD3T2XMH.js} +3 -3
- package/dist/{capture-T2TBROMS.js → capture-GUO23PVN.js} +1 -1
- package/dist/{catalog-IMIHQ4KX.js → catalog-6PF7KZAZ.js} +5 -5
- package/dist/{catch-up-UY5MJWOG.js → catch-up-5LTQTD3X.js} +4 -4
- package/dist/{check-6NAJSJHQ.js → check-NE2YXJR2.js} +18 -18
- package/dist/{checkBrowser-JWNQFYTY.js → checkBrowser-XDTXDEYO.js} +14 -14
- package/dist/{chunk-VD7WQYTO.js → chunk-5YSHR4XC.js} +5 -5
- package/dist/{chunk-FRGYHRP6.js → chunk-7ZKEOQ5N.js} +1 -1
- package/dist/{chunk-D46G5IQH.js → chunk-AHNJMWMU.js} +7 -7
- package/dist/{chunk-D2WGLWIK.js → chunk-AY2FEJDR.js} +2 -2
- package/dist/{chunk-V2FQQILJ.js → chunk-CGOGEA62.js} +1 -1
- package/dist/chunk-FFJ5TDRD.js +1181 -0
- package/dist/{chunk-GOASXWDD.js → chunk-G5AZMSQP.js} +1 -1
- package/dist/{chunk-PINFPWRV.js → chunk-GIPNECWL.js} +12 -10
- package/dist/{chunk-4VYNMVMC.js → chunk-H4VXLZ6O.js} +4 -4
- package/dist/{chunk-KRE55WLI.js → chunk-HM4PLNTX.js} +1 -1
- package/dist/{chunk-MYGXKD7A.js → chunk-I6CVW5CA.js} +2 -2
- package/dist/{chunk-VZ6LCTOZ.js → chunk-IL6C7T5X.js} +1 -1
- package/dist/{chunk-ICZVCVD3.js → chunk-KZPG4DXX.js} +1 -1
- package/dist/{chunk-6MCASYIS.js → chunk-ON2HYNGB.js} +4 -4
- package/dist/{chunk-IGTGLGML.js → chunk-PYMYGIZ4.js} +2 -2
- package/dist/{chunk-7OFNPEKR.js → chunk-PZH7FTRZ.js} +2 -2
- package/dist/{chunk-GOTCGKX6.js → chunk-RGYEL7QD.js} +1 -1
- package/dist/{chunk-3XPVZLAR.js → chunk-SAS6XBY5.js} +1 -1
- package/dist/{chunk-KWRBEDJI.js → chunk-TXG6W4X4.js} +2 -2
- package/dist/{chunk-TTFDAWCS.js → chunk-VYTGK5YA.js} +1 -1
- package/dist/{chunk-37LNY2EC.js → chunk-WDUEIP22.js} +1 -1
- package/dist/{chunk-TU2CQJO3.js → chunk-WNXVPPMV.js} +2 -2
- package/dist/{chunk-SHVDJON6.js → chunk-XWWYGXJ5.js} +1 -1
- package/dist/{clean-RRE6PMKZ.js → clean-4Y4G3YV3.js} +6 -6
- package/dist/cli.js +49 -49
- package/dist/{cloud-OIR6K7TS.js → cloud-BWXJB2B7.js} +4 -4
- package/dist/{compare-E57W737I.js → compare-OLLJBVNL.js} +11 -11
- package/dist/{component-I2O4AX56.js → component-ULSAC45J.js} +3 -3
- package/dist/{compositions-B6NKZHIN.js → compositions-G2JTQBTJ.js} +5 -5
- package/dist/{delete-DTUNBC6K.js → delete-BONV4XUI.js} +2 -2
- package/dist/{doctor-SCNB73Z3.js → doctor-EMLC4VZ4.js} +3 -3
- package/dist/{events-6LQNRKHZ.js → events-LF2J436R.js} +3 -3
- package/dist/{events-Q37UJL36.js → events-ZFGAA5UD.js} +2 -2
- package/dist/{feedback-FOEBE2OW.js → feedback-WKCUQE6T.js} +5 -5
- package/dist/{figma-54MRTBKH.js → figma-VJKG252Z.js} +3 -3
- package/dist/fontLocalizeCli.js +1 -1
- package/dist/{get-EPGHM2SA.js → get-OINAGI2B.js} +2 -2
- package/dist/{grade-compare-LOJWX6SK.js → grade-compare-DY5VFXPK.js} +11 -11
- package/dist/{help-6FPZI3WP.js → help-HHB7JRZA.js} +1 -1
- package/dist/{history-GRXSUA5R.js → history-D44JJBBC.js} +6 -6
- package/dist/hyperframes-player.global.js +1 -1
- package/dist/{info-LV7DFCLD.js → info-2WZLISAR.js} +5 -5
- package/dist/{init-3J7BSAC3.js → init-UYZS2WXQ.js} +6 -6
- package/dist/{inspect-5RON554C.js → inspect-IEFCB4PT.js} +13 -13
- package/dist/{keyframes-KHXTHI5C.js → keyframes-FS43QHM2.js} +6 -6
- package/dist/{layout-DVWWT2TR.js → layout-AK2A7Q46.js} +13 -13
- package/dist/{lint-Q5WFB5ST.js → lint-JRJUSZO7.js} +6 -6
- package/dist/{list-RFEUTW6N.js → list-BKKGPT7E.js} +2 -2
- package/dist/{login-WBHWA4M2.js → login-QJZTUDDO.js} +4 -4
- package/dist/{media-treatment-WRHVNWLS.js → media-treatment-473OM7G7.js} +6 -6
- package/dist/{motionShot-BZLRN3SS.js → motionShot-C2YDYRNB.js} +5 -5
- package/dist/{normalize-audio-4YXE3RIF.js → normalize-audio-M3KVTFTQ.js} +4 -4
- package/dist/{open-MCGYEFML.js → open-363TDMEP.js} +4 -4
- package/dist/{play-WXOV3BD7.js → play-2UB33EGB.js} +9 -9
- package/dist/{present-RVG4J6CJ.js → present-F3ASSDJX.js} +9 -9
- package/dist/{preview-PDUQR2JQ.js → preview-EF76WIHK.js} +5 -5
- package/dist/publish-VX5OH4ST.js +1 -1
- package/dist/{render-JNJVFBFF.js → render-GL72Z2W2.js} +6 -6
- package/dist/{render-5BGU3FXU.js → render-GUBYSVQD.js} +7 -7
- package/dist/skills/hyperframes-cli/references/beats.md +2 -6
- package/dist/{skills-LKJA27ZN.js → skills-7XI4DHLN.js} +5 -5
- package/dist/{skillsUpdateCheck-RFZJKZC4.js → skillsUpdateCheck-34WOCMHV.js} +3 -3
- package/dist/{snapshot-EOYA4MLG.js → snapshot-SGTO2OQ4.js} +13 -13
- package/dist/{staticProjectServer-S3NK3V4S.js → staticProjectServer-4CQ3KDQK.js} +5 -5
- package/dist/studio/assets/{hyperframes-player-CXL23NEv.js → hyperframes-player-C8VFb4NS.js} +1 -1
- package/dist/studio/assets/{index-D7BvLHtZ.js → index-J8Bpbg6o.js} +118 -118
- package/dist/studio/assets/{index-CejKvBNw.js → index-RZnZPb-B.js} +1 -1
- package/dist/studio/index.html +1 -1
- package/dist/{studio-api-YM5HLXUS.js → studio-api-RYCDLEOR.js} +3 -3
- package/dist/{studioServer-CEXJK5TC.js → studioServer-VIIVGFZH.js} +9 -9
- package/dist/{telemetry-7AWYECPL.js → telemetry-YKVYLJSB.js} +3 -3
- package/dist/{timeline-G7H373XX.js → timeline-LDRA7QZ3.js} +8 -8
- package/dist/{tokens-GYH7NXXA.js → tokens-ZAOFRU55.js} +2 -2
- package/dist/{transcribe-CIC7WAT5.js → transcribe-CJXUHTAS.js} +3 -3
- package/dist/{updateCheck-MQ44KGUR.js → updateCheck-TIRFQBSI.js} +2 -2
- package/dist/{upgrade-OL47SID3.js → upgrade-7R5G3MNO.js} +2 -2
- package/dist/{validate-TDCH6X3C.js → validate-VGEPWGQO.js} +11 -11
- package/package.json +2 -3
- package/dist/audio-GK4OVVTH.js +0 -2086
- package/dist/beat-analyzer.global.js +0 -341
- package/dist/beats-C3ZSK2R2.js +0 -322
- package/dist/lib-TPG2ZMAX.js +0 -128
- package/dist/studio/assets/index-Bl_xrA5T.js +0 -1
- /package/dist/{capture-ZPRK3TOZ.js → capture-M5ZAD3BI.js} +0 -0
- /package/dist/{lambda-4KCSMIM6.js → lambda-RAJA6ZNA.js} +0 -0
|
@@ -0,0 +1,1181 @@
|
|
|
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
|
+
findFfBinary
|
|
9
|
+
} from "./chunk-VAS6HQX3.js";
|
|
10
|
+
|
|
11
|
+
// src/audio/analysis/decode.ts
|
|
12
|
+
import { execFile } from "child_process";
|
|
13
|
+
import { promisify } from "util";
|
|
14
|
+
var run = promisify(execFile);
|
|
15
|
+
function requiredBinary(name) {
|
|
16
|
+
const path = findFfBinary(name, { configuredMustExist: true });
|
|
17
|
+
if (!path) throw new Error(`${name} is required to analyze audio; run: npx hyperframes doctor`);
|
|
18
|
+
return path;
|
|
19
|
+
}
|
|
20
|
+
async function probeAudio(file) {
|
|
21
|
+
const { stdout } = await run(
|
|
22
|
+
requiredBinary("ffprobe"),
|
|
23
|
+
[
|
|
24
|
+
"-v",
|
|
25
|
+
"error",
|
|
26
|
+
"-select_streams",
|
|
27
|
+
"a:0",
|
|
28
|
+
"-show_streams",
|
|
29
|
+
"-show_format",
|
|
30
|
+
"-of",
|
|
31
|
+
"json",
|
|
32
|
+
"--",
|
|
33
|
+
file
|
|
34
|
+
],
|
|
35
|
+
{ maxBuffer: 8 * 1024 * 1024, windowsHide: true }
|
|
36
|
+
);
|
|
37
|
+
const parsed = JSON.parse(stdout);
|
|
38
|
+
const stream = parsed.streams?.[0];
|
|
39
|
+
if (!stream) return null;
|
|
40
|
+
return {
|
|
41
|
+
sampleRate: Number(stream.sample_rate),
|
|
42
|
+
channels: stream.channels ?? 1,
|
|
43
|
+
duration: Number(stream.duration ?? parsed.format?.duration ?? 0)
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
async function decodeAudio(file, window = {}, probed) {
|
|
47
|
+
const info = probed ?? await probeAudio(file);
|
|
48
|
+
if (!info) return null;
|
|
49
|
+
const seek = window.from ? ["-ss", String(window.from)] : [];
|
|
50
|
+
const length = window.seconds === void 0 ? [] : ["-t", String(window.seconds)];
|
|
51
|
+
const { stdout } = await run(
|
|
52
|
+
requiredBinary("ffmpeg"),
|
|
53
|
+
[
|
|
54
|
+
"-v",
|
|
55
|
+
"error",
|
|
56
|
+
...seek,
|
|
57
|
+
"-i",
|
|
58
|
+
file,
|
|
59
|
+
...length,
|
|
60
|
+
"-map",
|
|
61
|
+
"0:a:0",
|
|
62
|
+
"-f",
|
|
63
|
+
"f32le",
|
|
64
|
+
"-acodec",
|
|
65
|
+
"pcm_f32le",
|
|
66
|
+
"-"
|
|
67
|
+
],
|
|
68
|
+
{ encoding: "buffer", maxBuffer: 2 * 1024 * 1024 * 1024, windowsHide: true }
|
|
69
|
+
);
|
|
70
|
+
const bytes = stdout.byteLength - stdout.byteLength % 4;
|
|
71
|
+
const interleaved = new Float32Array(
|
|
72
|
+
stdout.buffer.slice(stdout.byteOffset, stdout.byteOffset + bytes)
|
|
73
|
+
);
|
|
74
|
+
const frames = Math.floor(interleaved.length / info.channels);
|
|
75
|
+
const channels = Array.from({ length: info.channels }, (_, c) => {
|
|
76
|
+
const samples = new Float32Array(frames);
|
|
77
|
+
for (let i = 0; i < frames; i++) samples[i] = interleaved[i * info.channels + c];
|
|
78
|
+
return samples;
|
|
79
|
+
});
|
|
80
|
+
return { signal: { sampleRate: info.sampleRate, channels }, stream: info };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ../core/dist/audioResample.js
|
|
84
|
+
var ZERO_CROSSINGS = 8;
|
|
85
|
+
var STEPS_PER_CROSSING = 256;
|
|
86
|
+
var CUTOFF = 0.95;
|
|
87
|
+
var KERNEL = Float64Array.from({ length: ZERO_CROSSINGS * STEPS_PER_CROSSING + 2 }, (_, i) => {
|
|
88
|
+
const x = i / STEPS_PER_CROSSING;
|
|
89
|
+
if (x >= ZERO_CROSSINGS)
|
|
90
|
+
return 0;
|
|
91
|
+
const sinc = x === 0 ? 1 : Math.sin(Math.PI * x) / (Math.PI * x);
|
|
92
|
+
return sinc * (0.5 + 0.5 * Math.cos(Math.PI * x / ZERO_CROSSINGS));
|
|
93
|
+
});
|
|
94
|
+
function kernelAt(x) {
|
|
95
|
+
const at = Math.abs(x) * STEPS_PER_CROSSING;
|
|
96
|
+
const i = Math.floor(at);
|
|
97
|
+
if (i + 1 >= KERNEL.length)
|
|
98
|
+
return 0;
|
|
99
|
+
const frac = at - i;
|
|
100
|
+
return KERNEL[i] * (1 - frac) + KERNEL[i + 1] * frac;
|
|
101
|
+
}
|
|
102
|
+
function resample(samples, from, to) {
|
|
103
|
+
if (from === to)
|
|
104
|
+
return samples;
|
|
105
|
+
const ratio = to / from;
|
|
106
|
+
const scale = Math.min(1, ratio) * CUTOFF;
|
|
107
|
+
const reach = ZERO_CROSSINGS / scale;
|
|
108
|
+
const out = new Float32Array(Math.floor(samples.length * ratio));
|
|
109
|
+
for (let n = 0; n < out.length; n++) {
|
|
110
|
+
const t = n / ratio;
|
|
111
|
+
const last = Math.min(samples.length - 1, Math.floor(t + reach));
|
|
112
|
+
let sum = 0;
|
|
113
|
+
for (let k = Math.max(0, Math.ceil(t - reach)); k <= last; k++)
|
|
114
|
+
sum += samples[k] * kernelAt((t - k) * scale);
|
|
115
|
+
out[n] = sum * scale;
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ../core/dist/fft.js
|
|
121
|
+
function fft(re, im) {
|
|
122
|
+
const n = re.length;
|
|
123
|
+
for (let i = 1, j = 0; i < n; i++) {
|
|
124
|
+
let bit = n >> 1;
|
|
125
|
+
for (; j & bit; bit >>= 1)
|
|
126
|
+
j ^= bit;
|
|
127
|
+
j ^= bit;
|
|
128
|
+
if (i < j) {
|
|
129
|
+
[re[i], re[j]] = [re[j], re[i]];
|
|
130
|
+
[im[i], im[j]] = [im[j], im[i]];
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
for (let len = 2; len <= n; len <<= 1) {
|
|
134
|
+
const ang = -2 * Math.PI / len;
|
|
135
|
+
const wr = Math.cos(ang);
|
|
136
|
+
const wi = Math.sin(ang);
|
|
137
|
+
for (let i = 0; i < n; i += len) {
|
|
138
|
+
let cr = 1;
|
|
139
|
+
let ci = 0;
|
|
140
|
+
for (let k = 0; k < len / 2; k++) {
|
|
141
|
+
const ur = re[i + k];
|
|
142
|
+
const ui = im[i + k];
|
|
143
|
+
const vr = re[i + k + len / 2] * cr - im[i + k + len / 2] * ci;
|
|
144
|
+
const vi = re[i + k + len / 2] * ci + im[i + k + len / 2] * cr;
|
|
145
|
+
re[i + k] = ur + vr;
|
|
146
|
+
im[i + k] = ui + vi;
|
|
147
|
+
re[i + k + len / 2] = ur - vr;
|
|
148
|
+
im[i + k + len / 2] = ui - vi;
|
|
149
|
+
const ncr = cr * wr - ci * wi;
|
|
150
|
+
ci = cr * wi + ci * wr;
|
|
151
|
+
cr = ncr;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ../core/dist/audioRhythm.js
|
|
158
|
+
var ANALYSIS_RATE = 22050;
|
|
159
|
+
var FRAME_SIZE = 1024;
|
|
160
|
+
var CHROMA_SIZE = 4096;
|
|
161
|
+
var CHROMA_FPS = 10;
|
|
162
|
+
var FRAMES_PER_SECOND = 100;
|
|
163
|
+
var LOW_BAND_HZ = 150;
|
|
164
|
+
var SLOWEST_BPM = 40;
|
|
165
|
+
var FASTEST_BPM = 240;
|
|
166
|
+
var TIGHTNESS = 100;
|
|
167
|
+
var ONSET_LAG_SECONDS = 0.015;
|
|
168
|
+
var round = (t) => Math.round(t * 1e3) / 1e3;
|
|
169
|
+
function bandEdges() {
|
|
170
|
+
const binHz = ANALYSIS_RATE / FRAME_SIZE;
|
|
171
|
+
const edges = [];
|
|
172
|
+
for (let hz = 30; hz < 9e3; hz *= 2 ** (1 / 5))
|
|
173
|
+
edges.push(Math.max(1, Math.round(hz / binHz)));
|
|
174
|
+
return edges;
|
|
175
|
+
}
|
|
176
|
+
var hann = (size) => Float64Array.from({ length: size }, (_, i) => 0.5 - 0.5 * Math.cos(2 * Math.PI * i / size));
|
|
177
|
+
function addFramePower(samples, center, window, edges, into) {
|
|
178
|
+
const size = window.length;
|
|
179
|
+
const re = new Float64Array(size);
|
|
180
|
+
const im = new Float64Array(size);
|
|
181
|
+
const first = center - size / 2;
|
|
182
|
+
for (let i = 0; i < size; i++) {
|
|
183
|
+
const at = first + i;
|
|
184
|
+
re[i] = at >= 0 && at < samples.length ? samples[at] * window[i] : 0;
|
|
185
|
+
}
|
|
186
|
+
fft(re, im);
|
|
187
|
+
for (let b = 0; b + 1 < edges.length; b++) {
|
|
188
|
+
const end = Math.max(edges[b] + 1, edges[b + 1]);
|
|
189
|
+
let power = 0;
|
|
190
|
+
for (let k = edges[b]; k < end; k++)
|
|
191
|
+
power += re[k] * re[k] + im[k] * im[k];
|
|
192
|
+
into[b] += power / (size * size * (end - edges[b]));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function bandPower(channels) {
|
|
196
|
+
const window = hann(FRAME_SIZE);
|
|
197
|
+
const edges = bandEdges();
|
|
198
|
+
const hop = ANALYSIS_RATE / FRAMES_PER_SECOND;
|
|
199
|
+
const bands = [];
|
|
200
|
+
for (let f = 0; Math.round(f * hop) < (channels[0]?.length ?? 0); f++) {
|
|
201
|
+
const power = new Float64Array(edges.length - 1);
|
|
202
|
+
for (const channel of channels)
|
|
203
|
+
addFramePower(channel, Math.round(f * hop), window, edges, power);
|
|
204
|
+
bands.push(power.map((p) => Math.log1p(1e9 * p / channels.length)));
|
|
205
|
+
}
|
|
206
|
+
return bands;
|
|
207
|
+
}
|
|
208
|
+
var PITCH_CLASS = Array.from({ length: CHROMA_SIZE / 2 }, (_, bin) => {
|
|
209
|
+
const hz = bin * ANALYSIS_RATE / CHROMA_SIZE;
|
|
210
|
+
if (hz < 55 || hz > 2e3)
|
|
211
|
+
return -1;
|
|
212
|
+
return ((Math.round(12 * Math.log2(hz / 440)) + 9) % 12 + 12) % 12;
|
|
213
|
+
});
|
|
214
|
+
function chromaFrames(channels) {
|
|
215
|
+
const window = hann(CHROMA_SIZE);
|
|
216
|
+
const hop = ANALYSIS_RATE / CHROMA_FPS;
|
|
217
|
+
const re = new Float64Array(CHROMA_SIZE);
|
|
218
|
+
const im = new Float64Array(CHROMA_SIZE);
|
|
219
|
+
const frames = [];
|
|
220
|
+
for (let f = 0; Math.round(f * hop) < (channels[0]?.length ?? 0); f++) {
|
|
221
|
+
const chroma = new Float64Array(12);
|
|
222
|
+
const first = Math.round(f * hop) - CHROMA_SIZE / 2;
|
|
223
|
+
for (const channel of channels) {
|
|
224
|
+
for (let i = 0; i < CHROMA_SIZE; i++) {
|
|
225
|
+
const at = first + i;
|
|
226
|
+
re[i] = at >= 0 && at < channel.length ? channel[at] * window[i] : 0;
|
|
227
|
+
im[i] = 0;
|
|
228
|
+
}
|
|
229
|
+
fft(re, im);
|
|
230
|
+
PITCH_CLASS.forEach((pc, bin) => {
|
|
231
|
+
if (pc >= 0)
|
|
232
|
+
chroma[pc] += Math.hypot(re[bin], im[bin]);
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
frames.push(chroma);
|
|
236
|
+
}
|
|
237
|
+
return frames;
|
|
238
|
+
}
|
|
239
|
+
function bandFrames(signal) {
|
|
240
|
+
const channels = signal.channels.map((c) => resample(c, signal.sampleRate, ANALYSIS_RATE));
|
|
241
|
+
return {
|
|
242
|
+
fps: FRAMES_PER_SECOND,
|
|
243
|
+
bands: bandPower(channels),
|
|
244
|
+
chroma: chromaFrames(channels),
|
|
245
|
+
chromaFps: CHROMA_FPS
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function aboveLocalMean(curve, radius) {
|
|
249
|
+
const sums = new Float64Array(curve.length + 1);
|
|
250
|
+
for (let i = 0; i < curve.length; i++)
|
|
251
|
+
sums[i + 1] = sums[i] + curve[i];
|
|
252
|
+
return curve.map((value, i) => {
|
|
253
|
+
const from = Math.max(0, i - radius);
|
|
254
|
+
const to = Math.min(curve.length, i + radius + 1);
|
|
255
|
+
return Math.max(0, value - (sums[to] - sums[from]) / (to - from));
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
function standardized(curve) {
|
|
259
|
+
const mean2 = curve.reduce((a, b) => a + b, 0) / (curve.length || 1);
|
|
260
|
+
const spread = Math.sqrt(curve.reduce((a, v) => a + (v - mean2) ** 2, 0) / (curve.length || 1));
|
|
261
|
+
return spread > 0 ? curve.map((v) => v / spread) : curve.map(() => 0);
|
|
262
|
+
}
|
|
263
|
+
function onsetCurves({ fps, bands }) {
|
|
264
|
+
const lowBands = bandEdges().filter((bin) => bin * ANALYSIS_RATE / FRAME_SIZE < LOW_BAND_HZ).length;
|
|
265
|
+
const broad = new Float64Array(bands.length);
|
|
266
|
+
const low = new Float64Array(bands.length);
|
|
267
|
+
for (let f = 1; f < bands.length; f++) {
|
|
268
|
+
const frame = bands[f];
|
|
269
|
+
const previous = bands[f - 1];
|
|
270
|
+
for (let b = 0; b < frame.length; b++) {
|
|
271
|
+
const rise = Math.max(0, frame[b] - previous[b]);
|
|
272
|
+
broad[f] += rise;
|
|
273
|
+
if (b < lowBands)
|
|
274
|
+
low[f] += rise;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const radius = Math.round(0.4 * fps);
|
|
278
|
+
return {
|
|
279
|
+
fps,
|
|
280
|
+
broad: standardized(aboveLocalMean(broad, radius)),
|
|
281
|
+
low: standardized(aboveLocalMean(low, radius))
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
function autocorrelation(curve, maxLag) {
|
|
285
|
+
const mean2 = curve.reduce((a, b) => a + b, 0) / (curve.length || 1);
|
|
286
|
+
const centred = curve.map((v) => v - mean2);
|
|
287
|
+
const ac = new Float64Array(maxLag + 1);
|
|
288
|
+
for (let lag = 0; lag <= maxLag && lag < centred.length; lag++) {
|
|
289
|
+
let sum = 0;
|
|
290
|
+
for (let i = lag; i < centred.length; i++)
|
|
291
|
+
sum += centred[i] * centred[i - lag];
|
|
292
|
+
ac[lag] = sum;
|
|
293
|
+
}
|
|
294
|
+
const zero = ac[0] || 1;
|
|
295
|
+
return ac.map((v) => v / zero);
|
|
296
|
+
}
|
|
297
|
+
var tempoPrior = (lag, fps) => Math.exp(-0.5 * (Math.log2(60 / (lag / fps) / 120) / 0.8) ** 2);
|
|
298
|
+
var fullRange = (fps) => ({
|
|
299
|
+
shortest: Math.max(1, Math.floor(60 / FASTEST_BPM * fps)),
|
|
300
|
+
longest: Math.ceil(60 / SLOWEST_BPM * fps)
|
|
301
|
+
});
|
|
302
|
+
function beatPeriod(curve, fps, { shortest, longest } = fullRange(fps)) {
|
|
303
|
+
const ac = autocorrelation(curve, 2 * longest + 1);
|
|
304
|
+
const at = (lag) => ac[Math.min(ac.length - 1, Math.max(0, lag))];
|
|
305
|
+
const score = (lag) => tempoPrior(lag, fps) * (at(lag) + 0.5 * at(2 * lag) + 0.25 * (at(2 * lag - 1) + at(2 * lag + 1)));
|
|
306
|
+
let best = shortest;
|
|
307
|
+
for (let lag = shortest; lag <= longest; lag++)
|
|
308
|
+
if (score(lag) > score(best))
|
|
309
|
+
best = lag;
|
|
310
|
+
const [a, b, c] = [score(best - 1), score(best), score(best + 1)];
|
|
311
|
+
const curvature = a - 2 * b + c;
|
|
312
|
+
const shift = curvature ? Math.max(-0.5, Math.min(0.5, 0.5 * (a - c) / curvature)) : 0;
|
|
313
|
+
return { period: best + shift, clarity: Math.max(0, Math.min(1, at(best))) };
|
|
314
|
+
}
|
|
315
|
+
function smoothed(curve, period) {
|
|
316
|
+
const half = Math.max(1, Math.round(period / 8));
|
|
317
|
+
const kernel = Array.from({ length: 2 * half + 1 }, (_, i) => Math.exp(-0.5 * ((i - half) * 32 / period) ** 2));
|
|
318
|
+
return curve.map((_, i) => {
|
|
319
|
+
let sum = 0;
|
|
320
|
+
for (let j = 0; j < kernel.length; j++)
|
|
321
|
+
sum += kernel[j] * (curve[i + j - half] ?? 0);
|
|
322
|
+
return sum;
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
function cumulativeScore(local, periodAt) {
|
|
326
|
+
const score = new Float64Array(local.length);
|
|
327
|
+
const back = new Int32Array(local.length).fill(-1);
|
|
328
|
+
for (let i = 0; i < local.length; i++) {
|
|
329
|
+
const period = periodAt(i);
|
|
330
|
+
let best = -Infinity;
|
|
331
|
+
let from = -1;
|
|
332
|
+
for (let j = Math.max(0, Math.round(i - 2 * period)); j <= i - Math.round(period / 2); j++) {
|
|
333
|
+
const candidate = score[j] - TIGHTNESS * Math.log((i - j) / period) ** 2;
|
|
334
|
+
if (candidate > best)
|
|
335
|
+
[best, from] = [candidate, j];
|
|
336
|
+
}
|
|
337
|
+
score[i] = local[i] + Math.max(0, best);
|
|
338
|
+
back[i] = best > 0 ? from : -1;
|
|
339
|
+
}
|
|
340
|
+
return { score, back };
|
|
341
|
+
}
|
|
342
|
+
function lastBeat(score) {
|
|
343
|
+
const maxima = [];
|
|
344
|
+
for (let i = 1; i + 1 < score.length; i++)
|
|
345
|
+
if (score[i] > score[i - 1] && score[i] >= score[i + 1])
|
|
346
|
+
maxima.push(i);
|
|
347
|
+
if (!maxima.length)
|
|
348
|
+
return score.length - 1;
|
|
349
|
+
const median = maxima.map((i) => score[i]).sort((a, b) => a - b)[maxima.length >> 1];
|
|
350
|
+
for (let k = maxima.length - 1; k >= 0; k--)
|
|
351
|
+
if (score[maxima[k]] >= 0.5 * median)
|
|
352
|
+
return maxima[k];
|
|
353
|
+
return maxima[maxima.length - 1];
|
|
354
|
+
}
|
|
355
|
+
function trimmed(beats, local) {
|
|
356
|
+
const rms = Math.sqrt(beats.reduce((a, b) => a + local[b] ** 2, 0) / (beats.length || 1));
|
|
357
|
+
let first = 0;
|
|
358
|
+
let end = beats.length;
|
|
359
|
+
while (first < end && local[beats[first]] < 0.5 * rms)
|
|
360
|
+
first++;
|
|
361
|
+
while (end > first && local[beats[end - 1]] < 0.5 * rms)
|
|
362
|
+
end--;
|
|
363
|
+
return beats.slice(first, end);
|
|
364
|
+
}
|
|
365
|
+
var LOCAL_WINDOW_SECONDS = 8;
|
|
366
|
+
function localPeriods(curve, fps, period) {
|
|
367
|
+
const window = Math.round(LOCAL_WINDOW_SECONDS * fps);
|
|
368
|
+
const hop = Math.round(fps);
|
|
369
|
+
const range = {
|
|
370
|
+
shortest: Math.max(fullRange(fps).shortest, Math.floor(period * 0.9)),
|
|
371
|
+
longest: Math.min(fullRange(fps).longest, Math.ceil(period * 1.1))
|
|
372
|
+
};
|
|
373
|
+
const centres = [];
|
|
374
|
+
const periods = [];
|
|
375
|
+
for (let start = 0; start === 0 || start + window <= curve.length; start += hop) {
|
|
376
|
+
const end = Math.min(curve.length, start + window);
|
|
377
|
+
periods.push(beatPeriod(curve.subarray(start, end), fps, range).period);
|
|
378
|
+
centres.push((start + end) / 2);
|
|
379
|
+
}
|
|
380
|
+
const median = periods.map((_, i) => {
|
|
381
|
+
const near = periods.slice(Math.max(0, i - 2), i + 3).sort((a, b) => a - b);
|
|
382
|
+
return near[near.length >> 1];
|
|
383
|
+
});
|
|
384
|
+
return (frame) => interpolate(centres, median, frame);
|
|
385
|
+
}
|
|
386
|
+
function interpolate(xs, ys, x) {
|
|
387
|
+
if (x <= xs[0])
|
|
388
|
+
return ys[0];
|
|
389
|
+
for (let i = 1; i < xs.length; i++)
|
|
390
|
+
if (x <= xs[i]) {
|
|
391
|
+
const t = (x - xs[i - 1]) / (xs[i] - xs[i - 1]);
|
|
392
|
+
return ys[i - 1] * (1 - t) + ys[i] * t;
|
|
393
|
+
}
|
|
394
|
+
return ys[ys.length - 1];
|
|
395
|
+
}
|
|
396
|
+
function beatFrames(curve, fps, period) {
|
|
397
|
+
const local = smoothed(curve, period);
|
|
398
|
+
const { score, back } = cumulativeScore(local, localPeriods(curve, fps, period));
|
|
399
|
+
const beats = [];
|
|
400
|
+
for (let i = lastBeat(score); i >= 0; i = back[i])
|
|
401
|
+
beats.push(i);
|
|
402
|
+
return { frames: trimmed(beats.reverse(), local), local };
|
|
403
|
+
}
|
|
404
|
+
function onsetLift(frames, local) {
|
|
405
|
+
const overall = local.reduce((a, b) => a + b, 0) / (local.length || 1);
|
|
406
|
+
const onBeats = frames.reduce((a, f) => a + local[f], 0) / (frames.length || 1);
|
|
407
|
+
return overall > 0 ? onBeats / overall - 1 : 0;
|
|
408
|
+
}
|
|
409
|
+
var mean = (values) => values.reduce((a, b) => a + b, 0) / (values.length || 1);
|
|
410
|
+
function zScored(values) {
|
|
411
|
+
const centre = mean(values);
|
|
412
|
+
const spread = Math.sqrt(mean(values.map((v) => (v - centre) ** 2))) || 1;
|
|
413
|
+
return values.map((v) => (v - centre) / spread);
|
|
414
|
+
}
|
|
415
|
+
var CHROMA_REACH_SECONDS = CHROMA_SIZE / 2 / ANALYSIS_RATE;
|
|
416
|
+
function chromaBetween({ chroma, chromaFps }, from, to) {
|
|
417
|
+
const middle = Math.round((from + to) / 2 * chromaFps);
|
|
418
|
+
let first = Math.ceil(from * chromaFps);
|
|
419
|
+
let end = Math.floor((to - CHROMA_REACH_SECONDS) * chromaFps) + 1;
|
|
420
|
+
if (end <= first)
|
|
421
|
+
[first, end] = [middle, middle + 1];
|
|
422
|
+
const sum = new Array(12).fill(0);
|
|
423
|
+
for (let f = first; f < end; f++)
|
|
424
|
+
for (let c = 0; c < 12; c++)
|
|
425
|
+
sum[c] += chroma[f]?.[c] ?? 0;
|
|
426
|
+
const length = Math.hypot(...sum) || 1;
|
|
427
|
+
return sum.map((v) => v / length);
|
|
428
|
+
}
|
|
429
|
+
var cosine = (a, b) => a.reduce((sum, v, i) => sum + v * b[i], 0);
|
|
430
|
+
function harmonicChange(beats, fps, frames, span) {
|
|
431
|
+
return beats.map((f, k) => {
|
|
432
|
+
if (k < span || k + span >= beats.length)
|
|
433
|
+
return 0;
|
|
434
|
+
const before = chromaBetween(frames, beats[k - span] / fps, f / fps);
|
|
435
|
+
return 1 - cosine(before, chromaBetween(frames, f / fps, beats[k + span] / fps));
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
function barStartEvidence(beats, curves, frames) {
|
|
439
|
+
const lowHit = (f) => Math.max(...[-1, 0, 1, 2].map((d) => curves.low[f + d] ?? 0));
|
|
440
|
+
const cues = [
|
|
441
|
+
zScored(beats.map(lowHit)),
|
|
442
|
+
zScored(harmonicChange(beats, curves.fps, frames, 1)),
|
|
443
|
+
zScored(harmonicChange(beats, curves.fps, frames, 2))
|
|
444
|
+
];
|
|
445
|
+
return beats.map((_, k) => cues.reduce((sum, cue) => sum + cue[k], 0));
|
|
446
|
+
}
|
|
447
|
+
var TRIPLE_METER_HANDICAP = 0.7;
|
|
448
|
+
function barPhase(beats, curves, frames) {
|
|
449
|
+
const evidence = barStartEvidence(beats, curves, frames);
|
|
450
|
+
let best = { meter: 4, phase: 0, contrast: -Infinity };
|
|
451
|
+
for (const meter of [4, 3])
|
|
452
|
+
for (let phase = 0; phase < meter; phase++) {
|
|
453
|
+
const on = evidence.filter((_, k) => (k - phase) % meter === 0);
|
|
454
|
+
const off = evidence.filter((_, k) => (k - phase) % meter !== 0);
|
|
455
|
+
const contrast = (mean(on) - mean(off)) * (meter === 3 ? TRIPLE_METER_HANDICAP : 1);
|
|
456
|
+
if (contrast > best.contrast)
|
|
457
|
+
best = { meter, phase, contrast };
|
|
458
|
+
}
|
|
459
|
+
return best;
|
|
460
|
+
}
|
|
461
|
+
function downbeatsOf(beatList, times, curves, frames) {
|
|
462
|
+
const bar = beatList.length >= 6 ? barPhase(beatList, curves, frames) : null;
|
|
463
|
+
return {
|
|
464
|
+
times: bar ? times.filter((_, k) => (k - bar.phase) % bar.meter === 0) : [],
|
|
465
|
+
meter: bar?.meter ?? null,
|
|
466
|
+
confidence: "low"
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
function medianInterval(times) {
|
|
470
|
+
const gaps = times.slice(1).map((t, i) => t - times[i]).sort((a, b) => a - b);
|
|
471
|
+
return gaps.length ? gaps[gaps.length >> 1] : null;
|
|
472
|
+
}
|
|
473
|
+
var noRhythm = (confidence, reason = "no steady beat") => ({
|
|
474
|
+
beats: { times: [], confidence, reason },
|
|
475
|
+
downbeats: { times: [], meter: null, confidence: "low" }
|
|
476
|
+
});
|
|
477
|
+
var SHORTEST_SECONDS = 6;
|
|
478
|
+
var STEADY_BEAT_CONFIDENCE = 0.15;
|
|
479
|
+
var beatConfidence = (clarity, lift) => round(Math.max(0, Math.min(1, clarity)) * Math.max(0, Math.min(1, lift / 3)));
|
|
480
|
+
function rhythmOf(frames) {
|
|
481
|
+
const curves = onsetCurves(frames);
|
|
482
|
+
const { period, clarity } = beatPeriod(curves.broad, curves.fps);
|
|
483
|
+
if (curves.broad.length < SHORTEST_SECONDS * curves.fps)
|
|
484
|
+
return noRhythm(0, "too short for a steady beat");
|
|
485
|
+
if (!curves.broad.some((v) => v > 0) || curves.broad.length < 2 * period)
|
|
486
|
+
return noRhythm(0);
|
|
487
|
+
const { frames: beatList, local } = beatFrames(curves.broad, curves.fps, period);
|
|
488
|
+
const confidence = beatConfidence(clarity, onsetLift(beatList, local));
|
|
489
|
+
if (beatList.length < 4 || confidence < STEADY_BEAT_CONFIDENCE)
|
|
490
|
+
return noRhythm(confidence);
|
|
491
|
+
const times = beatList.map((f) => round(f / curves.fps + ONSET_LAG_SECONDS));
|
|
492
|
+
return { beats: { times, confidence }, downbeats: downbeatsOf(beatList, times, curves, frames) };
|
|
493
|
+
}
|
|
494
|
+
var TEMPO_STEP_SECONDS = 4;
|
|
495
|
+
function steadyInterval(times) {
|
|
496
|
+
const median = medianInterval(times);
|
|
497
|
+
if (!median)
|
|
498
|
+
return null;
|
|
499
|
+
const near = times.slice(1).map((t, i) => t - times[i]).filter((gap) => Math.abs(gap - median) <= 0.1 * median);
|
|
500
|
+
return near.reduce((a, b) => a + b, 0) / near.length;
|
|
501
|
+
}
|
|
502
|
+
function tempoOf(times) {
|
|
503
|
+
const interval = steadyInterval(times);
|
|
504
|
+
if (!interval)
|
|
505
|
+
return { value: null, curve: [] };
|
|
506
|
+
const bpm = (gap) => Math.round(600 / gap) / 10;
|
|
507
|
+
const curve = [];
|
|
508
|
+
const last = times[times.length - 1];
|
|
509
|
+
for (let t = times[0]; t <= last; t += TEMPO_STEP_SECONDS) {
|
|
510
|
+
const gap = steadyInterval(times.filter((b) => Math.abs(b - t) <= TEMPO_STEP_SECONDS));
|
|
511
|
+
if (gap)
|
|
512
|
+
curve.push({ t: round(t), bpm: bpm(gap) });
|
|
513
|
+
}
|
|
514
|
+
return { value: bpm(interval), curve };
|
|
515
|
+
}
|
|
516
|
+
var measured = /* @__PURE__ */ new WeakMap();
|
|
517
|
+
function bandFramesOf(signal) {
|
|
518
|
+
let entry = measured.get(signal);
|
|
519
|
+
if (!entry)
|
|
520
|
+
measured.set(signal, entry = { frames: bandFrames(signal) });
|
|
521
|
+
return entry.frames;
|
|
522
|
+
}
|
|
523
|
+
function rhythm(signal) {
|
|
524
|
+
const frames = bandFramesOf(signal);
|
|
525
|
+
const entry = measured.get(signal);
|
|
526
|
+
return entry.rhythm ??= rhythmOf(frames);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// ../core/dist/audioAnalysisLadder.js
|
|
530
|
+
function defaultLadder(capability, providers) {
|
|
531
|
+
return providers.filter((p) => p.capabilities.includes(capability)).map((p) => p.id);
|
|
532
|
+
}
|
|
533
|
+
function exclusion(provider, quality) {
|
|
534
|
+
if (quality === "fast" && !provider.signal)
|
|
535
|
+
return "quality fast allows signal processing only";
|
|
536
|
+
if (quality === "local" && !provider.local)
|
|
537
|
+
return "quality local keeps audio on this machine";
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
var message = (error) => error instanceof Error ? error.message : String(error);
|
|
541
|
+
async function availabilityOf(provider) {
|
|
542
|
+
try {
|
|
543
|
+
return provider.available ? await provider.available() : { ok: true };
|
|
544
|
+
} catch (error) {
|
|
545
|
+
return { ok: false, reason: message(error) };
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
async function blocked(id, candidate, capability, quality) {
|
|
549
|
+
if (!candidate?.capabilities.includes(capability))
|
|
550
|
+
return { provider: id, outcome: "unavailable", reason: `no provider ${id} for ${capability}` };
|
|
551
|
+
const excluded = exclusion(candidate, quality);
|
|
552
|
+
if (excluded)
|
|
553
|
+
return { provider: id, outcome: "excluded", reason: excluded };
|
|
554
|
+
const availability = await availabilityOf(candidate);
|
|
555
|
+
if (!availability.ok)
|
|
556
|
+
return { provider: id, outcome: "unavailable", reason: availability.reason };
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
function passedOver(capability, steps, answered) {
|
|
560
|
+
const skipped = steps.filter((s) => s.outcome === "unavailable" || s.outcome === "failed");
|
|
561
|
+
if (!skipped.length)
|
|
562
|
+
return null;
|
|
563
|
+
const why = skipped.map((s) => `${s.provider} ${s.outcome}${s.reason ? ` (${s.reason})` : ""}`).join("; ");
|
|
564
|
+
return answered ? `${capability}: used ${answered}; ${why}` : `${capability}: ${why}`;
|
|
565
|
+
}
|
|
566
|
+
async function runLadder(capability, input, providers, { quality = "best", ladder, provider, options = {} } = {}) {
|
|
567
|
+
const byId = new Map(providers.map((p) => [p.id, p]));
|
|
568
|
+
const order = provider ? [provider] : ladder ?? defaultLadder(capability, providers);
|
|
569
|
+
const steps = [];
|
|
570
|
+
for (const id of order) {
|
|
571
|
+
const candidate = byId.get(id);
|
|
572
|
+
const skip = await blocked(id, candidate, capability, quality);
|
|
573
|
+
if (skip) {
|
|
574
|
+
steps.push(skip);
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
try {
|
|
578
|
+
const value = await candidate.analyze(capability, input, options);
|
|
579
|
+
const warning = passedOver(capability, steps, id);
|
|
580
|
+
steps.push({ provider: id, outcome: "answered" });
|
|
581
|
+
return { value, provenance: id, steps, warnings: warning ? [warning] : [] };
|
|
582
|
+
} catch (error) {
|
|
583
|
+
steps.push({ provider: id, outcome: "failed", reason: message(error) });
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
const none = order.length ? "" : " (none offers it)";
|
|
587
|
+
const why = passedOver(capability, steps, null)?.slice(capability.length + 2);
|
|
588
|
+
return {
|
|
589
|
+
value: null,
|
|
590
|
+
provenance: null,
|
|
591
|
+
steps,
|
|
592
|
+
warnings: [`${capability}: no provider answered${none}${why ? `; ${why}` : ""}`]
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// ../core/dist/audioSnap.js
|
|
597
|
+
function nearestIndex(sorted, t) {
|
|
598
|
+
let low = 0;
|
|
599
|
+
let high = sorted.length - 1;
|
|
600
|
+
while (low < high) {
|
|
601
|
+
const mid = low + high >> 1;
|
|
602
|
+
if (sorted[mid] < t)
|
|
603
|
+
low = mid + 1;
|
|
604
|
+
else
|
|
605
|
+
high = mid;
|
|
606
|
+
}
|
|
607
|
+
return low > 0 && t - sorted[low - 1] <= sorted[low] - t ? low - 1 : low;
|
|
608
|
+
}
|
|
609
|
+
function snapper(measured2, toleranceSeconds, reference) {
|
|
610
|
+
const shifts = [];
|
|
611
|
+
const unsnapped = [];
|
|
612
|
+
return {
|
|
613
|
+
snap(t) {
|
|
614
|
+
const nearest = measured2.length ? measured2[nearestIndex(measured2, t)] : null;
|
|
615
|
+
if (nearest === null || Math.abs(nearest - t) > toleranceSeconds) {
|
|
616
|
+
unsnapped.push(t);
|
|
617
|
+
return t;
|
|
618
|
+
}
|
|
619
|
+
shifts.push(Math.abs(nearest - t));
|
|
620
|
+
return nearest;
|
|
621
|
+
},
|
|
622
|
+
report() {
|
|
623
|
+
const sorted = [...shifts].sort((a, b) => a - b);
|
|
624
|
+
const median = sorted.length ? sorted[sorted.length >> 1] : null;
|
|
625
|
+
return {
|
|
626
|
+
reference,
|
|
627
|
+
toleranceMs: Math.round(toleranceSeconds * 1e3),
|
|
628
|
+
snapped: shifts.length,
|
|
629
|
+
unsnapped: [...unsnapped],
|
|
630
|
+
medianShiftMs: median === null ? null : Math.round(median * 1e3)
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// ../core/dist/audioSections.js
|
|
637
|
+
var BLOCK_SECONDS = 0.5;
|
|
638
|
+
var KERNEL_BLOCKS = 16;
|
|
639
|
+
var SAME_SECTION = 0.9;
|
|
640
|
+
var round2 = (t) => Math.round(t * 1e3) / 1e3;
|
|
641
|
+
function meanOf(frames) {
|
|
642
|
+
const sum = new Array(frames[0]?.length ?? 0).fill(0);
|
|
643
|
+
for (const frame of frames)
|
|
644
|
+
for (let b = 0; b < sum.length; b++)
|
|
645
|
+
sum[b] += frame[b];
|
|
646
|
+
return sum.map((v) => v / (frames.length || 1));
|
|
647
|
+
}
|
|
648
|
+
function standardizeBands(blocks) {
|
|
649
|
+
for (let b = 0; b < (blocks[0]?.length ?? 0); b++) {
|
|
650
|
+
const column = blocks.map((block) => block[b]);
|
|
651
|
+
const mean2 = column.reduce((a, v) => a + v, 0) / column.length;
|
|
652
|
+
const spread = Math.sqrt(column.reduce((a, v) => a + (v - mean2) ** 2, 0) / column.length);
|
|
653
|
+
for (const block of blocks)
|
|
654
|
+
block[b] = (block[b] - mean2) / (spread || 1);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
function blockParts({ fps, bands, chroma, chromaFps }) {
|
|
658
|
+
const per = Math.max(1, Math.round(BLOCK_SECONDS * fps));
|
|
659
|
+
const chromaPer = Math.max(1, Math.round(BLOCK_SECONDS * chromaFps));
|
|
660
|
+
const blocks = [];
|
|
661
|
+
for (let start = 0, b = 0; start + per <= bands.length; start += per, b++) {
|
|
662
|
+
const pitch = meanOf(chroma.slice(b * chromaPer, (b + 1) * chromaPer)).map(Math.log1p);
|
|
663
|
+
blocks.push([...meanOf(bands.slice(start, start + per)), ...unit(pitch)]);
|
|
664
|
+
}
|
|
665
|
+
return blocks;
|
|
666
|
+
}
|
|
667
|
+
function blockFeatures(frames) {
|
|
668
|
+
const blocks = blockParts(frames);
|
|
669
|
+
standardizeBands(blocks);
|
|
670
|
+
const split = frames.bands[0]?.length ?? 0;
|
|
671
|
+
return blocks.map((block) => [...unit(block.slice(0, split)), ...unit(block.slice(split))].map((v) => v * Math.SQRT1_2));
|
|
672
|
+
}
|
|
673
|
+
function unit(vector) {
|
|
674
|
+
const length = Math.hypot(...vector) || 1;
|
|
675
|
+
return vector.map((v) => v / length);
|
|
676
|
+
}
|
|
677
|
+
var dot = (a, b) => a.reduce((sum, v, i) => sum + v * b[i], 0);
|
|
678
|
+
var CHECKERBOARD = (() => {
|
|
679
|
+
const offsets = Array.from({ length: 2 * KERNEL_BLOCKS }, (_, k) => k - KERNEL_BLOCKS);
|
|
680
|
+
const taper = (k) => Math.exp(-0.5 * (((k < 0 ? -k - 1 : k) + 0.5) / (KERNEL_BLOCKS / 2)) ** 2);
|
|
681
|
+
return offsets.map((a) => offsets.map((b) => (a < 0 === b < 0 ? 1 : -1) * taper(a) * taper(b)));
|
|
682
|
+
})();
|
|
683
|
+
function noveltyAt(blocks, i) {
|
|
684
|
+
let sum = 0;
|
|
685
|
+
CHECKERBOARD.forEach((row, a) => {
|
|
686
|
+
const x = blocks[i + a - KERNEL_BLOCKS];
|
|
687
|
+
if (!x)
|
|
688
|
+
return;
|
|
689
|
+
row.forEach((weight, b) => {
|
|
690
|
+
const y = blocks[i + b - KERNEL_BLOCKS];
|
|
691
|
+
if (y)
|
|
692
|
+
sum += weight * dot(x, y);
|
|
693
|
+
});
|
|
694
|
+
});
|
|
695
|
+
return sum;
|
|
696
|
+
}
|
|
697
|
+
var novelty = (blocks) => blocks.map((_, i) => noveltyAt(blocks, i));
|
|
698
|
+
function boundaryBlocks(curve, minBlocks) {
|
|
699
|
+
const mean2 = curve.reduce((a, b) => a + b, 0) / (curve.length || 1);
|
|
700
|
+
const spread = Math.sqrt(curve.reduce((a, v) => a + (v - mean2) ** 2, 0) / (curve.length || 1));
|
|
701
|
+
const peaks = [];
|
|
702
|
+
for (let i = minBlocks; i <= curve.length - minBlocks; i++) {
|
|
703
|
+
const neighbourhood = curve.slice(Math.max(0, i - minBlocks), i + minBlocks + 1);
|
|
704
|
+
if (curve[i] < Math.max(...neighbourhood) || curve[i] <= mean2 + spread)
|
|
705
|
+
continue;
|
|
706
|
+
if (!peaks.length || i - peaks[peaks.length - 1] >= minBlocks)
|
|
707
|
+
peaks.push(i);
|
|
708
|
+
}
|
|
709
|
+
return peaks;
|
|
710
|
+
}
|
|
711
|
+
function labels(blocks, edges) {
|
|
712
|
+
const means = [];
|
|
713
|
+
const letters = [];
|
|
714
|
+
for (let s = 0; s + 1 < edges.length; s++) {
|
|
715
|
+
const part = blocks.slice(edges[s], edges[s + 1]);
|
|
716
|
+
const mean2 = unit(part[0].map((_, b) => part.reduce((a, block) => a + block[b], 0)));
|
|
717
|
+
const match = means.findIndex((other) => dot(other, mean2) >= SAME_SECTION);
|
|
718
|
+
letters.push(match >= 0 ? letters[match] : String.fromCharCode(65 + new Set(letters).size % 26));
|
|
719
|
+
means.push(mean2);
|
|
720
|
+
}
|
|
721
|
+
return letters;
|
|
722
|
+
}
|
|
723
|
+
var beatTolerance = (beats) => (medianInterval(beats) ?? 0) / 2;
|
|
724
|
+
function sections(signal, options = {}) {
|
|
725
|
+
const duration = round2((signal.channels[0]?.length ?? 0) / signal.sampleRate);
|
|
726
|
+
const blocks = blockFeatures(bandFramesOf(signal));
|
|
727
|
+
const minBlocks = Math.max(1, Math.round((options.minSeconds ?? 4) / BLOCK_SECONDS));
|
|
728
|
+
if (blocks.length < 2 * minBlocks)
|
|
729
|
+
return { spans: [{ start: 0, end: duration, label: "A" }] };
|
|
730
|
+
const edges = [0, ...boundaryBlocks(novelty(blocks), minBlocks), blocks.length];
|
|
731
|
+
const letters = labels(blocks, edges);
|
|
732
|
+
const beats = rhythm(signal).beats.times;
|
|
733
|
+
const onBeat = snapper(beats, beatTolerance(beats), "signal beats");
|
|
734
|
+
const times = edges.map((e, i) => i === 0 ? 0 : i === edges.length - 1 ? duration : round2(onBeat.snap(e * BLOCK_SECONDS)));
|
|
735
|
+
return {
|
|
736
|
+
spans: letters.map((label, s) => ({ start: times[s], end: times[s + 1], label })),
|
|
737
|
+
snap: onBeat.report()
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// ../core/dist/audioAnalysis.js
|
|
742
|
+
var DB_FLOOR = -120;
|
|
743
|
+
var signalDuration = (signal) => (signal.channels[0]?.length ?? 0) / signal.sampleRate;
|
|
744
|
+
function energyMix(signal) {
|
|
745
|
+
const [first, ...rest] = signal.channels;
|
|
746
|
+
if (!first)
|
|
747
|
+
return new Float32Array(0);
|
|
748
|
+
if (!rest.length)
|
|
749
|
+
return first;
|
|
750
|
+
const mixed = new Float32Array(first.length);
|
|
751
|
+
for (const channel of signal.channels)
|
|
752
|
+
for (let i = 0; i < mixed.length; i++)
|
|
753
|
+
mixed[i] += channel[i] * channel[i];
|
|
754
|
+
const scale = 1 / signal.channels.length;
|
|
755
|
+
for (let i = 0; i < mixed.length; i++)
|
|
756
|
+
mixed[i] = Math.sqrt(mixed[i] * scale);
|
|
757
|
+
return mixed;
|
|
758
|
+
}
|
|
759
|
+
var toDb = (amplitude) => amplitude > 0 ? Math.max(DB_FLOOR, 20 * Math.log10(amplitude)) : DB_FLOOR;
|
|
760
|
+
var hopSamples = (sampleRate, hopMs) => Math.max(1, Math.round(sampleRate * hopMs / 1e3));
|
|
761
|
+
var round3 = (t) => Math.round(t * 1e4) / 1e4;
|
|
762
|
+
function rmsFrames(samples, sampleRate, hopMs, windowMs) {
|
|
763
|
+
const hop = hopSamples(sampleRate, hopMs);
|
|
764
|
+
const half = Math.max(1, Math.round(sampleRate * windowMs / 2e3));
|
|
765
|
+
const frames = [];
|
|
766
|
+
for (let center = 0; center < samples.length; center += hop) {
|
|
767
|
+
const from = Math.max(0, center - half);
|
|
768
|
+
const to = Math.min(samples.length, center + half);
|
|
769
|
+
let sum = 0;
|
|
770
|
+
for (let i = from; i < to; i++)
|
|
771
|
+
sum += samples[i] * samples[i];
|
|
772
|
+
frames.push(to > from ? Math.sqrt(sum / (to - from)) : 0);
|
|
773
|
+
}
|
|
774
|
+
return frames;
|
|
775
|
+
}
|
|
776
|
+
var fftSizeFor = (sampleRate) => 2 ** Math.round(Math.log2(sampleRate * 0.023));
|
|
777
|
+
function bandBins(sampleRate, size, band) {
|
|
778
|
+
const binHz = sampleRate / size;
|
|
779
|
+
const low = Math.max(1, Math.floor((band?.[0] ?? 0) / binHz));
|
|
780
|
+
return [low, Math.max(low, Math.min(size / 2, Math.ceil((band?.[1] ?? sampleRate / 2) / binHz)))];
|
|
781
|
+
}
|
|
782
|
+
function loadFrame(samples, center, window, re, im) {
|
|
783
|
+
const first = center - window.length / 2;
|
|
784
|
+
for (let i = 0; i < window.length; i++) {
|
|
785
|
+
const at = first + i;
|
|
786
|
+
re[i] = at >= 0 && at < samples.length ? samples[at] * window[i] : 0;
|
|
787
|
+
im[i] = 0;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
function spectra(samples, sampleRate, hopMs, band) {
|
|
791
|
+
const size = fftSizeFor(sampleRate);
|
|
792
|
+
const hop = hopSamples(sampleRate, hopMs);
|
|
793
|
+
const window = Float64Array.from({ length: size }, (_, i) => 0.5 - 0.5 * Math.cos(2 * Math.PI * i / size));
|
|
794
|
+
const [low, high] = bandBins(sampleRate, size, band);
|
|
795
|
+
const frames = [];
|
|
796
|
+
const re = new Float64Array(size);
|
|
797
|
+
const im = new Float64Array(size);
|
|
798
|
+
for (let center = 0; center < samples.length; center += hop) {
|
|
799
|
+
loadFrame(samples, center, window, re, im);
|
|
800
|
+
fft(re, im);
|
|
801
|
+
const magnitudes = new Float64Array(high - low);
|
|
802
|
+
for (let k = low; k < high; k++)
|
|
803
|
+
magnitudes[k - low] = Math.hypot(re[k], im[k]);
|
|
804
|
+
frames.push(magnitudes);
|
|
805
|
+
}
|
|
806
|
+
return frames;
|
|
807
|
+
}
|
|
808
|
+
function averageMagnitudes(frames) {
|
|
809
|
+
const mixed = new Float64Array(frames[0]?.length ?? 0);
|
|
810
|
+
for (const frame of frames)
|
|
811
|
+
for (let bin = 0; bin < mixed.length; bin++)
|
|
812
|
+
mixed[bin] += frame[bin];
|
|
813
|
+
const scale = frames.length || 1;
|
|
814
|
+
for (let bin = 0; bin < mixed.length; bin++)
|
|
815
|
+
mixed[bin] /= scale;
|
|
816
|
+
return mixed;
|
|
817
|
+
}
|
|
818
|
+
function meanSpectra(signal, hopMs, band) {
|
|
819
|
+
const stacked = signal.channels.map((channel) => spectra(channel, signal.sampleRate, hopMs, band));
|
|
820
|
+
const count = stacked[0]?.length ?? 0;
|
|
821
|
+
const mixed = [];
|
|
822
|
+
for (let frame = 0; frame < count; frame++)
|
|
823
|
+
mixed.push(averageMagnitudes(stacked.map((channel) => channel[frame])));
|
|
824
|
+
return mixed;
|
|
825
|
+
}
|
|
826
|
+
function envelope(signal, options = {}) {
|
|
827
|
+
const { hopMs = 5, scale = "db", band } = options;
|
|
828
|
+
const windowMs = options.windowMs ?? hopMs * 2;
|
|
829
|
+
const size = fftSizeFor(signal.sampleRate);
|
|
830
|
+
const amplitudes = band ? meanSpectra(signal, hopMs, band).map((frame) => {
|
|
831
|
+
let sum = 0;
|
|
832
|
+
for (const magnitude of frame)
|
|
833
|
+
sum += magnitude * magnitude;
|
|
834
|
+
return Math.sqrt(2 * sum / (size * size * 0.375));
|
|
835
|
+
}) : rmsFrames(energyMix(signal), signal.sampleRate, hopMs, windowMs);
|
|
836
|
+
return { hopMs, scale, values: amplitudes.map((a) => scale === "db" ? toDb(a) : a) };
|
|
837
|
+
}
|
|
838
|
+
function noveltyCurve(signal, method, hopMs, band) {
|
|
839
|
+
const samples = energyMix(signal);
|
|
840
|
+
if (method === "energy") {
|
|
841
|
+
const rms = rmsFrames(samples, signal.sampleRate, hopMs, hopMs * 2);
|
|
842
|
+
return rms.map((value, i) => Math.max(0, value - (rms[i - 1] ?? 0)));
|
|
843
|
+
}
|
|
844
|
+
const frames = meanSpectra(signal, hopMs, band).map((frame) => frame.map((m) => Math.log1p(100 * m)));
|
|
845
|
+
const lastWhole = Math.floor((samples.length - fftSizeFor(signal.sampleRate) / 2) / hopSamples(signal.sampleRate, hopMs));
|
|
846
|
+
return frames.map((frame, i) => {
|
|
847
|
+
const previous = frames[i - 1];
|
|
848
|
+
if (!previous || i > lastWhole)
|
|
849
|
+
return 0;
|
|
850
|
+
let flux = 0;
|
|
851
|
+
for (let k = 0; k < frame.length; k++)
|
|
852
|
+
flux += Math.max(0, frame[k] - previous[k]);
|
|
853
|
+
return flux;
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
function isOnsetPeak(curve, i, gap, context, top) {
|
|
857
|
+
const value = curve[i];
|
|
858
|
+
let sum = 0;
|
|
859
|
+
let count = 0;
|
|
860
|
+
for (let j = Math.max(0, i - context); j <= Math.min(curve.length - 1, i + context); j++) {
|
|
861
|
+
if (Math.abs(j - i) <= gap && curve[j] > value)
|
|
862
|
+
return false;
|
|
863
|
+
sum += curve[j];
|
|
864
|
+
count++;
|
|
865
|
+
}
|
|
866
|
+
return value >= sum / count + 0.05 * top;
|
|
867
|
+
}
|
|
868
|
+
function onsets(signal, options = {}) {
|
|
869
|
+
const { method = "spectral-flux", band, threshold = 0.1, minGapMs = 30, hopMs = 5 } = options;
|
|
870
|
+
const curve = noveltyCurve(signal, method, hopMs, band);
|
|
871
|
+
const top = Math.max(0, ...curve);
|
|
872
|
+
if (top <= 0)
|
|
873
|
+
return { method, events: [] };
|
|
874
|
+
const gap = Math.max(1, Math.round(minGapMs / hopMs));
|
|
875
|
+
const context = Math.max(gap, Math.round(100 / hopMs));
|
|
876
|
+
const hopSeconds = hopSamples(signal.sampleRate, hopMs) / signal.sampleRate;
|
|
877
|
+
const events = [];
|
|
878
|
+
for (let i = 0; i < curve.length; i++) {
|
|
879
|
+
const strength = curve[i] / top;
|
|
880
|
+
if (strength < threshold || !isOnsetPeak(curve, i, gap, context, top))
|
|
881
|
+
continue;
|
|
882
|
+
const onset = { t: round3(i * hopSeconds), strength: round3(strength) };
|
|
883
|
+
const last = events[events.length - 1];
|
|
884
|
+
if (!last || onset.t - last.t >= minGapMs / 1e3)
|
|
885
|
+
events.push(onset);
|
|
886
|
+
else if (onset.strength > last.strength)
|
|
887
|
+
events[events.length - 1] = onset;
|
|
888
|
+
}
|
|
889
|
+
return { method, events };
|
|
890
|
+
}
|
|
891
|
+
function moments(signal, options = {}) {
|
|
892
|
+
const { onsetRatio = 0.2, tailDb = -40, hopMs = 1 } = options;
|
|
893
|
+
const rms = rmsFrames(energyMix(signal), signal.sampleRate, hopMs, Math.max(4, hopMs * 4));
|
|
894
|
+
const hopSeconds = hopSamples(signal.sampleRate, hopMs) / signal.sampleRate;
|
|
895
|
+
let peakIndex = 0;
|
|
896
|
+
for (let i = 1; i < rms.length; i++)
|
|
897
|
+
if (rms[i] > rms[peakIndex])
|
|
898
|
+
peakIndex = i;
|
|
899
|
+
const peak = rms[peakIndex] ?? 0;
|
|
900
|
+
if (peak <= 0)
|
|
901
|
+
return null;
|
|
902
|
+
let onsetIndex = rms.findIndex((value) => value >= peak * onsetRatio);
|
|
903
|
+
if (onsetIndex < 0)
|
|
904
|
+
onsetIndex = peakIndex;
|
|
905
|
+
let steepestIndex = onsetIndex;
|
|
906
|
+
let steepestRise = -Infinity;
|
|
907
|
+
for (let i = 1; i <= peakIndex; i++) {
|
|
908
|
+
const rise = rms[i] - rms[i - 1];
|
|
909
|
+
if (rise > steepestRise)
|
|
910
|
+
[steepestRise, steepestIndex] = [rise, i];
|
|
911
|
+
}
|
|
912
|
+
const floor = peak * 10 ** (tailDb / 20);
|
|
913
|
+
let tailIndex = peakIndex;
|
|
914
|
+
for (let i = rms.length - 1; i > peakIndex; i--)
|
|
915
|
+
if (rms[i] >= floor) {
|
|
916
|
+
tailIndex = i;
|
|
917
|
+
break;
|
|
918
|
+
}
|
|
919
|
+
return {
|
|
920
|
+
onset: round3(onsetIndex * hopSeconds),
|
|
921
|
+
steepest: round3(steepestIndex * hopSeconds),
|
|
922
|
+
peak: round3(peakIndex * hopSeconds),
|
|
923
|
+
tailEnd: round3(tailIndex * hopSeconds)
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
function kWeighting(sampleRate) {
|
|
927
|
+
let k = Math.tan(Math.PI * 1681.974450955533 / sampleRate);
|
|
928
|
+
const q = 0.7071752369554196;
|
|
929
|
+
const vh = 10 ** (3.999843853973347 / 20);
|
|
930
|
+
const vb = vh ** 0.4996667741545416;
|
|
931
|
+
let a0 = 1 + k / q + k * k;
|
|
932
|
+
const shelf = [
|
|
933
|
+
(vh + vb * k / q + k * k) / a0,
|
|
934
|
+
2 * (k * k - vh) / a0,
|
|
935
|
+
(vh - vb * k / q + k * k) / a0,
|
|
936
|
+
2 * (k * k - 1) / a0,
|
|
937
|
+
(1 - k / q + k * k) / a0
|
|
938
|
+
];
|
|
939
|
+
k = Math.tan(Math.PI * 38.13547087602444 / sampleRate);
|
|
940
|
+
const q2 = 0.5003270373238773;
|
|
941
|
+
a0 = 1 + k / q2 + k * k;
|
|
942
|
+
const highPass = [1, -2, 1, 2 * (k * k - 1) / a0, (1 - k / q2 + k * k) / a0];
|
|
943
|
+
return [shelf, highPass];
|
|
944
|
+
}
|
|
945
|
+
function filtered(samples, [b0, b1, b2, a1, a2]) {
|
|
946
|
+
const out = new Float64Array(samples.length);
|
|
947
|
+
let x1 = 0, x2 = 0, y1 = 0, y2 = 0;
|
|
948
|
+
for (let i = 0; i < samples.length; i++) {
|
|
949
|
+
const x = samples[i];
|
|
950
|
+
const y = b0 * x + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2;
|
|
951
|
+
x2 = x1;
|
|
952
|
+
x1 = x;
|
|
953
|
+
y2 = y1;
|
|
954
|
+
y1 = y;
|
|
955
|
+
out[i] = y;
|
|
956
|
+
}
|
|
957
|
+
return out;
|
|
958
|
+
}
|
|
959
|
+
var blockLoudness = (meanSquare) => meanSquare > 0 ? -0.691 + 10 * Math.log10(meanSquare) : -Infinity;
|
|
960
|
+
function squareSums(channel) {
|
|
961
|
+
const sums = new Float64Array(channel.length + 1);
|
|
962
|
+
for (let i = 0; i < channel.length; i++)
|
|
963
|
+
sums[i + 1] = sums[i] + channel[i] * channel[i];
|
|
964
|
+
return sums;
|
|
965
|
+
}
|
|
966
|
+
function blockPowers(sums, sampleRate, blockSeconds, stepSeconds) {
|
|
967
|
+
const length = (sums[0]?.length ?? 1) - 1;
|
|
968
|
+
if (!length)
|
|
969
|
+
return [];
|
|
970
|
+
const block = Math.min(length, Math.round(blockSeconds * sampleRate));
|
|
971
|
+
const step = Math.round(stepSeconds * sampleRate);
|
|
972
|
+
const powers = [];
|
|
973
|
+
for (let start = 0; start + block <= length; start += step)
|
|
974
|
+
powers.push(sums.reduce((power, sum) => power + (sum[start + block] - sum[start]) / block, 0));
|
|
975
|
+
return powers;
|
|
976
|
+
}
|
|
977
|
+
var TRUE_PEAK_OVERSAMPLE = 4;
|
|
978
|
+
var TRUE_PEAK_TAPS = 12;
|
|
979
|
+
var TRUE_PEAK_KERNELS = Array.from({ length: TRUE_PEAK_OVERSAMPLE - 1 }, (_, phase) => {
|
|
980
|
+
const offset = (phase + 1) / TRUE_PEAK_OVERSAMPLE;
|
|
981
|
+
return Float64Array.from({ length: 2 * TRUE_PEAK_TAPS }, (_2, tap) => {
|
|
982
|
+
const x = tap - TRUE_PEAK_TAPS + 1 - offset;
|
|
983
|
+
const sinc = x === 0 ? 1 : Math.sin(Math.PI * x) / (Math.PI * x);
|
|
984
|
+
return sinc * (0.5 + 0.5 * Math.cos(Math.PI * x / TRUE_PEAK_TAPS));
|
|
985
|
+
});
|
|
986
|
+
});
|
|
987
|
+
function truePeak(channel) {
|
|
988
|
+
let samplePeak = 0;
|
|
989
|
+
for (let i = 0; i < channel.length; i++)
|
|
990
|
+
samplePeak = Math.max(samplePeak, Math.abs(channel[i]));
|
|
991
|
+
const floor = samplePeak / 2;
|
|
992
|
+
const taps = 2 * TRUE_PEAK_TAPS;
|
|
993
|
+
let peak = samplePeak;
|
|
994
|
+
for (let n = 0; n + taps <= channel.length; n++) {
|
|
995
|
+
if (Math.abs(channel[n + TRUE_PEAK_TAPS - 1]) < floor && Math.abs(channel[n + TRUE_PEAK_TAPS]) < floor)
|
|
996
|
+
continue;
|
|
997
|
+
for (const kernel of TRUE_PEAK_KERNELS) {
|
|
998
|
+
let y = 0;
|
|
999
|
+
for (let tap = 0; tap < taps; tap++)
|
|
1000
|
+
y += channel[n + tap] * kernel[tap];
|
|
1001
|
+
if (y > peak)
|
|
1002
|
+
peak = y;
|
|
1003
|
+
else if (-y > peak)
|
|
1004
|
+
peak = -y;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
return peak;
|
|
1008
|
+
}
|
|
1009
|
+
function loudness(signal) {
|
|
1010
|
+
const [shelf, highPass] = kWeighting(signal.sampleRate);
|
|
1011
|
+
const sums = signal.channels.map((channel) => squareSums(filtered(filtered(channel, shelf), highPass)));
|
|
1012
|
+
const momentary = blockPowers(sums, signal.sampleRate, 0.4, 0.1);
|
|
1013
|
+
const audible = momentary.filter((power) => blockLoudness(power) > -70);
|
|
1014
|
+
let lufsIntegrated = null;
|
|
1015
|
+
if (audible.length) {
|
|
1016
|
+
const relative = blockLoudness(audible.reduce((a, b) => a + b, 0) / audible.length) - 10;
|
|
1017
|
+
const gated = audible.filter((power) => blockLoudness(power) > relative);
|
|
1018
|
+
if (gated.length)
|
|
1019
|
+
lufsIntegrated = Math.round(blockLoudness(gated.reduce((a, b) => a + b, 0) / gated.length) * 10) / 10;
|
|
1020
|
+
}
|
|
1021
|
+
const shortTerm = blockPowers(sums, signal.sampleRate, 3, 0.1).map(blockLoudness).filter(Number.isFinite);
|
|
1022
|
+
let samplePeak = 0;
|
|
1023
|
+
let count = 0;
|
|
1024
|
+
let firstAt = null;
|
|
1025
|
+
for (const channel of signal.channels)
|
|
1026
|
+
for (let i = 0; i < channel.length; i++) {
|
|
1027
|
+
const magnitude = Math.abs(channel[i]);
|
|
1028
|
+
samplePeak = Math.max(samplePeak, magnitude);
|
|
1029
|
+
if (magnitude >= 0.999) {
|
|
1030
|
+
count++;
|
|
1031
|
+
const t = i / signal.sampleRate;
|
|
1032
|
+
if (firstAt === null || t < firstAt)
|
|
1033
|
+
firstAt = round3(t);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
return {
|
|
1037
|
+
lufsIntegrated,
|
|
1038
|
+
lufsShortTermMax: shortTerm.length ? Math.round(Math.max(...shortTerm) * 10) / 10 : null,
|
|
1039
|
+
samplePeakDb: Math.round(toDb(samplePeak) * 10) / 10,
|
|
1040
|
+
truePeakDb: Math.round(toDb(Math.max(0, ...signal.channels.map(truePeak))) * 10) / 10,
|
|
1041
|
+
clipping: { count, firstAt }
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
function silence(signal, options = {}) {
|
|
1045
|
+
const { thresholdDb = -50, minMs = 200, hopMs = 5 } = options;
|
|
1046
|
+
const db = rmsFrames(energyMix(signal), signal.sampleRate, hopMs, hopMs * 2).map(toDb);
|
|
1047
|
+
const hopSeconds = hopSamples(signal.sampleRate, hopMs) / signal.sampleRate;
|
|
1048
|
+
const spans = [];
|
|
1049
|
+
let from = null;
|
|
1050
|
+
for (let i = 0; i <= db.length; i++) {
|
|
1051
|
+
const quiet = i < db.length && db[i] < thresholdDb;
|
|
1052
|
+
if (quiet && from === null)
|
|
1053
|
+
from = i;
|
|
1054
|
+
if (!quiet && from !== null) {
|
|
1055
|
+
if ((i - from) * hopSeconds * 1e3 >= minMs)
|
|
1056
|
+
spans.push({
|
|
1057
|
+
start: round3(from * hopSeconds),
|
|
1058
|
+
end: round3(Math.min(i * hopSeconds, signalDuration(signal)))
|
|
1059
|
+
});
|
|
1060
|
+
from = null;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
return spans;
|
|
1064
|
+
}
|
|
1065
|
+
function periodicity(curve, hopMs) {
|
|
1066
|
+
const mean2 = curve.reduce((a, b) => a + b, 0) / (curve.length || 1);
|
|
1067
|
+
const centred = curve.map((v) => v - mean2);
|
|
1068
|
+
const energy = centred.reduce((a, v) => a + v * v, 0);
|
|
1069
|
+
if (!energy)
|
|
1070
|
+
return 0;
|
|
1071
|
+
let best = 0;
|
|
1072
|
+
for (let lag = Math.round(300 / hopMs); lag <= Math.round(1500 / hopMs) && lag < centred.length; lag++) {
|
|
1073
|
+
let sum = 0;
|
|
1074
|
+
for (let i = lag; i < centred.length; i++)
|
|
1075
|
+
sum += centred[i] * centred[i - lag];
|
|
1076
|
+
best = Math.max(best, sum / energy);
|
|
1077
|
+
}
|
|
1078
|
+
return best;
|
|
1079
|
+
}
|
|
1080
|
+
function syllableModulation(signal) {
|
|
1081
|
+
const hopMs = 10;
|
|
1082
|
+
const rms = rmsFrames(energyMix(signal), signal.sampleRate, hopMs, 25);
|
|
1083
|
+
const size = 2 ** Math.floor(Math.log2(Math.min(rms.length, 1024)));
|
|
1084
|
+
if (size < 64)
|
|
1085
|
+
return 0;
|
|
1086
|
+
const re = Float64Array.from(rms.slice(0, size));
|
|
1087
|
+
const mean2 = re.reduce((a, b) => a + b, 0) / size;
|
|
1088
|
+
for (let i = 0; i < size; i++)
|
|
1089
|
+
re[i] = re[i] - mean2;
|
|
1090
|
+
const im = new Float64Array(size);
|
|
1091
|
+
fft(re, im);
|
|
1092
|
+
const rate = 1e3 / hopMs;
|
|
1093
|
+
let inBand = 0;
|
|
1094
|
+
let total = 0;
|
|
1095
|
+
for (let k = 1; k < size / 2; k++) {
|
|
1096
|
+
const power = re[k] * re[k] + im[k] * im[k];
|
|
1097
|
+
const hz = k * rate / size;
|
|
1098
|
+
total += power;
|
|
1099
|
+
if (hz >= 3 && hz <= 8)
|
|
1100
|
+
inBand += power;
|
|
1101
|
+
}
|
|
1102
|
+
return total ? inBand / total : 0;
|
|
1103
|
+
}
|
|
1104
|
+
function sustainShare(signal) {
|
|
1105
|
+
const levels = rmsFrames(energyMix(signal), signal.sampleRate, 50, 100).map(toDb);
|
|
1106
|
+
if (!levels.length)
|
|
1107
|
+
return 0;
|
|
1108
|
+
const loud = [...levels].sort((a, b) => a - b)[Math.floor((levels.length - 1) * 0.95)];
|
|
1109
|
+
return levels.filter((level) => level > loud - 20).length / levels.length;
|
|
1110
|
+
}
|
|
1111
|
+
function kindFeatures(signal) {
|
|
1112
|
+
return {
|
|
1113
|
+
duration: round3(signalDuration(signal)),
|
|
1114
|
+
regularity: round3(periodicity(noveltyCurve(signal, "spectral-flux", 10), 10)),
|
|
1115
|
+
syllables: round3(syllableModulation(signal)),
|
|
1116
|
+
sustain: round3(sustainShare(signal))
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
function classify(signal) {
|
|
1120
|
+
const features = kindFeatures(signal);
|
|
1121
|
+
const { duration, regularity, syllables, sustain } = features;
|
|
1122
|
+
const speechLike = syllables >= 0.35 && regularity < 0.35;
|
|
1123
|
+
if (duration >= 8) {
|
|
1124
|
+
if (sustain < 0.4)
|
|
1125
|
+
return { value: "sfx", confidence: 0.6, features };
|
|
1126
|
+
if (speechLike)
|
|
1127
|
+
return { value: "voice", confidence: round3(Math.min(0.9, 0.4 + syllables)), features };
|
|
1128
|
+
return { value: "music", confidence: round3(Math.min(0.9, 0.5 + regularity)), features };
|
|
1129
|
+
}
|
|
1130
|
+
if (duration >= 1.5 && syllables >= 0.17 && regularity < 0.3 && sustain >= 0.7)
|
|
1131
|
+
return { value: "voice", confidence: speechLike ? 0.8 : 0.6, features };
|
|
1132
|
+
return { value: "sfx", confidence: duration <= 2 ? 0.85 : 0.65, features };
|
|
1133
|
+
}
|
|
1134
|
+
var SIGNAL_MEASURES = {
|
|
1135
|
+
beats: (input) => rhythm(input).beats,
|
|
1136
|
+
downbeats: (input) => rhythm(input).downbeats,
|
|
1137
|
+
sections: (input, options) => sections(input, options),
|
|
1138
|
+
envelope: (input, options) => envelope(input, options),
|
|
1139
|
+
onsets: (input, options) => onsets(input, options),
|
|
1140
|
+
moments: (input, options) => moments(input, options),
|
|
1141
|
+
loudness: (input) => loudness(input),
|
|
1142
|
+
silence: (input, options) => silence(input, options),
|
|
1143
|
+
kind: (input) => classify(input)
|
|
1144
|
+
};
|
|
1145
|
+
var signalProvider = {
|
|
1146
|
+
id: "signal",
|
|
1147
|
+
local: true,
|
|
1148
|
+
signal: true,
|
|
1149
|
+
capabilities: [
|
|
1150
|
+
"envelope",
|
|
1151
|
+
"onsets",
|
|
1152
|
+
"moments",
|
|
1153
|
+
"loudness",
|
|
1154
|
+
"silence",
|
|
1155
|
+
"kind",
|
|
1156
|
+
"beats",
|
|
1157
|
+
"downbeats",
|
|
1158
|
+
"sections"
|
|
1159
|
+
],
|
|
1160
|
+
async analyze(capability, input, options) {
|
|
1161
|
+
const measure = SIGNAL_MEASURES[capability];
|
|
1162
|
+
if (!measure)
|
|
1163
|
+
throw new Error(`signal processing does not measure ${capability}`);
|
|
1164
|
+
return measure(input, options);
|
|
1165
|
+
}
|
|
1166
|
+
};
|
|
1167
|
+
|
|
1168
|
+
export {
|
|
1169
|
+
medianInterval,
|
|
1170
|
+
tempoOf,
|
|
1171
|
+
bandFramesOf,
|
|
1172
|
+
rhythm,
|
|
1173
|
+
snapper,
|
|
1174
|
+
runLadder,
|
|
1175
|
+
signalDuration,
|
|
1176
|
+
energyMix,
|
|
1177
|
+
envelope,
|
|
1178
|
+
signalProvider,
|
|
1179
|
+
probeAudio,
|
|
1180
|
+
decodeAudio
|
|
1181
|
+
};
|