Commit | Line | Data |
---|---|---|
4fd38868 RK |
1 | /* |
2 | * This file is part of DisOrder. | |
5a65808b | 3 | * Copyright (C) 2009, 2013 Richard Kettlewell |
4fd38868 RK |
4 | * |
5 | * This program is free software: you can redistribute it and/or modify | |
6 | * it under the terms of the GNU General Public License as published by | |
7 | * the Free Software Foundation, either version 3 of the License, or | |
8 | * (at your option) any later version. | |
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 | * | |
15 | * You should have received a copy of the GNU General Public License | |
16 | * along with this program. If not, see <http://www.gnu.org/licenses/>. | |
17 | */ | |
18 | /** @file lib/uaudio-alsa.c | |
19 | * @brief Support for ALSA backend */ | |
20 | #include "common.h" | |
21 | ||
22 | #if HAVE_ALSA_ASOUNDLIB_H | |
23 | ||
24 | #include <alsa/asoundlib.h> | |
25 | ||
26 | #include "mem.h" | |
27 | #include "log.h" | |
28 | #include "uaudio.h" | |
ba70caca | 29 | #include "configuration.h" |
4fd38868 RK |
30 | |
31 | /** @brief The current PCM handle */ | |
32 | static snd_pcm_t *alsa_pcm; | |
33 | ||
34 | static const char *const alsa_options[] = { | |
35 | "device", | |
b50cfb8a RK |
36 | "mixer-control", |
37 | "mixer-channel", | |
4fd38868 RK |
38 | NULL |
39 | }; | |
40 | ||
b50cfb8a RK |
41 | /** @brief Mixer handle */ |
42 | snd_mixer_t *alsa_mixer_handle; | |
43 | ||
44 | /** @brief Mixer control */ | |
45 | static snd_mixer_elem_t *alsa_mixer_elem; | |
46 | ||
47 | /** @brief Left channel */ | |
48 | static snd_mixer_selem_channel_id_t alsa_mixer_left; | |
49 | ||
50 | /** @brief Right channel */ | |
51 | static snd_mixer_selem_channel_id_t alsa_mixer_right; | |
52 | ||
53 | /** @brief Minimum level */ | |
54 | static long alsa_mixer_min; | |
55 | ||
56 | /** @brief Maximum level */ | |
57 | static long alsa_mixer_max; | |
58 | ||
4fd38868 | 59 | /** @brief Actually play sound via ALSA */ |
b1f6ca8c RK |
60 | static size_t alsa_play(void *buffer, size_t samples, unsigned flags) { |
61 | /* If we're paused we just pretend. We rely on snd_pcm_writei() blocking so | |
62 | * we have to fake up a sleep here. However it doesn't have to be all that | |
63 | * accurate - in particular it's quite acceptable to greatly underestimate | |
64 | * the required wait time. For 'lengthy' waits we do this by the blunt | |
65 | * instrument of halving it. */ | |
66 | if(flags & UAUDIO_PAUSED) { | |
67 | if(samples > 64) | |
68 | samples /= 2; | |
69 | const uint64_t ns = ((uint64_t)samples * 1000000000 | |
70 | / (uaudio_rate * uaudio_channels)); | |
71 | struct timespec ts[1]; | |
72 | ts->tv_sec = ns / 1000000000; | |
73 | ts->tv_nsec = ns % 1000000000; | |
74 | while(nanosleep(ts, ts) < 0 && errno == EINTR) | |
75 | ; | |
76 | return samples; | |
77 | } | |
4fd38868 RK |
78 | int err; |
79 | /* ALSA wants 'frames', where frame = several concurrently played samples */ | |
80 | const snd_pcm_uframes_t frames = samples / uaudio_channels; | |
81 | ||
82 | snd_pcm_sframes_t rc = snd_pcm_writei(alsa_pcm, buffer, frames); | |
83 | if(rc < 0) { | |
84 | switch(rc) { | |
85 | case -EPIPE: | |
86 | if((err = snd_pcm_prepare(alsa_pcm))) | |
c04511f6 | 87 | disorder_fatal(0, "error calling snd_pcm_prepare: %d", err); |
4fd38868 RK |
88 | return 0; |
89 | case -EAGAIN: | |
90 | return 0; | |
91 | default: | |
2e9ba080 | 92 | disorder_fatal(0, "error calling snd_pcm_writei: %d", (int)rc); |
4fd38868 RK |
93 | } |
94 | } | |
95 | return rc * uaudio_channels; | |
96 | } | |
97 | ||
98 | /** @brief Open the ALSA sound device */ | |
99 | static void alsa_open(void) { | |
b50cfb8a | 100 | const char *device = uaudio_get("device", "default"); |
4fd38868 RK |
101 | int err; |
102 | ||
4fd38868 RK |
103 | if((err = snd_pcm_open(&alsa_pcm, |
104 | device, | |
105 | SND_PCM_STREAM_PLAYBACK, | |
106 | 0))) | |
2e9ba080 | 107 | disorder_fatal(0, "error from snd_pcm_open: %d", err); |
5a65808b | 108 | /* Hardware parameters */ |
4fd38868 RK |
109 | snd_pcm_hw_params_t *hwparams; |
110 | snd_pcm_hw_params_alloca(&hwparams); | |
111 | if((err = snd_pcm_hw_params_any(alsa_pcm, hwparams)) < 0) | |
2e9ba080 | 112 | disorder_fatal(0, "error from snd_pcm_hw_params_any: %d", err); |
4fd38868 RK |
113 | if((err = snd_pcm_hw_params_set_access(alsa_pcm, hwparams, |
114 | SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) | |
2e9ba080 | 115 | disorder_fatal(0, "error from snd_pcm_hw_params_set_access: %d", err); |
4fd38868 RK |
116 | int sample_format; |
117 | if(uaudio_bits == 16) | |
118 | sample_format = uaudio_signed ? SND_PCM_FORMAT_S16 : SND_PCM_FORMAT_U16; | |
119 | else | |
120 | sample_format = uaudio_signed ? SND_PCM_FORMAT_S8 : SND_PCM_FORMAT_U8; | |
121 | if((err = snd_pcm_hw_params_set_format(alsa_pcm, hwparams, | |
122 | sample_format)) < 0) | |
2e9ba080 | 123 | disorder_fatal(0, "error from snd_pcm_hw_params_set_format (%d): %d", |
4fd38868 RK |
124 | sample_format, err); |
125 | unsigned rate = uaudio_rate; | |
126 | if((err = snd_pcm_hw_params_set_rate_near(alsa_pcm, hwparams, &rate, 0)) < 0) | |
2e9ba080 | 127 | disorder_fatal(0, "error from snd_pcm_hw_params_set_rate_near (%d): %d", |
4fd38868 RK |
128 | rate, err); |
129 | if((err = snd_pcm_hw_params_set_channels(alsa_pcm, hwparams, | |
130 | uaudio_channels)) < 0) | |
2e9ba080 | 131 | disorder_fatal(0, "error from snd_pcm_hw_params_set_channels (%d): %d", |
4fd38868 RK |
132 | uaudio_channels, err); |
133 | if((err = snd_pcm_hw_params(alsa_pcm, hwparams)) < 0) | |
2e9ba080 | 134 | disorder_fatal(0, "error calling snd_pcm_hw_params: %d", err); |
5a65808b RK |
135 | /* Software parameters */ |
136 | snd_pcm_sw_params_t *swparams; | |
137 | snd_pcm_sw_params_alloca(&swparams); | |
138 | if((err = snd_pcm_sw_params_current(alsa_pcm, swparams)) < 0) | |
139 | disorder_fatal(-err, "error calling snd_pcm_sw_params_current"); | |
140 | /* Bump the start threshold a bit since Pulseaudio sulks with the defaults */ | |
141 | if((err = snd_pcm_sw_params_set_start_threshold(alsa_pcm, swparams, 1024)) < 0) | |
142 | disorder_fatal(-err, "error calling snd_pcm_sw_params_set_start_threshold"); | |
143 | if((err = snd_pcm_sw_params(alsa_pcm, swparams)) < 0) | |
144 | disorder_fatal(-err, "error calling snd_pcm_sw_params"); | |
4fd38868 RK |
145 | } |
146 | ||
4fd38868 | 147 | static void alsa_start(uaudio_callback *callback, |
06385470 | 148 | void *userdata) { |
4fd38868 | 149 | if(uaudio_channels != 1 && uaudio_channels != 2) |
2e9ba080 | 150 | disorder_fatal(0, "asked for %d channels but only support 1 or 2", |
4fd38868 RK |
151 | uaudio_channels); |
152 | if(uaudio_bits != 8 && uaudio_bits != 16) | |
2e9ba080 | 153 | disorder_fatal(0, "asked for %d bits/channel but only support 8 or 16", |
4fd38868 RK |
154 | uaudio_bits); |
155 | alsa_open(); | |
156 | uaudio_thread_start(callback, userdata, alsa_play, | |
157 | 32 / uaudio_sample_size, | |
63761c19 RK |
158 | 4096 / uaudio_sample_size, |
159 | 0); | |
4fd38868 RK |
160 | } |
161 | ||
162 | static void alsa_stop(void) { | |
163 | uaudio_thread_stop(); | |
164 | snd_pcm_close(alsa_pcm); | |
165 | alsa_pcm = 0; | |
166 | } | |
167 | ||
b50cfb8a RK |
168 | /** @brief Convert a level to a percentage */ |
169 | static int to_percent(long n) { | |
170 | return (n - alsa_mixer_min) * 100 / (alsa_mixer_max - alsa_mixer_min); | |
171 | } | |
172 | ||
42f738c2 RK |
173 | /** @brief Convert a percentage to a level */ |
174 | static int from_percent(int n) { | |
175 | return alsa_mixer_min + n * (alsa_mixer_max - alsa_mixer_min) / 100; | |
176 | } | |
177 | ||
b50cfb8a RK |
178 | static void alsa_open_mixer(void) { |
179 | int err; | |
180 | snd_mixer_selem_id_t *id; | |
181 | const char *device = uaudio_get("device", "default"); | |
182 | const char *mixer = uaudio_get("mixer-control", "0"); | |
4f96b17e | 183 | const char *channel = uaudio_get("mixer-channel", "Master"); |
b50cfb8a RK |
184 | |
185 | snd_mixer_selem_id_alloca(&id); | |
186 | if((err = snd_mixer_open(&alsa_mixer_handle, 0))) | |
2e9ba080 | 187 | disorder_fatal(0, "snd_mixer_open: %s", snd_strerror(err)); |
42f738c2 | 188 | if((err = snd_mixer_attach(alsa_mixer_handle, device))) |
2e9ba080 | 189 | disorder_fatal(0, "snd_mixer_attach %s: %s", device, snd_strerror(err)); |
b50cfb8a RK |
190 | if((err = snd_mixer_selem_register(alsa_mixer_handle, |
191 | 0/*options*/, 0/*classp*/))) | |
2e9ba080 | 192 | disorder_fatal(0, "snd_mixer_selem_register %s: %s", |
b50cfb8a RK |
193 | device, snd_strerror(err)); |
194 | if((err = snd_mixer_load(alsa_mixer_handle))) | |
2e9ba080 | 195 | disorder_fatal(0, "snd_mixer_load %s: %s", device, snd_strerror(err)); |
b50cfb8a RK |
196 | snd_mixer_selem_id_set_name(id, channel); |
197 | snd_mixer_selem_id_set_index(id, atoi(mixer)); | |
198 | if(!(alsa_mixer_elem = snd_mixer_find_selem(alsa_mixer_handle, id))) | |
2e9ba080 RK |
199 | disorder_fatal(0, "device '%s' mixer control '%s,%s' does not exist", |
200 | device, channel, mixer); | |
b50cfb8a | 201 | if(!snd_mixer_selem_has_playback_volume(alsa_mixer_elem)) |
2e9ba080 RK |
202 | disorder_fatal(0, |
203 | "device '%s' mixer control '%s,%s' has no playback volume", | |
204 | device, channel, mixer); | |
b50cfb8a RK |
205 | if(snd_mixer_selem_is_playback_mono(alsa_mixer_elem)) { |
206 | alsa_mixer_left = alsa_mixer_right = SND_MIXER_SCHN_MONO; | |
207 | } else { | |
208 | alsa_mixer_left = SND_MIXER_SCHN_FRONT_LEFT; | |
209 | alsa_mixer_right = SND_MIXER_SCHN_FRONT_RIGHT; | |
210 | } | |
211 | if(!snd_mixer_selem_has_playback_channel(alsa_mixer_elem, | |
212 | alsa_mixer_left) | |
213 | || !snd_mixer_selem_has_playback_channel(alsa_mixer_elem, | |
214 | alsa_mixer_right)) | |
2e9ba080 RK |
215 | disorder_fatal(0, "device '%s' mixer control '%s,%s' lacks required playback channels", |
216 | device, channel, mixer); | |
b50cfb8a RK |
217 | snd_mixer_selem_get_playback_volume_range(alsa_mixer_elem, |
218 | &alsa_mixer_min, &alsa_mixer_max); | |
219 | ||
220 | } | |
221 | ||
222 | static void alsa_close_mixer(void) { | |
223 | /* TODO alsa_mixer_elem */ | |
224 | if(alsa_mixer_handle) | |
225 | snd_mixer_close(alsa_mixer_handle); | |
226 | } | |
227 | ||
228 | static void alsa_get_volume(int *left, int *right) { | |
229 | long l, r; | |
230 | int err; | |
231 | ||
232 | if((err = snd_mixer_selem_get_playback_volume(alsa_mixer_elem, | |
233 | alsa_mixer_left, &l)) | |
234 | || (err = snd_mixer_selem_get_playback_volume(alsa_mixer_elem, | |
235 | alsa_mixer_right, &r))) | |
2e9ba080 RK |
236 | disorder_fatal(0, "snd_mixer_selem_get_playback_volume: %s", |
237 | snd_strerror(err)); | |
b50cfb8a RK |
238 | *left = to_percent(l); |
239 | *right = to_percent(r); | |
240 | } | |
241 | ||
242 | static void alsa_set_volume(int *left, int *right) { | |
243 | long l, r; | |
244 | int err; | |
245 | ||
246 | /* Set the volume */ | |
247 | if(alsa_mixer_left == alsa_mixer_right) { | |
248 | /* Mono output - just use the loudest */ | |
249 | if((err = snd_mixer_selem_set_playback_volume | |
250 | (alsa_mixer_elem, alsa_mixer_left, | |
251 | from_percent(*left > *right ? *left : *right)))) | |
2e9ba080 RK |
252 | disorder_fatal(0, "snd_mixer_selem_set_playback_volume: %s", |
253 | snd_strerror(err)); | |
b50cfb8a RK |
254 | } else { |
255 | /* Stereo output */ | |
256 | if((err = snd_mixer_selem_set_playback_volume | |
257 | (alsa_mixer_elem, alsa_mixer_left, from_percent(*left))) | |
258 | || (err = snd_mixer_selem_set_playback_volume | |
259 | (alsa_mixer_elem, alsa_mixer_right, from_percent(*right)))) | |
2e9ba080 RK |
260 | disorder_fatal(0, "snd_mixer_selem_set_playback_volume: %s", |
261 | snd_strerror(err)); | |
b50cfb8a RK |
262 | } |
263 | /* Read it back to see what we ended up at */ | |
264 | if((err = snd_mixer_selem_get_playback_volume(alsa_mixer_elem, | |
265 | alsa_mixer_left, &l)) | |
266 | || (err = snd_mixer_selem_get_playback_volume(alsa_mixer_elem, | |
267 | alsa_mixer_right, &r))) | |
2e9ba080 RK |
268 | disorder_fatal(0, "snd_mixer_selem_get_playback_volume: %s", |
269 | snd_strerror(err)); | |
b50cfb8a RK |
270 | *left = to_percent(l); |
271 | *right = to_percent(r); | |
272 | } | |
273 | ||
ba70caca RK |
274 | static void alsa_configure(void) { |
275 | uaudio_set("device", config->device); | |
276 | uaudio_set("mixer-control", config->mixer); | |
277 | uaudio_set("mixer-channel", config->channel); | |
278 | } | |
279 | ||
4fd38868 RK |
280 | const struct uaudio uaudio_alsa = { |
281 | .name = "alsa", | |
282 | .options = alsa_options, | |
283 | .start = alsa_start, | |
284 | .stop = alsa_stop, | |
b1f6ca8c RK |
285 | .activate = uaudio_thread_activate, |
286 | .deactivate = uaudio_thread_deactivate, | |
b50cfb8a RK |
287 | .open_mixer = alsa_open_mixer, |
288 | .close_mixer = alsa_close_mixer, | |
289 | .get_volume = alsa_get_volume, | |
290 | .set_volume = alsa_set_volume, | |
06385470 RK |
291 | .configure = alsa_configure, |
292 | .flags = UAUDIO_API_CLIENT | UAUDIO_API_SERVER, | |
4fd38868 RK |
293 | }; |
294 | ||
295 | #endif | |
296 | ||
297 | /* | |
298 | Local Variables: | |
299 | c-basic-offset:2 | |
300 | comment-column:40 | |
301 | fill-column:79 | |
302 | indent-tabs-mode:nil | |
303 | End: | |
304 | */ |