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.
- pyqa40x_i2c-0.1.0/.github/workflows/code_checker.yml +22 -0
- pyqa40x_i2c-0.1.0/.github/workflows/publish_package.yml +36 -0
- pyqa40x_i2c-0.1.0/.github/workflows/unit_tests.yml +37 -0
- pyqa40x_i2c-0.1.0/.gitignore +6 -0
- pyqa40x_i2c-0.1.0/.pylint_ignore +1 -0
- pyqa40x_i2c-0.1.0/LICENSE +21 -0
- pyqa40x_i2c-0.1.0/LibTest/FirstPrinciples.py +193 -0
- pyqa40x_i2c-0.1.0/LibTest/PyQa40x_acoustic_chirp_spl_bt.py +131 -0
- pyqa40x_i2c-0.1.0/LibTest/PyQa40x_acoustic_dbspl.py +49 -0
- pyqa40x_i2c-0.1.0/LibTest/PyQa40x_acoustic_energy_decay_bt.py +121 -0
- pyqa40x_i2c-0.1.0/LibTest/PyQa40x_coherent_averaging.py +73 -0
- pyqa40x_i2c-0.1.0/LibTest/PyQa40x_cross_correlation.py +122 -0
- pyqa40x_i2c-0.1.0/LibTest/PyQa40x_freq_response_with_mask.py +197 -0
- pyqa40x_i2c-0.1.0/LibTest/PyQa40x_pinknoise.py +168 -0
- pyqa40x_i2c-0.1.0/LibTest/PyQa40x_thdn.py +53 -0
- pyqa40x_i2c-0.1.0/LibTest/mask_freq.txt +10 -0
- pyqa40x_i2c-0.1.0/LibTest/mask_phase.txt +10 -0
- pyqa40x_i2c-0.1.0/MANIFEST.in +2 -0
- pyqa40x_i2c-0.1.0/PKG-INFO +110 -0
- pyqa40x_i2c-0.1.0/README.md +63 -0
- pyqa40x_i2c-0.1.0/RELEASE_NOTES.md +3 -0
- pyqa40x_i2c-0.1.0/_ci/is_dev_version.sh +11 -0
- pyqa40x_i2c-0.1.0/pyproject.toml +27 -0
- pyqa40x_i2c-0.1.0/requirements-dev.txt +1 -0
- pyqa40x_i2c-0.1.0/requirements-test.txt +4 -0
- pyqa40x_i2c-0.1.0/requirements.txt +4 -0
- pyqa40x_i2c-0.1.0/setup.cfg +4 -0
- pyqa40x_i2c-0.1.0/src/PyQa40x/__init__.py +18 -0
- pyqa40x_i2c-0.1.0/src/PyQa40x/_version.py +24 -0
- pyqa40x_i2c-0.1.0/src/PyQa40x/analyzer.py +322 -0
- pyqa40x_i2c-0.1.0/src/PyQa40x/analyzer_params.py +66 -0
- pyqa40x_i2c-0.1.0/src/PyQa40x/bluetooth.py +134 -0
- pyqa40x_i2c-0.1.0/src/PyQa40x/control.py +137 -0
- pyqa40x_i2c-0.1.0/src/PyQa40x/cross_correlation.py +80 -0
- pyqa40x_i2c-0.1.0/src/PyQa40x/fft_processor.py +93 -0
- pyqa40x_i2c-0.1.0/src/PyQa40x/freq_series_plotter.py +81 -0
- pyqa40x_i2c-0.1.0/src/PyQa40x/helpers.py +163 -0
- pyqa40x_i2c-0.1.0/src/PyQa40x/math_chirp.py +122 -0
- pyqa40x_i2c-0.1.0/src/PyQa40x/math_energy.py +36 -0
- pyqa40x_i2c-0.1.0/src/PyQa40x/math_general.py +28 -0
- pyqa40x_i2c-0.1.0/src/PyQa40x/math_sines.py +126 -0
- pyqa40x_i2c-0.1.0/src/PyQa40x/math_windows.py +64 -0
- pyqa40x_i2c-0.1.0/src/PyQa40x/registers.py +43 -0
- pyqa40x_i2c-0.1.0/src/PyQa40x/series_plotter.py +191 -0
- pyqa40x_i2c-0.1.0/src/PyQa40x/sig_proc.py +62 -0
- pyqa40x_i2c-0.1.0/src/PyQa40x/stream.py +180 -0
- pyqa40x_i2c-0.1.0/src/PyQa40x/wave.py +277 -0
- pyqa40x_i2c-0.1.0/src/PyQa40x/wave_chirp.py +175 -0
- pyqa40x_i2c-0.1.0/src/PyQa40x/wave_sine.py +85 -0
- pyqa40x_i2c-0.1.0/src/PyQa40x.egg-info/PKG-INFO +20 -0
- pyqa40x_i2c-0.1.0/src/PyQa40x.egg-info/SOURCES.txt +28 -0
- pyqa40x_i2c-0.1.0/src/PyQa40x.egg-info/dependency_links.txt +1 -0
- pyqa40x_i2c-0.1.0/src/PyQa40x.egg-info/requires.txt +4 -0
- pyqa40x_i2c-0.1.0/src/PyQa40x.egg-info/top_level.txt +1 -0
- pyqa40x_i2c-0.1.0/src/PyQa40x_i2c.egg-info/PKG-INFO +110 -0
- pyqa40x_i2c-0.1.0/src/PyQa40x_i2c.egg-info/SOURCES.txt +60 -0
- pyqa40x_i2c-0.1.0/src/PyQa40x_i2c.egg-info/dependency_links.txt +1 -0
- pyqa40x_i2c-0.1.0/src/PyQa40x_i2c.egg-info/requires.txt +17 -0
- pyqa40x_i2c-0.1.0/src/PyQa40x_i2c.egg-info/top_level.txt +1 -0
- pyqa40x_i2c-0.1.0/tests/__init__.py +0 -0
- pyqa40x_i2c-0.1.0/tests/test_template.py +10 -0
- 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 @@
|
|
|
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()
|