@octoseq/mir 0.1.0-main.4ecb074 → 0.1.0-main.5fdb072
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/chunk-HF3QHCRK.js +3234 -0
- package/dist/chunk-HF3QHCRK.js.map +1 -0
- package/dist/index.d.ts +2122 -47
- package/dist/index.js +3524 -39
- package/dist/index.js.map +1 -1
- package/dist/runMir-CnJQbBr8.d.ts +187 -0
- package/dist/runner/runMir.d.ts +2 -2
- package/dist/runner/runMir.js +1 -1
- package/dist/runner/workerProtocol.d.ts +8 -1
- package/dist/runner/workerProtocol.js.map +1 -1
- package/dist/types-DqH4umN8.d.ts +761 -0
- package/package.json +2 -2
- package/src/dsp/activity.ts +544 -0
- package/src/dsp/bandCqt.ts +662 -0
- package/src/dsp/bandEvents.ts +351 -0
- package/src/dsp/bandMask.ts +225 -0
- package/src/dsp/bandMir.ts +524 -0
- package/src/dsp/bandProposal.ts +552 -0
- package/src/dsp/beatCandidates.ts +299 -0
- package/src/dsp/cqt.ts +386 -0
- package/src/dsp/cqtSignals.ts +462 -0
- package/src/dsp/customSignalReduction.ts +841 -0
- package/src/dsp/eventToSignal.ts +531 -0
- package/src/dsp/frequencyBand.ts +956 -0
- package/src/dsp/mel.ts +56 -3
- package/src/dsp/musicalTime.ts +240 -0
- package/src/dsp/onset.ts +296 -30
- package/src/dsp/peakPicking.ts +519 -0
- package/src/dsp/phaseAlignment.ts +153 -0
- package/src/dsp/pitch.ts +289 -0
- package/src/dsp/resample.ts +44 -0
- package/src/dsp/signalTransforms.ts +660 -0
- package/src/dsp/silenceGating.ts +511 -0
- package/src/dsp/spectral.ts +124 -4
- package/src/dsp/tempoHypotheses.ts +395 -0
- package/src/gpu/bufferPool.ts +266 -0
- package/src/gpu/context.ts +30 -6
- package/src/gpu/helpers.ts +85 -0
- package/src/gpu/melProject.ts +83 -0
- package/src/index.ts +366 -5
- package/src/runner/runMir.ts +234 -2
- package/src/runner/workerProtocol.ts +9 -1
- package/src/types.ts +768 -3
- package/dist/chunk-DUWYCAVG.js +0 -1525
- package/dist/chunk-DUWYCAVG.js.map +0 -1
- package/dist/runMir-CSIBwNZ3.d.ts +0 -84
- package/dist/types-BE3py4fZ.d.ts +0 -83
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event to Signal Conversion
|
|
3
|
+
*
|
|
4
|
+
* Converts discrete event streams (onsets, beats, authored events)
|
|
5
|
+
* into continuous 1D signals for use in derived signal pipelines.
|
|
6
|
+
*
|
|
7
|
+
* Reducer algorithms:
|
|
8
|
+
* - eventCount: Count events per window
|
|
9
|
+
* - eventDensity: Normalized count (events per second)
|
|
10
|
+
* - weightedSum: Sum of event weights per window
|
|
11
|
+
* - weightedMean: Mean of event weights per window
|
|
12
|
+
* - envelope: Generate continuous envelope from events
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// TYPES
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A discrete event with time and optional weight.
|
|
21
|
+
*/
|
|
22
|
+
export interface DiscreteEvent {
|
|
23
|
+
/** Event time in seconds. */
|
|
24
|
+
time: number;
|
|
25
|
+
/** Event weight (default 1.0). */
|
|
26
|
+
weight?: number;
|
|
27
|
+
/** Optional duration in seconds. */
|
|
28
|
+
duration?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Window specification for event aggregation.
|
|
33
|
+
*/
|
|
34
|
+
export type EventWindowSpec =
|
|
35
|
+
| { kind: "seconds"; windowSize: number }
|
|
36
|
+
| { kind: "samples"; windowSize: number };
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Envelope shape for event-to-signal conversion.
|
|
40
|
+
*/
|
|
41
|
+
export type EnvelopeShape =
|
|
42
|
+
| { kind: "impulse" }
|
|
43
|
+
| { kind: "gaussian"; widthMs: number }
|
|
44
|
+
| { kind: "attackDecay"; attackMs: number; decayMs: number }
|
|
45
|
+
| { kind: "gate" }; // Uses event duration
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Options for event-to-signal conversion.
|
|
49
|
+
*/
|
|
50
|
+
export interface EventToSignalOptions {
|
|
51
|
+
/** Sample rate for output signal (samples per second). */
|
|
52
|
+
sampleRate: number;
|
|
53
|
+
/** Total duration in seconds. */
|
|
54
|
+
duration: number;
|
|
55
|
+
/** Whether to normalize output to 0-1 range. */
|
|
56
|
+
normalize?: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Result of event-to-signal conversion.
|
|
61
|
+
*/
|
|
62
|
+
export interface EventSignalResult {
|
|
63
|
+
/** Signal values. */
|
|
64
|
+
values: Float32Array;
|
|
65
|
+
/** Time points in seconds. */
|
|
66
|
+
times: Float32Array;
|
|
67
|
+
/** Value range before normalization. */
|
|
68
|
+
rawRange: { min: number; max: number };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// ENVELOPE GENERATORS
|
|
73
|
+
// ============================================================================
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Generate a Gaussian envelope centered at a given sample.
|
|
77
|
+
*/
|
|
78
|
+
function generateGaussianEnvelope(
|
|
79
|
+
centerSample: number,
|
|
80
|
+
widthSamples: number,
|
|
81
|
+
numSamples: number
|
|
82
|
+
): Float32Array {
|
|
83
|
+
const envelope = new Float32Array(numSamples);
|
|
84
|
+
const sigma = widthSamples / 4; // 4 sigma covers ~95% of the bell
|
|
85
|
+
const twoSigmaSq = 2 * sigma * sigma;
|
|
86
|
+
|
|
87
|
+
const startSample = Math.max(0, Math.floor(centerSample - widthSamples));
|
|
88
|
+
const endSample = Math.min(numSamples, Math.ceil(centerSample + widthSamples));
|
|
89
|
+
|
|
90
|
+
for (let i = startSample; i < endSample; i++) {
|
|
91
|
+
const diff = i - centerSample;
|
|
92
|
+
envelope[i] = Math.exp(-(diff * diff) / twoSigmaSq);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return envelope;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Generate an attack-decay envelope starting at a given sample.
|
|
100
|
+
*/
|
|
101
|
+
function generateAttackDecayEnvelope(
|
|
102
|
+
startSample: number,
|
|
103
|
+
attackSamples: number,
|
|
104
|
+
decaySamples: number,
|
|
105
|
+
numSamples: number
|
|
106
|
+
): Float32Array {
|
|
107
|
+
const envelope = new Float32Array(numSamples);
|
|
108
|
+
|
|
109
|
+
// Attack phase
|
|
110
|
+
const attackEnd = Math.min(numSamples, startSample + attackSamples);
|
|
111
|
+
for (let i = startSample; i < attackEnd; i++) {
|
|
112
|
+
if (i >= 0) {
|
|
113
|
+
const t = (i - startSample) / attackSamples;
|
|
114
|
+
envelope[i] = t;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Decay phase
|
|
119
|
+
const decayStart = startSample + attackSamples;
|
|
120
|
+
const decayEnd = Math.min(numSamples, decayStart + decaySamples);
|
|
121
|
+
for (let i = decayStart; i < decayEnd; i++) {
|
|
122
|
+
if (i >= 0) {
|
|
123
|
+
const t = (i - decayStart) / decaySamples;
|
|
124
|
+
envelope[i] = 1 - t;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return envelope;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Generate a gate envelope (rectangular) for an event with duration.
|
|
133
|
+
*/
|
|
134
|
+
function generateGateEnvelope(
|
|
135
|
+
startSample: number,
|
|
136
|
+
durationSamples: number,
|
|
137
|
+
numSamples: number
|
|
138
|
+
): Float32Array {
|
|
139
|
+
const envelope = new Float32Array(numSamples);
|
|
140
|
+
|
|
141
|
+
const endSample = Math.min(numSamples, startSample + durationSamples);
|
|
142
|
+
for (let i = Math.max(0, startSample); i < endSample; i++) {
|
|
143
|
+
envelope[i] = 1;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return envelope;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ============================================================================
|
|
150
|
+
// REDUCER IMPLEMENTATIONS
|
|
151
|
+
// ============================================================================
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Count events per window (sliding window approach).
|
|
155
|
+
*/
|
|
156
|
+
export function eventCount(
|
|
157
|
+
events: DiscreteEvent[],
|
|
158
|
+
windowSpec: EventWindowSpec,
|
|
159
|
+
options: EventToSignalOptions
|
|
160
|
+
): EventSignalResult {
|
|
161
|
+
const { sampleRate, duration, normalize = false } = options;
|
|
162
|
+
const numSamples = Math.ceil(duration * sampleRate);
|
|
163
|
+
const values = new Float32Array(numSamples);
|
|
164
|
+
const times = new Float32Array(numSamples);
|
|
165
|
+
|
|
166
|
+
// Generate time array
|
|
167
|
+
for (let i = 0; i < numSamples; i++) {
|
|
168
|
+
times[i] = i / sampleRate;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const windowSamples =
|
|
172
|
+
windowSpec.kind === "seconds"
|
|
173
|
+
? Math.ceil(windowSpec.windowSize * sampleRate)
|
|
174
|
+
: windowSpec.windowSize;
|
|
175
|
+
|
|
176
|
+
const halfWindow = Math.floor(windowSamples / 2);
|
|
177
|
+
|
|
178
|
+
// Sort events by time
|
|
179
|
+
const sortedEvents = [...events].sort((a, b) => a.time - b.time);
|
|
180
|
+
|
|
181
|
+
// Count events in each window
|
|
182
|
+
for (let i = 0; i < numSamples; i++) {
|
|
183
|
+
const windowStart = (i - halfWindow) / sampleRate;
|
|
184
|
+
const windowEnd = (i + halfWindow) / sampleRate;
|
|
185
|
+
|
|
186
|
+
let count = 0;
|
|
187
|
+
for (const event of sortedEvents) {
|
|
188
|
+
if (event.time >= windowStart && event.time < windowEnd) {
|
|
189
|
+
count++;
|
|
190
|
+
}
|
|
191
|
+
// Early exit if past window
|
|
192
|
+
if (event.time >= windowEnd) break;
|
|
193
|
+
}
|
|
194
|
+
values[i] = count;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Compute range
|
|
198
|
+
let min = 0;
|
|
199
|
+
let max = 0;
|
|
200
|
+
if (values.length > 0) {
|
|
201
|
+
min = values[0]!;
|
|
202
|
+
max = values[0]!;
|
|
203
|
+
for (let i = 1; i < values.length; i++) {
|
|
204
|
+
const v = values[i]!;
|
|
205
|
+
if (v < min) min = v;
|
|
206
|
+
if (v > max) max = v;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Normalize if requested
|
|
211
|
+
if (normalize && max > min) {
|
|
212
|
+
const range = max - min;
|
|
213
|
+
for (let i = 0; i < values.length; i++) {
|
|
214
|
+
values[i] = (values[i]! - min) / range;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return { values, times, rawRange: { min, max } };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Event density: events per second in each window.
|
|
223
|
+
*/
|
|
224
|
+
export function eventDensity(
|
|
225
|
+
events: DiscreteEvent[],
|
|
226
|
+
windowSpec: EventWindowSpec,
|
|
227
|
+
options: EventToSignalOptions
|
|
228
|
+
): EventSignalResult {
|
|
229
|
+
const result = eventCount(events, windowSpec, { ...options, normalize: false });
|
|
230
|
+
|
|
231
|
+
const windowSeconds =
|
|
232
|
+
windowSpec.kind === "seconds"
|
|
233
|
+
? windowSpec.windowSize
|
|
234
|
+
: windowSpec.windowSize / options.sampleRate;
|
|
235
|
+
|
|
236
|
+
// Convert count to density
|
|
237
|
+
for (let i = 0; i < result.values.length; i++) {
|
|
238
|
+
result.values[i] = result.values[i]! / windowSeconds;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Recompute range
|
|
242
|
+
let min = 0;
|
|
243
|
+
let max = 0;
|
|
244
|
+
if (result.values.length > 0) {
|
|
245
|
+
min = result.values[0]!;
|
|
246
|
+
max = result.values[0]!;
|
|
247
|
+
for (let i = 1; i < result.values.length; i++) {
|
|
248
|
+
const v = result.values[i]!;
|
|
249
|
+
if (v < min) min = v;
|
|
250
|
+
if (v > max) max = v;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Normalize if requested
|
|
255
|
+
if (options.normalize && max > min) {
|
|
256
|
+
const range = max - min;
|
|
257
|
+
for (let i = 0; i < result.values.length; i++) {
|
|
258
|
+
result.values[i] = (result.values[i]! - min) / range;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return { ...result, rawRange: { min, max } };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Weighted sum of events per window.
|
|
267
|
+
*/
|
|
268
|
+
export function weightedSum(
|
|
269
|
+
events: DiscreteEvent[],
|
|
270
|
+
windowSpec: EventWindowSpec,
|
|
271
|
+
options: EventToSignalOptions
|
|
272
|
+
): EventSignalResult {
|
|
273
|
+
const { sampleRate, duration, normalize = false } = options;
|
|
274
|
+
const numSamples = Math.ceil(duration * sampleRate);
|
|
275
|
+
const values = new Float32Array(numSamples);
|
|
276
|
+
const times = new Float32Array(numSamples);
|
|
277
|
+
|
|
278
|
+
// Generate time array
|
|
279
|
+
for (let i = 0; i < numSamples; i++) {
|
|
280
|
+
times[i] = i / sampleRate;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const windowSamples =
|
|
284
|
+
windowSpec.kind === "seconds"
|
|
285
|
+
? Math.ceil(windowSpec.windowSize * sampleRate)
|
|
286
|
+
: windowSpec.windowSize;
|
|
287
|
+
|
|
288
|
+
const halfWindow = Math.floor(windowSamples / 2);
|
|
289
|
+
|
|
290
|
+
// Sort events by time
|
|
291
|
+
const sortedEvents = [...events].sort((a, b) => a.time - b.time);
|
|
292
|
+
|
|
293
|
+
// Sum weights in each window
|
|
294
|
+
for (let i = 0; i < numSamples; i++) {
|
|
295
|
+
const windowStart = (i - halfWindow) / sampleRate;
|
|
296
|
+
const windowEnd = (i + halfWindow) / sampleRate;
|
|
297
|
+
|
|
298
|
+
let sum = 0;
|
|
299
|
+
for (const event of sortedEvents) {
|
|
300
|
+
if (event.time >= windowStart && event.time < windowEnd) {
|
|
301
|
+
sum += event.weight ?? 1;
|
|
302
|
+
}
|
|
303
|
+
if (event.time >= windowEnd) break;
|
|
304
|
+
}
|
|
305
|
+
values[i] = sum;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Compute range
|
|
309
|
+
let min = 0;
|
|
310
|
+
let max = 0;
|
|
311
|
+
if (values.length > 0) {
|
|
312
|
+
min = values[0]!;
|
|
313
|
+
max = values[0]!;
|
|
314
|
+
for (let i = 1; i < values.length; i++) {
|
|
315
|
+
const v = values[i]!;
|
|
316
|
+
if (v < min) min = v;
|
|
317
|
+
if (v > max) max = v;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Normalize if requested
|
|
322
|
+
if (normalize && max > min) {
|
|
323
|
+
const range = max - min;
|
|
324
|
+
for (let i = 0; i < values.length; i++) {
|
|
325
|
+
values[i] = (values[i]! - min) / range;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return { values, times, rawRange: { min, max } };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Weighted mean of events per window.
|
|
334
|
+
*/
|
|
335
|
+
export function weightedMean(
|
|
336
|
+
events: DiscreteEvent[],
|
|
337
|
+
windowSpec: EventWindowSpec,
|
|
338
|
+
options: EventToSignalOptions
|
|
339
|
+
): EventSignalResult {
|
|
340
|
+
const { sampleRate, duration, normalize = false } = options;
|
|
341
|
+
const numSamples = Math.ceil(duration * sampleRate);
|
|
342
|
+
const values = new Float32Array(numSamples);
|
|
343
|
+
const times = new Float32Array(numSamples);
|
|
344
|
+
|
|
345
|
+
// Generate time array
|
|
346
|
+
for (let i = 0; i < numSamples; i++) {
|
|
347
|
+
times[i] = i / sampleRate;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const windowSamples =
|
|
351
|
+
windowSpec.kind === "seconds"
|
|
352
|
+
? Math.ceil(windowSpec.windowSize * sampleRate)
|
|
353
|
+
: windowSpec.windowSize;
|
|
354
|
+
|
|
355
|
+
const halfWindow = Math.floor(windowSamples / 2);
|
|
356
|
+
|
|
357
|
+
// Sort events by time
|
|
358
|
+
const sortedEvents = [...events].sort((a, b) => a.time - b.time);
|
|
359
|
+
|
|
360
|
+
// Compute weighted mean in each window
|
|
361
|
+
for (let i = 0; i < numSamples; i++) {
|
|
362
|
+
const windowStart = (i - halfWindow) / sampleRate;
|
|
363
|
+
const windowEnd = (i + halfWindow) / sampleRate;
|
|
364
|
+
|
|
365
|
+
let sum = 0;
|
|
366
|
+
let count = 0;
|
|
367
|
+
for (const event of sortedEvents) {
|
|
368
|
+
if (event.time >= windowStart && event.time < windowEnd) {
|
|
369
|
+
sum += event.weight ?? 1;
|
|
370
|
+
count++;
|
|
371
|
+
}
|
|
372
|
+
if (event.time >= windowEnd) break;
|
|
373
|
+
}
|
|
374
|
+
values[i] = count > 0 ? sum / count : 0;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Compute range
|
|
378
|
+
let min = 0;
|
|
379
|
+
let max = 0;
|
|
380
|
+
if (values.length > 0) {
|
|
381
|
+
min = values[0]!;
|
|
382
|
+
max = values[0]!;
|
|
383
|
+
for (let i = 1; i < values.length; i++) {
|
|
384
|
+
const v = values[i]!;
|
|
385
|
+
if (v < min) min = v;
|
|
386
|
+
if (v > max) max = v;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Normalize if requested
|
|
391
|
+
if (normalize && max > min) {
|
|
392
|
+
const range = max - min;
|
|
393
|
+
for (let i = 0; i < values.length; i++) {
|
|
394
|
+
values[i] = (values[i]! - min) / range;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return { values, times, rawRange: { min, max } };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Generate continuous envelope from events.
|
|
403
|
+
* Each event contributes an envelope shape to the output.
|
|
404
|
+
*/
|
|
405
|
+
export function eventEnvelope(
|
|
406
|
+
events: DiscreteEvent[],
|
|
407
|
+
shape: EnvelopeShape,
|
|
408
|
+
options: EventToSignalOptions
|
|
409
|
+
): EventSignalResult {
|
|
410
|
+
const { sampleRate, duration, normalize = false } = options;
|
|
411
|
+
const numSamples = Math.ceil(duration * sampleRate);
|
|
412
|
+
const values = new Float32Array(numSamples);
|
|
413
|
+
const times = new Float32Array(numSamples);
|
|
414
|
+
|
|
415
|
+
// Generate time array
|
|
416
|
+
for (let i = 0; i < numSamples; i++) {
|
|
417
|
+
times[i] = i / sampleRate;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Generate envelope for each event and sum
|
|
421
|
+
for (const event of events) {
|
|
422
|
+
const eventSample = Math.floor(event.time * sampleRate);
|
|
423
|
+
const weight = event.weight ?? 1;
|
|
424
|
+
let envelope: Float32Array;
|
|
425
|
+
|
|
426
|
+
switch (shape.kind) {
|
|
427
|
+
case "impulse":
|
|
428
|
+
// Single sample impulse
|
|
429
|
+
if (eventSample >= 0 && eventSample < numSamples) {
|
|
430
|
+
values[eventSample] = values[eventSample]! + weight;
|
|
431
|
+
}
|
|
432
|
+
continue;
|
|
433
|
+
|
|
434
|
+
case "gaussian":
|
|
435
|
+
const widthSamples = (shape.widthMs / 1000) * sampleRate;
|
|
436
|
+
envelope = generateGaussianEnvelope(eventSample, widthSamples, numSamples);
|
|
437
|
+
break;
|
|
438
|
+
|
|
439
|
+
case "attackDecay":
|
|
440
|
+
const attackSamples = (shape.attackMs / 1000) * sampleRate;
|
|
441
|
+
const decaySamples = (shape.decayMs / 1000) * sampleRate;
|
|
442
|
+
envelope = generateAttackDecayEnvelope(eventSample, attackSamples, decaySamples, numSamples);
|
|
443
|
+
break;
|
|
444
|
+
|
|
445
|
+
case "gate":
|
|
446
|
+
const durationSamples = event.duration
|
|
447
|
+
? event.duration * sampleRate
|
|
448
|
+
: sampleRate * 0.1; // Default 100ms
|
|
449
|
+
envelope = generateGateEnvelope(eventSample, durationSamples, numSamples);
|
|
450
|
+
break;
|
|
451
|
+
|
|
452
|
+
default:
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Add weighted envelope to output
|
|
457
|
+
for (let i = 0; i < numSamples; i++) {
|
|
458
|
+
values[i] = values[i]! + envelope[i]! * weight;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Compute range
|
|
463
|
+
let min = 0;
|
|
464
|
+
let max = 0;
|
|
465
|
+
if (values.length > 0) {
|
|
466
|
+
min = values[0]!;
|
|
467
|
+
max = values[0]!;
|
|
468
|
+
for (let i = 1; i < values.length; i++) {
|
|
469
|
+
const v = values[i]!;
|
|
470
|
+
if (v < min) min = v;
|
|
471
|
+
if (v > max) max = v;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Normalize if requested
|
|
476
|
+
if (normalize && max > min) {
|
|
477
|
+
const range = max - min;
|
|
478
|
+
for (let i = 0; i < values.length; i++) {
|
|
479
|
+
values[i] = (values[i]! - min) / range;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return { values, times, rawRange: { min, max } };
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// ============================================================================
|
|
487
|
+
// MAIN CONVERSION FUNCTION
|
|
488
|
+
// ============================================================================
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Convert events to signal using specified reducer.
|
|
492
|
+
*/
|
|
493
|
+
export type EventReducer = "eventCount" | "eventDensity" | "weightedSum" | "weightedMean" | "envelope";
|
|
494
|
+
|
|
495
|
+
export interface EventToSignalParams {
|
|
496
|
+
/** Reducer algorithm. */
|
|
497
|
+
reducer: EventReducer;
|
|
498
|
+
/** Window spec for count/density/sum/mean reducers. */
|
|
499
|
+
window?: EventWindowSpec;
|
|
500
|
+
/** Envelope shape for envelope reducer. */
|
|
501
|
+
envelopeShape?: EnvelopeShape;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
export function eventsToSignal(
|
|
505
|
+
events: DiscreteEvent[],
|
|
506
|
+
params: EventToSignalParams,
|
|
507
|
+
options: EventToSignalOptions
|
|
508
|
+
): EventSignalResult {
|
|
509
|
+
const defaultWindow: EventWindowSpec = { kind: "seconds", windowSize: 0.5 };
|
|
510
|
+
const defaultShape: EnvelopeShape = { kind: "attackDecay", attackMs: 5, decayMs: 100 };
|
|
511
|
+
|
|
512
|
+
switch (params.reducer) {
|
|
513
|
+
case "eventCount":
|
|
514
|
+
return eventCount(events, params.window ?? defaultWindow, options);
|
|
515
|
+
|
|
516
|
+
case "eventDensity":
|
|
517
|
+
return eventDensity(events, params.window ?? defaultWindow, options);
|
|
518
|
+
|
|
519
|
+
case "weightedSum":
|
|
520
|
+
return weightedSum(events, params.window ?? defaultWindow, options);
|
|
521
|
+
|
|
522
|
+
case "weightedMean":
|
|
523
|
+
return weightedMean(events, params.window ?? defaultWindow, options);
|
|
524
|
+
|
|
525
|
+
case "envelope":
|
|
526
|
+
return eventEnvelope(events, params.envelopeShape ?? defaultShape, options);
|
|
527
|
+
|
|
528
|
+
default:
|
|
529
|
+
throw new Error(`Unknown event reducer: ${params.reducer}`);
|
|
530
|
+
}
|
|
531
|
+
}
|