audio 2.0.0-1 → 2.1.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/cache.js ADDED
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Page cache — LRU eviction to OPFS and lazy restore.
3
+ * Self-registers on import — exposes opfsCache/evict/ensurePages on audio.
4
+ */
5
+
6
+ import audio from './core.js'
7
+
8
+ const DEFAULT_BUDGET = 500 * 1024 * 1024 // 500MB
9
+
10
+ /** Evict pages to cache until resident bytes fit within budget. True LRU. */
11
+ async function evict(a) {
12
+ if (!a.cache || a.budget === Infinity) return
13
+ let bytes = p => p ? p.reduce((s, ch) => s + ch.byteLength, 0) : 0
14
+ let current = a.pages.reduce((sum, p) => sum + bytes(p), 0)
15
+ if (current <= a.budget) return
16
+ // Build eviction order: LRU (oldest first) if tracked, else FIFO fallback
17
+ let order = a._.lru && a._.lru.size
18
+ ? [...a._.lru]
19
+ : a.pages.map((_, i) => i)
20
+ for (let i of order) {
21
+ if (current <= a.budget) break
22
+ if (!a.pages[i]) continue
23
+ await a.cache.write(i, a.pages[i])
24
+ current -= bytes(a.pages[i])
25
+ a.pages[i] = null
26
+ if (a._.lru) a._.lru.delete(i)
27
+ }
28
+ }
29
+
30
+ /** Restore evicted pages covering a sample range from cache. */
31
+ async function ensurePages(a, offset, duration) {
32
+ if (!a.cache) return
33
+ let PS = audio.PAGE_SIZE, sr = a.sampleRate
34
+ let s = offset != null ? Math.max(0, Math.round(offset * sr)) : 0
35
+ let len = duration != null ? Math.round(duration * sr) : a._.len - s
36
+ let p0 = Math.floor(s / PS), pEnd = Math.min(Math.ceil((s + len) / PS), a.pages.length)
37
+ for (let i = p0; i < pEnd; i++)
38
+ if (a.pages[i] === null && await a.cache.has(i)) a.pages[i] = await a.cache.read(i)
39
+ }
40
+
41
+ /** Create an OPFS-backed cache backend. Browser only. */
42
+ async function opfsCache(dirName = 'audio-cache') {
43
+ if (typeof navigator === 'undefined' || !navigator.storage?.getDirectory)
44
+ throw new Error('OPFS not available in this environment')
45
+ let root = await navigator.storage.getDirectory()
46
+ let dir = await root.getDirectoryHandle(dirName, { create: true })
47
+
48
+ return {
49
+ async read(i) {
50
+ let handle = await dir.getFileHandle(`p${i}`)
51
+ let file = await handle.getFile()
52
+ let buf = await file.arrayBuffer()
53
+ let view = new Float32Array(buf)
54
+ let ch = view[0] | 0, samplesPerCh = ((view.length - 1) / ch) | 0
55
+ let data = []
56
+ for (let c = 0; c < ch; c++) data.push(view.slice(1 + c * samplesPerCh, 1 + (c + 1) * samplesPerCh))
57
+ return data
58
+ },
59
+ async write(i, data) {
60
+ let handle = await dir.getFileHandle(`p${i}`, { create: true })
61
+ let writable = await handle.createWritable()
62
+ let total = 1 + data.reduce((s, ch) => s + ch.length, 0)
63
+ let packed = new Float32Array(total)
64
+ packed[0] = data.length
65
+ let off = 1
66
+ for (let ch of data) { packed.set(ch, off); off += ch.length }
67
+ await writable.write(packed.buffer)
68
+ await writable.close()
69
+ },
70
+ has(i) {
71
+ return dir.getFileHandle(`p${i}`).then(() => true, () => false)
72
+ },
73
+ async evict(i) {
74
+ try { await dir.removeEntry(`p${i}`) } catch {}
75
+ },
76
+ async clear() {
77
+ for await (let [name] of dir) await dir.removeEntry(name)
78
+ }
79
+ }
80
+ }
81
+
82
+
83
+ // ── Self-register ────────────────────────────────────────────────
84
+
85
+
86
+ audio.opfsCache = opfsCache
87
+ audio.evict = evict
88
+ audio.ensurePages = ensurePages
89
+ audio.DEFAULT_BUDGET = DEFAULT_BUDGET