Odtwarzanie danych audio na mikrokontrolerach

Obsługa audio nie jest obca mikrokontrolerom. Proste odtwarzacze audio można zrobić z wykorzystaniem Arduino i innych płytek z odpowiednimi mikrokontrolerami lub wykorzystać dedykowany moduł.

Sygnał audio można generować na poziomie samego mikrokontrolera za pomocą przetwornika cyfrowo-analogowego (Digital to Analog Converter, DAC). Jeżeli mikrokontroler nie oferuje takiego przetwornika można próbować wykorzystywać zegary używane przez PWM (lub użyć zewnętrznego przetwornika).

W tym artykule przejdę przez różne sposoby implementacji odtwarzania danych audio przez mikrokontroler. Pominę gotowe płytki-odtwarzacze mp3.

Audio na Arduino z ATmega328

Arduino Due posiada przetwornik DAC, natomiast Arduino Uno, czy Duemilanove nie posiadają go. Na highlowtech.org opisano jak zrealizować prosty odtwarzacz na tych dwóch płytkach z Atmegą.

Rozwiązanie dla Arduino Uno wykorzystuje dwa zegary (timers) normalnie używane do generowania wyjścia PWM za pomocą funkcji analogWrite. Jeden zegar generuje falę kwadratową o wysokiej częstotliwości i współczynniku wypełnienia (duty cycle) zależnym od wartości odtwarzanych danych audio. Drugi zegar jest używany do aktualizowania tego współczynnika z częstotliwością 8 kHZ (co odpowiada próbkowaniu tych danych audio). W efekcie przestaje działać PWM na pinach 3, 9, 10 i 11. Sygnał audio generowany jest na pinie 11.

  • Pobieramy bibliotekę PCM i dodajemy ją do Arduino IDE
  • Otwieramy przykład (Plik - Przykłady - PCM)
  • Podłączamy głośnik/słuchawki do pinu 11 oraz masy (GND)

Kod wygląda mniej więcej tak:

#include <PCM.h>

#include <PCM.h>

const unsigned char sample[] PROGMEM = {
  ......
};

void setup()
{
  startPlayback(sample, sizeof(sample));
}

void loop()
{
}

Tablica sample[] zawiera dane audio - plik Wav zapisany numerycznie. Ze względu na rozmiar takich danych tablicę tą zapisujemy do pamięci flash mikrokontrolera a nie do pamięci RAM, która jest znacznie mniejsza (dzięki PROGMEM). Edytor nie radzi sobie dobrze z długimi liniami i może nie wyświetlić przykładowych danych wewnątrz tej tablicy. Możemy przykład skompilować i wrzucić na Arduino. Gdy proces zostanie zakończony powinniśmy usłyszeć sygnał audio na głośniku (może być dość cichy).

Jeżeli wgranie kodu na Arduino nie udaje się to trzeba zmniejszyć rozmiar danych audio wycinając część liczb. Na Arduino Uno da się zmieścić zaledwie kilka (3-4) sekund danych audio.

Żeby wrzucić własne dane audio będziemy musieli skonwertować plik do formatu wav z próbkowaniem 8kHz i bitrate nie większym niż 16 kbps. To dość niska jakość, która powinna wystarczyć do głosu i prostych melodii. Do konwersji plików pod Linuksem można użyć aplikacji SoundConverter (aplikacja GNOME). Do zapisania pliku wav w postaci numerycznej potrzeba będzie dodatkowa aplikacja EncodeAudio, dostępna na highlowtech.org. Jest to aplikacja napisana w Javie. Pobieramy ZIP, rozpakowujemy, uruchamiamy aplikację i wybieramy plik Wav. Dane zostaną skopiowane do schowka - możemy wkleić je do edytora. W przypadku Linuksa dane te mogą być dostępne do czasu zamknięcia okienka informującego o skopiowaniu danych do schowka (przydaje się też menedżer zawartości schowka) - tak więc wklej dane zanim zamkniesz to okienko. Dla Arduino IDE najlepiej podzielić dane na wiele linii tak by program ten poprawnie je wyświetlał.

Poprawianie jakości sygnału audio

Powyższe rozwiązanie jest bardzo proste i wymaga poprawek. Raz że głośnik będzie odtwarzał dane dość cicho, a dwa - będzie dało się słyszeć spore szumy i zniekształcenia. Pierwszy problem rozwiąże wzmacniacz audio, a drugi filtr dolnoprzepustowy.

Chcąc efektywnie użyć głośnik o większej mocy potrzebować będziemy wzmacniacza audio. Wzmacniacze niedużej mocy, dobrze pasujące do zabawy z Arduino mają postać gotowych układów scalonych lub gotowych płytek z goldpinamy do płytek stykowych. Ja wybrałem wzmacniacz PAM8403 w postaci gotowej płytki i 3W głośnik. Jest to wzmacniacz dwukanałowy, więc wykorzystamy tylko jeden kanał (mamy sygnał mono). Wykorzystanie innych wzmacniaczy tego typu będzie bardzo podobne.

Umieszczamy wzmacniacz na płytce stykowej, podłączamy zasilanie (w tym przypadku wystarczy zasilanie z Arduino), masę i pin 11 do jednego z kanałów wejściowych. Głośnik podłączamy do wyjścia tego kanału i jego masy. Gotowe. Teraz powinno być głośniej (a szumy jeszcze bardziej słyszalne).

Filtr dolnoprzepustowy jak sama nazwa wskazuje tłumi częstotliwości sygnału powyżej pewnej częstotliwości granicznej. Może być użyty do odsiania zakłóceń z naszego sygnału audio. Do wykonania filtra potrzebować będziemy ceramiczny kondensator 100 nF oraz dobrany rezystor (kilkaset omów w moim przypadku). Schemat można znaleźć na wikipedii.

Pomiędzy filtrem a głośnikiem będzie jeszcze nasz wzmacniacz audio. Rezystor należy dobrać tak by skutecznie tłumił szum pozostawiając pożądany sygnał audio. W zależności od jakości głośnika i wzmacniacza taki filtr może okazać się zbędny.

Odtwarzanie plików wav

Pamięć mikrokontrolera jest mała i nie pozwala na przechowywanie większych ilości danych. Pliki audio można umieścić na karcie SD/microSD i odtwarzać podobnie jak w powyższym przykładzie. W przypadku Arduino dostępne są shieldy ze slotem na karty lub same adaptery, które z Arduino komunikują się po SPI. W przypadku niektórych innych płytek, jak np. pyboard z MicroPythonem slot microSD jest dostępny na płytce i może zastępować flash mikrokontrolera - na kod i dane.

Dla Arduino możemy użyć biblioteki TMRpcm. Przykład zaprezentowano na stronie diyhacking.com. Ja dostosowałem go do posiadanego wzmacniacza i filtru dolnoprzepustowego: pin 9 z sygnałem audio i GND przez filtr dolnoprzepustowy do wzmacniacza.

Adapter kart microSD podłączyłem do Arduino Uno:

  • MOSI – pin 11
  • MISO – pin 12
  • CLK/SCK – pin 13
  • CS – pin 4

Kolejny etap to konwersja pliku audio do obsługiwanego formatu 8-bitowego Wava. Można to zrobić na stronie audio.online-convert.com tak jak zaprezentowano to w artykule. Biblioteka nie jest w stanie odtwarzać innych formatów.

Od strony Arduino wystarczy przykładowy kod biblioteki:

#include "SD.h"
#define SD_ChipSelectPin 4
#include "TMRpcm.h"
#include "SPI.h"

TMRpcm tmrpcm;

void setup(){
tmrpcm.speakerPin = 9;
Serial.begin(9600);
if (!SD.begin(SD_ChipSelectPin)) {
Serial.println("SD fail");
return;
}

tmrpcm.setVolume(6);
tmrpcm.play("plik.wav");
}

void loop(){  }

Jeżeli plik znajduje się na karcie SD i jest w poprawnym formacie to powinien zostać odtworzony. Jakość jednak nie będzie zachwycająca - tak jakby nie całe pasmo częstotliwości było przenoszone na głośnik. Powinno wystarczyć do prostych melodii i komend głosowych o odpowiednio dobranych częstotliwościach.

Lepszą jakość audio może jednak zapewnić biblioteka SimpleSDAudio, która używa wyższej częstotliwości PWM niż biblioteka TMRpcm (pełne porównanie na stronie). Hardware nie ulega zmianie, natomiast kod już tak:

#include <SimpleSDAudio.h>
 
void setup()
{
SdPlay.setSDCSPin(4);

if (!SdPlay.init(SSDA_MODE_FULLRATE | SSDA_MODE_MONO | SSDA_MODE_AUTOWORKER))
{
while(1);
}

if(!SdPlay.setFile("plik.wav"))
{
while(1);
}
}

void loop(void) {

SdPlay.play();

while(!SdPlay.isStopped()) {
;
}
}

Możemy użyć też lepszych parametrów przy konwersji Wave - stereo i większy sampling. Jakość dźwięku powinna być wyraźnie lepsza. Żeby skompilować przykład z wykorzystaniem tej biblioteki musiałem użyć najnowszego Arduino IDE (nowszego niż to dostępne w Ubuntu 17.04). Przykład wykorzystania tej biblioteki znajdziemy też na stronie make.robimek.com.

DAC - przetwornik cyfrowo-analogowy

DAC jest znacznie lepszym rozwiązaniem do generowania sygnału audio niż wykorzystywanie zegarów PWM. Niestety przetworniki tego typu nie są dostępne na popularnych modelach Arduino. Trzeba stosować zewnętrzne chipy lub lepsze mikrokontrolery - Arduino Due, płytki oparte o STM32, czy ESP32.

W przypadku płytki PyBoard wyjście DAC znajduje się na pinie X5. W dokumentacji znajdziemy przykład odtwarzacza wav.

Bardzo krótki plik wav dołączony w dokumentacji powinien dać się odtworzyć bez problemu z pamięci flash płytki:

import wave
from pyb import DAC

dac = DAC(1)
f = wave.open('test.wav')
dac.write_timed(f.readframes(f.getnframes()), f.getframerate())

Do obsługi większych plików będziemy musieli użyć karty microSD jak i czytać dane stopniowo, gdyż całość na raz w pamięci się nie zmieści:

import wave
from pyb import DAC
from pyb import delay
dac = DAC(1)


def play(filename):
    f = wave.open(filename, 'r')
    total_frames = f.getnframes()
    framerate = f.getframerate()
    
    for position in range(0, total_frames, framerate):
        f.setpos(position)
        dac.write_timed(f.readframes(framerate), framerate)
        delay(1000)
        print(position)

W tej funkcji pobieram ilość klatek, próbkowanie (ilość klatek na sekundę) i następnie w pętli odczytuje klatki dla jednej sekundy, wysyłam na wyjście DAC i przesuwam wskaźnik. Wymaga to 8 bitowych plików wave z próbkowaniem maksimum 16kHz.

Ze względu na użycie modułu pyb kod ten wymaga użycia płytek pyboard. Płytki PyCom (np. WiPy 2.0) nie implementują tego modułu i nie są kompatybilne.

ESP32

Popularny mikrokontroler ESP32, następca ESP8266 posiada dwa wyjścia DAC. Można je użyć podobnie jak w MicroPythonie na STM32. Do programowania ESP32 użyłem nakładki dla Arduino IDE: arduino-esp32.

Najprostszy przykład będzie podobny do pierwszego przykładu dla Arduino - odtwarzanie danych audio zapisanych numerycznie w tablicy:

const unsigned char sample[] = {
  ......
};

void setup() {
  int j;
  for (j = 0; j < 25000; j++ ) { // hardcoded array size
        dacWrite(25, sample[j]);
        delayMicroseconds(125); // 8kHz sampling
  }
}

void loop() {
  
}

Mając tablicę wysyłamy dane z prędkością równą próbkowaniu danych (1/8000 = 125 microsec) dzięki delayMicroseconds. Można też użyć przykładu timera (przykłady ESP32 w Arduino IDE) - usunąć zbędną część i odtwarzać dane za jego pomocą.

const unsigned char sample[] = {
  ......
};
hw_timer_t * timer = NULL;
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;

volatile uint32_t isrCounter = 0;
volatile uint32_t lastIsrAt = 0;

void IRAM_ATTR onTimer(){
  // Increment the counter and set the time of ISR
  portENTER_CRITICAL_ISR(&timerMux);
  isrCounter++;
  lastIsrAt = millis();
  portEXIT_CRITICAL_ISR(&timerMux);
  // It is safe to use digitalRead/Write here if you want to toggle an output
  if (isrCounter > 25000) { // hardcoded size
      if (timer) {
      // Stop and free timer
      timerEnd(timer);
      timer = NULL;
      }
    }
  dacWrite(25, sample[isrCounter]);
}

void setup() {
  // Use 1st timer of 4 (counted from zero).
  // Set 80 divider for prescaler (see ESP32 Technical Reference Manual for more
  // info).
  timer = timerBegin(0, 80, true);

  // Attach onTimer function to our timer.
  timerAttachInterrupt(timer, &onTimer, true);

  // Set alarm to call onTimer function every second (value in microseconds).
  // Repeat the alarm (third parameter)
  timerAlarmWrite(timer, 125, true); // 8kHz

  // Start an alarm
  timerAlarmEnable(timer);
}

void loop() {
  
}

Niektóre płytki z ESP32 mogą mieć nawet 16MB flash co pozwala na odtwarzanie nieco większych danych audio. Dłuższe pliki audio nadal muszą być odczytywane z karty SD lub innego nośnika.

Powiązane artykuły

RkBlog

Elektronika i Python, 24 July 2017

Comment article
Comment article RkBlog main page Search RSS Contact