Merge Aether tools
This commit is contained in:
11
tools/mpeg/audio/Element.hx
Normal file
11
tools/mpeg/audio/Element.hx
Normal 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;
|
||||
}
|
||||
7
tools/mpeg/audio/Emphasis.hx
Normal file
7
tools/mpeg/audio/Emphasis.hx
Normal file
@@ -0,0 +1,7 @@
|
||||
package mpeg.audio;
|
||||
|
||||
enum Emphasis {
|
||||
None;
|
||||
RedBook;
|
||||
J17;
|
||||
}
|
||||
13
tools/mpeg/audio/Frame.hx
Normal file
13
tools/mpeg/audio/Frame.hx
Normal 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;
|
||||
}
|
||||
}
|
||||
35
tools/mpeg/audio/FrameHeader.hx
Normal file
35
tools/mpeg/audio/FrameHeader.hx
Normal 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
15
tools/mpeg/audio/Info.hx
Normal 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;
|
||||
}
|
||||
}
|
||||
7
tools/mpeg/audio/Layer.hx
Normal file
7
tools/mpeg/audio/Layer.hx
Normal file
@@ -0,0 +1,7 @@
|
||||
package mpeg.audio;
|
||||
|
||||
enum Layer {
|
||||
Layer1;
|
||||
Layer2;
|
||||
Layer3;
|
||||
}
|
||||
8
tools/mpeg/audio/Mode.hx
Normal file
8
tools/mpeg/audio/Mode.hx
Normal file
@@ -0,0 +1,8 @@
|
||||
package mpeg.audio;
|
||||
|
||||
enum Mode {
|
||||
Stereo;
|
||||
JointStereo;
|
||||
DualChannel;
|
||||
SingleChannel;
|
||||
}
|
||||
13
tools/mpeg/audio/MpegAudio.hx
Normal file
13
tools/mpeg/audio/MpegAudio.hx
Normal 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;
|
||||
}
|
||||
}
|
||||
413
tools/mpeg/audio/MpegAudioReader.hx
Normal file
413
tools/mpeg/audio/MpegAudioReader.hx
Normal 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;
|
||||
}
|
||||
7
tools/mpeg/audio/MpegVersion.hx
Normal file
7
tools/mpeg/audio/MpegVersion.hx
Normal file
@@ -0,0 +1,7 @@
|
||||
package mpeg.audio;
|
||||
|
||||
enum MpegVersion {
|
||||
Version1;
|
||||
Version2;
|
||||
Version25;
|
||||
}
|
||||
23
tools/mpeg/audio/Utils.hx
Normal file
23
tools/mpeg/audio/Utils.hx
Normal 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;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user