Napisz do mnie

Wyślij prywatną wiadomość

ARDUINO SOUNDCARD

opublikowano: 14 sty 2014 wyświetleń: 3326 komentarzy: 4

#arduino #avr #sound #atmel #dźwięk #wave #pcm

Odtwarzanie wielu dźwięków jednocześnie na Arduino (AVR). Symulator dźwięku silnika do modeli RC.

W trakcie prac nad elektroniczną częścią łóżka dla dziecka w postaci straży pożarnej, stanąłem przed wyzwaniem generowania dźwięku silnika. Samemu symulatorowi silnika poświęcę najprawdopodobniej osoby wpis. Tutaj chciałbym podzielić się wiedzą i spostrzeżeniami nt. generowania samego dźwięku.

Założenia

Odtwarzacz dźwięku przede wszystkim powinien mieć możliwość zmiany wysokości dźwięku, tak aby móc zasymulować różne obroty silnika. Do jednego z wejśc analogowych podłączony jest potencjometr, służący do regulowania "obrotów".

PC Speaker - level easy

Najprostszym sposobem wydobycia z Arduino dźwięku jest generowanie sygnału prostokątnego o zadanej częstotliwości za pomocą funkcji tone(pin, frequency). Funkcja ta na określonym w parametrze pinie generuje ton. Dźwięk silnika składający się ze zwykłej fali prostokątnej brzmi raczej jak za czasów pierwszych gier komputerowych. Postanowiłem zatem podczas generowania dźwięku modulować nieco wysokość dźwięku za pomocą kilku wartości odczytywanych w pętli.

unsigned char engineWave[] = {0,20,40,34,28,10,8,5};
int engineWaveIndex = 0;
unsigned long timeDelay = 0;


loop()
{
    int potValue = analogRead(pot);
    
    if( millis() - timeDelay > map(potValue, 0, 1023, 25, 6) )
    {
        timeDelay = millis();
        engineWaveIndex++;
        if( engineWaveIndex == 8)
         engineWaveIndex = 0;
    }
    tone(speaker, map(potValue, 0, 1023, 40, 250)+ engineWave[engineWaveIndex] );
}

Szybkość modulacji pilnowana jest przez zmienną timeDelay, która po upłynięciu pewnego czasu inkrementuje zmienną engineWaveIndex. Ta z kolei jest dodawana do aktualnej wartości częstotliwości dla funkcji tone(). Czas ten jest zmienny i zależy w tym przypadku od obrotów silnika, czyli wartości potencjometru potValue. Dzieki temu im wyższe obroty tym szybciej powtarzany jest wzorzec zmian częstotliwości podstawowej.
Poniżej efekt tegoż kodu.

Po chwili zabawy z tak generowanym dźwiękiem doszedłem jednak do wniosku, że skoro już strażą pożarną ma zarządzać mikrokontroler, to może zamiast prostego modulowanego tonu, który na dłuższą metę jest uciążliwy dla słuchacza, generować dźwięk silnika z próbki dźwiękowej w formacie PCM.

Odtwarzanie pliku wave - level medium

Poszperałem trochę w Internecie i znalazłem opis odtwarzania pliku wave za pomocą Arduino. Kod znajduje się tutaj: http://playground.arduino.cc/Code/PCMAudio
Jeśli posiadasz Arduino Mega, jak w moim przypadku, w kodzie należy zmienić wyjścia głośnika z 11 na 10. Wynika to z budowy mikrokontrolera i wyprowadzeniu wyjść rejestru OCR. Szkoda, że o tym nie wspomnieli, zaoszczędziłbym kilka godzin na poszukiwaniu problemu ;).
Domyślnie kod podany na stronie powyżej odtwarza dźwięk wave w formacie PCM 8 bit / 8 kHz. Format PCM jest to zapis kolejnych wartości sygnału (próbek), które w tym wypadku posiadają 256 poziomów amplitudy (8bit) oraz są próbkowane z częstotliwością 8kHz, czyli 8 tysięcy razy na sekundę. W porównaniu z wersją PC Speaker, która modulowała częstotliwość podstawową około 10 razy na sekundę wydaje się to wręcz oszałamiającą ilością danych do przetworzenia.
W praktyce, próbkowanie 8kHz pozwala odtworzyć częstotliwości nie wyższe niż połowa częstotliwości próbkowania, czyli 4kHz. Odpowiada to mniej więcej jakości sygnału telefonicznego. Wstępnie postanowiłem zatem zwiększyć częstotliwość próbkowania do 16kHz, dzięki czemu z głośnika wydobywają się znacznie ciekawsze walory silnika Diesla, którego akurat próbką się posłużyłem ;).

Odtwarzanie wave'ów korzysta z dwóch timerów mikrokontrolera AVR. Pojęcie timera może być obce dla początkujących użytkowników Arduino, gdyż są one przed nim ukryte. Przykładowo, funkcja tone(), jak również używana przez wielu funkcja analogWrite() generująca sygnał PWM oraz milis() korzystają pod spodem z timerów. W przypadku generowania sygnału PWM za pomocą analogWrite() timer ustawiany jest na częstotliwość około 400Hz (lub około 900Hz zależnie od timera). Częstotliwości te są zbyt niskie na potrzeby audio, stąd w kodzie znajduje się "ręczna" obsługa timerów.

Czym jest zatem timer, w skrócie. Jest to 8 lub 16 bitowy licznik, liczący każdy takt zegara mikrokontrolera. Timery/Liczniki mają dosyć rozbudowaną konfigurację, dzięki czemu można np. ustawić timer/licznik tak, aby po odliczeniu pewnej liczby taktów (czyli upłynięciu pewnego czasu, który da się wyliczyć ze wzorów) mikrokontroler przerwał to co akurat robi i zajął się obsługą tzw. przerwania od timera - czyli wykonał jakiś odrębny kawałek kodu. Można również zautomatyzować proces i zlecić hardware'ową obsługę przerwania, np polegającą na zmianie stanu wyjścia procka na przeciwny.
Nie bez powodu podałem akurat te dwa przykłady, gdyż stanowią one trzon odtwarzacza dźwięków. Hardware'owa zmiana stanu pinu po odliczeniu narzuconego przez nas czasu używana jest do generowania sygnału PWM (Pulse Width Modulation). Sygnał taki można dosyć łatwo zamienić w sygnał analogowy za pomocą najprostszego filtru dolnoprzepustowego RC (rezystor + kondensator). Zmieniając wartość rejestru OCR (Output Compare Register) ustalamy do ilu ma dany timer zliczać, a więc przez jaki czas jego pełnego cyklu na wyjściu mikrokontrolera będzie stan wysoki i niski. Nie ma sensu dokładniej tutaj tłumaczyc jak działa PWM, bo bez problemu można takie informacje znaleźć.
W przybliżeniu wystarczy wiedzieć, że stosunek czasu trwania na wyjściu 1 do 0 przekłada się na wygenerowane analogowe napięcie. W najprostszym przypadku można w ten sposób sterować jasnością diody LED. W naszym przypadku zdigitalizowana wartość napięcia sygnału audio z pliku PCM będzie podawana w postaci kolejnych próbek do rejestru OCR2A (czyli do kanału A timera 2 rejestru komparatora wyjścia). Dzięki temu analogowe napięcie wyjściowe będzie odpowiadać amplitudzie zapisanej w kolejnych próbkach pliku PCM.

Druga sprawa, to jak szybko podawać kolejne próbki? Tutaj z pomocą przychodzi drugi timer, który ustawiony jest tak, aby generować przerwanie z częstotliwością 8 kHz (w moim przypadku 16kHz), czyli co około 62µs. W funkcji tej wczytywana jest kolejna próbka i ustawiana dla poprzedniego timera, aby ten generował "napięcie" odpowiadające tej próbce.

Zmiana wysokości dźwięku

Najłatwiej efekt ten uzyskać zmieniając częstotliwość próbkowania odtwarzanego dźwięku. W tutorialu z playGrounda Arduino zdefiniowana jest stała o nazwie SAMPLE_RATE, która ładowana jest do rejestru OCR timera odpowiedzialnego za wczytywanie kolejnych próbek. Jest ona stała. Co jednak się stanie gdy będziemy tą wartość modyfikować? Próbki będą wczytywane wolniej lub szybciej niż powinny. Jeśli dźwięk o próbkowaniu 16kHz będzie próbkowany z częstotliwością wyższą, wtedy podniesie się jego ton, dokładnie tak jak się kiedyś słuchało muzykę na gramofonie czy magnetofonie, zwiększając szybkość przesuwu nośnika danych względem głowicy odczytującej.
No więc zmieniajmy tą częstotliwość za pomocą potencjometru. Jedna tylko uwaga! Częstotliwość tą należy zmieniać w procedurze obsługi przerwania, czyli w momencie wczytywania kolejnej próbki. W innym przypadku możemy się spotkać z przykrą niespodzianką w postaci zaników lub zniekształceń dźwięku. Wynika to ze specyfiki działania liczników i ustawiania w nich nowych wartości. Jeśli nowa wartość będzie mniejsza niż aktualny stan licznika, to komparator wartości wygeneruje przerwanie dopiero jak licznik zliczy do końca zakresu i zacznie liczyć od nowa.
Najpierw kod.

ISR(TIMER1_COMPA_vect)
{
    if( sample2 >= sound2_length )
        sample2=0;
    OCR2B = pgm_read_byte(&Sounds::Wave::EngineWorking[sample2]);
    ++sample2;
    OCR1A = F_CPU / SAMPLE_RATE-potValue;
}

Powyżej znajduje się nieco zmodyfikowana i okrojona z niepotrzebnych rzeczy procedura obsługi przerwania od timera1. Zmienna sample2 inkrementowana jest od 0 do sound2_length - czyli do długości danych PCM, po czym resetowana jest do 0. Dzieki temu uzyskujemy zapętlenie dźwięku. Do "rejestru PWM" OCR2B ładowana jest kolejna próbka dźwięku z tablicy Sounds::Wave::EngineWorking[]. Ten dziwny zapis z podwójnym dwukropkiem to tzw namespace'y. Pozwalają one pogrupować sobie kod, zmienne, stałe, co w przypadku korzystania z podpowiadania składni w edytorze kodu ułatwia pisanie programów.
Na końcu zmieniana jest wartość rejestru OCR1A poprzez odjęcie od domyślnej częstotliwości próbkowania wartości zczytanej z potencjometru (w głównej pętli programu).

Wg mnie jest już dobrze, oceńcie sami :).

Słychać jeszcze charakterystyczny efekt zapętlenia sampla silnika, ale o samym generowaniu sampli potem.

C.D.N.


POZOSTAŁE CZĘŚCI ARTYKUŁU


Comments (4)

  1. Nuwan:
    lip 06, 2015 at 07:15

    Hi,

    The second video has more realistic sound than the first one, is it the same Audio sample array or are you using a audio file loaded from a SD card or something?

  2. Bartek:
    lip 06, 2015 at 10:40

    First one is the simple synthesized sound with some different values in array, while the second one is PCM wave sample data played in loop. Data is stored in byte array.

  3. Nuwan:
    lip 07, 2015 at 03:48

    Ok got it, do you have the PCM audio of this and the complete source code?

  4. Bartek:
    lip 07, 2015 at 10:47

    I've used PCM player code available in arduino site with some modifications to change playback speed. I guess I've got source code and sample file.. somewhere ;). It was a while ago and the project is waiting now in a drawer for future publication.

  1. 1

Allowed tags: <b><i><br>


PODZIEL SIĘ

PODZIEL SIĘ


POZOSTAŁE CZĘŚCI


1. Arduino soundcard