Repository: psychopy/psychopy

Issue:
pyo: error when parsing devices
In the following method of the pyo backend, depending on the connected devices, it can fail to parse the device list:

https://github.com/psychopy/psychopy/blob/0b0d4f3c26584ecf720c44406dbdecbd69b75ea4/psychopy/sound/backend_pyo.py#L77-L89

Traceback:

```
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/elodie/.local/lib/python3.6/site-packages/psychopy/sound/backend_pyo.py", line 120, in get_input_devices
    devices = _query_devices()
  File "/home/elodie/.local/lib/python3.6/site-packages/psychopy/sound/backend_pyo.py", line 86, in _query_devices
    info[v.strip().split(": ")[0]] = v.strip().split(": ")[1]
IndexError: list index out of range
```

The method `pyo.pa_list_devices()` returns a string, which is then split by line and each line is parsed, splitted by commas.
Here's an example of device (on Ubuntu) which causes an error:

```
AUDIO devices:
0: IN, name: Logitech USB Headset: Audio (hw:1,0), host api index: 0, default sr: 44100 Hz, latency: 0.008707 s
0: OUT, name: Logitech USB Headset: Audio (hw:1,0), host api index: 0, default sr: 44100 Hz, latency: 0.008707 s
1: IN, name: sysdefault, host api index: 0, default sr: 48000 Hz, latency: 0.021333 s
1: OUT, name: sysdefault, host api index: 0, default sr: 48000 Hz, latency: 0.021333 s
2: OUT, name: front, host api index: 0, default sr: 44100 Hz, latency: 0.008707 s
3: OUT, name: surround40, host api index: 0, default sr: 44100 Hz, latency: 0.008707 s
4: OUT, name: surround51, host api index: 0, default sr: 44100 Hz, latency: 0.008707 s
5: OUT, name: surround71, host api index: 0, default sr: 44100 Hz, latency: 0.008707 s
6: OUT, name: hdmi, host api index: 0, default sr: 44100 Hz, latency: 0.008707 s
7: IN, name: default, host api index: 0, default sr: 48000 Hz, latency: 0.021333 s
7: OUT, name: default, host api index: 0, default sr: 48000 Hz, latency: 0.021333 s
8: OUT, name: dmix, host api index: 0, default sr: 48000 Hz, latency: 0.021333 s
9: IN, name: /dev/dsp, host api index: 1, default sr: 44100 Hz, latency: 0.008707 s
9: OUT, name: /dev/dsp, host api index: 1, default sr: 44100 Hz, latency: 0.008707 s
```

As the line `0: IN, name: Logitech USB Headset: Audio (hw:1,0), host api index: 0, default sr: 44100 Hz, latency: 0.008707 s` is split by commas, at some point, it is confused by `(hw:1,0)` in `name: Logitech USB Headset: Audio (hw:1,0)`.

I have no background on psychopy and pyo, but I wonder if `_query_devices` couldn't be replaced by [`pyo.pa_get_devices_infos()`](https://github.com/belangeo/pyo/blob/274ed61d6664d44d23416957e781b8afbb559589/src/engine/pyomodule.c#L139)?


Discussion:
Note: some forums reports may be related: https://discourse.psychopy.org/t/indexerror-list-index-out-of-range-when-opening-preferences/14436, https://discourse.psychopy.org/t/cant-record-sound-in-experiment-on-psychopy/13113/3, https://discourse.psychopy.org/t/error-with-microphone-component-on-psychopy-2020/13168/10

Resolving patch:
diff --git a/psychopy/sound/backend_pyo.py b/psychopy/sound/backend_pyo.py
--- a/psychopy/sound/backend_pyo.py
+++ b/psychopy/sound/backend_pyo.py
@@ -12,6 +12,7 @@
 import os
 import atexit
 import threading
+from itertools import chain
 from numpy import float64
 from psychopy import prefs, exceptions
 from psychopy import core, logging
@@ -74,57 +75,29 @@ def _bestDriver(devNames, devIDs):
         return None, None
 
 
-def _query_devices():
-    f = StringIO()
-    with redirect_stdout(f):
-        pyo.pa_list_devices()
-    s = f.getvalue().strip().split("\n")[1:]
-    devices = []
-    for device in s:
-        info = {}
-        for v in device.split(",")[1:]:
-            info[v.strip().split(": ")[0]] = v.strip().split(": ")[1]
-        info["id"], info["type"] = device.split(",")[0].split(": ")
-        devices.append(info)
-    return devices
-
-
 def get_devices_infos():
-    devices = _query_devices()
-    in_devices = {}
-    out_devices = {}
-    for device in devices:
-        param = {'host api index': device['host api index'],
-                 'latency': device['latency'],
-                 'default sr': device['default sr'],
-                 'name': device['name']}
-        if device['type'] == 'IN':
-            in_devices[int(device["id"])] = param
-        if device['type'] == 'OUT':
-            out_devices[int(device["id"])] = param
+    in_devices, out_devices = pyo.pa_get_devices_infos()
+    for index, device in chain(in_devices.items(), out_devices.items()):
+        device.update({
+            'default sr': '{} Hz'.format(device['default sr']),
+            'host api index': str(device['host api index']),
+            'latency': '{} s'.format(round(device['latency'], 6)),
+        })
     return (in_devices, out_devices)
 
 
 def get_output_devices():
-    devices = _query_devices()
-    names = []
-    ids = []
-    for device in devices:
-        if device['type'] == 'OUT':
-            names.append(device['name'])
-            ids.append(int(device["id"]))
-    return (names, ids)
+    _, out_devices = get_devices_infos()
+    return tuple(zip(*[
+        (device['name'], dev_id) for dev_id, device in out_devices.items()
+    ]))
 
 
 def get_input_devices():
-    devices = _query_devices()
-    names = []
-    ids = []
-    for device in devices:
-        if device['type'] == 'IN':
-            names.append(device['name'])
-            ids.append(int(device["id"]))
-    return (names, ids)
+    in_devices, _ = get_devices_infos()
+    return tuple(zip(*[
+        (device['name'], dev_id) for dev_id, device in in_devices.items()
+    ]))
 
 
 def getDevices(kind=None):


Test patch: