Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

get_microphone(), get_speaker(): Improved comparison between the call parameter "id" and existing device names #147

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion soundcard/coreaudio.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import numpy
import collections
import time
import re
import math
import threading
import warnings
Expand Down
1 change: 0 additions & 1 deletion soundcard/mediafoundation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import os
import cffi
import re
import time
import struct
import collections
Expand Down
1 change: 0 additions & 1 deletion soundcard/pulseaudio.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import atexit
import collections
import time
import re
import threading
import warnings
import numpy
Expand Down
38 changes: 28 additions & 10 deletions soundcard/utils.py
Original file line number Diff line number Diff line change
@@ -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)}
bastibe marked this conversation as resolved.
Show resolved Hide resolved
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'.^$*+?{}\[]|()':
bastibe marked this conversation as resolved.
Show resolved Hide resolved
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))
152 changes: 152 additions & 0 deletions test_soundcard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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]