diff --git a/NEWS b/NEWS index 0bff87f25..2ee609f6f 100644 --- a/NEWS +++ b/NEWS @@ -17,18 +17,23 @@ ver 0.23 (not yet released) - new tags "ComposerSort", "Ensemble", "Movement", "MovementNumber", and "Location" * new build-time dependency: libfmt -ver 0.22.10 (not yet released) +ver 0.22.10 (2021/08/06) * protocol - support "albumart" for virtual tracks in CUE sheets * database - simple: fix crash bug + - simple: fix absolute paths in CUE "as_directory" entries + - simple: prune CUE entries from database for non-existent songs * input - curl: fix crash bug after stream with Icy metadata was closed by peer - tidal: remove defunct unmaintained plugin * tags - fix crash caused by bug in TagBuilder and a few potential reference leaks * output + - httpd: fix missing tag after seeking into a new song - oss: fix channel order of multi-channel files +* mixer + - alsa: fix yet more rounding errors ver 0.22.9 (2021/06/23) * database diff --git a/android/build.py b/android/build.py index eb6600111..664bcccdd 100755 --- a/android/build.py +++ b/android/build.py @@ -91,8 +91,9 @@ class AndroidNdkToolchain: self.arch = arch self.install_prefix = install_prefix + self.toolchain_arch = abi_info['toolchain_arch'] - toolchain_path = os.path.join(ndk_path, 'toolchains', abi_info['toolchain_arch'] + '-' + gcc_version, 'prebuilt', build_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_triple = abi_info['llvm_triple'] + android_api_level diff --git a/doc/plugins.rst b/doc/plugins.rst index 8e9c4582f..8573ae8a8 100644 --- a/doc/plugins.rst +++ b/doc/plugins.rst @@ -1257,7 +1257,7 @@ This plugin requires building with ``libavfilter`` (FFmpeg). * - **graph "..."** - Specifies the ``libavfilter`` graph; read the `FFmpeg documentation - `_ + `_ for details diff --git a/python/build/libs.py b/python/build/libs.py index 419a52a66..fcc349193 100644 --- a/python/build/libs.py +++ b/python/build/libs.py @@ -388,14 +388,14 @@ ffmpeg = FfmpegProject( ) openssl = OpenSSLProject( - 'https://www.openssl.org/source/openssl-3.0.0-alpha16.tar.gz', - '08ce8244b59d75f40f91170dfcb012bf25309cdcb1fef9502e39d694f883d1d1', + 'https://www.openssl.org/source/openssl-3.0.0-beta2.tar.gz', + 'e76ab22879201b12f014393ee4becec7f264d8f6955b1036839128002868df71', 'include/openssl/ossl_typ.h', ) curl = AutotoolsProject( - 'https://curl.se/download/curl-7.76.1.tar.xz', - '64bb5288c39f0840c07d077e30d9052e1cbb9fa6c2dc52523824cc859e679145', + 'https://curl.se/download/curl-7.78.0.tar.xz', + 'be42766d5664a739c3974ee3dfbbcbe978a4ccb1fe628bb1d9b59ac79e445fb5', 'lib/libcurl.a', [ '--disable-shared', '--enable-static', diff --git a/python/build/openssl.py b/python/build/openssl.py index a7350e6ac..605a04c74 100644 --- a/python/build/openssl.py +++ b/python/build/openssl.py @@ -17,6 +17,12 @@ class OpenSSLProject(MakeProject): 'build_libs', ] + def get_make_install_args(self, toolchain): + # OpenSSL's Makefile runs "ranlib" during installation + return MakeProject.get_make_install_args(self, toolchain) + [ + 'RANLIB=' + toolchain.ranlib, + ] + def build(self, toolchain): src = self.unpack(toolchain, out_of_tree=False) @@ -42,6 +48,7 @@ class OpenSSLProject(MakeProject): } openssl_arch = openssl_archs[toolchain.arch] + cross_compile_prefix = toolchain.toolchain_arch + '-' subprocess.check_call(['./Configure', 'no-shared', @@ -50,6 +57,7 @@ class OpenSSLProject(MakeProject): 'no-tests', 'no-asm', # "asm" causes build failures on Windows openssl_arch, + '--cross-compile-prefix=' + cross_compile_prefix, '--prefix=' + toolchain.install_prefix], cwd=src, env=toolchain.env) MakeProject.build(self, toolchain, src) diff --git a/python/build/project.py b/python/build/project.py index 374ccdb14..e0868b27b 100644 --- a/python/build/project.py +++ b/python/build/project.py @@ -20,7 +20,7 @@ class Project: self.base = base if name is None or version is None: - m = re.match(r'^([-\w]+)-(\d[\d.]*[a-z]?[\d.]*(?:-alpha\d+)?)(\+.*)?$', self.base) + m = re.match(r'^([-\w]+)-(\d[\d.]*[a-z]?[\d.]*(?:-(?:alpha|beta)\d+)?)$', self.base) if name is None: name = m.group(1) if version is None: version = m.group(2) diff --git a/src/db/plugins/simple/Directory.cxx b/src/db/plugins/simple/Directory.cxx index 4c7bb2873..b03994730 100644 --- a/src/db/plugins/simple/Directory.cxx +++ b/src/db/plugins/simple/Directory.cxx @@ -109,6 +109,23 @@ Directory::FindChild(std::string_view name) const noexcept return nullptr; } +bool +Directory::TargetExists(std::string_view _target) const noexcept +{ + StringView target{_target}; + + if (target.SkipPrefix("../")) { + if (parent == nullptr) + return false; + + return parent->TargetExists(target); + } + + /* sorry for the const_cast ... */ + const auto lr = const_cast(this)->LookupDirectory(target); + return lr.directory->FindSong(lr.rest) != nullptr; +} + void Directory::PruneEmpty() noexcept { diff --git a/src/db/plugins/simple/Directory.hxx b/src/db/plugins/simple/Directory.hxx index 670860de6..87e8351e7 100644 --- a/src/db/plugins/simple/Directory.hxx +++ b/src/db/plugins/simple/Directory.hxx @@ -118,13 +118,17 @@ public: return new Directory(std::string(), nullptr); } + bool IsPlaylist() const noexcept { + return device == DEVICE_PLAYLIST; + } + /** * Is this really a regular file which is being treated like a * directory? */ bool IsReallyAFile() const noexcept { return device == DEVICE_INARCHIVE || - device == DEVICE_PLAYLIST || + IsPlaylist() || device == DEVICE_CONTAINER; } @@ -206,11 +210,13 @@ public: * Looks up a directory by its relative URI. * * @param uri the relative URI - * @return the Directory, or nullptr if none was found */ gcc_pure LookupResult LookupDirectory(std::string_view uri) noexcept; + [[gnu::pure]] + bool TargetExists(std::string_view target) const noexcept; + gcc_pure bool IsEmpty() const noexcept { return children.empty() && diff --git a/src/db/update/Playlist.cxx b/src/db/update/Playlist.cxx index 855b055f1..db4eddb73 100644 --- a/src/db/update/Playlist.cxx +++ b/src/db/update/Playlist.cxx @@ -31,6 +31,7 @@ #include "playlist/SongEnumerator.hxx" #include "storage/FileInfo.hxx" #include "storage/StorageInterface.hxx" +#include "fs/Traits.hxx" #include "util/StringFormat.hxx" #include "Log.hxx" @@ -71,7 +72,14 @@ UpdateWalk::UpdatePlaylistFile(Directory &parent, std::string_view name, auto db_song = std::make_unique(std::move(*song), *directory); - db_song->target = "../" + db_song->filename; + db_song->target = + PathTraitsUTF8::IsAbsoluteOrHasScheme(db_song->filename.c_str()) + ? db_song->filename + /* prepend "../" to relative paths to + go from the virtual directory + (DEVICE_PLAYLIST) to the containing + directory */ + : "../" + db_song->filename; db_song->filename = StringFormat<64>("track%04u", ++track); diff --git a/src/db/update/Walk.cxx b/src/db/update/Walk.cxx index 74583b90d..168051d56 100644 --- a/src/db/update/Walk.cxx +++ b/src/db/update/Walk.cxx @@ -133,6 +133,28 @@ UpdateWalk::PurgeDeletedFromDirectory(Directory &directory) noexcept } } +void +UpdateWalk::PurgeDanglingFromPlaylists(Directory &directory) noexcept +{ + /* recurse */ + for (Directory &child : directory.children) + PurgeDanglingFromPlaylists(child); + + if (!directory.IsPlaylist()) + /* this check is only for virtual directories + representing a playlist file */ + return; + + directory.ForEachSongSafe([&](Song &song){ + if (!song.target.empty() && + !PathTraitsUTF8::IsAbsoluteOrHasScheme(song.target.c_str()) && + !directory.TargetExists(song.target)) { + editor.DeleteSong(directory, &song); + modified = true; + } + }); +} + #ifndef _WIN32 static bool update_directory_stat(Storage &storage, Directory &directory) noexcept @@ -530,5 +552,10 @@ UpdateWalk::Walk(Directory &root, const char *path, bool discard) noexcept UpdateDirectory(root, exclude_list, info); } + { + const ScopeDatabaseLock protect; + PurgeDanglingFromPlaylists(root); + } + return modified; } diff --git a/src/db/update/Walk.hxx b/src/db/update/Walk.hxx index e1ac9b8ec..b2417fab4 100644 --- a/src/db/update/Walk.hxx +++ b/src/db/update/Walk.hxx @@ -85,6 +85,12 @@ private: void PurgeDeletedFromDirectory(Directory &directory) noexcept; + /** + * Remove all virtual songs inside playlists whose "target" + * field points to a non-existing song file. + */ + void PurgeDanglingFromPlaylists(Directory &directory) noexcept; + void UpdateSongFile2(Directory &directory, const char *name, std::string_view suffix, const StorageFileInfo &info) noexcept; diff --git a/src/decoder/Bridge.cxx b/src/decoder/Bridge.cxx index 5f326a7f1..81b8b8f8e 100644 --- a/src/decoder/Bridge.cxx +++ b/src/decoder/Bridge.cxx @@ -582,10 +582,6 @@ DecoderBridge::SubmitTag(InputStream *is, Tag &&tag) noexcept decoder_tag = std::make_unique(std::move(tag)); - /* check for a new stream tag */ - - UpdateStreamTag(is); - /* check if we're seeking */ if (PrepareInitialSeek()) @@ -594,6 +590,10 @@ DecoderBridge::SubmitTag(InputStream *is, Tag &&tag) noexcept function here */ return DecoderCommand::SEEK; + /* check for a new stream tag */ + + UpdateStreamTag(is); + /* send tag to music pipe */ if (stream_tag != nullptr) diff --git a/src/fs/Traits.cxx b/src/fs/Traits.cxx index 35d7fa1fd..32f65105a 100644 --- a/src/fs/Traits.cxx +++ b/src/fs/Traits.cxx @@ -19,6 +19,7 @@ #include "Traits.hxx" #include "util/StringCompare.hxx" +#include "util/UriExtract.hxx" #include @@ -220,6 +221,12 @@ PathTraitsUTF8::Build(string_view a, string_view b) noexcept return BuildPathImpl(a, b); } +bool +PathTraitsUTF8::IsAbsoluteOrHasScheme(const_pointer p) noexcept +{ + return IsAbsolute(p) || uri_has_scheme(p); +} + PathTraitsUTF8::const_pointer PathTraitsUTF8::GetBase(const_pointer p) noexcept { diff --git a/src/fs/Traits.hxx b/src/fs/Traits.hxx index a5495a29f..f88cf33ac 100644 --- a/src/fs/Traits.hxx +++ b/src/fs/Traits.hxx @@ -274,6 +274,13 @@ struct PathTraitsUTF8 { return IsSeparator(*p); } + /** + * Is this any kind of absolute URI? (Unlike IsAbsolute(), + * this includes URIs/URLs with a scheme) + */ + [[gnu::pure]] [[gnu::nonnull]] + static bool IsAbsoluteOrHasScheme(const_pointer p) noexcept; + gcc_pure gcc_nonnull_all static bool IsSpecialFilename(const_pointer name) noexcept { return (name[0] == '.' && name[1] == 0) || diff --git a/src/mixer/plugins/AlsaMixerPlugin.cxx b/src/mixer/plugins/AlsaMixerPlugin.cxx index 30e158be2..5649f765e 100644 --- a/src/mixer/plugins/AlsaMixerPlugin.cxx +++ b/src/mixer/plugins/AlsaMixerPlugin.cxx @@ -83,6 +83,24 @@ class AlsaMixer final : public Mixer { AlsaMixerMonitor *monitor; + /** + * These fields are our workaround for rounding errors when + * the resolution of a mixer knob isn't fine enough to + * represent all 101 possible values (0..100). + * + * "desired_volume" is the percent value passed to + * SetVolume(), and "resulting_volume" is the volume which was + * actually set, and would be returned by the next + * GetPercentVolume() call. + * + * When GetVolume() is called, we compare the + * "resulting_volume" with the value returned by + * GetPercentVolume(), and if it's the same, we're still on + * the same value that was previously set (but may have been + * rounded down or up). + */ + int desired_volume, resulting_volume; + public: AlsaMixer(EventLoop &_event_loop, MixerListener &_listener) :Mixer(alsa_mixer_plugin, _listener), @@ -101,6 +119,27 @@ public: void Close() noexcept override; int GetVolume() override; void SetVolume(unsigned volume) override; + +private: + [[gnu::const]] + static unsigned NormalizedToPercent(double normalized) noexcept { + return lround(100 * normalized); + } + + [[gnu::pure]] + double GetNormalizedVolume() const noexcept { + return get_normalized_playback_volume(elem, + SND_MIXER_SCHN_FRONT_LEFT); + } + + [[gnu::pure]] + unsigned GetPercentVolume() const noexcept { + return NormalizedToPercent(GetNormalizedVolume()); + } + + static int ElemCallback(snd_mixer_elem_t *elem, + unsigned mask) noexcept; + }; static constexpr Domain alsa_mixer_domain("alsa_mixer"); @@ -144,18 +183,26 @@ AlsaMixerMonitor::DispatchSockets() noexcept * */ -static int -alsa_mixer_elem_callback(snd_mixer_elem_t *elem, unsigned mask) +int +AlsaMixer::ElemCallback(snd_mixer_elem_t *elem, unsigned mask) noexcept { AlsaMixer &mixer = *(AlsaMixer *) snd_mixer_elem_get_callback_private(elem); if (mask & SND_CTL_EVENT_MASK_VALUE) { - try { - int volume = mixer.GetVolume(); - mixer.listener.OnMixerVolumeChanged(mixer, volume); - } catch (...) { - } + int volume = mixer.GetPercentVolume(); + + if (mixer.resulting_volume >= 0 && + volume == mixer.resulting_volume) + /* still the same volume (this might be a + callback caused by SetVolume()) - switch to + desired_volume */ + volume = mixer.desired_volume; + else + /* flush */ + mixer.desired_volume = mixer.resulting_volume = -1; + + mixer.listener.OnMixerVolumeChanged(mixer, volume); } return 0; @@ -233,7 +280,7 @@ AlsaMixer::Setup() throw FormatRuntimeError("no such mixer control: %s", control); snd_mixer_elem_set_callback_private(elem, this); - snd_mixer_elem_set_callback(elem, alsa_mixer_elem_callback); + snd_mixer_elem_set_callback(elem, ElemCallback); monitor = new AlsaMixerMonitor(event_loop, handle); } @@ -241,6 +288,8 @@ AlsaMixer::Setup() void AlsaMixer::Open() { + desired_volume = resulting_volume = -1; + int err; err = snd_mixer_open(&handle, 0); @@ -279,7 +328,12 @@ AlsaMixer::GetVolume() throw FormatRuntimeError("snd_mixer_handle_events() failed: %s", snd_strerror(err)); - return lround(100 * get_normalized_playback_volume(elem, SND_MIXER_SCHN_FRONT_LEFT)); + int volume = GetPercentVolume(); + if (resulting_volume >= 0 && volume == resulting_volume) + /* we're still on the value passed to SetVolume() */ + volume = desired_volume; + + return volume; } void @@ -287,12 +341,13 @@ AlsaMixer::SetVolume(unsigned volume) { assert(handle != nullptr); - double cur = get_normalized_playback_volume(elem, SND_MIXER_SCHN_FRONT_LEFT); - int delta = volume - lround(100.*cur); - int err = set_normalized_playback_volume(elem, cur + 0.01*delta, delta); + int err = set_normalized_playback_volume(elem, 0.01*volume, 1); if (err < 0) throw FormatRuntimeError("failed to set ALSA volume: %s", snd_strerror(err)); + + desired_volume = volume; + resulting_volume = GetPercentVolume(); } const MixerPlugin alsa_mixer_plugin = { diff --git a/src/playlist/PlaylistSong.cxx b/src/playlist/PlaylistSong.cxx index 3cecbaeab..349282456 100644 --- a/src/playlist/PlaylistSong.cxx +++ b/src/playlist/PlaylistSong.cxx @@ -95,8 +95,8 @@ playlist_check_translate_song(DetachedSong &song, std::string_view base_uri, } #endif - if (base_uri.data() != nullptr && !uri_has_scheme(uri) && - !PathTraitsUTF8::IsAbsolute(uri)) + if (base_uri.data() != nullptr && + !PathTraitsUTF8::IsAbsoluteOrHasScheme(uri)) song.SetURI(PathTraitsUTF8::Build(base_uri, uri)); return playlist_check_load_song(song, loader); diff --git a/src/song/DetachedSong.cxx b/src/song/DetachedSong.cxx index e79595d75..0dc151129 100644 --- a/src/song/DetachedSong.cxx +++ b/src/song/DetachedSong.cxx @@ -61,7 +61,7 @@ DetachedSong::IsInDatabase() const noexcept GetRealURI() is never relative */ const char *_uri = GetURI(); - return !uri_has_scheme(_uri) && !PathTraitsUTF8::IsAbsolute(_uri); + return !PathTraitsUTF8::IsAbsoluteOrHasScheme(_uri); } SignedSongTime