release v0.22.11
-----BEGIN PGP SIGNATURE----- iQJEBAABCgAuFiEEA5IzWngIOJSkMBxDI26KWMbbRRIFAmElU1oQHG1heEBtdXNp Y3BkLm9yZwAKCRAjbopYxttFEnfUD/0fmlPB0ud6UdyedOp6yqbZoWvUanGFyrFX 4eaTfSZq4qAs3JKSLqGVcseDNG4wk/VkDhXekbpFPIgCjBQpypxusQ1NowoT6gps NYFClU2cxBnGYHMZQeTT+4WB4VRGreZGJ42IfQbKDyrSNImKu+5CmtDvsoGezeMK OnL9EkNR3D7nl6uIUstuyOG6f/x1QNNVlntatslMxPYfnrZgHElwZiodqEGDh0c4 XxBQmVyAX+AGjmwepXTQvUap2rd2x0mW7jQ7C3FFTP/eSImPsspQb4zO/TGnp3/o 9pSIDUSlnOS78C3GIJCZB4ac7i/Gh+tSV6Y3wSJmY9s/EvqnnHwySeiJnTLG3q3G 03Wy7r64enytU3jdgjtYb3h8fCnsQjhuhmUXtDLTF3hFeBR/EkviYQOpGqVbXpzm Wx+yINtHJcUr4jikxboFSQacmf2WbJbIyZ3RhsWTWANHWDqHrMzW73E850Dg3kg3 RoeMKV3B9vFv6kolsCZR/Pq6+vFSsuDt8o+/RJqBwSKKwPP9Hm7ntlamTsJ5kA/0 kvr5WE9RcDOU+lyTjra1OW7OInxbhr05PFNBw8GfeR9QyatUT6c3hgyhoCbZ24DV oPh3TwKrwtm82TMxBktGMAWb02RebEYVpPSlVrnWhpI9vlm4gHebAxANbFVWA/ur FxQv+PXg0w== =G8gT -----END PGP SIGNATURE----- Merge tag 'v0.22.11' release v0.22.11
This commit is contained in:
commit
8a29805767
11
NEWS
11
NEWS
|
@ -17,6 +17,17 @@ ver 0.23 (not yet released)
|
||||||
- new tags "ComposerSort", "Ensemble", "Movement", "MovementNumber", and "Location"
|
- new tags "ComposerSort", "Ensemble", "Movement", "MovementNumber", and "Location"
|
||||||
* new build-time dependency: libfmt
|
* new build-time dependency: libfmt
|
||||||
|
|
||||||
|
ver 0.22.11 (2021/08/24)
|
||||||
|
* protocol
|
||||||
|
- fix "albumart" crash
|
||||||
|
* filter
|
||||||
|
- ffmpeg: pass "channel_layout" instead of "channels" to buffersrc
|
||||||
|
- ffmpeg: fix "av_buffersink_get_frame() failed: Resource temporarily unavailable"
|
||||||
|
- ffmpeg: support double-precision samples (by converting to single precision)
|
||||||
|
* Android
|
||||||
|
- build with NDK r23
|
||||||
|
- playlist_directory defaults to "/sdcard/Android/data/org.musicpd/files/playlists"
|
||||||
|
|
||||||
ver 0.22.10 (2021/08/06)
|
ver 0.22.10 (2021/08/06)
|
||||||
* protocol
|
* protocol
|
||||||
- support "albumart" for virtual tracks in CUE sheets
|
- support "albumart" for virtual tracks in CUE sheets
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="org.musicpd"
|
package="org.musicpd"
|
||||||
android:installLocation="auto"
|
android:installLocation="auto"
|
||||||
android:versionCode="58"
|
android:versionCode="59"
|
||||||
android:versionName="0.22.10">
|
android:versionName="0.22.11">
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="29"/>
|
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="29"/>
|
||||||
|
|
||||||
|
|
|
@ -24,15 +24,13 @@ android_abis = {
|
||||||
'armeabi-v7a': {
|
'armeabi-v7a': {
|
||||||
'arch': 'arm-linux-androideabi',
|
'arch': 'arm-linux-androideabi',
|
||||||
'ndk_arch': 'arm',
|
'ndk_arch': 'arm',
|
||||||
'toolchain_arch': 'arm-linux-androideabi',
|
|
||||||
'llvm_triple': 'armv7-linux-androideabi',
|
'llvm_triple': 'armv7-linux-androideabi',
|
||||||
'cflags': '-fpic -march=armv7-a -mfpu=vfpv3-d16 -mfloat-abi=softfp',
|
'cflags': '-fpic -mfpu=neon -mfloat-abi=softfp',
|
||||||
},
|
},
|
||||||
|
|
||||||
'arm64-v8a': {
|
'arm64-v8a': {
|
||||||
'arch': 'aarch64-linux-android',
|
'arch': 'aarch64-linux-android',
|
||||||
'ndk_arch': 'arm64',
|
'ndk_arch': 'arm64',
|
||||||
'toolchain_arch': 'aarch64-linux-android',
|
|
||||||
'llvm_triple': 'aarch64-linux-android',
|
'llvm_triple': 'aarch64-linux-android',
|
||||||
'cflags': '-fpic',
|
'cflags': '-fpic',
|
||||||
},
|
},
|
||||||
|
@ -40,7 +38,6 @@ android_abis = {
|
||||||
'x86': {
|
'x86': {
|
||||||
'arch': 'i686-linux-android',
|
'arch': 'i686-linux-android',
|
||||||
'ndk_arch': 'x86',
|
'ndk_arch': 'x86',
|
||||||
'toolchain_arch': 'x86',
|
|
||||||
'llvm_triple': 'i686-linux-android',
|
'llvm_triple': 'i686-linux-android',
|
||||||
'cflags': '-fPIC -march=i686 -mtune=intel -mssse3 -mfpmath=sse -m32',
|
'cflags': '-fPIC -march=i686 -mtune=intel -mssse3 -mfpmath=sse -m32',
|
||||||
},
|
},
|
||||||
|
@ -48,7 +45,6 @@ android_abis = {
|
||||||
'x86_64': {
|
'x86_64': {
|
||||||
'arch': 'x86_64-linux-android',
|
'arch': 'x86_64-linux-android',
|
||||||
'ndk_arch': 'x86_64',
|
'ndk_arch': 'x86_64',
|
||||||
'toolchain_arch': 'x86_64',
|
|
||||||
'llvm_triple': 'x86_64-linux-android',
|
'llvm_triple': 'x86_64-linux-android',
|
||||||
'cflags': '-fPIC -m64',
|
'cflags': '-fPIC -m64',
|
||||||
},
|
},
|
||||||
|
@ -84,37 +80,28 @@ class AndroidNdkToolchain:
|
||||||
ndk_arch = abi_info['ndk_arch']
|
ndk_arch = abi_info['ndk_arch']
|
||||||
android_api_level = '21'
|
android_api_level = '21'
|
||||||
|
|
||||||
# select the NDK compiler
|
|
||||||
gcc_version = '4.9'
|
|
||||||
|
|
||||||
install_prefix = os.path.join(arch_path, 'root')
|
install_prefix = os.path.join(arch_path, 'root')
|
||||||
|
|
||||||
self.arch = arch
|
self.arch = arch
|
||||||
self.install_prefix = install_prefix
|
self.install_prefix = install_prefix
|
||||||
self.toolchain_arch = abi_info['toolchain_arch']
|
|
||||||
|
|
||||||
toolchain_path = os.path.join(ndk_path, 'toolchains', self.toolchain_arch + '-' + gcc_version, 'prebuilt', build_arch)
|
|
||||||
llvm_path = os.path.join(ndk_path, 'toolchains', 'llvm', 'prebuilt', build_arch)
|
llvm_path = os.path.join(ndk_path, 'toolchains', 'llvm', 'prebuilt', build_arch)
|
||||||
llvm_triple = abi_info['llvm_triple'] + android_api_level
|
llvm_triple = abi_info['llvm_triple'] + android_api_level
|
||||||
|
|
||||||
common_flags = '-Os -g'
|
common_flags = '-Os -g'
|
||||||
common_flags += ' ' + abi_info['cflags']
|
common_flags += ' ' + abi_info['cflags']
|
||||||
|
|
||||||
toolchain_bin = os.path.join(toolchain_path, 'bin')
|
|
||||||
llvm_bin = os.path.join(llvm_path, 'bin')
|
llvm_bin = os.path.join(llvm_path, 'bin')
|
||||||
self.cc = os.path.join(llvm_bin, 'clang')
|
self.cc = os.path.join(llvm_bin, 'clang')
|
||||||
self.cxx = os.path.join(llvm_bin, 'clang++')
|
self.cxx = os.path.join(llvm_bin, 'clang++')
|
||||||
common_flags += ' -target ' + llvm_triple + ' -gcc-toolchain ' + toolchain_path
|
common_flags += ' -target ' + llvm_triple
|
||||||
|
|
||||||
common_flags += ' -fvisibility=hidden -fdata-sections -ffunction-sections'
|
common_flags += ' -fvisibility=hidden -fdata-sections -ffunction-sections'
|
||||||
|
|
||||||
# required flags from https://android.googlesource.com/platform/ndk/+/ndk-release-r20/docs/BuildSystemMaintainers.md#additional-required-arguments
|
self.ar = os.path.join(llvm_bin, 'llvm-ar')
|
||||||
common_flags += ' -fno-addrsig'
|
self.ranlib = os.path.join(llvm_bin, 'llvm-ranlib')
|
||||||
|
self.nm = os.path.join(llvm_bin, 'llvm-nm')
|
||||||
self.ar = os.path.join(toolchain_bin, arch + '-ar')
|
self.strip = os.path.join(llvm_bin, 'llvm-strip')
|
||||||
self.ranlib = os.path.join(toolchain_bin, arch + '-ranlib')
|
|
||||||
self.nm = os.path.join(toolchain_bin, arch + '-nm')
|
|
||||||
self.strip = os.path.join(toolchain_bin, arch + '-strip')
|
|
||||||
|
|
||||||
self.cflags = common_flags
|
self.cflags = common_flags
|
||||||
self.cxxflags = common_flags
|
self.cxxflags = common_flags
|
||||||
|
|
|
@ -177,7 +177,7 @@ Compiling for Android
|
||||||
You need:
|
You need:
|
||||||
|
|
||||||
* Android SDK
|
* Android SDK
|
||||||
* `Android NDK r22 <https://developer.android.com/ndk/downloads>`_
|
* `Android NDK r23 <https://developer.android.com/ndk/downloads>`_
|
||||||
* `Meson 0.49.0 <http://mesonbuild.com/>`__ and `Ninja
|
* `Meson 0.49.0 <http://mesonbuild.com/>`__ and `Ninja
|
||||||
<https://ninja-build.org/>`__
|
<https://ninja-build.org/>`__
|
||||||
* cmake
|
* cmake
|
||||||
|
|
|
@ -48,7 +48,6 @@ class OpenSSLProject(MakeProject):
|
||||||
}
|
}
|
||||||
|
|
||||||
openssl_arch = openssl_archs[toolchain.arch]
|
openssl_arch = openssl_archs[toolchain.arch]
|
||||||
cross_compile_prefix = toolchain.toolchain_arch + '-'
|
|
||||||
|
|
||||||
subprocess.check_call(['./Configure',
|
subprocess.check_call(['./Configure',
|
||||||
'no-shared',
|
'no-shared',
|
||||||
|
@ -57,7 +56,6 @@ class OpenSSLProject(MakeProject):
|
||||||
'no-tests',
|
'no-tests',
|
||||||
'no-asm', # "asm" causes build failures on Windows
|
'no-asm', # "asm" causes build failures on Windows
|
||||||
openssl_arch,
|
openssl_arch,
|
||||||
'--cross-compile-prefix=' + cross_compile_prefix,
|
|
||||||
'--prefix=' + toolchain.install_prefix],
|
'--prefix=' + toolchain.install_prefix],
|
||||||
cwd=src, env=toolchain.env)
|
cwd=src, env=toolchain.env)
|
||||||
MakeProject.build(self, toolchain, src)
|
MakeProject.build(self, toolchain, src)
|
||||||
|
|
12
src/Main.cxx
12
src/Main.cxx
|
@ -158,7 +158,17 @@ glue_daemonize_init(const struct options *options,
|
||||||
static void
|
static void
|
||||||
glue_mapper_init(const ConfigData &config)
|
glue_mapper_init(const ConfigData &config)
|
||||||
{
|
{
|
||||||
mapper_init(config.GetPath(ConfigOption::PLAYLIST_DIR));
|
auto playlist_directory = config.GetPath(ConfigOption::PLAYLIST_DIR);
|
||||||
|
|
||||||
|
#ifdef ANDROID
|
||||||
|
/* if there is no explicit configuration, store playlists in
|
||||||
|
"/sdcard/Android/data/org.musicpd/files/playlists" */
|
||||||
|
if (playlist_directory.IsNull())
|
||||||
|
playlist_directory = context->GetExternalFilesDir(Java::GetEnv(),
|
||||||
|
"playlists");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
mapper_init(std::move(playlist_directory));
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef ENABLE_DATABASE
|
#ifdef ENABLE_DATABASE
|
||||||
|
|
|
@ -26,6 +26,25 @@
|
||||||
|
|
||||||
#include "AudioManager.hxx"
|
#include "AudioManager.hxx"
|
||||||
|
|
||||||
|
AllocatedPath
|
||||||
|
Context::GetExternalFilesDir(JNIEnv *env, const char *_type) noexcept
|
||||||
|
{
|
||||||
|
assert(_type != nullptr);
|
||||||
|
|
||||||
|
Java::Class cls{env, env->GetObjectClass(Get())};
|
||||||
|
jmethodID method = env->GetMethodID(cls, "getExternalFilesDir",
|
||||||
|
"(Ljava/lang/String;)Ljava/io/File;");
|
||||||
|
assert(method);
|
||||||
|
|
||||||
|
Java::String type{env, _type};
|
||||||
|
|
||||||
|
jobject file = env->CallObjectMethod(Get(), method, type.Get());
|
||||||
|
if (Java::DiscardException(env) || file == nullptr)
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
return Java::File::ToAbsolutePath(env, file);
|
||||||
|
}
|
||||||
|
|
||||||
AllocatedPath
|
AllocatedPath
|
||||||
Context::GetCacheDir(JNIEnv *env) const noexcept
|
Context::GetCacheDir(JNIEnv *env) const noexcept
|
||||||
{
|
{
|
||||||
|
|
|
@ -30,10 +30,14 @@ public:
|
||||||
Context(JNIEnv *env, jobject obj) noexcept
|
Context(JNIEnv *env, jobject obj) noexcept
|
||||||
:Java::GlobalObject(env, obj) {}
|
:Java::GlobalObject(env, obj) {}
|
||||||
|
|
||||||
gcc_pure
|
[[gnu::pure]]
|
||||||
|
AllocatedPath GetExternalFilesDir(JNIEnv *env,
|
||||||
|
const char *type) noexcept;
|
||||||
|
|
||||||
|
[[gnu::pure]]
|
||||||
AllocatedPath GetCacheDir(JNIEnv *env) const noexcept;
|
AllocatedPath GetCacheDir(JNIEnv *env) const noexcept;
|
||||||
|
|
||||||
gcc_pure
|
[[gnu::pure]]
|
||||||
AudioManager *GetAudioManager(JNIEnv *env) noexcept;
|
AudioManager *GetAudioManager(JNIEnv *env) noexcept;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -33,10 +33,10 @@ namespace Environment {
|
||||||
/**
|
/**
|
||||||
* Determine the mount point of the external SD card.
|
* Determine the mount point of the external SD card.
|
||||||
*/
|
*/
|
||||||
gcc_pure
|
[[gnu::pure]]
|
||||||
AllocatedPath getExternalStorageDirectory() noexcept;
|
AllocatedPath getExternalStorageDirectory() noexcept;
|
||||||
|
|
||||||
gcc_pure
|
[[gnu::pure]]
|
||||||
AllocatedPath getExternalStoragePublicDirectory(const char *type) noexcept;
|
AllocatedPath getExternalStoragePublicDirectory(const char *type) noexcept;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -251,6 +251,9 @@ try {
|
||||||
|
|
||||||
AtScopeExit(db, song) { db->ReturnSong(song); };
|
AtScopeExit(db, song) { db->ReturnSong(song); };
|
||||||
|
|
||||||
|
if (song->real_uri == nullptr)
|
||||||
|
return directory_uri;
|
||||||
|
|
||||||
const char *real_uri = song->real_uri;
|
const char *real_uri = song->real_uri;
|
||||||
|
|
||||||
/* this is a simplification which is just enough for CUE
|
/* this is a simplification which is just enough for CUE
|
||||||
|
|
|
@ -32,12 +32,12 @@ extern "C" {
|
||||||
FfmpegFilter::FfmpegFilter(const AudioFormat &in_audio_format,
|
FfmpegFilter::FfmpegFilter(const AudioFormat &in_audio_format,
|
||||||
const AudioFormat &_out_audio_format,
|
const AudioFormat &_out_audio_format,
|
||||||
Ffmpeg::FilterGraph &&_graph,
|
Ffmpeg::FilterGraph &&_graph,
|
||||||
Ffmpeg::FilterContext &&_buffer_src,
|
AVFilterContext &_buffer_src,
|
||||||
Ffmpeg::FilterContext &&_buffer_sink) noexcept
|
AVFilterContext &_buffer_sink) noexcept
|
||||||
:Filter(_out_audio_format),
|
:Filter(_out_audio_format),
|
||||||
graph(std::move(_graph)),
|
graph(std::move(_graph)),
|
||||||
buffer_src(std::move(_buffer_src)),
|
buffer_src(_buffer_src),
|
||||||
buffer_sink(std::move(_buffer_sink)),
|
buffer_sink(_buffer_sink),
|
||||||
in_format(Ffmpeg::ToFfmpegSampleFormat(in_audio_format.format)),
|
in_format(Ffmpeg::ToFfmpegSampleFormat(in_audio_format.format)),
|
||||||
in_sample_rate(in_audio_format.sample_rate),
|
in_sample_rate(in_audio_format.sample_rate),
|
||||||
in_channels(in_audio_format.channels),
|
in_channels(in_audio_format.channels),
|
||||||
|
@ -61,7 +61,7 @@ FfmpegFilter::FilterPCM(ConstBuffer<void> src)
|
||||||
|
|
||||||
memcpy(frame.GetData(0), src.data, src.size);
|
memcpy(frame.GetData(0), src.data, src.size);
|
||||||
|
|
||||||
int err = av_buffersrc_add_frame(buffer_src.get(), frame.get());
|
int err = av_buffersrc_add_frame(&buffer_src, frame.get());
|
||||||
if (err < 0)
|
if (err < 0)
|
||||||
throw MakeFfmpegError(err, "av_buffersrc_write_frame() failed");
|
throw MakeFfmpegError(err, "av_buffersrc_write_frame() failed");
|
||||||
|
|
||||||
|
@ -69,7 +69,7 @@ FfmpegFilter::FilterPCM(ConstBuffer<void> src)
|
||||||
|
|
||||||
frame.Unref();
|
frame.Unref();
|
||||||
|
|
||||||
err = av_buffersink_get_frame(buffer_sink.get(), frame.get());
|
err = av_buffersink_get_frame(&buffer_sink, frame.get());
|
||||||
if (err < 0) {
|
if (err < 0) {
|
||||||
if (err == AVERROR(EAGAIN) || err == AVERROR_EOF)
|
if (err == AVERROR(EAGAIN) || err == AVERROR_EOF)
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
*/
|
*/
|
||||||
class FfmpegFilter final : public Filter {
|
class FfmpegFilter final : public Filter {
|
||||||
Ffmpeg::FilterGraph graph;
|
Ffmpeg::FilterGraph graph;
|
||||||
Ffmpeg::FilterContext buffer_src, buffer_sink;
|
AVFilterContext &buffer_src, &buffer_sink;
|
||||||
Ffmpeg::Frame frame;
|
Ffmpeg::Frame frame;
|
||||||
|
|
||||||
FfmpegBuffer interleave_buffer;
|
FfmpegBuffer interleave_buffer;
|
||||||
|
@ -51,8 +51,8 @@ public:
|
||||||
FfmpegFilter(const AudioFormat &in_audio_format,
|
FfmpegFilter(const AudioFormat &in_audio_format,
|
||||||
const AudioFormat &_out_audio_format,
|
const AudioFormat &_out_audio_format,
|
||||||
Ffmpeg::FilterGraph &&_graph,
|
Ffmpeg::FilterGraph &&_graph,
|
||||||
Ffmpeg::FilterContext &&_buffer_src,
|
AVFilterContext &_buffer_src,
|
||||||
Ffmpeg::FilterContext &&_buffer_sink) noexcept;
|
AVFilterContext &_buffer_sink) noexcept;
|
||||||
|
|
||||||
/* virtual methods from class Filter */
|
/* virtual methods from class Filter */
|
||||||
ConstBuffer<void> FilterPCM(ConstBuffer<void> src) override;
|
ConstBuffer<void> FilterPCM(ConstBuffer<void> src) override;
|
||||||
|
|
|
@ -37,39 +37,79 @@ public:
|
||||||
std::unique_ptr<Filter> Open(AudioFormat &af) override;
|
std::unique_ptr<Filter> Open(AudioFormat &af) override;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fallback for PreparedFfmpegFilter::Open() just in case the filter's
|
||||||
|
* native output format could not be determined.
|
||||||
|
*
|
||||||
|
* TODO: improve the MPD filter API to allow returning the output
|
||||||
|
* format later, and eliminate this kludge
|
||||||
|
*/
|
||||||
|
static auto
|
||||||
|
OpenWithAformat(const char *graph_string, AudioFormat &in_audio_format)
|
||||||
|
{
|
||||||
|
Ffmpeg::FilterGraph graph;
|
||||||
|
|
||||||
|
auto &buffer_src =
|
||||||
|
Ffmpeg::MakeAudioBufferSource(in_audio_format, *graph);
|
||||||
|
|
||||||
|
auto &buffer_sink = Ffmpeg::MakeAudioBufferSink(*graph);
|
||||||
|
|
||||||
|
AudioFormat out_audio_format = in_audio_format;
|
||||||
|
auto &aformat = Ffmpeg::MakeAformat(out_audio_format, *graph);
|
||||||
|
|
||||||
|
int error = avfilter_link(&aformat, 0, &buffer_sink, 0);
|
||||||
|
if (error < 0)
|
||||||
|
throw MakeFfmpegError(error, "avfilter_link() failed");
|
||||||
|
|
||||||
|
graph.ParseSingleInOut(graph_string, aformat, buffer_src);
|
||||||
|
graph.CheckAndConfigure();
|
||||||
|
|
||||||
|
return std::make_unique<FfmpegFilter>(in_audio_format,
|
||||||
|
out_audio_format,
|
||||||
|
std::move(graph),
|
||||||
|
buffer_src,
|
||||||
|
buffer_sink);
|
||||||
|
}
|
||||||
|
|
||||||
std::unique_ptr<Filter>
|
std::unique_ptr<Filter>
|
||||||
PreparedFfmpegFilter::Open(AudioFormat &in_audio_format)
|
PreparedFfmpegFilter::Open(AudioFormat &in_audio_format)
|
||||||
{
|
{
|
||||||
Ffmpeg::FilterGraph graph;
|
Ffmpeg::FilterGraph graph;
|
||||||
|
|
||||||
auto buffer_src =
|
auto &buffer_src =
|
||||||
Ffmpeg::FilterContext::MakeAudioBufferSource(in_audio_format,
|
Ffmpeg::MakeAudioBufferSource(in_audio_format, *graph);
|
||||||
*graph);
|
|
||||||
|
|
||||||
auto buffer_sink = Ffmpeg::FilterContext::MakeAudioBufferSink(*graph);
|
auto &buffer_sink = Ffmpeg::MakeAudioBufferSink(*graph);
|
||||||
|
|
||||||
Ffmpeg::FilterInOut io_sink("out", *buffer_sink);
|
/* if the filter's output format is not supported by MPD, this
|
||||||
Ffmpeg::FilterInOut io_src("in", *buffer_src);
|
"aformat" filter is inserted at the end and takes care for
|
||||||
auto io = graph.Parse(graph_string, std::move(io_sink),
|
the required conversion */
|
||||||
std::move(io_src));
|
auto &aformat = Ffmpeg::MakeAutoAformat(*graph);
|
||||||
|
|
||||||
if (io.first.get() != nullptr)
|
int error = avfilter_link(&aformat, 0, &buffer_sink, 0);
|
||||||
throw std::runtime_error("FFmpeg filter has an open input");
|
if (error < 0)
|
||||||
|
throw MakeFfmpegError(error, "avfilter_link() failed");
|
||||||
if (io.second.get() != nullptr)
|
|
||||||
throw std::runtime_error("FFmpeg filter has an open output");
|
|
||||||
|
|
||||||
|
graph.ParseSingleInOut(graph_string, aformat, buffer_src);
|
||||||
graph.CheckAndConfigure();
|
graph.CheckAndConfigure();
|
||||||
|
|
||||||
const auto out_audio_format =
|
const auto out_audio_format =
|
||||||
Ffmpeg::DetectFilterOutputFormat(in_audio_format, *buffer_src,
|
Ffmpeg::DetectFilterOutputFormat(in_audio_format, buffer_src,
|
||||||
*buffer_sink);
|
buffer_sink);
|
||||||
|
|
||||||
|
if (!out_audio_format.IsDefined())
|
||||||
|
/* the filter's native output format could not be
|
||||||
|
determined yet, but we need to know it now; as a
|
||||||
|
workaround for this MPD API deficiency, try again
|
||||||
|
with an "aformat" filter which forces a specific
|
||||||
|
output format */
|
||||||
|
return OpenWithAformat(graph_string, in_audio_format);
|
||||||
|
|
||||||
return std::make_unique<FfmpegFilter>(in_audio_format,
|
return std::make_unique<FfmpegFilter>(in_audio_format,
|
||||||
out_audio_format,
|
out_audio_format,
|
||||||
std::move(graph),
|
std::move(graph),
|
||||||
std::move(buffer_src),
|
buffer_src,
|
||||||
std::move(buffer_sink));
|
buffer_sink);
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::unique_ptr<PreparedFilter>
|
static std::unique_ptr<PreparedFilter>
|
||||||
|
|
|
@ -42,24 +42,13 @@ OpenHdcdFilter(AudioFormat &in_audio_format)
|
||||||
{
|
{
|
||||||
Ffmpeg::FilterGraph graph;
|
Ffmpeg::FilterGraph graph;
|
||||||
|
|
||||||
auto buffer_src =
|
auto &buffer_src =
|
||||||
Ffmpeg::FilterContext::MakeAudioBufferSource(in_audio_format,
|
Ffmpeg::MakeAudioBufferSource(in_audio_format,
|
||||||
*graph);
|
*graph);
|
||||||
|
|
||||||
auto buffer_sink = Ffmpeg::FilterContext::MakeAudioBufferSink(*graph);
|
auto &buffer_sink = Ffmpeg::MakeAudioBufferSink(*graph);
|
||||||
|
|
||||||
Ffmpeg::FilterInOut io_sink("out", *buffer_sink);
|
|
||||||
Ffmpeg::FilterInOut io_src("in", *buffer_src);
|
|
||||||
|
|
||||||
auto io = graph.Parse(hdcd_graph, std::move(io_sink),
|
|
||||||
std::move(io_src));
|
|
||||||
|
|
||||||
if (io.first.get() != nullptr)
|
|
||||||
throw std::runtime_error("FFmpeg filter has an open input");
|
|
||||||
|
|
||||||
if (io.second.get() != nullptr)
|
|
||||||
throw std::runtime_error("FFmpeg filter has an open output");
|
|
||||||
|
|
||||||
|
graph.ParseSingleInOut(hdcd_graph, buffer_sink, buffer_src);
|
||||||
graph.CheckAndConfigure();
|
graph.CheckAndConfigure();
|
||||||
|
|
||||||
auto out_audio_format = in_audio_format;
|
auto out_audio_format = in_audio_format;
|
||||||
|
@ -69,8 +58,8 @@ OpenHdcdFilter(AudioFormat &in_audio_format)
|
||||||
return std::make_unique<FfmpegFilter>(in_audio_format,
|
return std::make_unique<FfmpegFilter>(in_audio_format,
|
||||||
out_audio_format,
|
out_audio_format,
|
||||||
std::move(graph),
|
std::move(graph),
|
||||||
std::move(buffer_src),
|
buffer_src,
|
||||||
std::move(buffer_sink));
|
buffer_sink);
|
||||||
}
|
}
|
||||||
|
|
||||||
class PreparedHdcdFilter final : public PreparedFilter {
|
class PreparedHdcdFilter final : public PreparedFilter {
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2003-2021 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_CHANNEL_LAYOUT_HXX
|
||||||
|
#define MPD_FFMPEG_CHANNEL_LAYOUT_HXX
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include <libavutil/channel_layout.h>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a MPD channel count to a libavutil channel_layout bit mask.
|
||||||
|
*/
|
||||||
|
static constexpr uint64_t
|
||||||
|
ToFfmpegChannelLayout(unsigned channels) noexcept
|
||||||
|
{
|
||||||
|
switch (channels) {
|
||||||
|
case 1:
|
||||||
|
return AV_CH_LAYOUT_MONO;
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
return AV_CH_LAYOUT_STEREO;
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
return AV_CH_LAYOUT_SURROUND;
|
||||||
|
|
||||||
|
case 4:
|
||||||
|
// TODO is this AV_CH_LAYOUT_2_2?
|
||||||
|
return AV_CH_LAYOUT_QUAD;
|
||||||
|
|
||||||
|
case 5:
|
||||||
|
// TODO is this AV_CH_LAYOUT_5POINT0_BACK?
|
||||||
|
return AV_CH_LAYOUT_5POINT0;
|
||||||
|
|
||||||
|
case 6:
|
||||||
|
return AV_CH_LAYOUT_5POINT1;
|
||||||
|
|
||||||
|
case 7:
|
||||||
|
return AV_CH_LAYOUT_6POINT1;
|
||||||
|
|
||||||
|
case 8:
|
||||||
|
return AV_CH_LAYOUT_7POINT1;
|
||||||
|
|
||||||
|
default:
|
||||||
|
/* unreachable */
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
|
@ -62,8 +62,14 @@ DetectFilterOutputFormat(const AudioFormat &in_audio_format,
|
||||||
frame.Unref();
|
frame.Unref();
|
||||||
|
|
||||||
err = av_buffersink_get_frame(&buffer_sink, frame.get());
|
err = av_buffersink_get_frame(&buffer_sink, frame.get());
|
||||||
if (err < 0)
|
if (err < 0) {
|
||||||
|
if (err == AVERROR(EAGAIN))
|
||||||
|
/* one sample was not enough input data for
|
||||||
|
the given filter graph */
|
||||||
|
return AudioFormat::Undefined();
|
||||||
|
|
||||||
throw MakeFfmpegError(err, "av_buffersink_get_frame() failed");
|
throw MakeFfmpegError(err, "av_buffersink_get_frame() failed");
|
||||||
|
}
|
||||||
|
|
||||||
const SampleFormat sample_format = FromFfmpegSampleFormat(AVSampleFormat(frame->format));
|
const SampleFormat sample_format = FromFfmpegSampleFormat(AVSampleFormat(frame->format));
|
||||||
if (sample_format == SampleFormat::UNDEFINED)
|
if (sample_format == SampleFormat::UNDEFINED)
|
||||||
|
|
|
@ -35,6 +35,9 @@ namespace Ffmpeg {
|
||||||
* between.
|
* between.
|
||||||
*
|
*
|
||||||
* This function can throw if the FFmpeg filter fails.
|
* This function can throw if the FFmpeg filter fails.
|
||||||
|
*
|
||||||
|
* @return the output format or AudioFormat::Undefined() if it was not
|
||||||
|
* possible to determine the format
|
||||||
*/
|
*/
|
||||||
AudioFormat
|
AudioFormat
|
||||||
DetectFilterOutputFormat(const AudioFormat &in_audio_format,
|
DetectFilterOutputFormat(const AudioFormat &in_audio_format,
|
||||||
|
|
|
@ -18,10 +18,13 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "Filter.hxx"
|
#include "Filter.hxx"
|
||||||
|
#include "ChannelLayout.hxx"
|
||||||
#include "SampleFormat.hxx"
|
#include "SampleFormat.hxx"
|
||||||
#include "pcm/AudioFormat.hxx"
|
#include "pcm/AudioFormat.hxx"
|
||||||
#include "util/RuntimeError.hxx"
|
#include "util/RuntimeError.hxx"
|
||||||
|
|
||||||
|
#include <cinttypes>
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
|
||||||
namespace Ffmpeg {
|
namespace Ffmpeg {
|
||||||
|
@ -36,9 +39,32 @@ RequireFilterByName(const char *name)
|
||||||
return *filter;
|
return *filter;
|
||||||
}
|
}
|
||||||
|
|
||||||
FilterContext
|
static AVFilterContext &
|
||||||
FilterContext::MakeAudioBufferSource(AudioFormat &audio_format,
|
CreateFilter(const AVFilter &filt,
|
||||||
AVFilterGraph &graph_ctx)
|
const char *name, const char *args, void *opaque,
|
||||||
|
AVFilterGraph &graph_ctx)
|
||||||
|
{
|
||||||
|
AVFilterContext *context = nullptr;
|
||||||
|
int err = avfilter_graph_create_filter(&context, &filt,
|
||||||
|
name, args, opaque,
|
||||||
|
&graph_ctx);
|
||||||
|
if (err < 0)
|
||||||
|
throw MakeFfmpegError(err, "avfilter_graph_create_filter() failed");
|
||||||
|
|
||||||
|
return *context;
|
||||||
|
}
|
||||||
|
|
||||||
|
static AVFilterContext &
|
||||||
|
CreateFilter(const AVFilter &filt,
|
||||||
|
const char *name,
|
||||||
|
AVFilterGraph &graph_ctx)
|
||||||
|
{
|
||||||
|
return CreateFilter(filt, name, nullptr, nullptr, graph_ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
AVFilterContext &
|
||||||
|
MakeAudioBufferSource(AudioFormat &audio_format,
|
||||||
|
AVFilterGraph &graph_ctx)
|
||||||
{
|
{
|
||||||
AVSampleFormat src_format = ToFfmpegSampleFormat(audio_format.format);
|
AVSampleFormat src_format = ToFfmpegSampleFormat(audio_format.format);
|
||||||
if (src_format == AV_SAMPLE_FMT_NONE) {
|
if (src_format == AV_SAMPLE_FMT_NONE) {
|
||||||
|
@ -57,19 +83,72 @@ FilterContext::MakeAudioBufferSource(AudioFormat &audio_format,
|
||||||
|
|
||||||
char abuffer_args[256];
|
char abuffer_args[256];
|
||||||
sprintf(abuffer_args,
|
sprintf(abuffer_args,
|
||||||
"sample_rate=%u:sample_fmt=%s:channels=%u:time_base=1/%u",
|
"sample_rate=%u:sample_fmt=%s:channel_layout=0x%" PRIx64 ":time_base=1/%u",
|
||||||
audio_format.sample_rate,
|
audio_format.sample_rate,
|
||||||
av_get_sample_fmt_name(src_format),
|
av_get_sample_fmt_name(src_format),
|
||||||
audio_format.channels,
|
ToFfmpegChannelLayout(audio_format.channels),
|
||||||
audio_format.sample_rate);
|
audio_format.sample_rate);
|
||||||
|
|
||||||
return {RequireFilterByName("abuffer"), "abuffer", abuffer_args, nullptr, graph_ctx};
|
return CreateFilter(RequireFilterByName("abuffer"), "abuffer",
|
||||||
|
abuffer_args, nullptr, graph_ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
FilterContext
|
AVFilterContext &
|
||||||
FilterContext::MakeAudioBufferSink(AVFilterGraph &graph_ctx)
|
MakeAudioBufferSink(AVFilterGraph &graph_ctx)
|
||||||
{
|
{
|
||||||
return {RequireFilterByName("abuffersink"), "abuffersink", graph_ctx};
|
return CreateFilter(RequireFilterByName("abuffersink"), "abuffersink",
|
||||||
|
graph_ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
AVFilterContext &
|
||||||
|
MakeAformat(AudioFormat &audio_format,
|
||||||
|
AVFilterGraph &graph_ctx)
|
||||||
|
{
|
||||||
|
AVSampleFormat dest_format = ToFfmpegSampleFormat(audio_format.format);
|
||||||
|
if (dest_format == AV_SAMPLE_FMT_NONE) {
|
||||||
|
switch (audio_format.format) {
|
||||||
|
case SampleFormat::S24_P32:
|
||||||
|
audio_format.format = SampleFormat::S32;
|
||||||
|
dest_format = AV_SAMPLE_FMT_S32;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
audio_format.format = SampleFormat::S16;
|
||||||
|
dest_format = AV_SAMPLE_FMT_S16;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
char args[256];
|
||||||
|
sprintf(args,
|
||||||
|
"sample_rates=%u:sample_fmts=%s:channel_layouts=0x%" PRIx64,
|
||||||
|
audio_format.sample_rate,
|
||||||
|
av_get_sample_fmt_name(dest_format),
|
||||||
|
ToFfmpegChannelLayout(audio_format.channels));
|
||||||
|
|
||||||
|
return CreateFilter(RequireFilterByName("aformat"), "aformat",
|
||||||
|
args, nullptr, graph_ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
AVFilterContext &
|
||||||
|
MakeAutoAformat(AVFilterGraph &graph_ctx)
|
||||||
|
{
|
||||||
|
return CreateFilter(RequireFilterByName("aformat"), "aformat",
|
||||||
|
"sample_fmts=flt|s32|s16",
|
||||||
|
nullptr, graph_ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
FilterGraph::ParseSingleInOut(const char *filters, AVFilterContext &in,
|
||||||
|
AVFilterContext &out)
|
||||||
|
{
|
||||||
|
auto [inputs, outputs] = Parse(filters, {"out", in}, {"in", out});
|
||||||
|
|
||||||
|
if (inputs.get() != nullptr)
|
||||||
|
throw std::runtime_error("FFmpeg filter has an open input");
|
||||||
|
|
||||||
|
if (outputs.get() != nullptr)
|
||||||
|
throw std::runtime_error("FFmpeg filter has an open output");
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Ffmpeg
|
} // namespace Ffmpeg
|
||||||
|
|
|
@ -77,63 +77,38 @@ public:
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
class FilterContext {
|
/**
|
||||||
AVFilterContext *context = nullptr;
|
* Create an "abuffer" filter.
|
||||||
|
*
|
||||||
|
* @param the input audio format; may be modified by the
|
||||||
|
* function to ask the caller to do format conversion
|
||||||
|
*/
|
||||||
|
AVFilterContext &
|
||||||
|
MakeAudioBufferSource(AudioFormat &audio_format,
|
||||||
|
AVFilterGraph &graph_ctx);
|
||||||
|
|
||||||
public:
|
/**
|
||||||
FilterContext() = default;
|
* Create an "abuffersink" filter.
|
||||||
|
*/
|
||||||
|
AVFilterContext &
|
||||||
|
MakeAudioBufferSink(AVFilterGraph &graph_ctx);
|
||||||
|
|
||||||
FilterContext(const AVFilter &filt,
|
/**
|
||||||
const char *name, const char *args, void *opaque,
|
* Create an "aformat" filter.
|
||||||
AVFilterGraph &graph_ctx) {
|
*
|
||||||
int err = avfilter_graph_create_filter(&context, &filt,
|
* @param the output audio format; may be modified by the function if
|
||||||
name, args, opaque,
|
* the given format is not supported by libavfilter
|
||||||
&graph_ctx);
|
*/
|
||||||
if (err < 0)
|
AVFilterContext &
|
||||||
throw MakeFfmpegError(err, "avfilter_graph_create_filter() failed");
|
MakeAformat(AudioFormat &audio_format,
|
||||||
}
|
AVFilterGraph &graph_ctx);
|
||||||
|
|
||||||
FilterContext(const AVFilter &filt,
|
/**
|
||||||
const char *name,
|
* Create an "aformat" filter which automatically converts the output
|
||||||
AVFilterGraph &graph_ctx)
|
* to a format supported by MPD.
|
||||||
:FilterContext(filt, name, nullptr, nullptr, graph_ctx) {}
|
*/
|
||||||
|
AVFilterContext &
|
||||||
FilterContext(FilterContext &&src) noexcept
|
MakeAutoAformat(AVFilterGraph &graph_ctx);
|
||||||
:context(std::exchange(src.context, nullptr)) {}
|
|
||||||
|
|
||||||
~FilterContext() noexcept {
|
|
||||||
if (context != nullptr)
|
|
||||||
avfilter_free(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
FilterContext &operator=(FilterContext &&src) noexcept {
|
|
||||||
using std::swap;
|
|
||||||
swap(context, src.context);
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an "abuffer" filter.
|
|
||||||
*
|
|
||||||
* @param the input audio format; may be modified by the
|
|
||||||
* function to ask the caller to do format conversion
|
|
||||||
*/
|
|
||||||
static FilterContext MakeAudioBufferSource(AudioFormat &audio_format,
|
|
||||||
AVFilterGraph &graph_ctx);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an "abuffersink" filter.
|
|
||||||
*/
|
|
||||||
static FilterContext MakeAudioBufferSink(AVFilterGraph &graph_ctx);
|
|
||||||
|
|
||||||
auto &operator*() noexcept {
|
|
||||||
return *context;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto *get() noexcept {
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
class FilterGraph {
|
class FilterGraph {
|
||||||
AVFilterGraph *graph = nullptr;
|
AVFilterGraph *graph = nullptr;
|
||||||
|
@ -180,6 +155,9 @@ public:
|
||||||
return std::make_pair(std::move(inputs), std::move(outputs));
|
return std::make_pair(std::move(inputs), std::move(outputs));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ParseSingleInOut(const char *filters, AVFilterContext &in,
|
||||||
|
AVFilterContext &out);
|
||||||
|
|
||||||
std::pair<FilterInOut, FilterInOut> Parse(const char *filters) {
|
std::pair<FilterInOut, FilterInOut> Parse(const char *filters) {
|
||||||
AVFilterInOut *inputs, *outputs;
|
AVFilterInOut *inputs, *outputs;
|
||||||
int err = avfilter_graph_parse2(graph, filters,
|
int err = avfilter_graph_parse2(graph, filters,
|
||||||
|
|
Loading…
Reference in New Issue