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/pad.js ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Pad — add silence to start and/or end of audio.
3
+ * pad(1) = 1s both sides. pad(1, 2) = 1s before, 2s after.
4
+ */
5
+
6
+ import { seg } from '../plan.js'
7
+
8
+ const padPlan = (segs, ctx) => {
9
+ let { total, sampleRate: sr, args } = ctx
10
+ let before = args[0] ?? 0, after = args.length > 1 ? args[1] : before
11
+ let bN = Math.round(before * sr), aN = Math.round(after * sr)
12
+ let r = segs.map(s => { let n = s.slice(); n[2] = s[2] + bN; return n })
13
+ if (bN > 0) r.unshift(seg(0, bN, 0, undefined, null))
14
+ if (aN > 0) r.push(seg(0, aN, total + bN, undefined, null))
15
+ return r
16
+ }
17
+
18
+ import audio from '../core.js'
19
+ audio.op('pad', { plan: padPlan })
package/fn/pan.js ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Pan — stereo balance control. -1 = full left, 0 = center, 1 = full right.
3
+ * Linear attenuation — never boosts, only attenuates one channel.
4
+ */
5
+
6
+ import { opRange } from '../plan.js'
7
+
8
+ const pan = (chs, ctx) => {
9
+ let val = ctx.args[0] ?? 0 // -1..1 or t => value
10
+ if (chs.length < 2) return false // mono: no-op
11
+ let auto = typeof val === 'function'
12
+ let [s, end] = opRange(ctx, chs[0].length)
13
+ let off = (ctx.blockOffset || 0) * ctx.sampleRate
14
+ if (!auto) {
15
+ val = Math.max(-1, Math.min(1, val))
16
+ let gL = val <= 0 ? 1 : 1 - val, gR = val >= 0 ? 1 : 1 + val, gains = [gL, gR]
17
+ for (let c = 0; c < chs.length; c++) {
18
+ let g = gains[c] ?? 1
19
+ if (g === 1) continue
20
+ for (let i = Math.max(0, s); i < Math.min(end, chs[c].length); i++) chs[c][i] *= g
21
+ }
22
+ return chs
23
+ }
24
+ // Automation: per-sample evaluation
25
+ let L = chs[0], R = chs[1]
26
+ for (let i = Math.max(0, s); i < Math.min(end, L.length); i++) {
27
+ let p = Math.max(-1, Math.min(1, val((off + i) / ctx.sampleRate)))
28
+ L[i] *= p <= 0 ? 1 : 1 - p
29
+ R[i] *= p >= 0 ? 1 : 1 + p
30
+ }
31
+ return chs
32
+ }
33
+
34
+ import audio from '../core.js'
35
+ audio.op('pan', { process: pan })
package/fn/play.js ADDED
@@ -0,0 +1,112 @@
1
+ import audio, { emit } from '../core.js'
2
+ import Speaker from 'audio-speaker'
3
+
4
+ /** Apply fade ramp to interleaved buffer. fadeIn=true ramps 0→1, fadeIn=false ramps 1→0 at end. */
5
+ function ramp(buf, ch, len, fadeIn, RAMP) {
6
+ let n = Math.min(RAMP, len)
7
+ if (fadeIn) {
8
+ for (let i = 0; i < n; i++) { let t = i / n; for (let c = 0; c < ch; c++) buf[i * ch + c] *= t }
9
+ } else {
10
+ let s = len - n
11
+ for (let i = 0; i < n; i++) { let t = 1 - i / n; for (let c = 0; c < ch; c++) buf[(s + i) * ch + c] *= t }
12
+ }
13
+ }
14
+
15
+ /** Playback via audio-speaker (cross-platform: Node + browser). */
16
+ audio.fn.play = function(opts) {
17
+ let offset = opts?.at ?? 0, duration = opts?.duration
18
+ let a = this, BLOCK = audio.BLOCK_SIZE
19
+ if (a.playing) { a.playing = false; a.paused = false; if (a._._wake) a._._wake() }
20
+ a.playing = false; a.paused = opts?.paused ?? false; a.currentTime = offset
21
+ a.volume = opts?.volume ?? 0; a.loop = opts?.loop ?? false
22
+ a.block = null; a._._wake = null; a._._seekTo = null
23
+
24
+ ;(async () => {
25
+ try {
26
+ let ch = a.channels, sr = a.sampleRate
27
+ a.playing = true
28
+ let wait = async () => { while (a.paused && a.playing && a._._seekTo == null) await new Promise(r => { a._._wake = r }); a._._wake = null }
29
+
30
+ let from = offset, RAMP = 256 // ~6ms anti-click ramp
31
+ while (a.playing) {
32
+ // If paused before playback starts (e.g. open without autoplay), wait silently
33
+ if (a.paused) {
34
+ await wait()
35
+ if (!a.playing) break
36
+ if (a._._seekTo != null) { from = a._._seekTo; a._._seekTo = null; a.currentTime = from }
37
+ }
38
+ let write = Speaker({ sampleRate: sr, channels: ch, bitDepth: 32 })
39
+ let seeked = false, played = 0, fadeIn = true
40
+ const flush = async () => {
41
+ let pad = new Uint8Array(BLOCK * ch * 4)
42
+ await new Promise(r => write(pad, r))
43
+ await new Promise(r => write(pad, r))
44
+ }
45
+ for await (let chunk of a.stream({at: from, duration})) {
46
+ if (!a.playing) break
47
+ let cLen = chunk[0].length
48
+ for (let bOff = 0; bOff < cLen; bOff += BLOCK) {
49
+ if (a._._seekTo != null) {
50
+ from = a._._seekTo; a._._seekTo = null
51
+ a.currentTime = from; seeked = true; break
52
+ }
53
+ let end = Math.min(bOff + BLOCK, cLen), len = end - bOff
54
+ a.block = chunk[0].subarray(bOff, end)
55
+
56
+ let g = 10 ** (a.volume / 20)
57
+ let buf = new Float32Array(len * ch)
58
+ for (let i = 0; i < len; i++) for (let c = 0; c < ch; c++)
59
+ buf[i * ch + c] = (chunk[c] || chunk[0])[bOff + i] * g
60
+ let send = () => new Promise(r => write(new Uint8Array(buf.buffer), r))
61
+ if (fadeIn) { ramp(buf, ch, len, true, RAMP); fadeIn = false }
62
+
63
+ if (a.paused) {
64
+ ramp(buf, ch, len, false, RAMP)
65
+ await send()
66
+ await flush()
67
+ played += len
68
+ a.currentTime = from + played / sr
69
+ await wait()
70
+ if (a._._seekTo != null) continue
71
+ if (!a.playing) break
72
+ fadeIn = true
73
+ continue
74
+ }
75
+
76
+ if (!a.playing) {
77
+ ramp(buf, ch, len, false, RAMP)
78
+ await send()
79
+ break
80
+ }
81
+
82
+ await send()
83
+ played += len
84
+ if (a._._seekTo == null) {
85
+ a.currentTime = from + played / sr
86
+ emit(a, 'timeupdate', a.currentTime)
87
+ }
88
+ }
89
+ if (seeked || !a.playing) break
90
+ }
91
+ if (!seeked && !a.playing) await flush()
92
+ if (seeked) { await flush(); fadeIn = true }
93
+ write(null)
94
+ if (seeked) continue
95
+ if (!a.playing) break
96
+ if (a.loop) { from = 0; a.currentTime = 0; emit(a, 'timeupdate', 0); continue }
97
+ a.playing = false
98
+ emit(a, 'timeupdate', a.currentTime)
99
+ break
100
+ }
101
+ a.playing = false; emit(a, 'ended')
102
+ } catch (err) {
103
+ console.error('Playback error:', err)
104
+ a.playing = false
105
+ }
106
+ })()
107
+ return this
108
+ }
109
+
110
+ let proto = audio.fn
111
+ proto.pause = function() { this.paused = true }
112
+ proto.resume = function() { this.paused = false; if (this._._wake) this._._wake() }
package/fn/remix.js ADDED
@@ -0,0 +1,25 @@
1
+ const remix = (chs, ctx) => {
2
+ let arg = ctx.args[0], len = chs[0].length
3
+ // array map: [0, 1, null, ...] — number = source ch, null = silence
4
+ if (Array.isArray(arg)) {
5
+ return arg.map(src =>
6
+ src == null ? new Float32Array(len) : new Float32Array(chs[((src % chs.length) + chs.length) % chs.length])
7
+ )
8
+ }
9
+ let n = chs.length, m = arg
10
+ if (n === m) return false
11
+ if (m < n) {
12
+ let out = new Float32Array(len)
13
+ for (let c = 0; c < n; c++)
14
+ for (let i = 0; i < len; i++) out[i] += chs[c][i]
15
+ let inv = 1 / n
16
+ for (let i = 0; i < len; i++) out[i] *= inv
17
+ return Array.from({ length: m }, () => new Float32Array(out))
18
+ }
19
+ return Array.from({ length: m }, (_, c) => new Float32Array(chs[c % n]))
20
+ }
21
+
22
+ const remixCh = (_, args) => Array.isArray(args[0]) ? args[0].length : args[0]
23
+
24
+ import audio from '../core.js'
25
+ audio.op('remix', { process: remix, ch: remixCh })
package/fn/remove.js ADDED
@@ -0,0 +1,25 @@
1
+ import { seg, planOffset } from '../plan.js'
2
+
3
+ function removeSegs(segs, off, dur) {
4
+ let r = [], end = off + dur
5
+ for (let s of segs) {
6
+ let se = s[2] + s[1]
7
+ if (se <= off) r.push(s)
8
+ else if (s[2] >= end) { let n = s.slice(); n[2] = s[2] - dur; r.push(n) }
9
+ else {
10
+ let absR = Math.abs(s[3] || 1)
11
+ if (s[2] < off) r.push(seg(s[0], off - s[2], s[2], s[3], s[4]))
12
+ if (se > end) r.push(seg(s[0] + (end - s[2]) * absR, se - end, off, s[3], s[4]))
13
+ }
14
+ }
15
+ return r
16
+ }
17
+
18
+ const removePlan = (segs, ctx) => {
19
+ let { total } = ctx
20
+ let s = planOffset(ctx.offset, total)
21
+ return removeSegs(segs, s, Math.min(ctx.length ?? (total - s), total - s))
22
+ }
23
+
24
+ import audio from '../core.js'
25
+ audio.op('remove', { plan: removePlan })
package/fn/repeat.js ADDED
@@ -0,0 +1,35 @@
1
+ import { cropSegs } from './crop.js'
2
+ import { seg, planOffset } from '../plan.js'
3
+
4
+ function repeatSegs(segs, times, total, off, dur) {
5
+ if (off == null) {
6
+ let r = []
7
+ for (let t = 0; t <= times; t++)
8
+ for (let s of segs) { let n = s.slice(); n[2] = s[2] + total * t; r.push(n) }
9
+ return r
10
+ }
11
+ let segLen = dur ?? (total - off), clip = cropSegs(segs, off, segLen), r = []
12
+ for (let s of segs) {
13
+ let se = s[2] + s[1]
14
+ if (se <= off + segLen) r.push(s)
15
+ else if (s[2] >= off + segLen) { let n = s.slice(); n[2] = s[2] + segLen * times; r.push(n) }
16
+ else {
17
+ let absR = Math.abs(s[3] || 1), split = off + segLen - s[2]
18
+ r.push(seg(s[0], split, s[2], s[3], s[4]))
19
+ r.push(seg(s[0] + split * absR, se - off - segLen, off + segLen * (times + 1), s[3], s[4]))
20
+ }
21
+ }
22
+ for (let t = 1; t <= times; t++)
23
+ for (let c of clip) { let n = c.slice(); n[2] = off + segLen * t + c[2]; r.push(n) }
24
+ r.sort((a, b) => a[2] - b[2])
25
+ return r
26
+ }
27
+
28
+ const repeatPlan = (segs, ctx) => {
29
+ let { total, args, length } = ctx
30
+ let off = ctx.offset != null ? planOffset(ctx.offset, total) : null
31
+ return repeatSegs(segs, args[0] ?? 1, total, off, length)
32
+ }
33
+
34
+ import audio from '../core.js'
35
+ audio.op('repeat', { plan: repeatPlan })
package/fn/reverse.js ADDED
@@ -0,0 +1,25 @@
1
+ import { seg, planOffset } from '../plan.js'
2
+
3
+ function reverseSegs(segs, off, end) {
4
+ let r = []
5
+ for (let s of segs) {
6
+ let se = s[2] + s[1]
7
+ if (se <= off || s[2] >= end) { r.push(s); continue }
8
+ let absR = Math.abs(s[3] || 1)
9
+ if (s[2] < off) r.push(seg(s[0], off - s[2], s[2], s[3], s[4]))
10
+ let iStart = Math.max(s[2], off), iEnd = Math.min(se, end)
11
+ r.push(seg(s[0] + (iStart - s[2]) * absR, iEnd - iStart, off + end - iEnd, -(s[3] || 1), s[4]))
12
+ if (se > end) r.push(seg(s[0] + (end - s[2]) * absR, se - end, end, s[3], s[4]))
13
+ }
14
+ r.sort((a, b) => a[2] - b[2])
15
+ return r
16
+ }
17
+
18
+ const reversePlan = (segs, ctx) => {
19
+ let { total, length } = ctx
20
+ let s = planOffset(ctx.offset, total)
21
+ return reverseSegs(segs, s, s + (length ?? total - s))
22
+ }
23
+
24
+ import audio from '../core.js'
25
+ audio.op('reverse', { plan: reversePlan })
package/fn/save.js ADDED
@@ -0,0 +1,55 @@
1
+ import audio, { emit, parseTime } from '../core.js'
2
+ import encode from 'encode-audio'
3
+
4
+ const FMT_ALIAS = { aif: 'aiff', oga: 'ogg' }
5
+
6
+ function resolveFormat(fmt) { return FMT_ALIAS[fmt] || fmt || 'wav' }
7
+
8
+ /** Stream-encode audio: calls sink(buf) per chunk, returns sink(null) at end. */
9
+ async function encodeStream(inst, fmt, opts, sink) {
10
+ let enc = await encode[fmt]({ sampleRate: inst.sampleRate, channels: inst.channels, ...opts.meta })
11
+ let written = 0, tick = 0
12
+ for await (let chunk of inst.stream({ at: opts.at, duration: opts.duration })) {
13
+ let buf = await enc(chunk)
14
+ if (buf.length) await sink(buf)
15
+ written += chunk[0].length
16
+ if (++tick % 2 === 0) await new Promise(r => setTimeout(r, 0))
17
+ emit(inst, 'progress', { offset: written / inst.sampleRate, total: (opts.duration != null ? parseTime(opts.duration) : null) ?? inst.duration })
18
+ }
19
+ let final = await enc()
20
+ if (final.length) await sink(final)
21
+ return sink(null)
22
+ }
23
+
24
+ /** Encode audio to bytes. */
25
+ audio.fn.encode = async function(fmt, opts = {}) {
26
+ if (typeof fmt === 'object') { opts = fmt; fmt = undefined }
27
+ fmt = resolveFormat(fmt)
28
+ if (!encode[fmt]) throw new Error('Unknown format: ' + fmt)
29
+ let parts = []
30
+ await encodeStream(this, fmt, opts, buf => { if (buf) parts.push(buf) })
31
+ let total = parts.reduce((n, p) => n + p.length, 0)
32
+ let out = new Uint8Array(total), pos = 0
33
+ for (let p of parts) { out.set(p, pos); pos += p.length }
34
+ return out
35
+ }
36
+
37
+ /** Save audio to file path (Node) or writable handle (browser). */
38
+ audio.fn.save = async function(target, opts = {}) {
39
+ let fmt = opts.format ?? (typeof target === 'string' ? target.split('.').pop() : 'wav')
40
+ fmt = resolveFormat(fmt)
41
+ if (!encode[fmt]) throw new Error('Unknown format: ' + fmt)
42
+
43
+ let write, finish
44
+ if (typeof target === 'string') {
45
+ let { createWriteStream } = await import('fs')
46
+ let ws = createWriteStream(target)
47
+ write = buf => { if (!ws.write(Buffer.from(buf))) return new Promise(r => ws.once('drain', r)) }
48
+ finish = () => new Promise((res, rej) => { ws.on('finish', res); ws.on('error', rej); ws.end() })
49
+ } else if (target?.write) {
50
+ write = buf => target.write(buf)
51
+ finish = () => target.close?.()
52
+ } else throw new Error('Invalid save target')
53
+
54
+ await encodeStream(this, fmt, opts, buf => buf ? write(buf) : finish?.())
55
+ }
package/fn/silence.js ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Silence detection stat — finds silent regions from block stats.
3
+ * a.stat('silence', { threshold?, minDuration? }) → [{ at, duration }, ...]
4
+ */
5
+
6
+ import audio from '../core.js'
7
+ import { queryRange } from '../stats.js'
8
+ import { resolveThreshold, isLoud } from './trim.js'
9
+
10
+ audio.fn.silence = async function(opts) {
11
+ let { stats, ch, sr, from, to } = await queryRange(this, opts)
12
+ let bs = stats.blockSize
13
+ let minDur = opts?.minDuration ?? 0.1
14
+ let thresh = resolveThreshold(stats, ch, from, to, opts?.threshold)
15
+
16
+ // Scan blocks for silence
17
+ let segs = [], start = null
18
+ for (let i = from; i < to; i++) {
19
+ if (!isLoud(stats, i, ch, thresh)) {
20
+ if (start == null) start = i
21
+ } else if (start != null) {
22
+ let segAt = start * bs / sr, segEnd = i * bs / sr
23
+ if (segEnd - segAt >= minDur) segs.push({ at: segAt, duration: segEnd - segAt })
24
+ start = null
25
+ }
26
+ }
27
+ // Trailing silence
28
+ if (start != null) {
29
+ let segAt = start * bs / sr, segEnd = Math.min(to * bs / sr, this.duration)
30
+ if (segEnd - segAt >= minDur) segs.push({ at: segAt, duration: segEnd - segAt })
31
+ }
32
+
33
+ return segs
34
+ }
package/fn/spectrum.js ADDED
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Mel-frequency spectrum — FFT → mel-binned magnitudes.
3
+ * Core analysis primitive for spectrum display, spectrogram, feature extraction.
4
+ * Used by stat system (`a.stat('spectrum')`) and CLI playback visualization.
5
+ */
6
+
7
+ import fft from 'fourier-transform'
8
+ import hann from 'window-function/hann'
9
+ import { a as aWeight } from 'a-weighting'
10
+
11
+ // ── Mel scale ───────────────────────────────────────────────────
12
+
13
+ export let toMel = f => 2595 * Math.log10(1 + f / 700)
14
+ export let fromMel = m => 700 * (10 ** (m / 2595) - 1)
15
+
16
+ // ── Window cache ────────────────────────────────────────────────
17
+
18
+ let windows = {}
19
+ function hannWin(n) {
20
+ if (windows[n]) return windows[n]
21
+ let w = new Float32Array(n)
22
+ for (let i = 0; i < n; i++) w[i] = hann(i, n)
23
+ return (windows[n] = w)
24
+ }
25
+
26
+ // ── Core ────────────────────────────────────────────────────────
27
+
28
+ /**
29
+ * Compute mel-binned magnitude spectrum from a block of samples.
30
+ * @param {Float32Array} samples — mono PCM block (length should be power of 2)
31
+ * @param {number} sr — sample rate
32
+ * @param {object} [opts]
33
+ * @param {number} [opts.bins=128] — number of mel frequency bins
34
+ * @param {number} [opts.fMin=30] — minimum frequency Hz
35
+ * @param {number} [opts.fMax] — maximum frequency Hz (default: min(sr/2, 20000))
36
+ * @param {boolean} [opts.weight=true] — apply A-weighting (perceptual loudness)
37
+ * @returns {Float32Array} magnitude per mel bin (linear scale)
38
+ */
39
+ export function melSpectrum(samples, sr, opts = {}) {
40
+ let { bins = 128, fMin = 30, fMax = Math.min(sr / 2, 20000), weight = true } = opts
41
+ let N = samples.length, win = hannWin(N)
42
+ let buf = new Float32Array(N)
43
+ for (let i = 0; i < N; i++) buf[i] = samples[i] * win[i]
44
+ let mag = fft(buf)
45
+
46
+ let mMin = toMel(fMin), mMax = toMel(fMax), binHz = sr / N
47
+ let out = new Float32Array(bins)
48
+
49
+ for (let b = 0; b < bins; b++) {
50
+ let f0 = fromMel(mMin + (mMax - mMin) * b / bins)
51
+ let f1 = fromMel(mMin + (mMax - mMin) * (b + 1) / bins)
52
+ let k0 = Math.max(1, Math.floor(f0 / binHz))
53
+ let k1 = Math.min(mag.length - 1, Math.ceil(f1 / binHz))
54
+ let sum = 0, cnt = 0
55
+ for (let k = k0; k <= k1; k++) { sum += mag[k] ** 2; cnt++ }
56
+ let rms = cnt > 0 ? Math.sqrt(sum / cnt) : 0
57
+ if (weight) rms *= aWeight((f0 + f1) / 2, sr)
58
+ out[b] = rms
59
+ }
60
+ return out
61
+ }
62
+
63
+
64
+ // ── Block analysis helper ───────────────────────────────────────
65
+
66
+ /** Stream ch0, buffer remainder, call fn(block, acc) per N-sample block. Returns {acc, cnt}. */
67
+ export async function analyzeBlocks(inst, opts, N, bins, fn) {
68
+ let acc = new Float64Array(bins), cnt = 0, rem = new Float32Array(0)
69
+ for await (let pcm of inst.stream({ at: opts?.at, duration: opts?.duration })) {
70
+ let ch0 = pcm[0]
71
+ if (!ch0 || !ch0.length) continue
72
+ let input = ch0
73
+ if (rem.length) {
74
+ input = new Float32Array(rem.length + ch0.length)
75
+ input.set(rem, 0)
76
+ input.set(ch0, rem.length)
77
+ }
78
+ let limit = input.length - (input.length % N)
79
+ for (let off = 0; off < limit; off += N) { fn(input.subarray(off, off + N), acc); cnt++ }
80
+ rem = limit < input.length ? input.slice(limit) : new Float32Array(0)
81
+ }
82
+ return { acc, cnt }
83
+ }
84
+
85
+ // ── Stat registration ───────────────────────────────────────────
86
+
87
+ import audio from '../core.js'
88
+
89
+ /** a.stat('spectrum', {bins}) → average mel spectrum in dB over range */
90
+ audio.fn.spectrum = async function(opts) {
91
+ let bins = opts?.bins ?? 128
92
+ let spectOpts = { bins, fMin: opts?.fMin, fMax: opts?.fMax, weight: opts?.weight }
93
+ let sr = this.sampleRate
94
+
95
+ let { acc, cnt } = await analyzeBlocks(this, opts, 1024, bins, (block, acc) => {
96
+ let mag = melSpectrum(block, sr, spectOpts)
97
+ for (let b = 0; b < bins; b++) acc[b] += mag[b] ** 2
98
+ })
99
+
100
+ if (cnt === 0) return new Float32Array(bins)
101
+ let out = new Float32Array(bins)
102
+ for (let b = 0; b < bins; b++) out[b] = 20 * Math.log10(Math.sqrt(acc[b] / cnt) + 1e-10)
103
+ return out
104
+ }
package/fn/speed.js ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Speed — change playback rate. speed(2) = double speed (half duration),
3
+ * speed(0.5) = half speed (double duration), speed(-1) = reverse.
4
+ */
5
+
6
+ import { seg } from '../plan.js'
7
+
8
+ const speedPlan = (segs, ctx) => {
9
+ let rate = ctx.args[0]
10
+ if (rate === 0) throw new RangeError('speed: rate cannot be 0')
11
+ if (!rate || rate === 1) return segs
12
+ let absR = Math.abs(rate)
13
+ let r = [], dst = 0
14
+ for (let s of segs) {
15
+ let count = Math.round(s[1] / absR)
16
+ r.push(seg(s[0], count, dst, s[4] === null ? s[3] : (s[3] || 1) * rate, s[4]))
17
+ dst += count
18
+ }
19
+ return r
20
+ }
21
+
22
+ import audio from '../core.js'
23
+ audio.op('speed', { plan: speedPlan })
package/fn/split.js ADDED
@@ -0,0 +1,9 @@
1
+ import audio, { parseTime } from '../core.js'
2
+
3
+ /** Split at offsets, returning views. No copies. */
4
+ audio.fn.split = function(...args) {
5
+ let offsets = (Array.isArray(args[0]) ? args[0] : args).map(parseTime)
6
+ let dur = this.duration
7
+ let cuts = [0, ...offsets.sort((a, b) => a - b).filter(t => t > 0 && t < dur), dur]
8
+ return cuts.slice(0, -1).map((start, i) => this.clip({at: start, duration: cuts[i + 1] - start}))
9
+ }
package/fn/stat.js ADDED
@@ -0,0 +1,77 @@
1
+ import audio from '../core.js'
2
+
3
+ let rMin = (src, from, to) => { let v = Infinity; for (let i = from; i < to; i++) if (src[i] < v) v = src[i]; return v === Infinity ? 0 : v }
4
+ let rMax = (src, from, to) => { let v = -Infinity; for (let i = from; i < to; i++) if (src[i] > v) v = src[i]; return v === -Infinity ? 0 : v }
5
+ let rSum = (src, from, to) => { let v = 0; for (let i = from; i < to; i++) v += src[i]; return v }
6
+ let rMean = (src, from, to) => { let n = to - from; if (!n) return 0; let v = 0; for (let i = from; i < to; i++) v += src[i]; return v / n }
7
+ let rRms = (src, from, to) => { let n = to - from; if (!n) return 0; let v = 0; for (let i = from; i < to; i++) v += src[i]; return Math.sqrt(v / n) }
8
+
9
+ audio.stat('min', {
10
+ block: (chs) => chs.map(ch => {
11
+ let mn = Infinity
12
+ for (let i = 0; i < ch.length; i++) if (ch[i] < mn) mn = ch[i]
13
+ return mn
14
+ }),
15
+ reduce: rMin,
16
+ query: (stats, chs, from, to) => {
17
+ let v = Infinity
18
+ for (let c of chs) for (let i = from; i < Math.min(to, stats.min[c].length); i++) if (stats.min[c][i] < v) v = stats.min[c][i]
19
+ return v === Infinity ? 0 : v
20
+ }
21
+ })
22
+
23
+ audio.stat('max', {
24
+ block: (chs) => chs.map(ch => {
25
+ let mx = -Infinity
26
+ for (let i = 0; i < ch.length; i++) if (ch[i] > mx) mx = ch[i]
27
+ return mx
28
+ }),
29
+ reduce: rMax,
30
+ query: (stats, chs, from, to) => {
31
+ let v = -Infinity
32
+ for (let c of chs) for (let i = from; i < Math.min(to, stats.max[c].length); i++) if (stats.max[c][i] > v) v = stats.max[c][i]
33
+ return v === -Infinity ? 0 : v
34
+ }
35
+ })
36
+
37
+ audio.stat('dc', {
38
+ block: (chs) => chs.map(ch => {
39
+ let sum = 0
40
+ for (let i = 0; i < ch.length; i++) sum += ch[i]
41
+ return sum / ch.length
42
+ }),
43
+ reduce: rMean
44
+ })
45
+
46
+ audio.stat('clipping', {
47
+ block: (chs) => chs.map(ch => {
48
+ let n = 0
49
+ for (let i = 0; i < ch.length; i++) if (ch[i] >= 1 || ch[i] <= -1) n++
50
+ return n
51
+ }),
52
+ reduce: rSum,
53
+ query: (stats, chs, from, to, sr) => {
54
+ let bs = stats.blockSize, times = []
55
+ for (let i = from; i < to; i++) {
56
+ let n = 0
57
+ for (let c of chs) n += stats.clipping[c][i] || 0
58
+ if (n > 0) times.push(i * bs / sr)
59
+ }
60
+ return new Float32Array(times)
61
+ }
62
+ })
63
+
64
+ audio.stat('rms', {
65
+ block: (chs) => chs.map(ch => {
66
+ let sum = 0
67
+ for (let i = 0; i < ch.length; i++) sum += ch[i] * ch[i]
68
+ return sum / ch.length
69
+ }),
70
+ reduce: rRms,
71
+ query: (stats, chs, from, to) => {
72
+ let sum = 0, n = 0
73
+ for (let c of chs)
74
+ for (let i = from; i < Math.min(to, stats.rms[c].length); i++) { sum += stats.rms[c][i]; n++ }
75
+ return n ? Math.sqrt(sum / n) : 0
76
+ }
77
+ })
@@ -0,0 +1,6 @@
1
+ import audio from '../core.js'
2
+
3
+ audio.op('transform', {
4
+ process: (chs, ctx) => ctx.args[0](chs, ctx),
5
+ call(std, f) { return this.run({ type: 'transform', args: [f] }) }
6
+ })
package/fn/trim.js ADDED
@@ -0,0 +1,68 @@
1
+ import audio from '../core.js'
2
+
3
+ export function autoThreshold(energies) {
4
+ let vals = energies.filter(e => e > 0)
5
+ if (!vals.length) return -40
6
+ vals.sort((a, b) => a - b)
7
+ let floor = vals[Math.floor(vals.length * 0.1)]
8
+ return Math.max(-80, Math.min(-20, 10 * Math.log10(floor) + 12))
9
+ }
10
+
11
+ /** Resolve dB threshold to linear. Auto-detects from energy stats when db is null. */
12
+ export function resolveThreshold(stats, ch, from, to, db) {
13
+ if (db == null) {
14
+ let energies = []
15
+ for (let c = 0; c < ch; c++) for (let i = from; i < to; i++) energies.push(stats.energy[c][i])
16
+ db = autoThreshold(energies)
17
+ }
18
+ return 10 ** (db / 20)
19
+ }
20
+
21
+ /** Check if block i is loud (any channel exceeds thresh). */
22
+ export let isLoud = (stats, i, ch, thresh) => {
23
+ for (let c = 0; c < ch; c++)
24
+ if (Math.max(Math.abs(stats.min[c][i]), Math.abs(stats.max[c][i])) > thresh) return true
25
+ return false
26
+ }
27
+
28
+ const trim = (chs, ctx) => {
29
+ let threshold = ctx.args[0]
30
+ if (threshold == null) {
31
+ let energies = []
32
+ for (let c = 0; c < chs.length; c++)
33
+ for (let off = 0; off < chs[c].length; off += audio.BLOCK_SIZE) {
34
+ let end = Math.min(off + audio.BLOCK_SIZE, chs[c].length), sum = 0
35
+ for (let i = off; i < end; i++) sum += chs[c][i] * chs[c][i]
36
+ energies.push(sum / (end - off))
37
+ }
38
+ threshold = autoThreshold(energies)
39
+ }
40
+ let thresh = 10 ** (threshold / 20)
41
+
42
+ let len = chs[0].length, s = 0, e = len - 1
43
+ for (; s < len; s++) { let loud = false; for (let c = 0; c < chs.length; c++) if (Math.abs(chs[c][s]) > thresh) { loud = true; break }; if (loud) break }
44
+ for (; e >= s; e--) { let loud = false; for (let c = 0; c < chs.length; c++) if (Math.abs(chs[c][e]) > thresh) { loud = true; break }; if (loud) break }
45
+ e++
46
+
47
+ return s === 0 && e === len ? false : chs.map(ch => ch.slice(s, e))
48
+ }
49
+
50
+ const trimResolve = (args, { stats, sampleRate, totalDuration }) => {
51
+ if (!stats?.min || !stats?.energy) return null
52
+ let ch = stats.min.length, blocks = stats.min[0].length
53
+ let total = Math.round(totalDuration * sampleRate)
54
+ let thresh = resolveThreshold(stats, ch, 0, stats.energy[0].length, args[0])
55
+
56
+ let s = 0, e = blocks - 1
57
+ for (; s < blocks; s++) if (isLoud(stats, s, ch, thresh)) break
58
+ for (; e >= s; e--) if (isLoud(stats, e, ch, thresh)) break
59
+ e++
60
+
61
+ if (s === 0 && e === blocks) return false
62
+ if (s >= e) return { type: 'crop', args: [], at: 0, duration: 0 }
63
+ let startSample = s * audio.BLOCK_SIZE
64
+ let endSample = Math.min(e * audio.BLOCK_SIZE, total)
65
+ return { type: 'crop', args: [], at: startSample / sampleRate, duration: (endSample - startSample) / sampleRate }
66
+ }
67
+
68
+ audio.op('trim', { process: trim, resolve: trimResolve })