/* ALAC plugin for deadbeef Copyright (C) 2012-2013 Alexey Yakovenko Uses the reverse engineered ALAC decoder (C) 2005 David Hammerton All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "../../deadbeef.h" #ifdef HAVE_CONFIG_H #include "../../config.h" #endif #include #include #include #include "mp4ff.h" #include "demux.h" #include "decomp.h" #include "stream.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; DB_functions_t *deadbeef; #ifdef WORDS_BIGENDIAN int host_bigendian = 1; #else int host_bigendian = 0; #endif #define BUFFER_SIZE (1024*24) #define IN_BUFFER_SIZE (1024*80) typedef struct { DB_fileinfo_t info; DB_FILE *file; demux_res_t demux_res; stream_t *stream; alac_file *alac; int junk; char out_buffer[BUFFER_SIZE]; int out_remaining; int skipsamples; int currentsample; int startsample; int endsample; int current_frame; int64_t dataoffs; } alacplug_info_t; // allocate codec control structure static DB_fileinfo_t * alacplug_open (uint32_t hints) { DB_fileinfo_t *_info = malloc (sizeof (alacplug_info_t)); alacplug_info_t *info = (alacplug_info_t *)_info; memset (info, 0, sizeof (alacplug_info_t)); return _info; } static uint32_t alacplug_fs_read (void *user_data, void *buffer, uint32_t length) { alacplug_info_t *info = user_data; return deadbeef->fread (buffer, 1, length, info->file); } static uint32_t alacplug_fs_seek (void *user_data, uint64_t position) { alacplug_info_t *info = user_data; return deadbeef->fseek (info->file, position+info->junk, SEEK_SET); } static int get_sample_info(demux_res_t *demux_res, uint32_t samplenum, uint32_t *sample_duration, uint32_t *sample_byte_size) { unsigned int duration_index_accum = 0; unsigned int duration_cur_index = 0; if (samplenum >= demux_res->num_sample_byte_sizes) { fprintf(stderr, "sample %i does not exist\n", samplenum); return 0; } if (!demux_res->num_time_to_samples) { fprintf(stderr, "no time to samples\n"); return 0; } while ((demux_res->time_to_sample[duration_cur_index].sample_count + duration_index_accum) <= samplenum) { duration_index_accum += demux_res->time_to_sample[duration_cur_index].sample_count; duration_cur_index++; if (duration_cur_index >= demux_res->num_time_to_samples) { fprintf(stderr, "sample %i does not have a duration\n", samplenum); return 0; } } *sample_duration = demux_res->time_to_sample[duration_cur_index].sample_duration; *sample_byte_size = demux_res->sample_byte_size[samplenum]; return 1; } static int alacplug_get_totalsamples (demux_res_t *demux_res) { int totalsamples = 0; for (int i = 0; i < demux_res->num_sample_byte_sizes; i++) { unsigned int thissample_duration = 0; unsigned int thissample_bytesize = 0; get_sample_info(demux_res, i, &thissample_duration, &thissample_bytesize); totalsamples += thissample_duration; } return totalsamples; } static int alacplug_init (DB_fileinfo_t *_info, DB_playItem_t *it) { alacplug_info_t *info = (alacplug_info_t *)_info; deadbeef->pl_lock (); info->file = deadbeef->fopen (deadbeef->pl_find_meta (it, ":URI")); deadbeef->pl_unlock (); if (!info->file) { return -1; } info->stream = stream_create_file (info->file, 1, info->junk); if (!qtmovie_read(info->stream, &info->demux_res)) { if (!info->demux_res.format_read || info->demux_res.format != MAKEFOURCC('a','l','a','c')) { return -1; } } info->dataoffs = deadbeef->ftell (info->file); info->alac = create_alac(info->demux_res.sample_size, info->demux_res.num_channels); alac_set_info(info->alac, info->demux_res.codecdata); info->demux_res.sample_rate = alac_get_samplerate (info->alac); int totalsamples = alacplug_get_totalsamples (&info->demux_res); if (!info->file->vfs->is_streaming ()) { if (it->endsample > 0) { info->startsample = it->startsample; info->endsample = it->endsample; plugin.seek_sample (_info, 0); } else { info->startsample = 0; info->endsample = totalsamples-1; } } _info->plugin = &plugin; _info->fmt.bps = info->demux_res.sample_size; _info->fmt.channels = info->demux_res.num_channels; _info->fmt.samplerate = info->demux_res.sample_rate; for (int i = 0; i < _info->fmt.channels; i++) { _info->fmt.channelmask |= 1 << i; } return 0; } static void alacplug_free (DB_fileinfo_t *_info) { alacplug_info_t *info = (alacplug_info_t *)_info; if (info) { if (info->file) { deadbeef->fclose (info->file); } if (info->stream) { stream_destroy (info->stream); } qtmovie_free_demux (&info->demux_res); if (info->alac) { alac_file_free (info->alac); } free (info); } } static int alacplug_read (DB_fileinfo_t *_info, char *bytes, int size) { alacplug_info_t *info = (alacplug_info_t *)_info; int samplesize = _info->fmt.channels * _info->fmt.bps / 8; if (!info->file->vfs->is_streaming ()) { if (info->currentsample + size / samplesize > info->endsample) { size = (info->endsample - info->currentsample + 1) * samplesize; if (size <= 0) { trace ("alacplug_read: eof (current=%d, total=%d)\n", info->currentsample, info->endsample); return 0; } } } int initsize = size; while (size > 0) { // handle seeking if (info->skipsamples > 0 && info->out_remaining > 0) { int skip = min (info->out_remaining, info->skipsamples); if (skip < info->out_remaining) { memmove (info->out_buffer, info->out_buffer + skip * samplesize, (info->out_remaining - skip) * samplesize); } info->out_remaining -= skip; info->skipsamples -= skip; } if (info->out_remaining > 0) { int n = size / samplesize; n = min (info->out_remaining, n); char *src = info->out_buffer; memcpy (bytes, src, n * samplesize); bytes += n * samplesize; src += n * samplesize; size -= n * samplesize; if (n == info->out_remaining) { info->out_remaining = 0; } else { memmove (info->out_buffer, src, (info->out_remaining - n) * samplesize); info->out_remaining -= n; } continue; } // decode next frame if (info->current_frame == info->demux_res.num_sample_byte_sizes) { break; // end of file } uint32_t sample_duration; uint32_t sample_byte_size; int outputBytes; /* just get one sample for now */ if (!get_sample_info(&info->demux_res, info->current_frame, &sample_duration, &sample_byte_size)) { fprintf(stderr, "alac: sample failed\n"); break; } if (IN_BUFFER_SIZE < sample_byte_size) { fprintf(stderr, "alac: buffer too small! (is %i want %i)\n", IN_BUFFER_SIZE, sample_byte_size); break; } char buffer[IN_BUFFER_SIZE]; stream_read(info->stream, sample_byte_size, buffer); outputBytes = BUFFER_SIZE; decode_frame(info->alac, buffer, info->out_buffer, &outputBytes); info->current_frame++; info->out_remaining += outputBytes / samplesize; } info->currentsample += (initsize-size) / samplesize; return initsize-size; } static int alacplug_seek_sample (DB_fileinfo_t *_info, int sample) { alacplug_info_t *info = (alacplug_info_t *)_info; sample += info->startsample; int totalsamples = 0; int64_t seekpos = 0; int i; for (i = 0; i < info->demux_res.num_sample_byte_sizes; i++) { unsigned int thissample_duration = 0; unsigned int thissample_bytesize = 0; get_sample_info(&info->demux_res, i, &thissample_duration, &thissample_bytesize); if (totalsamples + thissample_duration > sample) { info->skipsamples = sample - totalsamples; break; } totalsamples += thissample_duration; seekpos += info->demux_res.sample_byte_size[i]; } if (i == info->demux_res.num_sample_byte_sizes) { return -1; } deadbeef->fseek(info->file, info->dataoffs + seekpos, SEEK_SET); info->current_frame = i; info->out_remaining = 0; info->currentsample = sample; _info->readpos = (float)(info->currentsample - info->startsample) / _info->fmt.samplerate; return 0; } static int alacplug_seek (DB_fileinfo_t *_info, float t) { return alacplug_seek_sample (_info, t * _info->fmt.samplerate); } static const char *metainfo[] = { "artist", "artist", "title", "title", "album", "album", "track", "track", "date", "year", "genre", "genre", "comment", "comment", "performer", "performer", "album_artist", "band", "writer", "composer", "vendor", "vendor", "disc", "disc", "compilation", "compilation", "totaldiscs", "numdiscs", "copyright", "copyright", "totaltracks", "numtracks", "tool", "tool", "MusicBrainz Track Id", "musicbrainz_trackid", NULL }; /* find a metadata item by name */ /* returns 0 if item found, 1 if no such item */ int32_t mp4ff_meta_find_by_name(const mp4ff_t *f, const char *item, char **value); void alacplug_load_tags (DB_playItem_t *it, mp4ff_t *mp4) { char *s = NULL; int got_itunes_tags = 0; int n = mp4ff_meta_get_num_items (mp4); for (int t = 0; t < n; t++) { char *key = NULL; char *value = NULL; int res = mp4ff_meta_get_by_index(mp4, t, &key, &value); if (key && value) { got_itunes_tags = 1; if (strcasecmp (key, "cover")) { if (!strcasecmp (key, "replaygain_track_gain")) { deadbeef->pl_set_item_replaygain (it, DDB_REPLAYGAIN_TRACKGAIN, atof (value)); } else if (!strcasecmp (key, "replaygain_album_gain")) { deadbeef->pl_set_item_replaygain (it, DDB_REPLAYGAIN_ALBUMGAIN, atof (value)); } else if (!strcasecmp (key, "replaygain_track_peak")) { deadbeef->pl_set_item_replaygain (it, DDB_REPLAYGAIN_TRACKPEAK, atof (value)); } else if (!strcasecmp (key, "replaygain_album_peak")) { deadbeef->pl_set_item_replaygain (it, DDB_REPLAYGAIN_ALBUMPEAK, atof (value)); } else { int i; for (i = 0; metainfo[i]; i += 2) { if (!strcasecmp (metainfo[i], key)) { deadbeef->pl_add_meta (it, metainfo[i+1], value); break; } } if (!metainfo[i]) { deadbeef->pl_add_meta (it, key, value); } } } } if (key) { free (key); } if (value) { free (value); } } if (got_itunes_tags) { uint32_t f = deadbeef->pl_get_item_flags (it); f |= DDB_TAG_ITUNES; deadbeef->pl_set_item_flags (it, f); } } int alacplug_read_metadata (DB_playItem_t *it) { deadbeef->pl_lock (); DB_FILE *fp = deadbeef->fopen (deadbeef->pl_find_meta (it, ":URI")); deadbeef->pl_unlock (); if (!fp) { return -1; } if (fp->vfs->is_streaming ()) { deadbeef->fclose (fp); return -1; } alacplug_info_t inf; memset (&inf, 0, sizeof (inf)); inf.file = fp; inf.junk = deadbeef->junk_get_leading_size (fp); if (inf.junk >= 0) { deadbeef->fseek (inf.file, inf.junk, SEEK_SET); } else { inf.junk = 0; } mp4ff_callback_t cb = { .read = alacplug_fs_read, .write = NULL, .seek = alacplug_fs_seek, .truncate = NULL, .user_data = &inf }; deadbeef->pl_delete_all_meta (it); mp4ff_t *mp4 = mp4ff_open_read (&cb); if (mp4) { alacplug_load_tags (it, mp4); mp4ff_close (mp4); } /*int apeerr = */deadbeef->junk_apev2_read (it, fp); /*int v2err = */deadbeef->junk_id3v2_read (it, fp); /*int v1err = */deadbeef->junk_id3v1_read (it, fp); deadbeef->fclose (fp); return 0; } static DB_playItem_t * alacplug_insert (ddb_playlist_t *plt, DB_playItem_t *after, const char *fname) { trace ("adding %s\n", fname); mp4ff_t *mp4 = NULL; DB_playItem_t *it = NULL; demux_res_t demux_res; memset (&demux_res, 0, sizeof (demux_res)); stream_t *stream; DB_FILE *fp = deadbeef->fopen (fname); if (!fp) { trace ("not found\n"); return NULL; } alacplug_info_t info = {0}; info.file = fp; info.junk = deadbeef->junk_get_leading_size (fp); if (info.junk >= 0) { trace ("junk: %d\n", info.junk); deadbeef->fseek (fp, info.junk, SEEK_SET); } else { info.junk = 0; } float duration = -1; stream = stream_create_file (fp, 1, info.junk); if (!stream) { trace ("alac: stream_create_file failed\n"); goto error; } if (!qtmovie_read(stream, &demux_res)) { if (!demux_res.format_read || demux_res.format != MAKEFOURCC('a','l','a','c')) { trace ("alac track not found in the file %s, expected atom %X got %X\n", fname, MAKEFOURCC('a','l','a','c'), demux_res.format); goto error; } } alac_file *alac = create_alac(demux_res.sample_size, demux_res.num_channels); alac_set_info(alac, demux_res.codecdata); demux_res.sample_rate = alac_get_samplerate (alac); alac_file_free (alac); it = deadbeef->pl_item_alloc_init (fname, plugin.plugin.id); deadbeef->pl_add_meta (it, ":FILETYPE", "ALAC"); int totalsamples = alacplug_get_totalsamples (&demux_res); duration = totalsamples / (float)demux_res.sample_rate; deadbeef->plt_set_item_duration (plt, it, duration); // read tags mp4ff_callback_t cb = { .read = alacplug_fs_read, .write = NULL, .seek = alacplug_fs_seek, .truncate = NULL, .user_data = &info }; deadbeef->fseek (fp, info.junk, SEEK_SET); mp4 = mp4ff_open_read (&cb); if (mp4) { alacplug_load_tags (it, mp4); } int apeerr = deadbeef->junk_apev2_read (it, fp); int v2err = deadbeef->junk_id3v2_read (it, fp); int v1err = deadbeef->junk_id3v1_read (it, fp); int64_t fsize = deadbeef->fgetlength (fp); deadbeef->fclose (fp); fp = NULL; stream_destroy (stream); stream = NULL; if (mp4) { mp4ff_close (mp4); mp4 = NULL; } int samplerate = demux_res.sample_rate; int bps = demux_res.sample_size; int channels = demux_res.num_channels; qtmovie_free_demux (&demux_res); trace ("duration %f\n", duration); if (duration > 0) { char s[100]; snprintf (s, sizeof (s), "%lld", fsize); deadbeef->pl_add_meta (it, ":FILE_SIZE", s); snprintf (s, sizeof (s), "%d", bps); deadbeef->pl_add_meta (it, ":BPS", s); snprintf (s, sizeof (s), "%d", channels); deadbeef->pl_add_meta (it, ":CHANNELS", s); snprintf (s, sizeof (s), "%d", samplerate); deadbeef->pl_add_meta (it, ":SAMPLERATE", s); int br = (int)roundf(fsize / duration * 8 / 1000); snprintf (s, sizeof (s), "%d", br); deadbeef->pl_add_meta (it, ":BITRATE", s); // embedded cue deadbeef->pl_lock (); const char *cuesheet = deadbeef->pl_find_meta (it, "cuesheet"); DB_playItem_t *cue = NULL; if (cuesheet) { cue = deadbeef->plt_insert_cue_from_buffer (plt, after, it, cuesheet, strlen (cuesheet), totalsamples, samplerate); if (cue) { deadbeef->pl_item_unref (it); deadbeef->pl_item_unref (cue); deadbeef->pl_unlock (); return cue; } } deadbeef->pl_unlock (); cue = deadbeef->plt_insert_cue (plt, after, it, totalsamples, samplerate); if (cue) { deadbeef->pl_item_unref (it); deadbeef->pl_item_unref (cue); return cue; } } trace ("success\n"); success: after = deadbeef->plt_insert_item (plt, after, it); deadbeef->pl_item_unref (it); error: if (fp) { deadbeef->fclose (fp); } if (mp4) { mp4ff_close (mp4); } qtmovie_free_demux (&demux_res); return it; } static const char * exts[] = { "mp4", "m4a", NULL }; // 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 = "alac", .plugin.name = "ALAC player", .plugin.descr = "plays alac files from MP4 and M4A files", .plugin.copyright = "ALAC plugin for deadbeef\n" "Copyright (C) 2012-2013 Alexey Yakovenko \n" "Uses the reverse engineered ALAC decoder (C) 2005 David Hammerton\n" "All rights reserved.\n" "\n" "Permission is hereby granted, free of charge, to any person\n" "obtaining a copy of this software and associated documentation\n" "files (the \"Software\"), to deal in the Software without\n" "restriction, including without limitation the rights to use,\n" "copy, modify, merge, publish, distribute, sublicense, and/or\n" "sell copies of the Software, and to permit persons to whom the\n" "Software is furnished to do so, subject to the following conditions:\n" "\n" "The above copyright notice and this permission notice shall be\n" "included in all copies or substantial portions of the Software.\n" "\n" "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n" "EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n" "OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n" "NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n" "HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n" "WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n" "FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n" "OTHER DEALINGS IN THE SOFTWARE.\n" , .plugin.website = "http://deadbeef.sf.net", .open = alacplug_open, .init = alacplug_init, .free = alacplug_free, .read = alacplug_read, .seek = alacplug_seek, .seek_sample = alacplug_seek_sample, .insert = alacplug_insert, .read_metadata = alacplug_read_metadata, .exts = exts, }; DB_plugin_t * alac_load (DB_functions_t *api) { deadbeef = api; return DB_PLUGIN (&plugin); }