Commit | Line | Data |
---|---|---|
b25aac59 | 1 | /* |
2 | * This file is part of DisOrder | |
5aff007d | 3 | * Copyright (C) 2007, 2008 Richard Kettlewell |
b25aac59 | 4 | * |
e7eb3a27 | 5 | * This program is free software: you can redistribute it and/or modify |
b25aac59 | 6 | * it under the terms of the GNU General Public License as published by |
e7eb3a27 | 7 | * the Free Software Foundation, either version 3 of the License, or |
b25aac59 | 8 | * (at your option) any later version. |
e7eb3a27 RK |
9 | * |
10 | * This program is distributed in the hope that it will be useful, | |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
13 | * GNU General Public License for more details. | |
14 | * | |
b25aac59 | 15 | * You should have received a copy of the GNU General Public License |
e7eb3a27 | 16 | * along with this program. If not, see <http://www.gnu.org/licenses/>. |
b25aac59 | 17 | */ |
18 | /** @file lib/mixer-alsa.c | |
19 | * @brief ALSA mixer support | |
20 | * | |
21 | * The documentation for ALSA's mixer support is completely hopeless, | |
22 | * which is a particular nuisnace given it's got an incredibly verbose | |
23 | * API. Much of this code is cribbed from | |
24 | * alsa-utils-1.0.13/amixer/amixer.c. | |
25 | * | |
26 | * Mono output devices are supported, but the support is not tested | |
27 | * (as I don't one). | |
28 | */ | |
29 | ||
05b75f8d | 30 | #include "common.h" |
b25aac59 | 31 | |
32 | #if HAVE_ALSA_ASOUNDLIB_H | |
33 | ||
b25aac59 | 34 | #include <fcntl.h> |
35 | #include <unistd.h> | |
b25aac59 | 36 | #include <errno.h> |
37 | #include <stddef.h> | |
38 | #include <ctype.h> | |
39 | #include <sys/ioctl.h> | |
40 | #include <alsa/asoundlib.h> | |
41 | ||
42 | #include "configuration.h" | |
43 | #include "mixer.h" | |
44 | #include "log.h" | |
45 | #include "syscalls.h" | |
46 | ||
47 | /** @brief Shared state for ALSA mixer support */ | |
48 | struct alsa_mixer_state { | |
49 | /** @brief Mixer handle */ | |
50 | snd_mixer_t *handle; | |
51 | ||
52 | /** @brief Mixer control */ | |
53 | snd_mixer_elem_t *elem; | |
54 | ||
55 | /** @brief Left channel */ | |
56 | snd_mixer_selem_channel_id_t left; | |
57 | ||
58 | /** @brief Right channel */ | |
59 | snd_mixer_selem_channel_id_t right; | |
60 | ||
61 | /** @brief Minimum level */ | |
62 | long min; | |
63 | ||
64 | /** @brief Maximum level */ | |
65 | long max; | |
66 | }; | |
67 | ||
68 | /** @brief Destroy a @ref alsa_mixer_state */ | |
69 | static void alsa_close(struct alsa_mixer_state *h) { | |
70 | /* TODO h->elem */ | |
71 | if(h->handle) | |
72 | snd_mixer_close(h->handle); | |
73 | } | |
74 | ||
75 | /** @brief Initialize a @ref alsa_mixer_state */ | |
76 | static int alsa_open(struct alsa_mixer_state *h) { | |
77 | int err; | |
78 | snd_mixer_selem_id_t *id; | |
79 | ||
80 | snd_mixer_selem_id_alloca(&id); | |
81 | memset(h, 0, sizeof h); | |
82 | if((err = snd_mixer_open(&h->handle, 0))) { | |
83 | error(0, "snd_mixer_open: %s", snd_strerror(err)); | |
84 | return -1; | |
85 | } | |
86 | if((err = snd_mixer_attach(h->handle, config->device))) { | |
87 | error(0, "snd_mixer_attach %s: %s", | |
88 | config->device, snd_strerror(err)); | |
89 | goto error; | |
90 | } | |
91 | if((err = snd_mixer_selem_register(h->handle, 0/*options*/, 0/*classp*/))) { | |
92 | error(0, "snd_mixer_selem_register %s: %s", | |
93 | config->device, snd_strerror(err)); | |
94 | goto error; | |
95 | } | |
96 | if((err = snd_mixer_load(h->handle))) { | |
97 | error(0, "snd_mixer_load %s: %s", | |
98 | config->device, snd_strerror(err)); | |
99 | goto error; | |
100 | } | |
101 | snd_mixer_selem_id_set_name(id, config->channel); | |
102 | snd_mixer_selem_id_set_index(id, atoi(config->mixer)); | |
103 | if(!(h->elem = snd_mixer_find_selem(h->handle, id))) { | |
8899ace0 RK |
104 | error(0, "device '%s' mixer control '%s,%s' does not exist", |
105 | config->device, config->channel, config->mixer); | |
b25aac59 | 106 | goto error; |
107 | } | |
108 | if(!snd_mixer_selem_has_playback_volume(h->elem)) { | |
8899ace0 RK |
109 | error(0, "device '%s' mixer control '%s,%s' has no playback volume", |
110 | config->device, config->channel, config->mixer); | |
b25aac59 | 111 | goto error; |
112 | } | |
113 | if(snd_mixer_selem_is_playback_mono(h->elem)) { | |
114 | h->left = h->right = SND_MIXER_SCHN_MONO; | |
115 | } else { | |
116 | h->left = SND_MIXER_SCHN_FRONT_LEFT; | |
117 | h->right = SND_MIXER_SCHN_FRONT_RIGHT; | |
118 | } | |
119 | if(!snd_mixer_selem_has_playback_channel(h->elem, h->left) | |
120 | || !snd_mixer_selem_has_playback_channel(h->elem, h->right)) { | |
8899ace0 RK |
121 | error(0, "device '%s' mixer control '%s,%s' lacks required playback channels", |
122 | config->device, config->channel, config->mixer); | |
b25aac59 | 123 | goto error; |
124 | } | |
125 | snd_mixer_selem_get_playback_volume_range(h->elem, &h->min, &h->max); | |
126 | return 0; | |
127 | error: | |
128 | alsa_close(h); | |
129 | return -1; | |
130 | } | |
131 | ||
132 | /** @brief Convert a level to a percentage */ | |
133 | static int to_percent(const struct alsa_mixer_state *h, long n) { | |
134 | return (n - h->min) * 100 / (h->max - h->min); | |
135 | } | |
136 | ||
137 | /** @brief Get ALSA volume */ | |
138 | static int alsa_get(int *left, int *right) { | |
139 | struct alsa_mixer_state h[1]; | |
140 | long l, r; | |
141 | int err; | |
142 | ||
143 | if(alsa_open(h)) | |
144 | return -1; | |
145 | if((err = snd_mixer_selem_get_playback_volume(h->elem, h->left, &l)) | |
146 | || (err = snd_mixer_selem_get_playback_volume(h->elem, h->right, &r))) { | |
147 | error(0, "snd_mixer_selem_get_playback_volume: %s", snd_strerror(err)); | |
148 | goto error; | |
149 | } | |
150 | *left = to_percent(h, l); | |
151 | *right = to_percent(h, r); | |
152 | alsa_close(h); | |
153 | return 0; | |
154 | error: | |
155 | alsa_close(h); | |
156 | return -1; | |
157 | } | |
158 | ||
159 | /** @brief Convert a percentage to a level */ | |
160 | static int from_percent(const struct alsa_mixer_state *h, int n) { | |
161 | return h->min + n * (h->max - h->min) / 100; | |
162 | } | |
163 | ||
164 | /** @brief Set ALSA volume */ | |
165 | static int alsa_set(int *left, int *right) { | |
166 | struct alsa_mixer_state h[1]; | |
167 | long l, r; | |
168 | int err; | |
169 | ||
170 | if(alsa_open(h)) | |
171 | return -1; | |
172 | /* Set the volume */ | |
173 | if(h->left == h->right) { | |
174 | /* Mono output - just use the loudest */ | |
175 | if((err = snd_mixer_selem_set_playback_volume | |
176 | (h->elem, h->left, | |
177 | from_percent(h, *left > *right ? *left : *right)))) { | |
178 | error(0, "snd_mixer_selem_set_playback_volume: %s", snd_strerror(err)); | |
179 | goto error; | |
180 | } | |
181 | } else { | |
182 | /* Stereo output */ | |
183 | if((err = snd_mixer_selem_set_playback_volume | |
184 | (h->elem, h->left, from_percent(h, *left))) | |
185 | || (err = snd_mixer_selem_set_playback_volume | |
186 | (h->elem, h->right, from_percent(h, *right)))) { | |
187 | error(0, "snd_mixer_selem_set_playback_volume: %s", snd_strerror(err)); | |
188 | goto error; | |
189 | } | |
190 | } | |
191 | /* Read it back to see what we ended up at */ | |
192 | if((err = snd_mixer_selem_get_playback_volume(h->elem, h->left, &l)) | |
193 | || (err = snd_mixer_selem_get_playback_volume(h->elem, h->right, &r))) { | |
194 | error(0, "snd_mixer_selem_get_playback_volume: %s", snd_strerror(err)); | |
195 | goto error; | |
196 | } | |
197 | *left = to_percent(h, l); | |
198 | *right = to_percent(h, r); | |
199 | alsa_close(h); | |
200 | return 0; | |
201 | error: | |
202 | alsa_close(h); | |
203 | return -1; | |
204 | } | |
205 | ||
206 | /** @brief ALSA mixer vtable */ | |
207 | const struct mixer mixer_alsa = { | |
208 | BACKEND_ALSA, | |
209 | alsa_get, | |
210 | alsa_set, | |
211 | "0", | |
212 | "PCM" | |
213 | }; | |
214 | #endif | |
215 | ||
216 | /* | |
217 | Local Variables: | |
218 | c-basic-offset:2 | |
219 | comment-column:40 | |
220 | End: | |
221 | */ |