Już od stuleci gatunek ludzki zastanawiał się nad takimi zagadnieniami jak natężenie dźwięku, autokorelacja sygnału oraz szybka transformacja Fouriera… Hmm, kłamstwo!

Ten jakże gładki i uniwersalny (ileż publikacji czy artykułów wszak się od niego zaczyna) wstęp jest wybitnie nieprawdziwy w przypadku akustyki i tematyki ochrony słuchu. Ba! Standardowe podejście do naszych uszu przedstawia się następująco: „nie boli – nic się nie dzieje”. Udowodnienie jury AppCampa, iż jest to podejście mocno niewłaściwe, zdaje się było kluczem do mojego uczestnictwa w finale tego konkursu. Ale wszystko po kolei…

Gdy przystępuje się do takiego konkursu niewątpliwie najważniejszy jest pomysł. Proces jego wynajdywania zawsze przedstawiany jest jako boska iskra, nagłe spięcie szarych komórek i ot – pomysł. Niestety w moim przypadku tak nie było.

Znana była tematyka konkursu – aplikacje medyczne i zdrowotne, wiadomym było też to, że pomysł powinien być oryginalny, tak więc wybierając nieco bardziej pragmatyczne podejście, zawężyłem dziedzinę potencjalnych pomysłów do kilku rozwiązań. Aplikacja miała być medyczna – znaczy miała coś badać. Raczej niewiele informacji możemy uzyskać poprzez kliknięcia użytkownika – sam słabo się zdiagnozuje, pozostają inne metody uzyskiwania przez urządzenie z iOSem informacji – kamerka oraz mikrofon. Pomimo zawężenia dziedziny nadal kryzys twórczy doskwierał, wymyślanie niebanalnej aplikacji to rzecz trudna. Z pomocą przyszła mi moja dziewczyna, która podsunęła myśl, by zbadać hałas nie tylko pod względem natężenia (takich aplikacji są tysiące), ale także przeanalizować jego charakter.

 Do dzieła!

W iOSie do dyspozycji programisty oddano całkiem przyjemny w użyciu framework AVFoundation. Jako punkt startowy developmentu swojej aplikacji przyjąłem rejestrację dźwięku oraz pomiar jego natężenia. To zadanie okazuje się proste w realizacji:

Et voilà. Teraz wystarczy tylko poczekać na koniec nagrywania i jeżeli ustawiliśmy delegatę dla recordera, to dalszą obróbkę dźwięku możemy przeprowadzić w metodzie audioRecorderDidFinishRecording:successfully:Pomiar poziomu natężenia dźwięku jest równie trywialny. Po ustawieniu propercji recorder.meteringEnabled na YES:

Uzyskany wynik należy oczywiście skorygować, gdyż leży w zakresie od -160 do 0dB, gdzie -160dB to najcichszy dźwięk „słyszany” przez urządzenie. Wymaga to pewnej kalibracji, ale na szczęście charakterystyka częstotliwościowa mikrofonu w iPhonach jest w przybliżeniu taka sama jak filtry stosowane w miernikach dźwięku – do podstawowych zastosowań zdecydowanie się nada.

Mając dostęp do chwilowego poziomu natężenia dźwięku oraz próbki o zadanej długości, mogłem zająć się opracowywaniem przebiegu analizy. Przebieg czasowy rejestrowany jest w sposób trywialny – przez cały czas nagrywania próbki za pomocą obiektu klasy NSTimer cyklicznie wywołuję averagePowerForChannel: i zapisuję te dane w tablicy.

Niestety dalsze postępowanie z plikiem audio nie jest już tak przyjemne jak jego podstawowa obróbka. Każdemu kto zajmuje się DSP (Digital Signal Processing) na urządzeniach z iOSem, zapewne śnią się po nocach zawiłości napisanego w C frameworku Accelerate. Oczywiście StackOverflow aż kipi pytaniami w rodzaju „jak obliczyć FFT sygnału w Accelerate”, ale dużo z odpowiedzi zawiera błędy (drobne, ale istotne dla precyzji uzyskanych wyników). Dużym problemem jest też znaleźć wskazówki dotyczące tego w jaki sposób czytać plik audio aby można było poddać go analizie częstotliwościowej.

Następnie w pętli czytamy plik aż do końca:

Gdzie offset to numer bajtu, od którego zaczynamy czytanie, toRead to ilość bajtów do przeczytania przekazywana jako referencja ze względu na to, że funkcja umieszcza w tym parametrze następnie ilość faktycznie odczytanych bajtów, a audioBuffer to nasz bufor zawierający próbki audio.

Mając próbki możemy w końcu przeprowadzić upragnione FFT. Czym jest FFT? Jest to odmiana zmyślnego aparatu matematycznego zwanego transformacją Fouriera. W jej wyniku uzyskujemy amplitudy sygnałów o poszczególnej częstotliwości w próbce, czyli „głośność” dźwięków o określonej wysokości (np. dźwięk A ma częstotliwość 440Hz). W wydaniu matematycznym przekształcenie to nie prezentuje się atrakcyjnie, szczególnie dla tych, których symbol całki przyprawia o ciarki. W wydaniu Accelerate wygląda ono następująco:

Tutaj przygotowujemy elementy niezbędne do przeprowadzenia FFT w tym frameworku. log2N to logarytm o podstawie 2 z maksymalnej liczby próbek na jakiej będzie przeprowadzana transformacja z pomocą tego setupu (fun fact: sporo nerwów i debuggingu kosztował mnie fakt, że 32 to w żadnym razie nie 26).

Kod wygląda na dość skomplikowany, ale gdy się go raz zrozumie… i tak jest skomplikowany. DSPSplitComplex to typ służący do przechowywania liczb zespolonych. Ale zaraz! Przecież mamy sygnał rzeczywisty, skąd tu naraz zespolone liczby? Otóż framework Accelerate stawia głównie na szybkość i wydajność kodu, stąd miejscami dziwne metody przechowywania danych. W zamian uzyskujemy nieprawdopodobną szybkość (wynik FFT uzyskujemy w czasie na poziomie mikrosekund), a także uniwersalność – w końcu gdybyśmy chcieli operować na sygnale zespolonym to możemy użyć tych samych struktur i funkcji. nOver2 to tylko i wyłącznie pomocnicza zmienna zawierająca liczbę próbek podzieloną przez 2. funkcja vDSP_ctoz pozwala nam na upakowanie naszych danych w strukturę DSPSplitComplex, windowBuffer – to ciągle ta sama próbka audio, tym razem już po operacji okienkowania (dla zgłębienia tematu polecam literaturę z zakresu DSP, a także dokumentację frameworku Accelerate), liczby występujące tu i ówdzie w owych funkcjach to tzw. stride czyli parametr określający co ile próbek „przeskakujemy” we wszelkich operacjach. Gdy już przygotowaliśmy dane, nie pozostaje nam nic innego jak przeprowadzenie samej transformacji z pomocą funkcji vDSP_fft_zrip (końcówka nazwy „-ip” oznacza, że wykonuje ona się „w miejscu” – wejście jest nadpisywane wyjściem i końcowy wynik mamy w tablicji fftBuffer. Szybkość ponad wszystko!). Format w jakim zapisany wynik jest również zaskakujący – polecam dokumentację!

W celach minimalizacji czasu analizy, część obliczeń w NoiseChecku opiera się na stabelaryzowanych wartościach dość skomplikowanych funkcji (akustyka to dziedzina lubująca się w gigantycznych wzorach). Bardzo ciekawym konceptem ułatwiającym ten typ pracy jest zastosowanie interpretera PHP jako preprocessora kodu objC (polecam post Gynvaela Coldwinda na ten temat).

Uff! C za nami, ale do wizualizacji wyników naszej pracy ciągle daleko. W czym problem? Otóż w iOSie nie ma natywnej biblioteki, która ułatwiałaby tworzenie ładnych, customizowalnych wykresów. Wspólnie z grafikiem (wielkie propsy dla Krzysztofa Janeczki za ogarnięcie tego zagadnienia) stworzyliśmy własny system wyświetlania wykresów (zainspirowany wrapperem do HighChartsów napisanym przez Tomasza Janeczkę). Opiera się on na jQuery oraz jqPlocie. Wyświetlamy komponent WebView z załadowanym pewnym templatem zawierającym podlinkowane biblioteki, utworzone zmienne odpowiedzialne za stylowanie oraz kontener wykresu – odpowiedni element <div>. Następnie w metodzie webViewDidFinishLoad: przekształcamy dane uzyskane z analizy na zapisaną w NSStringu tablicę JavaScript, opakowujemy to w odpowiednie dla jqPlota funkcje i uruchamiamy „na” WebView przy pomocy metody stringByEvaluatingJavaScriptFromString:.

O NoiseChecku mógłbym napisać jeszcze sporo, ale wydaje się, że w tym króciutkim poście zawarłem kwintesencję jego działania i najciekawsze zastosowane w nim koncepty. Był to projekt, który sporo mnie nauczył i dzięki któremu przeszedłem wszystkie stadia nienawiści i uwielbienia dla języka Objective-C. Mam nadzieję, że choć trochę przybliżyłem czytelnikom tematykę DSP na urządzeniach Apple’a. Zachęcam do eksperymentów!

Aplikację można pobrać bezpłatnie tutaj.