nicholas@2224
|
1 /**
|
nicholas@2224
|
2 * loundess.js
|
nicholas@2224
|
3 * Loudness module for the Web Audio Evaluation Toolbox
|
nicholas@2224
|
4 * Allows for automatic calculation of loudness of Web Audio API Buffer objects,
|
nicholas@2224
|
5 * return gain values to correct for a target loudness or match loudness between
|
nicholas@2224
|
6 * multiple objects
|
nicholas@2224
|
7 */
|
nicholas@2224
|
8
|
nicholas@2224
|
9 var interval_cal_loudness_event = null;
|
nicholas@2224
|
10
|
nicholas@2538
|
11 if (typeof OfflineAudioContext == "undefined") {
|
nicholas@2538
|
12 var OfflineAudioContext = webkitOfflineAudioContext;
|
nicholas@2224
|
13 }
|
nicholas@2224
|
14
|
nicholas@2538
|
15 function calculateLoudness(buffer, timescale, target, offlineContext) {
|
nicholas@2538
|
16 // This function returns the EBU R 128 specification loudness model and sets the linear gain required to match -23 LUFS
|
nicholas@2538
|
17 // buffer -> Web Audio API Buffer object
|
nicholas@2538
|
18 // timescale -> M or Momentary (returns Array), S or Short (returns Array),
|
nicholas@2538
|
19 // I or Integrated (default, returns number)
|
nicholas@2538
|
20 // target -> default is -23 LUFS but can be any LUFS measurement.
|
nicholas@2538
|
21 if (navigator.platform == 'iPad' || navigator.platform == 'iPhone') {
|
n@2432
|
22 buffer.ready();
|
n@2432
|
23 }
|
nicholas@2538
|
24 if (buffer == undefined) {
|
nicholas@2538
|
25 return 0;
|
nicholas@2538
|
26 }
|
nicholas@2538
|
27 if (timescale == undefined) {
|
nicholas@2538
|
28 timescale = "I";
|
nicholas@2538
|
29 }
|
nicholas@2538
|
30 if (target == undefined) {
|
nicholas@2538
|
31 target = -23;
|
nicholas@2538
|
32 }
|
nicholas@2538
|
33 if (offlineContext == undefined) {
|
nicholas@2538
|
34 offlineContext = new OfflineAudioContext(audioContext.destination.channelCount, buffer.buffer.duration * audioContext.sampleRate, audioContext.sampleRate);
|
nicholas@2538
|
35 }
|
nicholas@2538
|
36 // Create the required filters
|
nicholas@2538
|
37 var KFilter = offlineContext.createBiquadFilter();
|
nicholas@2538
|
38 KFilter.type = "highshelf";
|
nicholas@2538
|
39 KFilter.gain.value = 4;
|
nicholas@2538
|
40 KFilter.frequency.value = 1500;
|
nicholas@2538
|
41
|
nicholas@2538
|
42 var HPFilter = offlineContext.createBiquadFilter();
|
nicholas@2538
|
43 HPFilter.type = "highpass";
|
nicholas@2538
|
44 HPFilter.Q.value = 0.5;
|
nicholas@2538
|
45 HPFilter.frequency.value = 38;
|
nicholas@2538
|
46 // copy Data into the process buffer
|
nicholas@2538
|
47 var processSource = offlineContext.createBufferSource();
|
nicholas@2538
|
48 processSource.buffer = buffer.buffer;
|
nicholas@2538
|
49
|
nicholas@2538
|
50 processSource.connect(KFilter);
|
nicholas@2538
|
51 KFilter.connect(HPFilter);
|
nicholas@2538
|
52 HPFilter.connect(offlineContext.destination);
|
nicholas@2538
|
53 offlineContext.oncomplete = function (renderedBuffer) {
|
nicholas@2538
|
54 // Have the renderedBuffer information, now continue processing
|
nicholas@2538
|
55 if (typeof renderedBuffer.renderedBuffer == 'object') {
|
nicholas@2538
|
56 renderedBuffer = renderedBuffer.renderedBuffer;
|
nicholas@2538
|
57 }
|
nicholas@2538
|
58 switch (timescale) {
|
nicholas@2538
|
59 case "I":
|
nicholas@2538
|
60 // Calculate the Mean Squared of a signal
|
nicholas@2538
|
61 var MS = calculateMeanSquared(renderedBuffer, 0.4, 0.75);
|
nicholas@2538
|
62 // Calculate the Loudness of each block
|
nicholas@2538
|
63 var MSL = calculateLoudnessFromBlocks(MS);
|
nicholas@2538
|
64 // Get blocks from Absolute Gate
|
nicholas@2538
|
65 var LK = loudnessGate(MSL, MS, -70);
|
nicholas@2538
|
66 // Calculate Loudness
|
nicholas@2538
|
67 var LK_gate = loudnessOfBlocks(LK);
|
nicholas@2538
|
68 // Get blocks from Relative Gate
|
nicholas@2538
|
69 var RK = loudnessGate(MSL, MS, LK_gate - 10);
|
nicholas@2538
|
70 var RK_gate = loudnessOfBlocks(RK);
|
nicholas@2538
|
71 buffer.buffer.lufs = RK_gate;
|
nicholas@2538
|
72 }
|
nicholas@2224
|
73 buffer.ready();
|
nicholas@2538
|
74 };
|
nicholas@2224
|
75 processSource.start(0);
|
nicholas@2538
|
76 offlineContext.startRendering();
|
nicholas@2224
|
77 }
|
nicholas@2224
|
78
|
nicholas@2538
|
79 function calculateMeanSquared(buffer, frame_dur, frame_overlap) {
|
nicholas@2538
|
80 frame_size = Math.floor(buffer.sampleRate * frame_dur);
|
nicholas@2538
|
81 step_size = Math.floor(frame_size * (1.0 - frame_overlap));
|
nicholas@2538
|
82 num_frames = Math.floor((buffer.length - frame_size) / step_size);
|
nicholas@2538
|
83
|
nicholas@2224
|
84 MS = Array(buffer.numberOfChannels);
|
nicholas@2538
|
85 for (var c = 0; c < buffer.numberOfChannels; c++) {
|
nicholas@2224
|
86 MS[c] = new Float32Array(num_frames);
|
nicholas@2224
|
87 var data = buffer.getChannelData(c);
|
nicholas@2538
|
88 for (var no = 0; no < num_frames; no++) {
|
nicholas@2224
|
89 MS[c][no] = 0.0;
|
nicholas@2538
|
90 for (var ptr = 0; ptr < frame_size; ptr++) {
|
nicholas@2538
|
91 var sample = data[no * step_size + ptr];
|
nicholas@2538
|
92 MS[c][no] += sample * sample;
|
nicholas@2224
|
93 }
|
nicholas@2224
|
94 MS[c][no] /= frame_size;
|
nicholas@2224
|
95 }
|
nicholas@2224
|
96 }
|
nicholas@2224
|
97 return MS;
|
nicholas@2224
|
98 }
|
nicholas@2224
|
99
|
nicholas@2538
|
100 function calculateLoudnessFromBlocks(blocks) {
|
nicholas@2224
|
101 var num_frames = blocks[0].length;
|
nicholas@2224
|
102 var num_channels = blocks.length;
|
nicholas@2224
|
103 var MSL = Array(num_frames);
|
nicholas@2538
|
104 for (var n = 0; n < num_frames; n++) {
|
nicholas@2224
|
105 var sum = 0;
|
nicholas@2538
|
106 for (var c = 0; c < num_channels; c++) {
|
nicholas@2224
|
107 var G = 1.0;
|
nicholas@2538
|
108 if (G >= 3) {
|
nicholas@2538
|
109 G = 1.41;
|
nicholas@2538
|
110 }
|
nicholas@2538
|
111 sum += blocks[c][n] * G;
|
nicholas@2224
|
112 }
|
nicholas@2538
|
113 MSL[n] = -0.691 + 10 * Math.log10(sum);
|
nicholas@2224
|
114 }
|
nicholas@2224
|
115 return MSL;
|
nicholas@2224
|
116 }
|
nicholas@2224
|
117
|
nicholas@2538
|
118 function loudnessGate(blocks, source, threshold) {
|
nicholas@2224
|
119 var num_frames = source[0].length;
|
nicholas@2224
|
120 var num_channels = source.length;
|
nicholas@2224
|
121 var LK = Array(num_channels);
|
nicholas@2538
|
122 for (var c = 0; c < num_channels; c++) {
|
nicholas@2224
|
123 LK[c] = [];
|
nicholas@2224
|
124 }
|
nicholas@2538
|
125
|
nicholas@2538
|
126 for (var n = 0; n < num_frames; n++) {
|
nicholas@2538
|
127 if (blocks[n] > threshold) {
|
nicholas@2538
|
128 for (var c = 0; c < num_channels; c++) {
|
nicholas@2224
|
129 LK[c].push(source[c][n]);
|
nicholas@2224
|
130 }
|
nicholas@2224
|
131 }
|
nicholas@2224
|
132 }
|
nicholas@2224
|
133 return LK;
|
nicholas@2224
|
134 }
|
nicholas@2224
|
135
|
nicholas@2538
|
136 function loudnessOfBlocks(blocks) {
|
nicholas@2224
|
137 var num_frames = blocks[0].length;
|
nicholas@2224
|
138 var num_channels = blocks.length;
|
nicholas@2224
|
139 var loudness = 0.0;
|
nicholas@2538
|
140 for (var n = 0; n < num_frames; n++) {
|
nicholas@2224
|
141 var sum = 0;
|
nicholas@2538
|
142 for (var c = 0; c < num_channels; c++) {
|
nicholas@2224
|
143 var G = 1.0;
|
nicholas@2538
|
144 if (G >= 3) {
|
nicholas@2538
|
145 G = 1.41;
|
nicholas@2538
|
146 }
|
nicholas@2538
|
147 sum += blocks[c][n] * G;
|
nicholas@2224
|
148 }
|
nicholas@2224
|
149 sum /= num_frames;
|
nicholas@2224
|
150 loudness += sum;
|
nicholas@2224
|
151 }
|
nicholas@2224
|
152 loudness = -0.691 + 10 * Math.log10(loudness);
|
nicholas@2224
|
153 return loudness;
|
nicholas@2538
|
154 }
|