/* CD audio plugin for DeaDBeeF Copyright (C) 2009 Viktor Semykin 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 . */ /* screwed/maintained by Alexey Yakovenko */ #ifdef HAVE_CONFIG_H # include "../../config.h" #endif #include #include #include #include #include #if HAVE_SYS_CDEFS_H #include #endif #if HAVE_SYS_SYSLIMITS_H #include #endif #include #include #include #include "../../deadbeef.h" //#define trace(...) { fprintf (stderr, __VA_ARGS__); } #define trace(fmt,...) #define DEFAULT_SERVER "freedb.org" #define DEFAULT_PORT 888 #define DEFAULT_USE_CDDB 1 #define DEFAULT_PROTOCOL 1 #define DEFAULT_PREFER_CDTEXT 1 #define SECTORSIZE CDIO_CD_FRAMESIZE_RAW //2352 #define SAMPLESIZE 4 //bytes #define BUFSIZE (CDIO_CD_FRAMESIZE_RAW * 2) static DB_decoder_t plugin; static DB_functions_t *deadbeef; typedef struct { DB_fileinfo_t info; CdIo_t* cdio; lsn_t first_sector; unsigned int sector_count; uint8_t tail [SECTORSIZE]; unsigned int tail_len; int current_sector; unsigned int current_sample; } cdda_info_t; struct cddb_thread_params { DB_playItem_t *items[100]; CdIo_t *cdio; }; static inline int min (int a, int b) { return a < b ? a : b; } static DB_fileinfo_t * cda_open (uint32_t hints) { DB_fileinfo_t *_info = malloc (sizeof (cdda_info_t)); memset (_info, 0, sizeof (cdda_info_t)); return _info; } static int cda_init (DB_fileinfo_t *_info, DB_playItem_t *it) { cdda_info_t *info = (cdda_info_t *)_info; trace ("cdda: init %s\n", deadbeef->pl_find_meta (it, ":URI")); char location[PATH_MAX]; deadbeef->pl_get_meta (it, ":URI", location, sizeof (location)); char *nr = strchr (location, '#'); if (nr) { *nr = 0; nr++; } else { trace ("cdda: bad name: %s\n", deadbeef->pl_find_meta (it, ":URI")); return -1; } int track_nr = atoi (nr); char *fname = (*location) ? location : NULL; //NULL if empty string; means pysical CD drive info->cdio = cdio_open (fname, DRIVER_UNKNOWN); if (!info->cdio) { trace ("cdda: Could not open CD\n"); return -1; } track_t first_track = cdio_get_first_track_num (info->cdio); if (first_track == 0xff) { trace ("cdda: no medium found\n"); return -1; } track_t tracks = cdio_get_num_tracks (info->cdio); track_t i; cddb_track_t *track; cddb_disc_t *disc = cddb_disc_new(); cddb_disc_set_length (disc, cdio_get_track_lba (info->cdio, CDIO_CDROM_LEADOUT_TRACK) / CDIO_CD_FRAMES_PER_SEC); for (i = 0; i < tracks; i++) { lsn_t offset = cdio_get_track_lba (info->cdio, i+first_track); track = cddb_track_new(); cddb_track_set_frame_offset (track, offset); cddb_disc_add_track (disc, track); } cddb_disc_calc_discid (disc); int discid = cddb_disc_get_discid (disc); int trk_discid = deadbeef->pl_find_meta_int (it, ":CDIO_DISCID", 0); if (trk_discid != discid) { cddb_disc_destroy (disc); trace ("cdda: the track belongs to another disc, skipped\n"); return -1; } cddb_disc_destroy (disc); if (TRACK_FORMAT_AUDIO != cdio_get_track_format (info->cdio, track_nr)) { trace ("cdda: Not an audio track (%d)\n", track_nr); return -1; } int channels = cdio_get_track_channels (info->cdio, track_nr); trace ("cdio nchannels: %d\n", channels); _info->plugin = &plugin; _info->fmt.bps = 16; _info->fmt.channels = 2; _info->fmt.samplerate = 44100; _info->fmt.channelmask = DDB_SPEAKER_FRONT_LEFT | DDB_SPEAKER_FRONT_RIGHT; _info->readpos = 0; info->first_sector = cdio_get_track_lsn (info->cdio, track_nr); info->sector_count = cdio_get_track_sec_count (info->cdio, track_nr); info->current_sector = info->first_sector; info->tail_len = 0; info->current_sample = 0; return 0; } int cda_read (DB_fileinfo_t *_info, char *bytes, int size) { cdda_info_t *info = (cdda_info_t *)_info; int extrasize = 0; if (info->tail_len > 0) { if (info->tail_len >= size) { // trace ("Easy case\n"); memcpy (bytes, info->tail, size); info->tail_len -= size; memmove (info->tail, info->tail+size, info->tail_len); return size; } // trace ("Prepending with tail of %d bytes\n", tail_len); extrasize = info->tail_len; memcpy (bytes, info->tail, info->tail_len); bytes += info->tail_len; size -= info->tail_len; info->tail_len = 0; } int sectors_to_read = size / SECTORSIZE + 1; int end = 0; if (info->current_sector + sectors_to_read > info->first_sector + info->sector_count) // reached end of track { end = 1; sectors_to_read = info->first_sector + info->sector_count - info->current_sector; // trace ("cdda: reached end of track\n"); } int bufsize = sectors_to_read * SECTORSIZE; info->tail_len = end ? 0 : bufsize - size; char *buf = alloca (bufsize); driver_return_code_t ret = cdio_read_audio_sectors (info->cdio, buf, info->current_sector, sectors_to_read); if (ret != DRIVER_OP_SUCCESS) return 0; info->current_sector += sectors_to_read; int retsize = end ? bufsize : size; memcpy (bytes, buf, retsize); if (!end) memcpy (info->tail, buf+retsize, info->tail_len); retsize += extrasize; // trace ("requested: %d; tail_len: %d; size: %d; sectors_to_read: %d; return: %d\n", initsize, tail_len, size, sectors_to_read, retsize); info->current_sample += retsize / SAMPLESIZE; _info->readpos = (float)info->current_sample / _info->fmt.samplerate; return retsize; } static void cda_free (DB_fileinfo_t *_info) { if (_info) { cdda_info_t *info = (cdda_info_t *)_info; if (info->cdio) { cdio_destroy (info->cdio); } free (_info); } } static int cda_seek_sample (DB_fileinfo_t *_info, int sample) { cdda_info_t *info = (cdda_info_t *)_info; int sector = sample / (SECTORSIZE / SAMPLESIZE) + info->first_sector; int offset = (sample % (SECTORSIZE / SAMPLESIZE)) * SAMPLESIZE; //in bytes char buf [SECTORSIZE]; driver_return_code_t ret = cdio_read_audio_sector (info->cdio, buf, sector); if (ret != DRIVER_OP_SUCCESS) return -1; memcpy (info->tail, buf + offset, SECTORSIZE - offset); info->current_sector = sector; info->current_sample = sample; _info->readpos = (float)info->current_sample / _info->fmt.samplerate; return 0; } static int cda_seek (DB_fileinfo_t *_info, float sec) { return cda_seek_sample (_info, sec * _info->fmt.samplerate); } cddb_disc_t* resolve_disc (CdIo_t *cdio) { track_t first_track = cdio_get_first_track_num (cdio); track_t tracks = cdio_get_num_tracks (cdio); track_t i; cddb_track_t *track; cddb_disc_t *disc = cddb_disc_new(); cddb_disc_set_length (disc, cdio_get_track_lba (cdio, CDIO_CDROM_LEADOUT_TRACK) / CDIO_CD_FRAMES_PER_SEC); for (i = 0; i < tracks; i++) { lsn_t offset = cdio_get_track_lba (cdio, i+first_track); track = cddb_track_new(); cddb_track_set_frame_offset (track, offset); cddb_disc_add_track (disc, track); } cddb_conn_t *conn = NULL; conn = cddb_new(); deadbeef->conf_lock (); cddb_set_server_name (conn, deadbeef->conf_get_str_fast ("cdda.freedb.host", DEFAULT_SERVER)); cddb_set_server_port (conn, deadbeef->conf_get_int ("cdda.freedb.port", DEFAULT_PORT)); if (!deadbeef->conf_get_int ("cdda.protocol", DEFAULT_PROTOCOL)) { cddb_http_enable (conn); if (deadbeef->conf_get_int ("network.proxy", 0)) { cddb_set_server_port(conn, deadbeef->conf_get_int ("network.proxy.port", 8080)); cddb_set_server_name(conn, deadbeef->conf_get_str_fast ("network.proxy.address", "")); } } deadbeef->conf_unlock (); int matches = cddb_query (conn, disc); if (matches == -1) { cddb_disc_destroy (disc); cddb_destroy (conn); return NULL; } cddb_read (conn, disc); cddb_destroy (conn); return disc; } static DB_playItem_t * insert_single_track (CdIo_t* cdio, ddb_playlist_t *plt, DB_playItem_t *after, const char* file, int track_nr, int discid) { char tmp[file ? strlen (file) + 20 : 20]; if (file) snprintf (tmp, sizeof (tmp), "%s#%d.cda", file, track_nr); else snprintf (tmp, sizeof (tmp), "#%d.cda", track_nr); if (TRACK_FORMAT_AUDIO != cdio_get_track_format (cdio, track_nr)) { trace ("Not an audio track (%d)\n", track_nr); return NULL; } int sector_count = cdio_get_track_sec_count (cdio, track_nr); DB_playItem_t *it = deadbeef->pl_item_alloc_init (tmp, plugin.plugin.id); deadbeef->pl_add_meta (it, ":FILETYPE", "cdda"); deadbeef->plt_set_item_duration (plt, it, (float)sector_count / 75.0); snprintf (tmp, sizeof (tmp), "CD Track %02d", track_nr); deadbeef->pl_add_meta (it, "title", tmp); snprintf (tmp, sizeof (tmp), "%02d", track_nr); deadbeef->pl_add_meta (it, "track", tmp); deadbeef->pl_set_meta_int (it, ":CDIO_DISCID", discid); after = deadbeef->plt_insert_item (plt, after, it); return after; } static void cleanup_thread_params (struct cddb_thread_params *params) { int i; for (i = 0; params->items[i]; i++) deadbeef->pl_item_unref (params->items[i]); cdio_destroy (params->cdio); free (params); } static void cddb_thread (void *items_i) { struct cddb_thread_params *params = (struct cddb_thread_params*)items_i; DB_playItem_t **items = params->items; trace ("calling resolve_disc\n"); cddb_disc_t* disc = resolve_disc (params->cdio); if (!disc) { trace ("disc not resolved\n"); if (params->cdio) { cdio_destroy (params->cdio); } free (params); return; } trace ("disc resolved\n"); const char *disc_title = cddb_disc_get_title (disc); const char *artist = cddb_disc_get_artist (disc); trace ("disc_title=%s, disk_artist=%s\n", disc_title, artist); cddb_track_t *track; int i; // FIXME: playlist must be locked before doing that int trk = 1; for (i = 0, track = cddb_disc_get_track_first (disc); items[i]; trk++, ++i, track = cddb_disc_get_track_next (disc)) { // FIXME: problem will happen here if item(s) were deleted from playlist, and new items were added in their places // possible solutions: catch EV_TRACKDELETED and mark item(s) in every thread as NULL trace ("track %d, artist=%s, album=%s, title=%s\n", i, artist, disc_title, cddb_track_get_title (track)); deadbeef->pl_delete_all_meta (items[i]); deadbeef->pl_add_meta (items[i], "artist", artist); deadbeef->pl_add_meta (items[i], "album", disc_title); deadbeef->pl_add_meta (items[i], "title", cddb_track_get_title (track)); char tmp[5]; snprintf (tmp, sizeof (tmp), "%02d", trk); deadbeef->pl_add_meta (items[i], "track", tmp); ddb_event_track_t *ev = (ddb_event_track_t *)deadbeef->event_alloc (DB_EV_TRACKINFOCHANGED); ev->track = items[i]; if (ev->track) { deadbeef->pl_item_ref (ev->track); } deadbeef->event_send ((ddb_event_t *)ev, 0, 0); } cddb_disc_destroy (disc); cleanup_thread_params (params); ddb_playlist_t *plt = deadbeef->plt_get_curr (); if (plt) { deadbeef->plt_modified (plt); deadbeef->plt_unref (plt); } deadbeef->sendmessage (DB_EV_PLAYLISTCHANGED, 0, 0, 0); } static void read_track_cdtext (CdIo_t *cdio, int track_nr, DB_playItem_t *item) { #if CDIO_API_VERSION >= 6 cdtext_t *cdtext = cdio_get_cdtext (cdio); #else cdtext_t *cdtext = cdio_get_cdtext (cdio, 0); #endif if (!cdtext) { trace ("No cdtext\n"); return; } const char *artist = NULL; const char *album = NULL; int field_type; for (field_type = 0; field_type < MAX_CDTEXT_FIELDS; field_type++) { #if CDIO_API_VERSION >= 6 const char *text = cdtext_get_const (cdtext, field_type, track_nr); #else const char *text = cdtext_get_const (field_type, cdtext); #endif const char *field = NULL; if (text) { switch (field_type) { #if CDIO_API_VERSION >= 6 case CDTEXT_FIELD_TITLE: album = text; break; case CDTEXT_FIELD_PERFORMER: artist = text; break; #else case CDTEXT_TITLE: album = text; break; case CDTEXT_PERFORMER: artist = text; break; #endif } } } trace ("artist: %s; album: %s\n", artist, album); if (artist) { deadbeef->pl_replace_meta (item, "artist", artist); } if (album) { deadbeef->pl_replace_meta (item, "album", album); } #if CDIO_API_VERSION >= 6 cdtext = cdio_get_cdtext (cdio); #else cdtext = cdio_get_cdtext (cdio, track_nr); #endif if (!cdtext) return; for (field_type = 0; field_type < MAX_CDTEXT_FIELDS; field_type++) { #if CDIO_API_VERSION >= 6 const char *text = cdtext_get_const (cdtext, field_type, track_nr); #else const char *text = cdtext_get_const (field_type, cdtext); #endif const char *field = NULL; if (!text) continue; switch (field_type) { #if CDIO_API_VERSION >= 6 case CDTEXT_FIELD_TITLE: field = "title"; break; case CDTEXT_FIELD_PERFORMER: field = "artist"; break; case CDTEXT_FIELD_COMPOSER: field = "composer"; break; case CDTEXT_FIELD_GENRE: field = "genre"; break; case CDTEXT_FIELD_SONGWRITER: field = "songwriter"; break; case CDTEXT_FIELD_MESSAGE: field = "comment"; break; #else case CDTEXT_TITLE: field = "title"; break; case CDTEXT_PERFORMER: field = "artist"; break; case CDTEXT_COMPOSER: field = "composer"; break; case CDTEXT_GENRE: field = "genre"; break; case CDTEXT_SONGWRITER: field = "songwriter"; break; case CDTEXT_MESSAGE: field = "comment"; break; #endif default: field = NULL; } if (field && text) { trace ("%s: %s\n", field, text); deadbeef->pl_replace_meta (item, field, text); } } } static int read_disc_cdtext (struct cddb_thread_params *params) { DB_playItem_t **items = params->items; #if CDIO_API_VERSION >= 6 cdtext_t *cdtext = cdio_get_cdtext (params->cdio); #else cdtext_t *cdtext = cdio_get_cdtext (params->cdio, 0); #endif if (!cdtext) return 0; track_t first_track = cdio_get_first_track_num (params->cdio); track_t tracks = cdio_get_num_tracks (params->cdio); track_t i; for (i = 0; i < tracks; i++) read_track_cdtext (params->cdio, i + first_track, params->items[i]); return 1; } static DB_playItem_t * cda_insert (ddb_playlist_t *plt, DB_playItem_t *after, const char *fname) { trace ("CDA insert: %s\n", fname); CdIo_t* cdio = NULL; int track_nr; DB_playItem_t *res; const char* shortname = strrchr (fname, '/'); if (shortname) { shortname++; } else { shortname = fname; } const char *ext = strrchr (shortname, '.') + 1; int is_image = ext && (0 == strcmp (ext, "nrg")); if (is_image && !deadbeef->conf_get_int ("cdda.enable_nrg", 0)) { return NULL; } if (0 == strcmp (ext, "cda")) { cdio = cdio_open (NULL, DRIVER_UNKNOWN); } else if (is_image) { cdio = cdio_open (fname, DRIVER_NRG); } if (!cdio) { trace ("not an audio disc/image, or file not found (%s)\n", fname); return NULL; } // calculate discid track_t first_track = cdio_get_first_track_num (cdio); if (first_track == 0xff) { trace ("cdda: no medium found\n"); cdio_destroy (cdio); return NULL; } track_t tracks = cdio_get_num_tracks (cdio); track_t i; cddb_track_t *track; cddb_disc_t *disc = cddb_disc_new(); cddb_disc_set_length (disc, cdio_get_track_lba (cdio, CDIO_CDROM_LEADOUT_TRACK) / CDIO_CD_FRAMES_PER_SEC); for (i = 0; i < tracks; i++) { lsn_t offset = cdio_get_track_lba (cdio, i+first_track); track = cddb_track_new(); cddb_track_set_frame_offset (track, offset); cddb_disc_add_track (disc, track); } cddb_disc_calc_discid (disc); int discid = cddb_disc_get_discid (disc); if (0 == strcasecmp (shortname, "all.cda") || is_image) { track_t i; res = after; struct cddb_thread_params *p = malloc (sizeof (struct cddb_thread_params)); memset (p, 0, sizeof (struct cddb_thread_params)); p->cdio = cdio; int enable_cddb = deadbeef->conf_get_int ("cdda.freedb.enable", DEFAULT_USE_CDDB); for (i = 0; i < tracks; i++) { trace ("inserting track %d\n", i); res = insert_single_track (cdio, plt, res, is_image ? fname : NULL, i+first_track, discid); if (res) { p->items[i] = res; } } int got_cdtext = read_disc_cdtext (p); int prefer_cdtext = deadbeef->conf_get_int ("cdda.prefer_cdtext", DEFAULT_PREFER_CDTEXT); if ((!got_cdtext || !prefer_cdtext) && enable_cddb) { trace ("cdda: querying freedb...\n"); intptr_t tid = deadbeef->thread_start (cddb_thread, p); //will destroy cdio deadbeef->thread_detach (tid); } else cleanup_thread_params (p); } else { track_nr = atoi (shortname); res = insert_single_track (cdio, plt, after, NULL, track_nr, discid); if (res) { read_track_cdtext (cdio, track_nr, res); deadbeef->pl_item_unref (res); } cdio_destroy (cdio); } cddb_disc_destroy (disc); return res; } static int cda_action_add_cd (DB_plugin_action_t *act, int ctx) { ddb_playlist_t *plt = deadbeef->plt_get_curr (); if (plt) { deadbeef->plt_add_files_begin (plt, 0); deadbeef->plt_add_file2 (0, plt, "all.cda", NULL, NULL); deadbeef->plt_add_files_end (plt, 0); deadbeef->plt_modified (plt); deadbeef->plt_unref (plt); } deadbeef->sendmessage (DB_EV_PLAYLISTCHANGED, 0, 0, 0); return 0; } static DB_plugin_action_t add_cd_action = { .name = "cd_add", .title = "File/Add Audio CD", .flags = DB_ACTION_COMMON | DB_ACTION_ADD_MENU, .callback2 = cda_action_add_cd, .next = NULL }; static DB_plugin_action_t * cda_get_actions (DB_playItem_t *unused) { return &add_cd_action; } static const char *exts[] = { "cda", "nrg", NULL }; static const char settings_dlg[] = "property \"Use CDDB/FreeDB\" checkbox cdda.freedb.enable 1;\n" "property \"Prefer CD-Text over CDDB\" checkbox cdda.prefer_cdtext 1;\n" "property \"CDDB url (e.g. 'freedb.org')\" entry cdda.freedb.host freedb.org;\n" "property \"CDDB port number (e.g. '888')\" entry cdda.freedb.port 888;\n" "property \"Prefer CDDB protocol over HTTP\" checkbox cdda.protocol 1;\n" "property \"Enable NRG image support\" checkbox cdda.enable_nrg 0;" ; // define plugin interface static DB_decoder_t plugin = { .plugin.api_vmajor = 1, .plugin.api_vminor = 0, .plugin.version_major = 1, .plugin.version_minor = 0, .plugin.type = DB_PLUGIN_DECODER, .plugin.id = "cda", .plugin.name = "Audio CD player", .plugin.descr = "Audio CD plugin using libcdio and libcddb", .plugin.copyright = "Copyright (C) 2009-2013 Alexey Yakovenko \n" "Copyright (C) 2009-2011 Viktor Semykin \n" "\n" "This program is free software; you can redistribute it and/or\n" "modify it under the terms of the GNU General Public License\n" "as published by the Free Software Foundation; either version 2\n" "of the License, or (at your option) any later version.\n" "\n" "This program is distributed in the hope that it will be useful,\n" "but WITHOUT ANY WARRANTY; without even the implied warranty of\n" "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" "GNU General Public License for more details.\n" "\n" "You should have received a copy of the GNU General Public License\n" "along with this program; if not, write to the Free Software\n" "Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n" , .plugin.website = "http://deadbeef.sf.net", .plugin.configdialog = settings_dlg, .plugin.get_actions = cda_get_actions, .open = cda_open, .init = cda_init, .free = cda_free, .read = cda_read, .seek = cda_seek, .seek_sample = cda_seek_sample, .insert = cda_insert, .exts = exts, }; DB_plugin_t * cdda_load (DB_functions_t *api) { deadbeef = api; return DB_PLUGIN (&plugin); }