PyQa40x-i2c 0.2.2__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 (65) hide show
  1. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/PKG-INFO +1 -1
  2. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/RELEASE_NOTES.md +4 -0
  3. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/src/PyQa40x/_version.py +3 -3
  4. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/src/PyQa40x/analyzer.py +65 -13
  5. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/src/PyQa40x_i2c.egg-info/PKG-INFO +1 -1
  6. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/src/PyQa40x_i2c.egg-info/scm_file_list.json +46 -46
  7. pyqa40x_i2c-0.3.0/src/PyQa40x_i2c.egg-info/scm_version.json +8 -0
  8. pyqa40x_i2c-0.2.2/src/PyQa40x_i2c.egg-info/scm_version.json +0 -8
  9. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/.github/workflows/code_checker.yml +0 -0
  10. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/.github/workflows/publish_package.yml +0 -0
  11. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/.github/workflows/unit_tests.yml +0 -0
  12. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/.gitignore +0 -0
  13. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/.pylint_ignore +0 -0
  14. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/LICENSE +0 -0
  15. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/LibTest/FirstPrinciples.py +0 -0
  16. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/LibTest/PyQa40x_acoustic_chirp_spl_bt.py +0 -0
  17. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/LibTest/PyQa40x_acoustic_dbspl.py +0 -0
  18. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/LibTest/PyQa40x_acoustic_energy_decay_bt.py +0 -0
  19. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/LibTest/PyQa40x_coherent_averaging.py +0 -0
  20. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/LibTest/PyQa40x_cross_correlation.py +0 -0
  21. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/LibTest/PyQa40x_freq_response_with_mask.py +0 -0
  22. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/LibTest/PyQa40x_pinknoise.py +0 -0
  23. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/LibTest/PyQa40x_thdn.py +0 -0
  24. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/LibTest/mask_freq.txt +0 -0
  25. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/LibTest/mask_phase.txt +0 -0
  26. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/MANIFEST.in +0 -0
  27. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/README.md +0 -0
  28. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/_ci/is_dev_version.sh +0 -0
  29. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/pyproject.toml +0 -0
  30. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/requirements-dev.txt +0 -0
  31. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/requirements-test.txt +0 -0
  32. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/requirements.txt +0 -0
  33. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/setup.cfg +0 -0
  34. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/src/PyQa40x/__init__.py +0 -0
  35. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/src/PyQa40x/analyzer_params.py +0 -0
  36. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/src/PyQa40x/bluetooth.py +0 -0
  37. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/src/PyQa40x/control.py +0 -0
  38. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/src/PyQa40x/cross_correlation.py +0 -0
  39. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/src/PyQa40x/fft_processor.py +0 -0
  40. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/src/PyQa40x/freq_series_plotter.py +0 -0
  41. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/src/PyQa40x/helpers.py +0 -0
  42. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/src/PyQa40x/math_chirp.py +0 -0
  43. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/src/PyQa40x/math_energy.py +0 -0
  44. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/src/PyQa40x/math_general.py +0 -0
  45. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/src/PyQa40x/math_sines.py +0 -0
  46. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/src/PyQa40x/math_windows.py +0 -0
  47. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/src/PyQa40x/registers.py +0 -0
  48. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/src/PyQa40x/series_plotter.py +0 -0
  49. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/src/PyQa40x/sig_proc.py +0 -0
  50. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/src/PyQa40x/stream.py +0 -0
  51. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/src/PyQa40x/wave.py +0 -0
  52. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/src/PyQa40x/wave_chirp.py +0 -0
  53. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/src/PyQa40x/wave_sine.py +0 -0
  54. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/src/PyQa40x.egg-info/PKG-INFO +0 -0
  55. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/src/PyQa40x.egg-info/SOURCES.txt +0 -0
  56. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/src/PyQa40x.egg-info/dependency_links.txt +0 -0
  57. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/src/PyQa40x.egg-info/requires.txt +0 -0
  58. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/src/PyQa40x.egg-info/top_level.txt +0 -0
  59. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/src/PyQa40x_i2c.egg-info/SOURCES.txt +0 -0
  60. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/src/PyQa40x_i2c.egg-info/dependency_links.txt +0 -0
  61. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/src/PyQa40x_i2c.egg-info/requires.txt +0 -0
  62. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/src/PyQa40x_i2c.egg-info/top_level.txt +0 -0
  63. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/tests/__init__.py +0 -0
  64. {pyqa40x_i2c-0.2.2 → pyqa40x_i2c-0.3.0}/tests/test_template.py +0 -0
  65. {pyqa40x_i2c-0.2.2 → 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.2
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,3 +1,7 @@
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
+
1
5
  ### 0.2.2
2
6
 
3
7
  [PATCH] Allow reliable streaming for higher sample rates (`96kHz`, `192kHz`).
@@ -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.2'
22
- __version_tuple__ = version_tuple = (0, 2, 2)
21
+ __version__ = version = '0.3.0'
22
+ __version_tuple__ = version_tuple = (0, 3, 0)
23
23
 
24
- __commit_id__ = commit_id = 'gf75dc49b9'
24
+ __commit_id__ = commit_id = 'geab2d8779'
@@ -27,6 +27,9 @@ TRANSFER_BYTES_MAP = {
27
27
 
28
28
  class Analyzer:
29
29
 
30
+ VENDOR_ID = 0x16C0
31
+ PRODUCT_IDS = (0x4E37, 0x4E39) # QA402, QA403
32
+
30
33
  def __init__(self):
31
34
  """
32
35
  Initializes the Analyzer class.
@@ -45,6 +48,55 @@ class Analyzer:
45
48
  self._dac_writer: threading.Thread | None = None # set in start_capture()
46
49
  self._dac_writer_error: Exception | None = None # error from DAC writer thread
47
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
+
48
100
  def init(
49
101
  self,
50
102
  sample_rate: int = 48000,
@@ -57,6 +109,7 @@ class Analyzer:
57
109
  amplitude_unit: str = "dbv",
58
110
  distortion_unit: str = "db",
59
111
  bt_device_name: str = "",
112
+ usb_serial: str | None = None,
60
113
  ) -> AnalyzerParams:
61
114
  """
62
115
  Initializes the analyzer hardware with the specified parameters.
@@ -72,6 +125,9 @@ class Analyzer:
72
125
  amplitude_unit (str): Unit for amplitude measurements, default is "dbv".
73
126
  distortion_unit (str): Unit for distortion measurements, default is "db".
74
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.
75
131
 
76
132
  Returns:
77
133
  AnalyzerParams: A class instance containing the hardware and parameter settings.
@@ -87,16 +143,12 @@ class Analyzer:
87
143
  )
88
144
  self.context = usb1.USBContext()
89
145
  self.context.open()
90
- self.chunk_size = TRANSFER_BYTES_MAP.get(sample_rate, Stream.DEFAULT_TRANSFER_BYTES)
91
-
92
- # Attempt to open QA402 or QA403 device
93
- self.device = self.context.openByVendorIDAndProductID(0x16C0, 0x4E37) # QA402
94
- if self.device is None:
95
- self.device = self.context.openByVendorIDAndProductID(
96
- 0x16C0, 0x4E39
97
- ) # QA403
98
- if self.device is None:
99
- raise SystemExit("No QA402/QA403 analyzer found")
146
+ self.chunk_size = TRANSFER_BYTES_MAP.get(
147
+ sample_rate, Stream.DEFAULT_TRANSFER_BYTES
148
+ )
149
+
150
+ # Attempt to open the requested QA402/QA403 device, or the first one
151
+ self.device = self._open_analyzer(usb_serial)
100
152
  self.device.resetDevice()
101
153
  self.device.claimInterface(0)
102
154
 
@@ -330,7 +382,7 @@ class Analyzer:
330
382
  interleaved_dac_data[1::2] = right_dac_data_float
331
383
 
332
384
  # Convert to bytes, multiplying by max int value
333
- max_int_value = 2 ** 31 - 1
385
+ max_int_value = 2**31 - 1
334
386
  interleaved_dac_data = (interleaved_dac_data * max_int_value).astype(np.int32)
335
387
 
336
388
  num_ints_per_chunk = self.chunk_size // 4 # 32-bit ints, so 4 bytes per int
@@ -347,8 +399,8 @@ class Analyzer:
347
399
  try:
348
400
  for i in range(total_chunks):
349
401
  chunk = interleaved_dac_data[
350
- i * num_ints_per_chunk: (i + 1) * num_ints_per_chunk
351
- ]
402
+ i * num_ints_per_chunk : (i + 1) * num_ints_per_chunk
403
+ ]
352
404
  buffer = struct.pack("<%di" % len(chunk), *chunk)
353
405
  self.stream.write(buffer)
354
406
  finally:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyQa40x-i2c
3
- Version: 0.2.2
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
- "tox.ini",
4
- "requirements.txt",
5
- "requirements-test.txt",
6
- "requirements-dev.txt",
7
- "MANIFEST.in",
8
- "pyproject.toml",
3
+ ".github/workflows/code_checker.yml",
4
+ ".github/workflows/publish_package.yml",
5
+ ".github/workflows/unit_tests.yml",
6
+ ".gitignore",
9
7
  ".pylint_ignore",
10
8
  "LICENSE",
11
- "RELEASE_NOTES.md",
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",
20
+ "MANIFEST.in",
12
21
  "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",
22
+ "RELEASE_NOTES.md",
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",
29
34
  "src/PyQa40x/analyzer.py",
30
35
  "src/PyQa40x/analyzer_params.py",
31
- "src/PyQa40x/__init__.py",
32
- "src/PyQa40x/math_general.py",
36
+ "src/PyQa40x/bluetooth.py",
33
37
  "src/PyQa40x/control.py",
34
- "src/PyQa40x/wave_chirp.py",
35
- "src/PyQa40x/series_plotter.py",
38
+ "src/PyQa40x/cross_correlation.py",
36
39
  "src/PyQa40x/fft_processor.py",
40
+ "src/PyQa40x/freq_series_plotter.py",
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",
47
+ "src/PyQa40x/registers.py",
48
+ "src/PyQa40x/series_plotter.py",
49
+ "src/PyQa40x/sig_proc.py",
37
50
  "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"
51
+ "src/PyQa40x/wave.py",
52
+ "src/PyQa40x/wave_chirp.py",
53
+ "src/PyQa40x/wave_sine.py",
54
+ "tests/__init__.py",
55
+ "tests/test_template.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,8 +0,0 @@
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
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes