From 585e9489d97a0529683a495a78a679075b9979dc Mon Sep 17 00:00:00 2001
From: Thomas Goyne <plorkyeran@aegisub.org>
Date: Wed, 9 Jul 2014 07:22:49 -0700
Subject: [PATCH] Move some of the audio provider machinery to libaegisub

And add tests.
---
 Makefile.target                               |   2 +-
 libaegisub/Makefile                           |   1 +
 libaegisub/audio/provider.cpp                 | 137 +++++++
 .../audio/provider_convert.cpp                |  51 +--
 libaegisub/audio/provider_dummy.cpp           |  80 ++++
 .../audio/provider_hd.cpp                     |  46 +--
 .../audio/provider_lock.cpp                   |  15 +-
 libaegisub/audio/provider_pcm.cpp             | 239 +++++++++++
 .../audio/provider_ram.cpp                    |  53 +--
 .../include/libaegisub/audio/provider.h       |  95 +++++
 src/Makefile                                  |  10 +-
 src/audio_controller.cpp                      |   6 +-
 src/audio_controller.h                        |  34 +-
 src/audio_display.cpp                         |   4 +-
 src/audio_display.h                           |   6 +-
 src/audio_karaoke.cpp                         |   2 +-
 src/audio_karaoke.h                           |   4 +-
 src/audio_player.cpp                          |  29 +-
 src/audio_player_alsa.cpp                     |  10 +-
 src/audio_player_dsound.cpp                   |  14 +-
 src/audio_player_dsound2.cpp                  |  22 +-
 src/audio_player_openal.cpp                   |  20 +-
 src/audio_player_oss.cpp                      |  17 +-
 src/audio_player_portaudio.cpp                |  16 +-
 src/audio_player_pulse.cpp                    |  20 +-
 src/audio_provider.cpp                        | 191 ---------
 src/audio_provider_avs.cpp                    |   4 +-
 src/audio_provider_dummy.cpp                  |  88 ----
 src/audio_provider_factory.cpp                | 126 ++++++
 src/audio_provider_factory.h                  |  27 ++
 src/audio_provider_ffmpegsource.cpp           |  23 +-
 src/audio_provider_pcm.cpp                    | 375 ------------------
 src/audio_renderer.cpp                        |   7 +-
 src/audio_renderer.h                          |  12 +-
 src/audio_renderer_spectrum.cpp               |   2 +-
 src/audio_renderer_waveform.cpp               |   3 +-
 src/command/audio.cpp                         |  59 +--
 src/frame_main.cpp                            |   2 +-
 src/frame_main.h                              |   4 +-
 src/include/aegisub/audio_player.h            |  12 +-
 src/include/aegisub/audio_provider.h          |  99 -----
 src/preferences.cpp                           |   4 +-
 src/project.cpp                               |   9 +-
 src/project.h                                 |   8 +-
 tests/tests/audio.cpp                         | 356 +++++++++++++++++
 tests/tests/iconv.cpp                         |   2 +-
 46 files changed, 1272 insertions(+), 1074 deletions(-)
 create mode 100644 libaegisub/audio/provider.cpp
 rename src/audio_provider_convert.cpp => libaegisub/audio/provider_convert.cpp (83%)
 create mode 100644 libaegisub/audio/provider_dummy.cpp
 rename src/audio_provider_hd.cpp => libaegisub/audio/provider_hd.cpp (66%)
 rename src/audio_provider_lock.cpp => libaegisub/audio/provider_lock.cpp (79%)
 create mode 100644 libaegisub/audio/provider_pcm.cpp
 rename src/audio_provider_ram.cpp => libaegisub/audio/provider_ram.cpp (51%)
 create mode 100644 libaegisub/include/libaegisub/audio/provider.h
 delete mode 100644 src/audio_provider.cpp
 delete mode 100644 src/audio_provider_dummy.cpp
 create mode 100644 src/audio_provider_factory.cpp
 create mode 100644 src/audio_provider_factory.h
 delete mode 100644 src/audio_provider_pcm.cpp
 delete mode 100644 src/include/aegisub/audio_provider.h
 create mode 100644 tests/tests/audio.cpp

diff --git a/Makefile.target b/Makefile.target
index 5ca482f81..d3b6e1500 100644
--- a/Makefile.target
+++ b/Makefile.target
@@ -1,5 +1,5 @@
 ifneq (yes, $(INCLUDING_CHILD_MAKEFILES))
-COMMANDS := all install clean distclean test depclean osx-bundle osx-dmg test-automation
+COMMANDS := all install clean distclean test depclean osx-bundle osx-dmg test-automation test-libaegisub
 .PHONY: $(COMMANDS)
 .DEFAULT_GOAL := all
 
diff --git a/libaegisub/Makefile b/libaegisub/Makefile
index 1492a6f79..64204ef91 100644
--- a/libaegisub/Makefile
+++ b/libaegisub/Makefile
@@ -4,6 +4,7 @@ aegisub_OBJ := \
 	$(d)common/parser.o \
 	$(d)ass/dialogue_parser.o \
 	$(d)ass/time.o \
+	$(subst .cpp,.o,$(wildcard $(d)audio/*.cpp)) \
 	$(subst .cpp,.o,$(wildcard $(d)common/cajun/*.cpp)) \
 	$(subst .cpp,.o,$(wildcard $(d)lua/modules/*.cpp)) \
 	$(subst .c,.o,$(wildcard $(d)lua/modules/*.c)) \
diff --git a/libaegisub/audio/provider.cpp b/libaegisub/audio/provider.cpp
new file mode 100644
index 000000000..3c027470b
--- /dev/null
+++ b/libaegisub/audio/provider.cpp
@@ -0,0 +1,137 @@
+// Copyright (c) 2014, Thomas Goyne <plorkyeran@aegisub.org>
+//
+// Permission to use, copy, modify, and distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+//
+// Aegisub Project http://www.aegisub.org/
+
+#include "libaegisub/audio/provider.h"
+
+#include "libaegisub/fs.h"
+#include "libaegisub/io.h"
+#include "libaegisub/log.h"
+#include "libaegisub/util.h"
+
+namespace agi {
+void AudioProvider::GetAudioWithVolume(void *buf, int64_t start, int64_t count, double volume) const {
+	GetAudio(buf, start, count);
+
+	if (volume == 1.0) return;
+	if (bytes_per_sample != 2)
+		throw agi::InternalError("GetAudioWithVolume called on unconverted audio stream");
+
+	auto buffer = static_cast<int16_t *>(buf);
+	for (size_t i = 0; i < (size_t)count; ++i)
+		buffer[i] = util::mid<int>(-0x8000, buffer[i] * volume + 0.5, 0x7FFF);
+}
+
+void AudioProvider::ZeroFill(void *buf, int64_t count) const {
+	if (bytes_per_sample == 1)
+		// 8 bit formats are usually unsigned with bias 127
+		memset(buf, 127, count * channels);
+	else // While everything else is signed
+		memset(buf, 0, count * bytes_per_sample * channels);
+}
+
+void AudioProvider::GetAudio(void *buf, int64_t start, int64_t count) const {
+	if (start < 0) {
+		ZeroFill(buf, std::min(-start, count));
+		buf = static_cast<char *>(buf) + -start * bytes_per_sample * channels;
+		count += start;
+		start = 0;
+	}
+
+	if (start + count > num_samples) {
+		int64_t zero_count = std::min(count, start + count - num_samples);
+		count -= zero_count;
+		ZeroFill(static_cast<char *>(buf) + count * bytes_per_sample * channels, zero_count);
+	}
+
+	if (count <= 0) return;
+
+	try {
+		FillBuffer(buf, start, count);
+	}
+	catch (AudioDecodeError const& e) {
+		// We don't have any good way to report errors here, so just log the
+		// failure and return silence
+		LOG_E("audio_provider") << e.GetMessage();
+		ZeroFill(buf, count);
+		return;
+	}
+	catch (...) {
+		LOG_E("audio_provider") << "Unknown audio decoding error";
+		ZeroFill(buf, count);
+		return;
+	}
+}
+
+namespace {
+class writer {
+	io::Save outfile;
+	std::ostream& out;
+
+public:
+	writer(agi::fs::path const& filename) : outfile(filename, true), out(outfile.Get()) { }
+
+	template<int N>
+	void write(const char(&str)[N]) {
+		out.write(str, N - 1);
+	}
+
+	void write(std::vector<char> const& data) {
+		out.write(data.data(), data.size());
+	}
+
+	template<typename Dest, typename Src>
+	void write(Src v) {
+		auto converted = static_cast<Dest>(v);
+		out.write(reinterpret_cast<char *>(&converted), sizeof(Dest));
+	}
+};
+}
+
+void SaveAudioClip(AudioProvider *provider, fs::path const& path, int start_time, int end_time) {
+	auto start_sample = ((int64_t)start_time * provider->GetSampleRate() + 999) / 1000;
+	auto end_sample = ((int64_t)end_time * provider->GetSampleRate() + 999) / 1000;
+	if (start_sample >= provider->GetNumSamples() || start_sample >= end_sample) return;
+
+	size_t bytes_per_sample = provider->GetBytesPerSample() * provider->GetChannels();
+	size_t bufsize = (end_sample - start_sample) * bytes_per_sample;
+
+	writer out{path};
+	out.write("RIFF");
+	out.write<int32_t>(bufsize + 36);
+
+	out.write("WAVEfmt ");
+	out.write<int32_t>(16); // Size of chunk
+	out.write<int16_t>(1);  // compression format (PCM)
+	out.write<int16_t>(provider->GetChannels());
+	out.write<int32_t>(provider->GetSampleRate());
+	out.write<int32_t>(provider->GetSampleRate() * provider->GetChannels() * provider->GetBytesPerSample());
+	out.write<int16_t>(provider->GetChannels() * provider->GetBytesPerSample());
+	out.write<int16_t>(provider->GetBytesPerSample() * 8);
+
+	out.write("data");
+	out.write<int32_t>(bufsize);
+
+	// samples per read
+	size_t spr = 65536 / bytes_per_sample;
+	std::vector<char> buf;
+	for (int64_t i = start_sample; i < end_sample; i += spr) {
+		spr = std::min<size_t>(spr, end_sample - i);
+		buf.resize(spr * bytes_per_sample);
+		provider->GetAudio(&buf[0], i, spr);
+		out.write(buf);
+	}
+}
+}
diff --git a/src/audio_provider_convert.cpp b/libaegisub/audio/provider_convert.cpp
similarity index 83%
rename from src/audio_provider_convert.cpp
rename to libaegisub/audio/provider_convert.cpp
index ac4c1af58..a9f6272e8 100644
--- a/src/audio_provider_convert.cpp
+++ b/libaegisub/audio/provider_convert.cpp
@@ -1,4 +1,4 @@
-// Copyright (c) 2011, Thomas Goyne <plorkyeran@aegisub.org>
+// Copyright (c) 2014, Thomas Goyne <plorkyeran@aegisub.org>
 //
 // Permission to use, copy, modify, and distribute this software for any
 // purpose with or without fee is hereby granted, provided that the above
@@ -14,57 +14,55 @@
 //
 // Aegisub Project http://www.aegisub.org/
 
-/// @file audio_provider_convert.cpp
-/// @brief Intermediate sample format-converting audio provider
-/// @ingroup audio_input
-///
-
-#include "include/aegisub/audio_provider.h"
-
-#include "audio_controller.h"
+#include "libaegisub/audio/provider.h"
 
 #include <libaegisub/log.h>
 #include <libaegisub/make_unique.h>
 
 #include <limits>
 
+using namespace agi;
+
 /// Anything integral -> 16 bit signed machine-endian audio converter
+namespace {
 template<class Target>
 class BitdepthConvertAudioProvider final : public AudioProviderWrapper {
 	int src_bytes_per_sample;
 public:
 	BitdepthConvertAudioProvider(std::unique_ptr<AudioProvider> src) : AudioProviderWrapper(std::move(src)) {
 		if (bytes_per_sample > 8)
-			throw agi::AudioProviderOpenError("Audio format converter: audio with bitdepths greater than 64 bits/sample is currently unsupported");
+			throw AudioProviderError("Audio format converter: audio with bitdepths greater than 64 bits/sample is currently unsupported");
 
 		src_bytes_per_sample = bytes_per_sample;
 		bytes_per_sample = sizeof(Target);
 	}
 
 	void FillBuffer(void *buf, int64_t start, int64_t count) const override {
-		std::vector<char> src_buf(count * src_bytes_per_sample * channels);
-		source->GetAudio(&src_buf[0], start, count);
+		std::vector<uint8_t> src_buf(count * src_bytes_per_sample * channels);
+		source->GetAudio(src_buf.data(), start, count);
 
 		int16_t *dest = reinterpret_cast<int16_t*>(buf);
 
 		for (int64_t i = 0; i < count * channels; ++i) {
 			int64_t sample = 0;
-			char *sample_ptr = (char*)&sample;
-			char *src = &src_buf[i * src_bytes_per_sample];
 
 			// 8 bits per sample is assumed to be unsigned with a bias of 127,
 			// while everything else is assumed to be signed with zero bias
 			if (src_bytes_per_sample == 1)
-				*sample_ptr = static_cast<uint8_t>(*src) - 127;
-			else
-				memcpy(sample_ptr, src, src_bytes_per_sample);
+				sample = src_buf[i] - 127;
+			else {
+				for (int j = 0; j < src_bytes_per_sample; ++j) {
+					sample <<= 8;
+					sample += src_buf[i * src_bytes_per_sample + j];
+				}
+			}
 
 			if (static_cast<size_t>(src_bytes_per_sample) > sizeof(Target))
 				sample >>= (src_bytes_per_sample - sizeof(Target)) * 8;
 			else if (static_cast<size_t>(src_bytes_per_sample) < sizeof(Target))
 				sample <<= (sizeof(Target) - src_bytes_per_sample ) * 8;
 
-			dest[i] = (Target)sample;
+			dest[i] = static_cast<Target>(sample);
 		}
 	}
 };
@@ -82,7 +80,7 @@ public:
 		std::vector<Source> src_buf(count * channels);
 		source->GetAudio(&src_buf[0], start, count);
 
-		Target *dest = reinterpret_cast<Target*>(buf);
+		auto dest = reinterpret_cast<Target*>(buf);
 
 		for (size_t i = 0; i < static_cast<size_t>(count * channels); ++i) {
 			Source expanded;
@@ -107,9 +105,9 @@ class DownmixAudioProvider final : public AudioProviderWrapper {
 public:
 	DownmixAudioProvider(std::unique_ptr<AudioProvider> src) : AudioProviderWrapper(std::move(src)) {
 		if (bytes_per_sample != 2)
-			throw agi::InternalError("DownmixAudioProvider requires 16-bit input");
+			throw InternalError("DownmixAudioProvider requires 16-bit input");
 		if (channels == 1)
-			throw agi::InternalError("DownmixAudioProvider requires multi-channel input");
+			throw InternalError("DownmixAudioProvider requires multi-channel input");
 		src_channels = channels;
 		channels = 1;
 	}
@@ -137,9 +135,9 @@ class SampleDoublingAudioProvider final : public AudioProviderWrapper {
 public:
 	SampleDoublingAudioProvider(std::unique_ptr<AudioProvider> src) : AudioProviderWrapper(std::move(src)) {
 		if (source->GetBytesPerSample() != 2)
-			throw agi::InternalError("UpsampleAudioProvider requires 16-bit input");
+			throw InternalError("UpsampleAudioProvider requires 16-bit input");
 		if (source->GetChannels() != 1)
-			throw agi::InternalError("UpsampleAudioProvider requires mono input");
+			throw InternalError("UpsampleAudioProvider requires mono input");
 
 		sample_rate *= 2;
 		num_samples *= 2;
@@ -149,11 +147,11 @@ public:
 	void FillBuffer(void *buf, int64_t start, int64_t count) const override {
 		if (count == 0) return;
 
-		int not_end = start + count < num_samples;
+		bool not_end = start + count < num_samples;
 		int64_t src_count = count / 2;
 		source->GetAudio(buf, start / 2, src_count + not_end);
 
-		int16_t *buf16 = reinterpret_cast<int16_t*>(buf);
+		auto buf16 = reinterpret_cast<int16_t*>(buf);
 
 		if (!not_end) {
 			// We weren't able to request a sample past the end so just
@@ -171,7 +169,9 @@ public:
 		}
 	}
 };
+}
 
+namespace agi {
 std::unique_ptr<AudioProvider> CreateConvertAudioProvider(std::unique_ptr<AudioProvider> provider) {
 	// Ensure 16-bit audio with proper endianness
 	if (provider->AreSamplesFloat()) {
@@ -200,3 +200,4 @@ std::unique_ptr<AudioProvider> CreateConvertAudioProvider(std::unique_ptr<AudioP
 
 	return provider;
 }
+}
diff --git a/libaegisub/audio/provider_dummy.cpp b/libaegisub/audio/provider_dummy.cpp
new file mode 100644
index 000000000..7c11b325c
--- /dev/null
+++ b/libaegisub/audio/provider_dummy.cpp
@@ -0,0 +1,80 @@
+// Copyright (c) 2014, Thomas Goyne <plorkyeran@aegisub.org>
+//
+// Permission to use, copy, modify, and distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+//
+// Aegisub Project http://www.aegisub.org/
+
+#include "libaegisub/audio/provider.h"
+
+#include "libaegisub/fs.h"
+#include "libaegisub/make_unique.h"
+
+#include <boost/algorithm/string/predicate.hpp>
+#include <random>
+
+/*
+ * scheme            ::= "dummy-audio" ":" signal-specifier "?" signal-parameters
+ * signal-specifier  ::= "silence" | "noise" | "sine" "/" frequency
+ * frequency         ::= integer
+ * signal-parameters ::= signal-parameter [ "&" signal-parameters ]
+ * signal-parameter  ::= signal-parameter-name "=" integer
+ * signal-parameter-name ::= "sr" | "bd" | "ch" | "ln"
+ *
+ * Signal types:
+ * "silence", a silent signal is generated.
+ * "noise", a white noise signal is generated.
+ * "sine", a sine wave is generated at the specified frequency.
+ *
+ * Signal parameters:
+ * "sr", sample rate to generate signal at.
+ * "bd", bit depth to generate signal at (usually 16).
+ * "ch", number of channels to generate, usually 1 or 2. The same signal is generated
+ *       in every channel even if one would be LFE.
+ * "ln", length of signal in samples. ln/sr gives signal length in seconds.
+ */
+
+namespace {
+using namespace agi;
+class DummyAudioProvider final : public AudioProvider {
+	bool noise;
+
+	void FillBuffer(void *buf, int64_t start, int64_t count) const override {
+		if (noise) {
+			std::default_random_engine e;
+			std::uniform_int_distribution<int16_t> uniform_dist(-5000, 5000);
+			for (size_t i = 0; i < count; ++i)
+				static_cast<short *>(buf)[i] = uniform_dist(e);
+		}
+		else
+			memset(buf, 0, count * bytes_per_sample);
+	}
+
+public:
+	DummyAudioProvider(agi::fs::path const& uri) {
+		noise = boost::contains(uri.string(), ":noise?");
+		channels = 1;
+		sample_rate = 44100;
+		bytes_per_sample = 2;
+		float_samples = false;
+		decoded_samples = num_samples = (int64_t)5*30*60*1000 * sample_rate / 1000;
+	}
+};
+}
+
+namespace agi {
+std::unique_ptr<AudioProvider> CreateDummyAudioProvider(agi::fs::path const& file, agi::BackgroundRunner *) {
+	if (!boost::starts_with(file.string(), "dummy-audio:"))
+		return {};
+	return agi::make_unique<DummyAudioProvider>(file);
+}
+}
diff --git a/src/audio_provider_hd.cpp b/libaegisub/audio/provider_hd.cpp
similarity index 66%
rename from src/audio_provider_hd.cpp
rename to libaegisub/audio/provider_hd.cpp
index d22c81be9..4dd6234c7 100644
--- a/src/audio_provider_hd.cpp
+++ b/libaegisub/audio/provider_hd.cpp
@@ -14,10 +14,7 @@
 //
 // Aegisub Project http://www.aegisub.org/
 
-#include "include/aegisub/audio_provider.h"
-
-#include "audio_controller.h"
-#include "options.h"
+#include "libaegisub/audio/provider.h"
 
 #include <libaegisub/file_mapping.h>
 #include <libaegisub/format.h>
@@ -27,11 +24,14 @@
 
 #include <boost/filesystem/path.hpp>
 #include <boost/interprocess/detail/os_thread_functions.hpp>
+#include <ctime>
 #include <thread>
 
 namespace {
+using namespace agi;
+
 class HDAudioProvider final : public AudioProviderWrapper {
-	std::unique_ptr<agi::temp_file_mapping> file;
+	mutable temp_file_mapping file;
 	std::atomic<bool> cancelled = {false};
 	std::thread decoder;
 
@@ -45,35 +45,31 @@ class HDAudioProvider final : public AudioProviderWrapper {
 		if (count > 0) {
 			start *= bytes_per_sample;
 			count *= bytes_per_sample;
-			memcpy(buf, file->read(start, count), count);
+			memcpy(buf, file.read(start, count), count);
 		}
 	}
 
+	fs::path CacheFilename(fs::path const& dir) {
+		// Check free space
+		if ((uint64_t)num_samples * bytes_per_sample > fs::FreeSpace(dir))
+			throw AudioProviderError("Not enough free disk space in " + dir.string() + " to cache the audio");
+
+		return format("audio-%lld-%lld", time(nullptr),
+		              boost::interprocess::ipcdetail::get_current_process_id());
+	}
+
 public:
-	HDAudioProvider(std::unique_ptr<AudioProvider> src)
+	HDAudioProvider(std::unique_ptr<AudioProvider> src, agi::fs::path const& dir)
 	: AudioProviderWrapper(std::move(src))
+	, file(dir / CacheFilename(dir), num_samples * bytes_per_sample)
 	{
 		decoded_samples = 0;
-
-		auto path = OPT_GET("Audio/Cache/HD/Location")->GetString();
-		if (path == "default")
-			path = "?temp";
-		auto cache_dir = config::path->MakeAbsolute(config::path->Decode(path), "?temp");
-
-		// Check free space
-		if ((uint64_t)num_samples * bytes_per_sample > agi::fs::FreeSpace(cache_dir))
-			throw agi::AudioCacheOpenError("Not enough free disk space in " + cache_dir.string() + " to cache the audio");
-
-		auto filename = agi::format("audio-%lld-%lld", time(nullptr),
-			boost::interprocess::ipcdetail::get_current_process_id());
-
-		file = agi::make_unique<agi::temp_file_mapping>(cache_dir / filename, num_samples * bytes_per_sample);
 		decoder = std::thread([&] {
 			int64_t block = 65536;
 			for (int64_t i = 0; i < num_samples; i += block) {
 				if (cancelled) break;
 				block = std::min(block, num_samples - i);
-				source->GetAudio(file->write(i * bytes_per_sample, block * bytes_per_sample), i, block);
+				source->GetAudio(file.write(i * bytes_per_sample, block * bytes_per_sample), i, block);
 				decoded_samples += block;
 			}
 		});
@@ -86,6 +82,8 @@ public:
 };
 }
 
-std::unique_ptr<AudioProvider> CreateHDAudioProvider(std::unique_ptr<AudioProvider> src) {
-	return agi::make_unique<HDAudioProvider>(std::move(src));
+namespace agi {
+std::unique_ptr<AudioProvider> CreateHDAudioProvider(std::unique_ptr<AudioProvider> src, agi::fs::path const& dir) {
+	return make_unique<HDAudioProvider>(std::move(src), dir);
+}
 }
diff --git a/src/audio_provider_lock.cpp b/libaegisub/audio/provider_lock.cpp
similarity index 79%
rename from src/audio_provider_lock.cpp
rename to libaegisub/audio/provider_lock.cpp
index 904832898..eb397e410 100644
--- a/src/audio_provider_lock.cpp
+++ b/libaegisub/audio/provider_lock.cpp
@@ -1,4 +1,4 @@
-// Copyright (c) 2012, Thomas Goyne <plorkyeran@aegisub.org>
+// Copyright (c) 2014, Thomas Goyne <plorkyeran@aegisub.org>
 //
 // Permission to use, copy, modify, and distribute this software for any
 // purpose with or without fee is hereby granted, provided that the above
@@ -11,20 +11,17 @@
 // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+//
+// Aegisub Project http://www.aegisub.org/
 
-/// @file audio_provider_lock.cpp
-/// @brief An audio provider adapter for un-threadsafe audio providers
-/// @ingroup audio_input
-
-#include "include/aegisub/audio_provider.h"
+#include "libaegisub/audio/provider.h"
 
 #include <libaegisub/make_unique.h>
 
-#include <memory>
 #include <mutex>
 
 namespace {
-class LockAudioProvider final : public AudioProviderWrapper {
+class LockAudioProvider final : public agi::AudioProviderWrapper {
 	mutable std::mutex mutex;
 
 	void FillBuffer(void *buf, int64_t start, int64_t count) const override {
@@ -40,6 +37,8 @@ public:
 };
 }
 
+namespace agi {
 std::unique_ptr<AudioProvider> CreateLockAudioProvider(std::unique_ptr<AudioProvider> src) {
 	return agi::make_unique<LockAudioProvider>(std::move(src));
 }
+}
diff --git a/libaegisub/audio/provider_pcm.cpp b/libaegisub/audio/provider_pcm.cpp
new file mode 100644
index 000000000..78efbfe89
--- /dev/null
+++ b/libaegisub/audio/provider_pcm.cpp
@@ -0,0 +1,239 @@
+// Copyright (c) 2014, Thomas Goyne <plorkyeran@aegisub.org>
+//
+// Permission to use, copy, modify, and distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+//
+// Aegisub Project http://www.aegisub.org/
+
+#include "libaegisub/audio/provider.h"
+
+#include "libaegisub/file_mapping.h"
+#include "libaegisub/fs.h"
+#include "libaegisub/make_unique.h"
+
+#include <vector>
+
+namespace {
+using namespace agi;
+
+struct IndexPoint {
+	uint64_t start_byte;
+	uint64_t num_samples;
+};
+
+struct file_ended {};
+
+class PCMAudioProvider : public AudioProvider {
+	void FillBuffer(void *buf, int64_t start, int64_t count) const override {
+		auto write_buf = static_cast<char *>(buf);
+		auto bps = bytes_per_sample * channels;
+		uint64_t pos = 0;
+
+		for (auto ip : index_points) {
+			if (count == 0) break;
+			if (pos + ip.num_samples <= (uint64_t)start) {
+				pos += ip.num_samples;
+				continue;
+			}
+
+			auto read_offset = start - pos;
+			auto read_count = std::min<size_t>(count, ip.num_samples - read_offset);
+			auto bytes = read_count * bps;
+			memcpy(write_buf, file.read(ip.start_byte + read_offset * bps, bytes), bytes);
+
+			write_buf += bytes;
+			count -= read_count;
+			start += read_count;
+			pos += ip.num_samples;
+		}
+	}
+
+protected:
+	mutable read_file_mapping file;
+	uint64_t file_pos = 0;
+
+	PCMAudioProvider(fs::path const& filename) : file(filename) { }
+
+	template<typename T, typename UInt>
+	const T *Read(UInt *data_left) {
+		if (*data_left < sizeof(T)) throw file_ended();
+		if (file.size() - file_pos < sizeof(T)) throw file_ended();
+
+		auto data = file.read(file_pos, sizeof(T));
+		file_pos += sizeof(T);
+		*data_left -= sizeof(T);
+		return reinterpret_cast<const T *>(data);
+	}
+
+	std::vector<IndexPoint> index_points;
+};
+
+struct FourCC {
+	std::array<char, 4> data;
+	bool operator!=(const char *cmp) const {
+		return data[0] != cmp[0] || data[1] != cmp[1]
+			|| data[2] != cmp[2] || data[3] != cmp[3];
+	}
+	bool operator==(const char *cmp) const { return !(*this != cmp); }
+};
+
+// Overview of RIFF WAV: <http://www.sonicspot.com/guide/wavefiles.html>
+struct RiffWav {
+	using DataSize = uint32_t;
+	using ChunkId = FourCC;
+
+	static const char *riff_id() { return "RIFF"; }
+	static const char *wave_id() { return "WAVE"; }
+	static const char *fmt_id()  { return "fmt "; }
+	static const char *data_id() { return "data "; }
+
+	static const int alignment = 1;
+
+	static uint32_t data_size(uint32_t size) { return size; }
+	static uint32_t chunk_size(uint32_t size) { return size; }
+};
+
+typedef std::array<uint8_t, 16> GUID;
+
+static const GUID w64GuidRIFF = {{
+	// {66666972-912E-11CF-A5D6-28DB04C10000}
+	0x72, 0x69, 0x66, 0x66, 0x2E, 0x91, 0xCF, 0x11, 0xA5, 0xD6, 0x28, 0xDB, 0x04, 0xC1, 0x00, 0x00
+}};
+
+static const GUID w64GuidWAVE = {{
+	// {65766177-ACF3-11D3-8CD1-00C04F8EDB8A}
+	0x77, 0x61, 0x76, 0x65, 0xF3, 0xAC, 0xD3, 0x11, 0x8C, 0xD1, 0x00, 0xC0, 0x4F, 0x8E, 0xDB, 0x8A
+}};
+
+static const GUID w64Guidfmt = {{
+	// {20746D66-ACF3-11D3-8CD1-00C04F8EDB8A}
+	0x66, 0x6D, 0x74, 0x20, 0xF3, 0xAC, 0xD3, 0x11, 0x8C, 0xD1, 0x00, 0xC0, 0x4F, 0x8E, 0xDB, 0x8A
+}};
+
+static const GUID w64Guiddata = {{
+	// {61746164-ACF3-11D3-8CD1-00C04F8EDB8A}
+	0x64, 0x61, 0x74, 0x61, 0xF3, 0xAC, 0xD3, 0x11, 0x8C, 0xD1, 0x00, 0xC0, 0x4F, 0x8E, 0xDB, 0x8A
+}};
+
+// http://www.vcs.de/fileadmin/user_upload/MBS/PDF/Whitepaper/Informations_about_Sony_Wave64.pdf
+struct Wave64 {
+	using DataSize = uint64_t;
+	using ChunkId = GUID;
+
+	static GUID riff_id() { return w64GuidRIFF; }
+	static GUID wave_id() { return w64GuidWAVE; }
+	static GUID fmt_id()  { return w64Guidfmt; }
+	static GUID data_id() { return w64Guiddata; }
+
+	static const uint64_t alignment = 7ULL;
+
+	// Wave 64 includes the size of the header in the chunk sizes
+	static uint64_t data_size(uint64_t size) { return size - 16; }
+	static uint64_t chunk_size(uint64_t size) { return size - 24; }
+};
+
+template<typename Impl>
+class WavPCMAudioProvider : public PCMAudioProvider {
+public:
+	WavPCMAudioProvider(fs::path const& filename)
+	: PCMAudioProvider(filename)
+	{
+		using DataSize = typename Impl::DataSize;
+		using ChunkId = typename Impl::ChunkId;
+
+		try {
+			auto data_left = std::numeric_limits<DataSize>::max();
+			if (*Read<ChunkId>(&data_left) != Impl::riff_id())
+				throw AudioDataNotFound("File is not a RIFF file");
+
+			data_left = Impl::data_size(*Read<DataSize>(&data_left));
+
+			if (*Read<ChunkId>(&data_left) != Impl::wave_id())
+				throw AudioDataNotFound("File is not a RIFF WAV file");
+
+			while (data_left) {
+				auto chunk_fcc = *Read<ChunkId>(&data_left);
+				auto chunk_size = Impl::chunk_size(*Read<DataSize>(&data_left));
+
+				data_left -= chunk_size;
+
+				if (chunk_fcc == Impl::fmt_id()) {
+					if (channels || sample_rate || bytes_per_sample)
+						throw AudioProviderError("Multiple 'fmt ' chunks not supported");
+
+					auto compression = *Read<uint16_t>(&chunk_size);
+					if (compression != 1)
+						throw AudioProviderError("File is not uncompressed PCM");
+
+					channels = *Read<uint16_t>(&chunk_size);
+					sample_rate = *Read<uint32_t>(&chunk_size);
+					Read<uint32_t>(&chunk_size); // Average bytes per sample; meaningless
+					Read<uint16_t>(&chunk_size); // Block align
+					bytes_per_sample = (*Read<uint16_t>(&chunk_size) + 7) / 8;
+				}
+				else if (chunk_fcc == Impl::data_id()) {
+					if (!channels || !sample_rate || !bytes_per_sample)
+						throw AudioProviderError("Found 'data' chunk without format being set.");
+					index_points.emplace_back(IndexPoint{file_pos, chunk_size / bytes_per_sample / channels});
+					num_samples += chunk_size / bytes_per_sample / channels;
+				}
+				// There's a bunch of other chunk types. They're all dumb.
+
+				// blocks are aligned and the padding bytes are not included in
+				// the size of the chunk
+				file_pos += (chunk_size + Impl::alignment) & ~Impl::alignment;
+			}
+
+		}
+		catch (file_ended) {
+			if (!channels || !sample_rate || !bytes_per_sample)
+				throw AudioDataNotFound("File ended before reaching format chunk");
+			// Truncated files are fine otherwise
+		}
+		decoded_samples = num_samples;
+	}
+};
+}
+
+namespace agi {
+std::unique_ptr<AudioProvider> CreatePCMAudioProvider(fs::path const& filename, BackgroundRunner *) {
+	bool wrong_file_type = true;
+	std::string msg;
+
+	try {
+		return make_unique<WavPCMAudioProvider<RiffWav>>(filename);
+	}
+	catch (AudioDataNotFound const& err) {
+		msg = "RIFF PCM WAV audio provider: " + err.GetMessage();
+	}
+	catch (AudioProviderError const& err) {
+		wrong_file_type = false;
+		msg = "RIFF PCM WAV audio provider: " + err.GetMessage();
+	}
+
+	try {
+		return make_unique<WavPCMAudioProvider<Wave64>>(filename);
+	}
+	catch (AudioDataNotFound const& err) {
+		msg += "\nWave64 audio provider: " + err.GetMessage();
+	}
+	catch (AudioProviderError const& err) {
+		wrong_file_type = false;
+		msg += "\nWave64 audio provider: " + err.GetMessage();
+	}
+
+	if (wrong_file_type)
+		throw AudioDataNotFound(msg);
+	else
+		throw AudioProviderError(msg);
+}
+}
diff --git a/src/audio_provider_ram.cpp b/libaegisub/audio/provider_ram.cpp
similarity index 51%
rename from src/audio_provider_ram.cpp
rename to libaegisub/audio/provider_ram.cpp
index 1a3a4e174..f177840b5 100644
--- a/src/audio_provider_ram.cpp
+++ b/libaegisub/audio/provider_ram.cpp
@@ -1,48 +1,29 @@
-// Copyright (c) 2005-2006, Rodrigo Braz Monteiro, Fredrik Mellbin
-// All rights reserved.
+// Copyright (c) 2014, Thomas Goyne <plorkyeran@aegisub.org>
 //
-// Redistribution and use in source and binary forms, with or without
-// modification, are permitted provided that the following conditions are met:
+// Permission to use, copy, modify, and distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
 //
-//   * Redistributions of source code must retain the above copyright notice,
-//     this list of conditions and the following disclaimer.
-//   * Redistributions in binary form must reproduce the above copyright notice,
-//     this list of conditions and the following disclaimer in the documentation
-//     and/or other materials provided with the distribution.
-//   * Neither the name of the Aegisub Group nor the names of its contributors
-//     may be used to endorse or promote products derived from this software
-//     without specific prior written permission.
-//
-// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
-// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
-// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
-// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
-// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
-// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
-// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
-// POSSIBILITY OF SUCH DAMAGE.
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 //
 // Aegisub Project http://www.aegisub.org/
 
-/// @file audio_provider_ram.cpp
-/// @brief Caching audio provider using heap memory for backing
-/// @ingroup audio_input
-///
-
-#include "include/aegisub/audio_provider.h"
+#include "libaegisub/audio/provider.h"
 
-#include "audio_controller.h"
-
-#include <libaegisub/make_unique.h>
+#include "libaegisub/make_unique.h"
 
 #include <array>
 #include <boost/container/stable_vector.hpp>
 #include <thread>
 
 namespace {
+using namespace agi;
 
 #define CacheBits 22
 #define CacheBlockSize (1 << CacheBits)
@@ -68,7 +49,7 @@ public:
 			blockcache.resize((source->GetNumSamples() * source->GetBytesPerSample() + CacheBlockSize - 1) >> CacheBits);
 		}
 		catch (std::bad_alloc const&) {
-			throw agi::AudioCacheOpenError("Couldn't open audio, not enough ram available.");
+			throw AudioProviderError("Not enough memory available to cache in RAM");
 		}
 
 		decoder = std::thread([&] {
@@ -108,6 +89,8 @@ void RAMAudioProvider::FillBuffer(void *buf, int64_t start, int64_t count) const
 }
 }
 
+namespace agi {
 std::unique_ptr<AudioProvider> CreateRAMAudioProvider(std::unique_ptr<AudioProvider> src) {
-	return agi::make_unique<RAMAudioProvider>(std::move(src));
+	return make_unique<RAMAudioProvider>(std::move(src));
+}
 }
diff --git a/libaegisub/include/libaegisub/audio/provider.h b/libaegisub/include/libaegisub/audio/provider.h
new file mode 100644
index 000000000..9e631bc40
--- /dev/null
+++ b/libaegisub/include/libaegisub/audio/provider.h
@@ -0,0 +1,95 @@
+// Copyright (c) 2014, Thomas Goyne <plorkyeran@aegisub.org>
+//
+// Permission to use, copy, modify, and distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+//
+// Aegisub Project http://www.aegisub.org/
+
+#pragma once
+
+#include <libaegisub/exception.h>
+#include <libaegisub/fs_fwd.h>
+
+#include <atomic>
+#include <vector>
+
+namespace agi {
+class AudioProvider {
+protected:
+	int channels = 0;
+	/// Total number of samples per channel
+	int64_t num_samples = 0;
+	/// Samples per channel which have been decoded and can be fetched with FillBuffer
+	/// Only applicable for the cache providers
+	std::atomic<int64_t> decoded_samples{0};
+	int sample_rate = 0;
+	int bytes_per_sample = 0;
+	bool float_samples = false;
+
+	virtual void FillBuffer(void *buf, int64_t start, int64_t count) const = 0;
+
+	void ZeroFill(void *buf, int64_t count) const;
+
+public:
+	virtual ~AudioProvider() = default;
+
+	void GetAudio(void *buf, int64_t start, int64_t count) const;
+	void GetAudioWithVolume(void *buf, int64_t start, int64_t count, double volume) const;
+
+	int64_t GetNumSamples()     const { return num_samples; }
+	int64_t GetDecodedSamples() const { return decoded_samples; }
+	int     GetSampleRate()     const { return sample_rate; }
+	int     GetBytesPerSample() const { return bytes_per_sample; }
+	int     GetChannels()       const { return channels; }
+	bool    AreSamplesFloat()   const { return float_samples; }
+
+	/// Does this provider benefit from external caching?
+	virtual bool NeedsCache() const { return false; }
+};
+
+/// Helper base class for an audio provider which wraps another provider
+class AudioProviderWrapper : public AudioProvider {
+protected:
+	std::unique_ptr<AudioProvider> source;
+public:
+	AudioProviderWrapper(std::unique_ptr<AudioProvider> src)
+	: source(std::move(src))
+	{
+		channels = source->GetChannels();
+		num_samples = source->GetNumSamples();
+		decoded_samples = source->GetDecodedSamples();
+		sample_rate = source->GetSampleRate();
+		bytes_per_sample = source->GetBytesPerSample();
+		float_samples = source->AreSamplesFloat();
+	}
+};
+
+DEFINE_EXCEPTION(AudioProviderError, Exception);
+
+/// Error of some sort occurred while decoding a frame
+DEFINE_EXCEPTION(AudioDecodeError, AudioProviderError);
+
+/// This provider could not find any audio data in the file
+DEFINE_EXCEPTION(AudioDataNotFound, AudioProviderError);
+
+class BackgroundRunner;
+
+std::unique_ptr<AudioProvider> CreateDummyAudioProvider(fs::path const& filename, BackgroundRunner *);
+std::unique_ptr<AudioProvider> CreatePCMAudioProvider(fs::path const& filename, BackgroundRunner *);
+
+std::unique_ptr<AudioProvider> CreateConvertAudioProvider(std::unique_ptr<AudioProvider> source_provider);
+std::unique_ptr<AudioProvider> CreateLockAudioProvider(std::unique_ptr<AudioProvider> source_provider);
+std::unique_ptr<AudioProvider> CreateHDAudioProvider(std::unique_ptr<AudioProvider> source_provider, fs::path const& dir);
+std::unique_ptr<AudioProvider> CreateRAMAudioProvider(std::unique_ptr<AudioProvider> source_provider);
+
+void SaveAudioClip(AudioProvider *provider, fs::path const& path, int start_time, int end_time);
+}
diff --git a/src/Makefile b/src/Makefile
index 76bf2127d..60d71015f 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -36,13 +36,7 @@ src_OBJ := \
 	$(d)audio_karaoke.o \
 	$(d)audio_marker.o \
 	$(d)audio_player.o \
-	$(d)audio_provider.o \
-	$(d)audio_provider_convert.o \
-	$(d)audio_provider_dummy.o \
-	$(d)audio_provider_hd.o \
-	$(d)audio_provider_lock.o \
-	$(d)audio_provider_pcm.o \
-	$(d)audio_provider_ram.o \
+	$(d)audio_provider_factory.o \
 	$(d)audio_renderer.o \
 	$(d)audio_renderer_spectrum.o \
 	$(d)audio_renderer_waveform.o \
@@ -190,7 +184,7 @@ endif
 #####################
 $(d)MatroskaParser.o_FLAGS              := -Wno-sometimes-uninitialized
 $(d)audio_player.o_FLAGS                := $(CFLAGS_ALSA) $(CFLAGS_PORTAUDIO) $(CFLAGS_LIBPULSE) $(CFLAGS_OPENAL)
-$(d)audio_provider.o_FLAGS              := $(CFLAGS_FFMS2)
+$(d)audio_provider_factory.o_FLAGS      := $(CFLAGS_FFMS2)
 $(d)auto4_base.o_FLAGS                  := $(CFLAGS_FREETYPE)
 $(d)charset_detect.o_FLAGS              := -D_X86_
 $(d)font_file_lister_fontconfig.o_FLAGS := $(CFLAGS_FONTCONFIG)
diff --git a/src/audio_controller.cpp b/src/audio_controller.cpp
index af7964531..4de1928d1 100644
--- a/src/audio_controller.cpp
+++ b/src/audio_controller.cpp
@@ -31,11 +31,12 @@
 
 #include "audio_timing.h"
 #include "include/aegisub/audio_player.h"
-#include "include/aegisub/audio_provider.h"
 #include "include/aegisub/context.h"
 #include "options.h"
 #include "project.h"
 
+#include <libaegisub/audio/provider.h>
+
 #include <algorithm>
 
 AudioController::AudioController(agi::Context *context)
@@ -102,12 +103,13 @@ void AudioController::OnAudioPlayerChanged()
 	}
 	catch (...)
 	{
+		/// @todo This really shouldn't be just swallowing all audio player open errors
 		context->project->CloseAudio();
 	}
 	AnnounceAudioPlayerOpened();
 }
 
-void AudioController::OnAudioProvider(AudioProvider *new_provider)
+void AudioController::OnAudioProvider(agi::AudioProvider *new_provider)
 {
 	provider = new_provider;
 	Stop();
diff --git a/src/audio_controller.h b/src/audio_controller.h
index 2f4c1bbed..6bc192762 100644
--- a/src/audio_controller.h
+++ b/src/audio_controller.h
@@ -36,10 +36,10 @@
 #include <wx/timer.h>
 
 class AudioPlayer;
-class AudioProvider;
 class AudioTimingController;
-namespace agi { struct Context; }
 class TimeRange;
+namespace agi { class AudioProvider; }
+namespace agi { struct Context; }
 
 /// @class AudioController
 /// @brief Manage playback of an open audio stream
@@ -84,10 +84,10 @@ class AudioController final : public wxEvtHandler {
 	wxTimer playback_timer;
 
 	/// The audio provider
-	AudioProvider *provider = nullptr;
+	agi::AudioProvider *provider = nullptr;
 	agi::signal::Connection provider_connection;
 
-	void OnAudioProvider(AudioProvider *new_provider);
+	void OnAudioProvider(agi::AudioProvider *new_provider);
 
 	/// Event handler for the playback timer
 	void OnPlaybackTimer(wxTimerEvent &event);
@@ -189,29 +189,3 @@ public:
 	DEFINE_SIGNAL_ADDERS(AnnounceTimingControllerChanged, AddTimingControllerListener)
 	DEFINE_SIGNAL_ADDERS(AnnounceAudioPlayerOpened,       AddAudioPlayerOpenListener)
 };
-
-namespace agi {
-	/// Base class for all audio-related errors
-	DEFINE_EXCEPTION(AudioError, Exception);
-
-	/// Opening the audio failed for any reason
-	DEFINE_EXCEPTION(AudioOpenError, AudioError);
-
-	/// There are no audio providers available to open audio files
-	DEFINE_EXCEPTION(NoAudioProvidersError, AudioOpenError);
-
-	/// The file exists, but no providers could find any audio tracks in it
-	DEFINE_EXCEPTION(AudioDataNotFoundError, AudioOpenError);
-
-	/// There are audio tracks, but no provider could actually read them
-	DEFINE_EXCEPTION(AudioProviderOpenError, AudioOpenError);
-
-	/// The audio cache failed to initialize
-	DEFINE_EXCEPTION(AudioCacheOpenError, AudioOpenError);
-
-	/// There are no audio players available
-	DEFINE_EXCEPTION(NoAudioPlayersError, AudioOpenError);
-
-	/// The audio player failed to initialize
-	DEFINE_EXCEPTION(AudioPlayerOpenError, AudioOpenError);
-}
diff --git a/src/audio_display.cpp b/src/audio_display.cpp
index dfb6cb9ad..d8c6fb56c 100644
--- a/src/audio_display.cpp
+++ b/src/audio_display.cpp
@@ -37,7 +37,6 @@
 #include "audio_timing.h"
 #include "compat.h"
 #include "format.h"
-#include "include/aegisub/audio_provider.h"
 #include "include/aegisub/context.h"
 #include "include/aegisub/hotkey.h"
 #include "options.h"
@@ -46,6 +45,7 @@
 #include "video_controller.h"
 
 #include <libaegisub/ass/time.h>
+#include <libaegisub/audio/provider.h>
 #include <libaegisub/make_unique.h>
 
 #include <algorithm>
@@ -1171,7 +1171,7 @@ int AudioDisplay::GetDuration() const
 	return (provider->GetNumSamples() * 1000 + provider->GetSampleRate() - 1) / provider->GetSampleRate();
 }
 
-void AudioDisplay::OnAudioOpen(AudioProvider *provider)
+void AudioDisplay::OnAudioOpen(agi::AudioProvider *provider)
 {
 	this->provider = provider;
 
diff --git a/src/audio_display.h b/src/audio_display.h
index faaf403a9..db607bcdd 100644
--- a/src/audio_display.h
+++ b/src/audio_display.h
@@ -40,12 +40,12 @@
 #include <wx/timer.h>
 #include <wx/window.h>
 
+namespace agi { class AudioProvider; }
 namespace agi { struct Context; }
 
 class AudioController;
 class AudioRenderer;
 class AudioRendererBitmapProvider;
-class AudioProvider;
 class TimeRange;
 
 // Helper classes used in implementation of the audio display
@@ -107,7 +107,7 @@ class AudioDisplay: public wxWindow {
 	/// The controller managing us
 	AudioController *controller = nullptr;
 
-	AudioProvider *provider = nullptr;
+	agi::AudioProvider *provider = nullptr;
 
 	/// Scrollbar helper object
 	std::unique_ptr<AudioDisplayScrollbar> scrollbar;
@@ -230,7 +230,7 @@ class AudioDisplay: public wxWindow {
 
 	int GetDuration() const;
 
-	void OnAudioOpen(AudioProvider *provider);
+	void OnAudioOpen(agi::AudioProvider *provider);
 	void OnPlaybackPosition(int ms_position);
 	void OnSelectionChanged();
 	void OnStyleRangesChanged();
diff --git a/src/audio_karaoke.cpp b/src/audio_karaoke.cpp
index 4f42bc970..60dd4c390 100644
--- a/src/audio_karaoke.cpp
+++ b/src/audio_karaoke.cpp
@@ -120,7 +120,7 @@ void AudioKaraoke::OnFileChanged(int type, std::set<const AssDialogue *> const&
 	}
 }
 
-void AudioKaraoke::OnAudioOpened(AudioProvider *provider) {
+void AudioKaraoke::OnAudioOpened(agi::AudioProvider *provider) {
 	if (provider)
 		SetEnabled(enabled);
 	else
diff --git a/src/audio_karaoke.h b/src/audio_karaoke.h
index 0cc7c3b39..e798bfc2b 100644
--- a/src/audio_karaoke.h
+++ b/src/audio_karaoke.h
@@ -26,8 +26,8 @@
 
 class AssDialogue;
 class AssKaraoke;
-class AudioProvider;
 class wxButton;
+namespace agi { class AudioProvider; }
 namespace agi { struct Context; }
 
 /// @class AudioKaraoke
@@ -141,7 +141,7 @@ class AudioKaraoke final : public wxWindow {
 	void OnMouse(wxMouseEvent &event);
 	void OnPaint(wxPaintEvent &event);
 	void OnSize(wxSizeEvent &event);
-	void OnAudioOpened(AudioProvider *provider);
+	void OnAudioOpened(agi::AudioProvider *provider);
 	void OnScrollTimer(wxTimerEvent &event);
 
 public:
diff --git a/src/audio_player.cpp b/src/audio_player.cpp
index c4500bdff..f5a8327ca 100644
--- a/src/audio_player.cpp
+++ b/src/audio_player.cpp
@@ -40,23 +40,18 @@
 
 #include <boost/range/iterator_range.hpp>
 
-AudioPlayer::AudioPlayer(AudioProvider *provider)
-: provider(provider)
-{
-}
-
-std::unique_ptr<AudioPlayer> CreateAlsaPlayer(AudioProvider *providers, wxWindow *window);
-std::unique_ptr<AudioPlayer> CreateDirectSoundPlayer(AudioProvider *providers, wxWindow *window);
-std::unique_ptr<AudioPlayer> CreateDirectSound2Player(AudioProvider *providers, wxWindow *window);
-std::unique_ptr<AudioPlayer> CreateOpenALPlayer(AudioProvider *providers, wxWindow *window);
-std::unique_ptr<AudioPlayer> CreatePortAudioPlayer(AudioProvider *providers, wxWindow *window);
-std::unique_ptr<AudioPlayer> CreatePulseAudioPlayer(AudioProvider *providers, wxWindow *window);
-std::unique_ptr<AudioPlayer> CreateOSSPlayer(AudioProvider *providers, wxWindow *window);
+std::unique_ptr<AudioPlayer> CreateAlsaPlayer(agi::AudioProvider *providers, wxWindow *window);
+std::unique_ptr<AudioPlayer> CreateDirectSoundPlayer(agi::AudioProvider *providers, wxWindow *window);
+std::unique_ptr<AudioPlayer> CreateDirectSound2Player(agi::AudioProvider *providers, wxWindow *window);
+std::unique_ptr<AudioPlayer> CreateOpenALPlayer(agi::AudioProvider *providers, wxWindow *window);
+std::unique_ptr<AudioPlayer> CreatePortAudioPlayer(agi::AudioProvider *providers, wxWindow *window);
+std::unique_ptr<AudioPlayer> CreatePulseAudioPlayer(agi::AudioProvider *providers, wxWindow *window);
+std::unique_ptr<AudioPlayer> CreateOSSPlayer(agi::AudioProvider *providers, wxWindow *window);
 
 namespace {
 	struct factory {
 		const char *name;
-		std::unique_ptr<AudioPlayer> (*create)(AudioProvider *, wxWindow *window);
+		std::unique_ptr<AudioPlayer> (*create)(agi::AudioProvider *, wxWindow *window);
 		bool hidden;
 	};
 
@@ -87,9 +82,9 @@ std::vector<std::string> AudioPlayerFactory::GetClasses() {
 	return ::GetClasses(boost::make_iterator_range(std::begin(factories), std::end(factories)));
 }
 
-std::unique_ptr<AudioPlayer> AudioPlayerFactory::GetAudioPlayer(AudioProvider *provider, wxWindow *window) {
+std::unique_ptr<AudioPlayer> AudioPlayerFactory::GetAudioPlayer(agi::AudioProvider *provider, wxWindow *window) {
 	if (std::begin(factories) == std::end(factories))
-		throw agi::NoAudioPlayersError("No audio players are available.");
+		throw AudioPlayerOpenError("No audio players are available.");
 
 	auto preferred = OPT_GET("Audio/Player")->GetString();
 	auto sorted = GetSorted(boost::make_iterator_range(std::begin(factories), std::end(factories)), preferred);
@@ -99,9 +94,9 @@ std::unique_ptr<AudioPlayer> AudioPlayerFactory::GetAudioPlayer(AudioProvider *p
 		try {
 			return factory->create(provider, window);
 		}
-		catch (agi::AudioPlayerOpenError const& err) {
+		catch (AudioPlayerOpenError const& err) {
 			error += std::string(factory->name) + " factory: " + err.GetMessage() + "\n";
 		}
 	}
-	throw agi::AudioPlayerOpenError(error);
+	throw AudioPlayerOpenError(error);
 }
diff --git a/src/audio_player_alsa.cpp b/src/audio_player_alsa.cpp
index fcd171242..5a1705622 100644
--- a/src/audio_player_alsa.cpp
+++ b/src/audio_player_alsa.cpp
@@ -36,11 +36,11 @@
 #include "include/aegisub/audio_player.h"
 
 #include "audio_controller.h"
-#include "include/aegisub/audio_provider.h"
 #include "compat.h"
 #include "frame_main.h"
 #include "options.h"
 
+#include <libaegisub/audio/provider.h>
 #include <libaegisub/log.h>
 #include <libaegisub/make_unique.h>
 
@@ -102,7 +102,7 @@ class AlsaPlayer final : public AudioPlayer {
 	}
 
 public:
-	AlsaPlayer(AudioProvider *provider);
+	AlsaPlayer(agi::AudioProvider *provider);
 	~AlsaPlayer();
 
 	void Play(int64_t start, int64_t count) override;
@@ -289,13 +289,13 @@ do_setup:
 	}
 }
 
-AlsaPlayer::AlsaPlayer(AudioProvider *provider) try
+AlsaPlayer::AlsaPlayer(agi::AudioProvider *provider) try
 : AudioPlayer(provider)
 , thread(&AlsaPlayer::PlaybackThread, this)
 {
 }
 catch (std::system_error const&) {
-	throw agi::AudioPlayerOpenError("AlsaPlayer: Creating the playback thread failed");
+	throw AudioPlayerOpenError("AlsaPlayer: Creating the playback thread failed");
 }
 
 AlsaPlayer::~AlsaPlayer()
@@ -347,7 +347,7 @@ int64_t AlsaPlayer::GetCurrentPosition()
 }
 }
 
-std::unique_ptr<AudioPlayer> CreateAlsaPlayer(AudioProvider *provider, wxWindow *)
+std::unique_ptr<AudioPlayer> CreateAlsaPlayer(agi::AudioProvider *provider, wxWindow *)
 {
 	return agi::make_unique<AlsaPlayer>(provider);
 }
diff --git a/src/audio_player_dsound.cpp b/src/audio_player_dsound.cpp
index 06ed2ca5d..01b47b354 100644
--- a/src/audio_player_dsound.cpp
+++ b/src/audio_player_dsound.cpp
@@ -36,10 +36,10 @@
 #include "include/aegisub/audio_player.h"
 
 #include "audio_controller.h"
-#include "include/aegisub/audio_provider.h"
 #include "frame_main.h"
 #include "utils.h"
 
+#include <libaegisub/audio/provider.h>
 #include <libaegisub/log.h>
 #include <libaegisub/make_unique.h>
 
@@ -81,7 +81,7 @@ class DirectSoundPlayer final : public AudioPlayer {
 	DirectSoundPlayerThread *thread = nullptr;
 
 public:
-	DirectSoundPlayer(AudioProvider *provider, wxWindow *parent);
+	DirectSoundPlayer(agi::AudioProvider *provider, wxWindow *parent);
 	~DirectSoundPlayer();
 
 	void Play(int64_t start,int64_t count);
@@ -96,13 +96,13 @@ public:
 	void SetVolume(double vol) { volume = vol; }
 };
 
-DirectSoundPlayer::DirectSoundPlayer(AudioProvider *provider, wxWindow *parent)
+DirectSoundPlayer::DirectSoundPlayer(agi::AudioProvider *provider, wxWindow *parent)
 : AudioPlayer(provider)
 {
 	// Initialize the DirectSound object
 	HRESULT res;
 	res = DirectSoundCreate8(&DSDEVID_DefaultPlayback,&directSound,nullptr); // TODO: support selecting audio device
-	if (FAILED(res)) throw agi::AudioPlayerOpenError("Failed initializing DirectSound");
+	if (FAILED(res)) throw AudioPlayerOpenError("Failed initializing DirectSound");
 
 	// Set DirectSound parameters
 	directSound->SetCooperativeLevel((HWND)parent->GetHandle(),DSSCL_PRIORITY);
@@ -133,11 +133,11 @@ DirectSoundPlayer::DirectSoundPlayer(AudioProvider *provider, wxWindow *parent)
 	// Create the buffer
 	IDirectSoundBuffer *buf;
 	res = directSound->CreateSoundBuffer(&desc,&buf,nullptr);
-	if (res != DS_OK) throw agi::AudioPlayerOpenError("Failed creating DirectSound buffer");
+	if (res != DS_OK) throw AudioPlayerOpenError("Failed creating DirectSound buffer");
 
 	// Copy interface to buffer
 	res = buf->QueryInterface(IID_IDirectSoundBuffer8,(LPVOID*) &buffer);
-	if (res != S_OK) throw agi::AudioPlayerOpenError("Failed casting interface to IDirectSoundBuffer8");
+	if (res != S_OK) throw AudioPlayerOpenError("Failed casting interface to IDirectSoundBuffer8");
 
 	// Set data
 	offset = 0;
@@ -367,7 +367,7 @@ void DirectSoundPlayerThread::Stop() {
 }
 }
 
-std::unique_ptr<AudioPlayer> CreateDirectSoundPlayer(AudioProvider *provider, wxWindow *parent) {
+std::unique_ptr<AudioPlayer> CreateDirectSoundPlayer(agi::AudioProvider *provider, wxWindow *parent) {
 	return agi::make_unique<DirectSoundPlayer>(provider, parent);
 }
 
diff --git a/src/audio_player_dsound2.cpp b/src/audio_player_dsound2.cpp
index 1e8081dba..1f937d34b 100644
--- a/src/audio_player_dsound2.cpp
+++ b/src/audio_player_dsound2.cpp
@@ -36,11 +36,11 @@
 #include "include/aegisub/audio_player.h"
 
 #include "audio_controller.h"
-#include "include/aegisub/audio_provider.h"
 #include "frame_main.h"
 #include "options.h"
 #include "utils.h"
 
+#include <libaegisub/audio/provider.h>
 #include <libaegisub/scoped_ptr.h>
 #include <libaegisub/log.h>
 #include <libaegisub/make_unique.h>
@@ -74,7 +74,7 @@ class DirectSoundPlayer2 final : public AudioPlayer {
 
 public:
 	/// @brief Constructor
-	DirectSoundPlayer2(AudioProvider *provider, wxWindow *parent);
+	DirectSoundPlayer2(agi::AudioProvider *provider, wxWindow *parent);
 	/// @brief Destructor
 	~DirectSoundPlayer2();
 
@@ -236,14 +236,14 @@ class DirectSoundPlayer2Thread {
 	DWORD last_playback_restart;
 
 	/// Audio provider to take sample data from
-	AudioProvider *provider;
+	agi::AudioProvider *provider;
 
 public:
 	/// @brief Constructor, creates and starts playback thread
 	/// @param provider       Audio provider to take sample data from
 	/// @param WantedLatency Desired length in milliseconds to write ahead of the playback cursor
 	/// @param BufferLength  Multiplier for WantedLatency to get total buffer length
-	DirectSoundPlayer2Thread(AudioProvider *provider, int WantedLatency, int BufferLength, wxWindow *parent);
+	DirectSoundPlayer2Thread(agi::AudioProvider *provider, int WantedLatency, int BufferLength, wxWindow *parent);
 	/// @brief Destructor, waits for thread to have died
 	~DirectSoundPlayer2Thread();
 
@@ -660,7 +660,7 @@ void DirectSoundPlayer2Thread::CheckError()
 	}
 }
 
-DirectSoundPlayer2Thread::DirectSoundPlayer2Thread(AudioProvider *provider, int WantedLatency, int BufferLength, wxWindow *parent)
+DirectSoundPlayer2Thread::DirectSoundPlayer2Thread(agi::AudioProvider *provider, int WantedLatency, int BufferLength, wxWindow *parent)
 : parent((HWND)parent->GetHandle())
 , event_start_playback  (CreateEvent(0, FALSE, FALSE, 0))
 , event_stop_playback   (CreateEvent(0, FALSE, FALSE, 0))
@@ -677,7 +677,7 @@ DirectSoundPlayer2Thread::DirectSoundPlayer2Thread(AudioProvider *provider, int
 	thread_handle = (HANDLE)_beginthreadex(0, 0, ThreadProc, this, 0, 0);
 
 	if (!thread_handle)
-		throw agi::AudioPlayerOpenError("Failed creating playback thread in DirectSoundPlayer2. This is bad.");
+		throw AudioPlayerOpenError("Failed creating playback thread in DirectSoundPlayer2. This is bad.");
 
 	HANDLE running_or_error[] = { thread_running, error_happened };
 	switch (WaitForMultipleObjects(2, running_or_error, FALSE, INFINITE))
@@ -688,10 +688,10 @@ DirectSoundPlayer2Thread::DirectSoundPlayer2Thread(AudioProvider *provider, int
 
 	case WAIT_OBJECT_0 + 1:
 		// error happened, we fail
-		throw agi::AudioPlayerOpenError(error_message);
+		throw AudioPlayerOpenError(error_message);
 
 	default:
-		throw agi::AudioPlayerOpenError("Failed wait for thread start or thread error in DirectSoundPlayer2. This is bad.");
+		throw AudioPlayerOpenError("Failed wait for thread start or thread error in DirectSoundPlayer2. This is bad.");
 	}
 }
 
@@ -800,7 +800,7 @@ bool DirectSoundPlayer2Thread::IsDead()
 	}
 }
 
-DirectSoundPlayer2::DirectSoundPlayer2(AudioProvider *provider, wxWindow *parent)
+DirectSoundPlayer2::DirectSoundPlayer2(agi::AudioProvider *provider, wxWindow *parent)
 : AudioPlayer(provider)
 {
 	// The buffer will hold BufferLength times WantedLatency milliseconds of audio
@@ -820,7 +820,7 @@ DirectSoundPlayer2::DirectSoundPlayer2(AudioProvider *provider, wxWindow *parent
 	catch (const char *msg)
 	{
 		LOG_E("audio/player/dsound") << msg;
-		throw agi::AudioPlayerOpenError(msg);
+		throw AudioPlayerOpenError(msg);
 	}
 }
 
@@ -929,7 +929,7 @@ void DirectSoundPlayer2::SetVolume(double vol)
 }
 }
 
-std::unique_ptr<AudioPlayer> CreateDirectSound2Player(AudioProvider *provider, wxWindow *parent) {
+std::unique_ptr<AudioPlayer> CreateDirectSound2Player(agi::AudioProvider *provider, wxWindow *parent) {
 	return agi::make_unique<DirectSoundPlayer2>(provider, parent);
 }
 
diff --git a/src/audio_player_openal.cpp b/src/audio_player_openal.cpp
index 0b3b1ee7f..302878e25 100644
--- a/src/audio_player_openal.cpp
+++ b/src/audio_player_openal.cpp
@@ -36,9 +36,9 @@
 #include "include/aegisub/audio_player.h"
 
 #include "audio_controller.h"
-#include "include/aegisub/audio_provider.h"
 #include "utils.h"
 
+#include <libaegisub/audio/provider.h>
 #include <libaegisub/log.h>
 #include <libaegisub/make_unique.h>
 
@@ -61,8 +61,6 @@
 #pragma comment(lib, "openal32.lib")
 #endif
 
-DEFINE_EXCEPTION(OpenALException, agi::AudioPlayerOpenError);
-
 namespace {
 class OpenALPlayer final : public AudioPlayer, wxTimer {
 	/// Number of OpenAL buffers to use
@@ -108,7 +106,7 @@ protected:
 	void Notify() override;
 
 public:
-	OpenALPlayer(AudioProvider *provider);
+	OpenALPlayer(agi::AudioProvider *provider);
 	~OpenALPlayer();
 
 	void Play(int64_t start,int64_t count) override;
@@ -122,7 +120,7 @@ public:
 	void SetVolume(double vol) override { volume = vol; }
 };
 
-OpenALPlayer::OpenALPlayer(AudioProvider *provider)
+OpenALPlayer::OpenALPlayer(agi::AudioProvider *provider)
 : AudioPlayer(provider)
 , samplerate(provider->GetSampleRate())
 , bpf(provider->GetChannels() * provider->GetBytesPerSample())
@@ -130,25 +128,25 @@ OpenALPlayer::OpenALPlayer(AudioProvider *provider)
 	try {
 		// Open device
 		device = alcOpenDevice(nullptr);
-		if (!device) throw OpenALException("Failed opening default OpenAL device");
+		if (!device) throw AudioPlayerOpenError("Failed opening default OpenAL device");
 
 		// Create context
 		context = alcCreateContext(device, nullptr);
-		if (!context) throw OpenALException("Failed creating OpenAL context");
-		if (!alcMakeContextCurrent(context)) throw OpenALException("Failed selecting OpenAL context");
+		if (!context) throw AudioPlayerOpenError("Failed creating OpenAL context");
+		if (!alcMakeContextCurrent(context)) throw AudioPlayerOpenError("Failed selecting OpenAL context");
 
 		// Clear error code
 		alGetError();
 
 		// Generate buffers
 		alGenBuffers(num_buffers, buffers);
-		if (alGetError() != AL_NO_ERROR) throw OpenALException("Error generating OpenAL buffers");
+		if (alGetError() != AL_NO_ERROR) throw AudioPlayerOpenError("Error generating OpenAL buffers");
 
 		// Generate source
 		alGenSources(1, &source);
 		if (alGetError() != AL_NO_ERROR) {
 			alDeleteBuffers(num_buffers, buffers);
-			throw OpenALException("Error generating OpenAL source");
+			throw AudioPlayerOpenError("Error generating OpenAL source");
 		}
 	}
 	catch (...)
@@ -284,7 +282,7 @@ int64_t OpenALPlayer::GetCurrentPosition()
 }
 }
 
-std::unique_ptr<AudioPlayer> CreateOpenALPlayer(AudioProvider *provider, wxWindow *)
+std::unique_ptr<AudioPlayer> CreateOpenALPlayer(agi::AudioProvider *provider, wxWindow *)
 {
 	return agi::make_unique<OpenALPlayer>(provider);
 }
diff --git a/src/audio_player_oss.cpp b/src/audio_player_oss.cpp
index 3b7548c24..fb52b87fc 100644
--- a/src/audio_player_oss.cpp
+++ b/src/audio_player_oss.cpp
@@ -35,10 +35,10 @@
 
 #include "audio_controller.h"
 #include "compat.h"
-#include "include/aegisub/audio_provider.h"
 #include "options.h"
 #include "utils.h"
 
+#include <libaegisub/audio/provider.h>
 #include <libaegisub/log.h>
 #include <libaegisub/make_unique.h>
 
@@ -54,7 +54,6 @@
 #endif
 
 namespace {
-DEFINE_EXCEPTION(OSSError, agi::AudioPlayerOpenError);
 class OSSPlayerThread;
 
 class OSSPlayer final : public AudioPlayer {
@@ -90,7 +89,7 @@ class OSSPlayer final : public AudioPlayer {
     void OpenStream();
 
 public:
-    OSSPlayer(AudioProvider *provider)
+    OSSPlayer(agi::AudioProvider *provider)
     : AudioPlayer(provider)
     {
         OpenStream();
@@ -153,7 +152,7 @@ void OSSPlayer::OpenStream()
     wxString device = to_wx(OPT_GET("Player/Audio/OSS/Device")->GetString());
     dspdev = ::open(device.utf8_str(), O_WRONLY, 0);
     if (dspdev < 0) {
-        throw OSSError("OSS player: opening device failed");
+        throw AudioPlayerOpenError("OSS player: opening device failed");
     }
 
     // Use a reasonable buffer policy for low latency (OSS4)
@@ -165,7 +164,7 @@ void OSSPlayer::OpenStream()
     // Set number of channels
     int channels = provider->GetChannels();
     if (ioctl(dspdev, SNDCTL_DSP_CHANNELS, &channels) < 0) {
-        throw OSSError("OSS player: setting channels failed");
+        throw AudioPlayerOpenError("OSS player: setting channels failed");
     }
 
     // Set sample format
@@ -178,17 +177,17 @@ void OSSPlayer::OpenStream()
             sample_format = AFMT_S16_LE;
             break;
         default:
-            throw OSSError("OSS player: can only handle 8 and 16 bit sound");
+            throw AudioPlayerOpenError("OSS player: can only handle 8 and 16 bit sound");
     }
 
     if (ioctl(dspdev, SNDCTL_DSP_SETFMT, &sample_format) < 0) {
-        throw OSSError("OSS player: setting sample format failed");
+        throw AudioPlayerOpenError("OSS player: setting sample format failed");
     }
 
     // Set sample rate
     rate = provider->GetSampleRate();
     if (ioctl(dspdev, SNDCTL_DSP_SPEED, &rate) < 0) {
-        throw OSSError("OSS player: setting samplerate failed");
+        throw AudioPlayerOpenError("OSS player: setting samplerate failed");
     }
 }
 
@@ -280,7 +279,7 @@ int64_t OSSPlayer::GetCurrentPosition()
 }
 }
 
-std::unique_ptr<AudioPlayer> CreateOSSPlayer(AudioProvider *provider, wxWindow *) {
+std::unique_ptr<AudioPlayer> CreateOSSPlayer(agi::AudioProvider *provider, wxWindow *) {
     return agi::make_unique<OSSPlayer>(provider);
 }
 
diff --git a/src/audio_player_portaudio.cpp b/src/audio_player_portaudio.cpp
index 37e884399..7a5babcdc 100644
--- a/src/audio_player_portaudio.cpp
+++ b/src/audio_player_portaudio.cpp
@@ -37,15 +37,13 @@
 
 #include "audio_controller.h"
 #include "compat.h"
-#include "include/aegisub/audio_provider.h"
 #include "options.h"
 #include "utils.h"
 
+#include <libaegisub/audio/provider.h>
 #include <libaegisub/log.h>
 #include <libaegisub/make_unique.h>
 
-DEFINE_EXCEPTION(PortAudioError, agi::AudioPlayerOpenError);
-
 // Uncomment to enable extremely spammy debug logging
 //#define PORTAUDIO_DEBUG
 
@@ -66,11 +64,11 @@ static const PaHostApiTypeId pa_host_api_priority[] = {
 };
 static const size_t pa_host_api_priority_count = sizeof(pa_host_api_priority) / sizeof(pa_host_api_priority[0]);
 
-PortAudioPlayer::PortAudioPlayer(AudioProvider *provider) : AudioPlayer(provider) {
+PortAudioPlayer::PortAudioPlayer(agi::AudioProvider *provider) : AudioPlayer(provider) {
 	PaError err = Pa_Initialize();
 
 	if (err != paNoError)
-		throw PortAudioError(std::string("Failed opening PortAudio: ") + Pa_GetErrorText(err));
+		throw AudioPlayerOpenError(std::string("Failed opening PortAudio: ") + Pa_GetErrorText(err));
 
 	// Build a list of host API-specific devices we can use
 	// Some host APIs may not support all audio formats, so build a priority
@@ -83,7 +81,7 @@ PortAudioPlayer::PortAudioPlayer(AudioProvider *provider) : AudioPlayer(provider
 	GatherDevices(Pa_GetDefaultHostApi());
 
 	if (devices.empty())
-		throw PortAudioError("No PortAudio output devices found");
+		throw AudioPlayerOpenError("No PortAudio output devices found");
 
 	if (provider)
 		OpenStream();
@@ -168,7 +166,7 @@ void PortAudioPlayer::OpenStream() {
 		}
 	}
 
-	throw PortAudioError("Failed initializing PortAudio stream: " + error);
+	throw AudioPlayerOpenError("Failed initializing PortAudio stream: " + error);
 }
 
 void PortAudioPlayer::paStreamFinishedCallback(void *) {
@@ -270,7 +268,7 @@ wxArrayString PortAudioPlayer::GetOutputDevices() {
 		for (auto it = player.devices.begin(); it != player.devices.end(); ++it)
 			list.push_back(to_wx(it->first));
 	}
-	catch (PortAudioError const&) {
+	catch (AudioPlayerOpenError const&) {
 		// No output devices, just return the list with only Default
 	}
 
@@ -281,7 +279,7 @@ bool PortAudioPlayer::IsPlaying() {
 	return !!Pa_IsStreamActive(stream);
 }
 
-std::unique_ptr<AudioPlayer> CreatePortAudioPlayer(AudioProvider *provider, wxWindow *) {
+std::unique_ptr<AudioPlayer> CreatePortAudioPlayer(agi::AudioProvider *provider, wxWindow *) {
 	return agi::make_unique<PortAudioPlayer>(provider);
 }
 
diff --git a/src/audio_player_pulse.cpp b/src/audio_player_pulse.cpp
index 1c2ce2d9c..7174356bd 100644
--- a/src/audio_player_pulse.cpp
+++ b/src/audio_player_pulse.cpp
@@ -36,9 +36,9 @@
 #include "include/aegisub/audio_player.h"
 
 #include "audio_controller.h"
-#include "include/aegisub/audio_provider.h"
 #include "utils.h"
 
+#include <libaegisub/audio/provider.h>
 #include <libaegisub/log.h>
 #include <libaegisub/make_unique.h>
 
@@ -83,7 +83,7 @@ class PulseAudioPlayer final : public AudioPlayer {
 	static void pa_stream_notify(pa_stream *p, PulseAudioPlayer *thread);
 
 public:
-	PulseAudioPlayer(AudioProvider *provider);
+	PulseAudioPlayer(agi::AudioProvider *provider);
 	~PulseAudioPlayer();
 
 	void Play(int64_t start,int64_t count);
@@ -97,11 +97,11 @@ public:
 	void SetVolume(double vol) { volume = vol; }
 };
 
-PulseAudioPlayer::PulseAudioPlayer(AudioProvider *provider) : AudioPlayer(provider) {
+PulseAudioPlayer::PulseAudioPlayer(agi::AudioProvider *provider) : AudioPlayer(provider) {
 	// Initialise a mainloop
 	mainloop = pa_threaded_mainloop_new();
 	if (!mainloop)
-		throw agi::AudioPlayerOpenError("Failed to initialise PulseAudio threaded mainloop object");
+		throw AudioPlayerOpenError("Failed to initialise PulseAudio threaded mainloop object");
 
 	pa_threaded_mainloop_start(mainloop);
 
@@ -109,7 +109,7 @@ PulseAudioPlayer::PulseAudioPlayer(AudioProvider *provider) : AudioPlayer(provid
 	context = pa_context_new(pa_threaded_mainloop_get_api(mainloop), "Aegisub");
 	if (!context) {
 		pa_threaded_mainloop_free(mainloop);
-		throw agi::AudioPlayerOpenError("Failed to create PulseAudio context");
+		throw AudioPlayerOpenError("Failed to create PulseAudio context");
 	}
 	pa_context_set_state_callback(context, (pa_context_notify_cb_t)pa_context_notify, this);
 
@@ -127,7 +127,7 @@ PulseAudioPlayer::PulseAudioPlayer(AudioProvider *provider) : AudioPlayer(provid
 			pa_context_unref(context);
 			pa_threaded_mainloop_stop(mainloop);
 			pa_threaded_mainloop_free(mainloop);
-			throw agi::AudioPlayerOpenError(std::string("PulseAudio reported error: ") + pa_strerror(paerror));
+			throw AudioPlayerOpenError(std::string("PulseAudio reported error: ") + pa_strerror(paerror));
 		}
 		// otherwise loop once more
 	}
@@ -148,7 +148,7 @@ PulseAudioPlayer::PulseAudioPlayer(AudioProvider *provider) : AudioPlayer(provid
 		pa_context_unref(context);
 		pa_threaded_mainloop_stop(mainloop);
 		pa_threaded_mainloop_free(mainloop);
-		throw agi::AudioPlayerOpenError("PulseAudio could not create stream");
+		throw AudioPlayerOpenError("PulseAudio could not create stream");
 	}
 	pa_stream_set_state_callback(stream, (pa_stream_notify_cb_t)pa_stream_notify, this);
 	pa_stream_set_write_callback(stream, (pa_stream_request_cb_t)pa_stream_write, this);
@@ -157,7 +157,7 @@ PulseAudioPlayer::PulseAudioPlayer(AudioProvider *provider) : AudioPlayer(provid
 	paerror = pa_stream_connect_playback(stream, nullptr, nullptr, (pa_stream_flags_t)(PA_STREAM_INTERPOLATE_TIMING|PA_STREAM_NOT_MONOTONOUS|PA_STREAM_AUTO_TIMING_UPDATE), nullptr, nullptr);
 	if (paerror) {
 		LOG_E("audio/player/pulse") << "Stream connection failed: " << pa_strerror(paerror) << "(" << paerror << ")";
-		throw agi::AudioPlayerOpenError(std::string("PulseAudio reported error: ") + pa_strerror(paerror));
+		throw AudioPlayerOpenError(std::string("PulseAudio reported error: ") + pa_strerror(paerror));
 	}
 	while (true) {
 		stream_notify.Wait();
@@ -166,7 +166,7 @@ PulseAudioPlayer::PulseAudioPlayer(AudioProvider *provider) : AudioPlayer(provid
 		} else if (sstate == PA_STREAM_FAILED) {
 			paerror = pa_context_errno(context);
 			LOG_E("audio/player/pulse") << "Stream connection failed: " << pa_strerror(paerror) << "(" << paerror << ")";
-			throw agi::AudioPlayerOpenError("PulseAudio player: Something went wrong connecting the stream");
+			throw AudioPlayerOpenError("PulseAudio player: Something went wrong connecting the stream");
 		}
 	}
 }
@@ -321,7 +321,7 @@ void PulseAudioPlayer::pa_stream_notify(pa_stream *p, PulseAudioPlayer *thread)
 }
 }
 
-std::unique_ptr<AudioPlayer> CreatePulseAudioPlayer(AudioProvider *provider, wxWindow *) {
+std::unique_ptr<AudioPlayer> CreatePulseAudioPlayer(agi::AudioProvider *provider, wxWindow *) {
 	return agi::make_unique<PulseAudioPlayer>(provider);
 }
 #endif // WITH_LIBPULSE
diff --git a/src/audio_provider.cpp b/src/audio_provider.cpp
deleted file mode 100644
index 58387e539..000000000
--- a/src/audio_provider.cpp
+++ /dev/null
@@ -1,191 +0,0 @@
-// Copyright (c) 2005-2006, Rodrigo Braz Monteiro
-// All rights reserved.
-//
-// Redistribution and use in source and binary forms, with or without
-// modification, are permitted provided that the following conditions are met:
-//
-//   * Redistributions of source code must retain the above copyright notice,
-//     this list of conditions and the following disclaimer.
-//   * Redistributions in binary form must reproduce the above copyright notice,
-//     this list of conditions and the following disclaimer in the documentation
-//     and/or other materials provided with the distribution.
-//   * Neither the name of the Aegisub Group nor the names of its contributors
-//     may be used to endorse or promote products derived from this software
-//     without specific prior written permission.
-//
-// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
-// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
-// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
-// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
-// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
-// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
-// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
-// POSSIBILITY OF SUCH DAMAGE.
-//
-// Aegisub Project http://www.aegisub.org/
-
-#include "include/aegisub/audio_provider.h"
-
-#include "audio_controller.h"
-#include "factory_manager.h"
-#include "options.h"
-#include "utils.h"
-
-#include <libaegisub/fs.h>
-#include <libaegisub/log.h>
-
-#include <boost/range/iterator_range.hpp>
-
-void AudioProvider::GetAudioWithVolume(void *buf, int64_t start, int64_t count, double volume) const {
-	GetAudio(buf, start, count);
-
-	if (volume == 1.0) return;
-	if (bytes_per_sample != 2)
-		throw agi::InternalError("GetAudioWithVolume called on unconverted audio stream");
-
-	short *buffer = static_cast<int16_t *>(buf);
-	for (size_t i = 0; i < (size_t)count; ++i)
-		buffer[i] = mid<int>(-0x8000, buffer[i] * volume + 0.5, 0x7FFF);
-}
-
-void AudioProvider::ZeroFill(void *buf, int64_t count) const {
-	if (bytes_per_sample == 1)
-		// 8 bit formats are usually unsigned with bias 127
-		memset(buf, 127, count * channels);
-	else
-		// While everything else is signed
-		memset(buf, 0, count * bytes_per_sample * channels);
-}
-
-void AudioProvider::GetAudio(void *buf, int64_t start, int64_t count) const {
-	if (start < 0) {
-		ZeroFill(buf, std::min(-start, count));
-		buf = static_cast<char *>(buf) + -start * bytes_per_sample * channels;
-		count += start;
-		start = 0;
-	}
-
-	if (start + count > num_samples) {
-		int64_t zero_count = std::min(count, start + count - num_samples);
-		count -= zero_count;
-		ZeroFill(static_cast<char *>(buf) + count * bytes_per_sample * channels, zero_count);
-	}
-
-	if (count <= 0) return;
-
-	try {
-		FillBuffer(buf, start, count);
-	}
-	catch (AudioDecodeError const& e) {
-		LOG_E("audio_provider") << e.GetMessage();
-		ZeroFill(buf, count);
-		return;
-	}
-	catch (...) {
-		// FIXME: Poor error handling though better than none, to patch issue #800.
-		// Just return blank audio if real provider fails.
-		LOG_E("audio_provider") << "Unknown audio decoding error";
-		ZeroFill(buf, count);
-		return;
-	}
-}
-
-std::unique_ptr<AudioProvider> CreateDummyAudioProvider(agi::fs::path const& filename, agi::BackgroundRunner *);
-std::unique_ptr<AudioProvider> CreatePCMAudioProvider(agi::fs::path const& filename, agi::BackgroundRunner *);
-std::unique_ptr<AudioProvider> CreateAvisynthAudioProvider(agi::fs::path const& filename, agi::BackgroundRunner *);
-std::unique_ptr<AudioProvider> CreateFFmpegSourceAudioProvider(agi::fs::path const& filename, agi::BackgroundRunner *);
-
-std::unique_ptr<AudioProvider> CreateConvertAudioProvider(std::unique_ptr<AudioProvider> source_provider);
-std::unique_ptr<AudioProvider> CreateLockAudioProvider(std::unique_ptr<AudioProvider> source_provider);
-std::unique_ptr<AudioProvider> CreateHDAudioProvider(std::unique_ptr<AudioProvider> source_provider);
-std::unique_ptr<AudioProvider> CreateRAMAudioProvider(std::unique_ptr<AudioProvider> source_provider);
-
-namespace {
-	struct factory {
-		const char *name;
-		std::unique_ptr<AudioProvider> (*create)(agi::fs::path const&, agi::BackgroundRunner *);
-		bool hidden;
-	};
-
-	const factory providers[] = {
-		{"Dummy", CreateDummyAudioProvider, true},
-		{"PCM", CreatePCMAudioProvider, true},
-#ifdef WITH_FFMS2
-		{"FFmpegSource", CreateFFmpegSourceAudioProvider, false},
-#endif
-#ifdef WITH_AVISYNTH
-		{"Avisynth", CreateAvisynthAudioProvider, false},
-#endif
-	};
-}
-
-std::vector<std::string> AudioProviderFactory::GetClasses() {
-	return ::GetClasses(boost::make_iterator_range(std::begin(providers), std::end(providers)));
-}
-
-std::unique_ptr<AudioProvider> AudioProviderFactory::GetProvider(agi::fs::path const& filename, agi::BackgroundRunner *br) {
-	auto preferred = OPT_GET("Audio/Provider")->GetString();
-	auto sorted = GetSorted(boost::make_iterator_range(std::begin(providers), std::end(providers)), preferred);
-
-	std::unique_ptr<AudioProvider> provider;
-	bool found_file = false;
-	bool found_audio = false;
-	std::string msg_all;     // error messages from all attempted providers
-	std::string msg_partial; // error messages from providers that could partially load the file (knows container, missing codec)
-
-	for (auto const& factory : sorted) {
-		try {
-			provider = factory->create(filename, br);
-			if (!provider) continue;
-			LOG_I("audio_provider") << "Using audio provider: " << factory->name;
-			break;
-		}
-		catch (agi::fs::FileNotFound const& err) {
-			LOG_D("audio_provider") << err.GetMessage();
-			msg_all += std::string(factory->name) + ": " + err.GetMessage() + " not found.\n";
-		}
-		catch (agi::AudioDataNotFoundError const& err) {
-			LOG_D("audio_provider") << err.GetMessage();
-			found_file = true;
-			msg_all += std::string(factory->name) + ": " + err.GetMessage() + "\n";
-		}
-		catch (agi::AudioOpenError const& err) {
-			LOG_D("audio_provider") << err.GetMessage();
-			found_audio = true;
-			found_file = true;
-			std::string thismsg = std::string(factory->name) + ": " + err.GetMessage() + "\n";
-			msg_all += thismsg;
-			msg_partial += thismsg;
-		}
-	}
-
-	if (!provider) {
-		if (found_audio)
-			throw agi::AudioProviderOpenError(msg_partial);
-		if (found_file)
-			throw agi::AudioDataNotFoundError(msg_all);
-		throw agi::fs::FileNotFound(filename);
-	}
-
-	bool needsCache = provider->NeedsCache();
-
-	// Give it a converter if needed
-	if (provider->GetBytesPerSample() != 2 || provider->GetSampleRate() < 32000 || provider->GetChannels() != 1)
-		provider = CreateConvertAudioProvider(std::move(provider));
-
-	// Change provider to RAM/HD cache if needed
-	int cache = OPT_GET("Audio/Cache/Type")->GetInt();
-	if (!cache || !needsCache)
-		return CreateLockAudioProvider(std::move(provider));
-
-	// Convert to RAM
-	if (cache == 1) return CreateRAMAudioProvider(std::move(provider));
-
-	// Convert to HD
-	if (cache == 2) return CreateHDAudioProvider(std::move(provider));
-
-	throw agi::AudioCacheOpenError("Unknown caching method");
-}
diff --git a/src/audio_provider_avs.cpp b/src/audio_provider_avs.cpp
index 24be20e10..26480188b 100644
--- a/src/audio_provider_avs.cpp
+++ b/src/audio_provider_avs.cpp
@@ -33,7 +33,7 @@
 ///
 
 #ifdef WITH_AVISYNTH
-#include "include/aegisub/audio_provider.h"
+#include <libaegisub/audio/provider.h>
 
 #include "avisynth.h"
 #include "avisynth_wrap.h"
@@ -50,7 +50,7 @@
 #include <mutex>
 
 namespace {
-class AvisynthAudioProvider final : public AudioProvider {
+class AvisynthAudioProvider final : public agi::AudioProvider {
 	AviSynthWrapper avs_wrapper;
 	PClip clip;
 
diff --git a/src/audio_provider_dummy.cpp b/src/audio_provider_dummy.cpp
deleted file mode 100644
index 6a1dc9d08..000000000
--- a/src/audio_provider_dummy.cpp
+++ /dev/null
@@ -1,88 +0,0 @@
-// Copyright (c) 2005-2006, Rodrigo Braz Monteiro, Fredrik Mellbin
-// All rights reserved.
-//
-// Redistribution and use in source and binary forms, with or without
-// modification, are permitted provided that the following conditions are met:
-//
-//   * Redistributions of source code must retain the above copyright notice,
-//     this list of conditions and the following disclaimer.
-//   * Redistributions in binary form must reproduce the above copyright notice,
-//     this list of conditions and the following disclaimer in the documentation
-//     and/or other materials provided with the distribution.
-//   * Neither the name of the Aegisub Group nor the names of its contributors
-//     may be used to endorse or promote products derived from this software
-//     without specific prior written permission.
-//
-// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
-// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
-// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
-// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
-// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
-// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
-// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
-// POSSIBILITY OF SUCH DAMAGE.
-//
-// Aegisub Project http://www.aegisub.org/
-
-#include "include/aegisub/audio_provider.h"
-
-#include <libaegisub/make_unique.h>
-
-#include <boost/algorithm/string/predicate.hpp>
-#include <boost/filesystem/path.hpp>
-
-/*
- * scheme            ::= "dummy-audio" ":" signal-specifier "?" signal-parameters
- * signal-specifier  ::= "silence" | "noise" | "sine" "/" frequency
- * frequency         ::= integer
- * signal-parameters ::= signal-parameter [ "&" signal-parameters ]
- * signal-parameter  ::= signal-parameter-name "=" integer
- * signal-parameter-name ::= "sr" | "bd" | "ch" | "ln"
- *
- * Signal types:
- * "silence", a silent signal is generated.
- * "noise", a white noise signal is generated.
- * "sine", a sine wave is generated at the specified frequency.
- *
- * Signal parameters:
- * "sr", sample rate to generate signal at.
- * "bd", bit depth to generate signal at (usually 16).
- * "ch", number of channels to generate, usually 1 or 2. The same signal is generated
- *       in every channel even if one would be LFE.
- * "ln", length of signal in samples. ln/sr gives signal length in seconds.
- */
-
-namespace {
-class DummyAudioProvider final : public AudioProvider {
-	bool noise;
-	void FillBuffer(void *buf, int64_t start, int64_t count) const override {
-		if (noise) {
-			auto workbuf = static_cast<uint16_t *>(buf);
-			while (count-- > 0)
-				*workbuf++ = (rand() - RAND_MAX/2) * 10000 / RAND_MAX;
-		}
-		else {
-			memset(buf, 0, count * bytes_per_sample);
-		}
-	}
-
-public:
-	DummyAudioProvider(agi::fs::path const& uri) {
-		noise = boost::contains(uri.string(), ":noise?");
-		channels = 1;
-		sample_rate = 44100;
-		bytes_per_sample = 2;
-		float_samples = false;
-		decoded_samples = num_samples = (int64_t)5*30*60*1000 * sample_rate / 1000;
-	}
-};
-}
-
-std::unique_ptr<AudioProvider> CreateDummyAudioProvider(agi::fs::path const& file, agi::BackgroundRunner *) {
-	if (!boost::starts_with(file.string(), "dummy-audio:"))
-		return {};
-	return agi::make_unique<DummyAudioProvider>(file);
-}
diff --git a/src/audio_provider_factory.cpp b/src/audio_provider_factory.cpp
new file mode 100644
index 000000000..0e5fe3589
--- /dev/null
+++ b/src/audio_provider_factory.cpp
@@ -0,0 +1,126 @@
+// Copyright (c) 2014, Thomas Goyne <plorkyeran@aegisub.org>
+//
+// Permission to use, copy, modify, and distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+//
+// Aegisub Project http://www.aegisub.org/
+
+#include "audio_provider_factory.h"
+
+#include "factory_manager.h"
+#include "options.h"
+#include "utils.h"
+
+#include <libaegisub/audio/provider.h>
+#include <libaegisub/fs.h>
+#include <libaegisub/log.h>
+#include <libaegisub/path.h>
+
+#include <boost/range/iterator_range.hpp>
+
+using namespace agi;
+
+std::unique_ptr<AudioProvider> CreateAvisynthAudioProvider(fs::path const& filename, BackgroundRunner *);
+std::unique_ptr<AudioProvider> CreateFFmpegSourceAudioProvider(fs::path const& filename, BackgroundRunner *);
+
+namespace {
+struct factory {
+	const char *name;
+	std::unique_ptr<AudioProvider> (*create)(fs::path const&, BackgroundRunner *);
+	bool hidden;
+};
+
+const factory providers[] = {
+	{"Dummy", CreateDummyAudioProvider, true},
+	{"PCM", CreatePCMAudioProvider, true},
+#ifdef WITH_FFMS2
+	{"FFmpegSource", CreateFFmpegSourceAudioProvider, false},
+#endif
+#ifdef WITH_AVISYNTH
+	{"Avisynth", CreateAvisynthAudioProvider, false},
+#endif
+};
+}
+
+std::vector<std::string> GetAudioProviderNames() {
+	return ::GetClasses(boost::make_iterator_range(std::begin(providers), std::end(providers)));
+}
+
+std::unique_ptr<AudioProvider> GetAudioProvider(fs::path const& filename, BackgroundRunner *br) {
+	auto preferred = OPT_GET("Audio/Provider")->GetString();
+	auto sorted = GetSorted(boost::make_iterator_range(std::begin(providers), std::end(providers)), preferred);
+
+	std::unique_ptr<AudioProvider> provider;
+	bool found_file = false;
+	bool found_audio = false;
+	std::string msg_all;     // error messages from all attempted providers
+	std::string msg_partial; // error messages from providers that could partially load the file (knows container, missing codec)
+
+	for (auto const& factory : sorted) {
+		try {
+			provider = factory->create(filename, br);
+			if (!provider) continue;
+			LOG_I("audio_provider") << "Using audio provider: " << factory->name;
+			break;
+		}
+		catch (fs::FileNotFound const& err) {
+			LOG_D("audio_provider") << err.GetMessage();
+			msg_all += std::string(factory->name) + ": " + err.GetMessage() + " not found.\n";
+		}
+		catch (AudioDataNotFound const& err) {
+			LOG_D("audio_provider") << err.GetMessage();
+			found_file = true;
+			msg_all += std::string(factory->name) + ": " + err.GetMessage() + "\n";
+		}
+		catch (AudioProviderError const& err) {
+			LOG_D("audio_provider") << err.GetMessage();
+			found_audio = true;
+			found_file = true;
+			std::string thismsg = std::string(factory->name) + ": " + err.GetMessage() + "\n";
+			msg_all += thismsg;
+			msg_partial += thismsg;
+		}
+	}
+
+	if (!provider) {
+		if (found_audio)
+			throw AudioProviderError(msg_partial);
+		if (found_file)
+			throw AudioDataNotFound(msg_all);
+		throw fs::FileNotFound(filename);
+	}
+
+	bool needs_cache = provider->NeedsCache();
+
+	// Give it a converter if needed
+	if (provider->GetBytesPerSample() != 2 || provider->GetSampleRate() < 32000 || provider->GetChannels() != 1)
+		provider = CreateConvertAudioProvider(std::move(provider));
+
+	// Change provider to RAM/HD cache if needed
+	int cache = OPT_GET("Audio/Cache/Type")->GetInt();
+	if (!cache || !needs_cache)
+		return CreateLockAudioProvider(std::move(provider));
+
+	// Convert to RAM
+	if (cache == 1) return CreateRAMAudioProvider(std::move(provider));
+
+	// Convert to HD
+	if (cache == 2) {
+		auto path = OPT_GET("Audio/Cache/HD/Location")->GetString();
+		if (path == "default")
+			path = "?temp";
+		auto cache_dir = config::path->MakeAbsolute(config::path->Decode(path), "?temp");
+		return CreateHDAudioProvider(std::move(provider), cache_dir);
+	}
+
+	throw InternalError("Invalid audio caching method");
+}
diff --git a/src/audio_provider_factory.h b/src/audio_provider_factory.h
new file mode 100644
index 000000000..bbd7af972
--- /dev/null
+++ b/src/audio_provider_factory.h
@@ -0,0 +1,27 @@
+// Copyright (c) 2014, Thomas Goyne <plorkyeran@aegisub.org>
+//
+// Permission to use, copy, modify, and distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+//
+// Aegisub Project http://www.aegisub.org/
+
+#include <libaegisub/fs_fwd.h>
+
+#include <memory>
+
+namespace agi {
+	class AudioProvider;
+	class BackgroundRunner;
+}
+
+std::unique_ptr<agi::AudioProvider> GetAudioProvider(agi::fs::path const& filename, agi::BackgroundRunner *br);
+std::vector<std::string> GetAudioProviderNames();
diff --git a/src/audio_provider_ffmpegsource.cpp b/src/audio_provider_ffmpegsource.cpp
index 3a89f6506..4e44b1d29 100644
--- a/src/audio_provider_ffmpegsource.cpp
+++ b/src/audio_provider_ffmpegsource.cpp
@@ -33,9 +33,8 @@
 ///
 
 #ifdef WITH_FFMS2
-#include "include/aegisub/audio_provider.h"
+#include <libaegisub/audio/provider.h>
 
-#include "audio_controller.h"
 #include "ffmpegsource_common.h"
 #include "options.h"
 
@@ -45,7 +44,7 @@
 #include <map>
 
 namespace {
-class FFmpegSourceAudioProvider final : public AudioProvider, FFmpegSourceProvider {
+class FFmpegSourceAudioProvider final : public agi::AudioProvider, FFmpegSourceProvider {
 	/// audio source object
 	agi::scoped_holder<FFMS_AudioSource*, void (FFMS_CC *)(FFMS_AudioSource*)> AudioSource;
 
@@ -55,7 +54,7 @@ class FFmpegSourceAudioProvider final : public AudioProvider, FFmpegSourceProvid
 	void LoadAudio(agi::fs::path const& filename);
 	void FillBuffer(void *Buf, int64_t Start, int64_t Count) const override {
 		if (FFMS_GetAudio(AudioSource, Buf, Start, Count, &ErrInfo))
-			throw AudioDecodeError(std::string("Failed to get audio samples: ") + ErrInfo.Buffer);
+			throw agi::AudioDecodeError(std::string("Failed to get audio samples: ") + ErrInfo.Buffer);
 	}
 
 public:
@@ -79,7 +78,7 @@ FFmpegSourceAudioProvider::FFmpegSourceAudioProvider(agi::fs::path const& filena
 	LoadAudio(filename);
 }
 catch (agi::EnvironmentError const& err) {
-	throw agi::AudioProviderOpenError(err.GetMessage());
+	throw agi::AudioProviderError(err.GetMessage());
 }
 
 void FFmpegSourceAudioProvider::LoadAudio(agi::fs::path const& filename) {
@@ -88,12 +87,12 @@ void FFmpegSourceAudioProvider::LoadAudio(agi::fs::path const& filename) {
 		if (ErrInfo.SubType == FFMS_ERROR_FILE_READ)
 			throw agi::fs::FileNotFound(std::string(ErrInfo.Buffer));
 		else
-			throw agi::AudioDataNotFoundError(ErrInfo.Buffer);
+			throw agi::AudioDataNotFound(ErrInfo.Buffer);
 	}
 
 	std::map<int, std::string> TrackList = GetTracksOfType(Indexer, FFMS_TYPE_AUDIO);
 	if (TrackList.empty())
-		throw agi::AudioDataNotFoundError("no audio tracks found");
+		throw agi::AudioDataNotFound("no audio tracks found");
 
 	// initialize the track number to an invalid value so we can detect later on
 	// whether the user actually had to choose a track or not
@@ -121,7 +120,7 @@ void FFmpegSourceAudioProvider::LoadAudio(agi::fs::path const& filename) {
 		if (TrackNumber < 0)
 			TrackNumber = FFMS_GetFirstTrackOfType(Index, FFMS_TYPE_AUDIO, &ErrInfo);
 		if (TrackNumber < 0)
-			throw agi::AudioDataNotFoundError(std::string("Couldn't find any audio tracks: ") + ErrInfo.Buffer);
+			throw agi::AudioDataNotFound(std::string("Couldn't find any audio tracks: ") + ErrInfo.Buffer);
 
 		// index is valid and track number is now set,
 		// but do we have indexing info for the desired audio track?
@@ -163,7 +162,7 @@ void FFmpegSourceAudioProvider::LoadAudio(agi::fs::path const& filename) {
 
 	AudioSource = FFMS_CreateAudioSource(filename.string().c_str(), TrackNumber, Index, -1, &ErrInfo);
 	if (!AudioSource)
-		throw agi::AudioProviderOpenError(std::string("Failed to open audio track: ") + ErrInfo.Buffer);
+		throw agi::AudioProviderError(std::string("Failed to open audio track: ") + ErrInfo.Buffer);
 
 	const FFMS_AudioProperties AudioInfo = *FFMS_GetAudioProperties(AudioSource);
 
@@ -172,7 +171,7 @@ void FFmpegSourceAudioProvider::LoadAudio(agi::fs::path const& filename) {
 	num_samples = AudioInfo.NumSamples;
 	decoded_samples = AudioInfo.NumSamples;
 	if (channels <= 0 || sample_rate <= 0 || num_samples <= 0)
-		throw agi::AudioProviderOpenError("sanity check failed, consult your local psychiatrist");
+		throw agi::AudioProviderError("sanity check failed, consult your local psychiatrist");
 
 	switch (AudioInfo.SampleFormat) {
 		case FFMS_FMT_U8:  bytes_per_sample = 1; float_samples = false; break;
@@ -181,7 +180,7 @@ void FFmpegSourceAudioProvider::LoadAudio(agi::fs::path const& filename) {
 		case FFMS_FMT_FLT: bytes_per_sample = 4; float_samples = true; break;
 		case FFMS_FMT_DBL: bytes_per_sample = 8; float_samples = true; break;
 		default:
-			throw agi::AudioProviderOpenError("unknown or unsupported sample format");
+			throw agi::AudioProviderError("unknown or unsupported sample format");
 	}
 
 #if FFMS_VERSION >= ((2 << 24) | (17 << 16) | (4 << 8) | 0)
@@ -203,7 +202,7 @@ void FFmpegSourceAudioProvider::LoadAudio(agi::fs::path const& filename) {
 
 }
 
-std::unique_ptr<AudioProvider> CreateFFmpegSourceAudioProvider(agi::fs::path const& file, agi::BackgroundRunner *br) {
+std::unique_ptr<agi::AudioProvider> CreateFFmpegSourceAudioProvider(agi::fs::path const& file, agi::BackgroundRunner *br) {
 	return agi::make_unique<FFmpegSourceAudioProvider>(file, br);
 }
 
diff --git a/src/audio_provider_pcm.cpp b/src/audio_provider_pcm.cpp
deleted file mode 100644
index 945dc007b..000000000
--- a/src/audio_provider_pcm.cpp
+++ /dev/null
@@ -1,375 +0,0 @@
-// Copyright (c) 2007-2008, Niels Martin Hansen
-// All rights reserved.
-//
-// Redistribution and use in source and binary forms, with or without
-// modification, are permitted provided that the following conditions are met:
-//
-//   * Redistributions of source code must retain the above copyright notice,
-//     this list of conditions and the following disclaimer.
-//   * Redistributions in binary form must reproduce the above copyright notice,
-//     this list of conditions and the following disclaimer in the documentation
-//     and/or other materials provided with the distribution.
-//   * Neither the name of the Aegisub Group nor the names of its contributors
-//     may be used to endorse or promote products derived from this software
-//     without specific prior written permission.
-//
-// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
-// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
-// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
-// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
-// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
-// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
-// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
-// POSSIBILITY OF SUCH DAMAGE.
-//
-// Aegisub Project http://www.aegisub.org/
-
-#include "include/aegisub/audio_provider.h"
-
-#include "audio_controller.h"
-
-#include <libaegisub/file_mapping.h>
-#include <libaegisub/fs.h>
-#include <libaegisub/make_unique.h>
-
-#include <vector>
-
-class PCMAudioProvider : public AudioProvider {
-	void FillBuffer(void *buf, int64_t start, int64_t count) const override;
-
-protected:
-	std::unique_ptr<agi::read_file_mapping> file;
-
-	PCMAudioProvider(agi::fs::path const& filename)
-	: file(agi::make_unique<agi::read_file_mapping>(filename))
-	{
-		float_samples = false;
-	}
-
-	const char *EnsureRangeAccessible(int64_t start, int64_t length) const {
-		try {
-			return file->read(start, static_cast<size_t>(length));
-		}
-		catch (agi::fs::FileSystemError const& e) {
-			throw AudioDecodeError(e.GetMessage());
-		}
-	}
-
-	template<typename T>
-	T const& Read(int64_t start) const {
-		return *reinterpret_cast<const T *>(EnsureRangeAccessible(start, sizeof(T)));
-	}
-
-	struct IndexPoint {
-		int64_t start_byte;
-		int64_t num_samples;
-	};
-	std::vector<IndexPoint> index_points;
-};
-
-void PCMAudioProvider::FillBuffer(void *buf, int64_t start, int64_t count) const {
-	auto write_buf = static_cast<char *>(buf);
-	auto bps = bytes_per_sample * channels;
-	int64_t pos = 0;
-
-	for (auto const& ip : index_points) {
-		if (count == 0) break;
-		if (pos + ip.num_samples < start) {
-			pos += ip.num_samples;
-			continue;
-		}
-
-		auto read_offset = start - pos;
-		auto read_count = std::min(count, ip.num_samples - read_offset);
-		auto bytes = read_count * bps;
-		memcpy(write_buf, file->read(ip.start_byte + read_offset * bps, bytes), bytes);
-
-		write_buf += bytes;
-		count -= read_count;
-		start += read_count;
-		pos += ip.num_samples;
-	}
-}
-
-/// @class RiffWavPCMAudioProvider
-/// @brief RIFF WAV PCM provider
-///
-/// Overview of RIFF WAV: <http://www.sonicspot.com/guide/wavefiles.html>
-class  RiffWavPCMAudioProvider : public PCMAudioProvider {
-	struct ChunkHeader {
-		/// Always "RIFF"
-		char type[4];
-		/// File size minus sizeof(ChunkHeader) (i.e. 8)
-		uint32_t size;
-	};
-
-	struct RIFFChunk {
-		ChunkHeader ch;
-		/// Always "WAVE"
-		char format[4];
-	};
-
-	struct fmtChunk {
-		/// compression format used
-		/// We support only PCM (0x1)
-		uint16_t compression;
-
-		/// Number of channels
-		uint16_t channels;
-
-		/// Samples per second
-		uint32_t samplerate;
-
-		/// Bytes per second
-		/// can't always be trusted
-		uint32_t avg_bytes_sec;
-
-		/// Bytes per sample
-		uint16_t block_align;
-
-		/// Bits per sample that are actually used; rest should be ignored
-		uint16_t significant_bits_sample;
-		// Here was supposed to be some more fields but we don't need them
-		// and just skipping by the size of the struct wouldn't be safe
-		// either way, as the fields can depend on the compression.
-	};
-
-	static bool CheckFourcc(const char (&str1)[4], const char (&str2)[5])
-	{
-		return str1[0] == str2[0]
-			&& str1[1] == str2[1]
-			&& str1[2] == str2[2]
-			&& str1[3] == str2[3];
-	}
-
-public:
-
-	RiffWavPCMAudioProvider(agi::fs::path const& filename)
-	: PCMAudioProvider(filename)
-	{
-		// Read header
-		auto const& header = Read<RIFFChunk>(0);
-
-		// Check magic values
-		if (!CheckFourcc(header.ch.type, "RIFF"))
-			throw agi::AudioDataNotFoundError("File is not a RIFF file");
-		if (!CheckFourcc(header.format, "WAVE"))
-			throw agi::AudioDataNotFoundError("File is not a RIFF WAV file");
-
-		// How far into the file we have processed.
-		// Must be incremented by the riff chunk size fields.
-		uint32_t filepos = sizeof(header);
-		// Count how much more data we can have in the entire file
-		// The first 4 bytes are already eaten by the header.format field
-		auto total_data = std::min<uint32_t>(header.ch.size - 4 + filepos, file->size());
-
-		bool got_fmt_header = false;
-
-		// Inherited from AudioProvider
-		num_samples = 0;
-
-		// Continue reading chunks until out of data
-		while (filepos + sizeof(ChunkHeader) < total_data) {
-			auto const& ch = Read<ChunkHeader>(filepos);
-			filepos += sizeof(ch);
-
-			if (CheckFourcc(ch.type, "fmt ")) {
-				if (got_fmt_header) throw agi::AudioProviderOpenError("Invalid file, multiple 'fmt ' chunks");
-				got_fmt_header = true;
-
-				auto const& fmt = Read<fmtChunk>(filepos);
-
-				if (fmt.compression != 1)
-					throw agi::AudioProviderOpenError("Can't use file, not PCM encoding");
-
-				// Set stuff inherited from the AudioProvider class
-				sample_rate = fmt.samplerate;
-				channels = fmt.channels;
-				bytes_per_sample = (fmt.significant_bits_sample + 7) / 8; // round up to nearest whole byte
-			}
-			else if (CheckFourcc(ch.type, "data")) {
-				// This won't pick up 'data' chunks inside 'wavl' chunks
-				// since the 'wavl' chunks wrap those.
-
-				if (!got_fmt_header) throw agi::AudioProviderOpenError("Found 'data' chunk before 'fmt ' chunk, file is invalid.");
-
-				auto samples = std::min(total_data - filepos, ch.size) / bytes_per_sample / channels;
-				index_points.push_back(IndexPoint{filepos, samples});
-				num_samples += samples;
-			}
-
-			// Support wavl (wave list) chunks too?
-
-			// Update counters
-			// Make sure they're word aligned
-			filepos += (ch.size + 1) & ~1;
-		}
-
-		decoded_samples = num_samples;
-	}
-};
-
-static const uint8_t w64GuidRIFF[16] = {
-	// {66666972-912E-11CF-A5D6-28DB04C10000}
-	0x72, 0x69, 0x66, 0x66, 0x2E, 0x91, 0xCF, 0x11, 0xA5, 0xD6, 0x28, 0xDB, 0x04, 0xC1, 0x00, 0x00
-};
-
-static const uint8_t w64GuidWAVE[16] = {
-	// {65766177-ACF3-11D3-8CD1-00C04F8EDB8A}
-	0x77, 0x61, 0x76, 0x65, 0xF3, 0xAC, 0xD3, 0x11, 0x8C, 0xD1, 0x00, 0xC0, 0x4F, 0x8E, 0xDB, 0x8A
-};
-
-static const uint8_t w64Guidfmt[16] = {
-	// {20746D66-ACF3-11D3-8CD1-00C04F8EDB8A}
-	0x66, 0x6D, 0x74, 0x20, 0xF3, 0xAC, 0xD3, 0x11, 0x8C, 0xD1, 0x00, 0xC0, 0x4F, 0x8E, 0xDB, 0x8A
-};
-
-static const uint8_t w64Guiddata[16] = {
-	// {61746164-ACF3-11D3-8CD1-00C04F8EDB8A}
-	0x64, 0x61, 0x74, 0x61, 0xF3, 0xAC, 0xD3, 0x11, 0x8C, 0xD1, 0x00, 0xC0, 0x4F, 0x8E, 0xDB, 0x8A
-};
-
-/// @class Wave64AudioProvider
-/// @brief Sony Wave64 audio provider
-///
-/// http://www.vcs.de/fileadmin/user_upload/MBS/PDF/Whitepaper/Informations_about_Sony_Wave64.pdf
-class Wave64AudioProvider final : public PCMAudioProvider {
-	// Here's some copy-paste from the FFmpegSource2 code
-
-	/// http://msdn.microsoft.com/en-us/library/dd757720(VS.85).aspx
-	struct WaveFormatEx {
-		uint16_t wFormatTag;
-		uint16_t nChannels;
-		uint32_t nSamplesPerSec;
-		uint32_t nAvgBytesPerSec;
-		uint16_t nBlockAlign;
-		uint16_t wBitsPerSample;
-		uint16_t cbSize;
-	};
-
-	struct RiffChunk {
-		uint8_t riff_guid[16];
-		uint64_t file_size;
-		uint8_t format_guid[16];
-	};
-
-	struct FormatChunk {
-		uint8_t chunk_guid[16];
-		uint64_t chunk_size;
-		WaveFormatEx format;
-		uint8_t padding[6];
-	};
-
-	struct DataChunk {
-		uint8_t chunk_guid[16];
-		uint64_t chunk_size;
-	};
-
-	bool CheckGuid(const uint8_t *guid1, const uint8_t *guid2) {
-		return memcmp(guid1, guid2, 16) == 0;
-	}
-
-public:
-
-	Wave64AudioProvider(agi::fs::path const& filename)
-	: PCMAudioProvider(filename)
-	{
-		size_t smallest_possible_file = sizeof(RiffChunk) + sizeof(FormatChunk) + sizeof(DataChunk);
-
-		if (file->size() < smallest_possible_file)
-			throw agi::AudioDataNotFoundError("File is too small to be a Wave64 file");
-
-		// Read header
-		auto const& header = Read<RiffChunk>(0);
-
-		// Check magic values
-		if (!CheckGuid(header.riff_guid, w64GuidRIFF))
-			throw agi::AudioDataNotFoundError("File is not a Wave64 RIFF file");
-		if (!CheckGuid(header.format_guid, w64GuidWAVE))
-			throw agi::AudioDataNotFoundError("File is not a Wave64 WAVE file");
-
-		// How far into the file we have processed.
-		// Must be incremented by the riff chunk size fields.
-		uint64_t filepos = sizeof(header);
-		// Count how much more data we can have in the entire file
-		auto total_data = std::min<uint64_t>(header.file_size, file->size());
-
-		bool got_fmt_header = false;
-
-		// Inherited from AudioProvider
-		num_samples = 0;
-
-		// Continue reading chunks until out of data
-		while (filepos + 24 < total_data) {
-			uint8_t *chunk_guid = (uint8_t*)EnsureRangeAccessible(filepos, 16);
-			auto chunk_size = std::min(total_data - filepos, Read<uint64_t>(filepos + 16)) - 24;
-			filepos += 24;
-
-			if (CheckGuid(chunk_guid, w64Guidfmt)) {
-				if (got_fmt_header)
-					throw agi::AudioProviderOpenError("Bad file, found more than one 'fmt' chunk");
-
-				auto const& fmt = Read<FormatChunk>(filepos);
-				if (fmt.format.wFormatTag != 1)
-					throw agi::AudioProviderOpenError("File is not uncompressed PCM");
-
-				got_fmt_header = true;
-				// Set stuff inherited from the AudioProvider class
-				sample_rate = fmt.format.nSamplesPerSec;
-				channels = fmt.format.nChannels;
-				bytes_per_sample = (fmt.format.wBitsPerSample + 7) / 8; // round up to nearest whole byte
-			}
-			else if (CheckGuid(chunk_guid, w64Guiddata)) {
-				if (!got_fmt_header)
-					throw agi::AudioProviderOpenError("Found 'data' chunk before 'fmt ' chunk, file is invalid.");
-
-				auto samples = chunk_size / bytes_per_sample / channels;
-				index_points.push_back(IndexPoint{
-					static_cast<int64_t>(filepos),
-					static_cast<int64_t>(samples)});
-				num_samples += samples;
-			}
-
-			// Update counters
-			// Make sure they're 64 bit aligned
-			filepos += (chunk_size + 7) & ~7;
-		}
-
-		decoded_samples = num_samples;
-	}
-};
-
-std::unique_ptr<AudioProvider> CreatePCMAudioProvider(agi::fs::path const& filename, agi::BackgroundRunner *) {
-	bool wrong_file_type = true;
-	std::string msg;
-
-	try {
-		return agi::make_unique<RiffWavPCMAudioProvider>(filename);
-	}
-	catch (agi::AudioDataNotFoundError const& err) {
-		msg = "RIFF PCM WAV audio provider: " + err.GetMessage();
-	}
-	catch (agi::AudioProviderOpenError const& err) {
-		wrong_file_type = false;
-		msg = "RIFF PCM WAV audio provider: " + err.GetMessage();
-	}
-
-	try {
-		return agi::make_unique<Wave64AudioProvider>(filename);
-	}
-	catch (agi::AudioDataNotFoundError const& err) {
-		msg += "\nWave64 audio provider: " + err.GetMessage();
-	}
-	catch (agi::AudioProviderOpenError const& err) {
-		wrong_file_type = false;
-		msg += "\nWave64 audio provider: " + err.GetMessage();
-	}
-
-	if (wrong_file_type)
-		throw agi::AudioDataNotFoundError(msg);
-	else
-		throw agi::AudioProviderOpenError(msg);
-}
diff --git a/src/audio_renderer.cpp b/src/audio_renderer.cpp
index 90872fce2..3ae563af0 100644
--- a/src/audio_renderer.cpp
+++ b/src/audio_renderer.cpp
@@ -33,8 +33,7 @@
 
 #include "audio_renderer.h"
 
-#include "include/aegisub/audio_provider.h"
-
+#include <libaegisub/audio/provider.h>
 #include <libaegisub/make_unique.h>
 
 #include <algorithm>
@@ -120,7 +119,7 @@ void AudioRenderer::SetRenderer(AudioRendererBitmapProvider *_renderer)
 	}
 }
 
-void AudioRenderer::SetAudioProvider(AudioProvider *_provider)
+void AudioRenderer::SetAudioProvider(agi::AudioProvider *_provider)
 {
 	if (compare_and_set(provider, _provider))
 	{
@@ -224,7 +223,7 @@ void AudioRenderer::Invalidate()
 	needs_age = false;
 }
 
-void AudioRendererBitmapProvider::SetProvider(AudioProvider *_provider)
+void AudioRendererBitmapProvider::SetProvider(agi::AudioProvider *_provider)
 {
 	if (compare_and_set(provider, _provider))
 		OnSetProvider();
diff --git a/src/audio_renderer.h b/src/audio_renderer.h
index c0f53ee97..7bfeb2428 100644
--- a/src/audio_renderer.h
+++ b/src/audio_renderer.h
@@ -37,10 +37,10 @@
 #include "audio_rendering_style.h"
 #include "block_cache.h"
 
-class AudioProvider;
-class AudioRendererBitmapProvider;
 class AudioRenderer;
+class AudioRendererBitmapProvider;
 class wxDC;
+namespace agi { class AudioProvider; }
 
 /// @class AudioRendererBitmapCacheBitmapFactory
 /// @brief Produces wxBitmap objects for DataBlockCache storage for the audio renderer
@@ -102,7 +102,7 @@ class AudioRenderer {
 	AudioRendererBitmapProvider *renderer = nullptr;
 
 	/// Audio provider to use as source
-	AudioProvider *provider = nullptr;
+	agi::AudioProvider *provider = nullptr;
 
 	/// @brief Make sure bitmap index i is in cache
 	/// @param i     Index of bitmap to get into cache
@@ -191,7 +191,7 @@ public:
 	/// Changing audio provider invalidates all cached bitmaps.
 	///
 	/// If a renderer is set, this will also set the audio provider for the renderer.
-	void SetAudioProvider(AudioProvider *provider);
+	void SetAudioProvider(agi::AudioProvider *provider);
 
 	/// @brief Render audio to a device context
 	/// @param dc       The device context to draw to
@@ -223,7 +223,7 @@ public:
 class AudioRendererBitmapProvider {
 protected:
 	/// Audio provider to use for rendering
-	AudioProvider *provider;
+	agi::AudioProvider *provider;
 	/// Horizontal zoom in milliseconds per pixel
 	double pixel_ms;
 	/// Vertical zoom/amplitude scale factor
@@ -271,7 +271,7 @@ public:
 
 	/// @brief Change audio provider
 	/// @param provider Audio provider to change to
-	void SetProvider(AudioProvider *provider);
+	void SetProvider(agi::AudioProvider *provider);
 
 	/// @brief Change horizontal zoom
 	/// @param pixel_ms Milliseconds per pixel to zoom to
diff --git a/src/audio_renderer_spectrum.cpp b/src/audio_renderer_spectrum.cpp
index 5aedb4b10..8f8eaee90 100644
--- a/src/audio_renderer_spectrum.cpp
+++ b/src/audio_renderer_spectrum.cpp
@@ -38,8 +38,8 @@
 #ifndef WITH_FFTW3
 #include "fft.h"
 #endif
-#include "include/aegisub/audio_provider.h"
 
+#include <libaegisub/audio/provider.h>
 #include <libaegisub/make_unique.h>
 
 #include <algorithm>
diff --git a/src/audio_renderer_waveform.cpp b/src/audio_renderer_waveform.cpp
index 5d848b1d8..d5bb802fb 100644
--- a/src/audio_renderer_waveform.cpp
+++ b/src/audio_renderer_waveform.cpp
@@ -30,9 +30,10 @@
 #include "audio_renderer_waveform.h"
 
 #include "audio_colorscheme.h"
-#include "include/aegisub/audio_provider.h"
 #include "options.h"
 
+#include <libaegisub/audio/provider.h>
+
 #include <algorithm>
 #include <wx/dcmemory.h>
 
diff --git a/src/command/audio.cpp b/src/command/audio.cpp
index af219c5fd..b03a24599 100644
--- a/src/command/audio.cpp
+++ b/src/command/audio.cpp
@@ -37,7 +37,6 @@
 #include "../audio_controller.h"
 #include "../audio_karaoke.h"
 #include "../audio_timing.h"
-#include "../include/aegisub/audio_provider.h"
 #include "../include/aegisub/context.h"
 #include "../libresrc/libresrc.h"
 #include "../options.h"
@@ -46,6 +45,7 @@
 #include "../utils.h"
 #include "../video_controller.h"
 
+#include <libaegisub/audio/provider.h>
 #include <libaegisub/make_unique.h>
 #include <libaegisub/io.h>
 
@@ -159,29 +159,6 @@ struct audio_view_waveform final : public Command {
 	}
 };
 
-class writer {
-	agi::io::Save outfile;
-	std::ostream& out;
-
-public:
-	writer(agi::fs::path const& filename) : outfile(filename, true), out(outfile.Get()) { }
-
-	template<int N>
-	void write(const char(&str)[N]) {
-		out.write(str, N - 1);
-	}
-
-	void write(std::vector<char> const& data) {
-		out.write(data.data(), data.size());
-	}
-
-	template<typename Dest, typename Src>
-	void write(Src v) {
-		auto converted = static_cast<Dest>(v);
-		out.write(reinterpret_cast<char *>(&converted), sizeof(Dest));
-	}
-};
-
 struct audio_save_clip final : public Command {
 	CMD_NAME("audio/save/clip")
 	STR_MENU("Create audio clip")
@@ -206,39 +183,7 @@ struct audio_save_clip final : public Command {
 			end = std::max(end, line->End);
 		}
 
-		auto provider = c->project->AudioProvider();
-
-		auto start_sample = ((int64_t)start * provider->GetSampleRate() + 999) / 1000;
-		auto end_sample = ((int64_t)end * provider->GetSampleRate() + 999) / 1000;
-		if (start_sample >= provider->GetNumSamples() || start_sample >= end_sample) return;
-
-		size_t bytes_per_sample = provider->GetBytesPerSample() * provider->GetChannels();
-		size_t bufsize = (end_sample - start_sample) * bytes_per_sample;
-
-		writer out{filename};
-		out.write("RIFF");
-		out.write<int32_t>(bufsize + 36);
-
-		out.write("WAVEfmt ");
-		out.write<int32_t>(16); // Size of chunk
-		out.write<int16_t>(1);  // compression format (PCM)
-		out.write<int16_t>(provider->GetChannels());
-		out.write<int32_t>(provider->GetSampleRate());
-		out.write<int32_t>(provider->GetSampleRate() * provider->GetChannels() * provider->GetBytesPerSample());
-		out.write<int16_t>(provider->GetChannels() * provider->GetBytesPerSample());
-		out.write<int16_t>(provider->GetBytesPerSample() * 8);
-
-		out.write("data");
-		out.write<int32_t>(bufsize);
-
-		// samples per read
-		size_t spr = 65536 / bytes_per_sample;
-		std::vector<char> buf(bufsize);
-		for (int64_t i = start_sample; i < end_sample; i += spr) {
-			buf.resize(std::min<size_t>(spr, end_sample - i) * bytes_per_sample);
-			provider->GetAudio(&buf[0], i, buf.size());
-			out.write(buf);
-		}
+		agi::SaveAudioClip(c->project->AudioProvider(), filename, start, end);
 	}
 };
 
diff --git a/src/frame_main.cpp b/src/frame_main.cpp
index 3d749900b..ba107d6c1 100644
--- a/src/frame_main.cpp
+++ b/src/frame_main.cpp
@@ -335,7 +335,7 @@ void FrameMain::OnStatusClear(wxTimerEvent &) {
 	SetStatusText("",1);
 }
 
-void FrameMain::OnAudioOpen(AudioProvider *provider) {
+void FrameMain::OnAudioOpen(agi::AudioProvider *provider) {
 	if (provider)
 		SetDisplayMode(-1, 1);
 	else
diff --git a/src/frame_main.h b/src/frame_main.h
index b91d3b2a6..e03295aed 100644
--- a/src/frame_main.h
+++ b/src/frame_main.h
@@ -34,8 +34,8 @@
 class AegisubApp;
 class AsyncVideoProvider;
 class AudioBox;
-class AudioProvider;
 class VideoBox;
+namespace agi { class AudioProvider; }
 namespace agi { struct Context; class OptionValue; }
 
 class FrameMain : public wxFrame {
@@ -63,7 +63,7 @@ class FrameMain : public wxFrame {
 	void OnStatusClear(wxTimerEvent &event);
 	void OnCloseWindow (wxCloseEvent &event);
 
-	void OnAudioOpen(AudioProvider *provider);
+	void OnAudioOpen(agi::AudioProvider *provider);
 	void OnVideoOpen(AsyncVideoProvider *provider);
 	void OnVideoDetach(agi::OptionValue const& opt);
 	void OnSubtitlesOpen();
diff --git a/src/include/aegisub/audio_player.h b/src/include/aegisub/audio_player.h
index 5293e13cd..8e29e1628 100644
--- a/src/include/aegisub/audio_player.h
+++ b/src/include/aegisub/audio_player.h
@@ -34,20 +34,22 @@
 
 #pragma once
 
+#include <libaegisub/exception.h>
+
 #include <cstdint>
 #include <memory>
 #include <string>
 #include <vector>
 
-class AudioProvider;
+namespace agi { class AudioProvider; }
 class wxWindow;
 
 class AudioPlayer {
 protected:
-	AudioProvider *provider;
+	agi::AudioProvider *provider;
 
 public:
-	AudioPlayer(AudioProvider *provider);
+	AudioPlayer(agi::AudioProvider *provider) : provider(provider) { }
 	virtual ~AudioPlayer() = default;
 
 	virtual void Play(int64_t start,int64_t count)=0;	// Play sample range
@@ -63,5 +65,7 @@ public:
 
 struct AudioPlayerFactory {
 	static std::vector<std::string> GetClasses();
-	static std::unique_ptr<AudioPlayer> GetAudioPlayer(AudioProvider *provider, wxWindow *window);
+	static std::unique_ptr<AudioPlayer> GetAudioPlayer(agi::AudioProvider *provider, wxWindow *window);
 };
+
+DEFINE_EXCEPTION(AudioPlayerOpenError, agi::Exception);
diff --git a/src/include/aegisub/audio_provider.h b/src/include/aegisub/audio_provider.h
deleted file mode 100644
index 5ea04c6ea..000000000
--- a/src/include/aegisub/audio_provider.h
+++ /dev/null
@@ -1,99 +0,0 @@
-// Copyright (c) 2006, Rodrigo Braz Monteiro
-// All rights reserved.
-//
-// Redistribution and use in source and binary forms, with or without
-// modification, are permitted provided that the following conditions are met:
-//
-//   * Redistributions of source code must retain the above copyright notice,
-//     this list of conditions and the following disclaimer.
-//   * Redistributions in binary form must reproduce the above copyright notice,
-//     this list of conditions and the following disclaimer in the documentation
-//     and/or other materials provided with the distribution.
-//   * Neither the name of the Aegisub Group nor the names of its contributors
-//     may be used to endorse or promote products derived from this software
-//     without specific prior written permission.
-//
-// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
-// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
-// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
-// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
-// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
-// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
-// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
-// POSSIBILITY OF SUCH DAMAGE.
-//
-// Aegisub Project http://www.aegisub.org/
-
-#pragma once
-
-#include <libaegisub/exception.h>
-#include <libaegisub/fs_fwd.h>
-
-#include <atomic>
-#include <vector>
-
-class AudioProvider {
-protected:
-	int channels;
-
-	/// for one channel, ie. number of PCM frames
-	int64_t num_samples;
-	std::atomic<int64_t> decoded_samples;
-	int sample_rate;
-	int bytes_per_sample;
-	bool float_samples;
-
-	virtual void FillBuffer(void *buf, int64_t start, int64_t count) const = 0;
-
-	void ZeroFill(void *buf, int64_t count) const;
-
-public:
-	virtual ~AudioProvider() = default;
-
-	void GetAudio(void *buf, int64_t start, int64_t count) const;
-	void GetAudioWithVolume(void *buf, int64_t start, int64_t count, double volume) const;
-
-	int64_t       GetNumSamples()     const { return num_samples; }
-	int64_t       GetDecodedSamples() const { return decoded_samples; }
-	int           GetSampleRate()     const { return sample_rate; }
-	int           GetBytesPerSample() const { return bytes_per_sample; }
-	int           GetChannels()       const { return channels; }
-	bool          AreSamplesFloat()   const { return float_samples; }
-
-	/// @brief Does this provider benefit from external caching?
-	virtual bool NeedsCache() const { return false; }
-};
-
-/// Helper base class for an audio provider which wraps another provider
-class AudioProviderWrapper : public AudioProvider {
-protected:
-	std::unique_ptr<AudioProvider> source;
-public:
-	AudioProviderWrapper(std::unique_ptr<AudioProvider> src)
-	: source(std::move(src))
-	{
-		channels = source->GetChannels();
-		num_samples = source->GetNumSamples();
-		decoded_samples = source->GetDecodedSamples();
-		sample_rate = source->GetSampleRate();
-		bytes_per_sample = source->GetBytesPerSample();
-		float_samples = source->AreSamplesFloat();
-	}
-};
-
-namespace agi { class BackgroundRunner; }
-
-struct AudioProviderFactory {
-	static std::vector<std::string> GetClasses();
-
-	/// Get a provider for the file
-	/// @param filename URI to open
-	static std::unique_ptr<AudioProvider> GetProvider(agi::fs::path const& filename, agi::BackgroundRunner *br);
-};
-
-DEFINE_EXCEPTION(AudioProviderError, agi::Exception);
-/// Error of some sort occurred while decoding a frame
-DEFINE_EXCEPTION(AudioDecodeError, AudioProviderError);
diff --git a/src/preferences.cpp b/src/preferences.cpp
index aa66b5f19..e1c2b190c 100644
--- a/src/preferences.cpp
+++ b/src/preferences.cpp
@@ -19,13 +19,13 @@
 #include "preferences.h"
 
 #include "ass_style_storage.h"
+#include "audio_provider_factory.h"
 #include "audio_renderer_waveform.h"
 #include "command/command.h"
 #include "compat.h"
 #include "help_button.h"
 #include "hotkey_data_view_model.h"
 #include "include/aegisub/audio_player.h"
-#include "include/aegisub/audio_provider.h"
 #include "include/aegisub/hotkey.h"
 #include "include/aegisub/subtitles_provider.h"
 #include "libresrc/libresrc.h"
@@ -356,7 +356,7 @@ void Advanced_Audio(wxTreebook *book, Preferences *parent) {
 
 	auto expert = p->PageSizer(_("Expert"));
 
-	wxArrayString ap_choice = to_wx(AudioProviderFactory::GetClasses());
+	wxArrayString ap_choice = to_wx(GetAudioProviderNames());
 	p->OptionChoice(expert, _("Audio provider"), ap_choice, "Audio/Provider");
 
 	wxArrayString apl_choice = to_wx(AudioPlayerFactory::GetClasses());
diff --git a/src/project.cpp b/src/project.cpp
index 90282ccfd..21bfe23ce 100644
--- a/src/project.cpp
+++ b/src/project.cpp
@@ -20,13 +20,13 @@
 #include "ass_file.h"
 #include "async_video_provider.h"
 #include "audio_controller.h"
+#include "audio_provider_factory.h"
 #include "base_grid.h"
 #include "charset_detect.h"
 #include "compat.h"
 #include "dialog_progress.h"
 #include "dialogs.h"
 #include "format.h"
-#include "include/aegisub/audio_provider.h"
 #include "include/aegisub/context.h"
 #include "include/aegisub/video_provider.h"
 #include "mkv_wrap.h"
@@ -37,6 +37,7 @@
 #include "video_controller.h"
 #include "video_display.h"
 
+#include <libaegisub/audio/provider.h>
 #include <libaegisub/format_path.h>
 #include <libaegisub/fs.h>
 #include <libaegisub/keyframe.h>
@@ -243,7 +244,7 @@ void Project::DoLoadAudio(agi::fs::path const& path, bool quiet) {
 
 	try {
 		try {
-			audio_provider = AudioProviderFactory::GetProvider(path, progress);
+			audio_provider = GetAudioProvider(path, progress);
 		}
 		catch (agi::UserCancelException const&) { return; }
 		catch (...) {
@@ -254,7 +255,7 @@ void Project::DoLoadAudio(agi::fs::path const& path, bool quiet) {
 	catch (agi::fs::FileNotFound const& e) {
 		return ShowError(_("The audio file was not found: ") + to_wx(e.GetMessage()));
 	}
-	catch (agi::AudioDataNotFoundError const& e) {
+	catch (agi::AudioDataNotFound const& e) {
 		if (quiet) {
 			LOG_D("video/open/audio") << "File " << video_file << " has no audio data: " << e.GetMessage();
 			return;
@@ -262,7 +263,7 @@ void Project::DoLoadAudio(agi::fs::path const& path, bool quiet) {
 		else
 			return ShowError(_("None of the available audio providers recognised the selected file as containing audio data.\n\nThe following providers were tried:\n") + to_wx(e.GetMessage()));
 	}
-	catch (agi::AudioProviderOpenError const& e) {
+	catch (agi::AudioProviderError const& e) {
 		return ShowError(_("None of the available audio providers have a codec available to handle the selected file.\n\nThe following providers were tried:\n") + to_wx(e.GetMessage()));
 	}
 	catch (agi::Exception const& e) {
diff --git a/src/project.h b/src/project.h
index b51cf4dc9..1fd034d08 100644
--- a/src/project.h
+++ b/src/project.h
@@ -23,14 +23,14 @@
 #include <vector>
 
 class AsyncVideoProvider;
-class AudioProvider;
 class DialogProgress;
 class wxString;
+namespace agi { class AudioProvider; }
 namespace agi { struct Context; }
 struct ProjectProperties;
 
 class Project {
-	std::unique_ptr<::AudioProvider> audio_provider;
+	std::unique_ptr<agi::AudioProvider> audio_provider;
 	std::unique_ptr<AsyncVideoProvider> video_provider;
 	agi::vfr::Framerate timecodes;
 	std::vector<int> keyframes;
@@ -40,7 +40,7 @@ class Project {
 	agi::fs::path timecodes_file;
 	agi::fs::path keyframes_file;
 
-	agi::signal::Signal<::AudioProvider *> AnnounceAudioProviderModified;
+	agi::signal::Signal<agi::AudioProvider *> AnnounceAudioProviderModified;
 	agi::signal::Signal<AsyncVideoProvider *> AnnounceVideoProviderModified;
 	agi::signal::Signal<agi::vfr::Framerate const&> AnnounceTimecodesModified;
 	agi::signal::Signal<std::vector<int> const&> AnnounceKeyframesModified;
@@ -75,7 +75,7 @@ public:
 
 	void LoadAudio(agi::fs::path path);
 	void CloseAudio();
-	::AudioProvider *AudioProvider() const { return audio_provider.get(); }
+	agi::AudioProvider *AudioProvider() const { return audio_provider.get(); }
 	agi::fs::path const& AudioName() const { return audio_file; }
 
 	void LoadVideo(agi::fs::path path);
diff --git a/tests/tests/audio.cpp b/tests/tests/audio.cpp
new file mode 100644
index 000000000..b30d69ffb
--- /dev/null
+++ b/tests/tests/audio.cpp
@@ -0,0 +1,356 @@
+// Copyright (c) 2014, Thomas Goyne <plorkyeran@aegisub.org>
+//
+// Permission to use, copy, modify, and distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+//
+// Aegisub Project http://www.aegisub.org/
+
+#include <main.h>
+
+#include <libaegisub/audio/provider.h>
+#include <libaegisub/fs.h>
+#include <libaegisub/make_unique.h>
+#include <libaegisub/path.h>
+#include <libaegisub/util.h>
+
+#include <boost/filesystem/fstream.hpp>
+
+namespace bfs = boost::filesystem;
+
+TEST(lagi_audio, dummy_blank) {
+	auto provider = agi::CreateDummyAudioProvider("dummy-audio:", nullptr);
+
+	char buff[1024];
+	memset(buff, sizeof(buff), 1);
+	provider->GetAudio(buff, 12356, 512);
+	for (size_t i = 0; i < sizeof(buff); ++i) ASSERT_EQ(0, buff[i]);
+}
+
+TEST(lagi_audio, dummy_noise) {
+	auto provider = agi::CreateDummyAudioProvider("dummy-audio:noise?", nullptr);
+
+	char buff[1024];
+	memset(buff, sizeof(buff), 0);
+	provider->GetAudio(buff, 12356, 512);
+	for (size_t i = 0; i < sizeof(buff); ++i) {
+		if (buff[i] != 0)
+			return;
+	}
+	bool all_zero = true;
+	ASSERT_FALSE(all_zero);
+}
+
+TEST(lagi_audio, dummy_rejects_non_dummy_url) {
+	auto provider = agi::CreateDummyAudioProvider("/tmp", nullptr);
+	ASSERT_EQ(nullptr, provider.get());
+}
+
+struct TestAudioProvider : agi::AudioProvider {
+	TestAudioProvider(int64_t duration = 90) {
+		channels = 1;
+		num_samples = duration * 48000;
+		decoded_samples = num_samples;
+		sample_rate = 48000;
+		bytes_per_sample = 2;
+		float_samples = false;
+	}
+
+	void FillBuffer(void *buf, int64_t start, int64_t count) const override {
+		auto out = static_cast<uint16_t *>(buf);
+		for (int64_t end = start + count; start < end; ++start)
+			*out++ = (uint16_t)start;
+	}
+};
+
+TEST(lagi_audio, before_sample_zero) {
+	TestAudioProvider provider;
+
+	uint16_t buff[16];
+	memset(buff, sizeof(buff), 1);
+	provider.GetAudio(buff, -8, 16);
+
+	for (int i = 0; i < 8; ++i)
+		ASSERT_EQ(0, buff[i]);
+	for (int i = 8; i < 16; ++i)
+		ASSERT_EQ(i - 8, buff[i]);
+}
+
+TEST(lagi_audio, after_end) {
+	TestAudioProvider provider(1);
+
+	uint16_t buff[16];
+	memset(buff, sizeof(buff), 1);
+	provider.GetAudio(buff, provider.GetNumSamples() - 8, 16);
+
+	for (int i = 0; i < 8; ++i)
+		ASSERT_NE(0, buff[i]);
+	for (int i = 8; i < 16; ++i)
+		ASSERT_EQ(0, buff[i]);
+}
+
+TEST(lagi_audio, save_audio_clip) {
+	auto path = agi::Path().Decode("?temp/save_clip");
+	agi::fs::Remove(path);
+
+	auto provider = agi::CreateDummyAudioProvider("dummy-audio:noise?", nullptr);
+	agi::SaveAudioClip(provider.get(), path, 60 * 60 * 1000, (60 * 60 + 10) * 1000);
+
+	{
+		bfs::ifstream s(path);
+		ASSERT_TRUE(s.good());
+		s.seekg(0, std::ios::end);
+		// 10 seconds of 44.1 kHz samples per second of 16-bit mono, plus 44 bytes of header
+		EXPECT_EQ(10 * 44100 * 2 + 44, s.tellg());
+	}
+	agi::fs::Remove(path);
+}
+
+TEST(lagi_audio, get_with_volume) {
+	TestAudioProvider provider;
+	uint16_t buff[4];
+
+	provider.GetAudioWithVolume(buff, 0, 4, 1.0);
+	EXPECT_EQ(0, buff[0]);
+	EXPECT_EQ(1, buff[1]);
+	EXPECT_EQ(2, buff[2]);
+	EXPECT_EQ(3, buff[3]);
+
+	provider.GetAudioWithVolume(buff, 0, 4, 0.0);
+	EXPECT_EQ(0, buff[0]);
+	EXPECT_EQ(0, buff[1]);
+	EXPECT_EQ(0, buff[2]);
+	EXPECT_EQ(0, buff[3]);
+
+	provider.GetAudioWithVolume(buff, 0, 4, 2.0);
+	EXPECT_EQ(0, buff[0]);
+	EXPECT_EQ(2, buff[1]);
+	EXPECT_EQ(4, buff[2]);
+	EXPECT_EQ(6, buff[3]);
+}
+
+TEST(lagi_audio, volume_should_clamp_rather_than_wrap) {
+	TestAudioProvider provider;
+	uint16_t buff[1];
+	provider.GetAudioWithVolume(buff, 30000, 1, 2.0);
+	EXPECT_EQ(SHRT_MAX, buff[0]);
+}
+
+TEST(lagi_audio, ram_cache) {
+	auto provider = agi::CreateRAMAudioProvider(agi::make_unique<TestAudioProvider>());
+	EXPECT_EQ(1, provider->GetChannels());
+	EXPECT_EQ(90 * 48000, provider->GetNumSamples());
+	EXPECT_EQ(48000, provider->GetSampleRate());
+	EXPECT_EQ(2, provider->GetBytesPerSample());
+	EXPECT_EQ(false, provider->AreSamplesFloat());
+	EXPECT_EQ(false, provider->NeedsCache());
+	while (provider->GetDecodedSamples() != provider->GetNumSamples()) agi::util::sleep_for(0);
+
+	uint16_t buff[512];
+	provider->GetAudio(buff, (1 << 22) - 256, 512); // Stride two cache blocks
+
+	for (size_t i = 0; i < 512; ++i)
+		ASSERT_EQ(static_cast<uint16_t>((1 << 22) - 256 + i), buff[i]);
+}
+
+TEST(lagi_audio, hd_cache) {
+	auto provider = agi::CreateHDAudioProvider(agi::make_unique<TestAudioProvider>(), agi::Path().Decode("?temp"));
+	while (provider->GetDecodedSamples() != provider->GetNumSamples()) agi::util::sleep_for(0);
+
+	uint16_t buff[512];
+	provider->GetAudio(buff, (1 << 22) - 256, 512);
+
+	for (size_t i = 0; i < 512; ++i)
+		ASSERT_EQ(static_cast<uint16_t>((1 << 22) - 256 + i), buff[i]);
+}
+
+TEST(lagi_audio, pcm_simple) {
+	auto path = agi::Path().Decode("?temp/pcm_simple");
+	{
+		TestAudioProvider provider;
+		agi::SaveAudioClip(&provider, path, 0, 1000);
+	}
+
+	auto provider = agi::CreatePCMAudioProvider(path, nullptr);
+	EXPECT_EQ(1, provider->GetChannels());
+	EXPECT_EQ(48000, provider->GetNumSamples());
+	EXPECT_EQ(48000, provider->GetSampleRate());
+	EXPECT_EQ(2, provider->GetBytesPerSample());
+	EXPECT_EQ(false, provider->AreSamplesFloat());
+	EXPECT_EQ(false, provider->NeedsCache());
+
+	for (int i = 0; i < 100; ++i) {
+		uint16_t sample;
+		provider->GetAudio(&sample, i, 1);
+		ASSERT_EQ(i, sample);
+	}
+
+	agi::fs::Remove(path);
+}
+
+TEST(lagi_audio, pcm_truncated) {
+	auto path = agi::Path().Decode("?temp/pcm_truncated");
+	{
+		TestAudioProvider provider;
+		agi::SaveAudioClip(&provider, path, 0, 1000);
+	}
+
+	char file[1000];
+
+	{ bfs::ifstream s(path); s.read(file, sizeof file); }
+	{ bfs::ofstream s(path); s.write(file, sizeof file); }
+
+	auto provider = agi::CreatePCMAudioProvider(path, nullptr);
+
+	// Should still report full duration
+	EXPECT_EQ(48000, provider->GetNumSamples());
+
+	// And should zero-pad past the end
+	auto sample_count = (1000 - 44) / 2;
+	uint16_t sample;
+
+	provider->GetAudio(&sample, sample_count - 1, 1);
+	EXPECT_EQ(sample_count - 1, sample);
+
+	provider->GetAudio(&sample, sample_count, 1);
+	EXPECT_EQ(0, sample);
+
+	agi::fs::Remove(path);
+}
+
+#define RIFF "RIFF\0\0\0\x60WAVE"
+#define FMT_VALID "fmt \20\0\0\0\1\0\1\0\20\0\0\0\0\0\0\0\0\0\20\0"
+#define DATA_VALID "data\1\0\0\0\0\0"
+#define WRITE(str) do { bfs::ofstream s(path); s.write(str, sizeof(str) - 1); } while (false)
+
+TEST(lagi_audio, pcm_incomplete) {
+	auto path = agi::Path().Decode("?temp/pcm_incomplete");
+
+	agi::fs::Remove(path);
+	ASSERT_THROW(agi::CreatePCMAudioProvider(path, nullptr), agi::fs::FileNotFound);
+
+	bfs::ofstream{path};
+	ASSERT_THROW(agi::CreatePCMAudioProvider(path, nullptr), agi::AudioDataNotFound);
+
+	// Invalid tags
+	WRITE("ASDF");
+	ASSERT_THROW(agi::CreatePCMAudioProvider(path, nullptr), agi::AudioDataNotFound);
+
+	WRITE("RIFF\0\0\0\x60" "ASDF");
+	ASSERT_THROW(agi::CreatePCMAudioProvider(path, nullptr), agi::AudioDataNotFound);
+
+	// Incomplete files
+	auto valid_file = RIFF FMT_VALID DATA_VALID;
+
+	// -1 for nul term, -3 so that longest file is still invalid
+	for (size_t i = 0; i < sizeof(valid_file) - 4; ++i) {
+		bfs::ofstream s(path);
+		s.write(valid_file, i);
+		ASSERT_THROW(agi::CreatePCMAudioProvider(path, nullptr), agi::AudioDataNotFound);
+	}
+
+	// fmt must come before data
+	WRITE(RIFF "data\0\0\0\x60");
+	ASSERT_THROW(agi::CreatePCMAudioProvider(path, nullptr), agi::AudioProviderError);
+
+	// Bad compression format
+	WRITE(RIFF "fmt \x60\0\0\0\2\0");
+	ASSERT_THROW(agi::CreatePCMAudioProvider(path, nullptr), agi::AudioProviderError);
+
+	// Multiple fmt chunks not supported
+	WRITE(RIFF FMT_VALID FMT_VALID);
+	ASSERT_THROW(agi::CreatePCMAudioProvider(path, nullptr), agi::AudioProviderError);
+
+	agi::fs::Remove(path);
+}
+
+TEST(lagi_audio, multiple_data_chunks) {
+	auto path = agi::Path().Decode("?temp/multiple_data");
+
+	WRITE(RIFF FMT_VALID "data\2\0\0\0\1\0" "data\2\0\0\0\2\0" "data\2\0\0\0\3\0");
+
+	auto provider = agi::CreatePCMAudioProvider(path, nullptr);
+	ASSERT_EQ(3, provider->GetNumSamples());
+
+	uint16_t samples[3];
+
+	provider->GetAudio(samples, 0, 3);
+	EXPECT_EQ(1, samples[0]);
+	EXPECT_EQ(2, samples[1]);
+	EXPECT_EQ(3, samples[2]);
+
+	samples[1] = 5;
+	provider->GetAudio(samples, 2, 1);
+	EXPECT_EQ(3, samples[0]);
+	EXPECT_EQ(5, samples[1]);
+
+	provider->GetAudio(samples, 1, 1);
+	EXPECT_EQ(2, samples[0]);
+	EXPECT_EQ(5, samples[1]);
+
+	provider->GetAudio(samples, 0, 1);
+	EXPECT_EQ(1, samples[0]);
+	EXPECT_EQ(5, samples[1]);
+
+	agi::fs::Remove(path);
+}
+
+#define WAVE64_FILE \
+	"riff\x2e\x91\xcf\x11\xa5\xd6\x28\xdb\x04\xc1\x00\x00"   /* RIFF GUID */          \
+	"\x74\x00\0\0\0\0\0\0"                                   /* file size */          \
+	"wave\xf3\xac\xd3\x11\x8c\xd1\x00\xc0\x4f\x8e\xdb\x8a"   /* WAVE GUID */          \
+	"fmt \xf3\xac\xd3\x11\x8c\xd1\x00\xc0\x4f\x8e\xdb\x8a"   /* fmt GUID */           \
+	"\x30\x00\0\0\0\0\0\0"                                   /* fmt chunk size */     \
+	"\1\0\1\0\x10\0\0\0\x20\0\0\0\2\0\x10\0\0\0\0\0\0\0\0\0" /* fmt chunk */          \
+	"data\xf3\xac\xd3\x11\x8c\xd1\x00\xc0\x4f\x8e\xdb\x8a"   /* data GUID */          \
+	"\x1c\0\0\0\0\0\0\0"                                     /* data chunk size */    \
+	"\1\0\2\0"                                               /* actual sample data */ \
+
+TEST(lagi_audio, wave64_simple) {
+	auto path = agi::Path().Decode("?temp/w64_valid");
+	WRITE(WAVE64_FILE);
+
+	auto provider = agi::CreatePCMAudioProvider(path, nullptr);
+	ASSERT_EQ(2, provider->GetNumSamples());
+
+	uint16_t samples[2];
+	provider->GetAudio(samples, 0, 2);
+	EXPECT_EQ(1, samples[0]);
+	EXPECT_EQ(2, samples[1]);
+
+	agi::fs::Remove(path);
+}
+
+TEST(lagi_audio, wave64_truncated) {
+	auto path = agi::Path().Decode("?temp/w64_truncated");
+
+	// Should be invalid until there's an entire sample
+	for (size_t i = 0; i < sizeof(WAVE64_FILE) - 4; ++i) {
+		bfs::ofstream s(path);
+		s.write(WAVE64_FILE, i);
+		ASSERT_THROW(agi::CreatePCMAudioProvider(path, nullptr), agi::AudioDataNotFound);
+	}
+
+	{
+		bfs::ofstream s(path);
+		s.write(WAVE64_FILE, sizeof(WAVE64_FILE) - 3);
+	}
+	ASSERT_NO_THROW(agi::CreatePCMAudioProvider(path, nullptr));
+
+	{
+		auto provider = agi::CreatePCMAudioProvider(path, nullptr);
+		uint16_t sample;
+		provider->GetAudio(&sample, 0, 1);
+		EXPECT_EQ(1, sample);
+	}
+
+	agi::fs::Remove(path);
+}
diff --git a/tests/tests/iconv.cpp b/tests/tests/iconv.cpp
index 6d7197cb1..cfec40f7b 100644
--- a/tests/tests/iconv.cpp
+++ b/tests/tests/iconv.cpp
@@ -135,7 +135,7 @@ TEST(lagi_iconv, wchar_tSupport) {
 }
 
 TEST(lagi_iconv, Roundtrip) {
-    for (auto const& name : GetEncodingsList<std::vector<std::string>>()) {
+	for (auto const& name : GetEncodingsList<std::vector<std::string>>()) {
 		ASSERT_NO_THROW(IconvWrapper("utf-8", name.c_str()));
 		ASSERT_NO_THROW(IconvWrapper(name.c_str(), "utf-8"));
 		EXPECT_EQ(
-- 
GitLab