From 51adaf2c47761e3f2095a52a7037cd8458b77990 Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Fri, 24 Jan 2014 00:02:24 +0100 Subject: decoder/*: move to decoder/plugins/ --- src/decoder/plugins/AdPlugDecoderPlugin.cxx | 141 +++ src/decoder/plugins/AdPlugDecoderPlugin.h | 25 + src/decoder/plugins/AudiofileDecoderPlugin.cxx | 267 ++++++ src/decoder/plugins/AudiofileDecoderPlugin.hxx | 25 + src/decoder/plugins/DsdLib.cxx | 163 ++++ src/decoder/plugins/DsdLib.hxx | 82 ++ src/decoder/plugins/DsdiffDecoderPlugin.cxx | 523 ++++++++++ src/decoder/plugins/DsdiffDecoderPlugin.hxx | 25 + src/decoder/plugins/DsfDecoderPlugin.cxx | 357 +++++++ src/decoder/plugins/DsfDecoderPlugin.hxx | 25 + src/decoder/plugins/FaadDecoderPlugin.cxx | 464 +++++++++ src/decoder/plugins/FaadDecoderPlugin.hxx | 25 + src/decoder/plugins/FfmpegDecoderPlugin.cxx | 686 ++++++++++++++ src/decoder/plugins/FfmpegDecoderPlugin.hxx | 25 + src/decoder/plugins/FfmpegMetaData.cxx | 76 ++ src/decoder/plugins/FfmpegMetaData.hxx | 38 + src/decoder/plugins/FlacCommon.cxx | 190 ++++ src/decoder/plugins/FlacCommon.hxx | 93 ++ src/decoder/plugins/FlacDecoderPlugin.cxx | 383 ++++++++ src/decoder/plugins/FlacDecoderPlugin.h | 26 + src/decoder/plugins/FlacDomain.cxx | 24 + src/decoder/plugins/FlacDomain.hxx | 27 + src/decoder/plugins/FlacIOHandle.cxx | 114 +++ src/decoder/plugins/FlacIOHandle.hxx | 45 + src/decoder/plugins/FlacInput.cxx | 154 +++ src/decoder/plugins/FlacInput.hxx | 75 ++ src/decoder/plugins/FlacMetadata.cxx | 223 +++++ src/decoder/plugins/FlacMetadata.hxx | 140 +++ src/decoder/plugins/FlacPcm.cxx | 110 +++ src/decoder/plugins/FlacPcm.hxx | 33 + src/decoder/plugins/FluidsynthDecoderPlugin.cxx | 224 +++++ src/decoder/plugins/FluidsynthDecoderPlugin.hxx | 25 + src/decoder/plugins/GmeDecoderPlugin.cxx | 295 ++++++ src/decoder/plugins/GmeDecoderPlugin.hxx | 25 + src/decoder/plugins/MadDecoderPlugin.cxx | 1155 +++++++++++++++++++++++ src/decoder/plugins/MadDecoderPlugin.hxx | 25 + src/decoder/plugins/MikmodDecoderPlugin.cxx | 248 +++++ src/decoder/plugins/MikmodDecoderPlugin.hxx | 25 + src/decoder/plugins/ModplugDecoderPlugin.cxx | 215 +++++ src/decoder/plugins/ModplugDecoderPlugin.hxx | 25 + src/decoder/plugins/MpcdecDecoderPlugin.cxx | 278 ++++++ src/decoder/plugins/MpcdecDecoderPlugin.hxx | 25 + src/decoder/plugins/Mpg123DecoderPlugin.cxx | 255 +++++ src/decoder/plugins/Mpg123DecoderPlugin.hxx | 25 + src/decoder/plugins/OggCodec.cxx | 51 + src/decoder/plugins/OggCodec.hxx | 40 + src/decoder/plugins/OggFind.cxx | 68 ++ src/decoder/plugins/OggFind.hxx | 56 ++ src/decoder/plugins/OggSyncState.hxx | 78 ++ src/decoder/plugins/OggUtil.cxx | 118 +++ src/decoder/plugins/OggUtil.hxx | 87 ++ src/decoder/plugins/OpusDecoderPlugin.cxx | 485 ++++++++++ src/decoder/plugins/OpusDecoderPlugin.h | 25 + src/decoder/plugins/OpusDomain.cxx | 24 + src/decoder/plugins/OpusDomain.hxx | 27 + src/decoder/plugins/OpusHead.cxx | 43 + src/decoder/plugins/OpusHead.hxx | 30 + src/decoder/plugins/OpusReader.hxx | 101 ++ src/decoder/plugins/OpusTags.cxx | 102 ++ src/decoder/plugins/OpusTags.hxx | 34 + src/decoder/plugins/PcmDecoderPlugin.cxx | 112 +++ src/decoder/plugins/PcmDecoderPlugin.hxx | 33 + src/decoder/plugins/SidplayDecoderPlugin.cxx | 436 +++++++++ src/decoder/plugins/SidplayDecoderPlugin.hxx | 25 + src/decoder/plugins/SndfileDecoderPlugin.cxx | 260 +++++ src/decoder/plugins/SndfileDecoderPlugin.hxx | 25 + src/decoder/plugins/VorbisComments.cxx | 141 +++ src/decoder/plugins/VorbisComments.hxx | 39 + src/decoder/plugins/VorbisDecoderPlugin.cxx | 349 +++++++ src/decoder/plugins/VorbisDecoderPlugin.h | 25 + src/decoder/plugins/VorbisDomain.cxx | 24 + src/decoder/plugins/VorbisDomain.hxx | 29 + src/decoder/plugins/WavpackDecoderPlugin.cxx | 571 +++++++++++ src/decoder/plugins/WavpackDecoderPlugin.hxx | 25 + src/decoder/plugins/WildmidiDecoderPlugin.cxx | 158 ++++ src/decoder/plugins/WildmidiDecoderPlugin.hxx | 25 + src/decoder/plugins/XiphTags.cxx | 28 + src/decoder/plugins/XiphTags.hxx | 28 + 78 files changed, 11131 insertions(+) create mode 100644 src/decoder/plugins/AdPlugDecoderPlugin.cxx create mode 100644 src/decoder/plugins/AdPlugDecoderPlugin.h create mode 100644 src/decoder/plugins/AudiofileDecoderPlugin.cxx create mode 100644 src/decoder/plugins/AudiofileDecoderPlugin.hxx create mode 100644 src/decoder/plugins/DsdLib.cxx create mode 100644 src/decoder/plugins/DsdLib.hxx create mode 100644 src/decoder/plugins/DsdiffDecoderPlugin.cxx create mode 100644 src/decoder/plugins/DsdiffDecoderPlugin.hxx create mode 100644 src/decoder/plugins/DsfDecoderPlugin.cxx create mode 100644 src/decoder/plugins/DsfDecoderPlugin.hxx create mode 100644 src/decoder/plugins/FaadDecoderPlugin.cxx create mode 100644 src/decoder/plugins/FaadDecoderPlugin.hxx create mode 100644 src/decoder/plugins/FfmpegDecoderPlugin.cxx create mode 100644 src/decoder/plugins/FfmpegDecoderPlugin.hxx create mode 100644 src/decoder/plugins/FfmpegMetaData.cxx create mode 100644 src/decoder/plugins/FfmpegMetaData.hxx create mode 100644 src/decoder/plugins/FlacCommon.cxx create mode 100644 src/decoder/plugins/FlacCommon.hxx create mode 100644 src/decoder/plugins/FlacDecoderPlugin.cxx create mode 100644 src/decoder/plugins/FlacDecoderPlugin.h create mode 100644 src/decoder/plugins/FlacDomain.cxx create mode 100644 src/decoder/plugins/FlacDomain.hxx create mode 100644 src/decoder/plugins/FlacIOHandle.cxx create mode 100644 src/decoder/plugins/FlacIOHandle.hxx create mode 100644 src/decoder/plugins/FlacInput.cxx create mode 100644 src/decoder/plugins/FlacInput.hxx create mode 100644 src/decoder/plugins/FlacMetadata.cxx create mode 100644 src/decoder/plugins/FlacMetadata.hxx create mode 100644 src/decoder/plugins/FlacPcm.cxx create mode 100644 src/decoder/plugins/FlacPcm.hxx create mode 100644 src/decoder/plugins/FluidsynthDecoderPlugin.cxx create mode 100644 src/decoder/plugins/FluidsynthDecoderPlugin.hxx create mode 100644 src/decoder/plugins/GmeDecoderPlugin.cxx create mode 100644 src/decoder/plugins/GmeDecoderPlugin.hxx create mode 100644 src/decoder/plugins/MadDecoderPlugin.cxx create mode 100644 src/decoder/plugins/MadDecoderPlugin.hxx create mode 100644 src/decoder/plugins/MikmodDecoderPlugin.cxx create mode 100644 src/decoder/plugins/MikmodDecoderPlugin.hxx create mode 100644 src/decoder/plugins/ModplugDecoderPlugin.cxx create mode 100644 src/decoder/plugins/ModplugDecoderPlugin.hxx create mode 100644 src/decoder/plugins/MpcdecDecoderPlugin.cxx create mode 100644 src/decoder/plugins/MpcdecDecoderPlugin.hxx create mode 100644 src/decoder/plugins/Mpg123DecoderPlugin.cxx create mode 100644 src/decoder/plugins/Mpg123DecoderPlugin.hxx create mode 100644 src/decoder/plugins/OggCodec.cxx create mode 100644 src/decoder/plugins/OggCodec.hxx create mode 100644 src/decoder/plugins/OggFind.cxx create mode 100644 src/decoder/plugins/OggFind.hxx create mode 100644 src/decoder/plugins/OggSyncState.hxx create mode 100644 src/decoder/plugins/OggUtil.cxx create mode 100644 src/decoder/plugins/OggUtil.hxx create mode 100644 src/decoder/plugins/OpusDecoderPlugin.cxx create mode 100644 src/decoder/plugins/OpusDecoderPlugin.h create mode 100644 src/decoder/plugins/OpusDomain.cxx create mode 100644 src/decoder/plugins/OpusDomain.hxx create mode 100644 src/decoder/plugins/OpusHead.cxx create mode 100644 src/decoder/plugins/OpusHead.hxx create mode 100644 src/decoder/plugins/OpusReader.hxx create mode 100644 src/decoder/plugins/OpusTags.cxx create mode 100644 src/decoder/plugins/OpusTags.hxx create mode 100644 src/decoder/plugins/PcmDecoderPlugin.cxx create mode 100644 src/decoder/plugins/PcmDecoderPlugin.hxx create mode 100644 src/decoder/plugins/SidplayDecoderPlugin.cxx create mode 100644 src/decoder/plugins/SidplayDecoderPlugin.hxx create mode 100644 src/decoder/plugins/SndfileDecoderPlugin.cxx create mode 100644 src/decoder/plugins/SndfileDecoderPlugin.hxx create mode 100644 src/decoder/plugins/VorbisComments.cxx create mode 100644 src/decoder/plugins/VorbisComments.hxx create mode 100644 src/decoder/plugins/VorbisDecoderPlugin.cxx create mode 100644 src/decoder/plugins/VorbisDecoderPlugin.h create mode 100644 src/decoder/plugins/VorbisDomain.cxx create mode 100644 src/decoder/plugins/VorbisDomain.hxx create mode 100644 src/decoder/plugins/WavpackDecoderPlugin.cxx create mode 100644 src/decoder/plugins/WavpackDecoderPlugin.hxx create mode 100644 src/decoder/plugins/WildmidiDecoderPlugin.cxx create mode 100644 src/decoder/plugins/WildmidiDecoderPlugin.hxx create mode 100644 src/decoder/plugins/XiphTags.cxx create mode 100644 src/decoder/plugins/XiphTags.hxx (limited to 'src/decoder/plugins') diff --git a/src/decoder/plugins/AdPlugDecoderPlugin.cxx b/src/decoder/plugins/AdPlugDecoderPlugin.cxx new file mode 100644 index 000000000..c288d8cf4 --- /dev/null +++ b/src/decoder/plugins/AdPlugDecoderPlugin.cxx @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "AdPlugDecoderPlugin.h" +#include "tag/TagHandler.hxx" +#include "../DecoderAPI.hxx" +#include "CheckAudioFormat.hxx" +#include "util/Error.hxx" +#include "util/Macros.hxx" +#include "Log.hxx" + +#include +#include + +#include + +static unsigned sample_rate; + +static bool +adplug_init(const config_param ¶m) +{ + Error error; + + sample_rate = param.GetBlockValue("sample_rate", 48000u); + if (!audio_check_sample_rate(sample_rate, error)) { + LogError(error); + return false; + } + + return true; +} + +static void +adplug_file_decode(Decoder &decoder, const char *path_fs) +{ + CEmuopl opl(sample_rate, true, true); + opl.init(); + + CPlayer *player = CAdPlug::factory(path_fs, &opl); + if (player == nullptr) + return; + + const AudioFormat audio_format(sample_rate, SampleFormat::S16, 2); + assert(audio_format.IsValid()); + + decoder_initialized(decoder, audio_format, false, + player->songlength() / 1000.); + + int16_t buffer[2048]; + const unsigned frames_per_buffer = ARRAY_SIZE(buffer) / 2; + DecoderCommand cmd; + + do { + if (!player->update()) + break; + + opl.update(buffer, frames_per_buffer); + cmd = decoder_data(decoder, nullptr, + buffer, sizeof(buffer), + 0); + } while (cmd == DecoderCommand::NONE); + + delete player; +} + +static void +adplug_scan_tag(TagType type, const std::string &value, + const struct tag_handler *handler, void *handler_ctx) +{ + if (!value.empty()) + tag_handler_invoke_tag(handler, handler_ctx, + type, value.c_str()); +} + +static bool +adplug_scan_file(const char *path_fs, + const struct tag_handler *handler, void *handler_ctx) +{ + CEmuopl opl(sample_rate, true, true); + opl.init(); + + CPlayer *player = CAdPlug::factory(path_fs, &opl); + if (player == nullptr) + return false; + + tag_handler_invoke_duration(handler, handler_ctx, + player->songlength() / 1000); + + if (handler->tag != nullptr) { + adplug_scan_tag(TAG_TITLE, player->gettitle(), + handler, handler_ctx); + adplug_scan_tag(TAG_ARTIST, player->getauthor(), + handler, handler_ctx); + adplug_scan_tag(TAG_COMMENT, player->getdesc(), + handler, handler_ctx); + } + + delete player; + return true; +} + +static const char *const adplug_suffixes[] = { + "amd", + "d00", + "hsc", + "laa", + "rad", + "raw", + "sa2", + nullptr +}; + +const struct DecoderPlugin adplug_decoder_plugin = { + "adplug", + adplug_init, + nullptr, + nullptr, + adplug_file_decode, + adplug_scan_file, + nullptr, + nullptr, + adplug_suffixes, + nullptr, +}; diff --git a/src/decoder/plugins/AdPlugDecoderPlugin.h b/src/decoder/plugins/AdPlugDecoderPlugin.h new file mode 100644 index 000000000..539dbbf0a --- /dev/null +++ b/src/decoder/plugins/AdPlugDecoderPlugin.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_DECODER_ADPLUG_H +#define MPD_DECODER_ADPLUG_H + +extern const struct DecoderPlugin adplug_decoder_plugin; + +#endif diff --git a/src/decoder/plugins/AudiofileDecoderPlugin.cxx b/src/decoder/plugins/AudiofileDecoderPlugin.cxx new file mode 100644 index 000000000..cad908cfd --- /dev/null +++ b/src/decoder/plugins/AudiofileDecoderPlugin.cxx @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "AudiofileDecoderPlugin.hxx" +#include "../DecoderAPI.hxx" +#include "InputStream.hxx" +#include "CheckAudioFormat.hxx" +#include "tag/TagHandler.hxx" +#include "util/Error.hxx" +#include "util/Domain.hxx" +#include "Log.hxx" + +#include +#include + +#include + +/* pick 1020 since its devisible for 8,16,24, and 32-bit audio */ +#define CHUNK_SIZE 1020 + +static constexpr Domain audiofile_domain("audiofile"); + +static int audiofile_get_duration(const char *file) +{ + int total_time; + AFfilehandle af_fp = afOpenFile(file, "r", nullptr); + if (af_fp == AF_NULL_FILEHANDLE) { + return -1; + } + total_time = (int) + ((double)afGetFrameCount(af_fp, AF_DEFAULT_TRACK) + / afGetRate(af_fp, AF_DEFAULT_TRACK)); + afCloseFile(af_fp); + return total_time; +} + +static ssize_t +audiofile_file_read(AFvirtualfile *vfile, void *data, size_t length) +{ + InputStream &is = *(InputStream *)vfile->closure; + + Error error; + size_t nbytes = is.LockRead(data, length, error); + if (nbytes == 0 && error.IsDefined()) { + LogError(error); + return -1; + } + + return nbytes; +} + +static AFfileoffset +audiofile_file_length(AFvirtualfile *vfile) +{ + InputStream &is = *(InputStream *)vfile->closure; + return is.GetSize(); +} + +static AFfileoffset +audiofile_file_tell(AFvirtualfile *vfile) +{ + InputStream &is = *(InputStream *)vfile->closure; + return is.GetOffset(); +} + +static void +audiofile_file_destroy(AFvirtualfile *vfile) +{ + assert(vfile->closure != nullptr); + + vfile->closure = nullptr; +} + +static AFfileoffset +audiofile_file_seek(AFvirtualfile *vfile, AFfileoffset offset, int is_relative) +{ + InputStream &is = *(InputStream *)vfile->closure; + int whence = (is_relative ? SEEK_CUR : SEEK_SET); + + Error error; + if (is.LockSeek(offset, whence, error)) { + return is.GetOffset(); + } else { + return -1; + } +} + +static AFvirtualfile * +setup_virtual_fops(InputStream &stream) +{ + AFvirtualfile *vf = new AFvirtualfile(); + vf->closure = &stream; + vf->write = nullptr; + vf->read = audiofile_file_read; + vf->length = audiofile_file_length; + vf->destroy = audiofile_file_destroy; + vf->seek = audiofile_file_seek; + vf->tell = audiofile_file_tell; + return vf; +} + +static SampleFormat +audiofile_bits_to_sample_format(int bits) +{ + switch (bits) { + case 8: + return SampleFormat::S8; + + case 16: + return SampleFormat::S16; + + case 24: + return SampleFormat::S24_P32; + + case 32: + return SampleFormat::S32; + } + + return SampleFormat::UNDEFINED; +} + +static SampleFormat +audiofile_setup_sample_format(AFfilehandle af_fp) +{ + int fs, bits; + + afGetSampleFormat(af_fp, AF_DEFAULT_TRACK, &fs, &bits); + if (!audio_valid_sample_format(audiofile_bits_to_sample_format(bits))) { + FormatDebug(audiofile_domain, + "input file has %d bit samples, converting to 16", + bits); + bits = 16; + } + + afSetVirtualSampleFormat(af_fp, AF_DEFAULT_TRACK, + AF_SAMPFMT_TWOSCOMP, bits); + afGetVirtualSampleFormat(af_fp, AF_DEFAULT_TRACK, &fs, &bits); + + return audiofile_bits_to_sample_format(bits); +} + +static void +audiofile_stream_decode(Decoder &decoder, InputStream &is) +{ + AFvirtualfile *vf; + int fs, frame_count; + AFfilehandle af_fp; + AudioFormat audio_format; + float total_time; + uint16_t bit_rate; + int ret; + char chunk[CHUNK_SIZE]; + + if (!is.IsSeekable()) { + LogWarning(audiofile_domain, "not seekable"); + return; + } + + vf = setup_virtual_fops(is); + + af_fp = afOpenVirtualFile(vf, "r", nullptr); + if (af_fp == AF_NULL_FILEHANDLE) { + LogWarning(audiofile_domain, "failed to input stream"); + return; + } + + Error error; + if (!audio_format_init_checked(audio_format, + afGetRate(af_fp, AF_DEFAULT_TRACK), + audiofile_setup_sample_format(af_fp), + afGetVirtualChannels(af_fp, AF_DEFAULT_TRACK), + error)) { + LogError(error); + afCloseFile(af_fp); + return; + } + + frame_count = afGetFrameCount(af_fp, AF_DEFAULT_TRACK); + + total_time = ((float)frame_count / (float)audio_format.sample_rate); + + bit_rate = (uint16_t)(is.GetSize() * 8.0 / total_time / 1000.0 + 0.5); + + fs = (int)afGetVirtualFrameSize(af_fp, AF_DEFAULT_TRACK, 1); + + decoder_initialized(decoder, audio_format, true, total_time); + + DecoderCommand cmd; + do { + ret = afReadFrames(af_fp, AF_DEFAULT_TRACK, chunk, + CHUNK_SIZE / fs); + if (ret <= 0) + break; + + cmd = decoder_data(decoder, nullptr, + chunk, ret * fs, + bit_rate); + + if (cmd == DecoderCommand::SEEK) { + AFframecount frame = decoder_seek_where(decoder) * + audio_format.sample_rate; + afSeekFrame(af_fp, AF_DEFAULT_TRACK, frame); + + decoder_command_finished(decoder); + cmd = DecoderCommand::NONE; + } + } while (cmd == DecoderCommand::NONE); + + afCloseFile(af_fp); +} + +static bool +audiofile_scan_file(const char *file, + const struct tag_handler *handler, void *handler_ctx) +{ + int total_time = audiofile_get_duration(file); + + if (total_time < 0) { + FormatWarning(audiofile_domain, + "Failed to get total song time from: %s", + file); + return false; + } + + tag_handler_invoke_duration(handler, handler_ctx, total_time); + return true; +} + +static const char *const audiofile_suffixes[] = { + "wav", "au", "aiff", "aif", nullptr +}; + +static const char *const audiofile_mime_types[] = { + "audio/x-wav", + "audio/x-aiff", + nullptr +}; + +const struct DecoderPlugin audiofile_decoder_plugin = { + "audiofile", + nullptr, + nullptr, + audiofile_stream_decode, + nullptr, + audiofile_scan_file, + nullptr, + nullptr, + audiofile_suffixes, + audiofile_mime_types, +}; diff --git a/src/decoder/plugins/AudiofileDecoderPlugin.hxx b/src/decoder/plugins/AudiofileDecoderPlugin.hxx new file mode 100644 index 000000000..61129076d --- /dev/null +++ b/src/decoder/plugins/AudiofileDecoderPlugin.hxx @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_DECODER_AUDIOFILE_HXX +#define MPD_DECODER_AUDIOFILE_HXX + +extern const struct DecoderPlugin audiofile_decoder_plugin; + +#endif diff --git a/src/decoder/plugins/DsdLib.cxx b/src/decoder/plugins/DsdLib.cxx new file mode 100644 index 000000000..43af7dee1 --- /dev/null +++ b/src/decoder/plugins/DsdLib.cxx @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +/* \file + * + * This file contains functions used by the DSF and DSDIFF decoders. + * + */ + +#include "config.h" +#include "DsdLib.hxx" +#include "../DecoderAPI.hxx" +#include "InputStream.hxx" +#include "tag/TagId3.hxx" +#include "util/Error.hxx" + +#include +#include +#include /* for SEEK_SET, SEEK_CUR */ + +#ifdef HAVE_ID3TAG +#include +#endif + +bool +DsdId::Equals(const char *s) const +{ + assert(s != nullptr); + assert(strlen(s) == sizeof(value)); + + return memcmp(value, s, sizeof(value)) == 0; +} + +bool +dsdlib_read(Decoder *decoder, InputStream &is, + void *data, size_t length) +{ + size_t nbytes = decoder_read(decoder, is, data, length); + return nbytes == length; +} + +/** + * Skip the #input_stream to the specified offset. + */ +bool +dsdlib_skip_to(Decoder *decoder, InputStream &is, + int64_t offset) +{ + if (is.IsSeekable()) + return is.Seek(offset, SEEK_SET, IgnoreError()); + + if (is.GetOffset() > offset) + return false; + + char buffer[8192]; + while (is.GetOffset() < offset) { + size_t length = sizeof(buffer); + if (offset - is.GetOffset() < (int64_t)length) + length = offset - is.GetOffset(); + + size_t nbytes = decoder_read(decoder, is, buffer, length); + if (nbytes == 0) + return false; + } + + assert(is.GetOffset() == offset); + return true; +} + +/** + * Skip some bytes from the #input_stream. + */ +bool +dsdlib_skip(Decoder *decoder, InputStream &is, + int64_t delta) +{ + assert(delta >= 0); + + if (delta == 0) + return true; + + if (is.IsSeekable()) + return is.Seek(delta, SEEK_CUR, IgnoreError()); + + char buffer[8192]; + while (delta > 0) { + size_t length = sizeof(buffer); + if ((int64_t)length > delta) + length = delta; + + size_t nbytes = decoder_read(decoder, is, buffer, length); + if (nbytes == 0) + return false; + + delta -= nbytes; + } + + return true; +} + +#ifdef HAVE_ID3TAG +void +dsdlib_tag_id3(InputStream &is, + const struct tag_handler *handler, + void *handler_ctx, int64_t tagoffset) +{ + assert(tagoffset >= 0); + + if (tagoffset == 0) + return; + + if (!dsdlib_skip_to(nullptr, is, tagoffset)) + return; + + struct id3_tag *id3_tag = nullptr; + id3_length_t count; + + /* Prevent broken files causing problems */ + const auto size = is.GetSize(); + const auto offset = is.GetOffset(); + if (offset >= size) + return; + + count = size - offset; + + /* Check and limit id3 tag size to prevent a stack overflow */ + if (count == 0 || count > 4096) + return; + + id3_byte_t dsdid3[count]; + id3_byte_t *dsdid3data; + dsdid3data = dsdid3; + + if (!dsdlib_read(nullptr, is, dsdid3data, count)) + return; + + id3_tag = id3_tag_parse(dsdid3data, count); + if (id3_tag == nullptr) + return; + + scan_id3_tag(id3_tag, handler, handler_ctx); + + id3_tag_delete(id3_tag); + + return; +} +#endif diff --git a/src/decoder/plugins/DsdLib.hxx b/src/decoder/plugins/DsdLib.hxx new file mode 100644 index 000000000..4c5e83114 --- /dev/null +++ b/src/decoder/plugins/DsdLib.hxx @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_DECODER_DSDLIB_HXX +#define MPD_DECODER_DSDLIB_HXX + +#include "system/ByteOrder.hxx" +#include "Compiler.h" + +#include +#include + +struct Decoder; +struct InputStream; + +struct DsdId { + char value[4]; + + gcc_pure + bool Equals(const char *s) const; +}; + +class DsdUint64 { + uint32_t lo; + uint32_t hi; + +public: + constexpr uint64_t Read() const { + return (uint64_t(FromLE32(hi)) << 32) | + uint64_t(FromLE32(lo)); + } +}; + +class DffDsdUint64 { + uint32_t hi; + uint32_t lo; + +public: + constexpr uint64_t Read() const { + return (uint64_t(FromBE32(hi)) << 32) | + uint64_t(FromBE32(lo)); + } +}; + +bool +dsdlib_read(Decoder *decoder, InputStream &is, + void *data, size_t length); + +bool +dsdlib_skip_to(Decoder *decoder, InputStream &is, + int64_t offset); + +bool +dsdlib_skip(Decoder *decoder, InputStream &is, + int64_t delta); + +/** + * Add tags from ID3 tag. All tags commonly found in the ID3 tags of + * DSF and DSDIFF files are imported + */ +void +dsdlib_tag_id3(InputStream &is, + const struct tag_handler *handler, + void *handler_ctx, int64_t tagoffset); + +#endif diff --git a/src/decoder/plugins/DsdiffDecoderPlugin.cxx b/src/decoder/plugins/DsdiffDecoderPlugin.cxx new file mode 100644 index 000000000..89ad2ecd0 --- /dev/null +++ b/src/decoder/plugins/DsdiffDecoderPlugin.cxx @@ -0,0 +1,523 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +/* \file + * + * This plugin decodes DSDIFF data (SACD) embedded in DFF files. + * The DFF code was modeled after the specification found here: + * http://www.sonicstudio.com/pdf/dsd/DSDIFF_1.5_Spec.pdf + * + * All functions common to both DSD decoders have been moved to dsdlib + */ + +#include "config.h" +#include "DsdiffDecoderPlugin.hxx" +#include "../DecoderAPI.hxx" +#include "InputStream.hxx" +#include "CheckAudioFormat.hxx" +#include "util/bit_reverse.h" +#include "util/Error.hxx" +#include "system/ByteOrder.hxx" +#include "tag/TagHandler.hxx" +#include "DsdLib.hxx" +#include "Log.hxx" + +struct DsdiffHeader { + DsdId id; + DffDsdUint64 size; + DsdId format; +}; + +struct DsdiffChunkHeader { + DsdId id; + DffDsdUint64 size; + + /** + * Read the "size" attribute from the specified header, converting it + * to the host byte order if needed. + */ + constexpr + uint64_t GetSize() const { + return size.Read(); + } +}; + +/** struct for DSDIFF native Artist and Title tags */ +struct dsdiff_native_tag { + uint32_t size; +}; + +struct DsdiffMetaData { + unsigned sample_rate, channels; + bool bitreverse; + uint64_t chunk_size; +#ifdef HAVE_ID3TAG + InputStream::offset_type id3_offset; + uint64_t id3_size; +#endif + /** offset for artist tag */ + InputStream::offset_type diar_offset; + /** offset for title tag */ + InputStream::offset_type diti_offset; +}; + +static bool lsbitfirst; + +static bool +dsdiff_init(const config_param ¶m) +{ + lsbitfirst = param.GetBlockValue("lsbitfirst", false); + return true; +} + +static bool +dsdiff_read_id(Decoder *decoder, InputStream &is, + DsdId *id) +{ + return dsdlib_read(decoder, is, id, sizeof(*id)); +} + +static bool +dsdiff_read_chunk_header(Decoder *decoder, InputStream &is, + DsdiffChunkHeader *header) +{ + return dsdlib_read(decoder, is, header, sizeof(*header)); +} + +static bool +dsdiff_read_payload(Decoder *decoder, InputStream &is, + const DsdiffChunkHeader *header, + void *data, size_t length) +{ + uint64_t size = header->GetSize(); + if (size != (uint64_t)length) + return false; + + size_t nbytes = decoder_read(decoder, is, data, length); + return nbytes == length; +} + +/** + * Read and parse a "SND" chunk inside "PROP". + */ +static bool +dsdiff_read_prop_snd(Decoder *decoder, InputStream &is, + DsdiffMetaData *metadata, + InputStream::offset_type end_offset) +{ + DsdiffChunkHeader header; + while ((InputStream::offset_type)(is.GetOffset() + sizeof(header)) <= end_offset) { + if (!dsdiff_read_chunk_header(decoder, is, &header)) + return false; + + InputStream::offset_type chunk_end_offset = is.GetOffset() + + header.GetSize(); + if (chunk_end_offset > end_offset) + return false; + + if (header.id.Equals("FS ")) { + uint32_t sample_rate; + if (!dsdiff_read_payload(decoder, is, &header, + &sample_rate, + sizeof(sample_rate))) + return false; + + metadata->sample_rate = FromBE32(sample_rate); + } else if (header.id.Equals("CHNL")) { + uint16_t channels; + if (header.GetSize() < sizeof(channels) || + !dsdlib_read(decoder, is, + &channels, sizeof(channels)) || + !dsdlib_skip_to(decoder, is, chunk_end_offset)) + return false; + + metadata->channels = FromBE16(channels); + } else if (header.id.Equals("CMPR")) { + DsdId type; + if (header.GetSize() < sizeof(type) || + !dsdlib_read(decoder, is, + &type, sizeof(type)) || + !dsdlib_skip_to(decoder, is, chunk_end_offset)) + return false; + + if (!type.Equals("DSD ")) + /* only uncompressed DSD audio data + is implemented */ + return false; + } else { + /* ignore unknown chunk */ + + if (!dsdlib_skip_to(decoder, is, chunk_end_offset)) + return false; + } + } + + return is.GetOffset() == end_offset; +} + +/** + * Read and parse a "PROP" chunk. + */ +static bool +dsdiff_read_prop(Decoder *decoder, InputStream &is, + DsdiffMetaData *metadata, + const DsdiffChunkHeader *prop_header) +{ + uint64_t prop_size = prop_header->GetSize(); + InputStream::offset_type end_offset = is.GetOffset() + prop_size; + + DsdId prop_id; + if (prop_size < sizeof(prop_id) || + !dsdiff_read_id(decoder, is, &prop_id)) + return false; + + if (prop_id.Equals("SND ")) + return dsdiff_read_prop_snd(decoder, is, metadata, end_offset); + else + /* ignore unknown PROP chunk */ + return dsdlib_skip_to(decoder, is, end_offset); +} + +static void +dsdiff_handle_native_tag(InputStream &is, + const struct tag_handler *handler, + void *handler_ctx, InputStream::offset_type tagoffset, + TagType type) +{ + if (!dsdlib_skip_to(nullptr, is, tagoffset)) + return; + + struct dsdiff_native_tag metatag; + + if (!dsdlib_read(nullptr, is, &metatag, sizeof(metatag))) + return; + + uint32_t length = FromBE32(metatag.size); + + /* Check and limit size of the tag to prevent a stack overflow */ + if (length == 0 || length > 60) + return; + + char string[length]; + char *label; + label = string; + + if (!dsdlib_read(nullptr, is, label, (size_t)length)) + return; + + string[length] = '\0'; + tag_handler_invoke_tag(handler, handler_ctx, type, label); + return; +} + +/** + * Read and parse additional metadata chunks for tagging purposes. By default + * dsdiff files only support equivalents for artist and title but some of the + * extract tools add an id3 tag to provide more tags. If such id3 is found + * this will be used for tagging otherwise the native tags (if any) will be + * used + */ + +static bool +dsdiff_read_metadata_extra(Decoder *decoder, InputStream &is, + DsdiffMetaData *metadata, + DsdiffChunkHeader *chunk_header, + const struct tag_handler *handler, + void *handler_ctx) +{ + + /* skip from DSD data to next chunk header */ + if (!dsdlib_skip(decoder, is, metadata->chunk_size)) + return false; + if (!dsdiff_read_chunk_header(decoder, is, chunk_header)) + return false; + +#ifdef HAVE_ID3TAG + metadata->id3_size = 0; +#endif + + /* Now process all the remaining chunk headers in the stream + and record their position and size */ + + const auto size = is.GetSize(); + while (is.GetOffset() < size) { + uint64_t chunk_size = chunk_header->GetSize(); + + /* DIIN chunk, is directly followed by other chunks */ + if (chunk_header->id.Equals("DIIN")) + chunk_size = 0; + + /* DIAR chunk - DSDIFF native tag for Artist */ + if (chunk_header->id.Equals("DIAR")) { + chunk_size = chunk_header->GetSize(); + metadata->diar_offset = is.GetOffset(); + } + + /* DITI chunk - DSDIFF native tag for Title */ + if (chunk_header->id.Equals("DITI")) { + chunk_size = chunk_header->GetSize(); + metadata->diti_offset = is.GetOffset(); + } +#ifdef HAVE_ID3TAG + /* 'ID3 ' chunk, offspec. Used by sacdextract */ + if (chunk_header->id.Equals("ID3 ")) { + chunk_size = chunk_header->GetSize(); + metadata->id3_offset = is.GetOffset(); + metadata->id3_size = chunk_size; + } +#endif + if (chunk_size != 0) { + if (!dsdlib_skip(decoder, is, chunk_size)) + break; + } + + if (is.GetOffset() < size) { + if (!dsdiff_read_chunk_header(decoder, is, chunk_header)) + return false; + } + } + /* done processing chunk headers, process tags if any */ + +#ifdef HAVE_ID3TAG + if (metadata->id3_offset != 0) + { + /* a ID3 tag has preference over the other tags, do not process + other tags if we have one */ + dsdlib_tag_id3(is, handler, handler_ctx, metadata->id3_offset); + return true; + } +#endif + + if (metadata->diar_offset != 0) + dsdiff_handle_native_tag(is, handler, handler_ctx, + metadata->diar_offset, TAG_ARTIST); + + if (metadata->diti_offset != 0) + dsdiff_handle_native_tag(is, handler, handler_ctx, + metadata->diti_offset, TAG_TITLE); + return true; +} + +/** + * Read and parse all metadata chunks at the beginning. Stop when the + * first "DSD" chunk is seen, and return its header in the + * "chunk_header" parameter. + */ +static bool +dsdiff_read_metadata(Decoder *decoder, InputStream &is, + DsdiffMetaData *metadata, + DsdiffChunkHeader *chunk_header) +{ + DsdiffHeader header; + if (!dsdlib_read(decoder, is, &header, sizeof(header)) || + !header.id.Equals("FRM8") || + !header.format.Equals("DSD ")) + return false; + + while (true) { + if (!dsdiff_read_chunk_header(decoder, is, + chunk_header)) + return false; + + if (chunk_header->id.Equals("PROP")) { + if (!dsdiff_read_prop(decoder, is, metadata, + chunk_header)) + return false; + } else if (chunk_header->id.Equals("DSD ")) { + const uint64_t chunk_size = chunk_header->GetSize(); + metadata->chunk_size = chunk_size; + return true; + } else { + /* ignore unknown chunk */ + const uint64_t chunk_size = chunk_header->GetSize(); + InputStream::offset_type chunk_end_offset = + is.GetOffset() + chunk_size; + + if (!dsdlib_skip_to(decoder, is, chunk_end_offset)) + return false; + } + } +} + +static void +bit_reverse_buffer(uint8_t *p, uint8_t *end) +{ + for (; p < end; ++p) + *p = bit_reverse(*p); +} + +/** + * Decode one "DSD" chunk. + */ +static bool +dsdiff_decode_chunk(Decoder &decoder, InputStream &is, + unsigned channels, + uint64_t chunk_size) +{ + uint8_t buffer[8192]; + + const size_t sample_size = sizeof(buffer[0]); + const size_t frame_size = channels * sample_size; + const unsigned buffer_frames = sizeof(buffer) / frame_size; + const unsigned buffer_samples = buffer_frames * frame_size; + const size_t buffer_size = buffer_samples * sample_size; + + while (chunk_size > 0) { + /* see how much aligned data from the remaining chunk + fits into the local buffer */ + size_t now_size = buffer_size; + if (chunk_size < (uint64_t)now_size) { + unsigned now_frames = + (unsigned)chunk_size / frame_size; + now_size = now_frames * frame_size; + } + + size_t nbytes = decoder_read(decoder, is, buffer, now_size); + if (nbytes != now_size) + return false; + + chunk_size -= nbytes; + + if (lsbitfirst) + bit_reverse_buffer(buffer, buffer + nbytes); + + const auto cmd = decoder_data(decoder, is, buffer, nbytes, 0); + switch (cmd) { + case DecoderCommand::NONE: + break; + + case DecoderCommand::START: + case DecoderCommand::STOP: + return false; + + case DecoderCommand::SEEK: + + /* Not implemented yet */ + decoder_seek_error(decoder); + break; + } + } + return dsdlib_skip(&decoder, is, chunk_size); +} + +static void +dsdiff_stream_decode(Decoder &decoder, InputStream &is) +{ + DsdiffMetaData metadata; + + DsdiffChunkHeader chunk_header; + /* check if it is is a proper DFF file */ + if (!dsdiff_read_metadata(&decoder, is, &metadata, &chunk_header)) + return; + + Error error; + AudioFormat audio_format; + if (!audio_format_init_checked(audio_format, metadata.sample_rate / 8, + SampleFormat::DSD, + metadata.channels, error)) { + LogError(error); + return; + } + + /* calculate song time from DSD chunk size and sample frequency */ + uint64_t chunk_size = metadata.chunk_size; + float songtime = ((chunk_size / metadata.channels) * 8) / + (float) metadata.sample_rate; + + /* success: file was recognized */ + decoder_initialized(decoder, audio_format, false, songtime); + + /* every iteration of the following loop decodes one "DSD" + chunk from a DFF file */ + + while (true) { + chunk_size = chunk_header.GetSize(); + + if (chunk_header.id.Equals("DSD ")) { + if (!dsdiff_decode_chunk(decoder, is, + metadata.channels, + chunk_size)) + break; + } else { + /* ignore other chunks */ + if (!dsdlib_skip(&decoder, is, chunk_size)) + break; + } + + /* read next chunk header; the first one was read by + dsdiff_read_metadata() */ + if (!dsdiff_read_chunk_header(&decoder, + is, &chunk_header)) + break; + } +} + +static bool +dsdiff_scan_stream(InputStream &is, + gcc_unused const struct tag_handler *handler, + gcc_unused void *handler_ctx) +{ + DsdiffMetaData metadata; + DsdiffChunkHeader chunk_header; + + /* First check for DFF metadata */ + if (!dsdiff_read_metadata(nullptr, is, &metadata, &chunk_header)) + return false; + + AudioFormat audio_format; + if (!audio_format_init_checked(audio_format, metadata.sample_rate / 8, + SampleFormat::DSD, + metadata.channels, IgnoreError())) + /* refuse to parse files which we cannot play anyway */ + return false; + + /* calculate song time and add as tag */ + unsigned songtime = ((metadata.chunk_size / metadata.channels) * 8) / + metadata.sample_rate; + tag_handler_invoke_duration(handler, handler_ctx, songtime); + + /* Read additional metadata and created tags if available */ + dsdiff_read_metadata_extra(nullptr, is, &metadata, &chunk_header, + handler, handler_ctx); + + return true; +} + +static const char *const dsdiff_suffixes[] = { + "dff", + nullptr +}; + +static const char *const dsdiff_mime_types[] = { + "application/x-dff", + nullptr +}; + +const struct DecoderPlugin dsdiff_decoder_plugin = { + "dsdiff", + dsdiff_init, + nullptr, + dsdiff_stream_decode, + nullptr, + nullptr, + dsdiff_scan_stream, + nullptr, + dsdiff_suffixes, + dsdiff_mime_types, +}; diff --git a/src/decoder/plugins/DsdiffDecoderPlugin.hxx b/src/decoder/plugins/DsdiffDecoderPlugin.hxx new file mode 100644 index 000000000..7aa36752b --- /dev/null +++ b/src/decoder/plugins/DsdiffDecoderPlugin.hxx @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_DECODER_DSDIFF_H +#define MPD_DECODER_DSDIFF_H + +extern const struct DecoderPlugin dsdiff_decoder_plugin; + +#endif diff --git a/src/decoder/plugins/DsfDecoderPlugin.cxx b/src/decoder/plugins/DsfDecoderPlugin.cxx new file mode 100644 index 000000000..bb056bbec --- /dev/null +++ b/src/decoder/plugins/DsfDecoderPlugin.cxx @@ -0,0 +1,357 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +/* \file + * + * This plugin decodes DSDIFF data (SACD) embedded in DSF files. + * + * The DSF code was created using the specification found here: + * http://dsd-guide.com/sonys-dsf-file-format-spec + * + * All functions common to both DSD decoders have been moved to dsdlib + */ + +#include "config.h" +#include "DsfDecoderPlugin.hxx" +#include "../DecoderAPI.hxx" +#include "InputStream.hxx" +#include "CheckAudioFormat.hxx" +#include "util/bit_reverse.h" +#include "util/Error.hxx" +#include "system/ByteOrder.hxx" +#include "DsdLib.hxx" +#include "tag/TagHandler.hxx" +#include "Log.hxx" + +struct DsfMetaData { + unsigned sample_rate, channels; + bool bitreverse; + uint64_t chunk_size; +#ifdef HAVE_ID3TAG + InputStream::offset_type id3_offset; + uint64_t id3_size; +#endif +}; + +struct DsfHeader { + /** DSF header id: "DSD " */ + DsdId id; + /** DSD chunk size, including id = 28 */ + DsdUint64 size; + /** total file size */ + DsdUint64 fsize; + /** pointer to id3v2 metadata, should be at the end of the file */ + DsdUint64 pmeta; +}; + +/** DSF file fmt chunk */ +struct DsfFmtChunk { + /** id: "fmt " */ + DsdId id; + /** fmt chunk size, including id, normally 52 */ + DsdUint64 size; + /** version of this format = 1 */ + uint32_t version; + /** 0: DSD raw */ + uint32_t formatid; + /** channel type, 1 = mono, 2 = stereo, 3 = 3 channels, etc */ + uint32_t channeltype; + /** Channel number, 1 = mono, 2 = stereo, ... 6 = 6 channels */ + uint32_t channelnum; + /** sample frequency: 2822400, 5644800 */ + uint32_t sample_freq; + /** bits per sample 1 or 8 */ + uint32_t bitssample; + /** Sample count per channel in bytes */ + DsdUint64 scnt; + /** block size per channel = 4096 */ + uint32_t block_size; + /** reserved, should be all zero */ + uint32_t reserved; +}; + +struct DsfDataChunk { + DsdId id; + /** "data" chunk size, includes header (id+size) */ + DsdUint64 size; +}; + +/** + * Read and parse all needed metadata chunks for DSF files. + */ +static bool +dsf_read_metadata(Decoder *decoder, InputStream &is, + DsfMetaData *metadata) +{ + DsfHeader dsf_header; + if (!dsdlib_read(decoder, is, &dsf_header, sizeof(dsf_header)) || + !dsf_header.id.Equals("DSD ")) + return false; + + const uint64_t chunk_size = dsf_header.size.Read(); + if (sizeof(dsf_header) != chunk_size) + return false; + +#ifdef HAVE_ID3TAG + const uint64_t metadata_offset = dsf_header.pmeta.Read(); +#endif + + /* read the 'fmt ' chunk of the DSF file */ + DsfFmtChunk dsf_fmt_chunk; + if (!dsdlib_read(decoder, is, &dsf_fmt_chunk, sizeof(dsf_fmt_chunk)) || + !dsf_fmt_chunk.id.Equals("fmt ")) + return false; + + const uint64_t fmt_chunk_size = dsf_fmt_chunk.size.Read(); + if (fmt_chunk_size != sizeof(dsf_fmt_chunk)) + return false; + + uint32_t samplefreq = FromLE32(dsf_fmt_chunk.sample_freq); + + /* for now, only support version 1 of the standard, DSD raw stereo + files with a sample freq of 2822400 or 5644800 Hz */ + + if (dsf_fmt_chunk.version != 1 || dsf_fmt_chunk.formatid != 0 + || dsf_fmt_chunk.channeltype != 2 + || dsf_fmt_chunk.channelnum != 2 + || (samplefreq != 2822400 && samplefreq != 5644800)) + return false; + + uint32_t chblksize = FromLE32(dsf_fmt_chunk.block_size); + /* according to the spec block size should always be 4096 */ + if (chblksize != 4096) + return false; + + /* read the 'data' chunk of the DSF file */ + DsfDataChunk data_chunk; + if (!dsdlib_read(decoder, is, &data_chunk, sizeof(data_chunk)) || + !data_chunk.id.Equals("data")) + return false; + + /* data size of DSF files are padded to multiple of 4096, + we use the actual data size as chunk size */ + + uint64_t data_size = data_chunk.size.Read(); + if (data_size < sizeof(data_chunk)) + return false; + + data_size -= sizeof(data_chunk); + + /* data_size cannot be bigger or equal to total file size */ + const uint64_t size = (uint64_t)is.GetSize(); + if (data_size >= size) + return false; + + /* use the sample count from the DSF header as the upper + bound, because some DSF files contain junk at the end of + the "data" chunk */ + const uint64_t samplecnt = dsf_fmt_chunk.scnt.Read(); + const uint64_t playable_size = samplecnt * 2 / 8; + if (data_size > playable_size) + data_size = playable_size; + + metadata->chunk_size = data_size; + metadata->channels = (unsigned) dsf_fmt_chunk.channelnum; + metadata->sample_rate = samplefreq; +#ifdef HAVE_ID3TAG + /* metada_offset cannot be bigger then or equal to total file size */ + if (metadata_offset >= size) + metadata->id3_offset = 0; + else + metadata->id3_offset = (InputStream::offset_type)metadata_offset; +#endif + /* check bits per sample format, determine if bitreverse is needed */ + metadata->bitreverse = dsf_fmt_chunk.bitssample == 1; + return true; +} + +static void +bit_reverse_buffer(uint8_t *p, uint8_t *end) +{ + for (; p < end; ++p) + *p = bit_reverse(*p); +} + +/** + * DSF data is build up of alternating 4096 blocks of DSD samples for left and + * right. Convert the buffer holding 1 block of 4096 DSD left samples and 1 + * block of 4096 DSD right samples to 8k of samples in normal PCM left/right + * order. + */ +static void +dsf_to_pcm_order(uint8_t *dest, uint8_t *scratch, size_t nrbytes) +{ + for (unsigned i = 0, j = 0; i < (unsigned)nrbytes; i += 2) { + scratch[i] = *(dest+j); + j++; + } + + for (unsigned i = 1, j = 0; i < (unsigned) nrbytes; i += 2) { + scratch[i] = *(dest+4096+j); + j++; + } + + for (unsigned i = 0; i < (unsigned)nrbytes; i++) { + *dest = scratch[i]; + dest++; + } +} + +/** + * Decode one complete DSF 'data' chunk i.e. a complete song + */ +static bool +dsf_decode_chunk(Decoder &decoder, InputStream &is, + unsigned channels, + uint64_t chunk_size, + bool bitreverse) +{ + uint8_t buffer[8192]; + + /* scratch buffer for DSF samples to convert to the needed + normal left/right regime of samples */ + uint8_t dsf_scratch_buffer[8192]; + + const size_t sample_size = sizeof(buffer[0]); + const size_t frame_size = channels * sample_size; + const unsigned buffer_frames = sizeof(buffer) / frame_size; + const unsigned buffer_samples = buffer_frames * frame_size; + const size_t buffer_size = buffer_samples * sample_size; + + while (chunk_size > 0) { + /* see how much aligned data from the remaining chunk + fits into the local buffer */ + size_t now_size = buffer_size; + if (chunk_size < (uint64_t)now_size) { + unsigned now_frames = + (unsigned)chunk_size / frame_size; + now_size = now_frames * frame_size; + } + + size_t nbytes = decoder_read(&decoder, is, buffer, now_size); + if (nbytes != now_size) + return false; + + chunk_size -= nbytes; + + if (bitreverse) + bit_reverse_buffer(buffer, buffer + nbytes); + + dsf_to_pcm_order(buffer, dsf_scratch_buffer, nbytes); + + const auto cmd = decoder_data(decoder, is, buffer, nbytes, 0); + switch (cmd) { + case DecoderCommand::NONE: + break; + + case DecoderCommand::START: + case DecoderCommand::STOP: + return false; + + case DecoderCommand::SEEK: + + /* not implemented yet */ + decoder_seek_error(decoder); + break; + } + } + return dsdlib_skip(&decoder, is, chunk_size); +} + +static void +dsf_stream_decode(Decoder &decoder, InputStream &is) +{ + /* check if it is a proper DSF file */ + DsfMetaData metadata; + if (!dsf_read_metadata(&decoder, is, &metadata)) + return; + + Error error; + AudioFormat audio_format; + if (!audio_format_init_checked(audio_format, metadata.sample_rate / 8, + SampleFormat::DSD, + metadata.channels, error)) { + LogError(error); + return; + } + /* Calculate song time from DSD chunk size and sample frequency */ + uint64_t chunk_size = metadata.chunk_size; + float songtime = ((chunk_size / metadata.channels) * 8) / + (float) metadata.sample_rate; + + /* success: file was recognized */ + decoder_initialized(decoder, audio_format, false, songtime); + + if (!dsf_decode_chunk(decoder, is, metadata.channels, + chunk_size, + metadata.bitreverse)) + return; +} + +static bool +dsf_scan_stream(InputStream &is, + gcc_unused const struct tag_handler *handler, + gcc_unused void *handler_ctx) +{ + /* check DSF metadata */ + DsfMetaData metadata; + if (!dsf_read_metadata(nullptr, is, &metadata)) + return false; + + AudioFormat audio_format; + if (!audio_format_init_checked(audio_format, metadata.sample_rate / 8, + SampleFormat::DSD, + metadata.channels, IgnoreError())) + /* refuse to parse files which we cannot play anyway */ + return false; + + /* calculate song time and add as tag */ + unsigned songtime = ((metadata.chunk_size / metadata.channels) * 8) / + metadata.sample_rate; + tag_handler_invoke_duration(handler, handler_ctx, songtime); + +#ifdef HAVE_ID3TAG + /* Add available tags from the ID3 tag */ + dsdlib_tag_id3(is, handler, handler_ctx, metadata.id3_offset); +#endif + return true; +} + +static const char *const dsf_suffixes[] = { + "dsf", + nullptr +}; + +static const char *const dsf_mime_types[] = { + "application/x-dsf", + nullptr +}; + +const struct DecoderPlugin dsf_decoder_plugin = { + "dsf", + nullptr, + nullptr, + dsf_stream_decode, + nullptr, + nullptr, + dsf_scan_stream, + nullptr, + dsf_suffixes, + dsf_mime_types, +}; diff --git a/src/decoder/plugins/DsfDecoderPlugin.hxx b/src/decoder/plugins/DsfDecoderPlugin.hxx new file mode 100644 index 000000000..02bea0b5c --- /dev/null +++ b/src/decoder/plugins/DsfDecoderPlugin.hxx @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_DECODER_DSF_H +#define MPD_DECODER_DSF_H + +extern const struct DecoderPlugin dsf_decoder_plugin; + +#endif diff --git a/src/decoder/plugins/FaadDecoderPlugin.cxx b/src/decoder/plugins/FaadDecoderPlugin.cxx new file mode 100644 index 000000000..79d6db9f7 --- /dev/null +++ b/src/decoder/plugins/FaadDecoderPlugin.cxx @@ -0,0 +1,464 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "FaadDecoderPlugin.hxx" +#include "../DecoderAPI.hxx" +#include "../DecoderBuffer.hxx" +#include "InputStream.hxx" +#include "CheckAudioFormat.hxx" +#include "tag/TagHandler.hxx" +#include "util/ConstBuffer.hxx" +#include "util/Error.hxx" +#include "util/Domain.hxx" +#include "Log.hxx" + +#include + +#include +#include +#include + +#define AAC_MAX_CHANNELS 6 + +static const unsigned adts_sample_rates[] = + { 96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, + 16000, 12000, 11025, 8000, 7350, 0, 0, 0 +}; + +static constexpr Domain faad_decoder_domain("faad_decoder"); + +/** + * Check whether the buffer head is an AAC frame, and return the frame + * length. Returns 0 if it is not a frame. + */ +static size_t +adts_check_frame(const unsigned char *data) +{ + /* check syncword */ + if (!((data[0] == 0xFF) && ((data[1] & 0xF6) == 0xF0))) + return 0; + + return (((unsigned int)data[3] & 0x3) << 11) | + (((unsigned int)data[4]) << 3) | + (data[5] >> 5); +} + +/** + * Find the next AAC frame in the buffer. Returns 0 if no frame is + * found or if not enough data is available. + */ +static size_t +adts_find_frame(DecoderBuffer *buffer) +{ + while (true) { + auto data = ConstBuffer::FromVoid(decoder_buffer_read(buffer)); + if (data.size < 8) { + /* not enough data yet */ + if (!decoder_buffer_fill(buffer)) + /* failed */ + return 0; + + continue; + } + + /* find the 0xff marker */ + const uint8_t *p = (const uint8_t *) + memchr(data.data, 0xff, data.size); + if (p == nullptr) { + /* no marker - discard the buffer */ + decoder_buffer_clear(buffer); + continue; + } + + if (p > data.data) { + /* discard data before 0xff */ + decoder_buffer_consume(buffer, p - data.data); + continue; + } + + /* is it a frame? */ + size_t frame_length = adts_check_frame(data.data); + if (frame_length == 0) { + /* it's just some random 0xff byte; discard it + and continue searching */ + decoder_buffer_consume(buffer, 1); + continue; + } + + if (data.size < frame_length) { + /* available buffer size is smaller than the + frame will be - attempt to read more + data */ + if (!decoder_buffer_fill(buffer)) { + /* not enough data; discard this frame + to prevent a possible buffer + overflow */ + decoder_buffer_clear(buffer); + } + + continue; + } + + /* found a full frame! */ + return frame_length; + } +} + +static float +adts_song_duration(DecoderBuffer *buffer) +{ + unsigned sample_rate = 0; + + /* Read all frames to ensure correct time and bitrate */ + unsigned frames = 0; + for (;; frames++) { + unsigned frame_length = adts_find_frame(buffer); + if (frame_length == 0) + break; + + if (frames == 0) { + auto data = ConstBuffer::FromVoid(decoder_buffer_read(buffer)); + assert(!data.IsEmpty()); + assert(frame_length <= data.size); + + sample_rate = adts_sample_rates[(data.data[2] & 0x3c) >> 2]; + } + + decoder_buffer_consume(buffer, frame_length); + } + + float frames_per_second = (float)sample_rate / 1024.0; + if (frames_per_second <= 0) + return -1; + + return (float)frames / frames_per_second; +} + +static float +faad_song_duration(DecoderBuffer *buffer, InputStream &is) +{ + const auto size = is.GetSize(); + const size_t fileread = size >= 0 ? size : 0; + + decoder_buffer_fill(buffer); + auto data = ConstBuffer::FromVoid(decoder_buffer_read(buffer)); + if (data.IsEmpty()) + return -1; + + size_t tagsize = 0; + if (data.size >= 10 && !memcmp(data.data, "ID3", 3)) { + /* skip the ID3 tag */ + + tagsize = (data.data[6] << 21) | (data.data[7] << 14) | + (data.data[8] << 7) | (data.data[9] << 0); + + tagsize += 10; + + bool success = decoder_buffer_skip(buffer, tagsize) && + decoder_buffer_fill(buffer); + if (!success) + return -1; + + data = ConstBuffer::FromVoid(decoder_buffer_read(buffer)); + if (data.IsEmpty()) + return -1; + } + + if (is.IsSeekable() && data.size >= 2 && + data.data[0] == 0xFF && ((data.data[1] & 0xF6) == 0xF0)) { + /* obtain the duration from the ADTS header */ + float song_length = adts_song_duration(buffer); + + is.LockSeek(tagsize, SEEK_SET, IgnoreError()); + + decoder_buffer_clear(buffer); + decoder_buffer_fill(buffer); + + return song_length; + } else if (data.size >= 5 && memcmp(data.data, "ADIF", 4) == 0) { + /* obtain the duration from the ADIF header */ + unsigned bit_rate; + size_t skip_size = (data.data[4] & 0x80) ? 9 : 0; + + if (8 + skip_size > data.size) + /* not enough data yet; skip parsing this + header */ + return -1; + + bit_rate = ((data.data[4 + skip_size] & 0x0F) << 19) | + (data.data[5 + skip_size] << 11) | + (data.data[6 + skip_size] << 3) | + (data.data[7 + skip_size] & 0xE0); + + if (fileread != 0 && bit_rate != 0) + return fileread * 8.0 / bit_rate; + else + return fileread; + } else + return -1; +} + +/** + * Wrapper for NeAACDecInit() which works around some API + * inconsistencies in libfaad. + */ +static bool +faad_decoder_init(NeAACDecHandle decoder, DecoderBuffer *buffer, + AudioFormat &audio_format, Error &error) +{ + uint32_t sample_rate; +#ifdef HAVE_FAAD_LONG + /* neaacdec.h declares all arguments as "unsigned long", but + internally expects uint32_t pointers. To avoid gcc + warnings, use this workaround. */ + unsigned long *sample_rate_p = (unsigned long *)(void *)&sample_rate; +#else + uint32_t *sample_rate_p = &sample_rate; +#endif + + auto data = ConstBuffer::FromVoid(decoder_buffer_read(buffer)); + if (data.IsEmpty()) { + error.Set(faad_decoder_domain, "Empty file"); + return false; + } + + uint8_t channels; + int32_t nbytes = NeAACDecInit(decoder, + /* deconst hack, libfaad requires this */ + const_cast(data.data), + data.size, + sample_rate_p, &channels); + if (nbytes < 0) { + error.Set(faad_decoder_domain, "Not an AAC stream"); + return false; + } + + decoder_buffer_consume(buffer, nbytes); + + return audio_format_init_checked(audio_format, sample_rate, + SampleFormat::S16, channels, error); +} + +/** + * Wrapper for NeAACDecDecode() which works around some API + * inconsistencies in libfaad. + */ +static const void * +faad_decoder_decode(NeAACDecHandle decoder, DecoderBuffer *buffer, + NeAACDecFrameInfo *frame_info) +{ + auto data = ConstBuffer::FromVoid(decoder_buffer_read(buffer)); + if (data.IsEmpty()) + return nullptr; + + return NeAACDecDecode(decoder, frame_info, + /* deconst hack, libfaad requires this */ + const_cast(data.data), + data.size); +} + +/** + * Get a song file's total playing time in seconds, as a float. + * Returns 0 if the duration is unknown, and a negative value if the + * file is invalid. + */ +static float +faad_get_file_time_float(InputStream &is) +{ + DecoderBuffer *buffer = + decoder_buffer_new(nullptr, is, + FAAD_MIN_STREAMSIZE * AAC_MAX_CHANNELS); + float length = faad_song_duration(buffer, is); + + if (length < 0) { + NeAACDecHandle decoder = NeAACDecOpen(); + + NeAACDecConfigurationPtr config = + NeAACDecGetCurrentConfiguration(decoder); + config->outputFormat = FAAD_FMT_16BIT; + NeAACDecSetConfiguration(decoder, config); + + decoder_buffer_fill(buffer); + + AudioFormat audio_format; + if (faad_decoder_init(decoder, buffer, audio_format, + IgnoreError())) + length = 0; + + NeAACDecClose(decoder); + } + + decoder_buffer_free(buffer); + + return length; +} + +/** + * Get a song file's total playing time in seconds, as an int. + * Returns 0 if the duration is unknown, and a negative value if the + * file is invalid. + */ +static int +faad_get_file_time(InputStream &is) +{ + int file_time = -1; + float length; + + if ((length = faad_get_file_time_float(is)) >= 0) + file_time = length + 0.5; + + return file_time; +} + +static void +faad_stream_decode(Decoder &mpd_decoder, InputStream &is) +{ + DecoderBuffer *buffer = + decoder_buffer_new(&mpd_decoder, is, + FAAD_MIN_STREAMSIZE * AAC_MAX_CHANNELS); + const float total_time = faad_song_duration(buffer, is); + + /* create the libfaad decoder */ + + NeAACDecHandle decoder = NeAACDecOpen(); + + NeAACDecConfigurationPtr config = + NeAACDecGetCurrentConfiguration(decoder); + config->outputFormat = FAAD_FMT_16BIT; + config->downMatrix = 1; + config->dontUpSampleImplicitSBR = 0; + NeAACDecSetConfiguration(decoder, config); + + while (!decoder_buffer_is_full(buffer) && !is.LockIsEOF() && + decoder_get_command(mpd_decoder) == DecoderCommand::NONE) { + adts_find_frame(buffer); + decoder_buffer_fill(buffer); + } + + /* initialize it */ + + Error error; + AudioFormat audio_format; + if (!faad_decoder_init(decoder, buffer, audio_format, error)) { + LogError(error); + NeAACDecClose(decoder); + decoder_buffer_free(buffer); + return; + } + + /* initialize the MPD core */ + + decoder_initialized(mpd_decoder, audio_format, false, total_time); + + /* the decoder loop */ + + DecoderCommand cmd; + unsigned bit_rate = 0; + do { + size_t frame_size; + const void *decoded; + NeAACDecFrameInfo frame_info; + + /* find the next frame */ + + frame_size = adts_find_frame(buffer); + if (frame_size == 0) + /* end of file */ + break; + + /* decode it */ + + decoded = faad_decoder_decode(decoder, buffer, &frame_info); + + if (frame_info.error > 0) { + FormatWarning(faad_decoder_domain, + "error decoding AAC stream: %s", + NeAACDecGetErrorMessage(frame_info.error)); + break; + } + + if (frame_info.channels != audio_format.channels) { + FormatDefault(faad_decoder_domain, + "channel count changed from %u to %u", + audio_format.channels, frame_info.channels); + break; + } + + if (frame_info.samplerate != audio_format.sample_rate) { + FormatDefault(faad_decoder_domain, + "sample rate changed from %u to %lu", + audio_format.sample_rate, + (unsigned long)frame_info.samplerate); + break; + } + + decoder_buffer_consume(buffer, frame_info.bytesconsumed); + + /* update bit rate and position */ + + if (frame_info.samples > 0) { + bit_rate = frame_info.bytesconsumed * 8.0 * + frame_info.channels * audio_format.sample_rate / + frame_info.samples / 1000 + 0.5; + } + + /* send PCM samples to MPD */ + + cmd = decoder_data(mpd_decoder, is, decoded, + (size_t)frame_info.samples * 2, + bit_rate); + } while (cmd != DecoderCommand::STOP); + + /* cleanup */ + + NeAACDecClose(decoder); + decoder_buffer_free(buffer); +} + +static bool +faad_scan_stream(InputStream &is, + const struct tag_handler *handler, void *handler_ctx) +{ + int file_time = faad_get_file_time(is); + + if (file_time < 0) + return false; + + tag_handler_invoke_duration(handler, handler_ctx, file_time); + return true; +} + +static const char *const faad_suffixes[] = { "aac", nullptr }; +static const char *const faad_mime_types[] = { + "audio/aac", "audio/aacp", nullptr +}; + +const DecoderPlugin faad_decoder_plugin = { + "faad", + nullptr, + nullptr, + faad_stream_decode, + nullptr, + nullptr, + faad_scan_stream, + nullptr, + faad_suffixes, + faad_mime_types, +}; diff --git a/src/decoder/plugins/FaadDecoderPlugin.hxx b/src/decoder/plugins/FaadDecoderPlugin.hxx new file mode 100644 index 000000000..968433e9b --- /dev/null +++ b/src/decoder/plugins/FaadDecoderPlugin.hxx @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_FAAD_DECODER_PLUGIN_HXX +#define MPD_FAAD_DECODER_PLUGIN_HXX + +extern const struct DecoderPlugin faad_decoder_plugin; + +#endif diff --git a/src/decoder/plugins/FfmpegDecoderPlugin.cxx b/src/decoder/plugins/FfmpegDecoderPlugin.cxx new file mode 100644 index 000000000..461ecb5e4 --- /dev/null +++ b/src/decoder/plugins/FfmpegDecoderPlugin.cxx @@ -0,0 +1,686 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +/* necessary because libavutil/common.h uses UINT64_C */ +#define __STDC_CONSTANT_MACROS + +#include "config.h" +#include "FfmpegDecoderPlugin.hxx" +#include "../DecoderAPI.hxx" +#include "FfmpegMetaData.hxx" +#include "tag/TagHandler.hxx" +#include "InputStream.hxx" +#include "CheckAudioFormat.hxx" +#include "util/Error.hxx" +#include "util/Domain.hxx" +#include "LogV.hxx" + +extern "C" { +#include +#include +#include +#include +#include +#include + +#if LIBAVUTIL_VERSION_MAJOR >= 53 +#include +#endif +} + +#include +#include + +static constexpr Domain ffmpeg_domain("ffmpeg"); + +/* suppress the ffmpeg compatibility macro */ +#ifdef SampleFormat +#undef SampleFormat +#endif + +static LogLevel +import_ffmpeg_level(int level) +{ + if (level <= AV_LOG_FATAL) + return LogLevel::ERROR; + + if (level <= AV_LOG_WARNING) + return LogLevel::WARNING; + + if (level <= AV_LOG_INFO) + return LogLevel::INFO; + + return LogLevel::DEBUG; +} + +static void +mpd_ffmpeg_log_callback(gcc_unused void *ptr, int level, + const char *fmt, va_list vl) +{ + const AVClass * cls = nullptr; + + if (ptr != nullptr) + cls = *(const AVClass *const*)ptr; + + if (cls != nullptr) { + char domain[64]; + snprintf(domain, sizeof(domain), "%s/%s", + ffmpeg_domain.GetName(), cls->item_name(ptr)); + const Domain d(domain); + LogFormatV(d, import_ffmpeg_level(level), fmt, vl); + } +} + +struct AvioStream { + Decoder *const decoder; + InputStream &input; + + AVIOContext *io; + + unsigned char buffer[8192]; + + AvioStream(Decoder *_decoder, InputStream &_input) + :decoder(_decoder), input(_input), io(nullptr) {} + + ~AvioStream() { + if (io != nullptr) + av_free(io); + } + + bool Open(); +}; + +static int +mpd_ffmpeg_stream_read(void *opaque, uint8_t *buf, int size) +{ + AvioStream *stream = (AvioStream *)opaque; + + return decoder_read(stream->decoder, stream->input, + (void *)buf, size); +} + +static int64_t +mpd_ffmpeg_stream_seek(void *opaque, int64_t pos, int whence) +{ + AvioStream *stream = (AvioStream *)opaque; + + if (whence == AVSEEK_SIZE) + return stream->input.size; + + if (!stream->input.LockSeek(pos, whence, IgnoreError())) + return -1; + + return stream->input.offset; +} + +bool +AvioStream::Open() +{ + io = avio_alloc_context(buffer, sizeof(buffer), + false, this, + mpd_ffmpeg_stream_read, nullptr, + input.seekable + ? mpd_ffmpeg_stream_seek : nullptr); + return io != nullptr; +} + +/** + * API compatibility wrapper for av_open_input_stream() and + * avformat_open_input(). + */ +static int +mpd_ffmpeg_open_input(AVFormatContext **ic_ptr, + AVIOContext *pb, + const char *filename, + AVInputFormat *fmt) +{ + AVFormatContext *context = avformat_alloc_context(); + if (context == nullptr) + return AVERROR(ENOMEM); + + context->pb = pb; + *ic_ptr = context; + return avformat_open_input(ic_ptr, filename, fmt, nullptr); +} + +static bool +ffmpeg_init(gcc_unused const config_param ¶m) +{ + av_log_set_callback(mpd_ffmpeg_log_callback); + + av_register_all(); + return true; +} + +static int +ffmpeg_find_audio_stream(const AVFormatContext *format_context) +{ + for (unsigned i = 0; i < format_context->nb_streams; ++i) + if (format_context->streams[i]->codec->codec_type == + AVMEDIA_TYPE_AUDIO) + return i; + + return -1; +} + +gcc_const +static double +time_from_ffmpeg(int64_t t, const AVRational time_base) +{ + assert(t != (int64_t)AV_NOPTS_VALUE); + + return (double)av_rescale_q(t, time_base, (AVRational){1, 1024}) + / (double)1024; +} + +gcc_const +static int64_t +time_to_ffmpeg(double t, const AVRational time_base) +{ + return av_rescale_q((int64_t)(t * 1024), (AVRational){1, 1024}, + time_base); +} + +static void +copy_interleave_frame2(uint8_t *dest, uint8_t **src, + unsigned nframes, unsigned nchannels, + unsigned sample_size) +{ + for (unsigned frame = 0; frame < nframes; ++frame) { + for (unsigned channel = 0; channel < nchannels; ++channel) { + memcpy(dest, src[channel] + frame * sample_size, + sample_size); + dest += sample_size; + } + } +} + +/** + * Copy PCM data from a AVFrame to an interleaved buffer. + */ +static int +copy_interleave_frame(const AVCodecContext *codec_context, + const AVFrame *frame, + uint8_t **output_buffer, + uint8_t **global_buffer, int *global_buffer_size) +{ + int plane_size; + const int data_size = + av_samples_get_buffer_size(&plane_size, + codec_context->channels, + frame->nb_samples, + codec_context->sample_fmt, 1); + if (data_size <= 0) + return data_size; + + if (av_sample_fmt_is_planar(codec_context->sample_fmt) && + codec_context->channels > 1) { + if(*global_buffer_size < data_size) { + av_freep(global_buffer); + + *global_buffer = (uint8_t*)av_malloc(data_size); + + if (!*global_buffer) + /* Not enough memory - shouldn't happen */ + return AVERROR(ENOMEM); + *global_buffer_size = data_size; + } + *output_buffer = *global_buffer; + copy_interleave_frame2(*output_buffer, frame->extended_data, + frame->nb_samples, + codec_context->channels, + av_get_bytes_per_sample(codec_context->sample_fmt)); + } else { + *output_buffer = frame->extended_data[0]; + } + + return data_size; +} + +static DecoderCommand +ffmpeg_send_packet(Decoder &decoder, InputStream &is, + const AVPacket *packet, + AVCodecContext *codec_context, + const AVStream *stream, + AVFrame *frame, + uint8_t **buffer, int *buffer_size) +{ + if (packet->pts >= 0 && packet->pts != (int64_t)AV_NOPTS_VALUE) + decoder_timestamp(decoder, + time_from_ffmpeg(packet->pts - stream->start_time, + stream->time_base)); + + AVPacket packet2 = *packet; + + uint8_t *output_buffer; + + DecoderCommand cmd = DecoderCommand::NONE; + while (packet2.size > 0 && cmd == DecoderCommand::NONE) { + int audio_size = 0; + int got_frame = 0; + int len = avcodec_decode_audio4(codec_context, + frame, &got_frame, + &packet2); + if (len >= 0 && got_frame) { + audio_size = copy_interleave_frame(codec_context, + frame, + &output_buffer, + buffer, buffer_size); + if (audio_size < 0) + len = audio_size; + } + + if (len < 0) { + /* if error, we skip the frame */ + LogDefault(ffmpeg_domain, + "decoding failed, frame skipped"); + break; + } + + packet2.data += len; + packet2.size -= len; + + if (audio_size <= 0) + continue; + + cmd = decoder_data(decoder, is, + output_buffer, audio_size, + codec_context->bit_rate / 1000); + } + return cmd; +} + +gcc_const +static SampleFormat +ffmpeg_sample_format(enum AVSampleFormat sample_fmt) +{ + switch (sample_fmt) { + case AV_SAMPLE_FMT_S16: + case AV_SAMPLE_FMT_S16P: + return SampleFormat::S16; + + case AV_SAMPLE_FMT_S32: + case AV_SAMPLE_FMT_S32P: + return SampleFormat::S32; + + case AV_SAMPLE_FMT_FLTP: + return SampleFormat::FLOAT; + + default: + break; + } + + char buffer[64]; + const char *name = av_get_sample_fmt_string(buffer, sizeof(buffer), + sample_fmt); + if (name != nullptr) + FormatError(ffmpeg_domain, + "Unsupported libavcodec SampleFormat value: %s (%d)", + name, sample_fmt); + else + FormatError(ffmpeg_domain, + "Unsupported libavcodec SampleFormat value: %d", + sample_fmt); + return SampleFormat::UNDEFINED; +} + +static AVInputFormat * +ffmpeg_probe(Decoder *decoder, InputStream &is) +{ + enum { + BUFFER_SIZE = 16384, + PADDING = 16, + }; + + unsigned char buffer[BUFFER_SIZE]; + size_t nbytes = decoder_read(decoder, is, buffer, BUFFER_SIZE); + if (nbytes <= PADDING || !is.LockRewind(IgnoreError())) + return nullptr; + + /* some ffmpeg parsers (e.g. ac3_parser.c) read a few bytes + beyond the declared buffer limit, which makes valgrind + angry; this workaround removes some padding from the buffer + size */ + nbytes -= PADDING; + + AVProbeData avpd; + avpd.buf = buffer; + avpd.buf_size = nbytes; + avpd.filename = is.uri.c_str(); + + return av_probe_input_format(&avpd, true); +} + +static void +ffmpeg_decode(Decoder &decoder, InputStream &input) +{ + AVInputFormat *input_format = ffmpeg_probe(&decoder, input); + if (input_format == nullptr) + return; + + FormatDebug(ffmpeg_domain, "detected input format '%s' (%s)", + input_format->name, input_format->long_name); + + AvioStream stream(&decoder, input); + if (!stream.Open()) { + LogError(ffmpeg_domain, "Failed to open stream"); + return; + } + + //ffmpeg works with ours "fileops" helper + AVFormatContext *format_context = nullptr; + if (mpd_ffmpeg_open_input(&format_context, stream.io, + input.uri.c_str(), + input_format) != 0) { + LogError(ffmpeg_domain, "Open failed"); + return; + } + + const int find_result = + avformat_find_stream_info(format_context, nullptr); + if (find_result < 0) { + LogError(ffmpeg_domain, "Couldn't find stream info"); + avformat_close_input(&format_context); + return; + } + + int audio_stream = ffmpeg_find_audio_stream(format_context); + if (audio_stream == -1) { + LogError(ffmpeg_domain, "No audio stream inside"); + avformat_close_input(&format_context); + return; + } + + AVStream *av_stream = format_context->streams[audio_stream]; + + AVCodecContext *codec_context = av_stream->codec; + if (codec_context->codec_name[0] != 0) + FormatDebug(ffmpeg_domain, "codec '%s'", + codec_context->codec_name); + + AVCodec *codec = avcodec_find_decoder(codec_context->codec_id); + + if (!codec) { + LogError(ffmpeg_domain, "Unsupported audio codec"); + avformat_close_input(&format_context); + return; + } + + const SampleFormat sample_format = + ffmpeg_sample_format(codec_context->sample_fmt); + if (sample_format == SampleFormat::UNDEFINED) + return; + + Error error; + AudioFormat audio_format; + if (!audio_format_init_checked(audio_format, + codec_context->sample_rate, + sample_format, + codec_context->channels, error)) { + LogError(error); + avformat_close_input(&format_context); + return; + } + + /* the audio format must be read from AVCodecContext by now, + because avcodec_open() has been demonstrated to fill bogus + values into AVCodecContext.channels - a change that will be + reverted later by avcodec_decode_audio3() */ + + const int open_result = avcodec_open2(codec_context, codec, nullptr); + if (open_result < 0) { + LogError(ffmpeg_domain, "Could not open codec"); + avformat_close_input(&format_context); + return; + } + + int total_time = format_context->duration != (int64_t)AV_NOPTS_VALUE + ? format_context->duration / AV_TIME_BASE + : 0; + + decoder_initialized(decoder, audio_format, + input.seekable, total_time); + +#if LIBAVUTIL_VERSION_MAJOR >= 53 + AVFrame *frame = av_frame_alloc(); +#else + AVFrame *frame = avcodec_alloc_frame(); +#endif + if (!frame) { + LogError(ffmpeg_domain, "Could not allocate frame"); + avformat_close_input(&format_context); + return; + } + + uint8_t *interleaved_buffer = nullptr; + int interleaved_buffer_size = 0; + + DecoderCommand cmd; + do { + AVPacket packet; + if (av_read_frame(format_context, &packet) < 0) + /* end of file */ + break; + + if (packet.stream_index == audio_stream) + cmd = ffmpeg_send_packet(decoder, input, + &packet, codec_context, + av_stream, + frame, + &interleaved_buffer, &interleaved_buffer_size); + else + cmd = decoder_get_command(decoder); + + av_free_packet(&packet); + + if (cmd == DecoderCommand::SEEK) { + int64_t where = + time_to_ffmpeg(decoder_seek_where(decoder), + av_stream->time_base) + + av_stream->start_time; + + if (av_seek_frame(format_context, audio_stream, where, + AV_TIME_BASE) < 0) + decoder_seek_error(decoder); + else { + avcodec_flush_buffers(codec_context); + decoder_command_finished(decoder); + } + } + } while (cmd != DecoderCommand::STOP); + +#if LIBAVUTIL_VERSION_MAJOR >= 53 + av_frame_free(&frame); +#elif LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(54, 28, 0) + avcodec_free_frame(&frame); +#else + av_freep(&frame); +#endif + av_freep(&interleaved_buffer); + + avcodec_close(codec_context); + avformat_close_input(&format_context); +} + +//no tag reading in ffmpeg, check if playable +static bool +ffmpeg_scan_stream(InputStream &is, + const struct tag_handler *handler, void *handler_ctx) +{ + AVInputFormat *input_format = ffmpeg_probe(nullptr, is); + if (input_format == nullptr) + return false; + + AvioStream stream(nullptr, is); + if (!stream.Open()) + return false; + + AVFormatContext *f = nullptr; + if (mpd_ffmpeg_open_input(&f, stream.io, is.uri.c_str(), + input_format) != 0) + return false; + + const int find_result = + avformat_find_stream_info(f, nullptr); + if (find_result < 0) { + avformat_close_input(&f); + return false; + } + + if (f->duration != (int64_t)AV_NOPTS_VALUE) + tag_handler_invoke_duration(handler, handler_ctx, + f->duration / AV_TIME_BASE); + + ffmpeg_scan_dictionary(f->metadata, handler, handler_ctx); + int idx = ffmpeg_find_audio_stream(f); + if (idx >= 0) + ffmpeg_scan_dictionary(f->streams[idx]->metadata, + handler, handler_ctx); + + avformat_close_input(&f); + return true; +} + +/** + * A list of extensions found for the formats supported by ffmpeg. + * This list is current as of 02-23-09; To find out if there are more + * supported formats, check the ffmpeg changelog since this date for + * more formats. + */ +static const char *const ffmpeg_suffixes[] = { + "16sv", "3g2", "3gp", "4xm", "8svx", "aa3", "aac", "ac3", "afc", "aif", + "aifc", "aiff", "al", "alaw", "amr", "anim", "apc", "ape", "asf", + "atrac", "au", "aud", "avi", "avm2", "avs", "bap", "bfi", "c93", "cak", + "cin", "cmv", "cpk", "daud", "dct", "divx", "dts", "dv", "dvd", "dxa", + "eac3", "film", "flac", "flc", "fli", "fll", "flx", "flv", "g726", + "gsm", "gxf", "iss", "m1v", "m2v", "m2t", "m2ts", + "m4a", "m4b", "m4v", + "mad", + "mj2", "mjpeg", "mjpg", "mka", "mkv", "mlp", "mm", "mmf", "mov", "mp+", + "mp1", "mp2", "mp3", "mp4", "mpc", "mpeg", "mpg", "mpga", "mpp", "mpu", + "mve", "mvi", "mxf", "nc", "nsv", "nut", "nuv", "oga", "ogm", "ogv", + "ogx", "oma", "ogg", "omg", "psp", "pva", "qcp", "qt", "r3d", "ra", + "ram", "rl2", "rm", "rmvb", "roq", "rpl", "rvc", "shn", "smk", "snd", + "sol", "son", "spx", "str", "swf", "tgi", "tgq", "tgv", "thp", "ts", + "tsp", "tta", "xa", "xvid", "uv", "uv2", "vb", "vid", "vob", "voc", + "vp6", "vmd", "wav", "webm", "wma", "wmv", "wsaud", "wsvga", "wv", + "wve", + nullptr +}; + +static const char *const ffmpeg_mime_types[] = { + "application/flv", + "application/m4a", + "application/mp4", + "application/octet-stream", + "application/ogg", + "application/x-ms-wmz", + "application/x-ms-wmd", + "application/x-ogg", + "application/x-shockwave-flash", + "application/x-shorten", + "audio/8svx", + "audio/16sv", + "audio/aac", + "audio/ac3", + "audio/aiff" + "audio/amr", + "audio/basic", + "audio/flac", + "audio/m4a", + "audio/mp4", + "audio/mpeg", + "audio/musepack", + "audio/ogg", + "audio/qcelp", + "audio/vorbis", + "audio/vorbis+ogg", + "audio/x-8svx", + "audio/x-16sv", + "audio/x-aac", + "audio/x-ac3", + "audio/x-aiff" + "audio/x-alaw", + "audio/x-au", + "audio/x-dca", + "audio/x-eac3", + "audio/x-flac", + "audio/x-gsm", + "audio/x-mace", + "audio/x-matroska", + "audio/x-monkeys-audio", + "audio/x-mpeg", + "audio/x-ms-wma", + "audio/x-ms-wax", + "audio/x-musepack", + "audio/x-ogg", + "audio/x-vorbis", + "audio/x-vorbis+ogg", + "audio/x-pn-realaudio", + "audio/x-pn-multirate-realaudio", + "audio/x-speex", + "audio/x-tta" + "audio/x-voc", + "audio/x-wav", + "audio/x-wma", + "audio/x-wv", + "video/anim", + "video/quicktime", + "video/msvideo", + "video/ogg", + "video/theora", + "video/webm", + "video/x-dv", + "video/x-flv", + "video/x-matroska", + "video/x-mjpeg", + "video/x-mpeg", + "video/x-ms-asf", + "video/x-msvideo", + "video/x-ms-wmv", + "video/x-ms-wvx", + "video/x-ms-wm", + "video/x-ms-wmx", + "video/x-nut", + "video/x-pva", + "video/x-theora", + "video/x-vid", + "video/x-wmv", + "video/x-xvid", + + /* special value for the "ffmpeg" input plugin: all streams by + the "ffmpeg" input plugin shall be decoded by this + plugin */ + "audio/x-mpd-ffmpeg", + + nullptr +}; + +const struct DecoderPlugin ffmpeg_decoder_plugin = { + "ffmpeg", + ffmpeg_init, + nullptr, + ffmpeg_decode, + nullptr, + nullptr, + ffmpeg_scan_stream, + nullptr, + ffmpeg_suffixes, + ffmpeg_mime_types +}; diff --git a/src/decoder/plugins/FfmpegDecoderPlugin.hxx b/src/decoder/plugins/FfmpegDecoderPlugin.hxx new file mode 100644 index 000000000..0a3e78e4b --- /dev/null +++ b/src/decoder/plugins/FfmpegDecoderPlugin.hxx @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_DECODER_FFMPEG_HXX +#define MPD_DECODER_FFMPEG_HXX + +extern const struct DecoderPlugin ffmpeg_decoder_plugin; + +#endif diff --git a/src/decoder/plugins/FfmpegMetaData.cxx b/src/decoder/plugins/FfmpegMetaData.cxx new file mode 100644 index 000000000..a39466945 --- /dev/null +++ b/src/decoder/plugins/FfmpegMetaData.cxx @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +/* necessary because libavutil/common.h uses UINT64_C */ +#define __STDC_CONSTANT_MACROS + +#include "config.h" +#include "FfmpegMetaData.hxx" +#include "tag/TagTable.hxx" +#include "tag/TagHandler.hxx" + +static const struct tag_table ffmpeg_tags[] = { + { "year", TAG_DATE }, + { "author-sort", TAG_ARTIST_SORT }, + { "album_artist", TAG_ALBUM_ARTIST }, + { "album_artist-sort", TAG_ALBUM_ARTIST_SORT }, + + /* sentinel */ + { nullptr, TAG_NUM_OF_ITEM_TYPES } +}; + +static void +ffmpeg_copy_metadata(TagType type, + AVDictionary *m, const char *name, + const struct tag_handler *handler, void *handler_ctx) +{ + AVDictionaryEntry *mt = nullptr; + + while ((mt = av_dict_get(m, name, mt, 0)) != nullptr) + tag_handler_invoke_tag(handler, handler_ctx, + type, mt->value); +} + +static void +ffmpeg_scan_pairs(AVDictionary *dict, + const struct tag_handler *handler, void *handler_ctx) +{ + AVDictionaryEntry *i = nullptr; + + while ((i = av_dict_get(dict, "", i, AV_DICT_IGNORE_SUFFIX)) != nullptr) + tag_handler_invoke_pair(handler, handler_ctx, + i->key, i->value); +} + +void +ffmpeg_scan_dictionary(AVDictionary *dict, + const struct tag_handler *handler, void *handler_ctx) +{ + for (unsigned i = 0; i < TAG_NUM_OF_ITEM_TYPES; ++i) + ffmpeg_copy_metadata(TagType(i), dict, tag_item_names[i], + handler, handler_ctx); + + for (const struct tag_table *i = ffmpeg_tags; + i->name != nullptr; ++i) + ffmpeg_copy_metadata(i->type, dict, i->name, + handler, handler_ctx); + + if (handler->pair != nullptr) + ffmpeg_scan_pairs(dict, handler, handler_ctx); +} diff --git a/src/decoder/plugins/FfmpegMetaData.hxx b/src/decoder/plugins/FfmpegMetaData.hxx new file mode 100644 index 000000000..5eb41db68 --- /dev/null +++ b/src/decoder/plugins/FfmpegMetaData.hxx @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_FFMPEG_METADATA_HXX +#define MPD_FFMPEG_METADATA_HXX + +extern "C" { +#include +} + +/* suppress the ffmpeg compatibility macro */ +#ifdef SampleFormat +#undef SampleFormat +#endif + +struct tag_handler; + +void +ffmpeg_scan_dictionary(AVDictionary *dict, + const tag_handler *handler, void *handler_ctx); + +#endif diff --git a/src/decoder/plugins/FlacCommon.cxx b/src/decoder/plugins/FlacCommon.cxx new file mode 100644 index 000000000..7b67585a0 --- /dev/null +++ b/src/decoder/plugins/FlacCommon.cxx @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +/* + * Common data structures and functions used by FLAC and OggFLAC + */ + +#include "config.h" +#include "FlacCommon.hxx" +#include "FlacMetadata.hxx" +#include "FlacPcm.hxx" +#include "CheckAudioFormat.hxx" +#include "util/Error.hxx" +#include "Log.hxx" + +flac_data::flac_data(Decoder &_decoder, + InputStream &_input_stream) + :FlacInput(_input_stream, &_decoder), + initialized(false), unsupported(false), + total_frames(0), first_frame(0), next_frame(0), position(0), + decoder(_decoder), input_stream(_input_stream) +{ +} + +static SampleFormat +flac_sample_format(unsigned bits_per_sample) +{ + switch (bits_per_sample) { + case 8: + return SampleFormat::S8; + + case 16: + return SampleFormat::S16; + + case 24: + return SampleFormat::S24_P32; + + case 32: + return SampleFormat::S32; + + default: + return SampleFormat::UNDEFINED; + } +} + +static void +flac_got_stream_info(struct flac_data *data, + const FLAC__StreamMetadata_StreamInfo *stream_info) +{ + if (data->initialized || data->unsupported) + return; + + Error error; + if (!audio_format_init_checked(data->audio_format, + stream_info->sample_rate, + flac_sample_format(stream_info->bits_per_sample), + stream_info->channels, error)) { + LogError(error); + data->unsupported = true; + return; + } + + data->frame_size = data->audio_format.GetFrameSize(); + + if (data->total_frames == 0) + data->total_frames = stream_info->total_samples; + + data->initialized = true; +} + +void flac_metadata_common_cb(const FLAC__StreamMetadata * block, + struct flac_data *data) +{ + if (data->unsupported) + return; + + ReplayGainInfo rgi; + + switch (block->type) { + case FLAC__METADATA_TYPE_STREAMINFO: + flac_got_stream_info(data, &block->data.stream_info); + break; + + case FLAC__METADATA_TYPE_VORBIS_COMMENT: + if (flac_parse_replay_gain(rgi, block)) + decoder_replay_gain(data->decoder, &rgi); + + decoder_mixramp(data->decoder, flac_parse_mixramp(block)); + + data->tag = flac_vorbis_comments_to_tag(&block->data.vorbis_comment); + break; + + default: + break; + } +} + +/** + * This function attempts to call decoder_initialized() in case there + * was no STREAMINFO block. This is allowed for nonseekable streams, + * where the server sends us only a part of the file, without + * providing the STREAMINFO block from the beginning of the file + * (e.g. when seeking with SqueezeBox Server). + */ +static bool +flac_got_first_frame(struct flac_data *data, const FLAC__FrameHeader *header) +{ + if (data->unsupported) + return false; + + Error error; + if (!audio_format_init_checked(data->audio_format, + header->sample_rate, + flac_sample_format(header->bits_per_sample), + header->channels, error)) { + LogError(error); + data->unsupported = true; + return false; + } + + data->frame_size = data->audio_format.GetFrameSize(); + + decoder_initialized(data->decoder, data->audio_format, + data->input_stream.seekable, + (float)data->total_frames / + (float)data->audio_format.sample_rate); + + data->initialized = true; + + return true; +} + +FLAC__StreamDecoderWriteStatus +flac_common_write(struct flac_data *data, const FLAC__Frame * frame, + const FLAC__int32 *const buf[], + FLAC__uint64 nbytes) +{ + void *buffer; + unsigned bit_rate; + + if (!data->initialized && !flac_got_first_frame(data, &frame->header)) + return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; + + size_t buffer_size = frame->header.blocksize * data->frame_size; + buffer = data->buffer.Get(buffer_size); + + flac_convert(buffer, frame->header.channels, + data->audio_format.format, buf, + 0, frame->header.blocksize); + + if (nbytes > 0) + bit_rate = nbytes * 8 * frame->header.sample_rate / + (1000 * frame->header.blocksize); + else + bit_rate = 0; + + auto cmd = decoder_data(data->decoder, data->input_stream, + buffer, buffer_size, + bit_rate); + data->next_frame += frame->header.blocksize; + switch (cmd) { + case DecoderCommand::NONE: + case DecoderCommand::START: + break; + + case DecoderCommand::STOP: + return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; + + case DecoderCommand::SEEK: + return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE; + } + + return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE; +} diff --git a/src/decoder/plugins/FlacCommon.hxx b/src/decoder/plugins/FlacCommon.hxx new file mode 100644 index 000000000..34ce0a3fc --- /dev/null +++ b/src/decoder/plugins/FlacCommon.hxx @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +/* + * Common data structures and functions used by FLAC and OggFLAC + */ + +#ifndef MPD_FLAC_COMMON_HXX +#define MPD_FLAC_COMMON_HXX + +#include "FlacInput.hxx" +#include "../DecoderAPI.hxx" +#include "pcm/PcmBuffer.hxx" + +#include + +struct flac_data : public FlacInput { + PcmBuffer buffer; + + /** + * The size of one frame in the output buffer. + */ + unsigned frame_size; + + /** + * Has decoder_initialized() been called yet? + */ + bool initialized; + + /** + * Does the FLAC file contain an unsupported audio format? + */ + bool unsupported; + + /** + * The validated audio format of the FLAC file. This + * attribute is defined if "initialized" is true. + */ + AudioFormat audio_format; + + /** + * The total number of frames in this song. The decoder + * plugin may initialize this attribute to override the value + * provided by libFLAC (e.g. for sub songs from a CUE sheet). + */ + FLAC__uint64 total_frames; + + /** + * The number of the first frame in this song. This is only + * non-zero if playing sub songs from a CUE sheet. + */ + FLAC__uint64 first_frame; + + /** + * The number of the next frame which is going to be decoded. + */ + FLAC__uint64 next_frame; + + FLAC__uint64 position; + + Decoder &decoder; + InputStream &input_stream; + + Tag tag; + + flac_data(Decoder &decoder, InputStream &input_stream); +}; + +void flac_metadata_common_cb(const FLAC__StreamMetadata * block, + struct flac_data *data); + +FLAC__StreamDecoderWriteStatus +flac_common_write(struct flac_data *data, const FLAC__Frame * frame, + const FLAC__int32 *const buf[], + FLAC__uint64 nbytes); + +#endif /* _FLAC_COMMON_H */ diff --git a/src/decoder/plugins/FlacDecoderPlugin.cxx b/src/decoder/plugins/FlacDecoderPlugin.cxx new file mode 100644 index 000000000..17949f8c5 --- /dev/null +++ b/src/decoder/plugins/FlacDecoderPlugin.cxx @@ -0,0 +1,383 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" /* must be first for large file support */ +#include "FlacDecoderPlugin.h" +#include "FlacDomain.hxx" +#include "FlacCommon.hxx" +#include "FlacMetadata.hxx" +#include "OggCodec.hxx" +#include "util/Error.hxx" +#include "Log.hxx" + +#if !defined(FLAC_API_VERSION_CURRENT) || FLAC_API_VERSION_CURRENT <= 7 +#error libFLAC is too old +#endif + +static void flacPrintErroredState(FLAC__StreamDecoderState state) +{ + switch (state) { + case FLAC__STREAM_DECODER_SEARCH_FOR_METADATA: + case FLAC__STREAM_DECODER_READ_METADATA: + case FLAC__STREAM_DECODER_SEARCH_FOR_FRAME_SYNC: + case FLAC__STREAM_DECODER_READ_FRAME: + case FLAC__STREAM_DECODER_END_OF_STREAM: + return; + + case FLAC__STREAM_DECODER_OGG_ERROR: + case FLAC__STREAM_DECODER_SEEK_ERROR: + case FLAC__STREAM_DECODER_ABORTED: + case FLAC__STREAM_DECODER_MEMORY_ALLOCATION_ERROR: + case FLAC__STREAM_DECODER_UNINITIALIZED: + break; + } + + LogError(flac_domain, FLAC__StreamDecoderStateString[state]); +} + +static void flacMetadata(gcc_unused const FLAC__StreamDecoder * dec, + const FLAC__StreamMetadata * block, void *vdata) +{ + flac_metadata_common_cb(block, (struct flac_data *) vdata); +} + +static FLAC__StreamDecoderWriteStatus +flac_write_cb(const FLAC__StreamDecoder *dec, const FLAC__Frame *frame, + const FLAC__int32 *const buf[], void *vdata) +{ + struct flac_data *data = (struct flac_data *) vdata; + FLAC__uint64 nbytes = 0; + + if (FLAC__stream_decoder_get_decode_position(dec, &nbytes)) { + if (data->position > 0 && nbytes > data->position) { + nbytes -= data->position; + data->position += nbytes; + } else { + data->position = nbytes; + nbytes = 0; + } + } else + nbytes = 0; + + return flac_common_write(data, frame, buf, nbytes); +} + +static bool +flac_scan_file(const char *file, + const struct tag_handler *handler, void *handler_ctx) +{ + FlacMetadataChain chain; + if (!chain.Read(file)) { + FormatDebug(flac_domain, + "Failed to read FLAC tags: %s", + chain.GetStatusString()); + return false; + } + + chain.Scan(handler, handler_ctx); + return true; +} + +static bool +flac_scan_stream(InputStream &is, + const struct tag_handler *handler, void *handler_ctx) +{ + FlacMetadataChain chain; + if (!chain.Read(is)) { + FormatDebug(flac_domain, + "Failed to read FLAC tags: %s", + chain.GetStatusString()); + return false; + } + + chain.Scan(handler, handler_ctx); + return true; +} + +/** + * Some glue code around FLAC__stream_decoder_new(). + */ +static FLAC__StreamDecoder * +flac_decoder_new(void) +{ + FLAC__StreamDecoder *sd = FLAC__stream_decoder_new(); + if (sd == nullptr) { + LogError(flac_domain, + "FLAC__stream_decoder_new() failed"); + return nullptr; + } + + if(!FLAC__stream_decoder_set_metadata_respond(sd, FLAC__METADATA_TYPE_VORBIS_COMMENT)) + LogDebug(flac_domain, + "FLAC__stream_decoder_set_metadata_respond() has failed"); + + return sd; +} + +static bool +flac_decoder_initialize(struct flac_data *data, FLAC__StreamDecoder *sd, + FLAC__uint64 duration) +{ + data->total_frames = duration; + + if (!FLAC__stream_decoder_process_until_end_of_metadata(sd)) { + LogWarning(flac_domain, "problem reading metadata"); + return false; + } + + if (data->initialized) { + /* done */ + decoder_initialized(data->decoder, data->audio_format, + data->input_stream.seekable, + (float)data->total_frames / + (float)data->audio_format.sample_rate); + return true; + } + + if (data->input_stream.seekable) + /* allow the workaround below only for nonseekable + streams*/ + return false; + + /* no stream_info packet found; try to initialize the decoder + from the first frame header */ + FLAC__stream_decoder_process_single(sd); + return data->initialized; +} + +static void +flac_decoder_loop(struct flac_data *data, FLAC__StreamDecoder *flac_dec, + FLAC__uint64 t_start, FLAC__uint64 t_end) +{ + Decoder &decoder = data->decoder; + + data->first_frame = t_start; + + while (true) { + DecoderCommand cmd; + if (!data->tag.IsEmpty()) { + cmd = decoder_tag(data->decoder, data->input_stream, + std::move(data->tag)); + data->tag.Clear(); + } else + cmd = decoder_get_command(decoder); + + if (cmd == DecoderCommand::SEEK) { + FLAC__uint64 seek_sample = t_start + + decoder_seek_where(decoder) * + data->audio_format.sample_rate; + if (seek_sample >= t_start && + (t_end == 0 || seek_sample <= t_end) && + FLAC__stream_decoder_seek_absolute(flac_dec, seek_sample)) { + data->next_frame = seek_sample; + data->position = 0; + decoder_command_finished(decoder); + } else + decoder_seek_error(decoder); + } else if (cmd == DecoderCommand::STOP || + FLAC__stream_decoder_get_state(flac_dec) == FLAC__STREAM_DECODER_END_OF_STREAM) + break; + + if (t_end != 0 && data->next_frame >= t_end) + /* end of this sub track */ + break; + + if (!FLAC__stream_decoder_process_single(flac_dec) && + decoder_get_command(decoder) == DecoderCommand::NONE) { + /* a failure that was not triggered by a + decoder command */ + flacPrintErroredState(FLAC__stream_decoder_get_state(flac_dec)); + break; + } + } +} + +static FLAC__StreamDecoderInitStatus +stream_init_oggflac(FLAC__StreamDecoder *flac_dec, struct flac_data *data) +{ + return FLAC__stream_decoder_init_ogg_stream(flac_dec, + FlacInput::Read, + FlacInput::Seek, + FlacInput::Tell, + FlacInput::Length, + FlacInput::Eof, + flac_write_cb, + flacMetadata, + FlacInput::Error, + data); +} + +static FLAC__StreamDecoderInitStatus +stream_init_flac(FLAC__StreamDecoder *flac_dec, struct flac_data *data) +{ + return FLAC__stream_decoder_init_stream(flac_dec, + FlacInput::Read, + FlacInput::Seek, + FlacInput::Tell, + FlacInput::Length, + FlacInput::Eof, + flac_write_cb, + flacMetadata, + FlacInput::Error, + data); +} + +static FLAC__StreamDecoderInitStatus +stream_init(FLAC__StreamDecoder *flac_dec, struct flac_data *data, bool is_ogg) +{ + return is_ogg + ? stream_init_oggflac(flac_dec, data) + : stream_init_flac(flac_dec, data); +} + +static void +flac_decode_internal(Decoder &decoder, + InputStream &input_stream, + bool is_ogg) +{ + FLAC__StreamDecoder *flac_dec; + + flac_dec = flac_decoder_new(); + if (flac_dec == nullptr) + return; + + struct flac_data data(decoder, input_stream); + + FLAC__StreamDecoderInitStatus status = + stream_init(flac_dec, &data, is_ogg); + if (status != FLAC__STREAM_DECODER_INIT_STATUS_OK) { + FLAC__stream_decoder_delete(flac_dec); + LogWarning(flac_domain, + FLAC__StreamDecoderInitStatusString[status]); + return; + } + + if (!flac_decoder_initialize(&data, flac_dec, 0)) { + FLAC__stream_decoder_finish(flac_dec); + FLAC__stream_decoder_delete(flac_dec); + return; + } + + flac_decoder_loop(&data, flac_dec, 0, 0); + + FLAC__stream_decoder_finish(flac_dec); + FLAC__stream_decoder_delete(flac_dec); +} + +static void +flac_decode(Decoder &decoder, InputStream &input_stream) +{ + flac_decode_internal(decoder, input_stream, false); +} + +static bool +oggflac_init(gcc_unused const config_param ¶m) +{ + return !!FLAC_API_SUPPORTS_OGG_FLAC; +} + +static bool +oggflac_scan_file(const char *file, + const struct tag_handler *handler, void *handler_ctx) +{ + FlacMetadataChain chain; + if (!chain.ReadOgg(file)) { + FormatDebug(flac_domain, + "Failed to read OggFLAC tags: %s", + chain.GetStatusString()); + return false; + } + + chain.Scan(handler, handler_ctx); + return true; +} + +static bool +oggflac_scan_stream(InputStream &is, + const struct tag_handler *handler, void *handler_ctx) +{ + FlacMetadataChain chain; + if (!chain.ReadOgg(is)) { + FormatDebug(flac_domain, + "Failed to read OggFLAC tags: %s", + chain.GetStatusString()); + return false; + } + + chain.Scan(handler, handler_ctx); + return true; +} + +static void +oggflac_decode(Decoder &decoder, InputStream &input_stream) +{ + if (ogg_codec_detect(&decoder, input_stream) != OGG_CODEC_FLAC) + return; + + /* rewind the stream, because ogg_codec_detect() has + moved it */ + input_stream.LockRewind(IgnoreError()); + + flac_decode_internal(decoder, input_stream, true); +} + +static const char *const oggflac_suffixes[] = { "ogg", "oga", nullptr }; +static const char *const oggflac_mime_types[] = { + "application/ogg", + "application/x-ogg", + "audio/ogg", + "audio/x-flac+ogg", + "audio/x-ogg", + nullptr +}; + +const struct DecoderPlugin oggflac_decoder_plugin = { + "oggflac", + oggflac_init, + nullptr, + oggflac_decode, + nullptr, + oggflac_scan_file, + oggflac_scan_stream, + nullptr, + oggflac_suffixes, + oggflac_mime_types, +}; + +static const char *const flac_suffixes[] = { "flac", nullptr }; +static const char *const flac_mime_types[] = { + "application/flac", + "application/x-flac", + "audio/flac", + "audio/x-flac", + nullptr +}; + +const struct DecoderPlugin flac_decoder_plugin = { + "flac", + nullptr, + nullptr, + flac_decode, + nullptr, + flac_scan_file, + flac_scan_stream, + nullptr, + flac_suffixes, + flac_mime_types, +}; diff --git a/src/decoder/plugins/FlacDecoderPlugin.h b/src/decoder/plugins/FlacDecoderPlugin.h new file mode 100644 index 000000000..fcdecf869 --- /dev/null +++ b/src/decoder/plugins/FlacDecoderPlugin.h @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_DECODER_FLAC_H +#define MPD_DECODER_FLAC_H + +extern const struct DecoderPlugin flac_decoder_plugin; +extern const struct DecoderPlugin oggflac_decoder_plugin; + +#endif diff --git a/src/decoder/plugins/FlacDomain.cxx b/src/decoder/plugins/FlacDomain.cxx new file mode 100644 index 000000000..fc5cc5498 --- /dev/null +++ b/src/decoder/plugins/FlacDomain.cxx @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "FlacDomain.hxx" +#include "util/Domain.hxx" + +const Domain flac_domain("flac"); diff --git a/src/decoder/plugins/FlacDomain.hxx b/src/decoder/plugins/FlacDomain.hxx new file mode 100644 index 000000000..a06c6c6b4 --- /dev/null +++ b/src/decoder/plugins/FlacDomain.hxx @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_FLAC_DOMAIN_HXX +#define MPD_FLAC_DOMAIN_HXX + +#include "check.h" + +extern const class Domain flac_domain; + +#endif diff --git a/src/decoder/plugins/FlacIOHandle.cxx b/src/decoder/plugins/FlacIOHandle.cxx new file mode 100644 index 000000000..aecee638b --- /dev/null +++ b/src/decoder/plugins/FlacIOHandle.cxx @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "FlacIOHandle.hxx" +#include "util/Error.hxx" +#include "Compiler.h" + +#include + +static size_t +FlacIORead(void *ptr, size_t size, size_t nmemb, FLAC__IOHandle handle) +{ + InputStream *is = (InputStream *)handle; + + uint8_t *const p0 = (uint8_t *)ptr, *p = p0, + *const end = p0 + size * nmemb; + + /* libFLAC is very picky about short reads, and expects the IO + callback to fill the whole buffer (undocumented!) */ + + Error error; + while (p < end) { + size_t nbytes = is->LockRead(p, end - p, error); + if (nbytes == 0) { + if (!error.IsDefined()) + /* end of file */ + break; + + if (error.IsDomain(errno_domain)) + errno = error.GetCode(); + else + /* just some random non-zero + errno value */ + errno = EINVAL; + return 0; + } + + p += nbytes; + } + + /* libFLAC expects a clean errno after returning from the IO + callbacks (undocumented!) */ + errno = 0; + return (p - p0) / size; +} + +static int +FlacIOSeek(FLAC__IOHandle handle, FLAC__int64 offset, int whence) +{ + InputStream *is = (InputStream *)handle; + + Error error; + return is->LockSeek(offset, whence, error) ? 0 : -1; +} + +static FLAC__int64 +FlacIOTell(FLAC__IOHandle handle) +{ + InputStream *is = (InputStream *)handle; + + return is->offset; +} + +static int +FlacIOEof(FLAC__IOHandle handle) +{ + InputStream *is = (InputStream *)handle; + + return is->LockIsEOF(); +} + +static int +FlacIOClose(gcc_unused FLAC__IOHandle handle) +{ + /* no-op because the libFLAC caller is repsonsible for closing + the #InputStream */ + + return 0; +} + +const FLAC__IOCallbacks flac_io_callbacks = { + FlacIORead, + nullptr, + nullptr, + nullptr, + FlacIOEof, + FlacIOClose, +}; + +const FLAC__IOCallbacks flac_io_callbacks_seekable = { + FlacIORead, + nullptr, + FlacIOSeek, + FlacIOTell, + FlacIOEof, + FlacIOClose, +}; diff --git a/src/decoder/plugins/FlacIOHandle.hxx b/src/decoder/plugins/FlacIOHandle.hxx new file mode 100644 index 000000000..1f95b83d1 --- /dev/null +++ b/src/decoder/plugins/FlacIOHandle.hxx @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_FLAC_IO_HANDLE_HXX +#define MPD_FLAC_IO_HANDLE_HXX + +#include "Compiler.h" +#include "InputStream.hxx" + +#include + +extern const FLAC__IOCallbacks flac_io_callbacks; +extern const FLAC__IOCallbacks flac_io_callbacks_seekable; + +static inline FLAC__IOHandle +ToFlacIOHandle(InputStream &is) +{ + return (FLAC__IOHandle)&is; +} + +static inline const FLAC__IOCallbacks & +GetFlacIOCallbacks(const InputStream &is) +{ + return is.seekable + ? flac_io_callbacks_seekable + : flac_io_callbacks; +} + +#endif diff --git a/src/decoder/plugins/FlacInput.cxx b/src/decoder/plugins/FlacInput.cxx new file mode 100644 index 000000000..43350c3cf --- /dev/null +++ b/src/decoder/plugins/FlacInput.cxx @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "FlacInput.hxx" +#include "FlacDomain.hxx" +#include "../DecoderAPI.hxx" +#include "InputStream.hxx" +#include "util/Error.hxx" +#include "Log.hxx" +#include "Compiler.h" + +FLAC__StreamDecoderReadStatus +FlacInput::Read(FLAC__byte buffer[], size_t *bytes) +{ + size_t r = decoder_read(decoder, input_stream, (void *)buffer, *bytes); + *bytes = r; + + if (r == 0) { + if (input_stream.LockIsEOF() || + (decoder != nullptr && + decoder_get_command(*decoder) != DecoderCommand::NONE)) + return FLAC__STREAM_DECODER_READ_STATUS_END_OF_STREAM; + else + return FLAC__STREAM_DECODER_READ_STATUS_ABORT; + } + + return FLAC__STREAM_DECODER_READ_STATUS_CONTINUE; +} + +FLAC__StreamDecoderSeekStatus +FlacInput::Seek(FLAC__uint64 absolute_byte_offset) +{ + if (!input_stream.seekable) + return FLAC__STREAM_DECODER_SEEK_STATUS_UNSUPPORTED; + + ::Error error; + if (!input_stream.LockSeek(absolute_byte_offset, SEEK_SET, error)) { + LogError(error); + return FLAC__STREAM_DECODER_SEEK_STATUS_ERROR; + } + + return FLAC__STREAM_DECODER_SEEK_STATUS_OK; +} + +FLAC__StreamDecoderTellStatus +FlacInput::Tell(FLAC__uint64 *absolute_byte_offset) +{ + if (!input_stream.seekable) + return FLAC__STREAM_DECODER_TELL_STATUS_UNSUPPORTED; + + *absolute_byte_offset = (FLAC__uint64)input_stream.offset; + return FLAC__STREAM_DECODER_TELL_STATUS_OK; +} + +FLAC__StreamDecoderLengthStatus +FlacInput::Length(FLAC__uint64 *stream_length) +{ + if (input_stream.size < 0) + return FLAC__STREAM_DECODER_LENGTH_STATUS_UNSUPPORTED; + + *stream_length = (FLAC__uint64)input_stream.size; + return FLAC__STREAM_DECODER_LENGTH_STATUS_OK; +} + +FLAC__bool +FlacInput::Eof() +{ + return (decoder != nullptr && + decoder_get_command(*decoder) != DecoderCommand::NONE && + decoder_get_command(*decoder) != DecoderCommand::SEEK) || + input_stream.LockIsEOF(); +} + +void +FlacInput::Error(FLAC__StreamDecoderErrorStatus status) +{ + if (decoder == nullptr || + decoder_get_command(*decoder) != DecoderCommand::STOP) + LogWarning(flac_domain, + FLAC__StreamDecoderErrorStatusString[status]); +} + +FLAC__StreamDecoderReadStatus +FlacInput::Read(gcc_unused const FLAC__StreamDecoder *flac_decoder, + FLAC__byte buffer[], size_t *bytes, + void *client_data) +{ + FlacInput *i = (FlacInput *)client_data; + + return i->Read(buffer, bytes); +} + +FLAC__StreamDecoderSeekStatus +FlacInput::Seek(gcc_unused const FLAC__StreamDecoder *flac_decoder, + FLAC__uint64 absolute_byte_offset, void *client_data) +{ + FlacInput *i = (FlacInput *)client_data; + + return i->Seek(absolute_byte_offset); +} + +FLAC__StreamDecoderTellStatus +FlacInput::Tell(gcc_unused const FLAC__StreamDecoder *flac_decoder, + FLAC__uint64 *absolute_byte_offset, void *client_data) +{ + FlacInput *i = (FlacInput *)client_data; + + return i->Tell(absolute_byte_offset); +} + +FLAC__StreamDecoderLengthStatus +FlacInput::Length(gcc_unused const FLAC__StreamDecoder *flac_decoder, + FLAC__uint64 *stream_length, void *client_data) +{ + FlacInput *i = (FlacInput *)client_data; + + return i->Length(stream_length); +} + +FLAC__bool +FlacInput::Eof(gcc_unused const FLAC__StreamDecoder *flac_decoder, + void *client_data) +{ + FlacInput *i = (FlacInput *)client_data; + + return i->Eof(); +} + +void +FlacInput::Error(gcc_unused const FLAC__StreamDecoder *decoder, + FLAC__StreamDecoderErrorStatus status, void *client_data) +{ + FlacInput *i = (FlacInput *)client_data; + + i->Error(status); +} + diff --git a/src/decoder/plugins/FlacInput.hxx b/src/decoder/plugins/FlacInput.hxx new file mode 100644 index 000000000..30ed55fd0 --- /dev/null +++ b/src/decoder/plugins/FlacInput.hxx @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_FLAC_INPUT_HXX +#define MPD_FLAC_INPUT_HXX + +#include + +struct Decoder; +struct InputStream; + +/** + * This class wraps an #InputStream in libFLAC stream decoder + * callbacks. + */ +class FlacInput { + Decoder *const decoder; + + InputStream &input_stream; + +public: + FlacInput(InputStream &_input_stream, + Decoder *_decoder=nullptr) + :decoder(_decoder), input_stream(_input_stream) {} + +protected: + FLAC__StreamDecoderReadStatus Read(FLAC__byte buffer[], size_t *bytes); + FLAC__StreamDecoderSeekStatus Seek(FLAC__uint64 absolute_byte_offset); + FLAC__StreamDecoderTellStatus Tell(FLAC__uint64 *absolute_byte_offset); + FLAC__StreamDecoderLengthStatus Length(FLAC__uint64 *stream_length); + FLAC__bool Eof(); + void Error(FLAC__StreamDecoderErrorStatus status); + +public: + static FLAC__StreamDecoderReadStatus + Read(const FLAC__StreamDecoder *flac_decoder, + FLAC__byte buffer[], size_t *bytes, void *client_data); + + static FLAC__StreamDecoderSeekStatus + Seek(const FLAC__StreamDecoder *flac_decoder, + FLAC__uint64 absolute_byte_offset, void *client_data); + + static FLAC__StreamDecoderTellStatus + Tell(const FLAC__StreamDecoder *flac_decoder, + FLAC__uint64 *absolute_byte_offset, void *client_data); + + static FLAC__StreamDecoderLengthStatus + Length(const FLAC__StreamDecoder *flac_decoder, + FLAC__uint64 *stream_length, void *client_data); + + static FLAC__bool + Eof(const FLAC__StreamDecoder *flac_decoder, void *client_data); + + static void + Error(const FLAC__StreamDecoder *decoder, + FLAC__StreamDecoderErrorStatus status, void *client_data); +}; + +#endif diff --git a/src/decoder/plugins/FlacMetadata.cxx b/src/decoder/plugins/FlacMetadata.cxx new file mode 100644 index 000000000..b921e8481 --- /dev/null +++ b/src/decoder/plugins/FlacMetadata.cxx @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "FlacMetadata.hxx" +#include "XiphTags.hxx" +#include "MixRampInfo.hxx" +#include "tag/TagHandler.hxx" +#include "tag/TagTable.hxx" +#include "tag/TagBuilder.hxx" +#include "tag/Tag.hxx" +#include "ReplayGainInfo.hxx" +#include "util/ASCII.hxx" +#include "util/SplitString.hxx" + +#include + +static const char * +vorbis_comment_value(const FLAC__StreamMetadata *block, + const char *name) +{ + int offset = + FLAC__metadata_object_vorbiscomment_find_entry_from(block, 0, + name); + if (offset < 0) + return nullptr; + + size_t name_length = strlen(name); + + const FLAC__StreamMetadata_VorbisComment_Entry &vc = + block->data.vorbis_comment.comments[offset]; + const char *comment = (const char *)vc.entry; + + /* 1 is for '=' */ + return comment + name_length + 1; +} + +static bool +flac_find_float_comment(const FLAC__StreamMetadata *block, + const char *cmnt, float *fl) +{ + const char *value = vorbis_comment_value(block, cmnt); + if (value == nullptr) + return false; + + *fl = (float)atof(value); + return true; +} + +bool +flac_parse_replay_gain(ReplayGainInfo &rgi, + const FLAC__StreamMetadata *block) +{ + rgi.Clear(); + + bool found = false; + if (flac_find_float_comment(block, "replaygain_album_gain", + &rgi.tuples[REPLAY_GAIN_ALBUM].gain)) + found = true; + if (flac_find_float_comment(block, "replaygain_album_peak", + &rgi.tuples[REPLAY_GAIN_ALBUM].peak)) + found = true; + if (flac_find_float_comment(block, "replaygain_track_gain", + &rgi.tuples[REPLAY_GAIN_TRACK].gain)) + found = true; + if (flac_find_float_comment(block, "replaygain_track_peak", + &rgi.tuples[REPLAY_GAIN_TRACK].peak)) + found = true; + + return found; +} + +gcc_pure +static std::string +flac_find_string_comment(const FLAC__StreamMetadata *block, const char *cmnt) +{ + const char *value = vorbis_comment_value(block, cmnt); + if (value == nullptr) + return std::string(); + + return std::string(value); +} + +MixRampInfo +flac_parse_mixramp(const FLAC__StreamMetadata *block) +{ + MixRampInfo mix_ramp; + mix_ramp.SetStart(flac_find_string_comment(block, "mixramp_start")); + mix_ramp.SetEnd(flac_find_string_comment(block, "mixramp_end")); + return mix_ramp; +} + +/** + * Checks if the specified name matches the entry's name, and if yes, + * returns the comment value; + */ +static const char * +flac_comment_value(const FLAC__StreamMetadata_VorbisComment_Entry *entry, + const char *name) +{ + size_t name_length = strlen(name); + const char *comment = (const char*)entry->entry; + + if (!StringEqualsCaseASCII(comment, name, name_length)) + return nullptr; + + if (comment[name_length] == '=') { + return comment + name_length + 1; + } + + return nullptr; +} + +/** + * Check if the comment's name equals the passed name, and if so, copy + * the comment value into the tag. + */ +static bool +flac_copy_comment(const FLAC__StreamMetadata_VorbisComment_Entry *entry, + const char *name, TagType tag_type, + const struct tag_handler *handler, void *handler_ctx) +{ + const char *value = flac_comment_value(entry, name); + if (value != nullptr) { + tag_handler_invoke_tag(handler, handler_ctx, tag_type, value); + return true; + } + + return false; +} + +static void +flac_scan_comment(const FLAC__StreamMetadata_VorbisComment_Entry *entry, + const struct tag_handler *handler, void *handler_ctx) +{ + if (handler->pair != nullptr) { + const char *comment = (const char *)entry->entry; + const SplitString split(comment, '='); + if (split.IsDefined() && !split.IsEmpty()) + tag_handler_invoke_pair(handler, handler_ctx, + split.GetFirst(), + split.GetSecond()); + } + + for (const struct tag_table *i = xiph_tags; i->name != nullptr; ++i) + if (flac_copy_comment(entry, i->name, i->type, + handler, handler_ctx)) + return; + + for (unsigned i = 0; i < TAG_NUM_OF_ITEM_TYPES; ++i) + if (flac_copy_comment(entry, + tag_item_names[i], (TagType)i, + handler, handler_ctx)) + return; +} + +static void +flac_scan_comments(const FLAC__StreamMetadata_VorbisComment *comment, + const struct tag_handler *handler, void *handler_ctx) +{ + for (unsigned i = 0; i < comment->num_comments; ++i) + flac_scan_comment(&comment->comments[i], + handler, handler_ctx); +} + +void +flac_scan_metadata(const FLAC__StreamMetadata *block, + const struct tag_handler *handler, void *handler_ctx) +{ + switch (block->type) { + case FLAC__METADATA_TYPE_VORBIS_COMMENT: + flac_scan_comments(&block->data.vorbis_comment, + handler, handler_ctx); + break; + + case FLAC__METADATA_TYPE_STREAMINFO: + if (block->data.stream_info.sample_rate > 0) + tag_handler_invoke_duration(handler, handler_ctx, + flac_duration(&block->data.stream_info)); + break; + + default: + break; + } +} + +Tag +flac_vorbis_comments_to_tag(const FLAC__StreamMetadata_VorbisComment *comment) +{ + TagBuilder tag_builder; + flac_scan_comments(comment, &add_tag_handler, &tag_builder); + return tag_builder.Commit(); +} + +void +FlacMetadataChain::Scan(const struct tag_handler *handler, void *handler_ctx) +{ + FLACMetadataIterator iterator(*this); + + do { + FLAC__StreamMetadata *block = iterator.GetBlock(); + if (block == nullptr) + break; + + flac_scan_metadata(block, handler, handler_ctx); + } while (iterator.Next()); +} diff --git a/src/decoder/plugins/FlacMetadata.hxx b/src/decoder/plugins/FlacMetadata.hxx new file mode 100644 index 000000000..e0449b2a2 --- /dev/null +++ b/src/decoder/plugins/FlacMetadata.hxx @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_FLAC_METADATA_H +#define MPD_FLAC_METADATA_H + +#include "Compiler.h" +#include "FlacIOHandle.hxx" + +#include + +#include + +struct tag_handler; +class MixRampInfo; + +class FlacMetadataChain { + FLAC__Metadata_Chain *chain; + +public: + FlacMetadataChain():chain(::FLAC__metadata_chain_new()) {} + + ~FlacMetadataChain() { + ::FLAC__metadata_chain_delete(chain); + } + + explicit operator FLAC__Metadata_Chain *() { + return chain; + } + + bool Read(const char *path) { + return ::FLAC__metadata_chain_read(chain, path); + } + + bool Read(FLAC__IOHandle handle, FLAC__IOCallbacks callbacks) { + return ::FLAC__metadata_chain_read_with_callbacks(chain, + handle, + callbacks); + } + + bool Read(InputStream &is) { + return Read(::ToFlacIOHandle(is), ::GetFlacIOCallbacks(is)); + } + + bool ReadOgg(const char *path) { + return ::FLAC__metadata_chain_read_ogg(chain, path); + } + + bool ReadOgg(FLAC__IOHandle handle, FLAC__IOCallbacks callbacks) { + return ::FLAC__metadata_chain_read_ogg_with_callbacks(chain, + handle, + callbacks); + } + + bool ReadOgg(InputStream &is) { + return ReadOgg(::ToFlacIOHandle(is), ::GetFlacIOCallbacks(is)); + } + + gcc_pure + FLAC__Metadata_ChainStatus GetStatus() const { + return ::FLAC__metadata_chain_status(chain); + } + + gcc_pure + const char *GetStatusString() const { + return FLAC__Metadata_ChainStatusString[GetStatus()]; + } + + void Scan(const tag_handler *handler, void *handler_ctx); +}; + +class FLACMetadataIterator { + FLAC__Metadata_Iterator *iterator; + +public: + FLACMetadataIterator():iterator(::FLAC__metadata_iterator_new()) {} + + FLACMetadataIterator(FlacMetadataChain &chain) + :iterator(::FLAC__metadata_iterator_new()) { + ::FLAC__metadata_iterator_init(iterator, + (FLAC__Metadata_Chain *)chain); + } + + ~FLACMetadataIterator() { + ::FLAC__metadata_iterator_delete(iterator); + } + + bool Next() { + return ::FLAC__metadata_iterator_next(iterator); + } + + gcc_pure + FLAC__StreamMetadata *GetBlock() { + return ::FLAC__metadata_iterator_get_block(iterator); + } +}; + +struct Tag; +struct ReplayGainInfo; + +static inline unsigned +flac_duration(const FLAC__StreamMetadata_StreamInfo *stream_info) +{ + assert(stream_info->sample_rate > 0); + + return (stream_info->total_samples + stream_info->sample_rate - 1) / + stream_info->sample_rate; +} + +bool +flac_parse_replay_gain(ReplayGainInfo &rgi, + const FLAC__StreamMetadata *block); + +MixRampInfo +flac_parse_mixramp(const FLAC__StreamMetadata *block); + +Tag +flac_vorbis_comments_to_tag(const FLAC__StreamMetadata_VorbisComment *comment); + +void +flac_scan_metadata(const FLAC__StreamMetadata *block, + const tag_handler *handler, void *handler_ctx); + +#endif diff --git a/src/decoder/plugins/FlacPcm.cxx b/src/decoder/plugins/FlacPcm.cxx new file mode 100644 index 000000000..311500f26 --- /dev/null +++ b/src/decoder/plugins/FlacPcm.cxx @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "FlacPcm.hxx" + +#include + +static void flac_convert_stereo16(int16_t *dest, + const FLAC__int32 * const buf[], + unsigned int position, unsigned int end) +{ + for (; position < end; ++position) { + *dest++ = buf[0][position]; + *dest++ = buf[1][position]; + } +} + +static void +flac_convert_16(int16_t *dest, + unsigned int num_channels, + const FLAC__int32 * const buf[], + unsigned int position, unsigned int end) +{ + unsigned int c_chan; + + for (; position < end; ++position) + for (c_chan = 0; c_chan < num_channels; c_chan++) + *dest++ = buf[c_chan][position]; +} + +/** + * Note: this function also handles 24 bit files! + */ +static void +flac_convert_32(int32_t *dest, + unsigned int num_channels, + const FLAC__int32 * const buf[], + unsigned int position, unsigned int end) +{ + unsigned int c_chan; + + for (; position < end; ++position) + for (c_chan = 0; c_chan < num_channels; c_chan++) + *dest++ = buf[c_chan][position]; +} + +static void +flac_convert_8(int8_t *dest, + unsigned int num_channels, + const FLAC__int32 * const buf[], + unsigned int position, unsigned int end) +{ + unsigned int c_chan; + + for (; position < end; ++position) + for (c_chan = 0; c_chan < num_channels; c_chan++) + *dest++ = buf[c_chan][position]; +} + +void +flac_convert(void *dest, + unsigned int num_channels, SampleFormat sample_format, + const FLAC__int32 *const buf[], + unsigned int position, unsigned int end) +{ + switch (sample_format) { + case SampleFormat::S16: + if (num_channels == 2) + flac_convert_stereo16((int16_t*)dest, buf, + position, end); + else + flac_convert_16((int16_t*)dest, num_channels, buf, + position, end); + break; + + case SampleFormat::S24_P32: + case SampleFormat::S32: + flac_convert_32((int32_t*)dest, num_channels, buf, + position, end); + break; + + case SampleFormat::S8: + flac_convert_8((int8_t*)dest, num_channels, buf, + position, end); + break; + + case SampleFormat::FLOAT: + case SampleFormat::DSD: + case SampleFormat::UNDEFINED: + assert(false); + gcc_unreachable(); + } +} diff --git a/src/decoder/plugins/FlacPcm.hxx b/src/decoder/plugins/FlacPcm.hxx new file mode 100644 index 000000000..30c318725 --- /dev/null +++ b/src/decoder/plugins/FlacPcm.hxx @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_FLAC_PCM_HXX +#define MPD_FLAC_PCM_HXX + +#include "AudioFormat.hxx" + +#include + +void +flac_convert(void *dest, + unsigned int num_channels, SampleFormat sample_format, + const FLAC__int32 *const buf[], + unsigned int position, unsigned int end); + +#endif diff --git a/src/decoder/plugins/FluidsynthDecoderPlugin.cxx b/src/decoder/plugins/FluidsynthDecoderPlugin.cxx new file mode 100644 index 000000000..3b4703e97 --- /dev/null +++ b/src/decoder/plugins/FluidsynthDecoderPlugin.cxx @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "FluidsynthDecoderPlugin.hxx" +#include "../DecoderAPI.hxx" +#include "CheckAudioFormat.hxx" +#include "util/Error.hxx" +#include "util/Domain.hxx" +#include "util/Macros.hxx" +#include "Log.hxx" + +#include + +static constexpr Domain fluidsynth_domain("fluidsynth"); + +static unsigned sample_rate; +static const char *soundfont_path; + +/** + * Convert a fluidsynth log level to a GLib log level. + */ +static LogLevel +fluidsynth_level_to_mpd(enum fluid_log_level level) +{ + switch (level) { + case FLUID_PANIC: + case FLUID_ERR: + return LogLevel::ERROR; + + case FLUID_WARN: + return LogLevel::WARNING; + + case FLUID_INFO: + return LogLevel::INFO; + + case FLUID_DBG: + case LAST_LOG_LEVEL: + return LogLevel::DEBUG; + } + + /* invalid fluidsynth log level */ + return LogLevel::INFO; +} + +/** + * The fluidsynth logging callback. It forwards messages to the GLib + * logging library. + */ +static void +fluidsynth_mpd_log_function(int level, char *message, gcc_unused void *data) +{ + Log(fluidsynth_domain, + fluidsynth_level_to_mpd(fluid_log_level(level)), + message); +} + +static bool +fluidsynth_init(const config_param ¶m) +{ + Error error; + + sample_rate = param.GetBlockValue("sample_rate", 48000u); + if (!audio_check_sample_rate(sample_rate, error)) { + LogError(error); + return false; + } + + soundfont_path = param.GetBlockValue("soundfont", + "/usr/share/sounds/sf2/FluidR3_GM.sf2"); + + fluid_set_log_function(LAST_LOG_LEVEL, + fluidsynth_mpd_log_function, nullptr); + + return true; +} + +static void +fluidsynth_file_decode(Decoder &decoder, const char *path_fs) +{ + char setting_sample_rate[] = "synth.sample-rate"; + /* + char setting_verbose[] = "synth.verbose"; + char setting_yes[] = "yes"; + */ + fluid_settings_t *settings; + fluid_synth_t *synth; + fluid_player_t *player; + int ret; + + /* set up fluid settings */ + + settings = new_fluid_settings(); + if (settings == nullptr) + return; + + fluid_settings_setnum(settings, setting_sample_rate, sample_rate); + + /* + fluid_settings_setstr(settings, setting_verbose, setting_yes); + */ + + /* create the fluid synth */ + + synth = new_fluid_synth(settings); + if (synth == nullptr) { + delete_fluid_settings(settings); + return; + } + + ret = fluid_synth_sfload(synth, soundfont_path, true); + if (ret < 0) { + LogWarning(fluidsynth_domain, "fluid_synth_sfload() failed"); + delete_fluid_synth(synth); + delete_fluid_settings(settings); + return; + } + + /* create the fluid player */ + + player = new_fluid_player(synth); + if (player == nullptr) { + delete_fluid_synth(synth); + delete_fluid_settings(settings); + return; + } + + ret = fluid_player_add(player, path_fs); + if (ret != 0) { + LogWarning(fluidsynth_domain, "fluid_player_add() failed"); + delete_fluid_player(player); + delete_fluid_synth(synth); + delete_fluid_settings(settings); + return; + } + + /* start the player */ + + ret = fluid_player_play(player); + if (ret != 0) { + LogWarning(fluidsynth_domain, "fluid_player_play() failed"); + delete_fluid_player(player); + delete_fluid_synth(synth); + delete_fluid_settings(settings); + return; + } + + /* initialization complete - announce the audio format to the + MPD core */ + + const AudioFormat audio_format(sample_rate, SampleFormat::S16, 2); + decoder_initialized(decoder, audio_format, false, -1); + + DecoderCommand cmd; + while (fluid_player_get_status(player) == FLUID_PLAYER_PLAYING) { + int16_t buffer[2048]; + const unsigned max_frames = ARRAY_SIZE(buffer) / 2; + + /* read samples from fluidsynth and send them to the + MPD core */ + + ret = fluid_synth_write_s16(synth, max_frames, + buffer, 0, 2, + buffer, 1, 2); + if (ret != 0) + break; + + cmd = decoder_data(decoder, nullptr, buffer, sizeof(buffer), + 0); + if (cmd != DecoderCommand::NONE) + break; + } + + /* clean up */ + + fluid_player_stop(player); + fluid_player_join(player); + + delete_fluid_player(player); + delete_fluid_synth(synth); + delete_fluid_settings(settings); +} + +static bool +fluidsynth_scan_file(const char *file, + gcc_unused const struct tag_handler *handler, + gcc_unused void *handler_ctx) +{ + return fluid_is_midifile(file); +} + +static const char *const fluidsynth_suffixes[] = { + "mid", + nullptr +}; + +const struct DecoderPlugin fluidsynth_decoder_plugin = { + "fluidsynth", + fluidsynth_init, + nullptr, + nullptr, + fluidsynth_file_decode, + fluidsynth_scan_file, + nullptr, + nullptr, + fluidsynth_suffixes, + nullptr, +}; diff --git a/src/decoder/plugins/FluidsynthDecoderPlugin.hxx b/src/decoder/plugins/FluidsynthDecoderPlugin.hxx new file mode 100644 index 000000000..cd8ec2d62 --- /dev/null +++ b/src/decoder/plugins/FluidsynthDecoderPlugin.hxx @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_DECODER_FLUIDSYNTH_HXX +#define MPD_DECODER_FLUIDSYNTH_HXX + +extern const struct DecoderPlugin fluidsynth_decoder_plugin; + +#endif diff --git a/src/decoder/plugins/GmeDecoderPlugin.cxx b/src/decoder/plugins/GmeDecoderPlugin.cxx new file mode 100644 index 000000000..53015facf --- /dev/null +++ b/src/decoder/plugins/GmeDecoderPlugin.cxx @@ -0,0 +1,295 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "GmeDecoderPlugin.hxx" +#include "../DecoderAPI.hxx" +#include "CheckAudioFormat.hxx" +#include "tag/TagHandler.hxx" +#include "util/Alloc.hxx" +#include "util/FormatString.hxx" +#include "util/UriUtil.hxx" +#include "util/Error.hxx" +#include "util/Domain.hxx" +#include "Log.hxx" + +#include +#include +#include +#include + +#include + +#define SUBTUNE_PREFIX "tune_" + +static constexpr Domain gme_domain("gme"); + +static constexpr unsigned GME_SAMPLE_RATE = 44100; +static constexpr unsigned GME_CHANNELS = 2; +static constexpr unsigned GME_BUFFER_FRAMES = 2048; +static constexpr unsigned GME_BUFFER_SAMPLES = + GME_BUFFER_FRAMES * GME_CHANNELS; + +/** + * returns the file path stripped of any /tune_xxx.* subtune + * suffix + */ +static char * +get_container_name(const char *path_fs) +{ + const char *subtune_suffix = uri_get_suffix(path_fs); + char *path_container = xstrdup(path_fs); + + char pat[64]; + snprintf(pat, sizeof(pat), "%s%s", + "*/" SUBTUNE_PREFIX "???.", + subtune_suffix); + GPatternSpec *path_with_subtune = g_pattern_spec_new(pat); + if (!g_pattern_match(path_with_subtune, + strlen(path_container), path_container, nullptr)) { + g_pattern_spec_free(path_with_subtune); + return path_container; + } + + char *ptr = g_strrstr(path_container, "/" SUBTUNE_PREFIX); + if (ptr != nullptr) + *ptr='\0'; + + g_pattern_spec_free(path_with_subtune); + return path_container; +} + +/** + * returns tune number from file.nsf/tune_xxx.* style path or 0 if no subtune + * is appended. + */ +static int +get_song_num(const char *path_fs) +{ + const char *subtune_suffix = uri_get_suffix(path_fs); + + char pat[64]; + snprintf(pat, sizeof(pat), "%s%s", + "*/" SUBTUNE_PREFIX "???.", + subtune_suffix); + GPatternSpec *path_with_subtune = g_pattern_spec_new(pat); + + if (g_pattern_match(path_with_subtune, + strlen(path_fs), path_fs, nullptr)) { + char *sub = g_strrstr(path_fs, "/" SUBTUNE_PREFIX); + g_pattern_spec_free(path_with_subtune); + if (!sub) + return 0; + + sub += strlen("/" SUBTUNE_PREFIX); + int song_num = strtol(sub, nullptr, 10); + + return song_num - 1; + } else { + g_pattern_spec_free(path_with_subtune); + return 0; + } +} + +static char * +gme_container_scan(const char *path_fs, const unsigned int tnum) +{ + Music_Emu *emu; + const char *gme_err = gme_open_file(path_fs, &emu, GME_SAMPLE_RATE); + if (gme_err != nullptr) { + LogWarning(gme_domain, gme_err); + return nullptr; + } + + const unsigned num_songs = gme_track_count(emu); + /* if it only contains a single tune, don't treat as container */ + if (num_songs < 2) + return nullptr; + + const char *subtune_suffix = uri_get_suffix(path_fs); + if (tnum <= num_songs){ + return FormatNew(SUBTUNE_PREFIX "%03u.%s", + tnum, subtune_suffix); + } else + return nullptr; +} + +static void +gme_file_decode(Decoder &decoder, const char *path_fs) +{ + char *path_container = get_container_name(path_fs); + + Music_Emu *emu; + const char *gme_err = + gme_open_file(path_container, &emu, GME_SAMPLE_RATE); + free(path_container); + if (gme_err != nullptr) { + LogWarning(gme_domain, gme_err); + return; + } + + gme_info_t *ti; + const int song_num = get_song_num(path_fs); + gme_err = gme_track_info(emu, &ti, song_num); + if (gme_err != nullptr) { + LogWarning(gme_domain, gme_err); + gme_delete(emu); + return; + } + + const float song_len = ti->length > 0 + ? ti->length / 1000.0 + : -1.0; + + /* initialize the MPD decoder */ + + Error error; + AudioFormat audio_format; + if (!audio_format_init_checked(audio_format, GME_SAMPLE_RATE, + SampleFormat::S16, GME_CHANNELS, + error)) { + LogError(error); + gme_free_info(ti); + gme_delete(emu); + return; + } + + decoder_initialized(decoder, audio_format, true, song_len); + + gme_err = gme_start_track(emu, song_num); + if (gme_err != nullptr) + LogWarning(gme_domain, gme_err); + + if (ti->length > 0) + gme_set_fade(emu, ti->length); + + /* play */ + DecoderCommand cmd; + do { + short buf[GME_BUFFER_SAMPLES]; + gme_err = gme_play(emu, GME_BUFFER_SAMPLES, buf); + if (gme_err != nullptr) { + LogWarning(gme_domain, gme_err); + return; + } + + cmd = decoder_data(decoder, nullptr, buf, sizeof(buf), 0); + if (cmd == DecoderCommand::SEEK) { + float where = decoder_seek_where(decoder); + gme_err = gme_seek(emu, int(where * 1000)); + if (gme_err != nullptr) + LogWarning(gme_domain, gme_err); + decoder_command_finished(decoder); + } + + if (gme_track_ended(emu)) + break; + } while (cmd != DecoderCommand::STOP); + + gme_free_info(ti); + gme_delete(emu); +} + +static bool +gme_scan_file(const char *path_fs, + const struct tag_handler *handler, void *handler_ctx) +{ + char *path_container = get_container_name(path_fs); + + Music_Emu *emu; + const char *gme_err = + gme_open_file(path_container, &emu, GME_SAMPLE_RATE); + g_free(path_container); + if (gme_err != nullptr) { + LogWarning(gme_domain, gme_err); + return false; + } + + const int song_num = get_song_num(path_fs); + + gme_info_t *ti; + gme_err = gme_track_info(emu, &ti, song_num); + if (gme_err != nullptr) { + LogWarning(gme_domain, gme_err); + gme_delete(emu); + return false; + } + + assert(ti != nullptr); + + if (ti->length > 0) + tag_handler_invoke_duration(handler, handler_ctx, + ti->length / 100); + + if (ti->song != nullptr) { + if (gme_track_count(emu) > 1) { + /* start numbering subtunes from 1 */ + char tag_title[1024]; + snprintf(tag_title, sizeof(tag_title), + "%s (%d/%d)", + ti->song, song_num + 1, + gme_track_count(emu)); + tag_handler_invoke_tag(handler, handler_ctx, + TAG_TITLE, tag_title); + } else + tag_handler_invoke_tag(handler, handler_ctx, + TAG_TITLE, ti->song); + } + + if (ti->author != nullptr) + tag_handler_invoke_tag(handler, handler_ctx, + TAG_ARTIST, ti->author); + + if (ti->game != nullptr) + tag_handler_invoke_tag(handler, handler_ctx, + TAG_ALBUM, ti->game); + + if (ti->comment != nullptr) + tag_handler_invoke_tag(handler, handler_ctx, + TAG_COMMENT, ti->comment); + + if (ti->copyright != nullptr) + tag_handler_invoke_tag(handler, handler_ctx, + TAG_DATE, ti->copyright); + + gme_free_info(ti); + gme_delete(emu); + + return true; +} + +static const char *const gme_suffixes[] = { + "ay", "gbs", "gym", "hes", "kss", "nsf", + "nsfe", "sap", "spc", "vgm", "vgz", + nullptr +}; + +extern const struct DecoderPlugin gme_decoder_plugin; +const struct DecoderPlugin gme_decoder_plugin = { + "gme", + nullptr, + nullptr, + nullptr, + gme_file_decode, + gme_scan_file, + nullptr, + gme_container_scan, + gme_suffixes, + nullptr, +}; diff --git a/src/decoder/plugins/GmeDecoderPlugin.hxx b/src/decoder/plugins/GmeDecoderPlugin.hxx new file mode 100644 index 000000000..f4885b6e4 --- /dev/null +++ b/src/decoder/plugins/GmeDecoderPlugin.hxx @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_DECODER_GME_HXX +#define MPD_DECODER_GME_HXX + +extern const struct DecoderPlugin gme_decoder_plugin; + +#endif diff --git a/src/decoder/plugins/MadDecoderPlugin.cxx b/src/decoder/plugins/MadDecoderPlugin.cxx new file mode 100644 index 000000000..07fd05f1b --- /dev/null +++ b/src/decoder/plugins/MadDecoderPlugin.cxx @@ -0,0 +1,1155 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "MadDecoderPlugin.hxx" +#include "../DecoderAPI.hxx" +#include "InputStream.hxx" +#include "ConfigGlobal.hxx" +#include "tag/TagId3.hxx" +#include "tag/TagRva2.hxx" +#include "tag/TagHandler.hxx" +#include "CheckAudioFormat.hxx" +#include "util/StringUtil.hxx" +#include "util/ASCII.hxx" +#include "util/Error.hxx" +#include "util/Domain.hxx" +#include "Log.hxx" + +#include + +#ifdef HAVE_ID3TAG +#include +#endif + +#include +#include +#include +#include + +#define FRAMES_CUSHION 2000 + +#define READ_BUFFER_SIZE 40960 + +enum mp3_action { + DECODE_SKIP = -3, + DECODE_BREAK = -2, + DECODE_CONT = -1, + DECODE_OK = 0 +}; + +enum muteframe { + MUTEFRAME_NONE, + MUTEFRAME_SKIP, + MUTEFRAME_SEEK +}; + +/* the number of samples of silence the decoder inserts at start */ +#define DECODERDELAY 529 + +#define DEFAULT_GAPLESS_MP3_PLAYBACK true + +static constexpr Domain mad_domain("mad"); + +static bool gapless_playback; + +static inline int32_t +mad_fixed_to_24_sample(mad_fixed_t sample) +{ + enum { + bits = 24, + MIN = -MAD_F_ONE, + MAX = MAD_F_ONE - 1 + }; + + /* round */ + sample = sample + (1L << (MAD_F_FRACBITS - bits)); + + /* clip */ + if (gcc_unlikely(sample > MAX)) + sample = MAX; + else if (gcc_unlikely(sample < MIN)) + sample = MIN; + + /* quantize */ + return sample >> (MAD_F_FRACBITS + 1 - bits); +} + +static void +mad_fixed_to_24_buffer(int32_t *dest, const struct mad_synth *synth, + unsigned int start, unsigned int end, + unsigned int num_channels) +{ + unsigned int i, c; + + for (i = start; i < end; ++i) { + for (c = 0; c < num_channels; ++c) + *dest++ = mad_fixed_to_24_sample(synth->pcm.samples[c][i]); + } +} + +static bool +mp3_plugin_init(gcc_unused const config_param ¶m) +{ + gapless_playback = config_get_bool(CONF_GAPLESS_MP3_PLAYBACK, + DEFAULT_GAPLESS_MP3_PLAYBACK); + return true; +} + +#define MP3_DATA_OUTPUT_BUFFER_SIZE 2048 + +struct MadDecoder { + struct mad_stream stream; + struct mad_frame frame; + struct mad_synth synth; + mad_timer_t timer; + unsigned char input_buffer[READ_BUFFER_SIZE]; + int32_t output_buffer[MP3_DATA_OUTPUT_BUFFER_SIZE]; + float total_time; + float elapsed_time; + float seek_where; + enum muteframe mute_frame; + long *frame_offsets; + mad_timer_t *times; + unsigned long highest_frame; + unsigned long max_frames; + unsigned long current_frame; + unsigned int drop_start_frames; + unsigned int drop_end_frames; + unsigned int drop_start_samples; + unsigned int drop_end_samples; + bool found_replay_gain; + bool found_xing; + bool found_first_frame; + bool decoded_first_frame; + unsigned long bit_rate; + Decoder *const decoder; + InputStream &input_stream; + enum mad_layer layer; + + MadDecoder(Decoder *decoder, InputStream &input_stream); + ~MadDecoder(); + + bool Seek(long offset); + bool FillBuffer(); + void ParseId3(size_t tagsize, Tag **mpd_tag); + enum mp3_action DecodeNextFrameHeader(Tag **tag); + enum mp3_action DecodeNextFrame(); + + gcc_pure + InputStream::offset_type ThisFrameOffset() const; + + gcc_pure + InputStream::offset_type RestIncludingThisFrame() const; + + /** + * Attempt to calulcate the length of the song from filesize + */ + void FileSizeToSongLength(); + + bool DecodeFirstFrame(Tag **tag); + + gcc_pure + long TimeToFrame(double t) const; + + void UpdateTimerNextFrame(); + + /** + * Sends the synthesized current frame via decoder_data(). + */ + DecoderCommand SendPCM(unsigned i, unsigned pcm_length); + + /** + * Synthesize the current frame and send it via + * decoder_data(). + */ + DecoderCommand SyncAndSend(); + + bool Read(); +}; + +MadDecoder::MadDecoder(Decoder *_decoder, + InputStream &_input_stream) + :mute_frame(MUTEFRAME_NONE), + frame_offsets(nullptr), + times(nullptr), + highest_frame(0), max_frames(0), current_frame(0), + drop_start_frames(0), drop_end_frames(0), + drop_start_samples(0), drop_end_samples(0), + found_replay_gain(false), found_xing(false), + found_first_frame(false), decoded_first_frame(false), + decoder(_decoder), input_stream(_input_stream), + layer(mad_layer(0)) +{ + mad_stream_init(&stream); + mad_stream_options(&stream, MAD_OPTION_IGNORECRC); + mad_frame_init(&frame); + mad_synth_init(&synth); + mad_timer_reset(&timer); +} + +inline bool +MadDecoder::Seek(long offset) +{ + Error error; + if (!input_stream.LockSeek(offset, SEEK_SET, error)) + return false; + + mad_stream_buffer(&stream, input_buffer, 0); + stream.error = MAD_ERROR_NONE; + + return true; +} + +inline bool +MadDecoder::FillBuffer() +{ + size_t remaining, length; + unsigned char *dest; + + if (stream.next_frame != nullptr) { + remaining = stream.bufend - stream.next_frame; + memmove(input_buffer, stream.next_frame, remaining); + dest = input_buffer + remaining; + length = READ_BUFFER_SIZE - remaining; + } else { + remaining = 0; + length = READ_BUFFER_SIZE; + dest = input_buffer; + } + + /* we've exhausted the read buffer, so give up!, these potential + * mp3 frames are way too big, and thus unlikely to be mp3 frames */ + if (length == 0) + return false; + + length = decoder_read(decoder, input_stream, dest, length); + if (length == 0) + return false; + + mad_stream_buffer(&stream, input_buffer, length + remaining); + stream.error = MAD_ERROR_NONE; + + return true; +} + +#ifdef HAVE_ID3TAG +static bool +parse_id3_replay_gain_info(ReplayGainInfo &rgi, + struct id3_tag *tag) +{ + int i; + char *key; + char *value; + struct id3_frame *frame; + bool found = false; + + rgi.Clear(); + + for (i = 0; (frame = id3_tag_findframe(tag, "TXXX", i)); i++) { + if (frame->nfields < 3) + continue; + + key = (char *) + id3_ucs4_latin1duplicate(id3_field_getstring + (&frame->fields[1])); + value = (char *) + id3_ucs4_latin1duplicate(id3_field_getstring + (&frame->fields[2])); + + if (StringEqualsCaseASCII(key, "replaygain_track_gain")) { + rgi.tuples[REPLAY_GAIN_TRACK].gain = atof(value); + found = true; + } else if (StringEqualsCaseASCII(key, "replaygain_album_gain")) { + rgi.tuples[REPLAY_GAIN_ALBUM].gain = atof(value); + found = true; + } else if (StringEqualsCaseASCII(key, "replaygain_track_peak")) { + rgi.tuples[REPLAY_GAIN_TRACK].peak = atof(value); + found = true; + } else if (StringEqualsCaseASCII(key, "replaygain_album_peak")) { + rgi.tuples[REPLAY_GAIN_ALBUM].peak = atof(value); + found = true; + } + + free(key); + free(value); + } + + return found || + /* fall back on RVA2 if no replaygain tags found */ + tag_rva2_parse(tag, rgi); +} +#endif + +#ifdef HAVE_ID3TAG +gcc_pure +static MixRampInfo +parse_id3_mixramp(struct id3_tag *tag) +{ + int i; + char *key; + char *value; + struct id3_frame *frame; + + MixRampInfo result; + + for (i = 0; (frame = id3_tag_findframe(tag, "TXXX", i)); i++) { + if (frame->nfields < 3) + continue; + + key = (char *) + id3_ucs4_latin1duplicate(id3_field_getstring + (&frame->fields[1])); + value = (char *) + id3_ucs4_latin1duplicate(id3_field_getstring + (&frame->fields[2])); + + if (StringEqualsCaseASCII(key, "mixramp_start")) { + result.SetStart(value); + } else if (StringEqualsCaseASCII(key, "mixramp_end")) { + result.SetEnd(value); + } + + free(key); + free(value); + } + + return result; +} +#endif + +inline void +MadDecoder::ParseId3(size_t tagsize, Tag **mpd_tag) +{ +#ifdef HAVE_ID3TAG + struct id3_tag *id3_tag = nullptr; + id3_length_t count; + id3_byte_t const *id3_data; + id3_byte_t *allocated = nullptr; + + count = stream.bufend - stream.this_frame; + + if (tagsize <= count) { + id3_data = stream.this_frame; + mad_stream_skip(&(stream), tagsize); + } else { + allocated = new id3_byte_t[tagsize]; + memcpy(allocated, stream.this_frame, count); + mad_stream_skip(&(stream), count); + + if (!decoder_read_full(decoder, input_stream, + allocated + count, tagsize - count)) { + LogDebug(mad_domain, "error parsing ID3 tag"); + delete[] allocated; + return; + } + + id3_data = allocated; + } + + id3_tag = id3_tag_parse(id3_data, tagsize); + if (id3_tag == nullptr) { + delete[] allocated; + return; + } + + if (mpd_tag) { + Tag *tmp_tag = tag_id3_import(id3_tag); + if (tmp_tag != nullptr) { + delete *mpd_tag; + *mpd_tag = tmp_tag; + } + } + + if (decoder != nullptr) { + ReplayGainInfo rgi; + + if (parse_id3_replay_gain_info(rgi, id3_tag)) { + decoder_replay_gain(*decoder, &rgi); + found_replay_gain = true; + } + + decoder_mixramp(*decoder, parse_id3_mixramp(id3_tag)); + } + + id3_tag_delete(id3_tag); + + delete[] allocated; +#else /* !HAVE_ID3TAG */ + (void)mpd_tag; + + /* This code is enabled when libid3tag is disabled. Instead + of parsing the ID3 frame, it just skips it. */ + + size_t count = stream.bufend - stream.this_frame; + + if (tagsize <= count) { + mad_stream_skip(&stream, tagsize); + } else { + mad_stream_skip(&stream, count); + decoder_skip(decoder, input_stream, tagsize - count); + } +#endif +} + +#ifndef HAVE_ID3TAG +/** + * This function emulates libid3tag when it is disabled. Instead of + * doing a real analyzation of the frame, it just checks whether the + * frame begins with the string "ID3". If so, it returns the length + * of the ID3 frame. + */ +static signed long +id3_tag_query(const void *p0, size_t length) +{ + const char *p = (const char *)p0; + + return length >= 10 && memcmp(p, "ID3", 3) == 0 + ? (p[8] << 7) + p[9] + 10 + : 0; +} +#endif /* !HAVE_ID3TAG */ + +enum mp3_action +MadDecoder::DecodeNextFrameHeader(Tag **tag) +{ + if ((stream.buffer == nullptr || stream.error == MAD_ERROR_BUFLEN) && + !FillBuffer()) + return DECODE_BREAK; + + if (mad_header_decode(&frame.header, &stream)) { + if (stream.error == MAD_ERROR_LOSTSYNC && stream.this_frame) { + signed long tagsize = id3_tag_query(stream.this_frame, + stream.bufend - + stream.this_frame); + + if (tagsize > 0) { + if (tag && !(*tag)) { + ParseId3((size_t)tagsize, tag); + } else { + mad_stream_skip(&stream, tagsize); + } + return DECODE_CONT; + } + } + if (MAD_RECOVERABLE(stream.error)) { + return DECODE_SKIP; + } else { + if (stream.error == MAD_ERROR_BUFLEN) + return DECODE_CONT; + else { + FormatWarning(mad_domain, + "unrecoverable frame level error: %s", + mad_stream_errorstr(&stream)); + return DECODE_BREAK; + } + } + } + + enum mad_layer new_layer = frame.header.layer; + if (layer == (mad_layer)0) { + if (new_layer != MAD_LAYER_II && new_layer != MAD_LAYER_III) { + /* Only layer 2 and 3 have been tested to work */ + return DECODE_SKIP; + } + + layer = new_layer; + } else if (new_layer != layer) { + /* Don't decode frames with a different layer than the first */ + return DECODE_SKIP; + } + + return DECODE_OK; +} + +enum mp3_action +MadDecoder::DecodeNextFrame() +{ + if ((stream.buffer == nullptr || stream.error == MAD_ERROR_BUFLEN) && + !FillBuffer()) + return DECODE_BREAK; + + if (mad_frame_decode(&frame, &stream)) { + if (stream.error == MAD_ERROR_LOSTSYNC) { + signed long tagsize = id3_tag_query(stream.this_frame, + stream.bufend - + stream.this_frame); + if (tagsize > 0) { + mad_stream_skip(&stream, tagsize); + return DECODE_CONT; + } + } + if (MAD_RECOVERABLE(stream.error)) { + return DECODE_SKIP; + } else { + if (stream.error == MAD_ERROR_BUFLEN) + return DECODE_CONT; + else { + FormatWarning(mad_domain, + "unrecoverable frame level error: %s", + mad_stream_errorstr(&stream)); + return DECODE_BREAK; + } + } + } + + return DECODE_OK; +} + +/* xing stuff stolen from alsaplayer, and heavily modified by jat */ +#define XI_MAGIC (('X' << 8) | 'i') +#define NG_MAGIC (('n' << 8) | 'g') +#define IN_MAGIC (('I' << 8) | 'n') +#define FO_MAGIC (('f' << 8) | 'o') + +enum xing_magic { + XING_MAGIC_XING, /* VBR */ + XING_MAGIC_INFO /* CBR */ +}; + +struct xing { + long flags; /* valid fields (see below) */ + unsigned long frames; /* total number of frames */ + unsigned long bytes; /* total number of bytes */ + unsigned char toc[100]; /* 100-point seek table */ + long scale; /* VBR quality */ + enum xing_magic magic; /* header magic */ +}; + +enum { + XING_FRAMES = 0x00000001L, + XING_BYTES = 0x00000002L, + XING_TOC = 0x00000004L, + XING_SCALE = 0x00000008L +}; + +struct lame_version { + unsigned major; + unsigned minor; +}; + +struct lame { + char encoder[10]; /* 9 byte encoder name/version ("LAME3.97b") */ + struct lame_version version; /* struct containing just the version */ + float peak; /* replaygain peak */ + float track_gain; /* replaygain track gain */ + float album_gain; /* replaygain album gain */ + int encoder_delay; /* # of added samples at start of mp3 */ + int encoder_padding; /* # of added samples at end of mp3 */ + int crc; /* CRC of the first 190 bytes of this frame */ +}; + +static bool +parse_xing(struct xing *xing, struct mad_bitptr *ptr, int *oldbitlen) +{ + unsigned long bits; + int bitlen; + int bitsleft; + int i; + + bitlen = *oldbitlen; + + if (bitlen < 16) + return false; + + bits = mad_bit_read(ptr, 16); + bitlen -= 16; + + if (bits == XI_MAGIC) { + if (bitlen < 16) + return false; + + if (mad_bit_read(ptr, 16) != NG_MAGIC) + return false; + + bitlen -= 16; + xing->magic = XING_MAGIC_XING; + } else if (bits == IN_MAGIC) { + if (bitlen < 16) + return false; + + if (mad_bit_read(ptr, 16) != FO_MAGIC) + return false; + + bitlen -= 16; + xing->magic = XING_MAGIC_INFO; + } + else if (bits == NG_MAGIC) xing->magic = XING_MAGIC_XING; + else if (bits == FO_MAGIC) xing->magic = XING_MAGIC_INFO; + else + return false; + + if (bitlen < 32) + return false; + xing->flags = mad_bit_read(ptr, 32); + bitlen -= 32; + + if (xing->flags & XING_FRAMES) { + if (bitlen < 32) + return false; + xing->frames = mad_bit_read(ptr, 32); + bitlen -= 32; + } + + if (xing->flags & XING_BYTES) { + if (bitlen < 32) + return false; + xing->bytes = mad_bit_read(ptr, 32); + bitlen -= 32; + } + + if (xing->flags & XING_TOC) { + if (bitlen < 800) + return false; + for (i = 0; i < 100; ++i) xing->toc[i] = mad_bit_read(ptr, 8); + bitlen -= 800; + } + + if (xing->flags & XING_SCALE) { + if (bitlen < 32) + return false; + xing->scale = mad_bit_read(ptr, 32); + bitlen -= 32; + } + + /* Make sure we consume no less than 120 bytes (960 bits) in hopes that + * the LAME tag is found there, and not right after the Xing header */ + bitsleft = 960 - ((*oldbitlen) - bitlen); + if (bitsleft < 0) + return false; + else if (bitsleft > 0) { + mad_bit_read(ptr, bitsleft); + bitlen -= bitsleft; + } + + *oldbitlen = bitlen; + + return true; +} + +static bool +parse_lame(struct lame *lame, struct mad_bitptr *ptr, int *bitlen) +{ + int adj = 0; + int name; + int orig; + int sign; + int gain; + int i; + + /* Unlike the xing header, the lame tag has a fixed length. Fail if + * not all 36 bytes (288 bits) are there. */ + if (*bitlen < 288) + return false; + + for (i = 0; i < 9; i++) + lame->encoder[i] = (char)mad_bit_read(ptr, 8); + lame->encoder[9] = '\0'; + + *bitlen -= 72; + + /* This is technically incorrect, since the encoder might not be lame. + * But there's no other way to determine if this is a lame tag, and we + * wouldn't want to go reading a tag that's not there. */ + if (!StringStartsWith(lame->encoder, "LAME")) + return false; + + if (sscanf(lame->encoder+4, "%u.%u", + &lame->version.major, &lame->version.minor) != 2) + return false; + + FormatDebug(mad_domain, "detected LAME version %i.%i (\"%s\")", + lame->version.major, lame->version.minor, lame->encoder); + + /* The reference volume was changed from the 83dB used in the + * ReplayGain spec to 89dB in lame 3.95.1. Bump the gain for older + * versions, since everyone else uses 89dB instead of 83dB. + * Unfortunately, lame didn't differentiate between 3.95 and 3.95.1, so + * it's impossible to make the proper adjustment for 3.95. + * Fortunately, 3.95 was only out for about a day before 3.95.1 was + * released. -- tmz */ + if (lame->version.major < 3 || + (lame->version.major == 3 && lame->version.minor < 95)) + adj = 6; + + mad_bit_read(ptr, 16); + + lame->peak = mad_f_todouble(mad_bit_read(ptr, 32) << 5); /* peak */ + FormatDebug(mad_domain, "LAME peak found: %f", lame->peak); + + lame->track_gain = 0; + name = mad_bit_read(ptr, 3); /* gain name */ + orig = mad_bit_read(ptr, 3); /* gain originator */ + sign = mad_bit_read(ptr, 1); /* sign bit */ + gain = mad_bit_read(ptr, 9); /* gain*10 */ + if (gain && name == 1 && orig != 0) { + lame->track_gain = ((sign ? -gain : gain) / 10.0) + adj; + FormatDebug(mad_domain, "LAME track gain found: %f", + lame->track_gain); + } + + /* tmz reports that this isn't currently written by any version of lame + * (as of 3.97). Since we have no way of testing it, don't use it. + * Wouldn't want to go blowing someone's ears just because we read it + * wrong. :P -- jat */ + lame->album_gain = 0; +#if 0 + name = mad_bit_read(ptr, 3); /* gain name */ + orig = mad_bit_read(ptr, 3); /* gain originator */ + sign = mad_bit_read(ptr, 1); /* sign bit */ + gain = mad_bit_read(ptr, 9); /* gain*10 */ + if (gain && name == 2 && orig != 0) { + lame->album_gain = ((sign ? -gain : gain) / 10.0) + adj; + FormatDebug(mad_domain, "LAME album gain found: %f", + lame->track_gain); + } +#else + mad_bit_read(ptr, 16); +#endif + + mad_bit_read(ptr, 16); + + lame->encoder_delay = mad_bit_read(ptr, 12); + lame->encoder_padding = mad_bit_read(ptr, 12); + + FormatDebug(mad_domain, "encoder delay is %i, encoder padding is %i", + lame->encoder_delay, lame->encoder_padding); + + mad_bit_read(ptr, 80); + + lame->crc = mad_bit_read(ptr, 16); + + *bitlen -= 216; + + return true; +} + +static inline float +mp3_frame_duration(const struct mad_frame *frame) +{ + return mad_timer_count(frame->header.duration, + MAD_UNITS_MILLISECONDS) / 1000.0; +} + +inline InputStream::offset_type +MadDecoder::ThisFrameOffset() const +{ + auto offset = input_stream.GetOffset(); + + if (stream.this_frame != nullptr) + offset -= stream.bufend - stream.this_frame; + else + offset -= stream.bufend - stream.buffer; + + return offset; +} + +inline InputStream::offset_type +MadDecoder::RestIncludingThisFrame() const +{ + return input_stream.GetSize() - ThisFrameOffset(); +} + +inline void +MadDecoder::FileSizeToSongLength() +{ + InputStream::offset_type rest = RestIncludingThisFrame(); + + if (rest > 0) { + float frame_duration = mp3_frame_duration(&frame); + + total_time = (rest * 8.0) / frame.header.bitrate; + max_frames = total_time / frame_duration + FRAMES_CUSHION; + } else { + max_frames = FRAMES_CUSHION; + total_time = 0; + } +} + +inline bool +MadDecoder::DecodeFirstFrame(Tag **tag) +{ + struct xing xing; + struct lame lame; + struct mad_bitptr ptr; + int bitlen; + enum mp3_action ret; + + /* stfu gcc */ + memset(&xing, 0, sizeof(struct xing)); + xing.flags = 0; + + while (true) { + do { + ret = DecodeNextFrameHeader(tag); + } while (ret == DECODE_CONT); + if (ret == DECODE_BREAK) + return false; + if (ret == DECODE_SKIP) continue; + + do { + ret = DecodeNextFrame(); + } while (ret == DECODE_CONT); + if (ret == DECODE_BREAK) + return false; + if (ret == DECODE_OK) break; + } + + ptr = stream.anc_ptr; + bitlen = stream.anc_bitlen; + + FileSizeToSongLength(); + + /* + * if an xing tag exists, use that! + */ + if (parse_xing(&xing, &ptr, &bitlen)) { + found_xing = true; + mute_frame = MUTEFRAME_SKIP; + + if ((xing.flags & XING_FRAMES) && xing.frames) { + mad_timer_t duration = frame.header.duration; + mad_timer_multiply(&duration, xing.frames); + total_time = ((float)mad_timer_count(duration, MAD_UNITS_MILLISECONDS)) / 1000; + max_frames = xing.frames; + } + + if (parse_lame(&lame, &ptr, &bitlen)) { + if (gapless_playback && input_stream.IsSeekable()) { + drop_start_samples = lame.encoder_delay + + DECODERDELAY; + drop_end_samples = lame.encoder_padding; + } + + /* Album gain isn't currently used. See comment in + * parse_lame() for details. -- jat */ + if (decoder != nullptr && !found_replay_gain && + lame.track_gain) { + ReplayGainInfo rgi; + rgi.Clear(); + rgi.tuples[REPLAY_GAIN_TRACK].gain = lame.track_gain; + rgi.tuples[REPLAY_GAIN_TRACK].peak = lame.peak; + decoder_replay_gain(*decoder, &rgi); + } + } + } + + if (!max_frames) + return false; + + if (max_frames > 8 * 1024 * 1024) { + FormatWarning(mad_domain, + "mp3 file header indicates too many frames: %lu", + max_frames); + return false; + } + + frame_offsets = new long[max_frames]; + times = new mad_timer_t[max_frames]; + + return true; +} + +MadDecoder::~MadDecoder() +{ + mad_synth_finish(&synth); + mad_frame_finish(&frame); + mad_stream_finish(&stream); + + delete[] frame_offsets; + delete[] times; +} + +/* this is primarily used for getting total time for tags */ +static int +mad_decoder_total_file_time(InputStream &is) +{ + MadDecoder data(nullptr, is); + return data.DecodeFirstFrame(nullptr) + ? data.total_time + 0.5 + : -1; +} + +long +MadDecoder::TimeToFrame(double t) const +{ + unsigned long i; + + for (i = 0; i < highest_frame; ++i) { + double frame_time = + mad_timer_count(times[i], + MAD_UNITS_MILLISECONDS) / 1000.; + if (frame_time >= t) + break; + } + + return i; +} + +void +MadDecoder::UpdateTimerNextFrame() +{ + if (current_frame >= highest_frame) { + /* record this frame's properties in frame_offsets + (for seeking) and times */ + bit_rate = frame.header.bitrate; + + if (current_frame >= max_frames) + /* cap current_frame */ + current_frame = max_frames - 1; + else + highest_frame++; + + frame_offsets[current_frame] = ThisFrameOffset(); + + mad_timer_add(&timer, frame.header.duration); + times[current_frame] = timer; + } else + /* get the new timer value from "times" */ + timer = times[current_frame]; + + current_frame++; + elapsed_time = mad_timer_count(timer, MAD_UNITS_MILLISECONDS) / 1000.0; +} + +DecoderCommand +MadDecoder::SendPCM(unsigned i, unsigned pcm_length) +{ + unsigned max_samples; + + max_samples = sizeof(output_buffer) / + sizeof(output_buffer[0]) / + MAD_NCHANNELS(&frame.header); + + while (i < pcm_length) { + unsigned int num_samples = pcm_length - i; + if (num_samples > max_samples) + num_samples = max_samples; + + i += num_samples; + + mad_fixed_to_24_buffer(output_buffer, &synth, + i - num_samples, i, + MAD_NCHANNELS(&frame.header)); + num_samples *= MAD_NCHANNELS(&frame.header); + + auto cmd = decoder_data(*decoder, input_stream, output_buffer, + sizeof(output_buffer[0]) * num_samples, + bit_rate / 1000); + if (cmd != DecoderCommand::NONE) + return cmd; + } + + return DecoderCommand::NONE; +} + +inline DecoderCommand +MadDecoder::SyncAndSend() +{ + mad_synth_frame(&synth, &frame); + + if (!found_first_frame) { + unsigned int samples_per_frame = synth.pcm.length; + drop_start_frames = drop_start_samples / samples_per_frame; + drop_end_frames = drop_end_samples / samples_per_frame; + drop_start_samples = drop_start_samples % samples_per_frame; + drop_end_samples = drop_end_samples % samples_per_frame; + found_first_frame = true; + } + + if (drop_start_frames > 0) { + drop_start_frames--; + return DecoderCommand::NONE; + } else if ((drop_end_frames > 0) && + (current_frame == (max_frames + 1 - drop_end_frames))) { + /* stop decoding, effectively dropping all remaining + frames */ + return DecoderCommand::STOP; + } + + unsigned i = 0; + if (!decoded_first_frame) { + i = drop_start_samples; + decoded_first_frame = true; + } + + unsigned pcm_length = synth.pcm.length; + if (drop_end_samples && + (current_frame == max_frames - drop_end_frames)) { + if (drop_end_samples >= pcm_length) + pcm_length = 0; + else + pcm_length -= drop_end_samples; + } + + auto cmd = SendPCM(i, pcm_length); + if (cmd != DecoderCommand::NONE) + return cmd; + + if (drop_end_samples && + (current_frame == max_frames - drop_end_frames)) + /* stop decoding, effectively dropping + * all remaining samples */ + return DecoderCommand::STOP; + + return DecoderCommand::NONE; +} + +inline bool +MadDecoder::Read() +{ + enum mp3_action ret; + + UpdateTimerNextFrame(); + + switch (mute_frame) { + DecoderCommand cmd; + + case MUTEFRAME_SKIP: + mute_frame = MUTEFRAME_NONE; + break; + case MUTEFRAME_SEEK: + if (elapsed_time >= seek_where) + mute_frame = MUTEFRAME_NONE; + break; + case MUTEFRAME_NONE: + cmd = SyncAndSend(); + if (cmd == DecoderCommand::SEEK) { + unsigned long j; + + assert(input_stream.IsSeekable()); + + j = TimeToFrame(decoder_seek_where(*decoder)); + if (j < highest_frame) { + if (Seek(frame_offsets[j])) { + current_frame = j; + decoder_command_finished(*decoder); + } else + decoder_seek_error(*decoder); + } else { + seek_where = decoder_seek_where(*decoder); + mute_frame = MUTEFRAME_SEEK; + decoder_command_finished(*decoder); + } + } else if (cmd != DecoderCommand::NONE) + return false; + } + + while (true) { + bool skip = false; + + do { + Tag *tag = nullptr; + + ret = DecodeNextFrameHeader(&tag); + + if (tag != nullptr) { + decoder_tag(*decoder, input_stream, + std::move(*tag)); + delete tag; + } + } while (ret == DECODE_CONT); + if (ret == DECODE_BREAK) + return false; + else if (ret == DECODE_SKIP) + skip = true; + + if (mute_frame == MUTEFRAME_NONE) { + do { + ret = DecodeNextFrame(); + } while (ret == DECODE_CONT); + if (ret == DECODE_BREAK) + return false; + } + + if (!skip && ret == DECODE_OK) + break; + } + + return ret != DECODE_BREAK; +} + +static void +mp3_decode(Decoder &decoder, InputStream &input_stream) +{ + MadDecoder data(&decoder, input_stream); + + Tag *tag = nullptr; + if (!data.DecodeFirstFrame(&tag)) { + delete tag; + + if (decoder_get_command(decoder) == DecoderCommand::NONE) + LogError(mad_domain, + "Input does not appear to be a mp3 bit stream"); + return; + } + + Error error; + AudioFormat audio_format; + if (!audio_format_init_checked(audio_format, + data.frame.header.samplerate, + SampleFormat::S24_P32, + MAD_NCHANNELS(&data.frame.header), + error)) { + LogError(error); + delete tag; + return; + } + + decoder_initialized(decoder, audio_format, + input_stream.IsSeekable(), + data.total_time); + + if (tag != nullptr) { + decoder_tag(decoder, input_stream, std::move(*tag)); + delete tag; + } + + while (data.Read()) {} +} + +static bool +mad_decoder_scan_stream(InputStream &is, + const struct tag_handler *handler, void *handler_ctx) +{ + int total_time; + + total_time = mad_decoder_total_file_time(is); + if (total_time < 0) + return false; + + tag_handler_invoke_duration(handler, handler_ctx, total_time); + return true; +} + +static const char *const mp3_suffixes[] = { "mp3", "mp2", nullptr }; +static const char *const mp3_mime_types[] = { "audio/mpeg", nullptr }; + +const struct DecoderPlugin mad_decoder_plugin = { + "mad", + mp3_plugin_init, + nullptr, + mp3_decode, + nullptr, + nullptr, + mad_decoder_scan_stream, + nullptr, + mp3_suffixes, + mp3_mime_types, +}; diff --git a/src/decoder/plugins/MadDecoderPlugin.hxx b/src/decoder/plugins/MadDecoderPlugin.hxx new file mode 100644 index 000000000..eb2a10d6f --- /dev/null +++ b/src/decoder/plugins/MadDecoderPlugin.hxx @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_DECODER_MAD_HXX +#define MPD_DECODER_MAD_HXX + +extern const struct DecoderPlugin mad_decoder_plugin; + +#endif diff --git a/src/decoder/plugins/MikmodDecoderPlugin.cxx b/src/decoder/plugins/MikmodDecoderPlugin.cxx new file mode 100644 index 000000000..aacb5922d --- /dev/null +++ b/src/decoder/plugins/MikmodDecoderPlugin.cxx @@ -0,0 +1,248 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "MikmodDecoderPlugin.hxx" +#include "../DecoderAPI.hxx" +#include "tag/TagHandler.hxx" +#include "system/FatalError.hxx" +#include "util/Domain.hxx" +#include "Log.hxx" + +#include + +#include + +static constexpr Domain mikmod_domain("mikmod"); + +/* this is largely copied from alsaplayer */ + +static constexpr size_t MIKMOD_FRAME_SIZE = 4096; + +static BOOL +mikmod_mpd_init(void) +{ + return VC_Init(); +} + +static void +mikmod_mpd_exit(void) +{ + VC_Exit(); +} + +static void +mikmod_mpd_update(void) +{ +} + +static BOOL +mikmod_mpd_is_present(void) +{ + return true; +} + +static char drv_name[] = PACKAGE_NAME; +static char drv_version[] = VERSION; + +#if (LIBMIKMOD_VERSION > 0x030106) +static char drv_alias[] = PACKAGE; +#endif + +static MDRIVER drv_mpd = { + nullptr, + drv_name, + drv_version, + 0, + 255, +#if (LIBMIKMOD_VERSION > 0x030106) + drv_alias, +#if (LIBMIKMOD_VERSION >= 0x030200) + nullptr, /* CmdLineHelp */ +#endif + nullptr, /* CommandLine */ +#endif + mikmod_mpd_is_present, + VC_SampleLoad, + VC_SampleUnload, + VC_SampleSpace, + VC_SampleLength, + mikmod_mpd_init, + mikmod_mpd_exit, + nullptr, + VC_SetNumVoices, + VC_PlayStart, + VC_PlayStop, + mikmod_mpd_update, + nullptr, + VC_VoiceSetVolume, + VC_VoiceGetVolume, + VC_VoiceSetFrequency, + VC_VoiceGetFrequency, + VC_VoiceSetPanning, + VC_VoiceGetPanning, + VC_VoicePlay, + VC_VoiceStop, + VC_VoiceStopped, + VC_VoiceGetPosition, + VC_VoiceRealVolume +}; + +static bool mikmod_loop; +static unsigned mikmod_sample_rate; + +static bool +mikmod_decoder_init(const config_param ¶m) +{ + static char params[] = ""; + + mikmod_loop = param.GetBlockValue("loop", false); + mikmod_sample_rate = param.GetBlockValue("sample_rate", 44100u); + if (!audio_valid_sample_rate(mikmod_sample_rate)) + FormatFatalError("Invalid sample rate in line %d: %u", + param.line, mikmod_sample_rate); + + md_device = 0; + md_reverb = 0; + + MikMod_RegisterDriver(&drv_mpd); + MikMod_RegisterAllLoaders(); + + md_pansep = 64; + md_mixfreq = mikmod_sample_rate; + md_mode = (DMODE_SOFT_MUSIC | DMODE_INTERP | DMODE_STEREO | + DMODE_16BITS); + + if (MikMod_Init(params)) { + FormatError(mikmod_domain, + "Could not init MikMod: %s", + MikMod_strerror(MikMod_errno)); + return false; + } + + return true; +} + +static void +mikmod_decoder_finish(void) +{ + MikMod_Exit(); +} + +static void +mikmod_decoder_file_decode(Decoder &decoder, const char *path_fs) +{ + /* deconstify the path because libmikmod wants a non-const + string pointer */ + char *const path2 = const_cast(path_fs); + + MODULE *handle; + int ret; + SBYTE buffer[MIKMOD_FRAME_SIZE]; + + handle = Player_Load(path2, 128, 0); + + if (handle == nullptr) { + FormatError(mikmod_domain, + "failed to open mod: %s", path_fs); + return; + } + + handle->loop = mikmod_loop; + + const AudioFormat audio_format(mikmod_sample_rate, SampleFormat::S16, 2); + assert(audio_format.IsValid()); + + decoder_initialized(decoder, audio_format, false, 0); + + Player_Start(handle); + + DecoderCommand cmd = DecoderCommand::NONE; + while (cmd == DecoderCommand::NONE && Player_Active()) { + ret = VC_WriteBytes(buffer, sizeof(buffer)); + cmd = decoder_data(decoder, nullptr, buffer, ret, 0); + } + + Player_Stop(); + Player_Free(handle); +} + +static bool +mikmod_decoder_scan_file(const char *path_fs, + const struct tag_handler *handler, void *handler_ctx) +{ + /* deconstify the path because libmikmod wants a non-const + string pointer */ + char *const path2 = const_cast(path_fs); + + MODULE *handle = Player_Load(path2, 128, 0); + + if (handle == nullptr) { + FormatDebug(mikmod_domain, + "Failed to open file: %s", path_fs); + return false; + } + + Player_Free(handle); + + char *title = Player_LoadTitle(path2); + if (title != nullptr) { + tag_handler_invoke_tag(handler, handler_ctx, + TAG_TITLE, title); +#if (LIBMIKMOD_VERSION >= 0x030200) + MikMod_free(title); +#else + free(title); +#endif + } + + return true; +} + +static const char *const mikmod_decoder_suffixes[] = { + "amf", + "dsm", + "far", + "gdm", + "imf", + "it", + "med", + "mod", + "mtm", + "s3m", + "stm", + "stx", + "ult", + "uni", + "xm", + nullptr +}; + +const struct DecoderPlugin mikmod_decoder_plugin = { + "mikmod", + mikmod_decoder_init, + mikmod_decoder_finish, + nullptr, + mikmod_decoder_file_decode, + mikmod_decoder_scan_file, + nullptr, + nullptr, + mikmod_decoder_suffixes, + nullptr, +}; diff --git a/src/decoder/plugins/MikmodDecoderPlugin.hxx b/src/decoder/plugins/MikmodDecoderPlugin.hxx new file mode 100644 index 000000000..27ba2a823 --- /dev/null +++ b/src/decoder/plugins/MikmodDecoderPlugin.hxx @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_DECODER_MIKMOD_HXX +#define MPD_DECODER_MIKMOD_HXX + +extern const struct DecoderPlugin mikmod_decoder_plugin; + +#endif diff --git a/src/decoder/plugins/ModplugDecoderPlugin.cxx b/src/decoder/plugins/ModplugDecoderPlugin.cxx new file mode 100644 index 000000000..29c452f73 --- /dev/null +++ b/src/decoder/plugins/ModplugDecoderPlugin.cxx @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "ModplugDecoderPlugin.hxx" +#include "../DecoderAPI.hxx" +#include "InputStream.hxx" +#include "tag/TagHandler.hxx" +#include "system/FatalError.hxx" +#include "util/WritableBuffer.hxx" +#include "util/Domain.hxx" +#include "Log.hxx" + +#include + + +#include + +static constexpr Domain modplug_domain("modplug"); + +static constexpr size_t MODPLUG_FRAME_SIZE = 4096; +static constexpr size_t MODPLUG_PREALLOC_BLOCK = 256 * 1024; +static constexpr InputStream::offset_type MODPLUG_FILE_LIMIT = 100 * 1024 * 1024; + +static int modplug_loop_count; + +static bool +modplug_decoder_init(const config_param ¶m) +{ + modplug_loop_count = param.GetBlockValue("loop_count", 0); + if (modplug_loop_count < -1) + FormatFatalError("Invalid loop count in line %d: %i", + param.line, modplug_loop_count); + + return true; +} + +static WritableBuffer +mod_loadfile(Decoder *decoder, InputStream &is) +{ + const InputStream::offset_type size = is.GetSize(); + + if (size == 0) { + LogWarning(modplug_domain, "file is empty"); + return { nullptr, 0 }; + } + + if (size > MODPLUG_FILE_LIMIT) { + LogWarning(modplug_domain, "file too large"); + return { nullptr, 0 }; + } + + //known/unknown size, preallocate array, lets read in chunks + + const bool is_stream = size < 0; + + WritableBuffer buffer; + buffer.size = is_stream ? MODPLUG_PREALLOC_BLOCK : size; + buffer.data = new uint8_t[buffer.size]; + + uint8_t *const end = buffer.end(); + uint8_t *p = buffer.begin(); + + while (true) { + size_t ret = decoder_read(decoder, is, p, end - p); + if (ret == 0) { + if (is.LockIsEOF()) + /* end of file */ + break; + + /* I/O error - skip this song */ + delete[] buffer.data; + buffer.data = nullptr; + return buffer; + } + + p += ret; + if (p == end) { + if (!is_stream) + break; + + LogWarning(modplug_domain, "stream too large"); + delete[] buffer.data; + buffer.data = nullptr; + return buffer; + } + } + + buffer.size = p - buffer.data; + return buffer; +} + +static ModPlugFile * +LoadModPlugFile(Decoder *decoder, InputStream &is) +{ + const auto buffer = mod_loadfile(decoder, is); + if (buffer.IsNull()) { + LogWarning(modplug_domain, "could not load stream"); + return nullptr; + } + + ModPlugFile *f = ModPlug_Load(buffer.data, buffer.size); + delete[] buffer.data; + return f; +} + +static void +mod_decode(Decoder &decoder, InputStream &is) +{ + ModPlug_Settings settings; + int ret; + char audio_buffer[MODPLUG_FRAME_SIZE]; + + ModPlug_GetSettings(&settings); + /* alter setting */ + settings.mResamplingMode = MODPLUG_RESAMPLE_FIR; /* RESAMP */ + settings.mChannels = 2; + settings.mBits = 16; + settings.mFrequency = 44100; + settings.mLoopCount = modplug_loop_count; + /* insert more setting changes here */ + ModPlug_SetSettings(&settings); + + ModPlugFile *f = LoadModPlugFile(&decoder, is); + if (f == nullptr) { + LogWarning(modplug_domain, "could not decode stream"); + return; + } + + static constexpr AudioFormat audio_format(44100, SampleFormat::S16, 2); + assert(audio_format.IsValid()); + + decoder_initialized(decoder, audio_format, + is.IsSeekable(), + ModPlug_GetLength(f) / 1000.0); + + DecoderCommand cmd; + do { + ret = ModPlug_Read(f, audio_buffer, MODPLUG_FRAME_SIZE); + if (ret <= 0) + break; + + cmd = decoder_data(decoder, nullptr, + audio_buffer, ret, + 0); + + if (cmd == DecoderCommand::SEEK) { + float where = decoder_seek_where(decoder); + + ModPlug_Seek(f, (int)(where * 1000.0)); + + decoder_command_finished(decoder); + } + + } while (cmd != DecoderCommand::STOP); + + ModPlug_Unload(f); +} + +static bool +modplug_scan_stream(InputStream &is, + const struct tag_handler *handler, void *handler_ctx) +{ + ModPlugFile *f = LoadModPlugFile(nullptr, is); + if (f == nullptr) + return false; + + tag_handler_invoke_duration(handler, handler_ctx, + ModPlug_GetLength(f) / 1000); + + const char *title = ModPlug_GetName(f); + if (title != nullptr) + tag_handler_invoke_tag(handler, handler_ctx, + TAG_TITLE, title); + + ModPlug_Unload(f); + + return true; +} + +static const char *const mod_suffixes[] = { + "669", "amf", "ams", "dbm", "dfm", "dsm", "far", "it", + "med", "mdl", "mod", "mtm", "mt2", "okt", "s3m", "stm", + "ult", "umx", "xm", + nullptr +}; + +const struct DecoderPlugin modplug_decoder_plugin = { + "modplug", + modplug_decoder_init, + nullptr, + mod_decode, + nullptr, + nullptr, + modplug_scan_stream, + nullptr, + mod_suffixes, + nullptr, +}; diff --git a/src/decoder/plugins/ModplugDecoderPlugin.hxx b/src/decoder/plugins/ModplugDecoderPlugin.hxx new file mode 100644 index 000000000..08f2ecb12 --- /dev/null +++ b/src/decoder/plugins/ModplugDecoderPlugin.hxx @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_DECODER_MODPLUG_HXX +#define MPD_DECODER_MODPLUG_HXX + +extern const struct DecoderPlugin modplug_decoder_plugin; + +#endif diff --git a/src/decoder/plugins/MpcdecDecoderPlugin.cxx b/src/decoder/plugins/MpcdecDecoderPlugin.cxx new file mode 100644 index 000000000..21596ddf7 --- /dev/null +++ b/src/decoder/plugins/MpcdecDecoderPlugin.cxx @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "MpcdecDecoderPlugin.hxx" +#include "../DecoderAPI.hxx" +#include "InputStream.hxx" +#include "CheckAudioFormat.hxx" +#include "tag/TagHandler.hxx" +#include "util/Error.hxx" +#include "util/Domain.hxx" +#include "util/Macros.hxx" +#include "Log.hxx" + +#include + +#include + +struct mpc_decoder_data { + InputStream &is; + Decoder *decoder; + + mpc_decoder_data(InputStream &_is, Decoder *_decoder) + :is(_is), decoder(_decoder) {} +}; + +static constexpr Domain mpcdec_domain("mpcdec"); + +static mpc_int32_t +mpc_read_cb(mpc_reader *reader, void *ptr, mpc_int32_t size) +{ + struct mpc_decoder_data *data = + (struct mpc_decoder_data *)reader->data; + + return decoder_read(data->decoder, data->is, ptr, size); +} + +static mpc_bool_t +mpc_seek_cb(mpc_reader *reader, mpc_int32_t offset) +{ + struct mpc_decoder_data *data = + (struct mpc_decoder_data *)reader->data; + + return data->is.LockSeek(offset, SEEK_SET, IgnoreError()); +} + +static mpc_int32_t +mpc_tell_cb(mpc_reader *reader) +{ + struct mpc_decoder_data *data = + (struct mpc_decoder_data *)reader->data; + + return (long)data->is.GetOffset(); +} + +static mpc_bool_t +mpc_canseek_cb(mpc_reader *reader) +{ + struct mpc_decoder_data *data = + (struct mpc_decoder_data *)reader->data; + + return data->is.IsSeekable(); +} + +static mpc_int32_t +mpc_getsize_cb(mpc_reader *reader) +{ + struct mpc_decoder_data *data = + (struct mpc_decoder_data *)reader->data; + + return data->is.GetSize(); +} + +/* this _looks_ performance-critical, don't de-inline -- eric */ +static inline int32_t +mpc_to_mpd_sample(MPC_SAMPLE_FORMAT sample) +{ + /* only doing 16-bit audio for now */ + int32_t val; + + enum { + bits = 24, + }; + + const int clip_min = -1 << (bits - 1); + const int clip_max = (1 << (bits - 1)) - 1; + +#ifdef MPC_FIXED_POINT + const int shift = bits - MPC_FIXED_POINT_SCALE_SHIFT; + + if (shift < 0) + val = sample >> -shift; + else + val = sample << shift; +#else + const int float_scale = 1 << (bits - 1); + + val = sample * float_scale; +#endif + + if (val < clip_min) + val = clip_min; + else if (val > clip_max) + val = clip_max; + + return val; +} + +static void +mpc_to_mpd_buffer(int32_t *dest, const MPC_SAMPLE_FORMAT *src, + unsigned num_samples) +{ + while (num_samples-- > 0) + *dest++ = mpc_to_mpd_sample(*src++); +} + +static void +mpcdec_decode(Decoder &mpd_decoder, InputStream &is) +{ + MPC_SAMPLE_FORMAT sample_buffer[MPC_DECODER_BUFFER_LENGTH]; + + mpc_decoder_data data(is, &mpd_decoder); + + mpc_reader reader; + reader.read = mpc_read_cb; + reader.seek = mpc_seek_cb; + reader.tell = mpc_tell_cb; + reader.get_size = mpc_getsize_cb; + reader.canseek = mpc_canseek_cb; + reader.data = &data; + + mpc_demux *demux = mpc_demux_init(&reader); + if (demux == nullptr) { + if (decoder_get_command(mpd_decoder) != DecoderCommand::STOP) + LogWarning(mpcdec_domain, + "Not a valid musepack stream"); + return; + } + + mpc_streaminfo info; + mpc_demux_get_info(demux, &info); + + Error error; + AudioFormat audio_format; + if (!audio_format_init_checked(audio_format, info.sample_freq, + SampleFormat::S24_P32, + info.channels, error)) { + LogError(error); + mpc_demux_exit(demux); + return; + } + + ReplayGainInfo rgi; + rgi.Clear(); + rgi.tuples[REPLAY_GAIN_ALBUM].gain = MPC_OLD_GAIN_REF - (info.gain_album / 256.); + rgi.tuples[REPLAY_GAIN_ALBUM].peak = pow(10, info.peak_album / 256. / 20) / 32767; + rgi.tuples[REPLAY_GAIN_TRACK].gain = MPC_OLD_GAIN_REF - (info.gain_title / 256.); + rgi.tuples[REPLAY_GAIN_TRACK].peak = pow(10, info.peak_title / 256. / 20) / 32767; + + decoder_replay_gain(mpd_decoder, &rgi); + + decoder_initialized(mpd_decoder, audio_format, + is.IsSeekable(), + mpc_streaminfo_get_length(&info)); + + DecoderCommand cmd = DecoderCommand::NONE; + do { + if (cmd == DecoderCommand::SEEK) { + mpc_int64_t where = decoder_seek_where(mpd_decoder) * + audio_format.sample_rate; + bool success; + + success = mpc_demux_seek_sample(demux, where) + == MPC_STATUS_OK; + if (success) + decoder_command_finished(mpd_decoder); + else + decoder_seek_error(mpd_decoder); + } + + mpc_uint32_t vbr_update_bits = 0; + + mpc_frame_info frame; + frame.buffer = (MPC_SAMPLE_FORMAT *)sample_buffer; + mpc_status status = mpc_demux_decode(demux, &frame); + if (status != MPC_STATUS_OK) { + LogWarning(mpcdec_domain, + "Failed to decode sample"); + break; + } + + if (frame.bits == -1) + break; + + mpc_uint32_t ret = frame.samples; + ret *= info.channels; + + int32_t chunk[ARRAY_SIZE(sample_buffer)]; + mpc_to_mpd_buffer(chunk, sample_buffer, ret); + + long bit_rate = vbr_update_bits * audio_format.sample_rate + / 1152 / 1000; + + cmd = decoder_data(mpd_decoder, is, + chunk, ret * sizeof(chunk[0]), + bit_rate); + } while (cmd != DecoderCommand::STOP); + + mpc_demux_exit(demux); +} + +static float +mpcdec_get_file_duration(InputStream &is) +{ + mpc_decoder_data data(is, nullptr); + + mpc_reader reader; + reader.read = mpc_read_cb; + reader.seek = mpc_seek_cb; + reader.tell = mpc_tell_cb; + reader.get_size = mpc_getsize_cb; + reader.canseek = mpc_canseek_cb; + reader.data = &data; + + mpc_demux *demux = mpc_demux_init(&reader); + if (demux == nullptr) + return -1; + + mpc_streaminfo info; + mpc_demux_get_info(demux, &info); + mpc_demux_exit(demux); + + return mpc_streaminfo_get_length(&info); +} + +static bool +mpcdec_scan_stream(InputStream &is, + const struct tag_handler *handler, void *handler_ctx) +{ + float total_time = mpcdec_get_file_duration(is); + + if (total_time < 0) + return false; + + tag_handler_invoke_duration(handler, handler_ctx, total_time); + return true; +} + +static const char *const mpcdec_suffixes[] = { "mpc", nullptr }; + +const struct DecoderPlugin mpcdec_decoder_plugin = { + "mpcdec", + nullptr, + nullptr, + mpcdec_decode, + nullptr, + nullptr, + mpcdec_scan_stream, + nullptr, + mpcdec_suffixes, + nullptr, +}; diff --git a/src/decoder/plugins/MpcdecDecoderPlugin.hxx b/src/decoder/plugins/MpcdecDecoderPlugin.hxx new file mode 100644 index 000000000..7f71311fa --- /dev/null +++ b/src/decoder/plugins/MpcdecDecoderPlugin.hxx @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_DECODER_MPCDEC_HXX +#define MPD_DECODER_MPCDEC_HXX + +extern const struct DecoderPlugin mpcdec_decoder_plugin; + +#endif diff --git a/src/decoder/plugins/Mpg123DecoderPlugin.cxx b/src/decoder/plugins/Mpg123DecoderPlugin.cxx new file mode 100644 index 000000000..cf8573c85 --- /dev/null +++ b/src/decoder/plugins/Mpg123DecoderPlugin.cxx @@ -0,0 +1,255 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" /* must be first for large file support */ +#include "Mpg123DecoderPlugin.hxx" +#include "../DecoderAPI.hxx" +#include "CheckAudioFormat.hxx" +#include "tag/TagHandler.hxx" +#include "util/Error.hxx" +#include "util/Domain.hxx" +#include "Log.hxx" + +#include + +#include + +static constexpr Domain mpg123_domain("mpg123"); + +static bool +mpd_mpg123_init(gcc_unused const config_param ¶m) +{ + mpg123_init(); + + return true; +} + +static void +mpd_mpg123_finish(void) +{ + mpg123_exit(); +} + +/** + * Opens a file with an existing #mpg123_handle. + * + * @param handle a handle which was created before; on error, this + * function will not free it + * @param audio_format this parameter is filled after successful + * return + * @return true on success + */ +static bool +mpd_mpg123_open(mpg123_handle *handle, const char *path_fs, + AudioFormat &audio_format) +{ + int error; + int channels, encoding; + long rate; + + /* mpg123_open() wants a writable string :-( */ + char *const path2 = const_cast(path_fs); + + error = mpg123_open(handle, path2); + if (error != MPG123_OK) { + FormatWarning(mpg123_domain, + "libmpg123 failed to open %s: %s", + path_fs, mpg123_plain_strerror(error)); + return false; + } + + /* obtain the audio format */ + + error = mpg123_getformat(handle, &rate, &channels, &encoding); + if (error != MPG123_OK) { + FormatWarning(mpg123_domain, + "mpg123_getformat() failed: %s", + mpg123_plain_strerror(error)); + return false; + } + + if (encoding != MPG123_ENC_SIGNED_16) { + /* other formats not yet implemented */ + FormatWarning(mpg123_domain, + "expected MPG123_ENC_SIGNED_16, got %d", + encoding); + return false; + } + + Error error2; + if (!audio_format_init_checked(audio_format, rate, SampleFormat::S16, + channels, error2)) { + LogError(error2); + return false; + } + + return true; +} + +static void +mpd_mpg123_file_decode(Decoder &decoder, const char *path_fs) +{ + mpg123_handle *handle; + int error; + off_t num_samples; + struct mpg123_frameinfo info; + + /* open the file */ + + handle = mpg123_new(nullptr, &error); + if (handle == nullptr) { + FormatError(mpg123_domain, + "mpg123_new() failed: %s", + mpg123_plain_strerror(error)); + return; + } + + AudioFormat audio_format; + if (!mpd_mpg123_open(handle, path_fs, audio_format)) { + mpg123_delete(handle); + return; + } + + num_samples = mpg123_length(handle); + + /* tell MPD core we're ready */ + + decoder_initialized(decoder, audio_format, true, + (float)num_samples / + (float)audio_format.sample_rate); + + if (mpg123_info(handle, &info) != MPG123_OK) { + info.vbr = MPG123_CBR; + info.bitrate = 0; + } + + switch (info.vbr) { + case MPG123_ABR: + info.bitrate = info.abr_rate; + break; + case MPG123_CBR: + break; + default: + info.bitrate = 0; + } + + /* the decoder main loop */ + + DecoderCommand cmd; + do { + unsigned char buffer[8192]; + size_t nbytes; + + /* decode */ + + error = mpg123_read(handle, buffer, sizeof(buffer), &nbytes); + if (error != MPG123_OK) { + if (error != MPG123_DONE) + FormatWarning(mpg123_domain, + "mpg123_read() failed: %s", + mpg123_plain_strerror(error)); + break; + } + + /* update bitrate for ABR/VBR */ + if (info.vbr != MPG123_CBR) { + /* FIXME: maybe skip, as too expensive? */ + /* FIXME: maybe, (info.vbr == MPG123_VBR) ? */ + if (mpg123_info (handle, &info) != MPG123_OK) + info.bitrate = 0; + } + + /* send to MPD */ + + cmd = decoder_data(decoder, nullptr, buffer, nbytes, info.bitrate); + + if (cmd == DecoderCommand::SEEK) { + off_t c = decoder_seek_where(decoder)*audio_format.sample_rate; + c = mpg123_seek(handle, c, SEEK_SET); + if (c < 0) + decoder_seek_error(decoder); + else { + decoder_command_finished(decoder); + decoder_timestamp(decoder, c/(double)audio_format.sample_rate); + } + + cmd = DecoderCommand::NONE; + } + } while (cmd == DecoderCommand::NONE); + + /* cleanup */ + + mpg123_delete(handle); +} + +static bool +mpd_mpg123_scan_file(const char *path_fs, + const struct tag_handler *handler, void *handler_ctx) +{ + mpg123_handle *handle; + int error; + off_t num_samples; + + handle = mpg123_new(nullptr, &error); + if (handle == nullptr) { + FormatError(mpg123_domain, + "mpg123_new() failed: %s", + mpg123_plain_strerror(error)); + return false; + } + + AudioFormat audio_format; + if (!mpd_mpg123_open(handle, path_fs, audio_format)) { + mpg123_delete(handle); + return false; + } + + num_samples = mpg123_length(handle); + if (num_samples <= 0) { + mpg123_delete(handle); + return false; + } + + /* ID3 tag support not yet implemented */ + + mpg123_delete(handle); + + tag_handler_invoke_duration(handler, handler_ctx, + num_samples / audio_format.sample_rate); + return true; +} + +static const char *const mpg123_suffixes[] = { + "mp3", + nullptr +}; + +const struct DecoderPlugin mpg123_decoder_plugin = { + "mpg123", + mpd_mpg123_init, + mpd_mpg123_finish, + /* streaming not yet implemented */ + nullptr, + mpd_mpg123_file_decode, + mpd_mpg123_scan_file, + nullptr, + nullptr, + mpg123_suffixes, + nullptr, +}; diff --git a/src/decoder/plugins/Mpg123DecoderPlugin.hxx b/src/decoder/plugins/Mpg123DecoderPlugin.hxx new file mode 100644 index 000000000..fd089c6a4 --- /dev/null +++ b/src/decoder/plugins/Mpg123DecoderPlugin.hxx @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_DECODER_MPG123_HXX +#define MPD_DECODER_MPG123_HXX + +extern const struct DecoderPlugin mpg123_decoder_plugin; + +#endif diff --git a/src/decoder/plugins/OggCodec.cxx b/src/decoder/plugins/OggCodec.cxx new file mode 100644 index 000000000..c7f39586e --- /dev/null +++ b/src/decoder/plugins/OggCodec.cxx @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +/* + * Common functions used for Ogg data streams (Ogg-Vorbis and OggFLAC) + */ + +#include "config.h" +#include "OggCodec.hxx" +#include "../DecoderAPI.hxx" + +#include + +enum ogg_codec +ogg_codec_detect(Decoder *decoder, InputStream &is) +{ + /* oggflac detection based on code in ogg123 and this post + * http://lists.xiph.org/pipermail/flac/2004-December/000393.html + * ogg123 trunk still doesn't have this patch as of June 2005 */ + unsigned char buf[41]; + size_t r = decoder_read(decoder, is, buf, sizeof(buf)); + if (r < sizeof(buf) || memcmp(buf, "OggS", 4) != 0) + return OGG_CODEC_UNKNOWN; + + if ((memcmp(buf + 29, "FLAC", 4) == 0 && + memcmp(buf + 37, "fLaC", 4) == 0) || + memcmp(buf + 28, "FLAC", 4) == 0 || + memcmp(buf + 28, "fLaC", 4) == 0) + return OGG_CODEC_FLAC; + + if (memcmp(buf + 28, "Opus", 4) == 0) + return OGG_CODEC_OPUS; + + return OGG_CODEC_VORBIS; +} diff --git a/src/decoder/plugins/OggCodec.hxx b/src/decoder/plugins/OggCodec.hxx new file mode 100644 index 000000000..6643d85ad --- /dev/null +++ b/src/decoder/plugins/OggCodec.hxx @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +/* + * Common functions used for Ogg data streams (Ogg-Vorbis and OggFLAC) + */ + +#ifndef MPD_OGG_CODEC_HXX +#define MPD_OGG_CODEC_HXX + +struct Decoder; +struct InputStream; + +enum ogg_codec { + OGG_CODEC_UNKNOWN, + OGG_CODEC_VORBIS, + OGG_CODEC_FLAC, + OGG_CODEC_OPUS, +}; + +enum ogg_codec +ogg_codec_detect(Decoder *decoder, InputStream &is); + +#endif /* _OGG_COMMON_H */ diff --git a/src/decoder/plugins/OggFind.cxx b/src/decoder/plugins/OggFind.cxx new file mode 100644 index 000000000..df4318da2 --- /dev/null +++ b/src/decoder/plugins/OggFind.cxx @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "OggFind.hxx" +#include "OggSyncState.hxx" +#include "util/Error.hxx" + +#include + +bool +OggFindEOS(OggSyncState &oy, ogg_stream_state &os, ogg_packet &packet) +{ + while (true) { + int r = ogg_stream_packetout(&os, &packet); + if (r == 0) { + if (!oy.ExpectPageIn(os)) + return false; + + continue; + } else if (r > 0 && packet.e_o_s) + return true; + } +} + +bool +OggSeekPageAtOffset(OggSyncState &oy, ogg_stream_state &os, InputStream &is, + InputStream::offset_type offset, int whence) +{ + oy.Reset(); + + /* reset the stream to clear any previous partial packet + data */ + ogg_stream_reset(&os); + + return is.LockSeek(offset, whence, IgnoreError()) && + oy.ExpectPageSeekIn(os); +} + +bool +OggSeekFindEOS(OggSyncState &oy, ogg_stream_state &os, ogg_packet &packet, + InputStream &is) +{ + if (is.size > 0 && is.size - is.offset < 65536) + return OggFindEOS(oy, os, packet); + + if (!is.CheapSeeking()) + return false; + + return OggSeekPageAtOffset(oy, os, is, -65536, SEEK_END) && + OggFindEOS(oy, os, packet); +} diff --git a/src/decoder/plugins/OggFind.hxx b/src/decoder/plugins/OggFind.hxx new file mode 100644 index 000000000..71b53323b --- /dev/null +++ b/src/decoder/plugins/OggFind.hxx @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_OGG_FIND_HXX +#define MPD_OGG_FIND_HXX + +#include "check.h" +#include "InputStream.hxx" + +#include + +class OggSyncState; + +/** + * Skip all pages/packets until an end-of-stream (EOS) packet for the + * specified stream is found. + * + * @return true if the EOS packet was found + */ +bool +OggFindEOS(OggSyncState &oy, ogg_stream_state &os, ogg_packet &packet); + +/** + * Seek the #InputStream and find the next Ogg page. + */ +bool +OggSeekPageAtOffset(OggSyncState &oy, ogg_stream_state &os, InputStream &is, + InputStream::offset_type offset, int whence); + +/** + * Try to find the end-of-stream (EOS) packet. Seek to the end of the + * file if necessary. + * + * @return true if the EOS packet was found + */ +bool +OggSeekFindEOS(OggSyncState &oy, ogg_stream_state &os, ogg_packet &packet, + InputStream &is); + +#endif diff --git a/src/decoder/plugins/OggSyncState.hxx b/src/decoder/plugins/OggSyncState.hxx new file mode 100644 index 000000000..024902fff --- /dev/null +++ b/src/decoder/plugins/OggSyncState.hxx @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_OGG_SYNC_STATE_HXX +#define MPD_OGG_SYNC_STATE_HXX + +#include "check.h" +#include "OggUtil.hxx" + +#include + +#include + +/** + * Wrapper for an ogg_sync_state. + */ +class OggSyncState { + ogg_sync_state oy; + + InputStream &is; + Decoder *const decoder; + +public: + OggSyncState(InputStream &_is, Decoder *const _decoder=nullptr) + :is(_is), decoder(_decoder) { + ogg_sync_init(&oy); + } + + ~OggSyncState() { + ogg_sync_clear(&oy); + } + + void Reset() { + ogg_sync_reset(&oy); + } + + bool Feed(size_t size) { + return OggFeed(oy, decoder, is, size); + } + + bool ExpectPage(ogg_page &page) { + return OggExpectPage(oy, page, decoder, is); + } + + bool ExpectFirstPage(ogg_stream_state &os) { + return OggExpectFirstPage(oy, os, decoder, is); + } + + bool ExpectPageIn(ogg_stream_state &os) { + return OggExpectPageIn(oy, os, decoder, is); + } + + bool ExpectPageSeek(ogg_page &page) { + return OggExpectPageSeek(oy, page, decoder, is); + } + + bool ExpectPageSeekIn(ogg_stream_state &os) { + return OggExpectPageSeekIn(oy, os, decoder, is); + } +}; + +#endif diff --git a/src/decoder/plugins/OggUtil.cxx b/src/decoder/plugins/OggUtil.cxx new file mode 100644 index 000000000..3d8a26f0b --- /dev/null +++ b/src/decoder/plugins/OggUtil.cxx @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "OggUtil.hxx" +#include "../DecoderAPI.hxx" + +bool +OggFeed(ogg_sync_state &oy, Decoder *decoder, + InputStream &input_stream, size_t size) +{ + char *buffer = ogg_sync_buffer(&oy, size); + if (buffer == nullptr) + return false; + + size_t nbytes = decoder_read(decoder, input_stream, + buffer, size); + if (nbytes == 0) + return false; + + ogg_sync_wrote(&oy, nbytes); + return true; +} + +bool +OggExpectPage(ogg_sync_state &oy, ogg_page &page, + Decoder *decoder, InputStream &input_stream) +{ + while (true) { + int r = ogg_sync_pageout(&oy, &page); + if (r != 0) + return r > 0; + + if (!OggFeed(oy, decoder, input_stream, 1024)) + return false; + } +} + +bool +OggExpectFirstPage(ogg_sync_state &oy, ogg_stream_state &os, + Decoder *decoder, InputStream &is) +{ + ogg_page page; + if (!OggExpectPage(oy, page, decoder, is)) + return false; + + ogg_stream_init(&os, ogg_page_serialno(&page)); + ogg_stream_pagein(&os, &page); + return true; +} + +bool +OggExpectPageIn(ogg_sync_state &oy, ogg_stream_state &os, + Decoder *decoder, InputStream &is) +{ + ogg_page page; + if (!OggExpectPage(oy, page, decoder, is)) + return false; + + ogg_stream_pagein(&os, &page); + return true; +} + +bool +OggExpectPageSeek(ogg_sync_state &oy, ogg_page &page, + Decoder *decoder, InputStream &input_stream) +{ + size_t remaining_skipped = 16384; + + while (true) { + int r = ogg_sync_pageseek(&oy, &page); + if (r > 0) + return true; + + if (r < 0) { + /* skipped -r bytes */ + size_t nbytes = -r; + if (nbytes > remaining_skipped) + /* still no ogg page - we lost our + patience, abort */ + return false; + + remaining_skipped -= nbytes; + continue; + } + + if (!OggFeed(oy, decoder, input_stream, 1024)) + return false; + } +} + +bool +OggExpectPageSeekIn(ogg_sync_state &oy, ogg_stream_state &os, + Decoder *decoder, InputStream &is) +{ + ogg_page page; + if (!OggExpectPageSeek(oy, page, decoder, is)) + return false; + + ogg_stream_pagein(&os, &page); + return true; +} diff --git a/src/decoder/plugins/OggUtil.hxx b/src/decoder/plugins/OggUtil.hxx new file mode 100644 index 000000000..3289702a3 --- /dev/null +++ b/src/decoder/plugins/OggUtil.hxx @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_OGG_UTIL_HXX +#define MPD_OGG_UTIL_HXX + +#include "check.h" + +#include + +#include + +struct InputStream; +struct Decoder; + +/** + * Feed data from the #InputStream into the #ogg_sync_state. + * + * @return false on error or end-of-file + */ +bool +OggFeed(ogg_sync_state &oy, Decoder *decoder, InputStream &is, + size_t size); + +/** + * Feed into the #ogg_sync_state until a page gets available. Garbage + * data at the beginning is considered a fatal error. + * + * @return true if a page is available + */ +bool +OggExpectPage(ogg_sync_state &oy, ogg_page &page, + Decoder *decoder, InputStream &is); + +/** + * Combines OggExpectPage(), ogg_stream_init() and + * ogg_stream_pagein(). + * + * @return true if the stream was initialized and the first page was + * delivered to it + */ +bool +OggExpectFirstPage(ogg_sync_state &oy, ogg_stream_state &os, + Decoder *decoder, InputStream &is); + +/** + * Combines OggExpectPage() and ogg_stream_pagein(). + * + * @return true if a page was delivered to the stream + */ +bool +OggExpectPageIn(ogg_sync_state &oy, ogg_stream_state &os, + Decoder *decoder, InputStream &is); + +/** + * Like OggExpectPage(), but allow skipping garbage (after seeking). + */ +bool +OggExpectPageSeek(ogg_sync_state &oy, ogg_page &page, + Decoder *decoder, InputStream &is); + +/** + * Combines OggExpectPageSeek() and ogg_stream_pagein(). + * + * @return true if a page was delivered to the stream + */ +bool +OggExpectPageSeekIn(ogg_sync_state &oy, ogg_stream_state &os, + Decoder *decoder, InputStream &is); + +#endif diff --git a/src/decoder/plugins/OpusDecoderPlugin.cxx b/src/decoder/plugins/OpusDecoderPlugin.cxx new file mode 100644 index 000000000..15a17eb01 --- /dev/null +++ b/src/decoder/plugins/OpusDecoderPlugin.cxx @@ -0,0 +1,485 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" /* must be first for large file support */ +#include "OpusDecoderPlugin.h" +#include "OpusDomain.hxx" +#include "OpusHead.hxx" +#include "OpusTags.hxx" +#include "OggFind.hxx" +#include "OggSyncState.hxx" +#include "../DecoderAPI.hxx" +#include "OggCodec.hxx" +#include "tag/TagHandler.hxx" +#include "tag/TagBuilder.hxx" +#include "InputStream.hxx" +#include "util/Error.hxx" +#include "Log.hxx" + +#include +#include + +#include + +#include + +static constexpr opus_int32 opus_sample_rate = 48000; + +gcc_pure +static bool +IsOpusHead(const ogg_packet &packet) +{ + return packet.bytes >= 8 && memcmp(packet.packet, "OpusHead", 8) == 0; +} + +gcc_pure +static bool +IsOpusTags(const ogg_packet &packet) +{ + return packet.bytes >= 8 && memcmp(packet.packet, "OpusTags", 8) == 0; +} + +static bool +mpd_opus_init(gcc_unused const config_param ¶m) +{ + LogDebug(opus_domain, opus_get_version_string()); + + return true; +} + +class MPDOpusDecoder { + Decoder &decoder; + InputStream &input_stream; + + ogg_stream_state os; + + OpusDecoder *opus_decoder; + opus_int16 *output_buffer; + unsigned output_size; + + bool os_initialized; + bool found_opus; + + int opus_serialno; + + ogg_int64_t eos_granulepos; + + size_t frame_size; + +public: + MPDOpusDecoder(Decoder &_decoder, + InputStream &_input_stream) + :decoder(_decoder), input_stream(_input_stream), + opus_decoder(nullptr), + output_buffer(nullptr), output_size(0), + os_initialized(false), found_opus(false) {} + ~MPDOpusDecoder(); + + bool ReadFirstPage(OggSyncState &oy); + bool ReadNextPage(OggSyncState &oy); + + DecoderCommand HandlePackets(); + DecoderCommand HandlePacket(const ogg_packet &packet); + DecoderCommand HandleBOS(const ogg_packet &packet); + DecoderCommand HandleTags(const ogg_packet &packet); + DecoderCommand HandleAudio(const ogg_packet &packet); + + bool Seek(OggSyncState &oy, double where); +}; + +MPDOpusDecoder::~MPDOpusDecoder() +{ + g_free(output_buffer); + + if (opus_decoder != nullptr) + opus_decoder_destroy(opus_decoder); + + if (os_initialized) + ogg_stream_clear(&os); +} + +inline bool +MPDOpusDecoder::ReadFirstPage(OggSyncState &oy) +{ + assert(!os_initialized); + + if (!oy.ExpectFirstPage(os)) + return false; + + os_initialized = true; + return true; +} + +inline bool +MPDOpusDecoder::ReadNextPage(OggSyncState &oy) +{ + assert(os_initialized); + + ogg_page page; + if (!oy.ExpectPage(page)) + return false; + + const auto page_serialno = ogg_page_serialno(&page); + if (page_serialno != os.serialno) + ogg_stream_reset_serialno(&os, page_serialno); + + ogg_stream_pagein(&os, &page); + return true; +} + +inline DecoderCommand +MPDOpusDecoder::HandlePackets() +{ + ogg_packet packet; + while (ogg_stream_packetout(&os, &packet) == 1) { + auto cmd = HandlePacket(packet); + if (cmd != DecoderCommand::NONE) + return cmd; + } + + return DecoderCommand::NONE; +} + +inline DecoderCommand +MPDOpusDecoder::HandlePacket(const ogg_packet &packet) +{ + if (packet.e_o_s) + return DecoderCommand::STOP; + + if (packet.b_o_s) + return HandleBOS(packet); + else if (!found_opus) + return DecoderCommand::STOP; + + if (IsOpusTags(packet)) + return HandleTags(packet); + + return HandleAudio(packet); +} + +/** + * Load the end-of-stream packet and restore the previous file + * position. + */ +static bool +LoadEOSPacket(InputStream &is, Decoder *decoder, int serialno, + ogg_packet &packet) +{ + if (!is.CheapSeeking()) + /* we do this for local files only, because seeking + around remote files is expensive and not worth the + troubl */ + return -1; + + const auto old_offset = is.offset; + if (old_offset < 0) + return -1; + + /* create temporary Ogg objects for seeking and parsing the + EOS packet */ + OggSyncState oy(is, decoder); + ogg_stream_state os; + ogg_stream_init(&os, serialno); + + bool result = OggSeekFindEOS(oy, os, packet, is); + ogg_stream_clear(&os); + + /* restore the previous file position */ + is.Seek(old_offset, SEEK_SET, IgnoreError()); + + return result; +} + +/** + * Load the end-of-stream granulepos and restore the previous file + * position. + * + * @return -1 on error + */ +gcc_pure +static ogg_int64_t +LoadEOSGranulePos(InputStream &is, Decoder *decoder, int serialno) +{ + ogg_packet packet; + if (!LoadEOSPacket(is, decoder, serialno, packet)) + return -1; + + return packet.granulepos; +} + +inline DecoderCommand +MPDOpusDecoder::HandleBOS(const ogg_packet &packet) +{ + assert(packet.b_o_s); + + if (found_opus || !IsOpusHead(packet)) + return DecoderCommand::STOP; + + unsigned channels; + if (!ScanOpusHeader(packet.packet, packet.bytes, channels) || + !audio_valid_channel_count(channels)) + return DecoderCommand::STOP; + + assert(opus_decoder == nullptr); + assert(output_buffer == nullptr); + + opus_serialno = os.serialno; + found_opus = true; + + /* TODO: parse attributes from the OpusHead (sample rate, + channels, ...) */ + + int opus_error; + opus_decoder = opus_decoder_create(opus_sample_rate, channels, + &opus_error); + if (opus_decoder == nullptr) { + FormatError(opus_domain, "libopus error: %s", + opus_strerror(opus_error)); + return DecoderCommand::STOP; + } + + eos_granulepos = LoadEOSGranulePos(input_stream, &decoder, + opus_serialno); + const double duration = eos_granulepos >= 0 + ? double(eos_granulepos) / opus_sample_rate + : -1.0; + + const AudioFormat audio_format(opus_sample_rate, + SampleFormat::S16, channels); + decoder_initialized(decoder, audio_format, + eos_granulepos > 0, duration); + frame_size = audio_format.GetFrameSize(); + + /* allocate an output buffer for 16 bit PCM samples big enough + to hold a quarter second, larger than 120ms required by + libopus */ + output_size = audio_format.sample_rate / 4; + output_buffer = (opus_int16 *) + g_malloc(sizeof(*output_buffer) * output_size * + audio_format.channels); + + return decoder_get_command(decoder); +} + +inline DecoderCommand +MPDOpusDecoder::HandleTags(const ogg_packet &packet) +{ + ReplayGainInfo rgi; + rgi.Clear(); + + TagBuilder tag_builder; + + DecoderCommand cmd; + if (ScanOpusTags(packet.packet, packet.bytes, + &rgi, + &add_tag_handler, &tag_builder) && + !tag_builder.IsEmpty()) { + decoder_replay_gain(decoder, &rgi); + + Tag tag = tag_builder.Commit(); + cmd = decoder_tag(decoder, input_stream, std::move(tag)); + } else + cmd = decoder_get_command(decoder); + + return cmd; +} + +inline DecoderCommand +MPDOpusDecoder::HandleAudio(const ogg_packet &packet) +{ + assert(opus_decoder != nullptr); + + int nframes = opus_decode(opus_decoder, + (const unsigned char*)packet.packet, + packet.bytes, + output_buffer, output_size, + 0); + if (nframes < 0) { + LogError(opus_domain, opus_strerror(nframes)); + return DecoderCommand::STOP; + } + + if (nframes > 0) { + const size_t nbytes = nframes * frame_size; + auto cmd = decoder_data(decoder, input_stream, + output_buffer, nbytes, + 0); + if (cmd != DecoderCommand::NONE) + return cmd; + + if (packet.granulepos > 0) + decoder_timestamp(decoder, + double(packet.granulepos) + / opus_sample_rate); + } + + return DecoderCommand::NONE; +} + +bool +MPDOpusDecoder::Seek(OggSyncState &oy, double where_s) +{ + assert(eos_granulepos > 0); + assert(input_stream.seekable); + assert(input_stream.size > 0); + assert(input_stream.offset >= 0); + + const ogg_int64_t where_granulepos(where_s * opus_sample_rate); + + /* interpolate the file offset where we expect to find the + given granule position */ + /* TODO: implement binary search */ + InputStream::offset_type offset(where_granulepos * input_stream.size + / eos_granulepos); + + if (!OggSeekPageAtOffset(oy, os, input_stream, offset, SEEK_SET)) + return false; + + decoder_timestamp(decoder, where_s); + return true; +} + +static void +mpd_opus_stream_decode(Decoder &decoder, + InputStream &input_stream) +{ + if (ogg_codec_detect(&decoder, input_stream) != OGG_CODEC_OPUS) + return; + + /* rewind the stream, because ogg_codec_detect() has + moved it */ + input_stream.LockRewind(IgnoreError()); + + MPDOpusDecoder d(decoder, input_stream); + OggSyncState oy(input_stream, &decoder); + + if (!d.ReadFirstPage(oy)) + return; + + while (true) { + auto cmd = d.HandlePackets(); + if (cmd == DecoderCommand::SEEK) { + if (d.Seek(oy, decoder_seek_where(decoder))) + decoder_command_finished(decoder); + else + decoder_seek_error(decoder); + + continue; + } + + if (cmd != DecoderCommand::NONE) + break; + + if (!d.ReadNextPage(oy)) + break; + } +} + +static bool +mpd_opus_scan_stream(InputStream &is, + const struct tag_handler *handler, void *handler_ctx) +{ + OggSyncState oy(is); + + ogg_stream_state os; + if (!oy.ExpectFirstPage(os)) + return false; + + /* read at most two more pages */ + unsigned remaining_pages = 2; + + bool result = false; + + ogg_packet packet; + while (true) { + int r = ogg_stream_packetout(&os, &packet); + if (r < 0) { + result = false; + break; + } + + if (r == 0) { + if (remaining_pages-- == 0) + break; + + if (!oy.ExpectPageIn(os)) { + result = false; + break; + } + + continue; + } + + if (packet.b_o_s) { + if (!IsOpusHead(packet)) + break; + + unsigned channels; + if (!ScanOpusHeader(packet.packet, packet.bytes, channels) || + !audio_valid_channel_count(channels)) { + result = false; + break; + } + + result = true; + } else if (!result) + break; + else if (IsOpusTags(packet)) { + if (!ScanOpusTags(packet.packet, packet.bytes, + nullptr, + handler, handler_ctx)) + result = false; + + break; + } + } + + if (packet.e_o_s || OggSeekFindEOS(oy, os, packet, is)) + tag_handler_invoke_duration(handler, handler_ctx, + packet.granulepos / opus_sample_rate); + + ogg_stream_clear(&os); + + return result; +} + +static const char *const opus_suffixes[] = { + "opus", + "ogg", + "oga", + nullptr +}; + +static const char *const opus_mime_types[] = { + "audio/opus", + nullptr +}; + +const struct DecoderPlugin opus_decoder_plugin = { + "opus", + mpd_opus_init, + nullptr, + mpd_opus_stream_decode, + nullptr, + nullptr, + mpd_opus_scan_stream, + nullptr, + opus_suffixes, + opus_mime_types, +}; diff --git a/src/decoder/plugins/OpusDecoderPlugin.h b/src/decoder/plugins/OpusDecoderPlugin.h new file mode 100644 index 000000000..260dab99a --- /dev/null +++ b/src/decoder/plugins/OpusDecoderPlugin.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_DECODER_OPUS_H +#define MPD_DECODER_OPUS_H + +extern const struct DecoderPlugin opus_decoder_plugin; + +#endif diff --git a/src/decoder/plugins/OpusDomain.cxx b/src/decoder/plugins/OpusDomain.cxx new file mode 100644 index 000000000..1efd64a48 --- /dev/null +++ b/src/decoder/plugins/OpusDomain.cxx @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "OpusDomain.hxx" +#include "util/Domain.hxx" + +const Domain opus_domain("opus"); diff --git a/src/decoder/plugins/OpusDomain.hxx b/src/decoder/plugins/OpusDomain.hxx new file mode 100644 index 000000000..fb19e0301 --- /dev/null +++ b/src/decoder/plugins/OpusDomain.hxx @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_OPUS_DOMAIN_HXX +#define MPD_OPUS_DOMAIN_HXX + +#include "check.h" + +extern const class Domain opus_domain; + +#endif diff --git a/src/decoder/plugins/OpusHead.cxx b/src/decoder/plugins/OpusHead.cxx new file mode 100644 index 000000000..bfa41d618 --- /dev/null +++ b/src/decoder/plugins/OpusHead.cxx @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "OpusHead.hxx" + +#include + +struct OpusHead { + char signature[8]; + uint8_t version, channels; + uint16_t pre_skip; + uint32_t sample_rate; + uint16_t output_gain; + uint8_t channel_mapping; +}; + +bool +ScanOpusHeader(const void *data, size_t size, unsigned &channels_r) +{ + const OpusHead *h = (const OpusHead *)data; + if (size < 19 || (h->version & 0xf0) != 0) + return false; + + channels_r = h->channels; + return true; +} diff --git a/src/decoder/plugins/OpusHead.hxx b/src/decoder/plugins/OpusHead.hxx new file mode 100644 index 000000000..c478d8d90 --- /dev/null +++ b/src/decoder/plugins/OpusHead.hxx @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_OPUS_HEAD_HXX +#define MPD_OPUS_HEAD_HXX + +#include "check.h" + +#include + +bool +ScanOpusHeader(const void *data, size_t size, unsigned &channels_r); + +#endif diff --git a/src/decoder/plugins/OpusReader.hxx b/src/decoder/plugins/OpusReader.hxx new file mode 100644 index 000000000..c5b8e9107 --- /dev/null +++ b/src/decoder/plugins/OpusReader.hxx @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_OPUS_READER_HXX +#define MPD_OPUS_READER_HXX + +#include "check.h" + +#include + +#include +#include + +class OpusReader { + const uint8_t *p, *const end; + +public: + OpusReader(const void *_p, size_t size) + :p((const uint8_t *)_p), end(p + size) {} + + bool Skip(size_t length) { + p += length; + return p <= end; + } + + const void *Read(size_t length) { + const uint8_t *result = p; + return Skip(length) + ? result + : nullptr; + } + + bool Expect(const void *value, size_t length) { + const void *data = Read(length); + return data != nullptr && memcmp(value, data, length) == 0; + } + + bool ReadByte(uint8_t &value_r) { + if (p >= end) + return false; + + value_r = *p++; + return true; + } + + bool ReadShort(uint16_t &value_r) { + const uint8_t *value = (const uint8_t *)Read(sizeof(value_r)); + if (value == nullptr) + return false; + + value_r = value[0] | (value[1] << 8); + return true; + } + + bool ReadWord(uint32_t &value_r) { + const uint8_t *value = (const uint8_t *)Read(sizeof(value_r)); + if (value == nullptr) + return false; + + value_r = value[0] | (value[1] << 8) + | (value[2] << 16) | (value[3] << 24); + return true; + } + + bool SkipString() { + uint32_t length; + return ReadWord(length) && Skip(length); + } + + char *ReadString() { + uint32_t length; + if (!ReadWord(length)) + return nullptr; + + const char *src = (const char *)Read(length); + if (src == nullptr) + return nullptr; + + char *dest = new char[length + 1]; + *std::copy_n(src, length, dest) = 0; + return dest; + } +}; + +#endif diff --git a/src/decoder/plugins/OpusTags.cxx b/src/decoder/plugins/OpusTags.cxx new file mode 100644 index 000000000..aff5479c0 --- /dev/null +++ b/src/decoder/plugins/OpusTags.cxx @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "OpusTags.hxx" +#include "OpusReader.hxx" +#include "XiphTags.hxx" +#include "tag/TagHandler.hxx" +#include "tag/Tag.hxx" +#include "ReplayGainInfo.hxx" + +#include +#include +#include + +gcc_pure +static TagType +ParseOpusTagName(const char *name) +{ + TagType type = tag_name_parse_i(name); + if (type != TAG_NUM_OF_ITEM_TYPES) + return type; + + return tag_table_lookup_i(xiph_tags, name); +} + +static void +ScanOneOpusTag(const char *name, const char *value, + ReplayGainInfo *rgi, + const struct tag_handler *handler, void *ctx) +{ + if (rgi != nullptr && strcmp(name, "R128_TRACK_GAIN") == 0) { + /* R128_TRACK_GAIN is a Q7.8 fixed point number in + dB */ + + char *endptr; + long l = strtol(value, &endptr, 10); + if (endptr > value && *endptr == 0) + rgi->tuples[REPLAY_GAIN_TRACK].gain = double(l) / 256.; + } + + tag_handler_invoke_pair(handler, ctx, name, value); + + if (handler->tag != nullptr) { + TagType t = ParseOpusTagName(name); + if (t != TAG_NUM_OF_ITEM_TYPES) + tag_handler_invoke_tag(handler, ctx, t, value); + } +} + +bool +ScanOpusTags(const void *data, size_t size, + ReplayGainInfo *rgi, + const struct tag_handler *handler, void *ctx) +{ + OpusReader r(data, size); + if (!r.Expect("OpusTags", 8)) + return false; + + if (handler->pair == nullptr && handler->tag == nullptr) + return true; + + if (!r.SkipString()) + return false; + + uint32_t n; + if (!r.ReadWord(n)) + return false; + + while (n-- > 0) { + char *p = r.ReadString(); + if (p == nullptr) + return false; + + char *eq = strchr(p, '='); + if (eq != nullptr && eq > p) { + *eq = 0; + + ScanOneOpusTag(p, eq + 1, rgi, handler, ctx); + } + + delete[] p; + } + + return true; +} diff --git a/src/decoder/plugins/OpusTags.hxx b/src/decoder/plugins/OpusTags.hxx new file mode 100644 index 000000000..be3ac3a8d --- /dev/null +++ b/src/decoder/plugins/OpusTags.hxx @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_OPUS_TAGS_HXX +#define MPD_OPUS_TAGS_HXX + +#include "check.h" + +#include + +struct ReplayGainInfo; + +bool +ScanOpusTags(const void *data, size_t size, + ReplayGainInfo *rgi, + const struct tag_handler *handler, void *ctx); + +#endif diff --git a/src/decoder/plugins/PcmDecoderPlugin.cxx b/src/decoder/plugins/PcmDecoderPlugin.cxx new file mode 100644 index 000000000..d92aecd51 --- /dev/null +++ b/src/decoder/plugins/PcmDecoderPlugin.cxx @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "PcmDecoderPlugin.hxx" +#include "../DecoderAPI.hxx" +#include "InputStream.hxx" +#include "util/Error.hxx" +#include "util/ByteReverse.hxx" +#include "Log.hxx" + +#include +#include /* for SEEK_SET */ + +static void +pcm_stream_decode(Decoder &decoder, InputStream &is) +{ + static constexpr AudioFormat audio_format = { + 44100, + SampleFormat::S16, + 2, + }; + + const char *const mime = is.GetMimeType(); + const bool reverse_endian = mime != nullptr && + strcmp(mime, "audio/x-mpd-cdda-pcm-reverse") == 0; + + const double time_to_size = audio_format.GetTimeToSize(); + + float total_time = -1; + const auto size = is.GetSize(); + if (size >= 0) + total_time = size / time_to_size; + + decoder_initialized(decoder, audio_format, + is.IsSeekable(), total_time); + + DecoderCommand cmd; + do { + char buffer[4096]; + + size_t nbytes = decoder_read(decoder, is, + buffer, sizeof(buffer)); + + if (nbytes == 0 && is.LockIsEOF()) + break; + + if (reverse_endian) + /* make sure we deliver samples in host byte order */ + reverse_bytes_16((uint16_t *)buffer, + (uint16_t *)buffer, + (uint16_t *)(buffer + nbytes)); + + cmd = nbytes > 0 + ? decoder_data(decoder, is, + buffer, nbytes, 0) + : decoder_get_command(decoder); + if (cmd == DecoderCommand::SEEK) { + InputStream::offset_type offset(time_to_size * + decoder_seek_where(decoder)); + + Error error; + if (is.LockSeek(offset, SEEK_SET, error)) { + decoder_command_finished(decoder); + } else { + LogError(error); + decoder_seek_error(decoder); + } + + cmd = DecoderCommand::NONE; + } + } while (cmd == DecoderCommand::NONE); +} + +static const char *const pcm_mime_types[] = { + /* for streams obtained by the cdio_paranoia input plugin */ + "audio/x-mpd-cdda-pcm", + + /* same as above, but with reverse byte order */ + "audio/x-mpd-cdda-pcm-reverse", + + nullptr +}; + +const struct DecoderPlugin pcm_decoder_plugin = { + "pcm", + nullptr, + nullptr, + pcm_stream_decode, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + pcm_mime_types, +}; diff --git a/src/decoder/plugins/PcmDecoderPlugin.hxx b/src/decoder/plugins/PcmDecoderPlugin.hxx new file mode 100644 index 000000000..3582e5856 --- /dev/null +++ b/src/decoder/plugins/PcmDecoderPlugin.hxx @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +/** \file + * + * Not really a decoder; this plugin forwards its input data "as-is". + * + * It was written only to support the "cdio_paranoia" input plugin, + * which does not need a decoder. + */ + +#ifndef MPD_DECODER_PCM_HXX +#define MPD_DECODER_PCM_HXX + +extern const struct DecoderPlugin pcm_decoder_plugin; + +#endif diff --git a/src/decoder/plugins/SidplayDecoderPlugin.cxx b/src/decoder/plugins/SidplayDecoderPlugin.cxx new file mode 100644 index 000000000..8b3e3f8c0 --- /dev/null +++ b/src/decoder/plugins/SidplayDecoderPlugin.cxx @@ -0,0 +1,436 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "SidplayDecoderPlugin.hxx" +#include "../DecoderAPI.hxx" +#include "tag/TagHandler.hxx" +#include "util/Alloc.hxx" +#include "util/Domain.hxx" +#include "system/ByteOrder.hxx" +#include "Log.hxx" + +#include +#include +#include +#include + +#include +#include +#include + +#define SUBTUNE_PREFIX "tune_" + +static constexpr Domain sidplay_domain("sidplay"); + +static GPatternSpec *path_with_subtune; +static const char *songlength_file; +static GKeyFile *songlength_database; + +static bool all_files_are_containers; +static unsigned default_songlength; + +static bool filter_setting; + +static GKeyFile * +sidplay_load_songlength_db(const char *path) +{ + GError *error = nullptr; + gchar *data; + gsize size; + + if (!g_file_get_contents(path, &data, &size, &error)) { + FormatError(sidplay_domain, + "unable to read songlengths file %s: %s", + path, error->message); + g_error_free(error); + return nullptr; + } + + /* replace any ; comment characters with # */ + for (gsize i = 0; i < size; i++) + if (data[i] == ';') + data[i] = '#'; + + GKeyFile *db = g_key_file_new(); + bool success = g_key_file_load_from_data(db, data, size, + G_KEY_FILE_NONE, &error); + g_free(data); + if (!success) { + FormatError(sidplay_domain, + "unable to parse songlengths file %s: %s", + path, error->message); + g_error_free(error); + g_key_file_free(db); + return nullptr; + } + + g_key_file_set_list_separator(db, ' '); + return db; +} + +static bool +sidplay_init(const config_param ¶m) +{ + /* read the songlengths database file */ + songlength_file = param.GetBlockValue("songlength_database"); + if (songlength_file != nullptr) + songlength_database = sidplay_load_songlength_db(songlength_file); + + default_songlength = param.GetBlockValue("default_songlength", 0u); + + all_files_are_containers = + param.GetBlockValue("all_files_are_containers", true); + + path_with_subtune=g_pattern_spec_new( + "*/" SUBTUNE_PREFIX "???.sid"); + + filter_setting = param.GetBlockValue("filter", true); + + return true; +} + +static void +sidplay_finish() +{ + g_pattern_spec_free(path_with_subtune); + + if(songlength_database) + g_key_file_free(songlength_database); +} + +/** + * returns the file path stripped of any /tune_xxx.sid subtune + * suffix + */ +static char * +get_container_name(const char *path_fs) +{ + char *path_container = strdup(path_fs); + + if(!g_pattern_match(path_with_subtune, + strlen(path_container), path_container, nullptr)) + return path_container; + + char *ptr=g_strrstr(path_container, "/" SUBTUNE_PREFIX); + if(ptr) *ptr='\0'; + + return path_container; +} + +/** + * returns tune number from file.sid/tune_xxx.sid style path or 1 if + * no subtune is appended + */ +static unsigned +get_song_num(const char *path_fs) +{ + if(g_pattern_match(path_with_subtune, + strlen(path_fs), path_fs, nullptr)) { + char *sub=g_strrstr(path_fs, "/" SUBTUNE_PREFIX); + if(!sub) return 1; + + sub+=strlen("/" SUBTUNE_PREFIX); + int song_num=strtol(sub, nullptr, 10); + + if (errno == EINVAL) + return 1; + else + return song_num; + } else + return 1; +} + +/* get the song length in seconds */ +static int +get_song_length(const char *path_fs) +{ + if (songlength_database == nullptr) + return -1; + + char *sid_file = get_container_name(path_fs); + SidTuneMod tune(sid_file); + free(sid_file); + if(!tune) { + LogWarning(sidplay_domain, + "failed to load file for calculating md5 sum"); + return -1; + } + char md5sum[SIDTUNE_MD5_LENGTH+1]; + tune.createMD5(md5sum); + + const unsigned song_num = get_song_num(path_fs); + + gsize num_items; + gchar **values=g_key_file_get_string_list(songlength_database, + "Database", md5sum, &num_items, nullptr); + if(!values || song_num>num_items) { + g_strfreev(values); + return -1; + } + + int minutes=strtol(values[song_num-1], nullptr, 10); + if(errno==EINVAL) minutes=0; + + int seconds; + char *ptr=strchr(values[song_num-1], ':'); + if(ptr) { + seconds=strtol(ptr+1, nullptr, 10); + if(errno==EINVAL) seconds=0; + } else + seconds=0; + + g_strfreev(values); + + return (minutes*60)+seconds; +} + +static void +sidplay_file_decode(Decoder &decoder, const char *path_fs) +{ + int channels; + + /* load the tune */ + + char *path_container=get_container_name(path_fs); + SidTune tune(path_container, nullptr, true); + free(path_container); + if (!tune) { + LogWarning(sidplay_domain, "failed to load file"); + return; + } + + int song_num=get_song_num(path_fs); + tune.selectSong(song_num); + + int song_len=get_song_length(path_fs); + if(song_len==-1) song_len=default_songlength; + + /* initialize the player */ + + sidplay2 player; + int iret = player.load(&tune); + if (iret != 0) { + FormatWarning(sidplay_domain, + "sidplay2.load() failed: %s", player.error()); + return; + } + + /* initialize the builder */ + + ReSIDBuilder builder("ReSID"); + if (!builder) { + LogWarning(sidplay_domain, + "failed to initialize ReSIDBuilder"); + return; + } + + builder.create(player.info().maxsids); + if (!builder) { + LogWarning(sidplay_domain, "ReSIDBuilder.create() failed"); + return; + } + + builder.filter(filter_setting); + if (!builder) { + LogWarning(sidplay_domain, "ReSIDBuilder.filter() failed"); + return; + } + + /* configure the player */ + + sid2_config_t config = player.config(); + + config.clockDefault = SID2_CLOCK_PAL; + config.clockForced = true; + config.clockSpeed = SID2_CLOCK_CORRECT; + config.frequency = 48000; + config.optimisation = SID2_DEFAULT_OPTIMISATION; + + config.precision = 16; + config.sidDefault = SID2_MOS6581; + config.sidEmulation = &builder; + config.sidModel = SID2_MODEL_CORRECT; + config.sidSamples = true; + config.sampleFormat = IsLittleEndian() + ? SID2_LITTLE_SIGNED + : SID2_BIG_SIGNED; + if (tune.isStereo()) { + config.playback = sid2_stereo; + channels = 2; + } else { + config.playback = sid2_mono; + channels = 1; + } + + iret = player.config(config); + if (iret != 0) { + FormatWarning(sidplay_domain, + "sidplay2.config() failed: %s", player.error()); + return; + } + + /* initialize the MPD decoder */ + + const AudioFormat audio_format(48000, SampleFormat::S16, channels); + assert(audio_format.IsValid()); + + decoder_initialized(decoder, audio_format, true, (float)song_len); + + /* .. and play */ + + const unsigned timebase = player.timebase(); + song_len *= timebase; + + DecoderCommand cmd; + do { + char buffer[4096]; + size_t nbytes; + + nbytes = player.play(buffer, sizeof(buffer)); + if (nbytes == 0) + break; + + decoder_timestamp(decoder, (double)player.time() / timebase); + + cmd = decoder_data(decoder, nullptr, buffer, nbytes, 0); + + if (cmd == DecoderCommand::SEEK) { + unsigned data_time = player.time(); + unsigned target_time = (unsigned) + (decoder_seek_where(decoder) * timebase); + + /* can't rewind so return to zero and seek forward */ + if(target_time 0 && player.time() >= (unsigned)song_len) + break; + + } while (cmd != DecoderCommand::STOP); +} + +static bool +sidplay_scan_file(const char *path_fs, + const struct tag_handler *handler, void *handler_ctx) +{ + int song_num=get_song_num(path_fs); + char *path_container=get_container_name(path_fs); + + SidTune tune(path_container, nullptr, true); + free(path_container); + if (!tune) + return false; + + const SidTuneInfo &info = tune.getInfo(); + + /* title */ + const char *title; + if (info.numberOfInfoStrings > 0 && info.infoString[0] != nullptr) + title=info.infoString[0]; + else + title=""; + + if(info.songs>1) { + char tag_title[1024]; + snprintf(tag_title, sizeof(tag_title), + "%s (%d/%d)", + title, song_num, info.songs); + tag_handler_invoke_tag(handler, handler_ctx, + TAG_TITLE, tag_title); + } else + tag_handler_invoke_tag(handler, handler_ctx, TAG_TITLE, title); + + /* artist */ + if (info.numberOfInfoStrings > 1 && info.infoString[1] != nullptr) + tag_handler_invoke_tag(handler, handler_ctx, TAG_ARTIST, + info.infoString[1]); + + /* track */ + char track[16]; + sprintf(track, "%d", song_num); + tag_handler_invoke_tag(handler, handler_ctx, TAG_TRACK, track); + + /* time */ + int song_len=get_song_length(path_fs); + if (song_len >= 0) + tag_handler_invoke_duration(handler, handler_ctx, song_len); + + return true; +} + +static char * +sidplay_container_scan(const char *path_fs, const unsigned int tnum) +{ + SidTune tune(path_fs, nullptr, true); + if (!tune) + return nullptr; + + const SidTuneInfo &info=tune.getInfo(); + + /* Don't treat sids containing a single tune + as containers */ + if(!all_files_are_containers && info.songs<2) + return nullptr; + + /* Construct container/tune path names, eg. + Delta.sid/tune_001.sid */ + if(tnum<=info.songs) { + char *subtune= g_strdup_printf( + SUBTUNE_PREFIX "%03u.sid", tnum); + return subtune; + } else + return nullptr; +} + +static const char *const sidplay_suffixes[] = { + "sid", + "mus", + "str", + "prg", + "P00", + nullptr +}; + +extern const struct DecoderPlugin sidplay_decoder_plugin; +const struct DecoderPlugin sidplay_decoder_plugin = { + "sidplay", + sidplay_init, + sidplay_finish, + nullptr, /* stream_decode() */ + sidplay_file_decode, + sidplay_scan_file, + nullptr, /* stream_tag() */ + sidplay_container_scan, + sidplay_suffixes, + nullptr, /* mime_types */ +}; diff --git a/src/decoder/plugins/SidplayDecoderPlugin.hxx b/src/decoder/plugins/SidplayDecoderPlugin.hxx new file mode 100644 index 000000000..58786e646 --- /dev/null +++ b/src/decoder/plugins/SidplayDecoderPlugin.hxx @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_DECODER_SIDPLAY_HXX +#define MPD_DECODER_SIDPLAY_HXX + +extern const struct DecoderPlugin sidplay_decoder_plugin; + +#endif diff --git a/src/decoder/plugins/SndfileDecoderPlugin.cxx b/src/decoder/plugins/SndfileDecoderPlugin.cxx new file mode 100644 index 000000000..8db078504 --- /dev/null +++ b/src/decoder/plugins/SndfileDecoderPlugin.cxx @@ -0,0 +1,260 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "SndfileDecoderPlugin.hxx" +#include "../DecoderAPI.hxx" +#include "InputStream.hxx" +#include "CheckAudioFormat.hxx" +#include "tag/TagHandler.hxx" +#include "util/Error.hxx" +#include "util/Domain.hxx" +#include "Log.hxx" + +#include + +static constexpr Domain sndfile_domain("sndfile"); + +static sf_count_t +sndfile_vio_get_filelen(void *user_data) +{ + const InputStream &is = *(const InputStream *)user_data; + + return is.GetSize(); +} + +static sf_count_t +sndfile_vio_seek(sf_count_t offset, int whence, void *user_data) +{ + InputStream &is = *(InputStream *)user_data; + + if (!is.LockSeek(offset, whence, IgnoreError())) + return -1; + + return is.GetOffset(); +} + +static sf_count_t +sndfile_vio_read(void *ptr, sf_count_t count, void *user_data) +{ + InputStream &is = *(InputStream *)user_data; + + Error error; + size_t nbytes = is.LockRead(ptr, count, error); + if (nbytes == 0 && error.IsDefined()) { + LogError(error); + return -1; + } + + return nbytes; +} + +static sf_count_t +sndfile_vio_write(gcc_unused const void *ptr, + gcc_unused sf_count_t count, + gcc_unused void *user_data) +{ + /* no writing! */ + return -1; +} + +static sf_count_t +sndfile_vio_tell(void *user_data) +{ + const InputStream &is = *(const InputStream *)user_data; + + return is.GetOffset(); +} + +/** + * This SF_VIRTUAL_IO implementation wraps MPD's #input_stream to a + * libsndfile stream. + */ +static SF_VIRTUAL_IO vio = { + sndfile_vio_get_filelen, + sndfile_vio_seek, + sndfile_vio_read, + sndfile_vio_write, + sndfile_vio_tell, +}; + +/** + * Converts a frame number to a timestamp (in seconds). + */ +static float +frame_to_time(sf_count_t frame, const AudioFormat *audio_format) +{ + return (float)frame / (float)audio_format->sample_rate; +} + +/** + * Converts a timestamp (in seconds) to a frame number. + */ +static sf_count_t +time_to_frame(float t, const AudioFormat *audio_format) +{ + return (sf_count_t)(t * audio_format->sample_rate); +} + +static void +sndfile_stream_decode(Decoder &decoder, InputStream &is) +{ + SNDFILE *sf; + SF_INFO info; + size_t frame_size; + sf_count_t read_frames, num_frames; + int buffer[4096]; + + info.format = 0; + + sf = sf_open_virtual(&vio, SFM_READ, &info, &is); + if (sf == nullptr) { + LogWarning(sndfile_domain, "sf_open_virtual() failed"); + return; + } + + /* for now, always read 32 bit samples. Later, we could lower + MPD's CPU usage by reading 16 bit samples with + sf_readf_short() on low-quality source files. */ + Error error; + AudioFormat audio_format; + if (!audio_format_init_checked(audio_format, info.samplerate, + SampleFormat::S32, + info.channels, error)) { + LogError(error); + return; + } + + decoder_initialized(decoder, audio_format, info.seekable, + frame_to_time(info.frames, &audio_format)); + + frame_size = audio_format.GetFrameSize(); + read_frames = sizeof(buffer) / frame_size; + + DecoderCommand cmd; + do { + num_frames = sf_readf_int(sf, buffer, read_frames); + if (num_frames <= 0) + break; + + cmd = decoder_data(decoder, is, + buffer, num_frames * frame_size, + 0); + if (cmd == DecoderCommand::SEEK) { + sf_count_t c = + time_to_frame(decoder_seek_where(decoder), + &audio_format); + c = sf_seek(sf, c, SEEK_SET); + if (c < 0) + decoder_seek_error(decoder); + else + decoder_command_finished(decoder); + cmd = DecoderCommand::NONE; + } + } while (cmd == DecoderCommand::NONE); + + sf_close(sf); +} + +static bool +sndfile_scan_file(const char *path_fs, + const struct tag_handler *handler, void *handler_ctx) +{ + SNDFILE *sf; + SF_INFO info; + const char *p; + + info.format = 0; + + sf = sf_open(path_fs, SFM_READ, &info); + if (sf == nullptr) + return false; + + if (!audio_valid_sample_rate(info.samplerate)) { + sf_close(sf); + FormatWarning(sndfile_domain, + "Invalid sample rate in %s", path_fs); + return false; + } + + tag_handler_invoke_duration(handler, handler_ctx, + info.frames / info.samplerate); + + p = sf_get_string(sf, SF_STR_TITLE); + if (p != nullptr) + tag_handler_invoke_tag(handler, handler_ctx, + TAG_TITLE, p); + + p = sf_get_string(sf, SF_STR_ARTIST); + if (p != nullptr) + tag_handler_invoke_tag(handler, handler_ctx, + TAG_ARTIST, p); + + p = sf_get_string(sf, SF_STR_DATE); + if (p != nullptr) + tag_handler_invoke_tag(handler, handler_ctx, + TAG_DATE, p); + + sf_close(sf); + + return true; +} + +static const char *const sndfile_suffixes[] = { + "wav", "aiff", "aif", /* Microsoft / SGI / Apple */ + "au", "snd", /* Sun / DEC / NeXT */ + "paf", /* Paris Audio File */ + "iff", "svx", /* Commodore Amiga IFF / SVX */ + "sf", /* IRCAM */ + "voc", /* Creative */ + "w64", /* Soundforge */ + "pvf", /* Portable Voice Format */ + "xi", /* Fasttracker */ + "htk", /* HMM Tool Kit */ + "caf", /* Apple */ + "sd2", /* Sound Designer II */ + + /* libsndfile also supports FLAC and Ogg Vorbis, but only by + linking with libFLAC and libvorbis - we can do better, we + have native plugins for these libraries */ + + nullptr +}; + +static const char *const sndfile_mime_types[] = { + "audio/x-wav", + "audio/x-aiff", + + /* what are the MIME types of the other supported formats? */ + + nullptr +}; + +const struct DecoderPlugin sndfile_decoder_plugin = { + "sndfile", + nullptr, + nullptr, + sndfile_stream_decode, + nullptr, + sndfile_scan_file, + nullptr, + nullptr, + sndfile_suffixes, + sndfile_mime_types, +}; diff --git a/src/decoder/plugins/SndfileDecoderPlugin.hxx b/src/decoder/plugins/SndfileDecoderPlugin.hxx new file mode 100644 index 000000000..d56acdd5a --- /dev/null +++ b/src/decoder/plugins/SndfileDecoderPlugin.hxx @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_DECODER_SNDFILE_HXX +#define MPD_DECODER_SNDFILE_HXX + +extern const struct DecoderPlugin sndfile_decoder_plugin; + +#endif diff --git a/src/decoder/plugins/VorbisComments.cxx b/src/decoder/plugins/VorbisComments.cxx new file mode 100644 index 000000000..2a0820ab5 --- /dev/null +++ b/src/decoder/plugins/VorbisComments.cxx @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "VorbisComments.hxx" +#include "XiphTags.hxx" +#include "tag/TagTable.hxx" +#include "tag/TagHandler.hxx" +#include "tag/TagBuilder.hxx" +#include "ReplayGainInfo.hxx" +#include "util/ASCII.hxx" +#include "util/SplitString.hxx" + +#include +#include +#include + +static const char * +vorbis_comment_value(const char *comment, const char *needle) +{ + size_t len = strlen(needle); + + if (StringEqualsCaseASCII(comment, needle, len) && + comment[len] == '=') + return comment + len + 1; + + return nullptr; +} + +bool +vorbis_comments_to_replay_gain(ReplayGainInfo &rgi, char **comments) +{ + rgi.Clear(); + + const char *temp; + bool found = false; + + while (*comments) { + if ((temp = + vorbis_comment_value(*comments, "replaygain_track_gain"))) { + rgi.tuples[REPLAY_GAIN_TRACK].gain = atof(temp); + found = true; + } else if ((temp = vorbis_comment_value(*comments, + "replaygain_album_gain"))) { + rgi.tuples[REPLAY_GAIN_ALBUM].gain = atof(temp); + found = true; + } else if ((temp = vorbis_comment_value(*comments, + "replaygain_track_peak"))) { + rgi.tuples[REPLAY_GAIN_TRACK].peak = atof(temp); + found = true; + } else if ((temp = vorbis_comment_value(*comments, + "replaygain_album_peak"))) { + rgi.tuples[REPLAY_GAIN_ALBUM].peak = atof(temp); + found = true; + } + + comments++; + } + + return found; +} + +/** + * Check if the comment's name equals the passed name, and if so, copy + * the comment value into the tag. + */ +static bool +vorbis_copy_comment(const char *comment, + const char *name, TagType tag_type, + const struct tag_handler *handler, void *handler_ctx) +{ + const char *value; + + value = vorbis_comment_value(comment, name); + if (value != nullptr) { + tag_handler_invoke_tag(handler, handler_ctx, tag_type, value); + return true; + } + + return false; +} + +static void +vorbis_scan_comment(const char *comment, + const struct tag_handler *handler, void *handler_ctx) +{ + if (handler->pair != nullptr) { + const SplitString split(comment, '='); + if (split.IsDefined() && !split.IsEmpty()) + tag_handler_invoke_pair(handler, handler_ctx, + split.GetFirst(), + split.GetSecond()); + } + + for (const struct tag_table *i = xiph_tags; i->name != nullptr; ++i) + if (vorbis_copy_comment(comment, i->name, i->type, + handler, handler_ctx)) + return; + + for (unsigned i = 0; i < TAG_NUM_OF_ITEM_TYPES; ++i) + if (vorbis_copy_comment(comment, + tag_item_names[i], TagType(i), + handler, handler_ctx)) + return; +} + +void +vorbis_comments_scan(char **comments, + const struct tag_handler *handler, void *handler_ctx) +{ + while (*comments) + vorbis_scan_comment(*comments++, + handler, handler_ctx); + +} + +Tag * +vorbis_comments_to_tag(char **comments) +{ + TagBuilder tag_builder; + vorbis_comments_scan(comments, &add_tag_handler, &tag_builder); + return tag_builder.IsEmpty() + ? nullptr + : tag_builder.CommitNew(); +} diff --git a/src/decoder/plugins/VorbisComments.hxx b/src/decoder/plugins/VorbisComments.hxx new file mode 100644 index 000000000..893c89277 --- /dev/null +++ b/src/decoder/plugins/VorbisComments.hxx @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_VORBIS_COMMENTS_HXX +#define MPD_VORBIS_COMMENTS_HXX + +#include "check.h" + +struct ReplayGainInfo; +struct tag_handler; +struct Tag; + +bool +vorbis_comments_to_replay_gain(ReplayGainInfo &rgi, char **comments); + +void +vorbis_comments_scan(char **comments, + const tag_handler *handler, void *handler_ctx); + +Tag * +vorbis_comments_to_tag(char **comments); + +#endif diff --git a/src/decoder/plugins/VorbisDecoderPlugin.cxx b/src/decoder/plugins/VorbisDecoderPlugin.cxx new file mode 100644 index 000000000..35551604e --- /dev/null +++ b/src/decoder/plugins/VorbisDecoderPlugin.cxx @@ -0,0 +1,349 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "VorbisDecoderPlugin.h" +#include "VorbisComments.hxx" +#include "VorbisDomain.hxx" +#include "../DecoderAPI.hxx" +#include "InputStream.hxx" +#include "OggCodec.hxx" +#include "util/Error.hxx" +#include "util/Macros.hxx" +#include "CheckAudioFormat.hxx" +#include "tag/TagHandler.hxx" +#include "Log.hxx" + +#ifndef HAVE_TREMOR +#define OV_EXCLUDE_STATIC_CALLBACKS +#include +#else +#include +/* Macros to make Tremor's API look like libogg. Tremor always + returns host-byte-order 16-bit signed data, and uses integer + milliseconds where libogg uses double seconds. +*/ +#define ov_read(VF, BUFFER, LENGTH, BIGENDIANP, WORD, SGNED, BITSTREAM) \ + ov_read(VF, BUFFER, LENGTH, BITSTREAM) +#define ov_time_total(VF, I) ((double)ov_time_total(VF, I)/1000) +#define ov_time_tell(VF) ((double)ov_time_tell(VF)/1000) +#define ov_time_seek_page(VF, S) (ov_time_seek_page(VF, (S)*1000)) +#endif /* HAVE_TREMOR */ + +#include + +struct vorbis_input_stream { + Decoder *decoder; + + InputStream *input_stream; + bool seekable; +}; + +static size_t ogg_read_cb(void *ptr, size_t size, size_t nmemb, void *data) +{ + struct vorbis_input_stream *vis = (struct vorbis_input_stream *)data; + size_t ret = decoder_read(vis->decoder, *vis->input_stream, + ptr, size * nmemb); + + errno = 0; + + return ret / size; +} + +static int ogg_seek_cb(void *data, ogg_int64_t offset, int whence) +{ + struct vorbis_input_stream *vis = (struct vorbis_input_stream *)data; + + Error error; + return vis->seekable && + (vis->decoder == nullptr || + decoder_get_command(*vis->decoder) != DecoderCommand::STOP) && + vis->input_stream->LockSeek(offset, whence, error) + ? 0 : -1; +} + +/* TODO: check Ogg libraries API and see if we can just not have this func */ +static int ogg_close_cb(gcc_unused void *data) +{ + return 0; +} + +static long ogg_tell_cb(void *data) +{ + struct vorbis_input_stream *vis = (struct vorbis_input_stream *)data; + + return (long)vis->input_stream->offset; +} + +static const ov_callbacks vorbis_is_callbacks = { + ogg_read_cb, + ogg_seek_cb, + ogg_close_cb, + ogg_tell_cb, +}; + +static const char * +vorbis_strerror(int code) +{ + switch (code) { + case OV_EREAD: + return "read error"; + + case OV_ENOTVORBIS: + return "not vorbis stream"; + + case OV_EVERSION: + return "vorbis version mismatch"; + + case OV_EBADHEADER: + return "invalid vorbis header"; + + case OV_EFAULT: + return "internal logic error"; + + default: + return "unknown error"; + } +} + +static bool +vorbis_is_open(struct vorbis_input_stream *vis, OggVorbis_File *vf, + Decoder *decoder, InputStream &input_stream) +{ + vis->decoder = decoder; + vis->input_stream = &input_stream; + vis->seekable = input_stream.CheapSeeking(); + + int ret = ov_open_callbacks(vis, vf, nullptr, 0, vorbis_is_callbacks); + if (ret < 0) { + if (decoder == nullptr || + decoder_get_command(*decoder) == DecoderCommand::NONE) + FormatWarning(vorbis_domain, + "Failed to open Ogg Vorbis stream: %s", + vorbis_strerror(ret)); + return false; + } + + return true; +} + +static void +vorbis_send_comments(Decoder &decoder, InputStream &is, + char **comments) +{ + Tag *tag = vorbis_comments_to_tag(comments); + if (!tag) + return; + + decoder_tag(decoder, is, std::move(*tag)); + delete tag; +} + +#ifndef HAVE_TREMOR +static void +vorbis_interleave(float *dest, const float *const*src, + unsigned nframes, unsigned channels) +{ + for (const float *const*src_end = src + channels; + src != src_end; ++src, ++dest) { + float *d = dest; + for (const float *s = *src, *s_end = s + nframes; + s != s_end; ++s, d += channels) + *d = *s; + } +} +#endif + +/* public */ +static void +vorbis_stream_decode(Decoder &decoder, + InputStream &input_stream) +{ + if (ogg_codec_detect(&decoder, input_stream) != OGG_CODEC_VORBIS) + return; + + /* rewind the stream, because ogg_codec_detect() has + moved it */ + input_stream.LockRewind(IgnoreError()); + + struct vorbis_input_stream vis; + OggVorbis_File vf; + if (!vorbis_is_open(&vis, &vf, &decoder, input_stream)) + return; + + const vorbis_info *vi = ov_info(&vf, -1); + if (vi == nullptr) { + LogWarning(vorbis_domain, "ov_info() has failed"); + return; + } + + Error error; + AudioFormat audio_format; + if (!audio_format_init_checked(audio_format, vi->rate, +#ifdef HAVE_TREMOR + SampleFormat::S16, +#else + SampleFormat::FLOAT, +#endif + vi->channels, error)) { + LogError(error); + return; + } + + float total_time = ov_time_total(&vf, -1); + if (total_time < 0) + total_time = 0; + + decoder_initialized(decoder, audio_format, vis.seekable, total_time); + +#ifdef HAVE_TREMOR + char buffer[4096]; +#else + float buffer[2048]; + const int frames_per_buffer = + ARRAY_SIZE(buffer) / audio_format.channels; + const unsigned frame_size = sizeof(buffer[0]) * audio_format.channels; +#endif + + int prev_section = -1; + unsigned kbit_rate = 0; + + DecoderCommand cmd = decoder_get_command(decoder); + do { + if (cmd == DecoderCommand::SEEK) { + double seek_where = decoder_seek_where(decoder); + if (0 == ov_time_seek_page(&vf, seek_where)) { + decoder_command_finished(decoder); + } else + decoder_seek_error(decoder); + } + + int current_section; + +#ifdef HAVE_TREMOR + long nbytes = ov_read(&vf, buffer, sizeof(buffer), + IsBigEndian(), 2, 1, + ¤t_section); +#else + float **per_channel; + long nframes = ov_read_float(&vf, &per_channel, + frames_per_buffer, + ¤t_section); + long nbytes = nframes; + if (nframes > 0) { + vorbis_interleave(buffer, + (const float*const*)per_channel, + nframes, audio_format.channels); + nbytes *= frame_size; + } +#endif + + if (nbytes == OV_HOLE) /* bad packet */ + nbytes = 0; + else if (nbytes <= 0) + /* break on EOF or other error */ + break; + + if (current_section != prev_section) { + vi = ov_info(&vf, -1); + if (vi == nullptr) { + LogWarning(vorbis_domain, + "ov_info() has failed"); + break; + } + + if (vi->rate != (long)audio_format.sample_rate || + vi->channels != (int)audio_format.channels) { + /* we don't support audio format + change yet */ + LogWarning(vorbis_domain, + "audio format change, stopping here"); + break; + } + + char **comments = ov_comment(&vf, -1)->user_comments; + vorbis_send_comments(decoder, input_stream, comments); + + ReplayGainInfo rgi; + if (vorbis_comments_to_replay_gain(rgi, comments)) + decoder_replay_gain(decoder, &rgi); + + prev_section = current_section; + } + + long test = ov_bitrate_instant(&vf); + if (test > 0) + kbit_rate = test / 1000; + + cmd = decoder_data(decoder, input_stream, + buffer, nbytes, + kbit_rate); + } while (cmd != DecoderCommand::STOP); + + ov_clear(&vf); +} + +static bool +vorbis_scan_stream(InputStream &is, + const struct tag_handler *handler, void *handler_ctx) +{ + struct vorbis_input_stream vis; + OggVorbis_File vf; + + if (!vorbis_is_open(&vis, &vf, nullptr, is)) + return false; + + tag_handler_invoke_duration(handler, handler_ctx, + (int)(ov_time_total(&vf, -1) + 0.5)); + + vorbis_comments_scan(ov_comment(&vf, -1)->user_comments, + handler, handler_ctx); + + ov_clear(&vf); + return true; +} + +static const char *const vorbis_suffixes[] = { + "ogg", "oga", nullptr +}; + +static const char *const vorbis_mime_types[] = { + "application/ogg", + "application/x-ogg", + "audio/ogg", + "audio/vorbis", + "audio/vorbis+ogg", + "audio/x-ogg", + "audio/x-vorbis", + "audio/x-vorbis+ogg", + nullptr +}; + +const struct DecoderPlugin vorbis_decoder_plugin = { + "vorbis", + nullptr, + nullptr, + vorbis_stream_decode, + nullptr, + nullptr, + vorbis_scan_stream, + nullptr, + vorbis_suffixes, + vorbis_mime_types +}; diff --git a/src/decoder/plugins/VorbisDecoderPlugin.h b/src/decoder/plugins/VorbisDecoderPlugin.h new file mode 100644 index 000000000..b54df2e97 --- /dev/null +++ b/src/decoder/plugins/VorbisDecoderPlugin.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_DECODER_VORBIS_H +#define MPD_DECODER_VORBIS_H + +extern const struct DecoderPlugin vorbis_decoder_plugin; + +#endif diff --git a/src/decoder/plugins/VorbisDomain.cxx b/src/decoder/plugins/VorbisDomain.cxx new file mode 100644 index 000000000..e3d880efa --- /dev/null +++ b/src/decoder/plugins/VorbisDomain.cxx @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "VorbisDomain.hxx" +#include "util/Domain.hxx" + +const Domain vorbis_domain("vorbis"); diff --git a/src/decoder/plugins/VorbisDomain.hxx b/src/decoder/plugins/VorbisDomain.hxx new file mode 100644 index 000000000..48715e328 --- /dev/null +++ b/src/decoder/plugins/VorbisDomain.hxx @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_VORBIS_DOMAIN_HXX +#define MPD_VORBIS_DOMAIN_HXX + +#include "check.h" + +class Domain; + +extern const Domain vorbis_domain; + +#endif diff --git a/src/decoder/plugins/WavpackDecoderPlugin.cxx b/src/decoder/plugins/WavpackDecoderPlugin.cxx new file mode 100644 index 000000000..c217e513e --- /dev/null +++ b/src/decoder/plugins/WavpackDecoderPlugin.cxx @@ -0,0 +1,571 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "WavpackDecoderPlugin.hxx" +#include "../DecoderAPI.hxx" +#include "InputStream.hxx" +#include "CheckAudioFormat.hxx" +#include "tag/TagHandler.hxx" +#include "tag/ApeTag.hxx" +#include "util/Error.hxx" +#include "util/Domain.hxx" +#include "util/Macros.hxx" +#include "Log.hxx" + +#include +#include + +#include +#include +#include + +#define ERRORLEN 80 + +static constexpr Domain wavpack_domain("wavpack"); + +/** A pointer type for format converter function. */ +typedef void (*format_samples_t)( + int bytes_per_sample, + void *buffer, uint32_t count +); + +/* + * This function has been borrowed from the tiny player found on + * wavpack.com. Modifications were required because mpd only handles + * max 24-bit samples. + */ +static void +format_samples_int(int bytes_per_sample, void *buffer, uint32_t count) +{ + int32_t *src = (int32_t *)buffer; + + switch (bytes_per_sample) { + case 1: { + int8_t *dst = (int8_t *)buffer; + /* + * The asserts like the following one are because we do the + * formatting of samples within a single buffer. The size + * of the output samples never can be greater than the size + * of the input ones. Otherwise we would have an overflow. + */ + static_assert(sizeof(*dst) <= sizeof(*src), "Wrong size"); + + /* pass through and align 8-bit samples */ + while (count--) { + *dst++ = *src++; + } + break; + } + case 2: { + uint16_t *dst = (uint16_t *)buffer; + static_assert(sizeof(*dst) <= sizeof(*src), "Wrong size"); + + /* pass through and align 16-bit samples */ + while (count--) { + *dst++ = *src++; + } + break; + } + + case 3: + case 4: + /* do nothing */ + break; + } +} + +/* + * This function converts floating point sample data to 24-bit integer. + */ +static void +format_samples_float(gcc_unused int bytes_per_sample, void *buffer, + uint32_t count) +{ + float *p = (float *)buffer; + + while (count--) { + *p /= (1 << 23); + ++p; + } +} + +/** + * Choose a MPD sample format from libwavpacks' number of bits. + */ +static SampleFormat +wavpack_bits_to_sample_format(bool is_float, int bytes_per_sample) +{ + if (is_float) + return SampleFormat::FLOAT; + + switch (bytes_per_sample) { + case 1: + return SampleFormat::S8; + + case 2: + return SampleFormat::S16; + + case 3: + return SampleFormat::S24_P32; + + case 4: + return SampleFormat::S32; + + default: + return SampleFormat::UNDEFINED; + } +} + +/* + * This does the main decoding thing. + * Requires an already opened WavpackContext. + */ +static void +wavpack_decode(Decoder &decoder, WavpackContext *wpc, bool can_seek) +{ + bool is_float = (WavpackGetMode(wpc) & MODE_FLOAT) != 0; + SampleFormat sample_format = + wavpack_bits_to_sample_format(is_float, + WavpackGetBytesPerSample(wpc)); + + Error error; + AudioFormat audio_format; + if (!audio_format_init_checked(audio_format, + WavpackGetSampleRate(wpc), + sample_format, + WavpackGetNumChannels(wpc), error)) { + LogError(error); + return; + } + + const format_samples_t format_samples = is_float + ? format_samples_float + : format_samples_int; + + const float total_time = float(WavpackGetNumSamples(wpc)) + / audio_format.sample_rate; + + const int bytes_per_sample = WavpackGetBytesPerSample(wpc); + const int output_sample_size = audio_format.GetFrameSize(); + + /* wavpack gives us all kind of samples in a 32-bit space */ + int32_t chunk[1024]; + const uint32_t samples_requested = ARRAY_SIZE(chunk) / + audio_format.channels; + + decoder_initialized(decoder, audio_format, can_seek, total_time); + + DecoderCommand cmd = decoder_get_command(decoder); + while (cmd != DecoderCommand::STOP) { + if (cmd == DecoderCommand::SEEK) { + if (can_seek) { + unsigned where = decoder_seek_where(decoder) * + audio_format.sample_rate; + + if (WavpackSeekSample(wpc, where)) { + decoder_command_finished(decoder); + } else { + decoder_seek_error(decoder); + } + } else { + decoder_seek_error(decoder); + } + } + + uint32_t samples_got = WavpackUnpackSamples(wpc, chunk, + samples_requested); + if (samples_got == 0) + break; + + int bitrate = (int)(WavpackGetInstantBitrate(wpc) / 1000 + + 0.5); + format_samples(bytes_per_sample, chunk, + samples_got * audio_format.channels); + + cmd = decoder_data(decoder, nullptr, chunk, + samples_got * output_sample_size, + bitrate); + } +} + +/** + * Locate and parse a floating point tag. Returns true if it was + * found. + */ +static bool +wavpack_tag_float(WavpackContext *wpc, const char *key, float *value_r) +{ + char buffer[64]; + if (WavpackGetTagItem(wpc, key, buffer, sizeof(buffer)) <= 0) + return false; + + *value_r = atof(buffer); + return true; +} + +static bool +wavpack_replaygain(ReplayGainInfo &rgi, + WavpackContext *wpc) +{ + rgi.Clear(); + + bool found = false; + found |= wavpack_tag_float(wpc, "replaygain_track_gain", + &rgi.tuples[REPLAY_GAIN_TRACK].gain); + found |= wavpack_tag_float(wpc, "replaygain_track_peak", + &rgi.tuples[REPLAY_GAIN_TRACK].peak); + found |= wavpack_tag_float(wpc, "replaygain_album_gain", + &rgi.tuples[REPLAY_GAIN_ALBUM].gain); + found |= wavpack_tag_float(wpc, "replaygain_album_peak", + &rgi.tuples[REPLAY_GAIN_ALBUM].peak); + + return found; +} + +static void +wavpack_scan_tag_item(WavpackContext *wpc, const char *name, + TagType type, + const struct tag_handler *handler, void *handler_ctx) +{ + char buffer[1024]; + int len = WavpackGetTagItem(wpc, name, buffer, sizeof(buffer)); + if (len <= 0 || (unsigned)len >= sizeof(buffer)) + return; + + tag_handler_invoke_tag(handler, handler_ctx, type, buffer); + +} + +static void +wavpack_scan_pair(WavpackContext *wpc, const char *name, + const struct tag_handler *handler, void *handler_ctx) +{ + char buffer[8192]; + int len = WavpackGetTagItem(wpc, name, buffer, sizeof(buffer)); + if (len <= 0 || (unsigned)len >= sizeof(buffer)) + return; + + tag_handler_invoke_pair(handler, handler_ctx, name, buffer); +} + +/* + * Reads metainfo from the specified file. + */ +static bool +wavpack_scan_file(const char *fname, + const struct tag_handler *handler, void *handler_ctx) +{ + char error[ERRORLEN]; + WavpackContext *wpc = WavpackOpenFileInput(fname, error, OPEN_TAGS, 0); + if (wpc == nullptr) { + FormatError(wavpack_domain, + "failed to open WavPack file \"%s\": %s", + fname, error); + return false; + } + + tag_handler_invoke_duration(handler, handler_ctx, + WavpackGetNumSamples(wpc) / + WavpackGetSampleRate(wpc)); + + /* the WavPack format implies APEv2 tags, which means we can + reuse the mapping from tag_ape.c */ + + for (unsigned i = 0; i < TAG_NUM_OF_ITEM_TYPES; ++i) { + const char *name = tag_item_names[i]; + if (name != nullptr) + wavpack_scan_tag_item(wpc, name, (TagType)i, + handler, handler_ctx); + } + + for (const struct tag_table *i = ape_tags; i->name != nullptr; ++i) + wavpack_scan_tag_item(wpc, i->name, i->type, + handler, handler_ctx); + + if (handler->pair != nullptr) { + char name[64]; + + for (int i = 0, n = WavpackGetNumTagItems(wpc); + i < n; ++i) { + int len = WavpackGetTagItemIndexed(wpc, i, name, + sizeof(name)); + if (len <= 0 || (unsigned)len >= sizeof(name)) + continue; + + wavpack_scan_pair(wpc, name, handler, handler_ctx); + } + } + + WavpackCloseFile(wpc); + + return true; +} + +/* + * mpd input_stream <=> WavpackStreamReader wrapper callbacks + */ + +/* This struct is needed for per-stream last_byte storage. */ +struct wavpack_input { + Decoder *decoder; + InputStream *is; + /* Needed for push_back_byte() */ + int last_byte; +}; + +/** + * Little wrapper for struct wavpack_input to cast from void *. + */ +static struct wavpack_input * +wpin(void *id) +{ + assert(id); + return (struct wavpack_input *)id; +} + +static int32_t +wavpack_input_read_bytes(void *id, void *data, int32_t bcount) +{ + uint8_t *buf = (uint8_t *)data; + int32_t i = 0; + + if (wpin(id)->last_byte != EOF) { + *buf++ = wpin(id)->last_byte; + wpin(id)->last_byte = EOF; + --bcount; + ++i; + } + + /* wavpack fails if we return a partial read, so we just wait + until the buffer is full */ + while (bcount > 0) { + size_t nbytes = decoder_read( + wpin(id)->decoder, *wpin(id)->is, buf, bcount + ); + if (nbytes == 0) { + /* EOF, error or a decoder command */ + break; + } + + i += nbytes; + bcount -= nbytes; + buf += nbytes; + } + + return i; +} + +static uint32_t +wavpack_input_get_pos(void *id) +{ + return wpin(id)->is->offset; +} + +static int +wavpack_input_set_pos_abs(void *id, uint32_t pos) +{ + return wpin(id)->is->LockSeek(pos, SEEK_SET, IgnoreError()) ? 0 : -1; +} + +static int +wavpack_input_set_pos_rel(void *id, int32_t delta, int mode) +{ + return wpin(id)->is->LockSeek(delta, mode, IgnoreError()) ? 0 : -1; +} + +static int +wavpack_input_push_back_byte(void *id, int c) +{ + if (wpin(id)->last_byte == EOF) { + wpin(id)->last_byte = c; + return c; + } else { + return EOF; + } +} + +static uint32_t +wavpack_input_get_length(void *id) +{ + if (wpin(id)->is->size < 0) + return 0; + + return wpin(id)->is->size; +} + +static int +wavpack_input_can_seek(void *id) +{ + return wpin(id)->is->seekable; +} + +static WavpackStreamReader mpd_is_reader = { + wavpack_input_read_bytes, + wavpack_input_get_pos, + wavpack_input_set_pos_abs, + wavpack_input_set_pos_rel, + wavpack_input_push_back_byte, + wavpack_input_get_length, + wavpack_input_can_seek, + nullptr /* no need to write edited tags */ +}; + +static void +wavpack_input_init(struct wavpack_input *isp, Decoder &decoder, + InputStream &is) +{ + isp->decoder = &decoder; + isp->is = &is; + isp->last_byte = EOF; +} + +static InputStream * +wavpack_open_wvc(Decoder &decoder, const char *uri, + Mutex &mutex, Cond &cond, + struct wavpack_input *wpi) +{ + /* + * As we use dc->utf8url, this function will be bad for + * single files. utf8url is not absolute file path :/ + */ + if (uri == nullptr) + return nullptr; + + char *wvc_url = g_strconcat(uri, "c", nullptr); + + InputStream *is_wvc = InputStream::Open(wvc_url, mutex, cond, + IgnoreError()); + g_free(wvc_url); + + if (is_wvc == nullptr) + return nullptr; + + /* + * And we try to buffer in order to get know + * about a possible 404 error. + */ + char first_byte; + size_t nbytes = decoder_read(decoder, *is_wvc, + &first_byte, sizeof(first_byte)); + if (nbytes == 0) { + is_wvc->Close(); + return nullptr; + } + + /* push it back */ + wavpack_input_init(wpi, decoder, *is_wvc); + wpi->last_byte = first_byte; + return is_wvc; +} + +/* + * Decodes a stream. + */ +static void +wavpack_streamdecode(Decoder &decoder, InputStream &is) +{ + int open_flags = OPEN_NORMALIZE; + bool can_seek = is.seekable; + + wavpack_input isp_wvc; + InputStream *is_wvc = wavpack_open_wvc(decoder, is.uri.c_str(), + is.mutex, is.cond, + &isp_wvc); + if (is_wvc != nullptr) { + open_flags |= OPEN_WVC; + can_seek &= is_wvc->seekable; + } + + if (!can_seek) { + open_flags |= OPEN_STREAMING; + } + + wavpack_input isp; + wavpack_input_init(&isp, decoder, is); + + char error[ERRORLEN]; + WavpackContext *wpc = + WavpackOpenFileInputEx(&mpd_is_reader, &isp, + open_flags & OPEN_WVC + ? &isp_wvc : nullptr, + error, open_flags, 23); + + if (wpc == nullptr) { + FormatError(wavpack_domain, + "failed to open WavPack stream: %s", error); + return; + } + + wavpack_decode(decoder, wpc, can_seek); + + WavpackCloseFile(wpc); + if (open_flags & OPEN_WVC) { + is_wvc->Close(); + } +} + +/* + * Decodes a file. + */ +static void +wavpack_filedecode(Decoder &decoder, const char *fname) +{ + char error[ERRORLEN]; + WavpackContext *wpc = WavpackOpenFileInput(fname, error, + OPEN_TAGS | OPEN_WVC | OPEN_NORMALIZE, + 23); + if (wpc == nullptr) { + FormatWarning(wavpack_domain, + "failed to open WavPack file \"%s\": %s", + fname, error); + return; + } + + ReplayGainInfo rgi; + if (wavpack_replaygain(rgi, wpc)) + decoder_replay_gain(decoder, &rgi); + + wavpack_decode(decoder, wpc, true); + + WavpackCloseFile(wpc); +} + +static char const *const wavpack_suffixes[] = { + "wv", + nullptr +}; + +static char const *const wavpack_mime_types[] = { + "audio/x-wavpack", + nullptr +}; + +const struct DecoderPlugin wavpack_decoder_plugin = { + "wavpack", + nullptr, + nullptr, + wavpack_streamdecode, + wavpack_filedecode, + wavpack_scan_file, + nullptr, + nullptr, + wavpack_suffixes, + wavpack_mime_types +}; diff --git a/src/decoder/plugins/WavpackDecoderPlugin.hxx b/src/decoder/plugins/WavpackDecoderPlugin.hxx new file mode 100644 index 000000000..2e5f9bd42 --- /dev/null +++ b/src/decoder/plugins/WavpackDecoderPlugin.hxx @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_DECODER_WAVPACK_HXX +#define MPD_DECODER_WAVPACK_HXX + +extern const struct DecoderPlugin wavpack_decoder_plugin; + +#endif diff --git a/src/decoder/plugins/WildmidiDecoderPlugin.cxx b/src/decoder/plugins/WildmidiDecoderPlugin.cxx new file mode 100644 index 000000000..9a6018022 --- /dev/null +++ b/src/decoder/plugins/WildmidiDecoderPlugin.cxx @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "WildmidiDecoderPlugin.hxx" +#include "../DecoderAPI.hxx" +#include "tag/TagHandler.hxx" +#include "util/Error.hxx" +#include "util/Domain.hxx" +#include "fs/AllocatedPath.hxx" +#include "fs/FileSystem.hxx" +#include "system/FatalError.hxx" +#include "Log.hxx" + +extern "C" { +#include +} + +static constexpr Domain wildmidi_domain("wildmidi"); + +static constexpr unsigned WILDMIDI_SAMPLE_RATE = 48000; + +static bool +wildmidi_init(const config_param ¶m) +{ + Error error; + const AllocatedPath path = + param.GetBlockPath("config_file", + "/etc/timidity/timidity.cfg", + error); + if (path.IsNull()) + FatalError(error); + + if (!FileExists(path)) { + const auto utf8 = path.ToUTF8(); + FormatDebug(wildmidi_domain, + "configuration file does not exist: %s", + utf8.c_str()); + return false; + } + + return WildMidi_Init(path.c_str(), WILDMIDI_SAMPLE_RATE, 0) == 0; +} + +static void +wildmidi_finish(void) +{ + WildMidi_Shutdown(); +} + +static void +wildmidi_file_decode(Decoder &decoder, const char *path_fs) +{ + static constexpr AudioFormat audio_format = { + WILDMIDI_SAMPLE_RATE, + SampleFormat::S16, + 2, + }; + midi *wm; + const struct _WM_Info *info; + + wm = WildMidi_Open(path_fs); + if (wm == nullptr) + return; + + info = WildMidi_GetInfo(wm); + if (info == nullptr) { + WildMidi_Close(wm); + return; + } + + decoder_initialized(decoder, audio_format, true, + info->approx_total_samples / WILDMIDI_SAMPLE_RATE); + + DecoderCommand cmd; + do { + char buffer[4096]; + int len; + + info = WildMidi_GetInfo(wm); + if (info == nullptr) + break; + + len = WildMidi_GetOutput(wm, buffer, sizeof(buffer)); + if (len <= 0) + break; + + cmd = decoder_data(decoder, nullptr, buffer, len, 0); + + if (cmd == DecoderCommand::SEEK) { + unsigned long seek_where = WILDMIDI_SAMPLE_RATE * + decoder_seek_where(decoder); + + WildMidi_FastSeek(wm, &seek_where); + decoder_command_finished(decoder); + cmd = DecoderCommand::NONE; + } + + } while (cmd == DecoderCommand::NONE); + + WildMidi_Close(wm); +} + +static bool +wildmidi_scan_file(const char *path_fs, + const struct tag_handler *handler, void *handler_ctx) +{ + midi *wm = WildMidi_Open(path_fs); + if (wm == nullptr) + return false; + + const struct _WM_Info *info = WildMidi_GetInfo(wm); + if (info == nullptr) { + WildMidi_Close(wm); + return false; + } + + int duration = info->approx_total_samples / WILDMIDI_SAMPLE_RATE; + tag_handler_invoke_duration(handler, handler_ctx, duration); + + WildMidi_Close(wm); + + return true; +} + +static const char *const wildmidi_suffixes[] = { + "mid", + nullptr +}; + +const struct DecoderPlugin wildmidi_decoder_plugin = { + "wildmidi", + wildmidi_init, + wildmidi_finish, + nullptr, + wildmidi_file_decode, + wildmidi_scan_file, + nullptr, + nullptr, + wildmidi_suffixes, + nullptr, +}; diff --git a/src/decoder/plugins/WildmidiDecoderPlugin.hxx b/src/decoder/plugins/WildmidiDecoderPlugin.hxx new file mode 100644 index 000000000..fc87aab80 --- /dev/null +++ b/src/decoder/plugins/WildmidiDecoderPlugin.hxx @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_DECODER_WILDMIDI_HXX +#define MPD_DECODER_WILDMIDI_HXX + +extern const struct DecoderPlugin wildmidi_decoder_plugin; + +#endif diff --git a/src/decoder/plugins/XiphTags.cxx b/src/decoder/plugins/XiphTags.cxx new file mode 100644 index 000000000..a5e534086 --- /dev/null +++ b/src/decoder/plugins/XiphTags.cxx @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include "XiphTags.hxx" + +const struct tag_table xiph_tags[] = { + { "tracknumber", TAG_TRACK }, + { "discnumber", TAG_DISC }, + { "album artist", TAG_ALBUM_ARTIST }, + { nullptr, TAG_NUM_OF_ITEM_TYPES } +}; diff --git a/src/decoder/plugins/XiphTags.hxx b/src/decoder/plugins/XiphTags.hxx new file mode 100644 index 000000000..48a27425f --- /dev/null +++ b/src/decoder/plugins/XiphTags.hxx @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2003-2014 The Music Player Daemon Project + * http://www.musicpd.org + * + * 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, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPD_XIPH_TAGS_HXX +#define MPD_XIPH_TAGS_HXX + +#include "check.h" +#include "tag/TagTable.hxx" + +extern const struct tag_table xiph_tags[]; + +#endif -- cgit v1.2.3