]> git.sur5r.net Git - i3/i3status/blob - src/pulse.c
PulseAudio support for volume input
[i3/i3status] / src / pulse.c
1 // vim:ts=4:sw=4:expandtab
2 #include <string.h>
3 #include <stdio.h>
4 #include <pulse/pulseaudio.h>
5 #include "i3status.h"
6 #include "queue.h"
7
8 #define APP_NAME "i3status"
9 #define APP_ID "org.i3wm"
10
11 typedef struct indexed_volume_s {
12     uint32_t idx;
13     int volume;
14     TAILQ_ENTRY(indexed_volume_s) entries;
15 } indexed_volume_t;
16
17 static pa_threaded_mainloop *main_loop = NULL;
18 static pa_context *context = NULL;
19 static pa_mainloop_api *api = NULL;
20 static bool context_ready = false;
21 static uint32_t default_sink_idx = DEFAULT_SINK_INDEX;
22 TAILQ_HEAD(tailhead, indexed_volume_s) cached_volume =
23     TAILQ_HEAD_INITIALIZER(cached_volume);
24 static pthread_mutex_t pulse_mutex = PTHREAD_MUTEX_INITIALIZER;
25
26 static void pulseaudio_error_log(pa_context *c) {
27     fprintf(stderr,
28             "i3status: PulseAudio: %s\n",
29             pa_strerror(pa_context_errno(c)));
30 }
31
32 static bool pulseaudio_free_operation(pa_context *c, pa_operation *o) {
33     if (o)
34         pa_operation_unref(o);
35     else
36         pulseaudio_error_log(c);
37     /* return false if the operation failed */
38     return o;
39 }
40
41 /*
42  * save the volume for the specified sink index
43  * returning true if the value was changed
44  */
45 static bool save_volume(uint32_t sink_idx, int new_volume) {
46     pthread_mutex_lock(&pulse_mutex);
47     indexed_volume_t *entry;
48     TAILQ_FOREACH(entry, &cached_volume, entries) {
49         if (entry->idx == sink_idx) {
50             const bool changed = (new_volume != entry->volume);
51             entry->volume = new_volume;
52             pthread_mutex_unlock(&pulse_mutex);
53             return changed;
54         }
55     }
56     /* index not found, store it */
57     entry = malloc(sizeof(*entry));
58     TAILQ_INSERT_HEAD(&cached_volume, entry, entries);
59     entry->idx = sink_idx;
60     entry->volume = new_volume;
61     pthread_mutex_unlock(&pulse_mutex);
62     return true;
63 }
64
65 static void store_volume_from_sink_cb(pa_context *c,
66                                       const pa_sink_info *info,
67                                       int eol,
68                                       void *userdata) {
69     if (eol < 0) {
70         if (pa_context_errno(c) == PA_ERR_NOENTITY)
71             return;
72
73         pulseaudio_error_log(c);
74         return;
75     }
76
77     if (eol > 0)
78         return;
79
80     int avg_vol = pa_cvolume_avg(&info->volume);
81     int vol_perc = (int)((long long)avg_vol * 100 / PA_VOLUME_NORM);
82
83     /* if this is the default sink we must try to save it twice: once with
84      * DEFAULT_SINK_INDEX as the index, and another with its proper value
85      * (using bitwise OR to avoid early-out logic) */
86     if ((info->index == default_sink_idx &&
87          save_volume(DEFAULT_SINK_INDEX, vol_perc)) |
88         save_volume(info->index, vol_perc)) {
89         /* if the volume changed, wake the main thread */
90         pthread_mutex_lock(&i3status_sleep_mutex);
91         pthread_cond_broadcast(&i3status_sleep_cond);
92         pthread_mutex_unlock(&i3status_sleep_mutex);
93     }
94 }
95
96 static void get_sink_info(pa_context *c, uint32_t idx) {
97     pa_operation *o =
98         idx == DEFAULT_SINK_INDEX ? pa_context_get_sink_info_by_name(
99                                         c, "@DEFAULT_SINK@", store_volume_from_sink_cb, NULL)
100                                   : pa_context_get_sink_info_by_index(
101                                         c, idx, store_volume_from_sink_cb, NULL);
102     pulseaudio_free_operation(c, o);
103 }
104
105 static void store_default_sink_cb(pa_context *c,
106                                   const pa_sink_info *i,
107                                   int eol,
108                                   void *userdata) {
109     if (i) {
110         if (default_sink_idx != i->index) {
111             /* default sink changed? */
112             default_sink_idx = i->index;
113             store_volume_from_sink_cb(c, i, eol, userdata);
114         }
115     }
116 }
117
118 static void update_default_sink(pa_context *c) {
119     pa_operation *o = pa_context_get_sink_info_by_name(
120         c,
121         "@DEFAULT_SINK@",
122         store_default_sink_cb,
123         NULL);
124     pulseaudio_free_operation(c, o);
125 }
126
127 static void subscribe_cb(pa_context *c, pa_subscription_event_type_t t,
128                          uint32_t idx, void *userdata) {
129     if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) != PA_SUBSCRIPTION_EVENT_CHANGE)
130         return;
131     pa_subscription_event_type_t facility =
132         t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK;
133     switch (facility) {
134         case PA_SUBSCRIPTION_EVENT_SERVER:
135             /* server change event, see if the default sink changed */
136             update_default_sink(c);
137             break;
138         case PA_SUBSCRIPTION_EVENT_SINK:
139             get_sink_info(c, idx);
140             break;
141         default:
142             break;
143     }
144 }
145
146 static void context_state_callback(pa_context *c, void *userdata) {
147     switch (pa_context_get_state(c)) {
148         case PA_CONTEXT_UNCONNECTED:
149         case PA_CONTEXT_CONNECTING:
150         case PA_CONTEXT_AUTHORIZING:
151         case PA_CONTEXT_SETTING_NAME:
152         case PA_CONTEXT_TERMINATED:
153         default:
154             break;
155
156         case PA_CONTEXT_READY: {
157             pa_context_set_subscribe_callback(c, subscribe_cb, NULL);
158             update_default_sink(c);
159
160             pa_operation *o = pa_context_subscribe(
161                 c,
162                 PA_SUBSCRIPTION_MASK_SINK | PA_SUBSCRIPTION_MASK_SERVER,
163                 NULL,
164                 NULL);
165             if (!pulseaudio_free_operation(c, o))
166                 break;
167             context_ready = true;
168         } break;
169
170         case PA_CONTEXT_FAILED:
171             pulseaudio_error_log(c);
172             break;
173     }
174 }
175
176 /*
177  * returns the current volume in percent, which, as per PulseAudio,
178  * may be > 100%
179  */
180 int volume_pulseaudio(uint32_t sink_idx) {
181     if (!context_ready || default_sink_idx == DEFAULT_SINK_INDEX)
182         return -1;
183
184     pthread_mutex_lock(&pulse_mutex);
185     const indexed_volume_t *entry;
186     TAILQ_FOREACH(entry, &cached_volume, entries) {
187         if (entry->idx == sink_idx) {
188             int vol = entry->volume;
189             pthread_mutex_unlock(&pulse_mutex);
190             return vol;
191         }
192     }
193     pthread_mutex_unlock(&pulse_mutex);
194     /* first time requires a prime callback call because we only get
195      * updates when the volume actually changes, but we need it to
196      * be correct even if it never changes */
197     pa_threaded_mainloop_lock(main_loop);
198     get_sink_info(context, sink_idx);
199     pa_threaded_mainloop_unlock(main_loop);
200     /* show 0 while we don't have this information */
201     return 0;
202 }
203
204 /*
205  *  detect and, if necessary, initialize the PulseAudio API
206  */
207 bool pulse_initialize(void) {
208     if (!main_loop) {
209         main_loop = pa_threaded_mainloop_new();
210         if (!main_loop)
211             return false;
212     }
213     if (!api) {
214         api = pa_threaded_mainloop_get_api(main_loop);
215         if (!api)
216             return false;
217     }
218     if (!context) {
219         pa_proplist *proplist = pa_proplist_new();
220         pa_proplist_sets(proplist, PA_PROP_APPLICATION_NAME, APP_NAME);
221         pa_proplist_sets(proplist, PA_PROP_APPLICATION_ID, APP_ID);
222         pa_proplist_sets(proplist, PA_PROP_APPLICATION_VERSION, VERSION);
223         context = pa_context_new_with_proplist(api, APP_NAME, proplist);
224         pa_proplist_free(proplist);
225         if (!context)
226             return false;
227         pa_context_set_state_callback(context,
228                                       context_state_callback,
229                                       NULL);
230         if (pa_context_connect(context,
231                                NULL,
232                                PA_CONTEXT_NOFAIL | PA_CONTEXT_NOAUTOSPAWN,
233                                NULL) < 0) {
234             pulseaudio_error_log(context);
235             return false;
236         }
237         if (pa_threaded_mainloop_start(main_loop) < 0) {
238             pulseaudio_error_log(context);
239             pa_threaded_mainloop_free(main_loop);
240             main_loop = NULL;
241             return false;
242         }
243     }
244     return true;
245 }