1 // vim:ts=4:sw=4:expandtab
6 #include <pulse/pulseaudio.h>
10 #define APP_NAME "i3status"
11 #define APP_ID "org.i3wm"
13 typedef struct index_info_s {
17 char description[MAX_SINK_DESCRIPTION_LEN];
18 TAILQ_ENTRY(index_info_s)
22 static pa_threaded_mainloop *main_loop = NULL;
23 static pa_context *context = NULL;
24 static pa_mainloop_api *api = NULL;
25 static bool context_ready = false;
26 static bool mainloop_thread_running = false;
27 static uint32_t default_sink_idx = DEFAULT_SINK_INDEX;
28 TAILQ_HEAD(tailhead, index_info_s)
30 TAILQ_HEAD_INITIALIZER(cached_info);
31 static pthread_mutex_t pulse_mutex = PTHREAD_MUTEX_INITIALIZER;
33 static void pulseaudio_error_log(pa_context *c) {
35 "i3status: PulseAudio: %s\n",
36 pa_strerror(pa_context_errno(c)));
39 static bool pulseaudio_free_operation(pa_context *c, pa_operation *o) {
41 pa_operation_unref(o);
43 pulseaudio_error_log(c);
44 /* return false if the operation failed */
49 * save the info for the specified sink index
50 * returning true if the value was changed
52 static bool save_info(uint32_t sink_idx, int new_volume, const char *new_description, const char *name) {
53 pthread_mutex_lock(&pulse_mutex);
56 /* if this is NULL, gracefully handle and replace with empty-string */
57 if (!new_description) {
59 fprintf(stderr, "i3status: PulseAudio: NULL new_description provided\n");
62 TAILQ_FOREACH(entry, &cached_info, entries) {
64 if (!entry->name || strcmp(entry->name, name)) {
68 if (entry->idx != sink_idx) {
75 if (new_volume != entry->volume) {
76 entry->volume = new_volume;
80 if (strncmp(entry->description, new_description, sizeof(entry->description))) {
81 strncpy(entry->description, new_description, sizeof(entry->description) - 1);
82 entry->description[sizeof(entry->description) - 1] = '\0';
86 pthread_mutex_unlock(&pulse_mutex);
89 /* index not found, store it */
90 entry = malloc(sizeof(*entry));
91 TAILQ_INSERT_HEAD(&cached_info, entry, entries);
92 entry->idx = sink_idx;
93 entry->volume = new_volume;
94 strncpy(entry->description, new_description, sizeof(entry->description) - 1);
95 entry->description[sizeof(entry->description) - 1] = '\0';
97 entry->name = malloc(strlen(name) + 1);
98 strcpy(entry->name, name);
102 pthread_mutex_unlock(&pulse_mutex);
106 static void store_info_from_sink_cb(pa_context *c,
107 const pa_sink_info *info,
111 if (pa_context_errno(c) == PA_ERR_NOENTITY)
114 pulseaudio_error_log(c);
121 int avg_vol = pa_cvolume_avg(&info->volume);
122 int vol_perc = roundf((float)avg_vol * 100 / PA_VOLUME_NORM);
123 int composed_volume = COMPOSE_VOLUME_MUTE(vol_perc, info->mute);
125 /* if this is the default sink we must try to save it twice: once with
126 * DEFAULT_SINK_INDEX as the index, and another with its proper value
127 * (using bitwise OR to avoid early-out logic) */
128 if ((info->index == default_sink_idx &&
129 save_info(DEFAULT_SINK_INDEX, composed_volume, info->description, NULL)) |
130 save_info(info->index, composed_volume, info->description, info->name)) {
131 /* if the volume, mute flag or description changed, wake the main thread */
132 pthread_kill(main_thread, SIGUSR1);
136 static void get_sink_info(pa_context *c, uint32_t idx, const char *name) {
139 if (name || idx == DEFAULT_SINK_INDEX) {
140 o = pa_context_get_sink_info_by_name(
141 c, name ? name : "@DEFAULT_SINK@", store_info_from_sink_cb, NULL);
143 o = pa_context_get_sink_info_by_index(
144 c, idx, store_info_from_sink_cb, NULL);
147 pulseaudio_free_operation(c, o);
151 static void store_default_sink_cb(pa_context *c,
152 const pa_sink_info *i,
156 if (default_sink_idx != i->index) {
157 /* default sink changed? */
158 default_sink_idx = i->index;
159 store_info_from_sink_cb(c, i, eol, userdata);
164 static void update_default_sink(pa_context *c) {
165 pa_operation *o = pa_context_get_sink_info_by_name(
168 store_default_sink_cb,
170 pulseaudio_free_operation(c, o);
173 static void subscribe_cb(pa_context *c, pa_subscription_event_type_t t,
174 uint32_t idx, void *userdata) {
175 if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) != PA_SUBSCRIPTION_EVENT_CHANGE)
177 pa_subscription_event_type_t facility =
178 t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK;
180 case PA_SUBSCRIPTION_EVENT_SERVER:
181 /* server change event, see if the default sink changed */
182 update_default_sink(c);
184 case PA_SUBSCRIPTION_EVENT_SINK:
185 get_sink_info(c, idx, NULL);
192 static void context_state_callback(pa_context *c, void *userdata) {
193 switch (pa_context_get_state(c)) {
194 case PA_CONTEXT_UNCONNECTED:
195 case PA_CONTEXT_CONNECTING:
196 case PA_CONTEXT_AUTHORIZING:
197 case PA_CONTEXT_SETTING_NAME:
198 case PA_CONTEXT_TERMINATED:
200 context_ready = false;
203 case PA_CONTEXT_READY: {
204 pa_context_set_subscribe_callback(c, subscribe_cb, NULL);
205 update_default_sink(c);
207 pa_operation *o = pa_context_subscribe(
209 PA_SUBSCRIPTION_MASK_SINK | PA_SUBSCRIPTION_MASK_SERVER,
212 if (!pulseaudio_free_operation(c, o))
214 context_ready = true;
217 case PA_CONTEXT_FAILED:
218 /* server disconnected us, attempt to reconnect */
219 context_ready = false;
220 pa_context_unref(context);
227 * returns the current volume in percent, which, as per PulseAudio,
230 int volume_pulseaudio(uint32_t sink_idx, const char *sink_name) {
231 if (!context_ready || default_sink_idx == DEFAULT_SINK_INDEX)
234 pthread_mutex_lock(&pulse_mutex);
235 const index_info_t *entry;
236 TAILQ_FOREACH(entry, &cached_info, entries) {
238 if (!entry->name || strcmp(entry->name, sink_name)) {
242 if (entry->idx != sink_idx) {
246 int vol = entry->volume;
247 pthread_mutex_unlock(&pulse_mutex);
250 pthread_mutex_unlock(&pulse_mutex);
251 /* first time requires a prime callback call because we only get updates
252 * when the description or volume actually changes, but we need it to be
253 * correct even if it never changes */
254 pa_threaded_mainloop_lock(main_loop);
255 get_sink_info(context, sink_idx, sink_name);
256 pa_threaded_mainloop_unlock(main_loop);
257 /* show 0 while we don't have this information */
261 bool description_pulseaudio(uint32_t sink_idx, const char *sink_name, char buffer[MAX_SINK_DESCRIPTION_LEN]) {
262 if (!context_ready || default_sink_idx == DEFAULT_SINK_INDEX) {
266 pthread_mutex_lock(&pulse_mutex);
267 const index_info_t *entry;
268 TAILQ_FOREACH(entry, &cached_info, entries) {
270 if (!entry->name || strcmp(entry->name, sink_name)) {
274 if (entry->idx != sink_idx) {
278 strncpy(buffer, entry->description, sizeof(entry->description) - 1);
279 pthread_mutex_unlock(&pulse_mutex);
280 buffer[sizeof(entry->description) - 1] = '\0';
283 pthread_mutex_unlock(&pulse_mutex);
284 /* first time requires a prime callback call because we only get updates
285 * when the description or volume actually changes, but we need it to be
286 * correct even if it never changes */
287 pa_threaded_mainloop_lock(main_loop);
288 get_sink_info(context, sink_idx, sink_name);
289 pa_threaded_mainloop_unlock(main_loop);
290 /* show empty string while we don't have this information */
296 * detect and, if necessary, initialize the PulseAudio API
298 bool pulse_initialize(void) {
300 main_loop = pa_threaded_mainloop_new();
305 api = pa_threaded_mainloop_get_api(main_loop);
310 pa_proplist *proplist = pa_proplist_new();
311 pa_proplist_sets(proplist, PA_PROP_APPLICATION_NAME, APP_NAME);
312 pa_proplist_sets(proplist, PA_PROP_APPLICATION_ID, APP_ID);
313 pa_proplist_sets(proplist, PA_PROP_APPLICATION_VERSION, VERSION);
314 context = pa_context_new_with_proplist(api, APP_NAME, proplist);
315 pa_proplist_free(proplist);
318 pa_context_set_state_callback(context,
319 context_state_callback,
321 if (pa_context_connect(context,
323 PA_CONTEXT_NOFAIL | PA_CONTEXT_NOAUTOSPAWN,
325 pulseaudio_error_log(context);
328 if (!mainloop_thread_running &&
329 pa_threaded_mainloop_start(main_loop) < 0) {
330 pulseaudio_error_log(context);
331 pa_threaded_mainloop_free(main_loop);
335 mainloop_thread_running = true;