diff --git a/Makefile.am b/Makefile.am
index 854934329..7f9ef3e93 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -151,6 +151,7 @@ libmpd_a_SOURCES = \
 	src/playlist/PlaylistSong.cxx src/playlist/PlaylistSong.hxx \
 	src/playlist/PlaylistQueue.cxx src/playlist/PlaylistQueue.hxx \
 	src/playlist/Print.cxx src/playlist/Print.hxx \
+	src/BulkEdit.hxx \
 	src/db/PlaylistVector.cxx src/db/PlaylistVector.hxx \
 	src/db/PlaylistInfo.hxx \
 	src/queue/IdTable.hxx \
diff --git a/NEWS b/NEWS
index 5249be7f8..9073a2a23 100644
--- a/NEWS
+++ b/NEWS
@@ -56,6 +56,9 @@ ver 0.18.12 (not yet released)
   - audiofile: improve responsiveness
   - audiofile: fix WAV stream playback
   - dsdiff, dsf: fix stream playback
+  - sndfile: improve responsiveness
+* randomize next song when enabling "random" mode while not playing
+* randomize next song when adding to single-song queue
 
 ver 0.18.11 (2014/05/12)
 * decoder
diff --git a/src/BulkEdit.hxx b/src/BulkEdit.hxx
new file mode 100644
index 000000000..422dc4f38
--- /dev/null
+++ b/src/BulkEdit.hxx
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_BULK_EDIT_HXX
+#define MPD_BULK_EDIT_HXX
+
+#include "Partition.hxx"
+
+/**
+ * Begin a "bulk edit" and commit it automatically.
+ */
+class ScopeBulkEdit {
+	Partition &partition;
+
+public:
+	ScopeBulkEdit(Partition &_partition):partition(_partition) {
+		partition.playlist.BeginBulk();
+	}
+
+	~ScopeBulkEdit() {
+		partition.playlist.CommitBulk(partition.pc);
+	}
+};
+
+#endif
diff --git a/src/command/DatabaseCommands.cxx b/src/command/DatabaseCommands.cxx
index 70d97a63e..a3ea8d0ae 100644
--- a/src/command/DatabaseCommands.cxx
+++ b/src/command/DatabaseCommands.cxx
@@ -32,6 +32,7 @@
 #include "util/Error.hxx"
 #include "SongFilter.hxx"
 #include "protocol/Result.hxx"
+#include "BulkEdit.hxx"
 
 #include <string.h>
 
@@ -106,6 +107,8 @@ handle_match_add(Client &client, unsigned argc, char *argv[], bool fold_case)
 		return CommandResult::ERROR;
 	}
 
+	const ScopeBulkEdit bulk_edit(client.partition);
+
 	const DatabaseSelection selection("", true, &filter);
 	Error error;
 	return AddFromDatabase(client.partition, selection, error)
diff --git a/src/command/PlaylistCommands.cxx b/src/command/PlaylistCommands.cxx
index 79bfb44d8..c2b18064c 100644
--- a/src/command/PlaylistCommands.cxx
+++ b/src/command/PlaylistCommands.cxx
@@ -26,6 +26,7 @@
 #include "PlaylistFile.hxx"
 #include "db/PlaylistVector.hxx"
 #include "SongLoader.hxx"
+#include "BulkEdit.hxx"
 #include "playlist/PlaylistQueue.hxx"
 #include "playlist/Print.hxx"
 #include "queue/Playlist.hxx"
@@ -66,6 +67,8 @@ handle_load(Client &client, unsigned argc, char *argv[])
 	} else if (!check_range(client, &start_index, &end_index, argv[2]))
 		return CommandResult::ERROR;
 
+	const ScopeBulkEdit bulk_edit(client.partition);
+
 	Error error;
 	const SongLoader loader(client);
 	if (!playlist_open_into_queue(argv[1],
diff --git a/src/command/QueueCommands.cxx b/src/command/QueueCommands.cxx
index 0d0cff5cf..1ff7a732b 100644
--- a/src/command/QueueCommands.cxx
+++ b/src/command/QueueCommands.cxx
@@ -28,6 +28,7 @@
 #include "PlaylistPrint.hxx"
 #include "client/Client.hxx"
 #include "Partition.hxx"
+#include "BulkEdit.hxx"
 #include "protocol/ArgParser.hxx"
 #include "protocol/Result.hxx"
 #include "ls.hxx"
@@ -74,6 +75,8 @@ handle_add(Client &client, gcc_unused unsigned argc, char *argv[])
 	}
 
 #ifdef ENABLE_DATABASE
+	const ScopeBulkEdit bulk_edit(client.partition);
+
 	const DatabaseSelection selection(uri, true);
 	Error error;
 	return AddFromDatabase(client.partition, selection, error)
diff --git a/src/decoder/plugins/AudiofileDecoderPlugin.cxx b/src/decoder/plugins/AudiofileDecoderPlugin.cxx
index 603f714d1..93a6e03a2 100644
--- a/src/decoder/plugins/AudiofileDecoderPlugin.cxx
+++ b/src/decoder/plugins/AudiofileDecoderPlugin.cxx
@@ -55,7 +55,7 @@ struct AudioFileInputStream {
 
 	size_t Read(void *buffer, size_t size) {
 		/* libaudiofile does not like partial reads at all,
-		   and wil abort playback; therefore always force full
+		   and will abort playback; therefore always force full
 		   reads */
 		return decoder_read_full(decoder, is, buffer, size)
 			? size
@@ -118,9 +118,11 @@ audiofile_file_seek(AFvirtualfile *vfile, AFfileoffset _offset,
 	if (is_relative)
 		offset += is.GetOffset();
 
-	if (is.LockSeek(offset, IgnoreError())) {
+	Error error;
+	if (is.LockSeek(offset, error)) {
 		return is.GetOffset();
 	} else {
+		LogError(error, "Seek failed");
 		return -1;
 	}
 }
diff --git a/src/decoder/plugins/SndfileDecoderPlugin.cxx b/src/decoder/plugins/SndfileDecoderPlugin.cxx
index cf3aa61d5..1901ea7e4 100644
--- a/src/decoder/plugins/SndfileDecoderPlugin.cxx
+++ b/src/decoder/plugins/SndfileDecoderPlugin.cxx
@@ -32,10 +32,24 @@
 
 static constexpr Domain sndfile_domain("sndfile");
 
+struct SndfileInputStream {
+	Decoder *const decoder;
+	InputStream &is;
+
+	size_t Read(void *buffer, size_t size) {
+		/* libsndfile chokes on partial reads; therefore
+		   always force full reads */
+		return decoder_read_full(decoder, is, buffer, size)
+			? size
+			: 0;
+	}
+};
+
 static sf_count_t
 sndfile_vio_get_filelen(void *user_data)
 {
-	const InputStream &is = *(const InputStream *)user_data;
+	SndfileInputStream &sis = *(SndfileInputStream *)user_data;
+	const InputStream &is = sis.is;
 
 	return is.GetSize();
 }
@@ -43,7 +57,8 @@ sndfile_vio_get_filelen(void *user_data)
 static sf_count_t
 sndfile_vio_seek(sf_count_t _offset, int whence, void *user_data)
 {
-	InputStream &is = *(InputStream *)user_data;
+	SndfileInputStream &sis = *(SndfileInputStream *)user_data;
+	InputStream &is = sis.is;
 
 	InputStream::offset_type offset = _offset;
 	switch (whence) {
@@ -65,8 +80,11 @@ sndfile_vio_seek(sf_count_t _offset, int whence, void *user_data)
 		return -1;
 	}
 
-	if (!is.LockSeek(offset, IgnoreError()))
+	Error error;
+	if (!is.LockSeek(offset, error)) {
+		LogError(error, "Seek failed");
 		return -1;
+	}
 
 	return is.GetOffset();
 }
@@ -74,30 +92,9 @@ sndfile_vio_seek(sf_count_t _offset, int whence, void *user_data)
 static sf_count_t
 sndfile_vio_read(void *ptr, sf_count_t count, void *user_data)
 {
-	InputStream &is = *(InputStream *)user_data;
+	SndfileInputStream &sis = *(SndfileInputStream *)user_data;
 
-	sf_count_t total_bytes = 0;
-	Error error;
-
-	/* this loop is necessary because libsndfile chokes on partial
-	   reads */
-
-	do {
-		size_t nbytes = is.LockRead((char *)ptr + total_bytes,
-					    count - total_bytes, error);
-		if (nbytes == 0) {
-			if (error.IsDefined()) {
-				LogError(error);
-				return -1;
-			}
-
-			break;
-		}
-
-		total_bytes += nbytes;
-	} while (total_bytes < count);
-
-	return total_bytes;
+	return sis.Read(ptr, count);
 }
 
 static sf_count_t
@@ -112,7 +109,8 @@ sndfile_vio_write(gcc_unused const void *ptr,
 static sf_count_t
 sndfile_vio_tell(void *user_data)
 {
-	const InputStream &is = *(const InputStream *)user_data;
+	SndfileInputStream &sis = *(SndfileInputStream *)user_data;
+	const InputStream &is = sis.is;
 
 	return is.GetOffset();
 }
@@ -158,7 +156,8 @@ sndfile_stream_decode(Decoder &decoder, InputStream &is)
 
 	info.format = 0;
 
-	sf = sf_open_virtual(&vio, SFM_READ, &info, &is);
+	SndfileInputStream sis{&decoder, is};
+	sf = sf_open_virtual(&vio, SFM_READ, &info, &sis);
 	if (sf == nullptr) {
 		LogWarning(sndfile_domain, "sf_open_virtual() failed");
 		return;
diff --git a/src/queue/Playlist.cxx b/src/queue/Playlist.cxx
index abcb2ceaa..b2fd673b4 100644
--- a/src/queue/Playlist.cxx
+++ b/src/queue/Playlist.cxx
@@ -99,6 +99,12 @@ playlist::UpdateQueuedSong(PlayerControl &pc, const DetachedSong *prev)
 	if (!playing)
 		return;
 
+	if (prev == nullptr && bulk_edit)
+		/* postponed until CommitBulk() to avoid always
+		   queueing the first song that is being added (in
+		   random mode) */
+		return;
+
 	assert(!queue.IsEmpty());
 	assert((queued < 0) == (prev == nullptr));
 
@@ -286,7 +292,9 @@ playlist::SetRandom(PlayerControl &pc, bool status)
 	if (queue.random) {
 		/* shuffle the queue order, but preserve current */
 
-		const int current_position = GetCurrentPosition();
+		const int current_position = playing
+			? GetCurrentPosition()
+			: -1;
 
 		queue.ShuffleOrder();
 
diff --git a/src/queue/Playlist.hxx b/src/queue/Playlist.hxx
index 4a4c7f30c..f2d778382 100644
--- a/src/queue/Playlist.hxx
+++ b/src/queue/Playlist.hxx
@@ -49,6 +49,18 @@ struct playlist {
 	 */
 	bool stop_on_error;
 
+	/**
+	 * If true, then a bulk edit has been initiated by
+	 * BeginBulk(), and UpdateQueuedSong() and OnModified() will
+	 * be postponed until CommitBulk()
+	 */
+	bool bulk_edit;
+
+	/**
+	 * Has the queue been modified during bulk edit mode?
+	 */
+	bool bulk_modified;
+
 	/**
 	 * Number of errors since playback was started.  If this
 	 * number exceeds the length of the playlist, MPD gives up,
@@ -73,7 +85,9 @@ struct playlist {
 	int queued;
 
 	playlist(unsigned max_length)
-		:queue(max_length), playing(false), current(-1), queued(-1) {
+		:queue(max_length), playing(false),
+		 bulk_edit(false),
+		 current(-1), queued(-1) {
 	}
 
 	~playlist() {
@@ -130,6 +144,9 @@ protected:
 	void UpdateQueuedSong(PlayerControl &pc, const DetachedSong *prev);
 
 public:
+	void BeginBulk();
+	void CommitBulk(PlayerControl &pc);
+
 	void Clear(PlayerControl &pc);
 
 	/**
diff --git a/src/queue/PlaylistControl.cxx b/src/queue/PlaylistControl.cxx
index 9d75cc26d..db0b8a25d 100644
--- a/src/queue/PlaylistControl.cxx
+++ b/src/queue/PlaylistControl.cxx
@@ -153,7 +153,7 @@ playlist::PlayNext(PlayerControl &pc)
 			queue.ShuffleOrder();
 
 			/* note that current and queued are
-			   now invalid, but playlist_play_order() will
+			   now invalid, but PlayOrder() will
 			   discard them anyway */
 		}
 
diff --git a/src/queue/PlaylistEdit.cxx b/src/queue/PlaylistEdit.cxx
index e8f1a178f..92865cab7 100644
--- a/src/queue/PlaylistEdit.cxx
+++ b/src/queue/PlaylistEdit.cxx
@@ -38,6 +38,12 @@
 void
 playlist::OnModified()
 {
+	if (bulk_edit) {
+		/* postponed to CommitBulk() */
+		bulk_modified = true;
+		return;
+	}
+
 	queue.IncrementVersion();
 
 	idle_add(IDLE_PLAYLIST);
@@ -54,6 +60,35 @@ playlist::Clear(PlayerControl &pc)
 	OnModified();
 }
 
+void
+playlist::BeginBulk()
+{
+	assert(!bulk_edit);
+
+	bulk_edit = true;
+	bulk_modified = false;
+}
+
+void
+playlist::CommitBulk(PlayerControl &pc)
+{
+	assert(bulk_edit);
+
+	bulk_edit = false;
+	if (!bulk_modified)
+		return;
+
+	if (queued < 0)
+		/* if no song was queued, UpdateQueuedSong() is being
+		   ignored in "bulk" edit mode; now that we have
+		   shuffled all new songs, we can pick a random one
+		   (instead of always picking the first one that was
+		   added) */
+		UpdateQueuedSong(pc, nullptr);
+
+	OnModified();
+}
+
 unsigned
 playlist::AppendSong(PlayerControl &pc, DetachedSong &&song, Error &error)
 {