Skip to content

fix: find the correct control to change volume on USB DAC - #851

Open
nborodikhin wants to merge 7 commits into
LoveRetro:mainfrom
nborodikhin:fix/usb-dac-volume-sidetone
Open

nborodikhin wants to merge 7 commits into
LoveRetro:mainfrom
nborodikhin:fix/usb-dac-volume-sidetone

Conversation

@nborodikhin

@nborodikhin nborodikhin commented Oct 1, 2026 •

Copy link
Copy Markdown

Description

I found at least two cases where USB volume adjustment misses the control and does nothing to the output volume.

In the first case, device registers Sidetone controls before Headset (USB-C Google Inc. Pixel earbuds id 18d1:5033 and Google Inc. Headphone adapter id 18d1:5034):

$ amixer -c 5 controls
numid=7,iface=MIXER,name='Sidetone Playback Switch'
numid=8,iface=MIXER,name='Sidetone Playback Volume' <----
numid=9,iface=MIXER,name='Headset Playback Switch'
numid=10,iface=MIXER,name='Headset Playback Volume' <----

volume control acts on control with the lowest id, which happens to be the sidetone control.

In the second case, device registers two devices (PositiveGrid Spark 40 amp id 10d6:1319):

$ cat /proc/asound/cards
 3 [S40            ]: USB-Audio - Spark 40      <---- no pcm, controls
 4 [USB            ]: USB-Audio - Spark 40 USB  <---- pcm, controls

where the first one has no controls. Due to the difference in card identification, volume control tries to find a suitable control on card #3, while audiomon routes the sound through card #4.

Related: #485

Change

  • introduce rank to classify USB DAC controls, get_dac_control_playback_priority():
    • Master control
    • Physical output channel (headphone, speaker etc)
    • PCM
    • Generic "Playback"
  • change the logic to find suitable control, SetRawVolume():
    • go through all controls, find the highest rank of output exposed by the card
    • change volume of all controls that have that rank
      • that should provide the best quality for multi-stage DACs, e.g. manipulation of analog "Headphones" stage is better than manipulation of digital "PCM"
      • when volume is 0, turn the output completely off using corresponding switch
    • always skip input and monitors ones
    • was: "find the first suitable control and change its volume"
  • change the logic to find the card to control, get_asoundrc_card_num():
    • read the card id from .asoundrc
    • keep the first card that is not "audiocodec" as a fall back
  • the same change for tg5040 and tg5050 (desktop does not use mixer)

Testing

  • volume keys change audible volume on all three devices
  • volume 0 is silent on each device

@nborodikhin nborodikhin changed the title fix: set the volume of the right control on the right USB audio card fix: find the correct control to change volume on USB DAC Oct 1, 2026
@nborodikhin
nborodikhin marked this pull request as ready for review October 1, 2026 21:11
nborodikhin added a commit to nborodikhin/nextui-music-player that referenced this pull request Oct 2, 2026
…ving (#52)

## Description

There are a few issues with playback:
- when audio device connects/disconnects (e.g. Bluetooth, USB DAC or USB
headphones), playback keeps playing on the current sound device until
restart
- podcast speed change stops Bluetooth playback
- playback speed change on podcast affects pitch
- with some headphones, the player gives no sound over Bluetooth at all,
and it hangs when the user exits it, while the emulators plays just fine
- status pill does not update on audio change events

## Change

- `launch.sh`:
- export `LIBASOUND_THREAD_SAFE=0` to avoid BlueALSA stalls, see also
LoveRetro/NextUI#811
- `bluetooth_sample_rate()`:
  - use NextUI sample rate setting to Bluetooth audio
- `Player_handleAudioSinkChange()`:
- called from `ModuleCommon_frameEnd()` to move the audio to a new
output on the main thread
- the reopen stops SDL audio and lets alsa-lib read its configuration
again
- use dlsym to find `snd_config_update_free_global()` (my355 workaround)
- let NextUI to drive the volume
  - this avoids double application of volume adjustment
  - also see LoveRetro/NextUI#851
- `Player_pollUSBHID()`:
- periodically check for headset controls presence, as they appear
asynchronously
- `load_streaming()`:
  - create the resampler for each track, not only when the rates differ
  - reset the resampler every time an audio device reopens
- add Sonic pitch-preserving speed change library in
`src/audio/sonic.{c,h}` (Apache-2.0)
- `resample_chunk()` converts the rate only, then `time_stretch_write()`
applies the speed
- add the time of each audio callback at its own speed, not the total
time at the current speed
- `dev`: add `./dev kill <device>` to stop the app on a device or on the
desktop

## Testing

- [x] unit tests
- [x] tested on TrimUI Brick, Miyoo Mini
- [x] during playback: speaker to Bluetooth, Bluetooth to speaker,
Bluetooth to USB DAC, USB DAC to speaker
- [x] main menu with no input: the Bluetooth icon reflects the changing
state of headset
- [x] volume is consistent over the handoff to and from Bluetooth, with
no large step at the first key press
- [x] switch the headset off and on during playback: play/pause, next
and previous on the headset work
- [x] at 1.5x and 2x, podcast voice sounds natural, speaker and
Bluetooth
- [x] playback progress is consistent with actual position
With a USB audio device as the output, the volume keys could do nothing:

- SetRawVolume() set the first control whose name had PCM or Playback
  and Volume, and stopped. On a USB headset adapter that is "Sidetone
  Playback Volume" (the microphone fed back into the headphones), which
  comes before "Headset Playback Volume" in the order of the controls.
- It took the first card that is not audiocodec. A device that registers
  two USB cards (a Spark 40 guitar amp: one for the amp, one for the
  playback) got the volume on the wrong card.

Now SetRawVolume() takes the card that audiomon wrote into .asoundrc,
and the first USB card only where the file names none. It ranks the
volume controls: Master, then the outputs (Headphone, Headset, Speaker,
Line Out, Digital Out, IEC958), then PCM, then other Playback controls.
Capture, the microphone and the inputs played into the output
(Sidetone, Monitor, Loopback, Bypass, line in) are not the output. It
sets each volume and switch of the best rank; the switches mute at 0.
Other controls keep their level.

Related: LoveRetro#485
The USB DAC path takes the card from .asoundrc, which audiomon writes,
and the first card that is not audiocodec only as a fallback. The
comment of AUDIO_SINK_USBDAC still said that the DAC is card 1.
get_asoundrc_card_num() and get_usbc_card_num() had one caller, which
called the second only where the first gave no card. get_usbc_card_num()
now reads .asoundrc first and falls back to the first card that is not
audiocodec.
The volume is a mixer control, thus the card is the one of the control
interface, ctl.!default. The lookup took the first card line, which is
pcm.!default. audiomon writes the same card in both today, but a file
can name another one for each, as a Bluetooth .asoundrc does (pcm
BlueALSA, ctl hw card 0). Any card line stays the fallback.
Apply the same USB DAC fix h700: take the card from the .asoundrc of audiomon,
then drive the output volume control skipping sidetone and monitor controls.
@frysee
frysee force-pushed the fix/usb-dac-volume-sidetone branch from 7a07975 to e11a714 Compare October 13, 2026 21:09

This branch has not been deployed

No deployments
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.

1 participant