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 RK |
59 | /** @brief Actually play sound via ALSA */ |
60 | static size_t alsa_play(void *buffer, size_t samples) { | |
61 | int err; | |
62 | /* ALSA wants 'frames', where frame = several concurrently played samples */ | |
63 | const snd_pcm_uframes_t frames = samples / uaudio_channels; | |
64 | ||
65 | snd_pcm_sframes_t rc = snd_pcm_writei(alsa_pcm, buffer, frames); | |
66 | if(rc < 0) { | |
67 | switch(rc) { | |
68 | case -EPIPE: | |
69 | if((err = snd_pcm_prepare(alsa_pcm))) | |
70 | fatal(0, "error calling snd_pcm_prepare: %d", err); | |
71 | return 0; | |
72 | case -EAGAIN: | |
73 | return 0; | |
74 | default: | |
75 | fatal(0, "error calling snd_pcm_writei: %d", (int)rc); | |
76 | } | |
77 | } | |
78 | return rc * uaudio_channels; | |
79 | } | |
80 | ||
81 | /** @brief Open the ALSA sound device */ | |
82 | static void alsa_open(void) { | |
b50cfb8a | 83 | const char *device = uaudio_get("device", "default"); |
4fd38868 RK |
84 | int err; |
85 | ||
4fd38868 RK |
86 | if((err = snd_pcm_open(&alsa_pcm, |
87 | device, | |
88 | SND_PCM_STREAM_PLAYBACK, | |
89 | 0))) | |
90 | fatal(0, "error from snd_pcm_open: %d", err); | |
91 | snd_pcm_hw_params_t *hwparams; | |
92 | snd_pcm_hw_params_alloca(&hwparams); | |
93 | if((err = snd_pcm_hw_params_any(alsa_pcm, hwparams)) < 0) | |
94 | fatal(0, "error from snd_pcm_hw_params_any: %d", err); | |
95 | if((err = snd_pcm_hw_params_set_access(alsa_pcm, hwparams, | |
96 | SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) | |
97 | fatal(0, "error from snd_pcm_hw_params_set_access: %d", err); | |
98 | int sample_format; | |
99 | if(uaudio_bits == 16) | |
100 | sample_format = uaudio_signed ? SND_PCM_FORMAT_S16 : SND_PCM_FORMAT_U16; | |
101 | else | |
102 | sample_format = uaudio_signed ? SND_PCM_FORMAT_S8 : SND_PCM_FORMAT_U8; | |
103 | if((err = snd_pcm_hw_params_set_format(alsa_pcm, hwparams, | |
104 | sample_format)) < 0) | |
105 | fatal(0, "error from snd_pcm_hw_params_set_format (%d): %d", | |
106 | sample_format, err); | |
107 | unsigned rate = uaudio_rate; | |
108 | if((err = snd_pcm_hw_params_set_rate_near(alsa_pcm, hwparams, &rate, 0)) < 0) | |
109 | fatal(0, "error from snd_pcm_hw_params_set_rate_near (%d): %d", | |
110 | rate, err); | |
111 | if((err = snd_pcm_hw_params_set_channels(alsa_pcm, hwparams, | |
112 | uaudio_channels)) < 0) | |
113 | fatal(0, "error from snd_pcm_hw_params_set_channels (%d): %d", | |
114 | uaudio_channels, err); | |
115 | if((err = snd_pcm_hw_params(alsa_pcm, hwparams)) < 0) | |
116 | fatal(0, "error calling snd_pcm_hw_params: %d", err); | |
117 | ||
118 | } | |
119 | ||
120 | static void alsa_activate(void) { | |
121 | uaudio_thread_activate(); | |
122 | } | |
123 | ||
124 | static void alsa_deactivate(void) { | |
125 | uaudio_thread_deactivate(); | |
126 | } | |
127 | ||
128 | static void alsa_start(uaudio_callback *callback, | |
129 | void *userdata) { | |
130 | if(uaudio_channels != 1 && uaudio_channels != 2) | |
131 | fatal(0, "asked for %d channels but only support 1 or 2", | |
132 | uaudio_channels); | |
133 | if(uaudio_bits != 8 && uaudio_bits != 16) | |
134 | fatal(0, "asked for %d bits/channel but only support 8 or 16", | |
135 | uaudio_bits); | |
136 | alsa_open(); | |
137 | uaudio_thread_start(callback, userdata, alsa_play, | |
138 | 32 / uaudio_sample_size, | |
139 | 4096 / uaudio_sample_size); | |
140 | } | |
141 | ||
142 | static void alsa_stop(void) { | |
143 | uaudio_thread_stop(); | |
144 | snd_pcm_close(alsa_pcm); | |
145 | alsa_pcm = 0; | |
146 | } | |
147 | ||
b50cfb8a RK |
148 | /** @brief Convert a level to a percentage */ |
149 | static int to_percent(long n) { | |
150 | return (n - alsa_mixer_min) * 100 / (alsa_mixer_max - alsa_mixer_min); | |
151 | } | |
152 | ||
153 | static void alsa_open_mixer(void) { | |
154 | int err; | |
155 | snd_mixer_selem_id_t *id; | |
156 | const char *device = uaudio_get("device", "default"); | |
157 | const char *mixer = uaudio_get("mixer-control", "0"); | |
158 | const char *channel = uaudio_get("mixer-channel", "PCM"); | |
159 | ||
160 | snd_mixer_selem_id_alloca(&id); | |
161 | if((err = snd_mixer_open(&alsa_mixer_handle, 0))) | |
162 | fatal(0, "snd_mixer_open: %s", snd_strerror(err)); | |
163 | if((err = snd_mixer_attach(alsa_mixer_handle, config->device))) | |
164 | fatal(0, "snd_mixer_attach %s: %s", config->device, snd_strerror(err)); | |
165 | if((err = snd_mixer_selem_register(alsa_mixer_handle, | |
166 | 0/*options*/, 0/*classp*/))) | |
167 | fatal(0, "snd_mixer_selem_register %s: %s", | |
168 | device, snd_strerror(err)); | |
169 | if((err = snd_mixer_load(alsa_mixer_handle))) | |
170 | fatal(0, "snd_mixer_load %s: %s", device, snd_strerror(err)); | |
171 | snd_mixer_selem_id_set_name(id, channel); | |
172 | snd_mixer_selem_id_set_index(id, atoi(mixer)); | |
173 | if(!(alsa_mixer_elem = snd_mixer_find_selem(alsa_mixer_handle, id))) | |
174 | fatal(0, "device '%s' mixer control '%s,%s' does not exist", | |
175 | device, channel, mixer); | |
176 | if(!snd_mixer_selem_has_playback_volume(alsa_mixer_elem)) | |
177 | fatal(0, "device '%s' mixer control '%s,%s' has no playback volume", | |
178 | device, channel, mixer); | |
179 | if(snd_mixer_selem_is_playback_mono(alsa_mixer_elem)) { | |
180 | alsa_mixer_left = alsa_mixer_right = SND_MIXER_SCHN_MONO; | |
181 | } else { | |
182 | alsa_mixer_left = SND_MIXER_SCHN_FRONT_LEFT; | |
183 | alsa_mixer_right = SND_MIXER_SCHN_FRONT_RIGHT; | |
184 | } | |
185 | if(!snd_mixer_selem_has_playback_channel(alsa_mixer_elem, | |
186 | alsa_mixer_left) | |
187 | || !snd_mixer_selem_has_playback_channel(alsa_mixer_elem, | |
188 | alsa_mixer_right)) | |
189 | fatal(0, "device '%s' mixer control '%s,%s' lacks required playback channels", | |
190 | device, channel, mixer); | |
191 | snd_mixer_selem_get_playback_volume_range(alsa_mixer_elem, | |
192 | &alsa_mixer_min, &alsa_mixer_max); | |
193 | ||
194 | } | |
195 | ||
196 | static void alsa_close_mixer(void) { | |
197 | /* TODO alsa_mixer_elem */ | |
198 | if(alsa_mixer_handle) | |
199 | snd_mixer_close(alsa_mixer_handle); | |
200 | } | |
201 | ||
202 | static void alsa_get_volume(int *left, int *right) { | |
203 | long l, r; | |
204 | int err; | |
205 | ||
206 | if((err = snd_mixer_selem_get_playback_volume(alsa_mixer_elem, | |
207 | alsa_mixer_left, &l)) | |
208 | || (err = snd_mixer_selem_get_playback_volume(alsa_mixer_elem, | |
209 | alsa_mixer_right, &r))) | |
210 | fatal(0, "snd_mixer_selem_get_playback_volume: %s", snd_strerror(err)); | |
211 | *left = to_percent(l); | |
212 | *right = to_percent(r); | |
213 | } | |
214 | ||
215 | static void alsa_set_volume(int *left, int *right) { | |
216 | long l, r; | |
217 | int err; | |
218 | ||
219 | /* Set the volume */ | |
220 | if(alsa_mixer_left == alsa_mixer_right) { | |
221 | /* Mono output - just use the loudest */ | |
222 | if((err = snd_mixer_selem_set_playback_volume | |
223 | (alsa_mixer_elem, alsa_mixer_left, | |
224 | from_percent(*left > *right ? *left : *right)))) | |
225 | fatal(0, "snd_mixer_selem_set_playback_volume: %s", snd_strerror(err)); | |
226 | } else { | |
227 | /* Stereo output */ | |
228 | if((err = snd_mixer_selem_set_playback_volume | |
229 | (alsa_mixer_elem, alsa_mixer_left, from_percent(*left))) | |
230 | || (err = snd_mixer_selem_set_playback_volume | |
231 | (alsa_mixer_elem, alsa_mixer_right, from_percent(*right)))) | |
232 | fatal(0, "snd_mixer_selem_set_playback_volume: %s", snd_strerror(err)); | |
233 | } | |
234 | /* Read it back to see what we ended up at */ | |
235 | if((err = snd_mixer_selem_get_playback_volume(alsa_mixer_elem, | |
236 | alsa_mixer_left, &l)) | |
237 | || (err = snd_mixer_selem_get_playback_volume(alsa_mixer_elem, | |
238 | alsa_mixer_right, &r))) | |
239 | fatal(0, "snd_mixer_selem_get_playback_volume: %s", snd_strerror(err)); | |
240 | *left = to_percent(l); | |
241 | *right = to_percent(r); | |
242 | } | |
243 | ||
ba70caca RK |
244 | static void alsa_configure(void) { |
245 | uaudio_set("device", config->device); | |
246 | uaudio_set("mixer-control", config->mixer); | |
247 | uaudio_set("mixer-channel", config->channel); | |
248 | } | |
249 | ||
4fd38868 RK |
250 | const struct uaudio uaudio_alsa = { |
251 | .name = "alsa", | |
252 | .options = alsa_options, | |
253 | .start = alsa_start, | |
254 | .stop = alsa_stop, | |
255 | .activate = alsa_activate, | |
b50cfb8a RK |
256 | .deactivate = alsa_deactivate, |
257 | .open_mixer = alsa_open_mixer, | |
258 | .close_mixer = alsa_close_mixer, | |
259 | .get_volume = alsa_get_volume, | |
260 | .set_volume = alsa_set_volume, | |
ba70caca | 261 | .configure = alsa_configure |
4fd38868 RK |
262 | }; |
263 | ||
264 | #endif | |
265 | ||
266 | /* | |
267 | Local Variables: | |
268 | c-basic-offset:2 | |
269 | comment-column:40 | |
270 | fill-column:79 | |
271 | indent-tabs-mode:nil | |
272 | End: | |
273 | */ |