blob: e8a630500d318c3dd44e9428e23cd2fbd9f50112 [file] [log] [blame] [edit]
/*
* 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-2023 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 <dave@mielke.cc>.
*/
#include "prologue.h"
#include <sys/asoundlib.h>
#include "log.h"
#include "io_misc.h"
#include "pcm.h"
struct PcmDeviceStruct {
int card;
int device;
snd_pcm_t *handle;
snd_pcm_channel_params_t parameters;
};
static void
logPcmError (int level, const char *action, int code) {
logMessage(level, "QSA PCM %s error: %s", action, snd_strerror(code));
}
static int
reconfigurePcmChannel (PcmDevice *pcm, int errorLevel) {
int code;
if ((code = snd_pcm_channel_params(pcm->handle, &pcm->parameters)) >= 0) {
snd_pcm_channel_setup_t setup;
setup.channel = pcm->parameters.channel;
if ((code = snd_pcm_channel_setup(pcm->handle, &setup)) >= 0) {
pcm->parameters.mode = setup.mode;
pcm->parameters.format = setup.format;
pcm->parameters.buf.block.frag_size = setup.buf.block.frag_size;
pcm->parameters.buf.block.frags_min = setup.buf.block.frags_min;
pcm->parameters.buf.block.frags_max = setup.buf.block.frags_max;
return 1;
} else {
logPcmError(errorLevel, "get channel setup", code);
}
} else {
logPcmError(errorLevel, "set channel parameters", code);
}
return 0;
}
PcmDevice *
openPcmDevice (int errorLevel, const char *device) {
PcmDevice *pcm;
if ((pcm = malloc(sizeof(*pcm)))) {
int code;
if (*device) {
{
int ok = 0;
long number;
char *end;
const char *component = device;
number = strtol(component, &end, 0);
if ((*end && (*end != ':')) || (number < 0) || (number > 0XFF)) {
logMessage(errorLevel, "Invalid QSA card number: %s", device);
} else if (end == component) {
logMessage(errorLevel, "Missing QSA card number: %s", device);
} else {
pcm->card = number;
if (*end) {
component = end + 1;
number = strtol(component, &end, 0);
if (*end || (number < 0) || (number > 0XFF)) {
logMessage(errorLevel, "Invalid QSA device number: %s", device);
} else if (end == component) {
logMessage(errorLevel, "Missing QSA device number: %s", device);
} else {
pcm->device = number;
ok = 1;
}
} else {
pcm->device = 0;
ok = 1;
}
}
if (!ok) goto openError;
}
if ((code = snd_pcm_open(&pcm->handle, pcm->card, pcm->device, SND_PCM_OPEN_PLAYBACK)) < 0) {
logPcmError(errorLevel, "open", code);
goto openError;
}
} else if ((code = snd_pcm_open_preferred(&pcm->handle, &pcm->card, &pcm->device, SND_PCM_OPEN_PLAYBACK)) < 0) {
logPcmError(errorLevel, "preferred open", code);
goto openError;
}
logMessage(LOG_DEBUG, "QSA PCM device opened: %d:%d", pcm->card, pcm->device);
{
snd_pcm_channel_info_t info;
info.channel = SND_PCM_CHANNEL_PLAYBACK;
if ((code = snd_pcm_channel_info(pcm->handle, &info)) >= 0) {
logMessage(LOG_DEBUG, "QSA PCM Info: Frag=%d-%d Rate=%d-%d Chan=%d-%d",
info.min_fragment_size, info.max_fragment_size,
info.min_rate, info.max_rate,
info.min_voices, info.max_voices);
memset(&pcm->parameters, 0, sizeof(pcm->parameters));
pcm->parameters.channel = info.channel;
pcm->parameters.start_mode = SND_PCM_START_DATA;
pcm->parameters.stop_mode = SND_PCM_STOP_ROLLOVER;
switch (pcm->parameters.mode = SND_PCM_MODE_BLOCK) {
case SND_PCM_MODE_BLOCK:
pcm->parameters.buf.block.frag_size = MIN(MAX(0X400, info.min_fragment_size), info.max_fragment_size);
pcm->parameters.buf.block.frags_min = 1;
pcm->parameters.buf.block.frags_max = 0X40;
break;
default:
logMessage(LOG_WARNING, "Unsupported QSA PCM mode: %d", pcm->parameters.mode);
goto openError;
}
pcm->parameters.format.interleave = 1;
pcm->parameters.format.rate = info.max_rate;
pcm->parameters.format.voices = MIN(MAX(1, info.min_voices), info.max_voices);
pcm->parameters.format.format = SND_PCM_SFMT_S16;
if (reconfigurePcmChannel(pcm, errorLevel)) {
if ((code = snd_pcm_channel_prepare(pcm->handle, pcm->parameters.channel)) >= 0) {
return pcm;
} else {
logPcmError(errorLevel, "prepare channel", code);
}
}
} else {
logPcmError(errorLevel, "get channel information", code);
}
}
openError:
free(pcm);
} else {
logSystemError("PCM device allocation");
}
return NULL;
}
void
closePcmDevice (PcmDevice *pcm) {
int code;
if ((code = snd_pcm_close(pcm->handle)) < 0) {
logPcmError(LOG_WARNING, "close", code);
}
free(pcm);
}
int
writePcmData (PcmDevice *pcm, const unsigned char *buffer, int count) {
return writeFile(snd_pcm_file_descriptor(pcm->handle, pcm->parameters.channel), buffer, count);
}
int
getPcmBlockSize (PcmDevice *pcm) {
return pcm->parameters.buf.block.frag_size;
}
int
getPcmSampleRate (PcmDevice *pcm) {
return pcm->parameters.format.rate;
}
int
setPcmSampleRate (PcmDevice *pcm, int rate) {
pcm->parameters.format.rate = rate;
reconfigurePcmChannel(pcm, LOG_WARNING);
return getPcmSampleRate(pcm);
}
int
getPcmChannelCount (PcmDevice *pcm) {
return pcm->parameters.format.voices;
}
int
setPcmChannelCount (PcmDevice *pcm, int channels) {
pcm->parameters.format.voices = channels;
reconfigurePcmChannel(pcm, LOG_WARNING);
return getPcmChannelCount(pcm);
}
typedef struct {
PcmAmplitudeFormat internal;
int external;
} AmplitudeFormatEntry;
static const AmplitudeFormatEntry amplitudeFormatTable[] = {
{PCM_FMT_U8 , SND_PCM_SFMT_U8 },
{PCM_FMT_S8 , SND_PCM_SFMT_S8 },
{PCM_FMT_U16B , SND_PCM_SFMT_U16_BE },
{PCM_FMT_S16B , SND_PCM_SFMT_S16_BE },
{PCM_FMT_U16L , SND_PCM_SFMT_U16_LE },
{PCM_FMT_S16L , SND_PCM_SFMT_S16_LE },
{PCM_FMT_ULAW , SND_PCM_SFMT_MU_LAW },
{PCM_FMT_UNKNOWN, SND_PCM_SFMT_SPECIAL}
};
PcmAmplitudeFormat
getPcmAmplitudeFormat (PcmDevice *pcm) {
const AmplitudeFormatEntry *entry = amplitudeFormatTable;
while (entry->internal != PCM_FMT_UNKNOWN) {
if (entry->external == pcm->parameters.format.format) break;
++entry;
}
return entry->internal;
}
PcmAmplitudeFormat
setPcmAmplitudeFormat (PcmDevice *pcm, PcmAmplitudeFormat format) {
const AmplitudeFormatEntry *entry = amplitudeFormatTable;
while (entry->internal != PCM_FMT_UNKNOWN) {
if (entry->internal == format) {
pcm->parameters.format.format = format;
reconfigurePcmChannel(pcm, LOG_WARNING);
break;
}
++entry;
}
return getPcmAmplitudeFormat(pcm);
}
void
pushPcmOutput (PcmDevice *pcm) {
}
void
awaitPcmOutput (PcmDevice *pcm) {
int code;
if ((code = snd_pcm_playback_flush(pcm->handle)) < 0) {
logPcmError(LOG_WARNING, "flush", code);
}
}
void
cancelPcmOutput (PcmDevice *pcm) {
int code;
if ((code = snd_pcm_playback_drain(pcm->handle)) < 0) {
logPcmError(LOG_WARNING, "drain", code);
}
}