Телефон на столе, ладонь сбоку, корабль на экране следует за ней

Телефон лежит на столе или в другой руке. Рядом в воздухе ладонь. Двигаешь её — корабль на экране поднимается и опускается. Экрана не касаешься. Камера не нужна: телефон пищит ультразвуком из своего динамика и слушает эхо своим микрофоном.

Играть: sonaroids.app (лучше на iPhone). Код: github.com/neokrasav4ik/sonaroids, MIT.

С чего всё началось

Мне попался проект sonar.cool: Мак листает страницы жестами руки, а «видит» их через собственные динамики и микрофон. Мака у меня нет, зато есть iPhone. Отсюда вопрос: получится ли то же самое в браузере на телефоне? Причём не отдельный жест «листай», а непрерывное управление: высота ладони = высота корабля.

Сразу признаюсь: это вайбкодинг

Сам я не написал ни строчки. Обработку сигнала, игру, сервер рекордов, тесты и даже гифку написал Claude Opus 5.5. От меня были идея, решения «так / не так» и проверка каждой версии своей ладонью. Для разбора я записывал с телефона сырые WAV: ладонь по линейке на 5, 10, 15 см. Модель гоняла по этим записям свои варианты обработки, пока цифры не сходились с линейкой. Ещё она сама собрала себе стенды: синтетический микрофон, повтор записанных партий, игру в Chromium без окна.

Как это работает

  • Зонд. Периодический сигнал из 23 тонов от 18,4 до 20,4 кГц, период 512 отсчётов при 48 кГц. AudioWorklet отдаёт поток микрофона без пропусков, так что картина эха считается каждые 10,7 мс.

  • Движение ладони видно по сдвигу фазы эха. Это точно, до долей миллиметра, но со временем уплывает.

  • Положение видно по тому, как эхо отличается от пустой комнаты. Это шумно, зато не уплывает. Обе оценки сливаются: фаза ведёт корабль, положение не даёт ему уползти.

  • Калибровки как таковой нет. Убираешь руку на секунду (игра запоминает пустую комнату), машешь ладонью на 5–10 см над столом или на 5–15 см от торца телефона, если он в руке, и экран подгоняется под твой размах.

Что не сработало: пик дальности (прыгал на 6–22 см), амплитуда эха (сплошной шум от пятен интерференции), стандартный AnalyserNode (окна слишком грубые) и две руки (Safari даёт только один микрофон).

UPD: Второй способ нашёлся случайно: телефон в руке

Во время тестов я взял телефон в руку, а другой ладонью, повёрнутой ребром вниз, стал водить у торца с разъёмом. Ближе — корабль ниже, дальше — выше. Оказалось, так управлять не хуже, чем над столом, а стол вообще не нужен. Переключателя в игре нет и не понадобился: сонар в обоих случаях просто меряет расстояние до ладони.

Но в руке всплыла новая беда. Иногда вся картина эха разом «переезжала» во времени — на 256 отсчётов на iPhone, на ~144 на OnePlus — хотя вход не прерывался. Игра принимала это за закрытый ладонью динамик, переставала сверять положение, и корабль уплывал к краю. Теперь, когда прямой сигнал пропадает, игра ищет, не сдвинулся ли весь отклик. Ищет по отклику, накопленному за треть секунды: эхо движущейся ладони при этом гасится, а прямой сигнал остаётся на месте. Если сдвиг нашёлся, игра просто переходит туда.

Почему «нужен флагман»

Ирония в том, что игра весит около 50 КБ в сжатом виде: графика, звуки и пиксельный шрифт нарисованы кодом. А запускается нормально только на хорошем железе. Всё решает полоса 18–20 кГц: динамик и микрофон телефона вовсе не обязаны её пропускать.

телефон

спад к 20,4 кГц

эхо ладони над пустой комнатой

iPhone 16

около −8 дБ

около 40 дБ

OnePlus 13

−33 дБ

10–15 дБ

Redmi Note 10

около −20 дБ

10–15 дБ

Точнее даже так: на столе нужен не просто флагман, а iPhone. OnePlus 13 тоже флагман, но над столом играть на нём тяжело. Зато с телефоном в руке он неожиданно почти догнал iPhone. У OnePlus ещё проваливается вход: телефон теряет куски сигнала. Redmi слышит ладонь только со стороны фронтальной камеры, а не со стороны разъёма, где по логике стоят динамик и микрофон. По справкам, которые игра отправляет на сервер, ладонь слышна и на Galaxy S25, Pixel 8 Pro, Nothing Phone (3a), realme GT Neo 5, Honor 600 Lite. На OnePlus 9 Pro — нет.

Часть проблем удалось обойти. Полоса выравнивается по уровню каждого тона: на записи с OnePlus ошибка против линейки упала с 81 до 13 мм. После провала входа прямой сигнал ищется заново. Игра сама выясняет, у какого конца телефона держать руку. Но 25 дБ разницы в эхе не вытянешь, поэтому Android пока «как повезёт с телефоном».

Рекорды без читеров

На сервер уходит не счёт, а вся партия: номер раскладки и высота ладони на каждом шаге. Это несколько килобайт на пару минут игры. Полёт детерминирован: постоянный шаг 60 Гц, генератор случайных чисел от номера раскладки, никаких sin и pow, которые могут по-разному округляться в разных движках. Поэтому сервер переигрывает партию тем же самым файлом правил, что и браузер, и записывает только счёт, который повторился. «Прислать 999999» не получится. Бот, который честно водит ладонью, пройдёт, но пусть сначала его кто-нибудь напишет.

Сервер — Node 22 без единого npm-пакета: node:http и встроенный node:sqlite. Ник — только латиница, цифры и _: ради защиты от желающих вписать в ник SQL-инъекцию.

Что дальше

Игра работает, рекорды пишутся: сегодня, неделя, всё время. Если у вас Android (особенно Pixel или Samsung), попробуйте оба способа — на столе и в руке — и напишите в комментариях, как вышло: модель телефона, слышит ли он ладонь и с какого конца. Если игра не слышит ладонь, тап по номеру версии на экране проигрыша включает журнал, его можно приложить к issue на GitHub.

Ссылки:

Комментарии (0)