Skocz do zawartości

Dekodowanie MP3 (Raspberry Pi Pico)


atlantis86

Pomocna odpowiedź

Szukałem ostatnio jakiegoś sposobu programowego dekodowania MP3 na mikrokontrolerze (w tym przypadku konkretnie Raspberry Pi Pico). Parę dni temu na forum polecono mi bibliotekę libmad. Udało mi się ją bez większego programu skompilować i przystąpiłem do jej uruchamiania.

Działanie biblioteki opiera się na callbackach - przystępując do dekodowania audio trzeba zdefiniować kilka funkcji, które będą wywoływane w określonych sytuacjach (odczyt porcji danych, zgłoszenie błędu, zapis zdekodowanych danych do bufora DAC ). Każda funkcja zwraca wartość determinującą dalsze postępowanie - najważniejsze z nich to kontynuowanie lub zakończenie procesu dekodowania.

Niestety, ku mojemu rozczarowaniu odkryłem, że biblioteka nie posiada możliwości pracy w sposób nieblokujący. Nie można np. wywołać jej akurat wtedy, gdy w buforze kończą się dane, poprosić o zdekodowanie jednej ramki i powrót do pętli głównej i zaczęcie gdzie się skończyło po następnym wywołaniu. Raz wywołane dekodowanie będzie leciało do przodu. Jeśli spróbuję przerwać ten proces np. po zdekodowaniu jednej ramki, tracony jest kontekst - nie mam możliwości rozpoczęcia od tego miejsca w buforze, na którym się znajdowałem.

Przykłady na które trafiłem w Internecie stosują dwa podejścia:

  1. W przypadku komputerów PC cały plik jest od razu konwertowany do PCM. Tak to wygląda chociażby tu: Link. Nie stanowi to problemu, gdy mamy system operacyjny z multitaskingiem i dużo pamięci.
  2. Blokujące wykonywanie kodu, na przykład ten kod dla PIC32: Link. Tutaj MCU podczas odtwarzania nie zajmuje się niczym innym. Program czeka w pętli na zwolnienie się bufora DAC wewnątrz funkcji odpowiedzialnej za przetwarzanie zdekodowanych danych.

Żadna z tych opcji nie jest zbyt dobra. Pewnie trzecim wyjściem byłoby zastosowanie FreeRTOS-a i przeznaczenie osobnego tasku na odtwarzanie, ale z tym nie miałem jeszcze do czynienia.

Czy ktoś z was zna może jakąś bibliotekę do dekodowania audio, która byłaby przyjazna dla mikrokontrolerów? Nie musi to być biblioteka do MP3, chociaż preferowałbym jakiś popularny format, który będę mógł wygenerować popularnym narzędziem w stylu ffmpeg.

Ewentualnie... Znajdę gdzieś jakiś prosty tutorial, który wytłumaczyłby mi jak podpiąć FreeRTOS do RasPi Pico? 

Link do komentarza
Share on other sites

8 godzin temu, atlantis86 napisał:

Pewnie trzecim wyjściem byłoby zastosowanie FreeRTOS-a i przeznaczenie osobnego tasku na odtwarzanie, ale z tym nie miałem jeszcze do czynienia.

Ale do tego wcale nie jest potrzebny FreeRTOS - masz drugi rdzeń i na nim możesz odpalić odtwarzanie dźwięku, a funkcje multicore masz w standardowych bibliotekach Pico, w dodatku bardzo porządnie opisane w dokumentacji.

Przy odtwarzaniu na jednym rdzeniu też nie musisz bezczynnie czekać - możesz coś robić i okresowo sprawdzać czy bufor jest już gotowy do przyjęcia następnych danych. Ogólnie - przeanalizowanie sobie jak to działa na ESP8266 (bez wątków, z jednym rdzeniem, wolniejszym zegarkiem i mniejszą ilością pamięci) może być pouczające... przynajmniej w moim przypadku było.

Co do FreeRTOS-a dla Pico - jeśli wierzyć forumowym wiadomościom coś takiego istnieje, nie sprawdzałem. Ale nie jestem przekonany, czy dodatkowy narzut na schedulera jest w tym przypadku najlepszym rozwiązaniem.

Link do komentarza
Share on other sites

(edytowany)
1 godzinę temu, ethanak napisał:

Ale do tego wcale nie jest potrzebny FreeRTOS - masz drugi rdzeń i na nim możesz odpalić odtwarzanie dźwięku, a funkcje multicore masz w standardowych bibliotekach Pico, w dodatku bardzo porządnie opisane w dokumentacji.

O tym też myślałem, ale na razie szukam bardziej uniwersalnego rozwiązania. Zakładam, że w przyszłości będę chciał wykorzystać tę samą bibliotekę także na innych układach, z jednym rdzeniem (PIC32, STM32).

Cytat

Przy odtwarzaniu na jednym rdzeniu też nie musisz bezczynnie czekać - możesz coś robić i okresowo sprawdzać czy bufor jest już gotowy do przyjęcia następnych danych.

Właśnie nie bardzo mogę. To znaczy mogę, ale kosztem użycia bardzo brzydkiego kodu.

W swoich dotychczasowych projektach stosuję rozwiązanie, które wygląda następująco:

int main (void) {
  while(1) {
    uart_rx_hndle();
    network_handle();
    log_data_to_file();
  }
  return 0;
}

Każda z takich funkcji wygląda następująco:

void func_handle (void) {
  static uint32_t timer = 0;
  
  if( (uint32_t)(millis()-timer) > 5000 ) {
    timer = millis();
    //do something here
  }
}

W ten sposób każda operacja jest podzielona na kawałki. Czasem bardziej skomplikowane operacje są wykonywane przez maszynę stanów zrealizowaną na zmiennej enum i instrukcji switch/case/default. W ten sposób w każdym przebiegu pętli głównej wykonywany jest kawałek każdej z tych operacji (jeśli w danej chwili jest jakaś akcja do wykonania).

Biblioteka libmad nie jest kompatybilna z takim rozwiązaniem, bo ona chce od razu dekodować cały plik/strumień danych. Jeśli zdekoduje się jedna ramka, od razu analizowana jest dalsza część bufora. Jeśli w buforze brakuje danych - od razu wywoływana jest funkcja odpowiedzialna za ich pobieranie. Z tego co widzę "bezpiecznie" mogę zakończyć ten proces dopiero po przetworzeniu całego strumienia. Jeśli przerwę go wcześniej, tracony jest kontekst i wznowienie operacji za kolejnym wywołanie jest albo niemożliwe, albo mocno problematyczne.

Najbliższą do takiego rozwiązanie rzeczą jaką udało mi się osiągnąć jest stosowanie zewnętrznego bufera, którego zawartość byłaby po kolei analizowana. Przy każdym obiegu przepisuję resztę danych na początek i uzupełniam wolne miejsce świeżymi danymi z pliku. Jakoś to działa, ale i tak bibliotece się takie rozwiązanie nie podoba, bo przy każdym kolejnym wywołaniu rzuca co najmniej jednym błędem dekodowania. Błędów tych nie ma, jeśli po prostu pozwalam jej działać cały czas.

Mógłbym to zrobić jeszcze inaczej - powtarzając wywołania funkcji odpowiedzialnych za inne zadania wewnątrz pętli oczekującej na zwolnienie się miejsca w buforze, wewnątrz funkcji obsługującej zdarzenie "output" libmad. To byłby jednak wyjątkowo brudny hack, którego wolałbym uniknąć...

Cytat

Ogólnie - przeanalizowanie sobie jak to działa na ESP8266 (bez wątków, z jednym rdzeniem, wolniejszym zegarkiem i mniejszą ilością pamięci) może być pouczające... przynajmniej w moim przypadku było.

Oj, nie wydaje mi się, żeby to było zrobione na ESP8266 bez pomocy wątków. Ten układ można programować za pomocą dwóch różnych SDK:

  • NONOS - właściwie już wycofywany. Mocno specyficzny. Programista musi tam samemu zadbać o takie rozbicie zadań, aby te zdążyły się wykonać w odpowiednim czasie. W przeciwnym razie zadziała WDT i zresetuje układ. Nie można zbyt długo zatrzymywać procesora na wykonywaniu jakiegoś zadania, bo wtedy brakłoby cykli, żeby na czas wykonać zadania stosu WiFi lub TCP/IP. W tym SDK można zdefiniować kilka wątków, ale z uwagi na brak schedulera trzeba samemu zadbać o wyjście z nich na czas. Jest to podejście wybitne niezgodne z libmad i naprawdę nie wyobrażam sobie korzystania za jego pomocą z tej biblioteki.
  • RTOS/IDF - tak naprawdę typowy FreeRTOS. Właśnie na tej bibliotece opiera się Arduino na ESP - kod Arduino jest po prostu odpalany jako task we FreeRTOS-ie. Podejrzewam, że właśnie z tego rozwiązania skorzystano portując libmad na ESP8266.
Cytat

Co do FreeRTOS-a dla Pico - jeśli wierzyć forumowym wiadomościom coś takiego istnieje, nie sprawdzałem.

Ok, udało mi się skompilować i uruchomić FreeRTOS na Pico. Współpracy z libmad jeszcze nie testowałem.

Cytat

Ale nie jestem przekonany, czy dodatkowy narzut na schedulera jest w tym przypadku najlepszym rozwiązaniem.

Nie bardzo wydaje mi się, żebym miał inne wyjście. Tej biblioteki nie da się obsłużyć za pomocą sekwencji wywołań w pętli głównej. Ewidentnie była pisana z myślą o odpalaniu w środowisku wielozadaniowym i w związku z tym jej odpalenie bezpośrednio na krzemie jest bardzo problematyczne.

Najwyżej potraktuję to jako okazję, żeby w końcu zapoznać się z FreeRTOS i wykorzystać go w którymś ze swoich projektów. 😉

Edytowano przez atlantis86
Link do komentarza
Share on other sites

2 minuty temu, atlantis86 napisał:

Oj, nie wydaje mi się, żeby to było zrobione na ESP8266 bez pomocy wątków

Oj, czytałeś kod? Oj, bo ja czytałem. I oj, żadnych wątków tam nie znalazłem. Oj, ani w oryginalnym kodzie espressifa, ani oj, w kodzie bibliotek Arduino. I oj, jakoś dało się to uruchomić i wykorzystać...

Co do bajek o Misiach Wydajach... sam wiesz.

A to, że Twoje rozwiązanie nie jest z czymś tam kompatybilne - to znak, że trzeba pomyśleć o zmianie rozwiązania. No bo...

19 minut temu, atlantis86 napisał:

szukam bardziej uniwersalnego rozwiązania

Uniwersalne mają to do siebie, że nie wykorzystują tego co potrafi konkretny chip - czyli nigdy nie będą działać optymalnie. No - chyba że uniwersalność polega na bandzie #ifdefów i połowie kodu różnej dla różnych procesorów.

  • Lubię! 1
Link do komentarza
Share on other sites

Zarejestruj się lub zaloguj, aby ukryć tę reklamę.
Zarejestruj się lub zaloguj, aby ukryć tę reklamę.

jlcpcb.jpg

jlcpcb.jpg

Produkcja i montaż PCB - wybierz sprawdzone PCBWay!
   • Darmowe płytki dla studentów i projektów non-profit
   • Tylko 5$ za 10 prototypów PCB w 24 godziny
   • Usługa projektowania PCB na zlecenie
   • Montaż PCB od 30$ + bezpłatna dostawa i szablony
   • Darmowe narzędzie do podglądu plików Gerber
Zobacz również » Film z fabryki PCBWay

(edytowany)
9 godzin temu, ethanak napisał:

Oj, czytałeś kod? Oj, bo ja czytałem. I oj, żadnych wątków tam nie znalazłem. Oj, ani w oryginalnym kodzie espressifa, ani oj, w kodzie bibliotek Arduino. I oj, jakoś dało się to uruchomić i wykorzystać...

Ok, już chyba wiem w czym rzecz. Spojrzałem na dokumentację i faktycznie masz rację - Arduino Core dla ESP8266 nadal korzysta z SDK NONOS.

Rzuciłem też okiem na port libmad dla ESP8266. Kod wykorzystuje low-level API, w które nawet jeszcze nie zacząłem się wgryzać. Wygląda na to, że za jego pomocą faktycznie można stworzyć kod sekwencyjny, ale kosztem napisania od podstaw swoich własnych funkcji odpowiedzialnych za pobieranie i zwracanie danych. Najwyraźniej właśnie to zrobili twórcy biblioteki Arduino, która poza oryginalnymi plikami libmad posiada też sporo nowego kodu.

Może faktycznie rzucę na to jeszcze okiem. Główne API w prosty sposób nie pozwala jednak na dekodowanie w sposób sekwencyjny wewnątrz pętli głównej.

No i oczywiście nie jest wykluczone, że finalni spróbuję odpalić dekodowane MP3 na drugim rdzeniu, jeśli w przypadku zastosowania FreeRTOS okaże się, że zaczyna mi brakować zasobów. Po prostu dochodzę do wniosku, że to dobra okazja, żeby w końcu trochę poeksperymentować z tym systemem. A co by nie mówić - Pico ma dość sporo zasobów jak na mikrokontroler, jeśli chodzi o flash i RAM.

Edytowano przez atlantis86
Link do komentarza
Share on other sites

Bądź aktywny - zaloguj się lub utwórz konto!

Tylko zarejestrowani użytkownicy mogą komentować zawartość tej strony

Utwórz konto w ~20 sekund!

Zarejestruj nowe konto, to proste!

Zarejestruj się »

Zaloguj się

Posiadasz własne konto? Użyj go!

Zaloguj się »
×
×
  • Utwórz nowe...

Ważne informacje

Ta strona używa ciasteczek (cookies), dzięki którym może działać lepiej. Więcej na ten temat znajdziesz w Polityce Prywatności.