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/README.md +647 -44
- package/audio.d.ts +232 -0
- package/audio.js +54 -0
- package/bin/cli.js +983 -0
- package/cache.js +89 -0
- package/core.js +602 -0
- package/dist/audio.all.js +105369 -0
- package/dist/audio.js +3490 -0
- package/dist/audio.min.js +14 -0
- package/fn/cepstrum.js +55 -0
- package/fn/clip.js +11 -0
- package/fn/crop.js +19 -0
- package/fn/fade.js +47 -0
- package/fn/filter.js +67 -0
- package/fn/gain.js +16 -0
- package/fn/insert.js +31 -0
- package/fn/loudness.js +102 -0
- package/fn/mix.js +21 -0
- package/fn/normalize.js +80 -0
- package/fn/pad.js +19 -0
- package/fn/pan.js +35 -0
- package/fn/play.js +112 -0
- package/fn/remix.js +25 -0
- package/fn/remove.js +25 -0
- package/fn/repeat.js +35 -0
- package/fn/reverse.js +25 -0
- package/fn/save.js +55 -0
- package/fn/silence.js +34 -0
- package/fn/spectrum.js +104 -0
- package/fn/speed.js +23 -0
- package/fn/split.js +9 -0
- package/fn/stat.js +77 -0
- package/fn/transform.js +6 -0
- package/fn/trim.js +68 -0
- package/fn/write.js +15 -0
- package/{LICENSE → license.md} +21 -21
- package/package.json +64 -19
- package/plan.js +456 -0
- package/stats.js +255 -0
- package/index.js +0 -107
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;
|