From 51a3e840b6f5a7a8b2f9334b7dfcc64396f786d4 Mon Sep 17 00:00:00 2001 From: Abel Deuring Date: Thu, 19 May 2022 15:37:20 +0200 Subject: [PATCH 1/3] New module soundcard.utils that provides a function match_device(). Functions _match_device()/_match_soundcard() in the modules soundcard.coreaudio, soundcard.mediafoundation, soundcard.pulseaudio replaced with soundcard.utils.match_device(). --- soundcard/coreaudio.py | 29 ++++--------------------- soundcard/mediafoundation.py | 28 ++++-------------------- soundcard/pulseaudio.py | 41 ++++-------------------------------- soundcard/utils.py | 21 ++++++++++++++++++ 4 files changed, 33 insertions(+), 86 deletions(-) create mode 100644 soundcard/utils.py diff --git a/soundcard/coreaudio.py b/soundcard/coreaudio.py index e13ab9f..f9def1b 100644 --- a/soundcard/coreaudio.py +++ b/soundcard/coreaudio.py @@ -8,6 +8,8 @@ import threading import warnings +from soundcard.utils import match_device + _ffi = cffi.FFI() _package_dir, _ = os.path.split(__file__) with open(os.path.join(_package_dir, 'coreaudio.py.h'), 'rt') as f: @@ -60,7 +62,7 @@ def get_speaker(id): fuzzy-matched pattern for the speaker name. """ - return _match_device(id, all_speakers()) + return match_device(id, all_speakers()) def default_microphone(): @@ -79,30 +81,7 @@ def get_microphone(id, include_loopback=False): fuzzy-matched pattern for the microphone name. """ - return _match_device(id, all_microphones(include_loopback)) - - -def _match_device(id, devices): - """Find id in a list of devices. - - id can be a CoreAudio id, a substring of the device name, or a - fuzzy-matched pattern for the microphone name. - - """ - devices_by_id = {device.id: device for device in devices} - devices_by_name = {device.name: device for device in devices} - if id in devices_by_id: - return devices_by_id[id] - # try substring match: - for name, device in devices_by_name.items(): - if id in name: - return device - # try fuzzy match: - pattern = '.*'.join(id) - for name, device in devices_by_name.items(): - if re.match(pattern, name): - return device - raise IndexError('no device with id {}'.format(id)) + return match_device(id, all_microphones(include_loopback)) def get_name(): diff --git a/soundcard/mediafoundation.py b/soundcard/mediafoundation.py index bffba73..d964bdb 100644 --- a/soundcard/mediafoundation.py +++ b/soundcard/mediafoundation.py @@ -10,6 +10,8 @@ import numpy +from soundcard.utils import match_device + _ffi = cffi.FFI() _package_dir, _ = os.path.split(__file__) with open(os.path.join(_package_dir, 'mediafoundation.py.h'), 'rt') as f: @@ -123,7 +125,7 @@ def get_speaker(id): fuzzy-matched pattern for the speaker name. """ - return _match_device(id, all_speakers()) + return match_device(id, all_speakers()) def all_microphones(include_loopback=False): """A list of all connected microphones. @@ -151,29 +153,7 @@ def get_microphone(id, include_loopback=False): fuzzy-matched pattern for the microphone name. """ - return _match_device(id, all_microphones(include_loopback)) - -def _match_device(id, devices): - """Find id in a list of devices. - - id can be a WASAPI id, a substring of the device name, or a - fuzzy-matched pattern for the microphone name. - - """ - devices_by_id = {device.id: device for device in devices} - devices_by_name = {device.name: device for device in devices} - if id in devices_by_id: - return devices_by_id[id] - # try substring match: - for name, device in devices_by_name.items(): - if id in name: - return device - # try fuzzy match: - pattern = '.*'.join(id) - for name, device in devices_by_name.items(): - if re.match(pattern, name): - return device - raise IndexError('no device with id {}'.format(id)) + return match_device(id, all_microphones(include_loopback)) def _str2wstr(string): """Converts a Python str to a Windows WSTR_T.""" diff --git a/soundcard/pulseaudio.py b/soundcard/pulseaudio.py index b675238..d8193f4 100644 --- a/soundcard/pulseaudio.py +++ b/soundcard/pulseaudio.py @@ -8,6 +8,8 @@ import numpy import cffi +from soundcard.utils import match_device + _ffi = cffi.FFI() _package_dir, _ = os.path.split(__file__) with open(os.path.join(_package_dir, 'pulseaudio.py.h'), 'rt') as f: @@ -302,8 +304,7 @@ def get_speaker(id): speaker : _Speaker """ - speakers = _pulse.sink_list - return _Speaker(id=_match_soundcard(id, speakers)['id']) + return match_device(id, all_speakers()) def all_microphones(include_loopback=False, exclude_monitors=True): @@ -367,41 +368,7 @@ def get_microphone(id, include_loopback=False, exclude_monitors=True): ------- microphone : _Microphone """ - - if not exclude_monitors: - warnings.warn("The exclude_monitors flag is being replaced by the include_loopback flag", DeprecationWarning) - include_loopback = not exclude_monitors - - microphones = _pulse.source_list - return _Microphone(id=_match_soundcard(id, microphones, include_loopback)['id']) - - -def _match_soundcard(id, soundcards, include_loopback=False): - """Find id in a list of soundcards. - - id can be a pulseaudio id, a substring of the microphone name, or - a fuzzy-matched pattern for the microphone name. - """ - if not include_loopback: - soundcards_by_id = {soundcard['id']: soundcard for soundcard in soundcards - if not 'monitor' in soundcard['id']} - soundcards_by_name = {soundcard['name']: soundcard for soundcard in soundcards - if not 'monitor' in soundcard['id']} - else: - soundcards_by_id = {soundcard['id']: soundcard for soundcard in soundcards} - soundcards_by_name = {soundcard['name']: soundcard for soundcard in soundcards} - if id in soundcards_by_id: - return soundcards_by_id[id] - # try substring match: - for name, soundcard in soundcards_by_name.items(): - if id in name: - return soundcard - # try fuzzy match: - pattern = '.*'.join(id) - for name, soundcard in soundcards_by_name.items(): - if re.match(pattern, name): - return soundcard - raise IndexError('no soundcard with id {}'.format(id)) + return match_device(id, all_microphones(include_loopback)) def get_name(): diff --git a/soundcard/utils.py b/soundcard/utils.py new file mode 100644 index 0000000..ab335a7 --- /dev/null +++ b/soundcard/utils.py @@ -0,0 +1,21 @@ +def match_device(id, devices): + """Find id in a list of devices. + + id can be a platfom-specific id, a substring of the device name, or a + fuzzy-matched pattern for the microphone name. + + """ + devices_by_id = {device.id: device for device in devices} + devices_by_name = {device.name: device for device in devices} + if id in devices_by_id: + return devices_by_id[id] + # try substring match: + for name, device in devices_by_name.items(): + if id in name: + return device + # try fuzzy match: + pattern = '.*'.join(id) + for name, device in devices_by_name.items(): + if re.match(pattern, name): + return device + raise IndexError('no device with id {}'.format(id)) From 5b971e5f295957b85cabcf5b7af18936355cf83b Mon Sep 17 00:00:00 2001 From: Abel Deuring Date: Fri, 20 May 2022 00:38:48 +0200 Subject: [PATCH 2/3] 1. soundcard.utils.match_device() improved: - Distinguish between real devices and loopback devices. Try a lookup for real devices first; when one of them matches, return it before trying to find a matching loopback device. - When a device name for a loopback device is also a substring of the name of a real device, the loopback device with the completely matching name is returned. - Symbols with a special meaning in regular expressions are escaped when pattern for regex matching is built. - Call of re.match() replaced with re.search() so that the "search parameter" passed to match_device does not have to match the start of a device name. 2. Obsolete "import re" removed from modules soundcard.coreaudio, soundcard.mediafoundation, soundcard.pulseaudio. --- soundcard/coreaudio.py | 1 - soundcard/mediafoundation.py | 1 - soundcard/pulseaudio.py | 1 - soundcard/utils.py | 38 ++++++--- test_soundcard.py | 152 +++++++++++++++++++++++++++++++++++ 5 files changed, 180 insertions(+), 13 deletions(-) diff --git a/soundcard/coreaudio.py b/soundcard/coreaudio.py index f9def1b..ce12e6d 100644 --- a/soundcard/coreaudio.py +++ b/soundcard/coreaudio.py @@ -3,7 +3,6 @@ import numpy import collections import time -import re import math import threading import warnings diff --git a/soundcard/mediafoundation.py b/soundcard/mediafoundation.py index d964bdb..b1a0533 100644 --- a/soundcard/mediafoundation.py +++ b/soundcard/mediafoundation.py @@ -2,7 +2,6 @@ import os import cffi -import re import time import struct import collections diff --git a/soundcard/pulseaudio.py b/soundcard/pulseaudio.py index d8193f4..35600f2 100644 --- a/soundcard/pulseaudio.py +++ b/soundcard/pulseaudio.py @@ -2,7 +2,6 @@ import atexit import collections import time -import re import threading import warnings import numpy diff --git a/soundcard/utils.py b/soundcard/utils.py index ab335a7..9cfdb96 100644 --- a/soundcard/utils.py +++ b/soundcard/utils.py @@ -1,21 +1,39 @@ +import re + def match_device(id, devices): """Find id in a list of devices. - id can be a platfom-specific id, a substring of the device name, or a + id can be a platfom specific id, a substring of the device name, or a fuzzy-matched pattern for the microphone name. - """ devices_by_id = {device.id: device for device in devices} - devices_by_name = {device.name: device for device in devices} + real_devices_by_name = { + device.name: device for device in devices + if not getattr(device, 'isloopback', True)} + loopback_devices_by_name = { + device.name: device for device in devices + if getattr(device, 'isloopback', True)} if id in devices_by_id: return devices_by_id[id] + for device_map in real_devices_by_name, loopback_devices_by_name: + if id in device_map: + return device_map[id] # try substring match: - for name, device in devices_by_name.items(): - if id in name: - return device + for device_map in real_devices_by_name, loopback_devices_by_name: + for name, device in device_map.items(): + if id in name: + return device # try fuzzy match: - pattern = '.*'.join(id) - for name, device in devices_by_name.items(): - if re.match(pattern, name): - return device + id_parts = list(id) + # Escape symbols in the provided id that have a special meaning + # in regular expression to prevent syntax errors e.g. for + # unbalanced parentheses. + for special_re_char in r'.^$*+?{}\[]|()': + while special_re_char in id_parts: + id_parts[id_parts.index(special_re_char)] = '\\' + special_re_char + pattern = '.*'.join(id_parts) + for device_map in real_devices_by_name, loopback_devices_by_name: + for name, device in device_map.items(): + if re.search(pattern, name): + return device raise IndexError('no device with id {}'.format(id)) diff --git a/test_soundcard.py b/test_soundcard.py index 68d7949..a62d60b 100644 --- a/test_soundcard.py +++ b/test_soundcard.py @@ -3,6 +3,14 @@ import numpy import pytest + +if sys.platform == 'linux': + import soundcard.pulseaudio as platform_lib +elif sys.platform == 'darwin': + import soundcard.coreaudio as platform_lib +elif sys.platform == 'win32': + import soundcard.mediafoundation as platform_lib + skip_if_not_linux = pytest.mark.skipif(sys.platform != 'linux', reason='Only implemented for PulseAudio so far') ones = numpy.ones(1024) @@ -157,3 +165,147 @@ def test_loopback_multichannel_channelmap(loopback_speaker, loopback_microphone) assert right.mean() < 0 assert (left > 0.5).sum() == len(signal) assert (right < -0.5).sum() == len(signal) + + +class FakeMicrophone: + def __init__(self, id, name, isloopback): + self.id = id + self.name = name + self.isloopback = isloopback + + +fake_microphones = [ + FakeMicrophone( + 'alsa_output.usb-PCM2702-00.analog-stereo.monitor', + 'Monitor of PCM2702 16-bit stereo audio DAC Analog Stereo', + True), + FakeMicrophone( + 'alsa_output.pci-0000_00_1b.0.analog-stereo.monitor', + 'Monitor of Build-in Sound Device Analog Stereo', + True), + FakeMicrophone( + 'alsa_input.pci-0000_00_1b.0.analog-stereo', + 'Build-in Sound Device Analog Stereo', + False), + FakeMicrophone( + 'alsa_output.pci-0000_00_03.0.hdmi-stereo-extra1.monitor', + 'Monitor of Build-in Sound Device Digital Stereo (HDMI 2)', + True), + FakeMicrophone( + 'alsa_input.bluetooth-stuff.monitor', + 'Name with regex pitfalls [).', + True), + FakeMicrophone( + 'alsa_input.bluetooth-stuff', + 'Name with regex pitfalls [). Longer than than the lookback name.', + False), + ] + +@pytest.fixture +def mock_all_microphones(monkeypatch): + + def mocked_all_microphones(include_loopback=False, exclude_monitors=True): + return fake_microphones + + monkeypatch.setattr( + platform_lib, "all_microphones", mocked_all_microphones) + + +def test_get_microphone(mock_all_microphones): + # Internal IDs can be specified. + mic = soundcard.get_microphone('alsa_input.pci-0000_00_1b.0.analog-stereo') + assert mic == fake_microphones[2] + # No fuzzy matching for IDs. + with pytest.raises(IndexError) as exc_info: + soundcard.get_microphone('alsa_input.pci-0000_00_1b.0') + assert ( + exc_info.exconly() == + 'IndexError: no device with id alsa_input.pci-0000_00_1b.0') + + # The name of a microphone can be specified. + mic = soundcard.get_microphone('Build-in Sound Device Analog Stereo') + assert mic == fake_microphones[2] + + # Complete name matches have precedence over substring matches. + mic = soundcard.get_microphone('Name with regex pitfalls [).') + assert mic == fake_microphones[4] + + mic = soundcard.get_microphone('Name with regex pitfalls') + assert mic == fake_microphones[5] + + + # A substring of a device name can be specified. If the parameter passed + # to get_microphone() is a substring of more than one microphone name, + # real microphones are preferably returned. + mic = soundcard.get_microphone('Sound Device Analog') + assert mic == fake_microphones[2] + + # If none of the lookup methods above matches a device, a "fuzzy match" + # is tried. + mic = soundcard.get_microphone('Snd Dev Analog') + assert mic == fake_microphones[2] + + # "Fuzzy matching" uses a regular expression; symbols with a specail + # meaning in regexes are escaped. + mic = soundcard.get_microphone('regex pitfall [') + assert mic == fake_microphones[5] + + +class FakeSpeaker: + def __init__(self, id, name): + self.id = id + self.name = name + + +fake_speakers = [ + FakeSpeaker( + 'alsa_output.usb-PCM2702-00.analog-stereo', + 'PCM2702 16-bit stereo audio DAC Analog Stereo'), + FakeSpeaker( + 'alsa_output.pci-0000_00_1b.0.analog-stereo', + 'Build-in Sound Device Analog Stereo'), + FakeSpeaker( + 'alsa_output.pci-0000_00_03.0.hdmi-stereo-extra1', + 'Build-in Sound Device Digital Stereo (HDMI 2)'), + FakeSpeaker( + 'alsa_output.wire_fire_thingy', + r'A nonsensical name \[a-z]{3}'), + ] + +@pytest.fixture +def mock_all_speakers(monkeypatch): + + def mocked_all_speakers(include_loopback=False, exclude_monitors=True): + return fake_speakers + + monkeypatch.setattr( + platform_lib, "all_speakers", mocked_all_speakers) + +def test_get_speaker(mock_all_speakers): + # Internal IDs can be specified. + spk = soundcard.get_speaker('alsa_output.pci-0000_00_1b.0.analog-stereo') + assert spk == fake_speakers[1] + # No fuzzy matching for IDs. + with pytest.raises(IndexError) as exc_info: + soundcard.get_speaker('alsa_output.pci-0000_00_1b.0') + assert ( + exc_info.exconly() == + 'IndexError: no device with id alsa_output.pci-0000_00_1b.0') + + # The name of a speaker can be specified. + spk = soundcard.get_speaker('Build-in Sound Device Analog Stereo') + assert spk == fake_speakers[1] + + # Substrings of a device name can be specified. + spk = soundcard.get_speaker('Sound Device Analog') + assert spk == fake_speakers[1] + + # If none of the lookup methods above matches a device, a "fuzzy match" + # is tried. + spk = soundcard.get_speaker('Snd Dev Analog') + assert spk == fake_speakers[1] + + # "Fuzzy matching" uses a regular expression; symbols with a specail + # meaning in regexes are escaped. + spk = soundcard.get_speaker('nonsense {3') + assert spk == fake_speakers[3] From aad3356b9e218aa7c22c7b65042d9ffb4f05ffd2 Mon Sep 17 00:00:00 2001 From: Abel Deuring Date: Fri, 20 May 2022 15:03:51 +0200 Subject: [PATCH 3/3] soundcard.utils.match_device(): - check if an integer ID was provided before attempting to match strings. - re.escape() now used to escape possible special symbols in ID strings instead of checks against a handcrafted list of symbols. - tests: Fake devices with IDs and names as used in MacOS and Windows added. --- soundcard/utils.py | 12 +++---- test_soundcard.py | 88 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 91 insertions(+), 9 deletions(-) diff --git a/soundcard/utils.py b/soundcard/utils.py index 9cfdb96..3c3df37 100644 --- a/soundcard/utils.py +++ b/soundcard/utils.py @@ -18,19 +18,17 @@ def match_device(id, devices): for device_map in real_devices_by_name, loopback_devices_by_name: if id in device_map: return device_map[id] + # MacOS/coreaudio uses integer IDs where string operations of course + # make no sense. + if isinstance(id, int): + raise IndexError('no device with id {}'.format(id)) # try substring match: for device_map in real_devices_by_name, loopback_devices_by_name: for name, device in device_map.items(): if id in name: return device # try fuzzy match: - id_parts = list(id) - # Escape symbols in the provided id that have a special meaning - # in regular expression to prevent syntax errors e.g. for - # unbalanced parentheses. - for special_re_char in r'.^$*+?{}\[]|()': - while special_re_char in id_parts: - id_parts[id_parts.index(special_re_char)] = '\\' + special_re_char + id_parts = [re.escape(c) for c in id] pattern = '.*'.join(id_parts) for device_map in real_devices_by_name, loopback_devices_by_name: for name, device in device_map.items(): diff --git a/test_soundcard.py b/test_soundcard.py index a62d60b..e3a675d 100644 --- a/test_soundcard.py +++ b/test_soundcard.py @@ -175,30 +175,57 @@ def __init__(self, id, name, isloopback): fake_microphones = [ + # Linux/Alsa name and ID pattern. + # [0] FakeMicrophone( 'alsa_output.usb-PCM2702-00.analog-stereo.monitor', 'Monitor of PCM2702 16-bit stereo audio DAC Analog Stereo', True), + # [1] FakeMicrophone( 'alsa_output.pci-0000_00_1b.0.analog-stereo.monitor', 'Monitor of Build-in Sound Device Analog Stereo', True), + # [2] FakeMicrophone( 'alsa_input.pci-0000_00_1b.0.analog-stereo', 'Build-in Sound Device Analog Stereo', False), + # [3] FakeMicrophone( 'alsa_output.pci-0000_00_03.0.hdmi-stereo-extra1.monitor', 'Monitor of Build-in Sound Device Digital Stereo (HDMI 2)', True), + # Two completely made-up examples where a monitor name is the substring + # of the anme of a real device. + # [4] FakeMicrophone( 'alsa_input.bluetooth-stuff.monitor', 'Name with regex pitfalls [).', True), + # [5] FakeMicrophone( 'alsa_input.bluetooth-stuff', 'Name with regex pitfalls [). Longer than than the lookback name.', False), + + # MacOS/coreaudio ID and name patterns. + # [6] + FakeMicrophone(42, 'Built-in Microphone', False), + # [7] + FakeMicrophone(59, 'Samson GoMic', False), + + # Windows/mediafoundazion ID and name patterns. + # [8] + FakeMicrophone( + '{0.0.1.00000000}.{c0c95239-3a6c-427b-a788-9caeb13a7f43}', + 'Mikrofonarray (Realtek(R) Audio) False', + False), + # [9] + FakeMicrophone( + '{0.0.0.00000000}.{f08702cd-ee32-4c95-ac85-ff21a9d4d8ec}', + 'Lautsprecher (Realtek(R) Audio) True', + True), ] @pytest.fixture @@ -213,14 +240,34 @@ def mocked_all_microphones(include_loopback=False, exclude_monitors=True): def test_get_microphone(mock_all_microphones): # Internal IDs can be specified. + # Linux ID. mic = soundcard.get_microphone('alsa_input.pci-0000_00_1b.0.analog-stereo') assert mic == fake_microphones[2] + # Windows ID. + mic = soundcard.get_microphone( + '{0.0.1.00000000}.{c0c95239-3a6c-427b-a788-9caeb13a7f43}') + assert mic == fake_microphones[8] + # Mac ID. + mic = soundcard.get_microphone(42) + assert mic == fake_microphones[6] + # No fuzzy matching for IDs. + # Non-existing Linux ID. with pytest.raises(IndexError) as exc_info: soundcard.get_microphone('alsa_input.pci-0000_00_1b.0') assert ( exc_info.exconly() == 'IndexError: no device with id alsa_input.pci-0000_00_1b.0') + # Non-existing Windows ID. + with pytest.raises(IndexError) as exc_info: + soundcard.get_microphone('0.0.1.00000000}.{c0c95239-3a6c-427b-') + assert ( + exc_info.exconly() == + 'IndexError: no device with id 0.0.1.00000000}.{c0c95239-3a6c-427b-') + # Non-existing Mac ID. + with pytest.raises(IndexError) as exc_info: + mic = soundcard.get_microphone(13) + assert exc_info.exconly() == 'IndexError: no device with id 13' # The name of a microphone can be specified. mic = soundcard.get_microphone('Build-in Sound Device Analog Stereo') @@ -233,7 +280,6 @@ def test_get_microphone(mock_all_microphones): mic = soundcard.get_microphone('Name with regex pitfalls') assert mic == fake_microphones[5] - # A substring of a device name can be specified. If the parameter passed # to get_microphone() is a substring of more than one microphone name, # real microphones are preferably returned. @@ -245,7 +291,7 @@ def test_get_microphone(mock_all_microphones): mic = soundcard.get_microphone('Snd Dev Analog') assert mic == fake_microphones[2] - # "Fuzzy matching" uses a regular expression; symbols with a specail + # "Fuzzy matching" uses a regular expression; symbols with a special # meaning in regexes are escaped. mic = soundcard.get_microphone('regex pitfall [') assert mic == fake_microphones[5] @@ -258,18 +304,37 @@ def __init__(self, id, name): fake_speakers = [ + # Linux/Alsa name and ID pattern. + # [0] FakeSpeaker( 'alsa_output.usb-PCM2702-00.analog-stereo', 'PCM2702 16-bit stereo audio DAC Analog Stereo'), + # [1] FakeSpeaker( 'alsa_output.pci-0000_00_1b.0.analog-stereo', 'Build-in Sound Device Analog Stereo'), + # [2] FakeSpeaker( 'alsa_output.pci-0000_00_03.0.hdmi-stereo-extra1', 'Build-in Sound Device Digital Stereo (HDMI 2)'), + # [3] FakeSpeaker( 'alsa_output.wire_fire_thingy', r'A nonsensical name \[a-z]{3}'), + # Windows/mediafoundazion ID and name patterns. + # [4] + FakeSpeaker( + '{0.0.0.00000000}.{075a16c1-576e-419e-84c1-70d03e0d6276}', + 'BenQ PD2700U (Intel(R) Display-Audio)'), + # [5] + FakeSpeaker( + '{0.0.0.00000000}.{27473622-d168-4a9c-87c5-230c148c09c9}', + 'Lautsprecher (4- Samson GoMic)'), + # MacOS/coreaudio ID and name patterns. + # [6] + FakeSpeaker(49, 'Built-in Output'), + # [7] + FakeSpeaker(59, 'Samson GoMic'), ] @pytest.fixture @@ -283,14 +348,33 @@ def mocked_all_speakers(include_loopback=False, exclude_monitors=True): def test_get_speaker(mock_all_speakers): # Internal IDs can be specified. + # Linux spk = soundcard.get_speaker('alsa_output.pci-0000_00_1b.0.analog-stereo') assert spk == fake_speakers[1] + # Windows. + spk = soundcard.get_speaker( + '{0.0.0.00000000}.{075a16c1-576e-419e-84c1-70d03e0d6276}') + assert spk == fake_speakers[4] + # MacOS. + spk = soundcard.get_speaker(49) + assert spk == fake_speakers[6] + # No fuzzy matching for IDs. with pytest.raises(IndexError) as exc_info: soundcard.get_speaker('alsa_output.pci-0000_00_1b.0') assert ( exc_info.exconly() == 'IndexError: no device with id alsa_output.pci-0000_00_1b.0') + with pytest.raises(IndexError) as exc_info: + soundcard.get_speaker('{0.0.0.00000000') + assert ( + exc_info.exconly() == + 'IndexError: no device with id {0.0.0.00000000') + with pytest.raises(IndexError) as exc_info: + soundcard.get_speaker(-15) + assert ( + exc_info.exconly() == + 'IndexError: no device with id -15') # The name of a speaker can be specified. spk = soundcard.get_speaker('Build-in Sound Device Analog Stereo')