Commit bd893e63 authored by Max Kellermann's avatar Max Kellermann

Merge tag 'v0.22.10'

release v0.22.10
parents a74b0772 64c39af5
...@@ -17,18 +17,23 @@ ver 0.23 (not yet released) ...@@ -17,18 +17,23 @@ 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.10 (not yet released) 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
* database * database
- simple: fix crash bug - simple: fix crash bug
- simple: fix absolute paths in CUE "as_directory" entries
- simple: prune CUE entries from database for non-existent songs
* input * input
- curl: fix crash bug after stream with Icy metadata was closed by peer - curl: fix crash bug after stream with Icy metadata was closed by peer
- tidal: remove defunct unmaintained plugin - tidal: remove defunct unmaintained plugin
* tags * tags
- fix crash caused by bug in TagBuilder and a few potential reference leaks - fix crash caused by bug in TagBuilder and a few potential reference leaks
* output * output
- httpd: fix missing tag after seeking into a new song
- oss: fix channel order of multi-channel files - oss: fix channel order of multi-channel files
* mixer
- alsa: fix yet more rounding errors
ver 0.22.9 (2021/06/23) ver 0.22.9 (2021/06/23)
* database * database
......
...@@ -91,8 +91,9 @@ class AndroidNdkToolchain: ...@@ -91,8 +91,9 @@ class AndroidNdkToolchain:
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', 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_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
......
...@@ -1257,7 +1257,7 @@ This plugin requires building with ``libavfilter`` (FFmpeg). ...@@ -1257,7 +1257,7 @@ This plugin requires building with ``libavfilter`` (FFmpeg).
* - **graph "..."** * - **graph "..."**
- Specifies the ``libavfilter`` graph; read the `FFmpeg - Specifies the ``libavfilter`` graph; read the `FFmpeg
documentation documentation
<https://libav.org/documentation/libavfilter.html#Filtergraph-syntax-1>`_ <https://ffmpeg.org/ffmpeg-filters.html#Filtergraph-syntax-1>`_
for details for details
......
...@@ -388,14 +388,14 @@ ffmpeg = FfmpegProject( ...@@ -388,14 +388,14 @@ ffmpeg = FfmpegProject(
) )
openssl = OpenSSLProject( openssl = OpenSSLProject(
'https://www.openssl.org/source/openssl-3.0.0-alpha16.tar.gz', 'https://www.openssl.org/source/openssl-3.0.0-beta2.tar.gz',
'08ce8244b59d75f40f91170dfcb012bf25309cdcb1fef9502e39d694f883d1d1', 'e76ab22879201b12f014393ee4becec7f264d8f6955b1036839128002868df71',
'include/openssl/ossl_typ.h', 'include/openssl/ossl_typ.h',
) )
curl = AutotoolsProject( curl = AutotoolsProject(
'https://curl.se/download/curl-7.76.1.tar.xz', 'https://curl.se/download/curl-7.78.0.tar.xz',
'64bb5288c39f0840c07d077e30d9052e1cbb9fa6c2dc52523824cc859e679145', 'be42766d5664a739c3974ee3dfbbcbe978a4ccb1fe628bb1d9b59ac79e445fb5',
'lib/libcurl.a', 'lib/libcurl.a',
[ [
'--disable-shared', '--enable-static', '--disable-shared', '--enable-static',
......
...@@ -17,6 +17,12 @@ class OpenSSLProject(MakeProject): ...@@ -17,6 +17,12 @@ class OpenSSLProject(MakeProject):
'build_libs', '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): def build(self, toolchain):
src = self.unpack(toolchain, out_of_tree=False) src = self.unpack(toolchain, out_of_tree=False)
...@@ -42,6 +48,7 @@ class OpenSSLProject(MakeProject): ...@@ -42,6 +48,7 @@ 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',
...@@ -50,6 +57,7 @@ class OpenSSLProject(MakeProject): ...@@ -50,6 +57,7 @@ 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)
...@@ -20,7 +20,7 @@ class Project: ...@@ -20,7 +20,7 @@ class Project:
self.base = base self.base = base
if name is None or version is None: 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 name is None: name = m.group(1)
if version is None: version = m.group(2) if version is None: version = m.group(2)
......
...@@ -109,6 +109,23 @@ Directory::FindChild(std::string_view name) const noexcept ...@@ -109,6 +109,23 @@ Directory::FindChild(std::string_view name) const noexcept
return nullptr; 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<Directory *>(this)->LookupDirectory(target);
return lr.directory->FindSong(lr.rest) != nullptr;
}
void void
Directory::PruneEmpty() noexcept Directory::PruneEmpty() noexcept
{ {
......
...@@ -118,13 +118,17 @@ public: ...@@ -118,13 +118,17 @@ public:
return new Directory(std::string(), nullptr); 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 * Is this really a regular file which is being treated like a
* directory? * directory?
*/ */
bool IsReallyAFile() const noexcept { bool IsReallyAFile() const noexcept {
return device == DEVICE_INARCHIVE || return device == DEVICE_INARCHIVE ||
device == DEVICE_PLAYLIST || IsPlaylist() ||
device == DEVICE_CONTAINER; device == DEVICE_CONTAINER;
} }
...@@ -206,11 +210,13 @@ public: ...@@ -206,11 +210,13 @@ public:
* Looks up a directory by its relative URI. * Looks up a directory by its relative URI.
* *
* @param uri the relative URI * @param uri the relative URI
* @return the Directory, or nullptr if none was found
*/ */
gcc_pure gcc_pure
LookupResult LookupDirectory(std::string_view uri) noexcept; LookupResult LookupDirectory(std::string_view uri) noexcept;
[[gnu::pure]]
bool TargetExists(std::string_view target) const noexcept;
gcc_pure gcc_pure
bool IsEmpty() const noexcept { bool IsEmpty() const noexcept {
return children.empty() && return children.empty() &&
......
...@@ -31,6 +31,7 @@ ...@@ -31,6 +31,7 @@
#include "playlist/SongEnumerator.hxx" #include "playlist/SongEnumerator.hxx"
#include "storage/FileInfo.hxx" #include "storage/FileInfo.hxx"
#include "storage/StorageInterface.hxx" #include "storage/StorageInterface.hxx"
#include "fs/Traits.hxx"
#include "util/StringFormat.hxx" #include "util/StringFormat.hxx"
#include "Log.hxx" #include "Log.hxx"
...@@ -71,7 +72,14 @@ UpdateWalk::UpdatePlaylistFile(Directory &parent, std::string_view name, ...@@ -71,7 +72,14 @@ UpdateWalk::UpdatePlaylistFile(Directory &parent, std::string_view name,
auto db_song = std::make_unique<Song>(std::move(*song), auto db_song = std::make_unique<Song>(std::move(*song),
*directory); *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", db_song->filename = StringFormat<64>("track%04u",
++track); ++track);
......
...@@ -133,6 +133,28 @@ UpdateWalk::PurgeDeletedFromDirectory(Directory &directory) noexcept ...@@ -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 #ifndef _WIN32
static bool static bool
update_directory_stat(Storage &storage, Directory &directory) noexcept update_directory_stat(Storage &storage, Directory &directory) noexcept
...@@ -530,5 +552,10 @@ UpdateWalk::Walk(Directory &root, const char *path, bool discard) noexcept ...@@ -530,5 +552,10 @@ UpdateWalk::Walk(Directory &root, const char *path, bool discard) noexcept
UpdateDirectory(root, exclude_list, info); UpdateDirectory(root, exclude_list, info);
} }
{
const ScopeDatabaseLock protect;
PurgeDanglingFromPlaylists(root);
}
return modified; return modified;
} }
...@@ -85,6 +85,12 @@ private: ...@@ -85,6 +85,12 @@ private:
void PurgeDeletedFromDirectory(Directory &directory) noexcept; 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, void UpdateSongFile2(Directory &directory,
const char *name, std::string_view suffix, const char *name, std::string_view suffix,
const StorageFileInfo &info) noexcept; const StorageFileInfo &info) noexcept;
......
...@@ -582,10 +582,6 @@ DecoderBridge::SubmitTag(InputStream *is, Tag &&tag) noexcept ...@@ -582,10 +582,6 @@ DecoderBridge::SubmitTag(InputStream *is, Tag &&tag) noexcept
decoder_tag = std::make_unique<Tag>(std::move(tag)); decoder_tag = std::make_unique<Tag>(std::move(tag));
/* check for a new stream tag */
UpdateStreamTag(is);
/* check if we're seeking */ /* check if we're seeking */
if (PrepareInitialSeek()) if (PrepareInitialSeek())
...@@ -594,6 +590,10 @@ DecoderBridge::SubmitTag(InputStream *is, Tag &&tag) noexcept ...@@ -594,6 +590,10 @@ DecoderBridge::SubmitTag(InputStream *is, Tag &&tag) noexcept
function here */ function here */
return DecoderCommand::SEEK; return DecoderCommand::SEEK;
/* check for a new stream tag */
UpdateStreamTag(is);
/* send tag to music pipe */ /* send tag to music pipe */
if (stream_tag != nullptr) if (stream_tag != nullptr)
......
...@@ -19,6 +19,7 @@ ...@@ -19,6 +19,7 @@
#include "Traits.hxx" #include "Traits.hxx"
#include "util/StringCompare.hxx" #include "util/StringCompare.hxx"
#include "util/UriExtract.hxx"
#include <string.h> #include <string.h>
...@@ -220,6 +221,12 @@ PathTraitsUTF8::Build(string_view a, string_view b) noexcept ...@@ -220,6 +221,12 @@ PathTraitsUTF8::Build(string_view a, string_view b) noexcept
return BuildPathImpl<PathTraitsUTF8>(a, b); return BuildPathImpl<PathTraitsUTF8>(a, b);
} }
bool
PathTraitsUTF8::IsAbsoluteOrHasScheme(const_pointer p) noexcept
{
return IsAbsolute(p) || uri_has_scheme(p);
}
PathTraitsUTF8::const_pointer PathTraitsUTF8::const_pointer
PathTraitsUTF8::GetBase(const_pointer p) noexcept PathTraitsUTF8::GetBase(const_pointer p) noexcept
{ {
......
...@@ -274,6 +274,13 @@ struct PathTraitsUTF8 { ...@@ -274,6 +274,13 @@ struct PathTraitsUTF8 {
return IsSeparator(*p); 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 gcc_pure gcc_nonnull_all
static bool IsSpecialFilename(const_pointer name) noexcept { static bool IsSpecialFilename(const_pointer name) noexcept {
return (name[0] == '.' && name[1] == 0) || return (name[0] == '.' && name[1] == 0) ||
......
...@@ -83,6 +83,24 @@ class AlsaMixer final : public Mixer { ...@@ -83,6 +83,24 @@ class AlsaMixer final : public Mixer {
AlsaMixerMonitor *monitor; 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: public:
AlsaMixer(EventLoop &_event_loop, MixerListener &_listener) AlsaMixer(EventLoop &_event_loop, MixerListener &_listener)
:Mixer(alsa_mixer_plugin, _listener), :Mixer(alsa_mixer_plugin, _listener),
...@@ -101,6 +119,27 @@ public: ...@@ -101,6 +119,27 @@ public:
void Close() noexcept override; void Close() noexcept override;
int GetVolume() override; int GetVolume() override;
void SetVolume(unsigned volume) 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"); static constexpr Domain alsa_mixer_domain("alsa_mixer");
...@@ -144,18 +183,26 @@ AlsaMixerMonitor::DispatchSockets() noexcept ...@@ -144,18 +183,26 @@ AlsaMixerMonitor::DispatchSockets() noexcept
* *
*/ */
static int int
alsa_mixer_elem_callback(snd_mixer_elem_t *elem, unsigned mask) AlsaMixer::ElemCallback(snd_mixer_elem_t *elem, unsigned mask) noexcept
{ {
AlsaMixer &mixer = *(AlsaMixer *) AlsaMixer &mixer = *(AlsaMixer *)
snd_mixer_elem_get_callback_private(elem); snd_mixer_elem_get_callback_private(elem);
if (mask & SND_CTL_EVENT_MASK_VALUE) { if (mask & SND_CTL_EVENT_MASK_VALUE) {
try { int volume = mixer.GetPercentVolume();
int volume = mixer.GetVolume();
mixer.listener.OnMixerVolumeChanged(mixer, volume); if (mixer.resulting_volume >= 0 &&
} catch (...) { 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; return 0;
...@@ -233,7 +280,7 @@ AlsaMixer::Setup() ...@@ -233,7 +280,7 @@ AlsaMixer::Setup()
throw FormatRuntimeError("no such mixer control: %s", control); throw FormatRuntimeError("no such mixer control: %s", control);
snd_mixer_elem_set_callback_private(elem, this); 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); monitor = new AlsaMixerMonitor(event_loop, handle);
} }
...@@ -241,6 +288,8 @@ AlsaMixer::Setup() ...@@ -241,6 +288,8 @@ AlsaMixer::Setup()
void void
AlsaMixer::Open() AlsaMixer::Open()
{ {
desired_volume = resulting_volume = -1;
int err; int err;
err = snd_mixer_open(&handle, 0); err = snd_mixer_open(&handle, 0);
...@@ -279,7 +328,12 @@ AlsaMixer::GetVolume() ...@@ -279,7 +328,12 @@ AlsaMixer::GetVolume()
throw FormatRuntimeError("snd_mixer_handle_events() failed: %s", throw FormatRuntimeError("snd_mixer_handle_events() failed: %s",
snd_strerror(err)); 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 void
...@@ -287,12 +341,13 @@ AlsaMixer::SetVolume(unsigned volume) ...@@ -287,12 +341,13 @@ AlsaMixer::SetVolume(unsigned volume)
{ {
assert(handle != nullptr); assert(handle != nullptr);
double cur = get_normalized_playback_volume(elem, SND_MIXER_SCHN_FRONT_LEFT); int err = set_normalized_playback_volume(elem, 0.01*volume, 1);
int delta = volume - lround(100.*cur);
int err = set_normalized_playback_volume(elem, cur + 0.01*delta, delta);
if (err < 0) if (err < 0)
throw FormatRuntimeError("failed to set ALSA volume: %s", throw FormatRuntimeError("failed to set ALSA volume: %s",
snd_strerror(err)); snd_strerror(err));
desired_volume = volume;
resulting_volume = GetPercentVolume();
} }
const MixerPlugin alsa_mixer_plugin = { const MixerPlugin alsa_mixer_plugin = {
......
...@@ -95,8 +95,8 @@ playlist_check_translate_song(DetachedSong &song, std::string_view base_uri, ...@@ -95,8 +95,8 @@ playlist_check_translate_song(DetachedSong &song, std::string_view base_uri,
} }
#endif #endif
if (base_uri.data() != nullptr && !uri_has_scheme(uri) && if (base_uri.data() != nullptr &&
!PathTraitsUTF8::IsAbsolute(uri)) !PathTraitsUTF8::IsAbsoluteOrHasScheme(uri))
song.SetURI(PathTraitsUTF8::Build(base_uri, uri)); song.SetURI(PathTraitsUTF8::Build(base_uri, uri));
return playlist_check_load_song(song, loader); return playlist_check_load_song(song, loader);
......
...@@ -61,7 +61,7 @@ DetachedSong::IsInDatabase() const noexcept ...@@ -61,7 +61,7 @@ DetachedSong::IsInDatabase() const noexcept
GetRealURI() is never relative */ GetRealURI() is never relative */
const char *_uri = GetURI(); const char *_uri = GetURI();
return !uri_has_scheme(_uri) && !PathTraitsUTF8::IsAbsolute(_uri); return !PathTraitsUTF8::IsAbsoluteOrHasScheme(_uri);
} }
SignedSongTime SignedSongTime
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment