UNPKG

@soundcut/decode-audio-data-fast

Version:

Decode audio data asynchronously, in the browser using Base​Audio​Context​.decode​Audio​Data(), as fast as possible.

1,179 lines (1,017 loc) 63.9 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.DADF = {})); }(this, (function (exports) { 'use strict'; var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; function createCommonjsModule(fn, basedir, module) { return module = { path: basedir, exports: {}, require: function (path, base) { return commonjsRequire(path, (base === undefined || base === null) ? module.path : base); } }, fn(module, module.exports), module.exports; } function commonjsRequire () { throw new Error('Dynamic requires are not currently supported by @rollup/plugin-commonjs'); } var lib = createCommonjsModule(function (module, exports) { // mp3-parser/lib v0.3.0 // https://github.com/biril/mp3-parser // Licensed and freely distributed under the MIT License // Copyright (c) 2013-2016 Alex Lambiris // ---- /* jshint browser:true */ /* global exports:false, define:false */ (function (globalObject, createModule) { // Global `exports` object signifies CommonJS enviroments with `module.exports`, e.g. Node { return createModule(exports); } }(commonjsGlobal, function (lib) { // Produce octet's binary representation as a string var octetToBinRep = (function () { var b = []; // The binary representation return function (octet) { b[0] = ((octet & 128) === 128 ? "1" : "0"); b[1] = ((octet & 64) === 64 ? "1" : "0"); b[2] = ((octet & 32) === 32 ? "1" : "0"); b[3] = ((octet & 16) === 16 ? "1" : "0"); b[4] = ((octet & 8) === 8 ? "1" : "0"); b[5] = ((octet & 4) === 4 ? "1" : "0"); b[6] = ((octet & 2) === 2 ? "1" : "0"); b[7] = ((octet & 1) === 1 ? "1" : "0"); return b.join(""); }; }()); // Get the number of bytes in a frame given its `bitrate`, `samplingRate` and `padding`. // Based on [magic formula](http://mpgedit.org/mpgedit/mpeg_format/mpeghdr.htm) lib.getFrameByteLength = function (kbitrate, samplingRate, padding, mpegVersion, layerVersion) { var sampleLength = lib.sampleLengthMap[mpegVersion][layerVersion]; var paddingSize = padding ? (layerVersion === "11" ? 4 : 1) : 0; var byteRate = kbitrate * 1000 / 8; return Math.floor((sampleLength * byteRate / samplingRate) + paddingSize); }; lib.getXingOffset = function (mpegVersion, channelMode) { var mono = channelMode === "11"; if (mpegVersion === "11") { // mpeg1 return mono ? 21 : 36; } else { return mono ? 13 : 21; } }; // lib.v1l1Bitrates = { "0000": "free", "0001": 32, "0010": 64, "0011": 96, "0100": 128, "0101": 160, "0110": 192, "0111": 224, "1000": 256, "1001": 288, "1010": 320, "1011": 352, "1100": 384, "1101": 416, "1110": 448, "1111": "bad" }; // lib.v1l2Bitrates = { "0000": "free", "0001": 32, "0010": 48, "0011": 56, "0100": 64, "0101": 80, "0110": 96, "0111": 112, "1000": 128, "1001": 160, "1010": 192, "1011": 224, "1100": 256, "1101": 320, "1110": 384, "1111": "bad" }; // lib.v1l3Bitrates = { "0000": "free", "0001": 32, "0010": 40, "0011": 48, "0100": 56, "0101": 64, "0110": 80, "0111": 96, "1000": 112, "1001": 128, "1010": 160, "1011": 192, "1100": 224, "1101": 256, "1110": 320, "1111": "bad" }; // lib.v2l1Bitrates = { "0000": "free", "0001": 32, "0010": 48, "0011": 56, "0100": 64, "0101": 80, "0110": 96, "0111": 112, "1000": 128, "1001": 144, "1010": 160, "1011": 176, "1100": 192, "1101": 224, "1110": 256, "1111": "bad" }; // lib.v2l2Bitrates = { "0000": "free", "0001": 8, "0010": 16, "0011": 24, "0100": 32, "0101": 40, "0110": 48, "0111": 56, "1000": 64, "1001": 80, "1010": 96, "1011": 112, "1100": 128, "1101": 144, "1110": 160, "1111": "bad" }; lib.v2l3Bitrates = lib.v2l2Bitrates; // lib.v1SamplingRates = { "00": 44100, "01": 48000, "10": 32000, "11": "reserved" }; // lib.v2SamplingRates = { "00": 22050, "01": 24000, "10": 16000, "11": "reserved" }; // lib.v25SamplingRates = { "00": 11025, "01": 12000, "10": 8000, "11": "reserved" }; // lib.channelModes = { "00": "Stereo", "01": "Joint stereo (Stereo)", "10": "Dual channel (Stereo)", "11": "Single channel (Mono)" }; // lib.mpegVersionDescription = { "00": "MPEG Version 2.5 (unofficial)", "01": "reserved", "10": "MPEG Version 2 (ISO/IEC 13818-3)", "11": "MPEG Version 1 (ISO/IEC 11172-3)" }; // lib.layerDescription = { "00": "reserved", "01": "Layer III", "10": "Layer II", "11": "Layer I" }; // lib.bitrateMap = { "11": { "01": lib.v1l3Bitrates, "10": lib.v1l2Bitrates, "11": lib.v1l1Bitrates }, "10": { "01": lib.v2l3Bitrates, "10": lib.v2l2Bitrates, "11": lib.v2l1Bitrates } }; // lib.samplingRateMap = { "00": lib.v25SamplingRates, "10": lib.v2SamplingRates, "11": lib.v1SamplingRates }; // lib.v1SampleLengths = { "01": 1152, "10": 1152, "11": 384 }; // lib.v2SampleLengths = { "01": 576, "10": 1152, "11": 384 }; // lib.sampleLengthMap = { "01": lib.v2SampleLengths, "10": lib.v2SampleLengths, "11": lib.v1SampleLengths }; // Convert the given string `str` to an array of words (octet pairs). If all characters in the // given string are within the ISO/IEC 8859-1 subset then the returned array may safely be // interpreted as an array of values in the [0, 255] range, where each value requires a single // octet to be represented. Otherwise it should be interpreted as an array of values in the // [0, 65.535] range, where each value requires a word (octet pair) to be represented. // // Not meant to be used with UTF-16 strings that contain chars outside the BMP. See // [charCodeAt on MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charCodeAt) lib.wordSeqFromStr = function (str) { for (var i = str.length - 1, seq = []; i >= 0; --i) { seq[i] = str.charCodeAt(i); } return seq; }; // Common character sequences converted to byte arrays lib.seq = { id3: lib.wordSeqFromStr("ID3"), xing: lib.wordSeqFromStr("Xing"), info: lib.wordSeqFromStr("Info") }; // A handy no-op to reuse lib.noOp = function () {}; // Decode a [synchsafe](http://en.wikipedia.org/wiki/Synchsafe) value. Synchsafes are used in // ID3 tags, instead of regular ints, to avoid the unintended introduction of bogus // frame-syncs. Note that the spec requires that syncsafe be always stored in big-endian order // (Implementation shamefully lifted from relevant wikipedia article) lib.unsynchsafe = function (value) { var out = 0; var mask = 0x7F000000; while (mask) { out >>= 1; out |= value & mask; mask >>= 8; } return out; }; // Get a value indicating whether given DataView `view` contains the `seq` sequence (array // of octets) at `offset` index. Note that no check is performed for the adequate length of // given view as this should be carried out by the caller lib.isSeq = function (seq, view, offset) { for (var i = seq.length - 1; i >= 0; i--) { if (seq[i] !== view.getUint8(offset + i)) { return false; } } return true; }; // Get a value indicating whether given DataView `view` contains the `str` string // at `offset` index. The view is parsed as an array of 8bit single-byte coded characters // (i.e. ISO/IEC 8859-1, _non_ Unicode). Will return the string itself if it does, false // otherwise. Note that no check is performed for the adequate length of given view as // this should be carried out be the caller as part of the section-parsing process /* isStr = function (str, view, offset) { return isSeq(lib.wordSeqFromStr(str), view, offset) ? str : false; }; */ // Locate first occurrence of sequence `seq` (an array of octets) in DataView `view`. // Search starts at given `offset` and ends after `length` octets. Will return the // absolute offset of sequence if found, -1 otherwise lib.locateSeq = function (seq, view, offset, length) { for (var i = 0, l = length - seq.length + 1; i < l; ++i) { if (lib.isSeq(seq, view, offset + i)) { return offset + i; } } return -1; }; lib.locateStrTrm = { // Locate the first occurrence of non-Unicode null-terminator (i.e. a single zeroed-out // octet) in DataView `view`. Search starts at given `offset` and ends after `length` // octets. Will return the absolute offset of sequence if found, -1 otherwise iso: function (view, offset, length) { return lib.locateSeq([0], view, offset, length); }, // Locate the first occurrence of Unicode null-terminator (i.e. a sequence of two // zeroed-out octets) in DataView `view`. Search starts at given `offset` and ends after // `length` octets. Will return the absolute offset of sequence if found, -1 otherwise ucs: function (view, offset, length) { var trmOffset = lib.locateSeq([0, 0], view, offset, length); if (trmOffset === -1) { return -1; } if ((trmOffset - offset) % 2 !== 0) { ++trmOffset; } return trmOffset; } }; lib.readStr = { // Parse DataView `view` begining at `offset` index and return a string built from // `length` octets. The view is parsed as an array of 8bit single-byte coded characters // (i.e. ISO/IEC 8859-1, _non_ Unicode). Will essentially return the string comprised of // octets [offset, offset + length). Note that no check is performed for the adequate // length of given view as this should be carried out be the caller as part of the // section-parsing process iso: function (view, offset, length) { return String.fromCharCode.apply(null, new Uint8Array(view.buffer, offset, length)); }, // UCS-2 (ISO/IEC 10646-1:1993, UCS-2) version of `readStr`. UCS-2 is the fixed-width // two-byte subset of Unicode that can only express values inside the Basic Multilingual // Plane (BMP). Note that this method is generally unsuitable for parsing non-trivial // UTF-16 strings which may contain surrogate pairs. [This is only marginally related // though as, according to ID3v2, all Unicode strings should be UCS-2.] Further info: // // * [How to convert ArrayBuffer to and from String](http://updates.html5rocks.com/2012/06/How-to-convert-ArrayBuffer-to-and-from-String) // * [The encoding spec](http://encoding.spec.whatwg.org/) // * [stringencoding shim](https://code.google.com/p/stringencoding/) // // About the BOM: The current implementation will check for and remove the leading BOM from // the given view to avoid invisible characters that mess up the resulting strings. MDN's // documentation for [fromCharCode](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/String/fromCharCode) // suggests that it can correctly convert UCS-2 buffers to strings. And indeed, tests // performed with UCS-2 LE encoded frames indicate that it does. However, no tests have // been made for UCS-2 BE. (Kid3, the ID3v2 Tag generator used for tests at the time of // this writing, goes totally weird when switched to BE) ucs: function (view, offset, length) { // Tweak offset to remove the BOM (LE: FF FE / BE: FE FF) if (view.getUint16(offset) === 0xFFFE || view.getUint16(offset) === 0xFEFF) { offset += 2; length -= 2; } var buffer = view.buffer; // When offset happens to be an even number of octets, the array-buffer may be wrapped // in a Uint16Array. In the event that it's _not_, an actual copy has to be made // (Note that Node <= 0.8 as well as IE <= 10 lack an ArrayBuffer#slice. TODO: shim it) if (offset % 2 === 1) { buffer = buffer.slice(offset, offset + length); offset = 0; } return String.fromCharCode.apply(null, new Uint16Array(buffer, offset, length / 2)); } }; lib.readTrmStr = { // Similar to `readStr.iso` but will check for a null-terminator determining the end of the // string. The returned string will be of _at most_ `length` octets iso: function (view, offset, length) { var trmOffset = lib.locateStrTrm.iso(view, offset, length); if (trmOffset !== -1) { length = trmOffset - offset; } return lib.readStr.iso(view, offset, length); }, // Similar to `readStr.ucs` but will check for a null-terminator determining the end of the // string. The returned string will be of _at most_ `length` octets ucs: function (view, offset, length) { var trmOffset = lib.locateStrTrm.ucs(view, offset, length); if (trmOffset !== -1) { length = trmOffset - offset; } return lib.readStr.ucs(view, offset, length); } }; // ### Read a Frame Header // // Read header of frame located at `offset` of DataView `view`. Returns null in the event // that no frame header is found at `offset` lib.readFrameHeader = function (view, offset) { offset || (offset = 0); // There should be more than 4 octets ahead if (view.byteLength - offset <= 4) { return null; } // Header's first (out of four) octet: `11111111`: Frame sync (all bits must be set) var b1 = view.getUint8(offset); if (b1 !== 255) { return null; } // Header's second (out of four) octet: `111xxxxx` // // * `111.....`: Rest of frame sync (all bits must be set) // * `...BB...`: MPEG Audio version ID (11 -> MPEG Version 1 (ISO/IEC 11172-3)) // * `.....CC.`: Layer description (01 -> Layer III) // * `.......1`: Protection bit (1 = Not protected) // Require the three most significant bits to be `111` (>= 224) var b2 = view.getUint8(offset + 1); if (b2 < 224) { return null; } var mpegVersion = octetToBinRep(b2).substr(3, 2); var layerVersion = octetToBinRep(b2).substr(5, 2); // var header = { _section: { type: "frameHeader", byteLength: 4, offset: offset }, mpegAudioVersionBits: mpegVersion, mpegAudioVersion: lib.mpegVersionDescription[mpegVersion], layerDescriptionBits: layerVersion, layerDescription: lib.layerDescription[layerVersion], isProtected: b2 & 1, // Just check if last bit is set }; header.protectionBit = header.isProtected ? "1" : "0"; if (header.mpegAudioVersion === "reserved") { return null; } if (header.layerDescription === "reserved") { return null; } // Header's third (out of four) octet: `EEEEFFGH` // // * `EEEE....`: Bitrate index. 1111 is invalid, everything else is accepted // * `....FF..`: Sampling rate, 00=44100, 01=48000, 10=32000, 11=reserved // * `......G.`: Padding bit, 0=frame not padded, 1=frame padded // * `.......H`: Private bit. This is informative var b3 = view.getUint8(offset + 2); b3 = octetToBinRep(b3); header.bitrateBits = b3.substr(0, 4); header.bitrate = lib.bitrateMap[mpegVersion][layerVersion][header.bitrateBits]; if (header.bitrate === "bad") { return null; } header.samplingRateBits = b3.substr(4, 2); header.samplingRate = lib.samplingRateMap[mpegVersion][header.samplingRateBits]; if (header.samplingRate === "reserved") { return null; } header.frameIsPaddedBit = b3.substr(6, 1); header.frameIsPadded = header.frameIsPaddedBit === "1"; header.framePadding = header.frameIsPadded ? 1 : 0; header.privateBit = b3.substr(7, 1); // Header's fourth (out of four) octet: `IIJJKLMM` // // * `II......`: Channel mode // * `..JJ....`: Mode extension (only if joint stereo) // * `....K...`: Copyright // * `.....L..`: Original // * `......MM`: Emphasis var b4 = view.getUint8(offset + 3); header.channelModeBits = octetToBinRep(b4).substr(0, 2); header.channelMode = lib.channelModes[header.channelModeBits]; return header; }; // ### Read a Frame // // Read frame located at `offset` of DataView `view`. Will acquire the frame header (see // `readFrameHeader`) plus some basic information about the frame - notably the frame's length // in bytes. If `requireNextFrame` is set, the presence of a _next_ valid frame will be // required for _this_ frame to be regarded as valid. Returns null in the event that no frame // is found at `offset` lib.readFrame = function (view, offset, requireNextFrame) { offset || (offset = 0); var frame = { _section: { type: "frame", offset: offset }, header: lib.readFrameHeader(view, offset) }; var head = frame.header; // Convenience shortcut // Frame should always begin with a valid header if (!head) { return null; } frame._section.sampleLength = lib.sampleLengthMap[head.mpegAudioVersionBits][head.layerDescriptionBits]; // frame._section.byteLength = lib.getFrameByteLength(head.bitrate, head.samplingRate, head.framePadding, head.mpegAudioVersionBits, head.layerDescriptionBits); frame._section.nextFrameIndex = offset + frame._section.byteLength; // No "Xing" or "Info" identifier should be present - this would indicate that this // is in fact a Xing tag masquerading as a frame var xingOffset = lib.getXingOffset(head.mpegAudioVersionBits, head.channelModeBits); if (lib.isSeq(lib.seq.xing, view, offset + xingOffset) || lib.isSeq(lib.seq.info, view, offset + xingOffset)) { return null; } // If a next frame is required then the data at `frame._section.nextFrameIndex` should be // a valid frame header if (requireNextFrame && !lib.readFrameHeader(view, frame._section.nextFrameIndex)) { return null; } return frame; }; })); }); var id3v2 = createCommonjsModule(function (module, exports) { // mp3-parser/id3v2 v0.3.0 // https://github.com/biril/mp3-parser // Licensed and freely distributed under the MIT License // Copyright (c) 2013-2016 Alex Lambiris // ---- /* jshint browser:true */ /* global exports:false, define:false, require:false */ (function (globalObject, createModule) { // Global `exports` object signifies CommonJS enviroments with `module.exports`, e.g. Node { return createModule(exports, lib); } }(commonjsGlobal, function (mp3Id3v2Parser, lib) { // var id3v2TagFrameNames = { AENC: "Audio encryption", APIC: "Attached picture", CHAP: "Chapter", COMM: "Comments", COMR: "Commercial frame", ENCR: "Encryption method registration", EQUA: "Equalization", ETCO: "Event timing codes", GEOB: "General encapsulated object", GRID: "Group identification registration", IPLS: "Involved people list", LINK: "Linked information", MCDI: "Music CD identifier", MLLT: "MPEG location lookup table", OWNE: "Ownership frame", PRIV: "Private frame", PCNT: "Play counter", POPM: "Popularimeter", POSS: "Position synchronisation frame", RBUF: "Recommended buffer size", RVAD: "Relative volume adjustment", RVRB: "Reverb", SYLT: "Synchronized lyric/text", SYTC: "Synchronized tempo codes", TALB: "Album/Movie/Show title", TBPM: "BPM (beats per minute)", TCOM: "Composer", TCON: "Content type", TCOP: "Copyright message", TDAT: "Date", TDLY: "Playlist delay", TENC: "Encoded by", TEXT: "Lyricist/Text writer", TFLT: "File type", TIME: "Time", TIT1: "Content group description", TIT2: "Title/songname/content description", TIT3: "Subtitle/Description refinement", TKEY: "Initial key", TLAN: "Language(s)", TLEN: "Length", TMED: "Media type", TOAL: "Original album/movie/show title", TOFN: "Original filename", TOLY: "Original lyricist(s)/text writer(s)", TOPE: "Original artist(s)/performer(s)", TORY: "Original release year", TOWN: "File owner/licensee", TPE1: "Lead performer(s)/Soloist(s)", TPE2: "Band/orchestra/accompaniment", TPE3: "Conductor/performer refinement", TPE4: "Interpreted, remixed, or otherwise modified by", TPOS: "Part of a set", TPUB: "Publisher", TRCK: "Track number/Position in set", TRDA: "Recording dates", TRSN: "Internet radio station name", TRSO: "Internet radio station owner", TSIZ: "Size", TSRC: "ISRC (international standard recording code)", TSSE: "Software/Hardware and settings used for encoding", TYER: "Year", TXXX: "User defined text information frame", UFID: "Unique file identifier", USER: "Terms of use", USLT: "Unsychronized lyric/text transcription", WCOM: "Commercial information", WCOP: "Copyright/Legal information", WOAF: "Official audio file webpage", WOAR: "Official artist/performer webpage", WOAS: "Official audio source webpage", WORS: "Official internet radio station homepage", WPAY: "Payment", WPUB: "Publishers official webpage", WXXX: "User defined URL link frame" }; // var readFrameContent = {}; // Read the content of a // [text-information frame](http://id3.org/id3v2.3.0#Text_information_frames). These are // common and contain info such as artist and album. There may only be one text info frame // of its kind in a tag. If the textstring is followed by a termination (00) all the // following information should be ignored and not be displayed. All text frame // identifiers begin with "T". Only text frame identifiers begin with "T", with the // exception of the "TXXX" frame // // * Encoding: a single octet where 0 = ISO-8859-1, 1 = UCS-2 // * Information: a text string according to encoding readFrameContent.T = function (view, offset, length) { var content = { encoding: view.getUint8(offset) }; content.value = lib.readStr[content.encoding === 0 ? "iso" : "ucs"]( view, offset + 1, length - 1); return content; }; // Read the content of a // [user-defined text-information frame](http://id3.org/id3v2.3.0#User_defined_text_information_frame). // Intended for one-string text information concerning the audiofile in a similar way to // the other "T"-frames. The frame body consists of a description of the string, // represented as a terminated string, followed by the actual string. There may be more // than one "TXXX" frame in each tag, but only one with the same description // // * Encoding: a single octet where 0 = ISO-8859-1, 1 = UCS-2 // * Description: a text string according to encoding (followed by 00 (00)) // * Value: a text string according to encoding readFrameContent.TXXX = function (view, offset, length) { // The content to be returned var content = { encoding: view.getUint8(offset) }; // Encoding + null term. = at least 2 octets if (length < 2) { return content; // Inadequate length! } // Encoding and content beginning (description field) var enc = content.encoding === 0 ? "iso" : "ucs"; var offsetBeg = offset + 1; // Locate the the null terminator seperating description and URL var offsetTrm = lib.locateStrTrm[enc](view, offsetBeg, length - 4); if (offsetTrm === -1) { return content; // Not found! } // Read description and value data into content content.description = lib.readStr[enc](view, offsetBeg, offsetTrm - offsetBeg); offsetTrm += enc === "ucs" ? 2 : 1; // Move past terminating sequence content.value = lib.readStr[enc](view, offsetTrm, offset + length - offsetTrm); return content; }; // Read the content of a // [URL-link frame](http://id3.org/id3v2.3.0#URL_link_frames). There may only be one // URL link frame of its kind in a tag, except when stated otherwise in the frame // description. If the textstring is followed by a termination (00) all the following // information should be ignored and not be displayed. All URL link frame identifiers // begins with "W". Only URL link frame identifiers begins with "W" // // * URL: a text string readFrameContent.W = function (view, offset, length) { return { value: lib.readStr.iso(view, offset, length) }; }; // Read the content of a // [user-defined URL-link frame](http://id3.org/id3v2.3.0#User_defined_URL_link_frame). // Intended for URL links concerning the audiofile in a similar way to the other // "W"-frames. The frame body consists of a description of the string, represented as a // terminated string, followed by the actual URL. The URL is always encoded with // ISO-8859-1. There may be more than one "WXXX" frame in each tag, but only one with the // same description // // * Encoding: a single octet where 0 = ISO-8859-1, 1 = UCS-2 // * Description: a text string according to encoding (followed by 00 (00)) // * URL: a text string readFrameContent.WXXX = function (view, offset, length) { // The content to be returned var content = { encoding: view.getUint8(offset) }; // Encoding + null term. = at least 2 octets if (length < 2) { return content; // Inadequate length! } // Encoding and content beginning (description field) var enc = content.encoding === 0 ? "iso" : "ucs"; var offsetBeg = offset + 1; // Locate the the null terminator seperating description and URL var offsetTrm = lib.locateStrTrm[enc](view, offsetBeg, length - 4); if (offsetTrm === -1) { return content; // Not found! } // Read description and value data into content content.description = lib.readStr[enc](view, offsetBeg, offsetTrm - offsetBeg); offsetTrm += enc === "ucs" ? 2 : 1; // Move past terminating sequence content.value = lib.readStr.iso(view, offsetTrm, offset + length - offsetTrm); return content; }; // Read the content of a [comment frame](http://id3.org/id3v2.3.0#Comments). // Intended for any kind of full text information that does not fit in any other frame. // Consists of a frame header followed by encoding, language and content descriptors and // ends with the actual comment as a text string. Newline characters are allowed in the // comment text string. There may be more than one comment frame in each tag, but only one // with the same language and content descriptor. [Note that the structure of comment // frames is identical to that of USLT frames - `readFrameContentComm` will handle both.] // // * Encoding: a single octet where 0 = ISO-8859-1, 1 = UCS-2 // * Language: 3 digit (octet) lang-code (ISO-639-2) // * Short descr: a text string according to encoding (followed by 00 (00)) // * Actual text: a text string according to encoding readFrameContent.COMM = function (view, offset, length) { // The content to be returned var content = { encoding: view.getUint8(offset) }; // Encoding + language + null term. = at least 5 octets if (length < 5) { return content; // Inadequate length! } // Encoding and content beggining (short description field) var enc = content.encoding === 0 ? "iso" : "ucs"; var offsetBeg = offset + 4; // Read the language field - 3 octets at most content.language = lib.readTrmStr.iso(view, offset + 1, 3); // Locate the the null terminator seperating description and text var offsetTrm = lib.locateStrTrm[enc](view, offsetBeg, length - 4); if (offsetTrm === -1) { return content; // Not found! } // Read short description and text data into content content.description = lib.readStr[enc](view, offsetBeg, offsetTrm - offsetBeg); offsetTrm += enc === "ucs" ? 2 : 1; // Move past terminating sequence content.text = lib.readStr[enc](view, offsetTrm, offset + length - offsetTrm); return content; }; // Read the content of a // [unique file identifier frame](http://id3.org/id3v2.3.0#Unique_file_identifier). Allows // identification of the audio file by means of some database that may contain more // information relevant to the content. Begins with a URL containing an email address, or // a link to a location where an email address can be found that belongs to the // organisation responsible for this specific database implementation. The 'Owner // identifier' must be non-empty (more than just a termination) and is followed by the // actual identifier, which may be up to 64 bytes. There may be more than one "UFID" frame // in a tag, but only one with the same 'Owner identifier'. Note that this frame is very // similar to the "PRIV" frame // // * Owner identifier: a text string (followed by 00) // * Identifier: up to 64 bytes of binary data readFrameContent.UFID = function (view, offset, length) { // Read up to the first null terminator to get the owner-identifier var ownerIdentifier = lib.readTrmStr.iso(view, offset, length); // Figure out the identifier based on frame length vs owner-identifier length var identifier = new DataView(view.buffer, offset + ownerIdentifier.length + 1, length - ownerIdentifier.length - 1); return { ownerIdentifier: ownerIdentifier, identifier: identifier }; }; // Read the content of an // [involved people list frame](http://id3.org/id3v2.3.0#Involved_people_list). Contains // names of those involved - those contributing to the audio file - and how they were // involved. The body simply contains the first 'involvement' as a terminated string, directly // followed by the first 'involvee' as a terminated string, followed by a second terminated // involvement string and so on. However, in the current implementation the frame's content is // parsed as a collection of strings without any semantics attached. There may only be one // "IPLS" frame in each tag // // * Encoding: a single octet where 0 = ISO-8859-1, 1 = UCS-2 // * People list strings: a series of strings, e.g. string 00 (00) string 00 (00) .. readFrameContent.IPLS = function (view, offset, length) { // The content to be returned var content = { encoding: view.getUint8(offset), values: [] }; // Encoding and content beginning (people list - specifically, first 'involvement' string) var enc = content.encoding === 0 ? "iso" : "ucs"; var offsetBeg = offset + 1; // Index of null-terminator found within people list (seperates involvement / involvee) var offsetNextStrTrm; while (offsetBeg < offset + length) { // We expect all strings within the people list to be null terminated .. offsetNextStrTrm = lib.locateStrTrm[enc](view, offsetBeg, length - (offsetBeg - offset)); // .. except _perhaps_ the last one. In this case fix the offset at the frame's end if (offsetNextStrTrm === -1) { offsetNextStrTrm = offset + length; } content.values.push(lib.readStr[enc](view, offsetBeg, offsetNextStrTrm - offsetBeg)); offsetBeg = offsetNextStrTrm + (enc === "ucs" ? 2 : 1); } return content; }; // Read the content of a [terms of use frame](http://id3.org/id3v2.3.0#Terms_of_use_frame). // Contains a description of the terms of use and ownership of the file. Newlines are // allowed in the text. There may only be one "USER" frame in a tag. // // * Encoding: a single octet where 0 = ISO-8859-1, 1 = UCS-2 // * Language: 3 digit (octet) lang-code (ISO-639-2) // * Actual text: a text string according to encoding readFrameContent.USER = function (view, offset, length) { // The content to be returned var content = { encoding: view.getUint8(offset) }; // Encoding + language + null term. = at least 5 octets if (length < 5) { return content; // Inadequate length! } // Read the language field - 3 octets at most content.language = lib.readTrmStr.iso(view, offset + 1, 3); // Read the text field var offsetBeg = offset + 4; var enc = content.encoding === 0 ? "iso" : "ucs"; content.text = lib.readStr[enc](view, offsetBeg, offset + length - offsetBeg); return content; }; // Read the content of a // [private frame](http://id3.org/id3v2.3.0#Private_frame). Contains binary data that does // no fit into the other frames. Begins with a URL containing an email address, or // a link to a location where an email address can be found. The 'Owner identifier' must // be non-empty (more than just a termination) and is followed by the actual data. There // may be more than one "PRIV" frame in a tag, but only with different contents. Note that // this frame is very similar to the "UFID" frame // // * Owner identifier: a text string (followed by 00) // * private data: binary data (of unbounded length) readFrameContent.PRIV = function (view, offset, length) { // Read up to the first null terminator to get the owner-identifier var ownerIdentifier = lib.readTrmStr.iso(view, offset, length); // Figure out the private data based on frame length vs owner-identifier length var privateData = new DataView(view.buffer, offset + ownerIdentifier.length + 1, length - ownerIdentifier.length - 1); return { ownerIdentifier: ownerIdentifier, privateData: privateData }; }; // Read the content of a [play counter](http://id3.org/id3v2.3.0#Play_counter). A counter // of the number of times a file has been played. There may only be one "PCNT" frame in a // tag. [According to the standard, "When the counter reaches all one's, one byte is // inserted in front of the counter thus making the counter eight bits bigger." This is // not currently taken into account] // // * Counter: 4 octets (at least ..) readFrameContent.PCNT = function (view, offset, length) { // The counter must be at least 4 octets long to begin with if (length < 4) { return {}; // Inadequate length! } // Assume the counter is always exactly 4 octets .. return { counter: view.getUint32(offset) }; }; // Read the content of a [popularimeter](http://id3.org/id3v2.3.0#Popularimeter). Intended // as a measure for the file's popularity, it contains a user's email address, one rating // octet and a four octer play counter, intended to be increased with one for every time // the file is played. If no personal counter is wanted it may be omitted. [As is the case // for the "PCNT" frame, according to the standard, "When the counter reaches all one's, // one byte is inserted in front of the counter thus making the counter eight bits // bigger." This is not currently taken into account]. There may be more than one "POPM" // frame in each tag, but only one with the same email address // // * Email to user: a text string (followed by 00) // * Rating: a single octet, values in 0-255 (0 = unknown, 1 = worst, 255 = best) // * Counter: 4 octets (at least ..) readFrameContent.POPM = function (view, offset, length) { var content = { email: lib.readTrmStr.iso(view, offset, length) }; // rating offset offset += content.email.length + 1; // email str term + rating + counter = at least 6 octets if (length < 6) { return content; // Inadequate length! } content.rating = view.getUint8(offset); // Assume the counter is always exactly 4 octets .. content.counter = view.getUint32(offset + 1); return content; }; // Read the content of an [attached picture](http://id3.org/id3v2.3.0#Attached_picture). // Contains a picture directly related to the audio file. In the event that the MIME media // type name is omitted, "image/" will be implied. The description has a maximum length of // 64 characters, but may be empty. There may be several pictures attached to one file, // each in their individual "APIC" frame, but only one with the same content descriptor. // There may only be one picture with the picture type declared as picture type $01 and // $02 respectively. // // * Encoding: a single octet where 0 = ISO-8859-1, 1 = UCS-2 // * MIME Type: a text string (followed by 00) - MIME type and subtype of image // * Picture type: a single octet, values in 0-255: a type-id as given by the standard // * Description: a text string according to encoding (followed by 00 (00)) // * Picture data: binary data (of unbounded length) readFrameContent.APIC = function (view, offset, length) { // The content to be returned var content = { encoding: view.getUint8(offset) }; // Encoding + MIME type string term + pic type octet + descr. string term = min 4 octets if (length < 4) { return content; // Inadequate length! } // Encoding and offsets of content beginning / null-terminator var enc = content.encoding === 0 ? "iso" : "ucs"; var offsetBeg, offsetTrm; // Locate the the null terminator seperating MIME type and picture type offsetBeg = offset + 1; // After the encoding octet offsetTrm = lib.locateStrTrm.iso(view, offsetBeg, length - 1); if (offsetTrm === -1) { return content; // Not found! } // Read MIME type content.mimeType = lib.readStr.iso(view, offsetBeg, offsetTrm - offsetBeg); // Read picture type offsetBeg = offsetTrm + 1; content.pictureType = view.getUint8(offsetBeg); // Locate the the null terminator seperating description and picture data offsetBeg += 1; offsetTrm = lib.locateStrTrm[enc](view, offsetBeg, offset + length - offsetBeg); if (offsetTrm === -1) { return content; // Not found! } // Read description content.description = lib.readStr[enc](view, offsetBeg, offsetTrm - offsetBeg); // Read picture data offsetBeg = offsetTrm + (enc === "ucs" ? 2 : 1); content.pictureData = new DataView(view.buffer, offsetBeg, offset + length - offsetBeg); return content; }; // Read the chapter tag according to the ID3v2 Chapter Frame Addendum (http://id3.org/id3v2-chapters-1.0) // The frame contains subframes, typically TIT2, and possibly additional frames // // * Id: string identifier of the chapter // * Start time: 4 octets specifying the start of the chapter in milliseconds // * End time: 4 octets specifying the end of the chapter in milliseconds // * Start offset: 4 octets specifying the start of the chapter in bytes // * End offset: 4 octets specifying the end of the chapter in bytes // * Frames: nested id3v2 frames readFrameContent.CHAP = function (view, offset, length) { // The content to be returned var content = { encoding: view.getUint8(offset) }; // Locate the the null terminator between id and start time var offsetTrm = lib.locateStrTrm.iso(view, offset, length - 1); if (offsetTrm === -1) { return content; // Not found! } // Read id content.id = lib.readStr.iso(view, offset, offsetTrm - offset); // Read start time content.startTime = view.getUint32(offsetTrm + 1); // Read end time content.endTime = view.getUint32(offsetTrm + 5); // Read start offset content.startOffset = view.getUint32(offsetTrm + 9); // Read end offset content.endOffset = view.getUint32(offsetTrm + 13); var offsetSubFrames = offsetTrm + 17; content.frames = []; while (offsetSubFrames < offset + length) { var subFrame = mp3Id3v2Parser.readId3v2TagFrame(view, offsetSubFrames); content.frames.push(subFrame); offsetSubFrames += subFrame.header.size + 10; } return content; }; // ### Read an ID3v2 Tag Frame // // Read [ID3v2 Tag frame](http://id3.org/id3v2.3.0#Declared_ID3v2_frames) located at `offset` // of DataView `view`. Returns null in the event that no tag-frame is found at `offset` mp3Id3v2Parser.readId3v2TagFrame = function (view, offset) { // All frames consist of a frame header followed by one or more fields containing the actual // information. The frame header is 10 octets long and laid out as `IIIISSSSFF`, where // // * `IIII......`: Frame id (four characters) // * `....SSSS..`: Size (frame size excluding frame header = frame size - 10) // * `........FF`: Flags var frame = { header: { id: lib.readStr.iso(view, offset, 4), size: view.getUint32(offset + 4), flagsOctet1: view.getUint8(offset + 8), flagsOctet2: view.getUint8(offset + 9) } }; // An ID3v2 tag frame must have a length of at least 1 octet, excluding the header if (frame.header.size < 1) { return frame; } // A function to read the frame's content var readContent = (function (read, id) { // jscs:disable requirePaddingNewLinesBeforeLineComments // User-defined text-information frames if (id === "TXXX") { return read.TXXX; } // Text-information frames if (id.charAt(0) === "T") { return read.T; } // User-defined URL-link frames if (id === "WXXX") { return read.WXXX; } // URL-link frames if (id.charAt(0) === "W") { return read.W; } // Comment frames or Unsychronised lyrics/text transcription frames if (id === "COMM" || id === "USLT") { return read.COMM; } // For any other frame such as UFID, IPLS, USER, etc, return the reader function // that's named after the frame. Return a 'no-op reader' (which just returns // `undefined` as the frame's content) if no implementation found for given frame return read[id] || lib.noOp; }(readFrameContent, frame.header.id)); // jscs-enable requirePaddingNewLinesBeforeLineComments // Store frame's friendly name frame.name = id3v2TagFrameNames[frame.header.id]; // Read frame's content frame.content = readContent(view, offset + 10, frame.header.size); return frame; }; // ### Read the ID3v2 Tag // // Read [ID3v2 Tag](http://id3.org/id3v2.3.0) located at `offset` of DataView `view`. Returns // null in the event that no tag is found at `offset` mp3Id3v2Parser.readId3v2Tag = function (view, offset) { offset || (offset = 0); // The ID3v2 tag header, which should be the first information in the file, is 10 octets // long and laid out as `IIIVVFSSSS`, where // // * `III.......`: id, always "ID3" (0x49/73, 0x44/68, 0x33/51) // * `...VV.....`: version (major version + revision number) // * `.....F....`: flags: abc00000. a:unsynchronisation, b:extended header, c:experimental // * `......SSSS`: tag's size as a synchsafe integer // There should be at least 10 bytes ahead if (view.byteLength - offset < 10) { return null; } // The 'ID3' identifier is expected at given offset if (!lib.isSeq(lib.seq.id3, view, offset)) { return null; } // var flagsOctet = view.getUint8(offset + 5); // var tag = { _section: { type: "ID3v2", offset: offset }, header: { majorVersion: view.getUint8(offset + 3), minorRevision: view.getUint8(offset + 4), flagsOctet: flagsOctet, unsynchronisationFlag: (flagsOctet & 128) === 128, extendedHeaderFlag: (flagsOctet & 64) === 64, experimentalIndicatorFlag: (flagsOctet & 32) === 32, size: lib.unsynchsafe(view.getUint32(offset + 6)) }, frames: [] }; // The size as expressed in the header is the size of the complete tag after // unsychronisation, including padding, excluding the header but not excluding the // extended header (total tag size - 10) tag._section.byteLength = tag.header.size + 10; // Index of octet following tag's last octet: The tag spans [offset, tagEnd) // (including the first 10 header octets) var tagEnd = offset + tag._section.byteLength; // TODO: Process extended header if present. The presence of an extended header will affect // the offset. Currently, it is asummed that no extended header is present so the offset // is fixed at 10 octets // if (tag.header.extendedHeaderFlag) { /* TODO */ } // Go on to read individual frames but only if the tag version is v2.3. This is the only // version currently supported if (tag.header.majorVersion !== 3) { return tag; } // To store frames as they're discovered while paring the tag var frame; // Move offset past the end of the tag header to start reading tag frames offset += 10; while (offset < tagEnd) { // Locating a frame with a zeroed out id indicates that all valid frames have already // been parsed. It's all dead space hereon so practically we're done if (view.getUint32(offset) === 0) { break; } frame = mp3Id3v2Parser.readId3v2TagFrame(view, offset); // Couldn't parse this frame so bail out if (!f