Fix DJM-900NXS2 USB capture: silent recordings and wire-format distortion - #1
Merged
Conversation
The vendor-class isochronous capture path for the DJM-900NXS2 read back nothing but zeros, even with confirmed-correct MIX/REC OUT routing and confirmed-audible audio on the mixer. A USBPcap capture of Pioneer's own driver actually recording (whit_sound_on.pcapng) showed it unconditionally sends a UAC1 SET_CUR sampling-frequency control transfer to the capture endpoint right after SET_INTERFACE, without ever probing GET_CUR first. This app's native code did the opposite: it GET_CUR'd first and silently skipped the SET whenever the GET returned a plausible-looking value -- the same kind of static, non-informative GET response already proven true for the MIX-route GET on this model. Sending SET_CUR unconditionally, matching the real driver's sequence exactly, unlocks real audio on the endpoint. Also, from the same pcap evidence and the earlier MIX-routing reverse engineering: - Added a native OUT-endpoint duplex keepalive (continuous silence on endpoint 0x01) mirroring DJM-A9's requiresPlaybackTraffic mechanism, since Pioneer's driver keeps that endpoint active throughout capture. - Route MIX/REC OUT to both the default output and USB9/10 (output 5) up front, since a pcap of the Setting Utility confirmed both accept it. - Hardened UsbIsoAudioSource::stop() against a redundant release/alt-setting call on the capture interface (NXS2's playback OUT endpoint shares the same interface+alt-setting as capture, unlike DJM-A9's separate one) -- found next to a destroyed-mutex crash inside libusb_close() during teardown after the first successful real-audio session. - Fixed auto-pick-loudest-pair thrashing: with real signal flowing, several output pairs read comparable amplitude, and the decision was previously re-evaluated on every incoming packet against a still-accumulating peak window, causing the selected pair to flip dozens of times per second and produce audibly distorted, gappy output. Decisions now happen once per finished measurement window, with a hysteresis margin against the currently-selected pair. Also adds a per-output-pair channel selector to the recording UI (mirroring Pioneer's Windows Setting Utility), diagnostics reporting for all of the above, and safe handling of Android 14+ foreground-service start rejections that surfaced during testing on a real device.
…rtion Confirmed real recordings were quiet and "washing machine"-distorted even after the earlier silence fix. Testing the real captured wire bytes from Pioneer's own driver (whit_sound_on.pcapng, device 2b73:000a, endpoint 0x82) against candidate channel-count/subframe combinations showed 12 channels at 3-byte (24-bit) subframes fits far better than the originally guessed 10 channels. Independently corroborated: this app's own captured packets are consistently 216 bytes, which divides evenly into 6 frames of 12ch x 3B (36B/frame) but never evenly into the old assumed 10ch x 3B frame (216/30 = 7.2) -- every packet this whole session was being sliced at the wrong boundary, which explains the cyclically-modulating distortion as the sample offset drifted and wrapped every few frames. Confirmed on real hardware: clean audio, no distortion.
Owner
|
Will run a few tests with this later today |
P2GR
pushed a commit
that referenced
this pull request
Jul 21, 2026
…pture profile + livestreaming (WIP) - DJM-900NXS2: vendor-class isochronous capture now produces real audio (PR #1, @tonnkatonno). Unconditional SET_CUR, OUT-endpoint duplex keepalive, corrected 12ch/24-bit wire format, channel-pair picker UI, raw hex dump diagnostics, Android 14+ foreground-service safety. - DJM-750MK2: vendor-capture override profile configured (same vendor-class if0/alt1 topology as NXS2). Channel count guessed at 10ch; raw hex dumps from first capture session will confirm or correct this. - Livestreaming: RTMP/RTMPS streaming with YouTube OAuth, Mixcloud, and Custom RTMP support. Artwork, rear camera, or front camera video. Marked WIP in-app. Twitch and TikTok removed for now. - README: polished for open-source, removed release workflow docs, updated mixer support table. - Removed streaming.properties.example (Twitch-only, no longer relevant).
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.
Summary
The DJM-900NXS2 vendor-class isochronous capture path recorded nothing but digital silence, even with correct MIX/REC OUT routing and confirmed-audible audio on the mixer. Once that was fixed, recordings came out quiet and "washing machine"-distorted. Both are now fixed and confirmed working on a real DJM-900NXS2 + OnePlus CPH2653 (Android 16) over several rounds of hardware testing.
1. Silent capture (was
nonzero bytes=0regardless of routing)A USBPcap capture of Pioneer's own driver actually recording real audio showed it unconditionally sends a UAC1
SET_CURsampling-frequency control transfer to the capture endpoint right afterSET_INTERFACE, without ever probingGET_CURfirst. This app's native code did the opposite: itGET_CUR'd first and silently skipped theSETwhenever the GET returned a plausible-looking value -- a static, non-informative response, the same failure shape already known from the MIX-route GET on this model. SendingSET_CURunconditionally, matching the real driver's sequence exactly, unlocks real audio on the endpoint.Also added, from the same pcap evidence:
0x01), mirroring the DJM-A9'srequiresPlaybackTrafficmechanism, since Pioneer's driver keeps that endpoint active throughout capture.2. Wire-format correction (was quiet + distorted after audio started flowing)
Once real audio was flowing, recordings were quiet and cyclically distorted. Testing the real captured wire bytes from Pioneer's driver against candidate channel-count/subframe combinations showed the true format is 12 channels at 3-byte (24-bit) subframes, not the originally guessed 10 channels. Independently corroborated: this app's own captured packets are consistently 216 bytes, which divides evenly into 6 frames of 12ch x 3B (36B/frame) but never evenly into the old assumed 10ch x 3B frame (216/30 = 7.2) -- every packet was being sliced at the wrong frame boundary, which explains the modulating distortion as the sample offset drifted and wrapped every few frames.
Also included
UsbIsoAudioSource::stop()against a redundant interface release call (NXS2's playback OUT endpoint shares the same interface+alt-setting as capture, unlike DJM-A9's separate one), found next to a destroyed-mutex crash insidelibusb_close()during teardown.Test plan
🤖 Generated with Claude Code