Энтузиаст Джеймс Воган поделился, что он приобрел несколько колонок со встроенными стриминговыми сервисами, но остался недоволен их системой регулировки громкости. Он решил настроить их так, чтобы получить более точный контроль в комфортном диапазоне воспроизведения.
Обычно Воган использует около 10% от диапазона громкости, на который способны колонки. Это затрудняет для него регулировку звука, так как крошечный ползунок можно использовать только на 10% или около 15 шагов, где переход от шага 3 к шагу 4 переводит динамики с уровня «немного тихо» на уровень «определённо беспокоит соседей».
Энтузиаст изучил недокументированные веб-интерфейсы динамиков, найдя их локальный IP-адрес через свой маршрутизатор.
Он обнаружил, что динамики предоставляют довольно простой HTTP API, включая GET/api/getData и POST/api/setData, которые позволяют читать и записывать текущий уровень громкости.
Затем Воган нашёл исходный код плагина Hombridge для динамиков KEF, которые, как и JBL, используют StreamSDK. Он обнаружил, что в веб-интерфейсе динамиков есть страница, позволяющая загружать системные журналы с копией части файловой системы, в которой хранятся текущие настройки. Это помогло отследить два конкретных пути конфигурации: player/attenuation и hostlink/maxVolume.
$ curl --url 'http://192.168.1.239/api/getData?path=settings:/hostlink/maxVolume&roles=@all' | jq { «timestamp»: 1711309370908, «title»: «Max volume setting (ARCAM-project specific)», «modifiable»: true, «type»: «value», «path»: «settings:/hostlink/maxVolume», «defaultValue»: { «type»: «i32_», «i32_»: 99 }, «value»: { «type»: «i32_», «i32_»: 46 } }
Энтузиаст создал небольшую веб-страницу, которая включает полноэкранный ползунок для настройки громкости.
Для его обслуживания он создал небольшой сервер с помощью Bun. Благодаря этому удалось ограничиться одним файлом TypeScript без каких-либо зависимостей, кроме самого Bun. Веб-сервер обслуживает страницу с ползунком и пересылает запросы на динамики.
// server.ts // Run this via `bun --hot server.ts` const MAX_VOLUME = 25; const SPEAKER_URL = «http://192.168.1.239»; const UPDATE_INTERVAL_SECONDS = 10; const getVolumeUrl = `${SPEAKER_URL}/api/getData?path=player:volume&roles=@all`; function html (strings: TemplateStringsArray, ...values: any[]) { return strings.reduce ((result, string, i) => { return result + string + (values[i] || ""); }, ««); } const pageHtml = html`<html> <head> <title>volume</title> <style> body { margin: 0; } #volume { -webkit-appearance: none; appearance: none; margin: 0; width: 100%; height: 100%; cursor: pointer; outline: none; background: linear-gradient (to right, blue var (--volume), white 0); } /* Hide the thumb */ #volume::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 0; height: 0; } #volume::-moz-range-thumb { width: 0; height: 0; } </style> </head> <body> <input type=»range» min="0" max="${MAX_VOLUME}" value="0" id="volume" disabled /> <script> // Yes, this is JavaScript embedded in HTML embedded in TypeScript. function setBackgroundGradient () { const percentage = (volume.value / ${MAX_VOLUME}) * 100; document.body.style.setProperty («--volume», `${percentage}%`); } // I only recently learned that you can reference elements by ID this way. // It's kind of horrible but also I love it on tiny pages like this. volume.oninput = async function setVolume () { fetch («volume», { method: «POST», body: JSON.stringify ({ volume: volume.value }), }); setBackgroundGradient (); }; async function getVolume () { const response = await fetch («volume»); const body = await response.text (); volume.value = body; volume.disabled = false; setBackgroundGradient (); } getVolume (); setInterval (getVolume, ${UPDATE_INTERVAL_SECONDS * 1000}); </script> </body> </html>`; const server = Bun.serve ({ async fetch (request) { const url = new URL (request.url); switch (url.pathname) { case «/»: return new Response (pageHtml, { headers: { «Content-Type»: «text/html» }, }); case «/volume»: switch (request.method) { case «GET»: { const response = await fetch (getVolumeUrl); const body = await response.json (); return new Response (body.value.i32_); } case «POST»: { const { volume } = await request.json (); console.log (`Setting volume to ${volume}.`); // I don't want to blow out the speakers or go deaf because of a bug // somewhere else in this code, so I check for high volumes here. if (volume > MAX_VOLUME) { console.error («That's too high!», volume); return new Response («Volume too high», { status: 500 }); } return fetch (`${SPEAKER_URL}/api/setData`, { method: «POST», headers: { «Content-Type»: «application/json», }, body: JSON.stringify ({ path: «player:volume», role: «value», value: { type: «i32_», i32_: volume }, _nocache: new Date ().getTime (), }), }); } } } console.error («Not found:», url.pathname); return new Response («Not Found», { status: 404 }); }, }); console.log (`Server running on http://localhost:${server.port}.`);
Теперь Воган намерен создать физическую ручку громкости с применением платы ESP32.
Источник: habr.com