Pyroomacoustics MIMO RIR interoperability recipe¶
Tutorial goal
Convert Pyroomacoustics-style room impulse responses to the MIMO Markov tensor shape used by lattice-dsp.
Note
New to the terminology? See the lattice DSP concept map and the causality/data-use guide for how online, offline, block, and MIMO examples should be read.
Context¶
Room-acoustics simulators can produce one impulse response for each microphone/source pair. This recipe keeps Pyroomacoustics outside the package dependency tree while documenting the data convention used to bring those paths into MIMO block-Hankel reduction.
Key idea and equations¶
The mapping is
room.rir[microphone][source][tap]
-> markov[tap, microphone, source]
How to read the result¶
Use the printed shape and relative Markov-response error to verify the conversion and reduced MIMO model.
Run command¶
python examples/pyroomacoustics_mimo_rir_recipe.py
Source code¶
1"""Recipe: use Pyroomacoustics-style MIMO room impulse responses.
2
3This example is dependency-free: it does not import Pyroomacoustics. Instead it
4uses a small fake object with the same ``room.rir[mic][source]`` structure used
5by Pyroomacoustics after room impulse responses are computed.
6
7In a real Pyroomacoustics script, build the room and compute its RIRs there, then
8pass the resulting room object to ``markov_from_pyroom_rir`` below.
9"""
10
11from __future__ import annotations
12
13import numpy as np
14
15import lattice_dsp as ld
16
17
18def markov_from_pyroom_rir(room, n_markov: int | None = None, dtype=float) -> np.ndarray:
19 """Convert ``room.rir[mic][source]`` impulse responses to a MIMO Markov tensor.
20
21 Parameters
22 ----------
23 room:
24 Any object with a Pyroomacoustics-style ``rir`` attribute.
25 n_markov:
26 Optional number of taps to keep. If omitted, the longest RIR length is
27 used and shorter channels are zero-padded.
28 dtype:
29 Output dtype.
30
31 Returns
32 -------
33 numpy.ndarray
34 Array with shape ``(n_taps, n_mics, n_sources)``.
35 """
36
37 n_outputs = len(room.rir)
38 if n_outputs == 0:
39 raise ValueError("room.rir must contain at least one microphone")
40 n_inputs = len(room.rir[0])
41 if n_inputs == 0:
42 raise ValueError("room.rir[0] must contain at least one source")
43
44 lengths = [len(room.rir[y][u]) for y in range(n_outputs) for u in range(n_inputs)]
45 n_taps = max(lengths) if n_markov is None else int(n_markov)
46 if n_taps <= 0:
47 raise ValueError("n_markov must be positive")
48
49 markov = np.zeros((n_taps, n_outputs, n_inputs), dtype=dtype)
50 for y in range(n_outputs):
51 if len(room.rir[y]) != n_inputs:
52 raise ValueError("all microphones must have the same number of source RIRs")
53 for u in range(n_inputs):
54 h = np.asarray(room.rir[y][u], dtype=dtype)
55 n = min(n_taps, h.size)
56 markov[:n, y, u] = h[:n]
57 return markov
58
59
60class FakePyroomRoom:
61 """Small stand-in for the Pyroomacoustics ``room.rir`` data structure."""
62
63 def __init__(self, rir):
64 self.rir = rir
65
66
67def damped_room_path(delay: int, gain: float, decay: float, n_taps: int) -> np.ndarray:
68 """Create a simple delayed, damped synthetic room path for the recipe."""
69
70 h = np.zeros(n_taps)
71 tail = gain * decay ** np.arange(n_taps - delay)
72 h[delay:] = tail
73 return h
74
75
76def make_fake_room(n_taps: int = 96) -> FakePyroomRoom:
77 """Create a 2-microphone, 2-source nested RIR list."""
78
79 rir = [
80 [
81 damped_room_path(delay=3, gain=0.95, decay=0.91, n_taps=n_taps),
82 damped_room_path(delay=8, gain=0.35, decay=0.88, n_taps=n_taps),
83 ],
84 [
85 damped_room_path(delay=6, gain=0.42, decay=0.89, n_taps=n_taps),
86 damped_room_path(delay=2, gain=0.82, decay=0.92, n_taps=n_taps),
87 ],
88 ]
89 return FakePyroomRoom(rir)
90
91
92def main() -> None:
93 room = make_fake_room()
94 markov = markov_from_pyroom_rir(room, n_markov=96)
95
96 result = ld.finite_hankel_reduce_mimo(
97 markov,
98 reduced_order=4,
99 block_rows=16,
100 block_cols=16,
101 )
102 approx = ld.mimo_state_space_markov_response(
103 result["A"], result["B"], result["C"], result["D"], markov.shape[0]
104 )
105
106 relative_error = float(np.linalg.norm(markov - approx) / np.linalg.norm(markov))
107 hsv = np.asarray(result["hankel_singular_values"])
108
109 print("converted shape:", markov.shape)
110 print("mapping: markov[tap, microphone, source]")
111 print("reduced order:", result["order"])
112 print("stable reduced model:", bool(result["stable"]))
113 print("leading block-Hankel singular values:", np.round(hsv[:6], 6).tolist())
114 print("relative Markov-response error:", f"{relative_error:.3e}")
115
116 print("\nReal Pyroomacoustics usage sketch:")
117 print(" # import pyroomacoustics as pra")
118 print(" # room = pra.ShoeBox(...); room.add_source(...); room.add_microphone_array(...)")
119 print(" # room.compute_rir()")
120 print(" # markov = markov_from_pyroom_rir(room, n_markov=512)")
121
122
123if __name__ == "__main__":
124 main()