Конечно, я погуглил решение этой проблемы. К сожалению, на 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, указав наш скрипт в качестве первого аргумента — тогда приложение автоматически запускается и незаметно работает в фоновом режиме.
Комментарии (20)
DollaR84
16.07.2018 20:33Интересно было почитать, спасибо.
Насчет нажатия программно комбинаций клавиш. Сам использовал pywin32 модуль, а потом как-то попался модуль keyboard. Вроде проще и довольно удобный. Не знаю может есть какие-то недостатки в работе, мне пока не попадалось вроде.
rdifb0
16.07.2018 21:19+1Ужас. А отпускать кнопку кто будет, Иван Фёдорович Крузенштерн?
def toggle_play(): win32api.keybd_event(VK_MEDIA_PLAY_PAUSE, 0, 0, 0)
Должно быть примерно так.
def toggle_play(): win32api.keybd_event(VK_MEDIA_PLAY_PAUSE, 0, 0, 0) win32api.keybd_event(VK_MEDIA_PLAY_PAUSE, 0, win32con.KEYEVENTF_KEYUP, 0)
Lertmind
17.07.2018 00:39Это перевод, нужно создавать pull request.
Там не нужно time.sleep(0.05) между нажатием/отпусканием? Видел такое в некоторых примерах использования.DollaR84
17.07.2018 10:31Тоже видел, но в своих программах делал без этой задержки и все работает прекрасно. Так что думаю вполне норм.
nobletracer
16.07.2018 21:20Это будет просто бесценная программа, если будет интегрироваться с браузером и стриминговыми сервисами.
Lertmind
17.07.2018 00:31Не знал, что в домашних ПК уже есть разъёмы с поддержкой гарнитуры.
У меня переходника нет, пробовал вставлять две гарнитуры в разъём микрофона. Та, которая от Nokia и не работает на моём устройстве Android, смогла передать сигнал при нажатии, хотя записи звука нет. Регистрация нажатия срабатывала через раз, увеличил SAMPLE_RATE на 2 и стало нормально.immaculate
17.07.2018 03:53У меня в 4-х летнем ноутбуке есть разъем с поддержкой гарнитуры. Только мне пока не удалось найти гарнитуру, которая к нему бы подходила (специально не искал, но телефонная не работает — звук не пишется).
Eloev
17.07.2018 07:12Звука в нокиевской гарнитуре нет из-за разных стандартов джека (OMTP и CTIA), по делу разница стандартов лишь в поменянных местами контактах GND и Mic. Я думаю, возьмёте любые другие наушники, стандарт у почти всех кроме нокии CTIA, и все будет хорошо.
DollaR84
17.07.2018 10:34У меня старенький Asus Vivobook еще года так 2012 если не ошибаюсь, или около того. Один аудио разъем для гарнитуры, совмещенный наушники с микрофоном.
Alexeyslav
17.07.2018 09:44Интуитивно по спецификации я бы предположил, что сигнал подскочит до 1 и останется там, пока кнопку не отпустить, но в реальности выглядит иначе.
Зная устройство микрофонного входа я бы такое не предположил бы никогда. Для работы микрофона на входе поддерживается некоторая постоянная составляющая — по сути источник тока с ограниченным напряжением в +3В примерно. Уровень напряжения «нуля»(в пределах 1..2В) при подключенном микрофоне будет зависеть от самого микрофона, разброса параметров микрофона и т.д. чтобы с этим не парится по входу стоит ВЧ-фильтр на 2-10Гц(на дешёвых карточках может быть и выше граница) в виде конденсатора, что в итоге даёт наблюдаемую картину.
И я бы в условие срабатывания включил бы проверку крутизны фронтов этого импульса, с вашей проверкой могут быть ложные срабатывания просто от громкого звука(микрофоны гарнитуры бывают довольно чувствительны и болтаются где ни попадя). Поэтому нужна полноценная частота семплирования и простейшая обработка сигнала.
RomanGL
17.07.2018 12:17С Bluetooth наушниками ничего такого нельзя сделать? У меня кнопки на наушниках (play/pause, next, previous) работают только в UWP приложениях и в iTunes.
Может как-то можно обойти, чтобы вызывалось срабатывание обычных медиаклавиш клавиатуры?
Mike-M
17.07.2018 13:07Интересная статья, спасибо.
Сам, правда, отказался от прослушивания музыки с ПК в пользу смартфона. Первый слишком прожорлив для этой задачи в плане энергопотребления.
ser-mk
17.07.2018 15:23Спасибо за перевод. И спасибо автору что не стал использовать нейросети для распознования сигнала кнопки (когда читал прям был уверен что сейчас начнется machine learning =)
Loki3000
18.07.2018 09:59Интересно, а любой ли набор резистивных кнопок можно подключить через вход наушников? А то те решения что я видел, были довольно дорогие и громоздкие.
Alexeyslav
18.07.2018 11:26А что мешает?
Loki3000
18.07.2018 11:35Ну не знаю… возможно, какое-то согласование уровней нужно или еще что-нибудь подобное…
Просто есть же джойстики для магнитол.
У них как раз миниджек в качестве разъема.Alexeyslav
18.07.2018 11:46Миниджек вообще ничего не значит. Там может быть то же самое что в телефонной гарнитуре или вовсе полностью цифровой интерфейс, просто сам разъём удобный. И всё же скорей всего там именно цифровой интерфейс. Подаёшь питание а на третьем выводе получаешь серии импульсов, соответствующих нажатым кнопкам и т.д. Или вообще двусторонний интерфейс с аутентификацией.
Надо расковыривать, наблюдать осциллографом на живой магнитоле и потом делать выводы.
Но технически, можно разобрать выкинуть всю начинку оттуда и сделать как в гарнитуре — на простых резисторах. Правда, с энкодерами могут быть проблемы — сложно будет отличать направление вращения. Ну и соответственно миниджек надо 4-х контактный. Короче, «каша из топора» получается.Loki3000
18.07.2018 11:50Нет там цифрового интерфейса — обычный набор резистивных кнопок. Просто не слышал чтобы его подключали напрямую через вход микрофона — обычно использовали плату согласования. Потому вопрос и возник.
Alexeyslav
18.07.2018 13:48Без анализа схемы можно гадать бесконечно. Там могут быть номиналы резисторов совсем не те что подойдут для микрофонного входа.
mayorovp
Вот так:
while True: pass
никогда не следует даже пытаться писать! Вариантwhile True: sleep(10)
чуть по-лучше, но все еще странный.Если нужно уснуть навсегда (до прерывания с клавиатуры или завершения процесса) — подойдет вот такой вариант (источник: https://stackoverflow.com/a/48631852):