Skip to content

Latest commit

 

History

History
584 lines (451 loc) · 26.5 KB

README.md

File metadata and controls

584 lines (451 loc) · 26.5 KB

MOTU MicroBook mk1 — Linux driver

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).

Status

  • 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×44 samples), 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_duplex module 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.

Known issues and post-mortems

Kept because these failures were intermittent and the diagnoses were not obvious — if something similar reappears, this is the trail to follow.

Capture coming up silent — xrun feedback loop

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.

Historical: the same symptom from a zeroed guitar trim

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 0 with packets climbing at ~1000/s — the transport was healthy.
  • bytes == packets × 576 exactly — every packet reached motu_audio_copy_in(); nothing was being skipped.
  • period_acc cycling to 0 — periods were crossing, so snd_pcm_period_elapsed() was firing thousands of times.
  • ALSA's hw_ptr nevertheless 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 microbook

period_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.

Build

cd driver
make            # builds motu-microbook.ko against the running kernel

Requires linux-headers matching the running kernel.

Install permanently (DKMS)

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-install

That 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-uninstall

Bump PACKAGE_VERSION in dkms.conf when the driver changes meaningfully; the Makefile reads the version from there, so nothing else needs editing.

Load / test

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

Module parameters

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.ko

How it works

Control channel (EP1, interrupt)

The 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.

Sample rate selection

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.

Audio transport (EP2, isochronous)

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”).

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-*/streams

Do not raise queue_ms beyond 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 the frames ahead figure in streams against delay in /proc/asound/MOTU/pcm0p/sub0/status — the former should be comfortably smaller.

Conflict with snd-usb-audio on cold boot

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=1

Diagnosing crackling

The 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 frames

Compare 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. underfills in the debugfs streams file will be climbing. Lower queue_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_ms helps 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.

Half-duplex guard

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.

Sample-rate lock during duplex

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.

Output routing and volumes

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.

Direct monitoring

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 device wedging (-110 on every control command)

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.

Saved mixer state (alsactl)

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 store

The 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.

Register probe

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, 0xf00001c2 begins output routing, and 0xf00001c8 onward is EQ coefficients.

PipeWire desktop integration

Optional WirePlumber timing policy and Cinnamon-compatible split input sources are provided under ../contrib.

License

This driver is licensed under GPL-2.0-only. See ../COPYING.

Files

  • 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.