nahue-passano commited on
Commit
f7fb447
1 Parent(s): 4d4b7d0

initial commit

Browse files
LICENSE ADDED
@@ -0,0 +1,339 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ GNU GENERAL PUBLIC LICENSE
2
+ Version 2, June 1991
3
+
4
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
5
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
6
+ Everyone is permitted to copy and distribute verbatim copies
7
+ of this license document, but changing it is not allowed.
8
+
9
+ Preamble
10
+
11
+ The licenses for most software are designed to take away your
12
+ freedom to share and change it. By contrast, the GNU General Public
13
+ License is intended to guarantee your freedom to share and change free
14
+ software--to make sure the software is free for all its users. This
15
+ General Public License applies to most of the Free Software
16
+ Foundation's software and to any other program whose authors commit to
17
+ using it. (Some other Free Software Foundation software is covered by
18
+ the GNU Lesser General Public License instead.) You can apply it to
19
+ your programs, too.
20
+
21
+ When we speak of free software, we are referring to freedom, not
22
+ price. Our General Public Licenses are designed to make sure that you
23
+ have the freedom to distribute copies of free software (and charge for
24
+ this service if you wish), that you receive source code or can get it
25
+ if you want it, that you can change the software or use pieces of it
26
+ in new free programs; and that you know you can do these things.
27
+
28
+ To protect your rights, we need to make restrictions that forbid
29
+ anyone to deny you these rights or to ask you to surrender the rights.
30
+ These restrictions translate to certain responsibilities for you if you
31
+ distribute copies of the software, or if you modify it.
32
+
33
+ For example, if you distribute copies of such a program, whether
34
+ gratis or for a fee, you must give the recipients all the rights that
35
+ you have. You must make sure that they, too, receive or can get the
36
+ source code. And you must show them these terms so they know their
37
+ rights.
38
+
39
+ We protect your rights with two steps: (1) copyright the software, and
40
+ (2) offer you this license which gives you legal permission to copy,
41
+ distribute and/or modify the software.
42
+
43
+ Also, for each author's protection and ours, we want to make certain
44
+ that everyone understands that there is no warranty for this free
45
+ software. If the software is modified by someone else and passed on, we
46
+ want its recipients to know that what they have is not the original, so
47
+ that any problems introduced by others will not reflect on the original
48
+ authors' reputations.
49
+
50
+ Finally, any free program is threatened constantly by software
51
+ patents. We wish to avoid the danger that redistributors of a free
52
+ program will individually obtain patent licenses, in effect making the
53
+ program proprietary. To prevent this, we have made it clear that any
54
+ patent must be licensed for everyone's free use or not licensed at all.
55
+
56
+ The precise terms and conditions for copying, distribution and
57
+ modification follow.
58
+
59
+ GNU GENERAL PUBLIC LICENSE
60
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
61
+
62
+ 0. This License applies to any program or other work which contains
63
+ a notice placed by the copyright holder saying it may be distributed
64
+ under the terms of this General Public License. The "Program", below,
65
+ refers to any such program or work, and a "work based on the Program"
66
+ means either the Program or any derivative work under copyright law:
67
+ that is to say, a work containing the Program or a portion of it,
68
+ either verbatim or with modifications and/or translated into another
69
+ language. (Hereinafter, translation is included without limitation in
70
+ the term "modification".) Each licensee is addressed as "you".
71
+
72
+ Activities other than copying, distribution and modification are not
73
+ covered by this License; they are outside its scope. The act of
74
+ running the Program is not restricted, and the output from the Program
75
+ is covered only if its contents constitute a work based on the
76
+ Program (independent of having been made by running the Program).
77
+ Whether that is true depends on what the Program does.
78
+
79
+ 1. You may copy and distribute verbatim copies of the Program's
80
+ source code as you receive it, in any medium, provided that you
81
+ conspicuously and appropriately publish on each copy an appropriate
82
+ copyright notice and disclaimer of warranty; keep intact all the
83
+ notices that refer to this License and to the absence of any warranty;
84
+ and give any other recipients of the Program a copy of this License
85
+ along with the Program.
86
+
87
+ You may charge a fee for the physical act of transferring a copy, and
88
+ you may at your option offer warranty protection in exchange for a fee.
89
+
90
+ 2. You may modify your copy or copies of the Program or any portion
91
+ of it, thus forming a work based on the Program, and copy and
92
+ distribute such modifications or work under the terms of Section 1
93
+ above, provided that you also meet all of these conditions:
94
+
95
+ a) You must cause the modified files to carry prominent notices
96
+ stating that you changed the files and the date of any change.
97
+
98
+ b) You must cause any work that you distribute or publish, that in
99
+ whole or in part contains or is derived from the Program or any
100
+ part thereof, to be licensed as a whole at no charge to all third
101
+ parties under the terms of this License.
102
+
103
+ c) If the modified program normally reads commands interactively
104
+ when run, you must cause it, when started running for such
105
+ interactive use in the most ordinary way, to print or display an
106
+ announcement including an appropriate copyright notice and a
107
+ notice that there is no warranty (or else, saying that you provide
108
+ a warranty) and that users may redistribute the program under
109
+ these conditions, and telling the user how to view a copy of this
110
+ License. (Exception: if the Program itself is interactive but
111
+ does not normally print such an announcement, your work based on
112
+ the Program is not required to print an announcement.)
113
+
114
+ These requirements apply to the modified work as a whole. If
115
+ identifiable sections of that work are not derived from the Program,
116
+ and can be reasonably considered independent and separate works in
117
+ themselves, then this License, and its terms, do not apply to those
118
+ sections when you distribute them as separate works. But when you
119
+ distribute the same sections as part of a whole which is a work based
120
+ on the Program, the distribution of the whole must be on the terms of
121
+ this License, whose permissions for other licensees extend to the
122
+ entire whole, and thus to each and every part regardless of who wrote it.
123
+
124
+ Thus, it is not the intent of this section to claim rights or contest
125
+ your rights to work written entirely by you; rather, the intent is to
126
+ exercise the right to control the distribution of derivative or
127
+ collective works based on the Program.
128
+
129
+ In addition, mere aggregation of another work not based on the Program
130
+ with the Program (or with a work based on the Program) on a volume of
131
+ a storage or distribution medium does not bring the other work under
132
+ the scope of this License.
133
+
134
+ 3. You may copy and distribute the Program (or a work based on it,
135
+ under Section 2) in object code or executable form under the terms of
136
+ Sections 1 and 2 above provided that you also do one of the following:
137
+
138
+ a) Accompany it with the complete corresponding machine-readable
139
+ source code, which must be distributed under the terms of Sections
140
+ 1 and 2 above on a medium customarily used for software interchange; or,
141
+
142
+ b) Accompany it with a written offer, valid for at least three
143
+ years, to give any third party, for a charge no more than your
144
+ cost of physically performing source distribution, a complete
145
+ machine-readable copy of the corresponding source code, to be
146
+ distributed under the terms of Sections 1 and 2 above on a medium
147
+ customarily used for software interchange; or,
148
+
149
+ c) Accompany it with the information you received as to the offer
150
+ to distribute corresponding source code. (This alternative is
151
+ allowed only for noncommercial distribution and only if you
152
+ received the program in object code or executable form with such
153
+ an offer, in accord with Subsection b above.)
154
+
155
+ The source code for a work means the preferred form of the work for
156
+ making modifications to it. For an executable work, complete source
157
+ code means all the source code for all modules it contains, plus any
158
+ associated interface definition files, plus the scripts used to
159
+ control compilation and installation of the executable. However, as a
160
+ special exception, the source code distributed need not include
161
+ anything that is normally distributed (in either source or binary
162
+ form) with the major components (compiler, kernel, and so on) of the
163
+ operating system on which the executable runs, unless that component
164
+ itself accompanies the executable.
165
+
166
+ If distribution of executable or object code is made by offering
167
+ access to copy from a designated place, then offering equivalent
168
+ access to copy the source code from the same place counts as
169
+ distribution of the source code, even though third parties are not
170
+ compelled to copy the source along with the object code.
171
+
172
+ 4. You may not copy, modify, sublicense, or distribute the Program
173
+ except as expressly provided under this License. Any attempt
174
+ otherwise to copy, modify, sublicense or distribute the Program is
175
+ void, and will automatically terminate your rights under this License.
176
+ However, parties who have received copies, or rights, from you under
177
+ this License will not have their licenses terminated so long as such
178
+ parties remain in full compliance.
179
+
180
+ 5. You are not required to accept this License, since you have not
181
+ signed it. However, nothing else grants you permission to modify or
182
+ distribute the Program or its derivative works. These actions are
183
+ prohibited by law if you do not accept this License. Therefore, by
184
+ modifying or distributing the Program (or any work based on the
185
+ Program), you indicate your acceptance of this License to do so, and
186
+ all its terms and conditions for copying, distributing or modifying
187
+ the Program or works based on it.
188
+
189
+ 6. Each time you redistribute the Program (or any work based on the
190
+ Program), the recipient automatically receives a license from the
191
+ original licensor to copy, distribute or modify the Program subject to
192
+ these terms and conditions. You may not impose any further
193
+ restrictions on the recipients' exercise of the rights granted herein.
194
+ You are not responsible for enforcing compliance by third parties to
195
+ this License.
196
+
197
+ 7. If, as a consequence of a court judgment or allegation of patent
198
+ infringement or for any other reason (not limited to patent issues),
199
+ conditions are imposed on you (whether by court order, agreement or
200
+ otherwise) that contradict the conditions of this License, they do not
201
+ excuse you from the conditions of this License. If you cannot
202
+ distribute so as to satisfy simultaneously your obligations under this
203
+ License and any other pertinent obligations, then as a consequence you
204
+ may not distribute the Program at all. For example, if a patent
205
+ license would not permit royalty-free redistribution of the Program by
206
+ all those who receive copies directly or indirectly through you, then
207
+ the only way you could satisfy both it and this License would be to
208
+ refrain entirely from distribution of the Program.
209
+
210
+ If any portion of this section is held invalid or unenforceable under
211
+ any particular circumstance, the balance of the section is intended to
212
+ apply and the section as a whole is intended to apply in other
213
+ circumstances.
214
+
215
+ It is not the purpose of this section to induce you to infringe any
216
+ patents or other property right claims or to contest validity of any
217
+ such claims; this section has the sole purpose of protecting the
218
+ integrity of the free software distribution system, which is
219
+ implemented by public license practices. Many people have made
220
+ generous contributions to the wide range of software distributed
221
+ through that system in reliance on consistent application of that
222
+ system; it is up to the author/donor to decide if he or she is willing
223
+ to distribute software through any other system and a licensee cannot
224
+ impose that choice.
225
+
226
+ This section is intended to make thoroughly clear what is believed to
227
+ be a consequence of the rest of this License.
228
+
229
+ 8. If the distribution and/or use of the Program is restricted in
230
+ certain countries either by patents or by copyrighted interfaces, the
231
+ original copyright holder who places the Program under this License
232
+ may add an explicit geographical distribution limitation excluding
233
+ those countries, so that distribution is permitted only in or among
234
+ countries not thus excluded. In such case, this License incorporates
235
+ the limitation as if written in the body of this License.
236
+
237
+ 9. The Free Software Foundation may publish revised and/or new versions
238
+ of the General Public License from time to time. Such new versions will
239
+ be similar in spirit to the present version, but may differ in detail to
240
+ address new problems or concerns.
241
+
242
+ Each version is given a distinguishing version number. If the Program
243
+ specifies a version number of this License which applies to it and "any
244
+ later version", you have the option of following the terms and conditions
245
+ either of that version or of any later version published by the Free
246
+ Software Foundation. If the Program does not specify a version number of
247
+ this License, you may choose any version ever published by the Free Software
248
+ Foundation.
249
+
250
+ 10. If you wish to incorporate parts of the Program into other free
251
+ programs whose distribution conditions are different, write to the author
252
+ to ask for permission. For software which is copyrighted by the Free
253
+ Software Foundation, write to the Free Software Foundation; we sometimes
254
+ make exceptions for this. Our decision will be guided by the two goals
255
+ of preserving the free status of all derivatives of our free software and
256
+ of promoting the sharing and reuse of software generally.
257
+
258
+ NO WARRANTY
259
+
260
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
261
+ FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
262
+ OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
263
+ PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
264
+ OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
265
+ MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
266
+ TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
267
+ PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
268
+ REPAIR OR CORRECTION.
269
+
270
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
271
+ WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
272
+ REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
273
+ INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
274
+ OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
275
+ TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
276
+ YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
277
+ PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
278
+ POSSIBILITY OF SUCH DAMAGES.
279
+
280
+ END OF TERMS AND CONDITIONS
281
+
282
+ How to Apply These Terms to Your New Programs
283
+
284
+ If you develop a new program, and you want it to be of the greatest
285
+ possible use to the public, the best way to achieve this is to make it
286
+ free software which everyone can redistribute and change under these terms.
287
+
288
+ To do so, attach the following notices to the program. It is safest
289
+ to attach them to the start of each source file to most effectively
290
+ convey the exclusion of warranty; and each file should have at least
291
+ the "copyright" line and a pointer to where the full notice is found.
292
+
293
+ <one line to give the program's name and a brief idea of what it does.>
294
+ Copyright (C) <year> <name of author>
295
+
296
+ This program is free software; you can redistribute it and/or modify
297
+ it under the terms of the GNU General Public License as published by
298
+ the Free Software Foundation; either version 2 of the License, or
299
+ (at your option) any later version.
300
+
301
+ This program is distributed in the hope that it will be useful,
302
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
303
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
304
+ GNU General Public License for more details.
305
+
306
+ You should have received a copy of the GNU General Public License along
307
+ with this program; if not, write to the Free Software Foundation, Inc.,
308
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
309
+
310
+ Also add information on how to contact you by electronic and paper mail.
311
+
312
+ If the program is interactive, make it output a short notice like this
313
+ when it starts in an interactive mode:
314
+
315
+ Gnomovision version 69, Copyright (C) year name of author
316
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
317
+ This is free software, and you are welcome to redistribute it
318
+ under certain conditions; type `show c' for details.
319
+
320
+ The hypothetical commands `show w' and `show c' should show the appropriate
321
+ parts of the General Public License. Of course, the commands you use may
322
+ be called something other than `show w' and `show c'; they could even be
323
+ mouse-clicks or menu items--whatever suits your program.
324
+
325
+ You should also get your employer (if you work as a programmer) or your
326
+ school, if any, to sign a "copyright disclaimer" for the program, if
327
+ necessary. Here is a sample; alter the names:
328
+
329
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
330
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
331
+
332
+ <signature of Ty Coon>, 1 April 1989
333
+ Ty Coon, President of Vice
334
+
335
+ This General Public License does not permit incorporating your program into
336
+ proprietary programs. If your program is a subroutine library, you may
337
+ consider it more useful to permit linking proprietary applications with the
338
+ library. If this is what you want to do, use the GNU Lesser General
339
+ Public License instead of this License.
README.md CHANGED
@@ -1,13 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
- title: AIRA
3
- emoji: 👁
4
- colorFrom: blue
5
- colorTo: indigo
6
- sdk: streamlit
7
- sdk_version: 1.21.0
8
- app_file: app.py
9
- pinned: false
10
- license: gpl-2.0
 
 
 
 
 
 
 
 
 
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <img src="docs/images/aira-banner.png" alt="AIRA banner" style="display: block; margin: 0 auto; width:600px"/>
2
+
3
+ [![License: GPL v2](https://img.shields.io/badge/License-GPL_v2-blue.svg)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)
4
+
5
+ ---
6
+
7
+ AIRA (Ambisonics Impulse Response Analyzer) is a novel software for visualizing impulse responses measured from Ambisonics microphones. Its innovative and interactive ui allows you to examine the reflections at a point with a hedgehog-type graph. It is also possible to export the graphs and print them on a floor plan of the analyzed room.
8
+
9
+ Here are some previews:
10
+
11
+
12
+ <img src="docs/images/aira-gui.png" alt="AIRA gui" style="display: block; margin: 0 auto; width:800px"/>
13
+
14
+
15
+
16
  ---
17
+ ## 🆕 **Demo running**
18
+ 1. Download the repository
19
+ ```bash
20
+ git clone https://github.com/nahue-passano/AIRA.git
21
+ cd AIRA
22
+ ```
23
+
24
+ 2. Create and initialize [poetry](https://python-poetry.org/) environment
25
+ ```bash
26
+ poetry install
27
+ poetry shell
28
+ ```
29
+
30
+ 3. Run the GUI file
31
+ ```bash
32
+ python3 aira/gui.py
33
+ ```
34
+
35
+ > **Usage note:** In case you do not have your own measurements, import test measurements from `test/mock_data/regio_theater`
36
+
37
  ---
38
 
39
+ ## 🌱 **Getting started (develop)**
40
+
41
+ 1. Download the repository
42
+ ```bash
43
+ git clone https://github.com/nahue-passano/AIRA.git
44
+ cd AIRA
45
+ ```
46
+
47
+ 2. Create and initialize [poetry](https://python-poetry.org/) environment
48
+ ```bash
49
+ poetry install
50
+ poetry shell
51
+ ```
52
+
53
+ > **Note**: If the environment already exists, run `poetry update` for possible changes in `pyproject.toml`.
54
+
55
+ 3. Install the pre-commit hooks for code formating and linting with `black` and `pylint`.
56
+ ```bash
57
+ pre-commit install
58
+ ```
59
+
60
+ > **Note**: If the changes to be commited are reformated, `black` will cancel the commit. You must add again the changes with `git add` and commit again
61
+
62
+ ---
aira/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ "Ambisonics Impulse Response Analyzer module"
aira/__pycache__/__init__.cpython-38.pyc ADDED
Binary file (196 Bytes). View file
 
aira/__pycache__/core.cpython-38.pyc ADDED
Binary file (4.28 kB). View file
 
aira/core.py ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Core processing for AIRA module."""
2
+ import numpy as np
3
+ from dataclasses import dataclass
4
+ from plotly import graph_objects as go
5
+
6
+ from aira.engine.input import InputProcessorChain, InputMode
7
+ from aira.engine.intensity import (
8
+ convert_bformat_to_intensity,
9
+ analysis_crop_2d,
10
+ integrate_intensity_directions,
11
+ intensity_thresholding,
12
+ )
13
+ from aira.engine.pressure import w_channel_preprocess
14
+ from aira.engine.plot import hedgehog, w_channel, setup_plotly_layout, get_xy_projection
15
+ from aira.engine.reflections import detect_reflections
16
+ from aira.utils import read_signals_dict, cartesian_to_spherical
17
+
18
+
19
+ INTEGRATION_TIME = 0.005
20
+ INTENSITY_THRESHOLD = -60
21
+ ANALYSIS_LENGTH = 1
22
+
23
+
24
+ @dataclass
25
+ class AmbisonicsImpulseResponseAnalyzer:
26
+ """Main class for analyzing Ambisonics impulse responses"""
27
+
28
+ integration_time: float = INTEGRATION_TIME
29
+ intensity_threshold: float = INTENSITY_THRESHOLD
30
+ analysis_length: float = ANALYSIS_LENGTH
31
+ bformat_frequency_correction: bool = True
32
+ input_builder = InputProcessorChain()
33
+
34
+ def analyze(
35
+ self,
36
+ input_dict: dict,
37
+ integration_time: float = INTEGRATION_TIME,
38
+ intensity_threshold: float = INTENSITY_THRESHOLD,
39
+ analysis_length: float = ANALYSIS_LENGTH,
40
+ show: bool = False,
41
+ ) -> go.Figure:
42
+ """Analyzes a set of measurements in Ambisonics format and plots a hedgehog
43
+ with the estimated reflections direction.
44
+
45
+ Parameters
46
+ ----------
47
+ input_dict : dict
48
+ Dictionary with all the data needed to analyze a set of measurements
49
+ (paths of the measurements, input mode, channels per file, etc.)
50
+ integration_time : float, optional
51
+ Time frame where intensity vectors are integrated by the mean of them,
52
+ by default INTEGRATION_TIME
53
+ intensity_threshold : float, optional
54
+ Bottom limit for intensity values in dB, by default INTENSITY_THRESHOLD
55
+ analysis_length : float, optional
56
+ Total time of analysis from intensity max peak, by default ANALYSIS_LENGTH
57
+ show : bool, optional
58
+ Shows plotly figure in browser, by default False
59
+
60
+ Returns
61
+ -------
62
+ go.Figure
63
+ Plotly figure with hedgehog and w-channel plot
64
+ """
65
+
66
+ signals_dict = read_signals_dict(input_dict)
67
+ sample_rate = signals_dict["sample_rate"]
68
+
69
+ bformat_signals = self.input_builder.process(input_dict)
70
+
71
+ intensity_directions = convert_bformat_to_intensity(bformat_signals)
72
+
73
+ intensity_directions_cropped = analysis_crop_2d(
74
+ analysis_length, sample_rate, intensity_directions
75
+ )
76
+
77
+ intensity_windowed, time = integrate_intensity_directions(
78
+ intensity_directions_cropped, integration_time, sample_rate
79
+ )
80
+
81
+ intensity, azimuth, elevation = cartesian_to_spherical(intensity_windowed)
82
+
83
+ (
84
+ intensity_peaks,
85
+ azimuth_peaks,
86
+ elevation_peaks,
87
+ reflections_idx,
88
+ ) = detect_reflections(intensity, azimuth, elevation)
89
+
90
+ (
91
+ reflex_to_direct,
92
+ azimuth_peaks,
93
+ elevation_peaks,
94
+ reflections_idx,
95
+ ) = intensity_thresholding(
96
+ intensity_threshold,
97
+ intensity_peaks,
98
+ azimuth_peaks,
99
+ elevation_peaks,
100
+ reflections_idx,
101
+ )
102
+
103
+ time = time[reflections_idx]
104
+
105
+ fig = setup_plotly_layout()
106
+
107
+ hedgehog(fig, time, reflex_to_direct, azimuth_peaks, elevation_peaks)
108
+
109
+ w_channel_signal = w_channel_preprocess(
110
+ bformat_signals[0, :],
111
+ int(integration_time * sample_rate),
112
+ analysis_length,
113
+ sample_rate,
114
+ )
115
+
116
+ w_channel(
117
+ fig,
118
+ np.arange(0, analysis_length, 1 / sample_rate) * 1000,
119
+ w_channel_signal,
120
+ intensity_threshold,
121
+ time,
122
+ )
123
+
124
+ if show:
125
+ fig.show()
126
+ return fig
127
+
128
+ def export_xy_projection(self, fig: go.Figure, img_name: str):
129
+ new_fig = get_xy_projection(fig)
130
+ new_fig.write_image(img_name, format="png")
131
+
132
+
133
+ if __name__ == "__main__":
134
+ # Regio theater
135
+ data = {
136
+ "front_left_up": "test/mock_data/regio_theater/soundfield_flu.wav",
137
+ "front_right_down": "test/mock_data/regio_theater/soundfield_frd.wav",
138
+ "back_right_up": "test/mock_data/regio_theater/soundfield_bru.wav",
139
+ "back_left_down": "test/mock_data/regio_theater/soundfield_bld.wav",
140
+ "inverse_filter": "test/mock_data/regio_theater/soundfield_inverse_filter.wav",
141
+ "input_mode": InputMode.LSS,
142
+ "channels_per_file": 1,
143
+ "frequency_correction": True,
144
+ }
145
+
146
+ # York auditorium
147
+ # data = {
148
+ # "stacked_signals": "test/mock_data/york_auditorium/s2r2.wav",
149
+ # "input_mode": InputMode.BFORMAT,
150
+ # "channels_per_file": 4,
151
+ # "frequency_correction": False,
152
+ # }
153
+
154
+ analyzer = AmbisonicsImpulseResponseAnalyzer()
155
+ fig = analyzer.analyze(data, show=True)
156
+ analyzer.export_xy_projection(fig, "projection.png")
aira/engine/__init__.py ADDED
File without changes
aira/engine/__pycache__/__init__.cpython-38.pyc ADDED
Binary file (148 Bytes). View file
 
aira/engine/__pycache__/filtering.cpython-38.pyc ADDED
Binary file (4.21 kB). View file
 
aira/engine/__pycache__/input.cpython-38.pyc ADDED
Binary file (4.87 kB). View file
 
aira/engine/__pycache__/intensity.cpython-38.pyc ADDED
Binary file (4.08 kB). View file
 
aira/engine/__pycache__/plot.cpython-38.pyc ADDED
Binary file (5.18 kB). View file
 
aira/engine/__pycache__/pressure.cpython-38.pyc ADDED
Binary file (1.45 kB). View file
 
aira/engine/__pycache__/reflections.cpython-38.pyc ADDED
Binary file (4.51 kB). View file
 
aira/engine/filtering.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Functionality for filtering signals."""
2
+
3
+ import numpy as np
4
+ from scipy.signal import bilinear, firwin, kaiserord, lfilter
5
+
6
+ MIC2CENTER = 3
7
+ SOUND_SPEED = 340
8
+ FILTER_TRANSITION_WIDTH_HZ = 250.0
9
+ FILTER_RIPPLE_DB = 60.0
10
+
11
+
12
+ class NonCoincidentMicsCorrection:
13
+ """Class for correct frequency response in Ambisonics B-format representation."""
14
+
15
+ def __init__(
16
+ self,
17
+ sample_rate: int,
18
+ mic2center: float = MIC2CENTER,
19
+ sound_speed: float = SOUND_SPEED,
20
+ ) -> None:
21
+ self.sample_rate = sample_rate
22
+ self.mic2center = mic2center / 100
23
+ self.sound_speed = sound_speed
24
+ self.delay2center = self.mic2center / self.sound_speed
25
+
26
+ def _filter(
27
+ # pylint: disable=invalid-name
28
+ self,
29
+ b: np.ndarray,
30
+ a: np.ndarray,
31
+ array: np.ndarray,
32
+ ) -> np.ndarray:
33
+ """Applies filter to array given numerator "b" and denominator "a" from
34
+ analog filter frequency response
35
+
36
+ Parameters
37
+ ----------
38
+ b : np.ndarray
39
+ Array containing numerator's coefficients
40
+ a : np.ndarray
41
+ Array containing denominator's coefficients
42
+ array : np.ndarray
43
+ Array to be filtered
44
+
45
+ Returns
46
+ -------
47
+ np.ndarray
48
+ Filtered array
49
+ """
50
+
51
+ # Analog to digital filter conversion
52
+ zeros, poles = bilinear(b, a, self.sample_rate)
53
+
54
+ # Filtering
55
+ array_filtered = lfilter(zeros, poles, array)
56
+
57
+ return array_filtered
58
+
59
+ def correct_axis(self, axis_signal: np.ndarray) -> np.ndarray:
60
+ """Applies correction filter to axis array signal
61
+
62
+ Parameters
63
+ ----------
64
+ axis_signal : np.ndarray
65
+ Array containing axis signal
66
+
67
+ Returns
68
+ -------
69
+ np.ndarray
70
+ Axis array signal corrected
71
+ """
72
+ # Filter equations
73
+ # pylint: disable=invalid-name
74
+ b = np.sqrt(6) * np.array(
75
+ [1, 1j * (1 / 3) * self.mic2center, -(1 / 3) * self.delay2center**2]
76
+ )
77
+ # pylint: disable=invalid-name
78
+ a = np.array([1, 1j * (1 / 3) * self.delay2center])
79
+
80
+ axis_corrected = self._filter(b, a, axis_signal)
81
+
82
+ return axis_corrected
83
+
84
+ def correct_omni(self, omni_signal: np.ndarray) -> np.ndarray:
85
+ """Applies correction filter to omnidirectional array signal
86
+
87
+ Parameters
88
+ ----------
89
+ omni_signal : np.ndarray
90
+ Array containing omnidirectional signal
91
+
92
+ Returns
93
+ -------
94
+ np.ndarray
95
+ Omnidirectional array signal corrected
96
+ """
97
+ # Filter equations
98
+ # pylint: disable=invalid-name
99
+ b = np.array([1, 1j * self.delay2center, -(1 / 3) * self.delay2center**2])
100
+ # pylint: disable=invalid-name
101
+ a = np.array([1, 1j * (1 / 3) * self.delay2center])
102
+
103
+ omni_corrected = self._filter(b, a, omni_signal)
104
+
105
+ return omni_corrected
106
+
107
+
108
+ def apply_low_pass_filter(
109
+ signal: np.ndarray, cutoff_frequency: int, sample_rate: int
110
+ ) -> np.ndarray:
111
+ """Filter a signal at the given cutoff with an optimized number of taps
112
+ (order of the filter).
113
+
114
+ Args:
115
+ signal (np.ndarray): signal to filter.
116
+ cutoff_frequency (int): cutoff frequency.
117
+ sample_rate (int): sample rate of the signal.
118
+
119
+ Returns:
120
+ np.ndarray: filtered signal.
121
+ """
122
+ nyquist_rate = sample_rate / 2.0
123
+
124
+ # Compute FIR filter parameters and apply to signal.
125
+ transition_width_normalized = FILTER_TRANSITION_WIDTH_HZ / nyquist_rate
126
+ filter_length, filter_beta = kaiserord(
127
+ FILTER_RIPPLE_DB, transition_width_normalized
128
+ )
129
+ filter_coefficients = firwin(
130
+ filter_length, cutoff_frequency / nyquist_rate, window=("kaiser", filter_beta)
131
+ )
132
+
133
+ return lfilter(filter_coefficients, 1.0, signal)
134
+
135
+
136
+ def moving_average_filter(array: np.ndarray, window_size: int) -> np.ndarray:
137
+ """_summary_
138
+
139
+ Parameters
140
+ ----------
141
+ array : np.ndarray
142
+ _description_
143
+ window_size : int
144
+ _description_
145
+
146
+ Returns
147
+ -------
148
+ np.ndarray
149
+ _description_
150
+ """
151
+ window = np.ones(window_size) / window_size
152
+ return np.convolve(array, window, mode="valid")
aira/engine/input.py ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Input preprocessing module."""
2
+ from abc import ABC, abstractmethod
3
+ from enum import Enum
4
+
5
+ import numpy as np
6
+ from scipy.signal import fftconvolve
7
+
8
+ from aira.engine.filtering import NonCoincidentMicsCorrection
9
+ from aira.utils import convert_ambisonics_a_to_b
10
+
11
+
12
+ # pylint: disable=too-few-public-methods
13
+ class InputMode(Enum):
14
+ """Enum class for accessing the existing `InputMode`s"""
15
+
16
+ LSS = "lss"
17
+ AFORMAT = "aformat"
18
+ BFORMAT = "bformat"
19
+
20
+
21
+ # pylint: disable=too-few-public-methods
22
+ class InputProcessor(ABC):
23
+ """Base interface for inputs processors"""
24
+
25
+ @abstractmethod
26
+ def process(self, input_dict: dict) -> dict:
27
+ """Abstract method to be overwritten by concrete implementations of
28
+ input processing."""
29
+
30
+
31
+ # pylint: disable=too-few-public-methods
32
+ class LSSInputProcessor(InputProcessor):
33
+ """Processing when input data is in LSS mode"""
34
+
35
+ def process(self, input_dict: dict) -> dict:
36
+ """Gets impulse response arrays from Long Sine Sweep (LSS) measurements. The new
37
+ signals are in A-Format.
38
+
39
+ Parameters
40
+ ----------
41
+ input_dict : dict
42
+ Dictionary with LSS measurement arrays
43
+
44
+ Returns
45
+ -------
46
+ dict
47
+ input_dict overwritten with A-Format signals
48
+ """
49
+ if input_dict["input_mode"] != InputMode.LSS:
50
+ return input_dict
51
+
52
+ input_dict["stacked_signals"] = np.apply_along_axis(
53
+ lambda array: fftconvolve(array, input_dict["inverse_filter"], mode="full"),
54
+ axis=1,
55
+ arr=input_dict["stacked_signals"],
56
+ )
57
+ input_dict["input_mode"] = InputMode.AFORMAT
58
+
59
+ return input_dict
60
+
61
+
62
+ # pylint: disable=too-few-public-methods
63
+ class AFormatProcessor(InputProcessor):
64
+ """Processing when input data is in mode AFORMAT"""
65
+
66
+ def process(self, input_dict: dict) -> dict:
67
+ """Gets B-format arrays from A-format arrays. For more details see
68
+ aira.utils.formatter.convert_ambisonics_a_to_b function.
69
+
70
+ Parameters
71
+ ----------
72
+ input_dict : dict
73
+ Dictionary with A-format arrays
74
+
75
+ Returns
76
+ -------
77
+ dict
78
+ input_dict overwritten with B-format signals
79
+ """
80
+ if input_dict["input_mode"] != InputMode.AFORMAT:
81
+ return input_dict
82
+ input_dict["stacked_signals"] = convert_ambisonics_a_to_b(
83
+ input_dict["stacked_signals"][0, :],
84
+ input_dict["stacked_signals"][1, :],
85
+ input_dict["stacked_signals"][2, :],
86
+ input_dict["stacked_signals"][3, :],
87
+ )
88
+ input_dict["input_mode"] = InputMode.BFORMAT
89
+ return input_dict
90
+
91
+
92
+ # pylint: disable=too-few-public-methods
93
+ class BFormatProcessor(InputProcessor):
94
+ """Processin when input data is in BFORMAT mode."""
95
+
96
+ def process(self, input_dict: dict) -> dict:
97
+ """Corrects B-format arrays frequency response for non-coincident microphones.
98
+
99
+ Parameters
100
+ ----------
101
+ input_dict : dict
102
+ Dictionary with B-format arrays.
103
+
104
+ Returns
105
+ -------
106
+ dict
107
+ input_dict overwritten with B-format frequency corrected arrays.
108
+ """
109
+ if input_dict["input_mode"] != InputMode.BFORMAT and not bool(
110
+ input_dict["frequency_correction"]
111
+ ):
112
+ return input_dict
113
+
114
+ frequency_corrector = NonCoincidentMicsCorrection(input_dict["sample_rate"])
115
+
116
+ input_dict["stacked_signals"][0, :] = frequency_corrector.correct_omni(
117
+ input_dict["stacked_signals"][0, :]
118
+ )
119
+ input_dict["stacked_signals"][1:, :] = frequency_corrector.correct_axis(
120
+ input_dict["stacked_signals"][1:, :]
121
+ )
122
+ input_dict["input_mode"] = InputMode.BFORMAT
123
+ return input_dict
124
+
125
+
126
+ # pylint: disable=too-few-public-methods
127
+ class InputProcessorChain:
128
+ """Chain of input processors"""
129
+
130
+ def __init__(self):
131
+ self.processors = [LSSInputProcessor(), AFormatProcessor(), BFormatProcessor()]
132
+
133
+ def process(self, input_dict: dict) -> np.ndarray:
134
+ """Applies the chain of processors for the input_mode setted.
135
+
136
+ Parameters
137
+ ----------
138
+ input_dict : dict
139
+ Contains arrays and input mode data
140
+
141
+ Returns
142
+ -------
143
+ np.ndarray
144
+ Arrays processed stacked in single numpy.ndarray object
145
+ """
146
+ for process_i in self.processors:
147
+ input_dict = process_i.process(input_dict)
148
+
149
+ return input_dict["stacked_signals"]
aira/engine/intensity.py ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Functionality for intensity computation and related signal processing."""
2
+
3
+ from typing import Tuple
4
+
5
+ import numpy as np
6
+
7
+ from aira.engine.filtering import apply_low_pass_filter
8
+
9
+ FILTER_CUTOFF = 5000
10
+ OVERLAP_RATIO = 0.5
11
+
12
+
13
+ def analysis_crop_2d(
14
+ analysis_length: float,
15
+ sample_rate: int,
16
+ intensity_directions: np.ndarray,
17
+ ):
18
+ """_summary_
19
+
20
+ Parameters
21
+ ----------
22
+ analysis_length : float
23
+ _description_
24
+ sample_rate : int
25
+ _description_
26
+ intensity_directions : np.ndarray
27
+ _description_
28
+
29
+ Returns
30
+ -------
31
+ _type_
32
+ _description_
33
+ """
34
+ # Get analysis length max index
35
+ analysis_length_idx = int(analysis_length * sample_rate)
36
+
37
+ # Slice from intensity max to analysis length from intensity max
38
+ earliest_peak_index = np.argmax(np.abs(intensity_directions), axis=1).min()
39
+ intensity_directions_cropped = intensity_directions[
40
+ :, earliest_peak_index : earliest_peak_index + analysis_length_idx
41
+ ]
42
+
43
+ return intensity_directions_cropped
44
+
45
+
46
+ def intensity_thresholding(
47
+ threshold: float,
48
+ intensity: np.ndarray,
49
+ azimuth: np.ndarray,
50
+ elevation: np.ndarray,
51
+ reflections: np.ndarray,
52
+ ) -> Tuple[np.ndarray]:
53
+ """_summary_
54
+
55
+ Parameters
56
+ ----------
57
+ threshold : _type_
58
+ _description_
59
+ """
60
+ reflex_to_direct = intensity_to_dB(intensity) - intensity_to_dB(intensity[0])
61
+ thresholding_mask = reflex_to_direct > threshold
62
+ return (
63
+ reflex_to_direct[thresholding_mask],
64
+ azimuth[thresholding_mask],
65
+ elevation[thresholding_mask],
66
+ reflections[thresholding_mask],
67
+ )
68
+
69
+
70
+ def intensity_to_dB(intensity_array: np.ndarray) -> np.ndarray:
71
+ """Converts intensity to dB scale using 1e-12 as intensity reference
72
+
73
+ Parameters
74
+ ----------
75
+ intensity_array : np.ndarray
76
+ Intensity array
77
+
78
+ Returns
79
+ -------
80
+ np.ndarray
81
+ Intensity array in dB scale
82
+ """
83
+ return 10 * np.log10(intensity_array / 1e-12)
84
+
85
+
86
+ def min_max_normalization(array: np.ndarray) -> np.ndarray:
87
+ """Returns the input array normalized by its minimum and maximum value.
88
+
89
+ Parameters
90
+ ----------
91
+ array : np.ndarray
92
+ Array to be normalized
93
+
94
+ Returns
95
+ -------
96
+ np.ndarray
97
+ Array normalized
98
+ """
99
+ return (array - array.min() * 1.1) / (array.max() - array.min() * 1.1)
100
+
101
+
102
+ def integrate_intensity_directions(
103
+ intensity_directions: np.ndarray,
104
+ duration_secs: float,
105
+ sample_rate: int,
106
+ ) -> np.ndarray:
107
+ """Integrate the intensity signal with Hamming windows of length `duration_secs`.
108
+
109
+ Args:
110
+ intensity_directions (np.ndarray): X, Y and Z intensity signals.
111
+ duration_secs (float): the length of the window to apply, in seconds.
112
+ sample_rate (int): sampling rate of the signal.
113
+
114
+ Returns:
115
+ np.ndarray: the integrated signal, of shape (3, ...)
116
+ """
117
+ if intensity_directions.shape[0] == 4:
118
+ intensity_directions = intensity_directions[1:, :]
119
+ elif (intensity_directions.shape[0] < 3) or (intensity_directions.shape[0] > 4):
120
+ raise ValueError(f"Unexpected input shape {intensity_directions.shape}")
121
+
122
+ # Convert integration time to samples
123
+ duration_samples = np.round(duration_secs * sample_rate).astype(np.int64)
124
+
125
+ # Padding and windowing
126
+ hop_size = int(duration_samples * (1 - OVERLAP_RATIO))
127
+ intensity_directions = np.concatenate(
128
+ [
129
+ intensity_directions,
130
+ np.zeros((3, intensity_directions.shape[1] % hop_size)),
131
+ ],
132
+ axis=1,
133
+ )
134
+ output_shape = (
135
+ 3,
136
+ int(intensity_directions.shape[1] / duration_samples / OVERLAP_RATIO) - 1,
137
+ )
138
+ intensity_windowed = np.zeros(output_shape)
139
+ time = np.zeros(output_shape[1])
140
+ window = np.hamming(duration_samples)
141
+
142
+ for i in range(0, output_shape[1]):
143
+ intensity_segment = intensity_directions[
144
+ :, i * hop_size : i * hop_size + duration_samples
145
+ ]
146
+ intensity_windowed[:, i] = np.mean(intensity_segment * window, axis=1)
147
+ time[i] = i * hop_size / sample_rate
148
+
149
+ # Add direct sound first with no windowing
150
+ intensity_windowed = np.insert(
151
+ intensity_windowed, 0, intensity_directions[:, 0], axis=1
152
+ )
153
+
154
+ return intensity_windowed, time
155
+
156
+
157
+ def convert_bformat_to_intensity(signal: np.ndarray) -> Tuple[np.ndarray]:
158
+ """Integrate and compute intensities for a B-format Ambisonics recording.
159
+
160
+ Args:
161
+ signal (np.ndarray): input B-format Ambisonics signal. Shape: (4, N).
162
+
163
+ Returns:
164
+ Tuple[np.ndarray]: integrated intensity, azimuth and elevation.
165
+ """
166
+ # signal_filtered = apply_low_pass_filter(signal, cutoff_frequency, sample_rate)
167
+ signal_filtered = signal
168
+
169
+ # Calculate intensity from directions
170
+ intensity_directions = (
171
+ signal_filtered[0, :] * signal_filtered[1:, :]
172
+ ) # Intensity = pressure (W channel) * pressure gradient (XYZ channels)
173
+ return intensity_directions
aira/engine/plot.py ADDED
@@ -0,0 +1,266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Plotting functions."""
2
+ from typing import Tuple, Dict
3
+ import numpy as np
4
+ import pandas as pd
5
+ import plotly.graph_objects as go
6
+ from plotly.subplots import make_subplots
7
+
8
+ from aira.utils.formatter import spherical_to_cartesian
9
+ from aira.engine.intensity import min_max_normalization
10
+
11
+
12
+ def hedgehog(
13
+ fig: go.Figure,
14
+ time_peaks: np.ndarray,
15
+ reflex_to_direct: np.ndarray,
16
+ azimuth_peaks: np.ndarray,
17
+ elevation_peaks: np.ndarray,
18
+ ) -> go.Figure:
19
+ """Create a hedgehog plot."""
20
+ time_peaks *= 1000 # seconds to miliseconds
21
+ normalized_intensities = min_max_normalization(reflex_to_direct)
22
+ # pylint: disable=invalid-name
23
+ x, y, z = spherical_to_cartesian(
24
+ normalized_intensities, azimuth_peaks, elevation_peaks
25
+ )
26
+
27
+ fig.add_trace(
28
+ go.Scatter3d(
29
+ x=zero_inserter(x),
30
+ y=zero_inserter(y),
31
+ z=zero_inserter(z),
32
+ marker={
33
+ "color": zero_inserter(normalized_intensities),
34
+ "colorscale": "portland",
35
+ "colorbar": {
36
+ "thickness": 20,
37
+ "tickvals": [0.99],
38
+ "ticktext": ["Direct <br>sound "],
39
+ "ticklabelposition": "inside",
40
+ "ticksuffix": " ",
41
+ "ticklabeloverflow": "allow",
42
+ "title": {"text": "<b>Time</b>"},
43
+ },
44
+ "size": 3,
45
+ },
46
+ line={
47
+ "width": 8,
48
+ "color": zero_inserter(normalized_intensities),
49
+ "colorscale": "portland",
50
+ },
51
+ customdata=np.stack(
52
+ (
53
+ zero_inserter(reflex_to_direct),
54
+ zero_inserter(time_peaks),
55
+ zero_inserter(azimuth_peaks),
56
+ zero_inserter(elevation_peaks),
57
+ ),
58
+ axis=-1,
59
+ ),
60
+ hovertemplate="<b>Reflection-to-direct [dB]:</b> %{customdata[0]:.2f} dB <br>"
61
+ + "<b>Time [ms]: </b>%{customdata[1]:.2f} ms <br>"
62
+ + "<b>Azimuth [°]: </b>%{customdata[2]:.2f}° <br>"
63
+ + "<b>Elevation [°]: </b>%{customdata[3]:.2f}° <extra></extra>",
64
+ showlegend=False,
65
+ ),
66
+ row=1,
67
+ col=1,
68
+ )
69
+
70
+ fig.update_layout(
71
+ scene={
72
+ "aspectmode": "cube",
73
+ "xaxis": {
74
+ "zerolinecolor": "white",
75
+ "showbackground": False,
76
+ "showticklabels": False,
77
+ },
78
+ "xaxis_title": " ◀️ Front - Rear ▶",
79
+ "yaxis": {
80
+ "zerolinecolor": "white",
81
+ "showbackground": False,
82
+ "showticklabels": False,
83
+ },
84
+ "yaxis_title": " ◀️ Right - Left ▶",
85
+ "zaxis": {
86
+ "zerolinecolor": "white",
87
+ "showbackground": False,
88
+ "showticklabels": False,
89
+ },
90
+ "zaxis_title": " ◀️ Up - Down ▶",
91
+ },
92
+ )
93
+ return fig
94
+
95
+
96
+ def w_channel(
97
+ fig: go.Figure,
98
+ time: np.ndarray,
99
+ w_channel: np.ndarray,
100
+ ylim: float,
101
+ time_reflections: np.ndarray,
102
+ ) -> go.Figure:
103
+ """_summary_
104
+
105
+ Parameters
106
+ ----------
107
+ fig : go.Figure
108
+ _description_
109
+ """
110
+ fig.add_trace(
111
+ go.Scatter(
112
+ x=time,
113
+ y=w_channel,
114
+ customdata=time,
115
+ hovertemplate="<b>Time [ms]:</b> %{customdata:.2f} ms <extra></extra>",
116
+ showlegend=False,
117
+ )
118
+ )
119
+ fig.add_trace(
120
+ go.Scatter(
121
+ mode="markers",
122
+ marker={"symbol": "star-diamond", "size": 10, "color": "rgb(72,116,212)"},
123
+ x=time_reflections,
124
+ y=np.ones_like(time_reflections) * 0.95,
125
+ customdata=time_reflections,
126
+ hovertemplate="<b>Time [ms]:</b> %{customdata:.2f} ms <extra></extra>",
127
+ showlegend=False,
128
+ )
129
+ )
130
+ fig.update_layout(yaxis_range=[0, 1], xaxis_range=[0, max(time)])
131
+ fig.update_xaxes(title_text="Time [ms]", row=2, col=1)
132
+ fig.update_yaxes(title_text="Relative amplitude", row=2, col=1)
133
+
134
+
135
+ def setup_plotly_layout() -> go.Figure:
136
+ """_summary_
137
+
138
+ Parameters
139
+ ----------
140
+ fig : go.Figure
141
+ _description_
142
+
143
+ Returns
144
+ -------
145
+ _type_
146
+ _description_
147
+ """
148
+ fig = make_subplots(
149
+ rows=2,
150
+ cols=1,
151
+ row_heights=[0.85, 0.15],
152
+ vertical_spacing=0.05,
153
+ specs=[[{"type": "scene"}], [{"type": "xy"}]],
154
+ subplot_titles=("<b>Hedgehog</b>", "<b>Omnidirectional channel</b>"),
155
+ )
156
+
157
+ camera, buttons = get_plotly_scenes()
158
+
159
+ fig.update_layout(
160
+ template="plotly_dark",
161
+ margin={"l": 0, "r": 100, "t": 30, "b": 0},
162
+ paper_bgcolor="rgb(49,52,56)",
163
+ plot_bgcolor="rgb(49,52,56)",
164
+ scene_camera=camera,
165
+ updatemenus=[{"buttons": buttons}],
166
+ showlegend=False,
167
+ )
168
+ return fig
169
+
170
+
171
+ def get_plotly_scenes() -> Tuple[Dict]:
172
+ """_summary_
173
+
174
+ Returns
175
+ -------
176
+ Tuple[Dict]
177
+ _description_
178
+ """
179
+ camera = {
180
+ "up": {"x": 0, "y": 0, "z": 1},
181
+ "center": {"x": 0, "y": 0, "z": 0},
182
+ "eye": {"x": 1.3, "y": 1.3, "z": 0.2},
183
+ }
184
+
185
+ button0 = {
186
+ "method": "relayout",
187
+ "args": [{"scene.camera.eye": {"x": 1.3, "y": 1.3, "z": 0.2}}],
188
+ "label": "3D perspective",
189
+ }
190
+
191
+ button1 = {
192
+ "method": "relayout",
193
+ "args": [
194
+ {
195
+ "scene.camera.eye": {"x": 0.0, "y": 0.0, "z": 2},
196
+ "scene.camera.up": {"x": 0.0, "y": 0.0, "z": 2},
197
+ }
198
+ ],
199
+ "label": "X-Y plane",
200
+ }
201
+
202
+ button2 = {
203
+ "method": "relayout",
204
+ "args": [{"scene.camera.eye": {"x": 0.0, "y": 2, "z": 0.0}}],
205
+ "label": "X-Z plane",
206
+ }
207
+
208
+ button3 = {
209
+ "method": "relayout",
210
+ "args": [{"scene.camera.eye": {"x": 2, "y": 0.0, "z": 0.0}}],
211
+ "label": "Y-Z plane",
212
+ }
213
+ buttons = [button0, button1, button2, button3]
214
+ return camera, buttons
215
+
216
+
217
+ def get_xy_projection(fig: go.Figure) -> go.Figure:
218
+ # Removing omnidireccional channel plot
219
+ traces = list(fig.data)
220
+ traces.pop(1)
221
+
222
+ # Creating new figure for xy projection
223
+ new_fig = make_subplots()
224
+ new_fig.add_trace(traces[0])
225
+
226
+ # Removing axes in Scatter plot
227
+ new_fig.update_layout(
228
+ scene={
229
+ "xaxis": {"visible": False},
230
+ "yaxis": {"visible": False},
231
+ "zaxis": {"visible": False},
232
+ }
233
+ )
234
+
235
+ # Removing colorbar
236
+ new_fig.update_traces(marker_showscale=False)
237
+
238
+ # Setting cenital camera and cube mode
239
+ new_fig.update_layout(
240
+ scene={"aspectmode": "cube"},
241
+ scene_camera={
242
+ "up": {"x": 0, "y": 1, "z": 0},
243
+ "center": {"x": 0, "y": 0, "z": 0},
244
+ "eye": {"x": 0, "y": 0, "z": 1.5},
245
+ },
246
+ paper_bgcolor="rgba(0,0,0,0)",
247
+ plot_bgcolor="rgba(0,0,0,0)",
248
+ )
249
+
250
+ return new_fig
251
+
252
+
253
+ def zero_inserter(array: np.ndarray) -> np.ndarray:
254
+ """_summary_
255
+
256
+ Parameters
257
+ ----------
258
+ array : np.ndarray
259
+ _description_
260
+
261
+ Returns
262
+ -------
263
+ np.ndarray
264
+ _description_
265
+ """
266
+ return np.insert(array, np.arange(len(array)), values=0)
aira/engine/pressure.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+
3
+ from aira.engine.filtering import moving_average_filter
4
+
5
+
6
+ def w_channel_preprocess(
7
+ w_channel: np.ndarray, window_size: int, analysis_length: float, sample_rate: float
8
+ ) -> np.ndarray:
9
+ """_summary_
10
+
11
+ Parameters
12
+ ----------
13
+ w_channel : np.ndarray
14
+ _description_
15
+ window_size : int
16
+ _description_
17
+ analysis_length : float
18
+ _description_
19
+ sample_rate : float
20
+ _description_
21
+
22
+ Returns
23
+ -------
24
+ np.ndarray
25
+ _description_
26
+ """
27
+ w_channel_cropped = np.abs(
28
+ analysis_crop_1d(w_channel, analysis_length, sample_rate)
29
+ )
30
+ w_channel_filtered = moving_average_filter(w_channel_cropped, int(window_size / 2))
31
+ w_channel_filtered /= np.max(w_channel_filtered)
32
+ return w_channel_filtered
33
+
34
+
35
+ def analysis_crop_1d(
36
+ array: np.ndarray,
37
+ analysis_length: float,
38
+ sample_rate: int,
39
+ ):
40
+ """_summary_
41
+
42
+ Parameters
43
+ ----------
44
+ analysis_length : float
45
+ _description_
46
+ sample_rate : int
47
+ _description_
48
+ array : np.ndarray
49
+ _description_
50
+
51
+ Returns
52
+ -------
53
+ _type_
54
+ _description_
55
+ """
56
+ # Get analysis length max index
57
+ analysis_length_idx = int(analysis_length * sample_rate)
58
+
59
+ # Slice from intensity max to analysis length from intensity max
60
+ earliest_peak_index = np.argmax(np.abs(array))
61
+ array_cropped = array[
62
+ earliest_peak_index : earliest_peak_index + analysis_length_idx
63
+ ]
64
+
65
+ return array_cropped
aira/engine/reflections.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Functionality for detecting reflections in a room impulse response (RIR)."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from enum import Enum
5
+ from typing import Tuple, Union
6
+
7
+ import numpy as np
8
+ from scipy.signal import find_peaks, find_peaks_cwt
9
+
10
+
11
+ # pylint: disable=too-few-public-methods
12
+ class ReflectionDetectionStrategy(ABC):
13
+ """Base interface for a reflection detection algorithm."""
14
+
15
+ @staticmethod
16
+ @abstractmethod
17
+ def get_indeces_of_reflections(intensity_magnitude: np.ndarray) -> np.ndarray:
18
+ """Abstract method to be overwritten by concrete implementations of
19
+ reflection detection."""
20
+
21
+
22
+ # pylint: disable=too-few-public-methods
23
+ class CorrelationReflectionDetectionStrategy(ReflectionDetectionStrategy):
24
+ """Algorithm for detecting reflections based on the correlation."""
25
+
26
+ @staticmethod
27
+ def get_indeces_of_reflections(intensity_magnitude: np.ndarray) -> np.ndarray:
28
+ raise NotImplementedError("Implemented method is Scipy's find_peaks")
29
+
30
+
31
+ # pylint: disable=too-few-public-methods
32
+ class ThresholdReflectionDetectionStrategy(ReflectionDetectionStrategy):
33
+ """Algorithm for detecting reflections based on a threshold."""
34
+
35
+ @staticmethod
36
+ def get_indeces_of_reflections(intensity_magnitude: np.ndarray) -> np.ndarray:
37
+ raise NotImplementedError("Implemented method is Scipy's find_peaks")
38
+
39
+
40
+ # pylint: disable=too-few-public-methods
41
+ class NeighborReflectionDetectionStrategy(ReflectionDetectionStrategy):
42
+ """Algorithm for detecting reflections based on the surrounding values
43
+ of local maxima."""
44
+
45
+ @staticmethod
46
+ def get_indeces_of_reflections(intensity_magnitude: np.ndarray) -> np.ndarray:
47
+ """Find local maxima in the intensity magnitude signal.
48
+
49
+ Args:
50
+ intensity_magnitude (np.ndarray): intensity magnitude signal.
51
+
52
+ Returns:
53
+ np.ndarray: an array with the indeces of the peaks.
54
+ """
55
+ # Drop peak properties ([0]) and direct sound peak ([1])
56
+ return find_peaks(intensity_magnitude)[0]
57
+
58
+
59
+ class WaveletReflectionDetectionStrategy(ReflectionDetectionStrategy):
60
+ """Algorithm for detecting reflections based on the wavelet transform"""
61
+
62
+ @staticmethod
63
+ def get_indeces_of_reflections(intensity_magnitude: np.ndarray) -> np.ndarray:
64
+ """Find local maxima in the intensity magnitude signal.
65
+
66
+ Args:
67
+ intensity_magnitude (np.ndarray): intensity magnitude signal.
68
+
69
+ Returns:
70
+ np.ndarray: an array with the indeces of the peaks.
71
+ """
72
+ # Drop peak properties ([0]) and direct sound peak ([1])
73
+ return find_peaks_cwt(intensity_magnitude, widths=np.arange(5, 15))[0]
74
+
75
+
76
+ class ReflectionDetectionStrategies(Enum):
77
+ """Enum class for accessing the existing `ReflectionDetectionStrategy`s"""
78
+
79
+ CORRELATION = CorrelationReflectionDetectionStrategy
80
+ THRESHOLD = ThresholdReflectionDetectionStrategy
81
+ SCIPY = NeighborReflectionDetectionStrategy
82
+ WAVELET = WaveletReflectionDetectionStrategy
83
+
84
+
85
+ def detect_reflections(
86
+ intensity: np.ndarray,
87
+ azimuth: np.ndarray,
88
+ elevation: np.ndarray,
89
+ detection_strategy: Union[
90
+ ReflectionDetectionStrategy, ReflectionDetectionStrategies
91
+ ] = NeighborReflectionDetectionStrategy,
92
+ ) -> Tuple[np.ndarray]:
93
+ """Analyze the normalized intensity, azimuth and elevation arrays to look for
94
+ reflections. Timeframes which don't contain a reflection are masked.
95
+
96
+ Args:
97
+ intensity (np.ndarray): normalized intensity array.
98
+ azimuth (np.ndarray): array with horizontal angles with respect to the XZ plane.
99
+ elevation (np.ndarray): array with vertical angles with respect to the XY plane.
100
+
101
+ Returns:
102
+ intensity (np.ndarray): masked intensities with only reflections different than 0.
103
+ azimuth (np.ndarray): masked intensities with only reflections different than 0.
104
+ elevation (np.ndarray): masked intensities with only reflections different than 0.
105
+ """
106
+ if isinstance(detection_strategy, ReflectionDetectionStrategies):
107
+ detection_strategy = detection_strategy.value
108
+
109
+ reflections_indeces = detection_strategy.get_indeces_of_reflections(intensity)
110
+ # Add direct sound
111
+ reflections_indeces = np.insert(reflections_indeces, 0, 0)
112
+ return (
113
+ intensity[reflections_indeces],
114
+ azimuth[reflections_indeces],
115
+ elevation[reflections_indeces],
116
+ reflections_indeces,
117
+ )
aira/gui.py ADDED
@@ -0,0 +1,1104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+
3
+ # Form implementation generated from reading ui file 'GUI_v1.ui'
4
+ #
5
+ # Created by: PyQt5 UI code generator 5.15.9
6
+ #
7
+ # WARNING: Any manual changes made to this file will be lost when pyuic5 is
8
+ # run again. Do not edit this file unless you know what you are doing.
9
+
10
+
11
+ from PyQt5 import QtCore, QtGui, QtWidgets, QtWebEngineWidgets
12
+ from PyQt5.QtWidgets import (
13
+ QLabel,
14
+ QFileDialog,
15
+ QMessageBox,
16
+ )
17
+ from PyQt5.QtGui import QPixmap
18
+ from PyQt5.QtCore import Qt
19
+ import os
20
+ from pathlib import Path
21
+
22
+ from aira.core import AmbisonicsImpulseResponseAnalyzer
23
+ from aira.engine.input import InputMode
24
+
25
+ INTEGRATION_TIME = 0.01
26
+
27
+ analyzer = AmbisonicsImpulseResponseAnalyzer(integration_time=INTEGRATION_TIME)
28
+
29
+
30
+ class Ui_MainWindow(object):
31
+ def setupUi(self, MainWindow):
32
+ MainWindow.setObjectName("MainWindow")
33
+ MainWindow.resize(1350, 727)
34
+ MainWindow.setMinimumSize(QtCore.QSize(1000, 800))
35
+ MainWindow.setWindowIcon(QtGui.QIcon(str(Path("docs/images/aira-icon.png"))))
36
+ self.centralwidget = QtWidgets.QWidget(MainWindow)
37
+ self.centralwidget.setStyleSheet("background-color:#313438 ; ")
38
+ self.centralwidget.setObjectName("centralwidget")
39
+ self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget)
40
+ self.verticalLayout.setContentsMargins(0, 0, 0, 0)
41
+ self.verticalLayout.setSpacing(0)
42
+ self.verticalLayout.setObjectName("verticalLayout")
43
+ self.frame_main = QtWidgets.QFrame(self.centralwidget)
44
+ self.frame_main.setFrameShape(QtWidgets.QFrame.StyledPanel)
45
+ self.frame_main.setFrameShadow(QtWidgets.QFrame.Raised)
46
+ self.frame_main.setObjectName("frame_main")
47
+ self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.frame_main)
48
+ self.verticalLayout_2.setContentsMargins(0, 0, 0, 0)
49
+ self.verticalLayout_2.setSpacing(0)
50
+ self.verticalLayout_2.setObjectName("verticalLayout_2")
51
+ self.tabWidget = QtWidgets.QTabWidget(self.frame_main)
52
+ self.tabWidget.setObjectName("tabWidget")
53
+ self.tab_main = QtWidgets.QWidget()
54
+ self.tab_main.setObjectName("tab_main")
55
+ self.horizontalLayout = QtWidgets.QHBoxLayout(self.tab_main)
56
+ self.horizontalLayout.setObjectName("horizontalLayout")
57
+ self.frame_inputs = QtWidgets.QFrame(self.tab_main)
58
+ self.frame_inputs.setMaximumSize(QtCore.QSize(300, 16777215))
59
+ self.frame_inputs.setFrameShape(QtWidgets.QFrame.StyledPanel)
60
+ self.frame_inputs.setFrameShadow(QtWidgets.QFrame.Raised)
61
+ self.frame_inputs.setObjectName("frame_inputs")
62
+ self.verticalLayout_7 = QtWidgets.QVBoxLayout(self.frame_inputs)
63
+ self.verticalLayout_7.setObjectName("verticalLayout_7")
64
+ spacerItem = QtWidgets.QSpacerItem(
65
+ 40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum
66
+ )
67
+ self.verticalLayout_7.addItem(spacerItem)
68
+ spacerItem1 = QtWidgets.QSpacerItem(
69
+ 277, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum
70
+ )
71
+ self.verticalLayout_7.addItem(spacerItem1)
72
+ self.frame_logo_main = QtWidgets.QFrame(self.frame_inputs)
73
+ self.frame_logo_main.setFrameShape(QtWidgets.QFrame.StyledPanel)
74
+ self.frame_logo_main.setFrameShadow(QtWidgets.QFrame.Raised)
75
+ self.frame_logo_main.setObjectName("frame_logo_main")
76
+ self.label_logo_main = QtWidgets.QLabel(self.frame_logo_main)
77
+ self.label_logo_main.setGeometry(QtCore.QRect(20, 30, 177, 50))
78
+ self.label_logo_main.setMaximumSize(QtCore.QSize(500, 500))
79
+ palette = QtGui.QPalette()
80
+ brush = QtGui.QBrush(QtGui.QColor(69, 113, 213))
81
+ brush.setStyle(QtCore.Qt.SolidPattern)
82
+ palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.WindowText, brush)
83
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
84
+ brush.setStyle(QtCore.Qt.SolidPattern)
85
+ palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)
86
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
87
+ brush.setStyle(QtCore.Qt.SolidPattern)
88
+ palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)
89
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
90
+ brush.setStyle(QtCore.Qt.SolidPattern)
91
+ palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)
92
+ brush = QtGui.QBrush(QtGui.QColor(69, 113, 213))
93
+ brush.setStyle(QtCore.Qt.SolidPattern)
94
+ palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.WindowText, brush)
95
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
96
+ brush.setStyle(QtCore.Qt.SolidPattern)
97
+ palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)
98
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
99
+ brush.setStyle(QtCore.Qt.SolidPattern)
100
+ palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)
101
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
102
+ brush.setStyle(QtCore.Qt.SolidPattern)
103
+ palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)
104
+ brush = QtGui.QBrush(QtGui.QColor(120, 120, 120))
105
+ brush.setStyle(QtCore.Qt.SolidPattern)
106
+ palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.WindowText, brush)
107
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
108
+ brush.setStyle(QtCore.Qt.SolidPattern)
109
+ palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)
110
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
111
+ brush.setStyle(QtCore.Qt.SolidPattern)
112
+ palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)
113
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
114
+ brush.setStyle(QtCore.Qt.SolidPattern)
115
+ palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)
116
+ self.label_logo_main.setPalette(palette)
117
+ font = QtGui.QFont()
118
+ font.setFamily("Medel")
119
+ font.setPointSize(24)
120
+ self.label_logo_main.setFont(font)
121
+ self.label_logo_main.setText("")
122
+ self.label_logo_main.setPixmap(
123
+ QtGui.QPixmap(str(Path("docs/images/aira-logo.png")))
124
+ )
125
+ self.label_logo_main.setScaledContents(True)
126
+ self.label_logo_main.setAlignment(QtCore.Qt.AlignCenter)
127
+ self.label_logo_main.setObjectName("label_logo_main")
128
+ self.verticalLayout_7.addWidget(self.frame_logo_main)
129
+ self.frame_settings = QtWidgets.QFrame(self.frame_inputs)
130
+ self.frame_settings.setFrameShape(QtWidgets.QFrame.StyledPanel)
131
+ self.frame_settings.setFrameShadow(QtWidgets.QFrame.Raised)
132
+ self.frame_settings.setObjectName("frame_settings")
133
+ self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.frame_settings)
134
+ self.verticalLayout_5.setObjectName("verticalLayout_5")
135
+ self.frame_analyze = QtWidgets.QFrame(self.frame_settings)
136
+ self.frame_analyze.setFrameShape(QtWidgets.QFrame.StyledPanel)
137
+ self.frame_analyze.setFrameShadow(QtWidgets.QFrame.Raised)
138
+ self.frame_analyze.setObjectName("frame_analyze")
139
+ self.verticalLayout_6 = QtWidgets.QVBoxLayout(self.frame_analyze)
140
+ self.verticalLayout_6.setObjectName("verticalLayout_6")
141
+ spacerItem2 = QtWidgets.QSpacerItem(
142
+ 20, 10, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum
143
+ )
144
+ self.verticalLayout_6.addItem(spacerItem2)
145
+ self.label_integration_window = QtWidgets.QLabel(self.frame_analyze)
146
+ palette = QtGui.QPalette()
147
+ brush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
148
+ brush.setStyle(QtCore.Qt.SolidPattern)
149
+ palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.WindowText, brush)
150
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
151
+ brush.setStyle(QtCore.Qt.SolidPattern)
152
+ palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)
153
+ brush = QtGui.QBrush(QtGui.QColor(0, 0, 0))
154
+ brush.setStyle(QtCore.Qt.SolidPattern)
155
+ palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Text, brush)
156
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
157
+ brush.setStyle(QtCore.Qt.SolidPattern)
158
+ palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)
159
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
160
+ brush.setStyle(QtCore.Qt.SolidPattern)
161
+ palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)
162
+ brush = QtGui.QBrush(QtGui.QColor(0, 0, 0))
163
+ brush.setStyle(QtCore.Qt.SolidPattern)
164
+ palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.ToolTipText, brush)
165
+ brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 128))
166
+ brush.setStyle(QtCore.Qt.SolidPattern)
167
+ palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.PlaceholderText, brush)
168
+ brush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
169
+ brush.setStyle(QtCore.Qt.SolidPattern)
170
+ palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.WindowText, brush)
171
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
172
+ brush.setStyle(QtCore.Qt.SolidPattern)
173
+ palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)
174
+ brush = QtGui.QBrush(QtGui.QColor(0, 0, 0))
175
+ brush.setStyle(QtCore.Qt.SolidPattern)
176
+ palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Text, brush)
177
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
178
+ brush.setStyle(QtCore.Qt.SolidPattern)
179
+ palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)
180
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
181
+ brush.setStyle(QtCore.Qt.SolidPattern)
182
+ palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)
183
+ brush = QtGui.QBrush(QtGui.QColor(0, 0, 0))
184
+ brush.setStyle(QtCore.Qt.SolidPattern)
185
+ palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.ToolTipText, brush)
186
+ brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 128))
187
+ brush.setStyle(QtCore.Qt.SolidPattern)
188
+ palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.PlaceholderText, brush)
189
+ brush = QtGui.QBrush(QtGui.QColor(120, 120, 120))
190
+ brush.setStyle(QtCore.Qt.SolidPattern)
191
+ palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.WindowText, brush)
192
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
193
+ brush.setStyle(QtCore.Qt.SolidPattern)
194
+ palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)
195
+ brush = QtGui.QBrush(QtGui.QColor(120, 120, 120))
196
+ brush.setStyle(QtCore.Qt.SolidPattern)
197
+ palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Text, brush)
198
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
199
+ brush.setStyle(QtCore.Qt.SolidPattern)
200
+ palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)
201
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
202
+ brush.setStyle(QtCore.Qt.SolidPattern)
203
+ palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)
204
+ brush = QtGui.QBrush(QtGui.QColor(0, 0, 0))
205
+ brush.setStyle(QtCore.Qt.SolidPattern)
206
+ palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.ToolTipText, brush)
207
+ brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 128))
208
+ brush.setStyle(QtCore.Qt.SolidPattern)
209
+ palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.PlaceholderText, brush)
210
+ self.label_integration_window.setPalette(palette)
211
+ font = QtGui.QFont()
212
+ font.setFamily("Lato")
213
+ font.setPointSize(12)
214
+ self.label_integration_window.setFont(font)
215
+ self.label_integration_window.setObjectName("label_integration_window")
216
+ self.verticalLayout_6.addWidget(self.label_integration_window)
217
+ self.rB_1ms = QtWidgets.QRadioButton(self.frame_analyze)
218
+ palette = QtGui.QPalette()
219
+ brush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
220
+ brush.setStyle(QtCore.Qt.SolidPattern)
221
+ palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.WindowText, brush)
222
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
223
+ brush.setStyle(QtCore.Qt.SolidPattern)
224
+ palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)
225
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
226
+ brush.setStyle(QtCore.Qt.SolidPattern)
227
+ palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)
228
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
229
+ brush.setStyle(QtCore.Qt.SolidPattern)
230
+ palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)
231
+ brush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
232
+ brush.setStyle(QtCore.Qt.SolidPattern)
233
+ palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.WindowText, brush)
234
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
235
+ brush.setStyle(QtCore.Qt.SolidPattern)
236
+ palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)
237
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
238
+ brush.setStyle(QtCore.Qt.SolidPattern)
239
+ palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)
240
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
241
+ brush.setStyle(QtCore.Qt.SolidPattern)
242
+ palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)
243
+ brush = QtGui.QBrush(QtGui.QColor(120, 120, 120))
244
+ brush.setStyle(QtCore.Qt.SolidPattern)
245
+ palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.WindowText, brush)
246
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
247
+ brush.setStyle(QtCore.Qt.SolidPattern)
248
+ palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)
249
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
250
+ brush.setStyle(QtCore.Qt.SolidPattern)
251
+ palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)
252
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
253
+ brush.setStyle(QtCore.Qt.SolidPattern)
254
+ palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)
255
+ self.rB_1ms.setPalette(palette)
256
+ font = QtGui.QFont()
257
+ font.setFamily("Lato")
258
+ font.setPointSize(12)
259
+ self.rB_1ms.setFont(font)
260
+ self.rB_1ms.setObjectName("rB_1ms")
261
+ self.rB_1ms.setChecked(True)
262
+ self.verticalLayout_6.addWidget(self.rB_1ms)
263
+ self.rB_5ms = QtWidgets.QRadioButton(self.frame_analyze)
264
+ palette = QtGui.QPalette()
265
+ brush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
266
+ brush.setStyle(QtCore.Qt.SolidPattern)
267
+ palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.WindowText, brush)
268
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
269
+ brush.setStyle(QtCore.Qt.SolidPattern)
270
+ palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)
271
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
272
+ brush.setStyle(QtCore.Qt.SolidPattern)
273
+ palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)
274
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
275
+ brush.setStyle(QtCore.Qt.SolidPattern)
276
+ palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)
277
+ brush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
278
+ brush.setStyle(QtCore.Qt.SolidPattern)
279
+ palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.WindowText, brush)
280
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
281
+ brush.setStyle(QtCore.Qt.SolidPattern)
282
+ palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)
283
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
284
+ brush.setStyle(QtCore.Qt.SolidPattern)
285
+ palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)
286
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
287
+ brush.setStyle(QtCore.Qt.SolidPattern)
288
+ palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)
289
+ brush = QtGui.QBrush(QtGui.QColor(120, 120, 120))
290
+ brush.setStyle(QtCore.Qt.SolidPattern)
291
+ palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.WindowText, brush)
292
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
293
+ brush.setStyle(QtCore.Qt.SolidPattern)
294
+ palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)
295
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
296
+ brush.setStyle(QtCore.Qt.SolidPattern)
297
+ palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)
298
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
299
+ brush.setStyle(QtCore.Qt.SolidPattern)
300
+ palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)
301
+ self.rB_5ms.setPalette(palette)
302
+ font = QtGui.QFont()
303
+ font.setFamily("Lato")
304
+ font.setPointSize(12)
305
+ self.rB_5ms.setFont(font)
306
+ self.rB_5ms.setObjectName("rB_5ms")
307
+ self.verticalLayout_6.addWidget(self.rB_5ms)
308
+ self.rB_10ms = QtWidgets.QRadioButton(self.frame_analyze)
309
+ palette = QtGui.QPalette()
310
+ brush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
311
+ brush.setStyle(QtCore.Qt.SolidPattern)
312
+ palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.WindowText, brush)
313
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
314
+ brush.setStyle(QtCore.Qt.SolidPattern)
315
+ palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)
316
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
317
+ brush.setStyle(QtCore.Qt.SolidPattern)
318
+ palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)
319
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
320
+ brush.setStyle(QtCore.Qt.SolidPattern)
321
+ palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)
322
+ brush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
323
+ brush.setStyle(QtCore.Qt.SolidPattern)
324
+ palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.WindowText, brush)
325
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
326
+ brush.setStyle(QtCore.Qt.SolidPattern)
327
+ palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)
328
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
329
+ brush.setStyle(QtCore.Qt.SolidPattern)
330
+ palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)
331
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
332
+ brush.setStyle(QtCore.Qt.SolidPattern)
333
+ palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)
334
+ brush = QtGui.QBrush(QtGui.QColor(120, 120, 120))
335
+ brush.setStyle(QtCore.Qt.SolidPattern)
336
+ palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.WindowText, brush)
337
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
338
+ brush.setStyle(QtCore.Qt.SolidPattern)
339
+ palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)
340
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
341
+ brush.setStyle(QtCore.Qt.SolidPattern)
342
+ palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)
343
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
344
+ brush.setStyle(QtCore.Qt.SolidPattern)
345
+ palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)
346
+ self.rB_10ms.setPalette(palette)
347
+ font = QtGui.QFont()
348
+ font.setFamily("Lato")
349
+ font.setPointSize(12)
350
+ self.rB_10ms.setFont(font)
351
+ self.rB_10ms.setObjectName("rB_10ms")
352
+ self.verticalLayout_6.addWidget(self.rB_10ms)
353
+ spacerItem3 = QtWidgets.QSpacerItem(
354
+ 40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum
355
+ )
356
+ self.verticalLayout_6.addItem(spacerItem3)
357
+ self.label_analysis_length = QtWidgets.QLabel(self.frame_analyze)
358
+ palette = QtGui.QPalette()
359
+ brush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
360
+ brush.setStyle(QtCore.Qt.SolidPattern)
361
+ palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.WindowText, brush)
362
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
363
+ brush.setStyle(QtCore.Qt.SolidPattern)
364
+ palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)
365
+ brush = QtGui.QBrush(QtGui.QColor(0, 0, 0))
366
+ brush.setStyle(QtCore.Qt.SolidPattern)
367
+ palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Text, brush)
368
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
369
+ brush.setStyle(QtCore.Qt.SolidPattern)
370
+ palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)
371
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
372
+ brush.setStyle(QtCore.Qt.SolidPattern)
373
+ palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)
374
+ brush = QtGui.QBrush(QtGui.QColor(0, 0, 0))
375
+ brush.setStyle(QtCore.Qt.SolidPattern)
376
+ palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.ToolTipText, brush)
377
+ brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 128))
378
+ brush.setStyle(QtCore.Qt.SolidPattern)
379
+ palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.PlaceholderText, brush)
380
+ brush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
381
+ brush.setStyle(QtCore.Qt.SolidPattern)
382
+ palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.WindowText, brush)
383
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
384
+ brush.setStyle(QtCore.Qt.SolidPattern)
385
+ palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)
386
+ brush = QtGui.QBrush(QtGui.QColor(0, 0, 0))
387
+ brush.setStyle(QtCore.Qt.SolidPattern)
388
+ palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Text, brush)
389
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
390
+ brush.setStyle(QtCore.Qt.SolidPattern)
391
+ palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)
392
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
393
+ brush.setStyle(QtCore.Qt.SolidPattern)
394
+ palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)
395
+ brush = QtGui.QBrush(QtGui.QColor(0, 0, 0))
396
+ brush.setStyle(QtCore.Qt.SolidPattern)
397
+ palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.ToolTipText, brush)
398
+ brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 128))
399
+ brush.setStyle(QtCore.Qt.SolidPattern)
400
+ palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.PlaceholderText, brush)
401
+ brush = QtGui.QBrush(QtGui.QColor(120, 120, 120))
402
+ brush.setStyle(QtCore.Qt.SolidPattern)
403
+ palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.WindowText, brush)
404
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
405
+ brush.setStyle(QtCore.Qt.SolidPattern)
406
+ palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)
407
+ brush = QtGui.QBrush(QtGui.QColor(120, 120, 120))
408
+ brush.setStyle(QtCore.Qt.SolidPattern)
409
+ palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Text, brush)
410
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
411
+ brush.setStyle(QtCore.Qt.SolidPattern)
412
+ palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)
413
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
414
+ brush.setStyle(QtCore.Qt.SolidPattern)
415
+ palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)
416
+ brush = QtGui.QBrush(QtGui.QColor(0, 0, 0))
417
+ brush.setStyle(QtCore.Qt.SolidPattern)
418
+ palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.ToolTipText, brush)
419
+ brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 128))
420
+ brush.setStyle(QtCore.Qt.SolidPattern)
421
+ palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.PlaceholderText, brush)
422
+ self.label_analysis_length.setPalette(palette)
423
+ font = QtGui.QFont()
424
+ font.setFamily("Lato")
425
+ font.setPointSize(12)
426
+ self.label_analysis_length.setFont(font)
427
+ self.label_analysis_length.setObjectName("label_analysis_length")
428
+ self.verticalLayout_6.addWidget(self.label_analysis_length)
429
+ self.lineEdit_aLength = QtWidgets.QLineEdit(self.frame_analyze)
430
+ self.lineEdit_aLength.setText("100")
431
+ font = QtGui.QFont()
432
+ font.setFamily("Lato")
433
+ font.setPointSize(10)
434
+ self.lineEdit_aLength.setFont(font)
435
+ self.lineEdit_aLength.setStyleSheet(
436
+ "background-color: rgb(255, 255, 255);\n" "\n" ""
437
+ )
438
+ self.lineEdit_aLength.setObjectName("lineEdit_aLength")
439
+ self.verticalLayout_6.addWidget(self.lineEdit_aLength)
440
+ spacerItem4 = QtWidgets.QSpacerItem(
441
+ 40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum
442
+ )
443
+ self.verticalLayout_6.addItem(spacerItem4)
444
+ self.label_threshold = QtWidgets.QLabel(self.frame_analyze)
445
+ palette = QtGui.QPalette()
446
+ brush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
447
+ brush.setStyle(QtCore.Qt.SolidPattern)
448
+ palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.WindowText, brush)
449
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
450
+ brush.setStyle(QtCore.Qt.SolidPattern)
451
+ palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)
452
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
453
+ brush.setStyle(QtCore.Qt.SolidPattern)
454
+ palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)
455
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
456
+ brush.setStyle(QtCore.Qt.SolidPattern)
457
+ palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)
458
+ brush = QtGui.QBrush(QtGui.QColor(255, 255, 255))
459
+ brush.setStyle(QtCore.Qt.SolidPattern)
460
+ palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.WindowText, brush)
461
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
462
+ brush.setStyle(QtCore.Qt.SolidPattern)
463
+ palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)
464
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
465
+ brush.setStyle(QtCore.Qt.SolidPattern)
466
+ palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)
467
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
468
+ brush.setStyle(QtCore.Qt.SolidPattern)
469
+ palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)
470
+ brush = QtGui.QBrush(QtGui.QColor(120, 120, 120))
471
+ brush.setStyle(QtCore.Qt.SolidPattern)
472
+ palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.WindowText, brush)
473
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
474
+ brush.setStyle(QtCore.Qt.SolidPattern)
475
+ palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)
476
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
477
+ brush.setStyle(QtCore.Qt.SolidPattern)
478
+ palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)
479
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
480
+ brush.setStyle(QtCore.Qt.SolidPattern)
481
+ palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)
482
+ self.label_threshold.setPalette(palette)
483
+ font = QtGui.QFont()
484
+ font.setFamily("Lato")
485
+ font.setPointSize(12)
486
+ self.label_threshold.setFont(font)
487
+ self.label_threshold.setObjectName("label_threshold")
488
+ self.verticalLayout_6.addWidget(self.label_threshold)
489
+ self.lineEdit_threshold = QtWidgets.QLineEdit(self.frame_analyze)
490
+ font = QtGui.QFont()
491
+ font.setFamily("Lato")
492
+ font.setPointSize(10)
493
+ self.lineEdit_threshold.setFont(font)
494
+ self.lineEdit_threshold.setStyleSheet(
495
+ "background-color: rgb(255, 255, 255);\n" "\n" ""
496
+ )
497
+ self.lineEdit_threshold.setText("60")
498
+ self.lineEdit_threshold.setObjectName("lineEdit_threshold")
499
+ self.verticalLayout_6.addWidget(self.lineEdit_threshold)
500
+ spacerItem5 = QtWidgets.QSpacerItem(
501
+ 40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum
502
+ )
503
+ self.verticalLayout_6.addItem(spacerItem5)
504
+ self.verticalLayout_5.addWidget(self.frame_analyze)
505
+ self.pb_analyze = QtWidgets.QPushButton(self.frame_settings)
506
+ self.pb_analyze.setMinimumSize(QtCore.QSize(100, 40))
507
+ font = QtGui.QFont()
508
+ font.setFamily("Lato")
509
+ font.setPointSize(14)
510
+ self.pb_analyze.setFont(font)
511
+ self.pb_analyze.setStyleSheet(
512
+ "QPushButton{\n"
513
+ " border: 2px solid rgb(69, 113, 213);\n"
514
+ " border-radius: 10px;\n"
515
+ " background: rgb(69, 113, 213);\n"
516
+ " color: rgb(255, 255, 255)\n"
517
+ "}\n"
518
+ "QPushButton:hover{\n"
519
+ " border: rgb(96, 133, 213);\n"
520
+ " background: rgb(96, 133, 213);\n"
521
+ "}"
522
+ )
523
+ self.pb_analyze.setObjectName("pb_analyze")
524
+ self.verticalLayout_5.addWidget(self.pb_analyze)
525
+ self.verticalLayout_7.addWidget(self.frame_settings)
526
+ spacerItem6 = QtWidgets.QSpacerItem(
527
+ 277, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum
528
+ )
529
+ self.verticalLayout_7.addItem(spacerItem6)
530
+ spacerItem7 = QtWidgets.QSpacerItem(
531
+ 40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum
532
+ )
533
+ self.verticalLayout_7.addItem(spacerItem7)
534
+ spacerItem8 = QtWidgets.QSpacerItem(
535
+ 40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum
536
+ )
537
+ self.verticalLayout_7.addItem(spacerItem8)
538
+ spacerItem9 = QtWidgets.QSpacerItem(
539
+ 40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum
540
+ )
541
+ self.verticalLayout_7.addItem(spacerItem9)
542
+ spacerItem10 = QtWidgets.QSpacerItem(
543
+ 277, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum
544
+ )
545
+ self.verticalLayout_7.addItem(spacerItem10)
546
+ self.horizontalLayout.addWidget(self.frame_inputs)
547
+ self.frame_graphics = QtWidgets.QFrame(self.tab_main)
548
+ self.frame_graphics.setFrameShape(QtWidgets.QFrame.StyledPanel)
549
+ self.frame_graphics.setFrameShadow(QtWidgets.QFrame.Raised)
550
+ self.frame_graphics.setObjectName("frame_graphics")
551
+ self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.frame_graphics)
552
+ self.verticalLayout_4.setContentsMargins(-1, 0, 0, 0)
553
+ self.verticalLayout_4.setObjectName("verticalLayout_4")
554
+ self.frame_hedgehog = QtWidgets.QFrame(self.frame_graphics)
555
+ self.frame_hedgehog.setMinimumSize(QtCore.QSize(0, 400))
556
+ self.frame_hedgehog.setFrameShape(QtWidgets.QFrame.StyledPanel)
557
+ self.frame_hedgehog.setFrameShadow(QtWidgets.QFrame.Raised)
558
+ self.frame_hedgehog.setObjectName("frame_hedgehog")
559
+ self.verticalLayout_8 = QtWidgets.QVBoxLayout(self.frame_hedgehog)
560
+ self.verticalLayout_8.setContentsMargins(0, 0, 0, 0)
561
+ self.verticalLayout_8.setObjectName("verticalLayout_8")
562
+ self.frame_hedgehog_label = QtWidgets.QFrame(self.frame_hedgehog)
563
+ self.frame_hedgehog_label.setMaximumSize(QtCore.QSize(16777215, 42))
564
+ self.frame_hedgehog_label.setFrameShape(QtWidgets.QFrame.StyledPanel)
565
+ self.frame_hedgehog_label.setFrameShadow(QtWidgets.QFrame.Raised)
566
+ self.frame_hedgehog_label.setObjectName("frame_hedgehog_label")
567
+ self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.frame_hedgehog_label)
568
+ self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0)
569
+ self.horizontalLayout_2.setObjectName("horizontalLayout_2")
570
+ spacerItem11 = QtWidgets.QSpacerItem(
571
+ 430, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum
572
+ )
573
+ self.horizontalLayout_2.addItem(spacerItem11)
574
+ self.label_hedgehog = QtWidgets.QLabel(self.frame_hedgehog_label)
575
+ palette = QtGui.QPalette()
576
+ brush = QtGui.QBrush(QtGui.QColor(69, 113, 213))
577
+ brush.setStyle(QtCore.Qt.SolidPattern)
578
+ palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.WindowText, brush)
579
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
580
+ brush.setStyle(QtCore.Qt.SolidPattern)
581
+ palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush)
582
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
583
+ brush.setStyle(QtCore.Qt.SolidPattern)
584
+ palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush)
585
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
586
+ brush.setStyle(QtCore.Qt.SolidPattern)
587
+ palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush)
588
+ brush = QtGui.QBrush(QtGui.QColor(69, 113, 213))
589
+ brush.setStyle(QtCore.Qt.SolidPattern)
590
+ palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.WindowText, brush)
591
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
592
+ brush.setStyle(QtCore.Qt.SolidPattern)
593
+ palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush)
594
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
595
+ brush.setStyle(QtCore.Qt.SolidPattern)
596
+ palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush)
597
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
598
+ brush.setStyle(QtCore.Qt.SolidPattern)
599
+ palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush)
600
+ brush = QtGui.QBrush(QtGui.QColor(120, 120, 120))
601
+ brush.setStyle(QtCore.Qt.SolidPattern)
602
+ palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.WindowText, brush)
603
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
604
+ brush.setStyle(QtCore.Qt.SolidPattern)
605
+ palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush)
606
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
607
+ brush.setStyle(QtCore.Qt.SolidPattern)
608
+ palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush)
609
+ brush = QtGui.QBrush(QtGui.QColor(49, 52, 56))
610
+ brush.setStyle(QtCore.Qt.SolidPattern)
611
+ palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush)
612
+ self.label_hedgehog.setPalette(palette)
613
+ font = QtGui.QFont()
614
+ font.setFamily("Lato")
615
+ font.setPointSize(15)
616
+ self.label_hedgehog.setFont(font)
617
+ self.label_hedgehog.setAlignment(QtCore.Qt.AlignCenter)
618
+ self.label_hedgehog.setObjectName("label_hedgehog")
619
+ self.horizontalLayout_2.addWidget(self.label_hedgehog)
620
+ spacerItem12 = QtWidgets.QSpacerItem(
621
+ 429, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum
622
+ )
623
+ self.horizontalLayout_2.addItem(spacerItem12)
624
+ self.verticalLayout_8.addWidget(self.frame_hedgehog_label)
625
+ self.frame_hedgehog_plot = QtWidgets.QFrame(self.frame_hedgehog)
626
+ self.frame_hedgehog_plot.setFrameShape(QtWidgets.QFrame.StyledPanel)
627
+ self.frame_hedgehog_plot.setFrameShadow(QtWidgets.QFrame.Raised)
628
+ self.frame_hedgehog_plot.setObjectName("frame_hedgehog_plot")
629
+ self.gV_hedgehog = QtWebEngineWidgets.QWebEngineView(self.frame_hedgehog_plot)
630
+ self.gV_hedgehog.setGeometry(QtCore.QRect(0, 0, 1558, 900))
631
+ self.gV_hedgehog.setStyleSheet("background-color: #313438")
632
+ self.gV_hedgehog.setObjectName("gV_hedgehog")
633
+ self.verticalLayout_8.addWidget(self.frame_hedgehog_plot)
634
+ self.verticalLayout_4.addWidget(self.frame_hedgehog)
635
+ self.horizontalLayout.addWidget(self.frame_graphics)
636
+ self.tabWidget.addTab(self.tab_main, "")
637
+ self.tab_plan = QtWidgets.QWidget() # acá empieza la tab 2
638
+ self.tab_plan.setObjectName("tab_plan")
639
+ self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.tab_plan)
640
+ self.verticalLayout_3.setObjectName("verticalLayout_3")
641
+ self.frame_tab_plan = QtWidgets.QFrame(self.tab_plan)
642
+ self.frame_tab_plan.setFrameShape(QtWidgets.QFrame.StyledPanel)
643
+ self.frame_tab_plan.setFrameShadow(QtWidgets.QFrame.Raised)
644
+ self.frame_tab_plan.setObjectName("frame_tab_plan")
645
+ self.verticalLayout_10 = QtWidgets.QVBoxLayout(self.frame_tab_plan)
646
+ self.verticalLayout_10.setObjectName("verticalLayout_10")
647
+ self.frame_plan_header = QtWidgets.QFrame(self.frame_tab_plan)
648
+ self.frame_plan_header.setMaximumSize(QtCore.QSize(16777215, 60))
649
+ self.frame_plan_header.setFrameShape(QtWidgets.QFrame.StyledPanel)
650
+ self.frame_plan_header.setFrameShadow(QtWidgets.QFrame.Raised)
651
+ self.frame_plan_header.setObjectName("frame_plan_header")
652
+ self.frame_logo_plan = QtWidgets.QFrame(self.frame_plan_header)
653
+ self.frame_logo_plan.setGeometry(QtCore.QRect(-10, 0, 201, 101))
654
+ self.frame_logo_plan.setFrameShape(QtWidgets.QFrame.StyledPanel)
655
+ self.frame_logo_plan.setFrameShadow(QtWidgets.QFrame.Raised)
656
+ self.frame_logo_plan.setObjectName("frame_logo_plan")
657
+ self.label_logo_plan = QtWidgets.QLabel(self.frame_logo_plan)
658
+ self.label_logo_plan.setGeometry(QtCore.QRect(30, 20, 141, 41))
659
+ self.label_logo_plan.setText("")
660
+ self.label_logo_plan.setPixmap(
661
+ QtGui.QPixmap(str(Path("docs/images/aira-logo.png")))
662
+ )
663
+ self.label_logo_plan.setScaledContents(True)
664
+ self.label_logo_plan.setObjectName("label_logo_plan")
665
+ self.verticalLayout_10.addWidget(self.frame_plan_header)
666
+ self.frame_view = QtWidgets.QFrame(self.frame_tab_plan)
667
+ self.frame_view.setFrameShape(QtWidgets.QFrame.StyledPanel)
668
+ self.frame_view.setFrameShadow(QtWidgets.QFrame.Raised)
669
+ self.frame_view.setObjectName("frame_view")
670
+ self.horizontalLayout_5 = QtWidgets.QHBoxLayout(self.frame_view)
671
+ self.horizontalLayout_5.setObjectName("horizontalLayout_5")
672
+ self.frame_plan_view = QtWidgets.QFrame(self.frame_view)
673
+ self.frame_plan_view.setFrameShape(QtWidgets.QFrame.StyledPanel)
674
+ self.frame_plan_view.setFrameShadow(QtWidgets.QFrame.Raised)
675
+ self.frame_plan_view.setObjectName("frame_plan_view")
676
+ self.horizontalLayout_6 = QtWidgets.QHBoxLayout(self.frame_plan_view)
677
+ self.horizontalLayout_6.setObjectName("horizontalLayout_6")
678
+ self.label_plan_view = QtWidgets.QLabel(self.frame_plan_view)
679
+ self.label_plan_view.setText("")
680
+ self.label_plan_view.setObjectName("label_plan_view")
681
+ self.label_plan_view.setAlignment(Qt.AlignCenter)
682
+ pixmap = QPixmap(str(Path("docs/images/aira-banner.png")))
683
+ self.label_plan_view.setPixmap(
684
+ pixmap.scaled(750, 3000, aspectRatioMode=Qt.KeepAspectRatio)
685
+ )
686
+ # self.label_plan_view.addWidget(self.label_plan_view)
687
+ self.horizontalLayout_6.addWidget(self.label_plan_view)
688
+ self.horizontalLayout_5.addWidget(self.frame_plan_view)
689
+ self.verticalLayout_10.addWidget(self.frame_view)
690
+ self.frame_plan_footer = QtWidgets.QFrame(self.frame_tab_plan)
691
+ self.frame_plan_footer.setMaximumSize(QtCore.QSize(16777215, 60))
692
+ self.frame_plan_footer.setFrameShape(QtWidgets.QFrame.StyledPanel)
693
+ self.frame_plan_footer.setFrameShadow(QtWidgets.QFrame.Raised)
694
+ self.frame_plan_footer.setObjectName("frame_plan_footer")
695
+ self.horizontalLayout_4 = QtWidgets.QHBoxLayout(self.frame_plan_footer)
696
+ self.horizontalLayout_4.setObjectName("horizontalLayout_4")
697
+ spacerItem15 = QtWidgets.QSpacerItem(
698
+ 265, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum
699
+ )
700
+ self.horizontalLayout_4.addItem(spacerItem15)
701
+ self.pB_load_plan = QtWidgets.QPushButton(self.frame_plan_footer)
702
+ self.pB_load_plan.setMinimumSize(QtCore.QSize(171, 40))
703
+ font = QtGui.QFont()
704
+ font.setFamily("Lato")
705
+ font.setPointSize(14)
706
+ self.pB_load_plan.setFont(font)
707
+ self.pB_load_plan.setStyleSheet(
708
+ "QPushButton{\n"
709
+ " border: 2px solid rgb(69, 113, 213);\n"
710
+ " border-radius: 10px;\n"
711
+ " background: rgb(69, 113, 213);\n"
712
+ " color: rgb(255, 255, 255)\n"
713
+ "}\n"
714
+ "QPushButton:hover{\n"
715
+ " border: rgb(96, 133, 213);\n"
716
+ " background: rgb(96, 133, 213);\n"
717
+ "}\n"
718
+ ""
719
+ )
720
+ self.pB_load_plan.setObjectName("pushButton")
721
+ self.horizontalLayout_4.addWidget(self.pB_load_plan)
722
+ spacerItem16 = QtWidgets.QSpacerItem(
723
+ 382, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum
724
+ )
725
+ self.horizontalLayout_4.addItem(spacerItem16)
726
+ self.pB_export_plan = QtWidgets.QPushButton(self.frame_plan_footer)
727
+ self.pB_export_plan.setEnabled(False)
728
+ self.pB_export_plan.setMinimumSize(QtCore.QSize(171, 40))
729
+ font = QtGui.QFont()
730
+ font.setFamily("Lato")
731
+ font.setPointSize(14)
732
+ self.pB_export_plan.setFont(font)
733
+ self.pB_export_plan.setStyleSheet(
734
+ "QPushButton{\n"
735
+ " border: 2px solid rgb(69, 76, 93);\n"
736
+ " border-radius: 10px;\n"
737
+ " background: rgb(69, 76, 93);\n"
738
+ " color: rgb(255, 255, 255)\n"
739
+ "}\n"
740
+ "QPushButton:hover{\n"
741
+ " border: rgb(96, 133, 213);\n"
742
+ " background: rgb(96, 133, 213);\n"
743
+ "}"
744
+ )
745
+ self.pB_export_plan.setObjectName("pB_export_plan")
746
+ self.horizontalLayout_4.addWidget(self.pB_export_plan)
747
+ spacerItem17 = QtWidgets.QSpacerItem(
748
+ 265, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum
749
+ )
750
+ self.horizontalLayout_4.addItem(spacerItem17)
751
+ self.verticalLayout_10.addWidget(self.frame_plan_footer)
752
+ self.verticalLayout_3.addWidget(self.frame_tab_plan)
753
+ self.tabWidget.addTab(self.tab_plan, "")
754
+ self.verticalLayout_2.addWidget(self.tabWidget)
755
+ self.verticalLayout.addWidget(self.frame_main)
756
+ MainWindow.setCentralWidget(self.centralwidget)
757
+ self.menubar = QtWidgets.QMenuBar(MainWindow)
758
+ self.menubar.setGeometry(QtCore.QRect(0, 0, 1350, 21))
759
+ self.menubar.setObjectName("menubar")
760
+ self.menuFile = QtWidgets.QMenu(self.menubar)
761
+ self.menuFile.setObjectName("menuFile")
762
+ self.menuImport = QtWidgets.QMenu(self.menuFile)
763
+ self.menuImport.setObjectName("menuImport")
764
+ self.menuImport_A_format = QtWidgets.QMenu(self.menuImport)
765
+ self.menuImport_A_format.setObjectName("menuImport_A_format")
766
+ self.menuImport_B_format = QtWidgets.QMenu(self.menuImport)
767
+ self.menuImport_B_format.setObjectName("menuImport_B_format")
768
+ self.menuExport = QtWidgets.QMenu(self.menuFile)
769
+ self.menuExport.setObjectName("menuExport")
770
+ MainWindow.setMenuBar(self.menubar)
771
+ self.statusbar = QtWidgets.QStatusBar(MainWindow)
772
+ self.statusbar.setObjectName("statusbar")
773
+ MainWindow.setStatusBar(self.statusbar)
774
+
775
+ self.actionImport_Aformat_1channel = QtWidgets.QAction(MainWindow)
776
+ self.actionImport_Aformat_1channel.setObjectName(
777
+ "actionImport_Aformat_1channel"
778
+ )
779
+ self.actionImport_Aformat_4channels = QtWidgets.QAction(MainWindow)
780
+ self.actionImport_Aformat_4channels.setObjectName(
781
+ "actionImport_Aformat_4channels"
782
+ )
783
+ self.actionImport_Bformat_1channel = QtWidgets.QAction(MainWindow)
784
+ self.actionImport_Bformat_1channel.setObjectName(
785
+ "actionImport_Bformat_1channel"
786
+ )
787
+ self.actionImport_Bformat_4channels = QtWidgets.QAction(MainWindow)
788
+ self.actionImport_Bformat_4channels.setObjectName(
789
+ "actionImport_Bformat_4channels"
790
+ )
791
+ self.actionImport_LSS = QtWidgets.QAction(MainWindow)
792
+ self.actionImport_LSS.setObjectName("actionImport_LSS")
793
+ self.actionExport_hedgehog_plot = QtWidgets.QAction(MainWindow)
794
+ self.actionExport_hedgehog_plot.setObjectName("actionExport_hedgehog_plot")
795
+
796
+ self.menuImport_A_format.addAction(self.actionImport_Aformat_1channel)
797
+ self.menuImport_A_format.addAction(self.actionImport_Aformat_4channels)
798
+ self.menuImport_B_format.addAction(self.actionImport_Bformat_1channel)
799
+ self.menuImport_B_format.addAction(self.actionImport_Bformat_4channels)
800
+ self.menuImport.addAction(self.actionImport_LSS)
801
+ self.menuExport.addAction(self.actionExport_hedgehog_plot)
802
+
803
+ self.menuFile.addAction(self.menuImport.menuAction())
804
+ self.menuImport.addAction(self.menuImport_A_format.menuAction())
805
+ self.menuImport.addAction(self.menuImport_B_format.menuAction())
806
+ self.menuFile.addAction(self.menuExport.menuAction())
807
+ self.menubar.addAction(self.menuFile.menuAction())
808
+
809
+ self.retranslateUi(MainWindow)
810
+ self.tabWidget.setCurrentIndex(0)
811
+ QtCore.QMetaObject.connectSlotsByName(MainWindow)
812
+
813
+ # CONEXIONES DE GUI CON ACCIONES
814
+
815
+ # Creo los labels para guardar la data que devuelven las funciones de import triggereadas
816
+ self.path_1 = QLabel()
817
+ self.path_2 = QLabel()
818
+ self.path_3 = QLabel()
819
+ self.path_4 = QLabel()
820
+ self.path_5 = QLabel()
821
+ self.input_mode_selected = QLabel()
822
+ self.channels_per_file_selected = QLabel()
823
+
824
+ self.actionImport_LSS.triggered.connect(self.import_LSS)
825
+ self.actionImport_Aformat_1channel.triggered.connect(
826
+ self.import_Aformat_1channel
827
+ )
828
+ self.actionImport_Aformat_4channels.triggered.connect(
829
+ self.import_Aformat_4channels
830
+ )
831
+ self.actionImport_Bformat_1channel.triggered.connect(
832
+ self.import_Bformat_1channel
833
+ )
834
+ self.actionImport_Bformat_4channels.triggered.connect(
835
+ self.import_Bformat_4channels
836
+ )
837
+
838
+ self.pb_analyze.clicked.connect(self.analyze)
839
+ self.pB_load_plan.clicked.connect(self.load_plan)
840
+ self.pB_export_plan.clicked.connect(self.export_plan)
841
+
842
+ def retranslateUi(self, MainWindow):
843
+ _translate = QtCore.QCoreApplication.translate
844
+ MainWindow.setWindowTitle(
845
+ _translate("MainWindow", "Ambisonics Impulse Response Analyzer")
846
+ )
847
+ self.label_integration_window.setText(
848
+ _translate("MainWindow", "Integration Window:")
849
+ )
850
+ self.rB_1ms.setText(_translate("MainWindow", "1 ms"))
851
+ self.rB_5ms.setText(_translate("MainWindow", "5 ms"))
852
+ self.rB_10ms.setText(_translate("MainWindow", "10 ms"))
853
+ self.label_analysis_length.setText(
854
+ _translate("MainWindow", "Analysis Length [ms]")
855
+ )
856
+ self.label_threshold.setText(_translate("MainWindow", "Threshold [dB]"))
857
+ self.pb_analyze.setText(_translate("MainWindow", "Analyze"))
858
+ self.label_hedgehog.setText(_translate("MainWindow", "Hedgehog Plot"))
859
+ self.tabWidget.setTabText(
860
+ self.tabWidget.indexOf(self.tab_main), _translate("MainWindow", "Main")
861
+ )
862
+ self.pB_load_plan.setText(_translate("MainWindow", "Load"))
863
+ self.pB_export_plan.setText(_translate("MainWindow", "Export"))
864
+ self.tabWidget.setTabText(
865
+ self.tabWidget.indexOf(self.tab_plan), _translate("MainWindow", "Plan View")
866
+ )
867
+ self.menuFile.setTitle(_translate("MainWindow", "File"))
868
+ self.menuImport.setTitle(_translate("MainWindow", "Import"))
869
+ self.menuImport_A_format.setTitle(_translate("MainWindow", "A-format"))
870
+ self.menuImport_B_format.setTitle(_translate("MainWindow", "B-format"))
871
+ self.menuExport.setTitle(_translate("MainWindow", "Export"))
872
+ self.actionImport_Aformat_1channel.setText(
873
+ _translate("MainWindow", "1-channel per file")
874
+ )
875
+ self.actionImport_Aformat_4channels.setText(
876
+ _translate("MainWindow", "4-channels per file")
877
+ )
878
+ self.actionImport_Bformat_1channel.setText(
879
+ _translate("MainWindow", "1-channel per file")
880
+ )
881
+ self.actionImport_Bformat_4channels.setText(
882
+ _translate("MainWindow", "4-channels per file")
883
+ )
884
+ self.actionImport_LSS.setText(
885
+ _translate("MainWindow", "LSS with inverse filter")
886
+ )
887
+ self.actionExport_hedgehog_plot.setText(
888
+ _translate("MainWindow", "Hedgehog plot")
889
+ )
890
+
891
+ # Acá empiezan los métodos para las acciones del usuario
892
+
893
+ def analyze(self):
894
+ if self.input_mode_selected.text() == "LSS":
895
+ input_mode = InputMode.LSS
896
+ FLU_path = self.path_1.text()
897
+ FRD_path = self.path_2.text()
898
+ BRU_path = self.path_3.text()
899
+ BLD_path = self.path_4.text()
900
+ IF_path = self.path_5.text()
901
+ channels_per_file = 1
902
+ data = {
903
+ "front_left_up": FLU_path,
904
+ "front_right_down": FRD_path,
905
+ "back_right_up": BRU_path,
906
+ "back_left_down": BLD_path,
907
+ "inverse_filter": IF_path,
908
+ "input_mode": input_mode,
909
+ "channels_per_file": channels_per_file,
910
+ "frequency_correction": True,
911
+ }
912
+ elif self.input_mode_selected.text() == "AFORMAT":
913
+ input_mode = InputMode.AFORMAT
914
+ if self.channels_per_file_selected.text() == "1":
915
+ FLU_path = self.path_1.text()
916
+ FRD_path = self.path_2.text()
917
+ BRU_path = self.path_3.text()
918
+ BLD_path = self.path_4.text()
919
+ channels_per_file = 1
920
+ data = {
921
+ "front_left_up": FLU_path,
922
+ "front_right_down": FRD_path,
923
+ "back_right_up": BRU_path,
924
+ "back_left_down": BLD_path,
925
+ "input_mode": input_mode,
926
+ "channels_per_file": channels_per_file,
927
+ "frequency_correction": True,
928
+ }
929
+ else:
930
+ A4_path = self.path_1.text()
931
+ channels_per_file = 4
932
+ data = {
933
+ "stacked_signals": A4_path,
934
+ "input_mode": input_mode,
935
+ "channels_per_file": channels_per_file,
936
+ "frequency_correction": True,
937
+ }
938
+ else:
939
+ input_mode = InputMode.BFORMAT
940
+ if self.channels_per_file_selected.text() == "1":
941
+ W_path = self.path_1.text()
942
+ X_path = self.path_2.text()
943
+ Y_path = self.path_3.text()
944
+ Z_path = self.path_4.text()
945
+ channels_per_file = 1
946
+ data = {
947
+ "channel_w": W_path,
948
+ "channel_X": X_path,
949
+ "channel_Y": Y_path,
950
+ "channel_Z": Z_path,
951
+ "input_mode": input_mode,
952
+ "channels_per_file": channels_per_file,
953
+ "frequency_correction": True,
954
+ }
955
+ else:
956
+ B4_path = self.path_1.text()
957
+ channels_per_file = 4
958
+ data = {
959
+ "stacked_signals": B4_path,
960
+ "input_mode": input_mode,
961
+ "channels_per_file": channels_per_file,
962
+ "frequency_correction": True,
963
+ }
964
+
965
+ fig = analyzer.analyze(input_dict=data)
966
+ self.gV_hedgehog.setHtml(fig.to_html(include_plotlyjs="cdn"))
967
+
968
+ def load_plan(self):
969
+ file_dialog = QFileDialog()
970
+ file_path, _ = file_dialog.getOpenFileName(
971
+ MainWindow, "Select image", "", "Image file (*.png *.jpg *.jpeg)"
972
+ )
973
+ if file_path:
974
+ pixmap = QPixmap(file_path)
975
+ self.label_plan_view.setPixmap(
976
+ pixmap.scaled(750, 3000, aspectRatioMode=Qt.KeepAspectRatio)
977
+ )
978
+ self.enable_export()
979
+
980
+ def enable_export(self):
981
+ self.pB_export_plan.setEnabled(True)
982
+ self.pB_export_plan.setStyleSheet(
983
+ """
984
+ QPushButton {
985
+ border: 2px solid rgb(69, 113, 213);
986
+ border-radius: 10px;
987
+ background-color: rgb(69, 113, 213);
988
+ color: rgb(255, 255, 255);
989
+ }
990
+ QPushButton:hover {
991
+ border: rgb(96, 133, 213);
992
+ background-color: rgb(96, 133, 213);
993
+ }
994
+ """
995
+ )
996
+
997
+ def export_plan(self):
998
+ file_dialog = QFileDialog()
999
+ save_path, _ = file_dialog.getSaveFileName(
1000
+ MainWindow, "Export image", "", "Image file (*.png *.jpg *.jpeg)"
1001
+ )
1002
+
1003
+ def import_Aformat_1channel(self):
1004
+ file_dialog = QFileDialog()
1005
+ file_path_FLU, _ = file_dialog.getOpenFileName(
1006
+ MainWindow, "Select channel Front-Left-Up", "", "WAV file (*.wav)"
1007
+ )
1008
+ file_path_FRD, _ = file_dialog.getOpenFileName(
1009
+ MainWindow, "Select channel Front-Right-Down", "", "WAV file (*.wav)"
1010
+ )
1011
+ file_path_BRU, _ = file_dialog.getOpenFileName(
1012
+ MainWindow, "Select channel Back-Right-Up", "", "WAV file (*.wav)"
1013
+ )
1014
+ file_path_BLD, _ = file_dialog.getOpenFileName(
1015
+ MainWindow, "Select channel Back-Left-Down", "", "WAV file (*.wav)"
1016
+ )
1017
+ self.path_1.setText(file_path_FLU)
1018
+ self.path_2.setText(file_path_FRD)
1019
+ self.path_3.setText(file_path_BRU)
1020
+ self.path_4.setText(file_path_BLD)
1021
+ self.input_mode_selected.setText("AFORMAT")
1022
+ self.channels_per_file_selected.setText("1")
1023
+
1024
+ def import_Aformat_4channels(self):
1025
+ file_dialog = QFileDialog()
1026
+ file_path_A4, _ = file_dialog.getOpenFileName(
1027
+ MainWindow, "Select audio file", "", "WAV file (*.wav)"
1028
+ )
1029
+ self.path_1.setText(file_path_A4)
1030
+ self.input_mode_selected.setText("AFORMAT")
1031
+ self.channels_per_file_selected.setText("4")
1032
+
1033
+ def import_Bformat_1channel(self):
1034
+ file_dialog = QFileDialog()
1035
+ file_path_W, _ = file_dialog.getOpenFileName(
1036
+ MainWindow, "Select channel W", "", "WAV file (*.wav)"
1037
+ )
1038
+ file_path_X, _ = file_dialog.getOpenFileName(
1039
+ MainWindow, "Select channel X", "", "WAV file (*.wav)"
1040
+ )
1041
+ file_path_Y, _ = file_dialog.getOpenFileName(
1042
+ MainWindow, "Select channel Y", "", "WAV file (*.wav)"
1043
+ )
1044
+ file_path_Z, _ = file_dialog.getOpenFileName(
1045
+ MainWindow, "Select channel Z", "", "WAV file (*.wav)"
1046
+ )
1047
+ self.path_1.setText(file_path_W)
1048
+ self.path_2.setText(file_path_X)
1049
+ self.path_3.setText(file_path_Y)
1050
+ self.path_4.setText(file_path_Z)
1051
+ self.input_mode_selected.setText("BFORMAT")
1052
+ self.channels_per_file_selected.setText("1")
1053
+
1054
+ def import_Bformat_4channels(self):
1055
+ file_dialog = QFileDialog()
1056
+ file_path_B4, _ = file_dialog.getOpenFileName(
1057
+ MainWindow, "Select audio file", "", "WAV file (*.wav)"
1058
+ )
1059
+ self.path_1.setText(file_path_B4)
1060
+ self.input_mode_selected.setText("BFORMAT")
1061
+ self.channels_per_file_selected.setText("4")
1062
+
1063
+ def import_LSS(self):
1064
+ file_dialog = QFileDialog()
1065
+ file_path_FLU, _ = file_dialog.getOpenFileName(
1066
+ MainWindow, "Select channel Front-Left-Up", "", "WAV file (*.wav)"
1067
+ )
1068
+ file_path_FRD, _ = file_dialog.getOpenFileName(
1069
+ MainWindow, "Select channel Front-Right-Down", "", "WAV file (*.wav)"
1070
+ )
1071
+ file_path_BRU, _ = file_dialog.getOpenFileName(
1072
+ MainWindow, "Select channel Back-Right-Up", "", "WAV file (*.wav)"
1073
+ )
1074
+ file_path_BLD, _ = file_dialog.getOpenFileName(
1075
+ MainWindow, "Select channel Back-Left-Down", "", "WAV file (*.wav)"
1076
+ )
1077
+ file_path_IF, _ = file_dialog.getOpenFileName(
1078
+ MainWindow, "Select inverse filter", "", "WAV file (*.wav)"
1079
+ )
1080
+ self.path_1.setText(file_path_FLU)
1081
+ self.path_2.setText(file_path_FRD)
1082
+ self.path_3.setText(file_path_BRU)
1083
+ self.path_4.setText(file_path_BLD)
1084
+ self.path_5.setText(file_path_IF)
1085
+ self.input_mode_selected.setText("LSS")
1086
+ self.channels_per_file_selected.setText("1")
1087
+
1088
+ def export_hedgehog(self):
1089
+ file_dialog = QFileDialog()
1090
+ save_path, _ = file_dialog.getSaveFileName(
1091
+ MainWindow, "Export image", "", "Image file (*.png *.jpg *.jpeg)"
1092
+ )
1093
+
1094
+
1095
+ if __name__ == "__main__":
1096
+ import sys
1097
+
1098
+ app = QtWidgets.QApplication(sys.argv)
1099
+ MainWindow = QtWidgets.QMainWindow()
1100
+ ui = Ui_MainWindow()
1101
+ ui.setupUi(MainWindow)
1102
+ MainWindow.show()
1103
+ MainWindow.showMaximized()
1104
+ sys.exit(app.exec_())
aira/utils/__init__.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ """Utils module imports."""
2
+
3
+ from .formatter import (
4
+ convert_ambisonics_a_to_b,
5
+ cartesian_to_spherical,
6
+ spherical_to_cartesian,
7
+ )
8
+ from .utils import read_signals_dict, read_aformat
aira/utils/__pycache__/__init__.cpython-38.pyc ADDED
Binary file (380 Bytes). View file
 
aira/utils/__pycache__/formatter.cpython-38.pyc ADDED
Binary file (3.32 kB). View file
 
aira/utils/__pycache__/utils.cpython-38.pyc ADDED
Binary file (5.22 kB). View file
 
aira/utils/formatter.py ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """3D format conversion for coordinates and Ambisonics"""
2
+
3
+ from functools import singledispatch
4
+ from typing import List, Tuple, Union
5
+
6
+ import numpy as np
7
+
8
+
9
+ @singledispatch
10
+ def convert_ambisonics_a_to_b(
11
+ front_left_up: np.ndarray,
12
+ front_right_down: np.ndarray,
13
+ back_right_up: np.ndarray,
14
+ back_left_down: np.ndarray,
15
+ ) -> np.ndarray:
16
+ """Converts Ambisonics A-format to B-format
17
+
18
+ Parameters
19
+ ----------
20
+ front_left_up : np.ndarray
21
+ Front Left Up signal from A-format
22
+ front_right_down : np.ndarray
23
+ Front Right Down signal from A-format
24
+ back_right_up : np.ndarray
25
+ Back Right Up signal from A-format
26
+ back_left_down : np.ndarray
27
+ Back Left Down signal from A-format
28
+
29
+ Returns
30
+ -------
31
+ np.ndarray
32
+ B-format outputs (W, X, Y, Z)
33
+ """
34
+
35
+ front = front_left_up + front_right_down
36
+ back = back_left_down + back_right_up
37
+ left = front_left_up + back_left_down
38
+ right = front_right_down + back_right_up
39
+ up = front_left_up + back_right_up # pylint: disable=invalid-name
40
+ down = front_right_down + back_left_down
41
+
42
+ w_channel = front + back
43
+ x_channel = front - back
44
+ y_channel = left - right
45
+ z_channel = up - down
46
+
47
+ return np.array([w_channel, x_channel, y_channel, z_channel])
48
+
49
+
50
+ @convert_ambisonics_a_to_b.register(list)
51
+ def _(aformat_channels: List[np.ndarray]) -> np.ndarray:
52
+ """Converts Ambisonics A-format to B-format.
53
+
54
+ Parameters
55
+ ----------
56
+ aformat_channels : List[np.ndarray]
57
+ A list containing the 4 channels of A-format Ambisonics in the following order:
58
+ 1. Front Left Up
59
+ 2. Front Right Down
60
+ 3. Back Right Up
61
+ 4. Back Left Down
62
+
63
+ Returns
64
+ -------
65
+ np.ndarray
66
+ B-format outputs (W, X, Y, Z)
67
+ """
68
+ assert (
69
+ len(aformat_channels) == 4
70
+ ), "Conversion from A-format to B-format requires 4 channels"
71
+ return convert_ambisonics_a_to_b.dispatch(np.ndarray)(
72
+ front_left_up=aformat_channels[0],
73
+ front_right_down=aformat_channels[1],
74
+ back_right_up=aformat_channels[2],
75
+ back_left_down=aformat_channels[3],
76
+ )
77
+
78
+
79
+ def spherical_to_cartesian(
80
+ radius: Union[float, np.ndarray],
81
+ azimuth: Union[float, np.ndarray],
82
+ elevation: Union[float, np.ndarray],
83
+ ) -> Tuple[Union[float, np.ndarray]]:
84
+ """Convert three 3D polar coordinates to Cartesian ones.
85
+
86
+ Parameters
87
+ radius: float | np.ndarray. The radii (or rho).
88
+ azimuth: float | np.ndarray. The azimuth (also called theta or alpha).
89
+ elevation: float | np.ndarray. The elevation (also called phi or polar).
90
+
91
+ Returns
92
+ (x, y, z): Tuple[float | np.ndarray]. The corresponding Cartesian coordinates.
93
+ """
94
+ return (
95
+ radius * np.cos(np.deg2rad(azimuth)) * np.cos(np.deg2rad(elevation)),
96
+ radius * np.sin(np.deg2rad(azimuth)) * np.cos(np.deg2rad(elevation)),
97
+ radius * np.sin(np.deg2rad(elevation)),
98
+ )
99
+
100
+
101
+ def cartesian_to_spherical(intensity_windowed: np.ndarray):
102
+ """_summary_
103
+
104
+ Parameters
105
+ ----------
106
+ intensity_windowed : np.ndarray
107
+ _description_
108
+
109
+ Returns
110
+ -------
111
+ _type_
112
+ _description_
113
+ """
114
+ # Convert to total intensity, azimuth and elevation
115
+ intensity = np.sqrt((intensity_windowed**2).sum(axis=0)).squeeze()
116
+ azimuth = np.rad2deg(
117
+ np.arctan2(intensity_windowed[1], intensity_windowed[0])
118
+ ).squeeze()
119
+ elevation = np.rad2deg(np.arcsin(intensity_windowed[2] / intensity)).squeeze()
120
+ return intensity, azimuth, elevation
aira/utils/utils.py ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Audio utilities"""
2
+
3
+ from functools import singledispatch
4
+ from pathlib import Path
5
+ from traceback import print_exc
6
+ from typing import Dict, List, Tuple, Union
7
+
8
+ import numpy as np
9
+ import soundfile as sf
10
+
11
+
12
+ def read_signals_dict(signals_dict: dict) -> dict:
13
+ """Read the signals contained in signals_dict and overwrites the paths with the arrays.
14
+
15
+ Parameters
16
+ ----------
17
+ signals_dict : dict
18
+ Dictionary with signals path.
19
+
20
+ Returns
21
+ -------
22
+ dict
23
+ Same signals_dict dictionary with the signals array overwritting signals path.
24
+ """
25
+ for key_i, path_i in signals_dict.items():
26
+ try:
27
+ signal_i, sample_rate = sf.read(path_i)
28
+ signals_dict[key_i] = signal_i.T
29
+ except:
30
+ pass
31
+
32
+ signals_dict["sample_rate"] = sample_rate
33
+
34
+ if signals_dict["channels_per_file"] == 1:
35
+ if signals_dict["input_mode"] == "bformat":
36
+ bformat_keys = ["w_channel", "x_channel", "y_channel", "z_channel"]
37
+ signals_dict["stacked_signals"] = stack_dict_arrays(
38
+ signals_dict, bformat_keys
39
+ )
40
+ else:
41
+ aformat_keys = [
42
+ "front_left_up",
43
+ "front_right_down",
44
+ "back_right_up",
45
+ "back_left_down",
46
+ ]
47
+ signals_dict["stacked_signals"] = stack_dict_arrays(
48
+ signals_dict, aformat_keys
49
+ )
50
+
51
+ return signals_dict
52
+
53
+
54
+ def stack_dict_arrays(signals_dict_array: dict, keys: List[str]) -> np.ndarray:
55
+ """Stacks arrays into single numpy.ndarray object given the dictionary and the keys
56
+ to be stacked.
57
+
58
+ Parameters
59
+ ----------
60
+ signals_dict_array : dict
61
+ Dictionary containing the arrays to be stacked
62
+ keys : List[str]
63
+ Keys of signals_dict_array with the arrays to be stacked
64
+
65
+ Returns
66
+ -------
67
+ np.ndarray
68
+ Stacked arrays into single numpy.ndarray object
69
+ """
70
+ audio_array = []
71
+ for key_i in keys:
72
+ audio_array.append(signals_dict_array[key_i])
73
+
74
+ return audio_array
75
+
76
+
77
+ @singledispatch
78
+ def read_aformat(audio_path: Union[str, Path]) -> Tuple[np.ndarray, float]:
79
+ """Read an A-format Ambisonics signal from a single audio path, which is expected
80
+ to contain 4 channels.
81
+
82
+ Parameters
83
+ ----------
84
+ audio_paths : str | Path
85
+ Strings containing the audio paths to be loaded
86
+
87
+ Returns
88
+ -------
89
+ Tuple[np.ndarray, float]
90
+ Sample rate of the audios and audios loaded as rows of a np.ndarray
91
+ """
92
+ signal, sample_rate = sf.read(audio_path)
93
+ signal = signal.T
94
+ assert signal.shape[0] == 4, (
95
+ f"Audio file {str(audio_path)} with shape {signal.shape} does not"
96
+ f"contain 4 channels, so it cannot be A-format Ambisonics"
97
+ )
98
+ return signal, sample_rate
99
+
100
+
101
+ @read_aformat.register(list)
102
+ def _(audio_paths: List[str]) -> Tuple[np.ndarray, float]:
103
+ """Read an A-format Ambisonics signal from audio paths. 4 paths are expected,
104
+ one for each cardioid signal, in the following order:
105
+ 1. front left up
106
+ 2. front right down
107
+ 3. back right up
108
+ 4. back left down
109
+
110
+ Parameters
111
+ ----------
112
+ audio_paths : List[str]
113
+ Strings containing the audio paths to be loaded
114
+
115
+ Returns
116
+ -------
117
+ Tuple[np.ndarray, float]
118
+ Sample rate of the audios and audios loaded as rows of a np.ndarray
119
+ """
120
+ assert (isinstance(audio_paths, (str, Path, list))) or (
121
+ len(audio_paths) in (1, 4)
122
+ ), "One wave file with 4 channels or a list of 4 wave files is expected"
123
+
124
+ audio_array = []
125
+ for audio_i in audio_paths:
126
+ try:
127
+ audio_array_i, sample_rate = sf.read(audio_i)
128
+ audio_array.append(audio_array_i)
129
+ except sf.SoundFileError:
130
+ print_exc()
131
+
132
+ return np.array(audio_array), sample_rate
133
+
134
+
135
+ @read_aformat.register(dict)
136
+ def _(audio_paths: Dict[str, str]) -> Tuple[np.ndarray, float]:
137
+ """Read an A-format Ambisonics signal from a dictionary with audio paths. 4 keys are expected,
138
+ one for each cardioid signal:
139
+ 1. front_left_up
140
+ 2. front_right_down
141
+ 3. back_right_up
142
+ 4. back_left_down
143
+
144
+ Parameters
145
+ ----------
146
+ audio_paths : Dict[str]
147
+ Key-value pair containing the audio paths to be loaded for each FLU/FRD/BRU/BLD channel
148
+
149
+ Returns
150
+ -------
151
+ Tuple[np.ndarray, float]
152
+ Sample rate of the audios and audios loaded as rows of a np.ndarray
153
+ """
154
+ ordered_aformat_channels = (
155
+ "front_left_up",
156
+ "front_right_down",
157
+ "back_right_up",
158
+ "back_left_down",
159
+ ) # Assert the ordering is standardized across the project
160
+ try:
161
+ audio_data = {
162
+ cardioid_channel: dict(zip(("signal", "sample_rate"), sf.read(path)))
163
+ for cardioid_channel, path in audio_paths.items()
164
+ }
165
+
166
+ # refactor from here
167
+ audio_signals = [
168
+ audio_data[channel_name]["signal"]
169
+ for channel_name in ordered_aformat_channels
170
+ ]
171
+ sample_rates = [
172
+ audio_data[channel_name]["sample_rate"]
173
+ for channel_name in ordered_aformat_channels
174
+ ]
175
+ assert len(set(sample_rates)) == 1, "Multiple different sample rates were found"
176
+
177
+ signals_array = np.array(audio_signals)
178
+ return signals_array, sample_rates[0]
179
+ except sf.SoundFileError:
180
+ print_exc()
app.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import plotly.graph_objects as go
3
+ from aira.core import AmbisonicsImpulseResponseAnalyzer
4
+ from aira.engine.input import InputMode
5
+ import os
6
+ import tempfile
7
+
8
+
9
+ def save_temp_file(file):
10
+ temp_dir = tempfile.gettempdir()
11
+ temp_file_path = os.path.join(temp_dir, file.name)
12
+ with open(temp_file_path, "wb") as temp_file:
13
+ temp_file.write(file.getvalue())
14
+ return temp_file_path
15
+
16
+
17
+ def run_streamlit_app():
18
+ st.set_page_config(
19
+ page_title="AIRA", page_icon="docs/images/aira-icon.png", layout="wide"
20
+ )
21
+
22
+ # Logo
23
+ logo_path = "docs/images/aira-banner.png"
24
+ personal_logo_path = "docs/images/nahue-passano.png"
25
+ st.columns(11)[10].image(personal_logo_path, use_column_width=True)
26
+ st.columns(3)[1].image(logo_path, use_column_width=True)
27
+
28
+ # Audio loading and settings
29
+
30
+ audio_files, settings = st.columns(2)
31
+
32
+ with audio_files:
33
+ st.header("🔉 LSS room responses in A-Format")
34
+ up_files, down_files = st.columns(2)
35
+
36
+ with up_files:
37
+ audio_file_flu = st.file_uploader("Front-Left-Up", type=["mp3", "wav"])
38
+ audio_file_bru = st.file_uploader("Back-Right-Up", type=["mp3", "wav"])
39
+
40
+ with down_files:
41
+ audio_file_frd = st.file_uploader("Front-Right-Down", type=["mp3", "wav"])
42
+ audio_file_bld = st.file_uploader("Back-Left-Down", type=["mp3", "wav"])
43
+
44
+ audio_file_inverse_filter = st.file_uploader(
45
+ "Inverse filter", type=["mp3", "wav"]
46
+ )
47
+
48
+ with settings:
49
+ st.header("⚙️ Settings")
50
+ integration_time = st.selectbox("Integration time [ms]", [1, 5, 10])
51
+ analysis_length = st.text_input("Analysis length [ms]", value="500")
52
+ intensity_threshold = st.text_input("Intensity threshold [dB]", value=-60)
53
+
54
+ # "Analyze" button
55
+ if st.button("Analyze", use_container_width=True):
56
+ data = {
57
+ "front_left_up": save_temp_file(audio_file_flu),
58
+ "front_right_down": save_temp_file(audio_file_frd),
59
+ "back_right_up": save_temp_file(audio_file_bru),
60
+ "back_left_down": save_temp_file(audio_file_bld),
61
+ "inverse_filter": save_temp_file(audio_file_inverse_filter),
62
+ "input_mode": InputMode.LSS,
63
+ "channels_per_file": 1,
64
+ "frequency_correction": True,
65
+ }
66
+
67
+ analyzer = AmbisonicsImpulseResponseAnalyzer(
68
+ int(integration_time),
69
+ float(intensity_threshold),
70
+ float(analysis_length),
71
+ )
72
+ fig = analyzer.analyze(data, show=False)
73
+ fig.update_layout(height=1080)
74
+ st.plotly_chart(fig, use_container_width=True, height=1080)
75
+
76
+ # Generar un gráfico genérico con Plotly
77
+ fig = go.Figure(data=go.Scatter(x=[1, 2, 3, 4], y=[10, 5, 7, 2]))
78
+ st.plotly_chart(fig)
79
+
80
+
81
+ if __name__ == "__main__":
82
+ run_streamlit_app()
docs/images/aira-banner.png ADDED
docs/images/aira-gui.png ADDED
docs/images/aira-icon.png ADDED
docs/images/aira-logo.png ADDED
docs/images/nahue-passano.png ADDED
pyproject.toml ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [tool.poetry]
2
+ name = "aira"
3
+ version = "0.1.0"
4
+ description = "3D Ambisonics Impulse Response Analyzer"
5
+ authors = [
6
+ "Ivan Pupkin <ipupkin@untref.edu.ar>",
7
+ "Nahuel Passano <passano43632@estudiantes.untref.edu.ar>"
8
+ ]
9
+
10
+ [tool.poetry.dependencies]
11
+ python = ">=3.8,<3.9.7 || >3.9.7,<3.11"
12
+ numpy = "^1.24.3"
13
+ librosa = "^0.10.0"
14
+ scipy = "^1.10.1"
15
+ soundfile = "^0.12.1"
16
+ plotly = "^5.14.1"
17
+ pandas = "^2.0.1"
18
+ kaleido = "0.2.1"
19
+ matplotlib = "^3.7.1"
20
+ pyqt5 = "^5.15.9"
21
+ pyqtwebengine = "^5.15.6"
22
+ streamlit = "^1.24.0"
23
+
24
+
25
+ [tool.poetry.dev-dependencies]
26
+ black = "^23.3.0"
27
+ pylint = "^2.17.3"
28
+ pytest = "^7.3.1"
29
+ pre-commit = "^3.2.2"
30
+
31
+ [build-system]
32
+ requires = ["poetry-core>=1.0.0"]
33
+ build-backend = "poetry.core.masonry.api"
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ numpy==1.24.3
2
+ librosa==0.10.0
3
+ scipy==1.10.1
4
+ soundfile==0.12.1
5
+ plotly==5.14.1
6
+ pandas==2.0.1
7
+ kaleido == 0.2.1
8
+ matplotlib==3.7.1
9
+ pyqt5==5.15.9
10
+ pyqtwebengine==5.15.6
11
+ streamlit==1.24.0