In my last blog, I described you I added M4A support to my Arduino Audio Tools project and how to use it.
As highlited we need to access and store the stsz size tabe to play back the audio: a typical size table contains aournd 40 – 80 kbytes, but for long audio this can get much bigger: This is not an amount of data, that you would like to have to store on a Microcontroller.
In this document I present a couple of solution approaches that avoid the need to store this amount of data in RAM
Avoiding the Container Class
My existing container classes use a streaming-based approach: you feed data in small chunks, which makes the processing logic independent of the data source type. To support M4A playback more efficiently, I introduced a new class:
M4AAudioFileDemuxer
This class acts as a demuxer for M4A/MP4 files, designed to extract audio data directly from a file without buffering the sample size table in RAM.
-
It locates the
mdat
andstsz
boxes using theMP4Parser
. -
You assign a
MultiDecoder
in the constructor. -
You start processing by calling
begin(File file)
. -
It offers a
copy()
method to extract frames by reading the sample sizes directly from thestsz
box in the file.
Because the sample sizes are read on-the-fly from the file, this solution is very memory-efficient. Data is passed directly to the decoder or an optional frame_callback
..
Buffers
Initially, I was storing the size table in a Vector of uint32_t. In order to half the memory requirements, I changed the data type to uint16_t: this is really sufficient for audio! In the next step, I replaced the Vector with a BaseBuffer, so that we can use any supported Buffer type and add it with the setSampleSizesBuffer() method.
Here are some of the most relevant buffer classes:
RingBufferFile
The RingBufferFile is a file backed ring buffer. Instead of storing the data in RAM we just store it in a separate file.
// Define global variables
MultiDecoder multi_decoder;
ContainerM4A dec_m4a(multi_decoder);
File buffer_file;
RingBufferFile<File,stsz_sample_size_t> file_buffer(0);
// in setup call
buffer_file = SD.open("/home/pschatzmann/tmp.tmp", O_RDWR | O_CREAT);
file_buffer.begin(buffer_file);
dec_m4a.setSampleSizesBuffer(file_buffer);
RedisBuffer
This implementation uses a Redis list as a circular buffer. Data is buffered locally and pushed/popped in batches to minimize network overhead.
-
Uses
RPUSH
for writing andLPOP
for reading. -
Built on the Arduino
Client
API. -
Supports automatic Redis key expiration.
-
The local batch size is configurable.
-
Works well for streaming scenarios.
.
// Define global variables
MultiDecoder multi_decoder;
ContainerM4A dec_m4a(multi_decoder);
WiFiClient client;
RedisBuffer<stsz_sample_size_t> redis(client,"m4a-buffer1",0, 1024, 0);
// in setup call
WiFi.begin("ssid","pwd");
while ( WiFi.status() != WL_CONNECTED) {
Serial.print(".");
}
if (!client.connect(IPAddress(192,168,1,10),6379)){
Serial.println("redis error");
stop();
}
dec_m4a.setSampleSizesBuffer(redis);
M4AFileSampleSizeBuffer
This class reuses the M4AAudioFileDemuxer
functionality and mimics a buffer interface. It reads sample sizes directly from an M4A file during playback without storing them in memory.
-
Works only with file-based sources (not streams).
-
Integrates with
AudioPlayer:
it registers asetOnStreamChangeCallback()
to automatically handle file changes.
// Define global variables
MultiDecoder multi_decoder;
AudioPlayer player(source, out, multi_decoder);
ContainerM4A dec_m4a(multi_decoder);
M4AFileSampleSizeBuffer sizes_buffer(player, dec_m4a);
// in setup call
dec_m4a.setSampleSizesBuffer(sizes_buffer);
Conclusion
These buffering strategies offer a lot of flexibility for optimizing your sketches to support M4A audio playback. Depending on your use case—whether you’re streaming, using files, or dealing with long audio content—you can choose the most appropriate approach to minimize RAM usage and maximize performance.
0 Comments