/*
* This file is part of DisOrder.
* Copyright (C) 2008 Richard Kettlewell
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
/** @file alsabg.c
* @brief Background-thread interface to ALSA
*
* This wraps ALSA with an interface which calls back to the client from a
* thread. It's not intended for completely general use, just what DisOrder
* needs.
*
* Only builds on Linux systems.
*/
#include "common.h"
#if HAVE_ALSA_ASOUNDLIB_H
#include
#include
#include "alsabg.h"
#include "log.h"
/** @brief Output handle */
static snd_pcm_t *pcm;
/** @brief Called to get audio data */
static alsa_bg_supply *supplyfn;
static pthread_t alsa_bg_collect_tid, alsa_bg_play_tid;
/** @brief Set to shut down the background threads */
static int alsa_bg_shutdown = 0;
/** @brief Number of channels (samples per frame) */
#define CHANNELS 2
/** @brief Number of bytes per samples */
#define BYTES_PER_SAMPLE 2
/** @brief Number of bytes per frame */
#define BYTES_PER_FRAME (CHANNELS * BYTES_PER_SAMPLE)
/** @brief Buffer size in bytes */
#define BUFFER_BYTES 65536
/** @brief Buffer size in frames */
#define BUFFER_FRAMES (BUFFER_BYTES / BYTES_PER_FRAME)
/** @brief Buffer size in samples */
#define BUFFER_SAMPLES (BUFFER_BYTES / BYTES_PER_SAMPLE)
/** @brief Audio buffer */
static uint8_t alsa_bg_buffer[BUFFER_BYTES];
/** @brief First playable byte in audio buffer */
static unsigned alsa_bg_start;
/** @brief Number of playable bytes in audio buffer */
static unsigned alsa_bg_count;
/** @brief Current enable status */
static int alsa_bg_enabled;
/** @brief Lock protecting audio buffer pointers */
static pthread_mutex_t alsa_bg_lock = PTHREAD_MUTEX_INITIALIZER;
/** @brief Signaled when buffer contents changes */
static pthread_cond_t alsa_bg_cond = PTHREAD_COND_INITIALIZER;
/** @brief Call a pthread_ function and fatal() on exit */
#define ep(x) do { \
int prc; \
\
if((prc = (x))) \
fatal(prc, "%s", #x); \
} while(0)
/** @brief Data collection thread
*
* This thread collects audio data to play and stores it in the ring
* buffer.
*/
static void *alsa_bg_collect(void attribute((unused)) *arg) {
unsigned avail_start, avail_count;
int count;
ep(pthread_mutex_lock(&alsa_bg_lock));
for(;;) {
/* If we're shutting down, quit straight away */
if(alsa_bg_shutdown)
break;
/* While we're disabled or the buffer is full, just wait */
if(!alsa_bg_enabled || alsa_bg_count == BUFFER_BYTES) {
ep(pthread_cond_wait(&alsa_bg_cond, &alsa_bg_lock));
continue;
}
/* Figure out where and how big the gap we can write into is */
avail_start = alsa_bg_start + alsa_bg_count;
if(avail_start < BUFFER_BYTES)
avail_count = BUFFER_BYTES - avail_start;
else {
avail_start %= BUFFER_BYTES;
avail_count = alsa_bg_start - avail_start;
}
assert(avail_start < BUFFER_BYTES);
assert(avail_count <= BUFFER_BYTES);
assert(avail_count + alsa_bg_count <= BUFFER_BYTES);
ep(pthread_mutex_unlock(&alsa_bg_lock));
count = supplyfn(alsa_bg_buffer + avail_start,
avail_count / BYTES_PER_SAMPLE);
ep(pthread_mutex_lock(&alsa_bg_lock));
alsa_bg_count += count * BYTES_PER_SAMPLE;
assert(alsa_bg_start < BUFFER_BYTES);
assert(alsa_bg_count <= BUFFER_BYTES);
ep(pthread_cond_signal(&alsa_bg_cond));
}
ep(pthread_mutex_unlock(&alsa_bg_lock));
return 0;
}
/** @brief Playback thread
*
* This thread reads audio data out of the ring buffer and plays it back
*/
static void *alsa_bg_play(void attribute((unused)) *arg) {
int prepared = 1, err;
int start, nbytes, nframes, rframes;
ep(pthread_mutex_lock(&alsa_bg_lock));
for(;;) {
/* If we're shutting down, quit straight away */
if(alsa_bg_shutdown)
break;
/* Wait for some data to be available. (If we are marked disabled
* we keep on playing what we've got.) */
if(alsa_bg_count == 0) {
if(prepared) {
if((err = snd_pcm_drain(pcm)))
fatal(0, "snd_pcm_drain: %d", err);
prepared = 0;
}
ep(pthread_cond_wait(&alsa_bg_cond, &alsa_bg_lock));
continue;
}
/* Calculate how much we can play */
start = alsa_bg_start;
if(start + alsa_bg_count <= BUFFER_BYTES)
nbytes = alsa_bg_count;
else
nbytes = BUFFER_BYTES - start;
/* Limit how much of the buffer we play. The effect is that we return from
* _writei earlier, and therefore free up more buffer space to read fresh
* data into. /2 works fine, /4 is just conservative. /1 (i.e. abolishing
* the heuristic) produces noticably noisy output. */
if(nbytes > BUFFER_BYTES / 4)
nbytes = BUFFER_BYTES / 4;
assert((unsigned)nbytes <= alsa_bg_count);
nframes = nbytes / BYTES_PER_FRAME;
ep(pthread_mutex_unlock(&alsa_bg_lock));
/* Make sure the PCM is prepared */
if(!prepared) {
if((err = snd_pcm_prepare(pcm)))
fatal(0, "snd_pcm_prepare: %d", err);
prepared = 1;
}
/* Play what we can */
rframes = snd_pcm_writei(pcm, alsa_bg_buffer + start, nframes);
ep(pthread_mutex_lock(&alsa_bg_lock));
if(rframes < 0) {
switch(rframes) {
case -EPIPE:
error(0, "underrun detected");
if((err = snd_pcm_prepare(pcm)))
fatal(0, "snd_pcm_prepare: %d", err);
break;
default:
fatal(0, "snd_pcm_writei: %d", rframes);
}
} else {
const int rbytes = rframes * BYTES_PER_FRAME;
/*fprintf(stderr, "%5d -> %5d\n", nbytes, rbytes);*/
/* Update the buffer pointers */
alsa_bg_count -= rbytes;
alsa_bg_start += rbytes;
if(alsa_bg_start >= BUFFER_BYTES)
alsa_bg_start -= BUFFER_BYTES;
assert(alsa_bg_start < BUFFER_BYTES);
assert(alsa_bg_count <= BUFFER_BYTES);
/* Let the collector know we've opened up some space */
ep(pthread_cond_signal(&alsa_bg_cond));
}
}
ep(pthread_mutex_unlock(&alsa_bg_lock));
return 0;
}
/** @brief Enable ALSA play */
void alsa_bg_enable(void) {
ep(pthread_mutex_lock(&alsa_bg_lock));
alsa_bg_enabled = 1;
ep(pthread_cond_broadcast(&alsa_bg_cond));
ep(pthread_mutex_unlock(&alsa_bg_lock));
}
/** @brief Disable ALSA play */
void alsa_bg_disable(void) {
ep(pthread_mutex_lock(&alsa_bg_lock));
alsa_bg_enabled = 0;
ep(pthread_cond_broadcast(&alsa_bg_cond));
ep(pthread_mutex_unlock(&alsa_bg_lock));
}
/** @brief Initialize background ALSA playback
* @param device Target device or NULL to use default
* @param supply Function to call to get audio data to play
*
* Playback is not initially enabled; see alsa_bg_enable(). When playback is
* enabled, @p supply will be called in a background thread to request audio
* data. It should return in a timely manner, but playback happens from a
* further thread and delays in @p supply will not delay transfer of data to
* the sound device (provided it doesn't actually run out).
*/
void alsa_bg_init(const char *device,
alsa_bg_supply *supply) {
snd_pcm_hw_params_t *hwparams;
/* Only support one format for now */
const int sample_format = SND_PCM_FORMAT_S16_BE;
unsigned rate = 44100;
int err;
if((err = snd_pcm_open(&pcm,
device ? device : "default",
SND_PCM_STREAM_PLAYBACK,
0)))
fatal(0, "error from snd_pcm_open: %d", err);
/* Set up 'hardware' parameters */
snd_pcm_hw_params_alloca(&hwparams);
if((err = snd_pcm_hw_params_any(pcm, hwparams)) < 0)
fatal(0, "error from snd_pcm_hw_params_any: %d", err);
if((err = snd_pcm_hw_params_set_access(pcm, hwparams,
SND_PCM_ACCESS_RW_INTERLEAVED)) < 0)
fatal(0, "error from snd_pcm_hw_params_set_access: %d", err);
if((err = snd_pcm_hw_params_set_format(pcm, hwparams,
sample_format)) < 0)
fatal(0, "error from snd_pcm_hw_params_set_format (%d): %d",
sample_format, err);
if((err = snd_pcm_hw_params_set_rate_near(pcm, hwparams, &rate, 0)) < 0)
fatal(0, "error from snd_pcm_hw_params_set_rate (%d): %d",
rate, err);
if((err = snd_pcm_hw_params_set_channels(pcm, hwparams,
CHANNELS)) < 0)
fatal(0, "error from snd_pcm_hw_params_set_channels (%d): %d",
CHANNELS, err);
if((err = snd_pcm_hw_params(pcm, hwparams)) < 0)
fatal(0, "error calling snd_pcm_hw_params: %d", err);
/* Record the audio supply function */
supplyfn = supply;
/* Create the audio output thread */
alsa_bg_shutdown = 0;
alsa_bg_enabled = 0;
ep(pthread_create(&alsa_bg_collect_tid, 0, alsa_bg_collect, 0));
ep(pthread_create(&alsa_bg_play_tid, 0, alsa_bg_play, 0));
}
/** @brief Deinitialize background ALSA playback
*
* The opposite of alsa_bg_init().
*/
void alsa_bg_close(void) {
void *r;
/* Notify background threads that we're shutting down */
ep(pthread_mutex_lock(&alsa_bg_lock));
alsa_bg_enabled = 0;
alsa_bg_shutdown = 1;
ep(pthread_cond_signal(&alsa_bg_cond));
ep(pthread_mutex_unlock(&alsa_bg_lock));
/* Join background threads when they're done */
ep(pthread_join(alsa_bg_collect_tid, &r));
ep(pthread_join(alsa_bg_play_tid, &r));
/* Close audio device */
snd_pcm_close(pcm);
pcm = 0;
}
#endif
/*
Local Variables:
c-basic-offset:2
comment-column:40
fill-column:79
indent-tabs-mode:nil
End:
*/