summaryrefslogtreecommitdiff
path: root/plugins/alac/alac_plugin.c
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/alac/alac_plugin.c')
-rw-r--r--plugins/alac/alac_plugin.c663
1 files changed, 663 insertions, 0 deletions
diff --git a/plugins/alac/alac_plugin.c b/plugins/alac/alac_plugin.c
new file mode 100644
index 00000000..25f45d1e
--- /dev/null
+++ b/plugins/alac/alac_plugin.c
@@ -0,0 +1,663 @@
+/*
+ ALAC plugin for deadbeef
+ Copyright (C) 2012 Alexey Yakovenko <waker@users.sourceforge.net>
+ 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 <stdlib.h>
+#include <string.h>
+#include <math.h>
+#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",
+ 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 Alexey Yakovenko <waker@users.sourceforge.net>\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);
+}