Saturday

AutoAnalogAudio: Decoding and playing MP3 files direct from SD card on nRF52

AutoAnalogAudio: Decoding and playing MP3 files direct from SD card on nRF52

Having fun with nRF52840 devices

I've been playing around quite a bit with my AnalogAudioAudio library since I got it working with I2S, including creating a simple Bluetooth controlled audio player.

Today, I found a simple MP3 decoding library, and decided to test it out. It actually worked! Now I have mp3 files being decoded directly from SD card on nRF52 devices like the Adafruit Feather 52840 or XIAO BLE Sense 52840.

There is lag of a few seconds prior to playback starting, but once that takes place, it seems to play the files well. The code I am using is still in prototyping stages but it does function!



I had to edit an existing MP3 decoding library a bit, but the current code I am using is at https://github.com/TMRh20/microDecoder

Any users that want to test it out can install directly from ZIP from this repository.

I would recommend testing the AutoAnalogAudio library with WAV files before venturing into MP3 playback, but to each his own...

Some example code using the above library is shown below. It has some bugs & quirks, but it does function.

#include <SD.h>
#include <AutoAnalogAudio.h>
#include "mp3.h"  // decoder
#include "pcm.h"

mp3 MP3;
pcm audio;
AutoAnalog aaAudio;

const char* audioFilename = "noceil.mp3";
uint8_t SD_CS_PIN = 5;                      // Set this to your CS pin for the SD card/module
#define USE_I2S 1

char songName[64];
float volumeControl = 0.2;
#define AUDIO_BUFFER_SIZE 6400

void setup() {

  pinMode(9, OUTPUT);
  digitalWrite(9, HIGH);
  pinMode(6, OUTPUT);  //Connected to SD pin of MAX98357A
  digitalWrite(6, HIGH);

  Serial.begin(115200);
  while (!Serial) delay(10);

  Serial.print("Init SD card...");
  if (!SD.begin(SD_CS_PIN)) {
    Serial.println("init failed!");
    return;
  }
  Serial.println("SD init ok");

  //aaAudio.I2S_PIN_LRCK = 28;  // Use different LRCK pin for Feather Express 52840
  aaAudio.maxBufferSize = AUDIO_BUFFER_SIZE;
  aaAudio.begin(0, 1, USE_I2S);

  playAudio(audioFilename);
}

void loop() {
 
  loadBuffer();

  // Control via Serial for testing
  if (Serial.available()) {
    char c = Serial.read();
    if (c == '=') {
      volumeControl += 0.1;
    } else if (c == '-') {
      volumeControl -= 0.1;
      volumeControl = max(0.0, volumeControl);
    } else if (c == 'p') {
      playAudio("waitress.mp3");
    }
    Serial.println(volumeControl);
  }

}


File myFile;

void playAudio(const char* audioFile) {

  if (myFile) {
    myFile.close();
    MP3.end();
  }
  //Open the designated file
  myFile = SD.open(audioFile);

  MP3.begin(myFile);
  MP3.getMetadata();
  aaAudio.setSampleRate(MP3.Fs, 1);
  Serial.println(MP3.Fs);
  aaAudio.dacBitsPerSample = MP3.bitsPerSample;
  Serial.println(MP3.bitsPerSample);
}

void loadBuffer() {

  if (myFile.available() > AUDIO_BUFFER_SIZE) {
    uint32_t sampleCounter = 0;
    for (uint32_t i = 0; i < 100; i++) {
      audio = MP3.decode();
      memcpy(&aaAudio.dacBuffer16[sampleCounter], audio.interleaved, 128);  // 128 bytes
      sampleCounter += 64;                                                  // 64 samples per go
    }
    for (uint32_t i = 0; i < 6400; i++) {
      int16_t sample = aaAudio.dacBuffer16[i];
      sample *= volumeControl;
      aaAudio.dacBuffer16[i] = sample;
    }
    aaAudio.feedDAC(0, 6400);
  } else {
    myFile.seek(0);
  }
}


Tuesday

AutoAnalogAudio Library: New examples for nRF52x including a BLE controlled Audio Player

 AutoAnalogAudio Library Updates for nRF52x:

 New examples for nRF52x & a BLE controlled Audio Player

With recent updates to the AutoAnalogAudio library, I've been able to put together a bunch of examples specific to the nRF52x platforms. The examples range from examples that use the onboard PDM microphone capabilities and either an I2S or Analog (PWM output) amplifier to a BLE controlled Audio Player.




The examples also demonstrate usage of the radio capabilities as well, using the radio either at a low level (nrf_to_nrf library), capable of streaming very high-quality audio or using BLE control to playback audio from SD card. 

When recording via PDM and either re-playing to an amplifier or broadcasting via radio, fairly high sample-rates can be used along with 16-bit modes, making for very decent quality wireless audio. There are some limitations when reading from SD card, as it seems the max SPI speed on these devices isn't that fast, so users need to play around with sample rates, stereo/mono modes and 8 or 16-bit samples.

Once the AutoAnalogAudio library is installed, the nRF52x examples can be found in Arduino examples under AutoAnalogAudio/Platforms/NRF52

XIAO BLE Sense 52840 used for testing


The BLE controlled audio player uses a bunch of different peripherals and pushes the capabilities of the device a bit, but it seems to work great. I've created another example using the Adafruit Bluefruit library as well, which supports faster SD reading & higher quality playback, which I will also include in the library soon. 

Here is the current code using the standard Arduino BLE library:

/* Arduino BLE control
led Audio Player for nRF52

 *
 * This is an example of me playing around with BLE control and different
 * services/characteristics to test the AutoAnalogAudio library.
 *
 * Requirements:
 * 1. nRF52 Device (Tested on nRF52840)
*  2. SD Card with WAV files: 8-bit, 16-24kHz, Mono
 * 3. I2S or Analog Amplifier + Speaker connected
 * 4. Mobile device or 'other' with nRF Connect installed
 *
 * Connect via nRF Connect App:
 * 1. Device should come up as BLE Audio Player
 * 2. You should see:
 *   a: Common Audio
*    b: Audio Input Type:
      Send a UTF-8 String to play a file: myfileDirectory/myfilename.wav
 *   c: Audio Input Control Point:
      Send an Unsigned value between 0-10 to set the volume low-high
 */


#include <SPI.h>
#include <SD.h>
#include <ArduinoBLE.h>
#include <AutoAnalogAudio.h>

AutoAnalog aaAudio;

/************** USER CONFIG ***********/
// File to play on startup
const char* audioFilename = "far8b16k.wav";  // 8-bit @ 24kHz audio is the max over SD card while BLE is running
uint8_t SD_CS_PIN = 2;                       // Set this to your CS pin for the SD card/module
#define USE_I2S 1                            // Set this to 0 for analog (PWM) audio output instead of I2S

/*********************************************************/
/* Tested with MAX98357A I2S breakout
/* BCLK connected to Arduino D1 (p0.03)
/* LRCK connected to Arduino D3 (p0.29)
/* DIN  connected to Arduino D5 (p0.05)
/* SD   connected to Arduino D6 (p1.11)
/*********************************************************/

#define FILENAME_BUFFER_LENGTH 64
char songName[FILENAME_BUFFER_LENGTH];
float volumeControl = 0.2;
#define AUDIO_BUFFER_SIZE 1600

BLEService audioService("1853");

// BLE Audio Charactaristic
BLECharacteristic audioDataCharacteristic("2b79", BLERead | BLEWrite | BLENotify, FILENAME_BUFFER_LENGTH);
BLEByteCharacteristic audioVolumeCharactaristic("2b7b", BLERead | BLEWrite);

void setup() {
  Serial.begin(115200);
  while (!Serial) delay(10);

  aaAudio.begin(0, 1, USE_I2S);  //Setup aaAudio using DAC and I2S or PWM

  // BLE initialization
  if (!BLE.begin()) {
    Serial.println("Starting BLE failed!");
    while (1) {};
  }

  BLE.setLocalName("BLE Audio Player");
  BLE.setAdvertisedService(audioService);

  audioService.addCharacteristic(audioDataCharacteristic);
  audioService.addCharacteristic(audioVolumeCharactaristic);
  BLE.addService(audioService);

  BLE.advertise();
  Serial.println("BLE Peripheral is now advertising");

  Serial.print("Init SD card...");
  if (!SD.begin(SD_CS_PIN)) {
    Serial.println("init failed!");
    return;
  }
  Serial.println("SD init ok");
  pinMode(6, OUTPUT);  //Connected to SD pin of MAX98357A
  digitalWrite(6, HIGH);

  playAudio(audioFilename);
}

void loop() {

  BLEDevice central = BLE.central();

  if (central) {

    if (central.connected()) {
      if (audioDataCharacteristic.written()) {
        memset(songName, 0, sizeof(songName));
        audioDataCharacteristic.readValue((uint8_t*)songName, FILENAME_BUFFER_LENGTH);
        playAudio(songName);
        Serial.println(songName);
      }
      if (audioVolumeCharactaristic.written()) {
        uint8_t vol;
        audioVolumeCharactaristic.readValue(vol);
        volumeControl = vol / 10.0;
        Serial.print("BLE Set Volume: ");
        Serial.println(volumeControl);
      }
    }
  }

  loadBuffer();

  // Control via Serial for testing
  if (Serial.available()) {
    char c = Serial.read();
    if (c == '=') {
      volumeControl += 0.1;
    } else if (c == '-') {
      volumeControl -= 0.1;
      volumeControl = max(0.0, volumeControl);
    } else if (c == 'p') {
      playAudio("brick/brick24.wav");
    }
    Serial.println(volumeControl);
  }
}

/*********************************************************/
/* A simple function to handle playing audio files
/*********************************************************/

File myFile;

void playAudio(const char* audioFile) {

  if (myFile) {
    myFile.close();
  }
  //Open the designated file
  myFile = SD.open(audioFile);

  myFile.seek(22);
  uint16_t var;
  uint32_t var2;
  myFile.read(&var, 2);   // Get channels (Stereo or Mono)
  myFile.read(&var2, 4);  // Get Sample Rate
  aaAudio.setSampleRate(var2, var - 1);

  myFile.seek(34);
  myFile.read(&var, 2);  // Get Bits Per Sample
  aaAudio.dacBitsPerSample = var;

  myFile.seek(44);  //Skip past the WAV header
}

void loadBuffer() {

  if (myFile.available()) {

    if (aaAudio.dacBitsPerSample == 8) {
      myFile.read(aaAudio.dacBuffer, AUDIO_BUFFER_SIZE);
      for (uint32_t i = 0; i < AUDIO_BUFFER_SIZE; i++) {
        aaAudio.dacBuffer[i] *= volumeControl;
      }
      aaAudio.feedDAC(0, AUDIO_BUFFER_SIZE);
    } else {
      myFile.read(aaAudio.dacBuffer16, AUDIO_BUFFER_SIZE);
      for (uint32_t i = 0; i < AUDIO_BUFFER_SIZE / 2; i++) {
        int16_t sample = aaAudio.dacBuffer16[i];
        sample *= volumeControl;
        aaAudio.dacBuffer16[i] = (uint16_t)sample;
      }
      aaAudio.feedDAC(0, AUDIO_BUFFER_SIZE / 2);
    }

  } else {
    myFile.seek(44);
  }
}



Thursday

AutoAnalogAudio Library & I2S Output Now Working on nRF52840

 AutoAnalogAudio Library & I2S Output Now Working on nRF52840

Playing around with high quality audio on the XIAO Sense 52840

Its been a while since my last post regarding I2S audio on the nRF52840, but I finally got it working!

It came down to too small of buffer sizes, plus incorrect pins used for the MAX98357A breakout board. Now that the main problems are figured out, I have it working. There are still some issues with buffering, as there are small clicks or pops here or there throughout playback if the buffer sizes are too small.



To get it working with the current code from GitHub users need to enable I2S by calling the begin(); function a little differently:

aaAudio.begin(0, 1, 1);  //Setup aaAudio using DAC and I2S

This example enables the 'DAC' output using I2S within the AutoAnalogAudio library.

Valid sample rates have now been modified to standard sample rates: 16kHz, 24kHz, 32kHz & 44kHz. These are the only accepted sample rates.

I've decided to leave the PWM output as the default behavior since it is simpler in nature, gives good results and works OK with smaller buffer sizes. This is important when transmitting or receiving audio over radio link, since you need pretty large buffers with I2S.

Users can set the I2S pins prior to calling the begin() function for other boards that use different pins.

aaAudio.I2S_PIN_LRCK = 28;

Default Arduino Pin-Out for MAX98357A on XIAO Sense 52840:

BCLK: D1 (P0.03)

LRCK: D3 (P0.29)

DIN: D5 (P0.05)

SD Pin needs to be set HIGH for left output

GAIN: VIN

Use buffer sizes from 3200 to 6400 samples with higher sample rates / bit-rates.

AutoAnalogAudio Library - nRF52 Now supports Successive Approximation Analog-to-Digital Converter for Audio Input

 AutoAnalogAudio Library - nRF52 Now supports SAADC Support for Successive Approximation Analog-to-Digital Converter for Audio Input I'v...