]> git.sur5r.net Git - i3/i3status/blob - src/print_volume.c
Implement %devicename specifier for volume module (#325)
[i3/i3status] / src / print_volume.c
1 // vim:ts=4:sw=4:expandtab
2 #include <time.h>
3 #include <string.h>
4 #include <stdlib.h>
5 #include <stdio.h>
6 #include <err.h>
7 #include <ctype.h>
8 #include <yajl/yajl_gen.h>
9 #include <yajl/yajl_version.h>
10
11 #ifdef LINUX
12 #include <alsa/asoundlib.h>
13 #include <alloca.h>
14 #include <math.h>
15 #endif
16
17 #if defined(__FreeBSD__) || defined(__DragonFly__)
18 #include <fcntl.h>
19 #include <unistd.h>
20 #include <sys/soundcard.h>
21 #endif
22
23 #ifdef __OpenBSD__
24 #include <fcntl.h>
25 #include <unistd.h>
26 #include <sys/audioio.h>
27 #include <sys/ioctl.h>
28 #endif
29
30 #include "i3status.h"
31 #include "queue.h"
32
33 #define ALSA_VOLUME(channel)                                                    \
34     err = snd_mixer_selem_get_##channel##_dB_range(elem, &min, &max) ||         \
35           snd_mixer_selem_get_##channel##_dB(elem, 0, &val);                    \
36     if (err != 0 || min >= max) {                                               \
37         err = snd_mixer_selem_get_##channel##_volume_range(elem, &min, &max) || \
38               snd_mixer_selem_get_##channel##_volume(elem, 0, &val);            \
39         force_linear = true;                                                    \
40     }
41
42 #define ALSA_MUTE_SWITCH(channel)                                                        \
43     if ((err = snd_mixer_selem_get_##channel##_switch(elem, 0, &pbval)) < 0)             \
44         fprintf(stderr, "i3status: ALSA: " #channel "_switch: %s\n", snd_strerror(err)); \
45     if (!pbval) {                                                                        \
46         START_COLOR("color_degraded");                                                   \
47         fmt = fmt_muted;                                                                 \
48     }
49
50 static char *apply_volume_format(const char *fmt, char *outwalk, int ivolume, const char *devicename) {
51     const char *walk = fmt;
52
53     for (; *walk != '\0'; walk++) {
54         if (*walk != '%') {
55             *(outwalk++) = *walk;
56
57         } else if (BEGINS_WITH(walk + 1, "%")) {
58             outwalk += sprintf(outwalk, "%s", pct_mark);
59             walk += strlen("%");
60
61         } else if (BEGINS_WITH(walk + 1, "volume")) {
62             outwalk += sprintf(outwalk, "%d%s", ivolume, pct_mark);
63             walk += strlen("volume");
64
65         } else if (BEGINS_WITH(walk + 1, "devicename")) {
66             outwalk += sprintf(outwalk, "%s", devicename);
67             walk += strlen("devicename");
68
69         } else {
70             *(outwalk++) = '%';
71         }
72     }
73     return outwalk;
74 }
75
76 void print_volume(yajl_gen json_gen, char *buffer, const char *fmt, const char *fmt_muted, const char *device, const char *mixer, int mixer_idx) {
77     char *outwalk = buffer;
78     int pbval = 1;
79
80     /* Printing volume works with ALSA and PulseAudio at the moment */
81     if (output_format == O_I3BAR) {
82         char *instance;
83         asprintf(&instance, "%s.%s.%d", device, mixer, mixer_idx);
84         INSTANCE(instance);
85         free(instance);
86     }
87
88 #if !defined(__DragonFly__) && !defined(__OpenBSD__)
89     /* Try PulseAudio first */
90
91     /* If the device name has the format "pulse[:N]" where N is the
92      * index of the PulseAudio sink then force PulseAudio, optionally
93      * overriding the default sink */
94     if (!strncasecmp(device, "pulse", strlen("pulse"))) {
95         uint32_t sink_idx = device[strlen("pulse")] == ':' ? (uint32_t)atoi(device + strlen("pulse:")) : DEFAULT_SINK_INDEX;
96         const char *sink_name = device[strlen("pulse")] == ':' &&
97                                         !isdigit(device[strlen("pulse:")])
98                                     ? device + strlen("pulse:")
99                                     : NULL;
100         int cvolume = 0;
101         char description[MAX_SINK_DESCRIPTION_LEN] = {'\0'};
102
103         if (pulse_initialize()) {
104             cvolume = volume_pulseaudio(sink_idx, sink_name);
105             /* false result means error, stick to empty-string */
106             if (!description_pulseaudio(sink_idx, sink_name, description)) {
107                 description[0] = '\0';
108             }
109         }
110
111         int ivolume = DECOMPOSE_VOLUME(cvolume);
112         bool muted = DECOMPOSE_MUTED(cvolume);
113         if (muted) {
114             START_COLOR("color_degraded");
115             pbval = 0;
116         }
117
118         /* negative result means error, stick to 0 */
119         if (ivolume < 0)
120             ivolume = 0;
121         outwalk = apply_volume_format(muted ? fmt_muted : fmt,
122                                       outwalk,
123                                       ivolume,
124                                       description);
125         goto out;
126     } else if (!strcasecmp(device, "default") && pulse_initialize()) {
127         /* no device specified or "default" set */
128         char description[MAX_SINK_DESCRIPTION_LEN];
129         bool success = description_pulseaudio(DEFAULT_SINK_INDEX, NULL, description);
130         int cvolume = volume_pulseaudio(DEFAULT_SINK_INDEX, NULL);
131         int ivolume = DECOMPOSE_VOLUME(cvolume);
132         bool muted = DECOMPOSE_MUTED(cvolume);
133         if (ivolume >= 0 && success) {
134             if (muted) {
135                 START_COLOR("color_degraded");
136                 pbval = 0;
137             }
138             outwalk = apply_volume_format(muted ? fmt_muted : fmt,
139                                           outwalk,
140                                           ivolume,
141                                           description);
142             goto out;
143         }
144         /* negative result or NULL description means error, fail PulseAudio attempt */
145     }
146 /* If some other device was specified or PulseAudio is not detected,
147  * proceed to ALSA / OSS */
148 #endif
149
150 #ifdef LINUX
151     const long MAX_LINEAR_DB_SCALE = 24;
152     int err;
153     snd_mixer_t *m;
154     snd_mixer_selem_id_t *sid;
155     snd_mixer_elem_t *elem;
156     long min, max, val;
157     const char *mixer_name;
158     bool force_linear = false;
159     int avg;
160
161     if ((err = snd_mixer_open(&m, 0)) < 0) {
162         fprintf(stderr, "i3status: ALSA: Cannot open mixer: %s\n", snd_strerror(err));
163         goto out;
164     }
165
166     /* Attach this mixer handle to the given device */
167     if ((err = snd_mixer_attach(m, device)) < 0) {
168         fprintf(stderr, "i3status: ALSA: Cannot attach mixer to device: %s\n", snd_strerror(err));
169         snd_mixer_close(m);
170         goto out;
171     }
172
173     /* Register this mixer */
174     if ((err = snd_mixer_selem_register(m, NULL, NULL)) < 0) {
175         fprintf(stderr, "i3status: ALSA: snd_mixer_selem_register: %s\n", snd_strerror(err));
176         snd_mixer_close(m);
177         goto out;
178     }
179
180     if ((err = snd_mixer_load(m)) < 0) {
181         fprintf(stderr, "i3status: ALSA: snd_mixer_load: %s\n", snd_strerror(err));
182         snd_mixer_close(m);
183         goto out;
184     }
185
186     snd_mixer_selem_id_malloc(&sid);
187     if (sid == NULL) {
188         snd_mixer_close(m);
189         goto out;
190     }
191
192     /* Find the given mixer */
193     snd_mixer_selem_id_set_index(sid, mixer_idx);
194     snd_mixer_selem_id_set_name(sid, mixer);
195     if (!(elem = snd_mixer_find_selem(m, sid))) {
196         fprintf(stderr, "i3status: ALSA: Cannot find mixer %s (index %u)\n",
197                 snd_mixer_selem_id_get_name(sid), snd_mixer_selem_id_get_index(sid));
198         snd_mixer_close(m);
199         snd_mixer_selem_id_free(sid);
200         goto out;
201     }
202
203     /* Get the volume range to convert the volume later */
204     snd_mixer_handle_events(m);
205     if (!strncasecmp(mixer, "capture", strlen("capture"))) {
206         ALSA_VOLUME(capture)
207     } else {
208         ALSA_VOLUME(playback)
209     }
210
211     if (err != 0) {
212         fprintf(stderr, "i3status: ALSA: Cannot get playback volume.\n");
213         goto out;
214     }
215
216     mixer_name = snd_mixer_selem_get_name(elem);
217     if (!mixer_name) {
218         fprintf(stderr, "i3status: ALSA: NULL mixer_name.\n");
219         goto out;
220     }
221
222     /* Use linear mapping for raw register values or small ranges of 24 dB */
223     if (force_linear || max - min <= MAX_LINEAR_DB_SCALE * 100) {
224         float avgf = ((float)(val - min) / (max - min)) * 100;
225         avg = (int)avgf;
226         avg = (avgf - avg < 0.5 ? avg : (avg + 1));
227     } else {
228         /* mapped volume to be more natural for the human ear */
229         double normalized = exp10((val - max) / 6000.0);
230         if (min != SND_CTL_TLV_DB_GAIN_MUTE) {
231             double min_norm = exp10((min - max) / 6000.0);
232             normalized = (normalized - min_norm) / (1 - min_norm);
233         }
234         avg = lround(normalized * 100);
235     }
236
237     /* Check for mute */
238     if (snd_mixer_selem_has_playback_switch(elem)) {
239         ALSA_MUTE_SWITCH(playback)
240     } else if (snd_mixer_selem_has_capture_switch(elem)) {
241         ALSA_MUTE_SWITCH(capture)
242     }
243
244     outwalk = apply_volume_format(fmt, outwalk, avg, mixer_name);
245
246     snd_mixer_close(m);
247     snd_mixer_selem_id_free(sid);
248
249 #endif
250 #if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__DragonFly__)
251     char *mixerpath;
252     char defaultmixer[] = "/dev/mixer";
253     int mixfd, vol, devmask = 0;
254     const char *devicename = "UNSUPPORTED"; /* TODO: implement support for this */
255     pbval = 1;
256
257     if (mixer_idx > 0)
258         asprintf(&mixerpath, "/dev/mixer%d", mixer_idx);
259     else
260         mixerpath = defaultmixer;
261
262     if ((mixfd = open(mixerpath, O_RDWR)) < 0) {
263 #if defined(__OpenBSD__)
264         warn("audioio: Cannot open mixer");
265 #else
266         warn("OSS: Cannot open mixer");
267 #endif
268         goto out;
269     }
270
271     if (mixer_idx > 0)
272         free(mixerpath);
273
274 #if defined(__OpenBSD__)
275     int oclass_idx = -1, master_idx = -1, master_mute_idx = -1;
276     int master_next = AUDIO_MIXER_LAST;
277     mixer_devinfo_t devinfo, devinfo2;
278     mixer_ctrl_t vinfo;
279
280     devinfo.index = 0;
281     while (ioctl(mixfd, AUDIO_MIXER_DEVINFO, &devinfo) >= 0) {
282         if (devinfo.type != AUDIO_MIXER_CLASS) {
283             devinfo.index++;
284             continue;
285         }
286         if (strncmp(devinfo.label.name, AudioCoutputs, MAX_AUDIO_DEV_LEN) == 0)
287             oclass_idx = devinfo.index;
288
289         devinfo.index++;
290     }
291
292     devinfo2.index = 0;
293     while (ioctl(mixfd, AUDIO_MIXER_DEVINFO, &devinfo2) >= 0) {
294         if ((devinfo2.type == AUDIO_MIXER_VALUE) && (devinfo2.mixer_class == oclass_idx) && (strncmp(devinfo2.label.name, AudioNmaster, MAX_AUDIO_DEV_LEN) == 0)) {
295             master_idx = devinfo2.index;
296             master_next = devinfo2.next;
297         }
298
299         if ((devinfo2.type == AUDIO_MIXER_ENUM) && (devinfo2.mixer_class == oclass_idx) && (strncmp(devinfo2.label.name, AudioNmute, MAX_AUDIO_DEV_LEN) == 0))
300             if (master_next == devinfo2.index)
301                 master_mute_idx = devinfo2.index;
302
303         if (master_next != AUDIO_MIXER_LAST)
304             master_next = devinfo2.next;
305         devinfo2.index++;
306     }
307
308     if (master_idx == -1)
309         goto out;
310
311     devinfo.index = master_idx;
312     if (ioctl(mixfd, AUDIO_MIXER_DEVINFO, &devinfo) == -1)
313         goto out;
314
315     vinfo.dev = master_idx;
316     vinfo.type = AUDIO_MIXER_VALUE;
317     vinfo.un.value.num_channels = devinfo.un.v.num_channels;
318     if (ioctl(mixfd, AUDIO_MIXER_READ, &vinfo) == -1)
319         goto out;
320
321     if (AUDIO_MAX_GAIN != 100) {
322         float avgf = ((float)vinfo.un.value.level[AUDIO_MIXER_LEVEL_MONO] / AUDIO_MAX_GAIN) * 100;
323         vol = (int)avgf;
324         vol = (avgf - vol < 0.5 ? vol : (vol + 1));
325     } else {
326         vol = (int)vinfo.un.value.level[AUDIO_MIXER_LEVEL_MONO];
327     }
328
329     vinfo.dev = master_mute_idx;
330     vinfo.type = AUDIO_MIXER_ENUM;
331     if (ioctl(mixfd, AUDIO_MIXER_READ, &vinfo) == -1)
332         goto out;
333
334     if (master_mute_idx != -1 && vinfo.un.ord) {
335         START_COLOR("color_degraded");
336         fmt = fmt_muted;
337         pbval = 0;
338     }
339
340 #else
341     if (ioctl(mixfd, SOUND_MIXER_READ_DEVMASK, &devmask) == -1) {
342         warn("OSS: Cannot read mixer information");
343         goto out;
344     }
345     if (ioctl(mixfd, MIXER_READ(0), &vol) == -1) {
346         warn("OSS: Cannot read mixer information");
347         goto out;
348     }
349
350     if (((vol & 0x7f) == 0) && (((vol >> 8) & 0x7f) == 0)) {
351         START_COLOR("color_degraded");
352         pbval = 0;
353     }
354
355 #endif
356     outwalk = apply_volume_format(fmt, outwalk, vol & 0x7f, devicename);
357     close(mixfd);
358 #endif
359
360 out:
361     *outwalk = '\0';
362     if (!pbval)
363         END_COLOR;
364     OUTPUT_FULL_TEXT(buffer);
365 }