/* MPEG audio format reader is part of 3LAS (Low Latency Live Audio Streaming) https://github.com/JoJoBond/3LAS */ window.startTimeConnectionWatchdog = performance.now(); var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { if (typeof b !== "function" && b !== null) throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var MPEGFrameInfo = /** @class */ (function () { function MPEGFrameInfo(data, sampleCount, sampleRate) { this.Data = data; this.SampleCount = sampleCount; this.SampleRate = sampleRate; } return MPEGFrameInfo; }()); var AudioFormatReader_MPEG = /** @class */ (function (_super) { __extends(AudioFormatReader_MPEG, _super); function AudioFormatReader_MPEG(audio, logger, errorCallback, beforeDecodeCheck, dataReadyCallback, addId3Tag, minDecodeFrames) { var _this = _super.call(this, audio, logger, errorCallback, beforeDecodeCheck, dataReadyCallback) || this; _this._OnDecodeSuccess = _this.OnDecodeSuccess.bind(_this); _this._OnDecodeError = _this.OnDecodeError.bind(_this); _this.AddId3Tag = addId3Tag; _this.MinDecodeFrames = minDecodeFrames; _this.Frames = new Array(); _this.FrameStartIdx = -1; _this.FrameEndIdx = -1; _this.FrameSamples = 0; _this.FrameSampleRate = 0; _this.TimeBudget = 0; return _this; } // Deletes all frames from the databuffer and framearray and all samples from the samplearray AudioFormatReader_MPEG.prototype.PurgeData = function () { _super.prototype.PurgeData.call(this); this.Frames = new Array(); this.FrameStartIdx = -1; this.FrameEndIdx = -1; this.FrameSamples = 0; this.FrameSampleRate = 0; this.TimeBudget = 0; }; // Extracts all currently possible frames AudioFormatReader_MPEG.prototype.ExtractAll = function () { // Look for frames this.FindFrame(); // Repeat as long as we can extract frames while (this.CanExtractFrame()) { // Extract frame and push into array this.Frames.push(this.ExtractFrame()); // Look for frames this.FindFrame(); } // Check if we have enough frames to decode if (this.Frames.length >= this.MinDecodeFrames) { // Note: // ===== // mp3 frames have an overlap of [granule size] so we can't use the first or last [granule size] samples. // [granule size] is equal to half of a [frame size] in samples (using the mp3's sample rate). // Sum up the playback time of each decoded frame and data buffer lengths // Note: Since mp3-Frames overlap by half of their sample-length we expect the // first and last frame to be only half as long. Some decoders will still output // the full frame length by adding zeros. var bufferLength = 0; var expectedTotalPlayTime_1 = 0; var firstGranulePlayTime_1 = 0; var lastGranulePlayTime_1 = 0; firstGranulePlayTime_1 = this.Frames[0].SampleCount / this.Frames[0].SampleRate / 2.0; // Only half of data is usable due to overlap expectedTotalPlayTime_1 += firstGranulePlayTime_1; bufferLength += this.Frames[0].Data.length; for (var i = 1; i < this.Frames.length - 1; i++) { expectedTotalPlayTime_1 += this.Frames[i].SampleCount / this.Frames[i].SampleRate; bufferLength += this.Frames[i].Data.length; } lastGranulePlayTime_1 = this.Frames[this.Frames.length - 1].SampleCount / this.Frames[this.Frames.length - 1].SampleRate / 2.0; // Only half of data is usable due to overlap expectedTotalPlayTime_1 += lastGranulePlayTime_1; bufferLength += this.Frames[this.Frames.length - 1].Data.length; // If needed, add some space for the ID3v2 tag if (this.AddId3Tag) { bufferLength += AudioFormatReader_MPEG.Id3v2Tag.length; } // Create a buffer long enough to hold everything var decodeBuffer = new Uint8Array(bufferLength); var offset = 0; // If needed, add ID3v2 tag to beginning of buffer if (this.AddId3Tag) { decodeBuffer.set(AudioFormatReader_MPEG.Id3v2Tag, offset); offset += AudioFormatReader_MPEG.Id3v2Tag.length; } // Add the frames to the window for (var i = 0; i < this.Frames.length; i++) { decodeBuffer.set(this.Frames[i].Data, offset); offset += this.Frames[i].Data.length; } // Remove the used frames from the array this.Frames.splice(0, this.Frames.length - 1); // Increment Id var id_1 = this.Id++; // Check if decoded frames might be too far back in the past if (!this.OnBeforeDecode(id_1, expectedTotalPlayTime_1)) return; // Push window to the decoder this.Audio.decodeAudioData(decodeBuffer.buffer, (function (decodedData) { var _id = id_1; var _expectedTotalPlayTime = expectedTotalPlayTime_1; var _firstGranulePlayTime = firstGranulePlayTime_1; var _lastGranulePlayTime = lastGranulePlayTime_1; this._OnDecodeSuccess(decodedData, _id, _expectedTotalPlayTime, _firstGranulePlayTime, _lastGranulePlayTime); }).bind(this), this._OnDecodeError.bind(this)); } }; // Finds frame boundries within the data buffer AudioFormatReader_MPEG.prototype.FindFrame = function () { // Find frame start if (this.FrameStartIdx < 0) { var i = 0; // Make sure we don't exceed array bounds while ((i + 1) < this.DataBuffer.length) { // Look for MPEG sync word if (this.DataBuffer[i] == 0xFF && (this.DataBuffer[i + 1] & 0xE0) == 0xE0) { // Sync found, set frame start this.FrameStartIdx = i; break; } i++; } } // Find frame end if (this.FrameStartIdx >= 0 && this.FrameEndIdx < 0) { // Check if we have enough data to process the header if ((this.FrameStartIdx + 2) < this.DataBuffer.length) { // Get header data // Version index var ver = (this.DataBuffer[this.FrameStartIdx + 1] & 0x18) >>> 3; // Layer index var lyr = (this.DataBuffer[this.FrameStartIdx + 1] & 0x06) >>> 1; // Padding? 0/1 var pad = (this.DataBuffer[this.FrameStartIdx + 2] & 0x02) >>> 1; // Bitrate index var brx = (this.DataBuffer[this.FrameStartIdx + 2] & 0xF0) >>> 4; // SampRate index var srx = (this.DataBuffer[this.FrameStartIdx + 2] & 0x0C) >>> 2; // Resolve flags to real values var bitrate = AudioFormatReader_MPEG.MPEG_bitrates[ver][lyr][brx] * 1000; var samprate = AudioFormatReader_MPEG.MPEG_srates[ver][srx]; var samples = AudioFormatReader_MPEG.MPEG_frame_samples[ver][lyr]; var slot_size = AudioFormatReader_MPEG.MPEG_slot_size[lyr]; // In-between calculations var bps = samples / 8.0; var fsize = ((bps * bitrate) / samprate) + ((pad == 1) ? slot_size : 0); // Truncate to integer var frameSize = Math.floor(fsize); // Store number of samples and samplerate for frame this.FrameSamples = samples; this.FrameSampleRate = samprate; // Set end frame boundry this.FrameEndIdx = this.FrameStartIdx + frameSize; } } }; // Checks if there is a frame ready to be extracted AudioFormatReader_MPEG.prototype.CanExtractFrame = function () { if (this.FrameStartIdx < 0 || this.FrameEndIdx < 0) return false; else if (this.FrameEndIdx <= this.DataBuffer.length) return true; else return false; }; // Extract a single frame from the buffer AudioFormatReader_MPEG.prototype.ExtractFrame = function () { // Extract frame data from buffer var frameArray = this.DataBuffer.buffer.slice(this.FrameStartIdx, this.FrameEndIdx); // Remove frame from buffer if ((this.FrameEndIdx + 1) < this.DataBuffer.length) this.DataBuffer = new Uint8Array(this.DataBuffer.buffer.slice(this.FrameEndIdx)); else this.DataBuffer = new Uint8Array(0); // Reset Start/End indices this.FrameStartIdx = 0; this.FrameEndIdx = -1; return new MPEGFrameInfo(new Uint8Array(frameArray), this.FrameSamples, this.FrameSampleRate); }; // Is called if the decoding of the window succeeded AudioFormatReader_MPEG.prototype.OnDecodeSuccess = function (decodedData, id, expectedTotalPlayTime, firstGranulePlayTime, lastGranulePlayTime) { window.startTimeConnectionWatchdog = performance.now(); var extractSampleCount; var extractSampleOffset; var delta = 0.001; // Check if we got the expected number of samples if (expectedTotalPlayTime > decodedData.duration) { // We got less samples than expect, we suspect that they were truncated equally at start and end. // This can happen in case of sample rate conversions. extractSampleCount = decodedData.length; extractSampleOffset = 0; this.TimeBudget += (expectedTotalPlayTime - decodedData.duration); } else if (expectedTotalPlayTime < decodedData.duration) { // We got more samples than expect, we suspect that zeros were added equally at start and end. // This can happen in case of sample rate conversions or edge frame handling. extractSampleCount = Math.ceil(expectedTotalPlayTime * decodedData.sampleRate); var budgetSamples = this.TimeBudget * decodedData.sampleRate; if (budgetSamples > 1.0) { if (budgetSamples > decodedData.length - extractSampleCount) { budgetSamples = decodedData.length - extractSampleCount; } extractSampleCount += budgetSamples; this.TimeBudget -= (budgetSamples / decodedData.sampleRate); } var diff = decodedData.duration - expectedTotalPlayTime; if ((diff - firstGranulePlayTime - lastGranulePlayTime) >= -delta) { // Both first and last granule are present. Cut out middle section. extractSampleOffset = Math.floor((decodedData.length - extractSampleCount) / 2); } if (Math.abs(firstGranulePlayTime - lastGranulePlayTime) <= delta) { // First and last granule are equal. We need to make an educated guess which one is present. if (isIOS || isIPadOS || isMacOSX) { // I don't know why, but Apple does things differently. extractSampleOffset = Math.floor((decodedData.length - extractSampleCount) / 2); } else { // Assume last granule is present. extractSampleOffset = decodedData.length - extractSampleCount; } } else if (Math.abs(diff - firstGranulePlayTime) <= delta) { // Last granule is present. extractSampleOffset = decodedData.length - extractSampleCount; } else if (Math.abs(diff - lastGranulePlayTime) <= delta) { // First granule is present. extractSampleOffset = 0; } else { // The difference is not equal to neither the first, the last nor the sum of both granule. So we just use the middle. extractSampleOffset = Math.floor((decodedData.length - extractSampleCount) / 2); } } else { // We got the expected number of samples, no adaption needed extractSampleCount = decodedData.length; extractSampleOffset = 0; } // Create a buffer that can hold the frame to extract var audioBuffer = this.Audio.createBuffer(decodedData.numberOfChannels, extractSampleCount, decodedData.sampleRate); // Fill buffer with the last part of the decoded frame leave out last granule for (var i = 0; i < decodedData.numberOfChannels; i++) audioBuffer.getChannelData(i).set(decodedData.getChannelData(i).subarray(extractSampleOffset, extractSampleOffset + extractSampleCount)); this.OnDataReady(id, audioBuffer); }; // Is called in case the decoding of the window fails AudioFormatReader_MPEG.prototype.OnDecodeError = function (_error) { this.ErrorCallback(); }; // MPEG versions - use [version] AudioFormatReader_MPEG.MPEG_versions = new Array(25, 0, 2, 1); // Layers - use [layer] AudioFormatReader_MPEG.MPEG_layers = new Array(0, 3, 2, 1); // Bitrates - use [version][layer][bitrate] AudioFormatReader_MPEG.MPEG_bitrates = new Array(new Array(// Version 2.5 new Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), // Reserved new Array(0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0), // Layer 3 new Array(0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0), // Layer 2 new Array(0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, 0) // Layer 1 ), new Array(// Reserved new Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), // Invalid new Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), // Invalid new Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), // Invalid new Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) // Invalid ), new Array(// Version 2 new Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), // Reserved new Array(0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0), // Layer 3 new Array(0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0), // Layer 2 new Array(0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, 0) // Layer 1 ), new Array(// Version 1 new Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), // Reserved new Array(0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0), // Layer 3 new Array(0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 0), // Layer 2 new Array(0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 0) // Layer 1 )); // Sample rates - use [version][srate] AudioFormatReader_MPEG.MPEG_srates = new Array(new Array(11025, 12000, 8000, 0), // MPEG 2.5 new Array(0, 0, 0, 0), // Reserved new Array(22050, 24000, 16000, 0), // MPEG 2 new Array(44100, 48000, 32000, 0) // MPEG 1 ); // Samples per frame - use [version][layer] AudioFormatReader_MPEG.MPEG_frame_samples = new Array( // Rsvd 3 2 1 < Layer v Version new Array(0, 576, 1152, 384), // 2.5 new Array(0, 0, 0, 0), // Reserved new Array(0, 576, 1152, 384), // 2 new Array(0, 1152, 1152, 384) // 1 ); AudioFormatReader_MPEG.Id3v2Tag = new Uint8Array(new Array(0x49, 0x44, 0x33, // File identifier: "ID3" 0x03, 0x00, // Version 2.3 0x00, // Flags: no unsynchronisation, no extended header, no experimental indicator 0x00, 0x00, 0x00, 0x0D, // Size of the (tag-)frames, extended header and padding 0x54, 0x49, 0x54, 0x32, // Title frame: "TIT2" 0x00, 0x00, 0x00, 0x02, // Size of the frame data 0x00, 0x00, // Frame Flags 0x00, 0x20, 0x00 // Frame data (space character) and padding )); // Slot size (MPEG unit of measurement) - use [layer] AudioFormatReader_MPEG.MPEG_slot_size = new Array(0, 1, 1, 4); // Rsvd, 3, 2, 1 return AudioFormatReader_MPEG; }(AudioFormatReader));