Skip to content
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)}
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))
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]