From d3c39952cfb432376d87531cc45616a1af9d905d Mon Sep 17 00:00:00 2001 From: 0x0003 <0x0003@users.noreply.github.com> Date: Tue, 14 Jul 2026 15:10:37 +0900 Subject: [PATCH 1/9] python/build/libs.py: add mpg123 1.33.6 --- NEWS | 2 ++ python/build/libs.py | 11 +++++++++++ win32/build.py | 1 + 3 files changed, 14 insertions(+) diff --git a/NEWS b/NEWS index cd0b4d33f6..f9b8444347 100644 --- a/NEWS +++ b/NEWS @@ -3,6 +3,8 @@ ver 0.24.14 (not yet released) - curl: bound WebDAV PROPFIND responses * input - qobuz: use HTTPS for API requests +* Windows + - enable the "mpg123" decoder plugin ver 0.24.13 (2026/07/10) * configuration diff --git a/python/build/libs.py b/python/build/libs.py index 1ffdeaa3e7..02d2fe96bb 100644 --- a/python/build/libs.py +++ b/python/build/libs.py @@ -562,6 +562,17 @@ ], ) +libmpg123 = AutotoolsProject( + 'https://mpg123.de/download/mpg123-1.33.6.tar.bz2', + '0b10b3b33547f51e52694d069ca3cfe6', + 'lib/libmpg123.a', + [ + '--disable-shared', '--enable-static', + '--disable-components', '--enable-libmpg123', + '--disable-modules', '--disable-debug', + ], +) + libnfs = AutotoolsProject( 'https://github.com/sahlberg/libnfs/archive/libnfs-6.0.2.tar.gz', '4e5459cc3e0242447879004e9ad28286d4d27daa42cbdcde423248fad911e747', diff --git a/win32/build.py b/win32/build.py index 898a6167cb..0fde75c2bf 100755 --- a/win32/build.py +++ b/win32/build.py @@ -44,6 +44,7 @@ wildmidi, gme, ffmpeg, + libmpg123, libnfs, libsamplerate, ] From f70785ad0546b8531ff6e970d91d097dcfe7f05a Mon Sep 17 00:00:00 2001 From: 0x0003 <0x0003@users.noreply.github.com> Date: Sun, 12 Jul 2026 08:51:44 +0900 Subject: [PATCH 2/9] win32: use InjectBreak() for cross-thread shutdown Break() only sets a flag without waking select(), so the console handler thread and service dispatcher thread can never actually stop the event loop. Use InjectBreak() instead, which writes to the wake pipe to interrupt select(). Without this, ^c in a console and "net stop mpd" both fail to shut down MPD gracefully - the process must be hard-killed, losing the state file. --- NEWS | 1 + src/win32/Win32Main.cxx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index f9b8444347..258c18e8b8 100644 --- a/NEWS +++ b/NEWS @@ -5,6 +5,7 @@ ver 0.24.14 (not yet released) - qobuz: use HTTPS for API requests * Windows - enable the "mpg123" decoder plugin + - fix shutdown in console mode ver 0.24.13 (2026/07/10) * configuration diff --git a/src/win32/Win32Main.cxx b/src/win32/Win32Main.cxx index b8747613bd..c48f03a644 100644 --- a/src/win32/Win32Main.cxx +++ b/src/win32/Win32Main.cxx @@ -89,7 +89,7 @@ console_handler(DWORD event) // regardless our thread is still active. // If this did not happen within 3 seconds // let's shutdown anyway. - global_instance->Break(); + global_instance->event_loop.InjectBreak(); // Under debugger it's better to wait indefinitely // to allow debugging of shutdown code. Sleep(IsDebuggerPresent() ? INFINITE : 3000); From 208a6b2abb69dde5c94a96adebc16001e1bb4569 Mon Sep 17 00:00:00 2001 From: 0x0003 <0x0003@users.noreply.github.com> Date: Tue, 14 Jul 2026 06:27:59 +0900 Subject: [PATCH 3/9] db/update: truncate mtime to seconds during song db checks Windows FILETIME has 100ns precision but the database stores mtimes as time_t (seconds). After restart, the comparison between the full-precision FILETIME value and the second-precision DB value always mismatches, forcing a full rescan on every startup. Truncate both sides of the comparison to seconds so this works whether song->mtime came from the filesystem (same session) or from a DB reload (restart). --- NEWS | 1 + src/db/update/UpdateSong.cxx | 19 +++++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/NEWS b/NEWS index 258c18e8b8..c808a32617 100644 --- a/NEWS +++ b/NEWS @@ -6,6 +6,7 @@ ver 0.24.14 (not yet released) * Windows - enable the "mpg123" decoder plugin - fix shutdown in console mode + - fix last-modified check during database update ver 0.24.13 (2026/07/10) * configuration diff --git a/src/db/update/UpdateSong.cxx b/src/db/update/UpdateSong.cxx index 33116894a0..5852eb4d8f 100644 --- a/src/db/update/UpdateSong.cxx +++ b/src/db/update/UpdateSong.cxx @@ -14,6 +14,18 @@ #include +/** + * Truncate to seconds to match the DB round-trip through time_t. + * This is the precision stored in the database file. + */ +static bool +CompareMtimeCoarse(std::chrono::system_clock::time_point info_mtime, + std::chrono::system_clock::time_point song_mtime) noexcept +{ + return std::chrono::system_clock::to_time_t(info_mtime) == + std::chrono::system_clock::to_time_t(song_mtime); +} + inline void UpdateWalk::UpdateSongFile2(Directory &directory, std::string_view name, std::string_view suffix, @@ -32,7 +44,9 @@ try { return; } - if (!(song != nullptr && info.mtime == song->mtime && !walk_discard) && + if (!(song != nullptr && + CompareMtimeCoarse(info.mtime, song->mtime) && + !walk_discard) && UpdateContainerFile(directory, name, suffix, info)) { return; } @@ -61,7 +75,8 @@ try { modified = true; FmtNotice(update_domain, "added {}/{}", directory.GetPath(), name); - } else if (info.mtime != song->mtime || walk_discard) { + } else if (!CompareMtimeCoarse(info.mtime, song->mtime) || + walk_discard) { FmtNotice(update_domain, "updating {}/{}", directory.GetPath(), name); if (song->UpdateFile(storage, info)) From 737edb8d523bd41b6fc9b8f5702b9ce5643a9782 Mon Sep 17 00:00:00 2001 From: Cyril Roelandt Date: Thu, 16 Jul 2026 23:24:44 +0200 Subject: [PATCH 4/9] mpdconf.example: Fix path for sticker_file --- doc/mpdconf.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/mpdconf.example b/doc/mpdconf.example index 419fd59e6a..55c8aaa02d 100644 --- a/doc/mpdconf.example +++ b/doc/mpdconf.example @@ -61,7 +61,7 @@ # The location of the sticker database. This is a database which # manages dynamic information attached to songs. # -#sticker_file "$XDG_CACHE_HOME/sticker.sql" +#sticker_file "$XDG_CACHE_HOME/mpd/sticker.sql" #sticker_file "~/.mpd/sticker.sql" # ############################################################################### From d5d049788fe74a1cc58635114755467de870afb2 Mon Sep 17 00:00:00 2001 From: Camille Scholtz Date: Fri, 17 Jul 2026 15:49:12 +0200 Subject: [PATCH 5/9] output/osx: fix use of uninitialized variable in osx_output_set_device_format() If the device has no stream with a matching linear PCM format, the function returned output_format.mSampleRate from an uninitialized struct - undefined behavior. With DSD enabled, this garbage value decided whether the DoP fallback to PCM was taken. Value-initialize output_format so the function deterministically returns 0 (kAudioStreamAnyRate) in that case, which the caller already interprets correctly as "the requested sample rate is not available". While at it, declare output_stream with its proper type AudioStreamID instead of int. --- src/output/plugins/OSXOutputPlugin.cxx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/output/plugins/OSXOutputPlugin.cxx b/src/output/plugins/OSXOutputPlugin.cxx index cc8d70b32d..70cfc11c80 100644 --- a/src/output/plugins/OSXOutputPlugin.cxx +++ b/src/output/plugins/OSXOutputPlugin.cxx @@ -388,8 +388,8 @@ osx_output_set_device_format(AudioDeviceID dev_id, aopa_device_streams); bool format_found = false; - int output_stream; - AudioStreamBasicDescription output_format; + AudioStreamID output_stream = kAudioObjectUnknown; + AudioStreamBasicDescription output_format{}; for (const auto stream : streams) { const auto direction = @@ -424,7 +424,7 @@ osx_output_set_device_format(AudioDeviceID dev_id, if (score > output_score) { output_score = score; output_format = format_desc; - output_stream = stream; // set the idx of the stream in the device + output_stream = stream; format_found = true; } } @@ -442,6 +442,9 @@ osx_output_set_device_format(AudioDeviceID dev_id, err); } + /* without a matching format, this returns 0 + (kAudioStreamAnyRate), which the caller interprets as "the + requested sample rate is not available" */ return output_format.mSampleRate; } From 1bd2911281c48145dee067793e460334ea3fb2f6 Mon Sep 17 00:00:00 2001 From: Camille Scholtz Date: Fri, 17 Jul 2026 15:49:32 +0200 Subject: [PATCH 6/9] output/osx: don't reset the best format score for each stream The best-score variable was declared inside the stream loop, so it was reset to zero for every output stream. On a device with more than one output stream, a worse-scoring format found in a later stream could overwrite a better-scoring format already selected from an earlier stream. Move the variable out of the loop so the selection considers all streams together. --- src/output/plugins/OSXOutputPlugin.cxx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/output/plugins/OSXOutputPlugin.cxx b/src/output/plugins/OSXOutputPlugin.cxx index 70cfc11c80..f8e7cd516e 100644 --- a/src/output/plugins/OSXOutputPlugin.cxx +++ b/src/output/plugins/OSXOutputPlugin.cxx @@ -388,6 +388,7 @@ osx_output_set_device_format(AudioDeviceID dev_id, aopa_device_streams); bool format_found = false; + float output_score = 0; AudioStreamID output_stream = kAudioObjectUnknown; AudioStreamBasicDescription output_format{}; @@ -402,8 +403,6 @@ osx_output_set_device_format(AudioDeviceID dev_id, AudioObjectGetPropertyDataArray(stream, aopa_stream_phys_formats); - float output_score = 0; - for (const auto &format : format_list) { AudioStreamBasicDescription format_desc = format.mFormat; std::string format_string; From 33885be06f9073f6d3691d6ce7ae3081fa6a2286 Mon Sep 17 00:00:00 2001 From: Camille Scholtz Date: Fri, 17 Jul 2026 15:50:54 +0200 Subject: [PATCH 7/9] output/osx: use std::lround() to convert the volume GetVolume() truncated the scaled volume, so e.g. 89.99 became 89 and a SetVolume()/GetVolume() round trip could drift downwards. Round to the nearest integer instead, like the other mixer plugins (Alsa, Wasapi, Winmm, PipeWire). --- src/output/plugins/OSXOutputPlugin.cxx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/output/plugins/OSXOutputPlugin.cxx b/src/output/plugins/OSXOutputPlugin.cxx index f8e7cd516e..50cdd6429e 100644 --- a/src/output/plugins/OSXOutputPlugin.cxx +++ b/src/output/plugins/OSXOutputPlugin.cxx @@ -28,6 +28,7 @@ #include #include +#include #include #include @@ -197,7 +198,7 @@ OSXOutput::GetVolume() const auto vol = AudioObjectGetPropertyDataT(dev_id, aopa); - return static_cast(vol * 100.0f); + return std::lround(vol * 100.0f); } void From 3154185c5732ae95cefd4a9a8bb750bf71eb8818 Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Fri, 17 Jul 2026 20:05:13 +0200 Subject: [PATCH 8/9] output/Command: throw ProtocolError on invalid index Code is simpler now. --- src/command/OutputCommands.cxx | 27 ++++++--------------------- src/output/OutputCommand.cxx | 23 +++++++++-------------- src/output/OutputCommand.hxx | 18 ++++++++++++------ 3 files changed, 27 insertions(+), 41 deletions(-) diff --git a/src/command/OutputCommands.cxx b/src/command/OutputCommands.cxx index f8395cc4be..d909b36870 100644 --- a/src/command/OutputCommands.cxx +++ b/src/command/OutputCommands.cxx @@ -14,53 +14,38 @@ #include "util/StringVerify.hxx" CommandResult -handle_enableoutput(Client &client, Request args, Response &r) +handle_enableoutput(Client &client, Request args, [[maybe_unused]] Response &r) { assert(args.size() == 1); unsigned device = args.ParseUnsigned(0); auto &partition = client.GetPartition(); - if (!audio_output_enable_index(partition, - device)) { - r.Error(ACK_ERROR_NO_EXIST, "No such audio output"); - return CommandResult::ERROR; - } - + audio_output_enable_index(partition, device); return CommandResult::OK; } CommandResult -handle_disableoutput(Client &client, Request args, Response &r) +handle_disableoutput(Client &client, Request args, [[maybe_unused]] Response &r) { assert(args.size() == 1); unsigned device = args.ParseUnsigned(0); auto &partition = client.GetPartition(); - if (!audio_output_disable_index(partition, - device)) { - r.Error(ACK_ERROR_NO_EXIST, "No such audio output"); - return CommandResult::ERROR; - } - + audio_output_disable_index(partition, device); return CommandResult::OK; } CommandResult -handle_toggleoutput(Client &client, Request args, Response &r) +handle_toggleoutput(Client &client, Request args, [[maybe_unused]] Response &r) { assert(args.size() == 1); unsigned device = args.ParseUnsigned(0); auto &partition = client.GetPartition(); - if (!audio_output_toggle_index(partition, - device)) { - r.Error(ACK_ERROR_NO_EXIST, "No such audio output"); - return CommandResult::ERROR; - } - + audio_output_toggle_index(partition, device); return CommandResult::OK; } diff --git a/src/output/OutputCommand.cxx b/src/output/OutputCommand.cxx index e0d11372e7..ce508c7d53 100644 --- a/src/output/OutputCommand.cxx +++ b/src/output/OutputCommand.cxx @@ -14,22 +14,23 @@ #include "mixer/Mixer.hxx" #include "mixer/Memento.hxx" #include "mixer/Listener.hxx" +#include "protocol/Ack.hxx" #include "protocol/IdleFlags.hxx" #include "Partition.hxx" extern unsigned audio_output_state_version; -bool +void audio_output_enable_index(Partition &partition, unsigned idx) { auto &outputs = partition.outputs; if (idx >= outputs.Size()) - return false; + throw ProtocolError(ACK_ERROR_NO_EXIST, "No such audio output"); auto &ao = outputs.Get(idx); if (!ao.LockSetEnabled(true)) - return true; + return; partition.EmitIdle(IDLE_OUTPUT); @@ -42,21 +43,19 @@ audio_output_enable_index(Partition &partition, ao.GetClient().ApplyEnabled(); ++audio_output_state_version; - - return true; } -bool +void audio_output_disable_index(Partition &partition, unsigned idx) { auto &outputs = partition.outputs; if (idx >= outputs.Size()) - return false; + throw ProtocolError(ACK_ERROR_NO_EXIST, "No such audio output"); auto &ao = outputs.Get(idx); if (!ao.LockSetEnabled(false)) - return true; + return; partition.EmitIdle(IDLE_OUTPUT); @@ -70,17 +69,15 @@ audio_output_disable_index(Partition &partition, ao.GetClient().ApplyEnabled(); ++audio_output_state_version; - - return true; } -bool +void audio_output_toggle_index(Partition &partition, unsigned idx) { auto &outputs = partition.outputs; if (idx >= outputs.Size()) - return false; + throw ProtocolError(ACK_ERROR_NO_EXIST, "No such audio output"); auto &ao = outputs.Get(idx); const bool enabled = ao.LockToggleEnabled(); @@ -98,6 +95,4 @@ audio_output_toggle_index(Partition &partition, ao.GetClient().ApplyEnabled(); ++audio_output_state_version; - - return true; } diff --git a/src/output/OutputCommand.hxx b/src/output/OutputCommand.hxx index a8d96e7dfb..78ecdbf6ab 100644 --- a/src/output/OutputCommand.hxx +++ b/src/output/OutputCommand.hxx @@ -13,25 +13,31 @@ struct Partition; /** - * Enables an audio output. Returns false if the specified output + * Enables an audio output. + * + * Throws #ProtocolError if the specified output * does not exist. */ -bool +void audio_output_enable_index(Partition &partition, unsigned idx); /** - * Disables an audio output. Returns false if the specified output + * Disables an audio output. + * + * Throws #ProtocolError if the specified output * does not exist. */ -bool +void audio_output_disable_index(Partition &partition, unsigned idx); /** - * Toggles an audio output. Returns false if the specified output + * Toggles an audio output. + * + * Throws #ProtocolError if the specified output * does not exist. */ -bool +void audio_output_toggle_index(Partition &partition, unsigned idx); From 6c3d6498b2ff66cf17ad93da76529bedb93b54f5 Mon Sep 17 00:00:00 2001 From: Max Kellermann Date: Fri, 17 Jul 2026 20:04:07 +0200 Subject: [PATCH 9/9] output/Command: add CheckPartitionOutput() --- src/command/OutputCommands.cxx | 8 +------- src/output/OutputCommand.cxx | 26 ++++++++++++-------------- src/output/OutputCommand.hxx | 8 ++++++++ 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/command/OutputCommands.cxx b/src/command/OutputCommands.cxx index d909b36870..237d60cb74 100644 --- a/src/command/OutputCommands.cxx +++ b/src/command/OutputCommands.cxx @@ -68,13 +68,7 @@ handle_outputset(Client &client, Request request, Response &response) const unsigned i = request.ParseUnsigned(0); auto &partition = client.GetPartition(); - auto &outputs = partition.outputs; - if (i >= outputs.Size()) { - response.Error(ACK_ERROR_NO_EXIST, "No such audio output"); - return CommandResult::ERROR; - } - - auto &ao = outputs.Get(i); + auto &ao = CheckPartitionOutput(partition, i); const char *const name = request[1]; if (!IsValidAttributeName(name)) { diff --git a/src/output/OutputCommand.cxx b/src/output/OutputCommand.cxx index ce508c7d53..c8a08443c1 100644 --- a/src/output/OutputCommand.cxx +++ b/src/output/OutputCommand.cxx @@ -20,15 +20,21 @@ extern unsigned audio_output_state_version; -void -audio_output_enable_index(Partition &partition, - unsigned idx) +AudioOutputControl & +CheckPartitionOutput(Partition &partition, unsigned idx) { auto &outputs = partition.outputs; if (idx >= outputs.Size()) throw ProtocolError(ACK_ERROR_NO_EXIST, "No such audio output"); - auto &ao = outputs.Get(idx); + return outputs.Get(idx); +} + +void +audio_output_enable_index(Partition &partition, + unsigned idx) +{ + auto &ao = CheckPartitionOutput(partition, idx); if (!ao.LockSetEnabled(true)) return; @@ -49,11 +55,7 @@ void audio_output_disable_index(Partition &partition, unsigned idx) { - auto &outputs = partition.outputs; - if (idx >= outputs.Size()) - throw ProtocolError(ACK_ERROR_NO_EXIST, "No such audio output"); - - auto &ao = outputs.Get(idx); + auto &ao = CheckPartitionOutput(partition, idx); if (!ao.LockSetEnabled(false)) return; @@ -75,11 +77,7 @@ void audio_output_toggle_index(Partition &partition, unsigned idx) { - auto &outputs = partition.outputs; - if (idx >= outputs.Size()) - throw ProtocolError(ACK_ERROR_NO_EXIST, "No such audio output"); - - auto &ao = outputs.Get(idx); + auto &ao = CheckPartitionOutput(partition, idx); const bool enabled = ao.LockToggleEnabled(); partition.EmitIdle(IDLE_OUTPUT); diff --git a/src/output/OutputCommand.hxx b/src/output/OutputCommand.hxx index 78ecdbf6ab..93d9228ec3 100644 --- a/src/output/OutputCommand.hxx +++ b/src/output/OutputCommand.hxx @@ -11,6 +11,14 @@ */ struct Partition; +class AudioOutputControl; + +/** + * Look up an output of the specified partition and throw + * #ProtocolError on error. + */ +AudioOutputControl & +CheckPartitionOutput(Partition &partition, unsigned idx); /** * Enables an audio output.