blob: 4cc5cb2b586dd0a3eb85a6e987a483f3e7e2f91f [file]
/*
* oss.c -- The Open Sound System backend for the spd_audio library.
*
* Copyright (C) 2004,2006 Brailcom, o.p.s.
*
* This is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; either version 2.1, or (at your option) any later
* version.
*
* This software 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 Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* $Id: oss.c,v 1.13 2006-07-11 16:12:26 hanke Exp $
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <stdio.h>
#include <sys/time.h>
#include <time.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/soundcard.h>
#include <errno.h>
#include <unistd.h> /* for open, close */
#include <sys/ioctl.h>
#include <pthread.h>
#include <glib.h>
#include <sys/soundcard.h>
#ifdef USE_DLOPEN
#define SPD_AUDIO_PLUGIN_ENTRY spd_audio_plugin_get
#else
#define SPD_AUDIO_PLUGIN_ENTRY spd_oss_LTX_spd_audio_plugin_get
#endif
#include <spd_audio_plugin.h>
#include "../common/common.h"
typedef struct {
AudioID id;
int fd;
char *device_name;
pthread_mutex_t fd_mutex;
pthread_cond_t pt_cond;
pthread_mutex_t pt_mutex;
} spd_oss_id_t;
static int _oss_open(spd_oss_id_t * id);
static int _oss_close(spd_oss_id_t * id);
static int _oss_sync(spd_oss_id_t * id);
/* Put a message into the logfile (stderr) */
#define MSG(level, arg, ...) if (level <= oss_log_level) { MSG(level, "OSS: " arg, ##__VA_ARGS__); }
#define ERR(arg, ...) MSG(0, "OSS ERROR: " arg, ##__VA_ARGS__)
static int oss_log_level;
static char const *oss_play_cmd = "play";
static int _oss_open(spd_oss_id_t * id)
{
MSG(1, "_oss_open()")
pthread_mutex_lock(&id->fd_mutex);
id->fd = open(id->device_name, O_WRONLY, 0);
if (id->fd < 0) {
perror(id->device_name);
pthread_mutex_unlock(&id->fd_mutex);
id = NULL;
return -1;
}
pthread_mutex_unlock(&id->fd_mutex);
return 0;
}
static int _oss_close(spd_oss_id_t * id)
{
MSG(1, "_oss_close()")
if (id == NULL)
return 0;
pthread_mutex_lock(&id->fd_mutex);
if (id->fd < 0) {
pthread_mutex_unlock(&id->fd_mutex);
return 0;
}
close(id->fd);
id->fd = -1;
pthread_mutex_unlock(&id->fd_mutex);
return 0;
}
/* Open OSS device
Arguments:
**pars:
(char*) pars[0] -- the name of the device (e.g. "/dev/dsp")
(void*) pars[1] = NULL
*/
static AudioID *oss_open(void **pars)
{
spd_oss_id_t *oss_id;
int ret;
if (pars[0] == NULL)
return NULL;
oss_id = (spd_oss_id_t *) g_malloc(sizeof(spd_oss_id_t));
oss_id->device_name = g_strdup((char *)pars[0]);
pthread_mutex_init(&oss_id->fd_mutex, NULL);
pthread_cond_init(&oss_id->pt_cond, NULL);
pthread_mutex_init(&oss_id->pt_mutex, NULL);
/* Test if it's possible to access the device */
ret = _oss_open(oss_id);
if (ret) {
g_free(oss_id->device_name);
g_free(oss_id);
return NULL;
}
ret = _oss_close(oss_id);
if (ret) {
g_free(oss_id->device_name);
g_free(oss_id);
return NULL;
}
return (AudioID *) oss_id;
}
/* Internal function. */
static int _oss_sync(spd_oss_id_t * id)
{
int ret;
ret = ioctl(id->fd, SNDCTL_DSP_POST, 0);
if (ret == -1) {
perror("reset");
return -1;
}
return 0;
}
static int oss_play(AudioID * id, AudioTrack track)
{
int ret, ret2;
struct timeval now;
struct timespec timeout;
float length;
int r = 0;
int format, oformat, channels, speed;
int bytes_per_sample;
int num_bytes;
signed short *output_samples;
float delay = 0;
float DELAY = 0.1; /* in seconds */
audio_buf_info info;
int bytes;
float real_volume;
int i;
int re;
spd_oss_id_t *oss_id = (spd_oss_id_t *) id;
AudioTrack track_volume;
if (oss_id == NULL)
return -1;
/* Open the sound device. This is necessary for OSS so that the
application doesn't prevent others from accessing /dev/dsp when
it doesn't play anything. */
ret = _oss_open(oss_id);
if (ret)
return -2;
/* Create a copy of track with the adjusted volume */
track_volume = track;
track_volume.samples =
(short *)g_malloc(sizeof(short) * track.num_samples);
real_volume = ((float)id->volume + 100) / (float)200;
for (i = 0; i <= track.num_samples - 1; i++)
track_volume.samples[i] = track.samples[i] * real_volume;
/* Choose the correct format */
if (track.bits == 16) {
format = AFMT_S16_NE;
bytes_per_sample = 2;
} else if (track.bits == 8) {
bytes_per_sample = 1;
format = AFMT_S8;
} else {
ERR("Audio: Unrecognized sound data format.\n");
_oss_close(oss_id);
return -10;
}
oformat = format;
ret = ioctl(oss_id->fd, SNDCTL_DSP_SETFMT, &format);
if (ret == -1) {
perror("OSS ERROR: format");
_oss_close(oss_id);
return -1;
}
if (format != oformat) {
ERR("Device doesn't support 16-bit sound format.\n");
_oss_close(oss_id);
return -2;
}
/* Choose the correct number of channels */
channels = track.num_channels;
ret = ioctl(oss_id->fd, SNDCTL_DSP_CHANNELS, &channels);
if (ret == -1) {
perror("OSS ERROR: channels");
_oss_close(oss_id);
return -3;
}
if (channels != track.num_channels) {
MSG(1, "Device doesn't support stereo sound.\n");
_oss_close(oss_id);
return -4;
}
/* Choose the correct sample rate */
speed = track.sample_rate;
ret = ioctl(oss_id->fd, SNDCTL_DSP_SPEED, &speed);
if (ret == -1) {
ERR("OSS ERROR: Can't set sample rate %d nor any similar.",
track.sample_rate);
_oss_close(oss_id);
return -5;
}
if (speed != track.sample_rate) {
ERR("Device doesn't support bitrate %d, using %d instead.\n",
track.sample_rate, speed);
}
/* Is it not an empty track? */
if (track.samples == NULL) {
_oss_close(oss_id);
return 0;
}
/* Loop until all samples are played on the device.
In the meantime, wait in pthread_cond_timedwait for more data
or for interruption. */
MSG(4, "Starting playback");
output_samples = track_volume.samples;
num_bytes = track.num_samples * bytes_per_sample;
MSG(4, "bytes to play: %d, (%f secs)", num_bytes,
(((float)(num_bytes) / 2) / (float)track.sample_rate));
while (num_bytes > 0) {
/* OSS doesn't support non-blocking write, so lets check how much data
can we write so that write() returns immediately */
re = ioctl(oss_id->fd, SNDCTL_DSP_GETOSPACE, &info);
if (re == -1) {
perror("OSS ERROR: GETOSPACE");
_oss_close(oss_id);
return -5;
}
/* If there is not enough space for a single fragment, try later.
(This shouldn't happen, it has very bad effect on synchronization!) */
if (info.fragments == 0) {
MSG(4,
"WARNING: There is not enough space for a single fragment, looping");
usleep(100);
continue;
}
MSG(4,
"There is space for %d more fragments, fragment size is %d bytes",
info.fragments, info.fragsize);
bytes = info.fragments * info.fragsize;
ret =
write(oss_id->fd, output_samples,
num_bytes > bytes ? bytes : num_bytes);
/* Handle write() errors */
if (ret <= 0) {
perror("audio");
_oss_close(oss_id);
return -6;
}
num_bytes -= ret;
output_samples += ret / 2;
MSG(4, "%d bytes written to OSS, %d remaining", ret, num_bytes);
/* If there is some more data that is less than a
full fragment, we need to write it immediately so
that it doesn't cause buffer underruns later. */
if ((num_bytes > 0)
&& (num_bytes < info.fragsize)
&& (bytes + num_bytes < info.bytes)) {
MSG(4,
"Writing the rest of the data (%d bytes) to OSS, not a full fragment",
num_bytes);
ret2 = write(oss_id->fd, output_samples, num_bytes);
num_bytes -= ret2;
output_samples += ret2 / 2;
ret += ret2;
}
/* Handle write() errors */
if (ret <= 0) {
perror("audio");
_oss_close(oss_id);
return -6;
}
/* Some timing magic...
We need to wait for the time computed from the number of
samples written. But this wait needs to be interruptible
by oss_stop(). Furthermore, there need to be no buffer
underrruns, so we actually wait a bit (DELAY) less
in the first pass through the while() loop. Then our timer
will be DELAY nsecs backwards.
*/
MSG(4, "Now we will try to wait");
pthread_mutex_lock(&oss_id->pt_mutex);
length = (((float)(ret) / 2) / (float)track.sample_rate);
if (!delay) {
delay = length > DELAY ? DELAY : length;
length -= delay;
}
MSG(4, "Wait for %f secs (begin: %f, delay: %f)", length,
length + delay, delay)
gettimeofday(&now, NULL);
timeout.tv_sec = now.tv_sec + (int)length;
timeout.tv_nsec =
now.tv_usec * 1000 + (length - (int)length) * 1000000000;
//MSG("5, waiting till %d:%d (%d:%d | %d:%d)", timeout.tv_sec, timeout.tv_nsec,
// now.tv_sec, now.tv_usec*1000, timeout.tv_sec - now.tv_sec, timeout.tv_nsec-now.tv_usec*1000);
timeout.tv_sec += timeout.tv_nsec / 1000000000;
timeout.tv_nsec = timeout.tv_nsec % 1000000000;
// MSG("6, waiting till %d:%d (%d:%d | %d:%d)", timeout.tv_sec, timeout.tv_nsec,
// now.tv_sec, now.tv_usec*1000, timeout.tv_sec - now.tv_sec, timeout.tv_nsec-now.tv_usec*1000);
r = pthread_cond_timedwait(&oss_id->pt_cond, &oss_id->pt_mutex,
&timeout);
pthread_mutex_unlock(&oss_id->pt_mutex);
MSG(4, "End of wait");
/* The pthread_cond_timedwait was interrupted by change in the
condition variable? if so, terminate. */
if (r != ETIMEDOUT) {
MSG(4, "Playback stopped, %d", r);
break;
}
}
/* ...one more excersise in timing magic.
Wait for the resting delay secs. */
/* Ugly hack: correct for the time we spend outside timing segments */
delay -= 0.05;
MSG(4, "Wait for the resting delay = %f secs", delay)
if ((delay > 0) && (r == ETIMEDOUT)) {
pthread_mutex_lock(&oss_id->pt_mutex);
gettimeofday(&now, NULL);
timeout.tv_sec = now.tv_sec;
timeout.tv_nsec = now.tv_usec * 1000 + delay * 1000000000;
// MSG("6, waiting till %d:%d (%d:%d | %d:%d)", timeout.tv_sec, timeout.tv_nsec,
// now.tv_sec, now.tv_usec*1000, timeout.tv_sec - now.tv_sec, timeout.tv_nsec-now.tv_usec*1000);
timeout.tv_sec += timeout.tv_nsec / 1000000000;
timeout.tv_nsec = timeout.tv_nsec % 1000000000;
// MSG("6, waiting till %d:%d (%d:%d | %d:%d)", timeout.tv_sec, timeout.tv_nsec,
// now.tv_sec, now.tv_usec*1000, timeout.tv_sec - now.tv_sec, timeout.tv_nsec-now.tv_usec*1000);
r = pthread_cond_timedwait(&oss_id->pt_cond, &oss_id->pt_mutex,
&timeout);
pthread_mutex_unlock(&oss_id->pt_mutex);
}
MSG(4, "End of wait");
if (track_volume.samples != NULL)
g_free(track_volume.samples);
/* Flush all the buffers */
_oss_sync(oss_id);
/* Close the device so that we don't block other apps trying to
access the device. */
_oss_close(oss_id);
MSG(4, "Device closed");
return 0;
}
/* Stop the playback on the device and interrupt oss_play */
static int oss_stop(AudioID * id)
{
int ret = 0;
spd_oss_id_t *oss_id = (spd_oss_id_t *) id;
if (oss_id == NULL)
return 0;
MSG(4, "stop() called");
/* Stop the playback on /dev/dsp */
pthread_mutex_lock(&oss_id->fd_mutex);
if (oss_id->fd >= 0)
ret = ioctl(oss_id->fd, SNDCTL_DSP_RESET, 0);
pthread_mutex_unlock(&oss_id->fd_mutex);
if (ret == -1) {
perror("reset");
return -1;
}
/* Interrupt oss_play by setting the condition variable */
pthread_mutex_lock(&oss_id->pt_mutex);
pthread_cond_signal(&oss_id->pt_cond);
pthread_mutex_unlock(&oss_id->pt_mutex);
return 0;
}
/* Close the device */
static int oss_close(AudioID * id)
{
spd_oss_id_t *oss_id = (spd_oss_id_t *) id;
/* Does nothing because the device is being automatically opened and
closed in oss_play before and after playing each sample. */
g_free(oss_id->device_name);
g_free(oss_id);
id = NULL;
return 0;
}
/* Set volume
Comments:
/dev/dsp can't set volume. We just multiply the track samples by
a constant in oss_play (see oss_play() for more information).
*/
static int oss_set_volume(AudioID * id, int volume)
{
return 0;
}
static void oss_set_loglevel(int level)
{
if (level) {
oss_log_level = level;
}
}
static char const *oss_get_playcmd(void)
{
return oss_play_cmd;
}
/* Provide the OSS backend. */
static spd_audio_plugin_t oss_functions = {
"oss",
oss_open,
oss_play,
oss_stop,
oss_close,
oss_set_volume,
oss_set_loglevel,
oss_get_playcmd
};
spd_audio_plugin_t *oss_plugin_get(void)
{
return &oss_functions;
}
spd_audio_plugin_t *
__attribute__ ((weak))
SPD_AUDIO_PLUGIN_ENTRY(void)
{
return &oss_functions;
}
#undef MSG
#undef ERR