Pora na opis rdzenia oprogramowania Waleta a więc użycie narzędzi Arduino Audio Tools. Na początku opisów podałem link do repozytorium tego narzędzia i zainteresowanych kolegów zachęcam do studiowania tego ogromu wiedzy na temat przetwarzania sygnałów audio. Ja uszczknąłem z tego "tortu" ledwie okruszek ale i tak dało mi to możliwość rozwiązania podstawowych potrzeb programowych choć liczę, że doczytam się także brakujących mi jeszcze informacji. Podjąłem nawet dyskusję z autorem (Phil Shatzmann) na potrzebny mi temat ale różnica w wiedzy na temat omawianych spraw była tak duża, że chyba nie potrafiłem nawet nazwać dobrze swojego problemu więc i nie doczekałem się satysfakcjonującej odpowiedzi (na razie).
Niemniej, w liniach 106-115 (każdorazowo odwołuję się do pierwszej wersji programu dostępnego na Github) następują podstawowe definicje przepływu informacji w Audio Tools. Instrukcje zakomentowane oczywiście się "nie liczą" ale pokazują ścieżki po jakich chodziłem i na razie takich elementów nie usuwałem. Podobnie jak nie usuwałem z kodu zakomentowanych alternatywnych rozwiązań, choćby na temat uzyskania przesunięcia fazy o 90 stopni wprost w si5351.
W liniach 345-353 znajdują się definicje łańcuchów filtrów cyfrowych jakich na starcie używa program a w komentarzach są alternatywne definicje używane dla innych trybów pracy.
Linie 355-376 definiują zarówno kodek jak i porty A/D i D/A oraz uruchamiają część odpowiedzialną za cyfrowe przetwarzanie audio.
Linie 752-777 to właściwa definicja przetwarzania audio, w szczególności polecenie copier.copy() to kwintesencja narzędzi Audio Tools i ono właśnie zawiaduje właściwym przepływem danych cyfrowych w porządku określonym zarówno w liniach 106-115 jak i w definicjach łańcuchów filtrów (345-353).
Jednocześnie linie 752-777 to oddzielne zadanie dla drugiego rdzenia procesora zajmujące się wyłącznie obsługą audio o czym napiszę dalej.
Czas więc na początek opisać szczegółowo jak prawidłowo zdefiniować filtry cyfrowe dla systemu Audio Tools. Aby je narzędzia przetwarzania zrozumiały muszą mieć właściwy format.
Krótko o filtrach cyfrowych. Zasadniczo wyróżnia się dwa rodzaje:
FIR (Finite Impulse Response): Posiadają skończoną odpowiedź impulsową, są zawsze stabilne i pozwalają uzyskać liniową charakterystykę fazową. Do obliczeń sygnału wyjściowego używa się zawsze ustabilizowanych danych "z przeszłości". Takim filtrem jest użyty w kodzie Waleta filtr Hilberta.
[Obrazek: http://lx-net.pl/walet/image/fir.jpg]
IIR (Infinite Impulse Response): Mają nieskończoną odpowiedź impulsową dzięki sprzężeniu zwrotnemu (rekurencji); są bardziej wydajne obliczeniowo, lecz mogą być niestabilne. W kodzie Waleta ten rodzaj filtrów służy do uzyskania charakterystyk typu LPF lub BPF. Filtry IIR korzystają za danych z "przeszłości" ale również z wartości obliczanych bieżąco stąd wynika potencjalna niestabilność.
[Obrazek: http://lx-net.pl/walet/image/iir.jpg]
W szczególności, w kodzie Waleta użyłem filtru typu BiQuad korzystający z niewielkiej ilości danych, i który jest obsługiwany prze Audio Tools
[Obrazek: http://lx-net.pl/walet/image/biq.jpg]
Do projektowania filtra Hilberta (typu FIR) używałem oprogramowania iowahills dostępnego w załączonym linku i choć oprogramowanie to nie jest rozwijane od lat to szukając właściwego rozwiązania znalazłem wiele pozytywnych jego recenzji.
Oprogramowanie Iowahills
Program działa tylko na Windows, nie wymaga instalacji a po rozpakowaniu należy uruchomić jedyny plik wykonywalny.
Istnieje na Github repozytorium pozwalające na skompilowanie programu we współczesnych warunkach ale ja nie przeszedłem tej drogi. Z pobieżnego oglądu wynika, że interface współczesny jest nieco zmodyfikowaną wersją dawnego rozwiązania ale trzeba nad nim samodzielnie popracować na początku.
[Obrazek: http://lx-net.pl/walet/image/iowa01.jpg]
Rysunek pokazuje w tle zawartość paczki oprogramowania po rozpakowaniu lokalnym oraz program HilbertFilters, którego działanie widać na pierwszym tle.
Projektując filtr należy przede wszystkim z menu PassBand wybrać Classic Hilbert a dodatkowo jego parametry a więc częstotliwość próbkowania (w moim przypadku 24kHz) ilość tzw. taps-ów (członów filtra) oraz dolną i górną granicę częstotliwościową. Ilość taps-ów powinna być nieparzysta.
Obliczenia uruchamia się klawiszem Calc a wynik otrzymuje się w formie grafiki wprost na ekranie oraz w pliku współczynników filtra przez wybranie File -> Save Coefficients.
[Obrazek: http://lx-net.pl/walet/image/iowa02.jpg]
Plik wyników zawiera tyle linii ile zostało zdefiniowanych taps-ów.
Tu pokazałem wynik dla filtra o 51 tap-sach oraz częstotliwości próbkowania 16kHz (w nazwie jest 51_16). Rysunek pokazuje środkową część pliku wyników gdzie wyraźnie widać symetrię współczynników filtra. Co drugi współczynnik jest zerowy a dodatkowo współczynniki z pierwszej połowy mają taką samą wartość ale z przeciwnym znakiem w stosunku do współczynników z drugiej połowy - względem elementu środkowego.
Tak uzyskany wynik należy doprowadzić do postaci akceptowalnej przez Audio Tools przy czym buduje się dzielony plik konfiguracyjny w taki sposób, że pierwsza połowa zawiera wiersze obliczonych właśnie współczynników a druga zawiera identyczną ilość elementów co pierwsza część z tym, że wszystkie elementy poza środkowym mają wartość 0 a element środkowy ma wartość 1.
To właśnie definiuje transformatę Hilberta, która działa na jeden z kanałów I lub Q budując dla transformowanego kanału wynik złożony z obliczeń na podstawie odpowiedniej ilości próbek a dla drugiego po prostu przenosi wartość próbki bez zmian. Ponieważ w nieformowanym kanale tylko środkowy element ma wartość 1 stąd wynika opóźnienie w procesowaniu sygnałów. Sygnał wyjściowy jest w najlepszym razie opóźniony o co najmniej połowę z ilości zdefiniowanych taps-ów.
W kodzie Waleta filtr hilberta hb_24_81.h , posiadający 81 taps-ów wygląda następująco:
float hilbert_24_81[] = {
0.000000000000000000,
0.001010454500352403,
0.000000000000000000,
0.001603255859532541,
....
-0.001603255859532541,
0.000000000000000000,
-0.001010454500352403,
0.000000000000000000,
};
float coeffs_delay_81[] = {
0.000000000000000000,
0.000000000000000000,
....
0.000000000000000000,
0.000000000000000000,
1.000000000000000000,
0.000000000000000000,
0.000000000000000000,
...
0.000000000000000000,
0.000000000000000000,
};
Każda z części filtra ma po 81 elementów, część aktywna (niebieska) zawiera obliczone wcześniej współczynniki a część pasywna (zielona) zawiera zera poza linią środkową (41) w której figuruje wartość 1 (w formie zmiennoprzecinkowej). Zwracam uwagę, że budując strukturę definiującą filtr Hilberta dla Audio Tools należy zastosować wymaganą gramatykę a więc również przecinki na końcu każdej linii oraz znaki nawiasów { } oraz końcowe średniki.
Do budowy filtrów LPF oraz BPF można wykorzystać struktury FIR ale znacznie wydajniejsze są tu filtry IIF a w szczególności filtry typu BiQuad
[Obrazek: http://lx-net.pl/walet/image/biquad.jpg]
Ja używam kalkulatora online dla filtrów LPF i BPF typu BiQuad z tego linku
W wyniku obliczeń uzyskuje się dwie trójki parametrów a pierwszy parametr w trójce parametrów b ma zawsze wartość 1 co zapewnia stabilność takiego filtra. Filtry BiQuad mają dodatnie sprzężenie zwrotne i dobranie nieodpowiednich współczynników prowadzi do wzbudzenia filtra (odpowiedź nieskończona).
Obliczone filtry LPF oraz BPF mają po 6 współczynników i dlatego definiowane są wprost w kodzie programu (linie 95-102) oddzielnie dla każdej trójki wyliczonych współczynników a oraz b:
//-----------------------------define lp and bp filters-------------------------------
const float b_lpf24_36[] = { 0.13109923, 0.26219847, 0.13109923 }; //biquad lpf 24kHz sampling, 3600Hz f max
const float a_lpf24_36[] = { 1.0, -0.74774808, 0.27214502 };
const float b_lpf24_24[] = { 0.06745228, 0.13490457, 0.06745228 }; //biquad lpf 24kHz sampling, 2400Hz f max
const float a_lpf24_24[] = { 1.0, -1.14292982, 0.41273895 };
const float b_bpf24_8_6[] = { 0.01703090, 0.0, -0.01703090 }; //biquad bpf 24kHz sampling, 800Hz central freq
const float a_bpf24_8_6[] = { 1.0, -1.92297774, 0.96593821 };
Ostatecznie, należy dla potrzeb Audio Tools zdefiniować tzw łańcuch filtrów w postaci (linie 345-353)
//define filters chain
//inFiltered.setFilter(0, new FIR<float>(hilbert_24_81));
inFiltered.setFilter(0, new FilterChain<float, 2>({new BiQuadDF2<float>(b_lpf24_36,a_lpf24_36), new FIR<float>(hilbert_24_81)}));
//inFiltered.setFilter(0, new FilterChain<float, 2>({new BiQuadDF2<float>(b_bpf24_8_6,a_bpf24_8_6), new FIR<float>(hilbert_24_81)}));
//inFiltered.setFilter(1, new FIR<float>(hilbert_24_81));
inFiltered.setFilter(1, new FilterChain<float, 2>({new BiQuadDF2<float>(b_lpf24_36,a_lpf24_36), new FIR<float>(coeffs_delay_81)}));
//inFiltered.setFilter(1, new FilterChain<float, 2>({new BiQuadDF2<float>(b_lpf24_24,a_lpf24_20), new FIR<float>(coeffs_delay_81)}));
//inFiltered.setFilter(1, new FilterChain<float, 2>({new BiQuadDF2<float>(b_bpf24_8_6,a_bpf24_8_6), new FIR<float>(coeffs_delay_81)}));
Zwracam uwagę (czerwone i purpurowe podkreślenia), że o ile nazwę dla pliku nagłówkowego .h dla danych filtra Hilberta można nadać dowolnie to przy definicji dla obu kanałów filtrowania (I oraz Q) muszą się znaleźć opisy obu części filtra tak jak je podano w pliku współczynników). Podobnie jest dla filtrów LPF i BPF ale one, że względu na małą ilość parametrów zdefiniowane są jako stałe i tej nazwy dla stałej należy używać.
Aktywne linie w powyższym opisie definiują filtrowanie dla kanałów I oraz Q więc różnią się jedynie częścią użytego filtra Hilberta bo filtrowanie LPF lub BPF są oczywiście dla obu kanałów identyczne.
Mam nadzieję, że podane opisy będą dla części kolegów inspiracją do eksperymentowania z własnymi rozwiązaniami. W szczególności dotyczy to filtra Hilberta. Cechą filtrów typu FIR jest to, że jakość filtracji wzrasta z ilością taps-ów ale wiąże się to z wydłużeniem procesu obliczeń z jednej strony ale także ze zwiększeniem opóźnienia w procesie obróbki sygnału o czym będzie także później.
Jakkolwiek, im dłuższy (więcej taps) filtr Hilberta tym lepsze będzie tłumienie niepożądanej wstęgi ale nie można wydłużać filtra w nieskończoność bo w pewnym momencie przekroczone zostaną moce obliczeniowe procesora.
Tu jednak jest możliwość eksperymentowania zarówno z długością filtra/filtrów jak i jego parametrami (np. okno dla filtra Hilberta) oraz z częstotliwością próbkowania.
W tym ostatnim przypadku trzeba pamiętać, że dla konkretnej częstotliwości próbkowania można z danych wyłuskać jedynie informacje o sygnałach mających częstotliwość dwukrotnie mniejszą niż częstotliwość próbkowania (tzw twierdzenie o próbkowaniu Nyquista-Shannona) a więc kod Waleta działający z prędkością próbkowania 24kHz może ewentualnie zidentyfikować sygnały nie szybsze niż 12kHz. Ma to takie przełożenie na wyniki przetwarzania, że w ewentualnym "wodospadzie sygnałów" nie będzie takich, które są oddalonych o więcej niż 12 kHz od częstotliwości pracy.
cdn. lj
Github
Niemniej, w liniach 106-115 (każdorazowo odwołuję się do pierwszej wersji programu dostępnego na Github) następują podstawowe definicje przepływu informacji w Audio Tools. Instrukcje zakomentowane oczywiście się "nie liczą" ale pokazują ścieżki po jakich chodziłem i na razie takich elementów nie usuwałem. Podobnie jak nie usuwałem z kodu zakomentowanych alternatywnych rozwiązań, choćby na temat uzyskania przesunięcia fazy o 90 stopni wprost w si5351.
W liniach 345-353 znajdują się definicje łańcuchów filtrów cyfrowych jakich na starcie używa program a w komentarzach są alternatywne definicje używane dla innych trybów pracy.
Linie 355-376 definiują zarówno kodek jak i porty A/D i D/A oraz uruchamiają część odpowiedzialną za cyfrowe przetwarzanie audio.
Linie 752-777 to właściwa definicja przetwarzania audio, w szczególności polecenie copier.copy() to kwintesencja narzędzi Audio Tools i ono właśnie zawiaduje właściwym przepływem danych cyfrowych w porządku określonym zarówno w liniach 106-115 jak i w definicjach łańcuchów filtrów (345-353).
Jednocześnie linie 752-777 to oddzielne zadanie dla drugiego rdzenia procesora zajmujące się wyłącznie obsługą audio o czym napiszę dalej.
Czas więc na początek opisać szczegółowo jak prawidłowo zdefiniować filtry cyfrowe dla systemu Audio Tools. Aby je narzędzia przetwarzania zrozumiały muszą mieć właściwy format.
Krótko o filtrach cyfrowych. Zasadniczo wyróżnia się dwa rodzaje:
FIR (Finite Impulse Response): Posiadają skończoną odpowiedź impulsową, są zawsze stabilne i pozwalają uzyskać liniową charakterystykę fazową. Do obliczeń sygnału wyjściowego używa się zawsze ustabilizowanych danych "z przeszłości". Takim filtrem jest użyty w kodzie Waleta filtr Hilberta.
[Obrazek: http://lx-net.pl/walet/image/fir.jpg]
IIR (Infinite Impulse Response): Mają nieskończoną odpowiedź impulsową dzięki sprzężeniu zwrotnemu (rekurencji); są bardziej wydajne obliczeniowo, lecz mogą być niestabilne. W kodzie Waleta ten rodzaj filtrów służy do uzyskania charakterystyk typu LPF lub BPF. Filtry IIR korzystają za danych z "przeszłości" ale również z wartości obliczanych bieżąco stąd wynika potencjalna niestabilność.
[Obrazek: http://lx-net.pl/walet/image/iir.jpg]
W szczególności, w kodzie Waleta użyłem filtru typu BiQuad korzystający z niewielkiej ilości danych, i który jest obsługiwany prze Audio Tools
[Obrazek: http://lx-net.pl/walet/image/biq.jpg]
Do projektowania filtra Hilberta (typu FIR) używałem oprogramowania iowahills dostępnego w załączonym linku i choć oprogramowanie to nie jest rozwijane od lat to szukając właściwego rozwiązania znalazłem wiele pozytywnych jego recenzji.
Oprogramowanie Iowahills
Program działa tylko na Windows, nie wymaga instalacji a po rozpakowaniu należy uruchomić jedyny plik wykonywalny.
Istnieje na Github repozytorium pozwalające na skompilowanie programu we współczesnych warunkach ale ja nie przeszedłem tej drogi. Z pobieżnego oglądu wynika, że interface współczesny jest nieco zmodyfikowaną wersją dawnego rozwiązania ale trzeba nad nim samodzielnie popracować na początku.
[Obrazek: http://lx-net.pl/walet/image/iowa01.jpg]
Rysunek pokazuje w tle zawartość paczki oprogramowania po rozpakowaniu lokalnym oraz program HilbertFilters, którego działanie widać na pierwszym tle.
Projektując filtr należy przede wszystkim z menu PassBand wybrać Classic Hilbert a dodatkowo jego parametry a więc częstotliwość próbkowania (w moim przypadku 24kHz) ilość tzw. taps-ów (członów filtra) oraz dolną i górną granicę częstotliwościową. Ilość taps-ów powinna być nieparzysta.
Obliczenia uruchamia się klawiszem Calc a wynik otrzymuje się w formie grafiki wprost na ekranie oraz w pliku współczynników filtra przez wybranie File -> Save Coefficients.
[Obrazek: http://lx-net.pl/walet/image/iowa02.jpg]
Plik wyników zawiera tyle linii ile zostało zdefiniowanych taps-ów.
Tu pokazałem wynik dla filtra o 51 tap-sach oraz częstotliwości próbkowania 16kHz (w nazwie jest 51_16). Rysunek pokazuje środkową część pliku wyników gdzie wyraźnie widać symetrię współczynników filtra. Co drugi współczynnik jest zerowy a dodatkowo współczynniki z pierwszej połowy mają taką samą wartość ale z przeciwnym znakiem w stosunku do współczynników z drugiej połowy - względem elementu środkowego.
Tak uzyskany wynik należy doprowadzić do postaci akceptowalnej przez Audio Tools przy czym buduje się dzielony plik konfiguracyjny w taki sposób, że pierwsza połowa zawiera wiersze obliczonych właśnie współczynników a druga zawiera identyczną ilość elementów co pierwsza część z tym, że wszystkie elementy poza środkowym mają wartość 0 a element środkowy ma wartość 1.
To właśnie definiuje transformatę Hilberta, która działa na jeden z kanałów I lub Q budując dla transformowanego kanału wynik złożony z obliczeń na podstawie odpowiedniej ilości próbek a dla drugiego po prostu przenosi wartość próbki bez zmian. Ponieważ w nieformowanym kanale tylko środkowy element ma wartość 1 stąd wynika opóźnienie w procesowaniu sygnałów. Sygnał wyjściowy jest w najlepszym razie opóźniony o co najmniej połowę z ilości zdefiniowanych taps-ów.
W kodzie Waleta filtr hilberta hb_24_81.h , posiadający 81 taps-ów wygląda następująco:
float hilbert_24_81[] = {
0.000000000000000000,
0.001010454500352403,
0.000000000000000000,
0.001603255859532541,
....
-0.001603255859532541,
0.000000000000000000,
-0.001010454500352403,
0.000000000000000000,
};
float coeffs_delay_81[] = {
0.000000000000000000,
0.000000000000000000,
....
0.000000000000000000,
0.000000000000000000,
1.000000000000000000,
0.000000000000000000,
0.000000000000000000,
...
0.000000000000000000,
0.000000000000000000,
};
Każda z części filtra ma po 81 elementów, część aktywna (niebieska) zawiera obliczone wcześniej współczynniki a część pasywna (zielona) zawiera zera poza linią środkową (41) w której figuruje wartość 1 (w formie zmiennoprzecinkowej). Zwracam uwagę, że budując strukturę definiującą filtr Hilberta dla Audio Tools należy zastosować wymaganą gramatykę a więc również przecinki na końcu każdej linii oraz znaki nawiasów { } oraz końcowe średniki.
Do budowy filtrów LPF oraz BPF można wykorzystać struktury FIR ale znacznie wydajniejsze są tu filtry IIF a w szczególności filtry typu BiQuad
[Obrazek: http://lx-net.pl/walet/image/biquad.jpg]
Ja używam kalkulatora online dla filtrów LPF i BPF typu BiQuad z tego linku
W wyniku obliczeń uzyskuje się dwie trójki parametrów a pierwszy parametr w trójce parametrów b ma zawsze wartość 1 co zapewnia stabilność takiego filtra. Filtry BiQuad mają dodatnie sprzężenie zwrotne i dobranie nieodpowiednich współczynników prowadzi do wzbudzenia filtra (odpowiedź nieskończona).
Obliczone filtry LPF oraz BPF mają po 6 współczynników i dlatego definiowane są wprost w kodzie programu (linie 95-102) oddzielnie dla każdej trójki wyliczonych współczynników a oraz b:
//-----------------------------define lp and bp filters-------------------------------
const float b_lpf24_36[] = { 0.13109923, 0.26219847, 0.13109923 }; //biquad lpf 24kHz sampling, 3600Hz f max
const float a_lpf24_36[] = { 1.0, -0.74774808, 0.27214502 };
const float b_lpf24_24[] = { 0.06745228, 0.13490457, 0.06745228 }; //biquad lpf 24kHz sampling, 2400Hz f max
const float a_lpf24_24[] = { 1.0, -1.14292982, 0.41273895 };
const float b_bpf24_8_6[] = { 0.01703090, 0.0, -0.01703090 }; //biquad bpf 24kHz sampling, 800Hz central freq
const float a_bpf24_8_6[] = { 1.0, -1.92297774, 0.96593821 };
Ostatecznie, należy dla potrzeb Audio Tools zdefiniować tzw łańcuch filtrów w postaci (linie 345-353)
//define filters chain
//inFiltered.setFilter(0, new FIR<float>(hilbert_24_81));
inFiltered.setFilter(0, new FilterChain<float, 2>({new BiQuadDF2<float>(b_lpf24_36,a_lpf24_36), new FIR<float>(hilbert_24_81)}));
//inFiltered.setFilter(0, new FilterChain<float, 2>({new BiQuadDF2<float>(b_bpf24_8_6,a_bpf24_8_6), new FIR<float>(hilbert_24_81)}));
//inFiltered.setFilter(1, new FIR<float>(hilbert_24_81));
inFiltered.setFilter(1, new FilterChain<float, 2>({new BiQuadDF2<float>(b_lpf24_36,a_lpf24_36), new FIR<float>(coeffs_delay_81)}));
//inFiltered.setFilter(1, new FilterChain<float, 2>({new BiQuadDF2<float>(b_lpf24_24,a_lpf24_20), new FIR<float>(coeffs_delay_81)}));
//inFiltered.setFilter(1, new FilterChain<float, 2>({new BiQuadDF2<float>(b_bpf24_8_6,a_bpf24_8_6), new FIR<float>(coeffs_delay_81)}));
Zwracam uwagę (czerwone i purpurowe podkreślenia), że o ile nazwę dla pliku nagłówkowego .h dla danych filtra Hilberta można nadać dowolnie to przy definicji dla obu kanałów filtrowania (I oraz Q) muszą się znaleźć opisy obu części filtra tak jak je podano w pliku współczynników). Podobnie jest dla filtrów LPF i BPF ale one, że względu na małą ilość parametrów zdefiniowane są jako stałe i tej nazwy dla stałej należy używać.
Aktywne linie w powyższym opisie definiują filtrowanie dla kanałów I oraz Q więc różnią się jedynie częścią użytego filtra Hilberta bo filtrowanie LPF lub BPF są oczywiście dla obu kanałów identyczne.
Mam nadzieję, że podane opisy będą dla części kolegów inspiracją do eksperymentowania z własnymi rozwiązaniami. W szczególności dotyczy to filtra Hilberta. Cechą filtrów typu FIR jest to, że jakość filtracji wzrasta z ilością taps-ów ale wiąże się to z wydłużeniem procesu obliczeń z jednej strony ale także ze zwiększeniem opóźnienia w procesie obróbki sygnału o czym będzie także później.
Jakkolwiek, im dłuższy (więcej taps) filtr Hilberta tym lepsze będzie tłumienie niepożądanej wstęgi ale nie można wydłużać filtra w nieskończoność bo w pewnym momencie przekroczone zostaną moce obliczeniowe procesora.
Tu jednak jest możliwość eksperymentowania zarówno z długością filtra/filtrów jak i jego parametrami (np. okno dla filtra Hilberta) oraz z częstotliwością próbkowania.
W tym ostatnim przypadku trzeba pamiętać, że dla konkretnej częstotliwości próbkowania można z danych wyłuskać jedynie informacje o sygnałach mających częstotliwość dwukrotnie mniejszą niż częstotliwość próbkowania (tzw twierdzenie o próbkowaniu Nyquista-Shannona) a więc kod Waleta działający z prędkością próbkowania 24kHz może ewentualnie zidentyfikować sygnały nie szybsze niż 12kHz. Ma to takie przełożenie na wyniki przetwarzania, że w ewentualnym "wodospadzie sygnałów" nie będzie takich, które są oddalonych o więcej niż 12 kHz od częstotliwości pracy.
cdn. lj
Github

