Commit | Line | Data |
---|---|---|
4fd38868 RK |
1 | /* |
2 | * This file is part of DisOrder. | |
3 | * Copyright (C) 2009 Richard Kettlewell | |
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); |
4fd38868 RK |
108 | snd_pcm_hw_params_t *hwparams; |
109 | snd_pcm_hw_params_alloca(&hwparams); | |
110 | if((err = snd_pcm_hw_params_any(alsa_pcm, hwparams)) < 0) | |
2e9ba080 | 111 | disorder_fatal(0, "error from snd_pcm_hw_params_any: %d", err); |
4fd38868 RK |
112 | if((err = snd_pcm_hw_params_set_access(alsa_pcm, hwparams, |
113 | SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) | |
2e9ba080 | 114 | disorder_fatal(0, "error from snd_pcm_hw_params_set_access: %d", err); |
4fd38868 RK |
115 | int sample_format; |
116 | if(uaudio_bits == 16) | |
117 | sample_format = uaudio_signed ? SND_PCM_FORMAT_S16 : SND_PCM_FORMAT_U16; | |
118 | else | |
119 | sample_format = uaudio_signed ? SND_PCM_FORMAT_S8 : SND_PCM_FORMAT_U8; | |
120 | if((err = snd_pcm_hw_params_set_format(alsa_pcm, hwparams, | |
121 | sample_format)) < 0) | |
2e9ba080 | 122 | disorder_fatal(0, "error from snd_pcm_hw_params_set_format (%d): %d", |
4fd38868 RK |
123 | sample_format, err); |
124 | unsigned rate = uaudio_rate; | |
125 | if((err = snd_pcm_hw_params_set_rate_near(alsa_pcm, hwparams, &rate, 0)) < 0) | |
2e9ba080 | 126 | disorder_fatal(0, "error from snd_pcm_hw_params_set_rate_near (%d): %d", |
4fd38868 RK |
127 | rate, err); |
128 | if((err = snd_pcm_hw_params_set_channels(alsa_pcm, hwparams, | |
129 | uaudio_channels)) < 0) | |
2e9ba080 | 130 | disorder_fatal(0, "error from snd_pcm_hw_params_set_channels (%d): %d", |
4fd38868 RK |
131 | uaudio_channels, err); |
132 | if((err = snd_pcm_hw_params(alsa_pcm, hwparams)) < 0) | |
2e9ba080 | 133 | disorder_fatal(0, "error calling snd_pcm_hw_params: %d", err); |
4fd38868 RK |
134 | |
135 | } | |
136 | ||
4fd38868 RK |
137 | static void alsa_start(uaudio_callback *callback, |
138 | void *userdata) { | |
139 | if(uaudio_channels != 1 && uaudio_channels != 2) | |
2e9ba080 | 140 | disorder_fatal(0, "asked for %d channels but only support 1 or 2", |
4fd38868 RK |
141 | uaudio_channels); |
142 | if(uaudio_bits != 8 && uaudio_bits != 16) | |
2e9ba080 | 143 | disorder_fatal(0, "asked for %d bits/channel but only support 8 or 16", |
4fd38868 RK |
144 | uaudio_bits); |
145 | alsa_open(); | |
146 | uaudio_thread_start(callback, userdata, alsa_play, | |
147 | 32 / uaudio_sample_size, | |
63761c19 RK |
148 | 4096 / uaudio_sample_size, |
149 | 0); | |
4fd38868 RK |
150 | } |
151 | ||
152 | static void alsa_stop(void) { | |
153 | uaudio_thread_stop(); | |
154 | snd_pcm_close(alsa_pcm); | |
155 | alsa_pcm = 0; | |
156 | } | |
157 | ||
b50cfb8a RK |
158 | /** @brief Convert a level to a percentage */ |
159 | static int to_percent(long n) { | |
160 | return (n - alsa_mixer_min) * 100 / (alsa_mixer_max - alsa_mixer_min); | |
161 | } | |
162 | ||
42f738c2 RK |
163 | /** @brief Convert a percentage to a level */ |
164 | static int from_percent(int n) { | |
165 | return alsa_mixer_min + n * (alsa_mixer_max - alsa_mixer_min) / 100; | |
166 | } | |
167 | ||
b50cfb8a RK |
168 | static void alsa_open_mixer(void) { |
169 | int err; | |
170 | snd_mixer_selem_id_t *id; | |
171 | const char *device = uaudio_get("device", "default"); | |
172 | const char *mixer = uaudio_get("mixer-control", "0"); | |
173 | const char *channel = uaudio_get("mixer-channel", "PCM"); | |
174 | ||
175 | snd_mixer_selem_id_alloca(&id); | |
176 | if((err = snd_mixer_open(&alsa_mixer_handle, 0))) | |
2e9ba080 | 177 | disorder_fatal(0, "snd_mixer_open: %s", snd_strerror(err)); |
42f738c2 | 178 | if((err = snd_mixer_attach(alsa_mixer_handle, device))) |
2e9ba080 | 179 | disorder_fatal(0, "snd_mixer_attach %s: %s", device, snd_strerror(err)); |
b50cfb8a RK |
180 | if((err = snd_mixer_selem_register(alsa_mixer_handle, |
181 | 0/*options*/, 0/*classp*/))) | |
2e9ba080 | 182 | disorder_fatal(0, "snd_mixer_selem_register %s: %s", |
b50cfb8a RK |
183 | device, snd_strerror(err)); |
184 | if((err = snd_mixer_load(alsa_mixer_handle))) | |
2e9ba080 | 185 | disorder_fatal(0, "snd_mixer_load %s: %s", device, snd_strerror(err)); |
b50cfb8a RK |
186 | snd_mixer_selem_id_set_name(id, channel); |
187 | snd_mixer_selem_id_set_index(id, atoi(mixer)); | |
188 | if(!(alsa_mixer_elem = snd_mixer_find_selem(alsa_mixer_handle, id))) | |
2e9ba080 RK |
189 | disorder_fatal(0, "device '%s' mixer control '%s,%s' does not exist", |
190 | device, channel, mixer); | |
b50cfb8a | 191 | if(!snd_mixer_selem_has_playback_volume(alsa_mixer_elem)) |
2e9ba080 RK |
192 | disorder_fatal(0, |
193 | "device '%s' mixer control '%s,%s' has no playback volume", | |
194 | device, channel, mixer); | |
b50cfb8a RK |
195 | if(snd_mixer_selem_is_playback_mono(alsa_mixer_elem)) { |
196 | alsa_mixer_left = alsa_mixer_right = SND_MIXER_SCHN_MONO; | |
197 | } else { | |
198 | alsa_mixer_left = SND_MIXER_SCHN_FRONT_LEFT; | |
199 | alsa_mixer_right = SND_MIXER_SCHN_FRONT_RIGHT; | |
200 | } | |
201 | if(!snd_mixer_selem_has_playback_channel(alsa_mixer_elem, | |
202 | alsa_mixer_left) | |
203 | || !snd_mixer_selem_has_playback_channel(alsa_mixer_elem, | |
204 | alsa_mixer_right)) | |
2e9ba080 RK |
205 | disorder_fatal(0, "device '%s' mixer control '%s,%s' lacks required playback channels", |
206 | device, channel, mixer); | |
b50cfb8a RK |
207 | snd_mixer_selem_get_playback_volume_range(alsa_mixer_elem, |
208 | &alsa_mixer_min, &alsa_mixer_max); | |
209 | ||
210 | } | |
211 | ||
212 | static void alsa_close_mixer(void) { | |
213 | /* TODO alsa_mixer_elem */ | |
214 | if(alsa_mixer_handle) | |
215 | snd_mixer_close(alsa_mixer_handle); | |
216 | } | |
217 | ||
218 | static void alsa_get_volume(int *left, int *right) { | |
219 | long l, r; | |
220 | int err; | |
221 | ||
222 | if((err = snd_mixer_selem_get_playback_volume(alsa_mixer_elem, | |
223 | alsa_mixer_left, &l)) | |
224 | || (err = snd_mixer_selem_get_playback_volume(alsa_mixer_elem, | |
225 | alsa_mixer_right, &r))) | |
2e9ba080 RK |
226 | disorder_fatal(0, "snd_mixer_selem_get_playback_volume: %s", |
227 | snd_strerror(err)); | |
b50cfb8a RK |
228 | *left = to_percent(l); |
229 | *right = to_percent(r); | |
230 | } | |
231 | ||
232 | static void alsa_set_volume(int *left, int *right) { | |
233 | long l, r; | |
234 | int err; | |
235 | ||
236 | /* Set the volume */ | |
237 | if(alsa_mixer_left == alsa_mixer_right) { | |
238 | /* Mono output - just use the loudest */ | |
239 | if((err = snd_mixer_selem_set_playback_volume | |
240 | (alsa_mixer_elem, alsa_mixer_left, | |
241 | from_percent(*left > *right ? *left : *right)))) | |
2e9ba080 RK |
242 | disorder_fatal(0, "snd_mixer_selem_set_playback_volume: %s", |
243 | snd_strerror(err)); | |
b50cfb8a RK |
244 | } else { |
245 | /* Stereo output */ | |
246 | if((err = snd_mixer_selem_set_playback_volume | |
247 | (alsa_mixer_elem, alsa_mixer_left, from_percent(*left))) | |
248 | || (err = snd_mixer_selem_set_playback_volume | |
249 | (alsa_mixer_elem, alsa_mixer_right, from_percent(*right)))) | |
2e9ba080 RK |
250 | disorder_fatal(0, "snd_mixer_selem_set_playback_volume: %s", |
251 | snd_strerror(err)); | |
b50cfb8a RK |
252 | } |
253 | /* Read it back to see what we ended up at */ | |
254 | if((err = snd_mixer_selem_get_playback_volume(alsa_mixer_elem, | |
255 | alsa_mixer_left, &l)) | |
256 | || (err = snd_mixer_selem_get_playback_volume(alsa_mixer_elem, | |
257 | alsa_mixer_right, &r))) | |
2e9ba080 RK |
258 | disorder_fatal(0, "snd_mixer_selem_get_playback_volume: %s", |
259 | snd_strerror(err)); | |
b50cfb8a RK |
260 | *left = to_percent(l); |
261 | *right = to_percent(r); | |
262 | } | |
263 | ||
ba70caca RK |
264 | static void alsa_configure(void) { |
265 | uaudio_set("device", config->device); | |
266 | uaudio_set("mixer-control", config->mixer); | |
267 | uaudio_set("mixer-channel", config->channel); | |
268 | } | |
269 | ||
4fd38868 RK |
270 | const struct uaudio uaudio_alsa = { |
271 | .name = "alsa", | |
272 | .options = alsa_options, | |
273 | .start = alsa_start, | |
274 | .stop = alsa_stop, | |
b1f6ca8c RK |
275 | .activate = uaudio_thread_activate, |
276 | .deactivate = uaudio_thread_deactivate, | |
b50cfb8a RK |
277 | .open_mixer = alsa_open_mixer, |
278 | .close_mixer = alsa_close_mixer, | |
279 | .get_volume = alsa_get_volume, | |
280 | .set_volume = alsa_set_volume, | |
ba70caca | 281 | .configure = alsa_configure |
4fd38868 RK |
282 | }; |
283 | ||
284 | #endif | |
285 | ||
286 | /* | |
287 | Local Variables: | |
288 | c-basic-offset:2 | |
289 | comment-column:40 | |
290 | fill-column:79 | |
291 | indent-tabs-mode:nil | |
292 | End: | |
293 | */ |