PyQa40x-i2c 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. pyqa40x_i2c-0.1.0/.github/workflows/code_checker.yml +22 -0
  2. pyqa40x_i2c-0.1.0/.github/workflows/publish_package.yml +36 -0
  3. pyqa40x_i2c-0.1.0/.github/workflows/unit_tests.yml +37 -0
  4. pyqa40x_i2c-0.1.0/.gitignore +6 -0
  5. pyqa40x_i2c-0.1.0/.pylint_ignore +1 -0
  6. pyqa40x_i2c-0.1.0/LICENSE +21 -0
  7. pyqa40x_i2c-0.1.0/LibTest/FirstPrinciples.py +193 -0
  8. pyqa40x_i2c-0.1.0/LibTest/PyQa40x_acoustic_chirp_spl_bt.py +131 -0
  9. pyqa40x_i2c-0.1.0/LibTest/PyQa40x_acoustic_dbspl.py +49 -0
  10. pyqa40x_i2c-0.1.0/LibTest/PyQa40x_acoustic_energy_decay_bt.py +121 -0
  11. pyqa40x_i2c-0.1.0/LibTest/PyQa40x_coherent_averaging.py +73 -0
  12. pyqa40x_i2c-0.1.0/LibTest/PyQa40x_cross_correlation.py +122 -0
  13. pyqa40x_i2c-0.1.0/LibTest/PyQa40x_freq_response_with_mask.py +197 -0
  14. pyqa40x_i2c-0.1.0/LibTest/PyQa40x_pinknoise.py +168 -0
  15. pyqa40x_i2c-0.1.0/LibTest/PyQa40x_thdn.py +53 -0
  16. pyqa40x_i2c-0.1.0/LibTest/mask_freq.txt +10 -0
  17. pyqa40x_i2c-0.1.0/LibTest/mask_phase.txt +10 -0
  18. pyqa40x_i2c-0.1.0/MANIFEST.in +2 -0
  19. pyqa40x_i2c-0.1.0/PKG-INFO +110 -0
  20. pyqa40x_i2c-0.1.0/README.md +63 -0
  21. pyqa40x_i2c-0.1.0/RELEASE_NOTES.md +3 -0
  22. pyqa40x_i2c-0.1.0/_ci/is_dev_version.sh +11 -0
  23. pyqa40x_i2c-0.1.0/pyproject.toml +27 -0
  24. pyqa40x_i2c-0.1.0/requirements-dev.txt +1 -0
  25. pyqa40x_i2c-0.1.0/requirements-test.txt +4 -0
  26. pyqa40x_i2c-0.1.0/requirements.txt +4 -0
  27. pyqa40x_i2c-0.1.0/setup.cfg +4 -0
  28. pyqa40x_i2c-0.1.0/src/PyQa40x/__init__.py +18 -0
  29. pyqa40x_i2c-0.1.0/src/PyQa40x/_version.py +24 -0
  30. pyqa40x_i2c-0.1.0/src/PyQa40x/analyzer.py +322 -0
  31. pyqa40x_i2c-0.1.0/src/PyQa40x/analyzer_params.py +66 -0
  32. pyqa40x_i2c-0.1.0/src/PyQa40x/bluetooth.py +134 -0
  33. pyqa40x_i2c-0.1.0/src/PyQa40x/control.py +137 -0
  34. pyqa40x_i2c-0.1.0/src/PyQa40x/cross_correlation.py +80 -0
  35. pyqa40x_i2c-0.1.0/src/PyQa40x/fft_processor.py +93 -0
  36. pyqa40x_i2c-0.1.0/src/PyQa40x/freq_series_plotter.py +81 -0
  37. pyqa40x_i2c-0.1.0/src/PyQa40x/helpers.py +163 -0
  38. pyqa40x_i2c-0.1.0/src/PyQa40x/math_chirp.py +122 -0
  39. pyqa40x_i2c-0.1.0/src/PyQa40x/math_energy.py +36 -0
  40. pyqa40x_i2c-0.1.0/src/PyQa40x/math_general.py +28 -0
  41. pyqa40x_i2c-0.1.0/src/PyQa40x/math_sines.py +126 -0
  42. pyqa40x_i2c-0.1.0/src/PyQa40x/math_windows.py +64 -0
  43. pyqa40x_i2c-0.1.0/src/PyQa40x/registers.py +43 -0
  44. pyqa40x_i2c-0.1.0/src/PyQa40x/series_plotter.py +191 -0
  45. pyqa40x_i2c-0.1.0/src/PyQa40x/sig_proc.py +62 -0
  46. pyqa40x_i2c-0.1.0/src/PyQa40x/stream.py +180 -0
  47. pyqa40x_i2c-0.1.0/src/PyQa40x/wave.py +277 -0
  48. pyqa40x_i2c-0.1.0/src/PyQa40x/wave_chirp.py +175 -0
  49. pyqa40x_i2c-0.1.0/src/PyQa40x/wave_sine.py +85 -0
  50. pyqa40x_i2c-0.1.0/src/PyQa40x.egg-info/PKG-INFO +20 -0
  51. pyqa40x_i2c-0.1.0/src/PyQa40x.egg-info/SOURCES.txt +28 -0
  52. pyqa40x_i2c-0.1.0/src/PyQa40x.egg-info/dependency_links.txt +1 -0
  53. pyqa40x_i2c-0.1.0/src/PyQa40x.egg-info/requires.txt +4 -0
  54. pyqa40x_i2c-0.1.0/src/PyQa40x.egg-info/top_level.txt +1 -0
  55. pyqa40x_i2c-0.1.0/src/PyQa40x_i2c.egg-info/PKG-INFO +110 -0
  56. pyqa40x_i2c-0.1.0/src/PyQa40x_i2c.egg-info/SOURCES.txt +60 -0
  57. pyqa40x_i2c-0.1.0/src/PyQa40x_i2c.egg-info/dependency_links.txt +1 -0
  58. pyqa40x_i2c-0.1.0/src/PyQa40x_i2c.egg-info/requires.txt +17 -0
  59. pyqa40x_i2c-0.1.0/src/PyQa40x_i2c.egg-info/top_level.txt +1 -0
  60. pyqa40x_i2c-0.1.0/tests/__init__.py +0 -0
  61. pyqa40x_i2c-0.1.0/tests/test_template.py +10 -0
  62. pyqa40x_i2c-0.1.0/tox.ini +33 -0
@@ -0,0 +1,22 @@
1
+ name: Code-checker
2
+
3
+ on: [push]
4
+
5
+ jobs:
6
+ build:
7
+ runs-on: ubuntu-latest
8
+ strategy:
9
+ matrix:
10
+ python-version: ["3.12"]
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ with:
14
+ fetch-depth: 0
15
+ - name: Set up Python ${{ matrix.python-version }}
16
+ uses: actions/setup-python@v3
17
+ with:
18
+ python-version: ${{ matrix.python-version }}
19
+ - name: Install dependencies
20
+ run: |
21
+ python -m pip install --upgrade pip
22
+ pip install .[dev]
@@ -0,0 +1,36 @@
1
+ name: Publish Python Package
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - '*'
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ build-and-publish:
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - name: Check out code
15
+ uses: actions/checkout@v4
16
+ with:
17
+ fetch-depth: 0 # required for setuptools-scm to determine version from tags
18
+
19
+ - name: Set up Python
20
+ uses: actions/setup-python@v4
21
+ with:
22
+ python-version: '3.12'
23
+
24
+ - name: Install dependencies
25
+ run: |
26
+ python -m pip install --upgrade pip
27
+ pip install build twine
28
+
29
+ - name: Build package
30
+ run: python -m build
31
+
32
+ - name: Publish to PyPI
33
+ env:
34
+ TWINE_USERNAME: __token__
35
+ TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
36
+ run: python -m twine upload --verbose dist/*
@@ -0,0 +1,37 @@
1
+ name: Unit-tests
2
+
3
+ on: [push]
4
+
5
+ jobs:
6
+ build:
7
+ runs-on: ubuntu-latest
8
+ strategy:
9
+ matrix:
10
+ python-version: ["3.12"]
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ with:
14
+ fetch-depth: 0
15
+ - name: Set up Python ${{ matrix.python-version }}
16
+ uses: actions/setup-python@v3
17
+ with:
18
+ python-version: ${{ matrix.python-version }}
19
+ - name: Install dependencies
20
+ run: |
21
+ python -m pip install --upgrade pip
22
+ pip install .[test]
23
+ - name: Run tests
24
+ run: |
25
+ pytest -vv --log-level DEBUG \
26
+ --cov src \
27
+ --cov-branch \
28
+ --cov-report html:reports/pytest_coverage \
29
+ --cov-fail-under 0 \
30
+ --junitxml=reports/junit/test-results-${{ matrix.python-version }}.xml
31
+ - name: Upload pytest test results
32
+ uses: actions/upload-artifact@v4
33
+ with:
34
+ name: reports-${{ matrix.python-version }}
35
+ path: |
36
+ reports
37
+ if: ${{ always() }}
@@ -0,0 +1,6 @@
1
+ *.pyc
2
+ *.wsuo
3
+ *.json
4
+ *.sqlite
5
+ *.vsidx
6
+ .vs/
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 QuantAsylum
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,193 @@
1
+ import numpy as np
2
+ import matplotlib.pyplot as plt
3
+ from scipy.signal.windows import flattop, boxcar, hamming
4
+ from scipy.fft import rfft, rfftfreq
5
+
6
+ #
7
+ # A standalone file that demonstrates how to preserve amplitudes
8
+ # and energy measurements across changes in FFT size, windows, etc
9
+ # using standard python libs
10
+
11
+ # Define the parameters
12
+
13
+ # FFT Size. Use power of two
14
+ N = 4096
15
+
16
+ # Sampel rate
17
+ fs = 48000
18
+
19
+ # Target frequency in Hz.
20
+ f_target = 1000
21
+
22
+ # Choose the window function
23
+ window_function = 'hamming' # Change to 'flattop', 'boxcar', or 'hamming'
24
+
25
+ # This sets the target frequency adjustment strategy.
26
+ # none will not make a change to the target frequency
27
+ # snap will nudge the frequency to the center of the nearest bin
28
+ # straddle will nudge the frequency such that it straddles two bins
29
+ f_adjust_strategy = 'snap' # Set to 'none', 'snap', or 'straddle'
30
+
31
+ ##################################
32
+ ##################################
33
+ ##################################
34
+
35
+ bin_resolution = fs / N
36
+
37
+ # Adjust f_target based on the provided strategy
38
+ if f_adjust_strategy == 'snap':
39
+ f = round(f_target / bin_resolution) * bin_resolution
40
+ elif f_adjust_strategy == 'straddle':
41
+ bin_index = round(f_target / bin_resolution)
42
+ f = (bin_index + 0.5) * bin_resolution
43
+ else:
44
+ f = f_target
45
+
46
+ t = np.arange(N) / fs # Time vector
47
+
48
+ # Create a sinusoidal signal with a peak amplitude of 1.41 = 0 dBV
49
+ amplitude = 1.41
50
+ signal = amplitude * np.sin(2 * np.pi * f * t)
51
+
52
+ if window_function == 'flattop':
53
+ window = flattop(N)
54
+ elif window_function == 'boxcar':
55
+ window = boxcar(N)
56
+ elif window_function == 'hamming':
57
+ window = hamming(N)
58
+ else:
59
+ raise ValueError("Unknown window function")
60
+
61
+ # Compute the Amplitude Correction Factor (ACF). This factor is used to correct the
62
+ # FFT results so that the amplitudes are correctly plotted. The ACF for a boxcar is 1.0
63
+ mean_w = np.mean(window)
64
+ ACF = 1 / mean_w
65
+
66
+ # Compute the Energy Correction Factor (ECF). This factor is used to ensure accurate
67
+ # energy calculations when integrating across the frequency domain. In the time domain,
68
+ # the RMS (Root Mean Square) value of a signal can be calculated directly using the formula:
69
+ # RMS = sqrt(mean(signal**2)). However, in the frequency domain, to accurately calculate
70
+ # the RMS energy over a range of frequencies, RMS = sqrt(sum(signal**2))*ECF
71
+ rms_w = np.sqrt(np.mean(window**2))
72
+ ECF = 1 / rms_w
73
+
74
+ # Print the window function, correction factors, frequencies, and bin size
75
+ print(f"Window function: {window_function}")
76
+ print(f"Amplitude Correction Factor (ACF): {ACF:.4f} ({20 * np.log10(ACF):.2f} dB)")
77
+ print(f"Energy Correction Factor (ECF): {ECF:.4f} ({20 * np.log10(ECF):.2f} dB)")
78
+ print(f"f_target: {f_target} Hz")
79
+ print(f"f (adjusted): {f} Hz")
80
+ print(f"Bin size: {bin_resolution:.2f} Hz")
81
+ print(f"Frequency adjustment strategy: {f_adjust_strategy}")
82
+
83
+ # Apply the selected window
84
+ windowed_signal = signal * window
85
+
86
+ # Perform the FFT and normalize. Normalizing means that, regardless of the FFT size,
87
+ # we'll still arrive at the same amplitude for a given time-domain signal Note the rfft
88
+ # assumes real input data, and computes only the positive freq components
89
+ fft_result = rfft(windowed_signal)
90
+ fft_normalized = (np.abs(fft_result) / (N / 2)) / np.sqrt(2)
91
+
92
+ # Apply the ACF to the entire FFT magnitude. With this step, a 0 dBV sine will have
93
+ # a peak of 0 dBV
94
+ fft_magnitude_acf = fft_normalized * ACF
95
+ fft_magnitude_acf_dBV = 20 * np.log10(fft_magnitude_acf) # Convert to dBV
96
+
97
+ # Compute the frequencies corresponding to the positive half of the FFT
98
+ frequencies = rfftfreq(N, d=1/fs)
99
+
100
+ # Find the peak in the corrected FFT magnitude
101
+ peak_index = np.argmax(fft_magnitude_acf) # Consider only the positive frequencies
102
+ plot_peak_frequency = peak_index * bin_resolution
103
+ peak_amplitude = fft_magnitude_acf[peak_index]
104
+ peak_amplitude_dBV = 20 * np.log10(peak_amplitude)
105
+
106
+ # Compute the RMS energy of the entire spectrum (excluding DC and Nyquist bins)
107
+ rms_energy_entire_spectrum = np.sqrt(np.sum(fft_normalized[1:]**2)) * ECF
108
+ rms_energy_entire_spectrum_dBV = 20 * np.log10(rms_energy_entire_spectrum)
109
+
110
+ # Compute the RMS energy from start frequency to stop frequency
111
+ start_freq = 900
112
+ stop_freq = 1100
113
+
114
+ # Find the indices corresponding to start and stop frequencies
115
+ start_index = np.searchsorted(frequencies, start_freq, side='left')
116
+ stop_index = np.searchsorted(frequencies, stop_freq, side='right')
117
+
118
+ # Compute the RMS energy within the specified frequency range
119
+ rms_energy_range = np.sqrt(np.sum(fft_normalized[start_index:stop_index]**2)) * ECF
120
+ rms_energy_range_dBV = 20 * np.log10(rms_energy_range)
121
+
122
+ # Print frequency domain peak values
123
+ print(f"Plot Peak Frequency: {plot_peak_frequency:.2f} Hz")
124
+ print(f"Plot Peak Amplitude: {peak_amplitude:.4f} ({peak_amplitude_dBV:.2f} dBV)")
125
+ print(f"RMS Energy of the Entire Spectrum (excluding DC and Nyquist): {rms_energy_entire_spectrum:.4f} ({rms_energy_entire_spectrum_dBV:.2f} dBV)")
126
+ print(f"RMS Energy from {start_freq} Hz to {stop_freq} Hz: {rms_energy_range:.4f} ({rms_energy_range_dBV:.2f} dBV)")
127
+
128
+ # Plot the window function, signal, and its FFT
129
+ plt.figure(figsize=(12, 6))
130
+
131
+ # Plot the window function
132
+ plt.subplot(2, 2, 1)
133
+ plt.plot(t, window, label='Window Function')
134
+ plt.xlabel('Time [s]')
135
+ plt.ylabel('Amplitude')
136
+ plt.title('Window Function')
137
+ plt.legend()
138
+ plt.grid()
139
+
140
+ # Plot the time domain signal
141
+ plt.subplot(2, 2, 2)
142
+ plt.plot(t, signal, label='Original Signal')
143
+ plt.plot(t, windowed_signal, label='Windowed Signal')
144
+ plt.xlabel('Time [s]')
145
+ plt.ylabel('Amplitude')
146
+ plt.title('Time Domain Signal')
147
+ plt.legend()
148
+ plt.grid()
149
+
150
+ # Plot the FFT magnitude
151
+ plt.subplot(2, 2, 3)
152
+ ax3 = plt.gca()
153
+ ax3.plot(frequencies, fft_magnitude_acf, label='Magnitude', color='b')
154
+ ax3.set_xlabel('Frequency [Hz]')
155
+ ax3.set_ylabel('Magnitude')
156
+ ax3.set_title('Frequency Domain (FFT)')
157
+ ax3.grid()
158
+
159
+ # Add a secondary y-axis to show dBV
160
+ ax3_right = ax3.twinx()
161
+ ax3_right.plot(frequencies, fft_magnitude_acf_dBV, color='r', alpha=0.5, linestyle='--', label='Magnitude (dBV)')
162
+ ax3_right.set_ylabel('Magnitude (dBV)')
163
+ ax3_right.grid(False)
164
+
165
+ # Combine legends
166
+ lines, labels = ax3.get_legend_handles_labels()
167
+ lines2, labels2 = ax3_right.get_legend_handles_labels()
168
+ ax3_right.legend(lines + lines2, labels + labels2, loc='upper right')
169
+
170
+ # Plot a zoomed-in view of +/- 10 bins from the center frequency
171
+ plt.subplot(2, 2, 4)
172
+ ax4 = plt.gca()
173
+ zoom_start = max(0, peak_index - 10)
174
+ zoom_end = min(len(frequencies), peak_index + 11)
175
+ ax4.plot(frequencies[zoom_start:zoom_end], fft_magnitude_acf[zoom_start:zoom_end], label='Magnitude', color='b')
176
+ ax4.set_xlabel('Frequency [Hz]')
177
+ ax4.set_ylabel('Magnitude')
178
+ ax4.set_title(f'Zoomed-in View (+/- 10 bins) around {plot_peak_frequency:.2f} Hz')
179
+ ax4.grid()
180
+
181
+ # Add a secondary y-axis to show dBV
182
+ ax4_right = ax4.twinx()
183
+ ax4_right.plot(frequencies[zoom_start:zoom_end], fft_magnitude_acf_dBV[zoom_start:zoom_end], color='r', alpha=0.5, linestyle='--', label='Magnitude (dBV)')
184
+ ax4_right.set_ylabel('Magnitude (dBV)')
185
+ ax4_right.grid(False)
186
+
187
+ # Combine legends
188
+ lines, labels = ax4.get_legend_handles_labels()
189
+ lines2, labels2 = ax4_right.get_legend_handles_labels()
190
+ ax4_right.legend(lines + lines2, labels + labels2, loc='upper right')
191
+
192
+ plt.tight_layout()
193
+ plt.show()
@@ -0,0 +1,131 @@
1
+ import os
2
+ import sys
3
+
4
+ import numpy as np
5
+ import matplotlib.pyplot as plt
6
+
7
+ sys.path.insert(0, os.path.abspath(os.path.join(sys.path[0], '..', 'src')))
8
+
9
+ from PyQa40x.analyzer import Analyzer
10
+ from PyQa40x.wave_chirp import WaveChirp
11
+ from PyQa40x.series_plotter import SeriesPlotter
12
+ from PyQa40x.math_general import *
13
+ import PyQa40x.math_chirp as mc
14
+
15
+ # Python code used to sweep the frequency response of a speaker.
16
+
17
+ #
18
+ # User Options
19
+ #
20
+
21
+ # Set sample rate
22
+ sample_rate = 48000
23
+
24
+ # If we enable this with the QA40x in loopback, then the dBSPL will be reported as
25
+ # as db_spl_at_0dbv. Bluetooth will not be used if doing a loopback test
26
+ test_loopback = False
27
+
28
+ # Set to true to see a list of available bluetooth devices. This will list the
29
+ # devices for the sample rate specified above and then exit
30
+ bluetooth_list_devices = False
31
+
32
+ # Specify the name of your bluetooth device here. The chirp will be mirrored to that
33
+ # device.
34
+ bluetooth_device = "SineAudio DS6301+"
35
+ bluetooth_device = "I13"
36
+ #bluetooth_device = ""
37
+
38
+ # if true, then a tight IR window will be applied. Otherwise, a looser window to take
39
+ # room characteristics into account
40
+ is_headphones = True
41
+
42
+ # Define chirp amplitude to be used in dBV. This will be used if
43
+ # bluetooth isn't being used OR if test_loopback is specified
44
+ chirp_amp_dbv = 0
45
+
46
+ # Define chirp amplitude to used in dBFS. This will be used if
47
+ # blueotooth is used
48
+ chirp_amp_dbfs = -5
49
+
50
+ # Mute left or right as desired
51
+ mute_left_dac = False
52
+ mute_right_dac = False
53
+
54
+ # Setup external gains and sensitivities
55
+ mic_ref_level_dbspl = 94 # Mic sense reference level
56
+ mic_sense_dbv_at_ref_level = -28.9 # Earthworks M23R
57
+ preamp_gain = 0 # QA472
58
+
59
+ # Define a quantity for this combination of mic and pre-amp: What is the dBSPL
60
+ # measured when 0 dBV is seen at inputs
61
+ db_spl_at_0dbv = mic_ref_level_dbspl - mic_sense_dbv_at_ref_level - preamp_gain
62
+
63
+ #
64
+ # Make measurement
65
+ #
66
+
67
+ # if loopback test, override any bluetooth devices
68
+ if (test_loopback):
69
+ bluetooth_device = ""
70
+
71
+ if bluetooth_list_devices:
72
+ Analyzer.list_audio_devices_by_sample_rate(sample_rate)
73
+ sys.exit()
74
+
75
+ # Create an Analyzer instance
76
+ analyzer = Analyzer()
77
+
78
+ # Initialize the analyzer with desired parameters
79
+ params = analyzer.init(sample_rate=sample_rate, max_input_level=6, max_output_level=18, fft_size=2**17, bt_device_name=bluetooth_device, pre_buf=16384)
80
+
81
+ # Dump key parameters
82
+ print(params)
83
+
84
+ # Create a chirp instance. Buffers are all zero'd (no sound)
85
+ wave_dac_left = WaveChirp(params)
86
+ wave_dac_right = WaveChirp(params)
87
+
88
+ if test_loopback or bluetooth_device == "":
89
+ print(f"db_spl_at_0dbv: {db_spl_at_0dbv:0.1f}")
90
+ if mute_left_dac is False:
91
+ wave_dac_left = wave_dac_left.gen_chirp_dbv(chirp_amp_dbv)
92
+ if mute_right_dac is False:
93
+ wave_dac_right = wave_dac_right.gen_chirp_dbv(chirp_amp_dbv)
94
+ else:
95
+ if mute_left_dac is False:
96
+ wave_dac_left = wave_dac_left.gen_chirp_dbfs(chirp_amp_dbfs)
97
+ if mute_right_dac is False:
98
+ wave_dac_right = wave_dac_right.gen_chirp_dbfs(chirp_amp_dbfs)
99
+
100
+ # Send chirp to left channel.
101
+ wave_adc_left, wave_adc_right = analyzer.send_receive(wave_dac_left, wave_dac_right)
102
+
103
+ if is_headphones:
104
+ window_start_time = 0.003 # Seconds before IR peak
105
+ window_end_time = 0.02 # Seconds after IR peak
106
+ ramp_up_time = 0.001 # attack ramp period
107
+ ramp_down_time = 0.005 # decay ramp period
108
+ else:
109
+ window_start_time = 0.010
110
+ window_end_time = 0.5
111
+ ramp_up_time = 0.005
112
+ ramp_down_time = 0.02
113
+
114
+ freq, fft_left, ir_left, window = wave_dac_left.compute_fft_db(wave_adc_left.get_main_buffer(), apply_window=True, window_start_time=window_start_time, window_end_time=window_end_time, ramp_up_time=ramp_up_time, ramp_down_time=ramp_down_time)
115
+ freq, fft_right, ir_left, window = wave_dac_right.compute_fft_db(wave_adc_right.get_main_buffer(), apply_window=True, window_start_time=window_start_time, window_end_time=window_end_time, ramp_up_time=ramp_up_time, ramp_down_time=ramp_down_time)
116
+
117
+ tsp = SeriesPlotter(num_columns=2)
118
+ tsp.add_time_series(wave_dac_left.get_buffer(), "DAC Left")
119
+ tsp.add_time_series(wave_dac_right.get_buffer(), "DAC Right")
120
+ tsp.newrow()
121
+ tsp.add_time_series(wave_adc_left.get_buffer(), "ADC Left")
122
+ tsp.add_time_series(wave_adc_right.get_buffer(), "ADC Right")
123
+ tsp.newrow()
124
+ tsp.add_time_series(ir_left, "IR Left", signal_right = window)
125
+ tsp.add_time_series(ir_left, "IR Right", signal_right = window)
126
+ tsp.newrow
127
+ tsp.add_freq_series(freq, fft_left + db_spl_at_0dbv, "FR Left dBSPL", logx=True, units="dBSPL", ymax=120, ymin=40)
128
+ tsp.add_freq_series(freq, fft_right + db_spl_at_0dbv, "FR Right dBSPL", logx=True, units = "dBSPL", ymax=120, ymin=40)
129
+ tsp.plot();
130
+
131
+ analyzer.cleanup()
@@ -0,0 +1,49 @@
1
+ import os
2
+ import sys
3
+ import numpy as np
4
+ import matplotlib.pyplot as plt
5
+
6
+ sys.path.insert(0, os.path.abspath(os.path.join(sys.path[0], '..', 'src')))
7
+
8
+ from PyQa40x.analyzer import Analyzer
9
+ from PyQa40x.wave_sine import WaveSine
10
+ from PyQa40x.series_plotter import SeriesPlotter
11
+ from PyQa40x.helpers import linear_to_dBV
12
+
13
+ # Setup mic constants below, and then using a mic calibrator, verify the plotted fft peak
14
+ # matches the expected amplitude. For the test below, an M23R was used with a QA472 at 0 dB
15
+ # of gain. A 94 dBSPL mic calibrator was used. The 1 kHz peak in the FFT measured 94.12 dBSPL.
16
+ # Test was repeated at 114 dBSPL from the calibrator and also 10 dB from the pre-amp to
17
+ # verify the results were again expected.
18
+
19
+ mic_ref_level_dbspl = 94 # Mic sense reference level
20
+ mic_sense_dbv_at_ref_level = -28.9 # Earthworks M23R
21
+ preamp_gain = 0 # QA472
22
+
23
+ dbSpl_at_0dbv = mic_ref_level_dbspl - mic_sense_dbv_at_ref_level - preamp_gain
24
+
25
+ # Create an Analyzer instance
26
+ analyzer = Analyzer()
27
+
28
+ # Initialize the analyzer with desired parameters
29
+ params = analyzer.init(sample_rate=48000, max_input_level=0, max_output_level=8, fft_size=2**14, window_type="flattop")
30
+
31
+ # Dump key parameters
32
+ print(params)
33
+
34
+ # By default, generates silence
35
+ ws = WaveSine(params)
36
+
37
+ # Send the DAC buffers to the hardware, and collect the left ADC buffer
38
+ wave_adc, _ = analyzer.send_receive(ws, ws)
39
+
40
+ # Get amplitude array (defaults to dBV) and freq array
41
+ adc_freq = wave_adc.get_frequency_array()
42
+ adc_amp_dbv = wave_adc.get_amplitude_array()
43
+
44
+ # Plot
45
+ tsp = SeriesPlotter(num_columns=2)
46
+ tsp.add_freq_series(adc_freq, adc_amp_dbv + dbSpl_at_0dbv, "dBSPL", units="dBSPL")
47
+ tsp.plot();
48
+
49
+ analyzer.cleanup()
@@ -0,0 +1,121 @@
1
+ import os
2
+ import sys
3
+
4
+ import numpy as np
5
+ import matplotlib.pyplot as plt
6
+
7
+ sys.path.insert(0, os.path.abspath(os.path.join(sys.path[0], '..', 'src')))
8
+
9
+ from PyQa40x.analyzer import Analyzer
10
+ from PyQa40x.wave_chirp import WaveChirp
11
+ from PyQa40x.series_plotter import SeriesPlotter
12
+ from PyQa40x.math_general import *
13
+ import PyQa40x.math_chirp as mc
14
+
15
+ # Python code used to play a chirp from a speaker or bluetooth, and measure the energy decay curve.
16
+ # The QA403 output goes into a +20 dB power amp (QA462), and that goes into a speaker.
17
+ # An SM58 mic is placed in the middle of the room to capture the chirp. That chirp
18
+ # is then converted into an impulse response, and the energy of that IR is plotted.
19
+ # The time for the energy to decay 20 dB is noted on the graph
20
+
21
+ #
22
+ # User Options
23
+ #
24
+
25
+ # Set sample rate
26
+ sample_rate = 48000
27
+
28
+ # If we enable this with the QA40x in loopback, then the dBSPL will be reported as
29
+ # as db_spl_at_0dbv. Bluetooth will not be used if doing a loopback test
30
+ test_loopback = False
31
+
32
+ # Set to true to see a list of available bluetooth devices. This will list the
33
+ # devices for the sample rate specified above and then exit
34
+ bluetooth_list_devices = False
35
+
36
+ # Specify the name of your bluetooth device here. The chirp will be mirrored to that
37
+ # device.
38
+ #bluetooth_device = "SineAudio DS6301+"
39
+ bluetooth_device = ""
40
+
41
+ # Define chirp amplitude to be used in dBV. This will be used if
42
+ # bluetooth isn't being used OR if test_loopback is specified
43
+ chirp_amp_dbv = -10
44
+
45
+ # Define chirp amplitude to used in dBFS. This will be used if
46
+ # blueotooth is used
47
+ chirp_amp_dbfs = -20
48
+
49
+ # Mute left or right as desired
50
+ mute_left_dac = False
51
+ mute_right_dac = False
52
+
53
+ mic_ref_level_dbspl = 94 # Mic sense reference level dBspl
54
+ mic_sense_dbv_at_ref_level = -28.9 # Earthworks M23R
55
+ preamp_gain = 0 # QA472
56
+
57
+ dbspl_at_0dbv = mic_ref_level_dbspl - mic_sense_dbv_at_ref_level - preamp_gain
58
+
59
+ #
60
+ # Make measurement
61
+ #
62
+
63
+ # if loopback test, override any bluetooth devices
64
+ if (test_loopback):
65
+ bluetooth_device = ""
66
+
67
+ if bluetooth_list_devices == True:
68
+ Analyzer.list_audio_devices_by_sample_rate(sample_rate)
69
+ sys.exit()
70
+
71
+ # Create an Analyzer instance
72
+ analyzer = Analyzer()
73
+
74
+ # Initialize the analyzer with desired parameters
75
+ params = analyzer.init(sample_rate=sample_rate, max_input_level=6, max_output_level=18, fft_size=2**18, bt_device_name=bluetooth_device)
76
+
77
+ # Dump key parameters
78
+ print(params)
79
+
80
+ wave_chirp = WaveChirp(params)
81
+
82
+ if test_loopback or bluetooth_device == "":
83
+ print(f"db_spl_at_0dbv: {dbspl_at_0dbv:0.1f}")
84
+ if mute_left_dac is False:
85
+ wave_dac_left = wave_chirp.gen_chirp_dbv(chirp_amp_dbv)
86
+ if mute_right_dac is False:
87
+ wave_dac_right = wave_chirp.gen_chirp_dbv(chirp_amp_dbv)
88
+ else:
89
+ if mute_left_dac is False:
90
+ wave_dac_left = wave_chirp.gen_chirp_dbfs(chirp_amp_dbfs)
91
+ if mute_right_dac is False:
92
+ wave_dac_right = wave_chirp.gen_chirp_dbfs(chirp_amp_dbfs)
93
+
94
+ # Send the DAC buffers to the hardware, and collect the left ADC buffer
95
+ wave_adc_left, _ = analyzer.send_receive(wave_dac_left, wave_dac_right)
96
+
97
+ # Remove DC
98
+ wave_adc_left.remove_dc()
99
+
100
+ # Compute peak RMS every 10 mS
101
+ adc_dbspl, _ = wave_adc_left.compute_instantaneous_dbspl(dbspl_at_0dbv, 10)
102
+
103
+ # Compute frequency response, applying a window around the impulse response
104
+ req, fft, ir, window = wave_chirp.compute_fft_db(wave_adc_left.get_main_buffer(), window_start_time=0.05, window_end_time=2, ramp_up_time = 0.02, ramp_down_time=0.5, apply_window=True)
105
+
106
+ # We're looking for the 20 dB decay point around the IR
107
+ db_decay = 20
108
+
109
+ # Apply the window and compute RT
110
+ rt, edc_db, start_idx, end_idx = wave_chirp.compute_rt(ir, db_decay)
111
+
112
+ # Plot
113
+ tsp = SeriesPlotter(num_columns=2)
114
+ tsp.add_time_series(wave_adc_left.get_main_buffer(), "ADC")
115
+ tsp.add_time_series(adc_dbspl, "ADC dBSPL")
116
+ tsp.newrow()
117
+ tsp.add_time_series(ir, "Room Impulse Response & Window", signal_right = window)
118
+ tsp.add_time_series(edc_db, f"Energy Decay Curve ({db_decay}dB = {rt:.2f} sec)", ymax=10, ymin=-70)
119
+ tsp.plot();
120
+
121
+ analyzer.cleanup()