diff --git a/NEWS b/NEWS
index 3d6f0a112..f0a8434c2 100644
--- a/NEWS
+++ b/NEWS
@@ -13,6 +13,8 @@ ver 0.22.10 (not yet released)
 * 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/src/mixer/plugins/AlsaMixerPlugin.cxx b/src/mixer/plugins/AlsaMixerPlugin.cxx
index 27243cfb4..8328e11a4 100644
--- a/src/mixer/plugins/AlsaMixerPlugin.cxx
+++ b/src/mixer/plugins/AlsaMixerPlugin.cxx
@@ -80,6 +80,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),
@@ -167,6 +185,17 @@ AlsaMixer::ElemCallback(snd_mixer_elem_t *elem, unsigned mask) noexcept
 
 	if (mask & SND_CTL_EVENT_MASK_VALUE) {
 		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);
 	}
 
@@ -253,6 +282,8 @@ AlsaMixer::Setup()
 void
 AlsaMixer::Open()
 {
+	desired_volume = resulting_volume = -1;
+
 	int err;
 
 	err = snd_mixer_open(&handle, 0);
@@ -291,7 +322,12 @@ AlsaMixer::GetVolume()
 		throw FormatRuntimeError("snd_mixer_handle_events() failed: %s",
 					 snd_strerror(err));
 
-	return GetPercentVolume();
+	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
@@ -299,12 +335,13 @@ AlsaMixer::SetVolume(unsigned volume)
 {
 	assert(handle != nullptr);
 
-	double cur = GetNormalizedVolume();
-	int delta = volume - NormalizedToPercent(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 = {