/* 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 bool alsa_devices_changed(obs_properties_t *props, obs_property_t *p, obs_data_t *settings); 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, .icon_type = OBS_ICON_TYPE_AUDIO_INPUT, }; 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; const char *device = obs_data_get_string(settings, "device_id"); if (strcmp(device, "__custom__") == 0) device = obs_data_get_string(settings, "custom_pcm"); data->device = bstrdup(device); 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(device, "__custom__") == 0) device = obs_data_get_string(settings, "custom_pcm"); 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_string(settings, "custom_pcm", "default"); obs_data_set_default_int(settings, "rate", 44100); } static bool alsa_devices_changed(obs_properties_t *props, obs_property_t *p, obs_data_t *settings) { UNUSED_PARAMETER(p); bool visible = false; const char *device_id = obs_data_get_string(settings, "device_id"); if (strcmp(device_id, "__custom__") == 0) visible = true; obs_property_t *custom_pcm = obs_properties_get(props, "custom_pcm"); obs_property_set_visible(custom_pcm, visible); obs_property_modified(custom_pcm, settings); return true; } 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"); obs_properties_add_text(props, "custom_pcm", obs_module_text("PCM"), OBS_TEXT_DEFAULT); rate = obs_properties_add_list(props, "rate", obs_module_text("Rate"), OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_INT); obs_property_set_modified_callback(devices, alsa_devices_changed); 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; } obs_property_list_add_string(devices, "Custom", "__custom__"); 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_4POINT0; case 5: return SPEAKERS_4POINT1; case 6: return SPEAKERS_5POINT1; case 8: return SPEAKERS_7POINT1; } return SPEAKERS_UNKNOWN; }