2017-09-07 23:27:21 -04:00
|
|
|
#include "adapters/pulseaudio.hpp"
|
2017-10-29 01:50:51 -04:00
|
|
|
#include "components/logger.hpp"
|
2017-09-07 23:27:21 -04:00
|
|
|
|
|
|
|
POLYBAR_NS
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Construct pulseaudio object
|
|
|
|
*/
|
2017-10-29 01:50:51 -04:00
|
|
|
pulseaudio::pulseaudio(const logger& logger, string&& sink_name) : m_log(logger), spec_s_name(sink_name) {
|
2017-09-07 23:27:21 -04:00
|
|
|
m_mainloop = pa_threaded_mainloop_new();
|
|
|
|
if (!m_mainloop) {
|
|
|
|
throw pulseaudio_error("Could not create pulseaudio threaded mainloop.");
|
|
|
|
}
|
|
|
|
pa_threaded_mainloop_lock(m_mainloop);
|
|
|
|
|
|
|
|
m_context = pa_context_new(pa_threaded_mainloop_get_api(m_mainloop), "polybar");
|
|
|
|
if (!m_context) {
|
|
|
|
pa_threaded_mainloop_unlock(m_mainloop);
|
|
|
|
pa_threaded_mainloop_free(m_mainloop);
|
|
|
|
throw pulseaudio_error("Could not create pulseaudio context.");
|
|
|
|
}
|
2017-10-07 23:19:06 -04:00
|
|
|
|
2017-09-07 23:27:21 -04:00
|
|
|
pa_context_set_state_callback(m_context, context_state_callback, this);
|
|
|
|
|
|
|
|
if (pa_context_connect(m_context, nullptr, PA_CONTEXT_NOFLAGS, nullptr) < 0) {
|
|
|
|
pa_context_disconnect(m_context);
|
|
|
|
pa_context_unref(m_context);
|
|
|
|
pa_threaded_mainloop_unlock(m_mainloop);
|
|
|
|
pa_threaded_mainloop_free(m_mainloop);
|
|
|
|
throw pulseaudio_error("Could not connect pulseaudio context.");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (pa_threaded_mainloop_start(m_mainloop) < 0) {
|
|
|
|
pa_context_disconnect(m_context);
|
|
|
|
pa_context_unref(m_context);
|
|
|
|
pa_threaded_mainloop_unlock(m_mainloop);
|
|
|
|
pa_threaded_mainloop_free(m_mainloop);
|
|
|
|
throw pulseaudio_error("Could not start pulseaudio mainloop.");
|
|
|
|
}
|
|
|
|
|
2017-10-29 01:50:51 -04:00
|
|
|
m_log.trace("pulseaudio: started mainloop");
|
|
|
|
|
2017-09-07 23:27:21 -04:00
|
|
|
pa_threaded_mainloop_wait(m_mainloop);
|
|
|
|
if (pa_context_get_state(m_context) != PA_CONTEXT_READY) {
|
2017-09-16 17:49:46 -04:00
|
|
|
pa_threaded_mainloop_unlock(m_mainloop);
|
|
|
|
pa_threaded_mainloop_stop(m_mainloop);
|
|
|
|
pa_context_disconnect(m_context);
|
|
|
|
pa_context_unref(m_context);
|
|
|
|
pa_threaded_mainloop_free(m_mainloop);
|
2017-09-07 23:27:21 -04:00
|
|
|
throw pulseaudio_error("Could not connect to pulseaudio server.");
|
|
|
|
}
|
|
|
|
|
2017-09-25 19:42:42 -04:00
|
|
|
pa_operation *op{nullptr};
|
|
|
|
if (!sink_name.empty()) {
|
|
|
|
op = pa_context_get_sink_info_by_name(m_context, sink_name.c_str(), sink_info_callback, this);
|
|
|
|
wait_loop(op, m_mainloop);
|
|
|
|
}
|
2017-10-29 01:50:51 -04:00
|
|
|
if (s_name.empty()) {
|
2017-09-07 23:27:21 -04:00
|
|
|
op = pa_context_get_server_info(m_context, get_default_sink_callback, this);
|
|
|
|
if (!op) {
|
|
|
|
throw pulseaudio_error("Failed to get pulseaudio server info.");
|
|
|
|
}
|
2017-09-16 00:10:25 -04:00
|
|
|
wait_loop(op, m_mainloop);
|
2017-09-21 00:59:38 -04:00
|
|
|
if (def_s_name.empty())
|
|
|
|
throw pulseaudio_error("Failed to get default sink.");
|
2017-09-07 23:27:21 -04:00
|
|
|
// get the sink index
|
2017-09-17 19:46:51 -04:00
|
|
|
op = pa_context_get_sink_info_by_name(m_context, def_s_name.c_str(), sink_info_callback, this);
|
2017-09-16 00:10:25 -04:00
|
|
|
wait_loop(op, m_mainloop);
|
2017-10-29 01:50:51 -04:00
|
|
|
m_log.warn("pulseaudio: using default sink %s", s_name);
|
|
|
|
} else {
|
|
|
|
m_log.trace("pulseaudio: using sink %s", s_name);
|
2017-09-07 23:27:21 -04:00
|
|
|
}
|
|
|
|
|
2017-09-16 17:49:46 -04:00
|
|
|
op = pa_context_subscribe(m_context, PA_SUBSCRIPTION_MASK_SINK, simple_callback, this);
|
2017-09-16 00:10:25 -04:00
|
|
|
wait_loop(op, m_mainloop);
|
2017-09-21 00:59:38 -04:00
|
|
|
if (!success)
|
|
|
|
throw pulseaudio_error("Failed to subscribe to server.");
|
2017-09-07 23:27:21 -04:00
|
|
|
pa_context_set_subscribe_callback(m_context, subscribe_callback, this);
|
|
|
|
|
2017-10-29 02:35:26 -04:00
|
|
|
update_volume(op);
|
|
|
|
|
2017-09-07 23:27:21 -04:00
|
|
|
pa_threaded_mainloop_unlock(m_mainloop);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Deconstruct pulseaudio
|
|
|
|
*/
|
|
|
|
pulseaudio::~pulseaudio() {
|
|
|
|
pa_threaded_mainloop_stop(m_mainloop);
|
|
|
|
pa_context_disconnect(m_context);
|
|
|
|
pa_context_unref(m_context);
|
|
|
|
pa_threaded_mainloop_free(m_mainloop);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get sink name
|
|
|
|
*/
|
|
|
|
const string& pulseaudio::get_name() {
|
2017-09-25 19:42:42 -04:00
|
|
|
return s_name;
|
2017-09-07 23:27:21 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2017-10-07 23:19:06 -04:00
|
|
|
* Wait for events
|
2017-09-07 23:27:21 -04:00
|
|
|
*/
|
2017-10-07 23:19:06 -04:00
|
|
|
bool pulseaudio::wait() {
|
2017-09-07 23:27:21 -04:00
|
|
|
return m_events.size() > 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Process queued pulseaudio events
|
|
|
|
*/
|
|
|
|
int pulseaudio::process_events() {
|
|
|
|
int ret = m_events.size();
|
|
|
|
pa_threaded_mainloop_lock(m_mainloop);
|
|
|
|
pa_operation *o{nullptr};
|
|
|
|
// clear the queue
|
|
|
|
while (!m_events.empty()) {
|
|
|
|
switch (m_events.front()) {
|
|
|
|
// try to get specified sink
|
|
|
|
case evtype::NEW:
|
2017-09-17 19:46:51 -04:00
|
|
|
// redundant if already using specified sink
|
2017-09-25 19:42:42 -04:00
|
|
|
if (!spec_s_name.empty()) {
|
|
|
|
o = pa_context_get_sink_info_by_name(m_context, spec_s_name.c_str(), sink_info_callback, this);
|
|
|
|
wait_loop(o, m_mainloop);
|
|
|
|
}
|
2017-09-07 23:27:21 -04:00
|
|
|
break;
|
2017-10-07 23:19:06 -04:00
|
|
|
// get default sink
|
2017-09-07 23:27:21 -04:00
|
|
|
case evtype::REMOVE:
|
|
|
|
o = pa_context_get_server_info(m_context, get_default_sink_callback, this);
|
2017-09-16 00:10:25 -04:00
|
|
|
wait_loop(o, m_mainloop);
|
2017-09-21 00:59:38 -04:00
|
|
|
if (def_s_name.empty())
|
|
|
|
throw pulseaudio_error("Failed to get default sink.");
|
2017-09-17 19:46:51 -04:00
|
|
|
o = pa_context_get_sink_info_by_name(m_context, def_s_name.c_str(), sink_info_callback, this);
|
2017-09-16 00:10:25 -04:00
|
|
|
wait_loop(o, m_mainloop);
|
2017-12-16 01:46:46 -05:00
|
|
|
if (spec_s_name != s_name)
|
|
|
|
m_log.warn("pulseaudio: using default sink %s", s_name);
|
2017-09-07 23:27:21 -04:00
|
|
|
break;
|
2017-10-29 02:35:26 -04:00
|
|
|
default:
|
|
|
|
break;
|
2017-09-07 23:27:21 -04:00
|
|
|
}
|
2017-10-29 02:35:26 -04:00
|
|
|
update_volume(o);
|
2017-09-07 23:27:21 -04:00
|
|
|
m_events.pop();
|
|
|
|
}
|
|
|
|
pa_threaded_mainloop_unlock(m_mainloop);
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get volume in percentage
|
|
|
|
*/
|
|
|
|
int pulseaudio::get_volume() {
|
|
|
|
// alternatively, user pa_cvolume_avg_mask() to average selected channels
|
2017-09-17 19:46:51 -04:00
|
|
|
return static_cast<int>(pa_cvolume_max(&cv) * 100.0f / PA_VOLUME_NORM + 0.5f);
|
2017-09-07 23:27:21 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set volume to given percentage
|
|
|
|
*/
|
|
|
|
void pulseaudio::set_volume(float percentage) {
|
|
|
|
pa_threaded_mainloop_lock(m_mainloop);
|
|
|
|
pa_volume_t vol = math_util::percentage_to_value<pa_volume_t>(percentage, PA_VOLUME_MUTED, PA_VOLUME_NORM);
|
|
|
|
pa_cvolume_scale(&cv, vol);
|
2017-10-29 02:35:26 -04:00
|
|
|
pa_operation *op = pa_context_set_sink_volume_by_index(m_context, m_index, &cv, simple_callback, this);
|
2017-09-16 00:10:25 -04:00
|
|
|
wait_loop(op, m_mainloop);
|
2017-09-21 00:59:38 -04:00
|
|
|
if (!success)
|
|
|
|
throw pulseaudio_error("Failed to set sink volume.");
|
2017-09-07 23:27:21 -04:00
|
|
|
pa_threaded_mainloop_unlock(m_mainloop);
|
|
|
|
}
|
|
|
|
|
2017-09-16 17:49:46 -04:00
|
|
|
/**
|
|
|
|
* Increment or decrement volume by given percentage (prevents accumulation of rounding errors from get_volume)
|
|
|
|
*/
|
|
|
|
void pulseaudio::inc_volume(int delta_perc) {
|
|
|
|
pa_threaded_mainloop_lock(m_mainloop);
|
2017-09-17 19:46:51 -04:00
|
|
|
pa_volume_t vol = math_util::percentage_to_value<pa_volume_t>(abs(delta_perc), PA_VOLUME_NORM);
|
2017-10-16 01:25:19 -04:00
|
|
|
if (delta_perc > 0) {
|
|
|
|
if (pa_cvolume_max(&cv) + vol <= PA_VOLUME_UI_MAX) {
|
|
|
|
pa_cvolume_inc(&cv, vol);
|
2017-10-29 01:50:51 -04:00
|
|
|
} else {
|
|
|
|
m_log.warn("pulseaudio: maximum volume reached");
|
2017-10-16 01:25:19 -04:00
|
|
|
}
|
|
|
|
} else
|
2017-09-16 17:49:46 -04:00
|
|
|
pa_cvolume_dec(&cv, vol);
|
2017-10-29 02:35:26 -04:00
|
|
|
pa_operation *op = pa_context_set_sink_volume_by_index(m_context, m_index, &cv, simple_callback, this);
|
2017-09-16 17:49:46 -04:00
|
|
|
wait_loop(op, m_mainloop);
|
2017-09-21 00:59:38 -04:00
|
|
|
if (!success)
|
|
|
|
throw pulseaudio_error("Failed to set sink volume.");
|
2017-09-16 17:49:46 -04:00
|
|
|
pa_threaded_mainloop_unlock(m_mainloop);
|
|
|
|
}
|
|
|
|
|
2017-09-07 23:27:21 -04:00
|
|
|
/**
|
|
|
|
* Set mute state
|
|
|
|
*/
|
|
|
|
void pulseaudio::set_mute(bool mode) {
|
|
|
|
pa_threaded_mainloop_lock(m_mainloop);
|
2017-09-17 19:46:51 -04:00
|
|
|
pa_operation *op = pa_context_set_sink_mute_by_index(m_context, m_index, mode, simple_callback, this);
|
2017-09-16 00:10:25 -04:00
|
|
|
wait_loop(op, m_mainloop);
|
2017-09-21 00:59:38 -04:00
|
|
|
if (!success)
|
|
|
|
throw pulseaudio_error("Failed to mute sink.");
|
2017-09-07 23:27:21 -04:00
|
|
|
pa_threaded_mainloop_unlock(m_mainloop);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Toggle mute state
|
|
|
|
*/
|
|
|
|
void pulseaudio::toggle_mute() {
|
|
|
|
set_mute(!is_muted());
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get current mute state
|
|
|
|
*/
|
|
|
|
bool pulseaudio::is_muted() {
|
|
|
|
return muted;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2017-10-29 02:35:26 -04:00
|
|
|
* Update local volume cache
|
2017-09-07 23:27:21 -04:00
|
|
|
*/
|
2017-10-29 02:35:26 -04:00
|
|
|
void pulseaudio::update_volume(pa_operation *o) {
|
|
|
|
o = pa_context_get_sink_info_by_index(m_context, m_index, get_sink_volume_callback, this);
|
|
|
|
wait_loop(o, m_mainloop);
|
2017-09-07 23:27:21 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Callback when getting volume
|
|
|
|
*/
|
2017-09-21 00:59:38 -04:00
|
|
|
void pulseaudio::get_sink_volume_callback(pa_context *, const pa_sink_info *info, int, void *userdata) {
|
2017-09-07 23:27:21 -04:00
|
|
|
pulseaudio* This = static_cast<pulseaudio *>(userdata);
|
2017-10-29 02:35:26 -04:00
|
|
|
if (info) {
|
2017-09-07 23:27:21 -04:00
|
|
|
This->cv = info->volume;
|
2017-10-29 02:35:26 -04:00
|
|
|
This->muted = info->mute;
|
|
|
|
}
|
2017-09-07 23:27:21 -04:00
|
|
|
pa_threaded_mainloop_signal(This->m_mainloop, 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Callback when subscribing to changes
|
|
|
|
*/
|
2017-09-21 00:59:38 -04:00
|
|
|
void pulseaudio::subscribe_callback(pa_context *, pa_subscription_event_type_t t, uint32_t idx, void* userdata) {
|
2017-09-07 23:27:21 -04:00
|
|
|
pulseaudio *This = static_cast<pulseaudio *>(userdata);
|
2017-09-17 19:46:51 -04:00
|
|
|
if (idx == PA_INVALID_INDEX)
|
2017-09-21 00:59:38 -04:00
|
|
|
return;
|
2017-09-07 23:27:21 -04:00
|
|
|
switch(t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) {
|
|
|
|
case PA_SUBSCRIPTION_EVENT_SINK:
|
|
|
|
switch(t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) {
|
|
|
|
case PA_SUBSCRIPTION_EVENT_NEW:
|
|
|
|
This->m_events.emplace(evtype::NEW);
|
2017-10-07 23:19:06 -04:00
|
|
|
break;
|
2017-09-07 23:27:21 -04:00
|
|
|
case PA_SUBSCRIPTION_EVENT_CHANGE:
|
2017-09-21 00:59:38 -04:00
|
|
|
if (idx == This->m_index)
|
2017-09-07 23:27:21 -04:00
|
|
|
This->m_events.emplace(evtype::CHANGE);
|
|
|
|
break;
|
|
|
|
case PA_SUBSCRIPTION_EVENT_REMOVE:
|
2017-09-21 00:59:38 -04:00
|
|
|
if (idx == This->m_index)
|
2017-09-07 23:27:21 -04:00
|
|
|
This->m_events.emplace(evtype::REMOVE);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
pa_threaded_mainloop_signal(This->m_mainloop, 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Simple callback to check for success
|
|
|
|
*/
|
2017-09-21 00:59:38 -04:00
|
|
|
void pulseaudio::simple_callback(pa_context *, int success, void *userdata) {
|
2017-09-07 23:27:21 -04:00
|
|
|
pulseaudio *This = static_cast<pulseaudio *>(userdata);
|
2017-09-21 00:59:38 -04:00
|
|
|
This->success = success;
|
2017-09-07 23:27:21 -04:00
|
|
|
pa_threaded_mainloop_signal(This->m_mainloop, 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Callback when getting default sink name
|
|
|
|
*/
|
2017-09-21 00:59:38 -04:00
|
|
|
void pulseaudio::get_default_sink_callback(pa_context *, const pa_server_info *info, void *userdata) {
|
2017-09-07 23:27:21 -04:00
|
|
|
pulseaudio *This = static_cast<pulseaudio *>(userdata);
|
2017-09-25 19:42:42 -04:00
|
|
|
if (info->default_sink_name) {
|
2017-10-07 23:19:06 -04:00
|
|
|
This->def_s_name = info->default_sink_name;
|
2017-09-25 19:42:42 -04:00
|
|
|
This->s_name = info->default_sink_name;
|
|
|
|
} else
|
2017-09-21 00:59:38 -04:00
|
|
|
This->def_s_name = ""s;
|
2017-09-07 23:27:21 -04:00
|
|
|
pa_threaded_mainloop_signal(This->m_mainloop, 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Callback when getting sink info & existence
|
|
|
|
*/
|
2017-09-21 00:59:38 -04:00
|
|
|
void pulseaudio::sink_info_callback(pa_context *, const pa_sink_info *info, int eol, void *userdata) {
|
2017-09-07 23:27:21 -04:00
|
|
|
pulseaudio *This = static_cast<pulseaudio *>(userdata);
|
2017-10-29 01:50:51 -04:00
|
|
|
if (!eol && info) {
|
2017-09-17 19:46:51 -04:00
|
|
|
This->m_index = info->index;
|
2017-09-25 19:42:42 -04:00
|
|
|
This->s_name = info->name;
|
2017-09-07 23:27:21 -04:00
|
|
|
}
|
|
|
|
pa_threaded_mainloop_signal(This->m_mainloop, 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Callback when context state changes
|
|
|
|
*/
|
|
|
|
void pulseaudio::context_state_callback(pa_context *context, void *userdata) {
|
|
|
|
pulseaudio* This = static_cast<pulseaudio *>(userdata);
|
|
|
|
switch (pa_context_get_state(context)) {
|
|
|
|
case PA_CONTEXT_READY:
|
|
|
|
case PA_CONTEXT_TERMINATED:
|
|
|
|
case PA_CONTEXT_FAILED:
|
|
|
|
pa_threaded_mainloop_signal(This->m_mainloop, 0);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case PA_CONTEXT_UNCONNECTED:
|
|
|
|
case PA_CONTEXT_CONNECTING:
|
|
|
|
case PA_CONTEXT_AUTHORIZING:
|
|
|
|
case PA_CONTEXT_SETTING_NAME:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-16 00:10:25 -04:00
|
|
|
inline void pulseaudio::wait_loop(pa_operation *op, pa_threaded_mainloop *loop) {
|
|
|
|
while (pa_operation_get_state(op) != PA_OPERATION_DONE)
|
|
|
|
pa_threaded_mainloop_wait(loop);
|
|
|
|
pa_operation_unref(op);
|
|
|
|
}
|
2017-09-17 19:46:51 -04:00
|
|
|
|
2017-09-07 23:27:21 -04:00
|
|
|
POLYBAR_NS_END
|