This example uses the imp003/LBWA1ZV1CD’s fixed-frequency DAC to play audio files. The audio data is first processed by the agent and sent to the device, where it is then saved to flash memory and played back locally.
If you are having problems getting the EVB online, please consult the BlinkUp Troubleshooting Guide.
A successful BlinkUp operation is indicated by a slowly flashing green LED on the EVB. The EVB will be listed in impCentral’s ‘My Development Devices’ list (accessed via the devices menu in the black bar at the top). The number presented in impCentral is the EVB’s unique Device ID. You can, if you wish, give it a more friendly name as follows:
In order to play audio from the imp, we first download the audio data to the device in chunks and save it to flash. Then, when it's time to play it back, we simply read the data back into buffers small enough to fit into the device's memory and feed those buffers to the fixed-frequency DAC.
This process is greatly simplified by the imp003's new SPI flash API, which allows users to access the same flash chip that the imp uses to store system data. A section of about 512KB is reserved for internal use, and the remaining space is made accessible through Squirrel.
The agent code processes a 16-bit, PCM-encoded WAV file into raw data before sending it to the device. There are three different ways to load the audio into the agent:
(In the latter two cases, the agent will fetch the WAV file from a remote server.)
The process begins when the agent receives an HTTP request.
// HTTP request handler
http.onrequest(function(req, res) {
// Receive WAV file directly as POST data
if (req.path == "/play") {
if (req.body.len() >= WAV_HEADER_SIZE && processWAV(req.body)) {
res.send(200, "Received valid WAV file.\n");
} else {
res.send(500, "Invalid WAV file!\n");
}
}
});
If the URL ends in /play
then the agent assumes method #1 and calls processWAV()
to validate the data.
function processWAV(wav) {
buf = blob(wav.len());
buf.writestring(wav);
buf.seek(0);
local chunkID = buf.readstring(4);
local filesize = buf.readn('i');
local format = buf.readstring(4);
server.log("Total filesize: " + filesize);
// Check required headers for a PCM WAV
// "RIFF" (0x 52 49 46 46)
// file size w/o RIFF header (4 bytes)
// "WAVE" (0x 57 41 56 45)
// "fmt " (0x 66 6d 74 d0)
if (chunkID != "RIFF" || format != "WAVE" || buf.readstring(4) != "fmt ") {
server.error("Incompatible headers");
return false;
}
[...]
// Strip headers, perform necessary conversions, and send to device
getRawAudio();
device.send("newAudio", params);
return true;
}
The data is thoroughly examined to see if it complies with the WAV header format, and size/sample rate information is saved in the params
table. If everything checks out, the agent extracts the raw audio data.
16-bit PCM WAV data is signed (and can contain values from -32768 to +32767.) The fixed-frequency DAC (or 'FFD') expects unsigned data, though, so we must convert it. We also allow stereo files to be submitted, but since there is only one speaker output the audio must be converted to mono. Finally, we don't want to play the header data through our speaker so we should remove that before playback. We perform all three of these operations using getRawAudio()
:
function getRawAudio() {
local temp = null;
local end = WAV_HEADER_SIZE + params.wavSize;
local stereo = params.numChannels == 2 ? true : false;
local w = 0;
for (local r = WAV_HEADER_SIZE; r < end; r += 2) {
buf.seek(r); // Seek to read pointer
temp = buf.readn('s') + 32768; // Read sample and convert to unsigned
// If stereo, scale each channel to 1/2 and sum them
if (stereo) {
temp = temp/2 + (buf.readn('s')+32768)/2;
r += 2;
}
buf.seek(w); // Seek to write pointer
buf.writen(temp, 'w'); // Write sample
w += 2; // Increment write pointer
}
// Shrink buffer to match size of audio data
params.wavSize = params.wavSize / params.numChannels;
buf.resize(params.wavSize);
}
After extracting the waveform data, the agent sends a message to the device indicating that it has new audio available, specifying the size and sample rate of the audio:
device.send("newAudio", params);
The device has two main tasks. First, it saves the audio data to flash. Then, after the user presses a button, it reads the audio back and plays it through the amplifier (to a speaker, headphones, etc.)
When the device receives a message from the agent indicating that new audio is available, it calls the newAudio()
function. This function first calculates the number of flash sectors required to store the audio and erases those sectors one at a time. Then, it writes a small cookie to the first 12 bytes of flash that contains the size and sample rate of the audio, to enable playback from cold boot without needing to contact the agent again. Finally, it sends a request for the agent to begin downloading chunks of the audio data.
function newAudio(newParams) {
params = newParams;
chunkIndex = 0;
// Erase the appropriate number of sectors
local numSectors = (math.ceil(newParams.wavSize / SECTOR_SIZE.tofloat())).tointeger();
for (local i = 0; i < numSectors; i++) {
flash.erasesector(i*SECTOR_SIZE);
}
writeCookie();
// Calculate the number of FFD buffers we'll use on playback
numBuffers = (math.ceil(params.wavSize / BUFFER_SIZE.tofloat())).tointeger();
agent.send("getChunk", chunkIndex);
}
The saveChunk()
function simply receives a new audio chunk from the agent and saves it to flash. Then, if there are additional chunks to download, it requests the next one. If not, it marks the audio as valid and the download is complete.
function saveChunk(chunk) {
local addr = COOKIE_LENGTH + chunkIndex * CHUNK_SIZE;
// Make sure there's enough space to fit the new chunk in flash
if (addr + chunk.len() <= flashSize) {
flash.write(addr, chunk);
chunkIndex++;
if (addr + chunk.len() < params.wavSize) {
// Get next chunk
agent.send("getChunk", chunkIndex);
} else {
// Download complete
validAudio = true;
}
} else {
server.error("Can't write chunk - out of space");
}
}
When Button 1 is pressed, the device begins playback. First, the Fixed-Frequency DAC must be configured with initial audio buffers. Depending on the size of the audio data, up to 2 buffers will be allocated and filled with data read from flash. Then, the Fixed-Frequency DAC is configured and started, and the audio amp is enabled after a brief delay to prevent popping.
function play() {
playbackPtr = COOKIE_LENGTH;
// Create the correct number of buffers
if (numBuffers == 1) {
buffers = [blob(params.wavSize)];
} else if (numBuffers == 2) {
buffers = [blob(BUFFER_SIZE), blob(params.wavSize - BUFFER_SIZE)];
} else {
buffers = [blob(BUFFER_SIZE), blob(BUFFER_SIZE)];
}
// Fill the initial buffers
for (local i = 0; i < (numBuffers==1?1:2); i++) {
buffers[i].seek(0);
writeBuffer(buffers[i]);
}
hardware.fixedfrequencydac.configure(speaker, params.sampleRate, buffers, bufferEmpty, AUDIO);
hardware.fixedfrequencydac.start();
imp.wakeup(0.1, function() { speakerEnable.write(1); });
}
When a buffer becomes empty, the Fixed-Frequency DAC calls a function specified during configuration - in our case, bufferEmpty()
. This function either calls writeBuffer()
to refill the empty buffer, if there is audio left to play, or it calls stop()
to disable the amplifier and shut down the DAC if playback is complete.
function bufferEmpty(buffer) {
// If we still have audio left to play, add a buffer
if (playbackPtr < params.wavSize + COOKIE_LENGTH) {
writeBuffer(buffer);
hardware.fixedfrequencydac.addbuffer(buffer);
} else {
if (!buffer) {
server.log("Null buffer callback.");
}
// Playback complete - disable the amplifier and shut down the Fixed-Frequency DAC
stop();
}
}
For a more detailed investigation of the fixed-frequency DAC, please see ‘Audio Waveforms and the Imp"’ in the Developer Center.
In the next section, we’ll use the imp003/LBWA1ZV1CD EVB’s multi-color LED as a display readout for weather data retrieved from the Internet.