Repository navigation
v0.7.0: USB audio out (M5c, #21) - #42
Merged
Merged
Conversation
The whole repository, unmodified (blobs checked against upstream), in third_party/sp1-usb-audio/upstream/, with a README beside it recording the commit, what each part is for in Wakes, and how to update. Nothing is compiled yet. Its regulator host test passes at 128- and 256-frame blocks and fails at Wakes' 96, so uacring needs retuning before use. NOTICE and LICENSES/README.md gain its Apache-2.0 entry; its own NOTICE (Nordic for the driver, chattock for the audio bring-up) is carried with it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ryan Gilmore's ring and packet regulator is tuned upstream for 128- and 256-frame producer blocks; Wakes renders 96. firmware/src/sp1_uac_tuning.h holds Wakes' values, force-included ahead of uacring.h by the firmware and the host test alike, and stops the build if the audio block changes. Swept against Wakes' render times (50 % light, 97.5 % held on the heaviest patch, one block in 997 at 200 % -- the longest the speaker survives with one queued block): first clean pass at TARGET 336 with a +/-32 dead band; 384 (8.0 ms) adds half a block, upstream's margin, and leaves 55 frames at worst. No corrections from load alone; ~9.5/s at +/-200 ppm. tools/host-tests/uactest.c (derived from upstream's test_uacring.c, Apache-2.0) runs in hostbuild.sh and CI, with a -s sweep mode for re-tuning. zephyr-patches/README.md records the ISO IN port review against Ryan's base. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
uacring.c copies with memcpy/memset, and the SDK's picolibc memcpy is a byte loop (ldrb/strb, confirmed in the linked image; ~6.6 cycles a byte, #36). The ring's push (audio thread, 384 B every 2 ms) and pop (USB interrupt, 192 B every 1 ms) would cost ~4 points through it. third_party/sp1-usb-audio/patches/uacring.patch replaces those five calls with one 32-bit word per stereo frame (frames_copy, unrolled by four; frames_zero). Cortex-M4 at -Os: 14 instructions per 16 bytes, ~1 cycle a byte, ~0.65 points in all. Applied strictly to a copy (tools/apply_patch.py); the vendored file is untouched. Without -fno-tree-loop-distribute-patterns GCC turns the silence fill back into memset (checked), so the copy is built with it, and check_image.py now fails an image whose uacring_push/pop call memcpy/memset (silent until USB audio is linked; ELF helpers shared with the #36 rule, whose results are unchanged). uactest builds the patched copy the same way: 6 000 121 checks, 0 failures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The SP-1's output, after VOL and the output select (the samples the codecs
get), goes to the host as a 48 kHz 16-bit stereo UAC1 input. Nothing is
received from the host.
- src/sp1_uac.{c,h} (Apache-2.0; the fill function and stream start/stop
follow Ryan Gilmore's usb_audio.c): a hand-rolled UAC1 function in the shape
of feldd's MIDI class. AudioControl (header, input terminal "line connector",
USB streaming output terminal) + AudioStreaming alt 0/1, Type I PCM, one ISO
IN endpoint, asynchronous with no feedback endpoint, 9-byte endpoint
descriptor, 196-byte packets. No IAD, like the MIDI function the OP-XY
accepts. Sample-rate requests answered though not advertised. Named
"Wakes Audio Out" on its interfaces and input terminal.
- Packets go out from the USB interrupt through the ISO IN fast path,
registered when the host selects alternate setting 1 and removed on 0,
disable, VBUS removed and bus reset; a resume re-primes the ring.
- zephyr-patches/udc_nrf-fast-paths.patch: the SOF interrupt now runs only
while a fill is registered (kept across a restart), and no SOF event reaches
the stack then -- a device that is not streaming pays nothing.
- sp1_audio.c: sp1_uac_claim() as a block starts rendering, sp1_uac_push()
with the finished block. Ring = Ryan's uacring.c with Wakes' word-copy
patch, built -fno-tree-loop-distribute-patterns with sp1_uac_tuning.h.
- Class order by name: cdc_acm_0, usb_midi1, wakes_uac (checked in the
image), so the console and MIDI keep their interface numbers.
- USB identity: PID 0x5213 (interim; pid.codes 1209:4440 requested), product
"Wakes" (was wakes-sp1 -- also the MIDI port's name; docs/MIDI.md says to
re-select it), manufacturer "Worldwave". console-log.py knows 0x5213.
- main.c: "audio out=" on the USB boot line; a UAC status line every 5 s
while the stream is open.
Build: 426 788 B (46.72 %), RAM 178 408 B, 0 warnings, check_image OK incl.
the push/pop word-copy checks; fallback 86 372 B OK. Not yet on hardware.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adara: disable the onboard speaker while USB audio out is in use. It rides on the headphone mute: the jack is debounced as before, and every 40 ms poll now reconciles the TAS2505's driver with what the speaker should be doing -- off while headphones are in OR a host has the stream open (sp1_uac_open), on otherwise. Headphones always play, so they still monitor. - A stream closing (host stops, cable pulled) brings the speaker back within one poll; an ON entry with a host already recording mutes at bring-up, so the first 40 ms do not reach the speaker. - Same fail-safe as before: the amp state changes only when the write succeeds (retried and counted otherwise), and if detection gives up the speaker is ON -- the USB mute with it. Nothing can leave the device silent by mistake. - New poll results 3 / 4 log "SPEAKER off/on" for USB; sp1_audio_speaker_on(). Build: 427 028 B, 0 warnings, check_image OK; fallback 86 544 B OK. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adara: check that the host accepts the stream, otherwise default to the speaker. Selecting the streaming alternate setting already requires the host's audio driver to have accepted the device and something to have opened the input, so a charger, a host without USB audio (or rejecting ours), a MIDI-only host and an unopened input all kept the speaker. The gap was a host that opened the stream and then stopped running the bus -- asleep, suspended, stalled: the speaker stayed muted. Now the speaker is muted only while sp1_uac_live(): the stream is open AND the USB interrupt has handed over packets since the previous 40 ms check (fill() runs at each start-of-frame, so a sleeping host stops it). Otherwise the speaker comes back within one poll, and goes quiet again when packets resume. One counter increment per millisecond in the interrupt. Build: 427 072 B, 0 warnings, check_image OK; fallback OK. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ows it Adara: include USB audio output in the delay compensation, as #32 built it. Wakes makes up its own output delay by reading the host's MIDI clock ahead (`clock_lead = auto`, and `notes` until it has learnt the host's offset): the delay is counted from the code (SP1_MIDI_OUTPUT_LATENCY_MS, (queue + 3) blocks = 8 ms) and the module that owns the pipeline BUILD_ASSERTs it. That was the speaker's path. A host recording over USB hears a different one. - sp1_midi.h: SP1_MIDI_USB_OUTPUT_LATENCY_US = TARGET + BLK/2 frames = 9.0 ms (1 block to place the message + the ring's average wait from render start, 7.0 ms). sp1_uac.c BUILD_ASSERTs it against the ring's tuning, the same rule as sp1_audio.c's check, so re-tuning the ring cannot leave the lead stale. - sp1_midi.cc: Lead() = fixed ms, or the learnt notes lead, or auto -- the USB path's delay while a host takes USB audio out, the speaker's otherwise (+ Plaits' TRIG delay: 9.5 vs 8.5 ms). Taken once per audio block; a change is followed at kFollow, never backwards. A learnt notes lead does not change (notes and Marbles share the path). - sp1_uac_live() is now stateless (time of the last packet, 20 ms window), so main feeds sp1_midi_set_output_usb() every tick and the speaker check uses it too. - main.c: an "AUD USB audio out: ~9.0 ms in to out" boot line, from the constant. - Host suites: uactest measures the ring's delay through the real ring -- 7.01 ms at 0 ppm, within the dead band at +/-200 ppm (6.35 .. 7.69); miditest_clock §14: Marbles leads by the USB path's delay with a host taking USB audio, back to the speaker's when it stops, no beat skipped or doubled, the two leads 1.00 ms apart (want 1.0). All nine suites pass. - midi.ini, MIDI.md, the CC chart: auto is 9.5 ms while a host records over USB. Build: 427 272 B, 0 warnings, check_image OK; fallback OK. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adara's spec: users choose how USB audio out sets its level and whether the
speaker plays while a host records, in a new config file.
config/audio.ini [usb] (-> tools/gen_audio.py -> sp1_audio_gen.h, like the
MIDI script: a bad value fails the build, naming the line):
- level = parked (default) | vol.
parked: while a host takes USB audio out, VOL is set to parked_level and its
buttons are locked ("LEVEL locked ..." logged on a press); when the host
stops, VOL returns to where it was. Wakes has one output level, so this
parks the headphones (and the speaker, if on) too -- Adara's call, and the
file says why: it is the cheapest way (a separate USB level would need a
second gain pass, ~0.3 % of the CPU). The level slews as VOL always does.
vol: VOL moves USB audio out with everything else (the M5c build so far).
- parked_level = -6 (default): one of VOL's steps, 0 .. -30 dB in 3 dB.
BUILD_ASSERTs tie gen_audio.py's step list to sp1_audio.c's table.
- speaker = off (default) | on: the speaker while a host takes USB audio out
(speaker_wanted; headphones always play).
Built both ways (shipped settings, and vol / -30 / on), 0 warnings; the
generator rejects a bad mode, a level off VOL's steps, a positive level, a bad
on/off and an unknown key, each naming its line. README lists the file.
Build: 427 660 B, 0 warnings, check_image OK; fallback OK.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ords
Seen on the OP-XY (logs/sp1-20261006-142157.log): 13 times in 5 minutes the
speaker came back for one 40 ms poll ("SPEAKER on: no host is taking USB
audio out", then "off" 43 ms later), and VOL unparked and re-parked within one
tick ~30 times -- while the stream never paused (312 075 packets, under=0,
over=0).
sp1_uac_live() read the clock, THEN last_fill. A packet handed over between
the two reads made last_fill later than now; the unsigned difference wrapped
to ~2^32 and a live stream read as dead for that call. Likelier than it
sounds: the clock read holds interrupts off, so a start-of-frame arriving
during it fires exactly in that gap.
Now last_fill is read first (a packet in between can only make it older) and
the comparison is signed (a time ahead of now counts as live). Checked in the
disassembly: the load precedes the clock call, the compare is signed.
Build: 427 660 B, 0 warnings, check_image OK; fallback OK.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
firmware/VERSION, README and the issue-form placeholders go to v0.7.0 for USB audio out (M5c). docs/Wakes - Manual.pdf is Adara's updated manual. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The USB audio part of #21 (M5c). Hardware test procedure: #40.
What it does
Wakes now shows up on a USB host as an audio input named Wakes Audio Out: 48 kHz, 16-bit stereo, the same signal as the headphone jack. It's output only. The SP-1 never receives audio from the host.
Class: USB Audio Class 1.0, hand-rolled (
firmware/src/sp1_uac.{c,h}):The ISO IN endpoint is async with no feedback endpoint. Packets are 47, 48 or 49 frames.
Data path: Ryan Gilmore's sp1-usb-audio (vendored unmodified in
third_party/sp1-usb-audio/).zephyr-patches/udc_nrf-fast-paths.patch.Word copies in the ring (
patches/uacring.patch): picolibc'smemcpyis a byte loop.check_image.pyfails an image where the ring's push or pop callsmemcpyormemset.Speaker: off while a host is actually taking the stream, judged by the last packet being under 20 ms old. A host that opens the stream and then sleeps gets the speaker back. Headphones always play.
Delay compensation: USB audio out joins the latency stack. Its path is 9.0 ms against the speaker's 8.0.
clock_lead = autois 9.5 ms while USB audio is live and 8.5 ms otherwise. ABUILD_ASSERTties the figure to the regulator's tuning.config/audio.ini(new, build-time):level = parked | vol, default parked.parked_level: one of VOL's steps, default −6 dB.speaker = off | on: the speaker while a host records, default off.Parked sets and locks VOL itself while a host records, and restores it afterwards. That's the cheapest way to do it, and the file says so.
Names: the USB product string is now Wakes (was wakes-sp1), and the manufacturer is Worldwave. The MIDI port takes the product name, so DAW routings need re-selecting once.
docs/MIDI.mdsays so.Interim PID 0x5213: VID 0x1209 / PID 0x4440 replace it once Add Worldwave org and 1209:4440 (Wakes) pidcodes/pidcodes.github.com#1301 merges.
v0.7.0:
firmware/VERSION, README, issue-form placeholders and the updated manual.CPU
About 2 points with a host recording (accepted):
The worst window with Chords, drive, Marbles on an external clock and USB audio all on was 88.8 %, with no over-budget blocks and no stream failures. With nothing recording, the cost is zero.
Verification
0x20000, 0 warnings.check_image.pypasses, including the new ring-copy checks.uactest, Wakes' render model with the 50 %, 97.5 % and 200 % blocks, now runs in CI. The measured ring delay is 7.01 ms.miditest_clock§14: the USB lead and the speaker lead are 1.00 ms apart.Still open
SPEAKER onorLEVEL backlines (Hardware test: M5c (USB audio out) #40).🤖 Generated with Claude Code