A from-scratch Linux kernel ALSA driver for the MOTU MicroBook mk1 USB audio
interface (USB 07fd:0004). This is a custom (non-UAC) device, so the
stock snd-usb-audio cannot drive it; this module implements the vendor
control protocol and isochronous audio transport directly.
Everything here is grounded in static RE of the macOS MOTUMicroBookAudio.kext
plus live USB captures (../re/ANALYSIS.md).
- USB probe/disconnect, claims interface 0.
- Control channel over interrupt EP1 IN/OUT — framed quadlet read/write (matches the captures byte-for-byte).
- ALSA PCM: 1 playback (2ch) + 1 capture (4ch) substream, both tested on MicroBook mk1 hardware.
- Format:
S24_3LE(24-bit signed packed little-endian). - Sample rates: 44100 and 48000 Hz, both tested on hardware.
- Isochronous audio URBs (EP2 IN/OUT); 10-frame playback cycle for 44.1 kHz
(
45 + 9×44samples), constant 48 samples/frame for 48 kHz. - Low latency at 48 kHz: one-frame playback URBs and a tunable queue depth
(
queue_ms), ~15 ms round trip measured with PipeWire (see “Latency”). - Stream start: disable on alt 0, switch to alt 1, enable at target rate on alt 1 (matches pcap2 sequence).
- Half-duplex guard for hub transaction translator crackling
(
allow_duplexmodule parameter, default off). - Full duplex on direct connections: sample rate is locked while both substreams are open, so a rate change can never reboot the device underneath the other running stream.
- PipeWire/Cinnamon desktop integration (see
../contrib/README.md). - Fixed output routing: playback always reaches the phones and line jacks; the Main Out fader is pinned at unity (see “Output routing”).
- Mixer controls: Headphone / Line / PCM volumes, mic trim,
phantom power, pad, guitar trim (via ALSA mixer / alsamixer), with
alsactl-safe values (see “Saved mixer state”). - Hardware direct monitoring: per-input zero-latency monitor levels (see “Direct monitoring”).
- debugfs register probe for further DSP reverse engineering (see “Register probe”).
- 88.2 / 96 kHz — not possible on mk1 (full-speed USB; endpoint max packet sizes only fit 48 kHz at full channel count).
- CueMix GUI application.
Kept because these failures were intermittent and the diagnoses were not obvious — if something similar reappears, this is the trail to follow.
Status: fixed. First configuration to survive a soft reboot and a disconnect/reconnect cycle back to back. Two earlier diagnoses were declared resolved on the strength of a few quiet boots and came back both times, so the trail is kept below.
Root cause (current). The driver used to raise snd_pcm_stop_xrun() when a
URB completed with -ESHUTDOWN/-ENOENT while running was still set, on the
theory that the URBs had been killed behind its back. But ALSA kills URBs as
part of its own recovery, via sync_stop — so an xrun from that path made
every recovery trigger another recovery. The audio server logged tens of
thousands of recover from error state cycles, and the stream never delivered a
period.
Clearing running in sync_stop to dodge that made it worse in a different
way: the completion handler stops resubmitting when running is false, so the
stream delivered exactly one round of URBs (n_active × period, visible as the
hardware pointer stalling at exactly 192 frames) and then died.
Both are now removed. A killed URB is treated as an ordinary stop again, and
sync_stop does not touch running — ownership of that flag belongs to
trigger, prepare and hw_free. Rate changes, the other way URBs can be killed
underneath a running stream, are prevented at source by the rate lock.
A third mechanism in the same family broke reconnects rather than boots. On a
failed re-arm the driver flagged the control IN URB dead and resubmitted it
before the next command — but -EPERM there only means usb_kill_urb() is
retiring the URB, an ordinary teardown. Resubmitting an URB that is still active
trips a URB submitted while active WARN and returns -EBUSY permanently, so
every later command failed and the device never came back from a replug. Also
removed; the handler now logs a genuine failure and does nothing else.
The earlier diagnosis was wrong. A stale a->substream in motu_pcm_close()
was a genuine latent bug and is still fixed, but it was not the cause — the fix
and the counter that would have tested it shipped together, so the theory was
never actually verified before being declared resolved.
The pattern. All three were speculative recovery added for a failure that had not been characterised, and each caused a worse failure than the one it guarded against. What actually found them was the logging and the debugfs counters, not the recovery logic. Prefer making a failure visible over trying to paper over it.
Symptom. After a cold boot, capture sometimes delivered digital silence to
every client: a DAW showed no input, and a recording measured an exact zero peak
on all four channels rather than a noise floor. Playback was unaffected, and
re-opening the PCM (reloading the module, or any exclusive arecord) cleared it
until the next boot.
Cause. motu_pcm_close() cleared a->substream unconditionally, without
checking which substream was closing. A close that landed after the next open —
which happens when the audio server renegotiates and reopens the device — wiped
the pointer belonging to the new stream. The URB completion handler then
reported every period against a stale substream, so ALSA never saw the capture
pointer advance.
How it was found. The debugfs counters made it arithmetic rather than guesswork:
silent packets 0 of N— the device was sending real audio, not zeros, which ruled out the DSP and the USB capture routing (../re/cuemix_registers.md§18).errors 0 empty 0 invalid 0with packets climbing at ~1000/s — the transport was healthy.bytes == packets × 576exactly — every packet reachedmotu_audio_copy_in(); nothing was being skipped.period_acccycling to 0 — periods were crossing, sosnd_pcm_period_elapsed()was firing thousands of times.- ALSA's
hw_ptrnevertheless sat at 192 frames (exactly two periods) and then reset to 0.
Thousands of period notifications against two that ALSA acted on can only mean two different streams.
If it recurs, capture this before reloading anything — the fault clears the moment the device is reopened:
sudo cat /sys/kernel/debug/motu-microbook-*/streams
grep -E '^state|^hw_ptr|^appl_ptr' /proc/asound/MOTU/pcm0c/sub0/status
journalctl -k -b | grep -i microbookperiod_calls against ALSA's hw_ptr is the discriminator: many calls but a
stalled hw_ptr means the notifications are going to the wrong stream again;
few calls means the completion handler stopped signalling, which is a different
bug in the accounting path. A URBs killed externally, signalling xrun line in
the kernel log would point at a third case — something re-running prepare() in
a loop.
cd driver
make # builds motu-microbook.ko against the running kernelRequires linux-headers matching the running kernel.
make install puts the module in /lib/modules/<running-kernel>/updates/, which
a kernel upgrade discards — leaving the device dead until you rebuild by hand.
DKMS rebuilds it for every kernel that gets installed:
cd driver
sudo make dkms-installThat copies the sources (not the build artefacts) to
/usr/src/motu-microbook-<version>/, registers them with DKMS, builds, installs
and runs depmod. The module lands in the same /updates directory make install uses, so the options and softdep in
../contrib/modprobe.d/motu-microbook.conf keep working
unchanged, and udev autoloads the driver when the device appears.
If you previously used make install, remove the stale copy first so it cannot
shadow the DKMS build:
sudo rm -f /lib/modules/$(uname -r)/updates/motu-microbook.ko*Check and remove:
dkms status -m motu-microbook
sudo make dkms-uninstallBump PACKAGE_VERSION in dkms.conf when the driver changes meaningfully; the
Makefile reads the version from there, so nothing else needs editing.
The device's interface class is vendor-specific (0xFF), so snd-usb-audio
will not grab it. Simply load the module:
sudo modprobe snd_pcm # dependencies
sudo insmod motu-microbook.ko
# After plugging in the MicroBook:
dmesg | tail # look for "MOTU MicroBook mk1 attached"
aplay -l # should list the card
arecord -l
# Playback test (2ch, 44.1 kHz):
speaker-test -D plughw:CARD=MOTU -c 2 -r 44100 -f 440 -t sine -p 5
# Playback test (2ch, 48 kHz):
speaker-test -D plughw:CARD=MOTU -c 2 -r 48000 -f 440 -t sine -p 5
# Capture test (4ch, 44.1 kHz):
arecord -D hw:CARD=MOTU,DEV=0 -f S24_3LE -r 44100 -c 4 -d 5 cap.wav
# Capture test (4ch, 48 kHz):
arecord -D hw:CARD=MOTU,DEV=0 -f S24_3LE -r 48000 -c 4 -d 5 cap48.wav
# Unload:
sudo rmmod motu_microbook| Parameter | Default | Description |
|---|---|---|
index |
0 | ALSA card index |
id |
(auto) | ALSA card id |
enable |
yes | Enable MOTU MicroBook card |
allow_duplex |
0 | Allow simultaneous capture + playback. Set to 1 for direct USB connection (no hub). Concurrent IN+OUT isochronous traffic through hub transaction translators can exceed TT buffer capacity, causing playback crackling with zero USB errors. |
auto_reset |
1 | USB-reset the device when the control channel stops answering. The device can wedge in a state that refuses every control command and survives a warm reboot, because rebooting does not cut USB bus power. Rate-limited to one reset per 30 s. Unverified — the wedge has not recurred since this was added, so it has never actually fired; set to 0 to disable and recover by replugging instead. |
queue_ms |
4 | Milliseconds of audio kept queued in USB URBs (playback read-ahead). Must stay below what the client keeps buffered — see “Latency”. Too low crackles from USB starvation, too high crackles from reading past the client. Writable at runtime; takes effect on the next stream start. |
# Direct USB connection — enable duplex:
sudo insmod motu-microbook.ko allow_duplex=1
# Through a USB hub — half-duplex (default):
sudo insmod motu-microbook.koThe host sends framed, big-endian command packets
[seq, opcode, b2, b3, addr_be, data_be...] on EP1 OUT and receives
[seq, opcode, ...] responses on EP1 IN. A WriteQuad(0x0b14, v) enables or
disables audio. Unsolicited packets (opcode 0) push async register changes.
Register 0x0b14 serves as both disable/enable and rate selector. The meaning
depends on the current alt setting:
| Code | Alt 0 (idle) | Alt 1 (streaming) |
|---|---|---|
| 0 | disable | enable at 48 kHz |
| 1 | — | enable at 44.1 kHz |
The boot sequence matches pcap2: disable (0x0b14=0) on alt 0, switch to alt 1, then enable at the target rate on alt 1. The enable must happen on alt 1 because 0x0b14=0 means "disable" on alt 0 but "enable at 48 kHz" on alt 1.
1 USB frame/ms. Capture = 4ch (EP2 IN, max 576 B), playback = 2ch (EP2 OUT, max 288 B). Samples are 24-bit signed LE packed (3 B/sample).
At 44.1 kHz, playback uses the capture-confirmed ten-frame cycle: one 45-sample
packet followed by nine 44-sample packets (270 + 9×264 = 2646 bytes per URB).
At 48 kHz, every frame carries exactly 48 samples (288 B OUT, 576 B IN — the
endpoint maximums).
Capture uses 2-frame URBs (matching the Windows pcap2 trace: 1056/1068-byte
aggregates every 2 ms). Eight URBs are allocated per direction; how many are
kept in flight follows queue_ms (see “Latency”).
Output latency has two parts, and only one of them is visible to your DAW.
The ALSA period, which the client picks within the driver's constraints. The
driver's period_bytes_min is one URB's worth of bytes — 1 ms for playback and
2 ms for capture at 48 kHz. (It was previously a flat 4096 bytes, which pinned
PipeWire to a 683-frame — 14 ms — playback period no matter what the DAW asked
for.) PipeWire adds a period of headroom for batch devices, so its contribution
is roughly twice the period.
The URB queue (queue_ms), which your DAW cannot see. Playback data is
copied out of the ALSA ring when an URB is queued, not when it plays, so the
queue is read-ahead. It is not an independent latency term: the client
refills to its own target, so total latency stays at that target and the queue
merely decides how much of the wait happens in an URB rather than in the ring.
What queue_ms does control is robustness, in both directions:
- Too small and the USB schedule can run dry between completions — crackling with zero USB errors and zero underfills.
- Too large and the driver reads past the client's write pointer, emitting
silence (counted as
underfills) — crackling that does show in the counter.
So queue_ms must sit below what the client keeps buffered, and the two cannot
be chosen independently. Measured on this hardware at 48 kHz with PipeWire:
| Quantum | ALSA period | Playback fill (min) | Usable queue_ms |
Round trip | |
|---|---|---|---|---|---|
| 512 | 256 | ~1056 fr | 4-8 | ~37 ms | |
| 256 | 128 | ~345 fr | 4 | ~15 ms | recommended |
| 128 | 64 | ~128 fr | 2 | ~10 ms | marginal — see below |
At quantum 128 the margin between the fill floor (128 frames) and the smallest
possible read-ahead (96 frames, two 1 ms URBs) is about 0.7 ms. That has run
clean for a session and has also collapsed into an unrecoverable playback
underrun loop — snd_pcm_start: Broken pipe cycling against
recover from error state, with no output at all until the quantum is raised.
Treat 256 as the working setting.
The read-ahead is queue_ms × 48 frames at 48 kHz, and it must stay under the
minimum fill, not the average. At quantum 128 the fill bottoms out at 128
frames: queue_ms=2 (96 frames) runs clean, while queue_ms=3 (144 frames)
produced 7135 underfills and audible crackling. Lower the quantum to cut
latency, then lower queue_ms until it fits under the fill.
Going below 128 frames of read-ahead is not possible: the driver keeps a minimum of two URBs in flight, which is 96 frames at 48 kHz.
For the full 128-frame setup — including the host CPU tuning it requires — see
“Recipe: 128 frames” in ../contrib/README.md.
The URB length is chosen per rate. At 44.1 kHz it must stay at ten USB frames to
reproduce the 45 + 9×44 clock cycle (see “Audio transport”), so the queue is
coarse and queue_ms rounds up to a multiple of 10 ms. At 48 kHz every frame
carries exactly 48 samples, there is no cycle to preserve, and the driver uses
one-frame URBs — so the queue is settable in 1 ms steps, and low latency is only
really available at 48 kHz.
Monitoring through the DAW means paying the round trip (capture + graph +
playback). Monitoring through the hardware (see “Direct monitoring”) costs only
converter latency, but bypasses your plugins. When you switch to DAW monitoring
for plugins, set Guitar Monitor Volume to 0 so the dry hardware signal does
not double the processed one.
To check what you actually got, with a stream running:
sudo cat /sys/kernel/debug/motu-microbook-*/streamsDo not raise
queue_msbeyond what the client keeps buffered. The driver queues ahead of the hardware pointer; if that gets ahead of the client's write pointer, it sends ring data the client has not written yet. Compare theframes aheadfigure instreamsagainstdelayin/proc/asound/MOTU/pcm0p/sub0/status— the former should be comfortably smaller.
Mainline snd-usb-audio carries a quirk for the MicroBook II whose USB ID
(07fd:0004) also matches the mk1. On a cold boot it probes the device before
an out-of-tree module is loaded, writes to the sample-rate register, and gives
up:
usb 3-1: Waiting for MOTU Microbook II to boot up...
usb 3-1: failed setting the sample rate for Motu MicroBook II: -22
It then releases the interface, so this driver still binds — but the device has
been left mid-configuration, and the first enable can time out
(enable (0x0b14=0) failed: -110). The enable is retried rather than failing
outright, and contrib/modprobe.d/motu-microbook.conf carries a
softdep snd-usb-audio pre: motu_microbook line so this driver registers first
and claims interface 0 before the quirk runs.
If you suspect the device is in a bad state from a cold boot, reloading the module re-runs the full boot sequence against a settled device:
sudo rmmod motu_microbook && sudo insmod motu-microbook.ko allow_duplex=1The two failure modes look identical but need opposite fixes, so measure rather than guess. Sample the client's buffer fill while it crackles:
for i in $(seq 200); do
grep '^delay' /proc/asound/MOTU/pcm0p/sub0/status | awk '{print $3}'
sleep 0.02
done | sort -n | head -1 # minimum fill, in framesCompare that minimum against the read-ahead (queue_ms × 48 frames at 48 kHz):
- Minimum fill below the read-ahead — the driver is overrunning the client.
underfillsin the debugfsstreamsfile will be climbing. Lowerqueue_ms. - Minimum fill comfortably above it — the driver is keeping up and the
crackle is USB starvation: the completion handler is not being serviced within
the queue depth. Raising
queue_mshelps only if the client's fill leaves room; otherwise this is a host scheduling problem, not a driver one.
For the starvation case, what matters is CPU wakeup latency and real-time scheduling:
# Is the audio server's data thread actually real-time? (expect FF / rtprio ~88)
ps -L -p $(pgrep -x pipewire) -o tid,cls,rtprio,comm=
# What do the idle states cost to exit?
cat /sys/devices/system/cpu/cpu0/cpuidle/state*/latency
# Hold a 100 us wakeup-latency budget while testing (no root needed where
# /dev/cpu_dma_latency is group-writable by `realtime`)
sh -c 'exec 3<>/dev/cpu_dma_latency; printf "\x64\x00\x00\x00" >&3; sleep 3600'PM QoS via /dev/cpu_dma_latency is the right mechanism here: it expresses a
latency budget the idle governor works within, and it is released when the
holder exits — unlike cpupower idle-set -D, which disables states outright
until something sets them back.
Note that host tuning has not been needed in practice. The reference machine
runs the 128-frame configuration cleanly with the powersave governor and every
idle state enabled, including a C3 that costs ~350 µs to exit. Every crackle
observed on this hardware so far has been the read-ahead overrunning the client,
which queue_ms fixes — check underfills before reaching for CPU settings.
The MicroBook is a full-speed (12 Mbps) USB device. Concurrent IN and OUT
isochronous traffic through hub transaction translators can exceed TT buffer
capacity, causing playback crackling with zero USB errors. The driver enforces
half-duplex by default (allow_duplex=0): if one direction is running, the
other returns -EBUSY. Direct USB connections (no hub) can use allow_duplex=1.
Capture and playback share one device clock, and changing the rate requires a
device reboot (SET_INTERFACE alt 0 → alt 1), which kills every in-flight URB
on the interface — including the other direction's stream. With PipeWire's
allowed-rates switching between 44.1 and 48 kHz this used to silently kill
the running stream (a frozen capture stream looked like “no input in Reaper”).
The driver therefore locks the rate while both substreams are open:
open()constrains the second stream to the currently running rate, so clients (PipeWire) resample instead of retuning the device.prepare()refuses (-EBUSY, logged) a rate change while the other substream is open, as a backstop.- If URBs are ever killed behind the driver's back anyway, the stream signals an xrun so userspace recovers instead of hanging on a frozen pointer.
The USB playback stream is inherently 2 channels — the endpoint max packet size (288 B) only fits 2 ch × 3 B × 48 samples. The phones, line and main jacks are all fed copies of this stereo bus by the device DSP; they are not separately addressable from the host, so a DAW will only ever see outputs 1/2.
The phones jack taps the main-out bus post-fader (“Phones mirrors Main”), so the driver pins the Main Out fader at unity on every device (re)boot and exposes no control for it. Playback therefore always reaches both the headphone and line jacks, with independent faders:
| ALSA control | Register pair | Jack |
|---|---|---|
Headphone Volume |
0xf00003b4/b5 |
Phones |
Line Volume |
0xf00003b2/b3 |
Line out |
PCM Volume |
0xf00003b6/b7 |
S/PDIF bus (no mk1 jack; likely the USB return level) |
Note: because the Main Out fader is pinned at unity, the main-out jacks are always live at full DSP level. User faders are re-applied after every device reboot (rate change) from the driver's cached values.
The device mixes your inputs to the outputs in hardware, with zero latency and no host involvement — which is why you hear a guitar the moment it is plugged in, even with nothing running. The DSP mixer is a 6-input × 5-bus matrix, and bus 0 is the monitor mix: the only bus carrying all six inputs, and the source all three output pairs are routed to. Its crosspoints are the direct monitoring levels, exposed as:
| ALSA control | Input | Registers (L, R) |
|---|---|---|
Guitar Monitor Volume |
Guitar | 0xf0000186, 0xf000018c |
Mic Monitor Volume |
Mic | 0xf0000185, 0xf000018b |
Line Monitor Volume |
Line in L/R | 0xf0000187, 0xf000018e |
Set one to 0 to monitor that input purely through the DAW instead (useful when you want to hear it with plugins applied and not doubled by the dry signal); leave it up for latency-free monitoring. These affect only what you hear — what the host captures is unchanged.
The computer playback return is inputs 4/5 of the same bus, left at unity, so DAW output and direct signal sum in the monitor mix. Inputs 0 and 1 are mono and centre-panned, so their 100% is the equal-power gain (√2/2) rather than 1.0.
Input identities were confirmed on hardware by muting one crosspoint at a time;
see ../re/cuemix_registers.md §17.
The MicroBook can end up refusing every control command with -ETIMEDOUT,
while still happily streaming isochronous audio. In that state nothing can be
configured: the sample rate cannot be set, motu_mixer_restore() cannot pin the
main output, and the outputs stay silent. It looks like this:
usb 3-1: boot: disable (0x0b14=0) failed: -110
The important part is that it survives a warm reboot — rebooting does not cut USB bus power, so the device keeps its wedged state across restarts. Only a port reset or physically replugging clears it. This is worth knowing before debugging anything else: a fault that persists across reboots has probably not been retested against a clean device at all.
The driver reacts to a timed-out boot sequence by scheduling a USB port reset
(auto_reset, on by default), logging:
usb 3-1: control channel unresponsive; resetting device
The reset is deferred to a work item, because it unbinds the driver and
disconnect() takes stream_lock — which prepare() is holding at the point
the failure is detected. It is rate-limited to one reset per 30 s so a device
that is wedged beyond recovery cannot reset in a loop; the skipped attempts log
still unresponsive; not resetting again yet.
If the reset does not clear it, power the device down completely — a full poweroff drops USB bus power where a reboot does not.
alsa-restore.service writes /var/lib/alsa/asound.state back to the card on
every boot, and alsa-state.service saves the current values at shutdown. That
applies to this driver's controls like any other, and it means a bad value
captured once is re-applied at every boot — a zeroed Guitar Trim looks
exactly like a dead input, and reloading the module appears to "fix" it only
because the reload re-reads the device instead of applying the saved state.
Save a known-good state once:
amixer -c MOTU contents # check the values first
sudo alsactl storeThe guitar trim register does not read back reliably, so the driver treats its
cache as authoritative and seeds it with MOTU_GUITAR_TRIM_DEFAULT rather than
reporting a placeholder. An untouched control that reported 0 would be captured
by alsactl at shutdown and restored as a real 0 at the next boot, silently
zeroing the guitar input gain.
When CONFIG_DEBUG_FS is enabled the driver exposes a raw window onto the
device's quadlet address space, for reverse engineering the parts of the DSP
that are not yet decoded (EQ, compressor, reverb, output routing):
/sys/kernel/debug/motu-microbook-<usbdev>/reg read/write one register
/sys/kernel/debug/motu-microbook-<usbdev>/dump dump the mixer/routing window
Write ADDR to reg to read a register and ADDR VALUE to write one (hex,
0x optional); reading reg returns the last operation. tools/motu-probe.sh
wraps this with dump, get, set, test-input and an interactive sweep.
Both require root, as debugfs always does.
Writes go straight to the DSP with no validation. Stay at or below
0xf00001c0: the mixer matrix ends there,0xf00001c2begins output routing, and0xf00001c8onward is EQ coefficients.
Optional WirePlumber timing policy and Cinnamon-compatible split input sources
are provided under ../contrib.
This driver is licensed under GPL-2.0-only. See ../COPYING.
motu-microbook.h— USB/protocol/format constants (all capture-confirmed).motu-microbook.c— the driver: control channel, isoc URBs, ALSA PCM.Makefile— out-of-tree module build.