#include "hrtf/rosella_model.h" #include #include #include #include #include #include #include #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(value) * kQ15; } float q15_exp(std::int32_t value, int exponent) { return q15(value) * static_cast(std::ldexp(1.0, exponent)); } std::uint16_t low16(std::int32_t value) { return static_cast(static_cast(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* 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(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& 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(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(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(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* padded) { padded->assign(static_cast(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(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* padded) { padded->assign(static_cast(padded_rows) * static_cast(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(destination)] = q15(serialized[source]); } } void unpack_table_a_extra(const std::int32_t* serialized, std::vector* 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(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(stream)), std::istreambuf_iterator()); 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 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 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 parameters; if (!json::parse_object(virtualizer->raw, ¶meters, &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 fields; if (json::parse_object(capture->raw, &fields, &error)) { const std::pair 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 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(option_count)); for (int index = 0; index < option_count; ++index) { model.table_a_option_ids[static_cast(index)] = low16(values[position + static_cast(index)]); } position += static_cast(option_count); model.table_a_option_values.resize(static_cast(option_count)); for (int index = 0; index < option_count; ++index) { model.table_a_option_values[static_cast(index)] = q15(values[position + static_cast(index)]); } position += static_cast(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(16 * dimension); for (int index = 0; index < 4; ++index) { model.table_a_four_integers[static_cast(index)] = low16(values[position + static_cast(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(8 * dimension); model.table_a_vector16.resize(16u); for (int index = 0; index < 16; ++index) { model.table_a_vector16[static_cast(index)] = q15(values[position + static_cast(index)]); } position += 16u; unpack_table_a_grid(values + position, dimension, 4, 5, 4, &model.table_a_filter_4x64_padded); position += static_cast(4 * dimension); model.table_a_extra_indices.resize(static_cast(extra)); for (int index = 0; index < extra; ++index) { model.table_a_extra_indices[static_cast(index)] = low16(values[position + static_cast(index)]); } position += static_cast(extra); model.table_a_extra_fields_padded.assign(static_cast(extra) * 160u, 0.0f); std::vector 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(index) * 160); position += 154u; } model.table_a_extra_vectors.assign(static_cast(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(index) * 16u + static_cast(lane)] = q15(values[position + static_cast(lane)]); } position += 16u; } const std::size_t table_b_start = position; if (table_b_start != 13u + 1821u + static_cast(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(index)] = q15_exp(values[position + static_cast(index)], model.matrix_exponent); } position += 36u; for (int index = 0; index < 36; ++index) { model.vector_right[static_cast(index)] = q15_exp(values[position + static_cast(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(index)] = low16(values[position + static_cast(index)]); if (model.hybrid_flags[static_cast(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(active_hybrid)); for (int index = 0; index < active_hybrid; ++index) { model.hybrid_values[static_cast(index)] = q15(values[position + static_cast(index)]); } position += static_cast(active_hybrid); model.model_scalars.resize(5u); for (int index = 0; index < 5; ++index) { model.model_scalars[static_cast(index)] = q15(values[position + static_cast(index)]); } position += 5u; const std::size_t expected_tail = table_b_start + static_cast(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(index)] = q15(values[position + static_cast(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(profile)] = parsed; } model.profile_tail.resize(8u); for (int index = 0; index < 8; ++index) { model.profile_tail[static_cast(index)] = q15(values[position + static_cast(index)]); } position += 8u; for (int index = 0; index < 3; ++index) { model.post_fields[static_cast(index)] = values[position + static_cast(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