diff --git a/extern/audaspace/AUTHORS b/extern/audaspace/AUTHORS index f1c740e2007..edf107f835d 100644 --- a/extern/audaspace/AUTHORS +++ b/extern/audaspace/AUTHORS @@ -19,9 +19,15 @@ The Equalizer sound effect has been added by - Marcos Perez +Some performance improvements, especially to the JOSResampler have been made by: + +- Aras Pranckevičius + Several people provided fixes: - Aaron Carlisle - Sebastian Parborg - Leon Zandman - Richard Antalik +- Robert-André Mauchin +- Lalit Shankar Chowdhury diff --git a/extern/audaspace/CHANGES b/extern/audaspace/CHANGES index 9682e1388a2..4218d4f52ce 100644 --- a/extern/audaspace/CHANGES +++ b/extern/audaspace/CHANGES @@ -1,3 +1,30 @@ +Audaspace 1.5 + +- Performance improvements and two more quality presets for the JOS resampler. +- Bugfixes for PulseAudio. +- CoreAudio device is only opened on demand. +- FFMPEG 7 support and dropped support for FFMPEG older than 6. +- Various minor fixes and improvements. + +Detailed list of changes: + +3566597 FFMPEG Update +c158a27 Porting bugfix from Blender. +edb388b Bugfix for PulseAudio: buffers not cleared after pause. +6affec8 Port fix from Blender. +a99262e Load CoreAudio device on demand +ae29ce2 Adding default quality parameter also to JOSResample. +dae2044 Renaming resample quality enum values in the C API. +8c810b5 Add resampling quality parameter to various mixdown functions, and to PySound resample +1b9b7f9 Add ResampleQuality enum and APIs to control resampling quality +4bfd596 Avoid std::string copies by value +0d18fe7 JOSResampleReader performance and faster quality settings (#18) +2a300d9 Filter design python script. +5f745ff Bugfix for reading an animated property with a negative time value. +04eeb56 Fix python documentation. +631850b Update AUTHORS. +db2ff58 Improve seeking for animated sequences + Audaspace 1.4 - Support for OS specific/native audio devices/backends has been added, that is PulseAudio (Linux), WASAPI (Windows) and CoreAudio (MacOS). diff --git a/extern/audaspace/CMakeLists.txt b/extern/audaspace/CMakeLists.txt index 98b02db250b..2abfd339a58 100644 --- a/extern/audaspace/CMakeLists.txt +++ b/extern/audaspace/CMakeLists.txt @@ -23,7 +23,7 @@ endif() project(audaspace) -set(AUDASPACE_VERSION 1.4) +set(AUDASPACE_VERSION 1.5) set(AUDASPACE_LONG_VERSION ${AUDASPACE_VERSION}.0) if(DEFINED AUDASPACE_CMAKE_CFG) @@ -298,6 +298,7 @@ if(AUDASPACE_STANDALONE) endif() if(NOT WIN32 AND NOT APPLE) option(WITH_PULSEAUDIO "Build With PulseAudio" TRUE) + option(WITH_PIPEWIRE "Build With PipeWire" TRUE) endif() if(WIN32) option(WITH_WASAPI "Build With WASAPI" TRUE) @@ -306,9 +307,7 @@ if(AUDASPACE_STANDALONE) if(WITH_STRICT_DEPENDENCIES) set(PACKAGE_OPTION REQUIRED) endif() -endif() -if(AUDASPACE_STANDALONE) if(WIN32 OR APPLE) set(DEFAULT_PLUGIN_PATH "." CACHE STRING "Default plugin installation and loading path.") set(DOCUMENTATION_INSTALL_PATH "doc" CACHE PATH "Path where the documentation is installed.") @@ -316,9 +315,7 @@ if(AUDASPACE_STANDALONE) set(DEFAULT_PLUGIN_PATH "${CMAKE_INSTALL_PREFIX}/share/audaspace/plugins" CACHE STRING "Default plugin installation and loading path.") set(DOCUMENTATION_INSTALL_PATH "share/doc/audaspace" CACHE PATH "Path where the documentation is installed.") endif() -endif() -if(AUDASPACE_STANDALONE) cmake_dependent_option(SEPARATE_C "Build C Binding as separate library" TRUE "WITH_C" FALSE) cmake_dependent_option(PLUGIN_COREAUDIO "Build CoreAudio Plugin" TRUE "WITH_COREAUDIO;SHARED_LIBRARY" FALSE) cmake_dependent_option(PLUGIN_FFMPEG "Build FFMPEG Plugin" TRUE "WITH_FFMPEG;SHARED_LIBRARY" FALSE) @@ -326,18 +323,18 @@ if(AUDASPACE_STANDALONE) cmake_dependent_option(PLUGIN_LIBSNDFILE "Build LibSndFile Plugin" TRUE "WITH_LIBSNDFILE;SHARED_LIBRARY" FALSE) cmake_dependent_option(PLUGIN_OPENAL "Build OpenAL Plugin" TRUE "WITH_OPENAL;SHARED_LIBRARY" FALSE) cmake_dependent_option(PLUGIN_PULSEAUDIO "Build PulseAudio Plugin" TRUE "WITH_PULSEAUDIO;SHARED_LIBRARY" FALSE) + cmake_dependent_option(PLUGIN_PIPEWIRE "Build PipeWire Plugin" TRUE "WITH_PIPEWIRE;SHARED_LIBRARY" FALSE) cmake_dependent_option(PLUGIN_SDL "Build SDL Plugin" TRUE "WITH_SDL;SHARED_LIBRARY" FALSE) cmake_dependent_option(PLUGIN_WASAPI "Build WASAPI Plugin" TRUE "WITH_WASAPI;SHARED_LIBRARY" FALSE) cmake_dependent_option(WITH_PYTHON_MODULE "Build Python Module" TRUE "WITH_PYTHON" FALSE) cmake_dependent_option(USE_SDL2 "Use SDL2 instead of 1 if available" TRUE "WITH_SDL" FALSE) cmake_dependent_option(DYNLOAD_JACK "Dynamically load JACK" FALSE "WITH_JACK" FALSE) cmake_dependent_option(DYNLOAD_PULSEAUDIO "Dynamically load PulseAudio" FALSE "WITH_PULSEAUDIO" FALSE) + cmake_dependent_option(DYNLOAD_PIPEWIRE "Dynamically load PipeWire" FALSE "WITH_PIPEWIRE" FALSE) cmake_dependent_option(WITH_BINDING_DOCS "Build C/Python HTML Documentation with Sphinx" TRUE "WITH_PYTHON_MODULE" FALSE) -endif() + cmake_dependent_option(WITH_VERSIONED_PLUGINS "Build Plugins With Sonumber" TRUE "SHARED_LIBRARY" FALSE) -# compiler options - -if(AUDASPACE_STANDALONE) + # compiler options set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -726,6 +723,47 @@ if(WITH_PULSEAUDIO) endif() endif() +# Pipewire +if(WITH_PIPEWIRE) + if(AUDASPACE_STANDALONE) + find_package(PkgConfig) + pkg_check_modules(PIPEWIRE ${PACKAGE_OPTION} libpipewire-0.3) + endif() + + if(PIPEWIRE_FOUND) + set(PIPEWIRE_SRC + plugins/pipewire/PipeWireDevice.cpp + plugins/pipewire/PipeWireLibrary.cpp + ) + set(PIPEWIRE_HDR + plugins/pipewire/PipeWireDevice.h + plugins/pipewire/PipeWireLibrary.h + plugins/pipewire/PipeWireSymbols.h + ) + + if(DYNLOAD_PIPEWIRE) + add_definitions(-DDYNLOAD_PIPEWIRE) + endif() + + if(NOT PLUGIN_PIPEWIRE) + list(APPEND INCLUDE ${PIPEWIRE_INCLUDE_DIRS}) + if(NOT DYNLOAD_PIPEWIRE) + list(APPEND LIBRARIES ${PIPEWIRE_LIBRARIES}) + endif() + list(APPEND SRC ${PIPEWIRE_SRC}) + list(APPEND HDR ${PIPEWIRE_HDR}) + list(APPEND STATIC_PLUGINS PipeWireDevice) + endif() + else() + if(AUDASPACE_STANDALONE) + set(WITH_PIPEWIRE FALSE CACHE BOOL "Build With PipeWire" FORCE) + else() + set(WITH_PIPEWIRE FALSE) + endif() + message(WARNING "PipeWire not found, plugin will not be built.") + endif() +endif() + # Python if(WITH_PYTHON) if(AUDASPACE_STANDALONE) @@ -926,7 +964,9 @@ if(WITH_FFMPEG AND PLUGIN_FFMPEG) include_directories(${INCLUDE} ${FFMPEG_INCLUDE_DIRS}) add_library(audffmpeg SHARED ${FFMPEG_SRC} ${FFMPEG_HDR} ${HDR}) target_link_libraries(audffmpeg audaspace ${FFMPEG_LIBRARIES}) - set_target_properties(audffmpeg PROPERTIES SOVERSION ${AUDASPACE_VERSION}) + if(WITH_VERSIONED_PLUGINS) + set_target_properties(audffmpeg PROPERTIES SOVERSION ${AUDASPACE_VERSION}) + endif() install(TARGETS audffmpeg DESTINATION ${DEFAULT_PLUGIN_PATH}) endif() @@ -939,7 +979,9 @@ if(WITH_JACK AND PLUGIN_JACK) else() target_link_libraries(audjack audaspace ${JACK_LIBRARIES}) endif() - set_target_properties(audjack PROPERTIES SOVERSION ${AUDASPACE_VERSION}) + if(WITH_VERSIONED_PLUGINS) + set_target_properties(audjack PROPERTIES SOVERSION ${AUDASPACE_VERSION}) + endif() install(TARGETS audjack DESTINATION ${DEFAULT_PLUGIN_PATH}) endif() @@ -947,7 +989,9 @@ if(WITH_LIBSNDFILE AND PLUGIN_LIBSNDFILE) add_definitions(-DLIBSNDFILE_PLUGIN) include_directories(${INCLUDE} ${LIBSNDFILE_INCLUDE_DIRS}) add_library(audlibsndfile SHARED ${LIBSNDFILE_SRC} ${LIBSNDFILE_HDR} ${HDR}) - set_target_properties(audlibsndfile PROPERTIES SOVERSION ${AUDASPACE_VERSION}) + if(WITH_VERSIONED_PLUGINS) + set_target_properties(audlibsndfile PROPERTIES SOVERSION ${AUDASPACE_VERSION}) + endif() target_link_libraries(audlibsndfile audaspace ${LIBSNDFILE_LIBRARIES}) install(TARGETS audlibsndfile DESTINATION ${DEFAULT_PLUGIN_PATH}) endif() @@ -956,7 +1000,9 @@ if(WITH_OPENAL AND PLUGIN_OPENAL) add_definitions(-DOPENAL_PLUGIN) include_directories(${INCLUDE} ${OPENAL_INCLUDE_DIR}) add_library(audopenal SHARED ${OPENAL_SRC} ${OPENAL_HDR} ${HDR}) - set_target_properties(audopenal PROPERTIES SOVERSION ${AUDASPACE_VERSION}) + if(WITH_VERSIONED_PLUGINS) + set_target_properties(audopenal PROPERTIES SOVERSION ${AUDASPACE_VERSION}) + endif() target_link_libraries(audopenal audaspace ${OPENAL_LIBRARY}) install(TARGETS audopenal DESTINATION ${DEFAULT_PLUGIN_PATH}) endif() @@ -965,7 +1011,9 @@ if(WITH_PULSEAUDIO AND PLUGIN_PULSEAUDIO) add_definitions(-DPULSEAUDIO_PLUGIN) include_directories(${INCLUDE} ${LIBPULSE_INCLUDE_DIR}) add_library(audpulseaudio SHARED ${PULSEAUDIO_SRC} ${PULSEAUDIO_HDR} ${HDR}) - set_target_properties(audpulseaudio PROPERTIES SOVERSION ${AUDASPACE_VERSION}) + if(WITH_VERSIONED_PLUGINS) + set_target_properties(audpulseaudio PROPERTIES SOVERSION ${AUDASPACE_VERSION}) + endif() if(DYNLOAD_PULSEAUDIO) target_link_libraries(audpulseaudio audaspace) else() @@ -974,11 +1022,28 @@ if(WITH_PULSEAUDIO AND PLUGIN_PULSEAUDIO) install(TARGETS audpulseaudio DESTINATION ${DEFAULT_PLUGIN_PATH}) endif() +if(WITH_PIPEWIRE AND PLUGIN_PIPEWIRE) + add_definitions(-DPIPEWIRE_PLUGIN) + include_directories(${INCLUDE} ${PIPEWIRE_INCLUDE_DIRS}) + add_library(audpipewire SHARED ${PIPEWIRE_SRC} ${PIPEWIRE_HDR} ${HDR}) + if(WITH_VERSIONED_PLUGINS) + set_target_properties(audpipewire PROPERTIES SOVERSION ${AUDASPACE_VERSION}) + endif() + if(DYNLOAD_PIPEWIRE) + target_link_libraries(audpipewire audaspace) + else() + target_link_libraries(audpipewire audaspace ${PIPEWIRE_LIBRARIES}) + endif() + install(TARGETS audpipewire DESTINATION ${DEFAULT_PLUGIN_PATH}) +endif() + if(WITH_SDL AND PLUGIN_SDL) add_definitions(-DSDL_PLUGIN) include_directories(${INCLUDE} ${SDL_INCLUDE_DIR}) add_library(audsdl SHARED ${SDL_SRC} ${SDL_HDR} ${HDR}) - set_target_properties(audsdl PROPERTIES SOVERSION ${AUDASPACE_VERSION}) + if(WITH_VERSIONED_PLUGINS) + set_target_properties(audsdl PROPERTIES SOVERSION ${AUDASPACE_VERSION}) + endif() target_link_libraries(audsdl audaspace ${SDL_LIBRARY}) install(TARGETS audsdl DESTINATION ${DEFAULT_PLUGIN_PATH}) endif() diff --git a/extern/audaspace/README.md b/extern/audaspace/README.md index bfc343e2b49..d78e82253fd 100644 --- a/extern/audaspace/README.md +++ b/extern/audaspace/README.md @@ -32,7 +32,7 @@ The following (probably incomplete) features are supported by audaspace: License ------- -> Copyright © 2009-2023 Jörg Müller. All rights reserved. +> Copyright © 2009-2024 Jörg Müller. All rights reserved. > > Licensed under the Apache License, Version 2.0 (the "License"); > you may not use this file except in compliance with the License. diff --git a/extern/audaspace/bindings/python/PySound.cpp b/extern/audaspace/bindings/python/PySound.cpp index 740db485a45..d99038e0caa 100644 --- a/extern/audaspace/bindings/python/PySound.cpp +++ b/extern/audaspace/bindings/python/PySound.cpp @@ -886,7 +886,7 @@ Sound_fadeout(Sound* self, PyObject* args) } PyDoc_STRVAR(M_aud_Sound_filter_doc, - ".. method:: filter(b, a = (1))\n\n" + ".. method:: filter(b, a = (1,))\n\n" " Filters a sound with the supplied IIR filter coefficients.\n" " Without the second parameter you'll get a FIR filter.\n\n" " If the first value of the a sequence is 0,\n" diff --git a/extern/audaspace/bindings/python/setup.py.in b/extern/audaspace/bindings/python/setup.py.in index e0fe4a0af42..7618f896ecc 100644 --- a/extern/audaspace/bindings/python/setup.py.in +++ b/extern/audaspace/bindings/python/setup.py.in @@ -38,7 +38,7 @@ else: audaspace = Extension( 'aud', - include_dirs = ['@CMAKE_CURRENT_BINARY_DIR@', '@FFTW_INCLUDE_DIR@', os.path.join(source_directory, '../../include'), numpy.get_include()], + include_dirs = ['@CMAKE_CURRENT_BINARY_DIR@', os.path.join(source_directory, '../../include'), numpy.get_include()] + (['@FFTW_INCLUDE_DIR@'] if '@WITH_FFTW@' == 'ON' else []), libraries = ['audaspace'], library_dirs = ['.', 'Release', 'Debug'], language = 'c++', diff --git a/extern/audaspace/plugins/ffmpeg/FFMPEG.cpp b/extern/audaspace/plugins/ffmpeg/FFMPEG.cpp index d9bfe0b50c4..6e6f70fedb3 100644 --- a/extern/audaspace/plugins/ffmpeg/FFMPEG.cpp +++ b/extern/audaspace/plugins/ffmpeg/FFMPEG.cpp @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright 2009-2016 Jörg Müller + * Copyright 2009-2024 Jörg Müller * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/extern/audaspace/plugins/ffmpeg/FFMPEG.h b/extern/audaspace/plugins/ffmpeg/FFMPEG.h index 974e3350b2d..972c8228784 100644 --- a/extern/audaspace/plugins/ffmpeg/FFMPEG.h +++ b/extern/audaspace/plugins/ffmpeg/FFMPEG.h @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright 2009-2016 Jörg Müller + * Copyright 2009-2024 Jörg Müller * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/extern/audaspace/plugins/ffmpeg/FFMPEGReader.cpp b/extern/audaspace/plugins/ffmpeg/FFMPEGReader.cpp index 38310d0ae86..efe36df4204 100644 --- a/extern/audaspace/plugins/ffmpeg/FFMPEGReader.cpp +++ b/extern/audaspace/plugins/ffmpeg/FFMPEGReader.cpp @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright 2009-2016 Jörg Müller + * Copyright 2009-2024 Jörg Müller * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/extern/audaspace/plugins/ffmpeg/FFMPEGReader.h b/extern/audaspace/plugins/ffmpeg/FFMPEGReader.h index dc8c292c0ed..30d321243f1 100644 --- a/extern/audaspace/plugins/ffmpeg/FFMPEGReader.h +++ b/extern/audaspace/plugins/ffmpeg/FFMPEGReader.h @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright 2009-2016 Jörg Müller + * Copyright 2009-2024 Jörg Müller * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/extern/audaspace/plugins/ffmpeg/FFMPEGWriter.cpp b/extern/audaspace/plugins/ffmpeg/FFMPEGWriter.cpp index df3cab46756..71c5e66f09d 100644 --- a/extern/audaspace/plugins/ffmpeg/FFMPEGWriter.cpp +++ b/extern/audaspace/plugins/ffmpeg/FFMPEGWriter.cpp @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright 2009-2016 Jörg Müller + * Copyright 2009-2024 Jörg Müller * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/extern/audaspace/plugins/ffmpeg/FFMPEGWriter.h b/extern/audaspace/plugins/ffmpeg/FFMPEGWriter.h index 7e39d8cb92b..a8e2984f31a 100644 --- a/extern/audaspace/plugins/ffmpeg/FFMPEGWriter.h +++ b/extern/audaspace/plugins/ffmpeg/FFMPEGWriter.h @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright 2009-2016 Jörg Müller + * Copyright 2009-2024 Jörg Müller * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/extern/audaspace/plugins/pipewire/PipeWireDevice.cpp b/extern/audaspace/plugins/pipewire/PipeWireDevice.cpp new file mode 100644 index 00000000000..ccb7f4ee910 --- /dev/null +++ b/extern/audaspace/plugins/pipewire/PipeWireDevice.cpp @@ -0,0 +1,387 @@ +/******************************************************************************* + * Copyright 2009-2024 Jörg Müller + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ + +#include "PipeWireDevice.h" + +#include + +#include "Exception.h" +#include "IReader.h" +#include "PipeWireLibrary.h" + +#include "devices/DeviceManager.h" +#include "devices/IDeviceFactory.h" + +AUD_NAMESPACE_BEGIN + +PipeWireDevice::PipeWireSynchronizer::PipeWireSynchronizer(PipeWireDevice* device) : m_device(device) +{ +} + +void PipeWireDevice::PipeWireSynchronizer::updateTickStart() +{ + if (!m_get_tick_start) + { + return; + } + pw_time tm; + AUD_pw_stream_get_time_n(m_device->m_stream, &tm, sizeof(tm)); + m_tick_start = tm.ticks; + m_get_tick_start = false; +} + +void PipeWireDevice::PipeWireSynchronizer::play() +{ + m_playing = true; + m_get_tick_start = true; +} + +void PipeWireDevice::PipeWireSynchronizer::stop() +{ + std::shared_ptr dummy_handle; + m_seek_pos = getPosition(dummy_handle); + m_playing = false; +} + +void PipeWireDevice::PipeWireSynchronizer::seek(std::shared_ptr handle, double time) +{ + /* Update start time here as we might update the seek position while playing back. */ + m_get_tick_start = true; + m_seek_pos = time; + handle->seek(time); +} + +double PipeWireDevice::PipeWireSynchronizer::getPosition(std::shared_ptr handle) +{ + if (!m_playing || m_get_tick_start) + { + return m_seek_pos; + } + pw_time tm; + AUD_pw_stream_get_time_n(m_device->m_stream, &tm, sizeof(tm)); + uint64_t now = AUD_pw_stream_get_nsec(m_device->m_stream); + int64_t diff = now - tm.now; + /* Elapsed time since the last sample was queued. */ + int64_t elapsed = (tm.rate.denom * diff) / (tm.rate.num * SPA_NSEC_PER_SEC); + + /* Calculate the elapsed time in seconds from the last seek position. */ + double elapsed_time = (tm.ticks - m_tick_start + elapsed) * tm.rate.num / double(tm.rate.denom); + return elapsed_time + m_seek_pos; +} + +void PipeWireDevice::handleStateChanged(void* device_ptr, enum pw_stream_state old, enum pw_stream_state state, const char* error) +{ + PipeWireDevice* device = (PipeWireDevice*) device_ptr; + //fprintf(stderr, "stream state: \"%s\"\n", pw_stream_state_as_string(state)); + if (state == PW_STREAM_STATE_PAUSED) + { + AUD_pw_stream_flush(device->m_stream, false); + } +} + +void PipeWireDevice::updateRingBuffers() +{ + uint32_t samplesize = AUD_DEVICE_SAMPLE_SIZE(m_specs); + + sample_t* rb_data = m_ringbuffer_data.getBuffer(); + uint32_t rb_size = m_ringbuffer_data.getSize(); + uint32_t rb_index; + Buffer mix_buffer = Buffer(rb_size); + sample_t* mix_buffer_data = mix_buffer.getBuffer(); + + std::unique_lock lock(m_mixingLock); + + while (m_run_mixing_thread) + { + /* Get the amount of bytes available for writing. */ + int32_t rb_avail = rb_size - spa_ringbuffer_get_write_index(&m_ringbuffer, &rb_index); + if (m_fill_ringbuffer && rb_avail > 0) { + /* As we allocated the ring buffer ourselves, we assume that the samplesize and + * the available bytes to read is evenly divisable. + */ + int32_t sample_count = rb_avail / samplesize; + mix(reinterpret_cast(mix_buffer_data), sample_count); + spa_ringbuffer_write_data(&m_ringbuffer, rb_data, rb_size, rb_index % rb_size, mix_buffer_data, rb_avail); + rb_index += rb_avail; + spa_ringbuffer_write_update(&m_ringbuffer, rb_index); + } + if (!m_fill_ringbuffer) { + /* Clear the ringbuffer when we are not playing back to make sure we don't + * keep any outdated data. + */ + spa_ringbuffer_read_update(&m_ringbuffer, rb_index); + } + m_mixingCondition.wait(lock); + } +} + +void PipeWireDevice::mixAudioBuffer(void* device_ptr) +{ + PipeWireDevice* device = (PipeWireDevice*) device_ptr; + + pw_buffer* pw_buf = AUD_pw_stream_dequeue_buffer(device->m_stream); + if(!pw_buf) + { + /* Couldn't get any buffer from PipeWire...*/ + return; + } + + /* We call this here as the tick is not guaranteed to be up to date + * until the "process" callback is triggered. + */ + device->m_synchronizer.updateTickStart(); + + spa_data& spa_data = pw_buf->buffer->datas[0]; + spa_chunk* chunk = spa_data.chunk; + + chunk->offset = 0; + chunk->stride = AUD_DEVICE_SAMPLE_SIZE(device->m_specs); + int n_frames = spa_data.maxsize / chunk->stride; + if(pw_buf->requested) + { + n_frames = SPA_MIN(pw_buf->requested, n_frames); + } + chunk->size = n_frames * chunk->stride; + + if(!device->m_fill_ringbuffer) + { + /* Queue up silence if we are not queuing up any samples. + * If we don't give Pipewire any buffers, it will think we encountered an error. + */ + memset(spa_data.data, 0, AUD_FORMAT_SIZE(device->m_specs.format) * chunk->size); + AUD_pw_stream_queue_buffer(device->m_stream, pw_buf); + return; + } + uint32_t rb_index; + spa_ringbuffer* ringbuffer = &device->m_ringbuffer; + + int32_t rb_avail = spa_ringbuffer_get_read_index(ringbuffer, &rb_index); + if (!rb_avail) + { + /* Nothing to read from the ring buffer. */ + device->m_mixingCondition.notify_all(); + memset(spa_data.data, 0, AUD_FORMAT_SIZE(device->m_specs.format) * chunk->size); + AUD_pw_stream_queue_buffer(device->m_stream, pw_buf); + return; + } + + /* Here we assume that, if we have available space to read, that the read + * buffer size is always enough to fill the output buffer. + * This is because the PW_KEY_NODE_LATENCY property that we set should guarantee + * that pipewire can't request any bigger buffer sizes than we requested. + * (But they can be smaller) + */ + uint32_t rb_size = device->m_ringbuffer_data.getSize(); + sample_t* rb_data = device->m_ringbuffer_data.getBuffer(); + spa_ringbuffer_read_data(ringbuffer, rb_data, rb_size, rb_index % rb_size, spa_data.data, chunk->size); + spa_ringbuffer_read_update(ringbuffer, rb_index + chunk->size); + device->m_mixingCondition.notify_all(); + AUD_pw_stream_queue_buffer(device->m_stream, pw_buf); +} + +void PipeWireDevice::playing(bool playing) +{ + AUD_pw_thread_loop_lock(m_thread); + AUD_pw_stream_set_active(m_stream, playing); + AUD_pw_thread_loop_unlock(m_thread); + m_fill_ringbuffer = playing; + /* Poke the mixing thread to ensure that it reacts to the m_fill_ringbuffer change. */ + m_mixingCondition.notify_all(); +} + +PipeWireDevice::PipeWireDevice(const std::string& name, DeviceSpecs specs, int buffersize) : + m_synchronizer(this), + m_fill_ringbuffer(false), + m_run_mixing_thread(true) +{ + if(specs.channels == CHANNELS_INVALID) + specs.channels = CHANNELS_STEREO; + if(specs.format == FORMAT_INVALID) + specs.format = FORMAT_FLOAT32; + if(specs.rate == RATE_INVALID) + specs.rate = RATE_48000; + + m_specs = specs; + spa_audio_format format = SPA_AUDIO_FORMAT_F32; + switch(m_specs.format) + { + case FORMAT_U8: + format = SPA_AUDIO_FORMAT_U8; + break; + case FORMAT_S16: + format = SPA_AUDIO_FORMAT_S16; + break; + case FORMAT_S24: + format = SPA_AUDIO_FORMAT_S24; + break; + case FORMAT_S32: + format = SPA_AUDIO_FORMAT_S32; + break; + case FORMAT_FLOAT32: + format = SPA_AUDIO_FORMAT_F32; + break; + case FORMAT_FLOAT64: + format = SPA_AUDIO_FORMAT_F64; + break; + default: + break; + } + + AUD_pw_init(nullptr, nullptr); + + m_thread = AUD_pw_thread_loop_new(name.c_str(), nullptr); + if(!m_thread) + { + AUD_THROW(DeviceException, "Could not create PipeWire thread."); + } + + m_events = std::make_unique(); + m_events->version = PW_VERSION_STREAM_EVENTS; + m_events->state_changed = PipeWireDevice::handleStateChanged; + m_events->process = PipeWireDevice::mixAudioBuffer; + + pw_properties *stream_props = AUD_pw_properties_new( + PW_KEY_MEDIA_TYPE, "Audio", + PW_KEY_MEDIA_CATEGORY, "Playback", + PW_KEY_MEDIA_ROLE, "Production", + NULL); + + /* Set the requested sample rate and latency. */ + AUD_pw_properties_setf(stream_props, PW_KEY_NODE_RATE, "1/%u", uint(m_specs.rate)); + AUD_pw_properties_setf(stream_props, PW_KEY_NODE_LATENCY, "%u/%u", buffersize, uint(m_specs.rate)); + + m_stream = AUD_pw_stream_new_simple( + AUD_pw_thread_loop_get_loop(m_thread), + name.c_str(), + stream_props, + m_events.get(), + this); + if(!m_stream) + { + AUD_pw_thread_loop_destroy(m_thread); + AUD_THROW(DeviceException, "Could not create PipeWire stream."); + } + + spa_audio_info_raw info{}; + info.channels = m_specs.channels; + info.format = format; + info.rate = m_specs.rate; + + uint8_t buffer[1024]; + spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + const spa_pod *param = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &info); + + AUD_pw_stream_connect(m_stream, + PW_DIRECTION_OUTPUT, + PW_ID_ANY, + static_cast(PW_STREAM_FLAG_AUTOCONNECT | + PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_INACTIVE | + PW_STREAM_FLAG_RT_PROCESS), + ¶m, 1); + AUD_pw_thread_loop_start(m_thread); + + create(); + + spa_ringbuffer_init(&m_ringbuffer); + m_ringbuffer_data.resize(buffersize * AUD_DEVICE_SAMPLE_SIZE(m_specs)); + m_mixingThread = std::thread(&PipeWireDevice::updateRingBuffers, this); +} + +PipeWireDevice::~PipeWireDevice() +{ + /* Ensure that we are not playing back anything anymore. */ + destroy(); + + /* Destruct all PipeWire data. */ + AUD_pw_thread_loop_stop(m_thread); + AUD_pw_stream_destroy(m_stream); + AUD_pw_thread_loop_destroy(m_thread); + AUD_pw_deinit(); + + { + /* Ensure that the mixing thread exits. */ + std::unique_lock lock(m_mixingLock); + m_run_mixing_thread = false; + m_mixingCondition.notify_all(); + } + m_mixingThread.join(); +} + +ISynchronizer* PipeWireDevice::getSynchronizer() +{ + return &m_synchronizer; +} + +class PipeWireDeviceFactory : public IDeviceFactory +{ +private: + DeviceSpecs m_specs; + int m_buffersize; + std::string m_name; + +public: + PipeWireDeviceFactory() : m_buffersize(AUD_DEFAULT_BUFFER_SIZE) + { + m_specs.format = FORMAT_S16; + m_specs.channels = CHANNELS_STEREO; + m_specs.rate = RATE_48000; + } + + virtual std::shared_ptr openDevice() + { + return std::shared_ptr(new PipeWireDevice(m_name, m_specs, m_buffersize)); + } + + virtual int getPriority() + { + return 1 << 16; + } + + virtual void setSpecs(DeviceSpecs specs) + { + m_specs = specs; + } + + virtual void setBufferSize(int buffersize) + { + m_buffersize = buffersize; + } + + virtual void setName(const std::string &name) + { + m_name = name; + } +}; + +void PipeWireDevice::registerPlugin() +{ + if(loadPipeWire()) + DeviceManager::registerDevice("PipeWire", std::shared_ptr(new PipeWireDeviceFactory)); +} + +#ifdef PIPEWIRE_PLUGIN +extern "C" AUD_PLUGIN_API void registerPlugin() +{ + PipeWireDevice::registerPlugin(); +} + +extern "C" AUD_PLUGIN_API const char* getName() +{ + return "Pipewire"; +} +#endif + +AUD_NAMESPACE_END diff --git a/extern/audaspace/plugins/pipewire/PipeWireDevice.h b/extern/audaspace/plugins/pipewire/PipeWireDevice.h new file mode 100644 index 00000000000..59e57c5bfc7 --- /dev/null +++ b/extern/audaspace/plugins/pipewire/PipeWireDevice.h @@ -0,0 +1,134 @@ +/******************************************************************************* + * Copyright 2009-2024 Jörg Müller + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ + +#pragma once + +#ifdef PIPEWIRE_PLUGIN +#define AUD_BUILD_PLUGIN +#endif + +/** + * @file PipeWireDevice.h + * @ingroup plugin + * The PipeWireDevice class. + */ + +#include +#include +#include +#include + +#include "devices/SoftwareDevice.h" + +AUD_NAMESPACE_BEGIN + +/** + * This device plays back through PipeWire, the simple direct media layer. + */ +class AUD_PLUGIN_API PipeWireDevice : public SoftwareDevice +{ +private: + class PipeWireSynchronizer : public DefaultSynchronizer + { + PipeWireDevice* m_device; + bool m_playing = false; + bool m_get_tick_start = false; + int64_t m_tick_start = 0.0f; + double m_seek_pos = 0.0f; + + public: + PipeWireSynchronizer(PipeWireDevice* device); + + void updateTickStart(); + virtual void play(); + virtual void stop(); + virtual void seek(std::shared_ptr handle, double time); + virtual double getPosition(std::shared_ptr handle); + }; + + /// Synchronizer. + PipeWireSynchronizer m_synchronizer; + + /** + * Whether we should start filling our ringbuffer with audio. + */ + bool m_fill_ringbuffer; + + pw_stream* m_stream; + pw_thread_loop* m_thread; + std::unique_ptr m_events; + + /** + * The mixing thread. + */ + std::thread m_mixingThread; + bool m_run_mixing_thread; + + /** + * Mutex for mixing. + */ + std::mutex m_mixingLock; + + /** + * The mixing ringbuffer and mixing data + */ + spa_ringbuffer m_ringbuffer; + Buffer m_ringbuffer_data; + std::condition_variable m_mixingCondition; + + AUD_LOCAL static void handleStateChanged(void* device_ptr, enum pw_stream_state old, enum pw_stream_state state, const char* error); + + /** + * Updates the ring buffers. + */ + AUD_LOCAL void updateRingBuffers(); + + /** + * Mixes the next bytes into the buffer. + * \param data The PipeWire device. + */ + AUD_LOCAL static void mixAudioBuffer(void* device_ptr); + + // delete copy constructor and operator= + PipeWireDevice(const PipeWireDevice&) = delete; + PipeWireDevice& operator=(const PipeWireDevice&) = delete; + +protected: + virtual void playing(bool playing); + +public: + /** + * Opens the PipeWire audio device for playback. + * \param specs The wanted audio specification. + * \param buffersize The size of the internal buffer. + * \note The specification really used for opening the device may differ. + * \exception Exception Thrown if the audio device cannot be opened. + */ + PipeWireDevice(const std::string& name, DeviceSpecs specs, int buffersize = AUD_DEFAULT_BUFFER_SIZE); + + /** + * Closes the PipeWire audio device. + */ + virtual ~PipeWireDevice(); + + virtual ISynchronizer* getSynchronizer(); + /** + * Registers this plugin. + */ + static void registerPlugin(); +}; + +AUD_NAMESPACE_END diff --git a/extern/audaspace/plugins/pipewire/PipeWireLibrary.cpp b/extern/audaspace/plugins/pipewire/PipeWireLibrary.cpp new file mode 100644 index 00000000000..e532c5b434a --- /dev/null +++ b/extern/audaspace/plugins/pipewire/PipeWireLibrary.cpp @@ -0,0 +1,59 @@ +/******************************************************************************* + * Copyright 2009-2024 Jörg Müller + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ + +#define PIPEWIRE_LIBRARY_IMPLEMENTATION + +#include +#include + +#include "PipeWireLibrary.h" + +#ifdef DYNLOAD_PIPEWIRE +#include "plugin/PluginManager.h" +#endif + +AUD_NAMESPACE_BEGIN + +bool loadPipeWire() +{ +#ifdef DYNLOAD_PIPEWIRE + std::array names = {"libpipewire-0.3.so", "libpipewire-0.3.so.0"}; + + void* handle = nullptr; + + for(auto& name : names) + { + handle = PluginManager::openLibrary(name); + if(handle) + break; + } + + if (!handle) + return false; + +#define PIPEWIRE_SYMBOL(sym) AUD_##sym = reinterpret_cast(PluginManager::lookupLibrary(handle, #sym)) +#else +#define PIPEWIRE_SYMBOL(sym) AUD_##sym = &sym +#endif + +#include "PipeWireSymbols.h" + +#undef PIPEWIRE_SYMBOL + + return AUD_pw_init != nullptr; +} + +AUD_NAMESPACE_END diff --git a/extern/audaspace/plugins/pipewire/PipeWireLibrary.h b/extern/audaspace/plugins/pipewire/PipeWireLibrary.h new file mode 100644 index 00000000000..2e6ed07d83b --- /dev/null +++ b/extern/audaspace/plugins/pipewire/PipeWireLibrary.h @@ -0,0 +1,47 @@ +/******************************************************************************* + * Copyright 2009-2024 Jörg Müller + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ + +#pragma once + +#ifdef PIPEWIRE_PLUGIN +#define AUD_BUILD_PLUGIN +#endif + +/** + * @file PipeWireLibrary.h + * @ingroup plugin + */ + +#include "Audaspace.h" + +#include +#include + +AUD_NAMESPACE_BEGIN + +#ifdef PIPEWIRE_LIBRARY_IMPLEMENTATION +#define PIPEWIRE_SYMBOL(sym) decltype(&sym) AUD_##sym +#else +#define PIPEWIRE_SYMBOL(sym) extern decltype(&sym) AUD_##sym +#endif + +#include "PipeWireSymbols.h" + +#undef PIPEWIRE_SYMBOL + +bool loadPipeWire(); + +AUD_NAMESPACE_END diff --git a/extern/audaspace/plugins/pipewire/PipeWireSymbols.h b/extern/audaspace/plugins/pipewire/PipeWireSymbols.h new file mode 100644 index 00000000000..3244abd32b2 --- /dev/null +++ b/extern/audaspace/plugins/pipewire/PipeWireSymbols.h @@ -0,0 +1,40 @@ +/******************************************************************************* + * Copyright 2009-2024 Jörg Müller + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ + +PIPEWIRE_SYMBOL(pw_init); +PIPEWIRE_SYMBOL(pw_deinit); + +PIPEWIRE_SYMBOL(pw_properties_new); +PIPEWIRE_SYMBOL(pw_properties_setf); + +PIPEWIRE_SYMBOL(pw_stream_connect); +PIPEWIRE_SYMBOL(pw_stream_destroy); +PIPEWIRE_SYMBOL(pw_stream_get_nsec); +PIPEWIRE_SYMBOL(pw_stream_get_time_n); +PIPEWIRE_SYMBOL(pw_stream_new_simple); +PIPEWIRE_SYMBOL(pw_stream_queue_buffer); +PIPEWIRE_SYMBOL(pw_stream_dequeue_buffer); +PIPEWIRE_SYMBOL(pw_stream_set_active); +PIPEWIRE_SYMBOL(pw_stream_flush); + +PIPEWIRE_SYMBOL(pw_thread_loop_destroy); +PIPEWIRE_SYMBOL(pw_thread_loop_get_loop); +PIPEWIRE_SYMBOL(pw_thread_loop_lock); +PIPEWIRE_SYMBOL(pw_thread_loop_unlock); +PIPEWIRE_SYMBOL(pw_thread_loop_new); +PIPEWIRE_SYMBOL(pw_thread_loop_start); +PIPEWIRE_SYMBOL(pw_thread_loop_stop); + diff --git a/extern/audaspace/plugins/pulseaudio/PulseAudioDevice.cpp b/extern/audaspace/plugins/pulseaudio/PulseAudioDevice.cpp index bfe67d245f6..d26afeb9066 100644 --- a/extern/audaspace/plugins/pulseaudio/PulseAudioDevice.cpp +++ b/extern/audaspace/plugins/pulseaudio/PulseAudioDevice.cpp @@ -15,28 +15,51 @@ ******************************************************************************/ #include "PulseAudioDevice.h" -#include "PulseAudioLibrary.h" -#include "devices/DeviceManager.h" -#include "devices/IDeviceFactory.h" + #include "Exception.h" #include "IReader.h" +#include "PulseAudioLibrary.h" + +#include "devices/DeviceManager.h" +#include "devices/IDeviceFactory.h" AUD_NAMESPACE_BEGIN -PulseAudioDevice::PulseAudioSynchronizer::PulseAudioSynchronizer(PulseAudioDevice *device) : - m_device(device) +PulseAudioDevice::PulseAudioSynchronizer::PulseAudioSynchronizer(PulseAudioDevice* device) : m_device(device) { } -double PulseAudioDevice::PulseAudioSynchronizer::getPosition(std::shared_ptr handle) +void PulseAudioDevice::PulseAudioSynchronizer::play() { - pa_usec_t latency; - int negative; - AUD_pa_stream_get_latency(m_device->m_stream, &latency, &negative); + /* Make sure that our start time is up to date. */ + AUD_pa_stream_get_time(m_device->m_stream, &m_time_start); + m_playing = true; +} - double delay = m_device->m_ring_buffer.getReadSize() / (AUD_SAMPLE_SIZE(m_device->m_specs) * m_device->m_specs.rate) + latency * 1.0e-6; +void PulseAudioDevice::PulseAudioSynchronizer::stop() +{ + std::shared_ptr dummy_handle; + m_seek_pos = getPosition(dummy_handle); + m_playing = false; +} - return handle->getPosition() - delay; +void PulseAudioDevice::PulseAudioSynchronizer::seek(std::shared_ptr handle, double time) +{ + /* Update start time here as we might update the seek position while playing back. */ + AUD_pa_stream_get_time(m_device->m_stream, &m_time_start); + m_seek_pos = time; + handle->seek(time); +} + +double PulseAudioDevice::PulseAudioSynchronizer::getPosition(std::shared_ptr /*handle*/) +{ + pa_usec_t time; + if(!m_playing) + { + return m_seek_pos; + } + AUD_pa_stream_get_time(m_device->m_stream, &time); + return (time - m_time_start) * 1.0e-6 + m_seek_pos; } void PulseAudioDevice::updateRingBuffer() @@ -71,13 +94,12 @@ void PulseAudioDevice::updateRingBuffer() } else { - if(m_ring_buffer.getReadSize() == 0 && !m_corked) + if(m_ring_buffer.getReadSize() == 0) { AUD_pa_threaded_mainloop_lock(m_mainloop); AUD_pa_stream_cork(m_stream, 1, nullptr, nullptr); AUD_pa_stream_flush(m_stream, nullptr, nullptr); AUD_pa_threaded_mainloop_unlock(m_mainloop); - m_corked = true; } } } @@ -86,18 +108,18 @@ void PulseAudioDevice::updateRingBuffer() } } -void PulseAudioDevice::PulseAudio_state_callback(pa_context *context, void *data) +void PulseAudioDevice::PulseAudio_state_callback(pa_context* context, void* data) { - PulseAudioDevice* device = (PulseAudioDevice*)data; + PulseAudioDevice* device = (PulseAudioDevice*) data; device->m_state = AUD_pa_context_get_state(context); AUD_pa_threaded_mainloop_signal(device->m_mainloop, 0); } -void PulseAudioDevice::PulseAudio_request(pa_stream *stream, size_t total_bytes, void *data) +void PulseAudioDevice::PulseAudio_request(pa_stream* stream, size_t total_bytes, void* data) { - PulseAudioDevice* device = (PulseAudioDevice*)data; + PulseAudioDevice* device = (PulseAudioDevice*) data; data_t* buffer; @@ -141,14 +163,12 @@ void PulseAudioDevice::playing(bool playing) AUD_pa_threaded_mainloop_lock(m_mainloop); AUD_pa_stream_cork(m_stream, 0, nullptr, nullptr); AUD_pa_threaded_mainloop_unlock(m_mainloop); - m_corked = false; } } PulseAudioDevice::PulseAudioDevice(const std::string &name, DeviceSpecs specs, int buffersize) : m_synchronizer(this), m_playback(false), - m_corked(true), m_state(PA_CONTEXT_UNCONNECTED), m_valid(true), m_underflows(0) diff --git a/extern/audaspace/plugins/pulseaudio/PulseAudioDevice.h b/extern/audaspace/plugins/pulseaudio/PulseAudioDevice.h index ce5aa7fbdff..b6e6a313a74 100644 --- a/extern/audaspace/plugins/pulseaudio/PulseAudioDevice.h +++ b/extern/audaspace/plugins/pulseaudio/PulseAudioDevice.h @@ -45,10 +45,16 @@ private: class PulseAudioSynchronizer : public DefaultSynchronizer { PulseAudioDevice* m_device; + bool m_playing = false; + pa_usec_t m_time_start = 0; + double m_seek_pos = 0.0f; public: PulseAudioSynchronizer(PulseAudioDevice* device); + virtual void play(); + virtual void stop(); + virtual void seek(std::shared_ptr handle, double time); virtual double getPosition(std::shared_ptr handle); }; @@ -60,8 +66,6 @@ private: */ volatile bool m_playback; - bool m_corked; - pa_threaded_mainloop* m_mainloop; pa_context* m_context; pa_stream* m_stream; diff --git a/extern/audaspace/plugins/pulseaudio/PulseAudioSymbols.h b/extern/audaspace/plugins/pulseaudio/PulseAudioSymbols.h index a33135b6e25..574ed4115c9 100644 --- a/extern/audaspace/plugins/pulseaudio/PulseAudioSymbols.h +++ b/extern/audaspace/plugins/pulseaudio/PulseAudioSymbols.h @@ -25,7 +25,7 @@ PULSEAUDIO_SYMBOL(pa_stream_begin_write); PULSEAUDIO_SYMBOL(pa_stream_connect_playback); PULSEAUDIO_SYMBOL(pa_stream_cork); PULSEAUDIO_SYMBOL(pa_stream_flush); -PULSEAUDIO_SYMBOL(pa_stream_get_latency); +PULSEAUDIO_SYMBOL(pa_stream_get_time); PULSEAUDIO_SYMBOL(pa_stream_is_corked); PULSEAUDIO_SYMBOL(pa_stream_new); PULSEAUDIO_SYMBOL(pa_stream_set_buffer_attr);