-
Notifications
You must be signed in to change notification settings - Fork 3
/
pyzones.py
1936 lines (1470 loc) · 67.8 KB
/
pyzones.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
from abc import abstractmethod
import numpy as np
from scipy.interpolate import griddata
import matplotlib.pyplot as plt
import csv
# ********* Geometric *********
class Shape:
"""
An abstract class for geometric shapes defining some key methods required
"""
@abstractmethod
def get_perimeter(self, start, end, num_points):
"""
Create a list of points between the user defined start and end positions on the perimeter of the shape
:param start: Position at which the list of points should begin
:type start: float
:param end: Position at which the list of points should end
:type end: float
:param num_points: Number of points
:type num_points: int
:return: A list of points (x,y) evenly spaced on the perimeter of the shape between the start and end positions
:rtype: numpy.ndarray
"""
pass
@abstractmethod
def get_grid(self, spacing):
"""
Create a grid of points spaced uniformly across the shape
:param spacing: Spacing between points in the grid
:type spacing: float
:return: A list of points (x,y) uniformly space across the shape
:rtype: numpy.ndarray
"""
pass
@abstractmethod
def is_point_inside(self, point):
"""
Check whether or not a point is inside the shape
:param point: list/tuple of the coordinates (x, y) of a point
:type point: list
:return: A bool stating whether or not the point is within the shape
:rtype: bool
"""
pass
class Circle(Shape):
"""
A geometric class for a circle
Attributes
----------
centre : list
A list of coordinates (x, y) describing the centre of the circle
radius : float
The radius of the circle
"""
def __init__(self, centre, radius):
"""
Creates a circle
:param centre: The coordinates (x,y) of centre of the circle
:type centre: list
:param radius: The radius of the circle
:type radius: float
:rtype: Circle
"""
self._centre = centre
self._radius = radius
@property
def centre(self):
"""
A list of coordinates (x, y) describing the centre of the circle
:return: (x, y) of the centre of the circle
:rtype: list
"""
return self._centre
@property
def radius(self):
"""
The radius of the circle
:return: The radius
:rtype: float
"""
return self._radius
def get_circular_points(self, start_angle, end_angle, num_points, radius, decimal_places=None):
"""
Create a list of points between the user defined start and end angles (in degrees) on the perimeter of a new circle sharing
the centre point of this circle with a different radius
:param start_angle: Position at which the list of points should begin
:type start_angle: float
:param end_angle: Position at which the list of points should end
:type end_angle: float
:param num_points: Number of points
:type num_points: int
:param radius: Radius of the circle on which the points are placed
:type radius: float
:param decimal_places: Number of decimal places the coordinates are returned with - None: there is no rounding
:type decimal_places: int
:return: An array of points (x,y) evenly spaced on the perimeter of the new circle between the start and end angles
:rtype: numpy.ndarray
"""
points = np.zeros((num_points, 2), float)
full_angle = 180 - abs(abs(end_angle - start_angle) - 180)
if full_angle == 0:
full_angle = 360
delta_angle = full_angle / num_points
for i in range(num_points):
points[i][0] = self._centre[0] + np.cos(np.radians(90 + start_angle + delta_angle * i)) * radius
points[i][1] = self._centre[1] + np.sin(np.radians(90 + start_angle + delta_angle * i)) * radius
if decimal_places is not None:
return np.array(np.around(points, decimal_places))
else:
return np.array(points)
def get_perimeter(self, start_angle, end_angle, num_points, decimal_places=None):
"""
Create a list of points between the user defined start and end angles on the perimeter of the circle
:param start_angle: Position at which the list of points should begin
:type start_angle: float
:param end_angle: Position at which the list of points should end
:type end_angle: float
:param num_points: Number of points
:type num_points: int
:param decimal_places: Number of decimal places the coordinates are returned with - None: there is no rounding
:type decimal_places: int
:return: A list of points (x,y) evenly spaced on the perimeter of the shape between the start and end angles
:rtype: numpy.ndarray
"""
return np.array(self.get_circular_points(start_angle, end_angle, num_points, self._radius, decimal_places))
def get_grid(self, spacing, alpha=2):
"""
Create a grid of points spaced uniformly across the circle using the sunflower seed arrangement algorithm
:param spacing: Approximate spacing between points in the grid
:type spacing: float
:param alpha: Determines the evenness of the boundary - 0 is jagged, 2 is smooth. Above 2 is not recommended
:type alpha: float
:return: A list of points (x,y) uniformly spaced across the circle
:rtype: numpy.ndarray
"""
# Algorithm is found at the stack overflow thread linked below:
# https://stackoverflow.com/questions/28567166/uniformly-distribute-x-points-inside-a-circle
# Calculates the number of points (n) from the spacing
area = np.pi * self._radius**2
n = int(area / spacing**2)
points = np.zeros((n, 2), float)
b = int(alpha * np.sqrt(n)) # number of boundary points
golden_ratio = (np.sqrt(5) + 1) / 2
for point in range(1, n + 1):
if point > n - b:
r = 1
else:
r = np.sqrt(point - 1 / 2) / np.sqrt(n - (b + 1) / 2)
theta = 2 * np.pi * point / golden_ratio**2
points[point - 1][0] = self._centre[0] + r*np.cos(theta) * self._radius
points[point - 1][1] = self._centre[1] + r*np.sin(theta) * self._radius
return np.array(points)
def is_point_inside(self, point):
"""
Check whether or not a point is inside the circle
:param point: List/tuple of the coordinates (x, y) of a point
:type point: list
:return: A bool stating whether or not the point is within the circle
:rtype: bool
"""
# checks if the distance from the centre of the circle to the point, d, is less than or equal to the radius
d = np.sqrt((point[0] - self.centre[0])**2 + (point[1] - self.centre[1])**2)
return d <= self.radius
class Rectangle(Shape):
"""
A geometric class for a rectangle
Attributes
----------
coordinate : list
A list of coordinates (x, y) describing the centre or bottom left of the rectangle
width : float
The width of the rectangle
height : float
The height of the rectangle
coordinate_pos : str
Describes the position of the coordinate parameter - either "centre" or "bottom left"
"""
def __init__(self, coordinate, width, height, coordinate_pos="bottom left"):
"""
Creates a rectangle
:param coordinate: A list of coordinates (x, y) describing the centre or bottom left of the rectangle
:type coordinate: list
:param width: The width of the rectangle
:type width: float
:param height: The height of the rectangle
:type height: float
:param coordinate_pos: Description of the position of the coordinate - "centre" or "bottom left" of the rectangle
:type coordinate_pos: str
:rtype: Rectangle
"""
if coordinate_pos == 'centre':
self._xy = [coordinate[0] - width / 2, coordinate[1] - height / 2]
elif coordinate_pos == 'bottom left':
self._xy = coordinate
else:
print("coordinate_pos must be in \"centre\" or \"bottom left\"")
quit(1)
self._width = width
self._height = height
@property
def xy(self):
"""
A list of coordinates (x, y) describing the bottom left of the rectangle
:return: (x, y) of the bottom left of the rectangle
:rtype: list
"""
return self._xy
@property
def width(self):
"""
The width of the rectangle
:return: The width
:rtype: float
"""
return self._width
@property
def height(self):
"""
The height of the rectangle
:return: The height
:rtype: float
"""
return self._height
def get_perimeter(self, start_point, end_point, num_points):
pass
def get_grid(self, spacing):
"""
Create a grid of points spaced uniformly across the rectangle
:param spacing: Approximate spacing between points in the grid
:type spacing: float
:return: A list of points (x,y) uniformly spaced across the rectangle
:rtype: numpy.ndarray
"""
num_x = int(np.floor(self._width / spacing)) + 1
num_y = int(np.floor(self._height / spacing)) + 1
num_points = int(num_x * num_y)
points = np.zeros((num_points, 2), float)
for x in range(num_x):
for y in range(num_y):
points[y * num_x + x][0] = self._xy[0] + x * spacing
points[y * num_x + x][1] = self._xy[1] + y * spacing
return np.array(points)
def is_point_inside(self, point):
"""
Check whether or not a point is inside the rectangle
:param point: list/tuple of the coordinates (x, y) of a point
:type point: tuple
:return: A bool stating whether or not the point is within the rectangle
:rtype: bool
"""
# checks that the x and y distance between the point and the bottom_left of the rectangle is less than the
# width and height
dx = abs(point[0] - self._xy[0]) + abs(self._xy[0] + self._width - point[0])
dy = abs(point[1] - self._xy[1]) + abs(self._xy[1] + self._height - point[1])
return dy <= self._height and dx <= self._width
# ********* PyZones specific setup classes *********
class Zone(Circle):
"""
A sound zone to be used in setup of the soundfield's geometry
Attributes
----------
centre : list
A list of coordinates (x, y) describing the centre of the circle
radius : float
The radius of the circle
colour : list
A list of float values (r, g, b)
"""
def __init__(self, centre, radius, colour=None):
"""
Creates a sound zone
:param centre: A list of coordinates (x, y) describing the centre of the circle
:type centre: list
:param radius: The radius of the circle
:type radius: float
:param colour: A list of float values (r, g, b) - None results in black (0, 0, 0)
:type colour: list
:rtype: Zone
"""
if colour is None:
self._colour = [0, 0, 0]
else:
self._colour = colour
Circle.__init__(self, centre, radius)
@property
def colour(self):
"""
A list of float values (r, g, b)
:return: A list of float values (r, g, b)
:rtype: list
"""
return self._colour
class Soundfield(Rectangle):
"""
The soundfield being used in the simulation. Can be thought of as the room, however no room reflections are modelled
Attributes
----------
_zones : list
A list of the zones used in the simulation.
_fig : float
The figure from the matplotlib.pyplot
_axes : float
The axes from the matplotlib.pyplot
"""
def __init__(self, coordinate, width, height, coordinate_pos="bottom left"):
"""
Creates a soundfield to be used for simulations. This class is exclusively for the graphics and visualisations
:param coordinate: A list of coordinates (x, y) describing the centre or bottom left of the rectangle
:type coordinate: list
:param width: The width of the rectangle
:type width: float
:param height: The height of the rectangle
:type height: float
:param coordinate_pos: The position of the coordinate - "centre" or "bottom left" of the rectangle
:type coordinate_pos: str
:rtype: Soundfield
"""
Rectangle.__init__(self, coordinate, width, height, coordinate_pos=coordinate_pos)
self._zones = []
self._fig = plt.figure(figsize=(6, 6), dpi=300)
self._axes = self._fig.add_subplot(111)
self._axes.set_xlim([self.xy[0], self.xy[0] + width])
self._axes.set_ylim([self.xy[1], self.xy[1] + height])
self._cax = self._fig.add_axes([0.125, 0.94, 0.775, 0.04])
def add_zones(self, zones):
"""
Add the sound zone(s) to the soundfield such that they can be seen in the visualisations of the soundfield
:param zones: The zone(s) to be added to the soundfield
:type zones: list[Zone]
"""
if type(zones) is not list:
zones = [zones]
for zone in zones:
circle = plt.Circle(zone.centre, zone.radius, fill=False)
circle.set_edgecolor(zone.colour)
self._axes.add_patch(circle)
self._zones.append(zone)
def add_sound_objects(self, *args):
"""
Add the sound objects to the soundfield such that they can be seen in the visualisations of the soundfield
:param args: a single Microphone/Loudspeaker or a MicrophoneArray/LoudspeakerArray
"""
def add_ls(ls):
centre = ls.position
x = centre[0] - (ls.width / 2)
y = centre[1] - (ls.height / 2)
angle = 0
# change the orientation of the loudspeaker such that it's looking at a point (purely aesthetic)
if ls.look_at is not None:
x_dif = ls.look_at[0] - centre[0]
y_dif = ls.look_at[1] - centre[1]
if x_dif == 0:
angle = 0
elif y_dif == 0:
angle = np.pi / 2
elif x_dif > 0:
angle = np.arctan(y_dif / x_dif) - np.pi / 2
else:
angle = np.arctan(y_dif / x_dif) + np.pi / 2
new_x = (x - centre[0]) * np.cos(angle) - (y - centre[1]) * np.sin(angle) + centre[0]
new_y = (x - centre[0]) * np.sin(angle) + (y - centre[1]) * np.cos(angle) + centre[1]
x = new_x
y = new_y
rect = plt.Rectangle((x, y), ls.width, ls.height, angle=np.rad2deg(angle), fill=False)
rect.set_edgecolor(ls.colour)
self._axes.add_patch(rect)
def add_mic(mic):
circle = plt.Circle(mic.position, mic.radius, fill=False)
circle.set_edgecolor(mic.colour)
self._axes.add_patch(circle)
for s_object in args:
if isinstance(s_object, Loudspeaker):
add_ls(s_object)
elif isinstance(s_object, LoudspeakerArray):
for item in s_object:
add_ls(item)
elif isinstance(s_object, Microphone):
add_mic(s_object)
elif isinstance(s_object, MicrophoneArray):
for item in s_object:
add_mic(item)
else:
"Please input a Microphone/Loudspeaker or MicrophoneArray/LoudspeakerArray to add."
return
def clear_graphs(self):
"""
Clear the SoundObjects and Zones from the visualisations
"""
self._axes.clear()
def plot_geometry(self, graph_name):
"""
Plot the geometry of the soundfield with any Zones or SoundObjects added
:param graph_name: The name and file location of the graph
:type graph_name: str
"""
self._axes.plot()
self._fig.savefig(graph_name)
def visualise(self, sim, graph_name, frequency=500, sf_spacing=0.1, zone_spacing=0.05, zone_alpha=2, transfer_functions=None, grid=None):
"""
Create a visualisation of the Soundfield at the given frequency. The frequency chosen must have been present in
the simulation provided. Transfer functions and visualisation micr positions can be provided to prevent them
being calculated more than once. Should the same frequency, loudspeakers and visualisation microphone positions
be kept the same, the returned transfer functions and microphone positions can be used again. The filter weights
used will be those most recently calculated in the simulation.
:param sim: Simulation for which the visualation is made - contains the filter weights.
:type sim: Simulation
:param graph_name: The name and file location of the graph
:type graph_name: str
:param frequency: Frequency at which the visualisation should be made - must have been present in the Simulation
:type frequency: int
:param sf_spacing: The spacing between the microphones in the grid across the soundfield in metres
:type sf_spacing: float
:param zone_spacing: The spacing between the microphones in the grid in the zone in metres
:type zone_spacing: float
:param zone_alpha: Determines the evenness of the boundary - 0 is jagged, 2 is smooth. Above 2 is not recommended
:type zone_alpha: float
:param transfer_functions: ndarray of transfer functions shape (microphones, loudspeakers). None - calculated
:type transfer_functions: numpy.ndarray
:param grid: Grid of mic positions (x, y) matching up with the provided transfer function. None - calculated
:type grid: list
:return: The grid and transfer functions used in the visualisation to prevent their unnecessary recalculation.
:rtype: numpy.ndarray, list
"""
# create the grid of microphones for visualisation
if grid is None:
points = self.get_grid(sf_spacing)
for zone in self._zones:
points = np.concatenate((points, zone.get_grid(zone_spacing, alpha=zone_alpha)), 0)
else:
points = grid
# create the transfer functions for the vis mics
if transfer_functions is None:
vis_mics = MicrophoneArray([Microphone(position=points[i]) for i in range(len(points))])
tfs = sim.calculate_transfer_functions("microphones", mic_array=vis_mics, frequency=frequency)
else:
tfs = transfer_functions
# find the simulation frequency index of the visualisation frequency
nonzero_array = np.nonzero(sim.frequencies == frequency)
if len(nonzero_array[0]) == 0:
print("Please visualise a frequency for which filter weights have been calculated")
return
freq_index = nonzero_array[0][0]
# use the most recently calculated filter weights corresponding to the visualisation frequency to calc pressure
q_matrix = np.array(sim.ls_array.get_q(frequency_index=freq_index))
p = (tfs[0] @ q_matrix[:, None]).flatten()
p = np.abs(p)
p = convert_to_db(p)
# plot the pressure
step = 0.01
xi = np.arange(self._xy[0], self._xy[0] + self._width + step, step)
yi = np.arange(self._xy[1], self._xy[1] + self._height + step, step)
xi, yi = np.meshgrid(xi, yi)
zi = griddata(points, p, (xi, yi), method='cubic', fill_value=1)
self._axes.contourf(xi, yi, zi, np.arange(0, 1.01, 0.01))
colours = self._axes.pcolormesh(xi, yi, zi, vmin=0, vmax=90)
self._fig.colorbar(colours, cax=self._cax, orientation='horizontal')
# self._axes.pcolormesh(xi, yi, zi, vmin=0, vmax=90) # remove once colour bar added
# self._axes.plot(*zip(*points), marker=',', color='r', ls='') SHOWS MIC POSITIONS
self._fig.savefig(graph_name, dpi=300)
return tfs, points
class SoundObject:
"""
A sound object to be used in simulations of sound zones
Attributes
----------
colour : list
A list of float values (r, g, b)
position : list
A list of coordinates (x, y) describing the centre of the sound object
"""
def __init__(self, position=None, colour=None):
"""
Creates a sound object for use in sound zone simulations
:param position: A list of coordinates (x, y) describing the position of the sound object
:type position: list
:param colour: A list of float values (r, g, b) - None results in black (0, 0, 0)
:type colour: list
"""
if position is None:
self._position = [0, 0]
else:
self._position = position
if colour is None:
self._colour = [0, 0, 0]
else:
self._colour = colour
@property
def colour(self):
"""
A list of float values (r, g, b)
:return: A list of float values (r, g, b)
:rtype: list
"""
return self._colour
@property
def position(self):
"""
A list of coordinates (x, y) describing the position of the sound object
:return: (x, y) of the bottom left of the rectangle
:rtype: list
"""
return self._position
@position.setter
def position(self, val):
"""
Set A list of coordinates (x, y) describing the position of the sound object
"""
self._position[0] = val[0]
self._position[1] = val[1]
class Microphone(SoundObject):
"""
A microphone to be used in simulations of sound zones. Inherits from the sound object class.
Attributes
----------------
_radius : static float
The radius of circles used to represent the microphones when rendered in the soundfield
Attributes
----------
colour : list
A list of float values (r, g, b)
position : list
A list of coordinates (x, y) describing the position of the microphone
zone : str
The zone in which this microphone is situated, "bright", "dark" or "either"
purpose : str
The purpose of the microphone, "setup", "evaluation" or "either
_pressure : list
The pressure for each frequency at the microphone most recently calculated and set
"""
_radius = 0.001
@property
def radius(self):
"""
The radius of circles used to represent the microphones when rendered in the soundfield
:return: The radius
:rtype: float
"""
return type(self)._radius
@radius.setter
def radius(self, val):
"""
Set the radius of circles used to represent the microphones when rendered in the soundfield
:param val: Value to be set as radius
:type val: float
"""
_radius = val
def __init__(self, zone="none", purpose="none", position=None, colour=None):
"""
Creates a microphone to be used in sound zone simulations
:param zone: The zone in which this microphone is situated, "bright", "dark" or "either"
:type zone: str
:param purpose: The purpose of the microphone, "setup", "evaluation" or "either"
:type purpose: str
:param position: A list of coordinates (x, y) describing the centre of the sound object
:type position: list
:param colour: A list of float values (r, g, b) - None results in black (0, 0, 0)
:type colour: list
"""
SoundObject.__init__(self, position, colour)
self._zone = zone
self._purpose = purpose
self._pressure = []
@property
def zone(self):
"""
The zone in which this microphone is situated, "bright" or "dark"
:return: The zone
:rtype: str
"""
return self._zone
@property
def purpose(self):
"""
The purpose of the microphone, "setup" or "evaluation"
:return: The purpose
:rtype: str
"""
return self._purpose
@property
def pressure(self):
"""
The pressure for each frequency at the microphone most recently calculated and set
:return: The pressure
:rtype: list
"""
return self._pressure
@pressure.setter
def pressure(self, list):
"""
The pressure for each frequency at the microphone most recently calculated and set
:param list: List of pressures at each frequency
:type list: list
"""
self._pressure = list
class Loudspeaker(SoundObject):
"""
A loudspeaker to be used as a source in simulations of sound zones. Inherits from the sound object class.
Attributes
-----------
_width : static float
The width of rectangles used to represent loudspeakers when rendered in the soundfield
_height : static float
The height of rectangles used to represent loudspeakers when rendered in the soundfield
Attributes
----------
colour : list
A list of float values (r, g, b)
position : list
A list of coordinates (x, y) describing the position of the loudspeaker
look_at : list
A list of coordinates (x, y) describing the position the loudspeaker faces
q : list
The filter weight at each frequency most recently calculated and set
"""
_width = 0.08
_height = 0.1
@property
def width(self):
"""
The width of rectangles used to represent loudspeakers when rendered in the soundfield
:return: The width
:rtype: float
"""
return type(self)._width
@property
def height(self):
"""
The height of rectangles used to represent loudspeakers when rendered in the soundfield
:return: The height
:rtype: float
"""
return type(self)._height
def __init__(self, position=None, colour=None, look_at=None):
"""
Creates a Loudspeaker to be used as a source in sound zone simulations
:param position: A list of coordinates (x, y) describing the position of the loudspeaker
:type position: list
:param colour: A list of float values (r, g, b)
:type colour: list
:param look_at: A list of coordinates (x, y) describing the position the loudspeaker faces
:type look_at: list
"""
SoundObject.__init__(self, position, colour)
self._look_at = look_at
self.q = []
@property
def look_at(self):
"""
A list of coordinates (x, y) describing the position the loudspeaker faces
:return: a list of coordinates (x, y)
:rtype: list
"""
return self._look_at
class SoundObjectArray(list):
"""
A container class for sound objects
"""
def __init__(self, *args):
"""
Creates an array of sound objects
"""
list.__init__(self, *args)
def position_objects(self, positions):
"""
Position the sound objects
:param positions: A list of positions the same length as the number of objects
:type positions: numpy.ndarray
"""
for i in range(len(positions)):
self[i].position = positions[i]
def get_object_positions(self):
"""
Returns a list of the positions of the sound objects
:return: list of positions
:rtype: list
"""
return [self[i].position for i in range(len(self))]
def __add__(self, other):
return type(self)(list.__add__(self, other))
def __iadd__(self, other):
return type(self)(list.__add__(self, other))
class LoudspeakerArray(SoundObjectArray):
"""
A container class for loudspeakers
"""
def initialise_q(self, num_frequencies):
"""
Initialise the filter weights of the loudspeakers in the loudspeaker array to one.
:param num_frequencies: The number of frequencies in the simulation.
:type num_frequencies: int
"""
for ls in self:
ls.q = np.ones(num_frequencies, complex)
def set_q(self, new_q, frequency_index):
"""
Set the filter weight values for the loudspeaker array at the frequency index
:param new_q: The list of filter weights
:type new_q: list
:param frequency_index: The index at which the relevant frequency is stored in the simulation's frequency vector
:type frequency_index: int
"""
for i in range(len(new_q)):
self[i].q[frequency_index] = new_q[i]
def get_q(self, frequency_index):
"""
Get the filter weight values for the loudspeaker array at the frequency index
:param frequency_index: The index at which the relevant frequency is stored in the simulation's frequency vector
:type frequency_index: int
:return: The list of filter weights calculated for the relevant frequency
:rtype: list
"""
if frequency_index < 0:
return [ls.q for ls in self]
else:
return [ls.q[frequency_index] for ls in self]
class MicrophoneArray(SoundObjectArray):
"""
A container class for microphones
"""
def initialise_pressures(self, num_frequencies):
"""
Initialise the pressures of the microphones in the microphone array to zero.
:param num_frequencies: The number of frequencies in the simulation.
:type num_frequencies: int
"""
for mic in self:
mic.pressure = np.zeros(num_frequencies, complex)
def set_pressures(self, new_pressures, frequency_index):
"""
Set the pressures values for the microphone array at the frequency index
:param new_pressures: The list of pressures
:type new_pressures: list
:param frequency_index: The index at which the relevant frequency is stored in the simulation's frequency vector
:type frequency_index: int
"""
for i in range(len(new_pressures)):
self[i].pressure[frequency_index] = new_pressures[i]
def get_pressures(self, frequency_index):
"""