diff --git a/UI/adv-audio-control.cpp b/UI/adv-audio-control.cppindex b7bfd4d08..531b00173 100644--- a/UI/adv-audio-control.cpp+++ b/UI/adv-audio-control.cpp@@ -32,7 +32,7 @@ OBSAdvAudioCtrl::OBSAdvAudioCtrl(QGridLayout *layout, obs_source_t *source_) volume = new QSpinBox(); forceMono = new QCheckBox(); panning = new QSlider(Qt::Horizontal);-#if defined(_WIN32) || defined(__APPLE__)+#if defined(_WIN32) || defined(__APPLE__) || HAVE_PULSEAUDIO monitoringType = new QComboBox(); #endif syncOffset = new QSpinBox();@@ -93,7 +93,7 @@ OBSAdvAudioCtrl::OBSAdvAudioCtrl(QGridLayout *layout, obs_source_t *source_) syncOffset->setValue(int(cur_sync / NSEC_PER_MSEC)); int idx;-#if defined(_WIN32) || defined(__APPLE__)+#if defined(_WIN32) || defined(__APPLE__) || HAVE_PULSEAUDIO monitoringType->addItem(QTStr("Basic.AdvAudio.Monitoring.None"), (int)OBS_MONITORING_TYPE_NONE); monitoringType->addItem(QTStr("Basic.AdvAudio.Monitoring.MonitorOnly"),@@ -138,7 +138,7 @@ OBSAdvAudioCtrl::OBSAdvAudioCtrl(QGridLayout *layout, obs_source_t *source_) this, SLOT(panningChanged(int))); QWidget::connect(syncOffset, SIGNAL(valueChanged(int)), this, SLOT(syncOffsetChanged(int)));-#if defined(_WIN32) || defined(__APPLE__)+#if defined(_WIN32) || defined(__APPLE__) || HAVE_PULSEAUDIO QWidget::connect(monitoringType, SIGNAL(currentIndexChanged(int)), this, SLOT(monitoringTypeChanged(int))); #endif@@ -163,7 +163,7 @@ OBSAdvAudioCtrl::OBSAdvAudioCtrl(QGridLayout *layout, obs_source_t *source_) layout->addWidget(forceMonoContainer, lastRow, idx++); layout->addWidget(panningContainer, lastRow, idx++); layout->addWidget(syncOffset, lastRow, idx++);-#if defined(_WIN32) || defined(__APPLE__)+#if defined(_WIN32) || defined(__APPLE__) || HAVE_PULSEAUDIO layout->addWidget(monitoringType, lastRow, idx++); #endif layout->addWidget(mixerContainer, lastRow, idx++);@@ -178,7 +178,7 @@ OBSAdvAudioCtrl::~OBSAdvAudioCtrl() forceMonoContainer->deleteLater(); panningContainer->deleteLater(); syncOffset->deleteLater();-#if defined(_WIN32) || defined(__APPLE__)+#if defined(_WIN32) || defined(__APPLE__) || HAVE_PULSEAUDIO monitoringType->deleteLater(); #endif mixerContainer->deleteLater();diff --git a/UI/window-basic-adv-audio.cpp b/UI/window-basic-adv-audio.cppindex 33fe0cb5a..2894f6bf2 100644--- a/UI/window-basic-adv-audio.cpp+++ b/UI/window-basic-adv-audio.cpp@@ -42,7 +42,7 @@ OBSBasicAdvAudio::OBSBasicAdvAudio(QWidget *parent) label = new QLabel(QTStr("Basic.AdvAudio.SyncOffset")); label->setAlignment(Qt::AlignHCenter); mainLayout->addWidget(label, 0, idx++);-#if defined(_WIN32) || defined(__APPLE__)+#if defined(_WIN32) || defined(__APPLE__) || HAVE_PULSEAUDIO label = new QLabel(QTStr("Basic.AdvAudio.Monitoring")); label->setAlignment(Qt::AlignHCenter); mainLayout->addWidget(label, 0, idx++);diff --git a/UI/window-basic-settings.cpp b/UI/window-basic-settings.cppindex 52f10b9b8..5785b1543 100644--- a/UI/window-basic-settings.cpp+++ b/UI/window-basic-settings.cpp@@ -410,7 +410,7 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) HookWidget(ui->colorRange, COMBO_CHANGED, ADV_CHANGED); HookWidget(ui->disableOSXVSync, CHECK_CHANGED, ADV_CHANGED); HookWidget(ui->resetOSXVSync, CHECK_CHANGED, ADV_CHANGED);-#if defined(_WIN32) || defined(__APPLE__)+#if defined(_WIN32) || defined(__APPLE__) || HAVE_PULSEAUDIO HookWidget(ui->monitoringDevice, COMBO_CHANGED, ADV_CHANGED); #endif #ifdef _WIN32@@ -431,15 +431,11 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) HookWidget(ui->enableNewSocketLoop, CHECK_CHANGED, ADV_CHANGED); HookWidget(ui->enableLowLatencyMode, CHECK_CHANGED, ADV_CHANGED); -#if !defined(_WIN32) && !defined(__APPLE__)- delete ui->monitoringDevice;- delete ui->monitoringDeviceLabel;- delete ui->advAudioGroupBox;+#if !defined(_WIN32) && !defined(__APPLE__) && !HAVE_PULSEAUDIO delete ui->enableAutoUpdates;- ui->monitoringDevice = nullptr;- ui->monitoringDeviceLabel = nullptr;- ui->advAudioGroupBox = nullptr;+ delete ui->advAudioGroupBox; ui->enableAutoUpdates = nullptr;+ ui->advAudioGroupBox = nullptr; #endif #ifdef _WIN32@@ -484,7 +480,7 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) delete ui->advancedGeneralGroupBox; delete ui->enableNewSocketLoop; delete ui->enableLowLatencyMode;-#ifdef __APPLE__+#if defined(__APPLE__) || HAVE_PULSEAUDIO delete ui->disableAudioDucking; #endif ui->rendererLabel = nullptr;@@ -496,7 +492,7 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) ui->advancedGeneralGroupBox = nullptr; ui->enableNewSocketLoop = nullptr; ui->enableLowLatencyMode = nullptr;-#ifdef __APPLE__+#if defined(__APPLE__) || HAVE_PULSEAUDIO ui->disableAudioDucking = nullptr; #endif #endif@@ -583,7 +579,7 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) FillSimpleRecordingValues(); FillSimpleStreamingValues();-#if defined(_WIN32) || defined(__APPLE__)+#if defined(_WIN32) || defined(__APPLE__) || HAVE_PULSEAUDIO FillAudioMonitoringDevices(); #endif @@ -2025,7 +2021,7 @@ void OBSBasicSettings::LoadAdvancedSettings() "Video", "ColorSpace"); const char *videoColorRange = config_get_string(main->Config(), "Video", "ColorRange");-#if defined(_WIN32) || defined(__APPLE__)+#if defined(_WIN32) || defined(__APPLE__) || HAVE_PULSEAUDIO const char *monDevName = config_get_string(main->Config(), "Audio", "MonitoringDeviceName"); const char *monDevId = config_get_string(main->Config(), "Audio",@@ -2059,7 +2055,7 @@ void OBSBasicSettings::LoadAdvancedSettings() LoadRendererList(); -#if defined(_WIN32) || defined(__APPLE__)+#if defined(_WIN32) || defined(__APPLE__) || HAVE_PULSEAUDIO if (!SetComboByValue(ui->monitoringDevice, monDevId)) SetInvalidValue(ui->monitoringDevice, monDevName, monDevId); #endif@@ -2622,7 +2618,7 @@ void OBSBasicSettings::SaveAdvancedSettings() SaveCombo(ui->colorFormat, "Video", "ColorFormat"); SaveCombo(ui->colorSpace, "Video", "ColorSpace"); SaveComboData(ui->colorRange, "Video", "ColorRange");-#if defined(_WIN32) || defined(__APPLE__)+#if defined(_WIN32) || defined(__APPLE__) || HAVE_PULSEAUDIO SaveCombo(ui->monitoringDevice, "Audio", "MonitoringDeviceName"); SaveComboData(ui->monitoringDevice, "Audio", "MonitoringDeviceId"); #endif@@ -2648,7 +2644,7 @@ void OBSBasicSettings::SaveAdvancedSettings() SaveSpinBox(ui->reconnectMaxRetries, "Output", "MaxRetries"); SaveComboData(ui->bindToIP, "Output", "BindIP"); -#if defined(_WIN32) || defined(__APPLE__)+#if defined(_WIN32) || defined(__APPLE__) || HAVE_PULSEAUDIO QString newDevice = ui->monitoringDevice->currentData().toString(); if (lastMonitoringDevice != newDevice) {diff --git a/libobs/CMakeLists.txt b/libobs/CMakeLists.txtindex cd2b80e1e..e86bb8f2b 100644--- a/libobs/CMakeLists.txt+++ b/libobs/CMakeLists.txt@@ -12,12 +12,20 @@ if (NOT "${FFMPEG_AVCODEC_LIBRARIES}" STREQUAL "") endif() if(UNIX)+ find_package(PulseAudio)+ if (NOT "${PULSEAUDIO_LIBRARY}" STREQUAL "")+ message(STATUS "Found PulseAudio - Audio Monitor enabled")+ set(HAVE_PULSEAUDIO "1")+ else()+ set(HAVE_PULSEAUDIO "0")+ endif() find_package(DBus QUIET) if (NOT APPLE) find_package(X11_XCB REQUIRED) endif() else() set(HAVE_DBUS "0")+ set(HAVE_PULSEAUDIO "0") endif() find_package(ImageMagick QUIET COMPONENTS MagickCore)@@ -145,12 +153,22 @@ elseif(UNIX) util/threading-posix.c util/pipe-posix.c util/platform-nix.c)+ set(libobs_PLATFORM_HEADERS util/threading-posix.h)- set(libobs_audio_monitoring_SOURCES- audio-monitoring/null/null-audio-monitoring.c- ) + if(HAVE_PULSEAUDIO)+ set(libobs_audio_monitoring_HEADERS+ audio-monitoring/pulse/pulseaudio-wrapper.h)++ set(libobs_audio_monitoring_SOURCES+ audio-monitoring/pulse/pulseaudio-wrapper.c+ audio-monitoring/pulse/pulseaudio-enum-devices.c+ audio-monitoring/pulse/pulseaudio-output.c)+ else()+ set(libobs_audio_monitoring_SOURCES+ audio-monitoring/null/null-audio-monitoring.c)+ endif() if(DBUS_FOUND) set(libobs_PLATFORM_SOURCES ${libobs_PLATFORM_SOURCES} util/platform-nix-dbus.c)@@ -168,6 +186,12 @@ elseif(UNIX) ${libobs_PLATFORM_DEPS} ${X11_XCB_LIBRARIES}) + if(HAVE_PULSEAUDIO)+ set(libobs_PLATFORM_DEPS+ ${libobs_PLATFORM_DEPS}+ ${PULSEAUDIO_LIBRARY})+ endif()+ if(${CMAKE_SYSTEM_NAME} MATCHES "FreeBSD") # use the sysinfo compatibility library on bsd find_package(Libsysinfo REQUIRED)@@ -358,7 +382,9 @@ set(libobs_SOURCES ${libobs_graphics_SOURCES} ${libobs_mediaio_SOURCES} ${libobs_util_SOURCES}- ${libobs_libobs_SOURCES})+ ${libobs_libobs_SOURCES}+ ${libobs_audio_monitoring_SOURCES}+ ) set(libobs_HEADERS ${libobs_config_HEADERS}@@ -367,7 +393,6 @@ set(libobs_HEADERS ${libobs_mediaio_HEADERS} ${libobs_util_HEADERS} ${libobs_libobs_HEADERS}- ${libobs_audio_monitoring_SOURCES} ${libobs_audio_monitoring_HEADERS} ) diff --git a/libobs/audio-monitoring/null/null-audio-monitoring.c b/libobs/audio-monitoring/null/null-audio-monitoring.cindex 9d7b2a4ed..7926e84f6 100644--- a/libobs/audio-monitoring/null/null-audio-monitoring.c+++ b/libobs/audio-monitoring/null/null-audio-monitoring.c@@ -1,4 +1,4 @@-#include "../../obs-internal.h"+#include <obs-internal.h> void obs_enum_audio_monitoring_devices(obs_enum_audio_device_cb cb, void *data) {diff --git a/libobs/audio-monitoring/pulse/pulseaudio-enum-devices.c b/libobs/audio-monitoring/pulse/pulseaudio-enum-devices.cnew file mode 100644index 000000000..f59882997--- /dev/null+++ b/libobs/audio-monitoring/pulse/pulseaudio-enum-devices.c@@ -0,0 +1,33 @@+#include <obs-internal.h>+#include "pulseaudio-wrapper.h"++static void pulseaudio_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;++ struct enum_cb *ecb = (struct enum_cb *) userdata;+ if (ecb->cont)+ ecb->cont = ecb->cb(ecb->data, i->description, i->name);++skip:+ pulseaudio_signal(0);+}++void obs_enum_audio_monitoring_devices(obs_enum_audio_device_cb cb,+ void *data)+{+ struct enum_cb *ecb = bzalloc(sizeof(struct enum_cb));+ ecb->cb = cb;+ ecb->data = data;+ ecb->cont = 1;++ pulseaudio_init();+ pa_source_info_cb_t pa_cb = pulseaudio_output_info;+ pulseaudio_get_source_info_list(pa_cb, (void *) ecb);+ pulseaudio_unref();++ bfree(ecb);+}diff --git a/libobs/audio-monitoring/pulse/pulseaudio-output.c b/libobs/audio-monitoring/pulse/pulseaudio-output.cnew file mode 100644index 000000000..b182eb088--- /dev/null+++ b/libobs/audio-monitoring/pulse/pulseaudio-output.c@@ -0,0 +1,469 @@+#include "obs-internal.h"+#include "pulseaudio-wrapper.h"++#define PULSE_DATA(voidptr) struct audio_monitor *data = voidptr;+#define blog(level, msg, ...) blog(level, "pulse-am: " msg, ##__VA_ARGS__)++struct audio_monitor {+ obs_source_t *source;+ pa_stream *stream;+ char *device;++ enum speaker_layout speakers;+ pa_sample_format_t format;+ uint_fast32_t samples_per_sec;+ uint_fast32_t bytes_per_frame;+ uint_fast8_t channels;++ uint_fast32_t packets;+ uint_fast64_t frames;++ struct circlebuf new_data;+ audio_resampler_t *resampler;+ size_t buffer_size;+ size_t bytesRemaining;++ size_t bytes_per_channel;+ bool ignore : 1;+ pthread_mutex_t playback_mutex;+};++static enum speaker_layout pulseaudio_channels_to_obs_speakers(+ uint_fast32_t channels)+{+ if ((channels >= 1 && channels <= 6) || channels == 8)+ return (enum speaker_layout) channels;+ return SPEAKERS_UNKNOWN;+}++static enum audio_format pulseaudio_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_S32LE:+ return AUDIO_FORMAT_32BIT;+ case PA_SAMPLE_FLOAT32LE:+ return AUDIO_FORMAT_FLOAT;+ default:+ return AUDIO_FORMAT_UNKNOWN;+ }+}++static void process_byte(void *p, size_t frames, size_t channels, float vol)+{+ register char *cur = (char *) p;+ register char *end = cur + frames * channels;++ while (cur < end)+ *(cur++) *= vol;+}++static void process_short(void *p, size_t frames, size_t channels, float vol)+{+ register short *cur = (short *) p;+ register short *end = cur + frames * channels;++ while (cur < end)+ *(cur++) *= vol;+}++static void process_float(void *p, size_t frames, size_t channels, float vol)+{+ register float *cur = (float *) p;+ register float *end = cur + frames * channels;++ while (cur < end)+ *(cur++) *= vol;+}++void process_volume(const struct audio_monitor *monitor, float vol,+ uint8_t *const *resample_data, uint32_t resample_frames)+{+ switch (monitor->bytes_per_channel) {+ case 1:+ process_byte(resample_data[0], resample_frames,+ monitor->channels, vol);+ break;+ case 2:+ process_short(resample_data[0], resample_frames,+ monitor->channels, vol);+ break;+ default:+ process_float(resample_data[0], resample_frames,+ monitor->channels, vol);+ break;+ }+}++static void do_stream_write(void *param)+{+ PULSE_DATA(param);+ uint8_t *buffer = NULL;++ while (data->new_data.size >= data->buffer_size &&+ data->bytesRemaining > 0) {+ size_t bytesToFill = data->buffer_size;++ if (bytesToFill > data->bytesRemaining)+ bytesToFill = data->bytesRemaining;++ pa_stream_begin_write(data->stream, (void **) &buffer,+ &bytesToFill);++ circlebuf_pop_front(&data->new_data, buffer, bytesToFill);++ pulseaudio_lock();+ pa_stream_write(data->stream, buffer, bytesToFill, NULL,+ 0LL, PA_SEEK_RELATIVE);+ pulseaudio_unlock();++ data->bytesRemaining -= bytesToFill;+ }+}++static void on_audio_playback(void *param, obs_source_t *source,+ const struct audio_data *audio_data, bool muted)+{+ struct audio_monitor *monitor = param;+ float vol = source->user_volume;+ size_t bytes;++ uint8_t *resample_data[MAX_AV_PLANES];+ uint32_t resample_frames;+ uint64_t ts_offset;+ bool success;++ if (pthread_mutex_trylock(&monitor->playback_mutex) != 0)+ return;++ if (os_atomic_load_long(&source->activate_refs) == 0)+ goto unlock;++ success = audio_resampler_resample(monitor->resampler, resample_data,+ &resample_frames, &ts_offset,+ (const uint8_t *const *) audio_data->data,+ (uint32_t) audio_data->frames);++ if (!success)+ goto unlock;++ bytes = monitor->bytes_per_frame * resample_frames;++ if (muted) {+ memset(resample_data[0], 0, bytes);+ } else {+ if (!close_float(vol, 1.0f, EPSILON)) {+ process_volume(monitor, vol, resample_data,+ resample_frames);+ }+ }++ circlebuf_push_back(&monitor->new_data, resample_data[0], bytes);+ monitor->packets++;+ monitor->frames += resample_frames;++unlock:+ pthread_mutex_unlock(&monitor->playback_mutex);+ do_stream_write(param);+}++static void pulseaudio_stream_write(pa_stream *p, size_t nbytes, void *userdata)+{+ UNUSED_PARAMETER(p);+ PULSE_DATA(userdata);++ pthread_mutex_lock(&data->playback_mutex);+ data->bytesRemaining += nbytes;+ pthread_mutex_unlock(&data->playback_mutex);++ pulseaudio_signal(0);+}++static void pulseaudio_server_info(pa_context *c, const pa_server_info *i,+ void *userdata)+{+ UNUSED_PARAMETER(c);+ UNUSED_PARAMETER(userdata);++ blog(LOG_INFO, "Server name: '%s %s'", i->server_name,+ i->server_version);++ pulseaudio_signal(0);+}++static void pulseaudio_source_info(pa_context *c, const pa_source_info *i,+ int eol, void *userdata)+{+ UNUSED_PARAMETER(c);+ PULSE_DATA(userdata);+ // An error occured+ if (eol < 0) {+ data->format = PA_SAMPLE_INVALID;+ goto skip;+ }+ // Terminating call for multi instance callbacks+ if (eol > 0)+ goto skip;++ blog(LOG_INFO, "Audio format: %s, %"PRIu32" Hz, %"PRIu8" channels",+ pa_sample_format_to_string(i->sample_spec.format),+ i->sample_spec.rate, i->sample_spec.channels);++ pa_sample_format_t format = i->sample_spec.format;+ if (pulseaudio_to_obs_audio_format(format) == AUDIO_FORMAT_UNKNOWN) {+ format = PA_SAMPLE_S16LE;++ blog(LOG_INFO, "Sample format %s not supported by OBS,"+ "using %s instead for recording",+ pa_sample_format_to_string(+ i->sample_spec.format),+ pa_sample_format_to_string(format));+ }++ uint8_t channels = i->sample_spec.channels;+ if (pulseaudio_channels_to_obs_speakers(channels) == SPEAKERS_UNKNOWN) {+ channels = 2;++ blog(LOG_INFO, "%c channels not supported by OBS,"+ "using %c instead for recording",+ i->sample_spec.channels,+ channels);+ }++ data->format = format;+ data->samples_per_sec = i->sample_spec.rate;+ data->channels = channels;+skip:+ pulseaudio_signal(0);+}++static void pulseaudio_stop_playback(struct audio_monitor *monitor)+{+ if (monitor->stream) {+ pa_stream_disconnect(monitor->stream);+ pa_stream_unref(monitor->stream);+ monitor->stream = NULL;+ }++ blog(LOG_INFO, "Stopped Monitoring in '%s'", monitor->device);+ blog(LOG_INFO, "Got %"PRIuFAST32" packets with %"PRIuFAST64" frames",+ monitor->packets, monitor->frames);++ monitor->packets = 0;+ monitor->frames = 0;+}++static bool audio_monitor_init(struct audio_monitor *monitor,+ obs_source_t *source)+{+ pthread_mutex_init_value(&monitor->playback_mutex);++ monitor->source = source;++ const char *id = obs->audio.monitoring_device_id;+ if (!id)+ return false;++ if (source->info.output_flags & OBS_SOURCE_DO_NOT_SELF_MONITOR) {+ obs_data_t *s = obs_source_get_settings(source);+ const char *s_dev_id = obs_data_get_string(s, "device_id");+ bool match = devices_match(s_dev_id, id);+ obs_data_release(s);++ if (match) {+ monitor->ignore = true;+ blog(LOG_INFO, "Prevented feedback-loop in '%s'",+ s_dev_id);+ return true;+ }+ }++ pulseaudio_init();++ if (strcmp(id, "default") == 0)+ get_default_id(&monitor->device);+ else+ monitor->device = bstrdup(id);++ if (!monitor->device)+ return false;++ if (pulseaudio_get_server_info(pulseaudio_server_info,+ (void *) monitor) < 0) {+ blog(LOG_ERROR, "Unable to get server info !");+ return false;+ }++ if (pulseaudio_get_source_info(pulseaudio_source_info, monitor->device,+ (void *) monitor) < 0) {+ blog(LOG_ERROR, "Unable to get source info !");+ return false;+ }+ if (monitor->format == PA_SAMPLE_INVALID) {+ blog(LOG_ERROR,+ "An error occurred while getting the source info!");+ return false;+ }++ pa_sample_spec spec;+ spec.format = monitor->format;+ spec.rate = (uint32_t) monitor->samples_per_sec;+ spec.channels = monitor->channels;++ if (!pa_sample_spec_valid(&spec)) {+ blog(LOG_ERROR, "Sample spec is not valid");+ return false;+ }++ const struct audio_output_info *info = audio_output_get_info(+ obs->audio.audio);++ struct resample_info from = {+ .samples_per_sec = info->samples_per_sec,+ .speakers = info->speakers,+ .format = AUDIO_FORMAT_FLOAT_PLANAR+ };+ struct resample_info to = {+ .samples_per_sec = (uint32_t) monitor->samples_per_sec,+ .speakers = pulseaudio_channels_to_obs_speakers(+ monitor->channels),+ .format = pulseaudio_to_obs_audio_format(+ monitor->format)+ };++ monitor->resampler = audio_resampler_create(&to, &from);+ if (!monitor->resampler) {+ blog(LOG_WARNING, "%s: %s", __FUNCTION__,+ "Failed to create resampler");+ return false;+ }++ monitor->bytes_per_channel = get_audio_bytes_per_channel(+ pulseaudio_to_obs_audio_format(monitor->format));+ monitor->speakers = pulseaudio_channels_to_obs_speakers(spec.channels);+ monitor->bytes_per_frame = pa_frame_size(&spec);++ monitor->stream = pulseaudio_stream_new(+ obs_source_get_name(monitor->source), &spec, NULL);+ if (!monitor->stream) {+ blog(LOG_ERROR, "Unable to create stream");+ return false;+ }++ pa_buffer_attr attr;+ attr.fragsize = (uint32_t) -1;+ attr.maxlength = (uint32_t) -1;+ attr.minreq = (uint32_t) -1;+ attr.prebuf = (uint32_t) -1;+ attr.tlength = pa_usec_to_bytes(25000, &spec);++ monitor->buffer_size =+ monitor->bytes_per_frame * pa_usec_to_bytes(100, &spec);++ pa_stream_flags_t flags = PA_STREAM_ADJUST_LATENCY;++ if (pthread_mutex_init(&monitor->playback_mutex, NULL) != 0) {+ blog(LOG_WARNING, "%s: %s", __FUNCTION__,+ "Failed to init mutex");+ return false;+ }++ int_fast32_t ret = pulseaudio_connect_playback(monitor->stream,+ monitor->device, &attr, flags);+ if (ret < 0) {+ pulseaudio_stop_playback(monitor);+ blog(LOG_ERROR, "Unable to connect to stream");+ return false;+ }++ blog(LOG_INFO, "Started Monitoring in '%s'", monitor->device);+ return true;+}++static void audio_monitor_init_final(struct audio_monitor *monitor)+{+ if (monitor->ignore)+ return;++ obs_source_add_audio_capture_callback(monitor->source,+ on_audio_playback, monitor);++ pulseaudio_write_callback(monitor->stream, pulseaudio_stream_write,+ (void *) monitor);+}++static inline void audio_monitor_free(struct audio_monitor *monitor)+{+ if (monitor->ignore)+ return;++ if (monitor->source)+ obs_source_remove_audio_capture_callback(monitor->source,+ on_audio_playback, monitor);++ audio_resampler_destroy(monitor->resampler);+ circlebuf_free(&monitor->new_data);++ if (monitor->stream)+ pulseaudio_stop_playback(monitor);+ pulseaudio_unref();++ bfree(monitor->device);+}++struct audio_monitor *audio_monitor_create(obs_source_t *source)+{+ struct audio_monitor monitor = {0};+ struct audio_monitor *out;++ if (!audio_monitor_init(&monitor, source))+ goto fail;++ out = bmemdup(&monitor, sizeof(monitor));++ pthread_mutex_lock(&obs->audio.monitoring_mutex);+ da_push_back(obs->audio.monitors, &out);+ pthread_mutex_unlock(&obs->audio.monitoring_mutex);++ audio_monitor_init_final(out);+ return out;++fail:+ audio_monitor_free(&monitor);+ return NULL;+}++void audio_monitor_reset(struct audio_monitor *monitor)+{+ struct audio_monitor new_monitor = {0};+ bool success;+ audio_monitor_free(monitor);++ pthread_mutex_lock(&monitor->playback_mutex);+ success = audio_monitor_init(&new_monitor, monitor->source);+ pthread_mutex_unlock(&monitor->playback_mutex);++ if (success) {+ *monitor = new_monitor;+ audio_monitor_init_final(monitor);+ } else {+ audio_monitor_free(&new_monitor);+ }+}++void audio_monitor_destroy(struct audio_monitor *monitor)+{+ if (monitor) {+ audio_monitor_free(monitor);++ pthread_mutex_lock(&obs->audio.monitoring_mutex);+ da_erase_item(obs->audio.monitors, &monitor);+ pthread_mutex_unlock(&obs->audio.monitoring_mutex);++ bfree(monitor);+ }+}diff --git a/libobs/audio-monitoring/pulse/pulseaudio-wrapper.c b/libobs/audio-monitoring/pulse/pulseaudio-wrapper.cnew file mode 100644index 000000000..91939ae1b--- /dev/null+++ b/libobs/audio-monitoring/pulse/pulseaudio-wrapper.c@@ -0,0 +1,330 @@+/*+Copyright (C) 2014 by Leonhard Oelke <[email protected]>+Copyright (C) 2017 by Fabio Madia <[email protected]>++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 <http://www.gnu.org/licenses/>.+*/++#include <pthread.h>++#include <pulse/thread-mainloop.h>++#include <util/base.h>+#include <obs.h>++#include "pulseaudio-wrapper.h"++/* global data */+static uint_fast32_t pulseaudio_refs = 0;+static pthread_mutex_t pulseaudio_mutex = PTHREAD_MUTEX_INITIALIZER;+static pa_threaded_mainloop *pulseaudio_mainloop = NULL;+static pa_context *pulseaudio_context = NULL;++static void pulseaudio_default_devices(pa_context *c, const pa_server_info *i,+ void *userdata)+{+ UNUSED_PARAMETER(c);+ struct pulseaudio_default_output *d =+ (struct pulseaudio_default_output *) userdata;+ d->default_sink_name = bstrdup(i->default_sink_name);+ pulseaudio_signal(0);+}++void get_default_id(char **id)+{+ pulseaudio_init();+ struct pulseaudio_default_output *pdo = bzalloc(+ sizeof(struct pulseaudio_default_output));+ pulseaudio_get_server_info(+ (pa_server_info_cb_t) pulseaudio_default_devices,+ (void *) pdo);+ *id = bzalloc(strlen(pdo->default_sink_name) + 9);+ strcat(*id, pdo->default_sink_name);+ strcat(*id, ".monitor");+ bfree(pdo->default_sink_name);+ bfree(pdo);+ pulseaudio_unref();+}++bool devices_match(const char *id1, const char *id2)+{+ bool match;+ char *name1 = NULL;+ char *name2 = NULL;++ if (!id1 || !id2)+ return false;++ if (strcmp(id1, "default") == 0) {+ get_default_id(&name1);+ id1 = name1;+ }+ if (strcmp(id2, "default") == 0) {+ get_default_id(&name2);+ id2 = name2;+ }++ match = strcmp(id1, id2) == 0;+ bfree(name1);+ bfree(name2);+ return match;+}++/**+ * context status change callback+ *+ * @todo this is currently a noop, we want to reconnect here if the connection+ * is lost ...+ */+static void pulseaudio_context_state_changed(pa_context *c, void *userdata)+{+ UNUSED_PARAMETER(userdata);+ UNUSED_PARAMETER(c);++ pulseaudio_signal(0);+}++/**+ * get the default properties+ */+static pa_proplist *pulseaudio_properties()+{+ pa_proplist *p = pa_proplist_new();++ pa_proplist_sets(p, PA_PROP_APPLICATION_NAME, "OBS");+ pa_proplist_sets(p, PA_PROP_APPLICATION_ICON_NAME, "obs");+ pa_proplist_sets(p, PA_PROP_MEDIA_ROLE, "production");++ return p;+}++/**+ * Initialize the pulse audio context with properties and callback+ */+static void pulseaudio_init_context()+{+ pulseaudio_lock();++ pa_proplist *p = pulseaudio_properties();+ pulseaudio_context = pa_context_new_with_proplist(+ pa_threaded_mainloop_get_api(pulseaudio_mainloop),+ "OBS-Monitor", p);++ pa_context_set_state_callback(pulseaudio_context,+ pulseaudio_context_state_changed, NULL);++ pa_context_connect(pulseaudio_context, NULL, PA_CONTEXT_NOAUTOSPAWN,+ NULL);+ pa_proplist_free(p);++ pulseaudio_unlock();+}++/**+ * wait for context to be ready+ */+static int_fast32_t pulseaudio_context_ready()+{+ pulseaudio_lock();++ if (!PA_CONTEXT_IS_GOOD(pa_context_get_state(pulseaudio_context))) {+ pulseaudio_unlock();+ return -1;+ }++ while (pa_context_get_state(pulseaudio_context) != PA_CONTEXT_READY)+ pulseaudio_wait();++ pulseaudio_unlock();+ return 0;+}++int_fast32_t pulseaudio_init()+{+ pthread_mutex_lock(&pulseaudio_mutex);++ if (pulseaudio_refs == 0) {+ pulseaudio_mainloop = pa_threaded_mainloop_new();+ pa_threaded_mainloop_start(pulseaudio_mainloop);++ pulseaudio_init_context();+ }++ pulseaudio_refs++;++ pthread_mutex_unlock(&pulseaudio_mutex);++ return 0;+}++void pulseaudio_unref()+{+ pthread_mutex_lock(&pulseaudio_mutex);++ if (--pulseaudio_refs == 0) {+ pulseaudio_lock();+ if (pulseaudio_context != NULL) {+ pa_context_disconnect(pulseaudio_context);+ pa_context_unref(pulseaudio_context);+ pulseaudio_context = NULL;+ }+ pulseaudio_unlock();++ if (pulseaudio_mainloop != NULL) {+ pa_threaded_mainloop_stop(pulseaudio_mainloop);+ pa_threaded_mainloop_free(pulseaudio_mainloop);+ pulseaudio_mainloop = NULL;+ }+ }++ pthread_mutex_unlock(&pulseaudio_mutex);+}++void pulseaudio_lock()+{+ pa_threaded_mainloop_lock(pulseaudio_mainloop);+}++void pulseaudio_unlock()+{+ pa_threaded_mainloop_unlock(pulseaudio_mainloop);+}++void pulseaudio_wait()+{+ pa_threaded_mainloop_wait(pulseaudio_mainloop);+}++void pulseaudio_signal(int wait_for_accept)+{+ pa_threaded_mainloop_signal(pulseaudio_mainloop, wait_for_accept);+}++void pulseaudio_accept()+{+ pa_threaded_mainloop_accept(pulseaudio_mainloop);+}++int_fast32_t pulseaudio_get_source_info_list(pa_source_info_cb_t cb,+ void *userdata)+{+ if (pulseaudio_context_ready() < 0)+ return -1;++ pulseaudio_lock();++ pa_operation *op = pa_context_get_source_info_list(+ pulseaudio_context, cb, userdata);+ if (!op) {+ pulseaudio_unlock();+ return -1;+ }+ while (pa_operation_get_state(op) == PA_OPERATION_RUNNING)+ pulseaudio_wait();+ pa_operation_unref(op);++ pulseaudio_unlock();++ return 0;+}++int_fast32_t pulseaudio_get_source_info(pa_source_info_cb_t cb,+ const char *name, void *userdata)+{+ if (pulseaudio_context_ready() < 0)+ return -1;++ pulseaudio_lock();++ pa_operation *op = pa_context_get_source_info_by_name(+ pulseaudio_context, name, cb, userdata);+ if (!op) {+ pulseaudio_unlock();+ return -1;+ }+ while (pa_operation_get_state(op) == PA_OPERATION_RUNNING)+ pulseaudio_wait();+ pa_operation_unref(op);++ pulseaudio_unlock();++ return 0;+}++int_fast32_t pulseaudio_get_server_info(pa_server_info_cb_t cb, void *userdata)+{+ if (pulseaudio_context_ready() < 0)+ return -1;++ pulseaudio_lock();++ pa_operation *op = pa_context_get_server_info(+ pulseaudio_context, cb, userdata);+ if (!op) {+ pulseaudio_unlock();+ return -1;+ }+ while (pa_operation_get_state(op) == PA_OPERATION_RUNNING)+ pulseaudio_wait();+ pa_operation_unref(op);++ pulseaudio_unlock();+ return 0;+}++pa_stream *pulseaudio_stream_new(const char *name, const pa_sample_spec *ss,+ const pa_channel_map *map)+{+ if (pulseaudio_context_ready() < 0)+ return NULL;++ pulseaudio_lock();++ pa_proplist *p = pulseaudio_properties();+ pa_stream *s = pa_stream_new_with_proplist(+ pulseaudio_context, name, ss, map, p);+ pa_proplist_free(p);++ pulseaudio_unlock();+ return s;+}++int_fast32_t pulseaudio_connect_playback(pa_stream *s, const char *name,+ const pa_buffer_attr *attr, pa_stream_flags_t flags)+{+ if (pulseaudio_context_ready() < 0)+ return -1;++ size_t dev_len = strlen(name) - 8;+ char device[dev_len];+ memcpy(device, name, dev_len);+ device[dev_len] = '\0';++ pulseaudio_lock();+ int_fast32_t ret = pa_stream_connect_playback(s, device, attr, flags,+ NULL, NULL);+ pulseaudio_unlock();+ return ret;+}++void pulseaudio_write_callback(pa_stream *p, pa_stream_request_cb_t cb,+ void *userdata)+{+ if (pulseaudio_context_ready() < 0)+ return;++ pulseaudio_lock();+ pa_stream_set_write_callback(p, cb, userdata);+ pulseaudio_unlock();+}diff --git a/libobs/audio-monitoring/pulse/pulseaudio-wrapper.h b/libobs/audio-monitoring/pulse/pulseaudio-wrapper.hnew file mode 100644index 000000000..3630ab0f0--- /dev/null+++ b/libobs/audio-monitoring/pulse/pulseaudio-wrapper.h@@ -0,0 +1,175 @@+/*+Copyright (C) 2014 by Leonhard Oelke <[email protected]>+Copyright (C) 2017 by Fabio Madia <[email protected]>++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 <http://www.gnu.org/licenses/>.+*/++#include <inttypes.h>+#include <pulse/stream.h>+#include <pulse/context.h>+#include <pulse/introspect.h>++#pragma once++struct pulseaudio_default_output {+ char *default_sink_name;+};++struct enum_cb {+ obs_enum_audio_device_cb cb;+ void *data;+ int cont;+};++void get_default_id(char **id);++bool devices_match(const char *id1, const char *id2);++/**+ * Initialize the pulseaudio mainloop and increase the reference count+ */+int_fast32_t pulseaudio_init();++/**+ * Unreference the pulseaudio mainloop, when the reference count reaches+ * zero the mainloop will automatically be destroyed+ */+void pulseaudio_unref();++/**+ * Lock the mainloop+ *+ * In order to allow for multiple threads to use the same mainloop pulseaudio+ * provides it's own locking mechanism. This function should be called before+ * using any pulseaudio function that is in any way related to the mainloop or+ * context.+ *+ * @note use of this function may cause deadlocks+ *+ * @warning do not use with pulseaudio_ wrapper functions+ */+void pulseaudio_lock();++/**+ * Unlock the mainloop+ *+ * @see pulseaudio_lock()+ */+void pulseaudio_unlock();++/**+ * Wait for events to happen+ *+ * This function should be called when waiting for an event to happen.+ */+void pulseaudio_wait();++/**+ * Wait for accept signal from calling thread+ *+ * This function tells the pulseaudio mainloop wheter the data provided to+ * the callback should be retained until the calling thread executes+ * pulseaudio_accept()+ *+ * If wait_for_accept is 0 the function returns and the data is freed.+ */+void pulseaudio_signal(int wait_for_accept);++/**+ * Signal the waiting callback to return+ *+ * This function is used in conjunction with pulseaudio_signal()+ */+void pulseaudio_accept();++/**+ * Request source information+ *+ * The function will block until the operation was executed and the mainloop+ * called the provided callback function.+ *+ * @return negative on error+ *+ * @note The function will block until the server context is ready.+ *+ * @warning call without active locks+ */+int_fast32_t pulseaudio_get_source_info_list(pa_source_info_cb_t cb,+ void *userdata);++/**+ * Request source information from a specific source+ *+ * The function will block until the operation was executed and the mainloop+ * called the provided callback function.+ *+ * @param cb pointer to the callback function+ * @param name the source name to get information for+ * @param userdata pointer to userdata the callback will be called with+ *+ * @return negative on error+ *+ * @note The function will block until the server context is ready.+ *+ * @warning call without active locks+ */+int_fast32_t pulseaudio_get_source_info(pa_source_info_cb_t cb,+ const char *name, void *userdata);++/**+ * Request server information+ *+ * The function will block until the operation was executed and the mainloop+ * called the provided callback function.+ *+ * @return negative on error+ *+ * @note The function will block until the server context is ready.+ *+ * @warning call without active locks+ */+int_fast32_t pulseaudio_get_server_info(pa_server_info_cb_t cb, void *userdata);++/**+ * Create a new stream with the default properties+ *+ * @note The function will block until the server context is ready.+ *+ * @warning call without active locks+ */+pa_stream *pulseaudio_stream_new(const char *name, const pa_sample_spec *ss,+ const pa_channel_map *map);++/**+ * Connect to a pulseaudio playback stream+ *+ * @param s pa_stream to connect to. NULL for default+ * @param attr pa_buffer_attr+ * @param name Device name. NULL for default device+ * @param flags pa_stream_flags_t+ * @return negative on error+ */+int_fast32_t pulseaudio_connect_playback(pa_stream *s, const char *name,+ const pa_buffer_attr *attr, pa_stream_flags_t flags);++/**+ * Sets a callback function for when data can be written to the stream+ *+ * @param p pa_stream to connect to. NULL for default+ * @param cb pa_stream_request_cb_t+ * @param userdata pointer to userdata the callback will be called with+ */+void pulseaudio_write_callback(pa_stream *p, pa_stream_request_cb_t cb,+ void *userdata);diff --git a/libobs/obs.c b/libobs/obs.cindex b41c12a8d..ab2fb3d1c 100644--- a/libobs/obs.c+++ b/libobs/obs.c@@ -1898,7 +1898,7 @@ bool obs_set_audio_monitoring_device(const char *name, const char *id) if (!obs || !name || !id || !*name || !*id) return false; -#ifdef _WIN32+#if defined(_WIN32) || HAVE_PULSEAUDIO pthread_mutex_lock(&obs->audio.monitoring_mutex); if (strcmp(id, obs->audio.monitoring_device_id) == 0) {diff --git a/libobs/obsconfig.h.in b/libobs/obsconfig.h.inindex f86962d59..130a8b203 100644--- a/libobs/obsconfig.h.in+++ b/libobs/obsconfig.h.in@@ -17,3 +17,4 @@ #define OBS_UNIX_STRUCTURE @OBS_UNIX_STRUCTURE@ #define BUILD_CAPTIONS @BUILD_CAPTIONS@ #define HAVE_DBUS @HAVE_DBUS@+#define HAVE_PULSEAUDIO @HAVE_PULSEAUDIO@
Comments