/**
* @file adin_mic_linux_alsa.c
*
*
* @brief マイク入力 (Linux/ALSA)
*
* ALSA API を使用する,マイク入力のための低レベル関数です.
* 使用には ALSA サウンドドライバーがインストールされていることが必要です.
*
* サウンドカードが 16bit モノラル で録音できることが必須です.
*
* JuliusはLinuxではミキサーデバイスの設定を一切行いません.録音デバイスの
* 選択(マイク/ライン)や録音ボリュームの調節は alsamixer など他のツールで
* 行なって下さい.
*
* 複数サウンドカードはサポートされていません.複数のサウンドカードが
* インストールされている場合,最初の1つが用いられます.
*
* バージョン 1.x に対応しています.1.0.13 で動作を確認しました.
*
* デバイス名は "default" が使用されます.環境変数 ALSADEV で変更できます.
*
*
* @brief Microphone input on Linux/ALSA
*
* Low level I/O functions for microphone input on Linux using
* Advanced Linux Sound Architechture (ALSA) API, developed on version 0.9.x.
*
* Julius does not alter any mixer device setting at all on Linux. You should
* configure the mixer for recording source (mic/line) and recording volume
* correctly using other audio tool such as alsamixer.
*
* Note that sound card should support 16bit monaural recording, and multiple
* cards are not supported (in that case the first one will be used).
*
* This file supports alsa version 1.x, and tested on 1.0.13.
*
* The default PCM device name is "default", and can be overwritten by
* environment variable "ALSADEV".
*
*
* @sa http://www.alsa-project.org/
*
* @author Akinobu LEE
* @date Sun Feb 13 16:18:26 2005
*
* $Revision: 1.13 $
*
*/
/*
* Copyright (c) 1991-2012 Kawahara Lab., Kyoto University
* Copyright (c) 2000-2005 Shikano Lab., Nara Institute of Science and Technology
* Copyright (c) 2005-2012 Julius project team, Nagoya Institute of Technology
* All rights reserved
*/
#include
#include
#include
#include
#include
#include
#ifdef HAS_ALSA
#if defined(HAVE_ALSA_ASOUNDLIB_H)
#include
#elif defined(HAVE_SYS_ASOUNDLIB_H)
#include
#endif
static int srate; ///< Required sampling rate
static snd_pcm_t *handle; ///< Audio handler
static char pcm_name[MAXPATHLEN]; ///< Name of the PCM device
static int latency = 32; ///< Lantency time in msec. You can override this value by specifying environment valuable "LATENCY_MSEC".
static boolean need_swap; ///< Whether samples need byte swap
#if (SND_LIB_MAJOR == 0)
static struct pollfd *ufds; ///< Poll descriptor
static int count; ///< Poll descriptor count
#endif
#define MAXPOLLINTERVAL 300 ///< Read timeout in msec.
#endif /* HAS_ALSA */
#ifdef HAS_ALSA
/**
* Output detailed device information.
*
* @param pcm_name [in] device name string
* @param handle [in] pcm audio handler
*
*/
static void
output_card_info(char *pcm_name, snd_pcm_t *handle)
{
int err;
snd_ctl_t *ctl;
snd_ctl_card_info_t *info;
snd_pcm_info_t *pcminfo;
snd_ctl_card_info_alloca(&info);
snd_pcm_info_alloca(&pcminfo);
char ctlname[30];
int card;
/* get PCM information to set current device and subdevice name */
if ((err = snd_pcm_info(handle, pcminfo)) < 0) {
jlog("Warning: adin_alsa: failed to obtain pcm info\n");
jlog("Warning: adin_alsa: skip output of detailed audio device info\n");
return;
}
/* open control associated with the pcm device name */
card = snd_pcm_info_get_card(pcminfo);
if (card < 0) {
strcpy(ctlname, "default");
} else {
snprintf(ctlname, 30, "hw:%d", card);
}
if ((err = snd_ctl_open(&ctl, ctlname, 0)) < 0) {
jlog("Warning: adin_alsa: failed to open control device \"%s\", \n", ctlname);
jlog("Warning: adin_alsa: skip output of detailed audio device info\n");
return;
}
/* get its card info */
if ((err = snd_ctl_card_info(ctl, info)) < 0) {
jlog("Warning: adin_alsa: unable to get card info for %s\n", ctlname);
jlog("Warning: adin_alsa: skip output of detailed audio device info\n");
snd_ctl_close(ctl);
return;
}
/* get detailed PCM information of current device from control */
if ((err = snd_ctl_pcm_info(ctl, pcminfo)) < 0) {
jlog("Error: adin_alsa: unable to get pcm info from card control\n");
jlog("Warning: adin_alsa: skip output of detailed audio device info\n");
snd_ctl_close(ctl);
return;
}
/* output */
jlog("Stat: \"%s\": %s [%s] device %s [%s] %s\n",
pcm_name,
snd_ctl_card_info_get_id(info),
snd_ctl_card_info_get_name(info),
snd_pcm_info_get_id(pcminfo),
snd_pcm_info_get_name(pcminfo),
snd_pcm_info_get_subdevice_name(pcminfo));
/* close controller */
snd_ctl_close(ctl);
}
#endif /* HAS_ALSA */
/**
* Device initialization: check machine capability
*
* @param sfreq [in] required sampling frequency.
* @param dummy [in] a dummy data
*
* @return TRUE on success, FALSE on failure.
*/
boolean
adin_alsa_standby(int sfreq, void *dummy)
{
#ifndef HAS_ALSA
jlog("Error: ALSA not compiled in\n");
return FALSE;
#else
/* store required sampling rate for checking after opening device */
srate = sfreq;
return TRUE;
#endif
}
/**
* Open the specified device and check capability of the opening device.
*
* @param devstr [in] device string to open
*
* @return TRUE on success, FALSE on failure.
*/
static boolean
adin_alsa_open(char *devstr)
{
#ifndef HAS_ALSA
jlog("Error: ALSA not compiled in\n");
return FALSE;
#else
int err;
snd_pcm_hw_params_t *hwparams; ///< Pointer to device hardware parameters
#if (SND_LIB_MAJOR == 0)
int actual_rate; /* sample rate returned by hardware */
#else
unsigned int actual_rate; /* sample rate returned by hardware */
#endif
int dir = 0; /* comparison result of exact rate and given rate */
/* open the device in non-block mode) */
if ((err = snd_pcm_open(&handle, devstr, SND_PCM_STREAM_CAPTURE, SND_PCM_NONBLOCK)) < 0) {
jlog("Error: adin_alsa: cannot open PCM device \"%s\" (%s)\n", devstr, snd_strerror(err));
return(FALSE);
}
/* set device to non-block mode */
if ((err = snd_pcm_nonblock(handle, 1)) < 0) {
jlog("Error: adin_alsa: cannot set PCM device to non-blocking mode\n");
return(FALSE);
}
/* allocate hwparam structure */
snd_pcm_hw_params_alloca(&hwparams);
/* initialize hwparam structure */
if ((err = snd_pcm_hw_params_any(handle, hwparams)) < 0) {
jlog("Error: adin_alsa: cannot initialize PCM device parameter structure (%s)\n", snd_strerror(err));
return(FALSE);
}
/* set interleaved read/write format */
if ((err = snd_pcm_hw_params_set_access(handle, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) {
jlog("Error: adin_alsa: cannot set PCM device access mode (%s)\n", snd_strerror(err));
return(FALSE);
}
/* set sample format */
#ifdef WORDS_BIGENDIAN
/* try big endian, then little endian with byte swap */
if ((err = snd_pcm_hw_params_set_format(handle, hwparams, SND_PCM_FORMAT_S16_BE)) >= 0) {
need_swap = FALSE;
} else if ((err = snd_pcm_hw_params_set_format(handle, hwparams, SND_PCM_FORMAT_S16_LE)) >= 0) {
need_swap = TRUE;
} else {
jlog("Error: adin_alsa: cannot set PCM device format to signed 16bit (%s)\n", snd_strerror(err));
return(FALSE);
}
#else /* LITTLE ENDIAN */
/* try little endian, then big endian with byte swap */
if ((err = snd_pcm_hw_params_set_format(handle, hwparams, SND_PCM_FORMAT_S16_LE)) >= 0) {
need_swap = FALSE;
} else if ((err = snd_pcm_hw_params_set_format(handle, hwparams, SND_PCM_FORMAT_S16_BE)) >= 0) {
need_swap = TRUE;
} else {
jlog("Error: adin_alsa: cannot set PCM device format to signed 16bit (%s)\n", snd_strerror(err));
return(FALSE);
}
#endif
/* set number of channels */
if ((err = snd_pcm_hw_params_set_channels(handle, hwparams, 1)) < 0) {
jlog("Error: adin_alsa: cannot set PCM channel to %d (%s)\n", 1, snd_strerror(err));
return(FALSE);
}
/* set sample rate (if the exact rate is not supported by the hardware, use nearest possible rate */
#if (SND_LIB_MAJOR == 0)
actual_rate = snd_pcm_hw_params_set_rate_near(handle, hwparams, srate, &dir);
if (actual_rate < 0) {
jlog("Error: adin_alsa: cannot set PCM device sample rate to %d (%s)\n", srate, snd_strerror(actual_rate));
return(FALSE);
}
#else
actual_rate = srate;
err = snd_pcm_hw_params_set_rate_near(handle, hwparams, &actual_rate, &dir);
if (err < 0) {
jlog("Error: adin_alsa: cannot set PCM device sample rate to %d (%s)\n", srate, snd_strerror(err));
return(FALSE);
}
#endif
if (actual_rate != srate) {
jlog("Warning: adin_alsa: the exact rate %d Hz is not available by your PCM hardware.\n", srate);
jlog("Warning: adin_alsa: using %d Hz instead.\n", actual_rate);
}
jlog("Stat: capture audio at %dHz\n", actual_rate);
/* set period size */
{
#if (SND_LIB_MAJOR == 0)
int periodsize; /* period size (bytes) */
int actual_size;
int maxsize, minsize;
#else
unsigned int period_time, period_time_current;
snd_pcm_uframes_t chunk_size;
boolean has_current_period;
#endif
boolean force = FALSE;
char *p;
/* set apropriate period size */
if ((p = getenv("LATENCY_MSEC")) != NULL) {
latency = atoi(p);
jlog("Stat: adin_alsa: trying to set latency to %d msec from LATENCY_MSEC)\n", latency);
force = TRUE;
}
/* get hardware max/min size */
#if (SND_LIB_MAJOR == 0)
if ((maxsize = snd_pcm_hw_params_get_period_size_max(hwparams, &dir)) < 0) {
jlog("Error: adin_alsa: cannot get maximum period size\n");
return(FALSE);
}
if ((minsize = snd_pcm_hw_params_get_period_size_min(hwparams, &dir)) < 0) {
jlog("Error: adin_alsa: cannot get minimum period size\n");
return(FALSE);
}
#else
has_current_period = TRUE;
if ((err = snd_pcm_hw_params_get_period_time(hwparams, &period_time_current, &dir)) < 0) {
has_current_period = FALSE;
}
if (has_current_period) {
jlog("Stat: adin_alsa: current latency time: %d msec\n", period_time_current / 1000);
}
#endif
/* set period time (near value will be used) */
#if (SND_LIB_MAJOR == 0)
periodsize = actual_rate * latency / 1000 * sizeof(SP16);
if (periodsize < minsize) {
jlog("Stat: adin_alsa: PCM latency of %d ms (%d bytes) too small, use device minimum %d bytes\n", latency, periodsize, minsize);
periodsize = minsize;
} else if (periodsize > maxsize) {
jlog("Stat: adin_alsa: PCM latency of %d ms (%d bytes) too large, use device maximum %d bytes\n", latency, periodsize, maxsize);
periodsize = maxsize;
}
actual_size = snd_pcm_hw_params_set_period_size_near(handle, hwparams, periodsize, &dir);
if (actual_size < 0) {
jlog("Error: adin_alsa: cannot set PCM record period size to %d (%s)\n", periodsize, snd_strerror(actual_size));
return(FALSE);
}
if (actual_size != periodsize) {
jlog("Stat: adin_alsa: PCM period size: %d bytes (%dms) -> %d bytes\n", periodsize, latency, actual_size);
}
jlog("Stat: Audio I/O Latency = %d msec (data fragment = %d frames)\n", actual_size * 1000 / (actual_rate * sizeof(SP16)), actual_size / sizeof(SP16));
#else
period_time = latency * 1000;
if (!force && has_current_period && period_time > period_time_current) {
jlog("Stat: adin_alsa: current latency (%dms) is shorter than %dms, leave it\n", period_time_current / 1000, latency);
period_time = period_time_current;
} else {
if ((err = snd_pcm_hw_params_set_period_time_near(handle, hwparams, &period_time, 0)) < 0) {
jlog("Error: adin_alsa: cannot set PCM record period time to %d msec (%s)\n", period_time / 1000, snd_strerror(err));
return(FALSE);
}
snd_pcm_hw_params_get_period_size(hwparams, &chunk_size, 0);
jlog("Stat: adin_alsa: latency set to %d msec (chunk = %d bytes)\n", period_time / 1000, chunk_size);
}
#endif
#if (SND_LIB_MAJOR == 0)
/* set number of periods ( = 2) */
if ((err = snd_pcm_hw_params_set_periods(handle, hwparams, sizeof(SP16), 0)) < 0) {
jlog("Error: adin_alsa: cannot set PCM number of periods to %d (%s)\n", sizeof(SP16), snd_strerror(err));
return(FALSE);
}
#endif
}
/* apply the configuration to the PCM device */
if ((err = snd_pcm_hw_params(handle, hwparams)) < 0) {
jlog("Error: adin_alsa: cannot set PCM hardware parameters (%s)\n", snd_strerror(err));
return(FALSE);
}
/* prepare for recording */
if ((err = snd_pcm_prepare(handle)) < 0) {
jlog("Error: adin_alsa: failed to prepare audio interface (%s)\n", snd_strerror(err));
}
#if (SND_LIB_MAJOR == 0)
/* prepare for polling */
count = snd_pcm_poll_descriptors_count(handle);
if (count <= 0) {
jlog("Error: adin_alsa: invalid PCM poll descriptors count\n");
return(FALSE);
}
ufds = mymalloc(sizeof(struct pollfd) * count);
if ((err = snd_pcm_poll_descriptors(handle, ufds, count)) < 0) {
jlog("Error: adin_alsa: unable to obtain poll descriptors for PCM recording (%s)\n", snd_strerror(err));
return(FALSE);
}
#endif
/* output status */
output_card_info(devstr, handle);
return(TRUE);
#endif /* HAS_ALSA */
}
#ifdef HAS_ALSA
/**
* Error recovery when PCM buffer underrun or suspend.
*
* @param handle [in] audio handler
* @param err [in] error code
*
* @return 0 on success, otherwise the given errno.
*/
static int
xrun_recovery(snd_pcm_t *handle, int err)
{
if (err == -EPIPE) { /* under-run */
err = snd_pcm_prepare(handle);
if (err < 0)
jlog("Error: adin_alsa: can't recovery from PCM buffer underrun, prepare failed: %s\n", snd_strerror(err));
return 0;
} else if (err == -ESTRPIPE) {
while ((err = snd_pcm_resume(handle)) == -EAGAIN)
sleep(1); /* wait until the suspend flag is released */
if (err < 0) {
err = snd_pcm_prepare(handle);
if (err < 0)
jlog("Error: adin_alsa: can't recovery from PCM buffer suspend, prepare failed: %s\n", snd_strerror(err));
}
return 0;
}
return err;
}
#endif /* HAS_ALSA */
/**
* Start recording.
*
* @param pathname [in] device name to open or NULL for default
*
* @return TRUE on success, FALSE on failure.
*/
boolean
adin_alsa_begin(char *pathname)
{
#ifndef HAS_ALSA
return FALSE;
#else
int err;
snd_pcm_state_t status;
char *p;
/* set device name to open to pcm_name */
if (pathname != NULL) {
strncpy(pcm_name, pathname, MAXPATHLEN);
jlog("Stat: adin_alsa: device name from argument: \"%s\"\n", pcm_name);
} else if ((p = getenv("ALSADEV")) != NULL) {
strncpy(pcm_name, p, MAXPATHLEN);
jlog("Stat: adin_alsa: device name from ALSADEV: \"%s\"\n", pcm_name);
} else {
strcpy(pcm_name, "default");
}
/* open the device */
if (adin_alsa_open(pcm_name) == FALSE) {
return FALSE;
}
/* check hardware status */
while(1) { /* wait till prepared */
status = snd_pcm_state(handle);
switch(status) {
case SND_PCM_STATE_PREPARED: /* prepared for operation */
if ((err = snd_pcm_start(handle)) < 0) {
jlog("Error: adin_alsa: cannot start PCM (%s)\n", snd_strerror(err));
return (FALSE);
}
return(TRUE);
break;
case SND_PCM_STATE_RUNNING: /* capturing the samples of other application */
if ((err = snd_pcm_drop(handle)) < 0) { /* discard the existing samples */
jlog("Error: adin_alsa: cannot drop PCM (%s)\n", snd_strerror(err));
return (FALSE);
}
break;
case SND_PCM_STATE_XRUN: /* buffer overrun */
if ((err = xrun_recovery(handle, -EPIPE)) < 0) {
jlog("Error: adin_alsa: PCM XRUN recovery failed (%s)\n", snd_strerror(err));
return(FALSE);
}
break;
case SND_PCM_STATE_SUSPENDED: /* suspended by power management system */
if ((err = xrun_recovery(handle, -ESTRPIPE)) < 0) {
jlog("Error: adin_alsa: PCM XRUN recovery failed (%s)\n", snd_strerror(err));
return(FALSE);
}
break;
default:
/* do nothing */
break;
}
}
return(TRUE);
#endif /* HAS_ALSA */
}
/**
* Stop recording.
*
* @return TRUE on success, FALSE on failure.
*/
boolean
adin_alsa_end()
{
int err;
if ((err = snd_pcm_close(handle)) < 0) {
jlog("Error: adin_alsa: cannot close PCM device (%s)\n", snd_strerror(err));
return(FALSE);
}
return(TRUE);
}
/**
* @brief Read samples from device
*
* Try to read @a sampnum samples and returns actual number of recorded
* samples currently available. This function will block until
* at least one sample can be obtained.
*
* @param buf [out] samples obtained in this function
* @param sampnum [in] wanted number of samples to be read
*
* @return actural number of read samples, -2 if an error occured.
*/
int
adin_alsa_read(SP16 *buf, int sampnum)
{
#ifndef HAS_ALSA
return -2;
#else
int cnt;
#if (SND_LIB_MAJOR == 0)
snd_pcm_sframes_t avail;
while ((avail = snd_pcm_avail_update(handle)) <= 0) {
usleep(latency * 1000);
}
if (avail < sampnum) {
cnt = snd_pcm_readi(handle, buf, avail);
} else {
cnt = snd_pcm_readi(handle, buf, sampnum);
}
#else
int ret;
snd_pcm_status_t *status;
int res;
struct timeval now, diff, tstamp;
ret = snd_pcm_wait(handle, MAXPOLLINTERVAL);
switch (ret) {
case 0: /* timeout */
jlog("Warning: adin_alsa: no data fragment after %d msec?\n", MAXPOLLINTERVAL);
cnt = 0;
break;
case 1: /* has data */
cnt = snd_pcm_readi(handle, buf, sampnum); /* read available (non-block) */
break;
case -EPIPE: /* pipe error */
/* try to recover the broken pipe */
snd_pcm_status_alloca(&status);
if ((res = snd_pcm_status(handle, status))<0) {
jlog("Error: adin_alsa: broken pipe: status error (%s)\n", snd_strerror(res));
return -2;
}
if (snd_pcm_status_get_state(status) == SND_PCM_STATE_XRUN) {
gettimeofday(&now, 0);
snd_pcm_status_get_trigger_tstamp(status, &tstamp);
timersub(&now, &tstamp, &diff);
jlog("Warning: adin_alsa: overrun!!! (at least %.3f ms long)\n",
diff.tv_sec * 1000 + diff.tv_usec / 1000.0);
if ((res = snd_pcm_prepare(handle))<0) {
jlog("Error: adin_alsa: overrun: prepare error (%s)", snd_strerror(res));
return -2;
}
break; /* ok, data should be accepted again */
} else if (snd_pcm_status_get_state(status) == SND_PCM_STATE_DRAINING) {
jlog("Warning: adin_alsa: draining: capture stream format change? attempting recover...\n");
if ((res = snd_pcm_prepare(handle))<0) {
jlog("Error: adin_alsa: draining: prepare error (%s)", snd_strerror(res));
return -2;
}
break;
}
jlog("Error: adin_alsa: error in snd_pcm_wait() (%s)\n", snd_pcm_state_name(snd_pcm_status_get_state(status)));
return -2;
default: /* other poll error */
jlog("Error: adin_alsa: error in snd_pcm_wait() (%s)\n", snd_strerror(ret));
return(-2); /* error */
}
#endif
if (cnt < 0) {
jlog("Error: adin_alsa: failed to read PCM (%s)\n", snd_strerror(cnt));
return(-2);
}
if (need_swap) {
swap_sample_bytes(buf, cnt);
}
return(cnt);
#endif /* HAS_ALSA */
}
/**
*
* Function to return current input source device name
*
* @return string of current input device name.
*
*/
char *
adin_alsa_input_name()
{
#ifndef HAS_ALSA
return NULL;
#else
return(pcm_name);
#endif
}
/* end of file */