Энтузиаст Джеймс Воган поделился, что он приобрел несколько колонок со встроенными стриминговыми сервисами, но остался недоволен их системой регулировки громкости. Он решил настроить их так, чтобы получить более точный контроль в комфортном диапазоне воспроизведения.
Обычно Воган использует около 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