/* * BRLTTY - A background process providing access to the console screen (when in * text mode) for a blind person using a refreshable braille display. * * Copyright (C) 1995-2021 by The BRLTTY Developers. * * BRLTTY comes with ABSOLUTELY NO WARRANTY. * * This is free software, placed under the terms of the * GNU Lesser General Public License, as published by the Free Software * Foundation; either version 2.1 of the License, or (at your option) any * later version. Please see the file LICENSE-LGPL for details. * * Web Page: http://brltty.app/ * * This software is maintained by Dave Mielke . */ #include "prologue.h" #define ALSA_PCM_NEW_HW_PARAMS_API #include #include "log.h" #include "timing.h" #include "pcm.h" struct PcmDeviceStruct { snd_pcm_t *handle; snd_pcm_hw_params_t *hardwareParameters; unsigned int channelCount; unsigned int sampleRate; unsigned int bufferTime; unsigned int periodTime; }; static void logPcmError (int level, const char *action, int code) { logMessage(level, "ALSA PCM %s error: %s", action, snd_strerror(code)); } static int configurePcmSampleFormat (PcmDevice *pcm, int errorLevel) { static const snd_pcm_format_t formats[] = { SND_PCM_FORMAT_S16, SND_PCM_FORMAT_U16, SND_PCM_FORMAT_U8, SND_PCM_FORMAT_S8, SND_PCM_FORMAT_MU_LAW, SND_PCM_FORMAT_UNKNOWN }; const snd_pcm_format_t *format = formats; while (*format != SND_PCM_FORMAT_UNKNOWN) { int result = snd_pcm_hw_params_set_format(pcm->handle, pcm->hardwareParameters, *format); if (result >= 0) return 1; if (result != -EINVAL) { logPcmError(errorLevel, "set format", result); return 0; } ++format; } logMessage(errorLevel, "Unsupported PCM sample format."); return 0; } static int configurePcmSampleRate (PcmDevice *pcm, int errorLevel) { int result; unsigned int minimum; unsigned int maximum; if ((result = snd_pcm_hw_params_get_rate_min(pcm->hardwareParameters, &minimum, NULL)) < 0) { logPcmError(errorLevel, "get rate min", result); return 0; } if ((result = snd_pcm_hw_params_get_rate_max(pcm->hardwareParameters, &maximum, NULL)) < 0) { logPcmError(errorLevel, "get rate max", result); return 0; } if ((minimum > maximum) || (minimum < 1)) { logMessage(errorLevel, "Invalid PCM rate range: %u-%u", minimum, maximum); return 0; } pcm->sampleRate = MIN(MAX(16000, minimum), maximum); if ((result = snd_pcm_hw_params_set_rate_near(pcm->handle, pcm->hardwareParameters, &pcm->sampleRate, NULL)) < 0) { logPcmError(errorLevel, "set rate near", result); return 0; } return 1; } static int configurePcmChannelCount (PcmDevice *pcm, int errorLevel) { int result; unsigned int minimum; unsigned int maximum; if ((result = snd_pcm_hw_params_get_channels_min(pcm->hardwareParameters, &minimum)) < 0) { logPcmError(errorLevel, "get channels min", result); return 0; } if ((result = snd_pcm_hw_params_get_channels_max(pcm->hardwareParameters, &maximum)) < 0) { logPcmError(errorLevel, "get channels max", result); return 0; } if ((minimum > maximum) || (minimum < 1)) { logMessage(errorLevel, "Invalid PCM channel range: %u-%u", minimum, maximum); return 0; } pcm->channelCount = minimum; if ((result = snd_pcm_hw_params_set_channels_near(pcm->handle, pcm->hardwareParameters, &pcm->channelCount)) < 0) { logPcmError(errorLevel, "set channels near", result); return 0; } return 1; } PcmDevice * openPcmDevice (int errorLevel, const char *device) { PcmDevice *pcm; if ((pcm = malloc(sizeof(*pcm)))) { int result; if (!*device) device = "default"; if ((result = snd_pcm_open(&pcm->handle, device, SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK)) >= 0) { snd_pcm_nonblock(pcm->handle, 0); if ((result = snd_pcm_hw_params_malloc(&pcm->hardwareParameters)) >= 0) { if ((result = snd_pcm_hw_params_any(pcm->handle, pcm->hardwareParameters)) >= 0) { if ((result = snd_pcm_hw_params_set_access(pcm->handle, pcm->hardwareParameters, SND_PCM_ACCESS_RW_INTERLEAVED)) >= 0) { if (configurePcmSampleFormat(pcm, errorLevel)) { if (configurePcmSampleRate(pcm, errorLevel)) { if (configurePcmChannelCount(pcm, errorLevel)) { pcm->bufferTime = 500000; if ((result = snd_pcm_hw_params_set_buffer_time_near(pcm->handle, pcm->hardwareParameters, &pcm->bufferTime, NULL)) >= 0) { pcm->periodTime = pcm->bufferTime / 8; if ((result = snd_pcm_hw_params_set_period_time_near(pcm->handle, pcm->hardwareParameters, &pcm->periodTime, NULL)) >= 0) { if ((result = snd_pcm_hw_params(pcm->handle, pcm->hardwareParameters)) >= 0) { logMessage(LOG_DEBUG, "ALSA PCM: Chan=%u Rate=%u BufTim=%u PerTim=%u", pcm->channelCount, pcm->sampleRate, pcm->bufferTime, pcm->periodTime); return pcm; } else { logPcmError(errorLevel, "set hardware parameters", result); } } else { logPcmError(errorLevel, "set period time near", result); } } else { logPcmError(errorLevel, "set buffer time near", result); } } } } } else { logPcmError(errorLevel, "set access", result); } } else { logPcmError(errorLevel, "get hardware parameters", result); } snd_pcm_hw_params_free(pcm->hardwareParameters); } else { logPcmError(errorLevel, "hardware parameters allocation", result); } snd_pcm_close(pcm->handle); } else { logPcmError(errorLevel, "open", result); } free(pcm); } else { logSystemError("PCM device allocation"); } return NULL; } void closePcmDevice (PcmDevice *pcm) { awaitPcmOutput(pcm); snd_pcm_close(pcm->handle); snd_pcm_hw_params_free(pcm->hardwareParameters); free(pcm); } static int getPcmFrameSize (PcmDevice *pcm) { return getPcmChannelCount(pcm) * (snd_pcm_hw_params_get_sbits(pcm->hardwareParameters) / 8); } int writePcmData (PcmDevice *pcm, const unsigned char *buffer, int count) { int frameSize = getPcmFrameSize(pcm); int framesLeft = count / frameSize; while (framesLeft > 0) { int result; if ((result = snd_pcm_writei(pcm->handle, buffer, framesLeft)) > 0) { framesLeft -= result; buffer += result * frameSize; } else { switch (result) { case -EPIPE: if ((result = snd_pcm_prepare(pcm->handle)) < 0) { logPcmError(LOG_WARNING, "underrun recovery - prepare", result); return 0; } continue; case -ESTRPIPE: while ((result = snd_pcm_resume(pcm->handle)) == -EAGAIN) approximateDelay(1); if (result < 0) { if ((result = snd_pcm_prepare(pcm->handle)) < 0) { logPcmError(LOG_WARNING, "resume - prepare", result); return 0; } } continue; } } } return 1; } int getPcmBlockSize (PcmDevice *pcm) { snd_pcm_uframes_t frames; int result; if ((result = snd_pcm_hw_params_get_period_size(pcm->hardwareParameters, &frames, NULL)) >= 0) { return frames * getPcmFrameSize(pcm); } else { logPcmError(LOG_ERR, "get period size", result); } return 65535; } int getPcmSampleRate (PcmDevice *pcm) { return pcm->sampleRate; } int setPcmSampleRate (PcmDevice *pcm, int rate) { int result; pcm->sampleRate = rate; if ((result = snd_pcm_hw_params_set_rate_near(pcm->handle, pcm->hardwareParameters, &pcm->sampleRate, NULL)) < 0) { logPcmError(LOG_ERR, "set rate near", result); } return getPcmSampleRate(pcm); } int getPcmChannelCount (PcmDevice *pcm) { return pcm->channelCount; } int setPcmChannelCount (PcmDevice *pcm, int channels) { int result; pcm->channelCount = channels; if ((result = snd_pcm_hw_params_set_channels_near(pcm->handle, pcm->hardwareParameters, &pcm->channelCount)) < 0) { logPcmError(LOG_ERR, "set channels near", result); } return getPcmChannelCount(pcm); } typedef struct { PcmAmplitudeFormat internal; snd_pcm_format_t external; } AmplitudeFormatEntry; static const AmplitudeFormatEntry amplitudeFormatTable[] = { {PCM_FMT_U8 , SND_PCM_FORMAT_U8 }, {PCM_FMT_S8 , SND_PCM_FORMAT_S8 }, {PCM_FMT_U16B , SND_PCM_FORMAT_U16_BE }, {PCM_FMT_S16B , SND_PCM_FORMAT_S16_BE }, {PCM_FMT_U16L , SND_PCM_FORMAT_U16_LE }, {PCM_FMT_S16L , SND_PCM_FORMAT_S16_LE }, {PCM_FMT_ULAW , SND_PCM_FORMAT_MU_LAW }, {PCM_FMT_UNKNOWN, SND_PCM_FORMAT_UNKNOWN} }; PcmAmplitudeFormat getPcmAmplitudeFormat (PcmDevice *pcm) { snd_pcm_format_t format; int result; if ((result = snd_pcm_hw_params_get_format(pcm->hardwareParameters, &format)) < 0) { logPcmError(LOG_ERR, "get format", result); } else { const AmplitudeFormatEntry *entry = amplitudeFormatTable; while (entry->internal != PCM_FMT_UNKNOWN) { if (entry->external == format) return entry->internal; ++entry; } } return PCM_FMT_UNKNOWN; } PcmAmplitudeFormat setPcmAmplitudeFormat (PcmDevice *pcm, PcmAmplitudeFormat format) { const AmplitudeFormatEntry *entry = amplitudeFormatTable; int result; while (entry->internal != PCM_FMT_UNKNOWN) { if (entry->internal == format) break; ++entry; } if ((result = snd_pcm_hw_params_set_format(pcm->handle, pcm->hardwareParameters, entry->external)) < 0) { logPcmError(LOG_ERR, "set format", result); return getPcmAmplitudeFormat(pcm); } return entry->internal; } void pushPcmOutput (PcmDevice *pcm) { } void awaitPcmOutput (PcmDevice *pcm) { int result; if ((result = snd_pcm_drain(pcm->handle)) < 0) logPcmError(LOG_WARNING, "drain", result); } void cancelPcmOutput (PcmDevice *pcm) { int result; if ((result = snd_pcm_drop(pcm->handle)) < 0) logPcmError(LOG_WARNING, "drop", result); }