diff --git a/src/robot_interface/utils/microphone.py b/src/robot_interface/utils/microphone.py new file mode 100644 index 0000000..c8f5ee3 --- /dev/null +++ b/src/robot_interface/utils/microphone.py @@ -0,0 +1,54 @@ +import logging + +logger = logging.getLogger(__name__) + + +def choose_mic_interactive(audio): + """ + Choose a microphone to use, interactively in the CLI. + + :param audio: An instance of PyAudio to use. + :type audio: pyaudio.PyAudio + + :return: A dictionary from PyAudio containing information about the microphone to use, or None + if there is no microphone. + :rtype: dict | None + """ + device_count = audio.get_device_count() + if device_count == 0: return None + + print("Found {} audio devices:".format(device_count)) + for i in range(device_count): + print("- {}: {}".format(i, audio.get_device_info_by_index(i)["name"])) + + microphone_index = None + while microphone_index is None: + chosen = raw_input("Which device would you like to use?\n> ") + try: + chosen = int(chosen) + if chosen < 0 or chosen >= device_count: raise ValueError() + microphone_index = chosen + except ValueError: + print("Please enter a number between 0 and {}".format(device_count-1)) + + chosen_microphone = audio.get_device_info_by_index(microphone_index) + logger.info("Chose microphone \"{}\"".format(chosen_microphone["name"])) + return chosen_microphone + + +def choose_mic_default(audio): + """ + Get the system's default microphone to use. + + :param audio: An instance of PyAudio to use. + :type audio: pyaudio.PyAudio + + :return: A dictionary from PyAudio containing information about the microphone to use, or None + if there is no microphone. + :rtype: dict | None + """ + device_count = audio.get_device_count() + if device_count == 0: return None + + default_device = audio.get_default_input_device_info() + return default_device diff --git a/test/unit/test_microphone_utils.py b/test/unit/test_microphone_utils.py new file mode 100644 index 0000000..0114f73 --- /dev/null +++ b/test/unit/test_microphone_utils.py @@ -0,0 +1,107 @@ +import functools +import random +from StringIO import StringIO +import sys + +import pyaudio +import pytest + +from robot_interface.utils.microphone import choose_mic_default, choose_mic_interactive + + +@pytest.fixture +def pyaudio_instance(): + audio = pyaudio.PyAudio() + yield audio + + +def test_choose_mic_default(pyaudio_instance): + """ + The result must contain at least "index", as this is used to identify the microphone. + The "name" is used for logging, so it should also exist. + It must have one or more channels. + Lastly it must be capable of sending at least 16000 samples per second. + """ + result = choose_mic_default(pyaudio_instance) + assert "index" in result + assert isinstance(result["index"], long) + + assert "name" in result + assert isinstance(result["name"], (str, unicode)) + + assert "maxInputChannels" in result + assert isinstance(result["maxInputChannels"], long) + assert result["maxInputChannels"] > 0 + + assert "defaultSampleRate" in result + assert isinstance(result["defaultSampleRate"], float) + assert result["defaultSampleRate"] >= 16000 + + +def test_choose_mic_interactive_input_not_int(pyaudio_instance, mocker): + """ + First mock an input that's not an integer, then a valid integer. There should be no errors. + """ + mock_input = mocker.patch("__builtin__.raw_input", side_effect=["not an integer", "0"]) + fake_out = StringIO() + mocker.patch.object(sys, "stdout", fake_out) + + result = choose_mic_interactive(pyaudio_instance) + assert "index" in result + assert isinstance(result["index"], (int, long)) + assert result["index"] == 0 + + assert mock_input.called + + assert any(p.startswith("Please enter a number") for p in fake_out.getvalue().splitlines()) + + +def test_choose_mic_interactive_negative_index(pyaudio_instance, mocker): + """ + Make sure that the interactive method does not allow negative integers as input. + """ + mock_input = mocker.patch("__builtin__.raw_input", side_effect=["-1", "0"]) + fake_out = StringIO() + mocker.patch.object(sys, "stdout", fake_out) + + result = choose_mic_interactive(pyaudio_instance) + assert "index" in result + assert isinstance(result["index"], (int, long)) + assert result["index"] == 0 + + assert mock_input.called + + assert any(p.startswith("Please enter a number") for p in fake_out.getvalue().splitlines()) + + +def test_choose_mic_interactive_index_too_high(pyaudio_instance, mocker): + """ + Make sure that the interactive method does not allow indices higher than the highest mic index. + """ + real_count = pyaudio_instance.get_device_count() + mock_input = mocker.patch("__builtin__.raw_input", side_effect=[str(real_count), "0"]) + fake_out = StringIO() + mocker.patch.object(sys, "stdout", fake_out) + + result = choose_mic_interactive(pyaudio_instance) + assert "index" in result + assert isinstance(result["index"], (int, long)) + assert result["index"] == 0 + + assert mock_input.called + + assert any(p.startswith("Please enter a number") for p in fake_out.getvalue().splitlines()) + + +def test_choose_mic_interactive_random_index(pyaudio_instance, mocker): + """ + Get a random index from the list of available mics, make sure it's correct. + """ + real_count = pyaudio_instance.get_device_count() + random_index = random.randrange(real_count) + mocker.patch("__builtin__.raw_input", side_effect=[str(random_index)]) + + result = choose_mic_interactive(pyaudio_instance) + assert "index" in result + assert isinstance(result["index"], (int, long)) + assert result["index"] == random_index