diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 4e80a6346a1a1593d9298535a00ae537d7c78121..d73838e79dfc80af1df784c493ad2babcdd97e83 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -21,6 +21,7 @@ elseif("${CMAKE_SYSTEM_NAME}" MATCHES "Linux") add_subdirectory(linux-pulseaudio) add_subdirectory(linux-v4l2) add_subdirectory(linux-jack) + add_subdirectory(linux-alsa) add_subdirectory(decklink/linux) elseif("${CMAKE_SYSTEM_NAME}" MATCHES "FreeBSD") add_subdirectory(linux-capture) diff --git a/plugins/linux-alsa/CMakeLists.txt b/plugins/linux-alsa/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..226a4e78b0ee7d25aabeff4da0320a3ec146e116 --- /dev/null +++ b/plugins/linux-alsa/CMakeLists.txt @@ -0,0 +1,34 @@ +project(linux-alsa) + +if(DISABLE_ALSA) + message(STATUS "ALSA support disabled") + return() +endif() + +find_package(ALSA) +if(NOT ALSA_FOUND AND ENABLE_ALSA) + message(FATAL_ERROR "ALSA not found but set as enabled") +elseif(NOT ALSA_FOUND) + message(STATUS "ALSA not found, disabling ALSA plugin") + return() +endif() + +include_directories( + SYSTEM "${CMAKE_SOURCE_DIR}/libobs" + ${ALSA_INCLUDE_DIR} +) + +set(linux-alsa_SOURCES + linux-alsa.c + alsa-input.c +) + +add_library(linux-alsa MODULE + ${linux-alsa_SOURCES} +) +target_link_libraries(linux-alsa + libobs + ${ALSA_LIBRARY} +) + +install_obs_plugin_with_data(linux-alsa data) diff --git a/plugins/linux-alsa/alsa-input.c b/plugins/linux-alsa/alsa-input.c new file mode 100644 index 0000000000000000000000000000000000000000..94ea5efebfbdc9bf1dbccdd2c147b0eb60f5f5bc --- /dev/null +++ b/plugins/linux-alsa/alsa-input.c @@ -0,0 +1,599 @@ +/* +Copyright (C) 2015. Guillermo A. Amaral B. + +Based on Pulse Input plugin by Leonhard Oelke. + +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 + +#define blog(level, msg, ...) blog(level, "alsa-input: " msg, ##__VA_ARGS__) + +#define NSEC_PER_SEC 1000000000LL +#define NSEC_PER_MSEC 1000000L +#define STARTUP_TIMEOUT_NS (500 * NSEC_PER_MSEC) +#define REOPEN_TIMEOUT 1000UL +#define SHUTDOWN_ON_DEACTIVATE false + +struct alsa_data { + obs_source_t *source; +#if SHUTDOWN_ON_DEACTIVATE + bool active; +#endif + + /* user settings */ + char *device; + + /* pthread */ + pthread_t listen_thread; + pthread_t reopen_thread; + os_event_t *abort_event; + volatile bool listen; + volatile bool reopen; + + /* alsa */ + snd_pcm_t *handle; + snd_pcm_format_t format; + snd_pcm_uframes_t period_size; + + unsigned int channels; + unsigned int rate; + unsigned int sample_size; + uint8_t *buffer; + uint64_t first_ts; +}; + +static const char * alsa_get_name(void *); +static obs_properties_t * alsa_get_properties(void *); +static void * alsa_create(obs_data_t *, obs_source_t *); +static void alsa_destroy(void *); +static void alsa_activate(void *); +static void alsa_deactivate(void *); +static void alsa_get_defaults(obs_data_t *); +static void alsa_update(void *, obs_data_t *); + +struct obs_source_info alsa_input_capture = { + .id = "alsa_input_capture", + .type = OBS_SOURCE_TYPE_INPUT, + .output_flags = OBS_SOURCE_AUDIO, + .create = alsa_create, + .destroy = alsa_destroy, +#if SHUTDOWN_ON_DEACTIVATE + .activate = alsa_activate, + .deactivate = alsa_deactivate, +#endif + .update = alsa_update, + .get_defaults = alsa_get_defaults, + .get_name = alsa_get_name, + .get_properties = alsa_get_properties +}; + +static bool _alsa_try_open(struct alsa_data *); +static bool _alsa_open(struct alsa_data *); +static void _alsa_close(struct alsa_data *); +static bool _alsa_configure(struct alsa_data *); +static void _alsa_start_reopen(struct alsa_data *); +static void _alsa_stop_reopen(struct alsa_data *); +static void * _alsa_listen(void *); +static void * _alsa_reopen(void *); + +static enum audio_format _alsa_to_obs_audio_format(snd_pcm_format_t); +static enum speaker_layout _alsa_channels_to_obs_speakers(unsigned int); + +/*****************************************************************************/ + +void * alsa_create(obs_data_t *settings, obs_source_t *source) +{ + struct alsa_data *data = bzalloc(sizeof(struct alsa_data)); + + data->source = source; +#if SHUTDOWN_ON_DEACTIVATE + data->active = false; +#endif + data->buffer = NULL; + data->device = NULL; + data->first_ts = 0; + data->handle = NULL; + data->listen = false; + data->reopen = false; + data->listen_thread = 0; + data->reopen_thread = 0; + + data->device = bstrdup(obs_data_get_string(settings, "device_id")); + data->rate = obs_data_get_int(settings, "rate"); + + if (os_event_init(&data->abort_event, OS_EVENT_TYPE_MANUAL) != 0) { + blog(LOG_ERROR, "Abort event creation failed!"); + goto cleanup; + } + +#if !SHUTDOWN_ON_DEACTIVATE + _alsa_try_open(data); +#endif + return data; + +cleanup: + if (data->device) + bfree(data->device); + + bfree(data); + return NULL; +} + +void alsa_destroy(void *vptr) +{ + struct alsa_data *data = vptr; + + if (data->handle) + _alsa_close(data); + + os_event_destroy(data->abort_event); + bfree(data->device); + bfree(data); +} + +#if SHUTDOWN_ON_DEACTIVATE +void alsa_activate(void *vptr) +{ + struct alsa_data *data = vptr; + + data->active = true; + _alsa_try_open(data); +} + +void alsa_deactivate(void *vptr) +{ + struct alsa_data *data = vptr; + + _alsa_stop_reopen(data); + _alsa_close(data); + data->active = false; +} +#endif + +void alsa_update(void *vptr, obs_data_t *settings) +{ + struct alsa_data *data = vptr; + const char *device; + unsigned int rate; + bool reset = false; + + device = obs_data_get_string(settings, "device_id"); + if (strcmp(data->device, device) != 0) { + bfree(data->device); + data->device = bstrdup(device); + reset = true; + } + + rate = obs_data_get_int(settings, "rate"); + if (data->rate != rate) { + data->rate = rate; + reset = true; + } + +#if SHUTDOWN_ON_DEACTIVATE + if (reset && data->handle) + _alsa_close(data); + + if (data->active && !data->handle) + _alsa_try_open(data); +#else + if (reset) { + if (data->handle) + _alsa_close(data); + _alsa_try_open(data); + } +#endif +} + +const char * alsa_get_name(void *unused) +{ + UNUSED_PARAMETER(unused); + return obs_module_text("AlsaInput"); +} + +void alsa_get_defaults(obs_data_t *settings) +{ + obs_data_set_default_string(settings, "device_id", "default"); + obs_data_set_default_int(settings, "rate", 44100); +} + +obs_properties_t * alsa_get_properties(void *unused) +{ + void **hints; + void **hint; + char *name = NULL; + char *descr = NULL; + char *io = NULL; + char *descr_i; + obs_properties_t *props; + obs_property_t *devices; + obs_property_t *rate; + + UNUSED_PARAMETER(unused); + + props = obs_properties_create(); + + devices = obs_properties_add_list(props, "device_id", + obs_module_text("Device"), OBS_COMBO_TYPE_LIST, + OBS_COMBO_FORMAT_STRING); + + obs_property_list_add_string(devices, "Default", "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, "32000 Hz", 32000); + obs_property_list_add_int(rate, "44100 Hz", 44100); + obs_property_list_add_int(rate, "48000 Hz", 48000); + + if (snd_device_name_hint(-1, "pcm", &hints) < 0) + return props; + + hint = hints; + while (*hint != NULL) { + /* check if we're dealing with an Input */ + io = snd_device_name_get_hint(*hint, "IOID"); + if (io != NULL && strcmp(io, "Input") != 0) + goto next; + + name = snd_device_name_get_hint(*hint, "NAME"); + if (name == NULL || strstr(name, "front:") == NULL) + goto next; + + descr = snd_device_name_get_hint(*hint, "DESC"); + if (!descr) + goto next; + + descr_i = descr; + while (*descr_i) { + if (*descr_i == '\n') { + *descr_i = '\0'; + break; + } + else ++descr_i; + } + + obs_property_list_add_string(devices, descr, name); + + next: + if (name != NULL) + free(name), name = NULL; + + if (descr != NULL) + free(descr), descr = NULL; + + if (io != NULL) + free(io), io = NULL; + + ++hint; + } + snd_device_name_free_hint(hints); + + return props; +} + +/*****************************************************************************/ + +bool _alsa_try_open(struct alsa_data *data) +{ + _alsa_stop_reopen(data); + + if (_alsa_open(data)) + return true; + + _alsa_start_reopen(data); + + return false; +} + +bool _alsa_open(struct alsa_data *data) +{ + pthread_attr_t attr; + int err; + + err = snd_pcm_open(&data->handle, data->device, + SND_PCM_STREAM_CAPTURE, 0); + if (err < 0) { + blog(LOG_ERROR, "Failed to open '%s': %s", + data->device, snd_strerror(err)); + return false; + } + + if (!_alsa_configure(data)) + goto cleanup; + + if (snd_pcm_state(data->handle) != SND_PCM_STATE_PREPARED) { + blog(LOG_ERROR, "Device not prepared: '%s'", + data->device); + goto cleanup; + } + + /* start listening */ + + err = snd_pcm_start(data->handle); + if (err < 0) { + blog(LOG_ERROR, "Failed to start '%s': %s", + data->device, snd_strerror(err)); + goto cleanup; + } + + /* create capture thread */ + + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE); + + err = pthread_create(&data->listen_thread, &attr, _alsa_listen, data); + if (err) { + pthread_attr_destroy(&attr); + blog(LOG_ERROR, + "Failed to create capture thread for device '%s'.", + data->device); + goto cleanup; + } + + pthread_attr_destroy(&attr); + return true; + +cleanup: + _alsa_close(data); + return false; +} + +void _alsa_close(struct alsa_data *data) +{ + if (data->listen_thread) { + os_atomic_set_bool(&data->listen, false); + pthread_join(data->listen_thread, NULL); + data->listen_thread = 0; + } + + if (data->handle) { + snd_pcm_drop(data->handle); + snd_pcm_close(data->handle), data->handle = NULL; + } + + if (data->buffer) + bfree(data->buffer), data->buffer = NULL; +} + +bool _alsa_configure(struct alsa_data *data) +{ + snd_pcm_hw_params_t *hwparams; + int err; + int dir; + + snd_pcm_hw_params_alloca(&hwparams); + + err = snd_pcm_hw_params_any(data->handle, hwparams); + if (err < 0) { + blog(LOG_ERROR, + "snd_pcm_hw_params_any failed: %s", + snd_strerror(err)); + return false; + } + + err = snd_pcm_hw_params_set_access(data->handle, hwparams, + SND_PCM_ACCESS_RW_INTERLEAVED); + if (err < 0) { + blog(LOG_ERROR, + "snd_pcm_hw_params_set_access failed: %s", + snd_strerror(err)); + return false; + } + + data->format = SND_PCM_FORMAT_S16; + err = snd_pcm_hw_params_set_format(data->handle, hwparams, + data->format); + if (err < 0) { + blog(LOG_ERROR, + "snd_pcm_hw_params_set_format failed: %s", + snd_strerror(err)); + return false; + } + + err = snd_pcm_hw_params_set_rate_near(data->handle, hwparams, + &data->rate, 0); + if (err < 0) { + blog(LOG_ERROR, + "snd_pcm_hw_params_set_rate_near failed: %s", + snd_strerror(err)); + return false; + } + blog(LOG_INFO, "PCM '%s' rate set to %d", data->device, data->rate); + + err = snd_pcm_hw_params_get_channels(hwparams, &data->channels); + if (err < 0) + data->channels = 2; + + err = snd_pcm_hw_params_set_channels_near(data->handle, hwparams, + &data->channels); + if (err < 0) { + blog(LOG_ERROR, + "snd_pcm_hw_params_set_channels_near failed: %s", + snd_strerror(err)); + return false; + } + blog(LOG_INFO, "PCM '%s' channels set to %d", + data->device, data->channels); + + err = snd_pcm_hw_params(data->handle, hwparams); + if (err < 0) { + blog(LOG_ERROR, "snd_pcm_hw_params failed: %s", + snd_strerror(err)); + return false; + } + + err = snd_pcm_hw_params_get_period_size(hwparams, &data->period_size, + &dir); + if (err < 0) { + blog(LOG_ERROR, + "snd_pcm_hw_params_get_period_size failed: %s", + snd_strerror(err)); + return false; + } + + data->sample_size = (data->channels + * snd_pcm_format_physical_width(data->format)) / 8; + + if (data->buffer) + bfree(data->buffer); + data->buffer = bzalloc(data->period_size * data->sample_size); + + return true; +} + +void _alsa_start_reopen(struct alsa_data *data) +{ + pthread_attr_t attr; + int err; + + if (os_atomic_load_bool(&data->reopen)) + return; + + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE); + + err = pthread_create(&data->reopen_thread, &attr, _alsa_reopen, data); + if (err) { + blog(LOG_ERROR, + "Failed to create reopen thread for device '%s'.", + data->device); + } + + pthread_attr_destroy(&attr); +} + +void _alsa_stop_reopen(struct alsa_data *data) +{ + if (os_atomic_load_bool(&data->reopen)) + os_event_signal(data->abort_event); + + if (data->reopen_thread) { + pthread_join(data->reopen_thread, NULL); + data->reopen_thread = 0; + } + + os_event_reset(data->abort_event); +} + +void * _alsa_listen(void *attr) +{ + struct alsa_data *data = attr; + struct obs_source_audio out; + + blog(LOG_DEBUG, "Capture thread started."); + + out.data[0] = data->buffer; + out.format = _alsa_to_obs_audio_format(data->format); + out.speakers = _alsa_channels_to_obs_speakers(data->channels); + out.samples_per_sec = data->rate; + + os_atomic_set_bool(&data->listen, true); + + do { + snd_pcm_sframes_t frames = snd_pcm_readi(data->handle, + data->buffer, data->period_size); + + if (!os_atomic_load_bool(&data->listen)) + break; + + if (frames <= 0) { + frames = snd_pcm_recover(data->handle, frames, 0); + if (frames <= 0) { + snd_pcm_wait(data->handle, 100); + continue; + } + } + + out.frames = frames; + out.timestamp = os_gettime_ns() + - ((frames * NSEC_PER_SEC) / data->rate); + + if (!data->first_ts) + data->first_ts = out.timestamp + STARTUP_TIMEOUT_NS; + + if (out.timestamp > data->first_ts) + obs_source_output_audio(data->source, &out); + } while (os_atomic_load_bool(&data->listen)); + + blog(LOG_DEBUG, "Capture thread is about to exit."); + + pthread_exit(NULL); + return NULL; +} + +void * _alsa_reopen(void *attr) +{ + struct alsa_data *data = attr; + unsigned long timeout = REOPEN_TIMEOUT; + + blog(LOG_DEBUG, "Reopen thread started."); + + os_atomic_set_bool(&data->reopen, true); + + while (os_event_timedwait(data->abort_event, timeout) == ETIMEDOUT) { + if (_alsa_open(data)) + break; + + if (timeout < (REOPEN_TIMEOUT * 5)) + timeout += REOPEN_TIMEOUT; + } + + os_atomic_set_bool(&data->reopen, false); + + blog(LOG_DEBUG, "Reopen thread is about to exit."); + + pthread_exit(NULL); + return NULL; +} + +enum audio_format _alsa_to_obs_audio_format(snd_pcm_format_t format) +{ + switch (format) { + case SND_PCM_FORMAT_U8: return AUDIO_FORMAT_U8BIT; + case SND_PCM_FORMAT_S16_LE: return AUDIO_FORMAT_16BIT; + case SND_PCM_FORMAT_S32_LE: return AUDIO_FORMAT_32BIT; + case SND_PCM_FORMAT_FLOAT_LE: return AUDIO_FORMAT_FLOAT; + default: break; + } + + return AUDIO_FORMAT_UNKNOWN; +} + +enum speaker_layout _alsa_channels_to_obs_speakers(unsigned int channels) +{ + switch(channels) { + case 1: return SPEAKERS_MONO; + case 2: return SPEAKERS_STEREO; + case 3: return SPEAKERS_2POINT1; + case 4: return SPEAKERS_SURROUND; + case 5: return SPEAKERS_4POINT1; + case 6: return SPEAKERS_5POINT1; + case 8: return SPEAKERS_7POINT1; + } + + return SPEAKERS_UNKNOWN; +} + diff --git a/plugins/linux-alsa/data/locale/en-US.ini b/plugins/linux-alsa/data/locale/en-US.ini new file mode 100644 index 0000000000000000000000000000000000000000..45d864717f706d1f3cbab389b7d2eb4903154267 --- /dev/null +++ b/plugins/linux-alsa/data/locale/en-US.ini @@ -0,0 +1,2 @@ +AlsaInput="Audio Capture Device (ALSA)" +Device="Device" diff --git a/plugins/linux-alsa/linux-alsa.c b/plugins/linux-alsa/linux-alsa.c new file mode 100644 index 0000000000000000000000000000000000000000..b0cad0cf1574fd0744c116d3ba7c92cec54d9eaa --- /dev/null +++ b/plugins/linux-alsa/linux-alsa.c @@ -0,0 +1,29 @@ +/* +Copyright (C) 2015. Guillermo A. Amaral B. + +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("linux-alsa", "en-US") + +extern struct obs_source_info alsa_input_capture; + +bool obs_module_load(void) +{ + obs_register_source(&alsa_input_capture); + return true; +} +