Im Moment baue ich an einer Aufnahmeprozedur, die die Audiodaten durch ein digitales Bandpassfilter schickt. Das sieht soweit schon recht gut aus, allerdings sind die herauskommenden Daten noch nicht überprüft.
Vorsicht Aufnahme: Unterbrechungsfrei AudioDaten sammeln
Der fortlaufende Datenstrom soll unterbrechungsfrei durch ein Bandpassfilter geleitet werden. Die Soundkarte liefert Daten immer Blockweise ab in sog. "chunks", üblicherweise 1024 (Stereo-) Samples. „Ruckelfreies“ Einsammeln dieser Datenblöcke erfordert den Einsatz der callback-funktion, einer Interruptroutine die von sich aus die AudioDaten in Einheiten von chunks übergibt. 1024 AudioSamples dauern bei 44,1kHz Abtastrate nur 23ms, zuwenig Zeit, um jedesmal ein rechenintensives Hauptprogramm dazwischen zu quetschen. Um SignalAussetzer zu vermeiden, muß dieser Puffer also vergrößert werden. Alternativ könnte natürlich auch die Abtastrate herabgesetzt werden, auf z.B 8kHz.
Die callback-routine sammelt immer mehrere chunks ein, hängt sie aneinander in einem lokalen Puffer und übergibt sie am Schluss an die Aussenwelt im globalen chunkbuffer. Durch diese doppelte Pufferung wird es möglich, unmittelbar nach Ablage des letzten Datenblockes in den chunkbuffer mit dem Einsammeln neuer Daten fort zu fahren.
Wenn der chunk-buffer gerade mal wieder voll ist, setzt die callback-Funktion das globale flag "DataReady". Dies wird vom Hauptprogramm gepollt. Je nach Größe des chunkbuffer hat das Hauptprogramm nun mehr oder weniger Zeit dies abzuarbeiten und das "DataReady" zu löschen.
Wenn zu Beginn eines neuen chunkbuffers das "DataReady" immer noch nicht gelöscht worden ist, erkennt die callback-routine auf ein nicht abgeholtes DatenPaket und gibt eine Bildschirmwarnung aus.
Um unnötige alsa-underrun-errors zu vermeiden, sollte der Datenstrom ein reiner input-stream geöffnet werden.
...und durch ein mitlaufendes Bandpassfilter schicken
Digitale Filter, berechnet von der CPU, dauern an dieser Stelle zu lange, um innerhalb der callback-Funktion abgearbeitet werden zu können. Wobei die Durchlaufzeit mit der FilterOrdnung deutlich zunimmt. Somit werden sämtlich array-Operationen und der Filter ins Hauptprogramm verlegt, wobei jeweils ein Block von chunks auf einmal abgearbeitet wird. Das zugehörige Demo-Programm „demo_butterworth_bandpass_filter.py“ gibt bei jedem callback-Aufruf den aktuellen Chunk-Schleifenzähler aus, dazwischen meldet sich das Hauptprogramm mit der Rechenzeit des Filters.
Ein "screenshot" aufgenommen während eines Programmlaufes, sieht z.B. so aus
Code:
callback buffer count =5
callback buffer count =4
callback buffer count =3
callback buffer count =2
callback buffer count =1
callback buffer count =0
callback buffer count =9
callback buffer count =8
callback buffer count =7
0.0205011367798
callback buffer count =6
callback buffer count =5
callback buffer count =4
callback buffer count =3
callback buffer count =2
callback buffer count =1
callback buffer count =0
callback buffer count =9
0.0198979377747
callback buffer count =8
callback buffer count =7
callback buffer count =6
callback buffer count =5
callback buffer count =4
callback buffer count =3
callback buffer count =2
callback buffer count =1
callback buffer count =0
callback buffer count =9
0.0199301242828
callback buffer count =8
callback buffer count =7
callback buffer count =6
callback buffer count =5
callback buffer count =4
Man erkennt die Rechenzeitausgaben des Hauptprogramms bei 20ms, eingesprenkelt in die callback Ausgaben des Puffer- Schleifenzählers. Übrigens erkennbar asynchron bezogen auf den callback-Schleifenzähler: Das Haupprogramm hängt teilweise mehrere callback-Durchläufe hinterher, bleibt dabei aber völlig im grünen Bereich.
Hi-Speed Mitlauffilter
Es hat sich herausgestellt, dass die Berechnung der Filterkoeffizienten um ein Vielfaches länger dauert als die eigentliche Filterung des Signales. Da diese Koeffizienten sich zur Laufzeit aber nicht ändern, spricht nichts dagegen, diese ein einziges mal am Programmanfang zu ermitteln. Der screenshot des modifizierten Programmes sieht nun folgendermaßen aus:
Code:
.
.
callback buffer count =1
callback buffer count =0
callback buffer count =9
0.000388860702515
callback buffer count =8
callback buffer count =7
callback buffer count =6
callback buffer count =5
callback buffer count =4
callback buffer count =3
callback buffer count =2
callback buffer count =1
callback buffer count =0
callback buffer count =9
0.000384092330933
callback buffer count =8
callback buffer count =7
callback buffer count =6
.
.
Die Filterberechnung dauert nur noch <0,4ms! Angesichts dieser Tatsache scheint Echtzeitfilterung mit direkter Monitor-Wiedergabe ja doch nicht so ganz ausgeschlossen.
Anhang Quelltext
Referenz : „python - How to implement band-pass Butterworth filter with Scipy.signal.butter - Stack Overflow-Dateien“
Code:
#! /usr/bin/python2.7
import pyaudio
import time
import sys
import numpy as np
from scipy.signal import butter, lfilter, freqz
# audio signal parameters
STREAM_CHANNELS = 2
STREAM_SAMPLE_RATE = 44100 #8000 #44100 #!!! sets time available for main program
STREAM_FORMAT = pyaudio.paFloat32
CHUNK_BUFFERS = 10 #!!! sets time available for main program
# bandpass filter parameters
BPLOWCUT = 900.0
BPHIGHCUT = 1100.0
BPORDER = 3
def GetFilterCoefficients():
global a, b
nyq = 0.5 * STREAM_SAMPLE_RATE
low = BPLOWCUT / nyq
high = BPHIGHCUT / nyq
b, a = butter(BPORDER, [low, high], btype='band')
return b, a
def BwBpFilter(data):
y = lfilter(b, a, data)
# y = lfilter(b, a, data, axis = (-1))
return y
def GetDefaultInputDevice():
CardId = p.get_default_input_device_info()["index"]
return CardId
def callback_2buffered (in_data, frame_count, time_info, status):
global RawData, DataReady, BufferCount, ChunkBuffer, FrameCount
# restart next data buffer
# data buffer = full
if BufferCount==0:
if DataReady==True:
print "!warning: 1 buffer may or may not have been lost!"
DataReady = True
# get first chunk
RawData = in_data
# save data = double buffered:
ChunkBuffer = RawData
BufferCount = CHUNK_BUFFERS
else:
# append one chunk
RawData = RawData + in_data
BufferCount = BufferCount - 1
FrameCount = frame_count
print "callback buffer count =%d"%BufferCount
return (in_data, pyaudio.paContinue)
# main program
print ""
print "full-duplex record/play through default soundcard"
print ""
p=pyaudio.PyAudio()
CardId=GetDefaultInputDevice()
print "Card id = %d"%CardId
#start with a full cycle of buffering & initialize RawData
BufferCount = 0
DataReady = False
InputStream = p.open(
format=STREAM_FORMAT,
channels=STREAM_CHANNELS,
rate = STREAM_SAMPLE_RATE,
input_device_index=CardId,
input=True,
output=False,
stream_callback=callback_2buffered)
InputStream.start_stream()
# wait for valid data to initialize arrays
GetFilterCoefficients()
time.sleep(2)
while InputStream.is_active():
if DataReady==True:
start = time.time()
NumArray = np.fromstring(ChunkBuffer, dtype=np.float32)
NumArrayRows = NumArray.size / STREAM_CHANNELS
#print "main loop, num array =%d"%NumArrayRows
NumArray = np.reshape(NumArray, (NumArrayRows, STREAM_CHANNELS))
NumArray = BwBpFilter(NumArray)
elapsed = time.time() - start
print elapsed
# send data done to callback
DataReady = False
#time.sleep(0.1)
InputStream.stop_stream()
InputStream.close()
p.terminate()