nicholas@2224: /** nicholas@2224: * loundess.js nicholas@2224: * Loudness module for the Web Audio Evaluation Toolbox nicholas@2224: * Allows for automatic calculation of loudness of Web Audio API Buffer objects, nicholas@2224: * return gain values to correct for a target loudness or match loudness between nicholas@2224: * multiple objects nicholas@2224: */ nicholas@2690: /* globals webkitOfflineAudioContext, navigator, audioContext, Float32Array */ nicholas@2224: var interval_cal_loudness_event = null; nicholas@2224: nicholas@2538: if (typeof OfflineAudioContext == "undefined") { nicholas@2538: var OfflineAudioContext = webkitOfflineAudioContext; nicholas@2224: } nicholas@2224: nicholas@2538: function calculateLoudness(buffer, timescale, target, offlineContext) { nicholas@2538: // This function returns the EBU R 128 specification loudness model and sets the linear gain required to match -23 LUFS nicholas@2538: // buffer -> Web Audio API Buffer object nicholas@2538: // timescale -> M or Momentary (returns Array), S or Short (returns Array), nicholas@2538: // I or Integrated (default, returns number) nicholas@2538: // target -> default is -23 LUFS but can be any LUFS measurement. nicholas@2538: if (navigator.platform == 'iPad' || navigator.platform == 'iPhone') { n@2432: buffer.ready(); n@2432: } nicholas@2690: if (buffer === undefined) { nicholas@2538: return 0; nicholas@2538: } nicholas@2690: if (timescale === undefined) { nicholas@2538: timescale = "I"; nicholas@2538: } nicholas@2690: if (target === undefined) { nicholas@2538: target = -23; nicholas@2538: } nicholas@2690: if (offlineContext === undefined) { n@2925: offlineContext = new OfflineAudioContext(audioContext.destination.channelCount, Math.max(0.4, buffer.buffer.duration) * audioContext.sampleRate, audioContext.sampleRate); nicholas@2538: } nicholas@2538: // Create the required filters nicholas@2538: var KFilter = offlineContext.createBiquadFilter(); nicholas@2538: KFilter.type = "highshelf"; nicholas@2538: KFilter.gain.value = 4; nicholas@2538: KFilter.frequency.value = 1500; nicholas@2538: nicholas@2538: var HPFilter = offlineContext.createBiquadFilter(); nicholas@2538: HPFilter.type = "highpass"; nicholas@2538: HPFilter.Q.value = 0.5; nicholas@2538: HPFilter.frequency.value = 38; nicholas@2538: // copy Data into the process buffer nicholas@2538: var processSource = offlineContext.createBufferSource(); nicholas@2538: processSource.buffer = buffer.buffer; nicholas@2538: nicholas@2538: processSource.connect(KFilter); nicholas@2538: KFilter.connect(HPFilter); nicholas@2538: HPFilter.connect(offlineContext.destination); nicholas@2538: offlineContext.oncomplete = function (renderedBuffer) { nicholas@2538: // Have the renderedBuffer information, now continue processing nicholas@2538: if (typeof renderedBuffer.renderedBuffer == 'object') { nicholas@2538: renderedBuffer = renderedBuffer.renderedBuffer; nicholas@2538: } nicholas@2538: switch (timescale) { nicholas@2538: case "I": nicholas@2538: // Calculate the Mean Squared of a signal nicholas@2538: var MS = calculateMeanSquared(renderedBuffer, 0.4, 0.75); nicholas@2538: // Calculate the Loudness of each block nicholas@2538: var MSL = calculateLoudnessFromBlocks(MS); nicholas@2538: // Get blocks from Absolute Gate nicholas@2538: var LK = loudnessGate(MSL, MS, -70); nicholas@2538: // Calculate Loudness nicholas@2538: var LK_gate = loudnessOfBlocks(LK); nicholas@2538: // Get blocks from Relative Gate nicholas@2538: var RK = loudnessGate(MSL, MS, LK_gate - 10); nicholas@2538: var RK_gate = loudnessOfBlocks(RK); nicholas@2538: buffer.buffer.lufs = RK_gate; nicholas@2538: } nicholas@2224: buffer.ready(); nicholas@2538: }; nicholas@2224: processSource.start(0); nicholas@2538: offlineContext.startRendering(); nicholas@2224: } nicholas@2224: nicholas@2538: function calculateMeanSquared(buffer, frame_dur, frame_overlap) { nicholas@2690: var frame_size = Math.floor(buffer.sampleRate * frame_dur); nicholas@2690: var step_size = Math.floor(frame_size * (1.0 - frame_overlap)); nicholas@2690: var num_frames = Math.floor((buffer.length - frame_size) / step_size); n@2925: num_frames = Math.max(num_frames, 1); nicholas@2538: nicholas@2690: var MS = Array(buffer.numberOfChannels); nicholas@2538: for (var c = 0; c < buffer.numberOfChannels; c++) { nicholas@2224: MS[c] = new Float32Array(num_frames); nicholas@2224: var data = buffer.getChannelData(c); nicholas@2538: for (var no = 0; no < num_frames; no++) { nicholas@2224: MS[c][no] = 0.0; nicholas@2538: for (var ptr = 0; ptr < frame_size; ptr++) { nicholas@2672: var i = no * step_size + ptr; nicholas@2672: if (i >= buffer.length) { nicholas@2672: break; nicholas@2672: } nicholas@2672: var sample = data[i]; nicholas@2538: MS[c][no] += sample * sample; nicholas@2224: } nicholas@2224: MS[c][no] /= frame_size; nicholas@2224: } nicholas@2224: } nicholas@2224: return MS; nicholas@2224: } nicholas@2224: nicholas@2538: function calculateLoudnessFromBlocks(blocks) { nicholas@2224: var num_frames = blocks[0].length; nicholas@2224: var num_channels = blocks.length; nicholas@2224: var MSL = Array(num_frames); nicholas@2538: for (var n = 0; n < num_frames; n++) { nicholas@2224: var sum = 0; nicholas@2538: for (var c = 0; c < num_channels; c++) { nicholas@2224: var G = 1.0; nicholas@2538: if (G >= 3) { nicholas@2538: G = 1.41; nicholas@2538: } nicholas@2538: sum += blocks[c][n] * G; nicholas@2224: } nicholas@2538: MSL[n] = -0.691 + 10 * Math.log10(sum); nicholas@2224: } nicholas@2224: return MSL; nicholas@2224: } nicholas@2224: nicholas@2538: function loudnessGate(blocks, source, threshold) { nicholas@2224: var num_frames = source[0].length; nicholas@2224: var num_channels = source.length; nicholas@2224: var LK = Array(num_channels); nicholas@2690: var n, c; nicholas@2690: for (c = 0; c < num_channels; c++) { nicholas@2224: LK[c] = []; nicholas@2224: } nicholas@2538: nicholas@2690: for (n = 0; n < num_frames; n++) { nicholas@2538: if (blocks[n] > threshold) { nicholas@2690: for (c = 0; c < num_channels; c++) { nicholas@2224: LK[c].push(source[c][n]); nicholas@2224: } nicholas@2224: } nicholas@2224: } nicholas@2224: return LK; nicholas@2224: } nicholas@2224: nicholas@2538: function loudnessOfBlocks(blocks) { nicholas@2224: var num_frames = blocks[0].length; nicholas@2224: var num_channels = blocks.length; nicholas@2224: var loudness = 0.0; nicholas@2538: for (var n = 0; n < num_frames; n++) { nicholas@2224: var sum = 0; nicholas@2538: for (var c = 0; c < num_channels; c++) { nicholas@2224: var G = 1.0; nicholas@2538: if (G >= 3) { nicholas@2538: G = 1.41; nicholas@2538: } nicholas@2538: sum += blocks[c][n] * G; nicholas@2224: } nicholas@2224: sum /= num_frames; nicholas@2224: loudness += sum; nicholas@2224: } nicholas@2224: loudness = -0.691 + 10 * Math.log10(loudness); nicholas@2224: return loudness; nicholas@2538: }