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@2690
|
8 /* globals webkitOfflineAudioContext, navigator, audioContext, Float32Array */
|
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@2690
|
24 if (buffer === undefined) {
|
nicholas@2538
|
25 return 0;
|
nicholas@2538
|
26 }
|
nicholas@2690
|
27 if (timescale === undefined) {
|
nicholas@2538
|
28 timescale = "I";
|
nicholas@2538
|
29 }
|
nicholas@2690
|
30 if (target === undefined) {
|
nicholas@2538
|
31 target = -23;
|
nicholas@2538
|
32 }
|
nicholas@2690
|
33 if (offlineContext === undefined) {
|
n@2925
|
34 offlineContext = new OfflineAudioContext(audioContext.destination.channelCount, Math.max(0.4, 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@2690
|
80 var frame_size = Math.floor(buffer.sampleRate * frame_dur);
|
nicholas@2690
|
81 var step_size = Math.floor(frame_size * (1.0 - frame_overlap));
|
nicholas@2690
|
82 var num_frames = Math.floor((buffer.length - frame_size) / step_size);
|
n@2925
|
83 num_frames = Math.max(num_frames, 1);
|
nicholas@2538
|
84
|
nicholas@2690
|
85 var MS = Array(buffer.numberOfChannels);
|
nicholas@2538
|
86 for (var c = 0; c < buffer.numberOfChannels; c++) {
|
nicholas@2224
|
87 MS[c] = new Float32Array(num_frames);
|
nicholas@2224
|
88 var data = buffer.getChannelData(c);
|
nicholas@2538
|
89 for (var no = 0; no < num_frames; no++) {
|
nicholas@2224
|
90 MS[c][no] = 0.0;
|
nicholas@2538
|
91 for (var ptr = 0; ptr < frame_size; ptr++) {
|
nicholas@2672
|
92 var i = no * step_size + ptr;
|
nicholas@2672
|
93 if (i >= buffer.length) {
|
nicholas@2672
|
94 break;
|
nicholas@2672
|
95 }
|
nicholas@2672
|
96 var sample = data[i];
|
nicholas@2538
|
97 MS[c][no] += sample * sample;
|
nicholas@2224
|
98 }
|
nicholas@2224
|
99 MS[c][no] /= frame_size;
|
nicholas@2224
|
100 }
|
nicholas@2224
|
101 }
|
nicholas@2224
|
102 return MS;
|
nicholas@2224
|
103 }
|
nicholas@2224
|
104
|
nicholas@2538
|
105 function calculateLoudnessFromBlocks(blocks) {
|
nicholas@2224
|
106 var num_frames = blocks[0].length;
|
nicholas@2224
|
107 var num_channels = blocks.length;
|
nicholas@2224
|
108 var MSL = Array(num_frames);
|
nicholas@2538
|
109 for (var n = 0; n < num_frames; n++) {
|
nicholas@2224
|
110 var sum = 0;
|
nicholas@2538
|
111 for (var c = 0; c < num_channels; c++) {
|
nicholas@2224
|
112 var G = 1.0;
|
nicholas@2538
|
113 if (G >= 3) {
|
nicholas@2538
|
114 G = 1.41;
|
nicholas@2538
|
115 }
|
nicholas@2538
|
116 sum += blocks[c][n] * G;
|
nicholas@2224
|
117 }
|
nicholas@2538
|
118 MSL[n] = -0.691 + 10 * Math.log10(sum);
|
nicholas@2224
|
119 }
|
nicholas@2224
|
120 return MSL;
|
nicholas@2224
|
121 }
|
nicholas@2224
|
122
|
nicholas@2538
|
123 function loudnessGate(blocks, source, threshold) {
|
nicholas@2224
|
124 var num_frames = source[0].length;
|
nicholas@2224
|
125 var num_channels = source.length;
|
nicholas@2224
|
126 var LK = Array(num_channels);
|
nicholas@2690
|
127 var n, c;
|
nicholas@2690
|
128 for (c = 0; c < num_channels; c++) {
|
nicholas@2224
|
129 LK[c] = [];
|
nicholas@2224
|
130 }
|
nicholas@2538
|
131
|
nicholas@2690
|
132 for (n = 0; n < num_frames; n++) {
|
nicholas@2538
|
133 if (blocks[n] > threshold) {
|
nicholas@2690
|
134 for (c = 0; c < num_channels; c++) {
|
nicholas@2224
|
135 LK[c].push(source[c][n]);
|
nicholas@2224
|
136 }
|
nicholas@2224
|
137 }
|
nicholas@2224
|
138 }
|
nicholas@2224
|
139 return LK;
|
nicholas@2224
|
140 }
|
nicholas@2224
|
141
|
nicholas@2538
|
142 function loudnessOfBlocks(blocks) {
|
nicholas@2224
|
143 var num_frames = blocks[0].length;
|
nicholas@2224
|
144 var num_channels = blocks.length;
|
nicholas@2224
|
145 var loudness = 0.0;
|
nicholas@2538
|
146 for (var n = 0; n < num_frames; n++) {
|
nicholas@2224
|
147 var sum = 0;
|
nicholas@2538
|
148 for (var c = 0; c < num_channels; c++) {
|
nicholas@2224
|
149 var G = 1.0;
|
nicholas@2538
|
150 if (G >= 3) {
|
nicholas@2538
|
151 G = 1.41;
|
nicholas@2538
|
152 }
|
nicholas@2538
|
153 sum += blocks[c][n] * G;
|
nicholas@2224
|
154 }
|
nicholas@2224
|
155 sum /= num_frames;
|
nicholas@2224
|
156 loudness += sum;
|
nicholas@2224
|
157 }
|
nicholas@2224
|
158 loudness = -0.691 + 10 * Math.log10(loudness);
|
nicholas@2224
|
159 return loudness;
|
nicholas@2538
|
160 }
|