Mercurial > audlegacy-plugins
diff src/crossfade/crossfade.c @ 3059:2e241e90494a
Import work in progress xmms-crossfade rewrite.
author | William Pitcock <nenolod@atheme.org> |
---|---|
date | Fri, 24 Apr 2009 05:57:35 -0500 |
parents | |
children | 43a336a7791b |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/crossfade/crossfade.c Fri Apr 24 05:57:35 2009 -0500 @@ -0,0 +1,2548 @@ +/* + * XMMS Crossfade Plugin + * Copyright (C) 2000-2007 Peter Eisenlohr <peter@eisenlohr.org> + * + * 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. + */ +/* indent -i8 -ts8 -hnl -bli0 -l128 -npcs -cli8 */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include "crossfade.h" +#include "cfgutil.h" +#include "format.h" +#include "convert.h" +#include "timing.h" + +#include "configure.h" +#include "monitor.h" + +#include "interface-2.0.h" +#include "support-2.0.h" + +#ifdef HAVE_LIBFFTW +# include "fft.h" +#endif + +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#include <ctype.h> + +#ifdef HAVE_DLFCN_H +# include <dlfcn.h> +#endif +#include <unistd.h> +#include <signal.h> +#include <sys/time.h> +#include <sys/types.h> +#include <sys/ioctl.h> + +#undef DEBUG_HARDCORE + +/* output plugin callback prototypes */ +static void xfade_init(); +static void xfade_cleanup(); /* audacious and patched only */ +static void xfade_set_volume(int l, int r); +static void xfade_get_volume(int *l, int *r); +static gint xfade_open_audio(AFormat fmt, int rate, int nch); +static void xfade_write_audio(void *ptr, int length); +static void xfade_close_audio(); +static void xfade_flush(int time); +static void xfade_pause(short paused); +static gint xfade_buffer_free(); +static gint xfade_buffer_playing(); +static gint xfade_written_time(); +static gint xfade_output_time(); + +/* output plugin callback table (extended, needs patched player) */ +static struct +{ + OutputPlugin xfade_op; + void (*cleanup) (void); +} +xfade_op_private = +{ + { + .description = "Crossfade Plugin", + .init = xfade_init, + .cleanup = xfade_cleanup, + .get_volume = xfade_get_volume, + .set_volume = xfade_set_volume, + .open_audio = xfade_open_audio, + .write_audio = xfade_write_audio, + .close_audio = xfade_close_audio, + .flush = xfade_flush, + .pause = xfade_pause, + .buffer_free = xfade_buffer_free, + .buffer_playing = xfade_buffer_playing, + .output_time = xfade_output_time, + .written_time = xfade_written_time, + .about = xfade_about, + .configure = xfade_configure, + .probe_priority = 2, + }, + NULL +}; + +static OutputPlugin *xfade_op = &xfade_op_private.xfade_op; + +static OutputPlugin *xfade_oplist[] = { &xfade_op_private.xfade_op, NULL }; +DECLARE_PLUGIN(crossfade, NULL, NULL, NULL, xfade_oplist, NULL, NULL, NULL); + +/* internal prototypes */ +static void load_symbols(); +static void output_list_hack(); +static gint open_output(); +static void buffer_reset(buffer_t *buf, config_t *cfg); +static void *buffer_thread_f(void *arg); +static void sync_output(); + +/* special XMMS symbols (dynamically looked up, see xfade_init) */ +static gboolean *xmms_playlist_get_info_going = NULL; /* XMMS */ +static gboolean *xmms_is_quitting = NULL; /* XMMS */ +static gboolean *input_stopped_for_restart = NULL; /* XMMS */ +static char * (*playlist_get_fadeinfo)(int) = NULL; /* XMMS patch */ + +static void (*xmms_input_get_song_info)(gchar *, gchar **, gint *); /* XMMS */ +static gchar **xmms_gentitle_format = NULL; /* XMMS private cfg */ + +/* This function has been stolen from libxmms/util.c. */ +void xfade_usleep(gint usec) +{ +#if defined(HAVE_G_USLEEP) + g_usleep(usec); +#elif defined(HAVE_NANOSLEEP) + struct timespec req; + + req.tv_sec = usec / 1000000; + usec -= req.tv_sec * 1000000; + req.tv_nsec = usec * 1000; + + nanosleep(&req, NULL); +#else + struct timeval tv; + + tv.tv_sec = usec / 1000000; + usec -= tv.tv_sec * 1000000; + tv.tv_usec = usec; + select(0, NULL, NULL, NULL, &tv); +#endif +} + + +/* local variables */ +static gboolean realtime; +static gboolean is_http; + +static gint64 streampos; /* position within current song (input bps) */ +static gboolean playing; +gboolean opened; /* TRUE between open_audio() and close_audio() */ +static gboolean paused; /* TRUE: no playback (but still filling buffer) */ +static gboolean stopped; /* TRUE: stop buffer thread ASAP */ +static gboolean eop; /* TRUE: wait until buffer is empty then sync() */ + +#ifdef HAVE_OSS /* avoid 'defined but not used' compiler warning */ +static plugin_config_t default_op_config = DEFAULT_OP_CONFIG; +#endif +static plugin_config_t the_op_config = DEFAULT_OP_CONFIG; + OutputPlugin *the_op = NULL; + gint the_rate = 44100; + +static gboolean input_playing = FALSE; + + gboolean output_opened = FALSE; + gboolean output_restart = FALSE; /* used by XMMS 'songchange' patch */ +static gint output_flush_time = 0; + gint output_offset = 0; +static gint64 output_written = 0; + gint64 output_streampos = 0; + +static gchar zero_4k[4096]; + +#ifdef TIMING_COMMENTS +typedef struct +{ + gboolean enable; + gint len_ms, volume, skip_ms, ofs_ms; +} +timing_half_config_t; + +typedef struct +{ + timing_half_config_t in; + timing_half_config_t out; +} +timing_config_t; + +static timing_config_t last_timing, current_timing; +#endif + +/* + * Available fade configs: + * + * fc_start: First song, only in_len and in_level are used + * fc_xfade: Automatic crossfade at end of song + * fc_album: Like xfade but for consecutive songs of the same album + * fc_manual: Manual crossfade (triggered by Next or Prev) + * fc_stop: Last song, only out_len and out_level are used + * fc_eop: Last song, only out_len and out_level are used + * + * NOTE: As of version 0.2 of xmms-crossfade, + * only xfade and manual are implemented. + * + * With version 0.2.3, fc_album has been added. + * + * With 0.2.4, all configs are implemented: + * + * Available parameters: + * + * | start | xfade | manual | album | stop | eop + * ------------+-------+-------+--------+-------+------+------ + * in_len | yes | yes | yes | no | no | no + * in_volume | yes | yes | yes | no | no | no + * offset | no | yes | yes | no | +yes | +yes + * out_len | no | yes | yes | no | yes | yes + * out_volume | no | yes | yes | no | yes | yes + * flush (*) | no | no | yes | no | yes | no + * ------------+-------+-------+--------+-------+------+------ + * + * Parameters marked with (*) are not configureable by the user + * + * The offset parameters for 'stop' and 'eop' is used to store the + * length of the additional silence to be added. It may be >= 0 only. + * + */ + +static struct timeval last_close; +static struct timeval last_write; + +static gchar *last_filename = NULL; + +static format_t in_format; +static format_t out_format; + +static buffer_t the_buffer; + buffer_t *buffer = &the_buffer; + +static THREAD buffer_thread; + MUTEX buffer_mutex = MUTEX_INITIALIZER; + +static convert_context_t convert_context; +#ifdef HAVE_LIBFFTW +static fft_context_t fft_context; +#endif + +static config_t the_config; + config_t *config = &the_config; + config_t config_default = CONFIG_DEFAULT; + +static fade_config_t *fade_config = NULL; + + +/* this is the entry point for XMMS */ +OutputPlugin * +get_oplugin_info() +{ + return xfade_op; +} + +OutputPlugin * +get_crossfade_oplugin_info() +{ + return xfade_op; +} + +static gboolean +open_output_f(gpointer data) +{ + DEBUG(("[crossfade] open_output_f: pid=%d\n", getpid())); + open_output(); + return FALSE; /* FALSE = 'do not call me again' */ +} + +void +xfade_realize_config() /* also called by xfade_init() */ +{ + /* 0.3.0: keep device opened */ + if (config->output_keep_opened && !output_opened) + { + DEBUG(("[crossfade] realize_config: keeping output opened...\n")); + + /* 0.3.1: HACK: this will make sure that we start playing silence after startup */ + gettimeofday(&last_close, NULL); + + /* 0.3.1: HACK: Somehow, if we open output here at XMMS startup, there + will be leftover filedescriptors later when closing output again. + Opening output in a timeout function seems to work around this... */ + DEBUG(("[crossfade] realize_config: adding timeout (pid=%d)\n", (int) getpid())); + g_timeout_add(0, open_output_f, NULL); + } +} + +static gint +output_list_f(gconstpointer a, gconstpointer b) +{ + OutputPlugin *op = (OutputPlugin *) a; + gchar *name = (gchar *) b; + + return strcmp(g_basename(op->filename), name); +} + +static OutputPlugin * +find_output() +{ + GList *list, *element; + OutputPlugin *op = NULL; + + /* find output plugin */ + { + if (config->op_name && (list = xfplayer_get_output_list())) + if ((element = g_list_find_custom(list, config->op_name, output_list_f))) + op = element->data; + + if (op == xfade_op) + { + DEBUG(("[crossfade] find_output: can't use myself as output plugin!\n")); + op = NULL; + } + else if (!op) + { + DEBUG(("[crossfade] find_output: could not find output plugin \"%s\"\n", + config->op_name ? config->op_name : "#NULL#")); + } + else /* ok, we have a plugin. last, get its compatibility options */ + xfade_load_plugin_config(config->op_config_string, config->op_name, &the_op_config); + } + + return op; +} + +static gint +open_output() +{ + /* sanity check */ + if (output_opened) + DEBUG(("[crossfade] open_output: WARNING: output_opened=TRUE!\n")); + + /* reset output_* */ + output_opened = FALSE; + output_flush_time = 0; + output_offset = 0; + output_written = 0; + output_streampos = 0; + + /* get output plugin (this will also init the_op_config) */ + if (!(the_op = find_output())) + { + DEBUG(("[crossfade] open_output: could not find any output!\n")); + return -1; + } + + /* print output plugin info */ + DEBUG(("[crossfade] open_output: using \"%s\" for output", the_op->description ? the_op->description : "#NULL#")); + + if (realtime) + DEBUG((" (RT)")); + + if (the_op_config.throttle_enable) + DEBUG((realtime ? " (throttled (disabled with RT))" : " (throttled)")); + + if (the_op_config.max_write_enable) + DEBUG((" (max_write=%d)", the_op_config.max_write_len)); + + DEBUG(("\n")); + + /* setup sample rate (note that OUTPUT_RATE is #defined as the_rate) */ + the_rate = config->output_rate; + + /* setup out_format. use host byte order for easy math */ + setup_format(FMT_S16_NE, OUTPUT_RATE, OUTPUT_NCH, &out_format); + + /* open plugin */ + if (!the_op->open_audio(out_format.fmt, out_format.rate, out_format.nch)) + { + DEBUG(("[crossfade] open_output: open_audio() failed!\n")); + the_op = NULL; + return -1; + } + + /* clear buffer struct */ + memset(buffer, 0, sizeof(*buffer)); + + /* calculate buffer size */ + buffer->mix_size = MS2B(xfade_mix_size_ms(config)) & -4; + buffer->sync_size = MS2B(config->sync_size_ms) & -4; + buffer->preload_size = MS2B(config->preload_size_ms) & -4; + + buffer->size = (buffer->mix_size + /* mixing area */ + buffer->sync_size + /* additional sync */ + buffer->preload_size); /* preload */ + + DEBUG(("[crossfade] open_output: buffer: size=%d (%d+%d+%d=%d ms) (%d Hz)\n", + buffer->size, + B2MS(buffer->mix_size), + B2MS(buffer->preload_size), + B2MS(buffer->sync_size), + B2MS(buffer->size), + the_rate)); + + /* allocate buffer */ + if (!(buffer->data = g_malloc0(buffer->size))) + { + DEBUG(("[crossfade] open_output: error allocating buffer!\n")); + the_op->close_audio(); + the_op = NULL; + return -1; + } + + /* reset buffer */ + buffer_reset(buffer, config); + + /* make sure stopped is TRUE -- otherwise the buffer thread would + * stop again immediatelly after it has been started. */ + stopped = FALSE; + + /* create and run buffer thread */ + if (THREAD_CREATE(buffer_thread, buffer_thread_f)) + { + PERROR("[crossfade] open_output: thread_create()"); + g_free(buffer->data); + the_op->close_audio(); + the_op = NULL; + return -1; + } + SCHED_YIELD; + + /* start updating monitor */ + xfade_start_monitor(); + + /* done */ + output_opened = TRUE; + return 0; +} + +static void +xfade_init() +{ + /* load config */ + memset(config, 0, sizeof(*config)); + *config = config_default; + xfade_load_config(); + + /* set default strings if there is no existing config */ + if (!config->oss_alt_audio_device) config->oss_alt_audio_device = g_strdup(DEFAULT_OSS_ALT_AUDIO_DEVICE); + if (!config->oss_alt_mixer_device) config->oss_alt_mixer_device = g_strdup(DEFAULT_OSS_ALT_MIXER_DEVICE); + if (!config->op_config_string) config->op_config_string = g_strdup(DEFAULT_OP_CONFIG_STRING); + if (!config->op_name) config->op_name = g_strdup(DEFAULT_OP_NAME); + + /* check for realtime priority, it needs some special attention */ + realtime = xfplayer_check_realtime_priority(); + + /* show monitor win if enabled in config */ + xfade_check_monitor_win(); + + /* init contexts */ + convert_init(&convert_context); +#ifdef HAVE_LIBFFTW + fft_init(&fft_context); +#endif + + /* reset */ + stopped = FALSE; + + /* find current output plugin early so that volume control works + * even if playback has not started yet. */ + if (!(the_op = find_output())) + DEBUG(("[crossfade] init: could not find any output!\n")); + + /* load any dynamic linked symbols */ + load_symbols(); + + /* HACK: make sure we are at the beginning of XMMS' output plugin list */ + output_list_hack(); + + /* realize config -- will also setup the pre-mixing effect plugin */ + xfade_realize_config(); +} + +static void +load_symbols() +{ +#ifdef HAVE_DLFCN_H + void *handle; + char *error; + gchar **xmms_cfg; + gchar * (*get_gentitle_format)(); + + /* open ourselves (that is, the XMMS binary) */ + handle = dlopen(NULL, RTLD_NOW); + if (!handle) + { + DEBUG(("[crossfade] init: dlopen(NULL) failed!\n")); + return; + } + + /* check for XMMS patches */ + DEBUG(("[crossfade] load_symbols: input_stopped_for_restart:")); + input_stopped_for_restart = dlsym(handle, "input_stopped_for_restart"); + DEBUG((!(error = dlerror())? " found\n" : " missing\n")); + + DEBUG(("[crossfade] load_symbols: is_quitting:")); + xmms_is_quitting = dlsym(handle, "is_quitting"); + DEBUG((!(error = dlerror())? " found\n" : " missing\n")); + + DEBUG(("[crossfade] load_symbols: playlist_get_fadeinfo:")); + playlist_get_fadeinfo = dlsym(handle, "playlist_get_fadeinfo"); + DEBUG((!(error = dlerror())? " found\n" : " missing\n")); + + /* check for some XMMS functions */ + xmms_playlist_get_info_going = dlsym(handle, "playlist_get_info_going"); + xmms_input_get_song_info = dlsym(handle, "input_get_song_info"); + + /* HACK: direct access to XMMS' config 'gentitle_format' */ + xmms_cfg = dlsym(handle, "cfg"); + get_gentitle_format = dlsym(handle, "xmms_get_gentitle_format"); + if (xmms_cfg && get_gentitle_format) + { + gchar *format = get_gentitle_format(); + + int i = 128; + gchar **p = (gchar **)xmms_cfg; + for (i = 128; i > 0 && *p != format; i--, p++); + if (*p == format) + xmms_gentitle_format = p; + } + + dlclose(handle); +#endif +} + +/* + HACK: Try to move ourselves to the beginning of XMMS output plugin list, + so that we will be freed first when XMMS is quitting. This way, we + avoid the segfault when using ALSA as the output plugin. +*/ +static void +output_list_hack() +{ + GList *output_list = xfplayer_get_output_list(); + if (!output_list) + return; + + int i0 = g_list_index(output_list, xfade_op), i1; + + GList *first = g_list_first(output_list); + GList *xfade = g_list_find(output_list, xfade_op); + xfade->data = first->data; + first->data = xfade_op; + + i1 = g_list_index(output_list, xfade_op); + if (i0 != i1) + DEBUG(("[crossfade] output_list_hack: crossfade moved from index %d to %d\n", i0, i1)); +} + +void +xfade_get_volume(int *l, int *r) +{ + if (config->mixer_software) + { + *l = config->mixer_reverse + ? config->mixer_vol_right + : config->mixer_vol_left; + *r = config->mixer_reverse + ? config->mixer_vol_left + : config->mixer_vol_right; + } + else + { + if (the_op && the_op->get_volume) + { + if (config->mixer_reverse) + the_op->get_volume(r, l); + else + the_op->get_volume(l, r); + } + } + + /* DEBUG(("[crossfade] xfade_get_volume: l=%d r=%d\n", *l, *r)); */ +} + +void +xfade_set_volume(int l, int r) +{ + /* DEBUG(("[crossfade] xfade_set_volume: l=%d r=%d\n", l, r)); */ + + if (!config->enable_mixer) + return; + + if (the_op && the_op->set_volume) + { + if (config->mixer_reverse) + the_op->set_volume(r, l); + else + the_op->set_volume(l, r); + } +} + +/*** buffer stuff ***********************************************************/ + +static void +buffer_mfg_reset(buffer_t *buf, config_t *cfg) +{ + buf->mix = 0; + buf->fade = 0; + buf->gap = (cfg->gap_lead_enable ? MS2B(cfg->gap_lead_len_ms) & -4 : 0); + buf->gap_len = buf->gap; + buf->gap_level = cfg->gap_lead_level; + buf->gap_killed = 0; + buf->skip = 0; + buf->skip_len = 0; +} + +static void +buffer_reset(buffer_t *buf, config_t *cfg) +{ + buffer_mfg_reset(buf, cfg); + + buf->rd_index = 0; + buf->used = 0; + buf->preload = buf->preload_size; + + buf->silence = 0; + buf->silence_len = 0; + buf->reopen = -1; + buf->pause = -1; +} + +/****************************************************************************/ + +static void +xfade_apply_fade_config(fade_config_t *fc) +{ + gint out_skip, in_skip; + gint avail, out_len, in_len, offset, preload; + gint index, length, fade, n; + gfloat out_scale, in_scale; + gboolean out_skip_clipped = FALSE; + gboolean out_len_clipped = FALSE; + gboolean offset_clipped = FALSE; + + /* Overwrites mix and fade; may add silence */ + + /* + * Example 1: offset < 0 --> mix streams together + * Example 2: offset > 0 --> insert pause between streams + * + * |----- out_len -----| * |out_len| + * | | * | | + * ~~~~~-_ /T~~~~~~~T~~ * ~~~~~\ | /T~~ + * ~-_ / | | * \ | / | + * ~-_/ | | * \ | / | + * /~-_| | * \ | / | + * / T-_ | * \ | / | + * / | ~-_ | * \ | / | + * _________/______|_____~-|__ * ___________\__________/______|__ + * |in_len| | * | |in_len| + * |<-- offset ---| * |offset-->| + * + * a) avail: max(0, used - preload) + * b) out_len: 0 .. avail + * c) in_len: 0 .. # + * d) offset: -avail .. buffer->mix_size - out_size + * e) skip: min(used, preload) + * + */ + + out_scale = 1.0f - (gfloat) xfade_cfg_fadeout_volume(fc) / 100.0f; + in_scale = 1.0f - (gfloat) xfade_cfg_fadein_volume (fc) / 100.0f; + + /* rules (see above) */ + /* a: leave preload untouched */ + avail = buffer->used - buffer->preload_size; + if (avail < 0) + avail = 0; + + /* skip end of song */ + out_skip = MS2B(xfade_cfg_out_skip(fc)) & -4; + if (out_skip > avail) + { + DEBUG(("[crossfade] apply_fade_config: WARNING: clipping out_skip (%d -> %d)!\n", B2MS(out_skip), B2MS(avail))); + out_skip = avail; + out_skip_clipped = TRUE; + } + + if (out_skip > 0) + { + buffer->used -= out_skip; + avail -= out_skip; + } + + /* b: fadeout */ + out_len = MS2B(xfade_cfg_fadeout_len(fc)) & -4; + if (out_len > avail) + { + DEBUG(("[crossfade] apply_fade_config: WARNING: clipping out_len (%d -> %d)!\n", B2MS(out_len), B2MS(avail))); + out_len = avail; + out_len_clipped = TRUE; + } + else if (out_len < 0) + out_len = 0; + + /* skip beginning of song */ + in_skip = MS2B(xfade_cfg_in_skip(fc)) & -4; + if (in_skip < 0) + in_skip = 0; + + /* c: fadein */ + in_len = MS2B(xfade_cfg_fadein_len(fc)) & -4; + if (in_len < 0) + in_len = 0; + + /* d: offset (mixing point) */ + offset = MS2B(xfade_cfg_offset(fc)) & -4; + if (offset < -avail) + { + DEBUG(("[crossfade] apply_fade_config: WARNING: clipping offset (%d -> %d)!\n", B2MS(offset), -B2MS(avail))); + offset = -avail; + offset_clipped = TRUE; + } + if (offset > (buffer->mix_size - out_len)) + offset = buffer->mix_size - out_len; + + /* e */ + preload = buffer->preload_size; + if (preload > buffer->used) + preload = buffer->used; + + /* cut off rest of stream (decreases latency on manual songchange) */ + if (fc->flush) + { + gint cutoff = avail - MAX(out_len, -offset); /* MAX() -> glib.h */ + if (cutoff > 0) + { + DEBUG(("[crossfade] apply_fade_config: %d ms flushed\n", B2MS(cutoff))); + buffer->used -= cutoff; + avail -= cutoff; + } + + /* make sure there is no pending silence */ + buffer->silence = 0; + buffer->silence_len = 0; + } + + /* begin modifying buffer at index */ + index = (buffer->rd_index + buffer->used - out_len) % buffer->size; + + /* fade out (modifies buffer directly) */ + fade = 0; + length = out_len; + while (length > 0) + { + gint16 *p = buffer->data + index; + gint blen = buffer->size - index; + if (blen > length) blen = length; + + for (n = blen / 4; n > 0; n--) + { + gfloat factor = 1.0f - (((gfloat) fade / out_len) * out_scale); + *p = (gfloat)*p * factor; p++; + *p = (gfloat)*p * factor; p++; + fade += 4; + } + + index = (index + blen) % buffer->size; + length -= blen; + } + + /* Initialize fadein. Note that the actual fading / mixing will be done + * on-the-fly when audio data is received by xfade_write_audio() */ + + /* start skipping */ + if (in_skip > 0) + { + buffer->skip = in_skip; + buffer->skip_len = in_skip; + } + else + buffer->skip = 0; + + /* start fading in */ + if (in_len > 0) + { + buffer->fade = in_len; + buffer->fade_len = in_len; + buffer->fade_scale = in_scale; + } + else + buffer->fade = 0; + + /* start mixing */ + if (offset < 0) + { + length = -offset; + buffer->mix = length; + buffer->used -= length; + } + else + buffer->mix = 0; + + /* start silence if applicable (will be applied in buffer_thread_f) */ + if (offset > 0) + { + if ((buffer->silence > 0) || (buffer->silence_len > 0)) + DEBUG(("[crossfade] apply_config: WARNING: silence in progress (%d/%d ms)\n", + B2MS(buffer->silence), B2MS(buffer->silence_len))); + + buffer->silence = buffer->used; + buffer->silence_len = offset; + } + + /* done */ + if (in_skip || out_skip) + DEBUG(("[crossfade] apply_fade_config: out_skip=%d in_skip=%d\n", B2MS(out_skip), B2MS(in_skip))); + DEBUG(("[crossfade] apply_fade_config: avail=%d out=%d in=%d offset=%d preload=%d\n", + B2MS(avail), B2MS(out_len), B2MS(in_len), B2MS(offset), B2MS(preload))); +} + +static gint +extract_track(const gchar *name) +{ +#if 1 + /* skip non-digits at beginning */ + while (*name && !isdigit(*name)) + name++; + + return atoi(name); +#else + /* Remove all but numbers. + * Will not work if a filename has number in the title, like "track-03-U2.mp3" + * Ideally, should look into id3 track entry and fallback to filename + * */ + gchar temp[8]; + int t = 0; + + memset(temp, 0, sizeof(temp)); + while (*name != '\0' && t < sizeof(temp)) + { + if (strcmp(name, "mp3") == 0) + break; + + if (isdigit(*name)) + temp[t++] = *name; + + name++; + } + return atoi(temp); +#endif +} + +static gint +album_match(gchar *old, gchar *new) +{ + gchar *old_dir, *new_dir; + gboolean same_dir; + gint old_track = 0, new_track = 0; + + if (!old || !new) + return 0; + + old_dir = g_dirname(old); + new_dir = g_dirname(new); + same_dir = !strcmp(old_dir, new_dir); + g_free(old_dir); + g_free(new_dir); + + if (!same_dir) + { + DEBUG(("[crossfade] album_match: no match (different dirs)\n")); + return 0; + } + + old_track = extract_track(g_basename(old)); + new_track = extract_track(g_basename(new)); + + if (new_track <= 0) + { + DEBUG(("[crossfade] album_match: can't parse track number:\n")); + DEBUG(("[crossfade] album_match: ... \"%s\"\n", g_basename(new))); + return 0; + } + + if ((old_track < 0) || (old_track + 1 != new_track)) + { + DEBUG(("[crossfade] album_match: no match (same dir, but non-successive (%d, %d))\n", old_track, new_track)); + return 0; + } + + DEBUG(("[crossfade] album_match: match detected (same dir, successive tracks (%d, %d))\n", old_track, new_track)); + + return old_track; +} + +static gint +xfade_open_audio(AFormat fmt, int rate, int nch) +{ + gint pos; + gchar *file, *title, *comment; +#if defined(HAVE_ID3LIB) + id3_t id3; +#endif + + struct timeval tv; + glong dt; + + DEBUG(("[crossfade]\n")); + DEBUG(("[crossfade] open_audio: pid=%d\n", (int) getpid())); + + /* sanity... don't do anything about it */ + if (opened) + DEBUG(("[crossfade] open_audio: WARNING: already opened!\n")); + + /* get filename */ + pos = xfplaylist_get_position (); + file = xfplaylist_get_filename (pos); + title = xfplaylist_get_songtitle(pos); + comment = playlist_get_fadeinfo ? playlist_get_fadeinfo(pos) : NULL; + + if (!file) + file = g_strdup(title); + + DEBUG(("[crossfade] open_audio: bname=\"%s\"\n", g_basename(file))); + DEBUG(("[crossfade] open_audio: title=\"%s\"\n", title)); + +#if 0 + /* HACK: try to get comment and track number from xmms by sneaking in a custom title format */ + if (xmms_gentitle_format) + { + gchar *old_gentitle_format = *xmms_gentitle_format; + + gchar *temp_title = NULL; + gint temp_length = 0; + + xmms_input_get_song_info(file, &temp_title, &temp_length); + DEBUG(("[crossfade] open_audio: TITLE: %s\n", temp_title)); + g_free(temp_title); + + *xmms_gentitle_format = "%n/%c"; + + xmms_input_get_song_info(file, &temp_title, &temp_length); + DEBUG(("[crossfade] open_audio: TRACK/COMMENT: %s\n", temp_title)); + g_free(temp_title); + + *xmms_gentitle_format = old_gentitle_format; + } +#endif + + /* try to read comment from ID3 tag */ +#if defined(HAVE_ID3LIB) + if (!comment && get_id3(file, &id3)) + comment = g_strdup(id3.comment); +#endif + + if (comment) + DEBUG(("[crossfade] open_audio: comment=\"%s\"\n", comment)) + else + DEBUG(("[crossfade] open_audio: comment=NULL\n")); + + /* is this an automatic crossfade? */ + if (last_filename && (fade_config == &config->fc[FADE_CONFIG_XFADE])) + { + /* check if next song is the same as the current one */ + if (config->no_xfade_if_same_file && !strcmp(last_filename, file)) + { + DEBUG(("[crossfade] open_audio: same file, disabling crossfade\n")); + fade_config = &config->fc[FADE_CONFIG_ALBUM]; + } + + /* check if next song is the next song from the same album */ + else if (config->album_detection && album_match(last_filename, file)) + { + gboolean use_fc_album = FALSE; + + if (xfade_cfg_gap_trail_enable(config)) + { + DEBUG(("[crossfade] album_match: " + "trailing gap: length=%d/%d ms\n", B2MS(buffer->gap_killed), B2MS(buffer->gap_len))); + + if (buffer->gap_killed < buffer->gap_len) + { + DEBUG(("[crossfade] album_match: " + "trailing gap: -> no silence, probably pre-faded\n")); + use_fc_album = TRUE; + } + else + { + DEBUG(("[crossfade] album_match: " "trailing gap: -> silence, sticking to XFADE\n")); + } + } + else + { + DEBUG(("[crossfade] album_match: " "trailing gap killer disabled\n")); + use_fc_album = TRUE; + } + + if (use_fc_album) + { + DEBUG(("[crossfade] album_match: " "-> using FADE_CONFIG_ALBUM\n")); + fade_config = &config->fc[FADE_CONFIG_ALBUM]; + } + } + } + g_free(last_filename); + last_filename = g_strdup(file); + +#if 0 + /* FIXME: finish this */ + /* Check if this is a short song. */ + if (fade_config == &config->fc[FADE_CONFIG_XFADE]) + { + DEBUG(("*** XFADE:\n")); + int current_length = playlist_get_current_length(); + DEBUG(("*** length=%d\n", current_length)); + if (current_length < 30 * 1000) + fade_config = &config->fc[FADE_CONFIG_ALBUM]; + } +#endif + +#ifdef TIMING_COMMENTS + last_timing = current_timing; + current_timing. in.enable = FALSE; + current_timing.out.enable = FALSE; + if (comment) + { + gchar *str; + if ((str = strstr(comment, "fadein="))) + { + current_timing.in.enable = + (3 == sscanf(str + 7, "%d,%d,%d", + ¤t_timing.in.len_ms, + ¤t_timing.in.volume, + ¤t_timing.in.skip_ms)); + current_timing.in.ofs_ms = 0; /* not used */ + } + + if ((str = strstr(comment, "fadeout="))) + { + current_timing.out.enable = + (4 == sscanf(str + 8, "%d,%d,%d,%d", + ¤t_timing.out.len_ms, + ¤t_timing.out.volume, + ¤t_timing.out.skip_ms, + ¤t_timing.out.ofs_ms)); + } + } + +#if 0 + // + // use the fade info only on a regular, non-manual songchange + // + if (fade_config && !((fade_config->config == FADE_CONFIG_XFADE) || + (fade_config->config == FADE_CONFIG_ALBUM))) + last_timing.out.enable = FALSE; +#endif + +#if 1 + if (last_timing.out.enable || current_timing.in.enable) +#else + if ((last_timing.out.enable || current_timing.in.enable) && + (!fade_config || + (fade_config->config == FADE_CONFIG_XFADE) || + (fade_config->config == FADE_CONFIG_ALBUM))) +#endif + { + config->fc[FADE_CONFIG_TIMING].out_len_ms = xfade_cfg_fadeout_len (fade_config); + config->fc[FADE_CONFIG_TIMING].out_volume = xfade_cfg_fadeout_volume(fade_config); + config->fc[FADE_CONFIG_TIMING].out_skip_ms = 0; + config->fc[FADE_CONFIG_TIMING].ofs_custom_ms = xfade_cfg_offset (fade_config); + config->fc[FADE_CONFIG_TIMING].in_skip_ms = 0; + config->fc[FADE_CONFIG_TIMING].in_len_ms = xfade_cfg_fadein_len (fade_config); + config->fc[FADE_CONFIG_TIMING].in_volume = xfade_cfg_fadein_volume (fade_config); + config->fc[FADE_CONFIG_TIMING].flush = fade_config && fade_config->flush; + + if (last_timing.out.enable && current_timing.in.enable) + config->fc[FADE_CONFIG_TIMING].ofs_custom_ms = 0; + + if (last_timing.out.enable) + { + DEBUG(("[crossfade] open_audio: TIMING: out: enable=%d len=%d volume=%d skip=%d ofs=%d\n", + last_timing.out.enable, + last_timing.out.len_ms, + last_timing.out.volume, + last_timing.out.skip_ms, + last_timing.out.ofs_ms)); + + config->fc[FADE_CONFIG_TIMING].out_len_ms = last_timing.out.len_ms; + config->fc[FADE_CONFIG_TIMING].out_volume = last_timing.out.volume; + config->fc[FADE_CONFIG_TIMING].out_skip_ms = last_timing.out.skip_ms; + config->fc[FADE_CONFIG_TIMING].ofs_custom_ms += last_timing.out.ofs_ms; + } + if (current_timing.in.enable) + { + DEBUG(("[crossfade] open_audio: TIMING: in: enable=%d len=%d volume=%d skip=%d\n", + current_timing.in.enable, + current_timing.in.len_ms, + current_timing.in.volume, + current_timing.in.skip_ms)); + + config->fc[FADE_CONFIG_TIMING].in_skip_ms = current_timing.in.skip_ms; + config->fc[FADE_CONFIG_TIMING].in_len_ms = current_timing.in.len_ms; + config->fc[FADE_CONFIG_TIMING].in_volume = current_timing.in.volume; + config->fc[FADE_CONFIG_TIMING].ofs_custom_ms -= current_timing.in.ofs_ms; + /* NOTE: in.ofs_ms is currently not used always 0 */ + } + + fade_config = &config->fc[FADE_CONFIG_TIMING]; + } +#endif + + /* cleanup */ + g_free(file); file = NULL; + g_free(title); title = NULL; + g_free(comment); comment = NULL; + + /* check for HTTP streaming */ + if (config->enable_http_workaround && (0 == strncasecmp(file, "http://", 7))) + { + DEBUG(("[crossfade] open_audio: HTTP underrun workaround enabled.\n")); + is_http = TRUE; + } + else + is_http = FALSE; + + /* lock buffer */ + MUTEX_LOCK(&buffer_mutex); + + /* reset writer timeout */ + gettimeofday(&last_write, NULL); + + /* calculate time since last close() (don't care about overflows at 24h) */ + if (output_opened) + { + gettimeofday(&tv, NULL); + dt = (tv.tv_sec - last_close.tv_sec) * 1000 + (tv.tv_usec - last_close.tv_usec) / 1000; + } + else + dt = 0; + + DEBUG(("[crossfade] open_audio: fmt=%s rate=%d nch=%d dt=%ld ms\n", format_name(fmt), rate, nch, dt)); + + /* check format */ + if (setup_format(fmt, rate, nch, &in_format) < 0) + { + DEBUG(("[crossfade] open_audio: format not supported!\n")); + return 0; + } + + /* (re)open the device if necessary */ + if (!output_opened) + { + if (open_output()) + { + DEBUG(("[crossfade] open_audio: error opening/configuring output!\n")); + MUTEX_UNLOCK(&buffer_mutex); + return 0; + } + fade_config = &config->fc[FADE_CONFIG_START]; + } + + /* reset */ + streampos = 0; + playing = TRUE; + opened = TRUE; + paused = FALSE; + + /* reset mix/fade/gap */ + buffer_mfg_reset(buffer, config); + + /* enable gap killer / zero crossing only for automatic/album songchange */ + switch (fade_config->config) + { + case FADE_CONFIG_XFADE: + case FADE_CONFIG_ALBUM: + break; + + default: + buffer->gap = GAP_SKIPPING_DONE; + } + + /* restart realtime throttling */ + output_written = 0; + + /* start mixing */ + switch (fade_config ? fade_config->type : -1) + { + case FADE_TYPE_FLUSH: + DEBUG(("[crossfade] open_audio: FLUSH:\n")); + + /* flush output plugin */ + the_op->flush(0); + output_streampos = 0; + + /* flush buffer */ + buffer_reset(buffer, config); + + /* apply fade config (pause/fadein after flush) */ + xfade_apply_fade_config(fade_config); + + /* also repopen device (if configured so in the plugin compat. options) */ + if (the_op_config.force_reopen) + { + buffer->reopen = 0; + buffer->reopen_sync = FALSE; + } + break; + + case FADE_TYPE_REOPEN: + DEBUG(("[crossfade] open_audio: REOPEN:\n")); + + /* flush buffer if applicable */ + if (fade_config->flush) + buffer_reset(buffer, config); + + if (buffer->reopen >= 0) + DEBUG(("[crossfade] open_audio: REOPEN: WARNING: reopen in progress (%d ms)\n", + B2MS(buffer->reopen))); + + /* start reopen countdown (will be executed in buffer_thread_f) */ + buffer->reopen = buffer->used; /* may be 0 */ + buffer->reopen_sync = FALSE; + break; + + case FADE_TYPE_NONE: + case FADE_TYPE_PAUSE: + case FADE_TYPE_SIMPLE_XF: + case FADE_TYPE_ADVANCED_XF: + case FADE_TYPE_FADEIN: + case FADE_TYPE_FADEOUT: + DEBUG(("[crossfade] open_audio: XFADE:\n")); + + /* apply fade config (do fadeout, init mix/fade/gap, add silence) */ + xfade_apply_fade_config(fade_config); + + /* set reopen countdown. after buffer_thread_f has written + * buffer->reopen bytes, it will close/reopen the output plugin. */ + if (the_op_config.force_reopen && !(fade_config->config == FADE_CONFIG_START)) + { + if (buffer->reopen >= 0) + DEBUG(("[crossfade] open_audio: XFADE: WARNING: reopen in progress (%d ms)\n", + B2MS(buffer->reopen))); + buffer->reopen = buffer->used; + buffer->reopen_sync = TRUE; + } + break; + } + + /* calculate offset of the output plugin */ + output_offset = the_op->written_time() + B2MS(buffer->used) + B2MS(buffer->silence_len); + + /* unlock buffer */ + MUTEX_UNLOCK(&buffer_mutex); + + /* done */ + return 1; +} + +void +xfade_write_audio(void *ptr, int length) +{ + gint free; + gint ofs = 0; + format_t format; + +#ifdef DEBUG_HARDCORE + DEBUG(("[crossfade] write_audio: ptr=0x%08lx, length=%d\n", (long) ptr, length)); +#endif + + /* sanity */ + if (length <= 0) + return; + + if (length & 3) + { + DEBUG(("[crossfade] write_audio: truncating %d bytes!\n", length & 3)); + length &= -4; + } + + /* update input accumulator (using input format size) */ + streampos += length; + + /* convert sample format (signed-16bit-ne 44100hz stereo) */ + format_copy(&format, &in_format); + length = convert_flow(&convert_context, (gpointer *) &ptr, length, &format); + + /* lock buffer */ + MUTEX_LOCK(&buffer_mutex); + + /* check if device has been closed, reopen if necessary */ + if (!output_opened) + { + if (open_output()) + { + DEBUG(("[crossfade] write_audio: reopening failed!\n")); + MUTEX_UNLOCK(&buffer_mutex); + return; + } + } + + /* reset timeout */ + gettimeofday(&last_write, NULL); + + /* calculate free buffer space, check for overflow (should never happen :) */ + free = buffer->size - buffer->used; + if (length > free) + { + DEBUG(("[crossfade] write_audio: %d bytes truncated!\n", length - free)); + length = free; + } + + /* skip beginning of song */ + if ((length > 0) && (buffer->skip > 0)) + { + gint blen = MIN(length, buffer->skip); + + buffer->skip -= blen; + length -= blen; + ptr += blen; + } + + /* kill leading gap */ + if ((length > 0) && (buffer->gap > 0)) + { + gint blen = MIN(length, buffer->gap); + gint16 *p = ptr; + gint index = 0; + + gint16 left, right; + while (index < blen) + { + left = *p++, right = *p++; + if (ABS(left) >= buffer->gap_level) break; + if (ABS(right) >= buffer->gap_level) break; + index += 4; + } + + buffer->gap -= index; + length -= index; + ptr += index; + + if ((index < blen) || (buffer->gap <= 0)) + { + buffer->gap_killed = buffer->gap_len - buffer->gap; + buffer->gap = 0; + + DEBUG(("[crossfade] write_audio: leading gap size: %d/%d ms\n", + B2MS(buffer->gap_killed), B2MS(buffer->gap_len))); + + /* fix streampos */ + streampos -= (gint64) buffer->gap_killed * in_format.bps / out_format.bps; + } + } + + /* start skipping to next crossing (if enabled) */ + if (buffer->gap == 0) + { + if (config->gap_crossing) + { + buffer->gap = GAP_SKIPPING_POSITIVE; + buffer->gap_skipped = 0; + } + else + buffer->gap = GAP_SKIPPING_DONE; + } + + /* skip until next zero crossing (pos -> neg) */ + if ((length > 0) && (buffer->gap == GAP_SKIPPING_POSITIVE)) + { + gint16 *p = ptr; + gint index = 0; + + gint16 left; + while (index < length) + { + left = *p++; + p++; + if (left < 0) + break; + index += 4; + } + + buffer->gap_skipped += index; + length -= index; + ptr += index; + + if (index < length) + buffer->gap = GAP_SKIPPING_NEGATIVE; + } + + /* skip until next zero crossing (neg -> pos) */ + if ((length > 0) && (buffer->gap == GAP_SKIPPING_NEGATIVE)) + { + gint16 *p = ptr; + gint index = 0; + + gint16 left; + while (index < length) + { + left = *p++; + p++; + if (left >= 0) + break; + index += 4; + } + + buffer->gap_skipped += index; + length -= index; + ptr += index; + + if (index < length) + { + DEBUG(("[crossfade] write_audio: %d samples to next crossing\n", buffer->gap_skipped)); + buffer->gap = GAP_SKIPPING_DONE; + } + } + + /* 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; + + /* fadein -- FIXME: is modifying the input/effect buffer safe? */ + if ((length > 0) && (buffer->fade > 0)) + { + gint16 *p = ptr; + gint blen = MIN(length, buffer->fade); + gint n; + + for (n = blen / 4; n > 0; n--) + { + gfloat factor = 1.0f - (((gfloat) buffer->fade / buffer->fade_len) * buffer->fade_scale); + *p = (gfloat)*p * factor; p++; + *p = (gfloat)*p * factor; p++; + buffer->fade -= 4; + } + } + + /* mix */ + while ((length > 0) && (buffer->mix > 0)) + { + gint wr_index = (buffer->rd_index + buffer->used) % buffer->size; + gint blen = buffer->size - wr_index; + gint16 *p1 = buffer->data + wr_index; + gint16 *p2 = ptr + ofs; + gint n; + + if (blen > length) blen = length; + if (blen > buffer->mix) blen = buffer->mix; + + for (n = blen / 2; n > 0; n--) + { + gint out = (gint)*p1 + *p2++; /* add */ + if (out > 32767) /* clamp */ + *p1++ = 32767; + else if (out < -32768) + *p1++ = -32768; + else + *p1++ = out; + } + + buffer->used += blen; + buffer->mix -= blen; + length -= blen; + ofs += blen; + } + + /* 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); +#ifdef DEBUG_HARDCORE + DEBUG(("[crossfade] write_audio: done.\n")); +#endif +} + +/* sync_output: wait for output plugin to finish playback */ +/* is only called from within buffer_thread_f */ +static void +sync_output() +{ + glong dt, total; + gint opt, opt_last; + struct timeval tv, tv_start, tv_last_change; + gboolean was_closed = !opened; + + if (!the_op->buffer_playing || !the_op->buffer_playing()) + { + DEBUG(("[crossfade] sync_output: nothing to do\n")); + return; + } + + DEBUG(("[crossfade] sync_output: waiting for plugin...\n")); + + dt = 0; + opt_last = 0; + gettimeofday(&tv_start, NULL); + gettimeofday(&tv_last_change, NULL); + + while ((dt < SYNC_OUTPUT_TIMEOUT) + && !stopped && output_opened && !(was_closed && opened) && the_op && the_op->buffer_playing()) + { + + /* use output_time() to check if the output plugin is still active */ + if (the_op->output_time) + { + opt = the_op->output_time(); + if (opt != opt_last) + { + /* output_time has changed */ + opt_last = opt; + gettimeofday(&tv_last_change, NULL); + } + else + { + /* calculate time since last change of the_op->output_time() */ + gettimeofday(&tv, NULL); + dt = (tv.tv_sec - tv_last_change.tv_sec) * 1000 + (tv.tv_usec - tv_last_change.tv_usec) / 1000; + } + } + + /* yield */ + MUTEX_UNLOCK(&buffer_mutex); + xfade_usleep(10000); + MUTEX_LOCK(&buffer_mutex); + } + + /* calculate total time we spent in here */ + gettimeofday(&tv, NULL); + total = (tv.tv_sec - tv_start.tv_sec) * 1000 + (tv.tv_usec - tv_start.tv_usec) / 1000; + + /* print some debug info */ + /* *INDENT-OFF* */ + if (stopped) + DEBUG(("[crossfade] sync_output: ... stopped\n")) + else if (was_closed && opened) + DEBUG(("[crossfade] sync_output: ... reopened\n")) + else if (dt >= SYNC_OUTPUT_TIMEOUT) + DEBUG(("[crossfade] sync_output: ... TIMEOUT! (%ld ms)\n", total)) + else + DEBUG(("[crossfade] sync_output: ... done (%ld ms)\n", total)); + /* *INDENT-ON* */ +} + +void * +buffer_thread_f(void *arg) +{ + gpointer data; + gint sync; + gint op_free; + gint length_bak, length, blen; + glong timeout, dt; + gboolean stopping; + + struct timeval tv; + struct timeval mark; + + DEBUG(("[crossfade] buffer_thread_f: thread started (pid=%d)\n", (int) getpid())); + + /* lock buffer */ + MUTEX_LOCK(&buffer_mutex); + + while (!stopped) + { + /* yield */ +#ifdef DEBUG_HARDCORE + DEBUG(("[crossfade] buffer_thread_f: yielding...\n")); +#endif + MUTEX_UNLOCK(&buffer_mutex); + xfade_usleep(10000); + MUTEX_LOCK(&buffer_mutex); + + /* --------------------------------------------------------------------- */ + + stopping = FALSE; + + /* V0.3.0: New timeout detection */ + if (!opened) + { + gboolean current = xfplayer_input_playing(); + + /* also see fini() */ + if (last_close.tv_sec || last_close.tv_usec) + { + gettimeofday(&tv, NULL); + timeout = (tv.tv_sec - last_close.tv_sec) * 1000 + + (tv.tv_usec - last_close.tv_usec) / 1000; + } + else + timeout = -1; + + if (current != input_playing) + { + input_playing = current; + + if (current) + DEBUG(("[crossfade] buffer_thread_f: input restarted after %ld ms\n", timeout)) + else + DEBUG(("[crossfade] buffer_thread_f: input stopped after + %ld ms\n", timeout)); + } + + /* 0.3.0: HACK: output_keep_opened: play silence during prebuffering */ + if (input_playing && config->output_keep_opened && (buffer->used == 0)) + { + buffer->silence = 0; + buffer->silence_len = MS2B(100); + } + + /* 0.3.9: Check for timeout only if we have not been stopped for restart */ + /* 0.3.11: When using the songchange hack, depend on it's output_restart + * flag. Without the hack, use the configuration's dialog songchange + * timeout setting instead of a fixed timeout value. */ + if (input_stopped_for_restart && !output_restart) + { + if (playing) + DEBUG(("[crossfade] buffer_thread_f: timeout:" + " stopping after %ld ms (songchange patch)\n", timeout)); + stopping = TRUE; + } + else if (((timeout < 0) || (timeout >= config->songchange_timeout && !output_restart)) && !input_playing) + { + if (playing) + DEBUG(("[crossfade] buffer_thread_f: timeout:" + " input did not restart after %ld ms\n", timeout)); + stopping = TRUE; + } + } + + /* V0.2.4: Moved the timeout checks in front of the buffer_free() check + * below. Before, buffer_thread_f could (theoretically) loop + * endlessly if buffer_free() returned 0 all the time. */ + + /* calculate time since last write to the buffer (ignore overflows) */ + gettimeofday(&tv, NULL); + timeout = (tv.tv_sec - last_write.tv_sec) * 1000 + + (tv.tv_usec - last_write.tv_usec) / 1000; + + /* check for timeout/eop (note this is the only way out of this loop) */ + if (stopping) + { + if (playing) + { + DEBUG(("[crossfade] buffer_thread_f: timeout: manual stop\n")); + + /* if CONFIG_STOP is of TYPE_NONE, immediatelly close the device... */ + if ((config->fc[FADE_CONFIG_STOP].type == FADE_TYPE_NONE) && !config->output_keep_opened) + break; + + /* special handling for pause */ + if (paused) + { + DEBUG(("[crossfade] buffer_thread_f: timeout: paused, closing now...\n")); + paused = FALSE; + if (config->output_keep_opened) + the_op->pause(0); + else + break; + } + else if (buffer->pause >= 0) + { + DEBUG(("[crossfade] buffer_thread_f: timeout: cancelling pause countdown\n")); + buffer->pause = -1; + } + + /* ...otherwise, do the fadeout first */ + xfade_apply_fade_config(&config->fc[FADE_CONFIG_STOP]); + + /* force CONFIG_START in case the user restarts playback during fadeout */ + fade_config = &config->fc[FADE_CONFIG_START]; + playing = FALSE; + eop = TRUE; + } + else + { + if (!eop) + { + DEBUG(("[crossfade] buffer_thread_f: timeout: end of playback\n")); + + /* 0.3.3: undo trailing gap killer at end of playlist */ + if (buffer->gap_killed) + { + buffer->used += buffer->gap_killed; + DEBUG(("[crossfade] buffer_thread_f: timeout:" + " undoing trailing gap (%d ms)\n", B2MS(buffer->gap_killed))); + } + + /* do the fadeout if applicable */ + if (config->fc[FADE_CONFIG_EOP].type != FADE_TYPE_NONE) + xfade_apply_fade_config(&config->fc[FADE_CONFIG_EOP]); + + fade_config = &config->fc[FADE_CONFIG_START]; /* see above */ + eop = TRUE; + } + + if (buffer->used == 0) + { + if (config->output_keep_opened) + { + /* 0.3.0: play silence while keeping the output opened */ + buffer->silence = 0; + buffer->silence_len = MS2B(100); + } + else if (buffer->silence_len <= 0) + { + sync_output(); + if (opened) + { + DEBUG(("[crossfade] buffer_thread_f: timeout, eop: device has been reopened\n")); + DEBUG(("[crossfade] buffer_thread_f: timeout, eop: -> continuing playback\n")); + eop = FALSE; + } + else + { + DEBUG(("[crossfade] buffer_thread_f: timeout, eop: closing output...\n")); + break; + } + } + } + } + } + else + eop = FALSE; + + /* --------------------------------------------------------------------- */ + + /* get free space in device output buffer + * NOTE: disk_writer always returns <big int> here */ + op_free = the_op->buffer_free() & -4; + + /* continue waiting if there is no room in the device buffer */ + if (op_free == 0) + continue; + + /* --- Limit OP buffer use (decreases latency) ------------------------- */ + + /* HACK: limit output plugin buffer usage to decrease latency */ + if (config->enable_op_max_used) + { + gint output_time = the_op->output_time(); + gint output_used = the_op->written_time() - output_time; + gint output_limit = MS2B(config->op_max_used_ms - MIN(output_used, config->op_max_used_ms)); + + if (output_flush_time != output_time) + { + /* slow down output, but always write _some_ data */ + if (output_limit < in_format.bps / 100 / 2) + output_limit = in_format.bps / 100 / 2; + + if (op_free > output_limit) + op_free = output_limit; + } + } + + /* --- write silence --------------------------------------------------- */ + + if (!paused && (buffer->silence <= 0) && (buffer->silence_len >= 4)) + { + /* write as much silence as a) there is left and b) the device can take */ + length = buffer->silence_len; + if (length > op_free) + length = op_free; + + /* make sure we always operate on stereo sample boundary */ + length &= -4; + + /* HACK: don't stay in here too long when in realtime mode (see below) */ + if (realtime) + gettimeofday(&mark, NULL); + + /* write length bytes to the device */ + length_bak = length; + while (length > 0) + { + data = zero_4k; + blen = sizeof(zero_4k); + if (blen > length) + blen = length; + + /* make sure zero_4k is cleared. The effect plugin within + * the output plugin may have modified this buffer! (0.2.8) */ + memset(zero_4k, 0, blen); + + /* HACK: the original OSS plugin hangs when writing large + * blocks (greater than device buffer size) in realtime mode */ + if (the_op_config.max_write_enable && (blen > the_op_config.max_write_len)) + blen = the_op_config.max_write_len; + + /* finally, write data */ + the_op->write_audio(data, blen); + length -= blen; + + /* HACK: don't stay in here too long (force yielding every 10 ms) */ + if (realtime) + { + gettimeofday(&tv, NULL); + dt = (tv.tv_sec - mark.tv_sec) * 1000 + + (tv.tv_usec - mark.tv_usec) / 1000; + if (dt >= 10) + break; + } + } + + /* calculate how many bytes actually have been written */ + length = length_bak - length; + } + + /* --- write data ------------------------------------------------- */ + + else if (!paused && (buffer->preload <= 0) && (buffer->used >= 4)) + { + /* write as much data as a) is available and b) the device can take */ + length = buffer->used; + if (length > op_free) + length = op_free; + + /* HACK: throttle output (used with fast output plugins) */ + if (the_op_config.throttle_enable && !realtime && opened) + { + sync = buffer->sync_size - (buffer->size - buffer->used); + if (sync < 0) length = 0; + else if (sync < length) length = sync; + } + + /* clip length to silence countdown (if applicable) */ + if ((buffer->silence >= 4) && (length > buffer->silence)) + length = buffer->silence; + + /* clip length to reopen countdown (if applicable) */ + if ((buffer->reopen >= 0) && (length > buffer->reopen)) + length = buffer->reopen; + + /* clip length to pause countdown (if applicable) */ + if ((buffer->pause >= 0) && (length > buffer->pause)) + length = buffer->pause; + + /* make sure we always operate on stereo sample boundary */ + length &= -4; + + /* HACK: don't stay in here too long when in realtime mode (see below) */ + if (realtime) + gettimeofday(&mark, NULL); + + /* write length bytes to the device */ + length_bak = length; + while (length > 0) + { + data = buffer->data + buffer->rd_index; + blen = buffer->size - buffer->rd_index; + if (blen > length) + blen = length; + + /* HACK: the original OSS plugin hangs when writing large + * blocks (greater than device buffer size) in realtime mode */ + if (the_op_config.max_write_enable && (blen > the_op_config.max_write_len)) + blen = the_op_config.max_write_len; + +#ifdef HAVE_LIBFFTW + /* fft playground */ + fft_flow(&fft_context, (gpointer) data, blen); +#endif + /* finally, write data */ + the_op->write_audio(data, blen); + + buffer->rd_index = (buffer->rd_index + blen) % buffer->size; + buffer->used -= blen; + length -= blen; + + /* HACK: don't stay in here too long (force yielding every 10 ms) */ + if (realtime) + { + gettimeofday(&tv, NULL); + dt = (tv.tv_sec - mark.tv_sec) * 1000 + (tv.tv_usec - mark.tv_usec) / 1000; + if (dt >= 10) + break; + } + } + + /* calculate how many bytes actually have been written */ + length = length_bak - length; + } + else + length = 0; + + /* update realtime throttling */ + output_written += length; + output_streampos += length; + + /* --- check countdowns ------------------------------------------------ */ + + if (buffer->silence > 0) + { + buffer->silence -= length; + if (buffer->silence < 0) + DEBUG(("[crossfade] buffer_thread_f: WARNING: silence overrun: %d\n", buffer->silence)); + } + else if (buffer->silence_len > 0) + { + buffer->silence_len -= length; + if (buffer->silence_len <= 0) + { + if (buffer->silence_len < 0) + DEBUG(("[crossfade] buffer_thread_f: WARNING: silence_len overrun: %d\n", + buffer->silence_len)); + } + } + + if ((buffer->reopen >= 0) && !((buffer->silence <= 0) && (buffer->silence_len > 0))) + { + buffer->reopen -= length; + if (buffer->reopen <= 0) + { + if (buffer->reopen < 0) + DEBUG(("[crossfade] buffer_thread_f: WARNING: reopen overrun: %d\n", buffer->reopen)); + + DEBUG(("[crossfade] buffer_thread_f: closing/reopening device\n")); + if (buffer->reopen_sync) + sync_output(); + + if (the_op->close_audio) + the_op->close_audio(); + + if (!the_op->open_audio(out_format.fmt, out_format.rate, out_format.nch)) + { + DEBUG(("[crossfade] buffer_thread_f: reopening output plugin failed!\n")); + g_free(buffer->data); + output_opened = FALSE; + MUTEX_UNLOCK(&buffer_mutex); + THREAD_EXIT(0); + return NULL; + } + + output_flush_time = 0; + output_written = 0; + output_streampos = 0; + + /* We need to take the leading gap killer into account here: + * It will fix streampos only after gapkilling has finished. + * So, if gapkilling is still in progress at this point, we + * have to fix it ourselves. */ + output_offset = buffer->used; + if ((buffer->gap_len > 0) && (buffer->gap > 0)) + output_offset += buffer->gap_len - buffer->gap; + output_offset = B2MS(output_offset) - xfade_written_time(); + + /* make sure reopen is not 0 */ + buffer->reopen = -1; + } + } + + if (buffer->pause >= 0) + { + buffer->pause -= length; + if (buffer->pause <= 0) + { + if (buffer->pause < 0) + DEBUG(("[crossfade] buffer_thread_f: WARNING: pause overrun: %d\n", buffer->pause)); + + DEBUG(("[crossfade] buffer_thread_f: pausing output\n")); + + paused = TRUE; + sync_output(); + + if (paused) + the_op->pause(1); + else + DEBUG(("[crossfade] buffer_thread_f: unpause during sync\n")) buffer->pause = -1; + } + } + } + + /* ----------------------------------------------------------------------- */ + + /* cleanup: close output */ + if (output_opened) + { + xfade_stop_monitor(); + + DEBUG(("[crossfade] buffer_thread_f: closing output...\n")); + + if (the_op->close_audio) + the_op->close_audio(); + + DEBUG(("[crossfade] buffer_thread_f: closing output... done\n")); + + g_free(buffer->data); + output_opened = FALSE; + } + else + DEBUG(("[crossfade] buffer_thread_f: output already closed!\n")); + + /* ----------------------------------------------------------------------- */ + + /* unlock buffer */ + MUTEX_UNLOCK(&buffer_mutex); + + /* done */ + DEBUG(("[crossfade] buffer_thread_f: thread finished\n")); + THREAD_EXIT(0); + return NULL; +} + +void +xfade_close_audio() +{ + DEBUG(("[crossfade] close:\n")); + DEBUG(("[crossfade] close: playing=%d filename=%s\n", + xfplayer_input_playing(), xfplaylist_get_filename(xfplaylist_get_position()))); + + /* lock buffer */ + MUTEX_LOCK(&buffer_mutex); + + /* sanity... the vorbis plugin likes to call close_audio() twice */ + if (!opened) + { + DEBUG(("[crossfade] close: WARNING: not opened!\n")); + MUTEX_UNLOCK(&buffer_mutex); + return; + } + +#if defined(COMPILE_FOR_AUDACIOUS) && AUDACIOUS_ABI_VERSION >= 2 + /* HACK: to distinguish between STOP and EOP, check Audacious' + input_playing() variable. It seems to be TRUE at this point + only when the end of the playlist is reached. + + Normally, 'playing' is constantly being updated in the + xfade_buffer_playing() callback, but Audacious does not seem + to use it. Therefore, we can set 'playing' to FALSE here, + which later 'buffer_thread' will interpret as EOP (see above). + */ + if (xfplayer_input_playing()) + playing = FALSE; +#else + /* HACK: use patched XMMS 'input_stopped_for_restart' */ + if (input_stopped_for_restart && *input_stopped_for_restart) + { + DEBUG(("[crossfade] close: playback will restart soon\n")); + output_restart = TRUE; + } + else + output_restart = FALSE; +#endif + + if (playing) + { + /* immediatelly close output when paused */ + if (paused) + { + buffer->pause = -1; + paused = FALSE; + if (config->output_keep_opened) + { + buffer->used = 0; + the_op->flush(0); + the_op->pause(0); + } + else + stopped = TRUE; + } + + /* HACK: If playlist_get_info_going is not true here, + * XMMS is about to exit. In this case, we stop + * the buffer thread before returning from this + * function. Otherwise, SEGFAULT may occur when + * XMMS tries to cleanup an output plugin which + * we are still using. + * + * NOTE: This hack has become obsolete as of 0.3.5. + * See output_list_hack(). + * + * NOTE: Not quite. There still are some problems when + * XMMS is exitting while a song is playing. So + * this HACK has been enabled again. + * + * NOTE: Another thing: If output_keep_opened is enabled, + * close_audio() is never called, so that the patch + * can not work. + */ +#if 1 + if ((xmms_is_quitting && *xmms_is_quitting) + || (xmms_playlist_get_info_going && !*xmms_playlist_get_info_going)) + { + DEBUG(("[crossfade] close: stop (about to quit)\n")) + + /* wait for buffer thread to clean up and terminate */ + stopped = TRUE; +#if 1 + MUTEX_UNLOCK(&buffer_mutex); + if (THREAD_JOIN(buffer_thread)) + PERROR("[crossfade] close: phtread_join()"); + MUTEX_LOCK(&buffer_mutex); +#else + while (output_opened) + { + MUTEX_UNLOCK(&buffer_mutex); + xfade_usleep(10000); + MUTEX_LOCK(&buffer_mutex); + } +#endif + } + else +#endif + DEBUG(("[crossfade] close: stop\n")); + + fade_config = &config->fc[FADE_CONFIG_MANUAL]; + } + else + { + /* gint x = *((gint *)0); */ /* force SEGFAULT for debugging */ + DEBUG(("[crossfade] close: songchange/eop\n")); + + /* kill trailing gap (does not use buffer->gap_*) */ + if (output_opened && xfade_cfg_gap_trail_enable(config)) + { + gint gap_len = MS2B(xfade_cfg_gap_trail_len(config)) & -4; + gint gap_level = xfade_cfg_gap_trail_level(config); + gint length = MIN(gap_len, buffer->used); + + /* DEBUG(("[crossfade] close: len=%d level=%d length=%d\n", gap_len, gap_level, length)); */ + + buffer->gap_killed = 0; + while (length > 0) + { + gint wr_xedni = (buffer->rd_index + buffer->used - 1) % buffer->size + 1; + gint blen = MIN(length, wr_xedni); + gint16 *p = buffer->data + wr_xedni, left, right; + gint index = 0; + + while (index < blen) + { + right = *--p; + left = *--p; + if (ABS(left) >= gap_level) break; + if (ABS(right) >= gap_level) break; + index += 4; + } + + buffer->used -= index; + buffer->gap_killed += index; + + if (index < blen) + break; + length -= blen; + } + + DEBUG(("[crossfade] close: trailing gap size: %d/%d ms\n", B2MS(buffer->gap_killed), B2MS(gap_len))); + } + + /* skip to previous zero crossing */ + if (output_opened && config->gap_crossing) + { + int crossing; + + buffer->gap_skipped = 0; + for (crossing = 0; crossing < 4; crossing++) + { + while (buffer->used > 0) + { + gint wr_xedni = (buffer->rd_index + buffer->used - 1) % buffer->size + 1; + gint blen = MIN(buffer->used, wr_xedni); + gint16 *p = buffer->data + wr_xedni, left; + gint index = 0; + + while (index < blen) + { + left = (--p, *--p); + if ((crossing & 1) ^ (left > 0)) + break; + index += 4; + } + + buffer->used -= index; + buffer->gap_skipped += index; + + if (index < blen) + break; + } + } + DEBUG(("[crossfade] close: skipped %d bytes to previous zero crossing\n", buffer->gap_skipped)); + + /* update gap_killed (for undoing gap_killer in case of EOP) */ + buffer->gap_killed += buffer->gap_skipped; + } + + fade_config = &config->fc[FADE_CONFIG_XFADE]; + } + + /* XMMS has left the building */ + opened = FALSE; + + /* update last_close */ + gettimeofday(&last_close, NULL); + input_playing = FALSE; + + /* unlock buffer */ + MUTEX_UNLOCK(&buffer_mutex); +} + +void +xfade_flush(gint time) +{ + gint pos; + gchar *file; + + DEBUG(("[crossfade] flush: time=%d\n", time)); + + /* get filename */ + pos = xfplaylist_get_position(); + file = xfplaylist_get_filename(pos); + + if (!file) + file = g_strdup(xfplaylist_get_songtitle(pos)); + +#if defined(COMPILE_FOR_AUDACIOUS) + /* HACK: special handling for audacious, which just calls flush(0) on a songchange */ + if (file && last_filename && strcmp(file, last_filename) != 0) + { + DEBUG(("[crossfade] flush: filename changed, forcing close/reopen...\n")); + xfade_close_audio(); + /* 0.3.14: xfade_close_audio sets fade_config to FADE_CONFIG_MANUAL, + * but this is an automatic songchange */ + fade_config = &config->fc[FADE_CONFIG_XFADE]; + xfade_open_audio(in_format.fmt, in_format.rate, in_format.nch); + DEBUG(("[crossfade] flush: filename changed, forcing close/reopen... done\n")); + return; + } +#endif + + /* lock buffer */ + MUTEX_LOCK(&buffer_mutex); + + /* update streampos with new stream position (input format size) */ + streampos = ((gint64) time * in_format.bps / 1000) & -4; + + /* flush output device / apply seek crossfade */ + if (config->fc[FADE_CONFIG_SEEK].type == FADE_TYPE_FLUSH) + { + /* flush output plugin */ + the_op->flush(time); + output_flush_time = time; + output_streampos = MS2B(time); + + /* flush buffer, disable leading gap killing */ + buffer_reset(buffer, config); + } + else if (paused) + { + fade_config_t fc; + + /* clear buffer */ + buffer->used = 0; + + /* apply only the fade_in part of FADE_CONFIG_PAUSE */ + memcpy(&fc, &config->fc[FADE_CONFIG_PAUSE], sizeof(fc)); + fc.out_len_ms = 0; + fc.ofs_custom_ms = 0; + xfade_apply_fade_config(&fc); + } + else + xfade_apply_fade_config(&config->fc[FADE_CONFIG_SEEK]); + + /* restart realtime throttling (should find another name for that var) */ + output_written = 0; + + /* make sure that the gapkiller is disabled */ + buffer->gap = 0; + + /* update output offset */ + output_offset = the_op->written_time() - time + B2MS(buffer->used) + B2MS(buffer->silence_len); + + /* unlock buffer */ + MUTEX_UNLOCK(&buffer_mutex); + +#ifdef DEBUG_HARDCORE + DEBUG(("[crossfade] flush: time=%d: done.\n", time)); +#endif +} + +void +xfade_pause(short p) +{ + /* lock buffer */ + MUTEX_LOCK(&buffer_mutex); + + if (p) + { + fade_config_t *fc = &config->fc[FADE_CONFIG_PAUSE]; + if (fc->type == FADE_TYPE_PAUSE_ADV) + { + int fade, length, n; + int index = buffer->rd_index; + int out_len = MS2B(xfade_cfg_fadeout_len(fc)) & -4; + int in_len = MS2B(xfade_cfg_fadein_len(fc)) & -4; + int silence_len = MS2B(xfade_cfg_offset(fc)) & -4; + + /* limit fadeout/fadein len to available data in buffer */ + if ((out_len + in_len) > buffer->used) + { + out_len = (buffer->used / 2) & -4; + in_len = out_len; + } + + DEBUG(("[crossfade] pause: paused=1 out=%d in=%d silence=%d\n", + B2MS(out_len), B2MS(in_len), B2MS(silence_len))); + + /* fade out (modifies buffer directly) */ + fade = 0; + length = out_len; + while (length > 0) + { + gint16 *p = buffer->data + index; + gint blen = buffer->size - index; + if (blen > length) + blen = length; + + for (n = blen / 4; n > 0; n--) + { + gfloat factor = 1.0f - ((gfloat) fade / out_len); + *p = (gfloat)*p * factor; p++; + *p = (gfloat)*p * factor; p++; + fade += 4; + } + + index = (index + blen) % buffer->size; + length -= blen; + } + + /* fade in (modifies buffer directly) */ + fade = 0; + length = in_len; + while (length > 0) + { + gint16 *p = buffer->data + index; + gint blen = buffer->size - index; + if (blen > length) + blen = length; + + for (n = blen / 4; n > 0; n--) + { + gfloat factor = (gfloat) fade / in_len; + *p = (gfloat)*p * factor; p++; + *p = (gfloat)*p * factor; p++; + fade += 4; + } + + index = (index + blen) % buffer->size; + length -= blen; + } + + /* start silence and pause countdowns */ + buffer->silence = out_len; + buffer->silence_len = silence_len; + buffer->pause = out_len + silence_len; + paused = FALSE; /* (!) will be set to TRUE in buffer_thread_f */ + } + else + { + the_op->pause(1); + paused = TRUE; + DEBUG(("[crossfade] pause: paused=1\n")); + } + } + else + { + the_op->pause(0); + buffer->pause = -1; + paused = FALSE; + DEBUG(("[crossfade] pause: paused=0\n")); + } + + /* unlock buffer */ + MUTEX_UNLOCK(&buffer_mutex); +} + +gint +xfade_buffer_free() +{ + gint size, free; + +#ifdef DEBUG_HARDCORE + DEBUG(("[crossfade] buffer_free:\n")); +#endif + + /* sanity check */ + if (!output_opened) + { + DEBUG(("[crossfade] buffer_free: WARNING: output closed!\n")); + return buffer->sync_size; + } + + /* lock buffer */ + MUTEX_LOCK(&buffer_mutex); + + size = buffer->size; + + /* When running in realtime mode, we need to take special care here: + * While XMMS is writing data to the output plugin, it will not yield + * ANY processing time to the buffer thread. It will only stop when + * xfade_buffer_free() no longer reports free buffer space. */ + if (realtime) + { + gint64 wanted = output_written + buffer->preload_size; + + /* Fix for XMMS misbehaviour (possibly a bug): If the free space as + * returned by xfade_buffer_free() is below a certain minimum block size + * (tests showed 2304 bytes), XMMS will not send more data until there + * is enough room for one of those blocks. + * + * This breaks preloading in realtime mode. To make sure that the pre- + * load buffer gets filled we request additional sync_size bytes. */ + wanted += buffer->sync_size; + + if (wanted <= size) + size = wanted; + } + + free = size - buffer->used; + if (free < 0) + free = 0; + + /* unlock buffer */ + MUTEX_UNLOCK(&buffer_mutex); + + /* Convert to input format size. For input rates > output rate this will + * return less free space than actually is available, but we don't care. */ + free /= (out_format.rate / (in_format.rate + 1)) + 1; + if (in_format.is_8bit) + free /= 2; + if (in_format.nch == 1) + free /= 2; + +#ifdef DEBUG_HARDCORE + DEBUG(("[crossfade] buffer_free: %d\n", free)); +#endif + return free; +} + +gint +xfade_buffer_playing() +{ + /* always return FALSE here (if not in pause) so XMMS immediatelly + * starts playback of the next song */ + + /* + * NOTE: this causes trouble when playing HTTP audio streams. + * + * mpg123.lib will start prebuffering (and thus stalling output) when both + * 1) it's internal buffer is emptied (does happen all the time) + * 2) the output plugin's buffer_playing() (this function) returns FALSE + */ + + if (paused) + playing = TRUE; + else + playing = + (is_http && (buffer->used > 0) && the_op->buffer_playing()) + || (buffer->reopen >= 0) + || (buffer->silence > 0) + || (buffer->silence_len > 0); + +#ifdef DEBUG_HARDCORE + DEBUG(("[crossfade] buffer_playing: %d\n", playing)); +#endif + return playing; +} + +gint +xfade_written_time() +{ + if (!output_opened) + return 0; + return (gint) (streampos * 1000 / in_format.bps); +} + +gint +xfade_output_time() +{ + gint time; + +#ifdef DEBUG_HARDCORE + DEBUG(("[crossfade] output_time:\n")); +#endif + + /* sanity check (note: this one _does_ happen all the time) */ + if (!output_opened) + return 0; + + /* lock buffer */ + MUTEX_LOCK(&buffer_mutex); + + time = the_op->output_time() - output_offset; + if (time < 0) + time = 0; + + /* unlock buffer */ + MUTEX_UNLOCK(&buffer_mutex); + +#ifdef DEBUG_HARDCORE + DEBUG(("[crossfade] output_time: time=%d\n", time)); +#endif + return time; +} + +void +xfade_cleanup() +{ + DEBUG(("[crossfade] cleanup:\n")); + + /* lock buffer */ + MUTEX_LOCK(&buffer_mutex); + + /* check if buffer thread is still running */ + if (output_opened) + { + DEBUG(("[crossfade] cleanup: closing output\n")); + + stopped = TRUE; + + /* wait for buffer thread to clean up and terminate */ + MUTEX_UNLOCK(&buffer_mutex); + if (THREAD_JOIN(buffer_thread)) + PERROR("[crossfade] close: thread_join()"); + MUTEX_LOCK(&buffer_mutex); + } + + /* unlock buffer */ + MUTEX_UNLOCK(&buffer_mutex); + + DEBUG(("[crossfade] cleanup: done\n")); +}