allure-os: Open Firmware for the Harman Kardon Allure

18 min read #linux #reverse engineering #home automation

Contents

This is part 2 of three posts about the Harman Kardon Allure: the software. Part 1 is the teardown, part 3 the open-source release of software and boards.

Introduction#

Part 1 ended with a plan: a Raspberry Pi Zero 2 W takes the place of the LinkPlay A88 module, and the speaker’s own chips keep their firmware. The Pi then has to do everything the A88 did, and do it the same way. The DAC alone does not make the Allure sound like an Allure, the microphone DSP needs its tuning, and the light rings stay dark until someone streams frames to them.

allure-os is the result: a complete system image for the Pi, built on Debian trixie. It plays music from Music Assistant, AirPlay 2, Spotify Connect, DLNA and Bluetooth, answers to Home Assistant Assist, and shows what it does on the original light ring animations. Voice, control and updates run locally; the speaker needs no manufacturer account and no cloud service.

Three phone screenshots of the setup page: a drawing of the speaker with the track that plays and the volume; the music sources AirPlay, Spotify Connect, DLNA, Bluetooth and the stereo pair; the sound settings with Signature Sound, bass and treble
Setup page: home, music sources and sound settings

Recovering the stock firmware#

To behave like the stock speaker, I first had to know what the stock speaker does. I desoldered the A88’s NAND flash and read it with a programmer: 65,536 pages of 2048 + 128 bytes, 136 MiB raw.

Every attempt to mount it failed. Strings looked garbled, the UBIFS tools refused the image, and my first notes concluded that the root file system was encrypted. It was not. UBI’s 910 erase-counter headers and 314 volume headers all had valid CRCs, and the flash held three UBI devices: the root file system, a staging area for updates and the user data. The garbled strings were simply LZO-compressed UBIFS data.

The UBIFS nodes were the harder part. About 30% of the 160-byte inode nodes failed their CRC, almost exactly the share that crosses a 512-byte boundary, and they failed only when they crossed one. The Amlogic NAND controller does not keep data and ECC apart. It stores each 2 KiB page as four interleaved chunks:

[512 data][2 user][14 ECC] x4  = 2112 bytes  (+64 unused spare bytes)

With that layout most nodes passed, but about 9% of the data nodes still failed: real bit errors in an old flash.

The 14 ECC bytes are a BCH code that corrects 8 bits per chunk, but no standard polynomial or bit order reproduced them. Instead of guessing the variant, I used a property every BCH code has: it is linear, so the ECC bytes are an affine function over GF(2) of the 514 data and user bytes.

  1. I took only chunks that lie completely inside a UBIFS node with a valid CRC, which are known to be free of errors: about 50,000 of them.
  2. I solved ECC = M · d ⊕ c for the matrix M and the constant c with Gaussian elimination over GF(2). Each chunk is one row of 4,113 bits: its 4,112 data bits plus one for c, kept as a Python integer.
  3. A handful of training chunks still had flipped ECC bits, which spoils a single solve. Solving shuffled subsets and keeping, for each output bit, a solution with no error on held-out chunks fixed that. Two of the 112 ECC bits never fit a linear model and are ignored.

With the learned matrix, correction is a table lookup. A flipped data bit i changes the ECC by column i of M. The syndrome, the recomputed ECC XOR the stored one, therefore names the bit; two flipped bits give the XOR of two columns. Tables for one and two flips were enough: after correction every UBIFS node passed its CRC, and the root file system came out complete. Of all chunks, 255,052 were clean, 1,664 needed one bit corrected and 20 needed two. The remaining 5,408 chunks did not fit the learned code; none of them belonged to a file-system node.

The recovery in four scenes:

A 2 KiB page is stored as four chunks of 512 data bytes, 2 user bytes and 14 ECC bytes. Read as plain data, every UBIFS node that crosses a 512-byte boundary picks up 16 foreign bytes and fails its CRC.

What the stock firmware does#

The root file system answered most questions; the overview of how the chips work together in part 1 comes from it. Three details matter for a replacement:

  • The sound is not in the DAC. Harman’s tuning is a 32-bit ARM ALSA plugin, and plain stereo into this hardware would send full-range music to the woofer.
  • The volume is a software volume before the plugin, 33 levels from −60 to 0 dB in steps of about 1.9 dB, with the DAC fixed at 0 dB.
  • The microphones need a short tuning sequence at start-up: gains for the microphone inputs and the boost, sent to the CX20924 over I2C.

A few findings corrected what I thought I knew about the speaker:

  • Alexa never ran on the microphone DSP. The CX20924 can detect a wake word itself, but the Allure’s build of its firmware contains no wake word engine. The wake word ran on the module (Amazon’s Pryon Lite); the CX20924 does beamforming, noise suppression and echo cancellation.
  • The direction is there, but unused. The CX20924 can report the direction of the voice; the stock firmware never used it.
  • The CX20924’s flash image sits in the root file system as dsp.bin, a name that made me expect DAC coefficients. It holds a 20 KiB bootloader and a 384 KiB dual-partition firmware. The image is named for Conexant’s evaluation kits (fcp_evk-galaxy-generic), reports version 7.7.0.32 and carries configurations for other products and factory test modes.

How the stock firmware updates#

LinkPlay’s update server still answered in October 2026. It offers exactly two builds for the Allure, both from 2019; the newer one, from September 2019, is the firmware in my dump. Both carry the same MSP430 and CX20924 firmware versions that my speakers run, so there is nothing newer for either chip. The speaker checks when the app asks it to, or at night if a time for silent updates is set.

  1. 1

    Ask

    The speaker fetches the product list from LinkPlay's server and picks its own entry. The requests carry no device identifiers.

  2. 2

    Download

    Lists and images come over HTTPS.

    Certificate not checked. Whoever sits on the network path can answer in the server's place.

  3. 3

    Unpack

    Lists and images are encrypted.

    One key for all. The key is the same in every Allure's firmware, so it keeps out no one who has a copy.

  4. 4

    Verify

    Each image is compared with a checksum from the same server. The kernel is also checked by the SoC's secure boot.

    Only the kernel is signed. The system, the MSP430 firmware and the CX20924 firmware pass with any matching checksum, and the two chips accept any version that differs from theirs, older ones too.

  5. 5

    Install

    The MSP430 and the CX20924 are flashed at once. After a reboot, the new system is written over the old one.

    A single system. There is no second copy to keep running while the first is replaced.

  6. 6

    Recover

    An interrupted write is probably retried at the next start.

    No way back. A faulty but intact image stays installed.

One update on the stock firmware; red steps leave the speaker unprotected.

Put together, anyone who can answer in place of the server, whether someone on the network path or a later owner of the domain, can install a system of their choosing on a stock Allure that checks for updates. Only the kernel is out of reach. A broken update leaves the speaker without a working system.

Hardware support#

With the stock behavior known, the Pi first needs drivers. The kernel, harman-kardon-allure-kernel, is a fork of the Raspberry Pi kernel 6.18. The PCM5121 works with the standard pcm512x driver. For the CX20924, Conexant wrote a cx2092x ASoC driver for its smart-speaker kits in 2017. I ported it to 6.18 and added a machine driver that runs both codecs, DAC playback and microphone capture, on the Pi’s single I2S bus. A device tree overlay adds the CX20924’s reset and wake-word pins.

The Pi has one full UART, and Bluetooth needs it for flow control and speed. The MSP430 gets the mini UART on GPIO 14 and 15, at 115200 baud.

Sound#

allure-os keeps Harman’s tuning, which the setup page calls Signature Sound. The DSP runs in a player process behind a PipeWire sink, so every source passes through it: music, voice replies and system sounds. There are two engines:

EngineWhat runsCPU at 800 MHz
Native (default)the Harman tuning rebuilt in C for arm64, sample for sample identical to the pluginabout 7% of one core
OriginalHarman’s plugin, unchanged, in its own 32-bit processabout 59% of one core

Native exists because heat matters in a closed speaker (Keeping it running). The original plugin stays as the reference the native engine is tested against.

The volume follows the stock curve. In the same pass, the speaker adds bass and treble, loudness compensation at low volume, a dialogue mode, and a volume limit with a lower limit at night. A sleep timer fades the music out.

Inside Harman’s tuning#

The plugin, allure_home_alsalib_20180305_v1 by its own version string, is a compact piece of DSP. I recovered its parameters by analyzing the plugin and measuring its output, and rebuilt the processing in C as named constants; none of Harman’s code or tables is copied.

The chain starts by summing left and right into mono with 18 dB of headroom: the Allure plays mono, and its 360° sound comes from the drivers around the chassis, not from stereo. Anti-phase content cancels to digital silence, and the output has 16-bit resolution. The stock speaker ran it at 44.1 kHz, while on the Pi the DAC link runs at 48 kHz. Because the plugin computes its filters for the stream’s rate, the response still matches within ±0.1 dB. From there, two paths:

PathFiltersLimiter (make-up gain)Output
Mid/high (right)LR4 high-pass at 300 Hz; +2 dB at 320 Hz (Q 5); +2 dB shelf above 1 kHz+8 dB, −23 dBFS, attack 5 ms, release 800 ms+10 dB
Woofer (left)LR4 high-pass at 35 Hz; +14 dB at 65 Hz (Q 2); LR4 low-pass at 300 Hz; split at 1 kHz into two bandsper band +13 dB, −21 dBFS, attack 10 ms, release 1 shigh-pass 35 Hz, +8 dB

LR4 is a fourth-order Linkwitz–Riley filter. After its limiter, the mid/high path gets two more cuts: −2.5 dB at 500 Hz (Q 1.5) and −2 dB at 2.4 kHz (Q 1.8). All filters are second-order sections, computed with the bilinear transform and run in single precision. The peaking and shelving filters follow the Audio EQ Cookbook with one difference: the plugin takes A = 10^(G/20) where the cookbook uses G/40. Below the limiter thresholds the tuning is linear:

Frequency response of both outputs: the woofer path peaks at 65 Hz about 16 dB above the mid/high level and falls off above 300 Hz; the mid/high path crosses it near 320 Hz and is flat within 1 dB from 700 Hz to 3 kHz
Small-signal response of both outputs; circles are sine measurements at −40 dBFS

The two Linkwitz–Riley slopes cross at about 320 Hz, at −4 dB each. The woofer gets 16 dB more than the mid/high level at 65 Hz: that is the bass of a small enclosure, bounded by a steep 35 Hz high-pass that keeps the driver’s excursion in check. The mid/high path stays within ±1 dB from 700 Hz to 3 kHz, with a gentle dip at 2.4 kHz, and rises to +2 dB above 5 kHz.

Stage-by-stage response of the woofer and mid/high paths, showing how each filter adds to the final curve
Response of each path, stage by stage

The limiters make the tuning depend on the level. They are peak limiters with a 20 ms lookahead, a 2.3 ms hold and a second-order smoothing filter on the gain, so the gain changes smoothly and adds little distortion:

Output level against input level for sine tones: linear below the threshold, flat above it for both paths
Static limiter curves: a hard knee at an infinite ratio
Frequency response of the woofer and mid/high outputs at four input levels: the 65 Hz bass peak shrinks as the level rises until the woofer band is flat
Response at −40, −20, −6 and 0 dBFS input

At low levels the full bass boost plays. As the level rises, the limiters first take back the 65 Hz peak, by 29 dB at full scale, then the whole woofer band, which ends up flat. The mid/high band compresses evenly and keeps its shape. The limits are −13 dBFS for the woofer, reached from −30 dBFS of input at 65 Hz, and −14.5 dBFS for the mid/high path. When a 100 Hz tone steps from −6 back to −40 dBFS, the 1 s release brings the quiet tone back about 16 dB down, and it takes about 3 s to recover.

This is how a small speaker plays loud without overloading its woofer, and it is why the volume has to be applied before the DSP. My first version used the PCM5121’s own digital volume instead, in 0.5 dB steps down to −103 dB. Spread over that range, everything below 50% was inaudible, and because the volume sat behind the limiters, 95% distorted.

The native engine reproduces the plugin bit for bit. A test suite runs sweeps, clicks, tone bursts, noise, square waves, anti-phase signals and a music-like signal through both engines, with the plugin in an ARM emulator, and compares them sample by sample: 0 differing samples in 24.7 million frames per build. Two details decide whether that holds. The C code must be built without fused multiply-add (-ffp-contract=off); with it, 31% of the samples differ by up to 56 LSB on the woofer, whose 300 Hz low-pass has very small coefficients. And the math library must return the same tan, pow, exp and other results as the plugin’s; Debian trixie’s glibc 2.41 does, on armhf, arm64 and x86-64.

Music#

Music comes from several sources:

  • Music Assistant over Snapcast, in sync with other players in the house. The speaker measures its own output delay while music plays, about 60 to 110 ms including the DSP’s 20 ms lookahead, and applies a new value only between songs.
  • AirPlay 2 with shairport-sync, Spotify Connect with go-librespot, and DLNA.
  • Bluetooth with AAC, through PipeWire.
  • Two Allures as a stereo pair, left and right, with one acting as the source for the other.

Two problems were not about audio at all. The Pi has no real-time clock, so the speaker started hours or days off. When NTP corrected the clock, Snapcast’s synchronization did not survive the jump and dropped every chunk. allure-os now keeps the last known time on the data partition and starts Snapcast after the first sync. The stereo pair first sent raw PCM between the speakers, 1.5 Mbit/s even in silence. On 2.4 GHz Wi-Fi that pushed the ping to the router from 81 ms to 330–1400 ms and crashed the follower’s Snapcast client about once a minute. FLAC, and a stream that runs only while something plays, brought the idle traffic down to 17 kbit/s.

Voice#

The CX20924 still does the hard part. It forms a beam from the four microphones and cancels the speaker’s own music, so the Pi gets one clean voice signal over I2S. On the Pi, Linux Voice Assistant, an ESPHome-protocol Assist satellite, listens for the wake word, streams the request to Home Assistant and plays the reply.

After a wake word, the speaker reads the direction of the beam from the CX20924 and shows it on the top ring; calibrating that direction to the speaker’s front is still open. While a reply plays, music is ducked. Because the CX20924 already removes the speaker’s own output from the microphones, the wake word stays live during a reply: saying it again stops the reply and starts a new request. The amplifier switches on with the wake word, so a reply does not lose its first syllable.

The same microphone signal feeds a sound detector. Smoke and CO alarms are recognized by their standard beep patterns, without a model. Other sounds, like breaking glass, a dog or the doorbell, go through EfficientAT’s DyMN-04 audio classifier, but only when something louder than the room’s noise floor happens, so a quiet room costs about 1% of a core. Only the names of the detected sounds leave the process; no audio is stored or sent.

Light and buttons#

The light rings run the original animations from the stock firmware: listening, thinking, speaking, Wi-Fi setup, alarms and updates. On top of that, the rings can show a light of their own: a solid color, breathing, a comet, a rainbow, or a music mode where the bottom ring follows the bass and the top ring the mids and highs. Home Assistant gets one more effect for wake-up automations: a 20-minute sunrise from deep red to warm white. Every frame goes to the MSP430 as described in part 1. One detail took a while: the MSP430 treats the four bytes AXX+ as the start of a new command wherever they appear, so a frame whose colors happen to spell them would be cut short. allure-os changes the + (0x2B) of such a run to 0x2C, one step in one color channel, which nobody can see.

The buttons work as before. The volume ring has the stock 33 levels, the microphone button switches microphone privacy, and the action button talks to Assist. Double press and hold are free for Home Assistant automations.

Setup and Home Assistant#

There is no app. A new speaker opens a Wi-Fi network named HK-Allure- and the last four characters of its MAC address. A phone that joins it gets the setup page, picks the home network, and the speaker switches over. From then on the same page is at the speaker’s address on the network. A change to the network settings rolls back by itself if the speaker cannot reach the network afterwards. Part 3 has the steps for a new speaker.

In Home Assistant the speaker appears as one device, over MQTT discovery: volume and media, the sound settings, the light, the sound detection, diagnostics and the software update, grouped into the sections of the device page. The voice assistant joins the same device through the ESPHome integration.

A separate developer page shows the internals: the sound pipeline as a graph, the Signature Sound engine, a Sound lab for features that are still being tuned, the microphones, Wi-Fi, services, both system slots, logs and crash reports.

The developer page on a desktop browser: the sound pipeline graph, the Signature Sound engine switch, the Sound lab with Deeper Bass and Room correction, and the system sounds
Developer page

Keeping it running#

The Zero 2 W’s Wi-Fi chip is the weak spot, and heat makes it worse. The Pi sits in the closed top section of the speaker, next to the amplifier board, with no airflow. When it ran hot, the Wi-Fi chip started to throw SDIO errors and then dropped off the network. For a speaker that is as bad as a failure gets: it has no Ethernet port and no display, so without Wi-Fi it can neither play nor be reached to find out why.

So allure-os keeps the Pi cool. It caps the CPU at 800 MHz and drops it to 600 MHz when nothing plays and nobody talks. After 10 s of digital silence, the native DSP engine writes zeros instead of computing them; the output stays bit-identical. Without these measures, the idle speaker used about 12% CPU at 68 °C.

Wi-Fi#

The Zero 2 W has 2.4 GHz Wi-Fi only; the A88 had 5 GHz as well. Under load the chip’s firmware also crashed (“Firmware has halted or crashed”, followed by SDIO timeouts). allure-os turns off the firmware features the crashes are attributed to, among them its own WPA handling, and leaves the handshake to wpa_supplicant. It also turns off Wi-Fi power saving, which had made voice and music stutter. A watchdog checks the router every 10 s; after 60 s without an answer it power-cycles the chip by re-binding the SDIO controller, and if the network is still gone 90 s later, it reboots. The hardware watchdog would not catch this, because the rest of the system keeps running.

Start-up#

Counted from the start of the kernel, music plays after about 41 s and the voice assistant is ready after about 49 s. Most of that came from ordering: the lights, buttons and local sound no longer wait for the network, the time sync starts as soon as Wi-Fi is up instead of after a 30 s retry, and the CPU runs at full clock until the voice assistant is up.

Updates and storage#

A speaker gets its power pulled without warning, and SD cards wear out. allure-os is built around both, and around the weaknesses of the stock update chain.

The SD card has two system slots, each mounted read-only, and a data partition for settings. /var and the logs live in RAM; the data partition is written only when a setting changes. In normal operation the only file written is the clock file; starting the voice assistant writes about 800 KB once per boot.

An update goes into the slot that is not running, boots once as a trial, and becomes the default only after a health check passes within three minutes. If it fails, or the power goes out halfway, the speaker boots the version that worked.

Updates are signed RAUC bundles, published as releases on my Git server and offered in Home Assistant and on the setup page. The speaker streams the bundle instead of storing it, and fetches only the 4 KiB blocks that changed: one update fetched 6.3 MB of a 228 MB bundle. The build ships the block indexes of both slots, so the first update on a new card does not spend 14 minutes indexing both 4 GB slots. Automatic updates, off by default, install between 3 and 5 at night when nothing plays.

Conclusion#

The Allure sounds like it did on the stock firmware, because it runs the same tuning, and it hears like it did, because the CX20924 still does the beamforming and echo cancellation. What changed is everything around it: a local voice assistant instead of Alexa, current streaming receivers, a setup without an app, and updates that fall back to the last working system.

Part 3 is the release: the code, the kernel, and the boards that let anyone do this to their own Allure.

Sources#

allure-os

Audio and DSP

  • S. Linkwitz, “Active Crossover Networks for Noncoincident Drivers”, JAES 24(1), 1976
  • R. Bristow-Johnson, “Audio EQ Cookbook”, W3C: https://www.w3.org/TR/audio-eq-cookbook/
  • Texas Instruments, “PCM512x” datasheet SLAS763C (digital volume range): see part 1

Software used

cd ~/projects