/* * XMMS Crossfade Plugin * Copyright (C) 2000-2007 Peter Eisenlohr * * based on the original OSS Output Plugin * Copyright (C) 1998-2000 Peter Alm, Mikael Alm, Olle Hallnas, Thomas Nilsson and 4Front Technologies * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, * USA. */ #ifdef HAVE_CONFIG_H # include "config.h" #endif #include "crossfade.h" #include "effect.h" #include "format.h" #include "oss.h" #include "player.h" #include #include #include #include #include #include #include #ifdef HAVE_SYS_SOUNDCARD_H # include #elif defined(HAVE_MACHINE_SOUNDCARD_H) # include #endif /* output plugin callback prototypes */ static void oss_get_volume(int *l, int *r); static void oss_set_volume(int l, int r); static gint oss_open_audio(AFormat format, int rate, int nch); static void oss_write_audio(void *ptr, int length); static void oss_close_audio(); static void oss_flush(int time); static void oss_pause(short paused); static gint oss_buffer_free(); static gint oss_buffer_playing(); static gint oss_output_time(); static gint oss_written_time(); /* output plugin callback table */ #if defined (COMPILE_FOR_AUDACIOUS) static OutputPlugin oss_op = { .description = "Builtin OSS Driver", .get_volume = oss_get_volume, .set_volume = oss_set_volume, .open_audio = oss_open_audio, .write_audio = oss_write_audio, .close_audio = oss_close_audio, .flush = oss_flush, .pause = oss_pause, .buffer_free = oss_buffer_free, .buffer_playing = oss_buffer_playing, .output_time = oss_output_time, .written_time = oss_written_time, }; #else static OutputPlugin oss_op = { NULL, NULL, "Builtin OSS Driver", NULL, NULL, NULL, oss_get_volume, oss_set_volume, oss_open_audio, oss_write_audio, oss_close_audio, oss_flush, oss_pause, oss_buffer_free, oss_buffer_playing, oss_output_time, oss_written_time, }; #endif /* local prototypes */ static void *buffer_thread_f(void *arg); /* local variables */ static gboolean realtime; static gint dsp_fd = -1; static gint dsp_buffer_size = 0; static gint dsp_fragment_size = 0; static gint dsp_fragment_utime = 0; static gboolean dsp_select_works = FALSE; static MUTEX buffer_mutex = MUTEX_INITIALIZER; static THREAD buffer_thread; static gboolean buffer_thread_finished; static format_t format; static gint64 streampos; static gboolean paused; /* TRUE: no playback (but still filling buffer) */ static gboolean stopped; /* TRUE: stop buffer thread ASAP */ static gpointer buffer_data; static gint buffer_size; static gint buffer_used; static gint buffer_rd_index; static gint64 buffer_written; static gint buffer_buffer_size; static gint buffer_preload; static gint buffer_preload_size; static effect_context_t effect_context; OutputPlugin * xfade_get_builtin_oss_oplugin_info(void) { return &oss_op; } static char * get_mixer_device_name() { if (config->oss_use_alt_mixer_device) return g_strdup(config->oss_alt_mixer_device); else if (config->oss_mixer_device > 0) return g_strdup_printf("/dev/mixer%d", config->oss_mixer_device); else return g_strdup("/dev/mixer"); } void oss_get_volume(int *l, int *r) { char *mixer_device_name; int mixer_fd; int devmask; int cmd, val; *l = 0; *r = 0; if (!config->enable_mixer) return; /* get device name */ mixer_device_name = get_mixer_device_name(); /* open device */ mixer_fd = open(mixer_device_name, O_RDONLY); g_free(mixer_device_name); if (mixer_fd == -1) return; /* check for capabilities */ ioctl(mixer_fd, SOUND_MIXER_READ_DEVMASK, &devmask); if ((devmask & SOUND_MASK_PCM) && !config->oss_mixer_use_master) cmd = SOUND_MIXER_READ_PCM; else if ((devmask & SOUND_MASK_VOLUME) && config->oss_mixer_use_master) cmd = SOUND_MIXER_READ_VOLUME; else { close(mixer_fd); return; } /* get volume */ ioctl(mixer_fd, cmd, &val); if (config->mixer_reverse) { *l = (val & 0xff00) >> 8; *r = (val & 0x00ff); } else { *r = (val & 0xff00) >> 8; *l = (val & 0x00ff); } /* close device */ close(mixer_fd); } void oss_set_volume(int l, int r) { char *mixer_device_name = "/dev/mixer"; int mixer_fd; int devmask, cmd; int val; if (!config->enable_mixer) return; /* get device name */ mixer_device_name = get_mixer_device_name(); /* open device */ mixer_fd = open(mixer_device_name, O_RDONLY); g_free(mixer_device_name); if (mixer_fd == -1) return; /* check for capabilities */ ioctl(mixer_fd, SOUND_MIXER_READ_DEVMASK, &devmask); if ((devmask & SOUND_MASK_PCM) && !config->oss_mixer_use_master) cmd = SOUND_MIXER_WRITE_PCM; else if ((devmask & SOUND_MASK_VOLUME) && config->oss_mixer_use_master) cmd = SOUND_MIXER_WRITE_VOLUME; else { close(mixer_fd); return; } /* set volume */ val = config->mixer_reverse ? ((l << 8) | r) : ((r << 8) | l); ioctl(mixer_fd, cmd, &val); /* close device */ close(mixer_fd); } gint oss_open_audio(AFormat fmt, int rate, int nch) { gint dsp_format; gint dsp_speed; gint dsp_stereo; guint32 dsp_fragment; gchar *device_name = NULL; audio_buf_info info; struct timeval tv; fd_set set; DEBUG(("[xfade-oss] open_audio: pid=%d\n", getpid())); /* check if we should use as much device buffer as available */ if (config->oss_maxbuf_enable) dsp_fragment = OSS_MAXFRAGMENT; else dsp_fragment = config->oss_fragments << 16 | config->oss_fragment_size; /* check for realtime priority, it needs some special attention */ realtime = xfplayer_check_realtime_priority(); DEBUG(("[xfade-oss] open_audio: fmt=%s rate=%d nch=%d\n", format_name(fmt), rate, nch)); if (dsp_fd != -1) { DEBUG(("[xfade-oss] open_audio: WARNING: device already opened!\n")); return 1; /* no error */ } /* setup format */ if (setup_format(fmt, rate, nch, &format)) { DEBUG(("[xfade-oss] open_audio: format not supported!\n")); return 0; } /* init effect context */ effect_init(&effect_context, EFFECT_USE_XMMS_PLUGIN); /* check format (hardcoded to signed-16-ne, any rate, stereo) */ switch (fmt) { case FMT_S16_LE: #ifdef WORDS_BIGENDIAN dsp_format = -1; #else dsp_format = AFMT_S16_LE; #endif break; case FMT_S16_BE: #ifdef WORDS_BIGENDIAN dsp_format = AFMT_S16_BE; #else dsp_format = -1; #endif break; case FMT_S16_NE: #ifdef AFMT_S16_NE dsp_format = AFMT_S16_NE; #else #ifdef WORDS_BIGENDIAN dsp_format = AFMT_S16_BE; #else dsp_format = AFMT_S16_LE; #endif #endif break; default: dsp_format = -1; } if (dsp_format == -1) { DEBUG(("[xfade-oss] open_audio: can't handle fmt=%d!\n", fmt)); return 0; } /* check rate */ if ((rate < 1) || (rate > MAX_RATE)) { DEBUG(("[xfade-oss] open_audio: illegal rate=%d!\n", rate)); return 0; } dsp_speed = rate; /* check channels */ if (nch != 2) { DEBUG(("[xfade-oss] open_audio: can't handle nch != 2 (%d)!\n", nch)); return 0; } dsp_stereo = TRUE; /* get device name */ if (config->oss_use_alt_audio_device && (config->oss_alt_audio_device != NULL)) device_name = g_strdup(config->oss_alt_audio_device); else if (config->oss_audio_device > 0) device_name = g_strdup_printf("/dev/dsp%d", config->oss_audio_device); else device_name = g_strdup("/dev/dsp"); /* HACK: Test if the device is not locked by another process. This is * just a crude workaround to avoid complete lockup of XMMS. It is * not perfect since it is theoretically possible that another program * locks the device between the close() and open() calls below. */ #if 0 DEBUG(("[xfade-oss] open_audio: WRITE TEST...\n")); dsp_fd = open(device_name, O_WRONLY|O_NONBLOCK); if (dsp_fd == -1) { PERROR("[xfade-oss] open_audio"); g_free(device_name); return 0; } close(dsp_fd); DEBUG(("[xfade-oss] open_audio: WRITE TEST... done\n")); #endif /* open device (we do need blocking io here!) */ dsp_fd = open(device_name, O_WRONLY); g_free(device_name); if (dsp_fd == -1) { PERROR("[xfade-oss] open_audio"); return 0; } /* configure dsp */ if ((ioctl(dsp_fd, SNDCTL_DSP_SETFMT, &dsp_format) == -1) || (ioctl(dsp_fd, SNDCTL_DSP_STEREO, &dsp_stereo) == -1) || (ioctl(dsp_fd, SNDCTL_DSP_SPEED, &dsp_speed) == -1)) { DEBUG(("[xfade-oss] open_audio: error configuring dsp!\n")); close(dsp_fd); dsp_fd = -1; return 0; } /* set fragment size/count */ if (!config->oss_maxbuf_enable) if (ioctl(dsp_fd, SNDCTL_DSP_SETFRAGMENT, &dsp_fragment) == -1) PERROR("[xfade-oss] open_audio: ioctl(SETFRAGMENT)"); /* get dsp_buffer_size */ if (ioctl(dsp_fd, SNDCTL_DSP_GETOSPACE, &info) == -1) { PERROR("[xfade-oss] open_audio: ioctl(GETOSPACE)"); close(dsp_fd); dsp_fd = -1; return 0; } dsp_buffer_size = info.fragsize * info.fragstotal; dsp_fragment_size = info.fragsize; dsp_fragment_utime = (gint) ((gint64) dsp_fragment_size * (1000000 / 2 / 2) / OUTPUT_RATE); /* HACK: (from OSS/audio.c) find out if the driver supports selects */ tv.tv_sec = 0; tv.tv_usec = 50000; FD_ZERO(&set); FD_SET(dsp_fd, &set); dsp_select_works = select(dsp_fd + 1, NULL, &set, NULL, &tv) > 0; if (!dsp_select_works) { DEBUG(("[xfade-oss] open_audio: select() does not work with this audio driver!\n")); DEBUG(("[xfade-oss] open_audio: ... enabled workaround (polling)\n")); } /* done */ DEBUG(("[xfade-oss] open_audio: device: fragments=%d fragstotal=%d\n", info.fragments, info.fragstotal)); DEBUG(("[xfade-oss] open_audio: device: fragsize=%d bytes=%d (%d ms)\n", info.fragsize, info.bytes, B2MS(info.bytes))); /* calculate buffer size */ buffer_buffer_size = MS2B(config->oss_buffer_size_ms) & -4; buffer_preload_size = MS2B(config->oss_preload_size_ms) & -4; buffer_size = buffer_buffer_size + buffer_preload_size; if (buffer_size < dsp_fragment_size) { DEBUG(("[xfade-oss] open_audio: buffer size adjusted to match fragment size\n")); buffer_preload_size += dsp_fragment_size - buffer_size; buffer_size = dsp_fragment_size; } buffer_size += dsp_buffer_size; /* for pause rewind */ /* allocate buffer */ if (!(buffer_data = g_malloc0(buffer_size))) { DEBUG(("[xfade-oss] open_audio: error allocating %d bytes " "of buffer memory!\n", buffer_size)); close(dsp_fd); dsp_fd = -1; return 0; } DEBUG(("[xfade-oss] open_audio: buffer: size=%d (%d+%d+%d=%d ms)\n", buffer_size, B2MS(buffer_buffer_size), B2MS(buffer_preload_size), B2MS(dsp_buffer_size), B2MS(buffer_size))); /* reset buffer */ streampos = 0; buffer_rd_index = 0; buffer_used = 0; buffer_written = 0; buffer_preload = buffer_preload_size; /* make sure stopped is TRUE -- otherwise the buffer thread would * stop again immediatelly after it has been started. */ stopped = FALSE; paused = FALSE; /* create and run buffer thread (detached) */ buffer_thread_finished = FALSE; if (THREAD_CREATE(buffer_thread, buffer_thread_f)) { PERROR("[xfade-oss] open_audio: thread_create()"); buffer_thread_finished = TRUE; g_free(buffer_data); buffer_data = NULL; close(dsp_fd); dsp_fd = -1; return 0; } /* done */ return 1; } void oss_write_audio(void *ptr, int length) { gint free, ofs = 0; /* sanity checks */ if (length <= 0) return; if (dsp_fd == -1) { DEBUG(("[xfade-oss] write_audio: device not opened!\n")); return; } /* lock buffer */ MUTEX_LOCK(&buffer_mutex); /* print warning on buffer overrun */ free = buffer_size - buffer_used; if (length > free) { DEBUG(("[xfade-oss] write_audio: WARNING: %d bytes truncated!\n", length - free)); length = free; } /* update streampos (using input format size) */ streampos += length; /* apply effect FIXME: reenable format change */ length = effect_flow(&effect_context, (gpointer *) &ptr, length, &format, FALSE); /* Update preload. The buffer thread will not write any * data to the device before preload is decreased below 1. */ if ((length > 0) && (buffer_preload > 0)) buffer_preload -= length; /* normal write */ while (length > 0) { gint wr_index = (buffer_rd_index + buffer_used) % buffer_size; gint blen = buffer_size - wr_index; if (blen > length) blen = length; memcpy(buffer_data + wr_index, ptr + ofs, blen); buffer_used += blen; length -= blen; ofs += blen; } /* unlock buffer */ MUTEX_UNLOCK(&buffer_mutex); } void * buffer_thread_f(void *arg) { audio_buf_info info; gpointer data; gint length, blen; fd_set set; int sel; struct timeval tv; DEBUG(("[xfade-oss] buffer_thread_f: thread started\n")); DEBUG(("[xfade-oss]\n")); /* lock buffer */ MUTEX_LOCK(&buffer_mutex); while (dsp_fd != -1) { /* wait for device */ if (dsp_select_works) { tv.tv_sec = 0; tv.tv_usec = dsp_fragment_utime; FD_ZERO(&set); FD_SET(dsp_fd, &set); MUTEX_UNLOCK(&buffer_mutex); sel = select(dsp_fd + 1, NULL, &set, NULL, &tv); MUTEX_LOCK(&buffer_mutex); if (sel == -1) { PERROR("[xfade-oss] buffer_thread_f: select()"); #if 1 MUTEX_UNLOCK(&buffer_mutex); xfade_usleep(dsp_fragment_utime); MUTEX_LOCK(&buffer_mutex); continue; #else break; #endif } else if (!sel) continue; } else { MUTEX_UNLOCK(&buffer_mutex); xfade_usleep(dsp_fragment_utime); MUTEX_LOCK(&buffer_mutex); } /* get free space in device output buffer */ if (ioctl(dsp_fd, SNDCTL_DSP_GETOSPACE, &info)) { PERROR("[xfade-oss] buffer_thread_f: ioctl(GETOSPACE)"); /* 0.3.4: try to reset device */ if (ioctl(dsp_fd, SNDCTL_DSP_RESET, 0)) { PERROR("[xfade-oss] buffer_thread_f: ioctl(RESET)"); break; /* give up */ } continue; } /* continue waiting if there is no room in the device buffer */ if (info.bytes < 4) continue; /* check for buffer underrun */ if (info.fragments == info.fragstotal) { /* FIXME: maybe do some sensible recovering */ } /* is there any data in the buffer? */ if (!paused && (buffer_used >= 4) && (buffer_preload <= 0)) { /* write as much data as a) is available and b) the device can take */ length = buffer_used; if (length > info.bytes) length = info.bytes; /* make sure we always operate on stereo sample boundary */ length &= -4; /* update local write accumulator */ buffer_written += length; /* write length bytes to the device */ while (length > 0) { data = buffer_data + buffer_rd_index; blen = buffer_size - buffer_rd_index; if (blen > length) blen = length; write(dsp_fd, data, blen); buffer_rd_index = (buffer_rd_index + blen) % buffer_size; buffer_used -= blen; length -= blen; } } else if (dsp_select_works) { /* HACK: Force yielding. This is _vital_ in realtime mode. * Without it this thread would eat up all CPU time polling * the audio device with select(), having no samples to write. */ MUTEX_UNLOCK(&buffer_mutex); xfade_usleep(dsp_fragment_utime / 4); MUTEX_LOCK(&buffer_mutex); } } /* unlock buffer */ MUTEX_UNLOCK(&buffer_mutex); DEBUG(("[xfade-oss] buffer_thread_f: thread finished\n")); buffer_thread_finished = TRUE; #ifdef USE_PTHREAD_JOIN THREAD_EXIT(0); #endif return NULL; } void oss_close_audio() { /* lock buffer */ MUTEX_LOCK(&buffer_mutex); /* free buffer */ g_free(buffer_data); /* close device */ if (dsp_fd != -1) { ioctl(dsp_fd, SNDCTL_DSP_RESET, NULL); close(dsp_fd); dsp_fd = -1; } /* unlock buffer */ MUTEX_UNLOCK(&buffer_mutex); /* wait for buffer thread to terminate */ #ifdef USE_PTHREAD_JOIN if (THREAD_JOIN(buffer_thread, NULL)) PERROR("[xfade-oss] close_audio: pthread_join()"); #else while (!buffer_thread_finished) xfade_usleep(10000); #endif /* free effect private data */ effect_free(&effect_context); } void oss_flush(int time) { /* lock buffer */ MUTEX_LOCK(&buffer_mutex); /* update streampos with new stream position (input format size) */ streampos = (gint64) time *format.bps / 1000; /* reset dsp */ ioctl(dsp_fd, SNDCTL_DSP_RESET, NULL); /* reset buffer */ buffer_rd_index = 0; buffer_used = 0; buffer_written = 0; buffer_preload = buffer_preload_size; /* unlock buffer */ MUTEX_UNLOCK(&buffer_mutex); } void oss_pause(short p) { audio_buf_info info; gint rewind; /* lock buffer */ MUTEX_LOCK(&buffer_mutex); paused = p; if (paused) { /* how much data is left in the device buffer? */ if ((dsp_fd == -1) || (ioctl(dsp_fd, SNDCTL_DSP_GETOSPACE, &info) == -1)) { MUTEX_UNLOCK(&buffer_mutex); return; } rewind = dsp_buffer_size - info.bytes; /* don't rewind too far if playback just has started */ if (rewind > buffer_written) rewind = buffer_written; /* make sure we always operate on sample boundaries */ /* NOTE: this one does happen quite often */ rewind &= -4; /* sanity check (should never happen) */ if (rewind > (buffer_size - buffer_used)) { DEBUG(("[xfade-oss] pause: rewind too large (%d, free=%d)!\n", rewind, buffer_size - buffer_used)); rewind = buffer_size - buffer_used; } /* reset device, this will stop playback immediatelly */ ioctl(dsp_fd, SNDCTL_DSP_RESET, NULL); /* rewind buffer */ buffer_rd_index -= rewind; if (buffer_rd_index < 0) buffer_rd_index += buffer_size; buffer_used += rewind; buffer_written -= rewind; } /* unlock buffer */ MUTEX_UNLOCK(&buffer_mutex); } gint oss_buffer_free() { gint size, free; /* lock buffer */ MUTEX_LOCK(&buffer_mutex); if (paused) size = buffer_size; else size = buffer_size - dsp_buffer_size; free = size - buffer_used; if (free < 0) free = 0; /* unlock buffer */ MUTEX_UNLOCK(&buffer_mutex); return free; } gint oss_buffer_playing() { audio_buf_info info; gint dsp_used; gboolean playing; /* lock buffer */ MUTEX_LOCK(&buffer_mutex); if ((dsp_fd == -1) || (ioctl(dsp_fd, SNDCTL_DSP_GETOSPACE, &info) == -1)) { MUTEX_UNLOCK(&buffer_mutex); return 0; } dsp_used = dsp_buffer_size - info.bytes; playing = (buffer_used > 0) || (dsp_used >= 3 * dsp_fragment_size); /* unlock buffer */ MUTEX_UNLOCK(&buffer_mutex); return playing; } gint oss_output_time() { audio_buf_info info; gint time; /* lock buffer */ MUTEX_LOCK(&buffer_mutex); if ((dsp_fd == -1) || (ioctl(dsp_fd, SNDCTL_DSP_GETOSPACE, &info) == -1)) { MUTEX_UNLOCK(&buffer_mutex); return 0; } time = oss_written_time() - B2MS(buffer_used + (dsp_buffer_size - info.bytes)); if (time < 0) time = 0; /* unlock buffer */ MUTEX_UNLOCK(&buffer_mutex); return time; } gint oss_written_time() { return streampos * 1000 / format.bps; }