Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 | 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 193x 193x 193x 193x 193x 193x 9x 9x 4986x 4986x 4985x 4985x 4985x 4986x 92x 92x 92x 92x 4985x 4985x 4985x 4986x 27142692x 27142692x 27142692x 27142692x 27142692x 27142692x 4985x 4985x 4985x 4986x 4986x 9x 9x 92x 92x 92x 92x 92x 9x 9x 308x 308x 9x 9x 59x 59x 59x 9x 292x 292x 292x 9x 9x 9x 9x 9x 9x 9x 174x 174x 60x 174x 174x 174x 174x 174x 174x 167x 167x 167x 174x 154x 154x 138x 154x 174x 174x 174x 9x 171x 171x 171x 171x 171x 171x 171x 171x 171x 171x 171x 171x 171x 171x 171x 171x 9x 9x 9x 99x 99x 99x 99x 95x 99x 99x 99x 99x 99x 99x 99x 99x 99x 99x 99x 99x 99x 91x 91x 91x 94x 99x 9x 9x 9x 9x 9x 9x 72x 72x 72x 72x 72x 72x 72x 72x 72x 72x 72x 72x 72x 9x 9x 9x 9x 9x 9x 9x 26x 26x 26x 26x 26x 3x 3x 3x 3x 22x 26x 9x 9x 9x 9x 9x 9x 143x 143x 5x 5x 4x 4x 4x 8x 4x 4x 4x 4x 4x 5x 1x 138x 143x | // Local audio precheck (DESIGN 2.9). Thresholds of 90103 equal the platform's silence guard, so a file
// rejected here is one the platform would have scored 0.
import { LOCAL_TABLE } from './error-table.js';
import { createError, localError } from './errors.js';
import { parseWav, detectFormat } from './audio.js';
import { toUint8 } from './util.js';
import { MAX_UPLOAD_BYTES, MAX_STREAM_BYTES, MAX_AUDIO_SECONDS, MIN_AUDIO_SECONDS } from './constants.js';
export const AudioPrecheckMode = Object.freeze({ OFF: 'OFF', WARN: 'WARN', REJECT: 'REJECT' });
const SILENT_PEAK = 200;
const SILENT_RMS = 30;
const LOW_VOLUME_DBFS = -45;
const REJECT_CODES = [90101, 90102, 90103, 90105];
export function isPrecheckMode(v) {
return v === 'OFF' || v === 'WARN' || v === 'REJECT';
}
/** Running peak and RMS of little-endian signed 16 bit samples, fed chunk by chunk. */
export class PcmStats {
constructor() {
this.samples = 0;
this.peak = 0;
this.sumSquares = 0;
this.bytes = 0;
this._carry = -1;
}
add(chunk) {
const u8 = toUint8(chunk);
if (!u8 || !u8.length) return;
const n = u8.length;
this.bytes += n;
let i = 0;
if (this._carry >= 0) {
this._acc((((u8[0] << 8) | this._carry) << 16) >> 16);
this._carry = -1;
i = 1;
}
let peak = this.peak;
let sum = 0;
let count = 0;
for (; i + 1 < n; i += 2) {
const s = (((u8[i + 1] << 8) | u8[i]) << 16) >> 16;
const a = s < 0 ? -s : s;
if (a > peak) peak = a;
sum += s * s;
count++;
}
this.peak = peak;
this.sumSquares += sum;
this.samples += count;
if (i < n) this._carry = u8[i];
}
_acc(s) {
const a = s < 0 ? -s : s;
if (a > this.peak) this.peak = a;
this.sumSquares += s * s;
this.samples++;
}
rms() {
return this.samples ? Math.sqrt(this.sumSquares / this.samples) : 0;
}
}
function warning(code) {
return { code, message: LOCAL_TABLE[code].message };
}
function dbfs(rms) {
return rms > 0 ? 20 * Math.log10(rms / 32768) : -Infinity;
}
/**
* Evaluates the precheck rules on facts about the audio.
* facts: {sizeBytes, maxBytes, durationSec, pcm16: boolean, stats: PcmStats|null, formatOk: boolean, sampleRate}
* maxBytes: 50 MB for an upload (the default), 10 MB for one streaming round.
*/
export function precheckFacts(facts) {
const codes = [];
const add = (c) => {
if (codes.indexOf(c) < 0) codes.push(c);
};
const maxBytes = typeof facts.maxBytes === 'number' ? facts.maxBytes : MAX_UPLOAD_BYTES;
if (facts.sizeBytes > maxBytes) add(90102);
if (facts.formatOk === false) add(90105);
if (typeof facts.sampleRate === 'number' && facts.sampleRate > 0 && facts.sampleRate < 16000) add(90105);
if (typeof facts.durationSec === 'number') {
if (facts.durationSec < MIN_AUDIO_SECONDS) add(90101);
if (facts.durationSec > MAX_AUDIO_SECONDS) add(90102);
}
if (facts.pcm16 && facts.stats && facts.stats.samples > 0) {
const rms = facts.stats.rms();
if (facts.stats.peak < SILENT_PEAK && rms < SILENT_RMS) add(90103);
else if (dbfs(rms) < LOW_VOLUME_DBFS) add(90104);
}
codes.sort((a, b) => a - b);
return codes.map(warning);
}
function report(format, facts, extra) {
const stats = facts.stats;
const rms = stats && stats.samples ? stats.rms() : null;
return {
format,
sizeBytes: facts.sizeBytes,
durationMs: typeof facts.durationSec === 'number' ? Math.round(facts.durationSec * 1000) : null,
sampleRate: extra.sampleRate || null,
channels: extra.channels || null,
bitsPerSample: extra.bitsPerSample || null,
peak: stats && stats.samples ? stats.peak : null,
rms,
rmsDbfs: rms === null ? null : dbfs(rms),
warnings: precheckFacts(facts),
};
}
/** Precheck of a WAV file or other container held in memory; maxBytes defaults to the 50 MB upload limit. */
export function analyzeContainer(bytes, format, maxBytes) {
const b = toUint8(bytes);
const limit = typeof maxBytes === 'number' ? maxBytes : MAX_UPLOAD_BYTES;
const fmt = format || detectFormat(b) || 'unknown';
if (fmt !== 'wav') return report(fmt, { sizeBytes: b.length, maxBytes: limit }, {});
const wav = parseWav(b);
if (!wav || !wav.ok) return report('wav', { sizeBytes: b.length, maxBytes: limit, formatOk: false }, {});
const pcm16 = wav.audioFormat === 1 && wav.bitsPerSample === 16;
const bytesPerSecond = wav.sampleRate * wav.channels * (wav.bitsPerSample / 8);
const facts = {
sizeBytes: b.length,
maxBytes: limit,
durationSec: bytesPerSecond > 0 ? wav.dataLength / bytesPerSecond : 0,
formatOk: pcm16,
sampleRate: wav.sampleRate,
pcm16,
stats: null,
};
if (pcm16) {
facts.stats = new PcmStats();
facts.stats.add(b.subarray(wav.dataOffset, wav.dataOffset + wav.dataLength));
}
return report('wav', facts, wav);
}
/**
* Precheck of raw 16 bit PCM described by running stats: one streaming round at end() (limit 10 MB),
* or PCM that will be uploaded when maxBytes says so.
*/
export function analyzePcmStats(stats, sampleRate, channels, maxBytes) {
const sr = sampleRate || 16000;
const ch = channels || 1;
const facts = {
sizeBytes: stats.bytes,
maxBytes: typeof maxBytes === 'number' ? maxBytes : MAX_STREAM_BYTES,
durationSec: stats.bytes / (sr * ch * 2),
formatOk: true,
sampleRate: sr,
pcm16: true,
stats,
};
return report('pcm', facts, { sampleRate: sr, channels: ch, bitsPerSample: 16 });
}
/**
* Public precheck helper. input: WAV bytes, or raw PCM with options.format "pcm". The size limit is
* the upload limit of 50 MB, or 10 MB with options.stream for the audio of one streaming round.
* Returns {format, sizeBytes, durationMs, sampleRate, channels, bitsPerSample, peak, rms, rmsDbfs, warnings}.
*/
export function precheckAudio(input, options) {
const o = options || {};
const b = toUint8(input);
if (!b) throw localError(90010, 'precheckAudio expects an ArrayBuffer or a typed array');
const maxBytes = o.stream === true ? MAX_STREAM_BYTES : MAX_UPLOAD_BYTES;
if (String(o.format || '').toLowerCase() === 'pcm' && detectFormat(b) !== 'wav') {
const stats = new PcmStats();
stats.add(b);
return analyzePcmStats(stats, o.sampleRate, o.numberOfChannels, maxBytes);
}
return analyzeContainer(b, null, maxBytes);
}
/**
* Applies a precheck mode to warnings. OFF: none. WARN: all are returned as warnings. REJECT: throws
* AudioQualityException for 90101, 90102, 90103 and 90105, 90104 stays a warning.
*/
export function applyPrecheckMode(warnings, mode, init) {
if (mode === 'OFF') return [];
if (mode === 'REJECT') {
for (let i = 0; i < warnings.length; i++) {
if (REJECT_CODES.indexOf(warnings[i].code) >= 0) {
const o = {};
const src = init || {};
Object.keys(src).forEach((k) => {
o[k] = src[k];
});
o.code = warnings[i].code;
o.warnings = warnings.slice();
throw createError('AUDIO', 'audio precheck rejected the audio: ' + warnings[i].message, o);
}
}
}
return warnings.slice();
}
|