Emulate Engine OS and other InMusic OSes inside QEMU
NOTE: Docs are currently awful, they will be better eventually. Basic docs for setting up arn64 emulation are here.
- BUILDING.md — building/assembling and booting an Engine OS image under QEMU
- ENGINEOS.md — Engine OS internals, product spoofing, known limitations
- BLOCKING_TELEMETRY.md — stopping Engine's crash/analytics reporting from reaching InMusic's real Sentry project
- BUILD_ARM64.md / BUILD_ARMV7_ENGINE.md / BUILD_MPC.md — what each target needs
- INSTANCES.md — running several emulated devices side by side
- MACOS_VIRGL.md.md — Build QEMU w/ Virgl on macOS
Prerequisites: docker, qemu-system-aarch64 and qemu-system-arm, binwalk
3.1.x, e2fsprogs (for dumpe2fs, debugfs, resize2fs), file.
# 1. One instance per device + firmware version. Point --firmware straight at an
# update image; there is no firmware directory to populate. The matching kernel
# is detected from the rootfs and built on demand -- once per architecture,
# then reused (~7 min arm64, ~2 min armv7, ~1 min per rootfs).
scripts/build_scripts/new_instance.sh --name rmz2-4.6.0 --device engine \
--firmware /path/to/SYSTEMONE-4.6.0-Update.img
# One image usually serves several products, so --product-code picks which one
# the guest claims to be. It is optional -- each builder has a default -- and it
# is recorded in instance.env, so a later --force rebuild keeps it.
scripts/build_scripts/new_instance.sh --name mpc-3.9.1 --device mpc \
--product-code ACV5 --firmware /path/to/MPC-3.9.1-Gen1-update.img
# Engine OS also ships on armv7 (the RK3288 Prime/SC/Mixstream controllers). It is
# the same --device engine, on a different architecture; which builder and disk
# layout it needs is read off the firmware rather than asked for.
scripts/build_scripts/new_instance.sh --name jp07-5.0.4 --device engine \
--firmware /path/to/JP07-JP08-JP11-5.0.4.img
# 2. Boot. Each instance owns its disks and its host ports, so these run at the
# same time without interfering.
scripts/qemu/run_instance.sh --name rmz2-4.6.0
scripts/qemu/run_instance.sh --name mpc-3.9.1
scripts/qemu/run_instance.sh --list
# Rendering goes to the host GPU through virgl by default, where QEMU can serve it.
# --no-gl rasterizes on the guest CPU instead, for when GL is what you are ruling out.
scripts/qemu/run_instance.sh --name rmz2-4.6.0 --no-glThe serial console appears in the terminal you launched from and root has no password.
Build one architecture at a time. Both pipelines use the same debian:bookworm
and debian:trixie tags at different architectures, and Docker caches a tag at only
one architecture at a time, so building an arm64 and an armv7 target concurrently
will fight over them. Sequentially is fine — each build pulls what it needs.
Picking the right firmware. --device mpc refers to the armv7 / RK3288 MPC family
(product codes ACV*). Akai also ships an arm64 "Gen 2" MPC image; that one belongs
to a different pipeline, and the build now rejects it up front rather than producing
an image that panics partway into boot.
In the MPC guest, failing units for LoadPin verity trustpoints and XMOS USB audio
firmware are expected: none of that hardware exists here. AZ0x system info logging
also fails, but for a different and fixable reason — it runs az0x-info, which reads
the faked devicetree, and only acvs.service carries the dtshim preload that makes
that readable. Run it by hand as LD_PRELOAD=/root/dtshim.so az0x-info to see what
the application itself resolves.
| 4.4.0+ (arm64) | 5.0.0+ (armv7) | 4.3.0 | |
|---|---|---|---|
| Rootfs Extraction | Y | Y | Y |
| Engine | Y | Y | Y |
| SoundSwitch | Y | ? | N |
| Display | Y | Y | Y |
| Virgl | Y | Y | N |
| Fake Touch | Y | Y | Y |
| Keyboard Navigation | Y | ? | Y |
| Audio Playback | Y | N | N |
| MIDI | Y | Y | N |
| External Media (USB/SD) | Y | Y | N |
The Code column is the inmusic,product-code devicetree value, which is what
actually selects the device — one firmware image usually serves several products and
/usr/Engine is shared across them, so the code is the only thing distinguishing
them (see ENGINEOS.md). Version cells
hold the exact firmware version booted, ? for untested, – where that major
version was never released for the device.
| Code | SOC | Signed FW | Arch | 1.x | 2.x | 3.x | 4.x | 5.x | |
|---|---|---|---|---|---|---|---|---|---|
| Denon DJ Prime 2 | JC16 | RK3288 | N | armv7 | ? | ? | ? | ? | ? |
| Denon DJ Prime 4 | JC11 | RK3288 | N | armv7 | ? | ? | ? | 4.3.0 | ? |
| Denon DJ Prime 4+ | JC11S | RK3288 | Y | armv7 | – | – | ? | ? | ? |
| Denon DJ Prime GO | JP11 | RK3288 | N | armv7 | ? | ? | ? | 4.3.0 ! | ? |
| Denon DJ Prime GO+ | JP11S | RK3288 | Y | armv7 | – | – | – | ? | ? |
| Denon DJ SC5000 Prime | JP07 | RK3288 | N | armv7 | ? | ? | ? | ? | 5.0.4 |
| Denon DJ SC5000M Prime | JP08 | RK3288 | N | armv7 | ? | ? | ? | ? | ? |
| Denon DJ SC6000 Prime | JP13 | RK3288 | N | armv7 | ? | ? | ? | ? | ? |
| Denon DJ SC6000M Prime | JP14 | RK3288 | N | armv7 | ? | ? | ? | ? | ? |
| Denon DJ SC Live 2 | JP20 | RK3288 | Y | armv7 | – | ? | ? | ? | ? |
| Denon DJ SC Live 4 | JP21 | RK3288 | Y | armv7 | – | ? | ? | ? | ? |
| Numark Mixstream Pro | NH08 | RK3288 | Y | armv7 | – | ? | ? | ? | ? |
| Numark Mixstream Pro+ | NH08S | RK3288 | Y | armv7 | – | ? | ? | ? | ? |
| Numark Mixstream Pro GO | NH10 | RK3288 | Y | armv7 | – | – | ? | ? | ? |
| RANE SYSTEM ONE | RMZ2 | RK3588 | Y | arm64 | – | – | – | 4.5.0-4.6.0 | 5.0.4 |
Same shape as the table above. Code is again the inmusic,product-code value,
read here from the model and inmusic,product-code properties of the
/boot/rk3288-*.dtb files the firmware ships. Version cells hold the exact firmware
version booted, ? for untested. The Gen 1 MPCs all ship from one image covering
eight models, so a version column is the image version.
| Code | SOC | Signed FW | Arch | 2.x | 3.x | |
|---|---|---|---|---|---|---|
| MPC X | ACV5 | RK3288 | 3.x only | armv7 | ? | 3.9.1 |
| MPC X SE | ACV5S | RK3288 | 3.x only | armv7 | ? | 3.9.1 |
| MPC Live | ACV8 | RK3288 | 3.x only | armv7 | ? | 3.9.1 |
| MPC Live Mk 2 | ACVB | RK3288 | 3.x only | armv7 | ? | 3.9.1 |
| MPC One | ACVA | RK3288 | 3.x only | armv7 | ? | 3.9.1 |
| MPC One+ | ACVA2 | RK3288 | 3.x only | armv7 | ? | 3.9.1 |
| MPC Key 61 | ACVM | RK3288 | 3.x only | armv7 | ? | 3.9.1 |
| MPC Key 37 | ACVR | RK3288 | 3.x only | armv7 | ? | 3.9.1 |
The MPC build spoofs a product code the same way the Engine builds do: PRODUCT_CODE
(or --product-code) selects it, and the RK3288 dtshim is preloaded into
acvs.service so MPC reads it. MPC resolves it through libaz0x-info and names
itself accordingly — ACV5 boots to a rendered UI that identifies as MPC X and shows
its firmware version, which is why that cell carries no *.
Two rows of MPC's own info screen stay unresolved on every model: the serial number and the control surface. Neither comes from the devicetree — both come from the control-surface MCU over USB at fixed bus positions, which nothing here emulates yet.
Signed FW is a version boundary here, not a per-model one. Every 2.x image
begins d00dfeed — a DTB wrapper around the whole file, which binwalk alone
cannot unpack, so those need mpcimg first. Every 3.x image begins 415a3031
(ASCII AZ01), the signed container binwalk reads directly. That is also why the
2.x column is untested: new_instance.sh cannot ingest those images as they stand.
The latest 2.x is 2.15.1.1.
All eight models appear in both majors, joining at different points — ACV5 and
ACV8 from 2.2.0, ACVA/ACVB from 2.7.2, ACVM from 2.10.0, ACV5S from
2.11.1, ACVA2 from 2.11.7, ACVR from 2.13.0.14 — which is visible in the image
filenames, each listing the codes it serves.
One caveat on names: ACV5 and ACV8 carry generic InMusic MPC ACV5/ACV8 model
strings rather than marketing ones, so the split of those two specifically between
MPC X and MPC Live follows
BUILD_MPC.md's product list rather than the
firmware. The other six name themselves outright (Akai Professional MPC One, and
so on). The az01/az05 platform split the dtb filenames show is dropped here to
keep the columns parallel with the controllers table; ACVA2, ACVR and a second
ACVM variant are az05, the rest az01.
The same az0x base distribution carries several product lines this project has
no rootfs builder for yet — new_instance.sh rejects them, since it matches on
/usr/Engine and /usr/bin/MPC and these have neither. Recorded here because the
firmware is in the same archive and the extraction recipe is the same:
| Line | Codes | Notes |
|---|---|---|
| Akai Force | ADA2 |
Standalone groovebox, closest sibling to the MPC line |
| Akai MPC Gen 2 | MPC-GEN2 |
arm64 — a different pipeline; the MPC build rejects it up front |
| Akai MPC Sample | AC50 |
|
| HeadRush | HV01 (Core), HV03 (VX5), HG02 (Gigboard), HG03 (Looperboard), HG04 (MX5), HG06 (Prime), HG12 (Flex Prime), MG01 (Pedalboard) |
Guitar FX/amp modellers |
| Alesis drum module | LDMD, LDMF |
Shipped as drummodule-*-update.img |
"Evil" is HeadRush. The HeadRush application binary is built around a namespace
called Evil — EvilApp, EvilGui, EvilClient, EvilDB, EvilAPI,
EvilAppRunner, Evil::BoardModes — with its database tables named for
amp-modeller concepts (RigDescriptorsList, SetlistDescriptorsList). The binary
also identifies itself as headrushfx. It is an internal codename, not a separate
product line: anything labelled "Evil" in an extracted rootfs is the HeadRush
application. Confirmed against HV01 (HeadRush Core).
"Looper" is the Looperboard. HG03 is the HeadRush Looperboard, and
LooperController lives inside that same Evil binary — the looper is a mode of
the shared HeadRush application rather than its own codebase, which is why one
firmware lineage covers both the pedal-style units and the Looperboard.
| Denon DJ Prime 4 | RANE SYSTEM ONE |
|---|---|
![]() |
![]() |
![]() |
![]() |



