Skip to content

AudioMoth Application

OpenAcousticDevices edited this page Nov 19, 2017 · 29 revisions

The code here is an annotated description of the default AudioMoth application intended to serve as an example for others wishing to developed AudioMoth applications.

The code starts by including standard headers along with the AudioMoth library header

#include <time.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdbool.h>

#include "audioMoth.h"

Next some constants determining the timing of LED flashes are defined.

/* Sleep and LED constants */

#define DEFAULT_WAIT_INTERVAL               1

#define WAITING_LED_FLASH_INTERVAL          2
#define WAITING_LED_FLASH_DURATION          1

#define LOW_BATTERY_LED_FLASHES             10

#define SHORT_LED_FLASH_DURATION            100
#define LONG_LED_FLASH_DURATION             500

Along with some useful time constants.

/* Useful time constants */

#define SECONDS_IN_MINUTE                   60
#define SECONDS_IN_HOUR                     (60 * SECONDS_IN_MINUTE)
#define SECONDS_IN_DAY                      (24 * SECONDS_IN_HOUR)

To make write microphones samples to the microSD card, whilst simultaneously receiving them from the microphone, an eight partition circular buffer is used. The first buffer of any recording is skipped to allow the DC filtering to reach equilibrium.

/* SRAM buffer constants */

#define NUMBER_OF_BUFFERS                   8
#define EXTERNAL_SRAM_SIZE_IN_SAMPLES       (AM_EXTERNAL_SRAM_SIZE_IN_BYTES / 2)
#define NUMBER_OF_SAMPLES_IN_BUFFER         (EXTERNAL_SRAM_SIZE_IN_SAMPLES / NUMBER_OF_BUFFERS)
#define NUMBER_OF_SAMPLES_IN_DMA_TRANSFER   1024
#define NUMBER_OF_BUFFERS_TO_SKIP           1

A number of other useful constants are defined dealing with the WAV header, the maximum number of start-stop periods that are allowed to be configured, and the constant used in the DC filter routine.

/* WAV header constant */

#define PCM_FORMAT                          1
#define RIFF_ID_LENGTH                      4
#define LENGTH_OF_COMMENT                   128

/* USB configuration constant */

#define MAX_START_STOP_PERIODS              5

/* DC filter constant */

#define DC_BLOCKING_FACTOR                  0.995f

A number of macros are defined to remove boiler plate code later in the application.

/* Useful macros */

#define FLASH_LED(led, duration) { \
    AudioMoth_set ## led ## LED(true); \
    AudioMoth_delay(duration); \
    AudioMoth_set ## led ## LED(false); \
}

#define RETURN_ON_ERROR(fn) { \
    bool success = (fn); \
    if (success != true) { \
        recordingCancelled = true; \
        FLASH_LED(Both, LONG_LED_FLASH_DURATION) \
        return; \
    } \
}

#define SAVE_SWITCH_POSITION_AND_POWER_DOWN(duration) { \
    *previousSwitchPosition = switchPosition; \
    AudioMoth_powerDownAndWake(duration, true); \
}

#define MAX(a,b) (((a) > (b)) ? (a) : (b))

#define MIN(a,b) (((a) < (b)) ? (a) : (b))

The header of the WAV file is defined in a series of data structures, and a static default header is provided.

/* WAV header */

#pragma pack(push, 1)

typedef struct {
    char id[RIFF_ID_LENGTH];
    uint32_t size;
} chunk_t;

typedef struct {
    chunk_t icmt;
    char comment[LENGTH_OF_COMMENT];
} icmt_t;

typedef struct {
    uint16_t format;
    uint16_t numberOfChannels;
    uint32_t samplesPerSecond;
    uint32_t bytesPerSecond;
    uint16_t bytesPerCapture;
    uint16_t bitsPerSample;
} wavFormat_t;

typedef struct {
    chunk_t riff;
    char format[RIFF_ID_LENGTH];
    chunk_t fmt;
    wavFormat_t wavFormat;
    chunk_t list;
    char info[RIFF_ID_LENGTH];
    icmt_t icmt;
    chunk_t data;
} wavHeader_t;

#pragma pack(pop)

static wavHeader_t wavHeader = {
    .riff = {.id = "RIFF", .size = 0},
    .format = "WAVE",
    .fmt = {.id = "fmt ", .size = sizeof(wavFormat_t)},
    .wavFormat = {.format = PCM_FORMAT, .numberOfChannels = 1, .samplesPerSecond = 0, .bytesPerSecond = 0, .bytesPerCapture = 2, .bitsPerSample = 16},
    .list = {.id = "LIST", .size = RIFF_ID_LENGTH + sizeof(icmt_t)},
    .info = "INFO",
    .icmt = {.icmt.id = "ICMT", .icmt.size = LENGTH_OF_COMMENT, .comment = ""},
    .data = {.id = "data", .size = 0}
};

Two utility functions allow details in the header to be updated. These include setting the sample rate and length of the recording, and also writing the unique ID of the device, the time stamp of the recording, and the battery state of the device into the comment field of the header.

void setHeaderDetails(uint32_t sampleRate, uint32_t numberOfSamples) {

    wavHeader.wavFormat.samplesPerSecond = sampleRate;
    wavHeader.wavFormat.bytesPerSecond = 2 * sampleRate;
    wavHeader.data.size = 2 * numberOfSamples;
    wavHeader.riff.size = 2 * numberOfSamples + sizeof(wavHeader_t) - sizeof(chunk_t);

}

void setHeaderComment(uint32_t currentTime, uint8_t *serialNumber, uint32_t gain) {

    time_t rawtime = currentTime;

    struct tm *time = gmtime(&rawtime);

    char *comment = wavHeader.icmt.comment;

    AM_batteryState_t batteryState = AudioMoth_getBatteryState();

    sprintf(comment, "Recorded at %02d:%02d:%02d %02d/%02d/%04d (UTC) by AudioMoth %08X%08X at gain setting %d while battery state was ",
        time->tm_hour, time->tm_min, time->tm_sec, time->tm_mday, 1 + time->tm_mon, 1900 + time->tm_year,
        (unsigned int)(serialNumber + 8), (unsigned int)serialNumber, (unsigned int)gain);

    comment += 110;

    if (batteryState == AM_BATTERY_LOW) {

        sprintf(comment, "< 3.6V");

    } else if (batteryState >= AM_BATTERY_FULL) {

        sprintf(comment, "> 5.0V");

    } else {

        batteryState += 35;

        sprintf(comment, "%01d.%01dV", batteryState / 10, batteryState % 10);

    }

}

The device is configured by data structures exchanged over USB. The application code must define the format of this data structure and provide a static default data structure.

/* USB configuration data structure */

#pragma pack(push, 1)

typedef struct {
    uint16_t startMinutes;
    uint16_t stopMinutes;
} startStopPeriod_t;

typedef struct {
    uint32_t time;
    uint8_t gain;
    uint8_t clockDivider;
    uint8_t acquisitionCycles;
    uint8_t oversampleRate;
    uint32_t sampleRate;
    uint8_t sampleRateDivider;
    uint16_t sleepDuration;
    uint16_t recordDuration;
    uint8_t enableLED;
    uint8_t activeStartStopPeriods;
    startStopPeriod_t startStopPeriods[MAX_START_STOP_PERIODS];
} configSettings_t;

#pragma pack(pop)

configSettings_t defaultConfigSettings = {
    .time = 0,
    .gain = 2,
    .clockDivider = 4,
    .acquisitionCycles = 16,
    .oversampleRate = 1,
    .sampleRate = 384000,
    .sampleRateDivider = 8,
    .sleepDuration = 0,
    .recordDuration = 60,
    .enableLED = 1,
    .activeStartStopPeriods = 0,
    .startStopPeriods = {
        {.startMinutes = 60, .stopMinutes = 120},
        {.startMinutes = 300, .stopMinutes = 420},
        {.startMinutes = 540, .stopMinutes = 600},
        {.startMinutes = 720, .stopMinutes = 780},
        {.startMinutes = 900, .stopMinutes = 960}
    }
};

Since the application repeatedly powers down the AudioMoth it is necessary to store a number of variables in persistent memory. This includes the configuration data structure that is currently active. To do so, pointers to the variables are declared in the backup domain sector of SRAM using the definition provided by the AudioMoth library.

uint32_t *previousSwitchPosition = (uint32_t*)AM_BACKUP_DOMAIN_START_ADDRESS;

uint32_t *timeOfNextRecording = (uint32_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 4);

uint32_t *durationOfNextRecording = (uint32_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 8);

configSettings_t *configSettings = (configSettings_t*)(AM_BACKUP_DOMAIN_START_ADDRESS + 12);

The DC filter removes any DC offset from the microphone samples. It uses a small number of global variables.

/* DC filter variables */

static int8_t bitsToShift;

static int32_t previousSample;
static int32_t previousFilterOutput;

Similarly, the circular buffer uses a small number of variables to indicate which buffer is currently being written to, whether the recording has been cancelled due to changing the switch position, and the location of the partitions of the circular buffer. Note that a number of these are volatile as their values is changed inside an interrupt routine.

/* SRAM buffer variables */

static volatile uint32_t writeBuffer;
static volatile uint32_t writeBufferIndex;

static volatile bool recordingCancelled;

static int16_t* buffers[NUMBER_OF_BUFFERS];

The application uses direct memory access to write samples from the microphone into memory. Two buffers are declared for these transfers.

/* DMA buffers */

static int16_t primaryBuffer[NUMBER_OF_SAMPLES_IN_DMA_TRANSFER];
static int16_t secondaryBuffer[NUMBER_OF_SAMPLES_IN_DMA_TRANSFER];

The hexadecimal Unix time is used as the file name of the WAV recordings. A global variable reserves space for this file name.

/* Current recording file name */

static char fileName[13];

A small number of function prototypes are declared.

/* Function prototypes */

static void flashLedToIndicateBatteryLife(void);
static void makeRecording(uint32_t currentTime, uint32_t recordDuration, bool enableLED);
static void filter(int16_t *source, int16_t *dest, uint8_t sampleRateDivider, uint32_t size);
static void scheduleRecording(uint32_t currentTime, uint32_t *timeOfNextRecording, uint32_t *durationOfNextRecording);

/* Main function */

int main(void) {

/* Initialise device */

AudioMoth_initialise();

AudioMoth_setUpDebugOutput();

while (true) printf("Hello\n");

AM_switchPosition_t switchPosition = AudioMoth_getSwitchPosition();

if (AudioMoth_isInitialPowerUp()) {

    *timeOfNextRecording = 0;

    *durationOfNextRecording = 0;

    *previousSwitchPosition = AM_SWITCH_NONE;

    memcpy(configSettings, &defaultConfigSettings, sizeof(configSettings_t));

} else {

    /* Indicate battery state is not initial power up and switch has been moved into USB */

    if (switchPosition != *previousSwitchPosition && switchPosition == AM_SWITCH_USB) {

        flashLedToIndicateBatteryLife();

    }

}

/* Handle the case that the switch is in USB position  */

if (switchPosition == AM_SWITCH_USB) {

    AudioMoth_handleUSB();

    SAVE_SWITCH_POSITION_AND_POWER_DOWN(DEFAULT_WAIT_INTERVAL);

}

/* Handle the case that the switch is in CUSTOM position but the time has not been set */

if (switchPosition == AM_SWITCH_CUSTOM && (AudioMoth_hasTimeBeenSet() == false || configSettings->activeStartStopPeriods == 0)) {

    FLASH_LED(Both, SHORT_LED_FLASH_DURATION)

    SAVE_SWITCH_POSITION_AND_POWER_DOWN(DEFAULT_WAIT_INTERVAL);

}

/* Calculate time of next recording if switch has changed position */

uint32_t currentTime = AudioMoth_getTime();

if (switchPosition != *previousSwitchPosition) {

     if (switchPosition == AM_SWITCH_DEFAULT) {

         /* Set parameters to start recording now */

         *timeOfNextRecording = currentTime;

         *durationOfNextRecording = configSettings->recordDuration;

     } else {

         /* Determine starting time and duration of next recording */

         scheduleRecording(currentTime, timeOfNextRecording, durationOfNextRecording);

     }

}

/* Make recording if appropriate */

bool enableLED = (switchPosition == AM_SWITCH_DEFAULT) || configSettings->enableLED;

if (currentTime >= *timeOfNextRecording) {

    makeRecording(currentTime, *durationOfNextRecording, enableLED);

	if (switchPosition == AM_SWITCH_DEFAULT) {

		/* Set parameters to start recording after sleep period */

		if (!recordingCancelled) {

			*timeOfNextRecording = currentTime + configSettings->recordDuration + configSettings->sleepDuration;

		}

	} else {

		/* Determine starting time and duration of next recording */

		scheduleRecording(currentTime, timeOfNextRecording, durationOfNextRecording);

	}

} else if (enableLED) {

    /* Flash LED to indicate waiting */

    FLASH_LED(Green, WAITING_LED_FLASH_DURATION)

}

/* Determine how long to power down */

uint32_t secondsToSleep = 0;

if (*timeOfNextRecording > currentTime) {

    secondsToSleep = MIN(*timeOfNextRecording - currentTime, WAITING_LED_FLASH_INTERVAL);

}

/* Power down */

SAVE_SWITCH_POSITION_AND_POWER_DOWN(secondsToSleep);

}

/* AudioMoth handlers */

inline void AudioMoth_handleSwitchInterrupt() {

recordingCancelled = true;

}

inline void AudioMoth_handleMicrophoneInterrupt(int16_t sample) { }

inline void AudioMoth_handleDirectMemoryAccessInterrupt(bool isPrimaryBuffer, int16_t **nextBuffer) {

int16_t *source = secondaryBuffer;

if (isPrimaryBuffer) source = primaryBuffer;

/* Update the current buffer index and write buffer */

filter(source, buffers[writeBuffer] + writeBufferIndex, configSettings->sampleRateDivider, NUMBER_OF_SAMPLES_IN_DMA_TRANSFER);

writeBufferIndex += NUMBER_OF_SAMPLES_IN_DMA_TRANSFER / configSettings->sampleRateDivider;

if (writeBufferIndex == NUMBER_OF_SAMPLES_IN_BUFFER) {

    writeBufferIndex = 0;

    writeBuffer = (writeBuffer + 1) & (NUMBER_OF_BUFFERS - 1);

}

}

inline void AudioMoth_usbApplicationPacketRequested(uint32_t messageType, uint8_t *transmitBuffer, uint32_t size) {

/* Copy the current time to the USB packet */

uint32_t currentTime = AudioMoth_getTime();

memcpy(transmitBuffer + 1, &currentTime, 4);

/* Copy the unique ID to the USB packet */

memcpy(transmitBuffer + 5, (uint8_t*)AM_UNIQUE_ID_START_ADDRESS, AM_UNIQUE_ID_SIZE_IN_BYTES);

/* Copy the battery state to the USB packet */

AM_batteryState_t batteryState = AudioMoth_getBatteryState();

memcpy(transmitBuffer + 5 + AM_UNIQUE_ID_SIZE_IN_BYTES, &batteryState, 1);

}

inline void AudioMoth_usbApplicationPacketReceived(uint32_t messageType, uint8_t* receiveBuffer, uint8_t *transmitBuffer, uint32_t size) {

/* Copy the USB packet contents to the back-up register data structure location */

memcpy(configSettings, receiveBuffer + 1, sizeof(configSettings_t));

/* Copy the back-up register data structure to the USB packet */

memcpy(transmitBuffer + 1, configSettings, sizeof(configSettings_t));

/* Set the time */

AudioMoth_setTime(configSettings->time);

}

/* Remove DC offset from the microphone samples */

static void filter(int16_t *source, int16_t *dest, uint8_t sampleRateDivider, uint32_t size) {

int32_t filteredOutput;
int32_t scaledPreviousFilterOutput;

int index = 0;

for (int i = 0; i < size; i += sampleRateDivider) {

    int32_t sample = 0;

    for (int j = 0; j < sampleRateDivider; j += 1) {

        sample += source[i + j];

    }

    if (bitsToShift > 0) sample = sample << bitsToShift;

    if (bitsToShift < 0) sample = sample >> -bitsToShift;

    scaledPreviousFilterOutput = (int32_t)(DC_BLOCKING_FACTOR * previousFilterOutput);

    filteredOutput = sample - previousSample + scaledPreviousFilterOutput;

    dest[index++] = (int16_t)filteredOutput;

    previousFilterOutput = filteredOutput;

    previousSample = (int32_t)sample;

}

}

/* Save recording to SD card */

static void makeRecording(uint32_t currentTime, uint32_t recordDuration, bool enableLED) {

/* Initialise buffers */

writeBuffer = 0;

writeBufferIndex = 0;

recordingCancelled = false;

buffers[0] = (int16_t*)AM_EXTERNAL_SRAM_START_ADDRESS;

for (int i = 1; i < NUMBER_OF_BUFFERS; i += 1) {
    buffers[i] = buffers[i - 1] + NUMBER_OF_SAMPLES_IN_BUFFER;
}

/* Calculate the bits to shift */

bitsToShift = 0;

uint8_t oversampleRate = configSettings->oversampleRate;

uint8_t sampleRateDivider = configSettings->sampleRateDivider;

while (oversampleRate < 16) {
    oversampleRate = oversampleRate << 1;
    bitsToShift += 1;
}

while (sampleRateDivider > 1) {
    sampleRateDivider = sampleRateDivider >> 1;
    bitsToShift -= 1;
}

/* Calculate recording parameters */

uint32_t numberOfSamplesInHeader = sizeof(wavHeader) >> 1;

uint32_t numberOfSamples = configSettings->sampleRate / configSettings->sampleRateDivider * recordDuration;

/* Initialise microphone for recording */

AudioMoth_enableExternalSRAM();

AudioMoth_enableMicrophone(configSettings->gain, configSettings->clockDivider, configSettings->acquisitionCycles, configSettings->oversampleRate);

AudioMoth_initialiseDirectMemoryAccess(primaryBuffer, secondaryBuffer, NUMBER_OF_SAMPLES_IN_DMA_TRANSFER);

AudioMoth_startMicrophoneSamples(configSettings->sampleRate);

/* Initialise file system and open a new file */

RETURN_ON_ERROR(AudioMoth_enableFileSystem());

/* Open a file with the name as a UNIX time stamp in HEX */

sprintf(fileName, "%08X.WAV", (unsigned int)currentTime);

RETURN_ON_ERROR(AudioMoth_openFile(fileName));

/* Main record loop */

uint32_t samplesWritten = 0;

uint32_t buffersProcessed = 0;

uint32_t readBuffer = writeBuffer;

while (samplesWritten < numberOfSamples + numberOfSamplesInHeader && !recordingCancelled) {

    while (readBuffer != writeBuffer && samplesWritten < numberOfSamples + numberOfSamplesInHeader && !recordingCancelled) {

        /* Light LED during SD card write if appropriate */

        if (enableLED) {

            AudioMoth_setRedLED(true);

        }

        /* Write the appropriate number of bytes to the SD card */

        uint32_t numberOfSamplesToWrite = 0;

        if (buffersProcessed >= NUMBER_OF_BUFFERS_TO_SKIP) {

            numberOfSamplesToWrite = MIN(numberOfSamples + numberOfSamplesInHeader - samplesWritten, NUMBER_OF_SAMPLES_IN_BUFFER);

        }

        AudioMoth_writeToFile(buffers[readBuffer], 2 * numberOfSamplesToWrite);

        /* Increment buffer counters */

        readBuffer = (readBuffer + 1) & (NUMBER_OF_BUFFERS - 1);

        samplesWritten += numberOfSamplesToWrite;

        buffersProcessed += 1;

        /* Clear LED */

        AudioMoth_setRedLED(false);

    }

    /* Sleep until next DMA transfer is complete */

    AudioMoth_sleep();

}

/* Initialise the WAV header */

samplesWritten = MAX(numberOfSamplesInHeader, samplesWritten);

setHeaderDetails(configSettings->sampleRate / configSettings->sampleRateDivider, samplesWritten - numberOfSamplesInHeader);

setHeaderComment(currentTime, (uint8_t*)AM_UNIQUE_ID_START_ADDRESS, configSettings->gain);

/* Write the header */

if (enableLED) {

    AudioMoth_setRedLED(true);

}

AudioMoth_seekInFile(0);

AudioMoth_writeToFile(&wavHeader, sizeof(wavHeader));

AudioMoth_setRedLED(false);

/* Close the file */

AudioMoth_closeFile();

}

When using scheduled recordings the application must determine when the next recording should take place and how long it should be. Recordings that overrun the end of a recording period are automatically truncated. If the switch is moved to CUSTOM mode whilst the time is already inside one of the recording periods, the recording will wait for the appropriate cycle to start to ensure that the time stamps of recordings on this and subsequent days match.

static void scheduleRecording(uint32_t currentTime, uint32_t *timeOfNextRecording, uint32_t *durationOfNextRecording) {

    /* Check number of active state stop periods */

    if (configSettings->activeStartStopPeriods > MAX_START_STOP_PERIODS) {

        configSettings->activeStartStopPeriods = MAX_START_STOP_PERIODS;

    }

    /* No active periods */

    if (configSettings->activeStartStopPeriods == 0) {

        *timeOfNextRecording = UINT32_MAX;

        *durationOfNextRecording = configSettings->recordDuration;

        return;

    }

    /* Calculate the number of seconds of this day */

    time_t rawtime = currentTime;

    struct tm *time = gmtime(&rawtime);

    uint32_t currentSeconds = SECONDS_IN_HOUR * time->tm_hour + SECONDS_IN_MINUTE * time->tm_min + time->tm_sec;

    /* Check each active start stop period */

    uint32_t durationOfCycle = configSettings->recordDuration + configSettings->sleepDuration;

    for (uint32_t i = 0; i < configSettings->activeStartStopPeriods; i += 1) {

        startStopPeriod_t *period = configSettings->startStopPeriods + i;

        /* Calculate the start and stop time of the current period */

        uint32_t startSeconds = SECONDS_IN_MINUTE * period->startMinutes;

        uint32_t stopSeconds = SECONDS_IN_MINUTE * period->stopMinutes;

        /* Calculate time to next period or time to next start in this period */

        if (currentSeconds < startSeconds) {

            *timeOfNextRecording = currentTime + (startSeconds - currentSeconds);

            *durationOfNextRecording = MIN(configSettings->recordDuration, stopSeconds - startSeconds);

            return;

        } else if (currentSeconds < stopSeconds) {

            uint32_t cycles = (currentSeconds - startSeconds + durationOfCycle) / durationOfCycle;

            uint32_t secondsFromStartOfPeriod = cycles * durationOfCycle;

            if (secondsFromStartOfPeriod < stopSeconds - startSeconds) {

                *timeOfNextRecording = currentTime + (startSeconds - currentSeconds) + secondsFromStartOfPeriod;

                *durationOfNextRecording = MIN(configSettings->recordDuration, stopSeconds - startSeconds - secondsFromStartOfPeriod);

                return;

            }

        }

    }

    /* Calculate time until first period tomorrow */

    startStopPeriod_t *firstPeriod = configSettings->startStopPeriods;

    uint32_t startSeconds = SECONDS_IN_MINUTE * firstPeriod->startMinutes;

    uint32_t stopSeconds = SECONDS_IN_MINUTE * firstPeriod->stopMinutes;

    *timeOfNextRecording = currentTime + (SECONDS_IN_DAY - currentSeconds) + startSeconds;

    *durationOfNextRecording = MIN(configSettings->recordDuration, stopSeconds - startSeconds);

}

When the switch position is moved into the USB position, the application flashes the red LED to indicate the current battery state (four times for full and once for low). If the battery voltage is too low for normal operation the red LED will flash ten times in quick succession.

static void flashLedToIndicateBatteryLife(void){

    uint32_t numberOfFlashes = LOW_BATTERY_LED_FLASHES;

    AM_batteryState_t batteryState = AudioMoth_getBatteryState();

    /* Set number of flashes according to battery state */

    if (batteryState > AM_BATTERY_LOW) {

        numberOfFlashes = (batteryState >= AM_BATTERY_4V6) ? 4 : (batteryState >= AM_BATTERY_4V4) ? 3 : (batteryState >= AM_BATTERY_4V0) ? 2 : 1;

    }

    /* Flash LED */

    for (uint32_t i = 0; i < numberOfFlashes; i += 1) {

        FLASH_LED(Red, SHORT_LED_FLASH_DURATION)

        if (numberOfFlashes == LOW_BATTERY_LED_FLASHES) {

            AudioMoth_delay(SHORT_LED_FLASH_DURATION);

        } else {

            AudioMoth_delay(LONG_LED_FLASH_DURATION);

        }

    }

}
Clone this wiki locally