Repository: nfrechette/acl (cpp)

Issue:
Implement non-hierarchical track support
It is common to have animation clips also add animated data that isn't a bone. Things like IK weights, blend shape weights, etc. We should support compressing stand-alone tracks of type: float1, float2, float3, float4.

No segmenting support for now.

API description:

*  raw_track_list (contains a list of raw_track which contains a track and track_description)
*  compressed_track_list (mostly opaque type?)
*  track_description (track type, error threshold, etc)
*  float1_track, float2_track, etc
*  quat_track
*  transform_track
*  decompress_track(...)
*  decompress_track_list(...)

A regression test should be added.


Discussion:
Progress in on-going and this should land in develop sometime in August.

Patch:
diff --git a/.gitmodules b/.gitmodules
--- a/.gitmodules
+++ b/.gitmodules
@@ -5,3 +5,7 @@
 [submodule "external/catch2"]
 	path = external/catch2
 	url = https://github.com/catchorg/Catch2
+[submodule "external/rtm"]
+	path = external/rtm
+	url = https://github.com/nfrechette/rtm.git
+	branch = develop
diff --git a/cmake/CMakeCompiler.cmake b/cmake/CMakeCompiler.cmake
--- a/cmake/CMakeCompiler.cmake
+++ b/cmake/CMakeCompiler.cmake
@@ -21,6 +21,7 @@ macro(setup_default_compiler_flags _project_name)
 			endif()
 		else()
 			add_definitions(-DACL_NO_INTRINSICS)
+			add_definitions(-DRTM_NO_INTRINSICS)
 		endif()
 
 		if(USE_POPCNT_INSTRUCTIONS)
@@ -49,6 +50,7 @@ macro(setup_default_compiler_flags _project_name)
 				endif()
 			else()
 				add_definitions(-DACL_NO_INTRINSICS)
+				add_definitions(-DRTM_NO_INTRINSICS)
 			endif()
 
 			if(USE_POPCNT_INSTRUCTIONS)
diff --git a/external/README.md b/external/README.md
--- a/external/README.md
+++ b/external/README.md
@@ -1,13 +1,19 @@
 # External dependencies
 
-Good news! There are no external dependencies needed by this library at runtime unless you use the [ACL file format](../docs/the_acl_file_format.md)!
+## Runtime dependencies
 
-## Catch2
+### Realtime Math
+
+[Reamtime Math v1.0.0-develop](https://github.com/nfrechette/rtm/releases/tag/v1.0.0) (MIT License) is used for some math types and functions. Its usage is currently limited but a full transition to use it exclusively will occur for ACL v2.0.
+
+## Development dependencies
+
+### Catch2
 
 [Catch2 v2.5.0](https://github.com/catchorg/Catch2/releases/tag/v2.5.0) (Boost Software License v1.0) is used by our [unit tests](../tests). You will only need it if you run the unit tests and it is included as-is without modifications.
 
-## sjson-cpp
+### sjson-cpp
 
-[sjson-cpp v0.6.0](https://github.com/nfrechette/sjson-cpp/releases/tag/v0.6.0) (MIT License) is used by our ACL file format [clip reader](../includes/acl/io/clip_reader.h) and [clip writer](../includes/acl/io/clip_writer.h) as well as by the [acl_compressor](../tools/acl_compressor) tool used for regression testing and profiling. Unless you use our ACL file format at runtime (which you shouldn't), you will not have this dependency included at all.
+[sjson-cpp v0.6.0-develop](https://github.com/nfrechette/sjson-cpp/releases/tag/v0.6.0) (MIT License) is used by our [ACL file format](../docs/the_acl_file_format.md) [clip reader](../includes/acl/io/clip_reader.h) and [clip writer](../includes/acl/io/clip_writer.h) as well as by the [acl_compressor](../tools/acl_compressor) tool used for regression testing and profiling. Unless you use our ACL file format at runtime (which you shouldn't), you will not have this dependency included at all.
 
 In fact, to use any of these things you must include `sjson-cpp` relevant headers manually before you include the ACL headers that need them. For convenience, you can use the included version here or your own version as long as the API remains compatible.
diff --git a/external/rtm b/external/rtm
new file mode 160000
--- /dev/null
+++ b/external/rtm
@@ -0,0 +1 @@
+Subproject commit 50cc1f278f0a8e37ca845c71db2440eb39e98677
diff --git a/external/sjson-cpp b/external/sjson-cpp
--- a/external/sjson-cpp
+++ b/external/sjson-cpp
@@ -1 +1 @@
-Subproject commit 7813d990aef4416f5517b9fb9e66033dfad661ae
+Subproject commit 7395b10557f5ade3d1ab826db1adfb052050dea5
diff --git a/includes/acl/algorithm/uniformly_sampled/decoder.h b/includes/acl/algorithm/uniformly_sampled/decoder.h
--- a/includes/acl/algorithm/uniformly_sampled/decoder.h
+++ b/includes/acl/algorithm/uniformly_sampled/decoder.h
@@ -269,7 +269,7 @@ namespace acl
 		// allows various decompression actions to be performed in a clip.
 		//
 		// Both the constructor and destructor are public because it is safe to place
-		// instances of this context on the stack or as members variables.
+		// instances of this context on the stack or as member variables.
 		//
 		// This compression algorithm is the simplest by far and as such it offers
 		// the fastest compression and decompression. Every sample is retained and
diff --git a/includes/acl/compression/compress.h b/includes/acl/compression/compress.h
new file mode 100644
--- /dev/null
+++ b/includes/acl/compression/compress.h
@@ -0,0 +1,170 @@
+#pragma once
+
+////////////////////////////////////////////////////////////////////////////////
+// The MIT License (MIT)
+//
+// Copyright (c) 2019 Nicholas Frechette & Animation Compression Library contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+////////////////////////////////////////////////////////////////////////////////
+
+#include "acl/core/compiler_utils.h"
+#include "acl/core/compressed_tracks.h"
+#include "acl/core/error.h"
+#include "acl/core/error_result.h"
+#include "acl/core/iallocator.h"
+#include "acl/core/scope_profiler.h"
+#include "acl/compression/compression_settings.h"
+#include "acl/compression/output_stats.h"
+#include "acl/compression/track_array.h"
+#include "acl/compression/impl/constant_track_impl.h"
+#include "acl/compression/impl/normalize_track_impl.h"
+#include "acl/compression/impl/quantize_track_impl.h"
+#include "acl/compression/impl/track_list_context.h"
+#include "acl/compression/impl/track_range_impl.h"
+#include "acl/compression/impl/write_compression_stats_impl.h"
+#include "acl/compression/impl/write_track_data_impl.h"
+
+#include <cstdint>
+
+ACL_IMPL_FILE_PRAGMA_PUSH
+
+namespace acl
+{
+	//////////////////////////////////////////////////////////////////////////
+	// Compresses a track array with uniform sampling.
+	//
+	// This compression algorithm is the simplest by far and as such it offers
+	// the fastest compression and decompression. Every sample is retained and
+	// every track has the same number of samples playing back at the same
+	// sample rate. This means that when we sample at a particular time within
+	// the clip, we can trivially calculate the offsets required to read the
+	// desired data. All the data is sorted in order to ensure all reads are
+	// as contiguous as possible for optimal cache locality during decompression.
+	//
+	//    allocator:				The allocator instance to use to allocate and free memory.
+	//    track_list:				The track list to compress.
+	//    settings:					The compression settings to use.
+	//    out_compressed_tracks:	The resulting compressed tracks. The caller owns the returned memory and must free it.
+	//    out_stats:				Stat output structure.
+	//////////////////////////////////////////////////////////////////////////
+	inline ErrorResult compress_track_list(IAllocator& allocator, const track_array& track_list, const compression_settings& settings, compressed_tracks*& out_compressed_tracks, OutputStats& out_stats)
+	{
+		using namespace acl_impl;
+
+		(void)settings;	// todo?
+
+		ErrorResult error_result = track_list.is_valid();
+		if (error_result.any())
+			return error_result;
+
+		ScopeProfiler compression_time;
+
+		track_list_context context;
+		initialize_context(allocator, track_list, context);
+
+		extract_track_ranges(context);
+		extract_constant_tracks(context);
+		normalize_tracks(context);
+		quantize_tracks(context);
+
+		// Done transforming our input tracks, time to pack them into their final form
+		const uint32_t per_track_metadata_size = write_track_metadata(context, nullptr);
+		const uint32_t constant_values_size = write_track_constant_values(context, nullptr);
+		const uint32_t range_values_size = write_track_range_values(context, nullptr);
+		const uint32_t animated_num_bits = write_track_animated_values(context, nullptr);
+		const uint32_t animated_values_size = (animated_num_bits + 7) / 8;		// Round up to nearest byte
+		const uint32_t num_bits_per_frame = animated_num_bits / context.num_samples;
+
+		uint32_t buffer_size = 0;
+		buffer_size += sizeof(compressed_tracks);								// Headers
+		ACL_ASSERT(is_aligned_to(buffer_size, alignof(track_metadata)), "Invalid alignment");
+		buffer_size += per_track_metadata_size;									// Per track metadata
+		buffer_size = align_to(buffer_size, 4);									// Align constant values
+		buffer_size += constant_values_size;									// Constant values
+		ACL_ASSERT(is_aligned_to(buffer_size, 4), "Invalid alignment");
+		buffer_size += range_values_size;										// Range values
+		ACL_ASSERT(is_aligned_to(buffer_size, 4), "Invalid alignment");
+		buffer_size += animated_values_size;									// Animated values
+		buffer_size += 15;														// Ensure we have sufficient padding for unaligned 16 byte loads
+
+		uint8_t* buffer = allocate_type_array_aligned<uint8_t>(allocator, buffer_size, alignof(compressed_tracks));
+		std::memset(buffer, 0, buffer_size);
+
+		const uint8_t* buffer_start = buffer;
+		out_compressed_tracks = reinterpret_cast<compressed_tracks*>(buffer);
+
+		raw_buffer_header* buffer_header = safe_ptr_cast<raw_buffer_header>(buffer);
+		buffer += sizeof(raw_buffer_header);
+
+		tracks_header* header = safe_ptr_cast<tracks_header>(buffer);
+		buffer += sizeof(tracks_header);
+
+		// Write our primary header
+		header->tag = k_compressed_tracks_tag;
+		header->version = get_algorithm_version(AlgorithmType8::UniformlySampled);
+		header->algorithm_type = AlgorithmType8::UniformlySampled;
+		header->track_type = track_list.get_track_type();
+		header->num_tracks = context.num_tracks;
+		header->num_samples = context.num_samples;
+		header->sample_rate = context.sample_rate;
+		header->num_bits_per_frame = num_bits_per_frame;
+
+		header->metadata_per_track = buffer - buffer_start;
+		buffer += per_track_metadata_size;
+		buffer = align_to(buffer, 4);
+		header->track_constant_values = buffer - buffer_start;
+		buffer += constant_values_size;
+		header->track_range_values = buffer - buffer_start;
+		buffer += range_values_size;
+		header->track_animated_values = buffer - buffer_start;
+		buffer += animated_values_size;
+		buffer += 15;
+
+		ACL_ASSERT(buffer_start + buffer_size == buffer, "Buffer size and pointer mismatch");
+
+		// Write our compressed data
+		track_metadata* per_track_metadata = header->get_track_metadata();
+		write_track_metadata(context, per_track_metadata);
+
+		float* constant_values = header->get_track_constant_values();
+		write_track_constant_values(context, constant_values);
+
+		float* range_values = header->get_track_range_values();
+		write_track_range_values(context, range_values);
+
+		uint8_t* animated_values = header->get_track_animated_values();
+		write_track_animated_values(context, animated_values);
+
+		// Finish the raw buffer header
+		buffer_header->size = buffer_size;
+		buffer_header->hash = hash32(safe_ptr_cast<const uint8_t>(header), buffer_size - sizeof(raw_buffer_header));	// Hash everything but the raw buffer header
+
+		compression_time.stop();
+
+#if defined(SJSON_CPP_WRITER)
+		if (out_stats.logging != StatLogging::None)
+			write_compression_stats(context, *out_compressed_tracks, compression_time, out_stats);
+#endif
+
+		return ErrorResult();
+	}
+}
+
+ACL_IMPL_FILE_PRAGMA_POP
diff --git a/includes/acl/compression/compression_settings.h b/includes/acl/compression/compression_settings.h
--- a/includes/acl/compression/compression_settings.h
+++ b/includes/acl/compression/compression_settings.h
@@ -269,6 +269,15 @@ namespace acl
 		settings.segmenting.range_reduction = RangeReductionFlags8::AllTracks;
 		return settings;
 	}
+
+	//////////////////////////////////////////////////////////////////////////
+	// Encapsulates all the compression settings.
+	// Note: Currently only used by scalar track compression which contain no global settings.
+	struct compression_settings
+	{
+		compression_settings()
+		{}
+	};
 }
 
 ACL_IMPL_FILE_PRAGMA_POP
diff --git a/includes/acl/compression/impl/constant_track_impl.h b/includes/acl/compression/impl/constant_track_impl.h
new file mode 100644
--- /dev/null
+++ b/includes/acl/compression/impl/constant_track_impl.h
@@ -0,0 +1,77 @@
+#pragma once
+
+////////////////////////////////////////////////////////////////////////////////
+// The MIT License (MIT)
+//
+// Copyright (c) 2019 Nicholas Frechette & Animation Compression Library contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+////////////////////////////////////////////////////////////////////////////////
+
+#include "acl/core/bitset.h"
+#include "acl/core/compiler_utils.h"
+#include "acl/core/iallocator.h"
+#include "acl/compression/impl/track_list_context.h"
+
+#include <cstdint>
+
+ACL_IMPL_FILE_PRAGMA_PUSH
+
+namespace acl
+{
+	namespace acl_impl
+	{
+		inline bool is_scalarf_track_constant(const track& track_, const track_range& range)
+		{
+			const track_desc_scalarf& desc = track_.get_description<track_desc_scalarf>();
+			return range.is_constant(desc.constant_threshold);
+		}
+
+		inline void extract_constant_tracks(track_list_context& context)
+		{
+			ACL_ASSERT(context.is_valid(), "Invalid context");
+
+			const BitSetDescription bitset_desc = BitSetDescription::make_from_num_bits(context.num_tracks);
+
+			context.constant_tracks_bitset = allocate_type_array<uint32_t>(*context.allocator, bitset_desc.get_size());
+			bitset_reset(context.constant_tracks_bitset, bitset_desc, false);
+
+			for (uint32_t track_index = 0; track_index < context.num_tracks; ++track_index)
+			{
+				const track& mut_track = context.track_list[track_index];
+				const track_range& range = context.range_list[track_index];
+
+				bool is_constant = false;
+				switch (range.category)
+				{
+				case track_category8::scalarf:
+					is_constant = is_scalarf_track_constant(mut_track, range);
+					break;
+				default:
+					ACL_ASSERT(false, "Invalid track category");
+					break;
+				}
+
+				bitset_set(context.constant_tracks_bitset, bitset_desc, track_index, is_constant);
+			}
+		}
+	}
+}
+
+ACL_IMPL_FILE_PRAGMA_POP
diff --git a/includes/acl/compression/impl/normalize_track_impl.h b/includes/acl/compression/impl/normalize_track_impl.h
new file mode 100644
--- /dev/null
+++ b/includes/acl/compression/impl/normalize_track_impl.h
@@ -0,0 +1,104 @@
+#pragma once
+
+////////////////////////////////////////////////////////////////////////////////
+// The MIT License (MIT)
+//
+// Copyright (c) 2019 Nicholas Frechette & Animation Compression Library contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+////////////////////////////////////////////////////////////////////////////////
+
+#include "acl/core/compiler_utils.h"
+#include "acl/compression/impl/track_list_context.h"
+
+#include <rtm/mask4i.h>
+#include <rtm/vector4f.h>
+
+#include <cstdint>
+
+ACL_IMPL_FILE_PRAGMA_PUSH
+
+namespace acl
+{
+	namespace acl_impl
+	{
+		inline void normalize_scalarf_track(track& mut_track, const scalarf_range& range)
+		{
+			ACL_ASSERT(mut_track.is_owner(), "Track must be writable");
+
+			using namespace rtm;
+
+			const vector4f one = rtm::vector_set(1.0f);
+			const vector4f zero = vector_zero();
+
+			track_vector4f& typed_track = track_cast<track_vector4f>(mut_track);
+
+			const uint32_t num_samples = mut_track.get_num_samples();
+
+			const vector4f range_min = range.get_min();
+			const vector4f range_extent = range.get_extent();
+			const mask4i is_range_zero_mask = vector_less_than(range_extent, rtm::vector_set(0.000000001f));
+
+			for (uint32_t sample_index = 0; sample_index < num_samples; ++sample_index)
+			{
+				// normalized value is between [0.0 .. 1.0]
+				// value = (normalized value * range extent) + range min
+				// normalized value = (value - range min) / range extent
+				const vector4f sample = typed_track[sample_index];
+
+				vector4f normalized_sample = vector_div(vector_sub(sample, range_min), range_extent);
+
+				// Clamp because the division might be imprecise
+				normalized_sample = vector_min(normalized_sample, one);
+				normalized_sample = vector_select(is_range_zero_mask, zero, normalized_sample);
+
+				ACL_ASSERT(vector_all_greater_equal(normalized_sample, zero) && vector_all_less_equal(normalized_sample, one), "Invalid normalized value. 0.0 <= [%f, %f, %f, %f] <= 1.0", vector_get_x(normalized_sample), vector_get_y(normalized_sample), vector_get_z(normalized_sample), vector_get_w(normalized_sample));
+
+				typed_track[sample_index] = normalized_sample;
+			}
+		}
+
+		inline void normalize_tracks(track_list_context& context)
+		{
+			ACL_ASSERT(context.is_valid(), "Invalid context");
+
+			for (uint32_t track_index = 0; track_index < context.num_tracks; ++track_index)
+			{
+				const bool is_track_constant = context.is_constant(track_index);
+				if (is_track_constant)
+					continue;	// Constant tracks don't need to be modified
+
+				const track_range& range = context.range_list[track_index];
+				track& mut_track = context.track_list[track_index];
+
+				switch (range.category)
+				{
+				case track_category8::scalarf:
+					normalize_scalarf_track(mut_track, range.range.scalarf);
+					break;
+				default:
+					ACL_ASSERT(false, "Invalid track category");
+					break;
+				}
+			}
+		}
+	}
+}
+
+ACL_IMPL_FILE_PRAGMA_POP
diff --git a/includes/acl/compression/impl/quantize_track_impl.h b/includes/acl/compression/impl/quantize_track_impl.h
new file mode 100644
--- /dev/null
+++ b/includes/acl/compression/impl/quantize_track_impl.h
@@ -0,0 +1,181 @@
+#pragma once
+
+////////////////////////////////////////////////////////////////////////////////
+// The MIT License (MIT)
+//
+// Copyright (c) 2019 Nicholas Frechette & Animation Compression Library contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+////////////////////////////////////////////////////////////////////////////////
+
+#include "acl/core/compiler_utils.h"
+#include "acl/core/track_types.h"
+#include "acl/compression/impl/track_list_context.h"
+
+#include <rtm/mask4i.h>
+#include <rtm/vector4f.h>
+
+#include <cstdint>
+
+ACL_IMPL_FILE_PRAGMA_PUSH
+
+namespace acl
+{
+	namespace acl_impl
+	{
+		struct quantization_scales
+		{
+			rtm::vector4f max_value;
+			rtm::vector4f inv_max_value;
+
+			quantization_scales(uint32_t num_bits)
+			{
+				ACL_ASSERT(num_bits > 0, "Cannot decay with 0 bits");
+				ACL_ASSERT(num_bits < 31, "Attempting to decay on too many bits");
+
+				const float max_value_ = safe_to_float((1 << num_bits) - 1);
+				max_value = rtm::vector_set(max_value_);
+				inv_max_value = rtm::vector_set(1.0f / max_value_);
+			}
+		};
+
+		// Decays the input value through quantization by packing and unpacking a normalized input value
+		inline rtm::vector4f RTM_SIMD_CALL decay_vector4_uXX(rtm::vector4f_arg0 value, const quantization_scales& scales)
+		{
+			using namespace rtm;
+
+			ACL_ASSERT(vector_all_greater_equal(value, vector_zero()) && vector_all_less_equal(value, rtm::vector_set(1.0f)), "Expected normalized unsigned input value: %f, %f, %f, %f", vector_get_x(value), vector_get_y(value), vector_get_z(value), vector_get_w(value));
+
+			const vector4f packed_value = vector_symmetric_round(vector_mul(value, scales.max_value));
+			const vector4f decayed_value = vector_mul(packed_value, scales.inv_max_value);
+			return decayed_value;
+		}
+
+		// Packs a normalized input value through quantization
+		inline rtm::vector4f RTM_SIMD_CALL pack_vector4_uXX(rtm::vector4f_arg0 value, const quantization_scales& scales)
+		{
+			using namespace rtm;
+
+			ACL_ASSERT(vector_all_greater_equal(value, vector_zero()) && vector_all_less_equal(value, rtm::vector_set(1.0f)), "Expected normalized unsigned input value: %f, %f, %f, %f", vector_get_x(value), vector_get_y(value), vector_get_z(value), vector_get_w(value));
+
+			return vector_symmetric_round(vector_mul(value, scales.max_value));
+		}
+
+		inline void quantize_scalarf_track(track_list_context& context, uint32_t track_index)
+		{
+			using namespace rtm;
+
+			const track& ref_track = (*context.reference_list)[track_index];
+			track_vector4f& mut_track = track_cast<track_vector4f>(context.track_list[track_index]);
+			ACL_ASSERT(mut_track.is_owner(), "Track must be writable");
+
+			const vector4f precision = vector_load1(&mut_track.get_description().precision);
+			const uint32_t ref_element_size = ref_track.get_sample_size();
+			const uint32_t num_samples = mut_track.get_num_samples();
+
+			const scalarf_range& range = context.range_list[track_index].range.scalarf;
+			const vector4f range_min = range.get_min();
+			const vector4f range_extent = range.get_extent();
+
+			vector4f raw_sample = vector_zero();
+			uint8_t best_bit_rate = k_highest_bit_rate;	// Default to raw if we fail to find something better
+
+			// First we look for the best bit rate possible that keeps us within our precision target
+			for (uint8_t bit_rate = k_highest_bit_rate - 1; bit_rate != 0; --bit_rate)	// Skip the raw bit rate and the constant bit rate
+			{
+				const uint32_t num_bits_at_bit_rate = get_num_bits_at_bit_rate(bit_rate);
+				const quantization_scales scales(num_bits_at_bit_rate);
+
+				bool is_error_to_high = false;
+				for (uint32_t sample_index = 0; sample_index < num_samples; ++sample_index)
+				{
+					std::memcpy(&raw_sample, ref_track[sample_index], ref_element_size);
+
+					const vector4f normalized_sample = mut_track[sample_index];
+
+					// Decay our value through quantization
+					const vector4f decayed_normalized_sample = decay_vector4_uXX(normalized_sample, scales);
+
+					// Undo normalization
+					const vector4f decayed_sample = vector_mul_add(decayed_normalized_sample, range_extent, range_min);
+
+					const vector4f delta = vector_abs(vector_sub(raw_sample, decayed_sample));
+					if (!vector_all_less_equal(delta, precision))
+					{
+						is_error_to_high = true;
+						break;
+					}
+				}
+
+				if (is_error_to_high)
+					break;	// Our error is too high, use the previous bit rate
+
+				// We were accurate enough, this is the best bit rate so far
+				best_bit_rate = bit_rate;
+			}
+
+			context.bit_rate_list[track_index].scalar.value = best_bit_rate;
+
+			// Done, update our track with the final result
+			if (best_bit_rate == k_highest_bit_rate)
+			{
+				// We can't quantize this track, keep it raw
+				for (uint32_t sample_index = 0; sample_index < num_samples; ++sample_index)
+					std::memcpy(&mut_track[sample_index], ref_track[sample_index], ref_element_size);
+			}
+			else
+			{
+				// Use the selected bit rate to quantize our track
+				const uint32_t num_bits_at_bit_rate = get_num_bits_at_bit_rate(best_bit_rate);
+				const quantization_scales scales(num_bits_at_bit_rate);
+
+				for (uint32_t sample_index = 0; sample_index < num_samples; ++sample_index)
+					mut_track[sample_index] = pack_vector4_uXX(mut_track[sample_index], scales);
+			}
+		}
+
+		inline void quantize_tracks(track_list_context& context)
+		{
+			ACL_ASSERT(context.is_valid(), "Invalid context");
+
+			context.bit_rate_list = allocate_type_array<track_bit_rate>(*context.allocator, context.num_tracks);
+
+			for (uint32_t track_index = 0; track_index < context.num_tracks; ++track_index)
+			{
+				const bool is_track_constant = context.is_constant(track_index);
+				if (is_track_constant)
+					continue;	// Constant tracks don't need to be modified
+
+				const track_range& range = context.range_list[track_index];
+
+				switch (range.category)
+				{
+				case track_category8::scalarf:
+					quantize_scalarf_track(context, track_index);
+					break;
+				default:
+					ACL_ASSERT(false, "Invalid track category");
+					break;
+				}
+			}
+		}
+	}
+}
+
+ACL_IMPL_FILE_PRAGMA_POP
diff --git a/includes/acl/compression/impl/track_list_context.h b/includes/acl/compression/impl/track_list_context.h
new file mode 100644
--- /dev/null
+++ b/includes/acl/compression/impl/track_list_context.h
@@ -0,0 +1,223 @@
+#pragma once
+
+////////////////////////////////////////////////////////////////////////////////
+// The MIT License (MIT)
+//
+// Copyright (c) 2019 Nicholas Frechette & Animation Compression Library contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+////////////////////////////////////////////////////////////////////////////////
+
+#include "acl/core/compiler_utils.h"
+#include "acl/core/iallocator.h"
+#include "acl/compression/track_array.h"
+#include "acl/compression/impl/track_range.h"
+
+#include <rtm/vector4f.h>
+
+#include <cstdint>
+
+ACL_IMPL_FILE_PRAGMA_PUSH
+
+namespace acl
+{
+	namespace acl_impl
+	{
+		struct scalar_bit_rate
+		{
+			uint8_t value;
+		};
+
+		struct qvv_bit_rate
+		{
+			uint8_t rotation;
+			uint8_t translation;
+			uint8_t scale;
+		};
+
+		union track_bit_rate
+		{
+			scalar_bit_rate		scalar;
+			qvv_bit_rate		qvv;
+
+			track_bit_rate() : qvv{k_invalid_bit_rate, k_invalid_bit_rate, k_invalid_bit_rate} {}
+		};
+
+		struct track_list_context
+		{
+			IAllocator* allocator;
+			const track_array* reference_list;
+
+			track_array track_list;
+
+			track_range* range_list;
+			uint32_t* constant_tracks_bitset;
+			track_bit_rate* bit_rate_list;
+			uint32_t* track_output_indices;
+
+			uint32_t num_tracks;
+			uint32_t num_output_tracks;
+			uint32_t num_samples;
+			float sample_rate;
+			float duration;
+
+			track_list_context()
+				: allocator(nullptr)
+				, reference_list(nullptr)
+				, track_list()
+				, range_list(nullptr)
+				, constant_tracks_bitset(nullptr)
+				, bit_rate_list(nullptr)
+				, track_output_indices(nullptr)
+				, num_tracks(0)
+				, num_output_tracks(0)
+				, num_samples(0)
+				, sample_rate(0.0f)
+				, duration(0.0f)
+			{}
+
+			~track_list_context()
+			{
+				if (allocator != nullptr)
+				{
+					deallocate_type_array(*allocator, range_list, num_tracks);
+
+					const BitSetDescription bitset_desc = BitSetDescription::make_from_num_bits(num_tracks);
+					deallocate_type_array(*allocator, constant_tracks_bitset, bitset_desc.get_size());
+
+					deallocate_type_array(*allocator, bit_rate_list, num_tracks);
+
+					deallocate_type_array(*allocator, track_output_indices, num_output_tracks);
+				}
+			}
+
+			bool is_valid() const { return allocator != nullptr; }
+			bool is_constant(uint32_t track_index) const { return bitset_test(constant_tracks_bitset, BitSetDescription::make_from_num_bits(num_tracks), track_index); }
+		};
+
+		// Promote scalar tracks to vector tracks for SIMD alignment and padding
+		inline track_array copy_and_promote_track_list(IAllocator& allocator, const track_array& ref_track_list)
+		{
+			using namespace rtm;
+
+			const uint32_t num_tracks = ref_track_list.get_num_tracks();
+			const uint32_t num_samples = ref_track_list.get_num_samples_per_track();
+			const float sample_rate = ref_track_list.get_sample_rate();
+
+			track_array out_track_list(allocator, num_tracks);
+
+			for (uint32_t track_index = 0; track_index < num_tracks; ++track_index)
+			{
+				const track& ref_track = ref_track_list[track_index];
+				track& out_track = out_track_list[track_index];
+
+				switch (ref_track.get_type())
+				{
+				case track_type8::float1f:
+				{
+					const track_float1f& typed_ref_track = track_cast<const track_float1f>(ref_track);
+					track_vector4f track = track_vector4f::make_reserve(ref_track.get_description<track_desc_scalarf>(), allocator, num_samples, sample_rate);
+					for (uint32_t sample_index = 0; sample_index < num_samples; ++sample_index)
+						track[sample_index] = vector_load1(&typed_ref_track[sample_index]);
+					out_track = std::move(track);
+					break;
+				}
+				case track_type8::float2f:
+				{
+					const track_float2f& typed_ref_track = track_cast<const track_float2f>(ref_track);
+					track_vector4f track = track_vector4f::make_reserve(ref_track.get_description<track_desc_scalarf>(), allocator, num_samples, sample_rate);
+					for (uint32_t sample_index = 0; sample_index < num_samples; ++sample_index)
+						track[sample_index] = vector_load2(&typed_ref_track[sample_index]);
+					out_track = std::move(track);
+					break;
+				}
+				case track_type8::float3f:
+				{
+					const track_float3f& typed_ref_track = track_cast<const track_float3f>(ref_track);
+					track_vector4f track = track_vector4f::make_reserve(ref_track.get_description<track_desc_scalarf>(), allocator, num_samples, sample_rate);
+					for (uint32_t sample_index = 0; sample_index < num_samples; ++sample_index)
+						track[sample_index] = vector_load3(&typed_ref_track[sample_index]);
+					out_track = std::move(track);
+					break;
+				}
+				case track_type8::float4f:
+				{
+					const track_float4f& typed_ref_track = track_cast<const track_float4f>(ref_track);
+					track_vector4f track = track_vector4f::make_reserve(ref_track.get_description<track_desc_scalarf>(), allocator, num_samples, sample_rate);
+					for (uint32_t sample_index = 0; sample_index < num_samples; ++sample_index)
+						track[sample_index] = vector_load(&typed_ref_track[sample_index]);
+					out_track = std::move(track);
+					break;
+				}
+				default:
+					// Copy as is
+					out_track = ref_track.get_copy(allocator);
+					break;
+				}
+			}
+
+			return out_track_list;
+		}
+
+		inline uint32_t* create_output_track_mapping(IAllocator& allocator, const track_array& track_list, uint32_t& out_num_output_tracks)
+		{
+			const uint32_t num_tracks = track_list.get_num_tracks();
+			uint32_t num_output_tracks = num_tracks;
+			for (uint32_t track_index = 0; track_index < num_tracks; ++track_index)
+			{
+				const uint32_t output_index = track_list[track_index].get_output_index();
+				if (output_index == k_invalid_track_index)
+					num_output_tracks--;	// Stripped from the output
+			}
+
+			uint32_t* output_indices = allocate_type_array<uint32_t>(allocator, num_output_tracks);
+			for (uint32_t track_index = 0; track_index < num_tracks; ++track_index)
+			{
+				const uint32_t output_index = track_list[track_index].get_output_index();
+				if (output_index != k_invalid_bone_index)
+					output_indices[output_index] = track_index;
+			}
+
+			out_num_output_tracks = num_output_tracks;
+			return output_indices;
+		}
+
+		inline void initialize_context(IAllocator& allocator, const track_array& track_list, track_list_context& context)
+		{
+			ACL_ASSERT(track_list.is_valid().empty(), "Invalid track list");
+			ACL_ASSERT(!context.is_valid(), "Context already initialized");
+
+			context.allocator = &allocator;
+			context.reference_list = &track_list;
+			context.track_list = copy_and_promote_track_list(allocator, track_list);
+			context.range_list = nullptr;
+			context.constant_tracks_bitset = nullptr;
+			context.track_output_indices = nullptr;
+			context.num_tracks = track_list.get_num_tracks();
+			context.num_output_tracks = 0;
+			context.num_samples = track_list.get_num_samples_per_track();
+			context.sample_rate = track_list.get_sample_rate();
+			context.duration = track_list.get_duration();
+
+			context.track_output_indices = create_output_track_mapping(allocator, track_list, context.num_output_tracks);
+		}
+	}
+}
+
+ACL_IMPL_FILE_PRAGMA_POP
diff --git a/includes/acl/compression/impl/track_range.h b/includes/acl/compression/impl/track_range.h
new file mode 100644
--- /dev/null
+++ b/includes/acl/compression/impl/track_range.h
@@ -0,0 +1,98 @@
+#pragma once
+
+////////////////////////////////////////////////////////////////////////////////
+// The MIT License (MIT)
+//
+// Copyright (c) 2019 Nicholas Frechette & Animation Compression Library contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+////////////////////////////////////////////////////////////////////////////////
+
+#include "acl/core/compiler_utils.h"
+#include "acl/core/iallocator.h"
+#include "acl/compression/track_array.h"
+
+#include <rtm/vector4f.h>
+
+#include <cstdint>
+
+ACL_IMPL_FILE_PRAGMA_PUSH
+
+namespace acl
+{
+	namespace acl_impl
+	{
+		class scalarf_range
+		{
+		public:
+			static constexpr track_category8 type = track_category8::scalarf;
+
+			scalarf_range() : m_min(), m_extent() {}
+			scalarf_range(rtm::vector4f_arg0 min, rtm::vector4f_arg1 extent) : m_min(min), m_extent(extent) {}
+
+			static scalarf_range RTM_SIMD_CALL from_min_max(rtm::vector4f_arg0 min, rtm::vector4f_arg1 max) { return scalarf_range(min, rtm::vector_sub(max, min)); }
+			static scalarf_range RTM_SIMD_CALL from_min_extent(rtm::vector4f_arg0 min, rtm::vector4f_arg1 extent) { return scalarf_range(min, extent); }
+
+			rtm::vector4f RTM_SIMD_CALL get_min() const { return m_min; }
+			rtm::vector4f RTM_SIMD_CALL get_max() const { return rtm::vector_add(m_min, m_extent); }
+
+			rtm::vector4f RTM_SIMD_CALL get_center() const { return rtm::vector_add(m_min, rtm::vector_mul(m_extent, 0.5f)); }
+			rtm::vector4f RTM_SIMD_CALL get_extent() const { return m_extent; }
+
+			bool is_constant(float threshold) const { return rtm::vector_all_less_than(rtm::vector_abs(m_extent), rtm::vector_set(threshold)); }
+
+		private:
+			rtm::vector4f m_min;
+			rtm::vector4f m_extent;
+		};
+
+		struct track_range
+		{
+			track_range() : range(), category(track_category8::scalarf) {}
+			track_range(const scalarf_range& range_) : range(range_), category(track_category8::scalarf) {}
+
+			bool is_constant(float threshold) const
+			{
+				switch (category)
+				{
+				case track_category8::scalarf:	return range.scalarf.is_constant(threshold);
+				default:
+					ACL_ASSERT(false, "Invalid track category");
+					return false;
+				}
+			}
+
+			union range_union
+			{
+				scalarf_range scalarf;
+				// TODO: Add qvv range and scalard/i/q ranges
+
+				range_union() : scalarf(scalarf_range::from_min_extent(rtm::vector_zero(), rtm::vector_zero())) {}
+				range_union(const scalarf_range& range) : scalarf(range) {}
+			};
+
+			range_union			range;
+			track_category8		category;
+
+			uint8_t				padding[15];
+		};
+	}
+}
+
+ACL_IMPL_FILE_PRAGMA_POP
diff --git a/includes/acl/compression/impl/track_range_impl.h b/includes/acl/compression/impl/track_range_impl.h
new file mode 100644
--- /dev/null
+++ b/includes/acl/compression/impl/track_range_impl.h
@@ -0,0 +1,86 @@
+#pragma once
+
+////////////////////////////////////////////////////////////////////////////////
+// The MIT License (MIT)
+//
+// Copyright (c) 2019 Nicholas Frechette & Animation Compression Library contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+////////////////////////////////////////////////////////////////////////////////
+
+#include "acl/core/compiler_utils.h"
+#include "acl/core/iallocator.h"
+#include "acl/compression/impl/track_list_context.h"
+
+#include <rtm/vector4f.h>
+
+#include <cstdint>
+
+ACL_IMPL_FILE_PRAGMA_PUSH
+
+namespace acl
+{
+	namespace acl_impl
+	{
+		inline scalarf_range extract_scalarf_range(const track& track)
+		{
+			using namespace rtm;
+
+			vector4f min = rtm::vector_set(1e10f);
+			vector4f max = rtm::vector_set(-1e10f);
+
+			const uint32_t num_samples = track.get_num_samples();
+			const track_vector4f& typed_track = track_cast<const track_vector4f>(track);
+			
+			for (uint32_t sample_index = 0; sample_index < num_samples; ++sample_index)
+			{
+				const vector4f sample = typed_track[sample_index];
+
+				min = vector_min(min, sample);
+				max = vector_max(max, sample);
+			}
+
+			return scalarf_range::from_min_max(min, max);
+		}
+
+		inline void extract_track_ranges(track_list_context& context)
+		{
+			ACL_ASSERT(context.is_valid(), "Invalid context");
+
+			context.range_list = allocate_type_array<track_range>(*context.allocator, context.num_tracks);
+
+			for (uint32_t track_index = 0; track_index < context.num_tracks; ++track_index)
+			{
+				const track& track = context.track_list[track_index];
+
+				switch (track.get_category())
+				{
+				case track_category8::scalarf:
+					context.range_list[track_index] = extract_scalarf_range(track);
+					break;
+				default:
+					ACL_ASSERT(false, "Invalid track category");
+					break;
+				}
+			}
+		}
+	}
+}
+
+ACL_IMPL_FILE_PRAGMA_POP
diff --git a/includes/acl/compression/impl/write_compression_stats_impl.h b/includes/acl/compression/impl/write_compression_stats_impl.h
new file mode 100644
--- /dev/null
+++ b/includes/acl/compression/impl/write_compression_stats_impl.h
@@ -0,0 +1,64 @@
+#pragma once
+
+////////////////////////////////////////////////////////////////////////////////
+// The MIT License (MIT)
+//
+// Copyright (c) 2019 Nicholas Frechette & Animation Compression Library contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+////////////////////////////////////////////////////////////////////////////////
+
+#include "acl/core/compiler_utils.h"
+#include "acl/core/error.h"
+#include "acl/core/scope_profiler.h"
+#include "acl/compression/impl/track_list_context.h"
+#include "acl/compression/output_stats.h"
+
+#include <cstdint>
+
+ACL_IMPL_FILE_PRAGMA_PUSH
+
+namespace acl
+{
+	namespace acl_impl
+	{
+		inline void write_compression_stats(const track_list_context& context, const compressed_tracks& tracks, const ScopeProfiler& compression_time, OutputStats& stats)
+		{
+			ACL_ASSERT(stats.writer != nullptr, "Attempted to log stats without a writer");
+
+			const uint32_t raw_size = context.reference_list->get_raw_size();
+			const uint32_t compressed_size = tracks.get_size();
+			const double compression_ratio = double(raw_size) / double(compressed_size);
+
+			sjson::ObjectWriter& writer = *stats.writer;
+			writer["algorithm_name"] = get_algorithm_name(AlgorithmType8::UniformlySampled);
+			//writer["algorithm_uid"] = settings.get_hash();
+			//writer["clip_name"] = clip.get_name().c_str();
+			writer["raw_size"] = raw_size;
+			writer["compressed_size"] = compressed_size;
+			writer["compression_ratio"] = compression_ratio;
+			writer["compression_time"] = compression_time.get_elapsed_seconds();
+			writer["duration"] = context.duration;
+			writer["num_samples"] = context.num_samples;
+			writer["num_tracks"] = context.num_tracks;
+		}
+	}
+}
+
+ACL_IMPL_FILE_PRAGMA_POP
diff --git a/includes/acl/compression/impl/write_track_data_impl.h b/includes/acl/compression/impl/write_track_data_impl.h
new file mode 100644
--- /dev/null
+++ b/includes/acl/compression/impl/write_track_data_impl.h
@@ -0,0 +1,188 @@
+#pragma once
+
+////////////////////////////////////////////////////////////////////////////////
+// The MIT License (MIT)
+//
+// Copyright (c) 2019 Nicholas Frechette & Animation Compression Library contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+////////////////////////////////////////////////////////////////////////////////
+
+#include "acl/core/compiler_utils.h"
+#include "acl/core/impl/compressed_headers.h"
+#include "acl/compression/impl/track_list_context.h"
+
+#include <rtm/vector4f.h>
+
+#include <cstdint>
+
+ACL_IMPL_FILE_PRAGMA_PUSH
+
+namespace acl
+{
+	namespace acl_impl
+	{
+		inline uint32_t write_track_metadata(const track_list_context& context, track_metadata* per_track_metadata)
+		{
+			ACL_ASSERT(context.is_valid(), "Invalid context");
+
+			uint8_t* output_buffer = reinterpret_cast<uint8_t*>(per_track_metadata);
+			const uint8_t* output_buffer_start = output_buffer;
+
+			for (uint32_t output_index = 0; output_index < context.num_output_tracks; ++output_index)
+			{
+				const uint32_t track_index = context.track_output_indices[output_index];
+
+				if (per_track_metadata != nullptr)
+					per_track_metadata[output_index].bit_rate = context.is_constant(track_index) ? 0 : context.bit_rate_list[track_index].scalar.value;
+
+				output_buffer += sizeof(track_metadata);
+			}
+
+			return safe_static_cast<uint32_t>(output_buffer - output_buffer_start);
+		}
+
+		inline uint32_t write_track_constant_values(const track_list_context& context, float* constant_values)
+		{
+			ACL_ASSERT(context.is_valid(), "Invalid context");
+
+			uint8_t* output_buffer = reinterpret_cast<uint8_t*>(constant_values);
+			const uint8_t* output_buffer_start = output_buffer;
+
+			for (uint32_t output_index = 0; output_index < context.num_output_tracks; ++output_index)
+			{
+				const uint32_t track_index = context.track_output_indices[output_index];
+
+				if (!context.is_constant(track_index))
+					continue;
+
+				const track& ref_track = (*context.reference_list)[track_index];
+				const uint32_t element_size = ref_track.get_sample_size();
+
+				if (constant_values != nullptr)
+					std::memcpy(output_buffer, ref_track[0], element_size);
+
+				output_buffer += element_size;
+			}
+
+			return safe_static_cast<uint32_t>(output_buffer - output_buffer_start);
+		}
+
+		inline uint32_t write_track_range_values(const track_list_context& context, float* range_values)
+		{
+			ACL_ASSERT(context.is_valid(), "Invalid context");
+
+			uint8_t* output_buffer = reinterpret_cast<uint8_t*>(range_values);
+			const uint8_t* output_buffer_start = output_buffer;
+
+			for (uint32_t output_index = 0; output_index < context.num_output_tracks; ++output_index)
+			{
+				const uint32_t track_index = context.track_output_indices[output_index];
+
+				if (context.is_constant(track_index))
+					continue;
+
+				const uint8_t bit_rate = context.bit_rate_list[track_index].scalar.value;
+				if (is_raw_bit_rate(bit_rate))
+					continue;
+
+				const track& ref_track = (*context.reference_list)[track_index];
+				const track_range& range = context.range_list[track_index];
+
+				const uint32_t element_size = ref_track.get_sample_size();
+
+				if (range_values != nullptr)
+				{
+					// Only support scalarf for now
+					ACL_ASSERT(range.category == track_category8::scalarf, "Unsupported category");
+					const rtm::vector4f range_min = range.range.scalarf.get_min();
+					const rtm::vector4f range_extent = range.range.scalarf.get_extent();
+					std::memcpy(output_buffer, &range_min, element_size);
+					std::memcpy(output_buffer + element_size, &range_extent, element_size);
+				}
+
+				output_buffer += element_size;	// Min
+				output_buffer += element_size;	// Extent
+			}
+
+			return safe_static_cast<uint32_t>(output_buffer - output_buffer_start);
+		}
+
+		inline uint32_t write_track_animated_values(const track_list_context& context, uint8_t* animated_values)
+		{
+			ACL_ASSERT(context.is_valid(), "Invalid context");
+
+			uint8_t* output_buffer = animated_values;
+			uint64_t output_bit_offset = 0;
+
+			const uint32_t num_components = get_track_num_sample_elements(context.reference_list->get_track_type());
+			ACL_ASSERT(num_components <= 4, "Unexpected number of elements");
+
+			for (uint32_t sample_index = 0; sample_index < context.num_samples; ++sample_index)
+			{
+				for (uint32_t output_index = 0; output_index < context.num_output_tracks; ++output_index)
+				{
+					const uint32_t track_index = context.track_output_indices[output_index];
+
+					if (context.is_constant(track_index))
+						continue;
+
+					const track& ref_track = (*context.reference_list)[track_index];
+					const track& mut_track = context.track_list[track_index];
+
+					// Only support scalarf for now
+					ACL_ASSERT(ref_track.get_category() == track_category8::scalarf, "Unsupported category");
+
+					const scalar_bit_rate bit_rate = context.bit_rate_list[track_index].scalar;
+					const uint64_t num_bits_per_component = get_num_bits_at_bit_rate(bit_rate.value);
+
+					const track& src_track = is_raw_bit_rate(bit_rate.value) ? ref_track : mut_track;
+
+					const uint32_t* sample_u32 = safe_ptr_cast<const uint32_t>(src_track[sample_index]);
+					const float* sample_f32 = safe_ptr_cast<const float>(src_track[sample_index]);
+					for (uint32_t component_index = 0; component_index < num_components; ++component_index)
+					{
+						if (animated_values != nullptr)
+						{
+							uint32_t value;
+							if (is_raw_bit_rate(bit_rate.value))
+								value = byte_swap(sample_u32[component_index]);
+							else
+							{
+								// TODO: Hacked, our values are still as floats, cast to int, shift, and byte swap
+								// Ideally should be done in the cache/mutable track with SIMD
+								value = safe_static_cast<uint32_t>(sample_f32[component_index]);
+								value = value << (32 - num_bits_per_component);
+								value = byte_swap(value);
+							}
+
+							memcpy_bits(output_buffer, output_bit_offset, &value, 0, num_bits_per_component);
+						}
+
+						output_bit_offset += num_bits_per_component;
+					}
+				}
+			}
+
+			return safe_static_cast<uint32_t>(output_bit_offset);
+		}
+	}
+}
+
+ACL_IMPL_FILE_PRAGMA_POP
diff --git a/includes/acl/compression/track.h b/includes/acl/compression/track.h
new file mode 100644
--- /dev/null
+++ b/includes/acl/compression/track.h
@@ -0,0 +1,493 @@
+#pragma once
+
+////////////////////////////////////////////////////////////////////////////////
+// The MIT License (MIT)
+//
+// Copyright (c) 2019 Nicholas Frechette & Animation Compression Library contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+////////////////////////////////////////////////////////////////////////////////
+
+#include "acl/core/compiler_utils.h"
+#include "acl/core/iallocator.h"
+#include "acl/core/track_traits.h"
+#include "acl/core/track_types.h"
+
+#include <cstdint>
+
+ACL_IMPL_FILE_PRAGMA_PUSH
+
+namespace acl
+{
+	//////////////////////////////////////////////////////////////////////////
+	// An untyped track of data. A track is a time series of values sampled
+	// uniformly over time at a specific sample rate. Tracks can either own
+	// their memory or reference an external buffer.
+	// For convenience, this type can be cast with the `track_cast(..)` family
+	// of functions. Each track type has the same size as every track description
+	// is contained within a union.
+	//////////////////////////////////////////////////////////////////////////
+	class track
+	{
+	public:
+		//////////////////////////////////////////////////////////////////////////
+		// Creates an empty, untyped track.
+		track()
+			: m_allocator(nullptr)
+			, m_data(nullptr)
+			, m_num_samples(0)
+			, m_stride(0)
+			, m_data_size(0)
+			, m_sample_rate(0.0f)
+			, m_type(track_type8::float1f)
+			, m_category(track_category8::scalarf)
+			, m_sample_size(0)
+			, m_desc()
+		{}
+
+		//////////////////////////////////////////////////////////////////////////
+		// Move constructor for a track.
+		track(track&& other)
+			: m_allocator(other.m_allocator)
+			, m_data(other.m_data)
+			, m_num_samples(other.m_num_samples)
+			, m_stride(other.m_stride)
+			, m_data_size(other.m_data_size)
+			, m_sample_rate(other.m_sample_rate)
+			, m_type(other.m_type)
+			, m_category(other.m_category)
+			, m_sample_size(other.m_sample_size)
+			, m_desc(other.m_desc)
+		{
+			other.m_allocator = nullptr;
+			other.m_data = nullptr;
+		}
+
+		//////////////////////////////////////////////////////////////////////////
+		// Destroys the track. If it owns the memory referenced, it will be freed.
+		~track()
+		{
+			if (is_owner())
+			{
+				// We own the memory, free it
+				m_allocator->deallocate(m_data, m_data_size);
+			}
+		}
+
+		//////////////////////////////////////////////////////////////////////////
+		// Move assignment for a track.
+		track& operator=(track&& other)
+		{
+			std::swap(m_allocator, other.m_allocator);
+			std::swap(m_data, other.m_data);
+			std::swap(m_num_samples, other.m_num_samples);
+			std::swap(m_stride, other.m_stride);
+			std::swap(m_data_size, other.m_data_size);
+			std::swap(m_sample_rate, other.m_sample_rate);
+			std::swap(m_type, other.m_type);
+			std::swap(m_category, other.m_category);
+			std::swap(m_sample_size, other.m_sample_size);
+			std::swap(m_desc, other.m_desc);
+			return *this;
+		}
+
+		//////////////////////////////////////////////////////////////////////////
+		// Returns a pointer to an untyped sample at the specified index.
+		void* operator[](uint32_t index)
+		{
+			// If we have an allocator, we own the memory and mutable pointers are allowed
+			ACL_ASSERT(is_owner(), "Mutable reference not allowed, create a copy instead");
+			ACL_ASSERT(index < m_num_samples, "Invalid sample index. %u >= %u", index, m_num_samples);
+			return m_allocator ? m_data + (index * m_stride) : nullptr;
+		}
+
+		//////////////////////////////////////////////////////////////////////////
+		// Returns a pointer to an untyped sample at the specified index.
+		const void* operator[](uint32_t index) const
+		{
+			ACL_ASSERT(index < m_num_samples, "Invalid sample index. %u >= %u", index, m_num_samples);
+			return m_data + (index * m_stride);
+		}
+
+		//////////////////////////////////////////////////////////////////////////
+		// Returns true if the track owns its memory, false otherwise.
+		bool is_owner() const { return m_allocator != nullptr; }
+
+		//////////////////////////////////////////////////////////////////////////
+		// Returns true if the track owns its memory, false otherwise.
+		bool is_ref() const { return m_allocator == nullptr; }
+
+		//////////////////////////////////////////////////////////////////////////
+		// Returns the number of samples contained within the track.
+		uint32_t get_num_samples() const { return m_num_samples; }
+
+		//////////////////////////////////////////////////////////////////////////
+		// Returns the stride in bytes in between samples as layed out in memory.
+		// This is always sizeof(sample_type) unless the memory isn't owned internally.
+		uint32_t get_stride() const { return m_stride; }
+
+		//////////////////////////////////////////////////////////////////////////
+		// Returns the track type.
+		track_type8 get_type() const { return m_type; }
+
+		//////////////////////////////////////////////////////////////////////////
+		// Returns the track category.
+		track_category8 get_category() const { return m_category; }
+
+		//////////////////////////////////////////////////////////////////////////
+		// Returns the size in bytes of each track sample.
+		uint32_t get_sample_size() const { return m_sample_size; }
+
+		//////////////////////////////////////////////////////////////////////////
+		// Returns the track sample rate.
+		// A track has its sampled uniformly distributed in time at a fixed rate (e.g. 30 samples per second).
+		float get_sample_rate() const { return m_sample_rate; }
+
+		//////////////////////////////////////////////////////////////////////////
+		// Returns the track output index.
+		// When compressing, it is often desirable to strip or re-order the tracks we output.
+		// This can be used to sort by LOD or to strip stale tracks. Tracks with an invalid
+		// track index are stripped in the output.
+		uint32_t get_output_index() const
+		{
+			switch (m_category)
+			{
+			default:
+			case track_category8::scalarf:	return m_desc.scalar.output_index;
+			}
+		}
+
+		//////////////////////////////////////////////////////////////////////////
+		// Returns the track description.
+		template<typename desc_type>
+		desc_type& get_description()
+		{
+			ACL_ASSERT(desc_type::category == m_category, "Unexpected track category");
+			switch (desc_type::category)
+			{
+			default:
+			case track_category8::scalarf:	return m_desc.scalar;
+			}
+		}
+
+		//////////////////////////////////////////////////////////////////////////
+		// Returns the track description.
+		template<typename desc_type>
+		const desc_type& get_description() const
+		{
+			ACL_ASSERT(desc_type::category == m_category, "Unexpected track category");
+			switch (desc_type::category)
+			{
+			default:
+			case track_category8::scalarf:	return m_desc.scalar;
+			}
+		}
+
+		//////////////////////////////////////////////////////////////////////////
+		// Returns a copy of the track where the memory will be owned by the copy.
+		track get_copy(IAllocator& allocator) const
+		{
+			track track_;
+			track_.m_allocator = &allocator;
+			track_.m_data = reinterpret_cast<uint8_t*>(allocator.allocate(m_data_size));
+			track_.m_num_samples = m_num_samples;
+			track_.m_stride = m_stride;
+			track_.m_data_size = m_data_size;
+			track_.m_type = m_type;
+			track_.m_category = m_category;
+			track_.m_desc = m_desc;
+
+			std::memcpy(track_.m_data, m_data, m_data_size);
+
+			return track_;
+		}
+
+		//////////////////////////////////////////////////////////////////////////
+		// Returns a reference to the track where the memory isn't owned.
+		track get_ref() const
+		{
+			track track_;
+			track_.m_allocator = nullptr;
+			track_.m_data = m_data;
+			track_.m_num_samples = m_num_samples;
+			track_.m_stride = m_stride;
+			track_.m_data_size = m_data_size;
+			track_.m_type = m_type;
+			track_.m_category = m_category;
+			track_.m_desc = m_desc;
+			return track_;
+		}
+
+	protected:
+		//////////////////////////////////////////////////////////////////////////
+		// We prohibit copying, use get_copy() and get_ref() instead.
+		track(const track&) = delete;
+		track& operator=(const track&) = delete;
+
+		//////////////////////////////////////////////////////////////////////////
+		// Internal constructor.
+		// Creates an empty, untyped track.
+		track(track_type8 type, track_category8 category)
+			: m_allocator(nullptr)
+			, m_data(nullptr)
+			, m_num_samples(0)
+			, m_stride(0)
+			, m_data_size(0)
+			, m_sample_rate(0.0f)
+			, m_type(type)
+			, m_category(category)
+			, m_sample_size(0)
+			, m_desc()
+		{}
+
+		//////////////////////////////////////////////////////////////////////////
+		// Internal constructor.
+		track(IAllocator* allocator, uint8_t* data, uint32_t num_samples, uint32_t stride, size_t data_size, float sample_rate, track_type8 type, track_category8 category, uint8_t sample_size)
+			: m_allocator(allocator)
+			, m_data(data)
+			, m_num_samples(num_samples)
+			, m_stride(stride)
+			, m_data_size(data_size)
+			, m_sample_rate(sample_rate)
+			, m_type(type)
+			, m_category(category)
+			, m_sample_size(sample_size)
+			, m_desc()
+		{}
+
+		IAllocator*				m_allocator;		// Optional allocator that owns the memory
+		uint8_t*				m_data;				// Pointer to the samples
+
+		uint32_t				m_num_samples;		// The number of samples
+		uint32_t				m_stride;			// The stride in bytes in between samples as layed out in memory
+		size_t					m_data_size;		// The total size of the buffer used by the samples
+
+		float					m_sample_rate;		// The track sample rate
+
+		track_type8				m_type;				// The track type
+		track_category8			m_category;			// The track category
+		uint16_t				m_sample_size;		// The size in bytes of each sample
+
+		//////////////////////////////////////////////////////////////////////////
+		// A union of every track description.
+		// This ensures every track has the same size regardless of its type.
+		union desc_union
+		{
+			track_desc_scalarf	scalar;
+			// TODO: Add other description types here
+
+			desc_union() : scalar() {}
+			desc_union(const track_desc_scalarf& desc) : scalar(desc) {}
+		};
+
+		desc_union				m_desc;				// The track description
+	};
+
+	//////////////////////////////////////////////////////////////////////////
+	// A typed track of data. See `track` for details.
+	//////////////////////////////////////////////////////////////////////////
+	template<track_type8 track_type_>
+	class track_typed final : public track
+	{
+	public:
+		//////////////////////////////////////////////////////////////////////////
+		// The track type.
+		static constexpr track_type8 type = track_type_;
+
+		//////////////////////////////////////////////////////////////////////////
+		// The track category.
+		static constexpr track_category8 category = track_traits<track_type_>::category;
+
+		//////////////////////////////////////////////////////////////////////////
+		// The type of each sample in this track.
+		using sample_type = typename track_traits<track_type_>::sample_type;
+
+		//////////////////////////////////////////////////////////////////////////
+		// The type of the track description.
+		using desc_type = typename track_traits<track_type_>::desc_type;
+
+		//////////////////////////////////////////////////////////////////////////
+		// Constructs an empty typed track.
+		track_typed() : track(type, category) { static_assert(sizeof(track_typed) == sizeof(track), "You cannot add member variables to this class"); }
+
+		//////////////////////////////////////////////////////////////////////////
+		// Move assignment for a track.
+		track_typed(track_typed&& other) : track(std::forward<track>(other)) {}
+
+		//////////////////////////////////////////////////////////////////////////
+		// Move assignment for a track.
+		track_typed& operator=(track_typed&& other) { return track::operator=(std::forward<track>(other)); }
+
+		//////////////////////////////////////////////////////////////////////////
+		// Returns the sample at the specified index.
+		// If this track does not own the memory, mutable references aren't allowed and an
+		// invalid reference will be returned, leading to a crash.
+		sample_type& operator[](uint32_t index)
+		{
+			// If we have an allocator, we own the memory and mutable references are allowed
+			ACL_ASSERT(is_owner(), "Mutable reference not allowed, create a copy instead");
+			ACL_ASSERT(index < m_num_samples, "Invalid sample index. %u >= %u", index, m_num_samples);
+			return m_allocator ? *reinterpret_cast<sample_type*>(m_data + (index * m_stride)) : *reinterpret_cast<sample_type*>(0x42);
+		}
+
+		//////////////////////////////////////////////////////////////////////////
+		// Returns the sample at the specified index.
+		const sample_type& operator[](uint32_t index) const
+		{
+			ACL_ASSERT(index < m_num_samples, "Invalid sample index. %u >= %u", index, m_num_samples);
+			return *reinterpret_cast<const sample_type*>(m_data + (index * m_stride));
+		}
+
+		//////////////////////////////////////////////////////////////////////////
+		// Returns the track description.
+		desc_type& get_description()
+		{
+			switch (category)
+			{
+			default:
+			case track_category8::scalarf:	return m_desc.scalar;
+			}
+		}
+
+		//////////////////////////////////////////////////////////////////////////
+		// Returns the track description.
+		const desc_type& get_description() const
+		{
+			switch (category)
+			{
+			default:
+			case track_category8::scalarf:	return m_desc.scalar;
+			}
+		}
+
+		//////////////////////////////////////////////////////////////////////////
+		// Returns the track type.
+		track_type8 get_type() const { return type; }
+
+		//////////////////////////////////////////////////////////////////////////
+		// Returns the track category.
+		track_category8 get_category() const { return category; }
+
+		//////////////////////////////////////////////////////////////////////////
+		// Creates a track that copies the data and owns the memory.
+		static track_typed<track_type_> make_copy(const desc_type& desc, IAllocator& allocator, const sample_type* data, uint32_t num_samples, float sample_rate, uint32_t stride = sizeof(sample_type))
+		{
+			const size_t data_size = size_t(num_samples) * sizeof(sample_type);
+			const uint8_t* data_raw = reinterpret_cast<const uint8_t*>(data);
+
+			// Copy the data manually to avoid preserving the stride
+			sample_type* data_copy = reinterpret_cast<sample_type*>(allocator.allocate(data_size));
+			for (uint32_t index = 0; index < num_samples; ++index)
+				data_copy[index] = *reinterpret_cast<sample_type*>(data_raw + (index * stride));
+
+			return track_typed<track_type_>(&allocator, reinterpret_cast<uint8_t*>(data_copy), num_samples, sizeof(sample_type), data_size, sample_rate, desc);
+		}
+
+		//////////////////////////////////////////////////////////////////////////
+		// Creates a track and preallocates but does not initialize the memory that it owns.
+		static track_typed<track_type_> make_reserve(const desc_type& desc, IAllocator& allocator, uint32_t num_samples, float sample_rate)
+		{
+			const size_t data_size = size_t(num_samples) * sizeof(sample_type);
+			return track_typed<track_type_>(&allocator, reinterpret_cast<uint8_t*>(allocator.allocate(data_size)), num_samples, sizeof(sample_type), data_size, sample_rate, desc);
+		}
+
+		//////////////////////////////////////////////////////////////////////////
+		// Creates a track and takes ownership of the already allocated memory.
+		static track_typed<track_type_> make_owner(const desc_type& desc, IAllocator& allocator, sample_type* data, uint32_t num_samples, float sample_rate, uint32_t stride = sizeof(sample_type))
+		{
+			const size_t data_size = size_t(num_samples) * stride;
+			return track_typed<track_type_>(&allocator, reinterpret_cast<uint8_t*>(data), num_samples, stride, data_size, sample_rate, desc);
+		}
+
+		//////////////////////////////////////////////////////////////////////////
+		// Creates a track that just references the data without owning it.
+		static track_typed<track_type_> make_ref(const desc_type& desc, const sample_type* data, uint32_t num_samples, float sample_rate, uint32_t stride = sizeof(sample_type))
+		{
+			const size_t data_size = size_t(num_samples) * stride;
+			return track_typed<track_type_>(nullptr, const_cast<uint8_t*>(reinterpret_cast<const uint8_t*>(data)), num_samples, stride, data_size, sample_rate, desc);
+		}
+
+	private:
+		//////////////////////////////////////////////////////////////////////////
+		// We prohibit copying, use get_copy() and get_ref() instead.
+		track_typed(const track_typed&) = delete;
+		track_typed& operator=(const track_typed&) = delete;
+
+		//////////////////////////////////////////////////////////////////////////
+		// Internal constructor.
+		track_typed(IAllocator* allocator, uint8_t* data, uint32_t num_samples, uint32_t stride, size_t data_size, float sample_rate, const desc_type& desc)
+			: track(allocator, data, num_samples, stride, data_size, sample_rate, type, category, sizeof(sample_type))
+		{
+			m_desc = desc;
+		}
+	};
+
+	//////////////////////////////////////////////////////////////////////////
+	// Casts an untyped track into the desired track type while asserting for safety.
+	template<typename track_type>
+	inline track_type& track_cast(track& track_)
+	{
+		ACL_ASSERT(track_type::type == track_.get_type(), "Unexpected track type");
+		return static_cast<track_type&>(track_);
+	}
+
+	//////////////////////////////////////////////////////////////////////////
+	// Casts an untyped track into the desired track type while asserting for safety.
+	template<typename track_type>
+	inline const track_type& track_cast(const track& track_)
+	{
+		ACL_ASSERT(track_type::type == track_.get_type(), "Unexpected track type");
+		return static_cast<const track_type&>(track_);
+	}
+
+	//////////////////////////////////////////////////////////////////////////
+	// Casts an untyped track into the desired track type. Returns nullptr if the types
+	// are not compatible or if the input is nullptr.
+	template<typename track_type>
+	inline track_type* track_cast(track* track_)
+	{
+		if (track_ == nullptr || track_type::type != track_->get_type())
+			return nullptr;
+
+		return static_cast<track_type*>(track_);
+	}
+
+	//////////////////////////////////////////////////////////////////////////
+	// Casts an untyped track into the desired track type. Returns nullptr if the types
+	// are not compatible or if the input is nullptr.
+	template<typename track_type>
+	inline const track_type* track_cast(const track* track_)
+	{
+		if (track_ == nullptr || track_type::type != track_->get_type())
+			return nullptr;
+
+		return static_cast<const track_type*>(track_);
+	}
+
+	//////////////////////////////////////////////////////////////////////////
+	// Create aliases for the various typed track types.
+
+	using track_float1f			= track_typed<track_type8::float1f>;
+	using track_float2f			= track_typed<track_type8::float2f>;
+	using track_float3f			= track_typed<track_type8::float3f>;
+	using track_float4f			= track_typed<track_type8::float4f>;
+	using track_vector4f		= track_typed<track_type8::vector4f>;
+}
+
+ACL_IMPL_FILE_PRAGMA_POP
diff --git a/includes/acl/compression/track_array.h b/includes/acl/compression/track_array.h
new file mode 100644
--- /dev/null
+++ b/includes/acl/compression/track_array.h
@@ -0,0 +1,301 @@
+#pragma once
+
+////////////////////////////////////////////////////////////////////////////////
+// The MIT License (MIT)
+//
+// Copyright (c) 2019 Nicholas Frechette & Animation Compression Library contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+////////////////////////////////////////////////////////////////////////////////
+
+#include "acl/core/compiler_utils.h"
+#include "acl/core/error_result.h"
+#include "acl/core/iallocator.h"
+#include "acl/core/track_writer.h"
+#include "acl/compression/track.h"
+
+#include <rtm/scalarf.h>
+#include <rtm/vector4f.h>
+
+#include <cstdint>
+#include <limits>
+
+ACL_IMPL_FILE_PRAGMA_PUSH
+
+namespace acl
+{
+	//////////////////////////////////////////////////////////////////////////
+	// An array of tracks.
+	// Although each track contained within is untyped, each track must have
+	// the same type. They must all have the same sample rate and the same
+	// number of samples.
+	//////////////////////////////////////////////////////////////////////////
+	class track_array
+	{
+	public:
+		//////////////////////////////////////////////////////////////////////////
+		// Constructs an empty track array.
+		track_array()
+			: m_allocator(nullptr)
+			, m_tracks(nullptr)
+			, m_num_tracks(0)
+		{}
+
+		//////////////////////////////////////////////////////////////////////////
+		// Constructs an array with the specified number of tracks.
+		// Tracks will be empty and untyped by default.
+		track_array(IAllocator& allocator, uint32_t num_tracks)
+			: m_allocator(&allocator)
+			, m_tracks(allocate_type_array<track>(allocator, num_tracks))
+			, m_num_tracks(num_tracks)
+		{}
+
+		//////////////////////////////////////////////////////////////////////////
+		// Move constructor for a track array.
+		track_array(track_array&& other)
+			: m_allocator(other.m_allocator)
+			, m_tracks(other.m_tracks)
+			, m_num_tracks(other.m_num_tracks)
+		{
+			other.m_allocator = nullptr;	// Make sure we don't free our data since we no longer own it
+		}
+
+		//////////////////////////////////////////////////////////////////////////
+		// Destroys a track array.
+		~track_array()
+		{
+			if (m_allocator != nullptr)
+				deallocate_type_array(*m_allocator, m_tracks, m_num_tracks);
+		}
+
+		//////////////////////////////////////////////////////////////////////////
+		// Move assignment for a track array.
+		track_array& operator=(track_array&& other)
+		{
+			std::swap(m_allocator, other.m_allocator);
+			std::swap(m_tracks, other.m_tracks);
+			std::swap(m_num_tracks, other.m_num_tracks);
+			return *this;
+		}
+
+		//////////////////////////////////////////////////////////////////////////
+		// Returns the number of tracks contained in this array.
+		uint32_t get_num_tracks() const { return m_num_tracks; }
+
+		//////////////////////////////////////////////////////////////////////////
+		// Returns the number of samples per track in this array.
+		uint32_t get_num_samples_per_track() const { return m_allocator != nullptr && m_num_tracks > 0 ? m_tracks->get_num_samples() : 0; }
+
+		//////////////////////////////////////////////////////////////////////////
+		// Returns the track type for tracks in this array.
+		track_type8 get_track_type() const { return m_allocator != nullptr && m_num_tracks > 0 ? m_tracks->get_type() : track_type8::float1f; }
+
+		//////////////////////////////////////////////////////////////////////////
+		// Returns the track category for tracks in this array.
+		track_category8 get_track_category() const { return m_allocator != nullptr && m_num_tracks > 0 ? m_tracks->get_category() : track_category8::scalarf; }
+
+		//////////////////////////////////////////////////////////////////////////
+		// Returns the sample rate for tracks in this array.
+		float get_sample_rate() const { return m_allocator != nullptr && m_num_tracks > 0 ? m_tracks->get_sample_rate() : 0.0f; }
+
+		//////////////////////////////////////////////////////////////////////////
+		// Returns the duration for tracks in this array.
+		float get_duration() const { return m_allocator != nullptr && m_num_tracks > 0 ? calculate_duration(uint32_t(m_tracks->get_num_samples()), m_tracks->get_sample_rate()) : 0.0f; }
+
+		//////////////////////////////////////////////////////////////////////////
+		// Returns the track at the specified index.
+		track& operator[](uint32_t index)
+		{
+			ACL_ASSERT(index < m_num_tracks, "Invalid track index. %u >= %u", index, m_num_tracks);
+			return m_tracks[index];
+		}
+
+		//////////////////////////////////////////////////////////////////////////
+		// Returns the track at the specified index.
+		const track& operator[](uint32_t index) const
+		{
+			ACL_ASSERT(index < m_num_tracks, "Invalid track index. %u >= %u", index, m_num_tracks);
+			return m_tracks[index];
+		}
+
+		//////////////////////////////////////////////////////////////////////////
+		// Iterator begin() and end() implementations.
+		track* begin() { return m_tracks; }
+		const track* begin() const { return m_tracks; }
+		const track* end() { return m_tracks + m_num_tracks; }
+		const track* end() const { return m_tracks + m_num_tracks; }
+
+		//////////////////////////////////////////////////////////////////////////
+		// Returns whether a track array is valid or not.
+		// An array is valid if:
+		//    - It is empty
+		//    - All tracks have the same type
+		//    - All tracks have the same number of samples
+		//    - All tracks have the same sample rate
+		ErrorResult is_valid() const
+		{
+			const track_type8 type = get_track_type();
+			const uint32_t num_samples = get_num_samples_per_track();
+			const float sample_rate = get_sample_rate();
+
+			for (uint32_t track_index = 0; track_index < m_num_tracks; ++track_index)
+			{
+				const track& track_ = m_tracks[track_index];
+				if (track_.get_type() != type)
+					return ErrorResult("Tracks must all have the same type within an array");
+
+				if (track_.get_num_samples() != num_samples)
+					return ErrorResult("Track array requires the same number of samples in every track");
+
+				if (track_.get_sample_rate() != sample_rate)
+					return ErrorResult("Track array requires the same sample rate in every track");
+			}
+
+			return ErrorResult();
+		}
+
+		//////////////////////////////////////////////////////////////////////////
+		// Sample all tracks within this array at the specified sample time and
+		// desired rounding policy. Track samples are written out using the `track_writer` provided.
+		template<class track_writer_type>
+		inline void sample_tracks(float sample_time, SampleRoundingPolicy rounding_policy, track_writer_type& writer) const;
+
+		//////////////////////////////////////////////////////////////////////////
+		// Sample a single track within this array at the specified sample time and
+		// desired rounding policy. The track sample is written out using the `track_writer` provided.
+		template<class track_writer_type>
+		inline void sample_track(uint32_t track_index, float sample_time, SampleRoundingPolicy rounding_policy, track_writer_type& writer) const;
+
+		//////////////////////////////////////////////////////////////////////////
+		// Returns the raw size for this track array. Note that this differs from the actual
+		// memory used by an instance of this class. It is meant for comparison against
+		// the compressed size.
+		uint32_t get_raw_size() const;
+
+	private:
+		//////////////////////////////////////////////////////////////////////////
+		// We prohibit copying
+		track_array(const track_array&) = delete;
+		track_array& operator=(const track_array&) = delete;
+
+		IAllocator*		m_allocator;		// The allocator used to allocate our tracks
+		track*			m_tracks;			// The track list
+		uint32_t		m_num_tracks;		// The number of tracks
+	};
+
+	//////////////////////////////////////////////////////////////////////////
+
+	template<class track_writer_type>
+	inline void track_array::sample_tracks(float sample_time, SampleRoundingPolicy rounding_policy, track_writer_type& writer) const
+	{
+		ACL_ASSERT(is_valid().empty(), "Invalid track array");
+
+		for (uint32_t track_index = 0; track_index < m_num_tracks; ++track_index)
+			sample_track(track_index, sample_time, rounding_policy, writer);
+	}
+
+	template<class track_writer_type>
+	inline void track_array::sample_track(uint32_t track_index, float sample_time, SampleRoundingPolicy rounding_policy, track_writer_type& writer) const
+	{
+		ACL_ASSERT(is_valid().empty(), "Invalid track array");
+		ACL_ASSERT(track_index < m_num_tracks, "Invalid track index");
+
+		const track& track_ = m_tracks[track_index];
+		const uint32_t num_samples = track_.get_num_samples();
+		const float sample_rate = track_.get_sample_rate();
+
+		uint32_t key_frame0;
+		uint32_t key_frame1;
+		float interpolation_alpha;
+		find_linear_interpolation_samples_with_sample_rate(num_samples, sample_rate, sample_time, rounding_policy, key_frame0, key_frame1, interpolation_alpha);
+
+		switch (track_.get_type())
+		{
+		case track_type8::float1f:
+		{
+			const track_float1f& track__ = track_cast<track_float1f>(track_);
+
+			const rtm::scalarf value0 = rtm::scalar_load(&track__[key_frame0]);
+			const rtm::scalarf value1 = rtm::scalar_load(&track__[key_frame1]);
+			const rtm::scalarf value = rtm::scalar_lerp(value0, value1, interpolation_alpha);
+			writer.write_float1(track_index, value);
+			break;
+		}
+		case track_type8::float2f:
+		{
+			const track_float2f& track__ = track_cast<track_float2f>(track_);
+
+			const rtm::vector4f value0 = rtm::vector_load2(&track__[key_frame0]);
+			const rtm::vector4f value1 = rtm::vector_load2(&track__[key_frame1]);
+			const rtm::vector4f value = rtm::vector_lerp(value0, value1, interpolation_alpha);
+			writer.write_float2(track_index, value);
+			break;
+		}
+		case track_type8::float3f:
+		{
+			const track_float3f& track__ = track_cast<track_float3f>(track_);
+
+			const rtm::vector4f value0 = rtm::vector_load3(&track__[key_frame0]);
+			const rtm::vector4f value1 = rtm::vector_load3(&track__[key_frame1]);
+			const rtm::vector4f value = rtm::vector_lerp(value0, value1, interpolation_alpha);
+			writer.write_float3(track_index, value);
+			break;
+		}
+		case track_type8::float4f:
+		{
+			const track_float4f& track__ = track_cast<track_float4f>(track_);
+
+			const rtm::vector4f value0 = rtm::vector_load(&track__[key_frame0]);
+			const rtm::vector4f value1 = rtm::vector_load(&track__[key_frame1]);
+			const rtm::vector4f value = rtm::vector_lerp(value0, value1, interpolation_alpha);
+			writer.write_float4(track_index, value);
+			break;
+		}
+		case track_type8::vector4f:
+		{
+			const track_vector4f& track__ = track_cast<track_vector4f>(track_);
+
+			const rtm::vector4f value0 = track__[key_frame0];
+			const rtm::vector4f value1 = track__[key_frame1];
+			const rtm::vector4f value = rtm::vector_lerp(value0, value1, interpolation_alpha);
+			writer.write_vector4(track_index, value);
+			break;
+		}
+		default:
+			ACL_ASSERT(false, "Invalid track type");
+			break;
+		}
+	}
+
+	uint32_t track_array::get_raw_size() const
+	{
+		const uint32_t num_samples = get_num_samples_per_track();
+
+		uint32_t total_size = 0;
+		for (uint32_t track_index = 0; track_index < m_num_tracks; ++track_index)
+		{
+			const track& track_ = m_tracks[track_index];
+			total_size += num_samples * track_.get_sample_size();
+		}
+
+		return total_size;
+	}
+}
+
+ACL_IMPL_FILE_PRAGMA_POP
diff --git a/includes/acl/compression/track_error.h b/includes/acl/compression/track_error.h
new file mode 100644
--- /dev/null
+++ b/includes/acl/compression/track_error.h
@@ -0,0 +1,168 @@
+#pragma once
+
+////////////////////////////////////////////////////////////////////////////////
+// The MIT License (MIT)
+//
+// Copyright (c) 2019 Nicholas Frechette & Animation Compression Library contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+////////////////////////////////////////////////////////////////////////////////
+
+#include "acl/core/compiler_utils.h"
+#include "acl/core/compressed_tracks.h"
+#include "acl/core/error.h"
+#include "acl/core/iallocator.h"
+#include "acl/core/impl/debug_track_writer.h"
+#include "acl/compression/track_array.h"
+#include "acl/decompression/decompress.h"
+
+#include <rtm/scalarf.h>
+#include <rtm/vector4f.h>
+
+#include <cstdint>
+
+ACL_IMPL_FILE_PRAGMA_PUSH
+
+namespace acl
+{
+	//////////////////////////////////////////////////////////////////////////
+	// A struct that contains the raw track index that has the worst error,
+	// its error, and the sample time at which it happens.
+	//////////////////////////////////////////////////////////////////////////
+	struct track_error
+	{
+		track_error() : index(k_invalid_track_index), error(0.0f), sample_time(0.0f) {}
+
+		//////////////////////////////////////////////////////////////////////////
+		// The raw track index with the worst error.
+		uint32_t index;
+
+		//////////////////////////////////////////////////////////////////////////
+		// The worst error for the raw track index.
+		float error;
+
+		//////////////////////////////////////////////////////////////////////////
+		// The sample time that has the worst error.
+		float sample_time;
+	};
+
+	//////////////////////////////////////////////////////////////////////////
+	// Calculates the worst compression error between a raw track array and its
+	// compressed tracks.
+	inline track_error calculate_compression_error(IAllocator& allocator, const track_array& raw_tracks, const compressed_tracks& tracks)
+	{
+		using namespace acl_impl;
+
+		track_error result;
+
+		const float duration = tracks.get_duration();
+		const float sample_rate = tracks.get_sample_rate();
+		const uint32_t num_tracks = tracks.get_num_tracks();
+		const uint32_t num_samples = tracks.get_num_samples_per_track();
+		const track_type8 track_type = raw_tracks.get_track_type();
+
+		decompression_context<debug_decompression_settings> context;
+		context.initialize(tracks);
+
+		debug_track_writer raw_tracks_writer(allocator, track_type, num_tracks);
+		debug_track_writer raw_track_writer(allocator, track_type, num_tracks);
+		debug_track_writer lossy_tracks_writer(allocator, track_type, num_tracks);
+		debug_track_writer lossy_track_writer(allocator, track_type, num_tracks);
+
+		const rtm::vector4f zero = rtm::vector_zero();
+
+		// Regression test
+		for (uint32_t sample_index = 0; sample_index < num_samples; ++sample_index)
+		{
+			const float sample_time = min(float(sample_index) / sample_rate, duration);
+
+			raw_tracks.sample_tracks(sample_time, SampleRoundingPolicy::None, raw_tracks_writer);
+
+			context.seek(sample_time, SampleRoundingPolicy::None);
+			context.decompress_tracks(lossy_tracks_writer);
+
+			// Validate decompress_tracks
+			for (uint32_t track_index = 0; track_index < num_tracks; ++track_index)
+			{
+				const track& track_ = raw_tracks[track_index];
+				const uint32_t output_index = track_.get_output_index();
+				if (output_index == k_invalid_track_index)
+					continue;	// Track is being stripped, ignore it
+
+				rtm::vector4f error = zero;
+
+				switch (track_type)
+				{
+				case track_type8::float1f:
+				{
+					const float raw_value = raw_tracks_writer.read_float1(track_index);
+					const float lossy_value = lossy_tracks_writer.read_float1(output_index);
+					error = rtm::vector_set(rtm::scalar_abs(raw_value - lossy_value));
+					break;
+				}
+				case track_type8::float2f:
+				{
+					const rtm::vector4f raw_value = raw_tracks_writer.read_float2(track_index);
+					const rtm::vector4f lossy_value = lossy_tracks_writer.read_float2(output_index);
+					error = rtm::vector_abs(rtm::vector_sub(raw_value, lossy_value));
+					error = rtm::vector_mix<rtm::mix4::x, rtm::mix4::y, rtm::mix4::c, rtm::mix4::d>(error, zero);
+					break;
+				}
+				case track_type8::float3f:
+				{
+					const rtm::vector4f raw_value = raw_tracks_writer.read_float3(track_index);
+					const rtm::vector4f lossy_value = lossy_tracks_writer.read_float3(output_index);
+					error = rtm::vector_abs(rtm::vector_sub(raw_value, lossy_value));
+					error = rtm::vector_mix<rtm::mix4::x, rtm::mix4::y, rtm::mix4::z, rtm::mix4::d>(error, zero);
+					break;
+				}
+				case track_type8::float4f:
+				{
+					const rtm::vector4f raw_value = raw_tracks_writer.read_float4(track_index);
+					const rtm::vector4f lossy_value = lossy_tracks_writer.read_float4(output_index);
+					error = rtm::vector_abs(rtm::vector_sub(raw_value, lossy_value));
+					break;
+				}
+				case track_type8::vector4f:
+				{
+					const rtm::vector4f raw_value = raw_tracks_writer.read_vector4(track_index);
+					const rtm::vector4f lossy_value = lossy_tracks_writer.read_vector4(output_index);
+					error = rtm::vector_abs(rtm::vector_sub(raw_value, lossy_value));
+					break;
+				}
+				default:
+					ACL_ASSERT(false, "Unsupported track type");
+					break;
+				}
+
+				const float max_error = rtm::vector_get_max_component(error);
+				if (max_error > result.error)
+				{
+					result.error = max_error;
+					result.index = track_index;
+					result.sample_time = sample_time;
+				}
+			}
+		}
+
+		return result;
+	}
+}
+
+ACL_IMPL_FILE_PRAGMA_POP
diff --git a/includes/acl/core/compressed_tracks.h b/includes/acl/core/compressed_tracks.h
new file mode 100644
--- /dev/null
+++ b/includes/acl/core/compressed_tracks.h
@@ -0,0 +1,155 @@
+#pragma once
+
+////////////////////////////////////////////////////////////////////////////////
+// The MIT License (MIT)
+//
+// Copyright (c) 2019 Nicholas Frechette & Animation Compression Library contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+////////////////////////////////////////////////////////////////////////////////
+
+#include "acl/core/algorithm_types.h"
+#include "acl/core/algorithm_versions.h"
+#include "acl/core/compiler_utils.h"
+#include "acl/core/error_result.h"
+#include "acl/core/hash.h"
+#include "acl/core/utils.h"
+#include "acl/core/impl/compressed_headers.h"
+
+#include <cstdint>
+
+ACL_IMPL_FILE_PRAGMA_PUSH
+
+namespace acl
+{
+	class compressed_tracks;
+
+	namespace acl_impl
+	{
+		////////////////////////////////////////////////////////////////////////////////
+		// A known tag value to distinguish compressed tracks from other things.
+		static constexpr uint32_t k_compressed_tracks_tag = 0xac11ac11;
+
+		const tracks_header& get_tracks_header(const compressed_tracks& tracks);
+	}
+
+	////////////////////////////////////////////////////////////////////////////////
+	// An instance of a compressed tracks.
+	// The compressed data immediately follows this instance in memory.
+	// The total size of the buffer can be queried with `get_size()`.
+	////////////////////////////////////////////////////////////////////////////////
+	class alignas(16) compressed_tracks final
+	{
+	public:
+		////////////////////////////////////////////////////////////////////////////////
+		// Returns the algorithm type used to compress the tracks.
+		AlgorithmType8 get_algorithm_type() const { return m_tracks_header.algorithm_type; }
+
+		////////////////////////////////////////////////////////////////////////////////
+		// Returns the size in bytes of the compressed tracks.
+		// Includes the 'compressed_tracks' instance size.
+		uint32_t get_size() const { return m_buffer_header.size; }
+
+		//////////////////////////////////////////////////////////////////////////
+		// Returns the hash for the compressed tracks.
+		// This is only used for sanity checking in case of memory corruption.
+		uint32_t get_hash() const { return m_buffer_header.hash; }
+
+		//////////////////////////////////////////////////////////////////////////
+		// Returns the number of tracks contained.
+		uint32_t get_num_tracks() const { return m_tracks_header.num_tracks; }
+
+		//////////////////////////////////////////////////////////////////////////
+		// Returns the number of samples each track contains.
+		uint32_t get_num_samples_per_track() const { return m_tracks_header.num_samples; }
+
+		//////////////////////////////////////////////////////////////////////////
+		// Returns the duration of each track.
+		float get_duration() const { return calculate_duration(m_tracks_header.num_samples, m_tracks_header.sample_rate); }
+
+		//////////////////////////////////////////////////////////////////////////
+		// Returns the sample rate used by each track.
+		float get_sample_rate() const { return m_tracks_header.sample_rate; }
+
+		////////////////////////////////////////////////////////////////////////////////
+		// Returns true if the compressed tracks are valid and usable.
+		// This mainly validates some invariants as well as ensuring that the
+		// memory has not been corrupted.
+		//
+		// check_hash: If true, the compressed tracks hash will also be compared.
+		ErrorResult is_valid(bool check_hash) const
+		{
+			if (!is_aligned_to(this, alignof(compressed_tracks)))
+				return ErrorResult("Invalid alignment");
+
+			if (m_tracks_header.tag != acl_impl::k_compressed_tracks_tag)
+				return ErrorResult("Invalid tag");
+
+			if (!is_valid_algorithm_type(m_tracks_header.algorithm_type))
+				return ErrorResult("Invalid algorithm type");
+
+			if (m_tracks_header.version != get_algorithm_version(m_tracks_header.algorithm_type))
+				return ErrorResult("Invalid algorithm version");
+
+			if (check_hash)
+			{
+				const uint32_t hash = hash32(safe_ptr_cast<const uint8_t>(&m_tracks_header), m_buffer_header.size - sizeof(acl_impl::raw_buffer_header));
+				if (hash != m_buffer_header.hash)
+					return ErrorResult("Invalid hash");
+			}
+
+			return ErrorResult();
+		}
+
+	private:
+		////////////////////////////////////////////////////////////////////////////////
+		// Hide everything
+		compressed_tracks() = delete;
+		compressed_tracks(const compressed_tracks&) = delete;
+		compressed_tracks(compressed_tracks&&) = delete;
+		compressed_tracks* operator=(const compressed_tracks&) = delete;
+		compressed_tracks* operator=(compressed_tracks&&) = delete;
+
+		////////////////////////////////////////////////////////////////////////////////
+		// Raw buffer header that isn't included in the hash.
+		////////////////////////////////////////////////////////////////////////////////
+
+		acl_impl::raw_buffer_header		m_buffer_header;
+
+		////////////////////////////////////////////////////////////////////////////////
+		// Everything starting here is included in the hash.
+		////////////////////////////////////////////////////////////////////////////////
+
+		acl_impl::tracks_header			m_tracks_header;
+
+		//////////////////////////////////////////////////////////////////////////
+		// Compressed data follows here in memory.
+		//////////////////////////////////////////////////////////////////////////
+
+		friend const acl_impl::tracks_header& acl_impl::get_tracks_header(const compressed_tracks& tracks);
+	};
+
+	namespace acl_impl
+	{
+		// Hide this implementation, it shouldn't be needed in user-space
+		inline const tracks_header& get_tracks_header(const compressed_tracks& tracks) { return tracks.m_tracks_header; }
+	}
+}
+
+ACL_IMPL_FILE_PRAGMA_POP
diff --git a/includes/acl/core/impl/compressed_headers.h b/includes/acl/core/impl/compressed_headers.h
new file mode 100644
--- /dev/null
+++ b/includes/acl/core/impl/compressed_headers.h
@@ -0,0 +1,104 @@
+#pragma once
+
+////////////////////////////////////////////////////////////////////////////////
+// The MIT License (MIT)
+//
+// Copyright (c) 2019 Nicholas Frechette & Animation Compression Library contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+////////////////////////////////////////////////////////////////////////////////
+
+#include "acl/core/track_types.h"
+#include "acl/core/algorithm_types.h"
+#include "acl/core/compiler_utils.h"
+#include "acl/core/ptr_offset.h"
+
+#include <cstdint>
+
+ACL_IMPL_FILE_PRAGMA_PUSH
+
+namespace acl
+{
+	namespace acl_impl
+	{
+		struct raw_buffer_header
+		{
+			// Total size in bytes of the raw buffer.
+			uint32_t		size;
+
+			// Hash of the raw buffer.
+			uint32_t		hash;
+		};
+
+		struct track_metadata
+		{
+			uint8_t			bit_rate;
+		};
+
+		struct tracks_header
+		{
+			// Serialization tag used to distinguish raw buffer types.
+			uint32_t		tag;
+
+			// Serialization version used to compress the tracks.
+			uint16_t		version;
+
+			// Algorithm type used to compress the tracks.
+			AlgorithmType8	algorithm_type;
+
+			// Type of the tracks contained in this compressed stream.
+			track_type8		track_type;
+
+			// The total number of tracks.
+			uint32_t		num_tracks;
+
+			// The total number of samples per track.
+			uint32_t		num_samples;
+
+			// The sample rate our tracks use.
+			float			sample_rate;								// TODO: Store duration as float instead?
+
+			// The number of bits used for a whole frame of data.
+			// The sum of one sample per track with all bit rates taken into account.
+			uint32_t		num_bits_per_frame;
+
+			// Various data offsets relative to the start of this header.
+			PtrOffset32<track_metadata>		metadata_per_track;
+			PtrOffset32<float>				track_constant_values;
+			PtrOffset32<float>				track_range_values;
+			PtrOffset32<uint8_t>			track_animated_values;
+
+			//////////////////////////////////////////////////////////////////////////
+
+			track_metadata*			get_track_metadata() { return metadata_per_track.add_to(this); }
+			const track_metadata*	get_track_metadata() const { return metadata_per_track.add_to(this); }
+
+			float*					get_track_constant_values() { return track_constant_values.add_to(this); }
+			const float*			get_track_constant_values() const { return track_constant_values.add_to(this); }
+
+			float*					get_track_range_values() { return track_range_values.add_to(this); }
+			const float*			get_track_range_values() const { return track_range_values.add_to(this); }
+
+			uint8_t*				get_track_animated_values() { return track_animated_values.add_to(this); }
+			const uint8_t*			get_track_animated_values() const { return track_animated_values.add_to(this); }
+		};
+	}
+}
+
+ACL_IMPL_FILE_PRAGMA_POP
diff --git a/includes/acl/core/impl/debug_track_writer.h b/includes/acl/core/impl/debug_track_writer.h
new file mode 100644
--- /dev/null
+++ b/includes/acl/core/impl/debug_track_writer.h
@@ -0,0 +1,143 @@
+#pragma once
+
+////////////////////////////////////////////////////////////////////////////////
+// The MIT License (MIT)
+//
+// Copyright (c) 2019 Nicholas Frechette & Animation Compression Library contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+////////////////////////////////////////////////////////////////////////////////
+
+#include "acl/core/compiler_utils.h"
+#include "acl/core/iallocator.h"
+#include "acl/core/track_types.h"
+#include "acl/core/track_writer.h"
+
+#include <rtm/scalarf.h>
+#include <rtm/vector4f.h>
+
+#include <cstdint>
+
+ACL_IMPL_FILE_PRAGMA_PUSH
+
+namespace acl
+{
+	namespace acl_impl
+	{
+		struct debug_track_writer final : public track_writer
+		{
+			debug_track_writer(IAllocator& allocator_, track_type8 type_, uint32_t num_tracks_)
+				: allocator(allocator_)
+				, tracks_typed{ nullptr }
+				, buffer_size(0)
+				, num_tracks(num_tracks_)
+				, type(type_)
+			{
+				// Large enough to accommodate the largest type
+				buffer_size = sizeof(rtm::vector4f) * num_tracks_;
+				tracks_typed.any = allocator_.allocate(buffer_size, alignof(rtm::vector4f));
+			}
+
+			~debug_track_writer()
+			{
+				allocator.deallocate(tracks_typed.any, buffer_size);
+			}
+
+			//////////////////////////////////////////////////////////////////////////
+			// Called by the decoder to write out a value for a specified track index.
+			void write_float1(uint32_t track_index, rtm::scalarf_arg0 value)
+			{
+				rtm::scalar_store(value, &tracks_typed.float1f[track_index]);
+			}
+
+			float read_float1(uint32_t track_index) const
+			{
+				return tracks_typed.float1f[track_index];
+			}
+
+			//////////////////////////////////////////////////////////////////////////
+			// Called by the decoder to write out a value for a specified track index.
+			void write_float2(uint32_t track_index, rtm::vector4f_arg0 value)
+			{
+				rtm::vector_store2(value, &tracks_typed.float2f[track_index]);
+			}
+
+			rtm::vector4f read_float2(uint32_t track_index) const
+			{
+				return rtm::vector_load2(&tracks_typed.float2f[track_index]);
+			}
+
+			//////////////////////////////////////////////////////////////////////////
+			// Called by the decoder to write out a value for a specified track index.
+			void write_float3(uint32_t track_index, rtm::vector4f_arg0 value)
+			{
+				rtm::vector_store3(value, &tracks_typed.float3f[track_index]);
+			}
+
+			rtm::vector4f read_float3(uint32_t track_index) const
+			{
+				return rtm::vector_load3(&tracks_typed.float3f[track_index]);
+			}
+
+			//////////////////////////////////////////////////////////////////////////
+			// Called by the decoder to write out a value for a specified track index.
+			void write_float4(uint32_t track_index, rtm::vector4f_arg0 value)
+			{
+				rtm::vector_store(value, &tracks_typed.float4f[track_index]);
+			}
+
+			rtm::vector4f read_float4(uint32_t track_index) const
+			{
+				return rtm::vector_load(&tracks_typed.float4f[track_index]);
+			}
+
+			//////////////////////////////////////////////////////////////////////////
+			// Called by the decoder to write out a value for a specified track index.
+			void write_vector4(uint32_t track_index, rtm::vector4f_arg0 value)
+			{
+				tracks_typed.vector4f[track_index] = value;
+			}
+
+			rtm::vector4f read_vector4(uint32_t track_index) const
+			{
+				return tracks_typed.vector4f[track_index];
+			}
+
+			union ptr_union
+			{
+				void*			any;
+				float*			float1f;
+				rtm::float2f*	float2f;
+				rtm::float3f*	float3f;
+				rtm::float4f*	float4f;
+				rtm::vector4f*	vector4f;
+			};
+
+			IAllocator& allocator;
+
+			ptr_union tracks_typed;
+			size_t buffer_size;
+			uint32_t num_tracks;
+
+			track_type8 type;
+		};
+	}
+}
+
+ACL_IMPL_FILE_PRAGMA_POP
diff --git a/includes/acl/core/track_traits.h b/includes/acl/core/track_traits.h
new file mode 100644
--- /dev/null
+++ b/includes/acl/core/track_traits.h
@@ -0,0 +1,97 @@
+#pragma once
+
+////////////////////////////////////////////////////////////////////////////////
+// The MIT License (MIT)
+//
+// Copyright (c) 2019 Nicholas Frechette & Animation Compression Library contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+////////////////////////////////////////////////////////////////////////////////
+
+#include "acl/core/compiler_utils.h"
+#include "acl/core/track_types.h"
+
+#include <rtm/types.h>
+
+#include <cstdint>
+
+ACL_IMPL_FILE_PRAGMA_PUSH
+
+namespace acl
+{
+	//////////////////////////////////////////////////////////////////////////
+	// Type tracks for tracks.
+	// Each trait contains:
+	//    - The category of the track
+	//    - The type of each sample in the track
+	//    - The type of the track description
+	//////////////////////////////////////////////////////////////////////////
+	template<track_type8 track_type>
+	struct track_traits {};
+
+	//////////////////////////////////////////////////////////////////////////
+	// Specializations for each track type.
+
+	template<>
+	struct track_traits<track_type8::float1f>
+	{
+		static constexpr track_category8 category = track_category8::scalarf;
+
+		using sample_type = float;
+		using desc_type = track_desc_scalarf;
+	};
+
+	template<>
+	struct track_traits<track_type8::float2f>
+	{
+		static constexpr track_category8 category = track_category8::scalarf;
+
+		using sample_type = rtm::float2f;
+		using desc_type = track_desc_scalarf;
+	};
+
+	template<>
+	struct track_traits<track_type8::float3f>
+	{
+		static constexpr track_category8 category = track_category8::scalarf;
+
+		using sample_type = rtm::float3f;
+		using desc_type = track_desc_scalarf;
+	};
+
+	template<>
+	struct track_traits<track_type8::float4f>
+	{
+		static constexpr track_category8 category = track_category8::scalarf;
+
+		using sample_type = rtm::float4f;
+		using desc_type = track_desc_scalarf;
+	};
+
+	template<>
+	struct track_traits<track_type8::vector4f>
+	{
+		static constexpr track_category8 category = track_category8::scalarf;
+
+		using sample_type = rtm::vector4f;
+		using desc_type = track_desc_scalarf;
+	};
+}
+
+ACL_IMPL_FILE_PRAGMA_POP
diff --git a/includes/acl/core/track_types.h b/includes/acl/core/track_types.h
--- a/includes/acl/core/track_types.h
+++ b/includes/acl/core/track_types.h
@@ -25,6 +25,7 @@
 ////////////////////////////////////////////////////////////////////////////////
 
 #include "acl/core/compiler_utils.h"
+#include "acl/core/memory_utils.h"
 
 #include <cstdint>
 #include <cstring>
@@ -97,6 +98,132 @@ namespace acl
 
 	//////////////////////////////////////////////////////////////////////////
 
+	//////////////////////////////////////////////////////////////////////////
+	// We only support up to 4294967295 tracks. We reserve 4294967295 for the invalid index
+	constexpr uint32_t k_invalid_track_index = 0xFFFFFFFFu;
+
+	//////////////////////////////////////////////////////////////////////////
+	// The various supported track types.
+	// Note: be careful when changing values here as they might be serialized.
+	enum class track_type8 : uint8_t
+	{
+		float1f		= 0,
+		float2f		= 1,
+		float3f		= 2,
+		float4f		= 3,
+		vector4f	= 4,
+
+		//float1d	= 5,
+		//float2d	= 6,
+		//float3d	= 7,
+		//float4d	= 8,
+		//vector4d	= 9,
+
+		//quatf		= 10,
+		//quatd		= 11,
+
+		//qvvf		= 12,
+		//qvvd		= 13,
+
+		//int1i		= 14,
+		//int2i		= 15,
+		//int3i		= 16,
+		//int4i		= 17,
+		//vector4i	= 18,
+
+		//int1q		= 19,
+		//int2q		= 20,
+		//int3q		= 21,
+		//int4q		= 22,
+		//vector4q	= 23,
+	};
+
+	//////////////////////////////////////////////////////////////////////////
+	// The categories of track types.
+	enum class track_category8 : uint8_t
+	{
+		scalarf		= 0,
+		//scalard	= 1,
+		//scalari	= 2,
+		//scalarq	= 3,
+		//transformf = 4,
+		//transformd = 5,
+	};
+
+	//////////////////////////////////////////////////////////////////////////
+	// This structure describes the various settings for floating point scalar tracks.
+	// Used by: float1f, float2f, float3f, float4f, vector4f
+	struct track_desc_scalarf
+	{
+		//////////////////////////////////////////////////////////////////////////
+		// The track category for this description.
+		static constexpr track_category8 category = track_category8::scalarf;
+
+		//////////////////////////////////////////////////////////////////////////
+		// The track output index. When writing out the compressed data stream, this index
+		// will be used instead of the track index. This allows custom reordering for things
+		// like LOD sorting or skeleton remapping. A value of 'k_invalid_track_index' will strip the track
+		// from the compressed data stream. Output indices must be unique and contiguous.
+		uint32_t output_index;
+
+		//////////////////////////////////////////////////////////////////////////
+		// The per component precision threshold to try and attain when optimizing the bit rate.
+		// If the error is below the precision threshold, we will remove bits until we reach it without
+		// exceeding it. If the error is above the precision threshold, we will add more bits until
+		// we lower it underneath.
+		float precision;
+
+		//////////////////////////////////////////////////////////////////////////
+		// The per component precision threshold used to detect constant tracks.
+		// A constant track is a track that has a single repeating value across every sample.
+		// TODO: Use the precision?
+		float constant_threshold;
+	};
+
+#if 0	// TODO: Add support for this
+	//////////////////////////////////////////////////////////////////////////
+	// This structure describes the various settings for transform tracks.
+	// Used by: quatf, qvvf
+	struct track_desc_transformf
+	{
+		//////////////////////////////////////////////////////////////////////////
+		// The track category for this description.
+		static constexpr track_category8 category = track_category8::transformf;
+
+		//////////////////////////////////////////////////////////////////////////
+		// The track output index. When writing out the compressed data stream, this index
+		// will be used instead of the track index. This allows custom reordering for things
+		// like LOD sorting or skeleton remapping. A value of 'k_invalid_track_index' will strip the track
+		// from the compressed data stream. Output indices must be unique and contiguous.
+		uint32_t output_index;
+
+		//////////////////////////////////////////////////////////////////////////
+		// The index of the parent transform track or `k_invalid_track_index` if it has no parent.
+		uint32_t parent_index;
+
+		//////////////////////////////////////////////////////////////////////////
+		// The shell precision threshold to try and attain when optimizing the bit rate.
+		// If the error is below the precision threshold, we will remove bits until we reach it without
+		// exceeding it. If the error is above the precision threshold, we will add more bits until
+		// we lower it underneath.
+		float precision;
+
+		//////////////////////////////////////////////////////////////////////////
+		// The error is measured on a rigidly deformed shell around every transform at the specified distance.
+		float shell_distance;
+
+		//////////////////////////////////////////////////////////////////////////
+		// TODO: Use the precision and shell distance?
+		float constant_rotation_threshold;
+		float constant_translation_threshold;
+		float constant_scale_threshold;
+	};
+#endif
+
+	// TODO: Add transform description?
+
+	//////////////////////////////////////////////////////////////////////////
+
 	// Bit rate 0 is reserved for tracks that are constant in a segment
 	constexpr uint8_t k_bit_rate_num_bits[] = { 0, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 32 };
 
@@ -296,6 +423,92 @@ namespace acl
 	{
 		return format == VectorFormat8::Vector3_Variable;
 	}
+
+	//////////////////////////////////////////////////////////////////////////
+	// Returns the string representation for the provided track type.
+	// TODO: constexpr
+	inline const char* get_track_type_name(track_type8 type)
+	{
+		switch (type)
+		{
+		case track_type8::float1f:			return "float1f";
+		case track_type8::float2f:			return "float2f";
+		case track_type8::float3f:			return "float3f";
+		case track_type8::float4f:			return "float4f";
+		case track_type8::vector4f:			return "vector4f";
+		default:							return "<Invalid>";
+		}
+	}
+
+	//////////////////////////////////////////////////////////////////////////
+	// Returns the track type from its string representation.
+	// Returns true on success, false otherwise.
+	inline bool get_track_type(const char* type, track_type8& out_type)
+	{
+		// Entries in the same order as the enum integral value
+		static const char* k_track_type_names[] =
+		{
+			"float1f",
+			"float2f",
+			"float3f",
+			"float4f",
+			"vector4f",
+		};
+
+		static_assert(get_array_size(k_track_type_names) == (size_t)track_type8::vector4f + 1, "Unexpected array size");
+
+		for (size_t type_index = 0; type_index < get_array_size(k_track_type_names); ++type_index)
+		{
+			const char* type_name = k_track_type_names[type_index];
+			if (std::strncmp(type, type_name, std::strlen(type_name)) == 0)
+			{
+				out_type = safe_static_cast<track_type8>(type_index);
+				return true;
+			}
+		}
+
+		return false;
+	}
+
+	//////////////////////////////////////////////////////////////////////////
+	// Returns the track category for the provided track type.
+	inline track_category8 get_track_category(track_type8 type)
+	{
+		// Entries in the same order as the enum integral value
+		static constexpr track_category8 k_track_type_to_category[]
+		{
+			track_category8::scalarf,	// float1f
+			track_category8::scalarf,	// float2f
+			track_category8::scalarf,	// float3f
+			track_category8::scalarf,	// float4f
+			track_category8::scalarf,	// vector4f
+		};
+
+		static_assert(get_array_size(k_track_type_to_category) == (size_t)track_type8::vector4f + 1, "Unexpected array size");
+
+		ACL_ASSERT(type <= track_type8::vector4f, "Unexpected track type");
+		return type <= track_type8::vector4f ? k_track_type_to_category[static_cast<uint32_t>(type)] : track_category8::scalarf;
+	}
+
+	//////////////////////////////////////////////////////////////////////////
+	// Returns the num of elements within a sample for the provided track type.
+	inline uint32_t get_track_num_sample_elements(track_type8 type)
+	{
+		// Entries in the same order as the enum integral value
+		static constexpr uint32_t k_track_type_to_num_elements[]
+		{
+			1,	// float1f
+			2,	// float2f
+			3,	// float3f
+			4,	// float4f
+			4,	// vector4f
+		};
+
+		static_assert(get_array_size(k_track_type_to_num_elements) == (size_t)track_type8::vector4f + 1, "Unexpected array size");
+
+		ACL_ASSERT(type <= track_type8::vector4f, "Unexpected track type");
+		return type <= track_type8::vector4f ? k_track_type_to_num_elements[static_cast<uint32_t>(type)] : 0;
+	}
 }
 
 ACL_IMPL_FILE_PRAGMA_POP
diff --git a/includes/acl/core/track_writer.h b/includes/acl/core/track_writer.h
new file mode 100644
--- /dev/null
+++ b/includes/acl/core/track_writer.h
@@ -0,0 +1,87 @@
+#pragma once
+
+////////////////////////////////////////////////////////////////////////////////
+// The MIT License (MIT)
+//
+// Copyright (c) 2019 Nicholas Frechette & Animation Compression Library contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+////////////////////////////////////////////////////////////////////////////////
+
+#include "acl/core/compiler_utils.h"
+
+#include <rtm/types.h>
+
+#include <cstdint>
+
+ACL_IMPL_FILE_PRAGMA_PUSH
+
+namespace acl
+{
+	//////////////////////////////////////////////////////////////////////////
+	// We use a struct like this to allow an arbitrary format on the end user side.
+	// Since our decode function is templated on this type implemented by the user,
+	// the callbacks can trivially be inlined and customized.
+	// Only called functions need to be overridden and implemented.
+	//////////////////////////////////////////////////////////////////////////
+	struct track_writer
+	{
+		//////////////////////////////////////////////////////////////////////////
+		// Called by the decoder to write out a value for a specified track index.
+		void write_float1(uint32_t track_index, rtm::scalarf_arg0 value)
+		{
+			(void)track_index;
+			(void)value;
+		}
+
+		//////////////////////////////////////////////////////////////////////////
+		// Called by the decoder to write out a value for a specified track index.
+		void write_float2(uint32_t track_index, rtm::vector4f_arg0 value)
+		{
+			(void)track_index;
+			(void)value;
+		}
+
+		//////////////////////////////////////////////////////////////////////////
+		// Called by the decoder to write out a value for a specified track index.
+		void write_float3(uint32_t track_index, rtm::vector4f_arg0 value)
+		{
+			(void)track_index;
+			(void)value;
+		}
+
+		//////////////////////////////////////////////////////////////////////////
+		// Called by the decoder to write out a value for a specified track index.
+		void write_float4(uint32_t track_index, rtm::vector4f_arg0 value)
+		{
+			(void)track_index;
+			(void)value;
+		}
+
+		//////////////////////////////////////////////////////////////////////////
+		// Called by the decoder to write out a value for a specified track index.
+		void write_vector4(uint32_t track_index, rtm::vector4f_arg0 value)
+		{
+			(void)track_index;
+			(void)value;
+		}
+	};
+}
+
+ACL_IMPL_FILE_PRAGMA_POP
diff --git a/includes/acl/decompression/decompress.h b/includes/acl/decompression/decompress.h
new file mode 100644
--- /dev/null
+++ b/includes/acl/decompression/decompress.h
@@ -0,0 +1,670 @@
+#pragma once
+
+////////////////////////////////////////////////////////////////////////////////
+// The MIT License (MIT)
+//
+// Copyright (c) 2019 Nicholas Frechette & Animation Compression Library contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+////////////////////////////////////////////////////////////////////////////////
+
+#include "acl/core/compiler_utils.h"
+#include "acl/core/compressed_tracks.h"
+#include "acl/core/error.h"
+#include "acl/core/iallocator.h"
+#include "acl/core/interpolation_utils.h"
+#include "acl/core/track_traits.h"
+#include "acl/core/track_types.h"
+#include "acl/core/track_writer.h"
+#include "acl/decompression/impl/track_sampling_impl.h"
+#include "acl/math/rtm_casts.h"
+
+#include <rtm/types.h>
+#include <rtm/scalarf.h>
+#include <rtm/vector4f.h>
+
+#include <cstdint>
+#include <type_traits>
+
+ACL_IMPL_FILE_PRAGMA_PUSH
+
+namespace acl
+{
+	//////////////////////////////////////////////////////////////////////////
+	// Deriving from this struct and overriding these constexpr functions
+	// allow you to control which code is stripped for maximum performance.
+	// With these, you can:
+	//    - Support only a subset of the formats and statically strip the rest
+	//    - Force a single format and statically strip the rest
+	//    - Decide all of this at runtime by not making the overrides constexpr
+	//
+	// By default, all formats are supported.
+	//////////////////////////////////////////////////////////////////////////
+	struct decompression_settings
+	{
+		//////////////////////////////////////////////////////////////////////////
+		// Whether or not to clamp the sample time when `seek(..)` is called. Defaults to true.
+		constexpr bool clamp_sample_time() const { return true; }
+
+		//////////////////////////////////////////////////////////////////////////
+		// Whether or not the specified track type is supported. Defaults to true.
+		// If a track type is statically known not to be supported, the compiler can strip
+		// the associated code.
+		constexpr bool is_track_type_supported(track_type8 /*type*/) const { return true; }
+	};
+
+	//////////////////////////////////////////////////////////////////////////
+	// These are debug settings, everything is enabled and nothing is stripped.
+	// It will have the worst performance but allows every feature.
+	//////////////////////////////////////////////////////////////////////////
+	struct debug_decompression_settings : public decompression_settings {};
+
+	//////////////////////////////////////////////////////////////////////////
+	// These are the default settings. Only the generally optimal settings
+	// are enabled and will offer the overall best performance.
+	//////////////////////////////////////////////////////////////////////////
+	struct default_decompression_settings : public decompression_settings {};
+
+	//////////////////////////////////////////////////////////////////////////
+	// Decompression context for the uniformly sampled algorithm. The context
+	// allows various decompression actions to be performed on a compressed track list.
+	//
+	// Both the constructor and destructor are public because it is safe to place
+	// instances of this context on the stack or as member variables.
+	//
+	// This compression algorithm is the simplest by far and as such it offers
+	// the fastest compression and decompression. Every sample is retained and
+	// every track has the same number of samples playing back at the same
+	// sample rate. This means that when we sample at a particular time within
+	// the track list, we can trivially calculate the offsets required to read the
+	// desired data. All the data is sorted in order to ensure all reads are
+	// as contiguous as possible for optimal cache locality during decompression.
+	//////////////////////////////////////////////////////////////////////////
+	template<class decompression_settings_type>
+	class decompression_context
+	{
+	public:
+		//////////////////////////////////////////////////////////////////////////
+		// Constructs a context instance with an optional allocator instance.
+		// The default constructor for the `decompression_settings_type` will be used.
+		// If an allocator is provided, it will be used in `release()` to free the context.
+		inline decompression_context(IAllocator* allocator = nullptr);
+
+		//////////////////////////////////////////////////////////////////////////
+		// Constructs a context instance from a set of static settings and an optional allocator instance.
+		// If an allocator is provided, it will be used in `release()` to free the context.
+		inline decompression_context(const decompression_settings_type& settings, IAllocator* allocator = nullptr);
+
+		//////////////////////////////////////////////////////////////////////////
+		// Destructs a context instance.
+		inline ~decompression_context();
+
+		//////////////////////////////////////////////////////////////////////////
+		// Initializes the context instance to a particular compressed tracks instance.
+		inline void initialize(const compressed_tracks& tracks);
+
+		inline bool is_dirty(const compressed_tracks& tracks);
+
+		//////////////////////////////////////////////////////////////////////////
+		// Seeks within the compressed tracks to a particular point in time with the
+		// desired rounding policy.
+		inline void seek(float sample_time, SampleRoundingPolicy rounding_policy);
+
+		//////////////////////////////////////////////////////////////////////////
+		// Decompress every track at the current sample time.
+		// The track_writer_type allows complete control over how the tracks are written out.
+		template<class track_writer_type>
+		inline void decompress_tracks(track_writer_type& writer);
+
+		//////////////////////////////////////////////////////////////////////////
+		// Decompress a single track at the current sample time.
+		// The track_writer_type allows complete control over how the track is written out.
+		template<class track_writer_type>
+		inline void decompress_track(uint32_t track_index, track_writer_type& writer);
+
+		//////////////////////////////////////////////////////////////////////////
+		// Releases the context instance if it contains an allocator reference.
+		inline void release();
+
+	private:
+		decompression_context(const decompression_context& other) = delete;
+		decompression_context& operator=(const decompression_context& other) = delete;
+
+		// Internal context data
+		acl_impl::persistent_decompression_context m_context;
+
+		// The static settings used to strip out code at runtime
+		decompression_settings_type m_settings;
+
+		// The optional allocator instance used to allocate this instance
+		IAllocator* m_allocator;
+
+		static_assert(std::is_base_of<decompression_settings, decompression_settings_type>::value, "decompression_settings_type must derive from decompression_settings!");
+	};
+
+	//////////////////////////////////////////////////////////////////////////
+	// Allocates and constructs an instance of the decompression context
+	template<class decompression_settings_type>
+	inline decompression_context<decompression_settings_type>* make_decompression_context(IAllocator& allocator)
+	{
+		return allocate_type<decompression_context<decompression_settings_type>>(allocator, &allocator);
+	}
+
+	//////////////////////////////////////////////////////////////////////////
+	// Allocates and constructs an instance of the decompression context
+	template<class decompression_settings_type>
+	inline decompression_context<decompression_settings_type>* make_decompression_context(IAllocator& allocator, const decompression_settings_type& settings)
+	{
+		return allocate_type<decompression_context<decompression_settings_type>>(allocator, settings, &allocator);
+	}
+
+	//////////////////////////////////////////////////////////////////////////
+	// decompression_context implementation
+
+	template<class decompression_settings_type>
+	inline decompression_context<decompression_settings_type>::decompression_context(IAllocator* allocator)
+		: m_context()
+		, m_settings()
+		, m_allocator(allocator)
+	{
+		m_context.tracks = nullptr;		// Only member used to detect if we are initialized
+	}
+
+	template<class decompression_settings_type>
+	inline decompression_context<decompression_settings_type>::decompression_context(const decompression_settings_type& settings, IAllocator* allocator)
+		: m_context()
+		, m_settings(settings)
+		, m_allocator(allocator)
+	{
+		m_context.tracks = nullptr;		// Only member used to detect if we are initialized
+	}
+
+	template<class decompression_settings_type>
+	inline decompression_context<decompression_settings_type>::~decompression_context()
+	{
+		release();
+	}
+
+	template<class decompression_settings_type>
+	inline void decompression_context<decompression_settings_type>::initialize(const compressed_tracks& tracks)
+	{
+		ACL_ASSERT(tracks.is_valid(false).empty(), "Compressed tracks are not valid");
+		ACL_ASSERT(tracks.get_algorithm_type() == AlgorithmType8::UniformlySampled, "Invalid algorithm type [%s], expected [%s]", get_algorithm_name(tracks.get_algorithm_type()), get_algorithm_name(AlgorithmType8::UniformlySampled));
+
+		m_context.tracks = &tracks;
+		m_context.tracks_hash = tracks.get_hash();
+		m_context.duration = tracks.get_duration();
+		m_context.sample_time = -1.0f;
+		m_context.interpolation_alpha = 0.0;
+	}
+
+	template<class decompression_settings_type>
+	inline bool decompression_context<decompression_settings_type>::is_dirty(const compressed_tracks& tracks)
+	{
+		if (m_context.tracks != &tracks)
+			return true;
+
+		if (m_context.tracks_hash != tracks.get_hash())
+			return true;
+
+		return false;
+	}
+
+	template<class decompression_settings_type>
+	inline void decompression_context<decompression_settings_type>::seek(float sample_time, SampleRoundingPolicy rounding_policy)
+	{
+		ACL_ASSERT(m_context.is_initialized(), "Context is not initialized");
+		ACL_ASSERT(rtm::scalar_is_finite(sample_time), "Invalid sample time");
+
+		// Clamp for safety, the caller should normally handle this but in practice, it often isn't the case
+		if (m_settings.clamp_sample_time())
+			sample_time = rtm::scalar_clamp(sample_time, 0.0f, m_context.duration);
+
+		if (m_context.sample_time == sample_time)
+			return;
+
+		m_context.sample_time = sample_time;
+
+		const acl_impl::tracks_header& header = acl_impl::get_tracks_header(*m_context.tracks);
+
+		uint32_t key_frame0;
+		uint32_t key_frame1;
+		find_linear_interpolation_samples_with_sample_rate(header.num_samples, header.sample_rate, sample_time, rounding_policy, key_frame0, key_frame1, m_context.interpolation_alpha);
+
+		m_context.key_frame_bit_offsets[0] = key_frame0 * header.num_bits_per_frame;
+		m_context.key_frame_bit_offsets[1] = key_frame1 * header.num_bits_per_frame;
+	}
+
+	template<class decompression_settings_type>
+	template<class track_writer_type>
+	inline void decompression_context<decompression_settings_type>::decompress_tracks(track_writer_type& writer)
+	{
+		static_assert(std::is_base_of<track_writer, track_writer_type>::value, "track_writer_type must derive from track_writer");
+		ACL_ASSERT(m_context.is_initialized(), "Context is not initialized");
+
+		const acl_impl::tracks_header& header = acl_impl::get_tracks_header(*m_context.tracks);
+
+		const acl_impl::track_metadata* per_track_metadata = header.get_track_metadata();
+		const float* constant_values = header.get_track_constant_values();
+		const float* range_values = header.get_track_range_values();
+		const uint8_t* animated_values = header.get_track_animated_values();
+
+		uint32_t track_bit_offset0 = m_context.key_frame_bit_offsets[0];
+		uint32_t track_bit_offset1 = m_context.key_frame_bit_offsets[1];
+
+		for (uint32_t track_index = 0; track_index < header.num_tracks; ++track_index)
+		{
+			const acl_impl::track_metadata& metadata = per_track_metadata[track_index];
+			const uint8_t bit_rate = metadata.bit_rate;
+			const uint8_t num_bits_per_component = get_num_bits_at_bit_rate(bit_rate);
+
+			if (header.track_type == track_type8::float1f && m_settings.is_track_type_supported(track_type8::float1f))
+			{
+				rtm::scalarf value;
+				if (is_constant_bit_rate(bit_rate))
+				{
+					value = rtm::scalar_load(constant_values);
+					constant_values += 1;
+				}
+				else
+				{
+					rtm::scalarf value0;
+					rtm::scalarf value1;
+					if (is_raw_bit_rate(bit_rate))
+					{
+						value0 = acl_impl::unpack_scalarf_96_unsafe(animated_values, track_bit_offset0);
+						value1 = acl_impl::unpack_scalarf_96_unsafe(animated_values, track_bit_offset1);
+					}
+					else
+					{
+						value0 = acl_impl::unpack_scalarf_uXX_unsafe(num_bits_per_component, animated_values, track_bit_offset0);
+						value1 = acl_impl::unpack_scalarf_uXX_unsafe(num_bits_per_component, animated_values, track_bit_offset1);
+
+						const rtm::scalarf range_min = rtm::scalar_load(range_values);
+						const rtm::scalarf range_extent = rtm::scalar_load(range_values + 1);
+						value0 = rtm::scalar_mul_add(value0, range_extent, range_min);
+						value1 = rtm::scalar_mul_add(value1, range_extent, range_min);
+					}
+
+					value = rtm::scalar_lerp(value0, value1, m_context.interpolation_alpha);
+
+					const uint32_t num_sample_bits = num_bits_per_component;
+					track_bit_offset0 += num_sample_bits;
+					track_bit_offset1 += num_sample_bits;
+					range_values += 2;
+				}
+
+				writer.write_float1(track_index, value);
+			}
+			else if (header.track_type == track_type8::float2f && m_settings.is_track_type_supported(track_type8::float2f))
+			{
+				rtm::vector4f value;
+				if (is_constant_bit_rate(bit_rate))
+				{
+					value = rtm::vector_load(constant_values);
+					constant_values += 2;
+				}
+				else
+				{
+					rtm::vector4f value0;
+					rtm::vector4f value1;
+					if (is_raw_bit_rate(bit_rate))
+					{
+						value0 = vector_acl2rtm(unpack_vector2_64_unsafe(animated_values, track_bit_offset0));
+						value1 = vector_acl2rtm(unpack_vector2_64_unsafe(animated_values, track_bit_offset1));
+					}
+					else
+					{
+						value0 = vector_acl2rtm(unpack_vector2_uXX_unsafe(num_bits_per_component, animated_values, track_bit_offset0));
+						value1 = vector_acl2rtm(unpack_vector2_uXX_unsafe(num_bits_per_component, animated_values, track_bit_offset1));
+
+						const rtm::vector4f range_min = rtm::vector_load(range_values);
+						const rtm::vector4f range_extent = rtm::vector_load(range_values + 2);
+						value0 = rtm::vector_mul_add(value0, range_extent, range_min);
+						value1 = rtm::vector_mul_add(value1, range_extent, range_min);
+					}
+
+					value = rtm::vector_lerp(value0, value1, m_context.interpolation_alpha);
+
+					const uint32_t num_sample_bits = num_bits_per_component * 2;
+					track_bit_offset0 += num_sample_bits;
+					track_bit_offset1 += num_sample_bits;
+					range_values += 4;
+				}
+
+				writer.write_float2(track_index, value);
+			}
+			else if (header.track_type == track_type8::float3f && m_settings.is_track_type_supported(track_type8::float3f))
+			{
+				rtm::vector4f value;
+				if (is_constant_bit_rate(bit_rate))
+				{
+					value = rtm::vector_load(constant_values);
+					constant_values += 3;
+				}
+				else
+				{
+					rtm::vector4f value0;
+					rtm::vector4f value1;
+					if (is_raw_bit_rate(bit_rate))
+					{
+						value0 = vector_acl2rtm(unpack_vector3_96_unsafe(animated_values, track_bit_offset0));
+						value1 = vector_acl2rtm(unpack_vector3_96_unsafe(animated_values, track_bit_offset1));
+					}
+					else
+					{
+						value0 = vector_acl2rtm(unpack_vector3_uXX_unsafe(num_bits_per_component, animated_values, track_bit_offset0));
+						value1 = vector_acl2rtm(unpack_vector3_uXX_unsafe(num_bits_per_component, animated_values, track_bit_offset1));
+
+						const rtm::vector4f range_min = rtm::vector_load(range_values);
+						const rtm::vector4f range_extent = rtm::vector_load(range_values + 3);
+						value0 = rtm::vector_mul_add(value0, range_extent, range_min);
+						value1 = rtm::vector_mul_add(value1, range_extent, range_min);
+					}
+
+					value = rtm::vector_lerp(value0, value1, m_context.interpolation_alpha);
+
+					const uint32_t num_sample_bits = num_bits_per_component * 3;
+					track_bit_offset0 += num_sample_bits;
+					track_bit_offset1 += num_sample_bits;
+					range_values += 6;
+				}
+
+				writer.write_float3(track_index, value);
+			}
+			else if (header.track_type == track_type8::float4f && m_settings.is_track_type_supported(track_type8::float4f))
+			{
+				rtm::vector4f value;
+				if (is_constant_bit_rate(bit_rate))
+				{
+					value = rtm::vector_load(constant_values);
+					constant_values += 4;
+				}
+				else
+				{
+					rtm::vector4f value0;
+					rtm::vector4f value1;
+					if (is_raw_bit_rate(bit_rate))
+					{
+						value0 = vector_acl2rtm(unpack_vector4_128_unsafe(animated_values, track_bit_offset0));
+						value1 = vector_acl2rtm(unpack_vector4_128_unsafe(animated_values, track_bit_offset1));
+					}
+					else
+					{
+						value0 = vector_acl2rtm(unpack_vector4_uXX_unsafe(num_bits_per_component, animated_values, track_bit_offset0));
+						value1 = vector_acl2rtm(unpack_vector4_uXX_unsafe(num_bits_per_component, animated_values, track_bit_offset1));
+
+						const rtm::vector4f range_min = rtm::vector_load(range_values);
+						const rtm::vector4f range_extent = rtm::vector_load(range_values + 4);
+						value0 = rtm::vector_mul_add(value0, range_extent, range_min);
+						value1 = rtm::vector_mul_add(value1, range_extent, range_min);
+					}
+
+					value = rtm::vector_lerp(value0, value1, m_context.interpolation_alpha);
+
+					const uint32_t num_sample_bits = num_bits_per_component * 4;
+					track_bit_offset0 += num_sample_bits;
+					track_bit_offset1 += num_sample_bits;
+					range_values += 8;
+				}
+
+				writer.write_float4(track_index, value);
+			}
+			else if (header.track_type == track_type8::vector4f && m_settings.is_track_type_supported(track_type8::vector4f))
+			{
+				rtm::vector4f value;
+				if (is_constant_bit_rate(bit_rate))
+				{
+					value = rtm::vector_load(constant_values);
+					constant_values += 4;
+				}
+				else
+				{
+					rtm::vector4f value0;
+					rtm::vector4f value1;
+					if (is_raw_bit_rate(bit_rate))
+					{
+						value0 = vector_acl2rtm(unpack_vector4_128_unsafe(animated_values, track_bit_offset0));
+						value1 = vector_acl2rtm(unpack_vector4_128_unsafe(animated_values, track_bit_offset1));
+					}
+					else
+					{
+						value0 = vector_acl2rtm(unpack_vector4_uXX_unsafe(num_bits_per_component, animated_values, track_bit_offset0));
+						value1 = vector_acl2rtm(unpack_vector4_uXX_unsafe(num_bits_per_component, animated_values, track_bit_offset1));
+
+						const rtm::vector4f range_min = rtm::vector_load(range_values);
+						const rtm::vector4f range_extent = rtm::vector_load(range_values + 4);
+						value0 = rtm::vector_mul_add(value0, range_extent, range_min);
+						value1 = rtm::vector_mul_add(value1, range_extent, range_min);
+					}
+
+					value = rtm::vector_lerp(value0, value1, m_context.interpolation_alpha);
+
+					const uint32_t num_sample_bits = num_bits_per_component * 4;
+					track_bit_offset0 += num_sample_bits;
+					track_bit_offset1 += num_sample_bits;
+					range_values += 8;
+				}
+
+				writer.write_vector4(track_index, value);
+			}
+		}
+	}
+
+	template<class decompression_settings_type>
+	template<class track_writer_type>
+	inline void decompression_context<decompression_settings_type>::decompress_track(uint32_t track_index, track_writer_type& writer)
+	{
+		static_assert(std::is_base_of<track_writer, track_writer_type>::value, "track_writer_type must derive from track_writer");
+		ACL_ASSERT(m_context.is_initialized(), "Context is not initialized");
+		ACL_ASSERT(track_index < m_context.tracks->get_num_tracks(), "Invalid track index");
+
+		const acl_impl::tracks_header& header = acl_impl::get_tracks_header(*m_context.tracks);
+
+		const float* constant_values = header.get_track_constant_values();
+		const float* range_values = header.get_track_range_values();
+
+		const uint32_t num_element_components = get_track_num_sample_elements(header.track_type);
+		uint32_t track_bit_offset = 0;
+
+		const acl_impl::track_metadata* per_track_metadata = header.get_track_metadata();
+		for (uint32_t scan_track_index = 0; scan_track_index < track_index; ++scan_track_index)
+		{
+			const acl_impl::track_metadata& metadata = per_track_metadata[scan_track_index];
+			const uint8_t bit_rate = metadata.bit_rate;
+			const uint32_t num_bits_per_component = get_num_bits_at_bit_rate(bit_rate);
+			track_bit_offset += num_bits_per_component * num_element_components;
+
+			if (is_constant_bit_rate(bit_rate))
+				constant_values += num_element_components;
+			else
+				range_values += num_element_components * 2;
+		}
+
+		const acl_impl::track_metadata& metadata = per_track_metadata[track_index];
+		const uint8_t bit_rate = metadata.bit_rate;
+		const uint8_t num_bits_per_component = get_num_bits_at_bit_rate(bit_rate);
+
+		const uint8_t* animated_values = header.get_track_animated_values();
+
+		if (header.track_type == track_type8::float1f && m_settings.is_track_type_supported(track_type8::float1f))
+		{
+			rtm::scalarf value;
+			if (is_constant_bit_rate(bit_rate))
+				value = rtm::scalar_load(constant_values);
+			else
+			{
+				rtm::scalarf value0;
+				rtm::scalarf value1;
+				if (is_raw_bit_rate(bit_rate))
+				{
+					value0 = acl_impl::unpack_scalarf_96_unsafe(animated_values, m_context.key_frame_bit_offsets[0] + track_bit_offset);
+					value1 = acl_impl::unpack_scalarf_96_unsafe(animated_values, m_context.key_frame_bit_offsets[1] + track_bit_offset);
+				}
+				else
+				{
+					value0 = acl_impl::unpack_scalarf_uXX_unsafe(num_bits_per_component, animated_values, m_context.key_frame_bit_offsets[0] + track_bit_offset);
+					value1 = acl_impl::unpack_scalarf_uXX_unsafe(num_bits_per_component, animated_values, m_context.key_frame_bit_offsets[1] + track_bit_offset);
+
+					const rtm::scalarf range_min = rtm::scalar_load(range_values);
+					const rtm::scalarf range_extent = rtm::scalar_load(range_values + num_element_components);
+					value0 = rtm::scalar_mul_add(value0, range_extent, range_min);
+					value1 = rtm::scalar_mul_add(value1, range_extent, range_min);
+				}
+
+				value = rtm::scalar_lerp(value0, value1, m_context.interpolation_alpha);
+			}
+
+			writer.write_float1(track_index, value);
+		}
+		else if (header.track_type == track_type8::float2f && m_settings.is_track_type_supported(track_type8::float2f))
+		{
+			rtm::vector4f value;
+			if (is_constant_bit_rate(bit_rate))
+				value = rtm::vector_load(constant_values);
+			else
+			{
+				rtm::vector4f value0;
+				rtm::vector4f value1;
+				if (is_raw_bit_rate(bit_rate))
+				{
+					value0 = vector_acl2rtm(unpack_vector2_64_unsafe(animated_values, m_context.key_frame_bit_offsets[0] + track_bit_offset));
+					value1 = vector_acl2rtm(unpack_vector2_64_unsafe(animated_values, m_context.key_frame_bit_offsets[1] + track_bit_offset));
+				}
+				else
+				{
+					value0 = vector_acl2rtm(unpack_vector2_uXX_unsafe(num_bits_per_component, animated_values, m_context.key_frame_bit_offsets[0] + track_bit_offset));
+					value1 = vector_acl2rtm(unpack_vector2_uXX_unsafe(num_bits_per_component, animated_values, m_context.key_frame_bit_offsets[1] + track_bit_offset));
+
+					const rtm::vector4f range_min = rtm::vector_load(range_values);
+					const rtm::vector4f range_extent = rtm::vector_load(range_values + num_element_components);
+					value0 = rtm::vector_mul_add(value0, range_extent, range_min);
+					value1 = rtm::vector_mul_add(value1, range_extent, range_min);
+				}
+
+				value = rtm::vector_lerp(value0, value1, m_context.interpolation_alpha);
+			}
+
+			writer.write_float2(track_index, value);
+		}
+		else if (header.track_type == track_type8::float3f && m_settings.is_track_type_supported(track_type8::float3f))
+		{
+			rtm::vector4f value;
+			if (is_constant_bit_rate(bit_rate))
+				value = rtm::vector_load(constant_values);
+			else
+			{
+				rtm::vector4f value0;
+				rtm::vector4f value1;
+				if (is_raw_bit_rate(bit_rate))
+				{
+					value0 = vector_acl2rtm(unpack_vector3_96_unsafe(animated_values, m_context.key_frame_bit_offsets[0] + track_bit_offset));
+					value1 = vector_acl2rtm(unpack_vector3_96_unsafe(animated_values, m_context.key_frame_bit_offsets[1] + track_bit_offset));
+				}
+				else
+				{
+					value0 = vector_acl2rtm(unpack_vector3_uXX_unsafe(num_bits_per_component, animated_values, m_context.key_frame_bit_offsets[0] + track_bit_offset));
+					value1 = vector_acl2rtm(unpack_vector3_uXX_unsafe(num_bits_per_component, animated_values, m_context.key_frame_bit_offsets[1] + track_bit_offset));
+
+					const rtm::vector4f range_min = rtm::vector_load(range_values);
+					const rtm::vector4f range_extent = rtm::vector_load(range_values + num_element_components);
+					value0 = rtm::vector_mul_add(value0, range_extent, range_min);
+					value1 = rtm::vector_mul_add(value1, range_extent, range_min);
+				}
+
+				value = rtm::vector_lerp(value0, value1, m_context.interpolation_alpha);
+			}
+
+			writer.write_float3(track_index, value);
+		}
+		else if (header.track_type == track_type8::float4f && m_settings.is_track_type_supported(track_type8::float4f))
+		{
+			rtm::vector4f value;
+			if (is_constant_bit_rate(bit_rate))
+				value = rtm::vector_load(constant_values);
+			else
+			{
+				rtm::vector4f value0;
+				rtm::vector4f value1;
+				if (is_raw_bit_rate(bit_rate))
+				{
+					value0 = vector_acl2rtm(unpack_vector4_128_unsafe(animated_values, m_context.key_frame_bit_offsets[0] + track_bit_offset));
+					value1 = vector_acl2rtm(unpack_vector4_128_unsafe(animated_values, m_context.key_frame_bit_offsets[1] + track_bit_offset));
+				}
+				else
+				{
+					value0 = vector_acl2rtm(unpack_vector4_uXX_unsafe(num_bits_per_component, animated_values, m_context.key_frame_bit_offsets[0] + track_bit_offset));
+					value1 = vector_acl2rtm(unpack_vector4_uXX_unsafe(num_bits_per_component, animated_values, m_context.key_frame_bit_offsets[1] + track_bit_offset));
+
+					const rtm::vector4f range_min = rtm::vector_load(range_values);
+					const rtm::vector4f range_extent = rtm::vector_load(range_values + num_element_components);
+					value0 = rtm::vector_mul_add(value0, range_extent, range_min);
+					value1 = rtm::vector_mul_add(value1, range_extent, range_min);
+				}
+
+				value = rtm::vector_lerp(value0, value1, m_context.interpolation_alpha);
+			}
+
+			writer.write_float4(track_index, value);
+		}
+		else if (header.track_type == track_type8::vector4f && m_settings.is_track_type_supported(track_type8::vector4f))
+		{
+			rtm::vector4f value;
+			if (is_constant_bit_rate(bit_rate))
+				value = rtm::vector_load(constant_values);
+			else
+			{
+				rtm::vector4f value0;
+				rtm::vector4f value1;
+				if (is_raw_bit_rate(bit_rate))
+				{
+					value0 = vector_acl2rtm(unpack_vector4_128_unsafe(animated_values, m_context.key_frame_bit_offsets[0] + track_bit_offset));
+					value1 = vector_acl2rtm(unpack_vector4_128_unsafe(animated_values, m_context.key_frame_bit_offsets[1] + track_bit_offset));
+				}
+				else
+				{
+					value0 = vector_acl2rtm(unpack_vector4_uXX_unsafe(num_bits_per_component, animated_values, m_context.key_frame_bit_offsets[0] + track_bit_offset));
+					value1 = vector_acl2rtm(unpack_vector4_uXX_unsafe(num_bits_per_component, animated_values, m_context.key_frame_bit_offsets[1] + track_bit_offset));
+
+					const rtm::vector4f range_min = rtm::vector_load(range_values);
+					const rtm::vector4f range_extent = rtm::vector_load(range_values + num_element_components);
+					value0 = rtm::vector_mul_add(value0, range_extent, range_min);
+					value1 = rtm::vector_mul_add(value1, range_extent, range_min);
+				}
+
+				value = rtm::vector_lerp(value0, value1, m_context.interpolation_alpha);
+			}
+
+			writer.write_vector4(track_index, value);
+		}
+	}
+
+	template<class decompression_settings_type>
+	inline void decompression_context<decompression_settings_type>::release()
+	{
+		IAllocator* allocator = m_allocator;
+		if (allocator != nullptr)
+		{
+			m_allocator = nullptr;
+			deallocate_type<decompression_context>(*allocator, this);
+		}
+	}
+}
+
+ACL_IMPL_FILE_PRAGMA_POP
diff --git a/includes/acl/decompression/impl/track_sampling_impl.h b/includes/acl/decompression/impl/track_sampling_impl.h
new file mode 100644
--- /dev/null
+++ b/includes/acl/decompression/impl/track_sampling_impl.h
@@ -0,0 +1,172 @@
+#pragma once
+
+////////////////////////////////////////////////////////////////////////////////
+// The MIT License (MIT)
+//
+// Copyright (c) 2019 Nicholas Frechette & Animation Compression Library contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+////////////////////////////////////////////////////////////////////////////////
+
+#include "acl/core/compiler_utils.h"
+#include "acl/core/compressed_tracks.h"
+
+#include <rtm/scalarf.h>
+#include <rtm/vector4f.h>
+
+#include <cstdint>
+
+ACL_IMPL_FILE_PRAGMA_PUSH
+
+namespace acl
+{
+	namespace acl_impl
+	{
+		struct alignas(64) persistent_decompression_context
+		{
+			// Clip related data							//   offsets
+			const compressed_tracks* tracks;				//   0 |   0
+
+			uint32_t tracks_hash;							//   4 |   8
+
+			float duration;									//   8 |  12
+
+			// Seeking related data
+			float interpolation_alpha;						//  12 |  16
+			float sample_time;								//  16 |  20
+
+			uint32_t key_frame_bit_offsets[2];				//  20 |  24	// Variable quantization
+
+			uint8_t padding_tail[sizeof(void*) == 4 ? 36 : 32];
+
+			//////////////////////////////////////////////////////////////////////////
+
+			inline bool is_initialized() const { return tracks != nullptr; }
+		};
+
+		// Assumes the 'vector_data' is in big-endian order and is padded in order to load up to 16 bytes from it
+		inline rtm::scalarf ACL_SIMD_CALL unpack_scalarf_96_unsafe(const uint8_t* vector_data, uint32_t bit_offset)
+		{
+#if defined(ACL_SSE2_INTRINSICS)
+			const uint32_t byte_offset = bit_offset / 8;
+			const uint32_t shift_offset = bit_offset % 8;
+			uint64_t vector_u64 = unaligned_load<uint64_t>(vector_data + byte_offset + 0);
+			vector_u64 = byte_swap(vector_u64);
+			vector_u64 <<= shift_offset;
+			vector_u64 >>= 32;
+
+			const uint32_t x32 = uint32_t(vector_u64);
+
+			return _mm_castsi128_ps(_mm_set1_epi32(x32));
+#elif defined(ACL_NEON_INTRINSICS)
+			const uint32_t byte_offset = bit_offset / 8;
+			const uint32_t shift_offset = bit_offset % 8;
+			uint64_t vector_u64 = unaligned_load<uint64_t>(vector_data + byte_offset + 0);
+			vector_u64 = byte_swap(vector_u64);
+			vector_u64 <<= shift_offset;
+			vector_u64 >>= 32;
+
+			const uint64_t x64 = vector_u64;
+
+			const uint32x2_t xy = vcreate_u32(x64);
+			return vget_lane_f32(vreinterpret_f32_u32(xy), 0);
+#else
+			const uint32_t byte_offset = bit_offset / 8;
+			const uint32_t shift_offset = bit_offset % 8;
+			uint64_t vector_u64 = unaligned_load<uint64_t>(vector_data + byte_offset + 0);
+			vector_u64 = byte_swap(vector_u64);
+			vector_u64 <<= shift_offset;
+			vector_u64 >>= 32;
+
+			const uint64_t x64 = vector_u64;
+
+			const float x = aligned_load<float>(&x64);
+
+			return rtm::scalar_set(x);
+#endif
+		}
+
+		// Assumes the 'vector_data' is in big-endian order and padded in order to load up to 16 bytes from it
+		inline rtm::scalarf ACL_SIMD_CALL unpack_scalarf_uXX_unsafe(uint8_t num_bits, const uint8_t* vector_data, uint32_t bit_offset)
+		{
+			ACL_ASSERT(num_bits <= 19, "This function does not support reading more than 19 bits per component");
+
+			struct PackedTableEntry
+			{
+				constexpr PackedTableEntry(uint8_t num_bits_)
+					: max_value(num_bits_ == 0 ? 1.0f : (1.0f / float((1 << num_bits_) - 1)))
+					, mask((1 << num_bits_) - 1)
+				{}
+
+				float max_value;
+				uint32_t mask;
+			};
+
+			// TODO: We technically don't need the first 3 entries, which could save a few bytes
+			alignas(64) static constexpr PackedTableEntry k_packed_constants[20] =
+			{
+				PackedTableEntry(0), PackedTableEntry(1), PackedTableEntry(2), PackedTableEntry(3),
+				PackedTableEntry(4), PackedTableEntry(5), PackedTableEntry(6), PackedTableEntry(7),
+				PackedTableEntry(8), PackedTableEntry(9), PackedTableEntry(10), PackedTableEntry(11),
+				PackedTableEntry(12), PackedTableEntry(13), PackedTableEntry(14), PackedTableEntry(15),
+				PackedTableEntry(16), PackedTableEntry(17), PackedTableEntry(18), PackedTableEntry(19),
+			};
+
+#if defined(ACL_SSE2_INTRINSICS)
+			const uint32_t bit_shift = 32 - num_bits;
+			const uint32_t mask = k_packed_constants[num_bits].mask;
+			const __m128 inv_max_value = _mm_load_ps1(&k_packed_constants[num_bits].max_value);
+
+			uint32_t byte_offset = bit_offset / 8;
+			uint32_t vector_u32 = unaligned_load<uint32_t>(vector_data + byte_offset);
+			vector_u32 = byte_swap(vector_u32);
+			const uint32_t x32 = (vector_u32 >> (bit_shift - (bit_offset % 8)));
+
+			const __m128 value = _mm_cvtsi32_ss(inv_max_value, x32 & mask);
+			return _mm_mul_ss(value, inv_max_value);
+#elif defined(ACL_NEON_INTRINSICS)
+			const uint32_t bit_shift = 32 - num_bits;
+			const uint32_t mask = k_packed_constants[num_bits].mask;
+			const float inv_max_value = k_packed_constants[num_bits].max_value;
+
+			uint32_t byte_offset = bit_offset / 8;
+			uint32_t vector_u32 = unaligned_load<uint32_t>(vector_data + byte_offset);
+			vector_u32 = byte_swap(vector_u32);
+			const uint32_t x32 = (vector_u32 >> (bit_shift - (bit_offset % 8)));
+
+			const int32_t value_u32 = x32 & mask;
+			const float value_f32 = static_cast<float>(value_u32);
+			return value_f32 * inv_max_value;
+#else
+			const uint32_t bit_shift = 32 - num_bits;
+			const uint32_t mask = k_packed_constants[num_bits].mask;
+			const float inv_max_value = k_packed_constants[num_bits].max_value;
+
+			uint32_t byte_offset = bit_offset / 8;
+			uint32_t vector_u32 = unaligned_load<uint32_t>(vector_data + byte_offset);
+			vector_u32 = byte_swap(vector_u32);
+			const uint32_t x32 = (vector_u32 >> (bit_shift - (bit_offset % 8))) & mask;
+
+			return rtm::scalar_set(static_cast<float>(x32) * inv_max_value);
+#endif
+		}
+	}
+}
+
+ACL_IMPL_FILE_PRAGMA_POP
diff --git a/includes/acl/io/clip_reader.h b/includes/acl/io/clip_reader.h
--- a/includes/acl/io/clip_reader.h
+++ b/includes/acl/io/clip_reader.h
@@ -29,6 +29,7 @@
 #include "acl/io/clip_reader_error.h"
 #include "acl/compression/animation_clip.h"
 #include "acl/compression/compression_settings.h"
+#include "acl/compression/track_array.h"
 #include "acl/compression/skeleton.h"
 #include "acl/core/algorithm_types.h"
 #include "acl/core/compiler_utils.h"
@@ -42,6 +43,36 @@ ACL_IMPL_FILE_PRAGMA_PUSH
 
 namespace acl
 {
+	//////////////////////////////////////////////////////////////////////////
+	// Enum to describe each type of raw content that an SJSON ACL file might contain.
+	enum class sjson_file_type
+	{
+		unknown,
+		raw_clip,
+		raw_track_list,
+	};
+
+	//////////////////////////////////////////////////////////////////////////
+	// A raw clip with transform tracks
+	struct sjson_raw_clip
+	{
+		std::unique_ptr<AnimationClip, Deleter<AnimationClip>> clip;
+		std::unique_ptr<RigidSkeleton, Deleter<RigidSkeleton>> skeleton;
+
+		bool has_settings;
+		AlgorithmType8 algorithm_type;
+		CompressionSettings settings;
+	};
+
+	//////////////////////////////////////////////////////////////////////////
+	// A raw track list
+	struct sjson_raw_track_list
+	{
+		track_array track_list;
+	};
+
+	//////////////////////////////////////////////////////////////////////////
+	// An SJSON ACL file reader.
 	class ClipReader
 	{
 	public:
@@ -56,28 +87,94 @@ namespace acl
 		{
 		}
 
+		ACL_DEPRECATED("Use get_file_type() and read_raw_clip(..) instead, to be removed in v2.0")
 		bool read_settings(bool& out_has_settings, AlgorithmType8& out_algorithm_type, CompressionSettings& out_settings)
 		{
 			reset_state();
 
-			return read_version() && read_clip_header() && read_settings(&out_has_settings, &out_algorithm_type, &out_settings);
+			return read_version() && read_raw_clip_header() && read_settings(&out_has_settings, &out_algorithm_type, &out_settings);
 		}
 
+		ACL_DEPRECATED("Use get_file_type() and read_raw_clip(..) instead, to be removed in v2.0")
 		bool read_skeleton(std::unique_ptr<RigidSkeleton, Deleter<RigidSkeleton>>& skeleton)
 		{
 			reset_state();
 
-			return read_version() && read_clip_header() && read_settings(nullptr, nullptr, nullptr) && create_skeleton(skeleton);
+			return read_version() && read_raw_clip_header() && read_settings(nullptr, nullptr, nullptr) && create_skeleton(skeleton);
 		}
 
+		ACL_DEPRECATED("Use get_file_type() and read_raw_clip(..) instead, to be removed in v2.0")
 		bool read_clip(std::unique_ptr<AnimationClip, Deleter<AnimationClip>>& clip, const RigidSkeleton& skeleton)
 		{
 			reset_state();
 
-			return read_version() && read_clip_header() && read_settings(nullptr, nullptr, nullptr) && read_skeleton() && create_clip(clip, skeleton) && read_tracks(*clip, skeleton) && nothing_follows();
+			return read_version() && read_raw_clip_header() && read_settings(nullptr, nullptr, nullptr) && read_skeleton() && create_clip(clip, skeleton) && read_tracks(*clip, skeleton) && nothing_follows();
+		}
+
+		sjson_file_type get_file_type()
+		{
+			reset_state();
+
+			if (!read_version())
+				return sjson_file_type::unknown;
+
+			if (m_parser.object_begins("clip"))
+				return sjson_file_type::raw_clip;
+
+			if (m_parser.object_begins("track_list"))
+				return sjson_file_type::raw_track_list;
+
+			return sjson_file_type::unknown;
+		}
+
+		bool read_raw_clip(sjson_raw_clip& out_data)
+		{
+			reset_state();
+
+			if (!read_version())
+				return false;
+
+			if (!read_raw_clip_header())
+				return false;
+
+			if (!read_settings(&out_data.has_settings, &out_data.algorithm_type, &out_data.settings))
+				return false;
+
+			if (!create_skeleton(out_data.skeleton))
+				return false;
+
+			if (!create_clip(out_data.clip, *out_data.skeleton))
+				return false;
+
+			if (!read_tracks(*out_data.clip, *out_data.skeleton))
+				return false;
+
+			return nothing_follows();
+		}
+
+		bool read_raw_track_list(sjson_raw_track_list& out_data)
+		{
+			reset_state();
+
+			if (!read_version())
+				return false;
+
+			if (!read_raw_track_list_header())
+				return false;
+
+			bool has_settings;				// Not used
+			AlgorithmType8 algorithm_type;	// Not used
+			CompressionSettings settings;	// Not used
+			if (!read_settings(&has_settings, &algorithm_type, &settings))
+				return false;
+
+			if (!create_track_list(out_data.track_list))
+				return false;
+
+			return nothing_follows();
 		}
 
-		ClipReaderError get_error() { return m_error; }
+		ClipReaderError get_error() const { return m_error; }
 
 	private:
 		IAllocator& m_allocator;
@@ -108,7 +205,7 @@ namespace acl
 				return false;
 			}
 
-			if (m_version > 3)
+			if (m_version > 4)
 			{
 				set_error(ClipReaderError::UnsupportedVersion);
 				return false;
@@ -117,7 +214,7 @@ namespace acl
 			return true;
 		}
 
-		bool read_clip_header()
+		bool read_raw_clip_header()
 		{
 			sjson::StringView additive_format;
 
@@ -191,6 +288,49 @@ namespace acl
 			return false;
 		}
 
+		bool read_raw_track_list_header()
+		{
+			if (!m_parser.object_begins("track_list"))
+				goto error;
+
+			if (!m_parser.read("name", m_clip_name))
+				goto error;
+
+			double num_samples;
+			if (!m_parser.read("num_samples", num_samples))
+				goto error;
+
+			m_num_samples = static_cast<uint32_t>(num_samples);
+			if (static_cast<double>(m_num_samples) != num_samples)
+			{
+				set_error(ClipReaderError::UnsignedIntegerExpected);
+				return false;
+			}
+
+			double sample_rate;
+			if (!m_parser.read("sample_rate", sample_rate))
+				goto error;
+
+			m_sample_rate = static_cast<float>(sample_rate);
+			if (m_sample_rate <= 0.0f)
+			{
+				set_error(ClipReaderError::PositiveValueExpected);
+				return false;
+			}
+
+			// Optional value
+			m_parser.try_read("is_binary_exact", m_is_binary_exact, false);
+
+			if (!m_parser.object_ends())
+				goto error;
+
+			return true;
+
+		error:
+			m_error = m_parser.get_error();
+			return false;
+		}
+
 		bool read_settings(bool* out_has_settings, AlgorithmType8* out_algorithm_type, CompressionSettings* out_settings)
 		{
 			if (!m_parser.try_object_begins("settings"))
@@ -366,6 +506,21 @@ namespace acl
 			return UInt64ToDouble(value_u64).dbl;
 		}
 
+		static float hex_to_float(const sjson::StringView& value)
+		{
+			union UInt32ToFloat
+			{
+				uint32_t u32;
+				float flt;
+
+				constexpr explicit UInt32ToFloat(uint32_t u32_value) : u32(u32_value) {}
+			};
+
+			ACL_ASSERT(value.size() <= 16, "Invalid binary exact double value");
+			uint32_t value_u32 = std::strtoul(value.c_str(), nullptr, 16);
+			return UInt32ToFloat(value_u32).flt;
+		}
+
 		static Quat_64 hex_to_quat(const sjson::StringView values[4])
 		{
 			return quat_set(hex_to_double(values[0]), hex_to_double(values[1]), hex_to_double(values[2]), hex_to_double(values[3]));
@@ -376,6 +531,19 @@ namespace acl
 			return vector_set(hex_to_double(values[0]), hex_to_double(values[1]), hex_to_double(values[2]));
 		}
 
+		static rtm::float4f hex_to_float4f(const sjson::StringView values[4], uint32_t num_components)
+		{
+			ACL_ASSERT(num_components <= 4, "Invalid number of components");
+
+			rtm::float4f result = { 0.0f, 0.0f, 0.0f, 0.0f };
+			float* result_ptr = &result.x;
+
+			for (uint32_t component_index = 0; component_index < num_components; ++num_components)
+				result_ptr[component_index] = hex_to_float(values[component_index]);
+
+			return result;
+		}
+
 		bool process_each_bone(RigidBone* bones, uint16_t& num_bones)
 		{
 			bool counting = bones == nullptr;
@@ -483,6 +651,267 @@ namespace acl
 			return true;
 		}
 
+		bool process_track_list(track* tracks, uint32_t& num_tracks)
+		{
+			const bool counting = tracks == nullptr;
+			track dummy;
+			track_type8 track_list_type = track_type8::float1f;
+
+			num_tracks = 0;
+
+			if (!m_parser.array_begins("tracks"))
+				goto error;
+
+			for (uint32_t i = 0; !m_parser.try_array_ends(); ++i)
+			{
+				track& track_ = counting ? dummy : tracks[i];
+
+				if (!m_parser.object_begins())
+					goto error;
+
+				sjson::StringView name;
+				if (!m_parser.read("name", name))
+					goto error;
+
+				// TODO: Store track name somewhere for debugging purposes
+
+				sjson::StringView type;
+				if (!m_parser.read("type", type))
+					goto error;
+
+				track_type8 track_type;
+				if (!get_track_type(type.c_str(), track_type))
+				{
+					m_error.error = ClipReaderError::InvalidTrackType;
+					return false;
+				}
+
+				if (num_tracks == 0)
+					track_list_type = track_type;
+				else if (track_type != track_list_type)
+				{
+					m_error.error = ClipReaderError::InvalidTrackType;
+					return false;
+				}
+
+				const uint32_t num_components = get_track_num_sample_elements(track_type);
+				ACL_ASSERT(num_components > 0 && num_components <= 4, "Cannot have 0 or more than 4 components");
+
+				float precision;
+				m_parser.try_read("precision", precision, 0.0001f);
+
+				float constant_threshold;
+				m_parser.try_read("constant_threshold", constant_threshold, 0.00001f);
+
+				uint32_t output_index;
+				m_parser.try_read("output_index", output_index, i);
+
+				track_desc_scalarf scalar_desc;
+				scalar_desc.output_index = output_index;
+				scalar_desc.precision = precision;
+				scalar_desc.constant_threshold = constant_threshold;
+
+				if (!m_parser.array_begins("data"))
+					goto error;
+
+				union track_samples_ptr_union
+				{
+					void*			any;
+					float*			float1f;
+					rtm::float2f*	float2f;
+					rtm::float3f*	float3f;
+					rtm::float4f*	float4f;
+					rtm::vector4f*	vector4f;
+				};
+				track_samples_ptr_union track_samples_typed = { nullptr };
+
+				switch (track_type)
+				{
+				case track_type8::float1f:
+					track_samples_typed.float1f = allocate_type_array<float>(m_allocator, m_num_samples);
+					break;
+				case track_type8::float2f:
+					track_samples_typed.float2f = allocate_type_array<rtm::float2f>(m_allocator, m_num_samples);
+					break;
+				case track_type8::float3f:
+					track_samples_typed.float3f = allocate_type_array<rtm::float3f>(m_allocator, m_num_samples);
+					break;
+				case track_type8::float4f:
+					track_samples_typed.float4f = allocate_type_array<rtm::float4f>(m_allocator, m_num_samples);
+					break;
+				case track_type8::vector4f:
+					track_samples_typed.vector4f = allocate_type_array<rtm::vector4f>(m_allocator, m_num_samples);
+					break;
+				default:
+					ACL_ASSERT(false, "Unsupported track type");
+					break;
+				}
+
+				bool has_error = false;
+				for (uint32_t sample_index = 0; sample_index < m_num_samples; ++sample_index)
+				{
+					if (!m_parser.array_begins())
+					{
+						has_error = true;
+						break;
+					}
+
+					if (m_is_binary_exact)
+					{
+						// TODO: Add test that uses this
+						sjson::StringView values[4];
+						if (m_parser.read(values, num_components))
+						{
+							switch (track_type)
+							{
+							case track_type8::float1f:
+							case track_type8::float2f:
+							case track_type8::float3f:
+							case track_type8::float4f:
+							case track_type8::vector4f:
+							{
+								const rtm::float4f value = hex_to_float4f(values, num_components);
+								std::memcpy(track_samples_typed.float1f + (sample_index * num_components), &value, sizeof(float) * num_components);
+								break;
+							}
+							default:
+								ACL_ASSERT(false, "Unsupported track type");
+								break;
+							}
+						}
+						else
+						{
+							has_error = true;
+							break;
+						}
+					}
+					else
+					{
+						double values[4];
+						if (m_parser.read(values, num_components))
+						{
+							switch (track_type)
+							{
+							case track_type8::float1f:
+							case track_type8::float2f:
+							case track_type8::float3f:
+							case track_type8::float4f:
+							case track_type8::vector4f:
+							{
+								const rtm::float4f value = { static_cast<float>(values[0]), static_cast<float>(values[1]), static_cast<float>(values[2]), static_cast<float>(values[3])};
+								std::memcpy(track_samples_typed.float1f + (sample_index * num_components), &value, sizeof(float) * num_components);
+								break;
+							}
+							default:
+								ACL_ASSERT(false, "Unsupported track type");
+								break;
+							}
+						}
+						else
+						{
+							has_error = true;
+							break;
+						}
+					}
+
+					if (!has_error && !m_parser.array_ends())
+					{
+						has_error = true;
+						break;
+					}
+				}
+
+				if (!has_error && !m_parser.array_ends())
+				{
+					has_error = true;
+					break;
+				}
+
+				if (!has_error && !m_parser.object_ends())
+				{
+					has_error = true;
+					break;
+				}
+
+				if (has_error)
+				{
+					switch (track_type)
+					{
+					case track_type8::float1f:
+						deallocate_type_array<float>(m_allocator, track_samples_typed.float1f, m_num_samples);
+						break;
+					case track_type8::float2f:
+						deallocate_type_array<rtm::float2f>(m_allocator, track_samples_typed.float2f, m_num_samples);
+						break;
+					case track_type8::float3f:
+						deallocate_type_array<rtm::float3f>(m_allocator, track_samples_typed.float3f, m_num_samples);
+						break;
+					case track_type8::float4f:
+						deallocate_type_array<rtm::float4f>(m_allocator, track_samples_typed.float4f, m_num_samples);
+						break;
+					case track_type8::vector4f:
+						deallocate_type_array<rtm::vector4f>(m_allocator, track_samples_typed.vector4f, m_num_samples);
+						break;
+					default:
+						ACL_ASSERT(false, "Unsupported track type");
+						break;
+					}
+				}
+				else
+				{
+					switch (track_type)
+					{
+					case track_type8::float1f:
+						track_ = track_float1f::make_owner(scalar_desc, m_allocator, track_samples_typed.float1f, m_num_samples, m_sample_rate);
+						break;
+					case track_type8::float2f:
+						track_ = track_float2f::make_owner(scalar_desc, m_allocator, track_samples_typed.float2f, m_num_samples, m_sample_rate);
+						break;
+					case track_type8::float3f:
+						track_ = track_float3f::make_owner(scalar_desc, m_allocator, track_samples_typed.float3f, m_num_samples, m_sample_rate);
+						break;
+					case track_type8::float4f:
+						track_ = track_float4f::make_owner(scalar_desc, m_allocator, track_samples_typed.float4f, m_num_samples, m_sample_rate);
+						break;
+					case track_type8::vector4f:
+						track_ = track_vector4f::make_owner(scalar_desc, m_allocator, track_samples_typed.vector4f, m_num_samples, m_sample_rate);
+						break;
+					default:
+						ACL_ASSERT(false, "Unsupported track type");
+						break;
+					}
+				}
+
+				num_tracks++;
+			}
+
+			return true;
+
+		error:
+			m_error = m_parser.get_error();
+			return false;
+		}
+
+		bool create_track_list(track_array& track_list)
+		{
+			const sjson::ParserState before_tracks = m_parser.save_state();
+
+			uint32_t num_tracks;
+			if (!process_track_list(nullptr, num_tracks))
+				return false;
+
+			m_parser.restore_state(before_tracks);
+
+			track_list = track_array(m_allocator, num_tracks);
+
+			if (!process_track_list(track_list.begin(), num_tracks))
+				return false;
+
+			ACL_ASSERT(num_tracks == track_list.get_num_tracks(), "Number of tracks read mismatch");
+
+			return true;
+		}
+
 		bool read_tracks(AnimationClip& clip, const RigidSkeleton& skeleton)
 		{
 			std::unique_ptr<AnimationClip, Deleter<AnimationClip>> base_clip;
diff --git a/includes/acl/io/clip_reader_error.h b/includes/acl/io/clip_reader_error.h
--- a/includes/acl/io/clip_reader_error.h
+++ b/includes/acl/io/clip_reader_error.h
@@ -45,6 +45,7 @@ namespace acl
 			InvalidCompressionSetting,
 			InvalidAdditiveClipFormat,
 			PositiveValueExpected,
+			InvalidTrackType,
 		};
 
 		ClipReaderError()
@@ -76,6 +77,8 @@ namespace acl
 				return "Invalid additive clip format provided";
 			case PositiveValueExpected:
 				return "A positive value is expected here";
+			case InvalidTrackType:
+				return "Invalid raw track type";
 			default:
 				return sjson::ParserError::get_description();
 			}
diff --git a/includes/acl/math/rtm_casts.h b/includes/acl/math/rtm_casts.h
new file mode 100644
--- /dev/null
+++ b/includes/acl/math/rtm_casts.h
@@ -0,0 +1,63 @@
+#pragma once
+
+////////////////////////////////////////////////////////////////////////////////
+// The MIT License (MIT)
+//
+// Copyright (c) 2019 Nicholas Frechette & Animation Compression Library contributors
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+////////////////////////////////////////////////////////////////////////////////
+
+#include "acl/core/compiler_utils.h"
+#include "acl/math/vector4_32.h"
+
+#include <rtm/vector4f.h>
+
+#include <cstdint>
+
+ACL_IMPL_FILE_PRAGMA_PUSH
+
+namespace acl
+{
+	//////////////////////////////////////////////////////////////////////////
+	// Casts an ACL Vector4_32 into a RTM vector4f.
+	// When SIMD intrinsics are enabled, this is a no-op.
+	inline rtm::vector4f RTM_SIMD_CALL vector_acl2rtm(Vector4_32Arg0 value)
+	{
+#if defined(RTM_SSE2_INTRINSICS) || defined(RTM_NEON_INTRINSICS)
+		return value;
+#else
+		return rtm::vector4f{value.x, value.y, value.z, value.w};
+#endif
+	}
+
+	//////////////////////////////////////////////////////////////////////////
+	// Casts a RTM vector4f into an ACL Vector4_32.
+	// When SIMD intrinsics are enabled, this is a no-op.
+	inline Vector4_32 RTM_SIMD_CALL vector_rtm2acl(rtm::vector4f_arg0 value)
+	{
+#if defined(RTM_SSE2_INTRINSICS) || defined(RTM_NEON_INTRINSICS)
+		return value;
+#else
+		return Vector4_32{value.x, value.y, value.z, value.w};
+#endif
+	}
+}
+
+ACL_IMPL_FILE_PRAGMA_POP
diff --git a/make.py b/make.py
--- a/make.py
+++ b/make.py
@@ -10,7 +10,7 @@
 import zipfile
 
 # The current test/decompression data version in use
-current_test_data = 'test_data_v2'
+current_test_data = 'test_data_v3'
 current_decomp_data = 'decomp_data_v4'
 
 def parse_argv():
diff --git a/tools/acl_compressor/main_android/CMakeLists.txt b/tools/acl_compressor/main_android/CMakeLists.txt
--- a/tools/acl_compressor/main_android/CMakeLists.txt
+++ b/tools/acl_compressor/main_android/CMakeLists.txt
@@ -15,6 +15,7 @@ set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/lib")
 set(CMAKE_ANDROID_JAVA_SOURCE_DIR "${PROJECT_SOURCE_DIR}/java")
 
 include_directories("${PROJECT_SOURCE_DIR}/../../../includes")
+include_directories("${PROJECT_SOURCE_DIR}/../../../external/rtm/includes")
 include_directories("${PROJECT_SOURCE_DIR}/../../../external/sjson-cpp/includes")
 include_directories("${PROJECT_SOURCE_DIR}/../includes")
 
@@ -47,6 +48,7 @@ target_compile_options(${PROJECT_NAME} PRIVATE -g)
 # Disable SIMD if not needed
 if(NOT USE_SIMD_INSTRUCTIONS)
 	add_definitions(-DACL_NO_INTRINSICS)
+	add_definitions(-DRTM_NO_INTRINSICS)
 endif()
 
 target_include_directories(${PROJECT_NAME} PUBLIC jni)
diff --git a/tools/acl_compressor/main_generic/CMakeLists.txt b/tools/acl_compressor/main_generic/CMakeLists.txt
--- a/tools/acl_compressor/main_generic/CMakeLists.txt
+++ b/tools/acl_compressor/main_generic/CMakeLists.txt
@@ -4,6 +4,7 @@ project(acl_compressor CXX)
 set(CMAKE_CXX_STANDARD 11)
 
 include_directories("${PROJECT_SOURCE_DIR}/../../../includes")
+include_directories("${PROJECT_SOURCE_DIR}/../../../external/rtm/includes")
 include_directories("${PROJECT_SOURCE_DIR}/../../../external/sjson-cpp/includes")
 include_directories("${PROJECT_SOURCE_DIR}/../includes")
 
@@ -35,6 +36,7 @@ endif()
 
 # Abort on failure, easier to debug issues this way
 add_definitions(-DACL_ON_ASSERT_ABORT)
+add_definitions(-DRTM_ON_ASSERT_ABORT)
 add_definitions(-DSJSON_CPP_ON_ASSERT_ABORT)
 
 install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION bin)
diff --git a/tools/acl_compressor/main_ios/CMakeLists.txt b/tools/acl_compressor/main_ios/CMakeLists.txt
--- a/tools/acl_compressor/main_ios/CMakeLists.txt
+++ b/tools/acl_compressor/main_ios/CMakeLists.txt
@@ -16,6 +16,7 @@ set(MACOSX_BUNDLE_GUI_IDENTIFIER "com.acl.acl-compressor")
 set(MACOSX_BUNDLE_BUNDLE_NAME "acl-compressor")
 
 include_directories("${PROJECT_SOURCE_DIR}/../../../includes")
+include_directories("${PROJECT_SOURCE_DIR}/../../../external/rtm/includes")
 include_directories("${PROJECT_SOURCE_DIR}/../../../external/sjson-cpp/includes")
 include_directories("${PROJECT_SOURCE_DIR}/../includes")
 
@@ -37,4 +38,5 @@ add_executable(${PROJECT_NAME} MACOSX_BUNDLE ${ALL_COMMON_SOURCE_FILES} ${ALL_MA
 # Disable SIMD if not needed
 if(NOT USE_SIMD_INSTRUCTIONS)
 	add_definitions(-DACL_NO_INTRINSICS)
+	add_definitions(-DRTM_NO_INTRINSICS)
 endif()
diff --git a/tools/acl_compressor/sources/acl_compressor.cpp b/tools/acl_compressor/sources/acl_compressor.cpp
--- a/tools/acl_compressor/sources/acl_compressor.cpp
+++ b/tools/acl_compressor/sources/acl_compressor.cpp
@@ -45,13 +45,17 @@
 #include "acl/core/range_reduction_types.h"
 #include "acl/core/ansi_allocator.h"
 #include "acl/core/string.h"
-#include "acl/compression/skeleton.h"
+#include "acl/core/impl/debug_track_writer.h"
 #include "acl/compression/animation_clip.h"
+#include "acl/compression/compress.h"
+#include "acl/compression/skeleton.h"
+#include "acl/compression/skeleton_error_metric.h"
+#include "acl/compression/stream/write_decompression_stats.h"
+#include "acl/compression/track_error.h"
 #include "acl/compression/utils.h"
+#include "acl/decompression/decompress.h"
 #include "acl/io/clip_reader.h"
 #include "acl/io/clip_writer.h"							// Included just so we compile it to test for basic errors
-#include "acl/compression/skeleton_error_metric.h"
-#include "acl/compression/stream/write_decompression_stats.h"
 
 #include "acl/algorithm/uniformly_sampled/encoder.h"
 #include "acl/algorithm/uniformly_sampled/decoder.h"
@@ -497,6 +501,188 @@ static void validate_accuracy(IAllocator& allocator, const AnimationClip& clip,
 	deallocate_type_array(allocator, lossy_pose_transforms, num_bones);
 }
 
+static void validate_accuracy(IAllocator& allocator, const track_array& raw_tracks, const compressed_tracks& tracks, double regression_error_threshold)
+{
+	(void)allocator;
+	(void)raw_tracks;
+	(void)tracks;
+	(void)regression_error_threshold;
+
+#if defined(ACL_HAS_ASSERT_CHECKS)
+	using namespace acl_impl;
+
+	const float regression_error_thresholdf = static_cast<float>(regression_error_threshold);
+	const rtm::vector4f regression_error_thresholdv = rtm::vector_set(regression_error_thresholdf);
+	(void)regression_error_thresholdf;
+	(void)regression_error_thresholdv;
+
+	const float duration = tracks.get_duration();
+	const float sample_rate = tracks.get_sample_rate();
+	const uint32_t num_tracks = tracks.get_num_tracks();
+	const uint32_t num_samples = tracks.get_num_samples_per_track();
+	const track_type8 track_type = raw_tracks.get_track_type();
+
+	ACL_ASSERT(duration == raw_tracks.get_duration(), "Duration mismatch");
+	ACL_ASSERT(sample_rate == raw_tracks.get_sample_rate(), "Sample rate mismatch");
+	ACL_ASSERT(num_tracks <= raw_tracks.get_num_tracks(), "Num tracks mismatch");
+	ACL_ASSERT(num_samples == raw_tracks.get_num_samples_per_track(), "Num samples mismatch");
+
+	decompression_context<debug_decompression_settings> context;
+	context.initialize(tracks);
+
+	debug_track_writer raw_tracks_writer(allocator, track_type, num_tracks);
+	debug_track_writer raw_track_writer(allocator, track_type, num_tracks);
+	debug_track_writer lossy_tracks_writer(allocator, track_type, num_tracks);
+	debug_track_writer lossy_track_writer(allocator, track_type, num_tracks);
+
+	const rtm::vector4f zero = rtm::vector_zero();
+
+	// Regression test
+	for (uint32_t sample_index = 0; sample_index < num_samples; ++sample_index)
+	{
+		const float sample_time = min(float(sample_index) / sample_rate, duration);
+
+		raw_tracks.sample_tracks(sample_time, SampleRoundingPolicy::None, raw_tracks_writer);
+
+		context.seek(sample_time, SampleRoundingPolicy::None);
+		context.decompress_tracks(lossy_tracks_writer);
+
+		// Validate decompress_tracks
+		for (uint32_t track_index = 0; track_index < num_tracks; ++track_index)
+		{
+			const track& track_ = raw_tracks[track_index];
+			const uint32_t output_index = track_.get_output_index();
+			if (output_index == k_invalid_track_index)
+				continue;	// Track is being stripped, ignore it
+
+			rtm::vector4f error = zero;
+
+			switch (track_type)
+			{
+			case track_type8::float1f:
+			{
+				const float raw_value = raw_tracks_writer.read_float1(track_index);
+				const float lossy_value = lossy_tracks_writer.read_float1(output_index);
+				error = rtm::vector_set(rtm::scalar_abs(raw_value - lossy_value));
+				break;
+			}
+			case track_type8::float2f:
+			{
+				const rtm::vector4f raw_value = raw_tracks_writer.read_float2(track_index);
+				const rtm::vector4f lossy_value = lossy_tracks_writer.read_float2(output_index);
+				error = rtm::vector_abs(rtm::vector_sub(raw_value, lossy_value));
+				error = rtm::vector_mix<rtm::mix4::x, rtm::mix4::y, rtm::mix4::c, rtm::mix4::d>(error, zero);
+				break;
+			}
+			case track_type8::float3f:
+			{
+				const rtm::vector4f raw_value = raw_tracks_writer.read_float3(track_index);
+				const rtm::vector4f lossy_value = lossy_tracks_writer.read_float3(output_index);
+				error = rtm::vector_abs(rtm::vector_sub(raw_value, lossy_value));
+				error = rtm::vector_mix<rtm::mix4::x, rtm::mix4::y, rtm::mix4::z, rtm::mix4::d>(error, zero);
+				break;
+			}
+			case track_type8::float4f:
+			{
+				const rtm::vector4f raw_value = raw_tracks_writer.read_float4(track_index);
+				const rtm::vector4f lossy_value = lossy_tracks_writer.read_float4(output_index);
+				error = rtm::vector_abs(rtm::vector_sub(raw_value, lossy_value));
+				break;
+			}
+			case track_type8::vector4f:
+			{
+				const rtm::vector4f raw_value = raw_tracks_writer.read_vector4(track_index);
+				const rtm::vector4f lossy_value = lossy_tracks_writer.read_vector4(output_index);
+				error = rtm::vector_abs(rtm::vector_sub(raw_value, lossy_value));
+				break;
+			}
+			default:
+				ACL_ASSERT(false, "Unsupported track type");
+				break;
+			}
+
+			(void)error;
+			ACL_ASSERT(rtm::vector_is_finite(error), "Returned error is not a finite value");
+			ACL_ASSERT(rtm::vector_all_less_than(error, regression_error_thresholdv), "Error too high for track %u at time %f", track_index, sample_time);
+		}
+
+		// Validate decompress_track
+		for (uint32_t track_index = 0; track_index < num_tracks; ++track_index)
+		{
+			const track& track_ = raw_tracks[track_index];
+			const uint32_t output_index = track_.get_output_index();
+			if (output_index == k_invalid_track_index)
+				continue;	// Track is being stripped, ignore it
+
+			raw_tracks.sample_track(track_index, sample_time, SampleRoundingPolicy::None, raw_track_writer);
+			context.decompress_track(output_index, lossy_track_writer);
+
+			switch (track_type)
+			{
+			case track_type8::float1f:
+			{
+				const float raw_value_ = raw_tracks_writer.read_float1(track_index);
+				const float lossy_value_ = lossy_tracks_writer.read_float1(output_index);
+				const float raw_value = raw_track_writer.read_float1(track_index);
+				const float lossy_value = lossy_track_writer.read_float1(output_index);
+				ACL_ASSERT(rtm::scalar_near_equal(raw_value, lossy_value, regression_error_thresholdf), "Error too high for track %u at time %f", track_index, sample_time);
+				ACL_ASSERT(rtm::scalar_near_equal(raw_value_, raw_value), "Failed to sample track %u at time %f", track_index, sample_time);
+				ACL_ASSERT(rtm::scalar_near_equal(lossy_value_, lossy_value), "Failed to sample track %u at time %f", track_index, sample_time);
+				break;
+			}
+			case track_type8::float2f:
+			{
+				const rtm::vector4f raw_value_ = raw_tracks_writer.read_float2(track_index);
+				const rtm::vector4f lossy_value_ = lossy_tracks_writer.read_float2(output_index);
+				const rtm::vector4f raw_value = raw_track_writer.read_float2(track_index);
+				const rtm::vector4f lossy_value = lossy_track_writer.read_float2(output_index);
+				ACL_ASSERT(rtm::vector_all_near_equal2(raw_value, lossy_value, regression_error_thresholdf), "Error too high for track %u at time %f", track_index, sample_time);
+				ACL_ASSERT(rtm::vector_all_near_equal2(raw_value_, raw_value), "Failed to sample track %u at time %f", track_index, sample_time);
+				ACL_ASSERT(rtm::vector_all_near_equal2(lossy_value_, lossy_value), "Failed to sample track %u at time %f", track_index, sample_time);
+				break;
+			}
+			case track_type8::float3f:
+			{
+				const rtm::vector4f raw_value_ = raw_tracks_writer.read_float3(track_index);
+				const rtm::vector4f lossy_value_ = lossy_tracks_writer.read_float3(output_index);
+				const rtm::vector4f raw_value = raw_track_writer.read_float3(track_index);
+				const rtm::vector4f lossy_value = lossy_track_writer.read_float3(output_index);
+				ACL_ASSERT(rtm::vector_all_near_equal3(raw_value, lossy_value, regression_error_thresholdf), "Error too high for track %u at time %f", track_index, sample_time);
+				ACL_ASSERT(rtm::vector_all_near_equal3(raw_value_, raw_value), "Failed to sample track %u at time %f", track_index, sample_time);
+				ACL_ASSERT(rtm::vector_all_near_equal3(lossy_value_, lossy_value), "Failed to sample track %u at time %f", track_index, sample_time);
+				break;
+			}
+			case track_type8::float4f:
+			{
+				const rtm::vector4f raw_value_ = raw_tracks_writer.read_float4(track_index);
+				const rtm::vector4f lossy_value_ = lossy_tracks_writer.read_float4(output_index);
+				const rtm::vector4f raw_value = raw_track_writer.read_float4(track_index);
+				const rtm::vector4f lossy_value = lossy_track_writer.read_float4(output_index);
+				ACL_ASSERT(rtm::vector_all_near_equal(raw_value, lossy_value, regression_error_thresholdf), "Error too high for track %u at time %f", track_index, sample_time);
+				ACL_ASSERT(rtm::vector_all_near_equal(raw_value_, raw_value), "Failed to sample track %u at time %f", track_index, sample_time);
+				ACL_ASSERT(rtm::vector_all_near_equal(lossy_value_, lossy_value), "Failed to sample track %u at time %f", track_index, sample_time);
+				break;
+			}
+			case track_type8::vector4f:
+			{
+				const rtm::vector4f raw_value_ = raw_tracks_writer.read_vector4(track_index);
+				const rtm::vector4f lossy_value_ = lossy_tracks_writer.read_vector4(output_index);
+				const rtm::vector4f raw_value = raw_track_writer.read_vector4(track_index);
+				const rtm::vector4f lossy_value = lossy_track_writer.read_vector4(output_index);
+				ACL_ASSERT(rtm::vector_all_near_equal(raw_value, lossy_value, regression_error_thresholdf), "Error too high for track %u at time %f", track_index, sample_time);
+				ACL_ASSERT(rtm::vector_all_near_equal(raw_value_, raw_value), "Failed to sample track %u at time %f", track_index, sample_time);
+				ACL_ASSERT(rtm::vector_all_near_equal(lossy_value_, lossy_value), "Failed to sample track %u at time %f", track_index, sample_time);
+				break;
+			}
+			default:
+				ACL_ASSERT(false, "Unsupported track type");
+				break;
+			}
+		}
+	}
+#endif	// defined(ACL_HAS_ASSERT_CHECKS)
+}
+
 static void try_algorithm(const Options& options, IAllocator& allocator, const AnimationClip& clip, const CompressionSettings& settings, AlgorithmType8 algorithm_type, StatLogging logging, sjson::ArrayWriter* runs_writer, double regression_error_threshold)
 {
 	(void)runs_writer;
@@ -576,12 +762,64 @@ static void try_algorithm(const Options& options, IAllocator& allocator, const A
 		try_algorithm_impl(nullptr);
 }
 
-static bool read_clip(IAllocator& allocator, const Options& options,
-					  std::unique_ptr<AnimationClip, Deleter<AnimationClip>>& out_clip,
-					  std::unique_ptr<RigidSkeleton, Deleter<RigidSkeleton>>& out_skeleton,
-					  bool& has_settings,
-					  AlgorithmType8& out_algorithm_type,
-					  CompressionSettings& out_settings)
+static void try_algorithm(const Options& options, IAllocator& allocator, const track_array& track_list, StatLogging logging, sjson::ArrayWriter* runs_writer, double regression_error_threshold)
+{
+	(void)runs_writer;
+
+	auto try_algorithm_impl = [&](sjson::ObjectWriter* stats_writer)
+	{
+		if (track_list.get_num_tracks() == 0)
+			return;
+
+		compression_settings settings;
+
+		OutputStats stats(logging, stats_writer);
+		compressed_tracks* compressed_tracks_ = nullptr;
+		const ErrorResult error_result = compress_track_list(allocator, track_list, settings, compressed_tracks_, stats);
+
+		ACL_ASSERT(error_result.empty(), error_result.c_str()); (void)error_result;
+		ACL_ASSERT(compressed_tracks_->is_valid(true).empty(), "Compressed tracks are invalid");
+
+#if defined(SJSON_CPP_WRITER)
+		if (logging != StatLogging::None)
+		{
+			const track_error error = calculate_compression_error(allocator, track_list, *compressed_tracks_);
+
+			stats_writer->insert("max_error", error.error);
+			stats_writer->insert("worst_track", error.index);
+			stats_writer->insert("worst_time", error.sample_time);
+
+			// TODO: measure decompression performance
+			//if (are_any_enum_flags_set(logging, StatLogging::SummaryDecompression))
+				//write_decompression_performance_stats(allocator, settings, *compressed_clip, logging, *stats_writer);
+		}
+#endif
+
+		if (options.regression_testing)
+			validate_accuracy(allocator, track_list, *compressed_tracks_, regression_error_threshold);
+
+		if (options.output_bin_filename != nullptr)
+		{
+			std::ofstream output_file_stream(options.output_bin_filename, std::ios_base::out | std::ios_base::binary | std::ios_base::trunc);
+			if (output_file_stream.is_open())
+				output_file_stream.write(reinterpret_cast<const char*>(compressed_tracks_), compressed_tracks_->get_size());
+		}
+
+		allocator.deallocate(compressed_tracks_, compressed_tracks_->get_size());
+	};
+
+#if defined(SJSON_CPP_WRITER)
+	if (runs_writer != nullptr)
+		runs_writer->push([&](sjson::ObjectWriter& writer) { try_algorithm_impl(&writer); });
+	else
+#endif
+		try_algorithm_impl(nullptr);
+}
+
+static bool read_acl_sjson_file(IAllocator& allocator, const Options& options,
+	sjson_file_type& out_file_type,
+	sjson_raw_clip& out_raw_clip,
+	sjson_raw_track_list& out_raw_track_list)
 {
 	char* sjson_file_buffer = nullptr;
 	size_t file_size = 0;
@@ -636,18 +874,33 @@ static bool read_clip(IAllocator& allocator, const Options& options,
 	ClipReader reader(allocator, sjson_file_buffer, file_size - 1);
 #endif
 
-	if (!reader.read_settings(has_settings, out_algorithm_type, out_settings)
-		|| !reader.read_skeleton(out_skeleton)
-		|| !reader.read_clip(out_clip, *out_skeleton))
+	const sjson_file_type ftype = reader.get_file_type();
+	out_file_type = ftype;
+
+	bool success = false;
+	switch (ftype)
 	{
-		ClipReaderError err = reader.get_error();
-		printf("\nError on line %d column %d: %s\n", err.line, err.column, err.get_description());
-		deallocate_type_array(allocator, sjson_file_buffer, file_size);
-		return false;
+	case sjson_file_type::unknown:
+	default:
+		printf("\nUnknown file type\n");
+		break;
+	case sjson_file_type::raw_clip:
+		success = reader.read_raw_clip(out_raw_clip);
+		break;
+	case sjson_file_type::raw_track_list:
+		success = reader.read_raw_track_list(out_raw_track_list);
+		break;
+	}
+
+	if (!success)
+	{
+		const ClipReaderError err = reader.get_error();
+		if (err.error != ClipReaderError::None)
+			printf("\nError on line %d column %d: %s\n", err.line, err.column, err.get_description());
 	}
 
 	deallocate_type_array(allocator, sjson_file_buffer, file_size);
-	return true;
+	return success;
 }
 
 static bool read_config(IAllocator& allocator, const Options& options, AlgorithmType8& out_algorithm_type, CompressionSettings& out_settings, double& out_regression_error_threshold)
@@ -908,8 +1161,21 @@ static int safe_main_impl(int argc, char* argv[])
 	AlgorithmType8 algorithm_type = AlgorithmType8::UniformlySampled;
 	CompressionSettings settings;
 
-	if (!is_input_acl_bin_file && !read_clip(allocator, options, clip, skeleton, use_external_config, algorithm_type, settings))
-		return -1;
+	sjson_file_type sjson_type = sjson_file_type::unknown;
+	sjson_raw_clip sjson_clip;
+	sjson_raw_track_list sjson_track_list;
+
+	if (!is_input_acl_bin_file)
+	{
+		if (!read_acl_sjson_file(allocator, options, sjson_type, sjson_clip, sjson_track_list))
+			return -1;
+
+		clip = std::move(sjson_clip.clip);
+		skeleton = std::move(sjson_clip.skeleton);
+		use_external_config = sjson_clip.has_settings;
+		algorithm_type = sjson_clip.algorithm_type;
+		settings = sjson_clip.settings;
+	}
 
 	double regression_error_threshold = 0.1;
 
@@ -919,7 +1185,7 @@ static int safe_main_impl(int argc, char* argv[])
 	if (options.config_filename != nullptr && std::strlen(options.config_filename) != 0)
 #endif
 	{
-		// Override whatever the ACL clip might have contained
+		// Override whatever the ACL SJSON file might have contained
 		algorithm_type = AlgorithmType8::UniformlySampled;
 		settings = CompressionSettings();
 
@@ -929,9 +1195,10 @@ static int safe_main_impl(int argc, char* argv[])
 		use_external_config = true;
 	}
 
+	// TODO: Make a unique_ptr
 	AnimationClip* base_clip = nullptr;
 
-	if (!is_input_acl_bin_file)
+	if (!is_input_acl_bin_file && sjson_type == sjson_file_type::raw_clip)
 	{
 		// Grab whatever clip we might have read from the sjson file and cast the const away so we can manage the memory
 		base_clip = const_cast<AnimationClip*>(clip->get_additive_base());
@@ -1004,84 +1271,91 @@ static int safe_main_impl(int argc, char* argv[])
 			}
 #endif
 		}
-		else if (use_external_config)
+		else if (sjson_type == sjson_file_type::raw_clip)
 		{
-			ACL_ASSERT(algorithm_type == AlgorithmType8::UniformlySampled, "Only UniformlySampled is supported for now");
+			if (use_external_config)
+			{
+				ACL_ASSERT(algorithm_type == AlgorithmType8::UniformlySampled, "Only UniformlySampled is supported for now");
 
-			if (options.compression_level_specified)
-				settings.level = options.compression_level;
+				if (options.compression_level_specified)
+					settings.level = options.compression_level;
 
-			try_algorithm(options, allocator, *clip, settings, AlgorithmType8::UniformlySampled, logging, runs_writer, regression_error_threshold);
-		}
-		else if (options.exhaustive_compression)
-		{
-			const bool use_segmenting_options[] = { false, true };
-			for (size_t segmenting_option_index = 0; segmenting_option_index < get_array_size(use_segmenting_options); ++segmenting_option_index)
+				try_algorithm(options, allocator, *clip, settings, AlgorithmType8::UniformlySampled, logging, runs_writer, regression_error_threshold);
+			}
+			else if (options.exhaustive_compression)
 			{
-				const bool use_segmenting = use_segmenting_options[segmenting_option_index];
-
-				CompressionSettings uniform_tests[] =
+				const bool use_segmenting_options[] = { false, true };
+				for (size_t segmenting_option_index = 0; segmenting_option_index < get_array_size(use_segmenting_options); ++segmenting_option_index)
 				{
-					make_settings(RotationFormat8::Quat_128, VectorFormat8::Vector3_96, VectorFormat8::Vector3_96, RangeReductionFlags8::None, use_segmenting),
-					make_settings(RotationFormat8::Quat_128, VectorFormat8::Vector3_96, VectorFormat8::Vector3_96, RangeReductionFlags8::Rotations, use_segmenting),
-					make_settings(RotationFormat8::Quat_128, VectorFormat8::Vector3_96, VectorFormat8::Vector3_96, RangeReductionFlags8::Translations, use_segmenting),
-					make_settings(RotationFormat8::Quat_128, VectorFormat8::Vector3_96, VectorFormat8::Vector3_96, RangeReductionFlags8::Rotations | RangeReductionFlags8::Translations, use_segmenting),
+					const bool use_segmenting = use_segmenting_options[segmenting_option_index];
 
-					make_settings(RotationFormat8::QuatDropW_96, VectorFormat8::Vector3_96, VectorFormat8::Vector3_96, RangeReductionFlags8::None, use_segmenting),
-					make_settings(RotationFormat8::QuatDropW_96, VectorFormat8::Vector3_96, VectorFormat8::Vector3_96, RangeReductionFlags8::Rotations, use_segmenting),
-					make_settings(RotationFormat8::QuatDropW_96, VectorFormat8::Vector3_96, VectorFormat8::Vector3_96, RangeReductionFlags8::Translations, use_segmenting),
-					make_settings(RotationFormat8::QuatDropW_96, VectorFormat8::Vector3_96, VectorFormat8::Vector3_96, RangeReductionFlags8::Rotations | RangeReductionFlags8::Translations, use_segmenting),
+					CompressionSettings uniform_tests[] =
+					{
+						make_settings(RotationFormat8::Quat_128, VectorFormat8::Vector3_96, VectorFormat8::Vector3_96, RangeReductionFlags8::None, use_segmenting),
+						make_settings(RotationFormat8::Quat_128, VectorFormat8::Vector3_96, VectorFormat8::Vector3_96, RangeReductionFlags8::Rotations, use_segmenting),
+						make_settings(RotationFormat8::Quat_128, VectorFormat8::Vector3_96, VectorFormat8::Vector3_96, RangeReductionFlags8::Translations, use_segmenting),
+						make_settings(RotationFormat8::Quat_128, VectorFormat8::Vector3_96, VectorFormat8::Vector3_96, RangeReductionFlags8::Rotations | RangeReductionFlags8::Translations, use_segmenting),
 
-					make_settings(RotationFormat8::QuatDropW_Variable, VectorFormat8::Vector3_Variable, VectorFormat8::Vector3_96, RangeReductionFlags8::Translations, use_segmenting),
-					make_settings(RotationFormat8::QuatDropW_Variable, VectorFormat8::Vector3_Variable, VectorFormat8::Vector3_96, RangeReductionFlags8::Rotations | RangeReductionFlags8::Translations, use_segmenting),
+						make_settings(RotationFormat8::QuatDropW_96, VectorFormat8::Vector3_96, VectorFormat8::Vector3_96, RangeReductionFlags8::None, use_segmenting),
+						make_settings(RotationFormat8::QuatDropW_96, VectorFormat8::Vector3_96, VectorFormat8::Vector3_96, RangeReductionFlags8::Rotations, use_segmenting),
+						make_settings(RotationFormat8::QuatDropW_96, VectorFormat8::Vector3_96, VectorFormat8::Vector3_96, RangeReductionFlags8::Translations, use_segmenting),
+						make_settings(RotationFormat8::QuatDropW_96, VectorFormat8::Vector3_96, VectorFormat8::Vector3_96, RangeReductionFlags8::Rotations | RangeReductionFlags8::Translations, use_segmenting),
 
-					make_settings(RotationFormat8::QuatDropW_Variable, VectorFormat8::Vector3_Variable, VectorFormat8::Vector3_Variable, RangeReductionFlags8::AllTracks, use_segmenting),
-				};
+						make_settings(RotationFormat8::QuatDropW_Variable, VectorFormat8::Vector3_Variable, VectorFormat8::Vector3_96, RangeReductionFlags8::Translations, use_segmenting),
+						make_settings(RotationFormat8::QuatDropW_Variable, VectorFormat8::Vector3_Variable, VectorFormat8::Vector3_96, RangeReductionFlags8::Rotations | RangeReductionFlags8::Translations, use_segmenting),
 
-				for (CompressionSettings test_settings : uniform_tests)
-				{
-					test_settings.error_metric = settings.error_metric;
+						make_settings(RotationFormat8::QuatDropW_Variable, VectorFormat8::Vector3_Variable, VectorFormat8::Vector3_Variable, RangeReductionFlags8::AllTracks, use_segmenting),
+					};
 
-					try_algorithm(options, allocator, *clip, test_settings, AlgorithmType8::UniformlySampled, logging, runs_writer, regression_error_threshold);
+					for (CompressionSettings test_settings : uniform_tests)
+					{
+						test_settings.error_metric = settings.error_metric;
+
+						try_algorithm(options, allocator, *clip, test_settings, AlgorithmType8::UniformlySampled, logging, runs_writer, regression_error_threshold);
+					}
 				}
-			}
 
-			{
-				CompressionSettings uniform_tests[] =
 				{
-					make_settings(RotationFormat8::Quat_128, VectorFormat8::Vector3_96, VectorFormat8::Vector3_96, RangeReductionFlags8::Rotations, true, RangeReductionFlags8::Rotations),
-					make_settings(RotationFormat8::Quat_128, VectorFormat8::Vector3_96, VectorFormat8::Vector3_96, RangeReductionFlags8::Translations, true, RangeReductionFlags8::Translations),
-					make_settings(RotationFormat8::Quat_128, VectorFormat8::Vector3_96, VectorFormat8::Vector3_96, RangeReductionFlags8::Rotations | RangeReductionFlags8::Translations, true, RangeReductionFlags8::Rotations | RangeReductionFlags8::Translations),
+					CompressionSettings uniform_tests[] =
+					{
+						make_settings(RotationFormat8::Quat_128, VectorFormat8::Vector3_96, VectorFormat8::Vector3_96, RangeReductionFlags8::Rotations, true, RangeReductionFlags8::Rotations),
+						make_settings(RotationFormat8::Quat_128, VectorFormat8::Vector3_96, VectorFormat8::Vector3_96, RangeReductionFlags8::Translations, true, RangeReductionFlags8::Translations),
+						make_settings(RotationFormat8::Quat_128, VectorFormat8::Vector3_96, VectorFormat8::Vector3_96, RangeReductionFlags8::Rotations | RangeReductionFlags8::Translations, true, RangeReductionFlags8::Rotations | RangeReductionFlags8::Translations),
 
-					make_settings(RotationFormat8::QuatDropW_96, VectorFormat8::Vector3_96, VectorFormat8::Vector3_96, RangeReductionFlags8::Rotations, true, RangeReductionFlags8::Rotations),
-					make_settings(RotationFormat8::QuatDropW_96, VectorFormat8::Vector3_96, VectorFormat8::Vector3_96, RangeReductionFlags8::Translations, true, RangeReductionFlags8::Translations),
-					make_settings(RotationFormat8::QuatDropW_96, VectorFormat8::Vector3_96, VectorFormat8::Vector3_96, RangeReductionFlags8::Rotations | RangeReductionFlags8::Translations, true, RangeReductionFlags8::Rotations | RangeReductionFlags8::Translations),
-					make_settings(RotationFormat8::QuatDropW_Variable, VectorFormat8::Vector3_Variable, VectorFormat8::Vector3_96, RangeReductionFlags8::Translations, true, RangeReductionFlags8::Translations),
-					make_settings(RotationFormat8::QuatDropW_Variable, VectorFormat8::Vector3_Variable, VectorFormat8::Vector3_96, RangeReductionFlags8::Rotations | RangeReductionFlags8::Translations, true, RangeReductionFlags8::Rotations | RangeReductionFlags8::Translations),
+						make_settings(RotationFormat8::QuatDropW_96, VectorFormat8::Vector3_96, VectorFormat8::Vector3_96, RangeReductionFlags8::Rotations, true, RangeReductionFlags8::Rotations),
+						make_settings(RotationFormat8::QuatDropW_96, VectorFormat8::Vector3_96, VectorFormat8::Vector3_96, RangeReductionFlags8::Translations, true, RangeReductionFlags8::Translations),
+						make_settings(RotationFormat8::QuatDropW_96, VectorFormat8::Vector3_96, VectorFormat8::Vector3_96, RangeReductionFlags8::Rotations | RangeReductionFlags8::Translations, true, RangeReductionFlags8::Rotations | RangeReductionFlags8::Translations),
+						make_settings(RotationFormat8::QuatDropW_Variable, VectorFormat8::Vector3_Variable, VectorFormat8::Vector3_96, RangeReductionFlags8::Translations, true, RangeReductionFlags8::Translations),
+						make_settings(RotationFormat8::QuatDropW_Variable, VectorFormat8::Vector3_Variable, VectorFormat8::Vector3_96, RangeReductionFlags8::Rotations | RangeReductionFlags8::Translations, true, RangeReductionFlags8::Rotations | RangeReductionFlags8::Translations),
 
-					make_settings(RotationFormat8::QuatDropW_Variable, VectorFormat8::Vector3_Variable, VectorFormat8::Vector3_Variable, RangeReductionFlags8::AllTracks, true, RangeReductionFlags8::AllTracks),
-				};
+						make_settings(RotationFormat8::QuatDropW_Variable, VectorFormat8::Vector3_Variable, VectorFormat8::Vector3_Variable, RangeReductionFlags8::AllTracks, true, RangeReductionFlags8::AllTracks),
+					};
 
-				for (CompressionSettings test_settings : uniform_tests)
-				{
-					test_settings.error_metric = settings.error_metric;
+					for (CompressionSettings test_settings : uniform_tests)
+					{
+						test_settings.error_metric = settings.error_metric;
 
-					if (options.compression_level_specified)
-						test_settings.level = options.compression_level;
+						if (options.compression_level_specified)
+							test_settings.level = options.compression_level;
 
-					try_algorithm(options, allocator, *clip, test_settings, AlgorithmType8::UniformlySampled, logging, runs_writer, regression_error_threshold);
+						try_algorithm(options, allocator, *clip, test_settings, AlgorithmType8::UniformlySampled, logging, runs_writer, regression_error_threshold);
+					}
 				}
 			}
-		}
-		else
-		{
-			CompressionSettings default_settings = get_default_compression_settings();
-			default_settings.error_metric = settings.error_metric;
+			else
+			{
+				CompressionSettings default_settings = get_default_compression_settings();
+				default_settings.error_metric = settings.error_metric;
 
-			if (options.compression_level_specified)
-				default_settings.level = options.compression_level;
+				if (options.compression_level_specified)
+					default_settings.level = options.compression_level;
 
-			try_algorithm(options, allocator, *clip, default_settings, AlgorithmType8::UniformlySampled, logging, runs_writer, regression_error_threshold);
+				try_algorithm(options, allocator, *clip, default_settings, AlgorithmType8::UniformlySampled, logging, runs_writer, regression_error_threshold);
+			}
+		}
+		else if (sjson_type == sjson_file_type::raw_track_list)
+		{
+			try_algorithm(options, allocator, sjson_track_list.track_list, logging, runs_writer, regression_error_threshold);
 		}
 	};
 
diff --git a/tools/acl_decompressor/main_android/CMakeLists.txt b/tools/acl_decompressor/main_android/CMakeLists.txt
--- a/tools/acl_decompressor/main_android/CMakeLists.txt
+++ b/tools/acl_decompressor/main_android/CMakeLists.txt
@@ -17,6 +17,7 @@ set(CMAKE_ANDROID_JAVA_SOURCE_DIR "${PROJECT_SOURCE_DIR}/java")
 include_directories("${PROJECT_SOURCE_DIR}/../../../includes")
 include_directories("${PROJECT_SOURCE_DIR}/../../acl_compressor/includes")
 include_directories("${PROJECT_SOURCE_DIR}/../../acl_compressor/sources")
+include_directories("${PROJECT_SOURCE_DIR}/../../../external/rtm/includes")
 include_directories("${PROJECT_SOURCE_DIR}/../../../external/sjson-cpp/includes")
 
 # Setup resources
@@ -67,6 +68,7 @@ add_definitions(-DACL_NO_ALLOCATOR_TRACKING)
 # Disable SIMD if not needed
 if(NOT USE_SIMD_INSTRUCTIONS)
 	add_definitions(-DACL_NO_INTRINSICS)
+	add_definitions(-DRTM_NO_INTRINSICS)
 endif()
 
 target_include_directories(${PROJECT_NAME} PUBLIC jni)
diff --git a/tools/acl_decompressor/main_generic/CMakeLists.txt b/tools/acl_decompressor/main_generic/CMakeLists.txt
--- a/tools/acl_decompressor/main_generic/CMakeLists.txt
+++ b/tools/acl_decompressor/main_generic/CMakeLists.txt
@@ -6,6 +6,7 @@ set(CMAKE_CXX_STANDARD 11)
 include_directories("${PROJECT_SOURCE_DIR}/../../../includes")
 include_directories("${PROJECT_SOURCE_DIR}/../../acl_compressor/includes")
 include_directories("${PROJECT_SOURCE_DIR}/../../acl_compressor/sources")
+include_directories("${PROJECT_SOURCE_DIR}/../../../external/rtm/includes")
 include_directories("${PROJECT_SOURCE_DIR}/../../../external/sjson-cpp/includes")
 
 # Grab all of our common source files
diff --git a/tools/acl_decompressor/main_ios/CMakeLists.txt b/tools/acl_decompressor/main_ios/CMakeLists.txt
--- a/tools/acl_decompressor/main_ios/CMakeLists.txt
+++ b/tools/acl_decompressor/main_ios/CMakeLists.txt
@@ -18,6 +18,7 @@ set(MACOSX_BUNDLE_BUNDLE_NAME "acl-decompressor")
 include_directories("${PROJECT_SOURCE_DIR}/../../../includes")
 include_directories("${PROJECT_SOURCE_DIR}/../../acl_compressor/includes")
 include_directories("${PROJECT_SOURCE_DIR}/../../acl_compressor/sources")
+include_directories("${PROJECT_SOURCE_DIR}/../../../external/rtm/includes")
 include_directories("${PROJECT_SOURCE_DIR}/../../../external/sjson-cpp/includes")
 
 # Setup resources
@@ -56,6 +57,7 @@ add_definitions(-DACL_NO_ALLOCATOR_TRACKING)
 # Disable SIMD if not needed
 if(NOT USE_SIMD_INSTRUCTIONS)
 	add_definitions(-DACL_NO_INTRINSICS)
+	add_definitions(-DRTM_NO_INTRINSICS)
 endif()
 
 target_link_libraries(${PROJECT_NAME} "-framework CoreFoundation")
diff --git a/tools/format_reference.acl.sjson b/tools/format_reference.acl.sjson
--- a/tools/format_reference.acl.sjson
+++ b/tools/format_reference.acl.sjson
@@ -4,12 +4,18 @@
 // Compressed clip filenames should have the form: *.acl.zip
 // Note: a compressed clip is just a zipped raw clip.
 
-// Each clip file contains the information of a single clip.
+// Each file contains the information of a single clip or track list (but not both).
 
 // The ACL file format version
-version = 123
+// version = 1		// Initial version
+// version = 2		// Introduced clip compression settings
+//version = 3		// Introduced additive clip related data
+version = 4			// Introduced track list related data
 
-// Clip general information
+// BEGIN CLIP RELATED DATA
+
+// Clip general information.
+// Optional, if present clip settings, bones, and tracks must be present as well.
 // Must come first, before settings, bones, and tracks
 clip =
 {
@@ -48,7 +54,7 @@ clip =
 	additive_base_sample_rate = 30
 }
 
-// Optional compression settings.
+// Optional clip compression settings.
 // Must come before bones
 settings =
 {
@@ -107,7 +113,7 @@ settings =
 	error_threshold = 0.01
 }
 
-// Reference skeleton, list of bones (any order)
+// Reference clip skeleton, list of bones (any order)
 // Must come before tracks
 bones =
 [
@@ -138,7 +144,7 @@ bones =
 	}
 ]
 
-// Animation data, list of tracks (any order)
+// Animation data, list of clip tracks (any order)
 tracks =
 [
 	{
@@ -175,3 +181,74 @@ tracks =
 		]
 	}
 ]
+
+// END CLIP RELATED DATA
+// BEGIN TRACK LIST RELATED DATA
+
+// List general information.
+// Optional, if present clip settings and tracks must be present as well.
+// Must come first, before settings and tracks
+track_list =
+{
+	// Track list properties can come in any order
+
+	// List name, handy for debugging. Optional, filename will be used if missing.
+	name = "A clip"
+
+	// Number of samples per track. All tracks must have the same number of samples.
+	// List duration in seconds = (num_samples - 1) / sample_rate
+	// Regardless of sample_rate, if we have a single sample, we have a 0.0 duration
+	// and thus represent a static snapshot.
+	num_samples = 73
+
+	// List sample rate in samples per second. All tracks must have the same sample rate.
+	sample_rate = 30
+
+	// Whether floating point values are stored in hexadecimal and thus binary exact. Optional, defaults to 'false'.
+	is_binary_exact = true
+}
+
+// Optional list compression settings.
+// Must come before tracks
+settings =
+{
+}
+
+// List of raw tracks
+tracks =
+[
+	{
+		// Track properties can come in any order
+
+		// Track name, optional
+		name = "Right Hand IK"
+
+		// Track type (float1f, float2f, float3f, float4f)
+		type = float4f
+
+		// The precision target used when optimizing the bit rate
+		// Defaults to '0.0001' units
+		precision = 0.0001
+
+		// The constant track detection threshold
+		// Defaults to '0.00001' units
+		constant_threshold = 0.00001
+
+		// The track output index. When writing out the compressed data stream, this index
+		// will be used instead of the track index. This allows custom reordering for things
+		// like LOD sorting or skeleton remapping. A value of 'k_invalid_track_index' (0xFFFFFFFF) will strip the track
+		// from the compressed data stream. Output indices must be unique and contiguous.
+		// Defaults to the track index
+		output_index = 12
+
+		// Track data
+		data =
+		[
+			[ 0.0, 0.0, 0.0, 1.0 ]
+			[ 1.0, 0.0, 0.0, 0.0 ]
+			// The number of samples here must match num_samples
+		]
+	}
+]
+
+// END TRACK LIST RELATED DATA
diff --git a/tools/regression_tester_android/CMakeLists.txt b/tools/regression_tester_android/CMakeLists.txt
--- a/tools/regression_tester_android/CMakeLists.txt
+++ b/tools/regression_tester_android/CMakeLists.txt
@@ -17,6 +17,7 @@ set(CMAKE_ANDROID_JAVA_SOURCE_DIR "${PROJECT_SOURCE_DIR}/java")
 include_directories("${PROJECT_SOURCE_DIR}/../../includes")
 include_directories("${PROJECT_SOURCE_DIR}/../acl_compressor/includes")
 include_directories("${PROJECT_SOURCE_DIR}/../acl_compressor/sources")
+include_directories("${PROJECT_SOURCE_DIR}/../../external/rtm/includes")
 include_directories("${PROJECT_SOURCE_DIR}/../../external/sjson-cpp/includes")
 
 # Setup resources
@@ -63,11 +64,13 @@ target_compile_options(${PROJECT_NAME} PRIVATE -g)
 
 # Throw on failure to allow us to catch them and recover
 add_definitions(-DACL_ON_ASSERT_THROW)
+add_definitions(-DRTM_ON_ASSERT_THROW)
 add_definitions(-DSJSON_CPP_ON_ASSERT_THROW)
 
 # Disable SIMD if not needed
 if(NOT USE_SIMD_INSTRUCTIONS)
 	add_definitions(-DACL_NO_INTRINSICS)
+	add_definitions(-DRTM_NO_INTRINSICS)
 endif()
 
 # Disable allocation tracking since if we fail a regression test, we'll throw an exception
diff --git a/tools/regression_tester_ios/CMakeLists.txt b/tools/regression_tester_ios/CMakeLists.txt
--- a/tools/regression_tester_ios/CMakeLists.txt
+++ b/tools/regression_tester_ios/CMakeLists.txt
@@ -18,6 +18,7 @@ set(MACOSX_BUNDLE_BUNDLE_NAME "acl-regression-tester")
 include_directories("${PROJECT_SOURCE_DIR}/../../includes")
 include_directories("${PROJECT_SOURCE_DIR}/../acl_compressor/includes")
 include_directories("${PROJECT_SOURCE_DIR}/../acl_compressor/sources")
+include_directories("${PROJECT_SOURCE_DIR}/../../external/rtm/includes")
 include_directories("${PROJECT_SOURCE_DIR}/../../external/sjson-cpp/includes")
 
 # Setup resources
@@ -51,6 +52,7 @@ add_executable(${PROJECT_NAME} MACOSX_BUNDLE ${ALL_MAIN_SOURCE_FILES} ${ALL_COMM
 
 # Throw on failure to allow us to catch them and recover
 add_definitions(-DACL_ON_ASSERT_THROW)
+add_definitions(-DRTM_ON_ASSERT_THROW)
 add_definitions(-DSJSON_CPP_ON_ASSERT_THROW)
 
 # Disable allocation track since if we fail a regression test, we'll throw an exception
@@ -60,6 +62,7 @@ add_definitions(-DACL_NO_ALLOCATOR_TRACKING)
 # Disable SIMD if not needed
 if(NOT USE_SIMD_INSTRUCTIONS)
 	add_definitions(-DACL_NO_INTRINSICS)
+	add_definitions(-DRTM_NO_INTRINSICS)
 endif()
 
 target_link_libraries(${PROJECT_NAME} "-framework CoreFoundation")
diff --git a/tools/vs_visualizers/acl.natvis b/tools/vs_visualizers/acl.natvis
--- a/tools/vs_visualizers/acl.natvis
+++ b/tools/vs_visualizers/acl.natvis
@@ -47,4 +47,51 @@
     <DisplayString>size={m_size}, num_bits={m_size * 32}</DisplayString>
   </Type>
 
+  <Type Name="acl::track">
+    <DisplayString>num_samples={m_num_samples}, type={m_type}</DisplayString>
+    <Expand>
+      <Item Name="num_samples">m_num_samples</Item>
+      <Item Name="stride">m_stride</Item>
+      <Item Name="type">m_type</Item>
+      <Item Name="precision" Condition="m_type == acl::track_type8::float1f">m_desc.scalar.precision</Item>
+      <Item Name="precision" Condition="m_type == acl::track_type8::float2f">m_desc.scalar.precision</Item>
+      <Item Name="precision" Condition="m_type == acl::track_type8::float3f">m_desc.scalar.precision</Item>
+      <Item Name="precision" Condition="m_type == acl::track_type8::float4f">m_desc.scalar.precision</Item>
+      <Item Name="precision" Condition="m_type == acl::track_type8::vector4f">m_desc.scalar.precision</Item>
+      <Item Name="is_ref">m_allocator == nullptr</Item>
+      <ArrayItems>
+        <Size>m_num_samples</Size>
+        <ValuePointer Condition="m_type == acl::track_type8::float1f">(float*)m_data</ValuePointer>
+        <ValuePointer Condition="m_type == acl::track_type8::float2f">(rtm::float2f*)m_data</ValuePointer>
+        <ValuePointer Condition="m_type == acl::track_type8::float3f">(rtm::float3f*)m_data</ValuePointer>
+        <ValuePointer Condition="m_type == acl::track_type8::float4f">(rtm::float4f*)m_data</ValuePointer>
+        <ValuePointer Condition="m_type == acl::track_type8::vector4f">(rtm::vector4f*)m_data</ValuePointer>
+      </ArrayItems>
+    </Expand>
+  </Type>
+
+  <Type Name="acl::track_array">
+    <DisplayString>num_tracks={m_num_tracks}</DisplayString>
+    <Expand>
+      <Item Name="num_tracks">m_num_tracks</Item>
+      <ArrayItems>
+        <Size>m_num_tracks</Size>
+        <ValuePointer>m_tracks</ValuePointer>
+      </ArrayItems>
+    </Expand>
+  </Type>
+
+  <Type Name="acl::acl_impl::scalarf_range">
+    <DisplayString>min={m_min}, extent={m_extent}</DisplayString>
+  </Type>
+
+  <Type Name="acl::acl_impl::track_range">
+    <DisplayString Condition="category == acl::track_category8::scalarf">{range.scalarf}</DisplayString>
+    <Expand>
+      <Item Name="category">category</Item>
+      <Item Name="min" Condition="category == acl::track_category8::scalarf">range.scalarf.m_min</Item>
+      <Item Name="extent" Condition="category == acl::track_category8::scalarf">range.scalarf.m_extent</Item>
+    </Expand>
+  </Type>
+
 </AutoVisualizer>


Test patch:
diff --git a/test_data/README.md b/test_data/README.md
--- a/test_data/README.md
+++ b/test_data/README.md
@@ -7,7 +7,8 @@ This directory is to contain all the relevant data to perform regression testing
 Find the latest test data zip file located on Google Drive and save it into this directory. If your data ever becomes stale, the python script will indicate as such and you will need to perform this step again. The zip file contains a number of clips from the [Carnegie-Mellon University](../docs/cmu_performance.md) database that were hand selected. A readme file within the zip file details this.
 
 *  **v1** Test data [link](https://drive.google.com/open?id=1psNO0riJ6RlD5_vsvPh2vPsgEBvLN3Hr)
-*  **v2** Test data [link](https://drive.google.com/open?id=192CWjNRwlskgdqakNI-k8dv-EefXwxtU) (**Latest**)
+*  **v2** Test data [link](https://drive.google.com/open?id=192CWjNRwlskgdqakNI-k8dv-EefXwxtU)
+*  **v3** Test data [link](https://drive.google.com/file/d/1ZxQp1-q_stN2MIgyQm6v6FP2zg6GmNPk/view?usp=sharing) (**Latest**)
 
 ## Running the tests