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])

room

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')

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.