@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.
Files changed (47) hide show
  1. package/dist/chunk-HF3QHCRK.js +3234 -0
  2. package/dist/chunk-HF3QHCRK.js.map +1 -0
  3. package/dist/index.d.ts +2122 -47
  4. package/dist/index.js +3524 -39
  5. package/dist/index.js.map +1 -1
  6. package/dist/runMir-CnJQbBr8.d.ts +187 -0
  7. package/dist/runner/runMir.d.ts +2 -2
  8. package/dist/runner/runMir.js +1 -1
  9. package/dist/runner/workerProtocol.d.ts +8 -1
  10. package/dist/runner/workerProtocol.js.map +1 -1
  11. package/dist/types-DqH4umN8.d.ts +761 -0
  12. package/package.json +2 -2
  13. package/src/dsp/activity.ts +544 -0
  14. package/src/dsp/bandCqt.ts +662 -0
  15. package/src/dsp/bandEvents.ts +351 -0
  16. package/src/dsp/bandMask.ts +225 -0
  17. package/src/dsp/bandMir.ts +524 -0
  18. package/src/dsp/bandProposal.ts +552 -0
  19. package/src/dsp/beatCandidates.ts +299 -0
  20. package/src/dsp/cqt.ts +386 -0
  21. package/src/dsp/cqtSignals.ts +462 -0
  22. package/src/dsp/customSignalReduction.ts +841 -0
  23. package/src/dsp/eventToSignal.ts +531 -0
  24. package/src/dsp/frequencyBand.ts +956 -0
  25. package/src/dsp/mel.ts +56 -3
  26. package/src/dsp/musicalTime.ts +240 -0
  27. package/src/dsp/onset.ts +296 -30
  28. package/src/dsp/peakPicking.ts +519 -0
  29. package/src/dsp/phaseAlignment.ts +153 -0
  30. package/src/dsp/pitch.ts +289 -0
  31. package/src/dsp/resample.ts +44 -0
  32. package/src/dsp/signalTransforms.ts +660 -0
  33. package/src/dsp/silenceGating.ts +511 -0
  34. package/src/dsp/spectral.ts +124 -4
  35. package/src/dsp/tempoHypotheses.ts +395 -0
  36. package/src/gpu/bufferPool.ts +266 -0
  37. package/src/gpu/context.ts +30 -6
  38. package/src/gpu/helpers.ts +85 -0
  39. package/src/gpu/melProject.ts +83 -0
  40. package/src/index.ts +366 -5
  41. package/src/runner/runMir.ts +234 -2
  42. package/src/runner/workerProtocol.ts +9 -1
  43. package/src/types.ts +768 -3
  44. package/dist/chunk-DUWYCAVG.js +0 -1525
  45. package/dist/chunk-DUWYCAVG.js.map +0 -1
  46. package/dist/runMir-CSIBwNZ3.d.ts +0 -84
  47. package/dist/types-BE3py4fZ.d.ts +0 -83
@@ -0,0 +1,395 @@
1
+ import type { BeatCandidate, TempoHypothesis, TempoHypothesisEvidence } from "../types";
2
+
3
+ /**
4
+ * Configuration for tempo hypothesis generation.
5
+ */
6
+ export type TempoHypothesesOptions = {
7
+ /** Minimum BPM to consider. Default: 24. */
8
+ minBpm?: number;
9
+ /** Maximum BPM to consider. Default: 300. */
10
+ maxBpm?: number;
11
+ /** Histogram bin size in BPM. Default: 1.0. */
12
+ binSizeBpm?: number;
13
+ /** Maximum hypotheses to return. Default: 10. */
14
+ maxHypotheses?: number;
15
+ /** Minimum confidence threshold (0-1). Default: 0.05. */
16
+ minConfidence?: number;
17
+ /** Weight IOIs by candidate strength. Default: true. */
18
+ weightByStrength?: boolean;
19
+ /** Include histogram data in output. Default: false. */
20
+ includeHistogram?: boolean;
21
+ };
22
+
23
+ export type TempoHypothesesOutput = {
24
+ hypotheses: TempoHypothesis[];
25
+ inputCandidateCount: number;
26
+ histogram?: {
27
+ bpmBins: Float32Array;
28
+ counts: Float32Array;
29
+ };
30
+ };
31
+
32
+ /**
33
+ * Convert interval (seconds) to BPM.
34
+ */
35
+ function intervalToBpm(intervalSec: number): number {
36
+ return 60.0 / intervalSec;
37
+ }
38
+
39
+ /**
40
+ * Convert BPM to interval (seconds).
41
+ */
42
+ function bpmToInterval(bpm: number): number {
43
+ return 60.0 / bpm;
44
+ }
45
+
46
+ type IOI = { intervalSec: number; weight: number };
47
+
48
+ /**
49
+ * Compute inter-onset intervals from beat candidates.
50
+ *
51
+ * @param candidates - Beat candidates sorted by time
52
+ * @param weightByStrength - Whether to weight by candidate strength
53
+ * @returns Array of { intervalSec, weight } pairs
54
+ */
55
+ function computeIOIs(candidates: BeatCandidate[], weightByStrength: boolean): IOI[] {
56
+ if (candidates.length < 2) return [];
57
+
58
+ const iois: IOI[] = [];
59
+
60
+ // Sort candidates by time (should already be sorted, but be defensive)
61
+ const sorted = [...candidates].sort((a, b) => a.time - b.time);
62
+
63
+ for (let i = 1; i < sorted.length; i++) {
64
+ const prev = sorted[i - 1]!;
65
+ const curr = sorted[i]!;
66
+ const interval = curr.time - prev.time;
67
+
68
+ // Skip invalid intervals
69
+ if (interval <= 0) continue;
70
+
71
+ // Weight is geometric mean of adjacent strengths, or 1.0 if not weighting
72
+ const weight = weightByStrength
73
+ ? Math.sqrt(prev.strength * curr.strength)
74
+ : 1.0;
75
+
76
+ iois.push({ intervalSec: interval, weight });
77
+ }
78
+
79
+ return iois;
80
+ }
81
+
82
+ /**
83
+ * Build a weighted histogram of BPM values from IOIs.
84
+ *
85
+ * @param iois - Inter-onset intervals with weights
86
+ * @param minBpm - Minimum BPM (determines max interval)
87
+ * @param maxBpm - Maximum BPM (determines min interval)
88
+ * @param binSizeBpm - Size of each histogram bin in BPM
89
+ * @returns { bins: center BPM of each bin, counts: weighted counts }
90
+ */
91
+ function buildBpmHistogram(
92
+ iois: IOI[],
93
+ minBpm: number,
94
+ maxBpm: number,
95
+ binSizeBpm: number
96
+ ): { bpmBins: Float32Array; counts: Float32Array } {
97
+ const numBins = Math.ceil((maxBpm - minBpm) / binSizeBpm);
98
+ const counts = new Float32Array(numBins);
99
+ const bpmBins = new Float32Array(numBins);
100
+
101
+ // Initialize bin centers
102
+ for (let i = 0; i < numBins; i++) {
103
+ bpmBins[i] = minBpm + (i + 0.5) * binSizeBpm;
104
+ }
105
+
106
+ // Convert interval range to BPM range
107
+ const minInterval = bpmToInterval(maxBpm);
108
+ const maxInterval = bpmToInterval(minBpm);
109
+
110
+ for (const { intervalSec, weight } of iois) {
111
+ // Filter to plausible range
112
+ if (intervalSec < minInterval || intervalSec > maxInterval) continue;
113
+
114
+ const bpm = intervalToBpm(intervalSec);
115
+ const binIndex = Math.floor((bpm - minBpm) / binSizeBpm);
116
+
117
+ if (binIndex >= 0 && binIndex < numBins) {
118
+ counts[binIndex] = (counts[binIndex] ?? 0) + weight;
119
+ }
120
+ }
121
+
122
+ return { bpmBins, counts };
123
+ }
124
+
125
+ /**
126
+ * Find peaks in the histogram using local maximum detection.
127
+ *
128
+ * @param counts - Weighted counts per bin
129
+ * @param minHeight - Minimum peak height (absolute)
130
+ * @returns Array of peak indices sorted by height descending
131
+ */
132
+ function findHistogramPeaks(counts: Float32Array, minHeight: number): number[] {
133
+ const peaks: Array<{ index: number; height: number }> = [];
134
+
135
+ for (let i = 1; i < counts.length - 1; i++) {
136
+ const curr = counts[i]!;
137
+ const prev = counts[i - 1]!;
138
+ const next = counts[i + 1]!;
139
+
140
+ // Local maximum
141
+ if (curr > prev && curr > next && curr >= minHeight) {
142
+ peaks.push({ index: i, height: curr });
143
+ }
144
+ }
145
+
146
+ // Also check boundary bins if they're high enough
147
+ if (counts.length > 0 && counts[0]! >= minHeight && counts[0]! > (counts[1] ?? 0)) {
148
+ peaks.push({ index: 0, height: counts[0]! });
149
+ }
150
+ if (counts.length > 1) {
151
+ const last = counts.length - 1;
152
+ if (counts[last]! >= minHeight && counts[last]! > (counts[last - 1] ?? 0)) {
153
+ peaks.push({ index: last, height: counts[last]! });
154
+ }
155
+ }
156
+
157
+ // Sort by height descending
158
+ peaks.sort((a, b) => b.height - a.height);
159
+
160
+ return peaks.map((p) => p.index);
161
+ }
162
+
163
+ /**
164
+ * Merge adjacent peak bins to get refined BPM estimate.
165
+ * Uses weighted centroid of adjacent bins.
166
+ */
167
+ function refinePeakBpm(
168
+ peakIndex: number,
169
+ bpmBins: Float32Array,
170
+ counts: Float32Array,
171
+ binSizeBpm: number
172
+ ): { bpm: number; peakHeight: number; binRange: [number, number]; totalWeight: number } {
173
+ // Consider the peak bin and immediate neighbors
174
+ let totalWeight = 0;
175
+ let weightedBpm = 0;
176
+ let minBinBpm = bpmBins[peakIndex]! - binSizeBpm / 2;
177
+ let maxBinBpm = bpmBins[peakIndex]! + binSizeBpm / 2;
178
+
179
+ for (let offset = -1; offset <= 1; offset++) {
180
+ const idx = peakIndex + offset;
181
+ if (idx < 0 || idx >= bpmBins.length) continue;
182
+
183
+ const w = counts[idx]!;
184
+ const bpm = bpmBins[idx]!;
185
+
186
+ totalWeight += w;
187
+ weightedBpm += w * bpm;
188
+
189
+ if (w > 0) {
190
+ minBinBpm = Math.min(minBinBpm, bpm - binSizeBpm / 2);
191
+ maxBinBpm = Math.max(maxBinBpm, bpm + binSizeBpm / 2);
192
+ }
193
+ }
194
+
195
+ const refinedBpm = totalWeight > 0 ? weightedBpm / totalWeight : bpmBins[peakIndex]!;
196
+
197
+ return {
198
+ bpm: refinedBpm,
199
+ peakHeight: counts[peakIndex]!,
200
+ binRange: [minBinBpm, maxBinBpm],
201
+ totalWeight,
202
+ };
203
+ }
204
+
205
+ /**
206
+ * Check if two BPMs are harmonically related (within tolerance).
207
+ * Returns the harmonic ratio if related, null otherwise.
208
+ */
209
+ function getHarmonicRatio(bpm1: number, bpm2: number, tolerance: number = 0.03): number | null {
210
+ const ratios = [0.5, 1 / 3, 2 / 3, 1.0, 1.5, 2.0, 3.0];
211
+
212
+ for (const ratio of ratios) {
213
+ const expected = bpm1 * ratio;
214
+ const relativeError = Math.abs(bpm2 - expected) / expected;
215
+ if (relativeError <= tolerance) {
216
+ return ratio;
217
+ }
218
+ }
219
+
220
+ return null;
221
+ }
222
+
223
+ /**
224
+ * Group hypotheses into harmonic families.
225
+ * Assigns familyId and harmonicRatio to each hypothesis.
226
+ *
227
+ * Uses deterministic family IDs based on the root BPM.
228
+ */
229
+ function assignHarmonicFamilies(hypotheses: TempoHypothesis[]): void {
230
+ if (hypotheses.length === 0) return;
231
+
232
+ const families: Map<string, { rootBpm: number; members: TempoHypothesis[] }> = new Map();
233
+
234
+ for (const hyp of hypotheses) {
235
+ let foundFamily = false;
236
+
237
+ for (const [familyId, family] of families) {
238
+ const ratio = getHarmonicRatio(family.rootBpm, hyp.bpm);
239
+ if (ratio !== null) {
240
+ hyp.familyId = familyId;
241
+ hyp.harmonicRatio = ratio;
242
+ family.members.push(hyp);
243
+ foundFamily = true;
244
+ break;
245
+ }
246
+ }
247
+
248
+ if (!foundFamily) {
249
+ // Create new family with this hypothesis as root
250
+ // Use deterministic family ID based on root BPM
251
+ const familyId = `fam-${Math.round(hyp.bpm)}`;
252
+ hyp.familyId = familyId;
253
+ hyp.harmonicRatio = 1.0;
254
+ families.set(familyId, { rootBpm: hyp.bpm, members: [hyp] });
255
+ }
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Normalize confidence scores to [0, 1] range.
261
+ */
262
+ function normalizeConfidence(hypotheses: TempoHypothesis[]): void {
263
+ if (hypotheses.length === 0) return;
264
+
265
+ const maxHeight = Math.max(...hypotheses.map((h) => h.evidence.peakHeight));
266
+ if (maxHeight <= 0) return;
267
+
268
+ for (const hyp of hypotheses) {
269
+ hyp.confidence = hyp.evidence.peakHeight / maxHeight;
270
+ }
271
+ }
272
+
273
+ /**
274
+ * Generate tempo hypotheses from beat candidates.
275
+ *
276
+ * Algorithm:
277
+ * 1. Compute inter-onset intervals (IOIs) from beat candidates
278
+ * 2. Filter IOIs to musically plausible range (0.2s-2.5s -> 24-300 BPM)
279
+ * 3. Build weighted histogram with configurable bin size
280
+ * 4. Extract peaks as tempo candidates
281
+ * 5. Refine BPM estimates using weighted centroid
282
+ * 6. Group into harmonic families
283
+ * 7. Normalize confidence scores
284
+ *
285
+ * @param candidates - Beat candidates from B1
286
+ * @param options - Configuration options
287
+ * @returns Tempo hypotheses with confidence and family groupings
288
+ */
289
+ export function generateTempoHypotheses(
290
+ candidates: BeatCandidate[],
291
+ options?: TempoHypothesesOptions
292
+ ): TempoHypothesesOutput {
293
+ const minBpm = options?.minBpm ?? 24;
294
+ const maxBpm = options?.maxBpm ?? 300;
295
+ const binSizeBpm = options?.binSizeBpm ?? 1.0;
296
+ const maxHypotheses = options?.maxHypotheses ?? 10;
297
+ const minConfidence = options?.minConfidence ?? 0.05;
298
+ const weightByStrength = options?.weightByStrength ?? true;
299
+ const includeHistogram = options?.includeHistogram ?? false;
300
+
301
+ // Early return if insufficient candidates
302
+ if (candidates.length < 2) {
303
+ return {
304
+ hypotheses: [],
305
+ inputCandidateCount: candidates.length,
306
+ histogram: includeHistogram
307
+ ? {
308
+ bpmBins: new Float32Array(0),
309
+ counts: new Float32Array(0),
310
+ }
311
+ : undefined,
312
+ };
313
+ }
314
+
315
+ // Step 1: Compute IOIs
316
+ const iois = computeIOIs(candidates, weightByStrength);
317
+
318
+ if (iois.length === 0) {
319
+ return {
320
+ hypotheses: [],
321
+ inputCandidateCount: candidates.length,
322
+ histogram: includeHistogram
323
+ ? {
324
+ bpmBins: new Float32Array(0),
325
+ counts: new Float32Array(0),
326
+ }
327
+ : undefined,
328
+ };
329
+ }
330
+
331
+ // Step 2-3: Build histogram (filtering happens during binning)
332
+ const { bpmBins, counts } = buildBpmHistogram(iois, minBpm, maxBpm, binSizeBpm);
333
+
334
+ // Calculate minimum height threshold based on minConfidence
335
+ const maxCount = Math.max(...counts);
336
+ const minHeight = maxCount * minConfidence;
337
+
338
+ // Step 4: Find peaks
339
+ const peakIndices = findHistogramPeaks(counts, minHeight);
340
+
341
+ // Step 5: Create hypotheses with refined BPM
342
+ const hypotheses: TempoHypothesis[] = [];
343
+
344
+ for (const peakIndex of peakIndices.slice(0, maxHypotheses * 2)) {
345
+ // Get extra for filtering
346
+ const { bpm, peakHeight, binRange, totalWeight } = refinePeakBpm(
347
+ peakIndex,
348
+ bpmBins,
349
+ counts,
350
+ binSizeBpm
351
+ );
352
+
353
+ // Skip if below confidence threshold
354
+ if (maxCount > 0 && peakHeight / maxCount < minConfidence) continue;
355
+
356
+ const evidence: TempoHypothesisEvidence = {
357
+ supportingIntervalCount: Math.round(totalWeight),
358
+ weightedSupport: totalWeight,
359
+ peakHeight,
360
+ binRange,
361
+ };
362
+
363
+ hypotheses.push({
364
+ id: "", // Will be assigned after sorting
365
+ bpm: Math.round(bpm * 10) / 10, // Round to 0.1 BPM precision
366
+ confidence: 0, // Will be normalized
367
+ evidence,
368
+ familyId: "", // Will be assigned
369
+ harmonicRatio: 1.0, // Will be assigned
370
+ });
371
+ }
372
+
373
+ // Step 6: Group into harmonic families
374
+ assignHarmonicFamilies(hypotheses);
375
+
376
+ // Step 7: Normalize confidence
377
+ normalizeConfidence(hypotheses);
378
+
379
+ // Filter by minConfidence and sort by confidence descending
380
+ const filtered = hypotheses
381
+ .filter((h) => h.confidence >= minConfidence)
382
+ .sort((a, b) => b.confidence - a.confidence)
383
+ .slice(0, maxHypotheses);
384
+
385
+ // Assign deterministic IDs based on rank
386
+ for (let i = 0; i < filtered.length; i++) {
387
+ filtered[i]!.id = `hyp-${i}`;
388
+ }
389
+
390
+ return {
391
+ hypotheses: filtered,
392
+ inputCandidateCount: candidates.length,
393
+ histogram: includeHistogram ? { bpmBins, counts } : undefined,
394
+ };
395
+ }
@@ -0,0 +1,266 @@
1
+ /**
2
+ * GPU Buffer Pool for efficient buffer reuse across MIR computations.
3
+ *
4
+ * Reduces allocation overhead by maintaining pools of buffers grouped by
5
+ * size and usage flags. Buffers are acquired for operations and released
6
+ * back to the pool for reuse.
7
+ */
8
+
9
+ export interface BufferPoolEntry {
10
+ buffer: GPUBuffer;
11
+ size: number;
12
+ usage: number;
13
+ lastUsed: number;
14
+ }
15
+
16
+ export interface BufferPoolStats {
17
+ totalBuffers: number;
18
+ activeBuffers: number;
19
+ availableBuffers: number;
20
+ poolsByUsage: Map<number, number>;
21
+ totalMemoryBytes: number;
22
+ }
23
+
24
+ /**
25
+ * Buffer pool configuration options.
26
+ */
27
+ export interface BufferPoolOptions {
28
+ /**
29
+ * Maximum number of buffers to keep in the pool per usage type.
30
+ * Default: 32
31
+ */
32
+ maxBuffersPerUsage?: number;
33
+
34
+ /**
35
+ * Time in milliseconds before an unused buffer is destroyed.
36
+ * Default: 60000 (1 minute)
37
+ */
38
+ bufferTTLMs?: number;
39
+
40
+ /**
41
+ * Whether to enable automatic cleanup of old buffers.
42
+ * Default: true
43
+ */
44
+ enableAutoCleanup?: boolean;
45
+
46
+ /**
47
+ * Interval in milliseconds for automatic cleanup.
48
+ * Default: 30000 (30 seconds)
49
+ */
50
+ cleanupIntervalMs?: number;
51
+ }
52
+
53
+ const DEFAULT_OPTIONS: Required<BufferPoolOptions> = {
54
+ maxBuffersPerUsage: 32,
55
+ bufferTTLMs: 60000,
56
+ enableAutoCleanup: true,
57
+ cleanupIntervalMs: 30000,
58
+ };
59
+
60
+ /**
61
+ * Key for indexing buffers by size and usage.
62
+ */
63
+ function makePoolKey(size: number, usage: number): string {
64
+ return `${size}-${usage}`;
65
+ }
66
+
67
+ /**
68
+ * BufferPool manages reusable GPU buffers to reduce allocation overhead.
69
+ *
70
+ * Usage:
71
+ * ```ts
72
+ * const pool = new BufferPool(gpu.device);
73
+ * const buffer = pool.acquire(1024, GPUBufferUsage.STORAGE);
74
+ * // ... use buffer ...
75
+ * pool.release(buffer);
76
+ * ```
77
+ */
78
+ export class BufferPool {
79
+ private device: GPUDevice;
80
+ private options: Required<BufferPoolOptions>;
81
+
82
+ // Available buffers indexed by size+usage key
83
+ private availableBuffers = new Map<string, BufferPoolEntry[]>();
84
+
85
+ // Currently in-use buffers (for tracking and stats)
86
+ private activeBuffers = new Set<GPUBuffer>();
87
+
88
+ // Cleanup interval handle
89
+ private cleanupInterval: ReturnType<typeof setInterval> | null = null;
90
+
91
+ constructor(device: GPUDevice, options: BufferPoolOptions = {}) {
92
+ this.device = device;
93
+ this.options = { ...DEFAULT_OPTIONS, ...options };
94
+
95
+ if (this.options.enableAutoCleanup) {
96
+ this.startAutoCleanup();
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Acquire a buffer from the pool or create a new one if none available.
102
+ *
103
+ * @param size - Byte size of the buffer
104
+ * @param usage - Buffer usage flags (e.g. GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST)
105
+ * @returns A GPUBuffer ready for use
106
+ */
107
+ acquire(size: number, usage: number): GPUBuffer {
108
+ const key = makePoolKey(size, usage);
109
+ const pool = this.availableBuffers.get(key);
110
+
111
+ // Try to reuse an existing buffer
112
+ if (pool && pool.length > 0) {
113
+ const entry = pool.pop()!;
114
+ this.activeBuffers.add(entry.buffer);
115
+ return entry.buffer;
116
+ }
117
+
118
+ // No buffer available, create a new one
119
+ const buffer = this.device.createBuffer({ size, usage });
120
+ this.activeBuffers.add(buffer);
121
+ return buffer;
122
+ }
123
+
124
+ /**
125
+ * Release a buffer back to the pool for reuse.
126
+ *
127
+ * @param buffer - The buffer to release
128
+ * @param size - Original size of the buffer
129
+ * @param usage - Original usage flags of the buffer
130
+ */
131
+ release(buffer: GPUBuffer, size: number, usage: number): void {
132
+ if (!this.activeBuffers.has(buffer)) {
133
+ console.warn("@octoseq/mir: Attempting to release buffer not tracked by pool");
134
+ return;
135
+ }
136
+
137
+ this.activeBuffers.delete(buffer);
138
+
139
+ const key = makePoolKey(size, usage);
140
+ let pool = this.availableBuffers.get(key);
141
+
142
+ if (!pool) {
143
+ pool = [];
144
+ this.availableBuffers.set(key, pool);
145
+ }
146
+
147
+ // Check if pool is full
148
+ if (pool.length >= this.options.maxBuffersPerUsage) {
149
+ // Pool is full, destroy the buffer instead of pooling it
150
+ buffer.destroy();
151
+ return;
152
+ }
153
+
154
+ // Add to pool for reuse
155
+ pool.push({
156
+ buffer,
157
+ size,
158
+ usage,
159
+ lastUsed: performance.now(),
160
+ });
161
+ }
162
+
163
+ /**
164
+ * Clean up old unused buffers that exceed the TTL.
165
+ */
166
+ cleanup(): void {
167
+ const now = performance.now();
168
+ const ttl = this.options.bufferTTLMs;
169
+
170
+ for (const [key, pool] of this.availableBuffers.entries()) {
171
+ const remaining: BufferPoolEntry[] = [];
172
+
173
+ for (const entry of pool) {
174
+ if (now - entry.lastUsed > ttl) {
175
+ // Buffer is too old, destroy it
176
+ entry.buffer.destroy();
177
+ } else {
178
+ // Keep this buffer
179
+ remaining.push(entry);
180
+ }
181
+ }
182
+
183
+ if (remaining.length === 0) {
184
+ this.availableBuffers.delete(key);
185
+ } else {
186
+ this.availableBuffers.set(key, remaining);
187
+ }
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Destroy all pooled buffers and clear the pool.
193
+ */
194
+ clear(): void {
195
+ // Destroy all available buffers
196
+ for (const pool of this.availableBuffers.values()) {
197
+ for (const entry of pool) {
198
+ entry.buffer.destroy();
199
+ }
200
+ }
201
+
202
+ this.availableBuffers.clear();
203
+
204
+ // Note: We don't destroy active buffers as they're still in use
205
+ // Callers are responsible for releasing them properly
206
+ }
207
+
208
+ /**
209
+ * Get statistics about the buffer pool.
210
+ */
211
+ getStats(): BufferPoolStats {
212
+ let totalBuffers = this.activeBuffers.size;
213
+ let totalMemoryBytes = 0;
214
+ const poolsByUsage = new Map<number, number>();
215
+
216
+ for (const pool of this.availableBuffers.values()) {
217
+ totalBuffers += pool.length;
218
+
219
+ for (const entry of pool) {
220
+ totalMemoryBytes += entry.size;
221
+
222
+ const count = poolsByUsage.get(entry.usage) || 0;
223
+ poolsByUsage.set(entry.usage, count + 1);
224
+ }
225
+ }
226
+
227
+ return {
228
+ totalBuffers,
229
+ activeBuffers: this.activeBuffers.size,
230
+ availableBuffers: totalBuffers - this.activeBuffers.size,
231
+ poolsByUsage,
232
+ totalMemoryBytes,
233
+ };
234
+ }
235
+
236
+ /**
237
+ * Start automatic cleanup of old buffers.
238
+ */
239
+ private startAutoCleanup(): void {
240
+ if (this.cleanupInterval !== null) {
241
+ return; // Already running
242
+ }
243
+
244
+ this.cleanupInterval = setInterval(() => {
245
+ this.cleanup();
246
+ }, this.options.cleanupIntervalMs);
247
+ }
248
+
249
+ /**
250
+ * Stop automatic cleanup.
251
+ */
252
+ stopAutoCleanup(): void {
253
+ if (this.cleanupInterval !== null) {
254
+ clearInterval(this.cleanupInterval);
255
+ this.cleanupInterval = null;
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Dispose of the buffer pool and destroy all buffers.
261
+ */
262
+ dispose(): void {
263
+ this.stopAutoCleanup();
264
+ this.clear();
265
+ }
266
+ }
@@ -1,20 +1,22 @@
1
+ import { BufferPool, type BufferPoolOptions, type BufferPoolStats } from "./bufferPool";
2
+
1
3
  /**
2
4
  * WebGPU context wrapper for MIR computations.
3
5
  *
4
- * v0.1 scope:
5
- * - Provide a safe, explicit way for callers to opt into GPU usage.
6
- * - Throw a clear error when called outside the browser or when WebGPU is unavailable.
6
+ * Includes buffer pooling for efficient GPU memory reuse across operations.
7
7
  */
8
8
  export class MirGPU {
9
9
  public readonly device: GPUDevice;
10
10
  public readonly queue: GPUQueue;
11
+ public readonly bufferPool: BufferPool;
11
12
 
12
- private constructor(device: GPUDevice) {
13
+ private constructor(device: GPUDevice, bufferPoolOptions?: BufferPoolOptions) {
13
14
  this.device = device;
14
15
  this.queue = device.queue;
16
+ this.bufferPool = new BufferPool(device, bufferPoolOptions);
15
17
  }
16
18
 
17
- static async create(): Promise<MirGPU> {
19
+ static async create(bufferPoolOptions?: BufferPoolOptions): Promise<MirGPU> {
18
20
  // Next.js note: callers must create MirGPU from a client component.
19
21
  if (typeof navigator === "undefined") {
20
22
  throw new Error(
@@ -39,6 +41,28 @@ export class MirGPU {
39
41
  // We keep this minimal: no required features for v0.1.
40
42
  const device = await adapter.requestDevice();
41
43
 
42
- return new MirGPU(device);
44
+ return new MirGPU(device, bufferPoolOptions);
45
+ }
46
+
47
+ /**
48
+ * Get statistics about buffer pool usage.
49
+ */
50
+ getBufferPoolStats(): BufferPoolStats {
51
+ return this.bufferPool.getStats();
52
+ }
53
+
54
+ /**
55
+ * Manually trigger buffer pool cleanup (normally runs automatically).
56
+ */
57
+ cleanupBufferPool(): void {
58
+ this.bufferPool.cleanup();
59
+ }
60
+
61
+ /**
62
+ * Dispose of the GPU context and clean up all resources.
63
+ */
64
+ dispose(): void {
65
+ this.bufferPool.dispose();
66
+ // Note: GPUDevice doesn't have a dispose method, but we clean up our resources
43
67
  }
44
68
  }