2016-01-05 35 views
8

Mam zamiar wdrożyć procesor sygnału "podobny do DSP" w Pythonie. Powinien przechwytywać małe fragmenty audio przez ALSA, przetwarzać je, a następnie odtwarzać za pomocą ALSA.Implementacja przetwarzania sygnału w czasie rzeczywistym w Pythonie - jak przechwytywać dźwięk w sposób ciągły?

Aby rozpocząć, napisałem następujący (bardzo prosty) kod.

import alsaaudio 

inp = alsaaudio.PCM(alsaaudio.PCM_CAPTURE, alsaaudio.PCM_NORMAL) 
inp.setchannels(1) 
inp.setrate(96000) 
inp.setformat(alsaaudio.PCM_FORMAT_U32_LE) 
inp.setperiodsize(1920) 

outp = alsaaudio.PCM(alsaaudio.PCM_PLAYBACK, alsaaudio.PCM_NORMAL) 
outp.setchannels(1) 
outp.setrate(96000) 
outp.setformat(alsaaudio.PCM_FORMAT_U32_LE) 
outp.setperiodsize(1920) 

while True: 
    l, data = inp.read() 
    # TODO: Perform some processing. 
    outp.write(data) 

Problem polega na tym, że dźwięk "zacina się" i nie jest pozbawiony szczelin. Próbowałem eksperymentować z trybem PCM, ustawiając go na PCM_ASYNC lub PCM_NONBLOCK, ale problem pozostaje. Myślę, że problem polega na tym, że próbki "pomiędzy" dwoma kolejnymi wywołaniami "inp.read()" zostają utracone.

Czy istnieje sposób na przechwytywanie dźwięku "w sposób ciągły" w języku Python (najlepiej bez potrzeby stosowania zbyt "specyficznych"/"niestandardowych" bibliotek)? Chciałbym, aby sygnał zawsze był przechwytywany "w tle" do jakiegoś bufora, z którego mogę odczytać jakiś "chwilowy stan", podczas gdy dźwięk jest dalej przechwytywany do bufora nawet w czasie, kiedy wykonuję operacje odczytu . Jak mogę to osiągnąć?

Nawet jeśli użyję dedykowanego procesu/wątku do przechwytywania dźwięku, ten proces/wątek będzie zawsze przynajmniej musiał (1) odczytać dźwięk ze źródła, (2) a następnie umieścić go w buforze (z którego Następnie odczytuje się proces/wątek "przetwarzania sygnału"). Te dwie operacje będą zatem nadal sekwencyjne w czasie, a zatem próbki zostaną utracone. Jak tego uniknąć?

Bardzo dziękuję za porady!

EDYTUJ 2: Teraz mam go uruchomione.

import alsaaudio 
from multiprocessing import Process, Queue 
import numpy as np 
import struct 

""" 
A class implementing buffered audio I/O. 
""" 
class Audio: 

    """ 
    Initialize the audio buffer. 
    """ 
    def __init__(self): 
     #self.__rate = 96000 
     self.__rate = 8000 
     self.__stride = 4 
     self.__pre_post = 4 
     self.__read_queue = Queue() 
     self.__write_queue = Queue() 

    """ 
    Reads audio from an ALSA audio device into the read queue. 
    Supposed to run in its own process. 
    """ 
    def __read(self): 
     inp = alsaaudio.PCM(alsaaudio.PCM_CAPTURE, alsaaudio.PCM_NORMAL) 
     inp.setchannels(1) 
     inp.setrate(self.__rate) 
     inp.setformat(alsaaudio.PCM_FORMAT_U32_BE) 
     inp.setperiodsize(self.__rate/50) 

     while True: 
      _, data = inp.read() 
      self.__read_queue.put(data) 

    """ 
    Writes audio to an ALSA audio device from the write queue. 
    Supposed to run in its own process. 
    """ 
    def __write(self): 
     outp = alsaaudio.PCM(alsaaudio.PCM_PLAYBACK, alsaaudio.PCM_NORMAL) 
     outp.setchannels(1) 
     outp.setrate(self.__rate) 
     outp.setformat(alsaaudio.PCM_FORMAT_U32_BE) 
     outp.setperiodsize(self.__rate/50) 

     while True: 
      data = self.__write_queue.get() 
      outp.write(data) 

    """ 
    Pre-post data into the output buffer to avoid buffer underrun. 
    """ 
    def __pre_post_data(self): 
     zeros = np.zeros(self.__rate/50, dtype = np.uint32) 

     for i in range(0, self.__pre_post): 
      self.__write_queue.put(zeros) 

    """ 
    Runs the read and write processes. 
    """ 
    def run(self): 
     self.__pre_post_data() 
     read_process = Process(target = self.__read) 
     write_process = Process(target = self.__write) 
     read_process.start() 
     write_process.start() 

    """ 
    Reads audio samples from the queue captured from the reading thread. 
    """ 
    def read(self): 
     return self.__read_queue.get() 

    """ 
    Writes audio samples to the queue to be played by the writing thread. 
    """ 
    def write(self, data): 
     self.__write_queue.put(data) 

    """ 
    Pseudonymize the audio samples from a binary string into an array of integers. 
    """ 
    def pseudonymize(self, s): 
     return struct.unpack(">" + ("I" * (len(s)/self.__stride)), s) 

    """ 
    Depseudonymize the audio samples from an array of integers into a binary string. 
    """ 
    def depseudonymize(self, a): 
     s = "" 

     for elem in a: 
      s += struct.pack(">I", elem) 

     return s 

    """ 
    Normalize the audio samples from an array of integers into an array of floats with unity level. 
    """ 
    def normalize(self, data, max_val): 
     data = np.array(data) 
     bias = int(0.5 * max_val) 
     fac = 1.0/(0.5 * max_val) 
     data = fac * (data - bias) 
     return data 

    """ 
    Denormalize the data from an array of floats with unity level into an array of integers. 
    """ 
    def denormalize(self, data, max_val): 
     bias = int(0.5 * max_val) 
     fac = 0.5 * max_val 
     data = np.array(data) 
     data = (fac * data).astype(np.int64) + bias 
     return data 

debug = True 
audio = Audio() 
audio.run() 

while True: 
    data = audio.read() 
    pdata = audio.pseudonymize(data) 

    if debug: 
     print "[PRE-PSEUDONYMIZED] Min: " + str(np.min(pdata)) + ", Max: " + str(np.max(pdata)) 

    ndata = audio.normalize(pdata, 0xffffffff) 

    if debug: 
     print "[PRE-NORMALIZED] Min: " + str(np.min(ndata)) + ", Max: " + str(np.max(ndata)) 
     print "[PRE-NORMALIZED] Level: " + str(int(10.0 * np.log10(np.max(np.absolute(ndata))))) 

    #ndata += 0.01 # When I comment in this line, it wreaks complete havoc! 

    if debug: 
     print "[POST-NORMALIZED] Level: " + str(int(10.0 * np.log10(np.max(np.absolute(ndata))))) 
     print "[POST-NORMALIZED] Min: " + str(np.min(ndata)) + ", Max: " + str(np.max(ndata)) 

    pdata = audio.denormalize(ndata, 0xffffffff) 

    if debug: 
     print "[POST-PSEUDONYMIZED] Min: " + str(np.min(pdata)) + ", Max: " + str(np.max(pdata)) 
     print "" 

    data = audio.depseudonymize(pdata) 
    audio.write(data) 

Jednak, kiedy nawet wykonać najmniejszego modyfikacji danych audio (np. G. Skomentować tę linię), dostaję dużo hałasu i ekstremalnych zniekształceń na wyjściu. Wygląda na to, że nie obsługuję poprawnie danych PCM. Dziwne jest to, że wynik "miernika poziomu" itp. Wydaje się mieć sens. Jednak wynik jest całkowicie zniekształcony (ale ciągły), gdy przesunę go nieznacznie.

EDIT 3: Właśnie dowiedziałem się, że moje algorytmy (nie zawarte tutaj) działają, gdy stosuję je do plików wave. Problem naprawdę wydaje się sprowadzać do API ALSA.

EDYTOWANIE 4: W końcu znalazłem problemy. Byli następujący.

Pierwsza - ALSA po cichu "cofnęła się" do PCM_FORMAT_U8_LE po zażądaniu PCM_FORMAT_U32_LE, więc zinterpretowałem dane niepoprawnie, zakładając, że każda próbka ma szerokość 4 bajtów. Działa, gdy zażądam PCM_FORMAT_S32_LE.

2-ty - Wyjście ALSA wydaje się spodziewać wielkości okres w bajtów, choć jawnie stwierdzają, że można się spodziewać, w klatek w specyfikacji. Dlatego musisz ustawić czterokrotnie większy okres dla wyjścia, jeśli korzystasz z 32-bitowej głębokości próbki.

3rd - Nawet w Pythonie (gdzie występuje "globalna blokada interpretera"), procesy są powolne w porównaniu do wątków. Możesz znacznie zmniejszyć opóźnienie, przechodząc na wątki, ponieważ wątki we/wy w zasadzie nie wykonują niczego, co wymaga dużej mocy obliczeniowej.

+0

Korzystanie z wątku do czytania i publikowania w kolejce powinno działać. 'PCM' ma bufor kontrolowany przez' setperiodsize' (domyślnie przyjmuje 32 klatki), co daje ci czas na wysłanie danych. – tdelaney

+0

Myślę, że problem polega na tym, że "read()" czyta tylko z urządzenia audio podczas jego działania. Jeśli się zwróci, operacja odczytu zostanie zakończona (w przeciwnym razie nie może zwrócić żadnych istotnych danych).Nawet jeśli mam drugi wątek uruchomiony, wykonuję "read()", a następnie dołączam zwrócone dane do bufora, to nie "odczytuje()" podczas dodawania, a zatem będzie luka w przechwytywaniu. –

+0

Wow. Wtedy ten interfejs jest poważnie uszkodzony. Interfejsy, które mają tradycyjne tryby blokowania/nie blokowania, wymagają pośrednich buforów z tego powodu, który opisujesz. Interfejs czasu rzeczywistego wymaga buforów preposting przed wygenerowaniem danych. Ale 'asaudio' nie wydaje się działać w ten sposób. Nie mogę sobie wyobrazić, jak ten moduł działałby bez buforowania. Więc ..., czy jesteś pewien, że tak to działa, czy spekulujesz? Myślę, że buforuje X klatek na raz i jeśli nie przeczytasz go do czasu pojawienia się następnego X, to stracisz go. Zgadnij z mojej strony! – tdelaney

Odpowiedz

2

Kiedy

  1. czytać jedną porcję danych,
  2. zapisu jeden kawałek danych,
  3. następnie czekać na drugą porcję danych do odczytu,

wówczas bufor urządzenia wyjściowego stanie się pusty, jeśli drugi porcja nie będzie krótszy niż pierwsza porcja.

Przed rozpoczęciem faktycznego przetwarzania należy wypełnić bufor urządzenia wyjściowego ciszą. Wtedy niewielkie opóźnienia w przetwarzaniu wejściowym lub wyjściowym nie będą miały znaczenia.

+0

Wielkie dzięki! Dowiedziałem się, że potrzebuję co najmniej czterech pełnych okresów w moim buforze wyjściowym, aby dźwięk był ciągły, ponieważ jest prawdopodobnie inny od systemu do systemu (i prawdopodobnie również zmienia się wraz z obciążeniem systemu), sprawię, że będzie to konfigurowalne Większy bufor -> większe opóźnienie, ale mniejsza szansa na "jąkanie", mniejszy bufor -> mniejsze opóźnienie, ale większe prawdopodobieństwo "jąkania" .Później mogę nawet "automatycznie dostroić" system poprzez dynamiczne zwiększenie bufora rozmiar na underruns i zmniejszenie go, gdy bufor wyjściowy jest "daleko od underrunning", gdy aplikat jon publikuje nowe próbki. –

2

Można zrobić wszystko ręcznie, jak @CL zalecane w jego/jej answer, ale polecam tylko przy użyciu GNU Radio zamiast:

Jest to struktura, która dba o wszystkie robi „Uzyskiwanie małe kawałki próbek wchodzących i wychodzących twój algorytm "; bardzo dobrze się skaluje i możesz napisać swoje przetwarzanie sygnałów w Pythonie lub C++.

W rzeczywistości jest wyposażony w źródło dźwięku i urządzenie audio, które bezpośrednio rozmawia z ALSA i po prostu daje/bierze ciągłe próbki. Polecam lekturę za pośrednictwem GNU Radio: Guided Tutorials; dokładnie wyjaśniają, co jest niezbędne do przetwarzania sygnału dla aplikacji audio.

Naprawdę minimalny przepływ wykres wyglądałby następująco:

Flow graph

można zastąpić filtr górnoprzepustowy dla własnego bloku przetwarzania sygnału, lub użyć dowolnej kombinacji istniejących bloków.

Jest pomocnych rzeczy, jak i plików wav umywalki i źródeł, filtry, resamplers, wzmacniacze (ok, mnożniki), ...

+0

Cóż, chciałbym to zrobić w najbardziej "niezależny" sposób. Moja aplikacja nie jest też odbiorem radiowym/demodulacją. To może jednak uchronić mnie przed kontaktem z urządzeniem audio. Być może powinienem spróbować pracować nad plikami audio, dopóki nie uda mi się uzyskać prawidłowego interfejsu ALSA. To wydaje się być problemem. Mogę teraz poprawnie czytać/pisać, ale nie mogę przetworzyć danych. Wygląda na to, że ma dziwny format. Nawet dodanie małej stałej ("przesunięcie DC") do każdej próbki spowoduje, że wyjście stanie się po prostu szumem, co jest dziwne, jak można oczekiwać, że "w zasadzie nic nie zrobi". –

+0

"standalone"! = Python, kłócę się. Naprawdę, GNU Radio stałoby się zależne od twojego programu Pythona, ale tak naprawdę to jest: biblioteka, która musi zostać zainstalowana, aby twój program działał, podobnie jak twój python musi mieć obsługę ALSA. –

+0

Cóż, nie może być tak trudno połączyć się z ALSA "bezpośrednio", prawda? Myślę, że problem polega na tym, że próbki są w jakimś "mętnym formacie danych", którego nie traktuję poprawnie. Co myślisz? –

0

końcu znalazłem problemów. Byli następujący.

Pierwsza - ALSA po cichu "cofnęła się" do PCM_FORMAT_U8_LE po zażądaniu PCM_FORMAT_U32_LE, więc zinterpretowałem dane niepoprawnie, zakładając, że każda próbka ma szerokość 4 bajtów. Działa, gdy zażądam PCM_FORMAT_S32_LE.

Drugi - Wydaje się, że wyjście ALSA oczekuje wielkości okresu w bajtach, mimo że wyraźnie wskazują, że oczekuje się w ramkach w specyfikacji. Dlatego musisz ustawić czterokrotnie większy okres dla wyjścia, jeśli korzystasz z 32-bitowej głębokości próbki.

3rd - Nawet w Pythonie (gdzie występuje "globalna blokada interpretera"), procesy są powolne w porównaniu do wątków. Możesz znacznie zmniejszyć opóźnienie, przechodząc na wątki, ponieważ wątki we/wy w zasadzie nie wykonują niczego, co wymaga dużej mocy obliczeniowej.

Dźwięk jest teraz pozbawiony szczelin i zniekształcony, ale opóźnienie jest o wiele za wysokie.