Files
lime/tools/mpeg/audio/MpegAudioReader.hx
Joshua Granick 1188b3aa42 Run formatter
2019-07-12 12:08:09 -07:00

484 lines
10 KiB
Haxe

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<Frame> = [];
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;
}