Raumakustik simulieren mit pyroomacoustics
In diesem Beispiel zeige ich, wie man mit Hilfe der Python Bibliothek pyroomacoustics 1 ein Double Bass Array (DBA) simulieren kann. Dazu wird zunächst ein Raum definiert, in dem dann die Lautsprecher und Mikrofone positioniert werden. Anschließend kann der Frequenzgang berechnet werden.
Das Ganze lässt sich gut in einem Jupyter-Notebook durchführen, da man die Frequenzgänge auch grafisch darstellen kann.
Als erstes müssen wir die Bibliothek mit pip install pyroomacoustics
installieren.
In Jupyter können wir dann mit einigen Imports beginnen
import numpy as np
import matplotlib.pyplot as plt
import pyroomacoustics as pra
Als nächstes definieren wir die Variablen für die Raumabmessungen. Diese werden später für die Positionierung der Lautsprecher benötigt. Außerdem legen wir eine Abtastfrequenz von 20000 fest. Damit lassen sich theoretisch Frequenzen bis 10 kHz beschreiben. Da wir hier Subwoofer simulieren wollen, reicht uns das.
width = 3.60
length = 8.27
height = 2.45
fs=20000
Um den Raum zu definieren, müssen wir auch angeben, wie stark die Wände dämpfen. Ich fand es hier am einfachsten, dies über einen voreingestellten RT60-Wert zu definieren. In der Software REW (Room-EQ-Wizard) wird dies über die “Surface Absorption” eingestellt. Für unseren Raum nehmen wir einen RT60 Wert von 0,5 Sekunden an.
rt60 = 0.5
e_absorption, max_order = pra.inverse_sabine(rt60, [width, length, height])
Danach definieren wir den Raum
room = pra.ShoeBox(
[width,length,height],
fs = fs,
materials = pra.Material(e_absorption),
max_order = max_order,
ray_tracing = True
)
Für die Simulation benötigen wir ein Testsignal, das von den Lautsprechern ausgegeben wird. Dazu erzeugen wir einen einfachen Sweep, der unsere Abtastfrequenz abdeckt. Deshalb geben wir keine obere Frequenz an.
sweep = pra.experimental.signals.linear_sweep(
2.0, # Dauer in Sekunden
fs, # Samplingfrequenz
f_lo = 0.0, # Untere Grenzfrequenz
f_hi = None, # Obere Grenzfrequenz
fade = None, # Dauer zum Ein- und Ausblenden
ascending = True # Frequenzen Aufsteigend
)
Die hinteren Subwoofer müssen verzögert, invertiert und gedämpft angesteuert werden. Um zu invertieren, setzen wir einfach das Minuszeichen vor die Variable sweep. Um einen Dämpfungsfaktor in dB angeben zu können, müssen wir diesen linear umrechnen. Dazu verwenden wir die Formel 10^(dB / 20)
.
db = -1.2
factor = 10 ** (db / 20)
isweep = -sweep * factor
Jetzt können wir die Subwoofer hinzufügen. Wir simulieren einen DBA, der vorne und hinten ein 2x2-Gitter hat. Diese werden auf 1/4 und 3/4 der Raumbreite und Raumhöhe platziert. Zusätzlich sind unsere virtuellen Gehäuse 30cm tief, also müssen wir sie so weit von der Wand entfernt platzieren. Das rückwärtige Signal wird um die Raumlänge in Millisekunden verzögert. Also Raumlänge in Meter geteilt durch 343 m/s Schallgeschwindigkeit.
# Vorne
room.add_source([width/4, 0.30, height/4], signal=sweep, delay=0.0)
room.add_source([width/4*3, 0.30, height/4], signal=sweep, delay=0.0)
room.add_source([width/4, 0.30, height/4*3], signal=sweep, delay=0.0)
room.add_source([width/4*3, 0.30, height/4*3], signal=sweep, delay=0.0)
# Hinten
room.add_source([width/4, length-0.30, height/4], signal=isweep, delay=length/343)
room.add_source([width/4*3, length-0.30, height/4], signal=isweep, delay=length/343)
room.add_source([width/4, length-0.30, height/4*3], signal=isweep, delay=length/343)
room.add_source([width/4*3, length-0.30, height/4*3], signal=isweep, delay=length/343)
Wir stellen 3 Mikrofone im Raum um den Hörplatz auf
mic_locs = np.c_[
[1.8, 3.5, 1.0], # mic 1
[1.3, 4.5, 0.8], # mic 2
[2.4, 3.0, 1.2], # mic 3
]
room.add_microphone_array(mic_locs)
Jetzt können wir den Raum einmal grafisch darstellen lassen. Die Dimensionen sollten so groß gewählt werden, dass der gesamte Raum dargestellt werden kann. Wenn alle Grenzen auf den gleichen Wert gesetzt werden, wird der Raum perspektivisch besser dargestellt.
fig, ax = room.plot()
ax.set_xlim([0, 9])
ax.set_ylim([0, 9])
ax.set_zlim([0, 9])
Die Simulation wird mit folgendem Befehl gestartet
recordings = room.simulate(return_premix=True, recompute_rir=True)
Wir bekommen ein Array zurück, das für jede Mikrofonposition jeden Subwoofer einzeln misst. Das interessiert uns im Moment nicht, kann aber für weitere Analysen verwendet werden. Wir wollen die gesamte Mischung an den Mikrofonen haben und darstellen. Dazu definieren wir uns zunächst eine Funktion zur Darstellung der Frequenzgänge.
def plot(*audios, freq_range=(20,500), mag_range=None, title=None, filename=None):
fig = plt.figure(figsize=(10,5))
ax1 = fig.add_subplot()
for audio in audios:
X = np.fft.rfft(audio)
freqs = np.arange(len(X)) / len(audio) * fs
plt.semilogx(freqs, pra.dB(X), linewidth=1)
ticks = np.array([10, 20, 50, 80, 100, 200, 300, 500, 1000, 2000, 5000, 10000, 20000])
tick_labels = np.array(["10", "20", "50", "80", "100", "200", "300", "500", "1k", "2k", "5k", "10k", "20k"])
filter_ticks = (freq_range[0] <= ticks) & (ticks <= freq_range[1])
plt.xlabel("Frequency [Hz]")
ax1.set_xlim(freq_range[0], freq_range[1])
if mag_range is not None:
ax1.set_ylim(mag_range[0], mag_range[1])
ax1.set_xticks(ticks[filter_ticks])
ax1.set_xticklabels(tick_labels[filter_ticks], color="xkcd:navy blue")
ax1.set_ylabel("Amplitude [dB]")
ax1.grid(True, which="both", axis="both", linestyle="-", linewidth=0.5, color=(0.8, 0.8, 0.8))
if title is not None:
ax1.set_title(title)
plt.tight_layout()
if filename is not None:
plt.savefig(filename)
plt.show()
Danach rufen wir sie für alle Positionen auf
plot(*room.mic_array.signals, freq_range=(20,400), mag_range=(20,70), title='2x2 DBA')
In diesem Fall sieht der Frequenzgang bis ca. 180 Hz sehr gut aus. Man kann aber sicher noch etwas Feintuning betreiben, indem man die Dämpfung des hinteren Arrays anpasst.
Das schöne an dieser Methode ist, dass man beliebig viele Subwoofer mit beliebig vielen Messpositionen simulieren kann. Also viel Spaß damit.