2023-01-13 15:13:56 +00:00
|
|
|
#pragma once
|
|
|
|
#include "System.h"
|
|
|
|
#include "Common.h"
|
2023-03-21 21:09:56 +00:00
|
|
|
#include "Config.h"
|
2023-01-13 15:13:56 +00:00
|
|
|
|
2023-02-22 15:45:29 +00:00
|
|
|
#include <cmath>
|
2023-01-13 15:13:56 +00:00
|
|
|
#include <pulse/pulseaudio.h>
|
|
|
|
#include <stdlib.h>
|
|
|
|
#include <algorithm>
|
2023-02-21 21:46:40 +00:00
|
|
|
#include <list>
|
2023-01-13 15:13:56 +00:00
|
|
|
|
|
|
|
namespace PulseAudio
|
|
|
|
{
|
|
|
|
|
|
|
|
static pa_mainloop* mainLoop;
|
|
|
|
static pa_context* context;
|
2023-02-21 21:46:40 +00:00
|
|
|
static std::list<pa_operation*> pendingOperations;
|
2023-01-13 15:13:56 +00:00
|
|
|
|
2023-02-24 13:27:41 +00:00
|
|
|
static System::AudioInfo info;
|
|
|
|
static bool queueUpdate = false;
|
|
|
|
static bool blockUpdate = false;
|
|
|
|
|
2023-01-13 15:13:56 +00:00
|
|
|
inline void FlushLoop()
|
|
|
|
{
|
2023-02-21 21:46:40 +00:00
|
|
|
while (pendingOperations.size() > 0)
|
2023-01-13 15:13:56 +00:00
|
|
|
{
|
|
|
|
pa_mainloop_iterate(mainLoop, 0, nullptr);
|
2023-02-21 21:46:40 +00:00
|
|
|
// Remove done or cancelled operations
|
|
|
|
pendingOperations.remove_if(
|
|
|
|
[](pa_operation* op)
|
|
|
|
{
|
|
|
|
pa_operation_state_t state = pa_operation_get_state(op);
|
|
|
|
if (state == PA_OPERATION_DONE)
|
|
|
|
{
|
|
|
|
pa_operation_unref(op);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
else if (state == PA_OPERATION_CANCELLED)
|
|
|
|
{
|
|
|
|
LOG("Warning: Cancelled pa_operation!");
|
|
|
|
pa_operation_unref(op);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
});
|
2023-01-13 15:13:56 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-24 13:27:41 +00:00
|
|
|
inline double PAVolumeToDouble(const pa_cvolume* volume)
|
|
|
|
{
|
|
|
|
double vol = (double)pa_cvolume_avg(volume) / (double)PA_VOLUME_NORM;
|
|
|
|
// Just round to 1% precision, should be enough
|
|
|
|
constexpr double precision = 0.01;
|
|
|
|
double volRounded = std::round(vol * 1 / precision) * precision;
|
|
|
|
return volRounded;
|
|
|
|
}
|
|
|
|
|
2023-03-21 21:09:56 +00:00
|
|
|
inline double PAVolumeToDoubleWithMinMax(const pa_cvolume* volume)
|
|
|
|
{
|
|
|
|
double volRoundend = PAVolumeToDouble(volume);
|
|
|
|
// Clamp it to min/max
|
|
|
|
double minVolume = Config::Get().audioMinVolume / 100.;
|
|
|
|
double maxVolume = Config::Get().audioMaxVolume / 100.;
|
|
|
|
volRoundend = std::clamp(volRoundend, minVolume, maxVolume);
|
|
|
|
// Remap min/max to 0/1
|
|
|
|
double volRemapped = (volRoundend - minVolume) / (maxVolume - minVolume);
|
|
|
|
return volRemapped;
|
|
|
|
}
|
|
|
|
|
|
|
|
inline double DoubleToVolumeWithMinMax(double value)
|
|
|
|
{
|
|
|
|
// Clamp to 0/1
|
|
|
|
value = std::clamp(value, 0., 1.);
|
|
|
|
// Remap 0/1 to min/max
|
|
|
|
double minVolume = Config::Get().audioMinVolume / 100.;
|
|
|
|
double maxVolume = Config::Get().audioMaxVolume / 100.;
|
|
|
|
double volRemapped = value * (maxVolume - minVolume) + minVolume;
|
|
|
|
return volRemapped;
|
|
|
|
}
|
|
|
|
|
2023-02-24 13:27:41 +00:00
|
|
|
inline void UpdateInfo()
|
|
|
|
{
|
|
|
|
LOG("PulseAudio: Update info");
|
|
|
|
struct ServerInfo
|
|
|
|
{
|
|
|
|
const char* defaultSink = nullptr;
|
|
|
|
const char* defaultSource = nullptr;
|
|
|
|
} serverInfo;
|
|
|
|
|
|
|
|
// 1. Get default sink
|
|
|
|
auto getServerInfo = [](pa_context*, const pa_server_info* paInfo, void* out)
|
|
|
|
{
|
|
|
|
if (!paInfo)
|
|
|
|
return;
|
|
|
|
|
|
|
|
ServerInfo* serverInfo = (ServerInfo*)out;
|
|
|
|
serverInfo->defaultSink = paInfo->default_sink_name;
|
|
|
|
serverInfo->defaultSource = paInfo->default_source_name;
|
|
|
|
|
|
|
|
auto sinkInfo = [](pa_context*, const pa_sink_info* paInfo, int, void* audioInfo)
|
|
|
|
{
|
|
|
|
if (!paInfo)
|
|
|
|
return;
|
|
|
|
|
|
|
|
System::AudioInfo* out = (System::AudioInfo*)audioInfo;
|
|
|
|
|
2023-03-21 21:09:56 +00:00
|
|
|
double vol = PAVolumeToDoubleWithMinMax(&paInfo->volume);
|
2023-02-24 13:27:41 +00:00
|
|
|
out->sinkVolume = vol;
|
|
|
|
out->sinkMuted = paInfo->mute;
|
|
|
|
};
|
|
|
|
if (serverInfo->defaultSink)
|
|
|
|
{
|
|
|
|
pa_operation* op = pa_context_get_sink_info_by_name(context, serverInfo->defaultSink, +sinkInfo, &info);
|
|
|
|
pa_operation_ref(op);
|
|
|
|
pendingOperations.push_back(op);
|
|
|
|
}
|
|
|
|
|
|
|
|
auto sourceInfo = [](pa_context*, const pa_source_info* paInfo, int, void* audioInfo)
|
|
|
|
{
|
|
|
|
if (!paInfo)
|
|
|
|
return;
|
|
|
|
|
|
|
|
System::AudioInfo* out = (System::AudioInfo*)audioInfo;
|
|
|
|
|
|
|
|
double vol = PAVolumeToDouble(&paInfo->volume);
|
|
|
|
out->sourceVolume = vol;
|
|
|
|
out->sourceMuted = paInfo->mute;
|
|
|
|
};
|
|
|
|
if (serverInfo->defaultSource)
|
|
|
|
{
|
|
|
|
pa_operation* op = pa_context_get_source_info_by_name(context, serverInfo->defaultSource, +sourceInfo, &info);
|
|
|
|
pa_operation_ref(op);
|
|
|
|
pendingOperations.push_back(op);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
pa_operation* op = pa_context_get_server_info(context, +getServerInfo, &serverInfo);
|
|
|
|
pa_operation_ref(op);
|
|
|
|
pendingOperations.push_back(op);
|
|
|
|
FlushLoop();
|
|
|
|
}
|
|
|
|
|
|
|
|
inline System::AudioInfo GetInfo()
|
|
|
|
{
|
|
|
|
pa_mainloop_iterate(mainLoop, 0, nullptr);
|
|
|
|
if (queueUpdate && !blockUpdate)
|
|
|
|
{
|
|
|
|
UpdateInfo();
|
|
|
|
}
|
|
|
|
queueUpdate = false;
|
|
|
|
blockUpdate = false;
|
|
|
|
return info;
|
|
|
|
}
|
|
|
|
|
2023-01-13 15:13:56 +00:00
|
|
|
inline void Init()
|
|
|
|
{
|
|
|
|
mainLoop = pa_mainloop_new();
|
|
|
|
pa_mainloop_api* api = pa_mainloop_get_api(mainLoop);
|
|
|
|
|
|
|
|
context = pa_context_new(api, "gBar PA context");
|
|
|
|
int res = pa_context_connect(context, nullptr, PA_CONTEXT_NOAUTOSPAWN, nullptr);
|
|
|
|
|
2023-02-21 21:46:40 +00:00
|
|
|
bool ready = false;
|
|
|
|
auto stateCallback = [](pa_context* c, void* ready)
|
2023-01-13 15:13:56 +00:00
|
|
|
{
|
|
|
|
switch (pa_context_get_state(c))
|
|
|
|
{
|
|
|
|
case PA_CONTEXT_TERMINATED:
|
|
|
|
case PA_CONTEXT_FAILED:
|
|
|
|
case PA_CONTEXT_UNCONNECTED: ASSERT(false, "PA Callback error!"); break;
|
|
|
|
case PA_CONTEXT_AUTHORIZING:
|
|
|
|
case PA_CONTEXT_SETTING_NAME:
|
|
|
|
case PA_CONTEXT_CONNECTING:
|
|
|
|
// Don't care
|
|
|
|
break;
|
2023-02-21 20:19:22 +00:00
|
|
|
case PA_CONTEXT_READY:
|
2023-02-21 21:46:40 +00:00
|
|
|
LOG("PulseAudio: Context is ready!");
|
|
|
|
*(bool*)ready = true;
|
2023-02-21 20:19:22 +00:00
|
|
|
break;
|
2023-01-13 15:13:56 +00:00
|
|
|
}
|
|
|
|
};
|
2023-02-21 21:46:40 +00:00
|
|
|
pa_context_set_state_callback(context, +stateCallback, &ready);
|
2023-01-13 15:13:56 +00:00
|
|
|
|
2023-02-21 21:46:40 +00:00
|
|
|
while (!ready)
|
|
|
|
{
|
|
|
|
pa_mainloop_iterate(mainLoop, 0, nullptr);
|
|
|
|
}
|
2023-01-13 15:13:56 +00:00
|
|
|
|
2023-02-24 13:27:41 +00:00
|
|
|
// Subscribe to source and sink changes
|
|
|
|
auto subscribeSuccess = [](pa_context*, int success, void*)
|
2023-01-13 15:13:56 +00:00
|
|
|
{
|
2023-02-24 13:27:41 +00:00
|
|
|
ASSERT(success >= 0, "Failed to subscribe to pulseaudio");
|
2023-01-13 15:13:56 +00:00
|
|
|
};
|
2023-02-24 13:27:41 +00:00
|
|
|
pa_operation* op = pa_context_subscribe(context, (pa_subscription_mask_t)(PA_SUBSCRIPTION_MASK_SINK | PA_SUBSCRIPTION_MASK_SOURCE),
|
|
|
|
+subscribeSuccess, nullptr);
|
2023-02-21 21:46:40 +00:00
|
|
|
pa_operation_ref(op);
|
|
|
|
pendingOperations.push_back(op);
|
2023-01-13 15:13:56 +00:00
|
|
|
FlushLoop();
|
|
|
|
|
2023-02-24 13:27:41 +00:00
|
|
|
auto subscribeCallback = [](pa_context*, pa_subscription_event_type_t type, uint32_t, void*)
|
2023-01-13 15:13:56 +00:00
|
|
|
{
|
2023-02-24 13:27:41 +00:00
|
|
|
if (type == PA_SUBSCRIPTION_EVENT_CHANGE)
|
|
|
|
queueUpdate = true;
|
2023-01-13 15:13:56 +00:00
|
|
|
};
|
2023-02-24 13:27:41 +00:00
|
|
|
pa_context_set_subscribe_callback(context, +subscribeCallback, nullptr);
|
2023-02-21 21:46:40 +00:00
|
|
|
|
2023-02-24 13:27:41 +00:00
|
|
|
// Initialise info
|
|
|
|
UpdateInfo();
|
2023-01-13 15:13:56 +00:00
|
|
|
|
2023-02-24 13:27:41 +00:00
|
|
|
ASSERT(res >= 0, "pa_context_connect failed!");
|
2023-01-13 15:13:56 +00:00
|
|
|
}
|
|
|
|
|
2023-02-22 15:45:29 +00:00
|
|
|
inline void SetVolumeSink(double value)
|
2023-01-13 15:13:56 +00:00
|
|
|
{
|
2023-03-21 21:09:56 +00:00
|
|
|
double valClamped = DoubleToVolumeWithMinMax(value);
|
2023-01-13 15:13:56 +00:00
|
|
|
// I'm too lazy to implement the c api for this. Since it will only be called when needed and doesn't pipe, it shouldn't be a problem to
|
|
|
|
// fallback for a command
|
2023-03-21 16:49:10 +00:00
|
|
|
LOG("Audio: Set volume of sink: " << valClamped);
|
2023-03-21 21:09:56 +00:00
|
|
|
std::string cmd = "pamixer --allow-boost --set-volume " + std::to_string((uint32_t)(valClamped * 100));
|
|
|
|
info.sinkVolume = std::clamp(value, 0., 1.); // We need to stay in 0/1 range
|
2023-02-24 13:27:41 +00:00
|
|
|
blockUpdate = true;
|
2023-01-13 15:13:56 +00:00
|
|
|
system(cmd.c_str());
|
|
|
|
}
|
|
|
|
|
2023-02-22 15:45:29 +00:00
|
|
|
inline void SetVolumeSource(double value)
|
|
|
|
{
|
|
|
|
double valClamped = std::clamp(value, 0., 1.);
|
|
|
|
// I'm too lazy to implement the c api for this. Since it will only be called when needed and doesn't pipe, it shouldn't be a problem to
|
|
|
|
// fallback for a command
|
2023-03-21 16:49:10 +00:00
|
|
|
LOG("Audio: Set volume of source: " << valClamped);
|
2023-02-22 15:45:29 +00:00
|
|
|
std::string cmd = "pamixer --default-source --set-volume " + std::to_string((uint32_t)(valClamped * 100));
|
2023-02-24 13:27:41 +00:00
|
|
|
info.sourceVolume = valClamped;
|
|
|
|
blockUpdate = true;
|
2023-02-22 15:45:29 +00:00
|
|
|
system(cmd.c_str());
|
|
|
|
}
|
|
|
|
|
2023-01-13 15:13:56 +00:00
|
|
|
inline void Shutdown()
|
|
|
|
{
|
|
|
|
pa_mainloop_free(mainLoop);
|
|
|
|
}
|
|
|
|
}
|