]> git.sur5r.net Git - i3/i3status/blob - src/pulse.c
fix: use SYSCONFDIR in error message
[i3/i3status] / src / pulse.c
1 // vim:ts=4:sw=4:expandtab
2 #include <config.h>
3 #include <string.h>
4 #include <stdio.h>
5 #include <math.h>
6 #include <signal.h>
7 #include <pulse/pulseaudio.h>
8 #include "i3status.h"
9 #include "queue.h"
10
11 #define APP_NAME "i3status"
12 #define APP_ID "org.i3wm"
13
14 typedef struct index_info_s {
15     char *name;
16     uint32_t idx;
17     int volume;
18     char description[MAX_SINK_DESCRIPTION_LEN];
19     TAILQ_ENTRY(index_info_s)
20     entries;
21 } index_info_t;
22
23 static pa_threaded_mainloop *main_loop = NULL;
24 static pa_context *context = NULL;
25 static pa_mainloop_api *api = NULL;
26 static bool context_ready = false;
27 static bool mainloop_thread_running = false;
28 static uint32_t default_sink_idx = DEFAULT_SINK_INDEX;
29 TAILQ_HEAD(tailhead, index_info_s)
30 cached_info =
31     TAILQ_HEAD_INITIALIZER(cached_info);
32 static pthread_mutex_t pulse_mutex = PTHREAD_MUTEX_INITIALIZER;
33
34 static void pulseaudio_error_log(pa_context *c) {
35     fprintf(stderr,
36             "i3status: PulseAudio: %s\n",
37             pa_strerror(pa_context_errno(c)));
38 }
39
40 static bool pulseaudio_free_operation(pa_context *c, pa_operation *o) {
41     if (o)
42         pa_operation_unref(o);
43     else
44         pulseaudio_error_log(c);
45     /* return false if the operation failed */
46     return o;
47 }
48
49 /*
50  * save the info for the specified sink index
51  * returning true if the value was changed
52  */
53 static bool save_info(uint32_t sink_idx, int new_volume, const char *new_description, const char *name) {
54     pthread_mutex_lock(&pulse_mutex);
55     index_info_t *entry;
56
57     /* if this is NULL, gracefully handle and replace with empty-string */
58     if (!new_description) {
59         new_description = "";
60         fprintf(stderr, "i3status: PulseAudio: NULL new_description provided\n");
61     }
62
63     TAILQ_FOREACH(entry, &cached_info, entries) {
64         if (name) {
65             if (!entry->name || strcmp(entry->name, name)) {
66                 continue;
67             }
68         } else {
69             if (entry->idx != sink_idx) {
70                 continue;
71             }
72         }
73
74         bool changed = false;
75
76         if (new_volume != entry->volume) {
77             entry->volume = new_volume;
78             changed = true;
79         }
80
81         if (strncmp(entry->description, new_description, sizeof(entry->description))) {
82             strncpy(entry->description, new_description, sizeof(entry->description) - 1);
83             entry->description[sizeof(entry->description) - 1] = '\0';
84             changed = true;
85         }
86
87         pthread_mutex_unlock(&pulse_mutex);
88         return changed;
89     }
90     /* index not found, store it */
91     entry = malloc(sizeof(*entry));
92     TAILQ_INSERT_HEAD(&cached_info, entry, entries);
93     entry->idx = sink_idx;
94     entry->volume = new_volume;
95     strncpy(entry->description, new_description, sizeof(entry->description) - 1);
96     entry->description[sizeof(entry->description) - 1] = '\0';
97     if (name) {
98         entry->name = malloc(strlen(name) + 1);
99         strcpy(entry->name, name);
100     } else {
101         entry->name = NULL;
102     }
103     pthread_mutex_unlock(&pulse_mutex);
104     return true;
105 }
106
107 static void store_info_from_sink_cb(pa_context *c,
108                                     const pa_sink_info *info,
109                                     int eol,
110                                     void *userdata) {
111     if (eol < 0) {
112         if (pa_context_errno(c) == PA_ERR_NOENTITY)
113             return;
114
115         pulseaudio_error_log(c);
116         return;
117     }
118
119     if (eol > 0)
120         return;
121
122     int avg_vol = pa_cvolume_avg(&info->volume);
123     int vol_perc = roundf((float)avg_vol * 100 / PA_VOLUME_NORM);
124     int composed_volume = COMPOSE_VOLUME_MUTE(vol_perc, info->mute);
125
126     /* if this is the default sink we must try to save it twice: once with
127      * DEFAULT_SINK_INDEX as the index, and another with its proper value
128      * (using bitwise OR to avoid early-out logic) */
129     if ((info->index == default_sink_idx &&
130          save_info(DEFAULT_SINK_INDEX, composed_volume, info->description, NULL)) |
131         save_info(info->index, composed_volume, info->description, info->name)) {
132         /* if the volume, mute flag or description changed, wake the main thread */
133         pthread_kill(main_thread, SIGUSR1);
134     }
135 }
136
137 static void get_sink_info(pa_context *c, uint32_t idx, const char *name) {
138     pa_operation *o;
139
140     if (name || idx == DEFAULT_SINK_INDEX) {
141         o = pa_context_get_sink_info_by_name(
142             c, name ? name : "@DEFAULT_SINK@", store_info_from_sink_cb, NULL);
143     } else {
144         o = pa_context_get_sink_info_by_index(
145             c, idx, store_info_from_sink_cb, NULL);
146     }
147     if (o) {
148         pulseaudio_free_operation(c, o);
149     }
150 }
151
152 static void store_default_sink_cb(pa_context *c,
153                                   const pa_sink_info *i,
154                                   int eol,
155                                   void *userdata) {
156     if (i) {
157         if (default_sink_idx != i->index) {
158             /* default sink changed? */
159             default_sink_idx = i->index;
160             store_info_from_sink_cb(c, i, eol, userdata);
161         }
162     }
163 }
164
165 static void update_default_sink(pa_context *c) {
166     pa_operation *o = pa_context_get_sink_info_by_name(
167         c,
168         "@DEFAULT_SINK@",
169         store_default_sink_cb,
170         NULL);
171     pulseaudio_free_operation(c, o);
172 }
173
174 static void subscribe_cb(pa_context *c, pa_subscription_event_type_t t,
175                          uint32_t idx, void *userdata) {
176     if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) != PA_SUBSCRIPTION_EVENT_CHANGE)
177         return;
178     pa_subscription_event_type_t facility =
179         t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK;
180     switch (facility) {
181         case PA_SUBSCRIPTION_EVENT_SERVER:
182             /* server change event, see if the default sink changed */
183             update_default_sink(c);
184             break;
185         case PA_SUBSCRIPTION_EVENT_SINK:
186             get_sink_info(c, idx, NULL);
187             break;
188         default:
189             break;
190     }
191 }
192
193 static void context_state_callback(pa_context *c, void *userdata) {
194     switch (pa_context_get_state(c)) {
195         case PA_CONTEXT_UNCONNECTED:
196         case PA_CONTEXT_CONNECTING:
197         case PA_CONTEXT_AUTHORIZING:
198         case PA_CONTEXT_SETTING_NAME:
199         case PA_CONTEXT_TERMINATED:
200         default:
201             context_ready = false;
202             break;
203
204         case PA_CONTEXT_READY: {
205             pa_context_set_subscribe_callback(c, subscribe_cb, NULL);
206             update_default_sink(c);
207
208             pa_operation *o = pa_context_subscribe(
209                 c,
210                 PA_SUBSCRIPTION_MASK_SINK | PA_SUBSCRIPTION_MASK_SERVER,
211                 NULL,
212                 NULL);
213             if (!pulseaudio_free_operation(c, o))
214                 break;
215             context_ready = true;
216         } break;
217
218         case PA_CONTEXT_FAILED:
219             /* server disconnected us, attempt to reconnect */
220             context_ready = false;
221             pa_context_unref(context);
222             context = NULL;
223             break;
224     }
225 }
226
227 /*
228  * returns the current volume in percent, which, as per PulseAudio,
229  * may be > 100%
230  */
231 int volume_pulseaudio(uint32_t sink_idx, const char *sink_name) {
232     if (!context_ready || default_sink_idx == DEFAULT_SINK_INDEX)
233         return -1;
234
235     pthread_mutex_lock(&pulse_mutex);
236     const index_info_t *entry;
237     TAILQ_FOREACH(entry, &cached_info, entries) {
238         if (sink_name) {
239             if (!entry->name || strcmp(entry->name, sink_name)) {
240                 continue;
241             }
242         } else {
243             if (entry->idx != sink_idx) {
244                 continue;
245             }
246         }
247         int vol = entry->volume;
248         pthread_mutex_unlock(&pulse_mutex);
249         return vol;
250     }
251     pthread_mutex_unlock(&pulse_mutex);
252     /* first time requires a prime callback call because we only get updates
253      * when the description or volume actually changes, but we need it to be
254      * correct even if it never changes */
255     pa_threaded_mainloop_lock(main_loop);
256     get_sink_info(context, sink_idx, sink_name);
257     pa_threaded_mainloop_unlock(main_loop);
258     /* show 0 while we don't have this information */
259     return 0;
260 }
261
262 bool description_pulseaudio(uint32_t sink_idx, const char *sink_name, char buffer[MAX_SINK_DESCRIPTION_LEN]) {
263     if (!context_ready || default_sink_idx == DEFAULT_SINK_INDEX) {
264         return false;
265     }
266
267     pthread_mutex_lock(&pulse_mutex);
268     const index_info_t *entry;
269     TAILQ_FOREACH(entry, &cached_info, entries) {
270         if (sink_name) {
271             if (!entry->name || strcmp(entry->name, sink_name)) {
272                 continue;
273             }
274         } else {
275             if (entry->idx != sink_idx) {
276                 continue;
277             }
278         }
279         strncpy(buffer, entry->description, sizeof(entry->description) - 1);
280         pthread_mutex_unlock(&pulse_mutex);
281         buffer[sizeof(entry->description) - 1] = '\0';
282         return true;
283     }
284     pthread_mutex_unlock(&pulse_mutex);
285     /* first time requires a prime callback call because we only get updates
286      * when the description or volume actually changes, but we need it to be
287      * correct even if it never changes */
288     pa_threaded_mainloop_lock(main_loop);
289     get_sink_info(context, sink_idx, sink_name);
290     pa_threaded_mainloop_unlock(main_loop);
291     /* show empty string while we don't have this information */
292     buffer[0] = '\0';
293     return true;
294 }
295
296 /*
297  *  detect and, if necessary, initialize the PulseAudio API
298  */
299 bool pulse_initialize(void) {
300     if (!main_loop) {
301         main_loop = pa_threaded_mainloop_new();
302         if (!main_loop)
303             return false;
304     }
305     if (!api) {
306         api = pa_threaded_mainloop_get_api(main_loop);
307         if (!api)
308             return false;
309     }
310     if (!context) {
311         pa_proplist *proplist = pa_proplist_new();
312         pa_proplist_sets(proplist, PA_PROP_APPLICATION_NAME, APP_NAME);
313         pa_proplist_sets(proplist, PA_PROP_APPLICATION_ID, APP_ID);
314         pa_proplist_sets(proplist, PA_PROP_APPLICATION_VERSION, I3STATUS_VERSION);
315         context = pa_context_new_with_proplist(api, APP_NAME, proplist);
316         pa_proplist_free(proplist);
317         if (!context)
318             return false;
319         pa_context_set_state_callback(context,
320                                       context_state_callback,
321                                       NULL);
322         if (pa_context_connect(context,
323                                NULL,
324                                PA_CONTEXT_NOFAIL | PA_CONTEXT_NOAUTOSPAWN,
325                                NULL) < 0) {
326             pulseaudio_error_log(context);
327             return false;
328         }
329         if (!mainloop_thread_running &&
330             pa_threaded_mainloop_start(main_loop) < 0) {
331             pulseaudio_error_log(context);
332             pa_threaded_mainloop_free(main_loop);
333             main_loop = NULL;
334             return false;
335         }
336         mainloop_thread_running = true;
337     }
338     return true;
339 }