PyQa40x-i2c 0.1.0__py3-none-any.whl

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/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ # __init__.py in PyQa40x
2
+
3
+ from .analyzer import Analyzer
4
+ from .analyzer_params import AnalyzerParams
5
+ from .wave_sine import Wave, WaveSine
6
+ from .registers import Registers
7
+ from .control import Control
8
+ from .stream import Stream
9
+ from .series_plotter import SeriesPlotter
10
+ from .freq_series_plotter import FreqSeriesPlotter
11
+
12
+ try:
13
+ from ._version import version as __version__
14
+ except ImportError:
15
+ __version__ = "unknown"
16
+ from .fft_processor import FFTProcessor
17
+ from .sig_proc import SigProc
18
+ from .helpers import *
PyQa40x/_version.py ADDED
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.1.0'
22
+ __version_tuple__ = version_tuple = (0, 1, 0)
23
+
24
+ __commit_id__ = commit_id = None
PyQa40x/analyzer.py ADDED
@@ -0,0 +1,322 @@
1
+ import numpy as np
2
+ import usb1 # pip install libusb1
3
+ import struct
4
+ import atexit
5
+ import scipy.signal # pip install scipy
6
+
7
+ from PyQa40x.registers import Registers
8
+ from PyQa40x.control import Control
9
+ 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
+ from PyQa40x.helpers import *
14
+ from PyQa40x.analyzer_params import AnalyzerParams
15
+ from PyQa40x.bluetooth import BluetoothAudioDevice
16
+
17
+
18
+ class Analyzer:
19
+ def __init__(self):
20
+ """
21
+ Initializes the Analyzer class.
22
+ """
23
+ self.params: AnalyzerParams | None = None
24
+ self.context: usb1.USBContext | None = None
25
+ self.device: usb1.USBDeviceHandle | None = None
26
+ self.registers: Registers | None = None
27
+ self.control: Control | None = None
28
+ self.stream: Stream | None = None
29
+ self.cal_data: dict | None = None
30
+ self.bt_device: BluetoothAudioDevice | None = None
31
+ self._cleanup_registered = False
32
+
33
+ def init(
34
+ self,
35
+ sample_rate: int = 48000,
36
+ max_input_level: int = 0,
37
+ max_output_level: int = 18,
38
+ pre_buf: int = 2048,
39
+ post_buf: int = 2048,
40
+ fft_size: int = 16384,
41
+ window_type: str = "boxcar",
42
+ amplitude_unit: str = "dbv",
43
+ distortion_unit: str = "db",
44
+ bt_device_name: str = "",
45
+ ) -> AnalyzerParams:
46
+ """
47
+ Initializes the analyzer hardware with the specified parameters.
48
+
49
+ Args:
50
+ sample_rate (int): The sample rate for the device. Default is 48000. Valid values are 48000, 96000, 192000.
51
+ max_input_level (int): Maximum input level in dBV. Valid values are 0, 6, 12, 18, 24, 30, 36, 42.
52
+ max_output_level (int): Maximum output level in dBV. Valid values are 18, 8, -2, -12.
53
+ pre_buf (int): Size of the pre-buffer.
54
+ post_buf (int): Size of the post-buffer.
55
+ fft_size (int): Size of the FFT.
56
+ window_type (str): Type of window function to apply to the signal before FFT.
57
+ amplitude_unit (str): Unit for amplitude measurements, default is "dbv".
58
+ distortion_unit (str): Unit for distortion measurements, default is "db".
59
+ bt_device_name (str): Name of the Bluetooth device to use. Default is empty.
60
+
61
+ Returns:
62
+ AnalyzerParams: A class instance containing the hardware and parameter settings.
63
+ """
64
+ self.params = AnalyzerParams(
65
+ sample_rate,
66
+ max_input_level,
67
+ max_output_level,
68
+ pre_buf,
69
+ post_buf,
70
+ fft_size,
71
+ window_type,
72
+ )
73
+ self.context = usb1.USBContext()
74
+ self.context.open()
75
+
76
+ # Attempt to open QA402 or QA403 device
77
+ self.device = self.context.openByVendorIDAndProductID(0x16C0, 0x4E37) # QA402
78
+ if self.device is None:
79
+ self.device = self.context.openByVendorIDAndProductID(
80
+ 0x16C0, 0x4E39
81
+ ) # QA403
82
+ if self.device is None:
83
+ raise SystemExit("No QA402/QA403 analyzer found")
84
+ self.device.resetDevice()
85
+ self.device.claimInterface(0)
86
+
87
+ self.registers = Registers(self.device)
88
+ self.control = Control(self.registers)
89
+ self.stream = Stream(self.context, self.device, self.registers)
90
+
91
+ # Load calibration data
92
+ self.cal_data = self.control.load_calibration()
93
+
94
+ # Set input/output levels and sample rate
95
+ self.control.set_input(max_input_level)
96
+ self.control.set_output(max_output_level)
97
+ self.control.set_samplerate(sample_rate)
98
+
99
+ # Initialize Bluetooth device if a name is provided
100
+ if bt_device_name:
101
+ self.bt_device = BluetoothAudioDevice(bt_device_name, sample_rate)
102
+ if self.bt_device.device_index is None:
103
+ raise SystemExit("Failed to connect to the specified Bluetooth device")
104
+
105
+ # Register cleanup function to be called on exit
106
+ if not self._cleanup_registered:
107
+ atexit.register(self.cleanup)
108
+ self._cleanup_registered = True
109
+
110
+ return self.params
111
+
112
+ def cleanup(self):
113
+ """
114
+ Releases the USB device interface and closes the context.
115
+ """
116
+ try:
117
+ if self.device:
118
+ self.device.releaseInterface(0)
119
+ self.device = None
120
+ if self.bt_device:
121
+ self.bt_device.close()
122
+ self.bt_device = None
123
+ if self.context:
124
+ self.context.close()
125
+ self.context = None
126
+ except Exception as e:
127
+ print(f"An error occurred during cleanup: {e}")
128
+
129
+ def send_receive(self, left_wave: Wave, right_wave: Wave) -> tuple[Wave, Wave]:
130
+ """
131
+ Sends DAC data to the device and receives ADC data.
132
+
133
+ Args:
134
+ left_wave (Wave): Wave instance with left channel DAC data.
135
+ right_wave (Wave): Wave instance with right channel DAC data.
136
+
137
+ Returns:
138
+ tuple[Wave, Wave]: Tuple containing Wave instances with left and right channel ADC data.
139
+ """
140
+ left_dac_data = left_wave.get_buffer()
141
+ right_dac_data = right_wave.get_buffer()
142
+
143
+ # On QA402 and QA403, outputs need to be swapped
144
+ left_dac_data, right_dac_data = right_dac_data, left_dac_data
145
+
146
+ left_peak = np.max(left_dac_data)
147
+
148
+ # Get calibration factors for outgoing data
149
+ cal_dac_left, cal_dac_right = self.control.get_dac_cal(
150
+ self.cal_data, self.params.max_output_level
151
+ )
152
+
153
+ # Convert to dBFS. Note the 3 dB adjustment--dBFS is peak, while dBV is RMS
154
+ dac_dbfs_adjustment = 10 ** -((self.params.max_output_level + 3) / 20)
155
+
156
+ # Apply calibration and dBFS scaling
157
+ left_dac_data = left_dac_data * dac_dbfs_adjustment * cal_dac_left
158
+ right_dac_data = right_dac_data * dac_dbfs_adjustment * cal_dac_right
159
+
160
+ # Convert incoming doubles to float
161
+ left_dac_data_float = left_dac_data.astype(np.float32)
162
+ right_dac_data_float = right_dac_data.astype(np.float32)
163
+
164
+ # Interleave the left and right channels
165
+ interleaved_dac_data = np.empty(
166
+ (left_dac_data_float.size + right_dac_data_float.size,), dtype=np.float32
167
+ )
168
+ interleaved_dac_data[0::2] = left_dac_data_float
169
+ interleaved_dac_data[1::2] = right_dac_data_float
170
+
171
+ # Convert to bytes, multiplying by max int value
172
+ max_int_value = 2**31 - 1
173
+ interleaved_dac_data = (interleaved_dac_data * max_int_value).astype(np.int32)
174
+
175
+ # Pack the data into chunks of 16k bytes
176
+ chunk_size = 16384 # 16k bytes
177
+ num_ints_per_chunk = chunk_size // 4 # 32-bit ints, so 4 bytes per int
178
+ total_chunks = len(interleaved_dac_data) // num_ints_per_chunk
179
+
180
+ self.stream.start()
181
+
182
+ # Play wave on Bluetooth if available
183
+ bt_thread = None
184
+ if self.bt_device:
185
+ # Play both left and right waves on Bluetooth
186
+ bt_thread = self.bt_device.play_wave_background(left_wave, right_wave)
187
+
188
+ try:
189
+ for i in range(total_chunks):
190
+ chunk = interleaved_dac_data[
191
+ i * num_ints_per_chunk : (i + 1) * num_ints_per_chunk
192
+ ]
193
+ buffer = struct.pack("<%di" % len(chunk), *chunk)
194
+ self.stream.write(buffer)
195
+ finally:
196
+ self.stream.stop()
197
+
198
+ # Collect ADC data. This is bytes
199
+ interleaved_adc_data = self.stream.collect_remaining_adc_data()
200
+
201
+ # Wait for the Bluetooth thread to finish after processing
202
+ if bt_thread:
203
+ bt_thread.join()
204
+
205
+ # Convert collected ADC data back to int
206
+ interleaved_adc_data = np.frombuffer(interleaved_adc_data, dtype=np.int32)
207
+
208
+ # Separate interleaved ADC data into left and right channels. This is int
209
+ left_adc_data_int = interleaved_adc_data[0::2]
210
+ right_adc_data_int = interleaved_adc_data[1::2]
211
+
212
+ # Convert left and right channels back to double
213
+ left_adc_data = left_adc_data_int.astype(np.float64) / max_int_value
214
+ right_adc_data = right_adc_data_int.astype(np.float64) / max_int_value
215
+
216
+ # Get calibration factors for incoming data
217
+ cal_adc_left, cal_adc_right = self.control.get_adc_cal(
218
+ self.cal_data, self.params.max_input_level
219
+ )
220
+
221
+ # Convert from dBFS to dBV. Note the 6 dB factor--the ADC is differential
222
+ adc_dbfs_correction = 10 ** ((self.params.max_input_level - 6) / 20)
223
+
224
+ # Apply calibration and dBFS scaling
225
+ left_adc_data = left_adc_data * cal_adc_left * adc_dbfs_correction
226
+ right_adc_data = right_adc_data * cal_adc_right * adc_dbfs_correction
227
+
228
+ # Ensure the buffer matches the expected shape. Is this needed?
229
+ expected_shape = (
230
+ self.params.pre_buf + self.params.fft_size + self.params.post_buf,
231
+ )
232
+ if left_adc_data.shape[0] != expected_shape[0]:
233
+ left_adc_data = np.resize(left_adc_data, expected_shape)
234
+ if right_adc_data.shape[0] != expected_shape[0]:
235
+ right_adc_data = np.resize(right_adc_data, expected_shape)
236
+
237
+ # Create instances of Wave with the full buffers
238
+ left_wave = Wave(self.params)
239
+ right_wave = Wave(self.params)
240
+ left_wave.set_buffer(left_adc_data)
241
+ right_wave.set_buffer(right_adc_data)
242
+
243
+ return left_wave, right_wave
244
+
245
+ @staticmethod
246
+ def list_audio_devices_by_sample_rate(target_sample_rate: int):
247
+ """List all available audio devices that support a specific sample rate by calling the method from BluetoothAudioDevice."""
248
+ BluetoothAudioDevice.list_audio_devices_by_sample_rate(target_sample_rate)
249
+
250
+ def capture(self, total_samples: int) -> tuple[np.ndarray, np.ndarray]:
251
+ """
252
+ Captures ``total_samples`` samples from both ADC input channels without
253
+ playing anything on the DAC outputs. Unlike ``send_receive()``, this
254
+ method:
255
+
256
+ * Starts the USB stream **once** for the entire capture — no stop/restart
257
+ between chunks, so no audio is ever lost mid-recording.
258
+ * Sends zero-filled DAC data (silence) only to satisfy the hardware
259
+ protocol; DAC output remains at 0 V throughout.
260
+ * Returns raw calibrated Volt arrays — **no pre/post guard samples are
261
+ discarded** and the length is exactly ``total_samples``.
262
+
263
+ Args:
264
+ total_samples (int): Number of samples to capture per channel.
265
+ At 48 kHz, 1 sample = 1/48000 s. There is no upper limit
266
+ imposed by this method, but the host must have enough RAM to
267
+ hold the full recording (2 × total_samples × 8 bytes for
268
+ float64).
269
+
270
+ Returns:
271
+ tuple[np.ndarray, np.ndarray]: ``(left_volts, right_volts)`` —
272
+ two float64 arrays each of length ``total_samples``, values
273
+ in Volts (calibrated, same scale as ``send_receive()``).
274
+ """
275
+ chunk_bytes = 16384 # 16 kB per USB transfer
276
+ bytes_per_stereo_sample = 8 # 2 × int32
277
+ need_bytes = total_samples * bytes_per_stereo_sample
278
+
279
+ # Number of DAC write / ADC read pairs needed to cover need_bytes.
280
+ # Each pair transfers chunk_bytes ADC bytes.
281
+ n_chunks = -(-need_bytes // chunk_bytes) # ceiling division
282
+
283
+ max_int_value = 2**31 - 1
284
+
285
+ # -- single stream start ------------------------------------------------
286
+ self.stream.start()
287
+ try:
288
+ # Submit all zero-DAC / ADC-read pairs up front so the USB host
289
+ # controller can pipeline them; no stop/restart in the middle.
290
+ for _ in range(n_chunks):
291
+ self.stream.write_zeros(chunk_bytes)
292
+
293
+ # Block until we have exactly the bytes we need
294
+ raw_bytes = self.stream.collect_adc_exact(need_bytes)
295
+ finally:
296
+ self.stream.stop()
297
+ # -- stream stopped -----------------------------------------------------
298
+
299
+ # Deserialise interleaved int32 ADC data
300
+ interleaved = np.frombuffer(raw_bytes, dtype=np.int32)
301
+
302
+ left_int = interleaved[0::2]
303
+ right_int = interleaved[1::2]
304
+
305
+ # Trim to exact length (collect_adc_exact already trims, but be safe)
306
+ left_int = left_int[:total_samples]
307
+ right_int = right_int[:total_samples]
308
+
309
+ # Convert int32 → float64 in the range ±1
310
+ left_f = left_int.astype(np.float64) / max_int_value
311
+ right_f = right_int.astype(np.float64) / max_int_value
312
+
313
+ # Apply ADC calibration and dBFS → Volts conversion
314
+ cal_adc_left, cal_adc_right = self.control.get_adc_cal(
315
+ self.cal_data, self.params.max_input_level
316
+ )
317
+ adc_dbfs_correction = 10 ** ((self.params.max_input_level - 6) / 20)
318
+
319
+ left_volts = left_f * cal_adc_left * adc_dbfs_correction
320
+ right_volts = right_f * cal_adc_right * adc_dbfs_correction
321
+
322
+ return left_volts, right_volts
@@ -0,0 +1,66 @@
1
+ import scipy.signal # pip install scipy
2
+ import numpy as np
3
+
4
+ class AnalyzerParams:
5
+ def __init__(self, sample_rate: int = 192000, max_input_level: int = 0, max_output_level: int = 18,
6
+ pre_buf: int = 2048, post_buf: int = 2048, fft_size: int = 16384, window_type='boxcar'):
7
+ """
8
+ Initializes the AnalyzerParams class with default or specified parameters.
9
+
10
+ Args:
11
+ sample_rate (int): Sample rate for the hardware. Valid values are 48000, 96000, 192000.
12
+ max_input_level (int): Maximum input level in dBV. Valid values are 0, 6, 12, 18, 24, 30, 36, 42.
13
+ max_output_level (int): Maximum output level in dBV. Valid values are 18, 8, -2, -12.
14
+ pre_buf (int): Size of the pre-buffer.
15
+ post_buf (int): Size of the post-buffer.
16
+ fft_size (int): Size of the FFT.
17
+ window_type (str): Type of window function to apply to the signal before FFT.
18
+ """
19
+ self.sample_rate: int = sample_rate
20
+ self.max_input_level: int = max_input_level
21
+ self.max_output_level: int = max_output_level
22
+ self.pre_buf: int = pre_buf
23
+ self.post_buf: int = post_buf
24
+ self.fft_size: int = fft_size
25
+
26
+ self.window_type = window_type
27
+ self.window = scipy.signal.get_window(self.window_type, self.fft_size)
28
+ mean_w = np.mean(self.window)
29
+ self.ACF = 1 / mean_w
30
+ rms_w = np.sqrt(np.mean(self.window ** 2))
31
+ self.ECF = 1 / rms_w
32
+
33
+ def __str__(self) -> str:
34
+ """
35
+ Returns a string representation of the AnalyzerParams instance.
36
+
37
+ Returns:
38
+ str: String representation of the parameters formatted as a table.
39
+ """
40
+ params = [
41
+ ("Sample Rate", f"{self.sample_rate} Hz"),
42
+ ("Max Input Level", f"{self.max_input_level} dBV"),
43
+ ("Max Output Level", f"{self.max_output_level} dBV"),
44
+ ("Pre Buffer", f"{self.pre_buf}"),
45
+ ("Post Buffer", f"{self.post_buf}"),
46
+ ("Buffer Size", f"{self.fft_size}"),
47
+ ("Duration", f"{self.fft_size/self.sample_rate:0.2f} sec"),
48
+ ("Window Type", f"{self.window_type}")
49
+ ]
50
+
51
+ col_widths = [max(len(item[i]) for item in params) for i in range(2)]
52
+ col_widths[1] += 5 # Add padding
53
+
54
+ table = "===== ACQUISITION PARAMETERS =====\n"
55
+ rows = len(params)
56
+ cols = 3
57
+
58
+ for row in range(0, rows, cols):
59
+ row_str = ""
60
+ for col in range(cols):
61
+ if row + col < rows:
62
+ name, value = params[row + col]
63
+ row_str += f"{name.ljust(col_widths[0])} : {value.ljust(col_widths[1])} "
64
+ table += row_str.strip() + "\n"
65
+
66
+ return table
PyQa40x/bluetooth.py ADDED
@@ -0,0 +1,134 @@
1
+ import pyaudio # pip install pyaudio
2
+ import threading
3
+ import numpy as np
4
+ from PyQa40x.wave import Wave
5
+
6
+ class BluetoothAudioDevice:
7
+ def __init__(self, device_name: str, sample_rate: int, debug: bool = False):
8
+ """
9
+ Initializes the BluetoothAudioDevice.
10
+
11
+ Args:
12
+ device_name (str): Name of the target Bluetooth audio device.
13
+ sample_rate (int): Sample rate for playback.
14
+ debug (bool): If True, print detailed debug information. Default is False.
15
+ """
16
+ self.device_name = device_name
17
+ self.sample_rate = sample_rate
18
+ self.debug = debug
19
+ self.device_index = None
20
+ self.p = pyaudio.PyAudio()
21
+
22
+ # Attempt to find and open the device
23
+ self.device_index = self._find_device()
24
+ if self.device_index is None:
25
+ print(f"Failed to open audio device '{self.device_name}' with the required sample rate {self.sample_rate} Hz.")
26
+
27
+ def _find_device(self):
28
+ """Find and return the index of the target audio device if it supports the required sample rate."""
29
+ target_device_index = None
30
+ if self.debug:
31
+ print("Enumerating audio devices...\n")
32
+
33
+ for i in range(self.p.get_device_count()):
34
+ info = self.p.get_device_info_by_index(i)
35
+ if self.debug:
36
+ print(f"Device {i}: {info['name']}")
37
+
38
+ if self.device_name in info['name']:
39
+ if self.debug:
40
+ print(f" Checking device '{info['name']}' for sample rate {self.sample_rate} Hz...")
41
+ try:
42
+ # Attempt to open a stream to check if the sample rate is supported
43
+ stream = self.p.open(format=pyaudio.paFloat32,
44
+ channels=2,
45
+ rate=self.sample_rate,
46
+ output=True,
47
+ output_device_index=i)
48
+ stream.close()
49
+ if self.debug:
50
+ print(f" -> Device {i} supports the required sample rate.")
51
+ if target_device_index is None:
52
+ target_device_index = i
53
+ except Exception as e:
54
+ if self.debug:
55
+ print(f" -> Device {i} does not support the required sample rate. ({str(e)})")
56
+ return target_device_index
57
+
58
+ def play_wave_background(self, left_wave: Wave, right_wave: Wave):
59
+ """Play left and right Wave objects in the background."""
60
+ if self.device_index is None:
61
+ print("No valid audio device found. Cannot play wave.")
62
+ return None
63
+
64
+ # Start playing the wave in a separate thread
65
+ thread = threading.Thread(target=self._play_wave, args=(left_wave, right_wave, self.sample_rate, self.device_index))
66
+ thread.start()
67
+ return thread
68
+
69
+ def _play_wave(self, left_wave: Wave, right_wave: Wave, sample_rate: int, device_index: int):
70
+ """Play a stereo Wave object."""
71
+ stream = self.p.open(format=pyaudio.paFloat32,
72
+ channels=2, # Stereo playback
73
+ rate=sample_rate,
74
+ output=True,
75
+ output_device_index=device_index)
76
+
77
+ actual_sample_rate = stream._rate
78
+ if self.debug:
79
+ print(f"Requested Sample Rate: {sample_rate}")
80
+ print(f"Actual Sample Rate: {actual_sample_rate}")
81
+
82
+ # Get buffers for left and right waves
83
+ left_buffer = left_wave.get_buffer().astype(np.float32)
84
+ right_buffer = right_wave.get_buffer().astype(np.float32)
85
+
86
+ # Ensure both buffers are the same length
87
+ min_length = min(len(left_buffer), len(right_buffer))
88
+ left_buffer = left_buffer[:min_length]
89
+ right_buffer = right_buffer[:min_length]
90
+
91
+ # Interleave left and right buffers for stereo output
92
+ interleaved = np.empty((min_length * 2,), dtype=np.float32)
93
+ interleaved[0::2] = left_buffer
94
+ interleaved[1::2] = right_buffer
95
+
96
+ # Convert wave data to float32 format and play
97
+ stream.write(interleaved.tobytes())
98
+ stream.stop_stream()
99
+ stream.close()
100
+
101
+ def close(self):
102
+ """Terminate the PyAudio instance."""
103
+ self.p.terminate()
104
+
105
+ @staticmethod
106
+ def list_audio_devices():
107
+ """List all available audio devices by name."""
108
+ p = pyaudio.PyAudio()
109
+ print("Available audio devices:\n")
110
+ for i in range(p.get_device_count()):
111
+ info = p.get_device_info_by_index(i)
112
+ print(f"Device {i}: {info['name']}")
113
+ p.terminate()
114
+
115
+ @staticmethod
116
+ def list_audio_devices_by_sample_rate(target_sample_rate: int):
117
+ """List all available audio devices that support a specific sample rate."""
118
+ p = pyaudio.PyAudio()
119
+ print(f"Audio devices supporting {target_sample_rate} Hz sample rate:\n")
120
+ for i in range(p.get_device_count()):
121
+ info = p.get_device_info_by_index(i)
122
+ try:
123
+ # Check if the device supports the specified sample rate
124
+ stream = p.open(format=pyaudio.paFloat32,
125
+ channels=2, # Check for stereo support
126
+ rate=target_sample_rate,
127
+ output=True,
128
+ output_device_index=i)
129
+ stream.close()
130
+ print(f"Device {i}: {info['name']} supports {target_sample_rate} Hz")
131
+ except Exception:
132
+ # Device does not support the specified sample rate
133
+ continue
134
+ p.terminate()