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