]> git.sur5r.net Git - i3/i3status/blob - src/print_volume.c
Fixed ALSA capture device monitoring.
[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) {
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 {
66             *(outwalk++) = '%';
67         }
68     }
69     return outwalk;
70 }
71
72 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) {
73     char *outwalk = buffer;
74     int pbval = 1;
75
76     /* Printing volume works with ALSA and PulseAudio at the moment */
77     if (output_format == O_I3BAR) {
78         char *instance;
79         asprintf(&instance, "%s.%s.%d", device, mixer, mixer_idx);
80         INSTANCE(instance);
81         free(instance);
82     }
83
84 #if !defined(__DragonFly__) && !defined(__OpenBSD__)
85     /* Try PulseAudio first */
86
87     /* If the device name has the format "pulse[:N]" where N is the
88      * index of the PulseAudio sink then force PulseAudio, optionally
89      * overriding the default sink */
90     if (!strncasecmp(device, "pulse", strlen("pulse"))) {
91         uint32_t sink_idx = device[strlen("pulse")] == ':' ? (uint32_t)atoi(device + strlen("pulse:")) : DEFAULT_SINK_INDEX;
92         const char *sink_name = device[strlen("pulse")] == ':' &&
93                                         !isdigit(device[strlen("pulse:")])
94                                     ? device + strlen("pulse:")
95                                     : NULL;
96         int cvolume = pulse_initialize() ? volume_pulseaudio(sink_idx, sink_name) : 0;
97         int ivolume = DECOMPOSE_VOLUME(cvolume);
98         bool muted = DECOMPOSE_MUTED(cvolume);
99         if (muted) {
100             START_COLOR("color_degraded");
101             pbval = 0;
102         }
103         /* negative result means error, stick to 0 */
104         if (ivolume < 0)
105             ivolume = 0;
106         outwalk = apply_volume_format(muted ? fmt_muted : fmt,
107                                       outwalk,
108                                       ivolume);
109         goto out;
110     } else if (!strcasecmp(device, "default") && pulse_initialize()) {
111         /* no device specified or "default" set */
112         int cvolume = volume_pulseaudio(DEFAULT_SINK_INDEX, NULL);
113         int ivolume = DECOMPOSE_VOLUME(cvolume);
114         bool muted = DECOMPOSE_MUTED(cvolume);
115         if (ivolume >= 0) {
116             if (muted) {
117                 START_COLOR("color_degraded");
118                 pbval = 0;
119             }
120             outwalk = apply_volume_format(muted ? fmt_muted : fmt,
121                                           outwalk,
122                                           ivolume);
123             goto out;
124         }
125         /* negative result means error, fail PulseAudio attempt */
126     }
127 /* If some other device was specified or PulseAudio is not detected,
128  * proceed to ALSA / OSS */
129 #endif
130
131 #ifdef LINUX
132     const long MAX_LINEAR_DB_SCALE = 24;
133     int err;
134     snd_mixer_t *m;
135     snd_mixer_selem_id_t *sid;
136     snd_mixer_elem_t *elem;
137     long min, max, val;
138     bool force_linear = false;
139     int avg;
140
141     if ((err = snd_mixer_open(&m, 0)) < 0) {
142         fprintf(stderr, "i3status: ALSA: Cannot open mixer: %s\n", snd_strerror(err));
143         goto out;
144     }
145
146     /* Attach this mixer handle to the given device */
147     if ((err = snd_mixer_attach(m, device)) < 0) {
148         fprintf(stderr, "i3status: ALSA: Cannot attach mixer to device: %s\n", snd_strerror(err));
149         snd_mixer_close(m);
150         goto out;
151     }
152
153     /* Register this mixer */
154     if ((err = snd_mixer_selem_register(m, NULL, NULL)) < 0) {
155         fprintf(stderr, "i3status: ALSA: snd_mixer_selem_register: %s\n", snd_strerror(err));
156         snd_mixer_close(m);
157         goto out;
158     }
159
160     if ((err = snd_mixer_load(m)) < 0) {
161         fprintf(stderr, "i3status: ALSA: snd_mixer_load: %s\n", snd_strerror(err));
162         snd_mixer_close(m);
163         goto out;
164     }
165
166     snd_mixer_selem_id_malloc(&sid);
167     if (sid == NULL) {
168         snd_mixer_close(m);
169         goto out;
170     }
171
172     /* Find the given mixer */
173     snd_mixer_selem_id_set_index(sid, mixer_idx);
174     snd_mixer_selem_id_set_name(sid, mixer);
175     if (!(elem = snd_mixer_find_selem(m, sid))) {
176         fprintf(stderr, "i3status: ALSA: Cannot find mixer %s (index %u)\n",
177                 snd_mixer_selem_id_get_name(sid), snd_mixer_selem_id_get_index(sid));
178         snd_mixer_close(m);
179         snd_mixer_selem_id_free(sid);
180         goto out;
181     }
182
183     /* Get the volume range to convert the volume later */
184     snd_mixer_handle_events(m);
185     if (!strncasecmp(mixer, "capture", strlen("capture"))) {
186         ALSA_VOLUME(capture)
187     } else {
188         ALSA_VOLUME(playback)
189     }
190
191     if (err != 0) {
192         fprintf(stderr, "i3status: ALSA: Cannot get playback volume.\n");
193         goto out;
194     }
195
196     /* Use linear mapping for raw register values or small ranges of 24 dB */
197     if (force_linear || max - min <= MAX_LINEAR_DB_SCALE * 100) {
198         float avgf = ((float)(val - min) / (max - min)) * 100;
199         avg = (int)avgf;
200         avg = (avgf - avg < 0.5 ? avg : (avg + 1));
201     } else {
202         /* mapped volume to be more natural for the human ear */
203         double normalized = exp10((val - max) / 6000.0);
204         if (min != SND_CTL_TLV_DB_GAIN_MUTE) {
205             double min_norm = exp10((min - max) / 6000.0);
206             normalized = (normalized - min_norm) / (1 - min_norm);
207         }
208         avg = lround(normalized * 100);
209     }
210
211     /* Check for mute */
212     if (snd_mixer_selem_has_playback_switch(elem)) {
213         ALSA_MUTE_SWITCH(playback)
214     } else if (snd_mixer_selem_has_capture_switch(elem)) {
215         ALSA_MUTE_SWITCH(capture)
216     }
217
218     snd_mixer_close(m);
219     snd_mixer_selem_id_free(sid);
220
221     outwalk = apply_volume_format(fmt, outwalk, avg);
222
223 #endif
224 #if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__DragonFly__)
225     char *mixerpath;
226     char defaultmixer[] = "/dev/mixer";
227     int mixfd, vol, devmask = 0;
228     pbval = 1;
229
230     if (mixer_idx > 0)
231         asprintf(&mixerpath, "/dev/mixer%d", mixer_idx);
232     else
233         mixerpath = defaultmixer;
234
235     if ((mixfd = open(mixerpath, O_RDWR)) < 0) {
236 #if defined(__OpenBSD__)
237         warn("audioio: Cannot open mixer");
238 #else
239         warn("OSS: Cannot open mixer");
240 #endif
241         goto out;
242     }
243
244     if (mixer_idx > 0)
245         free(mixerpath);
246
247 #if defined(__OpenBSD__)
248     int oclass_idx = -1, master_idx = -1, master_mute_idx = -1;
249     int master_next = AUDIO_MIXER_LAST;
250     mixer_devinfo_t devinfo, devinfo2;
251     mixer_ctrl_t vinfo;
252
253     devinfo.index = 0;
254     while (ioctl(mixfd, AUDIO_MIXER_DEVINFO, &devinfo) >= 0) {
255         if (devinfo.type != AUDIO_MIXER_CLASS) {
256             devinfo.index++;
257             continue;
258         }
259         if (strncmp(devinfo.label.name, AudioCoutputs, MAX_AUDIO_DEV_LEN) == 0)
260             oclass_idx = devinfo.index;
261
262         devinfo.index++;
263     }
264
265     devinfo2.index = 0;
266     while (ioctl(mixfd, AUDIO_MIXER_DEVINFO, &devinfo2) >= 0) {
267         if ((devinfo2.type == AUDIO_MIXER_VALUE) && (devinfo2.mixer_class == oclass_idx) && (strncmp(devinfo2.label.name, AudioNmaster, MAX_AUDIO_DEV_LEN) == 0)) {
268             master_idx = devinfo2.index;
269             master_next = devinfo2.next;
270         }
271
272         if ((devinfo2.type == AUDIO_MIXER_ENUM) && (devinfo2.mixer_class == oclass_idx) && (strncmp(devinfo2.label.name, AudioNmute, MAX_AUDIO_DEV_LEN) == 0))
273             if (master_next == devinfo2.index)
274                 master_mute_idx = devinfo2.index;
275
276         if (master_next != AUDIO_MIXER_LAST)
277             master_next = devinfo2.next;
278         devinfo2.index++;
279     }
280
281     if (master_idx == -1)
282         goto out;
283
284     devinfo.index = master_idx;
285     if (ioctl(mixfd, AUDIO_MIXER_DEVINFO, &devinfo) == -1)
286         goto out;
287
288     vinfo.dev = master_idx;
289     vinfo.type = AUDIO_MIXER_VALUE;
290     vinfo.un.value.num_channels = devinfo.un.v.num_channels;
291     if (ioctl(mixfd, AUDIO_MIXER_READ, &vinfo) == -1)
292         goto out;
293
294     if (AUDIO_MAX_GAIN != 100) {
295         float avgf = ((float)vinfo.un.value.level[AUDIO_MIXER_LEVEL_MONO] / AUDIO_MAX_GAIN) * 100;
296         vol = (int)avgf;
297         vol = (avgf - vol < 0.5 ? vol : (vol + 1));
298     } else {
299         vol = (int)vinfo.un.value.level[AUDIO_MIXER_LEVEL_MONO];
300     }
301
302     vinfo.dev = master_mute_idx;
303     vinfo.type = AUDIO_MIXER_ENUM;
304     if (ioctl(mixfd, AUDIO_MIXER_READ, &vinfo) == -1)
305         goto out;
306
307     if (master_mute_idx != -1 && vinfo.un.ord) {
308         START_COLOR("color_degraded");
309         fmt = fmt_muted;
310         pbval = 0;
311     }
312
313 #else
314     if (ioctl(mixfd, SOUND_MIXER_READ_DEVMASK, &devmask) == -1) {
315         warn("OSS: Cannot read mixer information");
316         goto out;
317     }
318     if (ioctl(mixfd, MIXER_READ(0), &vol) == -1) {
319         warn("OSS: Cannot read mixer information");
320         goto out;
321     }
322
323     if (((vol & 0x7f) == 0) && (((vol >> 8) & 0x7f) == 0)) {
324         START_COLOR("color_degraded");
325         pbval = 0;
326     }
327
328 #endif
329     outwalk = apply_volume_format(fmt, outwalk, vol & 0x7f);
330     close(mixfd);
331 #endif
332
333 out:
334     *outwalk = '\0';
335     if (!pbval)
336         END_COLOR;
337     OUTPUT_FULL_TEXT(buffer);
338 }