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