Я почти каждый день слушаю музыку на смартфоне и использую кнопки управления на гарнитуре. Но мне всегда не нравилась одна вещь. Я прихожу домой, продолжая прослушивание, гарнитура подключается к домашнему ПК — и внезапно кнопки перестают работать.
Конечно, я погуглил решение этой проблемы. К сожалению, на Windows эта замечательная функция не слишком поддерживается. Пара минут поиска дали только мутные упоминания на Stack Overflow о звуковых картах и сообщения некоторых людей, что на их ноутбуках всё работает нормально.
Меня это не испугало — и я решил принять проблему как интересный вызов: можно ли создать какую-то программу для активации кнопок управления, если аппаратной поддержки для них вообще нет? Ответ — да, можно. И вот как сделать это за полчаса.
Первое, что нужно понять — как работают кнопки гарнитуры. Быстрый поиск в интернете нашёл эту спецификацию из документации Android. Там есть диаграмма.
Как можно понять, при нажатии кнопки на гарнитуре замыкается цепь на одном из резисторов. Особого внимания заслуживает Кнопка A (Play/Pause/Hook) с сопротивлением 0 Ом, то есть замыканием микрофона. Если мы способны обнаружить короткое замыкание микрофона, то так сможем определить нажатие кнопки Play/Pause.
Прежде чем начать программировать, хотелось бы проверить разумность наших рассуждений в принципе. То есть того, что по сигналу с микрофона можно определить нажатие кнопки Play/Pause. К счастью, для этого достаточно просто записать звук на компьютере и посмотреть на результат. Я запустил Audacity, нажал во время записи кнопку Play/Pause — и получил такой сигнал.
Бинго
Как видим, нажатие кнопки очевидно отражается в форме сигнала: внезапное падение до −1 с последующим внезапным переходом к 1 и постепенным уменьшением до 0. Интуитивно по спецификации я бы предположил, что сигнал подскочит до 1 и останется там, пока кнопку не отпустить, но в реальности выглядит иначе. Тем не менее, такую картинку всё равно легко обнаружить, если захватить аудиопоток с микрофона.
Зная способ, как обнаружить нажатие кнопок на гарнитуре, можно подумать о главной цели: как управлять плеером на рабочем столе с помощью кнопок гарнитуры.
Первый шаг — обнаружение нажатия кнопки. Для этого нужно захватить аудиопоток с микрофона и обнаружить отчётливую подпись, которую мы видели ранее. Для простоты реализуем решение на Python. После ещё одного небольшого поиска в интернете я нашёл пакет под названием sounddevice, который позволяет абстрагироваться от самой трудной части — реального аудиозахвата с микрофона.
Немножко кодирования даёт нам следующее:
Такой код непрерывно выдаёт среднее значение каждой партии образцов. Мы установили частоту дискретизации 1000, что ужасно мало для обработки звука (обычно используется 44100), но нам в реальности не нужна большая точность. Размер блока определяет, сколько сэмплов в буфере инициируют обратный вызов. Опять же, мы установили очень низкие значения. Размер блока 100 и частота дискретизации 1000 фактически означает срабатывание 10 раз в секунду, где при каждом вызове обрабатывается только 100 сэмплов.
Теперь мы захватываем аудиопоток и можно реализовать реальный механизм для обнаружения нажатия кнопки. Напомним, что сигнал подскакивает до 1 всякий раз при нажатии. Это подсказывает самый простой способ обнаружения: если у N последовательных блоков значения сигнала выше 0,9, то есть нажатие.
Реализуем алгоритм в нашей функции:
По сути мы запустили внутренний счётчик, сколько обработанных блоков отвечают пороговому требованию, которое просто установили на 0,9, предусмотрев неизбежное зашумление образца. Если блок не удовлетворяет требованию, счётчик сбрасывается — и мы начинаем заново. Переменная
Теперь осталось только заменить в реальном коде комментарий “The button was pressed!”, чтобы управлять воспроизведением звука в Windows. Снова погуглим, чтобы разобраться, как это сделать: оказывается, можно управлять воспроизведением, имитируя нажатие клавиш соответствующими кодами виртуальных клавиш.
Оказалось, что имитировать нажатия клавиш очень легко с помощью пакета pywin32, который является просто оболочкой Python для Windows API. Собрав всё вместе, мы можем создать следующую функцию:
И у нас получилось! Обращение к функции
Тесты показали, что код работает на удивление хорошо. Единственное различие между функциональностью на Android и Windows заключается в небольшой задержке при нажатии на кнопку, но с этим можно жить.
И вот что получилось
Скрипт Python состоит из 51 строки, которые активируют кнопки гарнитуры Android в Windows. Окончательный исходный код этого проекта лежит на Github.
После счастливого использования программы в течение нескольких часов я заметил серьёзную проблему:
Программа использует почти 30% CPU! Очевидно, это неприемлемо при длительной работе, что-то нужно делать. Посмотрев на код, я понял, что основной поток находится в состоянии ожидания в основном цикле, хотя там ничего не происходит. Наиболее логичное решение — просто усыпить поток навсегда: поскольку колбэк вызывается автоматически, нам всё равно не нужен цикл.
Я также не хотел запускать скрипт Python вручную после каждого запуска компьютера. К счастью Python для Windows поставляется с полезной утилитой pythonw.exe, которая запускает процесс «демона» без подключенного терминала. Размещаем ярлык к этому процессу в каталоге Microsoft\Windows\Start Menu\Programs\Startup, указав наш скрипт в качестве первого аргумента — тогда приложение автоматически запускается и незаметно работает в фоновом режиме.
Конечно, я погуглил решение этой проблемы. К сожалению, на Windows эта замечательная функция не слишком поддерживается. Пара минут поиска дали только мутные упоминания на Stack Overflow о звуковых картах и сообщения некоторых людей, что на их ноутбуках всё работает нормально.
Меня это не испугало — и я решил принять проблему как интересный вызов: можно ли создать какую-то программу для активации кнопок управления, если аппаратной поддержки для них вообще нет? Ответ — да, можно. И вот как сделать это за полчаса.
Как работают кнопки гарнитуры Android
Первое, что нужно понять — как работают кнопки гарнитуры. Быстрый поиск в интернете нашёл эту спецификацию из документации Android. Там есть диаграмма.
Как можно понять, при нажатии кнопки на гарнитуре замыкается цепь на одном из резисторов. Особого внимания заслуживает Кнопка A (Play/Pause/Hook) с сопротивлением 0 Ом, то есть замыканием микрофона. Если мы способны обнаружить короткое замыкание микрофона, то так сможем определить нажатие кнопки Play/Pause.
Проверка гипотезы
Прежде чем начать программировать, хотелось бы проверить разумность наших рассуждений в принципе. То есть того, что по сигналу с микрофона можно определить нажатие кнопки Play/Pause. К счастью, для этого достаточно просто записать звук на компьютере и посмотреть на результат. Я запустил Audacity, нажал во время записи кнопку Play/Pause — и получил такой сигнал.
Бинго
Как видим, нажатие кнопки очевидно отражается в форме сигнала: внезапное падение до −1 с последующим внезапным переходом к 1 и постепенным уменьшением до 0. Интуитивно по спецификации я бы предположил, что сигнал подскочит до 1 и останется там, пока кнопку не отпустить, но в реальности выглядит иначе. Тем не менее, такую картинку всё равно легко обнаружить, если захватить аудиопоток с микрофона.
Захват звука средствами Python
Зная способ, как обнаружить нажатие кнопок на гарнитуре, можно подумать о главной цели: как управлять плеером на рабочем столе с помощью кнопок гарнитуры.
Первый шаг — обнаружение нажатия кнопки. Для этого нужно захватить аудиопоток с микрофона и обнаружить отчётливую подпись, которую мы видели ранее. Для простоты реализуем решение на Python. После ещё одного небольшого поиска в интернете я нашёл пакет под названием sounddevice, который позволяет абстрагироваться от самой трудной части — реального аудиозахвата с микрофона.
Немножко кодирования даёт нам следующее:
import sounddevice as sd
SAMPLE_RATE = 1000 # Sample rate for our input stream
BLOCK_SIZE = 100 # Number of samples before we trigger a processing callback
class HeadsetButtonController:
def process_frames(self, indata, frames, time, status):
mean = sum([y for x in indata[:] for y in x])/len(indata[:])
print(mean)
def __init__(self):
self.stream = sd.InputStream(
samplerate=SAMPLE_RATE,
blocksize=BLOCK_SIZE,
channels=1,
callback=self.process_frames
)
self.stream.start()
if __name__ == '__main__':
controller = HeadsetButtonController()
while True:
pass
Такой код непрерывно выдаёт среднее значение каждой партии образцов. Мы установили частоту дискретизации 1000, что ужасно мало для обработки звука (обычно используется 44100), но нам в реальности не нужна большая точность. Размер блока определяет, сколько сэмплов в буфере инициируют обратный вызов. Опять же, мы установили очень низкие значения. Размер блока 100 и частота дискретизации 1000 фактически означает срабатывание 10 раз в секунду, где при каждом вызове обрабатывается только 100 сэмплов.
Определение нажатия кнопки: наверное, слишком простой способ
Теперь мы захватываем аудиопоток и можно реализовать реальный механизм для обнаружения нажатия кнопки. Напомним, что сигнал подскакивает до 1 всякий раз при нажатии. Это подсказывает самый простой способ обнаружения: если у N последовательных блоков значения сигнала выше 0,9, то есть нажатие.
Реализуем алгоритм в нашей функции:
import sounddevice as sd
SAMPLE_RATE = 1000 # Sample rate for our input stream
BLOCK_SIZE = 100 # Number of samples before we trigger a processing callback
PRESS_SECONDS = 0.2 # Number of seconds button should be held to register press
PRESS_SAMPLE_THRESHOLD = 0.9 # Signal amplitude to register as a button press
BLOCKS_TO_PRESS = (SAMPLE_RATE/BLOCK_SIZE) * PRESS_SECONDS
...
def process_frames(self, indata, frames, time, status):
mean = sum([y for x in indata[:] for y in x])/len(indata[:])
if mean < PRESS_SAMPLE_THRESHOLD:
self.times_pressed += 1
if self.times_pressed > BLOCKS_TO_PRESS and not self.is_held:
# The button was pressed!
self.is_held = True
else:
self.is_held = False
self.times_pressed = 0
...
По сути мы запустили внутренний счётчик, сколько обработанных блоков отвечают пороговому требованию, которое просто установили на 0,9, предусмотрев неизбежное зашумление образца. Если блок не удовлетворяет требованию, счётчик сбрасывается — и мы начинаем заново. Переменная
is_held
отслеживает срабатывания, чтобы не регистрировать их многократно, если кнопка не отпускается.Управление воспроизведением в Windows
Теперь осталось только заменить в реальном коде комментарий “The button was pressed!”, чтобы управлять воспроизведением звука в Windows. Снова погуглим, чтобы разобраться, как это сделать: оказывается, можно управлять воспроизведением, имитируя нажатие клавиш соответствующими кодами виртуальных клавиш.
Оказалось, что имитировать нажатия клавиш очень легко с помощью пакета pywin32, который является просто оболочкой Python для Windows API. Собрав всё вместе, мы можем создать следующую функцию:
import win32api
import win32con
VK_MEDIA_PLAY_PAUSE = 0xB3
def toggle_play():
win32api.keybd_event(VK_MEDIA_PLAY_PAUSE, 0, 0, 0)
И у нас получилось! Обращение к функции
toggle_play
в том месте кода, где был комментарий “The button was pressed!”, позволяет управлять любым медиаплеером в Windows с помощью кнопок на гарнитуре Android.Тесты показали, что код работает на удивление хорошо. Единственное различие между функциональностью на Android и Windows заключается в небольшой задержке при нажатии на кнопку, но с этим можно жить.
И вот что получилось
Скрипт Python состоит из 51 строки, которые активируют кнопки гарнитуры Android в Windows. Окончательный исходный код этого проекта лежит на Github.
Погодите, это ещё не всё!
После счастливого использования программы в течение нескольких часов я заметил серьёзную проблему:
Программа использует почти 30% CPU! Очевидно, это неприемлемо при длительной работе, что-то нужно делать. Посмотрев на код, я понял, что основной поток находится в состоянии ожидания в основном цикле, хотя там ничего не происходит. Наиболее логичное решение — просто усыпить поток навсегда: поскольку колбэк вызывается автоматически, нам всё равно не нужен цикл.
from time import sleep
if __name__ == '__main__':
controller = HeadsetButtonController()
while True:
sleep(10)
Я также не хотел запускать скрипт Python вручную после каждого запуска компьютера. К счастью Python для Windows поставляется с полезной утилитой pythonw.exe, которая запускает процесс «демона» без подключенного терминала. Размещаем ярлык к этому процессу в каталоге Microsoft\Windows\Start Menu\Programs\Startup, указав наш скрипт в качестве первого аргумента — тогда приложение автоматически запускается и незаметно работает в фоновом режиме.