/* DeaDBeeF - ultimate music player for GNU/Linux systems with X11 Copyright (C) 2009 Alexey Yakovenko 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, see . */ #include #include #include #include #include #ifdef HAVE_CONFIG_H # include #endif #include "../../deadbeef.h" #define min(x,y) ((x)<(y)?(x):(y)) #define max(x,y) ((x)>(y)?(x):(y)) //#define trace(...) { fprintf (stderr, __VA_ARGS__); } #define trace(fmt,...) static DB_decoder_t plugin; static DB_functions_t *deadbeef; static DB_FILE *file; static OggVorbis_File vorbis_file; static vorbis_info *vi; static int cur_bit_stream; static int startsample; static int endsample; static int currentsample; static int last_comment_update; static DB_playItem_t *ptrack; static void cvorbis_free (void); static size_t cvorbis_fread (void *ptr, size_t size, size_t nmemb, void *datasource) { size_t ret = deadbeef->fread (ptr, size, nmemb, datasource); trace ("cvorbis_fread %d %d %d\n", size, nmemb, ret); return ret; } static int cvorbis_fseek (void *datasource, ogg_int64_t offset, int whence) { DB_FILE *f = (DB_FILE *)datasource; return deadbeef->fseek (datasource, offset, whence); } static int cvorbis_fclose (void *datasource) { deadbeef->fclose (datasource); return 0; } static long cvorbis_ftell (void *datasource) { return deadbeef->ftell (datasource); } static void update_vorbis_comments (DB_playItem_t *it, vorbis_comment *vc) { if (vc) { deadbeef->pl_delete_all_meta (it); deadbeef->pl_add_meta (it, "vendor", vc->vendor); for (int i = 0; i < vc->comments; i++) { if (!strncasecmp (vc->user_comments[i], "artist=", 7)) { deadbeef->pl_add_meta (it, "artist", vc->user_comments[i] + 7); } else if (!strncasecmp (vc->user_comments[i], "album=", 6)) { deadbeef->pl_add_meta (it, "album", vc->user_comments[i] + 6); } else if (!strncasecmp (vc->user_comments[i], "title=", 6)) { deadbeef->pl_add_meta (it, "title", vc->user_comments[i] + 6); } else if (!strncasecmp (vc->user_comments[i], "tracknumber=", 12)) { deadbeef->pl_add_meta (it, "track", vc->user_comments[i] + 12); } else if (!strncasecmp (vc->user_comments[i], "date=", 5)) { deadbeef->pl_add_meta (it, "year", vc->user_comments[i] + 5); } else if (!strncasecmp (vc->user_comments[i], "comment=", 8)) { deadbeef->pl_add_meta (it, "comment", vc->user_comments[i] + 8); } else if (!strncasecmp (vc->user_comments[i], "genre=", 6)) { deadbeef->pl_add_meta (it, "genre", vc->user_comments[i] + 6); } else if (!strncasecmp (vc->user_comments[i], "copyright=", 10)) { deadbeef->pl_add_meta (it, "copyright", vc->user_comments[i] + 10); } else if (!strncasecmp (vc->user_comments[i], "cuesheet=", 9)) { deadbeef->pl_add_meta (it, "cuesheet", vc->user_comments[i] + 9); } else if (!strncasecmp (vc->user_comments[i], "replaygain_album_gain=", 22)) { it->replaygain_album_gain = atof (vc->user_comments[i] + 22); } else if (!strncasecmp (vc->user_comments[i], "replaygain_album_peak=", 22)) { it->replaygain_album_peak = atof (vc->user_comments[i] + 22); } else if (!strncasecmp (vc->user_comments[i], "replaygain_track_gain=", 22)) { it->replaygain_track_gain = atof (vc->user_comments[i] + 22); } else if (!strncasecmp (vc->user_comments[i], "replaygain_track_peak=", 22)) { it->replaygain_track_peak = atof (vc->user_comments[i] + 22); } } } deadbeef->pl_add_meta (it, "title", NULL); } static int cvorbis_init (DB_playItem_t *it) { file = NULL; vi = NULL; cur_bit_stream = -1; ptrack = it; file = plugin.info.file = deadbeef->fopen (it->fname); if (!file) { return -1; } memset (&plugin.info, 0, sizeof (plugin.info)); int ln = deadbeef->fgetlength (file); if (file->vfs->streaming && ln == -1) { ov_callbacks ovcb = { .read_func = cvorbis_fread, .seek_func = NULL, .close_func = cvorbis_fclose, .tell_func = NULL }; trace ("calling ov_open_callbacks\n"); int err = ov_open_callbacks (file, &vorbis_file, NULL, 0, ovcb); if (err != 0) { trace ("ov_open_callbacks returned %d\n", err); plugin.free (); return -1; } deadbeef->pl_set_item_duration (it, -1); } else { ov_callbacks ovcb = { .read_func = cvorbis_fread, .seek_func = cvorbis_fseek, .close_func = cvorbis_fclose, .tell_func = cvorbis_ftell }; trace ("calling ov_open_callbacks\n"); int err = ov_open_callbacks (file, &vorbis_file, NULL, 0, ovcb); if (err != 0) { trace ("ov_open_callbacks returned %d\n", err); plugin.free (); return -1; } // deadbeef->pl_set_item_duration (it, ov_time_total (&vorbis_file, -1)); } vi = ov_info (&vorbis_file, -1); if (!vi) { // not a vorbis stream cvorbis_free (); trace ("not a vorbis stream\n"); return -1; } plugin.info.bps = 16; //plugin.info.dataSize = ov_pcm_total (&vorbis_file, -1) * vi->channels * 2; plugin.info.channels = vi->channels; plugin.info.samplerate = vi->rate; plugin.info.readpos = 0; currentsample = 0; if (!file->vfs->streaming) { if (it->endsample > 0) { startsample = it->startsample; endsample = it->endsample; plugin.seek_sample (0); } else { startsample = 0; endsample = ov_pcm_total (&vorbis_file, -1)-1; } } else { startsample = 0; if (deadbeef->pl_get_item_duration (it) < 0) { endsample = -1; } else { endsample = ov_pcm_total (&vorbis_file, -1)-1; } vorbis_comment *vc = ov_comment (&vorbis_file, -1); update_vorbis_comments (it, vc); } return 0; } static void cvorbis_free (void) { plugin.info.file = NULL; if (file) { ptrack = NULL; ov_clear (&vorbis_file); //fclose (file); //-- ov_clear closes it file = NULL; vi = NULL; } } static int cvorbis_read (char *bytes, int size) { // trace ("cvorbis_read %d bytes\n", size); if (!file->vfs->streaming) { if (currentsample + size / (2 * plugin.info.channels) > endsample) { size = (endsample - currentsample + 1) * 2 * plugin.info.channels; trace ("size truncated to %d bytes, cursample=%d, endsample=%d, totalsamples=%d\n", size, currentsample, endsample, ov_pcm_total (&vorbis_file, -1)); if (size <= 0) { return 0; } } } else { if (ptrack && currentsample - last_comment_update > 5 * plugin.info.samplerate) { int idx = deadbeef->pl_get_idx_of (ptrack); if (idx >= 0) { last_comment_update = currentsample; vorbis_comment *vc = ov_comment (&vorbis_file, -1); update_vorbis_comments (ptrack, vc); deadbeef->sendmessage (M_TRACKCHANGED, 0, idx, 0); } else { ptrack = NULL; } } } // trace ("cvorbis_read %d bytes[2]\n", size); int initsize = size; long ret; for (;;) { // read ogg int endianess = 0; #if WORDS_BIGENDIAN endianess = 1; #endif ret=ov_read (&vorbis_file, bytes, size, endianess, 2, 1, &cur_bit_stream); if (ret <= 0) { if (ret < 0) { trace ("ov_read returned %d\n", ret); switch (ret) { case OV_HOLE: trace ("OV_HOLE\n"); break; case OV_EBADLINK: trace ("OV_EBADLINK\n"); break; case OV_EINVAL: trace ("OV_EINVAL\n"); break; } } if (ret == OV_HOLE) { continue; } // error or eof break; } else if (ret < size) { currentsample += ret / (vi->channels * 2); size -= ret; bytes += ret; } else { currentsample += ret / (vi->channels * 2); size = 0; break; } } plugin.info.readpos = (float)(ov_pcm_tell(&vorbis_file)-startsample)/vi->rate; trace ("cvorbis_read got %d bytes, readpos %f, currentsample %d, ret %d\n", initsize-size, plugin.info.readpos, currentsample, ret); deadbeef->streamer_set_bitrate (ov_bitrate_instant (&vorbis_file)/1000); return initsize - size; } static int cvorbis_seek_sample (int sample) { if (sample < 0) { trace ("vorbis: negative seek sample - ignored, but it is a bug!\n"); return -1; } if (!file) { trace ("vorbis: file is NULL on seek\n"); return -1; } trace ("vorbis: seek to sample %d\n"); sample += startsample; int res = ov_pcm_seek (&vorbis_file, sample); if (res != 0 && res != OV_ENOSEEK) { trace ("vorbis: error %x seeking to sample %d\n", sample); return -1; } int tell = ov_pcm_tell (&vorbis_file); if (tell != sample) { trace ("oggvorbis: failed to do sample-accurate seek (%d->%d)\n", sample, tell); } trace ("vorbis: seek successful\n") currentsample = sample; plugin.info.readpos = (float)(ov_pcm_tell(&vorbis_file) - startsample)/vi->rate; return 0; } static int cvorbis_seek (float time) { return cvorbis_seek_sample (time * vi->rate); } static DB_playItem_t * cvorbis_insert (DB_playItem_t *after, const char *fname) { // check for validity DB_FILE *fp = deadbeef->fopen (fname); if (!fp) { trace ("vorbis: failed to fopen %s\n", fname); return NULL; } if (fp->vfs->streaming) { DB_playItem_t *it = deadbeef->pl_item_alloc (); it->decoder = &plugin; it->fname = strdup (fname); it->filetype = "OggVorbis"; deadbeef->pl_set_item_duration (it, -1); deadbeef->pl_add_meta (it, "title", NULL); after = deadbeef->pl_insert_item (after, it); return after; } ov_callbacks ovcb = { .read_func = cvorbis_fread, .seek_func = cvorbis_fseek, .close_func = cvorbis_fclose, .tell_func = cvorbis_ftell }; OggVorbis_File vorbis_file; vorbis_info *vi; int err = ov_open_callbacks (fp, &vorbis_file, NULL, 0, ovcb); if (err != 0) { trace ("ov_open_callbacks returned %d\n", err); return NULL; } vi = ov_info (&vorbis_file, -1); if (!vi) { // not a vorbis stream trace ("vorbis: failed to ov_open %s\n", fname); return NULL; } float duration = ov_time_total (&vorbis_file, -1); int totalsamples = ov_pcm_total (&vorbis_file, -1); DB_playItem_t *it = deadbeef->pl_item_alloc (); it->decoder = &plugin; it->fname = strdup (fname); it->filetype = "OggVorbis"; deadbeef->pl_set_item_duration (it, duration); // metainfo vorbis_comment *vc = ov_comment (&vorbis_file, -1); update_vorbis_comments (it, vc); int samplerate = vi->rate; ov_clear (&vorbis_file); DB_playItem_t *cue = deadbeef->pl_insert_cue (after, it, totalsamples, samplerate); if (cue) { deadbeef->pl_item_free (it); return cue; } // embedded cue const char *cuesheet = deadbeef->pl_find_meta (it, "cuesheet"); if (cuesheet) { cue = deadbeef->pl_insert_cue_from_buffer (after, it, cuesheet, strlen (cuesheet), totalsamples, samplerate); if (cue) { deadbeef->pl_item_free (it); return cue; } } after = deadbeef->pl_insert_item (after, it); return after; } static int vorbis_trackdeleted (DB_event_track_t *ev, uintptr_t data) { if (ev->track == ptrack) { ptrack = NULL; } return 0; } static int vorbis_start (void) { deadbeef->ev_subscribe (DB_PLUGIN (&plugin), DB_EV_TRACKDELETED, DB_CALLBACK (vorbis_trackdeleted), 0); return 0; } static int vorbis_stop (void) { deadbeef->ev_unsubscribe (DB_PLUGIN (&plugin), DB_EV_TRACKDELETED, DB_CALLBACK (vorbis_trackdeleted), 0); return 0; } static const char * exts[] = { "ogg", NULL }; static const char *filetypes[] = { "OggVorbis", NULL }; // define plugin interface static DB_decoder_t plugin = { DB_PLUGIN_SET_API_VERSION .plugin.version_major = 0, .plugin.version_minor = 1, .plugin.type = DB_PLUGIN_DECODER, .plugin.name = "OggVorbis decoder", .plugin.descr = "OggVorbis decoder using standard xiph.org libraries", .plugin.author = "Alexey Yakovenko", .plugin.email = "waker@users.sourceforge.net", .plugin.website = "http://deadbeef.sf.net", .plugin.start = vorbis_start, .plugin.stop = vorbis_stop, .init = cvorbis_init, .free = cvorbis_free, .read_int16 = cvorbis_read, // vorbisfile can't output float32 // .read_float32 = cvorbis_read_float32, .seek = cvorbis_seek, .seek_sample = cvorbis_seek_sample, .insert = cvorbis_insert, .exts = exts, .id = "stdogg", .filetypes = filetypes }; DB_plugin_t * vorbis_load (DB_functions_t *api) { deadbeef = api; return DB_PLUGIN (&plugin); }