Audaspace: merge changes from upstream.

Add the Pipewire backend and Pulseaudio fixes.
There are also some other minor changes.
This commit is contained in:
Sebastian Parborg
2024-12-03 14:47:25 +01:00
committed by Sebastian Parborg
parent 33af6d9c5b
commit bc39ee692d
20 changed files with 835 additions and 46 deletions

View File

@@ -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

View File

@@ -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).

View File

@@ -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()

View File

@@ -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.

View File

@@ -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"

View File

@@ -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++',

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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 <spa/param/audio/format-utils.h>
#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<IHandle> dummy_handle;
m_seek_pos = getPosition(dummy_handle);
m_playing = false;
}
void PipeWireDevice::PipeWireSynchronizer::seek(std::shared_ptr<IHandle> 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<IHandle> 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<std::mutex> 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<data_t*>(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<pw_stream_events>();
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_flags>(PW_STREAM_FLAG_AUTOCONNECT |
PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_INACTIVE |
PW_STREAM_FLAG_RT_PROCESS),
&param, 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<std::mutex> 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<IDevice> openDevice()
{
return std::shared_ptr<IDevice>(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<IDeviceFactory>(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

View File

@@ -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 <condition_variable>
#include <thread>
#include <pipewire/pipewire.h>
#include <spa/utils/ringbuffer.h>
#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<IHandle> handle, double time);
virtual double getPosition(std::shared_ptr<IHandle> 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<pw_stream_events> 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

View File

@@ -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 <string>
#include <array>
#include "PipeWireLibrary.h"
#ifdef DYNLOAD_PIPEWIRE
#include "plugin/PluginManager.h"
#endif
AUD_NAMESPACE_BEGIN
bool loadPipeWire()
{
#ifdef DYNLOAD_PIPEWIRE
std::array<const std::string, 2> 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<decltype(&sym)>(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

View File

@@ -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 <pipewire/pipewire.h>
#include <pipewire/stream.h>
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

View File

@@ -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);

View File

@@ -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<IHandle> 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<IHandle> dummy_handle;
m_seek_pos = getPosition(dummy_handle);
m_playing = false;
}
return handle->getPosition() - delay;
void PulseAudioDevice::PulseAudioSynchronizer::seek(std::shared_ptr<IHandle> 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<IHandle> /*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)

View File

@@ -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<IHandle> handle, double time);
virtual double getPosition(std::shared_ptr<IHandle> 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;

View File

@@ -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);