Низкочастотная фильтрация в реальном времени wav-файлов или необработанного входного аудио и одновременное воспроизведение в Python - PullRequest
0 голосов
/ 26 июня 2018

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

Ниже я приведу наиболее важные фрагменты кода программы.Мой код по крайней мере слабо основан на ответах, которые я читаю здесь, а также на некоторых моих собственных идеях (таких как кольцевой буфер).Хотя моя основная цель - получить помощь, есть еще одна причина, по которой я пишу это.В качестве благодарности за информацию, которую это сообщество предоставило мне, я верну то, что знаю сейчас, в надежде, что все это в одном месте поможет кому-то другому, желающему достичь той же или аналогичной цели.Сценарий подробно описан ниже.

Сначала загружаются необходимые встроенные модули.

import sys, wave, math, subprocess

Несколько глобальных переменных объявлены и инициализированы.Важно, чтобы они были глобальными, потому что данные должны сохраняться между вызовами функции фильтра.Функция зажима ОЧЕНЬ важна, потому что без нее преобразование обратно в s16le из подписанного int завершится с ошибкой переполнения.Мне также нужно загрузить aplay как подпроцесс, чтобы отправить необработанные образцы s16le после обработки.Я выбрал aplay в качестве метода вывода звука, потому что он удобно буферизует сами данные и очень прост в использовании, потому что вы просто передаете данные на него.

aplay=subprocess.Popen(('aplay','-f','cd'),stdin=subprocess.PIPE)
source=wave.open(sys.argv[1],"rb")
frameRate=source.getframerate()
frequencyRatio=(int(sys.argv[2])/frameRate)
global windowSize
windowSize=int(math.sqrt(0.196196+frequencyRatio**2)/frequencyRatio)
global bufferIndex0; bufferIndex0=0
global bufferL0; bufferL0=[]
global bufferR0; bufferR0=[]
for _ in range(windowSize+1):
    bufferL0.append(0); bufferR0.append(0
clamp = lambda n, minn, maxn: max(min(maxn, n), minn)

Далее, волновой файл загружается в память как«данные» и «данные» разрываются и разделяются на отдельные кадры, хранящиеся в виде массива frame [].Объект / переменная «data» теперь бесполезен и, вероятно, занимает 1 ГБ или более ОЗУ, поэтому он получает значение «del data».Затем данные кадра зацикливаются и преобразуются в 16-разрядные целые числа со знаком.Он передается функции скользящего среднего.Как вы можете видеть, у меня есть три копии, чтобы достичь желаемой частоты среза.Я также избегал максимально возможного хранения данных в переменных, и это значительно уменьшило потребление памяти и значительно ускорило мой код.Это перешло от заикания к плавному воспроизведению, но при этом потребляя почти всю доступную вычислительную мощность на одном ядре.

if __name__=="__main__":
    length=source.getnframes()
    data=source.readframes(length)
    frame=[data[_:_+4] for _ in range(0,len(data),4)]
    del data
    channel=[]
    for _ in range(length):
        channel=rollingAverage_stage2(rollingAverage_stage1(rollingAverage_stage0([int.from_bytes(frame[_][:2], byteorder='little', signed=True),int.from_bytes(frame[_][2:], byteorder='little', signed=True)])))
        aplay.stdin.write(bytearray(channel[0].to_bytes(2, byteorder='little', signed=True)+channel[1].to_bytes(2, byteorder='little', signed=True)))

Функция "rollAverage" подробно описана здесь.Я, очевидно, показываю только одну копию, поскольку они все одинаковые, за исключением имен переменных.Дополнительные глобальные переменные ringIndex1, ringIndex2, bufferL1, bufferL2, bufferR1 и bufferR2 каждый объявляется в начале скрипта и в соответствующих функциях.Возможно, было бы лучше создать динамические переменные и некоторые экземпляры класса «RollingAverage» на основе входного параметра с числом проходов, а не три фиксированных копии.

def rollingAverage_stage0(channel):
    global bufferL0; global bufferR0; global ringIndex0
    bufferL0[ringIndex0],bufferR0[ringIndex0]=channel[0],channel[1]
    channel=[clamp(int(sum(bufferL0)/windowSize),-32768,32767),clamp(int(sum(bufferR0)/windowSize),-32768,32767)]
    if ringIndex0==windowSize:
        ringIndex0=0
    else:
        ringIndex0+=1
    return channel

Это суммирует код.Он работает на удивление хорошо для низких частот среза (например, 500), но немного обрезает звук при использовании с частотой среза более 5000 Гц.Не проблема для моего использования, так как я намереваюсь обрезать частоты до полосы голоса, которая составляет 3000 Гц и ниже, и я могу перейти на 2000 Гц и ниже.Мой последний скрипт будет читать необработанные кадры из rtl_sdr, используя канал подпроцесса.Программа будет использоваться следующим образом:

lowfilter.py <parameters>

rtl_sdr - это команда, которая контролирует и собирает данные с радиоуправляемых программ на базе USB Realtek RTL2832.Я мог бы также добавить подпроцесс sox в сценарий для выполнения шумоподавления.

Как я упоминал ранее, я, скорее всего, не буду полностью переписывать программу, но тяжелая модификация, которая близка к полной перезаписи, определеннов плане.Я потратил на это несколько дней и многому научился.

Звук, который я собираюсь обработать, будет представлять собой погодное радио NOAA и аналогичные FM-передачи с низкой пропускной способностью.Я также буду реализовывать какую-то автоматическую регулировку усиления и сжатие динамического диапазона, если это возможно, но сейчас фильтрация достаточно хороша, и я собираюсь сохранить свою цель.Это то, чего я хочу достичь, и это причина узнать больше о Python.

Может быть, некоторые вещи могут быть адаптированы к сопрограммам и генераторам для меньшего использования ресурсов.Я их плохо понимаю, если вообще, но готов учиться.Кажется, они могут быть весьма применимы к этому приложению.Любые изменения, которые я делаю, предпочтительнее без дополнительных библиотек, кроме, возможно, numpy и чего-то для многопоточности.Я тоже не понимаю numpy, так что это было бы совершенно новым для меня.Ссылки на лучшие ресурсы для начинающих могут помочь, так как даже некоторые из примеров, которые я нашел для фильтрации нижних частот и тому подобного, были ПУТЬ за пределы того, что я мог понять.Вот почему я пишу свою собственную фильтрацию низких частот.Numpy, вероятно, намного быстрее, чем встроенная математическая библиотека, поэтому это мой второй фокус.

Мой текущий и основной фокус - оптимизация и балансировка нагрузки.Разделив мой скрипт на разные файлы и запустив их с помощью subprocess.Popen, я могу запустить их как отдельные процессы.Это неизбежно приведет к тому, что ядро ​​поместит их в отдельные ядра.Это означает, что я могу отправлять большие двоичные данные на стандартный ввод, и сразу же после получения последнего большого двоичного объекта данных подпроцесс сбрасывает на стандартный вывод и передает его следующему подпроцессу.Это можно сделать даже в сценарии bash вместо основного сценария python.Результат такой оптимизации будет означать, что основной сценарий тратит большую часть своего времени, просто перетасовывая данные, и приведет к значительному увеличению производительности.

Какой бы совет вы ни посоветовали, я бы хотел его услышать.

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

1 Ответ

0 голосов
/ 29 июня 2018

Я получил помощь от кого-то по этой теме, а также провел последние несколько дней, проводя некоторые собственные исследования.Ответ на мой вопрос (в настоящее время) следующий:

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

Код для input.py, который загружает файл WAV (и скоро будет захватывать данные).из команды, которая вместо этого получает необработанный звук с USB SDR), здесь.Для ясности в код добавлены комментарии, описывающие каждый шаг.

input.py:

#!/bin/env python3
# Low pass filter written in python3
# (C) TheNH813 2018
# License: WTFPLv2

# Import sys for argv[], wave for io, socket for threads
import sys, wave, socket

# Start listener server on localhost:37420
listener=socket.socket()
listener.bind(("localhost",37420))
listener.listen()
# Load the file specified in argv[1] into data
source=wave.open(sys.argv[1],"rb")
data=source.readframes(source.getnframes())
# Split the frames into an array from frames
frame=[data[_:_+4] for _ in range(0,len(data),4)]
# Delete data because we have everything we need in frame[]
del data

while True:
    # Accept incoming connections, execution stops until a connection is made
    network,address=listener.accept()
    # Let user know it's working and sending data
    print("inputServer: Sending data")
    try:
        # Attempt to send data until it's all sent
        for _ in range(source.getnframes()):
            network.send(frame[_])
    # Trap all errors and restart if something goes wrong
    except BaseException as ded:
        # Let user know server encountered error or connection died
        print("inputServer: "+str(ded))

В списке указан код, который выполняет фильтрацию и больше не зависит от группы глобальных переменных.Вот.Были сделаны некоторые оптимизации, такие как изменение массивов ringBuffer вместо передачи их туда-сюда.Я понятия не имел, что это было возможно, прежде чем я начал.Данные передаются через простой скользящий средний фильтр в несколько этапов, чтобы получить ОЧЕНЬ резкое срезание фильтра.Для моего предполагаемого применения это то, что мне нужно, просто удаление или добавление одного или двух проходов уменьшит или усилит эффект.После завершения обработки данные отправляются блоками по 65535 кадров в финальный сценарий, который воспроизводит его.Я выбрал 65535 без всякой причины, кроме того, что был необходим буфер на 1 секунду, и мне нравятся степени 2. Код здесь.

lowpass.py:

#!/bin/env python3
# Low pass filter written in python3
# (C) TheNH813 2018
# License: WTFPLv2

# Import sys for argv[], socket for multithreading, math for sqrt and sum
import sys, socket, math

# Calculating the frequency ratio is necessary for calculating window length
# Change 44100 if your sample rate is different, or get as aparameter with sys.argv[]
frequencyRatio=int(sys.argv[1])/44100
windowSize=int(math.sqrt(0.196196+frequencyRatio**2)/frequencyRatio)
# This function is necessary to clamp the output value to the 16 bit signed limits
clamp = lambda n, minn, maxn: max(min(maxn, n), minn)

# This function perform the actual filtering
def lowPassFilter(channel,ringBuffer,ringIndex):
    # Set the current index of the ring buffer to the input
    ringBuffer[ringIndex]=channel
    # Average the contents of the ring buffer
    channel=int(sum(ringBuffer)/windowSize)
    # Check if ring buffer has reached end
    if ringIndex==windowSize:
        # If it has, reset index to 0
        ringIndex=0
    else:
        # Otherwise, increment by 1
        ringIndex+=1
    # Return the processed sample and current buffer position
    return channel,ringIndex

# Define the ring buffers
ringBuffer0L=[]
for _ in range(windowSize+1):
    ringBuffer0L.append(0)
# Copying is faster the iterating again
ringBuffer0R=ringBuffer0L.copy()
ringBuffer1L=ringBuffer0L.copy()
ringBuffer1R=ringBuffer0L.copy()
ringBuffer2L=ringBuffer0L.copy()
ringBuffer2R=ringBuffer0L.copy()
# Define the ring buffer indexes
ringIndex0L=0
ringIndex0R=0
ringIndex1L=0
ringIndex1R=0
ringIndex2L=0
ringIndex2R=0

# Start the server on localhost:37421, and recieve from localhost:37420
listener=socket.socket()
listener.bind(("localhost",37421))
listener.listen()
stream=socket.socket()
stream.connect(("localhost",37420))

while True:
    # Accept incoming connections
    network,address=listener.accept()
    # Let the user know it's started and working
    print("filterServer: Began sending data to "+str(address))
    try:
        # Keep looping forever unless error occurs
        while True:
            # Clear array
            frame=[]
            # Load data into array as buffer
            for _ in range(65535):
                frame.append(stream.recv(4))
            # Perform digital signal processing
            for _ in range(65535):
                # Split each frame into the left and right channels as integers
                channel=[int.from_bytes(frame[_][:2], byteorder='little',signed=True),int.from_bytes(frame[_][2:],byteorder='little',signed=True)]
                # Perform low pass filter on left channel
                channel[0],ringIndex0L=lowPassFilter(channel[0],ringBuffer0L,ringIndex0L)
                # Perform low pass filter on left channel (again)
                channel[0],ringIndex1L=lowPassFilter(channel[0],ringBuffer1L,ringIndex1L)
                # Perform low pass filter on left channel (again)
                channel[0],ringIndex2L=lowPassFilter(channel[0],ringBuffer2L,ringIndex2L)
                # Clamp left channel so it dosen't go out of bounds and cause a over/underflow error
                channel[0]=clamp(channel[0],-32768,32767)
                # Perform low pass filter on left channel
                channel[1],ringIndex0R=lowPassFilter(channel[1],ringBuffer0R,ringIndex0R)
                # Perform low pass filter on right channel (again)
                channel[1],ringIndex1R=lowPassFilter(channel[1],ringBuffer1R,ringIndex1R)
                # Perform low pass filter on right channel (again)
                channel[1],ringIndex2R=lowPassFilter(channel[1],ringBuffer2R,ringIndex2R)
                # Clamp rihgt channel so it dosen't go out of bounds and cause a over/underflow error
                channel[1]=clamp(channel[1],-32768,32767)
                # Join the integers back into a single binary frame
                frame[_]=bytearray(channel[0].to_bytes(2, byteorder='little', signed=True)+channel[1].to_bytes(2, byteorder='little', signed=True))
            # Iterate through processed data
            for data in frame:
                # Send it off to the playback script
                network.send(data)
    except BaseException as ded:
        # Let the user know if the server dies or encountered a error
        print("filterServer: "+str(ded))

Наконец, мывозьмите необработанные данные и сгенерируйте их в aplay, которая буферизует их и записывает на звуковую карту.Это работает только в Linux или других системах с установленной ALSA.

player.py:

#!/bin/env python3
# Low pass filter written in python3
# (C) TheNH813 2018
# License: WTFPLv2

# Import socket for multithreading, subprocess for audio output
import socket, subprocess

# Connect to the filter server and recieve the packets
stream=socket.socket()
stream.connect(("localhost",37421))
# Launch aplay subprocess and have it ready to play audio
aplay=subprocess.Popen('aplay -f cd'.split(),stdin=subprocess.PIPE)

# Infinite loop
while True:
    # Clear data buffer
    data=[]
    # Fill buffer with data from server
    for _ in range(65535):
        # Request a frame of audio
        data.append(stream.recv(4))
    # Iterate through data
    for frame in data:
        # Play the frames of audio
        aplay.stdin.write(frame)

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

Вы можете использовать этот код для чего угодно, тем самым я выпускаю его под WTFPLv2.

...