/* Copyright (C) 2014 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 "pulse-wrapper.h" #define PULSE_DATA(voidptr) struct pulse_data *data = voidptr; struct pulse_data { obs_source_t source; char *device; enum speaker_layout speakers; pa_sample_format_t format; uint_fast32_t samples_per_sec; uint_fast8_t channels; uint_fast32_t bytes_per_frame; pa_stream *stream; }; /* * get obs from pulse audio format */ static enum audio_format pulse_to_obs_audio_format( pa_sample_format_t format) { switch (format) { case PA_SAMPLE_U8: return AUDIO_FORMAT_U8BIT; case PA_SAMPLE_S16LE: return AUDIO_FORMAT_16BIT; case PA_SAMPLE_S24_32LE: return AUDIO_FORMAT_32BIT; case PA_SAMPLE_FLOAT32LE: return AUDIO_FORMAT_FLOAT; default: return AUDIO_FORMAT_UNKNOWN; } return AUDIO_FORMAT_UNKNOWN; } /* * get the buffer size needed for length msec with current settings */ static uint_fast32_t get_buffer_size(struct pulse_data *data, uint_fast32_t length) { return (length * data->samples_per_sec * data->bytes_per_frame) / 1000; } /* * Get latency for a pulse audio stream */ static int pulse_get_stream_latency(pa_stream *stream, int64_t *latency) { int ret; int sign; pa_usec_t abs; ret = pa_stream_get_latency(stream, &abs, &sign); *latency = (sign) ? -(int64_t) abs : (int64_t) abs; return ret; } /* * Callback for pulse which gets executed when new audio data is available */ static void pulse_stream_read(pa_stream *p, size_t nbytes, void *userdata) { UNUSED_PARAMETER(p); UNUSED_PARAMETER(nbytes); PULSE_DATA(userdata); const void *frames; size_t bytes; uint64_t pa_time; int64_t pa_latency; pa_stream_peek(data->stream, &frames, &bytes); // check if we got data if (!bytes) goto exit; if (!frames) { blog(LOG_DEBUG, "pulse-input: Got audio hole of %u bytes", (unsigned int) bytes); pa_stream_drop(data->stream); goto exit; } if (pa_stream_get_time(data->stream, &pa_time) < 0) { blog(LOG_ERROR, "pulse-input: Failed to get timing info !"); pa_stream_drop(data->stream); goto exit; } pulse_get_stream_latency(data->stream, &pa_latency); struct source_audio out; out.speakers = data->speakers; out.samples_per_sec = data->samples_per_sec; out.format = pulse_to_obs_audio_format(data->format); out.data[0] = (uint8_t *) frames; out.frames = bytes / data->bytes_per_frame; out.timestamp = (pa_time - pa_latency) * 1000; obs_source_output_audio(data->source, &out); pa_stream_drop(data->stream); exit: pulse_signal(0); } /* * Server info callback */ static void pulse_server_info(pa_context *c, const pa_server_info *i, void *userdata) { UNUSED_PARAMETER(c); PULSE_DATA(userdata); data->format = i->sample_spec.format; data->samples_per_sec = i->sample_spec.rate; data->channels = i->sample_spec.channels; blog(LOG_DEBUG, "pulse-input: Default format: %s, %u Hz, %u channels", pa_sample_format_to_string(i->sample_spec.format), i->sample_spec.rate, i->sample_spec.channels); pulse_signal(0); } /* * start recording */ static int_fast32_t pulse_start_recording(struct pulse_data *data) { if (pulse_get_server_info(pulse_server_info, (void *) data) < 0) { blog(LOG_ERROR, "pulse-input: Unable to get server info !"); return -1; } pa_sample_spec spec; spec.format = data->format; spec.rate = data->samples_per_sec; spec.channels = data->channels; if (!pa_sample_spec_valid(&spec)) { blog(LOG_ERROR, "pulse-input: Sample spec is not valid"); return -1; } data->bytes_per_frame = pa_frame_size(&spec); blog(LOG_DEBUG, "pulse-input: %u bytes per frame", (unsigned int) data->bytes_per_frame); data->stream = pulse_stream_new(obs_source_getname(data->source), &spec, NULL); if (!data->stream) { blog(LOG_ERROR, "pulse-input: Unable to create stream"); return -1; } pulse_lock(); pa_stream_set_read_callback(data->stream, pulse_stream_read, (void *) data); pulse_unlock(); pa_buffer_attr attr; attr.fragsize = get_buffer_size(data, 250); attr.maxlength = (uint32_t) -1; attr.minreq = (uint32_t) -1; attr.prebuf = (uint32_t) -1; attr.tlength = (uint32_t) -1; pa_stream_flags_t flags = PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_AUTO_TIMING_UPDATE | PA_STREAM_ADJUST_LATENCY; pulse_lock(); int_fast32_t ret = pa_stream_connect_record(data->stream, data->device, &attr, flags); pulse_unlock(); if (ret < 0) { blog(LOG_ERROR, "pulse-input: Unable to connect to stream"); return -1; } blog(LOG_DEBUG, "pulse-input: Recording started"); return 0; } /* * stop recording */ static void pulse_stop_recording(struct pulse_data *data) { if (data->stream) { pulse_lock(); pa_stream_disconnect(data->stream); pa_stream_unref(data->stream); pulse_unlock(); } } /* * input info callback */ static void pulse_input_info(pa_context *c, const pa_source_info *i, int eol, void *userdata) { UNUSED_PARAMETER(c); if (eol != 0 || i->monitor_of_sink != PA_INVALID_INDEX) goto skip; obs_property_list_add_string((obs_property_t) userdata, i->description, i->name); skip: pulse_signal(0); } /** * output info callback */ static void pulse_output_info(pa_context *c, const pa_source_info *i, int eol, void *userdata) { UNUSED_PARAMETER(c); if (eol != 0 || i->monitor_of_sink == PA_INVALID_INDEX) goto skip; obs_property_list_add_string((obs_property_t) userdata, i->description, i->name); skip: pulse_signal(0); } /* * get plugin properties */ static obs_properties_t pulse_properties(const char *locale, bool input) { obs_properties_t props = obs_properties_create(locale); obs_property_t devices = obs_properties_add_list(props, "device_id", "Device", OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING); pulse_init(); pa_source_info_cb_t cb = (input) ? pulse_input_info : pulse_output_info; pulse_get_source_info_list(cb, (void *) devices); pulse_unref(); return props; } static obs_properties_t pulse_input_properties(const char *locale) { return pulse_properties(locale, true); } static obs_properties_t pulse_output_properties(const char *locale) { return pulse_properties(locale, false); } /* * get plugin defaults */ static void pulse_defaults(obs_data_t settings) { obs_data_set_default_string(settings, "device_id", "default"); } /* * Returns the name of the plugin */ static const char *pulse_input_getname(const char *locale) { UNUSED_PARAMETER(locale); return "Pulse Audio Input Capture"; } static const char *pulse_output_getname(const char *locale) { UNUSED_PARAMETER(locale); return "Pulse Audio Output Capture"; } /* * Destroy the plugin object and free all memory */ static void pulse_destroy(void *vptr) { PULSE_DATA(vptr); if (!data) return; pulse_stop_recording(data); pulse_unref(); if (data->device) bfree(data->device); bfree(data); blog(LOG_DEBUG, "pulse-input: Input destroyed"); } /* * Create the plugin object */ static void *pulse_create(obs_data_t settings, obs_source_t source) { struct pulse_data *data = bzalloc(sizeof(struct pulse_data)); data->source = source; data->speakers = SPEAKERS_STEREO; data->device = bstrdup(obs_data_getstring(settings, "device_id")); pulse_init(); if (pulse_start_recording(data) < 0) goto fail; return data; fail: pulse_destroy(data); return NULL; } static void pulse_update(void *vptr, obs_data_t settings) { UNUSED_PARAMETER(vptr); UNUSED_PARAMETER(settings); } struct obs_source_info pulse_input_capture = { .id = "pulse_input_capture", .type = OBS_SOURCE_TYPE_INPUT, .output_flags = OBS_SOURCE_AUDIO, .getname = pulse_input_getname, .create = pulse_create, .destroy = pulse_destroy, .update = pulse_update, .defaults = pulse_defaults, .properties = pulse_input_properties }; struct obs_source_info pulse_output_capture = { .id = "pulse_output_capture", .type = OBS_SOURCE_TYPE_INPUT, .output_flags = OBS_SOURCE_AUDIO, .getname = pulse_output_getname, .create = pulse_create, .destroy = pulse_destroy, .update = pulse_update, .defaults = pulse_defaults, .properties = pulse_output_properties };