Mercurial > audlegacy-plugins
view src/crossfade/crossfade.c @ 3077:d58963762734
crossfade-ng: probe_priority = 0
author | William Pitcock <nenolod@atheme.org> |
---|---|
date | Mon, 27 Apr 2009 04:47:07 -0500 |
parents | fbe76a1d61e2 |
children | 354e90c81253 |
line wrap: on
line source
/* * 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 "configure.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 OutputPluginInitStatus 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 = 0, }, 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 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 */ /* 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]; /* * 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; /* done */ output_opened = TRUE; return 0; } static OutputPluginInitStatus 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(); /* 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(); /* realize config -- will also setup the pre-mixing effect plugin */ xfade_realize_config(); return OUTPUT_PLUGIN_INIT_NO_DEVICES; } 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 } 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; 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); 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 /* 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); /* check for streaming */ if (aud_vfs_is_remote(file)) { DEBUG(("[crossfade] open_audio: HTTP underrun workaround enabled.\n")); is_http = TRUE; } else is_http = FALSE; g_free(file); file = NULL; g_free(title); title = NULL; /* 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) { 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: 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")); }