@banou/ripple 0.0.17 → 0.0.18
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/fkn.json +1 -1
- package/index.js +3785 -3625
- package/package.json +1 -1
- package/sw.js +2 -2
- package/volume-normalizer-worklet.js +162 -0
package/package.json
CHANGED
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 = "
|
|
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 = "fc51cb07c39a848d" // @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"],["/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)
|