Skip to content

Fix DJM-900NXS2 USB capture: silent recordings and wire-format distortion - #1

Merged
P2GR merged 2 commits into
P2GR:mainfrom
tonnkatonno:main
Jul 21, 2026
Merged

Fix DJM-900NXS2 USB capture: silent recordings and wire-format distortion#1
P2GR merged 2 commits into
P2GR:mainfrom
tonnkatonno:main

Conversation

@tonnkatonno

Copy link
Copy Markdown
Contributor

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=0 regardless of routing)

A USBPcap capture of Pioneer's own driver actually recording real audio 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 -- a static, non-informative response, the same failure shape already known from 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 added, from the same pcap evidence:

  • A native OUT-endpoint duplex keepalive (continuous silence on endpoint 0x01), mirroring the DJM-A9's requiresPlaybackTraffic mechanism, since Pioneer's driver keeps that endpoint active throughout capture.
  • MIX/REC OUT routed to both the default output and USB9/10 (output 5) up front, confirmed via a pcap of the Setting Utility toggling both.

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

  • Fixed auto-pick-loudest-pair channel 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, flipping the selected pair dozens of times per second and producing audibly gappy output. Decisions now happen once per finished measurement window, with a hysteresis margin against the currently-selected pair.
  • Hardened 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 inside libusb_close() during teardown.
  • Added a per-output-pair channel selector to the recording UI, mirroring Pioneer's Windows Setting Utility.
  • Diagnostics reporting for all of the above.
  • Safe handling of Android 14+ foreground-service start rejections that surfaced during testing on a real device.

Test plan

  • Confirmed via USBPcap capture that the MIX/REC OUT route SET encoding matches Pioneer's Setting Utility byte-for-byte.
  • Confirmed via USBPcap capture of a real Windows recording session that the SET_CUR sequence and 12ch/24-bit wire format match this fix.
  • Tested on real hardware (DJM-900NXS2 + OnePlus CPH2653, Android 16): recording produces clean, correctly-leveled, non-distorted audio.
  • Tested stop/restart cycles for regressions after the teardown hardening.

🤖 Generated with Claude Code

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.
@P2GR

P2GR commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Will run a few tests with this later today

@P2GR
P2GR merged commit f2cb103 into P2GR:main Jul 21, 2026
1 check passed
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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants