2016-01-05 6 views
8

Я планирую реализовать процессор сигналов типа «DSP-like» в Python. Он должен захватывать небольшие фрагменты аудио через ALSA, обрабатывать их, а затем воспроизводить их через ALSA.Реализация обработки сигналов в реальном времени в Python - как непрерывно записывать звук?

Чтобы все началось, я написал следующий (очень простой) код.

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) 

Проблема в том, что звук «заикается» и не безразличен. Я попытался поэкспериментировать с режимом PCM, установив его на PCM_ASYNC или PCM_NONBLOCK, но проблема остается. Я думаю, проблема заключается в том, что образцы «между» двумя последующими вызовами «inp.read()» теряются.

Есть ли способ записи звука «непрерывно» в Python (желательно без необходимости слишком «конкретных»/«нестандартных» библиотек)? Я бы хотел, чтобы сигнал всегда захватывался «в фоновом режиме» в некоторый буфер, из которого я мог читать некоторое «мгновенное состояние», в то время как звук также захватывается в буфер даже в течение времени, когда я выполняю свои операции чтения , Как я могу это достичь?

Даже если я использую выделенный процесс/поток для захвата аудио, этот процесс/поток всегда будет по крайней мере иметь (1) читать аудио из источника, (2) затем помещать его в некоторый буфер (из которого обработка/процесс обработки сигнала обрабатывается). Таким образом, эти две операции будут по-прежнему последовательными по времени и, следовательно, образцы будут потеряны. Как мне избежать этого?

Большое спасибо за ваш совет!

EDIT 2: Теперь у меня он работает.

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) 

Однако, когда я даже выполнять малейшие изменения в аудиоданные (е. Г. Комментарий этой строки в), я получаю много шума и экстремальных искажений на выходе. Похоже, я не правильно обрабатываю данные PCM. Странно то, что выход «измерителя уровня» и т. Д., Кажется, имеет смысл. Однако выход полностью искажен (но непрерывный), когда я слегка его компенсировал.

EDIT 3: Я только выяснил, что мои алгоритмы (не включены сюда) работают, когда я применяю их к волновым файлам. Таким образом, на самом деле проблема действительно сводится к API ALSA.

EDIT 4: Я, наконец, нашел проблемы. Они были следующими.

1-й - ALSA тихо «упал обратно» на PCM_FORMAT_U8_LE после запроса PCM_FORMAT_U32_LE, поэтому я неправильно интерпретировал данные, предположив, что каждый образец имеет ширину 4 байта. Он работает, когда я запрашиваю PCM_FORMAT_S32_LE.

второй - выход ALSA кажется ожидать размер периода в байтах , даже если они явно заявляют, что, как ожидается, в кадров в спецификации. Таким образом, вы должны установить размер периода в четыре раза выше для вывода, если вы используете 32-битную глубину образца.

3rd - Даже в Python (где есть «блокировка глобального интерпретатора») процессы медленны по сравнению с Threads. Вы можете значительно уменьшить время ожидания, переключившись на потоки, так как потоки ввода-вывода в основном не делают ничего интенсивного в вычислительном режиме.

+0

Использование потока для чтения и публикации в очереди должно работать. 'PCM' имеет буфер, управляемый' setperiodsize' (по умолчанию он равен 32 кадрам), что дает вам время для публикации данных. – tdelaney

+0

Я думаю, что проблема в том, что «read()» только читается с аудиоустройства во время его запуска. Если он вернется, операция чтения будет завершена (иначе он не сможет вернуть сколько-нибудь значимые данные).Даже если у меня есть второй поток, выполняющий «read()», а затем добавляя возвращенные данные в буфер, он не будет «читать()» при добавлении, и поэтому в захвате будет пробел. –

+0

Ничего себе. Тогда этот интерфейс серьезно нарушен. Интерфейсы, которые имеют традиционные блокирующие/неблокирующие режимы, нуждаются в промежуточных буферах по той причине, которую вы описываете. Интерфейс реального времени требует буферов предварительного заполнения перед созданием данных. Но 'isaudio', похоже, не работает так. Я не могу представить, как этот модуль работал бы без буферизации. Итак ... вы уверены, что так оно и работает, или вы спекулируете? Я думаю, что он буферизирует X-фреймы за раз, и если вы не прочитаете его к тому времени, когда появится следующий X, тогда его потеряют. Просто догадаться с моей стороны! – tdelaney

ответ

2

Когда вы

  1. прочитать одну порцию данных,
  2. записи один кусок данных,
  3. затем ждать второй порции данных для чтения,

то буфер выходного устройства станет пустым, если второй кусок не короче, чем первый кусок.

Перед началом фактической обработки необходимо заполнить буфер выходного устройства тишиной. Тогда небольшие задержки в обработке ввода или вывода не будут иметь значения.

+0

Большое спасибо! Я узнал, что мне нужно как минимум четыре полных периода в моем буфере вывода для непрерывного звука. Так как это, вероятно, отличается от системы к системе (и, возможно, также изменяется с загрузкой системы), я сделаю это настраиваемым переменная Bigger buffer -> более высокая латентность, но более низкая вероятность «заикания», меньшего буфера -> более низкая латентность, но более высокая вероятность «заикания». Позже я мог бы даже «автонастраивать» систему динамически увеличивающимся буфером размер на недоработках и сокращение его, когда выходной буфер «далеко не поддается», когда аппликация ион отправляет новые образцы. –

2

Вы можете сделать это все вручную, как @CL рекомендовать его/ее answer, но я бы рекомендовал просто использовать GNU Radio вместо:

Это структура, которая берет на себя делать все «получать небольшие куски выборки в и из вашего алгоритма »; он очень хорошо масштабируется, и вы можете написать свою обработку сигнала либо в Python, либо в C++.

Фактически, он поставляется с источником звука и звуковой раковиной, которые напрямую разговаривают с ALSA и просто дают/берут непрерывные образцы. Я бы рекомендовал читать через GNU Radio Guided Tutorials; они точно объясняют, что необходимо для обработки ваших сигналов для аудио приложения.

Граф действительно минимальный поток будет выглядеть следующим образом:

Flow graph

Вы можете заменить фильтр высоких частот для вашего собственного блока обработки сигналов, или использовать любую комбинацию из существующих блоков.

Там в полезные вещи, как и файл WAV поглотителей и источников, фильтров, resamplers, усилители (хорошо, умножители), ...

+0

Ну, я бы хотел сделать это самым «автономным» способом. Мое приложение не является радиоприемником/демодуляцией. Это могло бы спасти меня от общения с аудиоустройством. Возможно, мне стоит попробовать работать с аудиофайлами, пока я не получу интерфейс к ALSA. Кажется, это проблема. Теперь я могу читать/писать правильно, но я не могу обработать данные. Кажется, он в каком-то странном формате. Даже добавление небольшой константы («смещение по постоянному току») к каждому образцу приведет к тому, что выход станет просто шумом, что является странным, поскольку можно было бы ожидать, что он «практически ничего не сделает». –

+0

«standalone»! = Python, я бы спорил. Действительно, GNU Radio станет зависимым от вашей программы python, но это действительно так: библиотека, которая должна быть установлена ​​для того, чтобы ваша программа функционировала, очень похоже на то, что ваш python должен иметь поддержку ALSA. –

+0

Ну, не может быть так сложно взаимодействовать с ALSA «напрямую», не так ли? Я думаю, проблема заключается в том, что образцы находятся в некотором «неясном формате данных», который я не обрабатываю правильно. Как вы думаете? –

0

я наконец-то нашел проблемы. Они были следующими.

1-й - ALSA тихо «упал обратно» на PCM_FORMAT_U8_LE после запроса PCM_FORMAT_U32_LE, поэтому я неправильно интерпретировал данные, предположив, что каждый образец имеет ширину 4 байта. Он работает, когда я запрашиваю PCM_FORMAT_S32_LE.

2nd - Выход ALSA, кажется, ожидает размер периода в байтах, хотя они явно указывают, что он ожидается в кадрах в спецификации. Таким образом, вы должны установить размер периода в четыре раза выше для вывода, если вы используете 32-битную глубину образца.

3rd - Даже в Python (где есть «блокировка глобального интерпретатора») процессы медленны по сравнению с Threads. Вы можете значительно уменьшить время ожидания, переключившись на потоки, так как потоки ввода-вывода в основном не делают ничего интенсивного в вычислительном режиме.

Аудио отсутствует, но латентность слишком высока.

 Смежные вопросы

  • Нет связанных вопросов^_^