1 // vim:ts=4:sw=4:expandtab
7 #include <pulse/pulseaudio.h>
11 #define APP_NAME "i3status"
12 #define APP_ID "org.i3wm"
14 typedef struct index_info_s {
18 char description[MAX_SINK_DESCRIPTION_LEN];
19 TAILQ_ENTRY(index_info_s)
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)
31 TAILQ_HEAD_INITIALIZER(cached_info);
32 static pthread_mutex_t pulse_mutex = PTHREAD_MUTEX_INITIALIZER;
34 static void pulseaudio_error_log(pa_context *c) {
36 "i3status: PulseAudio: %s\n",
37 pa_strerror(pa_context_errno(c)));
40 static bool pulseaudio_free_operation(pa_context *c, pa_operation *o) {
42 pa_operation_unref(o);
44 pulseaudio_error_log(c);
45 /* return false if the operation failed */
50 * save the info for the specified sink index
51 * returning true if the value was changed
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);
57 /* if this is NULL, gracefully handle and replace with empty-string */
58 if (!new_description) {
60 fprintf(stderr, "i3status: PulseAudio: NULL new_description provided\n");
63 TAILQ_FOREACH(entry, &cached_info, entries) {
65 if (!entry->name || strcmp(entry->name, name)) {
69 if (entry->idx != sink_idx) {
76 if (new_volume != entry->volume) {
77 entry->volume = new_volume;
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';
87 pthread_mutex_unlock(&pulse_mutex);
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';
98 entry->name = malloc(strlen(name) + 1);
99 strcpy(entry->name, name);
103 pthread_mutex_unlock(&pulse_mutex);
107 static void store_info_from_sink_cb(pa_context *c,
108 const pa_sink_info *info,
112 if (pa_context_errno(c) == PA_ERR_NOENTITY)
115 pulseaudio_error_log(c);
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);
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);
137 static void get_sink_info(pa_context *c, uint32_t idx, const char *name) {
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);
144 o = pa_context_get_sink_info_by_index(
145 c, idx, store_info_from_sink_cb, NULL);
148 pulseaudio_free_operation(c, o);
152 static void store_default_sink_cb(pa_context *c,
153 const pa_sink_info *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);
165 static void update_default_sink(pa_context *c) {
166 pa_operation *o = pa_context_get_sink_info_by_name(
169 store_default_sink_cb,
171 pulseaudio_free_operation(c, o);
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)
178 pa_subscription_event_type_t facility =
179 t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK;
181 case PA_SUBSCRIPTION_EVENT_SERVER:
182 /* server change event, see if the default sink changed */
183 update_default_sink(c);
185 case PA_SUBSCRIPTION_EVENT_SINK:
186 get_sink_info(c, idx, NULL);
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:
201 context_ready = false;
204 case PA_CONTEXT_READY: {
205 pa_context_set_subscribe_callback(c, subscribe_cb, NULL);
206 update_default_sink(c);
208 pa_operation *o = pa_context_subscribe(
210 PA_SUBSCRIPTION_MASK_SINK | PA_SUBSCRIPTION_MASK_SERVER,
213 if (!pulseaudio_free_operation(c, o))
215 context_ready = true;
218 case PA_CONTEXT_FAILED:
219 /* server disconnected us, attempt to reconnect */
220 context_ready = false;
221 pa_context_unref(context);
228 * returns the current volume in percent, which, as per PulseAudio,
231 int volume_pulseaudio(uint32_t sink_idx, const char *sink_name) {
232 if (!context_ready || default_sink_idx == DEFAULT_SINK_INDEX)
235 pthread_mutex_lock(&pulse_mutex);
236 const index_info_t *entry;
237 TAILQ_FOREACH(entry, &cached_info, entries) {
239 if (!entry->name || strcmp(entry->name, sink_name)) {
243 if (entry->idx != sink_idx) {
247 int vol = entry->volume;
248 pthread_mutex_unlock(&pulse_mutex);
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 */
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) {
267 pthread_mutex_lock(&pulse_mutex);
268 const index_info_t *entry;
269 TAILQ_FOREACH(entry, &cached_info, entries) {
271 if (!entry->name || strcmp(entry->name, sink_name)) {
275 if (entry->idx != sink_idx) {
279 strncpy(buffer, entry->description, sizeof(entry->description) - 1);
280 pthread_mutex_unlock(&pulse_mutex);
281 buffer[sizeof(entry->description) - 1] = '\0';
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 */
297 * detect and, if necessary, initialize the PulseAudio API
299 bool pulse_initialize(void) {
301 main_loop = pa_threaded_mainloop_new();
306 api = pa_threaded_mainloop_get_api(main_loop);
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);
319 pa_context_set_state_callback(context,
320 context_state_callback,
322 if (pa_context_connect(context,
324 PA_CONTEXT_NOFAIL | PA_CONTEXT_NOAUTOSPAWN,
326 pulseaudio_error_log(context);
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);
336 mainloop_thread_running = true;