Merge Aether tools

This commit is contained in:
Joshua Granick
2014-09-30 17:41:39 -07:00
parent f1e3707ad9
commit 540aa48c39
272 changed files with 35574 additions and 127 deletions

View File

@@ -0,0 +1,11 @@
package mpeg.audio;
import haxe.io.Bytes;
enum Element {
Frame(frame:Frame);
Info(info:Info);
GaplessInfo(encoderDelay:Int, endPadding:Int);
Unknown(bytes:Bytes);
End;
}

View File

@@ -0,0 +1,7 @@
package mpeg.audio;
enum Emphasis {
None;
RedBook;
J17;
}

13
tools/mpeg/audio/Frame.hx Normal file
View File

@@ -0,0 +1,13 @@
package mpeg.audio;
import haxe.io.Bytes;
class Frame {
public var header(default, null):FrameHeader;
public var frameData(default, null):Bytes;
public function new(header:FrameHeader, frameData:Bytes) {
this.header = header;
this.frameData = frameData;
}
}

View File

@@ -0,0 +1,35 @@
package mpeg.audio;
import haxe.io.Bytes;
class FrameHeader {
public var version(default, null):MpegVersion;
public var layer(default, null):Layer;
public var hasCrc(default, null):Bool;
public var bitrate(default, null):Int;
public var samplingFrequency(default, null):Int;
public var hasPadding(default, null):Bool;
public var privateBit(default, null):Bool;
public var mode(default, null):Mode;
public var modeExtension(default, null):Int;
public var copyright(default, null):Bool;
public var original(default, null):Bool;
public var emphasis(default, null):Emphasis;
public function new(version:MpegVersion, layer:Layer, hasCrc:Bool, bitrate:Int, samplingFrequency:Int,
hasPadding:Bool, privateBit:Bool, mode:Mode, modeExtension:Int, copyright:Bool, original:Bool,
emphasis:Emphasis) {
this.version = version;
this.layer = layer;
this.hasCrc = hasCrc;
this.bitrate = bitrate;
this.samplingFrequency = samplingFrequency;
this.hasPadding = hasPadding;
this.privateBit = privateBit;
this.mode = mode;
this.modeExtension = modeExtension;
this.copyright = copyright;
this.original = original;
this.emphasis = emphasis;
}
}

15
tools/mpeg/audio/Info.hx Normal file
View File

@@ -0,0 +1,15 @@
package mpeg.audio;
import haxe.io.Bytes;
class Info {
public var header(default, null):FrameHeader;
public var infoStartIndex(default, null):Int;
public var frameData(default, null):Bytes;
public function new(header:FrameHeader, startIndex:Int, frameData:Bytes) {
this.header = header;
this.infoStartIndex = startIndex;
this.frameData = frameData;
}
}

View File

@@ -0,0 +1,7 @@
package mpeg.audio;
enum Layer {
Layer1;
Layer2;
Layer3;
}

8
tools/mpeg/audio/Mode.hx Normal file
View File

@@ -0,0 +1,8 @@
package mpeg.audio;
enum Mode {
Stereo;
JointStereo;
DualChannel;
SingleChannel;
}

View File

@@ -0,0 +1,13 @@
package mpeg.audio;
class MpegAudio {
public var frames(default, null):Iterable<Frame>;
public var encoderDelay:Int;
public var endPadding:Int;
public function new(frames:Array<Frame>, encoderDelay:Int, endPadding:Int) {
this.frames = frames;
this.encoderDelay = encoderDelay;
this.endPadding = endPadding;
}
}

View File

@@ -0,0 +1,413 @@
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;
}

View File

@@ -0,0 +1,7 @@
package mpeg.audio;
enum MpegVersion {
Version1;
Version2;
Version25;
}

23
tools/mpeg/audio/Utils.hx Normal file
View File

@@ -0,0 +1,23 @@
package mpeg.audio;
using Lambda;
class Utils {
public static function calculateAudioLengthSamples(mpegAudio:MpegAudio) {
return mpegAudio.frames
.map(function(frame) { return lookupSamplesPerFrame(frame.header.version, frame.header.layer); })
.fold(function(frameSampleCount, totalSampleCount) { return frameSampleCount + totalSampleCount; },
-mpegAudio.encoderDelay - mpegAudio.endPadding);
}
public static function lookupSamplesPerFrame(mpegVersion:MpegVersion, layer:Layer) {
return switch (layer) {
case Layer1: 384;
case Layer2: 1152;
case Layer3: switch (mpegVersion) {
case Version1: 1152;
case Version2, Version25: 576;
};
};
}
}