annotate js/loudness.js @ 3141:335bc77627e0 tip

fixing discrete interface to allow labels to display
author Dave Moffat <me@davemoffat.com>
date Mon, 26 Jul 2021 12:15:24 +0100
parents 21d548eb40f1
children
rev   line source
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 }