This is a header-only c++20 library for storing audio data in memory.
If you write audio code you have probably written a data structure that looks something like this at some point:
template <size_t channel_count>
struct audio_data {
std::array<std::vector<float>, channel_count> frames;
};If you are like me then you have probably written multiple variations of this. There are four obvious variations depending on which dimensions of the storage are known at compile time:
std::array<std::vector<float>, channel_count>
- Channel count known at compile time, but dynamic number of frames (as above.)
std::vector<std::array<float, frame_count>>
- Frame count known at compile time, but dynamic number of channels (rare.)
std::array<std::array<float, frame_count>, channel_count>
- Both channel count and frame count known at compile time.
std::vector<std::vector<float>>
- Dynamic number of channels and frames.
This library consolidates all these variations into one consistent interface, and provides utilities for reading and writing the data, iterating over multi-channel data frame-by-frame, and interleaving operations.
- c++20 or above
- This library make use of
aligned_allocatorandsmall_vectorfrom Boost so you just need to make sure these are available in your include paths:
#include <boost/align/aligned_allocator.hpp>
#include <boost/container/small_vector.hpp>If using CMake then this will happen automatically as long as find_package(Boost REQUIRED COMPONENTS headers CONFIG) succeeds.
After cloning you can use the library in your CMake project like this:
add_subdirectory("/path/to/ads")
target_link_libraries(your-project ads::ads)Or if not using CMake, simply copy+paste the headers into your project.
template <typename ValueType, uint64_t channel_count, uint64_t frame_count> ads::data
- Main audio storage type. The underlying storage type depends on the template arguments.
ads::DYNAMIC_EXTENTcan be used for eitherchannel_countorframe_count, or both.ads::DYNAMIC_EXTENTmeans the count can be specified at runtime (and theresize()function will be available for that dimension.)
template <typename ValueType, uint64_t frame_count> ads::mono
- 1 channel of a compile-time-known number of frames (unless
DYNAMIC_EXTENTis specified.) - An alias for
ads::data<ValueType, 1, frame_count>.
template <typename ValueType, uint64_t frame_count> ads::stereo
- 2 channels of a compile-time-known number of frames (unless
DYNAMIC_EXTENTis specified.) - An alias for
ads::data<ValueType, 2, frame_count>.
template <typename ValueType> ads::dynamic_mono
- 1 channel of a dynamic number of frames.
- An alias for
ads::data<ValueType, 1, ads::DYNAMIC_EXTENT>.
template <typename ValueType> ads::dynamic_stereo
- 2 channels of a dynamic number of frames.
- An alias for
ads::data<ValueType, 2, ads::DYNAMIC_EXTENT>.
template <typename ValueType> ads::fully_dynamic
- A dynamic number of channels and frames.
- An alias for
ads::data<ValueType, ads::DYNAMIC_EXTENT, ads::DYNAMIC_EXTENT>.
template <typename ValueType> ads::interleaved
- A wrapper around
ads::dynamic_monointended to be used for interleaved audio channel data. - The channel count and frame count are specified at runtime and the total number of required underlying frames is calculated for you.
// Mono data
// Frame count known at runtime
// type == ads::dynamic_mono<float> / ads::data<float, 1, ads::DYNAMIC_EXTENT>
auto mono_data0 = ads::make_mono<float>(ads::frame_count{10000});
auto mono_data1 = ads::make<float, 1>(ads::frame_count{10000}); // equivalent
// Stereo data
// Frame count known at runtime
// type == ads::dynamic_stereo<float> / ads::data<float, 2, ads::DYNAMIC_EXTENT>
auto stereo_data0 = ads::make_stereo<float>(ads::frame_count{10000});
auto stereo_data1 = ads::make<float, 2>(ads::frame_count{10000}); // equivalent
// Arbitrary number of channels known at compile time
// Frame count known at runtime
// type == ads::data<float, 10, ads::DYNAMIC_EXTENT>
auto data0 = ads::make<float, 10>(ads::frame_count{10000});
// Frame count known at compile time
// Channel count known at runtime
// type == ads::data<float, ads::DYNAMIC_EXTENT, 10>
auto data1 = ads::make<float, 10>(ads::channel_count{2});
// Channel count and frame count both known at compile time
// type == ads::data<float, 2, 64>
auto data2 = ads::make<float, 2, 64>();
// Channel count and frame count both known at runtime
// type == ads::fully_dynamic<float> / ads::data<float, DYNAMIC_EXTENT, DYNAMIC_EXTENT>
auto data3 = ads::make<float>(ads::channel_count{2}, ads::frame_count{10000});
// 10,000 frames of interleaved stereo data (therefore the underlying buffer is a single channel of 20,000 frames.)
auto interleaved = ads::interleaved<float>{ads::channel_count{2}, ads::frame_count{10000}};
// Convert from interleaved to multi-channel
ads::deinterleave(interleaved, data3.begin());
// Convert from multi-channel to interleaved
ads::interleave(data3, interleaved.begin());
// You can also just use any old range of floats for interleaved data
auto buffer = std::vector<float>(20000, 0.0f);
ads::interleave(data3, buffer.begin());
ads::deinterleave(buffer, data3.begin());Although their types are different, the same interface (more or less) is provided for the data0, data1, data2 and data3 objects created above. There are some extra things enabled if the storage is known at compile-time to be mono-channel.
get_channel_count()get_frame_count()begin()/end(): returns specialized iterators for iterating frame-by-frame (even though channels are stored in separate buffers)resize(): resize the storage (channel count or frame count, or both)set(): set individual frame valuesat(): return individual frames (by reference), or underlying channel bufferswrite(): for writing audio data to the storageread(): for reading audio data from the storagedata(): access the rawfloat*buffers
The read() and write() functions are based around the idea of reading and writing chunks of the underlying storage buffers, since this is usually what you want to do in audio code, rather than iterating frame-by-frame.
You can pass in either a single-channel or multi-channel read/write function.
A "single-channel" read function has the form:
(const float* buffer, ads::frame_idx start, ads::frame_count frame_count) -> ads::frame_count
A "multi-channel" read function has the form:
(const float* buffer, ads::channel_idx ch, ads::frame_idx start, ads::frame_count frame_count) -> ads::frame_count
A "single-channel" write function has the form:
(float* buffer, ads::frame_idx start, ads::frame_count frame_count) -> ads::frame_count
A "multi-channel" write function has the form:
(float* buffer, ads::channel_idx ch, ads::frame_idx start, ads::frame_count frame_count) -> ads::frame_count
bufferis always pre-offset into the part of the underlying buffer that you are reading from or writing to.chis the index of the channel that you are reading from or writing to.startis the index of the first frame of the chunk of frames you are reading from or writing to. This is often not needed but is useful in some situations.frame_countis the maximum number of frames you should try to read or write. This value will never overflow the end of the underlying buffer.- The actual number of frames read or written should be returned from the function.
This will write ones to both channels:
auto data = ads::make(ads::channel_count{2}, ads::frame_count{10000});
data.write([](float* buffer, ads::frame_idx start, ads::frame_count frame_count){
std::fill(buffer, buffer + frame_count.value, 1.0f);
return frame_count;
});This will write ones to only the second channel:
auto data = ads::make<float>(ads::channel_count{2}, ads::frame_count{10000});
data.write(ads::channel_idx{1}, [](float* buffer, ads::frame_idx start, ads::frame_count frame_count){
std::fill(buffer, buffer + frame_count.value, 1.0f);
return frame_count;
});This will write zeros to the first channel, and ones to the second channel:
auto data = ads::make<float>(ads::channel_count{2}, ads::frame_count{10000});
data.write([](float* buffer, ads::channel_idx ch, ads::frame_idx start, ads::frame_count frame_count){
if (ch.value == 0) { std::fill(buffer, buffer + frame_count.value, 0.0f); }
else { std::fill(buffer, buffer + frame_count.value, 1.0f); }
return frame_count;
});This does the same thing:
auto data = ads::make<float>(ads::channel_count{2}, ads::frame_count{10000});
data.write(ads::channel_idx{0}, [](float* buffer, ads::frame_idx start, ads::frame_count frame_count){
std::fill(buffer, buffer + frame_count.value, 0.0f);
return frame_count;
});
data.write(ads::channel_idx{1}, [](float* buffer, ads::frame_idx start, ads::frame_count frame_count){
std::fill(buffer, buffer + frame_count.value, 1.0f);
return frame_count;
});This will write ones to the first 100 frames:
auto data = ads::make<float>(ads::channel_count{2}, ads::frame_count{10000});
data.write(ads::frame_count{100}, [](float* buffer, ads::frame_idx start, ads::frame_count frame_count){
std::fill(buffer, buffer + frame_count.value, 1.0f);
return frame_count;
});This will write ones to frames 50-99:
auto data = ads::make<float>(ads::channel_count{2}, ads::frame_count{10000});
data.write(ads::frame_idx{50}, ads::frame_count{50}, [](float* buffer, ads::frame_idx start, ads::frame_count frame_count){
std::fill(buffer, buffer + frame_count.value, 1.0f);
return frame_count;
});If you happen to use Madronalib in your project there is an extra header with some utilities for interacting with ml::DSPVector, ml::DSPVectorArray, and ml::DSPVectorDynamic:
#include <ads-ml.hpp>