package mpeg.audio; import haxe.io.Bytes; import haxe.io.Eof; import haxe.io.Input; class MpegAudioReader { // The theoretical absolute maximum frame size is 2881 bytes // (MPEG 2.5 Layer II 160Kb/s, with a padding slot). // // This is the next-largest power-of-two. static inline var BUFFER_SIZE = 4096; static inline var HEADER_SIZE = 4; static inline var CRC_SIZE = 4; static var infoTagSignature = Bytes.ofString("Info"); static var xingTagSignature = Bytes.ofString("Xing"); static var versions = [MpegVersion.Version25, null, MpegVersion.Version2, MpegVersion.Version1]; static var layers = [null, Layer.Layer3, Layer.Layer2, Layer.Layer1]; static var version1Bitrates = [ [ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null ], [ 0, 32000, 40000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 160000, 192000, 224000, 256000, 320000, null ], [ 0, 32000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 160000, 192000, 224000, 256000, 320000, 384000, null ], [ 0, 32000, 64000, 96000, 128000, 160000, 192000, 224000, 256000, 288000, 320000, 352000, 384000, 416000, 448000, null ] ]; static var version2Bitrates = [ [ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null ], [ 0, 8000, 16000, 24000, 32000, 40000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 144000, 160000, null ], [ 0, 8000, 16000, 24000, 32000, 40000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 144000, 160000, null ], [ 0, 32000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 144000, 160000, 176000, 192000, 224000, 256000, null ] ]; static var samplingFrequenciesByVersionIndex = [ [11025, 12000, 8000, null], [null, null, null, null], [22050, 24000, 16000, null], [44100, 48000, 32000, null] ]; static var modes = [Mode.Stereo, Mode.JointStereo, Mode.DualChannel, Mode.SingleChannel]; static var emphases = [Emphasis.None, Emphasis.RedBook, null, Emphasis.J17]; static var slotSizeByLayerIndex = [0, 1, 1, 4]; static var slotsPerBitPerSampleByLayerIndexByVersionIndex = [[null, 72, 144, 12], null, [null, 72, 144, 12], [null, 144, 144, 12]]; var input:Input; var state:MpegAudioReaderState; var seenFirstFrame:Bool; var buffer:Bytes; var bufferCursor:Int; var bufferLength:Int; public function new(input:Input) { if (input == null) { throw "input must not be null"; } this.input = input; this.state = MpegAudioReaderState.Start; seenFirstFrame = false; buffer = Bytes.alloc(BUFFER_SIZE); bufferCursor = 0; bufferLength = 0; } public function readAll() { if (state != MpegAudioReaderState.Start) { throw "Cannot combine calls to readNext and readAll"; } var frames:Array = []; var encoderDelay:Int = 0; var endPadding:Int = 0; while (true) { var element = readNext(); switch (element) { case Frame(frame): frames.push(frame); case Info(_): // Discard info tag. case GaplessInfo(giEncoderDelay, giEndPadding): encoderDelay = giEncoderDelay; endPadding = giEndPadding; case Unknown(_): // Discard unknown bytes case End: break; } } var audio = new MpegAudio(frames, encoderDelay, endPadding); return audio; } public function readNext() { switch (state) { case Start, Seeking: return seek(); case Info(info): return infoTagGaplessInfo(info); case Frame: return frame(); case End: return end(); case Ended: throw new Eof(); } } function seek() { bufferCursor = 0; try { do { do { if (!bufferSpace(2)) { return yieldUnknown(); } } while (readByte() != 0xff); } while ((readByte() & 0x80) != 0x80); } catch (eof:Eof) { return end(); } if (bufferCursor > 2) { state = MpegAudioReaderState.Frame; return yieldUnknown(bufferCursor - 2); } else { return frame(); } } function frame() { var b:Int; try { b = readByte(1); } catch (eof:Eof) { return end(); } var versionIndex = (b >> 3) & 0x3; var layerIndex = (b >> 1) & 0x3; var hasCrc = b & 1 == 0; try { b = readByte(2); } catch (eof:Eof) { return end(); } var bitrateIndex = (b >> 4) & 0xf; var samplingFrequencyIndex = (b >> 2) & 0x3; var hasPadding = (b >> 1) & 1 == 1; var privateBit = b & 1 == 1; try { b = readByte(3); } catch (eof:Eof) { return end(); } var modeIndex = (b >> 6) & 0x3; var modeExtension = (b >> 4) & 0x3; var copyright = (b >> 3) & 1 == 1; var original = (b >> 2) & 1 == 1; var emphasisIndex = b & 0x3; var version = versions[versionIndex]; var layer = layers[layerIndex]; var bitrate = switch (version) { case Version1: version1Bitrates[layerIndex][bitrateIndex]; case Version2, Version25: version2Bitrates[layerIndex][bitrateIndex]; } var samplingFrequency = samplingFrequenciesByVersionIndex[versionIndex][samplingFrequencyIndex]; var mode = modes[modeIndex]; var emphasis = emphases[emphasisIndex]; if (version == null || layer == null || bitrate == null || samplingFrequency == null || emphasis == null) { // This isn't a valid frame. // Seek for another frame starting from the byte after the bogus syncword. state = MpegAudioReaderState.Seeking; return yieldUnknown(1); } var frameData:Bytes; if (bitrate == 0) { // free-format bitrate var end = false; try { do { do { if (!bufferSpace(2)) { return yieldUnknown(); } } while (readByte() != 0xff); } while ((readByte() & 0xf8) != 0xf8); } catch (eof:Eof) { end = true; } var frameLengthBytes = if (end) bufferCursor else bufferCursor - 2; frameLengthBytes -= (frameLengthBytes % slotSizeByLayerIndex[layerIndex]); var frameLengthSlots = Math.floor(frameLengthBytes / slotSizeByLayerIndex[layerIndex]); bitrate = Math.floor(samplingFrequency * frameLengthSlots / slotsPerBitPerSampleByLayerIndexByVersionIndex[versionIndex][layerIndex]); // TODO should bitrate be Float? frameData = yieldBytes(frameLengthBytes); } else { var frameLengthSlots = Math.floor(slotsPerBitPerSampleByLayerIndexByVersionIndex[versionIndex][layerIndex] * bitrate / samplingFrequency); if (hasPadding) { frameLengthSlots += 1; } var frameLengthBytes = frameLengthSlots * slotSizeByLayerIndex[layerIndex]; try { readBytesTo(frameLengthBytes - 1); } catch (eof:Eof) { return end(); } frameData = yieldBytes(); } var header = new FrameHeader(version, layer, hasCrc, bitrate, samplingFrequency, hasPadding, privateBit, mode, modeExtension, copyright, original, emphasis); if (!seenFirstFrame) { seenFirstFrame = true; if (layer == Layer.Layer3) { var info = readInfo(header, frameData); if (info != null) { state = MpegAudioReaderState.Info(info); return Element.Info(info); } } } var frame = new Frame(header, frameData); state = MpegAudioReaderState.Seeking; return Element.Frame(frame); } function readInfo(header:FrameHeader, frameData:Bytes) { var sideInformationSize = switch (header.version) { case Version1: switch (header.mode) { case Stereo, JointStereo, DualChannel: 32; case SingleChannel: 17; }; case Version2, Version25: switch (header.mode) { case Stereo, JointStereo, DualChannel: 17; case SingleChannel: 9; } }; var sideInformationStartIndex = HEADER_SIZE + (if (header.hasCrc) CRC_SIZE else 0); var infoStartIndex = sideInformationStartIndex + sideInformationSize; for (i in sideInformationStartIndex...infoStartIndex) { if (frameData.get(i) != 0) { return null; } } if (frameData.sub(infoStartIndex, infoTagSignature.length).compare(infoTagSignature) == 0 || frameData.sub(infoStartIndex, xingTagSignature.length).compare(xingTagSignature) == 0) { return new Info(header, infoStartIndex, frameData); } else { return null; } } function infoTagGaplessInfo(info:Info) { var b0 = info.frameData.get(info.infoStartIndex + 0x8d); var b1 = info.frameData.get(info.infoStartIndex + 0x8e); var b2 = info.frameData.get(info.infoStartIndex + 0x8f); var encoderDelay = ((b0 << 4) & 0xff0) | ((b1 >> 4) & 0xf); var endPadding = ((b1 << 8) & 0xf00) | (b2 & 0xff); state = MpegAudioReaderState.Seeking; return Element.GaplessInfo(encoderDelay, endPadding); } function end() { var unknownElement = yieldUnknown(bufferLength); if (unknownElement == null) { state = MpegAudioReaderState.Ended; return Element.End; } else { state = MpegAudioReaderState.End; return unknownElement; } } function yieldUnknown(length = -1) { if (length == -1) { length = bufferCursor; } if (length == 0) { return null; } return Element.Unknown(yieldBytes(length)); } function yieldBytes(length = -1) { if (length == -1) { length = bufferCursor; } else if (length == 0) { return Bytes.alloc(0); } assert(length > 0 && length <= bufferLength); var bytes:Bytes = Bytes.alloc(length); bytes.blit(0, buffer, 0, length); buffer.blit(0, buffer, length, bufferLength - length); bufferLength -= length; bufferCursor -= length; return bytes; } inline function assert(condition:Bool) { if (!condition) { throw "MpegAudioReader internal error"; } } inline function bufferSpace(bytes = 1) { return bufferCursor + bytes <= BUFFER_SIZE; } inline function readByte(position:Int = -1) { if (position == -1) { position = bufferCursor; } readBytesTo(position); return buffer.get(position); } inline function readBytes(count:Int) { readBytesTo(bufferCursor + count); } inline function readBytesTo(position:Int) { assert(position >= 0 && position < BUFFER_SIZE); while (bufferLength <= position) { buffer.set(bufferLength, input.readByte()); bufferCursor = ++bufferLength; } bufferCursor = position + 1; } } private enum MpegAudioReaderState { Start; Seeking; Frame; Info(info:Info); End; Ended; }