audio 2.0.0-0 → 2.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/fn/write.js ADDED
@@ -0,0 +1,15 @@
1
+ import { opRange } from '../plan.js'
2
+
3
+ const write = (chs, ctx) => {
4
+ let data = ctx.args[0]
5
+ let [p, end] = opRange(ctx, chs[0].length)
6
+ let srcOff = Math.max(0, -p), dstOff = Math.max(0, p)
7
+ for (let c = 0; c < chs.length; c++) {
8
+ let s = Array.isArray(data) ? (data[c] || data[0]) : data
9
+ for (let i = srcOff; i < s.length && dstOff + i - srcOff < end; i++) chs[c][dstOff + i - srcOff] = s[i]
10
+ }
11
+ return chs
12
+ }
13
+
14
+ import audio from '../core.js'
15
+ audio.op('write', { process: write })
@@ -1,21 +1,21 @@
1
- The MIT License (MIT)
2
-
3
- Copyright (c) 2016 Jamen Marzonie <jamenmarz@gmail.com> (http://jamenmarz.com)
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in
13
- all copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
- THE SOFTWARE.
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Jamen Marzonie <jamenmarz@gmail.com> (http://jamenmarz.com)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/package.json CHANGED
@@ -1,30 +1,75 @@
1
1
  {
2
2
  "name": "audio",
3
- "version": "2.0.0-0",
4
- "description": "Digital audio in JavaScript.",
5
- "repository": "audiojs/audio",
6
- "main": "index.js",
3
+ "version": "2.0.0",
4
+ "description": "Audio loading, editing, and rendering for JavaScript",
5
+ "type": "module",
6
+ "main": "audio.js",
7
+ "exports": {
8
+ ".": {
9
+ "browser": "./dist/audio.js",
10
+ "default": "./audio.js"
11
+ },
12
+ "./core": "./core.js",
13
+ "./plan": "./plan.js",
14
+ "./stats": "./stats.js",
15
+ "./cache": "./cache.js",
16
+ "./fn/*": "./fn/*"
17
+ },
18
+ "types": "./audio.d.ts",
19
+ "bin": "./bin/cli.js",
7
20
  "license": "MIT",
21
+ "repository": "audiojs/audio",
8
22
  "files": [
9
- "index.js"
23
+ "core.js",
24
+ "audio.js",
25
+ "audio.d.ts",
26
+ "plan.js",
27
+ "stats.js",
28
+ "cache.js",
29
+ "fn/",
30
+ "bin/",
31
+ "dist/"
10
32
  ],
11
- "scripts": {
12
- "test": "eslint index.js test && tape 'test/*.js' | tap-spec"
13
- },
14
- "devDependencies": {
15
- "eslint": "^2.11.0",
16
- "eslint-config-google": "^0.5.0",
17
- "tap-spec": "^4.1.1",
18
- "tape": "^4.5.1"
19
- },
20
33
  "keywords": [
21
34
  "audiojs",
22
35
  "audio",
23
- "javascript",
24
- "pcm",
25
- "lpcm"
36
+ "dsp",
37
+ "pcm"
26
38
  ],
27
- "engines": {
28
- "node": ">=0.11"
39
+ "scripts": {
40
+ "build": "node .esbuild.js",
41
+ "test": "node test/index.js",
42
+ "test:cli": "TST_PARALLEL=3 node test/cli.js",
43
+ "test:browser": "node test/browser.js",
44
+ "test:all": "CI=1 npm test && CI=1 npm run test:cli && npm run test:browser",
45
+ "version": "node -p \"var s=require('fs'),v=require('./package.json').version;s.writeFileSync('core.js',s.readFileSync('core.js','utf8').replace(/audio\\.version = '[^']+'/,'audio.version = \\''+v+'\\''));''\" && git add core.js",
46
+ "prepublishOnly": "npm run version && npm run test:all",
47
+ "serve": "node test/serve.js",
48
+ "demo": "vhs player.tape"
49
+ },
50
+ "dependencies": {
51
+ "@audio/decode-aiff": "^1.1.0",
52
+ "@audio/decode-caf": "^1.1.0",
53
+ "@audio/decode-wav": "^1.1.0",
54
+ "@audio/encode-mp3": "^1.0.1",
55
+ "a-weighting": "^2.0.1",
56
+ "audio-decode": "^3.8.1",
57
+ "audio-filter": "^2.2.2",
58
+ "audio-speaker": "^2.1.1",
59
+ "audio-type": "^2.4.1",
60
+ "encode-audio": "^1.2.2",
61
+ "fourier-transform": "^2.2.0",
62
+ "parse-duration": "^2.1.6",
63
+ "pcm-convert": "^3.1.1",
64
+ "window-function": "^3.0.1"
65
+ },
66
+ "optionalDependencies": {
67
+ "audio-mic": "^1.0.0"
68
+ },
69
+ "devDependencies": {
70
+ "audio-lena": "^3.0.0",
71
+ "esbuild": "^0.28.0",
72
+ "playwright": "^1.59.1",
73
+ "tst": "^9.4.0"
29
74
  }
30
75
  }
package/plan.js ADDED
@@ -0,0 +1,456 @@
1
+ /**
2
+ * Plan — non-destructive edit pipeline.
3
+ * Intercepts create/run/read/stream to track and materialize edits.
4
+ */
5
+
6
+ import audio, { readPages, copyPages, walkPages, parseTime, LOAD, READ, emit } from './core.js'
7
+
8
+ let fn = audio.fn
9
+ let ops = {}
10
+
11
+ // ── Segments: [src, count, dst, rate?, ref?] ────────────────────
12
+ export function seg(src, count, dst, rate, ref) {
13
+ let s = [src, count, dst]
14
+ if (rate != null && rate !== 1) s[3] = rate
15
+ if (ref !== undefined) s[4] = ref
16
+ return s
17
+ }
18
+
19
+ // ── Range Helpers ────────────────────────────────────────────────
20
+
21
+ /** Normalize an offset in samples: negative = from-end, clamp to [0, total]. dflt used when offset is null. */
22
+ export function planOffset(offset, total, dflt = 0) {
23
+ let s = offset ?? dflt
24
+ if (s < 0) s = total + s
25
+ return Math.min(Math.max(0, s), total)
26
+ }
27
+
28
+ /** Compute [start, end] sample range from a process ctx (at/duration) over a buffer of given len. */
29
+ export function opRange(ctx, len) {
30
+ let sr = ctx.sampleRate
31
+ let s = ctx.at != null ? Math.round(ctx.at * sr) : 0
32
+ return [s, ctx.duration != null ? s + Math.round(ctx.duration * sr) : len]
33
+ }
34
+
35
+ // ── Op Registration ─────────────────────────────────────────────
36
+
37
+ function isOpts(v) {
38
+ return v != null && typeof v === 'object' && !Array.isArray(v) && !ArrayBuffer.isView(v) && !v.pages && !v.getChannelData
39
+ }
40
+
41
+ /** Register/query op: audio.op(name, descriptor|process) */
42
+ audio.op = function(name, arg1, arg2, arg3) {
43
+ if (!arguments.length) return ops
44
+ if (arguments.length === 1) return ops[name]
45
+
46
+ // Normalize to descriptor object
47
+ let desc
48
+ if (typeof arg1 !== 'function') {
49
+ desc = arg1
50
+ } else {
51
+ // Legacy positional: audio.op(name, process, plan?, opts?)
52
+ let plan, opts
53
+ if (typeof arg2 === 'function') { plan = arg2; opts = arg3 }
54
+ else opts = arg2
55
+ desc = { process: arg1 }
56
+ if (plan) desc.plan = plan
57
+ if (opts) Object.assign(desc, opts)
58
+ }
59
+
60
+ if (!fn[name] && !desc.hidden) {
61
+ let stdMethod = function(...a) {
62
+ let edit = { type: name, args: a }, last = a[a.length - 1]
63
+ if (a.length && isOpts(last)) {
64
+ let { at, duration, channel, offset, length, ...extra } = last
65
+ edit.args = a.slice(0, -1)
66
+ if (at != null) edit.at = parseTime(at)
67
+ if (duration != null) edit.duration = parseTime(duration)
68
+ if (offset != null) edit.offset = offset
69
+ if (length != null) edit.length = length
70
+ if (channel != null) edit.channel = channel
71
+ Object.assign(edit, extra)
72
+ }
73
+ return this.run(edit)
74
+ }
75
+ fn[name] = desc.call
76
+ ? function(...a) { return desc.call.call(this, stdMethod, ...a) }
77
+ : stdMethod
78
+ }
79
+ ops[name] = desc
80
+ }
81
+
82
+
83
+ // ── Edit Tracking ───────────────────────────────────────────────
84
+
85
+ /** Push an edit, bump version, notify. */
86
+ export function pushEdit(a, edit) {
87
+ a.edits.push(edit)
88
+ a.version++
89
+ emit(a, 'change')
90
+ }
91
+
92
+ /** Pop an edit, bump version, notify. */
93
+ export function popEdit(a) {
94
+ let e = a.edits.pop()
95
+ if (e) { a.version++; emit(a, 'change') }
96
+ return e
97
+ }
98
+
99
+
100
+ // ── Virtual Length/Channels ─────────────────────────────────────
101
+
102
+ Object.defineProperties(fn, {
103
+ length: { get() {
104
+ if (this._.lenV === this.version) return this._.lenC
105
+ let len = this.edits.length ? buildPlan(this).totalLen : this._.len
106
+ this._.lenC = len; this._.lenV = this.version
107
+ return len
108
+ }, configurable: true },
109
+ channels: { get() {
110
+ if (this._.chV === this.version) return this._.chC
111
+ let ch = this._.ch
112
+ for (let edit of this.edits) { if (ops[edit.type]?.ch) ch = ops[edit.type].ch(ch, edit.args) }
113
+ this._.chC = ch; this._.chV = this.version
114
+ return ch
115
+ }, configurable: true },
116
+ })
117
+
118
+
119
+ // ── Read ───────────────────────────────────────────────────────────────
120
+
121
+ /** Ensure cache pages for the source ranges a plan will access. */
122
+ export async function ensurePlan(a, plan, offset, duration) {
123
+ if (!audio.ensurePages) return
124
+ let { segs, sr } = plan
125
+ let s = Math.round((offset || 0) * sr)
126
+ let e = duration != null ? s + Math.round(duration * sr) : plan.totalLen
127
+ for (let sg of segs) {
128
+ let iStart = Math.max(s, sg[2]), iEnd = Math.min(e, sg[2] + sg[1])
129
+ if (iStart >= iEnd) continue
130
+ let absR = Math.abs(sg[3] || 1)
131
+ let srcStart = sg[0] + (iStart - sg[2]) * absR
132
+ let srcLen = (iEnd - iStart) * absR + 1
133
+ let target = sg[4] === null ? null : sg[4] || a
134
+ if (target) await audio.ensurePages(target, srcStart / sr, srcLen / sr)
135
+ }
136
+ }
137
+
138
+ async function loadRefs(a) {
139
+ for (let { args } of a.edits) if (args?.[0]?.pages) await args[0][LOAD]()
140
+ }
141
+
142
+ fn[READ] = async function(offset, duration) {
143
+ if (!this.edits.length) {
144
+ if (audio.ensurePages) await audio.ensurePages(this, offset, duration)
145
+ return readPages(this, offset, duration)
146
+ }
147
+ await this[LOAD]()
148
+ await loadRefs(this)
149
+
150
+ let plan = buildPlan(this)
151
+ await ensurePlan(this, plan, offset, duration)
152
+ return readPlan(this, plan, offset, duration)
153
+ }
154
+
155
+
156
+ // ── Stream ─────────────────────────────────────────────────────
157
+
158
+ fn[Symbol.asyncIterator] = fn.stream = async function*(opts) {
159
+ let offset = parseTime(opts?.at), duration = parseTime(opts?.duration)
160
+ // Live decode streaming (no edits, still decoding)
161
+ // Position-based: reads from full pages (zero-copy) or partial buffer (copied).
162
+ // Granularity = decoder chunk size, NOT page size. Pages are for memory, not streaming.
163
+ if (this._.waiters && !this.decoded && !this.edits.length) {
164
+ let sr = this.sampleRate, acc = this._.acc, PS = audio.PAGE_SIZE
165
+ let startSample = offset ? Math.round(offset * sr) : 0
166
+ let endSample = duration != null ? startSample + Math.round(duration * sr) : Infinity
167
+ let pos = startSample
168
+ while (pos < endSample) {
169
+ let available = acc ? this.pages.length * PS + acc.partialLen : this._.len
170
+ while (pos >= available && !this.decoded) {
171
+ await new Promise(r => this._.waiters.push(r))
172
+ available = acc ? this.pages.length * PS + acc.partialLen : this._.len
173
+ }
174
+ if (pos >= available) break
175
+ let end = Math.min(endSample, available)
176
+ let pi = Math.floor(pos / PS), po = pos % PS
177
+ if (pi < this.pages.length) {
178
+ let page = this.pages[pi], e = Math.min(page[0].length - po, end - pos)
179
+ yield page.map(ch => ch.subarray(po, po + e))
180
+ pos += e
181
+ } else if (acc) {
182
+ let e = Math.min(acc.partialLen - po, end - pos)
183
+ if (e > 0) { yield acc.partial.map(ch => ch.subarray(po, po + e).slice()); pos += e }
184
+ }
185
+ }
186
+ return
187
+ }
188
+
189
+ // Edit-aware streaming (plan-based)
190
+ await this.ready
191
+ await this[LOAD]()
192
+ await loadRefs(this)
193
+ let plan = buildPlan(this)
194
+ let seen = new Set()
195
+ for (let s of plan.segs) if (s[4] && s[4] !== null && !seen.has(s[4])) { seen.add(s[4]); await s[4][LOAD]() }
196
+ await ensurePlan(this, plan, offset, duration)
197
+ for (let chunk of streamPlan(this, plan, offset, duration)) yield chunk
198
+ }
199
+
200
+
201
+ // ── API ────────────────────────────────────────────────────────
202
+
203
+ fn.undo = function(n = 1) {
204
+ if (!this.edits.length) return n === 1 ? null : []
205
+ let removed = []
206
+ for (let i = 0; i < n && this.edits.length; i++) removed.push(popEdit(this))
207
+ return n === 1 ? removed[0] : removed
208
+ }
209
+
210
+ fn.run = function(...edits) {
211
+ let sr = this.sampleRate
212
+ for (let e of edits) {
213
+ if (!e.type) throw new TypeError('audio.run: edit must have type')
214
+ let edit = { ...e, args: e.args || [] }
215
+ if (edit.at != null) edit.at = parseTime(edit.at)
216
+ if (edit.duration != null) edit.duration = parseTime(edit.duration)
217
+ if (edit.offset != null) { edit.at = edit.offset / sr; delete edit.offset }
218
+ if (edit.length != null) { edit.duration = edit.length / sr; delete edit.length }
219
+ pushEdit(this, edit)
220
+ }
221
+ return this
222
+ }
223
+
224
+ fn.toJSON = function() {
225
+ let edits = this.edits.filter(e => !e.args?.some(a => typeof a === 'function'))
226
+ return { source: this.source, edits, sampleRate: this.sampleRate, channels: this.channels, duration: this.duration }
227
+ }
228
+
229
+ fn.clone = function() {
230
+ let b = audio.from(this)
231
+ for (let e of this.edits) pushEdit(b, { ...e })
232
+ return b
233
+ }
234
+
235
+
236
+ // ── Render Engine ──────────────────────────────────────────────
237
+
238
+ const MAX_FLAT_SIZE = 2 ** 29
239
+
240
+ /** Get sample length from any source type. */
241
+ export function srcLen(s) {
242
+ return Array.isArray(s) ? s[0].length : s?.getChannelData ? s.length : s._.len
243
+ }
244
+
245
+ /** Render all edits into flat PCM, or read a slice. For ctx.render in PCM ops. */
246
+ export function render(a, offset, count) {
247
+ // Raw Float32Array[]
248
+ if (Array.isArray(a) && a[0] instanceof Float32Array) {
249
+ return offset != null ? a.map(ch => ch.subarray(offset, offset + count)) : a
250
+ }
251
+ // AudioBuffer
252
+ if (a?.getChannelData && !a.pages) {
253
+ let chs = Array.from({ length: a.numberOfChannels }, (_, i) => new Float32Array(a.getChannelData(i)))
254
+ return offset != null ? chs.map(ch => ch.subarray(offset, offset + count)) : chs
255
+ }
256
+ if (offset != null) return readRange(a, offset, count)
257
+ if (a._.pcm && a._.pcmV === a.version) return a._.pcm
258
+ if (!a.edits.length) { let r = readPages(a); a._.pcm = r; a._.pcmV = a.version; return r }
259
+ let plan = buildPlan(a)
260
+ let virtualLen = planLen(plan.segs)
261
+ if (virtualLen > MAX_FLAT_SIZE) throw new Error(`Audio too large for flat render (${(virtualLen / 1e6).toFixed(0)}M samples). Use streaming.`)
262
+ let r = readPlan(a, plan)
263
+ a._.pcm = r; a._.pcmV = a.version
264
+ return r
265
+ }
266
+
267
+ function planLen(segs) { let m = 0; for (let s of segs) m = Math.max(m, s[2] + s[1]); return m }
268
+
269
+ /** Build a read plan from edit list. Always succeeds — every op is plannable. */
270
+ export function buildPlan(a) {
271
+ if (a._.plan && a._.planV === a.version) return a._.plan
272
+ let sr = a.sampleRate, ch = a._.ch
273
+ let segs = [[0, a._.len, 0]], pipeline = []
274
+
275
+ for (let edit of a.edits) {
276
+ let { type, args = [], at, duration, channel, ...extra } = edit
277
+ let op = ops[type]
278
+ if (!op) throw new Error(`Unknown op: ${type}`)
279
+
280
+ // resolve: try stats-aware replacement first
281
+ if (op.resolve) {
282
+ let ctx = { ...extra, stats: a._.srcStats || a.stats, sampleRate: sr, channelCount: ch, channel, at, duration, totalDuration: planLen(segs) / sr }
283
+ let resolved = op.resolve(args, ctx)
284
+ if (resolved === false) continue
285
+ if (resolved) {
286
+ let edits = Array.isArray(resolved) ? resolved : [resolved]
287
+ for (let r of edits) {
288
+ if (channel != null && r.channel == null) r.channel = channel
289
+ if (at != null && r.at == null) r.at = at
290
+ if (duration != null && r.duration == null) r.duration = duration
291
+ let rOp = ops[r.type]
292
+ if (rOp?.plan && typeof rOp.plan === 'function') {
293
+ let t = planLen(segs), rOffset = r.at != null ? Math.round(r.at * sr) : null, rLength = r.duration != null ? Math.round(r.duration * sr) : null
294
+ segs = rOp.plan(segs, { total: t, sampleRate: sr, args: r.args || [], offset: rOffset, length: rLength })
295
+ } else {
296
+ pipeline.push(r)
297
+ }
298
+ }
299
+ continue
300
+ }
301
+ // resolved null — fall through to plan or per-page
302
+ }
303
+
304
+ // plan: structural segment rewrite
305
+ if (op.plan) {
306
+ let t = planLen(segs), offset = at != null ? Math.round(at * sr) : null, length = duration != null ? Math.round(duration * sr) : null
307
+ segs = op.plan(segs, { total: t, sampleRate: sr, args, offset, length })
308
+ } else {
309
+ pipeline.push(edit)
310
+ }
311
+ }
312
+ let plan = { segs, pipeline, totalLen: planLen(segs), sr }
313
+ a._.plan = plan; a._.planV = a.version
314
+ return plan
315
+ }
316
+
317
+
318
+ // ── Plan Execution ─────────────────────────────────────────────
319
+
320
+ // Reusable resample buffer — avoids GC pressure during playback at non-unit rates
321
+ let _rsBuf = null, _rsLen = 0
322
+
323
+ /** Read channel samples from pages, resampled by rate. */
324
+ function readSource(a, c, srcOff, n, target, tOff, rate) {
325
+ let r = rate || 1, absR = Math.abs(r)
326
+ if (absR === 1) {
327
+ if (r > 0) return copyPages(a, c, srcOff, n, target, tOff)
328
+ return walkPages(a, c, srcOff, n, (pg, ch, s, e, off) => {
329
+ for (let i = s; i < e; i++) target[tOff + (n - 1 - (off + i - s))] = pg[ch][i]
330
+ })
331
+ }
332
+ let srcN = Math.ceil(n * absR) + 1
333
+ if (srcN > _rsLen) { _rsLen = srcN; _rsBuf = new Float32Array(srcN) }
334
+ let buf = _rsBuf.subarray(0, srcN)
335
+ buf.fill(0)
336
+ copyPages(a, c, srcOff, srcN, buf, 0)
337
+ resample(buf, target, tOff, n, r)
338
+ }
339
+
340
+ /** Linear interpolation resample: src buffer → n output samples at given rate. */
341
+ function resample(src, target, tOff, n, rate) {
342
+ let absR = Math.abs(rate), rev = rate < 0
343
+ for (let i = 0; i < n; i++) {
344
+ let pos = (rev ? n - 1 - i : i) * absR
345
+ let idx = pos | 0, frac = pos - idx
346
+ target[tOff + i] = idx + 1 < src.length
347
+ ? src[idx] + (src[idx + 1] - src[idx]) * frac
348
+ : src[idx] || 0
349
+ }
350
+ }
351
+
352
+ /** Read a sample range from an audio instance (handles edits via plan). */
353
+ function readRange(a, srcStart, n) {
354
+ if (!a.edits.length) {
355
+ return Array.from({ length: a._.ch }, (_, c) => {
356
+ let out = new Float32Array(n)
357
+ copyPages(a, c, srcStart, n, out, 0)
358
+ return out
359
+ })
360
+ }
361
+ let plan = buildPlan(a), sr = plan.sr
362
+ return readPlan(a, plan, srcStart / sr, n / sr)
363
+ }
364
+
365
+ /** Stream chunks from a read plan. */
366
+ export function* streamPlan(a, plan, offset, duration) {
367
+ let { segs, pipeline, totalLen, sr } = plan
368
+ let s = Math.round((offset || 0) * sr), e = duration != null ? s + Math.round(duration * sr) : totalLen
369
+
370
+ let totalDur = totalLen / sr
371
+ let procs = pipeline.map(ed => {
372
+ let m = ops[ed.type]
373
+ let { type, args, at, duration, channel, ...extra } = ed
374
+ return {
375
+ op: m.process,
376
+ at: at != null && at < 0 ? totalDur + at : at,
377
+ dur: duration,
378
+ channel,
379
+ ctx: { ...extra, args: args || [], duration, sampleRate: sr, totalDuration: totalDur, render }
380
+ }
381
+ })
382
+
383
+ // Warm up stateful ops (filters) when seeking — render prior blocks silently to settle IIR state
384
+ let WARMUP = 8 // ~185ms at 44.1kHz — enough for most IIR filters to settle
385
+ let ws = (s > 0 && procs.length) ? Math.max(0, s - audio.BLOCK_SIZE * WARMUP) : s
386
+
387
+ for (let outOff = ws; outOff < e; outOff += audio.BLOCK_SIZE) {
388
+ let blockEnd = outOff < s ? s : e
389
+ let len = Math.min(audio.BLOCK_SIZE, blockEnd - outOff)
390
+ let chunk = Array.from({ length: a._.ch }, () => new Float32Array(len))
391
+
392
+ for (let sg of segs) {
393
+ let iStart = Math.max(outOff, sg[2]), iEnd = Math.min(outOff + len, sg[2] + sg[1])
394
+ if (iStart >= iEnd) continue
395
+ let rate = sg[3] || 1, ref = sg[4], absR = Math.abs(rate)
396
+ let n = iEnd - iStart, dstOff = iStart - outOff
397
+ // For negative rate, read from the far end of the source range so reversal is globally correct across blocks
398
+ let srcStart = rate < 0
399
+ ? sg[0] + (sg[1] - (iStart - sg[2]) - n) * absR
400
+ : sg[0] + (iStart - sg[2]) * absR
401
+ if (ref === null) {
402
+ // zero-filled by default
403
+ } else if (ref) {
404
+ if (ref.edits.length === 0) {
405
+ for (let c = 0; c < a._.ch; c++)
406
+ readSource(ref, c % ref._.ch, srcStart, n, chunk[c], dstOff, rate)
407
+ } else {
408
+ let srcN = Math.ceil(n * absR) + 1
409
+ let srcPcm = readRange(ref, srcStart, srcN)
410
+ for (let c = 0; c < a._.ch; c++) {
411
+ let src = srcPcm[c % srcPcm.length]
412
+ if (absR === 1) {
413
+ if (rate < 0) { for (let i = 0; i < n; i++) chunk[c][dstOff + i] = src[n - 1 - i] }
414
+ else chunk[c].set(src.subarray(0, n), dstOff)
415
+ } else resample(src, chunk[c], dstOff, n, rate)
416
+ }
417
+ }
418
+ } else {
419
+ for (let c = 0; c < a._.ch; c++) readSource(a, c, srcStart, n, chunk[c], dstOff, rate)
420
+ }
421
+ }
422
+
423
+ let blockOff = outOff / sr
424
+ for (let proc of procs) {
425
+ let { op, at, channel, ctx } = proc
426
+ if (!op) continue
427
+ ctx.at = at != null ? at - blockOff : undefined
428
+ ctx.blockOffset = blockOff
429
+
430
+ if (channel != null) {
431
+ let chs = typeof channel === 'number' ? [channel] : channel
432
+ let sub = chs.map(c => chunk[c])
433
+ let result = op(sub, ctx)
434
+ if (result && result !== false) for (let i = 0; i < chs.length; i++) chunk[chs[i]] = result[i]
435
+ } else {
436
+ let result = op(chunk, ctx)
437
+ if (result === false || result === null) continue
438
+ if (result) chunk = result
439
+ }
440
+ }
441
+
442
+ if (outOff >= s) yield chunk
443
+ }
444
+ }
445
+
446
+ function readPlan(a, plan, offset, duration) {
447
+ let chunks = []
448
+ for (let chunk of streamPlan(a, plan, offset, duration)) chunks.push(chunk)
449
+ if (!chunks.length) return Array.from({ length: a.channels }, () => new Float32Array(0))
450
+ let ch = chunks[0].length, totalLen = chunks.reduce((n, c) => n + c[0].length, 0)
451
+ return Array.from({ length: ch }, (_, c) => {
452
+ let out = new Float32Array(totalLen), pos = 0
453
+ for (let chunk of chunks) { out.set(chunk[c], pos); pos += chunk[0].length }
454
+ return out
455
+ })
456
+ }