449 lines
9.7 KiB
C++
449 lines
9.7 KiB
C++
/*
|
|
* Copyright 2003-2018 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 "OggDecoder.hxx"
|
|
#include "lib/xiph/VorbisComments.hxx"
|
|
#include "lib/xiph/OggPacket.hxx"
|
|
#include "lib/xiph/OggFind.hxx"
|
|
#include "VorbisDomain.hxx"
|
|
#include "../DecoderAPI.hxx"
|
|
#include "input/InputStream.hxx"
|
|
#include "input/Reader.hxx"
|
|
#include "OggCodec.hxx"
|
|
#include "pcm/Interleave.hxx"
|
|
#include "util/Macros.hxx"
|
|
#include "util/ScopeExit.hxx"
|
|
#include "CheckAudioFormat.hxx"
|
|
#include "tag/Handler.hxx"
|
|
#include "Log.hxx"
|
|
|
|
#ifndef HAVE_TREMOR
|
|
#include <vorbis/codec.h>
|
|
#else
|
|
#include <tremor/ivorbiscodec.h>
|
|
#endif /* HAVE_TREMOR */
|
|
|
|
#include <stdexcept>
|
|
|
|
class VorbisDecoder final : public OggDecoder {
|
|
#ifdef HAVE_TREMOR
|
|
static constexpr SampleFormat sample_format = SampleFormat::S16;
|
|
typedef ogg_int32_t in_sample_t;
|
|
typedef int16_t out_sample_t;
|
|
#else
|
|
static constexpr SampleFormat sample_format = SampleFormat::FLOAT;
|
|
typedef float in_sample_t;
|
|
typedef float out_sample_t;
|
|
#endif
|
|
|
|
unsigned remaining_header_packets;
|
|
|
|
vorbis_info vi;
|
|
vorbis_comment vc;
|
|
vorbis_dsp_state dsp;
|
|
vorbis_block block;
|
|
|
|
/**
|
|
* If non-zero, then a previous Vorbis stream has been found
|
|
* already with this number of channels.
|
|
*/
|
|
AudioFormat audio_format = AudioFormat::Undefined();
|
|
size_t frame_size;
|
|
|
|
bool dsp_initialized = false;
|
|
|
|
public:
|
|
explicit VorbisDecoder(DecoderReader &reader)
|
|
:OggDecoder(reader) {
|
|
InitVorbis();
|
|
}
|
|
|
|
~VorbisDecoder() {
|
|
DeinitVorbis();
|
|
}
|
|
|
|
bool Seek(uint64_t where_frame);
|
|
|
|
static AudioFormat CheckAudioFormat(const vorbis_info &vi) {
|
|
return ::CheckAudioFormat(vi.rate, sample_format, vi.channels);
|
|
}
|
|
|
|
AudioFormat CheckAudioFormat() const {
|
|
return CheckAudioFormat(vi);
|
|
}
|
|
|
|
private:
|
|
void InitVorbis() {
|
|
vorbis_info_init(&vi);
|
|
vorbis_comment_init(&vc);
|
|
}
|
|
|
|
void DeinitVorbis() {
|
|
if (dsp_initialized) {
|
|
dsp_initialized = false;
|
|
|
|
vorbis_block_clear(&block);
|
|
vorbis_dsp_clear(&dsp);
|
|
}
|
|
|
|
vorbis_comment_clear(&vc);
|
|
vorbis_info_clear(&vi);
|
|
}
|
|
|
|
void ReinitVorbis() {
|
|
DeinitVorbis();
|
|
InitVorbis();
|
|
}
|
|
|
|
void SubmitInit();
|
|
bool SubmitSomePcm();
|
|
void SubmitPcm();
|
|
|
|
protected:
|
|
/* virtual methods from class OggVisitor */
|
|
void OnOggBeginning(const ogg_packet &packet) override;
|
|
void OnOggPacket(const ogg_packet &packet) override;
|
|
void OnOggEnd() override;
|
|
};
|
|
|
|
bool
|
|
VorbisDecoder::Seek(uint64_t where_frame)
|
|
{
|
|
assert(IsSeekable());
|
|
assert(input_stream.IsSeekable());
|
|
assert(input_stream.KnownSize());
|
|
|
|
const ogg_int64_t where_granulepos(where_frame);
|
|
|
|
try {
|
|
SeekGranulePos(where_granulepos);
|
|
vorbis_synthesis_restart(&dsp);
|
|
return true;
|
|
} catch (...) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void
|
|
VorbisDecoder::OnOggBeginning(const ogg_packet &_packet)
|
|
{
|
|
/* libvorbis wants non-const packets */
|
|
ogg_packet &packet = const_cast<ogg_packet &>(_packet);
|
|
|
|
ReinitVorbis();
|
|
|
|
if (vorbis_synthesis_headerin(&vi, &vc, &packet) != 0)
|
|
throw std::runtime_error("Unrecognized Vorbis BOS packet");
|
|
|
|
remaining_header_packets = 2;
|
|
}
|
|
|
|
static void
|
|
vorbis_send_comments(DecoderClient &client, InputStream &is,
|
|
char **comments)
|
|
{
|
|
auto tag = vorbis_comments_to_tag(comments);
|
|
if (!tag)
|
|
return;
|
|
|
|
client.SubmitTag(is, std::move(*tag));
|
|
}
|
|
|
|
void
|
|
VorbisDecoder::SubmitInit()
|
|
{
|
|
assert(!dsp_initialized);
|
|
|
|
audio_format = CheckAudioFormat(vi);
|
|
|
|
frame_size = audio_format.GetFrameSize();
|
|
|
|
const auto eos_granulepos = UpdateEndGranulePos();
|
|
const auto duration = eos_granulepos >= 0
|
|
? SignedSongTime::FromScale<uint64_t>(eos_granulepos,
|
|
audio_format.sample_rate)
|
|
: SignedSongTime::Negative();
|
|
|
|
client.Ready(audio_format, eos_granulepos > 0, duration);
|
|
}
|
|
|
|
#ifdef HAVE_TREMOR
|
|
static inline int16_t tremor_clip_sample(int32_t x)
|
|
{
|
|
x >>= 9;
|
|
|
|
if (x < INT16_MIN)
|
|
return INT16_MIN;
|
|
if (x > INT16_MAX)
|
|
return INT16_MAX;
|
|
|
|
return x;
|
|
}
|
|
#endif
|
|
|
|
bool
|
|
VorbisDecoder::SubmitSomePcm()
|
|
{
|
|
in_sample_t **pcm;
|
|
int result = vorbis_synthesis_pcmout(&dsp, &pcm);
|
|
if (result <= 0)
|
|
return false;
|
|
|
|
out_sample_t buffer[4096];
|
|
const unsigned channels = audio_format.channels;
|
|
size_t max_frames = ARRAY_SIZE(buffer) / channels;
|
|
size_t n_frames = std::min(size_t(result), max_frames);
|
|
|
|
#ifdef HAVE_TREMOR
|
|
for (unsigned c = 0; c < channels; ++c) {
|
|
const auto *src = pcm[c];
|
|
auto *dest = &buffer[c];
|
|
|
|
for (size_t i = 0; i < n_frames; ++i) {
|
|
*dest = tremor_clip_sample(*src++);
|
|
dest += channels;
|
|
}
|
|
}
|
|
#else
|
|
PcmInterleaveFloat(buffer,
|
|
ConstBuffer<const in_sample_t *>(pcm,
|
|
channels),
|
|
n_frames);
|
|
#endif
|
|
|
|
vorbis_synthesis_read(&dsp, n_frames);
|
|
|
|
const size_t nbytes = n_frames * frame_size;
|
|
auto cmd = client.SubmitData(input_stream,
|
|
buffer, nbytes,
|
|
0);
|
|
if (cmd != DecoderCommand::NONE)
|
|
throw cmd;
|
|
|
|
return true;
|
|
}
|
|
|
|
void
|
|
VorbisDecoder::SubmitPcm()
|
|
{
|
|
while (SubmitSomePcm()) {}
|
|
}
|
|
|
|
void
|
|
VorbisDecoder::OnOggPacket(const ogg_packet &_packet)
|
|
{
|
|
/* libvorbis wants non-const packets */
|
|
ogg_packet &packet = const_cast<ogg_packet &>(_packet);
|
|
|
|
if (remaining_header_packets > 0) {
|
|
if (vorbis_synthesis_headerin(&vi, &vc, &packet) != 0)
|
|
throw std::runtime_error("Unrecognized Vorbis header packet");
|
|
|
|
if (--remaining_header_packets > 0)
|
|
return;
|
|
|
|
if (audio_format.IsDefined()) {
|
|
/* TODO: change the MPD decoder plugin API to
|
|
allow mid-song AudioFormat changes */
|
|
if ((unsigned)vi.rate != audio_format.sample_rate ||
|
|
(unsigned)vi.channels != audio_format.channels)
|
|
throw std::runtime_error("Next stream has different audio format");
|
|
} else
|
|
SubmitInit();
|
|
|
|
vorbis_send_comments(client, input_stream, vc.user_comments);
|
|
|
|
ReplayGainInfo rgi;
|
|
if (vorbis_comments_to_replay_gain(rgi, vc.user_comments))
|
|
client.SubmitReplayGain(&rgi);
|
|
} else {
|
|
if (!dsp_initialized) {
|
|
dsp_initialized = true;
|
|
|
|
vorbis_synthesis_init(&dsp, &vi);
|
|
vorbis_block_init(&dsp, &block);
|
|
}
|
|
|
|
if (vorbis_synthesis(&block, &packet) != 0) {
|
|
/* ignore bad packets, but give the MPD core a
|
|
chance to stop us */
|
|
auto cmd = client.GetCommand();
|
|
if (cmd != DecoderCommand::NONE)
|
|
throw cmd;
|
|
return;
|
|
}
|
|
|
|
if (vorbis_synthesis_blockin(&dsp, &block) != 0)
|
|
throw std::runtime_error("vorbis_synthesis_blockin() failed");
|
|
|
|
SubmitPcm();
|
|
|
|
#ifndef HAVE_TREMOR
|
|
if (packet.granulepos > 0)
|
|
client.SubmitTimestamp(FloatDuration(vorbis_granule_time(&dsp, packet.granulepos)));
|
|
#endif
|
|
}
|
|
}
|
|
|
|
void
|
|
VorbisDecoder::OnOggEnd()
|
|
{
|
|
}
|
|
|
|
/* public */
|
|
|
|
static bool
|
|
vorbis_init(gcc_unused const ConfigBlock &block)
|
|
{
|
|
#ifndef HAVE_TREMOR
|
|
LogDebug(vorbis_domain, vorbis_version_string());
|
|
#endif
|
|
return true;
|
|
}
|
|
|
|
static void
|
|
vorbis_stream_decode(DecoderClient &client,
|
|
InputStream &input_stream)
|
|
{
|
|
if (ogg_codec_detect(&client, input_stream) != OGG_CODEC_VORBIS)
|
|
return;
|
|
|
|
/* rewind the stream, because ogg_codec_detect() has
|
|
moved it */
|
|
try {
|
|
input_stream.LockRewind();
|
|
} catch (...) {
|
|
}
|
|
|
|
DecoderReader reader(client, input_stream);
|
|
VorbisDecoder d(reader);
|
|
|
|
while (true) {
|
|
try {
|
|
d.Visit();
|
|
break;
|
|
} catch (DecoderCommand cmd) {
|
|
if (cmd == DecoderCommand::SEEK) {
|
|
if (d.Seek(client.GetSeekFrame()))
|
|
client.CommandFinished();
|
|
else
|
|
client.SeekError();
|
|
} else if (cmd != DecoderCommand::NONE)
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void
|
|
VisitVorbisDuration(InputStream &is,
|
|
OggSyncState &sync, OggStreamState &stream,
|
|
unsigned sample_rate,
|
|
TagHandler &handler) noexcept
|
|
{
|
|
ogg_packet packet;
|
|
|
|
if (!OggSeekFindEOS(sync, stream, packet, is))
|
|
return;
|
|
|
|
const auto duration =
|
|
SongTime::FromScale<uint64_t>(packet.granulepos,
|
|
sample_rate);
|
|
handler.OnDuration(duration);
|
|
}
|
|
|
|
static bool
|
|
vorbis_scan_stream(InputStream &is, TagHandler &handler) noexcept
|
|
{
|
|
/* initialize libogg */
|
|
|
|
InputStreamReader reader(is);
|
|
OggSyncState sync(reader);
|
|
|
|
ogg_page first_page;
|
|
if (!sync.ExpectPage(first_page))
|
|
return false;
|
|
|
|
OggStreamState stream(first_page);
|
|
|
|
/* initialize libvorbis */
|
|
|
|
vorbis_info vi;
|
|
vorbis_info_init(&vi);
|
|
AtScopeExit(&) { vorbis_info_clear(&vi); };
|
|
|
|
vorbis_comment vc;
|
|
vorbis_comment_init(&vc);
|
|
AtScopeExit(&) { vorbis_comment_clear(&vc); };
|
|
|
|
/* feed the first 3 packets to libvorbis */
|
|
|
|
for (unsigned i = 0; i < 3; ++i) {
|
|
ogg_packet packet;
|
|
if (!OggReadPacket(sync, stream, packet) ||
|
|
vorbis_synthesis_headerin(&vi, &vc, &packet) != 0)
|
|
return false;
|
|
}
|
|
|
|
/* visit the Vorbis comments we just read */
|
|
|
|
vorbis_comments_scan(vc.user_comments, handler);
|
|
|
|
/* check the song duration by locating the e_o_s packet */
|
|
|
|
VisitVorbisDuration(is, sync, stream, vi.rate, handler);
|
|
|
|
try {
|
|
handler.OnAudioFormat(VorbisDecoder::CheckAudioFormat(vi));
|
|
} catch (...) {
|
|
}
|
|
|
|
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",
|
|
vorbis_init,
|
|
nullptr,
|
|
vorbis_stream_decode,
|
|
nullptr,
|
|
nullptr,
|
|
vorbis_scan_stream,
|
|
nullptr,
|
|
vorbis_suffixes,
|
|
vorbis_mime_types
|
|
};
|