/************************************************************************************ xmms-wavpack - input plugin for WavPack audio files in xmms Copyright (C) 2009 by Lefungus, Miles Egan, and David Bryant 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA *************************************************************************************/ #include #include #include #include #include #include #include extern "C" { #include #include #include #include } #include #include #include #include #include "equalizer.h" #include "tags.h" #ifndef M_LN10 #define M_LN10 2.3025850929940456840179914546843642 #endif #define DBG(format, args...) fprintf(stderr, format, ## args) #define BUFFER_SIZE 256 // read buffer size, in samples extern "C" InputPlugin * get_iplugin_info(void); static float calculate_gain (WavpackContext *wpc); static void wv_load_config(); static int wv_is_our_file(char *); static void wv_play(char *); static void wv_stop(void); static void wv_pause(short); static void wv_seek(int); static void wv_set_eq(int, float, float *); static int wv_get_time(void); static void wv_get_song_info(char *, char **, int *); static char *generate_title(const char *, WavpackContext *ctx); static int isSeek, paused; static bool killDecodeThread; static bool AudioError; static pthread_t thread_handle; static gboolean EQ_on; // in ui.cpp void wv_configure(); void wv_about_box(void); void wv_file_info_box(char *); extern gboolean clipPreventionEnabled; extern gboolean dynBitrateEnabled; extern gboolean replaygainEnabled; extern gboolean albumReplaygainEnabled; extern gboolean openedAudio; InputPlugin mod = { NULL, //handle NULL, //filename NULL, wv_load_config, wv_about_box, wv_configure, wv_is_our_file, NULL, //no use wv_play, wv_stop, wv_pause, wv_seek, wv_set_eq, //set eq wv_get_time, NULL, //get volume NULL, //set volume NULL, //cleanup NULL, //obsolete NULL, //add_vis NULL, NULL, wv_get_song_info, wv_file_info_box, //info box NULL, //output }; class WavpackDecoder { public: InputPlugin *mod; int32_t *input; int16_t *output; int sample_rate; int num_channels; int bytes_per_sample; WavpackContext *ctx; char error_buff[80]; float play_gain, shaping_error [8]; WavpackDecoder(InputPlugin *mod) : mod(mod) { ctx = NULL; input = NULL; output = NULL; } ~WavpackDecoder() { if (input != NULL) { free(input); input = NULL; } if (output != NULL) { free(output); output = NULL; } if (ctx != NULL) { WavpackCloseFile(ctx); ctx = NULL; } } bool attach(const char *filename) { ctx = WavpackOpenFileInput(filename, error_buff, OPEN_TAGS | OPEN_WVC | OPEN_NORMALIZE, 0); if (ctx == NULL) { return false; } sample_rate = WavpackGetSampleRate(ctx); num_channels = WavpackGetNumChannels(ctx); bytes_per_sample = WavpackGetBytesPerSample(ctx); input = (int32_t *)calloc(BUFFER_SIZE, num_channels * sizeof(int32_t)); output = (int16_t *)calloc(BUFFER_SIZE, num_channels * sizeof(int16_t)); memset (shaping_error, 0, sizeof (shaping_error)); mod->set_info(generate_title(filename, ctx), (int) (WavpackGetNumSamples(ctx) / sample_rate) * 1000, (int) WavpackGetAverageBitrate(ctx, true), (int) sample_rate, num_channels); play_gain = calculate_gain (ctx); DBG("gain value = %g\n", play_gain); return true; } bool open_audio() { return mod->output->open_audio(FMT_S16_NE, sample_rate, num_channels); } void process_buffer(size_t num_samples) { int tsamples = num_samples * num_channels; if (!(WavpackGetMode (ctx) & MODE_FLOAT)) { float scaler = (float) (1.0 / ((unsigned int32_t) 1 << (bytes_per_sample * 8 - 1))); float *fptr = (float *) input; int32_t *lptr = input; int cnt = tsamples; while (cnt--) *fptr++ = *lptr++ * scaler; } if (play_gain != 1.0) { float *fptr = (float *) input; int cnt = tsamples; while (cnt--) *fptr++ *= play_gain; } if (tsamples) { float *fptr = (float *) input; short *sptr = (short *) output; int cnt = num_samples, ch; while (cnt--) for (ch = 0; ch < num_channels; ++ch) { int dst; *fptr -= shaping_error [ch]; if (*fptr >= 1.0) dst = 32767; else if (*fptr <= -1.0) dst = -32768; else dst = (int) floor (*fptr * 32768.0); shaping_error [ch] = (float)(dst / 32768.0 - *fptr++); *sptr++ = dst; } } if (EQ_on) iir ((char *) output, tsamples * sizeof(int16_t)); mod->add_vis_pcm(mod->output->written_time(), FMT_S16_NE, num_channels, tsamples * sizeof(int16_t), output); mod->output->write_audio(output, tsamples * sizeof(int16_t)); } }; extern "C" InputPlugin * get_iplugin_info(void) { mod.description = g_strdup_printf(("Wavpack Decoder Plugin %s"), VERSION); return &mod; } static int wv_is_our_file(char *filename) { char *ext; ext = strrchr(filename, '.'); if (ext) { if (!strcasecmp(ext, ".wv")) { return TRUE; } } return FALSE; } void load_tag(ape_tag *tag, WavpackContext *ctx) { memset(tag, 0, sizeof(ape_tag)); WavpackGetTagItem(ctx, "Album", tag->album, sizeof(tag->album)); WavpackGetTagItem(ctx, "Artist", tag->artist, sizeof(tag->artist)); WavpackGetTagItem(ctx, "Comment", tag->comment, sizeof(tag->comment)); WavpackGetTagItem(ctx, "Genre", tag->genre, sizeof(tag->genre)); WavpackGetTagItem(ctx, "Title", tag->title, sizeof(tag->title)); WavpackGetTagItem(ctx, "Track", tag->track, sizeof(tag->track)); WavpackGetTagItem(ctx, "Year", tag->year, sizeof(tag->year)); } void update_tag(ape_tag *tag, char *filename) { WavpackContext *ctx; char error_buff [80]; ctx = WavpackOpenFileInput (filename, error_buff, OPEN_TAGS | OPEN_EDIT_TAGS, 0); if (!ctx) { char text[256]; sprintf(text, "File \"%s\" not found or is read protected!\n", filename); xmms_show_message("File-Error", (gchar *) text, "Ok", FALSE, NULL, NULL); return; } if (strlen (tag->album)) WavpackAppendTagItem (ctx, "Album", tag->album, strlen (tag->album)); else WavpackDeleteTagItem (ctx, "Album"); if (strlen (tag->artist)) WavpackAppendTagItem (ctx, "Artist", tag->artist, strlen (tag->artist)); else WavpackDeleteTagItem (ctx, "Artist"); if (strlen (tag->comment)) WavpackAppendTagItem (ctx, "Comment", tag->comment, strlen (tag->comment)); else WavpackDeleteTagItem (ctx, "Comment"); if (strlen (tag->genre)) WavpackAppendTagItem (ctx, "Genre", tag->genre, strlen (tag->genre)); else WavpackDeleteTagItem (ctx, "Genre"); if (strlen (tag->title)) WavpackAppendTagItem (ctx, "Title", tag->title, strlen (tag->title)); else WavpackDeleteTagItem (ctx, "Title"); if (strlen (tag->track)) WavpackAppendTagItem (ctx, "Track", tag->track, strlen (tag->track)); else WavpackDeleteTagItem (ctx, "Track"); if (strlen (tag->year)) WavpackAppendTagItem (ctx, "Year", tag->year, strlen (tag->year)); else WavpackDeleteTagItem (ctx, "Year"); if (!WavpackWriteTag (ctx)) { char text[256]; sprintf(text, "Couldn't write tag to \"%s\"!\n", filename); xmms_show_message("File-Error", (gchar *) text, "Ok", FALSE, NULL, NULL); } WavpackCloseFile (ctx); } void delete_tag(char *filename) { WavpackContext *ctx; char error_buff [80]; char text [256]; ctx = WavpackOpenFileInput (filename, error_buff, OPEN_TAGS | OPEN_EDIT_TAGS, 0); if (!ctx) { sprintf(text, "File \"%s\" not found or is read protected!\n", filename); xmms_show_message("File-Error", (gchar *) text, "Ok", FALSE, NULL, NULL); return; } while (WavpackGetTagItemIndexed (ctx, 0, text, sizeof (text))) WavpackDeleteTagItem (ctx, text); if (!WavpackWriteTag (ctx)) { char text[256]; sprintf(text, "Couldn't write tag to \"%s\"!\n", filename); xmms_show_message("File-Error", (gchar *) text, "Ok", FALSE, NULL, NULL); } WavpackCloseFile (ctx); } static char * convertUTF8toLocale(char *utf8) { // note - opens a new iconv descriptor for each call // will have to find a way to reuse the descriptor if this turns // out to be too slow iconv_t idesc = iconv_open("", "UTF-8"); if (idesc == (iconv_t) -1) { perror("iconv_open failed"); return g_strdup(utf8); } size_t in_left = strlen(utf8); size_t out_left = 2 * in_left + 1; char *buf = (char *)g_malloc(out_left); #if 1 char *in = utf8; #else const char *in = (const char *) utf8; // some systems (freeBSD?) require const here #endif char *out = buf; memset(buf, 0, out_left); size_t err = iconv(idesc, &in, &in_left, &out, &out_left); iconv_close(idesc); return buf; } static void * end_thread() { pthread_exit(NULL); return 0; } static void wv_set_eq(int on, float preamp_ctrl, float *eq_ctrl) { EQ_on = on; init_iir(EQ_on, preamp_ctrl, eq_ctrl); } static void * DecodeThread(void *a) { ape_tag tag; char *filename = (char *) a; int bps_updateCounter = 0; int bps; int i; WavpackDecoder d(&mod); if (!d.attach(filename)) { printf("wavpack: Error opening file: \"%s\"\n", filename); killDecodeThread = true; return end_thread(); } bps = WavpackGetBytesPerSample(d.ctx) * d.num_channels; DBG("reading %s at %d rate with %d channels\n", filename, d.sample_rate, d.num_channels); if (!d.open_audio()) { DBG("error opening xmms audio channel\n"); killDecodeThread = true; AudioError = true; openedAudio = false; } else { DBG("opened xmms audio channel\n"); openedAudio = true; } unsigned status; char *display = generate_title(filename, d.ctx); int length = (int) (1000 * WavpackGetNumSamples(d.ctx)); while (!killDecodeThread) { if (isSeek != -1) { DBG("seeking to position %d\n", isSeek); WavpackSeekSample(d.ctx, isSeek * d.sample_rate); isSeek = -1; } if (paused == 0 && (mod.output->buffer_free() >= (1152 * 2 * (16 / 8)) << (mod.output->buffer_playing()? 1 : 0))) { status = WavpackUnpackSamples(d.ctx, d.input, BUFFER_SIZE); if (status == (unsigned) (-1)) { printf("wavpack: Error decoding file.\n"); break; } else if (status == 0) { killDecodeThread = true; break; } else { d.process_buffer(status); } } else { xmms_usleep(10000); } } return end_thread(); } static void wv_play(char *filename) { paused = 0; isSeek = -1; killDecodeThread = false; AudioError = false; pthread_create(&thread_handle, NULL, DecodeThread, (void *) filename); return; } static char * generate_title(const char *fn, WavpackContext *ctx) { static char *displaytitle = NULL; ape_tag tag; TitleInput *ti; ti = (TitleInput *) g_malloc0(sizeof(TitleInput)); ti->__size = XMMS_TITLEINPUT_SIZE; ti->__version = XMMS_TITLEINPUT_VERSION; ti->file_name = g_strdup(g_basename(fn)); ti->file_ext = "wv"; load_tag(&tag, ctx); // xmms doesn't support unicode... ti->track_name = convertUTF8toLocale(tag.title); ti->performer = convertUTF8toLocale(tag.artist); ti->album_name = convertUTF8toLocale(tag.album); ti->date = convertUTF8toLocale(tag.year); ti->track_number = atoi(tag.track); if (ti->track_number < 0) ti->track_number = 0; ti->year = atoi(tag.year); if (ti->year < 0) ti->year = 0; ti->genre = convertUTF8toLocale(tag.genre); ti->comment = convertUTF8toLocale(tag.comment); displaytitle = xmms_get_titlestring(xmms_get_gentitle_format(), ti); if (!displaytitle || *displaytitle == '\0' || (strlen(tag.title) == 0 && strlen(tag.artist) == 0)) displaytitle = ti->file_name; g_free(ti->track_name); g_free(ti->performer); g_free(ti->album_name); g_free(ti->genre); g_free(ti->comment); g_free(ti); return displaytitle; } static void wv_get_song_info(char *filename, char **title, int *length) { assert(filename != NULL); char error_buff[80]; WavpackContext *ctx = WavpackOpenFileInput(filename, error_buff, OPEN_TAGS | OPEN_WVC, 0); if (ctx == NULL) { printf("wavpack: Error opening file: \"%s: %s\"\n", filename, error_buff); return; } int sample_rate = WavpackGetSampleRate(ctx); int num_channels = WavpackGetNumChannels(ctx); DBG("reading %s at %d rate with %d channels\n", filename, sample_rate, num_channels); *length = (int)(WavpackGetNumSamples(ctx) / sample_rate) * 1000, *title = generate_title(filename, ctx); DBG("title for %s = %s\n", filename, *title); WavpackCloseFile(ctx); } static int wv_get_time(void) { if (!mod.output) return -1; if (AudioError) return -2; if (killDecodeThread && !mod.output->buffer_playing()) return -1; return mod.output->output_time(); } static void wv_seek(int sec) { isSeek = sec; mod.output->flush((int) (1000 * isSeek)); } static void wv_pause(short pause) { mod.output->pause(paused = pause); } static void wv_stop(void) { killDecodeThread = true; if (thread_handle != 0) { pthread_join(thread_handle, NULL); if (openedAudio) { mod.output->buffer_free(); mod.output->close_audio(); } openedAudio = false; if (AudioError) printf("Could not open Audio\n"); } } static void wv_load_config() { ConfigFile *cfg; cfg = xmms_cfg_open_default_file(); xmms_cfg_read_boolean(cfg, "wavpack", "clip_prevention", &clipPreventionEnabled); xmms_cfg_read_boolean(cfg, "wavpack", "album_replaygain", &albumReplaygainEnabled); xmms_cfg_read_boolean(cfg, "wavpack", "dyn_bitrate", &dynBitrateEnabled); xmms_cfg_read_boolean(cfg, "wavpack", "replaygain", &replaygainEnabled); xmms_cfg_free(cfg); openedAudio = false; } ////////////////////////////////////////////////////////////////////////////// // This function uses the ReplayGain mode selected by the user and the info // // stored in the specified tag to determine the gain value used to play the // // file. Note that the gain is in voltage scaling (not dB), so a value of // // 1.0 (not 0.0) is unity gain. // ////////////////////////////////////////////////////////////////////////////// static float calculate_gain (WavpackContext *wpc) { if (replaygainEnabled) { float gain_value = 0.0, peak_value = 1.0; char value [32]; if (albumReplaygainEnabled && WavpackGetTagItem (wpc, "replaygain_album_gain", value, sizeof (value))) { gain_value = (float) atof (value); if (WavpackGetTagItem (wpc, "replaygain_album_peak", value, sizeof (value))) peak_value = (float) atof (value); } else if (WavpackGetTagItem (wpc, "replaygain_track_gain", value, sizeof (value))) { gain_value = (float) atof (value); if (WavpackGetTagItem (wpc, "replaygain_track_peak", value, sizeof (value))) peak_value = (float) atof (value); } else return 1.0; // convert gain from dB to voltage (with +/- 20 dB limit) if (gain_value > 20.0) gain_value = 10.0; else if (gain_value < -20.0) gain_value = (float) 0.1; else gain_value = (float) pow (10.0, gain_value / 20.0); if (peak_value * gain_value > 1.0 && clipPreventionEnabled) gain_value = (float)(1.0 / peak_value); return gain_value; } else return 1.0; }