loudness-worklet 1.6.3 → 1.6.6
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/README.md +110 -105
- package/package.json +7 -15
- package/packages/lib/dist/index.js +80 -71
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
# Loudness
|
|
1
|
+
# Loudness Worklet
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/loudness-worklet)
|
|
4
4
|
[](LICENSE)
|
|
5
|
+
[](https://lcweden.github.io/loudness-worklet/)
|
|
5
6
|
|
|
6
7
|
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.
|
|
7
8
|
|
|
@@ -9,10 +10,17 @@ A loudness meter for the `Web Audio API`, based on the [ITU-R BS.1770-5](https:/
|
|
|
9
10
|
|
|
10
11
|
## Features
|
|
11
12
|
|
|
12
|
-
- **
|
|
13
|
-
- **Comprehensive Metrics**: Calculates Momentary, Short-term, and Integrated Loudness,
|
|
14
|
-
- **
|
|
15
|
-
- **
|
|
13
|
+
- **Standard Compliant**: Strictly follows **ITU-R BS.1770-5** for accurate loudness measurement.
|
|
14
|
+
- **Comprehensive Metrics**: Calculates Momentary, Short-term, and Integrated Loudness, plus Loudness Range (LRA) and True-Peak levels.
|
|
15
|
+
- **Versatile Input**: Seamlessly supports both live audio streams ("Microphone/WebRTC") and offline file analysis.
|
|
16
|
+
- **Zero Dependencies**: Lightweight, pure AudioWorklet implementation requiring no external libraries.
|
|
17
|
+
|
|
18
|
+
## Use Cases
|
|
19
|
+
|
|
20
|
+
- **Volume Normalization**: Dynamically analyze and harmonize volume levels across multi-source playlists directly in the browser, eliminating the need for server-side processing.
|
|
21
|
+
- **Pre-upload Validation**: Verify if audio files meet platform-specific loudness standards (e.g., -14 LUFS) locally before uploading.
|
|
22
|
+
- **Live Metering**: Build responsive, standard-compliant loudness meters for web-based recorders, conferencing tools, or broadcasting interfaces.
|
|
23
|
+
- **Web Audio Editors**: Integrate professional-grade loudness analysis into browser-based Digital Audio Workstations (DAWs).
|
|
16
24
|
|
|
17
25
|
## Installation
|
|
18
26
|
|
|
@@ -21,8 +29,8 @@ A loudness meter for the `Web Audio API`, based on the [ITU-R BS.1770-5](https:/
|
|
|
21
29
|
Import directly in your code:
|
|
22
30
|
|
|
23
31
|
```javascript
|
|
24
|
-
const
|
|
25
|
-
await audioContext.audioWorklet.addModule(
|
|
32
|
+
const moduleUrl = "https://lcweden.github.io/loudness-worklet/loudness.worklet.js";
|
|
33
|
+
await audioContext.audioWorklet.addModule(moduleUrl);
|
|
26
34
|
const worklet = new AudioWorkletNode(audioContext, "loudness-processor");
|
|
27
35
|
```
|
|
28
36
|
|
|
@@ -47,63 +55,18 @@ npm install loudness-worklet
|
|
|
47
55
|
Use helper functions to create and load the worklet:
|
|
48
56
|
|
|
49
57
|
```javascript
|
|
50
|
-
import { createLoudnessWorklet
|
|
58
|
+
import { createLoudnessWorklet } from "loudness-worklet";
|
|
51
59
|
|
|
52
60
|
const worklet = await createLoudnessWorklet(audioContext);
|
|
53
|
-
|
|
54
|
-
// or
|
|
55
|
-
|
|
56
|
-
await LoudnessWorkletNode.loadModule(audioContext);
|
|
57
|
-
const worklet = new LoudnessWorkletNode(audioContext);
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
## Concepts
|
|
61
|
-
|
|
62
|
-
### Contexts
|
|
63
|
-
|
|
64
|
-
Provide the execution environment for audio processing.
|
|
65
|
-
|
|
66
|
-
#### AudioContext
|
|
67
|
-
|
|
68
|
-
`AudioContext` is used for real-time audio processing, such as live audio input from a microphone or media stream.
|
|
69
|
-
|
|
70
|
-
#### OfflineAudioContext
|
|
71
|
-
|
|
72
|
-
`OfflineAudioContext` is used for processing audio data offline, allowing for rendering and analysis without requiring real-time playback.
|
|
73
|
-
|
|
74
|
-
### Nodes
|
|
75
|
-
|
|
76
|
-
Nodes are the building blocks of an audio graph, representing audio sources, processing modules, and destinations. The following nodes are commonly used as a source input:
|
|
77
|
-
|
|
78
|
-
#### AudioBufferSourceNode
|
|
79
|
-
|
|
80
|
-
`AudioBufferSourceNode` is used to play audio data stored in an `AudioBuffer`, typically for pre-recorded audio files.
|
|
81
|
-
|
|
82
|
-
```javascript
|
|
83
|
-
const audioContext = new AudioContext();
|
|
84
|
-
const arrayBuffer = await file.arrayBuffer();
|
|
85
|
-
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
|
|
86
|
-
const bufferSource = new AudioBufferSourceNode(audioContext, { buffer: audioBuffer });
|
|
87
61
|
```
|
|
88
62
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
`MediaStreamAudioSourceNode` is used to play audio from a `MediaStream`, such as a live microphone input or a video element.
|
|
63
|
+
or
|
|
92
64
|
|
|
93
65
|
```javascript
|
|
94
|
-
|
|
95
|
-
const mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
96
|
-
const mediaStreamSource = new MediaStreamAudioSourceNode(audioContext, { mediaStream });
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
#### MediaElementAudioSourceNode
|
|
100
|
-
|
|
101
|
-
`MediaElementAudioSourceNode` is used to play audio from an HTML `<audio>` or `<video>` element.
|
|
66
|
+
import { LoudnessWorkletNode } from "loudness-worklet";
|
|
102
67
|
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
const mediaElement = document.querySelector("audio");
|
|
106
|
-
const elementSource = new MediaElementAudioSourceNode(audioContext, { mediaElement });
|
|
68
|
+
await LoudnessWorkletNode.loadModule(audioContext);
|
|
69
|
+
const worklet = new LoudnessWorkletNode(audioContext);
|
|
107
70
|
```
|
|
108
71
|
|
|
109
72
|
## Quick Start
|
|
@@ -119,24 +82,24 @@ This example shows the easiest way to get started with the Loudness Audio Workle
|
|
|
119
82
|
<button>Share Screen</button>
|
|
120
83
|
<pre></pre>
|
|
121
84
|
<script>
|
|
122
|
-
const
|
|
85
|
+
const moduleUrl = "https://lcweden.github.io/loudness-worklet/loudness.worklet.js";
|
|
123
86
|
const button = document.querySelector("button");
|
|
124
87
|
const pre = document.querySelector("pre");
|
|
125
88
|
|
|
126
89
|
button.onclick = async () => {
|
|
127
|
-
// Get the screen stream with audio, for example a
|
|
90
|
+
// Get the screen stream with audio, for example a YouTube tab
|
|
128
91
|
const mediaStream = await navigator.mediaDevices.getDisplayMedia({ audio: true });
|
|
129
92
|
const context = new AudioContext();
|
|
130
93
|
|
|
131
94
|
// Load the loudness worklet processor
|
|
132
|
-
await context.audioWorklet.addModule(
|
|
95
|
+
await context.audioWorklet.addModule(moduleUrl);
|
|
133
96
|
|
|
134
97
|
// Create the audio node from the stream
|
|
135
98
|
const source = new MediaStreamAudioSourceNode(context, { mediaStream });
|
|
136
99
|
// Create the loudness worklet node
|
|
137
100
|
const worklet = new AudioWorkletNode(context, "loudness-processor", {
|
|
138
101
|
processorOptions: {
|
|
139
|
-
interval: 0.
|
|
102
|
+
interval: 0.02, // every 0.02s a message will be sent
|
|
140
103
|
capacity: 600 // 1 minute of history can be stored
|
|
141
104
|
}
|
|
142
105
|
});
|
|
@@ -204,6 +167,55 @@ source.connect(worklet);
|
|
|
204
167
|
// Optionally connect to destination for monitoring (echo)
|
|
205
168
|
```
|
|
206
169
|
|
|
170
|
+
## Concepts
|
|
171
|
+
|
|
172
|
+
### Contexts
|
|
173
|
+
|
|
174
|
+
Provide the execution environment for audio processing.
|
|
175
|
+
|
|
176
|
+
#### AudioContext
|
|
177
|
+
|
|
178
|
+
`AudioContext` is used for real-time audio processing, such as live audio input from a microphone or media stream.
|
|
179
|
+
|
|
180
|
+
#### OfflineAudioContext
|
|
181
|
+
|
|
182
|
+
`OfflineAudioContext` is used for processing audio data offline, allowing for rendering and analysis without requiring real-time playback.
|
|
183
|
+
|
|
184
|
+
### Nodes
|
|
185
|
+
|
|
186
|
+
Nodes are the building blocks of an audio graph, representing audio sources, processing modules, and destinations. The following nodes are commonly used as a source input:
|
|
187
|
+
|
|
188
|
+
#### AudioBufferSourceNode
|
|
189
|
+
|
|
190
|
+
`AudioBufferSourceNode` is used to play audio data stored in an `AudioBuffer`, typically for pre-recorded audio files.
|
|
191
|
+
|
|
192
|
+
```javascript
|
|
193
|
+
const audioContext = new AudioContext();
|
|
194
|
+
const arrayBuffer = await file.arrayBuffer();
|
|
195
|
+
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
|
|
196
|
+
const bufferSource = new AudioBufferSourceNode(audioContext, { buffer: audioBuffer });
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
#### MediaStreamAudioSourceNode
|
|
200
|
+
|
|
201
|
+
`MediaStreamAudioSourceNode` is used to play audio from a `MediaStream`, such as a live microphone input or a video element.
|
|
202
|
+
|
|
203
|
+
```javascript
|
|
204
|
+
const audioContext = new AudioContext();
|
|
205
|
+
const mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
206
|
+
const mediaStreamSource = new MediaStreamAudioSourceNode(audioContext, { mediaStream });
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
#### MediaElementAudioSourceNode
|
|
210
|
+
|
|
211
|
+
`MediaElementAudioSourceNode` is used to play audio from an HTML `<audio>` or `<video>` element.
|
|
212
|
+
|
|
213
|
+
```javascript
|
|
214
|
+
const audioContext = new AudioContext();
|
|
215
|
+
const mediaElement = document.querySelector("audio");
|
|
216
|
+
const elementSource = new MediaElementAudioSourceNode(audioContext, { mediaElement });
|
|
217
|
+
```
|
|
218
|
+
|
|
207
219
|
## API
|
|
208
220
|
|
|
209
221
|
### Options
|
|
@@ -214,11 +226,8 @@ The `AudioWorkletNode` constructor accepts the following options:
|
|
|
214
226
|
|
|
215
227
|
| Option | Type | Required | Default | Description |
|
|
216
228
|
| ------------------------- | ---------- | -------- | ------- | ---------------------------------------------------------------------------------------------------- |
|
|
217
|
-
|
|
|
218
|
-
|
|
|
219
|
-
| outputChannelCount | `number[]` | `N` | - | Determined at runtime automatically. |
|
|
220
|
-
| processorOptions.interval | `number` | `N` | `null` | Message interval in seconds. |
|
|
221
|
-
| processorOptions.capacity | `number` | `N` | `null` | Maximum seconds of history to keep. If set to `null`, the processor will not limit the history size. |
|
|
229
|
+
| processorOptions.interval | `number` | `No` | `0.02` | Message interval in seconds. |
|
|
230
|
+
| processorOptions.capacity | `number` | `No` | `0` | Maximum seconds of history to keep. If set to `0`, the processor will not limit the history size. |
|
|
222
231
|
|
|
223
232
|
#### Example
|
|
224
233
|
|
|
@@ -227,19 +236,16 @@ Most of the time, you only need to set `processorOptions`.
|
|
|
227
236
|
```javascript
|
|
228
237
|
const { numberOfChannels, length, sampleRate } = audioBuffer;
|
|
229
238
|
const worklet = new AudioWorkletNode(context, "loudness-processor", {
|
|
230
|
-
numberOfInputs: 1,
|
|
231
|
-
numberOfOutputs: 1,
|
|
232
|
-
outputChannelCount: [numberOfChannels], // Unnecessary
|
|
233
239
|
processorOptions: {
|
|
234
240
|
capacity: length / sampleRate,
|
|
235
|
-
interval: 0.
|
|
241
|
+
interval: 0.02
|
|
236
242
|
}
|
|
237
243
|
});
|
|
238
244
|
```
|
|
239
245
|
|
|
240
246
|
### Message Format
|
|
241
247
|
|
|
242
|
-
Measurement results are sent back to the main thread via `port.onmessage`
|
|
248
|
+
Measurement results are sent back to the main thread via `port.onmessage` as a `LoudnessSnapshot` object:
|
|
243
249
|
|
|
244
250
|
```typescript
|
|
245
251
|
type LoudnessMeasurements = {
|
|
@@ -269,7 +275,7 @@ type LoudnessSnapshot = {
|
|
|
269
275
|
| `maximumMomentaryLoudness` | `LUFS`/`LKFS` |
|
|
270
276
|
| `maximumShortTermLoudness` | `LUFS`/`LKFS` |
|
|
271
277
|
| `maximumTruePeakLevel` | `dBTP` |
|
|
272
|
-
| `loudnessRange` | `LU`
|
|
278
|
+
| `loudnessRange` | `LU` |
|
|
273
279
|
|
|
274
280
|
### Supported Channels
|
|
275
281
|
|
|
@@ -284,7 +290,7 @@ The following coefficients are used for the K-weighting filter:
|
|
|
284
290
|
|
|
285
291
|
| | highshelf | highpass |
|
|
286
292
|
| --- | ----------------- | ----------------- |
|
|
287
|
-
| a1 | -1.69065929318241 |
|
|
293
|
+
| a1 | -1.69065929318241 | -1.99004745483398 |
|
|
288
294
|
| a2 | 0.73248077421585 | 0.99007225036621 |
|
|
289
295
|
| b0 | 1.53512485958697 | 1.0 |
|
|
290
296
|
| b1 | -2.69169618940638 | -2.0 |
|
|
@@ -294,25 +300,24 @@ The following FIR filter coefficients are used for true-peak measurement:
|
|
|
294
300
|
|
|
295
301
|
| Phase 0 | Phase 1 | Phase 2 | Phase 3 |
|
|
296
302
|
| ---------------- | ---------------- | ---------------- | ---------------- |
|
|
297
|
-
| 0.0017089843750 |
|
|
303
|
+
| 0.0017089843750 | -0.0291748046875 | -0.0189208984375 | -0.0083007812500 |
|
|
298
304
|
| 0.0109863281250 | 0.0292968750000 | 0.0330810546875 | 0.0148925781250 |
|
|
299
|
-
|
|
|
305
|
+
| -0.0196533203125 | -0.0517578125000 | -0.0582275390625 | -0.0266113281250 |
|
|
300
306
|
| 0.0332031250000 | 0.0891113281250 | 0.1015625000000 | 0.0476074218750 |
|
|
301
|
-
|
|
|
307
|
+
| -0.0594482421875 | -0.1665039062500 | -0.2003173828125 | -0.1022949218750 |
|
|
302
308
|
| 0.1373291015625 | 0.4650878906250 | 0.7797851562500 | 0.9721679687500 |
|
|
303
309
|
| 0.9721679687500 | 0.7797851562500 | 0.4650878906250 | 0.1373291015625 |
|
|
304
|
-
|
|
|
310
|
+
| -0.1022949218750 | -0.2003173828125 | -0.1665039062500 | -0.0594482421875 |
|
|
305
311
|
| 0.0476074218750 | 0.1015625000000 | 0.0891113281250 | 0.0332031250000 |
|
|
306
|
-
|
|
|
312
|
+
| -0.0266113281250 | -0.0582275390625 | -0.0517578125000 | -0.0196533203125 |
|
|
307
313
|
| 0.0148925781250 | 0.0330810546875 | 0.0292968750000 | 0.0109863281250 |
|
|
308
|
-
|
|
|
314
|
+
| -0.0083007812500 | -0.0189208984375 | -0.0291748046875 | 0.0017089843750 |
|
|
309
315
|
|
|
310
316
|
## Validation
|
|
311
317
|
|
|
312
318
|
### ITU-R BS.2217
|
|
313
319
|
|
|
314
|
-
|
|
315
|
-
meets the specifications within Recommendation [ITU-R BS.1770](https://www.itu.int/rec/R-REC-BS.1770).
|
|
320
|
+
Code correctness is verified against the official **[ITU-R BS.2217](https://www.itu.int/pub/R-REP-BS.2217)** compliance test suite, ensuring strict adherence to the **[ITU-R BS.1770](https://www.itu.int/rec/R-REC-BS.1770)** specification.
|
|
316
321
|
|
|
317
322
|
| file | measurement | channels | |
|
|
318
323
|
| ------------------------------------ | ----------- | -------- | ------------------ |
|
|
@@ -371,33 +376,33 @@ meets the specifications within Recommendation [ITU-R BS.1770](https://www.itu.i
|
|
|
371
376
|
|
|
372
377
|
### EBU TECH 3341 Minimum requirements test signals
|
|
373
378
|
|
|
374
|
-
[EBU TECH 3341](https://tech.ebu.ch/publications/tech3341)
|
|
379
|
+
Validated against **[EBU TECH 3341](https://tech.ebu.ch/publications/tech3341)** minimum requirements for loudness metering, including gating behavior, time scales, and true-peak accuracy.
|
|
375
380
|
|
|
376
381
|
| file | expected response and accepted tolerances | |
|
|
377
382
|
| ------------------------------------ | ----------------------------------------------------------- | ------------------ |
|
|
378
|
-
| seq-3341-1-16bit | M, S, I =
|
|
379
|
-
| seq-3341-2-16bit | M, S, I =
|
|
380
|
-
| seq-3341-3-16bit-v02 | I =
|
|
381
|
-
| seq-3341-4-16bit-v02 | I =
|
|
382
|
-
| seq-3341-5-16bit-v02 | I =
|
|
383
|
-
| seq-3341-6-6channels-WAVEEX-16bit | I =
|
|
384
|
-
| seq-3341-7_seq-3342-5-24bit | I =
|
|
385
|
-
| seq-3341-2011-8_seq-3342-6-24bit-v02 | I =
|
|
386
|
-
| seq-3341-9-24bit | S =
|
|
387
|
-
| seq-3341-10-\*-24bit | Max S =
|
|
388
|
-
| seq-3341-11-24bit | Max S =
|
|
389
|
-
| seq-3341-12-24bit | M =
|
|
390
|
-
| seq-3341-13-\*-24bit | Max M =
|
|
391
|
-
| seq-3341-14-24bit | Max M =
|
|
392
|
-
| seq-3341-15-24bit | Max true-peak =
|
|
393
|
-
| seq-3341-16-24bit | Max true-peak =
|
|
394
|
-
| seq-3341-17-24bit | Max true-peak =
|
|
395
|
-
| seq-3341-18-24bit | Max true-peak =
|
|
396
|
-
| seq-3341-19-24bit | Max true-peak = +3.0 +0.2
|
|
397
|
-
| seq-3341-20-24bit | Max true-peak = 0.0 +0.2
|
|
398
|
-
| seq-3341-21-24bit | Max true-peak = 0.0 +0.2
|
|
399
|
-
| seq-3341-22-24bit | Max true-peak = 0.0 +0.2
|
|
400
|
-
| seq-3341-23-24bit | Max true-peak = 0.0 +0.2
|
|
383
|
+
| seq-3341-1-16bit | M, S, I = -23.0 ±0.1 LUFS | :white_check_mark: |
|
|
384
|
+
| seq-3341-2-16bit | M, S, I = -33.0 ±0.1 LUFS | :white_check_mark: |
|
|
385
|
+
| seq-3341-3-16bit-v02 | I = -23.0 ±0.1 LUFS | :white_check_mark: |
|
|
386
|
+
| seq-3341-4-16bit-v02 | I = -23.0 ±0.1 LUFS | :white_check_mark: |
|
|
387
|
+
| seq-3341-5-16bit-v02 | I = -23.0 ±0.1 LUFS | :white_check_mark: |
|
|
388
|
+
| seq-3341-6-6channels-WAVEEX-16bit | I = -23.0 ±0.1 LUFS | :white_check_mark: |
|
|
389
|
+
| seq-3341-7_seq-3342-5-24bit | I = -23.0 ±0.1 LUFS | :white_check_mark: |
|
|
390
|
+
| seq-3341-2011-8_seq-3342-6-24bit-v02 | I = -23.0 ±0.1 LUFS | :white_check_mark: |
|
|
391
|
+
| seq-3341-9-24bit | S = -23.0 ±0.1 LUFS, constant after 3 s | :white_check_mark: |
|
|
392
|
+
| seq-3341-10-\*-24bit | Max S = -23.0 ±0.1 LUFS, for each segment | :white_check_mark: |
|
|
393
|
+
| seq-3341-11-24bit | Max S = -38.0, -37.0, …, -19.0 ±0.1 LUFS, successive values | :white_check_mark: |
|
|
394
|
+
| seq-3341-12-24bit | M = -23.0 ±0.1 LUFS, constant after 1 s | :white_check_mark: |
|
|
395
|
+
| seq-3341-13-\*-24bit | Max M = -23.0 ±0.1 LUFS, for each segment | :white_check_mark: |
|
|
396
|
+
| seq-3341-14-24bit | Max M = -38.0, …, -19.0 ±0.1 LUFS, successive values | :white_check_mark: |
|
|
397
|
+
| seq-3341-15-24bit | Max true-peak = -6.0 +0.2/-0.4 dBTP | :white_check_mark: |
|
|
398
|
+
| seq-3341-16-24bit | Max true-peak = -6.0 +0.2/-0.4 dBTP | :white_check_mark: |
|
|
399
|
+
| seq-3341-17-24bit | Max true-peak = -6.0 +0.2/-0.4 dBTP | :white_check_mark: |
|
|
400
|
+
| seq-3341-18-24bit | Max true-peak = -6.0 +0.2/-0.4 dBTP | :white_check_mark: |
|
|
401
|
+
| seq-3341-19-24bit | Max true-peak = +3.0 +0.2/-0.4 dBTP | :white_check_mark: |
|
|
402
|
+
| seq-3341-20-24bit | Max true-peak = 0.0 +0.2/-0.4 dBTP | :white_check_mark: |
|
|
403
|
+
| seq-3341-21-24bit | Max true-peak = 0.0 +0.2/-0.4 dBTP | :white_check_mark: |
|
|
404
|
+
| seq-3341-22-24bit | Max true-peak = 0.0 +0.2/-0.4 dBTP | :white_check_mark: |
|
|
405
|
+
| seq-3341-23-24bit | Max true-peak = 0.0 +0.2/-0.4 dBTP | :white_check_mark: |
|
|
401
406
|
|
|
402
407
|
### EBU TECH 3342 Minimum requirements test signals
|
|
403
408
|
|
package/package.json
CHANGED
|
@@ -1,24 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loudness-worklet",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.6",
|
|
4
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
5
|
"keywords": [
|
|
6
6
|
"web-audio",
|
|
7
|
-
"web-audio-api",
|
|
8
|
-
"audio-meter",
|
|
9
7
|
"audio-worklet",
|
|
10
8
|
"loudness",
|
|
11
|
-
"loudness-meter",
|
|
12
|
-
"browser-audio",
|
|
13
9
|
"ebu-r128",
|
|
14
|
-
"itu-r-bs1770"
|
|
15
|
-
"audio-processing",
|
|
16
|
-
"audio-analysis",
|
|
17
|
-
"true-peak",
|
|
18
|
-
"lufs",
|
|
19
|
-
"lra",
|
|
20
|
-
"javascript",
|
|
21
|
-
"typescript"
|
|
10
|
+
"itu-r-bs1770"
|
|
22
11
|
],
|
|
23
12
|
"type": "module",
|
|
24
13
|
"main": "./packages/lib/dist/index.js",
|
|
@@ -50,7 +39,10 @@
|
|
|
50
39
|
"lint": "biome lint --write"
|
|
51
40
|
},
|
|
52
41
|
"devDependencies": {
|
|
53
|
-
"@biomejs/biome": "2.3.
|
|
54
|
-
"@types/node": "^24.10.1"
|
|
42
|
+
"@biomejs/biome": "2.3.13",
|
|
43
|
+
"@types/node": "^24.10.1",
|
|
44
|
+
"typescript": "^5.9.3",
|
|
45
|
+
"vite": "^7.3.1",
|
|
46
|
+
"vite-plugin-dts": "^4.5.4"
|
|
55
47
|
}
|
|
56
48
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
const i = `const
|
|
1
|
+
const i = `const C = {
|
|
2
2
|
highshelf: {
|
|
3
3
|
a: [-1.69065929318241, 0.73248077421585],
|
|
4
4
|
b: [1.53512485958697, -2.69169618940638, 1.19839281085285]
|
|
5
5
|
},
|
|
6
6
|
highpass: { a: [-1.99004745483398, 0.99007225036621], b: [1, -2, 1] }
|
|
7
|
-
},
|
|
7
|
+
}, x = {
|
|
8
8
|
lowpass: {
|
|
9
9
|
phase0: [
|
|
10
10
|
0.001708984375,
|
|
@@ -96,8 +96,8 @@ const i = `const x = {
|
|
|
96
96
|
1,
|
|
97
97
|
1
|
|
98
98
|
]
|
|
99
|
-
}, P = 0.4,
|
|
100
|
-
class
|
|
99
|
+
}, P = 0.4, O = 0.1, H = 3, G = 0.1, W = 0.1, k = 0.95, M = 12.04, U = -70, Y = -10, z = -70, j = -20;
|
|
100
|
+
class V {
|
|
101
101
|
#t = new Float32Array(2);
|
|
102
102
|
#e = new Float32Array(3);
|
|
103
103
|
#s = new Float32Array(2);
|
|
@@ -155,9 +155,9 @@ class N {
|
|
|
155
155
|
process(e) {
|
|
156
156
|
this.#e[this.#s] = e, this.#s = (this.#s + 1) % this.#e.length;
|
|
157
157
|
let i = 0;
|
|
158
|
-
for (let
|
|
159
|
-
const s = (this.#s - 1 -
|
|
160
|
-
i += this.#t[
|
|
158
|
+
for (let u = 0; u < this.#t.length; u++) {
|
|
159
|
+
const s = (this.#s - 1 - u + this.#e.length) % this.#e.length;
|
|
160
|
+
i += this.#t[u] * this.#e[s];
|
|
161
161
|
}
|
|
162
162
|
return i;
|
|
163
163
|
}
|
|
@@ -217,12 +217,12 @@ class I {
|
|
|
217
217
|
slice(e, i) {
|
|
218
218
|
if (e >= i)
|
|
219
219
|
return [];
|
|
220
|
-
const
|
|
220
|
+
const u = [];
|
|
221
221
|
for (let s = Math.max(0, e); s < Math.min(this.#i, i); s++) {
|
|
222
222
|
const n = (this.#s + s) % this.#e;
|
|
223
|
-
|
|
223
|
+
u.push(this.#t[n]);
|
|
224
224
|
}
|
|
225
|
-
return
|
|
225
|
+
return u;
|
|
226
226
|
}
|
|
227
227
|
/**
|
|
228
228
|
* Adds an item to the buffer and
|
|
@@ -265,11 +265,11 @@ class I {
|
|
|
265
265
|
}
|
|
266
266
|
}
|
|
267
267
|
}
|
|
268
|
-
class
|
|
268
|
+
class K extends AudioWorkletProcessor {
|
|
269
269
|
capacity;
|
|
270
270
|
interval;
|
|
271
271
|
previousTime = 0;
|
|
272
|
-
attenuation = 10 ** (-
|
|
272
|
+
attenuation = 10 ** (-M / 20);
|
|
273
273
|
measurements = [];
|
|
274
274
|
kWeightingFilters = [];
|
|
275
275
|
overSamplingFilters = [];
|
|
@@ -287,11 +287,20 @@ class j extends AudioWorkletProcessor {
|
|
|
287
287
|
sTraceDirtyFlags = [];
|
|
288
288
|
constructor(e) {
|
|
289
289
|
super();
|
|
290
|
-
const { numberOfInputs: i = 1, processorOptions:
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
290
|
+
const { numberOfInputs: i = 1, processorOptions: u } = e ?? {};
|
|
291
|
+
if (typeof i != "number" || !Number.isInteger(i) || i < 1)
|
|
292
|
+
throw new Error("numberOfInputs must be a positive integer.");
|
|
293
|
+
if (u && typeof u != "object")
|
|
294
|
+
throw new Error("processorOptions must be an object.");
|
|
295
|
+
const { capacity: s, interval: n } = u ?? {};
|
|
296
|
+
if (s !== void 0 && (typeof s != "number" || !Number.isFinite(s) || s < 0))
|
|
297
|
+
throw new Error("Capacity must be a non-negative finite number.");
|
|
298
|
+
if (n !== void 0 && (typeof n != "number" || !Number.isFinite(n) || n < 0))
|
|
299
|
+
throw new Error("Interval must be a non-negative finite number.");
|
|
300
|
+
this.capacity = s || 0, this.interval = n ?? 0.02;
|
|
301
|
+
for (let a = 0; a < i; a++) {
|
|
302
|
+
const F = Math.round(sampleRate * P), E = Math.round(sampleRate * H), L = Math.ceil(this.capacity / O), v = Math.ceil(this.capacity / G);
|
|
303
|
+
this.mEnergySums[a] = 0, this.mSampleAccumulators[a] = 0, this.mEnergyBuffers[a] = new I(F), this.mTraces[a] = this.capacity ? new I(L) : [], this.sEnergySums[a] = 0, this.sSampleAccumulators[a] = 0, this.sEnergyBuffers[a] = new I(E), this.sTraces[a] = this.capacity ? new I(v) : [], this.measurements[a] = {
|
|
295
304
|
momentaryLoudness: Number.NEGATIVE_INFINITY,
|
|
296
305
|
shortTermLoudness: Number.NEGATIVE_INFINITY,
|
|
297
306
|
integratedLoudness: Number.NEGATIVE_INFINITY,
|
|
@@ -302,67 +311,67 @@ class j extends AudioWorkletProcessor {
|
|
|
302
311
|
};
|
|
303
312
|
}
|
|
304
313
|
}
|
|
305
|
-
process(e, i,
|
|
314
|
+
process(e, i, u) {
|
|
306
315
|
for (let s = 0; s < e.length; s++) {
|
|
307
316
|
if (!e[s].length)
|
|
308
317
|
continue;
|
|
309
|
-
const n = e[s].length,
|
|
318
|
+
const n = e[s].length, a = e[s][0].length, F = sampleRate >= 96e3 ? 2 : 4, E = B[n], L = this.mEnergyBuffers[s].capacity, v = this.sEnergyBuffers[s].capacity;
|
|
310
319
|
if (!this.kWeightingFilters[s] || this.kWeightingFilters[s].length !== n) {
|
|
311
|
-
const { highshelf: o, highpass: t } =
|
|
320
|
+
const { highshelf: o, highpass: t } = C;
|
|
312
321
|
this.kWeightingFilters[s] = this.kWeightingFilters[s] || [];
|
|
313
|
-
for (let
|
|
314
|
-
this.kWeightingFilters[s][
|
|
315
|
-
new
|
|
316
|
-
new
|
|
322
|
+
for (let h = 0; h < n; h++)
|
|
323
|
+
this.kWeightingFilters[s][h] = [
|
|
324
|
+
new V(o.a, o.b),
|
|
325
|
+
new V(t.a, t.b)
|
|
317
326
|
];
|
|
318
327
|
}
|
|
319
328
|
if (!this.overSamplingFilters[s] || this.overSamplingFilters[s].length !== n) {
|
|
320
|
-
const { lowpass: o } =
|
|
329
|
+
const { lowpass: o } = x, { phase0: t, phase1: h, phase2: f, phase3: m } = o;
|
|
321
330
|
this.overSamplingFilters[s] = this.overSamplingFilters[s] || [];
|
|
322
331
|
for (let l = 0; l < n; l++)
|
|
323
332
|
this.overSamplingFilters[s][l] = [
|
|
324
333
|
new N(t),
|
|
325
|
-
new N(
|
|
334
|
+
new N(h),
|
|
326
335
|
new N(f),
|
|
327
|
-
new N(
|
|
336
|
+
new N(m)
|
|
328
337
|
];
|
|
329
338
|
}
|
|
330
|
-
for (let o = 0; o <
|
|
339
|
+
for (let o = 0; o < a; o++) {
|
|
331
340
|
let t = 0;
|
|
332
|
-
for (let
|
|
333
|
-
const l = e[s][
|
|
334
|
-
t += S *
|
|
335
|
-
const
|
|
336
|
-
let
|
|
337
|
-
for (let
|
|
338
|
-
const
|
|
339
|
-
|
|
341
|
+
for (let m = 0; m < n; m++) {
|
|
342
|
+
const l = e[s][m][o], [c, r] = this.kWeightingFilters[s][m], y = c.process(l), T = r.process(y), S = T * T, g = E ? E[m] ?? 1 : 1;
|
|
343
|
+
t += S * g;
|
|
344
|
+
const p = l * this.attenuation;
|
|
345
|
+
let d = 0;
|
|
346
|
+
for (let A = 0; A < F; A++) {
|
|
347
|
+
const D = this.overSamplingFilters[s][m][A], w = Math.abs(D.process(p));
|
|
348
|
+
d < w && (d = w);
|
|
340
349
|
}
|
|
341
|
-
this.overSampledValues[s] !== void 0 ?
|
|
350
|
+
this.overSampledValues[s] !== void 0 ? d > this.overSampledValues[s] && (this.overSampledValues[s] = d, this.overSampledValueDirtyFlags[s] = !0) : (this.overSampledValues[s] = d, this.overSampledValueDirtyFlags[s] = !0);
|
|
342
351
|
}
|
|
343
|
-
const
|
|
344
|
-
this.mEnergySums[s] += t -
|
|
352
|
+
const h = this.mEnergyBuffers[s].evict(t) ?? 0;
|
|
353
|
+
this.mEnergySums[s] += t - h;
|
|
345
354
|
const f = this.sEnergyBuffers[s].evict(t) ?? 0;
|
|
346
355
|
if (this.sEnergySums[s] += t - f, this.mEnergyBuffers[s].isFull()) {
|
|
347
|
-
const
|
|
356
|
+
const m = this.mEnergySums[s] / L, l = this.energyToLoudness(m), c = this.measurements[s].maximumMomentaryLoudness, r = Math.max(l, c);
|
|
348
357
|
this.measurements[s].momentaryLoudness = l, this.measurements[s].maximumMomentaryLoudness = r;
|
|
349
358
|
}
|
|
350
359
|
}
|
|
351
|
-
this.mSampleAccumulators[s] +=
|
|
352
|
-
const
|
|
353
|
-
for (; this.mSampleAccumulators[s] >=
|
|
360
|
+
this.mSampleAccumulators[s] += a, this.sSampleAccumulators[s] += a;
|
|
361
|
+
const _ = Math.round(sampleRate * O), R = Math.round(sampleRate * G);
|
|
362
|
+
for (; this.mSampleAccumulators[s] >= _; ) {
|
|
354
363
|
if (this.mEnergyBuffers[s].isFull()) {
|
|
355
364
|
const o = this.mEnergySums[s] / L, t = this.energyToLoudness(o);
|
|
356
365
|
this.mTraces[s].push(t), this.mTraceDirtyFlags[s] = !0;
|
|
357
366
|
}
|
|
358
|
-
this.mSampleAccumulators[s] -=
|
|
367
|
+
this.mSampleAccumulators[s] -= _;
|
|
359
368
|
}
|
|
360
|
-
for (; this.sSampleAccumulators[s] >=
|
|
369
|
+
for (; this.sSampleAccumulators[s] >= R; ) {
|
|
361
370
|
if (this.sEnergyBuffers[s].isFull()) {
|
|
362
|
-
const o = this.sEnergySums[s] /
|
|
371
|
+
const o = this.sEnergySums[s] / v, t = this.energyToLoudness(o), h = this.measurements[s].maximumShortTermLoudness, f = Math.max(t, h);
|
|
363
372
|
this.measurements[s].shortTermLoudness = t, this.measurements[s].maximumShortTermLoudness = f, this.sTraces[s].push(t), this.sTraceDirtyFlags[s] = !0;
|
|
364
373
|
}
|
|
365
|
-
this.sSampleAccumulators[s] -=
|
|
374
|
+
this.sSampleAccumulators[s] -= R;
|
|
366
375
|
}
|
|
367
376
|
if (this.mTraces[s].length > 2 && this.mTraceDirtyFlags[s]) {
|
|
368
377
|
const o = [];
|
|
@@ -372,19 +381,19 @@ class j extends AudioWorkletProcessor {
|
|
|
372
381
|
const t = [];
|
|
373
382
|
for (const r of o)
|
|
374
383
|
t.push(this.loudnessToEnergy(r));
|
|
375
|
-
let
|
|
384
|
+
let h = 0;
|
|
376
385
|
for (const r of t)
|
|
377
|
-
|
|
378
|
-
const f =
|
|
386
|
+
h += r;
|
|
387
|
+
const f = h / t.length, l = this.energyToLoudness(f) + Y, c = [];
|
|
379
388
|
for (const r of o)
|
|
380
389
|
r > l && c.push(r);
|
|
381
390
|
if (c.length > 2) {
|
|
382
391
|
const r = [];
|
|
383
|
-
for (const
|
|
384
|
-
r.push(this.loudnessToEnergy(
|
|
392
|
+
for (const g of c)
|
|
393
|
+
r.push(this.loudnessToEnergy(g));
|
|
385
394
|
let y = 0;
|
|
386
|
-
for (const
|
|
387
|
-
y +=
|
|
395
|
+
for (const g of r)
|
|
396
|
+
y += g;
|
|
388
397
|
const T = y / r.length, S = this.energyToLoudness(T);
|
|
389
398
|
this.measurements[s].integratedLoudness = S;
|
|
390
399
|
}
|
|
@@ -399,23 +408,23 @@ class j extends AudioWorkletProcessor {
|
|
|
399
408
|
const t = [];
|
|
400
409
|
for (const r of o)
|
|
401
410
|
t.push(this.loudnessToEnergy(r));
|
|
402
|
-
let
|
|
411
|
+
let h = 0;
|
|
403
412
|
for (const r of t)
|
|
404
|
-
|
|
405
|
-
const f =
|
|
413
|
+
h += r;
|
|
414
|
+
const f = h / t.length, l = this.energyToLoudness(f) + j, c = [];
|
|
406
415
|
for (const r of o)
|
|
407
416
|
r > l && c.push(r);
|
|
408
417
|
if (c.length > 2) {
|
|
409
|
-
const r = c.sort((
|
|
418
|
+
const r = c.sort((g, p) => g - p), [y, T] = [
|
|
410
419
|
W,
|
|
411
420
|
k
|
|
412
|
-
].map((
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
),
|
|
416
|
-
|
|
421
|
+
].map((g) => {
|
|
422
|
+
const p = Math.floor(
|
|
423
|
+
g * (r.length - 1)
|
|
424
|
+
), d = Math.ceil(
|
|
425
|
+
g * (r.length - 1)
|
|
417
426
|
);
|
|
418
|
-
return
|
|
427
|
+
return d === p ? r[p] : r[p] + (r[d] - r[p]) * (g * (r.length - 1) - p);
|
|
419
428
|
}), S = T - y;
|
|
420
429
|
this.measurements[s].loudnessRange = S;
|
|
421
430
|
}
|
|
@@ -425,9 +434,9 @@ class j extends AudioWorkletProcessor {
|
|
|
425
434
|
}
|
|
426
435
|
if (currentTime - this.previousTime >= Number(this.interval)) {
|
|
427
436
|
for (let n = 0; n < this.measurements.length; n++) {
|
|
428
|
-
const
|
|
437
|
+
const a = this.overSampledValues[n];
|
|
429
438
|
if (this.overSampledValueDirtyFlags[n]) {
|
|
430
|
-
const E = 20 * Math.log10(
|
|
439
|
+
const E = 20 * Math.log10(a) + M;
|
|
431
440
|
this.measurements[n].maximumTruePeakLevel = E, this.overSampledValueDirtyFlags[n] = !1;
|
|
432
441
|
}
|
|
433
442
|
}
|
|
@@ -450,9 +459,9 @@ class j extends AudioWorkletProcessor {
|
|
|
450
459
|
return 10 ** ((e + 0.691) / 10);
|
|
451
460
|
}
|
|
452
461
|
}
|
|
453
|
-
registerProcessor("loudness-processor",
|
|
462
|
+
registerProcessor("loudness-processor", K);
|
|
454
463
|
`, t = "loudness-processor";
|
|
455
|
-
class
|
|
464
|
+
class o extends AudioWorkletNode {
|
|
456
465
|
constructor(n, s) {
|
|
457
466
|
super(n, t, s);
|
|
458
467
|
}
|
|
@@ -460,7 +469,7 @@ class a extends AudioWorkletNode {
|
|
|
460
469
|
return r(n);
|
|
461
470
|
}
|
|
462
471
|
}
|
|
463
|
-
async function
|
|
472
|
+
async function a(e, n) {
|
|
464
473
|
return await r(e), new AudioWorkletNode(e, t, n);
|
|
465
474
|
}
|
|
466
475
|
async function r(e) {
|
|
@@ -472,6 +481,6 @@ async function r(e) {
|
|
|
472
481
|
}
|
|
473
482
|
}
|
|
474
483
|
export {
|
|
475
|
-
|
|
476
|
-
|
|
484
|
+
o as LoudnessWorkletNode,
|
|
485
|
+
a as createLoudnessWorklet
|
|
477
486
|
};
|