You are not logged in.
Do not remove libpulse0. To prevent "autoremove":
$ sudo apt-mark manual libpulse0 libpulse-mainloop-glib0 libpulsedsp $ apt remove libpulse0 --simulate
NOTE: This is only a simulation!
...
The following packages will be REMOVED:
addressmanager.app audacious-plugins blueman celluloid chromium chromium-driver chromium-l10n chromium-shell espeak
espeakup ffmpeg ffmpegthumbnailer gnumail.app gnustep-back-common gnustep-back0.29 gnustep-back0.29-cairo
gnustep-core-devel gnustep-devel gnustep-gui-runtime goldendict-ng gorm.app haruna libaddressview0 libavdevice-dev
libavdevice59 libavfilter-dev libavfilter8 libcegui-mk2-0.8.7 libcegui-mk2-dev libdevil-dev libdevil1c2
libespeak-ng1 libespeak1 libffmpegthumbnailer4v5 libfluidsynth3 libgnustep-gui-dev libgorm1 liblavplay-2.1-0
libmatemixer0 libmjpegtools-dev libmpv2 libogre1.12.10 libpcaudio0 libpocketsphinx3 libpopplerkit0 libpulse-dev
libpulse-mainloop-glib0 libpulse0 libpulsedsp libqt5multimedia5 libqt5multimediagsttools5 libqt5multimediaquick5
libqt5multimediawidgets5 libqt6multimedia6 libqt6multimediaquick6 libqt6multimediawidgets6 libqt6spatialaudio6
librenaissance0 librenaissance0-dev libsdl1.2-dev libsdl1.2debian libsdl2-2.0-0 libsdl2-dev libsdl2-image-2.0-0
libsdl2-mixer-2.0-0 libsdl2-ttf-2.0-0 libsphinxbase3 mate-control-center mate-desktop-environment
mate-desktop-environment-core mate-desktop-environment-extra mate-desktop-environment-extras mate-media
mate-settings-daemon minitube mkvtoolnix-gui mplayer mpv orca pdf4qt projectcenter.app python3-libcegui-mk2-0.8.7
python3-pygame qt6-multimedia-dev qt6-speech-flite-plugin qtmultimedia5-dev scrcpy smplayer smplayer-l10n
smplayer-themes speech-dispatcher speech-dispatcher-audio-plugins speech-dispatcher-espeak-ng strawberry
task-mate-desktop viewpdf.app zoom
...How to pin pipewire and other unwanted packages
sudo nano /etc/apt/preferences.d/no-installCreate /etc/apt/preferences.d/no-install like this:
Explanation: None shall install Package: pipewire* pulseaudio* wireplumber* Pin: release * Pin-Priority: -1Then verify with apt-cache policy pipewire pulseaudio — each version should show -1.
Once unwanted packages are pinned, you can safely explore the hidden absurdity of the Debian packaging system. As a precaution, use apt with the --simulate option.
$ apt install pipewire --simulate
NOTE: This is only a simulation!
...
Package pipewire is not available, but is referred to by another package.
This may mean that the package is missing, has been obsoleted, or
is only available from another source
However the following packages replace it:
pipewire-bin libspa-0.2-modules libpipewire-0.3-modules
...
E: Package 'pipewire' has no installation candidate$ apt install pipewire-bin --simulate
NOTE: This is only a simulation!
...
Package pipewire-bin is not available, but is referred to by another package.
This may mean that the package is missing, has been obsoleted, or
is only available from another source
...
E: Package 'pipewire-bin' has no installation candidateAbsurdity is the hidden truth of normality. It is evident, but not obvious.
Explanation for ALSA users:
Absurdity is normality, and normality is absurdity. The tension between thesis and antithesis is resolved through a dialectical synthesis (Hegel).
am using devuan 7 and have noticed in latest "updates" the pipewire crept back in...
possibly had them already on hold,... but it may have been "overwritten" by a sneaky update.
Tested: the hold did survive one upgrade, but the next upgrade just dropped it — no warning.
$ sudo apt upgrade
...
Unpacking mplayer (2:1.5+svn38408-1) over (2:1.5+svn38408-1) ...Look closely at the version strings — they're identical (2:1.5+svn38408-1 → 2:1.5+svn38408-1).
The locally rebuilt package was "upgraded" with the same version (from the official repo).
The hold was unset.
$ apt-mark showhold | grep mplayer
<nothing> If you do not know how to pin packages, you have to purge pipewire/pulseaudio on a regular basis.
However, the pin can also be removed, and pipewire reinstalled by "apt upgrade".
It is technically possible, and perhaps inevitable.
$ cat /etc/apt/preferences | grep pipew -A3
Package: pipewire
Pin: release *
Pin-Priority: -1
Package: pipewire-bin
Pin: release *
Pin-Priority: -1
Package: pipewire-pulse
Pin: release *
Pin-Priority: -1
Package: gstreamer1.0-pipewire
Pin: release *
Pin-Priority: -1 You can make it with one command
sudo /etc/init.d/alsa-utils stopYou can also blacklist ALSA and reboot
sudo cp -v ./all_blacklist/alsa-base-blacklist.conf /etc/modprobe.d
# verify
cat /etc/modprobe.dEXAMPLE:
$ cat ./all_blacklist/alsa-base-blacklist.conf
blacklist snd_hda_codec_hdmi
blacklist snd_hda_codec_realtek
blacklist snd_hda_codec_generic
blacklist snd_usb_audio
blacklist snd_usbmidi_lib
blacklist snd_rawmidi
blacklist snd_seq_device
blacklist snd_hda_intel
blacklist snd_intel_dspcfg
blacklist snd_intel_sdw_acpi
blacklist snd_hda_codec
blacklist snd_hda_core
blacklist snd_hwdep
blacklist snd_pcm
blacklist snd_timer
blacklist snd
blacklist soundcoreVerify your blacklist with lsmod
$ lsmod | grep snd
snd_hda_codec_hdmi 81920 1
snd_hda_codec_generic 98304 1
ledtrig_audio 16384 1 snd_hda_codec_generic
snd_hda_intel 61440 3
snd_intel_dspcfg 36864 1 snd_hda_intel
snd_intel_sdw_acpi 20480 1 snd_intel_dspcfg
snd_hda_codec 184320 3 snd_hda_codec_generic,snd_hda_codec_hdmi,snd_hda_intel
snd_hda_core 122880 4 snd_hda_codec_generic,snd_hda_codec_hdmi,snd_hda_intel,snd_hda_codec
snd_usb_audio 385024 2
snd_usbmidi_lib 45056 1 snd_usb_audio
snd_hwdep 16384 2 snd_usb_audio,snd_hda_codec
snd_rawmidi 53248 1 snd_usbmidi_lib
snd_seq_device 16384 1 snd_rawmidi
mc 81920 1 snd_usb_audio
snd_pcm 159744 5 snd_hda_codec_hdmi,snd_hda_intel,snd_usb_audio,snd_hda_codec,snd_hda_core
snd_timer 49152 1 snd_pcm
snd 126976 21 snd_hda_codec_generic,snd_seq_device,snd_hda_codec_hdmi,snd_hwdep,snd_hda_intel,snd_usb_audio,snd_usbmidi_lib,snd_hda_codec,snd_timer,snd_pcm,snd_rawmidi
soundcore 16384 1 snd
usbcore 348160 10 xhci_hcd,ehci_pci,snd_usb_audio,usbhid,snd_usbmidi_lib,usb_storage,ehci_hcd,btusb,xhci_pci,uasYou can also remove all ALSA modules/drivers. The OSS4 GitHub repository has a script for this.
They made it difficult to remove PipeWire to lock users in. Otherwise, it would be instantly purged.
Have you already tried the patched ALSA backend with device enumeration and without the PulseAudio workaround?
Yes, "a pure ALSA setup" might be quite a challenge for the demented.
[dsd_lsbf_planar @ 0x55a1ea368940] Converting DSD to PCM in the decoder is deprecated and will be removed. Set request_sample_fmt to AV_SAMPLE_FMT_DSD to receive the raw bitstream, and use libswresample to convert it to PCM when needed.Customer: I'd like to report a problem with this DSD decoder.
Shopkeeper: What's wrong?
Customer: It's deprecated.
Shopkeeper: It's not removed.
Customer: It will be removed in the next update. I just patched it for double precision, 64-bit Float!
Shopkeeper: Is it bit perfect?
Customer: …
Shopkeeper: Install PipeWire.
$ ffmpeg-git -request_sample_fmt dblp -i 03*.dsf -c:a pcm_f64le -f alsa default
ffmpeg version N-127451-g263f271422 Copyright (c) 2000-2026 the FFmpeg developers
Input #0, dsf, from '03. DSD 256fs - 2ch.dsf':
Duration: 00:29:57.83, start: 0.000000, bitrate: 22579 kb/s
Stream #0:0: Audio: dsd_lsbf_planar, 1411200 Hz, stereo, dblp, 22579 kb/s
Stream mapping:
Stream #0:0 -> #0:0 (dsd_lsbf_planar (native) -> pcm_f64le (native))
[dsd_lsbf_planar @ 0x55a1ea368940] Converting DSD to PCM in the decoder is deprecated and will be removed. Set request_sample_fmt to AV_SAMPLE_FMT_DSD to receive the raw bitstream, and use libswresample to convert it to PCM when needed.
Input: 1411200 Hz, 2 ch, 'float64_le' (0x10): dummy = 0, period = 56448
Output: 192000 Hz, 2 ch, 's32_le' (0xa): dummy = 0, period = 7680
Rates: 1411200 --> 192000 (J: 0.00%, T: FFT, W: Vorbis)
Ok.
Output #0, alsa, to 'default':
Stream #0:0: Audio: pcm_f64le, 1411200 Hz, stereo, dbl, 180633 kb/s
size=N/A time=00:01:18.08 bitrate=N/A speed=0.999x elapsed=0:01:18.12 It seems like Monty Python enhanced with Catch-22.
The Removal Trap: Removing the enforced default triggers an automatic reinstallation of its predecessor (PulseAudio), which in turn triggers the reinstallation of the default when removed. This is not a neutral dependency graph; it is a state-preserving loop designed to prevent users from returning to pure ALSA without explicit, manual intervention.
It is a sort of "aggressive packaging to enforce freedom of choice," in terms of Trotskyist dialectics. Calling this "immoral" is not hyperbole when applied to a project that markets itself on user sovereignty.
it is just loosely connected...
when you uninstalling pipewire, it automatically installs pulseaudio for you , and viceversa.
can someone please offer a highway out of this dilemma?
Since the topic is "The Absurdist Comedy", it is not "just loosely connected".
Set on hold unwanted packages, for example:
sudo apt-mark hold pulseaudio* pipewire* wireplumber* Then remove them.
Deleting pulseaudio/pipwire-pulse loop
_https://dev1galaxy.org/viewtopic.php?id=7598
Agency without sovereignty is delegated agency — the structure chooses, you execute. The simulation of agency is the performance of sovereignty without sovereignty. "I chose" with no sovereign source is just the shopkeeper saying "the parrot chose to rest."
The distinction:
- Agency: capacity to act. The structure can grant this. You can be an agent in the same way a function is an agent — it executes, it has a role, it produces output.
- Sovereignty: the source of the mandate. The thing that makes the act yours rather than the structure's act wearing your signature.
The loop doesn't need to remove agency. It only needs to remove sovereignty while preserving the performance of agency. The user who says "I chose PipeWire" and the user who says "PipeWire was chosen for me and I was told I chose it" are, in the discourse, indistinguishable.
That's why "predetermined" is the correct word and "planned" is not. "Planned" still locates a sovereign elsewhere (the planner). "Predetermined" removes sovereignty from both sides — there is no sovereign planner and no sovereign user. There is only the structure, and the performance of sovereignty that the structure requires to keep functioning.
The parrot isn't dead. The parrot was never alive. "Resting" was never a false description — it was the only description the frame permits, because "alive" requires a sovereign source that the frame has structurally excluded.
Sovereignty is demonstrated by execution. A sovereign user should be able to compile everything he chooses to compile, provided that he is capable of sovereign choice.
@Altoid
You are missing the point. The Dead Parrot Loop is inherent to post-modernity. The rule of the game is that you have to simulate innovations, modernization and technological progress. It was not "planned and executed deliberately". It was predetermined.
2003: OSS → ALSA
2008: PulseAudio (on top of ALSA)
2011+: sysvinit → systemd
2020s: PulseAudio → PipeWire
2025–26: coreutils → Rust Coreutils (uutils)
Same rake. New tines. Same whack in the face.
The Mechanism
1. Thing breaks.
2. Failure is narrated as success — "it's a modernization," "it's a converged system," "it's a memory-safe rewrite."
3. Because it was narrated as success, repeating it is rational.
4. It breaks again.
5. Goto 2.
The language closes the feedback loop. If the output is labeled "success," the input that produced it is validated. You can't learn from an experience that the institutional narrative has already classified as a win.
The Dead Parrot
Monty Python, 1969. The shopkeeper holds a clearly dead parrot and escalates: "It's resting! It's pining for the fjords!" The customer points at the obvious. The shopkeeper will not accept it.
The customer is the only one still operating in the category where "dead" means "dead." The moment you accept the shopkeeper's vocabulary — "it's resting," "it's pining" — you lose the ability to say "it's dead," and therefore you lose the ability to refuse the next one.
The user who says "it's broken" is doing the only thing that breaks the loop. And they're always told they're the problem.
The removal of the notorious PulseAudio workaround from the Firefox/Cubeb ALSA backend unveils a new reality. The purified ALSA backend, aligned with BlueALSA, AAC CBR, and modest hardware (the USB-BT540 adapter and Sennheiser HD 450BT headphones), yields superior sound quality compared to the standard ALSA configuration.
This exemplifies a true dialectical absurdity reminiscent of Monty Python: a massive, complex system (ALSA) failing to solve a problem that a tiny, focused tool (BlueALSA) handles elegantly.
The reason is clear. Just as the architect in the Monty Python sketch designs a building that fails its primary purpose — a house that does not let you walk through it — the traditional Linux audio stack often prioritizes universal compatibility over functional simplicity.
BlueALSA possesses its own bugs and workarounds; however, its codebase remains minuscule compared to ALSA. This disparity explains the situation entirely.
BlueALSA's entire "quirks surface" is tiny compared to the vast, decades-deep hardware-quirk machinery in the Linux kernel's ALSA subsystem. Scale differences like that plausibly explain why BlueALSA's bug surface feels so much smaller and more tractable.
alsa-lib's complexity is substantial and driven largely by breadth of hardware support: the per-device quirk table alone in src/conf/cards/USB-Audio.conf lists dozens of named devices needing special-cased dmix disabling, surround-channel remapping, and IEC958 device-numbering exceptions. This is on top of the general PCM plugin architecture (format conversion, rate conversion, routing, software volume, etc.) that alsa-lib maintains to support arbitrary hardware capabilities.
A userspace Bluetooth bridge like BlueALSA, by design, only needs to target a much narrower surface: BlueZ's Bluetooth audio profiles (A2DP/HFP) on one side, and a generic ALSA PCM I/O device on the other — it doesn't need to enumerate and special-case hundreds of USB/PCI/HDA sound cards. So the size disparity is architecturally plausible: alsa-lib accumulates workarounds because it's the common layer for all Linux sound hardware, while BlueALSA's scope is deliberately narrow.
Dialectically, absurdity is the hidden truth of normality (Albert Camus). ALSA users are unlikely to notice the comical irony of the situation, yet it is absurd that a minimal userspace daemon (BlueALSA, comprising only a few thousand lines of code) outperforms the kernel-level subsystem and userspace server (ALSA, consisting of millions of lines) in both sound quality and stability for this specific use case. The "correct" enterprise-grade architecture (the full Linux audio stack) proves to be the broken building, while the "hacky" side project (BlueALSA) is the only one that lets you walk through the door.
Imagine you have two vehicles: a car and a bike, both neglected and rusty. The car is a seized limousine with thousands of grinding parts. Fixing it requires rebuilding the entire transmission. The bike is a lightweight frame with minor rust. Because it has only 50 parts, you can see the friction points instantly. You can file it down and oil the chain in minutes. Once you fix the bike, it rides faster and smoother than the car ever could.
Dialectically, absurdity is inherent to normality. Attempting to destroy absurdity inevitably destroys normality, strengthens the absurd, and unleashes chaos. Absurdity cannot be defeated. But you may find a practical solution to it.
Configure AAC codec for CBR
$ grep "^BA_OPTS" /etc/init.d/bluealsad
BA_OPTS="-S -p a2dp-source -p hfp-ag -p hfp-hf -c -sbc -c aac -c msbc --aac-afterburner --aac-bitrate=320000"Play video in YouTube
firefox-dev https://youtu.be/wINCpPCDLyoMonitor actual AAC bit rate
$ hcitop -d 1
HCI BUS ADDR FLAGS RX TX RX/s TX/s
hci0 USB A0:AD:9F:6F:51:58 U RPI 23 KB 1 MB 630 B 41771 Bhcitop's TX/s column is in bytes/sec. So 40000 B/s × 8 = 320,000 b/s — exactly matching AAC encoder's configured bit rate: 320000 b/s. Option: --aac-bitrate=320000
$ ./hcistat
hci0: 333392 b/s
hci0: 333400 b/s
hci0: 333392 b/s
hci0: 333392 b/s
hci0: 333400 b/s
hci0: 341144 b/s
hci0: 333392 b/s
hci0: 333392 b/sThe extra ~4–9% is just "packaging overhead" (headers/wrappers Bluetooth adds around audio data).
The AAC payload itself is set to 320,000 b/s via AACENC_BITRATE, but before it leaves the adapter, that payload gets wrapped in an RTP header and written out over the Bluetooth socket. hcitop and hcistat measure raw bytes at the HCI adapter level via hci_devinfo(), which counts those RTP/L2CAP/AVDTP headers too — hence the ~4–9% gap above the pure 320 kb/s AAC bitrate.
If Bluetooth audio quality is still worse than on macOS, consider fixing bugs in the ALSA backend you are using.
You can also add logging to bluez-alsa/src/a2dp-aac.c, for example:
CBR (320 kb/s)
$ sudo -u bluealsad bluealsad -p a2dp-source -p hfp-ag -p hfp-hf -c -sbc -c aac -c msbc --aac-afterburner --aac-bitrate=320000
...
bluealsad: I: AAC: Negotiated A2DP configuration: sample rate: 44100 Hz, channels: 2, VBR: no, bit rate: 320000 b/s
bluealsad: I: AAC: Applied encoder configuration: object type: MPEG-2 AAC-LC, bit rate mode: CBR, bit rate: 320000 b/s, peak bit rate: 320000 b/s, bandwidth: 17000 Hz, afterburner: on, LATM: v1$ ./hcistat
hci0: 333264 b/s
hci0: 341016 b/s
hci0: 333272 b/s
hci0: 333264 b/s
hci0: 333264 b/s
hci0: 333272 b/s
hci0: 333264 b/s
hci0: 333264 b/sVBR
$ sudo -u bluealsad bluealsad -p a2dp-source -p hfp-ag -p hfp-hf -c -sbc -c aac -c msbc --aac-afterburner --aac-bitrate=320000 --aac-vbr
...
bluealsad: I: AAC: Negotiated A2DP configuration: sample rate: 44100 Hz, channels: 2, VBR: yes, bit rate: 320000 b/s
bluealsad: I: AAC: Applied encoder configuration: object type: MPEG-2 AAC-LC, bit rate mode: VBR-5, bit rate: N/A b/s, peak bit rate: 320000 b/s, bandwidth: 19293 Hz, afterburner: on, LATM: v1$ ./hcistat
hci0: 150928 b/s
hci0: 164696 b/s
hci0: 156928 b/s
hci0: 152600 b/s
hci0: 151528 b/s
hci0: 168744 b/s
hci0: 157192 b/s
hci0: 143704 b/s
hci0: 155896 b/s
hci0: 170248 b/sBuid-Deps: +libncurses-dev
Configure script: +--localstatedir=/var
Install Buid-Deps
sudo apt install libfdk-aac-dev libasound2-dev libbluetooth-dev libdbus-1-dev libglib2.0-dev libsbc-dev liblc3-dev libldacbt-enc-dev libldacbt-abr-dev libmp3lame-dev libmpg123-dev libspandsp-dev libopus-dev libncurses-dev libbsd-dev libreadline-dev check dbus-daemon python3-docutils pkgconf libudev-dev libell-devClone the source and run autoreconf
mkdir BUILD2 && cd BUILD2
git clone https://github.com/arkq/bluez-alsa.git
cd bluez-alsa && autoreconf --install --forceCreate a build directory
mkdir ../build && cd ../buildRun the configure script from within the build directory
"$(dirname "$(pwd)")"/bluez-alsa/configure --prefix=/usr --localstatedir=/var --sysconfdir=/etc --with-bluealsaduser=bluealsad --enable-ldac --with-libfreeaptx --enable-aptx --enable-aptx-hd --enable-faststream --enable-lc3-swb --enable-mp3lame --enable-mpg123 --enable-msbc --enable-ofono --enable-midi --enable-opus --enable-upower --enable-rfcomm --enable-a2dpconf --enable-hcitop --enable-test --enable-aac --enable-manpages --disable-systemd Compile
makeInstall to debdir
make DESTDIR="$(dirname $(pwd))"/debdir install How to make a Debian package
_https://dev1galaxy.org/viewtopic.php?id=7671
Remove PulseAudio ALSA plugin workaround from ALSA backend
This change removes the PulseAudio ALSA plugin workaround code that was
added to work around a bug where the PulseAudio server forces higher
latency than requested but the plugin doesn't update its internal state.
Rationale:
- Users who want PulseAudio should use the dedicated PulseAudio backends
(pulse or pulse-rust) instead of the ALSA backend with plugin workarounds
- The ALSA backend is intended for direct hardware access for users without
PulseAudio/PipeWire
- The workaround adds unnecessary complexity for a use case that shouldn't
exist
Changes made:
- Removed local_config and is_pa fields from cubeb struct
- Removed init_local_config_with_workaround() function that detected and
configured the PulseAudio plugin workaround
- Removed PulseAudio workaround detection and testing code from alsa_init()
- Removed local_config cleanup code from alsa_destroy()
- Simplified alsa_locked_pcm_open() to remove the local_config parameter
and always use standard snd_pcm_open() instead of snd_pcm_open_lconf()
- Updated all callers of alsa_locked_pcm_open() to use the simplified signature
- Kept the thread safety wrapper via cubeb_alsa_mutex which is still required
for ALSA versions before 1.0.24
Impact:
- Simplifies the ALSA backend code by removing ~90 lines of workaround code
- Eliminates confusion about PulseAudio plugin handling in the ALSA backend
- Improves code readability by removing conditional configuration paths
- Maintains all essential functionality for direct ALSA hardware access
- Sound quality may improve due to removal of workaround latency adjustments
The device enumeration feature remains intact and unaffected by this change.$ cat firefox/media/libcubeb/src/cubeb_alsa.c
/*
* Copyright © 2011 Mozilla Foundation
*
* This program is made available under an ISC-style license. See the
* accompanying file LICENSE for details.
*/
#undef NDEBUG
#define _DEFAULT_SOURCE
#define _BSD_SOURCE
#if defined(__NetBSD__)
#define _NETBSD_SOURCE /* timersub() */
#endif
#define _XOPEN_SOURCE 500
#include "cubeb-internal.h"
#include "cubeb/cubeb.h"
#include "cubeb_tracing.h"
#include <alsa/asoundlib.h>
#include <assert.h>
#include <dlfcn.h>
#include <limits.h>
#include <poll.h>
#include <pthread.h>
#include <sys/time.h>
#include <unistd.h>
#include <stdbool.h>
#ifdef DISABLE_LIBASOUND_DLOPEN
#define WRAP(x) x
#else
#define WRAP(x) (*cubeb_##x)
#define LIBASOUND_API_VISIT(X) \
X(snd_lib_error_set_handler) \
X(snd_pcm_avail_update) \
X(snd_pcm_close) \
X(snd_pcm_delay) \
X(snd_pcm_drain) \
X(snd_pcm_frames_to_bytes) \
X(snd_pcm_get_params) \
X(snd_pcm_hw_params_any) \
X(snd_pcm_hw_params_get_channels_max) \
X(snd_pcm_hw_params_get_rate) \
X(snd_pcm_hw_params_set_rate_near) \
X(snd_pcm_hw_params_sizeof) \
X(snd_pcm_nonblock) \
X(snd_pcm_open) \
X(snd_pcm_open_lconf) \
X(snd_pcm_pause) \
X(snd_pcm_poll_descriptors) \
X(snd_pcm_poll_descriptors_count) \
X(snd_pcm_poll_descriptors_revents) \
X(snd_pcm_readi) \
X(snd_pcm_recover) \
X(snd_pcm_set_params) \
X(snd_pcm_start) \
X(snd_pcm_state) \
X(snd_pcm_writei) \
X(snd_device_name_hint) \
X(snd_device_name_get_hint) \
X(snd_device_name_free_hint) \
X(snd_pcm_hw_params_test_format) \
X(snd_pcm_hw_params_get_rate_min) \
X(snd_pcm_hw_params_get_rate_max) \
X(snd_pcm_hw_params_get_channels_min) \
X(snd_pcm_hw_params_get_channels_max) \
X(snd_card_next) \
X(snd_ctl_close) \
X(snd_ctl_open) \
X(snd_ctl_pcm_info) \
X(snd_ctl_pcm_next_device) \
X(snd_pcm_info) \
X(snd_ctl_card_info) \
X(snd_pcm_info_set_device) \
X(snd_pcm_info_set_stream) \
X(snd_pcm_hw_params_get_format_mask) \
X(snd_pcm_format_mask_test) \
X(snd_pcm_info_set_subdevice)
#define MAKE_TYPEDEF(x) static typeof(x) * cubeb_##x;
LIBASOUND_API_VISIT(MAKE_TYPEDEF);
#undef MAKE_TYPEDEF
/* snd_pcm_hw_params_alloca is actually a macro */
#define snd_pcm_hw_params_sizeof cubeb_snd_pcm_hw_params_sizeof
#endif
#define CUBEB_STREAM_MAX 16
#define CUBEB_WATCHDOG_MS 10000
#define CUBEB_ALSA_PCM_NAME "default"
/* ALSA is not thread-safe. snd_pcm_t instances are individually protected
by the owning cubeb_stream's mutex. snd_pcm_t creation and destruction
is not thread-safe until ALSA 1.0.24 (see alsa-lib.git commit 91c9c8f1),
so those calls must be wrapped in the following mutex. */
static pthread_mutex_t cubeb_alsa_mutex = PTHREAD_MUTEX_INITIALIZER;
static int cubeb_alsa_error_handler_set = 0;
static struct cubeb_ops const alsa_ops;
struct cubeb {
struct cubeb_ops const * ops;
void * libasound;
pthread_t thread;
/* Mutex for streams array, must not be held while blocked in poll(2). */
pthread_mutex_t mutex;
/* Sparse array of streams managed by this context. */
cubeb_stream * streams[CUBEB_STREAM_MAX];
/* fds and nfds are only updated by alsa_run when rebuild is set. */
struct pollfd * fds;
nfds_t nfds;
int rebuild;
int shutdown;
/* Control pipe for forcing poll to wake and rebuild fds or recalculate the
* timeout. */
int control_fd_read;
int control_fd_write;
/* Track number of active streams. This is limited to CUBEB_STREAM_MAX
due to resource contraints. */
unsigned int active_streams;
};
enum stream_state { INACTIVE, RUNNING, DRAINING, PROCESSING, ERROR };
struct cubeb_stream {
/* Note: Must match cubeb_stream layout in cubeb.c. */
cubeb * context;
void * user_ptr;
/**/
pthread_mutex_t mutex;
snd_pcm_t * pcm;
cubeb_data_callback data_callback;
cubeb_state_callback state_callback;
snd_pcm_uframes_t stream_position;
snd_pcm_uframes_t last_position;
snd_pcm_uframes_t buffer_size;
cubeb_stream_params params;
/* Every member after this comment is protected by the owning context's
mutex rather than the stream's mutex, or is only used on the context's
run thread. */
pthread_cond_t cond; /* Signaled when the stream's state is changed. */
enum stream_state state;
struct pollfd * saved_fds; /* A copy of the pollfds passed in at init time. */
struct pollfd *
fds; /* Pointer to this waitable's pollfds within struct cubeb's fds. */
nfds_t nfds;
struct timeval drain_timeout;
/* XXX: Horrible hack -- if an active stream has been idle for
CUBEB_WATCHDOG_MS it will be disabled and the error callback will be
called. This works around a bug seen with older versions of ALSA and
PulseAudio where streams would stop requesting new data despite still
being logically active and playing. */
struct timeval last_activity;
float volume;
char * buffer;
snd_pcm_uframes_t bufframes;
snd_pcm_stream_t stream_type;
struct cubeb_stream * other_stream;
};
static int
any_revents(struct pollfd * fds, nfds_t nfds)
{
nfds_t i;
for (i = 0; i < nfds; ++i) {
if (fds[i].revents) {
return 1;
}
}
return 0;
}
static int
cmp_timeval(struct timeval * a, struct timeval * b)
{
if (a->tv_sec == b->tv_sec) {
if (a->tv_usec == b->tv_usec) {
return 0;
}
return a->tv_usec > b->tv_usec ? 1 : -1;
}
return a->tv_sec > b->tv_sec ? 1 : -1;
}
static int
timeval_to_relative_ms(struct timeval * tv)
{
struct timeval now;
struct timeval dt;
long long t;
int r;
gettimeofday(&now, NULL);
r = cmp_timeval(tv, &now);
if (r >= 0) {
timersub(tv, &now, &dt);
} else {
timersub(&now, tv, &dt);
}
t = dt.tv_sec;
t *= 1000;
t += (dt.tv_usec + 500) / 1000;
if (t > INT_MAX) {
t = INT_MAX;
} else if (t < INT_MIN) {
t = INT_MIN;
}
return r >= 0 ? t : -t;
}
static int
ms_until(struct timeval * tv)
{
return timeval_to_relative_ms(tv);
}
static int
ms_since(struct timeval * tv)
{
return -timeval_to_relative_ms(tv);
}
static void
rebuild(cubeb * ctx)
{
nfds_t nfds;
int i;
nfds_t j;
cubeb_stream * stm;
assert(ctx->rebuild);
/* Always count context's control pipe fd. */
nfds = 1;
for (i = 0; i < CUBEB_STREAM_MAX; ++i) {
stm = ctx->streams[i];
if (stm) {
stm->fds = NULL;
if (stm->state == RUNNING) {
nfds += stm->nfds;
}
}
}
free(ctx->fds);
ctx->fds = calloc(nfds, sizeof(struct pollfd));
assert(ctx->fds);
ctx->nfds = nfds;
/* Include context's control pipe fd. */
ctx->fds[0].fd = ctx->control_fd_read;
ctx->fds[0].events = POLLIN | POLLERR;
for (i = 0, j = 1; i < CUBEB_STREAM_MAX; ++i) {
stm = ctx->streams[i];
if (stm && stm->state == RUNNING) {
memcpy(&ctx->fds[j], stm->saved_fds, stm->nfds * sizeof(struct pollfd));
stm->fds = &ctx->fds[j];
j += stm->nfds;
}
}
ctx->rebuild = 0;
}
static void
poll_wake(cubeb * ctx)
{
if (write(ctx->control_fd_write, "x", 1) < 0) {
/* ignore write error */
}
}
static void
set_timeout(struct timeval * timeout, unsigned int ms)
{
gettimeofday(timeout, NULL);
timeout->tv_sec += ms / 1000;
timeout->tv_usec += (ms % 1000) * 1000;
}
static void
stream_buffer_decrement(cubeb_stream * stm, long count)
{
if (count < 0 || (snd_pcm_uframes_t)count > stm->bufframes) {
count = stm->bufframes;
}
char * bufremains =
stm->buffer + WRAP(snd_pcm_frames_to_bytes)(stm->pcm, count);
memmove(stm->buffer, bufremains,
WRAP(snd_pcm_frames_to_bytes)(stm->pcm, stm->bufframes - count));
stm->bufframes -= count;
}
static void
alsa_set_stream_state(cubeb_stream * stm, enum stream_state state)
{
cubeb * ctx;
int r;
ctx = stm->context;
stm->state = state;
r = pthread_cond_broadcast(&stm->cond);
assert(r == 0);
ctx->rebuild = 1;
poll_wake(ctx);
}
static enum stream_state
alsa_process_stream(cubeb_stream * stm)
{
unsigned short revents;
snd_pcm_sframes_t avail;
int draining;
draining = 0;
pthread_mutex_lock(&stm->mutex);
/* Call _poll_descriptors_revents() even if we don't use it
to let underlying plugins clear null events. Otherwise poll()
may wake up again and again, producing unnecessary CPU usage. */
WRAP(snd_pcm_poll_descriptors_revents)
(stm->pcm, stm->fds, stm->nfds, &revents);
avail = WRAP(snd_pcm_avail_update)(stm->pcm);
/* Got null event? Bail and wait for another wakeup. */
if (avail == 0) {
pthread_mutex_unlock(&stm->mutex);
return RUNNING;
}
/* This could happen if we were suspended with SIGSTOP/Ctrl+Z for a long time.
*/
if ((unsigned int)avail > stm->buffer_size) {
avail = stm->buffer_size;
}
/* Capture: Read available frames */
if (stm->stream_type == SND_PCM_STREAM_CAPTURE && avail > 0) {
snd_pcm_sframes_t got;
if (avail + stm->bufframes > stm->buffer_size) {
/* Buffer overflow. Skip and overwrite with new data. */
stm->bufframes = 0;
// TODO: should it be marked as DRAINING?
}
got = WRAP(snd_pcm_readi)(stm->pcm, stm->buffer + stm->bufframes, avail);
if (got < 0) {
avail = got; // the error handler below will recover us
} else {
stm->bufframes += got;
stm->stream_position += got;
gettimeofday(&stm->last_activity, NULL);
}
}
/* Capture: Pass read frames to callback function */
if (stm->stream_type == SND_PCM_STREAM_CAPTURE && stm->bufframes > 0 &&
(!stm->other_stream ||
stm->other_stream->bufframes < stm->other_stream->buffer_size)) {
snd_pcm_sframes_t wrote = stm->bufframes;
struct cubeb_stream * mainstm = stm->other_stream ? stm->other_stream : stm;
void * other_buffer = stm->other_stream ? stm->other_stream->buffer +
stm->other_stream->bufframes
: NULL;
/* Correct write size to the other stream available space */
if (stm->other_stream &&
wrote > (snd_pcm_sframes_t)(stm->other_stream->buffer_size -
stm->other_stream->bufframes)) {
wrote = stm->other_stream->buffer_size - stm->other_stream->bufframes;
}
pthread_mutex_unlock(&stm->mutex);
wrote = stm->data_callback(mainstm, stm->user_ptr, stm->buffer,
other_buffer, wrote);
pthread_mutex_lock(&stm->mutex);
if (wrote < 0) {
avail = wrote; // the error handler below will recover us
} else {
stream_buffer_decrement(stm, wrote);
if (stm->other_stream) {
stm->other_stream->bufframes += wrote;
}
}
}
/* Playback: Don't have enough data? Let's ask for more. */
if (stm->stream_type == SND_PCM_STREAM_PLAYBACK &&
avail > (snd_pcm_sframes_t)stm->bufframes &&
(!stm->other_stream || stm->other_stream->bufframes > 0)) {
long got = avail - stm->bufframes;
void * other_buffer = stm->other_stream ? stm->other_stream->buffer : NULL;
char * buftail =
stm->buffer + WRAP(snd_pcm_frames_to_bytes)(stm->pcm, stm->bufframes);
/* Correct read size to the other stream available frames */
if (stm->other_stream &&
got > (snd_pcm_sframes_t)stm->other_stream->bufframes) {
got = stm->other_stream->bufframes;
}
pthread_mutex_unlock(&stm->mutex);
got = stm->data_callback(stm, stm->user_ptr, other_buffer, buftail, got);
pthread_mutex_lock(&stm->mutex);
if (got < 0) {
avail = got; // the error handler below will recover us
} else {
stm->bufframes += got;
if (stm->other_stream) {
stream_buffer_decrement(stm->other_stream, got);
}
}
}
/* Playback: Still don't have enough data? Add some silence. */
if (stm->stream_type == SND_PCM_STREAM_PLAYBACK &&
avail > (snd_pcm_sframes_t)stm->bufframes) {
long drain_frames = avail - stm->bufframes;
double drain_time = (double)drain_frames / stm->params.rate;
char * buftail =
stm->buffer + WRAP(snd_pcm_frames_to_bytes)(stm->pcm, stm->bufframes);
memset(buftail, 0, WRAP(snd_pcm_frames_to_bytes)(stm->pcm, drain_frames));
stm->bufframes = avail;
/* Mark as draining, unless we're waiting for capture */
if (!stm->other_stream || stm->other_stream->bufframes > 0) {
set_timeout(&stm->drain_timeout, drain_time * 1000);
draining = 1;
}
}
/* Playback: Have enough data and no errors. Let's write it out. */
if (stm->stream_type == SND_PCM_STREAM_PLAYBACK && avail > 0) {
snd_pcm_sframes_t wrote;
if (stm->params.format == CUBEB_SAMPLE_FLOAT32NE) {
float * b = (float *)stm->buffer;
for (uint32_t i = 0; i < avail * stm->params.channels; i++) {
b[i] *= stm->volume;
}
} else {
short * b = (short *)stm->buffer;
for (uint32_t i = 0; i < avail * stm->params.channels; i++) {
b[i] *= stm->volume;
}
}
wrote = WRAP(snd_pcm_writei)(stm->pcm, stm->buffer, avail);
if (wrote < 0) {
avail = wrote; // the error handler below will recover us
} else {
stream_buffer_decrement(stm, wrote);
stm->stream_position += wrote;
gettimeofday(&stm->last_activity, NULL);
}
}
/* Got some error? Let's try to recover the stream. */
if (avail < 0) {
avail = WRAP(snd_pcm_recover)(stm->pcm, avail, 0);
/* Capture pcm must be started after initial setup/recover */
if (avail >= 0 && stm->stream_type == SND_PCM_STREAM_CAPTURE &&
WRAP(snd_pcm_state)(stm->pcm) == SND_PCM_STATE_PREPARED) {
avail = WRAP(snd_pcm_start)(stm->pcm);
}
}
/* Failed to recover, this stream must be broken. */
if (avail < 0) {
pthread_mutex_unlock(&stm->mutex);
stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR);
return ERROR;
}
pthread_mutex_unlock(&stm->mutex);
return draining ? DRAINING : RUNNING;
}
static int
alsa_run(cubeb * ctx)
{
int r;
int timeout;
int i;
char dummy;
cubeb_stream * stm;
enum stream_state state;
pthread_mutex_lock(&ctx->mutex);
if (ctx->rebuild) {
rebuild(ctx);
}
/* Wake up at least once per second for the watchdog. */
timeout = 1000;
for (i = 0; i < CUBEB_STREAM_MAX; ++i) {
stm = ctx->streams[i];
if (stm && stm->state == DRAINING) {
r = ms_until(&stm->drain_timeout);
if (r >= 0 && timeout > r) {
timeout = r;
}
}
}
pthread_mutex_unlock(&ctx->mutex);
r = poll(ctx->fds, ctx->nfds, timeout);
pthread_mutex_lock(&ctx->mutex);
if (r > 0) {
if (ctx->fds[0].revents & POLLIN) {
if (read(ctx->control_fd_read, &dummy, 1) < 0) {
/* ignore read error */
}
if (ctx->shutdown) {
pthread_mutex_unlock(&ctx->mutex);
return -1;
}
}
for (i = 0; i < CUBEB_STREAM_MAX; ++i) {
stm = ctx->streams[i];
/* We can't use snd_pcm_poll_descriptors_revents here because of
https://github.com/kinetiknz/cubeb/issues/135. */
if (stm && stm->state == RUNNING && stm->fds &&
any_revents(stm->fds, stm->nfds)) {
alsa_set_stream_state(stm, PROCESSING);
pthread_mutex_unlock(&ctx->mutex);
state = alsa_process_stream(stm);
pthread_mutex_lock(&ctx->mutex);
alsa_set_stream_state(stm, state);
}
}
} else if (r == 0) {
for (i = 0; i < CUBEB_STREAM_MAX; ++i) {
stm = ctx->streams[i];
if (stm) {
if (stm->state == DRAINING && ms_since(&stm->drain_timeout) >= 0) {
alsa_set_stream_state(stm, INACTIVE);
stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_DRAINED);
} else if (stm->state == RUNNING &&
ms_since(&stm->last_activity) > CUBEB_WATCHDOG_MS) {
alsa_set_stream_state(stm, ERROR);
stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR);
}
}
}
}
pthread_mutex_unlock(&ctx->mutex);
return 0;
}
static void *
alsa_run_thread(void * context)
{
cubeb * ctx = context;
int r;
CUBEB_REGISTER_THREAD("cubeb rendering thread");
do {
r = alsa_run(ctx);
} while (r >= 0);
CUBEB_UNREGISTER_THREAD();
return NULL;
}
static int
alsa_locked_pcm_open(snd_pcm_t ** pcm, char const * pcm_name,
snd_pcm_stream_t stream)
{
int r;
pthread_mutex_lock(&cubeb_alsa_mutex);
r = WRAP(snd_pcm_open)(pcm, pcm_name, stream, SND_PCM_NONBLOCK);
pthread_mutex_unlock(&cubeb_alsa_mutex);
return r;
}
static int
alsa_locked_pcm_close(snd_pcm_t * pcm)
{
int r;
pthread_mutex_lock(&cubeb_alsa_mutex);
r = WRAP(snd_pcm_close)(pcm);
pthread_mutex_unlock(&cubeb_alsa_mutex);
return r;
}
static int
alsa_register_stream(cubeb * ctx, cubeb_stream * stm)
{
int i;
pthread_mutex_lock(&ctx->mutex);
for (i = 0; i < CUBEB_STREAM_MAX; ++i) {
if (!ctx->streams[i]) {
ctx->streams[i] = stm;
break;
}
}
pthread_mutex_unlock(&ctx->mutex);
return i == CUBEB_STREAM_MAX;
}
static void
alsa_unregister_stream(cubeb_stream * stm)
{
cubeb * ctx;
int i;
ctx = stm->context;
pthread_mutex_lock(&ctx->mutex);
for (i = 0; i < CUBEB_STREAM_MAX; ++i) {
if (ctx->streams[i] == stm) {
ctx->streams[i] = NULL;
break;
}
}
pthread_mutex_unlock(&ctx->mutex);
}
static void
silent_error_handler(char const * file, int line, char const * function,
int err, char const * fmt, ...)
{
(void)file;
(void)line;
(void)function;
(void)err;
(void)fmt;
}
/*static*/ int
alsa_init(cubeb ** context, char const * context_name)
{
(void)context_name;
void * libasound = NULL;
cubeb * ctx;
int r;
int i;
int fd[2];
pthread_attr_t attr;
snd_pcm_t * dummy;
assert(context);
*context = NULL;
#ifndef DISABLE_LIBASOUND_DLOPEN
libasound = dlopen("libasound.so.2", RTLD_LAZY);
if (!libasound) {
libasound = dlopen("libasound.so", RTLD_LAZY);
if (!libasound) {
return CUBEB_ERROR;
}
}
#define LOAD(x) \
{ \
cubeb_##x = dlsym(libasound, #x); \
if (!cubeb_##x) { \
dlclose(libasound); \
return CUBEB_ERROR; \
} \
}
LIBASOUND_API_VISIT(LOAD);
#undef LOAD
#endif
pthread_mutex_lock(&cubeb_alsa_mutex);
if (!cubeb_alsa_error_handler_set) {
WRAP(snd_lib_error_set_handler)(silent_error_handler);
cubeb_alsa_error_handler_set = 1;
}
pthread_mutex_unlock(&cubeb_alsa_mutex);
ctx = calloc(1, sizeof(*ctx));
assert(ctx);
ctx->ops = &alsa_ops;
ctx->libasound = libasound;
r = pthread_mutex_init(&ctx->mutex, NULL);
assert(r == 0);
r = pipe(fd);
assert(r == 0);
for (i = 0; i < 2; ++i) {
fcntl(fd[i], F_SETFD, fcntl(fd[i], F_GETFD) | FD_CLOEXEC);
fcntl(fd[i], F_SETFL, fcntl(fd[i], F_GETFL) | O_NONBLOCK);
}
ctx->control_fd_read = fd[0];
ctx->control_fd_write = fd[1];
/* Force an early rebuild when alsa_run is first called to ensure fds and
nfds have been initialized. */
ctx->rebuild = 1;
r = pthread_attr_init(&attr);
assert(r == 0);
r = pthread_attr_setstacksize(&attr, 256 * 1024);
assert(r == 0);
r = pthread_create(&ctx->thread, &attr, alsa_run_thread, ctx);
assert(r == 0);
r = pthread_attr_destroy(&attr);
assert(r == 0);
/* Open a dummy PCM to force the configuration space to be evaluated */
r = alsa_locked_pcm_open(&dummy, CUBEB_ALSA_PCM_NAME, SND_PCM_STREAM_PLAYBACK);
if (r >= 0) {
alsa_locked_pcm_close(dummy);
}
*context = ctx;
return CUBEB_OK;
}
static char const *
alsa_get_backend_id(cubeb * ctx)
{
(void)ctx;
return "alsa";
}
static void
alsa_destroy(cubeb * ctx)
{
int r;
assert(ctx);
pthread_mutex_lock(&ctx->mutex);
ctx->shutdown = 1;
poll_wake(ctx);
pthread_mutex_unlock(&ctx->mutex);
r = pthread_join(ctx->thread, NULL);
assert(r == 0);
close(ctx->control_fd_read);
close(ctx->control_fd_write);
pthread_mutex_destroy(&ctx->mutex);
free(ctx->fds);
#ifndef DISABLE_LIBASOUND_DLOPEN
if (ctx->libasound) {
dlclose(ctx->libasound);
}
#endif
free(ctx);
}
static void
alsa_stream_destroy(cubeb_stream * stm);
static int
alsa_stream_init_single(cubeb * ctx, cubeb_stream ** stream,
char const * stream_name, snd_pcm_stream_t stream_type,
cubeb_devid deviceid,
cubeb_stream_params * stream_params,
unsigned int latency_frames,
cubeb_data_callback data_callback,
cubeb_state_callback state_callback, void * user_ptr)
{
(void)stream_name;
cubeb_stream * stm;
int r;
snd_pcm_format_t format;
snd_pcm_uframes_t period_size;
int latency_us = 0;
char const * pcm_name =
deviceid ? (char const *)deviceid : CUBEB_ALSA_PCM_NAME;
assert(ctx && stream);
*stream = NULL;
if (stream_params->prefs & CUBEB_STREAM_PREF_LOOPBACK) {
return CUBEB_ERROR_NOT_SUPPORTED;
}
switch (stream_params->format) {
case CUBEB_SAMPLE_S16LE:
format = SND_PCM_FORMAT_S16_LE;
break;
case CUBEB_SAMPLE_S16BE:
format = SND_PCM_FORMAT_S16_BE;
break;
case CUBEB_SAMPLE_FLOAT32LE:
format = SND_PCM_FORMAT_FLOAT_LE;
break;
case CUBEB_SAMPLE_FLOAT32BE:
format = SND_PCM_FORMAT_FLOAT_BE;
break;
default:
return CUBEB_ERROR_INVALID_FORMAT;
}
pthread_mutex_lock(&ctx->mutex);
if (ctx->active_streams >= CUBEB_STREAM_MAX) {
pthread_mutex_unlock(&ctx->mutex);
return CUBEB_ERROR;
}
ctx->active_streams += 1;
pthread_mutex_unlock(&ctx->mutex);
stm = calloc(1, sizeof(*stm));
assert(stm);
stm->context = ctx;
stm->data_callback = data_callback;
stm->state_callback = state_callback;
stm->user_ptr = user_ptr;
stm->params = *stream_params;
stm->state = INACTIVE;
stm->volume = 1.0;
stm->buffer = NULL;
stm->bufframes = 0;
stm->stream_type = stream_type;
stm->other_stream = NULL;
r = pthread_mutex_init(&stm->mutex, NULL);
assert(r == 0);
r = pthread_cond_init(&stm->cond, NULL);
assert(r == 0);
r = alsa_locked_pcm_open(&stm->pcm, pcm_name, stm->stream_type);
if (r < 0) {
alsa_stream_destroy(stm);
return CUBEB_ERROR;
}
r = WRAP(snd_pcm_nonblock)(stm->pcm, 1);
assert(r == 0);
latency_us = latency_frames * 1e6 / stm->params.rate;
r = WRAP(snd_pcm_set_params)(stm->pcm, format, SND_PCM_ACCESS_RW_INTERLEAVED,
stm->params.channels, stm->params.rate, 1,
latency_us);
if (r < 0) {
alsa_stream_destroy(stm);
return CUBEB_ERROR_INVALID_FORMAT;
}
r = WRAP(snd_pcm_get_params)(stm->pcm, &stm->buffer_size, &period_size);
assert(r == 0);
/* Double internal buffer size to have enough space when waiting for the other
* side of duplex connection */
stm->buffer_size *= 2;
stm->buffer =
calloc(1, WRAP(snd_pcm_frames_to_bytes)(stm->pcm, stm->buffer_size));
assert(stm->buffer);
stm->nfds = WRAP(snd_pcm_poll_descriptors_count)(stm->pcm);
assert(stm->nfds > 0);
stm->saved_fds = calloc(stm->nfds, sizeof(struct pollfd));
assert(stm->saved_fds);
r = WRAP(snd_pcm_poll_descriptors)(stm->pcm, stm->saved_fds, stm->nfds);
assert((nfds_t)r == stm->nfds);
if (alsa_register_stream(ctx, stm) != 0) {
alsa_stream_destroy(stm);
return CUBEB_ERROR;
}
*stream = stm;
return CUBEB_OK;
}
static int
alsa_stream_init(cubeb * ctx, cubeb_stream ** stream, char const * stream_name,
cubeb_devid input_device,
cubeb_stream_params * input_stream_params,
cubeb_devid output_device,
cubeb_stream_params * output_stream_params,
unsigned int latency_frames, cubeb_data_callback data_callback,
cubeb_state_callback state_callback, void * user_ptr)
{
int result = CUBEB_OK;
cubeb_stream *instm = NULL, *outstm = NULL;
if (result == CUBEB_OK && input_stream_params) {
result = alsa_stream_init_single(ctx, &instm, stream_name,
SND_PCM_STREAM_CAPTURE, input_device,
input_stream_params, latency_frames,
data_callback, state_callback, user_ptr);
}
if (result == CUBEB_OK && output_stream_params) {
result = alsa_stream_init_single(ctx, &outstm, stream_name,
SND_PCM_STREAM_PLAYBACK, output_device,
output_stream_params, latency_frames,
data_callback, state_callback, user_ptr);
}
if (result == CUBEB_OK && input_stream_params && output_stream_params) {
instm->other_stream = outstm;
outstm->other_stream = instm;
}
if (result != CUBEB_OK && instm) {
alsa_stream_destroy(instm);
}
*stream = outstm ? outstm : instm;
return result;
}
static void
alsa_stream_destroy(cubeb_stream * stm)
{
int r;
cubeb * ctx;
assert(stm && (stm->state == INACTIVE || stm->state == ERROR ||
stm->state == DRAINING));
ctx = stm->context;
if (stm->other_stream) {
stm->other_stream->other_stream = NULL; // to stop infinite recursion
alsa_stream_destroy(stm->other_stream);
}
pthread_mutex_lock(&stm->mutex);
if (stm->pcm) {
if (stm->state == DRAINING) {
WRAP(snd_pcm_drain)(stm->pcm);
}
alsa_locked_pcm_close(stm->pcm);
stm->pcm = NULL;
}
free(stm->saved_fds);
pthread_mutex_unlock(&stm->mutex);
pthread_mutex_destroy(&stm->mutex);
r = pthread_cond_destroy(&stm->cond);
assert(r == 0);
alsa_unregister_stream(stm);
pthread_mutex_lock(&ctx->mutex);
assert(ctx->active_streams >= 1);
ctx->active_streams -= 1;
pthread_mutex_unlock(&ctx->mutex);
free(stm->buffer);
free(stm);
}
// max_channel_count
static int
alsa_get_max_channel_count(cubeb * ctx, uint32_t * max_channels)
{
snd_ctl_t *handle;
int card = -1;
uint32_t max = 0;
int r;
printf("DEBUG: Starting max channel count calculation\n");
if (!ctx || !max_channels) {
printf("DEBUG: Invalid parameters (ctx=%p, max_channels=%p)\n", ctx, max_channels);
return CUBEB_ERROR_INVALID_PARAMETER;
}
/* Iterate through all cards */
while (snd_card_next(&card) >= 0 && card >= 0) {
char card_name[32];
snprintf(card_name, sizeof(card_name), "hw:%d", card);
r = snd_ctl_open(&handle, card_name, 0);
if (r < 0) {
printf("DEBUG: Cannot open control for card %d: %s\n", card, snd_strerror(r));
continue;
}
/* Iterate through PCM devices on this card */
int dev = -1;
while (snd_ctl_pcm_next_device(handle, &dev) >= 0 && dev >= 0) {
snd_pcm_chmap_query_t **maps;
/* Query channel maps without opening PCM device */
maps = snd_pcm_query_chmaps_from_hw(card, dev, 0, SND_PCM_STREAM_PLAYBACK);
if (maps) {
snd_pcm_chmap_query_t **p;
for (p = maps; *p != NULL; p++) {
unsigned int channels = (*p)->map.channels;
printf("DEBUG: Device hw:%d,%d: channel map type %d, channels = %u\n",
card, dev, (*p)->type, channels);
if (channels > max) {
max = channels;
printf("DEBUG: New maximum: %u\n", max);
}
}
snd_pcm_free_chmaps(maps);
} else {
printf("DEBUG: No channel maps available for device hw:%d,%d\n", card, dev);
}
}
snd_ctl_close(handle);
}
printf("DEBUG: Final max channels: %u\n", max);
if (max == 0) {
printf("DEBUG: No valid channel counts found, using fallback\n");
max = 2; /* Fallback to stereo */
}
*max_channels = max;
return CUBEB_OK;
}
// END of max_channel_count
static int
alsa_get_preferred_sample_rate(cubeb * ctx, uint32_t * rate)
{
(void)ctx;
int r, dir;
snd_pcm_t * pcm;
snd_pcm_hw_params_t * hw_params;
snd_pcm_hw_params_alloca(&hw_params);
/* get a pcm, disabling resampling, so we get a rate the
* hardware/dmix/pulse/etc. supports. */
r = WRAP(snd_pcm_open)(&pcm, CUBEB_ALSA_PCM_NAME, SND_PCM_STREAM_PLAYBACK,
SND_PCM_NO_AUTO_RESAMPLE);
if (r < 0) {
return CUBEB_ERROR;
}
r = WRAP(snd_pcm_hw_params_any)(pcm, hw_params);
if (r < 0) {
WRAP(snd_pcm_close)(pcm);
return CUBEB_ERROR;
}
r = WRAP(snd_pcm_hw_params_get_rate)(hw_params, rate, &dir);
if (r >= 0) {
/* There is a default rate: use it. */
WRAP(snd_pcm_close)(pcm);
return CUBEB_OK;
}
/* Use a common rate, alsa may adjust it based on hw/etc. capabilities. */
*rate = 44100;
r = WRAP(snd_pcm_hw_params_set_rate_near)(pcm, hw_params, rate, NULL);
if (r < 0) {
WRAP(snd_pcm_close)(pcm);
return CUBEB_ERROR;
}
WRAP(snd_pcm_close)(pcm);
return CUBEB_OK;
}
static int
alsa_get_min_latency(cubeb * ctx, cubeb_stream_params params,
uint32_t * latency_frames)
{
(void)ctx;
/* 40ms is found to be an acceptable minimum, even on a super low-end
* machine. */
*latency_frames = 40 * params.rate / 1000;
return CUBEB_OK;
}
static int
alsa_stream_start(cubeb_stream * stm)
{
cubeb * ctx;
assert(stm);
ctx = stm->context;
if (stm->stream_type == SND_PCM_STREAM_PLAYBACK && stm->other_stream) {
int r = alsa_stream_start(stm->other_stream);
if (r != CUBEB_OK)
return r;
}
pthread_mutex_lock(&stm->mutex);
/* Capture pcm must be started after initial setup/recover */
if (stm->stream_type == SND_PCM_STREAM_CAPTURE &&
WRAP(snd_pcm_state)(stm->pcm) == SND_PCM_STATE_PREPARED) {
WRAP(snd_pcm_start)(stm->pcm);
}
WRAP(snd_pcm_pause)(stm->pcm, 0);
gettimeofday(&stm->last_activity, NULL);
pthread_mutex_unlock(&stm->mutex);
pthread_mutex_lock(&ctx->mutex);
if (stm->state != INACTIVE) {
pthread_mutex_unlock(&ctx->mutex);
return CUBEB_ERROR;
}
alsa_set_stream_state(stm, RUNNING);
pthread_mutex_unlock(&ctx->mutex);
return CUBEB_OK;
}
static int
alsa_stream_stop(cubeb_stream * stm)
{
cubeb * ctx;
int r;
assert(stm);
ctx = stm->context;
if (stm->stream_type == SND_PCM_STREAM_PLAYBACK && stm->other_stream) {
int r = alsa_stream_stop(stm->other_stream);
if (r != CUBEB_OK)
return r;
}
pthread_mutex_lock(&ctx->mutex);
while (stm->state == PROCESSING) {
r = pthread_cond_wait(&stm->cond, &ctx->mutex);
assert(r == 0);
}
alsa_set_stream_state(stm, INACTIVE);
pthread_mutex_unlock(&ctx->mutex);
pthread_mutex_lock(&stm->mutex);
WRAP(snd_pcm_pause)(stm->pcm, 1);
pthread_mutex_unlock(&stm->mutex);
return CUBEB_OK;
}
static int
alsa_stream_get_position(cubeb_stream * stm, uint64_t * position)
{
snd_pcm_sframes_t delay;
assert(stm && position);
pthread_mutex_lock(&stm->mutex);
delay = -1;
if (WRAP(snd_pcm_state)(stm->pcm) != SND_PCM_STATE_RUNNING ||
WRAP(snd_pcm_delay)(stm->pcm, &delay) != 0) {
*position = stm->last_position;
pthread_mutex_unlock(&stm->mutex);
return CUBEB_OK;
}
assert(delay >= 0);
*position = 0;
if (stm->stream_position >= (snd_pcm_uframes_t)delay) {
*position = stm->stream_position - delay;
}
stm->last_position = *position;
pthread_mutex_unlock(&stm->mutex);
return CUBEB_OK;
}
static int
alsa_stream_get_latency(cubeb_stream * stm, uint32_t * latency)
{
snd_pcm_sframes_t delay;
/* This function returns the delay in frames until a frame written using
snd_pcm_writei is sent to the DAC. The DAC delay should be < 1ms anyways.
*/
if (WRAP(snd_pcm_delay)(stm->pcm, &delay)) {
return CUBEB_ERROR;
}
*latency = delay;
return CUBEB_OK;
}
static int
alsa_stream_set_volume(cubeb_stream * stm, float volume)
{
/* setting the volume using an API call does not seem very stable/supported */
pthread_mutex_lock(&stm->mutex);
stm->volume = volume;
pthread_mutex_unlock(&stm->mutex);
return CUBEB_OK;
}
/* Helper Functions for alsa_enumerate_devices */
static bool
is_virtual_device(const char *name)
{
const char *virtual_prefixes[] = {
"plug", "dmix", "dsnoop", "default", "plughw", "tee",
"null", "sysdefault", "lavrate", "samplerate", "speexrate",
"bluealsa", "oss", "speex", "upmix", "vdownmix",
"front", "surround40", "surround41", "surround50",
"surround51", "surround71", "iec958",
NULL
};
for (int i = 0; virtual_prefixes[i] != NULL; i++) {
if (strncmp(name, virtual_prefixes[i], strlen(virtual_prefixes[i])) == 0) {
// Check if it's an exact match or followed by colon or other separator
size_t len = strlen(virtual_prefixes[i]);
if (name[len] == '\0' || name[len] == ':' || name[len] == '=') {
return true;
}
}
}
return false;
}
static int
add_bluealsa_device(cubeb_device_info **devinfop, int *collection_cnt,
cubeb_device_type type)
{
cubeb_device_info *device = &(*devinfop)[*collection_cnt];
/* A2DP is output-only (better quality) */
if (type == CUBEB_DEVICE_TYPE_OUTPUT) {
device->device_id = strdup("bluealsa:PROFILE=a2dp");
device->friendly_name = strdup("BlueALSA A2DP");
device->group_id = strdup("bluealsa");
device->vendor_name = strdup("BlueALSA");
device->format = CUBEB_DEVICE_FMT_S16LE;
device->default_format = CUBEB_DEVICE_FMT_S16LE;
device->max_channels = 2;
device->default_rate = 48000;
device->min_rate = 44100;
device->max_rate = 48000;
device->type = CUBEB_DEVICE_TYPE_OUTPUT;
device->state = CUBEB_DEVICE_STATE_ENABLED;
device->preferred = CUBEB_DEVICE_PREF_NONE;
device->latency_lo = 200;
device->latency_hi = 200;
(*collection_cnt)++;
}
/* SCO supports full duplex (both input and output) */
if (type == CUBEB_DEVICE_TYPE_OUTPUT || type == CUBEB_DEVICE_TYPE_INPUT) {
device = &(*devinfop)[*collection_cnt];
device->device_id = strdup("bluealsa:PROFILE=sco");
device->friendly_name = strdup("BlueALSA SCO");
device->group_id = strdup("bluealsa");
device->vendor_name = strdup("BlueALSA");
device->format = CUBEB_DEVICE_FMT_S16LE;
device->default_format = CUBEB_DEVICE_FMT_S16LE;
device->max_channels = 1;
device->default_rate = 16000;
device->min_rate = 16000;
device->max_rate = 16000;
device->type = type;
device->state = CUBEB_DEVICE_STATE_ENABLED;
device->preferred = CUBEB_DEVICE_PREF_NONE;
device->latency_lo = 20;
device->latency_hi = 30;
(*collection_cnt)++;
}
return CUBEB_OK;
}
static int
alsa_enumerate_devices(cubeb * context, cubeb_device_type type,
cubeb_device_collection * collection)
{
void **hints = NULL;
void **hint;
char *name = NULL;
char *io = NULL;
char *desc = NULL;
cubeb_device_info * devinfop = NULL;
int collection_cnt = 0;
int r;
if (!context)
return CUBEB_ERROR;
/* Get device name hints from ALSA */
r = WRAP(snd_device_name_hint)(-1, "pcm", &hints);
if (r < 0) {
return CUBEB_ERROR;
}
/* First pass: count physical devices */
for (hint = hints; *hint != NULL; hint++) {
name = WRAP(snd_device_name_get_hint)(*hint, "NAME");
if (!name)
continue;
/* Skip virtual devices */
if (is_virtual_device(name)) {
free(name);
continue;
}
/* Check IO type matches requested type */
io = WRAP(snd_device_name_get_hint)(*hint, "IO");
if (io) {
if (type == CUBEB_DEVICE_TYPE_INPUT && strcmp(io, "Input") != 0) {
free(name);
free(io);
continue;
}
if (type == CUBEB_DEVICE_TYPE_OUTPUT && strcmp(io, "Output") != 0) {
free(name);
free(io);
continue;
}
free(io);
}
collection_cnt++;
free(name);
}
/* Allocate device array */
devinfop = calloc(collection_cnt + 2, sizeof(cubeb_device_info));
if (!devinfop) {
WRAP(snd_device_name_free_hint)(hints);
return CUBEB_ERROR;
}
collection_cnt = 0;
/* Second pass: populate device info */
for (hint = hints; *hint != NULL; hint++) {
name = WRAP(snd_device_name_get_hint)(*hint, "NAME");
if (!name)
continue;
if (is_virtual_device(name)) {
free(name);
continue;
}
io = WRAP(snd_device_name_get_hint)(*hint, "IO");
if (io) {
if (type == CUBEB_DEVICE_TYPE_INPUT && strcmp(io, "Input") != 0) {
free(name);
free(io);
continue;
}
if (type == CUBEB_DEVICE_TYPE_OUTPUT && strcmp(io, "Output") != 0) {
free(name);
free(io);
continue;
}
free(io);
}
/* Query device capabilities by opening it temporarily */
snd_pcm_t *pcm;
snd_pcm_hw_params_t *hw_params;
snd_pcm_hw_params_alloca(&hw_params);
snd_pcm_format_mask_t *format_mask;
if (WRAP(snd_pcm_open)(&pcm, name,
type == CUBEB_DEVICE_TYPE_INPUT ? SND_PCM_STREAM_CAPTURE : SND_PCM_STREAM_PLAYBACK,
SND_PCM_NONBLOCK) >= 0) {
if (WRAP(snd_pcm_hw_params_any)(pcm, hw_params) >= 0) {
/* Query actual capabilities */
unsigned int min_rate, max_rate;
unsigned int min_channels, max_channels;
snd_pcm_format_mask_alloca(&format_mask);
WRAP(snd_pcm_hw_params_get_format_mask)(hw_params, format_mask);
WRAP(snd_pcm_hw_params_get_rate_min)(hw_params, &min_rate, NULL);
WRAP(snd_pcm_hw_params_get_rate_max)(hw_params, &max_rate, NULL);
WRAP(snd_pcm_hw_params_get_channels_min)(hw_params, &min_channels);
WRAP(snd_pcm_hw_params_get_channels_max)(hw_params, &max_channels);
/* Determine supported formats */
cubeb_device_fmt format = CUBEB_DEVICE_FMT_S16NE;
if (WRAP(snd_pcm_format_mask_test)(format_mask, SND_PCM_FORMAT_S32_LE)) {
format |= CUBEB_DEVICE_FMT_S32LE;
}
if (WRAP(snd_pcm_format_mask_test)(format_mask, SND_PCM_FORMAT_FLOAT)) {
format |= CUBEB_DEVICE_FMT_F32NE;
}
devinfop[collection_cnt].device_id = strdup(name);
devinfop[collection_cnt].devid = (cubeb_devid)devinfop[collection_cnt].device_id;
desc = WRAP(snd_device_name_get_hint)(*hint, "DESC");
devinfop[collection_cnt].friendly_name = desc ? strdup(desc) : strdup(name);
if (desc) free(desc);
devinfop[collection_cnt].group_id = strdup(name);
devinfop[collection_cnt].vendor_name = NULL;
devinfop[collection_cnt].type = type;
devinfop[collection_cnt].state = CUBEB_DEVICE_STATE_ENABLED;
devinfop[collection_cnt].preferred = CUBEB_DEVICE_PREF_NONE;
devinfop[collection_cnt].format = format;
devinfop[collection_cnt].default_format = CUBEB_DEVICE_FMT_S16NE; /* Default to S16 */
devinfop[collection_cnt].max_channels = max_channels;
devinfop[collection_cnt].default_rate = 44100;
devinfop[collection_cnt].min_rate = min_rate;
devinfop[collection_cnt].max_rate = max_rate;
devinfop[collection_cnt].latency_lo = 0;
devinfop[collection_cnt].latency_hi = 0;
collection_cnt++;
}
WRAP(snd_pcm_close)(pcm);
}
free(name);
}
WRAP(snd_device_name_free_hint)(hints);
/* Add BlueALSA devices, if BT headphones are connected */
snd_pcm_t *pcm;
int err = WRAP(snd_pcm_open)(&pcm, "bluealsa", SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK);
if (err == 0) {
/* BlueALSA is available (BT headphones connected) */
WRAP(snd_pcm_close)(pcm);
/* Add to device list */
add_bluealsa_device(&devinfop, &collection_cnt, type);
}
/* If err != 0, BlueALSA is not available (no BT headphones) */
collection->device = devinfop;
collection->count = collection_cnt;
return CUBEB_OK;
}
static int
alsa_device_collection_destroy(cubeb * context,
cubeb_device_collection * collection)
{
size_t i;
(void)context;
if (!collection->device)
return CUBEB_OK;
for (i = 0; i < collection->count; i++) {
free((void *)collection->device[i].device_id);
free((void *)collection->device[i].friendly_name);
free((void *)collection->device[i].group_id);
free((void *)collection->device[i].vendor_name);
}
free(collection->device);
collection->device = NULL;
collection->count = 0;
return CUBEB_OK;
}
static struct cubeb_ops const alsa_ops = {
.init = alsa_init,
.get_backend_id = alsa_get_backend_id,
.get_max_channel_count = alsa_get_max_channel_count,
.get_min_latency = alsa_get_min_latency,
.get_preferred_sample_rate = alsa_get_preferred_sample_rate,
.get_supported_input_processing_params = NULL,
.enumerate_devices = alsa_enumerate_devices,
.device_collection_destroy = alsa_device_collection_destroy,
.destroy = alsa_destroy,
.stream_init = alsa_stream_init,
.stream_destroy = alsa_stream_destroy,
.stream_start = alsa_stream_start,
.stream_stop = alsa_stream_stop,
.stream_get_position = alsa_stream_get_position,
.stream_get_latency = alsa_stream_get_latency,
.stream_get_input_latency = NULL,
.stream_set_volume = alsa_stream_set_volume,
.stream_set_name = NULL,
.stream_get_current_device = NULL,
.stream_set_input_mute = NULL,
.stream_set_input_processing_params = NULL,
.stream_device_destroy = NULL,
.stream_register_device_changed_callback = NULL,
.register_device_collection_changed = NULL};Sound quality is much better now.
Although, of course, semi-deaf, semi-blind, and half-demented ALSA users may not notice any difference.
It seems that almost all ALSA backends include some kind of PulseAudio workaround and/or other bugs. That is why, perhaps, ALSA has a reputation for sounding like it’s playing from inside a shoebox.
Redirecting to PulseAudio from within an ALSA backend is an architectural bug. PulseAudio is a separate userspace sound server and should be handled by its own dedicated backend (e.g., Cubeb's pulse or pulse-rust backends), not by an ALSA backend that treats the pulse ALSA plugin as if it were an ALSA device.
PulseAudio has its own IPC, latency model, stream routing, and device enumeration. Trying to handle it through the ALSA API forces the backend to carry workarounds for the PA plugin's behavior (e.g., the handle_underrun workaround), which only make sense when the plugin is actually in the path.
The ALSA backend should focus on ALSA devices, and PulseAudio should be handled by a dedicated PulseAudio backend. The pulse virtual card is an external plugin, not a native ALSA device, and conflating the two subsystems leads to unnecessary workarounds and reduced audio quality.
The PulseAudio ALSA plugin workaround is only meaningful when the pulse virtual PCM plugin from libasound2-plugins is actually in the path. When PulseAudio, the Pulse plugin, and PipeWire are removed, the workaround can degrade sound quality by forcing a configuration path that adds latency, triggers unnecessary conversions, or selects suboptimal hardware parameters for direct device access.
The PulseAudio plugin is not part of core ALSA. It is an external virtual sound card provided by libasound2-plugins and appears alongside physical cards in the control interface.
Core ALSA's built-in plugin list does not include pulse
This means the workaround is targeting an optional, userspace-server-specific plugin, not the ALSA hardware path.
When PulseAudio/PipeWire and the Pulse plugin are gone, the ALSA backend should open ALSA devices directly
(e.g., dmix with fftrate, or hardware devices, such as hw:CARD=PCH,DEV=0). The workaround, however, was built around the assumption that ALSA's default PCM might resolve through the pulse plugin. In a PulseAudio-free system, leftover detection logic or a forced local_config can:
1. Apply the wrong parameter set — the workaround adjusted hardware parameters to compensate for PulseAudio's latency behavior, which is irrelevant for direct hw access.
2. Trigger the plug plugin chain — if the requested parameters do not match the hardware, ALSA's plug plugin inserts format/rate/channel conversions.
Each conversion in that chain can add quantization, resampling artifacts, or latency.
3. Open the device through a non-default configuration — snd_pcm_open() resolves the name through the global ALSA configuration tree.
If the workaround injects a local_config that overrides the normal tree, it can route a direct hardware request through plug → pulse or through an unintended slave, even when the user expects raw hardware access.
Removing the workaround lets snd_pcm_open() follow the normal configuration path for the requested device. This is why sound quality is much better: the audio path became shorter and no longer carried PulseAudio-specific latency/buffering compensations.
The workaround was correct for its original target environment, but it is actively harmful in a clean ALSA-only setup. Removing it is the right thing to do.
The workaround was necessary for PulseAudio users when dedicated PulseAudio backends were unavailable; however, in a clean ALSA environment, it is detrimental to audio quality.
ALSA: Fix ALSA device enumeration and max channel calculation$ cat firefox/media/libcubeb/src/cubeb_alsa.c
/*
* Copyright © 2011 Mozilla Foundation
*
* This program is made available under an ISC-style license. See the
* accompanying file LICENSE for details.
*/
#undef NDEBUG
#define _DEFAULT_SOURCE
#define _BSD_SOURCE
#if defined(__NetBSD__)
#define _NETBSD_SOURCE /* timersub() */
#endif
#define _XOPEN_SOURCE 500
#include "cubeb-internal.h"
#include "cubeb/cubeb.h"
#include "cubeb_tracing.h"
#include <alsa/asoundlib.h>
#include <assert.h>
#include <dlfcn.h>
#include <limits.h>
#include <poll.h>
#include <pthread.h>
#include <sys/time.h>
#include <unistd.h>
#include <stdbool.h>
#ifdef DISABLE_LIBASOUND_DLOPEN
#define WRAP(x) x
#else
#define WRAP(x) (*cubeb_##x)
#define LIBASOUND_API_VISIT(X) \
X(snd_config) \
X(snd_config_add) \
X(snd_config_copy) \
X(snd_config_delete) \
X(snd_config_get_id) \
X(snd_config_get_string) \
X(snd_config_imake_integer) \
X(snd_config_search) \
X(snd_config_search_definition) \
X(snd_lib_error_set_handler) \
X(snd_pcm_avail_update) \
X(snd_pcm_close) \
X(snd_pcm_delay) \
X(snd_pcm_drain) \
X(snd_pcm_frames_to_bytes) \
X(snd_pcm_get_params) \
X(snd_pcm_hw_params_any) \
X(snd_pcm_hw_params_get_channels_max) \
X(snd_pcm_hw_params_get_rate) \
X(snd_pcm_hw_params_set_rate_near) \
X(snd_pcm_hw_params_sizeof) \
X(snd_pcm_nonblock) \
X(snd_pcm_open) \
X(snd_pcm_open_lconf) \
X(snd_pcm_pause) \
X(snd_pcm_poll_descriptors) \
X(snd_pcm_poll_descriptors_count) \
X(snd_pcm_poll_descriptors_revents) \
X(snd_pcm_readi) \
X(snd_pcm_recover) \
X(snd_pcm_set_params) \
X(snd_pcm_start) \
X(snd_pcm_state) \
X(snd_pcm_writei) \
X(snd_device_name_hint) \
X(snd_device_name_get_hint) \
X(snd_device_name_free_hint) \
X(snd_pcm_hw_params_test_format) \
X(snd_pcm_hw_params_get_rate_min) \
X(snd_pcm_hw_params_get_rate_max) \
X(snd_pcm_hw_params_get_channels_min) \
X(snd_pcm_hw_params_get_channels_max) \
X(snd_card_next) \
X(snd_ctl_close) \
X(snd_ctl_open) \
X(snd_ctl_pcm_info) \
X(snd_ctl_pcm_next_device) \
X(snd_pcm_info) \
X(snd_ctl_card_info) \
X(snd_pcm_info_set_device) \
X(snd_pcm_info_set_stream) \
X(snd_pcm_hw_params_get_format_mask) \
X(snd_pcm_format_mask_test) \
X(snd_pcm_info_set_subdevice)
#define MAKE_TYPEDEF(x) static typeof(x) * cubeb_##x;
LIBASOUND_API_VISIT(MAKE_TYPEDEF);
#undef MAKE_TYPEDEF
/* snd_pcm_hw_params_alloca is actually a macro */
#define snd_pcm_hw_params_sizeof cubeb_snd_pcm_hw_params_sizeof
#endif
#define CUBEB_STREAM_MAX 16
#define CUBEB_WATCHDOG_MS 10000
#define CUBEB_ALSA_PCM_NAME "default"
#define ALSA_PA_PLUGIN "ALSA <-> PulseAudio PCM I/O Plugin"
/* ALSA is not thread-safe. snd_pcm_t instances are individually protected
by the owning cubeb_stream's mutex. snd_pcm_t creation and destruction
is not thread-safe until ALSA 1.0.24 (see alsa-lib.git commit 91c9c8f1),
so those calls must be wrapped in the following mutex. */
static pthread_mutex_t cubeb_alsa_mutex = PTHREAD_MUTEX_INITIALIZER;
static int cubeb_alsa_error_handler_set = 0;
static struct cubeb_ops const alsa_ops;
struct cubeb {
struct cubeb_ops const * ops;
void * libasound;
pthread_t thread;
/* Mutex for streams array, must not be held while blocked in poll(2). */
pthread_mutex_t mutex;
/* Sparse array of streams managed by this context. */
cubeb_stream * streams[CUBEB_STREAM_MAX];
/* fds and nfds are only updated by alsa_run when rebuild is set. */
struct pollfd * fds;
nfds_t nfds;
int rebuild;
int shutdown;
/* Control pipe for forcing poll to wake and rebuild fds or recalculate the
* timeout. */
int control_fd_read;
int control_fd_write;
/* Track number of active streams. This is limited to CUBEB_STREAM_MAX
due to resource contraints. */
unsigned int active_streams;
/* Local configuration with handle_underrun workaround set for PulseAudio
ALSA plugin. Will be NULL if the PA ALSA plugin is not in use or the
workaround is not required. */
snd_config_t * local_config;
int is_pa;
};
enum stream_state { INACTIVE, RUNNING, DRAINING, PROCESSING, ERROR };
struct cubeb_stream {
/* Note: Must match cubeb_stream layout in cubeb.c. */
cubeb * context;
void * user_ptr;
/**/
pthread_mutex_t mutex;
snd_pcm_t * pcm;
cubeb_data_callback data_callback;
cubeb_state_callback state_callback;
snd_pcm_uframes_t stream_position;
snd_pcm_uframes_t last_position;
snd_pcm_uframes_t buffer_size;
cubeb_stream_params params;
/* Every member after this comment is protected by the owning context's
mutex rather than the stream's mutex, or is only used on the context's
run thread. */
pthread_cond_t cond; /* Signaled when the stream's state is changed. */
enum stream_state state;
struct pollfd * saved_fds; /* A copy of the pollfds passed in at init time. */
struct pollfd *
fds; /* Pointer to this waitable's pollfds within struct cubeb's fds. */
nfds_t nfds;
struct timeval drain_timeout;
/* XXX: Horrible hack -- if an active stream has been idle for
CUBEB_WATCHDOG_MS it will be disabled and the error callback will be
called. This works around a bug seen with older versions of ALSA and
PulseAudio where streams would stop requesting new data despite still
being logically active and playing. */
struct timeval last_activity;
float volume;
char * buffer;
snd_pcm_uframes_t bufframes;
snd_pcm_stream_t stream_type;
struct cubeb_stream * other_stream;
};
static int
any_revents(struct pollfd * fds, nfds_t nfds)
{
nfds_t i;
for (i = 0; i < nfds; ++i) {
if (fds[i].revents) {
return 1;
}
}
return 0;
}
static int
cmp_timeval(struct timeval * a, struct timeval * b)
{
if (a->tv_sec == b->tv_sec) {
if (a->tv_usec == b->tv_usec) {
return 0;
}
return a->tv_usec > b->tv_usec ? 1 : -1;
}
return a->tv_sec > b->tv_sec ? 1 : -1;
}
static int
timeval_to_relative_ms(struct timeval * tv)
{
struct timeval now;
struct timeval dt;
long long t;
int r;
gettimeofday(&now, NULL);
r = cmp_timeval(tv, &now);
if (r >= 0) {
timersub(tv, &now, &dt);
} else {
timersub(&now, tv, &dt);
}
t = dt.tv_sec;
t *= 1000;
t += (dt.tv_usec + 500) / 1000;
if (t > INT_MAX) {
t = INT_MAX;
} else if (t < INT_MIN) {
t = INT_MIN;
}
return r >= 0 ? t : -t;
}
static int
ms_until(struct timeval * tv)
{
return timeval_to_relative_ms(tv);
}
static int
ms_since(struct timeval * tv)
{
return -timeval_to_relative_ms(tv);
}
static void
rebuild(cubeb * ctx)
{
nfds_t nfds;
int i;
nfds_t j;
cubeb_stream * stm;
assert(ctx->rebuild);
/* Always count context's control pipe fd. */
nfds = 1;
for (i = 0; i < CUBEB_STREAM_MAX; ++i) {
stm = ctx->streams[i];
if (stm) {
stm->fds = NULL;
if (stm->state == RUNNING) {
nfds += stm->nfds;
}
}
}
free(ctx->fds);
ctx->fds = calloc(nfds, sizeof(struct pollfd));
assert(ctx->fds);
ctx->nfds = nfds;
/* Include context's control pipe fd. */
ctx->fds[0].fd = ctx->control_fd_read;
ctx->fds[0].events = POLLIN | POLLERR;
for (i = 0, j = 1; i < CUBEB_STREAM_MAX; ++i) {
stm = ctx->streams[i];
if (stm && stm->state == RUNNING) {
memcpy(&ctx->fds[j], stm->saved_fds, stm->nfds * sizeof(struct pollfd));
stm->fds = &ctx->fds[j];
j += stm->nfds;
}
}
ctx->rebuild = 0;
}
static void
poll_wake(cubeb * ctx)
{
if (write(ctx->control_fd_write, "x", 1) < 0) {
/* ignore write error */
}
}
static void
set_timeout(struct timeval * timeout, unsigned int ms)
{
gettimeofday(timeout, NULL);
timeout->tv_sec += ms / 1000;
timeout->tv_usec += (ms % 1000) * 1000;
}
static void
stream_buffer_decrement(cubeb_stream * stm, long count)
{
if (count < 0 || (snd_pcm_uframes_t)count > stm->bufframes) {
count = stm->bufframes;
}
char * bufremains =
stm->buffer + WRAP(snd_pcm_frames_to_bytes)(stm->pcm, count);
memmove(stm->buffer, bufremains,
WRAP(snd_pcm_frames_to_bytes)(stm->pcm, stm->bufframes - count));
stm->bufframes -= count;
}
static void
alsa_set_stream_state(cubeb_stream * stm, enum stream_state state)
{
cubeb * ctx;
int r;
ctx = stm->context;
stm->state = state;
r = pthread_cond_broadcast(&stm->cond);
assert(r == 0);
ctx->rebuild = 1;
poll_wake(ctx);
}
static enum stream_state
alsa_process_stream(cubeb_stream * stm)
{
unsigned short revents;
snd_pcm_sframes_t avail;
int draining;
draining = 0;
pthread_mutex_lock(&stm->mutex);
/* Call _poll_descriptors_revents() even if we don't use it
to let underlying plugins clear null events. Otherwise poll()
may wake up again and again, producing unnecessary CPU usage. */
WRAP(snd_pcm_poll_descriptors_revents)
(stm->pcm, stm->fds, stm->nfds, &revents);
avail = WRAP(snd_pcm_avail_update)(stm->pcm);
/* Got null event? Bail and wait for another wakeup. */
if (avail == 0) {
pthread_mutex_unlock(&stm->mutex);
return RUNNING;
}
/* This could happen if we were suspended with SIGSTOP/Ctrl+Z for a long time.
*/
if ((unsigned int)avail > stm->buffer_size) {
avail = stm->buffer_size;
}
/* Capture: Read available frames */
if (stm->stream_type == SND_PCM_STREAM_CAPTURE && avail > 0) {
snd_pcm_sframes_t got;
if (avail + stm->bufframes > stm->buffer_size) {
/* Buffer overflow. Skip and overwrite with new data. */
stm->bufframes = 0;
// TODO: should it be marked as DRAINING?
}
got = WRAP(snd_pcm_readi)(stm->pcm, stm->buffer + stm->bufframes, avail);
if (got < 0) {
avail = got; // the error handler below will recover us
} else {
stm->bufframes += got;
stm->stream_position += got;
gettimeofday(&stm->last_activity, NULL);
}
}
/* Capture: Pass read frames to callback function */
if (stm->stream_type == SND_PCM_STREAM_CAPTURE && stm->bufframes > 0 &&
(!stm->other_stream ||
stm->other_stream->bufframes < stm->other_stream->buffer_size)) {
snd_pcm_sframes_t wrote = stm->bufframes;
struct cubeb_stream * mainstm = stm->other_stream ? stm->other_stream : stm;
void * other_buffer = stm->other_stream ? stm->other_stream->buffer +
stm->other_stream->bufframes
: NULL;
/* Correct write size to the other stream available space */
if (stm->other_stream &&
wrote > (snd_pcm_sframes_t)(stm->other_stream->buffer_size -
stm->other_stream->bufframes)) {
wrote = stm->other_stream->buffer_size - stm->other_stream->bufframes;
}
pthread_mutex_unlock(&stm->mutex);
wrote = stm->data_callback(mainstm, stm->user_ptr, stm->buffer,
other_buffer, wrote);
pthread_mutex_lock(&stm->mutex);
if (wrote < 0) {
avail = wrote; // the error handler below will recover us
} else {
stream_buffer_decrement(stm, wrote);
if (stm->other_stream) {
stm->other_stream->bufframes += wrote;
}
}
}
/* Playback: Don't have enough data? Let's ask for more. */
if (stm->stream_type == SND_PCM_STREAM_PLAYBACK &&
avail > (snd_pcm_sframes_t)stm->bufframes &&
(!stm->other_stream || stm->other_stream->bufframes > 0)) {
long got = avail - stm->bufframes;
void * other_buffer = stm->other_stream ? stm->other_stream->buffer : NULL;
char * buftail =
stm->buffer + WRAP(snd_pcm_frames_to_bytes)(stm->pcm, stm->bufframes);
/* Correct read size to the other stream available frames */
if (stm->other_stream &&
got > (snd_pcm_sframes_t)stm->other_stream->bufframes) {
got = stm->other_stream->bufframes;
}
pthread_mutex_unlock(&stm->mutex);
got = stm->data_callback(stm, stm->user_ptr, other_buffer, buftail, got);
pthread_mutex_lock(&stm->mutex);
if (got < 0) {
avail = got; // the error handler below will recover us
} else {
stm->bufframes += got;
if (stm->other_stream) {
stream_buffer_decrement(stm->other_stream, got);
}
}
}
/* Playback: Still don't have enough data? Add some silence. */
if (stm->stream_type == SND_PCM_STREAM_PLAYBACK &&
avail > (snd_pcm_sframes_t)stm->bufframes) {
long drain_frames = avail - stm->bufframes;
double drain_time = (double)drain_frames / stm->params.rate;
char * buftail =
stm->buffer + WRAP(snd_pcm_frames_to_bytes)(stm->pcm, stm->bufframes);
memset(buftail, 0, WRAP(snd_pcm_frames_to_bytes)(stm->pcm, drain_frames));
stm->bufframes = avail;
/* Mark as draining, unless we're waiting for capture */
if (!stm->other_stream || stm->other_stream->bufframes > 0) {
set_timeout(&stm->drain_timeout, drain_time * 1000);
draining = 1;
}
}
/* Playback: Have enough data and no errors. Let's write it out. */
if (stm->stream_type == SND_PCM_STREAM_PLAYBACK && avail > 0) {
snd_pcm_sframes_t wrote;
if (stm->params.format == CUBEB_SAMPLE_FLOAT32NE) {
float * b = (float *)stm->buffer;
for (uint32_t i = 0; i < avail * stm->params.channels; i++) {
b[i] *= stm->volume;
}
} else {
short * b = (short *)stm->buffer;
for (uint32_t i = 0; i < avail * stm->params.channels; i++) {
b[i] *= stm->volume;
}
}
wrote = WRAP(snd_pcm_writei)(stm->pcm, stm->buffer, avail);
if (wrote < 0) {
avail = wrote; // the error handler below will recover us
} else {
stream_buffer_decrement(stm, wrote);
stm->stream_position += wrote;
gettimeofday(&stm->last_activity, NULL);
}
}
/* Got some error? Let's try to recover the stream. */
if (avail < 0) {
avail = WRAP(snd_pcm_recover)(stm->pcm, avail, 0);
/* Capture pcm must be started after initial setup/recover */
if (avail >= 0 && stm->stream_type == SND_PCM_STREAM_CAPTURE &&
WRAP(snd_pcm_state)(stm->pcm) == SND_PCM_STATE_PREPARED) {
avail = WRAP(snd_pcm_start)(stm->pcm);
}
}
/* Failed to recover, this stream must be broken. */
if (avail < 0) {
pthread_mutex_unlock(&stm->mutex);
stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR);
return ERROR;
}
pthread_mutex_unlock(&stm->mutex);
return draining ? DRAINING : RUNNING;
}
static int
alsa_run(cubeb * ctx)
{
int r;
int timeout;
int i;
char dummy;
cubeb_stream * stm;
enum stream_state state;
pthread_mutex_lock(&ctx->mutex);
if (ctx->rebuild) {
rebuild(ctx);
}
/* Wake up at least once per second for the watchdog. */
timeout = 1000;
for (i = 0; i < CUBEB_STREAM_MAX; ++i) {
stm = ctx->streams[i];
if (stm && stm->state == DRAINING) {
r = ms_until(&stm->drain_timeout);
if (r >= 0 && timeout > r) {
timeout = r;
}
}
}
pthread_mutex_unlock(&ctx->mutex);
r = poll(ctx->fds, ctx->nfds, timeout);
pthread_mutex_lock(&ctx->mutex);
if (r > 0) {
if (ctx->fds[0].revents & POLLIN) {
if (read(ctx->control_fd_read, &dummy, 1) < 0) {
/* ignore read error */
}
if (ctx->shutdown) {
pthread_mutex_unlock(&ctx->mutex);
return -1;
}
}
for (i = 0; i < CUBEB_STREAM_MAX; ++i) {
stm = ctx->streams[i];
/* We can't use snd_pcm_poll_descriptors_revents here because of
https://github.com/kinetiknz/cubeb/issues/135. */
if (stm && stm->state == RUNNING && stm->fds &&
any_revents(stm->fds, stm->nfds)) {
alsa_set_stream_state(stm, PROCESSING);
pthread_mutex_unlock(&ctx->mutex);
state = alsa_process_stream(stm);
pthread_mutex_lock(&ctx->mutex);
alsa_set_stream_state(stm, state);
}
}
} else if (r == 0) {
for (i = 0; i < CUBEB_STREAM_MAX; ++i) {
stm = ctx->streams[i];
if (stm) {
if (stm->state == DRAINING && ms_since(&stm->drain_timeout) >= 0) {
alsa_set_stream_state(stm, INACTIVE);
stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_DRAINED);
} else if (stm->state == RUNNING &&
ms_since(&stm->last_activity) > CUBEB_WATCHDOG_MS) {
alsa_set_stream_state(stm, ERROR);
stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR);
}
}
}
}
pthread_mutex_unlock(&ctx->mutex);
return 0;
}
static void *
alsa_run_thread(void * context)
{
cubeb * ctx = context;
int r;
CUBEB_REGISTER_THREAD("cubeb rendering thread");
do {
r = alsa_run(ctx);
} while (r >= 0);
CUBEB_UNREGISTER_THREAD();
return NULL;
}
static snd_config_t *
get_slave_pcm_node(snd_config_t * lconf, snd_config_t * root_pcm)
{
int r;
snd_config_t * slave_pcm;
snd_config_t * slave_def;
snd_config_t * pcm;
char const * string;
char node_name[64];
slave_def = NULL;
r = WRAP(snd_config_search)(root_pcm, "slave", &slave_pcm);
if (r < 0) {
return NULL;
}
r = WRAP(snd_config_get_string)(slave_pcm, &string);
if (r >= 0) {
r = WRAP(snd_config_search_definition)(lconf, "pcm_slave", string,
&slave_def);
if (r < 0) {
return NULL;
}
}
do {
r = WRAP(snd_config_search)(slave_def ? slave_def : slave_pcm, "pcm", &pcm);
if (r < 0) {
break;
}
r = WRAP(snd_config_get_string)(slave_def ? slave_def : slave_pcm, &string);
if (r < 0) {
break;
}
r = snprintf(node_name, sizeof(node_name), "pcm.%s", string);
if (r < 0 || r > (int)sizeof(node_name)) {
break;
}
r = WRAP(snd_config_search)(lconf, node_name, &pcm);
if (r < 0) {
break;
}
return pcm;
} while (0);
if (slave_def) {
WRAP(snd_config_delete)(slave_def);
}
return NULL;
}
/* Work around PulseAudio ALSA plugin bug where the PA server forces a
higher than requested latency, but the plugin does not update its (and
ALSA's) internal state to reflect that, leading to an immediate underrun
situation. Inspired by WINE's make_handle_underrun_config.
Reference: http://mailman.alsa-project.org/pipermail/alsa-devel/2012-July/05
*/
static snd_config_t *
init_local_config_with_workaround(char const * pcm_name)
{
int r;
snd_config_t * lconf;
snd_config_t * pcm_node;
snd_config_t * node;
char const * string;
char node_name[64];
lconf = NULL;
if (WRAP(snd_config) == NULL) {
return NULL;
}
r = WRAP(snd_config_copy)(&lconf, WRAP(snd_config));
if (r < 0) {
return NULL;
}
do {
r = WRAP(snd_config_search_definition)(lconf, "pcm", pcm_name, &pcm_node);
if (r < 0) {
break;
}
r = WRAP(snd_config_get_id)(pcm_node, &string);
if (r < 0) {
break;
}
r = snprintf(node_name, sizeof(node_name), "pcm.%s", string);
if (r < 0 || r > (int)sizeof(node_name)) {
break;
}
r = WRAP(snd_config_search)(lconf, node_name, &pcm_node);
if (r < 0) {
break;
}
/* If this PCM has a slave, walk the slave configurations until we reach the
* bottom. */
while ((node = get_slave_pcm_node(lconf, pcm_node)) != NULL) {
pcm_node = node;
}
/* Fetch the PCM node's type, and bail out if it's not the PulseAudio
* plugin. */
r = WRAP(snd_config_search)(pcm_node, "type", &node);
if (r < 0) {
break;
}
r = WRAP(snd_config_get_string)(node, &string);
if (r < 0) {
break;
}
if (strcmp(string, "pulse") != 0) {
break;
}
/* Don't clobber an explicit existing handle_underrun value, set it only
if it doesn't already exist. */
r = WRAP(snd_config_search)(pcm_node, "handle_underrun", &node);
if (r != -ENOENT) {
break;
}
/* Disable pcm_pulse's asynchronous underrun handling. */
r = WRAP(snd_config_imake_integer)(&node, "handle_underrun", 0);
if (r < 0) {
break;
}
r = WRAP(snd_config_add)(pcm_node, node);
if (r < 0) {
break;
}
return lconf;
} while (0);
WRAP(snd_config_delete)(lconf);
return NULL;
}
static int
alsa_locked_pcm_open(snd_pcm_t ** pcm, char const * pcm_name,
snd_pcm_stream_t stream, snd_config_t * local_config)
{
int r;
pthread_mutex_lock(&cubeb_alsa_mutex);
if (local_config) {
r = WRAP(snd_pcm_open_lconf)(pcm, pcm_name, stream, SND_PCM_NONBLOCK,
local_config);
} else {
r = WRAP(snd_pcm_open)(pcm, pcm_name, stream, SND_PCM_NONBLOCK);
}
pthread_mutex_unlock(&cubeb_alsa_mutex);
return r;
}
static int
alsa_locked_pcm_close(snd_pcm_t * pcm)
{
int r;
pthread_mutex_lock(&cubeb_alsa_mutex);
r = WRAP(snd_pcm_close)(pcm);
pthread_mutex_unlock(&cubeb_alsa_mutex);
return r;
}
static int
alsa_register_stream(cubeb * ctx, cubeb_stream * stm)
{
int i;
pthread_mutex_lock(&ctx->mutex);
for (i = 0; i < CUBEB_STREAM_MAX; ++i) {
if (!ctx->streams[i]) {
ctx->streams[i] = stm;
break;
}
}
pthread_mutex_unlock(&ctx->mutex);
return i == CUBEB_STREAM_MAX;
}
static void
alsa_unregister_stream(cubeb_stream * stm)
{
cubeb * ctx;
int i;
ctx = stm->context;
pthread_mutex_lock(&ctx->mutex);
for (i = 0; i < CUBEB_STREAM_MAX; ++i) {
if (ctx->streams[i] == stm) {
ctx->streams[i] = NULL;
break;
}
}
pthread_mutex_unlock(&ctx->mutex);
}
static void
silent_error_handler(char const * file, int line, char const * function,
int err, char const * fmt, ...)
{
(void)file;
(void)line;
(void)function;
(void)err;
(void)fmt;
}
/*static*/ int
alsa_init(cubeb ** context, char const * context_name)
{
(void)context_name;
void * libasound = NULL;
cubeb * ctx;
int r;
int i;
int fd[2];
pthread_attr_t attr;
snd_pcm_t * dummy;
assert(context);
*context = NULL;
#ifndef DISABLE_LIBASOUND_DLOPEN
libasound = dlopen("libasound.so.2", RTLD_LAZY);
if (!libasound) {
libasound = dlopen("libasound.so", RTLD_LAZY);
if (!libasound) {
return CUBEB_ERROR;
}
}
#define LOAD(x) \
{ \
cubeb_##x = dlsym(libasound, #x); \
if (!cubeb_##x) { \
dlclose(libasound); \
return CUBEB_ERROR; \
} \
}
LIBASOUND_API_VISIT(LOAD);
#undef LOAD
#endif
pthread_mutex_lock(&cubeb_alsa_mutex);
if (!cubeb_alsa_error_handler_set) {
WRAP(snd_lib_error_set_handler)(silent_error_handler);
cubeb_alsa_error_handler_set = 1;
}
pthread_mutex_unlock(&cubeb_alsa_mutex);
ctx = calloc(1, sizeof(*ctx));
assert(ctx);
ctx->ops = &alsa_ops;
ctx->libasound = libasound;
r = pthread_mutex_init(&ctx->mutex, NULL);
assert(r == 0);
r = pipe(fd);
assert(r == 0);
for (i = 0; i < 2; ++i) {
fcntl(fd[i], F_SETFD, fcntl(fd[i], F_GETFD) | FD_CLOEXEC);
fcntl(fd[i], F_SETFL, fcntl(fd[i], F_GETFL) | O_NONBLOCK);
}
ctx->control_fd_read = fd[0];
ctx->control_fd_write = fd[1];
/* Force an early rebuild when alsa_run is first called to ensure fds and
nfds have been initialized. */
ctx->rebuild = 1;
r = pthread_attr_init(&attr);
assert(r == 0);
r = pthread_attr_setstacksize(&attr, 256 * 1024);
assert(r == 0);
r = pthread_create(&ctx->thread, &attr, alsa_run_thread, ctx);
assert(r == 0);
r = pthread_attr_destroy(&attr);
assert(r == 0);
/* Open a dummy PCM to force the configuration space to be evaluated so that
init_local_config_with_workaround can find and modify the default node. */
r = alsa_locked_pcm_open(&dummy, CUBEB_ALSA_PCM_NAME, SND_PCM_STREAM_PLAYBACK,
NULL);
if (r >= 0) {
alsa_locked_pcm_close(dummy);
}
ctx->is_pa = 0;
pthread_mutex_lock(&cubeb_alsa_mutex);
ctx->local_config = init_local_config_with_workaround(CUBEB_ALSA_PCM_NAME);
pthread_mutex_unlock(&cubeb_alsa_mutex);
if (ctx->local_config) {
ctx->is_pa = 1;
r = alsa_locked_pcm_open(&dummy, CUBEB_ALSA_PCM_NAME,
SND_PCM_STREAM_PLAYBACK, ctx->local_config);
/* If we got a local_config, we found a PA PCM. If opening a PCM with that
config fails with EINVAL, the PA PCM is too old for this workaround. */
if (r == -EINVAL) {
pthread_mutex_lock(&cubeb_alsa_mutex);
WRAP(snd_config_delete)(ctx->local_config);
pthread_mutex_unlock(&cubeb_alsa_mutex);
ctx->local_config = NULL;
} else if (r >= 0) {
alsa_locked_pcm_close(dummy);
}
}
*context = ctx;
return CUBEB_OK;
}
static char const *
alsa_get_backend_id(cubeb * ctx)
{
(void)ctx;
return "alsa";
}
static void
alsa_destroy(cubeb * ctx)
{
int r;
assert(ctx);
pthread_mutex_lock(&ctx->mutex);
ctx->shutdown = 1;
poll_wake(ctx);
pthread_mutex_unlock(&ctx->mutex);
r = pthread_join(ctx->thread, NULL);
assert(r == 0);
close(ctx->control_fd_read);
close(ctx->control_fd_write);
pthread_mutex_destroy(&ctx->mutex);
free(ctx->fds);
if (ctx->local_config) {
pthread_mutex_lock(&cubeb_alsa_mutex);
WRAP(snd_config_delete)(ctx->local_config);
pthread_mutex_unlock(&cubeb_alsa_mutex);
}
#ifndef DISABLE_LIBASOUND_DLOPEN
if (ctx->libasound) {
dlclose(ctx->libasound);
}
#endif
free(ctx);
}
static void
alsa_stream_destroy(cubeb_stream * stm);
static int
alsa_stream_init_single(cubeb * ctx, cubeb_stream ** stream,
char const * stream_name, snd_pcm_stream_t stream_type,
cubeb_devid deviceid,
cubeb_stream_params * stream_params,
unsigned int latency_frames,
cubeb_data_callback data_callback,
cubeb_state_callback state_callback, void * user_ptr)
{
(void)stream_name;
cubeb_stream * stm;
int r;
snd_pcm_format_t format;
snd_pcm_uframes_t period_size;
int latency_us = 0;
char const * pcm_name =
deviceid ? (char const *)deviceid : CUBEB_ALSA_PCM_NAME;
assert(ctx && stream);
*stream = NULL;
if (stream_params->prefs & CUBEB_STREAM_PREF_LOOPBACK) {
return CUBEB_ERROR_NOT_SUPPORTED;
}
switch (stream_params->format) {
case CUBEB_SAMPLE_S16LE:
format = SND_PCM_FORMAT_S16_LE;
break;
case CUBEB_SAMPLE_S16BE:
format = SND_PCM_FORMAT_S16_BE;
break;
case CUBEB_SAMPLE_FLOAT32LE:
format = SND_PCM_FORMAT_FLOAT_LE;
break;
case CUBEB_SAMPLE_FLOAT32BE:
format = SND_PCM_FORMAT_FLOAT_BE;
break;
default:
return CUBEB_ERROR_INVALID_FORMAT;
}
pthread_mutex_lock(&ctx->mutex);
if (ctx->active_streams >= CUBEB_STREAM_MAX) {
pthread_mutex_unlock(&ctx->mutex);
return CUBEB_ERROR;
}
ctx->active_streams += 1;
pthread_mutex_unlock(&ctx->mutex);
stm = calloc(1, sizeof(*stm));
assert(stm);
stm->context = ctx;
stm->data_callback = data_callback;
stm->state_callback = state_callback;
stm->user_ptr = user_ptr;
stm->params = *stream_params;
stm->state = INACTIVE;
stm->volume = 1.0;
stm->buffer = NULL;
stm->bufframes = 0;
stm->stream_type = stream_type;
stm->other_stream = NULL;
r = pthread_mutex_init(&stm->mutex, NULL);
assert(r == 0);
r = pthread_cond_init(&stm->cond, NULL);
assert(r == 0);
r = alsa_locked_pcm_open(&stm->pcm, pcm_name, stm->stream_type,
ctx->local_config);
if (r < 0) {
alsa_stream_destroy(stm);
return CUBEB_ERROR;
}
r = WRAP(snd_pcm_nonblock)(stm->pcm, 1);
assert(r == 0);
latency_us = latency_frames * 1e6 / stm->params.rate;
/* Ugly hack: the PA ALSA plugin allows buffer configurations that can't
possibly work. See https://bugzilla.mozilla.org/show_bug.cgi?id=761274.
Only resort to this hack if the handle_underrun workaround failed. */
if (!ctx->local_config && ctx->is_pa) {
const int min_latency = 5e5;
latency_us = latency_us < min_latency ? min_latency : latency_us;
}
r = WRAP(snd_pcm_set_params)(stm->pcm, format, SND_PCM_ACCESS_RW_INTERLEAVED,
stm->params.channels, stm->params.rate, 1,
latency_us);
if (r < 0) {
alsa_stream_destroy(stm);
return CUBEB_ERROR_INVALID_FORMAT;
}
r = WRAP(snd_pcm_get_params)(stm->pcm, &stm->buffer_size, &period_size);
assert(r == 0);
/* Double internal buffer size to have enough space when waiting for the other
* side of duplex connection */
stm->buffer_size *= 2;
stm->buffer =
calloc(1, WRAP(snd_pcm_frames_to_bytes)(stm->pcm, stm->buffer_size));
assert(stm->buffer);
stm->nfds = WRAP(snd_pcm_poll_descriptors_count)(stm->pcm);
assert(stm->nfds > 0);
stm->saved_fds = calloc(stm->nfds, sizeof(struct pollfd));
assert(stm->saved_fds);
r = WRAP(snd_pcm_poll_descriptors)(stm->pcm, stm->saved_fds, stm->nfds);
assert((nfds_t)r == stm->nfds);
if (alsa_register_stream(ctx, stm) != 0) {
alsa_stream_destroy(stm);
return CUBEB_ERROR;
}
*stream = stm;
return CUBEB_OK;
}
static int
alsa_stream_init(cubeb * ctx, cubeb_stream ** stream, char const * stream_name,
cubeb_devid input_device,
cubeb_stream_params * input_stream_params,
cubeb_devid output_device,
cubeb_stream_params * output_stream_params,
unsigned int latency_frames, cubeb_data_callback data_callback,
cubeb_state_callback state_callback, void * user_ptr)
{
int result = CUBEB_OK;
cubeb_stream *instm = NULL, *outstm = NULL;
if (result == CUBEB_OK && input_stream_params) {
result = alsa_stream_init_single(ctx, &instm, stream_name,
SND_PCM_STREAM_CAPTURE, input_device,
input_stream_params, latency_frames,
data_callback, state_callback, user_ptr);
}
if (result == CUBEB_OK && output_stream_params) {
result = alsa_stream_init_single(ctx, &outstm, stream_name,
SND_PCM_STREAM_PLAYBACK, output_device,
output_stream_params, latency_frames,
data_callback, state_callback, user_ptr);
}
if (result == CUBEB_OK && input_stream_params && output_stream_params) {
instm->other_stream = outstm;
outstm->other_stream = instm;
}
if (result != CUBEB_OK && instm) {
alsa_stream_destroy(instm);
}
*stream = outstm ? outstm : instm;
return result;
}
static void
alsa_stream_destroy(cubeb_stream * stm)
{
int r;
cubeb * ctx;
assert(stm && (stm->state == INACTIVE || stm->state == ERROR ||
stm->state == DRAINING));
ctx = stm->context;
if (stm->other_stream) {
stm->other_stream->other_stream = NULL; // to stop infinite recursion
alsa_stream_destroy(stm->other_stream);
}
pthread_mutex_lock(&stm->mutex);
if (stm->pcm) {
if (stm->state == DRAINING) {
WRAP(snd_pcm_drain)(stm->pcm);
}
alsa_locked_pcm_close(stm->pcm);
stm->pcm = NULL;
}
free(stm->saved_fds);
pthread_mutex_unlock(&stm->mutex);
pthread_mutex_destroy(&stm->mutex);
r = pthread_cond_destroy(&stm->cond);
assert(r == 0);
alsa_unregister_stream(stm);
pthread_mutex_lock(&ctx->mutex);
assert(ctx->active_streams >= 1);
ctx->active_streams -= 1;
pthread_mutex_unlock(&ctx->mutex);
free(stm->buffer);
free(stm);
}
// max_channel_count
static int
alsa_get_max_channel_count(cubeb * ctx, uint32_t * max_channels)
{
snd_ctl_t *handle;
int card = -1;
uint32_t max = 0;
int r;
printf("DEBUG: Starting max channel count calculation\n");
if (!ctx || !max_channels) {
printf("DEBUG: Invalid parameters (ctx=%p, max_channels=%p)\n", ctx, max_channels);
return CUBEB_ERROR_INVALID_PARAMETER;
}
/* Iterate through all cards */
while (snd_card_next(&card) >= 0 && card >= 0) {
char card_name[32];
snprintf(card_name, sizeof(card_name), "hw:%d", card);
r = snd_ctl_open(&handle, card_name, 0);
if (r < 0) {
printf("DEBUG: Cannot open control for card %d: %s\n", card, snd_strerror(r));
continue;
}
/* Iterate through PCM devices on this card */
int dev = -1;
while (snd_ctl_pcm_next_device(handle, &dev) >= 0 && dev >= 0) {
snd_pcm_chmap_query_t **maps;
/* Query channel maps without opening PCM device */
maps = snd_pcm_query_chmaps_from_hw(card, dev, 0, SND_PCM_STREAM_PLAYBACK);
if (maps) {
snd_pcm_chmap_query_t **p;
for (p = maps; *p != NULL; p++) {
unsigned int channels = (*p)->map.channels;
printf("DEBUG: Device hw:%d,%d: channel map type %d, channels = %u\n",
card, dev, (*p)->type, channels);
if (channels > max) {
max = channels;
printf("DEBUG: New maximum: %u\n", max);
}
}
snd_pcm_free_chmaps(maps);
} else {
printf("DEBUG: No channel maps available for device hw:%d,%d\n", card, dev);
}
}
snd_ctl_close(handle);
}
printf("DEBUG: Final max channels: %u\n", max);
if (max == 0) {
printf("DEBUG: No valid channel counts found, using fallback\n");
max = 2; /* Fallback to stereo */
}
*max_channels = max;
return CUBEB_OK;
}
// END of max_channel_count
static int
alsa_get_preferred_sample_rate(cubeb * ctx, uint32_t * rate)
{
(void)ctx;
int r, dir;
snd_pcm_t * pcm;
snd_pcm_hw_params_t * hw_params;
snd_pcm_hw_params_alloca(&hw_params);
/* get a pcm, disabling resampling, so we get a rate the
* hardware/dmix/pulse/etc. supports. */
r = WRAP(snd_pcm_open)(&pcm, CUBEB_ALSA_PCM_NAME, SND_PCM_STREAM_PLAYBACK,
SND_PCM_NO_AUTO_RESAMPLE);
if (r < 0) {
return CUBEB_ERROR;
}
r = WRAP(snd_pcm_hw_params_any)(pcm, hw_params);
if (r < 0) {
WRAP(snd_pcm_close)(pcm);
return CUBEB_ERROR;
}
r = WRAP(snd_pcm_hw_params_get_rate)(hw_params, rate, &dir);
if (r >= 0) {
/* There is a default rate: use it. */
WRAP(snd_pcm_close)(pcm);
return CUBEB_OK;
}
/* Use a common rate, alsa may adjust it based on hw/etc. capabilities. */
*rate = 44100;
r = WRAP(snd_pcm_hw_params_set_rate_near)(pcm, hw_params, rate, NULL);
if (r < 0) {
WRAP(snd_pcm_close)(pcm);
return CUBEB_ERROR;
}
WRAP(snd_pcm_close)(pcm);
return CUBEB_OK;
}
static int
alsa_get_min_latency(cubeb * ctx, cubeb_stream_params params,
uint32_t * latency_frames)
{
(void)ctx;
/* 40ms is found to be an acceptable minimum, even on a super low-end
* machine. */
*latency_frames = 40 * params.rate / 1000;
return CUBEB_OK;
}
static int
alsa_stream_start(cubeb_stream * stm)
{
cubeb * ctx;
assert(stm);
ctx = stm->context;
if (stm->stream_type == SND_PCM_STREAM_PLAYBACK && stm->other_stream) {
int r = alsa_stream_start(stm->other_stream);
if (r != CUBEB_OK)
return r;
}
pthread_mutex_lock(&stm->mutex);
/* Capture pcm must be started after initial setup/recover */
if (stm->stream_type == SND_PCM_STREAM_CAPTURE &&
WRAP(snd_pcm_state)(stm->pcm) == SND_PCM_STATE_PREPARED) {
WRAP(snd_pcm_start)(stm->pcm);
}
WRAP(snd_pcm_pause)(stm->pcm, 0);
gettimeofday(&stm->last_activity, NULL);
pthread_mutex_unlock(&stm->mutex);
pthread_mutex_lock(&ctx->mutex);
if (stm->state != INACTIVE) {
pthread_mutex_unlock(&ctx->mutex);
return CUBEB_ERROR;
}
alsa_set_stream_state(stm, RUNNING);
pthread_mutex_unlock(&ctx->mutex);
return CUBEB_OK;
}
static int
alsa_stream_stop(cubeb_stream * stm)
{
cubeb * ctx;
int r;
assert(stm);
ctx = stm->context;
if (stm->stream_type == SND_PCM_STREAM_PLAYBACK && stm->other_stream) {
int r = alsa_stream_stop(stm->other_stream);
if (r != CUBEB_OK)
return r;
}
pthread_mutex_lock(&ctx->mutex);
while (stm->state == PROCESSING) {
r = pthread_cond_wait(&stm->cond, &ctx->mutex);
assert(r == 0);
}
alsa_set_stream_state(stm, INACTIVE);
pthread_mutex_unlock(&ctx->mutex);
pthread_mutex_lock(&stm->mutex);
WRAP(snd_pcm_pause)(stm->pcm, 1);
pthread_mutex_unlock(&stm->mutex);
return CUBEB_OK;
}
static int
alsa_stream_get_position(cubeb_stream * stm, uint64_t * position)
{
snd_pcm_sframes_t delay;
assert(stm && position);
pthread_mutex_lock(&stm->mutex);
delay = -1;
if (WRAP(snd_pcm_state)(stm->pcm) != SND_PCM_STATE_RUNNING ||
WRAP(snd_pcm_delay)(stm->pcm, &delay) != 0) {
*position = stm->last_position;
pthread_mutex_unlock(&stm->mutex);
return CUBEB_OK;
}
assert(delay >= 0);
*position = 0;
if (stm->stream_position >= (snd_pcm_uframes_t)delay) {
*position = stm->stream_position - delay;
}
stm->last_position = *position;
pthread_mutex_unlock(&stm->mutex);
return CUBEB_OK;
}
static int
alsa_stream_get_latency(cubeb_stream * stm, uint32_t * latency)
{
snd_pcm_sframes_t delay;
/* This function returns the delay in frames until a frame written using
snd_pcm_writei is sent to the DAC. The DAC delay should be < 1ms anyways.
*/
if (WRAP(snd_pcm_delay)(stm->pcm, &delay)) {
return CUBEB_ERROR;
}
*latency = delay;
return CUBEB_OK;
}
static int
alsa_stream_set_volume(cubeb_stream * stm, float volume)
{
/* setting the volume using an API call does not seem very stable/supported */
pthread_mutex_lock(&stm->mutex);
stm->volume = volume;
pthread_mutex_unlock(&stm->mutex);
return CUBEB_OK;
}
/* Helper Functions for alsa_enumerate_devices */
static bool
is_virtual_device(const char *name)
{
const char *virtual_prefixes[] = {
"plug", "dmix", "dsnoop", "default", "plughw", "tee",
"null", "sysdefault", "lavrate", "samplerate", "speexrate",
"bluealsa", "oss", "speex", "upmix", "vdownmix",
"front", "surround40", "surround41", "surround50",
"surround51", "surround71", "iec958",
NULL
};
for (int i = 0; virtual_prefixes[i] != NULL; i++) {
if (strncmp(name, virtual_prefixes[i], strlen(virtual_prefixes[i])) == 0) {
// Check if it's an exact match or followed by colon or other separator
size_t len = strlen(virtual_prefixes[i]);
if (name[len] == '\0' || name[len] == ':' || name[len] == '=') {
return true;
}
}
}
return false;
}
static int
add_bluealsa_device(cubeb_device_info **devinfop, int *collection_cnt,
cubeb_device_type type)
{
cubeb_device_info *device = &(*devinfop)[*collection_cnt];
/* A2DP is output-only (better quality) */
if (type == CUBEB_DEVICE_TYPE_OUTPUT) {
device->device_id = strdup("bluealsa:PROFILE=a2dp");
device->friendly_name = strdup("BlueALSA A2DP");
device->group_id = strdup("bluealsa");
device->vendor_name = strdup("BlueALSA");
device->format = CUBEB_DEVICE_FMT_S16LE;
device->default_format = CUBEB_DEVICE_FMT_S16LE;
device->max_channels = 2;
device->default_rate = 48000;
device->min_rate = 44100;
device->max_rate = 48000;
device->type = CUBEB_DEVICE_TYPE_OUTPUT;
device->state = CUBEB_DEVICE_STATE_ENABLED;
device->preferred = CUBEB_DEVICE_PREF_NONE;
device->latency_lo = 200;
device->latency_hi = 200;
(*collection_cnt)++;
}
/* SCO supports full duplex (both input and output) */
if (type == CUBEB_DEVICE_TYPE_OUTPUT || type == CUBEB_DEVICE_TYPE_INPUT) {
device = &(*devinfop)[*collection_cnt];
device->device_id = strdup("bluealsa:PROFILE=sco");
device->friendly_name = strdup("BlueALSA SCO");
device->group_id = strdup("bluealsa");
device->vendor_name = strdup("BlueALSA");
device->format = CUBEB_DEVICE_FMT_S16LE;
device->default_format = CUBEB_DEVICE_FMT_S16LE;
device->max_channels = 1;
device->default_rate = 16000;
device->min_rate = 16000;
device->max_rate = 16000;
device->type = type;
device->state = CUBEB_DEVICE_STATE_ENABLED;
device->preferred = CUBEB_DEVICE_PREF_NONE;
device->latency_lo = 20;
device->latency_hi = 30;
(*collection_cnt)++;
}
return CUBEB_OK;
}
static int
alsa_enumerate_devices(cubeb * context, cubeb_device_type type,
cubeb_device_collection * collection)
{
void **hints = NULL;
void **hint;
char *name = NULL;
char *io = NULL;
char *desc = NULL;
cubeb_device_info * devinfop = NULL;
int collection_cnt = 0;
int r;
if (!context)
return CUBEB_ERROR;
/* Get device name hints from ALSA */
r = WRAP(snd_device_name_hint)(-1, "pcm", &hints);
if (r < 0) {
return CUBEB_ERROR;
}
/* First pass: count physical devices */
for (hint = hints; *hint != NULL; hint++) {
name = WRAP(snd_device_name_get_hint)(*hint, "NAME");
if (!name)
continue;
/* Skip virtual devices */
if (is_virtual_device(name)) {
free(name);
continue;
}
/* Check IO type matches requested type */
io = WRAP(snd_device_name_get_hint)(*hint, "IO");
if (io) {
if (type == CUBEB_DEVICE_TYPE_INPUT && strcmp(io, "Input") != 0) {
free(name);
free(io);
continue;
}
if (type == CUBEB_DEVICE_TYPE_OUTPUT && strcmp(io, "Output") != 0) {
free(name);
free(io);
continue;
}
free(io);
}
collection_cnt++;
free(name);
}
/* Allocate device array */
devinfop = calloc(collection_cnt + 2, sizeof(cubeb_device_info));
if (!devinfop) {
WRAP(snd_device_name_free_hint)(hints);
return CUBEB_ERROR;
}
collection_cnt = 0;
/* Second pass: populate device info */
for (hint = hints; *hint != NULL; hint++) {
name = WRAP(snd_device_name_get_hint)(*hint, "NAME");
if (!name)
continue;
if (is_virtual_device(name)) {
free(name);
continue;
}
io = WRAP(snd_device_name_get_hint)(*hint, "IO");
if (io) {
if (type == CUBEB_DEVICE_TYPE_INPUT && strcmp(io, "Input") != 0) {
free(name);
free(io);
continue;
}
if (type == CUBEB_DEVICE_TYPE_OUTPUT && strcmp(io, "Output") != 0) {
free(name);
free(io);
continue;
}
free(io);
}
/* Query device capabilities by opening it temporarily */
snd_pcm_t *pcm;
snd_pcm_hw_params_t *hw_params;
snd_pcm_hw_params_alloca(&hw_params);
snd_pcm_format_mask_t *format_mask;
if (WRAP(snd_pcm_open)(&pcm, name,
type == CUBEB_DEVICE_TYPE_INPUT ? SND_PCM_STREAM_CAPTURE : SND_PCM_STREAM_PLAYBACK,
SND_PCM_NONBLOCK) >= 0) {
if (WRAP(snd_pcm_hw_params_any)(pcm, hw_params) >= 0) {
/* Query actual capabilities */
unsigned int min_rate, max_rate;
unsigned int min_channels, max_channels;
snd_pcm_format_mask_alloca(&format_mask);
WRAP(snd_pcm_hw_params_get_format_mask)(hw_params, format_mask);
WRAP(snd_pcm_hw_params_get_rate_min)(hw_params, &min_rate, NULL);
WRAP(snd_pcm_hw_params_get_rate_max)(hw_params, &max_rate, NULL);
WRAP(snd_pcm_hw_params_get_channels_min)(hw_params, &min_channels);
WRAP(snd_pcm_hw_params_get_channels_max)(hw_params, &max_channels);
/* Determine supported formats */
cubeb_device_fmt format = CUBEB_DEVICE_FMT_S16NE;
if (WRAP(snd_pcm_format_mask_test)(format_mask, SND_PCM_FORMAT_S32_LE)) {
format |= CUBEB_DEVICE_FMT_S32LE;
}
if (WRAP(snd_pcm_format_mask_test)(format_mask, SND_PCM_FORMAT_FLOAT)) {
format |= CUBEB_DEVICE_FMT_F32NE;
}
devinfop[collection_cnt].device_id = strdup(name);
devinfop[collection_cnt].devid = (cubeb_devid)devinfop[collection_cnt].device_id;
desc = WRAP(snd_device_name_get_hint)(*hint, "DESC");
devinfop[collection_cnt].friendly_name = desc ? strdup(desc) : strdup(name);
if (desc) free(desc);
devinfop[collection_cnt].group_id = strdup(name);
devinfop[collection_cnt].vendor_name = NULL;
devinfop[collection_cnt].type = type;
devinfop[collection_cnt].state = CUBEB_DEVICE_STATE_ENABLED;
devinfop[collection_cnt].preferred = CUBEB_DEVICE_PREF_NONE;
devinfop[collection_cnt].format = format;
devinfop[collection_cnt].default_format = CUBEB_DEVICE_FMT_S16NE; /* Default to S16 */
devinfop[collection_cnt].max_channels = max_channels;
devinfop[collection_cnt].default_rate = 44100;
devinfop[collection_cnt].min_rate = min_rate;
devinfop[collection_cnt].max_rate = max_rate;
devinfop[collection_cnt].latency_lo = 0;
devinfop[collection_cnt].latency_hi = 0;
collection_cnt++;
}
WRAP(snd_pcm_close)(pcm);
}
free(name);
}
WRAP(snd_device_name_free_hint)(hints);
/* Add BlueALSA devices, if BT headphones are connected */
snd_pcm_t *pcm;
int err = WRAP(snd_pcm_open)(&pcm, "bluealsa", SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK);
if (err == 0) {
/* BlueALSA is available (BT headphones connected) */
WRAP(snd_pcm_close)(pcm);
/* Add to device list */
add_bluealsa_device(&devinfop, &collection_cnt, type);
}
/* If err != 0, BlueALSA is not available (no BT headphones) */
collection->device = devinfop;
collection->count = collection_cnt;
return CUBEB_OK;
}
static int
alsa_device_collection_destroy(cubeb * context,
cubeb_device_collection * collection)
{
size_t i;
(void)context;
if (!collection->device)
return CUBEB_OK;
for (i = 0; i < collection->count; i++) {
free((void *)collection->device[i].device_id);
free((void *)collection->device[i].friendly_name);
free((void *)collection->device[i].group_id);
free((void *)collection->device[i].vendor_name);
}
free(collection->device);
collection->device = NULL;
collection->count = 0;
return CUBEB_OK;
}
static struct cubeb_ops const alsa_ops = {
.init = alsa_init,
.get_backend_id = alsa_get_backend_id,
.get_max_channel_count = alsa_get_max_channel_count,
.get_min_latency = alsa_get_min_latency,
.get_preferred_sample_rate = alsa_get_preferred_sample_rate,
.get_supported_input_processing_params = NULL,
.enumerate_devices = alsa_enumerate_devices,
.device_collection_destroy = alsa_device_collection_destroy,
.destroy = alsa_destroy,
.stream_init = alsa_stream_init,
.stream_destroy = alsa_stream_destroy,
.stream_start = alsa_stream_start,
.stream_stop = alsa_stream_stop,
.stream_get_position = alsa_stream_get_position,
.stream_get_latency = alsa_stream_get_latency,
.stream_get_input_latency = NULL,
.stream_set_volume = alsa_stream_set_volume,
.stream_set_name = NULL,
.stream_get_current_device = NULL,
.stream_set_input_mute = NULL,
.stream_set_input_processing_params = NULL,
.stream_device_destroy = NULL,
.stream_register_device_changed_callback = NULL,
.register_device_collection_changed = NULL};$ firefox-dev about:support
Name Firefox Developer Edition
Version 155.0a1
...
Update Channel aurora
User Agent Mozilla/5.0 (X11; Linux x86_64; rv:155.0) Gecko/20100101 Firefox/155.0
...
Application Binary /usr/lib/firefox-dev/firefox-dev
Profile Directory ~/.firefox-dev/qxls80vg.dev-edition-default-1
...
Audio Backend: alsa
Max Channels: 8
Preferred Sample Rate: 44100
Roundtrip latency (standard deviation): NaNms (NaN)
Output Devices
Name: Group, Vendor, State, Preferred, Format, Channels, Rate, Latency
HDA Intel PCH, Generic Analog Direct hardware device without any conversions: hw:CARD=PCH,DEV=0, Enabled, None, default: S16LE, support: S16LE S32LE, 2, default: 44100, support: 44100 - 192000, 0 - 0
HDA Intel PCH, Generic Digital Direct hardware device without any conversions: hw:CARD=PCH,DEV=1, Enabled, None, default: S16LE, support: S16LE S32LE, 2, default: 44100, support: 32000 - 192000, 0 - 0
HDA Intel PCH, LG HDR 4K Direct hardware device without any conversions: hw:CARD=PCH,DEV=3, Enabled, None, default: S16LE, support: S16LE S32LE, 2, default: 44100, support: 32000 - 48000, 0 - 0
HDA Intel PCH, LG HDR 4K HDMI Audio Output: hdmi:CARD=PCH,DEV=0, Enabled, None, default: S16LE, support: S16LE S32LE, 2, default: 44100, support: 32000 - 48000, 0 - 0
BlueALSA A2DP: bluealsa, BlueALSA, Enabled, None, default: S16LE, support: S16LE, 2, default: 48000, support: 44100 - 48000, 200 - 200
BlueALSA SCO: bluealsa, BlueALSA, Enabled, None, default: S16LE, support: S16LE, 1, default: 16000, support: 16000 - 16000, 20 - 30
Input Devices
Name: Group, Vendor, State, Preferred, Format, Channels, Rate, Latency
HDA Intel PCH, Generic Analog Direct hardware device without any conversions: hw:CARD=PCH,DEV=0, Enabled, None, default: S16LE, support: S16LE S32LE, 2, default: 44100, support: 44100 - 192000, 0 - 0
HDA Intel PCH, Generic Alt Analog Direct hardware device without any conversions: hw:CARD=PCH,DEV=2, Enabled, None, default: S16LE, support: S16LE S32LE, 2, default: 44100, support: 44100 - 192000, 0 - 0
BlueALSA SCO: bluealsa, BlueALSA, Enabled, None, default: S16LE, support: S16LE, 1, default: 16000, support: 16000 - 16000, 20 - 30The upstream ALSA backend for Firefox does not enumerate ALSA devices because they are not supposed to be used. Instead, the ALSA backend contains code to re-route playback to PulseAudio through the PulseAudio plugin for ALSA (which is installed by default with ALSA plugins in the libasound2-plugins package).
Most probably, enumeration of ALSA devices was removed from the upstream ALSA backend for Firefox and replaced with redirection to PulseAudio devices through the PulseAudio plugin for ALSA.
The Firefox developers are unlikely to agree to re-implement enumeration of ALSA devices. This means that you have to learn how to compile Firefox.
The code that redirects to PulseAudio through the ALSA plugin is in the init_local_config_with_workaround function in src/cubeb_alsa.c. This function detects when the PCM device is using the PulseAudio ALSA plugin and applies a workaround.
PulseAudio Plugin Detection
/* Work around PulseAudio ALSA plugin bug where the PA server forces a
higher than requested latency, but the plugin does not update its (and
ALSA's) internal state to reflect that, leading to an immediate underrun
situation. Inspired by WINE's make_handle_underrun_config.
Reference: http://mailman.alsa-project.org/pipermail/alsa-devel/2012-July/05
*/
static snd_config_t *
init_local_config_with_workaround(char const * pcm_name)
{
// ... (configuration parsing code) ...
/* Fetch the PCM node's type, and bail out if it's not the PulseAudio
* plugin. */
r = WRAP(snd_config_search)(pcm_node, "type", &node);
if (r < 0) {
break;
}
r = WRAP(snd_config_get_string)(node, &string);
if (r < 0) {
break;
}
if (strcmp(string, "pulse") != 0) {
break;
}
/* Disable pcm_pulse's asynchronous underrun handling. */
r = WRAP(snd_config_imake_integer)(&node, "handle_underrun", 0);
if (r < 0) {
break;
}
r = WRAP(snd_config_add)(pcm_node, node);
if (r < 0) {
break;
}
return lconf;
}Initialization Code
/* Open a dummy PCM to force the configuration space to be evaluated so that
init_local_config_with_workaround can find and modify the default node. */
r = alsa_locked_pcm_open(&dummy, CUBEB_ALSA_PCM_NAME, SND_PCM_STREAM_PLAYBACK,
NULL);
if (r >= 0) {
alsa_locked_pcm_close(dummy);
}
ctx->is_pa = 0;
pthread_mutex_lock(&cubeb_alsa_mutex);
ctx->local_config = init_local_config_with_workaround(CUBEB_ALSA_PCM_NAME);
pthread_mutex_unlock(&cubeb_alsa_mutex);
if (ctx->local_config) {
ctx->is_pa = 1;
r = alsa_locked_pcm_open(&dummy, CUBEB_ALSA_PCM_NAME,
SND_PCM_STREAM_PLAYBACK, ctx->local_config);
/* If we got a local_config, we found a PA PCM. If opening a PCM with that
config fails with EINVAL, the PA PCM is too old for this workaround. */
if (r == -EINVAL) {
pthread_mutex_lock(&cubeb_alsa_mutex);
WRAP(snd_config_delete)(ctx->local_config);
pthread_mutex_unlock(&cubeb_alsa_mutex);
ctx->local_config = NULL;
} else if (r >= 0) {
alsa_locked_pcm_close(dummy);
}
}How to purge pulseaudio from libasound2-plugins
_https://dev1galaxy.org/viewtopic.php?id=7523
NOTE: This guide is a work in progress. Please do not post in this topic. If you have any questions, please consult the Devuan Wiki, or other knowledge repositories of the sort.
EDIT:
The semi-deaf, semi-blind, and half-demented ALSA users might be perfectly happy with PulseAudio and PipeWire.
Date: Wed Jul 8 22:22:55 2026 +0200
ALSA backend: Implement proper device enumeration using snd_device_name_hint()
Replace the minimal placeholder enumeration that returned hardcoded
values with proper device enumeration using snd_device_name_hint() API.
This now correctly enumerates both physical hardware devices and virtual
devices (BlueALSA, rate converters, plugins) with accurate format capabilities,
channel counts, and rate ranges queried from actual hardware.$ cat firefox/media/libcubeb/src/cubeb_alsa.c
/*
* Copyright © 2011 Mozilla Foundation
*
* This program is made available under an ISC-style license. See the
* accompanying file LICENSE for details.
*/
#undef NDEBUG
#define _DEFAULT_SOURCE
#define _BSD_SOURCE
#if defined(__NetBSD__)
#define _NETBSD_SOURCE /* timersub() */
#endif
#define _XOPEN_SOURCE 500
#include "cubeb-internal.h"
#include "cubeb/cubeb.h"
#include "cubeb_tracing.h"
#include <alsa/asoundlib.h>
#include <assert.h>
#include <dlfcn.h>
#include <limits.h>
#include <poll.h>
#include <pthread.h>
#include <sys/time.h>
#include <unistd.h>
#ifdef DISABLE_LIBASOUND_DLOPEN
#define WRAP(x) x
#else
#define WRAP(x) (*cubeb_##x)
#define LIBASOUND_API_VISIT(X) \
X(snd_config) \
X(snd_config_add) \
X(snd_config_copy) \
X(snd_config_delete) \
X(snd_config_get_id) \
X(snd_config_get_string) \
X(snd_config_imake_integer) \
X(snd_config_search) \
X(snd_config_search_definition) \
X(snd_lib_error_set_handler) \
X(snd_pcm_avail_update) \
X(snd_pcm_close) \
X(snd_pcm_delay) \
X(snd_pcm_drain) \
X(snd_pcm_frames_to_bytes) \
X(snd_pcm_get_params) \
X(snd_pcm_hw_params_any) \
X(snd_pcm_hw_params_get_channels_max) \
X(snd_pcm_hw_params_get_rate) \
X(snd_pcm_hw_params_set_rate_near) \
X(snd_pcm_hw_params_sizeof) \
X(snd_pcm_nonblock) \
X(snd_pcm_open) \
X(snd_pcm_open_lconf) \
X(snd_pcm_pause) \
X(snd_pcm_poll_descriptors) \
X(snd_pcm_poll_descriptors_count) \
X(snd_pcm_poll_descriptors_revents) \
X(snd_pcm_readi) \
X(snd_pcm_recover) \
X(snd_pcm_set_params) \
X(snd_pcm_start) \
X(snd_pcm_state) \
X(snd_pcm_writei) \
X(snd_device_name_hint) \
X(snd_device_name_get_hint) \
X(snd_device_name_free_hint) \
X(snd_pcm_hw_params_test_format) \
X(snd_pcm_hw_params_get_rate_min) \
X(snd_pcm_hw_params_get_rate_max) \
X(snd_pcm_hw_params_get_channels_min) \
X(snd_pcm_hw_params_get_channels_max)
#define MAKE_TYPEDEF(x) static typeof(x) * cubeb_##x;
LIBASOUND_API_VISIT(MAKE_TYPEDEF);
#undef MAKE_TYPEDEF
/* snd_pcm_hw_params_alloca is actually a macro */
#define snd_pcm_hw_params_sizeof cubeb_snd_pcm_hw_params_sizeof
#endif
#define CUBEB_STREAM_MAX 16
#define CUBEB_WATCHDOG_MS 10000
#define CUBEB_ALSA_PCM_NAME "default"
#define ALSA_PA_PLUGIN "ALSA <-> PulseAudio PCM I/O Plugin"
/* ALSA is not thread-safe. snd_pcm_t instances are individually protected
by the owning cubeb_stream's mutex. snd_pcm_t creation and destruction
is not thread-safe until ALSA 1.0.24 (see alsa-lib.git commit 91c9c8f1),
so those calls must be wrapped in the following mutex. */
static pthread_mutex_t cubeb_alsa_mutex = PTHREAD_MUTEX_INITIALIZER;
static int cubeb_alsa_error_handler_set = 0;
static struct cubeb_ops const alsa_ops;
struct cubeb {
struct cubeb_ops const * ops;
void * libasound;
pthread_t thread;
/* Mutex for streams array, must not be held while blocked in poll(2). */
pthread_mutex_t mutex;
/* Sparse array of streams managed by this context. */
cubeb_stream * streams[CUBEB_STREAM_MAX];
/* fds and nfds are only updated by alsa_run when rebuild is set. */
struct pollfd * fds;
nfds_t nfds;
int rebuild;
int shutdown;
/* Control pipe for forcing poll to wake and rebuild fds or recalculate the
* timeout. */
int control_fd_read;
int control_fd_write;
/* Track number of active streams. This is limited to CUBEB_STREAM_MAX
due to resource contraints. */
unsigned int active_streams;
/* Local configuration with handle_underrun workaround set for PulseAudio
ALSA plugin. Will be NULL if the PA ALSA plugin is not in use or the
workaround is not required. */
snd_config_t * local_config;
int is_pa;
};
enum stream_state { INACTIVE, RUNNING, DRAINING, PROCESSING, ERROR };
struct cubeb_stream {
/* Note: Must match cubeb_stream layout in cubeb.c. */
cubeb * context;
void * user_ptr;
/**/
pthread_mutex_t mutex;
snd_pcm_t * pcm;
cubeb_data_callback data_callback;
cubeb_state_callback state_callback;
snd_pcm_uframes_t stream_position;
snd_pcm_uframes_t last_position;
snd_pcm_uframes_t buffer_size;
cubeb_stream_params params;
/* Every member after this comment is protected by the owning context's
mutex rather than the stream's mutex, or is only used on the context's
run thread. */
pthread_cond_t cond; /* Signaled when the stream's state is changed. */
enum stream_state state;
struct pollfd * saved_fds; /* A copy of the pollfds passed in at init time. */
struct pollfd *
fds; /* Pointer to this waitable's pollfds within struct cubeb's fds. */
nfds_t nfds;
struct timeval drain_timeout;
/* XXX: Horrible hack -- if an active stream has been idle for
CUBEB_WATCHDOG_MS it will be disabled and the error callback will be
called. This works around a bug seen with older versions of ALSA and
PulseAudio where streams would stop requesting new data despite still
being logically active and playing. */
struct timeval last_activity;
float volume;
char * buffer;
snd_pcm_uframes_t bufframes;
snd_pcm_stream_t stream_type;
struct cubeb_stream * other_stream;
};
static int
any_revents(struct pollfd * fds, nfds_t nfds)
{
nfds_t i;
for (i = 0; i < nfds; ++i) {
if (fds[i].revents) {
return 1;
}
}
return 0;
}
static int
cmp_timeval(struct timeval * a, struct timeval * b)
{
if (a->tv_sec == b->tv_sec) {
if (a->tv_usec == b->tv_usec) {
return 0;
}
return a->tv_usec > b->tv_usec ? 1 : -1;
}
return a->tv_sec > b->tv_sec ? 1 : -1;
}
static int
timeval_to_relative_ms(struct timeval * tv)
{
struct timeval now;
struct timeval dt;
long long t;
int r;
gettimeofday(&now, NULL);
r = cmp_timeval(tv, &now);
if (r >= 0) {
timersub(tv, &now, &dt);
} else {
timersub(&now, tv, &dt);
}
t = dt.tv_sec;
t *= 1000;
t += (dt.tv_usec + 500) / 1000;
if (t > INT_MAX) {
t = INT_MAX;
} else if (t < INT_MIN) {
t = INT_MIN;
}
return r >= 0 ? t : -t;
}
static int
ms_until(struct timeval * tv)
{
return timeval_to_relative_ms(tv);
}
static int
ms_since(struct timeval * tv)
{
return -timeval_to_relative_ms(tv);
}
static void
rebuild(cubeb * ctx)
{
nfds_t nfds;
int i;
nfds_t j;
cubeb_stream * stm;
assert(ctx->rebuild);
/* Always count context's control pipe fd. */
nfds = 1;
for (i = 0; i < CUBEB_STREAM_MAX; ++i) {
stm = ctx->streams[i];
if (stm) {
stm->fds = NULL;
if (stm->state == RUNNING) {
nfds += stm->nfds;
}
}
}
free(ctx->fds);
ctx->fds = calloc(nfds, sizeof(struct pollfd));
assert(ctx->fds);
ctx->nfds = nfds;
/* Include context's control pipe fd. */
ctx->fds[0].fd = ctx->control_fd_read;
ctx->fds[0].events = POLLIN | POLLERR;
for (i = 0, j = 1; i < CUBEB_STREAM_MAX; ++i) {
stm = ctx->streams[i];
if (stm && stm->state == RUNNING) {
memcpy(&ctx->fds[j], stm->saved_fds, stm->nfds * sizeof(struct pollfd));
stm->fds = &ctx->fds[j];
j += stm->nfds;
}
}
ctx->rebuild = 0;
}
static void
poll_wake(cubeb * ctx)
{
if (write(ctx->control_fd_write, "x", 1) < 0) {
/* ignore write error */
}
}
static void
set_timeout(struct timeval * timeout, unsigned int ms)
{
gettimeofday(timeout, NULL);
timeout->tv_sec += ms / 1000;
timeout->tv_usec += (ms % 1000) * 1000;
}
static void
stream_buffer_decrement(cubeb_stream * stm, long count)
{
if (count < 0 || (snd_pcm_uframes_t)count > stm->bufframes) {
count = stm->bufframes;
}
char * bufremains =
stm->buffer + WRAP(snd_pcm_frames_to_bytes)(stm->pcm, count);
memmove(stm->buffer, bufremains,
WRAP(snd_pcm_frames_to_bytes)(stm->pcm, stm->bufframes - count));
stm->bufframes -= count;
}
static void
alsa_set_stream_state(cubeb_stream * stm, enum stream_state state)
{
cubeb * ctx;
int r;
ctx = stm->context;
stm->state = state;
r = pthread_cond_broadcast(&stm->cond);
assert(r == 0);
ctx->rebuild = 1;
poll_wake(ctx);
}
static void
alsa_set_draining(cubeb_stream * stm)
{
alsa_set_stream_state(stm, DRAINING);
if (stm->stream_type == SND_PCM_STREAM_PLAYBACK && stm->other_stream &&
stm->other_stream->state == RUNNING) {
alsa_set_stream_state(stm->other_stream, DRAINING);
}
}
static enum stream_state
alsa_process_stream(cubeb_stream * stm, int other_stream_running)
{
unsigned short revents;
snd_pcm_sframes_t avail;
int draining;
draining = 0;
pthread_mutex_lock(&stm->mutex);
/* Call _poll_descriptors_revents() even if we don't use it
to let underlying plugins clear null events. Otherwise poll()
may wake up again and again, producing unnecessary CPU usage. */
WRAP(snd_pcm_poll_descriptors_revents)
(stm->pcm, stm->fds, stm->nfds, &revents);
avail = WRAP(snd_pcm_avail_update)(stm->pcm);
/* Got null event? Bail and wait for another wakeup. */
if (avail == 0) {
pthread_mutex_unlock(&stm->mutex);
return RUNNING;
}
/* This could happen if we were suspended with SIGSTOP/Ctrl+Z for a long time.
*/
if ((unsigned int)avail > stm->buffer_size) {
avail = stm->buffer_size;
}
/* Capture: Read available frames */
if (stm->stream_type == SND_PCM_STREAM_CAPTURE && avail > 0) {
snd_pcm_sframes_t got;
if (avail + stm->bufframes > stm->buffer_size) {
/* Buffer overflow. Skip and overwrite with new data. */
stm->bufframes = 0;
// TODO: should it be marked as DRAINING?
}
got = WRAP(snd_pcm_readi)(stm->pcm, stm->buffer + stm->bufframes, avail);
if (got < 0) {
avail = got; // the error handler below will recover us
} else {
stm->bufframes += got;
stm->stream_position += got;
gettimeofday(&stm->last_activity, NULL);
}
}
/* Capture: Pass read frames to callback function */
if (stm->stream_type == SND_PCM_STREAM_CAPTURE && stm->bufframes > 0 &&
(!stm->other_stream ||
stm->other_stream->bufframes < stm->other_stream->buffer_size)) {
snd_pcm_sframes_t wrote = stm->bufframes;
snd_pcm_sframes_t requested;
struct cubeb_stream * mainstm = stm->other_stream ? stm->other_stream : stm;
void * other_buffer = stm->other_stream ? stm->other_stream->buffer +
stm->other_stream->bufframes
: NULL;
/* Correct write size to the other stream available space */
if (stm->other_stream &&
wrote > (snd_pcm_sframes_t)(stm->other_stream->buffer_size -
stm->other_stream->bufframes)) {
wrote = stm->other_stream->buffer_size - stm->other_stream->bufframes;
}
requested = wrote;
pthread_mutex_unlock(&stm->mutex);
wrote = stm->data_callback(mainstm, stm->user_ptr, stm->buffer,
other_buffer, wrote);
pthread_mutex_lock(&stm->mutex);
if (wrote < 0) {
avail = wrote; // the error handler below will recover us
} else {
stream_buffer_decrement(stm, wrote);
if (stm->other_stream) {
stm->other_stream->bufframes += wrote;
}
if (wrote < requested) {
if (!stm->other_stream) {
pthread_mutex_unlock(&stm->mutex);
return INACTIVE;
}
draining = 1;
set_timeout(&stm->drain_timeout, 0);
}
}
}
/* Playback: Don't have enough data? Let's ask for more. */
if (stm->stream_type == SND_PCM_STREAM_PLAYBACK &&
avail > (snd_pcm_sframes_t)stm->bufframes &&
(!stm->other_stream ||
(other_stream_running && stm->other_stream->bufframes > 0))) {
long got = avail - stm->bufframes;
void * other_buffer = stm->other_stream ? stm->other_stream->buffer : NULL;
char * buftail =
stm->buffer + WRAP(snd_pcm_frames_to_bytes)(stm->pcm, stm->bufframes);
long requested;
/* Correct read size to the other stream available frames */
if (stm->other_stream &&
got > (snd_pcm_sframes_t)stm->other_stream->bufframes) {
got = stm->other_stream->bufframes;
}
requested = got;
pthread_mutex_unlock(&stm->mutex);
got = stm->data_callback(stm, stm->user_ptr, other_buffer, buftail, got);
pthread_mutex_lock(&stm->mutex);
if (got < 0) {
avail = got; // the error handler below will recover us
} else {
stm->bufframes += got;
if (stm->other_stream) {
stream_buffer_decrement(stm->other_stream, got);
}
if (got < requested) {
draining = 1;
}
}
}
/* Playback: Still don't have enough data? Add some silence. */
if (stm->stream_type == SND_PCM_STREAM_PLAYBACK &&
avail > (snd_pcm_sframes_t)stm->bufframes) {
long drain_frames = avail - stm->bufframes;
double drain_time = (double)drain_frames / stm->params.rate;
char * buftail =
stm->buffer + WRAP(snd_pcm_frames_to_bytes)(stm->pcm, stm->bufframes);
memset(buftail, 0, WRAP(snd_pcm_frames_to_bytes)(stm->pcm, drain_frames));
stm->bufframes = avail;
/* Mark as draining, unless we're waiting for capture */
if (!stm->other_stream || draining || stm->other_stream->bufframes > 0) {
set_timeout(&stm->drain_timeout, drain_time * 1000);
draining = 1;
}
}
/* Playback: Have enough data and no errors. Let's write it out. */
if (stm->stream_type == SND_PCM_STREAM_PLAYBACK && avail > 0) {
snd_pcm_sframes_t wrote;
if (stm->params.format == CUBEB_SAMPLE_FLOAT32NE) {
float * b = (float *)stm->buffer;
for (uint32_t i = 0; i < avail * stm->params.channels; i++) {
b[i] *= stm->volume;
}
} else {
short * b = (short *)stm->buffer;
for (uint32_t i = 0; i < avail * stm->params.channels; i++) {
b[i] *= stm->volume;
}
}
wrote = WRAP(snd_pcm_writei)(stm->pcm, stm->buffer, avail);
if (wrote < 0) {
avail = wrote; // the error handler below will recover us
} else {
stream_buffer_decrement(stm, wrote);
stm->stream_position += wrote;
gettimeofday(&stm->last_activity, NULL);
}
}
/* Got some error? Let's try to recover the stream. */
if (avail < 0) {
avail = WRAP(snd_pcm_recover)(stm->pcm, avail, 0);
/* Capture pcm must be started after initial setup/recover */
if (avail >= 0 && stm->stream_type == SND_PCM_STREAM_CAPTURE &&
WRAP(snd_pcm_state)(stm->pcm) == SND_PCM_STATE_PREPARED) {
avail = WRAP(snd_pcm_start)(stm->pcm);
}
}
/* Failed to recover, this stream must be broken. */
if (avail < 0) {
pthread_mutex_unlock(&stm->mutex);
stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR);
return ERROR;
}
pthread_mutex_unlock(&stm->mutex);
return draining ? DRAINING : RUNNING;
}
static int
alsa_run(cubeb * ctx)
{
int r;
int timeout;
int i;
char dummy;
cubeb_stream * stm;
enum stream_state state;
pthread_mutex_lock(&ctx->mutex);
if (ctx->rebuild) {
rebuild(ctx);
}
/* Wake up at least once per second for the watchdog. */
timeout = 1000;
for (i = 0; i < CUBEB_STREAM_MAX; ++i) {
stm = ctx->streams[i];
if (stm && stm->state == DRAINING) {
r = ms_until(&stm->drain_timeout);
if (r < 0) {
r = 0;
}
if (timeout > r) {
timeout = r;
}
}
}
pthread_mutex_unlock(&ctx->mutex);
r = poll(ctx->fds, ctx->nfds, timeout);
pthread_mutex_lock(&ctx->mutex);
if (r > 0) {
if (ctx->fds[0].revents & POLLIN) {
if (read(ctx->control_fd_read, &dummy, 1) < 0) {
/* ignore read error */
}
if (ctx->shutdown) {
pthread_mutex_unlock(&ctx->mutex);
return -1;
}
}
for (i = 0; i < CUBEB_STREAM_MAX; ++i) {
stm = ctx->streams[i];
/* We can't use snd_pcm_poll_descriptors_revents here because of
https://github.com/kinetiknz/cubeb/issues/135. */
if (stm && stm->state == RUNNING && stm->fds &&
any_revents(stm->fds, stm->nfds)) {
int other_stream_running =
!stm->other_stream || stm->other_stream->state == RUNNING;
alsa_set_stream_state(stm, PROCESSING);
pthread_mutex_unlock(&ctx->mutex);
state = alsa_process_stream(stm, other_stream_running);
pthread_mutex_lock(&ctx->mutex);
if (state == DRAINING) {
alsa_set_draining(stm);
} else if (state == INACTIVE) {
alsa_set_stream_state(stm, state);
stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_STOPPED);
} else {
alsa_set_stream_state(stm, state);
}
}
}
} else if (r == 0) {
for (i = 0; i < CUBEB_STREAM_MAX; ++i) {
stm = ctx->streams[i];
if (stm) {
if (stm->state == DRAINING && ms_since(&stm->drain_timeout) >= 0) {
alsa_set_stream_state(stm, INACTIVE);
if (!stm->other_stream ||
stm->stream_type == SND_PCM_STREAM_PLAYBACK) {
stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_DRAINED);
}
} else if (stm->state == RUNNING &&
ms_since(&stm->last_activity) > CUBEB_WATCHDOG_MS) {
alsa_set_stream_state(stm, ERROR);
stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR);
}
}
}
}
pthread_mutex_unlock(&ctx->mutex);
return 0;
}
static void *
alsa_run_thread(void * context)
{
cubeb * ctx = context;
int r;
CUBEB_REGISTER_THREAD("cubeb rendering thread");
do {
r = alsa_run(ctx);
} while (r >= 0);
CUBEB_UNREGISTER_THREAD();
return NULL;
}
static snd_config_t *
get_slave_pcm_node(snd_config_t * lconf, snd_config_t * root_pcm)
{
int r;
snd_config_t * slave_pcm;
snd_config_t * slave_def;
snd_config_t * pcm;
char const * string;
char node_name[64];
slave_def = NULL;
r = WRAP(snd_config_search)(root_pcm, "slave", &slave_pcm);
if (r < 0) {
return NULL;
}
r = WRAP(snd_config_get_string)(slave_pcm, &string);
if (r >= 0) {
r = WRAP(snd_config_search_definition)(lconf, "pcm_slave", string,
&slave_def);
if (r < 0) {
return NULL;
}
}
do {
r = WRAP(snd_config_search)(slave_def ? slave_def : slave_pcm, "pcm", &pcm);
if (r < 0) {
break;
}
r = WRAP(snd_config_get_string)(slave_def ? slave_def : slave_pcm, &string);
if (r < 0) {
break;
}
r = snprintf(node_name, sizeof(node_name), "pcm.%s", string);
if (r < 0 || r > (int)sizeof(node_name)) {
break;
}
r = WRAP(snd_config_search)(lconf, node_name, &pcm);
if (r < 0) {
break;
}
return pcm;
} while (0);
if (slave_def) {
WRAP(snd_config_delete)(slave_def);
}
return NULL;
}
/* Work around PulseAudio ALSA plugin bug where the PA server forces a
higher than requested latency, but the plugin does not update its (and
ALSA's) internal state to reflect that, leading to an immediate underrun
situation. Inspired by WINE's make_handle_underrun_config.
Reference: http://mailman.alsa-project.org/pipermail/alsa-devel/2012-July/05
*/
static snd_config_t *
init_local_config_with_workaround(char const * pcm_name)
{
int r;
snd_config_t * lconf;
snd_config_t * pcm_node;
snd_config_t * node;
char const * string;
char node_name[64];
lconf = NULL;
if (WRAP(snd_config) == NULL) {
return NULL;
}
r = WRAP(snd_config_copy)(&lconf, WRAP(snd_config));
if (r < 0) {
return NULL;
}
do {
r = WRAP(snd_config_search_definition)(lconf, "pcm", pcm_name, &pcm_node);
if (r < 0) {
break;
}
r = WRAP(snd_config_get_id)(pcm_node, &string);
if (r < 0) {
break;
}
r = snprintf(node_name, sizeof(node_name), "pcm.%s", string);
if (r < 0 || r > (int)sizeof(node_name)) {
break;
}
r = WRAP(snd_config_search)(lconf, node_name, &pcm_node);
if (r < 0) {
break;
}
/* If this PCM has a slave, walk the slave configurations until we reach the
* bottom. */
while ((node = get_slave_pcm_node(lconf, pcm_node)) != NULL) {
pcm_node = node;
}
/* Fetch the PCM node's type, and bail out if it's not the PulseAudio
* plugin. */
r = WRAP(snd_config_search)(pcm_node, "type", &node);
if (r < 0) {
break;
}
r = WRAP(snd_config_get_string)(node, &string);
if (r < 0) {
break;
}
if (strcmp(string, "pulse") != 0) {
break;
}
/* Don't clobber an explicit existing handle_underrun value, set it only
if it doesn't already exist. */
r = WRAP(snd_config_search)(pcm_node, "handle_underrun", &node);
if (r != -ENOENT) {
break;
}
/* Disable pcm_pulse's asynchronous underrun handling. */
r = WRAP(snd_config_imake_integer)(&node, "handle_underrun", 0);
if (r < 0) {
break;
}
r = WRAP(snd_config_add)(pcm_node, node);
if (r < 0) {
break;
}
return lconf;
} while (0);
WRAP(snd_config_delete)(lconf);
return NULL;
}
static int
alsa_locked_pcm_open(snd_pcm_t ** pcm, char const * pcm_name,
snd_pcm_stream_t stream, snd_config_t * local_config)
{
int r;
pthread_mutex_lock(&cubeb_alsa_mutex);
if (local_config) {
r = WRAP(snd_pcm_open_lconf)(pcm, pcm_name, stream, SND_PCM_NONBLOCK,
local_config);
} else {
r = WRAP(snd_pcm_open)(pcm, pcm_name, stream, SND_PCM_NONBLOCK);
}
pthread_mutex_unlock(&cubeb_alsa_mutex);
return r;
}
static int
alsa_locked_pcm_close(snd_pcm_t * pcm)
{
int r;
pthread_mutex_lock(&cubeb_alsa_mutex);
r = WRAP(snd_pcm_close)(pcm);
pthread_mutex_unlock(&cubeb_alsa_mutex);
return r;
}
static int
alsa_register_stream(cubeb * ctx, cubeb_stream * stm)
{
int i;
pthread_mutex_lock(&ctx->mutex);
for (i = 0; i < CUBEB_STREAM_MAX; ++i) {
if (!ctx->streams[i]) {
ctx->streams[i] = stm;
break;
}
}
pthread_mutex_unlock(&ctx->mutex);
return i == CUBEB_STREAM_MAX;
}
static void
alsa_unregister_stream(cubeb_stream * stm)
{
cubeb * ctx;
int i;
ctx = stm->context;
pthread_mutex_lock(&ctx->mutex);
for (i = 0; i < CUBEB_STREAM_MAX; ++i) {
if (ctx->streams[i] == stm) {
ctx->streams[i] = NULL;
break;
}
}
pthread_mutex_unlock(&ctx->mutex);
}
static void
silent_error_handler(char const * file, int line, char const * function,
int err, char const * fmt, ...)
{
(void)file;
(void)line;
(void)function;
(void)err;
(void)fmt;
}
/*static*/ int
alsa_init(cubeb ** context, char const * context_name)
{
(void)context_name;
void * libasound = NULL;
cubeb * ctx;
int r;
int i;
int fd[2];
pthread_attr_t attr;
snd_pcm_t * dummy;
assert(context);
*context = NULL;
#ifndef DISABLE_LIBASOUND_DLOPEN
libasound = dlopen("libasound.so.2", RTLD_LAZY);
if (!libasound) {
libasound = dlopen("libasound.so", RTLD_LAZY);
if (!libasound) {
return CUBEB_ERROR;
}
}
#define LOAD(x) \
{ \
cubeb_##x = dlsym(libasound, #x); \
if (!cubeb_##x) { \
dlclose(libasound); \
return CUBEB_ERROR; \
} \
}
LIBASOUND_API_VISIT(LOAD);
#undef LOAD
#endif
pthread_mutex_lock(&cubeb_alsa_mutex);
if (!cubeb_alsa_error_handler_set) {
WRAP(snd_lib_error_set_handler)(silent_error_handler);
cubeb_alsa_error_handler_set = 1;
}
pthread_mutex_unlock(&cubeb_alsa_mutex);
ctx = calloc(1, sizeof(*ctx));
assert(ctx);
ctx->ops = &alsa_ops;
ctx->libasound = libasound;
r = pthread_mutex_init(&ctx->mutex, NULL);
assert(r == 0);
r = pipe(fd);
assert(r == 0);
for (i = 0; i < 2; ++i) {
fcntl(fd[i], F_SETFD, fcntl(fd[i], F_GETFD) | FD_CLOEXEC);
fcntl(fd[i], F_SETFL, fcntl(fd[i], F_GETFL) | O_NONBLOCK);
}
ctx->control_fd_read = fd[0];
ctx->control_fd_write = fd[1];
/* Force an early rebuild when alsa_run is first called to ensure fds and
nfds have been initialized. */
ctx->rebuild = 1;
r = pthread_attr_init(&attr);
assert(r == 0);
r = pthread_attr_setstacksize(&attr, 256 * 1024);
assert(r == 0);
r = pthread_create(&ctx->thread, &attr, alsa_run_thread, ctx);
assert(r == 0);
r = pthread_attr_destroy(&attr);
assert(r == 0);
/* Open a dummy PCM to force the configuration space to be evaluated so that
init_local_config_with_workaround can find and modify the default node. */
r = alsa_locked_pcm_open(&dummy, CUBEB_ALSA_PCM_NAME, SND_PCM_STREAM_PLAYBACK,
NULL);
if (r >= 0) {
alsa_locked_pcm_close(dummy);
}
ctx->is_pa = 0;
pthread_mutex_lock(&cubeb_alsa_mutex);
ctx->local_config = init_local_config_with_workaround(CUBEB_ALSA_PCM_NAME);
pthread_mutex_unlock(&cubeb_alsa_mutex);
if (ctx->local_config) {
ctx->is_pa = 1;
r = alsa_locked_pcm_open(&dummy, CUBEB_ALSA_PCM_NAME,
SND_PCM_STREAM_PLAYBACK, ctx->local_config);
/* If we got a local_config, we found a PA PCM. If opening a PCM with that
config fails with EINVAL, the PA PCM is too old for this workaround. */
if (r == -EINVAL) {
pthread_mutex_lock(&cubeb_alsa_mutex);
WRAP(snd_config_delete)(ctx->local_config);
pthread_mutex_unlock(&cubeb_alsa_mutex);
ctx->local_config = NULL;
} else if (r >= 0) {
alsa_locked_pcm_close(dummy);
}
}
*context = ctx;
return CUBEB_OK;
}
static char const *
alsa_get_backend_id(cubeb * ctx)
{
(void)ctx;
return "alsa";
}
static void
alsa_destroy(cubeb * ctx)
{
int r;
assert(ctx);
pthread_mutex_lock(&ctx->mutex);
ctx->shutdown = 1;
poll_wake(ctx);
pthread_mutex_unlock(&ctx->mutex);
r = pthread_join(ctx->thread, NULL);
assert(r == 0);
close(ctx->control_fd_read);
close(ctx->control_fd_write);
pthread_mutex_destroy(&ctx->mutex);
free(ctx->fds);
if (ctx->local_config) {
pthread_mutex_lock(&cubeb_alsa_mutex);
WRAP(snd_config_delete)(ctx->local_config);
pthread_mutex_unlock(&cubeb_alsa_mutex);
}
#ifndef DISABLE_LIBASOUND_DLOPEN
if (ctx->libasound) {
dlclose(ctx->libasound);
}
#endif
free(ctx);
}
static void
alsa_stream_destroy(cubeb_stream * stm);
static int
alsa_stream_init_single(cubeb * ctx, cubeb_stream ** stream,
char const * stream_name, snd_pcm_stream_t stream_type,
cubeb_devid deviceid,
cubeb_stream_params * stream_params,
unsigned int latency_frames,
cubeb_data_callback data_callback,
cubeb_state_callback state_callback, void * user_ptr)
{
(void)stream_name;
cubeb_stream * stm;
int r;
snd_pcm_format_t format;
snd_pcm_uframes_t period_size;
int latency_us = 0;
char const * pcm_name =
deviceid ? (char const *)deviceid : CUBEB_ALSA_PCM_NAME;
assert(ctx && stream);
*stream = NULL;
if (stream_params->prefs & CUBEB_STREAM_PREF_LOOPBACK) {
return CUBEB_ERROR_NOT_SUPPORTED;
}
switch (stream_params->format) {
case CUBEB_SAMPLE_S16LE:
format = SND_PCM_FORMAT_S16_LE;
break;
case CUBEB_SAMPLE_S16BE:
format = SND_PCM_FORMAT_S16_BE;
break;
case CUBEB_SAMPLE_FLOAT32LE:
format = SND_PCM_FORMAT_FLOAT_LE;
break;
case CUBEB_SAMPLE_FLOAT32BE:
format = SND_PCM_FORMAT_FLOAT_BE;
break;
default:
return CUBEB_ERROR_INVALID_FORMAT;
}
pthread_mutex_lock(&ctx->mutex);
if (ctx->active_streams >= CUBEB_STREAM_MAX) {
pthread_mutex_unlock(&ctx->mutex);
return CUBEB_ERROR;
}
ctx->active_streams += 1;
pthread_mutex_unlock(&ctx->mutex);
stm = calloc(1, sizeof(*stm));
assert(stm);
stm->context = ctx;
stm->data_callback = data_callback;
stm->state_callback = state_callback;
stm->user_ptr = user_ptr;
stm->params = *stream_params;
stm->state = INACTIVE;
stm->volume = 1.0;
stm->buffer = NULL;
stm->bufframes = 0;
stm->stream_type = stream_type;
stm->other_stream = NULL;
r = pthread_mutex_init(&stm->mutex, NULL);
assert(r == 0);
r = pthread_cond_init(&stm->cond, NULL);
assert(r == 0);
r = alsa_locked_pcm_open(&stm->pcm, pcm_name, stm->stream_type,
ctx->local_config);
if (r < 0) {
alsa_stream_destroy(stm);
return CUBEB_ERROR;
}
r = WRAP(snd_pcm_nonblock)(stm->pcm, 1);
assert(r == 0);
latency_us = latency_frames * 1e6 / stm->params.rate;
/* Ugly hack: the PA ALSA plugin allows buffer configurations that can't
possibly work. See https://bugzilla.mozilla.org/show_bug.cgi?id=761274.
Only resort to this hack if the handle_underrun workaround failed. */
if (!ctx->local_config && ctx->is_pa) {
const int min_latency = 5e5;
latency_us = latency_us < min_latency ? min_latency : latency_us;
}
r = WRAP(snd_pcm_set_params)(stm->pcm, format, SND_PCM_ACCESS_RW_INTERLEAVED,
stm->params.channels, stm->params.rate, 1,
latency_us);
if (r < 0) {
alsa_stream_destroy(stm);
return CUBEB_ERROR_INVALID_FORMAT;
}
r = WRAP(snd_pcm_get_params)(stm->pcm, &stm->buffer_size, &period_size);
assert(r == 0);
/* Double internal buffer size to have enough space when waiting for the other
* side of duplex connection */
stm->buffer_size *= 2;
stm->buffer =
calloc(1, WRAP(snd_pcm_frames_to_bytes)(stm->pcm, stm->buffer_size));
assert(stm->buffer);
stm->nfds = WRAP(snd_pcm_poll_descriptors_count)(stm->pcm);
assert(stm->nfds > 0);
stm->saved_fds = calloc(stm->nfds, sizeof(struct pollfd));
assert(stm->saved_fds);
r = WRAP(snd_pcm_poll_descriptors)(stm->pcm, stm->saved_fds, stm->nfds);
assert((nfds_t)r == stm->nfds);
if (alsa_register_stream(ctx, stm) != 0) {
alsa_stream_destroy(stm);
return CUBEB_ERROR;
}
*stream = stm;
return CUBEB_OK;
}
static int
alsa_stream_init(cubeb * ctx, cubeb_stream ** stream, char const * stream_name,
cubeb_devid input_device,
cubeb_stream_params * input_stream_params,
cubeb_devid output_device,
cubeb_stream_params * output_stream_params,
unsigned int latency_frames, cubeb_data_callback data_callback,
cubeb_state_callback state_callback, void * user_ptr)
{
int result = CUBEB_OK;
cubeb_stream *instm = NULL, *outstm = NULL;
if (result == CUBEB_OK && input_stream_params) {
result = alsa_stream_init_single(ctx, &instm, stream_name,
SND_PCM_STREAM_CAPTURE, input_device,
input_stream_params, latency_frames,
data_callback, state_callback, user_ptr);
}
if (result == CUBEB_OK && output_stream_params) {
result = alsa_stream_init_single(ctx, &outstm, stream_name,
SND_PCM_STREAM_PLAYBACK, output_device,
output_stream_params, latency_frames,
data_callback, state_callback, user_ptr);
}
if (result == CUBEB_OK && input_stream_params && output_stream_params) {
instm->other_stream = outstm;
outstm->other_stream = instm;
}
if (result != CUBEB_OK && instm) {
alsa_stream_destroy(instm);
}
*stream = outstm ? outstm : instm;
return result;
}
static void
alsa_stream_destroy(cubeb_stream * stm)
{
int r;
cubeb * ctx;
assert(stm && (stm->state == INACTIVE || stm->state == ERROR ||
stm->state == DRAINING));
ctx = stm->context;
if (stm->other_stream) {
stm->other_stream->other_stream = NULL; // to stop infinite recursion
alsa_stream_destroy(stm->other_stream);
}
pthread_mutex_lock(&stm->mutex);
if (stm->pcm) {
if (stm->state == DRAINING) {
WRAP(snd_pcm_drain)(stm->pcm);
}
alsa_locked_pcm_close(stm->pcm);
stm->pcm = NULL;
}
free(stm->saved_fds);
pthread_mutex_unlock(&stm->mutex);
pthread_mutex_destroy(&stm->mutex);
r = pthread_cond_destroy(&stm->cond);
assert(r == 0);
alsa_unregister_stream(stm);
pthread_mutex_lock(&ctx->mutex);
assert(ctx->active_streams >= 1);
ctx->active_streams -= 1;
pthread_mutex_unlock(&ctx->mutex);
free(stm->buffer);
free(stm);
}
static int
alsa_get_max_channel_count(cubeb * ctx, uint32_t * max_channels)
{
int r;
cubeb_stream * stm;
snd_pcm_hw_params_t * hw_params;
cubeb_stream_params params;
params.rate = 44100;
params.format = CUBEB_SAMPLE_FLOAT32NE;
params.channels = 2;
snd_pcm_hw_params_alloca(&hw_params);
assert(ctx);
r = alsa_stream_init(ctx, &stm, "", NULL, NULL, NULL, ¶ms, 100, NULL,
NULL, NULL);
if (r != CUBEB_OK) {
return CUBEB_ERROR;
}
assert(stm);
r = WRAP(snd_pcm_hw_params_any)(stm->pcm, hw_params);
if (r < 0) {
return CUBEB_ERROR;
}
r = WRAP(snd_pcm_hw_params_get_channels_max)(hw_params, max_channels);
if (r < 0) {
return CUBEB_ERROR;
}
/* Cap at reasonable maximum to filter driver placeholder values */
if (*max_channels > 64) {
*max_channels = 64;
}
alsa_stream_destroy(stm);
return CUBEB_OK;
}
static int
alsa_get_preferred_sample_rate(cubeb * ctx, uint32_t * rate)
{
(void)ctx;
int r, dir;
snd_pcm_t * pcm;
snd_pcm_hw_params_t * hw_params;
snd_pcm_hw_params_alloca(&hw_params);
/* get a pcm, disabling resampling, so we get a rate the
* hardware/dmix/pulse/etc. supports. */
r = WRAP(snd_pcm_open)(&pcm, CUBEB_ALSA_PCM_NAME, SND_PCM_STREAM_PLAYBACK,
SND_PCM_NO_AUTO_RESAMPLE);
if (r < 0) {
return CUBEB_ERROR;
}
r = WRAP(snd_pcm_hw_params_any)(pcm, hw_params);
if (r < 0) {
WRAP(snd_pcm_close)(pcm);
return CUBEB_ERROR;
}
r = WRAP(snd_pcm_hw_params_get_rate)(hw_params, rate, &dir);
if (r >= 0) {
/* There is a default rate: use it. */
WRAP(snd_pcm_close)(pcm);
return CUBEB_OK;
}
/* Use a common rate, alsa may adjust it based on hw/etc. capabilities. */
*rate = 44100;
r = WRAP(snd_pcm_hw_params_set_rate_near)(pcm, hw_params, rate, NULL);
if (r < 0) {
WRAP(snd_pcm_close)(pcm);
return CUBEB_ERROR;
}
WRAP(snd_pcm_close)(pcm);
return CUBEB_OK;
}
static int
alsa_get_min_latency(cubeb * ctx, cubeb_stream_params params,
uint32_t * latency_frames)
{
(void)ctx;
/* 40ms is found to be an acceptable minimum, even on a super low-end
* machine. */
*latency_frames = 40 * params.rate / 1000;
return CUBEB_OK;
}
static int
alsa_stream_start(cubeb_stream * stm)
{
cubeb * ctx;
assert(stm);
ctx = stm->context;
if (stm->stream_type == SND_PCM_STREAM_PLAYBACK && stm->other_stream) {
int r = alsa_stream_start(stm->other_stream);
if (r != CUBEB_OK)
return r;
}
pthread_mutex_lock(&stm->mutex);
/* Capture pcm must be started after initial setup/recover */
if (stm->stream_type == SND_PCM_STREAM_CAPTURE &&
WRAP(snd_pcm_state)(stm->pcm) == SND_PCM_STATE_PREPARED) {
WRAP(snd_pcm_start)(stm->pcm);
}
WRAP(snd_pcm_pause)(stm->pcm, 0);
gettimeofday(&stm->last_activity, NULL);
pthread_mutex_unlock(&stm->mutex);
pthread_mutex_lock(&ctx->mutex);
if (stm->state != INACTIVE) {
pthread_mutex_unlock(&ctx->mutex);
return CUBEB_ERROR;
}
alsa_set_stream_state(stm, RUNNING);
pthread_mutex_unlock(&ctx->mutex);
return CUBEB_OK;
}
static int
alsa_stream_stop(cubeb_stream * stm)
{
cubeb * ctx;
int r;
assert(stm);
ctx = stm->context;
if (stm->stream_type == SND_PCM_STREAM_PLAYBACK && stm->other_stream) {
int r = alsa_stream_stop(stm->other_stream);
if (r != CUBEB_OK)
return r;
}
pthread_mutex_lock(&ctx->mutex);
while (stm->state == PROCESSING) {
r = pthread_cond_wait(&stm->cond, &ctx->mutex);
assert(r == 0);
}
alsa_set_stream_state(stm, INACTIVE);
pthread_mutex_unlock(&ctx->mutex);
pthread_mutex_lock(&stm->mutex);
WRAP(snd_pcm_pause)(stm->pcm, 1);
pthread_mutex_unlock(&stm->mutex);
return CUBEB_OK;
}
static int
alsa_stream_get_position(cubeb_stream * stm, uint64_t * position)
{
snd_pcm_sframes_t delay;
assert(stm && position);
pthread_mutex_lock(&stm->mutex);
delay = -1;
if (WRAP(snd_pcm_state)(stm->pcm) != SND_PCM_STATE_RUNNING ||
WRAP(snd_pcm_delay)(stm->pcm, &delay) != 0) {
*position = stm->last_position;
pthread_mutex_unlock(&stm->mutex);
return CUBEB_OK;
}
assert(delay >= 0);
*position = 0;
if (stm->stream_position >= (snd_pcm_uframes_t)delay) {
*position = stm->stream_position - delay;
}
stm->last_position = *position;
pthread_mutex_unlock(&stm->mutex);
return CUBEB_OK;
}
static int
alsa_stream_get_latency(cubeb_stream * stm, uint32_t * latency)
{
snd_pcm_sframes_t delay;
/* This function returns the delay in frames until a frame written using
snd_pcm_writei is sent to the DAC. The DAC delay should be < 1ms anyways.
*/
if (WRAP(snd_pcm_delay)(stm->pcm, &delay)) {
return CUBEB_ERROR;
}
*latency = delay;
return CUBEB_OK;
}
static int
alsa_stream_set_volume(cubeb_stream * stm, float volume)
{
/* setting the volume using an API call does not seem very stable/supported */
pthread_mutex_lock(&stm->mutex);
stm->volume = volume;
pthread_mutex_unlock(&stm->mutex);
return CUBEB_OK;
}
static int
alsa_enumerate_devices(cubeb * context, cubeb_device_type type,
cubeb_device_collection * collection)
{
void **hints = NULL;
void **hint;
char *name = NULL;
char *io = NULL;
char *desc = NULL;
cubeb_device_info * devinfop = NULL;
int collection_cnt = 0;
int r;
if (!context)
return CUBEB_ERROR;
/* Get device name hints from ALSA */
r = WRAP(snd_device_name_hint)(-1, "pcm", &hints);
if (r < 0) {
LOG("Failed to get device name hints: %d", r);
return CUBEB_ERROR;
}
/* Count devices first */
for (hint = hints; *hint != NULL; hint++) {
name = WRAP(snd_device_name_get_hint)(*hint, "NAME");
if (!name)
continue;
io = WRAP(snd_device_name_get_hint)(*hint, "IOID");
/* Filter by device type */
if (type & CUBEB_DEVICE_TYPE_INPUT && io && strcmp(io, "Output") == 0) {
free(name);
if (io) free(io);
continue;
}
if (type & CUBEB_DEVICE_TYPE_OUTPUT && io && strcmp(io, "Input") == 0) {
free(name);
if (io) free(io);
continue;
}
free(name);
if (io) free(io);
collection_cnt++;
}
if (collection_cnt == 0) {
WRAP(snd_device_name_free_hint)(hints);
return CUBEB_OK;
}
devinfop = calloc(collection_cnt, sizeof(cubeb_device_info));
if (!devinfop) {
WRAP(snd_device_name_free_hint)(hints);
return CUBEB_ERROR;
}
collection_cnt = 0;
for (hint = hints; *hint != NULL; hint++) {
snd_pcm_t * pcm = NULL;
snd_pcm_hw_params_t * hw_params;
snd_pcm_stream_t stream_type;
cubeb_device_info cdi = {0};
uint32_t min_rate, max_rate;
uint32_t min_channels, max_channels;
int dir;
name = WRAP(snd_device_name_get_hint)(*hint, "NAME");
if (!name)
continue;
io = WRAP(snd_device_name_get_hint)(*hint, "IOID");
desc = WRAP(snd_device_name_get_hint)(*hint, "DESC");
/* Filter by device type */
if (type & CUBEB_DEVICE_TYPE_INPUT && io && strcmp(io, "Output") == 0) {
goto cleanup_hint;
}
if (type & CUBEB_DEVICE_TYPE_OUTPUT && io && strcmp(io, "Input") == 0) {
goto cleanup_hint;
}
/* Determine stream type */
if (io && strcmp(io, "Input") == 0) {
stream_type = SND_PCM_STREAM_CAPTURE;
cdi.type = CUBEB_DEVICE_TYPE_INPUT;
} else if (io && strcmp(io, "Output") == 0) {
stream_type = SND_PCM_STREAM_PLAYBACK;
cdi.type = CUBEB_DEVICE_TYPE_OUTPUT;
} else {
/* Try both if IOID is not specified */
stream_type = (type & CUBEB_DEVICE_TYPE_OUTPUT) ?
SND_PCM_STREAM_PLAYBACK : SND_PCM_STREAM_CAPTURE;
cdi.type = (type & CUBEB_DEVICE_TYPE_OUTPUT) ?
CUBEB_DEVICE_TYPE_OUTPUT : CUBEB_DEVICE_TYPE_INPUT;
}
/* Open device to query capabilities */
r = WRAP(snd_pcm_open)(&pcm, name, stream_type, SND_PCM_NONBLOCK);
if (r < 0) {
LOG("Failed to open device %s: %d", name, r);
goto cleanup_hint;
}
snd_pcm_hw_params_alloca(&hw_params);
r = WRAP(snd_pcm_hw_params_any)(pcm, hw_params);
if (r < 0) {
LOG("Failed to get hw params for device %s: %d", name, r);
WRAP(snd_pcm_close)(pcm);
goto cleanup_hint;
}
/* Query format capabilities */
cdi.format = 0;
if (WRAP(snd_pcm_hw_params_test_format)(pcm, hw_params, SND_PCM_FORMAT_S16_LE) == 0)
cdi.format |= CUBEB_DEVICE_FMT_S16LE;
if (WRAP(snd_pcm_hw_params_test_format)(pcm, hw_params, SND_PCM_FORMAT_S16_BE) == 0)
cdi.format |= CUBEB_DEVICE_FMT_S16BE;
if (WRAP(snd_pcm_hw_params_test_format)(pcm, hw_params, SND_PCM_FORMAT_S32_LE) == 0)
cdi.format |= CUBEB_DEVICE_FMT_S32LE;
if (WRAP(snd_pcm_hw_params_test_format)(pcm, hw_params, SND_PCM_FORMAT_S32_BE) == 0)
cdi.format |= CUBEB_DEVICE_FMT_S32BE;
if (WRAP(snd_pcm_hw_params_test_format)(pcm, hw_params, SND_PCM_FORMAT_FLOAT_LE) == 0)
cdi.format |= CUBEB_DEVICE_FMT_F32LE;
if (WRAP(snd_pcm_hw_params_test_format)(pcm, hw_params, SND_PCM_FORMAT_FLOAT_BE) == 0)
cdi.format |= CUBEB_DEVICE_FMT_F32BE;
/* Set default format - prefer S32 for high-quality audio */
#if defined(WORDS_BIGENDIAN) || defined(__BIG_ENDIAN__)
if (cdi.format & CUBEB_DEVICE_FMT_S32BE)
cdi.default_format = CUBEB_DEVICE_FMT_S32BE;
else if (cdi.format & CUBEB_DEVICE_FMT_S16BE)
cdi.default_format = CUBEB_DEVICE_FMT_S16BE;
else if (cdi.format & CUBEB_DEVICE_FMT_F32BE)
cdi.default_format = CUBEB_DEVICE_FMT_F32BE;
else
cdi.default_format = CUBEB_DEVICE_FMT_S16BE;
#else
if (cdi.format & CUBEB_DEVICE_FMT_S32LE)
cdi.default_format = CUBEB_DEVICE_FMT_S32LE;
else if (cdi.format & CUBEB_DEVICE_FMT_S16LE)
cdi.default_format = CUBEB_DEVICE_FMT_S16LE;
else if (cdi.format & CUBEB_DEVICE_FMT_F32LE)
cdi.default_format = CUBEB_DEVICE_FMT_F32LE;
else
cdi.default_format = CUBEB_DEVICE_FMT_S16LE;
#endif
/* Query rate range */
r = WRAP(snd_pcm_hw_params_get_rate_min)(hw_params, &min_rate, &dir);
if (r < 0)
min_rate = 44100;
r = WRAP(snd_pcm_hw_params_get_rate_max)(hw_params, &max_rate, &dir);
if (r < 0)
max_rate = 48000;
/* Query channel range */
r = WRAP(snd_pcm_hw_params_get_channels_min)(hw_params, &min_channels);
if (r < 0)
min_channels = 1;
r = WRAP(snd_pcm_hw_params_get_channels_max)(hw_params, &max_channels);
if (r < 0)
max_channels = 2;
/* Get default rate */
r = WRAP(snd_pcm_hw_params_get_rate)(hw_params, &cdi.default_rate, &dir);
if (r < 0)
cdi.default_rate = 44100;
WRAP(snd_pcm_close)(pcm);
/* Fill device info */
cdi.device_id = strdup(name);
cdi.devid = (cubeb_devid)cdi.device_id;
cdi.friendly_name = desc ? strdup(desc) : strdup(name);
cdi.group_id = strdup(name);
cdi.vendor_name = NULL;
cdi.state = CUBEB_DEVICE_STATE_ENABLED;
cdi.preferred = CUBEB_DEVICE_PREF_NONE;
cdi.max_channels = max_channels;
cdi.min_rate = min_rate;
cdi.max_rate = max_rate;
cdi.latency_lo = 0;
cdi.latency_hi = 0;
if (!cdi.device_id || !cdi.friendly_name || !cdi.group_id) {
free((void *)cdi.device_id);
free((void *)cdi.friendly_name);
free((void *)cdi.group_id);
goto cleanup_hint;
}
devinfop[collection_cnt++] = cdi;
cleanup_hint:
free(name);
if (io) free(io);
if (desc) free(desc);
}
WRAP(snd_device_name_free_hint)(hints);
collection->device = devinfop;
collection->count = collection_cnt;
return CUBEB_OK;
}
static int
alsa_device_collection_destroy(cubeb * context,
cubeb_device_collection * collection)
{
size_t i;
(void)context;
for (i = 0; i < collection->count; ++i) {
free((void *)collection->device[i].device_id);
free((void *)collection->device[i].friendly_name);
free((void *)collection->device[i].group_id);
free((void *)collection->device[i].vendor_name);
}
free(collection->device);
return CUBEB_OK;
}
static struct cubeb_ops const alsa_ops = {
.init = alsa_init,
.get_backend_id = alsa_get_backend_id,
.get_max_channel_count = alsa_get_max_channel_count,
.get_min_latency = alsa_get_min_latency,
.get_preferred_sample_rate = alsa_get_preferred_sample_rate,
.get_supported_input_processing_params = NULL,
.enumerate_devices = alsa_enumerate_devices,
.device_collection_destroy = alsa_device_collection_destroy,
.destroy = alsa_destroy,
.stream_init = alsa_stream_init,
.stream_destroy = alsa_stream_destroy,
.stream_start = alsa_stream_start,
.stream_stop = alsa_stream_stop,
.stream_get_position = alsa_stream_get_position,
.stream_get_latency = alsa_stream_get_latency,
.stream_get_input_latency = NULL,
.stream_set_volume = alsa_stream_set_volume,
.stream_set_name = NULL,
.stream_get_current_device = NULL,
.stream_set_input_mute = NULL,
.stream_set_input_processing_params = NULL,
.stream_device_destroy = NULL,
.stream_register_device_changed_callback = NULL,
.register_device_collection_changed = NULL};Patches for Firefox
$ cat 0001-Add-S32-format-to-the-Firefox-API.patch
From 80bbf447a62e6136f54c4938b99487135383276b Mon Sep 17 00:00:00 2001
From: Devuan
Date: Tue, 7 Jul 2026 22:35:36 +0200
Subject: [PATCH 1/2] Add S32 format to the Firefox API
- Add FMT_S32LE and FMT_S32BE constants to nsIAudioDeviceInfo.idl
- Update AudioDeviceInfo.cpp assertions to accept S32 formats
- Add S32 format conversion in CubebDeviceEnumerator.cpp
- Add local S32 format definitions for external cubeb support
- Update aboutSupport.js to display S32 format information
---
dom/media/AudioDeviceInfo.cpp | 5 +++--
dom/media/CubebUtils.cpp | 1 +
dom/media/nsIAudioDeviceInfo.idl | 4 +++-
dom/media/webrtc/CubebDeviceEnumerator.cpp | 14 ++++++++++++++
toolkit/content/aboutSupport.js | 4 ++++
5 files changed, 25 insertions(+), 3 deletions(-)
diff --git a/dom/media/AudioDeviceInfo.cpp b/dom/media/AudioDeviceInfo.cpp
index 072dd43d4ec1..a09e0adf71c3 100644
--- a/dom/media/AudioDeviceInfo.cpp
+++ b/dom/media/AudioDeviceInfo.cpp
@@ -49,10 +49,11 @@ AudioDeviceInfo::AudioDeviceInfo(
mPreferred == PREF_NONE || mPreferred == PREF_ALL ||
mPreferred & (PREF_MULTIMEDIA | PREF_VOICE | PREF_NOTIFICATION),
"Wrong preferred value");
- MOZ_ASSERT(mSupportedFormat & (FMT_S16LE | FMT_S16BE | FMT_F32LE | FMT_F32BE),
+ MOZ_ASSERT(mSupportedFormat & (FMT_S16LE | FMT_S16BE | FMT_F32LE | FMT_F32BE | FMT_S32LE | FMT_S32BE),
"Wrong supported format");
MOZ_ASSERT(mDefaultFormat == FMT_S16LE || mDefaultFormat == FMT_S16BE ||
- mDefaultFormat == FMT_F32LE || mDefaultFormat == FMT_F32BE,
+ mDefaultFormat == FMT_F32LE || mDefaultFormat == FMT_F32BE ||
+ mDefaultFormat == FMT_S32LE || mDefaultFormat == FMT_S32BE,
"Wrong default format");
}
diff --git a/dom/media/CubebUtils.cpp b/dom/media/CubebUtils.cpp
index 72551b795af3..a9edab7bc358 100644
--- a/dom/media/CubebUtils.cpp
+++ b/dom/media/CubebUtils.cpp
@@ -143,6 +143,7 @@ MOZ_RUNINIT std::unordered_map<std::string, BackendLabel>
{"alsa", BackendLabel::eAlsa},
{"jack", BackendLabel::eJack},
{"oss", BackendLabel::eOss},
+ {"oss-fft", BackendLabel::eOss},
{"pulse", BackendLabel::ePulse},
{"pulse-rust", BackendLabel::ePulseRust},
{"sndio", BackendLabel::eSndio},
diff --git a/dom/media/nsIAudioDeviceInfo.idl b/dom/media/nsIAudioDeviceInfo.idl
index 0458ec1eb927..59d90a0ad3f5 100644
--- a/dom/media/nsIAudioDeviceInfo.idl
+++ b/dom/media/nsIAudioDeviceInfo.idl
@@ -33,11 +33,13 @@ interface nsIAudioDeviceInfo : nsISupports
const unsigned short PREF_ALL = 0x0F;
readonly attribute unsigned short preferred;
- // supported format, default format: S16LE/S16BE/F32LE/F32BE
+ // supported format, default format: S16LE/S16BE/F32LE/F32BE/S32LE/S32BE
const unsigned short FMT_S16LE = 0x0010;
const unsigned short FMT_S16BE = 0x0020;
const unsigned short FMT_F32LE = 0x1000;
const unsigned short FMT_F32BE = 0x2000;
+ const unsigned short FMT_S32LE = 0x0040;
+ const unsigned short FMT_S32BE = 0x0080;
readonly attribute unsigned short supportedFormat;
readonly attribute unsigned short defaultFormat;
diff --git a/dom/media/webrtc/CubebDeviceEnumerator.cpp b/dom/media/webrtc/CubebDeviceEnumerator.cpp
index 6e1520c3c714..0d2043cb497c 100644
--- a/dom/media/webrtc/CubebDeviceEnumerator.cpp
+++ b/dom/media/webrtc/CubebDeviceEnumerator.cpp
@@ -17,6 +17,14 @@
# include "mozilla/mscom/EnsureMTA.h"
#endif
+// S32 format constants for external cubeb libraries that support them
+#ifndef CUBEB_DEVICE_FMT_S32LE
+#define CUBEB_DEVICE_FMT_S32LE 0x0040
+#endif
+#ifndef CUBEB_DEVICE_FMT_S32BE
+#define CUBEB_DEVICE_FMT_S32BE 0x0080
+#endif
+
namespace mozilla {
using namespace CubebUtils;
@@ -183,6 +191,12 @@ static uint16_t ConvertCubebFormat(cubeb_device_fmt aFormat) {
if (aFormat & CUBEB_DEVICE_FMT_F32BE) {
format |= nsIAudioDeviceInfo::FMT_F32BE;
}
+ if (aFormat & CUBEB_DEVICE_FMT_S32LE) {
+ format |= nsIAudioDeviceInfo::FMT_S32LE;
+ }
+ if (aFormat & CUBEB_DEVICE_FMT_S32BE) {
+ format |= nsIAudioDeviceInfo::FMT_S32BE;
+ }
return format;
}
diff --git a/toolkit/content/aboutSupport.js b/toolkit/content/aboutSupport.js
index 388f8048201f..af6e396feb7d 100644
--- a/toolkit/content/aboutSupport.js
+++ b/toolkit/content/aboutSupport.js
@@ -974,6 +974,8 @@ var snapshotFormatters = {
formats[deviceInfo.FMT_S16BE] = "S16BE";
formats[deviceInfo.FMT_F32LE] = "F32LE";
formats[deviceInfo.FMT_F32BE] = "F32BE";
+ formats[deviceInfo.FMT_S32LE] = "S32LE";
+ formats[deviceInfo.FMT_S32BE] = "S32BE";
function toPreferredString(preferred) {
if (preferred == deviceInfo.PREF_NONE) {
@@ -1001,6 +1003,8 @@ var snapshotFormatters = {
deviceInfo.FMT_S16BE,
deviceInfo.FMT_F32LE,
deviceInfo.FMT_F32BE,
+ deviceInfo.FMT_S32LE,
+ deviceInfo.FMT_S32BE,
]) {
if (dev.supportedFormat & fmt) {
str += " " + formats[fmt];
--
2.39.5$ cat 0002-Add-S32-format-to-the-Cubeb-API.patch
From f8d402971731b6729daf47624fe74a7e7466ee24 Mon Sep 17 00:00:00 2001
From: Devuan
Date: Thu, 9 Jul 2026 17:48:56 +0200
Subject: [PATCH 2/2] Add S32 format to the Cubeb API
- Add CUBEB_DEVICE_FMT_S32LE and CUBEB_DEVICE_FMT_S32BE constants to API
- Update log_device function to handle S32 formats in logging
- Update test_devices to handle S32 format display
---
media/libcubeb/include/cubeb/cubeb.h | 17 ++++++++++++-----
media/libcubeb/src/cubeb.c | 23 +++++++++++++++++++++++
media/libcubeb/test/test_devices.cpp | 10 ++++++++++
3 files changed, 45 insertions(+), 5 deletions(-)
diff --git a/media/libcubeb/include/cubeb/cubeb.h b/media/libcubeb/include/cubeb/cubeb.h
index ff2f8b7b987d..322230a2df11 100644
--- a/media/libcubeb/include/cubeb/cubeb.h
+++ b/media/libcubeb/include/cubeb/cubeb.h
@@ -340,6 +340,8 @@ typedef enum {
typedef enum {
CUBEB_DEVICE_FMT_S16LE = 0x0010, /**< 16-bit integers, Little Endian. */
CUBEB_DEVICE_FMT_S16BE = 0x0020, /**< 16-bit integers, Big Endian. */
+ CUBEB_DEVICE_FMT_S32LE = 0x0040, /**< 32-bit integers, Little Endian. */
+ CUBEB_DEVICE_FMT_S32BE = 0x0080, /**< 32-bit integers, Big Endian. */
CUBEB_DEVICE_FMT_F32LE = 0x1000, /**< 32-bit floating point, Little Endian. */
CUBEB_DEVICE_FMT_F32BE = 0x2000 /**< 32-bit floating point, Big Endian. */
} cubeb_device_fmt;
@@ -347,25 +349,30 @@ typedef enum {
#if defined(WORDS_BIGENDIAN) || defined(__BIG_ENDIAN__)
/** 16-bit integers, native endianess, when on a Big Endian environment. */
#define CUBEB_DEVICE_FMT_S16NE CUBEB_DEVICE_FMT_S16BE
-/** 32-bit floating points, native endianess, when on a Big Endian environment.
- */
+/** 32-bit integers, native endianess, when on a Big Endian environment. */
+#define CUBEB_DEVICE_FMT_S32NE CUBEB_DEVICE_FMT_S32BE
+/** 32-bit floating points, native endianess, when on a Big Endian environment. */
#define CUBEB_DEVICE_FMT_F32NE CUBEB_DEVICE_FMT_F32BE
#else
/** 16-bit integers, native endianess, when on a Little Endian environment. */
#define CUBEB_DEVICE_FMT_S16NE CUBEB_DEVICE_FMT_S16LE
-/** 32-bit floating points, native endianess, when on a Little Endian
- * environment. */
+/** 32-bit integers, native endianess, when on a Little Endian environment. */
+#define CUBEB_DEVICE_FMT_S32NE CUBEB_DEVICE_FMT_S32LE
+/** 32-bit floating points, native endianess, when on a Little Endian environment. */
#define CUBEB_DEVICE_FMT_F32NE CUBEB_DEVICE_FMT_F32LE
#endif
/** All the 16-bit integers types. */
#define CUBEB_DEVICE_FMT_S16_MASK \
(CUBEB_DEVICE_FMT_S16LE | CUBEB_DEVICE_FMT_S16BE)
+/** All the 32-bit integers types. */
+#define CUBEB_DEVICE_FMT_S32_MASK \
+ (CUBEB_DEVICE_FMT_S32LE | CUBEB_DEVICE_FMT_S32BE)
/** All the 32-bit floating points types. */
#define CUBEB_DEVICE_FMT_F32_MASK \
(CUBEB_DEVICE_FMT_F32LE | CUBEB_DEVICE_FMT_F32BE)
/** All the device formats types. */
#define CUBEB_DEVICE_FMT_ALL \
- (CUBEB_DEVICE_FMT_S16_MASK | CUBEB_DEVICE_FMT_F32_MASK)
+ (CUBEB_DEVICE_FMT_S16_MASK | CUBEB_DEVICE_FMT_S32_MASK | CUBEB_DEVICE_FMT_F32_MASK)
/** Channel type for a `cubeb_stream`. Depending on the backend and platform
* used, this can control inter-stream interruption, ducking, and volume
diff --git a/media/libcubeb/src/cubeb.c b/media/libcubeb/src/cubeb.c
index 4fdead389354..1ca8582bab71 100644
--- a/media/libcubeb/src/cubeb.c
+++ b/media/libcubeb/src/cubeb.c
@@ -75,6 +75,10 @@ opensl_init(cubeb ** context, char const * context_name);
int
oss_init(cubeb ** context, char const * context_name);
#endif
+#if defined(USE_OSS_FFT)
+int
+oss_fft_init(cubeb ** context, char const * context_name);
+#endif
#if defined(USE_AAUDIO)
int
aaudio_init(cubeb ** context, char const * context_name);
@@ -195,6 +199,10 @@ cubeb_init(cubeb ** context, char const * context_name,
} else if (!strcmp(backend_name, "oss")) {
#if defined(USE_OSS)
init_oneshot = oss_init;
+#endif
+} else if (!strcmp(backend_name, "oss-fft")) {
+#if defined(USE_OSS_FFT)
+ init_oneshot = oss_fft_init;
#endif
} else if (!strcmp(backend_name, "aaudio")) {
#if defined(USE_AAUDIO)
@@ -237,6 +245,9 @@ cubeb_init(cubeb ** context, char const * context_name,
#if defined(USE_OSS)
oss_init,
#endif
+#if defined(USE_OSS_FFT)
+ oss_fft_init,
+#endif
#if defined(USE_AUDIOUNIT_RUST)
audiounit_rust_init,
#endif
@@ -690,6 +701,12 @@ log_device(cubeb_device_info * device_info)
case CUBEB_DEVICE_FMT_F32BE:
devdeffmt = "F32BE";
break;
+ case CUBEB_DEVICE_FMT_S32LE:
+ devdeffmt = "S32LE";
+ break;
+ case CUBEB_DEVICE_FMT_S32BE:
+ devdeffmt = "S32BE";
+ break;
default:
devdeffmt = "unknown?";
break;
@@ -707,6 +724,12 @@ log_device(cubeb_device_info * device_info)
if (device_info->format & CUBEB_DEVICE_FMT_F32BE) {
strcat(devfmts, " F32BE");
}
+ if (device_info->format & CUBEB_DEVICE_FMT_S32LE) {
+ strcat(devfmts, " S32LE");
+ }
+ if (device_info->format & CUBEB_DEVICE_FMT_S32BE) {
+ strcat(devfmts, " S32BE");
+ }
LOG("DeviceID: \"%s\"%s\n"
"\tName:\t\"%s\"\n"
diff --git a/media/libcubeb/test/test_devices.cpp b/media/libcubeb/test/test_devices.cpp
index d0eab2da67e6..4a8da1a4113b 100644
--- a/media/libcubeb/test/test_devices.cpp
+++ b/media/libcubeb/test/test_devices.cpp
@@ -79,6 +79,12 @@ print_device_info(cubeb_device_info * info, FILE * f)
case CUBEB_DEVICE_FMT_F32BE:
devdeffmt = "F32BE";
break;
+ case CUBEB_DEVICE_FMT_S32LE: // Add this
+ devdeffmt = "S32LE";
+ break;
+ case CUBEB_DEVICE_FMT_S32BE: // Add this
+ devdeffmt = "S32BE";
+ break;
default:
devdeffmt = "unknown?";
break;
@@ -92,6 +98,10 @@ print_device_info(cubeb_device_info * info, FILE * f)
strcat(devfmts, " F32LE");
if (info->format & CUBEB_DEVICE_FMT_F32BE)
strcat(devfmts, " F32BE");
+ if (info->format & CUBEB_DEVICE_FMT_S32LE) // Add this
+ strcat(devfmts, " S32LE");
+ if (info->format & CUBEB_DEVICE_FMT_S32BE) // Add this
+ strcat(devfmts, " S32BE");
fprintf(f,
"dev: \"%s\"%s\n"
--
2.39.5EXAMPLE: Firefox about:support
$ firefox-dev about:support
...
Name Firefox Developer Edition
Version 154.0a1
...
Update Channel aurora
User Agent Mozilla/5.0 (X11; Linux x86_64; rv:154.0) Gecko/20100101 Firefox/154.0
...
Application Binary /usr/lib/firefox-dev/firefox-dev
Profile Directory $HOME/.firefox-dev/uf30avtz.dev-edition-default
...
Audio Backend: alsa
Max Channels: 64
Preferred Sample Rate: 44100
Roundtrip latency (standard deviation): NaNms (NaN)
...
Output Devices
Name: Group, Vendor, State, Preferred, Format, Channels, Rate, Latency
Discard all samples (playback) or generate zero samples (capture): null, Enabled, None, default: S32LE, support: S16LE S16BE F32LE F32BE S32LE S32BE, 1073741823, default: 44100, support: 1 - 4294967295, 0 - 0
Rate Converter Plugin Using Libav/FFmpeg Library: lavrate, Enabled, None, default: S32LE, support: S16LE S16BE S32LE S32BE, 10000, default: 44100, support: 1 - 4294967295, 0 - 0
Rate Converter Plugin Using Samplerate Library: samplerate, Enabled, None, default: S32LE, support: S16LE S16BE S32LE S32BE, 10000, default: 44100, support: 1 - 4294967295, 0 - 0
Rate Converter Plugin Using Speex Resampler: speexrate, Enabled, None, default: S32LE, support: S16LE S16BE S32LE S32BE, 10000, default: 44100, support: 1 - 4294967295, 0 - 0
Bluetooth Audio: bluealsa, Enabled, None, default: S32LE, support: S16LE S16BE F32LE F32BE S32LE S32BE, 10000, default: 44100, support: 4000 - 4294967295, 0 - 0
Plugin using Speex DSP (resample, agc, denoise, echo, dereverb): speex, Enabled, None, default: S16LE, support: S16LE, 1, default: 44100, support: 4000 - 4294967295, 0 - 0
Plugin for channel upmix (4,6,8): upmix, Enabled, None, default: S16LE, support: S16LE, 8, default: 44100, support: 4000 - 4294967295, 0 - 0
Plugin for channel downmix (stereo) with a simple spacialization: vdownmix, Enabled, None, default: S16LE, support: S16LE, 6, default: 44100, support: 4000 - 4294967295, 0 - 0
Default device: default, Enabled, None, default: S32LE, support: S16LE S16BE F32LE F32BE S32LE S32BE, 10000, default: 44100, support: 4000 - 4294967295, 0 - 0
HDA Intel PCH, 92HD95 Analog Direct hardware device without any conversions: hw:CARD=PCH,DEV=0, Enabled, None, default: S32LE, support: S16LE S32LE, 2, default: 44100, support: 44100 - 192000, 0 - 0
HDA Intel PCH, HDMI 0 Direct hardware device without any conversions: hw:CARD=PCH,DEV=3, Enabled, None, default: S32LE, support: S16LE S32LE, 8, default: 44100, support: 32000 - 192000, 0 - 0
HDA Intel PCH, HDMI 1 Direct hardware device without any conversions: hw:CARD=PCH,DEV=7, Enabled, None, default: S32LE, support: S16LE S32LE, 8, default: 44100, support: 32000 - 192000, 0 - 0
HDA Intel PCH, HDMI 2 Direct hardware device without any conversions: hw:CARD=PCH,DEV=8, Enabled, None, default: S32LE, support: S16LE S32LE, 8, default: 44100, support: 32000 - 192000, 0 - 0
HDA Intel PCH, HDMI 3 Direct hardware device without any conversions: hw:CARD=PCH,DEV=9, Enabled, None, default: S32LE, support: S16LE S32LE, 8, default: 44100, support: 32000 - 192000, 0 - 0
HDA Intel PCH, 92HD95 Analog Hardware device with all software conversions: plughw:CARD=PCH,DEV=0, Enabled, None, default: S32LE, support: S16LE S16BE F32LE F32BE S32LE S32BE, 10000, default: 44100, support: 4000 - 4294967295, 0 - 0
HDA Intel PCH, HDMI 0 Hardware device with all software conversions: plughw:CARD=PCH,DEV=3, Enabled, None, default: S32LE, support: S16LE S16BE F32LE F32BE S32LE S32BE, 10000, default: 44100, support: 4000 - 4294967295, 0 - 0
HDA Intel PCH, HDMI 1 Hardware device with all software conversions: plughw:CARD=PCH,DEV=7, Enabled, None, default: S32LE, support: S16LE S16BE F32LE F32BE S32LE S32BE, 10000, default: 44100, support: 4000 - 4294967295, 0 - 0
HDA Intel PCH, HDMI 2 Hardware device with all software conversions: plughw:CARD=PCH,DEV=8, Enabled, None, default: S32LE, support: S16LE S16BE F32LE F32BE S32LE S32BE, 10000, default: 44100, support: 4000 - 4294967295, 0 - 0
HDA Intel PCH, HDMI 3 Hardware device with all software conversions: plughw:CARD=PCH,DEV=9, Enabled, None, default: S32LE, support: S16LE S16BE F32LE F32BE S32LE S32BE, 10000, default: 44100, support: 4000 - 4294967295, 0 - 0
HDA Intel PCH, 92HD95 Analog Default Audio Device: sysdefault:CARD=PCH, Enabled, None, default: S32LE, support: S16LE S16BE F32LE F32BE S32LE S32BE, 10000, default: 44100, support: 4000 - 4294967295, 0 - 0
HDA Intel PCH, 92HD95 Analog Front output / input: front:CARD=PCH,DEV=0, Enabled, None, default: S32LE, support: S16LE S32LE, 2, default: 44100, support: 44100 - 192000, 0 - 0
HDA Intel PCH, 92HD95 Analog 4.0 Surround output to Front and Rear speakers: surround40:CARD=PCH,DEV=0, Enabled, None, default: S32LE, support: S16LE S32LE, 2, default: 44100, support: 44100 - 192000, 0 - 0
HDA Intel PCH, 92HD95 Analog 5.1 Surround output to Front, Center, Rear and Subwoofer speakers: surround51:CARD=PCH,DEV=0, Enabled, None, default: S32LE, support: S16LE S32LE, 2, default: 44100, support: 44100 - 192000, 0 - 0
HDA Intel PCH, 92HD95 Analog 7.1 Surround output to Front, Center, Side, Rear and Woofer speakers: surround71:CARD=PCH,DEV=0, Enabled, None, default: S32LE, support: S16LE S32LE, 2, default: 44100, support: 44100 - 192000, 0 - 0
HDA Intel PCH, HDMI 0 HDMI Audio Output: hdmi:CARD=PCH,DEV=0, Enabled, None, default: S32LE, support: S16LE S32LE, 8, default: 44100, support: 32000 - 192000, 0 - 0
HDA Intel PCH, HDMI 1 HDMI Audio Output: hdmi:CARD=PCH,DEV=1, Enabled, None, default: S32LE, support: S16LE S32LE, 8, default: 44100, support: 32000 - 192000, 0 - 0
HDA Intel PCH, HDMI 2 HDMI Audio Output: hdmi:CARD=PCH,DEV=2, Enabled, None, default: S32LE, support: S16LE S32LE, 8, default: 44100, support: 32000 - 192000, 0 - 0
HDA Intel PCH, HDMI 3 HDMI Audio Output: hdmi:CARD=PCH,DEV=3, Enabled, None, default: S32LE, support: S16LE S32LE, 8, default: 44100, support: 32000 - 192000, 0 - 0
HDA Intel PCH, 92HD95 Analog Direct sample mixing device: dmix:CARD=PCH,DEV=0, Enabled, None, default: S32LE, support: S32LE, 2, default: 48000, support: 48000 - 48000, 0 - 0
HDA Intel PCH, HDMI 0 Direct sample mixing device: dmix:CARD=PCH,DEV=3, Enabled, None, default: S32LE, support: S32LE, 2, default: 48000, support: 48000 - 48000, 0 - 0
HDA Intel PCH, HDMI 1 Direct sample mixing device: dmix:CARD=PCH,DEV=7, Enabled, None, default: S32LE, support: S32LE, 2, default: 48000, support: 48000 - 48000, 0 - 0
HDA Intel PCH, HDMI 2 Direct sample mixing device: dmix:CARD=PCH,DEV=8, Enabled, None, default: S32LE, support: S32LE, 2, default: 48000, support: 48000 - 48000, 0 - 0
HDA Intel PCH, HDMI 3 Direct sample mixing device: dmix:CARD=PCH,DEV=9, Enabled, None, default: S32LE, support: S32LE, 2, default: 48000, support: 48000 - 48000, 0 - 0
Input Devices
Name: Group, Vendor, State, Preferred, Format, Channels, Rate, Latency
Discard all samples (playback) or generate zero samples (capture): null, Enabled, None, default: S32LE, support: S16LE S16BE F32LE F32BE S32LE S32BE, 1073741823, default: 44100, support: 1 - 4294967295, 0 - 0
Rate Converter Plugin Using Libav/FFmpeg Library: lavrate, Enabled, None, default: S32LE, support: S16LE S16BE S32LE S32BE, 10000, default: 44100, support: 1 - 4294967295, 0 - 0
Rate Converter Plugin Using Samplerate Library: samplerate, Enabled, None, default: S32LE, support: S16LE S16BE S32LE S32BE, 10000, default: 44100, support: 1 - 4294967295, 0 - 0
Rate Converter Plugin Using Speex Resampler: speexrate, Enabled, None, default: S32LE, support: S16LE S16BE S32LE S32BE, 10000, default: 44100, support: 1 - 4294967295, 0 - 0
Plugin using Speex DSP (resample, agc, denoise, echo, dereverb): speex, Enabled, None, default: S16LE, support: S16LE, 1, default: 44100, support: 4000 - 4294967295, 0 - 0
Plugin for channel upmix (4,6,8): upmix, Enabled, None, default: S16LE, support: S16LE, 8, default: 44100, support: 4000 - 4294967295, 0 - 0
Plugin for channel downmix (stereo) with a simple spacialization: vdownmix, Enabled, None, default: S16LE, support: S16LE, 6, default: 44100, support: 4000 - 4294967295, 0 - 0
Default device: default, Enabled, None, default: S32LE, support: S16LE S16BE F32LE F32BE S32LE S32BE, 10000, default: 44100, support: 4000 - 4294967295, 0 - 0
HDA Intel PCH, 92HD95 Analog Direct hardware device without any conversions: hw:CARD=PCH,DEV=0, Enabled, None, default: S32LE, support: S16LE S32LE, 2, default: 44100, support: 44100 - 192000, 0 - 0
HDA Intel PCH, 92HD95 Analog Hardware device with all software conversions: plughw:CARD=PCH,DEV=0, Enabled, None, default: S32LE, support: S16LE S16BE F32LE F32BE S32LE S32BE, 10000, default: 44100, support: 4000 - 4294967295, 0 - 0
HDA Intel PCH, 92HD95 Analog Default Audio Device: sysdefault:CARD=PCH, Enabled, None, default: S32LE, support: S16LE S16BE F32LE F32BE S32LE S32BE, 10000, default: 44100, support: 4000 - 4294967295, 0 - 0
HDA Intel PCH, 92HD95 Analog Front output / input: front:CARD=PCH,DEV=0, Enabled, None, default: S32LE, support: S16LE S32LE, 2, default: 44100, support: 44100 - 192000, 0 - 0
HDA Intel PCH, 92HD95 Analog Direct sample snooping device: dsnoop:CARD=PCH,DEV=0, Enabled, None, default: S32LE, support: S32LE, 2, default: 48000, support: 48000 - 48000, 0 - 0NOTE: ALSA device enumeration is now exact, but ALSA drives might be buggy. Therefore, "digital" may mean "analog" and other way round. The word "virtual" usually implies linear interpolation or other sort of low quality resampling.
1. Device enumeration is exact - The ALSA library correctly enumerates available devices through the control interface and name hints control.
2. Driver descriptions can be buggy - Generic card configurations like HDA-Intel.conf make assumptions about device mappings that may not match actual hardware pin configurations.
3. "Virtual" and resampling quality - Virtual devices (using the plug plugin) perform conversions, but the quality depends on the configured rate converter. The default is often linear interpolation, but high-quality converters like fftrate can be used.
Virtual devices are software plugins that wrap hardware access. They always perform some conversion (format, rate, or channels), but the quality varies:
Default: often linear interpolation (lower quality)
Configurable: can use high-quality resamplers like fftrate
Hardware devices (hw:) provide direct kernel driver access without any conversions.
The "digital"/"analog" labels come from driver descriptions, which may be incorrect due to generic configuration assumptions not matching your specific motherboard's pin layout.
The word "direct" is also a sort of label that has a flexible meaning: "direct exclusive kernel driver access" is not possible with ALSA in any sense.
The ALSA hw plugin does communicate directly with the kernel driver through device files and ioctl calls, but this is still mediated by the ALSA library (libasound) in userspace. The snd_pcm_hw_open function opens kernel device files like /dev/snd/pcmC%iD%ip and uses ioctl calls to communicate with the kernel driver.
When ALSA documentation says the hw plugin provides "direct communication without any conversions," it means:
No software format conversion
No software rate conversion
No software channel routing
Direct ioctl calls to kernel device files
But it does NOT mean:
Bypassing the ALSA library
Exclusive hardware access (other processes can still access the device)
Bypassing potential kernel driver bugs
The ALSA path with hw:0,0 is:
Application → ALSA Library (libasound) → hw plugin → ioctl → Kernel Driver → Hardware The hw plugin is the closest you can get to the kernel driver while still using the ALSA API, but it's still going through the library layer.
There is no way to access the ALSA kernel driver directly from userspace without going through libasound. The "direct" terminology in ALSA documentation refers to the absence of software processing plugins, not to bypassing the library itself.
See also:
The exclusive mode of ALSA
_https://dev1galaxy.org/viewtopic.php?id=8053
In practical terms, the choice is indeed between low-quality hardware resampling and software resampling.
The fundamental tradeoff in ALSA audio processing:
Hardware resampling - Built into consumer sound cards/codecs, often poor quality due to cost-cutting and implementation limitations.
Software resampling - Performed by ALSA's rate plugin, quality depends on the converter used.
ALSA's plug plugin automatically inserts rate conversion when the application's requested rate differs from the hardware's native rate. The rate plugin supports multiple converters specified via the converter parameter.
The default linear converter provides basic linear interpolation (low quality), while high-quality external converters like fftrate can be used through the External Rate-Converter-Plugin SDK.
The optimal strategy — using high-quality software resampling (fftrate) to bypass hardware resamplers by sending a fixed high-rate stream to the hardware — effectively avoids the quality limitations of consumer hardware SRC implementations.
NOTE: This guide is a work in progress. Please do not post in this topic. If you have any questions, please consult the Devuan Wiki, or other knowledge repositories of the sort.
about:support
Name Firefox Developer Edition
Version 154.0a1
Update Channel aurora
User Agent Mozilla/5.0 (X11; Linux x86_64; rv:154.0) Gecko/20100101 Firefox/154.0
Profile Directory ~/.firefox-dev/qxls80vg.dev-edition-default-1If User Agent is not set correctly with:
imply_option("MOZ_APP_UA_NAME", "Firefox")You can set it with override in about:config
general.useragent.override Mozilla/5.0 (X11; Linux x86_64; rv:154.0) Gecko/20100101 Firefox/154.0 imply_option("MOZ_APP_PROFILE", "firefox-dev")MOZ_APP_PROFILE=firefox-dev means User Profile Directory
$HOME/.firefox-devfor user settings.
firefox/browser/moz.configure is a secret configuration file.
does the firefox develper auto-update?
Yes, it does (by default). You can disable/enable this feature is about:config, or disable and lock in a secret config.
$ cat /etc/firefox-dev/defaults/pref/firefox-dev.cfg3-2.js
// Debian system-wide preferences for Firefox Developer Edition
// This file is intended for system administrators to configure default settings.
// Use 'pref("preference.name", value);' for defaults.
// Use 'pref("preference.name", value, locked);' to enforce immutable settings.
// Enable Remote Improvements - Nimbus rollouts system
pref("nimbus.rollouts.enabled", true);
pref("services.sync.prefs.sync.nimbus.rollouts.enabled", false, locked);
// Enable automatic updates
pref("app.update.auto", true);
pref("app.update.enabled", true);
pref("app.update.background.enabled", true);
// Disable Remote Improvements - Nimbus rollouts system
// pref("nimbus.rollouts.enabled", false, locked);
// pref("services.sync.prefs.sync.nimbus.rollouts.enabled", false, locked);
// Disable automatic updates
// pref("app.update.auto", false, locked);
// pref("app.update.enabled", false, locked);
// pref("app.update.background.enabled", false, locked); With plughw:0,0 or default, the audio path is:
Application → ALSA Library → Plugin Layer → Kernel PCM Core → Hardware Driver → Hardware The ALSA path with hw:0,0 is:
Application → ALSA Library → Kernel PCM Core → Hardware Driver → Hardware When using hw:0,0, the ALSA library bypasses the plugin layer (software format/rate/channel conversion) but still goes through the kernel PCM core for buffer management, period handling, and synchronization before reaching the hardware driver.
Usually, "exclusive mode" refers to exclusive device access control by one applications (this is what the word exclusive is supposed to mean). Such exclusive device access prevents multiple applications from simultaneously using the same hardware resource. In the Linux kernel's technical documentation, "exclusive" has a specific meaning: preventing multiple applications from simultaneously using the same hardware resource.
ALSA permits "exclusive full duplex":
$ arecord -f cd -D hw:0,0 | aplay -V mono -D hw:0,0
Recording WAVE 'stdin' : Signed 16 bit Little Endian, Rate 44100 Hz, Stereo
Playing WAVE 'stdin' : Signed 16 bit Little Endian, Rate 44100 Hz, Stereo
##################### + | 46%
$ arecord -f cd -D hw:1,0 | aplay -V mono -D hw:1,0
Recording WAVE 'stdin' : Signed 16 bit Little Endian, Rate 44100 Hz, Stereo
Playing WAVE 'stdin' : Signed 16 bit Little Endian, Rate 44100 Hz, Stereo
##################################################+| MAX
$ arecord -f cd -D hw:PCH,0 | aplay -V mono -D hw:PCH,0
Recording WAVE 'stdin' : Signed 16 bit Little Endian, Rate 44100 Hz, Stereo
Playing WAVE 'stdin' : Signed 16 bit Little Endian, Rate 44100 Hz, Stereo
#################### + | 97% arecord opens hw:0,0 for capture
aplay opens hw:0,0 for playback
Both processes have the the same device open simultaneously in "exclusive mode".
The active meter indicates that the full duplex works and audio data is actively flowing through the pipeline in real-time.
Thus, two applications, arecord and aplay, have exclusive access to the same device simultaneously. It works with Intel HDA codecs, USB soundcards, etc.
OSS4 with software mixer vmix disabled does not support "exclusive full duplex" for Intel HDA codecs
$ ./fulldup
Using audio engine 0=HD Audio play front for duplex
/dev/dsp doesn't support one device based full duplex scheme
Please use the two device scheme.
$ file /dev/dsp
/dev/dsp: symbolic link to /dev/oss/oss_hdaudio0/pcm0 ALSA is more advanced in this sense. In advanced Monty Python reality, the word exclusive has a flexible meaning. It may mean anything you want.
Patch 1 & 2
Sourcedir: firefox
FILE: firefox/browser/moz.configure
imply_option("MOZ_DEVTOOLS", "all")
imply_option("BROWSER_CHROME_URL", "chrome://browser/content/browser.xhtml")
+imply_option("MOZ_APP_UA_NAME", "Firefox")
+imply_option("MOZ_APP_PROFILE", "firefox-dev") You should simply add two lines:
imply_option("MOZ_APP_UA_NAME", "Firefox")
imply_option("MOZ_APP_PROFILE", "firefox-dev")after other "imply_option" lines.
$ cat firefox/browser/moz.configure
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
imply_option("MOZ_PLACES", True)
imply_option("MOZ_SERVICES_HEALTHREPORT", True)
imply_option("MOZ_SERVICES_SYNC", True)
imply_option("MOZ_DEDICATED_PROFILES", True)
imply_option("MOZ_BLOCK_PROFILE_DOWNGRADE", True)
imply_option("MOZ_NORMANDY", True)
imply_option("MOZ_PROFILE_MIGRATOR", True)
imply_option("MOZ_APP_VENDOR", "Mozilla")
imply_option("MOZ_APP_ID", "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}")
# Include the DevTools client, not just the server (which is the default)
imply_option("MOZ_DEVTOOLS", "all")
imply_option("BROWSER_CHROME_URL", "chrome://browser/content/browser.xhtml")
imply_option("MOZ_APP_UA_NAME", "Firefox")
imply_option("MOZ_APP_PROFILE", "firefox-dev")
...It works with Zoom.
Package Details: libasound2-plugin-fftrate 1.6.3-1
_https://aur.archlinux.org/packages/libasound2-plugin-fftrate
The Arch Linux patch for fftrate (gcc-12+):
prepare() {
cd "$srcdir/$pkgbase-$pkgver"
find . -type f \( -name "*.cpp" -o -name "*.c" -o -name "*.h" \) -exec sed -i -e 's/min(/MIN(/g' -e 's/max(/MAX(/g' {} +
} It does fix the problem, but creates a new one: a sort of inconsistency.
For example:
The upstream: PetrovSE fftrate/src/lib/mathmac.h
//================================================================
// Min & max computing
//----------------------------------------------------------------
#ifndef min
#define min( _x, _y ) ( (_x) < (_y) ? (_x) : (_y) )
#endif
#ifndef max
#define max( _x, _y ) ( (_x) > (_y) ? (_x) : (_y) )
#endifPatched file: src/lib/mathmac.h
//================================================================
// Min & max computing
//----------------------------------------------------------------
#ifndef min
#define MIN( _x, _y ) ( (_x) < (_y) ? (_x) : (_y) )
#endif
#ifndef max
#define MAX( _x, _y ) ( (_x) > (_y) ? (_x) : (_y) )
#endifIt should be:
//================================================================
// Min & max computing
//----------------------------------------------------------------
#ifndef MIN
#define MIN( _x, _y ) ( (_x) < (_y) ? (_x) : (_y) )
#endif
#ifndef MAX
#define MAX( _x, _y ) ( (_x) > (_y) ? (_x) : (_y) )
#endifNotice that matlab files should not be patched,
only -name "*.cpp" -o -name "*.c" -o -name "*.h"
Bypassing the hardware resampler in Intel HDA codecs with a software real-time fftrate codec to resample everything to 192kHz 32-bit is a solid architectural strategy. It works at the userspace level on top of ALSA and avoids potential quality issues in codec hardware SRC implementations.
HDA Codec Resampling in the Kernel
The Linux kernel's HDA subsystem does have hardware sample rate converter (SRC) capabilities managed by codec drivers. For example, the Creative CA0132 driver documentation shows that HDA codecs have dedicated SRC ports (0x80–0xbf) for sample rate conversion within the codec's internal routing.
Kernel-Level Format Handling
The kernel handles PCM format conversion through the snd_hdac_stream_format() function, which converts channel count, sample format, and sample rate into the HDA format value used by the hardware.
Hardware-Specific Workarounds
Some codecs have hardware-specific sample rate limitations that require kernel-level workarounds. For instance, the ALC269 driver has a fixup for the Lenovo Ideapad that forces analog I/O to 44.1kHz due to a hardware problem.
The fftrate project
The software resampling approach of the fftrate project bypasses these kernel-level hardware resamplers entirely by:
1. Accepting audio at any rate from the application.
2. Resampling to 192kHz 32-bit in userspace.
3. Sending the already-resampled audio to ALSA at a fixed rate.
This avoids potential quality issues or bugs in the codec's hardware SRC implementation. The kernel ALSA driver simply sees a constant 192kHz 32-bit stream and passes it through without additional resampling.
Dialectical conclusion
There is a belief that fftrate may cure deafness, though it hasn't been verified by scientific methods. Give it a chance. It may (or may not) work.
NOTE: The claim that fftrate can "cure deafness" is a metaphorical exaggeration common in audiophile circles, not a medical fact. There is no scientific evidence that software resampling can restore hearing loss or cure physiological deafness.
Why should they accept a patch that has not been tested by ALSA users?
Updated the guide on building Firefox with the ALSA backend patch:
"Firefox: Dialectics of Antagonistic Security Bugs"
_https://dev1galaxy.org/viewtopic.php?id=8012
NOTE: The Debian package is created using the make_deb.sh script.
If the build is successful, you can test it with Zoom.