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.
Files changed (66) hide show
  1. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/PKG-INFO +1 -1
  2. pyqa40x_i2c-0.3.0/RELEASE_NOTES.md +27 -0
  3. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/_version.py +3 -3
  4. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/analyzer.py +187 -11
  5. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/control.py +1 -1
  6. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/stream.py +24 -3
  7. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/wave.py +1 -1
  8. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x_i2c.egg-info/PKG-INFO +1 -1
  9. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x_i2c.egg-info/scm_file_list.json +43 -43
  10. pyqa40x_i2c-0.3.0/src/PyQa40x_i2c.egg-info/scm_version.json +8 -0
  11. pyqa40x_i2c-0.2.0/RELEASE_NOTES.md +0 -15
  12. pyqa40x_i2c-0.2.0/src/PyQa40x_i2c.egg-info/scm_version.json +0 -8
  13. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/.github/workflows/code_checker.yml +0 -0
  14. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/.github/workflows/publish_package.yml +0 -0
  15. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/.github/workflows/unit_tests.yml +0 -0
  16. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/.gitignore +0 -0
  17. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/.pylint_ignore +0 -0
  18. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/LICENSE +0 -0
  19. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/LibTest/FirstPrinciples.py +0 -0
  20. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/LibTest/PyQa40x_acoustic_chirp_spl_bt.py +0 -0
  21. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/LibTest/PyQa40x_acoustic_dbspl.py +0 -0
  22. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/LibTest/PyQa40x_acoustic_energy_decay_bt.py +0 -0
  23. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/LibTest/PyQa40x_coherent_averaging.py +0 -0
  24. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/LibTest/PyQa40x_cross_correlation.py +0 -0
  25. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/LibTest/PyQa40x_freq_response_with_mask.py +0 -0
  26. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/LibTest/PyQa40x_pinknoise.py +0 -0
  27. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/LibTest/PyQa40x_thdn.py +0 -0
  28. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/LibTest/mask_freq.txt +0 -0
  29. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/LibTest/mask_phase.txt +0 -0
  30. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/MANIFEST.in +0 -0
  31. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/README.md +0 -0
  32. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/_ci/is_dev_version.sh +0 -0
  33. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/pyproject.toml +0 -0
  34. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/requirements-dev.txt +0 -0
  35. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/requirements-test.txt +0 -0
  36. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/requirements.txt +0 -0
  37. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/setup.cfg +0 -0
  38. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/__init__.py +0 -0
  39. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/analyzer_params.py +0 -0
  40. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/bluetooth.py +0 -0
  41. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/cross_correlation.py +0 -0
  42. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/fft_processor.py +0 -0
  43. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/freq_series_plotter.py +0 -0
  44. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/helpers.py +0 -0
  45. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/math_chirp.py +0 -0
  46. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/math_energy.py +0 -0
  47. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/math_general.py +0 -0
  48. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/math_sines.py +0 -0
  49. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/math_windows.py +0 -0
  50. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/registers.py +0 -0
  51. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/series_plotter.py +0 -0
  52. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/sig_proc.py +0 -0
  53. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/wave_chirp.py +0 -0
  54. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x/wave_sine.py +0 -0
  55. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x.egg-info/PKG-INFO +0 -0
  56. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x.egg-info/SOURCES.txt +0 -0
  57. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x.egg-info/dependency_links.txt +0 -0
  58. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x.egg-info/requires.txt +0 -0
  59. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x.egg-info/top_level.txt +0 -0
  60. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x_i2c.egg-info/SOURCES.txt +0 -0
  61. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x_i2c.egg-info/dependency_links.txt +0 -0
  62. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x_i2c.egg-info/requires.txt +0 -0
  63. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/src/PyQa40x_i2c.egg-info/top_level.txt +0 -0
  64. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/tests/__init__.py +0 -0
  65. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/tests/test_template.py +0 -0
  66. {pyqa40x_i2c-0.2.0 → pyqa40x_i2c-0.3.0}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyQa40x-i2c
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: A Python library for the QA40x audio analyser (int2code fork)
5
5
  Maintainer-email: Hubert Stepniewski <hubert.stepniewski@int2code.com>
6
6
  License: MIT License
@@ -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.2.0'
22
- __version_tuple__ = version_tuple = (0, 2, 0)
21
+ __version__ = version = '0.3.0'
22
+ __version_tuple__ = version_tuple = (0, 3, 0)
23
23
 
24
- __commit_id__ = commit_id = 'g6a5ba3357'
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
- CAPTURE_CHUNK_BYTES = Stream.TRANSFER_BYTES # bytes per USB transfer
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 QA403 device
83
- self.device = self.context.openByVendorIDAndProductID(0x16C0, 0x4E37) # QA402
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.CAPTURE_CHUNK_BYTES)
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.1) # A small delay to ensure the sample rate is set
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
- TRANSFER_BYTES = 16384 # 16 kB per USB bulk transfer (hardware chunk size)
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.TRANSFER_BYTES
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 = TRANSFER_BYTES) -> None:
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, or 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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyQa40x-i2c
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: A Python library for the QA40x audio analyser (int2code fork)
5
5
  Maintainer-email: Hubert Stepniewski <hubert.stepniewski@int2code.com>
6
6
  License: MIT License
@@ -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
- "requirements-test.txt",
7
- "requirements.txt",
8
- "requirements-dev.txt",
9
- "tox.ini",
10
- "pyproject.toml",
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
- "LICENSE",
14
- "src/PyQa40x/wave.py",
15
- "src/PyQa40x/math_general.py",
16
- "src/PyQa40x/fft_processor.py",
17
- "src/PyQa40x/wave_sine.py",
18
- "src/PyQa40x/freq_series_plotter.py",
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/sig_proc.py",
36
+ "src/PyQa40x/bluetooth.py",
21
37
  "src/PyQa40x/control.py",
22
- "src/PyQa40x/math_sines.py",
23
- "src/PyQa40x/analyzer.py",
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/math_windows.py",
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.egg-info/requires.txt",
36
- "src/PyQa40x.egg-info/SOURCES.txt",
37
- "src/PyQa40x.egg-info/top_level.txt",
38
- "src/PyQa40x.egg-info/dependency_links.txt",
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
- "tests/__init__.py"
56
+ "tox.ini"
57
57
  ]
58
58
  }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.3.0",
3
+ "distance": 0,
4
+ "node": "geab2d87790fb78dcedbb2918bc95d4b682060108",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-08-24"
8
+ }
@@ -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.
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "0.2.0",
3
- "distance": 0,
4
- "node": "g6a5ba3357cf07c1d160ef5f122265823f2560ec9",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-07-16"
8
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes