diff --git a/NEWS b/NEWS index 511001e5c..e0f129fae 100644 --- a/NEWS +++ b/NEWS @@ -70,11 +70,16 @@ ver 0.24 (not yet released) * documentation: switch to sphinx-rtd-theme * require Meson 1.0 -ver 0.23.17 (not yet released) +ver 0.23.17 (2025/01/29) +* protocol + - "albumart" tries to send larger chunks if available + - explicitly disallow "idle" and "noidle" in command lists * storage - nfs: require libnfs 4.0 or later * database - inotify: trigger update after symlink was created +* decoder + - ffmpeg: prefer over sndfile and audiofile for its DTS-WAV support * support libfmt 11.1 ver 0.23.16 (2024/12/03) diff --git a/doc/protocol.rst b/doc/protocol.rst index 3062549be..7dbe4506c 100644 --- a/doc/protocol.rst +++ b/doc/protocol.rst @@ -156,6 +156,9 @@ fails, no more commands are executed and the appropriate ``list_OK`` is returned for each successful command executed in the command list. +Only synchronous commands can be used in command lists. Commands that +suspend execution (``idle`` and ``noidle``) are not allowed. + Ranges ====== diff --git a/src/client/Process.cxx b/src/client/Process.cxx index f463fcf1e..fbcd832ff 100644 --- a/src/client/Process.cxx +++ b/src/client/Process.cxx @@ -36,6 +36,13 @@ Client::ProcessCommandList(bool list_ok, return CommandResult::OK; } +[[gnu::pure]] +static bool +IsAsyncCommmand(const char *line) noexcept +{ + return StringIsEqual(line, "idle") || StringIsEqual(line, "noidle"); +} + CommandResult Client::ProcessLine(char *line) noexcept { @@ -51,6 +58,13 @@ Client::ProcessLine(char *line) noexcept return CommandResult::CLOSE; } + if (cmd_list.IsActive() && IsAsyncCommmand(line)) { + FmtWarning(client_domain, + "[{}] not possible in comand list: \"{}\"", + num, line); + return CommandResult::CLOSE; + } + if (StringIsEqual(line, "noidle")) { if (idle_waiting) { /* send empty idle response and leave idle mode */ diff --git a/src/command/FileCommands.cxx b/src/command/FileCommands.cxx index 96ad2087a..fae16dceb 100644 --- a/src/command/FileCommands.cxx +++ b/src/command/FileCommands.cxx @@ -197,7 +197,19 @@ read_stream_art(Response &r, const std::string_view art_directory, if (buffer_size > 0) { std::unique_lock lock{is->mutex}; is->Seek(lock, offset); + + const bool was_ready = is->IsReady(); + read_size = is->Read(lock, {buffer.get(), buffer_size}); + + if (was_ready && read_size < buffer_size / 2) + /* the InputStream was ready before, but we + got only very little data; probably just + some data left in the buffer without doing + any I/O; let's wait for the next low-level + read to complete to get more data for the + client */ + read_size += is->Read(lock, {buffer.get() + read_size, buffer_size - read_size}); } r.Fmt("size: {}\n", art_file_size); diff --git a/src/decoder/DecoderList.cxx b/src/decoder/DecoderList.cxx index f5b83acb9..d7ddac5fc 100644 --- a/src/decoder/DecoderList.cxx +++ b/src/decoder/DecoderList.cxx @@ -57,12 +57,6 @@ constinit const struct DecoderPlugin *const decoder_plugins[] = { #ifdef ENABLE_OPUS &opus_decoder_plugin, #endif -#ifdef ENABLE_SNDFILE - &sndfile_decoder_plugin, -#endif -#ifdef ENABLE_AUDIOFILE - &audiofile_decoder_plugin, -#endif #ifdef ENABLE_DSD &dsdiff_decoder_plugin, &dsf_decoder_plugin, @@ -103,6 +97,19 @@ constinit const struct DecoderPlugin *const decoder_plugins[] = { #ifdef ENABLE_FFMPEG &ffmpeg_decoder_plugin, #endif + + /* these WAV-decoding plugins are below ffmpeg_decoder_plugin + to give FFmpeg a chance to decode DTS-WAV files which is + technically DTS Coherent Acoustics (DCA) stream wrapped in + fake 16-bit stereo samples; neither libsndfile nor + libaudiofile detect this, but FFmpeg does */ +#ifdef ENABLE_SNDFILE + &sndfile_decoder_plugin, +#endif +#ifdef ENABLE_AUDIOFILE + &audiofile_decoder_plugin, +#endif + &pcm_decoder_plugin, nullptr }; diff --git a/src/input/AsyncInputStream.cxx b/src/input/AsyncInputStream.cxx index b6e464be5..7b036d3f8 100644 --- a/src/input/AsyncInputStream.cxx +++ b/src/input/AsyncInputStream.cxx @@ -171,9 +171,12 @@ AsyncInputStream::Read(std::unique_lock &lock, Check(); r = buffer.Read(); - if (!r.empty() || IsEOF()) + if (!r.empty()) break; + if (IsEOF()) + return 0; + caller_cond.wait(lock); } @@ -181,6 +184,12 @@ AsyncInputStream::Read(std::unique_lock &lock, memcpy(dest.data(), r.data(), nbytes); buffer.Consume(nbytes); + if (buffer.empty()) + /* when the buffer becomes empty, reset its head and + tail so the next write can fill the whole buffer + and not just the part after the tail */ + buffer.Clear(); + offset += (offset_type)nbytes; if (paused && buffer.GetSize() < resume_at) diff --git a/src/input/ThreadInputStream.cxx b/src/input/ThreadInputStream.cxx index 8487acf41..e72fd17a5 100644 --- a/src/input/ThreadInputStream.cxx +++ b/src/input/ThreadInputStream.cxx @@ -169,6 +169,14 @@ ThreadInputStream::Read(std::unique_lock &lock, size_t nbytes = std::min(dest.size(), r.size()); memcpy(dest.data(), r.data(), nbytes); buffer.Consume(nbytes); + + if (buffer.empty()) + /* when the buffer becomes empty, + reset its head and tail so the next + write can fill the whole buffer and + not just the part after the tail */ + buffer.Clear(); + wake_cond.notify_one(); offset += nbytes; return nbytes;