diff --git a/.gitignore b/.gitignore
index c2fc6512f..cf00b5c57 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,6 @@
 /output/
 
 __pycache__/
+
+/.clangd/
+/compile_commands.json
diff --git a/.travis.yml b/.travis.yml
index 38bf7a331..baa55430e 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -109,7 +109,8 @@ jobs:
             - chromaprint
             - libsamplerate
             - libsoxr
-            - libzzip
+            # libzzip appears to be broken on Homebrew: "ld: library not found for -lzzip"
+            #- libzzip
             - flac
             - opus
             - libvorbis
diff --git a/NEWS b/NEWS
index 696aa4506..8a8dd0c92 100644
--- a/NEWS
+++ b/NEWS
@@ -43,8 +43,17 @@ ver 0.21.24 (not yet released)
   - "tagtypes" requires no permissions
 * database
   - simple: fix crash when mounting twice
+* decoder
+  - modplug: fix Windows build failure
+  - wildmidi: attempt to detect WildMidi using pkg-config
+  - wildmidi: fix Windows build failure
+* Android
+  - enable the decoder plugins ModPlug and WildMidi
+  - fix build failure with Android NDK r21
+* Windows
+  - enable the decoder plugins ModPlug and WildMidi
+  - work around Meson bug breaking the Windows build with GCC 10
 * fix unit test failure
-* fix build failure with Android NDK r21
 
 ver 0.21.23 (2020/04/23)
 * protocol
diff --git a/android/build.py b/android/build.py
index 91e292683..8716fc1a9 100755
--- a/android/build.py
+++ b/android/build.py
@@ -168,6 +168,8 @@ thirdparty_libs = [
     opus,
     flac,
     libid3tag,
+    libmodplug,
+    wildmidi,
     ffmpeg,
     curl,
     libexpat,
diff --git a/meson.build b/meson.build
index 085b97bbc..ed1a36eb2 100644
--- a/meson.build
+++ b/meson.build
@@ -5,7 +5,8 @@ project(
   meson_version: '>= 0.49.0',
   default_options: [
     'c_std=c99',
-    'cpp_std=c++17'
+    'cpp_std=c++17',
+    'warning_level=2',
   ],
   license: 'GPLv2+',
 )
@@ -153,7 +154,13 @@ conf.set('HAVE_GETPWNAM_R', compiler.has_function('getpwnam_r'))
 conf.set('HAVE_GETPWUID_R', compiler.has_function('getpwuid_r'))
 conf.set('HAVE_INITGROUPS', compiler.has_function('initgroups'))
 conf.set('HAVE_FNMATCH', compiler.has_function('fnmatch'))
-conf.set('HAVE_STRNDUP', compiler.has_function('strndup', prefix: '#define _GNU_SOURCE\n#include <string.h>'))
+
+# Explicitly exclude Windows in this check because
+# https://github.com/mesonbuild/meson/issues/3672 (reported in 2018,
+# still not fixed in 2020) causes Meson to believe it exists, because
+# __builtin_strndup() exists (but strndup() still cannot be used).
+conf.set('HAVE_STRNDUP', not is_windows and compiler.has_function('strndup', prefix: '#define _GNU_SOURCE\n#include <string.h>'))
+
 conf.set('HAVE_STRCASESTR', compiler.has_function('strcasestr'))
 
 conf.set('HAVE_PRCTL', is_linux)
diff --git a/python/build/cmake.py b/python/build/cmake.py
new file mode 100644
index 000000000..76f217715
--- /dev/null
+++ b/python/build/cmake.py
@@ -0,0 +1,45 @@
+import subprocess
+
+from build.project import Project
+
+def configure(toolchain, src, build, args=()):
+    cross_args = []
+
+    if toolchain.is_windows:
+        cross_args.append('-DCMAKE_SYSTEM_NAME=Windows')
+        cross_args.append('-DCMAKE_RC_COMPILER=' + toolchain.windres)
+
+    configure = [
+        'cmake',
+        src,
+
+        '-DCMAKE_INSTALL_PREFIX=' + toolchain.install_prefix,
+        '-DCMAKE_BUILD_TYPE=release',
+
+        '-DCMAKE_C_COMPILER=' + toolchain.cc,
+        '-DCMAKE_CXX_COMPILER=' + toolchain.cxx,
+
+        '-DCMAKE_C_FLAGS=' + toolchain.cflags + ' ' + toolchain.cppflags,
+        '-DCMAKE_CXX_FLAGS=' + toolchain.cxxflags + ' ' + toolchain.cppflags,
+
+        '-GNinja',
+    ] + cross_args + args
+
+    subprocess.check_call(configure, env=toolchain.env, cwd=build)
+
+class CmakeProject(Project):
+    def __init__(self, url, md5, installed, configure_args=[],
+                 **kwargs):
+        Project.__init__(self, url, md5, installed, **kwargs)
+        self.configure_args = configure_args
+
+    def configure(self, toolchain):
+        src = self.unpack(toolchain)
+        build = self.make_build_path(toolchain)
+        configure(toolchain, src, build, self.configure_args)
+        return build
+
+    def build(self, toolchain):
+        build = self.configure(toolchain)
+        subprocess.check_call(['ninja', 'install'],
+                              cwd=build, env=toolchain.env)
diff --git a/python/build/libs.py b/python/build/libs.py
index 0063e220c..c7b2250cd 100644
--- a/python/build/libs.py
+++ b/python/build/libs.py
@@ -4,6 +4,7 @@ from os.path import abspath
 from build.project import Project
 from build.zlib import ZlibProject
 from build.meson import MesonProject
+from build.cmake import CmakeProject
 from build.autotools import AutotoolsProject
 from build.ffmpeg import FfmpegProject
 from build.boost import BoostProject
@@ -111,9 +112,32 @@ liblame = AutotoolsProject(
     ],
 )
 
+libmodplug = AutotoolsProject(
+    'https://downloads.sourceforge.net/modplug-xmms/libmodplug/0.8.9.0/libmodplug-0.8.9.0.tar.gz',
+    '457ca5a6c179656d66c01505c0d95fafaead4329b9dbaa0f997d00a3508ad9de',
+    'lib/libmodplug.a',
+    [
+        '--disable-shared', '--enable-static',
+    ],
+)
+
+wildmidi = CmakeProject(
+    'https://codeload.github.com/Mindwerks/wildmidi/tar.gz/wildmidi-0.4.3',
+    '498e5a96455bb4b91b37188ad6dcb070824e92c44f5ed452b90adbaec8eef3c5',
+    'lib/libWildMidi.a',
+    [
+        '-DBUILD_SHARED_LIBS=OFF',
+        '-DWANT_PLAYER=OFF',
+        '-DWANT_STATIC=ON',
+    ],
+    base='wildmidi-wildmidi-0.4.3',
+    name='wildmidi',
+    version='0.4.3',
+)
+
 ffmpeg = FfmpegProject(
-    'http://ffmpeg.org/releases/ffmpeg-4.2.2.tar.xz',
-    'cb754255ab0ee2ea5f66f8850e1bd6ad5cac1cd855d0a2f4990fb8c668b0d29c',
+    'http://ffmpeg.org/releases/ffmpeg-4.2.3.tar.xz',
+    '9df6c90aed1337634c1fb026fb01c154c29c82a64ea71291ff2da9aacb9aad31',
     'lib/libavcodec.a',
     [
         '--disable-shared', '--enable-static',
diff --git a/src/decoder/plugins/ModplugDecoderPlugin.cxx b/src/decoder/plugins/ModplugDecoderPlugin.cxx
index 380891f34..1cd820e80 100644
--- a/src/decoder/plugins/ModplugDecoderPlugin.cxx
+++ b/src/decoder/plugins/ModplugDecoderPlugin.cxx
@@ -27,6 +27,12 @@
 #include "util/StringView.hxx"
 #include "Log.hxx"
 
+#ifdef _WIN32
+/* assume ModPlug is built as static library on Windows; without
+   this, linking to the static library would fail */
+#define MODPLUG_STATIC
+#endif
+
 #include <libmodplug/modplug.h>
 
 #include <cassert>
diff --git a/src/decoder/plugins/WildmidiDecoderPlugin.cxx b/src/decoder/plugins/WildmidiDecoderPlugin.cxx
index ff3d42e19..d6ab6da40 100644
--- a/src/decoder/plugins/WildmidiDecoderPlugin.cxx
+++ b/src/decoder/plugins/WildmidiDecoderPlugin.cxx
@@ -25,8 +25,15 @@
 #include "fs/AllocatedPath.hxx"
 #include "fs/FileSystem.hxx"
 #include "fs/Path.hxx"
+#include "fs/NarrowPath.hxx"
 #include "PluginUnavailable.hxx"
 
+#ifdef _WIN32
+/* assume WildMidi is built as static library on Windows; without
+   this, linking to the static library would fail */
+#define WILDMIDI_STATIC
+#endif
+
 extern "C" {
 #include <wildmidi_lib.h>
 }
@@ -52,7 +59,8 @@ wildmidi_init(const ConfigBlock &block)
 	AtScopeExit() { WildMidi_ClearError(); };
 #endif
 
-	if (WildMidi_Init(path.c_str(), wildmidi_audio_format.sample_rate,
+	if (WildMidi_Init(NarrowPath(path),
+			  wildmidi_audio_format.sample_rate,
 			  0) != 0) {
 #ifdef LIBWILDMIDI_VERSION
 		/* WildMidi_GetError() requires libwildmidi 0.4 */
@@ -95,7 +103,7 @@ wildmidi_file_decode(DecoderClient &client, Path path_fs)
 	midi *wm;
 	const struct _WM_Info *info;
 
-	wm = WildMidi_Open(path_fs.c_str());
+	wm = WildMidi_Open(NarrowPath(path_fs));
 	if (wm == nullptr)
 		return;
 
@@ -135,7 +143,7 @@ wildmidi_file_decode(DecoderClient &client, Path path_fs)
 static bool
 wildmidi_scan_file(Path path_fs, TagHandler &handler) noexcept
 {
-	midi *wm = WildMidi_Open(path_fs.c_str());
+	midi *wm = WildMidi_Open(NarrowPath(path_fs));
 	if (wm == nullptr)
 		return false;
 
diff --git a/src/decoder/plugins/meson.build b/src/decoder/plugins/meson.build
index 6cb5d427d..0b37363fe 100644
--- a/src/decoder/plugins/meson.build
+++ b/src/decoder/plugins/meson.build
@@ -129,7 +129,16 @@ if wavpack_dep.found()
   decoder_plugins_sources += 'WavpackDecoderPlugin.cxx'
 endif
 
-wildmidi_dep = c_compiler.find_library('WildMidi', required: get_option('wildmidi'))
+wildmidi_required = get_option('wildmidi')
+if wildmidi_required.enabled()
+  # if the user has force-enabled WildMidi, allow the pkg-config test
+  # to fail; after that, the find_library() check must succeed
+  wildmidi_required = false
+endif
+wildmidi_dep = dependency('wildmidi', required: wildmidi_required)
+if not wildmidi_dep.found()
+  wildmidi_dep = c_compiler.find_library('WildMidi', required: get_option('wildmidi'))
+endif
 decoder_features.set('ENABLE_WILDMIDI', wildmidi_dep.found())
 if wildmidi_dep.found()
   decoder_plugins_sources += 'WildmidiDecoderPlugin.cxx'
diff --git a/src/lib/ffmpeg/Time.hxx b/src/lib/ffmpeg/Time.hxx
index 86bea09a2..52f6bc25f 100644
--- a/src/lib/ffmpeg/Time.hxx
+++ b/src/lib/ffmpeg/Time.hxx
@@ -45,7 +45,7 @@ FfmpegTimeToDouble(int64_t t, const AVRational time_base) noexcept
 {
 	assert(t != (int64_t)AV_NOPTS_VALUE);
 
-	return FloatDuration(av_rescale_q(t, time_base, (AVRational){1, 1024}))
+	return FloatDuration(av_rescale_q(t, time_base, {1, 1024}))
 		/ 1024;
 }
 
@@ -69,7 +69,7 @@ FromFfmpegTime(int64_t t, const AVRational time_base) noexcept
 	assert(t != (int64_t)AV_NOPTS_VALUE);
 
 	return SongTime::FromMS(av_rescale_q(t, time_base,
-					     (AVRational){1, 1000}));
+					     {1, 1000}));
 }
 
 /**
diff --git a/win32/build.py b/win32/build.py
index 0e09003fe..9e1b6e9e4 100755
--- a/win32/build.py
+++ b/win32/build.py
@@ -96,6 +96,8 @@ thirdparty_libs = [
     zlib,
     libid3tag,
     liblame,
+    libmodplug,
+    wildmidi,
     ffmpeg,
     curl,
     libexpat,