PyQa40x-i2c 0.2.0__tar.gz → 0.3.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.2.0 → pyqa40x_i2c-0.3.0}/PKG-INFO +1 -1
- pyqa40x_i2c-0.3.0/RELEASE_NOTES.md +27 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/_version.py +3 -3
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/analyzer.py +187 -11
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/control.py +1 -1
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/stream.py +24 -3
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/wave.py +1 -1
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x_i2c.egg-info/PKG-INFO +1 -1
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x_i2c.egg-info/scm_file_list.json +43 -43
- pyqa40x_i2c-0.3.0/src/PyQa40x_i2c.egg-info/scm_version.json +8 -0
- pyqa40x_i2c-0.2.0/RELEASE_NOTES.md +0 -15
- pyqa40x_i2c-0.2.0/src/PyQa40x_i2c.egg-info/scm_version.json +0 -8
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/.github/workflows/code_checker.yml +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/.github/workflows/publish_package.yml +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/.github/workflows/unit_tests.yml +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/.gitignore +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/.pylint_ignore +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/LICENSE +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/LibTest/FirstPrinciples.py +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/LibTest/PyQa40x_acoustic_chirp_spl_bt.py +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/LibTest/PyQa40x_acoustic_dbspl.py +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/LibTest/PyQa40x_acoustic_energy_decay_bt.py +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/LibTest/PyQa40x_coherent_averaging.py +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/LibTest/PyQa40x_cross_correlation.py +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/LibTest/PyQa40x_freq_response_with_mask.py +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/LibTest/PyQa40x_pinknoise.py +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/LibTest/PyQa40x_thdn.py +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/LibTest/mask_freq.txt +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/LibTest/mask_phase.txt +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/MANIFEST.in +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/README.md +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/_ci/is_dev_version.sh +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/pyproject.toml +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/requirements-dev.txt +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/requirements-test.txt +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/requirements.txt +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/setup.cfg +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/__init__.py +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/analyzer_params.py +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/bluetooth.py +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/cross_correlation.py +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/fft_processor.py +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/freq_series_plotter.py +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/helpers.py +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/math_chirp.py +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/math_energy.py +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/math_general.py +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/math_sines.py +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/math_windows.py +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/registers.py +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/series_plotter.py +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/sig_proc.py +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/wave_chirp.py +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/wave_sine.py +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x.egg-info/PKG-INFO +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x.egg-info/SOURCES.txt +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x.egg-info/dependency_links.txt +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x.egg-info/requires.txt +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x.egg-info/top_level.txt +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x_i2c.egg-info/SOURCES.txt +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x_i2c.egg-info/dependency_links.txt +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x_i2c.egg-info/requires.txt +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x_i2c.egg-info/top_level.txt +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/tests/__init__.py +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/tests/test_template.py +0 -0
- {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/tox.ini +0 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
### 0.3.0
|
|
2
|
+
|
|
3
|
+
[MINOR] `Analyzer.init()` accepts `usb_serial` to open a specific QA402/QA403 by its USB serial number, so a PC with several analyzers can drive a chosen one. The argument is last and optional; without it the first analyzer found is opened, exactly as before.
|
|
4
|
+
|
|
5
|
+
### 0.2.2
|
|
6
|
+
|
|
7
|
+
[PATCH] Allow reliable streaming for higher sample rates (`96kHz`, `192kHz`).
|
|
8
|
+
|
|
9
|
+
### 0.2.1
|
|
10
|
+
|
|
11
|
+
[PATCH] Restore removed `Analyzer.send_receive` functionality.
|
|
12
|
+
|
|
13
|
+
### 0.2.0
|
|
14
|
+
|
|
15
|
+
[MINOR] Add endless audio capturing
|
|
16
|
+
|
|
17
|
+
### 0.1.2
|
|
18
|
+
|
|
19
|
+
[PATCH] Fix usb teardown deadlock.
|
|
20
|
+
|
|
21
|
+
### 0.1.1
|
|
22
|
+
|
|
23
|
+
[PATCH] Fix missing dependency.
|
|
24
|
+
|
|
25
|
+
### 0.1.0
|
|
26
|
+
|
|
27
|
+
[MINOR] Initial release as PyQa40x-i2c fork. Unlimited audio samples capture added.
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.
|
|
22
|
-
__version_tuple__ = version_tuple = (0,
|
|
21
|
+
__version__ = version = '0.3.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 3, 0)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'geab2d8779'
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import threading
|
|
2
2
|
import logging
|
|
3
|
+
import struct
|
|
3
4
|
|
|
4
5
|
import numpy as np
|
|
5
6
|
import usb1 # pip install libusb1
|
|
@@ -11,13 +12,23 @@ from PyQa40x.stream import Stream
|
|
|
11
12
|
from PyQa40x.helpers import *
|
|
12
13
|
from PyQa40x.analyzer_params import AnalyzerParams
|
|
13
14
|
from PyQa40x.bluetooth import BluetoothAudioDevice
|
|
15
|
+
from PyQa40x.wave_sine import Wave
|
|
14
16
|
|
|
15
17
|
logger = logging.getLogger(__name__)
|
|
16
18
|
|
|
19
|
+
# Sample rate Hz vs bytes per USB transfer
|
|
20
|
+
TRANSFER_BYTES_MAP = {
|
|
21
|
+
44100: Stream.DEFAULT_TRANSFER_BYTES,
|
|
22
|
+
48000: Stream.DEFAULT_TRANSFER_BYTES,
|
|
23
|
+
96000: 32768,
|
|
24
|
+
192000: 65536,
|
|
25
|
+
}
|
|
26
|
+
|
|
17
27
|
|
|
18
28
|
class Analyzer:
|
|
19
29
|
|
|
20
|
-
|
|
30
|
+
VENDOR_ID = 0x16C0
|
|
31
|
+
PRODUCT_IDS = (0x4E37, 0x4E39) # QA402, QA403
|
|
21
32
|
|
|
22
33
|
def __init__(self):
|
|
23
34
|
"""
|
|
@@ -25,6 +36,7 @@ class Analyzer:
|
|
|
25
36
|
"""
|
|
26
37
|
self.params: AnalyzerParams | None = None
|
|
27
38
|
self.context: usb1.USBContext | None = None
|
|
39
|
+
self.chunk_size: int | None = None
|
|
28
40
|
self.device: usb1.USBDeviceHandle | None = None
|
|
29
41
|
self.registers: Registers | None = None
|
|
30
42
|
self.control: Control | None = None
|
|
@@ -36,6 +48,55 @@ class Analyzer:
|
|
|
36
48
|
self._dac_writer: threading.Thread | None = None # set in start_capture()
|
|
37
49
|
self._dac_writer_error: Exception | None = None # error from DAC writer thread
|
|
38
50
|
|
|
51
|
+
def _open_analyzer(self, usb_serial: str | None = None):
|
|
52
|
+
"""
|
|
53
|
+
Opens a QA402/QA403 analyzer on the USB bus.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
usb_serial (str | None): USB serial number of the analyzer to open.
|
|
57
|
+
None opens the first analyzer found, which is the behaviour of
|
|
58
|
+
every release before serial selection was added.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
usb1.USBDeviceHandle: Handle of the opened analyzer.
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
SystemExit: If no analyzer is connected, or if none of the connected
|
|
65
|
+
analyzers has the requested serial number.
|
|
66
|
+
"""
|
|
67
|
+
if usb_serial is None:
|
|
68
|
+
for product_id in self.PRODUCT_IDS:
|
|
69
|
+
device = self.context.openByVendorIDAndProductID(
|
|
70
|
+
self.VENDOR_ID, product_id
|
|
71
|
+
)
|
|
72
|
+
if device is not None:
|
|
73
|
+
return device
|
|
74
|
+
raise SystemExit("No QA402/QA403 analyzer found")
|
|
75
|
+
|
|
76
|
+
connected: list[str] = []
|
|
77
|
+
for device in self.context.getDeviceList(skip_on_error=True):
|
|
78
|
+
if (
|
|
79
|
+
device.getVendorID() != self.VENDOR_ID
|
|
80
|
+
or device.getProductID() not in self.PRODUCT_IDS
|
|
81
|
+
):
|
|
82
|
+
continue
|
|
83
|
+
try:
|
|
84
|
+
serial = device.getSerialNumber()
|
|
85
|
+
except usb1.USBError as error:
|
|
86
|
+
# Unreadable serial (usually missing udev permissions): the
|
|
87
|
+
# device cannot be matched, so report it as unknown.
|
|
88
|
+
logging.warning("Cannot read serial of a QA40x analyzer: %s", error)
|
|
89
|
+
serial = None
|
|
90
|
+
if serial == usb_serial:
|
|
91
|
+
logging.info("Opening QA40x analyzer with serial %s", serial)
|
|
92
|
+
return device.open()
|
|
93
|
+
connected.append(repr(serial))
|
|
94
|
+
|
|
95
|
+
raise SystemExit(
|
|
96
|
+
f"No QA402/QA403 analyzer with serial {usb_serial!r} found "
|
|
97
|
+
f"(connected: {', '.join(connected) or 'none'})"
|
|
98
|
+
)
|
|
99
|
+
|
|
39
100
|
def init(
|
|
40
101
|
self,
|
|
41
102
|
sample_rate: int = 48000,
|
|
@@ -48,6 +109,7 @@ class Analyzer:
|
|
|
48
109
|
amplitude_unit: str = "dbv",
|
|
49
110
|
distortion_unit: str = "db",
|
|
50
111
|
bt_device_name: str = "",
|
|
112
|
+
usb_serial: str | None = None,
|
|
51
113
|
) -> AnalyzerParams:
|
|
52
114
|
"""
|
|
53
115
|
Initializes the analyzer hardware with the specified parameters.
|
|
@@ -63,6 +125,9 @@ class Analyzer:
|
|
|
63
125
|
amplitude_unit (str): Unit for amplitude measurements, default is "dbv".
|
|
64
126
|
distortion_unit (str): Unit for distortion measurements, default is "db".
|
|
65
127
|
bt_device_name (str): Name of the Bluetooth device to use. Default is empty.
|
|
128
|
+
usb_serial (str | None): USB serial number of the analyzer to open,
|
|
129
|
+
e.g. "4334_E3B6". Needed only when several analyzers are
|
|
130
|
+
connected. Default None opens the first QA402/QA403 found.
|
|
66
131
|
|
|
67
132
|
Returns:
|
|
68
133
|
AnalyzerParams: A class instance containing the hardware and parameter settings.
|
|
@@ -78,21 +143,18 @@ class Analyzer:
|
|
|
78
143
|
)
|
|
79
144
|
self.context = usb1.USBContext()
|
|
80
145
|
self.context.open()
|
|
146
|
+
self.chunk_size = TRANSFER_BYTES_MAP.get(
|
|
147
|
+
sample_rate, Stream.DEFAULT_TRANSFER_BYTES
|
|
148
|
+
)
|
|
81
149
|
|
|
82
|
-
# Attempt to open QA402 or
|
|
83
|
-
self.device = self.
|
|
84
|
-
if self.device is None:
|
|
85
|
-
self.device = self.context.openByVendorIDAndProductID(
|
|
86
|
-
0x16C0, 0x4E39
|
|
87
|
-
) # QA403
|
|
88
|
-
if self.device is None:
|
|
89
|
-
raise SystemExit("No QA402/QA403 analyzer found")
|
|
150
|
+
# Attempt to open the requested QA402/QA403 device, or the first one
|
|
151
|
+
self.device = self._open_analyzer(usb_serial)
|
|
90
152
|
self.device.resetDevice()
|
|
91
153
|
self.device.claimInterface(0)
|
|
92
154
|
|
|
93
155
|
self.registers = Registers(self.device)
|
|
94
156
|
self.control = Control(self.registers)
|
|
95
|
-
self.stream = Stream(self.context, self.device, self.registers)
|
|
157
|
+
self.stream = Stream(self.context, self.device, self.registers, self.chunk_size)
|
|
96
158
|
|
|
97
159
|
# Load calibration data
|
|
98
160
|
self.cal_data = self.control.load_calibration()
|
|
@@ -212,7 +274,7 @@ class Analyzer:
|
|
|
212
274
|
"""Continuously submit zero-DAC / ADC-read transfer pairs while capturing."""
|
|
213
275
|
while self._capturing and self.stream.running:
|
|
214
276
|
try:
|
|
215
|
-
self.stream.write_zeros(self.
|
|
277
|
+
self.stream.write_zeros(self.chunk_size)
|
|
216
278
|
except Exception as exc: # pylint: disable=broad-except
|
|
217
279
|
# Racing with stop() is expected; a real failure while we still
|
|
218
280
|
# expect to run is recorded in the post-loop reconciliation below.
|
|
@@ -277,6 +339,120 @@ class Analyzer:
|
|
|
277
339
|
if self._dac_writer is not None:
|
|
278
340
|
self._dac_writer.join(timeout=5.0)
|
|
279
341
|
|
|
342
|
+
def send_receive(self, left_wave: Wave, right_wave: Wave) -> tuple[Wave, Wave]:
|
|
343
|
+
"""
|
|
344
|
+
Sends DAC data to the device and receives ADC data.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
left_wave (Wave): Wave instance with left channel DAC data.
|
|
348
|
+
right_wave (Wave): Wave instance with right channel DAC data.
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
tuple[Wave, Wave]: Tuple containing Wave instances with left and right channel ADC data.
|
|
352
|
+
"""
|
|
353
|
+
left_dac_data = left_wave.get_buffer()
|
|
354
|
+
right_dac_data = right_wave.get_buffer()
|
|
355
|
+
|
|
356
|
+
# On QA402 and QA403, outputs need to be swapped
|
|
357
|
+
left_dac_data, right_dac_data = right_dac_data, left_dac_data
|
|
358
|
+
|
|
359
|
+
left_peak = np.max(left_dac_data)
|
|
360
|
+
|
|
361
|
+
# Get calibration factors for outgoing data
|
|
362
|
+
cal_dac_left, cal_dac_right = self.control.get_dac_cal(
|
|
363
|
+
self.cal_data, self.params.max_output_level
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
# Convert to dBFS. Note the 3 dB adjustment--dBFS is peak, while dBV is RMS
|
|
367
|
+
dac_dbfs_adjustment = 10 ** -((self.params.max_output_level + 3) / 20)
|
|
368
|
+
|
|
369
|
+
# Apply calibration and dBFS scaling
|
|
370
|
+
left_dac_data = left_dac_data * dac_dbfs_adjustment * cal_dac_left
|
|
371
|
+
right_dac_data = right_dac_data * dac_dbfs_adjustment * cal_dac_right
|
|
372
|
+
|
|
373
|
+
# Convert incoming doubles to float
|
|
374
|
+
left_dac_data_float = left_dac_data.astype(np.float32)
|
|
375
|
+
right_dac_data_float = right_dac_data.astype(np.float32)
|
|
376
|
+
|
|
377
|
+
# Interleave the left and right channels
|
|
378
|
+
interleaved_dac_data = np.empty(
|
|
379
|
+
(left_dac_data_float.size + right_dac_data_float.size,), dtype=np.float32
|
|
380
|
+
)
|
|
381
|
+
interleaved_dac_data[0::2] = left_dac_data_float
|
|
382
|
+
interleaved_dac_data[1::2] = right_dac_data_float
|
|
383
|
+
|
|
384
|
+
# Convert to bytes, multiplying by max int value
|
|
385
|
+
max_int_value = 2**31 - 1
|
|
386
|
+
interleaved_dac_data = (interleaved_dac_data * max_int_value).astype(np.int32)
|
|
387
|
+
|
|
388
|
+
num_ints_per_chunk = self.chunk_size // 4 # 32-bit ints, so 4 bytes per int
|
|
389
|
+
total_chunks = len(interleaved_dac_data) // num_ints_per_chunk
|
|
390
|
+
|
|
391
|
+
self.stream.start()
|
|
392
|
+
|
|
393
|
+
# Play wave on Bluetooth if available
|
|
394
|
+
bt_thread = None
|
|
395
|
+
if self.bt_device:
|
|
396
|
+
# Play both left and right waves on Bluetooth
|
|
397
|
+
bt_thread = self.bt_device.play_wave_background(left_wave, right_wave)
|
|
398
|
+
|
|
399
|
+
try:
|
|
400
|
+
for i in range(total_chunks):
|
|
401
|
+
chunk = interleaved_dac_data[
|
|
402
|
+
i * num_ints_per_chunk : (i + 1) * num_ints_per_chunk
|
|
403
|
+
]
|
|
404
|
+
buffer = struct.pack("<%di" % len(chunk), *chunk)
|
|
405
|
+
self.stream.write(buffer)
|
|
406
|
+
finally:
|
|
407
|
+
self.stream.stop()
|
|
408
|
+
|
|
409
|
+
# Collect ADC data. This is bytes
|
|
410
|
+
interleaved_adc_data = self.stream.collect_remaining_adc_data()
|
|
411
|
+
|
|
412
|
+
# Wait for the Bluetooth thread to finish after processing
|
|
413
|
+
if bt_thread:
|
|
414
|
+
bt_thread.join()
|
|
415
|
+
|
|
416
|
+
# Convert collected ADC data back to int
|
|
417
|
+
interleaved_adc_data = np.frombuffer(interleaved_adc_data, dtype=np.int32)
|
|
418
|
+
|
|
419
|
+
# Separate interleaved ADC data into left and right channels. This is int
|
|
420
|
+
left_adc_data_int = interleaved_adc_data[0::2]
|
|
421
|
+
right_adc_data_int = interleaved_adc_data[1::2]
|
|
422
|
+
|
|
423
|
+
# Convert left and right channels back to double
|
|
424
|
+
left_adc_data = left_adc_data_int.astype(np.float64) / max_int_value
|
|
425
|
+
right_adc_data = right_adc_data_int.astype(np.float64) / max_int_value
|
|
426
|
+
|
|
427
|
+
# Get calibration factors for incoming data
|
|
428
|
+
cal_adc_left, cal_adc_right = self.control.get_adc_cal(
|
|
429
|
+
self.cal_data, self.params.max_input_level
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
# Convert from dBFS to dBV. Note the 6 dB factor--the ADC is differential
|
|
433
|
+
adc_dbfs_correction = 10 ** ((self.params.max_input_level - 6) / 20)
|
|
434
|
+
|
|
435
|
+
# Apply calibration and dBFS scaling
|
|
436
|
+
left_adc_data = left_adc_data * cal_adc_left * adc_dbfs_correction
|
|
437
|
+
right_adc_data = right_adc_data * cal_adc_right * adc_dbfs_correction
|
|
438
|
+
|
|
439
|
+
# Ensure the buffer matches the expected shape. Is this needed?
|
|
440
|
+
expected_shape = (
|
|
441
|
+
self.params.pre_buf + self.params.fft_size + self.params.post_buf,
|
|
442
|
+
)
|
|
443
|
+
if left_adc_data.shape[0] != expected_shape[0]:
|
|
444
|
+
left_adc_data = np.resize(left_adc_data, expected_shape)
|
|
445
|
+
if right_adc_data.shape[0] != expected_shape[0]:
|
|
446
|
+
right_adc_data = np.resize(right_adc_data, expected_shape)
|
|
447
|
+
|
|
448
|
+
# Create instances of Wave with the full buffers
|
|
449
|
+
left_wave = Wave(self.params)
|
|
450
|
+
right_wave = Wave(self.params)
|
|
451
|
+
left_wave.set_buffer(left_adc_data)
|
|
452
|
+
right_wave.set_buffer(right_adc_data)
|
|
453
|
+
|
|
454
|
+
return left_wave, right_wave
|
|
455
|
+
|
|
280
456
|
def capture(self, total_samples: int) -> tuple[np.ndarray, np.ndarray]:
|
|
281
457
|
"""
|
|
282
458
|
Captures ``total_samples`` samples from both ADC input channels without
|
|
@@ -46,7 +46,7 @@ class Control:
|
|
|
46
46
|
"""
|
|
47
47
|
val = self.samplerate2reg[rate]
|
|
48
48
|
self.registers.write(9, val)
|
|
49
|
-
time.sleep(0.
|
|
49
|
+
time.sleep(0.2) # A small delay to ensure the sample rate is set
|
|
50
50
|
|
|
51
51
|
def load_calibration(self):
|
|
52
52
|
"""
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import threading
|
|
2
2
|
import queue
|
|
3
|
+
import time
|
|
4
|
+
|
|
3
5
|
import usb1
|
|
4
6
|
import logging
|
|
5
7
|
from typing import Callable
|
|
@@ -27,7 +29,7 @@ class Stream:
|
|
|
27
29
|
"""
|
|
28
30
|
|
|
29
31
|
TRANSFER_TIMEOUT_MS = 1000 # Timeout for USB transfers in milliseconds
|
|
30
|
-
|
|
32
|
+
DEFAULT_TRANSFER_BYTES = 16384 # 16 kB per USB bulk transfer (hardware chunk size)
|
|
31
33
|
# cap on unread ADC data before overflow
|
|
32
34
|
MAX_BUFFERED_BYTES = 256 * 1024 * 1024
|
|
33
35
|
|
|
@@ -36,6 +38,7 @@ class Stream:
|
|
|
36
38
|
context: usb1.USBContext,
|
|
37
39
|
device: usb1.USBDeviceHandle,
|
|
38
40
|
registers: Registers,
|
|
41
|
+
transfer_bytes_size: int = DEFAULT_TRANSFER_BYTES
|
|
39
42
|
) -> None:
|
|
40
43
|
"""
|
|
41
44
|
Initialize the Stream.
|
|
@@ -44,10 +47,12 @@ class Stream:
|
|
|
44
47
|
context (usb1.USBContext): The USB context.
|
|
45
48
|
device (usb1.USBDeviceHandle): The USB device handle.
|
|
46
49
|
registers (Registers): The Registers instance for control writes.
|
|
50
|
+
transfer_bytes_size (int):
|
|
47
51
|
"""
|
|
48
52
|
self.context = context
|
|
49
53
|
self.device = device
|
|
50
54
|
self.registers = registers
|
|
55
|
+
self.transfer_bytes_size = transfer_bytes_size # USB bulk transfer (hardware chunk size)
|
|
51
56
|
self.endpoint_read = usb1.ENDPOINT_IN | 0x02 # EP2 in
|
|
52
57
|
self.endpoint_write = usb1.ENDPOINT_OUT | 0x02 # EP2 out
|
|
53
58
|
self.dacQueue = queue.Queue(
|
|
@@ -79,7 +84,7 @@ class Stream:
|
|
|
79
84
|
self._adc_data_carry = bytearray() # leftover ADC bytes between consume_adc calls
|
|
80
85
|
# latched, raised by consume_adc
|
|
81
86
|
self._overflow_error: AdcOverflowError | None = None
|
|
82
|
-
self._max_queued_chunks = self.MAX_BUFFERED_BYTES // self.
|
|
87
|
+
self._max_queued_chunks = self.MAX_BUFFERED_BYTES // self.transfer_bytes_size
|
|
83
88
|
|
|
84
89
|
@property
|
|
85
90
|
def overflow_error(self) -> "AdcOverflowError | None":
|
|
@@ -205,10 +210,26 @@ class Stream:
|
|
|
205
210
|
self.dacQueue.put(dac)
|
|
206
211
|
self.adcQueue.put(read)
|
|
207
212
|
|
|
208
|
-
def write_zeros(self, chunk_bytes: int =
|
|
213
|
+
def write_zeros(self, chunk_bytes: int = DEFAULT_TRANSFER_BYTES) -> None:
|
|
209
214
|
"""Feed the ADC pipeline with a zero DAC buffer (continuous capture)."""
|
|
210
215
|
self.write(bytes(chunk_bytes))
|
|
211
216
|
|
|
217
|
+
def collect_remaining_adc_data(self):
|
|
218
|
+
"""
|
|
219
|
+
Waits for all remaining ADC transfers to complete and returns the collected data.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
bytearray: The collected ADC data.
|
|
223
|
+
"""
|
|
224
|
+
while not self.adcQueue.empty():
|
|
225
|
+
time.sleep(0.01)
|
|
226
|
+
|
|
227
|
+
while not self._adc_data.empty():
|
|
228
|
+
chunk = self._adc_data.get()
|
|
229
|
+
self._adc_data_carry.extend(chunk)
|
|
230
|
+
|
|
231
|
+
return self._adc_data_carry
|
|
232
|
+
|
|
212
233
|
def consume_adc(
|
|
213
234
|
self,
|
|
214
235
|
need_bytes: int,
|
|
@@ -26,7 +26,7 @@ class Wave:
|
|
|
26
26
|
|
|
27
27
|
self.amplitude_unit: str = amplitude_unit # Lowercase. Use dbv or dbu
|
|
28
28
|
self.distortion_unit: str = distortion_unit # Lowercase. Use db or pct
|
|
29
|
-
self.energy_unit: str = energy_unit # Lowercase. Use dbv, dbu, V
|
|
29
|
+
self.energy_unit: str = energy_unit # Lowercase. Use dbv, dbu, V or V^2
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
def set_buffer(self, buffer: np.ndarray):
|
|
@@ -1,58 +1,58 @@
|
|
|
1
1
|
{
|
|
2
2
|
"files": [
|
|
3
|
+
".github/workflows/code_checker.yml",
|
|
4
|
+
".github/workflows/publish_package.yml",
|
|
5
|
+
".github/workflows/unit_tests.yml",
|
|
3
6
|
".gitignore",
|
|
4
|
-
"README.md",
|
|
5
7
|
".pylint_ignore",
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
8
|
+
"LICENSE",
|
|
9
|
+
"LibTest/FirstPrinciples.py",
|
|
10
|
+
"LibTest/PyQa40x_acoustic_chirp_spl_bt.py",
|
|
11
|
+
"LibTest/PyQa40x_acoustic_dbspl.py",
|
|
12
|
+
"LibTest/PyQa40x_acoustic_energy_decay_bt.py",
|
|
13
|
+
"LibTest/PyQa40x_coherent_averaging.py",
|
|
14
|
+
"LibTest/PyQa40x_cross_correlation.py",
|
|
15
|
+
"LibTest/PyQa40x_freq_response_with_mask.py",
|
|
16
|
+
"LibTest/PyQa40x_pinknoise.py",
|
|
17
|
+
"LibTest/PyQa40x_thdn.py",
|
|
18
|
+
"LibTest/mask_freq.txt",
|
|
19
|
+
"LibTest/mask_phase.txt",
|
|
11
20
|
"MANIFEST.in",
|
|
21
|
+
"README.md",
|
|
12
22
|
"RELEASE_NOTES.md",
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"src/PyQa40x/
|
|
23
|
+
"_ci/is_dev_version.sh",
|
|
24
|
+
"pyproject.toml",
|
|
25
|
+
"requirements-dev.txt",
|
|
26
|
+
"requirements-test.txt",
|
|
27
|
+
"requirements.txt",
|
|
28
|
+
"src/PyQa40x.egg-info/PKG-INFO",
|
|
29
|
+
"src/PyQa40x.egg-info/SOURCES.txt",
|
|
30
|
+
"src/PyQa40x.egg-info/dependency_links.txt",
|
|
31
|
+
"src/PyQa40x.egg-info/requires.txt",
|
|
32
|
+
"src/PyQa40x.egg-info/top_level.txt",
|
|
33
|
+
"src/PyQa40x/__init__.py",
|
|
34
|
+
"src/PyQa40x/analyzer.py",
|
|
19
35
|
"src/PyQa40x/analyzer_params.py",
|
|
20
|
-
"src/PyQa40x/
|
|
36
|
+
"src/PyQa40x/bluetooth.py",
|
|
21
37
|
"src/PyQa40x/control.py",
|
|
22
|
-
"src/PyQa40x/
|
|
23
|
-
"src/PyQa40x/
|
|
38
|
+
"src/PyQa40x/cross_correlation.py",
|
|
39
|
+
"src/PyQa40x/fft_processor.py",
|
|
40
|
+
"src/PyQa40x/freq_series_plotter.py",
|
|
24
41
|
"src/PyQa40x/helpers.py",
|
|
42
|
+
"src/PyQa40x/math_chirp.py",
|
|
43
|
+
"src/PyQa40x/math_energy.py",
|
|
44
|
+
"src/PyQa40x/math_general.py",
|
|
45
|
+
"src/PyQa40x/math_sines.py",
|
|
46
|
+
"src/PyQa40x/math_windows.py",
|
|
25
47
|
"src/PyQa40x/registers.py",
|
|
26
|
-
"src/PyQa40x/cross_correlation.py",
|
|
27
48
|
"src/PyQa40x/series_plotter.py",
|
|
28
|
-
"src/PyQa40x/
|
|
29
|
-
"src/PyQa40x/bluetooth.py",
|
|
30
|
-
"src/PyQa40x/math_energy.py",
|
|
31
|
-
"src/PyQa40x/math_chirp.py",
|
|
32
|
-
"src/PyQa40x/wave_chirp.py",
|
|
33
|
-
"src/PyQa40x/__init__.py",
|
|
49
|
+
"src/PyQa40x/sig_proc.py",
|
|
34
50
|
"src/PyQa40x/stream.py",
|
|
35
|
-
"src/PyQa40x
|
|
36
|
-
"src/PyQa40x
|
|
37
|
-
"src/PyQa40x
|
|
38
|
-
"
|
|
39
|
-
"src/PyQa40x.egg-info/PKG-INFO",
|
|
40
|
-
".github/workflows/unit_tests.yml",
|
|
41
|
-
".github/workflows/code_checker.yml",
|
|
42
|
-
".github/workflows/publish_package.yml",
|
|
43
|
-
"_ci/is_dev_version.sh",
|
|
44
|
-
"LibTest/PyQa40x_cross_correlation.py",
|
|
45
|
-
"LibTest/PyQa40x_acoustic_chirp_spl_bt.py",
|
|
46
|
-
"LibTest/PyQa40x_acoustic_dbspl.py",
|
|
47
|
-
"LibTest/PyQa40x_freq_response_with_mask.py",
|
|
48
|
-
"LibTest/PyQa40x_pinknoise.py",
|
|
49
|
-
"LibTest/PyQa40x_acoustic_energy_decay_bt.py",
|
|
50
|
-
"LibTest/mask_phase.txt",
|
|
51
|
-
"LibTest/PyQa40x_coherent_averaging.py",
|
|
52
|
-
"LibTest/PyQa40x_thdn.py",
|
|
53
|
-
"LibTest/FirstPrinciples.py",
|
|
54
|
-
"LibTest/mask_freq.txt",
|
|
51
|
+
"src/PyQa40x/wave.py",
|
|
52
|
+
"src/PyQa40x/wave_chirp.py",
|
|
53
|
+
"src/PyQa40x/wave_sine.py",
|
|
54
|
+
"tests/__init__.py",
|
|
55
55
|
"tests/test_template.py",
|
|
56
|
-
"
|
|
56
|
+
"tox.ini"
|
|
57
57
|
]
|
|
58
58
|
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
### 0.2.0
|
|
2
|
-
|
|
3
|
-
[MINOR] Add endless audio capturing
|
|
4
|
-
|
|
5
|
-
### 0.1.2
|
|
6
|
-
|
|
7
|
-
[PATCH] Fix usb teardown deadlock.
|
|
8
|
-
|
|
9
|
-
### 0.1.1
|
|
10
|
-
|
|
11
|
-
[PATCH] Fix missing dependency.
|
|
12
|
-
|
|
13
|
-
### 0.1.0
|
|
14
|
-
|
|
15
|
-
[MINOR] Initial release as PyQa40x-i2c fork. Unlimited audio samples capture added.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|