Реализация обработки сигналов в реальном времени в Python - как захватить аудио непрерывно?

Я планирую реализовать" DSP-подобный " сигнальный процессор в Python. Он должен захватывать небольшие фрагменты аудио через ALSA, обрабатывать их, а затем воспроизводить их через ALSA.

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

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)

однако, когда Я даже выполняю малейшую модификацию аудиоданных (e. г. прокомментируйте эту строку), я получаю много шума и экстремальных искажений на выходе. Похоже, я неправильно обрабатываю данные PCM. Самое странное, что выход "измеритель уровня" и т. д. кажется, все имеет смысл. Однако выход полностью искажается (но непрерывный), когда я немного смещаю его.

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

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

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

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

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

3 ответов


когда вы

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

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

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


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

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

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

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

Flow graph

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

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


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

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

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

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

звук теперь беззазорный и неискаженный, но задержка слишком высока.