├── .gitignore ├── LICENSE ├── Makefile ├── README.md └── pcm_sndio.c /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.so 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008-2012 Alexandre Ratchov 2 | Copyright (c) 2018 Duncan Overbruck 3 | 4 | Permission to use, copy, modify, and distribute this software for any 5 | purpose with or without fee is hereby granted, provided that the above 6 | copyright notice and this permission notice appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PREFIX ?= /usr/local 2 | 3 | ALSA_CFLAGS = $(shell pkg-config --cflags alsa) 4 | ALSA_LIBS = $(shell pkg-config --libs alsa) 5 | SIO_LIBS = -lsndio 6 | SOFLAGS = -Wl,-soname -Wl,libasound_module_pcm_sndio.so -Wl,-export-dynamic -Wl,-no-undefined 7 | 8 | all: libasound_module_pcm_sndio.so 9 | 10 | pcm_sndio.o: pcm_sndio.c 11 | ${CC} -c -fPIC -DPIC ${ALSA_CFLAGS} ${CFLAGS} $< 12 | 13 | libasound_module_pcm_sndio.so: pcm_sndio.o 14 | ${CC} -shared ${LDFLAGS} $^ ${LIBS} ${ALSA_LIBS} ${SIO_LIBS} -ldl ${SOFLAGS} -o $@ 15 | 16 | install: libasound_module_pcm_sndio.so 17 | install -D -m755 libasound_module_pcm_sndio.so ${DESTDIR}/${PREFIX}/lib/alsa-lib/libasound_module_pcm_sndio.so 18 | 19 | clean: 20 | rm -f *.o *.so 21 | 22 | .PONY: all install clean 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # alsa-sndio 2 | 3 | This alsa plugin provides a pcm that connects to a sndiod server 4 | as a fallback for applications that don't support sndio. 5 | 6 | At the moment only playback is supported, capturing might be added 7 | later. 8 | 9 | Each time the pcm is used a new sndio slot is created and sndio can 10 | control the volume per application. 11 | 12 | The downside of this fallback instead of using sndio directly is that 13 | the application can't have control over the sndio per slot volume 14 | because alsa has no "per application" mixers. 15 | 16 | ### Configuration 17 | 18 | The simplest `.asoundrc` just sets the default pcm to this plugin. 19 | 20 | ``` 21 | pcm.!default { 22 | type sndio 23 | } 24 | ``` 25 | 26 | The `volume` configuration option can be used to set an initial volume. 27 | 28 | The `device` is used to overwrite the default device and/or the device 29 | set by the `AUDIODEVICE` environment variable. 30 | -------------------------------------------------------------------------------- /pcm_sndio.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2008-2012 Alexandre Ratchov 3 | * Copyright (c) 2018 Duncan Overbruck 4 | * 5 | * Permission to use, copy, modify, and distribute this software for any 6 | * purpose with or without fee is hereby granted, provided that the above 7 | * copyright notice and this permission notice appear in all copies. 8 | * 9 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | */ 17 | 18 | #include 19 | #include 20 | 21 | #include 22 | 23 | #include 24 | #include 25 | 26 | #include 27 | 28 | #define ARRAY_SIZE(a) (sizeof(a)/sizeof((a)[0])) 29 | 30 | static snd_pcm_format_t cap_fmts[] = { 31 | /* XXX add s24le3 and s24be3 */ 32 | SND_PCM_FORMAT_S32_LE, SND_PCM_FORMAT_S32_BE, 33 | SND_PCM_FORMAT_S24_LE, SND_PCM_FORMAT_S24_BE, 34 | SND_PCM_FORMAT_S16_LE, SND_PCM_FORMAT_S16_BE, 35 | SND_PCM_FORMAT_U8 36 | }; 37 | 38 | static snd_pcm_access_t cap_access[] = { 39 | SND_PCM_ACCESS_RW_INTERLEAVED, 40 | }; 41 | 42 | typedef struct snd_pcm_sndio { 43 | snd_pcm_ioplug_t io; 44 | 45 | struct sio_hdl *hdl; 46 | struct sio_par par; 47 | 48 | unsigned int bpf; 49 | 50 | snd_pcm_sframes_t ptr; 51 | snd_pcm_sframes_t realptr; 52 | 53 | int started; 54 | } snd_pcm_sndio_t; 55 | 56 | static void sndio_free(snd_pcm_sndio_t *); 57 | 58 | static snd_pcm_sframes_t 59 | sndio_write(snd_pcm_ioplug_t *io, 60 | const snd_pcm_channel_area_t *areas, 61 | snd_pcm_uframes_t offset, 62 | snd_pcm_uframes_t size) 63 | { 64 | snd_pcm_sndio_t *sndio = io->private_data; 65 | unsigned int bufsz, n; 66 | char *buf; 67 | 68 | n = 0; 69 | buf = (char *)areas->addr + (areas->first + areas->step * offset) / 8; 70 | bufsz = (size * sndio->bpf); 71 | 72 | if ((n = sio_write(sndio->hdl, buf, bufsz)) <= 0) { 73 | if (sio_eof(sndio->hdl) == 1) 74 | return -EIO; 75 | return n; 76 | } 77 | 78 | sndio->ptr += n / sndio->bpf; 79 | return n / sndio->bpf; 80 | } 81 | 82 | static int 83 | sndio_delay(snd_pcm_ioplug_t *io, snd_pcm_sframes_t *delayp) 84 | { 85 | snd_pcm_sndio_t *sndio = io->private_data; 86 | *delayp = sndio->ptr - sndio->realptr; 87 | return 0; 88 | } 89 | 90 | static snd_pcm_sframes_t 91 | sndio_pointer(snd_pcm_ioplug_t *io) 92 | { 93 | snd_pcm_sndio_t *sndio = io->private_data; 94 | return sndio->ptr; 95 | } 96 | 97 | static int 98 | sndio_start(snd_pcm_ioplug_t *io) 99 | { 100 | return 0; 101 | } 102 | 103 | static int 104 | sndio_stop(snd_pcm_ioplug_t *io) 105 | { 106 | snd_pcm_sndio_t *sndio = io->private_data; 107 | if (sndio->started) { 108 | sio_stop(sndio->hdl); 109 | sndio->started = 0; 110 | } 111 | return 0; 112 | } 113 | 114 | static int 115 | sndio_close(snd_pcm_ioplug_t *io) 116 | { 117 | snd_pcm_sndio_t *sndio = io->private_data; 118 | if (sndio->started) 119 | sndio_stop(io); 120 | sndio_free(sndio); 121 | return 0; 122 | } 123 | 124 | static int 125 | sndio_drain(snd_pcm_ioplug_t *io) 126 | { 127 | snd_pcm_sndio_t *sndio = io->private_data; 128 | sio_stop(sndio->hdl); 129 | return 0; 130 | } 131 | 132 | static int 133 | sndio_prepare(snd_pcm_ioplug_t *io) 134 | { 135 | snd_pcm_sndio_t *sndio = io->private_data; 136 | 137 | sndio->ptr = 0; 138 | sndio->realptr = 0; 139 | 140 | sndio_stop(io); 141 | 142 | if (sio_start(sndio->hdl) == 0) { 143 | if (sio_eof(sndio->hdl) == 1) 144 | return -EBADFD; 145 | return -EAGAIN; 146 | } 147 | sndio->started = 1; 148 | 149 | return 0; 150 | } 151 | 152 | static int 153 | sndio_hw_constraint(snd_pcm_sndio_t *sndio) 154 | { 155 | struct sio_cap cap; 156 | 157 | snd_pcm_ioplug_t *io = &sndio->io; 158 | 159 | unsigned int cap_rates[SIO_NRATE]; 160 | unsigned int cap_chans[SIO_NCHAN]; 161 | 162 | int err, i, chan; 163 | int nchan = 0; 164 | int nrate = 0; 165 | 166 | err = sio_getcap(sndio->hdl, &cap); 167 | if (err == 0) 168 | return -EINVAL; 169 | 170 | err = snd_pcm_ioplug_set_param_list(io, 171 | SND_PCM_IOPLUG_HW_ACCESS, 172 | ARRAY_SIZE(cap_access), 173 | cap_access); 174 | if(err < 0) 175 | return err; 176 | 177 | err = snd_pcm_ioplug_set_param_list(io, 178 | SND_PCM_IOPLUG_HW_FORMAT, 179 | ARRAY_SIZE(cap_fmts), 180 | (unsigned int *)cap_fmts); 181 | if(err < 0) 182 | return err; 183 | 184 | chan = (io->stream == SND_PCM_STREAM_PLAYBACK) ? cap.confs[0].pchan : cap.confs[0].rchan; 185 | for (i = 0; i < SIO_NCHAN; i++) { 186 | if ((chan & (1 << i)) == 0) 187 | continue; 188 | cap_chans[nchan++] = (io->stream == SND_PCM_STREAM_PLAYBACK) ? cap.pchan[i] : cap.rchan[i]; 189 | } 190 | err = snd_pcm_ioplug_set_param_list(io, 191 | SND_PCM_IOPLUG_HW_CHANNELS, 192 | ARRAY_SIZE(cap_chans), 193 | cap_chans-1); 194 | if (err < 0) 195 | return err; 196 | 197 | for (i = 0; i < SIO_NRATE; i++) { 198 | if ((cap.confs[0].rate & (1 << i)) == 0) 199 | continue; 200 | cap_rates[nrate++] = cap.rate[i]; 201 | } 202 | err = snd_pcm_ioplug_set_param_list(io, 203 | SND_PCM_IOPLUG_HW_RATE, 204 | nrate-1, 205 | cap_rates); 206 | if (err < 0) 207 | return err; 208 | 209 | err = snd_pcm_ioplug_set_param_minmax(io, 210 | SND_PCM_IOPLUG_HW_BUFFER_BYTES, 211 | 64, 212 | 4 * 1024 * 1024); 213 | if (err < 0) 214 | return err; 215 | 216 | err = snd_pcm_ioplug_set_param_minmax(io, 217 | SND_PCM_IOPLUG_HW_PERIOD_BYTES, 218 | 64, 219 | 2 * 1024 * 1024); 220 | if (err < 0) 221 | return err; 222 | 223 | err = snd_pcm_ioplug_set_param_minmax(io, 224 | SND_PCM_IOPLUG_HW_PERIODS, 225 | 1, 226 | 2048); 227 | if(err < 0) 228 | return err; 229 | 230 | return 0; 231 | } 232 | 233 | static int 234 | sndio_alsa_fmttopar(snd_pcm_format_t fmt, 235 | unsigned int *bits, unsigned int *sig, unsigned int *le) 236 | { 237 | switch (fmt) { 238 | case SND_PCM_FORMAT_U8: 239 | *bits = 8; 240 | *sig = 0; 241 | break; 242 | case SND_PCM_FORMAT_S8: 243 | *bits = 8; 244 | *sig = 1; 245 | break; 246 | case SND_PCM_FORMAT_S16_LE: 247 | *bits = 16; 248 | *sig = 1; 249 | *le = 1; 250 | break; 251 | case SND_PCM_FORMAT_S16_BE: 252 | *bits = 16; 253 | *sig = 1; 254 | *le = 0; 255 | break; 256 | case SND_PCM_FORMAT_U16_LE: 257 | *bits = 16; 258 | *sig = 0; 259 | *le = 1; 260 | break; 261 | case SND_PCM_FORMAT_U16_BE: 262 | *bits = 16; 263 | *sig = 0; 264 | *le = 0; 265 | break; 266 | case SND_PCM_FORMAT_S24_LE: 267 | *bits = 24; 268 | *sig = 1; 269 | *le = 1; 270 | break; 271 | case SND_PCM_FORMAT_S24_BE: 272 | *bits = 24; 273 | *sig = 1; 274 | *le = 0; 275 | break; 276 | case SND_PCM_FORMAT_U24_LE: 277 | *bits = 24; 278 | *sig = 0; 279 | *le = 1; 280 | break; 281 | case SND_PCM_FORMAT_U24_BE: 282 | *bits = 24; 283 | *sig = 0; 284 | *le = 0; 285 | break; 286 | case SND_PCM_FORMAT_S32_LE: 287 | *bits = 32; 288 | *sig = 1; 289 | *le = 1; 290 | break; 291 | case SND_PCM_FORMAT_S32_BE: 292 | *bits = 32; 293 | *sig = 1; 294 | *le = 0; 295 | break; 296 | case SND_PCM_FORMAT_U32_LE: 297 | *bits = 32; 298 | *sig = 0; 299 | *le = 1; 300 | break; 301 | case SND_PCM_FORMAT_U32_BE: 302 | *bits = 32; 303 | *sig = 0; 304 | *le = 0; 305 | break; 306 | default: 307 | SNDERR("sndio: sndio_alsa_fmttopar: 0x%x: unsupported format\n", fmt); 308 | return 0; 309 | } 310 | return 1; 311 | } 312 | 313 | static int 314 | sndio_hw_params(snd_pcm_ioplug_t *io, 315 | snd_pcm_hw_params_t *params) 316 | { 317 | snd_pcm_sndio_t *sndio = io->private_data; 318 | struct sio_par *par, retpar; 319 | 320 | par = &sndio->par; 321 | par->pchan = io->channels; 322 | par->rchan = io->channels; 323 | 324 | if (sndio_alsa_fmttopar(io->format, &par->bits, &par->sig, &par->le) == 0) 325 | return -EINVAL; 326 | 327 | sndio->bpf = 328 | ((snd_pcm_format_physical_width(io->format) * io->channels) / 8); 329 | 330 | par->bps = SIO_BPS(par->bits); 331 | par->rate = io->rate; 332 | par->appbufsz = io->period_size; 333 | 334 | if (sio_setpar(sndio->hdl, par) == 0 || 335 | sio_getpar(sndio->hdl, &retpar) == 0) 336 | return -EINVAL; 337 | 338 | if (par->bits != retpar.bits || 339 | par->bps != retpar.bps || 340 | par->rate != retpar.rate || 341 | (par->bps > 1 && par->le != retpar.le) || 342 | (par->bits < par->bps * 8 && par->msb != retpar.msb)) 343 | return -1; 344 | 345 | return 0; 346 | } 347 | 348 | static void 349 | sndio_free(snd_pcm_sndio_t *sndio) 350 | { 351 | if (sndio->hdl) 352 | sio_close(sndio->hdl); 353 | free(sndio); 354 | } 355 | 356 | static void 357 | cb(void *arg, int delta) 358 | { 359 | snd_pcm_sndio_t *sndio = arg; 360 | sndio->realptr += delta; 361 | } 362 | 363 | static snd_pcm_ioplug_callback_t sndio_pcm_callback = { 364 | .start = sndio_start, 365 | .stop = sndio_stop, 366 | .drain = sndio_drain, 367 | .transfer = sndio_write, 368 | .pointer = sndio_pointer, 369 | .close = sndio_close, 370 | .prepare = sndio_prepare, 371 | .hw_params = sndio_hw_params, 372 | .delay = sndio_delay, 373 | }; 374 | 375 | static int 376 | sndio_open(snd_pcm_t **pcmp, const char *name, const char *device, 377 | snd_pcm_stream_t stream, int mode, long volume) 378 | { 379 | snd_pcm_sndio_t *pcm_sndio; 380 | int err; 381 | 382 | if (stream != SND_PCM_STREAM_PLAYBACK) { 383 | SNDERR("sndio: only playback streams are supported"); 384 | return -ENOTSUP; 385 | } 386 | 387 | pcm_sndio = calloc(1, sizeof *pcm_sndio); 388 | if(pcm_sndio == NULL) 389 | return -ENOMEM; 390 | 391 | pcm_sndio->hdl = sio_open(device ? device : SIO_DEVANY, 392 | stream == SND_PCM_STREAM_PLAYBACK ? SIO_PLAY : SIO_REC, 0); 393 | if (pcm_sndio->hdl == NULL) { 394 | free(pcm_sndio); 395 | return -ENOENT; 396 | } 397 | 398 | sio_onmove(pcm_sndio->hdl, cb, pcm_sndio); 399 | 400 | if (volume >= 0 && volume <= SIO_MAXVOL) { 401 | if (sio_setvol(pcm_sndio->hdl, (unsigned int)volume) == 0) 402 | SNDERR("sndio: couldn't set intial volume"); 403 | } 404 | 405 | sio_initpar(&pcm_sndio->par); 406 | 407 | pcm_sndio->io.version = SND_PCM_IOPLUG_VERSION; 408 | pcm_sndio->io.name = "ALSA <-> SNDIO PCM I/O Plugin"; 409 | pcm_sndio->io.callback = &sndio_pcm_callback; 410 | pcm_sndio->io.private_data = pcm_sndio; 411 | pcm_sndio->io.mmap_rw = 0; 412 | 413 | pcm_sndio->ptr = 0; 414 | pcm_sndio->started = 0; 415 | 416 | err = snd_pcm_ioplug_create(&pcm_sndio->io, name, stream, mode); 417 | if(err < 0) { 418 | sndio_free(pcm_sndio); 419 | return err; 420 | } 421 | 422 | err = sndio_hw_constraint(pcm_sndio); 423 | if (err < 0) { 424 | snd_pcm_ioplug_delete(&pcm_sndio->io); 425 | sndio_free(pcm_sndio); 426 | } 427 | 428 | *pcmp = pcm_sndio->io.pcm; 429 | 430 | return 0; 431 | } 432 | 433 | SND_PCM_PLUGIN_DEFINE_FUNC(sndio) 434 | { 435 | snd_config_iterator_t i, next; 436 | const char *device = NULL; 437 | int err; 438 | long volume = -1; 439 | 440 | snd_config_for_each(i, next, conf) { 441 | snd_config_t *n = snd_config_iterator_entry(i); 442 | const char *id; 443 | 444 | if (snd_config_get_id(n, &id) < 0) 445 | continue; 446 | 447 | if (strcmp(id, "comment") == 0 || strcmp(id, "type") == 0 || strcmp(id, "hint") == 0) 448 | continue; 449 | 450 | if (strcmp(id, "device") == 0) { 451 | snd_config_get_string(n, &device); 452 | continue; 453 | } 454 | 455 | if (strcmp(id, "volume") == 0) { 456 | snd_config_get_integer(n, &volume); 457 | continue; 458 | } 459 | 460 | SNDERR("Unknown field %s", id); 461 | return -EINVAL; 462 | } 463 | 464 | err = sndio_open(pcmp, name, device, stream, mode, volume); 465 | return err; 466 | } 467 | 468 | SND_PCM_PLUGIN_SYMBOL(sndio); 469 | --------------------------------------------------------------------------------