In letzter Zeit nimmt die Anzahl der Distributoren zu, die 3D-Modelle wie VTuber verwenden. Viele Verteiler verwenden akustische Einkanalinformationen für die Verteilung. Um die Verteilung jedoch immersiver zu gestalten, wird mithilfe eines Mikrofonarrays wie 3Dio stereoskopischer Klang erzeugt. Wenn mehrere Schallquellen vorhanden sind, wird empfohlen, ein solches Mikrofonarray zu verwenden. In einer Situation wie ASMR kann jedoch durch Signalverarbeitung stereoskopischer Schall erzeugt werden, wenn eine einzelne Schallquelle (Person) zu stereoskopischem Schall verarbeitet wird. Natürlich sind mehrere Schallquellen möglich, aber ...
Wichtig ist hierbei, dass der Mikrofonteil des für ASMR verwendeten Mikrofonarrays wie 3Dio, bei dem es sich um die Kopfübertragungsfunktion handelt, die Form eines Ohrs hat. Dies liegt daran, dass es die Übertragung von Schall von der Schallquelle zum Trommelfell des Ohrs nachahmt. Diese Übertragungsfunktion wird als Kopfübertragungsfunktion (HRTF) bezeichnet. In diesem Artikel werde ich beschreiben, wie ein System implementiert wird, das stereophonen Klang durch Signalverarbeitung mit HRTF für das rechte und linke Ohr in Python erzeugt.
Hinweis: HRTFs variieren von Person zu Person, und Schätzungen der Tonbildposition vorher und nachher sind häufig inkonsistent. Selbst wenn 3Dio usw. verwendet wird, ahmt es nur die Standardohrform nach und kann keine individuellen Unterschiede lösen.
Da das Programm lang ist, werde ich nur die Gliederung erklären. Weitere Informationen finden Sie im folgenden Git-Programm. Außerdem wird in git beschrieben, wie man es benutzt. Git: k-washi/stereophonic-Sound-System
Informationen zum Protokollieren, Lesen der Einstellungsdatei und zur gRPC-Kommunikation zum Abrufen der im Programm beschriebenen Standortinformationen finden Sie im folgenden Qiita-Artikel.
Bitte klicken Sie auf das Bild unten. Sie können den dreidimensionalen Klang hören, der tatsächlich durch Verknüpfen auf Youtube erzeugt wird. Da das Mikrofon an den Mac angeschlossen ist, ist der Originalton schlecht, aber Sie können sehen, dass es sich um einen dreidimensionalen Ton handelt.
pip install PyAudio==0.2.11
pip install grpcio-tools
pip install numpy
Hier wird die HRTF-Datenbank gelesen, in den Frequenzbereich konvertiert und mit pickle gespeichert.
Für die HRTF wurden HRTF-Daten (2) der Nagoya University HRTF verwendet.
Das Programm wird unten beschrieben. Die Details werden weggelassen als ... Das vollständige Programm finden Sie unter acoustic / spacialSound.py.
acoustic/spacialSound.py
...
class HRTF():
def __init__(self):
...
def checkModel(self):
#Modelldateinamen abrufen
...
self.getModelNameList()
...
def getModelNameList(self):
#Analysieren Sie den Namen der HRTF-Datei für jeden Azimut und jede Höhe
...
def openData(self, path, dataType = np.int16):
#H jede Richtung,Die Anzahl der Abtastungen des HRTF-Signals im Elevationswinkel beträgt 512 Punkte, und diese Daten werden gelesen und in das Numpy-Format konvertiert.
with open(path, 'r') as rData:
temp = rData.read().split("\n")
data = []
for item in temp:
if item != '':
data.append(float(item))
return np.array(data)
def convHRTF2Np(self):
#Lesen Sie die HRTF-Daten für jede Ausrichtung und Höhe sowie 512 Punkte basierend auf der Überlappungs-Add-Methode+0 füllen(512 Punkte)Ist FFT.(FFT wird im nächsten Kapitel beschrieben)
#Die obige FFT wird an beiden Ohren durchgeführt und alle Daten werden durch Pickle gespeichert. Die Funktion saveData wird zum Speichern verwendet.
#Zum Lesen können die gespeicherten Pickle-Daten mit readData gelesen werden.
for e, _ in enumerate(self.elev):
LaziData = []
RaziData = []
for a, _ in enumerate(self.azimuth[e]):
Lpath = self.Lpath[e][a]
Rpath = self.Rpath[e][a]
Ldata = spec.overlapAdderFFT(self.openData(Lpath))
Rdata = spec.overlapAdderFFT(self.openData(Rpath))
LaziData.append(Ldata)
RaziData.append(Rdata)
self.hrtf[self.left].append(LaziData)
self.hrtf[self.right].append(RaziData)
self.saveData(self.hrtf, Conf.HRTFpath)
self.saveData(self.elev, Conf.Elevpath)
self.saveData(self.azimuth, Conf.Azimuthpath)
def saveData(self, data, path):
#Pickle-Datenspeicherung
try:
with open(path, 'wb') as hrtf:
pickle.dump(data, hrtf)
except Exception as e:
logger.critical(e)
def readData(self, path):
#Lesen von Pickle-Daten
try:
with open(path, 'rb') as hrtf:
data = pickle.load(hrtf)
except Exception as e:
logger.critical(e)
return data
Hier wird die Implementierung in Bezug auf FFT beschrieben.
Weitere Informationen finden Sie unter acoustic / acousticSignalProc.py
Bei der Verarbeitung akustischer Informationen ist eine Faltungsintegration von HRTF und Mikrofoneingang erforderlich. Bei der Verarbeitung mit einem rohen akustischen Signal nimmt die Verarbeitung jedoch Zeit in Anspruch und die Daten sind schwierig zu verarbeiten. Daher ist eine Fourier-Transformation erforderlich, die das Signal im Frequenzbereich in Informationen umwandelt. FFT ist diejenige, die diese Fourier-Transformation mit hoher Geschwindigkeit durchführt.
Die OverLap Add-Methode (OLA) wird häufig verwendet, wenn die HRTF mit dem Mikrofoneingang gefaltet wird. Wenn beispielsweise die Anzahl der Abtastwerte für HRTF und Mikrofoneingang 512 Punkte beträgt, werden zusätzlich zu den Daten 512 Punkte mit 0 gefüllt. Das heißt, 512 + 512 (0) Daten werden erstellt. Danach wird die positive Frequenz von FFT durch die rfft-Funktion von numpy berechnet. Im Fall von fft of numpy werden 512/2 = 256 Punkte positiver Frequenzkomponenten und 256 Punkte negativer Frequenzkomponenten berechnet. Rfft wird jedoch in vielen technischen Anwendungen verwendet, da häufig nur positive Frequenzkomponenten ausreichen. Obwohl die Berechnungsalgorithmen von numpys rfft und fft unterschiedlich sind, ist der Fehler des Ergebnisses ziemlich klein, so dass diesmal rfft verwendet wird. Nach dem Ausführen von FFT werden dann HRTF und Mikrofoneingang für jede Frequenz multipliziert. Diese Kombination wird im nächsten Kapitel erläutert.
Im folgenden Programm werden zwei FFTs, OverlapAdderFFT und SpacializeFFT, vorbereitet. Was ist der Unterschied? Gibt an, ob das Fenster (Fensterfunktion) multipliziert wird. Da die Fensterfunktion die Periodizität des durch die Fourier-Transformation ausgeschnittenen Bereichs annimmt, wird eine Funktion, die das Ende kleiner macht, auf die Daten angewendet, so dass die Enden verbunden werden. HRTF hat jedoch nur 512 Punkte pro Daten. Wenn die Fensterfunktion angewendet wird, können die Originaldaten nicht wiederhergestellt werden, sodass sie ohne Anwendung der Fensterfunktion verwendet werden. Andererseits hat der Mikrofoneingang eine Fensterfunktion. Wie im nächsten Kapitel erläutert wird, gehen bei Anwendung der Fensterfunktion die Endinformationen verloren, sodass alle Daten beim Verschieben um 128 Punkte verwendet werden.
acoustic/acousticSignalProc.py
import pyaudio
import numpy as np
class SpectrogramProcessing():
def __init__(self, freq = Conf.SamplingRate):
self.window = np.hamming(Conf.SysChunk)
self.overlapFreq = np.fft.rfftfreq(Conf.SysChunk * 2, d=1./freq)
self.overlapData = np.zeros((int(Conf.SysChunk * 2)), dtype = np.float32)
def overlapAdderFFT(self, data):
#Füllen Sie 0 und FFT aus
self.overlapData[:Conf.SysChunk] = data
return np.fft.rfft(self.overlapData)
def spacializeFFT(self, data):
#Füllen Sie 0 aus und wenden Sie das Hanning-Fenster an.
self.overlapData[:Conf.SysChunk] = data * self.window
return np.fft.rfft(self.overlapData)
def ifft(self, data):
#in: chanel_num x freq num (if 1.6kHz, 0,...,7984.375 Hz)
#out: chanel_num x frame num(Conf.SysChunk = 512)
return np.fft.irfft(data)
Als nächstes wird das tatsächliche Mikrofoneingabe-, -ausgabe- und -konvertierungsprogramm für stereophonen Klang beschrieben. Die Verarbeitung erfolgt unter Verwendung der oben beschriebenen Funktionen und dergleichen. Bezüglich der Einstellungen und des gRPC verweisen wir bitte auf meine früheren Artikel, wie am Anfang erwähnt.
acoustic/audioStreamOverlapAdder.py
...
from acoustic.acousticSignalProc import AudioDevice, SpectrogramProcessing, WaveProcessing, convNp2pa, convPa2np
from acoustic.spacialSound import spacialSound
# ------------
import pyaudio
import numpy as np
import time
class MicAudioStream():
def __init__(self):
self.pAudio = pyaudio.PyAudio()
self.micInfo = AudioDevice(Conf.MicID)
self.outInfo = AudioDevice(Conf.OutpuID)
#Verarbeitung der Einschränkung von Ausgabegeräten
if self.outInfo.micOutChannelNum < 2:
self.left = 0
self.right = 0
else:
self.left = 0
self.right = 1
if self.outInfo.micOutChannelNum > 2:
self.outInfo.micChannelNum = 2
logger.info("Ich habe es auf 2 Kanäle begrenzt, weil die Anzahl der Ausgangsmikrofone zu hoch ist.")
self.startTime = time.time()
#Derzeit wird nur eine Bitbreite von 16 Bit unterstützt.(Weil wir die Operation in anderen Fällen nicht bestätigt haben)
if Conf.SysSampleWidth == 2:
self.format = pyaudio.paInt16
self.dtype = np.int16
else:
logger.critical("Wird derzeit nicht unterstützt")
exec(-1)
self.fft = SpectrogramProcessing()
#Wenn Sie jedes Mal Daten im Numpy-Array-Format erstellen und die Speicherzuweisung ausführen, dauert dies einige Zeit. Erstellen Sie sie daher im Voraus.
self.data = np.zeros((int(Conf.StreamChunk * 2)), dtype=self.dtype)
self.npData = np.zeros((int(Conf.StreamChunk * 2)) , dtype=self.dtype)
self.overlapNum = int(Conf.StreamChunk / Conf.SysFFToverlap)
self.freqData = np.zeros((self.overlapNum, self.outInfo.micOutChannelNum, self.fft.overlapFreq.shape[0]), dtype=np.complex)
self.convFreqData = np.zeros((self.outInfo.micOutChannelNum, int(Conf.StreamChunk*3)) , dtype=self.dtype)
self.outData = np.zeros((self.outInfo.micOutChannelNum * Conf.StreamChunk), dtype=self.dtype)
self.Aweight = self.fft.Aweight() #Das A-Merkmal wird angewendet, aber da es sich kaum geändert hat, besteht kein Grund zur Sorge. (Kann gelöscht werden)
#Anfangswert der Positionsinformationen
self.x = 0.2
self.y = 10
self.z = 0.2
#HRTF-Lesung (akustisch/spacialSound.py)
#Sie können den Prozess der Rückgabe von HRTF für die Positionsinformationen ausführen.
self.hrft = spacialSound()
#Bei der Aufnahme von stereophonem Ton
if Conf.Record:
#test/listOrNumpy.Geschwindigkeitsvergleich mit py
#Für das Array-Format von numpy ist es schneller, numpy in eine Liste zu konvertieren und eine Liste zu erweitern, als Array-Formate zu kombinieren.
self.recordList = []
def spacialSoundConvering(self, freqData):
#Gibt HRTF für Position zurück
lhrtf, rhrtf = self.hrft.getHRTF(self.x, self.y, self.z)
#Die Eingangsdaten des HRTF-Mikrofons werden wie unten gezeigt gefaltet, um stereophonen Klang zu erzeugen.
freqData[self.left] = freqData[self.left] * lhrtf
freqData[self.right] = freqData[self.right] * rhrtf
return freqData * self.Aweight
def callback(self, in_data, frame_count, time_info, status):
#Eine Funktion, die Sounddaten bei der Stream-Verarbeitung von pyAudio verarbeitet.
#in_Daten werden eingegeben, Rückgabe erfolgt nicht_Tondaten werden als Daten ausgegeben.
if time.time() - self.startTime > Conf.SysCutTime:
#Konvertieren der Eingabe im pyAudio-Format in das Numpy-Format.
self.npData[Conf.StreamChunk:] = convPa2np(np.fromstring(in_data, self.dtype), channelNum=self.micInfo.micChannelNum)[0, :] #ch1 input
#Überlappen Sie die Breite der Daten unten(128)Erzeugen Sie dreidimensionalen Klang, während Sie ihn nacheinander verschieben.
for i in range(self.overlapNum):
#512 Punkte(SysChunk)FFT wird mit der Breite von durchgeführt.
self.freqData[i, :, :] = self.fft.spacializeFFT(self.npData[Conf.SysFFToverlap * i : Conf.SysChunk + Conf.SysFFToverlap * i])
#Der HRTF- und der Mikrofoneingang sind gefaltet.
self.freqData[i, :, :] = self.spacialSoundConvering(self.freqData[i])
#Der Frequenzbereich wird durch inverse Fourier-Transformation in den Zeitbereich umgewandelt.
self.convFreqData[:, Conf.SysFFToverlap * i : Conf.SysChunk * 2 + Conf.SysFFToverlap * i] += self.fft.ifft(self.freqData[i]).real.astype(self.dtype)#[:,:Conf.SysChunk]
#Konvertieren vom Numpy-Format in das pyAudio-Ausgabeformat.
self.outData[:] = convNp2pa(self.convFreqData[:,:Conf.StreamChunk])
#Die Entfernungsdämpfung des Schalls wird berechnet. Außerdem ist der Ton zu laut, daher teile ich ihn durch SysAttenuation.
self.outData[:] = self.hrft.disanceAtenuation(self.outData[:], self.x, self.y, self.z) / Conf.SysAttenuation
if Conf.Record:
self.recordList += self.outData.tolist()
#Initialisieren Sie für den nächsten Mikrofoneingang
self.npData[:Conf.StreamChunk] = self.npData[Conf.StreamChunk:]
self.convFreqData[:, :Conf.StreamChunk*2] = self.convFreqData[:, Conf.StreamChunk:]
self.convFreqData[:,Conf.StreamChunk*2:] = 0
#In das Datenausgabeformat im pyAudio-Format konvertieren
out_data = self.outData.tostring()
return (out_data, pyaudio.paContinue)
def start(self):
#Stellen Sie das Eingabe- / Ausgabegerät und das Format auf das folgende Format ein und starten Sie die Verarbeitung.
"""
rate – Sampling rate
channels – Number of channels
format – Sampling size and format. See PortAudio Sample Format.
input – Specifies whether this is an input stream. Defaults to False.
output – Specifies whether this is an output stream. Defaults to False.
input_device_index – Index of Input Device to use. Unspecified (or None) uses default device. Ignored if input is False.
output_device_index – Index of Output Device to use. Unspecified (or None) uses the default device. Ignored if output is False.
frames_per_buffer – Specifies the number of frames per buffer.
start – Start the stream running immediately. Defaults to True. In general, there is no reason to set this to False.
input_host_api_specific_stream_info – Specifies a host API specific stream information data structure for input.
output_host_api_specific_stream_info – Specifies a host API specific stream information data structure for output.
stream_callback –Specifies a callback function for non-blocking (callback) operation. Default is None, which indicates blocking operation (i.e., Stream.read() and Stream.write()). To use non-blocking operation, specify a callback that conforms to the following signature:
callback(in_data, # recorded data if input=True; else None
frame_count, # number of frames
time_info, # dictionary
status_flags) # PaCallbackFlags
time_info is a dictionary with the following keys: input_buffer_adc_time, current_time, and output_buffer_dac_time; see the PortAudio documentation for their meanings. status_flags is one of PortAutio Callback Flag.
The callback must return a tuple:
(out_data, flag)
out_data is a byte array whose length should be the (frame_count * channels * bytes-per-channel) if output=True or None if output=False. flag must be either paContinue, paComplete or paAbort (one of PortAudio Callback Return Code). When output=True and out_data does not contain at least frame_count frames, paComplete is assumed for flag.
"""
self.stream = self.pAudio.open(
format = self.format,
rate = Conf.SamplingRate,#self.micInfo.samplingRate,
channels = self.micInfo.micChannelNum,
input = True,
output = True,
input_device_index = Conf.MicID,
output_device_index = Conf.OutpuID,
stream_callback = self.callback,
frames_per_buffer = Conf.StreamChunk
)
self.stream.start_stream()
def stop(self):
#Die notwendige Verarbeitung wird getrennt von der Tonverarbeitung ausgeführt. Schließlich wird der Schließvorgang auch ausgeführt, wenn das System heruntergefahren wird.
#Hier wird gRPC verwendet, um die Positionsinformationen der Schallquelle zu aktualisieren.
from proto.client import posClient
grpcPosGetter = posClient()
grpcPosGetter.open()
while self.stream.is_active():
time.sleep(0.1)
try:
ok = grpcPosGetter.posRequest()
if ok:
self.x, self.y, self.z = grpcPosGetter.getPos()
except Exception as e:
logger.error("pos getter error {0}".format(e))
if time.time() - self.startTime > Conf.RecordTime + Conf.SysCutTime:
break
if Conf.Record:
record = WaveProcessing()
record.SaveFlatteData(self.recordList, channelNum=self.outInfo.micOutChannelNum)
self.stream.start_stream()
self.stream.close()
self.close()
grpcPosGetter.close()
def close(self):
self.pAudio.terminate()
logger.debug("Close proc")
exit(0)
if __name__ == "__main__":
st = MicAudioStream()
st.start()
try:
pass
finally:
st.stop()
Um die Tonverarbeitung tatsächlich zu testen, muss die ID des Eingabe- / Ausgabegeräts bestätigt werden.
Beispiel für Geräteinformationen
2020-01-16 03:46:49,436 [acousticSignalProc.py:34] INFO Index: 0 | Name: Built-in Microphone | ChannelNum: in 2 out 0 | SampleRate: 44100.0
2020-01-16 03:46:49,436 [acousticSignalProc.py:34] INFO Index: 1 | Name: Built-in Output | ChannelNum: in 0 out 2 | SampleRate: 44100.0
2020-01-16 03:46:49,436 [acousticSignalProc.py:34] INFO Index: 2 | Name: DisplayPort | ChannelNum: in 0 out 2 | SampleRate: 48000.0
...
Das folgende Beispiel zeigt ein Programm, das Informationen zu Eingabe- / Ausgabegeräten mit PyAudio ausgibt.
acoustic/acousticSignalProc.py
...
import pyaudio
import numpy as np
class AudioDevice():
def __init__(self, devId = Conf.MicID):
self.pAudio = pyaudio.PyAudio()
self.setAudioDeviceInfo(devId)
self.samplingRate = Conf.SamplingRate
def getAudioDeviceInfo(self):
#Geräteinformationen mit PyAudio ausgeben.
for i in range(self.pAudio.get_device_count()):
tempDic = self.pAudio.get_device_info_by_index(i)
text = 'Index: {0} | Name: {1} | ChannelNum: in {2} out {3} | SampleRate: {4}'.format(tempDic['index'], tempDic['name'], tempDic['maxInputChannels'], tempDic['maxOutputChannels'], tempDic['defaultSampleRate'])
logger.info(text)
def setAudioDeviceInfo(self, micId = 0):
#Überprüfen Sie, ob die eingestellte Geräte-ID vorhanden ist, und behalten Sie die Informationen dieser ID bei
micInfoDic = {}
for i in range(self.pAudio.get_device_count()):
micInfoDic = self.pAudio.get_device_info_by_index(i)
if micInfoDic['index'] == micId:
self.micName = micInfoDic['name']
self.micChannelNum = micInfoDic['maxInputChannels']
self.micOutChannelNum = micInfoDic['maxOutputChannels']
self.micSamplingRate = int(micInfoDic['defaultSampleRate'])
text = 'Set Audio Device Info || Index: {0} | Name: {1} | ChannelNum: {2}, {3} | SampleRate: {4}'.format(micId, self.micName, self.micChannelNum,self.micOutChannelNum, self.micSamplingRate)
logger.info(text)
if self.micChannelNum > 2:
logger.critical("Es werden 3 oder mehr Mikrofoneingänge nicht unterstützt.")
exit(-1)
break
if self.pAudio.get_device_count() == i + 1:
logger.critical("Es gibt kein entsprechendes ID-Mikrofon.")
Das Obige ist die Erklärung des dreidimensionalen Soundsystems mit Python. Es kann kompliziert sein, weil einige Teile weggelassen wurden und auf andere Artikel verwiesen werden muss, aber ich hoffe, dass es hilfreich sein wird. Es wird gesagt, dass Python langsam ist, aber wenn Sie numpy effizient verwenden, z. B. indem Sie Speicher im Voraus zuweisen, kann es häufig mit ausreichender Geschwindigkeit ausgeführt werden. Ich habe verschiedene andere Artikel geschrieben, bitte beziehen Sie sich auf sie.
Recommended Posts