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@2672
|
83 num_frames = Math.max(num_frames, 1);
|
nicholas@2538
|
84
|
nicholas@2224
|
85 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@2538
|
127 for (var c = 0; c < num_channels; c++) {
|
nicholas@2224
|
128 LK[c] = [];
|
nicholas@2224
|
129 }
|
nicholas@2538
|
130
|
nicholas@2538
|
131 for (var n = 0; n < num_frames; n++) {
|
nicholas@2538
|
132 if (blocks[n] > threshold) {
|
nicholas@2538
|
133 for (var c = 0; c < num_channels; c++) {
|
nicholas@2224
|
134 LK[c].push(source[c][n]);
|
nicholas@2224
|
135 }
|
nicholas@2224
|
136 }
|
nicholas@2224
|
137 }
|
nicholas@2224
|
138 return LK;
|
nicholas@2224
|
139 }
|
nicholas@2224
|
140
|
nicholas@2538
|
141 function loudnessOfBlocks(blocks) {
|
nicholas@2224
|
142 var num_frames = blocks[0].length;
|
nicholas@2224
|
143 var num_channels = blocks.length;
|
nicholas@2224
|
144 var loudness = 0.0;
|
nicholas@2538
|
145 for (var n = 0; n < num_frames; n++) {
|
nicholas@2224
|
146 var sum = 0;
|
nicholas@2538
|
147 for (var c = 0; c < num_channels; c++) {
|
nicholas@2224
|
148 var G = 1.0;
|
nicholas@2538
|
149 if (G >= 3) {
|
nicholas@2538
|
150 G = 1.41;
|
nicholas@2538
|
151 }
|
nicholas@2538
|
152 sum += blocks[c][n] * G;
|
nicholas@2224
|
153 }
|
nicholas@2224
|
154 sum /= num_frames;
|
nicholas@2224
|
155 loudness += sum;
|
nicholas@2224
|
156 }
|
nicholas@2224
|
157 loudness = -0.691 + 10 * Math.log10(loudness);
|
nicholas@2224
|
158 return loudness;
|
nicholas@2538
|
159 }
|