loudness-worklet 1.4.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 lcweden
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,343 @@
1
+ # Loudness Audio Worklet Processor
2
+
3
+ A loudness meter for the `Web Audio API`, based on the [ITU-R BS.1770-5](https://www.itu.int/rec/R-REC-BS.1770) standard and implemented as an AudioWorkletProcessor, [try demo](https://lcweden.github.io/loudness-audio-worklet-processor/).
4
+
5
+ [![screenshot](https://github.com/lcweden/loudness-audio-worklet-processor/blob/main/public/screenshots/meter.png)](https://lcweden.github.io/loudness-audio-worklet-processor/)
6
+
7
+ ## Features
8
+
9
+ - **Loudness Measurement**: Compliant with the **ITU-R BS.1770-5** standard.
10
+ - **Comprehensive Metrics**: Calculates Momentary, Short-term, and Integrated Loudness, as well as Loudness Range (LRA) and True-Peak levels.
11
+ - **Flexible**: Works with live audio and pre-recorded files.
12
+ - **Lightweight**: No external dependencies required.
13
+
14
+ ## Installation
15
+
16
+ ### Download
17
+
18
+ 1. Download the pre-built file: [loudness.worklet.js](https://lcweden.github.io/loudness-audio-worklet-processor/loudness.worklet.js).
19
+ 2. Place `loudness.worklet.js` in your project directory (e.g., `/public/`).
20
+
21
+ ```javascript
22
+ audioContext.audioWorklet.addModule("loudness.worklet.js");
23
+ ```
24
+
25
+ ### Import
26
+
27
+ Import the `AudioWorkletProcessor` directly in your code:
28
+
29
+ ```javascript
30
+ const module = new URL("https://lcweden.github.io/loudness-audio-worklet-processor/loudness.worklet.js");
31
+ audioContext.audioWorklet.addModule(module);
32
+ ```
33
+
34
+ ## Quick Start
35
+
36
+ ### Example
37
+
38
+ This example shows the easiest way to get started with the Loudness Audio Worklet Processor.
39
+
40
+ ```html
41
+ <!doctype html>
42
+ <html>
43
+ <body>
44
+ <pre></pre>
45
+ <script>
46
+ const pre = document.querySelector("pre");
47
+ navigator.mediaDevices.getDisplayMedia({ audio: true }).then((mediaStream) => {
48
+ const audioTrack = mediaStream.getAudioTracks()[0];
49
+ const { channelCount } = audioTrack.getSettings();
50
+ const context = new AudioContext();
51
+ context.audioWorklet
52
+ .addModule("https://lcweden.github.io/loudness-audio-worklet-processor/loudness.worklet.js")
53
+ .then(() => {
54
+ const source = new MediaStreamAudioSourceNode(context, { mediaStream });
55
+ const worklet = new AudioWorkletNode(context, "loudness-processor", {
56
+ processorOptions: {
57
+ interval: 0.1,
58
+ capacity: 600
59
+ }
60
+ });
61
+
62
+ source.connect(worklet).port.onmessage = (event) => {
63
+ pre.textContent = JSON.stringify(event.data, null, 2);
64
+ };
65
+ });
66
+ });
67
+ </script>
68
+ </body>
69
+ </html>
70
+ ```
71
+
72
+ ### File-based measurement
73
+
74
+ Suppose you already have an audio file (e.g., from an input[type="file"]):
75
+
76
+ ```javascript
77
+ const arrayBuffer = await file.arrayBuffer();
78
+ const audioBuffer = await new AudioContext().decodeAudioData(arrayBuffer);
79
+
80
+ const { numberOfChannels, length, sampleRate } = audioBuffer;
81
+ const context = new OfflineAudioContext(numberOfChannels, length, sampleRate);
82
+
83
+ await context.audioWorklet.addModule("loudness.worklet.js");
84
+
85
+ const source = new AudioBufferSourceNode(context, { buffer: audioBuffer });
86
+ const worklet = new AudioWorkletNode(context, "loudness-processor");
87
+
88
+ worklet.port.onmessage = (event) => {
89
+ console.log("Loudness Data:", event.data);
90
+ };
91
+
92
+ source.connect(worklet).connect(context.destination);
93
+ source.start();
94
+
95
+ context.startRendering();
96
+ ```
97
+
98
+ ### Live-based measurement
99
+
100
+ Supports `MediaStream` or `MediaElement` sources:
101
+
102
+ ```javascript
103
+ const context = new AudioContext({ sampleRate: 48000 });
104
+
105
+ await context.audioWorklet.addModule("loudness.worklet.js");
106
+
107
+ const audioTrack = mediaStream.getAudioTracks()[0];
108
+ const { channelCount } = audioTrack.getSettings();
109
+
110
+ const source = new MediaStreamAudioSourceNode(context, { mediaStream });
111
+ const worklet = new AudioWorkletNode(context, "loudness-processor", {
112
+ processorOptions: {
113
+ capacity: 600 // Seconds of history to keep, prevent memory overflow
114
+ }
115
+ });
116
+
117
+ worklet.port.onmessage = (event) => {
118
+ console.log("Loudness Data:", event.data);
119
+ };
120
+
121
+ source.connect(worklet).connect(context.destination);
122
+ ```
123
+
124
+ ## API
125
+
126
+ ### Options
127
+
128
+ The `AudioWorkletNode` constructor accepts the following options:
129
+
130
+ #### Params
131
+
132
+ | Option | Type | Required | Default | Description |
133
+ | ------------------------- | ---------- | -------- | ------- | ---------------------------------------------------------------------------------------------------- |
134
+ | numberOfInputs | `number` | `N` | `1` | Number of input channels. |
135
+ | numberOfOutputs | `number` | `N` | `1` | Number of output channels. |
136
+ | outputChannelCount | `number[]` | `N` | - | Determined at runtime automatically. |
137
+ | processorOptions.interval | `number` | `N` | `null` | Message interval in seconds. |
138
+ | processorOptions.capacity | `number` | `N` | `null` | Maximum seconds of history to keep. If set to `null`, the processor will not limit the history size. |
139
+
140
+ #### Example
141
+
142
+ ```javascript
143
+ const { numberOfChannels, length, sampleRate } = audioBuffer;
144
+ const worklet = new AudioWorkletNode(context, "loudness-processor", {
145
+ numberOfInputs: 1,
146
+ numberOfOutputs: 1,
147
+ outputChannelCount: [numberOfChannels], // Unnecessary
148
+ processorOptions: {
149
+ capacity: length / sampleRate,
150
+ interval: 0.1
151
+ }
152
+ });
153
+ ```
154
+
155
+ ### Message Format
156
+
157
+ Measurement results are sent back to the main thread via `port.onmessage` with the following format:
158
+
159
+ ```typescript
160
+ type AudioLoudnessSnapshot = {
161
+ currentFrame: number;
162
+ currentTime: number;
163
+ currentMetrics: [
164
+ {
165
+ momentaryLoudness: number;
166
+ shortTermLoudness: number;
167
+ integratedLoudness: number;
168
+ maximumMomentaryLoudness: number;
169
+ maximumShortTermLoudness: number;
170
+ maximumTruePeakLevel: number;
171
+ loudnessRange: number;
172
+ }
173
+ ];
174
+ };
175
+ ```
176
+
177
+ ### Units
178
+
179
+ | Metric | Unit |
180
+ | -------------------------- | ------------- |
181
+ | `momentaryLoudness` | `LUFS`/`LKFS` |
182
+ | `shortTermLoudness` | `LUFS`/`LKFS` |
183
+ | `integratedLoudness` | `LUFS`/`LKFS` |
184
+ | `maximumMomentaryLoudness` | `LUFS`/`LKFS` |
185
+ | `maximumShortTermLoudness` | `LUFS`/`LKFS` |
186
+ | `maximumTruePeakLevel` | `dBTP` |
187
+ | `loudnessRange` | `LRA` |
188
+
189
+ ### Supported Channels
190
+
191
+ Supported channel counts: `1`, `2`, `6`, `8`, `10`, `12`, `24`
192
+
193
+ > [!NOTE]
194
+ > Channel counts not listed above are weighted at `1.0`.
195
+
196
+ ### Coefficients
197
+
198
+ The following coefficients are used for the K-weighting filter:
199
+
200
+ | | highshelf | highpass |
201
+ | --- | ----------------- | ----------------- |
202
+ | a1 | -1.69065929318241 | −1.99004745483398 |
203
+ | a2 | 0.73248077421585 | 0.99007225036621 |
204
+ | b0 | 1.53512485958697 | 1.0 |
205
+ | b1 | -2.69169618940638 | -2.0 |
206
+ | b2 | 1.19839281085285 | 1.0 |
207
+
208
+ The following FIR filter coefficients are used for true-peak measurement:
209
+
210
+ | Phase 0 | Phase 1 | Phase 2 | Phase 3 |
211
+ | ---------------- | ---------------- | ---------------- | ---------------- |
212
+ | 0.0017089843750 | −0.0291748046875 | −0.0189208984375 | −0.0083007812500 |
213
+ | 0.0109863281250 | 0.0292968750000 | 0.0330810546875 | 0.0148925781250 |
214
+ | −0.0196533203125 | −0.0517578125000 | −0.0582275390625 | −0.0266113281250 |
215
+ | 0.0332031250000 | 0.0891113281250 | 0.1015625000000 | 0.0476074218750 |
216
+ | −0.0594482421875 | −0.1665039062500 | −0.2003173828125 | −0.1022949218750 |
217
+ | 0.1373291015625 | 0.4650878906250 | 0.7797851562500 | 0.9721679687500 |
218
+ | 0.9721679687500 | 0.7797851562500 | 0.4650878906250 | 0.1373291015625 |
219
+ | −0.1022949218750 | −0.2003173828125 | −0.1665039062500 | −0.0594482421875 |
220
+ | 0.0476074218750 | 0.1015625000000 | 0.0891113281250 | 0.0332031250000 |
221
+ | −0.0266113281250 | −0.0582275390625 | −0.0517578125000 | −0.0196533203125 |
222
+ | 0.0148925781250 | 0.0330810546875 | 0.0292968750000 | 0.0109863281250 |
223
+ | −0.0083007812500 | −0.0189208984375 | −0.0291748046875 | 0.0017089843750 |
224
+
225
+ ## Validation
226
+
227
+ ### ITU-R BS.2217
228
+
229
+ The [ITU-R BS.2217](https://www.itu.int/pub/R-REP-BS.2217) test suite provides a table of compliance test files and related information for verifying that a meter
230
+ meets the specifications within Recommendation [ITU-R BS.1770](https://www.itu.int/rec/R-REC-BS.1770).
231
+
232
+ | file | measurement | channels | |
233
+ | ------------------------------------ | ----------- | -------- | ------------------ |
234
+ | 1770Comp_2_RelGateTest | -10.0 LKFS | 2 | :white_check_mark: |
235
+ | 1770Comp_2_AbsGateTest | -69.5 LKFS | 2 | :white_check_mark: |
236
+ | 1770Comp_2_24LKFS_25Hz_2ch | -24.0 LKFS | 2 | :white_check_mark: |
237
+ | 1770Comp_2_24LKFS_100Hz_2ch | -24.0 LKFS | 2 | :white_check_mark: |
238
+ | 1770Comp_2_24LKFS_500Hz_2ch | -24.0 LKFS | 2 | :white_check_mark: |
239
+ | 1770Comp_2_24LKFS_1000Hz_2ch | -24.0 LKFS | 2 | :white_check_mark: |
240
+ | 1770Comp_2_24LKFS_2000Hz_2ch | -24.0 LKFS | 2 | :white_check_mark: |
241
+ | 1770Comp_2_24LKFS_10000Hz_2ch | -24.0 LKFS | 2 | :white_check_mark: |
242
+ | 1770Comp_2_23LKFS_25Hz_2ch | -23.0 LKFS | 2 | :white_check_mark: |
243
+ | 1770Comp_2_23LKFS_100Hz_2ch | -23.0 LKFS | 2 | :white_check_mark: |
244
+ | 1770Comp_2_23LKFS_500Hz_2ch | -23.0 LKFS | 2 | :white_check_mark: |
245
+ | 1770Comp_2_23LKFS_1000Hz_2ch | -23.0 LKFS | 2 | :white_check_mark: |
246
+ | 1770Comp_2_23LKFS_2000Hz_2ch | -23.0 LKFS | 2 | :white_check_mark: |
247
+ | 1770Comp_2_23LKFS_10000Hz_2ch | -23.0 LKFS | 2 | :white_check_mark: |
248
+ | 1770Comp_2_18LKFS_FrequencySweep | -18.0 LKFS | 1 | :white_check_mark: |
249
+ | 1770Comp_2_24LKFS_SummingTest | -24.0 LKFS | 6 | :white_check_mark: |
250
+ | 1770Comp_2_23LKFS_SummingTest | -23.0 LKFS | 6 | :white_check_mark: |
251
+ | 1770Comp_2_24LKFS_ChannelCheckLeft | -24.0 LKFS | 6 | :white_check_mark: |
252
+ | 1770Comp_2_24LKFS_ChannelCheckRight | -24.0 LKFS | 6 | :white_check_mark: |
253
+ | 1770Comp_2_24LKFS_ChannelCheckCentre | -24.0 LKFS | 6 | :white_check_mark: |
254
+ | 1770Comp_2_24LKFS_ChannelCheckLFE | -inf LKFS | 6 | :white_check_mark: |
255
+ | 1770Comp_2_24LKFS_ChannelCheckLs | -24.0 LKFS | 6 | :white_check_mark: |
256
+ | 1770Comp_2_24LKFS_ChannelCheckRs | -24.0 LKFS | 6 | :white_check_mark: |
257
+ | 1770Comp_2_23LKFS_ChannelCheckLeft | -23.0 LKFS | 6 | :white_check_mark: |
258
+ | 1770Comp_2_23LKFS_ChannelCheckRight | -23.0 LKFS | 6 | :white_check_mark: |
259
+ | 1770Comp_2_23LFKS_ChannelCheckCentre | -23.0 LKFS | 6 | :white_check_mark: |
260
+ | 1770Comp_2_23LKFS_ChannelCheckLFE | -inf LKFS | 6 | :white_check_mark: |
261
+ | 1770Comp_2_23LKFS_ChannelCheckLs | -23.0 LKFS | 6 | :white_check_mark: |
262
+ | 1770Comp_2_23LKFS_ChannelCheckRs | -23.0 LKFS | 6 | :white_check_mark: |
263
+ | 1770-2 Conf 6ch VinCntr-24LKFS | -24.0 LKFS | 6 | :white_check_mark: |
264
+ | 1770-2 Conf 6ch VinL+R-24LKFS | -24.0 LKFS | 6 | :white_check_mark: |
265
+ | 1770-2 Conf 6ch VinL-R-C-24LKFS | -24.0 LKFS | 6 | :white_check_mark: |
266
+ | 1770-2 Conf Stereo VinL+R-24LKFS | -24.0 LKFS | 2 | :white_check_mark: |
267
+ | 1770-2 Conf Mono Voice+Music-24LKFS | -24.0 LKFS | 1 | :white_check_mark: |
268
+ | 1770-2 Conf 6ch VinCntr-23LKFS | -23.0 LKFS | 6 | :white_check_mark: |
269
+ | 1770-2 Conf 6ch VinL+R-23LKFS | -23.0 LKFS | 6 | :white_check_mark: |
270
+ | 1770-2 Conf 6ch VinL-R-C-23LKFS | -23.0 LKFS | 6 | :white_check_mark: |
271
+ | 1770-2 Conf Stereo VinL+R-23LKFS | -23.0 LKFS | 2 | :white_check_mark: |
272
+ | 1770-2 Conf Mono Voice+Music-23LKFS | -23.0 LKFS | 1 | :white_check_mark: |
273
+ | 1770Conf-8channels_24LKFS | -24.0 LKFS | 8 | :white_check_mark: |
274
+ | 1770Conf-8channels_23LKFS | -23.0 LKFS | 8 | :white_check_mark: |
275
+ | 1770Conf-10channels_24LKFS | -24.0 LKFS | 10 | :white_check_mark: |
276
+ | 1770Conf-10channels_23LKFS | -23.0 LKFS | 10 | :white_check_mark: |
277
+ | 1770Conf-12channels_24LKFS | -24.0 LKFS | 12 | :white_check_mark: |
278
+ | 1770Conf-12channels_23LKFS | -23.0 LKFS | 12 | :white_check_mark: |
279
+ | 1770Conf-24channels_24LKFS | -24.0 LKFS | 24 | :white_check_mark: |
280
+ | 1770Conf-24channels_23LKFS | -23.0 LKFS | 24 | :white_check_mark: |
281
+
282
+ > [!TIP]
283
+ > If `decodeAudioData` fails, it's often due to the browser's specific support for the audio file's format (codec), container, or channel layout, rather than a general API incompatibility.
284
+ > Try a different browser or convert the audio file to a more widely supported format.
285
+ > For example, Chrome has limited support for certain codecs in audio files, while Safari offers broader support. (1770Conf-24channels_24LKFS)
286
+
287
+ ### EBU TECH 3341 Minimum requirements test signals
288
+
289
+ [EBU TECH 3341](https://tech.ebu.ch/publications/tech3341) defines minimum requirements and corresponding test signals for verifying momentary, short-term, and integrated loudness accuracy, gating behavior, and true-peak measurement.
290
+
291
+ | file | expected response and accepted tolerances | |
292
+ | ------------------------------------ | ----------------------------------------------------------- | ------------------ |
293
+ | seq-3341-1-16bit | M, S, I = −23.0 ±0.1 LUFS | :white_check_mark: |
294
+ | seq-3341-2-16bit | M, S, I = −33.0 ±0.1 LUFS | :white_check_mark: |
295
+ | seq-3341-3-16bit-v02 | I = −23.0 ±0.1 LUFS | :white_check_mark: |
296
+ | seq-3341-4-16bit-v02 | I = −23.0 ±0.1 LUFS | :white_check_mark: |
297
+ | seq-3341-5-16bit-v02 | I = −23.0 ±0.1 LUFS | :white_check_mark: |
298
+ | seq-3341-6-6channels-WAVEEX-16bit | I = −23.0 ±0.1 LUFS | :white_check_mark: |
299
+ | seq-3341-7_seq-3342-5-24bit | I = −23.0 ±0.1 LUFS | :white_check_mark: |
300
+ | seq-3341-2011-8_seq-3342-6-24bit-v02 | I = −23.0 ±0.1 LUFS | :white_check_mark: |
301
+ | seq-3341-9-24bit | S = −23.0 ±0.1 LUFS, constant after 3 s | :white_check_mark: |
302
+ | seq-3341-10-\*-24bit | Max S = −23.0 ±0.1 LUFS, for each segment | :white_check_mark: |
303
+ | seq-3341-11-24bit | Max S = −38.0, −37.0, …, −19.0 ±0.1 LUFS, successive values | :white_check_mark: |
304
+ | seq-3341-12-24bit | M = −23.0 ±0.1 LUFS, constant after 1 s | :white_check_mark: |
305
+ | seq-3341-13-\*-24bit | Max M = −23.0 ±0.1 LUFS, for each segment | :white_check_mark: |
306
+ | seq-3341-14-24bit | Max M = −38.0, …, −19.0 ±0.1 LUFS, successive values | :white_check_mark: |
307
+ | seq-3341-15-24bit | Max true-peak = −6.0 +0.2/−0.4 dBTP | :white_check_mark: |
308
+ | seq-3341-16-24bit | Max true-peak = −6.0 +0.2/−0.4 dBTP | :white_check_mark: |
309
+ | seq-3341-17-24bit | Max true-peak = −6.0 +0.2/−0.4 dBTP | :white_check_mark: |
310
+ | seq-3341-18-24bit | Max true-peak = −6.0 +0.2/−0.4 dBTP | :white_check_mark: |
311
+ | seq-3341-19-24bit | Max true-peak = +3.0 +0.2/−0.4 dBTP | :white_check_mark: |
312
+ | seq-3341-20-24bit | Max true-peak = 0.0 +0.2/−0.4 dBTP | :white_check_mark: |
313
+ | seq-3341-21-24bit | Max true-peak = 0.0 +0.2/−0.4 dBTP | :white_check_mark: |
314
+ | seq-3341-22-24bit | Max true-peak = 0.0 +0.2/−0.4 dBTP | :white_check_mark: |
315
+ | seq-3341-23-24bit | Max true-peak = 0.0 +0.2/−0.4 dBTP | :white_check_mark: |
316
+
317
+ ### EBU TECH 3342 Minimum requirements test signals
318
+
319
+ [EBU TECH 3342](https://tech.ebu.ch/publications/tech3342) focuses on the measurement of loudness range.
320
+
321
+ | file | expected response and accepted tolerances | |
322
+ | ------------------------------------ | ----------------------------------------- | ------------------ |
323
+ | seq-3342-1-16bit | LRA = 10 ±1 LU | :white_check_mark: |
324
+ | seq-3342-2-16bit | LRA = 5 ±1 LU | :white_check_mark: |
325
+ | seq-3342-3-16bit | LRA = 20 ±1 LU | :white_check_mark: |
326
+ | seq-3342-4-16bit | LRA = 15 ±1 LU | :white_check_mark: |
327
+ | seq-3341-7_seq-3342-5-24bit | LRA = 5 ±1 LU | :white_check_mark: |
328
+ | seq-3341-2011-8_seq-3342-6-24bit-v02 | LRA = 15 ±1 LU | :white_check_mark: |
329
+
330
+ ## Acknowledgments
331
+
332
+ This project is a learning experiment aimed at exploring audio signal processing and ITU-R BS.1770 loudness measurement standards. I am not an expert in audio engineering or signal processing, and this project was developed as a way to better understand the concepts of audio loudness and implementation techniques. Thanks to the ITU-R BS.1770 standards for providing the theoretical basis for loudness measurement.
333
+
334
+ ## License
335
+
336
+ This project is licensed under the [MIT License](LICENSE).
337
+
338
+ ## References
339
+
340
+ - [ITU-R BS.1770](https://www.itu.int/rec/R-REC-BS.1770)
341
+ - [ITU-R BS.2217](https://www.itu.int/pub/R-REP-BS.2217)
342
+ - [EBU Tech 3341](https://tech.ebu.ch/publications/tech3341)
343
+ - [EBU Tech 3342](https://tech.ebu.ch/publications/tech3342)
@@ -0,0 +1,6 @@
1
+ declare class LoudnessWorkletNode extends AudioWorkletNode {
2
+ constructor(context: BaseAudioContext, options?: AudioWorkletNodeOptions);
3
+ static loadModule(context: BaseAudioContext): Promise<void>;
4
+ }
5
+ declare function createLoudnessWorklet(context: BaseAudioContext, options?: AudioWorkletNodeOptions): Promise<AudioWorkletNode>;
6
+ export { createLoudnessWorklet, LoudnessWorkletNode };
package/dist/index.js ADDED
@@ -0,0 +1,390 @@
1
+ const i = `/**
2
+ * A lightweight and efficient AudioWorklet for real-time loudness measurement in the browser, compliant with the ITU-R BS.1770-5 standard.
3
+ *
4
+ * @file loudness.worklet.js
5
+ * @version 1.4.2
6
+ * @author lcweden
7
+ * @license MIT
8
+ * @see git+https://github.com/lcweden/loudness-audio-worklet-processor.git
9
+ * @date 2025-09-25T17:15:25.559Z
10
+ */\r
11
+ \r
12
+ class O {
13
+ #t = new Float32Array(2);
14
+ #e = new Float32Array(3);
15
+ #s = new Float32Array(2);
16
+ #r = new Float32Array(2);
17
+ /**
18
+ * Creates a new BiquadraticFilter with given coefficients.
19
+ * @param { number[] } a - Feedback coefficients [a1, a2]
20
+ * @param { number[] } b - Feedforward coefficients [b0, b1, b2]
21
+ */
22
+ constructor(e, r) {
23
+ this.reset(), this.set(e, r);
24
+ }
25
+ /**
26
+ * Processes a single input sample and returns the filtered output.
27
+ * @param { number } input - The input sample.
28
+ * @returns { number } - The filtered output sample.
29
+ */
30
+ process(e) {
31
+ const r = this.#e[0] * e + this.#e[1] * this.#s[0] + this.#e[2] * this.#s[1] - this.#t[0] * this.#r[0] - this.#t[1] * this.#r[1];
32
+ return this.#s[1] = this.#s[0], this.#s[0] = e, this.#r[1] = this.#r[0], this.#r[0] = r, r;
33
+ }
34
+ /**
35
+ * Sets new filter coefficients.
36
+ * @param { number[] } a - Feedback coefficients [a1, a2]
37
+ * @param { number[] } b - Feedforward coefficients [b0, b1, b2]
38
+ */
39
+ set(e, r) {
40
+ this.#t.set((e.length = 2, e)), this.#e.set((r.length = 3, r));
41
+ }
42
+ /**
43
+ * Resets the filter state.
44
+ */
45
+ reset() {
46
+ this.#s.fill(0), this.#r.fill(0);
47
+ }
48
+ }
49
+ class d {
50
+ #t;
51
+ #e;
52
+ #s = 0;
53
+ #r = 0;
54
+ #n = 0;
55
+ constructor(e) {
56
+ this.#e = e || 0, this.#t = new Array(e);
57
+ }
58
+ push(e) {
59
+ this.#t[this.#r] = e, this.isFull() ? this.#s = (this.#s + 1) % this.#e : this.#n++, this.#r = (this.#r + 1) % this.#e;
60
+ }
61
+ pop() {
62
+ if (this.isEmpty())
63
+ return;
64
+ const e = this.#t[this.#s];
65
+ return this.#s = (this.#s + 1) % this.#e, this.#n--, e;
66
+ }
67
+ peek() {
68
+ if (!this.isEmpty())
69
+ return this.#t[this.#s];
70
+ }
71
+ slice(e = 0, r = this.#n) {
72
+ if (e < 0 && (e = 0), r > this.#n && (r = this.#n), e >= r)
73
+ return [];
74
+ const n = [];
75
+ for (let s = e; s < r; s++) {
76
+ const u = (this.#s + s) % this.#e;
77
+ n.push(this.#t[u]);
78
+ }
79
+ return n;
80
+ }
81
+ isEmpty() {
82
+ return this.#n === 0;
83
+ }
84
+ isFull() {
85
+ return this.#n === this.#e;
86
+ }
87
+ get length() {
88
+ return this.#n;
89
+ }
90
+ get capacity() {
91
+ return this.#e;
92
+ }
93
+ *[Symbol.iterator]() {
94
+ for (let e = 0; e < this.#n; e++) {
95
+ const r = (this.#s + e) % this.#e;
96
+ yield this.#t[r];
97
+ }
98
+ }
99
+ }
100
+ const L = {
101
+ highshelf: { a: [-1.69065929318241, 0.73248077421585], b: [1.53512485958697, -2.69169618940638, 1.19839281085285] },
102
+ highpass: { a: [-1.99004745483398, 0.99007225036621], b: [1, -2, 1] }
103
+ }, R = {
104
+ lowpass: {
105
+ phase0: [
106
+ 0.001708984375,
107
+ 0.010986328125,
108
+ -0.0196533203125,
109
+ 0.033203125,
110
+ -0.0594482421875,
111
+ 0.1373291015625,
112
+ 0.97216796875,
113
+ -0.102294921875,
114
+ 0.047607421875,
115
+ -0.026611328125,
116
+ 0.014892578125,
117
+ -0.00830078125
118
+ ],
119
+ phase1: [
120
+ -0.0291748046875,
121
+ 0.029296875,
122
+ -0.0517578125,
123
+ 0.089111328125,
124
+ -0.16650390625,
125
+ 0.465087890625,
126
+ 0.77978515625,
127
+ -0.2003173828125,
128
+ 0.1015625,
129
+ -0.0582275390625,
130
+ 0.0330810546875,
131
+ -0.0189208984375
132
+ ],
133
+ phase2: [
134
+ -0.0189208984375,
135
+ 0.0330810546875,
136
+ -0.0582275390625,
137
+ 0.1015625,
138
+ -0.2003173828125,
139
+ 0.77978515625,
140
+ 0.465087890625,
141
+ -0.16650390625,
142
+ 0.089111328125,
143
+ -0.0517578125,
144
+ 0.029296875,
145
+ -0.0291748046875
146
+ ],
147
+ phase3: [
148
+ -0.00830078125,
149
+ 0.014892578125,
150
+ -0.026611328125,
151
+ 0.047607421875,
152
+ -0.102294921875,
153
+ 0.97216796875,
154
+ 0.1373291015625,
155
+ -0.0594482421875,
156
+ 0.033203125,
157
+ -0.0196533203125,
158
+ 0.010986328125,
159
+ 0.001708984375
160
+ ]
161
+ }
162
+ }, G = {
163
+ 1: { mono: 1 },
164
+ 2: { L: 1, R: 1 },
165
+ 6: { L: 1, R: 1, C: 1, LFE: 0, Ls: 1.41, Rs: 1.41 },
166
+ 8: { L: 1, R: 1, C: 1, LFE: 0, Lss: 1.41, Rss: 1.41, Lrs: 1, Rrs: 1 },
167
+ 10: { L: 1, R: 1, C: 1, LFE: 0, Ls: 1.41, Rs: 1.41, Tfl: 1, Tfr: 1, Tbl: 1, Tbr: 1 },
168
+ 12: {
169
+ L: 1,
170
+ R: 1,
171
+ C: 1,
172
+ LFE: 0,
173
+ Lss: 1.41,
174
+ Rss: 1.41,
175
+ Lrs: 1,
176
+ Rrs: 1,
177
+ Tfl: 1,
178
+ Tfr: 1,
179
+ Tbl: 1,
180
+ Tbr: 1
181
+ },
182
+ 24: {
183
+ FL: 1.41,
184
+ FR: 1.41,
185
+ FC: 1,
186
+ LFE1: 0,
187
+ BL: 1,
188
+ BR: 1,
189
+ FLc: 1,
190
+ FRc: 1,
191
+ BC: 1,
192
+ LFE2: 0,
193
+ SiL: 1.41,
194
+ SiR: 1.41,
195
+ TpFL: 1,
196
+ TpFR: 1,
197
+ TpFC: 1,
198
+ TpC: 1,
199
+ TpBL: 1,
200
+ TpBR: 1,
201
+ TpSiL: 1,
202
+ TpSiR: 1,
203
+ TpBC: 1,
204
+ BtFC: 1,
205
+ BtFL: 1,
206
+ BtFR: 1
207
+ }
208
+ }, U = 0.4, B = 0.1, Y = 3, C = 0.1, j = 0.1, q = 0.95, w = 12.04, z = -70, K = -10, J = -70, Q = -20;
209
+ class F {
210
+ #t;
211
+ #e;
212
+ constructor(e) {
213
+ this.#t = e, this.#e = Array(e.length).fill(0);
214
+ }
215
+ process(e) {
216
+ if (Array.isArray(e))
217
+ return e.map((n) => this.process(n));
218
+ {
219
+ const r = e;
220
+ this.#e.pop(), this.#e.unshift(r);
221
+ let n = 0;
222
+ for (let s = 0; s < this.#t.length; s++)
223
+ n += this.#t[s] * this.#e[s];
224
+ return n;
225
+ }
226
+ }
227
+ reset() {
228
+ this.#e.fill(0);
229
+ }
230
+ }
231
+ class X extends AudioWorkletProcessor {
232
+ capacity = null;
233
+ interval = null;
234
+ lastTime = 0;
235
+ metrics = [];
236
+ kWeightingFilters = [];
237
+ truePeakFilters = [];
238
+ momentaryEnergyBuffers = [];
239
+ momentaryEnergyRunningSums = [];
240
+ momentarySampleAccumulators = [];
241
+ momentaryLoudnessHistories = [];
242
+ shortTermEnergyBuffers = [];
243
+ shortTermEnergyRunningSums = [];
244
+ shortTermLoudnessHistories = [];
245
+ shortTermSampleAccumulators = [];
246
+ constructor(e) {
247
+ super();
248
+ const { numberOfInputs: r = 1, processorOptions: n } = e;
249
+ if (n) {
250
+ const { capacity: s, interval: u } = n;
251
+ this.capacity = s ?? null, this.interval = u ?? null;
252
+ }
253
+ for (let s = 0; s < r; s++)
254
+ this.momentaryEnergyRunningSums[s] = 0, this.momentarySampleAccumulators[s] = 0, this.momentaryEnergyBuffers[s] = new d(Math.round(sampleRate * U)), this.momentaryLoudnessHistories[s] = this.capacity ? new d(Math.ceil(this.capacity / B)) : new Array(), this.shortTermEnergyRunningSums[s] = 0, this.shortTermSampleAccumulators[s] = 0, this.shortTermEnergyBuffers[s] = new d(Math.round(sampleRate * Y)), this.shortTermLoudnessHistories[s] = this.capacity ? new d(Math.ceil(this.capacity / C)) : new Array(), this.metrics[s] = {
255
+ momentaryLoudness: Number.NEGATIVE_INFINITY,
256
+ shortTermLoudness: Number.NEGATIVE_INFINITY,
257
+ integratedLoudness: Number.NEGATIVE_INFINITY,
258
+ maximumMomentaryLoudness: Number.NEGATIVE_INFINITY,
259
+ maximumShortTermLoudness: Number.NEGATIVE_INFINITY,
260
+ maximumTruePeakLevel: Number.NEGATIVE_INFINITY,
261
+ loudnessRange: Number.NEGATIVE_INFINITY
262
+ };
263
+ }
264
+ process(e, r) {
265
+ const n = e.length;
266
+ for (let s = 0; s < n; s++) {
267
+ if (!e[s].length) continue;
268
+ const u = e[s], c = u.length, A = u[0].length, H = Object.values(G[c] || G[1]), x = Math.pow(10, -w / 20);
269
+ (!this.kWeightingFilters[s] || this.kWeightingFilters[s].length !== c) && (this.kWeightingFilters[s] = Array.from({ length: c }, () => [
270
+ new O(L.highshelf.a, L.highshelf.b),
271
+ new O(L.highpass.a, L.highpass.b)
272
+ ])), (!this.truePeakFilters[s] || this.truePeakFilters[s].length !== c) && (this.truePeakFilters[s] = Array.from({ length: c }, () => [
273
+ new F(R.lowpass.phase0),
274
+ new F(R.lowpass.phase1),
275
+ new F(R.lowpass.phase2),
276
+ new F(R.lowpass.phase3)
277
+ ]));
278
+ for (let o = 0; o < A; o++) {
279
+ let i = 0;
280
+ for (let t = 0; t < c; t++) {
281
+ const h = u[t][o], [y, T] = this.kWeightingFilters[s][t], m = y.process(h), f = T.process(m) ** 2, v = H[t] ?? 1;
282
+ i += f * v;
283
+ const P = u[t][o] * x, k = sampleRate >= 96e3 ? 2 : 4, b = [];
284
+ for (let N = 0; N < k; N++) {
285
+ const D = this.truePeakFilters[s][t][N];
286
+ b.push(Math.abs(D.process(P)));
287
+ }
288
+ const W = 20 * Math.log10(Math.max(...b)) + w, V = this.metrics[s].maximumTruePeakLevel;
289
+ this.metrics[s].maximumTruePeakLevel = Math.max(V, W);
290
+ }
291
+ const g = i, p = this.momentaryEnergyBuffers[s].peek() ?? 0, I = this.momentaryEnergyBuffers[s].isFull() ? p : 0;
292
+ this.momentaryEnergyRunningSums[s] += g - I, this.momentaryEnergyBuffers[s].push(g);
293
+ const E = this.shortTermEnergyBuffers[s].peek() ?? 0, l = this.shortTermEnergyBuffers[s].isFull() ? E : 0;
294
+ if (this.shortTermEnergyRunningSums[s] += g - l, this.shortTermEnergyBuffers[s].push(g), this.momentaryEnergyBuffers[s].isFull()) {
295
+ const t = this.momentaryEnergyRunningSums[s] / this.momentaryEnergyBuffers[s].capacity, h = this.#s(t);
296
+ this.metrics[s].momentaryLoudness = h, this.metrics[s].maximumMomentaryLoudness = Math.max(
297
+ this.metrics[s].maximumMomentaryLoudness,
298
+ h
299
+ );
300
+ }
301
+ }
302
+ this.momentarySampleAccumulators[s] += A;
303
+ const _ = Math.round(sampleRate * B);
304
+ for (; this.momentarySampleAccumulators[s] >= _; ) {
305
+ if (this.momentaryEnergyBuffers[s].isFull()) {
306
+ const o = this.momentaryEnergyRunningSums[s] / this.momentaryEnergyBuffers[s].capacity, i = this.#s(o);
307
+ this.momentaryLoudnessHistories[s].push(i);
308
+ }
309
+ this.momentarySampleAccumulators[s] -= _;
310
+ }
311
+ this.shortTermSampleAccumulators[s] += A;
312
+ const M = Math.round(sampleRate * C);
313
+ for (; this.shortTermSampleAccumulators[s] >= M; ) {
314
+ if (this.shortTermEnergyBuffers[s].isFull()) {
315
+ const o = this.shortTermEnergyRunningSums[s] / this.shortTermEnergyBuffers[s].capacity, i = this.#s(o);
316
+ this.metrics[s].shortTermLoudness = i, this.metrics[s].maximumShortTermLoudness = Math.max(
317
+ this.metrics[s].maximumShortTermLoudness,
318
+ i
319
+ ), this.shortTermLoudnessHistories[s].push(i);
320
+ }
321
+ this.shortTermSampleAccumulators[s] -= M;
322
+ }
323
+ if (this.momentaryLoudnessHistories[s].length > 2) {
324
+ const o = Array.from(this.momentaryLoudnessHistories[s]).filter(
325
+ (i) => i > z
326
+ );
327
+ if (o.length > 2) {
328
+ const i = o.map(this.#r), p = i.reduce((t, h) => t + h, 0) / i.length, E = this.#s(p) + K, l = o.filter((t) => t > E);
329
+ if (l.length > 2) {
330
+ const t = l.map(this.#r), y = t.reduce((m, a) => m + a, 0) / t.length, T = this.#s(y);
331
+ this.metrics[s].integratedLoudness = T;
332
+ }
333
+ }
334
+ }
335
+ if (this.shortTermLoudnessHistories[s].length > 2) {
336
+ const o = Array.from(this.shortTermLoudnessHistories[s]).filter(
337
+ (i) => i > J
338
+ );
339
+ if (o.length > 2) {
340
+ const i = o.map(this.#r), p = i.reduce((t, h) => t + h, 0) / i.length, E = this.#s(p) + Q, l = o.filter((t) => t > E);
341
+ if (l.length > 2) {
342
+ const t = l.toSorted((m, a) => m - a), [h, y] = [
343
+ j,
344
+ q
345
+ ].map((m) => {
346
+ const a = Math.floor(m * (t.length - 1)), f = Math.ceil(m * (t.length - 1));
347
+ return f === a ? t[a] : t[a] + (t[f] - t[a]) * (m * (t.length - 1) - a);
348
+ }), T = y - h;
349
+ this.metrics[s].loudnessRange = T;
350
+ }
351
+ }
352
+ }
353
+ }
354
+ return this.#e(e, r), this.#t(), !0;
355
+ }
356
+ #t() {
357
+ if (currentTime - this.lastTime >= Number(this.interval)) {
358
+ const e = { currentFrame, currentTime, currentMetrics: this.metrics };
359
+ this.port.postMessage(e), this.lastTime = currentTime;
360
+ }
361
+ }
362
+ #e(e, r) {
363
+ for (let n = 0; n < Math.min(e.length, r.length); n++)
364
+ for (let s = 0; s < Math.min(e[n].length, r[n].length); s++)
365
+ r[n][s].set(e[n][s]);
366
+ }
367
+ #s(e) {
368
+ return -0.691 + 10 * Math.log10(Math.max(e, Number.EPSILON));
369
+ }
370
+ #r(e) {
371
+ return Math.pow(10, (e + 0.691) / 10);
372
+ }
373
+ }
374
+ registerProcessor("loudness-processor", X);
375
+ `, e = URL.createObjectURL(new Blob([i], { type: "application/javascript" })), t = "loudness-processor";
376
+ class h extends AudioWorkletNode {
377
+ constructor(n, r) {
378
+ super(n, t, r);
379
+ }
380
+ static async loadModule(n) {
381
+ return await n.audioWorklet.addModule(e);
382
+ }
383
+ }
384
+ async function o(s, n) {
385
+ return await s.audioWorklet.addModule(e), new AudioWorkletNode(s, t, n);
386
+ }
387
+ export {
388
+ h as LoudnessWorkletNode,
389
+ o as createLoudnessWorklet
390
+ };
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "loudness-worklet",
3
+ "version": "1.4.2",
4
+ "description": "A lightweight and efficient AudioWorklet for real-time loudness measurement in the browser, compliant with the ITU-R BS.1770-5 standard.",
5
+ "type": "module",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "license": "MIT",
18
+ "author": "lcweden",
19
+ "homepage": "https://lcweden.github.io/loudness-audio-worklet-processor/",
20
+ "repository": {
21
+ "url": "git+https://github.com/lcweden/loudness-audio-worklet-processor.git"
22
+ },
23
+ "bugs": {
24
+ "url": "https://github.com/lcweden/loudness-audio-worklet-processor/issues"
25
+ },
26
+ "scripts": {
27
+ "dev": "vite --mode dev",
28
+ "demo": "vite --mode demo",
29
+ "test": "vitest",
30
+ "build": "vite build --mode static && vite build --mode demo",
31
+ "lib": "vite build --mode static && vite build --mode lib && tsc -p tsconfig.lib.json",
32
+ "prepare": "npm run lib",
33
+ "preview": "vite preview --mode demo",
34
+ "format": "prettier --write ."
35
+ },
36
+ "devDependencies": {
37
+ "@solidjs/router": "^0.15.3",
38
+ "@tailwindcss/vite": "^4.1.4",
39
+ "@types/audioworklet": "^0.0.85",
40
+ "@types/node": "^22.14.1",
41
+ "daisyui": "^5.0.28",
42
+ "echarts": "^6.0.0",
43
+ "prettier": "^3.5.3",
44
+ "prettier-plugin-tailwindcss": "^0.6.11",
45
+ "solid-js": "^1.9.5",
46
+ "tailwindcss": "^4.1.4",
47
+ "typescript": "~5.7.2",
48
+ "vite": "^6.3.1",
49
+ "vite-plugin-solid": "^2.11.6",
50
+ "vitest": "^3.2.2"
51
+ }
52
+ }