Skip to content

projection: add ambisonic mapping matrices for 6th to 14th order - #492

Open
abhayap wants to merge 1 commit into
xiph:mainfrom
abhayap:hoa-projection-orders-6-14
Open

projection: add ambisonic mapping matrices for 6th to 14th order#492
abhayap wants to merge 1 commit into
xiph:mainfrom
abhayap:hoa-projection-orders-6-14

Conversation

@abhayap

@abhayap abhayap commented Aug 17, 2026

Copy link
Copy Markdown

Channel mapping family 3 already accepts up to 227 channels, but the built-in projection matrices stop at 5th order, so anything above that fails in opus_projection_ambisonics_encoder_create(). Add the missing tables for 6th to 14th order in a new src/mapping_matrix_hoa.c.

These are not built the way the 4th- and 5th-order tables are. Those sample the spherical-harmonic matrix at Fliege-Maier cubature nodes and invert it in the least-squares sense, which is fine to 9th order and then degrades badly: cond(A) goes 3.8 at 9th order, 86 at 10th, 723 at 12th, because those node sets are chosen to be good cubature rules rather than good interpolation points, and the 121-point set is the first with a negative weight. Conditioning is what the demixing step costs in SNR -- per-sector coding noise returns ||A||_F.||A^-1||_F/N times louder in the ambisonic domain -- so extending that construction would give up 15 dB at 10th order and 32 dB at 12th.

Instead these sample at maximum-determinant (Fekete) nodes, which come in sets of exactly (N+1)^2 points chosen to maximize |det Y|, and take the polar factor of the result: the nearest matrix with orthonormal columns. That is energy-preserving ambisonic decoding (Zotter, Pomberger & Noisternig, Acta Acustica 98(1), 2012) at the critically sampled limit L = (N+1)^2. At every order it gives cond(A) = 1, needs no gain field, makes the demixing matrix the exact transpose of the mixing matrix, and holds the Q15 round-trip error at the quantization floor instead of letting it grow with order.

Also:

  • Replace the per-order if/else chains in opus_projection_encoder.c with mapping_matrix_get_ambisonic(), so adding an order is a table entry rather than four more branches in two places.

  • Drop the 65004-octet cap in mapping_matrix_get_size(). That is the most a mapping matrix can be and still fit an Ogg OpusHead, which is a container limit for the muxer to enforce, not an allocator limit; it excludes only 13th and 14th order (78 KB and 103 KB of cells). The 255-channel bound stays.

  • Extend tests/test_opus_projection.c: check that each built-in pair inverts itself to within Q15 (46-64 dB, on a diffuse field), and that a synthetic ambisonic signal -- four tonal sources encoded as plane waves in ACN/SN3D -- survives encode and decode at every order, with and without the non-diegetic channels (~27 dB at 64 kbit/s per channel). The codec figure averages two sets of source directions, because how sparsely four sources happen to land on the sectors moves it by 1-2 dB either way, which is more than the matrices differ by. The synthesis is deterministic and self-contained; no test vectors are needed.

Channel mapping family 3 already accepts up to 227 channels, but the
built-in projection matrices stop at 5th order, so anything above that
fails in opus_projection_ambisonics_encoder_create(). Add the missing
tables for 6th to 14th order in a new src/mapping_matrix_hoa.c.

These are not built the way the 4th- and 5th-order tables are. Those
sample the spherical-harmonic matrix at Fliege-Maier cubature nodes and
invert it in the least-squares sense, which is fine to 9th order and then
degrades badly: cond(A) goes 3.8 at 9th order, 86 at 10th, 723 at 12th,
because those node sets are chosen to be good cubature rules rather than
good interpolation points, and the 121-point set is the first with a
negative weight. Conditioning is what the demixing step costs in SNR --
per-sector coding noise returns ||A||_F.||A^-1||_F/N times louder in the
ambisonic domain -- so extending that construction would give up 15 dB at
10th order and 32 dB at 12th.

Instead these sample at maximum-determinant (Fekete) nodes, which come in
sets of exactly (N+1)^2 points chosen to maximize |det Y|, and take the
polar factor of the result: the nearest matrix with orthonormal columns.
That is energy-preserving ambisonic decoding (Zotter, Pomberger &
Noisternig, Acta Acustica 98(1), 2012) at the critically sampled limit
L = (N+1)^2. At every order it gives cond(A) = 1, needs no gain field,
makes the demixing matrix the exact transpose of the mixing matrix, and
holds the Q15 round-trip error at the quantization floor instead of
letting it grow with order.

Also:

  - Replace the per-order if/else chains in opus_projection_encoder.c
    with mapping_matrix_get_ambisonic(), so adding an order is a table
    entry rather than four more branches in two places.

  - Drop the 65004-octet cap in mapping_matrix_get_size(). That is the
    most a mapping matrix can be and still fit an Ogg OpusHead, which is
    a container limit for the muxer to enforce, not an allocator limit;
    it excludes only 13th and 14th order (78 KB and 103 KB of cells).
    The 255-channel bound stays.

  - Extend tests/test_opus_projection.c: check that each built-in pair
    inverts itself to within Q15 (46-64 dB, on a diffuse field), and that
    a synthetic ambisonic signal -- four tonal sources encoded as plane
    waves in ACN/SN3D -- survives encode and decode at every order, with
    and without the non-diegetic channels (~27 dB at 64 kbit/s per
    channel). The codec figure averages two sets of source directions,
    because how sparsely four sources happen to land on the sectors moves
    it by 1-2 dB either way, which is more than the matrices differ by.
    The synthesis is deterministic and self-contained; no test vectors are
    needed.

Tables generated with AmbisonicArrays.jl.
@abhayap
abhayap force-pushed the hoa-projection-orders-6-14 branch from 8cedc68 to 7b24ed3 Compare August 17, 2026 22:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant