diff --git a/meson_options.txt b/meson_options.txt index 7c57c9da1..48622b5f1 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -107,6 +107,7 @@ option('zzip', type: 'feature', description: 'ZIP support using zziplib') # option('id3tag', type: 'feature', description: 'ID3 support using libid3tag') +option('chromaprint', type: 'feature', description: 'ChromaPrint / AcoustID support') # # Decoder plugins diff --git a/src/tag/Chromaprint.hxx b/src/tag/Chromaprint.hxx new file mode 100644 index 000000000..3aaeb74d9 --- /dev/null +++ b/src/tag/Chromaprint.hxx @@ -0,0 +1,73 @@ +/* + * 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. + */ + +#ifndef MPD_CHROMAPRINT_HXX +#define MPD_CHROMAPRINT_HXX + +#include "util/ScopeExit.hxx" + +#include + +#include +#include + +namespace Chromaprint { + +class Context { + ChromaprintContext *const ctx; + +public: + Context() noexcept + :ctx(chromaprint_new(CHROMAPRINT_ALGORITHM_DEFAULT)) {} + + ~Context() noexcept { + chromaprint_free(ctx); + } + + Context(const Context &) = delete; + Context &operator=(const Context &) = delete; + + void Start(unsigned sample_rate, unsigned num_channels) { + if (chromaprint_start(ctx, sample_rate, num_channels) != 1) + throw std::runtime_error("chromaprint_start() failed"); + } + + void Feed(const int16_t *data, size_t size) { + if (chromaprint_feed(ctx, data, size) != 1) + throw std::runtime_error("chromaprint_feed() failed"); + } + + void Finish() { + if (chromaprint_finish(ctx) != 1) + throw std::runtime_error("chromaprint_finish() failed"); + } + + std::string GetFingerprint() const { + char *fingerprint; + if (chromaprint_get_fingerprint(ctx, &fingerprint) != 1) + throw std::runtime_error("chromaprint_get_fingerprint() failed"); + + AtScopeExit(fingerprint) { chromaprint_dealloc(fingerprint); }; + return fingerprint; + } +}; + +} //namespace Chromaprint + +#endif diff --git a/src/tag/meson.build b/src/tag/meson.build index c423417d1..08b9e5e92 100644 --- a/src/tag/meson.build +++ b/src/tag/meson.build @@ -33,6 +33,9 @@ if libid3tag_dep.found() ] endif +chromaprint_dep = dependency('libchromaprint', required: get_option('chromaprint')) +conf.set('ENABLE_CHROMAPRINT', chromaprint_dep.found()) + tag = static_library( 'tag', tag_sources, diff --git a/test/RunChromaprint.cxx b/test/RunChromaprint.cxx new file mode 100644 index 000000000..311ad4eb7 --- /dev/null +++ b/test/RunChromaprint.cxx @@ -0,0 +1,273 @@ +/* + * 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 "config/File.hxx" +#include "config/Migrate.hxx" +#include "config/Data.hxx" +#include "tag/Chromaprint.hxx" +#include "pcm/PcmConvert.hxx" +#include "event/Thread.hxx" +#include "decoder/DecoderList.hxx" +#include "decoder/DecoderPlugin.hxx" +#include "decoder/Client.hxx" +#include "input/Init.hxx" +#include "input/InputStream.hxx" +#include "fs/Path.hxx" +#include "AudioFormat.hxx" +#include "util/OptionDef.hxx" +#include "util/OptionParser.hxx" +#include "util/PrintException.hxx" +#include "Log.hxx" +#include "LogBackend.hxx" + +#include + +#include +#include +#include +#include + +struct CommandLine { + const char *decoder = nullptr; + const char *uri = nullptr; + + Path config_path = nullptr; + + bool verbose = false; +}; + +enum Option { + OPTION_CONFIG, + OPTION_VERBOSE, +}; + +static constexpr OptionDef option_defs[] = { + {"config", 0, true, "Load a MPD configuration file"}, + {"verbose", 'v', false, "Verbose logging"}, +}; + +static CommandLine +ParseCommandLine(int argc, char **argv) +{ + CommandLine c; + + OptionParser option_parser(option_defs, argc, argv); + while (auto o = option_parser.Next()) { + switch (Option(o.index)) { + case OPTION_CONFIG: + c.config_path = Path::FromFS(o.value); + break; + + case OPTION_VERBOSE: + c.verbose = true; + break; + } + } + + auto args = option_parser.GetRemaining(); + if (args.size != 2) + throw std::runtime_error("Usage: RunChromaprint [--verbose] [--config=FILE] DECODER URI"); + + c.decoder = args[0]; + c.uri = args[1]; + return c; +} + +class GlobalInit { + ConfigData config; + EventThread io_thread; + +public: + GlobalInit(Path config_path, bool verbose) { + SetLogThreshold(verbose ? LogLevel::DEBUG : LogLevel::INFO); + + if (!config_path.IsNull()) { + ReadConfigFile(config, config_path); + Migrate(config); + } + + io_thread.Start(); + + input_stream_global_init(config, + io_thread.GetEventLoop()); + decoder_plugin_init_all(config); + + pcm_convert_global_init(config); + } + + ~GlobalInit() { + decoder_plugin_deinit_all(); + input_stream_global_finish(); + } +}; + +class ChromaprintDecoderClient final : public DecoderClient { + bool ready = false; + + bool need_convert = false; + + PcmConvert convert; + + Chromaprint::Context chromaprint; + + uint64_t remaining_bytes; + +public: + Mutex mutex; + + ~ChromaprintDecoderClient() noexcept { + if (need_convert) + convert.Close(); + } + + void PrintResult(); + + /* virtual methods from DecoderClient */ + void Ready(AudioFormat audio_format, + bool seekable, SignedSongTime duration) override; + + DecoderCommand GetCommand() noexcept override { + return remaining_bytes > 0 + ? DecoderCommand::NONE + : DecoderCommand::STOP; + } + + void CommandFinished() override {} + + SongTime GetSeekTime() noexcept override { + return SongTime::zero(); + } + + uint64_t GetSeekFrame() noexcept override { + return 0; + } + + void SeekError() override {} + + InputStreamPtr OpenUri(const char *) override { + throw std::runtime_error("Not implemented"); + } + + size_t Read(InputStream &is, void *buffer, size_t length) override { + return is.LockRead(buffer, length); + } + + void SubmitTimestamp(FloatDuration) override {} + DecoderCommand SubmitData(InputStream *is, + const void *data, size_t length, + uint16_t kbit_rate) override; + + DecoderCommand SubmitTag(InputStream *, Tag &&) override { + return GetCommand(); + } + + void SubmitReplayGain(const ReplayGainInfo *) {} + void SubmitMixRamp(MixRampInfo &&) {} +}; + +void +ChromaprintDecoderClient::PrintResult() +{ + if (!ready) + throw std::runtime_error("Decoding failed"); + + if (need_convert) { + auto flushed = convert.Flush(); + auto data = ConstBuffer::FromVoid(flushed); + chromaprint.Feed(data.data, data.size); + } + + chromaprint.Finish(); + + printf("%s\n", chromaprint.GetFingerprint().c_str()); +} + +void +ChromaprintDecoderClient::Ready(AudioFormat audio_format, bool, SignedSongTime) +{ + /* feed the first two minutes into libchromaprint */ + remaining_bytes = audio_format.TimeToSize(std::chrono::minutes(2)); + + if (audio_format.format != SampleFormat::S16) { + const AudioFormat src_audio_format = audio_format; + audio_format.format = SampleFormat::S16; + + convert.Open(src_audio_format, audio_format); + need_convert = true; + } + + chromaprint.Start(audio_format.sample_rate, audio_format.channels); + + ready = true; +} + +DecoderCommand +ChromaprintDecoderClient::SubmitData(InputStream *, + const void *_data, size_t length, + uint16_t) +{ + if (length > remaining_bytes) + remaining_bytes = 0; + else + remaining_bytes -= length; + + ConstBuffer src{_data, length}; + ConstBuffer data; + + if (need_convert) { + auto result = convert.Convert(src); + data = ConstBuffer::FromVoid(result); + } else + data = ConstBuffer::FromVoid(src); + + chromaprint.Feed(data.data, data.size); + + return GetCommand(); +} + +int main(int argc, char **argv) +try { + const auto c = ParseCommandLine(argc, argv); + + const GlobalInit init(c.config_path, c.verbose); + + const DecoderPlugin *plugin = decoder_plugin_from_name(c.decoder); + if (plugin == nullptr) { + fprintf(stderr, "No such decoder: %s\n", c.decoder); + return EXIT_FAILURE; + } + + ChromaprintDecoderClient client; + if (plugin->file_decode != nullptr) { + plugin->FileDecode(client, Path::FromFS(c.uri)); + } else if (plugin->stream_decode != nullptr) { + auto is = InputStream::OpenReady(c.uri, client.mutex); + plugin->StreamDecode(client, *is); + } else { + fprintf(stderr, "Decoder plugin is not usable\n"); + return EXIT_FAILURE; + } + + client.PrintResult(); + return EXIT_SUCCESS; +} catch (...) { + PrintException(std::current_exception()); + return EXIT_FAILURE; +} diff --git a/test/meson.build b/test/meson.build index f88dd9f80..6fe6f6f29 100644 --- a/test/meson.build +++ b/test/meson.build @@ -381,6 +381,26 @@ executable( ], ) +# +# Tag +# + +if chromaprint_dep.found() + executable( + 'RunChromaprint', + 'RunChromaprint.cxx', + '../src/Log.cxx', + '../src/LogBackend.cxx', + include_directories: inc, + dependencies: [ + decoder_glue_dep, + input_glue_dep, + archive_glue_dep, + chromaprint_dep, + ], + ) +endif + # # Decoder #