PyQa40x-i2c 0.1.2__tar.gz → 0.2.2__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.1.2 → pyqa40x_i2c-0.2.2}/PKG-INFO +1 -1
  2. pyqa40x_i2c-0.2.2/RELEASE_NOTES.md +23 -0
  3. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/src/PyQa40x/_version.py +3 -3
  4. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/src/PyQa40x/analyzer.py +163 -63
  5. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/src/PyQa40x/control.py +1 -1
  6. pyqa40x_i2c-0.2.2/src/PyQa40x/stream.py +274 -0
  7. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/src/PyQa40x/wave.py +1 -1
  8. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/src/PyQa40x_i2c.egg-info/PKG-INFO +1 -1
  9. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/src/PyQa40x_i2c.egg-info/SOURCES.txt +2 -0
  10. pyqa40x_i2c-0.2.2/src/PyQa40x_i2c.egg-info/scm_file_list.json +58 -0
  11. pyqa40x_i2c-0.2.2/src/PyQa40x_i2c.egg-info/scm_version.json +8 -0
  12. pyqa40x_i2c-0.1.2/RELEASE_NOTES.md +0 -11
  13. pyqa40x_i2c-0.1.2/src/PyQa40x/stream.py +0 -208
  14. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/.github/workflows/code_checker.yml +0 -0
  15. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/.github/workflows/publish_package.yml +0 -0
  16. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/.github/workflows/unit_tests.yml +0 -0
  17. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/.gitignore +0 -0
  18. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/.pylint_ignore +0 -0
  19. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/LICENSE +0 -0
  20. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/LibTest/FirstPrinciples.py +0 -0
  21. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/LibTest/PyQa40x_acoustic_chirp_spl_bt.py +0 -0
  22. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/LibTest/PyQa40x_acoustic_dbspl.py +0 -0
  23. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/LibTest/PyQa40x_acoustic_energy_decay_bt.py +0 -0
  24. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/LibTest/PyQa40x_coherent_averaging.py +0 -0
  25. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/LibTest/PyQa40x_cross_correlation.py +0 -0
  26. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/LibTest/PyQa40x_freq_response_with_mask.py +0 -0
  27. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/LibTest/PyQa40x_pinknoise.py +0 -0
  28. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/LibTest/PyQa40x_thdn.py +0 -0
  29. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/LibTest/mask_freq.txt +0 -0
  30. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/LibTest/mask_phase.txt +0 -0
  31. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/MANIFEST.in +0 -0
  32. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/README.md +0 -0
  33. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/_ci/is_dev_version.sh +0 -0
  34. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/pyproject.toml +0 -0
  35. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/requirements-dev.txt +0 -0
  36. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/requirements-test.txt +0 -0
  37. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/requirements.txt +0 -0
  38. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/setup.cfg +0 -0
  39. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/src/PyQa40x/__init__.py +0 -0
  40. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/src/PyQa40x/analyzer_params.py +0 -0
  41. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/src/PyQa40x/bluetooth.py +0 -0
  42. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/src/PyQa40x/cross_correlation.py +0 -0
  43. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/src/PyQa40x/fft_processor.py +0 -0
  44. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/src/PyQa40x/freq_series_plotter.py +0 -0
  45. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/src/PyQa40x/helpers.py +0 -0
  46. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/src/PyQa40x/math_chirp.py +0 -0
  47. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/src/PyQa40x/math_energy.py +0 -0
  48. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/src/PyQa40x/math_general.py +0 -0
  49. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/src/PyQa40x/math_sines.py +0 -0
  50. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/src/PyQa40x/math_windows.py +0 -0
  51. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/src/PyQa40x/registers.py +0 -0
  52. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/src/PyQa40x/series_plotter.py +0 -0
  53. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/src/PyQa40x/sig_proc.py +0 -0
  54. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/src/PyQa40x/wave_chirp.py +0 -0
  55. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/src/PyQa40x/wave_sine.py +0 -0
  56. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/src/PyQa40x.egg-info/PKG-INFO +0 -0
  57. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/src/PyQa40x.egg-info/SOURCES.txt +0 -0
  58. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/src/PyQa40x.egg-info/dependency_links.txt +0 -0
  59. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/src/PyQa40x.egg-info/requires.txt +0 -0
  60. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/src/PyQa40x.egg-info/top_level.txt +0 -0
  61. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/src/PyQa40x_i2c.egg-info/dependency_links.txt +0 -0
  62. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/src/PyQa40x_i2c.egg-info/requires.txt +0 -0
  63. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/src/PyQa40x_i2c.egg-info/top_level.txt +0 -0
  64. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/tests/__init__.py +0 -0
  65. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/tests/test_template.py +0 -0
  66. {pyqa40x_i2c-0.1.2 → pyqa40x_i2c-0.2.2}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyQa40x-i2c
3
- Version: 0.1.2
3
+ Version: 0.2.2
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,23 @@
1
+ ### 0.2.2
2
+
3
+ [PATCH] Allow reliable streaming for higher sample rates (`96kHz`, `192kHz`).
4
+
5
+ ### 0.2.1
6
+
7
+ [PATCH] Restore removed `Analyzer.send_receive` functionality.
8
+
9
+ ### 0.2.0
10
+
11
+ [MINOR] Add endless audio capturing
12
+
13
+ ### 0.1.2
14
+
15
+ [PATCH] Fix usb teardown deadlock.
16
+
17
+ ### 0.1.1
18
+
19
+ [PATCH] Fix missing dependency.
20
+
21
+ ### 0.1.0
22
+
23
+ [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.1.2'
22
- __version_tuple__ = version_tuple = (0, 1, 2)
21
+ __version__ = version = '0.2.2'
22
+ __version_tuple__ = version_tuple = (0, 2, 2)
23
23
 
24
- __commit_id__ = commit_id = 'g1bd064f6c'
24
+ __commit_id__ = commit_id = 'gf75dc49b9'
@@ -1,27 +1,39 @@
1
+ import threading
2
+ import logging
3
+ import struct
4
+
1
5
  import numpy as np
2
6
  import usb1 # pip install libusb1
3
- import struct
4
7
  import atexit
5
- import scipy.signal # pip install scipy
6
8
 
7
9
  from PyQa40x.registers import Registers
8
10
  from PyQa40x.control import Control
9
11
  from PyQa40x.stream import Stream
10
- from PyQa40x.wave_sine import Wave
11
- from PyQa40x.fft_processor import FFTProcessor
12
- from PyQa40x.sig_proc import SigProc
13
12
  from PyQa40x.helpers import *
14
13
  from PyQa40x.analyzer_params import AnalyzerParams
15
14
  from PyQa40x.bluetooth import BluetoothAudioDevice
15
+ from PyQa40x.wave_sine import Wave
16
+
17
+ logger = logging.getLogger(__name__)
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
+ }
16
26
 
17
27
 
18
28
  class Analyzer:
29
+
19
30
  def __init__(self):
20
31
  """
21
32
  Initializes the Analyzer class.
22
33
  """
23
34
  self.params: AnalyzerParams | None = None
24
35
  self.context: usb1.USBContext | None = None
36
+ self.chunk_size: int | None = None
25
37
  self.device: usb1.USBDeviceHandle | None = None
26
38
  self.registers: Registers | None = None
27
39
  self.control: Control | None = None
@@ -29,6 +41,9 @@ class Analyzer:
29
41
  self.cal_data: dict | None = None
30
42
  self.bt_device: BluetoothAudioDevice | None = None
31
43
  self._cleanup_registered = False
44
+ self._capturing: bool = False
45
+ self._dac_writer: threading.Thread | None = None # set in start_capture()
46
+ self._dac_writer_error: Exception | None = None # error from DAC writer thread
32
47
 
33
48
  def init(
34
49
  self,
@@ -72,6 +87,7 @@ class Analyzer:
72
87
  )
73
88
  self.context = usb1.USBContext()
74
89
  self.context.open()
90
+ self.chunk_size = TRANSFER_BYTES_MAP.get(sample_rate, Stream.DEFAULT_TRANSFER_BYTES)
75
91
 
76
92
  # Attempt to open QA402 or QA403 device
77
93
  self.device = self.context.openByVendorIDAndProductID(0x16C0, 0x4E37) # QA402
@@ -86,7 +102,7 @@ class Analyzer:
86
102
 
87
103
  self.registers = Registers(self.device)
88
104
  self.control = Control(self.registers)
89
- self.stream = Stream(self.context, self.device, self.registers)
105
+ self.stream = Stream(self.context, self.device, self.registers, self.chunk_size)
90
106
 
91
107
  # Load calibration data
92
108
  self.cal_data = self.control.load_calibration()
@@ -139,6 +155,138 @@ class Analyzer:
139
155
  except Exception as e:
140
156
  print(f"An error occurred during cleanup: {e}")
141
157
 
158
+ @staticmethod
159
+ def list_audio_devices_by_sample_rate(target_sample_rate: int):
160
+ """
161
+ List all available audio devices that support a specific sample rate
162
+ by calling the method from BluetoothAudioDevice.
163
+ """
164
+ BluetoothAudioDevice.list_audio_devices_by_sample_rate(target_sample_rate)
165
+
166
+ def _convert_adc_bytes_to_volts(
167
+ self, raw_bytes: bytearray, n_samples: int
168
+ ) -> tuple[np.ndarray, np.ndarray]:
169
+ """
170
+ Convert interleaved int32 ADC bytes to calibrated Volt arrays.
171
+
172
+ Args:
173
+ raw_bytes (bytearray): Interleaved little-endian int32 stereo ADC data.
174
+ n_samples (int): Samples per channel to return.
175
+
176
+ Returns:
177
+ tuple[np.ndarray, np.ndarray]: (left_volts, right_volts), float64.
178
+ """
179
+ # Reads the raw bytes and treat every 4 bytes as one signed 32‑bit integer
180
+ # [L0, R0, L1, R1, L2, R2, …]
181
+ interleaved = np.frombuffer(raw_bytes, dtype=np.int32)
182
+
183
+ # Split interleaved int32 array into left and right channels
184
+ left_int = interleaved[0::2][:n_samples]
185
+ right_int = interleaved[1::2][:n_samples]
186
+
187
+ # Convert int32 → float64 and normalise to range +-1
188
+ max_int_value = 2**31 - 1
189
+ left_f = left_int.astype(np.float64) / max_int_value
190
+ right_f = right_int.astype(np.float64) / max_int_value
191
+
192
+ # Factor for computing the voltage that corresponds to digital full scale
193
+ dbfs_correction = 10 ** ((self.params.max_input_level - 6) / 20)
194
+ # Stored QA40x calibration factors for the current input level.
195
+ cal_adc_left, cal_adc_right = self.control.get_adc_cal(
196
+ self.cal_data, self.params.max_input_level
197
+ )
198
+
199
+ # Apply ADC calibration and dBFS → Volts conversion
200
+ left_volts = left_f * cal_adc_left * dbfs_correction
201
+ right_volts = right_f * cal_adc_right * dbfs_correction
202
+
203
+ return left_volts, right_volts
204
+
205
+ def start_capture(self) -> None:
206
+ """
207
+ Starts a continuous capture of ADC data in a background thread.
208
+ The captured data can be retrieved later using the `read_capture` method.
209
+ """
210
+ if self._capturing:
211
+ raise RuntimeError("Capture is already in progress.")
212
+
213
+ self._capturing = True
214
+ self._dac_writer_error = None
215
+ self.stream.start()
216
+ self._dac_writer = threading.Thread(
217
+ target=self._dac_writer_loop, name="qa40x-dac-writer-thread", daemon=True
218
+ )
219
+ self._dac_writer.start()
220
+
221
+ def _dac_writer_loop(self) -> None:
222
+ """Continuously submit zero-DAC / ADC-read transfer pairs while capturing."""
223
+ while self._capturing and self.stream.running:
224
+ try:
225
+ self.stream.write_zeros(self.chunk_size)
226
+ except Exception as exc: # pylint: disable=broad-except
227
+ # Racing with stop() is expected; a real failure while we still
228
+ # expect to run is recorded in the post-loop reconciliation below.
229
+ if self._capturing and self.stream.running:
230
+ self._dac_writer_error = exc
231
+ logger.error("DAC writer failed; stopping capture.", exc_info=exc)
232
+ break
233
+
234
+ # If the loop ended but we never asked to stop, the stream halted
235
+ # underneath us (error or ADC overflow): record it and clear _capturing so
236
+ # the analyzer isn't wedged "capturing" behind a dead writer thread.
237
+ if self._capturing:
238
+ self._capturing = False
239
+ if self._dac_writer_error is None:
240
+ self._dac_writer_error = self.stream.overflow_error or RuntimeError(
241
+ "Stream stopped unexpectedly during capture."
242
+ )
243
+
244
+ def read_capture(self, n_samples: int) -> tuple[np.ndarray, np.ndarray]:
245
+ """
246
+ Return the next `n_samples` samples per channel as Volt arrays.
247
+
248
+ Blocks until the samples are available. Consecutive calls return
249
+ contiguous audio (no gap) because the stream is never stopped between
250
+ reads. Returns fewer than `n_samples` only if the capture is stopping.
251
+
252
+ Args:
253
+ n_samples (int): Samples per channel to return.
254
+
255
+ Returns:
256
+ tuple[np.ndarray, np.ndarray]: (left_volts, right_volts), float64.
257
+
258
+ Raises:
259
+ RuntimeError: If the DAC writer thread failed during capture.
260
+ AdcOverflowError: If capture data was not read fast enough.
261
+ """
262
+ if self._dac_writer_error is not None:
263
+ raise RuntimeError(
264
+ "DAC writer thread encountered an error during capture."
265
+ ) from self._dac_writer_error
266
+
267
+ need_bytes = n_samples * 8 # 2 channels x int32
268
+
269
+ raw = self.stream.consume_adc(
270
+ need_bytes, should_keep_waiting=lambda: self._capturing
271
+ )
272
+ got_samples = len(raw) // 8
273
+ return self._convert_adc_bytes_to_volts(raw, got_samples)
274
+
275
+ def stop_capture(self) -> None:
276
+ """
277
+ Stop the continuous capture: end the feeder thread and stop the stream.
278
+
279
+ Order matters: clearing `_capturing` and stopping the stream releases a
280
+ dac writer blocked on the dacQueue (stop() cancels transfers and drains the
281
+ queue), after which the dac writer observes `_capturing == False` and exits.
282
+ """
283
+ if not self._capturing:
284
+ return
285
+ self._capturing = False
286
+ self.stream.stop()
287
+ if self._dac_writer is not None:
288
+ self._dac_writer.join(timeout=5.0)
289
+
142
290
  def send_receive(self, left_wave: Wave, right_wave: Wave) -> tuple[Wave, Wave]:
143
291
  """
144
292
  Sends DAC data to the device and receives ADC data.
@@ -182,12 +330,10 @@ class Analyzer:
182
330
  interleaved_dac_data[1::2] = right_dac_data_float
183
331
 
184
332
  # Convert to bytes, multiplying by max int value
185
- max_int_value = 2**31 - 1
333
+ max_int_value = 2 ** 31 - 1
186
334
  interleaved_dac_data = (interleaved_dac_data * max_int_value).astype(np.int32)
187
335
 
188
- # Pack the data into chunks of 16k bytes
189
- chunk_size = 16384 # 16k bytes
190
- num_ints_per_chunk = chunk_size // 4 # 32-bit ints, so 4 bytes per int
336
+ num_ints_per_chunk = self.chunk_size // 4 # 32-bit ints, so 4 bytes per int
191
337
  total_chunks = len(interleaved_dac_data) // num_ints_per_chunk
192
338
 
193
339
  self.stream.start()
@@ -201,8 +347,8 @@ class Analyzer:
201
347
  try:
202
348
  for i in range(total_chunks):
203
349
  chunk = interleaved_dac_data[
204
- i * num_ints_per_chunk : (i + 1) * num_ints_per_chunk
205
- ]
350
+ i * num_ints_per_chunk: (i + 1) * num_ints_per_chunk
351
+ ]
206
352
  buffer = struct.pack("<%di" % len(chunk), *chunk)
207
353
  self.stream.write(buffer)
208
354
  finally:
@@ -255,11 +401,6 @@ class Analyzer:
255
401
 
256
402
  return left_wave, right_wave
257
403
 
258
- @staticmethod
259
- def list_audio_devices_by_sample_rate(target_sample_rate: int):
260
- """List all available audio devices that support a specific sample rate by calling the method from BluetoothAudioDevice."""
261
- BluetoothAudioDevice.list_audio_devices_by_sample_rate(target_sample_rate)
262
-
263
404
  def capture(self, total_samples: int) -> tuple[np.ndarray, np.ndarray]:
264
405
  """
265
406
  Captures ``total_samples`` samples from both ADC input channels without
@@ -285,51 +426,10 @@ class Analyzer:
285
426
  two float64 arrays each of length ``total_samples``, values
286
427
  in Volts (calibrated, same scale as ``send_receive()``).
287
428
  """
288
- chunk_bytes = 16384 # 16 kB per USB transfer
289
- bytes_per_stereo_sample = 8 # 2 × int32
290
- need_bytes = total_samples * bytes_per_stereo_sample
291
-
292
- # Number of DAC write / ADC read pairs needed to cover need_bytes.
293
- # Each pair transfers chunk_bytes ADC bytes.
294
- n_chunks = -(-need_bytes // chunk_bytes) # ceiling division
295
-
296
- max_int_value = 2**31 - 1
297
-
298
- # -- single stream start ------------------------------------------------
299
- self.stream.start()
429
+ # A fixed-length capture is just a continuous capture read once: start
430
+ # the gapless stream, read exactly total_samples, then stop.
431
+ self.start_capture()
300
432
  try:
301
- # Submit all zero-DAC / ADC-read pairs up front so the USB host
302
- # controller can pipeline them; no stop/restart in the middle.
303
- for _ in range(n_chunks):
304
- self.stream.write_zeros(chunk_bytes)
305
-
306
- # Block until we have exactly the bytes we need
307
- raw_bytes = self.stream.collect_adc_exact(need_bytes)
433
+ return self.read_capture(total_samples)
308
434
  finally:
309
- self.stream.stop()
310
- # -- stream stopped -----------------------------------------------------
311
-
312
- # Deserialise interleaved int32 ADC data
313
- interleaved = np.frombuffer(raw_bytes, dtype=np.int32)
314
-
315
- left_int = interleaved[0::2]
316
- right_int = interleaved[1::2]
317
-
318
- # Trim to exact length (collect_adc_exact already trims, but be safe)
319
- left_int = left_int[:total_samples]
320
- right_int = right_int[:total_samples]
321
-
322
- # Convert int32 → float64 in the range ±1
323
- left_f = left_int.astype(np.float64) / max_int_value
324
- right_f = right_int.astype(np.float64) / max_int_value
325
-
326
- # Apply ADC calibration and dBFS → Volts conversion
327
- cal_adc_left, cal_adc_right = self.control.get_adc_cal(
328
- self.cal_data, self.params.max_input_level
329
- )
330
- adc_dbfs_correction = 10 ** ((self.params.max_input_level - 6) / 20)
331
-
332
- left_volts = left_f * cal_adc_left * adc_dbfs_correction
333
- right_volts = right_f * cal_adc_right * adc_dbfs_correction
334
-
335
- return left_volts, right_volts
435
+ self.stop_capture()
@@ -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
  """
@@ -0,0 +1,274 @@
1
+ import threading
2
+ import queue
3
+ import time
4
+
5
+ import usb1
6
+ import logging
7
+ from typing import Callable
8
+
9
+ from PyQa40x.registers import Registers
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class AdcOverflowError(RuntimeError):
15
+ """
16
+ Raised when the ADC receive buffer exceeds its high-water mark.
17
+
18
+ The capture pipeline requires the consumer to read captured data
19
+ continuously (see ``consume_adc``). If the consumer falls behind, the
20
+ receive buffer grows without bound toward OOM. Rather than leak memory,
21
+ the stream treats this as a hard error: the worker thread latches it and
22
+ ``consume_adc`` re-raises it on the consumer's thread.
23
+ """
24
+
25
+
26
+ class Stream:
27
+ """
28
+ Class to manage buffer streaming, with event worker thread.
29
+ """
30
+
31
+ TRANSFER_TIMEOUT_MS = 1000 # Timeout for USB transfers in milliseconds
32
+ DEFAULT_TRANSFER_BYTES = 16384 # 16 kB per USB bulk transfer (hardware chunk size)
33
+ # cap on unread ADC data before overflow
34
+ MAX_BUFFERED_BYTES = 256 * 1024 * 1024
35
+
36
+ def __init__(
37
+ self,
38
+ context: usb1.USBContext,
39
+ device: usb1.USBDeviceHandle,
40
+ registers: Registers,
41
+ transfer_bytes_size: int = DEFAULT_TRANSFER_BYTES
42
+ ) -> None:
43
+ """
44
+ Initialize the Stream.
45
+
46
+ Args:
47
+ context (usb1.USBContext): The USB context.
48
+ device (usb1.USBDeviceHandle): The USB device handle.
49
+ registers (Registers): The Registers instance for control writes.
50
+ transfer_bytes_size (int):
51
+ """
52
+ self.context = context
53
+ self.device = device
54
+ self.registers = registers
55
+ self.transfer_bytes_size = transfer_bytes_size # USB bulk transfer (hardware chunk size)
56
+ self.endpoint_read = usb1.ENDPOINT_IN | 0x02 # EP2 in
57
+ self.endpoint_write = usb1.ENDPOINT_OUT | 0x02 # EP2 out
58
+ self.dacQueue = queue.Queue(
59
+ maxsize=5
60
+ ) # Max. 5 overlapping buffers in flight, block on more
61
+ self.adcQueue = queue.Queue() # Unlimited queue for received data buffers
62
+ self.transfer_helper = usb1.USBTransferHelper() # Use the callback dispatcher
63
+ # Handle COMPLETED transfers normally; also handle ERROR/TIMED_OUT/CANCELLED
64
+ # so the queues are always drained and dacQueue.put() never deadlocks.
65
+ for status in (
66
+ usb1.TRANSFER_COMPLETED,
67
+ usb1.TRANSFER_ERROR,
68
+ usb1.TRANSFER_TIMED_OUT,
69
+ usb1.TRANSFER_CANCELLED,
70
+ usb1.TRANSFER_STALL,
71
+ usb1.TRANSFER_NO_DEVICE,
72
+ usb1.TRANSFER_OVERFLOW,
73
+ ):
74
+ self.transfer_helper.setEventCallback(status, self.callback)
75
+ self.thread: threading.Thread | None = None
76
+ self.running = False
77
+ # In-flight transfers, keyed by identity; pruned in callback, bulk-cancelled in stop.
78
+ self._pending_transfers: set = set()
79
+ # raised to break the worker out of stuck queues
80
+ self._force_stop = threading.Event()
81
+ self._transfer_lock = threading.Lock() # protects _pending_transfers
82
+ self._epoch = 0 # session counter; stale-session callbacks are ignored
83
+ self._adc_data = queue.Queue() # received ADC data, one chunk per item
84
+ self._adc_data_carry = bytearray() # leftover ADC bytes between consume_adc calls
85
+ # latched, raised by consume_adc
86
+ self._overflow_error: AdcOverflowError | None = None
87
+ self._max_queued_chunks = self.MAX_BUFFERED_BYTES // self.transfer_bytes_size
88
+
89
+ @property
90
+ def overflow_error(self) -> "AdcOverflowError | None":
91
+ """Latched ADC overflow error, or None if the buffer never overran."""
92
+ return self._overflow_error
93
+
94
+ def start(self) -> None:
95
+ """
96
+ Start streaming and spawn the worker thread.
97
+ """
98
+ self._force_stop.clear()
99
+ self._epoch += 1 # new session; callbacks from prior sessions are ignored
100
+ self.thread = threading.Thread(target=self.worker, daemon=True)
101
+ self.running = True
102
+ self.thread.start()
103
+ self._adc_data = queue.Queue() # Reset the ADC data queue
104
+ self._adc_data_carry = bytearray() # Reset the carry buffer
105
+ self._overflow_error = None # Reset the overflow error
106
+ self.registers.write(8, 0x05) # Start streaming
107
+
108
+ def stop(self) -> None:
109
+ """
110
+ Stop streaming and end the worker thread.
111
+
112
+ Sets ``running`` to False and cancels in-flight USB transfers so their
113
+ callbacks fire and drain the queues, which allows the worker loop to
114
+ exit naturally. If the worker has not exited within 5 seconds (e.g.
115
+ because a USB cancellation is slow), ``_force_stop`` is set so the
116
+ worker breaks unconditionally after its current
117
+ ``handleEventsTimeout(0.1)`` call (≤ 100 ms), and we give it one
118
+ further second to join before proceeding.
119
+ """
120
+ # Cancel any in-flight transfers so their callbacks fire and drain the queues,
121
+ # allowing the worker thread to exit its loop. The lock ensure no transfer is added
122
+ # to the list while we are cancelling.
123
+ with self._transfer_lock:
124
+ self.running = False
125
+ for transfer in self._pending_transfers:
126
+ try:
127
+ transfer.cancel()
128
+ except Exception: # pylint: disable=broad-except
129
+ pass
130
+ self._pending_transfers.clear()
131
+
132
+ if self.thread is not None and self.thread.is_alive():
133
+ self.thread.join(timeout=5.0)
134
+ if self.thread.is_alive():
135
+ # Worker is stuck (slow USB cancellation). Force it out so
136
+ # context.close() does not deadlock.
137
+ self._force_stop.set()
138
+ self.thread.join(timeout=1.0)
139
+ self.registers.write(8, 0x00) # Stop streaming
140
+
141
+ def worker(self) -> None:
142
+ """
143
+ Event loop for the asynchronous transfers.
144
+
145
+ Runs until *both* conditions are true:
146
+ * ``running`` is False (``stop()`` has been called), and
147
+ * both USB queues are empty (all callbacks have fired).
148
+
149
+ An early exit is also triggered by ``_force_stop`` (set by ``stop()``
150
+ when the normal join times out) so that ``context.close()`` is never
151
+ called while this thread is still inside ``handleEventsTimeout``.
152
+ """
153
+ while self.running or not (
154
+ self.dacQueue.empty() and self.adcQueue.empty()
155
+ ): # Play until the last
156
+ if self._force_stop.is_set():
157
+ break
158
+ # 100 ms timeout prevents blocking forever
159
+ self.context.handleEventsTimeout(0.1)
160
+
161
+ def callback(self, transfer: usb1.USBTransfer) -> None:
162
+ """
163
+ Handle a completed transfer (runs on the worker thread).
164
+
165
+ On a completed ADC read, buffer the bytes; if the receive queue passes
166
+ its limit, latch an AdcOverflowError and stop so consume_adc can raise it.
167
+
168
+ Args:
169
+ transfer (usb1.USBTransfer): The USB transfer that has completed.
170
+ """
171
+ current = transfer.getUserData() == self._epoch # ignore stale-session data
172
+ if transfer.getEndpoint() == self.endpoint_read:
173
+ if current and transfer.getStatus() == usb1.TRANSFER_COMPLETED:
174
+ if self._overflow_error is None:
175
+ # Copy: the transfer buffer is freed once we prune the transfer.
176
+ self._adc_data.put(bytes(transfer.getBuffer()))
177
+ if self._adc_data.qsize() > self._max_queued_chunks:
178
+ self._overflow_error = AdcOverflowError(
179
+ "ADC receive queue exceeded high-water mark "
180
+ f"({self._adc_data.qsize()} chunks); consumer not "
181
+ "reading capture fast enough."
182
+ )
183
+ self.running = False # unwind feeder + worker
184
+ logger.error(str(self._overflow_error))
185
+ self.adcQueue.get_nowait() # Always drain the queue
186
+ else:
187
+ self.dacQueue.get_nowait() # Always drain the queue
188
+
189
+ # Prune this finished transfer so _pending_transfers stays bounded during
190
+ # a long capture; discard() is a no-op if stop() already cleared it.
191
+ with self._transfer_lock:
192
+ self._pending_transfers.discard(transfer)
193
+
194
+ def write(self, buffer: bytes) -> None:
195
+ """Submit one DAC playback buffer and one matching ADC read transfer."""
196
+ with self._transfer_lock:
197
+ if not self.running:
198
+ raise RuntimeError(
199
+ "Stream is not running; call start() first.")
200
+ dac = self.device.getTransfer()
201
+ dac.setBulk(self.endpoint_write, buffer, self.transfer_helper,
202
+ self._epoch, self.TRANSFER_TIMEOUT_MS)
203
+ dac.submit()
204
+ self._pending_transfers.add(dac)
205
+ read = self.device.getTransfer()
206
+ read.setBulk(self.endpoint_read, len(buffer), self.transfer_helper,
207
+ self._epoch, self.TRANSFER_TIMEOUT_MS)
208
+ read.submit()
209
+ self._pending_transfers.add(read)
210
+ self.dacQueue.put(dac)
211
+ self.adcQueue.put(read)
212
+
213
+ def write_zeros(self, chunk_bytes: int = DEFAULT_TRANSFER_BYTES) -> None:
214
+ """Feed the ADC pipeline with a zero DAC buffer (continuous capture)."""
215
+ self.write(bytes(chunk_bytes))
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
+
233
+ def consume_adc(
234
+ self,
235
+ need_bytes: int,
236
+ should_keep_waiting: Callable[[], bool] | None = None,
237
+ poll_s: float = 0.005,
238
+ ) -> bytearray:
239
+ """
240
+ Block until `need_bytes` of ADC data are available, then remove and
241
+ return exactly that many bytes from the front of the receive buffer.
242
+
243
+ Args:
244
+ need_bytes (int): Number of bytes to consume.
245
+ should_keep_waiting (Callable[[], bool] | None): Polled each iteration;
246
+ when it returns False, stop waiting and return what is available.
247
+ poll_s (float): Poll interval in seconds.
248
+
249
+ Returns:
250
+ bytearray: Up to `need_bytes` of ADC data (fewer only on early stop).
251
+
252
+ Raises:
253
+ AdcOverflowError: If the receive buffer exceeded its limit because
254
+ captured data was not read fast enough.
255
+ """
256
+ while len(self._adc_data_carry) < need_bytes:
257
+ if self._overflow_error is not None:
258
+ raise self._overflow_error
259
+ if should_keep_waiting is not None and not should_keep_waiting():
260
+ break
261
+ if not self.running and self._adc_data.empty():
262
+ break # stream stopped and no more data will arrive, return what we have
263
+ try:
264
+ chunk = self._adc_data.get(timeout=poll_s)
265
+ self._adc_data_carry.extend(chunk)
266
+ except queue.Empty:
267
+ continue # No new data yet, loop and check conditions again
268
+
269
+ if self._overflow_error is not None:
270
+ raise self._overflow_error
271
+ take_bytes_count = min(need_bytes, len(self._adc_data_carry))
272
+ out_bytes = self._adc_data_carry[:take_bytes_count]
273
+ del self._adc_data_carry[:take_bytes_count]
274
+ return out_bytes
@@ -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.1.2
3
+ Version: 0.2.2
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
@@ -55,6 +55,8 @@ src/PyQa40x_i2c.egg-info/PKG-INFO
55
55
  src/PyQa40x_i2c.egg-info/SOURCES.txt
56
56
  src/PyQa40x_i2c.egg-info/dependency_links.txt
57
57
  src/PyQa40x_i2c.egg-info/requires.txt
58
+ src/PyQa40x_i2c.egg-info/scm_file_list.json
59
+ src/PyQa40x_i2c.egg-info/scm_version.json
58
60
  src/PyQa40x_i2c.egg-info/top_level.txt
59
61
  tests/__init__.py
60
62
  tests/test_template.py
@@ -0,0 +1,58 @@
1
+ {
2
+ "files": [
3
+ "tox.ini",
4
+ "requirements.txt",
5
+ "requirements-test.txt",
6
+ "requirements-dev.txt",
7
+ "MANIFEST.in",
8
+ "pyproject.toml",
9
+ ".pylint_ignore",
10
+ "LICENSE",
11
+ "RELEASE_NOTES.md",
12
+ "README.md",
13
+ ".gitignore",
14
+ ".github/workflows/code_checker.yml",
15
+ ".github/workflows/unit_tests.yml",
16
+ ".github/workflows/publish_package.yml",
17
+ "tests/__init__.py",
18
+ "tests/test_template.py",
19
+ "src/PyQa40x/sig_proc.py",
20
+ "src/PyQa40x/wave.py",
21
+ "src/PyQa40x/helpers.py",
22
+ "src/PyQa40x/freq_series_plotter.py",
23
+ "src/PyQa40x/math_sines.py",
24
+ "src/PyQa40x/wave_sine.py",
25
+ "src/PyQa40x/registers.py",
26
+ "src/PyQa40x/math_windows.py",
27
+ "src/PyQa40x/math_energy.py",
28
+ "src/PyQa40x/math_chirp.py",
29
+ "src/PyQa40x/analyzer.py",
30
+ "src/PyQa40x/analyzer_params.py",
31
+ "src/PyQa40x/__init__.py",
32
+ "src/PyQa40x/math_general.py",
33
+ "src/PyQa40x/control.py",
34
+ "src/PyQa40x/wave_chirp.py",
35
+ "src/PyQa40x/series_plotter.py",
36
+ "src/PyQa40x/fft_processor.py",
37
+ "src/PyQa40x/stream.py",
38
+ "src/PyQa40x/cross_correlation.py",
39
+ "src/PyQa40x/bluetooth.py",
40
+ "src/PyQa40x.egg-info/SOURCES.txt",
41
+ "src/PyQa40x.egg-info/dependency_links.txt",
42
+ "src/PyQa40x.egg-info/requires.txt",
43
+ "src/PyQa40x.egg-info/PKG-INFO",
44
+ "src/PyQa40x.egg-info/top_level.txt",
45
+ "_ci/is_dev_version.sh",
46
+ "LibTest/PyQa40x_thdn.py",
47
+ "LibTest/PyQa40x_freq_response_with_mask.py",
48
+ "LibTest/mask_phase.txt",
49
+ "LibTest/PyQa40x_coherent_averaging.py",
50
+ "LibTest/FirstPrinciples.py",
51
+ "LibTest/PyQa40x_cross_correlation.py",
52
+ "LibTest/PyQa40x_pinknoise.py",
53
+ "LibTest/mask_freq.txt",
54
+ "LibTest/PyQa40x_acoustic_energy_decay_bt.py",
55
+ "LibTest/PyQa40x_acoustic_dbspl.py",
56
+ "LibTest/PyQa40x_acoustic_chirp_spl_bt.py"
57
+ ]
58
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.2.2",
3
+ "distance": 0,
4
+ "node": "gf75dc49b93b52857fb5f1d4ae3825e1a36fc7a30",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-08-06"
8
+ }
@@ -1,11 +0,0 @@
1
- ### 0.1.2
2
-
3
- [PATCH] Fix usb teardown deadlock.
4
-
5
- ### 0.1.1
6
-
7
- [PATCH] Fix missing dependency.
8
-
9
- ### 0.1.0
10
-
11
- [MINOR] Initial release as PyQa40x-i2c fork. Unlimited audio samples capture added.
@@ -1,208 +0,0 @@
1
- import threading
2
- import queue
3
- import time
4
- import usb1
5
- import numpy as np
6
-
7
-
8
- class Stream:
9
- """
10
- Class to manage buffer streaming, with event worker thread.
11
- """
12
-
13
- def __init__(self, context, device, registers):
14
- """
15
- Initializes the Stream class.
16
-
17
- Args:
18
- context (usb1.USBContext): The USB context.
19
- device (usb1.USBDeviceHandle): The USB device handle.
20
- registers (Registers): An instance of the Registers class.
21
- """
22
- self.context = context
23
- self.device = device
24
- self.registers = registers
25
- self.endpoint_read = usb1.ENDPOINT_IN | 0x02 # EP2 in
26
- self.endpoint_write = usb1.ENDPOINT_OUT | 0x02 # EP2 out
27
- self.dacQueue = queue.Queue(
28
- maxsize=5
29
- ) # Max. 5 overlapping buffers in flight, block on more
30
- self.adcQueue = queue.Queue() # Unlimited queue for received data buffers
31
- self.transfer_helper = usb1.USBTransferHelper() # Use the callback dispatcher
32
- # Handle COMPLETED transfers normally; also handle ERROR/TIMED_OUT/CANCELLED
33
- # so the queues are always drained and dacQueue.put() never deadlocks.
34
- for status in (
35
- usb1.TRANSFER_COMPLETED,
36
- usb1.TRANSFER_ERROR,
37
- usb1.TRANSFER_TIMED_OUT,
38
- usb1.TRANSFER_CANCELLED,
39
- usb1.TRANSFER_STALL,
40
- usb1.TRANSFER_NO_DEVICE,
41
- usb1.TRANSFER_OVERFLOW,
42
- ):
43
- self.transfer_helper.setEventCallback(status, self.callback)
44
- # self.received_data = bytearray() # Collection of received data bytes
45
- self.thread = None
46
- self.running = False
47
- self._pending_transfers = [] # track all submitted transfers for cancellation
48
- self._force_stop = (
49
- threading.Event()
50
- ) # raised to break the worker out of stuck queues
51
-
52
- def start(self):
53
- """
54
- Starts the streaming and spawns the worker thread.
55
- """
56
- self._force_stop.clear()
57
- self.thread = threading.Thread(target=self.worker, daemon=True)
58
- self.running = True
59
- self.thread.start()
60
- self.received_data = bytearray()
61
- self.registers.write(8, 0x05) # Start streaming
62
-
63
- def stop(self):
64
- """
65
- Stops the streaming and ends the worker thread.
66
-
67
- Sets ``running`` to False and cancels in-flight USB transfers so their
68
- callbacks fire and drain the queues, which allows the worker loop to
69
- exit naturally. If the worker has not exited within 5 seconds (e.g.
70
- because a USB cancellation is slow), ``_force_stop`` is set so the
71
- worker breaks unconditionally after its current
72
- ``handleEventsTimeout(0.1)`` call (≤ 100 ms), and we give it one
73
- further second to join before proceeding.
74
- """
75
- self.running = False
76
- # Cancel any in-flight transfers so their callbacks fire and drain the queues,
77
- # allowing the worker thread to exit its loop.
78
- for t in self._pending_transfers:
79
- try:
80
- t.cancel()
81
- except Exception:
82
- pass
83
- self._pending_transfers.clear()
84
- if self.thread is not None and self.thread.is_alive():
85
- self.thread.join(timeout=5.0)
86
- if self.thread.is_alive():
87
- # Worker is stuck (slow USB cancellation). Force it out so
88
- # context.close() does not deadlock.
89
- self._force_stop.set()
90
- self.thread.join(timeout=1.0)
91
- self.registers.write(8, 0x00) # Stop streaming
92
-
93
- def write(self, buffer):
94
- """
95
- Adds a buffer to the playback queue.
96
-
97
- Args:
98
- buffer (bytes): The data buffer to be written.
99
- """
100
- transfer = self.device.getTransfer()
101
- transfer.setBulk(self.endpoint_write, buffer, self.transfer_helper, None, 1000)
102
- transfer.submit() # Asynchronous transfer
103
- self._pending_transfers.append(transfer)
104
- self.dacQueue.put(transfer) # It doesn't matter what we put in here
105
-
106
- # Submit a USB bulk transfer to read
107
- read_transfer = self.device.getTransfer()
108
- read_transfer.setBulk(
109
- self.endpoint_read, 16384, self.transfer_helper, None, 1000
110
- )
111
- read_transfer.submit() # Asynchronous transfer
112
- self._pending_transfers.append(read_transfer)
113
- self.adcQueue.put(read_transfer) # It doesn't matter what we put in here
114
-
115
- def worker(self):
116
- """
117
- Event loop for the asynchronous transfers.
118
-
119
- Runs until *both* conditions are true:
120
- * ``running`` is False (``stop()`` has been called), and
121
- * both USB queues are empty (all callbacks have fired).
122
-
123
- An early exit is also triggered by ``_force_stop`` (set by ``stop()``
124
- when the normal join times out) so that ``context.close()`` is never
125
- called while this thread is still inside ``handleEventsTimeout``.
126
- """
127
- while self.running or not (
128
- self.dacQueue.empty() and self.adcQueue.empty()
129
- ): # Play until the last
130
- if self._force_stop.is_set():
131
- break
132
- self.context.handleEventsTimeout(
133
- 0.1
134
- ) # 100 ms timeout prevents blocking forever
135
-
136
- def callback(self, transfer):
137
- """
138
- Callback of the worker thread to handle completed transfers.
139
-
140
- Args:
141
- transfer (usb1.USBTransfer): The USB transfer that has completed.
142
- """
143
- if transfer.getEndpoint() == self.endpoint_read:
144
- if transfer.getStatus() == usb1.TRANSFER_COMPLETED:
145
- self.received_data.extend(
146
- transfer.getBuffer()
147
- ) # Collect received data bytes
148
- self.adcQueue.get_nowait() # Always drain the queue
149
- else:
150
- self.dacQueue.get_nowait() # Always drain the queue
151
-
152
- def collect_remaining_adc_data(self):
153
- """
154
- Waits for all remaining ADC transfers to complete and returns the collected data.
155
-
156
- Returns:
157
- bytearray: The collected ADC data.
158
- """
159
- while not self.adcQueue.empty():
160
- time.sleep(0.01)
161
-
162
- return self.received_data
163
-
164
- def write_zeros(self, chunk_bytes: int = 16384):
165
- """
166
- Sends a zero-filled DAC buffer of `chunk_bytes` bytes and queues one
167
- matching ADC read transfer. Functionally identical to write() but
168
- generates the zero payload internally so no DAC Wave is needed.
169
-
170
- Args:
171
- chunk_bytes (int): Number of bytes per transfer (must be a multiple
172
- of 4). Defaults to 16384 (16 kB) to match the hardware chunk
173
- size used by write().
174
- """
175
- zeros = bytes(chunk_bytes)
176
-
177
- dac_transfer = self.device.getTransfer()
178
- dac_transfer.setBulk(
179
- self.endpoint_write, zeros, self.transfer_helper, None, 1000
180
- )
181
- dac_transfer.submit()
182
- self._pending_transfers.append(dac_transfer)
183
- self.dacQueue.put(dac_transfer)
184
-
185
- read_transfer = self.device.getTransfer()
186
- read_transfer.setBulk(
187
- self.endpoint_read, chunk_bytes, self.transfer_helper, None, 1000
188
- )
189
- read_transfer.submit()
190
- self._pending_transfers.append(read_transfer)
191
- self.adcQueue.put(read_transfer)
192
-
193
- def collect_adc_exact(self, n_bytes: int) -> bytearray:
194
- """
195
- Blocks until at least `n_bytes` of ADC data have been collected, then
196
- returns exactly `n_bytes` (any excess is discarded). The stream must
197
- have been started with start() and all required write_zeros() calls
198
- must have been submitted before calling this.
199
-
200
- Args:
201
- n_bytes (int): Exact number of bytes to return.
202
-
203
- Returns:
204
- bytearray: Exactly n_bytes of calibrated ADC data.
205
- """
206
- while len(self.received_data) < n_bytes:
207
- time.sleep(0.01)
208
- return bytearray(self.received_data[:n_bytes])
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes