Хак для поддержки кнопок Android-гарнитуры под Windows

в 12:09, , рубрики: Android-гарнитура, sounddevice, аппаратная поддержка, периферия, разработка под windows

Я почти каждый день слушаю музыку на смартфоне и использую кнопки управления на гарнитуре. Но мне всегда не нравилась одна вещь. Я прихожу домой, продолжая прослушивание, гарнитура подключается к домашнему ПК — и внезапно кнопки перестают работать.

Конечно, я погуглил решение этой проблемы. К сожалению, на Windows эта замечательная функция не слишком поддерживается. Пара минут поиска дали только мутные упоминания на Stack Overflow о звуковых картах и сообщения некоторых людей, что на их ноутбуках всё работает нормально.

Меня это не испугало — и я решил принять проблему как интересный вызов: можно ли создать какую-то программу для активации кнопок управления, если аппаратной поддержки для них вообще нет? Ответ — да, можно. И вот как сделать это за полчаса.

Как работают кнопки гарнитуры Android

Первое, что нужно понять — как работают кнопки гарнитуры. Быстрый поиск в интернете нашёл эту спецификацию из документации Android. Там есть диаграмма.

Хак для поддержки кнопок Android-гарнитуры под Windows - 1

Как можно понять, при нажатии кнопки на гарнитуре замыкается цепь на одном из резисторов. Особого внимания заслуживает Кнопка A (Play/Pause/Hook) с сопротивлением 0 Ом, то есть замыканием микрофона. Если мы способны обнаружить короткое замыкание микрофона, то так сможем определить нажатие кнопки Play/Pause.

Проверка гипотезы

Прежде чем начать программировать, хотелось бы проверить разумность наших рассуждений в принципе. То есть того, что по сигналу с микрофона можно определить нажатие кнопки Play/Pause. К счастью, для этого достаточно просто записать звук на компьютере и посмотреть на результат. Я запустил Audacity, нажал во время записи кнопку Play/Pause — и получил такой сигнал.

Хак для поддержки кнопок Android-гарнитуры под Windows - 2
Бинго

Как видим, нажатие кнопки очевидно отражается в форме сигнала: внезапное падение до −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 заключается в небольшой задержке при нажатии на кнопку, но с этим можно жить.

Хак для поддержки кнопок Android-гарнитуры под Windows - 3
И вот что получилось

Скрипт Python состоит из 51 строки, которые активируют кнопки гарнитуры Android в Windows. Окончательный исходный код этого проекта лежит на Github.

Погодите, это ещё не всё!

После счастливого использования программы в течение нескольких часов я заметил серьёзную проблему:

Хак для поддержки кнопок Android-гарнитуры под Windows - 4

Программа использует почти 30% CPU! Очевидно, это неприемлемо при длительной работе, что-то нужно делать. Посмотрев на код, я понял, что основной поток находится в состоянии ожидания в основном цикле, хотя там ничего не происходит. Наиболее логичное решение — просто усыпить поток навсегда: поскольку колбэк вызывается автоматически, нам всё равно не нужен цикл.

from time import sleep

if __name__ == '__main__':
    controller = HeadsetButtonController()

    while True:
        sleep(10)

Хак для поддержки кнопок Android-гарнитуры под Windows - 5

Я также не хотел запускать скрипт Python вручную после каждого запуска компьютера. К счастью Python для Windows поставляется с полезной утилитой pythonw.exe, которая запускает процесс «демона» без подключенного терминала. Размещаем ярлык к этому процессу в каталоге MicrosoftWindowsStart MenuProgramsStartup, указав наш скрипт в качестве первого аргумента — тогда приложение автоматически запускается и незаметно работает в фоновом режиме.

Автор: m1rko

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js