From 12719816fcf129050cfd47dbfd36c586b0c5cd1d Mon Sep 17 00:00:00 2001 From: Vadim Zhukov Date: Sat, 14 Nov 2020 22:58:55 +0300 Subject: [PATCH] Add sndio support (#3715) Add sndio support --- .github/workflows/main.yml | 1 + cmake/Modules/FindSndio.cmake | 71 +++++ plugins/CMakeLists.txt | 5 + plugins/sndio/CMakeLists.txt | 35 +++ plugins/sndio/data/locale/en-US.ini | 6 + plugins/sndio/sndio-input.c | 418 ++++++++++++++++++++++++++++ plugins/sndio/sndio-input.h | 18 ++ plugins/sndio/sndio.c | 32 +++ 8 files changed, 586 insertions(+) create mode 100644 cmake/Modules/FindSndio.cmake create mode 100644 plugins/sndio/CMakeLists.txt create mode 100644 plugins/sndio/data/locale/en-US.ini create mode 100644 plugins/sndio/sndio-input.c create mode 100644 plugins/sndio/sndio-input.h create mode 100644 plugins/sndio/sndio.c diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 679f6e860..a605ecdf5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -358,6 +358,7 @@ jobs: libluajit-5.1-dev \ libpulse-dev \ libqt5x11extras5-dev \ + libsndio-dev \ libspeexdsp-dev \ libswresample-dev \ libswscale-dev \ diff --git a/cmake/Modules/FindSndio.cmake b/cmake/Modules/FindSndio.cmake new file mode 100644 index 000000000..4c206614d --- /dev/null +++ b/cmake/Modules/FindSndio.cmake @@ -0,0 +1,71 @@ +# Distributed under the OSI-approved BSD 3-Clause License. See accompanying +# file Copyright.txt or https://cmake.org/licensing for details. + +#[=======================================================================[.rst: +FindSndio +------- + +Finds the Sndio library. + +Imported Targets +^^^^^^^^^^^^^^^^ + +This module provides the following imported targets, if found: + +``Sndio::Sndio`` + The Sndio library + +Result Variables +^^^^^^^^^^^^^^^^ + +This will define the following variables: + +``Sndio_FOUND`` + True if the system has the Sndio library. +``Sndio_VERSION`` + The version of the Sndio library which was found. +``Sndio_INCLUDE_DIRS`` + Include directories needed to use Sndio. +``Sndio_LIBRARIES`` + Libraries needed to link to Sndio. + +Cache Variables +^^^^^^^^^^^^^^^ + +The following cache variables may also be set: + +``Sndio_INCLUDE_DIR`` + The directory containing ``sndio.h``. +``Sndio_LIBRARY`` + The path to the Sndio library. + +#]=======================================================================] + +find_path(Sndio_INCLUDE_DIR sndio.h) +find_library(Sndio_LIBRARY sndio) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Sndio + FOUND_VAR Sndio_FOUND + REQUIRED_VARS + Sndio_LIBRARY + Sndio_INCLUDE_DIR +) + +if(Sndio_FOUND) + set(Sndio_LIBRARIES ${Sndio_LIBRARY}) + set(Sndio_INCLUDE_DIRS ${Sndio_INCLUDE_DIR}) +endif() + +if(Sndio_FOUND AND NOT TARGET Sndio::Sndio) + add_library(Sndio::Sndio UNKNOWN IMPORTED) + set_target_properties(Sndio::Sndio PROPERTIES + IMPORTED_LOCATION "${Sndio_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${Sndio_INCLUDE_DIR}" + ) +endif() + +mark_as_advanced( + Sndio_INCLUDE_DIR + Sndio_LIBRARY +) diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index cf655e475..900bd8f31 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -42,6 +42,7 @@ elseif("${CMAKE_SYSTEM_NAME}" MATCHES "Linux") add_subdirectory(linux-alsa) add_subdirectory(decklink/linux) add_subdirectory(vlc-video) + add_subdirectory(sndio) elseif("${CMAKE_SYSTEM_NAME}" MATCHES "FreeBSD") add_subdirectory(linux-capture) add_subdirectory(linux-pulseaudio) @@ -50,6 +51,10 @@ elseif("${CMAKE_SYSTEM_NAME}" MATCHES "FreeBSD") add_subdirectory(linux-alsa) add_subdirectory(vlc-video) add_subdirectory(oss-audio) + add_subdirectory(sndio) +elseif("${CMAKE_SYSTEM_NAME}" MATCHES "OpenBSD") + add_subdirectory(linux-capture) + add_subdirectory(sndio) endif() option(BUILD_BROWSER "Build browser plugin" OFF) diff --git a/plugins/sndio/CMakeLists.txt b/plugins/sndio/CMakeLists.txt new file mode 100644 index 000000000..24f8d943b --- /dev/null +++ b/plugins/sndio/CMakeLists.txt @@ -0,0 +1,35 @@ +project(sndio) + +if(DISABLE_SNDIO) + message(STATUS "Sndio support disabled") + return() +endif() + +find_package(Sndio) +if(NOT Sndio_FOUND AND ENABLE_SNDIO) + message(FATAL_ERROR "Sndio not found but set as enabled") +elseif(NOT Sndio_FOUND) + message(STATUS "Sndio not found, disabling Sndio plugin") + return() +endif() + +include_directories( + SYSTEM "${CMAKE_SOURCE_DIR}/libobs" SYSTEM "${CMAKE_SOURCE_DIR}/../../libobs" + ${Sndio_INCLUDE_DIRS} +) + +set(sndio_SOURCES + sndio.c + sndio-input.c +) + +add_library(sndio MODULE + ${sndio_SOURCES} +) +target_link_libraries(sndio + libobs + ${Sndio_LIBRARIES} +) +set_target_properties(sndio PROPERTIES FOLDER "plugins") + +install_obs_plugin_with_data(sndio data) diff --git a/plugins/sndio/data/locale/en-US.ini b/plugins/sndio/data/locale/en-US.ini new file mode 100644 index 000000000..1d7c467d9 --- /dev/null +++ b/plugins/sndio/data/locale/en-US.ini @@ -0,0 +1,6 @@ +Channels="Number of channels" +Device="Device" +BitsPerSample="Bits per sample" +Rate="Rate" +SndioInput="Sndio input client" + diff --git a/plugins/sndio/sndio-input.c b/plugins/sndio/sndio-input.c new file mode 100644 index 000000000..e1e6bce03 --- /dev/null +++ b/plugins/sndio/sndio-input.c @@ -0,0 +1,418 @@ +/* +Copyright (C) 2020 by Vadim Zhukov + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "sndio-input.h" + +#define blog(level, msg, ...) \ + blog(level, "sndio-input: %s: " msg, __func__, ##__VA_ARGS__); + +#define berr(level, msg, ...) \ + do { \ + const char *errstr = strerror_l(errno, NULL); \ + blog(level, msg ": %s", ##__VA_ARGS__, errstr); \ + } while (0) + +#define NSEC_PER_SEC 1000000000LL + +/** + * Returns the name of the plugin + */ +static const char *sndio_input_getname(void *unused) +{ + UNUSED_PARAMETER(unused); + return obs_module_text("SndioInput"); +} + +static enum speaker_layout sndio_channels_to_obs_speakers(int channels) +{ + switch (channels) { + case 1: + return SPEAKERS_MONO; + case 2: + return SPEAKERS_STEREO; + case 3: + return SPEAKERS_2POINT1; + case 4: + return SPEAKERS_4POINT0; + case 5: + return SPEAKERS_4POINT1; + case 6: + return SPEAKERS_5POINT1; + case 8: + return SPEAKERS_7POINT1; + } + return SPEAKERS_UNKNOWN; +} + +static enum audio_format sndio_to_obs_audio_format(const struct sio_par *par) +{ + switch (par->bits) { + case 8: + return AUDIO_FORMAT_U8BIT; + case 16: + return AUDIO_FORMAT_16BIT; + case 32: + return AUDIO_FORMAT_32BIT; + } + return AUDIO_FORMAT_UNKNOWN; +} + +/** + * Runs sndio tasks on a pre-opened audio device. + * Whenever user chooses another device, this thread gets signalled to exit, + * and another one starts immediately, without waiting for the previous. + */ +static void *sndio_thread(void *attr) +{ + struct sndio_thr_data *thrdata = attr; + size_t msgread = 0; // msg bytes read from socket + size_t nsiofds = sio_nfds(thrdata->hdl); + struct pollfd pfd[1 + nsiofds]; + struct sio_par par; + uint64_t ts; + ssize_t nread; + int pollres; + uint8_t *buf; + size_t bufsz; + + ts = os_gettime_ns(); + + bufsz = thrdata->par.appbufsz * thrdata->par.bps * 2; + if ((buf = bmalloc(bufsz * 2)) == NULL) { + blog(LOG_ERROR, "could not allocate record buffer of %zu bytes", + bufsz); + goto finish; + } + + memset(pfd, 0, sizeof(pfd)); + pfd[0].fd = thrdata->sock; + + for (;;) { + for (size_t i = 0; i <= nsiofds; i++) + pfd[i].revents = 0; + pfd[0].events = POLLIN; + sio_pollfd(thrdata->hdl, pfd + 1, POLLIN); + + if (poll(pfd, 1 + nsiofds, /*INFTIM*/ -1) == -1) { + if (errno == EINTR) + continue; + berr(LOG_ERROR, "exiting due to poll error"); + goto finish; + } + + if ((pfd[0].revents & POLLHUP) == POLLHUP) { + blog(LOG_INFO, + "exiting upon receiving EOF at IPC socket"); + goto finish; + } + if ((pfd[0].revents & POLLIN) == POLLIN) { + nread = read(pfd[0].fd, ((uint8_t *)&par) + msgread, + sizeof(par) - msgread); + switch (nread) { + case -1: + if (errno == EAGAIN) + goto proceed_sio; + berr(LOG_ERROR, + "reading from IPC socket failed"); + goto finish; + + case 0: + blog(LOG_INFO, + "exiting upon receiving EOF at IPC socket"); + goto finish; + + default: + msgread += (size_t)nread; + if (msgread == sizeof(struct sio_par)) { + size_t tbufsz; + uint8_t *tbuf; + + msgread = 0; + sio_stop(thrdata->hdl); + if (!sio_setpar(thrdata->hdl, &par)) { + blog(LOG_WARNING, + "sio_setpar failed, keeping old params"); + } + blog(LOG_INFO, + "after sio_setpar(): appbufsz=%u bps=%u", + par.appbufsz, par.bps); + memcpy(&thrdata->par, &par, + sizeof(struct sio_par)); + + tbufsz = thrdata->par.appbufsz * + thrdata->par.bps * 2; + if ((tbuf = brealloc(buf, tbufsz)) == + NULL) { + blog(LOG_ERROR, + "could not reallocate record buffer of %zu bytes", + tbufsz); + goto finish; + } + buf = tbuf; + bufsz = tbufsz; + + if (!sio_start(thrdata->hdl)) { + blog(LOG_ERROR, + "sio_start failed, exiting"); + goto finish; + } + ts = os_gettime_ns(); + // Since we restarted recording, + // do not try to handle events we lost. + continue; + } + } + } + + proceed_sio: + pollres = sio_revents(thrdata->hdl, pfd + 1); + if ((pollres & POLLHUP) == POLLHUP) { + blog(LOG_ERROR, "sndio device error happened, exiting"); + goto finish; + } + if ((pollres & POLLIN) == POLLIN) { + struct obs_source_audio out; + unsigned int nframes; + + nread = (ssize_t)sio_read(thrdata->hdl, buf, bufsz); + if (nread == 0) { + if (sio_eof(thrdata->hdl)) { + blog(LOG_ERROR, + "sndio device EOF happened, exiting"); + goto finish; + } + continue; + } + nframes = (unsigned int)nread / thrdata->par.bps; + //blog(LOG_INFO, "sio_read returned %u, nframes = %u", nread, nframes); + + memset(&out, 0, sizeof(struct obs_source_audio)); + out.data[0] = buf; + out.frames = nframes; + out.format = sndio_to_obs_audio_format(&thrdata->par); + out.speakers = sndio_channels_to_obs_speakers( + thrdata->par.rchan); + out.samples_per_sec = thrdata->par.rate; + out.timestamp = ts; + + ts += util_mul_div64(nframes, NSEC_PER_SEC, + thrdata->par.rate); + + obs_source_output_audio(thrdata->source, &out); + } + } + +finish: + sio_close(thrdata->hdl); + close(thrdata->sock); + bfree(buf); + bfree(thrdata); + return NULL; +} + +/** + * Destroy the plugin object and free all memory + */ +static void sndio_destroy(void *vptr) +{ + struct sndio_data *data = vptr; + + if (!data) + return; + close(data->sock); + data->sock = -1; + pthread_attr_destroy(&data->attr); + bfree(data); +} + +/** + * Tries to apply the input settings. + * Must be called on stopped device. + */ +static void sndio_apply(struct sndio_data *data, obs_data_t *settings) +{ + struct sndio_thr_data *thrdata; + pthread_t thread; + int ec; + int socks[2] = {-1, -1}; + const char *devname = obs_data_get_string(settings, "device"); + + if ((thrdata = bzalloc(sizeof(struct sndio_thr_data))) == NULL) { + blog(LOG_ERROR, "malloc"); + return; + } + if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0, + socks) == -1) { + berr(LOG_ERROR, "socketpair"); + goto error; + } + + if (data->sock != -1) + close(data->sock); + data->sock = socks[0]; + thrdata->sock = socks[1]; + thrdata->source = data->source; + + thrdata->hdl = sio_open(devname, SIO_REC, 0); + if (!thrdata->hdl) { + berr(LOG_ERROR, "could not open %s sndio device", devname); + goto error; + } + + sio_initpar(&thrdata->par); + thrdata->par.bits = obs_data_get_int(settings, "bits"); + thrdata->par.bps = SIO_BPS(thrdata->par.bits); + thrdata->par.sig = thrdata->par.bits > 8; + thrdata->par.le = 1; + thrdata->par.rate = obs_data_get_int(settings, "rate"); + thrdata->par.rchan = obs_data_get_int(settings, "channels"); + thrdata->par.xrun = SIO_SYNC; // makes timestamping easy + if (!sio_setpar(thrdata->hdl, &thrdata->par)) { + berr(LOG_ERROR, "could not set parameters for %s sndio device", + devname); + goto error; + } + blog(LOG_INFO, "after initial sio_setpar(): appbufsz=%u bps=%u", + thrdata->par.appbufsz, thrdata->par.bps); + + if (!sio_start(thrdata->hdl)) { + berr(LOG_ERROR, "could not start recording on %s sndio device", + devname); + goto error; + } + + ec = pthread_create(&thread, &data->attr, sndio_thread, thrdata); + if (ec != 0) { + blog(LOG_ERROR, "could not start thread"); + goto error; + } + + return; + +error: + if (thrdata->hdl != NULL) + sio_close(thrdata->hdl); + close(socks[0]); + close(socks[1]); + bfree(thrdata); +} + +/** + * Update the input settings. + */ +static void sndio_update(void *vptr, obs_data_t *settings) +{ + struct sndio_data *data = vptr; + sndio_apply(data, settings); +} + +/** + * Create the plugin object + */ +static void *sndio_create(obs_data_t *settings, obs_source_t *source) +{ + struct sndio_data *data = bzalloc(sizeof(struct sndio_data)); + pthread_attr_init(&data->attr); + pthread_attr_setdetachstate(&data->attr, PTHREAD_CREATE_DETACHED); + data->source = source; + data->sock = -1; + sndio_apply(data, settings); + return data; +} + +/** + * Get plugin defaults + */ +static void sndio_input_defaults(obs_data_t *settings) +{ + obs_data_set_default_string(settings, "device", SIO_DEVANY); + obs_data_set_default_int(settings, "rate", 48000); + obs_data_set_default_int(settings, "bits", 16); + obs_data_set_default_int(settings, "channels", 2); +} + +/** + * Get plugin properties + */ +static obs_properties_t *sndio_input_properties(void *unused) +{ + obs_property_t *rate, *bits; + + UNUSED_PARAMETER(unused); + + obs_properties_t *props = obs_properties_create(); + + obs_properties_add_text(props, "device", obs_module_text("Device"), + OBS_TEXT_DEFAULT); + + rate = obs_properties_add_list(props, "rate", obs_module_text("Rate"), + OBS_COMBO_TYPE_LIST, + OBS_COMBO_FORMAT_INT); + obs_property_list_add_int(rate, "11025 Hz", 11025); + obs_property_list_add_int(rate, "22050 Hz", 22050); + obs_property_list_add_int(rate, "32000 Hz", 32000); + obs_property_list_add_int(rate, "44100 Hz", 44100); + obs_property_list_add_int(rate, "48000 Hz", 48000); + obs_property_list_add_int(rate, "96000 Hz", 96000); + obs_property_list_add_int(rate, "192000 Hz", 192000); + + bits = obs_properties_add_list(props, "bits", + obs_module_text("BitsPerSample"), + OBS_COMBO_TYPE_LIST, + OBS_COMBO_FORMAT_INT); + obs_property_list_add_int(bits, "8", 8); + obs_property_list_add_int(bits, "16", 16); + obs_property_list_add_int(bits, "32", 32); + + obs_properties_add_int(props, "channels", obs_module_text("Channels"), + 1, 8, 1); + + return props; +} + +struct obs_source_info sndio_output_capture = { + .id = "sndio_output_capture", + .type = OBS_SOURCE_TYPE_INPUT, + .output_flags = OBS_SOURCE_AUDIO, + .get_name = sndio_input_getname, + .create = sndio_create, + .destroy = sndio_destroy, +#if SHUTDOWN_ON_DEACTIVATE + .activate = sndio_activate, + .deactivate = sndio_deactivate, +#endif + .update = sndio_update, + .get_defaults = sndio_input_defaults, + .get_properties = sndio_input_properties, + .icon_type = OBS_ICON_TYPE_AUDIO_OUTPUT, +}; diff --git a/plugins/sndio/sndio-input.h b/plugins/sndio/sndio-input.h new file mode 100644 index 000000000..0fa286b95 --- /dev/null +++ b/plugins/sndio/sndio-input.h @@ -0,0 +1,18 @@ +#ifndef OBS_SNDIO_INPUT_H +#define OBS_SNDIO_INPUT_H + +struct sndio_thr_data { + obs_source_t *source; + struct obs_source_audio out; + struct sio_hdl *hdl; + struct sio_par par; + int sock; +}; + +struct sndio_data { + obs_source_t *source; + pthread_attr_t attr; + int sock; +}; + +#endif // OBS_SNDIO_INPUT_H diff --git a/plugins/sndio/sndio.c b/plugins/sndio/sndio.c new file mode 100644 index 000000000..5e1c0ba44 --- /dev/null +++ b/plugins/sndio/sndio.c @@ -0,0 +1,32 @@ +/* +Copyright (C) 2020 by Vadim Zhukov + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ +#include + +OBS_DECLARE_MODULE() +OBS_MODULE_USE_DEFAULT_LOCALE("sndio", "en-US") +MODULE_EXPORT const char *obs_module_description(void) +{ + return "sndio output capture"; +} + +extern struct obs_source_info sndio_output_capture; + +bool obs_module_load(void) +{ + obs_register_source(&sndio_output_capture); + return true; +}