From 1536b5a9d602688354648106ca8d0e34cac3c933 Mon Sep 17 00:00:00 2001
From: Max Kellermann <max@duempel.org>
Date: Tue, 4 Sep 2012 09:26:18 +0200
Subject: [PATCH] src/decoder/opus: new decoder plugin for the Opus codec

Using libopus and libogg.
---
 INSTALL                           |   3 +
 Makefile.am                       |  15 ++
 NEWS                              |   2 +
 configure.ac                      |  17 +-
 src/decoder/OggUtil.cxx           |  56 +++++
 src/decoder/OggUtil.hxx           |  48 ++++
 src/decoder/OpusDecoderPlugin.cxx | 366 ++++++++++++++++++++++++++++++
 src/decoder/OpusDecoderPlugin.h   |  25 ++
 src/decoder/OpusHead.cxx          |  44 ++++
 src/decoder/OpusHead.hxx          |  30 +++
 src/decoder/OpusReader.hxx        |  97 ++++++++
 src/decoder/OpusTags.cxx          |  77 +++++++
 src/decoder/OpusTags.hxx          |  31 +++
 src/decoder/ogg_codec.c           |   3 +
 src/decoder/ogg_codec.h           |   1 +
 src/decoder_list.c                |   4 +
 16 files changed, 818 insertions(+), 1 deletion(-)
 create mode 100644 src/decoder/OggUtil.cxx
 create mode 100644 src/decoder/OggUtil.hxx
 create mode 100644 src/decoder/OpusDecoderPlugin.cxx
 create mode 100644 src/decoder/OpusDecoderPlugin.h
 create mode 100644 src/decoder/OpusHead.cxx
 create mode 100644 src/decoder/OpusHead.hxx
 create mode 100644 src/decoder/OpusReader.hxx
 create mode 100644 src/decoder/OpusTags.cxx
 create mode 100644 src/decoder/OpusTags.hxx

diff --git a/INSTALL b/INSTALL
index fdebbe71d..6139f787f 100644
--- a/INSTALL
+++ b/INSTALL
@@ -81,6 +81,9 @@ Alternative for MP3 support.
 Ogg Vorbis - http://www.xiph.org/ogg/vorbis/
 For Ogg Vorbis support.  You will need libogg and libvorbis.
 
+libopus - http://libopus.sourceforge.net/
+Opus codec support
+
 FLAC - http://flac.sourceforge.net/
 For FLAC support.  You will need version 1.1.0 or higher of libflac.
 
diff --git a/Makefile.am b/Makefile.am
index a0f4b2a77..ed284b50a 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -538,6 +538,7 @@ libdecoder_plugins_a_CPPFLAGS = $(AM_CPPFLAGS) \
 	$(WAVPACK_CFLAGS) \
 	$(MAD_CFLAGS) \
 	$(MPG123_CFLAGS) \
+	$(OPUS_CFLAGS) \
 	$(FFMPEG_CFLAGS) \
 	$(MPCDEC_CFLAGS) \
 	$(FAAD_CFLAGS)
@@ -555,6 +556,7 @@ DECODER_LIBS = \
 	$(WAVPACK_LIBS) \
 	$(MAD_LIBS) \
 	$(MPG123_LIBS) \
+	$(OPUS_LIBS) \
 	$(MP4FF_LIBS) \
 	$(FFMPEG_LIBS) \
 	$(MPCDEC_LIBS) \
@@ -574,6 +576,19 @@ if HAVE_MPCDEC
 libdecoder_plugins_a_SOURCES += src/decoder/mpcdec_decoder_plugin.c
 endif
 
+if HAVE_OPUS
+libdecoder_plugins_a_SOURCES += \
+	src/decoder/OggUtil.cxx \
+	src/decoder/OggUtil.hxx \
+	src/decoder/OpusReader.hxx \
+	src/decoder/OpusHead.hxx \
+	src/decoder/OpusHead.cxx \
+	src/decoder/OpusTags.cxx \
+	src/decoder/OpusTags.hxx \
+	src/decoder/OpusDecoderPlugin.cxx \
+	src/decoder/OpusDecoderPlugin.h
+endif
+
 if HAVE_WAVPACK
 libdecoder_plugins_a_SOURCES += src/decoder/wavpack_decoder_plugin.c
 endif
diff --git a/NEWS b/NEWS
index 9cde9bce3..429e16fa4 100644
--- a/NEWS
+++ b/NEWS
@@ -1,4 +1,6 @@
 ver 0.18 (2012/??/??)
+* decoder:
+  - opus: new decoder plugin for the Opus codec
 * improved decoder/output error reporting
 
 ver 0.17.2 (2012/??/??)
diff --git a/configure.ac b/configure.ac
index b33760084..c72dfdf15 100644
--- a/configure.ac
+++ b/configure.ac
@@ -329,6 +329,11 @@ AC_ARG_ENABLE(openal,
 		[enable OpenAL support (default: disable)]),,
 	enable_openal=no)
 
+AC_ARG_ENABLE(opus,
+	AS_HELP_STRING([--enable-opus],
+		[enable Opus codec support (default: auto)]),,
+	enable_opus=auto)
+
 AC_ARG_ENABLE(oss,
 	AS_HELP_STRING([--disable-oss],
 		[disable OSS support (default: enable)]),,
@@ -919,6 +924,14 @@ if test x$enable_modplug = xyes; then
 fi
 AM_CONDITIONAL(HAVE_MODPLUG, test x$enable_modplug = xyes)
 
+dnl -------------------------------- libopus ----------------------------------
+MPD_AUTO_PKG(opus, OPUS, [opus ogg],
+	[opus decoder plugin], [libopus not found])
+if test x$enable_opus = xyes; then
+	AC_DEFINE(HAVE_OPUS, 1, [Define to use libopus])
+fi
+AM_CONDITIONAL(HAVE_OPUS, test x$enable_opus = xyes)
+
 dnl -------------------------------- libsndfile -------------------------------
 dnl See above test, which may disable this.
 MPD_AUTO_PKG(sndfile, SNDFILE, [sndfile],
@@ -1096,6 +1109,7 @@ if
 	test x$enable_mp4 = xno &&
 	test x$enable_mpc = xno &&
 	test x$enable_mpg123 = xno &&
+	test x$enable_opus = xno &&
 	test x$enable_sidplay = xno &&
 	test x$enable_tremor = xno &&
 	test x$enable_vorbis = xno &&
@@ -1106,7 +1120,7 @@ if
 fi
 
 AM_CONDITIONAL(HAVE_XIPH,
-	test x$enable_vorbis = xyes || test x$enable_tremor = xyes || test x$enable_flac = xyes)
+	test x$enable_vorbis = xyes || test x$enable_tremor = xyes || test x$enable_flac = xyes || test x$enable_opus = xyes)
 
 AM_CONDITIONAL(HAVE_FLAC_COMMON,
 	  test x$enable_flac = xyes)
@@ -1581,6 +1595,7 @@ results(mpg123, [MPG123])
 results(mp4, [MP4])
 results(mpc, [Musepack])
 printf '\n\t'
+results(opus, [OPUS])
 results(tremor, [OggTremor])
 results(vorbis, [OggVorbis])
 results(audiofile, [WAVE])
diff --git a/src/decoder/OggUtil.cxx b/src/decoder/OggUtil.cxx
new file mode 100644
index 000000000..99f73d48e
--- /dev/null
+++ b/src/decoder/OggUtil.cxx
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2003-2012 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"
+
+extern "C" {
+#include "decoder_api.h"
+}
+
+bool
+OggFeed(ogg_sync_state &oy, struct decoder *decoder,
+	struct input_stream *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,
+	      struct decoder *decoder, struct input_stream *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;
+	}
+}
diff --git a/src/decoder/OggUtil.hxx b/src/decoder/OggUtil.hxx
new file mode 100644
index 000000000..95bf6472f
--- /dev/null
+++ b/src/decoder/OggUtil.hxx
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2003-2012 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 <ogg/ogg.h>
+
+#include <stddef.h>
+
+/**
+ * Feed data from the #input_stream into the #ogg_sync_state.
+ *
+ * @return false on error or end-of-file
+ */
+bool
+OggFeed(ogg_sync_state &oy, struct decoder *decoder,
+	struct input_stream *input_stream, 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,
+	      struct decoder *decoder, struct input_stream *input_stream);
+
+#endif
diff --git a/src/decoder/OpusDecoderPlugin.cxx b/src/decoder/OpusDecoderPlugin.cxx
new file mode 100644
index 000000000..35e368ca9
--- /dev/null
+++ b/src/decoder/OpusDecoderPlugin.cxx
@@ -0,0 +1,366 @@
+/*
+ * Copyright (C) 2003-2012 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 "OpusHead.hxx"
+#include "OpusTags.hxx"
+#include "OggUtil.hxx"
+
+extern "C" {
+#include "ogg_codec.h"
+#include "decoder_api.h"
+}
+
+#include "audio_check.h"
+#include "tag_handler.h"
+
+#include <opus.h>
+#include <ogg/ogg.h>
+
+#include <glib.h>
+
+#include <stdio.h>
+
+#undef G_LOG_DOMAIN
+#define G_LOG_DOMAIN "opus"
+
+static const 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(G_GNUC_UNUSED const struct config_param *param)
+{
+	g_debug("%s", opus_get_version_string());
+
+	return true;
+}
+
+class MPDOpusDecoder {
+	struct decoder *decoder;
+	struct input_stream *input_stream;
+
+	ogg_stream_state os;
+
+	OpusDecoder *opus_decoder = nullptr;
+	opus_int16 *output_buffer = nullptr;
+	unsigned output_size = 0;
+
+	bool os_initialized = false;
+	bool found_opus = false;
+
+	int opus_serialno;
+
+	size_t frame_size;
+
+public:
+	MPDOpusDecoder(struct decoder *_decoder,
+		       struct input_stream *_input_stream)
+		:decoder(_decoder), input_stream(_input_stream) {}
+	~MPDOpusDecoder();
+
+	enum decoder_command HandlePage(ogg_page &page);
+	enum decoder_command HandlePacket(const ogg_packet &packet);
+	enum decoder_command HandleBOS(const ogg_packet &packet);
+	enum decoder_command HandleTags(const ogg_packet &packet);
+	enum decoder_command HandleAudio(const ogg_packet &packet);
+};
+
+MPDOpusDecoder::~MPDOpusDecoder()
+{
+	g_free(output_buffer);
+
+	if (opus_decoder != nullptr)
+		opus_decoder_destroy(opus_decoder);
+
+	if (os_initialized)
+		ogg_stream_clear(&os);
+}
+
+enum decoder_command
+MPDOpusDecoder::HandlePage(ogg_page &page)
+{
+	const auto page_serialno = ogg_page_serialno(&page);
+	if (!os_initialized) {
+		os_initialized = true;
+		ogg_stream_init(&os, page_serialno);
+	} else if (page_serialno != os.serialno)
+		ogg_stream_reset_serialno(&os, page_serialno);
+
+	ogg_stream_pagein(&os, &page);
+
+	ogg_packet packet;
+	while (ogg_stream_packetout(&os, &packet) == 1) {
+		enum decoder_command cmd = HandlePacket(packet);
+		if (cmd != DECODE_COMMAND_NONE)
+			return cmd;
+	}
+
+	return DECODE_COMMAND_NONE;
+}
+
+enum decoder_command
+MPDOpusDecoder::HandlePacket(const ogg_packet &packet)
+{
+	if (packet.e_o_s)
+		return DECODE_COMMAND_STOP;
+
+	if (packet.b_o_s)
+		return HandleBOS(packet);
+	else if (!found_opus)
+		return DECODE_COMMAND_STOP;
+
+	if (IsOpusTags(packet))
+		return HandleTags(packet);
+
+	return HandleAudio(packet);
+}
+
+enum decoder_command
+MPDOpusDecoder::HandleBOS(const ogg_packet &packet)
+{
+	assert(packet.b_o_s);
+
+	if (found_opus || !IsOpusHead(packet))
+		return DECODE_COMMAND_STOP;
+
+	unsigned channels;
+	if (!ScanOpusHeader(packet.packet, packet.bytes, channels) ||
+	    !audio_valid_channel_count(channels))
+		return DECODE_COMMAND_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) {
+		g_warning("libopus error: %s",
+			  opus_strerror(opus_error));
+		return DECODE_COMMAND_STOP;
+	}
+
+	struct audio_format audio_format;
+	audio_format_init(&audio_format, opus_sample_rate,
+			  SAMPLE_FORMAT_S16, channels);
+	decoder_initialized(decoder, &audio_format, false, -1);
+	frame_size = audio_format_frame_size(&audio_format);
+
+	/* 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);
+}
+
+enum decoder_command
+MPDOpusDecoder::HandleTags(const ogg_packet &packet)
+{
+	struct tag *tag = tag_new();
+
+	enum decoder_command cmd;
+	if (ScanOpusTags(packet.packet, packet.bytes, &add_tag_handler, tag) &&
+	    !tag_is_empty(tag))
+		cmd = decoder_tag(decoder, input_stream, tag);
+	else
+		cmd = decoder_get_command(decoder);
+
+	tag_free(tag);
+	return cmd;
+}
+
+enum decoder_command
+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) {
+		g_warning("%s", opus_strerror(nframes));
+		return DECODE_COMMAND_STOP;
+	}
+
+	if (nframes > 0) {
+		const size_t nbytes = nframes * frame_size;
+		enum decoder_command cmd =
+			decoder_data(decoder, input_stream,
+				     output_buffer, nbytes,
+				     0);
+		if (cmd != DECODE_COMMAND_NONE)
+			return cmd;
+	}
+
+	return DECODE_COMMAND_NONE;
+}
+
+static void
+mpd_opus_stream_decode(struct decoder *decoder,
+		       struct input_stream *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_lock_seek(input_stream, 0, SEEK_SET, nullptr);
+
+	MPDOpusDecoder d(decoder, input_stream);
+
+	ogg_sync_state oy;
+	ogg_sync_init(&oy);
+
+	while (true) {
+		if (!OggFeed(oy, decoder, input_stream, 1024))
+			break;
+
+		ogg_page page;
+		while (ogg_sync_pageout(&oy, &page) == 1) {
+			enum decoder_command cmd = d.HandlePage(page);
+			if (cmd != DECODE_COMMAND_NONE)
+				break;
+		}
+	}
+
+	ogg_sync_clear(&oy);
+}
+
+static bool
+mpd_opus_scan_stream(struct input_stream *is,
+		     const struct tag_handler *handler, void *handler_ctx)
+{
+	ogg_sync_state oy;
+	ogg_sync_init(&oy);
+
+	ogg_page page;
+	if (!OggExpectPage(oy, page, nullptr, is)) {
+		ogg_sync_clear(&oy);
+		return false;
+	}
+
+	/* read at most two more pages */
+	unsigned remaining_pages = 2;
+
+	bool result = false;
+
+	ogg_stream_state os;
+	ogg_stream_init(&os, ogg_page_serialno(&page));
+	ogg_stream_pagein(&os, &page);
+
+	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 (!OggExpectPage(oy, page, nullptr, is)) {
+				result = false;
+				break;
+			}
+
+			ogg_stream_pagein(&os, &page);
+			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,
+					  handler, handler_ctx))
+				result = false;
+
+			break;
+		}
+	}
+
+	ogg_stream_clear(&os);
+	ogg_sync_clear(&oy);
+
+	return result;
+}
+
+static const char *const opus_suffixes[] = {
+	"opus",
+	"ogg",
+	"oga",
+	nullptr
+};
+
+static const char *const opus_mime_types[] = {
+	"audio/opus",
+	nullptr
+};
+
+const struct decoder_plugin 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/OpusDecoderPlugin.h b/src/decoder/OpusDecoderPlugin.h
new file mode 100644
index 000000000..c95d6ded3
--- /dev/null
+++ b/src/decoder/OpusDecoderPlugin.h
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2003-2012 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 decoder_plugin opus_decoder_plugin;
+
+#endif
diff --git a/src/decoder/OpusHead.cxx b/src/decoder/OpusHead.cxx
new file mode 100644
index 000000000..c57e08e10
--- /dev/null
+++ b/src/decoder/OpusHead.cxx
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2003-2012 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 <stdint.h>
+#include <string.h>
+
+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/OpusHead.hxx b/src/decoder/OpusHead.hxx
new file mode 100644
index 000000000..9f75c4f70
--- /dev/null
+++ b/src/decoder/OpusHead.hxx
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2003-2012 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 <stddef.h>
+
+bool
+ScanOpusHeader(const void *data, size_t size, unsigned &channels_r);
+
+#endif
diff --git a/src/decoder/OpusReader.hxx b/src/decoder/OpusReader.hxx
new file mode 100644
index 000000000..2cfc14118
--- /dev/null
+++ b/src/decoder/OpusReader.hxx
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2003-2012 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 <stdint.h>
+#include <string.h>
+
+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;
+
+		return strndup(src, length);
+	}
+};
+
+#endif
diff --git a/src/decoder/OpusTags.cxx b/src/decoder/OpusTags.cxx
new file mode 100644
index 000000000..cb35a6247
--- /dev/null
+++ b/src/decoder/OpusTags.cxx
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2003-2012 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.h"
+#include "tag_handler.h"
+
+#include <stdint.h>
+#include <string.h>
+#include <stdlib.h>
+
+static void
+ScanOneOpusTag(const char *name, const char *value,
+	       const struct tag_handler *handler, void *ctx)
+{
+	tag_handler_invoke_pair(handler, ctx, name, value);
+
+	if (handler->tag != nullptr) {
+		enum tag_type t = tag_table_lookup_i(xiph_tags, name);
+		if (t != TAG_NUM_OF_ITEM_TYPES)
+			tag_handler_invoke_tag(handler, ctx, t, value);
+	}
+}
+
+bool
+ScanOpusTags(const void *data, size_t size,
+	     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, handler, ctx);
+		}
+
+		free(p);
+	}
+
+	return true;
+}
diff --git a/src/decoder/OpusTags.hxx b/src/decoder/OpusTags.hxx
new file mode 100644
index 000000000..2f3eec844
--- /dev/null
+++ b/src/decoder/OpusTags.hxx
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2003-2012 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 <stddef.h>
+
+bool
+ScanOpusTags(const void *data, size_t size,
+	     const struct tag_handler *handler, void *ctx);
+
+#endif
diff --git a/src/decoder/ogg_codec.c b/src/decoder/ogg_codec.c
index e016e5181..7416f27da 100644
--- a/src/decoder/ogg_codec.c
+++ b/src/decoder/ogg_codec.c
@@ -41,5 +41,8 @@ ogg_codec_detect(struct decoder *decoder, struct input_stream *is)
 	    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/ogg_codec.h b/src/decoder/ogg_codec.h
index d99758cad..fd1fecfbb 100644
--- a/src/decoder/ogg_codec.h
+++ b/src/decoder/ogg_codec.h
@@ -30,6 +30,7 @@ enum ogg_codec {
 	OGG_CODEC_UNKNOWN,
 	OGG_CODEC_VORBIS,
 	OGG_CODEC_FLAC,
+	OGG_CODEC_OPUS,
 };
 
 enum ogg_codec
diff --git a/src/decoder_list.c b/src/decoder_list.c
index 177b632ad..3ea704e98 100644
--- a/src/decoder_list.c
+++ b/src/decoder_list.c
@@ -26,6 +26,7 @@
 #include "decoder/pcm_decoder_plugin.h"
 #include "decoder/dsdiff_decoder_plugin.h"
 #include "decoder/dsf_decoder_plugin.h"
+#include "decoder/OpusDecoderPlugin.h"
 
 #include <glib.h>
 
@@ -66,6 +67,9 @@ const struct decoder_plugin *const decoder_plugins[] = {
 #ifdef HAVE_FLAC
 	&flac_decoder_plugin,
 #endif
+#ifdef HAVE_OPUS
+	&opus_decoder_plugin,
+#endif
 #ifdef ENABLE_SNDFILE
 	&sndfile_decoder_plugin,
 #endif