From 3e0dc2678bae4d0ecea8f2baa495ec884809be93 Mon Sep 17 00:00:00 2001 From: Dmitry Sharshakov Date: Sun, 15 Jan 2023 08:01:37 +0300 Subject: [PATCH] filter-chain: add spatializer SOFA is a file format used for storing and accessing spatial audio data, namely head-related transfer functions. These can be used to create binaural spatial sound using head- or earphones. This commit introduces libmysofa as an optional dependency for loading SOFA files and creates a spatializer plugin for the filter-chain ci: install libmysofa-devel for full build ci: bump FDO_DISTRIBUTION_TAG --- .gitlab-ci.yml | 3 +- meson.build | 3 + meson_options.txt | 4 + src/modules/meson.build | 2 +- .../module-filter-chain/builtin_plugin.c | 252 ++++++++++++++++++ 5 files changed, 262 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2ab6a08f3..61fbfafba 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -25,7 +25,7 @@ include: .fedora: variables: # Update this tag when you want to trigger a rebuild - FDO_DISTRIBUTION_TAG: '2022-11-07.0' + FDO_DISTRIBUTION_TAG: '2023-01-18.0' FDO_DISTRIBUTION_VERSION: '35' FDO_DISTRIBUTION_PACKAGES: >- alsa-lib-devel @@ -46,6 +46,7 @@ include: jack-audio-connection-kit-devel libcanberra-devel libldac-devel + libmysofa-devel libsndfile-devel libusb-devel lilv-devel diff --git a/meson.build b/meson.build index f99350e99..feca30ae1 100644 --- a/meson.build +++ b/meson.build @@ -285,6 +285,9 @@ ncurses_dep = dependency('ncursesw', required : false) sndfile_dep = dependency('sndfile', version : '>= 1.0.20', required : get_option('sndfile')) summary({'sndfile': sndfile_dep.found()}, bool_yn: true, section: 'pw-cat/pw-play/pw-dump/filter-chain') cdata.set('HAVE_SNDFILE', sndfile_dep.found()) +libmysofa_dep = dependency('libmysofa', required : get_option('libmysofa')) +summary({'libmysofa': libmysofa_dep.found()}, bool_yn: true, section: 'filter-chain') +cdata.set('HAVE_LIBMYSOFA', libmysofa_dep.found()) pulseaudio_dep = dependency('libpulse', required : get_option('libpulse')) summary({'libpulse': pulseaudio_dep.found()}, bool_yn: true, section: 'Streaming between daemons') avahi_dep = dependency('avahi-client', required : get_option('avahi')) diff --git a/meson_options.txt b/meson_options.txt index b4555f2d4..99b057c52 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -213,6 +213,10 @@ option('sndfile', description: 'Enable code that depends on libsndfile', type: 'feature', value: 'auto') +option('libmysofa', + description: 'Enable code that depends on libmysofa', + type: 'feature', + value: 'auto') option('libpulse', description: 'Enable code that depends on libpulse', type: 'feature', diff --git a/src/modules/meson.build b/src/modules/meson.build index 1bfb03b4b..6f3d7424c 100644 --- a/src/modules/meson.build +++ b/src/modules/meson.build @@ -111,7 +111,7 @@ filter_chain_sources = [ 'module-filter-chain/convolver.c' ] filter_chain_dependencies = [ - mathlib, dl_lib, pipewire_dep, sndfile_dep, audioconvert_dep + mathlib, dl_lib, pipewire_dep, sndfile_dep, audioconvert_dep, libmysofa_dep ] if lilv_lib.found() diff --git a/src/modules/module-filter-chain/builtin_plugin.c b/src/modules/module-filter-chain/builtin_plugin.c index 068df9932..4e0b72894 100644 --- a/src/modules/module-filter-chain/builtin_plugin.c +++ b/src/modules/module-filter-chain/builtin_plugin.c @@ -26,10 +26,16 @@ #include #include + #ifdef HAVE_SNDFILE #include #endif +#ifdef HAVE_LIBMYSOFA +#include +#include +#endif + #include #include #include @@ -46,6 +52,14 @@ #define MAX_RATES 32u +#ifdef HAVE_LIBMYSOFA +// > If your program is using several threads, you must use +// > appropriate synchronisation mechanisms so only +// > a single thread can access the mysofa_open_cached +// > and mysofa_close_cached functions at a given time. +static pthread_mutex_t libmysofa_mutex = PTHREAD_MUTEX_INITIALIZER; +#endif + static struct dsp_ops *dsp_ops; struct builtin { @@ -847,6 +861,242 @@ static const struct fc_descriptor convolve_desc = { .cleanup = convolver_cleanup, }; +struct spatializer_impl { + unsigned long rate; + float *port[64]; + + struct convolver *l_conv; + struct convolver *r_conv; +}; + +static void * spatializer_instantiate(const struct fc_descriptor * Descriptor, + unsigned long SampleRate, int index, const char *config) +{ +#ifdef HAVE_LIBMYSOFA + struct spatializer_impl *impl; + float *samples = NULL; + int offset = 0, length = 0, n_samples; + struct spa_json it[2]; + const char *val; + char key[256]; + char filename[PATH_MAX] = ""; + int blocksize = 0, tailsize = 0; + float coords[3] = { 0, 0, 1 }; + struct MYSOFA_EASY *sofa; + + errno = EINVAL; + if (config == NULL) + return NULL; + + spa_json_init(&it[0], config, strlen(config)); + if (spa_json_enter_object(&it[0], &it[1]) <= 0) + return NULL; + + while (spa_json_get_string(&it[1], key, sizeof(key)) > 0) { + if (spa_streq(key, "blocksize")) { + if (spa_json_get_int(&it[1], &blocksize) <= 0) { + pw_log_error("spatializer:blocksize requires a number"); + return NULL; + } + } + else if (spa_streq(key, "tailsize")) { + if (spa_json_get_int(&it[1], &tailsize) <= 0) { + pw_log_error("spatializer:tailsize requires a number"); + return NULL; + } + } + else if (spa_streq(key, "filename")) { + if (spa_json_get_string(&it[1], filename, sizeof(filename)) <= 0) { + pw_log_error("spatializer:filename requires a string"); + return NULL; + } + } + else if (spa_streq(key, "offset")) { + if (spa_json_get_int(&it[1], &offset) <= 0) { + pw_log_error("spatializer:offset requires a number"); + return NULL; + } + } + else if (spa_streq(key, "length")) { + if (spa_json_get_int(&it[1], &length) <= 0) { + pw_log_error("spatializer:length requires a number"); + return NULL; + } + } + else if (spa_streq(key, "azimuth")) { + if (spa_json_get_float(&it[1], &coords[0]) <= 0) { + pw_log_error("spatializer:azimuth requires a float number"); + return NULL; + } + } + else if (spa_streq(key, "elevation")) { + if (spa_json_get_float(&it[1], &coords[1]) <= 0) { + pw_log_error("spatializer:elevation requires a float number"); + return NULL; + } + } + else if (spa_streq(key, "radius")) { + if (spa_json_get_float(&it[1], &coords[2]) <= 0) { + pw_log_error("spatializer:radius requires a float number"); + return NULL; + } + } + else if (spa_json_next(&it[1], &val) < 0) + break; + } + if (!filename[0]) { + pw_log_error("spatializer:filename was not given"); + return NULL; + } + + int ret = MYSOFA_OK; + + pthread_mutex_lock(&libmysofa_mutex); + sofa = mysofa_open_cached(filename, SampleRate, &n_samples, &ret); + pthread_mutex_unlock(&libmysofa_mutex); + + if (ret != MYSOFA_OK) { + pw_log_error("Unable to load HRTF from %s: %d %m", filename, ret); + errno = ENOENT; + return NULL; + } + + float *left_ir = calloc(n_samples, sizeof(float)); + float *right_ir = calloc(n_samples, sizeof(float)); + float left_delay; + float right_delay; + + mysofa_s2c(coords); + + mysofa_getfilter_float( + sofa, + coords[0], + coords[1], + coords[2], + left_ir, + right_ir, + &left_delay, + &right_delay + ); + + // TODO: make use of delay + pw_log_info("delay l: %f, r: %f", left_delay, right_delay); + + if (blocksize <= 0) + blocksize = SPA_CLAMP(n_samples, 64, 256); + if (tailsize <= 0) + tailsize = SPA_CLAMP(4096, blocksize, 32768); + + pw_log_info("using n_samples:%u %d:%d blocksize sofa:%s", n_samples, + blocksize, tailsize, filename); + + impl = calloc(1, sizeof(*impl)); + if (impl == NULL) + goto error; + + impl->rate = SampleRate; + + impl->l_conv = convolver_new(dsp_ops, blocksize, tailsize, left_ir, n_samples); + if (impl->l_conv == NULL) + goto error; + + impl->r_conv = convolver_new(dsp_ops, blocksize, tailsize, right_ir, n_samples); + if (impl->r_conv == NULL) + goto error; + + free(samples); + if (sofa) { + pthread_mutex_lock(&libmysofa_mutex); + mysofa_close_cached(sofa); + pthread_mutex_unlock(&libmysofa_mutex); + } + + return impl; +error: + if (samples) + free(samples); + if (sofa) { + pthread_mutex_lock(&libmysofa_mutex); + mysofa_close_cached(sofa); + pthread_mutex_unlock(&libmysofa_mutex); + } + free(impl); + return NULL; +#else + pw_log_error("libmysofa is required for spatializer, but disabled at compile time"); + errno = EINVAL; + return NULL; +#endif +} + +static void spatializer_run(void * Instance, unsigned long SampleCount) +{ +#ifdef HAVE_LIBMYSOFA + struct spatializer_impl *impl = Instance; + convolver_run(impl->l_conv, impl->port[2], impl->port[0], SampleCount); + convolver_run(impl->r_conv, impl->port[2], impl->port[1], SampleCount); +#endif +} + +static void spatializer_connect_port(void * Instance, unsigned long Port, + float * DataLocation) +{ + struct spatializer_impl *impl = Instance; + impl->port[Port] = DataLocation; +} + +static void spatializer_cleanup(void * Instance) +{ + struct spatializer_impl *impl = Instance; + if (impl->l_conv) + convolver_free(impl->l_conv); + if (impl->r_conv) + convolver_free(impl->r_conv); +#ifdef HAVE_LIBMYSOFA + if (impl->sofa) { + pthread_mutex_lock(&libmysofa_mutex); + mysofa_close_cached(impl->sofa); + pthread_mutex_unlock(&libmysofa_mutex); + } +#endif + free(impl); +} + +static struct fc_port spatializer_ports[] = { + { .index = 0, + .name = "Out L", + .flags = FC_PORT_OUTPUT | FC_PORT_AUDIO, + }, + { .index = 1, + .name = "Out R", + .flags = FC_PORT_OUTPUT | FC_PORT_AUDIO, + }, + { .index = 2, + .name = "In", + .flags = FC_PORT_INPUT | FC_PORT_AUDIO, + }, +}; + +static void spatializer_deactivate(void * Instance) +{ + struct spatializer_impl *impl = Instance; + convolver_reset(impl->l_conv); + convolver_reset(impl->r_conv); +} + +static const struct fc_descriptor spatializer_desc = { + .name = "spatializer", + + .n_ports = 3, + .ports = spatializer_ports, + + .instantiate = spatializer_instantiate, + .connect_port = spatializer_connect_port, + .deactivate = spatializer_deactivate, + .run = spatializer_run, + .cleanup = spatializer_cleanup, +}; + /** delay */ struct delay_impl { unsigned long rate; @@ -1005,6 +1255,8 @@ static const struct fc_descriptor * builtin_descriptor(unsigned long Index) return &convolve_desc; case 11: return &delay_desc; + case 12: + return &spatializer_desc; } return NULL; }