The Smallest WAV Player

SD2G

In the last three chapters of the Flying PIC24 book,  I developed a simple audio (WAV) file player. What most readers might not know though is  that that code was really written first for an 8-bit micro controller back in 2002, when I had my hands for the first time on a PIC18F8720  engineering sample. This was for me the first opportunity to work with a PIC microcontroller with 4K bytes of RAM. In truth I had developed the file system code almost a year earlier on a smaller PIC18 model but connecting it to a Compact Flash card and using an external (parallel) static RAM memory chip (8K bytes).

Traces of that code, that I used to access the external RAM memory “expansion”, can be found today (14 years later)  in the latest version of the MLA File System library , just look for the  ReadWord() and ReadLong()  functions.

As my confidence and understanding of the FAT file system and audio playback grew, I reduced the RAM usage from 8K down to 4K  (eventually replacing the CF cards with SD cards) before I  migrated it all to the PIC24 first and (much) later to the PIC32.  Eventually the code was optimised further to use less than 2K bytes of RAM including both file system AND audio ping/pong buffers.

On the PIC32, RAM scarcity (so to speak) became a factor again only when I tried fitting/porting the Helix MP3 (soft) decoder algorithm in the 32K RAM available on the early PIC32MX4 series. I conquered that challenge too eventually but that is another story.

The challenge

Only very recently I got the request from a reader to bring the PIC24 WAV player down to the smallest possible PIC microcontroller and this got me interested once more in closing the circle and going back to 8-bit solutions.

It turns out that Flash memory is today abundant even in 8-pin device (PIC12F18xx), SPI ports are easy to come by too, but it is RAM that is still the main discriminating factor.

In my mind there was the incredible One Chip Player project of the amazingly talented Dmitry Grinberg.  This represented the ultimate “hack”, the “absolute zero” of WAV players, but I was looking for a less extreme solution that would still maintain a level of sound quality and flexibility that would make the solution palatable to a wider audience.

A quick porting attempt of the WAV Player code (MDD File System + SD card driver + WAV decoder), using the latest XC8 compiler for the PIC16, surprised me digesting right away the source code (originally written for the XC16 compiler) with little or no complaints.

I then used the MCC (Microchip Code Configurator) to generate all the peripheral drivers that needed replacing: TMR, interrupt, and PWM initialisation, I/O configuration and device configuration. Once all the pieces were combined, in a surprisingly short amount of time, I had  a 3,400 words (approx 5K bytes) application.

This was small enough to fit in a 14-pin device (such as the PIC16F1825) if only I could keep the RAM usage down to less than 1K!

Keep in mind that the File System alone needs  a 512-byte RAM buffer to capture one “sector” of data, the smallest unit of storage, as far as a FAT file system is conceived at least. WAV file opening and decoding requires another 40 odd bytes of data to be extracted and worked on, but it is the audio buffers (of which you need two to maintain a continuous playback) that will quickly eat the rest of the available RAM space and some. It is not difficult to do the math, considering a desired 22,050 sample/s playback rate (mono) and considering a max SD card response time of 3ms (before the first byte is returned for a requested sector including a FAT look up) that at least 256 bytes (x2) is the optimal size. That would seem close to the 1K total if it was not for the fact that we still need a stack and a number of additional global and local variables to run ANY application code on top.

Looking at Dmitry’s solution, which runs in 256 bytes (!) of RAM, there was a long list of tricks that I could have “borrowed”  but each would have brought some serious limitation to the resulting application. For example, FAT (file allocation table) look ups can be removed  by pre-scanning the entire “chain of clusters” and storing (in EEPROM/Flash?) the resulting list (possibly compressed). But this implies that before starting playback, the scanning sequence must be performed (time consuming) and the fragmentation of the cluster chain must be limited, so to fit in the (EEPROM/Flash) space available. Not having even a single sector wide buffer (<512 bytes RAM), implies also that data must be consumed in the same order as it is read from the media. A limitation that practically precludes all cases where the cluster chain is not strictly “monotonic” (no jumps back or in other words never delete ANY file from the SD card). This turns out to be a much more severe limitation than one would imagine as it would force a frequent reformat of the SD card to ensure play-ability of the WAV files list contained.

Limiting further the sample rate, on the other hand, is  a compromise I would not want to consider as descending below 22KHz would mean a severe audio quality reduction. In my experience a 10KHz low pass filter (required to satisfy Nyquist limit for a 22KHz WAV file) will be barely noticeable to most listeners, whereas a 5KHz low pass  (required to satisfy Nyquist limit for a 11KHz WAV file definitely leaves a strong mark.

The solution

A less crippling solution eventually occurred to me as I realised that, if I could “align” the audio buffers with the borders of the file sectors/clusters, only one of the two audio buffers (always the same one) would be truly in use every time the file system fread() function was invoked to fill the “other” buffer (and the 512 secotr buffer would be therefore in use).  In other words only 756 bytes of RAM (sector buffer of 512 + 1x audio buffer of 256) are required if the WAV file contents are aligned during the first read. Unfortunately this is a very unlikely condition to happen on a generic WAV file (1/512 chances) as the WAV file header size is variable in length ( it can include numerous “chunks”  some of which are of unpredictable size). Yet it is possible to achieve the alignment simply by “discarding” a small number of samples at the very beginning of the audio “piece”. This results in a “loss” but as it can be demonstrated a very small one. In the worst case, a maximum of 511 samples are discarded and that, at 22K samples/s, is effectively impacting only the very first 25 ms of the “song”. In almost all cases this is either part of a quiet preamble or an otherwise un-noticeable  (to our ears) omission.

Other than the alignment problem (solved), the application requires only one additional tweak to the file system libraries, as one of the two audio buffers is effectively now overlapping the sector buffer. The fread() function must be able to simply leave the data “in place” when it is reading the contents of the first audio buffer. To this end, I modified it to accept a NULL destination pointer. Upond detecting this particular condition, the read function will perform all the housekeeping required to properly advance along the file contents except for the final “transfer”.

Interestingly this results in a further performance improvement as the otherwise unnecessary data transfer is avoided and the same trick (calling fread() with a NULL destination pointer) can be used to perform the initial buffer alignment without having to resort to  a hack (such as accessing dangerously the inner contents of the MFILE data structure).

The output stage is then performed by one of the 10-bit PWM peripherals of the PIC16F1825 device chosen. Once filtered (10KHz second order low pass) inexpensively using a single OpAmp and a pair of resistors and capacitors the result is a really smooth playback of mono WAV files (encoded using 22,050 rate) that can cover gracefully the entire range of applications from alarms and  sirens to voice messages and music reproduction with a quality and dynamic similar to a typical FM radio.

Most importantly, the player code is leaving plenty of room, both  RAM and Flash, so that it is possible to integrate it into a real application. The code is also very close to the well documented examples in the  PIC24 and PIC32  books and the bill of materials can be limited to the sole (micro) SD card connector and an audio jack…

 

This entry was posted in AV16/32, Tips and Tricks and tagged , , . Bookmark the permalink.