prx-wavefile
Version:
Create, read and write wav files according to the specs.
375 lines (341 loc) • 10.4 kB
JavaScript
/*
* Copyright (c) 2020 Andrew Kuklewicz
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
*/
/**
* @fileoverview The MpegReader class.
* @see https://github.com/rochars/wavefile
*/
import { unpackString, unpack } from "./parsers/binary";
/**
* A class to perform low-level reading of mpeg audio files.
*/
export class MpegReader {
constructor(mpegBuffer) {
/**
* @type {number}
* @protected
*/
this.head = 0;
// Header values
/**
* @type {number}
*/
this.version = 0;
/**
* @type {number}
*/
this.layer = 0;
/**
* @type {boolean}
*/
this.errorProtection = false;
/**
* @type {number}
*/
this.bitRate = 0;
/**
* @type {number}
*/
this.sampleRate = 0;
/**
* @type {boolean}
*/
this.padding = false;
/**
* @type {boolean}
*/
this.privateBit = false;
/**
* @type {string}
*/
this.channelMode = "";
/**
* @type {number}
*/
this.modeExtension = 0;
/**
* @type {boolean}
*/
this.copyright = false;
/**
* @type {boolean}
*/
this.original = false;
/**
* @type {number}
*/
this.emphasis = 0;
// Calculated
/**
* @type {number}
*/
this.numChannels = 0;
/**
* @type {number}
*/
this.id3v2Offset = 0;
/**
* @type {number}
*/
this.samplesPerFrame = 0;
/**
* @type {number}
*/
this.frameSize = 0;
/**
* @type {number}
*/
this.sampleLength = 0;
/**
* @type {number}
*/
this.durationEstimate = 0.0;
/**
* @type {boolean}
*/
this.homogeneous = true;
/**
* @type {boolean}
*/
this.freeForm = false;
this.uInt32BE = { bits: 32, be: true };
// Constants & Lookups
/** MPEG Versions
00: MPEG Version 2.5 (unofficial)
01: (reserved)
10: MPEG Version 2 (ISO/IEC 13818-3)
11: MPEG Version 1 (ISO/IEC 11172-3)
*/
this.VERSIONS = [2.5, null, 2, 1];
/** MPEG Layers
00: (reserved)
01: Layer III (i.e. mp3 files)
10: Layer II (i.e. mp2 files)
11: Layer I
*/
this.LAYERS = [0, 3, 2, 1];
/**
* Sample rate table:
* the sample rate value is calculated based on the mpeg version
*/
this.CHANNEL_MODES = ["stereo", "joint-stereo", "dual-mono", "mono"];
/**
* Samples per frame table:
* the number of samples per frame is calculated based on the mpeg version and layer
*/
this.SAMPLES_PER_FRAME = {
1: { 0: 0, 1: 384, 2: 1152, 3: 1152 },
2: { 0: 0, 1: 384, 2: 1152, 3: 576 }
};
/**
* Bitrates table:
* the bitRate value is calculated based on the mpeg version and layer
*/
// prettier-ignore
this.BITRATES = {
1: {
1: [0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 0],
2: [0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 0],
3: [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0],
4: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
},
2: {
1: [0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, 0],
2: [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0],
3: [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0],
4: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
}
};
/**
* Sample rate table:
* the sample rate value is calculated based on the mpeg version
*/
this.SAMPLE_RATES = {
1: [44100, 48000, 32000, 0],
2: [22050, 24000, 16000, 0]
};
if (mpegBuffer) {
this.fromBuffer(mpegBuffer);
}
}
/**
* Set up the MpegReader object from an mpeg byte buffer.
* @param {!Uint8Array} mpegBuffer The buffer.
* @throws {Error} If format is not mpeg.
*/
fromBuffer(mpegBuffer) {
this.findFirstFrame_(mpegBuffer);
this.parseFrame_(mpegBuffer);
}
/**
* Find the first mpeg frame, skipping id3 and other garbage, looking for 11111111 111
* @param {!Uint8Array} buffer The buffer.
* @throws {Error} If format is not mpeg.
*/
findFirstFrame_(buffer) {
this.id3v2Offset = this.skipId3_(buffer);
this.head += this.id3v2Offset;
// b[0] should be 11111111 == 255 (0xff)
// b[1] should be 111????? > 128 + 64 + 32 > 224 (0xe0)
while (this.head + 4 < buffer.length) {
let b = [buffer[this.head], buffer[this.head + 1]];
if (b[0] == 0xff && (b[1] & 0xe0) == 0xe0) {
break;
} else {
this.head += b[1] == 0xff ? 1 : 2;
}
}
return this.head;
}
/**
* Skip any id3 or other data at the start of the file
* @param {!Uint8Array} buffer The buffer.
* @throws {Error} If format is not mpeg.
*/
skipId3_(buffer) {
let offset = 0;
// http://id3.org/d3v2.3.0
if (unpackString(buffer, 0, 3) == "ID3") {
// Decode bytes 6-9 as a 32-bit "synchsafe int" (refer to any ID3v2 spec).
let tagSizeBuffer = buffer.subarray(6, 10);
let tagSize = unsynchsafe(unpack(tagSizeBuffer, this.uInt32BE, 0));
offset = 10 + tagSize;
// If the 0x10 bit of byte 5 is set, let OFFSET = OFFSET + 10 (for the footer).
if (isBitSet(buffer[5], 2)) {
offset += 10;
}
}
return offset;
}
/**
* Parse the header and calculate derived values
* @param {!Uint8Array} buffer The buffer.
* @throws {Error} If format is not mpeg.
*/
parseFrame_(buffer) {
this.parseHeader_(buffer);
this.numChannels = this.channelMode == "mono" ? 1 : 2;
this.samplesPerFrame = this.SAMPLES_PER_FRAME[this.version][this.layer];
this.frameSize = this.frameSizeCalc_();
this.sampleLength = this.sampleLengthCalc_(buffer);
this.durationEstimate = this.durationEstimateCalc_(buffer);
}
durationEstimateCalc_(buffer) {
return (buffer.length - this.id3v2Offset) / ((this.bitRate * 1000) / 8);
}
sampleLengthCalc_(buffer) {
let fs = Math.floor((buffer.length - this.id3v2Offset) / this.frameSize);
return fs * this.samplesPerFrame;
}
frameSizeCalc_() {
return this.mpegFrameSizeCalc(
this.samplesPerFrame,
this.layer,
this.bitRate,
this.sampleRate,
this.padding
);
}
// version from ruby code
mpegFrameSizeCalc(samplesPerFrame, layer, bitRate, sampleRate, padding) {
var byteRate = (bitRate * 1000) / 8;
var pad = (padding ? 1 : 0) * (layer == 1 ? 4 : 1);
return ((samplesPerFrame * byteRate) / sampleRate + pad) | 0;
}
// http://mpgedit.org/mpgedit/mpeg_format/mpeghdr.htm
parseHeader_(buffer) {
let h = [];
for (let i = 0; i < 4; i++) {
h[i] = this.readUInt8(buffer);
}
// Validate the frane sync
if (h[0] !== 0xff || (h[1] & 0xe0) !== 0xe0) {
throw new Error(`Invalid frame header: [255, 224] != [${h[0]}, ${h[1]}]`);
}
// Byte 0: `AAAAAAAA`
// `AAAAAAAA` | 8 | (32-24) | Frame sync, part I (all bits set)
// Byte 1: `AAABBCCD`
// `AAA.....` | 3 | (23,21) | Frame sync part II (3 bits set)
// `...BB...` | 2 | (20,19) | MPEG Audio version ID (11 -> MPEG Version 1 (ISO/IEC 11172-3))
// `.....CC.` | 2 | (18,17) | Layer description
// `.......D` | 1 | (16) | Protection bit | 0 - Protected by CRC, 1 - Not protected
this.version = this.VERSIONS[(h[1] >> 3) & 0x03];
this.layer = this.LAYERS[(h[1] >> 1) & 0x03];
this.errorProtection = !isBitSet(h[1], 1);
// Byte 2: `EEEEFFGH`
// `EEEE....` | 4 | (15,12) | Bitrate index
// `....FF..` | 2 | (11,10) | Sampling rate frequency index (values are in Hz)
// `......G.` | 1 | (9) | Padding bit, 0 - frame is not padded, 1 - frame is padded with one extra slot
// `.......H` | 1 | (8) | Private bit. This is informative
this.bitRate = this.BITRATES[this.version][this.layer][(h[2] >> 4) & 0x0f];
this.sampleRate = this.SAMPLE_RATES[this.version][(h[2] >> 2) & 0x03];
this.padding = isBitSet(h[2], 2);
this.privateBit = isBitSet(h[2], 1);
// Byte 3: `IIJJKLMM`
// `II......` | 2 | (7,6) | Channel Mode
// `..JJ....` | 2 | (5,4) | Mode extension (Only if Joint stereo)
// `....K...` | 1 | (3) | Copyright
// `.....L..` | 1 | (2) | Original
// `......MM` | 2 | (1,0) | Emphasis
this.channelMode = this.CHANNEL_MODES[(h[3] >> 6) & 0x03];
this.modeExtension = (h[3] >> 6) & 0x03;
this.copyright = isBitSet(h[3], 4);
this.original = isBitSet(h[3], 3);
this.emphasis = h[3] & 0x03;
}
/**
* Read a number from a chunk.
* @param {!Uint8Array} bytes The chunk bytes.
* @return {number} The number.
* @protected
*/
readUInt8(bytes) {
let value = bytes[this.head];
this.head += 1;
return value;
}
}
/**
* Decodes a sync-safe integer
* @param {number} i sync-safe integer
* @return {number} un-sync-safe integer
*/
export function unsynchsafe(i) {
let mask = 0x7f000000;
let out = 0;
while (mask) {
out >>= 1;
out |= i & mask;
mask >>= 8;
}
return out;
}
/**
* Returns
* @param {number} n integer to look for a bit set
* @param {number} b position of the bit to check
* @return {boolean} True if the bit is set
*/
function isBitSet(n, b) {
return n & (1 << (b - 1)) ? true : false;
}