PortAudio / portaudio

PortAudio is a cross-platform, open-source C language library for real-time audio input and output.
Other
1.37k stars 286 forks source link

CoreAudio + PulseAudio support => no devices at all if the PulseAudio daemon isn't running #900

Open RJVB opened 2 months ago

RJVB commented 2 months ago

Describe the bug I patched the CMake file and built a Mac portaudio dylib supporting both the native CoreAudio plus PulseAudio, with the idea of being able to use either if and when I have the pulse daemon running (= when needed).

This works, but leaves me with no devices at all as soon as the pulse audio exits.

To Reproduce Steps to reproduce the behavior. Include code if applicable.

  1. Build and install on Mac with pulseaudio support. IIRC this is possible "as is" using the configure script; use the patch below under Additional Info to build with CMake:
  2. start the pulse daemon and run a dependent (or run pa_devs); observe it sees the CoreAudio devices and the devices provided through PulseAudio.
  3. kill the pulse daemon and repeat

Expected behaviour In step 3. I would expect to see the CoreAudio devices

Actual behaviour No devices are shown at all:

> pa_devs
ERROR: Pa_Initialize returned 0xffffd8f1
Error number: -9999
Error message: Unanticipated host error
Exit 241

Desktop (please complete the following information):

Additional context

diff --git CMakeLists.txt CMakeLists.txt
index 77e5388..3b5a86a 100644
--- CMakeLists.txt
+++ CMakeLists.txt
@@ -373,9 +373,20 @@ elseif(UNIX)
       set(PKGCONFIG_CFLAGS "${PKGCONFIG_CFLAGS} -DPA_USE_AUDIOIO=1")
     endif()

-    find_package(PulseAudio)
-    cmake_dependent_option(PA_USE_PULSEAUDIO "Enable support for PulseAudio general purpose sound server" ON PulseAudio_FOUND OFF)
+    pkg_check_modules(SNDIO sndio)
+    cmake_dependent_option(PA_USE_SNDIO "Enable support for sndio" ON SNDIO_FOUND OFF)
+    if(PA_USE_SNDIO)
+      target_link_libraries(PortAudio PRIVATE "${SNDIO_LIBRARIES}")
+      target_sources(PortAudio PRIVATE src/hostapi/sndio/pa_sndio.c)
+      target_compile_definitions(PortAudio PUBLIC PA_USE_SNDIO=1)
+      set(PKGCONFIG_CFLAGS "${PKGCONFIG_CFLAGS} -DPA_USE_SNDIO=1")
+      set(PKGCONFIG_REQUIRES_PRIVATE "${PKGCONFIG_REQUIRES_PRIVATE} sndio")
+    endif()
+  endif()
+endif()
+    option(PA_USE_PULSEAUDIO "Enable support for PulseAudio general purpose sound server" OFF)
     if(PA_USE_PULSEAUDIO)
+      find_package(PulseAudio REQUIRED)
       target_link_libraries(PortAudio PRIVATE PulseAudio::PulseAudio)
       target_sources(PortAudio PRIVATE
         src/hostapi/pulseaudio/pa_linux_pulseaudio_block.c
@@ -383,24 +394,15 @@ elseif(UNIX)
         src/hostapi/pulseaudio/pa_linux_pulseaudio_cb.c)

       target_compile_definitions(PortAudio PUBLIC PA_USE_PULSEAUDIO=1)
-      set(PKGCONFIG_CFLAGS "${PKGCONFIG_CFLAGS} -DPA_USE_PULSEAUDIO=1")
-      set(PKGCONFIG_REQUIRES_PRIVATE "${PKGCONFIG_REQUIRES_PRIVATE} libpulse")
+    if (NOT APPLE)
+        set(PKGCONFIG_CFLAGS "${PKGCONFIG_CFLAGS} -DPA_USE_PULSEAUDIO=1")
+        set(PKGCONFIG_REQUIRES_PRIVATE "${PKGCONFIG_REQUIRES_PRIVATE} libpulse")
+      endif()

       # needed for PortAudioConfig.cmake so `find_package(PortAudio)` works in downstream projects
       install(FILES cmake/modules/FindPulseAudio.cmake DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/portaudio/modules")
     endif()

-    pkg_check_modules(SNDIO sndio)
-    cmake_dependent_option(PA_USE_SNDIO "Enable support for sndio" ON SNDIO_FOUND OFF)
-    if(PA_USE_SNDIO)
-      target_link_libraries(PortAudio PRIVATE "${SNDIO_LIBRARIES}")
-      target_sources(PortAudio PRIVATE src/hostapi/sndio/pa_sndio.c)
-      target_compile_definitions(PortAudio PUBLIC PA_USE_SNDIO=1)
-      set(PKGCONFIG_CFLAGS "${PKGCONFIG_CFLAGS} -DPA_USE_SNDIO=1")
-      set(PKGCONFIG_REQUIRES_PRIVATE "${PKGCONFIG_REQUIRES_PRIVATE} sndio")
-    endif()
-  endif()
-endif()

 # Make sure PA_USE_ALSA is available as it is used for PortAudioConfig.cmake configuration
 if (NOT PA_USE_ALSA)
@@ -461,6 +463,7 @@ set_target_properties(PortAudio PROPERTIES
   WINDOWS_EXPORT_ALL_SYMBOLS TRUE
   VERSION ${PROJECT_VERSION}
   SOVERSION 2
+  MACHO_COMPATIBILITY_VERSION 3
 )
 install(TARGETS PortAudio
   EXPORT PortAudio-targets

At first glance this seems to be cause InitializeHostApis() bails when it encounters an error on one of the supported APIs, rather than just skipping it. I'm going to explore that idea.

RJVB commented 2 months ago

This patch seems to work; dependent applications will get the choice of the audio devices accessible through those of the configured APIs that initialised correctly.

For instance, on Linux, applications using this patched portaudio can provide or record sound even if the pulseaudio daemon was (temporarily) killed.

diff --git src/common/pa_front.c src/common/pa_front.c
index 9f81f26..aad1917 100644
--- src/common/pa_front.c
+++ src/common/pa_front.c
@@ -222,47 +222,58 @@ static PaError InitializeHostApis( void )
         PA_DEBUG(( "before paHostApiInitializers[%d].\n",i));

         result = paHostApiInitializers[i]( &hostApis_[hostApisCount_], hostApisCount_ );
-        if( result != paNoError )
-            goto error;
+        if( result == paNoError ) {

-        PA_DEBUG(( "after paHostApiInitializers[%d].\n",i));
+            PA_DEBUG(( "after paHostApiInitializers[%d].\n",i));

-        if( hostApis_[hostApisCount_] )
-        {
-            PaUtilHostApiRepresentation* hostApi = hostApis_[hostApisCount_];
-            assert( hostApi->info.defaultInputDevice < hostApi->info.deviceCount );
-            assert( hostApi->info.defaultOutputDevice < hostApi->info.deviceCount );
-
-            /* the first successfully initialized host API with a default input *or*
-               output device is used as the default host API.
-            */
-            if( (defaultHostApiIndex_ == -1) &&
-                    ( hostApi->info.defaultInputDevice != paNoDevice
-                        || hostApi->info.defaultOutputDevice != paNoDevice ) )
+            if( hostApis_[hostApisCount_] )
             {
-                defaultHostApiIndex_ = hostApisCount_;
-            }
+                PaUtilHostApiRepresentation* hostApi = hostApis_[hostApisCount_];
+                assert( hostApi->info.defaultInputDevice < hostApi->info.deviceCount );
+                assert( hostApi->info.defaultOutputDevice < hostApi->info.deviceCount );
+
+                /* the first successfully initialized host API with a default input *or*
+                   output device is used as the default host API.
+                */
+                if( (defaultHostApiIndex_ == -1) &&
+                        ( hostApi->info.defaultInputDevice != paNoDevice
+                            || hostApi->info.defaultOutputDevice != paNoDevice ) )
+                {
+                    defaultHostApiIndex_ = hostApisCount_;
+                }

-            hostApi->privatePaFrontInfo.baseDeviceIndex = baseDeviceIndex;
+                hostApi->privatePaFrontInfo.baseDeviceIndex = baseDeviceIndex;

-            if( hostApi->info.defaultInputDevice != paNoDevice )
-                hostApi->info.defaultInputDevice += baseDeviceIndex;
+                if( hostApi->info.defaultInputDevice != paNoDevice )
+                    hostApi->info.defaultInputDevice += baseDeviceIndex;

-            if( hostApi->info.defaultOutputDevice != paNoDevice )
-                hostApi->info.defaultOutputDevice += baseDeviceIndex;
+                if( hostApi->info.defaultOutputDevice != paNoDevice )
+                    hostApi->info.defaultOutputDevice += baseDeviceIndex;

-            baseDeviceIndex += hostApi->info.deviceCount;
-            deviceCount_ += hostApi->info.deviceCount;
+                baseDeviceIndex += hostApi->info.deviceCount;
+                deviceCount_ += hostApi->info.deviceCount;

-            ++hostApisCount_;
+                ++hostApisCount_;
+            }
+        }
+        else
+        {
+            PA_DEBUG(( "paHostApiInitializers[%d] failed.\n",i));
         }
     }

-    /* if no host APIs have devices, the default host API is the first initialized host API */
-    if( defaultHostApiIndex_ == -1 )
-        defaultHostApiIndex_ = 0;
+    if( hostApisCount_ )
+    {
+        /* if no host APIs have devices, the default host API is the first initialized host API */
+        if( defaultHostApiIndex_ == -1 )
+            defaultHostApiIndex_ = 0;

-    return result;
+        return paNoError;
+    }
+    else
+    {
+        PA_DEBUG(( "All paHostApiInitializers failed!\n",i));
+    }

 error:
     TerminateHostApis();
illuusio commented 2 months ago

I didn't know that Pulseaudio could be used in Mac OS. Patch is bit polluted as it moves place of sndio also which is not the topic. Why there is NOT APPLE which makes impossible build it or is this just testing patch?

RJVB commented 2 months ago

I didn't know that Pulseaudio could be used in Mac OS.

Judging from the code it also runs on MSWin. I don't know how many projects really need PulseAudio but if enough of them are also ported to the other big 2 platforms it makes sense that PulseAudio runs there too. For me one reasons to run it is that it allows me to play remote audio on my Mac; I would guess that this would work on MSWin too.

And that's also the reason why I filed this ticket: I want to be able to use my regular applications that access CoreAudio devices through PortAudio (QMPlay2, Audacity, ...) regardless of whether I have the PulseAudio daemon running, and without having to activate a different build variant of the PortAudio port (that's a MacPorts feature).

Patch is bit polluted as it moves place of sndio also which is not the topic.

You'd guess that from looking at the patch, but it's just the impression you get because I moved the pulseaudio bit below the platform specific code. And maybe because I changed the logic a bit to do find_package(pulseaudio) only when the user requests it. That makes interpreting the cmake output more straightforward (no more "whaaat, I didn't ask for PulseAudio support to be built in" ;) )

Why there is NOT APPLE which makes impossible build it or is this just testing patch?

Overall this is indeed a patch for my own use that I only attached to show how I obtained PulseAudio support, but I think there's an additional issue here that I didn't want to address properly for now. I may have overlooked something, but there doesn't appear to be a reason to add -DPA_USE_PULSEAUDIO to the CFLAGS of dependent projects, certainly not to recompile them in entirety (with an invalidated ccache!) if you reinstall portaudio with or without PulseAudio support. If dependents need to be able to check this token (or others) it would be cleaner to put them in a dedicated headerfile which can be included when required. That way only those source files need recompilation.

Libpulse is linked as a private dependency of libportaudio; on Mac ("Darwin"), there is no need to add these libraries to the link command of dependents.

So for me, on my local set-up, the easiest solution for these 2 issues was not to modify the pkgconfig file.