forked from LeenkxTeam/LNXSDK
171 lines
4.4 KiB
Haxe
171 lines
4.4 KiB
Haxe
/**
|
||
Specification:
|
||
V1: https://github.com/kcat/openal-soft/blob/be7938ed385e18c7800c663672262bb2976aa734/docs/hrtf.txt
|
||
V2: https://github.com/kcat/openal-soft/blob/0349bcc500fdb9b1245a5ddce01b2896bcf9bbb9/docs/hrtf.txt
|
||
V3: https://github.com/kcat/openal-soft/blob/3ef4bffaf959d06527a247faa19cc869781745e4/docs/hrtf.txt
|
||
**/
|
||
|
||
package aura.format.mhr;
|
||
|
||
import haxe.Int64;
|
||
import haxe.ds.Vector;
|
||
import haxe.io.Bytes;
|
||
import haxe.io.BytesInput;
|
||
|
||
import kha.arrays.Float32Array;
|
||
|
||
import aura.types.HRTF;
|
||
|
||
using aura.format.InputExtension;
|
||
|
||
/**
|
||
Load MHR HRTF files (format versions 1–3 are supported) into `HRTF` objects.
|
||
**/
|
||
class MHRReader {
|
||
|
||
public static function read(bytes: Bytes): HRTF {
|
||
final inp = new BytesInput(bytes);
|
||
inp.bigEndian = false;
|
||
|
||
final magic = inp.readString(8, UTF8);
|
||
final version = versionFromMagic(magic);
|
||
|
||
final sampleRate = Int64.toInt(inp.readUInt32());
|
||
final sampleType = switch (version) {
|
||
case V1: SampleType16Bit;
|
||
case V2: inp.readByte();
|
||
case V3: SampleType24Bit;
|
||
}
|
||
|
||
final channelType = switch (version) {
|
||
case V1: 0; // mono
|
||
case V2 | V3: inp.readByte();
|
||
}
|
||
final channels = channelType + 1;
|
||
|
||
// Samples per HRIR (head related impulse response) per channel
|
||
final hrirSize = inp.readByte();
|
||
|
||
// Number of fields used by the data set. Each field represents a
|
||
// set of points for a given distance.
|
||
final fieldCount = version == V1 ? 1 : inp.readByte();
|
||
|
||
final fields = new Vector<Field>(fieldCount);
|
||
var totalHRIRCount = 0;
|
||
for (i in 0...fieldCount) {
|
||
final field = new Field();
|
||
|
||
// 1000mm is arbitrary, but it doesn't matter since the interpolation
|
||
// can only access one distance anyway...
|
||
field.distance = version == V1 ? 1000 : inp.readUInt16();
|
||
field.evCount = inp.readByte();
|
||
field.azCount = new Vector<Int>(field.evCount);
|
||
field.evHRIROffsets = new Vector<Int>(field.evCount);
|
||
|
||
var fieldHrirCount = 0;
|
||
for (j in 0...field.evCount) {
|
||
// Calculate the offset into the HRIR arrays. Different
|
||
// elevations may have different amounts of azimuths/HRIRs
|
||
field.evHRIROffsets[j] = fieldHrirCount;
|
||
|
||
field.azCount[j] = inp.readByte();
|
||
fieldHrirCount += field.azCount[j];
|
||
}
|
||
field.hrirCount = fieldHrirCount;
|
||
totalHRIRCount += fieldHrirCount;
|
||
|
||
fields[i] = field;
|
||
}
|
||
|
||
// Read actual HRIR samples into coeffs
|
||
for (i in 0...fieldCount) {
|
||
final field = fields[i];
|
||
final hrirs = new Vector<HRIR>(field.hrirCount);
|
||
field.hrirs = hrirs;
|
||
|
||
for (j in 0...field.hrirCount) {
|
||
// Create individual HRIR
|
||
final hrir = hrirs[j] = new HRIR();
|
||
|
||
hrir.coeffs = new Float32Array(hrirSize * channels);
|
||
switch (sampleType) {
|
||
case SampleType16Bit:
|
||
for (s in 0...hrirSize) {
|
||
final coeff = inp.readInt16();
|
||
// 32768 = 2^15
|
||
hrir.coeffs[s] = coeff / (coeff < 0 ? 32768.0 : 32767.0);
|
||
}
|
||
|
||
case SampleType24Bit:
|
||
for (s in 0...hrirSize) {
|
||
final coeff = inp.readInt24();
|
||
// 8388608 = 2^23
|
||
hrir.coeffs[s] = coeff / (coeff < 0 ? 8388608.0 : 8388607.0);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Read per-HRIR delay
|
||
var maxDelayLength = 0.0;
|
||
for (i in 0...fieldCount) {
|
||
final field = fields[i];
|
||
|
||
for (j in 0...field.hrirCount) {
|
||
final hrir = field.hrirs[j];
|
||
|
||
hrir.delays = new Vector<Float>(channels);
|
||
for (ch in 0...channels) {
|
||
// 6.2 fixed point
|
||
final delayRaw = inp.readByte();
|
||
final delayIntPart = delayRaw >> 2;
|
||
final delayFloatPart = isBitSet(delayRaw, 1) * 0.5 + isBitSet(delayRaw, 0) * 0.25;
|
||
final delay = delayIntPart + delayFloatPart;
|
||
hrir.delays[ch] = delay;
|
||
if (delay > maxDelayLength) {
|
||
maxDelayLength = delay;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// This should error if uncommented, check if we have reached the end of
|
||
// the file.
|
||
// inp.readByte();
|
||
|
||
return {
|
||
sampleRate: sampleRate,
|
||
numChannels: channels,
|
||
hrirSize: hrirSize,
|
||
hrirCount: totalHRIRCount,
|
||
fields: fields,
|
||
maxDelayLength: maxDelayLength
|
||
};
|
||
}
|
||
|
||
static inline function isBitSet(byte: Int, position: Int): Int {
|
||
return (byte & (1 << position) == 0) ? 0 : 1;
|
||
}
|
||
|
||
static inline function versionFromMagic(magic: String): MHRVersion {
|
||
return switch (magic) {
|
||
case "MinPHR01": V1;
|
||
case "MinPHR02": V2;
|
||
case "MinPHR03": V3;
|
||
default:
|
||
throw 'File is not an MHR HRTF file! Unknown magic string "$magic".';
|
||
}
|
||
}
|
||
}
|
||
|
||
private enum abstract SampleType(Int) from Int {
|
||
var SampleType16Bit;
|
||
var SampleType24Bit;
|
||
}
|
||
|
||
private enum abstract MHRVersion(Int) {
|
||
var V1;
|
||
var V2;
|
||
var V3;
|
||
}
|