audio 2.0.0-1 → 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/stats.js ADDED
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Stats engine — block-level stat computation + unified stat query.
3
+ * Self-registers on import — exposes statSession on audio, adds fn.stat.
4
+ */
5
+
6
+ import audio, { parseTime, LOAD } from './core.js'
7
+ import { buildPlan, streamPlan, ensurePlan } from './plan.js'
8
+
9
+ // ── Stat descriptor registry ────────────────────────────────────
10
+
11
+ let statDefs = {}
12
+
13
+ /** Register/query stat: audio.stat(), audio.stat(name), audio.stat(name, descriptor|blockFn) */
14
+ audio.stat = function(name, desc) {
15
+ if (!arguments.length) return statDefs
16
+ if (arguments.length === 1) return statDefs[name]
17
+ if (typeof desc === 'function') desc = { block: desc }
18
+ statDefs[name] = desc
19
+ }
20
+
21
+ /** Create a stat computation session. ch inferred from first .page() call. */
22
+ function statSession(sr) {
23
+ let fns, acc, ch, last = 0, rem = null, remLen = 0
24
+
25
+ function init(c) {
26
+ ch = c
27
+ fns = Object.entries(audio.stat())
28
+ .filter(([_, d]) => d.block)
29
+ .map(([name, d]) => ({ name, fn: d.block, ctx: { sampleRate: sr } }))
30
+ acc = Object.create(null)
31
+ for (let { name } of fns) acc[name] = Array.from({ length: ch }, () => [])
32
+ }
33
+
34
+ function processBlock(block) {
35
+ for (let { name, fn, ctx } of fns) {
36
+ let v = fn(block, ctx)
37
+ if (typeof v === 'number') for (let c = 0; c < ch; c++) acc[name][c].push(v)
38
+ else for (let c = 0; c < ch; c++) acc[name][c].push(v[c])
39
+ }
40
+ }
41
+
42
+ return {
43
+ page(page) {
44
+ if (!acc) init(page.length)
45
+ let BS = audio.BLOCK_SIZE, off = 0, len = page[0].length
46
+
47
+ // Complete partial remainder from previous push
48
+ if (remLen > 0) {
49
+ let need = BS - remLen
50
+ if (len >= need) {
51
+ for (let c = 0; c < ch; c++) rem[c].set(page[c].subarray(0, need), remLen)
52
+ processBlock(rem)
53
+ off = need
54
+ remLen = 0
55
+ } else {
56
+ for (let c = 0; c < ch; c++) rem[c].set(page[c].subarray(0, len), remLen)
57
+ remLen += len
58
+ return this
59
+ }
60
+ }
61
+
62
+ // Process full blocks
63
+ while (off + BS <= len) {
64
+ processBlock(Array.from({ length: ch }, (_, c) => page[c].subarray(off, off + BS)))
65
+ off += BS
66
+ }
67
+
68
+ // Buffer remainder
69
+ if (off < len) {
70
+ if (!rem) rem = Array.from({ length: ch }, () => new Float32Array(BS))
71
+ for (let c = 0; c < ch; c++) rem[c].set(page[c].subarray(off))
72
+ remLen = len - off
73
+ }
74
+
75
+ return this
76
+ },
77
+ /** Flush any buffered partial block as a short final block. */
78
+ flush() {
79
+ if (remLen > 0) {
80
+ processBlock(Array.from({ length: ch }, (_, c) => rem[c].subarray(0, remLen)))
81
+ remLen = 0
82
+ }
83
+ },
84
+ delta() {
85
+ if (!acc) return
86
+ let firstKey = Object.keys(acc)[0]
87
+ if (!firstKey) return
88
+ let cur = acc[firstKey][0].length
89
+ if (cur <= last) return
90
+ let d = { fromBlock: last }
91
+ for (let name in acc) d[name] = acc[name].map(a => new Float32Array(a.slice(last)))
92
+ last = cur
93
+ return d
94
+ },
95
+ done() {
96
+ this.flush()
97
+ let out = { blockSize: audio.BLOCK_SIZE }
98
+ if (acc) for (let name in acc) out[name] = acc[name].map(a => new Float32Array(a))
99
+ return out
100
+ }
101
+ }
102
+ }
103
+
104
+ // ── Bin reduction ────────────────────────────────────────────────
105
+
106
+ function binReduce(src, from, to, bins, reduce) {
107
+ if (bins <= 0 || to <= from) return new Float32Array(Math.max(0, bins))
108
+ from = Math.max(0, from); to = Math.min(to, src.length)
109
+ if (to <= from) return new Float32Array(bins)
110
+ let out = new Float32Array(bins), bpp = (to - from) / bins
111
+ for (let i = 0; i < bins; i++) {
112
+ let a = from + Math.floor(i * bpp), b = Math.min(from + Math.floor((i + 1) * bpp), to)
113
+ if (b <= a) b = a + 1
114
+ out[i] = reduce(src, a, b)
115
+ }
116
+ return out
117
+ }
118
+
119
+
120
+ /** Remap source stats by segment layout (plan-only edits, no sample pipeline).
121
+ * Falls back to null if segments are too complex to remap cheaply. */
122
+ function remapStats(srcStats, plan, sr) {
123
+ let bs = srcStats.blockSize, segs = plan.segs, totalLen = plan.totalLen
124
+ // Check feasibility: only self-ref (undefined) and silence (null), rate ±1
125
+ for (let s of segs) {
126
+ let rate = s[3] || 1, ref = s[4]
127
+ if (ref !== undefined && ref !== null) return null // external ref
128
+ if (Math.abs(rate) !== 1) return null // resampled
129
+ if (s[0] % bs !== 0 || s[2] % bs !== 0) return null // unaligned — force recompute
130
+ }
131
+ let outBlocks = Math.ceil(totalLen / bs)
132
+ let fields = Object.keys(srcStats).filter(k => k !== 'blockSize' && Array.isArray(srcStats[k]))
133
+ let ch = srcStats[fields[0]]?.length || 1
134
+ let out = { blockSize: bs }
135
+ for (let f of fields) out[f] = Array.from({ length: ch }, () => new Float32Array(outBlocks))
136
+
137
+ for (let s of segs) {
138
+ let srcOff = s[0], count = s[1], dstOff = s[2], rate = s[3] || 1, ref = s[4]
139
+ let dstBlockStart = Math.floor(dstOff / bs)
140
+ let dstBlockEnd = Math.ceil((dstOff + count) / bs)
141
+ if (ref === null) continue // silence — Float32Array already zeroed
142
+
143
+ let srcBlockStart = Math.floor(srcOff / bs)
144
+ let srcBlocks = srcStats[fields[0]][0].length
145
+ let rev = rate < 0
146
+ for (let i = dstBlockStart; i < dstBlockEnd && i < outBlocks; i++) {
147
+ let si = rev ? srcBlockStart + (dstBlockEnd - 1 - i) : srcBlockStart + (i - dstBlockStart)
148
+ if (si < 0 || si >= srcBlocks) continue
149
+ for (let f of fields) for (let c = 0; c < ch; c++) out[f][c][i] = srcStats[f][c][si]
150
+ }
151
+ }
152
+ return out
153
+ }
154
+
155
+
156
+ // ── Self-register ────────────────────────────────────────────────
157
+
158
+ audio.statSession = statSession
159
+
160
+ /** Resolve block range from opts. Recomputes stats if edits are dirty. */
161
+ export async function queryRange(inst, opts) {
162
+ await inst[LOAD]()
163
+ let at = parseTime(opts?.at), dur = parseTime(opts?.duration)
164
+ let hasRange = at != null || dur != null
165
+
166
+ if (inst.edits?.length && inst._.statsV !== inst.version) {
167
+ if (!inst._.srcStats) inst._.srcStats = inst.stats
168
+
169
+ // Range query on dirty edits — compute stats for just the requested range
170
+ if (hasRange) {
171
+ let plan = buildPlan(inst)
172
+ await ensurePlan(inst, plan, at || 0, dur)
173
+ let s = statSession(inst.sampleRate)
174
+ for (let chunk of streamPlan(inst, plan, at || 0, dur)) s.page(chunk)
175
+ let stats = s.done()
176
+ let first = Object.values(stats).find(v => v?.[0]?.length)
177
+ let blocks = first?.[0]?.length || 0
178
+ return { stats, ch: inst.channels, sr: inst.sampleRate, from: 0, to: blocks }
179
+ }
180
+
181
+ // Plan-only edits (no sample pipeline) — remap source stats by segments
182
+ let plan = buildPlan(inst)
183
+ if (!plan.pipeline.length && inst._.srcStats?.blockSize) {
184
+ let remapped = remapStats(inst._.srcStats, plan, inst.sampleRate)
185
+ if (remapped) { inst.stats = remapped; inst._.statsV = inst.version }
186
+ else {
187
+ let s = statSession(inst.sampleRate); await ensurePlan(inst, plan); for (let chunk of streamPlan(inst, plan)) s.page(chunk); inst.stats = s.done()
188
+ inst._.statsV = inst.version
189
+ }
190
+ } else {
191
+ // Full recompute — has sample-level ops
192
+ let s = statSession(inst.sampleRate); await ensurePlan(inst, plan); for (let chunk of streamPlan(inst, plan)) s.page(chunk); inst.stats = s.done()
193
+ inst._.statsV = inst.version
194
+ }
195
+ }
196
+ let sr = inst.sampleRate, bs = inst.stats?.blockSize
197
+ if (!bs) return { stats: inst.stats, ch: inst.channels, sr, from: 0, to: 0 }
198
+ let first = Object.values(inst.stats).find(v => v?.[0]?.length)
199
+ let blocks = first?.[0]?.length || 0
200
+ let atN = at != null && at < 0 ? inst.duration + at : at
201
+ let from = atN != null ? Math.floor(atN * sr / bs) : 0
202
+ let to = dur != null ? Math.ceil(((atN || 0) + dur) * sr / bs) : blocks
203
+ from = Math.max(0, Math.min(from, blocks))
204
+ to = Math.max(from, Math.min(to, blocks))
205
+ return { stats: inst.stats, ch: inst.channels, sr, from, to }
206
+ }
207
+
208
+ audio.fn.stat = async function(name, opts) {
209
+ // Array of stat names — parallel query, positional result
210
+ if (Array.isArray(name)) return Promise.all(name.map(n => this.stat(n, opts)))
211
+
212
+ // Instance methods (spectrum, cepstrum, etc.)
213
+ if (typeof this[name] === 'function' && !audio.stat(name)) return this[name](opts)
214
+
215
+ let { stats, ch, sr, from, to } = await queryRange(this, opts)
216
+ let bins = opts?.bins
217
+
218
+ // Resolve channel selection once
219
+ let chSel = opts?.channel
220
+ let perCh = Array.isArray(chSel)
221
+ let chs = chSel != null ? (perCh ? chSel : [chSel]) : Array.from({ length: ch }, (_, i) => i)
222
+
223
+ let desc = audio.stat(name)
224
+
225
+ // Derived stats — custom query (skip if bins requested on block stat)
226
+ if (desc?.query && bins == null) return desc.query(stats, chs, from, to, sr)
227
+
228
+ // Raw block stats
229
+ let src = stats[name], reduce = desc?.reduce
230
+ if (!src) throw new Error(`Unknown stat: '${name}'`)
231
+ if (!reduce) throw new Error(`No reducer for stat: '${name}'`)
232
+
233
+ // Binned mode
234
+ if (bins != null) {
235
+ let n = bins ?? (to - from)
236
+ let reduce1 = (c) => binReduce(src[c], from, to, n, reduce)
237
+ if (perCh) return chs.map(reduce1)
238
+ if (chs.length === 1) return reduce1(chs[0])
239
+ let out = new Float32Array(n), bpp = (to - from) / n
240
+ for (let i = 0; i < n; i++) {
241
+ let a = from + Math.floor(i * bpp), b = Math.min(from + Math.floor((i + 1) * bpp), to)
242
+ if (b <= a) b = a + 1
243
+ let sum = 0
244
+ for (let c of chs) sum += reduce(src[c], a, b)
245
+ out[i] = sum / chs.length
246
+ }
247
+ return out
248
+ }
249
+
250
+ // Scalar mode
251
+ if (perCh) return chs.map(c => reduce(src[c], from, to))
252
+ if (chs.length === 1) return reduce(src[chs[0]], from, to)
253
+ let vals = chs.map(c => reduce(src[c], from, to))
254
+ return vals.reduce((a, b) => a + b, 0) / vals.length
255
+ }
package/index.js DELETED
@@ -1,107 +0,0 @@
1
- // Default values.
2
- var DEFAULT_SAMPLE_RATE = 44100;
3
- var DEFAULT_BIT_DEPTH = 16;
4
- var DEFAULT_CHANNELS = 2;
5
- var DEFAULT_BYTE_ORDER = 'LE';
6
-
7
- var Audio = function Audio(options, _replaceSource) {
8
- options = options || {};
9
-
10
- // Sample rate: PCM sample rate in hertz
11
- this.sampleRate = options.sampleRate || DEFAULT_SAMPLE_RATE;
12
-
13
- // Bit depth: PCM bit-depth.
14
- this.bitDepth = options.bitDepth || DEFAULT_BIT_DEPTH;
15
-
16
- // Amount of channels: Mono, stereo, etc.
17
- this.channels = options.channels || DEFAULT_CHANNELS;
18
-
19
- // Byte order: Either "BE" or "LE".
20
- this.byteOrder = options.byteOrder || DEFAULT_BYTE_ORDER;
21
-
22
- // Byte depth: Bit depth in bytes.
23
- this._byteDepth = options._byteDepth || Math.ceil(this.bitDepth / 8);
24
-
25
- // Block size: Byte depth alignment with channels.
26
- this._blockSize = options._blockSize || this.channels * this._byteDepth;
27
-
28
- // Block rate: Sample rate alignment with blocks.
29
- this._blockRate = options._blockRate || this._blockSize * this.sampleRate;
30
-
31
- // Source: Buffer containing PCM data that is formatted to the options.
32
- if (options.source || _replaceSource) {
33
- this.source = _replaceSource || options.source;
34
- } else {
35
- var length = this._blockRate * options.duration || 0;
36
- this.source = new Buffer(length).fill(0);
37
- }
38
-
39
- // Check that the source is aligned with the block size.
40
- if (this.source.length % this._blockSize !== 0 && !options.noAssert) {
41
- throw new RangeError('Source is not aligned to the block size.');
42
- }
43
-
44
- // Length: The amount of blocks.
45
- this.length = options.length || this.source.length / this._blockSize;
46
-
47
- // Signed: Whether or not the PCM data is signed.
48
- if (typeof options.signed === 'undefined') {
49
- // If bit depth is 8 be unsigned, otherwise be signed.
50
- this.signed = this.bitDepth !== 8;
51
- } else {
52
- this.signed = options.signed;
53
- }
54
-
55
- // Alias helper functions
56
- var order = (this._byteDepth * 8) === 8 ? '' : this.byteOrder;
57
- var sign = this.signed ? '' : 'U';
58
- var typeTag = sign + 'Int' + (this._byteDepth * 8) + order;
59
- this._write = this.source['write' + typeTag].bind(this.source);
60
- this._read = this.source['read' + typeTag].bind(this.source);
61
- };
62
-
63
- Audio.prototype = {
64
- constructor: Audio,
65
-
66
- // Read sample data.
67
- read: function read(offset, channel) {
68
- channel = channel || 1;
69
-
70
- // Align inputs to source bytes.
71
- offset *= this._blockSize;
72
- channel--;
73
- channel *= this._byteDepth;
74
-
75
- // Read value from source.
76
- return this._read(offset + channel);
77
- },
78
-
79
- // Write sample data.
80
- write: function write(value, offset, channel) {
81
- channel = channel || 1;
82
-
83
- // Align inputs to source bytes.
84
- offset *= this._blockSize;
85
- channel--;
86
- channel *= this._byteDepth;
87
-
88
- // Write value to source.
89
- return this._write(value, offset + channel);
90
- },
91
-
92
- // Slice or replicate the audio.
93
- slice: function slice(start, end) {
94
- start = start || 0;
95
- end = typeof end === 'number' ? end : this.length;
96
-
97
- // Align start and end to blocs.
98
- start *= this._blockSize;
99
- end *= this._blockSize;
100
-
101
- // Replicate self, with a new sliced source.
102
- var override = this.source.slice(start, end);
103
- return new Audio(this, override);
104
- }
105
- };
106
-
107
- module.exports = Audio;