@banou/ripple 0.0.17 → 0.0.19

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/libav.wasm CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@banou/ripple",
3
- "version": "0.0.17",
3
+ "version": "0.0.19",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
package/sw.js CHANGED
@@ -8,8 +8,8 @@ const live = new Map()
8
8
  /* Stamped at build time. These are the dev defaults and must never reach production: a worker whose
9
9
  BUILD never changes is never reinstalled, so the update this feature announces would never fire. */
10
10
  const MODE = "cache" // @stamp:mode
11
- const BUILD = "482277b398f7502a" // @stamp:build
12
- const MANIFEST = [["/api-DIgTslNb-BSzu7eku.js","073ef0eb99057485"],["/api-d5i206BT.js","591582894701a02e"],["/assets/empty-BvRk9kiK.js","e3b0c44298fc1c14"],["/assets/libtorrent-CnjRmD5O.js","ab4f7006e069c0dc"],["/assets/worker-1qEVNJrZ.js","477458982f6cbe54"],["/build-JdIfVZn7.js","68868ae23db91aa0"],["/remote-U0iHKFtw.js","1d57209b4c2f6a6f"]] // @stamp:manifest
11
+ const BUILD = "e7e5e77cf2cbb85e" // @stamp:build
12
+ const MANIFEST = [["/api-DIgTslNb-BSzu7eku.js","073ef0eb99057485"],["/api-d5i206BT.js","591582894701a02e"],["/assets/empty-BvRk9kiK.js","e3b0c44298fc1c14"],["/assets/libtorrent-CnjRmD5O.js","ab4f7006e069c0dc"],["/assets/worker-i0pDn8fp.js","cdaae62b1ed74938"],["/build-JdIfVZn7.js","68868ae23db91aa0"],["/remote-U0iHKFtw.js","1d57209b4c2f6a6f"],["/volume-normalizer-worklet.js","b8444040de06692c"]] // @stamp:manifest
13
13
 
14
14
  const CACHE = 'ripple-assets-' + BUILD
15
15
 
@@ -0,0 +1,162 @@
1
+ /*
2
+ * The volume normalizer's AudioWorkletProcessor, served to the browser as it is written: plain script, no imports,
3
+ * nothing a bundler has to touch. Holds EBU short-term loudness (K-weighted, ITU-R BS.1770, gated) at a target by
4
+ * turning the gain down, never up, so it needs no limiter and adds no delay.
5
+ *
6
+ * processorOptions, every one optional:
7
+ * targetLufs -24 short-term loudness held, typical anime dialogue
8
+ * enabled true false starts it bypassed
9
+ * inputGainDb 0 gain already applied upstream (the element's volume), judged without
10
+ *
11
+ * Port messages: `{ enabled }` bypasses or engages with a 50 ms crossfade, `{ inputGainDb }` follows the volume,
12
+ * `{ reset: 'seek' }` empties the loudness window, so the new position is judged on its own while the gain holds
13
+ * until three blocks have refilled it, and `{ reset: 'source' }` also returns the gain to 0 dB, because nothing
14
+ * heard so far says anything about a new file.
15
+ */
16
+
17
+ const BLOCK_SECONDS = 0.1
18
+ const WINDOW_SECONDS = 3
19
+ const MAX_CUT_DB = 20
20
+ // time constants of a cut and of a recovery
21
+ const ATTACK_SECONDS = 0.25
22
+ const RELEASE_SECONDS = 1
23
+ // 100 ms blocks quieter than this are left out, so a pause holds the gain
24
+ const GATE_LUFS = -50
25
+
26
+ // ITU-R BS.1770 K-weighting at any sample rate, in the bilinear form libebur128 uses
27
+ const kWeighting = (rate) => {
28
+ let f0 = 1681.974450955533
29
+ const G = 3.999843853973347
30
+ let Q = 0.7071752369554196
31
+ let K = Math.tan(Math.PI * f0 / rate)
32
+ const Vh = Math.pow(10, G / 20)
33
+ const Vb = Math.pow(Vh, 0.4996667741545416)
34
+ let a0 = 1 + K / Q + K * K
35
+ const shelf = [
36
+ (Vh + Vb * K / Q + K * K) / a0, 2 * (K * K - Vh) / a0, (Vh - Vb * K / Q + K * K) / a0,
37
+ 2 * (K * K - 1) / a0, (1 - K / Q + K * K) / a0,
38
+ ]
39
+ f0 = 38.13547087602444
40
+ Q = 0.5003270373238773
41
+ K = Math.tan(Math.PI * f0 / rate)
42
+ a0 = 1 + K / Q + K * K
43
+ const highpass = [1, -2, 1, 2 * (K * K - 1) / a0, (1 - K / Q + K * K) / a0]
44
+ return { shelf, highpass }
45
+ }
46
+
47
+ // BS.1770 channel weights in WAVE order: the LFE counts for nothing, the surrounds for +1.5 dB
48
+ const channelWeight = (index, count) => count >= 6 ? [1, 1, 1, 0, 1.41, 1.41, 1.41, 1.41][index] ?? 1 : 1
49
+
50
+ class VolumeNormalizer extends AudioWorkletProcessor {
51
+ constructor({ processorOptions = {} } = {}) {
52
+ super()
53
+ const option = (key, fallback) => typeof processorOptions[key] === 'number' ? processorOptions[key] : fallback
54
+ this.target = option('targetLufs', -24)
55
+ this.enabled = processorOptions.enabled !== false
56
+ this.mix = this.enabled ? 1 : 0
57
+ this.inputPower = Math.pow(10, option('inputGainDb', 0) / 10)
58
+
59
+ this.coefficients = kWeighting(sampleRate)
60
+ this.filterState = []
61
+ this.blockLength = Math.round(sampleRate * BLOCK_SECONDS)
62
+ this.blockFill = 0
63
+ this.blockSum = 0
64
+ // mean square of each block in the window, or -1 for a block the gate left out
65
+ this.blocks = new Float64Array(Math.round(WINDOW_SECONDS / BLOCK_SECONDS)).fill(-1)
66
+ this.blockIndex = 0
67
+ this.desiredDb = 0
68
+ this.gainDb = 0
69
+ this.appliedGain = 1
70
+ this.energy = new Float32Array(128)
71
+
72
+ this.port.onmessage = ({ data }) => {
73
+ if (typeof data?.enabled === 'boolean') this.enabled = data.enabled
74
+ if (typeof data?.inputGainDb === 'number') this.inputPower = Math.pow(10, data.inputGainDb / 10)
75
+ if (data?.reset === 'seek' || data?.reset === 'source') {
76
+ this.blocks.fill(-1)
77
+ this.blockSum = 0
78
+ this.blockFill = 0
79
+ }
80
+ if (data?.reset === 'source') {
81
+ this.desiredDb = 0
82
+ this.gainDb = 0
83
+ }
84
+ }
85
+ }
86
+
87
+ closeBlock() {
88
+ const meanSquare = this.blockSum / this.blockLength / this.inputPower
89
+ this.blocks[this.blockIndex] = -0.691 + 10 * Math.log10(meanSquare) >= GATE_LUFS ? meanSquare : -1
90
+ this.blockIndex = (this.blockIndex + 1) % this.blocks.length
91
+ this.blockSum = 0
92
+ this.blockFill = 0
93
+
94
+ let sum = 0
95
+ let active = 0
96
+ for (const block of this.blocks) {
97
+ if (block >= 0) { sum += block; active++ }
98
+ }
99
+ // fewer than three blocks with anything in them is a pause, and the gain holds where it is
100
+ if (active < 3) return
101
+ const level = -0.691 + 10 * Math.log10(sum / active)
102
+ this.desiredDb = Math.max(-MAX_CUT_DB, Math.min(0, this.target - level))
103
+ }
104
+
105
+ process(inputs, outputs) {
106
+ const input = inputs[0]
107
+ const output = outputs[0]
108
+ const frames = output[0].length
109
+ const channels = input.length
110
+ if (channels === 0) {
111
+ for (const channel of output) channel.fill(0)
112
+ return true
113
+ }
114
+
115
+ if (this.energy.length < frames) this.energy = new Float32Array(frames)
116
+ const energy = this.energy
117
+ energy.fill(0, 0, frames)
118
+ const [b0, b1, b2, a1, a2] = this.coefficients.shelf
119
+ const [c0, c1, c2, d1, d2] = this.coefficients.highpass
120
+ for (let c = 0; c < channels; c++) {
121
+ const weight = channelWeight(c, channels)
122
+ if (!this.filterState[c]) this.filterState[c] = new Float64Array(4)
123
+ if (weight === 0) continue
124
+ const state = this.filterState[c]
125
+ let s1 = state[0], s2 = state[1], t1 = state[2], t2 = state[3]
126
+ const x = input[c]
127
+ for (let i = 0; i < frames; i++) {
128
+ const v = x[i]
129
+ const y = b0 * v + s1
130
+ s1 = b1 * v - a1 * y + s2
131
+ s2 = b2 * v - a2 * y
132
+ const z = c0 * y + t1
133
+ t1 = c1 * y - d1 * z + t2
134
+ t2 = c2 * y - d2 * z
135
+ energy[i] += weight * z * z
136
+ }
137
+ state[0] = s1; state[1] = s2; state[2] = t1; state[3] = t2
138
+ }
139
+ for (let i = 0; i < frames; i++) {
140
+ this.blockSum += energy[i]
141
+ if (++this.blockFill === this.blockLength) this.closeBlock()
142
+ }
143
+
144
+ const seconds = frames / sampleRate
145
+ const tau = this.desiredDb < this.gainDb ? ATTACK_SECONDS : RELEASE_SECONDS
146
+ this.gainDb += (this.desiredDb - this.gainDb) * (1 - Math.exp(-seconds / tau))
147
+ const crossfade = seconds / 0.05
148
+ this.mix += Math.max(-crossfade, Math.min(crossfade, (this.enabled ? 1 : 0) - this.mix))
149
+ const from = this.appliedGain
150
+ const to = Math.pow(10, this.gainDb * this.mix / 20)
151
+ this.appliedGain = to
152
+ const step = (to - from) / frames
153
+ for (let c = 0; c < output.length; c++) {
154
+ const x = input[c] ?? input[0]
155
+ const y = output[c]
156
+ for (let i = 0; i < frames; i++) y[i] = x[i] * (from + step * (i + 1))
157
+ }
158
+ return true
159
+ }
160
+ }
161
+
162
+ registerProcessor('banou-volume-normalizer', VolumeNormalizer)