Init foo_input_joc
build / windows (push) Has been cancelled

This commit is contained in:
2026-09-25 17:07:04 +08:00
commit ad62a914e6
127 changed files with 38545 additions and 0 deletions
+333
View File
@@ -0,0 +1,333 @@
#include "adm/adm_metadata.h"
#include <algorithm>
#include <cmath>
#include <cstring>
#include "foundation/py_num.h"
namespace joc::adm {
namespace {
constexpr const char* kBedNames[10] = {
"RoomCentricLeft", "RoomCentricRight", "RoomCentricCenter", "RoomCentricLFE",
"RoomCentricLeftSideSurround", "RoomCentricRightSideSurround",
"RoomCentricLeftRearSurround", "RoomCentricRightRearSurround",
"RoomCentricLeftTopSurround", "RoomCentricRightTopSurround"};
constexpr const char* kBedLabels[10] = {"RC_L", "RC_R", "RC_C", "RC_LFE", "RC_Lss",
"RC_Rss", "RC_Lrs", "RC_Rrs", "RC_Lts", "RC_Rts"};
constexpr double kBedPos[10][3] = {{-1.0, 1.0, 0.0}, {1.0, 1.0, 0.0}, {0.0, 1.0, 0.0},
{-1.0, 1.0, -1.0}, {-1.0, 0.0, 0.0}, {1.0, 0.0, 0.0},
{-1.0, -1.0, 0.0}, {1.0, -1.0, 0.0}, {-1.0, 0.0, 1.0},
{1.0, 0.0, 1.0}};
std::string hex4(std::uint32_t value) {
char buffer[16];
std::snprintf(buffer, sizeof(buffer), "%04x", value);
return std::string(buffer);
}
std::string hex8(std::uint32_t value) {
char buffer[16];
std::snprintf(buffer, sizeof(buffer), "%08x", value);
return std::string(buffer);
}
void put_u16(std::string* out, std::uint16_t value) {
char buffer[2];
std::memcpy(buffer, &value, 2);
out->append(buffer, 2);
}
void put_u32(std::string* out, std::uint32_t value) {
char buffer[4];
std::memcpy(buffer, &value, 4);
out->append(buffer, 4);
}
std::uint8_t checksum(const std::string& segment) {
int sum = static_cast<int>(segment.size());
for (const char raw : segment) {
sum += static_cast<unsigned char>(raw);
}
return static_cast<std::uint8_t>((~sum + 1) & 0xFF);
}
} // namespace
std::string ts(double seconds) {
long long whole = static_cast<long long>(seconds);
long long fraction = pynum::py_round((seconds - static_cast<double>(whole)) * 100000.0);
if (fraction >= 100000) {
whole += 1;
fraction = 0;
}
char buffer[32];
std::snprintf(buffer, sizeof(buffer), "%02lld:%02lld:%02lld.%05lld", whole / 3600,
(whole % 3600) / 60, whole % 60, fraction);
return std::string(buffer);
}
bool binaural_mode_from_name(const char* name, BinauralMode* out) {
if (name == nullptr || out == nullptr) {
return false;
}
if (std::strcmp(name, "off") == 0) { *out = BinauralMode::Off; return true; }
if (std::strcmp(name, "near") == 0) { *out = BinauralMode::Near; return true; }
if (std::strcmp(name, "far") == 0) { *out = BinauralMode::Far; return true; }
if (std::strcmp(name, "mid") == 0) { *out = BinauralMode::Mid; return true; }
if (std::strcmp(name, "unspecified") == 0) { *out = BinauralMode::Unspecified; return true; }
return false;
}
std::string build_chna() {
std::string out;
put_u16(&out, static_cast<std::uint16_t>(kTrackCount));
put_u16(&out, static_cast<std::uint16_t>(kTrackCount));
for (std::uint32_t i = 0; i < 10; ++i) {
put_u16(&out, static_cast<std::uint16_t>(i + 1));
out += "ATU_" + hex8(i + 1);
out += "AT_0001" + hex4(0x1001 + i) + "_01";
out += "AP_00011001";
out.push_back('\0');
}
for (std::uint32_t i = 0; i < kObjectCount; ++i) {
put_u16(&out, static_cast<std::uint16_t>(i + 11));
out += "ATU_" + hex8(i + 11);
out += "AT_0003" + hex4(0x1001 + i) + "_01";
out += "AP_0003" + hex4(0x1001 + i);
out.push_back('\0');
}
return out;
}
Status build_dbmd(std::uint32_t object_count, BinauralMode mode, std::string* out) {
if (out == nullptr) {
return Status::fail(JOC_ERR_INVALID_ARGUMENT, stage::kOutput, "null output");
}
const std::uint32_t mode_value = static_cast<std::uint32_t>(mode);
if (mode_value > 4u) {
return Status::fail(JOC_ERR_INVALID_ARGUMENT, stage::kOutput,
"invalid JOC binaural render mode");
}
out->clear();
put_u32(out, 0x01000006u);
std::string segment7(96, '\0');
segment7[1] = static_cast<char>(0x47);
segment7[5] = static_cast<char>(0x60);
segment7[8] = static_cast<char>(0x24);
segment7[9] = static_cast<char>(0x24);
out->push_back(7);
put_u16(out, 96);
out->append(segment7);
out->push_back(static_cast<char>(checksum(segment7)));
std::string segment9(248, '\0');
const std::string creator = "Created with EAC3JOC";
const std::string renderer = "EAC3JOC Python Renderer";
std::memcpy(&segment9[0], creator.data(), creator.size());
std::memcpy(&segment9[32], renderer.data(), renderer.size());
segment9[96] = 2;
segment9[97] = 1;
segment9[98] = 0;
segment9[103] = 0x03;
segment9[106] = 0x01;
segment9[111] = 0x22;
segment9[112] = static_cast<char>(0xFF);
out->push_back(9);
put_u16(out, 248);
out->append(segment9);
out->push_back(static_cast<char>(checksum(segment9)));
// The reference allocates the body zeroed and then fills only the trailing
// `object_count` bytes with 0x84, so the template region stays zero.
const std::size_t object_body = 5u + 262u + object_count;
std::string segment10(object_body, '\0');
const std::uint32_t sync = 0xF8726FBDu;
std::memcpy(&segment10[0], &sync, 4);
segment10[4] = static_cast<char>(object_count);
for (std::size_t i = 5u + 262u; i < segment10.size(); ++i) {
segment10[i] = static_cast<char>(0x84);
}
const std::size_t object_modes = 4u + 2u + 1u + 9u * 15u + object_count;
for (std::uint32_t i = 10; i < std::min<std::uint32_t>(object_count, 10u + kObjectCount); ++i) {
const std::size_t index = object_modes + i;
if (index >= segment10.size()) {
return Status::fail(JOC_ERR_INTERNAL, stage::kOutput, "dbmd object slot out of range");
}
segment10[index] = static_cast<char>((static_cast<unsigned char>(segment10[index]) & 0xF8u) |
mode_value);
}
out->push_back(10);
put_u16(out, static_cast<std::uint16_t>(segment10.size()));
out->append(segment10);
out->push_back(static_cast<char>(checksum(segment10)));
out->append("\0\0", 2);
return Status::success();
}
std::string build_axml(const std::vector<Track>& tracks, double duration_sec, std::uint32_t rate) {
const double scale = static_cast<double>(rate);
std::string out;
out.reserve(64u * 1024u);
const std::string duration_ts = ts(duration_sec);
out += "<?xml version=\"1.0\" encoding=\"utf-8\"?>";
out += "<ebuCoreMain xsi:schemaLocation=\"urn:ebu:metadata-schema:ebuCore_2016 ebucore.xsd\" "
"lang=\"en\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" "
"xmlns=\"urn:ebu:metadata-schema:ebuCore_2016\">";
out += "<coreMetadata><format><audioFormatExtended>";
out += "<audioProgramme audioProgrammeID=\"APR_1001\" audioProgrammeName=\"EAC3JOC_Export\" "
"start=\"" +
ts(0.0) + "\" end=\"" + duration_ts + "\">";
out += "<audioContentIDRef>ACO_1001</audioContentIDRef>";
out += "<audioContentIDRef>ACO_1002</audioContentIDRef>";
out += "</audioProgramme>";
out += "<audioContent audioContentID=\"ACO_1001\" "
"audioContentName=\"EAC3JOC_Master_Content\">";
out += "<audioObjectIDRef>AO_1001</audioObjectIDRef>";
out += "<dialogue mixedContentKind=\"0\">2</dialogue>";
out += "</audioContent>";
out += "<audioContent audioContentID=\"ACO_1002\" audioContentName=\"Objects\">";
for (std::uint32_t i = 0; i < kObjectCount; ++i) {
out += "<audioObjectIDRef>AO_" + hex4(0x100b + i) + "</audioObjectIDRef>";
}
out += "<dialogue mixedContentKind=\"0\">2</dialogue>";
out += "</audioContent>";
out += "<audioObject audioObjectID=\"AO_1001\" audioObjectName=\"Bed\" start=\"" + ts(0.0) +
"\" duration=\"" + duration_ts + "\">";
out += "<audioPackFormatIDRef>AP_00011001</audioPackFormatIDRef>";
for (std::uint32_t i = 0; i < 10; ++i) {
out += "<audioTrackUIDRef>ATU_" + hex8(i + 1) + "</audioTrackUIDRef>";
}
out += "</audioObject>";
for (std::uint32_t i = 0; i < kObjectCount; ++i) {
out += "<audioObject audioObjectID=\"AO_" + hex4(0x100b + i) +
"\" audioObjectName=\"Audio Object " + std::to_string(i + 1) + "\" start=\"" +
ts(0.0) + "\" duration=\"" + duration_ts + "\">";
out += "<audioPackFormatIDRef>AP_0003" + hex4(0x1001 + i) + "</audioPackFormatIDRef>";
out += "<audioTrackUIDRef>ATU_" + hex8(11 + i) + "</audioTrackUIDRef>";
out += "</audioObject>";
}
out += "<audioPackFormat audioPackFormatID=\"AP_00011001\" "
"audioPackFormatName=\"EAC3JOCBedPack\" typeDefinition=\"DirectSpeakers\" "
"typeLabel=\"0001\">";
for (std::uint32_t i = 0; i < 10; ++i) {
out += "<audioChannelFormatIDRef>AC_0001" + hex4(0x1001 + i) +
"</audioChannelFormatIDRef>";
}
out += "</audioPackFormat>";
for (std::uint32_t i = 0; i < kObjectCount; ++i) {
out += "<audioPackFormat audioPackFormatID=\"AP_0003" + hex4(0x1001 + i) +
"\" audioPackFormatName=\"JOC_Object_" + std::to_string(i + 1) +
"\" typeDefinition=\"Objects\" typeLabel=\"0003\">";
out += "<audioChannelFormatIDRef>AC_0003" + hex4(0x1001 + i) +
"</audioChannelFormatIDRef>";
out += "</audioPackFormat>";
}
for (std::uint32_t i = 0; i < 10; ++i) {
out += "<audioChannelFormat audioChannelFormatID=\"AC_0001" + hex4(0x1001 + i) +
"\" audioChannelFormatName=\"" + kBedNames[i] +
"\" typeDefinition=\"DirectSpeakers\" typeLabel=\"0001\">";
out += "<audioBlockFormat audioBlockFormatID=\"AB_0001" + hex4(0x1001 + i) +
"_00000001\">";
out += "<cartesian>1</cartesian>";
out += "<position coordinate=\"X\">" + pynum::format_fixed(kBedPos[i][0], 10) +
"</position>";
out += "<position coordinate=\"Y\">" + pynum::format_fixed(kBedPos[i][1], 10) +
"</position>";
if (kBedPos[i][2] != 0.0) {
out += "<position coordinate=\"Z\">" + pynum::format_fixed(kBedPos[i][2], 10) +
"</position>";
}
out += std::string("<speakerLabel>") + kBedLabels[i] + "</speakerLabel>";
out += "</audioBlockFormat>";
out += "</audioChannelFormat>";
}
for (std::size_t i = 0; i < tracks.size(); ++i) {
const Track& track = tracks[i];
out += "<audioChannelFormat audioChannelFormatID=\"AC_0003" +
hex4(0x1001 + static_cast<std::uint32_t>(i)) + "\" audioChannelFormatName=\"" +
track.name + "\" typeDefinition=\"Objects\" typeLabel=\"0003\">";
for (std::size_t k = 0; k < track.blocks.size(); ++k) {
const Keyframe& block = track.blocks[k];
out += "<audioBlockFormat audioBlockFormatID=\"AB_0003" +
hex4(0x1001 + static_cast<std::uint32_t>(i)) + "_" +
hex8(static_cast<std::uint32_t>(k + 1)) + "\" rtime=\"" +
ts(static_cast<double>(block.rtime_samples) / scale) + "\" duration=\"" +
ts(static_cast<double>(block.duration_samples) / scale) + "\">";
out += "<cartesian>1</cartesian>";
out += "<position coordinate=\"X\">" + pynum::format_fixed(block.x, 10) + "</position>";
out += "<position coordinate=\"Y\">" + pynum::format_fixed(block.y, 10) + "</position>";
if (block.z != 0.0) {
out += "<position coordinate=\"Z\">" + pynum::format_fixed(block.z, 10) +
"</position>";
}
out += "<jumpPosition interpolationLength=\"" +
pynum::format_fixed(static_cast<double>(block.interpolation_samples) / scale,
5) +
"\">1</jumpPosition>";
out += "</audioBlockFormat>";
}
out += "</audioChannelFormat>";
}
for (std::uint32_t i = 0; i < 10; ++i) {
out += "<audioTrackUID UID=\"ATU_" + hex8(i + 1) +
"\" bitDepth=\"24\" sampleRate=\"48000\">";
out += "<audioTrackFormatIDRef>AT_0001" + hex4(0x1001 + i) + "_01</audioTrackFormatIDRef>";
out += "<audioPackFormatIDRef>AP_00011001</audioPackFormatIDRef>";
out += "</audioTrackUID>";
}
for (std::uint32_t i = 0; i < kObjectCount; ++i) {
out += "<audioTrackUID UID=\"ATU_" + hex8(11 + i) +
"\" bitDepth=\"24\" sampleRate=\"48000\">";
out += "<audioTrackFormatIDRef>AT_0003" + hex4(0x1001 + i) + "_01</audioTrackFormatIDRef>";
out += "<audioPackFormatIDRef>AP_0003" + hex4(0x1001 + i) + "</audioPackFormatIDRef>";
out += "</audioTrackUID>";
}
for (std::uint32_t i = 0; i < 10; ++i) {
out += "<audioTrackFormat audioTrackFormatID=\"AT_0001" + hex4(0x1001 + i) +
"_01\" audioTrackFormatName=\"PCM_" + kBedNames[i] +
"\" formatDefinition=\"PCM\" formatLabel=\"0001\">";
out += "<audioStreamFormatIDRef>AS_0001" + hex4(0x1001 + i) +
"</audioStreamFormatIDRef>";
out += "</audioTrackFormat>";
}
for (std::uint32_t i = 0; i < kObjectCount; ++i) {
out += "<audioTrackFormat audioTrackFormatID=\"AT_0003" + hex4(0x1001 + i) +
"_01\" audioTrackFormatName=\"PCM_JOC_Object_" + std::to_string(i + 1) +
"\" formatDefinition=\"PCM\" formatLabel=\"0001\">";
out += "<audioStreamFormatIDRef>AS_0003" + hex4(0x1001 + i) +
"</audioStreamFormatIDRef>";
out += "</audioTrackFormat>";
}
for (std::uint32_t i = 0; i < 10; ++i) {
out += "<audioStreamFormat audioStreamFormatID=\"AS_0001" + hex4(0x1001 + i) +
"\" audioStreamFormatName=\"PCM_" + kBedNames[i] +
"\" formatDefinition=\"PCM\" formatLabel=\"0001\">";
out += "<audioChannelFormatIDRef>AC_0001" + hex4(0x1001 + i) +
"</audioChannelFormatIDRef>";
out += "<audioPackFormatIDRef>AP_00011001</audioPackFormatIDRef>";
out += "<audioTrackFormatIDRef>AT_0001" + hex4(0x1001 + i) +
"_01</audioTrackFormatIDRef>";
out += "</audioStreamFormat>";
}
for (std::uint32_t i = 0; i < kObjectCount; ++i) {
out += "<audioStreamFormat audioStreamFormatID=\"AS_0003" + hex4(0x1001 + i) +
"\" audioStreamFormatName=\"PCM_JOC_Object_" + std::to_string(i + 1) +
"\" formatDefinition=\"PCM\" formatLabel=\"0001\">";
out += "<audioChannelFormatIDRef>AC_0003" + hex4(0x1001 + i) +
"</audioChannelFormatIDRef>";
out += "<audioPackFormatIDRef>AP_0003" + hex4(0x1001 + i) + "</audioPackFormatIDRef>";
out += "<audioTrackFormatIDRef>AT_0003" + hex4(0x1001 + i) +
"_01</audioTrackFormatIDRef>";
out += "</audioStreamFormat>";
}
out += "</audioFormatExtended></format></coreMetadata>";
out += "</ebuCoreMain>";
return out;
}
} // namespace joc::adm
+43
View File
@@ -0,0 +1,43 @@
// Port of src/adm_atmos.py.
#pragma once
#include <cstdint>
#include <string>
#include <vector>
#include "foundation/status.h"
namespace joc::adm {
inline constexpr int kObjectCount = 15;
inline constexpr std::uint32_t kTrackCount = 25;
struct Keyframe {
std::int64_t rtime_samples = 0;
double x = 0.0;
double y = 0.0;
double z = 0.0;
std::int64_t duration_samples = 0;
std::int64_t interpolation_samples = 0;
};
struct Track {
std::string name;
std::vector<Keyframe> blocks;
};
// HH:MM:SS.fffff with the reference's truncation + round-half-even carry.
std::string ts(double seconds);
enum class BinauralMode : std::uint32_t { Off = 0, Near = 1, Far = 2, Mid = 3, Unspecified = 4 };
bool binaural_mode_from_name(const char* name, BinauralMode* out);
std::string build_axml(const std::vector<Track>& tracks, double duration_sec, std::uint32_t rate);
std::string build_chna();
Status build_dbmd(std::uint32_t object_count, BinauralMode mode, std::string* out);
} // namespace joc::adm
+281
View File
@@ -0,0 +1,281 @@
#include "adm/adm_tracks.h"
#include <algorithm>
#include <cmath>
#include "foundation/geometry.h"
namespace joc::adm {
namespace {
struct Point {
std::int64_t sample = 0;
double x = 0.0;
double y = 0.0;
double z = 0.0;
std::int64_t interpolation_samples = 0;
};
// otherwise (the reference drops silently).
void append_point(std::vector<Point>* points, std::int64_t sample, double x, double y, double z,
std::int64_t interpolation_samples) {
if (!points->empty() && sample == points->back().sample) {
points->back() = Point{sample, x, y, z, interpolation_samples};
} else if (points->empty() || sample > points->back().sample) {
points->push_back(Point{sample, x, y, z, interpolation_samples});
}
}
void lerp(double ax, double ay, double az, double bx, double by, double bz, double amount,
double* x, double* y, double* z) {
*x = ax + (bx - ax) * amount;
*y = ay + (by - ay) * amount;
*z = az + (bz - az) * amount;
}
void points_to_blocks(const std::vector<Point>& points, std::int64_t total_samples,
std::vector<Keyframe>* out) {
for (std::size_t index = 0; index < points.size(); ++index) {
const Point& point = points[index];
const std::int64_t end =
(index + 1 < points.size()) ? points[index + 1].sample : total_samples;
const std::int64_t duration = std::max<std::int64_t>(0, end - point.sample);
if (duration == 0) {
continue;
}
Keyframe keyframe;
keyframe.rtime_samples = point.sample;
keyframe.x = point.x;
keyframe.y = point.y;
keyframe.z = point.z;
keyframe.duration_samples = duration;
keyframe.interpolation_samples = std::min(point.interpolation_samples, duration);
out->push_back(keyframe);
}
}
Status non_monotonic(const char* name, const char* message, int object_index, std::int64_t sample,
std::int64_t previous_sample) {
return Status::fail(JOC_ERR_OAMD_UNSUPPORTED_VARIANT, stage::kOamd,
std::string(name) + ": " + message + " (object " +
std::to_string(object_index) + ", sample " + std::to_string(sample) +
", previous " + std::to_string(previous_sample) + ")");
}
} // namespace
Status expand_compact(const std::vector<OamdEvent>& events, std::int64_t total_samples, std::uint32_t rate,
std::int64_t update_quantum_samples, std::int64_t object_delay_samples,
int object_index, std::vector<Keyframe>* out) {
out->clear();
const double scale = static_cast<double>(rate);
if (events.empty()) {
Keyframe keyframe;
keyframe.rtime_samples = 0;
keyframe.duration_samples = total_samples;
keyframe.interpolation_samples = 0;
keyframe.x = 0.0;
keyframe.y = 0.0;
keyframe.z = 0.0;
out->push_back(keyframe);
return Status::success();
}
std::vector<Point> points;
double current[3] = {events[0].x, events[0].y, events[0].z};
append_point(&points, 0, current[0], current[1], current[2], 0);
for (std::size_t index = 1; index < events.size(); ++index) {
const OamdEvent& event = events[index];
const std::int64_t event_start = event.sample + object_delay_samples;
if (event_start >= total_samples) {
break;
}
const std::int64_t effective_ramp =
std::max<std::int64_t>(0, event.ramp_samples - update_quantum_samples);
const std::int64_t block_start =
event_start + (effective_ramp != 0 ? update_quantum_samples : 0);
if (block_start >= total_samples) {
break;
}
const std::int64_t ramp_end = block_start + effective_ramp;
if (block_start < points.back().sample) {
return non_monotonic("non_monotonic_compact_position_updates",
"compact object position update moved backwards", object_index,
block_start, points.back().sample);
}
if (index + 1 < events.size()) {
const std::int64_t next_event_start = events[index + 1].sample + object_delay_samples;
const std::int64_t next_effective =
std::max<std::int64_t>(0, events[index + 1].ramp_samples - update_quantum_samples);
const std::int64_t next_block_start =
next_event_start + (next_effective != 0 ? update_quantum_samples : 0);
if (next_block_start < ramp_end) {
return non_monotonic("overlapping_compact_position_ramps",
"a new position update arrived before the previous compact "
"ramp finished",
object_index, block_start, ramp_end);
}
}
double target[3] = {event.x, event.y, event.z};
std::int64_t interpolation = effective_ramp;
const std::int64_t available = total_samples - block_start;
if (effective_ramp > available) {
const double amount =
static_cast<double>(available) / static_cast<double>(effective_ramp);
double x = 0.0;
double y = 0.0;
double z = 0.0;
lerp(current[0], current[1], current[2], event.x, event.y, event.z, amount, &x, &y, &z);
target[0] = x;
target[1] = y;
target[2] = z;
interpolation = available;
}
append_point(&points, block_start, target[0], target[1], target[2], interpolation);
current[0] = event.x;
current[1] = event.y;
current[2] = event.z;
}
points_to_blocks(points, total_samples, out);
(void)scale;
return Status::success();
}
Status expand_dense64(const std::vector<OamdEvent>& events, std::int64_t total_samples, std::uint32_t rate,
std::int64_t update_quantum_samples, std::int64_t object_delay_samples,
int object_index, std::vector<Keyframe>* out) {
out->clear();
(void)rate;
if (events.empty()) {
Keyframe keyframe;
keyframe.duration_samples = total_samples;
out->push_back(keyframe);
return Status::success();
}
std::vector<Point> points;
double current[3] = {events[0].x, events[0].y, events[0].z};
append_point(&points, 0, current[0], current[1], current[2], 0);
for (std::size_t index = 1; index < events.size(); ++index) {
const OamdEvent& event = events[index];
const std::int64_t start = event.sample + object_delay_samples;
if (start >= total_samples) {
break;
}
if (start < points.back().sample) {
return non_monotonic("non_monotonic_position_updates",
"object position update moved backwards", object_index, start,
points.back().sample);
}
if (start > points.back().sample) {
append_point(&points, start, current[0], current[1], current[2], 0);
}
const std::int64_t effective_ramp =
std::max<std::int64_t>(0, event.ramp_samples - update_quantum_samples);
if (effective_ramp == 0) {
append_point(&points, start, event.x, event.y, event.z, 0);
current[0] = event.x;
current[1] = event.y;
current[2] = event.z;
continue;
}
const std::int64_t steps =
(effective_ramp + update_quantum_samples - 1) / update_quantum_samples;
const std::int64_t end = start + steps * update_quantum_samples;
if (index + 1 < events.size()) {
const std::int64_t next_start = events[index + 1].sample + object_delay_samples;
if (next_start < end) {
return non_monotonic("overlapping_position_ramps",
"a new position update arrived before the previous ramp "
"finished",
object_index, start, end);
}
}
std::int64_t future = effective_ramp;
std::int64_t elapsed = 0;
double position[3] = {current[0], current[1], current[2]};
while (future > 0) {
const double amount =
std::min(static_cast<double>(update_quantum_samples) / static_cast<double>(future),
1.0);
double x = 0.0;
double y = 0.0;
double z = 0.0;
lerp(position[0], position[1], position[2], event.x, event.y, event.z, amount, &x, &y,
&z);
position[0] = x;
position[1] = y;
position[2] = z;
elapsed += update_quantum_samples;
const std::int64_t sample = start + elapsed;
if (sample >= total_samples) {
break;
}
append_point(&points, sample, position[0], position[1], position[2],
update_quantum_samples);
future -= update_quantum_samples;
}
current[0] = event.x;
current[1] = event.y;
current[2] = event.z;
}
points_to_blocks(points, total_samples, out);
return Status::success();
}
void TrajectoryBuilder::submit_frame(std::int64_t frame_index, const oamd::OamdUpdate* update,
std::int64_t outer_offset) {
std::int64_t event_sample = frame_index * 1536;
std::int64_t ramp_samples = 0;
if (update != nullptr) {
state_.apply(*update);
event_sample += outer_offset + static_cast<std::int64_t>(update->block_offset_samples);
ramp_samples = static_cast<std::int64_t>(update->ramp_duration_samples);
}
for (int object = 1; object <= kObjectCount; ++object) {
double x = 0.0;
double y = 0.0;
double z = 0.0;
geometry::q_to_adm_xyz(state_.q(object, 0), state_.q(object, 1), state_.q(object, 2), &x,
&y, &z);
const int slot = object - 1;
if (!has_previous_[slot] || previous_[slot][0] != x || previous_[slot][1] != y ||
previous_[slot][2] != z) {
events_[slot].push_back(OamdEvent{event_sample, x, y, z, ramp_samples});
previous_[slot][0] = x;
previous_[slot][1] = y;
previous_[slot][2] = z;
has_previous_[slot] = true;
}
}
}
Status TrajectoryBuilder::build(std::int64_t total_samples, TrajectoryMode mode,
std::vector<Track>* out) const {
out->clear();
out->reserve(kObjectCount);
for (int object = 1; object <= kObjectCount; ++object) {
Track track;
track.name = "JOC_Object_" + std::to_string(object);
const Status status =
(mode == TrajectoryMode::Compact)
? expand_compact(events_[object - 1], total_samples, rate_, quantum_,
object_delay_, object, &track.blocks)
: expand_dense64(events_[object - 1], total_samples, rate_, quantum_,
object_delay_, object, &track.blocks);
if (!status.ok()) {
return status;
}
out->push_back(std::move(track));
}
return Status::success();
}
} // namespace joc::adm
+61
View File
@@ -0,0 +1,61 @@
// Port of src/oamd_tracks.py.
#pragma once
#include <cstdint>
#include <string>
#include <vector>
#include "adm/adm_metadata.h"
#include "foundation/status.h"
#include "oamd/oamd_parser.h"
namespace joc::adm {
struct OamdEvent {
std::int64_t sample = 0;
double x = 0.0;
double y = 0.0;
double z = 0.0;
std::int64_t ramp_samples = 0;
};
enum class TrajectoryMode { Compact, Dense64 };
// Feeds the same per-frame OAMD state machine the reference's build_adm_tracks
// runs, and records one event per object whenever its coordinates change.
class TrajectoryBuilder {
public:
TrajectoryBuilder(std::uint32_t rate = 48000, std::int64_t update_quantum_samples = 64,
std::int64_t object_delay_samples = 1473)
: rate_(rate),
quantum_(update_quantum_samples),
object_delay_(object_delay_samples) {}
void submit_frame(std::int64_t frame_index, const oamd::OamdUpdate* update, std::int64_t outer_offset);
Status build(std::int64_t total_samples, TrajectoryMode mode, std::vector<Track>* out) const;
const std::vector<OamdEvent>& events(int object_index) const { return events_[object_index]; }
std::uint32_t rate() const { return rate_; }
std::int64_t object_delay_samples() const { return object_delay_; }
private:
std::uint32_t rate_;
std::int64_t quantum_;
std::int64_t object_delay_;
oamd::OamdState state_;
std::vector<OamdEvent> events_[kObjectCount];
bool has_previous_[kObjectCount] = {};
double previous_[kObjectCount][3] = {};
};
Status expand_compact(const std::vector<OamdEvent>& events, std::int64_t total_samples, std::uint32_t rate,
std::int64_t update_quantum_samples, std::int64_t object_delay_samples,
int object_index, std::vector<Keyframe>* out);
Status expand_dense64(const std::vector<OamdEvent>& events, std::int64_t total_samples, std::uint32_t rate,
std::int64_t update_quantum_samples, std::int64_t object_delay_samples,
int object_index, std::vector<Keyframe>* out);
} // namespace joc::adm
+249
View File
@@ -0,0 +1,249 @@
#include "joc_core.h"
#include <cstring>
#include <string>
#include "eac3_transport/eac3_reader.h"
#include "emdf/emdf_parser.h"
#include "foundation/status.h"
#include "joc_bitstream/joc_parser.h"
namespace {
thread_local std::string g_detail;
joc_error finish(const joc::Status& status) {
if (status.ok()) {
g_detail.clear();
return JOC_OK;
}
g_detail.assign(status.stage());
g_detail.append(": ");
g_detail.append(status.message());
return status.code();
}
joc_error arg_fail(const char* message) {
return finish(joc::Status::fail(JOC_ERR_INVALID_ARGUMENT, "core", message));
}
void fill_emdf_info(const joc::emdf::Container& container, joc_emdf_info* out) {
std::memset(out, 0, sizeof(*out));
out->struct_size = sizeof(joc_emdf_info);
out->struct_version = JOC_EMDF_INFO_VERSION;
out->start_bit = static_cast<std::uint32_t>(container.start_bit);
out->container_bytes = static_cast<std::uint32_t>(container.raw_size);
out->payload_count = static_cast<std::uint32_t>(container.payload_count);
for (std::size_t i = 0; i < container.payload_count; ++i) {
out->payloads[i].id = container.payloads[i].id;
out->payloads[i].sample_offset = container.payloads[i].sample_offset;
out->payloads[i].bit_offset = static_cast<std::uint32_t>(container.payloads[i].bit_offset);
out->payloads[i].size = static_cast<std::uint32_t>(container.payloads[i].size);
}
}
} // namespace
extern "C" {
std::uint32_t JOC_CALL joc_abi_version(void) { return JOC_ABI_VERSION; }
const char* JOC_CALL joc_version_string(void) { return "0.1.0-m1"; }
std::uint32_t JOC_CALL joc_event_size(void) { return static_cast<std::uint32_t>(sizeof(joc_event)); }
std::uint32_t JOC_CALL joc_task_config_size(void) {
return static_cast<std::uint32_t>(sizeof(joc_task_config));
}
std::uint32_t JOC_CALL joc_task_result_size(void) {
return static_cast<std::uint32_t>(sizeof(joc_task_result));
}
const char* JOC_CALL joc_build_info(void) {
static const std::string info = [] {
std::string text = "joc_core 0.1.0-m1 (";
#if defined(_MSC_VER)
text += "msvc " + std::to_string(_MSC_VER);
#elif defined(__clang__)
text += std::string("clang ") + __clang_version__;
#elif defined(__GNUC__)
text += "gcc " + std::to_string(__GNUC__) + "." + std::to_string(__GNUC_MINOR__);
#else
text += "unknown-compiler";
#endif
#if defined(_M_AMD64) || defined(__x86_64__)
text += ", x64";
#elif defined(_M_ARM64) || defined(__aarch64__)
text += ", arm64";
#elif defined(_M_IX86) || defined(__i386__)
text += ", x86";
#endif
text += ", c++";
text += std::to_string(static_cast<long long>(__cplusplus / 100 % 100));
text += ")";
return text;
}();
return info.c_str();
}
const char* JOC_CALL joc_error_name(joc_error code) {
switch (code) {
case JOC_OK: return "JOC_OK";
case JOC_ERR_INVALID_ARGUMENT: return "JOC_ERR_INVALID_ARGUMENT";
case JOC_ERR_INVALID_CONFIG: return "JOC_ERR_INVALID_CONFIG";
case JOC_ERR_OUT_OF_MEMORY: return "JOC_ERR_OUT_OF_MEMORY";
case JOC_ERR_IO: return "JOC_ERR_IO";
case JOC_ERR_UNSUPPORTED_PLATFORM: return "JOC_ERR_UNSUPPORTED_PLATFORM";
case JOC_ERR_LIBRARY_MISSING: return "JOC_ERR_LIBRARY_MISSING";
case JOC_ERR_INPUT_NOT_FOUND: return "JOC_ERR_INPUT_NOT_FOUND";
case JOC_ERR_INPUT_FORMAT: return "JOC_ERR_INPUT_FORMAT";
case JOC_ERR_EAC3_SYNCFRAME: return "JOC_ERR_EAC3_SYNCFRAME";
case JOC_ERR_EMDF_TRANSPORT: return "JOC_ERR_EMDF_TRANSPORT";
case JOC_ERR_EMDF_SYNTAX: return "JOC_ERR_EMDF_SYNTAX";
case JOC_ERR_JOC_SYNTAX: return "JOC_ERR_JOC_SYNTAX";
case JOC_ERR_JOC_UNSUPPORTED_VARIANT: return "JOC_ERR_JOC_UNSUPPORTED_VARIANT";
case JOC_ERR_OAMD_SYNTAX: return "JOC_ERR_OAMD_SYNTAX";
case JOC_ERR_OAMD_UNSUPPORTED_VARIANT: return "JOC_ERR_OAMD_UNSUPPORTED_VARIANT";
case JOC_ERR_BITSTREAM_TRUNCATED: return "JOC_ERR_BITSTREAM_TRUNCATED";
case JOC_ERR_BITSTREAM_PADDING: return "JOC_ERR_BITSTREAM_PADDING";
case JOC_ERR_HRTF_NOT_FOUND: return "JOC_ERR_HRTF_NOT_FOUND";
case JOC_ERR_HRTF_FORMAT: return "JOC_ERR_HRTF_FORMAT";
case JOC_ERR_HRTF_VERSION: return "JOC_ERR_HRTF_VERSION";
case JOC_ERR_HRTF_HASH: return "JOC_ERR_HRTF_HASH";
case JOC_ERR_HRTF_UNSUPPORTED_CONVENTION: return "JOC_ERR_HRTF_UNSUPPORTED_CONVENTION";
case JOC_ERR_LAYOUT_UNSUPPORTED: return "JOC_ERR_LAYOUT_UNSUPPORTED";
case JOC_ERR_RENDER_FAILED: return "JOC_ERR_RENDER_FAILED";
case JOC_ERR_OUTPUT_OPEN: return "JOC_ERR_OUTPUT_OPEN";
case JOC_ERR_OUTPUT_WRITE: return "JOC_ERR_OUTPUT_WRITE";
case JOC_ERR_OUTPUT_CLIP_ABORT: return "JOC_ERR_OUTPUT_CLIP_ABORT";
case JOC_ERR_ADM_VALIDATION: return "JOC_ERR_ADM_VALIDATION";
case JOC_ERR_CANCELLED: return "JOC_ERR_CANCELLED";
case JOC_ERR_STATE: return "JOC_ERR_STATE";
case JOC_ERR_NOT_SUPPORTED: return "JOC_ERR_NOT_SUPPORTED";
case JOC_ERR_INTERNAL: return "JOC_ERR_INTERNAL";
default: return "JOC_ERR_UNKNOWN";
}
}
const char* JOC_CALL joc_error_stage(joc_error code) {
switch (code) {
case JOC_ERR_EAC3_SYNCFRAME:
case JOC_ERR_INPUT_NOT_FOUND:
case JOC_ERR_INPUT_FORMAT:
return "eac3_transport";
case JOC_ERR_EMDF_TRANSPORT:
case JOC_ERR_EMDF_SYNTAX:
return "emdf";
case JOC_ERR_JOC_SYNTAX:
case JOC_ERR_JOC_UNSUPPORTED_VARIANT:
return "joc";
case JOC_ERR_OAMD_SYNTAX:
case JOC_ERR_OAMD_UNSUPPORTED_VARIANT:
return "oamd";
case JOC_ERR_BITSTREAM_TRUNCATED:
case JOC_ERR_BITSTREAM_PADDING:
return "bitstream";
case JOC_ERR_HRTF_NOT_FOUND:
case JOC_ERR_HRTF_FORMAT:
case JOC_ERR_HRTF_VERSION:
case JOC_ERR_HRTF_HASH:
case JOC_ERR_HRTF_UNSUPPORTED_CONVENTION:
return "hrtf";
case JOC_ERR_LAYOUT_UNSUPPORTED:
case JOC_ERR_RENDER_FAILED:
return "render";
case JOC_ERR_OUTPUT_OPEN:
case JOC_ERR_OUTPUT_WRITE:
case JOC_ERR_OUTPUT_CLIP_ABORT:
case JOC_ERR_ADM_VALIDATION:
return "output";
case JOC_ERR_CANCELLED:
case JOC_ERR_STATE:
case JOC_ERR_NOT_SUPPORTED:
case JOC_ERR_INTERNAL:
return "task";
default:
return "core";
}
}
const char* JOC_CALL joc_last_error_detail(void) { return g_detail.c_str(); }
joc_error JOC_CALL joc_parse_id14(const std::uint8_t* payload, std::size_t payload_size,
joc_frame_params* out_params) {
if (payload == nullptr || out_params == nullptr || payload_size == 0) {
return arg_fail("joc_parse_id14 requires a non-empty payload and an output struct");
}
return finish(joc::joc::parse_id14(payload, payload_size, out_params, nullptr));
}
joc_error JOC_CALL joc_parse_eac3_frame(const std::uint8_t* frame, std::size_t frame_size,
joc_frame_params* out_params, joc_emdf_info* out_emdf) {
if (frame == nullptr || out_params == nullptr || frame_size == 0) {
return arg_fail("joc_parse_eac3_frame requires a frame and an output struct");
}
joc::emdf::Container container;
const joc::Status status =
joc::joc::parse_eac3_frame(frame, frame_size, out_params, &container, nullptr);
if (!status.ok()) {
return finish(status);
}
if (out_emdf != nullptr) {
fill_emdf_info(container, out_emdf);
}
return JOC_OK;
}
joc_error JOC_CALL joc_extract_payload(const std::uint8_t* frame, std::size_t frame_size,
const joc_emdf_payload_info* payload, std::uint8_t* out,
std::size_t out_capacity, std::size_t* out_size) {
if (frame == nullptr || payload == nullptr || out_size == nullptr) {
return arg_fail("joc_extract_payload requires frame, payload and out_size");
}
*out_size = payload->size;
if (out == nullptr) {
return JOC_OK;
}
if (out_capacity < payload->size) {
return arg_fail("joc_extract_payload output buffer too small");
}
joc::emdf::Payload entry;
entry.id = payload->id;
entry.sample_offset = payload->sample_offset;
entry.bit_offset = payload->bit_offset;
entry.size = payload->size;
std::vector<std::uint8_t> bytes;
const joc::Status status = joc::emdf::extract_payload_bytes(frame, frame_size, entry, &bytes);
if (!status.ok()) {
return finish(status);
}
if (!bytes.empty()) {
std::memcpy(out, bytes.data(), bytes.size());
}
*out_size = bytes.size();
return JOC_OK;
}
joc_error JOC_CALL joc_eac3_frame_bytes(const std::uint8_t* data, std::size_t size, std::size_t offset,
std::size_t* out_frame_bytes) {
if (data == nullptr || out_frame_bytes == nullptr) {
return arg_fail("joc_eac3_frame_bytes requires data and out_frame_bytes");
}
const joc_error code = joc::eac3::FrameReader::frame_bytes(data, size, offset, out_frame_bytes);
if (code != JOC_OK) {
return finish(joc::Status::fail(code, joc::stage::kEac3,
"invalid or truncated E-AC-3 syncframe at byte " +
std::to_string(offset)));
}
return JOC_OK;
}
joc_error JOC_CALL joc_check_id14_padding(const std::uint8_t* payload, std::size_t payload_size,
std::uint32_t* out_trailing_bits) {
if (payload == nullptr || payload_size == 0) {
return arg_fail("joc_check_id14_padding requires a payload");
}
return finish(joc::joc::check_id14_padding(payload, payload_size, out_trailing_bits));
}
} // extern "C"
+162
View File
@@ -0,0 +1,162 @@
#include <cstring>
#include <string>
#include "foundation/status.h"
#include "joc_core.h"
#include "joc_stream.h"
#include "stream/stream.h"
struct joc_stream {
joc::stream::Stream instance;
};
namespace {
joc_error finish_stream(const joc::Status& status) {
return status.code();
}
joc::stream::Config to_config(const joc_stream_config& config) {
joc::stream::Config out;
out.input = config.input;
out.output = config.output;
if (config.speaker_layout_name != nullptr) { out.layout = config.speaker_layout_name; }
if (config.speaker_metadata_offset != 0u) {
out.metadata_offset = config.speaker_metadata_offset;
}
out.binaural_mode = config.binaural_mode != 0u ? config.binaural_mode : JOC_BINAURAL_MID;
if (config.hrtf_path != nullptr) { out.hrtf_path = config.hrtf_path; }
if (config.kernels_path != nullptr) { out.kernels_path = config.kernels_path; }
if (config.binaural_tail_seconds > 0.0) { out.tail_seconds = config.binaural_tail_seconds; }
if (config.object_delay_samples != 0u) {
out.object_delay_samples = config.object_delay_samples;
}
out.gain_db = config.gain_db;
out.native_threads = config.native_threads;
// The binaural HRTF inputs of joc_task_config, copied with the same defaults:
// the policy is taken verbatim (0 is "none", a real choice, not "unset") and
// the radius keeps its documented default of 1.0 when the field is not set.
if (config.hrtf_sofa_path != nullptr) { out.hrtf_sofa_path = config.hrtf_sofa_path; }
if (config.personalized_headphone_path != nullptr) {
out.personalized_headphone_path = config.personalized_headphone_path;
}
if (config.hrtf_cache_dir != nullptr) { out.hrtf_cache_dir = config.hrtf_cache_dir; }
out.hrtf_cache_policy = config.hrtf_cache_policy;
if (config.hrtf_radius_m > 0.0) { out.hrtf_radius_m = config.hrtf_radius_m; }
return out;
}
} // namespace
extern "C" {
joc_error JOC_CALL joc_stream_create(const joc_stream_config* config, joc_stream** out) {
if (config == nullptr || out == nullptr || config->struct_size != sizeof(joc_stream_config)) {
return JOC_ERR_INVALID_ARGUMENT;
}
auto* stream = new (std::nothrow) joc_stream();
if (stream == nullptr) {
return JOC_ERR_OUT_OF_MEMORY;
}
const joc::Status status = stream->instance.create(to_config(*config));
if (!status.ok()) {
delete stream;
return finish_stream(status);
}
*out = stream;
return JOC_OK;
}
joc_error JOC_CALL joc_stream_push(joc_stream* stream, const joc_stream_buffer* input,
std::uint32_t* consumed_samples, std::uint32_t* consumed_bytes) {
if (stream == nullptr || input == nullptr ||
input->struct_size != sizeof(joc_stream_buffer)) {
return JOC_ERR_INVALID_ARGUMENT;
}
const joc::Status status = [&] {
if (input->kind == JOC_STREAM_IN_EAC3) {
std::size_t consumed = 0;
const joc::Status pushed =
stream->instance.push_eac3(input->bytes, input->byte_count, &consumed);
if (consumed_bytes != nullptr) {
*consumed_bytes = static_cast<std::uint32_t>(consumed);
}
return pushed;
}
if (input->kind == JOC_STREAM_IN_PCM_OBJECTS16) {
std::size_t consumed = 0;
const joc::Status pushed =
stream->instance.push_objects16(input->pcm, input->sample_count, &consumed);
if (consumed_samples != nullptr) {
*consumed_samples = static_cast<std::uint32_t>(consumed);
}
return pushed;
}
std::size_t consumed = 0;
const joc::Status pushed =
stream->instance.push_bed(input->pcm, input->sample_count, &consumed);
if (consumed_samples != nullptr) {
*consumed_samples = static_cast<std::uint32_t>(consumed);
}
return pushed; }();
return finish_stream(status);
}
joc_error JOC_CALL joc_stream_pull(joc_stream* stream, joc_stream_buffer* output,
std::uint32_t* produced_samples) {
if (stream == nullptr || output == nullptr || output->out_pcm == nullptr ||
output->struct_size != sizeof(joc_stream_buffer)) {
return JOC_ERR_INVALID_ARGUMENT;
}
std::size_t produced = 0;
const joc::Status status =
stream->instance.pull(output->out_pcm, output->sample_count, &produced);
if (!status.ok()) {
return finish_stream(status);
}
if (produced_samples != nullptr) {
*produced_samples = static_cast<std::uint32_t>(produced);
}
output->sample_count = static_cast<std::uint32_t>(produced);
return JOC_OK;
}
joc_error JOC_CALL joc_stream_flush(joc_stream* stream) {
if (stream == nullptr) {
return JOC_ERR_INVALID_ARGUMENT;
}
return finish_stream(stream->instance.flush());
}
joc_error JOC_CALL joc_stream_reset(joc_stream* stream) {
if (stream == nullptr) {
return JOC_ERR_INVALID_ARGUMENT;
}
return finish_stream(stream->instance.reset());
}
joc_error JOC_CALL joc_stream_status(const joc_stream* stream, joc_stream_status_info* out) {
if (stream == nullptr || out == nullptr ||
out->struct_size != sizeof(joc_stream_status_info)) {
return JOC_ERR_INVALID_ARGUMENT;
}
const joc::stream::Info& info = stream->instance.info();
out->frames_in = info.frames_in;
out->frames_out = info.frames_out;
out->samples_in = info.samples_in;
out->samples_out = info.samples_out;
out->bytes_in = info.bytes_in;
out->buffered_samples = stream->instance.buffered_samples();
out->oamd_payloads = info.oamd_payloads;
out->oamd_transitions = info.oamd_transitions;
out->output_channels = info.output_channels;
out->ended = info.ended;
return JOC_OK;
}
joc_error JOC_CALL joc_stream_destroy(joc_stream* stream) {
delete stream;
return JOC_OK;
}
} // extern "C"
+120
View File
@@ -0,0 +1,120 @@
#include <atomic>
#include <cstring>
#include <new>
#include <string>
#include <vector>
#include "foundation/status.h"
#include "joc_core.h"
#include "task/task.h"
// task and whichever frontend wants to stop it (plan 29.1/29.2).
struct joc_cancel_token {
std::atomic<std::uint32_t> requested{0u};
};
namespace {
thread_local std::string g_task_detail;
joc_error finish_task(const joc::Status& status) {
if (status.ok()) {
g_task_detail.clear();
return JOC_OK;
}
g_task_detail.assign(status.stage());
g_task_detail.append(": ");
g_task_detail.append(status.message());
return status.code();
}
} // namespace
extern "C" {
joc_cancel_token* JOC_CALL joc_cancel_token_create(void) {
return new (std::nothrow) joc_cancel_token();
}
void JOC_CALL joc_cancel_token_request(joc_cancel_token* token) {
if (token != nullptr) {
token->requested.store(1u, std::memory_order_relaxed);
}
}
std::int32_t JOC_CALL joc_cancel_token_is_requested(const joc_cancel_token* token) {
return (token != nullptr && token->requested.load(std::memory_order_relaxed) != 0u) ? 1 : 0;
}
void JOC_CALL joc_cancel_token_destroy(joc_cancel_token* token) { delete token; }
joc_error JOC_CALL joc_task_validate(const joc_task_config* config,
joc_validation_issue* issues, std::uint32_t capacity,
std::uint32_t* count) {
if (config == nullptr) {
return JOC_ERR_INVALID_ARGUMENT;
}
if (config->struct_size != sizeof(joc_task_config)) {
return JOC_ERR_INVALID_ARGUMENT;
}
std::vector<joc_validation_issue> found;
std::uint32_t errors = 0;
const joc::Status status = joc::task::validate(*config, &found, &errors);
if (count != nullptr) {
*count = static_cast<std::uint32_t>(found.size());
}
if (issues != nullptr) {
for (std::uint32_t i = 0; i < capacity && i < found.size(); ++i) {
issues[i] = found[i];
}
}
return status.ok() ? JOC_OK : JOC_ERR_INVALID_CONFIG;
}
joc_error JOC_CALL joc_task_execute(const joc_task_config* config, const joc_event_sink* sink,
joc_task_result* out) {
if (config == nullptr || config->struct_size != sizeof(joc_task_config)) {
return JOC_ERR_INVALID_ARGUMENT;
}
if (out != nullptr) {
std::memset(out, 0, sizeof(*out));
out->struct_size = sizeof(joc_task_result);
out->struct_version = JOC_TASK_RESULT_VERSION;
}
const joc::Status status = joc::task::run(*config, sink, out);
if (!status.ok() && out != nullptr && out->status == 0u) {
out->status = JOC_TASK_FAILED;
out->error_code = static_cast<std::uint32_t>(status.code());
std::snprintf(out->error_stage, sizeof(out->error_stage), "%s", status.stage().c_str());
std::snprintf(out->error_message, sizeof(out->error_message), "%s",
status.message().c_str());
}
g_task_detail = status.ok() ? std::string() : status.message();
return status.code();
}
joc_error JOC_CALL joc_task_result_to_json(const joc_task_result* result, char* buffer,
std::size_t capacity, std::size_t* needed) {
if (result == nullptr) {
return JOC_ERR_INVALID_ARGUMENT;
}
std::string json;
const joc::Status status = joc::task::result_to_json(*result, &json);
if (!status.ok()) {
return status.code();
}
if (needed != nullptr) {
*needed = json.size() + 1u;
}
if (buffer == nullptr) {
return JOC_OK;
}
if (capacity < json.size() + 1u) {
return JOC_ERR_INVALID_ARGUMENT;
}
std::memcpy(buffer, json.c_str(), json.size() + 1u);
return JOC_OK;
}
} // extern "C"
+664
View File
@@ -0,0 +1,664 @@
#define EJOC_BUILD_DLL
#include "eac3joc_core.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <new>
#include <vector>
namespace ejoc::binaural {
struct Complex {
double re;
double im;
};
inline Complex add(Complex a, Complex b) noexcept {
return {a.re + b.re, a.im + b.im};
}
inline Complex mul(Complex a, Complex b) noexcept {
return {a.re * b.re - a.im * b.im, a.re * b.im + a.im * b.re};
}
inline Complex scale(Complex value, double gain) noexcept {
return {value.re * gain, value.im * gain};
}
constexpr double kPi = 3.141592653589793238462643383279502884;
constexpr int kChannels = EJOC_BINAURAL_INPUT_CHANNELS;
constexpr int kEars = EJOC_BINAURAL_OUTPUT_CHANNELS;
constexpr int kBlock = EJOC_BINAURAL_BLOCK_SAMPLES;
constexpr int kSlots = kBlock / 64;
constexpr int kQmf = EJOC_BINAURAL_QMF_BANDS;
constexpr int kHybrid = EJOC_BINAURAL_HYBRID_BANDS;
constexpr int kRank = 4;
class Renderer final {
public:
Renderer() noexcept {
initialize_fft();
reset();
}
int configure_kernels(
const double* qmf_analysis,
const double* hybrid_low,
const int16_t* hybrid_indices,
const double* hybrid_values,
uint32_t hybrid_count,
const double* qmf_basis,
const double* qmf_taps) noexcept {
if (!qmf_analysis || !hybrid_low || !hybrid_indices || !hybrid_values ||
!qmf_basis || !qmf_taps || hybrid_count == 0) {
return fail("invalid binaural kernel configuration");
}
std::memcpy(qmf_analysis_.data(), qmf_analysis,
qmf_analysis_.size() * sizeof(double));
hybrid_low_.assign(hybrid_low, hybrid_low + 3 * 2 * 13 * 16 * 2);
hybrid_indices_.assign(hybrid_indices, hybrid_indices + hybrid_count * 4);
hybrid_values_.assign(hybrid_values, hybrid_values + hybrid_count);
std::memcpy(qmf_basis_.data(), qmf_basis,
qmf_basis_.size() * sizeof(double));
std::memcpy(qmf_taps_.data(), qmf_taps,
qmf_taps_.size() * sizeof(double));
kernels_ready_ = true;
reset();
return 0;
}
int configure_room(
uint32_t bands,
uint32_t allpass_count,
const uint32_t* allpass_delays,
const double* allpass_gains,
const uint32_t* fdn_delays,
const double* fdn_matrix,
uint32_t output_tap_delay,
const double* feedback_complex,
const double* output_taps,
const double* output_complex,
uint32_t extra_count,
const uint32_t* extra_delays,
const double* extra_fields_complex,
const double* extra_matrices) noexcept {
if (bands != 64 || !fdn_delays || !fdn_matrix || !feedback_complex ||
!output_taps || !output_complex ||
(allpass_count && (!allpass_delays || !allpass_gains)) ||
(extra_count && (!extra_delays || !extra_fields_complex || !extra_matrices))) {
return fail("invalid binaural room configuration");
}
room_bands_ = bands;
if (allpass_count) {
allpass_delays_.assign(allpass_delays, allpass_delays + allpass_count);
allpass_gains_.assign(allpass_gains, allpass_gains + allpass_count);
} else {
allpass_delays_.clear();
allpass_gains_.clear();
}
allpass_offsets_.resize(allpass_count);
allpass_positions_.assign(allpass_count, 0);
size_t allpass_size = 0;
for (uint32_t index = 0; index < allpass_count; ++index) {
if (allpass_delays_[index] == 0) {
return fail("binaural allpass delay must be positive");
}
allpass_offsets_[index] = allpass_size;
allpass_size += static_cast<size_t>(allpass_delays_[index]) * bands;
}
allpass_memory_.assign(allpass_size, {});
room_capacity_ = 0;
for (int branch = 0; branch < 4; ++branch) {
fdn_delays_[branch] = fdn_delays[branch];
room_capacity_ = std::max(room_capacity_, fdn_delays_[branch]);
}
if (room_capacity_ == 0) {
return fail("binaural room delay must be positive");
}
std::copy(fdn_matrix, fdn_matrix + 16, fdn_matrix_.begin());
output_tap_delay_ = output_tap_delay;
for (int band = 0; band < 64; ++band) {
for (int branch = 0; branch < 4; ++branch) {
const size_t complex_index = (static_cast<size_t>(band) * 4 + branch) * 2;
feedback_[band][branch] = {
feedback_complex[complex_index], feedback_complex[complex_index + 1]};
output_taps_[band][branch] = output_taps[band * 4 + branch];
for (int ear = 0; ear < 2; ++ear) {
const size_t output_index =
((static_cast<size_t>(ear) * 64 + band) * 4 + branch) * 2;
output_matrix_[ear][band][branch] = {
output_complex[output_index], output_complex[output_index + 1]};
}
}
}
room_memory_.assign(static_cast<size_t>(room_capacity_) * 64 * 4, {});
if (extra_count) {
extra_delays_.assign(extra_delays, extra_delays + extra_count);
} else {
extra_delays_.clear();
}
extra_fields_.resize(static_cast<size_t>(extra_count) * 64);
extra_matrices_.resize(static_cast<size_t>(extra_count) * 16);
for (uint32_t extra = 0; extra < extra_count; ++extra) {
for (int band = 0; band < 64; ++band) {
const size_t source = (static_cast<size_t>(extra) * 64 + band) * 2;
extra_fields_[static_cast<size_t>(extra) * 64 + band] = {
extra_fields_complex[source], extra_fields_complex[source + 1]};
}
std::copy(extra_matrices + static_cast<size_t>(extra) * 16,
extra_matrices + static_cast<size_t>(extra + 1) * 16,
extra_matrices_.begin() + static_cast<size_t>(extra) * 16);
}
room_ready_ = true;
reset();
return 0;
}
int reset() noexcept {
qmf_history_.fill(0.0);
hybrid_low_history_.fill({});
hybrid_high_history_.fill({});
synthesis_history_.fill(0.0);
std::fill(allpass_memory_.begin(), allpass_memory_.end(), Complex{});
std::fill(allpass_positions_.begin(), allpass_positions_.end(), 0u);
std::fill(room_memory_.begin(), room_memory_.end(), Complex{});
room_position_ = 0;
error_[0] = '\0';
return 0;
}
const char* error() const noexcept {
return error_[0] ? error_ : "";
}
int process(
const double* input,
const double* gains,
const double* room_sends,
double output_gain,
double* output) noexcept {
if (!kernels_ready_ || !room_ready_) {
return fail("binaural renderer is not configured");
}
if (!input || !gains || !room_sends || !output || !std::isfinite(output_gain)) {
return fail("invalid binaural process arguments");
}
for (int slot = 0; slot < kSlots; ++slot) {
std::array<Complex, kChannels * kQmf> qmf{};
std::array<Complex, kChannels * kHybrid> hybrid{};
analyze_qmf(input + static_cast<size_t>(slot) * 64 * kChannels, qmf);
analyze_hybrid(qmf, hybrid);
std::array<Complex, kEars * kHybrid> rendered{};
std::array<Complex, kHybrid> room_input{};
for (int source = kChannels - 1; source >= 0; --source) {
for (int band = 0; band < kHybrid; ++band) {
const Complex value = hybrid[source * kHybrid + band];
room_input[band] = add(room_input[band], scale(value, room_sends[source]));
for (int ear = 0; ear < kEars; ++ear) {
const size_t gain_index =
(((static_cast<size_t>(source) * kEars + ear) * kHybrid + band) * 2);
const Complex gain{gains[gain_index], gains[gain_index + 1]};
rendered[ear * kHybrid + band] = add(
rendered[ear * kHybrid + band], mul(value, gain));
}
}
}
const auto room = process_room(room_input);
for (size_t index = 0; index < rendered.size(); ++index) {
rendered[index] = add(rendered[index], room[index]);
}
std::array<Complex, kEars * kQmf> qmf_output{};
synthesize_hybrid(rendered, qmf_output);
for (int ear = 0; ear < kEars; ++ear) {
std::array<double, 64> samples{};
synthesize_qmf(qmf_output.data() + ear * kQmf, ear, samples);
for (int sample = 0; sample < 64; ++sample) {
output[(static_cast<size_t>(slot) * 64 + sample) * 2 + ear] =
samples[sample] * output_gain;
}
}
}
return 0;
}
private:
int fail(const char* message) noexcept {
std::snprintf(error_, sizeof(error_), "%s", message);
return -1;
}
void initialize_fft() noexcept {
for (int index = 0; index < 128; ++index) {
int value = index;
int reversed = 0;
for (int bit = 0; bit < 7; ++bit) {
reversed = (reversed << 1) | (value & 1);
value >>= 1;
}
bit_reverse_[index] = static_cast<uint8_t>(reversed);
}
for (int phase = 0; phase < 64; ++phase) {
const double angle = -kPi * static_cast<double>(phase) / 128.0;
premod_[phase] = {std::cos(angle), std::sin(angle)};
const double post_angle =
-3.0 * (static_cast<double>(phase) + 0.5) * kPi / 128.0;
post_[phase] = {std::cos(post_angle), std::sin(post_angle)};
even_post_[phase] = {0.0, (phase & 1) ? -1.0 : 1.0};
}
}
void fft128(std::array<Complex, 128>& values) const noexcept {
for (int index = 0; index < 128; ++index) {
const int reversed = bit_reverse_[index];
if (reversed > index) {
std::swap(values[index], values[reversed]);
}
}
for (int length = 2; length <= 128; length <<= 1) {
const double angle = -2.0 * kPi / static_cast<double>(length);
const Complex step{std::cos(angle), std::sin(angle)};
for (int start = 0; start < 128; start += length) {
Complex rotation{1.0, 0.0};
for (int offset = 0; offset < length / 2; ++offset) {
const Complex even = values[start + offset];
const Complex odd = mul(values[start + offset + length / 2], rotation);
values[start + offset] = {even.re + odd.re, even.im + odd.im};
values[start + offset + length / 2] = {
even.re - odd.re, even.im - odd.im};
rotation = mul(rotation, step);
}
}
}
}
void qmf_transform(const std::array<double, 64>& source,
std::array<Complex, 64>& target) const noexcept {
std::array<Complex, 128> work{};
for (int phase = 0; phase < 64; ++phase) {
work[phase] = scale(premod_[phase], source[phase]);
}
fft128(work);
for (int band = 0; band < 64; ++band) {
target[band] = mul(work[band], post_[band]);
}
}
void analyze_qmf(const double* input,
std::array<Complex, kChannels * kQmf>& output) noexcept {
for (int channel = 0; channel < kChannels; ++channel) {
for (int lag = 9; lag > 0; --lag) {
for (int phase = 0; phase < 64; ++phase) {
qmf_history_[qmf_history_index(lag, channel, phase)] =
qmf_history_[qmf_history_index(lag - 1, channel, phase)];
}
}
for (int phase = 0; phase < 64; ++phase) {
qmf_history_[qmf_history_index(0, channel, phase)] =
input[phase * kChannels + channel];
}
std::array<double, 64> even{};
std::array<double, 64> odd{};
for (int phase = 0; phase < 64; ++phase) {
for (int lag = 0; lag < 10; ++lag) {
const double value =
qmf_history_[qmf_history_index(lag, channel, phase)] *
qmf_analysis_[phase * 10 + lag];
(lag & 1 ? odd[phase] : even[phase]) += value;
}
}
std::array<Complex, 64> even_fft{};
std::array<Complex, 64> odd_fft{};
qmf_transform(even, even_fft);
qmf_transform(odd, odd_fft);
for (int band = 0; band < 64; ++band) {
output[channel * 64 + band] = add(
odd_fft[band], mul(even_fft[band], even_post_[band]));
}
}
}
void analyze_hybrid(
const std::array<Complex, kChannels * kQmf>& qmf,
std::array<Complex, kChannels * kHybrid>& output) noexcept {
for (int channel = 0; channel < kChannels; ++channel) {
for (int lag = 12; lag > 0; --lag) {
for (int band = 0; band < 3; ++band) {
hybrid_low_history_[hybrid_low_history_index(lag, channel, band)] =
hybrid_low_history_[hybrid_low_history_index(lag - 1, channel, band)];
}
}
for (int band = 0; band < 3; ++band) {
hybrid_low_history_[hybrid_low_history_index(0, channel, band)] =
qmf[channel * 64 + band];
}
for (int output_band = 0; output_band < 16; ++output_band) {
Complex value{};
for (int lag = 0; lag < 13; ++lag) {
for (int input_band = 0; input_band < 3; ++input_band) {
const Complex source = hybrid_low_history_[
hybrid_low_history_index(lag, channel, input_band)];
const double components[2]{source.re, source.im};
for (int input_component = 0; input_component < 2; ++input_component) {
value.re += components[input_component] * hybrid_low_[
hybrid_low_kernel_index(input_band, input_component, lag,
output_band, 0)];
value.im += components[input_component] * hybrid_low_[
hybrid_low_kernel_index(input_band, input_component, lag,
output_band, 1)];
}
}
}
output[channel * kHybrid + output_band] = value;
}
for (int band = 0; band < 61; ++band) {
output[channel * kHybrid + 16 + band] =
hybrid_high_history_[hybrid_high_history_index(0, channel, band)];
for (int delay = 0; delay < 5; ++delay) {
hybrid_high_history_[hybrid_high_history_index(delay, channel, band)] =
hybrid_high_history_[hybrid_high_history_index(delay + 1, channel, band)];
}
hybrid_high_history_[hybrid_high_history_index(5, channel, band)] =
qmf[channel * 64 + 3 + band];
}
}
}
std::array<Complex, kEars * kHybrid> process_room(
const std::array<Complex, kHybrid>& input) noexcept {
std::array<Complex, 64> filtered{};
for (int band = 0; band < 64; ++band) {
filtered[band] = scale(input[band], 0.70710677);
}
for (size_t stage = 0; stage < allpass_delays_.size(); ++stage) {
const uint32_t position = allpass_positions_[stage];
const double gain = allpass_gains_[stage];
for (int band = 0; band < 64; ++band) {
Complex& memory = allpass_memory_[
allpass_offsets_[stage] + static_cast<size_t>(position) * 64 + band];
const Complex residual = add(filtered[band], scale(memory, -gain));
filtered[band] = add(scale(residual, gain), memory);
memory = residual;
}
allpass_positions_[stage] = (position + 1) % allpass_delays_[stage];
}
std::array<Complex, 64 * 4> branches{};
std::array<Complex, 64 * 4> taps{};
for (int band = 0; band < 64; ++band) {
for (int branch = 0; branch < 4; ++branch) {
Complex value = filtered[band];
for (int source = 0; source < 4; ++source) {
const uint32_t position =
(room_position_ + room_capacity_ - fdn_delays_[source]) % room_capacity_;
value = add(value, scale(room_memory_[
room_memory_index(position, band, source)],
fdn_matrix_[branch * 4 + source]));
}
branches[band * 4 + branch] = value;
const uint32_t tap_position =
(room_position_ + room_capacity_ -
(output_tap_delay_ % room_capacity_)) % room_capacity_;
taps[band * 4 + branch] =
room_memory_[room_memory_index(tap_position, band, branch)];
}
}
for (int band = 0; band < 64; ++band) {
for (int branch = 0; branch < 4; ++branch) {
room_memory_[room_memory_index(room_position_, band, branch)] =
mul(branches[band * 4 + branch], feedback_[band][branch]);
}
}
room_position_ = (room_position_ + 1) % room_capacity_;
std::array<Complex, 64 * 4> extra{};
for (size_t index = 0; index < extra_delays_.size(); ++index) {
const uint32_t position =
(room_position_ + room_capacity_ -
((extra_delays_[index] + 1) % room_capacity_)) % room_capacity_;
for (int band = 0; band < 64; ++band) {
for (int target = 0; target < 4; ++target) {
Complex mixed{};
for (int source = 0; source < 4; ++source) {
mixed = add(mixed, scale(room_memory_[
room_memory_index(position, band, source)],
extra_matrices_[index * 16 + target * 4 + source]));
}
extra[band * 4 + target] = add(
extra[band * 4 + target],
mul(mixed, extra_fields_[index * 64 + band]));
}
}
}
std::array<Complex, kEars * kHybrid> output{};
for (int ear = 0; ear < 2; ++ear) {
for (int band = 0; band < 64; ++band) {
Complex value{};
for (int branch = 0; branch < 4; ++branch) {
const Complex signal = add(
scale(taps[band * 4 + branch], output_taps_[band][branch]),
extra[band * 4 + branch]);
value = add(value, mul(
signal, output_matrix_[ear][band][branch]));
}
output[ear * kHybrid + band] = value;
}
}
return output;
}
void synthesize_hybrid(
const std::array<Complex, kEars * kHybrid>& input,
std::array<Complex, kEars * kQmf>& output) const noexcept {
for (size_t mapping = 0; mapping < hybrid_values_.size(); ++mapping) {
const int16_t* index = hybrid_indices_.data() + mapping * 4;
const int input_band = index[0];
const int input_component = index[1];
const int output_band = index[2];
const int output_component = index[3];
const double gain = hybrid_values_[mapping];
for (int ear = 0; ear < 2; ++ear) {
const Complex source = input[ear * kHybrid + input_band];
Complex& target = output[ear * kQmf + output_band];
const double component = input_component == 0 ? source.re : source.im;
(output_component == 0 ? target.re : target.im) += component * gain;
}
}
}
void synthesize_qmf(const Complex* input, int ear,
std::array<double, 64>& output) noexcept {
std::array<double, 64 * kRank> features{};
std::array<double, 128> flat{};
for (int band = 0; band < 64; ++band) {
flat[band * 2] = input[band].re;
flat[band * 2 + 1] = input[band].im;
}
for (int phase = 0; phase < 64; ++phase) {
for (int rank = 0; rank < kRank; ++rank) {
double value = 0.0;
const size_t base = (static_cast<size_t>(phase) * kRank + rank) * 128;
for (int component = 0; component < 128; ++component) {
value += flat[component] * qmf_basis_[base + component];
}
features[phase * kRank + rank] = value;
}
}
for (int phase = 0; phase < 64; ++phase) {
double value = 0.0;
for (int lag = 0; lag < 10; ++lag) {
for (int rank = 0; rank < kRank; ++rank) {
const double feature = lag == 0
? features[phase * kRank + rank]
: synthesis_history_[synthesis_history_index(
ear, lag - 1, phase, rank)];
value += feature * qmf_taps_[
((static_cast<size_t>(phase) * 10 + lag) * kRank + rank)];
}
}
output[phase] = value;
}
for (int lag = 8; lag > 0; --lag) {
for (int phase = 0; phase < 64; ++phase) {
for (int rank = 0; rank < kRank; ++rank) {
synthesis_history_[synthesis_history_index(ear, lag, phase, rank)] =
synthesis_history_[synthesis_history_index(
ear, lag - 1, phase, rank)];
}
}
}
for (int phase = 0; phase < 64; ++phase) {
for (int rank = 0; rank < kRank; ++rank) {
synthesis_history_[synthesis_history_index(ear, 0, phase, rank)] =
features[phase * kRank + rank];
}
}
}
static size_t qmf_history_index(int lag, int channel, int phase) noexcept {
return (static_cast<size_t>(lag) * kChannels + channel) * 64 + phase;
}
static size_t hybrid_low_history_index(int lag, int channel, int band) noexcept {
return (static_cast<size_t>(lag) * kChannels + channel) * 3 + band;
}
static size_t hybrid_high_history_index(int delay, int channel, int band) noexcept {
return (static_cast<size_t>(delay) * kChannels + channel) * 61 + band;
}
static size_t hybrid_low_kernel_index(
int input_band, int input_component, int lag,
int output_band, int output_component) noexcept {
return (((static_cast<size_t>(input_band) * 2 + input_component) * 13 + lag) *
16 + output_band) * 2 + output_component;
}
size_t room_memory_index(uint32_t position, int band, int branch) const noexcept {
return (static_cast<size_t>(position) * 64 + band) * 4 + branch;
}
static size_t synthesis_history_index(
int ear, int lag, int phase, int rank) noexcept {
return (((static_cast<size_t>(ear) * 9 + lag) * 64 + phase) * kRank + rank);
}
bool kernels_ready_ = false;
bool room_ready_ = false;
std::array<double, 64 * 10> qmf_analysis_{};
std::vector<double> hybrid_low_;
std::vector<int16_t> hybrid_indices_;
std::vector<double> hybrid_values_;
std::array<double, 64 * kRank * 128> qmf_basis_{};
std::array<double, 64 * 10 * kRank> qmf_taps_{};
std::array<double, 10 * kChannels * 64> qmf_history_{};
std::array<Complex, 13 * kChannels * 3> hybrid_low_history_{};
std::array<Complex, 6 * kChannels * 61> hybrid_high_history_{};
std::array<double, kEars * 9 * 64 * kRank> synthesis_history_{};
uint32_t room_bands_ = 0;
std::vector<uint32_t> allpass_delays_;
std::vector<double> allpass_gains_;
std::vector<size_t> allpass_offsets_;
std::vector<uint32_t> allpass_positions_;
std::vector<Complex> allpass_memory_;
std::array<uint32_t, 4> fdn_delays_{};
std::array<double, 16> fdn_matrix_{};
uint32_t room_capacity_ = 0;
uint32_t output_tap_delay_ = 0;
std::array<std::array<Complex, 4>, 64> feedback_{};
std::array<std::array<double, 4>, 64> output_taps_{};
std::array<std::array<std::array<Complex, 4>, 64>, 2> output_matrix_{};
std::vector<Complex> room_memory_;
uint32_t room_position_ = 0;
std::vector<uint32_t> extra_delays_;
std::vector<Complex> extra_fields_;
std::vector<double> extra_matrices_;
std::array<uint8_t, 128> bit_reverse_{};
std::array<Complex, 64> premod_{};
std::array<Complex, 64> post_{};
std::array<Complex, 64> even_post_{};
char error_[256]{};
};
} // namespace ejoc::binaural
extern "C" {
ejoc_binaural_renderer_handle EJOC_CALL ejoc_binaural_renderer_create(void) {
return new (std::nothrow) ejoc::binaural::Renderer();
}
void EJOC_CALL ejoc_binaural_renderer_destroy(ejoc_binaural_renderer_handle handle) {
delete static_cast<ejoc::binaural::Renderer*>(handle);
}
int EJOC_CALL ejoc_binaural_renderer_reset(ejoc_binaural_renderer_handle handle) {
return handle ? static_cast<ejoc::binaural::Renderer*>(handle)->reset() : -1;
}
const char* EJOC_CALL ejoc_binaural_renderer_last_error(
ejoc_binaural_renderer_handle handle) {
return handle ? static_cast<ejoc::binaural::Renderer*>(handle)->error()
: "null binaural renderer handle";
}
int EJOC_CALL ejoc_binaural_renderer_configure_kernels(
ejoc_binaural_renderer_handle handle,
const double* qmf_analysis,
const double* hybrid_low,
const int16_t* hybrid_indices,
const double* hybrid_values,
uint32_t hybrid_count,
const double* qmf_basis,
const double* qmf_taps) {
return handle ? static_cast<ejoc::binaural::Renderer*>(handle)->configure_kernels(
qmf_analysis, hybrid_low, hybrid_indices, hybrid_values,
hybrid_count, qmf_basis, qmf_taps) : -1;
}
int EJOC_CALL ejoc_binaural_renderer_configure_room(
ejoc_binaural_renderer_handle handle,
uint32_t bands,
uint32_t allpass_count,
const uint32_t* allpass_delays,
const double* allpass_gains,
const uint32_t* fdn_delays,
const double* fdn_matrix,
uint32_t output_tap_delay,
const double* feedback_complex,
const double* output_taps,
const double* output_complex,
uint32_t extra_count,
const uint32_t* extra_delays,
const double* extra_fields_complex,
const double* extra_matrices) {
return handle ? static_cast<ejoc::binaural::Renderer*>(handle)->configure_room(
bands, allpass_count, allpass_delays, allpass_gains,
fdn_delays, fdn_matrix, output_tap_delay,
feedback_complex, output_taps, output_complex,
extra_count, extra_delays, extra_fields_complex, extra_matrices) : -1;
}
int EJOC_CALL ejoc_binaural_renderer_process(
ejoc_binaural_renderer_handle handle,
const double* input16_interleaved,
const double* gains_complex,
const double* room_sends,
double output_gain,
double* output_stereo_interleaved) {
return handle ? static_cast<ejoc::binaural::Renderer*>(handle)->process(
input16_interleaved, gains_complex, room_sends,
output_gain, output_stereo_interleaved) : -1;
}
} // extern "C"
+251
View File
@@ -0,0 +1,251 @@
#include "binaural/binaural_runtime.h"
#include <algorithm>
#include <cmath>
#include <cstring>
#include <string>
namespace joc::binaural {
namespace {
double max_delay_bound(const hrtf::Field& field) {
double maximum = 0.0;
for (const double value : field.delay_bounds) {
maximum = std::max(maximum, value);
}
return maximum;
}
} // namespace
bool profile_from_name(const char* name, Profile* out) {
if (name == nullptr || out == nullptr) {
return false;
}
if (std::strcmp(name, "near") == 0) { *out = Profile::Near; return true; }
if (std::strcmp(name, "mid") == 0) { *out = Profile::Mid; return true; }
if (std::strcmp(name, "far") == 0) { *out = Profile::Far; return true; }
return false;
}
SofaBinauralRuntime::~SofaBinauralRuntime() {
if (handle_ != nullptr) {
ejoc_sofa_binaural_destroy(handle_);
handle_ = nullptr;
}
}
Status SofaBinauralRuntime::open(const hrtf::Field& field, const hrtf::Kernels& kernels,
Profile profile, const RoomConstants& room) {
if (handle_ != nullptr) {
return Status::fail(JOC_ERR_STATE, stage::kRender, "binaural runtime already open");
}
if (field.coefficients.size() != static_cast<std::size_t>(hrtf::kShTerms * hrtf::kEars *
hrtf::kHybridBands * 2) ||
field.band_centers_hz.size() != static_cast<std::size_t>(hrtf::kHybridBands)) {
return Status::fail(JOC_ERR_HRTF_FORMAT, stage::kRender,
"compiled HRTF field has unexpected array sizes");
}
handle_ = ejoc_sofa_binaural_create();
if (handle_ == nullptr) {
return Status::fail(JOC_ERR_OUT_OF_MEMORY, stage::kRender,
"ejoc_sofa_binaural_create failed");
}
profile_ = profile;
if (ejoc_sofa_binaural_configure_kernels(
handle_, kernels.qmf_analysis.data(), kernels.hybrid_low.data(),
kernels.hybrid_indices.data(), kernels.hybrid_values.data(), kernels.hybrid_count,
kernels.qmf_basis.data(), kernels.qmf_taps.data()) != 0) {
const char* message = ejoc_sofa_binaural_last_error(handle_);
return Status::fail(JOC_ERR_HRTF_FORMAT, stage::kRender,
std::string("configure_kernels failed: ") +
(message != nullptr ? message : "unknown"));
}
if (ejoc_sofa_binaural_configure_field(handle_, field.coefficients.data(),
field.delay_coefficients.data(),
field.delay_bounds.data(), field.band_centers_hz.data(),
field.measurement_radius_m) != 0) {
const char* message = ejoc_sofa_binaural_last_error(handle_);
return Status::fail(JOC_ERR_HRTF_FORMAT, stage::kRender,
std::string("configure_field failed: ") +
(message != nullptr ? message : "unknown"));
}
if (ejoc_sofa_binaural_configure_room(
handle_, room.dims, room.listener, room.walls, room.speed_of_sound, room.fdn_delays,
room.fdn_feedback, room.damping, room.fdn_output_gain, room.allpass_delays,
room.allpass_gains, room.enable_early_reflections, room.enable_late_room) != 0) {
const char* message = ejoc_sofa_binaural_last_error(handle_);
return Status::fail(JOC_ERR_RENDER_FAILED, stage::kRender,
std::string("configure_room failed: ") +
(message != nullptr ? message : "unknown"));
}
maximum_hrtf_delay_ =
static_cast<std::int64_t>(std::ceil(max_delay_bound(field) - 1e-9));
hrtf_history_slots_ = static_cast<std::uint32_t>(std::max<std::int64_t>(1, (maximum_hrtf_delay_ + 63) / 64));
staging_.assign(kBlockSamples * kSourceCount, 0.0);
block_output_.assign(kBlockSamples * 2u, 0.0);
output_.clear();
staged_ = 0;
input_samples_ = 0;
processed_samples_ = 0;
blocks_processed_ = 0;
return Status::success();
}
Status SofaBinauralRuntime::process_block() {
double positions[timeline::kTimelineObjects][3] = {};
const Status queried = timeline_.positions_at(static_cast<std::int64_t>(processed_samples_),
positions);
if (!queried.ok()) {
return queried;
}
// The reference adapter calls set_source without a `fade` argument, so the
// backend default (fade enabled) applies - the per-object path crossfade is
// part of the reference behaviour, not an optional extra.
constexpr std::uint32_t kFade = 1u;
if (ejoc_sofa_binaural_set_source(handle_, 0u, kLfePosition,
static_cast<std::uint32_t>(profile_), 1.0, 1u, 1u,
kFade) != 0) {
const char* message = ejoc_sofa_binaural_last_error(handle_);
return Status::fail(JOC_ERR_RENDER_FAILED, stage::kRender,
std::string("set_source(LFE) failed: ") +
(message != nullptr ? message : "unknown"));
}
for (std::uint32_t source = 0; source < timeline::kTimelineObjects; ++source) {
if (ejoc_sofa_binaural_set_source(handle_, source + 1u, positions[source],
static_cast<std::uint32_t>(profile_), 1.0, 1u, 0u,
kFade) != 0) {
const char* message = ejoc_sofa_binaural_last_error(handle_);
return Status::fail(JOC_ERR_RENDER_FAILED, stage::kRender,
std::string("set_source(object ") + std::to_string(source + 1u) +
") failed: " + (message != nullptr ? message : "unknown"));
}
}
const int trimmed = ejoc_sofa_binaural_process(handle_, staging_.data(), kBlockSamples, 1.0,
block_output_.data());
if (trimmed < 0) {
const char* message = ejoc_sofa_binaural_last_error(handle_);
return Status::fail(JOC_ERR_RENDER_FAILED, stage::kRender,
std::string("sofa process failed: ") +
(message != nullptr ? message : "unknown"));
}
if (trimmed > 0) {
output_.insert(output_.end(), block_output_.begin(),
block_output_.begin() + static_cast<std::ptrdiff_t>(trimmed) * 2);
}
staged_ = 0;
processed_samples_ += kBlockSamples;
++blocks_processed_;
return Status::success();
}
Status SofaBinauralRuntime::submit_frame(const float* objects16_planar,
const oamd::OamdUpdate* update, std::int64_t frame_index,
std::int64_t outer_sample_offset,
std::int64_t object_delay_samples) {
if (handle_ == nullptr) {
return Status::fail(JOC_ERR_STATE, stage::kRender, "binaural runtime is not open");
}
if (objects16_planar == nullptr) {
return Status::fail(JOC_ERR_INVALID_ARGUMENT, stage::kRender, "null frame");
}
// A frame without an ID11 payload submits nothing at all (the reference only
if (update != nullptr) {
const Status submitted = timeline_.submit_update(
*update, frame_index * JOC_FRAME_SAMPLES, outer_sample_offset, object_delay_samples,
static_cast<std::int64_t>(input_samples_));
if (!submitted.ok()) {
return submitted;
}
}
std::size_t offset = 0;
while (offset < JOC_FRAME_SAMPLES) {
const std::size_t room = kBlockSamples - staged_;
const std::size_t count = std::min<std::size_t>(room, JOC_FRAME_SAMPLES - offset);
for (std::size_t sample = 0; sample < count; ++sample) {
double* row = staging_.data() + (staged_ + sample) * kSourceCount;
for (std::size_t channel = 0; channel < kSourceCount; ++channel) {
row[channel] = static_cast<double>(
objects16_planar[channel * JOC_FRAME_SAMPLES + offset + sample]);
}
}
staged_ += count;
offset += count;
if (staged_ == kBlockSamples) {
const Status status = process_block();
if (!status.ok()) {
return status;
}
}
}
input_samples_ += JOC_FRAME_SAMPLES;
return Status::success();
}
std::uint32_t SofaBinauralRuntime::finish_capacity(double tail_seconds) const {
std::int64_t requested = tail_samples_;
if (tail_seconds >= 0.0) {
requested = static_cast<std::int64_t>(std::ceil(tail_seconds * 48000.0 - 1e-9));
}
const std::int64_t hrtf_bound = static_cast<std::int64_t>(hrtf_history_slots_) * 64;
const std::int64_t early_bound = hrtf_bound + 2048 + 256 * 64;
std::int64_t drain = std::max(requested, early_bound) + 961;
drain = ((drain + 63) / 64) * 64;
return static_cast<std::uint32_t>(drain);
}
Status SofaBinauralRuntime::reset() {
if (handle_ == nullptr) {
return Status::fail(JOC_ERR_STATE, stage::kRender, "binaural runtime is not open");
}
if (ejoc_sofa_binaural_reset(handle_) != 0) {
return Status::fail(JOC_ERR_RENDER_FAILED, stage::kRender, "sofa reset failed");
}
timeline_ = timeline::OamdPositionTimeline();
output_.clear();
staged_ = 0;
input_samples_ = 0;
processed_samples_ = 0;
blocks_processed_ = 0;
return Status::success();
}
Status SofaBinauralRuntime::finish(std::uint32_t flush_samples, std::vector<double>* out) {
if (handle_ == nullptr) {
return Status::fail(JOC_ERR_STATE, stage::kRender, "binaural runtime is not open");
}
if (out == nullptr) {
return Status::fail(JOC_ERR_INVALID_ARGUMENT, stage::kRender, "null output");
}
out->clear();
if (flush_samples == 0u) {
return Status::success();
}
std::vector<double> chunk(static_cast<std::size_t>(kBlockSamples) * 2u, 0.0);
std::uint32_t produced_total = 0;
std::uint32_t remaining = flush_samples;
while (remaining > 0) {
const std::uint32_t request = std::min<std::uint32_t>(remaining, kBlockSamples);
const int produced = ejoc_sofa_binaural_finish(handle_, request, chunk.data(), request);
if (produced < 0) {
const char* message = ejoc_sofa_binaural_last_error(handle_);
return Status::fail(JOC_ERR_RENDER_FAILED, stage::kRender,
std::string("sofa finish failed: ") +
(message != nullptr ? message : "unknown"));
}
if (produced == 0) {
break;
}
out->insert(out->end(), chunk.begin(),
chunk.begin() + static_cast<std::ptrdiff_t>(produced) * 2);
produced_total += static_cast<std::uint32_t>(produced);
remaining -= std::min<std::uint32_t>(remaining, static_cast<std::uint32_t>(produced));
}
return Status::success();
}
} // namespace joc::binaural
+96
View File
@@ -0,0 +1,96 @@
#pragma once
#include <cstdint>
#include <vector>
#include "eac3joc_core.h"
#include "foundation/status.h"
#include "hrtf/jochrtf.h"
#include "oamd/oamd_parser.h"
#include "timeline/position_timeline.h"
namespace joc::binaural {
// ADM direction of the LFE source, as the reference passes it.
inline constexpr double kLfePosition[3] = {0.0, 1.0, 0.0};
inline constexpr int kSourceCount = 16;
inline constexpr std::uint32_t kBlockSamples = 512;
// Room constants the reference's native bridge passes for the default shoebox.
struct RoomConstants {
double dims[3] = {18.0, 18.0, 14.0};
double listener[3] = {9.0, 9.0, 7.0};
double walls[6] = {0.62, 0.60, 0.58, 0.61, 0.52, 0.56};
double speed_of_sound = 343.3;
std::uint32_t fdn_delays[4] = {1427u, 1783u, 1973u, 2099u};
double fdn_feedback[4] = {0.7853685923259284, 0.7394299865898056, 0.7160221718631921,
0.7009092068085467};
double damping = 0.32;
double fdn_output_gain = 0.22;
std::uint32_t allpass_delays[2] = {113u, 331u};
double allpass_gains[2] = {0.63, 0.51};
std::uint32_t enable_early_reflections = 1;
std::uint32_t enable_late_room = 1;
};
enum class Profile : std::uint32_t { Near = 0, Mid = 1, Far = 2 };
bool profile_from_name(const char* name, Profile* out);
class SofaBinauralRuntime {
public:
SofaBinauralRuntime() = default;
~SofaBinauralRuntime();
SofaBinauralRuntime(const SofaBinauralRuntime&) = delete;
SofaBinauralRuntime& operator=(const SofaBinauralRuntime&) = delete;
Status open(const hrtf::Field& field, const hrtf::Kernels& kernels, Profile profile,
const RoomConstants& room = RoomConstants{});
Status submit_frame(const float* objects16_planar, const oamd::OamdUpdate* update,
std::int64_t frame_index, std::int64_t outer_sample_offset,
std::int64_t object_delay_samples);
// Resets the kernel, the timeline and the counters (plan 31.2).
Status reset();
// Drains the room tail. `flush_samples` is the drain length; the reference
Status finish(std::uint32_t flush_samples, std::vector<double>* out);
// Program output (input minus the 961-sample kernel latency), interleaved.
const std::vector<double>& output() const { return output_; }
void take_output(std::vector<double>* out) {
out->swap(output_);
output_.clear();
}
std::uint64_t input_samples() const { return input_samples_; }
std::uint64_t blocks_processed() const { return blocks_processed_; }
std::size_t staged_samples() const { return staged_; }
const timeline::OamdPositionTimeline& timeline() const { return timeline_; }
// finish_output_capacity as the reference computes it (plan 21.4).
std::uint32_t finish_capacity(double tail_seconds) const;
private:
Status process_block();
ejoc_sofa_binaural_handle handle_ = nullptr;
Profile profile_ = Profile::Mid;
timeline::OamdPositionTimeline timeline_;
std::vector<double> staging_;
std::size_t staged_ = 0;
std::vector<double> block_output_;
std::vector<double> output_;
std::uint64_t input_samples_ = 0;
std::uint64_t processed_samples_ = 0;
std::uint64_t blocks_processed_ = 0;
std::int64_t maximum_hrtf_delay_ = 0;
std::uint32_t hrtf_history_slots_ = 1;
std::uint32_t tail_samples_ = 61200;
};
} // namespace joc::binaural
File diff suppressed because it is too large Load Diff
+795
View File
@@ -0,0 +1,795 @@
// joc_cli -- command line frontend, argument-compatible with the reference
// Python CLI (main.py): the same positional input, the same mode selection
// (ADM BWF by default, --speaker-layout or --binaural), the same option names,
// choices and defaults, and the same default output naming under output/.
//
// Options that exist only because this build has no Python side or no Rosella
// import chain (--backend python, --sofa-hrtf, --personalized-headphone,
// metadata sidecars) fail with an explicit message instead of being ignored.
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <stdexcept>
#include <string>
#include <vector>
#include "foundation/fs_utf8.h"
#include "joc_core.h"
namespace {
namespace fs = std::filesystem;
namespace fs_utf8 = joc::fs_utf8;
constexpr double kRate = 48000.0;
constexpr int kFrameSamples = 1536;
struct Options {
std::string input;
std::string output;
std::string speaker_output;
std::string binaural_output;
std::string speaker_layout;
bool binaural = false;
std::string speaker_format = "float32";
std::string binaural_format = "float32";
std::string clip_action = "ask";
int speaker_metadata_offset = 1473;
std::string binaural_mode = "mid";
std::string sofa_hrtf;
std::string compiled_hrtf_cache;
std::string personalized_headphone;
bool personalized_headphone_used = false;
std::string hrtf_cache_policy;
std::string hrtf_cache_dir;
double hrtf_radius_m = 1.0;
double binaural_tail_seconds = 5.0;
double binaural_tail_threshold = 1.0e-8;
int binaural_chunk_frames = 64;
double gain_db = 0.0;
double duration = 0.0;
bool duration_set = false;
int object_delay_samples = 1473;
std::string trajectory_mode = "compact";
std::string ffmpeg;
double eac3_drc_scale = 0.0;
int eac3_target_level = 0;
std::string backend = "auto";
std::string native_library;
int native_threads = 0;
bool native_threads_set = false;
std::string metadata_dir;
std::string metadata_cache;
std::string metadata_backend = "auto";
std::string print_metadata = "none";
std::string metadata_json;
bool metadata_only = false;
bool keep_raw = false;
bool skip_sha256 = false;
int progress_every = 1000;
// C++-side additions (documented as such; the Python CLI has no equivalent).
std::string bed;
std::string kernels;
std::string work_dir;
std::string report_json;
bool report_json_set = false;
bool dry_run = false;
bool quiet = false;
bool help = false;
};
const char* kLayoutChoices =
"2.0 3.0 3.1 4.0 5.0 5.1 5.1.2 5.1.4 6.1 7.0 7.1 7.1.2 7.1.4 9.1.4 9.1.6 22.2";
void print_usage() {
std::printf(
"usage: joc_cli [options] input\n"
"\n"
"JustOneCacophony (JOC):E-AC-3 JOC → 25ch ADM BWF、扬声器 WAV 或双耳 WAV\n"
"\n"
"位置参数:\n"
" input 输入 .m4a/.eac3/.ec3\n"
"\n"
"模式(默认输出 ADM BWF):\n"
" --speaker-layout L 直接扬声器渲染布局,例如 2.0、5.1、7.1.2\n"
" 可选值: %s\n"
" --binaural 直接双耳渲染;不生成临时 ADM BWF\n"
"\n"
"输出:\n"
" -o, --output PATH 输出文件;默认 output/<名称>.adm.wav、\n"
" output/<名称>.<布局>.wav 或 output/<名称>.binaural.wav\n"
" --speaker-output PATH 扬声器 WAV 路径;仅与 --speaker-layout 一起使用\n"
" --binaural-output PATH 双耳 WAV 路径;仅与 --binaural 一起使用\n"
" --speaker-format F 扬声器 WAV 格式 float32|int24,默认 float32\n"
" --binaural-format F 双耳 WAV 格式 float32|int24,默认 float32\n"
" --clip-action A int24 削波处理 ask|continue|float32|abort,默认 ask\n"
"\n"
"渲染:\n"
" --speaker-metadata-offset N 扬声器渲染 metadata 相对帧偏移,默认 1473 samples\n"
" --binaural-mode M 双耳渲染模式 off|near|mid|far,默认 mid;\n"
" off 仅用于 ADM BWF(关闭 DBMD 双耳提示)\n"
" --sofa-hrtf PATH SOFA SimpleFreeFieldHRIR 输入;.jochrtf 由本工具内部编译\n"
" --personalized-headphone [PATH] Rosella 个性化模型,默认 "
"HRTF/binaural.personalized_headphone\n"
" --compiled-hrtf-cache PATH 直接读取 .jochrtf(高级用法,跳过 SOFA 编译)\n"
" --hrtf-cache-policy P SOFA 编译缓存策略 none|memory|disk,默认 memory\n"
" --hrtf-cache-dir DIR disk cache 目录,默认 <exe>/output/hrtf-cache\n"
" --hrtf-radius-m R 选择最近的 SOFA measurement-radius shell,默认 1.0 m\n"
" --binaural-tail-seconds S 双耳 room/filterbank flush 上限,默认 5 秒\n"
" --binaural-tail-threshold T 双耳尾声裁切阈值,默认 1e-8;主体至少保留原时长\n"
" --binaural-chunk-frames N 双耳内部批处理帧数,默认 64(本构建按 512 块渲染,\n"
" 取值不影响输出)\n"
" --gain-db X 成品增益 dB,默认 0;双耳路径以 float64 应用\n"
" --duration S 只处理开头指定秒数\n"
" --object-delay-samples N 对象 PCM/OAMD 时间补偿,默认 1473 samples\n"
" --trajectory-mode M ADM 对象轨迹表示 compact|dense64,默认 compact\n"
"\n"
"输入与解码:\n"
" --ffmpeg PATH ffmpeg 可执行文件,默认取 FFMPEG 环境变量或 PATH\n"
" --eac3-drc-scale X E-AC-3 解码器 -drc_scale,0=关闭码流 dynrng,默认 0\n"
" --eac3-target-level N E-AC-3 解码器 -target_level,0=不施加,默认 0\n"
" --backend B JOC/扬声器 DSP 后端 auto|native;本构建无 python 后端\n"
" --native-threads N 原生 DSP 总线程数;默认在 4 核以上使用 2\n"
"\n"
"诊断:\n"
" --print-metadata M 诊断元数据输出 none|summary|frames,默认 none\n"
" --metadata-json PATH 元数据汇总 JSON 路径\n"
" --metadata-only 解析/打印元数据后退出\n"
" --keep-raw 额外保留 16ch f32le 对象中间文件\n"
" --skip-sha256 跳过最终文件 SHA-256 全量复扫\n"
" --progress-every N 进度输出间隔,默认 1000 帧(渲染与收尾写盘同一节奏)\n"
"\n"
"本构建特有(Python 版没有对应参数):\n"
" --bed PATH 已解码的 6 通道 float32 PCM;给出后不调用 ffmpeg 解码\n"
" --kernels PATH 双耳滤波器组表 rosella_kernels.npz\n"
" --work-dir DIR 临时目录\n"
" --report-json PATH 结果 JSON 路径;默认 <输出>.report.json\n"
" --dry-run 只校验配置\n"
" --quiet 只输出警告与错误\n",
kLayoutChoices);
}
[[noreturn]] void fail(const std::string& message) { throw std::runtime_error(message); }
std::string require_value(const std::vector<std::string>& arguments, int* index) {
if (static_cast<std::size_t>(*index) + 1u >= arguments.size()) {
fail("argument " + arguments[static_cast<std::size_t>(*index)] +
": expected one argument");
}
return arguments[static_cast<std::size_t>(++(*index))];
}
double to_double(const std::string& text, const char* name) {
try {
std::size_t used = 0;
const double value = std::stod(text, &used);
if (used != text.size()) {
fail(std::string(name) + ": invalid float value: " + text);
}
return value;
} catch (const std::exception&) {
fail(std::string(name) + ": invalid float value: " + text);
}
}
long long to_int(const std::string& text, const char* name) {
try {
std::size_t used = 0;
const long long value = std::stoll(text, &used);
if (used != text.size()) {
fail(std::string(name) + ": invalid int value: " + text);
}
return value;
} catch (const std::exception&) {
fail(std::string(name) + ": invalid int value: " + text);
}
}
void check_choice(const std::string& value, const char* name,
std::initializer_list<const char*> allowed) {
for (const char* candidate : allowed) {
if (value == candidate) {
return;
}
}
std::string list;
for (const char* candidate : allowed) {
list += list.empty() ? candidate : (", " + std::string(candidate));
}
fail(std::string(name) + ": invalid choice: '" + value + "' (choose from " + list + ")");
}
void parse_args(const std::vector<std::string>& arguments, Options* options) {
std::vector<std::string> positional;
const int argc = static_cast<int>(arguments.size());
for (int index = 1; index < argc; ++index) {
const std::string arg = arguments[static_cast<std::size_t>(index)];
if (arg == "-h" || arg == "--help") { options->help = true; }
else if (arg == "-o" || arg == "--output") { options->output = require_value(arguments, &index); }
else if (arg == "--speaker-output") { options->speaker_output = require_value(arguments, &index); }
else if (arg == "--binaural-output") { options->binaural_output = require_value(arguments, &index); }
else if (arg == "--speaker-layout") { options->speaker_layout = require_value(arguments, &index); }
else if (arg == "--binaural") { options->binaural = true; }
else if (arg == "--speaker-format") { options->speaker_format = require_value(arguments, &index); }
else if (arg == "--binaural-format") { options->binaural_format = require_value(arguments, &index); }
else if (arg == "--clip-action") { options->clip_action = require_value(arguments, &index); }
else if (arg == "--speaker-metadata-offset") {
options->speaker_metadata_offset = static_cast<int>(
to_int(require_value(arguments, &index), "--speaker-metadata-offset"));
}
else if (arg == "--binaural-mode") { options->binaural_mode = require_value(arguments, &index); }
else if (arg == "--sofa-hrtf") { options->sofa_hrtf = require_value(arguments, &index); }
else if (arg == "--compiled-hrtf-cache") { options->compiled_hrtf_cache = require_value(arguments, &index); }
else if (arg == "--personalized-headphone") {
options->personalized_headphone_used = true;
// nargs="?": the path is optional, so the next token may be the input.
// Without a path the executable-anchored default is resolved later.
if (static_cast<std::size_t>(index) + 1u < arguments.size() &&
arguments[static_cast<std::size_t>(index) + 1u][0] != '-') {
options->personalized_headphone = require_value(arguments, &index);
}
}
else if (arg == "--hrtf-cache-policy") { options->hrtf_cache_policy = require_value(arguments, &index); }
else if (arg == "--hrtf-cache-dir") { options->hrtf_cache_dir = require_value(arguments, &index); }
else if (arg == "--hrtf-radius-m") { options->hrtf_radius_m = to_double(require_value(arguments, &index), "--hrtf-radius-m"); }
else if (arg == "--binaural-tail-seconds") { options->binaural_tail_seconds = to_double(require_value(arguments, &index), "--binaural-tail-seconds"); }
else if (arg == "--binaural-tail-threshold") { options->binaural_tail_threshold = to_double(require_value(arguments, &index), "--binaural-tail-threshold"); }
else if (arg == "--binaural-chunk-frames") { options->binaural_chunk_frames = static_cast<int>(to_int(require_value(arguments, &index), "--binaural-chunk-frames")); }
else if (arg == "--gain-db") { options->gain_db = to_double(require_value(arguments, &index), "--gain-db"); }
else if (arg == "--duration") { options->duration = to_double(require_value(arguments, &index), "--duration"); options->duration_set = true; }
else if (arg == "--object-delay-samples") { options->object_delay_samples = static_cast<int>(to_int(require_value(arguments, &index), "--object-delay-samples")); }
else if (arg == "--trajectory-mode") { options->trajectory_mode = require_value(arguments, &index); }
else if (arg == "--ffmpeg") { options->ffmpeg = require_value(arguments, &index); }
else if (arg == "--eac3-drc-scale") { options->eac3_drc_scale = to_double(require_value(arguments, &index), "--eac3-drc-scale"); }
else if (arg == "--eac3-target-level") { options->eac3_target_level = static_cast<int>(to_int(require_value(arguments, &index), "--eac3-target-level")); }
else if (arg == "--backend") { options->backend = require_value(arguments, &index); }
else if (arg == "--native-library") { options->native_library = require_value(arguments, &index); }
else if (arg == "--native-threads") { options->native_threads = static_cast<int>(to_int(require_value(arguments, &index), "--native-threads")); options->native_threads_set = true; }
else if (arg == "--metadata-dir") { options->metadata_dir = require_value(arguments, &index); }
else if (arg == "--metadata-cache") { options->metadata_cache = require_value(arguments, &index); }
else if (arg == "--metadata-backend") { options->metadata_backend = require_value(arguments, &index); }
else if (arg == "--print-metadata") { options->print_metadata = require_value(arguments, &index); }
else if (arg == "--metadata-json") { options->metadata_json = require_value(arguments, &index); }
else if (arg == "--metadata-only") { options->metadata_only = true; }
else if (arg == "--keep-raw") { options->keep_raw = true; }
else if (arg == "--skip-sha256") { options->skip_sha256 = true; }
else if (arg == "--progress-every") { options->progress_every = static_cast<int>(to_int(require_value(arguments, &index), "--progress-every")); }
else if (arg == "--bed") { options->bed = require_value(arguments, &index); }
else if (arg == "--kernels") { options->kernels = require_value(arguments, &index); }
else if (arg == "--work-dir") { options->work_dir = require_value(arguments, &index); }
else if (arg == "--report-json") { options->report_json = require_value(arguments, &index); options->report_json_set = true; }
else if (arg == "--dry-run") { options->dry_run = true; }
else if (arg == "--quiet") { options->quiet = true; }
else if (!arg.empty() && arg[0] == '-' && arg != "-") { fail("unrecognized argument: " + arg); }
else { positional.push_back(arg); }
}
if (positional.size() > 1u) {
fail("unrecognized extra arguments: " + positional[1] +
(positional.size() > 2u ? " ..." : ""));
}
if (!positional.empty()) {
options->input = positional.front();
}
}
// Mirrors the reference resolve_output(): <project>/output plus a mode-specific
// name. The project directory is the executable's directory, as upstream uses
// the script's directory, so the layout does not depend on the working directory.
std::string resolve_output(const Options& options, const std::string& source,
const std::string& executable_directory) {
const std::string requested = !options.speaker_output.empty() ? options.speaker_output
: !options.binaural_output.empty() ? options.binaural_output
: options.output;
if (!requested.empty()) {
std::error_code error;
const fs::path absolute = fs::absolute(fs_utf8::to_path(requested), error);
return error ? requested : fs_utf8::from_path(absolute);
}
const fs::path directory = fs_utf8::to_path(executable_directory) / "output";
const std::string stem = fs_utf8::from_path(fs_utf8::to_path(source).stem());
if (!options.speaker_layout.empty()) {
return fs_utf8::from_path(directory /
fs_utf8::to_path(stem + "." + options.speaker_layout + ".wav"));
}
if (options.binaural) {
return fs_utf8::from_path(directory / fs_utf8::to_path(stem + ".binaural.wav"));
}
return fs_utf8::from_path(directory / fs_utf8::to_path(stem + ".adm.wav"));
}
// The project directory the reference anchors its defaults at: the directory of
// the running executable, never the working directory.
std::string executable_dir(const std::string& argv0) {
const std::string own_path = fs_utf8::executable_path();
if (!own_path.empty()) {
const fs::path path = fs_utf8::to_path(own_path);
if (path.has_parent_path()) {
return fs_utf8::from_path(path.parent_path());
}
}
if (argv0.empty()) {
return ".";
}
std::error_code error;
const fs::path path = fs::absolute(fs_utf8::to_path(argv0), error);
if (error || path.empty()) {
return ".";
}
return fs_utf8::from_path(path.parent_path());
}
std::string find_kernels(const Options& options, const std::string& argv0) {
(void)argv0;
if (!options.kernels.empty() && !fs_utf8::exists(options.kernels)) {
fail("--kernels 指向的文件不存在: " + options.kernels);
}
// Empty means the tables compiled into the library.
return options.kernels;
}
// Mirrors the reference binaural HRTF resolution (main.py:89-160): the SOFA file
// is the user-facing input and the .jochrtf is only its compiled cache. Paths
// are anchored at the executable directory, as the reference anchors them at the
// project directory.
struct HrtfInput {
std::string sofa_path; // compile this
std::string compiled_path; // or read this .jochrtf directly
std::string cache_dir; // disk policy directory
std::string personalized_path; // Rosella .personalized_headphone
bool disk = false;
};
std::string resolve_compiled_hrtf(const Options& options, const std::string& project_directory) {
if (!options.compiled_hrtf_cache.empty()) {
return options.compiled_hrtf_cache;
}
const std::string directory_utf8 =
options.hrtf_cache_dir.empty()
? fs_utf8::from_path(fs_utf8::to_path(project_directory) / "output" / "hrtf-cache")
: options.hrtf_cache_dir;
if (!fs_utf8::is_directory(directory_utf8)) {
return std::string();
}
const fs::path directory = fs_utf8::to_path(directory_utf8);
std::vector<fs::path> candidates;
for (const fs::directory_entry& entry : fs::directory_iterator(directory)) {
if (entry.is_regular_file() && entry.path().extension() == ".jochrtf") {
candidates.push_back(entry.path());
}
}
std::sort(candidates.begin(), candidates.end());
if (candidates.size() > 1u) {
fail(directory_utf8 +
" 下有多个 .jochrtf 缓存,无法自动选择;请用 --sofa-hrtf PATH 或 "
"--compiled-hrtf-cache PATH 显式指定");
}
return candidates.empty() ? std::string() : fs_utf8::from_path(candidates.front());
}
HrtfInput resolve_hrtf_input(const Options& options, const std::string& project_directory) {
HrtfInput input;
const std::string default_sofa =
fs_utf8::from_path(fs_utf8::to_path(project_directory) / "HRTF" / "binaural.sofa");
const std::string default_private = fs_utf8::from_path(
fs_utf8::to_path(project_directory) / "HRTF" / "binaural.personalized_headphone");
const std::string default_cache_dir =
fs_utf8::from_path(fs_utf8::to_path(project_directory) / "output" / "hrtf-cache");
if (!options.compiled_hrtf_cache.empty() && !options.hrtf_cache_policy.empty()) {
fail("显式 .jochrtf 输入不能再指定 --hrtf-cache-policy");
}
if (!options.compiled_hrtf_cache.empty() && options.hrtf_radius_m != 1.0) {
fail("显式 .jochrtf 输入不能再选择 SOFA radius shell");
}
if (options.personalized_headphone_used &&
(!options.hrtf_cache_policy.empty() || !options.hrtf_cache_dir.empty() ||
options.hrtf_radius_m != 1.0)) {
fail("Rosella 模型输入不能使用 --hrtf-cache-policy/--hrtf-cache-dir/--hrtf-radius-m");
}
std::string sofa = options.sofa_hrtf;
std::string compiled = options.compiled_hrtf_cache;
std::string personalized =
options.personalized_headphone_used ? options.personalized_headphone : std::string();
if (options.personalized_headphone_used && personalized.empty()) {
// "--personalized-headphone" without a path means the project default.
personalized = default_private;
}
if (sofa.empty() && compiled.empty() && personalized.empty()) {
// The reference order: the SOFA file, then the unique compiled cache, then the
// personalized model.
if (fs_utf8::exists(default_sofa)) {
sofa = default_sofa;
} else {
compiled = resolve_compiled_hrtf(options, project_directory);
if (compiled.empty() && fs_utf8::exists(default_private)) {
personalized = default_private;
}
}
}
if (!personalized.empty()) {
if (!fs_utf8::exists(personalized)) {
fail("双耳模型不存在: " + personalized);
}
input.personalized_path = personalized;
return input;
}
if (sofa.empty() && compiled.empty()) {
if (!options.hrtf_cache_policy.empty() || !options.hrtf_cache_dir.empty() ||
options.hrtf_radius_m != 1.0) {
fail("HRTF cache/radius 选项需要 --sofa-hrtf");
}
fail("--binaural 未找到 HRTF 输入:默认 " + default_sofa + "、" + default_private +
" 或 " + default_cache_dir +
" 下的 .jochrtf 都不存在,请用 --sofa-hrtf PATH、--personalized-headphone PATH "
"或 --compiled-hrtf-cache PATH 指定");
}
if (sofa.empty() && (!options.hrtf_cache_policy.empty() || !options.hrtf_cache_dir.empty() ||
options.hrtf_radius_m != 1.0)) {
fail("HRTF cache/radius 选项需要 --sofa-hrtf");
}
if (sofa.empty()) {
input.compiled_path = compiled;
return input;
}
const std::string effective_policy =
options.hrtf_cache_policy.empty() ? "memory" : options.hrtf_cache_policy;
if (!options.hrtf_cache_dir.empty() && effective_policy != "disk") {
fail("--hrtf-cache-dir 需要 SOFA 与 disk cache policy 一起使用");
}
input.sofa_path = sofa;
input.disk = effective_policy == "disk";
input.cache_dir = options.hrtf_cache_dir.empty() ? default_cache_dir : options.hrtf_cache_dir;
if (!fs_utf8::exists(input.sofa_path)) {
fail("SOFA HRTF 不存在: " + input.sofa_path);
}
return input;
}
std::uint32_t binaural_mode_value(const std::string& name) {
if (name == "off") { return JOC_BINAURAL_OFF; }
if (name == "near") { return JOC_BINAURAL_NEAR; }
if (name == "far") { return JOC_BINAURAL_FAR; }
return JOC_BINAURAL_MID;
}
std::uint32_t clip_action_value(const std::string& name) {
if (name == "continue") { return JOC_CLIP_CONTINUE; }
if (name == "float32") { return JOC_CLIP_FLOAT32; }
if (name == "abort") { return JOC_CLIP_ABORT; }
return JOC_CLIP_ASK;
}
std::string format_eta(double seconds) {
if (seconds < 0.0 || seconds > 86400.0) {
return "--";
}
char buffer[64];
std::snprintf(buffer, sizeof(buffer), "%.0fs", seconds);
return buffer;
}
void JOC_CALL on_event(void* user, const joc_event* event) {
const Options* options = static_cast<const Options*>(user);
if (event == nullptr) {
return;
}
switch (event->type) {
case JOC_EV_PROGRESS: {
if (options->quiet) {
return;
}
const double fraction = event->progress >= 0.0 ? event->progress : 0.0;
const double remaining =
fraction > 0.0 ? event->elapsed_seconds * (1.0 - fraction) / fraction : -1.0;
std::printf("[%s] %llu/%llu %.1fx realtime ETA %s\n", event->stage_name,
static_cast<unsigned long long>(event->current_frame),
static_cast<unsigned long long>(event->total_frames),
event->realtime_factor, format_eta(remaining).c_str());
std::fflush(stdout);
return;
}
case JOC_EV_LOG: {
if (options->quiet || event->log_level < JOC_LOG_INFO || event->message[0] == '\0') {
return;
}
std::printf("[%s] %s\n", event->stage_name, event->message);
std::fflush(stdout);
return;
}
case JOC_EV_WARNING:
std::printf("[warning] %s\n", event->message);
return;
case JOC_EV_ERROR:
std::fprintf(stderr, "[error] %s (%s)\n", event->message,
joc_error_name(event->error_code));
return;
default:
if (options->quiet || event->log_level < JOC_LOG_INFO || event->message[0] == '\0') {
return;
}
std::printf("[%s] %s\n", event->stage_name, event->message);
return;
}
}
} // namespace
int main(int argc, char** argv) {
fs_utf8::configure_console();
const std::vector<std::string> arguments = fs_utf8::command_line_arguments(argc, argv);
Options options;
try {
parse_args(arguments, &options);
} catch (const std::exception& error) {
std::fprintf(stderr, "joc_cli: error: %s\n", error.what());
return 2;
}
if (options.help) {
print_usage();
return 0;
}
if (options.input.empty()) {
print_usage();
return 2;
}
try {
check_choice(options.speaker_format, "--speaker-format", {"float32", "int24"});
check_choice(options.binaural_format, "--binaural-format", {"float32", "int24"});
check_choice(options.clip_action, "--clip-action",
{"ask", "continue", "float32", "abort"});
check_choice(options.binaural_mode, "--binaural-mode", {"off", "near", "mid", "far"});
check_choice(options.trajectory_mode, "--trajectory-mode", {"compact", "dense64"});
check_choice(options.backend, "--backend", {"auto", "native", "python"});
check_choice(options.print_metadata, "--print-metadata", {"none", "summary", "frames"});
check_choice(options.metadata_backend, "--metadata-backend", {"auto", "emdf", "sidecar"});
if (!options.hrtf_cache_policy.empty()) {
check_choice(options.hrtf_cache_policy, "--hrtf-cache-policy",
{"none", "memory", "disk"});
}
const bool speaker_mode = !options.speaker_layout.empty();
const bool binaural_mode = options.binaural;
if (speaker_mode && binaural_mode) {
fail("argument --binaural: not allowed with argument --speaker-layout");
}
if (options.binaural_mode == "off" && (speaker_mode || binaural_mode)) {
fail("--binaural-mode off 仅用于 ADM BWF 输出(关闭 DBMD 双耳提示);"
"直接双耳渲染请使用 near/mid/far");
}
if (!options.speaker_output.empty() && !speaker_mode) {
fail("--speaker-output 必须与 --speaker-layout 一起使用");
}
if (!options.binaural_output.empty() && !binaural_mode) {
fail("--binaural-output 必须与 --binaural 一起使用");
}
const bool specific_output =
!options.speaker_output.empty() || !options.binaural_output.empty();
if (!options.output.empty() && specific_output) {
fail("-o/--output 与 --speaker-output/--binaural-output 不能同时使用");
}
if (!options.speaker_output.empty() && !options.binaural_output.empty()) {
fail("--speaker-output 与 --binaural-output 不能同时使用");
}
if (options.speaker_metadata_offset < 0) {
fail("speaker-metadata-offset 不能为负数");
}
const bool hrtf_options_used =
!options.sofa_hrtf.empty() || !options.compiled_hrtf_cache.empty() ||
options.personalized_headphone_used || !options.hrtf_cache_policy.empty() ||
!options.hrtf_cache_dir.empty() || options.hrtf_radius_m != 1.0;
if (hrtf_options_used && !binaural_mode) {
fail("SOFA/HRTF 选项仅与 --binaural 一起使用");
}
if (!std::isfinite(options.binaural_tail_seconds) ||
options.binaural_tail_seconds < 0.0) {
fail("binaural-tail-seconds 必须是非负有限值");
}
if (!std::isfinite(options.binaural_tail_threshold) ||
options.binaural_tail_threshold < 0.0) {
fail("binaural-tail-threshold 必须是非负有限值");
}
if (options.binaural_chunk_frames <= 0) {
fail("binaural-chunk-frames 必须大于 0");
}
if (!std::isfinite(options.hrtf_radius_m) || options.hrtf_radius_m <= 0.0) {
fail("hrtf-radius-m 必须是正有限值");
}
if (options.duration_set && options.duration <= 0.0) {
fail("duration 必须大于 0");
}
if (options.object_delay_samples < 0) {
fail("object-delay-samples 不能为负数");
}
if (options.native_threads_set && options.native_threads < 1) {
fail("native-threads 必须大于 0");
}
if (!std::isfinite(options.gain_db) || std::abs(options.gain_db) > 200.0) {
fail("gain-db 超出支持范围");
}
// Options this build cannot honour: fail loudly instead of ignoring them.
if (options.backend == "python") {
fail("--backend python 在本构建中不可用(已无 Python 后端);请使用 auto 或 native");
}
if (options.metadata_backend == "sidecar" || !options.metadata_dir.empty() ||
!options.metadata_cache.empty()) {
fail("metadata sidecar 在本构建中不可用(始终直接扫描 EMDF)");
}
if (!options.native_library.empty()) {
std::fprintf(stderr, "[info] --native-library 在本构建中忽略(单一 joc_core.dll)\n");
}
if (!fs_utf8::exists(options.input)) {
fail("输入文件不存在: " + options.input);
}
} catch (const std::exception& error) {
std::fprintf(stderr, "joc_cli: error: %s\n", error.what());
return 2;
}
const bool speaker_mode = !options.speaker_layout.empty();
const bool binaural_mode = options.binaural;
const std::string project_directory =
executable_dir(arguments.empty() ? std::string() : arguments.front());
const std::string output_path = resolve_output(options, options.input, project_directory);
std::error_code directory_error;
fs::create_directories(fs_utf8::to_path(output_path).parent_path(), directory_error);
joc_task_config config{};
config.struct_size = sizeof(config);
config.struct_version = JOC_TASK_CONFIG_VERSION;
config.input_path = options.input.c_str();
config.output_path = output_path.c_str();
config.ffmpeg_path = options.ffmpeg.empty() ? nullptr : options.ffmpeg.c_str();
config.bed_path = options.bed.empty() ? nullptr : options.bed.c_str();
config.work_dir = options.work_dir.empty() ? nullptr : options.work_dir.c_str();
config.eac3_drc_scale = options.eac3_drc_scale;
config.eac3_target_level = options.eac3_target_level;
config.operation =
binaural_mode ? JOC_OP_BINAURAL : speaker_mode ? JOC_OP_SPEAKER : JOC_OP_ADM_BWF;
const std::string& requested_format =
binaural_mode ? options.binaural_format : options.speaker_format;
config.output_format =
requested_format == "int24" ? JOC_FORMAT_PCM24 : JOC_FORMAT_FLOAT32;
config.clip_action = clip_action_value(options.clip_action);
config.speaker_layout_name = speaker_mode ? options.speaker_layout.c_str() : nullptr;
config.speaker_metadata_offset = static_cast<std::uint32_t>(options.speaker_metadata_offset);
config.binaural_mode = binaural_mode_value(options.binaural_mode);
config.adm_binaural_mode = binaural_mode_value(options.binaural_mode);
config.binaural_tail_seconds = options.binaural_tail_seconds;
config.binaural_tail_threshold = options.binaural_tail_threshold;
config.binaural_chunk_frames = static_cast<std::uint32_t>(options.binaural_chunk_frames);
config.object_delay_samples = static_cast<std::uint32_t>(options.object_delay_samples);
config.trajectory_mode =
options.trajectory_mode == "dense64" ? JOC_TRAJECTORY_DENSE64 : JOC_TRAJECTORY_COMPACT;
config.gain_db = options.gain_db;
config.progress_interval_frames = static_cast<std::uint32_t>(options.progress_every);
config.native_threads =
options.native_threads_set ? static_cast<std::uint32_t>(options.native_threads) : 0u;
config.print_metadata = options.print_metadata == "frames" ? 2u
: options.print_metadata == "summary" ? 1u
: 0u;
config.metadata_json_path =
options.metadata_json.empty() ? nullptr : options.metadata_json.c_str();
config.duration_frames =
options.duration_set
? static_cast<std::uint64_t>(
std::ceil(options.duration * kRate / static_cast<double>(kFrameSamples)))
: 0u;
config.flags = 0u;
if (options.skip_sha256) { config.flags |= JOC_TASK_F_SKIP_SHA256; }
if (options.keep_raw) { config.flags |= JOC_TASK_F_KEEP_INTERMEDIATE; }
if (options.metadata_only) { config.flags |= JOC_TASK_F_METADATA_ONLY; }
if (options.quiet) { config.flags |= JOC_TASK_F_QUIET; }
std::string hrtf_path;
std::string hrtf_sofa_path;
std::string hrtf_cache_dir;
std::string personalized_path;
std::string kernels_path;
if (binaural_mode) {
try {
const HrtfInput input = resolve_hrtf_input(options, project_directory);
hrtf_path = input.compiled_path;
hrtf_sofa_path = input.sofa_path;
hrtf_cache_dir = input.cache_dir;
personalized_path = input.personalized_path;
kernels_path = find_kernels(options, arguments.empty() ? std::string()
: arguments.front());
} catch (const std::exception& error) {
std::fprintf(stderr, "joc_cli: error: %s\n", error.what());
return 2;
}
}
config.hrtf_path = hrtf_path.empty() ? nullptr : hrtf_path.c_str();
config.hrtf_sofa_path = hrtf_sofa_path.empty() ? nullptr : hrtf_sofa_path.c_str();
config.hrtf_cache_dir = hrtf_cache_dir.empty() ? nullptr : hrtf_cache_dir.c_str();
config.personalized_headphone_path =
personalized_path.empty() ? nullptr : personalized_path.c_str();
config.hrtf_cache_policy = options.hrtf_cache_policy == "disk" ? JOC_HRTF_CACHE_DISK
: options.hrtf_cache_policy == "none" ? JOC_HRTF_CACHE_NONE
: JOC_HRTF_CACHE_MEMORY;
config.hrtf_radius_m = options.hrtf_radius_m;
config.kernels_path = kernels_path.empty() ? nullptr : kernels_path.c_str();
joc_validation_issue issues[32];
std::uint32_t issue_count = 0;
const joc_error validated = joc_task_validate(&config, issues, 32u, &issue_count);
for (std::uint32_t index = 0; index < std::min(issue_count, 32u); ++index) {
if (issues[index].severity >= 2u) {
std::fprintf(stderr, "[error] %s: %s\n", issues[index].field, issues[index].message);
} else if (!options.quiet) {
std::fprintf(stderr, "[warning] %s: %s\n", issues[index].field,
issues[index].message);
}
}
if (validated != JOC_OK) {
std::fprintf(stderr, "joc_cli: error: configuration rejected (%u issue(s))\n", issue_count);
return 2;
}
if (options.dry_run) {
std::printf("configuration accepted (%u issue(s))\n", issue_count);
return 0;
}
joc_event_sink sink{};
sink.struct_size = sizeof(sink);
sink.callback = &on_event;
sink.user = &options;
if (!options.quiet) {
const char* mode_name = binaural_mode ? "binaural" : speaker_mode ? "speaker" : "adm";
std::printf("[cli] %s -> %s (%s)\n", options.input.c_str(), output_path.c_str(),
mode_name);
std::fflush(stdout);
}
joc_task_result result{};
const joc_error status = joc_task_execute(&config, &sink, &result);
const std::string report_path =
options.report_json_set ? options.report_json : (output_path + ".report.json");
{
std::size_t needed = 0;
joc_task_result_to_json(&result, nullptr, 0u, &needed);
std::vector<char> buffer(needed + 1u);
if (joc_task_result_to_json(&result, buffer.data(), buffer.size(), &needed) == JOC_OK) {
if (std::FILE* file = fs_utf8::fopen(report_path, "wb")) {
std::fwrite(buffer.data(), 1, std::strlen(buffer.data()), file);
std::fputc('\n', file);
std::fclose(file);
}
}
}
std::printf("\nresult: %s\n", status == JOC_OK ? "ok" : joc_error_name(status));
std::printf(" output : %s\n", output_path.c_str());
std::printf(" frames : %llu (%.2f s)\n",
static_cast<unsigned long long>(result.input_frames), result.duration_sec);
std::printf(" output samples: %llu\n",
static_cast<unsigned long long>(result.output_samples));
std::printf(" output bytes : %llu\n",
static_cast<unsigned long long>(result.output_file_bytes));
std::printf(" format : %s\n",
result.output_format_actual == JOC_FORMAT_PCM24 ? "int24" : "float32");
std::printf(" peak : %.9g (%llu sample(s) above full scale)\n", result.output_peak,
static_cast<unsigned long long>(result.output_over_unity_values));
std::printf(" sha256 : %s\n",
result.output_sha256[0] != '\0' ? result.output_sha256 : "(skipped)");
std::printf(" report : %s\n", report_path.c_str());
// Each stage time is measured where that stage actually runs, and the three
// stages now overlap (see the pipeline in src/task/task.cpp), so the stage
// times deliberately do not add up to the wall-clock total.
std::printf(" timings : decode %.2fs, joc %.2fs, dsp %.2fs, write %.2fs"
" (stage times, concurrent), total %.2fs\n",
result.t_decode_bed, result.t_render, result.t_render_dsp, result.t_write_file,
result.t_total);
if (status != JOC_OK) {
std::fprintf(stderr, "joc_cli: error: %s: %s\n", result.error_stage, result.error_message);
}
return status == JOC_OK ? 0 : 1;
}
+113
View File
@@ -0,0 +1,113 @@
#include "eac3_transport/eac3_reader.h"
#include <utility>
#include "foundation/status.h"
namespace joc::eac3 {
namespace {
constexpr std::size_t kHeaderBytes = 4;
} // namespace
void FrameReader::push(const std::uint8_t* data, std::size_t size) {
if (failed_ || data == nullptr || size == 0) {
return;
}
if (consumed_ > 0) {
compact();
}
buffer_.insert(buffer_.end(), data, data + size);
}
void FrameReader::compact() {
if (consumed_ == 0) {
return;
}
buffer_.erase(buffer_.begin(), buffer_.begin() + static_cast<std::ptrdiff_t>(consumed_));
base_offset_ += consumed_;
consumed_ = 0;
}
void FrameReader::fail(joc_error code, std::string message) {
failed_ = true;
error_ = code;
message_ = std::move(message);
}
FrameReader::Next FrameReader::next(Frame* out) {
if (failed_) {
return Next::Fail;
}
const std::size_t available = buffer_.size() - consumed_;
if (available == 0) {
return Next::End;
}
const std::uint8_t* p = buffer_.data() + consumed_;
// The reference implementation rejects a frame whose header does not fit,
// rather than silently resynchronising on the next 0x0B77.
if (available < kHeaderBytes) {
if (finished_) {
fail(JOC_ERR_EAC3_SYNCFRAME, "E-AC-3 syncframe header truncated at end of input");
return Next::Fail;
}
return Next::End;
}
const std::uint16_t syncword = static_cast<std::uint16_t>((static_cast<std::uint16_t>(p[0]) << 8) | p[1]);
if (syncword != kSyncword) {
fail(JOC_ERR_EAC3_SYNCFRAME, "invalid E-AC-3 syncword (silent resynchronisation is not allowed)");
return Next::Fail;
}
// frmsiz: 11 bits spread over the low 3 bits of byte 2 and all of byte 3,
const std::size_t words =
static_cast<std::size_t>(((p[2] & 0x07u) << 8) | p[3]) + 1u;
const std::size_t frame_bytes = words * 2u;
if (frame_bytes > available) {
if (!finished_) {
return Next::End;
}
fail(JOC_ERR_BITSTREAM_TRUNCATED,
"last E-AC-3 syncframe extends past end of input (declared " +
std::to_string(frame_bytes) + " bytes, remaining " +
std::to_string(available) + ")");
return Next::Fail;
}
if (out != nullptr) {
out->data = p;
out->size = frame_bytes;
out->offset = base_offset_ + consumed_;
}
consumed_ += frame_bytes;
stream_offset_ = base_offset_ + consumed_;
++frames_emitted_;
return Next::Ok;
}
joc_error FrameReader::frame_bytes(const std::uint8_t* data, std::size_t size, std::size_t offset,
std::size_t* out_frame_bytes) {
if (data == nullptr || out_frame_bytes == nullptr) {
return JOC_ERR_INVALID_ARGUMENT;
}
if (offset + kHeaderBytes > size) {
return JOC_ERR_EAC3_SYNCFRAME;
}
if (static_cast<std::uint16_t>((static_cast<std::uint16_t>(data[offset]) << 8) | data[offset + 1]) !=
kSyncword) {
return JOC_ERR_EAC3_SYNCFRAME;
}
const std::size_t words =
static_cast<std::size_t>(((data[offset + 2] & 0x07u) << 8) | data[offset + 3]) + 1u;
const std::size_t frame_bytes = words * 2u;
if (offset + frame_bytes > size) {
return JOC_ERR_BITSTREAM_TRUNCATED;
}
*out_frame_bytes = frame_bytes;
return JOC_OK;
}
} // namespace joc::eac3
+68
View File
@@ -0,0 +1,68 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
#include "joc_core.h"
namespace joc::eac3 {
struct Frame {
const std::uint8_t* data = nullptr;
std::size_t size = 0;
std::size_t offset = 0; // byte offset of the frame start in the fed stream
};
class FrameReader {
public:
enum class Next {
Ok,
End,
Fail
};
FrameReader() = default;
FrameReader(const std::uint8_t* data, std::size_t size) {
push(data, size);
finish();
}
// Appends bytes to the internal buffer (used in incremental mode).
void push(const std::uint8_t* data, std::size_t size);
// Declares that no further bytes will arrive; a frame that is still
void finish() { finished_ = true; }
Next next(Frame* out);
joc_error error() const { return error_; }
const std::string& error_message() const { return message_; }
std::size_t frames_emitted() const { return frames_emitted_; }
std::size_t stream_offset() const { return stream_offset_; }
// report its declared byte length.
static joc_error frame_bytes(const std::uint8_t* data, std::size_t size, std::size_t offset,
std::size_t* out_frame_bytes);
static constexpr std::uint16_t kSyncword = 0x0B77;
private:
void compact();
void fail(joc_error code, std::string message);
std::vector<std::uint8_t> buffer_;
std::size_t consumed_ = 0; // bytes of buffer_ already turned into frames
std::size_t base_offset_ = 0;
bool finished_ = false;
bool failed_ = false;
joc_error error_ = JOC_OK;
std::string message_;
std::size_t frames_emitted_ = 0;
std::size_t stream_offset_ = 0;
};
} // namespace joc::eac3
+305
View File
@@ -0,0 +1,305 @@
#include "emdf/emdf_parser.h"
#include <algorithm>
#include <string>
#include "foundation/bit_reader.h"
namespace joc::emdf {
namespace {
Status syntax_fail(const std::string& message) {
return Status::fail(JOC_ERR_EMDF_SYNTAX, stage::kEmdf, message);
}
Status truncated_fail(const bits::BitReader& reader) {
return Status::fail(JOC_ERR_BITSTREAM_TRUNCATED, stage::kEmdf,
std::string("EMDF bitstream truncated: ") + reader.error_message());
}
} // namespace
Status parse_at(const std::uint8_t* data, std::size_t size, std::size_t start_bit, Container* out) {
if (data == nullptr || out == nullptr) {
return Status::fail(JOC_ERR_INVALID_ARGUMENT, stage::kEmdf, "null buffer or output");
}
if (start_bit + 16u > size * 8u) {
return syntax_fail("EMDF syncword position beyond buffer");
}
bits::BitReader reader;
reader.reset(data, size, start_bit);
if (reader.read(16) != kSyncword) {
return syntax_fail("EMDF syncword mismatch at bit " + std::to_string(start_bit));
}
const std::uint32_t length = reader.read(16);
const std::size_t body_start = reader.position();
const std::size_t body_end = body_start + static_cast<std::size_t>(length) * 8u;
if (body_end > reader.limit()) {
return syntax_fail("EMDF container length " + std::to_string(length) +
" exceeds buffer at bit " + std::to_string(start_bit));
}
reader.set_limit_bits(body_end);
std::uint32_t version = reader.read(2);
if (version == 3u) {
std::uint32_t extra = 0;
if (!bits::variable_bits(reader, 2, 8, &extra)) {
return reader.error() == JOC_ERR_BITSTREAM_TRUNCATED ? truncated_fail(reader)
: syntax_fail(reader.error_message());
}
version += extra;
}
std::uint32_t key_id = reader.read(3);
if (key_id == 7u) {
std::uint32_t extra = 0;
if (!bits::variable_bits(reader, 3, 8, &extra)) {
return reader.error() == JOC_ERR_BITSTREAM_TRUNCATED ? truncated_fail(reader)
: syntax_fail(reader.error_message());
}
key_id += extra;
}
if (reader.failed()) {
return truncated_fail(reader);
}
// TS 103 420 JOC uses version 0 / key_id 0; the strict check also rejects
// false 0x5838 markers that happen to sit inside audio data.
if (version != 0u || key_id != 0u) {
return syntax_fail("unsupported EMDF version/key_id " + std::to_string(version) + "/" +
std::to_string(key_id));
}
Container container;
container.start_bit = start_bit;
bool terminated = false;
while (reader.position() + 5u <= body_end) {
std::uint32_t payload_id = reader.read(5);
if (reader.failed()) {
return truncated_fail(reader);
}
if (payload_id == 0u) {
terminated = true;
break;
}
if (payload_id == 0x1Fu) {
std::uint32_t extra = 0;
if (!bits::variable_bits(reader, 5, 8, &extra)) {
return reader.error() == JOC_ERR_BITSTREAM_TRUNCATED ? truncated_fail(reader)
: syntax_fail(reader.error_message());
}
payload_id += extra;
}
for (std::size_t i = 0; i < container.payload_count; ++i) {
if (container.payloads[i].id == static_cast<std::uint8_t>(payload_id)) {
return syntax_fail("duplicate EMDF payload id " + std::to_string(payload_id));
}
}
if (container.payload_count >= kMaxPayloads) {
return syntax_fail("EMDF payload count exceeds " + std::to_string(kMaxPayloads));
}
const std::uint32_t has_sample_offset = reader.read(1);
std::uint16_t sample_offset = 0;
if (has_sample_offset != 0u) {
sample_offset = static_cast<std::uint16_t>(reader.read(12) >> 1);
}
if (reader.read(1) != 0u) {
std::uint32_t ignored = 0;
if (!bits::variable_bits(reader, 11, 8, &ignored)) {
return reader.error() == JOC_ERR_BITSTREAM_TRUNCATED ? truncated_fail(reader)
: syntax_fail(reader.error_message());
}
}
if (reader.read(1) != 0u) {
std::uint32_t ignored = 0;
if (!bits::variable_bits(reader, 2, 8, &ignored)) {
return reader.error() == JOC_ERR_BITSTREAM_TRUNCATED ? truncated_fail(reader)
: syntax_fail(reader.error_message());
}
}
if (reader.read(1) != 0u) {
if (!reader.skip(8)) {
return truncated_fail(reader);
}
}
if (reader.read(1) == 0u) {
bool frame_aligned = false;
if (has_sample_offset == 0u) {
frame_aligned = reader.read(1) != 0u;
if (frame_aligned) {
if (!reader.skip(2)) {
return truncated_fail(reader);
}
}
}
if (has_sample_offset != 0u || frame_aligned) {
if (!reader.skip(7)) {
return truncated_fail(reader);
}
}
}
if (reader.failed()) {
return truncated_fail(reader);
}
std::uint32_t payload_size = 0;
if (!bits::variable_bits(reader, 8, 8, &payload_size)) {
return reader.error() == JOC_ERR_BITSTREAM_TRUNCATED ? truncated_fail(reader)
: syntax_fail(reader.error_message());
}
const std::size_t payload_bits = static_cast<std::size_t>(payload_size) * 8u;
if (reader.position() + payload_bits > body_end) {
return syntax_fail("EMDF payload id " + std::to_string(payload_id) +
" extends past container body (size " + std::to_string(payload_size) +
" at bit " + std::to_string(reader.position()) + ")");
}
Payload& entry = container.payloads[container.payload_count++];
entry.id = static_cast<std::uint8_t>(payload_id);
entry.sample_offset = sample_offset;
entry.bit_offset = reader.position();
entry.size = payload_size;
if (!reader.skip(payload_bits)) {
return truncated_fail(reader);
}
}
if (!terminated) {
return syntax_fail("EMDF container has no payload id 0 terminator");
}
container.raw_size = 4u + static_cast<std::size_t>(length);
*out = container;
return Status::success();
}
void marker_offsets(const std::uint8_t* data, std::size_t size, std::vector<std::size_t>* out) {
out->clear();
if (data == nullptr || size < 4u) {
return;
}
// Eight global bit alignments. For shift != 0 the reference builds an
// (n-1)-byte shifted view and only scans pairs inside it, which is what the
// bounds below reproduce exactly.
for (std::size_t shift = 0; shift < 8u; ++shift) {
const std::size_t aligned_len = (shift == 0u) ? size : (size - 1u);
auto aligned_byte = [&](std::size_t index) -> std::uint8_t {
if (shift == 0u) {
return data[index];
}
const std::uint16_t high = static_cast<std::uint16_t>(data[index]) << shift;
const std::uint16_t low = static_cast<std::uint16_t>(data[index + 1u]) >> (8u - shift);
return static_cast<std::uint8_t>((high | low) & 0xFFu);
};
if (aligned_len < 2u) {
continue;
}
for (std::size_t i = 0; i + 1u < aligned_len; ++i) {
if (aligned_byte(i) == 0x58u && aligned_byte(i + 1u) == 0x38u) {
out->push_back(i * 8u + shift);
}
}
}
std::sort(out->begin(), out->end());
}
Status find_joc_emdf(const std::uint8_t* data, std::size_t size, Container* out) {
if (data == nullptr || out == nullptr) {
return Status::fail(JOC_ERR_INVALID_ARGUMENT, stage::kEmdf, "null buffer or output");
}
std::vector<std::size_t> offsets;
marker_offsets(data, size, &offsets);
std::vector<Container> matches;
std::size_t parse_errors = 0;
std::string first_parse_error;
for (const std::size_t start_bit : offsets) {
Container candidate;
const Status status = parse_at(data, size, start_bit, &candidate);
if (!status.ok()) {
++parse_errors;
if (first_parse_error.empty()) {
first_parse_error = "@bit" + std::to_string(start_bit) + ": " + status.message();
}
continue;
}
if (candidate.find(kIdOamd) != nullptr && candidate.find(kIdJoc) != nullptr) {
matches.push_back(candidate);
}
}
if (matches.empty()) {
// Classification stays at the transport level (identical to the reference
// implementation, which raises emdf_transport here), but the underlying
std::string message =
"no contiguous EMDF container carrying ID11+ID14 in this syncframe (markers=" +
std::to_string(offsets.size()) + ", parse_failures=" + std::to_string(parse_errors) +
")";
if (!first_parse_error.empty()) {
message += "; first candidate error " + first_parse_error;
}
return Status::fail(JOC_ERR_EMDF_TRANSPORT, stage::kEmdf, message);
}
std::sort(matches.begin(), matches.end(),
[](const Container& a, const Container& b) { return a.start_bit < b.start_bit; });
// A payload may contain bytes that look like another 0x5838 container; a
std::vector<Container> top_level;
for (const Container& candidate : matches) {
bool nested = false;
for (const Container& parent : top_level) {
if (parent.start_bit < candidate.start_bit &&
candidate.start_bit < parent.start_bit + parent.raw_size * 8u) {
nested = true;
break;
}
}
if (!nested) {
top_level.push_back(candidate);
}
}
if (top_level.size() != 1u) {
return Status::fail(JOC_ERR_EMDF_TRANSPORT, stage::kEmdf,
"multiple top-level JOC EMDF containers (" +
std::to_string(top_level.size()) +
"); automatic selection is not defined");
}
*out = top_level.front();
return Status::success();
}
void extract_container_bytes(const std::uint8_t* data, std::size_t size, const Container& container,
std::vector<std::uint8_t>* out) {
out->assign(container.raw_size, 0u);
if (out->empty()) {
return;
}
bits::BitReader reader;
reader.reset(data, size, container.start_bit);
reader.read_bytes(out->data(), out->size());
}
Status extract_payload_bytes(const std::uint8_t* data, std::size_t size, const Payload& payload,
std::vector<std::uint8_t>* out) {
if (data == nullptr || out == nullptr) {
return Status::fail(JOC_ERR_INVALID_ARGUMENT, stage::kEmdf, "null buffer or output");
}
out->assign(payload.size, 0u);
if (out->empty()) {
return Status::success();
}
bits::BitReader reader;
reader.reset(data, size, payload.bit_offset);
if (!reader.read_bytes(out->data(), out->size())) {
return Status::fail(JOC_ERR_BITSTREAM_TRUNCATED, stage::kEmdf,
"payload bytes extend past the syncframe");
}
return Status::success();
}
} // namespace joc::emdf
+57
View File
@@ -0,0 +1,57 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <vector>
#include "joc_core.h"
#include "foundation/status.h"
namespace joc::emdf {
inline constexpr std::uint16_t kSyncword = 0x5838;
inline constexpr std::uint8_t kIdOamd = 11;
inline constexpr std::uint8_t kIdJoc = 14;
inline constexpr std::size_t kMaxPayloads = JOC_MAX_EMDF_PAYLOADS;
struct Payload {
std::uint8_t id = 0;
std::uint16_t sample_offset = 0;
std::size_t bit_offset = 0; // MSB-first bit position of the payload bytes
std::size_t size = 0; // payload byte count
};
struct Container {
std::size_t start_bit = 0;
std::size_t raw_size = 0;
std::size_t payload_count = 0;
Payload payloads[kMaxPayloads] = {};
const Payload* find(std::uint8_t id) const {
for (std::size_t i = 0; i < payload_count; ++i) {
if (payloads[i].id == id) {
return &payloads[i];
}
}
return nullptr;
}
};
Status parse_at(const std::uint8_t* data, std::size_t size, std::size_t start_bit, Container* out);
// All candidate 0x5838 bit offsets over the eight alignments, ascending.
void marker_offsets(const std::uint8_t* data, std::size_t size, std::vector<std::size_t>* out);
Status find_joc_emdf(const std::uint8_t* data, std::size_t size, Container* out);
// Extract the container's bytes exactly as the bit reader sees them (identical
// to a memcpy for byte-aligned containers).
void extract_container_bytes(const std::uint8_t* data, std::size_t size, const Container& container,
std::vector<std::uint8_t>* out);
// Extract one payload's bytes with the same MSB-first semantics.
Status extract_payload_bytes(const std::uint8_t* data, std::size_t size, const Payload& payload,
std::vector<std::uint8_t>* out);
} // namespace joc::emdf
+30
View File
@@ -0,0 +1,30 @@
#include "foundation/bit_reader.h"
namespace joc::bits {
bool variable_bits(BitReader& reader, unsigned width, unsigned max_groups, std::uint32_t* out_value) {
std::uint32_t value = 0;
for (unsigned group = 0; group < max_groups; ++group) {
value += reader.read(width);
if (reader.failed()) {
return false;
}
const std::uint32_t more = reader.read(1);
if (reader.failed()) {
return false;
}
if (more == 0u) {
if (out_value != nullptr) {
*out_value = value;
}
return true;
}
value = (value + 1u) << width;
}
// Same failure mode as the reference implementation: an extension chain
// that never terminates is a syntax error, not a truncation.
reader.fail(JOC_ERR_EMDF_SYNTAX, "variable_bits extension groups exceeded");
return false;
}
} // namespace joc::bits
+125
View File
@@ -0,0 +1,125 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include "joc_core.h"
namespace joc::bits {
class BitReader {
public:
BitReader() = default;
BitReader(const std::uint8_t* data, std::size_t size) { reset(data, size); }
void reset(const std::uint8_t* data, std::size_t size, std::size_t start_bit = 0) {
data_ = data;
size_bits_ = size * 8u;
pos_ = start_bit;
limit_ = size_bits_;
error_ = JOC_OK;
message_ = "";
}
void set_limit_bits(std::size_t limit_bits) {
limit_ = limit_bits < size_bits_ ? limit_bits : size_bits_;
}
std::size_t position() const { return pos_; }
std::size_t limit() const { return limit_; }
std::size_t remaining_bits() const { return pos_ <= limit_ ? limit_ - pos_ : 0; }
const std::uint8_t* data() const { return data_; }
bool failed() const { return error_ != JOC_OK; }
joc_error error() const { return error_; }
const char* error_message() const { return message_; }
std::uint32_t read(unsigned count) {
if (count == 0) {
return 0;
}
if (!can_read(count)) {
fail_truncated(count);
return 0;
}
std::uint32_t value = 0;
if ((pos_ & 7u) == 0u && count >= 8u) {
while (count >= 8u) {
value = (value << 8) | data_[pos_ >> 3];
pos_ += 8u;
count -= 8u;
}
}
while (count-- > 0u) {
const std::uint32_t bit = (data_[pos_ >> 3] >> (7u - (pos_ & 7u))) & 1u;
value = (value << 1) | bit;
++pos_;
}
return value;
}
std::uint64_t read64(unsigned count) {
if (count <= 32u) {
return static_cast<std::uint64_t>(read(count));
}
const std::uint64_t high = static_cast<std::uint64_t>(read(count - 32u));
const std::uint64_t low = static_cast<std::uint64_t>(read(32u));
return (high << 32) | low;
}
bool skip(std::size_t count) {
if (!can_read(count)) {
fail_truncated(count);
return false;
}
pos_ += count;
return true;
}
bool read_bytes(std::uint8_t* out, std::size_t count) {
if (count == 0) {
return true;
}
if (!can_read(count * 8u)) {
fail_truncated(count * 8u);
return false;
}
for (std::size_t i = 0; i < count; ++i) {
out[i] = static_cast<std::uint8_t>(read(8u));
}
return true;
}
bool can_read(std::size_t count) const {
return !failed() && count <= limit_ && pos_ <= limit_ - count;
}
// semantic check fails, so the reader never continues past it).
void fail(joc_error code, const char* message) {
if (!failed()) {
error_ = code;
message_ = message;
}
}
private:
void fail_truncated(std::size_t count) {
fail(JOC_ERR_BITSTREAM_TRUNCATED, "bit read past end of buffer");
last_request_ = count;
}
const std::uint8_t* data_ = nullptr;
std::size_t size_bits_ = 0;
std::size_t pos_ = 0;
std::size_t limit_ = 0;
std::size_t last_request_ = 0;
joc_error error_ = JOC_OK;
const char* message_ = "";
};
// followed by a continuation bit. Mirrors src/emdf.py:variable_bits().
bool variable_bits(BitReader& reader, unsigned width, unsigned max_groups,
std::uint32_t* out_value);
} // namespace joc::bits
+206
View File
@@ -0,0 +1,206 @@
#include "foundation/fft.h"
#include <cmath>
#include "simd/simd.h"
namespace joc::dsp {
// The dispatched kernels read and write the spectrum as interleaved doubles, and
// an array of std::complex<double> is exactly that: two doubles per element, no
// padding, no vtable.
static_assert(sizeof(Complex) == 2u * sizeof(double), "complex layout");
namespace {
constexpr double kPi = 3.14159265358979323846;
template <typename Container>
void fft_in_place(Container* data, bool inverse) {
const std::size_t count = data->size();
if (count < 2u) {
return;
}
for (std::size_t index = 1u, reversed = 0u; index < count; ++index) {
std::size_t bit = count >> 1u;
for (; (reversed & bit) != 0u; bit >>= 1u) {
reversed ^= bit;
}
reversed ^= bit;
if (index < reversed) {
std::swap((*data)[index], (*data)[reversed]);
}
}
for (std::size_t length = 2u; length <= count; length <<= 1u) {
const double angle = (inverse ? 2.0 : -2.0) * kPi / static_cast<double>(length);
const Complex step(std::cos(angle), std::sin(angle));
for (std::size_t start = 0u; start < count; start += length) {
Complex factor(1.0, 0.0);
for (std::size_t offset = 0u; offset < length / 2u; ++offset) {
const Complex even = (*data)[start + offset];
const Complex odd = (*data)[start + offset + length / 2u] * factor;
(*data)[start + offset] = even + odd;
(*data)[start + offset + length / 2u] = even - odd;
factor *= step;
}
}
}
if (inverse) {
for (Complex& value : *data) {
value /= static_cast<double>(count);
}
}
}
bool is_power_of_two(std::size_t value) { return value != 0u && (value & (value - 1u)) == 0u; }
} // namespace
FftPlan::FftPlan(std::size_t size, bool inverse) : size_(size), inverse_(inverse) {
reverse_.resize(size);
for (std::size_t index = 1u, reversed = 0u; index < size; ++index) {
std::size_t bit = size >> 1u;
for (; (reversed & bit) != 0u; bit >>= 1u) {
reversed ^= bit;
}
reversed ^= bit;
reverse_[index] = reversed;
}
for (std::size_t length = 2u; length <= size; length <<= 1u) {
const double angle = (inverse ? 2.0 : -2.0) * kPi / static_cast<double>(length);
const Complex step(std::cos(angle), std::sin(angle));
stage_begin_.push_back(twiddle_.size());
Complex factor(1.0, 0.0);
for (std::size_t offset = 0u; offset < length / 2u; ++offset) {
twiddle_.push_back(factor);
factor *= step;
}
}
}
// Exactly the operations fft_in_place performs, in the same order, with the
// twiddles read from the precomputed recurrence instead of being re-derived.
template <typename Container>
void FftPlan::apply(Container* data) const {
const std::size_t count = data->size();
if (count < 2u) {
return;
}
const std::size_t* reverse = reverse_.data();
for (std::size_t index = 1u; index < count; ++index) {
const std::size_t reversed = reverse[index];
if (index < reversed) {
std::swap((*data)[index], (*data)[reversed]);
}
}
// The cascade is dispatched for every power-of-two size the kernels can pack
// whole groups into a vector (JOC_SIMD pins one tier for verification). A
// kernel only ever puts independent butterflies in the same vector, so every
// output keeps the operation sequence and the roundings written below; small
// transforms -- and the caller's own table -- keep the portable loop.
if (count >= simd::kMinVectorFftSize && (count & (count - 1u)) == 0u) {
simd::fft_butterflies(reinterpret_cast<double*>(data->data()), count,
reinterpret_cast<const double*>(twiddle_.data()),
stage_begin_.data());
} else {
std::size_t stage = 0u;
for (std::size_t length = 2u; length <= count; length <<= 1u, ++stage) {
const Complex* table = twiddle_.data() + stage_begin_[stage];
for (std::size_t start = 0u; start < count; start += length) {
for (std::size_t offset = 0u; offset < length / 2u; ++offset) {
const Complex even = (*data)[start + offset];
const Complex odd = (*data)[start + offset + length / 2u] * table[offset];
(*data)[start + offset] = even + odd;
(*data)[start + offset + length / 2u] = even - odd;
}
}
}
}
if (inverse_) {
for (Complex& value : *data) {
value /= static_cast<double>(count);
}
}
}
void fft_radix2(std::vector<Complex>* data, const FftPlan& plan) { plan.apply(data); }
void fft_radix2(std::array<Complex, kQmfFftSize>* data, const FftPlan& plan) { plan.apply(data); }
void fft_radix2(std::vector<Complex>* data, bool inverse) { fft_in_place(data, inverse); }
void fft_radix2(std::array<Complex, kQmfFftSize>* data, bool inverse) {
fft_in_place(data, inverse);
}
void fft_any(const std::vector<Complex>& input, bool inverse, std::vector<Complex>* output) {
const std::size_t count = input.size();
if (is_power_of_two(count)) {
*output = input;
fft_radix2(output, inverse);
return;
}
std::size_t size = 1u;
while (size < 2u * count + 1u) {
size <<= 1u;
}
const double sign = inverse ? 1.0 : -1.0;
std::vector<Complex> left(size, Complex(0.0, 0.0));
std::vector<Complex> right(size, Complex(0.0, 0.0));
for (std::size_t index = 0u; index < count; ++index) {
const std::size_t wrapped = (index * index) % (2u * count);
const double angle = kPi * static_cast<double>(wrapped) / static_cast<double>(count);
const Complex chirp(std::cos(angle), sign * std::sin(angle));
left[index] = input[index] * chirp;
right[index] = std::conj(chirp);
if (index != 0u) {
right[size - index] = std::conj(chirp);
}
}
fft_radix2(&left, false);
fft_radix2(&right, false);
for (std::size_t index = 0u; index < size; ++index) {
left[index] *= right[index];
}
fft_radix2(&left, true);
output->resize(count);
for (std::size_t index = 0u; index < count; ++index) {
const std::size_t wrapped = (index * index) % (2u * count);
const double angle = kPi * static_cast<double>(wrapped) / static_cast<double>(count);
const Complex chirp(std::cos(angle), sign * std::sin(angle));
(*output)[index] = left[index] * chirp;
if (inverse) {
(*output)[index] /= static_cast<double>(count);
}
}
}
std::size_t next_fast_len(std::size_t value) {
if (value <= 6u) {
return value;
}
std::size_t best = value;
for (std::size_t power2 = 1u; power2 < value * 2u; power2 *= 2u) {
for (std::size_t power3 = power2; power3 < value * 2u; power3 *= 3u) {
std::size_t power5 = power3;
while (power5 < value) {
power5 *= 5u;
}
best = std::min(best, power5);
if (power3 >= value) {
break;
}
}
}
return best;
}
std::size_t next_power_of_two(std::size_t value) {
std::size_t result = 1u;
while (result < value) {
result <<= 1u;
}
return result;
}
} // namespace joc::dsp
+68
View File
@@ -0,0 +1,68 @@
#pragma once
#include <array>
#include <complex>
#include <cstddef>
#include <vector>
// Complex transforms shared by the HRTF and Rosella DSP cores. The convention is
// NumPy's: the forward transform is unnormalised and the inverse scales by 1/N,
// so a ported pipeline keeps the reference's arithmetic bit for bit.
namespace joc::dsp {
using Complex = std::complex<double>;
inline constexpr std::size_t kQmfFftSize = 128;
// Precomputed radix-2 plan for one size and direction.
//
// The transform derives each butterfly's twiddle by multiplying the previous one
// by the stage step, so the twiddle at offset k is `step` multiplied k times in
// that order, independently of the group. Materialising that exact recurrence --
// and the bit-reversal permutation -- removes one complex multiply and a
// (length/2)-deep serial dependency from every stage's inner loop. The table
// entries are the recurrence's own values, so the transform is bit-identical.
//
// The 128-point cascade is executed by the runtime-dispatched SIMD kernel
// (src/simd/simd.h): it computes independent butterflies in parallel lanes,
// which leaves both the table and every output's summation order untouched.
class FftPlan {
public:
FftPlan(std::size_t size, bool inverse);
std::size_t size() const { return size_; }
bool inverse() const { return inverse_; }
private:
template <typename Container>
void apply(Container* data) const;
friend void fft_radix2(std::vector<Complex>* data, const FftPlan& plan);
friend void fft_radix2(std::array<Complex, kQmfFftSize>* data, const FftPlan& plan);
std::size_t size_ = 0;
bool inverse_ = false;
std::vector<std::size_t> reverse_; // bit-reversal permutation, [size]
std::vector<std::size_t> stage_begin_; // twiddle offset of each stage
std::vector<Complex> twiddle_; // per stage, length/2 entries, concatenated
};
// In-place radix-2 transform; the size must be a power of two.
void fft_radix2(std::vector<Complex>* data, bool inverse);
void fft_radix2(std::array<Complex, kQmfFftSize>* data, bool inverse);
// Plan-driven forms: the plan carries the size and the direction, so a caller that
// transforms the same length repeatedly builds it once.
void fft_radix2(std::vector<Complex>* data, const FftPlan& plan);
void fft_radix2(std::array<Complex, kQmfFftSize>* data, const FftPlan& plan);
// Exact-length transform: radix-2 when the size allows it, Bluestein otherwise.
// scipy/numpy use a mixed-radix transform, which is the same transform.
void fft_any(const std::vector<Complex>& input, bool inverse, std::vector<Complex>* output);
// scipy's next_fast_len: the smallest 5-smooth number that is not smaller.
std::size_t next_fast_len(std::size_t value);
std::size_t next_power_of_two(std::size_t value);
} // namespace joc::dsp
+164
View File
@@ -0,0 +1,164 @@
#include "foundation/fs_utf8.h"
#include <cstring>
#include <fstream>
#include <vector>
#if defined(_WIN32)
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <windows.h>
#include <shellapi.h>
#include <fcntl.h>
#include <io.h>
#else
#include <cstdlib>
#include <unistd.h>
#endif
namespace joc::fs_utf8 {
namespace fs = std::filesystem;
fs::path to_path(const std::string& utf8) {
return fs::path(std::u8string(reinterpret_cast<const char8_t*>(utf8.data()), utf8.size()));
}
std::string from_path(const fs::path& path) {
const std::u8string text = path.u8string();
return std::string(reinterpret_cast<const char*>(text.data()), text.size());
}
std::FILE* fopen(const std::string& utf8_path, const char* mode) {
#if defined(_WIN32)
const std::wstring wide_mode(mode, mode + std::strlen(mode));
return ::_wfopen(to_path(utf8_path).c_str(), wide_mode.c_str());
#else
return std::fopen(utf8_path.c_str(), mode);
#endif
}
std::FILE* fopen_spool(const std::string& utf8_path) {
#if defined(_WIN32)
// Delete-on-close handed to the CRT: if the process is killed the file goes with
// it, which is what stops an aborted run from leaving hundreds of gigabytes.
HANDLE handle = ::CreateFileW(
to_path(utf8_path).c_str(), GENERIC_READ | GENERIC_WRITE | DELETE,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, CREATE_ALWAYS,
FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE, nullptr);
if (handle == INVALID_HANDLE_VALUE) {
return nullptr;
}
const int descriptor = ::_open_osfhandle(reinterpret_cast<std::intptr_t>(handle), 0);
if (descriptor == -1) {
::CloseHandle(handle);
return nullptr;
}
return ::_fdopen(descriptor, "wb+");
#else
return std::fopen(utf8_path.c_str(), "wb+");
#endif
}
int remove(const std::string& utf8_path) {
#if defined(_WIN32)
return ::_wremove(to_path(utf8_path).c_str());
#else
return std::remove(utf8_path.c_str());
#endif
}
bool exists(const std::string& utf8_path) {
std::error_code error;
return fs::exists(to_path(utf8_path), error);
}
bool is_directory(const std::string& utf8_path) {
std::error_code error;
return fs::is_directory(to_path(utf8_path), error);
}
std::uintmax_t file_size(const std::string& utf8_path, std::error_code& error) {
return fs::file_size(to_path(utf8_path), error);
}
std::string temp_directory() {
std::error_code error;
const fs::path directory = fs::temp_directory_path(error);
return error ? std::string(".") : from_path(directory);
}
std::string executable_path() {
#if defined(_WIN32)
std::vector<wchar_t> buffer(MAX_PATH);
while (true) {
const DWORD written =
::GetModuleFileNameW(nullptr, buffer.data(), static_cast<DWORD>(buffer.size()));
if (written == 0) {
return std::string();
}
if (written < buffer.size()) {
return from_path(fs::path(std::wstring(buffer.data(), written)));
}
buffer.resize(buffer.size() * 2u);
}
#elif defined(__linux__)
std::vector<char> buffer(4096u, '\0');
const ssize_t written = ::readlink("/proc/self/exe", buffer.data(), buffer.size() - 1u);
return written > 0 ? std::string(buffer.data(), static_cast<std::size_t>(written))
: std::string();
#else
return std::string();
#endif
}
std::ifstream open_input(const std::string& utf8_path) {
return std::ifstream(to_path(utf8_path), std::ios::binary);
}
std::ofstream open_output(const std::string& utf8_path) {
return std::ofstream(to_path(utf8_path), std::ios::binary);
}
std::vector<std::string> command_line_arguments(int argc, char** argv) {
#if defined(_WIN32)
(void)argc;
(void)argv;
int count = 0;
LPWSTR* wide = ::CommandLineToArgvW(::GetCommandLineW(), &count);
std::vector<std::string> arguments;
if (wide == nullptr) {
return arguments;
}
arguments.reserve(static_cast<std::size_t>(count));
for (int index = 0; index < count; ++index) {
const std::wstring_view text(wide[index]);
const int size = ::WideCharToMultiByte(CP_UTF8, 0, text.data(),
static_cast<int>(text.size()), nullptr, 0, nullptr,
nullptr);
std::string utf8(static_cast<std::size_t>(size), '\0');
if (size > 0) {
::WideCharToMultiByte(CP_UTF8, 0, text.data(), static_cast<int>(text.size()),
utf8.data(), size, nullptr, nullptr);
}
arguments.push_back(std::move(utf8));
}
::LocalFree(wide);
return arguments;
#else
std::vector<std::string> arguments;
arguments.reserve(static_cast<std::size_t>(argc));
for (int index = 0; index < argc; ++index) {
arguments.emplace_back(argv[index]);
}
return arguments;
#endif
}
void configure_console() {
#if defined(_WIN32)
::SetConsoleOutputCP(CP_UTF8);
#endif
}
} // namespace joc::fs_utf8
+47
View File
@@ -0,0 +1,47 @@
#pragma once
#include <cstdint>
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <string>
#include <system_error>
#include <vector>
// Paths inside this library are always UTF-8, on every platform. std::filesystem
// stores UTF-16 on Windows and bytes elsewhere, and the narrow CRT uses the ANSI
// code page on Windows, so every path crosses into the OS through this shim: that
// is what makes non-ASCII names (Japanese, Chinese, ...) work.
namespace joc::fs_utf8 {
std::filesystem::path to_path(const std::string& utf8);
std::string from_path(const std::filesystem::path& path);
// File handles and queries take a UTF-8 path: _wfopen on Windows, plain calls
// elsewhere. Nothing else in the library may call the narrow CRT with a path.
std::FILE* fopen(const std::string& utf8_path, const char* mode);
// Temporary spool handle: on Windows the file is opened delete-on-close, so killing
// the process removes it instead of leaving a multi-gigabyte leftover behind.
std::FILE* fopen_spool(const std::string& utf8_path);
int remove(const std::string& utf8_path);
bool exists(const std::string& utf8_path);
bool is_directory(const std::string& utf8_path);
std::uintmax_t file_size(const std::string& utf8_path, std::error_code& error);
std::string temp_directory();
// The running executable's own path, UTF-8, or empty when the platform cannot
// report it. Defaults are anchored here so they never depend on the CWD.
std::string executable_path();
// Streams: std::ifstream/ofstream accept a std::filesystem::path, which is the
// portable way to open a UTF-8 path.
std::ifstream open_input(const std::string& utf8_path);
std::ofstream open_output(const std::string& utf8_path);
// Command line arguments as UTF-8. Windows hands the process UTF-16 and the
// narrow CRT would convert it through the ANSI code page, so the wide command
// line is re-parsed there; on POSIX argv is already bytes in the user's locale.
std::vector<std::string> command_line_arguments(int argc, char** argv);
void configure_console();
} // namespace joc::fs_utf8
+25
View File
@@ -0,0 +1,25 @@
// Port of the reference's adm_atmos.q_to_adm_xyz: OAMD Q15 coordinates to the ADM
// cartesian triple. It lives in foundation because both the ADM writer and the
// object position timeline need it, and the timeline must not depend on output.
#pragma once
#include <algorithm>
#include "foundation/py_num.h"
namespace joc::geometry {
inline void q_to_adm_xyz(int q1, int q2, int q3, double* x, double* y, double* z) {
const double posX = std::min(1.0, static_cast<double>(pynum::py_round(
static_cast<double>(q1) * 62.0 / 32767.0)) / 62.0);
const double posY = std::min(1.0, static_cast<double>(pynum::py_round(
static_cast<double>(q2) * 62.0 / 32767.0)) / 62.0);
double posZ = static_cast<double>(pynum::py_round(
static_cast<double>(q3) * 15.0 / 32767.0)) / 15.0;
posZ = std::max(-1.0, std::min(1.0, posZ));
*x = posX * 2.0 - 1.0;
*y = 1.0 - posY * 2.0;
*z = posZ;
}
} // namespace joc::geometry
+227
View File
@@ -0,0 +1,227 @@
#include "foundation/mini_json.h"
#include <cmath>
#include <cstdlib>
namespace joc::json {
namespace {
void skip_space(const std::string& text, std::size_t* index) {
while (*index < text.size() &&
(text[*index] == ' ' || text[*index] == '\t' || text[*index] == '\n' ||
text[*index] == '\r')) {
++(*index);
}
}
bool read_string(const std::string& text, std::size_t* index, std::string* out) {
if (*index >= text.size() || text[*index] != '"') {
return false;
}
++(*index);
out->clear();
while (*index < text.size()) {
const char c = text[*index];
if (c == '\\') {
if (*index + 1 >= text.size()) {
return false;
}
const char escape = text[*index + 1];
*index += 2;
switch (escape) {
case '"': out->push_back('"'); break;
case '\\': out->push_back('\\'); break;
case '/': out->push_back('/'); break;
case 'b': out->push_back('\b'); break;
case 'f': out->push_back('\f'); break;
case 'n': out->push_back('\n'); break;
case 'r': out->push_back('\r'); break;
case 't': out->push_back('\t'); break;
case 'u': {
if (*index + 4 > text.size()) {
return false;
}
unsigned code = 0;
for (int i = 0; i < 4; ++i) {
const char digit = text[*index + static_cast<std::size_t>(i)];
code <<= 4;
if (digit >= '0' && digit <= '9') { code |= static_cast<unsigned>(digit - '0'); }
else if (digit >= 'a' && digit <= 'f') { code |= static_cast<unsigned>(digit - 'a' + 10); }
else if (digit >= 'A' && digit <= 'F') { code |= static_cast<unsigned>(digit - 'A' + 10); }
else { return false; }
}
*index += 4;
if (code < 0x80u) {
out->push_back(static_cast<char>(code));
} else if (code < 0x800u) {
out->push_back(static_cast<char>(0xC0u | (code >> 6)));
out->push_back(static_cast<char>(0x80u | (code & 0x3Fu)));
} else {
out->push_back(static_cast<char>(0xE0u | (code >> 12)));
out->push_back(static_cast<char>(0x80u | ((code >> 6) & 0x3Fu)));
out->push_back(static_cast<char>(0x80u | (code & 0x3Fu)));
}
break;
}
default: return false;
}
continue;
}
if (c == '"') {
++(*index);
return true;
}
out->push_back(c);
++(*index);
}
return false;
}
bool read_compound(const std::string& text, std::size_t* index, std::string* out) {
const char open = text[*index];
const char close = open == '{' ? '}' : ']';
int depth = 0;
const std::size_t start = *index;
while (*index < text.size()) {
const char c = text[*index];
if (c == '"') {
std::string ignored;
if (!read_string(text, index, &ignored)) {
return false;
}
continue;
}
if (c == open) {
++depth;
} else if (c == close) {
--depth;
if (depth == 0) {
++(*index);
*out = text.substr(start, *index - start);
return true;
}
}
++(*index);
}
return false;
}
} // namespace
bool parse_object(const std::string& text, std::vector<Member>* out, std::string* error) {
out->clear();
std::size_t index = 0;
skip_space(text, &index);
if (index >= text.size() || text[index] != '{') {
if (error != nullptr) { *error = "metadata is not a JSON object"; }
return false;
}
++index;
for (;;) {
skip_space(text, &index);
if (index < text.size() && text[index] == '}') {
++index;
break;
}
if (index >= text.size() || text[index] == ',') {
if (index >= text.size()) {
if (error != nullptr) { *error = "unterminated JSON object"; }
return false;
}
++index;
continue;
}
Member member;
if (!read_string(text, &index, &member.key)) {
if (error != nullptr) { *error = "expected a JSON key"; }
return false;
}
skip_space(text, &index);
if (index >= text.size() || text[index] != ':') {
if (error != nullptr) { *error = "expected ':' after JSON key " + member.key; }
return false;
}
++index;
skip_space(text, &index);
if (index >= text.size()) {
if (error != nullptr) { *error = "missing JSON value for " + member.key; }
return false;
}
if (text[index] == '"') {
member.is_string = true;
if (!read_string(text, &index, &member.raw)) {
if (error != nullptr) { *error = "bad JSON string for " + member.key; }
return false;
}
} else if (text[index] == '{' || text[index] == '[') {
if (!read_compound(text, &index, &member.raw)) {
if (error != nullptr) { *error = "bad JSON container for " + member.key; }
return false;
}
} else {
const std::size_t start = index;
while (index < text.size() && text[index] != ',' && text[index] != '}') {
++index;
}
member.raw = text.substr(start, index - start);
while (!member.raw.empty() &&
(member.raw.back() == ' ' || member.raw.back() == '\n' ||
member.raw.back() == '\r' || member.raw.back() == '\t')) {
member.raw.pop_back();
}
}
for (const Member& existing : *out) {
if (existing.key == member.key) {
if (error != nullptr) { *error = "duplicate JSON key " + member.key; }
return false;
}
}
out->push_back(std::move(member));
}
return true;
}
const Member* find(const std::vector<Member>& members, const std::string& key) {
for (const Member& member : members) {
if (member.key == key) {
return &member;
}
}
return nullptr;
}
bool as_string(const Member& member, std::string* out) {
if (!member.is_string || out == nullptr) {
return false;
}
*out = member.raw;
return true;
}
bool as_number(const Member& member, double* out) {
if (member.is_string || out == nullptr) {
return false;
}
char* end = nullptr;
const double value = std::strtod(member.raw.c_str(), &end);
if (end == member.raw.c_str() || !std::isfinite(value)) {
return false;
}
*out = value;
return true;
}
bool as_integer(const Member& member, long long* out) {
double value = 0.0;
if (!as_number(member, &value) || out == nullptr) {
return false;
}
if (value != std::floor(value)) {
return false;
}
*out = static_cast<long long>(value);
return true;
}
} // namespace joc::json
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#include <string>
#include <utility>
#include <vector>
namespace joc::json {
struct Member {
std::string key;
std::string raw;
bool is_string = false;
};
// Parses a top-level JSON object. Rejects non-objects and duplicate keys.
bool parse_object(const std::string& text, std::vector<Member>* out, std::string* error);
const Member* find(const std::vector<Member>& members, const std::string& key);
bool as_string(const Member& member, std::string* out);
bool as_number(const Member& member, double* out);
bool as_integer(const Member& member, long long* out);
} // namespace joc::json
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#include <cfenv>
#include <cmath>
#include <cstdio>
#include <string>
namespace joc::pynum {
inline long long py_round(double value) {
return static_cast<long long>(std::nearbyint(value));
}
inline std::string format_fixed(double value, int decimals) {
char buffer[64];
std::snprintf(buffer, sizeof(buffer), "%.*f", decimals, value);
return std::string(buffer);
}
inline long long trunc_to_ll(double value) {
return static_cast<long long>(value);
}
} // namespace joc::pynum
+165
View File
@@ -0,0 +1,165 @@
#include "foundation/sha256.h"
#include <cstring>
namespace joc::crypto {
namespace {
constexpr std::uint32_t kK[64] = {
0x428a2f98u, 0x71374491u, 0xb5c0fbcfu, 0xe9b5dba5u, 0x3956c25bu, 0x59f111f1u, 0x923f82a4u,
0xab1c5ed5u, 0xd807aa98u, 0x12835b01u, 0x243185beu, 0x550c7dc3u, 0x72be5d74u, 0x80deb1feu,
0x9bdc06a7u, 0xc19bf174u, 0xe49b69c1u, 0xefbe4786u, 0x0fc19dc6u, 0x240ca1ccu, 0x2de92c6fu,
0x4a7484aau, 0x5cb0a9dcu, 0x76f988dau, 0x983e5152u, 0xa831c66du, 0xb00327c8u, 0xbf597fc7u,
0xc6e00bf3u, 0xd5a79147u, 0x06ca6351u, 0x14292967u, 0x27b70a85u, 0x2e1b2138u, 0x4d2c6dfcu,
0x53380d13u, 0x650a7354u, 0x766a0abbu, 0x81c2c92eu, 0x92722c85u, 0xa2bfe8a1u, 0xa81a664bu,
0xc24b8b70u, 0xc76c51a3u, 0xd192e819u, 0xd6990624u, 0xf40e3585u, 0x106aa070u, 0x19a4c116u,
0x1e376c08u, 0x2748774cu, 0x34b0bcb5u, 0x391c0cb3u, 0x4ed8aa4au, 0x5b9cca4fu, 0x682e6ff3u,
0x748f82eeu, 0x78a5636fu, 0x84c87814u, 0x8cc70208u, 0x90befffau, 0xa4506cebu, 0xbef9a3f7u,
0xc67178f2u};
inline std::uint32_t rotr(std::uint32_t value, unsigned count) {
return (value >> count) | (value << (32u - count));
}
} // namespace
void Sha256::reset() {
state_[0] = 0x6a09e667u;
state_[1] = 0xbb67ae85u;
state_[2] = 0x3c6ef372u;
state_[3] = 0xa54ff53au;
state_[4] = 0x510e527fu;
state_[5] = 0x9b05688cu;
state_[6] = 0x1f83d9abu;
state_[7] = 0x5be0cd19u;
bit_count_ = 0;
buffer_used_ = 0;
std::memset(buffer_, 0, sizeof(buffer_));
}
void Sha256::transform(const std::uint8_t block[64]) {
std::uint32_t w[64];
for (unsigned i = 0; i < 16; ++i) {
w[i] = (static_cast<std::uint32_t>(block[i * 4]) << 24) |
(static_cast<std::uint32_t>(block[i * 4 + 1]) << 16) |
(static_cast<std::uint32_t>(block[i * 4 + 2]) << 8) |
static_cast<std::uint32_t>(block[i * 4 + 3]);
}
for (unsigned i = 16; i < 64; ++i) {
const std::uint32_t s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >> 3);
const std::uint32_t s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >> 10);
w[i] = w[i - 16] + s0 + w[i - 7] + s1;
}
std::uint32_t a = state_[0];
std::uint32_t b = state_[1];
std::uint32_t c = state_[2];
std::uint32_t d = state_[3];
std::uint32_t e = state_[4];
std::uint32_t f = state_[5];
std::uint32_t g = state_[6];
std::uint32_t h = state_[7];
for (unsigned i = 0; i < 64; ++i) {
const std::uint32_t s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
const std::uint32_t ch = (e & f) ^ (~e & g);
const std::uint32_t temp1 = h + s1 + ch + kK[i] + w[i];
const std::uint32_t s0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
const std::uint32_t maj = (a & b) ^ (a & c) ^ (b & c);
const std::uint32_t temp2 = s0 + maj;
h = g;
g = f;
f = e;
e = d + temp1;
d = c;
c = b;
b = a;
a = temp1 + temp2;
}
state_[0] += a;
state_[1] += b;
state_[2] += c;
state_[3] += d;
state_[4] += e;
state_[5] += f;
state_[6] += g;
state_[7] += h;
}
void Sha256::update(const void* data, std::size_t size) {
const std::uint8_t* bytes = static_cast<const std::uint8_t*>(data);
bit_count_ += static_cast<std::uint64_t>(size) * 8u;
while (size > 0) {
const std::size_t space = 64u - buffer_used_;
const std::size_t take = size < space ? size : space;
std::memcpy(buffer_ + buffer_used_, bytes, take);
buffer_used_ += take;
bytes += take;
size -= take;
if (buffer_used_ == 64u) {
transform(buffer_);
buffer_used_ = 0;
}
}
}
void Sha256::finish(std::uint8_t out[32]) {
const std::uint64_t total_bits = bit_count_;
const std::uint8_t pad = 0x80u;
update(&pad, 1);
const std::uint8_t zero = 0x00u;
while (buffer_used_ != 56u) {
update(&zero, 1);
}
std::uint8_t length_bytes[8];
for (unsigned i = 0; i < 8; ++i) {
length_bytes[i] = static_cast<std::uint8_t>((total_bits >> (56u - i * 8u)) & 0xFFu);
}
std::memcpy(buffer_ + buffer_used_, length_bytes, 8);
buffer_used_ += 8;
transform(buffer_);
buffer_used_ = 0;
for (unsigned i = 0; i < 8; ++i) {
out[i * 4 + 0] = static_cast<std::uint8_t>((state_[i] >> 24) & 0xFFu);
out[i * 4 + 1] = static_cast<std::uint8_t>((state_[i] >> 16) & 0xFFu);
out[i * 4 + 2] = static_cast<std::uint8_t>((state_[i] >> 8) & 0xFFu);
out[i * 4 + 3] = static_cast<std::uint8_t>(state_[i] & 0xFFu);
}
}
std::string Sha256::finish_hex() {
std::uint8_t digest[32];
finish(digest);
static const char* kHex = "0123456789abcdef";
std::string text;
text.resize(64);
for (unsigned i = 0; i < 32; ++i) {
text[i * 2] = kHex[(digest[i] >> 4) & 0x0Fu];
text[i * 2 + 1] = kHex[digest[i] & 0x0Fu];
}
return text;
}
std::string sha256_hex(const void* data, std::size_t size) {
Sha256 hash;
hash.update(data, size);
return hash.finish_hex();
}
bool sha256_hex_matches(const void* data, std::size_t size, const std::string& expected_hex) {
if (expected_hex.size() != 64) {
return false;
}
std::string actual = sha256_hex(data, size);
for (std::size_t i = 0; i < 64; ++i) {
char expected = expected_hex[i];
if (expected >= 'A' && expected <= 'F') {
expected = static_cast<char>(expected - 'A' + 'a');
}
if (actual[i] != expected) {
return false;
}
}
return true;
}
} // namespace joc::crypto
+31
View File
@@ -0,0 +1,31 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <string>
namespace joc::crypto {
class Sha256 {
public:
Sha256() { reset(); }
void reset();
void update(const void* data, std::size_t size);
void finish(std::uint8_t out[32]);
std::string finish_hex();
private:
void transform(const std::uint8_t block[64]);
std::uint32_t state_[8] = {};
std::uint64_t bit_count_ = 0;
std::uint8_t buffer_[64] = {};
std::size_t buffer_used_ = 0;
};
std::string sha256_hex(const void* data, std::size_t size);
bool sha256_hex_matches(const void* data, std::size_t size, const std::string& expected_hex);
} // namespace joc::crypto
+48
View File
@@ -0,0 +1,48 @@
#pragma once
#include <string>
#include <utility>
#include "joc_core.h"
namespace joc {
class Status {
public:
Status() = default;
static Status success() { return Status(); }
static Status fail(joc_error code, std::string stage, std::string message) {
Status s;
s.code_ = code;
s.stage_ = std::move(stage);
s.message_ = std::move(message);
return s;
}
bool ok() const { return code_ == JOC_OK; }
joc_error code() const { return code_; }
const std::string& stage() const { return stage_; }
const std::string& message() const { return message_; }
private:
joc_error code_ = JOC_OK;
std::string stage_ = "none";
std::string message_;
};
// Stage names are kept as plain literals so that C++ and the Python frontend
namespace stage {
inline constexpr const char* kFoundation = "foundation";
inline constexpr const char* kEac3 = "eac3_transport";
inline constexpr const char* kEmdf = "emdf";
inline constexpr const char* kJoc = "joc";
inline constexpr const char* kOamd = "oamd";
inline constexpr const char* kDsp = "dsp";
inline constexpr const char* kRender = "render";
inline constexpr const char* kOutput = "output";
} // namespace stage
} // namespace joc
+401
View File
@@ -0,0 +1,401 @@
#include "hrtf/jochrtf.h"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include "foundation/mini_json.h"
#include "foundation/sha256.h"
#include "io/npy.h"
#include "io/zip_reader.h"
namespace joc::hrtf {
namespace {
std::string to_upper(std::string text) {
for (char& c : text) {
if (c >= 'a' && c <= 'z') {
c = static_cast<char>(c - 'a' + 'A');
}
}
return text;
}
bool is_sha256_hex(const std::string& text) {
if (text.size() != 64) {
return false;
}
for (const char c : text) {
const bool digit = c >= '0' && c <= '9';
const bool upper = c >= 'A' && c <= 'F';
if (!digit && !upper) {
return false;
}
}
return true;
}
// json.dumps(list(shape)) as the reference writes it, e.g. "[36, 2, 77]".
std::string shape_json(const std::vector<std::int64_t>& shape) {
std::string text = "[";
for (std::size_t i = 0; i < shape.size(); ++i) {
text += (i == 0 ? "" : ", ");
text += std::to_string(shape[i]);
}
text += "]";
return text;
}
Status hrtf_fail(const std::string& message) {
return Status::fail(JOC_ERR_HRTF_FORMAT, stage::kRender, message);
}
std::string payload_sha256(const std::vector<double>& centers,
const std::vector<double>& coefficients,
const std::vector<double>& delay_coefficients,
const std::vector<double>& delay_bounds) {
crypto::Sha256 hash;
const char prefix[] = "JOC-HRTF-CACHE-PAYLOAD-V1";
hash.update(prefix, sizeof(prefix) - 1);
const std::uint8_t zero = 0;
hash.update(&zero, 1);
struct Entry {
const char* name;
const char* dtype;
const std::vector<double>* values;
std::vector<std::int64_t> shape;
};
const Entry entries[4] = {
{"band_center_frequencies_hz", "<f8", &centers, {kHybridBands}},
{"coefficients", "<c16", &coefficients, {kShTerms, kEars, kHybridBands}},
{"delay_coefficients", "<f8", &delay_coefficients, {kShTerms, kEars}},
{"delay_bounds", "<f8", &delay_bounds, {2, 2}},
};
for (const Entry& entry : entries) {
const std::string name(entry.name);
const std::string dtype(entry.dtype);
const std::string shape = shape_json(entry.shape);
hash.update(name.data(), name.size());
hash.update(&zero, 1);
hash.update(dtype.data(), dtype.size());
hash.update(&zero, 1);
hash.update(shape.data(), shape.size());
hash.update(&zero, 1);
hash.update(entry.values->data(), entry.values->size() * sizeof(double));
}
return hash.finish_hex();
}
} // namespace
Status load_jochrtf(const std::string& path, Field* out) {
if (out == nullptr) {
return Status::fail(JOC_ERR_INVALID_ARGUMENT, stage::kRender, "null field");
}
io::ZipArchive archive;
std::string error;
if (!archive.open(path, &error)) {
return Status::fail(JOC_ERR_HRTF_NOT_FOUND, stage::kRender,
"cannot read compiled HRTF " + path + ": " + error);
}
// Member set must be exactly the five expected names.
static const char* kMembers[5] = {"metadata_json.npy", "band_center_frequencies_hz.npy",
"coefficients.npy", "delay_coefficients.npy",
"delay_bounds.npy"};
if (archive.entries().size() != 5u) {
return hrtf_fail("compiled HRTF cache has an invalid member set (" +
std::to_string(archive.entries().size()) + " members)");
}
for (const char* name : kMembers) {
if (archive.find(name) == nullptr) {
return hrtf_fail(std::string("compiled HRTF cache is missing ") + name);
}
}
auto read_member = [&](const char* name, std::vector<std::uint8_t>* raw,
io::NpyArray* array) -> Status {
if (!archive.read_member(name, raw, &error)) {
return hrtf_fail(std::string("compiled HRTF member ") + name + ": " + error);
}
if (!io::parse_npy(raw->data(), raw->size(), array, &error)) {
return hrtf_fail(std::string("compiled HRTF member ") + name + ": " + error);
}
if (array->fortran_order) {
return hrtf_fail(std::string("compiled HRTF member must be C-contiguous: ") + name);
}
return Status::success();
};
std::vector<std::uint8_t> raw;
io::NpyArray array;
Status status = read_member("metadata_json.npy", &raw, &array);
if (!status.ok()) {
return status;
}
std::string metadata_text;
if (!io::npy_unicode_to_utf8(array, &metadata_text, &error)) {
return hrtf_fail("compiled HRTF metadata: " + error);
}
if (metadata_text.size() > 64u * 1024u) {
return hrtf_fail("compiled HRTF metadata is too large");
}
status = read_member("band_center_frequencies_hz.npy", &raw, &array);
if (!status.ok()) {
return status;
}
if (array.descr != "<f8" || !io::npy_shape_is(array, {kHybridBands})) {
return hrtf_fail("band_center_frequencies_hz must be <f8(77,)");
}
std::vector<double> centers;
io::npy_to_double(array, &centers, &error);
status = read_member("coefficients.npy", &raw, &array);
if (!status.ok()) {
return status;
}
if (array.descr != "<c16" || !io::npy_shape_is(array, {kShTerms, kEars, kHybridBands})) {
return hrtf_fail("coefficients must be <c16(36, 2, 77)");
}
std::vector<double> coefficients;
if (!io::npy_to_double(array, &coefficients, &error)) {
return hrtf_fail("coefficients: " + error);
}
status = read_member("delay_coefficients.npy", &raw, &array);
if (!status.ok()) {
return status;
}
if (array.descr != "<f8" || !io::npy_shape_is(array, {kShTerms, kEars})) {
return hrtf_fail("delay_coefficients must be <f8(36, 2)");
}
std::vector<double> delay_coefficients;
io::npy_to_double(array, &delay_coefficients, &error);
status = read_member("delay_bounds.npy", &raw, &array);
if (!status.ok()) {
return status;
}
if (array.descr != "<f8" || !io::npy_shape_is(array, {2, 2})) {
return hrtf_fail("delay_bounds must be <f8(2, 2)");
}
std::vector<double> delay_bounds;
io::npy_to_double(array, &delay_bounds, &error);
std::vector<json::Member> members;
if (!json::parse_object(metadata_text, &members, &error)) {
return hrtf_fail("compiled HRTF metadata: " + error);
}
auto require_string = [&](const char* key, std::string* value) -> Status {
const json::Member* member = json::find(members, key);
if (member == nullptr || !json::as_string(*member, value)) {
return hrtf_fail(std::string("compiled HRTF metadata is missing ") + key);
}
return Status::success();
};
std::string magic;
std::string schema;
std::string source_sha256;
std::string cache_key;
std::string payload_hash;
std::string delay_source;
status = require_string("magic", &magic);
if (!status.ok()) { return status; }
status = require_string("cache_schema", &schema);
if (!status.ok()) { return status; }
status = require_string("source_sha256", &source_sha256);
if (!status.ok()) { return status; }
status = require_string("cache_key", &cache_key);
if (!status.ok()) { return status; }
status = require_string("payload_sha256", &payload_hash);
if (!status.ok()) { return status; }
status = require_string("delay_source", &delay_source);
if (!status.ok()) { return status; }
if (magic != kMagic) {
return hrtf_fail("compiled HRTF magic mismatch: " + magic);
}
if (schema != kCacheSchema) {
return hrtf_fail("compiled HRTF cache schema mismatch: " + schema);
}
const json::Member* version_member = json::find(members, "format_version");
long long version = -1;
if (version_member == nullptr || !json::as_integer(*version_member, &version)) {
return hrtf_fail("compiled HRTF metadata is missing format_version");
}
if (version != kFormatVersion) {
return Status::fail(JOC_ERR_HRTF_VERSION, stage::kRender,
"unsupported .jochrtf version " + std::to_string(version) +
"; rebuild it from the source SOFA");
}
out->source_sha256 = to_upper(source_sha256);
out->cache_key = to_upper(cache_key);
if (!is_sha256_hex(out->source_sha256)) {
return hrtf_fail("compiled HRTF source_sha256 is not a 64-digit digest");
}
if (!is_sha256_hex(out->cache_key)) {
return hrtf_fail("compiled HRTF cache_key is not a 64-digit digest");
}
const std::string expected = payload_sha256(centers, coefficients, delay_coefficients,
delay_bounds);
if (to_upper(payload_hash) != to_upper(expected)) {
return Status::fail(JOC_ERR_HRTF_HASH, stage::kRender,
"compiled HRTF payload hash mismatch");
}
out->payload_sha256 = to_upper(payload_hash);
const json::Member* radius_member = json::find(members, "measurement_radius_m");
double radius = 0.0;
if (radius_member == nullptr || !json::as_number(*radius_member, &radius) || radius <= 0.0) {
return hrtf_fail("compiled HRTF measurement_radius_m must be a positive number");
}
out->measurement_radius_m = radius;
const json::Member* order_member = json::find(members, "order");
long long order = 0;
if (order_member == nullptr || !json::as_integer(*order_member, &order) || order <= 0 ||
order * order > kShTerms) {
return hrtf_fail("compiled HRTF order is out of range");
}
out->order = order;
for (const double value : coefficients) {
if (!std::isfinite(value)) {
return hrtf_fail("compiled HRTF coefficients contain non-finite values");
}
}
for (const double value : delay_coefficients) {
if (!std::isfinite(value) || std::abs(value) > 48000.0 * 64.0) {
return hrtf_fail("compiled HRTF delay coefficients are out of range");
}
}
for (const double value : delay_bounds) {
if (!std::isfinite(value)) {
return hrtf_fail("compiled HRTF delay bounds contain non-finite values");
}
}
if (delay_bounds.size() == 4u && delay_bounds[0] > delay_bounds[1]) {
return hrtf_fail("compiled HRTF delay bounds are inverted");
}
if (const json::Member* member = json::find(members, "compiler_version")) {
json::as_string(*member, &out->compiler_version);
}
if (const json::Member* member = json::find(members, "phase_policy_version")) {
json::as_string(*member, &out->phase_policy_version);
}
if (const json::Member* member = json::find(members, "sh_convention")) {
json::as_string(*member, &out->sh_convention);
}
if (const json::Member* member = json::find(members, "source_display_name")) {
json::as_string(*member, &out->source_display_name);
}
if (const json::Member* member = json::find(members, "projection_ridge")) {
json::as_number(*member, &out->projection_ridge);
}
if (const json::Member* member = json::find(members, "spherical_harmonic_ridge")) {
json::as_number(*member, &out->spherical_harmonic_ridge);
}
if (const json::Member* member = json::find(members, "fit_report")) {
out->fit_report_json = member->raw;
}
if (const json::Member* member = json::find(members, "filterbank")) {
out->filterbank_json = member->raw;
}
out->delay_source = delay_source;
out->metadata_json = metadata_text;
out->coefficients = std::move(coefficients);
out->delay_coefficients = std::move(delay_coefficients);
out->delay_bounds = std::move(delay_bounds);
out->band_centers_hz = std::move(centers);
return Status::success();
}
Status load_kernels(const std::string& npz_path, Kernels* out) {
if (out == nullptr) {
return Status::fail(JOC_ERR_INVALID_ARGUMENT, stage::kRender, "null kernels");
}
io::ZipArchive archive;
std::string error;
if (!archive.open(npz_path, &error)) {
return Status::fail(JOC_ERR_HRTF_NOT_FOUND, stage::kRender,
"cannot read kernel tables " + npz_path + ": " + error);
}
struct Request {
const char* member;
const char* shape_text;
std::vector<std::int64_t> shape;
};
const Request requests[6] = {
{"qmf_analysis_coefficients.npy", "<f4", {64, 10}},
{"hybrid_analysis_low_kernel.npy", "<f4", {3, 2, 13, 16, 2}},
{"hybrid_synthesis_indices.npy", "<i2", {154, 4}},
{"hybrid_synthesis_values.npy", "<f4", {154}},
{"qmf_synthesis_basis.npy", "<f8", {64, 4, 128}},
{"qmf_synthesis_taps.npy", "<f8", {64, 10, 4}},
};
std::vector<std::uint8_t> raw;
std::vector<std::uint8_t> ordered;
for (const Request& request : requests) {
const std::string name = request.member;
if (!archive.read_member(name, &raw, &error)) {
return Status::fail(JOC_ERR_HRTF_FORMAT, stage::kRender,
"kernel table member " + name + ": " + error);
}
io::NpyArray array;
if (!io::parse_npy(raw.data(), raw.size(), &array, &error)) {
return Status::fail(JOC_ERR_HRTF_FORMAT, stage::kRender,
"kernel table member " + name + ": " + error);
}
if (array.descr != request.shape_text || !io::npy_shape_is(array, request.shape)) {
return Status::fail(JOC_ERR_HRTF_FORMAT, stage::kRender,
"kernel table member " + name + " has an unexpected dtype/shape");
}
// Logical C order: required because the reused kernel indexes the hybrid
// synthesis table row-major while the shipped member is Fortran-order.
if (!io::npy_to_c_order(array, &ordered, &error)) {
return Status::fail(JOC_ERR_HRTF_FORMAT, stage::kRender,
"kernel table member " + name + ": " + error);
}
const std::size_t count = array.element_count();
if (std::strcmp(request.member, "qmf_analysis_coefficients.npy") == 0) {
std::vector<float> values(count);
std::memcpy(values.data(), ordered.data(), count * sizeof(float));
out->qmf_analysis.assign(values.begin(), values.end());
} else if (std::strcmp(request.member, "hybrid_analysis_low_kernel.npy") == 0) {
std::vector<float> values(count);
std::memcpy(values.data(), ordered.data(), count * sizeof(float));
out->hybrid_low.assign(values.begin(), values.end());
} else if (std::strcmp(request.member, "hybrid_synthesis_indices.npy") == 0) {
out->hybrid_indices.resize(count);
std::memcpy(out->hybrid_indices.data(), ordered.data(), count * sizeof(std::int16_t));
} else if (std::strcmp(request.member, "hybrid_synthesis_values.npy") == 0) {
std::vector<float> values(count);
std::memcpy(values.data(), ordered.data(), count * sizeof(float));
out->hybrid_values.assign(values.begin(), values.end());
} else if (std::strcmp(request.member, "qmf_synthesis_basis.npy") == 0) {
std::memcpy(out->qmf_basis.empty() ? (out->qmf_basis.resize(count), out->qmf_basis.data())
: out->qmf_basis.data(),
ordered.data(), count * sizeof(double));
out->qmf_basis.resize(count);
} else {
out->qmf_taps.resize(count);
std::memcpy(out->qmf_taps.data(), ordered.data(), count * sizeof(double));
}
}
out->hybrid_count = static_cast<std::uint32_t>(out->hybrid_values.size());
if (out->hybrid_count == 0u) {
return Status::fail(JOC_ERR_HRTF_FORMAT, stage::kRender,
"kernel tables contain no hybrid synthesis entries");
}
return Status::success();
}
} // namespace joc::hrtf
+66
View File
@@ -0,0 +1,66 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
#include "foundation/status.h"
namespace joc::hrtf {
inline constexpr int kShTerms = 36;
inline constexpr int kEars = 2;
inline constexpr int kHybridBands = 77;
inline constexpr int kFormatVersion = 1;
inline constexpr const char* kMagic = "JOC-HRTF-CACHE";
inline constexpr const char* kCacheSchema = "joc-compiled-hrtf-v1";
struct Field {
std::vector<double> coefficients;
std::vector<double> delay_coefficients;
std::vector<double> delay_bounds;
std::vector<double> band_centers_hz;
double measurement_radius_m = 1.0;
long long order = 5;
std::string source_sha256;
std::string cache_key;
std::string payload_sha256;
std::string delay_source;
std::string compiler_version;
std::string phase_policy_version;
std::string sh_convention;
std::string filterbank_json;
std::string metadata_json;
// Compile-side metadata, needed to write the cache back out unchanged.
std::string source_display_name;
std::string fit_report_json;
double projection_ridge = 0.0;
double spherical_harmonic_ridge = 0.0;
};
Status load_jochrtf(const std::string& path, Field* out);
// Binaural filterbank kernels, as the reused kernel expects them (C order, the
// exact dtypes of the ABI parameters).
struct Kernels {
std::vector<double> qmf_analysis;
std::vector<double> hybrid_low;
std::vector<std::int16_t> hybrid_indices;
std::vector<double> hybrid_values;
std::vector<double> qmf_basis;
std::vector<double> qmf_taps;
std::uint32_t hybrid_count = 0;
};
// Loads a kernel-table archive. The file path is an override for verification;
// the shipped tables are embedded (see builtin_kernels) so no data file is needed.
// The Fortran-order index member is transposed into C order on purpose: the reused
// kernel indexes the hybrid synthesis table row-major.
Status load_kernels(const std::string& npz_path, Kernels* out);
// The public filterbank tables compiled into the library (identical values to the
// archive the file loader accepts; the unit test checks their hashes).
const Kernels& builtin_kernels();
} // namespace joc::hrtf
File diff suppressed because it is too large Load Diff
+610
View File
@@ -0,0 +1,610 @@
#include "hrtf/public_filterbank.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "foundation/fft.h"
#include "simd/simd.h"
namespace joc::hrtf {
namespace {
constexpr double kPi = 3.14159265358979323846;
constexpr std::size_t kQmfLength = dsp::kQmfFftSize;
constexpr int kQmfTaps = 10;
constexpr int kSynthesisRank = 4;
constexpr int kSynthesisTaps = 10;
// ----------------------------------------------------------- filterbank -----
// One shared forward plan for the 128-point QMF transform. The analysis bank runs
// it 2 * slots * channels times per chunk, so the twiddle recurrence is built once
// instead of being re-derived inside every butterfly.
const dsp::FftPlan& qmf_fft_plan() {
static const dsp::FftPlan plan(dsp::kQmfFftSize, false);
return plan;
}
// Public 64-band complex QMF analysis (public_filterbank.QmfAnalysis).
class QmfAnalysis {
public:
static_assert(static_cast<std::size_t>(kQmfBands) == simd::kQmfAnalysisBands,
"the dispatched accumulate is written for this band count");
QmfAnalysis(const Kernels& kernels, std::size_t channels)
: channels_(channels), coefficients_(kernels.qmf_analysis) {
history_.assign(9u * channels_ * kQmfBands, 0.0);
// The polyphase MAC consumes one coefficient per band, so the shipped
// [band][tap] layout makes its inner loop a stride-10 gather. Transposing
// once here turns that into a contiguous AXPY. The coefficient values and
// the accumulation order are untouched, so the sums are bit-identical.
coefficients_by_lag_.resize(static_cast<std::size_t>(kQmfTaps) * kQmfBands);
for (int band = 0; band < kQmfBands; ++band) {
for (int tap = 0; tap < kQmfTaps; ++tap) {
coefficients_by_lag_[static_cast<std::size_t>(tap) * kQmfBands +
static_cast<std::size_t>(band)] =
coefficients_[static_cast<std::size_t>(band) * kQmfTaps +
static_cast<std::size_t>(tap)];
}
}
premultiply_.resize(kQmfBands);
post_.resize(kQmfBands);
even_post_.resize(kQmfBands);
for (int band = 0; band < kQmfBands; ++band) {
const double phase = static_cast<double>(band);
premultiply_[static_cast<std::size_t>(band)] =
std::polar(1.0, -kPi * phase / 128.0);
post_[static_cast<std::size_t>(band)] =
std::polar(1.0, -3.0 * (phase + 0.5) * kPi / 128.0);
even_post_[static_cast<std::size_t>(band)] =
Complex(0.0, band % 2 == 0 ? 1.0 : -1.0);
}
}
void reset() { std::fill(history_.begin(), history_.end(), 0.0); }
// samples: [slots*64, channels]; output: [slots, channels, 64] complex.
void process(const std::vector<double>& samples, std::size_t slots,
std::vector<Complex>* output) {
const std::size_t joined_slots = 9u + slots;
const std::size_t history_size = 9u * channels_ * kQmfBands;
const std::size_t joined_size = joined_slots * channels_ * kQmfBands;
// The joined window is filled completely -- the history lands in its first
// 9 * channels * 64 entries and the new samples in the rest -- so it is a
// reusable scratch buffer rather than a fresh zero-filled allocation. The
// history tail is taken by index instead of from end(), because the buffer may
// be longer than the window this call uses.
if (joined_.size() < joined_size) {
joined_.resize(joined_size);
}
std::copy(history_.begin(), history_.end(), joined_.begin());
std::copy(samples.begin(), samples.begin() + static_cast<std::ptrdiff_t>(slots * channels_ * kQmfBands),
joined_.begin() + static_cast<std::ptrdiff_t>(history_size));
// The two polyphase accumulators are read before they are written, so their
// zero fill is load-bearing and stays; only the per-call allocation goes.
const std::size_t accumulator_size = slots * channels_ * kQmfBands;
if (even_.size() < accumulator_size) {
even_.resize(accumulator_size);
}
if (odd_.size() < accumulator_size) {
odd_.resize(accumulator_size);
}
std::fill(even_.begin(), even_.begin() + static_cast<std::ptrdiff_t>(accumulator_size), 0.0);
std::fill(odd_.begin(), odd_.begin() + static_cast<std::ptrdiff_t>(accumulator_size), 0.0);
// The ten lags are ten accumulate passes over the same 64 bands with one
// shared coefficient row; the bands are independent accumulations of a
// single product each, so they are what the dispatched kernel puts in its
// lanes, and every band keeps the caller's own multiply-then-add.
//
// Slots are processed in blocks, with the lag loop inside: one lag pass
// touches every source row once, so running the ten passes over the whole
// chunk re-reads the joined window ten times -- at 1536 slots that is
// hundreds of megabytes per chunk and the loop ends up bound by memory, not
// by arithmetic. A block's ten lag passes instead slide over a window of
// (block + 9) rows that stays in the second-level cache. Lags still run in
// ascending order inside a block, which is the order each output's sum is
// formed in, so nothing about the arithmetic changes.
constexpr std::size_t kSlotBlock = 32;
for (std::size_t first = 0u; first < slots; first += kSlotBlock) {
const std::size_t block = std::min(kSlotBlock, slots - first);
for (int lag = 0; lag < kQmfTaps; ++lag) {
std::vector<double>& target = (lag % 2 == 0) ? even_ : odd_;
const double* row =
coefficients_by_lag_.data() + static_cast<std::size_t>(lag) * kQmfBands;
const std::size_t source_slot = 9u - static_cast<std::size_t>(lag) + first;
simd::qmf_analysis_taps(
target.data() + first * channels_ * kQmfBands,
joined_.data() + source_slot * channels_ * kQmfBands, row,
block * channels_);
}
}
std::copy(joined_.begin() + static_cast<std::ptrdiff_t>(joined_size - history_size),
joined_.begin() + static_cast<std::ptrdiff_t>(joined_size), history_.begin());
// Every output element is assigned below, so the size is all that has to be
// established; a resize of an already correctly sized buffer touches nothing.
output->resize(slots * channels_ * kQmfBands);
std::array<Complex, dsp::kQmfFftSize> even_spectrum{};
std::array<Complex, dsp::kQmfFftSize> odd_spectrum{};
for (std::size_t slot = 0u; slot < slots; ++slot) {
for (std::size_t channel = 0u; channel < channels_; ++channel) {
const double* even_values = even_.data() + (slot * channels_ + channel) * kQmfBands;
const double* odd_values = odd_.data() + (slot * channels_ + channel) * kQmfBands;
transform(even_values, &even_spectrum);
transform(odd_values, &odd_spectrum);
Complex* destination =
output->data() + (slot * channels_ + channel) * kQmfBands;
for (int band = 0; band < kQmfBands; ++band) {
destination[band] = odd_spectrum[static_cast<std::size_t>(band)] +
even_spectrum[static_cast<std::size_t>(band)] *
even_post_[static_cast<std::size_t>(band)];
}
}
}
}
private:
void transform(const double* values, std::array<Complex, dsp::kQmfFftSize>* spectrum) {
for (int band = 0; band < kQmfBands; ++band) {
(*spectrum)[static_cast<std::size_t>(band)] =
Complex(values[band], 0.0) * premultiply_[static_cast<std::size_t>(band)];
}
for (int index = kQmfBands; index < dsp::kQmfFftSize; ++index) {
(*spectrum)[static_cast<std::size_t>(index)] = Complex(0.0, 0.0);
}
dsp::fft_radix2(spectrum, qmf_fft_plan());
for (int band = 0; band < kQmfBands; ++band) {
(*spectrum)[static_cast<std::size_t>(band)] *= post_[static_cast<std::size_t>(band)];
}
}
std::size_t channels_;
std::vector<double> coefficients_; // [64][10]
std::vector<double> coefficients_by_lag_; // [10][64], the same values transposed
std::vector<double> history_; // [9][channels][64]
std::vector<double> joined_; // scratch, [9 + slots][channels][64]
std::vector<double> even_; // scratch, [slots][channels][64], zeroed per call
std::vector<double> odd_; // scratch, [slots][channels][64], zeroed per call
std::vector<Complex> premultiply_;
std::vector<Complex> post_;
std::vector<Complex> even_post_;
};
// Sparse 64-QMF to 77-hybrid analysis (public_filterbank.HybridAnalysis).
class HybridAnalysis {
public:
HybridAnalysis(const Kernels& kernels, std::size_t channels)
: channels_(channels), low_kernel_(kernels.hybrid_low) {
history_.assign(12u * channels_ * 3u * 2u, 0.0);
high_history_.assign(6u * channels_ * 61u, Complex(0.0, 0.0));
// The dispatched join walks one term at a time and adds its 32 weights to
// 32 outputs, so the shipped [tap][band][component] table is regrouped to
// the term order the caller accumulates in. Same weights, same order.
const std::size_t outputs = simd::kHybridOutputs;
low_by_term_.resize(simd::kHybridTerms * outputs);
for (int lag = 0; lag < 13; ++lag) {
for (int point = 0; point < 3; ++point) {
for (int input = 0; input < 2; ++input) {
const std::size_t term =
(static_cast<std::size_t>(lag) * 3u + static_cast<std::size_t>(point)) * 2u +
static_cast<std::size_t>(input);
const std::size_t source = (static_cast<std::size_t>(point) * 2u +
static_cast<std::size_t>(input)) * 13u +
static_cast<std::size_t>(lag);
for (std::size_t output = 0u; output < outputs; ++output) {
low_by_term_[term * outputs + output] =
low_kernel_[source * outputs + output];
}
}
}
}
low_values_.resize(simd::kHybridJoinBlock * simd::kHybridTerms);
low_out_.resize(simd::kHybridJoinBlock * outputs);
}
void reset() {
std::fill(history_.begin(), history_.end(), 0.0);
std::fill(high_history_.begin(), high_history_.end(), Complex(0.0, 0.0));
}
// qmf: [slots, channels, 64]; output: [slots, channels, 77] complex.
void process(const std::vector<Complex>& qmf, std::size_t slots,
std::vector<Complex>* output) {
const std::size_t joined_slots = 12u + slots;
const std::size_t history_size = 12u * channels_ * 6u;
const std::size_t joined_size = joined_slots * channels_ * 3u * 2u;
// Both the joined window and the pending high-band history are written in full
// before they are read, so they are reused scratch buffers; the history tail is
// taken by index because the buffer can be longer than this call's window.
if (joined_.size() < joined_size) {
joined_.resize(joined_size);
}
std::copy(history_.begin(), history_.end(), joined_.begin());
for (std::size_t slot = 0u; slot < slots; ++slot) {
for (std::size_t channel = 0u; channel < channels_; ++channel) {
const Complex* source = qmf.data() + (slot * channels_ + channel) * kQmfBands;
double* destination =
joined_.data() + ((12u + slot) * channels_ + channel) * 6u;
for (int band = 0; band < 3; ++band) {
destination[static_cast<std::size_t>(band) * 2u] = source[band].real();
destination[static_cast<std::size_t>(band) * 2u + 1u] = source[band].imag();
}
}
}
// The low bands are accumulated in a register block and written straight into
// the output, and the high bands are written by the pass below; between them
// every one of the 77 bands is assigned, so only the size has to be set.
output->resize(slots * channels_ * kHybridBands);
// The thirteen taps are summed in a per-output register block and the low
// bands are written straight into the output. Keeping a separate low plane
// and then copying it into the output re-streams tens of megabytes per chunk
// for nothing, and only the first kHybridLow bands are ever touched. The
// join itself is dispatched (see src/simd/simd.h): the 32 outputs of a
// row are 32 independent accumulations over the same 78 terms, which is what
// shares a vector. Every lane keeps the caller's term order -- lag, then
// point, then input -- and its two roundings, and skips exactly the terms
// this loop skips. Rows are staged in blocks so the gathered values do not
// spill out of the first-level cache.
const std::size_t hybrid_rows = slots * channels_;
const std::size_t block = simd::kHybridJoinBlock;
const std::size_t terms = simd::kHybridTerms;
for (std::size_t first = 0u; first < hybrid_rows; first += block) {
const std::size_t count = std::min(block, hybrid_rows - first);
for (std::size_t index = 0u; index < count; ++index) {
const std::size_t row = first + index;
const std::size_t slot = row / channels_;
const std::size_t channel = row % channels_;
double* staged = low_values_.data() + index * terms;
for (int lag = 0; lag < 13; ++lag) {
const std::size_t source_slot = 12u - static_cast<std::size_t>(lag) + slot;
const double* source =
joined_.data() + (source_slot * channels_ + channel) * 6u;
for (int point = 0; point < 3; ++point) {
for (int input = 0; input < 2; ++input) {
staged[(static_cast<std::size_t>(lag) * 3u +
static_cast<std::size_t>(point)) * 2u +
static_cast<std::size_t>(input)] =
source[static_cast<std::size_t>(point) * 2u +
static_cast<std::size_t>(input)];
}
}
}
}
simd::hybrid_low_join(low_values_.data(), low_by_term_.data(),
low_out_.data(), count);
for (std::size_t index = 0u; index < count; ++index) {
Complex* destination = output->data() + (first + index) * kHybridBands;
const double* values = low_out_.data() + index * simd::kHybridOutputs;
for (int band = 0; band < kHybridLow; ++band) {
destination[band] = Complex(values[static_cast<std::size_t>(band) * 2u],
values[static_cast<std::size_t>(band) * 2u + 1u]);
}
}
}
std::copy(joined_.begin() + static_cast<std::ptrdiff_t>(joined_size - history_size),
joined_.begin() + static_cast<std::ptrdiff_t>(joined_size), history_.begin());
// The high bands pass through unchanged but delayed by the six slots of
// history the reference concatenates in front of them. Only the last six
// entries of that concatenation survive into high_history_, so a six-entry
// register replaces the (6 + slots) plane and its full copy. Note the
// output reads the concatenation at index `slot`, not `6 + slot`, so the
// first six output slots come from the history: that offset is part of the
// current output and is preserved verbatim.
for (std::size_t slot = 0u; slot < slots; ++slot) {
for (std::size_t channel = 0u; channel < channels_; ++channel) {
Complex* destination = output->data() +
(slot * channels_ + channel) * kHybridBands + kHybridLow;
if (slot < 6u) {
const Complex* source =
high_history_.data() + (slot * channels_ + channel) * 61u;
for (int band = 0; band < 61; ++band) {
destination[band] = source[band];
}
} else {
const Complex* source =
qmf.data() + ((slot - 6u) * channels_ + channel) * kQmfBands;
for (int band = 3; band < kQmfBands; ++band) {
destination[static_cast<std::size_t>(band - 3)] = source[band];
}
}
}
}
// Every entry of the pending high-band history is written here, so it is a
// reusable scratch buffer; the copy into the live history is kept as it was.
if (next_high_history_.size() < 6u * channels_ * 61u) {
next_high_history_.resize(6u * channels_ * 61u);
}
for (std::size_t entry = 0u; entry < 6u; ++entry) {
const std::size_t combined = slots + entry;
for (std::size_t channel = 0u; channel < channels_; ++channel) {
Complex* destination =
next_high_history_.data() + (entry * channels_ + channel) * 61u;
if (combined < 6u) {
const Complex* source =
high_history_.data() + (combined * channels_ + channel) * 61u;
for (int band = 0; band < 61; ++band) {
destination[band] = source[band];
}
} else {
const Complex* source =
qmf.data() + ((combined - 6u) * channels_ + channel) * kQmfBands;
for (int band = 3; band < kQmfBands; ++band) {
destination[static_cast<std::size_t>(band - 3)] = source[band];
}
}
}
}
std::copy(next_high_history_.begin(), next_high_history_.end(), high_history_.begin());
}
private:
std::size_t channels_;
std::vector<double> low_kernel_; // [3][2][13][16][2]
std::vector<double> low_by_term_; // [78][32], the same weights in the caller's term order
std::vector<double> low_values_; // scratch, [block][78]
std::vector<double> low_out_; // scratch, [block][32]
std::vector<double> history_; // [12][channels][3][2]
std::vector<Complex> high_history_; // [6][channels][61]
std::vector<double> joined_; // scratch, [12 + slots][channels][3][2]
std::vector<Complex> next_high_history_; // scratch, [6][channels][61]
};
// Instantaneous sparse 77-hybrid to 64-QMF synthesis map.
class HybridSynthesis {
public:
explicit HybridSynthesis(const Kernels& kernels) {
const std::size_t rows = kernels.hybrid_indices.size() / 4u;
mapping_.reserve(rows);
for (std::size_t index = 0u; index < rows; ++index) {
Entry entry;
for (int field = 0; field < 4; ++field) {
entry.index[static_cast<std::size_t>(field)] =
kernels.hybrid_indices[index * 4u + static_cast<std::size_t>(field)];
}
entry.gain = kernels.hybrid_values[index];
mapping_.push_back(entry);
}
}
// hybrid: [slots, channels, 77]; output: [slots, channels, 64] complex.
// The sparse map moves a real or imaginary part of one band into a real or
// imaginary part of another, so the two components are accumulated apart.
void process(const std::vector<Complex>& hybrid, std::size_t slots, std::size_t channels,
std::vector<Complex>* output) const {
const std::size_t rows = slots * channels;
std::vector<double> real(rows * kQmfBands, 0.0);
std::vector<double> imaginary(rows * kQmfBands, 0.0);
for (std::size_t slot = 0u; slot < slots; ++slot) {
for (std::size_t channel = 0u; channel < channels; ++channel) {
const std::size_t row = slot * channels + channel;
const Complex* source = hybrid.data() + row * kHybridBands;
for (const Entry& entry : mapping_) {
const double value = entry.index[1] == 0u ? source[entry.index[0]].real()
: source[entry.index[0]].imag();
if (value == 0.0) {
continue;
}
double* destination =
(entry.index[3] == 0u ? real.data() : imaginary.data()) + row * kQmfBands;
destination[entry.index[2]] += value * entry.gain;
}
}
}
// Every output element is assigned from the two accumulators below, so the
// zero fill that `assign` performed was dead; only the size is needed.
output->resize(rows * kQmfBands);
for (std::size_t index = 0u; index < output->size(); ++index) {
(*output)[index] = Complex(real[index], imaginary[index]);
}
}
private:
struct Entry {
std::size_t index[4] = {0u, 0u, 0u, 0u};
double gain = 0.0;
};
std::vector<Entry> mapping_;
};
// Rank-4 64-band synthesis.
class QmfSynthesis {
public:
QmfSynthesis(const Kernels& kernels, std::size_t channels)
: channels_(channels), basis_(kernels.qmf_basis), taps_(kernels.qmf_taps) {
history_.assign(9u * channels_ * kQmfBands * kSynthesisRank, 0.0);
// The dispatched basis kernel reads the four ranks of one (band, tap) as
// one vector, so the shipped [band][rank][tap] table is reordered once
// here. The weights are the same doubles, only their order differs.
const std::size_t bands = static_cast<std::size_t>(kQmfBands);
const std::size_t ranks = static_cast<std::size_t>(kSynthesisRank);
const std::size_t taps = dsp::kQmfFftSize;
basis_by_tap_.resize(bands * taps * ranks);
for (std::size_t band = 0u; band < bands; ++band) {
for (std::size_t tap = 0u; tap < taps; ++tap) {
for (std::size_t rank = 0u; rank < ranks; ++rank) {
basis_by_tap_[(band * taps + tap) * ranks + rank] =
basis_[(band * ranks + rank) * taps + tap];
}
}
}
}
void reset() { std::fill(history_.begin(), history_.end(), 0.0); }
// qmf: [slots, channels, 64]; output: [slots*64, channels] real.
void process(const std::vector<Complex>& qmf, std::size_t slots, std::vector<double>* output) {
const std::size_t rows = slots * channels_;
// [row][band][component] staging for the basis application. Both staging
// planes and the joined window are reusable scratch: every element of each is
// written before it is read, so the buffers are sized once and kept instead of
// being allocated and zero-filled on every call.
const std::size_t flat_size = rows * dsp::kQmfFftSize;
if (flat_.size() < flat_size) {
flat_.resize(flat_size);
}
for (std::size_t row = 0u; row < rows; ++row) {
for (int band = 0; band < kQmfBands; ++band) {
flat_[row * dsp::kQmfFftSize + static_cast<std::size_t>(band) * 2u] =
qmf[row * kQmfBands + static_cast<std::size_t>(band)].real();
flat_[row * dsp::kQmfFftSize + static_cast<std::size_t>(band) * 2u + 1u] =
qmf[row * kQmfBands + static_cast<std::size_t>(band)].imag();
}
}
// The sums are written straight into the joined window: the destination index
// is known up front, the summation order is untouched, and the application
// itself is dispatched -- the four ranks of a band are four independent dot
// products over the same 128 values, so they share a vector while every lane
// keeps the tap order and the two roundings of `sum +=`.
const std::size_t history_size = 9u * channels_ * kQmfBands * kSynthesisRank;
const std::size_t joined_size = history_size + rows * kQmfBands * kSynthesisRank;
if (joined_.size() < joined_size) {
joined_.resize(joined_size);
}
std::copy(history_.begin(), history_.end(), joined_.begin());
simd::qmf_synthesis_basis(flat_.data(), basis_by_tap_.data(),
joined_.data() + history_size, rows);
std::copy(joined_.begin() + static_cast<std::ptrdiff_t>(joined_size - history_size),
joined_.begin() + static_cast<std::ptrdiff_t>(joined_size), history_.begin());
output->assign(rows * kQmfBands, 0.0);
for (int lag = 0; lag < kSynthesisTaps; ++lag) {
for (std::size_t slot = 0u; slot < slots; ++slot) {
const std::size_t source_slot = 9u - static_cast<std::size_t>(lag) + slot;
for (std::size_t channel = 0u; channel < channels_; ++channel) {
const double* source =
joined_.data() +
(source_slot * channels_ + channel) * kQmfBands * kSynthesisRank;
double* destination =
output->data() + (slot * channels_ + channel) * kQmfBands;
for (int band = 0; band < kQmfBands; ++band) {
double sum = 0.0;
for (int rank = 0; rank < kSynthesisRank; ++rank) {
sum += source[static_cast<std::size_t>(band) * kSynthesisRank +
static_cast<std::size_t>(rank)] *
taps_[(static_cast<std::size_t>(band) * kSynthesisTaps +
static_cast<std::size_t>(lag)) * kSynthesisRank +
static_cast<std::size_t>(rank)];
}
destination[band] += sum;
}
}
}
}
}
private:
std::size_t channels_;
std::vector<double> basis_; // [64][4][128]
std::vector<double> basis_by_tap_; // [64][128][4], the same weights transposed
std::vector<double> taps_; // [64][10][4]
std::vector<double> history_; // [9][channels][64][4]
std::vector<double> flat_; // scratch, [rows][128], fully written per call
std::vector<double> joined_; // scratch, [9 + slots][channels][64][4]
};
// public_filterbank.PublicAnalysis77.process: [N, channels] -> [N/64, channels, 77].
void analysis_77(const std::vector<double>& samples, std::size_t slots,
std::size_t channels, QmfAnalysis& qmf,
HybridAnalysis& hybrid_analysis, std::vector<Complex>* hybrid) {
std::vector<double> hops(slots * channels * kQmfHop, 0.0);
for (std::size_t slot = 0u; slot < slots; ++slot) {
for (std::size_t channel = 0u; channel < channels; ++channel) {
for (int index = 0; index < kQmfHop; ++index) {
hops[(slot * channels + channel) * kQmfHop + static_cast<std::size_t>(index)] =
samples[(slot * kQmfHop + static_cast<std::size_t>(index)) * channels + channel];
}
}
}
std::vector<Complex> qmf_bands;
qmf.process(hops, slots, &qmf_bands);
hybrid_analysis.process(qmf_bands, slots, hybrid);
}
// public_filterbank.PublicSynthesis77.process: [slots, channels, 77] -> [slots*64, channels].
void synthesis_77(const std::vector<Complex>& hybrid, std::size_t slots,
std::size_t channels, const HybridSynthesis& synthesis,
QmfSynthesis& qmf, std::vector<double>* time) {
std::vector<Complex> qmf_bands;
synthesis.process(hybrid, slots, channels, &qmf_bands);
std::vector<double> samples;
qmf.process(qmf_bands, slots, &samples);
// The reference transposes (slots, channels, 64) to sample-major output.
time->assign(samples.size(), 0.0);
for (std::size_t slot = 0u; slot < slots; ++slot) {
for (std::size_t channel = 0u; channel < channels; ++channel) {
for (int band = 0; band < kQmfBands; ++band) {
(*time)[(slot * kQmfHop + static_cast<std::size_t>(band)) * channels + channel] =
samples[(slot * channels + channel) * kQmfBands + static_cast<std::size_t>(band)];
}
}
}
}
} // namespace
struct PublicFilterbank::Impl {
Impl(const Kernels& kernels, std::size_t channels)
: channels(channels), qmf(kernels, channels), hybrid_analysis(kernels, channels),
hybrid_synthesis(kernels), qmf_synthesis(kernels, channels) {}
std::size_t channels;
QmfAnalysis qmf;
HybridAnalysis hybrid_analysis;
HybridSynthesis hybrid_synthesis;
QmfSynthesis qmf_synthesis;
};
PublicFilterbank::PublicFilterbank(const Kernels& kernels, std::size_t channels)
: impl_(std::make_unique<Impl>(kernels, channels)) {}
PublicFilterbank::~PublicFilterbank() = default;
void PublicFilterbank::reset() {
impl_->qmf.reset();
impl_->hybrid_analysis.reset();
impl_->qmf_synthesis.reset();
}
void PublicFilterbank::analyze_full_rate(const std::vector<double>& samples, std::size_t slots,
std::vector<Complex>* hybrid) {
analysis_77(samples, slots, impl_->channels, impl_->qmf, impl_->hybrid_analysis, hybrid);
}
void PublicFilterbank::synthesize_full_rate(const std::vector<Complex>& hybrid, std::size_t slots,
std::vector<double>* time) {
synthesis_77(hybrid, slots, impl_->channels, impl_->hybrid_synthesis, impl_->qmf_synthesis,
time);
}
void PublicFilterbank::analyze_qmf(const std::vector<double>& hops, std::size_t slots,
std::vector<Complex>* qmf) {
impl_->qmf.process(hops, slots, qmf);
}
void PublicFilterbank::analyze_hybrid(const std::vector<Complex>& qmf, std::size_t slots,
std::vector<Complex>* hybrid) {
impl_->hybrid_analysis.process(qmf, slots, hybrid);
}
void PublicFilterbank::synthesize_hybrid(const std::vector<Complex>& hybrid, std::size_t slots,
std::vector<Complex>* qmf) {
impl_->hybrid_synthesis.process(hybrid, slots, impl_->channels, qmf);
}
void PublicFilterbank::synthesize_qmf(const std::vector<Complex>& qmf, std::size_t slots,
std::vector<double>* time) {
impl_->qmf_synthesis.process(qmf, slots, time);
}
} // namespace joc::hrtf
+55
View File
@@ -0,0 +1,55 @@
#pragma once
#include <complex>
#include <cstddef>
#include <memory>
#include <vector>
#include "hrtf/jochrtf.h"
// Public 64-QMF / 77-hybrid filterbank, shared by the SOFA field compiler and the
// Rosella renderer (upstream public_filterbank.py and rosella_filterbank.py are
// the same bank). Everything is float64/complex128, as the reference computes it,
// and the stateful half-steps are exposed because Rosella drives them directly.
namespace joc::hrtf {
inline constexpr int kQmfBands = 64;
inline constexpr int kQmfHop = 64;
inline constexpr int kHybridLow = 16;
inline constexpr int kHybridBandCount = 77;
inline constexpr int kLatencySamples = 961;
using Complex = std::complex<double>;
class PublicFilterbank {
public:
PublicFilterbank(const Kernels& kernels, std::size_t channels);
~PublicFilterbank();
PublicFilterbank(const PublicFilterbank&) = delete;
PublicFilterbank& operator=(const PublicFilterbank&) = delete;
void reset();
// Full-rate [slots*64, channels] -> hybrid [slots, channels, 77].
void analyze_full_rate(const std::vector<double>& samples, std::size_t slots,
std::vector<Complex>* hybrid);
// Hybrid [slots, channels, 77] -> full-rate [slots*64, channels].
void synthesize_full_rate(const std::vector<Complex>& hybrid, std::size_t slots,
std::vector<double>* time);
// The stateful half-steps, in the order the reference runs them.
void analyze_qmf(const std::vector<double>& hops, std::size_t slots,
std::vector<Complex>* qmf);
void analyze_hybrid(const std::vector<Complex>& qmf, std::size_t slots,
std::vector<Complex>* hybrid);
void synthesize_hybrid(const std::vector<Complex>& hybrid, std::size_t slots,
std::vector<Complex>* qmf);
void synthesize_qmf(const std::vector<Complex>& qmf, std::size_t slots,
std::vector<double>* time);
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace joc::hrtf
+535
View File
@@ -0,0 +1,535 @@
#include "hrtf/rosella_model.h"
#include <algorithm>
#include <cmath>
#include <cstring>
#include <fstream>
#include <string>
#include <utility>
#include <vector>
#include "foundation/fs_utf8.h"
#include "foundation/mini_json.h"
#include "foundation/sha256.h"
namespace joc::hrtf {
namespace {
// The model's fixed-point lane scale: every stored value is a Q15 integer.
constexpr float kQ15 = 1.0f / 32768.0f;
Status model_fail(joc_error code, const std::string& message) {
return Status::fail(code, stage::kRender, message);
}
float q15(std::int32_t value) { return static_cast<float>(value) * kQ15; }
float q15_exp(std::int32_t value, int exponent) {
return q15(value) * static_cast<float>(std::ldexp(1.0, exponent));
}
std::uint16_t low16(std::int32_t value) {
return static_cast<std::uint16_t>(static_cast<std::uint32_t>(value) & 0xFFFFu);
}
std::string trim(const std::string& text) {
const std::size_t begin = text.find_first_not_of(" \t\r\n");
const std::size_t end = text.find_last_not_of(" \t\r\n");
return begin == std::string::npos ? std::string() : text.substr(begin, end - begin + 1u);
}
// The lane array is read straight out of the JSON text: it is one flat list of
// integers, and building a 15691-node DOM for it would only cost time.
bool parse_int_array(const std::string& raw, std::vector<std::int32_t>* out, std::string* error) {
out->clear();
const char* cursor = raw.c_str();
const char* end = cursor + raw.size();
while (cursor < end && *cursor != '[') {
++cursor;
}
if (cursor == end) {
*error = "rosella_coefficients must be a JSON array";
return false;
}
++cursor;
while (cursor < end) {
while (cursor < end && (*cursor == ' ' || *cursor == '\t' || *cursor == '\r' ||
*cursor == '\n' || *cursor == ',')) {
++cursor;
}
if (cursor >= end) {
break;
}
if (*cursor == ']') {
return true;
}
const bool negative = *cursor == '-';
if (negative) {
++cursor;
}
if (cursor >= end || *cursor < '0' || *cursor > '9') {
*error = "rosella_coefficients contains a non-integer value";
return false;
}
long long value = 0;
while (cursor < end && *cursor >= '0' && *cursor <= '9') {
value = value * 10 + (*cursor - '0');
if (value > (1ll << 40)) {
*error = "rosella_coefficients value is out of range";
return false;
}
++cursor;
}
// A fractional part or an exponent means the value is not an exact integer.
if (cursor < end && (*cursor == '.' || *cursor == 'e' || *cursor == 'E')) {
*error = "rosella_coefficients contains a non-integer value";
return false;
}
if (negative) {
value = -value;
}
if (value < -(1ll << 31) || value > (1ll << 31) - 1) {
*error = "rosella_coefficients value is outside signed int32";
return false;
}
out->push_back(static_cast<std::int32_t>(value));
}
*error = "rosella_coefficients array is truncated";
return false;
}
struct RpHeader {
std::uint16_t stored_checksum = 0;
std::uint16_t computed_checksum = 0;
bool checksum_valid = false;
bool table_a_present = false;
bool table_b_present = false;
bool table_c_present = false;
int table_a_dimension = 0;
int table_a_option = 0;
int table_a_extra = 0;
int table_b_dimension = 0;
int table_b_extra = 0;
int table_b_groups = 0;
int table_c_dimension = 0;
std::size_t active_lanes = 0;
};
Status inspect_rp(const std::vector<std::int32_t>& lanes, RpHeader* out) {
if (lanes.size() < 5u) {
return model_fail(JOC_ERR_HRTF_FORMAT, "Rosella rp must contain whole int32 lanes");
}
if (low16(lanes[0]) != 0x7072u) {
return model_fail(JOC_ERR_HRTF_FORMAT, "bad Rosella rp magic");
}
out->stored_checksum = low16(lanes[1]);
out->table_a_present = low16(lanes[2]) != 0u;
out->table_b_present = low16(lanes[3]) != 0u;
out->table_c_present = low16(lanes[4]) != 0u;
std::size_t index = 5u;
if (out->table_a_present) {
out->table_a_dimension = low16(lanes[index]);
out->table_a_option = low16(lanes[index + 1u]);
out->table_a_extra = low16(lanes[index + 2u]);
index += 5u;
} else {
out->table_a_dimension = 77;
}
if (out->table_b_present) {
if (!out->table_a_present) {
return model_fail(JOC_ERR_HRTF_FORMAT,
"Rosella rp table B cannot be present without table A");
}
out->table_b_dimension = low16(lanes[index]);
out->table_b_extra = low16(lanes[index + 1u]);
out->table_b_groups = low16(lanes[index + 2u]);
index += 3u;
}
if (out->table_c_present) {
out->table_c_dimension = low16(lanes[index]);
index += 1u;
}
const long long payload_words =
static_cast<long long>(index) - 2 +
(out->table_b_present ? (out->table_b_dimension + 380 * out->table_b_groups +
out->table_b_extra + 79)
: 0) +
(out->table_a_present ? (171 * out->table_a_extra + 79 +
2 * (out->table_a_option + 14 * out->table_a_dimension))
: 0) +
11 + (out->table_c_present ? (314 * out->table_c_dimension + 1) : 0);
if (payload_words < 0) {
return model_fail(JOC_ERR_HRTF_FORMAT, "malformed Rosella rp header");
}
out->active_lanes = static_cast<std::size_t>(2 + payload_words);
if (lanes.size() < out->active_lanes) {
return model_fail(JOC_ERR_HRTF_FORMAT, "Rosella rp is truncated");
}
std::uint32_t computed = 0xA569u;
for (std::size_t lane = 2u; lane < out->active_lanes; ++lane) {
computed ^= low16(lanes[lane]);
}
out->computed_checksum = static_cast<std::uint16_t>(computed & 0xFFFFu);
out->checksum_valid = out->computed_checksum == out->stored_checksum;
return Status::success();
}
// _unpack_field: the serialized 154-per-direction field lanes to the padded grid.
void unpack_field(const std::int32_t* serialized, int directions, int exponent,
std::vector<float>* padded) {
padded->assign(static_cast<std::size_t>(160 * directions), 0.0f);
const int stride8 = 8 * directions;
const int stride2 = 2 * directions;
for (int source = 0; source < 154 * directions; ++source) {
const int group4 = (source % stride8) / stride2;
const int destination = (group4 & 3) + 4 * (source % stride2 +
2 * directions * (source / stride8 +
(group4 >> 2)));
(*padded)[static_cast<std::size_t>(destination)] =
q15_exp(serialized[source], exponent);
}
}
// _unpack_table_a_grid: the serialized table-A rows to the padded lane grid.
void unpack_table_a_grid(const std::int32_t* serialized, int dimension, int serialized_rows,
int padded_rows, int lane_group, std::vector<float>* padded) {
padded->assign(static_cast<std::size_t>(padded_rows) * static_cast<std::size_t>(dimension),
0.0f);
const int group_width = lane_group * 4;
for (int source = 0; source < serialized_rows * dimension; ++source) {
const int remainder = source % group_width;
const int destination = (remainder / lane_group) +
4 * (remainder % lane_group +
group_width / 4 * (source / group_width));
(*padded)[static_cast<std::size_t>(destination)] = q15(serialized[source]);
}
}
void unpack_table_a_extra(const std::int32_t* serialized, std::vector<float>* padded) {
padded->assign(160u, 0.0f);
for (int source = 0; source < 154; ++source) {
const int remainder = source & 7;
const int destination = (remainder >> 1) + 4 * ((source & 1) + 2 * (source >> 3));
(*padded)[static_cast<std::size_t>(destination)] = q15(serialized[source]);
}
}
} // namespace
std::string RosellaModel::summary() const {
std::string name = capture.name.empty() ? std::string("unnamed") : capture.name;
return "Rosella personalized_headphone '" + name + "' (" +
(room_model.empty() ? std::string("unknown room") : room_model) + "), " +
std::to_string(table_a_dimension) + " HQMF / 77 hybrid @ " +
std::to_string(sample_rate) + " Hz";
}
Status load_personalized_headphone(const std::string& path, RosellaModel* out) {
if (out == nullptr) {
return model_fail(JOC_ERR_INVALID_ARGUMENT, "null Rosella model destination");
}
if (!fs_utf8::exists(path)) {
return model_fail(JOC_ERR_HRTF_NOT_FOUND, "personalized headphone model not found: " + path);
}
std::ifstream stream = fs_utf8::open_input(path);
if (!stream.good()) {
return model_fail(JOC_ERR_IO, "cannot open " + path);
}
std::string text((std::istreambuf_iterator<char>(stream)), std::istreambuf_iterator<char>());
if (text.empty()) {
return model_fail(JOC_ERR_HRTF_FORMAT, "empty personalized headphone model: " + path);
}
// The checksum is taken over the coefficient lanes, exactly as upstream hashes
// the int32 image of the array.
const std::size_t first = text.find_first_not_of(" \t\r\n");
if (first == std::string::npos || text[first] != '{') {
return model_fail(JOC_ERR_NOT_SUPPORTED,
"raw rp models are not supported; use a .personalized_headphone JSON");
}
std::vector<json::Member> root;
std::string error;
if (!json::parse_object(text, &root, &error)) {
return model_fail(JOC_ERR_HRTF_FORMAT, "invalid personalized headphone JSON: " + error);
}
const json::Member* personalized = json::find(root, "personalized_hrtf");
if (personalized == nullptr) {
return model_fail(JOC_ERR_HRTF_FORMAT, "personalized_hrtf is missing");
}
std::vector<json::Member> inner;
if (!json::parse_object(personalized->raw, &inner, &error)) {
return model_fail(JOC_ERR_HRTF_FORMAT, "invalid personalized_hrtf object: " + error);
}
const json::Member* virtualizer = json::find(inner, "virtualizer_parameters");
if (virtualizer == nullptr) {
return model_fail(JOC_ERR_HRTF_FORMAT, "virtualizer_parameters is missing");
}
std::vector<json::Member> parameters;
if (!json::parse_object(virtualizer->raw, &parameters, &error)) {
return model_fail(JOC_ERR_HRTF_FORMAT, "invalid virtualizer_parameters: " + error);
}
const json::Member* coefficient_member = json::find(parameters, "rosella_coefficients");
if (coefficient_member == nullptr) {
return model_fail(JOC_ERR_HRTF_FORMAT, "rosella_coefficients is missing");
}
RosellaModel model;
model.source_path = path;
if (const json::Member* member = json::find(parameters, "rosella_coefficients_version")) {
json::as_string(*member, &model.coefficient_version);
}
if (const json::Member* member = json::find(parameters, "room_model")) {
json::as_string(*member, &model.room_model);
}
if (const json::Member* capture = json::find(inner, "phrtf_capture_metadata")) {
std::vector<json::Member> fields;
if (json::parse_object(capture->raw, &fields, &error)) {
const std::pair<const char*, std::string*> mapping[] = {
{"capture_submission_date", &model.capture.capture_submission_date},
{"capture_type", &model.capture.capture_type},
{"label", &model.capture.label},
{"name", &model.capture.name},
{"phrtf_algorithm_version", &model.capture.algorithm_version},
{"phrtf_creation_date", &model.capture.creation_date},
{"uuid", &model.capture.uuid},
{"version", &model.capture.version},
};
for (const auto& entry : mapping) {
if (const json::Member* member = json::find(fields, entry.first)) {
json::as_string(*member, entry.second);
}
}
}
}
std::vector<std::int32_t> lanes;
if (!parse_int_array(coefficient_member->raw, &lanes, &error)) {
return model_fail(JOC_ERR_HRTF_FORMAT, error);
}
{
crypto::Sha256 hash;
hash.update(lanes.data(), lanes.size() * sizeof(std::int32_t));
model.coefficient_sha256 = hash.finish_hex();
}
RpHeader header;
Status status = inspect_rp(lanes, &header);
if (!status.ok()) {
return status;
}
if (!header.checksum_valid || header.active_lanes != lanes.size()) {
return model_fail(JOC_ERR_HRTF_FORMAT,
"invalid or non-active Rosella rp coefficient sequence");
}
if (!header.table_a_present || !header.table_b_present || header.table_c_present) {
return model_fail(JOC_ERR_NOT_SUPPORTED,
"the renderer requires table A+B and no table C");
}
if (header.table_a_dimension != 64 || header.table_a_option != 3) {
return model_fail(JOC_ERR_NOT_SUPPORTED,
"the renderer requires the observed 64-channel HQMF layout");
}
if (header.table_b_dimension != 20 || header.table_b_groups != 36) {
return model_fail(JOC_ERR_NOT_SUPPORTED,
"the renderer requires 20 hybrid groups and 36 direction terms");
}
const std::int32_t* values = lanes.data();
const std::size_t total = lanes.size();
std::size_t position = 13u;
const int extra = header.table_a_extra;
model.table_a_dimension = header.table_a_dimension;
model.table_a_option = header.table_a_option;
model.table_a_extra = extra;
model.table_a_header_field = low16(values[8]);
model.table_a_header_25 = low16(values[9]);
model.table_a_control = low16(values[position]);
model.field_exponent = values[position];
position += 1u;
const int option_count = header.table_a_option;
model.table_a_option_ids.resize(static_cast<std::size_t>(option_count));
for (int index = 0; index < option_count; ++index) {
model.table_a_option_ids[static_cast<std::size_t>(index)] =
low16(values[position + static_cast<std::size_t>(index)]);
}
position += static_cast<std::size_t>(option_count);
model.table_a_option_values.resize(static_cast<std::size_t>(option_count));
for (int index = 0; index < option_count; ++index) {
model.table_a_option_values[static_cast<std::size_t>(index)] =
q15(values[position + static_cast<std::size_t>(index)]);
}
position += static_cast<std::size_t>(option_count);
model.table_a_scalar = q15(values[position]);
position += 1u;
const int dimension = header.table_a_dimension;
unpack_table_a_grid(values + position, dimension, 16, 20, 16,
&model.table_a_filter_16x64_padded);
position += static_cast<std::size_t>(16 * dimension);
for (int index = 0; index < 4; ++index) {
model.table_a_four_integers[static_cast<std::size_t>(index)] =
low16(values[position + static_cast<std::size_t>(index)]);
}
position += 4u;
model.table_a_integer = low16(values[position]);
position += 1u;
unpack_table_a_grid(values + position, dimension, 8, 10, 8,
&model.table_a_filter_8x64_padded);
position += static_cast<std::size_t>(8 * dimension);
model.table_a_vector16.resize(16u);
for (int index = 0; index < 16; ++index) {
model.table_a_vector16[static_cast<std::size_t>(index)] =
q15(values[position + static_cast<std::size_t>(index)]);
}
position += 16u;
unpack_table_a_grid(values + position, dimension, 4, 5, 4,
&model.table_a_filter_4x64_padded);
position += static_cast<std::size_t>(4 * dimension);
model.table_a_extra_indices.resize(static_cast<std::size_t>(extra));
for (int index = 0; index < extra; ++index) {
model.table_a_extra_indices[static_cast<std::size_t>(index)] =
low16(values[position + static_cast<std::size_t>(index)]);
}
position += static_cast<std::size_t>(extra);
model.table_a_extra_fields_padded.assign(static_cast<std::size_t>(extra) * 160u, 0.0f);
std::vector<float> unpacked;
for (int index = 0; index < extra; ++index) {
unpack_table_a_extra(values + position, &unpacked);
std::copy(unpacked.begin(), unpacked.end(),
model.table_a_extra_fields_padded.begin() + static_cast<std::ptrdiff_t>(index) * 160);
position += 154u;
}
model.table_a_extra_vectors.assign(static_cast<std::size_t>(extra) * 16u, 0.0f);
for (int index = 0; index < extra; ++index) {
for (int lane = 0; lane < 16; ++lane) {
model.table_a_extra_vectors[static_cast<std::size_t>(index) * 16u +
static_cast<std::size_t>(lane)] =
q15(values[position + static_cast<std::size_t>(lane)]);
}
position += 16u;
}
const std::size_t table_b_start = position;
if (table_b_start != 13u + 1821u + static_cast<std::size_t>(171 * extra)) {
return model_fail(JOC_ERR_HRTF_FORMAT, "Rosella table-A parser lost its place");
}
model.sample_rate = 2 * low16(values[position]);
position += 1u;
model.matrix_exponent = values[position];
position += 1u;
const std::size_t matrix_count = 36u * 36u;
model.matrix_left.resize(matrix_count);
model.matrix_right.resize(matrix_count);
for (std::size_t index = 0; index < matrix_count; ++index) {
model.matrix_left[index] = q15_exp(values[position + index], model.matrix_exponent);
}
position += matrix_count;
for (std::size_t index = 0; index < matrix_count; ++index) {
model.matrix_right[index] = q15_exp(values[position + index], model.matrix_exponent);
}
position += matrix_count;
model.vector_left.resize(36u);
model.vector_right.resize(36u);
for (int index = 0; index < 36; ++index) {
model.vector_left[static_cast<std::size_t>(index)] =
q15_exp(values[position + static_cast<std::size_t>(index)], model.matrix_exponent);
}
position += 36u;
for (int index = 0; index < 36; ++index) {
model.vector_right[static_cast<std::size_t>(index)] =
q15_exp(values[position + static_cast<std::size_t>(index)], model.matrix_exponent);
}
position += 36u;
const std::size_t serialized_count = 154u * 36u;
unpack_field(values + position, 36, model.field_exponent, &model.field_left_padded);
bool odd_zero = true;
for (std::size_t index = 1u; index < serialized_count; index += 2u) {
const float value = q15_exp(values[position + index], model.field_exponent);
if (std::abs(value) > 1.0e-6f) {
odd_zero = false;
break;
}
}
model.field_left_odd_serialized_zero = odd_zero;
position += serialized_count;
unpack_field(values + position, 36, model.field_exponent, &model.field_right_padded);
position += serialized_count;
model.hybrid_flags.resize(20u);
int active_hybrid = 0;
for (int index = 0; index < 20; ++index) {
model.hybrid_flags[static_cast<std::size_t>(index)] =
low16(values[position + static_cast<std::size_t>(index)]);
if (model.hybrid_flags[static_cast<std::size_t>(index)] == 1) {
++active_hybrid;
}
}
position += 20u;
if (active_hybrid != header.table_b_extra) {
return model_fail(JOC_ERR_HRTF_FORMAT, "hybrid value count does not match the header");
}
model.hybrid_values.resize(static_cast<std::size_t>(active_hybrid));
for (int index = 0; index < active_hybrid; ++index) {
model.hybrid_values[static_cast<std::size_t>(index)] =
q15(values[position + static_cast<std::size_t>(index)]);
}
position += static_cast<std::size_t>(active_hybrid);
model.model_scalars.resize(5u);
for (int index = 0; index < 5; ++index) {
model.model_scalars[static_cast<std::size_t>(index)] =
q15(values[position + static_cast<std::size_t>(index)]);
}
position += 5u;
const std::size_t expected_tail =
table_b_start + static_cast<std::size_t>(header.table_b_dimension +
380 * header.table_b_groups +
header.table_b_extra + 79);
if (position != expected_tail) {
return model_fail(JOC_ERR_HRTF_FORMAT, "Rosella table-B parser lost its place");
}
model.header_float_scalars[0] = q15(values[position]);
model.header_float_scalars[1] = q15(values[position + 1u]) * 16.0f;
model.header_integer_fields[0] = values[position + 2u];
model.header_integer_fields[1] = low16(values[position + 3u]);
position += 4u;
for (int profile = 0; profile < 4; ++profile) {
RosellaDistanceProfile parsed;
for (int index = 0; index < 6; ++index) {
parsed.bounds[static_cast<std::size_t>(index)] =
q15(values[position + static_cast<std::size_t>(index)]);
}
position += 6u;
parsed.distance_scale_m =
q15_exp(values[position], values[position + 1u]);
position += 2u;
parsed.inverse_distance_per_m = q15(values[position]);
parsed.axis_scales_internal[0] = q15(values[position + 1u]);
parsed.axis_scales_internal[1] = q15(values[position + 2u]);
parsed.axis_scales_internal[2] = q15(values[position + 3u]);
parsed.minimum_normalized_radius = q15(values[position + 4u]);
position += 5u;
model.profiles[static_cast<std::size_t>(profile)] = parsed;
}
model.profile_tail.resize(8u);
for (int index = 0; index < 8; ++index) {
model.profile_tail[static_cast<std::size_t>(index)] =
q15(values[position + static_cast<std::size_t>(index)]);
}
position += 8u;
for (int index = 0; index < 3; ++index) {
model.post_fields[static_cast<std::size_t>(index)] =
values[position + static_cast<std::size_t>(index)];
}
position += 3u;
if (position != total) {
return model_fail(JOC_ERR_HRTF_FORMAT, "unparsed Rosella coefficient lanes");
}
if (model.sample_rate != 48000) {
return model_fail(JOC_ERR_HRTF_FORMAT,
"Rosella model sample rate must be 48000, got " +
std::to_string(model.sample_rate));
}
*out = std::move(model);
return Status::success();
}
} // namespace joc::hrtf
+88
View File
@@ -0,0 +1,88 @@
#pragma once
#include <array>
#include <cstdint>
#include <string>
#include <vector>
#include "foundation/status.h"
// Parser for the Dolby ".personalized_headphone" model (upstream rosella_model.py).
// The file is JSON whose virtualizer_parameters carry the raw "rp" coefficient
// lanes; everything the renderer needs is unpacked here, in the same float32
// arithmetic the reference uses, because those values are part of the model.
namespace joc::hrtf {
struct RosellaDistanceProfile {
std::array<float, 6> bounds{};
float distance_scale_m = 0.0f;
float inverse_distance_per_m = 0.0f;
std::array<float, 3> axis_scales_internal{};
float minimum_normalized_radius = 0.0f;
};
struct RosellaCaptureMetadata {
std::string capture_submission_date;
std::string capture_type;
std::string label;
std::string name;
std::string algorithm_version;
std::string creation_date;
std::string uuid;
std::string version;
};
struct RosellaModel {
std::string source_path;
std::string coefficient_sha256;
std::string coefficient_version;
std::string room_model;
RosellaCaptureMetadata capture;
int table_a_dimension = 0;
int table_a_option = 0;
int table_a_extra = 0;
int table_a_header_field = 0;
int table_a_header_25 = 0;
int table_a_control = 0;
std::vector<int> table_a_option_ids;
std::vector<float> table_a_option_values;
float table_a_scalar = 0.0f;
std::vector<float> table_a_filter_16x64_padded;
std::array<int, 4> table_a_four_integers{};
int table_a_integer = 0;
std::vector<float> table_a_filter_8x64_padded;
std::vector<float> table_a_vector16;
std::vector<float> table_a_filter_4x64_padded;
std::vector<int> table_a_extra_indices;
std::vector<float> table_a_extra_fields_padded;
std::vector<float> table_a_extra_vectors;
int sample_rate = 0;
int matrix_exponent = 0;
int field_exponent = 0;
std::vector<float> matrix_left;
std::vector<float> matrix_right;
std::vector<float> vector_left;
std::vector<float> vector_right;
std::vector<float> field_left_padded;
std::vector<float> field_right_padded;
bool field_left_odd_serialized_zero = false;
std::vector<int> hybrid_flags;
std::vector<float> hybrid_values;
std::vector<float> model_scalars;
std::array<float, 2> header_float_scalars{};
std::array<int, 2> header_integer_fields{};
std::array<RosellaDistanceProfile, 4> profiles{};
std::vector<float> profile_tail;
std::array<int, 3> post_fields{};
// One line for reports and logs: the capture name and room model are the
// model's own strings, followed by the table layout and sample rate, e.g.
// "Rosella personalized_headphone '<name>' (<room>), <N> HQMF / 77 hybrid @ <rate> Hz".
std::string summary() const;
};
Status load_personalized_headphone(const std::string& path, RosellaModel* out);
} // namespace joc::hrtf
File diff suppressed because it is too large Load Diff
+64
View File
@@ -0,0 +1,64 @@
#pragma once
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
#include "foundation/status.h"
#include "hrtf/rosella_model.h"
#include "oamd/oamd_parser.h"
#include "timeline/position_timeline.h"
// Rosella ".personalized_headphone" binaural renderer (upstream rosella_core.py,
// rosella_direct.py, rosella_room.py and rosella_binaural_renderer.py). It takes
// the same pipeline slot as the SOFA runtime: sixteen object channels per frame in,
// interleaved stereo out, with the OAMD timeline driving the per-block parameters.
namespace joc::hrtf {
// rosella_direct.BINAURAL_PROFILE_NAMES.
enum class RosellaProfile : std::int32_t { Near = 1, Far = 2, Mid = 3 };
struct RosellaRenderOptions {
RosellaProfile profile = RosellaProfile::Mid;
std::int64_t object_delay_samples = 1473;
double tail_seconds = 5.0;
double output_gain = 1.0;
int chunk_frames = 64;
int room_impulse_slots = 4096;
};
class RosellaRuntime {
public:
RosellaRuntime();
~RosellaRuntime();
RosellaRuntime(const RosellaRuntime&) = delete;
RosellaRuntime& operator=(const RosellaRuntime&) = delete;
Status open(const RosellaModel& model, const RosellaRenderOptions& options);
// objects16_planar is channel-major: channel * 1536 + sample.
Status submit_frame(const float* objects16_planar, const oamd::OamdUpdate* update,
std::int64_t frame_index, std::int64_t outer_sample_offset,
std::int64_t object_delay_samples);
// Drains the flush tail: the pending partial chunk plus flush_samples of silence.
Status finish(std::uint32_t flush_samples, std::vector<double>* out);
std::uint32_t finish_capacity(double tail_seconds) const;
Status reset();
const std::vector<double>& output() const;
void take_output(std::vector<double>* out);
std::uint64_t input_samples() const;
std::uint64_t processed_input_samples() const;
std::uint64_t metadata_block_updates() const;
const timeline::OamdPositionTimeline& timeline() const;
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace joc::hrtf
+255
View File
@@ -0,0 +1,255 @@
#include "hrtf/sofa.h"
#include <algorithm>
#include <cmath>
#include <cctype>
#include <cstdio>
#include <fstream>
#include <utility>
#include "foundation/fs_utf8.h"
#include "foundation/sha256.h"
#include "io/hdf5.h"
namespace joc::hrtf {
namespace {
Status sofa_fail(joc_error code, const std::string& message) {
return Status::fail(code, stage::kRender, message);
}
std::string format_number(double value) {
if (std::isfinite(value) && value == std::floor(value) && std::fabs(value) < 1.0e15) {
return std::to_string(static_cast<long long>(value));
}
char buffer[32];
std::snprintf(buffer, sizeof(buffer), "%.6g", value);
return std::string(buffer);
}
// Every array is checked against the element count the convention prescribes, so
// a file whose shape disagrees with its metadata is rejected instead of silently
// producing a shifted impulse response.
Status read_doubles(const io::Hdf5File& file, const std::string& path, std::uint64_t expected,
std::vector<double>* out) {
if (!file.has_dataset(path)) {
return sofa_fail(JOC_ERR_HRTF_FORMAT, "SOFA file has no " + path + " dataset");
}
const Status status = file.read_dataset_double(path, out);
if (!status.ok()) {
return sofa_fail(JOC_ERR_HRTF_FORMAT, "SOFA dataset " + path + ": " + status.message());
}
if (out->size() != expected) {
return sofa_fail(JOC_ERR_HRTF_FORMAT,
"SOFA dataset " + path + " holds " + std::to_string(out->size()) +
" values, expected " + std::to_string(expected));
}
return Status::success();
}
Status read_text(const io::Hdf5File& file, const std::string& name, bool required,
std::string* out) {
io::Hdf5Attribute attribute;
const Status status = file.attribute("", name, &attribute);
if (!status.ok()) {
if (required) {
return sofa_fail(JOC_ERR_HRTF_FORMAT,
"SOFA file has no root attribute " + name + ": " + status.message());
}
return Status::success();
}
*out = attribute.text;
return Status::success();
}
// SHA-256 of the whole file: the compiled-cache key is derived from it, so the
// digest is taken over the exact bytes the parse consumed.
std::string file_digest(const std::string& path) {
std::ifstream stream = fs_utf8::open_input(path);
if (!stream.good()) {
return std::string();
}
crypto::Sha256 hash;
std::vector<char> buffer(1u << 20);
while (stream.good()) {
stream.read(buffer.data(), static_cast<std::streamsize>(buffer.size()));
const std::streamsize count = stream.gcount();
if (count > 0) {
hash.update(buffer.data(), static_cast<std::size_t>(count));
}
}
return hash.finish_hex();
}
// The coordinate declaration of one dataset, when the file carries it.
void read_coordinates(const io::Hdf5File& file, const std::string& dataset,
SofaCoordinate* out) {
io::Hdf5Attribute attribute;
if (file.attribute(dataset, "Type", &attribute).ok()) {
out->type = attribute.text;
}
if (file.attribute(dataset, "Units", &attribute).ok()) {
out->units = attribute.text;
}
}
} // namespace
std::string SofaHrir::summary() const {
return "SOFA " + sofa_conventions + ", " + std::to_string(ir_count) + " IRs x " +
std::to_string(ir_length) + " taps @ " + format_number(sample_rate) + " Hz";
}
Status load_sofa(const std::string& path, SofaHrir* out) {
if (out == nullptr) {
return sofa_fail(JOC_ERR_INVALID_ARGUMENT, "null SOFA destination");
}
if (!fs_utf8::exists(path)) {
return sofa_fail(JOC_ERR_HRTF_NOT_FOUND, "SOFA file not found: " + path);
}
io::Hdf5File file;
Status status = file.open(path);
if (!status.ok()) {
return sofa_fail(JOC_ERR_HRTF_FORMAT, "SOFA file " + path + ": " + status.message());
}
SofaHrir sofa;
sofa.source_path = path;
sofa.source_sha256 = file_digest(path);
for (char& character : sofa.source_sha256) {
character = static_cast<char>(std::toupper(static_cast<unsigned char>(character)));
}
status = read_text(file, "Conventions", true, &sofa.conventions);
if (!status.ok()) {
return status;
}
status = read_text(file, "SOFAConventions", true, &sofa.sofa_conventions);
if (!status.ok()) {
return status;
}
if (sofa.conventions != "SOFA" || sofa.sofa_conventions != "SimpleFreeFieldHRIR") {
return sofa_fail(JOC_ERR_HRTF_UNSUPPORTED_CONVENTION,
"SOFA conventions " + sofa.conventions + "/" + sofa.sofa_conventions +
" are not SimpleFreeFieldHRIR");
}
status = read_text(file, "SOFAConventionsVersion", false, &sofa.convention_version);
if (!status.ok()) {
return status;
}
status = read_text(file, "Version", false, &sofa.version);
if (!status.ok()) {
return status;
}
status = read_text(file, "DataType", false, &sofa.data_type);
if (!status.ok()) {
return status;
}
status = read_text(file, "RoomType", false, &sofa.room_type);
if (!status.ok()) {
return status;
}
status = read_text(file, "Title", false, &sofa.title);
if (!status.ok()) {
return status;
}
status = read_text(file, "DatabaseName", false, &sofa.database_name);
if (!status.ok()) {
return status;
}
status = read_text(file, "ListenerShortName", false, &sofa.listener_short_name);
if (!status.ok()) {
return status;
}
status = read_text(file, "Comment", false, &sofa.comment);
if (!status.ok()) {
return status;
}
io::Hdf5DatasetInfo info;
status = file.dataset_info("Data.IR", &info);
if (!status.ok()) {
return sofa_fail(JOC_ERR_HRTF_FORMAT,
"SOFA file has no usable Data.IR dataset: " + status.message());
}
if (info.shape.size() != 3u || info.shape[1] != 2u || info.shape[0] == 0u ||
info.shape[2] == 0u) {
return sofa_fail(JOC_ERR_HRTF_FORMAT, "SOFA Data.IR is not shaped (M, 2, N)");
}
if (info.shape[0] > 0xFFFFFFFFull || info.shape[2] > 0xFFFFFFFFull) {
return sofa_fail(JOC_ERR_HRTF_FORMAT, "SOFA Data.IR is larger than this reader accepts");
}
sofa.ir_count = static_cast<std::uint32_t>(info.shape[0]);
sofa.ir_length = static_cast<std::uint32_t>(info.shape[2]);
const std::uint64_t taps = static_cast<std::uint64_t>(sofa.ir_count) * 2u * sofa.ir_length;
status = read_doubles(file, "Data.IR", taps, &sofa.ir);
if (!status.ok()) {
return status;
}
std::vector<double> scalar;
status = read_doubles(file, "Data.SamplingRate", 1u, &scalar);
if (!status.ok()) {
return status;
}
sofa.sample_rate = scalar[0];
io::Hdf5Attribute attribute;
if (file.attribute("Data.SamplingRate", "Units", &attribute).ok()) {
sofa.sampling_rate_units = attribute.text;
}
// Data.Delay is optional in the wild; absent means "no delay was measured".
if (file.has_dataset("Data.Delay")) {
std::vector<double> delay;
status = read_doubles(file, "Data.Delay", 2u, &delay);
if (!status.ok()) {
return status;
}
sofa.delay[0] = delay[0];
sofa.delay[1] = delay[1];
}
const std::uint64_t measurements = sofa.ir_count;
status = read_doubles(file, "SourcePosition", measurements * 3u, &sofa.source_position);
if (!status.ok()) {
return status;
}
read_coordinates(file, "SourcePosition", &sofa.source_position_coordinates);
std::vector<double> vector;
status = read_doubles(file, "ListenerPosition", 3u, &vector);
if (!status.ok()) {
return status;
}
std::copy(vector.begin(), vector.end(), sofa.listener_position);
read_coordinates(file, "ListenerPosition", &sofa.listener_position_coordinates);
status = read_doubles(file, "ListenerView", 3u, &vector);
if (!status.ok()) {
return status;
}
std::copy(vector.begin(), vector.end(), sofa.listener_view);
read_coordinates(file, "ListenerView", &sofa.listener_view_coordinates);
status = read_doubles(file, "ListenerUp", 3u, &vector);
if (!status.ok()) {
return status;
}
std::copy(vector.begin(), vector.end(), sofa.listener_up);
read_coordinates(file, "ListenerUp", &sofa.listener_up_coordinates);
status = read_doubles(file, "EmitterPosition", 3u, &vector);
if (!status.ok()) {
return status;
}
std::copy(vector.begin(), vector.end(), sofa.emitter_position);
read_coordinates(file, "EmitterPosition", &sofa.emitter_position_coordinates);
status = read_doubles(file, "ReceiverPosition", 6u, &vector);
if (!status.ok()) {
return status;
}
std::copy(vector.begin(), vector.end(), sofa.receiver_position);
read_coordinates(file, "ReceiverPosition", &sofa.receiver_position_coordinates);
*out = std::move(sofa);
return Status::success();
}
} // namespace joc::hrtf
+61
View File
@@ -0,0 +1,61 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
#include "foundation/status.h"
namespace joc::hrtf {
// Coordinate declaration of one SOFA variable: Type ("spherical"/"cartesian") and
// Units. Empty when the file does not declare them (ListenerUp inherits).
struct SofaCoordinate {
std::string type;
std::string units;
};
// SOFA SimpleFreeFieldHRIR as this project consumes it: the impulse responses,
// the measurement geometry and the metadata needed to report what was loaded.
// All angles are degrees, all distances metres, exactly as the file stores them.
struct SofaHrir {
double sample_rate = 0.0;
std::uint32_t ir_count = 0; // M: number of measurements
std::uint32_t ir_length = 0; // N: taps per impulse response
std::vector<double> ir; // C order [M][2][N]
double delay[2] = {0.0, 0.0};
std::vector<double> source_position; // M*3
double listener_position[3] = {0.0, 0.0, 0.0};
double listener_view[3] = {1.0, 0.0, 0.0};
double listener_up[3] = {0.0, 0.0, 1.0};
double emitter_position[3] = {0.0, 0.0, 0.0};
double receiver_position[6] = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0};
std::string conventions;
std::string sofa_conventions;
std::string convention_version;
std::string version;
std::string data_type;
std::string room_type;
std::string title;
std::string database_name;
std::string listener_short_name;
std::string comment;
std::string sampling_rate_units;
SofaCoordinate source_position_coordinates;
SofaCoordinate listener_position_coordinates;
SofaCoordinate listener_view_coordinates;
SofaCoordinate listener_up_coordinates;
SofaCoordinate emitter_position_coordinates;
SofaCoordinate receiver_position_coordinates;
// Identity of the file itself, needed for the compiled-cache key.
std::string source_path;
std::string source_sha256;
// One line for reports and logs:
// "SOFA SimpleFreeFieldHRIR, <M> IRs x <N> taps @ <rate> Hz".
std::string summary() const;
};
Status load_sofa(const std::string& path, SofaHrir* out);
} // namespace joc::hrtf
+237
View File
@@ -0,0 +1,237 @@
#include "hrtf/sofa_cache.h"
#include <algorithm>
#include <filesystem>
#include <list>
#include <mutex>
#include <utility>
#include <vector>
#include "foundation/fs_utf8.h"
#include "hrtf/sofa.h"
namespace joc::hrtf {
namespace {
namespace fs = std::filesystem;
// Small process-local cache: the reference keeps the last eight compiled fields.
constexpr std::size_t kMemoryCacheEntries = 8;
struct MemoryEntry {
std::string key;
Field field;
};
std::mutex& memory_mutex() {
static std::mutex mutex;
return mutex;
}
std::list<MemoryEntry>& memory_cache() {
static std::list<MemoryEntry> cache;
return cache;
}
bool memory_cache_get(const std::string& key, Field* out) {
std::lock_guard<std::mutex> lock(memory_mutex());
std::list<MemoryEntry>& cache = memory_cache();
for (auto entry = cache.begin(); entry != cache.end(); ++entry) {
if (entry->key == key) {
*out = entry->field;
cache.splice(cache.begin(), cache, entry);
return true;
}
}
return false;
}
void memory_cache_put(const std::string& key, const Field& field) {
std::lock_guard<std::mutex> lock(memory_mutex());
std::list<MemoryEntry>& cache = memory_cache();
for (auto entry = cache.begin(); entry != cache.end(); ++entry) {
if (entry->key == key) {
entry->field = field;
cache.splice(cache.begin(), cache, entry);
return;
}
}
cache.push_front(MemoryEntry{key, field});
while (cache.size() > kMemoryCacheEntries) {
cache.pop_back();
}
}
Status cache_fail(joc_error code, const std::string& message) {
return Status::fail(code, stage::kRender, message);
}
std::string upper(std::string text) {
std::transform(text.begin(), text.end(), text.begin(), [](unsigned char value) {
return static_cast<char>(std::toupper(value));
});
return text;
}
} // namespace
Status parse_cache_policy(const std::string& text, CachePolicy* out) {
if (out == nullptr) {
return cache_fail(JOC_ERR_INVALID_ARGUMENT, "null cache policy");
}
if (text == "none") {
*out = CachePolicy::None;
return Status::success();
}
if (text == "memory") {
*out = CachePolicy::Memory;
return Status::success();
}
if (text == "disk") {
*out = CachePolicy::Disk;
return Status::success();
}
return cache_fail(JOC_ERR_INVALID_CONFIG, "cache_policy must be none, memory, or disk");
}
Status validate_jochrtf(const std::string& path, const std::string& source_sha256,
const std::string& cache_key, Field* out) {
Field field;
const Status status = load_jochrtf(path, &field);
if (!status.ok()) {
return status;
}
if (!source_sha256.empty() && field.source_sha256 != upper(source_sha256)) {
return cache_fail(JOC_ERR_HRTF_HASH, "compiled HRTF source hash mismatch");
}
if (!cache_key.empty() && field.cache_key != upper(cache_key)) {
return cache_fail(JOC_ERR_HRTF_HASH, "compiled HRTF configuration hash mismatch");
}
if (out != nullptr) {
*out = std::move(field);
}
return Status::success();
}
Status save_jochrtf_atomic(const Field& field, const std::string& path) {
if (path.empty()) {
return cache_fail(JOC_ERR_INVALID_ARGUMENT, "empty compiled HRTF cache path");
}
const fs::path target = fs_utf8::to_path(path);
std::error_code error;
if (target.has_parent_path()) {
fs::create_directories(target.parent_path(), error);
if (error) {
return cache_fail(JOC_ERR_OUTPUT_OPEN,
"cannot create " + fs_utf8::from_path(target.parent_path()));
}
}
const std::string temporary = path + ".tmp";
Status status = write_jochrtf(field, temporary);
if (!status.ok()) {
return status;
}
// The rename is what makes a half-written cache impossible to observe.
fs::rename(fs_utf8::to_path(temporary), target, error);
if (error) {
fs::remove(fs_utf8::to_path(temporary), error);
return cache_fail(JOC_ERR_OUTPUT_WRITE, "cannot replace " + path);
}
return Status::success();
}
Status load_or_compile_sofa_field(const SofaFieldRequest& request, Field* out,
std::string* cache_path) {
if (out == nullptr) {
return cache_fail(JOC_ERR_INVALID_ARGUMENT, "null compiled HRTF destination");
}
if (request.sofa_path.empty()) {
return cache_fail(JOC_ERR_INVALID_ARGUMENT, "no SOFA path for the compiled HRTF field");
}
if (request.policy == CachePolicy::Disk && request.cache_dir.empty()) {
return cache_fail(JOC_ERR_INVALID_CONFIG, "the disk cache policy needs a cache directory");
}
SofaHrir sofa;
Status status = load_sofa(request.sofa_path, &sofa);
if (!status.ok()) {
return status;
}
CanonicalHrtf canonical;
status = canonicalize_sofa(sofa, &canonical);
if (!status.ok()) {
return status;
}
// The key depends on the shell that the radius selects, exactly as upstream.
double actual_radius = request.options.shell_radius_m;
(void)canonical_shell_indices(canonical, request.options.shell_radius_m, &actual_radius);
const std::string key = compiled_hrtf_cache_key(
canonical.source_sha256, canonical.sample_rate_hz, actual_radius, request.options.order,
request.options.projection_ridge, request.options.sh_ridge);
if (cache_path != nullptr) {
cache_path->clear();
}
std::string target;
if (request.policy == CachePolicy::Disk) {
target = request.cache_dir;
if (!target.empty() && target.back() != '/' && target.back() != '\\') {
target += "/";
}
target += cache_file_name(canonical.source_path.empty()
? std::string()
: canonical.source_path,
key);
if (fs_utf8::exists(target)) {
Field cached_field;
const Status cached =
validate_jochrtf(target, canonical.source_sha256, key, &cached_field);
if (cached.ok()) {
memory_cache_put(key, cached_field);
*out = std::move(cached_field);
if (cache_path != nullptr) {
*cache_path = target;
}
return Status::success();
}
}
}
Field field;
if (request.policy != CachePolicy::None && memory_cache_get(key, &field)) {
// A memory hit still materialises the disk cache the caller asked for.
if (request.policy == CachePolicy::Disk) {
status = save_jochrtf_atomic(field, target);
if (!status.ok()) {
return status;
}
if (cache_path != nullptr) {
*cache_path = target;
}
}
*out = std::move(field);
return Status::success();
}
status = compile_canonical_field(canonical, request.options, &field);
if (!status.ok()) {
return status;
}
if (field.cache_key != key) {
return cache_fail(JOC_ERR_INTERNAL, "internal compiled HRTF cache-key mismatch");
}
if (request.policy == CachePolicy::Disk) {
status = save_jochrtf_atomic(field, target);
if (!status.ok()) {
return status;
}
if (cache_path != nullptr) {
*cache_path = target;
}
}
if (request.policy != CachePolicy::None) {
memory_cache_put(key, field);
}
*out = std::move(field);
return Status::success();
}
} // namespace joc::hrtf
+38
View File
@@ -0,0 +1,38 @@
#pragma once
#include <string>
#include "foundation/status.h"
#include "hrtf/jochrtf.h"
#include "hrtf/sofa_field.h"
// Compiled-field cache: the .jochrtf is an internal artifact, so the caller only
// names the SOFA file and the policy. "memory" keeps the compiled field in this
// process, "disk" additionally reuses (and writes) <cache_dir>/<name>.<key>.jochrtf.
namespace joc::hrtf {
enum class CachePolicy { None, Memory, Disk };
struct SofaFieldRequest {
std::string sofa_path;
CompileOptions options;
CachePolicy policy = CachePolicy::Memory;
std::string cache_dir; // required for the disk policy
};
// Parses "none"/"memory"/"disk"; anything else is rejected.
Status parse_cache_policy(const std::string& text, CachePolicy* out);
// Returns the compiled field, reusing a valid cache when the policy allows it.
// `cache_path` (optional) receives the cache file that was read or written.
Status load_or_compile_sofa_field(const SofaFieldRequest& request, Field* out,
std::string* cache_path);
// Writes the field to `path` through a temporary file and an atomic rename.
Status save_jochrtf_atomic(const Field& field, const std::string& path);
// Verifies that a cache file belongs to `source_sha256` and `cache_key`.
Status validate_jochrtf(const std::string& path, const std::string& source_sha256,
const std::string& cache_key, Field* out);
} // namespace joc::hrtf
File diff suppressed because it is too large Load Diff
+103
View File
@@ -0,0 +1,103 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
#include "foundation/status.h"
#include "hrtf/jochrtf.h"
#include "hrtf/sofa.h"
// SOFA SimpleFreeFieldHRIR -> compiled directional field, ported from the
// reference chain (sofa_canonical.py + sofa_hrtf_field.py + the public
// filterbank): the measurement shell is selected, one delay representation is
// separated, the FIRs are projected onto the 64-QMF/77-hybrid filterbank and the
// result is fitted with fifth-order ACN/N3D real spherical harmonics.
namespace joc::hrtf {
inline constexpr int kFieldOrder = 5;
inline constexpr int kFieldTerms = 36;
inline constexpr double kFieldSampleRateHz = 48000.0;
inline constexpr double kDefaultShellRadiusM = 1.0;
inline constexpr double kDefaultProjectionRidge = 1.0e-3;
inline constexpr double kDefaultSphericalHarmonicRidge = 1.0e-5;
inline constexpr const char* kCompilerVersion = "joc-sofa-compiler-v1";
inline constexpr const char* kPhasePolicyVersion = "sofa-delay-exactly-once-v1";
inline constexpr const char* kShConvention = "ACN/N3D real";
inline constexpr const char* kFilterbankTableVersion = "joc-public-64qmf-77hybrid-v1";
// SHA-256 of the standard filterbank archive the embedded tables came from. It
// participates in the cache key, so it is part of the file-format contract.
inline constexpr const char* kFilterbankArchiveSha256 =
"C05BEF4D26E96ECBD4694E2572F05DA400255C777BA5047300B9D3B1F81081CD";
struct CompileOptions {
double shell_radius_m = kDefaultShellRadiusM;
int order = kFieldOrder;
double projection_ridge = kDefaultProjectionRidge;
double sh_ridge = kDefaultSphericalHarmonicRidge;
};
// Canonical HRIR set: Data.IR and Data.Delay stay separate, the listener frame is
// applied to the source positions and the ears are ordered left/right.
struct CanonicalHrtf {
std::string source_path;
std::string source_sha256;
std::string convention;
std::string convention_version;
std::string processing_label;
double sample_rate_hz = 0.0;
std::uint32_t measurements = 0;
std::uint32_t taps = 0;
int left_receiver_index = 0;
int right_receiver_index = 1;
std::vector<double> source_position_cartesian_m; // [M,3] listener-local
std::vector<double> unit_directions; // [M,3]
std::vector<double> measurement_radius_m; // [M]
std::vector<double> hrir; // [M,2,N] canonical L/R
std::vector<double> delay_samples; // [M,2], not applied
};
// Port of load_simple_free_field_hrir(): strict SimpleFreeFieldHRIR import.
Status canonicalize_sofa(const SofaHrir& sofa, CanonicalHrtf* out);
// Compiles the canonical set into the runtime field (port of SofaHrtfField.fit).
Status compile_sofa_field(const SofaHrir& sofa, const CompileOptions& options, Field* out);
// The measurements on the shell nearest to radius_m; actual_radius_m receives the
// mean radius of that shell (upstream CanonicalHrtf.shell_indices).
std::vector<std::size_t> canonical_shell_indices(const CanonicalHrtf& canonical, double radius_m,
double* actual_radius_m);
// Compiles an already canonicalized set (used by tests and the cache layer).
Status compile_canonical_field(const CanonicalHrtf& canonical, const CompileOptions& options,
Field* out);
// Configuration hash that names the cache file (upstream compiled_hrtf_cache_key).
std::string compiled_hrtf_cache_key(const std::string& source_sha256, double sample_rate_hz,
double shell_radius_m, int order, double projection_ridge,
double sh_ridge);
// Payload hash over the four arrays (upstream _payload_sha256).
std::string field_payload_sha256(const Field& field);
// "<stem>.<first 20 key digits>.jochrtf", the upstream cache file name.
std::string cache_file_name(const std::string& display_name, const std::string& cache_key);
// Serializes the field as a .jochrtf cache the upstream loader also accepts.
Status write_jochrtf(const Field& field, const std::string& path);
// The analysis/gain/synthesis dictionary the projection solves against (dev check).
std::vector<double> hybrid_gain_synthesis_dictionary_for_check(std::size_t sample_count);
// Shell directions and their spherical Voronoi weights (dev check).
void shell_directions_and_weights_for_check(const SofaHrir& sofa, double radius_m,
std::vector<double>* directions,
std::vector<double>* weights);
// PublicAnalysis77 on a unit impulse, interleaved complex (dev check).
std::vector<double> analysis_impulse_for_check(std::size_t total_samples);
// The 77 hybrid-band centre frequencies at 48 kHz.
const std::vector<double>& hybrid_band_center_frequencies_hz();
} // namespace joc::hrtf
+241
View File
@@ -0,0 +1,241 @@
#include "io/adm_writer.h"
#include "foundation/fs_utf8.h"
#include <cstring>
#include <filesystem>
#include "io/wav_writer.h" // pack_int24 (shared int24 quantisation)
namespace joc::io {
namespace {
constexpr long kDs64BodyOffset = 20;
constexpr long kDataSizeOffset = 76;
void put_u16(std::string* out, std::uint16_t value) {
char buffer[2];
std::memcpy(buffer, &value, 2);
out->append(buffer, 2);
}
void put_u32(std::string* out, std::uint32_t value) {
char buffer[4];
std::memcpy(buffer, &value, 4);
out->append(buffer, 4);
}
void put_u64(std::string* out, std::uint64_t value) {
char buffer[8];
std::memcpy(buffer, &value, 8);
out->append(buffer, 8);
}
} // namespace
AdmBwfWriter::~AdmBwfWriter() { abort(); }
Status AdmBwfWriter::open(const std::string& path, std::size_t block_samples) {
if (block_samples < JOC_FRAME_SAMPLES) {
return Status::fail(JOC_ERR_INVALID_ARGUMENT, stage::kOutput,
"ADM block size must hold at least one E-AC-3 frame");
}
path_ = path;
block_samples_ = block_samples;
used_ = 0;
frames_ = 0;
finalized_ = false;
buffer_.assign(block_samples * kChannels, 0.0f);
file_ = fs_utf8::fopen(path, "wb+");
if (file_ == nullptr) {
std::error_code ignored;
const std::filesystem::path parent = std::filesystem::path(path).parent_path();
if (!parent.empty()) {
std::filesystem::create_directories(parent, ignored);
}
file_ = fs_utf8::fopen(path, "wb+");
}
if (file_ == nullptr) {
return Status::fail(JOC_ERR_OUTPUT_OPEN, stage::kOutput, "cannot open " + path);
}
std::string header;
header.append("RF64", 4);
put_u32(&header, 0xFFFFFFFFu);
header.append("WAVE", 4);
if (std::fwrite(header.data(), 1, header.size(), file_) != header.size()) {
abort();
return Status::fail(JOC_ERR_OUTPUT_WRITE, stage::kOutput, "cannot write " + path);
}
Status status = write_chunk("ds64", std::string(28, '\0'));
if (!status.ok()) {
abort();
return status;
}
std::string fmt;
put_u16(&fmt, 1);
put_u16(&fmt, static_cast<std::uint16_t>(kChannels));
put_u32(&fmt, kRate);
put_u32(&fmt, kRate * kChannels * 3u);
put_u16(&fmt, static_cast<std::uint16_t>(kChannels * 3u));
put_u16(&fmt, 24);
status = write_chunk("fmt ", fmt);
if (!status.ok()) {
abort();
return status;
}
status = write_chunk("data", std::string());
if (!status.ok()) {
abort();
return status;
}
return Status::success();
}
Status AdmBwfWriter::write_chunk(const char id[4], const std::string& body) {
std::string header;
header.append(id, 4);
put_u32(&header, static_cast<std::uint32_t>(body.size()));
if (std::fwrite(header.data(), 1, header.size(), file_) != header.size()) {
return Status::fail(JOC_ERR_OUTPUT_WRITE, stage::kOutput, "chunk header write failed");
}
if (!body.empty() &&
std::fwrite(body.data(), 1, body.size(), file_) != body.size()) {
return Status::fail(JOC_ERR_OUTPUT_WRITE, stage::kOutput, "chunk body write failed");
}
if ((body.size() & 1u) != 0u) {
const char pad = '\0';
if (std::fwrite(&pad, 1, 1, file_) != 1) {
return Status::fail(JOC_ERR_OUTPUT_WRITE, stage::kOutput, "chunk padding write failed");
}
}
return Status::success();
}
Status AdmBwfWriter::flush() {
if (used_ == 0) {
return Status::success();
}
if (file_ == nullptr) {
return Status::fail(JOC_ERR_STATE, stage::kOutput, "ADM writer is not open");
}
packed_.clear();
pack_int24(buffer_.data(), used_, kChannels, &packed_);
if (std::fwrite(packed_.data(), 1, packed_.size(), file_) != packed_.size()) {
return Status::fail(JOC_ERR_OUTPUT_WRITE, stage::kOutput, "audio write failed for " + path_);
}
used_ = 0;
return Status::success();
}
Status AdmBwfWriter::write_objects16(const float* planar16) {
if (file_ == nullptr) {
return Status::fail(JOC_ERR_STATE, stage::kOutput, "ADM writer is not open");
}
if (planar16 == nullptr) {
return Status::fail(JOC_ERR_INVALID_ARGUMENT, stage::kOutput, "null frame");
}
std::size_t source = 0;
while (source < JOC_FRAME_SAMPLES) {
const std::size_t available = block_samples_ - used_;
const std::size_t count =
std::min(available, static_cast<std::size_t>(JOC_FRAME_SAMPLES) - source);
float* target = buffer_.data() + used_ * kChannels;
std::memset(target, 0, count * kChannels * sizeof(float));
for (std::size_t sample = 0; sample < count; ++sample) {
float* row = target + sample * kChannels;
row[3] = planar16[0u * JOC_FRAME_SAMPLES + source + sample];
for (std::size_t object = 0; object < 15u; ++object) {
row[10u + object] =
planar16[(object + 1u) * JOC_FRAME_SAMPLES + source + sample];
}
}
used_ += count;
source += count;
if (used_ == block_samples_) {
const Status status = flush();
if (!status.ok()) {
return status;
}
}
}
frames_ += JOC_FRAME_SAMPLES;
return Status::success();
}
Status AdmBwfWriter::finalize(const std::string& axml, const std::string& chna,
const std::string& dbmd) {
if (file_ == nullptr) {
return Status::fail(JOC_ERR_STATE, stage::kOutput, "ADM writer is not open");
}
if (finalized_) {
return Status::fail(JOC_ERR_STATE, stage::kOutput, "ADM writer already finalized");
}
Status status = flush();
if (!status.ok()) {
return status;
}
status = write_chunk("axml", axml);
if (!status.ok()) {
return status;
}
status = write_chunk("chna", chna);
if (!status.ok()) {
return status;
}
status = write_chunk("dbmd", dbmd);
if (!status.ok()) {
return status;
}
if (std::fseek(file_, 0, SEEK_END) != 0) {
return Status::fail(JOC_ERR_OUTPUT_WRITE, stage::kOutput, "seek failed for " + path_);
}
const long long total = std::ftell(file_);
if (total < 0) {
return Status::fail(JOC_ERR_OUTPUT_WRITE, stage::kOutput, "tell failed for " + path_);
}
const std::uint64_t data_len = frames_ * kChannels * 3u;
const std::uint32_t data_field =
data_len <= 0xFFFFFFFFull ? static_cast<std::uint32_t>(data_len) : 0xFFFFFFFFu;
if (std::fseek(file_, kDataSizeOffset, SEEK_SET) != 0) {
return Status::fail(JOC_ERR_OUTPUT_WRITE, stage::kOutput, "seek failed for " + path_);
}
char buffer[4];
std::memcpy(buffer, &data_field, 4);
if (std::fwrite(buffer, 1, 4, file_) != 4) {
return Status::fail(JOC_ERR_OUTPUT_WRITE, stage::kOutput, "data size patch failed");
}
std::string ds64;
put_u64(&ds64, static_cast<std::uint64_t>(total) - 8u);
put_u64(&ds64, data_len);
put_u64(&ds64, frames_);
put_u32(&ds64, 0);
if (std::fseek(file_, kDs64BodyOffset, SEEK_SET) != 0) {
return Status::fail(JOC_ERR_OUTPUT_WRITE, stage::kOutput, "seek failed for " + path_);
}
if (std::fwrite(ds64.data(), 1, ds64.size(), file_) != ds64.size()) {
return Status::fail(JOC_ERR_OUTPUT_WRITE, stage::kOutput, "ds64 patch failed");
}
finalized_ = true;
if (std::fclose(file_) != 0) {
file_ = nullptr;
return Status::fail(JOC_ERR_OUTPUT_WRITE, stage::kOutput, "close failed for " + path_);
}
file_ = nullptr;
return Status::success();
}
void AdmBwfWriter::abort() {
if (file_ != nullptr) {
std::fclose(file_);
file_ = nullptr;
}
if (!finalized_ && !path_.empty()) {
fs_utf8::remove(path_);
}
}
} // namespace joc::io
+52
View File
@@ -0,0 +1,52 @@
#pragma once
#include <cstdint>
#include <cstdio>
#include <string>
#include <vector>
#include "foundation/status.h"
#include "joc_core.h"
namespace joc::io {
class AdmBwfWriter {
public:
static constexpr std::uint32_t kChannels = 25;
static constexpr std::uint32_t kRate = 48000;
static constexpr std::size_t kDefaultBlockSamples = 131072;
AdmBwfWriter() = default;
~AdmBwfWriter();
AdmBwfWriter(const AdmBwfWriter&) = delete;
AdmBwfWriter& operator=(const AdmBwfWriter&) = delete;
Status open(const std::string& path, std::size_t block_samples = kDefaultBlockSamples);
Status write_objects16(const float* planar16);
Status finalize(const std::string& axml, const std::string& chna, const std::string& dbmd);
// Closes and removes a file that was never finalized (plan 28.3: abort must
void abort();
std::uint64_t frames() const { return frames_; }
bool open_ok() const { return file_ != nullptr; }
private:
Status write_chunk(const char id[4], const std::string& body);
Status flush();
std::FILE* file_ = nullptr;
std::string path_;
std::size_t block_samples_ = kDefaultBlockSamples;
std::size_t used_ = 0;
std::uint64_t frames_ = 0;
std::vector<float> buffer_;
std::string packed_;
bool finalized_ = false;
};
} // namespace joc::io
File diff suppressed because it is too large Load Diff
+87
View File
@@ -0,0 +1,87 @@
#pragma once
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
#include "foundation/status.h"
// Read-only subset of the HDF5 file format, sized for the SOFA files this
// project consumes: superblock v0, version 2 object headers, fractal-heap link
// and attribute storage, compact and contiguous datasets. The file is opened
// lazily: only the requested dataset's bytes are read into memory, everything
// else (superblock, object headers, heap blocks) is fetched on demand and the
// metadata that was parsed is cached by file address.
//
// Paths are HDF5 link paths ("Data.IR" is a single link name here, "Group/Set"
// walks two links); the empty path names the root group. Byte order is
// normalized on read, so callers never see the file's own endianness.
namespace joc::io {
enum class Hdf5Type {
Unknown,
Int8,
Int16,
Int32,
Int64,
UInt8,
UInt16,
UInt32,
UInt64,
Float32,
Float64,
String,
};
struct Hdf5TypeInfo {
Hdf5Type type = Hdf5Type::Unknown;
std::uint32_t size = 0; // bytes per element as stored in the file
bool big_endian = false;
bool is_signed = false;
};
struct Hdf5DatasetInfo {
std::vector<std::uint64_t> shape;
Hdf5TypeInfo type;
std::uint64_t element_count() const;
};
struct Hdf5Attribute {
Hdf5TypeInfo type;
std::vector<std::uint64_t> shape;
std::vector<std::uint8_t> raw; // C order, host byte order
std::string text; // decoded for fixed-length string attributes
};
class Hdf5File {
public:
Hdf5File();
~Hdf5File();
Hdf5File(Hdf5File&&) noexcept;
Hdf5File& operator=(Hdf5File&&) noexcept;
Hdf5File(const Hdf5File&) = delete;
Hdf5File& operator=(const Hdf5File&) = delete;
Status open(const std::string& path);
bool is_open() const;
// Names of the links of a group ("" is the root group).
Status links(const std::string& group_path, std::vector<std::string>* names) const;
bool has_dataset(const std::string& path) const;
Status dataset_info(const std::string& path, Hdf5DatasetInfo* out) const;
Status read_dataset_raw(const std::string& path, std::vector<std::uint8_t>* out) const;
Status read_dataset_double(const std::string& path, std::vector<double>* out) const;
Status attribute_names(const std::string& object_path, std::vector<std::string>* names) const;
Status attribute(const std::string& object_path, const std::string& name, Hdf5Attribute* out) const;
Status attribute_text(const std::string& object_path, const std::string& name, std::string* out) const;
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace joc::io
+276
View File
@@ -0,0 +1,276 @@
#include "io/inflate.h"
#include <cstring>
namespace joc::io {
namespace {
class LsbBitReader {
public:
LsbBitReader(const std::uint8_t* data, std::size_t size) : data_(data), size_(size) {}
bool ok() const { return ok_; }
std::size_t byte_position() const { return position_ >> 3; }
std::uint32_t bits(unsigned count) {
std::uint32_t value = 0;
for (unsigned i = 0; i < count; ++i) {
if ((position_ >> 3) >= size_) {
ok_ = false;
return value;
}
const std::uint32_t bit = (data_[position_ >> 3] >> (position_ & 7u)) & 1u;
value |= bit << i;
++position_;
}
return value;
}
void align_to_byte() { position_ = (position_ + 7u) & ~static_cast<std::size_t>(7u); }
void skip_bytes(std::size_t count) { position_ += count * 8u; }
private:
const std::uint8_t* data_;
std::size_t size_;
std::size_t position_ = 0;
bool ok_ = true;
};
struct Huffman {
std::uint16_t counts[16] = {};
std::uint16_t symbols[288] = {};
int max_length = 0;
bool build(const std::uint8_t* lengths, int count) {
for (int i = 0; i < 16; ++i) {
counts[i] = 0;
}
for (int i = 0; i < count; ++i) {
counts[lengths[i]]++;
}
counts[0] = 0;
std::uint16_t offsets[16] = {};
std::uint16_t total = 0;
for (int length = 1; length < 16; ++length) {
offsets[length] = total;
total = static_cast<std::uint16_t>(total + counts[length]);
}
if (total == 0) {
return false;
}
for (int symbol = 0; symbol < count; ++symbol) {
const std::uint8_t length = lengths[symbol];
if (length != 0) {
symbols[offsets[length]++] = static_cast<std::uint16_t>(symbol);
}
}
max_length = 15;
while (max_length > 0 && counts[max_length] == 0) {
--max_length;
}
return max_length != 0;
}
int decode(LsbBitReader* reader) const {
int code = 0;
int first = 0;
int index = 0;
for (int length = 1; length <= max_length; ++length) {
code |= static_cast<int>(reader->bits(1));
if (!reader->ok()) {
return -1;
}
const int count = counts[length];
if (code - first < count) {
return symbols[index + (code - first)];
}
index += count;
first = (first + count) << 1;
code <<= 1;
}
return -1;
}
};
constexpr std::uint16_t kLengthBase[29] = {3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19,
23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163,
195, 227, 258};
constexpr std::uint8_t kLengthExtra[29] = {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2,
2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0};
constexpr std::uint16_t kDistanceBase[30] = {1, 2, 3, 4, 5, 7, 9, 13,
17, 25, 33, 49, 65, 97, 129, 193,
257, 385, 513, 769, 1025, 1537, 2049, 3073,
4097, 6145, 8193, 12289, 16385, 24577};
constexpr std::uint8_t kDistanceExtra[30] = {0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6,
6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13};
constexpr std::uint8_t kCodeLengthOrder[19] = {16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2,
14, 1, 15};
bool inflate_block_data(LsbBitReader* reader, const Huffman& literal, const Huffman& distance,
std::vector<std::uint8_t>* out) {
for (;;) {
const int symbol = literal.decode(reader);
if (symbol < 0) {
return false;
}
if (symbol < 256) {
out->push_back(static_cast<std::uint8_t>(symbol));
continue;
}
if (symbol == 256) {
return true;
}
const int length_index = symbol - 257;
if (length_index >= 29) {
return false;
}
const std::uint32_t length =
kLengthBase[length_index] + reader->bits(kLengthExtra[length_index]);
const int distance_symbol = distance.decode(reader);
if (distance_symbol < 0 || distance_symbol >= 30) {
return false;
}
const std::uint32_t distance_value =
kDistanceBase[distance_symbol] + reader->bits(kDistanceExtra[distance_symbol]);
if (!reader->ok() || distance_value == 0 || distance_value > out->size()) {
return false;
}
const std::size_t start = out->size() - distance_value;
for (std::uint32_t i = 0; i < length; ++i) {
out->push_back((*out)[start + i]);
}
}
}
bool inflate_fixed(LsbBitReader* reader, std::vector<std::uint8_t>* out) {
std::uint8_t lengths[288];
for (int i = 0; i < 144; ++i) { lengths[i] = 8; }
for (int i = 144; i < 256; ++i) { lengths[i] = 9; }
for (int i = 256; i < 280; ++i) { lengths[i] = 7; }
for (int i = 280; i < 288; ++i) { lengths[i] = 8; }
Huffman literal;
if (!literal.build(lengths, 288)) {
return false;
}
std::uint8_t distance_lengths[30];
for (int i = 0; i < 30; ++i) { distance_lengths[i] = 5; }
Huffman distance;
if (!distance.build(distance_lengths, 30)) {
return false;
}
return inflate_block_data(reader, literal, distance, out);
}
bool inflate_dynamic(LsbBitReader* reader, std::vector<std::uint8_t>* out) {
const int literal_count = static_cast<int>(reader->bits(5)) + 257;
const int distance_count = static_cast<int>(reader->bits(5)) + 1;
const int code_length_count = static_cast<int>(reader->bits(4)) + 4;
if (!reader->ok() || literal_count > 286 || distance_count > 30) {
return false;
}
std::uint8_t code_lengths[19] = {};
for (int i = 0; i < code_length_count; ++i) {
code_lengths[kCodeLengthOrder[i]] = static_cast<std::uint8_t>(reader->bits(3));
}
if (!reader->ok()) {
return false;
}
Huffman code_length_tree;
if (!code_length_tree.build(code_lengths, 19)) {
return false;
}
std::uint8_t lengths[288 + 30] = {};
const int total = literal_count + distance_count;
int index = 0;
while (index < total) {
const int symbol = code_length_tree.decode(reader);
if (symbol < 0) {
return false;
}
if (symbol < 16) {
lengths[index++] = static_cast<std::uint8_t>(symbol);
continue;
}
int repeat = 0;
std::uint8_t value = 0;
if (symbol == 16) {
if (index == 0) {
return false;
}
value = lengths[index - 1];
repeat = 3 + static_cast<int>(reader->bits(2));
} else if (symbol == 17) {
repeat = 3 + static_cast<int>(reader->bits(3));
} else {
repeat = 11 + static_cast<int>(reader->bits(7));
}
if (!reader->ok() || index + repeat > total) {
return false;
}
for (int i = 0; i < repeat; ++i) {
lengths[index++] = value;
}
}
Huffman literal;
if (!literal.build(lengths, literal_count)) {
return false;
}
Huffman distance;
if (!distance.build(lengths + literal_count, distance_count)) {
return false;
}
return inflate_block_data(reader, literal, distance, out);
}
} // namespace
bool inflate_raw(const std::uint8_t* data, std::size_t size, std::vector<std::uint8_t>* out) {
if (data == nullptr || out == nullptr) {
return false;
}
out->clear();
LsbBitReader reader(data, size);
for (;;) {
const std::uint32_t final_block = reader.bits(1);
const std::uint32_t type = reader.bits(2);
if (!reader.ok()) {
return false;
}
if (type == 0) {
reader.align_to_byte();
const std::size_t position = reader.byte_position();
if (position + 4 > size) {
return false;
}
const std::uint16_t length = static_cast<std::uint16_t>(data[position] | (data[position + 1] << 8));
const std::uint16_t complement =
static_cast<std::uint16_t>(data[position + 2] | (data[position + 3] << 8));
if (static_cast<std::uint16_t>(length ^ 0xFFFFu) != complement) {
return false;
}
if (position + 4 + length > size) {
return false;
}
out->insert(out->end(), data + position + 4, data + position + 4 + length);
reader.skip_bytes(4u + length);
} else if (type == 1) {
if (!inflate_fixed(&reader, out)) {
return false;
}
} else if (type == 2) {
if (!inflate_dynamic(&reader, out)) {
return false;
}
} else {
return false;
}
if (final_block != 0u) {
break;
}
}
return true;
}
} // namespace joc::io
+12
View File
@@ -0,0 +1,12 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <vector>
namespace joc::io {
bool inflate_raw(const std::uint8_t* data, std::size_t size, std::vector<std::uint8_t>* out);
} // namespace joc::io
+420
View File
@@ -0,0 +1,420 @@
#include "io/npy.h"
#include <cstring>
namespace joc::io {
namespace {
std::uint16_t read_u16(const std::uint8_t* p) { return static_cast<std::uint16_t>(p[0] | (p[1] << 8)); }
std::uint32_t read_u32(const std::uint8_t* p) {
return static_cast<std::uint32_t>(p[0]) | (static_cast<std::uint32_t>(p[1]) << 8) |
(static_cast<std::uint32_t>(p[2]) << 16) | (static_cast<std::uint32_t>(p[3]) << 24);
}
NpyType classify(const std::string& descr) {
if (descr == "<f8" || descr == "=f8" || descr == "|f8") { return NpyType::Float64; }
if (descr == "<f4" || descr == "=f4") { return NpyType::Float32; }
if (descr == "<i8" || descr == "=i8") { return NpyType::Int64; }
if (descr == "<i4" || descr == "=i4") { return NpyType::Int32; }
if (descr == "<i2" || descr == "=i2") { return NpyType::Int16; }
if (descr == "|u1" || descr == "<u1") { return NpyType::UInt8; }
if (descr == "<c16" || descr == "=c16") { return NpyType::Complex128; }
if (descr.size() > 2 && descr[0] == '<' && descr[1] == 'U') {
return NpyType::Unicode;
}
if (descr.size() > 2 && descr[0] == '=' && descr[1] == 'U') {
return NpyType::Unicode;
}
return NpyType::Unknown;
}
std::size_t unicode_length(const std::string& descr) {
std::size_t index = 0;
while (index < descr.size() && (descr[index] == '<' || descr[index] == '=')) {
++index;
}
if (index >= descr.size() || descr[index] != 'U') {
return 0;
}
++index;
std::size_t value = 0;
bool any = false;
while (index < descr.size() && descr[index] >= '0' && descr[index] <= '9') {
value = value * 10 + static_cast<std::size_t>(descr[index] - '0');
++index;
any = true;
}
return any ? value : 0;
}
bool is_big_endian(const std::string& descr) { return !descr.empty() && descr[0] == '>'; }
bool header_value(const std::string& header, const std::string& key, std::string* out) {
const std::string needle = "'" + key + "'";
const std::size_t position = header.find(needle);
if (position == std::string::npos) {
return false;
}
const std::size_t colon = header.find(':', position + needle.size());
if (colon == std::string::npos) {
return false;
}
std::size_t start = colon + 1;
while (start < header.size() && (header[start] == ' ' || header[start] == '\t')) {
++start;
}
*out = header.substr(start);
return true;
}
} // namespace
std::size_t NpyArray::element_count() const {
std::size_t count = 1;
for (const std::int64_t dimension : shape) {
count *= static_cast<std::size_t>(dimension < 0 ? 0 : dimension);
}
return count;
}
std::size_t NpyArray::element_size() const {
switch (type) {
case NpyType::Float64: return 8;
case NpyType::Float32: return 4;
case NpyType::Int64: return 8;
case NpyType::Int32: return 4;
case NpyType::Int16: return 2;
case NpyType::UInt8: return 1;
case NpyType::Complex128: return 16;
case NpyType::Unicode: return item_bytes;
default: return 0;
}
}
bool parse_npy(const std::uint8_t* data, std::size_t size, NpyArray* out, std::string* error) {
if (data == nullptr || out == nullptr) {
return false;
}
const std::uint8_t magic[6] = {0x93u, 'N', 'U', 'M', 'P', 'Y'};
if (size < 10u || std::memcmp(data, magic, 6) != 0) {
if (error != nullptr) { *error = "not a .npy image"; }
return false;
}
const std::uint8_t major = data[6];
std::size_t header_length = 0;
std::size_t header_offset = 0;
if (major == 1u) {
header_length = read_u16(data + 8);
header_offset = 10;
} else if (major == 2u || major == 3u) {
if (size < 12u) {
if (error != nullptr) { *error = "truncated .npy v2 header"; }
return false;
}
header_length = read_u32(data + 8);
header_offset = 12;
} else {
if (error != nullptr) { *error = "unsupported .npy version " + std::to_string(major); }
return false;
}
if (header_offset + header_length > size) {
if (error != nullptr) { *error = "truncated .npy header"; }
return false;
}
const std::string header(reinterpret_cast<const char*>(data + header_offset), header_length);
out->descr.clear();
std::string value;
if (!header_value(header, "descr", &value)) {
if (error != nullptr) { *error = ".npy header without descr"; }
return false;
}
const std::size_t first_quote = value.find('\'');
const std::size_t second_quote =
first_quote == std::string::npos ? std::string::npos : value.find('\'', first_quote + 1);
if (first_quote == std::string::npos || second_quote == std::string::npos) {
if (error != nullptr) { *error = ".npy descr is not a quoted string"; }
return false;
}
out->descr = value.substr(first_quote + 1, second_quote - first_quote - 1);
out->type = classify(out->descr);
if (out->type == NpyType::Unknown) {
if (error != nullptr) { *error = "unsupported .npy dtype " + out->descr; }
return false;
}
out->item_bytes = 0;
if (out->type == NpyType::Unicode) {
const std::size_t length = unicode_length(out->descr);
if (length == 0) {
if (error != nullptr) { *error = "malformed unicode .npy dtype " + out->descr; }
return false;
}
out->item_bytes = length * 4u;
}
out->fortran_order = header.find("'fortran_order': True") != std::string::npos;
if (!header_value(header, "shape", &value)) {
if (error != nullptr) { *error = ".npy header without shape"; }
return false;
}
out->shape.clear();
for (std::size_t i = 0; i < value.size(); ++i) {
if (value[i] >= '0' && value[i] <= '9') {
long long dimension = 0;
while (i < value.size() && value[i] >= '0' && value[i] <= '9') {
dimension = dimension * 10 + (value[i] - '0');
++i;
}
out->shape.push_back(dimension);
} else if (value[i] == ')') {
break;
}
}
const std::size_t expected = out->element_count() * out->element_size();
if (header_offset + header_length + expected > size) {
if (error != nullptr) {
*error = ".npy payload truncated (need " + std::to_string(expected) + " bytes)";
}
return false;
}
out->data = data + header_offset + header_length;
out->data_bytes = expected;
return true;
}
bool npy_shape_is(const NpyArray& array, const std::vector<std::int64_t>& expected) {
return array.shape == expected;
}
namespace {
template <typename T>
void load_le(const std::uint8_t* source, std::size_t count, bool swap, std::vector<T>* out) {
out->resize(count);
std::memcpy(out->data(), source, count * sizeof(T));
if (swap) {
std::uint8_t* bytes = reinterpret_cast<std::uint8_t*>(out->data());
for (std::size_t i = 0; i < count; ++i) {
for (std::size_t b = 0; b < sizeof(T) / 2; ++b) {
const std::uint8_t temporary = bytes[i * sizeof(T) + b];
bytes[i * sizeof(T) + b] = bytes[i * sizeof(T) + sizeof(T) - 1 - b];
bytes[i * sizeof(T) + sizeof(T) - 1 - b] = temporary;
}
}
}
}
} // namespace
bool npy_to_double(const NpyArray& array, std::vector<double>* out, std::string* error) {
const bool swap = is_big_endian(array.descr);
const std::size_t count = array.element_count();
switch (array.type) {
case NpyType::Float64:
load_le(array.data, count, swap, out);
return true;
case NpyType::Complex128:
load_le(array.data, count * 2u, swap, out);
return true;
case NpyType::Float32: {
std::vector<float> values;
load_le(array.data, count, swap, &values);
out->resize(count);
for (std::size_t i = 0; i < count; ++i) {
(*out)[i] = static_cast<double>(values[i]);
}
return true;
}
case NpyType::Int64: {
std::vector<std::int64_t> values;
load_le(array.data, count, swap, &values);
out->resize(count);
for (std::size_t i = 0; i < count; ++i) {
(*out)[i] = static_cast<double>(values[i]);
}
return true;
}
case NpyType::Int32: {
std::vector<std::int32_t> values;
load_le(array.data, count, swap, &values);
out->resize(count);
for (std::size_t i = 0; i < count; ++i) {
(*out)[i] = static_cast<double>(values[i]);
}
return true;
}
case NpyType::Int16: {
std::vector<std::int16_t> values;
load_le(array.data, count, swap, &values);
out->resize(count);
for (std::size_t i = 0; i < count; ++i) {
(*out)[i] = static_cast<double>(values[i]);
}
return true;
}
case NpyType::UInt8: {
out->resize(count);
for (std::size_t i = 0; i < count; ++i) {
(*out)[i] = static_cast<double>(array.data[i]);
}
return true;
}
default:
if (error != nullptr) { *error = "cannot convert " + array.descr + " to double"; }
return false;
}
}
bool npy_to_int16(const NpyArray& array, std::vector<std::int16_t>* out, std::string* error) {
const bool swap = is_big_endian(array.descr);
const std::size_t count = array.element_count();
switch (array.type) {
case NpyType::Int16:
load_le(array.data, count, swap, out);
return true;
case NpyType::Int32: {
std::vector<std::int32_t> values;
load_le(array.data, count, swap, &values);
out->resize(count);
for (std::size_t i = 0; i < count; ++i) {
(*out)[i] = static_cast<std::int16_t>(values[i]);
}
return true;
}
case NpyType::Int64: {
std::vector<std::int64_t> values;
load_le(array.data, count, swap, &values);
out->resize(count);
for (std::size_t i = 0; i < count; ++i) {
(*out)[i] = static_cast<std::int16_t>(values[i]);
}
return true;
}
default:
if (error != nullptr) { *error = "cannot convert " + array.descr + " to int16"; }
return false;
}
}
bool npy_to_int32(const NpyArray& array, std::vector<std::int32_t>* out, std::string* error) {
const bool swap = is_big_endian(array.descr);
const std::size_t count = array.element_count();
switch (array.type) {
case NpyType::Int32:
load_le(array.data, count, swap, out);
return true;
case NpyType::Int64: {
std::vector<std::int64_t> values;
load_le(array.data, count, swap, &values);
out->resize(count);
for (std::size_t i = 0; i < count; ++i) {
(*out)[i] = static_cast<std::int32_t>(values[i]);
}
return true;
}
case NpyType::Int16: {
std::vector<std::int16_t> values;
load_le(array.data, count, swap, &values);
out->resize(count);
for (std::size_t i = 0; i < count; ++i) {
(*out)[i] = static_cast<std::int32_t>(values[i]);
}
return true;
}
default:
if (error != nullptr) { *error = "cannot convert " + array.descr + " to int32"; }
return false;
}
}
bool npy_to_uint8(const NpyArray& array, std::vector<std::uint8_t>* out, std::string* error) {
if (array.type != NpyType::UInt8) {
if (error != nullptr) { *error = "cannot convert " + array.descr + " to uint8"; }
return false;
}
out->assign(array.data, array.data + array.element_count());
return true;
}
bool npy_unicode_to_utf8(const NpyArray& array, std::string* out, std::string* error) {
if (array.type != NpyType::Unicode) {
if (error != nullptr) { *error = "not a unicode .npy member: " + array.descr; }
return false;
}
if (array.shape.size() != 0) {
if (error != nullptr) { *error = "unicode .npy member must be a scalar"; }
return false;
}
out->clear();
const std::size_t count = array.item_bytes / 4u;
for (std::size_t i = 0; i < count; ++i) {
const std::uint8_t* p = array.data + i * 4u;
const std::uint32_t code = static_cast<std::uint32_t>(p[0]) | (static_cast<std::uint32_t>(p[1]) << 8) |
(static_cast<std::uint32_t>(p[2]) << 16) |
(static_cast<std::uint32_t>(p[3]) << 24);
if (code == 0u) {
break;
}
if (code < 0x80u) {
out->push_back(static_cast<char>(code));
} else if (code < 0x800u) {
out->push_back(static_cast<char>(0xC0u | (code >> 6)));
out->push_back(static_cast<char>(0x80u | (code & 0x3Fu)));
} else if (code < 0x10000u) {
out->push_back(static_cast<char>(0xE0u | (code >> 12)));
out->push_back(static_cast<char>(0x80u | ((code >> 6) & 0x3Fu)));
out->push_back(static_cast<char>(0x80u | (code & 0x3Fu)));
} else {
out->push_back(static_cast<char>(0xF0u | (code >> 18)));
out->push_back(static_cast<char>(0x80u | ((code >> 12) & 0x3Fu)));
out->push_back(static_cast<char>(0x80u | ((code >> 6) & 0x3Fu)));
out->push_back(static_cast<char>(0x80u | (code & 0x3Fu)));
}
}
return true;
}
bool npy_to_c_order(const NpyArray& array, std::vector<std::uint8_t>* out, std::string* error) {
const std::size_t element = array.element_size();
if (element == 0) {
if (error != nullptr) { *error = "unsupported element size for " + array.descr; }
return false;
}
if (!array.fortran_order) {
out->assign(array.data, array.data + array.data_bytes);
return true;
}
const std::size_t dimensions = array.shape.size();
if (dimensions == 0) {
out->assign(array.data, array.data + element);
return true;
}
// Source (Fortran) strides in elements; destination is C order.
std::vector<std::size_t> source_stride(dimensions, 1);
std::size_t running = 1;
for (std::size_t d = 0; d < dimensions; ++d) {
source_stride[d] = running;
running *= static_cast<std::size_t>(array.shape[d]);
}
out->assign(array.data_bytes, 0);
std::vector<std::size_t> index(dimensions, 0);
const std::size_t total = array.element_count();
for (std::size_t linear = 0; linear < total; ++linear) {
std::size_t remainder = linear;
for (std::size_t d = dimensions; d-- > 0;) {
index[d] = remainder % static_cast<std::size_t>(array.shape[d]);
remainder /= static_cast<std::size_t>(array.shape[d]);
}
std::size_t source = 0;
for (std::size_t d = 0; d < dimensions; ++d) {
source += index[d] * source_stride[d];
}
std::memcpy(out->data() + linear * element, array.data + source * element, element);
}
return true;
}
} // namespace joc::io
+42
View File
@@ -0,0 +1,42 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
namespace joc::io {
enum class NpyType { Unknown, Float64, Float32, Int64, Int32, Int16, UInt8, Complex128, Unicode };
struct NpyArray {
std::string descr;
NpyType type = NpyType::Unknown;
bool fortran_order = false;
std::vector<std::int64_t> shape;
const std::uint8_t* data = nullptr;
std::size_t data_bytes = 0;
std::size_t item_bytes = 0; // bytes per element as stored
std::size_t element_count() const;
std::size_t element_size() const; // bytes per element in the file
};
// Parses the header of one `.npy` image. `data` must outlive the NpyArray.
bool parse_npy(const std::uint8_t* data, std::size_t size, NpyArray* out, std::string* error);
bool npy_to_double(const NpyArray& array, std::vector<double>* out, std::string* error);
bool npy_to_int16(const NpyArray& array, std::vector<std::int16_t>* out, std::string* error);
bool npy_to_int32(const NpyArray& array, std::vector<std::int32_t>* out, std::string* error);
bool npy_to_uint8(const NpyArray& array, std::vector<std::uint8_t>* out, std::string* error);
bool npy_unicode_to_utf8(const NpyArray& array, std::string* out, std::string* error);
// Materializes the array in C order as raw element bytes. Fortran-order members
// hybrid synthesis table as [count][4] row-major while the shipped table stores it
// Fortran-order, so passing the file bytes straight through would transpose it.
bool npy_to_c_order(const NpyArray& array, std::vector<std::uint8_t>* out, std::string* error);
bool npy_shape_is(const NpyArray& array, const std::vector<std::int64_t>& expected);
} // namespace joc::io
+263
View File
@@ -0,0 +1,263 @@
#include "io/npy_writer.h"
#include <array>
#include <charconv>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <string>
#include "foundation/fs_utf8.h"
#include "io/zip_reader.h"
namespace joc::io {
namespace {
constexpr std::size_t kNpyHeaderAlignment = 64;
void append_u16(std::vector<std::uint8_t>* out, std::uint16_t value) {
out->push_back(static_cast<std::uint8_t>(value & 0xFFu));
out->push_back(static_cast<std::uint8_t>((value >> 8) & 0xFFu));
}
void append_u32(std::vector<std::uint8_t>* out, std::uint32_t value) {
for (int index = 0; index < 4; ++index) {
out->push_back(static_cast<std::uint8_t>((value >> (8 * index)) & 0xFFu));
}
}
void append_bytes(std::vector<std::uint8_t>* out, const void* data, std::size_t size) {
const std::uint8_t* bytes = static_cast<const std::uint8_t*>(data);
out->insert(out->end(), bytes, bytes + size);
}
std::string shape_literal(const std::vector<std::uint64_t>& shape) {
if (shape.empty()) {
return "()";
}
std::string text = "(";
for (std::size_t index = 0; index < shape.size(); ++index) {
if (index != 0u) {
text += ", ";
}
text += std::to_string(shape[index]);
}
if (shape.size() == 1u) {
text += ",";
}
text += ")";
return text;
}
} // namespace
std::vector<std::uint8_t> npy_image(const std::string& descr,
const std::vector<std::uint64_t>& shape,
const std::vector<std::uint8_t>& data) {
std::string header = "{'descr': '" + descr + "', 'fortran_order': False, 'shape': " +
shape_literal(shape) + ", }";
// NumPy pads the header so that the payload starts on a 64-byte boundary.
const std::size_t preamble = 10u; // magic, version, two byte header length
std::size_t total = preamble + header.size() + 1u;
const std::size_t padding = (kNpyHeaderAlignment - (total % kNpyHeaderAlignment)) %
kNpyHeaderAlignment;
header.append(padding, ' ');
header.push_back('\n');
std::vector<std::uint8_t> out;
out.reserve(preamble + header.size() + data.size());
static const std::uint8_t kMagic[6] = {0x93u, 'N', 'U', 'M', 'P', 'Y'};
append_bytes(&out, kMagic, sizeof(kMagic));
out.push_back(1u); // major
out.push_back(0u); // minor
append_u16(&out, static_cast<std::uint16_t>(header.size()));
append_bytes(&out, header.data(), header.size());
append_bytes(&out, data.data(), data.size());
return out;
}
std::vector<std::uint8_t> zip_bytes(const std::vector<NpyMember>& members) {
std::vector<std::uint8_t> out;
struct Entry {
std::string name;
std::uint32_t crc = 0;
std::uint32_t size = 0;
std::uint32_t offset = 0;
};
std::vector<Entry> entries;
entries.reserve(members.size());
for (const NpyMember& member : members) {
const std::string name = member.name + ".npy";
const std::vector<std::uint8_t> payload = npy_image(member.descr, member.shape, member.data);
Entry entry;
entry.name = name;
entry.crc = crc32_of(payload.data(), payload.size());
entry.size = static_cast<std::uint32_t>(payload.size());
entry.offset = static_cast<std::uint32_t>(out.size());
entries.push_back(entry);
append_u32(&out, 0x04034B50u); // local file header
append_u16(&out, 20u); // version needed
append_u16(&out, 0u); // flags
append_u16(&out, 0u); // method: stored
append_u16(&out, 0u); // time
append_u16(&out, 0x2821u); // date: 2000-01-01, fixed for reproducibility
append_u32(&out, entry.crc);
append_u32(&out, entry.size);
append_u32(&out, entry.size);
append_u16(&out, static_cast<std::uint16_t>(name.size()));
append_u16(&out, 0u); // extra length
append_bytes(&out, name.data(), name.size());
append_bytes(&out, payload.data(), payload.size());
}
const std::uint32_t directory_offset = static_cast<std::uint32_t>(out.size());
for (const Entry& entry : entries) {
append_u32(&out, 0x02014B50u); // central directory header
append_u16(&out, 20u); // version made by
append_u16(&out, 20u); // version needed
append_u16(&out, 0u); // flags
append_u16(&out, 0u); // method: stored
append_u16(&out, 0u); // time
append_u16(&out, 0x2821u); // date
append_u32(&out, entry.crc);
append_u32(&out, entry.size);
append_u32(&out, entry.size);
append_u16(&out, static_cast<std::uint16_t>(entry.name.size()));
append_u16(&out, 0u); // extra
append_u16(&out, 0u); // comment
append_u16(&out, 0u); // disk
append_u16(&out, 0u); // internal attributes
append_u32(&out, 0u); // external attributes
append_u32(&out, entry.offset);
append_bytes(&out, entry.name.data(), entry.name.size());
}
const std::uint32_t directory_size = static_cast<std::uint32_t>(out.size()) - directory_offset;
append_u32(&out, 0x06054B50u); // end of central directory
append_u16(&out, 0u);
append_u16(&out, 0u);
append_u16(&out, static_cast<std::uint16_t>(entries.size()));
append_u16(&out, static_cast<std::uint16_t>(entries.size()));
append_u32(&out, directory_size);
append_u32(&out, directory_offset);
append_u16(&out, 0u);
return out;
}
bool write_zip(const std::string& path, const std::vector<NpyMember>& members,
std::string* error) {
const std::vector<std::uint8_t> bytes = zip_bytes(members);
std::FILE* stream = fs_utf8::fopen(path, "wb");
if (stream == nullptr) {
if (error != nullptr) {
*error = "cannot open " + path + " for writing";
}
return false;
}
const std::size_t written = std::fwrite(bytes.data(), 1, bytes.size(), stream);
const bool flushed = std::fclose(stream) == 0;
if (written != bytes.size() || !flushed) {
if (error != nullptr) {
*error = "short write to " + path;
}
return false;
}
return true;
}
std::vector<std::uint8_t> utf8_to_utf32le(const std::string& text) {
std::vector<std::uint8_t> out;
out.reserve(text.size() * 4u);
std::size_t index = 0;
while (index < text.size()) {
const std::uint8_t lead = static_cast<std::uint8_t>(text[index]);
std::uint32_t code = 0;
std::size_t extra = 0;
if (lead < 0x80u) {
code = lead;
} else if ((lead & 0xE0u) == 0xC0u) {
code = lead & 0x1Fu;
extra = 1;
} else if ((lead & 0xF0u) == 0xE0u) {
code = lead & 0x0Fu;
extra = 2;
} else if ((lead & 0xF8u) == 0xF0u) {
code = lead & 0x07u;
extra = 3;
} else {
code = 0xFFFDu; // invalid lead byte: substitute rather than fail
extra = 0;
}
++index;
for (std::size_t count = 0; count < extra && index < text.size(); ++count) {
code = (code << 6) | (static_cast<std::uint8_t>(text[index]) & 0x3Fu);
++index;
}
for (int byte = 0; byte < 4; ++byte) {
out.push_back(static_cast<std::uint8_t>((code >> (8 * byte)) & 0xFFu));
}
}
return out;
}
std::string python_float_repr(double value) {
if (std::isnan(value)) {
return "NaN";
}
if (std::isinf(value)) {
return value > 0.0 ? "Infinity" : "-Infinity";
}
// to_chars gives the shortest round-trip digits; Python's repr uses the same
// digits but its own notation, so the digits are re-laid-out here.
std::array<char, 64> buffer{};
const std::to_chars_result converted =
std::to_chars(buffer.data(), buffer.data() + buffer.size(), value);
std::string text(buffer.data(), converted.ptr);
const bool negative = !text.empty() && text[0] == '-';
const std::string body = negative ? text.substr(1) : text;
const std::size_t exponent_at = body.find_first_of("eE");
std::string digits = body;
int exponent = 0;
if (exponent_at != std::string::npos) {
digits = body.substr(0, exponent_at);
exponent = std::atoi(body.c_str() + exponent_at + 1);
}
const std::size_t point = digits.find('.');
std::string mantissa = digits;
if (point != std::string::npos) {
mantissa = digits.substr(0, point) + digits.substr(point + 1);
exponent += static_cast<int>(point) - 1;
} else {
exponent += static_cast<int>(digits.size()) - 1;
}
while (mantissa.size() > 1u && mantissa.back() == '0') {
mantissa.pop_back();
}
// Python switches to exponent notation below 1e-4 and at 1e16 and above.
std::string result;
if (exponent < -4 || exponent >= 16) {
result = mantissa.substr(0, 1);
if (mantissa.size() > 1u) {
result += "." + mantissa.substr(1);
}
char tail[16];
std::snprintf(tail, sizeof(tail), "e%+03d", exponent);
result += tail;
} else if (exponent >= 0) {
if (static_cast<std::size_t>(exponent) + 1u >= mantissa.size()) {
result = mantissa + std::string(static_cast<std::size_t>(exponent) + 1u - mantissa.size(), '0');
result += ".0";
} else {
result = mantissa.substr(0, static_cast<std::size_t>(exponent) + 1u) + "." +
mantissa.substr(static_cast<std::size_t>(exponent) + 1u);
}
} else {
result = "0." + std::string(static_cast<std::size_t>(-exponent - 1), '0') + mantissa;
}
return negative ? "-" + result : result;
}
} // namespace joc::io
+39
View File
@@ -0,0 +1,39 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
namespace joc::io {
// NPY 1.0 images and a minimal ZIP container, used to write the compiled HRTF
// cache in exactly the layout the reader (and NumPy) expects. Only what the
// cache needs is implemented: little-endian C-order arrays and stored members.
struct NpyMember {
std::string name; // archive member name, without the .npy suffix
std::string descr; // NumPy dtype string, e.g. "<f8", "<c16", "<U123"
std::vector<std::uint64_t> shape;
std::vector<std::uint8_t> data; // C order payload in the dtype's byte order
};
// Serializes one array as an NPY 1.0 image (magic, header, 64-byte aligned).
std::vector<std::uint8_t> npy_image(const std::string& descr,
const std::vector<std::uint64_t>& shape,
const std::vector<std::uint8_t>& data);
// Writes a ZIP archive with stored (uncompressed) members. The upstream reader
// accepts stored members, and compression would need a deflate encoder.
bool write_zip(const std::string& path, const std::vector<NpyMember>& members,
std::string* error);
// Serializes the archive in memory (same layout as write_zip).
std::vector<std::uint8_t> zip_bytes(const std::vector<NpyMember>& members);
// UTF-8 text as the payload of a NumPy Unicode scalar string ('<U<n>').
std::vector<std::uint8_t> utf8_to_utf32le(const std::string& text);
// Python's repr() for a double: shortest round-trip digits with Python's
// exponent rules, which is what json.dumps emits for the cache metadata.
std::string python_float_repr(double value);
} // namespace joc::io
+186
View File
@@ -0,0 +1,186 @@
#include "io/process.h"
#include "foundation/fs_utf8.h"
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <random>
#if defined(_WIN32)
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <windows.h>
#else
#include <sys/wait.h>
#endif
namespace joc::io {
namespace {
std::string quote_argument(const std::string& argument) {
if (!argument.empty() && argument.find_first_of(" \t\"") == std::string::npos) {
return argument;
}
std::string quoted = "\"";
unsigned backslashes = 0;
for (const char c : argument) {
if (c == '\\') {
++backslashes;
continue;
}
if (c == '"') {
quoted.append(backslashes * 2 + 1, '\\');
quoted.push_back('"');
backslashes = 0;
continue;
}
quoted.append(backslashes, '\\');
backslashes = 0;
quoted.push_back(c);
}
quoted.append(backslashes * 2, '\\');
quoted.push_back('"');
return quoted;
}
std::string tail_of(const std::string& text, std::size_t limit) {
if (text.size() <= limit) {
return text;
}
return text.substr(text.size() - limit);
}
} // namespace
Status run_process(const std::vector<std::string>& argv, ProcessResult* out) {
if (out == nullptr || argv.empty()) {
return Status::fail(JOC_ERR_INVALID_ARGUMENT, stage::kOutput, "empty command");
}
out->output.clear();
out->exit_code = 0;
const std::filesystem::path log_path =
std::filesystem::temp_directory_path() /
("joc_process_" + std::to_string(std::random_device{}()) + ".log");
auto read_log = [&]() {
#if defined(_WIN32)
return; // the Windows branch reads the handle it opened
#else
std::ifstream log = fs_utf8::open_input(fs_utf8::from_path(log_path));
if (log) {
std::string text((std::istreambuf_iterator<char>(log)),
std::istreambuf_iterator<char>());
out->output = tail_of(text, 4096);
}
#endif
};
#if defined(_WIN32)
std::string command;
for (std::size_t i = 0; i < argv.size(); ++i) {
if (i != 0) {
command.push_back(' ');
}
command += quote_argument(argv[i]);
}
auto widen = [](const std::string& text) {
if (text.empty()) {
return std::wstring();
}
const int size = MultiByteToWideChar(CP_UTF8, 0, text.c_str(),
static_cast<int>(text.size()), nullptr, 0);
std::wstring wide(static_cast<std::size_t>(size), L'\0');
MultiByteToWideChar(CP_UTF8, 0, text.c_str(), static_cast<int>(text.size()), wide.data(),
size);
return wide;
};
const std::wstring wide_command = widen(command);
const std::wstring wide_log = widen(fs_utf8::from_path(log_path));
SECURITY_ATTRIBUTES attributes{};
attributes.nLength = sizeof(attributes);
attributes.bInheritHandle = TRUE;
// DELETE access plus FILE_FLAG_DELETE_ON_CLOSE means the log disappears when
// the last handle goes away - including when this process is killed, which
// would otherwise leave joc_process_*.log litter in the temp directory.
HANDLE log_handle = CreateFileW(
wide_log.c_str(), GENERIC_READ | GENERIC_WRITE | DELETE,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, &attributes, CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_DELETE_ON_CLOSE, nullptr);
if (log_handle == INVALID_HANDLE_VALUE) {
return Status::fail(JOC_ERR_IO, stage::kOutput, "cannot create the process log file");
}
// A delete-on-close file cannot be reopened by name (it is delete-pending), so
// the child's output is read back through the handle it wrote to.
auto read_log_handle = [&]() {
LARGE_INTEGER start{};
start.QuadPart = 0;
if (!SetFilePointerEx(log_handle, start, nullptr, FILE_BEGIN)) {
return;
}
std::string text;
char buffer[1024];
DWORD count = 0;
while (ReadFile(log_handle, buffer, sizeof(buffer), &count, nullptr) && count > 0) {
text.append(buffer, count);
}
out->output = tail_of(text, 4096);
};
STARTUPINFOW startup{};
startup.cb = sizeof(startup);
startup.dwFlags = STARTF_USESTDHANDLES;
startup.hStdOutput = log_handle;
startup.hStdError = log_handle;
startup.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
PROCESS_INFORMATION process{};
std::vector<wchar_t> mutable_command(wide_command.begin(), wide_command.end());
mutable_command.push_back(L'\0');
const BOOL started = CreateProcessW(nullptr, mutable_command.data(), nullptr, nullptr, TRUE,
CREATE_NO_WINDOW, nullptr, nullptr, &startup, &process);
if (!started) {
CloseHandle(log_handle); // delete-on-close removes the file
return Status::fail(JOC_ERR_LIBRARY_MISSING, stage::kOutput,
"cannot start " + argv[0] + " (is it on PATH?)");
}
WaitForSingleObject(process.hProcess, INFINITE);
DWORD exit_code = 0;
GetExitCodeProcess(process.hProcess, &exit_code);
CloseHandle(process.hThread);
CloseHandle(process.hProcess);
// Read the log before the delete-on-close handle goes away.
read_log_handle();
CloseHandle(log_handle);
out->exit_code = static_cast<std::uint32_t>(exit_code);
#else
std::string command;
for (std::size_t i = 0; i < argv.size(); ++i) {
if (i != 0) {
command.push_back(' ');
}
command += quote_argument(argv[i]);
}
command += " > " + quote_argument(fs_utf8::from_path(log_path)) + " 2>&1";
const int status = std::system(command.c_str());
// system() reports a wait status, not the child's exit code.
out->exit_code = status == -1 ? 127u
: WIFEXITED(status) ? static_cast<std::uint32_t>(WEXITSTATUS(status))
: 128u;
read_log();
std::error_code ignored;
std::filesystem::remove(log_path, ignored);
#endif
if (out->exit_code != 0) {
return Status::fail(JOC_ERR_INPUT_FORMAT, stage::kOutput,
argv[0] + " failed with exit code " + std::to_string(out->exit_code) +
(out->output.empty() ? "" : ": " + tail_of(out->output, 400)));
}
return Status::success();
}
} // namespace joc::io
+19
View File
@@ -0,0 +1,19 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
#include "foundation/status.h"
namespace joc::io {
struct ProcessResult {
std::uint32_t exit_code = 0;
std::string output;
};
Status run_process(const std::vector<std::string>& argv, ProcessResult* out);
} // namespace joc::io
+242
View File
@@ -0,0 +1,242 @@
#include "io/wav_writer.h"
#include "foundation/fs_utf8.h"
#include <cmath>
#include <cstring>
#include <filesystem>
#include <limits>
#include <vector>
namespace joc::io {
namespace {
constexpr std::uint16_t kWaveFormatPcm = 0x0001;
constexpr std::uint16_t kWaveFormatIeeeFloat = 0x0003;
constexpr std::uint16_t kWaveFormatExtensible = 0xFFFE;
constexpr std::uint8_t kPcmGuid[16] = {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00,
0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71};
constexpr std::uint8_t kFloatGuid[16] = {0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00,
0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71};
void put_u16(std::string* out, std::uint16_t value) {
char buffer[2];
std::memcpy(buffer, &value, 2);
out->append(buffer, 2);
}
void put_u32(std::string* out, std::uint32_t value) {
char buffer[4];
std::memcpy(buffer, &value, 4);
out->append(buffer, 4);
}
void put_u64(std::string* out, std::uint64_t value) {
char buffer[8];
std::memcpy(buffer, &value, 8);
out->append(buffer, 8);
}
// Port of speaker_wav._fmt_chunk.
std::string fmt_chunk(std::uint32_t channels, std::uint32_t rate, SampleFormat format, WavInfo* info) {
std::uint16_t simple_tag = 0;
const std::uint8_t* guid = nullptr;
if (format == SampleFormat::Float32) {
info->bits_per_sample = 32;
info->bytes_per_sample = 4;
simple_tag = kWaveFormatIeeeFloat;
guid = kFloatGuid;
} else {
info->bits_per_sample = 24;
info->bytes_per_sample = 3;
simple_tag = kWaveFormatPcm;
guid = kPcmGuid;
}
const std::uint32_t block_align = channels * info->bytes_per_sample;
const std::uint32_t byte_rate = rate * block_align;
info->block_align = block_align;
std::string body;
if (channels <= 2) {
put_u16(&body, simple_tag);
put_u16(&body, static_cast<std::uint16_t>(channels));
put_u32(&body, rate);
put_u32(&body, byte_rate);
put_u16(&body, static_cast<std::uint16_t>(block_align));
put_u16(&body, static_cast<std::uint16_t>(info->bits_per_sample));
} else {
put_u16(&body, kWaveFormatExtensible);
put_u16(&body, static_cast<std::uint16_t>(channels));
put_u32(&body, rate);
put_u32(&body, byte_rate);
put_u16(&body, static_cast<std::uint16_t>(block_align));
put_u16(&body, static_cast<std::uint16_t>(info->bits_per_sample));
put_u16(&body, 22);
put_u16(&body, static_cast<std::uint16_t>(info->bits_per_sample));
put_u32(&body, 0);
body.append(reinterpret_cast<const char*>(guid), 16);
}
return body;
}
// int32 conversion identical to NumPy's float32 -> int32 cast after clipping.
std::int32_t to_int32(const float value) {
if (!std::isfinite(value)) {
return std::numeric_limits<std::int32_t>::min();
}
return static_cast<std::int32_t>(value);
}
} // namespace
void pack_int24(const float* interleaved, std::size_t frames, std::size_t channels,
std::string* out) {
const std::size_t count = frames * channels;
out->resize(count * 3);
char* target = out->data();
for (std::size_t i = 0; i < count; ++i) {
float value = interleaved[i];
if (value > 1.0f) {
value = 1.0f;
} else if (value < -1.0f) {
value = -1.0f;
}
const std::int32_t scaled = to_int32(value * 8388607.0f);
const std::uint32_t bits = static_cast<std::uint32_t>(scaled);
target[i * 3 + 0] = static_cast<char>(bits & 0xFFu);
target[i * 3 + 1] = static_cast<char>((bits >> 8) & 0xFFu);
target[i * 3 + 2] = static_cast<char>((bits >> 16) & 0xFFu);
}
}
WavWriter::~WavWriter() {
if (file_ != nullptr) {
std::fclose(file_);
file_ = nullptr;
}
}
Status WavWriter::open(const std::string& path, std::uint32_t channels, std::uint32_t rate,
SampleFormat format, std::uint64_t total_frames) {
if (channels == 0 || rate == 0) {
return Status::fail(JOC_ERR_INVALID_ARGUMENT, stage::kOutput,
"WAV writer needs a positive channel count and rate");
}
path_ = path;
channels_ = channels;
total_frames_ = total_frames;
frames_written_ = 0;
finalized_ = false;
info_ = WavInfo{};
info_.format = format;
const std::string fmt = fmt_chunk(channels, rate, format, &info_);
const std::uint64_t data_size = total_frames * info_.block_align;
info_.data_bytes = data_size;
const std::uint64_t riff_file_size = 12u + 8u + fmt.size() + 8u + data_size;
const bool rf64 = (riff_file_size - 8u) > 0xFFFFFFFFull;
info_.rf64 = rf64;
std::string header;
if (rf64) {
const std::uint64_t file_size = 12u + 36u + 8u + fmt.size() + 8u + data_size;
header.append("RF64", 4);
put_u32(&header, 0xFFFFFFFFu);
header.append("WAVE", 4);
header.append("ds64", 4);
put_u32(&header, 28);
put_u64(&header, file_size - 8u);
put_u64(&header, data_size);
put_u64(&header, total_frames);
put_u32(&header, 0);
} else {
header.append("RIFF", 4);
put_u32(&header, static_cast<std::uint32_t>(riff_file_size - 8u));
header.append("WAVE", 4);
}
header.append("fmt ", 4);
put_u32(&header, static_cast<std::uint32_t>(fmt.size()));
header.append(fmt);
header.append("data", 4);
put_u32(&header, rf64 ? 0xFFFFFFFFu : static_cast<std::uint32_t>(data_size));
file_ = fs_utf8::fopen(path, "wb");
if (file_ == nullptr) {
// The reference creates the parent directory itself.
std::error_code ignored;
const std::filesystem::path parent = std::filesystem::path(path).parent_path();
if (!parent.empty()) {
std::filesystem::create_directories(parent, ignored);
}
file_ = fs_utf8::fopen(path, "wb");
}
if (file_ == nullptr) {
return Status::fail(JOC_ERR_OUTPUT_OPEN, stage::kOutput, "cannot open " + path);
}
if (std::fwrite(header.data(), 1, header.size(), file_) != header.size()) {
std::fclose(file_);
file_ = nullptr;
return Status::fail(JOC_ERR_OUTPUT_WRITE, stage::kOutput, "cannot write header to " + path);
}
return Status::success();
}
Status WavWriter::write(const double* interleaved, std::size_t frames) {
if (file_ == nullptr) {
return Status::fail(JOC_ERR_STATE, stage::kOutput, "WAV writer is not open");
}
if (frames == 0) {
return Status::success();
}
if (frames_written_ + frames > total_frames_) {
return Status::fail(JOC_ERR_OUTPUT_WRITE, stage::kOutput,
"WAV writer received more frames than the header declared (declared " +
std::to_string(total_frames_) + ", written " +
std::to_string(frames_written_) + ", requested " +
std::to_string(frames) + ")");
}
const std::size_t count = frames * channels_;
if (info_.format == SampleFormat::Float32) {
std::vector<float> converted(count);
for (std::size_t i = 0; i < count; ++i) {
converted[i] = static_cast<float>(interleaved[i]);
}
if (std::fwrite(converted.data(), sizeof(float), count, file_) != count) {
return Status::fail(JOC_ERR_OUTPUT_WRITE, stage::kOutput, "write failed for " + path_);
}
} else {
std::vector<float> converted(count);
for (std::size_t i = 0; i < count; ++i) {
converted[i] = static_cast<float>(interleaved[i]);
}
std::string packed;
pack_int24(converted.data(), frames, channels_, &packed);
if (std::fwrite(packed.data(), 1, packed.size(), file_) != packed.size()) {
return Status::fail(JOC_ERR_OUTPUT_WRITE, stage::kOutput, "write failed for " + path_);
}
}
frames_written_ += frames;
return Status::success();
}
Status WavWriter::finalize() {
if (file_ == nullptr) {
return Status::fail(JOC_ERR_STATE, stage::kOutput, "WAV writer is not open");
}
if (frames_written_ != total_frames_) {
std::fclose(file_);
file_ = nullptr;
return Status::fail(JOC_ERR_OUTPUT_WRITE, stage::kOutput,
"WAV writer wrote " + std::to_string(frames_written_) + " of " +
std::to_string(total_frames_) + " frames");
}
const int result = std::fclose(file_);
file_ = nullptr;
finalized_ = true;
if (result != 0) {
return Status::fail(JOC_ERR_OUTPUT_WRITE, stage::kOutput, "close failed for " + path_);
}
return Status::success();
}
} // namespace joc::io
+61
View File
@@ -0,0 +1,61 @@
// Port of src/speaker_wav.py.
#pragma once
#include <cstdint>
#include <cstdio>
#include <string>
#include "foundation/status.h"
#include "joc_core.h"
namespace joc::io {
enum class SampleFormat { Float32, Int24 };
struct WavInfo {
SampleFormat format = SampleFormat::Float32;
std::uint32_t bits_per_sample = 32;
std::uint32_t bytes_per_sample = 4;
std::uint32_t block_align = 0;
std::uint64_t data_bytes = 0;
bool rf64 = false;
};
// int24 packing shared by the WAV and ADM writers:
// trunc(clip(v, -1, 1) * 8388607.0f) with the low three bytes written LE.
// NaN follows NumPy's float->int cast (INT32_MIN) so that the C++ conversion is
// never undefined; the reference passes it through unguarded (plan TD-3.11).
void pack_int24(const float* interleaved, std::size_t frames, std::size_t channels,
std::string* out);
class WavWriter {
public:
WavWriter() = default;
~WavWriter();
WavWriter(const WavWriter&) = delete;
WavWriter& operator=(const WavWriter&) = delete;
// `total_frames` must be known up front: the header depends on it.
Status open(const std::string& path, std::uint32_t channels, std::uint32_t rate,
SampleFormat format, std::uint64_t total_frames);
Status write(const double* interleaved, std::size_t frames);
Status finalize();
const WavInfo& info() const { return info_; }
std::uint64_t frames_written() const { return frames_written_; }
private:
std::FILE* file_ = nullptr;
std::string path_;
WavInfo info_;
std::uint32_t channels_ = 0;
std::uint64_t total_frames_ = 0;
std::uint64_t frames_written_ = 0;
bool finalized_ = false;
};
} // namespace joc::io
+215
View File
@@ -0,0 +1,215 @@
#include "io/zip_reader.h"
#include "foundation/fs_utf8.h"
#include <cstdio>
#include <cstring>
#include "io/inflate.h"
namespace joc::io {
namespace {
constexpr std::uint32_t kLocalHeaderSignature = 0x04034b50u;
constexpr std::uint32_t kCentralHeaderSignature = 0x02014b50u;
constexpr std::uint32_t kEndOfCentralDirectory = 0x06054b50u;
std::uint16_t read_u16(const std::uint8_t* p) {
return static_cast<std::uint16_t>(p[0] | (p[1] << 8));
}
std::uint32_t read_u32(const std::uint8_t* p) {
return static_cast<std::uint32_t>(p[0]) | (static_cast<std::uint32_t>(p[1]) << 8) |
(static_cast<std::uint32_t>(p[2]) << 16) | (static_cast<std::uint32_t>(p[3]) << 24);
}
} // namespace
std::uint32_t crc32_of(const std::uint8_t* data, std::size_t size) {
static std::uint32_t table[256];
static bool ready = false;
if (!ready) {
for (std::uint32_t i = 0; i < 256; ++i) {
std::uint32_t value = i;
for (int bit = 0; bit < 8; ++bit) {
value = (value & 1u) ? (0xEDB88320u ^ (value >> 1)) : (value >> 1);
}
table[i] = value;
}
ready = true;
}
std::uint32_t crc = 0xFFFFFFFFu;
for (std::size_t i = 0; i < size; ++i) {
crc = table[(crc ^ data[i]) & 0xFFu] ^ (crc >> 8);
}
return crc ^ 0xFFFFFFFFu;
}
bool ZipArchive::open(const std::string& path, std::string* error) {
entries_.clear();
data_.clear();
std::FILE* file = fs_utf8::fopen(path, "rb");
if (file == nullptr) {
if (error != nullptr) {
*error = "cannot open " + path;
}
return false;
}
std::fseek(file, 0, SEEK_END);
const long long size = std::ftell(file);
std::fseek(file, 0, SEEK_SET);
if (size <= 0) {
std::fclose(file);
if (error != nullptr) {
*error = "empty file " + path;
}
return false;
}
data_.resize(static_cast<std::size_t>(size));
const std::size_t got = std::fread(data_.data(), 1, data_.size(), file);
std::fclose(file);
if (got != data_.size()) {
if (error != nullptr) {
*error = "short read on " + path;
}
return false;
}
std::size_t eocd = std::string::npos;
const std::size_t scan_start = data_.size() > 65557u ? data_.size() - 65557u : 0u;
for (std::size_t i = data_.size(); i-- > scan_start;) {
if (i + 4u <= data_.size() && read_u32(&data_[i]) == kEndOfCentralDirectory) {
eocd = i;
break;
}
if (i == 0) {
break;
}
}
if (eocd == std::string::npos || eocd + 22u > data_.size()) {
if (error != nullptr) {
*error = "not a zip archive (no end-of-central-directory)";
}
return false;
}
const std::uint16_t entry_count = read_u16(&data_[eocd + 10]);
const std::uint32_t directory_offset = read_u32(&data_[eocd + 16]);
if (directory_offset >= data_.size()) {
if (error != nullptr) {
*error = "central directory offset out of range";
}
return false;
}
std::size_t cursor = directory_offset;
for (std::uint16_t index = 0; index < entry_count; ++index) {
if (cursor + 46u > data_.size() || read_u32(&data_[cursor]) != kCentralHeaderSignature) {
if (error != nullptr) {
*error = "malformed central directory entry " + std::to_string(index);
}
return false;
}
ZipEntry entry;
entry.method = read_u16(&data_[cursor + 10]);
entry.crc32 = read_u32(&data_[cursor + 16]);
entry.compressed_size = read_u32(&data_[cursor + 20]);
entry.uncompressed_size = read_u32(&data_[cursor + 24]);
const std::uint16_t name_length = read_u16(&data_[cursor + 28]);
const std::uint16_t extra_length = read_u16(&data_[cursor + 30]);
const std::uint16_t comment_length = read_u16(&data_[cursor + 32]);
entry.local_header_offset = read_u32(&data_[cursor + 42]);
if (entry.compressed_size == 0xFFFFFFFFu || entry.uncompressed_size == 0xFFFFFFFFu ||
entry.local_header_offset == 0xFFFFFFFFu) {
if (error != nullptr) {
*error = "zip64 archives are not supported";
}
return false;
}
if (cursor + 46u + name_length > data_.size()) {
if (error != nullptr) {
*error = "member name out of range";
}
return false;
}
entry.name.assign(reinterpret_cast<const char*>(&data_[cursor + 46]), name_length);
entries_.push_back(std::move(entry));
cursor += 46u + name_length + extra_length + comment_length;
}
return true;
}
const ZipEntry* ZipArchive::find(const std::string& name) const {
for (const ZipEntry& entry : entries_) {
if (entry.name == name) {
return &entry;
}
}
return nullptr;
}
bool ZipArchive::extract(const ZipEntry& entry, std::vector<std::uint8_t>* out,
std::string* error) const {
if (out == nullptr) {
return false;
}
const std::size_t offset = entry.local_header_offset;
if (offset + 30u > data_.size() || read_u32(&data_[offset]) != kLocalHeaderSignature) {
if (error != nullptr) {
*error = "bad local header for " + entry.name;
}
return false;
}
const std::uint16_t name_length = read_u16(&data_[offset + 26]);
const std::uint16_t extra_length = read_u16(&data_[offset + 28]);
const std::size_t start = offset + 30u + name_length + extra_length;
if (start + entry.compressed_size > data_.size()) {
if (error != nullptr) {
*error = "member data out of range for " + entry.name;
}
return false;
}
if (entry.method == 0u) {
out->assign(data_.begin() + static_cast<std::ptrdiff_t>(start),
data_.begin() + static_cast<std::ptrdiff_t>(start + entry.compressed_size));
} else if (entry.method == 8u) {
if (!inflate_raw(&data_[start], entry.compressed_size, out)) {
if (error != nullptr) {
*error = "deflate error in " + entry.name;
}
return false;
}
} else {
if (error != nullptr) {
*error = "unsupported compression method " + std::to_string(entry.method) + " for " +
entry.name;
}
return false;
}
if (entry.uncompressed_size != 0u && out->size() != entry.uncompressed_size) {
if (error != nullptr) {
*error = "size mismatch for " + entry.name + " (" + std::to_string(out->size()) +
" vs " + std::to_string(entry.uncompressed_size) + ")";
}
return false;
}
if (entry.crc32 != 0u && crc32_of(out->data(), out->size()) != entry.crc32) {
if (error != nullptr) {
*error = "CRC mismatch for " + entry.name;
}
return false;
}
return true;
}
bool ZipArchive::read_member(const std::string& name, std::vector<std::uint8_t>* out,
std::string* error) const {
const ZipEntry* entry = find(name);
if (entry == nullptr) {
if (error != nullptr) {
*error = "member not found: " + name;
}
return false;
}
return extract(*entry, out, error);
}
} // namespace joc::io
+38
View File
@@ -0,0 +1,38 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
namespace joc::io {
struct ZipEntry {
std::string name;
std::uint16_t method = 0;
std::uint32_t crc32 = 0;
std::uint32_t compressed_size = 0;
std::uint32_t uncompressed_size = 0;
std::uint32_t local_header_offset = 0;
};
class ZipArchive {
public:
bool open(const std::string& path, std::string* error);
const std::vector<ZipEntry>& entries() const { return entries_; }
const ZipEntry* find(const std::string& name) const;
bool extract(const ZipEntry& entry, std::vector<std::uint8_t>* out, std::string* error) const;
bool read_member(const std::string& name, std::vector<std::uint8_t>* out, std::string* error) const;
private:
std::vector<std::uint8_t> data_;
std::vector<ZipEntry> entries_;
};
std::uint32_t crc32_of(const std::uint8_t* data, std::size_t size);
} // namespace joc::io
@@ -0,0 +1,89 @@
#pragma once
// JOC Huffman trees required by the bitstream parser.
static const int joc_huff_code_coarse_generic[][2] =
{
{ -1, 1}, { 2, -2}, { -96, 3}, { 4, -3}, { -95, 5}, { 6, 7}, { -4, -94}, { 8, 9}, { -5, -93}, { 10, 11},
{ -6, -92}, { 12, 13}, { -7, -91}, { 14, 15}, { 16, -90}, { -8, 17}, { 18, -89}, { -9, 19}, { 20, 21}, { -88, -10},
{ 22, 23}, { -11, -87}, { 24, 25}, { 26, -86}, { -12, 27}, { 28, -85}, { -13, 29}, { 30, 31}, { 32, -84}, { -14, 33},
{ 34, -15}, { -83, 35}, { 36, 37}, { -16, 38}, { -17, -82}, { 39, 40}, { 41, -81}, { 42, 43}, { 44, 45}, { 46, 47},
{ 48, 49}, { 50, 51}, { 52, -18}, { -78, 53}, { -19, 54}, { 55, 56}, { 57, 58}, { -22, 59}, { 60, 61}, { 62, 63},
{ 64, 65}, { 66, 67}, { 68, -20}, { -21, -79}, { -80, -25}, { 69, 70}, { -26, 71}, { 72, 73}, { 74, 75}, { 76, 77},
{ 78, 79}, { 80, 81}, { 82, 83}, { 84, 85}, { 86, 87}, { 88, 89}, { 90, 91}, { 92, 93}, { 94, -23}, { -74, -75},
{ -72, -73}, { -76, -77}, { -34, -35}, { -32, -33}, { -38, -39}, { -36, -37}, { -30, -31}, { -28, -29}, { -50, -51}, { -48, -49},
{ -54, -55}, { -52, -53}, { -42, -43}, { -40, -41}, { -46, -47}, { -44, -45}, { -66, -67}, { -64, -65}, { -70, -71}, { -68, -69},
{ -58, -59}, { -56, -57}, { -62, -63}, { -60, -61}, { -24, -27}
};
static const int joc_huff_code_fine_generic[][2] =
{
{ -1, 1}, { 2, 3}, { -2,-192}, { 4, 5}, { 6, -3}, {-191, 7}, { 8, 9}, { -4,-190}, { 10, 11}, { -5,-189},
{ 12, 13}, { -6, 14}, {-188, 15}, { 16, -7}, {-187, 17}, { 18, -8}, {-186, 19}, { 20, -9}, {-185, 21}, { 22, -10},
{-184, 23}, { 24, -11}, { 25,-183}, { 26, 27}, { -12,-182}, { 28, 29}, { -13,-181}, { 30, 31}, {-180, -14}, { 32, 33},
{ 34,-179}, { -15, 35}, { 36,-178}, { -16, 37}, { 38,-177}, { 39, -17}, { 40, 41}, {-176, 42}, { -18, 43}, { -19, 44},
{-175, 45}, { 46,-174}, { -20, 47}, {-173, 48}, { 49, -21}, { 50, 51}, { 52, -22}, { 53, 54}, {-172, 55}, {-171, -23},
{ 56, 57}, { 58,-170}, { 59, -24}, { -25, 60}, {-169, 61}, { 62, 63}, { 64, 65}, { 66, 67}, {-168, 68}, { -26, 69},
{-167, -27}, { 70,-166}, {-165, 71}, { -29, 72}, { 73, 74}, { -30, 75}, { 76, 77}, { 78, 79}, { 80, -28}, { 81, 82},
{ 83,-163}, { -31, -33}, {-164,-161}, { 84, 85}, { 86, 87}, { 88, 89}, { 90, 91}, { 92, 93}, { 94, 95}, { 96, 97},
{ 98, 99}, { -32,-162}, { 100, 101}, { 102, 103}, { 104, 105}, { 106, 107}, { 108, 109}, { 110, 111}, {-160, 112}, { -36, -38},
{ 113, 114}, { 115, 116}, { 117, 118}, { 119, 120}, { 121, 122}, { 123, 124}, { 125, 126}, { 127, 128}, { 129, 130}, { 131, 132},
{ 133, -35}, {-158, 134}, {-155,-156}, { -37, -42}, { 135, 136}, { 137, 138}, { 139, 140}, { 141, 142}, { 143, 144}, { 145, 146},
{ 147, 148}, { 149, 150}, { 151, 152}, { 153, 154}, { 155, 156}, { 157, 158}, { 159, 160}, { 161, 162}, { 163, 164}, { 165, 166},
{ 167, 168}, { 169, 170}, { 171, 172}, { 173, 174}, { 175, 176}, { 177, 178}, { 179, 180}, { 181, 182}, { 183, 184}, {-157, 185},
{ -45, -48}, { 186, 187}, { 188, 189}, { -34, -41}, { 190, -39}, { -60, -61}, { -58, -59}, { -64, -65}, { -62, -63}, { -52, -53},
{ -50, -51}, { -56, -57}, { -54, -55}, { -76, -77}, { -74, -75}, { -80, -81}, { -78, -79}, { -68, -69}, { -66, -67}, { -72, -73},
{ -70, -71}, { -47, -49}, { -44, -46}, {-124,-125}, {-122,-123}, {-128,-129}, {-126,-127}, {-116,-117}, {-114,-115}, {-120,-121},
{-118,-119}, {-140,-141}, {-138,-139}, {-144,-145}, {-142,-143}, {-132,-133}, {-130,-131}, {-136,-137}, {-134,-135}, { -92, -93},
{ -90, -91}, { -96, -97}, { -94, -95}, { -84, -85}, { -82, -83}, { -88, -89}, { -86, -87}, {-108,-109}, {-106,-107}, {-112,-113},
{-110,-111}, {-100,-101}, { -98, -99}, {-104,-105}, {-102,-103}, {-154,-159}, {-148,-149}, {-146,-147}, {-152,-153}, {-150,-151},
{ -40, -43}
};
static const int joc_huff_code_coarse_coeff_sparse[][2] =
{
{ -1, 1}, { 2, 3}, { -2, -96}, { 4, 5}, { 6, -95}, { -3, 7}, { 8, 9}, { -4, 10}, { -94, 11}, { 12, -5},
{ -93, 13}, { 14, 15}, { -6, -92}, { 16, 17}, { 18, -7}, { -91, 19}, { 20, -8}, { -90, 21}, { 22, 23}, { -9, -89},
{ 24, 25}, { 26, -10}, { -88, 27}, { 28, 29}, { 30, -11}, { -87, 31}, { 32, 33}, { 34, 35}, { -12, -86}, { 36, 37},
{ 38, -13}, { 39, -85}, { 40, 41}, { 42, 43}, { -14, -84}, { 44, 45}, { 46, 47}, { -83, -15}, { 48, 49}, { 50, -16},
{ 51, 52}, { -82, 53}, { 54, -81}, { 55, 56}, { -17, 57}, { 58, -80}, { 59, 60}, { -18, 61}, { 62, 63}, { -79, 64},
{ -19, -78}, { 65, 66}, { 67, 68}, { 69, -20}, { -77, -21}, { 70, 71}, { 72, 73}, { 74, -76}, { 75, -22}, { 76, 77},
{ -75, 78}, { 79, 80}, { -54, -74}, { -73, 81}, { -23, 82}, { -50, -24}, { -55, -25}, { 83, -47}, { -49, -44}, { -71, 84},
{ -48, -51}, { 85, -72}, { -26, -53}, { -70, -27}, { 86, -45}, { 87, 88}, { -68, 89}, { -29, -43}, { 90, -30}, { -46, -69},
{ 91, -28}, { -52, -31}, { 92, -32}, { 93, -64}, { -67, 94}, { -36, -33}, { -63, -37}, { -65, -61}, { -66, -59}, { -34, -38},
{ -41, -42}, { -35, -60}, { -39, -57}, { -56, -40}, { -62, -58}
};
static const int joc_huff_code_fine_coeff_sparse[][2] =
{
{ 1, -1}, { 2, 3}, { 4, -2}, {-192, 5}, { 6, 7}, { 8, -3}, {-191, 9}, { 10, 11}, { 12,-190}, { -4, 13},
{ 14, 15}, {-189, -5}, { 16, 17}, { 18, -6}, {-188, 19}, { 20, 21}, { -7,-187}, { 22, 23}, { -8, 24}, {-186, 25},
{ -9, 26}, { 27,-185}, { 28, -10}, { 29, 30}, {-184, 31}, { -11, 32}, { 33,-183}, { 34, -12}, { 35,-182}, { 36, 37},
{ 38, -13}, {-181, 39}, { 40, -14}, { 41,-180}, { 42, 43}, {-179, -15}, { 44, -16}, { 45,-178}, { 46, 47}, { 48, 49},
{ 50,-177}, { -17, 51}, { -18, 52}, {-176, 53}, { 54, 55}, {-175, -19}, { 56, 57}, { 58, -20}, { 59,-174}, { 60, 61},
{ -21, 62}, { 63,-173}, { 64, 65}, { 66,-172}, { 67, 68}, { -22, 69}, { 70, 71}, { -23, 72}, {-171, 73}, { 74, 75},
{ 76, -24}, { 77,-170}, { -25, 78}, { 79, 80}, { 81,-169}, { 82, 83}, { 84, -26}, { 85,-168}, { 86, 87}, { 88, 89},
{-167, 90}, { -27, 91}, { 92, -28}, { 93,-166}, { 94, -29}, { 95, 96}, { 97, 98}, {-165, 99}, { 100, -30}, {-164, 101},
{ 102, 103}, { 104, 105}, {-163, 106}, { -31, 107}, { -32, 108}, { 109, 110}, {-161, 111}, {-160,-162}, { 112, -34}, { -33, 113},
{ 114, 115}, { 116, 117}, { 118, 119}, { 120,-159}, { 121, 122}, { 123,-158}, { 124, 125}, { -36,-155}, { 126, 127}, { -35, 128},
{ 129, 130}, {-157, 131}, {-156, 132}, { -37, 133}, { 134, 135}, {-154, -38}, { 136, 137}, { -39, -41}, { 138,-153}, { 139, -40},
{-149, 140}, { 141, 142}, { 143, 144}, {-151, 145}, { 146, 147}, { 148, -42}, { -43, 149}, { 150, 151}, {-152, 152}, { -46, -98},
{ 153, 154}, { 155,-147}, { 156, 157}, { 158,-107}, { 159, 160}, {-145,-150}, { -96, 161}, { 162, -45}, {-146, 163}, { 164, -97},
{-108,-105}, {-148,-106}, { -44, 165}, { -94,-141}, { -99, 166}, { -89, 167}, { -50, -95}, {-100, -48}, {-144, 168}, { 169, 170},
{ -51,-142}, { -90, -91}, { -47, -49}, { 171, -53}, { -93,-143}, {-137,-138}, { -55,-101}, { 172, 173}, { -54, -86}, { -88, -87},
{-103, 174}, { 175, -61}, {-109, 176}, { 177, 178}, { -52,-139}, { -57,-140}, { 179, 180}, { -56,-136}, { -58,-102}, { 181, 182},
{ -60,-135}, { 183,-104}, {-128,-134}, { -92, 184}, { -59, -62}, { 185, 186}, { -71,-133}, { 187,-127}, {-126, 188}, { -63, -64},
{ -85,-132}, { 189, -66}, {-121,-125}, { 190, -68}, { -74, -75}, { -70, -73}, { -81, -65}, {-118,-131}, { -72,-110}, {-119,-120},
{ -76, -84}, {-122,-130}, { -83,-117}, { -69, -78}, { -80, -82}, {-123,-124}, { -67,-116}, {-129, -77}, {-113,-114}, {-112,-115},
{ -79,-111}
};
static const int joc_huff_code_5ch_pos_index_sparse[][2] =
{
{ -1, 1}, { 2, 3}, { -4, -3}, { -2, -5}
};
static const int joc_huff_code_7ch_pos_index_sparse[][2] =
{
{ -1, 1}, { 2, 3}, { 4, 5}, { -4, -3}, { -2, -5}, { -6, -7}
};
+415
View File
@@ -0,0 +1,415 @@
#include "joc_bitstream/joc_parser.h"
#include <algorithm>
#include <cmath>
#include <cstring>
#include <string>
#include "foundation/bit_reader.h"
#include "joc_huffman_tables.h"
namespace joc::joc {
namespace {
struct NumChannelsEntry {
std::uint32_t config;
std::int16_t channels;
};
constexpr NumChannelsEntry kNumChannels[] = {
{0u, 5}, {1u, 7}, {2u, 7}, {3u, 5}, {4u, 7},
};
struct NumBandsEntry {
std::uint32_t index;
std::int16_t bands;
};
constexpr NumBandsEntry kNumBands[] = {
{0u, 1}, {1u, 3}, {2u, 5}, {3u, 7}, {4u, 9}, {5u, 12}, {6u, 15}, {7u, 23},
};
// Floored modulo: Python's % operator semantics, so that the ported
inline std::int64_t floored_mod(std::int64_t value, std::int64_t modulus) {
const std::int64_t remainder = value % modulus;
return remainder < 0 ? remainder + modulus : remainder;
}
enum class SymbolKind { Mtx, Idx, Vec };
struct Tree {
const int (*nodes)[2] = nullptr;
int count = 0;
};
Tree select_tree(std::uint32_t quant_idx, SymbolKind kind, int n_channels) {
Tree tree;
switch (kind) {
case SymbolKind::Idx:
if (n_channels == 5) {
tree.nodes = joc_huff_code_5ch_pos_index_sparse;
tree.count = static_cast<int>(sizeof(joc_huff_code_5ch_pos_index_sparse) /
sizeof(joc_huff_code_5ch_pos_index_sparse[0]));
} else {
tree.nodes = joc_huff_code_7ch_pos_index_sparse;
tree.count = static_cast<int>(sizeof(joc_huff_code_7ch_pos_index_sparse) /
sizeof(joc_huff_code_7ch_pos_index_sparse[0]));
}
break;
case SymbolKind::Vec:
if (quant_idx == 0u) {
tree.nodes = joc_huff_code_coarse_coeff_sparse;
tree.count = static_cast<int>(sizeof(joc_huff_code_coarse_coeff_sparse) /
sizeof(joc_huff_code_coarse_coeff_sparse[0]));
} else {
tree.nodes = joc_huff_code_fine_coeff_sparse;
tree.count = static_cast<int>(sizeof(joc_huff_code_fine_coeff_sparse) /
sizeof(joc_huff_code_fine_coeff_sparse[0]));
}
break;
case SymbolKind::Mtx:
default:
if (quant_idx == 0u) {
tree.nodes = joc_huff_code_coarse_generic;
tree.count = static_cast<int>(sizeof(joc_huff_code_coarse_generic) /
sizeof(joc_huff_code_coarse_generic[0]));
} else {
tree.nodes = joc_huff_code_fine_generic;
tree.count = static_cast<int>(sizeof(joc_huff_code_fine_generic) /
sizeof(joc_huff_code_fine_generic[0]));
}
break;
}
return tree;
}
// infinite loop (plan 40.4 BL-6).
bool huff_decode(const Tree& tree, bits::BitReader& reader, std::int16_t* out_value) {
int node = 0;
int steps = 0;
while (node >= 0) {
if (node >= tree.count || steps > tree.count) {
reader.fail(JOC_ERR_JOC_SYNTAX, "Huffman tree walk left the valid node range");
return false;
}
++steps;
const std::uint32_t bit = reader.read(1);
if (reader.failed()) {
return false;
}
node = tree.nodes[node][bit];
}
*out_value = static_cast<std::int16_t>(-node - 1);
return true;
}
Status syntax_fail(const std::string& message) {
return Status::fail(JOC_ERR_JOC_SYNTAX, stage::kJoc, message);
}
Status truncated_fail(const bits::BitReader& reader) {
if (reader.error() == JOC_ERR_JOC_SYNTAX) {
return syntax_fail(reader.error_message());
}
return Status::fail(JOC_ERR_BITSTREAM_TRUNCATED, stage::kJoc,
std::string("JOC bitstream truncated: ") + reader.error_message());
}
void reconstruct_dense(const ObjectSymbols& symbols, std::uint32_t dp, int n_channels, std::int64_t nquant,
std::int64_t offset,
std::int64_t q[JOC_MAX_DPOINTS][JOC_MAX_CORE_CHANNELS][JOC_MAX_PARAMETER_BANDS]) {
for (int ch = 0; ch < n_channels; ++ch) {
q[dp][ch][0] =
floored_mod(offset + static_cast<std::int64_t>(symbols.mtx[dp][ch][0]), nquant);
for (int pb = 1; pb < symbols.n_bands; ++pb) {
q[dp][ch][pb] = floored_mod(
q[dp][ch][pb - 1] + static_cast<std::int64_t>(symbols.mtx[dp][ch][pb]), nquant);
}
}
}
// across parameter bands and is deliberately NOT reset when the active channel
Status reconstruct_sparse(
const ObjectSymbols& symbols, std::uint32_t dp, int n_channels, std::int64_t nquant, std::int64_t offset,
std::int64_t q[JOC_MAX_DPOINTS][JOC_MAX_CORE_CHANNELS][JOC_MAX_PARAMETER_BANDS]) {
if (n_channels != 5 && n_channels != 7) {
return Status::fail(JOC_ERR_JOC_UNSUPPORTED_VARIANT, stage::kJoc,
"sparse JOC requires 5 or 7 core channels, got " +
std::to_string(n_channels));
}
const int initial_channel = symbols.idx[dp][0];
if (initial_channel < 0 || initial_channel >= n_channels) {
return syntax_fail("sparse JOC initial channel " + std::to_string(initial_channel) +
" out of range for " + std::to_string(n_channels) + " channels");
}
// Non-active entries take nquant/2, which dequantizes to exactly zero.
for (int ch = 0; ch < n_channels; ++ch) {
for (int pb = 0; pb < symbols.n_bands; ++pb) {
q[dp][ch][pb] = nquant / 2;
}
}
int active = initial_channel;
std::int64_t coefficient = offset;
for (int pb = 0; pb < symbols.n_bands; ++pb) {
if (pb != 0) {
active = static_cast<int>(
floored_mod(static_cast<std::int64_t>(active) + symbols.idx[dp][pb], n_channels));
}
coefficient = floored_mod(coefficient + static_cast<std::int64_t>(symbols.vec[dp][pb]), nquant);
q[dp][active][pb] = coefficient;
}
return Status::success();
}
} // namespace
std::int16_t num_channels_for_config(std::uint32_t dmx_config_idx) {
for (const NumChannelsEntry& entry : kNumChannels) {
if (entry.config == dmx_config_idx) {
return entry.channels;
}
}
return -1;
}
std::int16_t num_bands_for_index(std::uint32_t num_bands_idx) {
for (const NumBandsEntry& entry : kNumBands) {
if (entry.index == num_bands_idx) {
return entry.bands;
}
}
return -1;
}
Status parse_id14(const std::uint8_t* payload, std::size_t payload_size, joc_frame_params* out,
FrameSymbols* symbols) {
if (payload == nullptr || out == nullptr) {
return Status::fail(JOC_ERR_INVALID_ARGUMENT, stage::kJoc, "null payload or output");
}
std::memset(out, 0, sizeof(*out));
out->struct_size = sizeof(joc_frame_params);
out->struct_version = JOC_FRAME_PARAMS_VERSION;
// capture is purely additive and never changes the parse result.
FrameSymbols local_symbols{};
FrameSymbols& capture = (symbols != nullptr) ? *symbols : local_symbols;
std::memset(&capture, 0, sizeof(capture));
bits::BitReader reader(payload, payload_size);
out->dmx_config_idx = static_cast<std::uint8_t>(reader.read(3));
out->num_objects_bits = static_cast<std::uint8_t>(reader.read(6));
out->ext_config_idx = static_cast<std::uint8_t>(reader.read(3));
const std::uint32_t n_objects = static_cast<std::uint32_t>(out->num_objects_bits) + 1u;
const std::int16_t n_channels = num_channels_for_config(out->dmx_config_idx);
if (n_channels < 0) {
return syntax_fail("unknown JOC downmix configuration " +
std::to_string(out->dmx_config_idx));
}
out->n_channels = static_cast<std::uint8_t>(n_channels);
if (n_objects > JOC_MAX_OBJECTS) {
// The reference implementation has no check here and fails later inside
// NumPy; the port reports it explicitly (plan 28.2).
return Status::fail(JOC_ERR_JOC_UNSUPPORTED_VARIANT, stage::kJoc,
"JOC frame declares " + std::to_string(n_objects) +
" objects, the ABI supports at most " +
std::to_string(static_cast<int>(JOC_MAX_OBJECTS)));
}
out->n_objects = static_cast<std::uint8_t>(n_objects);
out->clipgain_x_bits = static_cast<std::uint8_t>(reader.read(3));
out->clipgain_y_bits = static_cast<std::uint8_t>(reader.read(5));
out->seq_count = reader.read(10);
// clipgain = 1 + (y/32) * 2^(x-4). The reference multiplies by an exact
// exactly for the whole legal range.
out->clipgain = 1.0 + static_cast<double>(out->clipgain_y_bits) / 32.0 *
std::ldexp(1.0, static_cast<int>(out->clipgain_x_bits) - 4);
for (std::uint32_t obj = 0; obj < n_objects; ++obj) {
joc_object_params& info = out->objects[obj];
info.present = static_cast<std::uint8_t>(reader.read(1));
if (info.present == 0u) {
continue;
}
info.num_bands_idx = static_cast<std::uint8_t>(reader.read(3));
const std::int16_t bands = num_bands_for_index(info.num_bands_idx);
if (bands < 0) {
return syntax_fail("unknown JOC num_bands index " +
std::to_string(info.num_bands_idx));
}
info.n_bands = static_cast<std::uint8_t>(bands);
info.sparse = static_cast<std::uint8_t>(reader.read(1));
info.quant_idx = static_cast<std::uint8_t>(reader.read(1));
info.slope_idx = static_cast<std::uint8_t>(reader.read(1));
info.num_dpoints_bits = static_cast<std::uint8_t>(reader.read(1));
info.n_dpoints = static_cast<std::uint8_t>(info.num_dpoints_bits + 1u);
if (info.slope_idx == 1u) {
for (std::uint32_t dp = 0; dp < info.n_dpoints; ++dp) {
info.offset_ts[dp] = static_cast<std::uint8_t>(reader.read(5) + 1u);
}
}
}
if (reader.failed()) {
return truncated_fail(reader);
}
for (std::uint32_t obj = 0; obj < n_objects; ++obj) {
const joc_object_params& info = out->objects[obj];
if (info.present == 0u) {
continue;
}
ObjectSymbols* symbol = &capture.objects[obj];
symbol->present = 1;
symbol->sparse = info.sparse;
symbol->n_bands = info.n_bands;
symbol->n_dpoints = info.n_dpoints;
symbol->n_channels = out->n_channels;
for (std::uint32_t dp = 0; dp < info.n_dpoints; ++dp) {
if (info.sparse == 1u) {
const Tree idx_tree = select_tree(info.quant_idx, SymbolKind::Idx, n_channels);
const std::uint32_t first = reader.read(3);
if (reader.failed()) {
return truncated_fail(reader);
}
symbol->idx[dp][0] = static_cast<std::uint8_t>(first);
for (int pb = 1; pb < info.n_bands; ++pb) {
std::int16_t value = 0;
if (!huff_decode(idx_tree, reader, &value)) {
return truncated_fail(reader);
}
symbol->idx[dp][pb] = static_cast<std::uint8_t>(value);
}
const Tree vec_tree = select_tree(info.quant_idx, SymbolKind::Vec, n_channels);
for (int pb = 0; pb < info.n_bands; ++pb) {
std::int16_t value = 0;
if (!huff_decode(vec_tree, reader, &value)) {
return truncated_fail(reader);
}
symbol->vec[dp][pb] = value;
}
} else {
const Tree mtx_tree = select_tree(info.quant_idx, SymbolKind::Mtx, n_channels);
for (int ch = 0; ch < n_channels; ++ch) {
for (int pb = 0; pb < info.n_bands; ++pb) {
std::int16_t value = 0;
if (!huff_decode(mtx_tree, reader, &value)) {
return truncated_fail(reader);
}
symbol->mtx[dp][ch][pb] = value;
}
}
}
}
}
out->data_end_bits = static_cast<std::uint32_t>(reader.position());
out->trailing_bits = static_cast<std::uint32_t>(payload_size * 8u - reader.position());
const std::size_t tail_offset = reader.position() / 8u;
if (tail_offset < payload_size) {
const std::size_t tail_bytes = std::min<std::size_t>(8u, payload_size - tail_offset);
std::memcpy(out->tail_bytes, payload + tail_offset, tail_bytes);
}
for (std::uint32_t obj = 0; obj < n_objects; ++obj) {
const joc_object_params& info = out->objects[obj];
if (info.present == 0u) {
continue;
}
std::uint32_t mask_bit = 1u << obj;
out->present_mask |= mask_bit;
const std::int64_t nquant = (info.quant_idx == 0u) ? 96 : 192;
std::int64_t q[JOC_MAX_DPOINTS][JOC_MAX_CORE_CHANNELS][JOC_MAX_PARAMETER_BANDS] = {};
ObjectSymbols* symbol = &capture.objects[obj];
for (std::uint32_t dp = 0; dp < info.n_dpoints; ++dp) {
if (info.sparse == 1u) {
const std::int64_t offset = (info.quant_idx == 0u) ? 50 : 100;
const Status status =
reconstruct_sparse(*symbol, dp, n_channels, nquant, offset, q);
if (!status.ok()) {
return status;
}
} else {
const std::int64_t offset = (info.quant_idx == 0u) ? 48 : 96;
reconstruct_dense(*symbol, dp, n_channels, nquant, offset, q);
}
}
// Operand order and types are kept identical to the reference so the
// result is bit-exact, not merely close.
const double nquant_half = static_cast<double>(nquant) / 2.0;
const double denominator = 4096.0 * static_cast<double>(1 + static_cast<int>(info.quant_idx));
for (std::uint32_t dp = 0; dp < info.n_dpoints; ++dp) {
for (int ch = 0; ch < n_channels; ++ch) {
for (int pb = 0; pb < info.n_bands; ++pb) {
const double value = static_cast<double>(q[dp][ch][pb]) - nquant_half;
out->objects[obj].dq[dp][ch][pb] = value * 820.0 / denominator;
}
}
}
for (std::uint32_t dp = 0; dp < info.n_dpoints; ++dp) {
for (int ch = 0; ch < n_channels; ++ch) {
for (int pb = 0; pb < info.n_bands; ++pb) {
symbol->q[dp][ch][pb] = q[dp][ch][pb];
}
}
}
}
capture.n_objects = out->n_objects;
capture.n_channels = out->n_channels;
return Status::success();
}
Status parse_eac3_frame(const std::uint8_t* frame, std::size_t frame_size, joc_frame_params* out,
emdf::Container* container, FrameSymbols* symbols) {
if (frame == nullptr || out == nullptr) {
return Status::fail(JOC_ERR_INVALID_ARGUMENT, stage::kJoc, "null frame or output");
}
emdf::Container local;
const Status status = emdf::find_joc_emdf(frame, frame_size, &local);
if (!status.ok()) {
return status;
}
if (container != nullptr) {
*container = local;
}
const emdf::Payload* payload = local.find(emdf::kIdJoc);
if (payload == nullptr) {
return Status::fail(JOC_ERR_EMDF_TRANSPORT, stage::kEmdf,
"EMDF container has no ID14 (JOC) payload");
}
std::vector<std::uint8_t> bytes;
const Status extract = emdf::extract_payload_bytes(frame, frame_size, *payload, &bytes);
if (!extract.ok()) {
return extract;
}
return parse_id14(bytes.data(), bytes.size(), out, symbols);
}
Status check_id14_padding(const std::uint8_t* payload, std::size_t payload_size,
std::uint32_t* out_trailing_bits) {
joc_frame_params params;
FrameSymbols symbols;
const Status status = parse_id14(payload, payload_size, &params, &symbols);
if (!status.ok()) {
return status;
}
if (out_trailing_bits != nullptr) {
*out_trailing_bits = params.trailing_bits;
}
if (params.trailing_bits > 7u) {
return Status::fail(JOC_ERR_BITSTREAM_PADDING, stage::kJoc,
"more than 7 bits left after joc_data (" +
std::to_string(params.trailing_bits) + ")");
}
for (std::size_t bit = params.data_end_bits; bit < payload_size * 8u; ++bit) {
if (((payload[bit >> 3] >> (7u - (bit & 7u))) & 1u) != 0u) {
return Status::fail(JOC_ERR_BITSTREAM_PADDING, stage::kJoc,
"non-zero trailing padding bit at " + std::to_string(bit));
}
}
return Status::success();
}
} // namespace joc::joc
+46
View File
@@ -0,0 +1,46 @@
// Port of src/joc_decode.py.
#pragma once
#include <cstddef>
#include <cstdint>
#include <vector>
#include "joc_core.h"
#include "emdf/emdf_parser.h"
#include "foundation/status.h"
namespace joc::joc {
struct ObjectSymbols {
std::uint8_t present = 0;
std::uint8_t sparse = 0;
std::uint8_t n_bands = 0;
std::uint8_t n_dpoints = 0;
std::uint8_t n_channels = 0;
std::int64_t q[JOC_MAX_DPOINTS][JOC_MAX_CORE_CHANNELS][JOC_MAX_PARAMETER_BANDS] = {};
std::int16_t mtx[JOC_MAX_DPOINTS][JOC_MAX_CORE_CHANNELS][JOC_MAX_PARAMETER_BANDS] = {};
std::uint8_t idx[JOC_MAX_DPOINTS][JOC_MAX_PARAMETER_BANDS] = {};
std::int16_t vec[JOC_MAX_DPOINTS][JOC_MAX_PARAMETER_BANDS] = {};
};
struct FrameSymbols {
std::uint8_t n_objects = 0;
std::uint8_t n_channels = 0;
ObjectSymbols objects[JOC_MAX_OBJECTS];
};
Status parse_id14(const std::uint8_t* payload, std::size_t payload_size, joc_frame_params* out,
FrameSymbols* symbols);
Status parse_eac3_frame(const std::uint8_t* frame, std::size_t frame_size, joc_frame_params* out,
emdf::Container* container, FrameSymbols* symbols);
Status check_id14_padding(const std::uint8_t* payload, std::size_t payload_size,
std::uint32_t* out_trailing_bits);
std::int16_t num_channels_for_config(std::uint32_t dmx_config_idx);
std::int16_t num_bands_for_index(std::uint32_t num_bands_idx);
} // namespace joc::joc
+735
View File
@@ -0,0 +1,735 @@
#define EJOC_BUILD_DLL
#include "eac3joc_core.h"
#include "qmf_tables.h"
#include <algorithm>
#include <atomic>
#include <barrier>
#include <cmath>
#include <cstddef>
#include <cstdio>
#include <cstring>
#include <memory>
#include <new>
#include <thread>
#include <vector>
namespace ejoc {
struct Complex {
double re;
double im;
};
inline Complex mul(const Complex a, const Complex b) noexcept {
return {a.re * b.re - a.im * b.im, a.re * b.im + a.im * b.re};
}
inline double clamp_unit(double value) noexcept {
return value < -1.0 ? -1.0 : (value > 1.0 ? 1.0 : value);
}
constexpr double kPi = 3.141592653589793238462643383279502884;
constexpr int kLfeDelay = 1217;
constexpr int kMaxBands = EJOC_MAX_PARAMETER_BANDS;
const uint8_t* parameter_band_map(const int bands) noexcept {
using namespace tables;
switch (bands) {
case 1: return kPbMap1;
case 3: return kPbMap3;
case 5: return kPbMap5;
case 7: return kPbMap7;
case 9: return kPbMap9;
case 12: return kPbMap12;
case 15: return kPbMap15;
case 23: return kPbMap23;
default: return nullptr;
}
}
class Renderer final {
public:
Renderer() noexcept {
initialize_tables();
reset();
}
~Renderer() noexcept {
stop_workers();
}
int reset() noexcept {
std::memset(analysis_fifo_, 0, sizeof(analysis_fifo_));
std::memset(analysis_delay_, 0, sizeof(analysis_delay_));
std::memset(surround_delay_, 0, sizeof(surround_delay_));
std::memset(surround_history_, 0, sizeof(surround_history_));
std::memset(lfe_delay_, 0, sizeof(lfe_delay_));
std::memset(matrix_previous_, 0, sizeof(matrix_previous_));
std::memset(synthesis_state_, 0, sizeof(synthesis_state_));
std::memset(x_, 0, sizeof(x_));
std::memset(z_, 0, sizeof(z_));
analysis_phase_ = 0.0625f;
error_[0] = '\0';
return 0;
}
int set_threads(uint32_t total_threads) noexcept {
if (total_threads < 1) {
total_threads = 1;
}
if (total_threads > EJOC_MAX_OBJECTS) {
total_threads = EJOC_MAX_OBJECTS;
}
stop_workers();
job_count_ = 0;
next_job_.store(0, std::memory_order_relaxed);
if (total_threads == 1) {
return 0;
}
try {
stop_.store(false, std::memory_order_relaxed);
pool_ready_.store(false, std::memory_order_relaxed);
work_barrier_ = std::make_unique<std::barrier<>>(static_cast<std::ptrdiff_t>(total_threads));
workers_.reserve(total_threads - 1);
for (uint32_t index = 1; index < total_threads; ++index) {
workers_.emplace_back([this]() noexcept { worker_loop(); });
}
pool_ready_.store(true, std::memory_order_release);
// Startup rendezvous: ensure every worker has entered the two-phase
// barrier loop before set_threads returns, so an immediate destroy
// cannot race a worker that exits before reaching the barrier.
work_barrier_->arrive_and_wait();
work_barrier_->arrive_and_wait();
} catch (...) {
stop_.store(true, std::memory_order_release);
pool_ready_.store(true, std::memory_order_release);
for (std::thread& worker : workers_) {
if (worker.joinable()) {
worker.join();
}
}
workers_.clear();
work_barrier_.reset();
stop_.store(false, std::memory_order_relaxed);
return fail("failed to create native worker threads");
}
return 0;
}
uint32_t thread_count() const noexcept {
return static_cast<uint32_t>(workers_.size() + 1);
}
const char* error() const noexcept {
return error_[0] ? error_ : "";
}
int process(
const float* bed5,
const float* lfe,
const uint32_t object_mask,
const uint8_t* n_bands,
const uint8_t* n_dpoints,
const uint8_t* slope_idx,
const uint8_t* offset_ts,
const double* dq,
const double clipgain,
const float phase_new,
const float output_scale,
float* output16) noexcept {
error_[0] = '\0';
if (!bed5 || !n_bands || !n_dpoints || !slope_idx || !offset_ts || !dq || !output16) {
return fail("null pointer passed to ejoc_renderer_process");
}
if (object_mask & ~((1u << EJOC_MAX_OBJECTS) - 1u)) {
return fail("object_mask contains an object index above 14");
}
if (!std::isfinite(clipgain) || !std::isfinite(phase_new) || !std::isfinite(output_scale)) {
return fail("clipgain, phase_new, and output_scale must be finite");
}
for (int object = 0; object < EJOC_MAX_OBJECTS; ++object) {
if ((object_mask & (1u << object)) == 0) {
continue;
}
if (!parameter_band_map(n_bands[object])) {
return fail("unsupported parameter-band count");
}
if (n_dpoints[object] < 1 || n_dpoints[object] > 2) {
return fail("n_dpoints must be 1 or 2");
}
if (slope_idx[object] > 1) {
return fail("slope_idx must be 0 or 1");
}
}
std::memset(output16, 0, sizeof(float) * EJOC_OUTPUT_CHANNELS * EJOC_FRAME_SAMPLES);
analysis(bed5, phase_new);
render_lfe(lfe, output_scale, output16);
process_objects(object_mask, n_bands, n_dpoints, slope_idx, offset_ts,
dq, clipgain, output_scale, output16);
return 0;
}
private:
void initialize_tables() noexcept {
for (int i = 0; i < 64; ++i) {
int value = i;
int reversed = 0;
for (int bit = 0; bit < 6; ++bit) {
reversed = (reversed << 1) | (value & 1);
value >>= 1;
}
bit_reverse_[i] = static_cast<uint8_t>(reversed);
const double theta = kPi * static_cast<double>(i) / 128.0;
rotation_sin_[i] = 0.5 * std::sin(theta);
rotation_cos_[i] = 0.5 * std::cos(theta);
}
for (int i = 0; i < 32; ++i) {
const double angle = -2.0 * kPi * static_cast<double>(i) / 64.0;
fft_twiddle_[i] = {std::cos(angle), std::sin(angle)};
}
}
int fail(const char* message) noexcept {
std::snprintf(error_, sizeof(error_), "%s", message);
return -1;
}
void fft64(Complex* values) const noexcept {
for (int i = 0; i < 64; ++i) {
const int j = bit_reverse_[i];
if (j > i) {
const Complex temp = values[i];
values[i] = values[j];
values[j] = temp;
}
}
for (int length = 2; length <= 64; length <<= 1) {
const int half = length >> 1;
const int twiddle_step = 64 / length;
for (int base = 0; base < 64; base += length) {
for (int j = 0; j < half; ++j) {
const Complex even = values[base + j];
const Complex odd = mul(values[base + j + half], fft_twiddle_[j * twiddle_step]);
values[base + j] = {even.re + odd.re, even.im + odd.im};
values[base + j + half] = {even.re - odd.re, even.im - odd.im};
}
}
}
}
void analysis_slot(const int channel, const float* ring, const int timeslot) noexcept {
double v36[64];
double v40[64];
Complex frequency[64];
for (int sample = 0; sample < 64; ++sample) {
v36[sample] =
analysis_fifo_[channel][0][sample] * tables::kAnalysisWindow[8 * 64 + sample] +
analysis_fifo_[channel][2][sample] * tables::kAnalysisWindow[6 * 64 + sample] +
analysis_fifo_[channel][4][sample] * tables::kAnalysisWindow[4 * 64 + sample] +
analysis_fifo_[channel][6][sample] * tables::kAnalysisWindow[2 * 64 + sample] +
analysis_fifo_[channel][8][sample] * tables::kAnalysisWindow[0 * 64 + sample];
v40[sample] =
analysis_fifo_[channel][1][sample] * tables::kAnalysisWindow[7 * 64 + sample] +
analysis_fifo_[channel][3][sample] * tables::kAnalysisWindow[5 * 64 + sample] +
analysis_fifo_[channel][5][sample] * tables::kAnalysisWindow[3 * 64 + sample] +
analysis_fifo_[channel][7][sample] * tables::kAnalysisWindow[1 * 64 + sample] +
static_cast<double>(ring[sample]) * tables::kAnalysisWindow[9 * 64 + sample];
}
for (int k = 0; k < 64; ++k) {
const int source = 63 - k;
const double re = v40[source];
const double im = v36[source];
const double a = rotation_sin_[k];
const double b = rotation_cos_[k];
frequency[k] = {im * a - re * b, im * b + re * a};
}
fft64(frequency);
constexpr double scale = 1.0 / 64.0;
for (int k = 0; k < 32; ++k) {
x_[channel][2 * k][timeslot] = {frequency[k].re * scale, -frequency[k].im * scale};
x_[channel][2 * k + 1][timeslot] = {
frequency[63 - k].re * scale,
frequency[63 - k].im * scale};
}
for (int history = 8; history > 0; --history) {
std::memcpy(analysis_fifo_[channel][history], analysis_fifo_[channel][history - 1],
sizeof(analysis_fifo_[channel][history]));
}
for (int sample = 0; sample < 64; ++sample) {
analysis_fifo_[channel][0][sample] = static_cast<double>(ring[sample]);
}
}
void surround_post() noexcept {
static constexpr double kDcA[21] = {
-.0006242550443857908, -.0019234686624258757, -.0042654648423194885,
-.008168308064341545, -.014327201060950756, -.023759860545396805,
-.03757232800126076, -.05577569454908371, -.07568276673555374,
-.09172472357749939, -.5979374051094055, -.09172472357749939,
-.07568276673555374, -.05577569454908371, -.03757232800126076,
-.023759860545396805, -.014327201060950756, -.008168308064341545,
-.0042654648423194885, -.0019234686624258757, -.0006242550443857908,
};
static constexpr double kDcB[21] = {
.0013996040215715766, .003839150769636035, .007512642536312342,
.012419373728334904, .018367428332567215, .0249701626598835,
.03167900815606117, .03785000368952751, .04283412545919418,
.04607561603188515, .047200120985507965, .04607561603188515,
.04283412545919418, .03785000368952751, .03167900815606117,
.0249701626598835, .018367428332567215, .012419373728334904,
.007512642536312342, .003839150769636035, .0013996040215715766,
};
for (int surround = 0; surround < 2; ++surround) {
const int channel = surround + 3;
for (int group = 0; group < 24; group += 4) {
Complex current[4][64];
Complex dc_buffer[24];
for (int slot = 0; slot < 4; ++slot) {
for (int band = 0; band < 64; ++band) {
current[slot][band] = x_[channel][band][group + slot];
const Complex delayed = surround_delay_[surround][slot][band];
x_[channel][band][group + slot] = {delayed.im, -delayed.re};
}
}
for (int i = 0; i < 20; ++i) {
dc_buffer[i] = surround_history_[surround][i];
}
for (int i = 0; i < 4; ++i) {
dc_buffer[20 + i] = current[i][0];
}
for (int slot = 0; slot < 4; ++slot) {
Complex sum{0.0, 0.0};
for (int tap = 0; tap < 21; ++tap) {
const Complex sample = dc_buffer[slot + tap];
const double cr = kDcB[tap];
const double ci = kDcA[tap];
sum.re += sample.re * cr - sample.im * ci;
sum.im += sample.re * ci + sample.im * cr;
}
x_[channel][0][group + slot] = {2.0 * sum.re, 2.0 * sum.im};
}
for (int i = 0; i < 20; ++i) {
surround_history_[surround][i] = dc_buffer[i + 4];
}
std::memmove(&surround_delay_[surround][0][0],
&surround_delay_[surround][4][0],
sizeof(Complex) * 6 * 64);
for (int slot = 0; slot < 4; ++slot) {
std::memcpy(surround_delay_[surround][6 + slot], current[slot],
sizeof(Complex) * 64);
}
}
}
}
void analysis_channel(const int channel) noexcept {
const float* bed5 = job_bed5_;
const float phase_old = job_phase_old_;
const float phase_new = job_phase_new_;
const bool ramp_phase = phase_old != phase_new;
const float phase_step = static_cast<float>((phase_new - phase_old) / 256.0f);
float scaled[64];
float delayed[64];
for (int timeslot = 0; timeslot < 24; ++timeslot) {
for (int sample = 0; sample < 64; ++sample) {
const int frame_sample = timeslot * 64 + sample;
float gain = phase_new;
if (ramp_phase && frame_sample < 256) {
const float product = static_cast<float>(static_cast<float>(frame_sample) * phase_step);
gain = static_cast<float>(phase_old + product);
}
scaled[sample] = static_cast<float>(bed5[channel * 1536 + frame_sample] * gain);
}
const float* analysis_input = scaled;
if (channel < 3) {
std::memcpy(delayed, analysis_delay_[channel][0], sizeof(delayed));
std::memmove(&analysis_delay_[channel][0][0],
&analysis_delay_[channel][1][0],
sizeof(float) * 9 * 64);
std::memcpy(analysis_delay_[channel][9], scaled, sizeof(scaled));
analysis_input = delayed;
}
analysis_slot(channel, analysis_input, timeslot);
}
}
void analysis(const float* bed5, const float phase_new) noexcept {
job_bed5_ = bed5;
job_phase_old_ = analysis_phase_;
job_phase_new_ = phase_new;
job_kind_ = JobKind::Analysis;
job_count_ = 5;
for (int channel = 0; channel < 5; ++channel) {
job_objects_[channel] = channel;
}
dispatch_jobs();
analysis_phase_ = phase_new;
surround_post();
}
static std::size_t dq_index(const int object, const int point, const int channel, const int band) noexcept {
return static_cast<std::size_t>((((object * 2 + point) * 5 + channel) * kMaxBands) + band);
}
void matrix_object(
const int object,
const uint8_t* n_bands,
const uint8_t* n_dpoints,
const uint8_t* slope_idx,
const uint8_t* offset_ts,
const double* dq) noexcept {
std::memset(z_[object], 0, sizeof(z_[object]));
const int bands = n_bands[object];
const int points = n_dpoints[object];
const int slope = slope_idx[object];
const uint8_t* pb_map = parameter_band_map(bands);
for (int channel = 0; channel < 5; ++channel) {
for (int subband = 0; subband < 64; ++subband) {
const int parameter_band = pb_map[subband];
const double previous = matrix_previous_[object][channel][subband];
const double target0 = dq[dq_index(object, 0, channel, parameter_band)];
const double target1 = points == 2
? dq[dq_index(object, 1, channel, parameter_band)]
: target0;
double last = previous;
for (int timeslot = 0; timeslot < 24; ++timeslot) {
double coefficient;
if (slope == 0) {
if (points == 1) {
const double alpha = static_cast<double>(timeslot + 1) / 24.0;
coefficient = previous * (1.0 - alpha) + target0 * alpha;
} else if (timeslot < 12) {
const double alpha = static_cast<double>(timeslot + 1) / 12.0;
coefficient = previous * (1.0 - alpha) + target0 * alpha;
} else {
const double alpha = static_cast<double>(timeslot - 11) / 12.0;
coefficient = target0 * (1.0 - alpha) + target1 * alpha;
}
} else if (points == 1) {
coefficient = timeslot < offset_ts[object * 2] ? previous : target0;
} else {
coefficient = timeslot < offset_ts[object * 2] ? previous : target0;
if (timeslot >= offset_ts[object * 2 + 1]) {
coefficient = target1;
}
}
z_[object][subband][timeslot].re += x_[channel][subband][timeslot].re * coefficient;
z_[object][subband][timeslot].im += x_[channel][subband][timeslot].im * coefficient;
last = coefficient;
}
matrix_previous_[object][channel][subband] = last;
}
}
}
void qmf5_step(double* state, const double* rotated, double* output) noexcept {
for (int block = 0; block < 16; ++block) {
for (int lane = 0; lane < 4; ++lane) {
const int sample = block * 4 + lane;
const int state_base = block * 36 + lane;
const double even = rotated[block * 8 + lane * 2];
const double odd = rotated[block * 8 + lane * 2 + 1];
output[sample] = 2.0 * (
tables::kQmf5Window[sample] * even + state[state_base]);
state[state_base] = tables::kQmf5Window[1 * 64 + sample] * odd + state[state_base + 4];
for (int slot = 0; slot < 7; ++slot) {
const double alternating = (slot & 1) == 0 ? even : odd;
state[state_base + (slot + 1) * 4] =
tables::kQmf5Window[(slot + 2) * 64 + sample] * alternating +
state[state_base + (slot + 2) * 4];
}
state[state_base + 8 * 4] = tables::kQmf5Window[9 * 64 + sample] * odd;
}
}
}
void synthesis_object(
const int object,
const double clipgain,
const float output_scale,
float* output16) noexcept {
Complex frequency[64];
double rotated[128];
double pcm64[64];
double* state = synthesis_state_[object];
float* destination = output16 + (object + 1) * 1536;
for (int timeslot = 0; timeslot < 24; ++timeslot) {
for (int k = 0; k < 32; ++k) {
const Complex even = z_[object][2 * k][timeslot];
const Complex odd = z_[object][2 * k + 1][timeslot];
frequency[k] = {even.re, -even.im};
frequency[63 - k] = {odd.re, odd.im};
}
fft64(frequency);
for (int k = 0; k < 64; ++k) {
const double re = frequency[k].re;
const double im = frequency[k].im;
const double sin_component = rotation_sin_[k];
const double cos_component = rotation_cos_[k];
rotated[2 * k] = 2.0 * (re * cos_component + im * sin_component);
rotated[2 * k + 1] = 2.0 * (im * cos_component - re * sin_component);
}
qmf5_step(state, rotated, pcm64);
for (int sample = 0; sample < 64; ++sample) {
const double clipped = clamp_unit(16.0 * pcm64[sample]);
const float value = static_cast<float>(clipped * clipgain);
destination[timeslot * 64 + sample] = static_cast<float>(value * output_scale);
}
}
}
void process_one_object(const int object) noexcept {
matrix_object(object, job_n_bands_, job_n_dpoints_, job_slope_idx_,
job_offset_ts_, job_dq_);
synthesis_object(object, job_clipgain_, job_output_scale_, job_output16_);
}
void execute_job_loop() noexcept {
while (true) {
const int index = next_job_.fetch_add(1, std::memory_order_relaxed);
if (index >= job_count_) {
break;
}
const int item = job_objects_[index];
if (job_kind_ == JobKind::Analysis) {
analysis_channel(item);
} else {
process_one_object(item);
}
}
}
void dispatch_jobs() noexcept {
if (workers_.empty() || job_count_ == 1) {
next_job_.store(0, std::memory_order_relaxed);
execute_job_loop();
return;
}
next_job_.store(0, std::memory_order_relaxed);
work_barrier_->arrive_and_wait();
execute_job_loop();
work_barrier_->arrive_and_wait();
}
void worker_loop() noexcept {
while (!pool_ready_.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
if (stop_.load(std::memory_order_acquire)) {
return;
}
while (true) {
work_barrier_->arrive_and_wait();
if (stop_.load(std::memory_order_acquire)) {
return;
}
execute_job_loop();
work_barrier_->arrive_and_wait();
}
}
void stop_workers() noexcept {
if (workers_.empty()) {
work_barrier_.reset();
stop_.store(false, std::memory_order_relaxed);
pool_ready_.store(false, std::memory_order_relaxed);
return;
}
stop_.store(true, std::memory_order_release);
work_barrier_->arrive_and_wait();
for (std::thread& worker : workers_) {
if (worker.joinable()) {
worker.join();
}
}
workers_.clear();
work_barrier_.reset();
stop_.store(false, std::memory_order_relaxed);
pool_ready_.store(false, std::memory_order_relaxed);
}
void process_objects(
const uint32_t object_mask,
const uint8_t* n_bands,
const uint8_t* n_dpoints,
const uint8_t* slope_idx,
const uint8_t* offset_ts,
const double* dq,
const double clipgain,
const float output_scale,
float* output16) noexcept {
int count = 0;
for (int object = 0; object < EJOC_MAX_OBJECTS; ++object) {
if (object_mask & (1u << object)) {
job_objects_[count++] = object;
}
}
if (count == 0) {
return;
}
job_kind_ = JobKind::Objects;
job_count_ = count;
job_n_bands_ = n_bands;
job_n_dpoints_ = n_dpoints;
job_slope_idx_ = slope_idx;
job_offset_ts_ = offset_ts;
job_dq_ = dq;
job_clipgain_ = clipgain;
job_output_scale_ = output_scale;
job_output16_ = output16;
dispatch_jobs();
}
void render_lfe(const float* lfe, const float output_scale, float* output16) noexcept {
if (!lfe) {
return;
}
for (int sample = 0; sample < kLfeDelay; ++sample) {
const float value = static_cast<float>(clamp_unit(lfe_delay_[sample]));
output16[sample] = static_cast<float>(value * output_scale);
}
for (int sample = kLfeDelay; sample < 1536; ++sample) {
const float value = static_cast<float>(clamp_unit(static_cast<double>(lfe[sample - kLfeDelay])));
output16[sample] = static_cast<float>(value * output_scale);
}
for (int sample = 0; sample < kLfeDelay; ++sample) {
lfe_delay_[sample] = static_cast<double>(lfe[sample + (1536 - kLfeDelay)]);
}
}
enum class JobKind : uint8_t { Analysis, Objects };
std::vector<std::thread> workers_;
std::unique_ptr<std::barrier<>> work_barrier_;
std::atomic<bool> stop_{false};
std::atomic<bool> pool_ready_{false};
std::atomic<int> next_job_{0};
int job_objects_[EJOC_MAX_OBJECTS]{};
int job_count_ = 0;
JobKind job_kind_ = JobKind::Objects;
const float* job_bed5_ = nullptr;
float job_phase_old_ = 0.0625f;
float job_phase_new_ = 0.0625f;
const uint8_t* job_n_bands_ = nullptr;
const uint8_t* job_n_dpoints_ = nullptr;
const uint8_t* job_slope_idx_ = nullptr;
const uint8_t* job_offset_ts_ = nullptr;
const double* job_dq_ = nullptr;
double job_clipgain_ = 1.0;
float job_output_scale_ = 1.0f;
float* job_output16_ = nullptr;
alignas(64) double analysis_fifo_[5][9][64];
alignas(64) float analysis_delay_[3][10][64];
float analysis_phase_;
alignas(64) Complex surround_delay_[2][10][64];
alignas(64) Complex surround_history_[2][20];
alignas(64) double lfe_delay_[kLfeDelay];
alignas(64) double matrix_previous_[15][5][64];
alignas(64) double synthesis_state_[15][640];
alignas(64) Complex x_[5][64][24];
alignas(64) Complex z_[15][64][24];
uint8_t bit_reverse_[64];
Complex fft_twiddle_[32];
double rotation_sin_[64];
double rotation_cos_[64];
char error_[256];
};
} // namespace ejoc
extern "C" {
uint32_t EJOC_CALL ejoc_abi_version(void) {
return EJOC_ABI_VERSION;
}
const char* EJOC_CALL ejoc_build_info(void) {
#if defined(_MSC_VER)
return "eac3joc-core abi=1 compiler=MSVC fft=fixed64 speaker=double binaural=double crt=static-by-build";
#elif defined(__clang__)
return "eac3joc-core abi=1 compiler=Clang fft=fixed64 speaker=double binaural=double";
#elif defined(__GNUC__)
return "eac3joc-core abi=1 compiler=GCC fft=fixed64 speaker=double binaural=double";
#else
return "eac3joc-core abi=1 compiler=unknown fft=fixed64 speaker=double binaural=double";
#endif
}
ejoc_renderer_handle EJOC_CALL ejoc_renderer_create(void) {
return new (std::nothrow) ejoc::Renderer();
}
void EJOC_CALL ejoc_renderer_destroy(ejoc_renderer_handle handle) {
delete static_cast<ejoc::Renderer*>(handle);
}
int EJOC_CALL ejoc_renderer_reset(ejoc_renderer_handle handle) {
if (!handle) {
return -1;
}
return static_cast<ejoc::Renderer*>(handle)->reset();
}
int EJOC_CALL ejoc_renderer_set_threads(ejoc_renderer_handle handle, uint32_t total_threads) {
if (!handle) {
return -1;
}
return static_cast<ejoc::Renderer*>(handle)->set_threads(total_threads);
}
uint32_t EJOC_CALL ejoc_renderer_thread_count(ejoc_renderer_handle handle) {
if (!handle) {
return 0;
}
return static_cast<ejoc::Renderer*>(handle)->thread_count();
}
const char* EJOC_CALL ejoc_renderer_last_error(ejoc_renderer_handle handle) {
if (!handle) {
return "null renderer handle";
}
return static_cast<ejoc::Renderer*>(handle)->error();
}
int EJOC_CALL ejoc_renderer_process(
ejoc_renderer_handle handle,
const float* bed5_planar,
const float* lfe,
const uint32_t object_mask,
const uint8_t* n_bands,
const uint8_t* n_dpoints,
const uint8_t* slope_idx,
const uint8_t* offset_ts,
const double* dq,
const double clipgain,
const float phase_new,
const float output_scale,
float* output16_planar) {
if (!handle) {
return -1;
}
return static_cast<ejoc::Renderer*>(handle)->process(
bed5_planar, lfe, object_mask, n_bands, n_dpoints, slope_idx,
offset_ts, dq, clipgain, phase_new, output_scale, output16_planar);
}
} // extern "C"
+71
View File
@@ -0,0 +1,71 @@
#include "joc_core/objects16.h"
#include <cstdint>
#include <cstdio>
namespace joc::joc {
Status rebuild_objects16(ejoc_renderer_handle handle, const joc_frame_params& params,
const float* bed5_planar, const float* lfe, float gain,
std::vector<float>* out16, std::string* error) {
if (handle == nullptr || bed5_planar == nullptr || out16 == nullptr) {
return Status::fail(JOC_ERR_INVALID_ARGUMENT, stage::kDsp, "null argument");
}
if (params.n_channels != JOC_CORE_CHANNELS) {
if (error != nullptr) {
*error = "the reused JOC kernel requires 5 core channels, frame declares " +
std::to_string(static_cast<unsigned>(params.n_channels));
}
return Status::fail(JOC_ERR_JOC_UNSUPPORTED_VARIANT, stage::kDsp, *error);
}
std::vector<double> dq(static_cast<std::size_t>(JOC_MAX_OBJECTS) * JOC_MAX_DPOINTS *
JOC_CORE_CHANNELS * JOC_MAX_PARAMETER_BANDS,
0.0);
std::uint8_t n_bands[JOC_MAX_OBJECTS] = {};
std::uint8_t n_dpoints[JOC_MAX_OBJECTS] = {};
std::uint8_t slope_idx[JOC_MAX_OBJECTS] = {};
std::uint8_t offset_ts[JOC_MAX_OBJECTS * JOC_MAX_DPOINTS] = {};
for (unsigned obj = 0; obj < JOC_MAX_OBJECTS; ++obj) {
const joc_object_params& object = params.objects[obj];
if (object.present == 0) {
continue;
}
n_bands[obj] = object.n_bands;
n_dpoints[obj] = object.n_dpoints;
slope_idx[obj] = object.slope_idx;
for (unsigned dp = 0; dp < JOC_MAX_DPOINTS; ++dp) {
offset_ts[obj * JOC_MAX_DPOINTS + dp] = object.offset_ts[dp];
}
for (unsigned dp = 0; dp < object.n_dpoints; ++dp) {
for (unsigned ch = 0; ch < JOC_CORE_CHANNELS; ++ch) {
for (unsigned pb = 0; pb < object.n_bands; ++pb) {
const std::size_t index =
((static_cast<std::size_t>(obj) * JOC_MAX_DPOINTS + dp) * JOC_CORE_CHANNELS +
ch) *
JOC_MAX_PARAMETER_BANDS +
pb;
dq[index] = object.dq[dp][ch][pb];
}
}
}
}
out16->assign(static_cast<std::size_t>(JOC_OUTPUT_CHANNELS) * JOC_FRAME_SAMPLES, 0.0f);
const int result = ejoc_renderer_process(
handle, bed5_planar, lfe, params.present_mask, n_bands, n_dpoints, slope_idx, offset_ts,
dq.data(), params.clipgain, 0.0625f, gain, out16->data());
if (result != 0) {
const char* detail = ejoc_renderer_last_error(handle);
const std::string message =
"ejoc_renderer_process failed (" + std::to_string(result) + "): " +
(detail != nullptr ? detail : "unknown");
if (error != nullptr) {
*error = message;
}
return Status::fail(JOC_ERR_RENDER_FAILED, stage::kDsp, message);
}
return Status::success();
}
} // namespace joc::joc
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include <string>
#include <vector>
#include "eac3joc_core.h"
#include "foundation/status.h"
#include "joc_core.h"
namespace joc::joc {
// [16][1536] planar float32. The kernel requires exactly 5 core channels.
Status rebuild_objects16(ejoc_renderer_handle handle, const joc_frame_params& params,
const float* bed5_planar, const float* lfe, float gain,
std::vector<float>* out16, std::string* error);
} // namespace joc::joc
+388
View File
@@ -0,0 +1,388 @@
#pragma once
#include <stdint.h>
// Generated table data; do not hand-edit.
namespace ejoc::tables {
inline constexpr double kAnalysisWindow[640] = {
0.00019903187057934701, 0.00024947625934146345, 0.00030217695166356862, 0.00035484600812196732,
0.0004058915947098285, 0.00045464080176316202, 0.00050126801943406463, 0.00054649583762511611,
0.00059120741207152605, 0.0006361178238876164, 0.00068160606315359473, 0.00072772568091750145,
0.00077434181002900004, 0.00082129903603345156, 0.00086853635730221868, 0.00091610715026035905,
0.00096411682898178697, 0.0010126305278390646, 0.0010616052895784378, 0.0011108826147392392,
0.0011602368904277682, 0.0012094489065930247, 0.0012583627831190825, 0.0013069023843854666,
0.0013550462899729609, 0.0014027846045792103, 0.0014500867109745741, 0.0014968989416956902,
0.0015431707724928856, 0.0015888890484347939, 0.0016340982401743531, 0.0016788924112915993,
0.0017233812250196934, 0.0017676511779427528, 0.0018117419676855206, 0.0018556505674496293,
0.0018993609119206667, 0.0019428766099736094, 0.0019862416666001081, 0.0020295341964811087,
0.0020728406962007284, 0.0021162291523069143, 0.0021597379818558693, 0.0022033930290490389,
0.0022472396958619356, 0.0022913739085197449, 0.0023359460756182671, 0.0023811329156160355,
0.0024270866997539997, 0.0024738919455558062, 0.0025215502828359604, 0.0025700139813125134,
0.0026192441582679749, 0.0026692659594118595, 0.002720177173614502, 0.0027720888610929251,
0.0028250094037503004, 0.0028787164483219385, 0.0029326770454645157, 0.0029860674403607845,
0.0030379060190171003, 0.0030872693751007318, 0.0031335193198174238, 0.0031764607410877943,
0.0032163741998374462, 0.0032539025414735079, 0.0032898378558456898, 0.0033248732797801495,
0.003359407652169466, 0.0033934540115296841, 0.0034266682341694832, 0.0034584659151732922,
0.0034881711471825838, 0.0035151413176208735, 0.0035388274118304253, 0.0035587677266448736,
0.0035745392087846994, 0.0035856980830430984, 0.0035917432978749275, 0.0035921167582273483,
0.0035862282384186983, 0.0035734928678721189, 0.0035533567424863577, 0.0035253004170954227,
0.0034888240043073893, 0.003443423192948103, 0.0033885682933032513, 0.0033236993476748466,
0.0032482317183166742, 0.0031615688931196928, 0.0030631136614829302, 0.0029522709082812071,
0.0028284420259296894, 0.0026910160668194294, 0.0025393660180270672, 0.0023728485684841871,
0.0021908141206949949, 0.0019926181994378567, 0.0017776311142370105, 0.0015452421503141522,
0.001294855959713459, 0.0010258855763822794, 0.00073774566408246756, 0.00042984966421499848,
0.00010161137470277026, -0.00024754938203841448, -0.00061819725669920444, -0.0010108760325238109,
-0.001426108181476593, -0.0018643926596269011, -0.0023262077011168003, -0.002812013728544116,
-0.0033222525380551815, -0.003857344388961792, -0.0044176783412694931, -0.0050036045722663403,
-0.0056154225021600723, -0.0062533821910619736, -0.0069176913239061832, -0.0076085370965301991,
-0.008326113224029541, -0.0090706516057252884, -0.0098424339666962624, -0.010641784407198429,
-0.011469035409390926, -0.012324465438723564, -0.013208229094743729, -0.014120301231741905,
0.015060451813042164, 0.016028247773647308, 0.017023105174303055, 0.018044359982013702,
0.019091326743364334, 0.020163353532552719, 0.021259821951389313, 0.022380130365490913,
0.02352365106344223, 0.024689681828022003, 0.025877414271235466, 0.027085918933153152,
0.028314167633652687, 0.029561035335063934, 0.030825328081846237, 0.032105788588523865,
0.033401083201169968, 0.034709792584180832, 0.036030396819114685, 0.037361271679401398,
0.03870067372918129, 0.040046781301498413, 0.041397668421268463, 0.042751342058181763,
0.044105727225542068, 0.045458663254976273, 0.046807888895273209, 0.048151064664125443,
0.049485750496387482, 0.050809424370527267, 0.05211947113275528, 0.053413204848766327,
0.054687850177288055, 0.055940557271242142, 0.057168368250131607, 0.058368254452943802,
0.059537097811698914, 0.06067170575261116, 0.061768818646669388, 0.062825113534927368,
0.063837200403213501, 0.064801648259162903, 0.065714947879314423, 0.066573545336723328,
0.067373812198638916, 0.068112112581729889, 0.068784743547439575, 0.069387979805469513,
0.069918066263198853, 0.070371203124523163, 0.070743560791015625, 0.071031264960765839,
0.071230456233024597, 0.071337237954139709, 0.071347743272781372, 0.071258097887039185,
0.07106444239616394, 0.070762887597084045, 0.070349536836147308, 0.069820456206798553,
0.069171726703643799, 0.068399444222450256, 0.067499779164791107, 0.066468983888626099,
0.065303429961204529, 0.063999593257904053, 0.062554046511650085, 0.060963429510593414,
0.059224434196949005, 0.057333782315254211, 0.055288247764110565, 0.05308464914560318,
0.050719890743494034, 0.04819098487496376, 0.045495055615901947, 0.042629346251487732,
0.039591230452060699, 0.036378197371959686, 0.032987859100103378, 0.029417969286441803,
0.025666400790214539, 0.0217311792075634, 0.017610486596822739, 0.013302664272487164,
0.0088062174618244171, 0.0041198157705366611, -0.00075770384864881635, -0.0058273370377719402,
-0.011089906096458435, -0.016546055674552917, -0.022196246311068535, -0.028040755540132523,
-0.034079667180776596, -0.040312871336936951, -0.046740073710680008, -0.053360763937234879,
-0.06017424538731575, -0.0671796053647995, -0.074375726282596588, -0.081761270761489868,
-0.089334696531295776, -0.097094230353832245, -0.10503791272640228, -0.1131635457277298,
-0.12146873027086258, -0.12995083630084991, -0.13860704004764557, -0.1474342942237854,
-0.15642932057380676, -0.1655886322259903, -0.17490856349468231, -0.18438516557216644,
-0.19401434063911438, -0.20379173755645752, -0.21371282637119293, -0.22377283871173859,
-0.23396681249141693, -0.24428960680961609, -0.25473582744598389, -0.26529994606971741,
-0.27597621083259583, -0.2867586612701416, -0.29764124751091003, -0.30861768126487732,
-0.31968152523040771, -0.33082622289657593, -0.34204500913619995, -0.35333094000816345,
0.36467701196670532, 0.37607598304748535, 0.38752046227455139, 0.39900293946266174,
0.41051584482192993, 0.42205137014389038, 0.4336017370223999, 0.44515895843505859,
0.45671501755714417, 0.46826183795928955, 0.47979119420051575, 0.49129492044448853,
0.50276464223861694, 0.51419198513031006, 0.52556860446929932, 0.53688603639602661,
0.54813587665557861, 0.55930960178375244, 0.57039880752563477, 0.58139497041702271,
0.59228962659835815, 0.60307443141937256, 0.61374092102050781, 0.62428075075149536,
0.63468557596206665, 0.64494723081588745, 0.65505754947662354, 0.66500836610794067,
0.67479169368743896, 0.68439966440200806, 0.69382447004318237, 0.70305842161178589,
0.71209394931793213, 0.72092366218566895, 0.72954028844833374, 0.73793661594390869,
0.74610573053359985, 0.75404083728790283, 0.76173526048660278, 0.76918256282806396,
0.77637648582458496, 0.78331100940704346, 0.78998017311096191, 0.79637837409973145,
0.80250018835067749, 0.80834043025970459, 0.81389403343200684, 0.81915634870529175,
0.82412278652191162, 0.82878917455673218, 0.83315145969390869, 0.83720588684082031,
0.84094899892807007, 0.84437751770019531, 0.84748858213424683, 0.85027939081192017,
0.85274755954742432, 0.85489106178283691, 0.85670793056488037, 0.8581966757774353,
0.85935592651367188, 0.86018466949462891, 0.86068224906921387, 0.86084812879562378,
0.86068224906921387, 0.86018466949462891, 0.85935592651367188, 0.8581966757774353,
0.85670793056488037, 0.85489106178283691, 0.85274755954742432, 0.85027939081192017,
0.84748858213424683, 0.84437751770019531, 0.84094899892807007, 0.83720588684082031,
0.83315145969390869, 0.82878917455673218, 0.82412278652191162, 0.81915634870529175,
0.81389403343200684, 0.80834043025970459, 0.80250018835067749, 0.79637837409973145,
0.78998017311096191, 0.78331100940704346, 0.77637648582458496, 0.76918256282806396,
0.76173526048660278, 0.75404083728790283, 0.74610573053359985, 0.73793661594390869,
0.72954028844833374, 0.72092366218566895, 0.71209394931793213, 0.70305842161178589,
0.69382447004318237, 0.68439966440200806, 0.67479169368743896, 0.66500836610794067,
0.65505754947662354, 0.64494723081588745, 0.63468557596206665, 0.62428075075149536,
0.61374092102050781, 0.60307443141937256, 0.59228962659835815, 0.58139497041702271,
0.57039880752563477, 0.55930960178375244, 0.54813587665557861, 0.53688603639602661,
0.52556860446929932, 0.51419198513031006, 0.50276464223861694, 0.49129492044448853,
0.47979119420051575, 0.46826183795928955, 0.45671501755714417, 0.44515895843505859,
0.4336017370223999, 0.42205137014389038, 0.41051584482192993, 0.39900293946266174,
0.38752046227455139, 0.37607598304748535, 0.36467701196670532, 0.35333094000816345,
-0.34204500913619995, -0.33082622289657593, -0.31968152523040771, -0.30861768126487732,
-0.29764124751091003, -0.2867586612701416, -0.27597621083259583, -0.26529994606971741,
-0.25473582744598389, -0.24428960680961609, -0.23396681249141693, -0.22377283871173859,
-0.21371282637119293, -0.20379173755645752, -0.19401434063911438, -0.18438516557216644,
-0.17490856349468231, -0.1655886322259903, -0.15642932057380676, -0.1474342942237854,
-0.13860704004764557, -0.12995083630084991, -0.12146873027086258, -0.1131635457277298,
-0.10503791272640228, -0.097094230353832245, -0.089334696531295776, -0.081761270761489868,
-0.074375726282596588, -0.0671796053647995, -0.06017424538731575, -0.053360763937234879,
-0.046740073710680008, -0.040312871336936951, -0.034079667180776596, -0.028040755540132523,
-0.022196246311068535, -0.016546055674552917, -0.011089906096458435, -0.0058273370377719402,
-0.00075770384864881635, 0.0041198157705366611, 0.0088062174618244171, 0.013302664272487164,
0.017610486596822739, 0.0217311792075634, 0.025666400790214539, 0.029417969286441803,
0.032987859100103378, 0.036378197371959686, 0.039591230452060699, 0.042629346251487732,
0.045495055615901947, 0.04819098487496376, 0.050719890743494034, 0.05308464914560318,
0.055288247764110565, 0.057333782315254211, 0.059224434196949005, 0.060963429510593414,
0.062554046511650085, 0.063999593257904053, 0.065303429961204529, 0.066468983888626099,
0.067499779164791107, 0.068399444222450256, 0.069171726703643799, 0.069820456206798553,
0.070349536836147308, 0.070762887597084045, 0.07106444239616394, 0.071258097887039185,
0.071347743272781372, 0.071337237954139709, 0.071230456233024597, 0.071031264960765839,
0.070743560791015625, 0.070371203124523163, 0.069918066263198853, 0.069387979805469513,
0.068784743547439575, 0.068112112581729889, 0.067373812198638916, 0.066573545336723328,
0.065714947879314423, 0.064801648259162903, 0.063837200403213501, 0.062825113534927368,
0.061768818646669388, 0.06067170575261116, 0.059537097811698914, 0.058368254452943802,
0.057168368250131607, 0.055940557271242142, 0.054687850177288055, 0.053413204848766327,
0.05211947113275528, 0.050809424370527267, 0.049485750496387482, 0.048151064664125443,
0.046807888895273209, 0.045458663254976273, 0.044105727225542068, 0.042751342058181763,
0.041397668421268463, 0.040046781301498413, 0.03870067372918129, 0.037361271679401398,
0.036030396819114685, 0.034709792584180832, 0.033401083201169968, 0.032105788588523865,
0.030825328081846237, 0.029561035335063934, 0.028314167633652687, 0.027085918933153152,
0.025877414271235466, 0.024689681828022003, 0.02352365106344223, 0.022380130365490913,
0.021259821951389313, 0.020163353532552719, 0.019091326743364334, 0.018044359982013702,
0.017023105174303055, 0.016028247773647308, 0.015060451813042164, 0.014120301231741905,
-0.013208229094743729, -0.012324465438723564, -0.011469035409390926, -0.010641784407198429,
-0.0098424339666962624, -0.0090706516057252884, -0.008326113224029541, -0.0076085370965301991,
-0.0069176913239061832, -0.0062533821910619736, -0.0056154225021600723, -0.0050036045722663403,
-0.0044176783412694931, -0.003857344388961792, -0.0033222525380551815, -0.002812013728544116,
-0.0023262077011168003, -0.0018643926596269011, -0.001426108181476593, -0.0010108760325238109,
-0.00061819725669920444, -0.00024754938203841448, 0.00010161137470277026, 0.00042984966421499848,
0.00073774566408246756, 0.0010258855763822794, 0.001294855959713459, 0.0015452421503141522,
0.0017776311142370105, 0.0019926181994378567, 0.0021908141206949949, 0.0023728485684841871,
0.0025393660180270672, 0.0026910160668194294, 0.0028284420259296894, 0.0029522709082812071,
0.0030631136614829302, 0.0031615688931196928, 0.0032482317183166742, 0.0033236993476748466,
0.0033885682933032513, 0.003443423192948103, 0.0034888240043073893, 0.0035253004170954227,
0.0035533567424863577, 0.0035734928678721189, 0.0035862282384186983, 0.0035921167582273483,
0.0035917432978749275, 0.0035856980830430984, 0.0035745392087846994, 0.0035587677266448736,
0.0035388274118304253, 0.0035151413176208735, 0.0034881711471825838, 0.0034584659151732922,
0.0034266682341694832, 0.0033934540115296841, 0.003359407652169466, 0.0033248732797801495,
0.0032898378558456898, 0.0032539025414735079, 0.0032163741998374462, 0.0031764607410877943,
0.0031335193198174238, 0.0030872693751007318, 0.0030379060190171003, 0.0029860674403607845,
0.0029326770454645157, 0.0028787164483219385, 0.0028250094037503004, 0.0027720888610929251,
0.002720177173614502, 0.0026692659594118595, 0.0026192441582679749, 0.0025700139813125134,
0.0025215502828359604, 0.0024738919455558062, 0.0024270866997539997, 0.0023811329156160355,
0.0023359460756182671, 0.0022913739085197449, 0.0022472396958619356, 0.0022033930290490389,
0.0021597379818558693, 0.0021162291523069143, 0.0020728406962007284, 0.0020295341964811087,
0.0019862416666001081, 0.0019428766099736094, 0.0018993609119206667, 0.0018556505674496293,
0.0018117419676855206, 0.0017676511779427528, 0.0017233812250196934, 0.0016788924112915993,
0.0016340982401743531, 0.0015888890484347939, 0.0015431707724928856, 0.0014968989416956902,
0.0014500867109745741, 0.0014027846045792103, 0.0013550462899729609, 0.0013069023843854666,
0.0012583627831190825, 0.0012094489065930247, 0.0011602368904277682, 0.0011108826147392392,
0.0010616052895784378, 0.0010126305278390646, 0.00096411682898178697, 0.00091610715026035905,
0.00086853635730221868, 0.00082129903603345156, 0.00077434181002900004, 0.00072772568091750145,
0.00068160606315359473, 0.0006361178238876164, 0.00059120741207152605, 0.00054649583762511611,
0.00050126801943406463, 0.00045464080176316202, 0.0004058915947098285, 0.00035484600812196732,
0.00030217695166356862, 0.00024947625934146345, 0.00019903187057934701, 0,
};
inline constexpr double kQmf5Window[640] = {
0, 0.00019903187057934701, 0.00024947625934146345, 0.00030217695166356862,
0.00035484600812196732, 0.0004058915947098285, 0.00045464080176316202, 0.00050126801943406463,
0.00054649583762511611, 0.00059120741207152605, 0.0006361178238876164, 0.00068160606315359473,
0.00072772568091750145, 0.00077434181002900004, 0.00082129903603345156, 0.00086853635730221868,
0.00091610715026035905, 0.00096411682898178697, 0.0010126305278390646, 0.0010616052895784378,
0.0011108826147392392, 0.0011602368904277682, 0.0012094489065930247, 0.0012583627831190825,
0.0013069023843854666, 0.0013550462899729609, 0.0014027846045792103, 0.0014500867109745741,
0.0014968989416956902, 0.0015431707724928856, 0.0015888890484347939, 0.0016340982401743531,
0.0016788924112915993, 0.0017233812250196934, 0.0017676511779427528, 0.0018117419676855206,
0.0018556505674496293, 0.0018993609119206667, 0.0019428766099736094, 0.0019862416666001081,
0.0020295341964811087, 0.0020728406962007284, 0.0021162291523069143, 0.0021597379818558693,
0.0022033930290490389, 0.0022472396958619356, 0.0022913739085197449, 0.0023359460756182671,
0.0023811329156160355, 0.0024270866997539997, 0.0024738919455558062, 0.0025215502828359604,
0.0025700139813125134, 0.0026192441582679749, 0.0026692659594118595, 0.002720177173614502,
0.0027720888610929251, 0.0028250094037503004, 0.0028787164483219385, 0.0029326770454645157,
0.0029860674403607845, 0.0030379060190171003, 0.0030872693751007318, 0.0031335193198174238,
0.0031764607410877943, 0.0032163741998374462, 0.0032539025414735079, 0.0032898378558456898,
0.0033248732797801495, 0.003359407652169466, 0.0033934540115296841, 0.0034266682341694832,
0.0034584659151732922, 0.0034881711471825838, 0.0035151413176208735, 0.0035388274118304253,
0.0035587677266448736, 0.0035745392087846994, 0.0035856980830430984, 0.0035917432978749275,
0.0035921167582273483, 0.0035862282384186983, 0.0035734928678721189, 0.0035533567424863577,
0.0035253004170954227, 0.0034888240043073893, 0.003443423192948103, 0.0033885682933032513,
0.0033236993476748466, 0.0032482317183166742, 0.0031615688931196928, 0.0030631136614829302,
0.0029522709082812071, 0.0028284420259296894, 0.0026910160668194294, 0.0025393660180270672,
0.0023728485684841871, 0.0021908141206949949, 0.0019926181994378567, 0.0017776311142370105,
0.0015452421503141522, 0.001294855959713459, 0.0010258855763822794, 0.00073774566408246756,
0.00042984966421499848, 0.00010161137470277026, -0.00024754938203841448, -0.00061819725669920444,
-0.0010108760325238109, -0.001426108181476593, -0.0018643926596269011, -0.0023262077011168003,
-0.002812013728544116, -0.0033222525380551815, -0.003857344388961792, -0.0044176783412694931,
-0.0050036045722663403, -0.0056154225021600723, -0.0062533821910619736, -0.0069176913239061832,
-0.0076085370965301991, -0.008326113224029541, -0.0090706516057252884, -0.0098424339666962624,
-0.010641784407198429, -0.011469035409390926, -0.012324465438723564, -0.013208229094743729,
0.014120301231741905, 0.015060451813042164, 0.016028247773647308, 0.017023105174303055,
0.018044359982013702, 0.019091326743364334, 0.020163353532552719, 0.021259821951389313,
0.022380130365490913, 0.02352365106344223, 0.024689681828022003, 0.025877414271235466,
0.027085918933153152, 0.028314167633652687, 0.029561035335063934, 0.030825328081846237,
0.032105788588523865, 0.033401083201169968, 0.034709792584180832, 0.036030396819114685,
0.037361271679401398, 0.03870067372918129, 0.040046781301498413, 0.041397668421268463,
0.042751342058181763, 0.044105727225542068, 0.045458663254976273, 0.046807888895273209,
0.048151064664125443, 0.049485750496387482, 0.050809424370527267, 0.05211947113275528,
0.053413204848766327, 0.054687850177288055, 0.055940557271242142, 0.057168368250131607,
0.058368254452943802, 0.059537097811698914, 0.06067170575261116, 0.061768818646669388,
0.062825113534927368, 0.063837200403213501, 0.064801648259162903, 0.065714947879314423,
0.066573545336723328, 0.067373812198638916, 0.068112112581729889, 0.068784743547439575,
0.069387979805469513, 0.069918066263198853, 0.070371203124523163, 0.070743560791015625,
0.071031264960765839, 0.071230456233024597, 0.071337237954139709, 0.071347743272781372,
0.071258097887039185, 0.07106444239616394, 0.070762887597084045, 0.070349536836147308,
0.069820456206798553, 0.069171726703643799, 0.068399444222450256, 0.067499779164791107,
0.066468983888626099, 0.065303429961204529, 0.063999593257904053, 0.062554046511650085,
0.060963429510593414, 0.059224434196949005, 0.057333782315254211, 0.055288247764110565,
0.05308464914560318, 0.050719890743494034, 0.04819098487496376, 0.045495055615901947,
0.042629346251487732, 0.039591230452060699, 0.036378197371959686, 0.032987859100103378,
0.029417969286441803, 0.025666400790214539, 0.0217311792075634, 0.017610486596822739,
0.013302664272487164, 0.0088062174618244171, 0.0041198157705366611, -0.00075770384864881635,
-0.0058273370377719402, -0.011089906096458435, -0.016546055674552917, -0.022196246311068535,
-0.028040755540132523, -0.034079667180776596, -0.040312871336936951, -0.046740073710680008,
-0.053360763937234879, -0.06017424538731575, -0.0671796053647995, -0.074375726282596588,
-0.081761270761489868, -0.089334696531295776, -0.097094230353832245, -0.10503791272640228,
-0.1131635457277298, -0.12146873027086258, -0.12995083630084991, -0.13860704004764557,
-0.1474342942237854, -0.15642932057380676, -0.1655886322259903, -0.17490856349468231,
-0.18438516557216644, -0.19401434063911438, -0.20379173755645752, -0.21371282637119293,
-0.22377283871173859, -0.23396681249141693, -0.24428960680961609, -0.25473582744598389,
-0.26529994606971741, -0.27597621083259583, -0.2867586612701416, -0.29764124751091003,
-0.30861768126487732, -0.31968152523040771, -0.33082622289657593, -0.34204500913619995,
0.35333094000816345, 0.36467701196670532, 0.37607598304748535, 0.38752046227455139,
0.39900293946266174, 0.41051584482192993, 0.42205137014389038, 0.4336017370223999,
0.44515895843505859, 0.45671501755714417, 0.46826183795928955, 0.47979119420051575,
0.49129492044448853, 0.50276464223861694, 0.51419198513031006, 0.52556860446929932,
0.53688603639602661, 0.54813587665557861, 0.55930960178375244, 0.57039880752563477,
0.58139497041702271, 0.59228962659835815, 0.60307443141937256, 0.61374092102050781,
0.62428075075149536, 0.63468557596206665, 0.64494723081588745, 0.65505754947662354,
0.66500836610794067, 0.67479169368743896, 0.68439966440200806, 0.69382447004318237,
0.70305842161178589, 0.71209394931793213, 0.72092366218566895, 0.72954028844833374,
0.73793661594390869, 0.74610573053359985, 0.75404083728790283, 0.76173526048660278,
0.76918256282806396, 0.77637648582458496, 0.78331100940704346, 0.78998017311096191,
0.79637837409973145, 0.80250018835067749, 0.80834043025970459, 0.81389403343200684,
0.81915634870529175, 0.82412278652191162, 0.82878917455673218, 0.83315145969390869,
0.83720588684082031, 0.84094899892807007, 0.84437751770019531, 0.84748858213424683,
0.85027939081192017, 0.85274755954742432, 0.85489106178283691, 0.85670793056488037,
0.8581966757774353, 0.85935592651367188, 0.86018466949462891, 0.86068224906921387,
0.86084812879562378, 0.86068224906921387, 0.86018466949462891, 0.85935592651367188,
0.8581966757774353, 0.85670793056488037, 0.85489106178283691, 0.85274755954742432,
0.85027939081192017, 0.84748858213424683, 0.84437751770019531, 0.84094899892807007,
0.83720588684082031, 0.83315145969390869, 0.82878917455673218, 0.82412278652191162,
0.81915634870529175, 0.81389403343200684, 0.80834043025970459, 0.80250018835067749,
0.79637837409973145, 0.78998017311096191, 0.78331100940704346, 0.77637648582458496,
0.76918256282806396, 0.76173526048660278, 0.75404083728790283, 0.74610573053359985,
0.73793661594390869, 0.72954028844833374, 0.72092366218566895, 0.71209394931793213,
0.70305842161178589, 0.69382447004318237, 0.68439966440200806, 0.67479169368743896,
0.66500836610794067, 0.65505754947662354, 0.64494723081588745, 0.63468557596206665,
0.62428075075149536, 0.61374092102050781, 0.60307443141937256, 0.59228962659835815,
0.58139497041702271, 0.57039880752563477, 0.55930960178375244, 0.54813587665557861,
0.53688603639602661, 0.52556860446929932, 0.51419198513031006, 0.50276464223861694,
0.49129492044448853, 0.47979119420051575, 0.46826183795928955, 0.45671501755714417,
0.44515895843505859, 0.4336017370223999, 0.42205137014389038, 0.41051584482192993,
0.39900293946266174, 0.38752046227455139, 0.37607598304748535, 0.36467701196670532,
-0.35333094000816345, -0.34204500913619995, -0.33082622289657593, -0.31968152523040771,
-0.30861768126487732, -0.29764124751091003, -0.2867586612701416, -0.27597621083259583,
-0.26529994606971741, -0.25473582744598389, -0.24428960680961609, -0.23396681249141693,
-0.22377283871173859, -0.21371282637119293, -0.20379173755645752, -0.19401434063911438,
-0.18438516557216644, -0.17490856349468231, -0.1655886322259903, -0.15642932057380676,
-0.1474342942237854, -0.13860704004764557, -0.12995083630084991, -0.12146873027086258,
-0.1131635457277298, -0.10503791272640228, -0.097094230353832245, -0.089334696531295776,
-0.081761270761489868, -0.074375726282596588, -0.0671796053647995, -0.06017424538731575,
-0.053360763937234879, -0.046740073710680008, -0.040312871336936951, -0.034079667180776596,
-0.028040755540132523, -0.022196246311068535, -0.016546055674552917, -0.011089906096458435,
-0.0058273370377719402, -0.00075770384864881635, 0.0041198157705366611, 0.0088062174618244171,
0.013302664272487164, 0.017610486596822739, 0.0217311792075634, 0.025666400790214539,
0.029417969286441803, 0.032987859100103378, 0.036378197371959686, 0.039591230452060699,
0.042629346251487732, 0.045495055615901947, 0.04819098487496376, 0.050719890743494034,
0.05308464914560318, 0.055288247764110565, 0.057333782315254211, 0.059224434196949005,
0.060963429510593414, 0.062554046511650085, 0.063999593257904053, 0.065303429961204529,
0.066468983888626099, 0.067499779164791107, 0.068399444222450256, 0.069171726703643799,
0.069820456206798553, 0.070349536836147308, 0.070762887597084045, 0.07106444239616394,
0.071258097887039185, 0.071347743272781372, 0.071337237954139709, 0.071230456233024597,
0.071031264960765839, 0.070743560791015625, 0.070371203124523163, 0.069918066263198853,
0.069387979805469513, 0.068784743547439575, 0.068112112581729889, 0.067373812198638916,
0.066573545336723328, 0.065714947879314423, 0.064801648259162903, 0.063837200403213501,
0.062825113534927368, 0.061768818646669388, 0.06067170575261116, 0.059537097811698914,
0.058368254452943802, 0.057168368250131607, 0.055940557271242142, 0.054687850177288055,
0.053413204848766327, 0.05211947113275528, 0.050809424370527267, 0.049485750496387482,
0.048151064664125443, 0.046807888895273209, 0.045458663254976273, 0.044105727225542068,
0.042751342058181763, 0.041397668421268463, 0.040046781301498413, 0.03870067372918129,
0.037361271679401398, 0.036030396819114685, 0.034709792584180832, 0.033401083201169968,
0.032105788588523865, 0.030825328081846237, 0.029561035335063934, 0.028314167633652687,
0.027085918933153152, 0.025877414271235466, 0.024689681828022003, 0.02352365106344223,
0.022380130365490913, 0.021259821951389313, 0.020163353532552719, 0.019091326743364334,
0.018044359982013702, 0.017023105174303055, 0.016028247773647308, 0.015060451813042164,
-0.014120301231741905, -0.013208229094743729, -0.012324465438723564, -0.011469035409390926,
-0.010641784407198429, -0.0098424339666962624, -0.0090706516057252884, -0.008326113224029541,
-0.0076085370965301991, -0.0069176913239061832, -0.0062533821910619736, -0.0056154225021600723,
-0.0050036045722663403, -0.0044176783412694931, -0.003857344388961792, -0.0033222525380551815,
-0.002812013728544116, -0.0023262077011168003, -0.0018643926596269011, -0.001426108181476593,
-0.0010108760325238109, -0.00061819725669920444, -0.00024754938203841448, 0.00010161137470277026,
0.00042984966421499848, 0.00073774566408246756, 0.0010258855763822794, 0.001294855959713459,
0.0015452421503141522, 0.0017776311142370105, 0.0019926181994378567, 0.0021908141206949949,
0.0023728485684841871, 0.0025393660180270672, 0.0026910160668194294, 0.0028284420259296894,
0.0029522709082812071, 0.0030631136614829302, 0.0031615688931196928, 0.0032482317183166742,
0.0033236993476748466, 0.0033885682933032513, 0.003443423192948103, 0.0034888240043073893,
0.0035253004170954227, 0.0035533567424863577, 0.0035734928678721189, 0.0035862282384186983,
0.0035921167582273483, 0.0035917432978749275, 0.0035856980830430984, 0.0035745392087846994,
0.0035587677266448736, 0.0035388274118304253, 0.0035151413176208735, 0.0034881711471825838,
0.0034584659151732922, 0.0034266682341694832, 0.0033934540115296841, 0.003359407652169466,
0.0033248732797801495, 0.0032898378558456898, 0.0032539025414735079, 0.0032163741998374462,
0.0031764607410877943, 0.0031335193198174238, 0.0030872693751007318, 0.0030379060190171003,
0.0029860674403607845, 0.0029326770454645157, 0.0028787164483219385, 0.0028250094037503004,
0.0027720888610929251, 0.002720177173614502, 0.0026692659594118595, 0.0026192441582679749,
0.0025700139813125134, 0.0025215502828359604, 0.0024738919455558062, 0.0024270866997539997,
0.0023811329156160355, 0.0023359460756182671, 0.0022913739085197449, 0.0022472396958619356,
0.0022033930290490389, 0.0021597379818558693, 0.0021162291523069143, 0.0020728406962007284,
0.0020295341964811087, 0.0019862416666001081, 0.0019428766099736094, 0.0018993609119206667,
0.0018556505674496293, 0.0018117419676855206, 0.0017676511779427528, 0.0017233812250196934,
0.0016788924112915993, 0.0016340982401743531, 0.0015888890484347939, 0.0015431707724928856,
0.0014968989416956902, 0.0014500867109745741, 0.0014027846045792103, 0.0013550462899729609,
0.0013069023843854666, 0.0012583627831190825, 0.0012094489065930247, 0.0011602368904277682,
0.0011108826147392392, 0.0010616052895784378, 0.0010126305278390646, 0.00096411682898178697,
0.00091610715026035905, 0.00086853635730221868, 0.00082129903603345156, 0.00077434181002900004,
0.00072772568091750145, 0.00068160606315359473, 0.0006361178238876164, 0.00059120741207152605,
0.00054649583762511611, 0.00050126801943406463, 0.00045464080176316202, 0.0004058915947098285,
0.00035484600812196732, 0.00030217695166356862, 0.00024947625934146345, 0.00019903187057934701,
};
inline constexpr uint8_t kPbMap1[64] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
};
inline constexpr uint8_t kPbMap3[64] = {
0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
};
inline constexpr uint8_t kPbMap5[64] = {
0, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
};
inline constexpr uint8_t kPbMap7[64] = {
0, 1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 5, 5,
5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
};
inline constexpr uint8_t kPbMap9[64] = {
0, 1, 2, 3, 3, 3, 4, 5, 5, 6, 6, 6, 7, 7, 8, 8,
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
};
inline constexpr uint8_t kPbMap12[64] = {
0, 1, 2, 3, 4, 4, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8,
8, 8, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11,
11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11,
};
inline constexpr uint8_t kPbMap15[64] = {
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 10, 10, 11, 11, 11,
12, 12, 12, 12, 13, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 14,
14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14,
14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14,
};
inline constexpr uint8_t kPbMap23[64] = {
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 12, 13, 13,
14, 14, 15, 15, 16, 16, 16, 17, 17, 17, 18, 18, 18, 18, 19, 19,
19, 19, 19, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, 21, 21,
22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22,
};
} // namespace ejoc::tables
+500
View File
@@ -0,0 +1,500 @@
#include "oamd/oamd_parser.h"
#include <cmath>
#include <string>
#include "foundation/bit_reader.h"
namespace joc::oamd {
namespace {
constexpr int kSampleOffsetIndex[4] = {8, 16, 18, 24};
constexpr int kRampDurations[3] = {0, 512, 1536};
constexpr int kRampDurationIndex[16] = {32, 64, 128, 256, 320, 480,
1000, 1001, 1024, 1600, 1601, 1602,
1920, 2000, 2002, 2048};
constexpr std::uint32_t kObjectElementId = 1;
constexpr int kIsfObjectCounts[6] = {4, 8, 10, 14, 15, 30};
constexpr int kStandardBedChannelCount[10] = {2, 1, 1, 2, 2, 2, 2, 2, 2, 1};
Status variant(const std::string& name, const std::string& message) {
return Status::fail(JOC_ERR_OAMD_UNSUPPORTED_VARIANT, stage::kOamd,
name + ": " + message);
}
Status syntax(const std::string& message) {
return Status::fail(JOC_ERR_OAMD_SYNTAX, stage::kOamd, message);
}
bool variable_bits_max(bits::BitReader& reader, unsigned width, unsigned max_groups,
std::uint32_t* out_value) {
std::uint32_t value = reader.read(width);
if (reader.failed()) {
return false;
}
std::uint32_t more = reader.read(1);
if (reader.failed()) {
return false;
}
unsigned num_group = 1;
if (max_groups > num_group) {
if (more != 0u) {
value = (value + 1u) << width;
}
while (more != 0u) {
value += reader.read(width);
more = reader.read(1);
if (reader.failed()) {
return false;
}
if (num_group >= max_groups) {
break;
}
if (more != 0u) {
value = (value + 1u) << width;
num_group += 1;
}
}
}
*out_value = value;
return true;
}
struct ProgramInfo {
bool dynamic_object_only = false;
bool lfe_present = false;
std::uint32_t num_bed_objects = 0;
std::uint32_t num_isf_objects = 0;
std::int32_t num_dynamic_objects = -1;
std::vector<BedAssignment> bed_assignments;
};
Status parse_program_assignment(bits::BitReader& reader, ProgramInfo* program) {
program->dynamic_object_only = reader.read(1) != 0u;
if (reader.failed()) {
return syntax("program_assignment truncated");
}
if (program->dynamic_object_only) {
program->lfe_present = reader.read(1) != 0u;
if (reader.failed()) {
return syntax("program_assignment truncated");
}
program->num_bed_objects = program->lfe_present ? 1u : 0u;
return Status::success();
}
const std::uint32_t mask = reader.read(4);
if (reader.failed()) {
return syntax("program_assignment truncated");
}
if ((mask & 0x1u) != 0u) {
reader.read(1);
const std::uint32_t multi = reader.read(1);
if (reader.failed()) {
return syntax("program_assignment truncated");
}
std::uint32_t instances = 1;
if (multi != 0u) {
instances = reader.read(3) + 2u;
}
for (std::uint32_t instance = 0; instance < instances; ++instance) {
BedAssignment assignment;
if (reader.read(1) != 0u) {
assignment.lfe_only = true;
assignment.mask = 0;
program->bed_assignments.push_back(assignment);
program->num_bed_objects += 1;
continue;
}
if (reader.read(1) != 0u) {
assignment.standard = true;
assignment.mask = reader.read(10);
for (int bit = 0; bit < 10; ++bit) {
if (((assignment.mask >> bit) & 1u) != 0u) {
program->num_bed_objects +=
static_cast<std::uint32_t>(kStandardBedChannelCount[bit]);
}
}
} else {
assignment.standard = false;
assignment.mask = reader.read(17);
for (int bit = 0; bit < 17; ++bit) {
if (((assignment.mask >> bit) & 1u) != 0u) {
program->num_bed_objects += 1;
}
}
}
program->bed_assignments.push_back(assignment);
if (reader.failed()) {
return syntax("program_assignment truncated");
}
}
}
if ((mask & 0x2u) != 0u) {
const std::uint32_t isf_idx = reader.read(3);
if (reader.failed()) {
return syntax("program_assignment truncated");
}
program->num_isf_objects =
(isf_idx < 6u) ? static_cast<std::uint32_t>(kIsfObjectCounts[isf_idx]) : 0u;
}
if ((mask & 0x4u) != 0u) {
std::uint32_t count = reader.read(5);
if (count == 0x1Fu) {
count += reader.read(7);
}
if (reader.failed()) {
return syntax("program_assignment truncated");
}
program->num_dynamic_objects = static_cast<std::int32_t>(count + 1u);
}
if ((mask & 0x8u) != 0u) {
const std::uint32_t reserved_bytes = reader.read(4) + 1u;
if (!reader.skip(static_cast<std::size_t>(reserved_bytes) * 8u)) {
return syntax("program_assignment reserved data truncated");
}
}
return Status::success();
}
Status parse_object_info_block(bits::BitReader& reader, int object_index, bool in_bed_or_isf,
int* position_x, int* position_y, int* position_z, bool* has_position) {
*has_position = false;
const bool not_active = reader.read(1) != 0u;
if (reader.failed()) {
return syntax("object_info_block truncated");
}
const std::uint32_t basic_status = not_active ? 0u : 1u;
if (basic_status == 1u) {
const std::uint32_t gain_idx = reader.read(2);
if (gain_idx == 2u) {
reader.read(6);
}
const bool default_priority = reader.read(1) != 0u;
if (!default_priority) {
reader.read(5);
}
if (reader.failed()) {
return syntax("object_info_block truncated");
}
}
const std::uint32_t render_status = (not_active || in_bed_or_isf) ? 0u : 1u;
if (render_status == 1u) {
const int x = static_cast<int>(reader.read(6));
const int y = static_cast<int>(reader.read(6));
const int z_sign = static_cast<int>(reader.read(1));
const int z = static_cast<int>(reader.read(4));
if (reader.failed()) {
return syntax("object_info_block truncated");
}
*position_x = x;
*position_y = y;
*position_z = z_sign != 0 ? z : -z;
*has_position = true;
if (reader.read(1) != 0u) {
if (reader.read(1) == 0u) {
reader.read(4);
}
}
reader.read(3);
reader.read(1);
const std::uint32_t size_idx = reader.read(2);
if (size_idx == 1u) {
reader.read(5);
} else if (size_idx == 2u) {
reader.read(15);
}
if (reader.read(1) != 0u) {
reader.read(3);
reader.read(2);
}
reader.read(1);
if (reader.failed()) {
return syntax("object_info_block truncated");
}
}
if (reader.read(1) != 0u) {
const std::uint32_t additional_bytes = reader.read(4) + 1u;
if (!reader.skip(static_cast<std::size_t>(additional_bytes) * 8u)) {
return syntax("object_info_block additional table truncated");
}
}
(void)object_index;
return Status::success();
}
struct ObjectElementInfo {
std::uint32_t sample_offset_code = 0;
std::uint32_t sample_offset = 0;
std::uint32_t block_count = 0;
std::uint32_t block_offset_samples = 0;
std::uint32_t ramp_duration_samples = 0;
bool reserved_data_not_present = false;
};
Status parse_object_element(bits::BitReader& reader, std::uint32_t object_count,
std::uint32_t bed_isf_objects, ObjectElementInfo* out, OamdUpdate* update) {
out->sample_offset_code = reader.read(2);
if (reader.failed()) {
return syntax("object_element truncated");
}
switch (out->sample_offset_code) {
case 0: out->sample_offset = 0; break;
case 1: {
const std::uint32_t index = reader.read(2);
if (reader.failed()) {
return syntax("object_element truncated");
}
out->sample_offset = static_cast<std::uint32_t>(kSampleOffsetIndex[index & 3u]);
break;
}
case 2: out->sample_offset = reader.read(5); break;
default:
return variant("md_sample_offset_mode",
"MD sample-offset mode " +
std::to_string(out->sample_offset_code) +
" is not covered by the 16-slot model");
}
out->block_count = reader.read(3) + 1u;
if (reader.failed()) {
return syntax("object_element truncated");
}
for (std::uint32_t block = 0; block < out->block_count; ++block) {
const std::uint32_t block_offset_factor = reader.read(6);
const std::uint32_t ramp_code = reader.read(2);
std::uint32_t ramp_duration = 0;
if (ramp_code == 3u) {
if (reader.read(1) != 0u) {
const std::uint32_t index = reader.read(4);
if (reader.failed()) {
return syntax("object_element truncated");
}
ramp_duration = static_cast<std::uint32_t>(kRampDurationIndex[index & 15u]);
} else {
ramp_duration = reader.read(11);
}
} else {
ramp_duration = static_cast<std::uint32_t>(kRampDurations[ramp_code]);
}
if (reader.failed()) {
return syntax("object_element truncated");
}
if (block == 0) {
out->block_offset_samples = out->sample_offset + block_offset_factor * 32u;
out->ramp_duration_samples = ramp_duration;
}
}
out->reserved_data_not_present = reader.read(1) != 0u;
if (!out->reserved_data_not_present) {
reader.read(5);
}
if (reader.failed()) {
return syntax("object_element truncated");
}
for (std::uint32_t index = 0; index < object_count; ++index) {
int x = 0;
int y = 0;
int z = 0;
bool has_position = false;
const bool in_bed_or_isf = index < bed_isf_objects;
const Status status =
parse_object_info_block(reader, static_cast<int>(index), in_bed_or_isf, &x, &y, &z,
&has_position);
if (!status.ok()) {
return status;
}
if (!has_position || index >= static_cast<std::uint32_t>(kMaxSlots)) {
continue;
}
update->slots[index].q1 =
(x >= 0 && x <= kNQ12) ? static_cast<std::int16_t>(q_of(x, kNQ12)) : static_cast<std::int16_t>(-1);
update->slots[index].q2 =
(y >= 0 && y <= kNQ12) ? static_cast<std::int16_t>(q_of(y, kNQ12)) : static_cast<std::int16_t>(-1);
if (z >= 0 && z <= kNQ3) {
update->slots[index].q3 = static_cast<std::int16_t>(q_of(z, kNQ3));
} else if (z < 0) {
update->slots[index].q3 = 0;
} else {
update->slots[index].q3 = static_cast<std::int16_t>(-1);
}
}
return Status::success();
}
} // namespace
int q_of(int k, int n) {
const double value = std::floor(32768.0 * static_cast<double>(k) / static_cast<double>(n) + 0.5);
const int quantised = static_cast<int>(value);
return quantised > 32767 ? 32767 : quantised;
}
Status parse_id11(const std::uint8_t* payload, std::size_t payload_size, OamdUpdate* out) {
return parse_id11_verbose(payload, payload_size, out, nullptr);
}
Status parse_id11_verbose(const std::uint8_t* payload, std::size_t payload_size, OamdUpdate* out,
ParseTrace* trace) {
if (payload == nullptr || out == nullptr) {
return Status::fail(JOC_ERR_INVALID_ARGUMENT, stage::kOamd, "null payload or output");
}
if (payload_size == 0) {
return variant("header_truncated", "OAMD payload is empty");
}
*out = OamdUpdate{};
bits::BitReader reader(payload, payload_size);
std::uint32_t version = reader.read(2);
if (reader.failed()) {
return variant("header_truncated", "OAMD payload cannot hold the 2-bit version");
}
if (version == 3u) {
version += reader.read(3);
if (reader.failed()) {
return syntax("OAMD version extension truncated");
}
}
std::uint32_t object_count_bits = reader.read(5);
if (object_count_bits == 0x1Fu) {
object_count_bits += reader.read(7);
}
if (reader.failed()) {
return syntax("OAMD object count truncated");
}
const std::uint32_t object_count = object_count_bits + 1u;
ProgramInfo program;
Status status = parse_program_assignment(reader, &program);
if (!status.ok()) {
return status;
}
const std::uint32_t alternate_present = reader.read(1);
if (reader.failed()) {
return syntax("OAMD alternate data flag truncated");
}
std::uint32_t element_count = reader.read(4);
if (element_count == 0xFu) {
element_count += reader.read(5);
}
if (reader.failed()) {
return syntax("OAMD element count truncated");
}
if (element_count == 0u) {
return variant("missing_object_element", "OAMD declares no element");
}
const std::uint32_t bed_isf_objects = program.num_bed_objects + program.num_isf_objects;
bool have_object_element = false;
ObjectElementInfo object_element;
for (std::uint32_t ordinal = 0; ordinal < element_count; ++ordinal) {
const std::size_t header_start = reader.position();
const std::uint32_t element_id = reader.read(4);
std::uint32_t size_field = 0;
if (!variable_bits_max(reader, 4, 4, &size_field)) {
return syntax("OAMD element header truncated at ordinal " + std::to_string(ordinal));
}
const std::uint32_t size_bytes = size_field + 1u;
const std::size_t region_start = reader.position();
const std::size_t region_end = region_start + static_cast<std::size_t>(size_bytes) * 8u;
if (region_end > reader.limit()) {
return variant("element_bounds",
"element " + std::to_string(ordinal) + " (id " +
std::to_string(element_id) + ") declares " +
std::to_string(size_bytes) +
" bytes, beyond the payload (header at bit " +
std::to_string(header_start) + ")");
}
if (alternate_present != 0u) {
reader.read(4);
}
reader.read(1);
if (reader.failed()) {
return syntax("OAMD element control fields truncated at ordinal " +
std::to_string(ordinal));
}
if (element_id == kObjectElementId) {
if (have_object_element) {
return variant("multiple_object_elements", "OAMD contains several object elements");
}
have_object_element = true;
const Status parsed = parse_object_element(reader, object_count, bed_isf_objects,
&object_element, out);
if (!parsed.ok()) {
return parsed;
}
if (trace != nullptr) {
trace->parsed_end_bit = static_cast<std::uint32_t>(reader.position());
}
if (reader.position() < region_end) {
reader.set_limit_bits(reader.limit());
reader.skip(region_end - reader.position());
}
} else {
if (!reader.skip(region_end - reader.position())) {
return syntax("OAMD element skip past payload end");
}
}
if (reader.failed()) {
return syntax("OAMD element parse failed at ordinal " + std::to_string(ordinal));
}
}
if (!have_object_element) {
return variant("missing_object_element", "OAMD has no object element");
}
// Trailing padding must be zero over the whole remainder.
{
bits::BitReader tail(payload, payload_size);
tail.reset(payload, payload_size, reader.position());
while (tail.remaining_bits() > 0) {
const std::size_t chunk =
tail.remaining_bits() > 32u ? 32u : tail.remaining_bits();
if (tail.read(static_cast<unsigned>(chunk)) != 0u) {
return Status::fail(JOC_ERR_BITSTREAM_PADDING, stage::kOamd,
"OAMD payload padding is not zero (from bit " +
std::to_string(reader.position()) + ")");
}
}
}
if (version != 0u) {
return variant("oamd_version",
"OAMD syntax version " + std::to_string(version) + " is not covered");
}
if (object_count > static_cast<std::uint32_t>(kMaxSlots)) {
return variant("object_count", "OAMD object count " + std::to_string(object_count) +
" exceeds the 16-slot model");
}
if (object_element.block_count != 1u) {
return variant("multiple_position_blocks",
"OAMD frame carries " + std::to_string(object_element.block_count) +
" position blocks; a single frame_update cannot express that");
}
out->block_offset_samples = object_element.block_offset_samples;
out->ramp_duration_samples = object_element.ramp_duration_samples;
out->object_count = object_count;
out->dynamic_object_only = program.dynamic_object_only;
out->lfe_present = program.lfe_present;
out->num_bed_objects = program.num_bed_objects;
out->num_isf_objects = program.num_isf_objects;
out->num_dynamic_objects = program.num_dynamic_objects;
if (trace != nullptr) {
trace->bed_assignments = program.bed_assignments;
trace->element_count = element_count;
trace->object_element_count = have_object_element ? 1u : 0u;
trace->sample_offset_code = object_element.sample_offset_code;
trace->sample_offset = object_element.sample_offset;
trace->block_count = object_element.block_count;
trace->reserved_data_not_present = object_element.reserved_data_not_present;
}
return Status::success();
}
} // namespace joc::oamd
+103
View File
@@ -0,0 +1,103 @@
// Port of src/oamd_bits.py.
#pragma once
#include <cstddef>
#include <cstdint>
#include <vector>
#include "foundation/status.h"
#include "joc_core.h"
namespace joc::oamd {
inline constexpr int kMaxSlots = 16;
inline constexpr int kObjects = 15;
inline constexpr int kAxes = 3;
inline constexpr int kNQ12 = 62;
inline constexpr int kNQ3 = 15;
inline constexpr int kQ15Scale = 32768;
// (the reference's None), i.e. hold the previous position.
struct SlotUpdate {
std::int16_t q1 = -1;
std::int16_t q2 = -1;
std::int16_t q3 = -1;
};
struct OamdUpdate {
SlotUpdate slots[kMaxSlots];
std::uint32_t block_offset_samples = 0;
std::uint32_t ramp_duration_samples = 0;
std::uint32_t object_count = 0;
bool dynamic_object_only = false;
bool lfe_present = false;
std::uint32_t num_bed_objects = 0;
std::uint32_t num_isf_objects = 0;
std::int32_t num_dynamic_objects = -1;
};
class OamdState {
public:
OamdState() { reset(); }
void reset() {
for (int slot = 0; slot < kMaxSlots; ++slot) {
for (int axis = 0; axis < kAxes; ++axis) {
q_[slot][axis] = 0;
}
}
q_[0][0] = 16384;
q_[0][1] = 16384;
}
void apply(const OamdUpdate& update) {
for (int slot = 0; slot < kMaxSlots; ++slot) {
const SlotUpdate& value = update.slots[slot];
if (value.q1 >= 0) { q_[slot][0] = value.q1; }
if (value.q2 >= 0) { q_[slot][1] = value.q2; }
if (value.q3 >= 0) { q_[slot][2] = value.q3; }
}
}
std::int16_t q(int slot, int axis) const { return q_[slot][axis]; }
// Objects 1..15 as q15 triples, the layout the speaker renderer consumes.
void object_positions_q15(std::uint16_t out[kObjects][kAxes]) const {
for (int object = 0; object < kObjects; ++object) {
for (int axis = 0; axis < kAxes; ++axis) {
out[object][axis] = static_cast<std::uint16_t>(q_[object + 1][axis]);
}
}
}
private:
std::int16_t q_[kMaxSlots][kAxes] = {};
};
// q_of(k, n) = min(32767, floor(32768 k / n + 0.5)), the reference's quantiser.
int q_of(int k, int n);
Status parse_id11(const std::uint8_t* payload, std::size_t payload_size, OamdUpdate* out);
struct BedAssignment {
bool lfe_only = false;
bool standard = false;
std::uint32_t mask = 0;
};
struct ParseTrace {
std::vector<BedAssignment> bed_assignments;
std::uint32_t element_count = 0;
std::uint32_t object_element_count = 0;
std::uint32_t sample_offset_code = 0;
std::uint32_t sample_offset = 0;
std::uint32_t block_count = 0;
std::uint32_t parsed_end_bit = 0;
bool reserved_data_not_present = false;
};
Status parse_id11_verbose(const std::uint8_t* payload, std::size_t payload_size, OamdUpdate* out,
ParseTrace* trace);
} // namespace joc::oamd
+101
View File
@@ -0,0 +1,101 @@
// CPU feature probe; see cpu_probe.h.
//
// Nothing here is ISA-specific code: the x86 path uses CPUID / XGETBV (MSVC) or
// the compiler's own runtime probe (GCC/Clang, so no inline assembly), and the
// AArch64 path reads the aux vector. That is what lets this unit stay baseline
// and run before the dispatcher has decided anything.
#include "simd/cpu_probe.h"
#if defined(_M_X64)
#include <immintrin.h>
#include <intrin.h>
#elif defined(__x86_64__)
#include <cpuid.h>
#endif
#if defined(__linux__) && (defined(__aarch64__) || defined(_M_ARM64))
#include <sys/auxv.h>
#endif
namespace joc::simd {
namespace {
// ------------------------------------------------------------------- x86-64 --
#if defined(_M_X64) || defined(__x86_64__)
#if defined(_M_X64)
// CPUID tells us what the silicon can do; XCR0 tells us whether the OS saves the
// state the wider registers need. Both have to agree, or the first AVX
// instruction after a context switch corrupts another thread's registers.
CpuFeatures probe_x86() noexcept {
CpuFeatures features;
int regs[4] = {0, 0, 0, 0};
__cpuid(regs, 0);
const int max_leaf = regs[0];
__cpuid(regs, 1);
features.sse2 = (regs[3] & (1 << 26)) != 0;
const bool osxsave = (regs[2] & (1 << 27)) != 0;
const bool avx = (regs[2] & (1 << 28)) != 0;
if (!osxsave || !avx || max_leaf < 7) {
return features;
}
const unsigned long long xcr0 = _xgetbv(0);
const bool ymm_state = (xcr0 & 0x06ull) == 0x06ull; // XMM + YMM saved
const bool zmm_state = (xcr0 & 0xe6ull) == 0xe6ull; // + opmask / ZMM / hi16
if (!ymm_state) {
return features;
}
__cpuidex(regs, 7, 0);
features.avx2 = (regs[1] & (1 << 5)) != 0;
features.avx512 = zmm_state && (regs[1] & (1 << 16)) != 0; // AVX512F
return features;
}
#else // GCC/Clang on x86-64
// The compiler runtime performs the same CPUID + XGETBV probe (libgcc's cpuinfo
// checks XCR0 before it reports AVX), which keeps this file free of inline
// assembly.
CpuFeatures probe_x86() noexcept {
CpuFeatures features;
features.sse2 = __builtin_cpu_supports("sse2") != 0;
features.avx2 = __builtin_cpu_supports("avx2") != 0;
features.avx512 = __builtin_cpu_supports("avx512f") != 0;
return features;
}
#endif
CpuFeatures probe() noexcept { return probe_x86(); }
// ----------------------------------------------------------------- AArch64 --
#elif defined(__aarch64__) || defined(_M_ARM64)
CpuFeatures probe() noexcept {
CpuFeatures features;
#if defined(__linux__)
// ASIMD is architectural for AArch64; the aux vector only confirms it.
features.neon = (getauxval(AT_HWCAP) & (1u << 1)) != 0u;
#else
features.neon = true;
#endif
return features;
}
// ------------------------------------------------------------ anything else --
#else
CpuFeatures probe() noexcept { return CpuFeatures{}; }
#endif
} // namespace
const CpuFeatures& cpu_features() noexcept {
static const CpuFeatures features = probe();
return features;
}
} // namespace joc::simd
+30
View File
@@ -0,0 +1,30 @@
#pragma once
// "Can this CPU and this operating system actually execute ISA X?"
//
// Its own unit and free of policy: it reads CPUID / XGETBV / HWCAP, caches the
// answer, and knows nothing about JOC_SIMD, the kernel table or which
// implementation is preferred. This is the one place that can fault the process
// if it is wrong, so it stays small enough to review on its own.
//
// Compiled with the baseline ISA like every other non-intrinsic unit (see
// CMakeLists.txt): it runs before any ISA unit is reached, so it must not execute
// a wide instruction itself.
namespace joc::simd {
// One flag per ISA the dispatcher can ask about. A flag means "usable here":
// both the silicon and the OS state-management agree, not merely that CPUID
// advertises the feature.
struct CpuFeatures {
bool sse2 = false;
bool avx2 = false;
bool avx512 = false;
bool neon = false;
};
// Probed once, on first use. Never throws and never reads the environment (the
// JOC_SIMD override is the dispatcher's business).
const CpuFeatures& cpu_features() noexcept;
} // namespace joc::simd
+293
View File
@@ -0,0 +1,293 @@
// Kernel dispatch: CPU features in, kernel table out.
//
// This file holds policy only -- which ISA is asked for, which ladder is walked,
// which slot ends up with which implementation. The question "can this machine
// run ISA X at all" belongs to cpu_probe.cpp, and the implementations themselves
// belong to the one ISA unit per ISA (kernels_scalar.cpp / kernels_intrin_*.cpp).
//
// JOC_SIMD=auto|scalar|sse2|avx2|avx512|neon. A forced ISA that this build or
// this machine cannot provide is reported on stderr and then ignored -- forcing a
// path the CPU lacks would fault instead of verifying anything.
//
// This unit is compiled with the baseline ISA: it runs before anything else in
// any binary that links the kernels, so it must not execute an ISA-specific
// instruction itself.
#include "simd/simd.h"
#include "simd/cpu_probe.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
// Which ISA units the build system put into this binary. An ISA that was not
// compiled in has no entry point to reference at all.
#if !defined(JOC_SIMD_HAVE_SSE2)
#define JOC_SIMD_HAVE_SSE2 0
#endif
#if !defined(JOC_SIMD_HAVE_AVX2)
#define JOC_SIMD_HAVE_AVX2 0
#endif
#if !defined(JOC_SIMD_HAVE_AVX512)
#define JOC_SIMD_HAVE_AVX512 0
#endif
#if !defined(JOC_SIMD_HAVE_NEON)
#define JOC_SIMD_HAVE_NEON 0
#endif
namespace joc::simd {
// Implemented by the per-ISA units; referenced only when compiled in. The file
// name carries the ISA (FLAC's `*_intrin_<isa>.c` convention), and the CMake list
// gives exactly those units their wider /arch or -m flag.
const Kernels& kernels_scalar() noexcept;
#if JOC_SIMD_HAVE_AVX2
const Kernels& kernels_intrin_avx2() noexcept;
#endif
#if JOC_SIMD_HAVE_AVX512
const Kernels& kernels_intrin_avx512() noexcept;
#endif
#if JOC_SIMD_HAVE_NEON
const Kernels& kernels_intrin_neon() noexcept;
#endif
namespace {
constexpr std::size_t kKernelCount = static_cast<std::size_t>(Kernel::count);
// The probe itself lives in cpu_probe.cpp; all this file needs from it is the
// cached answer, turned into the two questions the ladder asks: "was it compiled
// into this binary" (JOC_SIMD_HAVE_*) and "can this machine run it".
struct Resolution {
Kernels kernels;
Isa active[kKernelCount] = {};
Isa selected = Isa::scalar;
bool forced = false;
};
int isa_rank(Isa isa) noexcept {
switch (isa) {
case Isa::scalar: return 0;
case Isa::sse2: return 1;
case Isa::neon: return 1;
case Isa::avx2: return 2;
case Isa::avx512: return 3;
}
return 0;
}
bool isa_compiled(Isa isa) noexcept {
switch (isa) {
case Isa::scalar: return true;
// The x86-64 baseline *is* SSE2: the baseline units are compiled for it
// and there is no separate unit, because a 128-bit SSE2 register is
// exactly the register a scalar double already uses -- SSE2 cannot widen
// a double-precision operation, so "sse2" selects the reference kernels.
case Isa::sse2: return JOC_SIMD_HAVE_SSE2 != 0;
case Isa::avx2: return JOC_SIMD_HAVE_AVX2 != 0;
case Isa::avx512: return JOC_SIMD_HAVE_AVX512 != 0;
case Isa::neon: return JOC_SIMD_HAVE_NEON != 0;
}
return false;
}
const Kernels& kernels_of(Isa isa) noexcept {
switch (isa) {
case Isa::scalar:
case Isa::sse2: return kernels_scalar();
#if JOC_SIMD_HAVE_AVX2
case Isa::avx2: return kernels_intrin_avx2();
#endif
#if JOC_SIMD_HAVE_AVX512
case Isa::avx512: return kernels_intrin_avx512();
#endif
#if JOC_SIMD_HAVE_NEON
case Isa::neon: return kernels_intrin_neon();
#endif
default: break;
}
return kernels_scalar();
}
bool parse_isa(const char* text, Isa* out) noexcept {
struct Entry {
const char* name;
Isa isa;
};
static const Entry kEntries[] = {
{"scalar", Isa::scalar}, {"sse2", Isa::sse2}, {"avx2", Isa::avx2},
{"avx512", Isa::avx512}, {"neon", Isa::neon},
};
for (const Entry& entry : kEntries) {
if (std::strcmp(text, entry.name) == 0) {
*out = entry.isa;
return true;
}
}
return false;
}
Resolution resolve() noexcept {
Resolution resolution;
Isa best = Isa::scalar;
bool forced = false;
if (const char* requested = std::getenv("JOC_SIMD");
requested != nullptr && requested[0] != '\0' && std::strcmp(requested, "auto") != 0) {
Isa wanted = Isa::scalar;
if (!parse_isa(requested, &wanted)) {
std::fprintf(stderr,
"joc: JOC_SIMD=%s is not one of auto|scalar|sse2|avx2|avx512|neon; "
"using auto\n",
requested);
} else if (!isa_compiled(wanted)) {
std::fprintf(stderr, "joc: JOC_SIMD=%s is not part of this build; using auto\n",
requested);
} else if (!isa_supported(wanted)) {
std::fprintf(stderr, "joc: JOC_SIMD=%s is not supported by this CPU or OS; using auto\n",
requested);
} else {
best = wanted;
forced = true;
}
}
if (!forced) {
// Widest runnable ISA, in descending order.
static const Isa kLadder[] = {Isa::avx512, Isa::avx2, Isa::sse2, Isa::neon};
for (const Isa candidate : kLadder) {
if (isa_compiled(candidate) && isa_supported(candidate)) {
best = candidate;
break;
}
}
}
resolution.selected = best;
resolution.forced = forced;
// Per kernel: widest compiled + runnable implementation at or below the
// selected ISA, falling back to the scalar reference, which is always there.
static const Isa kCandidates[] = {Isa::avx512, Isa::avx2, Isa::sse2, Isa::neon, Isa::scalar};
for (std::size_t index = 0u; index < kKernelCount; ++index) {
const Kernel kernel = static_cast<Kernel>(index);
resolution.active[index] = Isa::scalar;
for (const Isa candidate : kCandidates) {
if (isa_rank(candidate) > isa_rank(best) || !isa_compiled(candidate)
|| !isa_supported(candidate)) {
continue;
}
const Kernels& set = kernels_of(candidate);
bool resolved = false;
switch (kernel) {
case Kernel::fft_butterflies:
if (set.fft_butterflies != nullptr) {
resolution.kernels.fft_butterflies = set.fft_butterflies;
resolved = true;
}
break;
case Kernel::qmf_synthesis_basis:
if (set.qmf_synthesis_basis != nullptr) {
resolution.kernels.qmf_synthesis_basis = set.qmf_synthesis_basis;
resolved = true;
}
break;
case Kernel::hybrid_low_join:
if (set.hybrid_low_join != nullptr) {
resolution.kernels.hybrid_low_join = set.hybrid_low_join;
resolved = true;
}
break;
case Kernel::render_hybrid_path:
if (set.render_hybrid_path != nullptr) {
resolution.kernels.render_hybrid_path = set.render_hybrid_path;
resolved = true;
}
break;
case Kernel::complex_axpy:
if (set.complex_axpy != nullptr) {
resolution.kernels.complex_axpy = set.complex_axpy;
resolved = true;
}
break;
case Kernel::qmf_analysis_taps:
if (set.qmf_analysis_taps != nullptr) {
resolution.kernels.qmf_analysis_taps = set.qmf_analysis_taps;
resolved = true;
}
break;
case Kernel::complex_multiply:
if (set.complex_multiply != nullptr) {
resolution.kernels.complex_multiply = set.complex_multiply;
resolved = true;
}
break;
case Kernel::count: break;
}
if (resolved) {
resolution.active[index] = candidate;
break;
}
}
}
if (std::getenv("JOC_SIMD_LOG") != nullptr) {
std::fprintf(stderr, "joc: simd selected=%s forced=%d\n", isa_name(resolution.selected),
resolution.forced ? 1 : 0);
for (std::size_t index = 0u; index < kKernelCount; ++index) {
std::fprintf(stderr, "joc: simd kernel[%u]=%s\n", static_cast<unsigned>(index),
isa_name(resolution.active[index]));
}
}
return resolution;
}
const Resolution& resolution() noexcept {
static const Resolution resolved = resolve();
return resolved;
}
} // namespace
const char* isa_name(Isa isa) noexcept {
switch (isa) {
case Isa::scalar: return "scalar";
case Isa::sse2: return "sse2";
case Isa::avx2: return "avx2";
case Isa::avx512: return "avx512";
case Isa::neon: return "neon";
}
return "?";
}
bool isa_supported(Isa isa) noexcept {
const CpuFeatures& cpu = cpu_features();
switch (isa) {
case Isa::scalar:
return true;
case Isa::sse2:
return cpu.sse2;
case Isa::avx2:
return cpu.avx2;
case Isa::avx512:
return cpu.avx512;
case Isa::neon:
return cpu.neon;
}
return false;
}
Isa selected_isa() noexcept { return resolution().selected; }
Isa active_isa(Kernel kernel) noexcept {
const std::size_t index = static_cast<std::size_t>(kernel);
if (index >= kKernelCount) {
return Isa::scalar;
}
return resolution().active[index];
}
const Kernels& kernels() noexcept { return resolution().kernels; }
} // namespace joc::simd
+321
View File
@@ -0,0 +1,321 @@
// AVX2 implementation of the dispatched kernels. Compiled with /arch:AVX2 (or
// -mavx2) and only ever called after the dispatcher has confirmed that this CPU
// and the OS state it saves can execute AVX2 code.
//
// CMake gives exactly the `kernels_intrin_*.cpp` units a wider flag, so no
// baseline unit can inherit one by accident.
//
// Nothing in this file may have dynamic initialisation: it is linked into the
// same image as the baseline code and would run before the dispatcher.
#include "simd/simd.h"
#include <immintrin.h>
#include <cstddef>
namespace joc::simd {
namespace {
// Two interleaved complexes per vector.
//
// The scalar kernel computes, for one butterfly,
// odd.re = o.re * w.re - o.im * w.im
// odd.im = o.re * w.im + o.im * w.re
// with separate multiplies and one rounding per operation. In a 256-bit vector
// the even element of every 128-bit half is a real part, so duplicating the even
// elements of the twiddle broadcasts w.re onto both components of a complex and
// permuting the odd data elements in lines up `o.im`, `o.re` against `w.im`.
// addsub then subtracts on the real lanes and adds on the imaginary ones, which
// is the same two roundings: `o.im * w.re + o.re * w.im` is the scalar's
// `o.re * w.im + o.im * w.re` with the summands exchanged, and floating-point
// addition is commutative.
inline __m256d mul_pair(__m256d odd, __m256d twiddle) noexcept {
const __m256d real = _mm256_mul_pd(odd, _mm256_movedup_pd(twiddle));
const __m256d cross = _mm256_mul_pd(_mm256_permute_pd(odd, 0x05), _mm256_permute_pd(twiddle, 0x0f));
return _mm256_addsub_pd(real, cross);
}
// The radix-2 DIT cascade over a bit-reversed buffer of any power-of-two size at
// or above kMinVectorFftSize.
//
// Stage s pairs element (start + offset) with (start + offset + 2^s) for every
// group of 2^(s+1) elements, and the butterflies of a stage are mutually
// independent: none of them reads a slot another one writes. They are therefore
// free to share a vector, and each one still accumulates nothing -- every output
// is a single add or subtract of two products, in lanes whose operations are
// independent of each other.
//
// Stage 0 is the one case whose group holds a single butterfly, so the pair is
// packed from two neighbouring groups instead: their factors are identical
// (offset is always 0), and a permute2f128 moves the even and odd halves into
// two vector registers.
void fft_butterflies_avx2(double* data, std::size_t size, const double* twiddle,
const std::size_t* stage_begin) noexcept {
{
const auto* pair = reinterpret_cast<const __m128d*>(twiddle + 2u * stage_begin[0]);
const __m256d factor = _mm256_broadcast_pd(pair);
for (std::size_t index = 0u; index < 2u * size; index += 8u) {
const __m256d low = _mm256_loadu_pd(data + index);
const __m256d high = _mm256_loadu_pd(data + index + 4u);
const __m256d even = _mm256_permute2f128_pd(low, high, 0x20);
const __m256d odd = _mm256_permute2f128_pd(low, high, 0x31);
const __m256d rotated = mul_pair(odd, factor);
const __m256d sum = _mm256_add_pd(even, rotated);
const __m256d difference = _mm256_sub_pd(even, rotated);
_mm256_storeu_pd(data + index, _mm256_permute2f128_pd(sum, difference, 0x20));
_mm256_storeu_pd(data + index + 4u, _mm256_permute2f128_pd(sum, difference, 0x31));
}
}
std::size_t stage = 1u;
for (std::size_t half = 2u; half < size; half <<= 1u, ++stage) {
const std::size_t length = half << 1u;
const double* table = twiddle + 2u * stage_begin[stage];
for (std::size_t start = 0u; start < size; start += length) {
for (std::size_t offset = 0u; offset < half; offset += 2u) {
const std::size_t even_index = (start + offset) * 2u;
const std::size_t odd_index = even_index + half * 2u;
const std::size_t factor_index = offset * 2u;
const __m256d even = _mm256_loadu_pd(data + even_index);
const __m256d odd = _mm256_loadu_pd(data + odd_index);
const __m256d factor = _mm256_loadu_pd(table + factor_index);
const __m256d rotated = mul_pair(odd, factor);
_mm256_storeu_pd(data + even_index, _mm256_add_pd(even, rotated));
_mm256_storeu_pd(data + odd_index, _mm256_sub_pd(even, rotated));
}
}
}
}
// QMF synthesis basis, four ranks of one band per vector.
//
// The four ranks are four dot products over the same 128 taps, so they are four
// independent accumulators and nothing has to be reassociated to fill a vector.
// Each lane performs exactly the caller's `sum += values[tap] * weight`, in tap
// order, with a separate multiply and add, and the rank-minor table makes the
// four weights one contiguous load.
//
// A band's chain is 128 dependent adds, so the loop is latency-bound long before
// it is throughput-bound; the cure is more independent chains, not a shorter
// chain (that would reassociate). `Rows` output rows share one weight load and
// run their chains side by side -- the caller's rows are consecutive output
// channels, which do read the same weights.
template <int Rows>
void synthesize_rows(const double* values, const double* basis, double* out) noexcept {
constexpr std::size_t kRanks = kSynthesisRanks;
constexpr std::size_t kTaps = kSynthesisTaps;
constexpr std::size_t kRowOut = kSynthesisBands * kRanks;
for (std::size_t band = 0u; band < kSynthesisBands; ++band) {
const double* table = basis + band * kTaps * kRanks;
__m256d sum[Rows];
for (int row = 0; row < Rows; ++row) {
sum[row] = _mm256_setzero_pd();
}
for (std::size_t tap = 0u; tap < kTaps; ++tap) {
const __m256d weight = _mm256_loadu_pd(table + tap * kRanks);
for (int row = 0; row < Rows; ++row) {
sum[row] = _mm256_add_pd(
sum[row],
_mm256_mul_pd(_mm256_broadcast_sd(values + row * kTaps + tap), weight));
}
}
for (int row = 0; row < Rows; ++row) {
_mm256_storeu_pd(out + row * kRowOut + band * kRanks, sum[row]);
}
}
}
void qmf_synthesis_basis_avx2(const double* values, const double* basis, double* out,
std::size_t rows) noexcept {
constexpr std::size_t kTaps = kSynthesisTaps;
constexpr std::size_t kRowOut = kSynthesisBands * kSynthesisRanks;
std::size_t row = 0u;
for (; row + 4u <= rows; row += 4u) {
synthesize_rows<4>(values + row * kTaps, basis, out + row * kRowOut);
}
for (; row < rows; ++row) {
synthesize_rows<1>(values + row * kTaps, basis, out + row * kRowOut);
}
}
// Hybrid analysis low join, one term against all 32 outputs.
//
// The 32 outputs of a row are independent accumulations over the same 78 terms,
// so they are what fills the four vectors: one broadcast of the term's value,
// then a multiply and an add per vector. Every lane keeps the term order and the
// two roundings of the caller's `out += value * weight`. Terms that are exactly
// zero are skipped, which is what the caller does and what leaves its sums
// unchanged (a lane's running sum is never a negative zero).
void hybrid_low_join_avx2(const double* values, const double* kernel, double* out,
std::size_t rows) noexcept {
constexpr std::size_t kTerms = kHybridTerms;
constexpr std::size_t kOutputs = kHybridOutputs;
static_assert(kOutputs == 32u, "eight 256-bit accumulators cover one row's outputs");
for (std::size_t row = 0u; row < rows; ++row) {
const double* source = values + row * kTerms;
double* destination = out + row * kOutputs;
__m256d sum0 = _mm256_setzero_pd();
__m256d sum1 = _mm256_setzero_pd();
__m256d sum2 = _mm256_setzero_pd();
__m256d sum3 = _mm256_setzero_pd();
__m256d sum4 = _mm256_setzero_pd();
__m256d sum5 = _mm256_setzero_pd();
__m256d sum6 = _mm256_setzero_pd();
__m256d sum7 = _mm256_setzero_pd();
for (std::size_t term = 0u; term < kTerms; ++term) {
const double value = source[term];
if (value == 0.0) {
continue;
}
const double* weights = kernel + term * kOutputs;
const __m256d factor = _mm256_broadcast_sd(&value);
sum0 = _mm256_add_pd(sum0, _mm256_mul_pd(factor, _mm256_loadu_pd(weights)));
sum1 = _mm256_add_pd(sum1, _mm256_mul_pd(factor, _mm256_loadu_pd(weights + 4u)));
sum2 = _mm256_add_pd(sum2, _mm256_mul_pd(factor, _mm256_loadu_pd(weights + 8u)));
sum3 = _mm256_add_pd(sum3, _mm256_mul_pd(factor, _mm256_loadu_pd(weights + 12u)));
sum4 = _mm256_add_pd(sum4, _mm256_mul_pd(factor, _mm256_loadu_pd(weights + 16u)));
sum5 = _mm256_add_pd(sum5, _mm256_mul_pd(factor, _mm256_loadu_pd(weights + 20u)));
sum6 = _mm256_add_pd(sum6, _mm256_mul_pd(factor, _mm256_loadu_pd(weights + 24u)));
sum7 = _mm256_add_pd(sum7, _mm256_mul_pd(factor, _mm256_loadu_pd(weights + 28u)));
}
_mm256_storeu_pd(destination, sum0);
_mm256_storeu_pd(destination + 4u, sum1);
_mm256_storeu_pd(destination + 8u, sum2);
_mm256_storeu_pd(destination + 12u, sum3);
_mm256_storeu_pd(destination + 16u, sum4);
_mm256_storeu_pd(destination + 20u, sum5);
_mm256_storeu_pd(destination + 24u, sum6);
_mm256_storeu_pd(destination + 28u, sum7);
}
}
// One rendered path, two bands per vector.
//
// A band's real and imaginary parts are adjacent, so a vector holds two whole
// bands and the cross terms are one swap away -- the same shape the FFT kernel
// uses. Two products are needed per component here (`h * t` and `h * swapped(t)`),
// each scaled on its own, and the real part is their difference while the
// imaginary part is their sum: addsub produces both pairings and a blend keeps
// one lane of each. Bands are independent accumulations into independent
// outputs, so this is still nothing but cross-band parallelism.
void render_hybrid_path_avx2(double* out, const double* transfer, const double* history0,
const double* history1, double scale) noexcept {
const __m256d factor = _mm256_set1_pd(scale);
const double* histories[2] = {history0, history1};
for (std::size_t ear = 0u; ear < 2u; ++ear) {
const double* source = histories[ear];
const double* taps = transfer + ear * kPathBands * 2u;
double* destination = out + ear * kPathBands * 2u;
std::size_t band = 0u;
for (; band + 2u <= kPathBands; band += 2u) {
const __m256d history = _mm256_loadu_pd(source + band * 2u);
const __m256d tap = _mm256_loadu_pd(taps + band * 2u);
const __m256d product = _mm256_mul_pd(_mm256_mul_pd(history, tap), factor);
const __m256d crossed = _mm256_mul_pd(
_mm256_mul_pd(history, _mm256_permute_pd(tap, 0x05)), factor);
const __m256d difference =
_mm256_addsub_pd(product, _mm256_permute_pd(product, 0x05));
const __m256d sum = _mm256_add_pd(crossed, _mm256_permute_pd(crossed, 0x05));
// `difference` carries the real parts in lanes 0 and 2, `sum` the
// imaginary ones in lanes 1 and 3 (four elements, so the blend takes a
// four-bit selector).
const __m256d result = _mm256_blend_pd(difference, sum, 0xa);
_mm256_storeu_pd(destination + band * 2u,
_mm256_add_pd(_mm256_loadu_pd(destination + band * 2u), result));
}
for (; band < kPathBands; ++band) {
const double source_real = source[band * 2u];
const double source_imag = source[band * 2u + 1u];
const double tap_real = taps[band * 2u];
const double tap_imag = taps[band * 2u + 1u];
destination[band * 2u] +=
source_real * tap_real * scale - source_imag * tap_imag * scale;
destination[band * 2u + 1u] +=
source_real * tap_imag * scale + source_imag * tap_real * scale;
}
}
}
// Scalar times a run of interleaved complexes, accumulated in place. The elements
// are independent, so two complexes (one 256-bit vector) go together and each
// component keeps the caller's own multiply-then-add.
void complex_axpy_avx2(double* out, const double* field, double scale,
std::size_t count) noexcept {
const __m256d factor = _mm256_set1_pd(scale);
std::size_t index = 0u;
for (; index + 2u <= count; index += 2u) {
const std::size_t offset = index * 2u;
_mm256_storeu_pd(out + offset,
_mm256_add_pd(_mm256_loadu_pd(out + offset),
_mm256_mul_pd(_mm256_loadu_pd(field + offset), factor)));
}
for (; index < count; ++index) {
out[index * 2u] += field[index * 2u] * scale;
out[index * 2u + 1u] += field[index * 2u + 1u] * scale;
}
}
// QMF analysis polyphase accumulate: the 64 bands of a row are 64 independent
// accumulations of one product each, and the coefficient row is reused by every
// row, so the rows stream past a first-level-cache-resident set of coefficients.
void qmf_analysis_taps_avx2(double* out, const double* source, const double* coefficients,
std::size_t rows) noexcept {
for (std::size_t row = 0u; row < rows; ++row) {
double* destination = out + row * kQmfAnalysisBands;
const double* values = source + row * kQmfAnalysisBands;
for (std::size_t band = 0u; band < kQmfAnalysisBands; band += 4u) {
_mm256_storeu_pd(destination + band,
_mm256_add_pd(_mm256_loadu_pd(destination + band),
_mm256_mul_pd(_mm256_loadu_pd(values + band),
_mm256_loadu_pd(coefficients + band))));
}
}
}
// Element-wise complex product of two runs, two complexes per vector. Both
// operands take part in every lane, so both products of the scalar formula are
// formed and addsub pairs the real difference with the imaginary sum: the same
// four multiplies and the same two roundings per component as `a * b`.
void complex_multiply_avx2(double* out, const double* left, const double* right,
std::size_t count) noexcept {
std::size_t index = 0u;
for (; index + 2u <= count; index += 2u) {
const std::size_t offset = index * 2u;
const __m256d first = _mm256_loadu_pd(left + offset);
const __m256d second = _mm256_loadu_pd(right + offset);
// (re*re, im*im) and (im*re, re*im): the first gives the real difference,
// the second the imaginary sum.
const __m256d product = _mm256_mul_pd(first, second);
const __m256d crossed = _mm256_mul_pd(_mm256_permute_pd(first, 0x05), second);
const __m256d difference =
_mm256_addsub_pd(product, _mm256_permute_pd(product, 0x05));
const __m256d total = _mm256_add_pd(crossed, _mm256_permute_pd(crossed, 0x05));
_mm256_storeu_pd(out + offset, _mm256_blend_pd(difference, total, 0xa));
}
for (; index < count; ++index) {
const std::size_t offset = index * 2u;
const double left_real = left[offset];
const double left_imag = left[offset + 1u];
const double right_real = right[offset];
const double right_imag = right[offset + 1u];
out[offset] = left_real * right_real - left_imag * right_imag;
out[offset + 1u] = left_real * right_imag + left_imag * right_real;
}
}
const Kernels kAvx2{
&fft_butterflies_avx2,
&qmf_synthesis_basis_avx2,
&hybrid_low_join_avx2,
&render_hybrid_path_avx2,
&complex_axpy_avx2,
&qmf_analysis_taps_avx2,
&complex_multiply_avx2,
};
} // namespace
const Kernels& kernels_intrin_avx2() noexcept { return kAvx2; }
} // namespace joc::simd
+272
View File
@@ -0,0 +1,272 @@
// AVX-512F implementation of the dispatched kernels. Compiled with /arch:AVX512
// (or -mavx512f) and only ever called after the dispatcher has confirmed AVX-512F
// *and* that the OS saves the opmask/ZMM state.
//
// CMake gives exactly the `kernels_intrin_*.cpp` units a wider flag, so no
// baseline unit can inherit one by accident.
//
// Nothing in this file may have dynamic initialisation: it is linked into the
// same image as the baseline code and would run before the dispatcher.
#include "simd/simd.h"
#include <immintrin.h>
#include <cstddef>
namespace joc::simd {
namespace {
// Four interleaved complexes per vector: a 512-bit register is four 128-bit
// lanes, each holding one (re, im) pair, so the AVX2 argument applies one lane
// wider -- movedup/permute broadcast the twiddle components inside every lane.
// AVX-512 never widened VADDSUBPD to 512 bits, so the real lanes take the
// difference and the imaginary ones the sum through merge masking: that is still
// one exact subtract and one exact add per output, with the same two roundings
// per component and no reassociation.
inline __m512d mul_quad(__m512d odd, __m512d twiddle) noexcept {
const __m512d real = _mm512_mul_pd(odd, _mm512_movedup_pd(twiddle));
const __m512d cross =
_mm512_mul_pd(_mm512_permute_pd(odd, 0x55), _mm512_permute_pd(twiddle, 0xff));
const __m512d upper = _mm512_mask_add_pd(real, 0xaa, real, cross);
return _mm512_mask_sub_pd(upper, 0x55, real, cross);
}
// Stages 0 and 1 hold fewer than four complexes in a group, so their butterflies
// are packed from neighbouring groups instead; the factor depends only on the
// offset inside a group, so one broadcast serves every packed lane.
//
// VSHUFF64X2 takes a two-bit selector per output lane, but its first two output
// lanes can only name lanes of its first operand and its last two only lanes of
// its second (verified against the instruction). These are the immediates the
// packings need, written as the four lane selections they spell out.
constexpr int kHalvesEven = 0x88; // (low.l0, low.l2, high.l0, high.l2) = x0,x2,x4,x6
constexpr int kHalvesOdd = 0xdd; // (low.l1, low.l3, high.l1, high.l3) = x1,x3,x5,x7
constexpr int kGroupsEven = 0x44; // (low.l0, low.l1, high.l0, high.l1)
constexpr int kGroupsOdd = 0xee; // (low.l2, low.l3, high.l2, high.l3)
constexpr int kSwapMiddle = 0xd8; // (a.l0, a.l2, a.l1, a.l3)
void pack_halves(double* buffer, __m512d factor) noexcept {
const __m512d low = _mm512_loadu_pd(buffer);
const __m512d high = _mm512_loadu_pd(buffer + 8u);
const __m512d even = _mm512_shuffle_f64x2(low, high, kHalvesEven);
const __m512d odd = _mm512_shuffle_f64x2(low, high, kHalvesOdd);
const __m512d rotated = mul_quad(odd, factor);
const __m512d sum = _mm512_add_pd(even, rotated);
const __m512d difference = _mm512_sub_pd(even, rotated);
// `sum` holds the results for the even positions of the four packed groups and
// `difference` those for the odd ones, and the buffer wants them interleaved
// again. One shuffle cannot do that, so each store pairs the two halves and
// then swaps the two middle lanes of the pair.
const __m512d pairs_low = _mm512_shuffle_f64x2(sum, difference, kGroupsEven);
const __m512d pairs_high = _mm512_shuffle_f64x2(sum, difference, kGroupsOdd);
_mm512_storeu_pd(buffer, _mm512_shuffle_f64x2(pairs_low, pairs_low, kSwapMiddle));
_mm512_storeu_pd(buffer + 8u, _mm512_shuffle_f64x2(pairs_high, pairs_high, kSwapMiddle));
}
void pack_groups(double* buffer, __m512d factor) noexcept {
const __m512d low = _mm512_loadu_pd(buffer);
const __m512d high = _mm512_loadu_pd(buffer + 8u);
const __m512d even = _mm512_shuffle_f64x2(low, high, kGroupsEven);
const __m512d odd = _mm512_shuffle_f64x2(low, high, kGroupsOdd);
const __m512d rotated = mul_quad(odd, factor);
const __m512d sum = _mm512_add_pd(even, rotated);
const __m512d difference = _mm512_sub_pd(even, rotated);
_mm512_storeu_pd(buffer, _mm512_shuffle_f64x2(sum, difference, kGroupsEven));
_mm512_storeu_pd(buffer + 8u, _mm512_shuffle_f64x2(sum, difference, kGroupsOdd));
}
void fft_butterflies_avx512(double* buffer, std::size_t size, const double* twiddle,
const std::size_t* stage_begin) noexcept {
// Stage 0: length 2, one butterfly per group, one factor for all of them.
{
const __m128d pair = _mm_loadu_pd(twiddle + 2u * stage_begin[0]);
const __m512d factor = _mm512_broadcast_f64x4(_mm256_broadcast_pd(&pair));
for (std::size_t index = 0u; index < 2u * size; index += 16u) {
pack_halves(buffer + index, factor);
}
}
// Stage 1: length 4, two butterflies per group and two factors.
{
const __m512d factor =
_mm512_broadcast_f64x4(_mm256_loadu_pd(twiddle + 2u * stage_begin[1]));
for (std::size_t index = 0u; index < 2u * size; index += 16u) {
pack_groups(buffer + index, factor);
}
}
std::size_t stage = 2u;
for (std::size_t half = 4u; half < size; half <<= 1u, ++stage) {
const std::size_t length = half << 1u;
const double* table = twiddle + 2u * stage_begin[stage];
for (std::size_t start = 0u; start < size; start += length) {
for (std::size_t offset = 0u; offset < half; offset += 4u) {
const std::size_t even_index = (start + offset) * 2u;
const std::size_t odd_index = even_index + half * 2u;
const std::size_t factor_index = offset * 2u;
const __m512d even = _mm512_loadu_pd(buffer + even_index);
const __m512d odd = _mm512_loadu_pd(buffer + odd_index);
const __m512d factor = _mm512_loadu_pd(table + factor_index);
const __m512d rotated = mul_quad(odd, factor);
_mm512_storeu_pd(buffer + even_index, _mm512_add_pd(even, rotated));
_mm512_storeu_pd(buffer + odd_index, _mm512_sub_pd(even, rotated));
}
}
}
}
// Scalar times a run of interleaved complexes: four complexes (eight doubles) per
// vector, every element an independent accumulation.
void complex_axpy_avx512(double* out, const double* field, double scale,
std::size_t count) noexcept {
const __m512d factor = _mm512_set1_pd(scale);
std::size_t index = 0u;
for (; index + 4u <= count; index += 4u) {
const std::size_t offset = index * 2u;
_mm512_storeu_pd(out + offset,
_mm512_add_pd(_mm512_loadu_pd(out + offset),
_mm512_mul_pd(_mm512_loadu_pd(field + offset), factor)));
}
for (; index < count; ++index) {
out[index * 2u] += field[index * 2u] * scale;
out[index * 2u + 1u] += field[index * 2u + 1u] * scale;
}
}
// QMF analysis polyphase accumulate, eight bands per vector.
void qmf_analysis_taps_avx512(double* out, const double* source, const double* coefficients,
std::size_t rows) noexcept {
for (std::size_t row = 0u; row < rows; ++row) {
double* destination = out + row * kQmfAnalysisBands;
const double* values = source + row * kQmfAnalysisBands;
for (std::size_t band = 0u; band < kQmfAnalysisBands; band += 8u) {
_mm512_storeu_pd(destination + band,
_mm512_add_pd(_mm512_loadu_pd(destination + band),
_mm512_mul_pd(_mm512_loadu_pd(values + band),
_mm512_loadu_pd(coefficients + band))));
}
}
}
// Hybrid analysis low join: 32 outputs are exactly four 512-bit vectors, so one
// broadcast of the term's value feeds four multiplies and four adds.
void hybrid_low_join_avx512(const double* values, const double* kernel, double* out,
std::size_t rows) noexcept {
constexpr std::size_t kTerms = kHybridTerms;
constexpr std::size_t kOutputs = kHybridOutputs;
static_assert(kOutputs == 32u, "four 512-bit accumulators cover one row");
for (std::size_t row = 0u; row < rows; ++row) {
const double* source = values + row * kTerms;
double* destination = out + row * kOutputs;
__m512d sum0 = _mm512_setzero_pd();
__m512d sum1 = _mm512_setzero_pd();
__m512d sum2 = _mm512_setzero_pd();
__m512d sum3 = _mm512_setzero_pd();
for (std::size_t term = 0u; term < kTerms; ++term) {
const double value = source[term];
if (value == 0.0) {
continue;
}
const double* weights = kernel + term * kOutputs;
const __m512d factor = _mm512_set1_pd(value);
sum0 = _mm512_add_pd(sum0, _mm512_mul_pd(factor, _mm512_loadu_pd(weights)));
sum1 = _mm512_add_pd(sum1, _mm512_mul_pd(factor, _mm512_loadu_pd(weights + 8u)));
sum2 = _mm512_add_pd(sum2, _mm512_mul_pd(factor, _mm512_loadu_pd(weights + 16u)));
sum3 = _mm512_add_pd(sum3, _mm512_mul_pd(factor, _mm512_loadu_pd(weights + 24u)));
}
_mm512_storeu_pd(destination, sum0);
_mm512_storeu_pd(destination + 8u, sum1);
_mm512_storeu_pd(destination + 16u, sum2);
_mm512_storeu_pd(destination + 24u, sum3);
}
}
// One rendered path, four bands per vector. AVX-512 has no 512-bit addsub, so the
// two pairings are built with merge masking -- still one exact subtract and one
// exact add per output, and the same separate scale multiply per product.
void render_hybrid_path_avx512(double* out, const double* transfer, const double* history0,
const double* history1, double scale) noexcept {
const __m512d factor = _mm512_set1_pd(scale);
const double* histories[2] = {history0, history1};
for (std::size_t ear = 0u; ear < 2u; ++ear) {
const double* source = histories[ear];
const double* taps = transfer + ear * kPathBands * 2u;
double* destination = out + ear * kPathBands * 2u;
std::size_t band = 0u;
for (; band + 4u <= kPathBands; band += 4u) {
const __m512d history = _mm512_loadu_pd(source + band * 2u);
const __m512d tap = _mm512_loadu_pd(taps + band * 2u);
const __m512d product = _mm512_mul_pd(
_mm512_mul_pd(history, tap), factor);
const __m512d crossed = _mm512_mul_pd(
_mm512_mul_pd(history, _mm512_permute_pd(tap, 0x55)), factor);
const __m512d swapped_product = _mm512_permute_pd(product, 0x55);
const __m512d swapped_crossed = _mm512_permute_pd(crossed, 0x55);
// The real component is the difference of the two products and sits in
// the even element of each complex, the imaginary one is the sum of the
// crossed pair and sits in the odd element -- so the subtract is masked
// to the even lanes and the blend keeps the sum on the odd ones.
const __m512d difference =
_mm512_mask_sub_pd(product, 0x55, product, swapped_product);
const __m512d total = _mm512_add_pd(crossed, swapped_crossed);
const __m512d result = _mm512_mask_blend_pd(0xaa, difference, total);
_mm512_storeu_pd(destination + band * 2u,
_mm512_add_pd(_mm512_loadu_pd(destination + band * 2u), result));
}
for (; band < kPathBands; ++band) {
const double source_real = source[band * 2u];
const double source_imag = source[band * 2u + 1u];
const double tap_real = taps[band * 2u];
const double tap_imag = taps[band * 2u + 1u];
destination[band * 2u] +=
source_real * tap_real * scale - source_imag * tap_imag * scale;
destination[band * 2u + 1u] +=
source_real * tap_imag * scale + source_imag * tap_real * scale;
}
}
}
// Element-wise complex product, four complexes per vector. AVX-512 has no 512-bit
// addsub, so the real difference is masked into the even lanes and the imaginary
// sum into the odd ones.
void complex_multiply_avx512(double* out, const double* left, const double* right,
std::size_t count) noexcept {
std::size_t index = 0u;
for (; index + 4u <= count; index += 4u) {
const std::size_t offset = index * 2u;
const __m512d first = _mm512_loadu_pd(left + offset);
const __m512d second = _mm512_loadu_pd(right + offset);
const __m512d product = _mm512_mul_pd(first, second);
const __m512d crossed = _mm512_mul_pd(_mm512_permute_pd(first, 0x55), second);
const __m512d difference =
_mm512_mask_sub_pd(product, 0x55, product, _mm512_permute_pd(product, 0x55));
const __m512d total = _mm512_add_pd(crossed, _mm512_permute_pd(crossed, 0x55));
_mm512_storeu_pd(out + offset, _mm512_mask_blend_pd(0xaa, difference, total));
}
for (; index < count; ++index) {
const std::size_t offset = index * 2u;
const double left_real = left[offset];
const double left_imag = left[offset + 1u];
const double right_real = right[offset];
const double right_imag = right[offset + 1u];
out[offset] = left_real * right_real - left_imag * right_imag;
out[offset + 1u] = left_real * right_imag + left_imag * right_real;
}
}
const Kernels kAvx512{
&fft_butterflies_avx512,
nullptr, // qmf_synthesis_basis: four ranks do not fill a 512-bit vector
&hybrid_low_join_avx512,
&render_hybrid_path_avx512,
&complex_axpy_avx512,
&qmf_analysis_taps_avx512,
&complex_multiply_avx512,
};
} // namespace
const Kernels& kernels_intrin_avx512() noexcept { return kAvx512; }
} // namespace joc::simd
+311
View File
@@ -0,0 +1,311 @@
// AArch64 NEON (ASIMD) implementation of the dispatched kernels.
//
// CMake gives exactly the `kernels_intrin_*.cpp` units their ISA flag, so no
// baseline unit can inherit one by accident.
//
// Unlike the x86 units this one needs no feature probe -- ASIMD is architectural
// for AArch64 -- but it is still only reached through the dispatcher, so
// JOC_SIMD=scalar, sse2 or neon all stay available on the same binary.
//
// A NEON register holds two doubles, so a vector carries two lanes where an AVX2
// vector carries four. The lanes are the same independent work items as in the
// AVX2 unit, and each lane performs exactly the scalar reference's operations in
// the scalar reference's order: separate multiplies and adds, never a fused
// multiply-add, and never a reassociated sum.
//
// Nothing in this file may have dynamic initialisation (it is linked into the
// same image as the baseline code and would run before the dispatcher).
//
// NOTE: the unit tests do not exercise this unit. It is built by the AArch64 CI
// job, and its bit-exactness is argued from the shared kernel contract (simd.h)
// rather than measured here.
#include "simd/simd.h"
#include <arm_neon.h>
#include <cstddef>
#include "foundation/fft.h"
namespace joc::simd {
namespace {
constexpr std::size_t kFftSize = dsp::kQmfFftSize;
static_assert(kFftSize == 128u, "the QMF transform is the 128-point case");
// ------------------------------------------------------------ radix-2 FFT ----
//
// Two butterflies per iteration, held as separate real/imaginary vectors: vld2q
// loads two interleaved complexes and deinterleaves them, vst2q puts them back.
// `sum = even + odd * w` and `difference = even - odd * w` are then two add/sub
// pairs, and the complex product is four multiplies and two add/subs, which is
// what the scalar kernel writes.
inline float64x2x2_t complex_mul(float64x2_t odd_real, float64x2_t odd_imag,
float64x2_t weight_real, float64x2_t weight_imag) {
float64x2x2_t result;
result.val[0] = vsubq_f64(vmulq_f64(odd_real, weight_real), vmulq_f64(odd_imag, weight_imag));
result.val[1] = vaddq_f64(vmulq_f64(odd_real, weight_imag), vmulq_f64(odd_imag, weight_real));
return result;
}
// Stages 0 and 1 hold fewer than two complexes per group, so their butterflies are
// packed from neighbouring groups; the factor depends only on the offset inside a
// group, so the two lanes share it.
void fft_butterflies_neon(double* buffer, std::size_t size, const double* twiddle,
const std::size_t* stage_begin) noexcept {
// Stage 0: length 2, one butterfly per group. The two lanes take the two
// halves of two neighbouring groups, which the transposes select.
{
const double* table = twiddle + 2u * stage_begin[0];
const float64x2_t weight_real = vdupq_n_f64(table[0]);
const float64x2_t weight_imag = vdupq_n_f64(table[1]);
for (std::size_t index = 0u; index < 2u * size; index += 8u) {
const float64x2_t first = vld1q_f64(buffer + index);
const float64x2_t second = vld1q_f64(buffer + index + 2u);
const float64x2_t third = vld1q_f64(buffer + index + 4u);
const float64x2_t fourth = vld1q_f64(buffer + index + 6u);
const float64x2_t even_real = vtrn1q_f64(first, third);
const float64x2_t even_imag = vtrn2q_f64(first, third);
const float64x2_t odd_real = vtrn1q_f64(second, fourth);
const float64x2_t odd_imag = vtrn2q_f64(second, fourth);
const float64x2x2_t rotated =
complex_mul(odd_real, odd_imag, weight_real, weight_imag);
const float64x2_t sum_real = vaddq_f64(even_real, rotated.val[0]);
const float64x2_t sum_imag = vaddq_f64(even_imag, rotated.val[1]);
const float64x2_t difference_real = vsubq_f64(even_real, rotated.val[0]);
const float64x2_t difference_imag = vsubq_f64(even_imag, rotated.val[1]);
// sum.* are the two groups' even-position results and difference.* their
// odd-position ones; a transpose of the two component vectors puts each
// complex back together, in the order the groups appear.
vst1q_f64(buffer + index, vtrn1q_f64(sum_real, sum_imag));
vst1q_f64(buffer + index + 2u, vtrn1q_f64(difference_real, difference_imag));
vst1q_f64(buffer + index + 4u, vtrn2q_f64(sum_real, sum_imag));
vst1q_f64(buffer + index + 6u, vtrn2q_f64(difference_real, difference_imag));
}
}
std::size_t stage = 1u;
for (std::size_t half = 2u; half < size; half <<= 1u, ++stage) {
const std::size_t length = half << 1u;
const double* table = twiddle + 2u * stage_begin[stage];
for (std::size_t start = 0u; start < size; start += length) {
for (std::size_t offset = 0u; offset < half; offset += 2u) {
double* even_values = buffer + (start + offset) * 2u;
double* odd_values = even_values + half * 2u;
const float64x2x2_t even = vld2q_f64(even_values);
const float64x2x2_t odd = vld2q_f64(odd_values);
const float64x2x2_t weight = vld2q_f64(table + offset * 2u);
const float64x2x2_t rotated =
complex_mul(odd.val[0], odd.val[1], weight.val[0], weight.val[1]);
float64x2x2_t sum;
float64x2x2_t difference;
sum.val[0] = vaddq_f64(even.val[0], rotated.val[0]);
sum.val[1] = vaddq_f64(even.val[1], rotated.val[1]);
difference.val[0] = vsubq_f64(even.val[0], rotated.val[0]);
difference.val[1] = vsubq_f64(even.val[1], rotated.val[1]);
vst2q_f64(even_values, sum);
vst2q_f64(odd_values, difference);
}
}
}
}
// --------------------------------------------------- QMF synthesis basis -----
//
// The four ranks of a band are four independent dot products over the same row, so
// two registers carry four lanes. `Rows` rows run side by side because a band's
// chain is 128 dependent adds: the cure is more independent chains, not a shorter
// chain.
template <int Rows>
void synthesize_rows_neon(const double* values, const double* basis, double* out) noexcept {
constexpr std::size_t kRanks = kSynthesisRanks;
constexpr std::size_t kTaps = kSynthesisTaps;
constexpr std::size_t kRowOut = kSynthesisBands * kRanks;
for (std::size_t band = 0u; band < kSynthesisBands; ++band) {
const double* table = basis + band * kTaps * kRanks;
float64x2_t low[Rows];
float64x2_t high[Rows];
for (int row = 0; row < Rows; ++row) {
low[row] = vdupq_n_f64(0.0);
high[row] = vdupq_n_f64(0.0);
}
for (std::size_t tap = 0u; tap < kTaps; ++tap) {
const float64x2_t weight_low = vld1q_f64(table + tap * kRanks);
const float64x2_t weight_high = vld1q_f64(table + tap * kRanks + 2u);
for (int row = 0; row < Rows; ++row) {
const float64x2_t factor = vdupq_n_f64(values[row * kTaps + tap]);
low[row] = vaddq_f64(low[row], vmulq_f64(factor, weight_low));
high[row] = vaddq_f64(high[row], vmulq_f64(factor, weight_high));
}
}
for (int row = 0; row < Rows; ++row) {
vst1q_f64(out + row * kRowOut + band * kRanks, low[row]);
vst1q_f64(out + row * kRowOut + band * kRanks + 2u, high[row]);
}
}
}
void qmf_synthesis_basis_neon(const double* values, const double* basis, double* out,
std::size_t rows) noexcept {
constexpr std::size_t kTaps = kSynthesisTaps;
constexpr std::size_t kRowOut = kSynthesisBands * kSynthesisRanks;
std::size_t row = 0u;
for (; row + 4u <= rows; row += 4u) {
synthesize_rows_neon<4>(values + row * kTaps, basis, out + row * kRowOut);
}
for (; row < rows; ++row) {
synthesize_rows_neon<1>(values + row * kTaps, basis, out + row * kRowOut);
}
}
// ------------------------------------------------------ Hybrid low join -----
//
// One term against all 32 outputs, two outputs per register: the outputs are
// independent accumulations over the same terms, which is what the lanes carry.
void hybrid_low_join_neon(const double* values, const double* kernel, double* out,
std::size_t rows) noexcept {
constexpr std::size_t kTerms = kHybridTerms;
constexpr std::size_t kOutputs = kHybridOutputs;
static_assert(kOutputs == 32u, "sixteen 128-bit accumulators cover one row");
for (std::size_t row = 0u; row < rows; ++row) {
const double* source = values + row * kTerms;
double* destination = out + row * kOutputs;
float64x2_t partial[16];
for (int chunk = 0; chunk < 16; ++chunk) {
partial[chunk] = vdupq_n_f64(0.0);
}
for (std::size_t term = 0u; term < kTerms; ++term) {
const double value = source[term];
if (value == 0.0) {
continue;
}
const double* weights = kernel + term * kOutputs;
const float64x2_t factor = vdupq_n_f64(value);
for (int chunk = 0; chunk < 16; ++chunk) {
partial[chunk] = vaddq_f64(
partial[chunk], vmulq_f64(factor, vld1q_f64(weights + chunk * 2)));
}
}
for (int chunk = 0; chunk < 16; ++chunk) {
vst1q_f64(destination + chunk * 2, partial[chunk]);
}
}
}
// --------------------------------------------------- Rendered hybrid path ---
//
// Two bands per iteration, real and imaginary held apart and reloaded from memory
// in the caller's own layout: `out += h * t * scale` for the real part and
// `out += h * swapped(t) * scale` for the imaginary one, with the scale applied to
// each product separately.
void render_hybrid_path_neon(double* out, const double* transfer, const double* history0,
const double* history1, double scale) noexcept {
const float64x2_t factor = vdupq_n_f64(scale);
const double* histories[2] = {history0, history1};
for (std::size_t ear = 0u; ear < 2u; ++ear) {
const double* source = histories[ear];
const double* taps = transfer + ear * kPathBands * 2u;
double* destination = out + ear * kPathBands * 2u;
std::size_t band = 0u;
for (; band + 2u <= kPathBands; band += 2u) {
const float64x2x2_t history = vld2q_f64(source + band * 2u);
const float64x2x2_t tap = vld2q_f64(taps + band * 2u);
float64x2x2_t output = vld2q_f64(destination + band * 2u);
// The caller scales each product on its own before combining, so the
// scale is a separate multiply of every product -- not a multiply of the
// difference and the sum, which would round differently.
const float64x2_t real = vsubq_f64(
vmulq_f64(vmulq_f64(history.val[0], tap.val[0]), factor),
vmulq_f64(vmulq_f64(history.val[1], tap.val[1]), factor));
const float64x2_t imaginary = vaddq_f64(
vmulq_f64(vmulq_f64(history.val[0], tap.val[1]), factor),
vmulq_f64(vmulq_f64(history.val[1], tap.val[0]), factor));
output.val[0] = vaddq_f64(output.val[0], real);
output.val[1] = vaddq_f64(output.val[1], imaginary);
vst2q_f64(destination + band * 2u, output);
}
for (; band < kPathBands; ++band) {
const double source_real = source[band * 2u];
const double source_imag = source[band * 2u + 1u];
const double tap_real = taps[band * 2u];
const double tap_imag = taps[band * 2u + 1u];
destination[band * 2u] +=
source_real * tap_real * scale - source_imag * tap_imag * scale;
destination[band * 2u + 1u] +=
source_real * tap_imag * scale + source_imag * tap_real * scale;
}
}
}
// Scalar times a run of interleaved complexes, two complexes per iteration.
void complex_axpy_neon(double* out, const double* field, double scale,
std::size_t count) noexcept {
const float64x2_t factor = vdupq_n_f64(scale);
std::size_t index = 0u;
for (; index + 2u <= count; index += 2u) {
const float64x2_t values = vld1q_f64(field + index * 2u);
const float64x2_t accumulated = vld1q_f64(out + index * 2u);
vst1q_f64(out + index * 2u, vaddq_f64(accumulated, vmulq_f64(values, factor)));
}
for (; index < count; ++index) {
out[index * 2u] += field[index * 2u] * scale;
out[index * 2u + 1u] += field[index * 2u + 1u] * scale;
}
}
// QMF analysis polyphase accumulate, two bands per iteration.
void qmf_analysis_taps_neon(double* out, const double* source, const double* coefficients,
std::size_t rows) noexcept {
for (std::size_t row = 0u; row < rows; ++row) {
double* destination = out + row * kQmfAnalysisBands;
const double* values = source + row * kQmfAnalysisBands;
for (std::size_t band = 0u; band < kQmfAnalysisBands; band += 2u) {
vst1q_f64(destination + band,
vaddq_f64(vld1q_f64(destination + band),
vmulq_f64(vld1q_f64(values + band),
vld1q_f64(coefficients + band))));
}
}
}
// Element-wise complex product of two runs, two complexes per iteration.
void complex_multiply_neon(double* out, const double* left, const double* right,
std::size_t count) noexcept {
std::size_t index = 0u;
for (; index + 2u <= count; index += 2u) {
const float64x2x2_t first = vld2q_f64(left + index * 2u);
const float64x2x2_t second = vld2q_f64(right + index * 2u);
float64x2x2_t result;
result.val[0] = vsubq_f64(vmulq_f64(first.val[0], second.val[0]),
vmulq_f64(first.val[1], second.val[1]));
result.val[1] = vaddq_f64(vmulq_f64(first.val[0], second.val[1]),
vmulq_f64(first.val[1], second.val[0]));
vst2q_f64(out + index * 2u, result);
}
for (; index < count; ++index) {
const std::size_t offset = index * 2u;
const double left_real = left[offset];
const double left_imag = left[offset + 1u];
const double right_real = right[offset];
const double right_imag = right[offset + 1u];
out[offset] = left_real * right_real - left_imag * right_imag;
out[offset + 1u] = left_real * right_imag + left_imag * right_real;
}
}
const Kernels kNeon{
&fft_butterflies_neon,
&qmf_synthesis_basis_neon,
&hybrid_low_join_neon,
&render_hybrid_path_neon,
&complex_axpy_neon,
&qmf_analysis_taps_neon,
&complex_multiply_neon,
};
} // namespace
const Kernels& kernels_intrin_neon() noexcept { return kNeon; }
} // namespace joc::simd
+156
View File
@@ -0,0 +1,156 @@
// Scalar reference implementations of the dispatched DSP kernels.
//
// This unit is compiled with the baseline ISA of the target (no /arch flag on
// MSVC, no -m flag elsewhere) and is the ground truth every vector path is
// compared against: JOC_SIMD=scalar selects exactly these functions, and their
// output must be bit-identical to any ISA path's.
//
// The arithmetic below is deliberately written with std::complex<double>, which
// on MSVC expands to the plain `re * rr - im * ri` / `re * ri + im * rr` pair
// (no helper call, no scaling trick), so the vector paths have an unambiguous
// two-roundings-per-component target to reproduce.
#include "simd/simd.h"
#include <complex>
#include "foundation/fft.h"
namespace joc::simd {
namespace {
using Complex = dsp::Complex;
// Exactly the stage loop of FftPlan::apply, over a table this kernel does not own,
// for any power-of-two size. The bit-reversal permutation stays with the caller:
// both call sites already write their input in permuted order.
void fft_butterflies_scalar(double* raw_data, std::size_t size, const double* raw_twiddle,
const std::size_t* stage_begin) noexcept {
auto* data = reinterpret_cast<Complex*>(raw_data);
const auto* twiddle = reinterpret_cast<const Complex*>(raw_twiddle);
std::size_t stage = 0u;
for (std::size_t length = 2u; length <= size; length <<= 1u, ++stage) {
const Complex* table = twiddle + stage_begin[stage];
for (std::size_t start = 0u; start < size; start += length) {
for (std::size_t offset = 0u; offset < length / 2u; ++offset) {
const Complex even = data[start + offset];
const Complex odd = data[start + offset + length / 2u] * table[offset];
data[start + offset] = even + odd;
data[start + offset + length / 2u] = even - odd;
}
}
}
}
// The reference for the synthesis basis: one accumulator per output, taps in
// increasing order, one rounding per multiply and per add. This is the shape
// both callers write, with the basis rows reordered rank-minor -- the products
// and their order are the caller's.
void qmf_synthesis_basis_scalar(const double* values, const double* basis, double* out,
std::size_t rows) noexcept {
for (std::size_t row = 0u; row < rows; ++row) {
const double* source = values + row * kSynthesisTaps;
double* destination = out + row * kSynthesisBands * kSynthesisRanks;
for (std::size_t band = 0u; band < kSynthesisBands; ++band) {
const double* table = basis + band * kSynthesisTaps * kSynthesisRanks;
for (std::size_t rank = 0u; rank < kSynthesisRanks; ++rank) {
double sum = 0.0;
for (std::size_t tap = 0u; tap < kSynthesisTaps; ++tap) {
sum += source[tap] * table[tap * kSynthesisRanks + rank];
}
destination[band * kSynthesisRanks + rank] = sum;
}
}
}
}
// The reference for the hybrid low join: one accumulator per output, terms in the
// caller's order, one rounding per multiply and per add, zero terms skipped.
void hybrid_low_join_scalar(const double* values, const double* kernel, double* out,
std::size_t rows) noexcept {
for (std::size_t row = 0u; row < rows; ++row) {
const double* source = values + row * kHybridTerms;
double* destination = out + row * kHybridOutputs;
for (std::size_t output = 0u; output < kHybridOutputs; ++output) {
destination[output] = 0.0;
}
for (std::size_t term = 0u; term < kHybridTerms; ++term) {
const double value = source[term];
if (value == 0.0) {
continue;
}
const double* weights = kernel + term * kHybridOutputs;
for (std::size_t output = 0u; output < kHybridOutputs; ++output) {
destination[output] += value * weights[output];
}
}
}
}
// The reference for one rendered path: each band of each ear accumulates its own
// value, in the caller's order, with the scale applied to each product.
void render_hybrid_path_scalar(double* out, const double* transfer, const double* history0,
const double* history1, double scale) noexcept {
const double* histories[2] = {history0, history1};
for (std::size_t ear = 0u; ear < 2u; ++ear) {
const double* source = histories[ear];
const double* taps = transfer + ear * kPathBands * 2u;
double* destination = out + ear * kPathBands * 2u;
for (std::size_t band = 0u; band < kPathBands; ++band) {
const double source_real = source[band * 2u];
const double source_imag = source[band * 2u + 1u];
const double tap_real = taps[band * 2u];
const double tap_imag = taps[band * 2u + 1u];
destination[band * 2u] +=
source_real * tap_real * scale - source_imag * tap_imag * scale;
destination[band * 2u + 1u] +=
source_real * tap_imag * scale + source_imag * tap_real * scale;
}
}
}
// Neighbouring complexes and neighbouring bands are independent outputs, so the
// references for the two accumulates are plain element-wise loops.
void complex_axpy_scalar(double* out, const double* field, double scale,
std::size_t count) noexcept {
for (std::size_t index = 0u; index < count * 2u; ++index) {
out[index] += field[index] * scale;
}
}
void qmf_analysis_taps_scalar(double* out, const double* source, const double* coefficients,
std::size_t rows) noexcept {
for (std::size_t row = 0u; row < rows; ++row) {
double* destination = out + row * kQmfAnalysisBands;
const double* values = source + row * kQmfAnalysisBands;
for (std::size_t band = 0u; band < kQmfAnalysisBands; ++band) {
destination[band] += values[band] * coefficients[band];
}
}
}
void complex_multiply_scalar(double* out, const double* left, const double* right,
std::size_t count) noexcept {
auto* destination = reinterpret_cast<Complex*>(out);
const auto* first = reinterpret_cast<const Complex*>(left);
const auto* second = reinterpret_cast<const Complex*>(right);
for (std::size_t index = 0u; index < count; ++index) {
destination[index] = first[index] * second[index];
}
}
const Kernels kScalar{
&fft_butterflies_scalar,
&qmf_synthesis_basis_scalar,
&hybrid_low_join_scalar,
&render_hybrid_path_scalar,
&complex_axpy_scalar,
&qmf_analysis_taps_scalar,
&complex_multiply_scalar,
};
} // namespace
const Kernels& kernels_scalar() noexcept { return kScalar; }
} // namespace joc::simd
+209
View File
@@ -0,0 +1,209 @@
#pragma once
#include <cstddef>
// Runtime-dispatched SIMD kernels for the DSP core.
//
// Module layout (the ISA lives in the file name, never in a subdirectory):
//
// simd.h this contract -- the only header a caller includes
// cpu_probe.{h,cpp} "can this CPU and OS run ISA X", no policy
// dispatch.cpp JOC_SIMD + the ladder + the kernel table
// kernels_scalar.cpp the reference implementation, Isa::scalar
// kernels_intrin_avx2.cpp /arch:AVX2 -mavx2 -- exactly the
// kernels_intrin_avx512.cpp /arch:AVX512 -mavx512f -- `kernels_intrin_*`
// kernels_intrin_neon.cpp AArch64 default -- units get a wider flag
//
// The scalar reference in kernels_scalar.cpp fixes both the data layout and the
// arithmetic: a vector path may only compute *independent* outputs in parallel
// lanes. It may never reassociate an accumulation, contract a multiply into an
// FMA, or change the order in which one output's terms are summed, so every ISA
// path reproduces the scalar bits exactly and JOC_SIMD=scalar is a valid
// reference for any of them. Kernel-level layout changes (AoS -> SoA inside a
// kernel, for instance) are fine as long as each individual output value is
// still produced by the same sequence of roundings.
//
// MSVC has no function-level ISA attribute, so each ISA lives in its own
// translation unit compiled with its own /arch (or -m) flag and the dispatcher
// picks one at run time: CPUID + XGETBV on x86-64, the architectural AArch64
// baseline (NEON/ASIMD needs no probing) elsewhere.
//
// The ISA units are linked into the same binaries as the baseline code, so they
// must stay free of objects with dynamic initialisation: a global constructor
// would execute ISA-specific instructions before the dispatcher has had a chance
// to look at the CPU. Constant tables are fine -- they live in .rdata.
namespace joc::simd {
enum class Isa {
scalar = 0,
sse2,
avx2,
avx512,
neon,
};
// One entry per dispatched kernel.
enum class Kernel {
// Power-of-two radix-2 DIT butterfly cascade, in place, over `size` interleaved
// (re, im) pairs already in bit-reversed order. `twiddle` is the caller's own
// factor table, read as interleaved pairs: stage s (length 2^(s+1)) takes its
// 2^s factors starting at stage_begin[s], a *complex* index into `twiddle`.
fft_butterflies = 0,
// QMF synthesis basis application, see kSynthesis* below.
qmf_synthesis_basis,
// Hybrid analysis low join, see kHybrid* below.
hybrid_low_join,
// Hybrid-domain path rendering, see kPathBands below.
render_hybrid_path,
// Scalar-times-vector accumulate over interleaved complexes.
complex_axpy,
// QMF analysis polyphase accumulate, see kQmfAnalysisBands below.
qmf_analysis_taps,
// Element-wise complex product of two whole runs.
complex_multiply,
count,
};
// Both filterbanks apply the same map: for every output row and every one of the
// 64 bands, four ranks each accumulate 128 taps.
inline constexpr std::size_t kSynthesisBands = 64;
inline constexpr std::size_t kSynthesisRanks = 4;
inline constexpr std::size_t kSynthesisTaps = 128;
// ... and both derive the same 16 low hybrid bands from 78 terms: 3 parents x 2
// components x 13 lags.
inline constexpr std::size_t kHybridTerms = 78;
inline constexpr std::size_t kHybridOutputs = 32;
// Rows the callers stage at a time: enough to keep the kernel busy, small enough
// that the staged values stay in the first-level cache.
inline constexpr std::size_t kHybridJoinBlock = 32;
// Bands in one ear of a rendered path (the SOFA renderer's 77-band hybrid domain).
inline constexpr std::size_t kPathBands = 77;
// The cascade packs whole groups into a vector, so it needs at least one full
// packing: a size of 8 covers a 512-bit stage. Smaller transforms are rare enough
// (and short enough) that the caller's portable loop is the right answer there.
inline constexpr std::size_t kMinVectorFftSize = 8;
// Bands in one QMF analysis row (the polyphase accumulator of both filterbanks).
inline constexpr std::size_t kQmfAnalysisBands = 64;
const char* isa_name(Isa isa) noexcept;
// Whether this CPU *and* the OS state it has to save can execute the ISA. The
// answer never changes, so it is computed once.
bool isa_supported(Isa isa) noexcept;
// The ISA the dispatcher settled on, after applying a JOC_SIMD override. This
// is the widest ISA considered, not necessarily the one that serves every
// kernel: active_isa() answers that per kernel.
Isa selected_isa() noexcept;
// Which ISA actually implements `kernel` after the per-kernel fallback.
Isa active_isa(Kernel kernel) noexcept;
// The dispatched kernels; a slot is never null.
struct Kernels {
void (*fft_butterflies)(double* data, std::size_t size, const double* twiddle,
const std::size_t* stage_begin) noexcept = nullptr;
// values: [rows][kSynthesisTaps] real/imaginary of the 64 bands of one row.
// basis: the same weights the caller holds, reordered rank-minor, so that
// (band, tap) addresses its kSynthesisRanks weights contiguously:
// basis[(band * kSynthesisTaps + tap) * kSynthesisRanks + rank].
// out: [rows][kSynthesisBands][kSynthesisRanks], rank-minor as well.
//
// Each of the four ranks is an independent dot product over the row, so the
// four of them are what shares a vector; every lane keeps the tap order and
// the two roundings of the caller's `sum += values[tap] * weight`.
void (*qmf_synthesis_basis)(const double* values, const double* basis, double* out,
std::size_t rows) noexcept = nullptr;
// Hybrid analysis low join. Every output row accumulates kHybridTerms values
// into kHybridOutputs outputs (16 bands x a real/imaginary pair, component
// minor):
// out[row][output] = sum over term of values[row][term] * kernel[term][output]
// values is [rows][kHybridTerms] in the caller's own term order and kernel is
// the caller's taps regrouped to that same order, so that one term's 32
// weights are contiguous. Terms whose value is exactly zero are skipped, as
// both callers do: adding a zero product to a lane can only leave it alone
// (no lane's running sum can be a negative zero, since it starts at +0 and
// sums without ever producing one).
void (*hybrid_low_join)(const double* values, const double* kernel, double* out,
std::size_t rows) noexcept = nullptr;
// One rendered path. For each ear e and band b, with the ear's history sample
// h and the path's transfer t:
// out[e][b].re += h.re * t[e][b].re * scale - h.im * t[e][b].im * scale
// out[e][b].im += h.re * t[e][b].im * scale + h.im * t[e][b].re * scale
// `history0` and `history1` are the two ears' [kPathBands] interleaved complex
// rows (they come from different places in the history ring); `transfer` and
// `out` are [2][kPathBands] interleaved complexes. The scale multiplies each
// product separately, exactly as the caller writes it, so the bands -- which
// are independent accumulations into independent outputs -- are what the lanes
// carry.
void (*render_hybrid_path)(double* out, const double* transfer, const double* history0,
const double* history1, double scale) noexcept = nullptr;
// `count` interleaved complexes, accumulated in place:
// out[i] += field[i] * scale
// Every element is an independent accumulation of one product, so the lanes
// carry neighbouring elements and each one keeps the caller's multiply-then-add.
void (*complex_axpy)(double* out, const double* field, double scale,
std::size_t count) noexcept = nullptr;
// QMF analysis polyphase accumulate: `rows` rows of kQmfAnalysisBands bands,
// out[row][band] += source[row][band] * coefficients[band]
// Neighbouring bands are neighbouring outputs, so they are what fills a vector;
// each band accumulates its own product once, in the caller's order.
void (*qmf_analysis_taps)(double* out, const double* source, const double* coefficients,
std::size_t rows) noexcept = nullptr;
// `count` interleaved complexes, multiplied element by element:
// out[i] = left[i] * right[i]
// with the caller's `re * re - im * im` and `re * im + im * re`, two roundings
// per component. Neighbouring complexes are independent products.
void (*complex_multiply)(double* out, const double* left, const double* right,
std::size_t count) noexcept = nullptr;
};
const Kernels& kernels() noexcept;
// Convenience wrappers.
inline void fft_butterflies(double* data, std::size_t size, const double* twiddle,
const std::size_t* stage_begin) noexcept {
kernels().fft_butterflies(data, size, twiddle, stage_begin);
}
inline void qmf_synthesis_basis(const double* values, const double* basis, double* out,
std::size_t rows) noexcept {
kernels().qmf_synthesis_basis(values, basis, out, rows);
}
inline void hybrid_low_join(const double* values, const double* kernel, double* out,
std::size_t rows) noexcept {
kernels().hybrid_low_join(values, kernel, out, rows);
}
inline void render_hybrid_path(double* out, const double* transfer, const double* history0,
const double* history1, double scale) noexcept {
kernels().render_hybrid_path(out, transfer, history0, history1, scale);
}
inline void complex_axpy(double* out, const double* field, double scale,
std::size_t count) noexcept {
kernels().complex_axpy(out, field, scale, count);
}
inline void qmf_analysis_taps(double* out, const double* source, const double* coefficients,
std::size_t rows) noexcept {
kernels().qmf_analysis_taps(out, source, coefficients, rows);
}
inline void complex_multiply(double* out, const double* left, const double* right,
std::size_t count) noexcept {
kernels().complex_multiply(out, left, right, count);
}
} // namespace joc::simd
@@ -0,0 +1,71 @@
#include "speaker/speaker_layout_lookup.h"
#include <cstring>
namespace joc::speaker {
namespace {
struct FrozenLayout {
const char* name;
std::uint32_t out_ch_config;
std::uint32_t speaker_bitfield;
};
constexpr FrozenLayout kLayouts[kLayoutCount] = {
{"2.0", 0, 1},
{"3.1", 3, 7},
{"5.1", 7, 15},
{"7.1", 11, 31},
{"5.1.2", 13, 1039},
{"5.1.4", 14, 2575},
{"7.1.2", 15, 1055},
{"7.1.4", 16, 2591},
{"9.1.4", 19, 2719},
{"9.1.6", 20, 3743},
};
void fill(const FrozenLayout& source, LayoutInfo* out) {
out->name = source.name;
out->out_ch_config = source.out_ch_config;
out->speaker_bitfield = source.speaker_bitfield;
out->channel_count = ejoc_speaker_layout_channel_count(source.speaker_bitfield);
}
} // namespace
bool layout_at(int index, LayoutInfo* out) {
if (out == nullptr || index < 0 || index >= kLayoutCount) {
return false;
}
fill(kLayouts[index], out);
return true;
}
bool layout_by_name(const char* name, LayoutInfo* out) {
if (name == nullptr || out == nullptr) {
return false;
}
for (int index = 0; index < kLayoutCount; ++index) {
if (std::strcmp(name, kLayouts[index].name) == 0) {
fill(kLayouts[index], out);
return true;
}
}
return false;
}
bool layout_by_bitfield(std::uint32_t bitfield, LayoutInfo* out) {
if (out == nullptr) {
return false;
}
for (int index = 0; index < kLayoutCount; ++index) {
if (kLayouts[index].speaker_bitfield == bitfield) {
fill(kLayouts[index], out);
return true;
}
}
return false;
}
} // namespace joc::speaker
@@ -0,0 +1,28 @@
#pragma once
#include <cstdint>
#include "eac3joc_core.h"
#include "joc_core.h"
namespace joc::speaker {
inline constexpr int kLayoutCount = 10;
struct LayoutInfo {
const char* name = "";
std::uint32_t out_ch_config = 0;
std::uint32_t speaker_bitfield = 0;
std::uint32_t channel_count = 0;
};
// All ten layouts in the reference's order (2.0, 3.1, 5.1, 7.1, 5.1.2, 5.1.4,
bool layout_at(int index, LayoutInfo* out);
// False when the name is unknown (case-sensitive, exactly as the reference CLI).
bool layout_by_name(const char* name, LayoutInfo* out);
bool layout_by_bitfield(std::uint32_t bitfield, LayoutInfo* out);
} // namespace joc::speaker
+534
View File
@@ -0,0 +1,534 @@
#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
namespace ejoc::speaker_tables {
inline constexpr std::size_t kMaxPoints = 15;
inline constexpr std::size_t kMaxGroups = 4;
inline constexpr std::size_t kMaxGroupSize = 3;
inline constexpr std::size_t kMaxChannels = 16;
struct SpeakerPoint {
std::array<std::uint16_t, 3> coordinate_q15{};
std::uint8_t speaker_id{};
};
struct AxisGroup {
std::uint8_t size{};
std::array<std::uint8_t, kMaxGroupSize> indices{};
};
struct RegionGeometry {
std::uint8_t point_count{};
std::uint8_t mode{};
std::uint8_t axis0_group_count{};
std::uint8_t axis1_group_count{};
std::array<SpeakerPoint, kMaxPoints> points{};
std::array<AxisGroup, kMaxGroups> axis0_groups{};
std::array<AxisGroup, kMaxGroups> axis1_groups{};
};
struct LayoutGeometry {
std::uint32_t speaker_bitfield{};
std::uint8_t out_ch_config{};
std::uint8_t channel_count{};
std::array<std::uint8_t, kMaxChannels> standard_from_internal{};
std::array<RegionGeometry, 7> regions{};
};
inline constexpr std::array<LayoutGeometry, 10> kLayouts{{
LayoutGeometry{
0x1u, 0, 2,
{{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}},
{{
RegionGeometry{
2, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
2, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
2, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
2, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
2, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
2, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
2, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
}
}}
},
LayoutGeometry{
0x7u, 3, 4,
{{0, 1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}},
{{
RegionGeometry{
3, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
3, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
3, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
1, 1, 1, 0,
{{SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{1, {0, 0, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
3, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
3, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
3, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
}
}}
},
LayoutGeometry{
0xFu, 7, 6,
{{0, 1, 2, 3, 4, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}},
{{
RegionGeometry{
5, 2, 2, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
5, 2, 2, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
5, 2, 2, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
3, 2, 2, 0,
{{SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{1, {0, 0, 0}}, AxisGroup{2, {1, 2, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
3, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
2, 1, 1, 0,
{{SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
3, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
}
}}
},
LayoutGeometry{
0x1Fu, 11, 8,
{{0, 1, 2, 3, 6, 7, 4, 5, 0, 0, 0, 0, 0, 0, 0, 0}},
{{
RegionGeometry{
7, 2, 3, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
5, 2, 2, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
5, 2, 2, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
3, 2, 2, 0,
{{SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{1, {0, 0, 0}}, AxisGroup{2, {1, 2, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
3, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
4, 2, 2, 0,
{{SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{2, {2, 3, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
3, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
}
}}
},
LayoutGeometry{
0x40Fu, 13, 8,
{{0, 1, 2, 3, 4, 5, 6, 7, 0, 0, 0, 0, 0, 0, 0, 0}},
{{
RegionGeometry{
7, 3, 2, 1,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{{7928, 16384, 32767}, 6}, SpeakerPoint{{24840, 16384, 32767}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
7, 3, 2, 1,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{{7928, 16384, 32767}, 6}, SpeakerPoint{{24840, 16384, 32767}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
7, 3, 2, 1,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{{7928, 16384, 32767}, 6}, SpeakerPoint{{24840, 16384, 32767}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
5, 3, 2, 1,
{{SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{{7928, 16384, 32767}, 6}, SpeakerPoint{{24840, 16384, 32767}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{1, {0, 0, 0}}, AxisGroup{2, {1, 2, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
5, 3, 1, 1,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{7928, 16384, 32767}, 6}, SpeakerPoint{{24840, 16384, 32767}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
4, 3, 1, 1,
{{SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{{7928, 16384, 32767}, 6}, SpeakerPoint{{24840, 16384, 32767}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {2, 3, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
3, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
}
}}
},
LayoutGeometry{
0xA0Fu, 14, 10,
{{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0}},
{{
RegionGeometry{
9, 3, 2, 2,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{{7928, 7928, 32767}, 6}, SpeakerPoint{{24840, 7928, 32767}, 7}, SpeakerPoint{{7928, 24840, 32767}, 8}, SpeakerPoint{{24840, 24840, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
9, 3, 2, 2,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{{7928, 7928, 32767}, 6}, SpeakerPoint{{24840, 7928, 32767}, 7}, SpeakerPoint{{7928, 24840, 32767}, 8}, SpeakerPoint{{24840, 24840, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
9, 3, 2, 2,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{{7928, 7928, 32767}, 6}, SpeakerPoint{{24840, 7928, 32767}, 7}, SpeakerPoint{{7928, 24840, 32767}, 8}, SpeakerPoint{{24840, 24840, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
7, 3, 2, 2,
{{SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{{7928, 7928, 32767}, 6}, SpeakerPoint{{24840, 7928, 32767}, 7}, SpeakerPoint{{7928, 24840, 32767}, 8}, SpeakerPoint{{24840, 24840, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{1, {0, 0, 0}}, AxisGroup{2, {1, 2, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
7, 3, 1, 2,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{7928, 7928, 32767}, 6}, SpeakerPoint{{24840, 7928, 32767}, 7}, SpeakerPoint{{7928, 24840, 32767}, 8}, SpeakerPoint{{24840, 24840, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
6, 3, 1, 2,
{{SpeakerPoint{{0, 32767, 0}, 4}, SpeakerPoint{{32767, 32767, 0}, 5}, SpeakerPoint{{7928, 7928, 32767}, 6}, SpeakerPoint{{24840, 7928, 32767}, 7}, SpeakerPoint{{7928, 24840, 32767}, 8}, SpeakerPoint{{24840, 24840, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {2, 3, 0}}, AxisGroup{2, {4, 5, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
5, 3, 1, 1,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{7928, 7928, 32767}, 6}, SpeakerPoint{{24840, 7928, 32767}, 7}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
}
}}
},
LayoutGeometry{
0x41Fu, 15, 10,
{{0, 1, 2, 3, 6, 7, 4, 5, 8, 9, 0, 0, 0, 0, 0, 0}},
{{
RegionGeometry{
9, 3, 3, 1,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 16384, 32767}, 8}, SpeakerPoint{{24840, 16384, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{}}},
{{AxisGroup{2, {7, 8, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
7, 3, 2, 1,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{7928, 16384, 32767}, 8}, SpeakerPoint{{24840, 16384, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
7, 3, 2, 1,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 16384, 32767}, 8}, SpeakerPoint{{24840, 16384, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
5, 3, 2, 1,
{{SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 16384, 32767}, 8}, SpeakerPoint{{24840, 16384, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{1, {0, 0, 0}}, AxisGroup{2, {1, 2, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
5, 3, 1, 1,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{7928, 16384, 32767}, 8}, SpeakerPoint{{24840, 16384, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
6, 3, 2, 1,
{{SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 16384, 32767}, 8}, SpeakerPoint{{24840, 16384, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{2, {2, 3, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {4, 5, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
3, 1, 1, 0,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
}
}}
},
LayoutGeometry{
0xA1Fu, 16, 12,
{{0, 1, 2, 3, 6, 7, 4, 5, 8, 9, 10, 11, 0, 0, 0, 0}},
{{
RegionGeometry{
11, 3, 3, 2,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 7928, 32767}, 8}, SpeakerPoint{{24840, 7928, 32767}, 9}, SpeakerPoint{{7928, 24840, 32767}, 10}, SpeakerPoint{{24840, 24840, 32767}, 11}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{}}},
{{AxisGroup{2, {7, 8, 0}}, AxisGroup{2, {9, 10, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
9, 3, 2, 2,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{7928, 7928, 32767}, 8}, SpeakerPoint{{24840, 7928, 32767}, 9}, SpeakerPoint{{7928, 24840, 32767}, 10}, SpeakerPoint{{24840, 24840, 32767}, 11}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
9, 3, 2, 2,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 7928, 32767}, 8}, SpeakerPoint{{24840, 7928, 32767}, 9}, SpeakerPoint{{7928, 24840, 32767}, 10}, SpeakerPoint{{24840, 24840, 32767}, 11}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
7, 3, 2, 2,
{{SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 7928, 32767}, 8}, SpeakerPoint{{24840, 7928, 32767}, 9}, SpeakerPoint{{7928, 24840, 32767}, 10}, SpeakerPoint{{24840, 24840, 32767}, 11}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{1, {0, 0, 0}}, AxisGroup{2, {1, 2, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
7, 3, 1, 2,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{7928, 7928, 32767}, 8}, SpeakerPoint{{24840, 7928, 32767}, 9}, SpeakerPoint{{7928, 24840, 32767}, 10}, SpeakerPoint{{24840, 24840, 32767}, 11}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
8, 3, 2, 2,
{{SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 7928, 32767}, 8}, SpeakerPoint{{24840, 7928, 32767}, 9}, SpeakerPoint{{7928, 24840, 32767}, 10}, SpeakerPoint{{24840, 24840, 32767}, 11}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{2, {2, 3, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {4, 5, 0}}, AxisGroup{2, {6, 7, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
5, 3, 1, 1,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{7928, 7928, 32767}, 8}, SpeakerPoint{{24840, 7928, 32767}, 9}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
}
}}
},
LayoutGeometry{
0xA9Fu, 19, 14,
{{0, 1, 2, 3, 6, 7, 4, 5, 10, 11, 12, 13, 8, 9, 0, 0}},
{{
RegionGeometry{
13, 3, 4, 2,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{0, 5285, 0}, 8}, SpeakerPoint{{32767, 5285, 0}, 9}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 24840, 32767}, 12}, SpeakerPoint{{24840, 24840, 32767}, 13}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}}},
{{AxisGroup{2, {9, 10, 0}}, AxisGroup{2, {11, 12, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
11, 3, 3, 2,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 5285, 0}, 8}, SpeakerPoint{{32767, 5285, 0}, 9}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 24840, 32767}, 12}, SpeakerPoint{{24840, 24840, 32767}, 13}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}}},
{{AxisGroup{2, {7, 8, 0}}, AxisGroup{2, {9, 10, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
9, 3, 2, 2,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 24840, 32767}, 12}, SpeakerPoint{{24840, 24840, 32767}, 13}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
7, 3, 2, 2,
{{SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 24840, 32767}, 12}, SpeakerPoint{{24840, 24840, 32767}, 13}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{1, {0, 0, 0}}, AxisGroup{2, {1, 2, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
7, 3, 1, 2,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 24840, 32767}, 12}, SpeakerPoint{{24840, 24840, 32767}, 13}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
8, 3, 2, 2,
{{SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 24840, 32767}, 12}, SpeakerPoint{{24840, 24840, 32767}, 13}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{2, {2, 3, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {4, 5, 0}}, AxisGroup{2, {6, 7, 0}}, AxisGroup{}, AxisGroup{}}}
},
RegionGeometry{
7, 3, 2, 1,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 5285, 0}, 8}, SpeakerPoint{{32767, 5285, 0}, 9}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
}
}}
},
LayoutGeometry{
0xE9Fu, 20, 16,
{{0, 1, 2, 3, 6, 7, 4, 5, 10, 11, 14, 15, 12, 13, 8, 9}},
{{
RegionGeometry{
15, 3, 4, 3,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{0, 5285, 0}, 8}, SpeakerPoint{{32767, 5285, 0}, 9}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 16384, 32767}, 12}, SpeakerPoint{{24840, 16384, 32767}, 13}, SpeakerPoint{{7928, 24840, 32767}, 14}, SpeakerPoint{{24840, 24840, 32767}, 15}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}}},
{{AxisGroup{2, {9, 10, 0}}, AxisGroup{2, {11, 12, 0}}, AxisGroup{2, {13, 14, 0}}, AxisGroup{}}}
},
RegionGeometry{
13, 3, 3, 3,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 5285, 0}, 8}, SpeakerPoint{{32767, 5285, 0}, 9}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 16384, 32767}, 12}, SpeakerPoint{{24840, 16384, 32767}, 13}, SpeakerPoint{{7928, 24840, 32767}, 14}, SpeakerPoint{{24840, 24840, 32767}, 15}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}}},
{{AxisGroup{2, {7, 8, 0}}, AxisGroup{2, {9, 10, 0}}, AxisGroup{2, {11, 12, 0}}, AxisGroup{}}}
},
RegionGeometry{
11, 3, 2, 3,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 16384, 32767}, 12}, SpeakerPoint{{24840, 16384, 32767}, 13}, SpeakerPoint{{7928, 24840, 32767}, 14}, SpeakerPoint{{24840, 24840, 32767}, 15}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{2, {9, 10, 0}}, AxisGroup{}}}
},
RegionGeometry{
9, 3, 2, 3,
{{SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 16384, 32767}, 12}, SpeakerPoint{{24840, 16384, 32767}, 13}, SpeakerPoint{{7928, 24840, 32767}, 14}, SpeakerPoint{{24840, 24840, 32767}, 15}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{1, {0, 0, 0}}, AxisGroup{2, {1, 2, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{}}}
},
RegionGeometry{
9, 3, 1, 3,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 16384, 32767}, 12}, SpeakerPoint{{24840, 16384, 32767}, 13}, SpeakerPoint{{7928, 24840, 32767}, 14}, SpeakerPoint{{24840, 24840, 32767}, 15}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {3, 4, 0}}, AxisGroup{2, {5, 6, 0}}, AxisGroup{2, {7, 8, 0}}, AxisGroup{}}}
},
RegionGeometry{
10, 3, 2, 3,
{{SpeakerPoint{{0, 16384, 0}, 4}, SpeakerPoint{{32767, 16384, 0}, 5}, SpeakerPoint{{0, 32767, 0}, 6}, SpeakerPoint{{32767, 32767, 0}, 7}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{{7928, 16384, 32767}, 12}, SpeakerPoint{{24840, 16384, 32767}, 13}, SpeakerPoint{{7928, 24840, 32767}, 14}, SpeakerPoint{{24840, 24840, 32767}, 15}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{2, {0, 1, 0}}, AxisGroup{2, {2, 3, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {4, 5, 0}}, AxisGroup{2, {6, 7, 0}}, AxisGroup{2, {8, 9, 0}}, AxisGroup{}}}
},
RegionGeometry{
7, 3, 2, 1,
{{SpeakerPoint{{0, 0, 0}, 0}, SpeakerPoint{{32767, 0, 0}, 1}, SpeakerPoint{{16384, 0, 0}, 2}, SpeakerPoint{{0, 5285, 0}, 8}, SpeakerPoint{{32767, 5285, 0}, 9}, SpeakerPoint{{7928, 7928, 32767}, 10}, SpeakerPoint{{24840, 7928, 32767}, 11}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}, SpeakerPoint{}}},
{{AxisGroup{3, {0, 2, 1}}, AxisGroup{2, {3, 4, 0}}, AxisGroup{}, AxisGroup{}}},
{{AxisGroup{2, {5, 6, 0}}, AxisGroup{}, AxisGroup{}, AxisGroup{}}}
}
}}
}
}};
inline constexpr const LayoutGeometry* find_layout(const std::uint32_t speaker_bitfield) noexcept {
for (const auto& layout : kLayouts) {
if (layout.speaker_bitfield == speaker_bitfield) {
return &layout;
}
}
return nullptr;
}
} // namespace ejoc::speaker_tables
+495
View File
@@ -0,0 +1,495 @@
#define EJOC_BUILD_DLL
#include "eac3joc_core.h"
#include "speaker_layouts.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <new>
namespace ejoc::speaker {
using speaker_tables::AxisGroup;
using speaker_tables::LayoutGeometry;
using speaker_tables::RegionGeometry;
constexpr double kPi = 3.141592653589793238462643383279502884;
constexpr double kQ15Scale = 32768.0;
constexpr double kQ15Max = 32767.0 / kQ15Scale;
constexpr double kGainSnapThreshold = 1.0e-4;
constexpr std::size_t kObjects = EJOC_MAX_OBJECTS;
constexpr std::size_t kChannels = EJOC_OUTPUT_CHANNELS;
constexpr std::size_t kBlock = EJOC_SPEAKER_BLOCK_SAMPLES;
using PointGains = std::array<double, speaker_tables::kMaxPoints>;
using ChannelGains = std::array<double, kChannels>;
using ObjectChannelGains = std::array<ChannelGains, kObjects>;
using RemainingCounts = std::array<std::array<std::uint32_t, kChannels>, kObjects>;
inline double clamp(const double value, const double low, const double high) noexcept {
return value < low ? low : (value > high ? high : value);
}
inline double coordinate(const RegionGeometry& region, const std::size_t point,
const std::size_t component) noexcept {
return static_cast<double>(region.points[point].coordinate_q15[component]) / kQ15Scale;
}
std::uint64_t expand_speaker_bitfield(const std::uint32_t compact) noexcept {
constexpr std::array<std::uint64_t, 22> expansions{{
0x00000003ULL, 0x00000004ULL, 0x00000008ULL, 0x00000030ULL,
0x000000C0ULL, 0x00000100ULL, 0x00000600ULL, 0x00001800ULL,
0x00006000ULL, 0x00018000ULL, 0x00060000ULL, 0x00180000ULL,
0x00600000ULL, 0x01800000ULL, 0x06000000ULL, 0x18000000ULL,
0x60000000ULL, 0x080000000ULL, 0x600000000ULL, 0x800000000ULL,
0x1000000000ULL, 0x2000000000ULL,
}};
std::uint64_t expanded = 0;
for (std::size_t bit_index = 0; bit_index < expansions.size(); ++bit_index) {
if ((compact & (1u << bit_index)) != 0) {
expanded |= expansions[bit_index];
}
}
return expanded;
}
inline int bit(const std::uint64_t value, const unsigned index) noexcept {
return static_cast<int>((value >> index) & 1ULL);
}
double layout_attenuation_db(const std::uint32_t compact) noexcept {
const std::uint64_t expanded = expand_speaker_bitfield(compact);
const int height_channels = 2 * (
bit(expanded, 13) + bit(expanded, 15) + bit(expanded, 17) +
bit(expanded, 19) + bit(expanded, 21));
const int floor_channels = bit(expanded, 8) + 2 * (
bit(expanded, 31) + bit(expanded, 4) + bit(expanded, 6) +
bit(expanded, 11) + bit(expanded, 25) + bit(expanded, 27) +
bit(expanded, 29) + bit(expanded, 33));
const double height_factor = std::min(static_cast<double>(height_channels) / 4.0, 1.0);
const double floor_factor = std::min(static_cast<double>(floor_channels) / 4.0, 1.0);
return -std::max(4.5 - 1.5 * height_factor - 3.0 * floor_factor, 0.0);
}
int floor_y_exponent(const std::uint32_t compact) noexcept {
const std::uint32_t low = static_cast<std::uint32_t>(expand_speaker_bitfield(compact));
return ((low & 0x130u) != 0 && (low & 0x18C0u) == 0) ? 1 : 0;
}
inline void equal_power_pair(const double position, double& lower, double& upper) noexcept {
const double angle = (kPi * 0.5) * position;
lower = std::cos(angle);
upper = std::sin(angle);
}
void axis0_gains(const RegionGeometry& region,
const std::array<AxisGroup, speaker_tables::kMaxGroups>& groups,
const std::uint8_t group_count,
const double value,
PointGains& output) noexcept {
output.fill(0.0);
for (std::size_t row = 0; row < group_count; ++row) {
const auto& group = groups[row];
if (group.size == 0) {
continue;
}
const std::size_t first = group.indices[0];
const std::size_t last = group.indices[group.size - 1];
const double first_value = coordinate(region, first, 0);
const double last_value = coordinate(region, last, 0);
if (value <= first_value) {
output[first] = 1.0;
continue;
}
if (value >= last_value) {
output[last] = 1.0;
continue;
}
for (std::size_t index = 0; index + 1 < group.size; ++index) {
const std::size_t lower_index = group.indices[index];
const std::size_t upper_index = group.indices[index + 1];
const double lower_value = coordinate(region, lower_index, 0);
const double upper_value = coordinate(region, upper_index, 0);
if (value > lower_value && value <= upper_value) {
const double position = (value - lower_value) / (upper_value - lower_value);
equal_power_pair(position, output[lower_index], output[upper_index]);
break;
}
}
}
}
void axis1_gains(const RegionGeometry& region,
const std::array<AxisGroup, speaker_tables::kMaxGroups>& groups,
const std::uint8_t group_count,
const double value,
PointGains& output) noexcept {
output.fill(0.0);
if (group_count == 0) {
return;
}
const auto& first_group = groups[0];
const auto& last_group = groups[group_count - 1];
const double first_value = coordinate(region, first_group.indices[0], 1);
const double last_value = coordinate(region, last_group.indices[0], 1);
if (value <= first_value) {
for (std::size_t index = 0; index < first_group.size; ++index) {
output[first_group.indices[index]] = 1.0;
}
return;
}
if (value > last_value) {
for (std::size_t index = 0; index < last_group.size; ++index) {
output[last_group.indices[index]] = 1.0;
}
return;
}
for (std::size_t row = 0; row + 1 < group_count; ++row) {
const auto& lower_group = groups[row];
const auto& upper_group = groups[row + 1];
const double lower_value = coordinate(region, lower_group.indices[0], 1);
const double upper_value = coordinate(region, upper_group.indices[0], 1);
if (value >= lower_value && value <= upper_value) {
const double position = (value - lower_value) / (upper_value - lower_value);
double lower_gain = 0.0;
double upper_gain = 0.0;
equal_power_pair(position, lower_gain, upper_gain);
for (std::size_t index = 0; index < lower_group.size; ++index) {
output[lower_group.indices[index]] = lower_gain;
}
for (std::size_t index = 0; index < upper_group.size; ++index) {
output[upper_group.indices[index]] = upper_gain;
}
return;
}
}
}
void plane_gains(const RegionGeometry& region,
const std::array<AxisGroup, speaker_tables::kMaxGroups>& groups,
const std::uint8_t group_count,
const double u,
const double v,
const std::uint8_t mode,
PointGains& output) noexcept {
axis0_gains(region, groups, group_count, u, output);
if (mode >= 2) {
PointGains vertical{};
axis1_gains(region, groups, group_count, v, vertical);
for (std::size_t point = 0; point < region.point_count; ++point) {
output[point] *= vertical[point];
}
}
}
class Renderer {
public:
explicit Renderer(const LayoutGeometry* layout) noexcept : layout_(layout) {
for (std::size_t standard = 0; standard < layout_->channel_count; ++standard) {
standard_index_for_internal_[layout_->standard_from_internal[standard]] =
static_cast<std::uint8_t>(standard);
}
attenuation_db_ = layout_attenuation_db(layout_->speaker_bitfield);
floor_y_exponent_ = floor_y_exponent(layout_->speaker_bitfield);
reset_state();
}
int reset() noexcept {
reset_state();
error_[0] = '\0';
return 0;
}
const char* last_error() const noexcept {
return error_[0] ? error_.data() : "";
}
int process(const float* input,
const std::uint32_t sample_count,
const std::uint32_t metadata_count,
const std::uint32_t* metadata_offsets,
const std::uint32_t* ramp_durations,
const std::uint16_t* positions_q15,
const std::uint8_t* region_indices,
const std::uint8_t* height_enabled,
const double* object_gains,
double* output) noexcept {
error_[0] = '\0';
if (!input || !output) {
return fail("null PCM pointer passed to ejoc_speaker_renderer_process");
}
if ((sample_count % kBlock) != 0) {
return fail("sample_count must be a multiple of 32");
}
if (metadata_count && (!metadata_offsets || !ramp_durations || !positions_q15)) {
return fail("metadata arrays are null while metadata_count is nonzero");
}
if (!validate_metadata(sample_count, metadata_count, metadata_offsets,
positions_q15, region_indices, object_gains)) {
return -1;
}
std::fill(output, output + static_cast<std::size_t>(sample_count) * layout_->channel_count, 0.0);
const bool has_lfe = (layout_->speaker_bitfield & 0x4u) != 0;
const std::size_t total_blocks = sample_count / kBlock;
std::size_t event = 0;
for (std::size_t block = 0; block < total_blocks; ++block) {
while (event < metadata_count && aligned_block(metadata_offsets[event]) == block) {
apply_event(event, ramp_durations, positions_q15, region_indices,
height_enabled, object_gains);
++event;
}
mix_block(input, output, block, has_lfe);
}
while (event < metadata_count && aligned_block(metadata_offsets[event]) == total_blocks) {
apply_event(event, ramp_durations, positions_q15, region_indices,
height_enabled, object_gains);
++event;
}
if (event != metadata_count) {
return fail("metadata alignment produced an event outside this process call");
}
return 0;
}
private:
void reset_state() noexcept {
for (auto& row : current_) row.fill(0.0);
for (auto& row : target_) row.fill(0.0);
for (auto& row : step_) row.fill(0.0);
for (auto& row : remaining_) row.fill(0);
}
int fail(const char* message) noexcept {
std::snprintf(error_.data(), error_.size(), "%s", message);
return -1;
}
bool validate_metadata(const std::uint32_t sample_count,
const std::uint32_t metadata_count,
const std::uint32_t* metadata_offsets,
const std::uint16_t* positions_q15,
const std::uint8_t* region_indices,
const double* object_gains) noexcept {
for (std::size_t event = 0; event < metadata_count; ++event) {
if (metadata_offsets[event] > sample_count) {
fail("metadata offset exceeds sample_count");
return false;
}
if (event && metadata_offsets[event] < metadata_offsets[event - 1]) {
fail("metadata offsets must be nondecreasing");
return false;
}
for (std::size_t object = 0; object < kObjects; ++object) {
const std::size_t object_event = event * kObjects + object;
if (region_indices && region_indices[object_event] >= 7) {
fail("region index is above 6");
return false;
}
if (object_gains && !std::isfinite(object_gains[object_event])) {
fail("object gain is not finite");
return false;
}
const std::size_t coordinate_base = object_event * EJOC_SPEAKER_COORDINATES;
for (std::size_t component = 0; component < EJOC_SPEAKER_COORDINATES; ++component) {
if (positions_q15[coordinate_base + component] > 32767u) {
fail("Q15 object coordinate is above 32767");
return false;
}
}
}
}
return true;
}
static std::size_t aligned_block(const std::uint32_t sample) noexcept {
return (static_cast<std::size_t>(sample) + kBlock / 2 - 1) / kBlock;
}
static std::uint32_t ramp_blocks(const std::uint32_t duration) noexcept {
return static_cast<std::uint32_t>(
(static_cast<std::size_t>(duration) + kBlock / 2 - 1) / kBlock);
}
void render_point(const std::uint16_t* position,
const std::uint8_t region_index,
const bool enable_height,
const double object_gain,
ChannelGains& output) const noexcept {
output.fill(0.0);
const auto& region = layout_->regions[region_index];
const double u = static_cast<double>(position[0]) / kQ15Scale;
const double v = static_cast<double>(position[1]) / kQ15Scale;
const double w = static_cast<double>(position[2]) / kQ15Scale;
const double floor_v = clamp(std::ldexp(v, floor_y_exponent_), 0.0, 1.0);
PointGains floor{};
plane_gains(region, region.axis0_groups, region.axis0_group_count,
u, floor_v, region.mode, floor);
PointGains point = floor;
if (region.mode == 3) {
PointGains height{};
plane_gains(region, region.axis1_groups, region.axis1_group_count,
u, v, 3, height);
const double z = enable_height ? clamp(w, 0.0, kQ15Max) : 0.0;
if (z >= kQ15Max) {
point = height;
} else if (z > 0.0) {
double floor_weight = 0.0;
double height_weight = 0.0;
equal_power_pair(z, floor_weight, height_weight);
for (std::size_t index = 0; index < region.point_count; ++index) {
point[index] = floor[index] * floor_weight + height[index] * height_weight;
}
}
}
const double y_term = clamp(v / 0.6, 0.0, 1.0);
const double z_term = clamp((w - 0.2) / 0.8, 0.0, 1.0);
const double amount = clamp(y_term + z_term, 0.0, 1.0);
const double gain = std::pow(10.0, attenuation_db_ * amount / 20.0) * object_gain;
for (std::size_t index = 0; index < region.point_count; ++index) {
output[region.points[index].speaker_id] = point[index] * gain;
}
}
void apply_event(const std::size_t event,
const std::uint32_t* ramp_durations,
const std::uint16_t* positions_q15,
const std::uint8_t* region_indices,
const std::uint8_t* height_enabled,
const double* object_gains) noexcept {
const std::uint32_t blocks = ramp_blocks(ramp_durations[event]);
for (std::size_t object = 0; object < kObjects; ++object) {
const std::size_t object_event = event * kObjects + object;
const auto* position = positions_q15 + object_event * EJOC_SPEAKER_COORDINATES;
const std::uint8_t region = region_indices ? region_indices[object_event] : 0;
const bool height = !height_enabled || height_enabled[object_event] != 0;
const double object_gain = object_gains ? object_gains[object_event] : 1.0;
ChannelGains next{};
render_point(position, region, height, object_gain, next);
for (std::size_t channel = 0; channel < layout_->channel_count; ++channel) {
const double difference = next[channel] - current_[object][channel];
target_[object][channel] = next[channel];
if (std::abs(difference) >= kGainSnapThreshold && blocks != 0) {
step_[object][channel] = difference / static_cast<double>(blocks);
remaining_[object][channel] = blocks;
} else {
current_[object][channel] = next[channel];
step_[object][channel] = 0.0;
remaining_[object][channel] = 0;
}
}
}
}
void mix_block(const float* input, double* output, const std::size_t block,
const bool has_lfe) noexcept {
const std::size_t start = block * kBlock;
if (has_lfe) {
for (std::size_t sample = 0; sample < kBlock; ++sample) {
output[(start + sample) * layout_->channel_count + 3] =
static_cast<double>(input[(start + sample) * EJOC_OUTPUT_CHANNELS]);
}
}
for (std::size_t object = 0; object < kObjects; ++object) {
for (std::size_t internal = 0; internal < layout_->channel_count; ++internal) {
const bool active = remaining_[object][internal] != 0;
const double fixed_gain = target_[object][internal];
if (!active && fixed_gain == 0.0) {
continue;
}
const std::size_t standard = standard_index_for_internal_[internal];
for (std::size_t sample = 0; sample < kBlock; ++sample) {
const double gain = active
? current_[object][internal] +
(static_cast<double>(sample) / static_cast<double>(kBlock)) *
step_[object][internal]
: fixed_gain;
output[(start + sample) * layout_->channel_count + standard] +=
static_cast<double>(
input[(start + sample) * EJOC_OUTPUT_CHANNELS + object + 1]) * gain;
}
if (active) {
current_[object][internal] += step_[object][internal];
--remaining_[object][internal];
if (remaining_[object][internal] == 0) {
current_[object][internal] = target_[object][internal];
}
} else {
current_[object][internal] = target_[object][internal];
}
}
}
}
const LayoutGeometry* layout_;
double attenuation_db_{};
int floor_y_exponent_{};
ObjectChannelGains current_{};
ObjectChannelGains target_{};
ObjectChannelGains step_{};
RemainingCounts remaining_{};
std::array<std::uint8_t, kChannels> standard_index_for_internal_{};
std::array<char, 256> error_{};
};
} // namespace ejoc::speaker
extern "C" {
uint32_t EJOC_CALL ejoc_speaker_layout_channel_count(const uint32_t speaker_bitfield) {
const auto* layout = ejoc::speaker_tables::find_layout(speaker_bitfield);
return layout ? layout->channel_count : 0;
}
ejoc_speaker_renderer_handle EJOC_CALL ejoc_speaker_renderer_create(
const uint32_t speaker_bitfield) {
const auto* layout = ejoc::speaker_tables::find_layout(speaker_bitfield);
if (!layout) {
return nullptr;
}
return new (std::nothrow) ejoc::speaker::Renderer(layout);
}
void EJOC_CALL ejoc_speaker_renderer_destroy(ejoc_speaker_renderer_handle handle) {
delete static_cast<ejoc::speaker::Renderer*>(handle);
}
int EJOC_CALL ejoc_speaker_renderer_reset(ejoc_speaker_renderer_handle handle) {
if (!handle) {
return -1;
}
return static_cast<ejoc::speaker::Renderer*>(handle)->reset();
}
const char* EJOC_CALL ejoc_speaker_renderer_last_error(ejoc_speaker_renderer_handle handle) {
if (!handle) {
return "speaker renderer handle is null";
}
return static_cast<ejoc::speaker::Renderer*>(handle)->last_error();
}
int EJOC_CALL ejoc_speaker_renderer_process(
ejoc_speaker_renderer_handle handle,
const float* objects16_interleaved,
const uint32_t sample_count,
const uint32_t metadata_count,
const uint32_t* metadata_offsets,
const uint32_t* ramp_durations,
const uint16_t* positions_q15,
const uint8_t* region_indices,
const uint8_t* height_enabled,
const double* object_gains,
double* output_interleaved) {
if (!handle) {
return -1;
}
return static_cast<ejoc::speaker::Renderer*>(handle)->process(
objects16_interleaved, sample_count, metadata_count,
metadata_offsets, ramp_durations, positions_q15,
region_indices, height_enabled, object_gains, output_interleaved);
}
} // extern "C"
+62
View File
@@ -0,0 +1,62 @@
#include "speaker/speaker_step.h"
#include <cstdio>
namespace joc::speaker {
Status step(SpeakerStep* context, const std::vector<float>& objects16_planar,
const oamd::OamdUpdate* update, std::string* error) {
if (context == nullptr || context->handle == nullptr) {
return Status::fail(JOC_ERR_STATE, stage::kRender, "speaker renderer is not open");
}
if (objects16_planar.size() !=
static_cast<std::size_t>(JOC_OUTPUT_CHANNELS) * JOC_FRAME_SAMPLES) {
return Status::fail(JOC_ERR_INVALID_ARGUMENT, stage::kRender,
"objects16 must be [16][1536]");
}
context->interleaved.assign(static_cast<std::size_t>(JOC_FRAME_SAMPLES) * JOC_OUTPUT_CHANNELS,
0.0f);
for (std::size_t n = 0; n < JOC_FRAME_SAMPLES; ++n) {
for (std::size_t c = 0; c < JOC_OUTPUT_CHANNELS; ++c) {
context->interleaved[n * JOC_OUTPUT_CHANNELS + c] =
objects16_planar[c * JOC_FRAME_SAMPLES + n];
}
}
context->output.assign(
static_cast<std::size_t>(JOC_FRAME_SAMPLES) * context->layout.channel_count, 0.0);
context->last_had_payload = update != nullptr;
std::uint32_t ramp = 0;
if (update != nullptr) {
context->state.apply(*update);
ramp = update->ramp_duration_samples;
context->last_block_offset = update->block_offset_samples;
context->last_ramp_duration = update->ramp_duration_samples;
context->last_object_count = update->object_count;
}
std::uint16_t positions[oamd::kObjects][3] = {};
context->state.object_positions_q15(positions);
// A frame without OAMD must not touch the gains at all (no event), otherwise a
// pending ramp would snap - matching the reference exactly.
const std::uint32_t event_count = context->last_had_payload ? 1u : 0u;
const std::uint32_t offset = context->metadata_offset;
const int result = ejoc_speaker_renderer_process(
context->handle, context->interleaved.data(), JOC_FRAME_SAMPLES, event_count,
event_count != 0u ? &offset : nullptr, event_count != 0u ? &ramp : nullptr,
event_count != 0u ? &positions[0][0] : nullptr, nullptr, nullptr, nullptr,
context->output.data());
if (result != 0) {
const char* message = ejoc_speaker_renderer_last_error(context->handle);
const std::string text =
"ejoc_speaker_renderer_process failed (" + std::to_string(result) + "): " +
(message != nullptr ? message : "unknown");
if (error != nullptr) {
*error = text;
}
return Status::fail(JOC_ERR_RENDER_FAILED, stage::kRender, text);
}
return Status::success();
}
} // namespace joc::speaker
+31
View File
@@ -0,0 +1,31 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
#include "eac3joc_core.h"
#include "foundation/status.h"
#include "oamd/oamd_parser.h"
#include "speaker/speaker_layout_lookup.h"
namespace joc::speaker {
struct SpeakerStep {
ejoc_speaker_renderer_handle handle = nullptr;
LayoutInfo layout{};
oamd::OamdState state;
std::uint32_t metadata_offset = 1473;
std::vector<float> interleaved;
std::vector<double> output;
std::uint32_t last_block_offset = 0;
std::uint32_t last_ramp_duration = 0;
std::uint32_t last_object_count = 0;
bool last_had_payload = false;
};
Status step(SpeakerStep* context, const std::vector<float>& objects16_planar,
const oamd::OamdUpdate* update, std::string* error);
} // namespace joc::speaker
+474
View File
@@ -0,0 +1,474 @@
#include "stream/stream.h"
#include <algorithm>
#include <cmath>
#include <cstring>
#include "adm/adm_metadata.h"
#include "foundation/status.h"
#include "hrtf/jochrtf.h"
#include "hrtf/rosella_model.h"
#include "hrtf/sofa_cache.h"
#include "joc_bitstream/joc_parser.h"
#include "joc_core/objects16.h"
namespace joc::stream {
namespace {
constexpr std::size_t kFrameSamples = JOC_FRAME_SAMPLES;
constexpr std::size_t kBedChannels = 6;
constexpr int kCoreChannels[5] = {0, 1, 2, 4, 5};
constexpr int kLfeChannel = 3;
} // namespace
Stream::~Stream() {
if (rebuilder_ != nullptr) {
ejoc_renderer_destroy(rebuilder_);
rebuilder_ = nullptr;
}
if (speaker_.handle != nullptr) {
ejoc_speaker_renderer_destroy(speaker_.handle);
speaker_.handle = nullptr;
}
}
void Stream::reset_state() {
reader_ = eac3::FrameReader();
metadata_.clear();
bed_pending_.clear();
objects16_.clear();
output_.clear();
read_offset_ = 0;
info_ = Info();
info_.output_channels = output_channels_;
if (rebuilder_ != nullptr) {
ejoc_renderer_reset(rebuilder_);
}
if (speaker_.handle != nullptr) {
ejoc_speaker_renderer_reset(speaker_.handle);
speaker_.state.reset();
speaker_.output.clear();
speaker_.last_had_payload = false;
}
if (binaural_ready_) {
binaural_.reset();
}
if (rosella_ready_) {
rosella_.reset();
}
rosella_pending_.clear();
rosella_read_offset_ = 0;
}
Status Stream::create(const Config& config) {
if (rebuilder_ != nullptr) {
return Status::fail(JOC_ERR_STATE, stage::kRender, "stream already created");
}
config_ = config;
if (config_.input > JOC_STREAM_IN_PCM_OBJECTS16 ||
config_.output > JOC_STREAM_OUT_BINAURAL) {
return Status::fail(JOC_ERR_INVALID_CONFIG, stage::kRender, "unknown stream kind");
}
if (config_.input == JOC_STREAM_IN_PCM_OBJECTS16 &&
config_.output == JOC_STREAM_OUT_PCM_OBJECTS16) {
return Status::fail(JOC_ERR_INVALID_CONFIG, stage::kRender,
"objects16 input with objects16 output would do nothing");
}
gain_ = static_cast<float>(std::pow(10.0, config_.gain_db / 20.0));
if (config_.input == JOC_STREAM_IN_EAC3) {
rebuilder_ = ejoc_renderer_create();
if (rebuilder_ == nullptr) {
return Status::fail(JOC_ERR_OUT_OF_MEMORY, stage::kDsp, "cannot create the JOC kernel");
}
if (config_.native_threads != 0u) {
ejoc_renderer_set_threads(rebuilder_, config_.native_threads);
}
}
if (config_.output == JOC_STREAM_OUT_SPEAKER) {
speaker_enabled_ = true;
if (!speaker::layout_by_name(config_.layout.c_str(), &speaker_.layout)) {
return Status::fail(JOC_ERR_LAYOUT_UNSUPPORTED, stage::kRender,
"unknown speaker layout: " + config_.layout);
}
speaker_.metadata_offset = config_.metadata_offset;
speaker_.handle = ejoc_speaker_renderer_create(speaker_.layout.speaker_bitfield);
if (speaker_.handle == nullptr) {
return Status::fail(JOC_ERR_RENDER_FAILED, stage::kRender,
"cannot create the speaker renderer");
}
output_channels_ = speaker_.layout.channel_count;
} else if (config_.output == JOC_STREAM_OUT_BINAURAL) {
binaural_enabled_ = true;
// The HRTF input precedence is joc_task_config's: a Rosella
// .personalized_headphone wins over a SOFA that the library compiles, and
// the compiled .jochrtf (hrtf_path) stays the fallback input.
if (!config_.personalized_headphone_path.empty()) {
hrtf::RosellaModel model;
Status status =
hrtf::load_personalized_headphone(config_.personalized_headphone_path, &model);
hrtf::RosellaRenderOptions render_options;
if (config_.binaural_mode == JOC_BINAURAL_NEAR) {
render_options.profile = hrtf::RosellaProfile::Near;
} else if (config_.binaural_mode == JOC_BINAURAL_FAR) {
render_options.profile = hrtf::RosellaProfile::Far;
}
render_options.object_delay_samples = config_.object_delay_samples;
render_options.tail_seconds = config_.tail_seconds;
render_options.output_gain = std::pow(10.0, config_.gain_db / 20.0);
if (status.ok()) {
status = rosella_.open(model, render_options);
}
if (!status.ok()) {
return Status::fail(status.code(), stage::kRender,
"binaural setup failed: " + status.message());
}
rosella_ready_ = true;
output_channels_ = 2;
} else {
hrtf::Field field;
hrtf::Kernels kernels;
Status status = Status::success();
if (!config_.hrtf_sofa_path.empty()) {
// The .jochrtf is an internal cache: the SOFA is the user-facing input.
hrtf::SofaFieldRequest request;
request.sofa_path = config_.hrtf_sofa_path;
request.options.shell_radius_m = config_.hrtf_radius_m;
request.cache_dir = config_.hrtf_cache_dir;
switch (config_.hrtf_cache_policy) {
case JOC_HRTF_CACHE_NONE: request.policy = hrtf::CachePolicy::None; break;
case JOC_HRTF_CACHE_DISK: request.policy = hrtf::CachePolicy::Disk; break;
default: request.policy = hrtf::CachePolicy::Memory; break;
}
std::string cache_path;
status = hrtf::load_or_compile_sofa_field(request, &field, &cache_path);
if (!status.ok()) {
return Status::fail(status.code(), stage::kRender,
"binaural setup failed: " + status.message());
}
} else {
status = hrtf::load_jochrtf(config_.hrtf_path, &field);
}
if (status.ok()) {
status = config_.kernels_path.empty()
? (kernels = hrtf::builtin_kernels(), Status::success())
: hrtf::load_kernels(config_.kernels_path, &kernels);
}
binaural::Profile profile = binaural::Profile::Mid;
if (status.ok() && config_.binaural_mode == JOC_BINAURAL_NEAR) {
profile = binaural::Profile::Near;
} else if (status.ok() && config_.binaural_mode == JOC_BINAURAL_FAR) {
profile = binaural::Profile::Far;
}
if (status.ok()) {
status = binaural_.open(field, kernels, profile);
}
if (!status.ok()) {
return status;
}
binaural_ready_ = true;
output_channels_ = 2;
}
} else {
output_channels_ = JOC_OUTPUT_CHANNELS;
}
reset_state();
return Status::success();
}
Status Stream::push_eac3(const std::uint8_t* data, std::size_t size, std::size_t* consumed) {
if (rebuilder_ == nullptr) {
return Status::fail(JOC_ERR_STATE, stage::kEmdf, "stream is not an E-AC-3 input");
}
if (consumed != nullptr) {
*consumed = size;
}
if (data != nullptr && size != 0u) {
reader_.push(data, size);
info_.bytes_in += size;
}
for (;;) {
eac3::Frame frame;
const eac3::FrameReader::Next state = reader_.next(&frame);
if (state == eac3::FrameReader::Next::End) {
break;
}
if (state == eac3::FrameReader::Next::Fail) {
return Status::fail(reader_.error(), stage::kEac3, reader_.error_message());
}
FrameMetadata entry;
emdf::Container container;
const Status parsed =
joc::parse_eac3_frame(frame.data, frame.size, &entry.params, &container, nullptr);
if (!parsed.ok()) {
return parsed;
}
if (const emdf::Payload* payload = container.find(emdf::kIdOamd)) {
std::vector<std::uint8_t> bytes;
const Status extracted =
emdf::extract_payload_bytes(frame.data, frame.size, *payload, &bytes);
if (!extracted.ok()) {
return extracted;
}
const Status oamd = oamd::parse_id11(bytes.data(), bytes.size(), &entry.update);
if (!oamd.ok()) {
return oamd;
}
entry.has_update = true;
entry.outer_offset = static_cast<std::int64_t>(payload->sample_offset);
}
metadata_.push_back(entry);
}
return process_ready_frames();
}
Status Stream::push_bed(const float* interleaved6, std::size_t samples, std::size_t* consumed) {
if (rebuilder_ == nullptr) {
return Status::fail(JOC_ERR_STATE, stage::kDsp, "stream is not an E-AC-3 input");
}
if (consumed != nullptr) {
*consumed = samples;
}
if (interleaved6 != nullptr && samples != 0u) {
bed_pending_.insert(bed_pending_.end(), interleaved6,
interleaved6 + samples * kBedChannels);
}
return process_ready_frames();
}
Status Stream::push_objects16(const float* planar16, std::size_t samples, std::size_t* consumed) {
if (consumed != nullptr) {
*consumed = samples;
}
if (planar16 == nullptr || samples == 0u) {
return Status::success();
}
// Rendered immediately: the host has already done the JOC rebuild.
for (std::size_t offset = 0; offset < samples; offset += kFrameSamples) {
const std::size_t count = std::min(kFrameSamples, samples - offset);
std::vector<float> frame(static_cast<std::size_t>(JOC_OUTPUT_CHANNELS) * kFrameSamples,
0.0f);
for (std::size_t channel = 0; channel < JOC_OUTPUT_CHANNELS; ++channel) {
std::memcpy(frame.data() + channel * kFrameSamples,
planar16 + channel * samples + offset, count * sizeof(float));
}
++info_.frames_in;
info_.samples_in += count;
const Status rendered = render_objects16(frame);
if (!rendered.ok()) {
return rendered;
}
if (count != kFrameSamples) {
break; // a partial frame is dropped; the host should push whole frames
}
}
return Status::success();
}
Status Stream::process_ready_frames() {
while (bed_pending_.size() / kBedChannels >= kFrameSamples && !metadata_.empty()) {
const FrameMetadata entry = metadata_.front();
metadata_.pop_front();
std::vector<float> bed5(static_cast<std::size_t>(JOC_CORE_CHANNELS) * kFrameSamples, 0.0f);
std::vector<float> lfe(kFrameSamples, 0.0f);
for (std::size_t sample = 0; sample < kFrameSamples; ++sample) {
for (std::size_t channel = 0; channel < JOC_CORE_CHANNELS; ++channel) {
bed5[channel * kFrameSamples + sample] =
bed_pending_[sample * kBedChannels + kCoreChannels[channel]];
}
lfe[sample] = bed_pending_[sample * kBedChannels + kLfeChannel];
}
bed_pending_.erase(bed_pending_.begin(),
bed_pending_.begin() + static_cast<std::ptrdiff_t>(kFrameSamples *
kBedChannels));
std::string error;
const Status rebuilt = joc::rebuild_objects16(rebuilder_, entry.params, bed5.data(),
lfe.data(), gain_, &objects16_, &error);
if (!rebuilt.ok()) {
return Status::fail(rebuilt.code(), stage::kDsp, error);
}
pending_metadata_ = entry;
const Status rendered = render_objects16(objects16_);
if (!rendered.ok()) {
return rendered;
}
++info_.frames_in;
info_.samples_in += kFrameSamples;
}
return Status::success();
}
Status Stream::render_objects16(const std::vector<float>& objects16) {
if (config_.output == JOC_STREAM_OUT_PCM_OBJECTS16) {
output_.insert(output_.end(), objects16.begin(), objects16.end());
info_.frames_out++;
info_.samples_out += kFrameSamples;
return Status::success();
}
if (speaker_enabled_) {
std::string error;
const Status stepped =
speaker::step(&speaker_, objects16, pending_metadata_.has_update
? &pending_metadata_.update
: nullptr,
&error);
if (!stepped.ok()) {
return Status::fail(stepped.code(), stage::kRender, error);
}
for (const double value : speaker_.output) {
output_.push_back(static_cast<float>(value));
}
info_.frames_out++;
info_.samples_out += kFrameSamples;
return Status::success();
}
if (rosella_ready_) {
return render_rosella_objects16(objects16);
}
const Status submitted =
binaural_.submit_frame(objects16.data(),
pending_metadata_.has_update ? &pending_metadata_.update : nullptr,
static_cast<std::int64_t>(info_.frames_out),
pending_metadata_.outer_offset,
static_cast<std::int64_t>(config_.object_delay_samples));
if (!submitted.ok()) {
return submitted;
}
std::vector<double> produced;
binaural_.take_output(&produced);
for (const double value : produced) {
output_.push_back(static_cast<float>(value));
}
info_.frames_out++;
info_.samples_out += produced.size() / 2u;
return Status::success();
}
// The Rosella runtime is driven exactly like the SOFA runtime (the same frame,
// update, frame index, outer offset and object delay), but it renders in chunks
// of its own size (64 frames by default), so a frame usually yields either
// nothing or a whole chunk. Its output therefore waits in a FIFO and is released
// one frame's worth at a time, which keeps the stream's contract intact: pushing
// one syncframe leaves exactly JOC_FRAME_SAMPLES samples for the caller to pull,
// and nothing is dropped or counted twice. pull() and flush() release the rest.
Status Stream::render_rosella_objects16(const std::vector<float>& objects16) {
const Status submitted =
rosella_.submit_frame(objects16.data(),
pending_metadata_.has_update ? &pending_metadata_.update : nullptr,
static_cast<std::int64_t>(info_.frames_out),
pending_metadata_.outer_offset,
static_cast<std::int64_t>(config_.object_delay_samples));
if (!submitted.ok()) {
return submitted;
}
std::vector<double> produced;
rosella_.take_output(&produced);
if (!produced.empty()) {
rosella_pending_.insert(rosella_pending_.end(), produced.begin(), produced.end());
}
release_rosella_output(kFrameSamples);
info_.frames_out++;
// Counted as the runtime produces it, which is also how the SOFA path counts:
// the totals are identical, only the frame they appear on differs.
info_.samples_out += produced.size() / 2u;
return Status::success();
}
void Stream::release_rosella_output(std::size_t limit) {
if (!rosella_ready_ || limit == 0u) {
return;
}
const std::size_t count = std::min(limit, rosella_pending_samples());
if (count == 0u) {
return;
}
const std::size_t values = count * 2u;
for (std::size_t index = 0; index < values; ++index) {
output_.push_back(static_cast<float>(rosella_pending_[rosella_read_offset_ + index]));
}
rosella_read_offset_ += values;
if (rosella_read_offset_ == rosella_pending_.size()) {
rosella_pending_.clear();
rosella_read_offset_ = 0;
}
}
Status Stream::pull(float* destination, std::size_t capacity_samples, std::size_t* produced) {
if (produced != nullptr) {
*produced = 0;
}
if (destination == nullptr || produced == nullptr) {
return Status::fail(JOC_ERR_INVALID_ARGUMENT, stage::kOutput, "null pull buffer");
}
// Rendered Rosella samples that the frame-at-a-time release above has not
// handed over yet are still the caller's to take; releasing them here keeps
// buffered_samples() and the amount pull() can deliver the same number.
release_rosella_output(capacity_samples);
const std::size_t available = buffered_samples();
const std::size_t count = std::min(capacity_samples, available);
if (count != 0u) {
std::memcpy(destination, output_.data() + read_offset_,
count * output_channels_ * sizeof(float));
read_offset_ += count * output_channels_;
if (read_offset_ == output_.size()) {
output_.clear();
read_offset_ = 0;
} else if (read_offset_ > (1u << 20)) {
output_.erase(output_.begin(),
output_.begin() + static_cast<std::ptrdiff_t>(read_offset_));
read_offset_ = 0;
}
}
*produced = static_cast<std::uint32_t>(count);
return Status::success();
}
Status Stream::flush() {
if (binaural_ready_) {
std::vector<double> tail;
const Status drained =
binaural_.finish(binaural_.finish_capacity(config_.tail_seconds), &tail);
if (!drained.ok()) {
return drained;
}
for (const double value : tail) {
output_.push_back(static_cast<float>(value));
}
info_.samples_out += tail.size() / 2u;
}
if (rosella_ready_) {
std::vector<double> tail;
const Status drained =
rosella_.finish(rosella_.finish_capacity(config_.tail_seconds), &tail);
if (!drained.ok()) {
return drained;
}
// Everything the runtime produced as the program is released first: the
// tail only sounds after it. The program samples were already counted by
// render_rosella_objects16, so only the tail is added here.
release_rosella_output(rosella_pending_samples());
for (const double value : tail) {
output_.push_back(static_cast<float>(value));
}
info_.samples_out += tail.size() / 2u;
}
info_.ended = 1;
return Status::success();
}
Status Stream::reset() {
if (rebuilder_ == nullptr && !speaker_enabled_ && !binaural_ready_ && !rosella_ready_ &&
config_.input != JOC_STREAM_IN_PCM_OBJECTS16) {
return Status::fail(JOC_ERR_STATE, stage::kRender, "stream is not created");
}
reset_state();
return Status::success();
}
} // namespace joc::stream
+125
View File
@@ -0,0 +1,125 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <deque>
#include <string>
#include <vector>
#include "binaural/binaural_runtime.h"
#include "eac3_transport/eac3_reader.h"
#include "foundation/status.h"
#include "hrtf/rosella_renderer.h"
#include "joc_core.h"
#include "joc_stream.h"
#include "oamd/oamd_parser.h"
#include "speaker/speaker_step.h"
namespace joc::stream {
struct Config {
std::uint32_t input = JOC_STREAM_IN_EAC3;
std::uint32_t output = JOC_STREAM_OUT_PCM_OBJECTS16;
std::string layout;
std::uint32_t metadata_offset = 1473;
std::uint32_t binaural_mode = JOC_BINAURAL_MID;
std::string hrtf_path;
std::string kernels_path;
double tail_seconds = 5.0;
std::uint32_t object_delay_samples = 1473;
double gain_db = 0.0;
std::uint32_t native_threads = 0;
// The three HRTF shapes of joc_task_config, in its precedence order: a Rosella
// model wins over a SOFA, and hrtf_path (.jochrtf) is the fallback.
std::string hrtf_sofa_path;
std::string personalized_headphone_path;
std::string hrtf_cache_dir;
std::uint32_t hrtf_cache_policy = JOC_HRTF_CACHE_MEMORY;
double hrtf_radius_m = 1.0;
};
struct Info {
std::uint64_t frames_in = 0;
std::uint64_t frames_out = 0;
std::uint64_t samples_in = 0;
std::uint64_t samples_out = 0;
std::uint64_t bytes_in = 0;
std::uint64_t oamd_payloads = 0;
std::uint64_t oamd_transitions = 0;
std::uint32_t output_channels = 0;
std::uint32_t ended = 0;
};
// One frame's metadata, queued while the matching core PCM arrives.
struct FrameMetadata {
joc_frame_params params{};
oamd::OamdUpdate update{};
bool has_update = false;
std::int64_t outer_offset = 0;
};
class Stream {
public:
Stream() = default;
~Stream();
Stream(const Stream&) = delete;
Stream& operator=(const Stream&) = delete;
Status create(const Config& config);
Status push_eac3(const std::uint8_t* data, std::size_t size, std::size_t* consumed);
Status push_bed(const float* interleaved6, std::size_t samples, std::size_t* consumed);
Status push_objects16(const float* planar16, std::size_t samples, std::size_t* consumed);
Status pull(float* destination, std::size_t capacity_samples, std::size_t* produced);
Status flush();
Status reset();
const Info& info() const { return info_; }
// Per-channel sample count, not the interleaved float count. The Rosella
// runtime renders in its own chunk size, so its output waits in a FIFO before
// it is released one frame at a time; those samples are rendered and unpulled
// as well, so the reported backlog has to include them.
std::size_t buffered_samples() const {
return output_channels_ != 0u
? (output_.size() - read_offset_) / output_channels_ + rosella_pending_samples()
: 0u;
}
private:
Status process_ready_frames();
Status render_objects16(const std::vector<float>& objects16);
Status render_rosella_objects16(const std::vector<float>& objects16);
// Moves at most `limit` rendered stereo samples per channel out of the FIFO
// into output_, oldest sample first.
void release_rosella_output(std::size_t limit);
std::size_t rosella_pending_samples() const {
return rosella_ready_ ? (rosella_pending_.size() - rosella_read_offset_) / 2u : 0u;
}
void reset_state();
Config config_;
Info info_;
eac3::FrameReader reader_;
std::deque<FrameMetadata> metadata_;
FrameMetadata pending_metadata_;
std::vector<float> bed_pending_;
std::vector<std::uint8_t> frame_copy_;
std::vector<float> objects16_;
std::vector<float> output_;
std::size_t read_offset_ = 0;
std::uint32_t output_channels_ = 0;
ejoc_renderer_handle rebuilder_ = nullptr;
speaker::SpeakerStep speaker_;
binaural::SofaBinauralRuntime binaural_;
hrtf::RosellaRuntime rosella_;
std::vector<double> rosella_pending_;
std::size_t rosella_read_offset_ = 0;
bool speaker_enabled_ = false;
bool binaural_enabled_ = false;
bool binaural_ready_ = false;
bool rosella_ready_ = false;
float gain_ = 1.0f;
};
} // namespace joc::stream
File diff suppressed because it is too large Load Diff
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
#include "foundation/status.h"
#include "joc_core.h"
namespace joc::task {
Status validate(const joc_task_config& raw, std::vector<joc_validation_issue>* issues,
std::uint32_t* error_count);
// Runs the task on the calling thread. Never throws; failures come back as the
Status run(const joc_task_config& raw, const joc_event_sink* sink, joc_task_result* out);
// Serialises the stable subset of the result (plan 22.2 / 33.5).
Status result_to_json(const joc_task_result& result, std::string* out);
} // namespace joc::task
+111
View File
@@ -0,0 +1,111 @@
#include "telemetry/event_bus.h"
#include <cstring>
#include <type_traits>
namespace joc::telemetry {
// The event must stay a trivially copyable POD with no pointers: that is what
// makes "an event can never carry audio" a compile-time property.
static_assert(std::is_trivially_copyable_v<joc_event>, "joc_event must be a POD");
static_assert(sizeof(joc_event) <= 512, "joc_event must stay small");
const char* stage_name(joc_stage stage) {
switch (stage) {
case JOC_STAGE_IDLE: return "idle";
case JOC_STAGE_INPUT: return "input";
case JOC_STAGE_METADATA: return "metadata";
case JOC_STAGE_DECODE: return "decode";
case JOC_STAGE_JOC: return "joc";
case JOC_STAGE_RENDER: return "render";
case JOC_STAGE_OUTPUT: return "output";
case JOC_STAGE_DONE: return "done";
default: return "unknown";
}
}
EventBus::EventBus(const joc_event_sink* sink) {
if (sink != nullptr && sink->callback != nullptr) {
sink_ = *sink;
has_sink_ = true;
}
}
void EventBus::set_totals(std::uint64_t total_frames, std::uint64_t total_samples) {
total_frames_ = total_frames;
total_samples_ = total_samples;
}
double EventBus::elapsed_seconds() const {
return std::chrono::duration<double>(std::chrono::steady_clock::now() - started_).count();
}
void EventBus::publish(joc_event* event) {
if (!has_sink_) {
return;
}
if (sink_.min_type != 0u && event->type < sink_.min_type) {
return;
}
if (sink_.max_type != 0u && event->type > sink_.max_type) {
return;
}
sink_.callback(sink_.user, event);
}
void EventBus::emit(std::uint32_t type, joc_stage stage, std::uint32_t log_level,
const std::string& message, joc_error code) {
joc_event event{};
event.struct_size = sizeof(joc_event);
event.type = type;
event.sequence = ++sequence_;
event.timestamp_us = static_cast<std::uint64_t>(elapsed_seconds() * 1e6);
event.total_frames = total_frames_;
event.total_samples = total_samples_;
event.stage = static_cast<std::uint32_t>(stage);
event.backend = backend_;
event.progress = total_frames_ != 0u
? static_cast<double>(event.current_frame) /
static_cast<double>(total_frames_)
: -1.0;
event.elapsed_seconds = elapsed_seconds();
event.error_code = code;
event.log_level = log_level;
std::snprintf(event.stage_name, sizeof(event.stage_name), "%s", stage_name(stage));
std::snprintf(event.message, sizeof(event.message), "%s", message.c_str());
publish(&event);
}
void EventBus::progress(std::uint64_t frame, std::uint64_t sample, std::uint64_t output_samples,
std::uint64_t output_bytes, double output_seconds) {
joc_event event{};
event.struct_size = sizeof(joc_event);
event.type = JOC_EV_PROGRESS;
event.sequence = ++sequence_;
event.timestamp_us = static_cast<std::uint64_t>(elapsed_seconds() * 1e6);
event.current_frame = frame;
event.total_frames = total_frames_;
event.current_sample = sample;
event.total_samples = total_samples_;
event.stage = static_cast<std::uint32_t>(stage_);
event.backend = backend_;
event.progress = total_frames_ != 0u
? static_cast<double>(frame) / static_cast<double>(total_frames_)
: -1.0;
event.elapsed_seconds = elapsed_seconds();
const double audio_seconds = static_cast<double>(sample) / 48000.0;
event.realtime_factor = event.elapsed_seconds > 0.0 ? audio_seconds / event.elapsed_seconds
: 0.0;
event.output_samples = output_samples;
event.output_bytes = output_bytes;
event.output_duration_seconds = output_seconds;
std::snprintf(event.stage_name, sizeof(event.stage_name), "%s", stage_name(stage_));
publish(&event);
}
void EventBus::stage(joc_stage stage, const std::string& message) {
stage_ = stage;
emit(JOC_EV_STAGE_CHANGED, stage, JOC_LOG_INFO, message);
}
} // namespace joc::telemetry
+62
View File
@@ -0,0 +1,62 @@
#pragma once
#include <chrono>
#include <cstdint>
#include <string>
#include "joc_core.h"
namespace joc::telemetry {
const char* stage_name(joc_stage stage);
class EventBus {
public:
EventBus() = default;
explicit EventBus(const joc_event_sink* sink);
void set_totals(std::uint64_t total_frames, std::uint64_t total_samples);
void set_backend(std::uint32_t backend) { backend_ = backend; }
// Generic emit; `message` is truncated into the 256-byte field.
void emit(std::uint32_t type, joc_stage stage, std::uint32_t log_level, const std::string& message,
joc_error code = JOC_OK);
void progress(std::uint64_t frame, std::uint64_t sample, std::uint64_t output_samples, std::uint64_t output_bytes,
double output_seconds);
void stage(joc_stage stage, const std::string& message = std::string());
void info(const std::string& message) { emit(JOC_EV_LOG, stage_, JOC_LOG_INFO, message); }
void warning(const std::string& message)
{
++warning_count_;
emit(JOC_EV_WARNING, stage_, JOC_LOG_WARNING, message);
}
void error(joc_error code, const std::string& stage_text, const std::string& message)
{
++error_count_;
emit(JOC_EV_ERROR, stage_, JOC_LOG_ERROR,
(stage_text.empty() ? message : stage_text + ": " + message), code);
}
std::uint64_t sequence() const { return sequence_; }
std::uint32_t warning_count() const { return warning_count_; }
std::uint32_t error_count() const { return error_count_; }
double elapsed_seconds() const;
private:
void publish(joc_event* event);
joc_event_sink sink_{};
bool has_sink_ = false;
std::chrono::steady_clock::time_point started_ = std::chrono::steady_clock::now();
std::uint64_t sequence_ = 0;
std::uint64_t total_frames_ = 0;
std::uint64_t total_samples_ = 0;
std::uint32_t backend_ = 0;
std::uint32_t warning_count_ = 0;
std::uint32_t error_count_ = 0;
joc_stage stage_ = JOC_STAGE_IDLE;
};
} // namespace joc::telemetry
+187
View File
@@ -0,0 +1,187 @@
#include "timeline/position_timeline.h"
#include <cstring>
#include "foundation/geometry.h"
namespace joc::timeline {
namespace {
Status timeline_fail(joc_error code, const std::string& message) {
return Status::fail(code, stage::kOamd, message);
}
} // namespace
void ObjectPositionTrack::set_initial(const double position[3]) {
for (int i = 0; i < 3; ++i) {
initial_[i] = position[i];
last_target_[i] = position[i];
}
}
Status ObjectPositionTrack::append(std::int64_t start_sample, std::int64_t duration_samples,
const double target[3], int object_index) {
if (start_sample < 0 || duration_samples < 0) {
return Status::fail(JOC_ERR_INVALID_ARGUMENT, stage::kOamd,
"position transition timing must be non-negative");
}
if (!transitions_.empty()) {
const PositionTransition& previous = transitions_.back();
if (start_sample < previous.end_sample()) {
return timeline_fail(
JOC_ERR_OAMD_UNSUPPORTED_VARIANT,
"overlapping_binaural_position_ramps: object " + std::to_string(object_index) +
" has a new position update at sample " + std::to_string(start_sample) +
" before the previous ramp ends at " + std::to_string(previous.end_sample()));
}
if (start_sample == previous.start_sample && previous.duration_samples == 0) {
PositionTransition replacement;
replacement.start_sample = start_sample;
replacement.duration_samples = duration_samples;
std::memcpy(replacement.origin, previous.origin, sizeof(replacement.origin));
std::memcpy(replacement.target, target, sizeof(replacement.target));
transitions_.back() = replacement;
std::memcpy(last_target_, target, sizeof(last_target_));
return Status::success();
}
}
PositionTransition transition;
transition.start_sample = start_sample;
transition.duration_samples = duration_samples;
std::memcpy(transition.origin, last_target_, sizeof(transition.origin));
std::memcpy(transition.target, target, sizeof(transition.target));
transitions_.push_back(transition);
std::memcpy(last_target_, target, sizeof(last_target_));
return Status::success();
}
Status ObjectPositionTrack::position_at(std::int64_t sample, double out[3]) {
if (sample < last_query_sample_) {
return Status::fail(JOC_ERR_STATE, stage::kOamd,
"binaural metadata positions must be queried monotonically");
}
last_query_sample_ = sample;
while (cursor_ < transitions_.size()) {
const PositionTransition& transition = transitions_[cursor_];
if (sample < transition.end_sample()) {
break;
}
std::memcpy(initial_, transition.target, sizeof(initial_));
++cursor_;
}
if (cursor_ >= transitions_.size()) {
std::memcpy(out, initial_, sizeof(initial_));
return Status::success();
}
const PositionTransition& transition = transitions_[cursor_];
if (sample < transition.start_sample) {
std::memcpy(out, initial_, sizeof(initial_));
return Status::success();
}
if (transition.duration_samples == 0) {
std::memcpy(out, transition.target, sizeof(transition.target));
return Status::success();
}
const double amount =
static_cast<double>(sample - transition.start_sample) /
static_cast<double>(transition.duration_samples);
for (int i = 0; i < 3; ++i) {
out[i] = transition.origin[i] + (transition.target[i] - transition.origin[i]) * amount;
}
return Status::success();
}
OamdPositionTimeline::OamdPositionTimeline(int object_count) : object_count_(object_count) {
if (object_count != kTimelineObjects) {
object_count_ = kTimelineObjects;
}
}
Status OamdPositionTimeline::submit_update(const oamd::OamdUpdate& update,
std::int64_t frame_start_sample,
std::int64_t outer_sample_offset,
std::int64_t object_delay_samples,
std::int64_t processed_sample) {
if (frame_start_sample < 0 || outer_sample_offset < 0 || object_delay_samples < 0) {
return Status::fail(JOC_ERR_INVALID_ARGUMENT, stage::kOamd,
"OAMD frame, outer offset and object delay must be non-negative");
}
state_.apply(update);
double targets[kTimelineObjects][3] = {};
for (int index = 0; index < kTimelineObjects; ++index) {
geometry::q_to_adm_xyz(state_.q(index + 1, 0), state_.q(index + 1, 1),
state_.q(index + 1, 2), &targets[index][0], &targets[index][1],
&targets[index][2]);
}
const std::int64_t coded_event =
frame_start_sample + outer_sample_offset +
static_cast<std::int64_t>(update.block_offset_samples);
if (coded_event < last_coded_event_sample_) {
return timeline_fail(JOC_ERR_OAMD_UNSUPPORTED_VARIANT,
"non_monotonic_binaural_updates: event sample " +
std::to_string(coded_event) + " follows " +
std::to_string(last_coded_event_sample_));
}
last_coded_event_sample_ = coded_event;
if (!initialized_) {
if (processed_sample > 0) {
return timeline_fail(JOC_ERR_OAMD_UNSUPPORTED_VARIANT,
"late_initial_binaural_state: the first OAMD state arrived after "
"sample " + std::to_string(processed_sample) +
" had been processed, so sample 0 cannot be backfilled");
}
for (int index = 0; index < kTimelineObjects; ++index) {
tracks_[index].set_initial(targets[index]);
std::memcpy(previous_targets_[index], targets[index], sizeof(targets[index]));
has_previous_[index] = true;
}
initialized_ = true;
++payload_count_;
return Status::success();
}
const std::int64_t ramp_duration = static_cast<std::int64_t>(update.ramp_duration_samples);
const std::int64_t effective_ramp =
ramp_duration - kOamdUpdateQuantumSamples > 0 ? ramp_duration - kOamdUpdateQuantumSamples
: 0;
std::int64_t transition_start = coded_event + object_delay_samples;
if (effective_ramp != 0) {
transition_start += kOamdUpdateQuantumSamples;
}
for (int index = 0; index < kTimelineObjects; ++index) {
const bool changed = !has_previous_[index] ||
std::memcmp(previous_targets_[index], targets[index],
sizeof(targets[index])) != 0;
if (!changed) {
continue;
}
const Status status = tracks_[index].append(transition_start, effective_ramp,
targets[index], index + 1);
if (!status.ok()) {
return status;
}
std::memcpy(previous_targets_[index], targets[index], sizeof(targets[index]));
has_previous_[index] = true;
++transition_count_;
}
++payload_count_;
return Status::success();
}
Status OamdPositionTimeline::positions_at(std::int64_t sample,
double positions[kTimelineObjects][3]) {
for (int index = 0; index < kTimelineObjects; ++index) {
const Status status = tracks_[index].position_at(sample, positions[index]);
if (!status.ok()) {
return status;
}
}
return Status::success();
}
} // namespace joc::timeline
+71
View File
@@ -0,0 +1,71 @@
// Port of src/binaural_metadata.py.
#pragma once
#include <cstdint>
#include <vector>
#include "foundation/status.h"
#include "oamd/oamd_parser.h"
namespace joc::timeline {
inline constexpr std::int64_t kOamdUpdateQuantumSamples = 64;
inline constexpr int kTimelineObjects = 15;
struct PositionTransition {
std::int64_t start_sample = 0;
std::int64_t duration_samples = 0;
double origin[3] = {};
double target[3] = {};
std::int64_t end_sample() const { return start_sample + duration_samples; }
};
class ObjectPositionTrack {
public:
void set_initial(const double position[3]);
Status append(std::int64_t start_sample, std::int64_t duration_samples, const double target[3],
int object_index);
// Monotonic queries only; out receives the interpolated position.
Status position_at(std::int64_t sample, double out[3]);
const std::vector<PositionTransition>& transitions() const { return transitions_; }
private:
double initial_[3] = {};
double last_target_[3] = {};
std::vector<PositionTransition> transitions_;
std::size_t cursor_ = 0;
std::int64_t last_query_sample_ = -1;
};
class OamdPositionTimeline {
public:
explicit OamdPositionTimeline(int object_count = kTimelineObjects);
Status submit_update(const oamd::OamdUpdate& update, std::int64_t frame_start_sample,
std::int64_t outer_sample_offset, std::int64_t object_delay_samples,
std::int64_t processed_sample);
// positions[15][3]; queries must be monotonically increasing.
Status positions_at(std::int64_t sample, double positions[kTimelineObjects][3]);
std::uint64_t payload_count() const { return payload_count_; }
std::uint64_t transition_count() const { return transition_count_; }
bool initialized() const { return initialized_; }
const ObjectPositionTrack& track(int index) const { return tracks_[index]; }
private:
int object_count_;
oamd::OamdState state_;
ObjectPositionTrack tracks_[kTimelineObjects];
bool initialized_ = false;
double previous_targets_[kTimelineObjects][3] = {};
bool has_previous_[kTimelineObjects] = {};
std::uint64_t payload_count_ = 0;
std::uint64_t transition_count_ = 0;
std::int64_t last_coded_event_sample_ = -1;
};
} // namespace joc::timeline