Skocz do zawartości

Tester głośności na bazie M5 Core S3 SE


Pomocna odpowiedź

Napisano

Pomiar głośności kojarzy się zwykle ze specjalizowanym sonometrem, jednak do wielu amatorskich zastosowań wystarczy znacznie prostsze urządzenie. Nie zawsze potrzebujemy laboratoryjnej dokładności – czasami chcemy jedynie porównać poziom hałasu w kilku miejscach, sprawdzić różnicę między pracującymi urządzeniami albo po prostu zobaczyć, jak zmienia się natężenie dźwięku w czasie.

Z tego powodu przygotowałem prosty miernik poziomu dźwięku wykorzystujący gotowy moduł M5Stack CoreS3. Urządzenie korzysta z wbudowanego mikrofonu i wyświetlacza, więc cały projekt od strony sprzętowej sprowadza się właściwie do jednego gotowego modułu. Po uruchomieniu na ekranie prezentowany jest oszacowany poziom dźwięku w decybelach oraz proste widmo częstotliwości. Dostępne są również tryby prezentujące wartość uśrednioną oraz największą zarejestrowaną wartość. Projekt powstał we współpracy ze sklepem Botland, a kod projektu dostępny jest w repozytorium GitHub: DecibelMeter – GitHub.

IMG_1743.thumb.JPG.cc6559f82d075621c4131ca738ccb3e1.JPG

Wybór modułu

W projekcie wykorzystałem M5Stack CoreS3, czyli niewielki moduł bazujący na ESP32-S3, wyposażony między innymi we wbudowany ekran oraz mikrofon. W przypadku tego projektu takie połączenie jest szczególnie wygodne. Nie trzeba osobno dobierać wyświetlacza, mikrofonu, wzmacniacza ani wykonywać dodatkowych połączeń. Wystarczy zaprogramować jeden moduł i zasilić go przez USB.

Nie oznacza to jednak, że projekt musi być wykonany dokładnie na CoreS3. Przy wyborze innej płytki sugerowałbym przede wszystkim szukać modułu, który posiada:

  • mikrokontroler o wystarczającej wydajności do bieżącej analizy dźwięku,
  • wbudowany mikrofon cyfrowy, najlepiej komunikujący się przez I2S lub podobny interfejs,
  • wbudowany wyświetlacz.

Najwygodniejsze będą więc różnego rodzaju gotowe terminale i zestawy rozwojowe oparte na ESP32 lub ESP32-S3, w których mikrofon i ekran znajdują się już na jednej płytce.

Co pokazuje urządzenie?

Najbardziej widocznym elementem interfejsu jest aktualna wartość poziomu dźwięku. Pod nią znajduje się natomiast wykres pokazujący orientacyjny rozkład energii sygnału w zakresie częstotliwości akustycznych.

IMG_1742.thumb.JPG.ee97dc0f4b9480734ce9385d5a3bc103.JPG

Program analizuje zakres od około 20 Hz do 20 kHz i przedstawia go za pomocą 18 punktów rozmieszczonych logarytmicznie. Dzięki temu niskie częstotliwości nie zostają ściśnięte na niewielkiej części wykresu, jak miałoby to miejsce przy osi liniowej.

Przycisk zasilania CoreS3 służy również do przełączania trybu prezentacji wyniku. Firmware przewiduje trzy widoki:

  • bieżący poziom,
  • AVG – poziom uśredniony,
  • MAX – największy poziom zarejestrowany w ostatnich 60 sekundach.

Jak obliczana jest głośność?

Mikrofon nie przekazuje do programu gotowej informacji w rodzaju „aktualnie jest 65 dB”. Otrzymujemy jedynie kolejne próbki opisujące chwilową amplitudę sygnału. W projekcie mikrofon pracuje z częstotliwością 44,1 kHz, a program analizuje kolejne bloki po 512 próbek:

constexpr uint32_t microphone_sample_rate_hz = 44100u; 
constexpr size_t microphone_frame_samples = 512u;

Usunięcie przesunięcia sygnału

Pierwszym etapem jest obliczenie średniej wartości wszystkich próbek:

int64_t sum = 0; 
for (size_t index = 0u; index < sample_count; ++index) 
{ 
  // Sumujemy wszystkie próbki z aktualnego fragmentu sygnału. 
  sum += samples[index]; 
} 

// Wyznaczamy średnią wartość sygnału. 
const float mean = static_cast<float>(sum) / static_cast<float>(sample_count);

Mikrofon i jego tor wejściowy mogą powodować niewielkie przesunięcie poziomu zerowego. Nie jest ono częścią mierzonego dźwięku, dlatego przed dalszymi obliczeniami średnia jest odejmowana od każdej próbki.

W następnym kroku program oblicza energię sygnału:

float squared_sum = 0.0f; 
for (size_t index = 0u; index < sample_count; ++index) 
{ 
  // Usuwamy składową stałą. 
  const float centered_sample = static_cast<float>(samples[index]) - mean; 
  
  // Próbkę podnosimy do kwadratu. Dzięki temu wartości dodatnie i ujemne nie znoszą się wzajemnie. 
  squared_sum += centered_sample * centered_sample; 
} 

// Wynikiem jest średnia wartość kwadratu sygnału. 
return squared_sum / static_cast<float>(sample_count);

Pełna funkcja odpowiedzialna za ten etap w programie nazywa się frame_mean_square().

Podnoszenie wartości do kwadratu jest tutaj istotne. Sygnał audio oscyluje wokół zera, więc zwykła średnia dla nawet bardzo głośnego dźwięku byłaby często bliska zeru. Średnia kwadratów daje natomiast wartość odpowiadającą energii sygnału.

Stabilizacja wyniku

Każdy blok 512 próbek daje osobny wynik. Pojedyncze stuknięcie, zakłócenie lub bardzo krótki impuls mogłyby przez to powodować gwałtowne zmiany wskazania.

Dlatego program przechowuje ostatnie 11 wyników energii:

constexpr size_t frame_energy_window_entries = 11u;

Po zapełnieniu bufora wyniki są sortowane. Następnie pomijana jest najmniejsza i największa wartość:

float sum = 0.0f; 
for (size_t index = 1u; index < frame_energy_window.count - 1u; ++index) 
{ 
  // Pomijamy pierwszy i ostatni element, czyli najmniejszy i największy wynik. Wyniki były wcześniej przesortowane.
  sum += sorted_energy[index]; 
} 

// Uśredniamy pozostałe wartości. 
return sum / static_cast<float>( frame_energy_window.count - 2u);

Jest to prosty sposób ograniczenia wpływu pojedynczych wartości odstających. Wskazanie pozostaje dzięki temu znacznie spokojniejsze, ale nadal reaguje na rzeczywistą zmianę poziomu dźwięku.

Od energii do RMS

Otrzymana wcześniej wartość jest średnią kwadratów próbek. Aby uzyskać wartość skuteczną sygnału, czyli RMS, trzeba wyciągnąć z niej pierwiastek:

constexpr float minimum_energy = 1.0f; 

// RMS jest pierwiastkiem ze średniej wartości kwadratu sygnału. 
const float rms = sqrtf(fmaxf(mean_square, minimum_energy));

fmaxf() zabezpiecza obliczenia przed bardzo małymi wartościami. RMS można potraktować jako pojedynczą liczbę opisującą amplitudę całego analizowanego fragmentu dźwięku. Im większa wartość RMS, tym silniejszy sygnał z mikrofonu.

Przeliczenie RMS na decybele

Kolejnym krokiem jest przejście ze skali liniowej na logarytmiczną:

constexpr float microphone_full_scale = 32768.0f; 
constexpr float microphone_calibration_offset_db = 94.0f; 
const float decibels = 20.0f * log10f(rms / microphone_full_scale) + microphone_calibration_offset_db;

microphone_full_scale odpowiada pełnemu zakresowi 16-bitowej próbki mikrofonu.

Najważniejsza jest tutaj funkcja: log10f(...). To właśnie ona powoduje, że wynik przedstawiany jest w skali logarytmicznej.

Cała funkcja wygląda więc bardzo prosto:

float energy_to_decibels(const float mean_square) 
{ 
  constexpr float minimum_energy = 1.0f; 
  
  // Zamiana średniej energii na RMS. 
  const float rms = sqrtf(fmaxf(mean_square, minimum_energy)); 
  
  // Przejście ze skali liniowej na logarytmiczną oraz zastosowanie kalibracji. 
  const float decibels = 20.0f * log10f(rms / microphone_full_scale) + microphone_calibration_offset_db; 
  
  // Ograniczenie wskazania do zakresu obsługiwanego przez interfejs. 
  return clamp_level_db(decibels); 
}

Warto przy tym zaznaczyć, że mikrofon cyfrowy nie jest fabrycznie skalibrowanym miernikiem ciśnienia akustycznego. Dlatego w kodzie znajduje się dodatkowa stała:

constexpr float microphone_calibration_offset_db = 94.0f;

Jej wartość można zmienić po porównaniu urządzenia z referencyjnym miernikiem poziomu dźwięku.

Uśrednianie wskazania

W projekcie zapisywana jest historia nie samych wartości w decybelach, lecz energii sygnału:

struct level_history_t { 
  float energy[level_history_entries]; 
  size_t count; 
  size_t next_index; 
};

Przy wyznaczaniu wartości średniej program najpierw sumuje energię:

float sum = 0.0f; 
for (size_t index = 0u; index < sample_count; ++index) 
{ 
  const size_t offset = (level_history.next_index + level_history_entries - 1u - index) % level_history_entries; 
  sum += level_history.energy[offset]; 
}

Dopiero średnia energia jest następnie przeliczana na decybele:

return energy_to_decibels( sum / static_cast<float>(sample_count));

Jest to ważny szczegół. Ponieważ skala dB jest logarytmiczna, bezpośrednie uśrednianie liczb wyświetlanych na ekranie nie odpowiadałoby uśrednianiu energii dźwięku.

IMG_1741.thumb.JPG.8bc935e0ffa03fca7d14375d8b7c6a5e.JPG

Analiza częstotliwości

Oprócz samego poziomu dźwięku urządzenie pokazuje również uproszczone widmo. Zamiast wykonywać pełną transformatę FFT program wykorzystuje algorytm Goertzela. Dobrze nadaje się on do sytuacji, w której interesuje nas tylko niewielka liczba konkretnych częstotliwości.

Dla pojedynczej częstotliwości obliczenie wygląda następująco:

float goertzel_power( const int16_t* samples, const size_t sample_count, const float frequency_hz) 
{ 
  // Częstotliwość, której szukamy, zamieniamy na pulsację cyfrową. 
  const float angular_frequency = 2.0f * PI * frequency_hz / static_cast<float>( microphone_sample_rate_hz); 
  const float coefficient = 2.0f * cosf(angular_frequency); 
  float previous = 0.0f; 
  float previous_previous = 0.0f; 
  
  for (size_t index = 0u; index < sample_count; ++index) 
  { 
    // Filtr Goertzela analizuje, jak silnie dana częstotliwość występuje w badanym sygnale. 
    const float current = static_cast<float>(samples[index]) + coefficient * previous - previous_previous; 
    previous_previous = previous; 
    previous = current; 
  } 
  
  // Wynikiem jest wartość proporcjonalna do energii dla wybranej częstotliwości. 
  return previous_previous * previous_previous + previous * previous - coefficient * previous * previous_previous; 
}

Nie potrzebujemy jednak tylko jednej częstotliwości. Ekran pokazuje 18 pasm:

constexpr size_t spectrum_band_count = 18u;

Zakres widma wynosi od około 20 Hz do 20 kHz:

constexpr float lowest_frequency_hz = 20.0f; constexpr float highest_frequency_hz = 20000.0f;

Poszczególne punkty nie są rozmieszczone liniowo. Program wykorzystuje skalę logarytmiczną:

const float frequency_ratio = highest_frequency_hz / lowest_frequency_hz; 
for (size_t index = 0u; index < spectrum_band_count; ++index) 
{ 
  // Pozycja aktualnego pasma od 0 do 1. 
  const float position = static_cast<float>(index) / static_cast<float>( spectrum_band_count - 1u); 
  
  // Wyznaczenie częstotliwości rozmieszczonej logarytmicznie pomiędzy 20 Hz i 20 kHz. 
  const float frequency_hz = lowest_frequency_hz * powf(frequency_ratio, position); 
  
  // Energia dla tej częstotliwości. 
  const float power = goertzel_power( samples, sample_count, frequency_hz); 
  
  // Logarytmiczne skalowanie wyniku do zakresu wygodnego dla wykresu. 
  const float normalized = log10f(power + 1.0f) / 10.5f; 
  levels[index] = fminf( 1.0f, fmaxf(0.0f, normalized)); 
}

Takie rozmieszczenie jest znacznie bardziej naturalne dla dźwięku. Gdyby punkty były rozłożone liniowo od 20 Hz do 20 kHz, zdecydowana większość niskich częstotliwości znalazłaby się na bardzo małej części wykresu.

Dzięki temu ekran pokazuje jednocześnie dwie różne informacje:

  • wartość w dB – jak silny jest dźwięk,
  • widmo – z jakich częstotliwości mniej więcej się składa.

Kalibracja

Najważniejszym ograniczeniem projektu jest mikrofon.

Dwa egzemplarze nawet tego samego modułu mogą zwracać trochę inne wartości. Dochodzą do tego różnice wynikające z konstrukcji obudowy, położenia otworu mikrofonu oraz jego charakterystyki częstotliwościowej.

Dlatego w kodzie znajduje się stała: microphone_calibration_offset_db

W momencie, gdy dysponujemy wiarygodnym miernikiem poziomu dźwięku, możemy ustawić oba urządzenia obok siebie i użyć możliwie stabilnego źródła dźwięku. Jeżeli CoreS3 pokazuje przykładowo 68 dB, a urządzenie wzorcowe 72 dB, współczynnik należy zwiększyć o około 4 dB. Zaś w przypadku, gdy wynik jest zawyżony – współczynnik należy odpowiednio zmniejszyć.

Najlepiej wykonać kilka porównań przy różnych poziomach głośności. Jednopunktowa kalibracja poprawia zgodność wskazań, ale nie zmienia charakterystyki samego mikrofonu.

Czy jest to prawdziwy sonometr?

Nie do końca. Profesjonalny miernik poziomu dźwięku jest urządzeniem pomiarowym o określonej charakterystyce mikrofonu, tolerancji, dynamice oraz filtrach częstotliwościowych i czasowych. Prezentowany projekt nie implementuje między innymi standaryzowanego ważenia częstotliwościowego A lub C, dlatego oznaczenia wyświetlane przez urządzenie należy traktować jako oszacowanie dB SPL po kalibracji, a nie jako pomiar nadający się przykładowo do oficjalnej oceny poziomu hałasu w miejscu pracy.

Nie przeszkadza to jednak w wielu zastosowaniach amatorskich. Urządzenie bardzo dobrze pokazuje różnice względne, na przykład:

  • jak zmienia się hałas po włączeniu wentylatora,
  • który z dwóch mechanizmów pracuje ciszej,
  • jak skuteczna jest wykonana obudowa tłumiąca,
  • jak zmienia się hałas wraz z odległością od źródła,
  • które częstotliwości dominują w danym dźwięku.

W takich zastosowaniach nawet prosty miernik okazuje się całkiem użytecznym narzędziem.

Kompilacja programu

Projekt przygotowany został z wykorzystaniem PlatformIO oraz biblioteki M5Unified.

Po pobraniu repozytorium program można skompilować poleceniem:

pio run

a następnie wgrać do modułu:

pio run --target upload

Po uruchomieniu urządzenie automatycznie inicjalizuje wbudowany mikrofon oraz ekran i rozpoczyna pomiar.

Nie jest wymagana dodatkowa aplikacja na komputerze – po zaprogramowaniu miernik działa całkowicie samodzielnie.

Możliwe modyfikacje

Projekt można potraktować również jako punkt wyjścia do bardziej rozbudowanego urządzenia.

Jedną z pierwszych modyfikacji mogłoby być dodanie ważenia częstotliwościowego A, dzięki któremu charakterystyka pomiaru byłaby bardziej zbliżona do sposobu, w jaki człowiek odbiera różne częstotliwości.

Inne możliwe rozszerzenia to:

  • wykres poziomu hałasu w funkcji czasu,
  • zapisywanie minimalnej i maksymalnej wartości,
  • transmisja wyników przez Wi-Fi lub Bluetooth,
  • możliwość wyboru różnych charakterystyk czasowych,
  • zwiększenie liczby analizowanych pasm częstotliwości,
  • przesyłanie wyników do komputera lub systemu automatyki domowej.

Ciekawym eksperymentem byłoby również porównanie wbudowanego mikrofonu z zewnętrznym mikrofonem I2S o znanej charakterystyce.

Podsumowanie

Projekt pokazuje, że do zbudowania prostego miernika poziomu dźwięku nie jest potrzebna rozbudowana elektronika. Moduł posiadający mikrofon, wyświetlacz i odpowiednio wydajny mikrokontroler wystarcza, aby przygotować całkowicie samodzielne urządzenie. Najciekawsza część projektu znajduje się tak naprawdę w oprogramowaniu. Surowe próbki z mikrofonu są przetwarzane na energię i wartość RMS, następnie zamieniane na skalę logarytmiczną wyrażoną w decybelach. Równolegle algorytm Goertzela analizuje wybrane częstotliwości, dzięki czemu urządzenie może wyświetlać również proste widmo.

Nie jest to zamiennik profesjonalnego sonometru, ale jako niewielki projekt DIY dobrze pokazuje kilka ciekawych zagadnień naraz: obsługę mikrofonu cyfrowego, próbkowanie sygnału, RMS, skalę decybelową, analizę częstotliwości oraz przetwarzanie danych w czasie rzeczywistym.

  • Lubię! 1

Podoba Ci się ten projekt? Zostaw pozytywny komentarz i daj znać autorowi, że zbudował coś fajnego!

Masz uwagi? Napisz kulturalnie co warto zmienić. Doceń pracę autora nad konstrukcją oraz opisem.

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...