1 /*
2 ALAC plugin for deadbeef
3 Copyright (C) 2012-2013 Alexey Yakovenko <waker@users.sourceforge.net>
4 Uses the reverse engineered ALAC decoder (C) 2005 David Hammerton
5 All rights reserved.
6
7 Permission is hereby granted, free of charge, to any person
8 obtaining a copy of this software and associated documentation
9 files (the "Software"), to deal in the Software without
10 restriction, including without limitation the rights to use,
11 copy, modify, merge, publish, distribute, sublicense, and/or
12 sell copies of the Software, and to permit persons to whom the
13 Software is furnished to do so, subject to the following conditions:
14
15 The above copyright notice and this permission notice shall be
16 included in all copies or substantial portions of the Software.
17
18 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
20 OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
22 HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
23 WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
24 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
25 OTHER DEALINGS IN THE SOFTWARE.
26 */
27 #include "../../deadbeef.h"
28 #ifdef HAVE_CONFIG_H
29 #include "../../config.h"
30 #endif
31 #include <stdlib.h>
32 #include <string.h>
33 #include <math.h>
34 #include "mp4ff.h"
35 #include "demux.h"
36 #include "decomp.h"
37 #include "stream.h"
38
39 #define min(x,y) ((x)<(y)?(x):(y))
40 #define max(x,y) ((x)>(y)?(x):(y))
41
42 //#define trace(...) { fprintf(stderr, __VA_ARGS__); }
43 #define trace(fmt,...)
44
45 static DB_decoder_t plugin;
46 DB_functions_t *deadbeef;
47
48 #ifdef WORDS_BIGENDIAN
49 int host_bigendian = 1;
50 #else
51 int host_bigendian = 0;
52 #endif
53
54 #define BUFFER_SIZE (1024*24)
55 #define IN_BUFFER_SIZE (1024*80)
56
57 typedef struct {
58 DB_fileinfo_t info;
59 DB_FILE *file;
60 demux_res_t demux_res;
61 stream_t *stream;
62 alac_file *alac;
63 int junk;
64 char out_buffer[BUFFER_SIZE];
65 int out_remaining;
66 int skipsamples;
67 int currentsample;
68 int startsample;
69 int endsample;
70 int current_frame;
71 int64_t dataoffs;
72 } alacplug_info_t;
73
74 // allocate codec control structure
75 static DB_fileinfo_t *
alacplug_open(uint32_t hints)76 alacplug_open (uint32_t hints) {
77 DB_fileinfo_t *_info = malloc (sizeof (alacplug_info_t));
78 alacplug_info_t *info = (alacplug_info_t *)_info;
79 memset (info, 0, sizeof (alacplug_info_t));
80 return _info;
81 }
82
83 static uint32_t
alacplug_fs_read(void * user_data,void * buffer,uint32_t length)84 alacplug_fs_read (void *user_data, void *buffer, uint32_t length) {
85 alacplug_info_t *info = user_data;
86 return deadbeef->fread (buffer, 1, length, info->file);
87 }
88
89 static uint32_t
alacplug_fs_seek(void * user_data,uint64_t position)90 alacplug_fs_seek (void *user_data, uint64_t position) {
91 alacplug_info_t *info = user_data;
92 return deadbeef->fseek (info->file, position+info->junk, SEEK_SET);
93 }
94
95 static int
get_sample_info(demux_res_t * demux_res,uint32_t samplenum,uint32_t * sample_duration,uint32_t * sample_byte_size)96 get_sample_info(demux_res_t *demux_res, uint32_t samplenum,
97 uint32_t *sample_duration,
98 uint32_t *sample_byte_size)
99 {
100 unsigned int duration_index_accum = 0;
101 unsigned int duration_cur_index = 0;
102
103 if (samplenum >= demux_res->num_sample_byte_sizes)
104 {
105 fprintf(stderr, "sample %i does not exist\n", samplenum);
106 return 0;
107 }
108
109 if (!demux_res->num_time_to_samples)
110 {
111 fprintf(stderr, "no time to samples\n");
112 return 0;
113 }
114 while ((demux_res->time_to_sample[duration_cur_index].sample_count + duration_index_accum)
115 <= samplenum)
116 {
117 duration_index_accum += demux_res->time_to_sample[duration_cur_index].sample_count;
118 duration_cur_index++;
119 if (duration_cur_index >= demux_res->num_time_to_samples)
120 {
121 fprintf(stderr, "sample %i does not have a duration\n", samplenum);
122 return 0;
123 }
124 }
125
126 *sample_duration = demux_res->time_to_sample[duration_cur_index].sample_duration;
127 *sample_byte_size = demux_res->sample_byte_size[samplenum];
128
129 return 1;
130 }
131
132 static int
alacplug_get_totalsamples(demux_res_t * demux_res)133 alacplug_get_totalsamples (demux_res_t *demux_res) {
134 int totalsamples = 0;
135 for (int i = 0; i < demux_res->num_sample_byte_sizes; i++)
136 {
137 unsigned int thissample_duration = 0;
138 unsigned int thissample_bytesize = 0;
139
140 get_sample_info(demux_res, i, &thissample_duration,
141 &thissample_bytesize);
142
143 totalsamples += thissample_duration;
144 }
145 return totalsamples;
146 }
147
148 static int
alacplug_init(DB_fileinfo_t * _info,DB_playItem_t * it)149 alacplug_init (DB_fileinfo_t *_info, DB_playItem_t *it) {
150 alacplug_info_t *info = (alacplug_info_t *)_info;
151
152 deadbeef->pl_lock ();
153 info->file = deadbeef->fopen (deadbeef->pl_find_meta (it, ":URI"));
154 deadbeef->pl_unlock ();
155 if (!info->file) {
156 return -1;
157 }
158
159 info->stream = stream_create_file (info->file, 1, info->junk);
160
161 if (!qtmovie_read(info->stream, &info->demux_res)) {
162 if (!info->demux_res.format_read || info->demux_res.format != MAKEFOURCC('a','l','a','c')) {
163 return -1;
164 }
165 }
166 info->dataoffs = deadbeef->ftell (info->file);
167
168 info->alac = create_alac(info->demux_res.sample_size, info->demux_res.num_channels);
169 alac_set_info(info->alac, info->demux_res.codecdata);
170 info->demux_res.sample_rate = alac_get_samplerate (info->alac);
171 info->demux_res.sample_size = alac_get_bitspersample (info->alac);
172
173 int totalsamples = alacplug_get_totalsamples (&info->demux_res);
174 if (!info->file->vfs->is_streaming ()) {
175 if (it->endsample > 0) {
176 info->startsample = it->startsample;
177 info->endsample = it->endsample;
178 plugin.seek_sample (_info, 0);
179 }
180 else {
181 info->startsample = 0;
182 info->endsample = totalsamples-1;
183 }
184 }
185
186 _info->plugin = &plugin;
187 _info->fmt.bps = info->demux_res.sample_size;
188 _info->fmt.channels = info->demux_res.num_channels;
189 _info->fmt.samplerate = info->demux_res.sample_rate;
190 for (int i = 0; i < _info->fmt.channels; i++) {
191 _info->fmt.channelmask |= 1 << i;
192 }
193
194 return 0;
195 }
196
197 static void
alacplug_free(DB_fileinfo_t * _info)198 alacplug_free (DB_fileinfo_t *_info) {
199 alacplug_info_t *info = (alacplug_info_t *)_info;
200 if (info) {
201 if (info->file) {
202 deadbeef->fclose (info->file);
203 }
204 if (info->stream) {
205 stream_destroy (info->stream);
206 }
207 qtmovie_free_demux (&info->demux_res);
208 if (info->alac) {
209 alac_file_free (info->alac);
210 }
211 free (info);
212 }
213 }
214
215 static int
alacplug_read(DB_fileinfo_t * _info,char * bytes,int size)216 alacplug_read (DB_fileinfo_t *_info, char *bytes, int size) {
217 alacplug_info_t *info = (alacplug_info_t *)_info;
218 int samplesize = _info->fmt.channels * _info->fmt.bps / 8;
219 if (!info->file->vfs->is_streaming ()) {
220 if (info->currentsample + size / samplesize > info->endsample) {
221 size = (info->endsample - info->currentsample + 1) * samplesize;
222 if (size <= 0) {
223 trace ("alacplug_read: eof (current=%d, total=%d)\n", info->currentsample, info->endsample);
224 return 0;
225 }
226 }
227 }
228 int initsize = size;
229 while (size > 0) {
230 // handle seeking
231 if (info->skipsamples > 0 && info->out_remaining > 0) {
232 int skip = min (info->out_remaining, info->skipsamples);
233 if (skip < info->out_remaining) {
234 memmove (info->out_buffer, info->out_buffer + skip * samplesize, (info->out_remaining - skip) * samplesize);
235 }
236 info->out_remaining -= skip;
237 info->skipsamples -= skip;
238 }
239 if (info->out_remaining > 0) {
240 int n = size / samplesize;
241 n = min (info->out_remaining, n);
242
243 char *src = info->out_buffer;
244 memcpy (bytes, src, n * samplesize);
245 bytes += n * samplesize;
246 src += n * samplesize;
247 size -= n * samplesize;
248
249 if (n == info->out_remaining) {
250 info->out_remaining = 0;
251 }
252 else {
253 memmove (info->out_buffer, src, (info->out_remaining - n) * samplesize);
254 info->out_remaining -= n;
255 }
256 continue;
257 }
258
259 // decode next frame
260 if (info->current_frame == info->demux_res.num_sample_byte_sizes) {
261 break; // end of file
262 }
263
264 uint32_t sample_duration;
265 uint32_t sample_byte_size;
266
267 int outputBytes;
268
269 /* just get one sample for now */
270 if (!get_sample_info(&info->demux_res, info->current_frame,
271 &sample_duration, &sample_byte_size))
272 {
273 fprintf(stderr, "alac: sample failed\n");
274 break;
275 }
276
277 if (IN_BUFFER_SIZE < sample_byte_size)
278 {
279 fprintf(stderr, "alac: buffer too small! (is %i want %i)\n",
280 IN_BUFFER_SIZE,
281 sample_byte_size);
282 break;
283 }
284
285 char buffer[IN_BUFFER_SIZE];
286 stream_read(info->stream, sample_byte_size, buffer);
287
288 outputBytes = BUFFER_SIZE;
289 decode_frame(info->alac, buffer, info->out_buffer, &outputBytes);
290 info->current_frame++;
291
292 info->out_remaining += outputBytes / samplesize;
293 }
294
295 info->currentsample += (initsize-size) / samplesize;
296 return initsize-size;
297 }
298
299 static int
alacplug_seek_sample(DB_fileinfo_t * _info,int sample)300 alacplug_seek_sample (DB_fileinfo_t *_info, int sample) {
301 alacplug_info_t *info = (alacplug_info_t *)_info;
302
303 sample += info->startsample;
304
305 int totalsamples = 0;
306 int64_t seekpos = 0;
307 int i;
308 for (i = 0; i < info->demux_res.num_sample_byte_sizes; i++)
309 {
310 unsigned int thissample_duration = 0;
311 unsigned int thissample_bytesize = 0;
312
313 get_sample_info(&info->demux_res, i, &thissample_duration,
314 &thissample_bytesize);
315
316 if (totalsamples + thissample_duration > sample) {
317 info->skipsamples = sample - totalsamples;
318 break;
319 }
320 totalsamples += thissample_duration;
321 seekpos += info->demux_res.sample_byte_size[i];
322 }
323
324 if (i == info->demux_res.num_sample_byte_sizes) {
325 return -1;
326 }
327
328
329 deadbeef->fseek(info->file, info->dataoffs + seekpos, SEEK_SET);
330
331 info->current_frame = i;
332 info->out_remaining = 0;
333 info->currentsample = sample;
334 _info->readpos = (float)(info->currentsample - info->startsample) / _info->fmt.samplerate;
335 return 0;
336 }
337
338 static int
alacplug_seek(DB_fileinfo_t * _info,float t)339 alacplug_seek (DB_fileinfo_t *_info, float t) {
340 return alacplug_seek_sample (_info, t * _info->fmt.samplerate);
341 }
342
343 static const char *metainfo[] = {
344 "artist", "artist",
345 "title", "title",
346 "album", "album",
347 "track", "track",
348 "date", "year",
349 "genre", "genre",
350 "comment", "comment",
351 "performer", "performer",
352 "album_artist", "band",
353 "writer", "composer",
354 "vendor", "vendor",
355 "disc", "disc",
356 "compilation", "compilation",
357 "totaldiscs", "numdiscs",
358 "copyright", "copyright",
359 "totaltracks", "numtracks",
360 "tool", "tool",
361 "MusicBrainz Track Id", "musicbrainz_trackid",
362 NULL
363 };
364
365
366 /* find a metadata item by name */
367 /* returns 0 if item found, 1 if no such item */
368 int32_t mp4ff_meta_find_by_name(const mp4ff_t *f, const char *item, char **value);
369
370
371 void
alacplug_load_tags(DB_playItem_t * it,mp4ff_t * mp4)372 alacplug_load_tags (DB_playItem_t *it, mp4ff_t *mp4) {
373 char *s = NULL;
374 int got_itunes_tags = 0;
375
376 int n = mp4ff_meta_get_num_items (mp4);
377 for (int t = 0; t < n; t++) {
378 char *key = NULL;
379 char *value = NULL;
380 int res = mp4ff_meta_get_by_index(mp4, t, &key, &value);
381 if (key && value) {
382 got_itunes_tags = 1;
383 if (strcasecmp (key, "cover")) {
384 if (!strcasecmp (key, "replaygain_track_gain")) {
385 deadbeef->pl_set_item_replaygain (it, DDB_REPLAYGAIN_TRACKGAIN, atof (value));
386 }
387 else if (!strcasecmp (key, "replaygain_album_gain")) {
388 deadbeef->pl_set_item_replaygain (it, DDB_REPLAYGAIN_ALBUMGAIN, atof (value));
389 }
390 else if (!strcasecmp (key, "replaygain_track_peak")) {
391 deadbeef->pl_set_item_replaygain (it, DDB_REPLAYGAIN_TRACKPEAK, atof (value));
392 }
393 else if (!strcasecmp (key, "replaygain_album_peak")) {
394 deadbeef->pl_set_item_replaygain (it, DDB_REPLAYGAIN_ALBUMPEAK, atof (value));
395 }
396 else {
397 int i;
398 for (i = 0; metainfo[i]; i += 2) {
399 if (!strcasecmp (metainfo[i], key)) {
400 deadbeef->pl_add_meta (it, metainfo[i+1], value);
401 break;
402 }
403 }
404 if (!metainfo[i]) {
405 deadbeef->pl_add_meta (it, key, value);
406 }
407 }
408 }
409 }
410 if (key) {
411 free (key);
412 }
413 if (value) {
414 free (value);
415 }
416 }
417
418 if (got_itunes_tags) {
419 uint32_t f = deadbeef->pl_get_item_flags (it);
420 f |= DDB_TAG_ITUNES;
421 deadbeef->pl_set_item_flags (it, f);
422 }
423 }
424
425 int
alacplug_read_metadata(DB_playItem_t * it)426 alacplug_read_metadata (DB_playItem_t *it) {
427 deadbeef->pl_lock ();
428 DB_FILE *fp = deadbeef->fopen (deadbeef->pl_find_meta (it, ":URI"));
429 deadbeef->pl_unlock ();
430 if (!fp) {
431 return -1;
432 }
433
434 if (fp->vfs->is_streaming ()) {
435 deadbeef->fclose (fp);
436 return -1;
437 }
438
439 alacplug_info_t inf;
440 memset (&inf, 0, sizeof (inf));
441 inf.file = fp;
442 inf.junk = deadbeef->junk_get_leading_size (fp);
443 if (inf.junk >= 0) {
444 deadbeef->fseek (inf.file, inf.junk, SEEK_SET);
445 }
446 else {
447 inf.junk = 0;
448 }
449
450 mp4ff_callback_t cb = {
451 .read = alacplug_fs_read,
452 .write = NULL,
453 .seek = alacplug_fs_seek,
454 .truncate = NULL,
455 .user_data = &inf
456 };
457
458 deadbeef->pl_delete_all_meta (it);
459
460 mp4ff_t *mp4 = mp4ff_open_read (&cb);
461 if (mp4) {
462 alacplug_load_tags (it, mp4);
463 mp4ff_close (mp4);
464 }
465 /*int apeerr = */deadbeef->junk_apev2_read (it, fp);
466 /*int v2err = */deadbeef->junk_id3v2_read (it, fp);
467 /*int v1err = */deadbeef->junk_id3v1_read (it, fp);
468 deadbeef->fclose (fp);
469 return 0;
470 }
471
472 static DB_playItem_t *
alacplug_insert(ddb_playlist_t * plt,DB_playItem_t * after,const char * fname)473 alacplug_insert (ddb_playlist_t *plt, DB_playItem_t *after, const char *fname) {
474 trace ("adding %s\n", fname);
475 mp4ff_t *mp4 = NULL;
476 DB_playItem_t *it = NULL;
477 demux_res_t demux_res;
478 memset (&demux_res, 0, sizeof (demux_res));
479 stream_t *stream;
480 DB_FILE *fp = deadbeef->fopen (fname);
481 if (!fp) {
482 trace ("not found\n");
483 return NULL;
484 }
485 alacplug_info_t info = {0};
486 info.file = fp;
487 info.junk = deadbeef->junk_get_leading_size (fp);
488 if (info.junk >= 0) {
489 trace ("junk: %d\n", info.junk);
490 deadbeef->fseek (fp, info.junk, SEEK_SET);
491 }
492 else {
493 info.junk = 0;
494 }
495
496 float duration = -1;
497
498 stream = stream_create_file (fp, 1, info.junk);
499 if (!stream) {
500 trace ("alac: stream_create_file failed\n");
501 goto error;
502 }
503
504 if (!qtmovie_read(stream, &demux_res)) {
505 if (!demux_res.format_read || demux_res.format != MAKEFOURCC('a','l','a','c')) {
506 trace ("alac track not found in the file %s, expected atom %X got %X\n", fname, MAKEFOURCC('a','l','a','c'), demux_res.format);
507 goto error;
508 }
509 }
510
511 alac_file *alac = create_alac(demux_res.sample_size, demux_res.num_channels);
512 alac_set_info(alac, demux_res.codecdata);
513 demux_res.sample_rate = alac_get_samplerate (alac);
514 demux_res.sample_size = alac_get_bitspersample (alac);
515 alac_file_free (alac);
516
517 it = deadbeef->pl_item_alloc_init (fname, plugin.plugin.id);
518 deadbeef->pl_add_meta (it, ":FILETYPE", "ALAC");
519
520 int totalsamples = alacplug_get_totalsamples (&demux_res);
521 duration = totalsamples / (float)demux_res.sample_rate;
522
523 deadbeef->plt_set_item_duration (plt, it, duration);
524
525 // read tags
526 mp4ff_callback_t cb = {
527 .read = alacplug_fs_read,
528 .write = NULL,
529 .seek = alacplug_fs_seek,
530 .truncate = NULL,
531 .user_data = &info
532 };
533 deadbeef->fseek (fp, info.junk, SEEK_SET);
534 mp4 = mp4ff_open_read (&cb);
535 if (mp4) {
536 alacplug_load_tags (it, mp4);
537 }
538
539 int apeerr = deadbeef->junk_apev2_read (it, fp);
540 int v2err = deadbeef->junk_id3v2_read (it, fp);
541 int v1err = deadbeef->junk_id3v1_read (it, fp);
542
543 int64_t fsize = deadbeef->fgetlength (fp);
544
545 deadbeef->fclose (fp);
546 fp = NULL;
547 stream_destroy (stream);
548 stream = NULL;
549 if (mp4) {
550 mp4ff_close (mp4);
551 mp4 = NULL;
552 }
553 int samplerate = demux_res.sample_rate;
554 int bps = demux_res.sample_size;
555 int channels = demux_res.num_channels;
556
557 qtmovie_free_demux (&demux_res);
558
559 trace ("duration %f\n", duration);
560 if (duration > 0) {
561 char s[100];
562 snprintf (s, sizeof (s), "%lld", fsize);
563 deadbeef->pl_add_meta (it, ":FILE_SIZE", s);
564 snprintf (s, sizeof (s), "%d", bps);
565 deadbeef->pl_add_meta (it, ":BPS", s);
566 snprintf (s, sizeof (s), "%d", channels);
567 deadbeef->pl_add_meta (it, ":CHANNELS", s);
568 snprintf (s, sizeof (s), "%d", samplerate);
569 deadbeef->pl_add_meta (it, ":SAMPLERATE", s);
570 int br = (int)roundf(fsize / duration * 8 / 1000);
571 snprintf (s, sizeof (s), "%d", br);
572 deadbeef->pl_add_meta (it, ":BITRATE", s);
573 // embedded cue
574 deadbeef->pl_lock ();
575 const char *cuesheet = deadbeef->pl_find_meta (it, "cuesheet");
576 DB_playItem_t *cue = NULL;
577
578 if (cuesheet) {
579 cue = deadbeef->plt_insert_cue_from_buffer (plt, after, it, cuesheet, strlen (cuesheet), totalsamples, samplerate);
580 if (cue) {
581 deadbeef->pl_item_unref (it);
582 deadbeef->pl_item_unref (cue);
583 deadbeef->pl_unlock ();
584 return cue;
585 }
586 }
587 deadbeef->pl_unlock ();
588
589 cue = deadbeef->plt_insert_cue (plt, after, it, totalsamples, samplerate);
590 if (cue) {
591 deadbeef->pl_item_unref (it);
592 deadbeef->pl_item_unref (cue);
593 return cue;
594 }
595 }
596
597 trace ("success\n");
598 success:
599 after = deadbeef->plt_insert_item (plt, after, it);
600 deadbeef->pl_item_unref (it);
601 error:
602 if (fp) {
603 deadbeef->fclose (fp);
604 }
605 if (mp4) {
606 mp4ff_close (mp4);
607 }
608 qtmovie_free_demux (&demux_res);
609 return it;
610 }
611
612 static const char * exts[] = { "mp4", "m4a", NULL };
613
614 // define plugin interface
615 static DB_decoder_t plugin = {
616 .plugin.api_vmajor = 1,
617 .plugin.api_vminor = 0,
618 .plugin.version_major = 1,
619 .plugin.version_minor = 0,
620 .plugin.type = DB_PLUGIN_DECODER,
621 .plugin.id = "alac",
622 .plugin.name = "ALAC player",
623 .plugin.descr = "plays alac files from MP4 and M4A files",
624 .plugin.copyright =
625 "ALAC plugin for deadbeef\n"
626 "Copyright (C) 2012-2013 Alexey Yakovenko <waker@users.sourceforge.net>\n"
627 "Uses the reverse engineered ALAC decoder (C) 2005 David Hammerton\n"
628 "All rights reserved.\n"
629 "\n"
630 "Permission is hereby granted, free of charge, to any person\n"
631 "obtaining a copy of this software and associated documentation\n"
632 "files (the \"Software\"), to deal in the Software without\n"
633 "restriction, including without limitation the rights to use,\n"
634 "copy, modify, merge, publish, distribute, sublicense, and/or\n"
635 "sell copies of the Software, and to permit persons to whom the\n"
636 "Software is furnished to do so, subject to the following conditions:\n"
637 "\n"
638 "The above copyright notice and this permission notice shall be\n"
639 "included in all copies or substantial portions of the Software.\n"
640 "\n"
641 "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n"
642 "EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n"
643 "OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n"
644 "NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n"
645 "HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n"
646 "WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n"
647 "FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n"
648 "OTHER DEALINGS IN THE SOFTWARE.\n"
649 ,
650 .plugin.website = "http://deadbeef.sf.net",
651 .open = alacplug_open,
652 .init = alacplug_init,
653 .free = alacplug_free,
654 .read = alacplug_read,
655 .seek = alacplug_seek,
656 .seek_sample = alacplug_seek_sample,
657 .insert = alacplug_insert,
658 .read_metadata = alacplug_read_metadata,
659 .exts = exts,
660 };
661
662 DB_plugin_t *
alac_load(DB_functions_t * api)663 alac_load (DB_functions_t *api) {
664 deadbeef = api;
665 return DB_PLUGIN (&plugin);
666 }
667