sofar 1.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- docs/Makefile +20 -0
- docs/api_reference.rst +20 -0
- docs/conf.py +167 -0
- docs/contributing.rst +1 -0
- docs/history.rst +1 -0
- docs/index.rst +4 -0
- docs/make.bat +36 -0
- docs/readme.rst +1 -0
- docs/resources/conventions.py +162 -0
- docs/resources/working_with_sofa_HRIR_lateral.png +0 -0
- docs/resources/working_with_sofa_source_horizontal.png +0 -0
- docs/resources/working_with_sofa_source_lateral.png +0 -0
- docs/sofar.rst +82 -0
- sofar/__init__.py +28 -0
- sofar/io.py +427 -0
- sofar/sofa.py +1835 -0
- sofar/sofa_conventions/VERSION +1 -0
- sofar/sofa_conventions/conventions/AnnotatedEmitterAudio_0.2.csv +46 -0
- sofar/sofa_conventions/conventions/AnnotatedEmitterAudio_0.2.json +353 -0
- sofar/sofa_conventions/conventions/AnnotatedReceiverAudio_0.2.csv +46 -0
- sofar/sofa_conventions/conventions/AnnotatedReceiverAudio_0.2.json +353 -0
- sofar/sofa_conventions/conventions/FreeFieldDirectivityTF_1.1.csv +59 -0
- sofar/sofa_conventions/conventions/FreeFieldDirectivityTF_1.1.json +444 -0
- sofar/sofa_conventions/conventions/FreeFieldHRIR_1.0.csv +43 -0
- sofar/sofa_conventions/conventions/FreeFieldHRIR_1.0.json +333 -0
- sofar/sofa_conventions/conventions/FreeFieldHRTF_1.0.csv +44 -0
- sofar/sofa_conventions/conventions/FreeFieldHRTF_1.0.json +340 -0
- sofar/sofa_conventions/conventions/GeneralFIR-E_2.0.csv +37 -0
- sofar/sofa_conventions/conventions/GeneralFIR-E_2.0.json +270 -0
- sofar/sofa_conventions/conventions/GeneralFIR_1.0.csv +40 -0
- sofar/sofa_conventions/conventions/GeneralFIR_1.0.json +295 -0
- sofar/sofa_conventions/conventions/GeneralSOS_1.0.csv +40 -0
- sofar/sofa_conventions/conventions/GeneralSOS_1.0.json +306 -0
- sofar/sofa_conventions/conventions/GeneralTF-E_1.0.csv +38 -0
- sofar/sofa_conventions/conventions/GeneralTF-E_1.0.json +277 -0
- sofar/sofa_conventions/conventions/GeneralTF_1.0.csv +38 -0
- sofar/sofa_conventions/conventions/GeneralTF_1.0.json +277 -0
- sofar/sofa_conventions/conventions/GeneralTF_2.0.csv +38 -0
- sofar/sofa_conventions/conventions/GeneralTF_2.0.json +277 -0
- sofar/sofa_conventions/conventions/SimpleFreeFieldHRIR_1.0.csv +47 -0
- sofar/sofa_conventions/conventions/SimpleFreeFieldHRIR_1.0.json +369 -0
- sofar/sofa_conventions/conventions/SimpleFreeFieldHRSOS_1.0.csv +43 -0
- sofar/sofa_conventions/conventions/SimpleFreeFieldHRSOS_1.0.json +349 -0
- sofar/sofa_conventions/conventions/SimpleFreeFieldHRTF_1.0.csv +44 -0
- sofar/sofa_conventions/conventions/SimpleFreeFieldHRTF_1.0.json +340 -0
- sofar/sofa_conventions/conventions/SimpleFreeFieldSOS_1.0.csv +43 -0
- sofar/sofa_conventions/conventions/SimpleFreeFieldSOS_1.0.json +349 -0
- sofar/sofa_conventions/conventions/SimpleHeadphoneIR_1.0.csv +51 -0
- sofar/sofa_conventions/conventions/SimpleHeadphoneIR_1.0.json +396 -0
- sofar/sofa_conventions/conventions/SingleRoomMIMOSRIR_1.0.csv +78 -0
- sofar/sofa_conventions/conventions/SingleRoomMIMOSRIR_1.0.json +601 -0
- sofar/sofa_conventions/conventions/SingleRoomSRIR_1.0.csv +78 -0
- sofar/sofa_conventions/conventions/SingleRoomSRIR_1.0.json +601 -0
- sofar/sofa_conventions/conventions/deprecated/AnnotatedEmitterAudio_0.1.csv +46 -0
- sofar/sofa_conventions/conventions/deprecated/AnnotatedEmitterAudio_0.1.json +351 -0
- sofar/sofa_conventions/conventions/deprecated/AnnotatedReceiverAudio_0.1.csv +46 -0
- sofar/sofa_conventions/conventions/deprecated/AnnotatedReceiverAudio_0.1.json +351 -0
- sofar/sofa_conventions/conventions/deprecated/FreeFieldDirectivityTF_1.0.csv +58 -0
- sofar/sofa_conventions/conventions/deprecated/FreeFieldDirectivityTF_1.0.json +437 -0
- sofar/sofa_conventions/conventions/deprecated/GeneralFIRE_1.0.csv +37 -0
- sofar/sofa_conventions/conventions/deprecated/GeneralFIRE_1.0.json +270 -0
- sofar/sofa_conventions/conventions/deprecated/MultiSpeakerBRIR_0.3.csv +48 -0
- sofar/sofa_conventions/conventions/deprecated/MultiSpeakerBRIR_0.3.json +376 -0
- sofar/sofa_conventions/conventions/deprecated/SimpleFreeFieldHRIR_0.4.csv +43 -0
- sofar/sofa_conventions/conventions/deprecated/SimpleFreeFieldHRIR_0.4.json +333 -0
- sofar/sofa_conventions/conventions/deprecated/SimpleFreeFieldTF_0.4.csv +44 -0
- sofar/sofa_conventions/conventions/deprecated/SimpleFreeFieldTF_0.4.json +340 -0
- sofar/sofa_conventions/conventions/deprecated/SimpleFreeFieldTF_1.0.csv +44 -0
- sofar/sofa_conventions/conventions/deprecated/SimpleFreeFieldTF_1.0.json +340 -0
- sofar/sofa_conventions/conventions/deprecated/SimpleHeadphoneIR_0.1.csv +51 -0
- sofar/sofa_conventions/conventions/deprecated/SimpleHeadphoneIR_0.1.json +396 -0
- sofar/sofa_conventions/conventions/deprecated/SimpleHeadphoneIR_0.2.csv +51 -0
- sofar/sofa_conventions/conventions/deprecated/SimpleHeadphoneIR_0.2.json +396 -0
- sofar/sofa_conventions/conventions/deprecated/SingleRoomDRIR_0.2.csv +47 -0
- sofar/sofa_conventions/conventions/deprecated/SingleRoomDRIR_0.2.json +360 -0
- sofar/sofa_conventions/conventions/deprecated/SingleRoomDRIR_0.3.csv +47 -0
- sofar/sofa_conventions/conventions/deprecated/SingleRoomDRIR_0.3.json +360 -0
- sofar/sofa_conventions/conventions/deprecated/SingleTrackedAudio_0.1.csv +47 -0
- sofar/sofa_conventions/conventions/deprecated/SingleTrackedAudio_0.1.json +366 -0
- sofar/sofa_conventions/conventions/deprecated/SingleTrackedAudio_0.2.csv +51 -0
- sofar/sofa_conventions/conventions/deprecated/SingleTrackedAudio_0.2.json +397 -0
- sofar/sofa_conventions/rules/deprecations.json +13 -0
- sofar/sofa_conventions/rules/rules.json +819 -0
- sofar/sofa_conventions/rules/unit_aliases.json +11 -0
- sofar/sofa_conventions/rules/upgrade.json +226 -0
- sofar/sofa_conventions/write_upgrade_rules.py +139 -0
- sofar/sofa_conventions/write_verification_data.py +313 -0
- sofar/sofa_conventions/write_verification_rules.py +356 -0
- sofar/sofastream.py +301 -0
- sofar/update_conventions.py +449 -0
- sofar/utils.py +316 -0
- sofar-1.2.1.dist-info/LICENSE +22 -0
- sofar-1.2.1.dist-info/METADATA +136 -0
- sofar-1.2.1.dist-info/RECORD +105 -0
- sofar-1.2.1.dist-info/WHEEL +5 -0
- sofar-1.2.1.dist-info/top_level.txt +3 -0
- tests/__init__.py +0 -0
- tests/conftest.py +27 -0
- tests/test_deprecations.py +19 -0
- tests/test_io.py +349 -0
- tests/test_sofa.py +353 -0
- tests/test_sofa_upgrade_conventions.py +111 -0
- tests/test_sofa_verify.py +480 -0
- tests/test_sofastream.py +127 -0
- tests/test_utils.py +250 -0
@@ -0,0 +1,356 @@
|
|
1
|
+
"""
|
2
|
+
Write rules for validating SOFA files to json files.
|
3
|
+
|
4
|
+
Writes the following json files:
|
5
|
+
rules.json
|
6
|
+
Rules for verifying SOFA conventions and files
|
7
|
+
unit_aliases.json
|
8
|
+
Allowed aliases for the standard units
|
9
|
+
|
10
|
+
For a more detailed information about the json files refer to _readme.txt
|
11
|
+
"""
|
12
|
+
# %%
|
13
|
+
import json
|
14
|
+
import os
|
15
|
+
from sofar.utils import _get_conventions
|
16
|
+
|
17
|
+
# deprecations
|
18
|
+
# NOTE: no further specific tests are enforced for deprecated conventions,
|
19
|
+
# i.e., there are no entries in `rules` for them.
|
20
|
+
deprecations = {
|
21
|
+
"GLOBAL:SOFAConventions": {
|
22
|
+
"GeneralFIRE":"GenerelFIR-E",
|
23
|
+
"MultiSpeakerBRIR": "SingleRoomMIMOSRIR",
|
24
|
+
"MusicalInstrumentDirectivityTF": "FreeFieldDirectivityTF",
|
25
|
+
"SimpleBRIR": "SingleRoomSRIR",
|
26
|
+
"SingleRoomDRIR": "SingleRoomSRIR",
|
27
|
+
"SimpleFreeFieldDirectivityTF": "FreeFieldDirectivityTF",
|
28
|
+
"SimpleFreeFieldSOS": "SimpleFreeFieldHRSOS",
|
29
|
+
"SimpleFreeFieldTF": "SimpleFreeFieldHRTF",
|
30
|
+
"SingleTrackedAudio":
|
31
|
+
"AnnotatedEmitterAudio or AnnotatedReceiverAudio",
|
32
|
+
},
|
33
|
+
}
|
34
|
+
|
35
|
+
# definition of valid coordinate systems. Not defined explicitly in AES69 but
|
36
|
+
# dozens of mentions of the following:
|
37
|
+
coords_min = ["cartesian", "spherical"]
|
38
|
+
coords_full = coords_min + ["spherical harmonics"]
|
39
|
+
|
40
|
+
# definition of units acordings to AES69 Table 7
|
41
|
+
units_min = ["metre", "degree, degree, metre"]
|
42
|
+
units_full = units_min + [units_min[1]]
|
43
|
+
|
44
|
+
# allowed alternative versions of units
|
45
|
+
# NOTE: AES69 allows multi-unit strings to be separated by comma, comma plus
|
46
|
+
# space and space (AES69 Section 4.7.8). This means that the unit
|
47
|
+
# "cubic metre" will be tested as a multi unit string and is thus also
|
48
|
+
# split in the aliases. A separate test for verifying that "cubic" is
|
49
|
+
# followed be "metre" must be performed.
|
50
|
+
unit_aliases = {
|
51
|
+
"metre": "metre",
|
52
|
+
"metres": "metre",
|
53
|
+
"meter": "metre",
|
54
|
+
"meters": "metre",
|
55
|
+
"cubic": "cubic",
|
56
|
+
"degree": "degree",
|
57
|
+
"degrees": "degree",
|
58
|
+
"second": "second",
|
59
|
+
"seconds": "second",
|
60
|
+
}
|
61
|
+
# possible values for restricted dimensions in the API
|
62
|
+
# Dimensions for spherical harmonics (considering orders up to 100) according
|
63
|
+
# to AES69 Eq. (5) and (6)
|
64
|
+
sh_dimension = ([(N+1)**2 for N in range(100)],
|
65
|
+
"(N+1)**2 where N is the spherical harmonics order")
|
66
|
+
# Dimensions for SOS (considering up to 100 sections) according to AES69
|
67
|
+
# Appendix C.5.2
|
68
|
+
sos_dimension = ([6 * (N + 1) for N in range(100)],
|
69
|
+
"an integer multiple of 6 greater 0")
|
70
|
+
|
71
|
+
# verification rules
|
72
|
+
rules = {
|
73
|
+
# Global --------------------------------------------------------------
|
74
|
+
# Check value of GLOBAL_DataType (AES69 Annex C)
|
75
|
+
# (FIRE and TFE are legacy data types from SOFA version 1.0)
|
76
|
+
"GLOBAL:DataType": {
|
77
|
+
"value": ["Audio", "FIR", "FIR-E", "FIRE", "TF", "TF-E", "SOS"],
|
78
|
+
"specific": {
|
79
|
+
"Audio": {
|
80
|
+
"Data.SamplingRate": None,
|
81
|
+
"Data.SamplingRate:Units": ["hertz"]},
|
82
|
+
"FIR": {
|
83
|
+
"Data.IR": None,
|
84
|
+
"Data.Delay": None,
|
85
|
+
"Data.SamplingRate": None,
|
86
|
+
"Data.SamplingRate:Units": ["hertz"]},
|
87
|
+
"FIR-E": {
|
88
|
+
"Data.IR": None,
|
89
|
+
"Data.Delay": None,
|
90
|
+
"Data.SamplingRate": None,
|
91
|
+
"Data.SamplingRate:Units": ["hertz"]},
|
92
|
+
"FIRE": {
|
93
|
+
"Data.IR": None,
|
94
|
+
"Data.Delay": None,
|
95
|
+
"Data.SamplingRate": None,
|
96
|
+
"Data.SamplingRate:Units": ["hertz"]},
|
97
|
+
"TF": {
|
98
|
+
"Data.Real": None,
|
99
|
+
"Data.Imag": None,
|
100
|
+
"N": None,
|
101
|
+
"N:Units": ["hertz"]},
|
102
|
+
"TF-E": {
|
103
|
+
"Data.Real": None,
|
104
|
+
"Data.Imag": None,
|
105
|
+
"N": None,
|
106
|
+
"N:Units": ["hertz"]},
|
107
|
+
"TFE": {
|
108
|
+
"Data.Real": None,
|
109
|
+
"Data.Imag": None,
|
110
|
+
"N": None,
|
111
|
+
"N:Units": ["hertz"]},
|
112
|
+
"SOS": {
|
113
|
+
"Data.SOS": None,
|
114
|
+
"Data.Delay": None,
|
115
|
+
"Data.SamplingRate": None,
|
116
|
+
"Data.SamplingRate:Units": ["hertz"],
|
117
|
+
# Checking the dimension of N if having SOS data
|
118
|
+
# (assuming up to 100 second order sections)
|
119
|
+
"_dimensions": {
|
120
|
+
"N": {
|
121
|
+
"value": sos_dimension[0],
|
122
|
+
"value_str": sos_dimension[1]},
|
123
|
+
}}}},
|
124
|
+
# Specified in AES69 Section 4.7.7 and Table 6
|
125
|
+
"GLOBAL:RoomType": {
|
126
|
+
"value": ["free field", "reverberant", "shoebox", "dae"],
|
127
|
+
"specific": {
|
128
|
+
"reverberant": {
|
129
|
+
"GLOBAL:RoomDescription": None},
|
130
|
+
"shoebox": {
|
131
|
+
"RoomCornerA": None,
|
132
|
+
"RoomCornerB": None},
|
133
|
+
"dae": {
|
134
|
+
"GLOBAL:RoomGeometry": None}}},
|
135
|
+
# Specified in AES69 Annex D
|
136
|
+
"GLOBAL:SOFAConventions": {
|
137
|
+
"value": _get_conventions(return_type="name"),
|
138
|
+
"specific": {
|
139
|
+
"AnnotatedEmitterAudio": {
|
140
|
+
"Data.Emitter": None},
|
141
|
+
"AnnotatedReceiverAudio": {
|
142
|
+
"Data.Receiver": None},
|
143
|
+
"GeneralFIR": {
|
144
|
+
"GLOBAL:DataType": ["FIR"]},
|
145
|
+
"GeneralFIR-E": {
|
146
|
+
"GLOBAL:DataType": ["FIR-E"]},
|
147
|
+
"GeneralTF": {
|
148
|
+
"GLOBAL:DataType": ["TF"]},
|
149
|
+
"GeneralTF-E": {
|
150
|
+
"GLOBAL:DataType": ["TF-E"]},
|
151
|
+
"SimpleFreeFieldHRIR": {
|
152
|
+
"GLOBAL:DataType": ["FIR"],
|
153
|
+
"GLOBAL:RoomType": ["free field"],
|
154
|
+
"EmitterPosition:Type": coords_min,
|
155
|
+
"_dimensions": {
|
156
|
+
"E": {
|
157
|
+
"value": [1],
|
158
|
+
"value_str": "1"}}},
|
159
|
+
"SimpleFreeFieldHRTF": {
|
160
|
+
"GLOBAL:DataType": ["TF"],
|
161
|
+
"GLOBAL:RoomType": ["free field"],
|
162
|
+
"EmitterPosition:Type": coords_min,
|
163
|
+
"_dimensions": {
|
164
|
+
"E": {
|
165
|
+
"value": [1],
|
166
|
+
"value_str": "1"}}},
|
167
|
+
"SimpleFreeFieldHRSOS": {
|
168
|
+
"GLOBAL:DataType": ["SOS"],
|
169
|
+
"GLOBAL:RoomType": ["free field"],
|
170
|
+
"EmitterPosition:Type": coords_min,
|
171
|
+
"_dimensions": {
|
172
|
+
"E": {
|
173
|
+
"value": [1],
|
174
|
+
"value_str": "1"}}},
|
175
|
+
"FreeFieldHRIR": {
|
176
|
+
"GLOBAL:DataType": ["FIR-E"],
|
177
|
+
"GLOBAL:RoomType": ["free field"]},
|
178
|
+
"FreeFieldHRTF": {
|
179
|
+
"GLOBAL:DataType": ["TF-E"],
|
180
|
+
"GLOBAL:RoomType": ["free field"]},
|
181
|
+
"SimpleHeadphoneIR": {
|
182
|
+
"GLOBAL:DataType": ["FIR"]},
|
183
|
+
"SingleRoomSRIR": {
|
184
|
+
"GLOBAL:DataType": ["FIR"]},
|
185
|
+
"SingleRoomMIMOSRIR": {
|
186
|
+
"GLOBAL:DataType": ["FIR-E"]},
|
187
|
+
"FreeFieldDirectivityTF": {
|
188
|
+
"GLOBAL:DataType": ["TF"]}}},
|
189
|
+
# check NLongName (AES69 Tables C.3 and C.4)
|
190
|
+
"N:LongName": {
|
191
|
+
"value": ["frequency"]},
|
192
|
+
# Listener ------------------------------------------------------------
|
193
|
+
# Possible values and dependencies are specified in
|
194
|
+
# AES69 Section 4.7.3
|
195
|
+
# ---------------------------------------------------------------------
|
196
|
+
# Check values and consistency of if ListenerPosition Type and Unit
|
197
|
+
"ListenerPosition:Type": {
|
198
|
+
"value": coords_min,
|
199
|
+
"specific": {
|
200
|
+
coords_min[0]: {
|
201
|
+
"ListenerPosition:Units": [units_min[0]]},
|
202
|
+
coords_min[1]: {
|
203
|
+
"ListenerPosition:Units": [units_min[1]]},
|
204
|
+
}},
|
205
|
+
# Check if dependencies of ListenerView are contained
|
206
|
+
"ListenerView": {
|
207
|
+
"value": None,
|
208
|
+
"general": ["ListenerView:Type", "ListenerView:Units"]},
|
209
|
+
# Check values and consistency of if ListenerView Type and Unit
|
210
|
+
"ListenerView:Type": {
|
211
|
+
"value": coords_min,
|
212
|
+
"specific": {
|
213
|
+
coords_min[0]: {
|
214
|
+
"ListenerView:Units": [units_min[0]]},
|
215
|
+
coords_min[1]: {
|
216
|
+
"ListenerView:Units": [units_min[1]]},
|
217
|
+
}},
|
218
|
+
# Check if dependencies of ListenerUp are contained
|
219
|
+
"ListenerUp": {
|
220
|
+
"value": None,
|
221
|
+
"general": ["ListenerView"]},
|
222
|
+
# Receiver ------------------------------------------------------------
|
223
|
+
# Possible values and dependencies are specified in
|
224
|
+
# AES69 Section 4.7.4
|
225
|
+
# ---------------------------------------------------------------------
|
226
|
+
# Check values and consistency of if ReceiverPosition Type and Unit
|
227
|
+
"ReceiverPosition:Type": {
|
228
|
+
"value": coords_full,
|
229
|
+
"specific": {
|
230
|
+
coords_full[0]: {
|
231
|
+
"ReceiverPosition:Units": [units_full[0]]},
|
232
|
+
coords_full[1]: {
|
233
|
+
"ReceiverPosition:Units": [units_full[1]]},
|
234
|
+
coords_full[2]: {
|
235
|
+
"ReceiverPosition:Units": [units_full[2]],
|
236
|
+
"_dimensions": {
|
237
|
+
# Check dimension R if using spherical harmonics for the
|
238
|
+
# receiver (assuming SH orders < 100)
|
239
|
+
"R": {
|
240
|
+
"value": sh_dimension[0],
|
241
|
+
"value_str": sh_dimension[1]}}},
|
242
|
+
}},
|
243
|
+
# Check if dependencies of ReceiverView are contained
|
244
|
+
"ReceiverView": {
|
245
|
+
"value": None,
|
246
|
+
"general": ["ReceiverView:Type", "ReceiverView:Units"]},
|
247
|
+
# Check values and consistency of if ReceiverView Type and Unit
|
248
|
+
"ReceiverView:Type": {
|
249
|
+
"value": coords_min,
|
250
|
+
"specific": {
|
251
|
+
coords_min[0]: {
|
252
|
+
"ReceiverView:Units": [units_min[0]]},
|
253
|
+
coords_min[1]: {
|
254
|
+
"ReceiverView:Units": [units_min[1]]},
|
255
|
+
}},
|
256
|
+
# Check if dependencies of ReceiverUp are contained
|
257
|
+
"ReceiverUp": {
|
258
|
+
"value": None,
|
259
|
+
"general": ["ReceiverView"]},
|
260
|
+
# Source --------------------------------------------------------------
|
261
|
+
# Possible values and dependencies are specified in
|
262
|
+
# AES69 Section 4.7.5
|
263
|
+
# ---------------------------------------------------------------------
|
264
|
+
# Check values and consistency of if SourcePosition Type and Unit
|
265
|
+
"SourcePosition:Type": {
|
266
|
+
"value": coords_min,
|
267
|
+
"specific": {
|
268
|
+
coords_min[0]: {
|
269
|
+
"SourcePosition:Units": [units_min[0]]},
|
270
|
+
coords_min[1]: {
|
271
|
+
"SourcePosition:Units": [units_min[1]]},
|
272
|
+
}},
|
273
|
+
# Check if dependencies of SourceView are contained
|
274
|
+
"SourceView": {
|
275
|
+
"value": None,
|
276
|
+
"general": ["SourceView:Type", "SourceView:Units"]},
|
277
|
+
# Check values and consistency of if SourceView Type and Unit
|
278
|
+
"SourceView:Type": {
|
279
|
+
"value": coords_min,
|
280
|
+
"specific": {
|
281
|
+
coords_min[0]: {
|
282
|
+
"SourceView:Units": [units_min[0]]},
|
283
|
+
coords_min[1]: {
|
284
|
+
"SourceView:Units": [units_min[1]]},
|
285
|
+
}},
|
286
|
+
# Check if dependencies of SourceUp are contained
|
287
|
+
"SourceUp": {
|
288
|
+
"value": None,
|
289
|
+
"general": ["SourceView"]},
|
290
|
+
# Emitter -------------------------------------------------------------
|
291
|
+
# Possible values and dependencies are specified in
|
292
|
+
# AES69 Section 4.7.6
|
293
|
+
# ---------------------------------------------------------------------
|
294
|
+
# Check values and consistency of if EmitterPosition Type and Unit
|
295
|
+
"EmitterPosition:Type": {
|
296
|
+
"value": coords_full,
|
297
|
+
"specific": {
|
298
|
+
coords_full[0]: {
|
299
|
+
"EmitterPosition:Units": [units_full[0]]},
|
300
|
+
coords_full[1]: {
|
301
|
+
"EmitterPosition:Units": [units_full[1]]},
|
302
|
+
coords_full[2]: {
|
303
|
+
"EmitterPosition:Units": [units_full[2]],
|
304
|
+
"_dimensions": {
|
305
|
+
# Check dimension R if using spherical harmonics for the
|
306
|
+
# receiver (assuming SH orders < 100)
|
307
|
+
"E": {
|
308
|
+
"value": sh_dimension[0],
|
309
|
+
"value_str": sh_dimension[1]}}},
|
310
|
+
}},
|
311
|
+
# Check if dependencies of EmitterView are contained
|
312
|
+
"EmitterView": {
|
313
|
+
"value": None,
|
314
|
+
"general": ["EmitterView:Type", "EmitterView:Units"]},
|
315
|
+
# Check values and consistency of if EmitterView Type and Unit
|
316
|
+
"EmitterView:Type": {
|
317
|
+
"value": coords_min,
|
318
|
+
"specific": {
|
319
|
+
coords_min[0]: {
|
320
|
+
"EmitterView:Units": [units_min[0]]},
|
321
|
+
coords_min[1]: {
|
322
|
+
"EmitterView:Units": [units_min[1]]},
|
323
|
+
}},
|
324
|
+
# Check if dependencies of EmitterUp are contained
|
325
|
+
"EmitterUp": {
|
326
|
+
"value": None,
|
327
|
+
"general": ["EmitterView"]},
|
328
|
+
# Check if dependencies of EmitterDescriptions are contained (specified in
|
329
|
+
# 5.3, D.12.5, D.13.5, D.14.5, D15.5)
|
330
|
+
"EmitterDescriptions": {
|
331
|
+
"value": None,
|
332
|
+
"general": ["GLOBAL:EmitterDescription"]},
|
333
|
+
# Room ----------------------------------------------------------------
|
334
|
+
# Possible values and dependencies are specified in
|
335
|
+
# AES69 Section 4.7.7
|
336
|
+
# ---------------------------------------------------------------------
|
337
|
+
"RoomVolume": {
|
338
|
+
"value": None,
|
339
|
+
"general": ["RoomVolume:Units"]},
|
340
|
+
"RoomTemperature": {
|
341
|
+
"value": None,
|
342
|
+
"general": ["RoomTemperature:Units"]},
|
343
|
+
"RoomVolume:Units": {
|
344
|
+
"value": ["cubic metre"]},
|
345
|
+
"RoomTemperature:Units": {
|
346
|
+
"value": ["kelvin"]},
|
347
|
+
}
|
348
|
+
|
349
|
+
# write to json files
|
350
|
+
for content, name in zip([rules, unit_aliases, deprecations],
|
351
|
+
["rules", "unit_aliases", "deprecations"]):
|
352
|
+
|
353
|
+
json_file = os.path.join(
|
354
|
+
os.path.dirname(__file__), "rules", name + '.json')
|
355
|
+
with open(json_file, 'w') as file:
|
356
|
+
json.dump(content, file, indent=4)
|
sofar/sofastream.py
ADDED
@@ -0,0 +1,301 @@
|
|
1
|
+
"""
|
2
|
+
Read desired data from SOFA-file directly from disk without loading entire
|
3
|
+
file into memory.
|
4
|
+
"""
|
5
|
+
from netCDF4 import Dataset
|
6
|
+
import numpy as np
|
7
|
+
|
8
|
+
|
9
|
+
class SofaStream():
|
10
|
+
"""
|
11
|
+
Read desired data from SOFA-file directly from disk without loading entire
|
12
|
+
file into memory.
|
13
|
+
|
14
|
+
:class:`SofaStream` opens a SOFA-file and retrieves only the requested
|
15
|
+
data.
|
16
|
+
|
17
|
+
If you want to use all the data from a SOFA-file use :class:`Sofa`
|
18
|
+
class and :func:`read_sofa` function instead.
|
19
|
+
|
20
|
+
Parameters
|
21
|
+
----------
|
22
|
+
filename : str
|
23
|
+
Full path to a SOFA-file
|
24
|
+
|
25
|
+
Returns
|
26
|
+
-------
|
27
|
+
sofa_stream : SofaStream
|
28
|
+
A SofaStream object which reads directly from the file.
|
29
|
+
|
30
|
+
Examples
|
31
|
+
--------
|
32
|
+
Get an attribute from a SOFA-file:
|
33
|
+
|
34
|
+
>>> import sofar as sf
|
35
|
+
>>> filename = "path/to/file.sofa"
|
36
|
+
>>> with sf.SofaStream(filename) as file:
|
37
|
+
>>> data = file.GLOBAL_RoomType
|
38
|
+
>>> print(data)
|
39
|
+
free field
|
40
|
+
|
41
|
+
Get a variable from a SOFA-file:
|
42
|
+
|
43
|
+
>>> with SofaStream(filename) as file:
|
44
|
+
>>> data = file.Data_IR
|
45
|
+
>>> print(data)
|
46
|
+
<class 'netCDF4._netCDF4.Variable'>
|
47
|
+
float64 Data.IR(M, R, N)
|
48
|
+
unlimited dimensions:
|
49
|
+
current shape = (11950, 2, 256)
|
50
|
+
filling on, default _FillValue of 9.969209968386869e+36 used
|
51
|
+
|
52
|
+
What is returned is a `netCDF-variable`. To access the values (in this
|
53
|
+
example the IRs) the variable needs to be sliced:
|
54
|
+
|
55
|
+
>>> with SofaStream(filename) as file:
|
56
|
+
>>> data = file.Data_IR
|
57
|
+
>>> # get all values
|
58
|
+
>>> all_irs = data[:]
|
59
|
+
>>> print(all_irs.shape)
|
60
|
+
(11950, 2, 256)
|
61
|
+
>>> # get data from first channel
|
62
|
+
>>> specific_irs = data[:,0,:]
|
63
|
+
>>> print(specific_irs.shape)
|
64
|
+
(11950, 256)
|
65
|
+
"""
|
66
|
+
|
67
|
+
def __init__(self, filename):
|
68
|
+
"""Initialize a new SofaStream object (see documentation above)."""
|
69
|
+
self._filename = filename
|
70
|
+
|
71
|
+
def __enter__(self):
|
72
|
+
"""
|
73
|
+
Executed when entering a ``with`` statement
|
74
|
+
(see documentation above).
|
75
|
+
"""
|
76
|
+
self._file = Dataset(self._filename, mode="r")
|
77
|
+
return self
|
78
|
+
|
79
|
+
def __exit__(self, *args):
|
80
|
+
"""
|
81
|
+
Executed when exiting a ``with`` statement
|
82
|
+
(see documentation above).
|
83
|
+
"""
|
84
|
+
self._file.close()
|
85
|
+
|
86
|
+
def __getattr__(self, name):
|
87
|
+
"""
|
88
|
+
Executed when accessing data within a with statement
|
89
|
+
(see documentation above).
|
90
|
+
"""
|
91
|
+
# get netCDF4-attributes and -variable-keys from SOFA-file
|
92
|
+
dset_variables = np.array(list(self._file.variables.keys()))
|
93
|
+
dset_attributes = np.asarray(self._file.ncattrs())
|
94
|
+
|
95
|
+
# remove delimiter from passed sofar-attribute
|
96
|
+
name_netcdf = name.replace(
|
97
|
+
'GLOBAL_', '').replace('Data_', 'Data.')
|
98
|
+
|
99
|
+
# Handle variable-attributes (e.g. '_Units' and '_Type')
|
100
|
+
var_attr = None
|
101
|
+
if "_" in name_netcdf:
|
102
|
+
name_netcdf, var_attr = name_netcdf.split('_')
|
103
|
+
|
104
|
+
# get value if passed attribute points to a netCDF4-variable
|
105
|
+
if name_netcdf in dset_variables:
|
106
|
+
# get variable from SOFA-file
|
107
|
+
self._data = self._file.variables[name_netcdf]
|
108
|
+
if var_attr is not None:
|
109
|
+
self._data = getattr(self._data, var_attr)
|
110
|
+
|
111
|
+
# get value if passed attribute points to a netCDF4-attribute
|
112
|
+
elif name_netcdf in dset_attributes:
|
113
|
+
# get attribute value from SOFA-file
|
114
|
+
self._data = self._file.getncattr(name_netcdf)
|
115
|
+
|
116
|
+
else:
|
117
|
+
raise AttributeError(f"{name} is not contained in SOFA-file")
|
118
|
+
|
119
|
+
return self._data
|
120
|
+
|
121
|
+
@property
|
122
|
+
def list_dimensions(self):
|
123
|
+
"""
|
124
|
+
Print the dimensions of the SOFA-file.
|
125
|
+
|
126
|
+
See :py:func:`~SofaStream.inspect` to see the shapes of the data inside
|
127
|
+
the SOFA-file and :py:func:`~SofaStream.get_dimension` to get the
|
128
|
+
size/value of a specific dimensions as integer number.
|
129
|
+
|
130
|
+
The SOFA standard defines the following dimensions that are used
|
131
|
+
to define the shape of the data entries:
|
132
|
+
|
133
|
+
M
|
134
|
+
number of measurements
|
135
|
+
N
|
136
|
+
number of samples, frequencies, SOS coefficients
|
137
|
+
(depending on GLOBAL_DataType)
|
138
|
+
R
|
139
|
+
Number of receivers or SH coefficients
|
140
|
+
(depending on ReceiverPosition_Type)
|
141
|
+
E
|
142
|
+
Number of emitters or SH coefficients
|
143
|
+
(depending on EmitterPosition_Type)
|
144
|
+
S
|
145
|
+
Maximum length of a string in a string array
|
146
|
+
C
|
147
|
+
Size of the coordinate dimension. This is always three.
|
148
|
+
I
|
149
|
+
Single dimension. This is always one.
|
150
|
+
|
151
|
+
"""
|
152
|
+
dim = self._file.dimensions
|
153
|
+
|
154
|
+
# get verbose description for dimesion N
|
155
|
+
if self._file.getncattr('DataType').startswith("FIR"):
|
156
|
+
N_verbose = "samples"
|
157
|
+
elif self._file.getncattr('DataType').startswith("TF"):
|
158
|
+
N_verbose = "frequencies"
|
159
|
+
elif self._file.getncattr('DataType').startswith("SOS"):
|
160
|
+
N_verbose = "SOS coefficients"
|
161
|
+
|
162
|
+
# get verbose description for dimensions R and E
|
163
|
+
R_verbose = (
|
164
|
+
"receiver spherical harmonics coefficients"
|
165
|
+
if 'harmonic'
|
166
|
+
in self._file.variables['ReceiverPosition'].getncattr('Type')
|
167
|
+
else "receiver"
|
168
|
+
)
|
169
|
+
E_verbose = (
|
170
|
+
"emitter spherical harmonics coefficients"
|
171
|
+
if 'harmonic'
|
172
|
+
in self._file.variables['EmitterPosition'].getncattr('Type')
|
173
|
+
else "emitter"
|
174
|
+
)
|
175
|
+
|
176
|
+
dimensions = {
|
177
|
+
"M": "measurements",
|
178
|
+
"N": N_verbose,
|
179
|
+
"R": R_verbose,
|
180
|
+
"E": E_verbose,
|
181
|
+
"S": "maximum string length",
|
182
|
+
"C": "coordinate dimensions, fixed",
|
183
|
+
"I": "single dimension, fixed"}
|
184
|
+
|
185
|
+
info_str = ""
|
186
|
+
for key, value in dim.items():
|
187
|
+
value = value.size
|
188
|
+
dim_info = dimensions[key] if key in dimensions \
|
189
|
+
else "custom dimension"
|
190
|
+
|
191
|
+
info_str += f"{key} = {value} {dim_info}" + '\n'
|
192
|
+
|
193
|
+
print(info_str)
|
194
|
+
|
195
|
+
def get_dimension(self, dimension):
|
196
|
+
"""
|
197
|
+
Get size of a SOFA dimension.
|
198
|
+
|
199
|
+
SOFA dimensions specify the shape of the data contained in a SOFA-file.
|
200
|
+
For a list of all dimensions see :py:func:`~list_dimensions`, for more
|
201
|
+
information about the data contained in a SOFA-file use
|
202
|
+
:py:func:`~inspect`.
|
203
|
+
|
204
|
+
Parameters
|
205
|
+
----------
|
206
|
+
dimension : str
|
207
|
+
The dimension as a string, e.g., ``'N'``.
|
208
|
+
|
209
|
+
Returns
|
210
|
+
-------
|
211
|
+
size : int
|
212
|
+
the size of the queried dimension.
|
213
|
+
"""
|
214
|
+
|
215
|
+
# get dimensons from SOFA-file
|
216
|
+
dims = self._file.dimensions
|
217
|
+
|
218
|
+
if dimension not in dims.keys():
|
219
|
+
raise ValueError((
|
220
|
+
f"{dimension} is not a valid dimension. "
|
221
|
+
"See Sofa.list_dimensions for a list of valid dimensions."))
|
222
|
+
|
223
|
+
return dims[dimension].size
|
224
|
+
|
225
|
+
def inspect(self, file=None):
|
226
|
+
"""
|
227
|
+
Get information about the data inside a SOFA-file.
|
228
|
+
|
229
|
+
Prints the values of all attributes and variables with six or less
|
230
|
+
entries and the shapes and type of all numeric and string variables.
|
231
|
+
When printing the values of arrays, single dimensions are discarded for
|
232
|
+
easy of display, i.e., an array of shape (1, 3, 2) will be displayed as
|
233
|
+
an array of shape (3, 2).
|
234
|
+
|
235
|
+
Parameters
|
236
|
+
----------
|
237
|
+
file : str
|
238
|
+
Full path of a file under which the information is to be stored in
|
239
|
+
plain text. The default ``None`` only print the information to the
|
240
|
+
console.
|
241
|
+
"""
|
242
|
+
|
243
|
+
# Header of inspect-print
|
244
|
+
info_str = (
|
245
|
+
f"{self._file.getncattr('SOFAConventions')} "
|
246
|
+
f"{self._file.getncattr('SOFAConventionsVersion')} "
|
247
|
+
f"(SOFA version {self._file.getncattr('Version')})\n")
|
248
|
+
info_str += "-" * len(info_str) + "\n"
|
249
|
+
|
250
|
+
# information for attributes
|
251
|
+
for attr in self._file.ncattrs():
|
252
|
+
|
253
|
+
value = self._file.getncattr(attr)
|
254
|
+
sofar_attr = f"GLOBAL_{attr}"
|
255
|
+
info_str += sofar_attr + ' : ' + str(value) + '\n'
|
256
|
+
|
257
|
+
# information for variables
|
258
|
+
for key in self._file.variables.keys():
|
259
|
+
# get values, shape and dimensions
|
260
|
+
data = self._file[key]
|
261
|
+
shape = data.shape
|
262
|
+
dimensions = data.dimensions
|
263
|
+
|
264
|
+
# add variable name to info-string
|
265
|
+
info_str += key.replace('.', '_') + ' : '
|
266
|
+
|
267
|
+
# pad shape if required (trailing single dimensions are
|
268
|
+
# discarded following the numpy default)
|
269
|
+
while len(shape) < len(dimensions):
|
270
|
+
shape += (1, )
|
271
|
+
|
272
|
+
# add value for scalars
|
273
|
+
if data.size == 1:
|
274
|
+
info_str += str(data[:][0]) + '\n'
|
275
|
+
|
276
|
+
# Handle multidimensional data
|
277
|
+
else:
|
278
|
+
# make verbose shape, e.g., '(M=100, R=2, N=128, '
|
279
|
+
shape_verbose = "("
|
280
|
+
for s, d in zip(shape, dimensions):
|
281
|
+
shape_verbose += f"{d}={s}, "
|
282
|
+
|
283
|
+
# add shape information
|
284
|
+
info_str += shape_verbose[:-2] + ")\n"
|
285
|
+
# add value information if not too much
|
286
|
+
if data.size < 7:
|
287
|
+
info_str += " " + \
|
288
|
+
str(np.squeeze(data[:])).replace("\n", "\n ") + "\n"
|
289
|
+
|
290
|
+
# Add variable-attributes to info string (e.g. 'Type' or 'Units)
|
291
|
+
for att_ in list(self._file[key].ncattrs()):
|
292
|
+
info_str += (key.replace('.', '_') + f'_{att_} : '
|
293
|
+
+ getattr(data, att_) + '\n')
|
294
|
+
|
295
|
+
# write to text file
|
296
|
+
if file is not None:
|
297
|
+
with open(file, 'w') as f_id:
|
298
|
+
f_id.write(info_str + "\n")
|
299
|
+
|
300
|
+
# print to console
|
301
|
+
print(info_str)
|