562 lines
15 KiB
562 lines
15 KiB
/*
|
|
|
|
Copyright (C) 2000,2001 Jozef Kosoru
|
|
jozef.kosoru@pobox.sk
|
|
(C) 2000,2001 Stefan Westerfeld
|
|
stefan@space.twc.de
|
|
|
|
This library is free software; you can redistribute it and/or
|
|
modify it under the terms of the GNU Library General Public
|
|
License as published by the Free Software Foundation; either
|
|
version 2 of the License, or (at your option) any later version.
|
|
|
|
This library 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
|
|
Library General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Library General Public License
|
|
along with this library; see the file COPYING.LIB. If not, write to
|
|
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
Boston, MA 02110-1301, USA.
|
|
|
|
*/
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include <config.h>
|
|
#endif
|
|
|
|
/**
|
|
* only compile 'alsa' AudioIO class if configure thinks it is a good idea
|
|
*/
|
|
#ifdef HAVE_LIBASOUND
|
|
|
|
#ifdef HAVE_ALSA_ASOUNDLIB_H
|
|
#include <alsa/asoundlib.h>
|
|
#elif defined(HAVE_SYS_ASOUNDLIB_H)
|
|
#include <sys/asoundlib.h>
|
|
#endif
|
|
|
|
#include <sys/types.h>
|
|
#include <sys/ioctl.h>
|
|
#include <sys/time.h>
|
|
#include <sys/stat.h>
|
|
|
|
#ifdef HAVE_SYS_SELECT_H
|
|
#include <sys/select.h> // Needed on some systems.
|
|
#endif
|
|
|
|
#include <assert.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <unistd.h>
|
|
#include <iostream>
|
|
#include <algorithm>
|
|
|
|
#include "debug.h"
|
|
#include "audioio.h"
|
|
|
|
namespace Arts {
|
|
|
|
class AudioIOALSA : public AudioIO {
|
|
protected:
|
|
int audio_read_fd;
|
|
int audio_write_fd;
|
|
int requestedFragmentSize;
|
|
int requestedFragmentCount;
|
|
|
|
enum BufferMode{block, stream};
|
|
int m_card;
|
|
int m_device;
|
|
int m_format;
|
|
BufferMode m_bufferMode;
|
|
|
|
snd_pcm_t *m_pcm_handle;
|
|
snd_pcm_channel_info_t m_cinfo;
|
|
snd_pcm_format_t m_cformat;
|
|
snd_pcm_channel_params_t m_params;
|
|
snd_pcm_channel_setup_t m_setup;
|
|
|
|
int setPcmParams(const int channel);
|
|
void checkCapabilities();
|
|
|
|
public:
|
|
AudioIOALSA();
|
|
|
|
void setParam(AudioParam param, int& value);
|
|
int getParam(AudioParam param);
|
|
|
|
bool open();
|
|
void close();
|
|
int read(void *buffer, int size);
|
|
int write(void *buffer, int size);
|
|
};
|
|
|
|
REGISTER_AUDIO_IO(AudioIOALSA,"alsa","Advanced Linux Sound Architecture");
|
|
};
|
|
|
|
using namespace std;
|
|
using namespace Arts;
|
|
|
|
AudioIOALSA::AudioIOALSA()
|
|
{
|
|
param(samplingRate) = 44100;
|
|
paramStr(deviceName) = "/dev/dsp"; //!! alsa doesn't need this
|
|
requestedFragmentSize = param(fragmentSize) = 1024;
|
|
requestedFragmentCount = param(fragmentCount) = 7;
|
|
param(channels) = 2;
|
|
param(direction) = directionWrite;
|
|
|
|
/*
|
|
* default parameters
|
|
*/
|
|
m_card = snd_defaults_pcm_card(); //!! need interface !!
|
|
m_device = snd_defaults_pcm_device(); //!!
|
|
#ifdef WORDS_BIGENDIAN
|
|
m_format = SND_PCM_SFMT_S16_BE;
|
|
#else
|
|
m_format = SND_PCM_SFMT_S16_LE;
|
|
#endif
|
|
m_bufferMode = block; //block/stream (stream mode doesn't work yet)
|
|
|
|
if(m_card >= 0) {
|
|
char* cardname = 0;
|
|
|
|
if(snd_card_get_name(m_card, &cardname) == 0 && cardname != 0)
|
|
{
|
|
//!! thats not what devicename is intended to do
|
|
//!! devicename is an input information into
|
|
//!! the "driver", to select which card to use
|
|
//!! not an output information
|
|
paramStr(deviceName) = cardname;
|
|
free(cardname);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool AudioIOALSA::open()
|
|
{
|
|
string& _error = paramStr(lastError);
|
|
string& _deviceName = paramStr(deviceName);
|
|
int& _channels = param(channels);
|
|
int& _fragmentSize = param(fragmentSize);
|
|
int& _fragmentCount = param(fragmentCount);
|
|
int& _samplingRate = param(samplingRate);
|
|
int& _direction = param(direction);
|
|
int& _format = param(format);
|
|
|
|
/*
|
|
* initialize format - TODO: implement fallback (i.e. if no format given,
|
|
* it should try 16bit first, then fall back to 8bit)
|
|
*/
|
|
switch(_format)
|
|
{
|
|
default: _format = 16;
|
|
|
|
case 16: // 16bit, signed little endian
|
|
m_format = SND_PCM_SFMT_S16_LE;
|
|
break;
|
|
|
|
case 17: // 16bit, signed big endian
|
|
m_format = SND_PCM_SFMT_S16_BE;
|
|
break;
|
|
|
|
case 8: // 8bit, unsigned
|
|
m_format = SND_PCM_SFMT_U8;
|
|
break;
|
|
}
|
|
|
|
/* open pcm device */
|
|
int mode = SND_PCM_OPEN_NONBLOCK;
|
|
|
|
if(_direction == directionReadWrite)
|
|
mode |= SND_PCM_OPEN_DUPLEX;
|
|
else if(_direction == directionWrite)
|
|
mode |= SND_PCM_OPEN_PLAYBACK;
|
|
else
|
|
{
|
|
_error = "invalid direction";
|
|
return false;
|
|
}
|
|
|
|
int err;
|
|
if((err = snd_pcm_open(&m_pcm_handle, m_card, m_device, mode)) < 0) {
|
|
_error = "device: ";
|
|
_error += _deviceName.c_str();
|
|
_error += " can't be opened (";
|
|
_error += snd_strerror(err);
|
|
_error += ")";
|
|
return false;
|
|
}
|
|
else {
|
|
artsdebug("ALSA driver: %s", _deviceName.c_str());
|
|
}
|
|
|
|
snd_pcm_nonblock_mode(m_pcm_handle, 0);
|
|
|
|
/* flush buffers */
|
|
(void)snd_pcm_capture_flush(m_pcm_handle);
|
|
if(_direction & directionRead)
|
|
(void)snd_pcm_channel_flush(m_pcm_handle, SND_PCM_CHANNEL_CAPTURE);
|
|
if(_direction & directionWrite)
|
|
(void)snd_pcm_channel_flush(m_pcm_handle, SND_PCM_CHANNEL_PLAYBACK);
|
|
|
|
/* check device capabilities */
|
|
checkCapabilities();
|
|
|
|
/* set the fragment settings to what the user requested */
|
|
_fragmentSize = requestedFragmentSize;
|
|
_fragmentCount = requestedFragmentCount;
|
|
|
|
/* set PCM communication parameters */
|
|
if((_direction & directionRead) && setPcmParams(SND_PCM_CHANNEL_CAPTURE))
|
|
return false;
|
|
if((_direction & directionWrite) && setPcmParams(SND_PCM_CHANNEL_PLAYBACK))
|
|
return false;
|
|
|
|
/* prepare channel */
|
|
if((_direction & directionRead) &&
|
|
snd_pcm_channel_prepare(m_pcm_handle, SND_PCM_CHANNEL_CAPTURE) < 0)
|
|
{
|
|
_error = "Unable to prepare capture channel!";
|
|
return false;
|
|
}
|
|
if((_direction & directionWrite) &&
|
|
snd_pcm_channel_prepare(m_pcm_handle, SND_PCM_CHANNEL_PLAYBACK) < 0)
|
|
{
|
|
_error = "Unable to prepare playback channel!";
|
|
return false;
|
|
}
|
|
|
|
/* obtain current PCM setup (may differ from requested one) */
|
|
(void)memset(&m_setup, 0, sizeof(m_setup));
|
|
|
|
m_setup.channel = SND_PCM_CHANNEL_PLAYBACK;
|
|
if(snd_pcm_channel_setup(m_pcm_handle, &m_setup) < 0) {
|
|
_error = "Unable to obtain channel setup!";
|
|
return false;
|
|
}
|
|
|
|
/* check samplerate */
|
|
const int tolerance = _samplingRate/10+1000;
|
|
if(abs(m_setup.format.rate-_samplingRate) > tolerance)
|
|
{
|
|
_error = "Can't set requested sampling rate!";
|
|
char details[80];
|
|
sprintf(details," (requested rate %d, got rate %d)",
|
|
_samplingRate, m_setup.format.rate);
|
|
_error += details;
|
|
return false;
|
|
}
|
|
_samplingRate = m_setup.format.rate;
|
|
|
|
/* check format */
|
|
if(m_setup.format.format != m_format) {
|
|
_error = "Can't set requested format:";
|
|
_error += snd_pcm_get_format_name(m_format);
|
|
return false;
|
|
}
|
|
|
|
/* check voices */
|
|
if(m_setup.format.voices != _channels) {
|
|
_error = "Audio device doesn't support number of requested channels!";
|
|
return false;
|
|
}
|
|
|
|
/* update fragment settings with what we got */
|
|
switch(m_bufferMode) {
|
|
case block:
|
|
_fragmentSize = m_setup.buf.block.frag_size;
|
|
_fragmentCount = m_setup.buf.block.frags_max-1;
|
|
break;
|
|
case stream:
|
|
_fragmentSize = m_setup.buf.stream.queue_size;
|
|
_fragmentCount = 1;
|
|
break;
|
|
}
|
|
|
|
artsdebug("buffering: %d fragments with %d bytes "
|
|
"(audio latency is %1.1f ms)", _fragmentCount, _fragmentSize,
|
|
(float)(_fragmentSize*_fragmentCount) /
|
|
(float)(2.0 * _samplingRate * _channels)*1000.0);
|
|
|
|
/* obtain PCM file descriptor(s) */
|
|
audio_read_fd = audio_write_fd = -1;
|
|
|
|
if(_direction & directionRead)
|
|
audio_read_fd = snd_pcm_file_descriptor(m_pcm_handle,
|
|
SND_PCM_CHANNEL_CAPTURE);
|
|
if(_direction & directionWrite)
|
|
audio_write_fd = snd_pcm_file_descriptor(m_pcm_handle,
|
|
SND_PCM_CHANNEL_PLAYBACK);
|
|
|
|
/* start recording */
|
|
if((_direction & directionRead) && snd_pcm_capture_go(m_pcm_handle)) {
|
|
_error = "Can't start recording!";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void AudioIOALSA::close()
|
|
{
|
|
int& _direction = param(direction);
|
|
if(_direction & directionRead)
|
|
(void)snd_pcm_channel_flush(m_pcm_handle, SND_PCM_CHANNEL_CAPTURE);
|
|
if(_direction & directionWrite)
|
|
(void)snd_pcm_channel_flush(m_pcm_handle, SND_PCM_CHANNEL_PLAYBACK);
|
|
(void)snd_pcm_close(m_pcm_handle);
|
|
}
|
|
|
|
void AudioIOALSA::setParam(AudioParam p, int& value)
|
|
{
|
|
switch(p)
|
|
{
|
|
case fragmentSize:
|
|
param(p) = requestedFragmentSize = value;
|
|
break;
|
|
case fragmentCount:
|
|
param(p) = requestedFragmentCount = value;
|
|
break;
|
|
default:
|
|
param(p) = value;
|
|
break;
|
|
}
|
|
}
|
|
|
|
int AudioIOALSA::getParam(AudioParam p)
|
|
{
|
|
snd_pcm_channel_status_t status;
|
|
(void)memset(&status, 0, sizeof(status));
|
|
|
|
switch(p)
|
|
{
|
|
case canRead:
|
|
status.channel = SND_PCM_CHANNEL_CAPTURE;
|
|
if(snd_pcm_channel_status(m_pcm_handle, &status) < 0) {
|
|
arts_warning("Capture channel status error!");
|
|
return -1;
|
|
}
|
|
return status.free;
|
|
break;
|
|
|
|
case canWrite:
|
|
status.channel = SND_PCM_CHANNEL_PLAYBACK;
|
|
if(snd_pcm_channel_status(m_pcm_handle, &status) < 0) {
|
|
arts_warning("Playback channel status error!");
|
|
return -1;
|
|
}
|
|
return status.free;
|
|
break;
|
|
|
|
case selectReadFD:
|
|
return audio_read_fd;
|
|
break;
|
|
|
|
case selectWriteFD:
|
|
return audio_write_fd;
|
|
break;
|
|
|
|
case autoDetect:
|
|
/*
|
|
* that the ALSA driver could be compiled doesn't say anything
|
|
* about whether it will work (the user might be using an OSS
|
|
* kernel driver) so we'll use a value less than the OSS one
|
|
* here, because OSS will most certainly work (ALSA's OSS emu)
|
|
*/
|
|
return 5;
|
|
break;
|
|
|
|
default:
|
|
return param(p);
|
|
break;
|
|
}
|
|
}
|
|
|
|
int AudioIOALSA::read(void *buffer, int size)
|
|
{
|
|
int length;
|
|
do {
|
|
length = snd_pcm_read(m_pcm_handle, buffer, size);
|
|
} while (length == -EINTR);
|
|
if(length == -EPIPE) {
|
|
snd_pcm_channel_status_t status;
|
|
(void)memset(&status, 0, sizeof(status));
|
|
status.channel = SND_PCM_CHANNEL_CAPTURE;
|
|
if(snd_pcm_channel_status(m_pcm_handle, &status) < 0) {
|
|
arts_info("Capture channel status error!");
|
|
return -1;
|
|
}
|
|
else if(status.status == SND_PCM_STATUS_RUNNING) {
|
|
length = 0;
|
|
}
|
|
else if(status.status == SND_PCM_STATUS_OVERRUN) {
|
|
artsdebug("Overrun at position: %d" ,status.scount);
|
|
if(snd_pcm_channel_prepare(m_pcm_handle, SND_PCM_CHANNEL_CAPTURE)<0)
|
|
{
|
|
arts_info("Overrun: capture prepare error!");
|
|
return -1;
|
|
}
|
|
length = 0;
|
|
}
|
|
else {
|
|
arts_info("Unknown capture error!");
|
|
return -1;
|
|
}
|
|
}
|
|
else if(length < 0) {
|
|
arts_info("Capture error: %s", snd_strerror(length));
|
|
return -1;
|
|
}
|
|
return length;
|
|
}
|
|
|
|
int AudioIOALSA::write(void *buffer, int size)
|
|
{
|
|
int length;
|
|
while((length = snd_pcm_write(m_pcm_handle, buffer, size)) != size) {
|
|
if (length == -EINTR)
|
|
continue; // Try again
|
|
snd_pcm_channel_status_t status;
|
|
(void)memset(&status, 0, sizeof(status));
|
|
status.channel = SND_PCM_CHANNEL_PLAYBACK;
|
|
|
|
if(snd_pcm_channel_status(m_pcm_handle, &status) < 0) {
|
|
arts_warning("Playback channel status error!");
|
|
return -1;
|
|
}
|
|
else if(status.status == SND_PCM_STATUS_UNDERRUN) {
|
|
artsdebug("Underrun at position: %d", status.scount);
|
|
if(snd_pcm_channel_prepare(m_pcm_handle, SND_PCM_CHANNEL_PLAYBACK)
|
|
< 0) {
|
|
arts_warning("Underrun: playback prepare error!");
|
|
return -1;
|
|
}
|
|
}
|
|
else {
|
|
arts_warning("Unknown playback error!");
|
|
return -1;
|
|
}
|
|
}
|
|
return size;
|
|
}
|
|
|
|
int AudioIOALSA::setPcmParams(const int channel)
|
|
{
|
|
int &_samplingRate = param(samplingRate);
|
|
int &_channels = param(channels);
|
|
int &_fragmentSize = param(fragmentSize);
|
|
int &_fragmentCount = param(fragmentCount);
|
|
|
|
(void)memset(&m_cformat, 0, sizeof(m_cformat));
|
|
m_cformat.interleave = 1;
|
|
m_cformat.format = m_format;
|
|
m_cformat.rate = _samplingRate;
|
|
m_cformat.voices = _channels;
|
|
|
|
(void)memset(&m_params, 0, sizeof(m_params));
|
|
switch(m_bufferMode){
|
|
case stream:
|
|
m_params.mode=SND_PCM_MODE_STREAM;
|
|
break;
|
|
case block:
|
|
m_params.mode=SND_PCM_MODE_BLOCK;
|
|
break;
|
|
}
|
|
m_params.channel=channel;
|
|
(void)memcpy(&m_params.format, &m_cformat, sizeof(m_cformat));
|
|
if(channel==SND_PCM_CHANNEL_CAPTURE){
|
|
m_params.start_mode=SND_PCM_START_GO;
|
|
m_params.stop_mode=SND_PCM_STOP_ROLLOVER;
|
|
}
|
|
else{ //SND_PCM_CHANNEL_PLAYBACK
|
|
m_params.start_mode= (m_bufferMode==block) ? SND_PCM_START_FULL : SND_PCM_START_DATA;
|
|
m_params.stop_mode=SND_PCM_STOP_ROLLOVER; // SND_PCM_STOP_STOP
|
|
//use this ^^^ if you want to track underruns
|
|
}
|
|
|
|
switch(m_bufferMode){
|
|
case stream:
|
|
m_params.buf.stream.queue_size=1024*1024; //_fragmentSize*_fragmentCount;
|
|
m_params.buf.stream.fill=SND_PCM_FILL_SILENCE_WHOLE;
|
|
m_params.buf.stream.max_fill=1024;
|
|
break;
|
|
case block:
|
|
m_params.buf.block.frag_size=_fragmentSize;
|
|
if(channel==SND_PCM_CHANNEL_CAPTURE){
|
|
m_params.buf.block.frags_max=1;
|
|
m_params.buf.block.frags_min=1;
|
|
}
|
|
else{ //SND_PCM_CHANNEL_PLAYBACK
|
|
m_params.buf.block.frags_max=_fragmentCount+1;
|
|
m_params.buf.block.frags_min=1;
|
|
}
|
|
}
|
|
if(snd_pcm_channel_params(m_pcm_handle, &m_params)<0){
|
|
paramStr(lastError) = "Unable to set channel params!";
|
|
return 1;
|
|
}
|
|
else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
void AudioIOALSA::checkCapabilities()
|
|
{
|
|
snd_pcm_info_t info;
|
|
(void)memset(&info, 0, sizeof(info));
|
|
if(!snd_pcm_info(m_pcm_handle, &info)) {
|
|
string flags = "";
|
|
if(info.flags & SND_PCM_INFO_PLAYBACK) flags += "playback ";
|
|
if(info.flags & SND_PCM_INFO_CAPTURE) flags += "capture ";
|
|
if(info.flags & SND_PCM_INFO_DUPLEX) flags += "duplex ";
|
|
if(info.flags & SND_PCM_INFO_DUPLEX_RATE) flags += "duplex_rate ";
|
|
artsdebug(" type:%d id:%s\n"
|
|
" flags:%s\n"
|
|
" playback_subdevices:%d capture_subdevices:%d",
|
|
info.type, info.id,
|
|
flags.c_str(),
|
|
info.playback+1, info.capture+1);
|
|
}
|
|
else {
|
|
arts_warning("Can't get device info!"); //not fatal error
|
|
}
|
|
|
|
(void)memset(&m_cinfo, 0, sizeof(m_cinfo));
|
|
m_cinfo.channel = SND_PCM_CHANNEL_PLAYBACK;
|
|
if(!snd_pcm_channel_info(m_pcm_handle, &m_cinfo)) {
|
|
string flags = "";
|
|
if(m_cinfo.flags & SND_PCM_CHNINFO_MMAP) flags += "mmap ";
|
|
if(m_cinfo.flags & SND_PCM_CHNINFO_STREAM) flags += "stream ";
|
|
if(m_cinfo.flags & SND_PCM_CHNINFO_BLOCK) flags += "block ";
|
|
if(m_cinfo.flags & SND_PCM_CHNINFO_BATCH) flags += "batch ";
|
|
if(m_cinfo.flags & SND_PCM_CHNINFO_INTERLEAVE) flags += "interleave ";
|
|
if(m_cinfo.flags & SND_PCM_CHNINFO_NONINTERLEAVE) flags += "noninterleave ";
|
|
if(m_cinfo.flags & SND_PCM_CHNINFO_BLOCK_TRANSFER) flags += "block_transfer ";
|
|
if(m_cinfo.flags & SND_PCM_CHNINFO_OVERRANGE) flags += "overrange ";
|
|
if(m_cinfo.flags & SND_PCM_CHNINFO_MMAP_VALID) flags += "mmap_valid ";
|
|
if(m_cinfo.flags & SND_PCM_CHNINFO_PAUSE) flags += "pause ";
|
|
|
|
artsdebug(" subdevice:%d\n"
|
|
" flags:%s\n"
|
|
" min_rate:%d max_rate:%d\n"
|
|
" buffer_size:%d min_fragment_size:%d max_fragment_size:%d\n"
|
|
" fragment_align:%d fifo_size:%d transfer_block_size:%d\n"
|
|
" mmap_size:%d",
|
|
m_cinfo.subdevice,
|
|
flags.c_str(),
|
|
m_cinfo.min_rate, m_cinfo.max_rate,
|
|
m_cinfo.buffer_size, m_cinfo.min_fragment_size, m_cinfo.max_fragment_size,
|
|
m_cinfo.fragment_align, m_cinfo.fifo_size, m_cinfo.transfer_block_size,
|
|
m_cinfo.mmap_size);
|
|
}
|
|
else {
|
|
arts_warning("Can't get channel info!"); //not fatal error
|
|
}
|
|
}
|
|
|
|
#endif /* HAVE_LIBASOUND */
|