Contents
Introduction and Background
From the earliest wax cylinders made by Thomas Edison [1] to be played on phonographs, to the standard .mp3 file format used ubiquitously today, storing audio has always been an important aspect of the music industry. Even though we are a far cry from only having cassette tapes or vinyl records to store audio, people have always strived to be able to store more songs on smaller and smaller devices. This was no different for my Senior Design project – a remote controlled BB-8 robot which needed to be able to play “realistic astromech droid sounds” [2], with limited processing power as well as limited storage capacity. In our case, the goal was to create an audio compression technique that would:
- Compress audio files down to fit within the flash memory of the microcontroller [3]
- Not be too computationally complex to decode so as to interfere with essential operation of BB-8
- Still sound “okay”; i.e. perceptually not have any huge distortions
Armed with this task, I decided to do what any engineer would do: look for an algorithm that already does what I want it to do. Lossless audio compression methods such as FLAC were dropped almost immediately, as the compression ratios found were much too high (not compressed enough) [4], and while MP3 seemed like the go-to choice, writing an MP3 decoder would likely be more work than the rest of the project itself. Therefore, I decided to try to utilize some concepts from ECE438, such as quantization and possibly LPC to create a simple, yet efficient, compression system. A huge portion of audio compression is in the encoding complexity; however, since we will only be playing static data, we don’t have to worry about the encoding complexity of the audio data.
Step 0: Defining Inputs and Outputs
The flow of data is straightforward: from a 16-bit mono PCM .wav file sampled at 48kHz, encode it into some minimized form of data, then be able to decode this data into an array of 8-bit unsigned integers that fits into the microcontroller’s memory to be pushed to the DAC. Therefore, what’s to be stored on the microcontroller is the minimized data for various sounds, so that we can store multiple sound effects (instead of being limited to one 2-second sound effect that fills up all of the flash storage).
Step 1: Finding a minimum sampling rate
While the sounds that BB-8 makes are high pitched, none come close to the maximum 24kHz that a 48kHz sampling rate offers. Additionally, a wonderful feature of the STM32F4xx microcontroller family is that it is able to output audio at arbitrary sampling rates. Therefore, we can specify the sampling rate of a sound effect in its header and then change the DAC output rate to match. Therefore, for each of these sound samples, we can arbitrarily empirically determine a slower sampling rate that doesn't result in a significant loss in quality. I decided that the threshold frequency would be the highest frequency where the magnitude of the FFT of the audio was greater than 2% of the maximum magnitude of the FFT.
MATLAB code for finding a minimum sampling rate
%% Step 1: %% determine new sampling rate dataFFT = fftshift(fft(data,512)); dataFFT = dataFFT(257:end); % determine highest frequency where the magnitude is > 2% of the maximum % magnitude % create a vector of booleans corresponding to if the value is greater than % 2% of the maximum booleanVector = abs(dataFFT) > max(abs(dataFFT))*0.02; lastValue = find(booleanVector,1,'last'); thresholdFreq = floor((lastValue/256).*24000); % resample data to new sampling rate resampledData = resample(data, thresholdFreq, 24000);
On this sample input, we determine that the threshold frequency is 12750 Hz. So, we can simply use MATLAB's resample function to resample the original data, then prepend the encoded audio file with the sample rate. Of course, the threshold frequency varies for different files; in this case, we have a compression ratio of 53% of the original data size, since the threshold frequency 12750Hz is 53% of 24kHz.
Step 3: Conversion to DAC values
The final step in this process is to quantize the 16-bit data into 8-bits. Of course, since the DAC on our microcontroller only allows for a uniform spacing of output voltage, a max quantizer isn't applicable to this solution. Therefore, we simply uniformly quantize by the following formula: $ Y = \lfloor \frac{X-min(X)}{max(X)}\cdot 255 \rceil $, where Y is the quantized signal and X is the original signal. Below is a plot of our sample input file and its 8-bit quantized version.
Of course, this waveform doesn't look any different, as the original waveform has $ 2^{16} = 65536 $ possible values, while the quantized waveform has $ 2^8 = 256 $ possible values, both quite high. Playing the two sounds side-by-side have almost no perceptible difference in quality.