-
Notifications
You must be signed in to change notification settings - Fork 419
/
room.py
2752 lines (2196 loc) · 97.5 KB
/
room.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Main Room class using to encapsulate the room acoustics simulator
# Copyright (C) 2019 Robin Scheibler, Ivan Dokmanic, Sidney Barthe, Cyril Cadoux
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# You should have received a copy of the MIT License along with this program. If
# not, see <https://opensource.org/licenses/MIT>.
r"""
Room
====
The three main classes are :py:obj:`pyroomacoustics.room.Room`,
:py:obj:`pyroomacoustics.soundsource.SoundSource`, and
:py:obj:`pyroomacoustics.beamforming.MicrophoneArray`. On a high level, a
simulation scenario is created by first defining a room to which a few sound
sources and a microphone array are attached. The actual audio is attached to
the source as raw audio samples.
Then, a simulation method is used to create artificial room impulse responses
(RIR) between the sources and microphones. The current default method is the
image source which considers the walls as perfect reflectors. An experimental
hybrid simulator based on image source method (ISM) [1]_ and ray tracing (RT) [2]_, [3]_, is also available. Ray tracing
better capture the later reflections and can also model effects such as
scattering.
The microphone signals are then created by convolving audio samples associated
to sources with the appropriate RIR. Since the simulation is done on
discrete-time signals, a sampling frequency is specified for the room and the
sources it contains. Microphones can optionally operate at a different sampling
frequency; a rate conversion is done in this case.
Simulating a Shoebox Room with the Image Source Model
-----------------------------------------------------
We will first walk through the steps to simulate a shoebox-shaped room in 3D.
We use the ISM is to find all image sources up to a maximum specified order and
room impulse responses (RIR) are generated from their positions.
The code for the full example can be found in `examples/room_from_rt60.py`.
Create the room
~~~~~~~~~~~~~~~
So-called shoebox rooms are pallelepipedic rooms with 4 or 6 walls (in 2D and
3D respectiely), all at right angles. They are defined by a single vector that
contains the lengths of the walls. They have the advantage of being simple to
define and very efficient to simulate. In the following example, we define a
``9m x 7.5m x 3.5m`` room. In addition, we use `Sabine's formula <https://en.wikipedia.org/wiki/Reverberation>`_
to find the wall energy absorption and maximum order of the ISM required
to achieve a desired reverberation time (*RT60*, i.e. the time it takes for
the RIR to decays by 60 dB).
.. code-block:: python
import pyroomacoustics as pra
# The desired reverberation time and dimensions of the room
rt60 = 0.5 # seconds
room_dim = [9, 7.5, 3.5] # meters
# We invert Sabine's formula to obtain the parameters for the ISM simulator
e_absorption, max_order = pra.inverse_sabine(rt60, room_dim)
# Create the room
room = pra.ShoeBox(
room_dim, fs=16000, materials=pra.Material(e_absorption), max_order=max_order
)
The second argument is the sampling frequency at which the RIR will be
generated. Note that the default value of ``fs`` is 8 kHz.
The third argument is the material of the wall, that itself takes the absorption as a parameter.
The fourth and last argument is the maximum number of reflections allowed in the ISM.
.. note::
Note that Sabine's formula is only an approximation and that the actually
simulated RT60 may vary by quite a bit.
.. warning::
Until recently, rooms would take an ``absorption`` parameter that was
actually **not** the energy absorption we use now. The ``absorption``
parameter is now deprecated and will be removed in the future.
Add sources and microphones
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Sources are fairly straightforward to create. They take their location as single
mandatory argument, and a signal and start time as optional arguments. Here we
create a source located at ``[2.5, 3.73, 1.76]`` within the room, that will utter
the content of the wav file ``speech.wav`` starting at ``1.3 s`` into the
simulation. The ``signal`` keyword argument to the
:py:func:`~pyroomacoustics.room.Room.add_source` method should be a
one-dimensional ``numpy.ndarray`` containing the desired sound signal.
.. code-block:: python
# import a mono wavfile as the source signal
# the sampling frequency should match that of the room
from scipy.io import wavfile
_, audio = wavfile.read('speech.wav')
# place the source in the room
room.add_source([2.5, 3.73, 1.76], signal=audio, delay=1.3)
The locations of the microphones in the array should be provided in a numpy
``nd-array`` of size ``(ndim, nmics)``, that is each column contains the
coordinates of one microphone. Note that it can be different from that
of the room, in which case resampling will occur. Here, we create an array
with two microphones placed at ``[6.3, 4.87, 1.2]`` and ``[6.3, 4.93, 1.2]``.
.. code-block:: python
# define the locations of the microphones
import numpy as np
mic_locs = np.c_[
[6.3, 4.87, 1.2], # mic 1
[6.3, 4.93, 1.2], # mic 2
]
# finally place the array in the room
room.add_microphone_array(mic_locs)
A number of routines exist to create regular array geometries in 2D.
- :py:func:`~pyroomacoustics.beamforming.linear_2D_array`
- :py:func:`~pyroomacoustics.beamforming.circular_2D_array`
- :py:func:`~pyroomacoustics.beamforming.square_2D_array`
- :py:func:`~pyroomacoustics.beamforming.poisson_2D_array`
- :py:func:`~pyroomacoustics.beamforming.spiral_2D_array`
Adding source or microphone directivity
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The directivity pattern of a source or microphone can be conveniently set
through the ``directivity`` keyword argument.
First, a :py:obj:`pyroomacoustics.directivities.Directivity` object needs to be created. As of
Sep 6, 2021, only frequency-independent directivities from the
`cardioid family <https://en.wikipedia.org/wiki/Microphone#Cardioid,_hypercardioid,_supercardioid,_subcardioid>`_
are supported, namely figure-eight, hypercardioid, cardioid, and subcardioid.
Below is how a :py:obj:`pyroomacoustics.directivities.Directivity` object can be created, more
specifically a hypercardioid pattern pointing at an azimuth angle of 90 degrees and a colatitude
angle of 15 degrees.
.. code-block:: python
# create directivity object
from pyroomacoustics.directivities import (
DirectivityPattern,
DirectionVector,
CardioidFamily,
)
dir_obj = CardioidFamily(
orientation=DirectionVector(azimuth=90, colatitude=15, degrees=True),
pattern_enum=DirectivityPattern.HYPERCARDIOID,
)
After creating a :py:obj:`pyroomacoustics.directivities.Directivity` object, it is straightforward
to set the directivity of a source, microphone, or microphone array, namely by using the
``directivity`` keyword argument.
For example, to set a source's directivity:
.. code-block:: python
# place the source in the room
room.add_source(position=source_pos, directivity=dir_obj)
To set a single microphone's directivity:
.. code-block:: python
# place the microphone in the room
room.add_microphone(loc=mic_pos, directivity=dir_obj)
The same directivity pattern can be used for all microphones in an array:
.. code-block:: python
# place the microphone array in the room
room.add_microphone_array(R, directivity=dir_obj)
Or a different directivity can be used for each microphone by passing a list of
:py:obj:`pyroomacoustics.directivities.Directivity` objects:
.. code-block:: python
# place the microphone array in the room
room.add_microphone_array(R, directivity=[dir_1, dir_2])
.. warning::
As of Sep 6, 2021, setting directivity patterns for sources and microphone is only supported for
the image source method (ISM). Moreover, source direcitivities are only supported for
shoebox-shaped rooms.
Create the Room Impulse Response
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
At this point, the RIRs are simply created by invoking the ISM via
:py:func:`~pyroomacoustics.room.Room.image_source_model`. This function will
generate all the images sources up to the order required and use them to
generate the RIRs, which will be stored in the ``rir`` attribute of ``room``.
The attribute ``rir`` is a list of lists so that the outer list is on microphones
and the inner list over sources.
.. code-block:: python
room.compute_rir()
# plot the RIR between mic 1 and source 0
import matplotlib.pyplot as plt
plt.plot(room.rir[1][0])
plt.show()
.. warning::
The simulator uses a fractional delay filter that introduce a global delay
in the RIR. The delay can be obtained as follows.
.. code-block:: python
global_delay = pra.constants.get("frac_delay_length") // 2
Simulate sound propagation
~~~~~~~~~~~~~~~~~~~~~~~~~~
By calling :py:func:`~pyroomacoustics.room.Room.simulate`, a convolution of the
signal of each source (if not ``None``) will be performed with the
corresponding room impulse response. The output from the convolutions will be summed up
at the microphones. The result is stored in the ``signals`` attribute of ``room.mic_array``
with each row corresponding to one microphone.
.. code-block:: python
room.simulate()
# plot signal at microphone 1
plt.plot(room.mic_array.signals[1,:])
Full Example
~~~~~~~~~~~~
This example is partly exctracted from `./examples/room_from_rt60.py`.
.. code-block:: python
import numpy as np
import matplotlib.pyplot as plt
import pyroomacoustics as pra
from scipy.io import wavfile
# The desired reverberation time and dimensions of the room
rt60_tgt = 0.3 # seconds
room_dim = [10, 7.5, 3.5] # meters
# import a mono wavfile as the source signal
# the sampling frequency should match that of the room
fs, audio = wavfile.read("examples/samples/guitar_16k.wav")
# We invert Sabine's formula to obtain the parameters for the ISM simulator
e_absorption, max_order = pra.inverse_sabine(rt60_tgt, room_dim)
# Create the room
room = pra.ShoeBox(
room_dim, fs=fs, materials=pra.Material(e_absorption), max_order=max_order
)
# place the source in the room
room.add_source([2.5, 3.73, 1.76], signal=audio, delay=0.5)
# define the locations of the microphones
mic_locs = np.c_[
[6.3, 4.87, 1.2], [6.3, 4.93, 1.2], # mic 1 # mic 2
]
# finally place the array in the room
room.add_microphone_array(mic_locs)
# Run the simulation (this will also build the RIR automatically)
room.simulate()
room.mic_array.to_wav(
f"examples/samples/guitar_16k_reverb_{args.method}.wav",
norm=True,
bitdepth=np.int16,
)
# measure the reverberation time
rt60 = room.measure_rt60()
print("The desired RT60 was {}".format(rt60_tgt))
print("The measured RT60 is {}".format(rt60[1, 0]))
# Create a plot
plt.figure()
# plot one of the RIR. both can also be plotted using room.plot_rir()
rir_1_0 = room.rir[1][0]
plt.subplot(2, 1, 1)
plt.plot(np.arange(len(rir_1_0)) / room.fs, rir_1_0)
plt.title("The RIR from source 0 to mic 1")
plt.xlabel("Time [s]")
# plot signal at microphone 1
plt.subplot(2, 1, 2)
plt.plot(room.mic_array.signals[1, :])
plt.title("Microphone 1 signal")
plt.xlabel("Time [s]")
plt.tight_layout()
plt.show()
Hybrid ISM/Ray Tracing Simulator
--------------------------------
.. warning::
The hybrid simulator has not been thoroughly tested yet and should be used with
care. The exact implementation and default settings may also change in the future.
Currently, the default behavior of :py:obj:`~pyroomacoustics.room.Room`
and :py:obj:`~pyroomacoustics.room.ShoeBox` has been kept as in previous
versions of the package. Bugs and user experience can be reported on
`github <https://github.com/LCAV/pyroomacoustics>`_.
The hybrid ISM/RT simulator uses ISM to simulate the early reflections in the RIR
and RT for the diffuse tail. Our implementation is based on [2]_ and [3]_.
The simulator has the following features.
- Scattering: Wall scattering can be defined by assigning a scattering
coefficient to the walls together with the energy absorption.
- Multi-band: The simulation can be carried out with different parameters for
different `octave bands <https://en.wikipedia.org/wiki/Octave_band>`_. The
octave bands go from 125 Hz to half the sampling frequency.
- Air absorption: The frequency dependent absorption of the air can be turned
by providing the keyword argument ``air_absorption=True`` to the room
constructor.
Here is a simple example using the hybrid simulator.
We suggest to use ``max_order=3`` with the hybrid simulator.
.. code-block:: python
# Create the room
room = pra.ShoeBox(
room_dim,
fs=16000,
materials=pra.Material(e_absorption),
max_order=3,
ray_tracing=True,
air_absorption=True,
)
# Activate the ray tracing
room.set_ray_tracing()
A few example programs are provided in ``./examples``.
- ``./examples/ray_tracing.py`` demonstrates use of ray tracing for rooms of different sizes
and with different amounts of reverberation
- ``./examples/room_L_shape_3d_rt.py`` shows how to simulate a polyhedral room
- ``./examples/room_from_stl.py`` demonstrates how to import a model from an STL file
Wall Materials
--------------
The wall materials are handled by the
:py:obj:`~pyroomacoustics.parameters.Material` objects. A material is defined
by at least one *absorption* coefficient that represents the ratio of sound
energy absorbed by a wall upon reflection.
A material may have multiple absorption coefficients corresponding to different
abosrptions at different octave bands.
When only one coefficient is provided, the absorption is assumed to be uniform at
all frequencies.
In addition, materials may have one or more scattering coefficients
corresponding to the ratio of energy scattered upon reflection.
The materials can be defined by providing the coefficients directly, or they can
be defined by chosing a material from the :doc:`materials database<pyroomacoustics.materials.database>` [2]_.
.. code-block:: python
import pyroomacoustics as pra
m = pra.Material(energy_absorption="hard_surface")
room = pra.ShoeBox([9, 7.5, 3.5], fs=16000, materials=m, max_order=17)
We can use different materials for different walls. In this case, the materials should be
provided in a dictionary. For a shoebox room, this can be done as follows.
.. code-block:: python
import pyroomacoustics as pra
m = pra.make_materials(
ceiling="hard_surface",
floor="6mm_carpet",
east="brickwork",
west="brickwork",
north="brickwork",
south="brickwork",
)
room = pra.ShoeBox(
[9, 7.5, 3.5], fs=16000, materials=m, max_order=17, air_absorption=True, ray_tracing=True
)
.. note::
For shoebox rooms, the walls are labelled as follows:
- ``west``/``east`` for the walls in the y-z plane with a small/large x coordinates, respectively
- ``south``/``north`` for the walls in the x-z plane with a small/large y coordinates, respectively
- ``floor``/``ceiling`` for the walls int x-y plane with small/large z coordinates, respectively
Controlling the signal-to-noise ratio
-------------------------------------
It is in general necessary to scale the signals from different sources to
obtain a specific signal-to-noise or signal-to-interference ratio (SNR and SIR,
respectively). This can be done by passing some options to the :py:func:`simulate()`
function. Because the relative amplitude of signals will change at different microphones
due to propagation, it is necessary to choose a reference microphone. By default, this
will be the first microphone in the array (index 0). The simplest choice is to choose
the variance of the noise \\(\\sigma_n^2\\) to achieve a desired SNR with respect
to the cumulative signal from all sources. Assuming that the signals from all sources
are scaled to have the same amplitude (e.g., unit amplitude) at the reference microphone,
the SNR is defined as
.. math::
\mathsf{SNR} = 10 \log_{10} \frac{K}{\sigma_n^2}
where \\(K\\) is the number of sources. For example, an SNR of 10 decibels (dB)
can be obtained using the following code
.. code-block:: python
room.simulate(reference_mic=0, snr=10)
Sometimes, more challenging normalizations are necessary. In that case,
a custom callback function can be provided to simulate. For example,
we can imagine a scenario where we have ``n_src`` out of which ``n_tgt``
are the targets, the rest being interferers. We will assume all
targets have unit variance, and all interferers have equal
variance \\(\\sigma_i^2\\) (at the reference microphone). In
addition, there is uncorrelated noise \\(\\sigma_n^2\\) at
every microphones. We will define SNR and SIR with respect
to a single target source:
.. math::
\mathsf{SNR} & = 10 \log_{10} \frac{1}{\sigma_n^2}
\mathsf{SIR} & = 10 \log_{10} \frac{1}{(\mathsf{n_{src}} - \mathsf{n_{tgt}}) \sigma_i^2}
The callback function ``callback_mix`` takes as argument an nd-array
``premix_signals`` of shape ``(n_src, n_mics, n_samples)`` that contains the
microphone signals prior to mixing. The signal propagated from the ``k``-th
source to the ``m``-th microphone is contained in ``premix_signals[k,m,:]``. It
is possible to provide optional arguments to the callback via
``callback_mix_kwargs`` optional argument. Here is the code
implementing the example described.
.. code-block:: python
# the extra arguments are given in a dictionary
callback_mix_kwargs = {
'snr' : 30, # SNR target is 30 decibels
'sir' : 10, # SIR target is 10 decibels
'n_src' : 6,
'n_tgt' : 2,
'ref_mic' : 0,
}
def callback_mix(premix, snr=0, sir=0, ref_mic=0, n_src=None, n_tgt=None):
# first normalize all separate recording to have unit power at microphone one
p_mic_ref = np.std(premix[:,ref_mic,:], axis=1)
premix /= p_mic_ref[:,None,None]
# now compute the power of interference signal needed to achieve desired SIR
sigma_i = np.sqrt(10 ** (- sir / 10) / (n_src - n_tgt))
premix[n_tgt:n_src,:,:] *= sigma_i
# compute noise variance
sigma_n = np.sqrt(10 ** (- snr / 10))
# Mix down the recorded signals
mix = np.sum(premix[:n_src,:], axis=0) + sigma_n * np.random.randn(*premix.shape[1:])
return mix
# Run the simulation
room.simulate(
callback_mix=callback_mix,
callback_mix_kwargs=callback_mix_kwargs,
)
mics_signals = room.mic_array.signals
In addition, it is desirable in some cases to obtain the microphone signals
with individual sources, prior to mixing. For example, this is useful to
evaluate the output from blind source separation algorithms. In this case, the
``return_premix`` argument should be set to ``True``
.. code-block:: python
premix = room.simulate(return_premix=True)
Reverberation Time
------------------
The reverberation time (RT60) is defined as the time needed for the enery of
the RIR to decrease by 60 dB. It is a useful measure of the amount of
reverberation. We provide a method in the
:py:func:`~pyroomacoustics.experimental.rt60.measure_rt60` to measure the RT60
of recorded or simulated RIR.
The method is also directly integrated in the :py:obj:`~pyroomacoustics.room.Room` object as the method :py:func:`~pyroomacoustics.room.Room.measure_rt60`.
.. code-block:: python
# assuming the simulation has already been carried out
rt60 = room.measure_rt60()
for m in room.n_mics:
for s in room.n_sources:
print(
"RT60 between the {}th mic and {}th source: {:.3f} s".format(m, s, rt60[m, s])
)
References
----------
.. [1] J. B. Allen and D. A. Berkley, *Image method for efficiently simulating small-room acoustics,* J. Acoust. Soc. Am., vol. 65, no. 4, p. 943, 1979.
.. [2] M. Vorlaender, Auralization, 1st ed. Berlin: Springer-Verlag, 2008, pp. 1-340.
.. [3] D. Schroeder, Physically based real-time auralization of interactive virtual environments. PhD Thesis, RWTH Aachen University, 2011.
"""
from __future__ import division, print_function
import math
import warnings
import numpy as np
import scipy.spatial as spatial
from . import beamforming as bf
from . import libroom
from .acoustics import OctaveBandsFactory, rt60_eyring, rt60_sabine
from .beamforming import MicrophoneArray
from .experimental import measure_rt60
from .libroom import Wall, Wall2D
from .parameters import Material, Physics, constants, eps, make_materials
from .soundsource import SoundSource
from .utilities import angle_function
from .directivities import CardioidFamily, source_angle_shoebox
def wall_factory(corners, absorption, scattering, name=""):
""" Call the correct method according to wall dimension """
if corners.shape[0] == 3:
return Wall(
corners,
absorption,
scattering,
name,
)
elif corners.shape[0] == 2:
return Wall2D(
corners,
absorption,
scattering,
name,
)
else:
raise ValueError("Rooms can only be 2D or 3D")
def sequence_generation(volume, duration, c, fs, max_rate=10000):
# repeated constant
fpcv = 4 * np.pi * c ** 3 / volume
# initial time
t0 = ((2 * np.log(2)) / fpcv) ** (1.0 / 3.0)
times = [t0]
while times[-1] < t0 + duration:
# uniform random variable
z = np.random.rand()
# rate of the point process at this time
mu = np.minimum(fpcv * (t0 + times[-1]) ** 2, max_rate)
# time interval to next point
dt = np.log(1 / z) / mu
times.append(times[-1] + dt)
# convert from continuous to discrete time
indices = (np.array(times) * fs).astype(np.int)
seq = np.zeros(indices[-1] + 1)
seq[indices] = np.random.choice([1, -1], size=len(indices))
return seq
def find_non_convex_walls(walls):
"""
Finds the walls that are not in the convex hull
Parameters
----------
walls: list of Wall objects
The walls that compose the room
Returns
-------
list of int
The indices of the walls no in the convex hull
"""
all_corners = []
for wall in walls[1:]:
all_corners.append(wall.corners.T)
X = np.concatenate(all_corners, axis=0)
convex_hull = spatial.ConvexHull(X, incremental=True)
# Now we need to check which walls are on the surface
# of the hull
in_convex_hull = [False] * len(walls)
for i, wall in enumerate(walls):
# We check if the center of the wall is co-linear or co-planar
# with a face of the convex hull
point = np.mean(wall.corners, axis=1)
for simplex in convex_hull.simplices:
if point.shape[0] == 2:
# check if co-linear
p0 = convex_hull.points[simplex[0]]
p1 = convex_hull.points[simplex[1]]
if libroom.ccw3p(p0, p1, point) == 0:
# co-linear point add to hull
in_convex_hull[i] = True
elif point.shape[0] == 3:
# Check if co-planar
p0 = convex_hull.points[simplex[0]]
p1 = convex_hull.points[simplex[1]]
p2 = convex_hull.points[simplex[2]]
normal = np.cross(p1 - p0, p2 - p0)
if np.abs(np.inner(normal, point - p0)) < eps:
# co-planar point found!
in_convex_hull[i] = True
return [i for i in range(len(walls)) if not in_convex_hull[i]]
class Room(object):
"""
A Room object has as attributes a collection of
:py:obj:`pyroomacoustics.wall.Wall` objects, a
:py:obj:`pyroomacoustics.beamforming.MicrophoneArray` array, and a list of
:py:obj:`pyroomacoustics.soundsource.SoundSource`. The room can be two
dimensional (2D), in which case the walls are simply line segments. A factory method
:py:func:`pyroomacoustics.room.Room.from_corners`
can be used to create the room from a polygon. In three dimensions (3D), the
walls are two dimensional polygons, namely a collection of points lying on a
common plane. Creating rooms in 3D is more tedious and for convenience a method
:py:func:`pyroomacoustics.room.Room.extrude` is provided to lift a 2D room
into 3D space by adding vertical walls and parallel floor and ceiling.
The Room is sub-classed by :py:obj:pyroomacoustics.room.ShoeBox` which
creates a rectangular (2D) or parallelepipedic (3D) room. Such rooms
benefit from an efficient algorithm for the image source method.
:attribute walls: (Wall array) list of walls forming the room
:attribute fs: (int) sampling frequency
:attribute max_order: (int) the maximum computed order for images
:attribute sources: (SoundSource array) list of sound sources
:attribute mics: (MicrophoneArray) array of microphones
:attribute corners: (numpy.ndarray 2xN or 3xN, N=number of walls) array containing a point belonging to each wall, used for calculations
:attribute absorption: (numpy.ndarray size N, N=number of walls) array containing the absorption factor for each wall, used for calculations
:attribute dim: (int) dimension of the room (2 or 3 meaning 2D or 3D)
:attribute wallsId: (int dictionary) stores the mapping "wall name -> wall id (in the array walls)"
Parameters
----------
walls: list of Wall or Wall2D objects
The walls forming the room.
fs: int, optional
The sampling frequency in Hz. Default is 8000.
t0: float, optional
The global starting time of the simulation in seconds. Default is 0.
max_order: int, optional
The maximum reflection order in the image source model. Default is 1,
namely direct sound and first order reflections.
sigma2_awgn: float, optional
The variance of the additive white Gaussian noise added during
simulation. By default, none is added.
sources: list of SoundSource objects, optional
Sources to place in the room. Sources can be added after room creating
with the `add_source` method by providing coordinates.
mics: MicrophoneArray object, optional
The microphone array to place in the room. A single microphone or
microphone array can be added after room creation with the
`add_microphone_array` method.
temperature: float, optional
The air temperature in the room in degree Celsius. By default, set so
that speed of sound is 343 m/s.
humidity: float, optional
The relative humidity of the air in the room (between 0 and 100). By
default set to 0.
air_absorption: bool, optional
If set to True, absorption of sound energy by the air will be
simulated.
ray_tracing: bool, optional
If set to True, the ray tracing simulator will be used along with
image source model.
"""
def __init__(
self,
walls,
fs=8000,
t0=0.0,
max_order=1,
sigma2_awgn=None,
sources=None,
mics=None,
temperature=None,
humidity=None,
air_absorption=False,
ray_tracing=False,
):
self.walls = walls
# Get the room dimension from that of the walls
self.dim = walls[0].dim
# Create a mapping with friendly names for walls
self._wall_mapping()
# initialize everything else
self._var_init(
fs,
t0,
max_order,
sigma2_awgn,
temperature,
humidity,
air_absorption,
ray_tracing,
)
# initialize the C++ room engine
self._init_room_engine()
# add the sources
self.sources = []
if sources is not None and isinstance(sources, list):
for src in sources:
self.add_soundsource(src)
# add the microphone array
if mics is not None:
self.add_microphone_array(mics)
else:
self.mic_array = None
def _var_init(
self,
fs,
t0,
max_order,
sigma2_awgn,
temperature,
humidity,
air_absorption,
ray_tracing,
):
self.fs = fs
if t0 != 0.0:
raise NotImplementedError(
"Global simulation delay not " "implemented (aka t0)"
)
self.t0 = t0
self.max_order = max_order
self.sigma2_awgn = sigma2_awgn
self.octave_bands = OctaveBandsFactory(fs=self.fs)
# Keep track of the state of the simulator
self.simulator_state = {
"ism_needed": (self.max_order >= 0),
"rt_needed": ray_tracing,
"air_abs_needed": air_absorption,
"ism_done": False,
"rt_done": False,
"rir_done": False,
}
# make it clear the room (C++) engine is not ready yet
self.room_engine = None
if temperature is None and humidity is None:
# default to package wide setting when nothing is provided
self.physics = Physics().from_speed(constants.get("c"))
else:
# use formulas when temperature and/or humidity are provided
self.physics = Physics(temperature=temperature, humidity=humidity)
self.set_sound_speed(self.physics.get_sound_speed())
self.air_absorption = None
if air_absorption:
self.set_air_absorption()
# default values for ray tracing parameters
self.set_ray_tracing()
if not ray_tracing:
self.unset_ray_tracing()
# in the beginning, nothing has been
self.visibility = None
# initialize the attribute for the impulse responses
self.rir = None
def _init_room_engine(self, *args):
args = list(args)
if len(args) == 0:
# This is a polygonal room
# find the non convex walls
obstructing_walls = find_non_convex_walls(self.walls)
args += [self.walls, obstructing_walls]
# for shoebox rooms, the required arguments are passed to
# the function
# initialize the C++ room engine
args += [
[],
self.c, # speed of sound
self.max_order,
self.rt_args["energy_thres"],
self.rt_args["time_thres"],
self.rt_args["receiver_radius"],
self.rt_args["hist_bin_size"],
self.simulator_state["ism_needed"] and self.simulator_state["rt_needed"],
]
# Create the real room object
if self.dim == 2:
self.room_engine = libroom.Room2D(*args)
else:
self.room_engine = libroom.Room(*args)
def _update_room_engine_params(self):
# Now, if it exists, set the parameters of room engine
if self.room_engine is not None:
self.room_engine.set_params(
self.c, # speed of sound
self.max_order,
self.rt_args["energy_thres"],
self.rt_args["time_thres"],
self.rt_args["receiver_radius"],
self.rt_args["hist_bin_size"],
(
self.simulator_state["ism_needed"]
and self.simulator_state["rt_needed"]
),
)
@property
def is_multi_band(self):
multi_band = False
for w in self.walls:
if len(w.absorption) > 1:
multi_band = True
return multi_band
def set_ray_tracing(
self,
n_rays=None,
receiver_radius=0.5,
energy_thres=1e-7,
time_thres=10.0,
hist_bin_size=0.004,
):
"""
Activates the ray tracer.
Parameters
----------
n_rays: int, optional
The number of rays to shoot in the simulation
receiver_radius: float, optional
The radius of the sphere around the microphone in which to
integrate the energy (default: 0.5 m)
energy_thres: float, optional
The energy thresold at which rays are stopped (default: 1e-7)
time_thres: float, optional
The maximum time of flight of rays (default: 10 s)
hist_bin_size: float
The time granularity of bins in the energy histogram (default: 4 ms)
"""
if hasattr(self, "mic_array"):
if self.mic_array.directivity is not None:
raise NotImplementedError("Directivity not supported with ray tracing.")
if hasattr(self, "sources"):
for source in self.sources:
if source.directivity is not None:
raise NotImplementedError("Directivity not supported with ray tracing.")
self.simulator_state["rt_needed"] = True
self.rt_args = {}
self.rt_args["energy_thres"] = energy_thres
self.rt_args["time_thres"] = time_thres
self.rt_args["receiver_radius"] = receiver_radius
self.rt_args["hist_bin_size"] = hist_bin_size
# set the histogram bin size so that it is an integer number of samples
self.rt_args["hist_bin_size_samples"] = math.floor(
self.fs * self.rt_args["hist_bin_size"]
)
self.rt_args["hist_bin_size"] = self.rt_args["hist_bin_size_samples"] / self.fs
if n_rays is None:
n_rays_auto_flag = True
# We follow Vorlaender 2008, Eq. (11.12) to set the default number of rays
# It depends on the mean hit rate we want to target
target_mean_hit_count = 20
# This is the multiplier for a single hit in average
k1 = self.get_volume() / (
np.pi
* (self.rt_args["receiver_radius"] ** 2)
* self.c
* self.rt_args["hist_bin_size"]
)
n_rays = int(target_mean_hit_count * k1)
if n_rays > 100000:
import warnings
warnings.warn(
"The number of rays used for ray tracing is larger than"
"100000 which may result in slow simulation. The number"
"of rays was automatically chosen to provide accurate"
"room impulse response based on the room volume and the"
"receiver radius around the microphones. The number of"
"rays may be reduced by increasing the size of the"
"receiver. This tends to happen especially for large"
"rooms with small receivers. The receiver is a sphere"
"around the microphone and its radius (in meters) may be"
"specified by providing the `receiver_radius` keyword"
"argument to the `set_ray_tracing` method."
)