Repository navigation
TrueHD Atmos with heights on Apple TV: our object-to-APAC rendering, offered as a reference #731
Description
What should the engine do?
Keep the heights and object positions of TrueHD Atmos on Apple TV and iPhone. Today the channel bridges decode the 7.1 presentation, so the objects are folded into the floor channels. We decode the Atmos objects instead, render them into a speaker bed, and deliver that bed as Apple Positional Audio (APAC). tvOS sends APAC to an Atmos receiver as Dolby MAT, and iOS plays it as Spatial Audio.
We built this in our fork for Silo. We'd like the engine to have it, maintained in one place, with Silo using it from upstream like any other host. We'd rather you build it your own way than review a large PR from us, so we're offering our implementation as a reference.
Motivating media or use case
UHD Blu-ray remuxes with TrueHD Atmos, which is most of them. On Apple TV, TrueHD can't be bitstreamed, so the bridge is the only route and the heights are lost. Our test title is Dolby's "Unfold" demo (M2TS, H.264, TrueHD Atmos with LFE + 15 objects).
How we implemented it
It's opt-in: LoadOptions.objectAudioRendering = .apac(SpatialSpeakerLayout), default .off. It only applies to TrueHD that FFmpeg marks as Atmos (profile 30), at 48 kHz, on OS 26+, where the APAC encoder API exists. Anything else, or any failure to start, falls back to audioBridgeMode with audio intact.
- Decoder. A new binary dependency, SiloObjectAudio. It wraps the Rust
truehdcrate behind a C API and ships as an xcframework, the same way LibDovi does, so consumers don't need Rust. It decodes the beds, up to 15 objects and their OAMD metadata, with update timing per ETSI TS 103 420 5.3. It also reports where each decoded block sits in the input, so audio stays on the source timeline after a seek. PCM is verified bit-exact againsttruehdd. - Renderer (
Audio/Spatial/, about 1,600 lines of Swift). A constant-power allocentric panner with object size, snap, zone masks and per-update gain ramps. Beds route discretely. Output layouts are 5.1.2, 5.1.4, 7.1.2, 7.1.4 and 9.1.6 in CoreAudio channel order. - Encoder. APAC through
AVAudioConverterat 320 kbps per bed channel, with DRC off. With DRC on, the encoder holds 1.5 to 5+ s of audio before its first packet, which leaves the first segment after a seek silent. The 2048 priming frames stay in the stream, because AVFoundation presents an APAC packet 2048 frames before its timestamp. Trimming them put the audio 42.7 ms ahead of the video. - Muxing. movenc can't write APAC, so the track is muxed as an ALAC stand-in, and the init segment's sample entry is replaced with an
apacentry carrying the encoder'sdapabox. The master advertisesapac.31.LL, because AVPlayer rejects a bareapac. - Route.
SpatialAudioBridgeis tried before the channel bridges.AudioBridgenow sits behind anAudioTranscodingBridgeprotocol so both bridges share the producer lifecycle. - Diagnostics and tooling. The session log reports per-bed-channel levels, object and elevated-object counts, LFE level and gain, the master's audio
CODECSandCHANNELS, and the audio route's rendering mode and channel labels. There's alsoaetherctl serve --atmos-bed <layout> --audio-index <n>.
Our PRs, with the full test plans: Silo-Server#6 (the feature), Silo-Server#8 (the priming/sync fix), Silo-Server#17 (diagnostics).
What's been tested
- Apple TV 4K (2nd gen), tvOS 26.6, into a Sonos Arc over eARC: the Sonos app reports Dolby Atmos.
- Apple TV 4K into a Sony HT-ST5000: a tester reports it sounds nearly perfect, and they can't tell it apart from the title's E-AC-3 Atmos track.
- iPhone on iOS 27 with AirPods: plays with head tracking.
- A/V sync measured on macOS against an ALAC reference, from the start and after seeks: the APAC path now lines up with the reference to within 0.1 ms.
- Unit tests for the panner, renderer, sample entry rewrite, priming and bridge timestamps, plus an opt-in real-content test (
AE_TRUEHD_ATMOS_SAMPLE).
It's a lossy conversion, not passthrough, which is why it's opt-in.
Licensing
SiloObjectAudio is Apache-2.0. The truehd and oamd crates it compiles in are Apache-2.0 as well (copyright Rainbaby and the truehdd contributors). Apache-2.0 can be combined with LGPL-3.0, so we don't see a conflict. The README's license section would need a paragraph like the LibDovi one, because apps have to ship the Apache license and the NOTICE text.
Our fork's engine code is under the engine's own LGPL terms, so you're free to copy, adapt or rewrite any of it. If you want to use SiloObjectAudio, we can re-license our wrapper code (the C API, framer, packaging) under whatever suits you: MIT, like LibDovi's packaging, or MIT OR Apache-2.0. That doesn't change the obligations for the truehd crate inside the binary.
What we're offering
Our implementation as a reference, if you'd like to build this into the engine yourself. We aren't planning to open a PR; the design, the code and how it lands are up to you.
- The engine code, tests and docs in our fork. The PRs linked above explain each piece and the problems we hit along the way: DRC latency, the priming offset, the decoder's wait for a major sync after a seek, and the ALAC stand-in in the init segment.
- SiloObjectAudio. Use it as it is, fork it, or have us transfer the repository to your account. GitHub redirects the old URL after a transfer, and you can rename the repository to match your other packages.
We're glad to answer questions and pass along what testers report. Once the engine has its own version, Silo would drop ours and track the engine's releases.
Is this something you'd want in the engine? If it isn't a fit, that's fine too, and we'll keep it in our fork.
Host app / integration context
Silo (tvOS / iOS client).
The code, tests and docs were written with Claude Opus 5.5 in Claude Code and reviewed by Claude and Codex agents. Hardware testing was done by people.
Activity
superuser404notfound commentedon Oct 10, 2026
Thank you, this is a generous offer and a very well documented one. Yes, I'd like this in the engine.
Here is how I'd go about it:
objectAudioRendering, fallback to the channel bridges on anything unsupported or failing, APAC behind an ALAC stand-in in the init segment, and the priming frames kept.superuser404notfound. I'll rename it to sit next to LibDovi and build the xcframework from source in its own release workflow. Relicensing your wrapper code (C API, framer, packaging) as MIT, matching LibDovi's packaging, would be ideal. The Apache-2.0 notice fortruehdandoamdgoes into the README's license section next to the LibDovi paragraph.I'll post here as the work progresses and again when it ships in a release, so Silo can switch to upstream. I'll probably have questions along the way, most likely about the wait for a major sync after a seek and about the DRC latency.
Quick104 commentedon Oct 10, 2026
Thanks for the quick and thoughtful reply, and for taking this on. Your plan sounds good to us.
The wrapper code (the C API, framer and packaging) is now MIT, and I've sent the transfer request to
superuser404notfound. It should show up in your GitHub notifications or email for you to accept.Once it's with you, please treat it as your own project. Feel free to rename it, restructure it, rewrite parts of it, or take the engine side in a different direction if that fits better with how the bridge works now. Our fork was how we got it working for Silo, so there's no need to stay close to it.
Thanks again!
docs: TrueHD Atmos with heights, and LibTrueHD's license (#731)
fix(audio): object renderer review round 1 (#731)
feat(audio): APAC encoder and the init rewrite that installs its samp…
fix(audio): APAC encoder review round 1 (#731)
feat(audio): TrueHD Atmos keeps its heights, rendered to APAC (#731)
fix(audio): spatial bridge review round 1 (#731)
feat(audio): prefer TrueHD Atmos within a language when rendering is …
chore(deps): LibTrueHD 2.1.0, x86_64 in the iOS simulator slice (#731)
fix(audio): TrueHD Atmos preference review round 1 (#731)
chore(release): prep 7.34.0 - TrueHD Atmos keeps its heights, rendere…
superuser404notfound commentedon Oct 11, 2026
This shipped in AetherEngine 7.34.0 (ed8f8c7). Thank you again, both for the implementation and for handing over the decoder.
What landed, and where it differs from your fork:
LoadOptions.objectAudioRendering = .apac(SpatialSpeakerLayout), default.off, TrueHD Atmos at 48 kHz on OS 26+, falling back toaudioBridgeModeon anything else. It is VOD only for now; a live session keeps the channel bridge.TrueHD, C API unchanged. 2.0.0 is a rebuild from source of your tree (it matched your shipped binary function for function), and 2.1.0 adds x86_64 to the iOS simulator slice, which a generic iOS Simulator build of a host needs. The Silo-Server/SiloObjectAudio URL redirects, so your 1.0.x pin keeps resolving.truehd_atmos_decoder_pullorpushreports a panic, the wrapper resets the handle, and the bridge re-anchors on the next packet instead of appending the rest of the run to the encoder early.CHANNELSin the master counts the bed (12 for 7.1.4) rather than the 2-channel ALAC stand-in; in our codebase it read the muxer's codec parameters.Hardware: Apple TV 4K (3rd gen, tvOS 27.0) into a Sonos soundbar, which reports Dolby Atmos; seeking and end of file checked there. Those sessions ran on the media playlist alone, so the master's
CODECS/CHANNELSpath is covered by tests but not yet by a device. The one thing to expect is cost: a seek past the buffer waits longer than on the channel bridge, since the decode and encode run at roughly 9.5x realtime on Apple Silicon against about 50x for EAC3. Pipelining decode and encode is the obvious next step.Sodalite turns it on by default in its 1.1 release. Happy to hear how it behaves for Silo once you are on upstream.
feat(audio): TrueHD object decoder and allocentric renderer for Atmos (…
perf(audio): encode APAC on its own queue while the next chunk decodes (
fix(audio): APAC pipeline review round 1 (#731)
fix(audio): settle the APAC bridge at keyframes, not inside the cut (#…
fix(audio): settle the APAC bridge only where routing moves (#731)
Merge branch 'perf/apac-encode-pipeline' (#731)
chore(deps): LibTrueHD 2.2.0, TrueHD object decode about 30% faster (#…
chore(release): prep 7.34.1 - TrueHD Atmos seeks about twice as fast (#…
superuser404notfound commentedon Oct 11, 2026
A follow-up in 7.34.1 (cc070bf): the TrueHD Atmos path is about twice as fast, which is what a seek past the buffer waits for.
truehdcrate copied 160-row sample buffers for blocks that are usually 40 samples. That fix is truehdd#42, shipped in LibTrueHD 2.2.0 from a fork until it is merged upstream, with bit-exact output.Dolby's Unfold demo now takes 1.40 s instead of 2.73 s end to end (M1, release build). On an Apple TV 4K into a Sonos soundbar, seeks past the buffer come back noticeably faster and the soundbar still reports Dolby Atmos. If your fork carries the same bridge, both changes should port as they are.
kristofferR commentedon Oct 11, 2026
Check out this fork harletty/truehdd, it includes the same fix ("size a block's sample rows to the block") plus further speed work: no block copying, table-driven Huffman, CRC-8 eight bytes at a time, vectorised prediction filters, whole-block matrices.
superuser404notfound commentedon Oct 13, 2026
Thanks for the pointer. I measured harletty's branch (5c8b880) against the LibTrueHD 2.2.x pin: the decode takes about a third less time again (Unfold 0.98 s to 0.60 s, Dolby's 7.1.4 test file 3.04 s to 2.00 s for 120 s, M1), with bit-identical output and no change to the wrapper.
It has not been proposed upstream yet, and it contains the same fix as truehdd#42, so I've asked harletty there to open it as a PR and will close #42 in its favour. Once it lands upstream, LibTrueHD moves back to an upstream pin.
One thing to set expectations: since 7.34.1 the APAC encoder is the slower stage of the pipeline, so a seek past the buffer gets only a little faster from this. The bigger gain is CPU: the decoder thread does about a third less work, which leaves more room on the Apple TV for everything else running during playback.
chore(deps): LibTrueHD 2.3.0, TrueHD object decode about a third fast…
Merge upstream AetherEngine 7.36.0 (74c81b3) into the Silo fork
docs(audio): the channel bridge, not the engine, drops Atmos objects (#…
feat(audio): LiveTelemetry.objectAudio reports the Atmos objects in play