From 5e6179159dd9de64a1f4d1ec6ad48e60162b7ab7 Mon Sep 17 00:00:00 2001 From: Robert Scheck Date: Mon, 20 Mar 2023 17:20:22 +0100 Subject: [PATCH] - Upgrade to 3.0.0 (#2180064) - Added (hopefully future upstream) patch for PipeWire support --- baresip-3.0.0-pipewire.patch | 1230 ++++++++++++++++++++++++++++++++++ baresip.spec | 40 +- sources | 2 +- 3 files changed, 1266 insertions(+), 6 deletions(-) create mode 100644 baresip-3.0.0-pipewire.patch diff --git a/baresip-3.0.0-pipewire.patch b/baresip-3.0.0-pipewire.patch new file mode 100644 index 0000000..8567b06 --- /dev/null +++ b/baresip-3.0.0-pipewire.patch @@ -0,0 +1,1230 @@ +From 5c473db1b9606134b400c0b1622df8740b16a0fc Mon Sep 17 00:00:00 2001 +From: Christian Spielberger +Date: Thu, 2 Feb 2023 08:52:54 +0100 +Subject: [PATCH 01/10] pipewire: add pipewire module + +--- + cmake/FindPIPEWIRE.cmake | 10 ++ + cmake/modules.cmake | 4 + + modules/pipewire/CMakeLists.txt | 17 ++++ + modules/pipewire/capture.c | 163 ++++++++++++++++++++++++++++++++ + modules/pipewire/pipewire.c | 161 +++++++++++++++++++++++++++++++ + modules/pipewire/pipewire.h | 17 ++++ + modules/pipewire/playback.c | 163 ++++++++++++++++++++++++++++++++ + 7 files changed, 535 insertions(+) + create mode 100644 cmake/FindPIPEWIRE.cmake + create mode 100644 modules/pipewire/CMakeLists.txt + create mode 100644 modules/pipewire/capture.c + create mode 100644 modules/pipewire/pipewire.c + create mode 100644 modules/pipewire/pipewire.h + create mode 100644 modules/pipewire/playback.c + +diff --git a/cmake/FindPIPEWIRE.cmake b/cmake/FindPIPEWIRE.cmake +new file mode 100644 +index 000000000..bd9b90181 +--- /dev/null ++++ b/cmake/FindPIPEWIRE.cmake +@@ -0,0 +1,10 @@ ++# Find the system's pipewire includes and library ++# ++# PIPEWIRE_INCLUDE_DIRS - where to find pipewire.h ++# PIPEWIRE_LIBRARIES - List of libraries when using pipewire ++# PIPEWIRE_FOUND - True if pipewire found ++ ++if(NOT WIN32) ++ find_package(PkgConfig) ++ pkg_search_module(PIPEWIRE libpipewire-0.3) ++endif() +diff --git a/cmake/modules.cmake b/cmake/modules.cmake +index ed99bf835..e68835595 100644 +--- a/cmake/modules.cmake ++++ b/cmake/modules.cmake +@@ -20,6 +20,7 @@ find_package(OPUS) + find_package(PNG) + find_package(PORTAUDIO) + find_package(PULSE) ++find_package(PIPEWIRE) + find_package(SDL) + find_package(SNDFILE) + find_package(SPANDSP) +@@ -140,6 +141,9 @@ endif() + if(PULSE_FOUND) + list(APPEND MODULES pulse) + endif() ++if(PIPEWIRE_FOUND) ++ list(APPEND MODULES pipewire) ++endif() + if(SDL_FOUND) + list(APPEND MODULES sdl) + endif() +diff --git a/modules/pipewire/CMakeLists.txt b/modules/pipewire/CMakeLists.txt +new file mode 100644 +index 000000000..8673873af +--- /dev/null ++++ b/modules/pipewire/CMakeLists.txt +@@ -0,0 +1,17 @@ ++project(pipewire) ++ ++set(SRCS pipewire.c playback.c capture.c) ++ ++if(STATIC) ++ add_library(${PROJECT_NAME} OBJECT ${SRCS}) ++else() ++ add_library(${PROJECT_NAME} MODULE ${SRCS}) ++endif() ++ ++target_include_directories(${PROJECT_NAME} PRIVATE ${PIPEWIRE_INCLUDE_DIRS}) ++target_link_directories(${PROJECT_NAME} PRIVATE ${PIPEWIRE_LIBRARY_DIRS}) ++target_link_libraries(${PROJECT_NAME} PRIVATE ${PIPEWIRE_LIBRARIES}) ++target_compile_options(${PROJECT_NAME} PRIVATE ++ -Wno-pedantic ++ -Wno-bad-function-cast ++) +diff --git a/modules/pipewire/capture.c b/modules/pipewire/capture.c +new file mode 100644 +index 000000000..3e92d131c +--- /dev/null ++++ b/modules/pipewire/capture.c +@@ -0,0 +1,163 @@ ++/** ++ * @file capture.c Pipewire sound driver - capture ++ * ++ * Copyright (C) 2023 Commend.com - c.spielberger@commend.com ++ */ ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include "pipewire.h" ++ ++ ++struct ausrc_st { ++ struct pw_stream *stream; ++ ++ struct ausrc_prm prm; ++ ausrc_read_h *rh; ++ struct spa_hook listener; ++ ausrc_error_h *errh; ++ ++ size_t sampsz; ++ uint64_t samps; ++ ++ void *arg; ++}; ++ ++ ++static void on_process(void *arg); ++ ++static const struct pw_stream_events stream_events = { ++ PW_VERSION_STREAM_EVENTS, ++ .process = on_process, ++}; ++ ++ ++static void ausrc_destructor(void *arg) ++{ ++ struct ausrc_st *st = arg; ++ ++ st->rh = NULL; ++ st->errh = NULL; ++ pw_stream_destroy(st->stream); ++} ++ ++ ++int pw_capture_alloc(struct ausrc_st **stp, const struct ausrc *as, ++ struct ausrc_prm *prm, const char *dev, ausrc_read_h *rh, ++ ausrc_error_h *errh, void *arg) ++{ ++ struct ausrc_st *st; ++ const struct spa_pod *params[1]; ++ uint8_t buffer[1024]; ++ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, ++ sizeof(buffer)); ++ const char name[] = "baresip-capture"; ++ int err = 0; ++ ++ if (!stp || !as || !prm || !rh) ++ return EINVAL; ++ ++ info ("pipewire: opening capture(%u Hz, %d channels," ++ "device '%s')\n", prm->srate, prm->ch, dev); ++ ++ st = mem_zalloc(sizeof(*st), ausrc_destructor); ++ if (!st) ++ return ENOMEM; ++ ++ st->prm.srate = prm->srate; ++ st->prm.ch = prm->ch; ++ st->prm.ptime = prm->ptime; ++ st->prm.fmt = prm->fmt; ++ ++ st->sampsz = aufmt_sample_size(prm->fmt); ++ st->samps = 0; ++ ++ st->rh = rh; ++ st->errh = errh; ++ st->arg = arg; ++ ++ pw_thread_loop_lock (pw_loop_instance()); ++ st->stream = pw_stream_new(pw_core_instance(), name, ++ pw_properties_new( ++ PW_KEY_MEDIA_TYPE, "Audio", ++ PW_KEY_MEDIA_CATEGORY, "Capture", ++ PW_KEY_MEDIA_ROLE, "Communication", ++ NULL)); ++ if (!st->stream) { ++ err = errno; ++ goto out; ++ } ++ ++ pw_stream_add_listener(st->stream, &st->listener, &stream_events, st); ++ params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, ++ &SPA_AUDIO_INFO_RAW_INIT( ++ .format = aufmt_to_pw_format(prm->fmt), ++ .channels = prm->ch, ++ .rate = prm->srate )); ++ if (!params[0]) ++ goto out; ++ ++ err = pw_stream_connect(st->stream, ++ PW_DIRECTION_INPUT, ++ PW_ID_ANY, ++ PW_STREAM_FLAG_AUTOCONNECT | ++ PW_STREAM_FLAG_MAP_BUFFERS | ++ PW_STREAM_FLAG_RT_PROCESS, ++ params, 1); ++ ++ pw_thread_loop_unlock(pw_loop_instance()); ++ ++ info ("pipewire: stream %s started (%m)\n", name, err); ++ ++ out: ++ if (err) ++ mem_deref(st); ++ else ++ *stp = st; ++ ++ return err; ++} ++ ++ ++/** ++ * Pipewire process callback ++ * ++ * @param arg Argument (ausrc_st object) ++ */ ++static void on_process(void *arg) ++{ ++ struct ausrc_st *st = arg; ++ struct pw_buffer *b; ++ struct spa_buffer *buf; ++ struct auframe af; ++ ++ void *sampv; ++ size_t sampc; ++ ++ b = pw_stream_dequeue_buffer(st->stream); ++ if (!b) ++ warning("pipewire: out of buffers (%m)\n", errno); ++ ++ buf = b->buffer; ++ sampv = buf->datas[0].data; ++ if (!sampv) ++ return; ++ ++ sampc = buf->datas[0].chunk->size / st->sampsz; ++ ++ auframe_init(&af, st->prm.fmt, sampv, sampc, ++ st->prm.srate, st->prm.ch); ++ ++ af.timestamp = st->samps * AUDIO_TIMEBASE / ++ (st->prm.srate * st->prm.ch); ++ st->samps += sampc; ++ st->rh(&af, st->arg); ++ ++ pw_stream_queue_buffer(st->stream, b); ++} +diff --git a/modules/pipewire/pipewire.c b/modules/pipewire/pipewire.c +new file mode 100644 +index 000000000..c9e6aaff4 +--- /dev/null ++++ b/modules/pipewire/pipewire.c +@@ -0,0 +1,161 @@ ++/** ++ * @file pipewire.c Pipewire sound driver ++ * ++ * Copyright (C) 2023 Commend.com - c.spielberger@commend.com ++ */ ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include "pipewire.h" ++ ++/** ++ * @defgroup pipewire pipewire ++ * ++ * Audio driver module for Pipewire ++ * ++ */ ++ ++enum { ++ RECONN_DELAY = 1500, ++}; ++ ++ ++struct pw_stat { ++ struct pw_thread_loop *loop; ++ struct pw_context *context; ++ struct pw_core *core; ++}; ++ ++ ++static struct pw_stat *d = NULL; ++ ++static struct auplay *auplay = NULL; ++static struct ausrc *ausrc = NULL; ++ ++ ++static void destructor(void *arg) ++{ ++ struct pw_stat *pw = arg; ++ ++ if (pw->core) ++ pw_core_disconnect(pw->core); ++ ++ if (pw->context) ++ pw_context_destroy(pw->context); ++ ++ if (pw->loop) { ++ pw_thread_loop_stop(pw->loop); ++ pw_thread_loop_destroy(pw->loop); ++ } ++} ++ ++ ++static struct pw_stat *pw_stat_alloc(void) ++{ ++ struct pw_stat *pw; ++ int err; ++ ++ pw = mem_zalloc(sizeof(*pw), destructor); ++ ++ pw->loop = pw_thread_loop_new("baresip pipewire", NULL); ++ if (!pw->loop) ++ goto errout; ++ ++ err = pw_thread_loop_start(pw->loop); ++ if (err) ++ goto errout; ++ ++ pw->context = pw_context_new(pw_thread_loop_get_loop(pw->loop), ++ NULL /* properties */, ++ 0 /* user_data size */); ++ if (!pw->context) ++ goto errout; ++ ++ pw->core = pw_context_connect(pw->context, ++ NULL /* properties */, ++ 0 /* user_data size */); ++ if (!pw->core) ++ goto errout; ++ ++ info("pipewire: connected to pipewire\n"); ++ return pw; ++ ++errout: ++ warning("pipewire: could not connect to pipewire\n"); ++ mem_deref(pw); ++ return NULL; ++} ++ ++ ++struct pw_core *pw_core_instance(void) ++{ ++ if (!d) ++ return NULL; ++ ++ return d->core; ++} ++ ++ ++struct pw_thread_loop *pw_loop_instance(void) ++{ ++ if (!d) ++ return NULL; ++ ++ return d->loop; ++} ++ ++ ++int aufmt_to_pw_format(enum aufmt fmt) ++{ ++ switch (fmt) { ++ case AUFMT_S16LE: return SPA_AUDIO_FORMAT_S16_LE; ++ case AUFMT_FLOAT: return SPA_AUDIO_FORMAT_F32; ++ default: return 0; ++ } ++} ++ ++ ++static int module_init(void) ++{ ++ int err = 0; ++ ++ pw_init(NULL, NULL); ++ info("pipewire: headers %s library %s \n", ++ pw_get_headers_version(), pw_get_library_version()); ++ ++ d = pw_stat_alloc(); ++ if (!d) ++ return errno; ++ ++ err = auplay_register(&auplay, baresip_auplayl(), ++ "pipewire", pw_playback_alloc); ++ err |= ausrc_register(&ausrc, baresip_ausrcl(), ++ "pipewire", pw_capture_alloc); ++ ++ return err; ++} ++ ++ ++static int module_close(void) ++{ ++ auplay = mem_deref(auplay); ++ ausrc = mem_deref(ausrc); ++ ++ d = mem_deref(d); ++ pw_deinit(); ++ return 0; ++} ++ ++ ++EXPORT_SYM const struct mod_export DECL_EXPORTS(pipewire) = { ++ "pipewire", ++ "audio", ++ module_init, ++ module_close, ++}; +diff --git a/modules/pipewire/pipewire.h b/modules/pipewire/pipewire.h +new file mode 100644 +index 000000000..0a93aaf52 +--- /dev/null ++++ b/modules/pipewire/pipewire.h +@@ -0,0 +1,17 @@ ++/** ++ * @file pipewire.h Pipewire sound driver - internal API ++ * ++ * Copyright (C) 2023 Commend.com - c.spielberger@commend.com ++ */ ++ ++int aufmt_to_pw_format(enum aufmt fmt); ++struct pw_core *pw_core_instance(void); ++struct pw_thread_loop *pw_loop_instance(void); ++ ++int pw_playback_alloc(struct auplay_st **stp, ++ const struct auplay *ap, ++ struct auplay_prm *prm, const char *dev, ++ auplay_write_h *wh, void *arg); ++int pw_capture_alloc(struct ausrc_st **stp, const struct ausrc *as, ++ struct ausrc_prm *prm, const char *dev, ausrc_read_h *rh, ++ ausrc_error_h *errh, void *arg); +diff --git a/modules/pipewire/playback.c b/modules/pipewire/playback.c +new file mode 100644 +index 000000000..94fceb1b0 +--- /dev/null ++++ b/modules/pipewire/playback.c +@@ -0,0 +1,163 @@ ++/** ++ * @file playback.c Pipewire sound driver - playback ++ * ++ * Copyright (C) 2023 Commend.com - c.spielberger@commend.com ++ */ ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include "pipewire.h" ++ ++struct auplay_st { ++ struct pw_stream *stream; ++ ++ struct auplay_prm prm; ++ auplay_write_h *wh; ++ struct spa_hook listener; ++ ++ size_t sampc; ++ size_t nbytes; ++ int32_t stride; ++ ++ void *arg; ++}; ++ ++static void on_process(void *arg); ++ ++static const struct pw_stream_events stream_events = { ++ PW_VERSION_STREAM_EVENTS, ++ .process = on_process, ++}; ++ ++ ++static void auplay_destructor(void *arg) ++{ ++ struct auplay_st *st = arg; ++ ++ st->wh = NULL; ++ pw_stream_destroy(st->stream); ++} ++ ++ ++int pw_playback_alloc(struct auplay_st **stp, const struct auplay *ap, ++ struct auplay_prm *prm, const char *dev, auplay_write_h *wh, void *arg) ++{ ++ struct auplay_st *st; ++ const struct spa_pod *params[1]; ++ uint8_t buffer[1024]; ++ struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, ++ sizeof(buffer)); ++ const char name[] = "baresip-playback"; ++ size_t sampsz; ++ int err = 0; ++ ++ if (!stp || !ap || !prm || !wh) ++ return EINVAL; ++ ++ info ("pipewire: opening playback (%u Hz, %d channels, device %s, " ++ "ptime %u)\n", prm->srate, prm->ch, dev, prm->ptime); ++ ++ st = mem_zalloc(sizeof(*st), auplay_destructor); ++ if (!st) ++ return ENOMEM; ++ ++ st->prm.srate = prm->srate; ++ st->prm.ch = prm->ch; ++ st->prm.ptime = prm->ptime; ++ st->prm.fmt = prm->fmt; ++ ++ sampsz = aufmt_sample_size(prm->fmt); ++ st->sampc = st->prm.ptime * st->prm.ch * st->prm.srate / 1000; ++ st->nbytes = st->sampc * sampsz; ++ st->stride = sampsz * prm->ch; ++ ++ st->wh = wh; ++ st->arg = arg; ++ ++ pw_thread_loop_lock (pw_loop_instance()); ++ st->stream = pw_stream_new(pw_core_instance(), name, ++ pw_properties_new( ++ PW_KEY_MEDIA_TYPE, "Audio", ++ PW_KEY_MEDIA_CATEGORY, "Playback", ++ PW_KEY_MEDIA_ROLE, "Communication", ++ NULL)); ++ if (!st->stream) { ++ err = errno; ++ goto out; ++ } ++ ++ pw_stream_add_listener(st->stream, &st->listener, &stream_events, st); ++ params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, ++ &SPA_AUDIO_INFO_RAW_INIT( ++ .format = aufmt_to_pw_format(prm->fmt), ++ .channels = prm->ch, ++ .rate = prm->srate )); ++ if (!params[0]) ++ goto out; ++ ++ err = pw_stream_connect(st->stream, ++ PW_DIRECTION_OUTPUT, ++ PW_ID_ANY, ++ PW_STREAM_FLAG_AUTOCONNECT | ++ PW_STREAM_FLAG_MAP_BUFFERS | ++ PW_STREAM_FLAG_RT_PROCESS, ++ params, 1); ++ ++ pw_thread_loop_unlock(pw_loop_instance()); ++ ++ info ("pipewire: stream %s started (%m)\n", name, err); ++ ++ out: ++ if (err) ++ mem_deref(st); ++ else ++ *stp = st; ++ ++ return err; ++} ++ ++ ++/** ++ * Pipewire process callback ++ * ++ * @param arg Argument (auplay_st object) ++ */ ++static void on_process(void *arg) ++{ ++ struct auplay_st *st = arg; ++ struct pw_buffer *b; ++ struct spa_buffer *buf; ++ struct auframe af; ++ void *sampv; ++ ++ b = pw_stream_dequeue_buffer(st->stream); ++ if (!b) ++ warning("pipewire: out of buffers (%m)\n", errno); ++ ++ buf = b->buffer; ++ sampv = buf->datas[0].data; ++ if (!sampv) ++ return; ++ ++ if (buf->datas[0].maxsize < st->nbytes) { ++ warning("pipewire: buffer to small\n"); ++ return; ++ } ++ ++ auframe_init(&af, st->prm.fmt, sampv, st->sampc, ++ st->prm.srate, st->prm.ch); ++ ++ st->wh(&af, st->arg); ++ ++ buf->datas[0].chunk->offset = 0; ++ buf->datas[0].chunk->stride = st->stride; ++ buf->datas[0].chunk->size = auframe_size(&af); ++ ++ pw_stream_queue_buffer(st->stream, b); ++} + +From 2c17a323ec53a9512ea2374e86b73057cecff9c5 Mon Sep 17 00:00:00 2001 +From: Christian Spielberger +Date: Thu, 2 Feb 2023 10:57:15 +0100 +Subject: [PATCH 02/10] cmake: repair FindPIPEWIRE.cmake + +--- + cmake/FindPIPEWIRE.cmake | 4 ++++ + 1 file changed, 4 insertions(+) + +diff --git a/cmake/FindPIPEWIRE.cmake b/cmake/FindPIPEWIRE.cmake +index bd9b90181..20fa46a6f 100644 +--- a/cmake/FindPIPEWIRE.cmake ++++ b/cmake/FindPIPEWIRE.cmake +@@ -8,3 +8,7 @@ if(NOT WIN32) + find_package(PkgConfig) + pkg_search_module(PIPEWIRE libpipewire-0.3) + endif() ++ ++include(FindPackageHandleStandardArgs) ++find_package_handle_standard_args(PIPEWIRE DEFAULT_MSG PIPEWIRE_INCLUDE_DIRS ++ PIPEWIRE_LIBRARIES) + +From fc4496f2b527a9216b59a2c2a1d2938b3eedef9f Mon Sep 17 00:00:00 2001 +From: Christian Spielberger +Date: Thu, 2 Feb 2023 14:19:41 +0100 +Subject: [PATCH 03/10] pipewire: set node latency and use chunk offset + +--- + modules/pipewire/capture.c | 16 +++++++++++++--- + 1 file changed, 13 insertions(+), 3 deletions(-) + +diff --git a/modules/pipewire/capture.c b/modules/pipewire/capture.c +index 3e92d131c..697845e7d 100644 +--- a/modules/pipewire/capture.c ++++ b/modules/pipewire/capture.c +@@ -58,6 +58,7 @@ int pw_capture_alloc(struct ausrc_st **stp, const struct ausrc *as, + struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, + sizeof(buffer)); + const char name[] = "baresip-capture"; ++ char nlat[10]; + int err = 0; + + if (!stp || !as || !prm || !rh) +@@ -81,6 +82,7 @@ int pw_capture_alloc(struct ausrc_st **stp, const struct ausrc *as, + st->rh = rh; + st->errh = errh; + st->arg = arg; ++ re_snprintf(nlat, sizeof(nlat), "%u/1000", prm->ptime); + + pw_thread_loop_lock (pw_loop_instance()); + st->stream = pw_stream_new(pw_core_instance(), name, +@@ -88,6 +90,7 @@ int pw_capture_alloc(struct ausrc_st **stp, const struct ausrc *as, + PW_KEY_MEDIA_TYPE, "Audio", + PW_KEY_MEDIA_CATEGORY, "Capture", + PW_KEY_MEDIA_ROLE, "Communication", ++ PW_KEY_NODE_LATENCY, nlat, + NULL)); + if (!st->stream) { + err = errno; +@@ -135,7 +138,10 @@ static void on_process(void *arg) + struct ausrc_st *st = arg; + struct pw_buffer *b; + struct spa_buffer *buf; ++ struct spa_data *d; + struct auframe af; ++ uint32_t offs; ++ uint32_t size; + + void *sampv; + size_t sampc; +@@ -145,11 +151,15 @@ static void on_process(void *arg) + warning("pipewire: out of buffers (%m)\n", errno); + + buf = b->buffer; +- sampv = buf->datas[0].data; +- if (!sampv) ++ d = &buf->datas[0]; ++ ++ if (!d->data) + return; + +- sampc = buf->datas[0].chunk->size / st->sampsz; ++ offs = SPA_MIN(d->chunk->offset, d->maxsize); ++ size = SPA_MIN(d->chunk->size, d->maxsize - offs); ++ sampv = SPA_PTROFF(d->data, offs, void); ++ sampc = size / st->sampsz; + + auframe_init(&af, st->prm.fmt, sampv, sampc, + st->prm.srate, st->prm.ch); + +From 6dd765174f55bb05ad9e4a339ab8f9b5d9ad4806 Mon Sep 17 00:00:00 2001 +From: Christian Spielberger +Date: Thu, 2 Feb 2023 14:20:30 +0100 +Subject: [PATCH 04/10] pipewire: playback cleanup + +--- + modules/pipewire/playback.c | 15 +++++++++------ + 1 file changed, 9 insertions(+), 6 deletions(-) + +diff --git a/modules/pipewire/playback.c b/modules/pipewire/playback.c +index 94fceb1b0..012731570 100644 +--- a/modules/pipewire/playback.c ++++ b/modules/pipewire/playback.c +@@ -133,6 +133,7 @@ static void on_process(void *arg) + struct auplay_st *st = arg; + struct pw_buffer *b; + struct spa_buffer *buf; ++ struct spa_data *d; + struct auframe af; + void *sampv; + +@@ -141,11 +142,13 @@ static void on_process(void *arg) + warning("pipewire: out of buffers (%m)\n", errno); + + buf = b->buffer; +- sampv = buf->datas[0].data; +- if (!sampv) ++ d = &buf->datas[0]; ++ ++ if (!d->data) + return; + +- if (buf->datas[0].maxsize < st->nbytes) { ++ sampv = d->data; ++ if (d->maxsize < st->nbytes) { + warning("pipewire: buffer to small\n"); + return; + } +@@ -155,9 +158,9 @@ static void on_process(void *arg) + + st->wh(&af, st->arg); + +- buf->datas[0].chunk->offset = 0; +- buf->datas[0].chunk->stride = st->stride; +- buf->datas[0].chunk->size = auframe_size(&af); ++ d->chunk->offset = 0; ++ d->chunk->stride = st->stride; ++ d->chunk->size = auframe_size(&af); + + pw_stream_queue_buffer(st->stream, b); + } + +From d08fcfe3cf2eb5cbc2f29958f9d1aff3ce1ecb66 Mon Sep 17 00:00:00 2001 +From: Christian Spielberger +Date: Fri, 3 Feb 2023 08:19:30 +0100 +Subject: [PATCH 05/10] pipewire: device selection + +--- + modules/pipewire/capture.c | 3 +- + modules/pipewire/pipewire.c | 155 +++++++++++++++++++++++++++++++++--- + modules/pipewire/pipewire.h | 1 + + modules/pipewire/playback.c | 3 +- + 4 files changed, 151 insertions(+), 11 deletions(-) + +diff --git a/modules/pipewire/capture.c b/modules/pipewire/capture.c +index 697845e7d..36f6d5d90 100644 +--- a/modules/pipewire/capture.c ++++ b/modules/pipewire/capture.c +@@ -90,6 +90,7 @@ int pw_capture_alloc(struct ausrc_st **stp, const struct ausrc *as, + PW_KEY_MEDIA_TYPE, "Audio", + PW_KEY_MEDIA_CATEGORY, "Capture", + PW_KEY_MEDIA_ROLE, "Communication", ++ PW_KEY_TARGET_OBJECT, dev, + PW_KEY_NODE_LATENCY, nlat, + NULL)); + if (!st->stream) { +@@ -108,7 +109,7 @@ int pw_capture_alloc(struct ausrc_st **stp, const struct ausrc *as, + + err = pw_stream_connect(st->stream, + PW_DIRECTION_INPUT, +- PW_ID_ANY, ++ pw_device_id(dev), + PW_STREAM_FLAG_AUTOCONNECT | + PW_STREAM_FLAG_MAP_BUFFERS | + PW_STREAM_FLAG_RT_PROCESS, +diff --git a/modules/pipewire/pipewire.c b/modules/pipewire/pipewire.c +index c9e6aaff4..182420f8e 100644 +--- a/modules/pipewire/pipewire.c ++++ b/modules/pipewire/pipewire.c +@@ -22,7 +22,16 @@ + */ + + enum { +- RECONN_DELAY = 1500, ++ RECONN_DELAY = 1500, ++ DEV_HASH_SIZE = 16, ++}; ++ ++ ++struct pw_dev { ++ struct le he; ++ ++ char *node_name; ++ uint32_t id; + }; + + +@@ -30,19 +39,27 @@ struct pw_stat { + struct pw_thread_loop *loop; + struct pw_context *context; + struct pw_core *core; ++ struct pw_registry *registry; ++ struct spa_hook registry_listener; ++ ++ struct auplay *auplay; ++ struct ausrc *ausrc; ++ struct hash *devices; + }; + + + static struct pw_stat *d = NULL; + +-static struct auplay *auplay = NULL; +-static struct ausrc *ausrc = NULL; +- + + static void destructor(void *arg) + { + struct pw_stat *pw = arg; + ++ mem_deref(pw->auplay); ++ mem_deref(pw->ausrc); ++ hash_flush(pw->devices); ++ mem_deref(pw->devices); ++ + if (pw->core) + pw_core_disconnect(pw->core); + +@@ -56,6 +73,105 @@ static void destructor(void *arg) + } + + ++static void pw_dev_destructor(void *arg) ++{ ++ struct pw_dev *pwd = arg; ++ ++ mem_deref(pwd->node_name); ++} ++ ++ ++static int pw_dev_add(uint32_t id, const char *node_name) ++{ ++ struct pw_dev *pwd; ++ int err; ++ ++ pwd = mem_zalloc(sizeof(*pwd), pw_dev_destructor); ++ if (!pwd) ++ return ENOMEM; ++ ++ pwd->id = id; ++ err = str_dup(&pwd->node_name, node_name); ++ if (err) { ++ mem_deref(pwd); ++ return ENOMEM; ++ } ++ ++ hash_append(d->devices, hash_joaat_str(node_name), &pwd->he, pwd); ++ return 0; ++} ++ ++ ++static void registry_event_global(void *arg, uint32_t id, ++ uint32_t permissions, const char *type, uint32_t version, ++ const struct spa_dict *props) ++{ ++ struct pw_stat *pw = arg; ++ const char *media_class; ++ const char *node_name; ++ (void)permissions; ++ (void)version; ++ ++ if (str_cmp(type, PW_TYPE_INTERFACE_Node)) ++ return; ++ ++ media_class = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS); ++ node_name = spa_dict_lookup(props, PW_KEY_NODE_NAME); ++ if (!str_cmp(media_class, "Audio/Source") && str_isset(node_name)) { ++ debug("pipewire: adding (%u) %s: \"%s\"\n", ++ id, media_class, node_name); ++ mediadev_add(&pw->ausrc->dev_list, node_name); ++ (void)pw_dev_add(id, node_name); ++ } ++ ++ if (!str_cmp(media_class, "Audio/Sink") && str_isset(node_name)) { ++ debug("pipewire: adding (%u) %s: \"%s\"\n", ++ id, media_class, node_name); ++ mediadev_add(&pw->auplay->dev_list, node_name); ++ (void)pw_dev_add(id, node_name); ++ } ++} ++ ++ ++static const struct pw_registry_events registry_events = { ++ PW_VERSION_REGISTRY_EVENTS, ++ .global = registry_event_global, ++}; ++ ++ ++struct pwd_cmp { ++ const char *node_name; ++}; ++ ++ ++static bool pw_dev_cmp(struct le *le, void *arg) ++{ ++ const struct pwd_cmp *cmp = arg; ++ const struct pw_dev *pwd = le->data; ++ ++ return !str_cmp(pwd->node_name, cmp->node_name); ++} ++ ++ ++int pw_device_id(const char *node_name) ++{ ++ struct le *le; ++ struct pw_dev *pwd; ++ struct pwd_cmp cmp; ++ ++ cmp.node_name = node_name; ++ ++ le = hash_lookup(d->devices, hash_joaat_str(node_name), ++ pw_dev_cmp, &cmp); ++ ++ if (!le || !le->data) ++ return PW_ID_ANY; ++ ++ pwd = le->data; ++ return pwd->id; ++} ++ ++ + static struct pw_stat *pw_stat_alloc(void) + { + struct pw_stat *pw; +@@ -93,6 +209,29 @@ static struct pw_stat *pw_stat_alloc(void) + } + + ++static int pw_start_registry_scan(struct pw_stat *pw) ++{ ++ int err; ++ ++ pw->registry = pw_core_get_registry(pw->core, PW_VERSION_REGISTRY, ++ 0 /* user_data size */); ++ ++ if (!pw->registry) ++ return errno; ++ ++ err = hash_alloc(&pw->devices, DEV_HASH_SIZE); ++ if (err) ++ return err; ++ ++ pw_thread_loop_lock (pw_loop_instance()); ++ spa_zero(pw->registry_listener); ++ pw_registry_add_listener(pw->registry, &pw->registry_listener, ++ ®istry_events, pw); ++ pw_thread_loop_unlock(pw_loop_instance()); ++ return 0; ++} ++ ++ + struct pw_core *pw_core_instance(void) + { + if (!d) +@@ -133,20 +272,18 @@ static int module_init(void) + if (!d) + return errno; + +- err = auplay_register(&auplay, baresip_auplayl(), ++ err = auplay_register(&d->auplay, baresip_auplayl(), + "pipewire", pw_playback_alloc); +- err |= ausrc_register(&ausrc, baresip_ausrcl(), ++ err |= ausrc_register(&d->ausrc, baresip_ausrcl(), + "pipewire", pw_capture_alloc); + ++ err |= pw_start_registry_scan(d); + return err; + } + + + static int module_close(void) + { +- auplay = mem_deref(auplay); +- ausrc = mem_deref(ausrc); +- + d = mem_deref(d); + pw_deinit(); + return 0; +diff --git a/modules/pipewire/pipewire.h b/modules/pipewire/pipewire.h +index 0a93aaf52..4d4c9787b 100644 +--- a/modules/pipewire/pipewire.h ++++ b/modules/pipewire/pipewire.h +@@ -7,6 +7,7 @@ + int aufmt_to_pw_format(enum aufmt fmt); + struct pw_core *pw_core_instance(void); + struct pw_thread_loop *pw_loop_instance(void); ++int pw_device_id(const char *node_name); + + int pw_playback_alloc(struct auplay_st **stp, + const struct auplay *ap, +diff --git a/modules/pipewire/playback.c b/modules/pipewire/playback.c +index 012731570..24b4f2c3a 100644 +--- a/modules/pipewire/playback.c ++++ b/modules/pipewire/playback.c +@@ -86,6 +86,7 @@ int pw_playback_alloc(struct auplay_st **stp, const struct auplay *ap, + PW_KEY_MEDIA_TYPE, "Audio", + PW_KEY_MEDIA_CATEGORY, "Playback", + PW_KEY_MEDIA_ROLE, "Communication", ++ PW_KEY_TARGET_OBJECT, dev, + NULL)); + if (!st->stream) { + err = errno; +@@ -103,7 +104,7 @@ int pw_playback_alloc(struct auplay_st **stp, const struct auplay *ap, + + err = pw_stream_connect(st->stream, + PW_DIRECTION_OUTPUT, +- PW_ID_ANY, ++ pw_device_id(dev), + PW_STREAM_FLAG_AUTOCONNECT | + PW_STREAM_FLAG_MAP_BUFFERS | + PW_STREAM_FLAG_RT_PROCESS, + +From 86e4a4fd4e0ec7723b7e3129e868f5dd599c5e2f Mon Sep 17 00:00:00 2001 +From: Christian Spielberger +Date: Wed, 1 Mar 2023 09:30:21 +0100 +Subject: [PATCH 06/10] pipewire: set _XOPEN_SOURCE=700 + +--- + modules/pipewire/CMakeLists.txt | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/modules/pipewire/CMakeLists.txt b/modules/pipewire/CMakeLists.txt +index 8673873af..bb2ad8f2a 100644 +--- a/modules/pipewire/CMakeLists.txt ++++ b/modules/pipewire/CMakeLists.txt +@@ -14,4 +14,5 @@ target_link_libraries(${PROJECT_NAME} PRIVATE ${PIPEWIRE_LIBRARIES}) + target_compile_options(${PROJECT_NAME} PRIVATE + -Wno-pedantic + -Wno-bad-function-cast ++ -D_XOPEN_SOURCE=700 + ) + +From 63f9a8a2afad5eff06f6fc55b87276a881f25c08 Mon Sep 17 00:00:00 2001 +From: Christian Spielberger +Date: Wed, 1 Mar 2023 09:34:19 +0100 +Subject: [PATCH 07/10] pipewire: replace _XOPEN_SOURCE by _GNU_SOURCE + +--- + modules/pipewire/CMakeLists.txt | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/modules/pipewire/CMakeLists.txt b/modules/pipewire/CMakeLists.txt +index bb2ad8f2a..50a4befef 100644 +--- a/modules/pipewire/CMakeLists.txt ++++ b/modules/pipewire/CMakeLists.txt +@@ -14,5 +14,5 @@ target_link_libraries(${PROJECT_NAME} PRIVATE ${PIPEWIRE_LIBRARIES}) + target_compile_options(${PROJECT_NAME} PRIVATE + -Wno-pedantic + -Wno-bad-function-cast +- -D_XOPEN_SOURCE=700 ++ -D_GNU_SOURCE + ) + +From 572d69b24917e66a76015b8605eb58b8e5377969 Mon Sep 17 00:00:00 2001 +From: Christian Spielberger +Date: Wed, 1 Mar 2023 10:28:30 +0100 +Subject: [PATCH 08/10] pipewire: thread safe stream termination + +--- + modules/pipewire/capture.c | 5 ++++- + modules/pipewire/playback.c | 5 ++++- + 2 files changed, 8 insertions(+), 2 deletions(-) + +diff --git a/modules/pipewire/capture.c b/modules/pipewire/capture.c +index 36f6d5d90..8d20eec52 100644 +--- a/modules/pipewire/capture.c ++++ b/modules/pipewire/capture.c +@@ -42,9 +42,11 @@ static void ausrc_destructor(void *arg) + { + struct ausrc_st *st = arg; + ++ pw_thread_loop_lock (pw_loop_instance()); + st->rh = NULL; + st->errh = NULL; + pw_stream_destroy(st->stream); ++ pw_thread_loop_unlock(pw_loop_instance()); + } + + +@@ -168,7 +170,8 @@ static void on_process(void *arg) + af.timestamp = st->samps * AUDIO_TIMEBASE / + (st->prm.srate * st->prm.ch); + st->samps += sampc; +- st->rh(&af, st->arg); ++ if (st->rh) ++ st->rh(&af, st->arg); + + pw_stream_queue_buffer(st->stream, b); + } +diff --git a/modules/pipewire/playback.c b/modules/pipewire/playback.c +index 24b4f2c3a..55aff42c6 100644 +--- a/modules/pipewire/playback.c ++++ b/modules/pipewire/playback.c +@@ -40,8 +40,10 @@ static void auplay_destructor(void *arg) + { + struct auplay_st *st = arg; + ++ pw_thread_loop_lock (pw_loop_instance()); + st->wh = NULL; + pw_stream_destroy(st->stream); ++ pw_thread_loop_unlock(pw_loop_instance()); + } + + +@@ -157,7 +159,8 @@ static void on_process(void *arg) + auframe_init(&af, st->prm.fmt, sampv, st->sampc, + st->prm.srate, st->prm.ch); + +- st->wh(&af, st->arg); ++ if (st->wh) ++ st->wh(&af, st->arg); + + d->chunk->offset = 0; + d->chunk->stride = st->stride; + +From dd142036ac430c8905ef90b1f73da7fe742b7a41 Mon Sep 17 00:00:00 2001 +From: Christian Spielberger +Date: Wed, 1 Mar 2023 10:29:15 +0100 +Subject: [PATCH 09/10] pipewire: remove unused errh + +--- + modules/pipewire/capture.c | 4 +--- + 1 file changed, 1 insertion(+), 3 deletions(-) + +diff --git a/modules/pipewire/capture.c b/modules/pipewire/capture.c +index 8d20eec52..00f6f5e99 100644 +--- a/modules/pipewire/capture.c ++++ b/modules/pipewire/capture.c +@@ -21,7 +21,6 @@ struct ausrc_st { + struct ausrc_prm prm; + ausrc_read_h *rh; + struct spa_hook listener; +- ausrc_error_h *errh; + + size_t sampsz; + uint64_t samps; +@@ -44,7 +43,6 @@ static void ausrc_destructor(void *arg) + + pw_thread_loop_lock (pw_loop_instance()); + st->rh = NULL; +- st->errh = NULL; + pw_stream_destroy(st->stream); + pw_thread_loop_unlock(pw_loop_instance()); + } +@@ -62,6 +60,7 @@ int pw_capture_alloc(struct ausrc_st **stp, const struct ausrc *as, + const char name[] = "baresip-capture"; + char nlat[10]; + int err = 0; ++ (void)errh; + + if (!stp || !as || !prm || !rh) + return EINVAL; +@@ -82,7 +81,6 @@ int pw_capture_alloc(struct ausrc_st **stp, const struct ausrc *as, + st->samps = 0; + + st->rh = rh; +- st->errh = errh; + st->arg = arg; + re_snprintf(nlat, sizeof(nlat), "%u/1000", prm->ptime); + + +From b3a65e2ddb353aeb28f45daad5e7bb575bb17ee0 Mon Sep 17 00:00:00 2001 +From: Christian Spielberger +Date: Thu, 2 Mar 2023 07:18:30 +0100 +Subject: [PATCH 10/10] config: add pipewire as option for DEFAULT_AUDIO_DEVICE + +--- + docs/examples/config | 1 + + src/config.c | 9 ++++++++- + 2 files changed, 9 insertions(+), 1 deletion(-) + +diff --git a/docs/examples/config b/docs/examples/config +index 07a8c8920..ff5fbbe33 100644 +--- a/docs/examples/config ++++ b/docs/examples/config +@@ -118,6 +118,7 @@ module auresamp.so + # Audio driver Modules + module alsa.so + #module pulse.so ++#module pipewire.so + #module jack.so + #module portaudio.so + #module aubridge.so +diff --git a/src/config.c b/src/config.c +index 7948215b3..37284966d 100644 +--- a/src/config.c ++++ b/src/config.c +@@ -975,13 +975,20 @@ int config_write_template(const char *file, const struct config *cfg) + #elif defined (WIN32) + (void)re_fprintf(f, "module\t\t\t" "winwave" MOD_EXT "\n"); + #else +- if (!strncmp(default_audio_device(), "pulse", 5)) { ++ if (!strncmp(default_audio_device(), "pipewire", 8)) { ++ (void)re_fprintf(f, "#module\t\t\t" "alsa" MOD_EXT "\n"); ++ (void)re_fprintf(f, "#module\t\t\t" "pulse" MOD_EXT "\n"); ++ (void)re_fprintf(f, "module\t\t\t" "pipewire" MOD_EXT "\n"); ++ } ++ else if (!strncmp(default_audio_device(), "pulse", 5)) { + (void)re_fprintf(f, "#module\t\t\t" "alsa" MOD_EXT "\n"); + (void)re_fprintf(f, "module\t\t\t" "pulse" MOD_EXT "\n"); ++ (void)re_fprintf(f, "#module\t\t\t" "pipewire" MOD_EXT "\n"); + } + else { + (void)re_fprintf(f, "module\t\t\t" "alsa" MOD_EXT "\n"); + (void)re_fprintf(f, "#module\t\t\t" "pulse" MOD_EXT"\n"); ++ (void)re_fprintf(f, "#module\t\t\t" "pipewire" MOD_EXT "\n"); + } + #endif + (void)re_fprintf(f, "#module\t\t\t" "jack" MOD_EXT "\n"); diff --git a/baresip.spec b/baresip.spec index 3aeba58..d4d5253 100644 --- a/baresip.spec +++ b/baresip.spec @@ -1,6 +1,6 @@ Summary: Modular SIP user-agent with audio and video support Name: baresip -Version: 2.12.0 +Version: 3.0.0 Release: 1%{?dist} License: BSD-3-Clause URL: https://github.com/baresip/baresip @@ -11,14 +11,14 @@ Source11: https://gitlab.gnome.org/GNOME/adwaita-icon-theme/-/raw/1e1d6921 Source12: https://gitlab.gnome.org/GNOME/adwaita-icon-theme/-/raw/master/COPYING#/COPYING.adwaita-icon-theme Source13: https://gitlab.gnome.org/GNOME/adwaita-icon-theme/-/raw/master/COPYING_CCBYSA3#/COPYING_CCBYSA3.adwaita-icon-theme Source14: https://gitlab.gnome.org/GNOME/adwaita-icon-theme/-/raw/master/COPYING_LGPL#/COPYING_LGPL.adwaita-icon-theme +Patch0: https://patch-diff.githubusercontent.com/raw/baresip/baresip/pull/2439.patch#/baresip-3.0.0-pipewire.patch BuildRequires: cmake %if 0%{?rhel} && 0%{?rhel} < 8 BuildRequires: cmake3 %endif BuildRequires: gcc BuildRequires: gcc-c++ -BuildRequires: libre-devel >= 2.12.0 -BuildRequires: librem-devel >= 2.12.0 +BuildRequires: libre-devel >= 3.0.0 %if 0%{?fedora} || 0%{?rhel} >= 8 BuildRequires: openssl-devel >= 1.1.0 %else @@ -26,11 +26,15 @@ BuildRequires: openssl11-devel # Atomic support in libre >= 2.1.0 BuildRequires: devtoolset-8-toolchain %endif -%if 0%{?fedora} || 0%{?rhel} > 7 +%if 0%{?fedora} || 0%{?rhel} > 8 +Recommends: %{name}-pipewire%{?_isa} = %{version}-%{release} +%else +%if 0%{?rhel} == 8 Recommends: %{name}-pulse%{?_isa} = %{version}-%{release} %else Requires: %{name}-pulse%{?_isa} = %{version}-%{release} %endif +%endif Obsoletes: %{name}-cairo < 1.1.0-1 Obsoletes: %{name}-rst < 2.0.0-1 Obsoletes: %{name}-speex_pp < 2.0.0-1 @@ -233,6 +237,18 @@ Baresip is a modular SIP user-agent with audio and video support. This module provides the Opus speech and audio codec module. +%if 0%{?fedora} || 0%{?rhel} > 8 +%package pipewire +Summary: PipeWire audio driver for baresip +BuildRequires: pkgconfig(libpipewire-0.3) +Requires: %{name}%{?_isa} = %{version}-%{release} + +%description pipewire +Baresip is a modular SIP user-agent with audio and video support. + +This module provides the PipeWire audio driver. +%endif + %package plc Summary: Packet Loss Concealment module for baresip BuildRequires: spandsp-devel @@ -351,6 +367,7 @@ This module provides the X11 video output driver. %prep %setup -q +%patch0 -p1 -b .pipewire %build %if 0%{?rhel} && 0%{?rhel} < 8 @@ -363,7 +380,11 @@ This module provides the X11 video output driver. %cmake \ -DDEFAULT_CAFILE:PATH="%{_sysconfdir}/pki/tls/certs/ca-bundle.crt" \ +%if 0%{?fedora} || 0%{?rhel} > 8 + -DDEFAULT_AUDIO_DEVICE:STRING="pipewire" \ +%else -DDEFAULT_AUDIO_DEVICE:STRING="pulse" \ +%endif %if 0%{?rhel} && 0%{?rhel} < 8 -DOPENSSL_ROOT_DIR:PATH="%{_includedir}/openssl11;%{_libdir}/openssl11" %endif @@ -425,7 +446,7 @@ gtk-update-icon-cache --force %{_datadir}/icons/Adwaita &>/dev/null || : %license LICENSE %doc CHANGELOG.md docs/THANKS docs/examples %{_bindir}/%{name} -%{_libdir}/lib%{name}.so.4* +%{_libdir}/lib%{name}.so.5* %dir %{_libdir}/%{name}/ %dir %{_libdir}/%{name}/modules/ %{_libdir}/%{name}/modules/account.so @@ -526,6 +547,11 @@ gtk-update-icon-cache --force %{_datadir}/icons/Adwaita &>/dev/null || : %{_libdir}/%{name}/modules/opus.so %{_libdir}/%{name}/modules/opus_multistream.so +%if 0%{?fedora} || 0%{?rhel} > 8 +%files pipewire +%{_libdir}/%{name}/modules/pipewire.so +%endif + %files plc %{_libdir}/%{name}/modules/plc.so @@ -560,6 +586,10 @@ gtk-update-icon-cache --force %{_datadir}/icons/Adwaita &>/dev/null || : %{_libdir}/%{name}/modules/x11.so %changelog +* Mon Mar 20 2023 Robert Scheck 3.0.0-1 +- Upgrade to 3.0.0 (#2180064) +- Added (hopefully future upstream) patch for PipeWire support + * Sat Feb 18 2023 Robert Scheck 2.12.0-1 - Upgrade to 2.12.0 (#2170292) diff --git a/sources b/sources index d4472c1..73863ba 100644 --- a/sources +++ b/sources @@ -1,4 +1,4 @@ -SHA512 (baresip-2.12.0.tar.gz) = b0c60be4b30c6e4497cf57a6a8e9598f840b85e6ed2088fe204f9bb2ccbd923aa32c3b9f6b62781d002c133cf092e6a8aa0ce9fad37bb396edb7e70562874eaa +SHA512 (baresip-3.0.0.tar.gz) = 7eaba0d36d14f6f4bb5077a816f3650192aa7d9ac372265a667bced24b10d13f4669eb426fb1121c3d3366ad01e3134ab25e494cfa987fcb7bb386679b265819 SHA512 (call-incoming-symbolic.svg) = 49b6422efff9986dd4a18b34df4ab185b01b46c44ab5b8c1d45ab1ca25694cb42e73428b0a69e5fe2eb61c4e7a7aba9c0df82b5e0290f45950f3942be63bf987 SHA512 (call-outgoing-symbolic.svg) = 142cf668d977e3a709d3c13e01d86fdd09e501affd1756df3000de84581c55b3b5082758b32a73ae0e47f45233cc7e55609f3e54effbba01666ca97d5a55fdaa SHA512 (COPYING.adwaita-icon-theme) = e8963bab4d94d9fbcfc930b95164afff9502e4e53209104f4836fcd6dab2c2e9f105c9f9a17b43bc9502903c9c7b2e0880dacf2339cbe110a19654f13742e20a