/* * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 as * published by the Free Software Foundation. See the file COPYING * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * Copyright (C) 2017 Frank van Maarseveen */ #include #include #include "common.h" #include "alsa.h" #define HISTSIZE 100 // chunks #define ADJ_ERRORTHRESHOLD 24 // ±0.5ms static snd_pcm_t * pcm; static snd_mixer_t * mix; static snd_mixer_elem_t * mixelem; static long volume_min; static long volume_max; static int availtarget; static int hist_buf[HISTSIZE]; static int * hist_ptr = hist_buf; static int *const hist_end = hist_buf + ARRAYSIZE(hist_buf); static int adj_deltaframes; static int adj_interframes; static int adj_neg_threshold; static int adj_pos_threshold; static uint16_t lastvol; static bool_t canpause = true; #define CALL(name, ...) \ do { \ int _e = name(__VA_ARGS__); \ if (_e < 0) \ die("%s: %s\n", #name, snd_strerror(_e)); \ } while (0) void alsa_volume(uint16_t vol) { long v; if (vol == lastvol) return; if (mixelem) { v = volume_min + (vol * (long long)(volume_max - volume_min) >> 16); CALL(snd_mixer_selem_set_playback_volume_all, mixelem, v); } out("volume %u\n", vol); lastvol = vol; } void alsa_open(const char *device, const struct play_param *p) { int snderr; snd_output_t *output; snd_pcm_sw_params_t *swparams; snd_pcm_hw_params_t *hwparams; snd_mixer_selem_id_t *sid; snd_mixer_elem_t *x; long lo, hi; const char *mixername; availtarget = p->bufferframes / 4; snd_pcm_sw_params_alloca(&swparams); CALL(snd_pcm_open, &pcm, device, SND_PCM_STREAM_PLAYBACK, 0); CALL(snd_pcm_set_params, pcm, SND_PCM_FORMAT_S16_LE, SND_PCM_ACCESS_RW_INTERLEAVED, PLAY_CHANNELS, PLAY_RATE, 0, p->bufferframes * 1000000LL / PLAY_RATE); CALL(snd_pcm_sw_params_current, pcm, swparams); CALL(snd_pcm_sw_params_set_start_threshold, pcm, swparams, p->bufferframes - availtarget); CALL(snd_pcm_sw_params, pcm, swparams); if (opt_v) { CALL(snd_output_stdio_attach, &output, stdout, 0); snd_pcm_dump(pcm, output); } CALL(snd_mixer_open, &mix, 0); CALL(snd_mixer_attach, mix, device); CALL(snd_mixer_selem_register, mix, NULL, NULL); CALL(snd_mixer_load, mix); snd_mixer_selem_id_alloca(&sid); for (x = snd_mixer_first_elem(mix); x; x = snd_mixer_elem_next(x)) { snd_mixer_selem_get_id(x, sid); mixername = snd_mixer_selem_id_get_name(sid); out("%s:\n", mixername); if (snd_mixer_selem_has_playback_volume(x)) { CALL(snd_mixer_selem_get_playback_volume_range, x, &lo, &hi); out(" Playback volume: %ld..%ld.\n", lo, hi); if (!strcmp(mixername, p->volume)) { volume_min = lo; volume_max = hi; mixelem = x; if (snd_mixer_selem_has_playback_switch(x)) CALL(snd_mixer_selem_set_playback_switch_all, x, true); } } if (snd_mixer_selem_has_playback_switch(x)) out(" Has playback switch(es).\n"); if (snd_mixer_selem_has_capture_volume(x)) { CALL(snd_mixer_selem_get_capture_volume_range, x, &lo, &hi); out(" Capture volume: %ld..%ld.\n", lo, hi); } if (snd_mixer_selem_has_capture_switch(x)) out(" Has capture switch(es).\n"); } if (!mixelem) err("No volume control. You may want to try the -v option.\n"); snd_pcm_hw_params_alloca(&hwparams); CALL(snd_pcm_hw_params_any, pcm, hwparams); snderr = snd_pcm_hw_params_can_pause(hwparams); if (snderr < 0) die("snd_pcm_hw_params_can_pause: %s\n", snd_strerror(snderr)); if (snderr != true) { err("No hardware pause.\n"); canpause = false; } lastvol = 1; alsa_volume(0); } void alsa_write(const void *chunk, int size) { int n; if (snd_pcm_avail(pcm) < size) { out("ALSA queue overrun.\n"); alsa_resync(); return; } n = snd_pcm_writei(pcm, chunk, size); if (n == -EPIPE) { out("ALSA queue underrun.\n"); alsa_resync(); return; } if (n != size) out("snd_pcm_writei: %d\n", n); if (n == -ESTRPIPE) snd_pcm_resume(pcm); } void alsa_resync(void) { int snderr; alsa_volume(0); // volume will be restored long before the ALSA starting threshold has been reached. snderr = snd_pcm_drop(pcm); if (snderr < 0) die("snd_pcm_drop: %s (resync)\n", snd_strerror(snderr)); snderr = snd_pcm_prepare(pcm); if (snderr < 0) die("snd_pcm_prepare: %s (resync)\n", snd_strerror(snderr)); memset(hist_buf, 0, sizeof (hist_buf)); adj_deltaframes = 0; adj_interframes = 0; adj_neg_threshold = 0; adj_pos_threshold = 0; } int alsa_syncadj(void) { int minavail, fill, adj, *h; if (snd_pcm_state(pcm) != SND_PCM_STATE_RUNNING) return 0; *hist_ptr++ = snd_pcm_avail(pcm); if (hist_ptr == hist_end) { hist_ptr = hist_buf; minavail = INT_MAX; for (h = hist_buf; h < hist_end; ++h) { if (*h && *h < minavail) minavail = *h; } fill = minavail - availtarget; if (minavail != INT_MAX && fill) { adj = fill; if (adj < 0) { if (adj >= adj_neg_threshold) adj = 0; else adj_neg_threshold = adj; if (adj_neg_threshold < -ADJ_ERRORTHRESHOLD) adj_neg_threshold = -ADJ_ERRORTHRESHOLD; } if (adj > 0) { if (adj <= adj_pos_threshold) adj = 0; else adj_pos_threshold = adj; if (adj_pos_threshold > ADJ_ERRORTHRESHOLD) adj_pos_threshold = ADJ_ERRORTHRESHOLD; } if (adj_deltaframes) out("\n"); out("err=%d -> adj=%d%s", -fill, adj, adj ? " " : "\n"); adj_deltaframes = adj; } } adj = 0; if (adj_deltaframes) { adj_interframes += PLAY_CHUNKSIZE; if (adj_interframes > PLAY_RATE / ADJ_ERRORTHRESHOLD) { adj_interframes -= PLAY_RATE / ADJ_ERRORTHRESHOLD; if (adj_deltaframes > 0) { adj = 1; out("+"); --adj_deltaframes; } else { adj = -1; out("-"); ++adj_deltaframes; } if (!adj_deltaframes) out("\n"); } } return adj; } void alsa_suspend(void) { if (canpause && snd_pcm_state(pcm) == SND_PCM_STATE_RUNNING) { CALL(snd_pcm_pause, pcm, true); out("suspend\n"); } } void alsa_resume(void) { if (canpause && snd_pcm_state(pcm) == SND_PCM_STATE_PAUSED) { CALL(snd_pcm_pause, pcm, false); out("resume\n"); } } void alsa_close(void) { CALL(snd_mixer_close, mix); CALL(snd_pcm_close, pcm); mixelem = NULL; }