mirror of
https://github.com/Palakis/obs-websocket.git
synced 2024-08-30 18:12:16 +00:00
EventHandler: Implement InputVolumeMeters
This is probably one of the most requested features for obs-websocket. This currently works by firing an event to all explicit subscribers with an array of all active audio sources every **60 milliseconds.** The `inputLevelsMul` field follows this data format: Base: [Channel, Channel] Channel: [magnitude (mul), peak (mul), input_peak (mul)] *Not Muted* *Muted* Example: [[0.3, 0.5, 0.9], [0.0, 0.0, 0.0]] (input_peak is the actual peak value, before volume adjustment.) You may notice that the values are only in mul. This is because we are trying to cut down on bandwidth. dB values can be calculated using this formula: `dB = 20.0 * log10(mul)`
This commit is contained in:
parent
1ac6ac6c87
commit
d48ddef031
@ -118,6 +118,7 @@ set(obs-websocket_SOURCES
|
||||
src/utils/Crypto.cpp
|
||||
src/utils/Json.cpp
|
||||
src/utils/Obs.cpp
|
||||
src/utils/ObsVolumeMeter.cpp
|
||||
src/utils/Platform.cpp
|
||||
src/utils/Compat.cpp
|
||||
deps/qr/cpp/QrCode.cpp)
|
||||
@ -141,6 +142,8 @@ set(obs-websocket_HEADERS
|
||||
src/utils/Crypto.h
|
||||
src/utils/Json.h
|
||||
src/utils/Obs.h
|
||||
src/utils/ObsVolumeMeter.h
|
||||
src/utils/ObsVolumeMeter_Helpers.h
|
||||
src/utils/Platform.h
|
||||
src/utils/Compat.h
|
||||
src/utils/Utils.h
|
||||
|
@ -75,8 +75,14 @@ void EventHandler::SetObsLoadedCallback(EventHandler::ObsLoadedCallback cb)
|
||||
// Function to increment refcounts for high volume event subscriptions
|
||||
void EventHandler::ProcessSubscription(uint64_t eventSubscriptions)
|
||||
{
|
||||
if ((eventSubscriptions & EventSubscription::InputVolumeMeters) != 0)
|
||||
_inputVolumeMetersRef++;
|
||||
if ((eventSubscriptions & EventSubscription::InputVolumeMeters) != 0) {
|
||||
if (_inputVolumeMetersRef.fetch_add(1) == 0) {
|
||||
if (_inputVolumeMetersHandler)
|
||||
blog(LOG_WARNING, "[EventHandler::ProcessSubscription] Input volume meter handler already exists!");
|
||||
else
|
||||
_inputVolumeMetersHandler = std::make_unique<Utils::Obs::VolumeMeter::Handler>(std::bind(&EventHandler::HandleInputVolumeMeters, this, std::placeholders::_1));
|
||||
}
|
||||
}
|
||||
if ((eventSubscriptions & EventSubscription::InputActiveStateChanged) != 0)
|
||||
_inputActiveStateChangedRef++;
|
||||
if ((eventSubscriptions & EventSubscription::InputShowStateChanged) != 0)
|
||||
@ -88,8 +94,10 @@ void EventHandler::ProcessSubscription(uint64_t eventSubscriptions)
|
||||
// Function to decrement refcounts for high volume event subscriptions
|
||||
void EventHandler::ProcessUnsubscription(uint64_t eventSubscriptions)
|
||||
{
|
||||
if ((eventSubscriptions & EventSubscription::InputVolumeMeters) != 0)
|
||||
_inputVolumeMetersRef--;
|
||||
if ((eventSubscriptions & EventSubscription::InputVolumeMeters) != 0) {
|
||||
if (_inputVolumeMetersRef.fetch_sub(1) == 1)
|
||||
_inputVolumeMetersHandler.reset();
|
||||
}
|
||||
if ((eventSubscriptions & EventSubscription::InputActiveStateChanged) != 0)
|
||||
_inputActiveStateChangedRef--;
|
||||
if ((eventSubscriptions & EventSubscription::InputShowStateChanged) != 0)
|
||||
|
@ -27,14 +27,9 @@ with this program. If not, see <https://www.gnu.org/licenses/>
|
||||
#include "types/EventSubscription.h"
|
||||
#include "../obs-websocket.h"
|
||||
#include "../utils/Obs.h"
|
||||
#include "../utils/ObsVolumeMeter.h"
|
||||
#include "../plugin-macros.generated.h"
|
||||
|
||||
template <typename T> T* GetCalldataPointer(const calldata_t *data, const char* name) {
|
||||
void *ptr = nullptr;
|
||||
calldata_get_ptr(data, name, &ptr);
|
||||
return reinterpret_cast<T*>(ptr);
|
||||
}
|
||||
|
||||
class EventHandler
|
||||
{
|
||||
public:
|
||||
@ -55,6 +50,7 @@ class EventHandler
|
||||
|
||||
std::atomic<bool> _obsLoaded;
|
||||
|
||||
std::unique_ptr<Utils::Obs::VolumeMeter::Handler> _inputVolumeMetersHandler;
|
||||
std::atomic<uint64_t> _inputVolumeMetersRef;
|
||||
std::atomic<uint64_t> _inputActiveStateChangedRef;
|
||||
std::atomic<uint64_t> _inputShowStateChangedRef;
|
||||
@ -107,6 +103,7 @@ class EventHandler
|
||||
void HandleInputCreated(obs_source_t *source);
|
||||
void HandleInputRemoved(obs_source_t *source);
|
||||
void HandleInputNameChanged(obs_source_t *source, std::string oldInputName, std::string inputName);
|
||||
void HandleInputVolumeMeters(std::vector<json> inputs); // AudioMeter::Handler callback
|
||||
static void HandleInputActiveStateChanged(void *param, calldata_t *data); // Direct callback
|
||||
static void HandleInputShowStateChanged(void *param, calldata_t *data); // Direct callback
|
||||
static void HandleInputMuteStateChanged(void *param, calldata_t *data); // Direct callback
|
||||
|
21
src/eventhandler/EventHandler_InputVolumeMeters.cpp
Normal file
21
src/eventhandler/EventHandler_InputVolumeMeters.cpp
Normal file
@ -0,0 +1,21 @@
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#include "EventHandler.h"
|
||||
|
@ -49,6 +49,13 @@ void EventHandler::HandleInputNameChanged(obs_source_t *, std::string oldInputNa
|
||||
BroadcastEvent(EventSubscription::Inputs, "InputNameChanged", eventData);
|
||||
}
|
||||
|
||||
void EventHandler::HandleInputVolumeMeters(std::vector<json> inputs)
|
||||
{
|
||||
json eventData;
|
||||
eventData["inputs"] = inputs;
|
||||
BroadcastEvent(EventSubscription::InputVolumeMeters, "InputVolumeMeters", eventData);
|
||||
}
|
||||
|
||||
void EventHandler::HandleInputActiveStateChanged(void *param, calldata_t *data)
|
||||
{
|
||||
auto eventHandler = reinterpret_cast<EventHandler*>(param);
|
||||
|
@ -142,6 +142,7 @@ void WebSocketApiEventCallback(std::string vendorName, std::string eventType, ob
|
||||
}
|
||||
|
||||
void ___source_dummy_addref(obs_source_t*) {}
|
||||
void ___weak_source_dummy_addref(obs_weak_source_t*) {}
|
||||
void ___scene_dummy_addref(obs_scene_t*) {}
|
||||
void ___sceneitem_dummy_addref(obs_sceneitem_t*) {}
|
||||
void ___data_dummy_addref(obs_data_t*) {}
|
||||
|
@ -33,6 +33,7 @@ with this program. If not, see <https://www.gnu.org/licenses/>
|
||||
|
||||
// Autorelease object definitions
|
||||
void ___source_dummy_addref(obs_source_t*);
|
||||
void ___weak_source_dummy_addref(obs_weak_source_t*);
|
||||
void ___scene_dummy_addref(obs_scene_t*);
|
||||
void ___sceneitem_dummy_addref(obs_sceneitem_t*);
|
||||
void ___data_dummy_addref(obs_data_t*);
|
||||
@ -43,6 +44,7 @@ void ___data_item_release(obs_data_item_t*);
|
||||
void ___properties_dummy_addref(obs_properties_t*);
|
||||
|
||||
using OBSSourceAutoRelease = OBSRef<obs_source_t*, ___source_dummy_addref, obs_source_release>;
|
||||
using OBSWeakSourceAutoRelease = OBSRef<obs_weak_source_t*, ___weak_source_dummy_addref, obs_weak_source_release>;
|
||||
using OBSSceneAutoRelease = OBSRef<obs_scene_t*, ___scene_dummy_addref, obs_scene_release>;
|
||||
using OBSSceneItemAutoRelease = OBSRef<obs_sceneitem_t*, ___sceneitem_dummy_addref, obs_sceneitem_release>;
|
||||
using OBSDataAutoRelease = OBSRef<obs_data_t*, ___data_dummy_addref, obs_data_release>;
|
||||
|
@ -24,6 +24,12 @@ with this program. If not, see <https://www.gnu.org/licenses/>
|
||||
|
||||
#include "Json.h"
|
||||
|
||||
template <typename T> T* GetCalldataPointer(const calldata_t *data, const char* name) {
|
||||
void *ptr = nullptr;
|
||||
calldata_get_ptr(data, name, &ptr);
|
||||
return reinterpret_cast<T*>(ptr);
|
||||
}
|
||||
|
||||
enum ObsOutputState {
|
||||
OBS_WEBSOCKET_OUTPUT_STARTING,
|
||||
OBS_WEBSOCKET_OUTPUT_STARTED,
|
||||
|
354
src/utils/ObsVolumeMeter.cpp
Normal file
354
src/utils/ObsVolumeMeter.cpp
Normal file
@ -0,0 +1,354 @@
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2014 by Leonhard Oelke <leonhard@in-verted.de>
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
#include "Obs.h"
|
||||
#include "ObsVolumeMeter.h"
|
||||
#include "ObsVolumeMeter_Helpers.h"
|
||||
|
||||
Utils::Obs::VolumeMeter::Meter::Meter(obs_source_t *input) :
|
||||
PeakMeterType(SAMPLE_PEAK_METER),
|
||||
_input(obs_source_get_weak_source(input)),
|
||||
_channels(0),
|
||||
_lastUpdate(0),
|
||||
_volume(obs_source_get_volume(input))
|
||||
{
|
||||
signal_handler_t *sh = obs_source_get_signal_handler(input);
|
||||
signal_handler_connect(sh, "volume", Meter::InputVolumeCallback, this);
|
||||
|
||||
obs_source_add_audio_capture_callback(input, Meter::InputAudioCaptureCallback, this);
|
||||
|
||||
blog_debug("[Utils::Obs::VolumeMeter::Meter::Meter] Meter created for input: %s", obs_source_get_name(input));
|
||||
}
|
||||
|
||||
Utils::Obs::VolumeMeter::Meter::~Meter()
|
||||
{
|
||||
OBSSourceAutoRelease input = obs_weak_source_get_source(_input);
|
||||
if (!input) {
|
||||
blog(LOG_WARNING, "[Utils::Obs::VolumeMeter::Meter::~Meter] Failed to get strong reference to input. Has it been destroyed?");
|
||||
return;
|
||||
}
|
||||
|
||||
signal_handler_t *sh = obs_source_get_signal_handler(input);
|
||||
signal_handler_disconnect(sh, "volume", Meter::InputVolumeCallback, this);
|
||||
|
||||
obs_source_remove_audio_capture_callback(input, Meter::InputAudioCaptureCallback, this);
|
||||
|
||||
blog_debug("[Utils::Obs::VolumeMeter::Meter::~Meter] Meter destroyed for input: %s", obs_source_get_name(input));
|
||||
}
|
||||
|
||||
bool Utils::Obs::VolumeMeter::Meter::InputValid()
|
||||
{
|
||||
return !obs_weak_source_expired(_input);
|
||||
}
|
||||
|
||||
json Utils::Obs::VolumeMeter::Meter::GetMeterData()
|
||||
{
|
||||
json ret;
|
||||
|
||||
OBSSourceAutoRelease input = obs_weak_source_get_source(_input);
|
||||
if (!input) {
|
||||
blog(LOG_WARNING, "[Utils::Obs::VolumeMeter::Meter::GetMeterData] Failed to get strong reference to input. Has it been destroyed?");
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::vector<std::vector<float>> levels;
|
||||
const float volume = _muted ? 0.0f : _volume.load();
|
||||
|
||||
std::unique_lock<std::mutex> l(_mutex);
|
||||
|
||||
if (_lastUpdate != 0 && (os_gettime_ns() - _lastUpdate) * 0.000000001 > 0.3)
|
||||
ResetAudioLevels();
|
||||
|
||||
for (int channel = 0; channel < _channels; channel++) {
|
||||
std::vector<float> level;
|
||||
level.push_back(_magnitude[channel] * volume);
|
||||
level.push_back(_peak[channel] * volume);
|
||||
level.push_back(_peak[channel]);
|
||||
|
||||
levels.push_back(level);
|
||||
}
|
||||
l.unlock();
|
||||
|
||||
ret["inputName"] = obs_source_get_name(input);
|
||||
ret["inputLevelsMul"] = levels;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// MUST HOLD LOCK
|
||||
void Utils::Obs::VolumeMeter::Meter::ResetAudioLevels()
|
||||
{
|
||||
_lastUpdate = 0;
|
||||
for (int channelNumber = 0; channelNumber < MAX_AUDIO_CHANNELS; channelNumber++) {
|
||||
_magnitude[channelNumber] = 0;
|
||||
_peak[channelNumber] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// MUST HOLD LOCK
|
||||
void Utils::Obs::VolumeMeter::Meter::ProcessAudioChannels(const struct audio_data *data)
|
||||
{
|
||||
int channels = 0;
|
||||
for (int i = 0; i < MAX_AV_PLANES; i++) {
|
||||
if (data->data[i])
|
||||
channels++;
|
||||
}
|
||||
|
||||
bool channelsChanged = _channels != channels;
|
||||
_channels = std::clamp(channels, 0, MAX_AUDIO_CHANNELS);
|
||||
|
||||
if (channelsChanged)
|
||||
ResetAudioLevels();
|
||||
}
|
||||
|
||||
// MUST HOLD LOCK
|
||||
void Utils::Obs::VolumeMeter::Meter::ProcessPeak(const struct audio_data *data)
|
||||
{
|
||||
size_t sampleCount = data->frames;
|
||||
int channelNumber = 0;
|
||||
|
||||
for (int planeNumber = 0; channelNumber < _channels; planeNumber++) {
|
||||
float *samples = (float*)data->data[planeNumber];
|
||||
if (!samples)
|
||||
continue;
|
||||
|
||||
if (((uintptr_t)samples & 0xf) > 0) {
|
||||
_peak[channelNumber] = 1.0f;
|
||||
channelNumber++;
|
||||
continue;
|
||||
}
|
||||
|
||||
__m128 previousSamples = _mm_loadu_ps(_previousSamples[channelNumber]);
|
||||
|
||||
float peak;
|
||||
switch (PeakMeterType) {
|
||||
default:
|
||||
case SAMPLE_PEAK_METER:
|
||||
peak = GetSamplePeak(previousSamples, samples, sampleCount);
|
||||
break;
|
||||
case TRUE_PEAK_METER:
|
||||
peak = GetTruePeak(previousSamples, samples, sampleCount);
|
||||
break;
|
||||
}
|
||||
|
||||
switch (sampleCount) {
|
||||
case 0:
|
||||
break;
|
||||
case 1:
|
||||
_previousSamples[channelNumber][0] = _previousSamples[channelNumber][1];
|
||||
_previousSamples[channelNumber][1] = _previousSamples[channelNumber][2];
|
||||
_previousSamples[channelNumber][2] = _previousSamples[channelNumber][3];
|
||||
_previousSamples[channelNumber][3] = samples[sampleCount - 1];
|
||||
break;
|
||||
case 2:
|
||||
_previousSamples[channelNumber][0] = _previousSamples[channelNumber][2];
|
||||
_previousSamples[channelNumber][1] = _previousSamples[channelNumber][3];
|
||||
_previousSamples[channelNumber][2] = samples[sampleCount - 2];
|
||||
_previousSamples[channelNumber][3] = samples[sampleCount - 1];
|
||||
break;
|
||||
case 3:
|
||||
_previousSamples[channelNumber][0] = _previousSamples[channelNumber][3];
|
||||
_previousSamples[channelNumber][1] = samples[sampleCount - 3];
|
||||
_previousSamples[channelNumber][2] = samples[sampleCount - 2];
|
||||
_previousSamples[channelNumber][3] = samples[sampleCount - 1];
|
||||
break;
|
||||
default:
|
||||
_previousSamples[channelNumber][0] = samples[sampleCount - 4];
|
||||
_previousSamples[channelNumber][1] = samples[sampleCount - 3];
|
||||
_previousSamples[channelNumber][2] = samples[sampleCount - 2];
|
||||
_previousSamples[channelNumber][3] = samples[sampleCount - 1];
|
||||
}
|
||||
|
||||
_peak[channelNumber] = peak;
|
||||
|
||||
channelNumber++;
|
||||
}
|
||||
|
||||
for (; channelNumber < MAX_AUDIO_CHANNELS; channelNumber++)
|
||||
_peak[channelNumber] = 0.0;
|
||||
}
|
||||
|
||||
// MUST HOLD LOCK
|
||||
void Utils::Obs::VolumeMeter::Meter::ProcessMagnitude(const struct audio_data *data)
|
||||
{
|
||||
size_t sampleCount = data->frames;
|
||||
|
||||
int channelNumber = 0;
|
||||
for (int planeNumber = 0; channelNumber < _channels; planeNumber++) {
|
||||
float *samples = (float*)data->data[planeNumber];
|
||||
if (!samples)
|
||||
continue;
|
||||
|
||||
float sum = 0.0;
|
||||
for (size_t i = 0; i < sampleCount; i++) {
|
||||
float sample = samples[i];
|
||||
sum += sample * sample;
|
||||
}
|
||||
|
||||
_magnitude[channelNumber] = std::sqrt(sum / sampleCount);
|
||||
|
||||
channelNumber++;
|
||||
}
|
||||
}
|
||||
|
||||
void Utils::Obs::VolumeMeter::Meter::InputAudioCaptureCallback(void *priv_data, obs_source_t *, const struct audio_data *data, bool muted)
|
||||
{
|
||||
auto c = static_cast<Meter*>(priv_data);
|
||||
|
||||
std::unique_lock<std::mutex> l(c->_mutex);
|
||||
|
||||
c->_muted = muted;
|
||||
c->ProcessAudioChannels(data);
|
||||
c->ProcessPeak(data);
|
||||
c->ProcessMagnitude(data);
|
||||
|
||||
c->_lastUpdate = os_gettime_ns();
|
||||
}
|
||||
|
||||
void Utils::Obs::VolumeMeter::Meter::InputVolumeCallback(void *priv_data, calldata_t *cd)
|
||||
{
|
||||
auto c = static_cast<Meter*>(priv_data);
|
||||
|
||||
c->_volume = (float)calldata_float(cd, "volume");
|
||||
}
|
||||
|
||||
Utils::Obs::VolumeMeter::Handler::Handler(UpdateCallback cb, uint64_t updatePeriod) :
|
||||
_updateCallback(cb),
|
||||
_updatePeriod(updatePeriod),
|
||||
_running(false)
|
||||
{
|
||||
signal_handler_t *sh = obs_get_signal_handler();
|
||||
if (!sh)
|
||||
return;
|
||||
|
||||
auto enumProc = [](void *priv_data, obs_source_t *input) {
|
||||
auto c = static_cast<Handler*>(priv_data);
|
||||
|
||||
if (!obs_source_active(input))
|
||||
return true;
|
||||
|
||||
uint32_t flags = obs_source_get_output_flags(input);
|
||||
if ((flags & OBS_SOURCE_AUDIO) == 0)
|
||||
return true;
|
||||
|
||||
c->_meters.emplace_back(std::move(new Meter(input)));
|
||||
|
||||
return true;
|
||||
};
|
||||
obs_enum_sources(enumProc, this);
|
||||
|
||||
signal_handler_connect(sh, "source_activate", Handler::InputActivateCallback, this);
|
||||
signal_handler_connect(sh, "source_deactivate", Handler::InputDeactivateCallback, this);
|
||||
|
||||
_running = true;
|
||||
_updateThread = std::thread(&Handler::UpdateThread, this);
|
||||
|
||||
blog_debug("[Utils::Obs::VolumeMeter::Handler::Handler] Handler created.");
|
||||
}
|
||||
|
||||
Utils::Obs::VolumeMeter::Handler::~Handler()
|
||||
{
|
||||
signal_handler_t *sh = obs_get_signal_handler();
|
||||
if (!sh)
|
||||
return;
|
||||
|
||||
signal_handler_disconnect(sh, "source_activate", Handler::InputActivateCallback, this);
|
||||
signal_handler_disconnect(sh, "source_deactivate", Handler::InputDeactivateCallback, this);
|
||||
|
||||
if (_running) {
|
||||
_mutex.lock();
|
||||
_running = false;
|
||||
_mutex.unlock();
|
||||
_cond.notify_all();
|
||||
}
|
||||
|
||||
if (_updateThread.joinable())
|
||||
_updateThread.join();
|
||||
|
||||
blog_debug("[Utils::Obs::VolumeMeter::Handler::~Handler] Handler destroyed.");
|
||||
}
|
||||
|
||||
void Utils::Obs::VolumeMeter::Handler::UpdateThread()
|
||||
{
|
||||
blog_debug("[Utils::Obs::VolumeMeter::Handler::UpdateThread] Thread started.");
|
||||
while (_running) {
|
||||
{
|
||||
std::unique_lock<std::mutex> l(_mutex);
|
||||
if (_cond.wait_for(l, std::chrono::milliseconds(_updatePeriod), [this]{ return !_running; }))
|
||||
break;
|
||||
}
|
||||
|
||||
std::vector<json> inputs;
|
||||
std::unique_lock<std::mutex> l(_meterMutex);
|
||||
for (auto &meter : _meters) {
|
||||
if (meter->InputValid())
|
||||
inputs.push_back(meter->GetMeterData());
|
||||
}
|
||||
l.unlock();
|
||||
|
||||
if (_updateCallback)
|
||||
_updateCallback(inputs);
|
||||
}
|
||||
blog_debug("[Utils::Obs::VolumeMeter::Handler::UpdateThread] Thread stopped.");
|
||||
}
|
||||
|
||||
void Utils::Obs::VolumeMeter::Handler::InputActivateCallback(void *priv_data, calldata_t *cd)
|
||||
{
|
||||
auto c = static_cast<Handler*>(priv_data);
|
||||
|
||||
obs_source_t *input = GetCalldataPointer<obs_source_t>(cd, "source");
|
||||
if (!input)
|
||||
return;
|
||||
|
||||
if (obs_source_get_type(input) != OBS_SOURCE_TYPE_INPUT)
|
||||
return;
|
||||
|
||||
uint32_t flags = obs_source_get_output_flags(input);
|
||||
if ((flags & OBS_SOURCE_AUDIO) == 0)
|
||||
return;
|
||||
|
||||
std::unique_lock<std::mutex> l(c->_meterMutex);
|
||||
c->_meters.emplace_back(std::move(new Meter(input)));
|
||||
}
|
||||
|
||||
void Utils::Obs::VolumeMeter::Handler::InputDeactivateCallback(void *priv_data, calldata_t *cd)
|
||||
{
|
||||
auto c = static_cast<Handler*>(priv_data);
|
||||
|
||||
obs_source_t *input = GetCalldataPointer<obs_source_t>(cd, "source");
|
||||
if (!input)
|
||||
return;
|
||||
|
||||
if (obs_source_get_type(input) != OBS_SOURCE_TYPE_INPUT)
|
||||
return;
|
||||
|
||||
// Don't ask me why, but using std::remove_if segfaults trying this.
|
||||
std::unique_lock<std::mutex> l(c->_meterMutex);
|
||||
std::vector<MeterPtr>::iterator iter;
|
||||
for (iter = c->_meters.begin(); iter != c->_meters.end();) {
|
||||
if (obs_weak_source_references_source(iter->get()->GetWeakInput(), input))
|
||||
iter = c->_meters.erase(iter);
|
||||
else
|
||||
++iter;
|
||||
}
|
||||
}
|
99
src/utils/ObsVolumeMeter.h
Normal file
99
src/utils/ObsVolumeMeter.h
Normal file
@ -0,0 +1,99 @@
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include <obs.hpp>
|
||||
|
||||
#include "../obs-websocket.h"
|
||||
#include "Json.h"
|
||||
|
||||
namespace Utils {
|
||||
namespace Obs {
|
||||
namespace VolumeMeter {
|
||||
// Some code copied from https://github.com/obsproject/obs-studio/blob/master/libobs/obs-audio-controls.c
|
||||
// Keeps a running tally of the current audio levels, for a specific input
|
||||
class Meter {
|
||||
public:
|
||||
Meter(obs_source_t *input);
|
||||
~Meter();
|
||||
|
||||
bool InputValid();
|
||||
obs_weak_source_t *GetWeakInput() { return _input; }
|
||||
json GetMeterData();
|
||||
|
||||
std::atomic<enum obs_peak_meter_type> PeakMeterType;
|
||||
|
||||
private:
|
||||
OBSWeakSourceAutoRelease _input;
|
||||
|
||||
// All values in mul
|
||||
std::mutex _mutex;
|
||||
bool _muted;
|
||||
int _channels;
|
||||
float _magnitude[MAX_AUDIO_CHANNELS];
|
||||
float _peak[MAX_AUDIO_CHANNELS];
|
||||
float _previousSamples[MAX_AUDIO_CHANNELS][4];
|
||||
|
||||
std::atomic<uint64_t> _lastUpdate;
|
||||
std::atomic<float> _volume;
|
||||
|
||||
void ResetAudioLevels();
|
||||
void ProcessAudioChannels(const struct audio_data *data);
|
||||
void ProcessPeak(const struct audio_data *data);
|
||||
void ProcessMagnitude(const struct audio_data *data);
|
||||
|
||||
static void InputAudioCaptureCallback(void *priv_data, obs_source_t *source, const struct audio_data *data, bool muted);
|
||||
static void InputVolumeCallback(void *priv_data, calldata_t *cd);
|
||||
};
|
||||
|
||||
// Maintains an array of active inputs
|
||||
class Handler {
|
||||
typedef std::function<void(std::vector<json>)> UpdateCallback;
|
||||
typedef std::unique_ptr<Meter> MeterPtr;
|
||||
|
||||
public:
|
||||
Handler(UpdateCallback cb, uint64_t updatePeriod = 60);
|
||||
~Handler();
|
||||
|
||||
private:
|
||||
UpdateCallback _updateCallback;
|
||||
|
||||
std::mutex _meterMutex;
|
||||
std::vector<MeterPtr> _meters;
|
||||
uint64_t _updatePeriod;
|
||||
|
||||
std::mutex _mutex;
|
||||
std::condition_variable _cond;
|
||||
bool _running;
|
||||
std::thread _updateThread;
|
||||
|
||||
void UpdateThread();
|
||||
static void InputActivateCallback(void *priv_data, calldata_t *cd);
|
||||
static void InputDeactivateCallback(void *priv_data, calldata_t *cd);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
108
src/utils/ObsVolumeMeter_Helpers.h
Normal file
108
src/utils/ObsVolumeMeter_Helpers.h
Normal file
@ -0,0 +1,108 @@
|
||||
/*
|
||||
obs-websocket
|
||||
Copyright (C) 2014 by Leonhard Oelke <leonhard@in-verted.de>
|
||||
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
|
||||
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>
|
||||
*/
|
||||
|
||||
// All of this is literally magic
|
||||
|
||||
// It should probably be imported as a libobs util someday though.
|
||||
|
||||
#define SHIFT_RIGHT_2PS(msb, lsb) \
|
||||
{ \
|
||||
__m128 tmp = \
|
||||
_mm_shuffle_ps(lsb, msb, _MM_SHUFFLE(0, 0, 3, 3)); \
|
||||
lsb = _mm_shuffle_ps(lsb, tmp, _MM_SHUFFLE(2, 1, 2, 1)); \
|
||||
msb = _mm_shuffle_ps(msb, msb, _MM_SHUFFLE(3, 3, 2, 1)); \
|
||||
}
|
||||
|
||||
#define abs_ps(v) _mm_andnot_ps(_mm_set1_ps(-0.f), v)
|
||||
|
||||
#define VECTOR_MATRIX_CROSS_PS(out, v, m0, m1, m2, m3) \
|
||||
{ \
|
||||
out = _mm_mul_ps(v, m0); \
|
||||
__m128 mul1 = _mm_mul_ps(v, m1); \
|
||||
__m128 mul2 = _mm_mul_ps(v, m2); \
|
||||
__m128 mul3 = _mm_mul_ps(v, m3); \
|
||||
\
|
||||
_MM_TRANSPOSE4_PS(out, mul1, mul2, mul3); \
|
||||
\
|
||||
out = _mm_add_ps(out, mul1); \
|
||||
out = _mm_add_ps(out, mul2); \
|
||||
out = _mm_add_ps(out, mul3); \
|
||||
}
|
||||
|
||||
#define hmax_ps(r, x4) \
|
||||
do { \
|
||||
float x4_mem[4]; \
|
||||
_mm_storeu_ps(x4_mem, x4); \
|
||||
r = x4_mem[0]; \
|
||||
r = fmaxf(r, x4_mem[1]); \
|
||||
r = fmaxf(r, x4_mem[2]); \
|
||||
r = fmaxf(r, x4_mem[3]); \
|
||||
} while (false)
|
||||
|
||||
float GetSamplePeak(__m128 previousSamples, const float *samples, size_t sampleCount)
|
||||
{
|
||||
__m128 peak = previousSamples;
|
||||
for (size_t i = 0; (i + 3) < sampleCount; i += 4) {
|
||||
__m128 newWork = _mm_load_ps(&samples[i]);
|
||||
peak = _mm_max_ps(peak, abs_ps(newWork));
|
||||
}
|
||||
|
||||
float ret;
|
||||
hmax_ps(ret, peak);
|
||||
return ret;
|
||||
}
|
||||
|
||||
float GetTruePeak(__m128 previousSamples, const float *samples, size_t sampleCount)
|
||||
{
|
||||
const __m128 m3 = _mm_set_ps(-0.155915f, 0.935489f, 0.233872f, -0.103943f);
|
||||
const __m128 m1 = _mm_set_ps(-0.216236f, 0.756827f, 0.504551f, -0.189207f);
|
||||
const __m128 p1 = _mm_set_ps(-0.189207f, 0.504551f, 0.756827f, -0.216236f);
|
||||
const __m128 p3 = _mm_set_ps(-0.103943f, 0.233872f, 0.935489f, -0.155915f);
|
||||
|
||||
__m128 work = previousSamples;
|
||||
__m128 peak = previousSamples;
|
||||
for (size_t i = 0; (i + 3) < sampleCount; i += 4) {
|
||||
__m128 newWork = _mm_load_ps(&samples[i]);
|
||||
__m128 intrpSamples;
|
||||
|
||||
__m128 absNewWork = abs_ps(newWork);
|
||||
peak = _mm_max_ps(peak, absNewWork);
|
||||
|
||||
SHIFT_RIGHT_2PS(newWork, work);
|
||||
VECTOR_MATRIX_CROSS_PS(intrpSamples, work, m3, m1, p1, p3);
|
||||
peak = _mm_max_ps(peak, abs_ps(intrpSamples));
|
||||
|
||||
SHIFT_RIGHT_2PS(newWork, work);
|
||||
VECTOR_MATRIX_CROSS_PS(intrpSamples, work, m3, m1, p1, p3);
|
||||
peak = _mm_max_ps(peak, abs_ps(intrpSamples));
|
||||
|
||||
SHIFT_RIGHT_2PS(newWork, work);
|
||||
VECTOR_MATRIX_CROSS_PS(intrpSamples, work, m3, m1, p1, p3);
|
||||
peak = _mm_max_ps(peak, abs_ps(intrpSamples));
|
||||
|
||||
SHIFT_RIGHT_2PS(newWork, work);
|
||||
VECTOR_MATRIX_CROSS_PS(intrpSamples, work, m3, m1, p1, p3);
|
||||
peak = _mm_max_ps(peak, abs_ps(intrpSamples));
|
||||
}
|
||||
|
||||
float ret;
|
||||
hmax_ps(ret, peak);
|
||||
return ret;
|
||||
}
|
@ -22,5 +22,6 @@ with this program. If not, see <https://www.gnu.org/licenses/>
|
||||
#include "Crypto.h"
|
||||
#include "Json.h"
|
||||
#include "Obs.h"
|
||||
#include "ObsVolumeMeter.h"
|
||||
#include "Platform.h"
|
||||
#include "Compat.h"
|
||||
|
Loading…
Reference in New Issue
Block a user