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