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