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 +21 -0
- package/README.md +343 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +390 -0
- package/package.json +52 -0
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
|
+
[](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)
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|