diff --git a/CMakeLists.txt b/CMakeLists.txt index fad8e6abb..fff8af999 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -365,6 +365,24 @@ elseif(UNIX) target_compile_definitions(PortAudio PUBLIC PA_USE_AUDIOIO=1) 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) + if(PA_USE_PULSEAUDIO) + target_link_libraries(PortAudio PRIVATE PulseAudio::PulseAudio) + target_sources(PortAudio PRIVATE + src/hostapi/pulseaudio/pa_linux_pulseaudio_block.c + src/hostapi/pulseaudio/pa_linux_pulseaudio.c + 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") + + # 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() + endif() endif() diff --git a/Makefile.in b/Makefile.in index ef5f2d857..05597cc23 100644 --- a/Makefile.in +++ b/Makefile.in @@ -44,7 +44,7 @@ PALIB = libportaudio.la PAINC = include/portaudio.h PA_LDFLAGS = $(LDFLAGS) $(SHARED_FLAGS) -rpath $(libdir) -no-undefined \ - -export-symbols-regex "(Pa|PaMacCore|PaJack|PaAlsa|PaAsio|PaOSS|PaWasapi|PaWasapiWinrt|PaWinMME)_.*" \ + -export-symbols-regex "(Pa|PaMacCore|PaPulseAudio|PaJack|PaAlsa|PaAsio|PaOSS|PaWasapi|PaWasapiWinrt|PaWinMME)_.*" \ -version-info $(LT_CURRENT):$(LT_REVISION):$(LT_AGE) COMMON_OBJS = \ @@ -66,7 +66,7 @@ LOOPBACK_OBJS = \ qa/loopback/src/test_audio_analyzer.o \ qa/loopback/src/write_wav.o \ qa/loopback/src/paqa.o - + EXAMPLES = \ bin/pa_devs \ bin/pa_fuzz \ @@ -82,7 +82,7 @@ SELFTESTS = \ bin/paqa_devs \ bin/paqa_errs \ bin/paqa_latency - + TESTS = \ bin/patest1 \ bin/patest_buffer \ @@ -146,6 +146,7 @@ SRC_DIRS = \ src/hostapi/coreaudio \ src/hostapi/dsound \ src/hostapi/jack \ + src/hostapi/pulseaudio \ src/hostapi/oss \ src/hostapi/skeleton \ src/hostapi/wasapi \ diff --git a/README.md b/README.md index cb33144cc..c280f7089 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ Please feel free to join. See http://www.portaudio.com for details. src/hostapi/dsound = Windows Direct Sound src/hostapi/jack = JACK Audio Connection Kit src/hostapi/oss = Unix Open Sound System (OSS) + src/hostapi/pulseaudio = Sound system for POSIX OSes src/hostapi/wasapi = Windows Vista WASAPI src/hostapi/wdmks = Windows WDM Kernel Streaming src/hostapi/wmme = Windows MultiMedia Extensions (MME) diff --git a/cmake/modules/FindPulseAudio.cmake b/cmake/modules/FindPulseAudio.cmake new file mode 100644 index 000000000..234692c43 --- /dev/null +++ b/cmake/modules/FindPulseAudio.cmake @@ -0,0 +1,147 @@ +# Copyright 2008 Matthias Kretz +# Copyright 2009 Marcus Hufgard +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# SPDX-FileCopyrightText: 2008 Matthias Kretz +# SPDX-FileCopyrightText: 2009 Marcus Hufgard +# +# SPDX-License-Identifier: BSD-3-Clause + +#.rst: +# FindPulseAudio +# -------------- +# +# This is base on +# https://invent.kde.org/frameworks/extra-cmake-modules/-/blob/master/find-modules/FindPulseAudio.cmake +# +# Try to locate the PulseAudio library. +# If found, this will define the following variables: +# +# ``PulseAudio_FOUND`` +# True if the system has the PulseAudio library of at least +# the minimum version specified by either the version parameter +# to find_package() or the variable PulseAudio_MINIMUM_VERSION +# ``PulseAudio_INCLUDE_DIRS`` +# The PulseAudio include directory +# ``PulseAudio_LIBRARIES`` +# The PulseAudio libraries for linking +# ``PulseAudio_MAINLOOP_LIBRARY`` +# The libraries needed to use PulseAudio Mainloop +# ``PulseAudio_VERSION`` +# The version of PulseAudio that was found +# ``PulseAudio_INCLUDE_DIR`` +# Deprecated, use ``PulseAudio_INCLUDE_DIRS`` +# ``PulseAudio_LIBRARY`` +# Deprecated, use ``PulseAudio_LIBRARIES`` +# +# If ``PulseAudio_FOUND`` is TRUE, it will also define the following +# imported target: +# +# ``PulseAudio::PulseAudio`` +# The PulseAudio library +# +# Since 5.41.0. + +# Support PulseAudio_MINIMUM_VERSION for compatibility: +if(NOT PulseAudio_FIND_VERSION) + set(PulseAudio_FIND_VERSION "${PulseAudio_MINIMUM_VERSION}") +endif() + +# the minimum version of PulseAudio we require +if(NOT PulseAudio_FIND_VERSION) + set(PulseAudio_FIND_VERSION "1.0.0") +endif() + +find_package(PkgConfig) +pkg_check_modules(PC_PulseAudio QUIET libpulse>=${PulseAudio_FIND_VERSION}) +pkg_check_modules(PC_PulseAudio_MAINLOOP QUIET libpulse-mainloop-glib) + +find_path(PulseAudio_INCLUDE_DIRS pulse/pulseaudio.h + HINTS + ${PC_PulseAudio_INCLUDEDIR} + ${PC_PulseAudio_INCLUDE_DIRS} + ) + +find_library(PulseAudio_LIBRARIES NAMES pulse libpulse + HINTS + ${PC_PulseAudio_LIBDIR} + ${PC_PulseAudio_LIBRARY_DIRS} + ) + +find_library(PulseAudio_MAINLOOP_LIBRARY + NAMES pulse-mainloop pulse-mainloop-glib libpulse-mainloop-glib + HINTS + ${PC_PulseAudio_LIBDIR} + ${PC_PulseAudio_LIBRARY_DIRS} + ) + +# Store the version number in the cache, +# so we don't have to search every time again: +if(PulseAudio_INCLUDE_DIRS AND NOT PulseAudio_VERSION) + + # get PulseAudio's version from its version.h + file(STRINGS "${PulseAudio_INCLUDE_DIRS}/pulse/version.h" pulse_version_h + REGEX ".*pa_get_headers_version\\(\\).*") + string(REGEX REPLACE ".*pa_get_headers_version\\(\\)\ \\(\"([0-9]+\\.[0-9]+\\.[0-9]+)[^\"]*\"\\).*" "\\1" + _PulseAudio_VERSION "${pulse_version_h}") + + set(PulseAudio_VERSION "${_PulseAudio_VERSION}" + CACHE STRING "Version number of PulseAudio" + FORCE) +endif() + +# Use the new extended syntax of +# find_package_handle_standard_args(), +# which also handles version checking: +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(PulseAudio + REQUIRED_VARS PulseAudio_LIBRARIES + PulseAudio_INCLUDE_DIRS + VERSION_VAR PulseAudio_VERSION) + +# Deprecated synonyms +set(PulseAudio_INCLUDE_DIR "${PulseAudio_INCLUDE_DIRS}") +set(PulseAudio_LIBRARY "${PulseAudio_LIBRARIES}") +set(PulseAudio_MAINLOOP_LIBRARY "${PulseAudio_MAINLOOP_LIBRARY}") +set(PulseAudio_FOUND "${PulseAudio_FOUND}") + +if(PulseAudio_FOUND AND NOT TARGET PulseAudio::PulseAudio) + add_library(PulseAudio::PulseAudio UNKNOWN IMPORTED) + set_target_properties(PulseAudio::PulseAudio PROPERTIES + IMPORTED_LOCATION "${PulseAudio_LIBRARIES}" + INTERFACE_INCLUDE_DIRECTORIES "${PulseAudio_INCLUDE_DIRS}") +endif() + +mark_as_advanced(PulseAudio_INCLUDE_DIRS PulseAudio_INCLUDE_DIR + PulseAudio_LIBRARIES PulseAudio_LIBRARY + PulseAudio_MAINLOOP_LIBRARY PulseAudio_MAINLOOP_LIBRARY) + +include(FeatureSummary) +set_package_properties(PulseAudio PROPERTIES + URL "https://www.freedesktop.org/wiki/Software/PulseAudio" + DESCRIPTION "Sound server, for sound stream routing and mixing") diff --git a/configure.in b/configure.in index d3f4fe198..a8de16aac 100644 --- a/configure.in +++ b/configure.in @@ -41,6 +41,10 @@ AC_ARG_WITH(jack, AS_HELP_STRING([--with-jack], [Enable support for JACK @<:@autodetect@:>@]), [with_jack=$withval]) +AC_ARG_WITH(pulseaudio, + AS_HELP_STRING([--with-pulseaudio], [Enable support for PulseAudio @<:@autodetect@:>@]), + [with_pulseaudio=$withval]) + AC_ARG_WITH(oss, AS_HELP_STRING([--with-oss], [Enable support for OSS @<:@autodetect@:>@]), [with_oss=$withval]) @@ -149,10 +153,17 @@ if test "x$with_oss" != "xno"; then AC_CHECK_LIB(ossaudio, _oss_ioctl, have_libossaudio=yes, have_libossaudio=no) fi fi +if [[ "x$with_jack" != "xno" ] || [ "x$with_pulseaudio" != "xno" ]]; then + PKG_PROG_PKG_CONFIG +fi have_jack=no if test "x$with_jack" != "xno"; then PKG_CHECK_MODULES(JACK, jack, have_jack=yes, have_jack=no) fi +have_pulse=no +if test "x$with_pulseaudio" != "xno"; then + PKG_CHECK_MODULES(PULSE, libpulse, have_pulse=yes, have_pulse=no) +fi dnl sizeof checks: we will need a 16-bit and a 32-bit type @@ -386,11 +397,26 @@ case "${host_os}" in if [[ "$have_jack" = "yes" ] && [ "$with_jack" != "no" ]] ; then DLL_LIBS="$DLL_LIBS $JACK_LIBS" CFLAGS="$CFLAGS $JACK_CFLAGS" - OTHER_OBJS="$OTHER_OBJS src/hostapi/jack/pa_jack.o src/common/pa_ringbuffer.o" + OTHER_OBJS="$OTHER_OBJS src/hostapi/jack/pa_jack.o" INCLUDES="$INCLUDES pa_jack.h" AC_DEFINE(PA_USE_JACK,1) fi + if [[ "$have_pulse" = "yes" ] || [ "$have_jack" = "yes" ]] ; then + OTHER_OBJS="$OTHER_OBJS src/common/pa_ringbuffer.o" + INCLUDES="$INCLUDES pa_linux_pulseaudio.h" + fi + + if [[ "$have_pulse" = "yes" ] && [ "$with_pulse" != "no" ]] ; then + DLL_LIBS="$DLL_LIBS $PULSE_LIBS" + CFLAGS="$CFLAGS $PULSE_CFLAGS" + OTHER_OBJS="$OTHER_OBJS src/hostapi/pulseaudio/pa_linux_pulseaudio_cb.o" + OTHER_OBJS="$OTHER_OBJS src/hostapi/pulseaudio/pa_linux_pulseaudio_block.o" + OTHER_OBJS="$OTHER_OBJS src/hostapi/pulseaudio/pa_linux_pulseaudio.o" + dnl INCLUDES="$INCLUDES pa_pulseaudio.h src/hostapi/pulseaudio/pa_linux_pulseaudio_internal.h src/hostapi/pulseaudio/pa_linux_pulseaudio_block_internal.h src/hostapi/pulseaudio/pa_linux_pulseaudio_cb_internal.h" + AC_DEFINE(PA_USE_PULSEAUDIO,1) + fi + if [[ "$with_oss" != "no" ]] ; then OTHER_OBJS="$OTHER_OBJS src/hostapi/oss/pa_unix_oss.o" if [[ "$have_libossaudio" = "yes" ]] ; then @@ -489,6 +515,7 @@ case "$target_os" in AudioIO ..................... $have_audioio OSS ......................... $have_oss JACK ........................ $have_jack + PulseAudio .................. $have_pulse ]) ;; esac diff --git a/include/pa_linux_pulseaudio.h b/include/pa_linux_pulseaudio.h new file mode 100644 index 000000000..7c5dfce05 --- /dev/null +++ b/include/pa_linux_pulseaudio.h @@ -0,0 +1,79 @@ +#ifndef PA_LINUX_PULSEAUDIO_H +#define PA_LINUX_PULSEAUDIO_H + +/* + * $Id$ + * PortAudio Portable Real-Time Audio Library + * PulseAudio-specific extensions + * + * Copyright (c) 1999-2000 Ross Bencina and Phil Burk + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files + * (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + * ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +/* + * The text above constitutes the entire PortAudio license; however, + * the PortAudio community also makes the following non-binding requests: + * + * Any person wishing to distribute modifications to the Software is + * requested to send the modifications to the original developer so that + * they can be incorporated into the canonical version. It is also + * requested that these non-binding requests be included along with the + * license above. + */ + +/** @file + * @ingroup public_header + * @brief PulseAudio-specific PortAudio API extension header file. + */ + +#include "portaudio.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Renames the PulseAudio description for the source that is opened + * by PortAudio. + * + * @param s The PortAudio stream to operate on. + * @param streamName The new name/description of the source. + * + * @return paNoError on success or the error encountered otherwise. + */ +PaError PaPulseAudio_RenameSource( PaStream *s, const char *streamName ); + +/** + * Renames the PulseAudio description for the sink that is opened + * by PortAudio. + * + * @param s The PortAudio stream to operate on. + * @param streamName The new name/description of the sink. + * + * @return paNoError on success or the error encountered otherwise. + */ +PaError PaPulseAudio_RenameSink( PaStream *s, const char *streamName ); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/include/portaudio.h b/include/portaudio.h index 9eaeeaedb..ab1efeb55 100644 --- a/include/portaudio.h +++ b/include/portaudio.h @@ -289,7 +289,8 @@ typedef enum PaHostApiTypeId paJACK=12, paWASAPI=13, paAudioScienceHPI=14, - paAudioIO=15 + paAudioIO=15, + paPulseAudio=16 } PaHostApiTypeId; diff --git a/src/common/pa_hostapi.h b/src/common/pa_hostapi.h index 4ac3ab60e..c716fa7e5 100644 --- a/src/common/pa_hostapi.h +++ b/src/common/pa_hostapi.h @@ -67,7 +67,7 @@ are defaulted to 1. #define PA_USE_SKELETON 1 #endif -#if defined(PA_NO_ASIO) || defined(PA_NO_DS) || defined(PA_NO_WMME) || defined(PA_NO_WASAPI) || defined(PA_NO_WDMKS) +#if defined(PA_NO_PULSEAUDIO) || defined(PA_NO_ASIO) || defined(PA_NO_DS) || defined(PA_NO_WMME) || defined(PA_NO_WASAPI) || defined(PA_NO_WDMKS) #error "Portaudio: PA_NO_ is no longer supported, please remove definition and use PA_USE_ instead" #endif @@ -132,6 +132,13 @@ are defaulted to 1. #define PA_USE_JACK 1 #endif +#ifndef PA_USE_PULSEAUDIO +#define PA_USE_PULSEAUDIO 0 +#elif (PA_USE_PULSEAUDIO != 0) && (PA_USE_PULSEAUDIO != 1) +#undef PA_USE_PULSEAUDIO +#define PA_USE_PULSEAUDIO 1 +#endif + #ifndef PA_USE_SGI #define PA_USE_SGI 0 #elif (PA_USE_SGI != 0) && (PA_USE_SGI != 1) diff --git a/src/hostapi/pulseaudio/pa_linux_pulseaudio.c b/src/hostapi/pulseaudio/pa_linux_pulseaudio.c new file mode 100644 index 000000000..27dcdf89e --- /dev/null +++ b/src/hostapi/pulseaudio/pa_linux_pulseaudio.c @@ -0,0 +1,1453 @@ + +/* + * PulseAudio host to play natively in Linux based systems without + * ALSA emulation + * + * Copyright (c) 2014-2023 Tuukka Pasanen + * Copyright (c) 2016 Sqweek + * Copyright (c) 2020 Daniel Schurmann + * + * Based on the Open Source API proposed by Ross Bencina + * Copyright (c) 1999-2002 Ross Bencina, Phil Burk + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files + * (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + * ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +/* + * The text above constitutes the entire PortAudio license; however, + * the PortAudio community also makes the following non-binding requests: + * + * Any person wishing to distribute modifications to the Software is + * requested to send the modifications to the original developer so that + * they can be incorporated into the canonical version. It is also + * requested that these non-binding requests be included along with the + * license above. + */ + +/** @file + @ingroup common_src + + @brief PulseAudio implementation of support for a host API. + + This host API implements PulseAudio support for portaudio + it has callbackmode and normal write mode support +*/ + + +#include /* strlen() */ + +#include "pa_linux_pulseaudio_cb_internal.h" +#include "pa_linux_pulseaudio_block_internal.h" + +/* PulseAudio headers */ +#include +#include +#include + +/* This is used to identify process name for PulseAudio. */ +extern char *__progname; + +/* PulseAudio specific functions */ +int PaPulseAudio_CheckConnection( PaPulseAudio_HostApiRepresentation * ptr ) +{ + pa_context_state_t state; + + + /* Sanity check if ptr if NULL don't go anywhere or + * it will SIGSEGV + */ + if( !ptr ) + { + PA_PULSEAUDIO_SET_LAST_HOST_ERROR( 0, + "PaPulseAudio_CheckConnection: Host API is NULL! Can't do anything about it" ); + return -1; + } + + if( !ptr->context || !ptr->mainloop ) + { + PA_PULSEAUDIO_SET_LAST_HOST_ERROR( 0, + "PaPulseAudio_CheckConnection: PulseAudio context or mainloop are NULL" ); + return -1; + } + + state = pa_context_get_state(ptr->context); + + if( !PA_CONTEXT_IS_GOOD(state) ) + { + switch( state ) + { + /* These can be found from + * https://freedesktop.org/software/pulseaudio/doxygen/def_8h.html + */ + + case PA_CONTEXT_UNCONNECTED: + PA_PULSEAUDIO_SET_LAST_HOST_ERROR( 0, + "PaPulseAudio_CheckConnection: The context hasn't been connected yet (PA_CONTEXT_UNCONNECTED)" ); + break; + + case PA_CONTEXT_FAILED: + PA_PULSEAUDIO_SET_LAST_HOST_ERROR( 0, + "PaPulseAudio_CheckConnection: The connection failed or was disconnected. (PA_CONTEXT_FAILED)" ); + break; + } + + return -1; + } + return 0; +} + +/* Create HostAPI presensentation */ +PaPulseAudio_HostApiRepresentation *PaPulseAudio_New( void ) +{ + PaPulseAudio_HostApiRepresentation *ptr; + int fd[2] = { -1, -1 }; + char pulseaudioDeviceName[PAPULSEAUDIO_MAX_DEVICENAME]; + + ptr = (PaPulseAudio_HostApiRepresentation *) + PaUtil_AllocateZeroInitializedMemory(sizeof(PaPulseAudio_HostApiRepresentation)); + + /* ptr is NULL if runs out of memory or pointer to allocated memory */ + if( !ptr ) + { + PA_PULSEAUDIO_SET_LAST_HOST_ERROR( 0, + "PaPulseAudio_HostApiRepresentation: Can't allocate memory required for using PulseAudio" ); + return NULL; + } + + /* Make sure we have NULL all struct first */ + memset(ptr, 0x00, sizeof(PaPulseAudio_HostApiRepresentation)); + + ptr->mainloop = pa_threaded_mainloop_new(); + + if( !ptr->mainloop ) + { + PA_PULSEAUDIO_SET_LAST_HOST_ERROR( 0, + "PaPulseAudio_HostApiRepresentation: Can't allocate PulseAudio mainloop" ); + goto fail; + } + + ptr->mainloopApi = pa_threaded_mainloop_get_api(ptr->mainloop); + + /* Use program name as PulseAudio device name */ + snprintf( pulseaudioDeviceName, PAPULSEAUDIO_MAX_DEVICENAME, "%s", __progname ); + + ptr->context = + pa_context_new( pa_threaded_mainloop_get_api(ptr->mainloop), pulseaudioDeviceName ); + + if( !ptr->context ) + { + PA_PULSEAUDIO_SET_LAST_HOST_ERROR( 0, + "PaPulseAudio_HostApiRepresentation: Can't instantiate PulseAudio context" ); + goto fail; + } + + pa_context_set_state_callback( ptr->context, PaPulseAudio_CheckContextStateCb, + ptr ); + + + if( pa_threaded_mainloop_start( ptr->mainloop ) < 0 ) + { + PA_PULSEAUDIO_SET_LAST_HOST_ERROR( 0, + "PaPulseAudio_HostApiRepresentation: PulseAudio can't start mainloop" ); + goto fail; + } + + ptr->deviceCount = 0; + + return ptr; + + fail: + PaPulseAudio_Free( ptr ); + return NULL; +} + +/* Free HostAPI */ +void PaPulseAudio_Free( PaPulseAudio_HostApiRepresentation * ptr ) +{ + + /* Sanity check if ptr if NULL don't go anywhere or + * it will SIGSEGV + */ + if( !ptr ) + { + PA_PULSEAUDIO_SET_LAST_HOST_ERROR( 0, + "PaPulseAudio_Free: Host API is NULL! Can't do anything about it" ); + return; + } + + if( ptr->mainloop ) + { + pa_threaded_mainloop_stop( ptr->mainloop ); + } + + if( ptr->context ) + { + pa_context_disconnect( ptr->context ); + pa_context_unref( ptr->context ); + ptr->context = NULL; + } + + if( ptr->mainloopApi && ptr->timeEvent ) + { + ptr->mainloopApi->time_free( ptr->timeEvent ); + ptr->mainloopApi = NULL; + ptr->timeEvent = NULL; + } + + + if( ptr->mainloop ) + { + pa_threaded_mainloop_free( ptr->mainloop ); + ptr->mainloop = NULL; + } + + PaUtil_FreeMemory( ptr ); +} + +/* If there is drop connection to server this one is called + * in future it should stop the stream also + */ +void PaPulseAudio_CheckContextStateCb( pa_context * c, + void *userdata ) +{ + PaPulseAudio_HostApiRepresentation *ptr = + (PaPulseAudio_HostApiRepresentation *) userdata; + /* If this is null we have big problems and we probably are out of memory */ + if( !c ) + { + PA_PULSEAUDIO_SET_LAST_HOST_ERROR( 0, + "PaPulseAudio_CheckContextStateCb: Invalid context " ); + pa_threaded_mainloop_signal( ptr->mainloop, 0 ); + return; + } + + pa_threaded_mainloop_signal( ptr->mainloop, 0 ); +} + +/* Server info callback */ +void PaPulseAudio_ServerInfoCb( pa_context *c, + const pa_server_info *i, + void *userdata ) +{ + PaPulseAudio_HostApiRepresentation *pulseaudioHostApi = + (PaPulseAudio_HostApiRepresentation *) userdata; + + if( !c || !i ) + { + PA_PULSEAUDIO_SET_LAST_HOST_ERROR( 0, + "PaPulseAudio_ServerInfoCb: Invalid context or can't get server info" ); + pa_threaded_mainloop_signal( pulseaudioHostApi->mainloop, 0 ); + return; + } + + pulseaudioHostApi->pulseaudioDefaultSampleSpec = i->sample_spec; + + pa_threaded_mainloop_signal( pulseaudioHostApi->mainloop, 0 ); +} + +/* Function adds device to list. It can be input or output stream + * or in pulseaudio source or sink. + */ +int _PaPulseAudio_AddAudioDevice( PaPulseAudio_HostApiRepresentation *hostapi, + const char *PaPulseAudio_SinkSourceName, + const char *PaPulseAudio_SinkSourceNameDesc, + int inputChannels, + int outputChannels, + double defaultLowInputLatency, + double defaultHighInputLatency, + double defaultLowOutputLatency, + double defaultHighOutputLatency, + const long defaultSampleRate ) +{ + /* These should be at least 1 + * + * Maximun size of string is 1024 (PAPULSEAUDIO_MAX_DEVICENAME) + * which should be mostly suffient even pulseaudio device + * names can be very long + */ + int pulseaudioRealNameSize = strnlen( PaPulseAudio_SinkSourceNameDesc, (PAPULSEAUDIO_MAX_DEVICENAME - 1) ) + 1; + int pulseaudioDeviceNameSize = strnlen( PaPulseAudio_SinkSourceName, (PAPULSEAUDIO_MAX_DEVICENAME - 1) ) + 1; + char *pulseaudioLocalDeviceName = NULL; + + hostapi->deviceInfoArray[hostapi->deviceCount].structVersion = 2; + hostapi->deviceInfoArray[hostapi->deviceCount].hostApi = hostapi->hostApiIndex; + hostapi->pulseaudioDeviceNames[hostapi->deviceCount] = + PaUtil_GroupAllocateZeroInitializedMemory( hostapi->allocations, + pulseaudioRealNameSize ); + pulseaudioLocalDeviceName = PaUtil_GroupAllocateZeroInitializedMemory( hostapi->allocations, + pulseaudioDeviceNameSize ); + if( !hostapi->pulseaudioDeviceNames[hostapi->deviceCount] && + !pulseaudioLocalDeviceName ) + { + PA_PULSEAUDIO_SET_LAST_HOST_ERROR( 0, + "_PaPulseAudio_AddAudioDevice: Can't alloc memory" ); + return paInsufficientMemory; + } + + /* We can maximum have 1024 (PAPULSEAUDIO_MAX_DEVICECOUNT) + * devices where to choose which should be mostly enough + */ + if( hostapi->deviceCount >= PAPULSEAUDIO_MAX_DEVICECOUNT ) + { + return paDeviceUnavailable; + } + + snprintf( hostapi->pulseaudioDeviceNames[hostapi->deviceCount], + pulseaudioRealNameSize, + "%s", + PaPulseAudio_SinkSourceNameDesc ); + snprintf( pulseaudioLocalDeviceName, + pulseaudioDeviceNameSize, + "%s", + PaPulseAudio_SinkSourceName ); + + + hostapi->deviceInfoArray[hostapi->deviceCount].name = pulseaudioLocalDeviceName; + + hostapi->deviceInfoArray[hostapi->deviceCount].maxInputChannels = inputChannels; + hostapi->deviceInfoArray[hostapi->deviceCount].maxOutputChannels = outputChannels; + hostapi->deviceInfoArray[hostapi->deviceCount].defaultLowInputLatency = defaultLowInputLatency; + hostapi->deviceInfoArray[hostapi->deviceCount].defaultLowOutputLatency = defaultLowOutputLatency; + hostapi->deviceInfoArray[hostapi->deviceCount].defaultHighInputLatency = defaultHighInputLatency; + hostapi->deviceInfoArray[hostapi->deviceCount].defaultHighOutputLatency = defaultHighOutputLatency; + hostapi->deviceInfoArray[hostapi->deviceCount].defaultSampleRate = defaultSampleRate; + hostapi->deviceCount++; + + return paNoError; +} + +/* Called when iterating through sinks */ +void PaPulseAudio_SinkListCb( pa_context * c, + const pa_sink_info * l, + int eol, + void *userdata ) +{ + PaPulseAudio_HostApiRepresentation *pulseaudioHostApi = + (PaPulseAudio_HostApiRepresentation *) userdata; + const char *pulseaudioDeviceName = NULL; + + + /* If this is null we have big problems and we probably are out of memory */ + if( !c || !l ) + { + PA_PULSEAUDIO_SET_LAST_HOST_ERROR( 0, + "PaPulseAudio_SinkListCb: Invalid context or sink info" ); + goto error; + } + + /* If eol is set to a positive number, you're at the end of the list */ + if( eol > 0 ) + { + goto error; + } + + pulseaudioDeviceName = l->name; + + if( l->description != NULL ) + { + pulseaudioDeviceName = l->description; + } + + if( _PaPulseAudio_AddAudioDevice( pulseaudioHostApi, + pulseaudioDeviceName, + l->name, + 0, + l->sample_spec.channels, + 0, + 0, + PA_PULSEAUDIO_DEFAULT_MIN_LATENCY, + PA_PULSEAUDIO_DEFAULT_MAX_LATENCY, + l->sample_spec.rate ) != paNoError ) + { + PA_PULSEAUDIO_SET_LAST_HOST_ERROR( 0, + "PaPulseAudio_SinkListCb: Can't add device. Maximum amount reached!" ); + } + + error: + pa_threaded_mainloop_signal( pulseaudioHostApi->mainloop, + 0 ); +} + +/* Called when iterating through sources */ +void PaPulseAudio_SourceListCb( pa_context * c, + const pa_source_info * l, + int eol, + void *userdata ) +{ + PaPulseAudio_HostApiRepresentation *pulseaudioHostApi = + (PaPulseAudio_HostApiRepresentation *) userdata; + const char *pulseaudioDeviceName = NULL; + + + /* If this is null we have big problems and we probably are out of memory */ + if( !c ) + { + PA_PULSEAUDIO_SET_LAST_HOST_ERROR( 0, + "PaPulseAudio_SourceListCb: Invalid context" ); + goto error; + } + + /* If eol is set to a positive number, you're at the end of the list */ + if( eol > 0 ) + { + goto error; + } + + pulseaudioDeviceName = l->name; + + if( l->description != NULL ) + { + pulseaudioDeviceName = l->description; + } + + if( _PaPulseAudio_AddAudioDevice( pulseaudioHostApi, + pulseaudioDeviceName, + l->name, + l->sample_spec.channels, + 0, + PA_PULSEAUDIO_DEFAULT_MIN_LATENCY, + PA_PULSEAUDIO_DEFAULT_MAX_LATENCY, + 0, + 0, + l->sample_spec.rate ) != paNoError ) + { + PA_PULSEAUDIO_SET_LAST_HOST_ERROR( 0, + "PaPulseAudio_SourceListCb: Can't add device. Maximum amount reached!" ); + } + + error: + pa_threaded_mainloop_signal( pulseaudioHostApi->mainloop, + 0 ); +} + +/* This routine is called whenever the stream state changes */ +void PaPulseAudio_StreamStateCb( pa_stream * s, + void *userdata ) +{ + const pa_buffer_attr *pulseaudioBufferAttr = NULL; + /* If you need debug pring enable these + * char cmt[PA_CHANNEL_MAP_SNPRINT_MAX], sst[PA_SAMPLE_SPEC_SNPRINT_MAX]; + */ + + + /* If this is null we have big problems and we probably are out of memory */ + if( !s ) + { + PA_PULSEAUDIO_SET_LAST_HOST_ERROR( 0, + "PaPulseAudio_StreamStateCb: Invalid stream" ); + return; + } + + switch( pa_stream_get_state(s) ) + { + case PA_STREAM_TERMINATED: + break; + + case PA_STREAM_CREATING: + break; + + case PA_STREAM_READY: + if (!(pulseaudioBufferAttr = pa_stream_get_buffer_attr(s))) + { + PA_DEBUG( ("Portaudio %s: Can get buffer attr: '%s'\n", + __FUNCTION__, + pa_strerror(pa_context_errno(pa_stream_get_context(s) ) )) ); + PA_PULSEAUDIO_SET_LAST_HOST_ERROR( 0, + "PaPulseAudio_StreamStateCb: Can't get Stream pa_buffer_attr" ); + } + else + { + PA_DEBUG( ("%s: %s Buffer metrics: maxlength=%u, tlength=%u, prebuf=%u, minreq=%u, fragsize=%u\n", + __FUNCTION__, pa_stream_get_device_name(s), + pulseaudioBufferAttr->maxlength, pulseaudioBufferAttr->tlength, pulseaudioBufferAttr->prebuf, + pulseaudioBufferAttr->minreq, pulseaudioBufferAttr->maxlength, pulseaudioBufferAttr->fragsize) ); + } + break; + + case PA_STREAM_FAILED: + default: + PA_DEBUG( ("Portaudio %s: FAILED '%s'\n", + __FUNCTION__, + pa_strerror( pa_context_errno( pa_stream_get_context( s ) ) )) ); + + break; + } +} + +/* If stream is underflowed then this callback is called + * one needs to enable debug to make use os this + * + * Otherwise it's used to update error message + */ +void PaPulseAudio_StreamUnderflowCb( pa_stream *s, + void *userdata ) +{ + PaPulseAudio_Stream *stream = (PaPulseAudio_Stream *) userdata; + pa_buffer_attr *pulseaudioOutputSampleSpec = NULL; + + /* If this is null we have big problems and we probably are out of memory */ + if( !s ) + { + PA_PULSEAUDIO_SET_LAST_HOST_ERROR( 0, + "PaPulseAudio_StreamUnderflowCb: Invalid stream" ); + return; + } + + stream->outputUnderflows++; + pulseaudioOutputSampleSpec = (pa_buffer_attr *)pa_stream_get_buffer_attr(s); + PA_DEBUG( ("Portaudio %s: PulseAudio '%s' with delay: %ld stream has underflowed\n", __FUNCTION__, pa_stream_get_device_name(s), pulseaudioOutputSampleSpec->tlength) ); + + PA_PULSEAUDIO_SET_LAST_HOST_ERROR( 0, + "PaPulseAudio_StreamUnderflowCb: Pulseaudio stream underflow"); + + pa_threaded_mainloop_signal( stream->mainloop, + 0 ); +} + +/* Initialize HostAPI */ +PaError PaPulseAudio_Initialize( PaUtilHostApiRepresentation ** hostApi, + PaHostApiIndex hostApiIndex ) +{ + PaError result = paNoError; + int i; + int deviceCount; + int ret = 0; + PaPulseAudio_HostApiRepresentation *pulseaudioHostApi = NULL; + PaDeviceInfo *deviceInfoArray = NULL; + + pa_operation *pulseaudioOperation = NULL; + + pulseaudioHostApi = PaPulseAudio_New(); + + if( !pulseaudioHostApi ) + { + result = paInsufficientMemory; + goto error; + } + + pulseaudioHostApi->allocations = PaUtil_CreateAllocationGroup(); + + if( !pulseaudioHostApi->allocations ) + { + result = paInsufficientMemory; + goto error; + } + + pulseaudioHostApi->hostApiIndex = hostApiIndex; + *hostApi = &pulseaudioHostApi->inheritedHostApiRep; + (*hostApi)->info.structVersion = 1; + (*hostApi)->info.type = paPulseAudio; + (*hostApi)->info.name = "PulseAudio"; + + (*hostApi)->info.defaultInputDevice = paNoDevice; + (*hostApi)->info.defaultOutputDevice = paNoDevice; + + /* Connect to server */ + PaPulseAudio_Lock( pulseaudioHostApi->mainloop ); + ret = pa_context_connect( pulseaudioHostApi->context, + NULL, + 0, + NULL ); + + if( ret < 0 ) + { + PA_PULSEAUDIO_SET_LAST_HOST_ERROR( 0, + "PulseAudio_Initialize: Can't connect to server"); + result = paNotInitialized; + PaPulseAudio_UnLock( pulseaudioHostApi->mainloop ); + goto error; + } + + ret = 0; + + /* We should wait that PulseAudio server let us in or fails us */ + while( !ret ) + { + pa_threaded_mainloop_wait( pulseaudioHostApi->mainloop ); + + switch( pa_context_get_state( pulseaudioHostApi->context ) ) + { + case PA_CONTEXT_READY: + ret = 1; + break; + case PA_CONTEXT_TERMINATED: + case PA_CONTEXT_FAILED: + goto error; + break; + case PA_CONTEXT_UNCONNECTED: + case PA_CONTEXT_CONNECTING: + case PA_CONTEXT_AUTHORIZING: + case PA_CONTEXT_SETTING_NAME: + break; + } + } + + memset( pulseaudioHostApi->deviceInfoArray, + 0x00, + sizeof(PaDeviceInfo) * PAPULSEAUDIO_MAX_DEVICECOUNT ); + for (i = 0; i < PAPULSEAUDIO_MAX_DEVICECOUNT; i++) + { + pulseaudioHostApi->pulseaudioDeviceNames[i] = NULL; + } + + /* Get info about server. This returns Default sink and soure name. */ + pulseaudioOperation = + pa_context_get_server_info( pulseaudioHostApi->context, + PaPulseAudio_ServerInfoCb, + pulseaudioHostApi ); + + while( pa_operation_get_state( pulseaudioOperation ) == PA_OPERATION_RUNNING ) + { + pa_threaded_mainloop_wait( pulseaudioHostApi->mainloop ); + } + + pa_operation_unref( pulseaudioOperation ); + + /* Add the "Default" sink at index 0 */ + if( _PaPulseAudio_AddAudioDevice( pulseaudioHostApi, + "Default Sink", + "The PulseAudio default sink", + 0, + PA_CHANNELS_MAX, + 0, + 0, + PA_PULSEAUDIO_DEFAULT_MIN_LATENCY, + PA_PULSEAUDIO_DEFAULT_MAX_LATENCY, + pulseaudioHostApi->pulseaudioDefaultSampleSpec.rate ) != paNoError ) + { + PA_PULSEAUDIO_SET_LAST_HOST_ERROR( 0, + "PaPulseAudio_SinkListCb: Can't add device. Maximum amount reached!" ); + } else { + pulseaudioHostApi->inheritedHostApiRep.info.defaultOutputDevice = + pulseaudioHostApi->deviceCount - 1; + } + + /* Add the "Default" source at index 1 */ + if( _PaPulseAudio_AddAudioDevice( pulseaudioHostApi, + "Default Source", + "The PulseAudio default source", + PA_CHANNELS_MAX, + 0, + PA_PULSEAUDIO_DEFAULT_MIN_LATENCY, + PA_PULSEAUDIO_DEFAULT_MAX_LATENCY, + 0, + 0, + pulseaudioHostApi->pulseaudioDefaultSampleSpec.rate ) != paNoError ) + { + PA_PULSEAUDIO_SET_LAST_HOST_ERROR( 0, + "PaPulseAudio_SinkListCb: Can't add device. Maximum amount reached!" ); + } else { + pulseaudioHostApi->inheritedHostApiRep.info.defaultInputDevice = + pulseaudioHostApi->deviceCount - 1; + } + + /* List PulseAudio sinks. If found callback: PaPulseAudio_SinkListCb */ + pulseaudioOperation = + pa_context_get_sink_info_list( pulseaudioHostApi->context, + PaPulseAudio_SinkListCb, + pulseaudioHostApi ); + + while( pa_operation_get_state( pulseaudioOperation ) == PA_OPERATION_RUNNING ) + { + pa_threaded_mainloop_wait( pulseaudioHostApi->mainloop ); + } + + pa_operation_unref( pulseaudioOperation ); + + /* List PulseAudio sources. If found callback: PaPulseAudio_SourceListCb */ + pulseaudioOperation = + pa_context_get_source_info_list( pulseaudioHostApi->context, + PaPulseAudio_SourceListCb, + pulseaudioHostApi ); + + while( pa_operation_get_state( pulseaudioOperation ) == PA_OPERATION_RUNNING ) + { + pa_threaded_mainloop_wait( pulseaudioHostApi->mainloop ); + } + + pa_operation_unref( pulseaudioOperation ); + + (*hostApi)->info.deviceCount = pulseaudioHostApi->deviceCount; + + if( pulseaudioHostApi->deviceCount > 0 ) + { + /* If you have over 1024 Audio devices.. shame on you! */ + + (*hostApi)->deviceInfos = + (PaDeviceInfo **) + PaUtil_GroupAllocateZeroInitializedMemory( pulseaudioHostApi->allocations, + sizeof(PaDeviceInfo *) * + pulseaudioHostApi->deviceCount ); + + if( !(*hostApi)->deviceInfos ) + { + result = paInsufficientMemory; + goto error; + } + + for ( i = 0; i < pulseaudioHostApi->deviceCount; i++ ) + { + (*hostApi)->deviceInfos[i] = + &pulseaudioHostApi->deviceInfoArray[i]; + } + } + + (*hostApi)->Terminate = Terminate; + (*hostApi)->OpenStream = OpenStream; + (*hostApi)->IsFormatSupported = IsFormatSupported; + + PaUtil_InitializeStreamInterface( &pulseaudioHostApi->callbackStreamInterface, + PaPulseAudio_CloseStreamCb, + PaPulseAudio_StartStreamCb, + PaPulseAudio_StopStreamCb, + PaPulseAudio_AbortStreamCb, + IsStreamStopped, + IsStreamActive, + GetStreamTime, + GetStreamCpuLoad, + PaUtil_DummyRead, + PaUtil_DummyWrite, + PaUtil_DummyGetReadAvailable, + PaUtil_DummyGetWriteAvailable ); + + PaUtil_InitializeStreamInterface( &pulseaudioHostApi->blockingStreamInterface, + PaPulseAudio_CloseStreamCb, + PaPulseAudio_StartStreamCb, + PaPulseAudio_StopStreamCb, + PaPulseAudio_AbortStreamCb, + IsStreamStopped, + IsStreamActive, + GetStreamTime, + PaUtil_DummyGetCpuLoad, + PaPulseAudio_ReadStreamBlock, + PaPulseAudio_WriteStreamBlock, + PaPulseAudio_GetStreamReadAvailableBlock, + PaUtil_DummyGetWriteAvailable ); + + PaPulseAudio_UnLock( pulseaudioHostApi->mainloop ); + return result; + + error: + + if( pulseaudioHostApi ) + { + if( pulseaudioHostApi->allocations ) + { + PaUtil_FreeAllAllocations( pulseaudioHostApi->allocations ); + PaUtil_DestroyAllocationGroup( pulseaudioHostApi->allocations ); + } + + PaPulseAudio_Free( pulseaudioHostApi ); + pulseaudioHostApi = NULL; + } + + PaPulseAudio_UnLock( pulseaudioHostApi->mainloop ); + return result; +} + +/* Drop stream now */ +void Terminate( struct PaUtilHostApiRepresentation *hostApi ) +{ + PaPulseAudio_HostApiRepresentation *pulseaudioHostApi = + (PaPulseAudio_HostApiRepresentation *) hostApi; + + if( pulseaudioHostApi->allocations ) + { + PaUtil_FreeAllAllocations( pulseaudioHostApi->allocations ); + PaUtil_DestroyAllocationGroup( pulseaudioHostApi->allocations ); + } + + PaPulseAudio_Lock( pulseaudioHostApi->mainloop ); + pa_context_disconnect( pulseaudioHostApi->context ); + PaPulseAudio_UnLock( pulseaudioHostApi->mainloop ); + + PaPulseAudio_Free( pulseaudioHostApi ); +} + +/* Checks from pulseaudio that is format supported */ +PaError IsFormatSupported( struct PaUtilHostApiRepresentation *hostApi, + const PaStreamParameters * inputParameters, + const PaStreamParameters * outputParameters, + double sampleRate ) +{ + int inputChannelCount, + outputChannelCount; + PaSampleFormat inputSampleFormat, + outputSampleFormat; + + if( inputParameters ) + { + inputChannelCount = inputParameters->channelCount; + inputSampleFormat = inputParameters->sampleFormat; + + /* all standard sample formats are supported by the buffer adapter, + * this implementation doesn't support any custom sample formats */ + if( inputSampleFormat & paCustomFormat ) + { + return paSampleFormatNotSupported; + } + + /* unless alternate device specification is supported, reject the use of + * paUseHostApiSpecificDeviceSpecification */ + + if( inputParameters->device == paUseHostApiSpecificDeviceSpecification ) + { + return paInvalidDevice; + } + + /* check that input device can support inputChannelCount */ + if( inputChannelCount > + hostApi->deviceInfos[inputParameters->device]->maxInputChannels ) + { + return paInvalidChannelCount; + } + + /* validate inputStreamInfo */ + if( inputParameters->hostApiSpecificStreamInfo ) + { + return paIncompatibleHostApiSpecificStreamInfo; /* this implementation doesn't use custom stream info */ + } + + } + + else + { + inputChannelCount = 0; + } + + if( outputParameters ) + { + outputChannelCount = outputParameters->channelCount; + outputSampleFormat = outputParameters->sampleFormat; + + /* all standard sample formats are supported by the buffer adapter, + * this implementation doesn't support any custom sample formats + */ + if( outputSampleFormat & paCustomFormat ) + { + return paSampleFormatNotSupported; + } + + /* unless alternate device specification is supported, reject the use of + * paUseHostApiSpecificDeviceSpecification + */ + if( outputParameters->device == paUseHostApiSpecificDeviceSpecification ) + { + return paInvalidDevice; + } + + /* check that output device can support outputChannelCount */ + if( outputChannelCount > + hostApi->deviceInfos[outputParameters->device]->maxOutputChannels ) + { + return paInvalidChannelCount; + } + + /* validate outputStreamInfo */ + if( outputParameters->hostApiSpecificStreamInfo ) + { + /* this implementation doesn't use custom stream info */ + return paIncompatibleHostApiSpecificStreamInfo; + } + + } + + else + { + outputChannelCount = 0; + } + + return paFormatIsSupported; +} + +/* Makes conversion from portaudio to pulseaudio sample defines + * Little endian formats are used (if there is some mystical big endian + * sound device this should be fixed but until then it's safe to believe + * this works + */ +PaError PaPulseAudio_ConvertPortaudioFormatToPaPulseAudio_( PaSampleFormat portaudiosf, + pa_sample_spec * pulseaudiosf ) +{ + switch( portaudiosf ) + { + case paFloat32: + pulseaudiosf->format = PA_SAMPLE_FLOAT32LE; + break; + + case paInt32: + pulseaudiosf->format = PA_SAMPLE_S32LE; + break; + + case paInt24: + pulseaudiosf->format = PA_SAMPLE_S24LE; + break; + + case paInt16: + pulseaudiosf->format = PA_SAMPLE_S16LE; + break; + + case paInt8: + pulseaudiosf->format = PA_SAMPLE_U8; + break; + + case paUInt8: + pulseaudiosf->format = PA_SAMPLE_U8; + break; + + case paCustomFormat: + case paNonInterleaved: + PA_DEBUG(("PaPulseAudio %s: THIS IS NOT SUPPORTED BY PULSEAUDIO!\n", + __FUNCTION__)); + return paSampleFormatNotSupported; + break; + } + + return paNoError; +} + + +/* Allocate buffer. */ +PaError PaPulseAudio_BlockingInitRingBuffer( PaUtilRingBuffer * rbuf, + int size ) +{ + char *ringbufferBuffer = (char *) malloc( size ); + PaError ret = paNoError; + + if( ringbufferBuffer == NULL ) + { + PA_PULSEAUDIO_SET_LAST_HOST_ERROR( 0, + "PaPulseAudio_BlockingInitRingBuffer: Not enough memory to handle request" ); + return paInsufficientMemory; + } + + memset( ringbufferBuffer, + 0x00, + size ); + + ret = PaUtil_InitializeRingBuffer( rbuf, + 1, + size, + ringbufferBuffer ); + + if( ret < paNoError ) + { + free( ringbufferBuffer ); + PA_DEBUG( ("Portaudio %s: Can't initialize input ringbuffer with size: %ld!\n", + __FUNCTION__, size) ); + PA_PULSEAUDIO_SET_LAST_HOST_ERROR( 0, + "PaPulseAudio_BlockingInitRingBuffer: Can't initialize input ringbuffer" ); + + return paNotInitialized; + } + + return paNoError; +} + +/* see pa_hostapi.h for a list of validity guarantees made about OpenStream parameters */ + +PaError OpenStream( struct PaUtilHostApiRepresentation *hostApi, + PaStream ** s, + const PaStreamParameters * inputParameters, + const PaStreamParameters * outputParameters, + double sampleRate, + unsigned long framesPerBuffer, + PaStreamFlags streamFlags, + PaStreamCallback * streamCallback, + void *userData ) +{ + PaError result = paNoError; + PaPulseAudio_HostApiRepresentation *pulseaudioHostApi = + (PaPulseAudio_HostApiRepresentation *) hostApi; + PaPulseAudio_Stream *stream = NULL; + unsigned long framesPerHostBuffer = framesPerBuffer; /* these may not be equivalent for all implementations */ + int inputChannelCount, + outputChannelCount; + PaSampleFormat inputSampleFormat, + outputSampleFormat; + PaSampleFormat hostInputSampleFormat, + hostOutputSampleFormat; + + /* validate platform specific flags */ + if( (streamFlags & paPlatformSpecificFlags) != 0 ) + { + return paInvalidFlag; + } + + /* This is something that Pulseaudio can handle + * and it's also bearable small + */ + if( framesPerBuffer == paFramesPerBufferUnspecified ) + { + framesPerBuffer = PAPULSEAUDIO_FRAMESPERBUFFERUNSPEC; + } + + PaPulseAudio_Lock(pulseaudioHostApi->mainloop); + stream = + (PaPulseAudio_Stream *) PaUtil_AllocateZeroInitializedMemory( sizeof( PaPulseAudio_Stream ) ); + + if( !stream ) + { + result = paInsufficientMemory; + goto openstream_error; + } + + /* Allocate memory for source and sink names. */ + const char defaultSourceStreamName[] = "Portaudio source"; + const char defaultSinkStreamName[] = "Portaudio sink"; + + stream->framesPerHostCallback = framesPerBuffer; + stream->inputStreamName = (char*)PaUtil_AllocateZeroInitializedMemory( sizeof( defaultSourceStreamName ) ); + stream->outputStreamName = (char*)PaUtil_AllocateZeroInitializedMemory( sizeof( defaultSinkStreamName ) ); + if ( !stream->inputStreamName || !stream->outputStreamName ) + { + result = paInsufficientMemory; + goto openstream_error; + } + + /* Copy initial stream names to memory. */ + memcpy( stream->inputStreamName, defaultSourceStreamName, sizeof( defaultSourceStreamName ) ); + memcpy( stream->outputStreamName, defaultSinkStreamName, sizeof( defaultSinkStreamName ) ); + + stream->isActive = 0; + stream->isStopped = 1; + stream->pulseaudioIsActive = 0; + stream->pulseaudioIsStopped = 1; + + stream->inputStream = NULL; + stream->outputStream = NULL; + memset( &stream->inputRing, + 0x00, + sizeof( PaUtilRingBuffer ) ); + + if( inputParameters ) + { + inputChannelCount = inputParameters->channelCount; + inputSampleFormat = inputParameters->sampleFormat; + + /* unless alternate device specification is supported, reject the use of + * paUseHostApiSpecificDeviceSpecification + */ + if( inputParameters->device == paUseHostApiSpecificDeviceSpecification ) + { + result = paInvalidDevice; + goto openstream_error; + } + + /* check that input device can support inputChannelCount */ + if( inputChannelCount > + hostApi->deviceInfos[inputParameters->device]->maxInputChannels ) + { + result = paInvalidChannelCount; + goto openstream_error; + } + + /* validate inputStreamInfo */ + if( inputParameters->hostApiSpecificStreamInfo ) + { + /* this implementation doesn't use custom stream info */ + result = paIncompatibleHostApiSpecificStreamInfo; + goto openstream_error; + } + + hostInputSampleFormat = + PaUtil_SelectClosestAvailableFormat( inputSampleFormat, + inputSampleFormat ); + + stream->inputFrameSize = Pa_GetSampleSize( inputSampleFormat ) * inputChannelCount; + + result = PaPulseAudio_ConvertPortaudioFormatToPaPulseAudio_( + hostInputSampleFormat, + &stream->inputSampleSpec ); + + if( result != paNoError ) + { + goto openstream_error; + } + + stream->inputSampleSpec.rate = sampleRate; + stream->inputSampleSpec.channels = inputChannelCount; + stream->inputChannelCount = inputChannelCount; + + if( !pa_sample_spec_valid( &stream->inputSampleSpec ) ) + { + PA_DEBUG( ("Portaudio %s: Invalid input audio spec!\n", + __FUNCTION__) ); + result = paUnanticipatedHostError; /* should have been caught above */ + goto openstream_error; + } + + stream->inputStream = + pa_stream_new( pulseaudioHostApi->context, + stream->inputStreamName, + &stream->inputSampleSpec, + NULL ); + + if( stream->inputStream != NULL ) + { + pa_stream_set_state_callback( stream->inputStream, + PaPulseAudio_StreamStateCb, + stream); + pa_stream_set_started_callback( stream->inputStream, + PaPulseAudio_StreamStartedCb, + stream ); + } + else + { + PA_DEBUG( ("Portaudio %s: Can't alloc input stream!\n", + __FUNCTION__) ); + } + + stream->inputDevice = inputParameters->device; + + /* + * This is too much as most of the time there is not much + * stuff in buffer but it's enough if we are doing blocked + * and reading is somewhat slower than callback + */ + result = PaPulseAudio_BlockingInitRingBuffer( &stream->inputRing, + (65536 * 4) ); + if( result != paNoError ) + { + goto openstream_error; + } + + } + + else + { + inputChannelCount = 0; + inputSampleFormat = hostInputSampleFormat = paFloat32; + } + + if( outputParameters ) + { + outputChannelCount = outputParameters->channelCount; + outputSampleFormat = outputParameters->sampleFormat; + + /* unless alternate device specification is supported, reject the use of + * paUseHostApiSpecificDeviceSpecification + */ + if( outputParameters->device == paUseHostApiSpecificDeviceSpecification ) + { + result = paInvalidDevice; + goto openstream_error; + } + + /* check that output device can support inputChannelCount */ + if( outputChannelCount > + hostApi->deviceInfos[outputParameters->device]->maxOutputChannels ) + { + result = paInvalidChannelCount; + goto openstream_error; + } + + /* validate outputStreamInfo */ + if( outputParameters->hostApiSpecificStreamInfo ) + { + result = paIncompatibleHostApiSpecificStreamInfo; /* this implementation doesn't use custom stream info */ + goto openstream_error; + } + + /* IMPLEMENT ME - establish which host formats are available */ + hostOutputSampleFormat = + PaUtil_SelectClosestAvailableFormat( outputSampleFormat + /* native formats */ , + outputSampleFormat ); + + stream->outputFrameSize = + Pa_GetSampleSize( outputSampleFormat ) * outputChannelCount; + + result = PaPulseAudio_ConvertPortaudioFormatToPaPulseAudio_( + hostOutputSampleFormat, + &stream->outputSampleSpec ); + + if( result != paNoError ) + { + goto openstream_error; + } + + stream->outputSampleSpec.rate = sampleRate; + stream->outputSampleSpec.channels = outputChannelCount; + stream->outputChannelCount = outputChannelCount; + + if( !pa_sample_spec_valid( &stream->outputSampleSpec ) ) + { + PA_DEBUG( ("Portaudio %s: Invalid audio spec for output!\n", + __FUNCTION__) ); + result = paUnanticipatedHostError; /* should have been caught above */ + goto openstream_error; + } + + stream->outputStream = + pa_stream_new( pulseaudioHostApi->context, + stream->outputStreamName, + &stream->outputSampleSpec, + NULL ); + + if( stream->outputStream != NULL ) + { + pa_stream_set_state_callback( stream->outputStream, + PaPulseAudio_StreamStateCb, + stream ); + pa_stream_set_started_callback( stream->outputStream, + PaPulseAudio_StreamStartedCb, + stream ); + } + + else + { + PA_DEBUG( ("Portaudio %s: Can't alloc output stream!\n", + __FUNCTION__) ); + } + + if( result != paNoError ) + { + goto openstream_error; + } + + if( result != paNoError ) + { + goto openstream_error; + } + + stream->outputDevice = outputParameters->device; + } + + else + { + outputChannelCount = 0; + outputSampleFormat = hostOutputSampleFormat = paFloat32; + } + + stream->hostapi = pulseaudioHostApi; + stream->context = pulseaudioHostApi->context; + stream->mainloop = pulseaudioHostApi->mainloop; + + if( streamCallback ) + { + PaUtil_InitializeStreamRepresentation( &stream->streamRepresentation, + &pulseaudioHostApi->callbackStreamInterface, + streamCallback, + userData ); + } + else + { + PaUtil_InitializeStreamRepresentation( &stream->streamRepresentation, + &pulseaudioHostApi->blockingStreamInterface, + streamCallback, + userData ); + } + + PaUtil_InitializeCpuLoadMeasurer( &stream->cpuLoadMeasurer, + sampleRate + ); + + /* we assume a fixed host buffer size in this example, but the buffer processor + * can also support bounded and unknown host buffer sizes by passing + * paUtilBoundedHostBufferSize or paUtilUnknownHostBufferSize instead of + * paUtilFixedHostBufferSize below. */ + + result = PaUtil_InitializeBufferProcessor( &stream->bufferProcessor, + inputChannelCount, + inputSampleFormat, + hostInputSampleFormat, + outputChannelCount, + outputSampleFormat, + hostOutputSampleFormat, + sampleRate, + streamFlags, + framesPerBuffer, + framesPerHostBuffer, + paUtilUnknownHostBufferSize, + streamCallback, + userData ); + + if( result != paNoError ) + { + goto openstream_error; + } + + /* inputLatency is specified in _seconds_ */ + stream->streamRepresentation.streamInfo.inputLatency = + (PaTime) PaUtil_GetBufferProcessorInputLatencyFrames( + &stream->bufferProcessor ) / sampleRate; + + /* outputLatency is specified in _seconds_ */ + stream->streamRepresentation.streamInfo.outputLatency = + (PaTime) PaUtil_GetBufferProcessorOutputLatencyFrames( + &stream->bufferProcessor ) / sampleRate; + + stream->streamRepresentation.streamInfo.sampleRate = sampleRate; + + stream->maxFramesHostPerBuffer = framesPerBuffer; + stream->maxFramesPerBuffer = framesPerBuffer; + + *s = (PaStream *) stream; + + openstream_end: + PaPulseAudio_UnLock( pulseaudioHostApi->mainloop ); + return result; + + openstream_error: + + if( stream ) + { + PaUtil_FreeMemory( stream->inputStreamName ); + PaUtil_FreeMemory( stream->outputStreamName ); + PaUtil_FreeMemory( stream ); + } + + goto openstream_end; +} + +PaError IsStreamStopped( PaStream * s ) +{ + PaPulseAudio_Stream *stream = (PaPulseAudio_Stream *) s; + return stream->isStopped; +} + + +PaError IsStreamActive( PaStream * s ) +{ + PaPulseAudio_Stream *stream = (PaPulseAudio_Stream *) s; + return stream->isActive; +} + + +PaTime GetStreamTime( PaStream * s ) +{ + PaPulseAudio_Stream *stream = (PaPulseAudio_Stream *) s; + PaPulseAudio_HostApiRepresentation *pulseaudioHostApi = stream->hostapi; + PaStreamCallbackTimeInfo timeInfo = { 0, 0, 0 }; + + PaPulseAudio_Lock( pulseaudioHostApi->mainloop ); + + if( stream->outputStream ) + { + if( PaPulseAudio_updateTimeInfo( stream->outputStream, + &timeInfo, + 0 ) == -PA_ERR_NODATA ) + { + return 0; + } + } + + if( stream->inputStream ) + { + if( PaPulseAudio_updateTimeInfo( stream->inputStream, + &timeInfo, + 1 ) == -PA_ERR_NODATA ) + { + return 0; + } + } + + PaPulseAudio_UnLock( pulseaudioHostApi->mainloop ); + return timeInfo.currentTime; +} + + +double GetStreamCpuLoad( PaStream * s ) +{ + PaPulseAudio_Stream *stream = (PaPulseAudio_Stream *) s; + + return PaUtil_GetCpuLoad( &stream->cpuLoadMeasurer ); +} + +/** Extensions */ +static void RenameStreamCb(pa_stream *s, int success, void *userdata) +{ + /* Currently does nothing but signal the caller. */ + PaPulseAudio_Stream *l_ptrStream = (PaPulseAudio_Stream *) userdata; + pa_threaded_mainloop_signal( l_ptrStream->mainloop, + 0 ); +} + +PaError PaPulseAudio_RenameSource( PaStream *s, const char *streamName ) +{ + PaPulseAudio_Stream *stream = (PaPulseAudio_Stream *) s; + PaError result = paNoError; + pa_operation *op = NULL; + + if ( stream->inputStream == NULL ) + { + return paInvalidDevice; + } + + /* Reallocate stream name in memory. */ + PaPulseAudio_Lock( stream->mainloop ); + char *newStreamName = (char*)PaUtil_AllocateZeroInitializedMemory( strnlen( streamName, PAPULSEAUDIO_MAX_DEVICENAME ) + 1 ); + if ( !newStreamName ) + { + PaPulseAudio_UnLock( stream->mainloop ); + return paInsufficientMemory; + } + snprintf( newStreamName, strnlen( streamName, PAPULSEAUDIO_MAX_DEVICENAME ) + 1, "%s", streamName ); + + PaUtil_FreeMemory( stream->inputStreamName ); + stream->inputStreamName = newStreamName; + + op = pa_stream_set_name( stream->inputStream, streamName, RenameStreamCb, stream ); + PaPulseAudio_UnLock( stream->mainloop ); + + /* Wait for completion. */ + while (pa_operation_get_state( op ) == PA_OPERATION_RUNNING) + { + pa_threaded_mainloop_wait( stream->mainloop ); + } + + return result; +} + +PaError PaPulseAudio_RenameSink( PaStream *s, const char *streamName ) +{ + PaPulseAudio_Stream *stream = (PaPulseAudio_Stream *) s; + PaError result = paNoError; + pa_operation *op = NULL; + + if ( stream->outputStream == NULL ) + { + return paInvalidDevice; + } + + /* Reallocate stream name in memory. */ + PaPulseAudio_Lock( stream->mainloop ); + char *newStreamName = (char*)PaUtil_AllocateZeroInitializedMemory( strnlen( streamName, PAPULSEAUDIO_MAX_DEVICENAME ) + 1 ); + if ( !newStreamName ) + { + PaPulseAudio_UnLock( stream->mainloop ); + return paInsufficientMemory; + } + snprintf( newStreamName, strnlen( streamName, PAPULSEAUDIO_MAX_DEVICENAME ) + 1, "%s", streamName ); + + PaUtil_FreeMemory( stream->outputStreamName ); + stream->outputStreamName = newStreamName; + + op = pa_stream_set_name( stream->outputStream, streamName, RenameStreamCb, stream ); + PaPulseAudio_UnLock( stream->mainloop ); + + /* Wait for completion. */ + while (pa_operation_get_state( op ) == PA_OPERATION_RUNNING) + { + pa_threaded_mainloop_wait( stream->mainloop ); + } + + return result; +} diff --git a/src/hostapi/pulseaudio/pa_linux_pulseaudio_block.c b/src/hostapi/pulseaudio/pa_linux_pulseaudio_block.c new file mode 100644 index 000000000..3ef9b458c --- /dev/null +++ b/src/hostapi/pulseaudio/pa_linux_pulseaudio_block.c @@ -0,0 +1,197 @@ + +/* + * PulseAudio host to play natively in Linux based systems without + * ALSA emulation + * + * Copyright (c) 2014-2023 Tuukka Pasanen + * Copyright (c) 2016 Sqweek + * + * Based on the Open Source API proposed by Ross Bencina + * Copyright (c) 1999-2002 Ross Bencina, Phil Burk + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files + * (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + * ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +/* + * The text above constitutes the entire PortAudio license; however, + * the PortAudio community also makes the following non-binding requests: + * + * Any person wishing to distribute modifications to the Software is + * requested to send the modifications to the original developer so that + * they can be incorporated into the canonical version. It is also + * requested that these non-binding requests be included along with the + * license above. + */ + +/** @file + @ingroup common_src + + @brief PulseAudio implementation of support for a host API. + + This host API implements PulseAudio support for portaudio + it has callbackmode and normal write mode support +*/ + +#include "pa_linux_pulseaudio_block_internal.h" +#include + +/* + As separate stream interfaces are used for blocking and callback + streams, the following functions can be guaranteed to only be called + for blocking streams. +*/ + +PaError PaPulseAudio_ReadStreamBlock( PaStream * s, + void *buffer, + unsigned long frames ) +{ + PaPulseAudio_Stream *pulseaudioStream = (PaPulseAudio_Stream *) s; + PaPulseAudio_HostApiRepresentation *pulseaudioHostApi = + pulseaudioStream->hostapi; + PaError ret = 0; + uint8_t *readableBuffer = (uint8_t *) buffer; + long bufferLeftToRead = (frames * pulseaudioStream->inputFrameSize); + + while( bufferLeftToRead > 0 ) + { + PA_PULSEAUDIO_IS_ERROR( pulseaudioStream, paStreamIsStopped ) + + PaPulseAudio_Lock( pulseaudioStream->mainloop ); + long l_read = PaUtil_ReadRingBuffer( &pulseaudioStream->inputRing, readableBuffer, + bufferLeftToRead ); + readableBuffer += l_read; + bufferLeftToRead -= l_read; + if( bufferLeftToRead > 0 ) + pa_threaded_mainloop_wait( pulseaudioStream->mainloop ); + + PaPulseAudio_UnLock( pulseaudioStream->mainloop ); + + if( bufferLeftToRead > 0 ) + { + /* Sleep small amount of time not burn CPU + * we block anyway so this is bearable + */ + usleep(100); + } + } + return paNoError; +} + + +PaError PaPulseAudio_WriteStreamBlock( PaStream * s, + const void *buffer, + unsigned long frames ) +{ + PaPulseAudio_Stream *pulseaudioStream = (PaPulseAudio_Stream *) s; + int ret = 0; + size_t pulseaudioWritable = 0; + uint8_t *writableBuffer = (uint8_t *) buffer; + long bufferLeftToWrite = (frames * pulseaudioStream->outputFrameSize); + pa_operation *pulseaudioOperation = NULL; + + PaUtil_BeginCpuLoadMeasurement( &pulseaudioStream->cpuLoadMeasurer ); + + while( bufferLeftToWrite > 0) + { + PA_PULSEAUDIO_IS_ERROR( pulseaudioStream, paStreamIsStopped ) + + PaPulseAudio_Lock( pulseaudioStream->mainloop ); + pulseaudioWritable = pa_stream_writable_size( pulseaudioStream->outputStream ); + PaPulseAudio_UnLock( pulseaudioStream->mainloop ); + + if( pulseaudioWritable > 0 ) + { + if( bufferLeftToWrite < pulseaudioWritable ) + { + pulseaudioWritable = bufferLeftToWrite; + } + PaPulseAudio_Lock( pulseaudioStream->mainloop ); + ret = pa_stream_write( pulseaudioStream->outputStream, + writableBuffer, + pulseaudioWritable, + NULL, + 0, + PA_SEEK_RELATIVE ); + + pulseaudioOperation = pa_stream_update_timing_info( pulseaudioStream->outputStream, + NULL, + NULL ); + PaPulseAudio_UnLock( pulseaudioStream->mainloop ); + + ret = 0; + + if( pulseaudioOperation == NULL ) + { + return paInsufficientMemory; + } + + while( pa_operation_get_state( pulseaudioOperation ) == PA_OPERATION_RUNNING ) + { + ret ++; + PA_PULSEAUDIO_IS_ERROR( pulseaudioStream, paStreamIsStopped ) + + /* As this shouldn never happen it's error if it does */ + if( ret >= 10000 ) + { + return paStreamIsStopped; + } + + usleep(100); + } + + PaPulseAudio_Lock( pulseaudioStream->mainloop ); + + pa_operation_unref( pulseaudioOperation ); + pulseaudioOperation = NULL; + + PaPulseAudio_UnLock( pulseaudioStream->mainloop ); + + writableBuffer += pulseaudioWritable; + bufferLeftToWrite -= pulseaudioWritable; + } + + if( bufferLeftToWrite > 0 ) + { + /* Sleep small amount of time not burn CPU + * we block anyway so this is bearable + */ + usleep(100); + } + + } + PaUtil_EndCpuLoadMeasurement( &pulseaudioStream->cpuLoadMeasurer, + frames ); + + return paNoError; +} + + +signed long PaPulseAudio_GetStreamReadAvailableBlock( PaStream * s ) +{ + PaPulseAudio_Stream *pulseaudioStream = (PaPulseAudio_Stream *) s; + + if( pulseaudioStream->inputStream == NULL ) + { + return 0; + } + + return ( PaUtil_GetRingBufferReadAvailable( &pulseaudioStream->inputRing ) / + pulseaudioStream->inputFrameSize ); +} diff --git a/src/hostapi/pulseaudio/pa_linux_pulseaudio_block_internal.h b/src/hostapi/pulseaudio/pa_linux_pulseaudio_block_internal.h new file mode 100644 index 000000000..1c1bb092c --- /dev/null +++ b/src/hostapi/pulseaudio/pa_linux_pulseaudio_block_internal.h @@ -0,0 +1,91 @@ + +/* + * PulseAudio host to play natively in Linux based systems without + * ALSA emulation + * + * Copyright (c) 2014-2023 Tuukka Pasanen + * Copyright (c) 2016 Sqweek + * + * Based on the Open Source API proposed by Ross Bencina + * Copyright (c) 1999-2002 Ross Bencina, Phil Burk + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files + * (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + * ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +/* + * The text above constitutes the entire PortAudio license; however, + * the PortAudio community also makes the following non-binding requests: + * + * Any person wishing to distribute modifications to the Software is + * requested to send the modifications to the original developer so that + * they can be incorporated into the canonical version. It is also + * requested that these non-binding requests be included along with the + * license above. + */ + +#ifndef _PA_HOSTAPI_PULSEAUDIO_BLOCK_H_ +#define _PA_HOSTAPI_PULSEAUDIO_BLOCK_H_ + +#include "pa_util.h" +#include "pa_allocation.h" +#include "pa_hostapi.h" +#include "pa_stream.h" +#include "pa_cpuload.h" +#include "pa_process.h" + +#include "pa_unix_util.h" +#include "pa_ringbuffer.h" + +/* PulseAudio headers */ +#include +#include +#include + +#include "pa_linux_pulseaudio_internal.h" + +#ifdef __cplusplus +extern "C" +{ +#endif /* __cplusplus */ + +PaError PaPulseAudio_CloseStreamBlock( PaStream * stream ); + +PaError PaPulseAudio_StartStreamBlock( PaStream * stream ); + +PaError PaPulseAudio_StopStreamBlock( PaStream * stream ); + +PaError PaPulseAudio_AbortStreamBlock( PaStream * stream ); + +PaError PaPulseAudio_ReadStreamBlock( PaStream * stream, + void *buffer, + unsigned long frames ); + +PaError PaPulseAudio_WriteStreamBlock( PaStream * stream, + const void *buffer, + unsigned long frames ); + +signed long PaPulseAudio_GetStreamReadAvailableBlock( PaStream * stream ); + +#ifdef __cplusplus +} +#endif /* __cplusplus */ + + +#endif diff --git a/src/hostapi/pulseaudio/pa_linux_pulseaudio_cb.c b/src/hostapi/pulseaudio/pa_linux_pulseaudio_cb.c new file mode 100644 index 000000000..18da32289 --- /dev/null +++ b/src/hostapi/pulseaudio/pa_linux_pulseaudio_cb.c @@ -0,0 +1,971 @@ + +/* + * PulseAudio host to play natively in Linux based systems without + * ALSA emulation + * + * Copyright (c) 2014-2023 Tuukka Pasanen + * Copyright (c) 2016 Sqweek + * + * Based on the Open Source API proposed by Ross Bencina + * Copyright (c) 1999-2002 Ross Bencina, Phil Burk + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files + * (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + * ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +/* + * The text above constitutes the entire PortAudio license; however, + * the PortAudio community also makes the following non-binding requests: + * + * Any person wishing to distribute modifications to the Software is + * requested to send the modifications to the original developer so that + * they can be incorporated into the canonical version. It is also + * requested that these non-binding requests be included along with the + * license above. + */ + +/** @file + @ingroup common_src + + @brief PulseAudio implementation of support for a host API. + + This host API implements PulseAudio support for portaudio + it has callback mode and normal write mode support +*/ + + +#include "pa_util.h" +#include "pa_allocation.h" +#include "pa_hostapi.h" +#include "pa_stream.h" +#include "pa_cpuload.h" +#include "pa_process.h" + +#include "pa_unix_util.h" +#include "pa_ringbuffer.h" + +#include "pa_linux_pulseaudio_cb_internal.h" + + +/* PulseAudio headers */ +#include +#include + +int PaPulseAudio_updateTimeInfo( pa_stream * s, + PaStreamCallbackTimeInfo *timeInfo, + int record ) +{ + unsigned int isNegative = 0; + pa_usec_t pulseaudioStreamTime = 0; + pa_usec_t pulseaudioStreamLatency = 0; + + if( pa_stream_get_time( s, + &pulseaudioStreamTime ) == -PA_ERR_NODATA ) + { + return -PA_ERR_NODATA; + } + else + { + timeInfo->currentTime = ((PaTime) pulseaudioStreamTime / (PaTime) 1000000); + } + + if( pa_stream_get_latency( s, + &pulseaudioStreamLatency, + &isNegative ) == -PA_ERR_NODATA ) + { + return -PA_ERR_NODATA; + } + else + { + if( record == 0 ) + { + timeInfo->outputBufferDacTime = timeInfo->currentTime + ((PaTime) pulseaudioStreamLatency / (PaTime) 1000000); + } + else + { + timeInfo->inputBufferAdcTime = timeInfo->currentTime - ((PaTime) pulseaudioStreamLatency / (PaTime) 1000000); + } + } + return 0; +} + + +/* locks the Pulse Main loop when not called from it */ +void PaPulseAudio_Lock( pa_threaded_mainloop *mainloop ) +{ + if( !pa_threaded_mainloop_in_thread( mainloop ) ) { + pa_threaded_mainloop_lock( mainloop ); + } +} + +/* unlocks the Pulse Main loop when not called from it */ +void PaPulseAudio_UnLock( pa_threaded_mainloop *mainloop ) +{ + if( !pa_threaded_mainloop_in_thread( mainloop ) ) { + pa_threaded_mainloop_unlock( mainloop ); + } +} + +void _PaPulseAudio_WriteRingBuffer( PaUtilRingBuffer *ringbuffer, + const void *buffer, + size_t length ) +{ + /* + * If there is not enough room. Read from ringbuffer to make + * sure that if not full and audio will just underrun + * + * If you try to read too much and there is no room then this + * will fail. But I don't know how to get into that? + */ + if( PaUtil_GetRingBufferWriteAvailable( ringbuffer ) < length ) + { + uint8_t tmpBuffer[ PULSEAUDIO_BUFFER_SIZE ]; + PaUtil_ReadRingBuffer( ringbuffer, + tmpBuffer, + length ); + } + + PaUtil_WriteRingBuffer( ringbuffer, + buffer, + length ); + +} + +void _PaPulseAudio_Read( PaPulseAudio_Stream *stream, + size_t length ) +{ + const void *pulseaudioData = NULL; + + /* + * Idea behind this is that we fill ringbuffer with data + * that comes from input device. When it's available + * we'll fill it to callback or blocking reading + */ + if( pa_stream_peek( stream->inputStream, + &pulseaudioData, + &length )) + { + PA_DEBUG( ("Portaudio %s: Can't read audio!\n", + __FUNCTION__) ); + } + else + { + _PaPulseAudio_WriteRingBuffer( &stream->inputRing, pulseaudioData, length ); + } + + pa_stream_drop( stream->inputStream ); + + pulseaudioData = NULL; + +} + +static int _PaPulseAudio_ProcessAudio(PaPulseAudio_Stream *stream, + size_t length) +{ + uint8_t pulseaudioSampleBuffer[PULSEAUDIO_BUFFER_SIZE]; + size_t hostFramesPerBuffer = stream->bufferProcessor.framesPerHostBuffer; + size_t pulseaudioOutputBytes = 0; + size_t pulseaudioInputBytes = 0; + size_t hostFrameCount = 0; + int isOutputCb = 0; + int isInputCb = 0; + PaStreamCallbackTimeInfo timeInfo; + int ret = paContinue; + void *bufferData = NULL; + size_t pulseaudioOutputWritten = 0; + + /* If there is no specified per host buffer then + * just generate one or but correct one in place + */ + if( hostFramesPerBuffer == paFramesPerBufferUnspecified ) + { + if( !stream->framesPerHostCallback ) + { + /* This just good enough and most + * Pulseaudio server and ALSA can handle it + * + * We should never get here but this is ultimate + * backup. + */ + hostFramesPerBuffer = PAPULSEAUDIO_FRAMESPERBUFFERUNSPEC; + + stream->framesPerHostCallback = hostFramesPerBuffer; + } + else + { + hostFramesPerBuffer = stream->framesPerHostCallback; + } + } + + if( stream->outputStream ) + { + /* Calculate how many bytes goes to one frame */ + pulseaudioInputBytes = pulseaudioOutputBytes = (hostFramesPerBuffer * stream->outputFrameSize); + + if( stream->bufferProcessor.streamCallback ) + { + isOutputCb = 1; + } + } + + /* If we just want to have input but not output (Not Duplex) + * Use this calculation + */ + if( stream->inputStream ) + { + pulseaudioInputBytes = pulseaudioOutputBytes = (hostFramesPerBuffer * stream->inputFrameSize); + + if( stream->bufferProcessor.streamCallback ) + { + isInputCb = 1; + } + } + + /* + * When stopped we should stop feeding or recording right away + */ + if( stream->isStopped ) + { + return paStreamIsStopped; + } + + /* + * This can be called before we have reached out + * starting Portaudio stream or Portaudio stream + * is stopped + */ + if( !stream->pulseaudioIsActive ) + { + if(stream->outputStream) + { + bufferData = pulseaudioSampleBuffer; + memset( bufferData, 0x00, length); + + pa_stream_write( stream->outputStream, + bufferData, + length, + NULL, + 0, + PA_SEEK_RELATIVE ); + } + + return paContinue; + } + + /* If we have input which is mono and + * output which is stereo. We have to copy + * mono to monomono which is stereo. + * Then just read half and copy + */ + if( isOutputCb && + stream->outputSampleSpec.channels == 2 && + stream->inputSampleSpec.channels == 1) + { + pulseaudioInputBytes /= 2; + } + + while(1) + { + /* + * If everything fail like stream vanish or mainloop + * is in error state try to handle error + */ + PA_PULSEAUDIO_IS_ERROR( stream, paStreamIsStopped ) + + /* There is only Record stream so + * see if we have enough stuff to feed record stream + * If not then bail out. + */ + if( isInputCb && + PaUtil_GetRingBufferReadAvailable(&stream->inputRing) < pulseaudioInputBytes ) + { + if(isOutputCb && (pulseaudioOutputWritten < length) && !stream->missedBytes) + { + stream->missedBytes = length - pulseaudioOutputWritten; + } + else + { + stream->missedBytes = 0; + } + break; + } + else if( pulseaudioOutputWritten >= length) + { + stream->missedBytes = 0; + break; + } + + if( stream->outputStream ) + { + PaPulseAudio_updateTimeInfo( stream->outputStream, + &timeInfo, + 0 ); + } + + if( stream->inputStream ) + { + PaPulseAudio_updateTimeInfo( stream->inputStream, + &timeInfo, + 1 ); + } + + PaUtil_BeginCpuLoadMeasurement( &stream->cpuLoadMeasurer ); + + /* When doing Portaudio Duplex one has to write and read same amount of data + * if not done that way Portaudio will go boo boo and nothing works. + * This is why this is done as it's done + * + * Pulseaudio does not care and this is something that needs small adjusting + */ + PaUtil_BeginBufferProcessing( &stream->bufferProcessor, + &timeInfo, + 0 ); + + /* Read of ther is something to read */ + if( isInputCb ) + { + PaUtil_ReadRingBuffer( &stream->inputRing, + pulseaudioSampleBuffer, + pulseaudioInputBytes); + + PaUtil_SetInterleavedInputChannels( &stream->bufferProcessor, + 0, + pulseaudioSampleBuffer, + stream->inputSampleSpec.channels ); + + PaUtil_SetInputFrameCount( &stream->bufferProcessor, + hostFramesPerBuffer ); + } + + if( isOutputCb ) + { + + size_t tmpSize = pulseaudioOutputBytes; + + /* Allocate memory to make it faster to output stuff */ + pa_stream_begin_write( stream->outputStream, &bufferData, &tmpSize ); + + /* If bufferData is NULL then output is not ready + * and we have to wait for it + */ + if(!bufferData) + { + return paNotInitialized; + } + + PaUtil_SetInterleavedOutputChannels( &stream->bufferProcessor, + 0, + bufferData, + stream->outputChannelCount ); + + PaUtil_SetOutputFrameCount( &stream->bufferProcessor, + hostFramesPerBuffer ); + + if( pa_stream_write( stream->outputStream, + bufferData, + pulseaudioOutputBytes, + NULL, + 0, + PA_SEEK_RELATIVE ) ) + { + PA_DEBUG( ("Portaudio %s: Can't write audio!\n", + __FUNCTION__) ); + } + + pulseaudioOutputWritten += pulseaudioOutputBytes; + } + + hostFrameCount = + PaUtil_EndBufferProcessing( &stream->bufferProcessor, + &ret ); + + PaUtil_EndCpuLoadMeasurement( &stream->cpuLoadMeasurer, + hostFrameCount ); + } + + return ret; +} + +void PaPulseAudio_StreamRecordCb( pa_stream * s, + size_t length, + void *userdata ) +{ + PaPulseAudio_Stream *pulseaudioStream = (PaPulseAudio_Stream *) userdata; + + if( !pulseaudioStream->pulseaudioIsActive ) + { + pulseaudioStream->pulseaudioIsActive = 1; + pulseaudioStream->pulseaudioIsStopped= 0; + } + + _PaPulseAudio_Read( pulseaudioStream, length ); + + /* Let's handle when output happens if Duplex + * + * Also there is no callback there is no meaning to continue + * as we have blocking reading + */ + if( pulseaudioStream->bufferProcessor.streamCallback ) + { + _PaPulseAudio_ProcessAudio( pulseaudioStream, length ); + } + + pa_threaded_mainloop_signal( pulseaudioStream->mainloop, + 0 ); +} + +void PaPulseAudio_StreamPlaybackCb( pa_stream * s, + size_t length, + void *userdata ) +{ + PaPulseAudio_Stream *pulseaudioStream = (PaPulseAudio_Stream *) userdata; + + if( !pulseaudioStream->inputStream && !pulseaudioStream->pulseaudioIsActive ) + { + pulseaudioStream->pulseaudioIsActive = 1; + pulseaudioStream->pulseaudioIsStopped = 0; + } + + if( pulseaudioStream->bufferProcessor.streamCallback ) + { + _PaPulseAudio_ProcessAudio( pulseaudioStream, length ); + } + + pa_threaded_mainloop_signal( pulseaudioStream->mainloop, + 0 ); +} + +/* This is left for future use! */ +static void PaPulseAudio_StreamSuccessCb( pa_stream * s, + int success, + void *userdata ) +{ + PaPulseAudio_Stream *pulseaudioStream = (PaPulseAudio_Stream *) userdata; + PA_DEBUG( ("Portaudio %s: %d\n", __FUNCTION__, + success) ); + pa_threaded_mainloop_signal( pulseaudioStream->mainloop, + 0 ); +} + +/* This is left for future use! */ +static void PaPulseAudio_CorkSuccessCb( + pa_stream * s, + int success, + void *userdata +) +{ + PaPulseAudio_Stream *pulseaudioStream = (PaPulseAudio_Stream *) userdata; + pa_threaded_mainloop_signal( pulseaudioStream->mainloop, + 0 ); +} + + +/* This is left for future use! */ +void PaPulseAudio_StreamStartedCb( pa_stream * stream, + void *userdata ) +{ + PaPulseAudio_Stream *pulseaudioStream = (PaPulseAudio_Stream *) userdata; + pa_threaded_mainloop_signal( pulseaudioStream->mainloop, + 0 ); +} + +/* + When CloseStream() is called, the multi-api layer ensures that + the stream has already been stopped or aborted. +*/ +PaError PaPulseAudio_CloseStreamCb( PaStream * s ) +{ + PaError result = paNoError; + PaPulseAudio_Stream *stream = (PaPulseAudio_Stream *) s; + PaPulseAudio_HostApiRepresentation *pulseaudioHostApi = stream->hostapi; + pa_operation *pulseaudioOperation = NULL; + int waitLoop = 0; + int pulseaudioError = 0; + + /* Wait for stream to be stopped */ + stream->isActive = 0; + stream->isStopped = 1; + stream->pulseaudioIsActive = 0; + stream->pulseaudioIsStopped = 1; + + if( stream->outputStream != NULL + && pa_stream_get_state( stream->outputStream ) == PA_STREAM_READY ) + { + PaPulseAudio_Lock(stream->mainloop); + /** + * Pause stream so it stops faster + */ + pulseaudioOperation = pa_stream_cork( stream->outputStream, + 1, + PaPulseAudio_CorkSuccessCb, + stream ); + + PaPulseAudio_UnLock( stream->mainloop ); + + while( pa_operation_get_state( pulseaudioOperation ) == PA_OPERATION_RUNNING ) + { + pa_threaded_mainloop_wait( pulseaudioHostApi->mainloop ); + waitLoop ++; + + if(waitLoop > 250) + { + break; + } + } + + waitLoop = 0; + + PaPulseAudio_Lock(stream->mainloop); + pa_operation_unref( pulseaudioOperation ); + pulseaudioOperation = NULL; + + pa_stream_disconnect( stream->outputStream ); + PaPulseAudio_UnLock( stream->mainloop ); + } + + if( stream->inputStream != NULL + && pa_stream_get_state( stream->inputStream ) == PA_STREAM_READY ) + { + PaPulseAudio_Lock( stream->mainloop ); + + /** + * Pause stream so it stops so it stops faster + */ + pulseaudioOperation = pa_stream_cork( stream->inputStream, + 1, + PaPulseAudio_CorkSuccessCb, + stream ); + + PaPulseAudio_UnLock( stream->mainloop ); + + while( pa_operation_get_state( pulseaudioOperation ) == PA_OPERATION_RUNNING ) + { + pa_threaded_mainloop_wait( pulseaudioHostApi->mainloop ); + waitLoop ++; + + if(waitLoop > 250) + { + break; + } + } + + waitLoop = 0; + + PaPulseAudio_Lock( stream->mainloop ); + pa_operation_unref( pulseaudioOperation ); + pulseaudioOperation = NULL; + + /* Then we disconnect stream and wait for + * Termination + */ + pa_stream_disconnect( stream->inputStream ); + + PaPulseAudio_UnLock( stream->mainloop ); + + } + + /* Wait for termination for both */ + while(!waitLoop) + { + PaPulseAudio_Lock( stream->mainloop ); + if( stream->inputStream != NULL + && pa_stream_get_state( stream->inputStream ) == PA_STREAM_TERMINATED ) + { + pa_stream_unref( stream->inputStream ); + stream->inputStream = NULL; + } + PaPulseAudio_UnLock( stream->mainloop ); + + PaPulseAudio_Lock( stream->mainloop ); + if( stream->outputStream != NULL + && pa_stream_get_state(stream->outputStream) == PA_STREAM_TERMINATED ) + { + pa_stream_unref( stream->outputStream ); + stream->outputStream = NULL; + } + PaPulseAudio_UnLock( stream->mainloop ); + + if((stream->outputStream == NULL + && stream->inputStream == NULL) + || pulseaudioError >= 5000 ) + { + waitLoop = 1; + } + + pulseaudioError ++; + usleep(10000); + } + + PaUtil_TerminateBufferProcessor( &stream->bufferProcessor ); + PaUtil_TerminateStreamRepresentation( &stream->streamRepresentation ); + + PaUtil_FreeMemory( stream->inputStreamName ); + PaUtil_FreeMemory( stream->outputStreamName ); + PaUtil_FreeMemory( stream ); + + return result; +} + +PaError PaPulseAudio_StartStreamCb( PaStream * s ) +{ + PaError ret = paNoError; + PaPulseAudio_Stream *stream = (PaPulseAudio_Stream *) s; + int pulseaudioPlaybackStarted = 0; + int pulseaudioRecordStarted = 0; + pa_stream_state_t pulseaudioState = PA_STREAM_UNCONNECTED; + PaPulseAudio_HostApiRepresentation *pulseaudioHostApi = stream->hostapi; + const char *pulseaudioName = NULL; + pa_operation *pulseaudioOperation = NULL; + int waitLoop = 0; + unsigned int pulseaudioReqFrameSize = (1024 * 2); + + stream->isActive = 0; + stream->isStopped = 1; + stream->pulseaudioIsActive = 0; + stream->pulseaudioIsStopped = 1; + stream->missedBytes = 0; + + /* Ready the processor */ + PaUtil_ResetBufferProcessor( &stream->bufferProcessor ); + + PaPulseAudio_Lock( pulseaudioHostApi->mainloop ); + /* Adjust latencies if that is wanted + * https://www.freedesktop.org/wiki/Software/PulseAudio/Documentation/Developer/Clients/LatencyControl/ + * + * tlength is for Playback + * fragsize if for Record + */ + stream->outputBufferAttr.maxlength = (uint32_t)-1; + stream->inputBufferAttr.maxlength = (uint32_t)-1; + + stream->outputBufferAttr.tlength = (uint32_t)-1; + stream->inputBufferAttr.tlength = (uint32_t)-1; + + stream->outputBufferAttr.fragsize = (uint32_t)-1; + stream->inputBufferAttr.fragsize = (uint32_t)-1; + + stream->outputBufferAttr.prebuf = (uint32_t)-1; + stream->inputBufferAttr.prebuf = (uint32_t)-1; + + stream->outputBufferAttr.minreq = (uint32_t)-1; + stream->inputBufferAttr.minreq = (uint32_t)-1; + + stream->outputUnderflows = 0; + PaPulseAudio_UnLock( pulseaudioHostApi->mainloop ); + + pa_stream_flags_t pulseaudioStreamFlags = PA_STREAM_INTERPOLATE_TIMING | + PA_STREAM_AUTO_TIMING_UPDATE | + PA_STREAM_ADJUST_LATENCY | + PA_STREAM_NO_REMIX_CHANNELS | + PA_STREAM_NO_REMAP_CHANNELS | + PA_STREAM_DONT_MOVE; + + if( stream->inputStream ) + { + /* Default input reads 65,535 bytes setting fragsize + * fragments request to smaller chunks of data so it's + * easier to get nicer looking timestamps with current + * callback system + */ + stream->inputBufferAttr.fragsize = pa_usec_to_bytes( pulseaudioReqFrameSize, + &stream->inputSampleSpec ); + + if( stream->inputDevice != paNoDevice) + { + PA_DEBUG( ("Portaudio %s: %d (%s)\n", __FUNCTION__, stream->inputDevice, + pulseaudioHostApi->pulseaudioDeviceNames[stream-> + inputDevice]) ); + } + + pa_stream_set_read_callback( stream->inputStream, + PaPulseAudio_StreamRecordCb, + stream ); + + PaDeviceIndex defaultInputDevice; + PaError result = PaUtil_DeviceIndexToHostApiDeviceIndex( + &defaultInputDevice, + pulseaudioHostApi->inheritedHostApiRep.info.defaultInputDevice, + &(pulseaudioHostApi->inheritedHostApiRep) ); + + /* NULL means default device */ + pulseaudioName = NULL; + + /* If default device is not requested then change to wanted device */ + if( result == paNoError && stream->inputDevice != defaultInputDevice ) + { + pulseaudioName = pulseaudioHostApi-> + pulseaudioDeviceNames[stream->inputDevice]; + } + + if ( result == paNoError ) + { + PaPulseAudio_Lock( pulseaudioHostApi->mainloop ); + /* Zero means success */ + if( ! pa_stream_connect_record( stream->inputStream, + pulseaudioName, + &stream->inputBufferAttr, + pulseaudioStreamFlags ) ) + { + pa_stream_set_started_callback( stream->inputStream, + PaPulseAudio_StreamStartedCb, + stream ); + } + else + { + PA_DEBUG( ("Portaudio %s: Can't read audio!\n", + __FUNCTION__) ); + + goto startstreamcb_error; + } + PaPulseAudio_UnLock( pulseaudioHostApi->mainloop ); + + for( waitLoop = 0; waitLoop < 100; waitLoop ++ ) + { + PaPulseAudio_Lock( pulseaudioHostApi->mainloop ); + pulseaudioState = pa_stream_get_state( stream->inputStream ); + PaPulseAudio_UnLock( pulseaudioHostApi->mainloop ); + + if( pulseaudioState == PA_STREAM_READY ) + { + break; + } + else if( pulseaudioState == PA_STREAM_FAILED || + pulseaudioState == PA_STREAM_TERMINATED ) + { + goto startstreamcb_error; + } + + usleep(10000); + } + } + else + { + goto startstreamcb_error; + } + + } + + if( stream->outputStream ) + { + /* tlength does almost the same as fragsize in record. + * See reasoning up there in comments. + * + * In future this should we tuned when things changed + * this just 'good' default + */ + stream->outputBufferAttr.tlength = pa_usec_to_bytes( pulseaudioReqFrameSize, + &stream->outputSampleSpec ); + + pa_stream_set_write_callback( stream->outputStream, + PaPulseAudio_StreamPlaybackCb, + stream ); + + /* Just keep on trucking if we are just corked */ + if( pa_stream_get_state( stream->outputStream ) == PA_STREAM_READY + && pa_stream_is_corked( stream->outputStream ) ) + { + PaPulseAudio_Lock( pulseaudioHostApi->mainloop ); + pulseaudioOperation = pa_stream_cork( stream->outputStream, + 0, + PaPulseAudio_CorkSuccessCb, + stream ); + PaPulseAudio_UnLock( pulseaudioHostApi->mainloop ); + + while( pa_operation_get_state( pulseaudioOperation ) == PA_OPERATION_RUNNING) + { + pa_threaded_mainloop_wait( pulseaudioHostApi->mainloop ); + } + + pa_operation_unref( pulseaudioOperation ); + pulseaudioOperation = NULL; + } + else + { + if( stream->outputDevice != paNoDevice ) + { + PA_DEBUG( ("Portaudio %s: %d (%s)\n", + __FUNCTION__, + stream->outputDevice, + pulseaudioHostApi->pulseaudioDeviceNames[stream-> + outputDevice]) ); + } + + PaDeviceIndex defaultOutputDevice; + PaError result = PaUtil_DeviceIndexToHostApiDeviceIndex( &defaultOutputDevice, + pulseaudioHostApi->inheritedHostApiRep.info.defaultOutputDevice, + &(pulseaudioHostApi->inheritedHostApiRep) ); + + /* NULL means default device */ + pulseaudioName = NULL; + + /* If default device is not requested then change to wanted device */ + if( result == paNoError && stream->outputDevice != defaultOutputDevice ) + { + pulseaudioName = pulseaudioHostApi-> + pulseaudioDeviceNames[stream->outputDevice]; + } + + if(result == paNoError) + { + PaPulseAudio_Lock( pulseaudioHostApi->mainloop ); + + if ( ! pa_stream_connect_playback( stream->outputStream, + pulseaudioName, + &stream->outputBufferAttr, + pulseaudioStreamFlags, + NULL, + NULL ) ) + { + pa_stream_set_underflow_callback( stream->outputStream, + PaPulseAudio_StreamUnderflowCb, + stream); + pa_stream_set_started_callback( stream->outputStream, + PaPulseAudio_StreamStartedCb, + stream ); + } + else + { + PA_DEBUG( ("Portaudio %s: Can't write audio!\n", + __FUNCTION__) ); + goto startstreamcb_error; + } + PaPulseAudio_UnLock( pulseaudioHostApi->mainloop ); + + for( waitLoop = 0; waitLoop < 100; waitLoop ++ ) + { + PaPulseAudio_Lock( pulseaudioHostApi->mainloop ); + pulseaudioState = pa_stream_get_state( stream->outputStream ); + PaPulseAudio_UnLock( pulseaudioHostApi->mainloop ); + + if( pulseaudioState = PA_STREAM_READY ) + { + break; + } + else if( pulseaudioState == PA_STREAM_FAILED || + pulseaudioState == PA_STREAM_TERMINATED ) + { + goto startstreamcb_error; + } + + usleep(10000); + } + + } + else + { + goto startstreamcb_error; + } + } + } + + if( !stream->outputStream && + !stream->inputStream ) + { + PA_DEBUG( ("Portaudio %s: Streams not initialized!\n", + __FUNCTION__) ); + goto startstreamcb_error; + } + + /* Make sure we pass no error on intialize */ + ret = paNoError; + + /* Stream is now active */ + stream->isActive = 1; + stream->isStopped = 0; + + /* Allways unlock.. so we don't get locked */ + startstreamcb_end: + return ret; + + error: + startstreamcb_error: + PA_DEBUG( ("Portaudio %s: Can't start audio!\n", + __FUNCTION__) ); + + if( pulseaudioPlaybackStarted || pulseaudioRecordStarted ) + { + PaPulseAudio_AbortStreamCb( stream ); + } + + stream->isActive = 0; + stream->isStopped = 1; + ret = paNotInitialized; + + goto startstreamcb_end; +} + +static PaError RequestStop( PaPulseAudio_Stream * stream, + int abort ) +{ + PaError ret = paNoError; + PaPulseAudio_HostApiRepresentation *pulseaudioHostApi = stream->hostapi; + pa_operation *pulseaudioOperation = NULL; + + PaPulseAudio_Lock( pulseaudioHostApi->mainloop ); + + /* Wait for stream to be stopped */ + stream->isActive = 0; + stream->isStopped = 1; + stream->pulseaudioIsActive = 0; + stream->pulseaudioIsStopped = 1; + + stream->missedBytes = 0; + + /* Test if there is something that we can play */ + if( stream->outputStream + && pa_stream_get_state( stream->outputStream ) == PA_STREAM_READY + && !pa_stream_is_corked( stream->outputStream ) + && !abort ) + { + pulseaudioOperation = pa_stream_cork( stream->outputStream, + 1, + PaPulseAudio_CorkSuccessCb, + stream ); + + while( pa_operation_get_state( pulseaudioOperation ) == PA_OPERATION_RUNNING ) + { + pa_threaded_mainloop_wait( pulseaudioHostApi->mainloop ); + } + + pa_operation_unref( pulseaudioOperation ); + + pulseaudioOperation = NULL; + } + + requeststop_error: + PaPulseAudio_UnLock( pulseaudioHostApi->mainloop ); + stream->isActive = 0; + stream->isStopped = 1; + stream->pulseaudioIsActive = 0; + stream->pulseaudioIsStopped = 1; + + return ret; +} + +PaError PaPulseAudio_StopStreamCb( PaStream * s ) +{ + return RequestStop( (PaPulseAudio_Stream *) s, + 0 ); +} + +PaError PaPulseAudio_AbortStreamCb( PaStream * s ) +{ + return RequestStop( (PaPulseAudio_Stream *) s, + 1 ); +} diff --git a/src/hostapi/pulseaudio/pa_linux_pulseaudio_cb_internal.h b/src/hostapi/pulseaudio/pa_linux_pulseaudio_cb_internal.h new file mode 100644 index 000000000..74e510de9 --- /dev/null +++ b/src/hostapi/pulseaudio/pa_linux_pulseaudio_cb_internal.h @@ -0,0 +1,97 @@ + +/* + * PulseAudio host to play natively in Linux based systems without + * ALSA emulation + * + * Copyright (c) 2014-2023 Tuukka Pasanen + * Copyright (c) 2016 Sqweek + * + * Based on the Open Source API proposed by Ross Bencina + * Copyright (c) 1999-2002 Ross Bencina, Phil Burk + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files + * (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + * ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +/* + * The text above constitutes the entire PortAudio license; however, + * the PortAudio community also makes the following non-binding requests: + * + * Any person wishing to distribute modifications to the Software is + * requested to send the modifications to the original developer so that + * they can be incorporated into the canonical version. It is also + * requested that these non-binding requests be included along with the + * license above. + */ + +#ifndef _PA_HOSTAPI_PULSEAUDIO_CB_H_ +#define _PA_HOSTAPI_PULSEAUDIO_CB_H_ + +#include "pa_util.h" +#include "pa_allocation.h" +#include "pa_hostapi.h" +#include "pa_stream.h" +#include "pa_cpuload.h" +#include "pa_process.h" + +#include "pa_unix_util.h" +#include "pa_ringbuffer.h" + + +/* PulseAudio headers */ +#include +#include +#include + +#include "pa_linux_pulseaudio_internal.h" + + +#ifdef __cplusplus +extern "C" +{ +#endif /* __cplusplus */ + +int PaPulseAudio_updateTimeInfo( pa_stream * s, + PaStreamCallbackTimeInfo *timeInfo, + int record ); + +void *PaPulseAudio_processThread( void *userdata ); + +PaError PaPulseAudio_CloseStreamCb( PaStream * stream ); + +PaError PaPulseAudio_StartStreamCb( PaStream * stream ); + +PaError PaPulseAudio_StopStreamCb( PaStream * stream ); + +PaError PaPulseAudio_AbortStreamCb( PaStream * stream ); + +void PaPulseAudio_StreamRecordCb( pa_stream * s, + size_t length, + void *userdata ); + +void PaPulseAudio_StreamPlaybackCb( pa_stream * s, + size_t length, + void *userdata ); + +#ifdef __cplusplus +} +#endif /* __cplusplus */ + + +#endif diff --git a/src/hostapi/pulseaudio/pa_linux_pulseaudio_internal.h b/src/hostapi/pulseaudio/pa_linux_pulseaudio_internal.h new file mode 100644 index 000000000..84bff2579 --- /dev/null +++ b/src/hostapi/pulseaudio/pa_linux_pulseaudio_internal.h @@ -0,0 +1,273 @@ + +/* + * PulseAudio host to play natively in Linux based systems without + * ALSA emulation + * + * Copyright (c) 2014-2023 Tuukka Pasanen + * Copyright (c) 2016 Sqweek + * + * Based on the Open Source API proposed by Ross Bencina + * Copyright (c) 1999-2002 Ross Bencina, Phil Burk + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files + * (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + * ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +/* + * The text above constitutes the entire PortAudio license; however, + * the PortAudio community also makes the following non-binding requests: + * + * Any person wishing to distribute modifications to the Software is + * requested to send the modifications to the original developer so that + * they can be incorporated into the canonical version. It is also + * requested that these non-binding requests be included along with the + * license above. + */ + +#ifndef _PA_HOSTAPI_PULSEAUDIO_H_ +#define _PA_HOSTAPI_PULSEAUDIO_H_ + +#include "pa_util.h" +#include "pa_allocation.h" +#include "pa_hostapi.h" +#include "pa_stream.h" +#include "pa_cpuload.h" +#include "pa_process.h" + +#include "pa_unix_util.h" +#include "pa_ringbuffer.h" +#include "pa_debugprint.h" + +/* PulseAudio headers */ +#include +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif /* __cplusplus */ + +/* prototypes for functions declared in this file */ + +#define PA_PULSEAUDIO_SET_LAST_HOST_ERROR(errorCode, errorText) \ + PaUtil_SetLastHostErrorInfo(paInDevelopment, errorCode, errorText) + +#define PAPULSEAUDIO_MAX_DEVICECOUNT 1024 +#define PAPULSEAUDIO_MAX_DEVICENAME 1024 + +/* Default latency values to expose. Chosen by trial and error to be reasonable. */ +#define PA_PULSEAUDIO_DEFAULT_MIN_LATENCY 0.010 +#define PA_PULSEAUDIO_DEFAULT_MAX_LATENCY 0.080 + +/* Just some value that Pulseaudio can handle */ +#define PAPULSEAUDIO_FRAMESPERBUFFERUNSPEC 32 + +/* Assuming of 2 seconds of 44100 Hz sample rate with FLOAT (4 bytes) and stereo channels (2 channels). + You should have pretty good size buffer with this. If output/intput doesn't happens in 2 second we + have more trouble than this buffer. + @todo change this to something more sophisticated */ +#define PULSEAUDIO_BUFFER_SIZE (96100 * 4 * 2) + +typedef struct +{ + PaUtilHostApiRepresentation inheritedHostApiRep; + PaUtilStreamInterface callbackStreamInterface; + PaUtilStreamInterface blockingStreamInterface; + + PaUtilAllocationGroup *allocations; + + PaHostApiIndex hostApiIndex; + PaDeviceInfo deviceInfoArray[PAPULSEAUDIO_MAX_DEVICECOUNT]; + char *pulseaudioDeviceNames[PAPULSEAUDIO_MAX_DEVICECOUNT]; + pa_sample_spec pulseaudioDefaultSampleSpec; + + /* PulseAudio stuff goes here */ + pa_threaded_mainloop *mainloop; + pa_mainloop_api *mainloopApi; + pa_context *context; + int deviceCount; + pa_time_event *timeEvent; +} +PaPulseAudio_HostApiRepresentation; + +/* PaPulseAudio_Stream - a stream data structure specifically for this implementation */ + +typedef struct PaPulseAudio_Stream +{ + PaUtilStreamRepresentation streamRepresentation; + PaUtilCpuLoadMeasurer cpuLoadMeasurer; + PaUtilBufferProcessor bufferProcessor; + PaPulseAudio_HostApiRepresentation *hostapi; + + unsigned long framesPerHostCallback; + pa_threaded_mainloop *mainloop; + pa_context *context; + pa_sample_spec outputSampleSpec; + pa_sample_spec inputSampleSpec; + pa_stream *outputStream; + pa_stream *inputStream; + pa_buffer_attr outputBufferAttr; + pa_buffer_attr inputBufferAttr; + int outputUnderflows; + int outputChannelCount; + int inputChannelCount; + + size_t maxFramesPerBuffer; + size_t maxFramesHostPerBuffer; + int outputFrameSize; + int inputFrameSize; + + PaDeviceIndex inputDevice; + PaDeviceIndex outputDevice; + + char *outputStreamName; + char *inputStreamName; + + PaUtilRingBuffer inputRing; + + size_t missedBytes; + + /* Used in communication between threads + * + * State machine works like this: + * When stream is wanted to start with Pa_StartStream + * then isActive is 1 if opening of devices goes well + * and isStopped is then 0. + * + * When requested to stop isStopped is 1 on isActive is 0 + * and nothing should be written to ouput or read from input + * anymore + * + * Pulseaudio does not like this as it creates streams and they + * start when they are ready and it can be after we have + * exited Pa_StartStream or before if get's kicked up very fast + * + * pulseaudioIsActive and pulseaudioIsStopped are used to find if + * there is stream active or stopped in pulseaudio side. They + * live their own life besides isActive and isStopped to make sure + * that portaudio will have input and output available before + * reading or writing to stream. + */ + volatile sig_atomic_t isActive; + volatile sig_atomic_t isStopped; + volatile sig_atomic_t pulseaudioIsActive; + volatile sig_atomic_t pulseaudioIsStopped; + +} +PaPulseAudio_Stream; + +/* PulseAudio Error checking macro */ +#define PA_PULSEAUDIO_IS_ERROR(pastream, errorCode) \ + if( !(pastream) || \ + !(pastream)->context || \ + !PA_CONTEXT_IS_GOOD( pa_context_get_state( (pastream)->context ) ) || \ + ( (pastream)->outputStream && \ + !PA_STREAM_IS_GOOD( pa_stream_get_state( (pastream)->outputStream ) ) ) || \ + ( (pastream)->inputStream && \ + !PA_STREAM_IS_GOOD( pa_stream_get_state( (pastream)->inputStream ) ) ) ) \ + { \ + if( !(pastream) || \ + ( (pastream)->context && \ + pa_context_get_state( (pastream)->context ) == PA_CONTEXT_FAILED ) || \ + ( (pastream)->outputStream && \ + pa_stream_get_state( (pastream)->outputStream ) == PA_STREAM_FAILED ) || \ + ( (pastream)->inputStream && \ + pa_stream_get_state( (pastream)->inputStream ) == PA_STREAM_FAILED ) ) \ + { \ + return errorCode; \ + } \ + } \ + if( !pastream->isActive || pastream->isStopped ) \ + { \ + return paStreamIsStopped; \ + } + +void PaPulseAudio_Lock( pa_threaded_mainloop *mainloop ); + +void PaPulseAudio_UnLock( pa_threaded_mainloop *mainloop ); + +PaError PaPulseAudio_Initialize( PaUtilHostApiRepresentation ** hostApi, + PaHostApiIndex index ); + +void Terminate( struct PaUtilHostApiRepresentation *hostApi ); + + +PaError IsFormatSupported( struct PaUtilHostApiRepresentation *hostApi, + const PaStreamParameters * inputParameters, + const PaStreamParameters * outputParameters, + double sampleRate ); + +PaError OpenStream( struct PaUtilHostApiRepresentation *hostApi, + PaStream ** s, + const PaStreamParameters * inputParameters, + const PaStreamParameters * outputParameters, + double sampleRate, + unsigned long framesPerBuffer, + PaStreamFlags streamFlags, + PaStreamCallback * streamCallback, + void *userData ); + + +PaError IsStreamStopped( PaStream * s ); +PaError IsStreamActive( PaStream * stream ); + +PaTime GetStreamTime( PaStream * stream ); +double GetStreamCpuLoad( PaStream * stream ); + +PaPulseAudio_HostApiRepresentation *PaPulseAudio_New( void ); +void PaPulseAudio_Free( PaPulseAudio_HostApiRepresentation * ptr ); + +int PaPulseAudio_CheckConnection( PaPulseAudio_HostApiRepresentation * ptr ); + +void PaPulseAudio_CheckContextStateCb( pa_context * c, + void *userdata ); +void PaPulseAudio_ServerInfoCb( pa_context *c, + const pa_server_info *i, + void *userdata ); + +void PaPulseAudio_SinkListCb( pa_context * c, + const pa_sink_info * l, + int eol, + void *userdata ); + +void PaPulseAudio_SourceListCb( pa_context * c, + const pa_source_info * l, + int eol, + void *userdata ); + +void PaPulseAudio_StreamStateCb( pa_stream * s, + void *userdata ); + +void PaPulseAudio_StreamStartedCb( pa_stream * s, + void *userdata ); + +void PaPulseAudio_StreamUnderflowCb( pa_stream * s, + void *userdata ); + +PaError PaPulseAudio_ConvertPortaudioFormatToPaPulseAudio_( PaSampleFormat portaudiosf, + pa_sample_spec * pulseaudiosf +); + +#ifdef __cplusplus +} +#endif /* __cplusplus */ + + +#endif diff --git a/src/os/unix/pa_unix_hostapis.c b/src/os/unix/pa_unix_hostapis.c index 85fefb267..95cd89b9d 100644 --- a/src/os/unix/pa_unix_hostapis.c +++ b/src/os/unix/pa_unix_hostapis.c @@ -43,6 +43,7 @@ #include "pa_hostapi.h" PaError PaJack_Initialize( PaUtilHostApiRepresentation **hostApi, PaHostApiIndex index ); +PaError PaPulseAudio_Initialize( PaUtilHostApiRepresentation **hostApi, PaHostApiIndex index ); PaError PaAlsa_Initialize( PaUtilHostApiRepresentation **hostApi, PaHostApiIndex index ); PaError PaOSS_Initialize( PaUtilHostApiRepresentation **hostApi, PaHostApiIndex index ); PaError PaAudioIO_Initialize( PaUtilHostApiRepresentation **hostApi, PaHostApiIndex index ); @@ -100,6 +101,10 @@ PaUtilHostApiInitializer *paHostApiInitializers[] = PaMacCore_Initialize, #endif +#if PA_USE_PULSEAUDIO + PaPulseAudio_Initialize, +#endif + #if PA_USE_SKELETON PaSkeleton_Initialize, #endif