@clipkit/music-analysis 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for describing the origin of the Work and
141
+ reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2026 Clipkit Contributors
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # @clipkit/music-analysis
2
+
3
+ Analyze an audio file into a **beat map** — tempo, beat grid, downbeats,
4
+ transient onsets, and structural sections — so motion graphics can be synced to
5
+ music.
6
+
7
+ Think of it as an importer with the last step removed. The After Effects and
8
+ Lottie importers parse a foreign format into an IR and then *convert* that IR to
9
+ a Clipkit Source. This package parses audio into an IR — the `BeatMap` — and
10
+ **stops there**. It does not emit a Source.
11
+
12
+ ## Where it sits
13
+
14
+ ```
15
+ audio file ─▶ @clipkit/music-analysis ─▶ BeatMap ─▶ @clipkit/patterns helpers ─▶ keyframes
16
+ (analysis: churns) (stable (mapping: intent → exact
17
+ contract) times) + the AI's taste
18
+ ```
19
+
20
+ Three jobs, deliberately split:
21
+
22
+ 1. **Analysis** (this package) — produce the facts: *where are the beats?*
23
+ 2. **Mapping** (`@clipkit/patterns`) — turn intent into exact keyframes:
24
+ *punch the logo on each downbeat.*
25
+ 3. **Taste** (the AI agent, via `@clipkit/mcp-server`) — decide *which* moment
26
+ gets *which* move, reading the beat map as authoring context.
27
+
28
+ ## Not part of the protocol
29
+
30
+ The beat map is **authoring-time data**. The runtime never reads it. Consumers
31
+ bake it down to ordinary keyframes / pure expressions, so the rendered Source
32
+ stays a deterministic function of time with **no audio dependency** — same
33
+ document, same pixels, on every backend. Resist the urge to let an expression
34
+ sample an audio envelope at render time; bake to keyframes instead.
35
+
36
+ ## Two tiers of sync
37
+
38
+ - **Tier 1 — tempo-parametric (pure).** Just `bpm` + `phase` drive a pure
39
+ expression that breathes on the beat (e.g.
40
+ `1 + 0.05 * max(0, sin(TAU * (bpm/60) * (t - phase)))`). ~70% of the feel,
41
+ zero new runtime concepts.
42
+ - **Tier 2 — event-baked (precise).** `downbeats` / `onsets` / `sections` place
43
+ accents and transitions on exact musical time.
44
+
45
+ ## Status
46
+
47
+ Scaffold. The `BeatMap` contract (`src/beat-map.ts`) and the `analyzeAudio`
48
+ signature (`src/analyze.ts`) are the stable parts; the analyzer behind them is
49
+ where the experimentation happens. `analyzeAudio` currently returns an empty map
50
+ so the contract and downstream wiring can be built against a real type today.
51
+
52
+ ## Usage
53
+
54
+ ```ts
55
+ // analyzeAudio reads files (node:fs) → import it from the Node-only subpath.
56
+ import { analyzeAudio } from '@clipkit/music-analysis/node';
57
+ import type { BeatMap } from '@clipkit/music-analysis';
58
+
59
+ const map: BeatMap = await analyzeAudio('track.mp3');
60
+ // → feed to a patterns helper, or hand to an agent as authoring context
61
+
62
+ // Browser-safe helpers (beatGrid, decodeWav, analyzePcm, types) live on the
63
+ // main entry: import { beatGrid } from '@clipkit/music-analysis';
64
+ ```
@@ -0,0 +1,18 @@
1
+ import type { BeatMap } from './beat-map.js';
2
+ export interface AnalyzeOptions {
3
+ /** Decoded mono (or multi-channel) PCM. When given, `source` is only a label
4
+ * and no decoding happens. */
5
+ samples?: Float32Array;
6
+ /** Sample rate of `samples`, Hz. Required when `samples` is given. */
7
+ sampleRate?: number;
8
+ /** Beats per bar for downbeat spacing. Default 4. */
9
+ beatsPerBar?: number;
10
+ }
11
+ /**
12
+ * Analyze an audio file (or raw PCM) into a {@link BeatMap}: detected `bpm`,
13
+ * `phase`, the implied beat/downbeat grid, detected `onsets` (hits), and
14
+ * energy-based `sections`. WAV files decode directly; for other formats pass
15
+ * decoded `samples`/`sampleRate` via {@link AnalyzeOptions}.
16
+ */
17
+ export declare function analyzeAudio(source: string, options?: AnalyzeOptions): Promise<BeatMap>;
18
+ //# sourceMappingURL=analyze.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyze.d.ts","sourceRoot":"","sources":["../src/analyze.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAI7C,MAAM,WAAW,cAAc;IAC7B;mCAC+B;IAC/B,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,sEAAsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qDAAqD;IACrD,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAgBD;;;;;GAKG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,OAAO,CAAC,CAqBlB"}
@@ -0,0 +1,54 @@
1
+ // analyzeAudio — audio in, beat map out. The real analyzer (see dsp.ts):
2
+ // decode → spectral-flux onsets + autocorrelation tempo/phase + energy-novelty
3
+ // sections. Deterministic: same bytes → same map.
4
+ //
5
+ // Decoding is WAV-only here (decode-wav.ts, no deps). For other containers
6
+ // (mp3/aac/…), decode upstream and pass `samples` + `sampleRate` via options —
7
+ // the DSP is container-agnostic. Tempo detection from raw audio lives here;
8
+ // when you already know the BPM, beatGrid() is the cheaper path.
9
+ import { analyzePcm } from './dsp.js';
10
+ import { decodeWav } from './decode-wav.js';
11
+ async function loadWav(source) {
12
+ let bytes;
13
+ if (/^https?:\/\//.test(source)) {
14
+ const res = await fetch(source);
15
+ if (!res.ok)
16
+ throw new Error(`analyzeAudio: fetch ${source} → ${res.status}`);
17
+ bytes = new Uint8Array(await res.arrayBuffer());
18
+ }
19
+ else {
20
+ // Local path — Node only (authoring-time tool).
21
+ const { readFile } = await import('node:fs/promises');
22
+ bytes = new Uint8Array(await readFile(source));
23
+ }
24
+ return decodeWav(bytes);
25
+ }
26
+ /**
27
+ * Analyze an audio file (or raw PCM) into a {@link BeatMap}: detected `bpm`,
28
+ * `phase`, the implied beat/downbeat grid, detected `onsets` (hits), and
29
+ * energy-based `sections`. WAV files decode directly; for other formats pass
30
+ * decoded `samples`/`sampleRate` via {@link AnalyzeOptions}.
31
+ */
32
+ export async function analyzeAudio(source, options = {}) {
33
+ let samples;
34
+ let sampleRate;
35
+ if (options.samples) {
36
+ samples = options.samples;
37
+ sampleRate = options.sampleRate ?? 44100;
38
+ }
39
+ else {
40
+ ({ samples, sampleRate } = await loadWav(source));
41
+ }
42
+ const a = analyzePcm(samples, sampleRate, options.beatsPerBar ?? 4);
43
+ return {
44
+ source,
45
+ duration: Math.round((samples.length / sampleRate) * 1000) / 1000,
46
+ bpm: a.bpm,
47
+ phase: a.phase,
48
+ beats: a.beats,
49
+ downbeats: a.downbeats,
50
+ onsets: a.onsets,
51
+ sections: a.sections,
52
+ };
53
+ }
54
+ //# sourceMappingURL=analyze.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyze.js","sourceRoot":"","sources":["../src/analyze.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,+EAA+E;AAC/E,kDAAkD;AAClD,EAAE;AACF,2EAA2E;AAC3E,+EAA+E;AAC/E,4EAA4E;AAC5E,iEAAiE;AAGjE,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAY5C,KAAK,UAAU,OAAO,CAAC,MAAc;IACnC,IAAI,KAAiB,CAAC;IACtB,IAAI,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QAChC,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,MAAM,CAAC,CAAC;QAChC,IAAI,CAAC,GAAG,CAAC,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,uBAAuB,MAAM,MAAM,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;QAC9E,KAAK,GAAG,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,WAAW,EAAE,CAAC,CAAC;IAClD,CAAC;SAAM,CAAC;QACN,gDAAgD;QAChD,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAAC,CAAC;QACtD,KAAK,GAAG,IAAI,UAAU,CAAC,MAAM,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;IACjD,CAAC;IACD,OAAO,SAAS,CAAC,KAAK,CAAC,CAAC;AAC1B,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,MAAc,EACd,UAA0B,EAAE;IAE5B,IAAI,OAAqB,CAAC;IAC1B,IAAI,UAAkB,CAAC;IACvB,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;QACpB,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;QAC1B,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,KAAK,CAAC;IAC3C,CAAC;SAAM,CAAC;QACN,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;IACpD,CAAC;IAED,MAAM,CAAC,GAAG,UAAU,CAAC,OAAO,EAAE,UAAU,EAAE,OAAO,CAAC,WAAW,IAAI,CAAC,CAAC,CAAC;IACpE,OAAO;QACL,MAAM;QACN,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,MAAM,GAAG,UAAU,CAAC,GAAG,IAAI,CAAC,GAAG,IAAI;QACjE,GAAG,EAAE,CAAC,CAAC,GAAG;QACV,KAAK,EAAE,CAAC,CAAC,KAAK;QACd,KAAK,EAAE,CAAC,CAAC,KAAK;QACd,SAAS,EAAE,CAAC,CAAC,SAAS;QACtB,MAAM,EAAE,CAAC,CAAC,MAAM;QAChB,QAAQ,EAAE,CAAC,CAAC,QAAQ;KACrB,CAAC;AACJ,CAAC"}
@@ -0,0 +1,66 @@
1
+ /** A musical event detected in the audio, at a point in time. */
2
+ export interface Marker {
3
+ /** Time of the event, in seconds from the start of the audio. */
4
+ time: number;
5
+ /**
6
+ * Relative salience of the event, 0..1. For onsets this is transient
7
+ * strength; for beats/downbeats it is detection confidence. Authoring uses
8
+ * it to scale accent magnitude — strong hits get bigger moves.
9
+ */
10
+ strength: number;
11
+ }
12
+ /** Frequency band an onset's energy is concentrated in. */
13
+ export type Band = 'low' | 'mid' | 'high';
14
+ /** A transient hit — a drum, a stab, a vocal onset. The "hit points" that
15
+ * discrete accents (punch, flash, shake) sync to. */
16
+ export interface Onset extends Marker {
17
+ /** Dominant band of the transient — lets authoring route kicks vs. hats to
18
+ * different motions (bass → scale pulse, highs → shimmer). */
19
+ band: Band;
20
+ }
21
+ /** A structural region of the track (intro / build / drop / verse / …). The
22
+ * unit at which scene changes and major reveals belong. */
23
+ export interface Section {
24
+ /** Start time, seconds. */
25
+ start: number;
26
+ /** End time, seconds (== next section's start, or track end). */
27
+ end: number;
28
+ /**
29
+ * Coarse label when the analyzer can infer one. Free-form on purpose — the
30
+ * analyzer may only know boundaries, not names — so authoring should treat an
31
+ * unknown label as "a boundary worth cutting on", not rely on the string.
32
+ */
33
+ label?: string;
34
+ /** Mean energy of the region, 0..1. The jump between adjacent sections is the
35
+ * "drop" signal: a low region followed by a high one. */
36
+ energy: number;
37
+ }
38
+ /**
39
+ * The full analysis of one audio file. Start minimal — tempo + the event
40
+ * tracks below cover Tier-1 tempo-pulse and Tier-2 accents. Add sampled band
41
+ * envelopes (continuous drivers) only once a consumer needs them; an IR field
42
+ * nothing reads is just maintenance.
43
+ */
44
+ export interface BeatMap {
45
+ /** Source audio URL or path this map was computed from. */
46
+ source: string;
47
+ /** Total duration of the audio, seconds. */
48
+ duration: number;
49
+ /** Estimated global tempo, beats per minute. */
50
+ bpm: number;
51
+ /**
52
+ * Time of the first beat (beat 0), seconds. With `bpm` this defines the whole
53
+ * grid: beat k ≈ phase + k·60/bpm. Tier-1 tempo-pulse needs only these two
54
+ * numbers — a pure expression can breathe on the beat from them alone.
55
+ */
56
+ phase: number;
57
+ /** Every detected beat (the full grid, including downbeats). */
58
+ beats: Marker[];
59
+ /** Beats that fall on bar boundaries — where the BIG moves go. */
60
+ downbeats: Marker[];
61
+ /** Transient hits, for discrete accents. */
62
+ onsets: Onset[];
63
+ /** Structural regions, for scene changes / major reveals. */
64
+ sections: Section[];
65
+ }
66
+ //# sourceMappingURL=beat-map.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"beat-map.d.ts","sourceRoot":"","sources":["../src/beat-map.ts"],"names":[],"mappings":"AAmBA,iEAAiE;AACjE,MAAM,WAAW,MAAM;IACrB,iEAAiE;IACjE,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,2DAA2D;AAC3D,MAAM,MAAM,IAAI,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC;AAE1C;sDACsD;AACtD,MAAM,WAAW,KAAM,SAAQ,MAAM;IACnC;mEAC+D;IAC/D,IAAI,EAAE,IAAI,CAAC;CACZ;AAED;4DAC4D;AAC5D,MAAM,WAAW,OAAO;IACtB,2BAA2B;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,iEAAiE;IACjE,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;8DAC0D;IAC1D,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;GAKG;AACH,MAAM,WAAW,OAAO;IACtB,2DAA2D;IAC3D,MAAM,EAAE,MAAM,CAAC;IACf,4CAA4C;IAC5C,QAAQ,EAAE,MAAM,CAAC;IAEjB,gDAAgD;IAChD,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd,gEAAgE;IAChE,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,kEAAkE;IAClE,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,4CAA4C;IAC5C,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,6DAA6D;IAC7D,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB"}
@@ -0,0 +1,20 @@
1
+ // The beat map — the intermediate artifact this package produces and the
2
+ // stable seam between audio analysis and motion authoring.
3
+ //
4
+ // Analysis (which DSP/ML approach, JS vs. a server-side analyzer) will churn;
5
+ // the motion side (AI agents, @clipkit/patterns helpers) reads ONLY this shape.
6
+ // Keep the two decoupled the way the importers keep `ir-types` between their
7
+ // parser and converter, so the analyzer can be rewritten without touching the
8
+ // authoring layer.
9
+ //
10
+ // This is AUTHORING-TIME data. It is deliberately NOT part of the Clipkit
11
+ // Protocol: the runtime never reads a beat map. A patterns helper consumes it
12
+ // and emits ordinary keyframes/expressions, so the rendered Source stays a pure,
13
+ // deterministic function of time with no audio dependency.
14
+ //
15
+ // ── Units ──────────────────────────────────────────────────────────────────
16
+ // ALL times are in SECONDS, matching the protocol's element `time` and keyframe
17
+ // `time`. (The AE importer's IR is in frames because AE is; here we line up with
18
+ // the Source directly, so a marker time drops straight into a keyframe.)
19
+ export {};
20
+ //# sourceMappingURL=beat-map.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"beat-map.js","sourceRoot":"","sources":["../src/beat-map.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,2DAA2D;AAC3D,EAAE;AACF,8EAA8E;AAC9E,gFAAgF;AAChF,6EAA6E;AAC7E,8EAA8E;AAC9E,mBAAmB;AACnB,EAAE;AACF,0EAA0E;AAC1E,8EAA8E;AAC9E,iFAAiF;AACjF,2DAA2D;AAC3D,EAAE;AACF,8EAA8E;AAC9E,gFAAgF;AAChF,iFAAiF;AACjF,yEAAyE"}
@@ -0,0 +1,6 @@
1
+ export interface DecodedAudio {
2
+ samples: Float32Array;
3
+ sampleRate: number;
4
+ }
5
+ export declare function decodeWav(bytes: Uint8Array): DecodedAudio;
6
+ //# sourceMappingURL=decode-wav.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decode-wav.d.ts","sourceRoot":"","sources":["../src/decode-wav.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,YAAY,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;CACpB;AAQD,wBAAgB,SAAS,CAAC,KAAK,EAAE,UAAU,GAAG,YAAY,CA0DzD"}
@@ -0,0 +1,72 @@
1
+ // Minimal WAV decoder → mono Float32 samples. No dependencies; handles PCM
2
+ // (8/16/24/32-bit) and 32-bit float, any channel count (downmixed to mono).
3
+ // Other containers (mp3/aac/ogg) are out of scope here — decode them upstream
4
+ // and hand `analyzeAudio` the raw samples via AnalyzeOptions.
5
+ function str(b, o, n) {
6
+ let s = '';
7
+ for (let i = 0; i < n; i++)
8
+ s += String.fromCharCode(b[o + i]);
9
+ return s;
10
+ }
11
+ export function decodeWav(bytes) {
12
+ const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
13
+ if (str(bytes, 0, 4) !== 'RIFF' || str(bytes, 8, 4) !== 'WAVE') {
14
+ throw new Error('decodeWav: not a RIFF/WAVE file');
15
+ }
16
+ let fmt = null;
17
+ let dataOff = -1;
18
+ let dataLen = 0;
19
+ // Walk chunks.
20
+ let p = 12;
21
+ while (p + 8 <= bytes.length) {
22
+ const id = str(bytes, p, 4);
23
+ const size = dv.getUint32(p + 4, true);
24
+ const body = p + 8;
25
+ if (id === 'fmt ') {
26
+ fmt = {
27
+ audioFormat: dv.getUint16(body, true),
28
+ channels: dv.getUint16(body + 2, true),
29
+ sampleRate: dv.getUint32(body + 4, true),
30
+ bits: dv.getUint16(body + 14, true),
31
+ };
32
+ }
33
+ else if (id === 'data') {
34
+ dataOff = body;
35
+ dataLen = size;
36
+ }
37
+ p = body + size + (size & 1); // chunks are word-aligned
38
+ }
39
+ if (!fmt || dataOff < 0)
40
+ throw new Error('decodeWav: missing fmt/data chunk');
41
+ const { channels, sampleRate, bits, audioFormat } = fmt;
42
+ const bytesPerSample = bits >> 3;
43
+ const frameCount = Math.floor(dataLen / (bytesPerSample * channels));
44
+ const out = new Float32Array(frameCount);
45
+ const isFloat = audioFormat === 3;
46
+ const read = (off) => {
47
+ if (isFloat)
48
+ return bits === 64 ? dv.getFloat64(off, true) : dv.getFloat32(off, true);
49
+ switch (bits) {
50
+ case 8: return (dv.getUint8(off) - 128) / 128; // 8-bit PCM is unsigned
51
+ case 16: return dv.getInt16(off, true) / 32768;
52
+ case 24: {
53
+ const b0 = dv.getUint8(off), b1 = dv.getUint8(off + 1), b2 = dv.getUint8(off + 2);
54
+ let v = b0 | (b1 << 8) | (b2 << 16);
55
+ if (v & 0x800000)
56
+ v -= 0x1000000;
57
+ return v / 8388608;
58
+ }
59
+ case 32: return dv.getInt32(off, true) / 2147483648;
60
+ default: throw new Error(`decodeWav: unsupported bit depth ${bits}`);
61
+ }
62
+ };
63
+ for (let i = 0; i < frameCount; i++) {
64
+ let acc = 0;
65
+ const base = dataOff + i * bytesPerSample * channels;
66
+ for (let c = 0; c < channels; c++)
67
+ acc += read(base + c * bytesPerSample);
68
+ out[i] = acc / channels; // downmix
69
+ }
70
+ return { samples: out, sampleRate };
71
+ }
72
+ //# sourceMappingURL=decode-wav.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decode-wav.js","sourceRoot":"","sources":["../src/decode-wav.ts"],"names":[],"mappings":"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"}
package/dist/dsp.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import type { Marker, Onset, Section } from './beat-map.js';
2
+ export interface PcmAnalysis {
3
+ bpm: number;
4
+ phase: number;
5
+ beats: Marker[];
6
+ downbeats: Marker[];
7
+ onsets: Onset[];
8
+ sections: Section[];
9
+ }
10
+ /** Full analysis of mono PCM. Beats/downbeats are the regular grid implied by
11
+ * the detected tempo+phase; onsets are the actual detected transients. */
12
+ export declare function analyzePcm(samples: Float32Array, sr: number, beatsPerBar?: number): PcmAnalysis;
13
+ //# sourceMappingURL=dsp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dsp.d.ts","sourceRoot":"","sources":["../src/dsp.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAQ,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAgNlE,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB;AAED;2EAC2E;AAC3E,wBAAgB,UAAU,CAAC,OAAO,EAAE,YAAY,EAAE,EAAE,EAAE,MAAM,EAAE,WAAW,SAAI,GAAG,WAAW,CA8B1F"}
package/dist/dsp.js ADDED
@@ -0,0 +1,295 @@
1
+ // The analysis pipeline: PCM samples → tempo, phase, onsets, sections.
2
+ // Deterministic, dependency-free, built on the FFT in fft.ts.
3
+ //
4
+ // STFT → spectral flux ─┬─ peak-pick → onsets (the actual hits, w/ band)
5
+ // └─ autocorrelation → tempo + phase → beat grid
6
+ // frame RMS → novelty ──→ section boundaries (structure)
7
+ //
8
+ // Tuned for music, verified against click tracks of known tempo. Section
9
+ // detection here is ENERGY-based (a build/drop reads as an energy jump); a
10
+ // timbral self-similarity pass is the future refinement.
11
+ import { magnitudeSpectrum } from './fft.js';
12
+ const FRAME = 2048;
13
+ const HOP = 512;
14
+ function hann(n) {
15
+ const w = new Float64Array(n);
16
+ for (let i = 0; i < n; i++)
17
+ w[i] = 0.5 - 0.5 * Math.cos((2 * Math.PI * i) / (n - 1));
18
+ return w;
19
+ }
20
+ /** STFT-derived per-frame features: spectral flux (overall + 3 bands) and RMS. */
21
+ function analyzeFrames(samples, sr) {
22
+ const win = hann(FRAME);
23
+ const nFrames = Math.max(1, Math.floor((samples.length - FRAME) / HOP) + 1);
24
+ const flux = new Float64Array(nFrames);
25
+ const low = new Float64Array(nFrames);
26
+ const mid = new Float64Array(nFrames);
27
+ const high = new Float64Array(nFrames);
28
+ const rms = new Float64Array(nFrames);
29
+ const half = FRAME >> 1;
30
+ // band edges in bins (bin width = sr / FRAME)
31
+ const binHz = sr / FRAME;
32
+ const loEdge = Math.round(200 / binHz);
33
+ const hiEdge = Math.round(5000 / binHz);
34
+ let prev = new Float64Array(half);
35
+ const frame = new Float64Array(FRAME);
36
+ for (let f = 0; f < nFrames; f++) {
37
+ const start = f * HOP;
38
+ let energy = 0;
39
+ for (let i = 0; i < FRAME; i++) {
40
+ const s = samples[start + i] ?? 0;
41
+ energy += s * s;
42
+ frame[i] = s * win[i];
43
+ }
44
+ rms[f] = Math.sqrt(energy / FRAME);
45
+ const mag = magnitudeSpectrum(frame);
46
+ let fl = 0, fLo = 0, fMid = 0, fHi = 0;
47
+ for (let b = 1; b < half; b++) {
48
+ const m = Math.log1p(mag[b]); // log-compress for musical dynamics
49
+ const d = m - prev[b];
50
+ if (d > 0) {
51
+ fl += d;
52
+ if (b < loEdge)
53
+ fLo += d;
54
+ else if (b < hiEdge)
55
+ fMid += d;
56
+ else
57
+ fHi += d;
58
+ }
59
+ prev[b] = m;
60
+ }
61
+ flux[f] = fl;
62
+ low[f] = fLo;
63
+ mid[f] = fMid;
64
+ high[f] = fHi;
65
+ }
66
+ // normalize flux to [0,1]
67
+ let mx = 0;
68
+ for (let f = 0; f < nFrames; f++)
69
+ if (flux[f] > mx)
70
+ mx = flux[f];
71
+ if (mx > 0)
72
+ for (let f = 0; f < nFrames; f++)
73
+ flux[f] /= mx;
74
+ return { flux, bandFlux: [low, mid, high], rms, dt: HOP / sr };
75
+ }
76
+ /** Peak-pick onsets from the flux envelope: local maxima above an adaptive
77
+ * threshold, with a minimum inter-onset gap. */
78
+ function pickOnsets(fr) {
79
+ const { flux, bandFlux, dt } = fr;
80
+ const n = flux.length;
81
+ const w = Math.max(2, Math.round(0.05 / dt)); // local window ~50ms
82
+ const meanW = Math.max(4, Math.round(0.1 / dt));
83
+ const minGap = 0.07; // seconds
84
+ const out = [];
85
+ let lastT = -Infinity;
86
+ for (let i = 1; i < n - 1; i++) {
87
+ const v = flux[i];
88
+ if (v < 0.06)
89
+ continue;
90
+ // local max?
91
+ let isMax = true;
92
+ for (let k = -w; k <= w; k++)
93
+ if (flux[i + k] !== undefined && flux[i + k] > v) {
94
+ isMax = false;
95
+ break;
96
+ }
97
+ if (!isMax)
98
+ continue;
99
+ // adaptive threshold: above the local mean by a margin
100
+ let sum = 0, cnt = 0;
101
+ for (let k = -meanW; k <= meanW; k++) {
102
+ const x = flux[i + k];
103
+ if (x !== undefined) {
104
+ sum += x;
105
+ cnt++;
106
+ }
107
+ }
108
+ if (v < (sum / cnt) * 1.4 + 0.03)
109
+ continue;
110
+ const t = i * dt;
111
+ if (t - lastT < minGap)
112
+ continue;
113
+ lastT = t;
114
+ // dominant band at this frame
115
+ const lo = bandFlux[0][i], md = bandFlux[1][i], hi = bandFlux[2][i];
116
+ const band = lo >= md && lo >= hi ? 'low' : hi >= md ? 'high' : 'mid';
117
+ out.push({ time: t, strength: Math.min(1, v), band });
118
+ }
119
+ return out;
120
+ }
121
+ /** Autocorrelation tempo over 60–180 BPM, picking the strongest lag. */
122
+ function detectTempo(flux, dt) {
123
+ const n = flux.length;
124
+ // de-mean
125
+ let mean = 0;
126
+ for (let i = 0; i < n; i++)
127
+ mean += flux[i];
128
+ mean /= n;
129
+ const env = new Float64Array(n);
130
+ for (let i = 0; i < n; i++)
131
+ env[i] = flux[i] - mean;
132
+ const lagMin = Math.max(1, Math.round((60 / 200) / dt));
133
+ const lagMax = Math.round((60 / 60) / dt);
134
+ const acf = new Float64Array(lagMax + 1);
135
+ let bestLag = lagMin;
136
+ let bestScore = -Infinity;
137
+ for (let lag = lagMin; lag <= lagMax; lag++) {
138
+ let s = 0;
139
+ for (let i = 0; i + lag < n; i++)
140
+ s += env[i] * env[i + lag];
141
+ acf[lag] = s;
142
+ // Perceptual tempo prior: a log-normal centred on 120 BPM nudges the octave
143
+ // ambiguity toward the tactus humans tap.
144
+ const bpm = 60 / (lag * dt);
145
+ const score = s * Math.exp(-0.5 * (Math.log2(bpm / 120) / 0.8) ** 2);
146
+ if (score > bestScore) {
147
+ bestScore = score;
148
+ bestLag = lag;
149
+ }
150
+ }
151
+ // Octave-doubling: a four-on-the-floor track correlates strongly at the
152
+ // half-bar (e.g. 60) because the backbeat repeats there — but if the
153
+ // double-tempo pulse (the kick, 120) is well-supported, that's the real beat.
154
+ for (;;) {
155
+ const half = Math.round(bestLag / 2);
156
+ const bpmD = 60 / (half * dt);
157
+ if (half < lagMin || bpmD > 185 || acf[half] < 0.5 * acf[bestLag])
158
+ break;
159
+ bestLag = half;
160
+ }
161
+ return Math.round((60 / (bestLag * dt)) * 10) / 10;
162
+ }
163
+ /** Best phase (time of beat 0) for a known tempo: the grid offset that lands on
164
+ * the most onset-envelope energy. */
165
+ function detectPhase(flux, bpm, dt) {
166
+ const periodF = 60 / bpm / dt; // frames per beat (float)
167
+ const steps = Math.max(1, Math.round(periodF));
168
+ let bestOff = 0;
169
+ let bestScore = -Infinity;
170
+ for (let o = 0; o < steps; o++) {
171
+ let s = 0;
172
+ for (let k = 0; o + k * periodF < flux.length; k++)
173
+ s += flux[Math.round(o + k * periodF)];
174
+ if (s > bestScore) {
175
+ bestScore = s;
176
+ bestOff = o;
177
+ }
178
+ }
179
+ return Math.round(bestOff * dt * 1000) / 1000;
180
+ }
181
+ /** Energy-novelty section boundaries: sustained RMS changes split the track. */
182
+ function detectSections(rms, dt, duration) {
183
+ const n = rms.length;
184
+ // smooth RMS over ~0.4s
185
+ const sw = Math.max(1, Math.round(0.4 / dt));
186
+ const sm = new Float64Array(n);
187
+ for (let i = 0; i < n; i++) {
188
+ let s = 0, c = 0;
189
+ for (let k = -sw; k <= sw; k++) {
190
+ const x = rms[i + k];
191
+ if (x !== undefined) {
192
+ s += x;
193
+ c++;
194
+ }
195
+ }
196
+ sm[i] = s / c;
197
+ }
198
+ // novelty = |future mean − past mean| over a ~1.5s half-window
199
+ const hw = Math.max(2, Math.round(1.5 / dt));
200
+ const nov = new Float64Array(n);
201
+ for (let i = 0; i < n; i++) {
202
+ let pa = 0, pc = 0, fa = 0, fc = 0;
203
+ for (let k = 1; k <= hw; k++) {
204
+ if (sm[i - k] !== undefined) {
205
+ pa += sm[i - k];
206
+ pc++;
207
+ }
208
+ if (sm[i + k] !== undefined) {
209
+ fa += sm[i + k];
210
+ fc++;
211
+ }
212
+ }
213
+ nov[i] = pc && fc ? Math.abs(fa / fc - pa / pc) : 0;
214
+ }
215
+ // peak-pick boundaries: local maxima, min section length, relative threshold
216
+ let mx = 0;
217
+ for (let i = 0; i < n; i++)
218
+ if (nov[i] > mx)
219
+ mx = nov[i];
220
+ const minLenF = Math.round(4 / dt); // ≥ 4s sections
221
+ const boundaries = [0];
222
+ if (mx > 0) {
223
+ let last = -minLenF;
224
+ for (let i = hw; i < n - hw; i++) {
225
+ const v = nov[i];
226
+ if (v < mx * 0.35)
227
+ continue;
228
+ let isMax = true;
229
+ for (let k = -hw; k <= hw; k++)
230
+ if (nov[i + k] !== undefined && nov[i + k] > v) {
231
+ isMax = false;
232
+ break;
233
+ }
234
+ if (isMax && i - last >= minLenF) {
235
+ boundaries.push(i);
236
+ last = i;
237
+ }
238
+ }
239
+ }
240
+ // build sections, energy normalized to the loudest section
241
+ const segs = [];
242
+ for (let b = 0; b < boundaries.length; b++) {
243
+ const s = boundaries[b];
244
+ const e = b + 1 < boundaries.length ? boundaries[b + 1] : n;
245
+ let sum = 0;
246
+ for (let i = s; i < e; i++)
247
+ sum += sm[i];
248
+ segs.push({ start: s * dt, end: (b + 1 < boundaries.length ? e * dt : duration), energy: sum / Math.max(1, e - s) });
249
+ }
250
+ let emax = 0;
251
+ for (const g of segs)
252
+ if (g.energy > emax)
253
+ emax = g.energy;
254
+ return segs.map((g) => {
255
+ const energy = emax > 0 ? Math.round((g.energy / emax) * 1000) / 1000 : 0;
256
+ const label = energy < 0.34 ? 'low' : energy < 0.7 ? 'mid' : 'high';
257
+ return { start: Math.round(g.start * 1000) / 1000, end: Math.round(g.end * 1000) / 1000, label, energy };
258
+ });
259
+ }
260
+ /** Full analysis of mono PCM. Beats/downbeats are the regular grid implied by
261
+ * the detected tempo+phase; onsets are the actual detected transients. */
262
+ export function analyzePcm(samples, sr, beatsPerBar = 4) {
263
+ const duration = samples.length / sr;
264
+ const fr = analyzeFrames(samples, sr);
265
+ const onsets = pickOnsets(fr);
266
+ // Tempo/phase run on an ENERGY-flux envelope (half-wave-rectified RMS diff),
267
+ // not the spectral flux: spectral flux over-weights broadband hits (claps,
268
+ // snares) which biases toward the half-bar, while the energy envelope tracks
269
+ // the kick pulse — the tactus. (Spectral flux still drives onset timing.)
270
+ const tempoEnv = new Float64Array(fr.rms.length);
271
+ for (let i = 1; i < fr.rms.length; i++) {
272
+ const d = fr.rms[i] - fr.rms[i - 1];
273
+ tempoEnv[i] = d > 0 ? d : 0;
274
+ }
275
+ const bpm = detectTempo(tempoEnv, fr.dt);
276
+ const phase = detectPhase(tempoEnv, bpm, fr.dt);
277
+ const interval = 60 / bpm;
278
+ const beats = [];
279
+ const downbeats = [];
280
+ for (let k = 0;; k++) {
281
+ const t = phase + k * interval;
282
+ if (t > duration)
283
+ break;
284
+ if (t < 0)
285
+ continue;
286
+ const isDown = k % beatsPerBar === 0;
287
+ const m = { time: Math.round(t * 1000) / 1000, strength: isDown ? 1 : 0.7 };
288
+ beats.push(m);
289
+ if (isDown)
290
+ downbeats.push(m);
291
+ }
292
+ const sections = detectSections(fr.rms, fr.dt, duration);
293
+ return { bpm, phase, beats, downbeats, onsets, sections };
294
+ }
295
+ //# sourceMappingURL=dsp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dsp.js","sourceRoot":"","sources":["../src/dsp.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,8DAA8D;AAC9D,EAAE;AACF,2EAA2E;AAC3E,yEAAyE;AACzE,4DAA4D;AAC5D,EAAE;AACF,yEAAyE;AACzE,2EAA2E;AAC3E,yDAAyD;AAEzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAG7C,MAAM,KAAK,GAAG,IAAI,CAAC;AACnB,MAAM,GAAG,GAAG,GAAG,CAAC;AAShB,SAAS,IAAI,CAAC,CAAS;IACrB,MAAM,CAAC,GAAG,IAAI,YAAY,CAAC,CAAC,CAAC,CAAC;IAC9B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE;QAAE,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,GAAG,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IACrF,OAAO,CAAC,CAAC;AACX,CAAC;AAED,kFAAkF;AAClF,SAAS,aAAa,CAAC,OAAqB,EAAE,EAAU;IACtD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;IACxB,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,MAAM,GAAG,KAAK,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;IAC5E,MAAM,IAAI,GAAG,IAAI,YAAY,CAAC,OAAO,CAAC,CAAC;IACvC,MAAM,GAAG,GAAG,IAAI,YAAY,CAAC,OAAO,CAAC,CAAC;IACtC,MAAM,GAAG,GAAG,IAAI,YAAY,CAAC,OAAO,CAAC,CAAC;IACtC,MAAM,IAAI,GAAG,IAAI,YAAY,CAAC,OAAO,CAAC,CAAC;IACvC,MAAM,GAAG,GAAG,IAAI,YAAY,CAAC,OAAO,CAAC,CAAC;IAEtC,MAAM,IAAI,GAAG,KAAK,IAAI,CAAC,CAAC;IACxB,8CAA8C;IAC9C,MAAM,KAAK,GAAG,EAAE,GAAG,KAAK,CAAC;IACzB,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,KAAK,CAAC,CAAC;IACvC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC;IAExC,IAAI,IAAI,GAAG,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC;IAClC,MAAM,KAAK,GAAG,IAAI,YAAY,CAAC,KAAK,CAAC,CAAC;IACtC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,EAAE,CAAC,EAAE,EAAE,CAAC;QACjC,MAAM,KAAK,GAAG,CAAC,GAAG,GAAG,CAAC;QACtB,IAAI,MAAM,GAAG,CAAC,CAAC;QACf,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;YAC/B,MAAM,CAAC,GAAG,OAAO,CAAC,KAAK,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC;YAClC,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC;YAChB,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,CAAE,CAAC;QACzB,CAAC;QACD,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,CAAC;QACnC,MAAM,GAAG,GAAG,iBAAiB,CAAC,KAAK,CAAC,CAAC;QACrC,IAAI,EAAE,GAAG,CAAC,EAAE,GAAG,GAAG,CAAC,EAAE,IAAI,GAAG,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC;QACvC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC;YAC9B,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,oCAAoC;YACnE,MAAM,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC,CAAE,CAAC;YACvB,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;gBACV,EAAE,IAAI,CAAC,CAAC;gBACR,IAAI,CAAC,GAAG,MAAM;oBAAE,GAAG,IAAI,CAAC,CAAC;qBACpB,IAAI,CAAC,GAAG,MAAM;oBAAE,IAAI,IAAI,CAAC,CAAC;;oBAC1B,GAAG,IAAI,CAAC,CAAC;YAChB,CAAC;YACD,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QACd,CAAC;QACD,IAAI,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;QACb,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC;QAAC,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;QAAC,IAAI,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC;IAC7C,CAAC;IACD,0BAA0B;IAC1B,IAAI,EAAE,GAAG,CAAC,CAAC;IACX,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,EAAE,CAAC,EAAE;QAAE,IAAI,IAAI,CAAC,CAAC,CAAE,GAAG,EAAE;YAAE,EAAE,GAAG,IAAI,CAAC,CAAC,CAAE,CAAC;IACnE,IAAI,EAAE,GAAG,CAAC;QAAE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,EAAE,CAAC,EAAE;YAAE,IAAI,CAAC,CAAC,CAAE,IAAI,EAAE,CAAC;IAE7D,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,GAAG,EAAE,EAAE,CAAC;AACjE,CAAC;AAED;iDACiD;AACjD,SAAS,UAAU,CAAC,EAAU;IAC5B,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC;IAClC,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC;IACtB,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,qBAAqB;IACnE,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,EAAE,CAAC,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,IAAI,CAAC,CAAC,UAAU;IAC/B,MAAM,GAAG,GAAY,EAAE,CAAC;IACxB,IAAI,KAAK,GAAG,CAAC,QAAQ,CAAC;IACtB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC/B,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,CAAE,CAAC;QACnB,IAAI,CAAC,GAAG,IAAI;YAAE,SAAS;QACvB,aAAa;QACb,IAAI,KAAK,GAAG,IAAI,CAAC;QACjB,KAAK,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE;YAAE,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,SAAS,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,CAAE,GAAG,CAAC,EAAE,CAAC;gBAAC,KAAK,GAAG,KAAK,CAAC;gBAAC,MAAM;YAAC,CAAC;QAC1G,IAAI,CAAC,KAAK;YAAE,SAAS;QACrB,uDAAuD;QACvD,IAAI,GAAG,GAAG,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC;QACrB,KAAK,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;YAAC,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YAAC,IAAI,CAAC,KAAK,SAAS,EAAE,CAAC;gBAAC,GAAG,IAAI,CAAC,CAAC;gBAAC,GAAG,EAAE,CAAC;YAAC,CAAC;QAAC,CAAC;QAC1G,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,GAAG,GAAG,IAAI;YAAE,SAAS;QAC3C,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC;QACjB,IAAI,CAAC,GAAG,KAAK,GAAG,MAAM;YAAE,SAAS;QACjC,KAAK,GAAG,CAAC,CAAC;QACV,8BAA8B;QAC9B,MAAM,EAAE,GAAG,QAAQ,CAAC,CAAC,CAAE,CAAC,CAAC,CAAE,EAAE,EAAE,GAAG,QAAQ,CAAC,CAAC,CAAE,CAAC,CAAC,CAAE,EAAE,EAAE,GAAG,QAAQ,CAAC,CAAC,CAAE,CAAC,CAAC,CAAE,CAAC;QAC1E,MAAM,IAAI,GAAS,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC;QAC5E,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;IACxD,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,wEAAwE;AACxE,SAAS,WAAW,CAAC,IAAkB,EAAE,EAAU;IACjD,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC;IACtB,UAAU;IACV,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE;QAAE,IAAI,IAAI,IAAI,CAAC,CAAC,CAAE,CAAC;IAC7C,IAAI,IAAI,CAAC,CAAC;IACV,MAAM,GAAG,GAAG,IAAI,YAAY,CAAC,CAAC,CAAC,CAAC;IAChC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE;QAAE,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAE,GAAG,IAAI,CAAC;IAErD,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;IACxD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC;IAC1C,MAAM,GAAG,GAAG,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACzC,IAAI,OAAO,GAAG,MAAM,CAAC;IACrB,IAAI,SAAS,GAAG,CAAC,QAAQ,CAAC;IAC1B,KAAK,IAAI,GAAG,GAAG,MAAM,EAAE,GAAG,IAAI,MAAM,EAAE,GAAG,EAAE,EAAE,CAAC;QAC5C,IAAI,CAAC,GAAG,CAAC,CAAC;QACV,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,GAAG,CAAC,EAAE,CAAC,EAAE;YAAE,CAAC,IAAI,GAAG,CAAC,CAAC,CAAE,GAAG,GAAG,CAAC,CAAC,GAAG,GAAG,CAAE,CAAC;QAC/D,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACb,4EAA4E;QAC5E,0CAA0C;QAC1C,MAAM,GAAG,GAAG,EAAE,GAAG,CAAC,GAAG,GAAG,EAAE,CAAC,CAAC;QAC5B,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;QACrE,IAAI,KAAK,GAAG,SAAS,EAAE,CAAC;YAAC,SAAS,GAAG,KAAK,CAAC;YAAC,OAAO,GAAG,GAAG,CAAC;QAAC,CAAC;IAC9D,CAAC;IACD,wEAAwE;IACxE,qEAAqE;IACrE,8EAA8E;IAC9E,SAAS,CAAC;QACR,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC;QACrC,MAAM,IAAI,GAAG,EAAE,GAAG,CAAC,IAAI,GAAG,EAAE,CAAC,CAAC;QAC9B,IAAI,IAAI,GAAG,MAAM,IAAI,IAAI,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,CAAE,GAAG,GAAG,GAAG,GAAG,CAAC,OAAO,CAAE;YAAE,MAAM;QAC3E,OAAO,GAAG,IAAI,CAAC;IACjB,CAAC;IACD,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,OAAO,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC;AACrD,CAAC;AAED;sCACsC;AACtC,SAAS,WAAW,CAAC,IAAkB,EAAE,GAAW,EAAE,EAAU;IAC9D,MAAM,OAAO,GAAG,EAAE,GAAG,GAAG,GAAG,EAAE,CAAC,CAAC,0BAA0B;IACzD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;IAC/C,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,IAAI,SAAS,GAAG,CAAC,QAAQ,CAAC;IAC1B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;QAC/B,IAAI,CAAC,GAAG,CAAC,CAAC;QACV,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,OAAO,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE;YAAE,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,CAAE,CAAC;QAC5F,IAAI,CAAC,GAAG,SAAS,EAAE,CAAC;YAAC,SAAS,GAAG,CAAC,CAAC;YAAC,OAAO,GAAG,CAAC,CAAC;QAAC,CAAC;IACpD,CAAC;IACD,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC;AAChD,CAAC;AAED,gFAAgF;AAChF,SAAS,cAAc,CAAC,GAAiB,EAAE,EAAU,EAAE,QAAgB;IACrE,MAAM,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC;IACrB,wBAAwB;IACxB,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,EAAE,CAAC,CAAC,CAAC;IAC7C,MAAM,EAAE,GAAG,IAAI,YAAY,CAAC,CAAC,CAAC,CAAC;IAC/B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3B,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;QACjB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;YAAC,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YAAC,IAAI,CAAC,KAAK,SAAS,EAAE,CAAC;gBAAC,CAAC,IAAI,CAAC,CAAC;gBAAC,CAAC,EAAE,CAAC;YAAC,CAAC;QAAC,CAAC;QAC/F,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAChB,CAAC;IACD,+DAA+D;IAC/D,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,EAAE,CAAC,CAAC,CAAC;IAC7C,MAAM,GAAG,GAAG,IAAI,YAAY,CAAC,CAAC,CAAC,CAAC;IAChC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3B,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;QACnC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;YAC7B,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,SAAS,EAAE,CAAC;gBAAC,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAE,CAAC;gBAAC,EAAE,EAAE,CAAC;YAAC,CAAC;YACxD,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,SAAS,EAAE,CAAC;gBAAC,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAE,CAAC;gBAAC,EAAE,EAAE,CAAC;YAAC,CAAC;QAC1D,CAAC;QACD,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACtD,CAAC;IACD,6EAA6E;IAC7E,IAAI,EAAE,GAAG,CAAC,CAAC;IACX,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE;QAAE,IAAI,GAAG,CAAC,CAAC,CAAE,GAAG,EAAE;YAAE,EAAE,GAAG,GAAG,CAAC,CAAC,CAAE,CAAC;IAC3D,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,gBAAgB;IACpD,MAAM,UAAU,GAAa,CAAC,CAAC,CAAC,CAAC;IACjC,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC;QACX,IAAI,IAAI,GAAG,CAAC,OAAO,CAAC;QACpB,KAAK,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;YACjC,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC,CAAE,CAAC;YAClB,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI;gBAAE,SAAS;YAC5B,IAAI,KAAK,GAAG,IAAI,CAAC;YACjB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE;gBAAE,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,SAAS,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,CAAE,GAAG,CAAC,EAAE,CAAC;oBAAC,KAAK,GAAG,KAAK,CAAC;oBAAC,MAAM;gBAAC,CAAC;YAC1G,IAAI,KAAK,IAAI,CAAC,GAAG,IAAI,IAAI,OAAO,EAAE,CAAC;gBAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;gBAAC,IAAI,GAAG,CAAC,CAAC;YAAC,CAAC;QACrE,CAAC;IACH,CAAC;IACD,2DAA2D;IAC3D,MAAM,IAAI,GAAqD,EAAE,CAAC;IAClE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3C,MAAM,CAAC,GAAG,UAAU,CAAC,CAAC,CAAE,CAAC;QACzB,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,CAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAC7D,IAAI,GAAG,GAAG,CAAC,CAAC;QACZ,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE;YAAE,GAAG,IAAI,EAAE,CAAC,CAAC,CAAE,CAAC;QAC1C,IAAI,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC;IACvH,CAAC;IACD,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,MAAM,CAAC,IAAI,IAAI;QAAE,IAAI,CAAC,CAAC,MAAM,GAAG,IAAI;YAAE,IAAI,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3D,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QACpB,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;QAC1E,MAAM,KAAK,GAAG,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC;QACpE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAC3G,CAAC,CAAC,CAAC;AACL,CAAC;AAWD;2EAC2E;AAC3E,MAAM,UAAU,UAAU,CAAC,OAAqB,EAAE,EAAU,EAAE,WAAW,GAAG,CAAC;IAC3E,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,GAAG,EAAE,CAAC;IACrC,MAAM,EAAE,GAAG,aAAa,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IACtC,MAAM,MAAM,GAAG,UAAU,CAAC,EAAE,CAAC,CAAC;IAC9B,6EAA6E;IAC7E,2EAA2E;IAC3E,6EAA6E;IAC7E,0EAA0E;IAC1E,MAAM,QAAQ,GAAG,IAAI,YAAY,CAAC,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACjD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACvC,MAAM,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAE,CAAC;QACtC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9B,CAAC;IACD,MAAM,GAAG,GAAG,WAAW,CAAC,QAAQ,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;IACzC,MAAM,KAAK,GAAG,WAAW,CAAC,QAAQ,EAAE,GAAG,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;IAEhD,MAAM,QAAQ,GAAG,EAAE,GAAG,GAAG,CAAC;IAC1B,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,SAAS,GAAa,EAAE,CAAC;IAC/B,KAAK,IAAI,CAAC,GAAG,CAAC,GAAI,CAAC,EAAE,EAAE,CAAC;QACtB,MAAM,CAAC,GAAG,KAAK,GAAG,CAAC,GAAG,QAAQ,CAAC;QAC/B,IAAI,CAAC,GAAG,QAAQ;YAAE,MAAM;QACxB,IAAI,CAAC,GAAG,CAAC;YAAE,SAAS;QACpB,MAAM,MAAM,GAAG,CAAC,GAAG,WAAW,KAAK,CAAC,CAAC;QACrC,MAAM,CAAC,GAAW,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;QACpF,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACd,IAAI,MAAM;YAAE,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAChC,CAAC;IACD,MAAM,QAAQ,GAAG,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC;IACzD,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;AAC5D,CAAC"}
package/dist/fft.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ /** In-place complex FFT. `re`/`im` length MUST be a power of two. */
2
+ export declare function fft(re: Float64Array, im: Float64Array): void;
3
+ /** Smallest power of two ≥ n. */
4
+ export declare function nextPow2(n: number): number;
5
+ /** Magnitude spectrum (first n/2 bins) of a real, already-windowed frame. The
6
+ * frame is copied and zero-padded to a power of two. */
7
+ export declare function magnitudeSpectrum(frame: Float64Array): Float64Array;
8
+ //# sourceMappingURL=fft.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fft.d.ts","sourceRoot":"","sources":["../src/fft.ts"],"names":[],"mappings":"AAIA,qEAAqE;AACrE,wBAAgB,GAAG,CAAC,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE,YAAY,GAAG,IAAI,CAiC5D;AAED,iCAAiC;AACjC,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAI1C;AAED;yDACyD;AACzD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,YAAY,GAAG,YAAY,CAUnE"}
package/dist/fft.js ADDED
@@ -0,0 +1,67 @@
1
+ // Iterative in-place radix-2 Cooley–Tukey FFT. The one piece of heavy math the
2
+ // analyzer leans on; everything else (flux, tempo, sections) is built on the
3
+ // magnitude spectra this produces. Pure and deterministic.
4
+ /** In-place complex FFT. `re`/`im` length MUST be a power of two. */
5
+ export function fft(re, im) {
6
+ const n = re.length;
7
+ // bit-reversal permutation
8
+ for (let i = 1, j = 0; i < n; i++) {
9
+ let bit = n >> 1;
10
+ for (; j & bit; bit >>= 1)
11
+ j ^= bit;
12
+ j ^= bit;
13
+ if (i < j) {
14
+ const tr = re[i];
15
+ re[i] = re[j];
16
+ re[j] = tr;
17
+ const ti = im[i];
18
+ im[i] = im[j];
19
+ im[j] = ti;
20
+ }
21
+ }
22
+ for (let len = 2; len <= n; len <<= 1) {
23
+ const ang = (-2 * Math.PI) / len;
24
+ const wr = Math.cos(ang);
25
+ const wi = Math.sin(ang);
26
+ const half = len >> 1;
27
+ for (let i = 0; i < n; i += len) {
28
+ let cr = 1;
29
+ let ci = 0;
30
+ for (let k = 0; k < half; k++) {
31
+ const a = i + k;
32
+ const b = a + half;
33
+ const xr = re[b] * cr - im[b] * ci;
34
+ const xi = re[b] * ci + im[b] * cr;
35
+ re[b] = re[a] - xr;
36
+ im[b] = im[a] - xi;
37
+ re[a] = re[a] + xr;
38
+ im[a] = im[a] + xi;
39
+ const ncr = cr * wr - ci * wi;
40
+ ci = cr * wi + ci * wr;
41
+ cr = ncr;
42
+ }
43
+ }
44
+ }
45
+ }
46
+ /** Smallest power of two ≥ n. */
47
+ export function nextPow2(n) {
48
+ let p = 1;
49
+ while (p < n)
50
+ p <<= 1;
51
+ return p;
52
+ }
53
+ /** Magnitude spectrum (first n/2 bins) of a real, already-windowed frame. The
54
+ * frame is copied and zero-padded to a power of two. */
55
+ export function magnitudeSpectrum(frame) {
56
+ const n = nextPow2(frame.length);
57
+ const re = new Float64Array(n);
58
+ const im = new Float64Array(n);
59
+ re.set(frame);
60
+ fft(re, im);
61
+ const half = n >> 1;
62
+ const mag = new Float64Array(half);
63
+ for (let i = 0; i < half; i++)
64
+ mag[i] = Math.hypot(re[i], im[i]);
65
+ return mag;
66
+ }
67
+ //# sourceMappingURL=fft.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fft.js","sourceRoot":"","sources":["../src/fft.ts"],"names":[],"mappings":"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"}
package/dist/grid.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ import type { BeatMap } from './beat-map.js';
2
+ export interface BeatGridOptions {
3
+ /** Tempo, beats per minute. Must be > 0. */
4
+ bpm: number;
5
+ /** Total span to fill with beats, seconds. */
6
+ duration: number;
7
+ /** Time of beat 0, seconds. Default 0. */
8
+ phase?: number;
9
+ /** Beats per bar — a downbeat lands every Nth beat from beat 0. Default 4. */
10
+ beatsPerBar?: number;
11
+ /** Label recorded as the map's `source`. Default "synthetic:bpm". */
12
+ source?: string;
13
+ }
14
+ /**
15
+ * Build a {@link BeatMap} from a known tempo. Populates `bpm`, `phase`,
16
+ * `beats`, and `downbeats`; `onsets` and `sections` are left empty (those need
17
+ * real audio analysis). The first beat at or after `phase` is beat 0, and every
18
+ * `beatsPerBar`-th beat from there is a downbeat.
19
+ */
20
+ export declare function beatGrid(opts: BeatGridOptions): BeatMap;
21
+ //# sourceMappingURL=grid.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grid.d.ts","sourceRoot":"","sources":["../src/grid.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,OAAO,EAAU,MAAM,eAAe,CAAC;AAErD,MAAM,WAAW,eAAe;IAC9B,4CAA4C;IAC5C,GAAG,EAAE,MAAM,CAAC;IACZ,8CAA8C;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,0CAA0C;IAC1C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAgCvD"}
package/dist/grid.js ADDED
@@ -0,0 +1,51 @@
1
+ // beatGrid — synthesize a BeatMap from a KNOWN tempo.
2
+ //
3
+ // Tempo detection from raw audio is the analyzer's job (see analyze.ts) and is
4
+ // still ahead of us. But the moment you know the BPM — and you very often do
5
+ // (the track's metadata, a creator who typed it in, a loop at a fixed tempo) —
6
+ // the entire grid is determined: beat k sits at `phase + k·60/bpm`. This builds
7
+ // that grid as a real, fully-formed BeatMap, so motion can be synced to music
8
+ // today while the detector is built behind the same BeatMap contract.
9
+ //
10
+ // Deterministic and pure: same options → same map.
11
+ /**
12
+ * Build a {@link BeatMap} from a known tempo. Populates `bpm`, `phase`,
13
+ * `beats`, and `downbeats`; `onsets` and `sections` are left empty (those need
14
+ * real audio analysis). The first beat at or after `phase` is beat 0, and every
15
+ * `beatsPerBar`-th beat from there is a downbeat.
16
+ */
17
+ export function beatGrid(opts) {
18
+ const { bpm, duration } = opts;
19
+ const phase = opts.phase ?? 0;
20
+ const beatsPerBar = opts.beatsPerBar ?? 4;
21
+ if (!(bpm > 0))
22
+ throw new RangeError(`beatGrid: bpm must be > 0, got ${bpm}`);
23
+ const interval = 60 / bpm; // seconds per beat
24
+ const beats = [];
25
+ const downbeats = [];
26
+ // Beat 0 is the first beat at or after `phase`; index counts from there so
27
+ // the downbeat phase is stable regardless of where `phase` falls.
28
+ for (let k = 0;; k++) {
29
+ const time = phase + k * interval;
30
+ if (time > duration)
31
+ break;
32
+ if (time < 0)
33
+ continue;
34
+ const isDownbeat = k % beatsPerBar === 0;
35
+ const marker = { time, strength: isDownbeat ? 1 : 0.7 };
36
+ beats.push(marker);
37
+ if (isDownbeat)
38
+ downbeats.push(marker);
39
+ }
40
+ return {
41
+ source: opts.source ?? `synthetic:${bpm}bpm`,
42
+ duration,
43
+ bpm,
44
+ phase,
45
+ beats,
46
+ downbeats,
47
+ onsets: [],
48
+ sections: [],
49
+ };
50
+ }
51
+ //# sourceMappingURL=grid.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grid.js","sourceRoot":"","sources":["../src/grid.ts"],"names":[],"mappings":"AAAA,sDAAsD;AACtD,EAAE;AACF,+EAA+E;AAC/E,6EAA6E;AAC7E,+EAA+E;AAC/E,gFAAgF;AAChF,8EAA8E;AAC9E,sEAAsE;AACtE,EAAE;AACF,mDAAmD;AAiBnD;;;;;GAKG;AACH,MAAM,UAAU,QAAQ,CAAC,IAAqB;IAC5C,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC;IAC/B,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC;IAC9B,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,IAAI,CAAC,CAAC;IAC1C,IAAI,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC;QAAE,MAAM,IAAI,UAAU,CAAC,kCAAkC,GAAG,EAAE,CAAC,CAAC;IAE9E,MAAM,QAAQ,GAAG,EAAE,GAAG,GAAG,CAAC,CAAC,mBAAmB;IAC9C,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,SAAS,GAAa,EAAE,CAAC;IAE/B,2EAA2E;IAC3E,kEAAkE;IAClE,KAAK,IAAI,CAAC,GAAG,CAAC,GAAI,CAAC,EAAE,EAAE,CAAC;QACtB,MAAM,IAAI,GAAG,KAAK,GAAG,CAAC,GAAG,QAAQ,CAAC;QAClC,IAAI,IAAI,GAAG,QAAQ;YAAE,MAAM;QAC3B,IAAI,IAAI,GAAG,CAAC;YAAE,SAAS;QACvB,MAAM,UAAU,GAAG,CAAC,GAAG,WAAW,KAAK,CAAC,CAAC;QACzC,MAAM,MAAM,GAAW,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;QAChE,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACnB,IAAI,UAAU;YAAE,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACzC,CAAC;IAED,OAAO;QACL,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,aAAa,GAAG,KAAK;QAC5C,QAAQ;QACR,GAAG;QACH,KAAK;QACL,KAAK;QACL,SAAS;QACT,MAAM,EAAE,EAAE;QACV,QAAQ,EAAE,EAAE;KACb,CAAC;AACJ,CAAC"}
@@ -0,0 +1,5 @@
1
+ export { beatGrid, type BeatGridOptions } from './grid.js';
2
+ export { decodeWav, type DecodedAudio } from './decode-wav.js';
3
+ export { analyzePcm, type PcmAnalysis } from './dsp.js';
4
+ export type * from './beat-map.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,QAAQ,EAAE,KAAK,eAAe,EAAE,MAAM,WAAW,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,KAAK,WAAW,EAAE,MAAM,UAAU,CAAC;AACxD,mBAAmB,eAAe,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ // @clipkit/music-analysis — audio → beat map.
2
+ //
3
+ // Produces an authoring-time beat map (tempo, downbeats, onsets, sections) that
4
+ // AI agents and @clipkit/patterns helpers read to sync motion to music. The
5
+ // beat map is NOT part of the rendered protocol; consumers bake it down to
6
+ // ordinary keyframes/expressions so renders stay deterministic.
7
+ // analyzeAudio reads files (node:fs) — it lives behind the Node-only
8
+ // '@clipkit/music-analysis/node' subpath so importing the browser-safe helpers
9
+ // below (beatGrid etc.) doesn't drag node:fs into client bundles. Mirrors the
10
+ // node/browser export split in @clipkit/speech-to-text.
11
+ export { beatGrid } from './grid.js';
12
+ export { decodeWav } from './decode-wav.js';
13
+ export { analyzePcm } from './dsp.js';
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,8CAA8C;AAC9C,EAAE;AACF,gFAAgF;AAChF,4EAA4E;AAC5E,2EAA2E;AAC3E,gEAAgE;AAEhE,qEAAqE;AACrE,+EAA+E;AAC/E,8EAA8E;AAC9E,wDAAwD;AACxD,OAAO,EAAE,QAAQ,EAAwB,MAAM,WAAW,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAqB,MAAM,iBAAiB,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAoB,MAAM,UAAU,CAAC"}
package/dist/node.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { analyzeAudio, type AnalyzeOptions } from './analyze.js';
2
+ //# sourceMappingURL=node.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,YAAY,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC"}
package/dist/node.js ADDED
@@ -0,0 +1,10 @@
1
+ // Node-only entry for @clipkit/music-analysis.
2
+ //
3
+ // analyzeAudio reads audio files from disk (node:fs/promises), so it can't be
4
+ // part of the browser-safe barrel (index.ts) — importing the barrel for a pure
5
+ // helper like beatGrid would otherwise drag node:fs into client bundles and
6
+ // break the web build. Authoring-time / CLI / MCP code imports it from here:
7
+ //
8
+ // import { analyzeAudio } from '@clipkit/music-analysis/node';
9
+ export { analyzeAudio } from './analyze.js';
10
+ //# sourceMappingURL=node.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node.js","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"AAAA,+CAA+C;AAC/C,EAAE;AACF,8EAA8E;AAC9E,+EAA+E;AAC/E,4EAA4E;AAC5E,6EAA6E;AAC7E,EAAE;AACF,iEAAiE;AAEjE,OAAO,EAAE,YAAY,EAAuB,MAAM,cAAc,CAAC"}
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@clipkit/music-analysis",
3
+ "version": "1.0.0",
4
+ "description": "Analyze an audio file into a Clipkit beat map — tempo, downbeats, onsets, and sections. The beat map is an authoring-time artifact: AI agents and @clipkit/patterns helpers read it to place transitions, accents, and reveals on musical time. It is NOT part of the rendered protocol — the runtime never sees it.",
5
+ "license": "Apache-2.0",
6
+ "repository": { "type": "git", "url": "git+https://github.com/clipkit-video/clipkit.git", "directory": "packages/music-analysis" },
7
+ "homepage": "https://clipkit.dev",
8
+ "bugs": "https://github.com/clipkit-video/clipkit/issues",
9
+ "type": "module",
10
+ "main": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "import": "./dist/index.js",
15
+ "types": "./dist/index.d.ts"
16
+ },
17
+ "./node": {
18
+ "import": "./dist/node.js",
19
+ "types": "./dist/node.d.ts"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "README.md",
25
+ "LICENSE"
26
+ ],
27
+ "scripts": {
28
+ "build": "tsc",
29
+ "dev": "tsc --watch",
30
+ "typecheck": "tsc --noEmit",
31
+ "clean": "rm -rf dist"
32
+ },
33
+ "dependencies": {},
34
+ "devDependencies": {
35
+ "@types/node": "^22.0.0",
36
+ "typescript": "^5.7.3"
37
+ }
38
+ }