Skip to main content

Audio Playback

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.

Peripherals Used

  • Class D audio amplifier, imp003/LBWA1ZV1CD ADC

APIs Used


Create a Developer Account and add your EVB to impCentral™

  1. Create a free Developer Account to access Electric Imp impCentral and log in.
  2. Download the Electric Imp app from the App Store or Google Play store on your iOS or Android device, and log in using your Developer Account credentials.
  3. On the Connect an Imp page, select your WiFi network.
    • The imp only operates using 2.4GHz 802.11b, g or n. 5GHz networks are not supported.
  4. Use BlinkUp™ to configure your EVB for Internet access. Power up the EVB then press Send BlinkUp in the Electric Imp app. As soon as you hear the countdown tones, place your tablet or phone’s screen against the EVB’s imp003/LBWA1ZV1CD breakout board until the screen stops flashing and you hear a second set of tones.

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:

  1. Click ‘Settings’ in the ‘MANAGE’ column.
  2. In the ‘Device Settings’ panel, enter a new name, eg. ‘LBWA1ZV1CD EVB’, ‘imp003 EVB’ or ‘My EVB’, and click ‘Update’.

Create a New Product and Device Group

  1. Click on your account name in the grey bar at the top of the window to view a list of your Products — this will be empty initially. If it isn’t, select the “EVB” Product, click ‘Create New Device Group’, and go to Step 4.
  2. Click ‘Create New Product’.
  3. Enter a name for the Product: “EVB”.
  4. Enter a name for the first Device Group: “Audio Playback”.

Add Code

  1. Copy the device and agent code from our GitHub repo and paste it into the impCentral’s code editor ‘Device Code’ and ‘Agent Code’ panes, respectively.
  2. Click ‘Build and Force Restart’ to transfer the device code to the imp003/LBWA1ZV1CD and the agent code to Electric Imp impCloud™, and to run them.

What’s Going On

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.

What the Agent Does

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:

  1. Upload the data directly to the agent (using curl, for example)
  2. Upload a URL directly to the agent that points to a WAV file
  3. Submit a URL using the agent-hosted web form

(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());

    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
    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) {;                    // 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;
        };                    // 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;

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);

What the Device Does

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.)

Saving to flash

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++) {
    // 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);
        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++) {
    hardware.fixedfrequencydac.configure(speaker, params.sampleRate, buffers, bufferEmpty, AUDIO);
    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) {
    } else {
        if (!buffer) {
            server.log("Null buffer callback.");
        // Playback complete - disable the amplifier and shut down the Fixed-Frequency DAC

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.