@robgold
robgold wrote:Unfortunately it croaks like most others.@MAJSTER XXL can you check?
Czy wolisz polską wersję strony elektroda?
Nie, dziękuję Przekieruj mnie tamrobgold wrote:Unfortunately it croaks like most others.@MAJSTER XXL can you check?
MAJSTER XXL wrote:Unfortunately it rattles like most others.
ejcon wrote:Hello,
I have a version with a real equalizer and eqalizer but there is a problem with the standard AAC and FLAC on these programs the equalizer is turned on it cuts transfers and jerks .
There is a prefix for automatic operation when ACC and FLAC are on, the equalizer switches itself off or cuts the bands so that it does not jerk, but there is still a problem. Operation of the equalizer via the website .Corrected version of the FLAC equaliser Correction of the auto level input analyser . Added automation with display of equalizer status .<!DOCTYPE html> <html lang="pl"> <head> <meta charset="UTF-8"> <title>EVO – Radiobrowser + EQ16</title> <meta name="viewport" content="width=device-width,initial-scale=1"> <style> :root { --bg: #111; --card: #181818; --fg: #eee; --accent: #4caf50; --accent-soft: #2e7d32; --border: #333; } * { box-sizing: border-box; } body { margin: 0; padding: 0; font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: var(--bg); color: var(--fg); } header { padding: 10px 16px; background: #222; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px; } header h1 { margin: 0; font-size: 18px; } header .host { font-size: 12px; color: #aaa; } main { min-height: calc(100vh - 46px); display: flex; justify-content: center; align-items: flex-start; padding: 18px 8px 40px; } .card { width: 100%; max-width: 960px; background: var(--card); border-radius: 12px; border: 1px solid var(--border); padding: 14px 14px 18px; box-shadow: 0 0 12px rgba(0,0,0,0.6); } h2 { margin: 0 0 6px; font-size: 15px; } /* ANALIZATOR */ .analyzer-header { margin-bottom: 4px; } .analyzer-desc { font-size: 11px; color: #aaa; margin-bottom: 6px; } #spectrumCanvas { width: 100%; height: 160px; /* wy sze okno analizatora */ background: #000; border-radius: 6px; border: 1px solid #444; display: block; } .an-freq-labels { display: flex; justify-content: space-between; margin-top: 4px; font-size: 10px; color: #ccc; } .an-freq-label { min-width: 0; text-align: center; flex: 1 1 auto; } /* Korektor */ .eq-header { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin-top: 18px; } .eq-toggle-group { display: flex; align-items: center; gap: 8px; } .eq-toggle { display: flex; align-items: center; gap: 4px; font-size: 12px; } .eq-toggle input { width: 16px; height: 16px; cursor: pointer; } .eq-info { font-size: 11px; color: #aaa; margin-top: 4px; margin-bottom: 8px; } .eq-sliders { display: flex; gap: 6px; justify-content: space-between; overflow-x: auto; padding: 6px 2px 4px; } .eq-band { display: flex; flex-direction: column; align-items: center; font-size: 10px; min-width: 24px; } .eq-slider-wrap { height: 120px; display: flex; align-items: center; justify-content: center; } .eq-slider { -webkit-appearance: none; appearance: none; width: 120px; height: 6px; transform: rotate(-90deg); background: #333; border-radius: 4px; outline: none; } .eq-slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 10px; height: 16px; border-radius: 3px; background: var(--accent); cursor: pointer; } .eq-slider::-moz-range-thumb { width: 10px; height: 16px; border-radius: 3px; background: var(--accent); cursor: pointer; border: none; } .eq-band-label { margin-top: 2px; } .eq-band-gain { margin-top: 2px; color: #ccc; } .eq-buttons { margin-top: 8px; display: flex; flex-wrap: wrap; gap: 6px; font-size: 12px; } .eq-buttons button { padding: 4px 8px; border-radius: 6px; border: 1px solid var(--border); background: #333; color: var(--fg); cursor: pointer; } .eq-buttons button:hover { background: #444; } /* Volume */ .vol-section { margin-top: 14px; font-size: 12px; } .vol-label-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; } /* KRÓTSZY I WYŚRODKOWANY SUWAK GŁOŚNOŚCI */ .vol-slider { -webkit-appearance: none; appearance: none; width: 40%; /* <-- tu ustaw długość, np. 40–60% */ height: 10px; background: #4caf50; border-radius: 5px; outline: none; display: block; margin: 0 auto; /* wyśrodkowanie w rzędzie */ } /* Radiobrowser */ .search-bar { margin-top: 18px; display: flex; flex-wrap: wrap; gap: 6px; } .search-bar input[type="text"], .search-bar input[type="number"], .search-bar select { padding: 5px 7px; border-radius: 6px; border: 1px solid var(--border); background: #000; color: var(--fg); font-size: 13px; } .search-bar button { padding: 6px 10px; border-radius: 6px; border: 1px solid var(--border); background: var(--accent-soft); color: var(--fg); cursor: pointer; font-size: 13px; white-space: nowrap; } .search-bar button:hover { background: var(--accent); } .status { font-size: 12px; color: #aaa; margin-top: 6px; } .results { max-height: 260px; overflow-y: auto; border-top: 1px solid var(--border); margin-top: 6px; padding-top: 4px; } .station { display: flex; gap: 8px; align-items: center; padding: 6px 4px; border-bottom: 1px solid #222; } .station:nth-child(even) { background: #151515; } .station-meta-box { flex: 1; } .station-name { font-size: 13px; font-weight: 600; } .station-meta { font-size: 11px; color: #aaa; } .station-url { font-size: 11px; word-break: break-all; color: #ccc; } .station-actions { display: flex; flex-direction: column; gap: 3px; } .btn-small { font-size: 11px; padding: 3px 6px; border-radius: 5px; border: 1px solid var(--border); background: #333; color: var(--fg); cursor: pointer; white-space: nowrap; } .btn-small:hover { background: #444; } @media (max-width: 640px) { .eq-slider-wrap { height: 100px; } } </style> </head> <body> <header> <h1>EVO – Radiobrowser + EQ16</h1> <div class="host">Host: <span id="hostInfo">?</span></div> </header> <main> <div class="card"> <!-- ANALIZATOR --> <div class="analyzer-header"> <h2>Analizator widma (wewn trzny EQ16)</h2> </div> <div class="analyzer-desc"> Pasek pokazuje aktualne pasma z analizatora w firmware (odczyt z <code>/api/eq/spectrum</code>). Po bokach skala w dB. </div> <canvas id="spectrumCanvas"></canvas> <div id="anFreqLabels" class="an-freq-labels"></div> <!-- EQ --> <div class="eq-header"> <h2>EQ16 – korektor graficzny</h2> <div class="eq-toggle-group"> <label class="eq-toggle"> <input type="checkbox" id="eqEnabled"> <span>Korektor</span> </label> <label class="eq-toggle"> <input type="checkbox" id="anEnabled"> <span>Analizator</span> </label> </div> </div> <div class="eq-info"> 16 pasm 20 Hz – 20 kHz, zakres -18 … +18 dB. Zmiany wysy ane przez API do EVO (<code>/api/eq/*</code>). </div> <div id="eqSliders" class="eq-sliders"></div> <div class="eq-buttons"> <button type="button" onclick="setAll(0)">P asko (0 dB)</button> <button type="button" onclick="setAll(-6)">Wyciszenie pasm (-6 dB)</button> <button type="button" onclick="setAll(6)">Podbicie pasm (+6 dB)</button> <button type="button" onclick="applyPreset('rock')">Preset ROCK</button> <button type="button" onclick="applyPreset('disco')">Preset DISCO</button> <button type="button" onclick="applyPreset('pop')">Preset POP</button> <button type="button" onclick="reloadFromDevice()">Odczytaj z EVO</button> <button type="button" onclick="saveEQToDevice()">Zapisz EQ</button> </div> <!-- Volume --> <div class="vol-section"> <div class="vol-label-row"> <span>G o no </span> <span id="volValue">--</span> </div> <input type="range" id="volSlider" class="vol-slider" min="1" max="42" step="1" value="1"> </div> <!-- Radiobrowser --> <h2 style="margin-top:18px;">Radiobrowser – wyszukiwarka stacji</h2> <div class="search-bar"> <input id="q" type="text" placeholder="Nazwa / kraj"> <input id="tag" type="text" placeholder="Tag (rock, 80s...)"> <input id="minBr" type="number" placeholder="Min kbps" min="0" step="32"> <select id="limit"> <option value="20">20</option> <option value="50" selected>50</option> <option value="100">100</option> </select> <button id="btnSearch">Szukaj</button> </div> <div id="status" class="status">Podaj fraz i kliknij „Szukaj”.</div> <div id="results" class="results"></div> </div> </main> <script> // Host info document.getElementById('hostInfo').textContent = window.location.hostname || 'localhost'; // ------- ANALIZATOR ------- const specCanvas = document.getElementById('spectrumCanvas'); const specCtx = specCanvas.getContext('2d'); // zakres wy wietlania skali const DISPLAY_MIN_DB = -60; const DISPLAY_MAX_DB = 6; // peak-hold let peakLevels = []; let peakHold = []; let peakAlpha = []; const PEAK_HOLD_FRAMES = 1; const PEAK_FALL_STEP = 0.12; const PEAK_FADE_FACTOR = 0.6; function resizeSpectrumCanvas() { const rect = specCanvas.getBoundingClientRect(); specCanvas.width = rect.width; specCanvas.height = rect.height; } window.addEventListener('resize', resizeSpectrumCanvas); resizeSpectrumCanvas(); function drawSpectrum(levels) { const ctx = specCtx; const w = specCanvas.width; const h = specCanvas.height; ctx.fillStyle = '#000'; ctx.fillRect(0, 0, w, h); const marginLeft = 32; const marginRight = 32; const paddingTop = 10; const paddingBottom = 20; const innerW = w - marginLeft - marginRight; const innerH = h - paddingTop - paddingBottom; // siatka dB ( cznie dodatnie warto ci) const dbTicks = [6, 0, -10, -20, -30, -40, -50, -60]; ctx.strokeStyle = '#333'; ctx.lineWidth = 1; ctx.font = '9px system-ui, sans-serif'; ctx.fillStyle = '#888'; ctx.textBaseline = 'middle'; dbTicks.forEach(db => { let norm = (db - DISPLAY_MIN_DB) / (DISPLAY_MAX_DB - DISPLAY_MIN_DB); if (norm < 0) norm = 0; if (norm > 1) norm = 1; const y = paddingTop + innerH - norm * innerH; ctx.beginPath(); ctx.moveTo(marginLeft, y); ctx.lineTo(w - marginRight, y); ctx.stroke(); const label = (db > 0 ? '+' : '') + db + ' dB'; ctx.textAlign = 'right'; ctx.fillText(label, marginLeft - 4, y); ctx.textAlign = 'left'; ctx.fillText(label, w - marginRight + 4, y); }); if (!levels || !levels.length) return; const n = levels.length; if (peakLevels.length !== n) { peakLevels = new Array(n).fill(0); peakHold = new Array(n).fill(0); peakAlpha = new Array(n).fill(0); } const barGap = 3; const totalGap = barGap * (n + 1); const barWidth = Math.max(3, (innerW - totalGap) / n); const steps = 18; for (let i = 0; i < n; i++) { // z firmware dostajemy 0..1 odpowiadaj ce -60..0 dB const vIn = levels[i]; let dbVal = vIn * 60 - 60; // -60..0 if (dbVal < -60) dbVal = -60; if (dbVal > 0) dbVal = 0; // przeskalowanie do zakresu -60..+6 (wizualnie) let v = (dbVal - DISPLAY_MIN_DB) / (DISPLAY_MAX_DB - DISPLAY_MIN_DB); if (v < 0) v = 0; if (v > 1) v = 1; // peak-hold if (v >= peakLevels[i]) { peakLevels[i] = v; peakHold[i] = 0; peakAlpha[i] = 1.0; } else { if (peakHold[i] < PEAK_HOLD_FRAMES) { peakHold[i]++; } else { peakLevels[i] = Math.max(0, peakLevels[i] - PEAK_FALL_STEP); peakAlpha[i] = Math.max(0, peakAlpha[i] * PEAK_FADE_FACTOR); } } const activeSteps = Math.round(v * steps); const x = marginLeft + barGap + i * (barWidth + barGap); const stepH = innerH / steps; // kostki s upka for (let s = 0; s < steps; s++) { const y = paddingTop + innerH - (s + 1) * stepH; if (s < activeSteps) { const frac = (s + 1) / steps; if (frac < 0.6) { ctx.fillStyle = '#2e7d32'; } else if (frac < 0.85) { ctx.fillStyle = '#f9a825'; } else { ctx.fillStyle = '#e53935'; } } else { ctx.fillStyle = '#111'; } ctx.fillRect(x, y, barWidth, stepH - 1); } // niebieski peak z wygaszaniem const peakV = peakLevels[i]; const alpha = peakAlpha[i]; if (peakV > 0.001 && alpha > 0.01) { const peakY = paddingTop + innerH - peakV * innerH; ctx.save(); ctx.globalAlpha = alpha; ctx.fillStyle = '#2196f3'; ctx.fillRect(x, peakY - 2, barWidth, 3); ctx.restore(); } } ctx.fillStyle = '#1b5e20'; ctx.fillRect(marginLeft, h - paddingBottom + 4, innerW, 2); } async function spectrumLoop() { const POLL_MS = 120; while (true) { let bands = []; try { const resp = await fetch('/api/eq/spectrum'); if (resp.ok) { const data = await resp.json(); if (data && Array.isArray(data.bands)) { bands = data.bands; } } } catch (e) { // brak endpointu – nic nie rysujemy } drawSpectrum(bands); await new Promise(r => setTimeout(r, POLL_MS)); } } spectrumLoop(); // ------- EQ16 + cz stotliwo ci ------- const EQ_BANDS = 16; const freqs = []; (function computeFreqs() { const f0 = 20, f1 = 20000; for (let i = 0; i < EQ_BANDS; i++) { const t = i / (EQ_BANDS - 1); const f = f0 * Math.pow(f1 / f0, t); freqs.push(f); } })(); const anFreqLabelsDiv = document.getElementById('anFreqLabels'); function createAnalyzerLabels() { anFreqLabelsDiv.innerHTML = ''; for (let i = 0; i < EQ_BANDS; i++) { const div = document.createElement('div'); div.className = 'an-freq-label'; const f = freqs[i]; div.textContent = f < 1000 ? Math.round(f) + ' Hz' : (f / 1000).toFixed(1) + ' k'; anFreqLabelsDiv.appendChild(div); } } const eqSlidersBox = document.getElementById('eqSliders'); const eqEnabledCheck = document.getElementById('eqEnabled'); const anEnabledCheck = document.getElementById('anEnabled'); let eqGains = new Array(EQ_BANDS).fill(0); function createEqUI() { eqSlidersBox.innerHTML = ''; for (let i = 0; i < EQ_BANDS; i++) { const bandDiv = document.createElement('div'); bandDiv.className = 'eq-band'; const wrap = document.createElement('div'); wrap.className = 'eq-slider-wrap'; const slider = document.createElement('input'); slider.type = 'range'; slider.min = '-18'; slider.max = '18'; slider.step = '0.5'; slider.value = '0'; slider.className = 'eq-slider'; slider.dataset.band = String(i); slider.addEventListener('input', onSliderInput); slider.addEventListener('change', onSliderChange); wrap.appendChild(slider); bandDiv.appendChild(wrap); const label = document.createElement('div'); label.className = 'eq-band-label'; const f = freqs[i]; label.textContent = f < 1000 ? Math.round(f) + ' Hz' : (f/1000).toFixed(1) + ' k'; bandDiv.appendChild(label); const gain = document.createElement('div'); gain.className = 'eq-band-gain'; gain.id = 'eqGain' + i; gain.textContent = '0 dB'; bandDiv.appendChild(gain); eqSlidersBox.appendChild(bandDiv); } } function onSliderInput(e) { const band = parseInt(e.target.dataset.band, 10); const val = parseFloat(e.target.value); const gainLabel = document.getElementById('eqGain' + band); gainLabel.textContent = (val > 0 ? '+' : '') + val.toFixed(1) + ' dB'; } function onSliderChange(e) { const band = parseInt(e.target.dataset.band, 10); const val = parseFloat(e.target.value); eqGains[band] = val; sendBandToDevice(band, val); } function setAll(v) { eqGains = eqGains.map(() => v); const sliders = eqSlidersBox.querySelectorAll('.eq-slider'); sliders.forEach((sl, i) => { sl.value = v; const lbl = document.getElementById('eqGain' + i); lbl.textContent = (v > 0 ? '+' : '') + Number(v).toFixed(1) + ' dB'; }); sendAllToDevice(); } // Presety ROCK / DISCO / POP function applyPreset(name) { let curve = new Array(EQ_BANDS).fill(0); if (name === 'rock') { curve = [5, 4, 3, 2, 1, 0, -1, -2, -2, -1, 0, 1, 2, 3, 4, 5]; } else if (name === 'disco') { curve = [6, 5, 4, 3, 2, 1, 0, -1, -1, 0, 1, 2, 3, 4, 5, 6]; } else if (name === 'pop') { curve = [3, 2, 1, 0, 0, 0, -1, -1, -1, 0, 1, 2, 3, 3, 3, 3]; } eqGains = curve.slice(0, EQ_BANDS); const sliders = eqSlidersBox.querySelectorAll('.eq-slider'); sliders.forEach((sl, i) => { const g = eqGains[i] || 0; sl.value = g; const lbl = document.getElementById('eqGain' + i); lbl.textContent = (g > 0 ? '+' : '') + g.toFixed(1) + ' dB'; }); sendAllToDevice(); saveEQToDevice(); } async function reloadFromDevice() { try { const resp = await fetch('/api/eq/state'); if (!resp.ok) throw new Error('HTTP ' + resp.status); const data = await resp.json(); eqEnabledCheck.checked = !!data.enabled; if ('analyzer' in data) { anEnabledCheck.checked = !!data.analyzer; } if (Array.isArray(data.gains)) { eqGains = data.gains.slice(0, EQ_BANDS).map(g => parseFloat(g) || 0); const sliders = eqSlidersBox.querySelectorAll('.eq-slider'); sliders.forEach((sl, i) => { const g = eqGains[i] || 0; sl.value = g; const lbl = document.getElementById('eqGain' + i); lbl.textContent = (g > 0 ? '+' : '') + g.toFixed(1) + ' dB'; }); } } catch (e) { alert('Nie uda o si odczyta EQ z /api/eq/state'); } } function sendBandToDevice(band, gain) { const url = '/api/eq/band?band=' + encodeURIComponent(band) + '&gain=' + encodeURIComponent(gain.toFixed(2)); fetch(url).catch(() => {}); } function sendAllToDevice() { const params = new URLSearchParams(); eqGains.forEach((g, i) => { params.append('g' + i, g.toFixed(2)); }); fetch('/api/eq/set?' + params.toString()).catch(() => {}); } async function saveEQToDevice() { try { const resp = await fetch('/api/eq/save'); if (!resp.ok) throw new Error('HTTP ' + resp.status); alert('EQ zapisany do pliku.'); } catch (e) { alert('B d zapisu EQ: ' + e.message); } } eqEnabledCheck.addEventListener('change', () => { const on = eqEnabledCheck.checked ? 1 : 0; fetch('/api/eq/enable?on=' + on).catch(() => {}); }); anEnabledCheck.addEventListener('change', () => { const on = anEnabledCheck.checked ? 1 : 0; fetch('/api/eq/analyzer?on=' + on).catch(() => {}); }); createAnalyzerLabels(); createEqUI(); reloadFromDevice(); // ------- VOLUME przez WebSocket ------- const volSlider = document.getElementById('volSlider'); const volValue = document.getElementById('volValue'); let websocket = null; function setVolumeLabel(v) { volValue.textContent = v; } function updateSliderVolume() { const sliderValue = volSlider.value; setVolumeLabel(sliderValue); if (websocket && websocket.readyState === WebSocket.OPEN) { websocket.send("volume:" + sliderValue); } else { console.warn("WebSocket niepo czony"); } } function connectWebSocket() { websocket = new WebSocket('ws://' + window.location.hostname + '/ws'); websocket.onopen = function () { console.log("WebSocket po czony (EQ panel)"); }; websocket.onclose = function () { console.log("WebSocket zamkni ty – reconnect za 3s"); setTimeout(connectWebSocket, 3000); }; websocket.onerror = function (error) { console.error("B d WebSocket:", error); websocket.close(); }; websocket.onmessage = function (event) { if (event.data.startsWith("volume:")) { const vol = parseInt(event.data.split(":")[1]); if (!isNaN(vol)) { volSlider.value = vol; setVolumeLabel(vol); } } }; } volSlider.addEventListener('input', () => { setVolumeLabel(volSlider.value); }); volSlider.addEventListener('change', () => { updateSliderVolume(); }); volSlider.addEventListener("wheel", function (event) { event.preventDefault(); let currentValue = parseInt(volSlider.value); const step = parseInt(volSlider.step) || 1; const max = parseInt(volSlider.max); const min = parseInt(volSlider.min); if (event.deltaY < 0) { volSlider.value = Math.min(currentValue + step, max); } else { volSlider.value = Math.max(currentValue - step, min); } updateSliderVolume(); }); connectWebSocket(); // ------- Radiobrowser ------- const statusEl = document.getElementById('status'); const resultsEl = document.getElementById('results'); const API_RB = 'https://de1.api.radio-browser.info/json/stations/search'; document.getElementById('btnSearch').addEventListener('click', searchStations); document.getElementById('q').addEventListener('keydown', e => { if (e.key === 'Enter') searchStations(); }); async function searchStations() { const q = document.getElementById('q').value.trim(); const tag = document.getElementById('tag').value.trim(); const minBr = document.getElementById('minBr').value.trim(); const limit = document.getElementById('limit').value; if (!q && !tag) { statusEl.textContent = 'Podaj przynajmniej nazw lub tag.'; return; } statusEl.textContent = 'Szukam...'; resultsEl.innerHTML = ''; const params = new URLSearchParams(); if (q) params.append('name', q); if (tag) params.append('tag', tag); params.append('hidebroken', 'true'); params.append('limit', limit); if (minBr) params.append('bitrate_min', minBr); try { const resp = await fetch(API_RB + '?' + params.toString()); if (!resp.ok) throw new Error('HTTP ' + resp.status); const data = await resp.json(); if (!Array.isArray(data) || data.length === 0) { statusEl.textContent = 'Brak wynik w.'; return; } statusEl.textContent = 'Znaleziono ' + data.length + ' stacji.'; data.forEach(addStationRow); } catch (e) { statusEl.textContent = 'B d pobierania: ' + e.message; } } function addStationRow(s) { const div = document.createElement('div'); div.className = 'station'; const metaBox = document.createElement('div'); metaBox.className = 'station-meta-box'; const name = document.createElement('div'); name.className = 'station-name'; name.textContent = s.name || '(bez nazwy)'; const meta = document.createElement('div'); meta.className = 'station-meta'; const country = s.country || ''; const br = s.bitrate ? s.bitrate + ' kbps' : ''; const codec = s.codec || ''; meta.textContent = [country, br, codec].filter(Boolean).join(' • '); const url = document.createElement('div'); url.className = 'station-url'; url.textContent = s.url_resolved || s.url || ''; metaBox.appendChild(name); metaBox.appendChild(meta); metaBox.appendChild(url); const actions = document.createElement('div'); actions.className = 'station-actions'; const btnSet = document.createElement('button'); btnSet.className = 'btn-small'; btnSet.textContent = 'Ustaw w EVO'; btnSet.onclick = () => sendToEvo(s); actions.appendChild(btnSet); const btnCopy = document.createElement('button'); btnCopy.className = 'btn-small'; btnCopy.textContent = 'Kopiuj URL'; btnCopy.onclick = () => copyUrl(url.textContent); actions.appendChild(btnCopy); div.appendChild(metaBox); div.appendChild(actions); resultsEl.appendChild(div); } function copyUrl(url) { if (!url) return; navigator.clipboard.writeText(url) .then(() => statusEl.textContent = 'Skopiowano URL.') .catch(() => statusEl.textContent = 'Nie uda o si skopiowa .'); } function sendToEvo(st) { const url = st.url_resolved || st.url || ''; if (!url) return; const name = st.name || 'Radio'; const href = '/update?url=' + encodeURIComponent(url) + '&name=' + encodeURIComponent(name); statusEl.textContent = 'Wysy am URL do EVO...'; fetch(href) .then(() => statusEl.textContent = 'URL wys any.') .catch(() => statusEl.textContent = 'B d wysy ania URL.'); } </script> </body> </html>
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<title>EVO – Radiobrowser + EQ16</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
:root {
--bg: #111;
--card: #181818;
--fg: #eee;
--accent: #4caf50;
--accent-soft: #2e7d32;
--border: #333;
}
* { box-sizing: border-box; }
body {
margin: 0;
padding: 0;
font-family: system-ui, -apple-system, BlinkMacSystemFont,
"Segoe UI", sans-serif;
background: var(--bg);
color: var(--fg);
}
header {
padding: 10px 16px;
background: #222;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 8px;
}
header h1 {
margin: 0;
font-size: 18px;
}
header .host {
font-size: 12px;
color: #aaa;
}
main {
min-height: calc(100vh - 46px);
display: flex;
justify-content: center;
align-items: flex-start;
padding: 18px 8px 40px;
}
.card {
width: 100%;
max-width: 960px;
background: var(--card);
border-radius: 12px;
border: 1px solid var(--border);
padding: 14px 14px 18px;
box-shadow: 0 0 12px rgba(0,0,0,0.6);
}
h2 {
margin: 0 0 6px;
font-size: 15px;
}
/* ANALIZATOR */
.analyzer-header {
margin-bottom: 4px;
}
.analyzer-desc {
font-size: 11px;
color: #aaa;
margin-bottom: 6px;
}
#spectrumCanvas {
width: 100%;
height: 160px; /* wy sze okno analizatora */
background: #000;
border-radius: 6px;
border: 1px solid #444;
display: block;
}
.an-freq-labels {
display: flex;
justify-content: space-between;
margin-top: 4px;
font-size: 10px;
color: #ccc;
}
.an-freq-label {
min-width: 0;
text-align: center;
flex: 1 1 auto;
}
/* Korektor */
.eq-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
margin-top: 18px;
}
.eq-toggle-group {
display: flex;
align-items: center;
gap: 8px;
}
.eq-toggle {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
}
.eq-toggle input {
width: 16px;
height: 16px;
cursor: pointer;
}
.eq-info {
font-size: 11px;
color: #aaa;
margin-top: 4px;
margin-bottom: 8px;
}
.eq-sliders {
display: flex;
gap: 6px;
justify-content: space-between;
overflow-x: auto;
padding: 6px 2px 4px;
}
.eq-band {
display: flex;
flex-direction: column;
align-items: center;
font-size: 10px;
min-width: 24px;
}
.eq-slider-wrap {
height: 120px;
display: flex;
align-items: center;
justify-content: center;
}
.eq-slider {
-webkit-appearance: none;
appearance: none;
width: 120px;
height: 6px;
transform: rotate(-90deg);
background: #333;
border-radius: 4px;
outline: none;
}
.eq-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 10px;
height: 16px;
border-radius: 3px;
background: var(--accent);
cursor: pointer;
}
.eq-slider::-moz-range-thumb {
width: 10px;
height: 16px;
border-radius: 3px;
background: var(--accent);
cursor: pointer;
border: none;
}
.eq-band-label {
margin-top: 2px;
}
.eq-band-gain {
margin-top: 2px;
color: #ccc;
}
.eq-buttons {
margin-top: 8px;
display: flex;
flex-wrap: wrap;
gap: 6px;
font-size: 12px;
}
.eq-buttons button {
padding: 4px 8px;
border-radius: 6px;
border: 1px solid var(--border);
background: #333;
color: var(--fg);
cursor: pointer;
}
.eq-buttons button:hover {
background: #444;
}
/* Volume */
.vol-section {
margin-top: 14px;
font-size: 12px;
}
.vol-label-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
/* KRÓTSZY I WYŚRODKOWANY SUWAK GŁOŚNOŚCI */
.vol-slider {
-webkit-appearance: none;
appearance: none;
width: 40%; /* <-- tu ustaw długość, np. 40–60% */
height: 10px;
background: #4caf50;
border-radius: 5px;
outline: none;
display: block;
margin: 0 auto; /* wyśrodkowanie w rzędzie */
}
/* Radiobrowser */
.search-bar {
margin-top: 18px;
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.search-bar input[type="text"],
.search-bar input[type="number"],
.search-bar select {
padding: 5px 7px;
border-radius: 6px;
border: 1px solid var(--border);
background: #000;
color: var(--fg);
font-size: 13px;
}
.search-bar button {
padding: 6px 10px;
border-radius: 6px;
border: 1px solid var(--border);
background: var(--accent-soft);
color: var(--fg);
cursor: pointer;
font-size: 13px;
white-space: nowrap;
}
.search-bar button:hover {
background: var(--accent);
}
.status {
font-size: 12px;
color: #aaa;
margin-top: 6px;
}
.results {
max-height: 260px;
overflow-y: auto;
border-top: 1px solid var(--border);
margin-top: 6px;
padding-top: 4px;
}
.station {
display: flex;
gap: 8px;
align-items: center;
padding: 6px 4px;
border-bottom: 1px solid #222;
}
.station:nth-child(even) {
background: #151515;
}
.station-meta-box {
flex: 1;
}
.station-name {
font-size: 13px;
font-weight: 600;
}
.station-meta {
font-size: 11px;
color: #aaa;
}
.station-url {
font-size: 11px;
word-break: break-all;
color: #ccc;
}
.station-actions {
display: flex;
flex-direction: column;
gap: 3px;
}
.btn-small {
font-size: 11px;
padding: 3px 6px;
border-radius: 5px;
border: 1px solid var(--border);
background: #333;
color: var(--fg);
cursor: pointer;
white-space: nowrap;
}
.btn-small:hover { background: #444; }
@media (max-width: 640px) {
.eq-slider-wrap { height: 100px; }
}
</style>
</head>
<body>
<header>
<h1>EVO – Radiobrowser + EQ16</h1>
<div class="host">Host: <span id="hostInfo">?</span></div>
</header>
<main>
<div class="card">
<!-- ANALIZATOR -->
<div class="analyzer-header">
<h2>Analizator widma (wewn trzny EQ16)</h2>
</div>
<div class="analyzer-desc">
Pasek pokazuje aktualne pasma z analizatora w firmware
(odczyt z <code>/api/eq/spectrum</code>). Po bokach skala w dB.
</div>
<canvas id="spectrumCanvas"></canvas>
<div id="anFreqLabels" class="an-freq-labels"></div>
<!-- EQ -->
<div class="eq-header">
<h2>EQ16 – korektor graficzny</h2>
<div class="eq-toggle-group">
<label class="eq-toggle">
<input type="checkbox" id="eqEnabled">
<span>Korektor</span>
</label>
<label class="eq-toggle">
<input type="checkbox" id="anEnabled">
<span>Analizator</span>
</label>
</div>
</div>
<div class="eq-info">
16 pasm 20 Hz – 20 kHz, zakres -18 … +18 dB.
Zmiany wysy ane przez API do EVO (<code>/api/eq/*</code>).
</div>
<div id="eqSliders" class="eq-sliders"></div>
<div class="eq-buttons">
<button type="button" onclick="setAll(0)">P asko (0 dB)</button>
<button type="button" onclick="setAll(-6)">Wyciszenie pasm (-6 dB)</button>
<button type="button" onclick="setAll(6)">Podbicie pasm (+6 dB)</button>
<button type="button" onclick="applyPreset('rock')">Preset ROCK</button>
<button type="button" onclick="applyPreset('disco')">Preset DISCO</button>
<button type="button" onclick="applyPreset('pop')">Preset POP</button>
<button type="button" onclick="reloadFromDevice()">Odczytaj z EVO</button>
<button type="button" onclick="saveEQToDevice()">Zapisz EQ</button>
</div>
<!-- Volume -->
<div class="vol-section">
<div class="vol-label-row">
<span>G o no </span>
<span id="volValue">--</span>
</div>
<input type="range"
id="volSlider"
class="vol-slider"
min="1" max="42" step="1" value="1">
</div>
<!-- Radiobrowser -->
<h2 style="margin-top:18px;">Radiobrowser – wyszukiwarka stacji</h2>
<div class="search-bar">
<input id="q" type="text" placeholder="Nazwa / kraj">
<input id="tag" type="text" placeholder="Tag (rock, 80s...)">
<input id="minBr" type="number" placeholder="Min kbps" min="0" step="32">
<select id="limit">
<option value="20">20</option>
<option value="50" selected>50</option>
<option value="100">100</option>
</select>
<button id="btnSearch">Szukaj</button>
</div>
<div id="status" class="status">Podaj fraz i kliknij „Szukaj”.</div>
<div id="results" class="results"></div>
</div>
</main>
<script>
// Host info
document.getElementById('hostInfo').textContent =
window.location.hostname || 'localhost';
// ------- ANALIZATOR -------
const specCanvas = document.getElementById('spectrumCanvas');
const specCtx = specCanvas.getContext('2d');
// zakres wy wietlania skali
const DISPLAY_MIN_DB = -60;
const DISPLAY_MAX_DB = 6;
// peak-hold
let peakLevels = [];
let peakHold = [];
let peakAlpha = [];
const PEAK_HOLD_FRAMES = 1;
const PEAK_FALL_STEP = 0.12;
const PEAK_FADE_FACTOR = 0.6;
function resizeSpectrumCanvas() {
const rect = specCanvas.getBoundingClientRect();
specCanvas.width = rect.width;
specCanvas.height = rect.height;
}
window.addEventListener('resize', resizeSpectrumCanvas);
resizeSpectrumCanvas();
function drawSpectrum(levels) {
const ctx = specCtx;
const w = specCanvas.width;
const h = specCanvas.height;
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, w, h);
const marginLeft = 32;
const marginRight = 32;
const paddingTop = 10;
const paddingBottom = 20;
const innerW = w - marginLeft - marginRight;
const innerH = h - paddingTop - paddingBottom;
// siatka dB ( cznie dodatnie warto ci)
const dbTicks = [6, 0, -10, -20, -30, -40, -50, -60];
ctx.strokeStyle = '#333';
ctx.lineWidth = 1;
ctx.font = '9px system-ui, sans-serif';
ctx.fillStyle = '#888';
ctx.textBaseline = 'middle';
dbTicks.forEach(db => {
let norm = (db - DISPLAY_MIN_DB) / (DISPLAY_MAX_DB - DISPLAY_MIN_DB);
if (norm < 0) norm = 0;
if (norm > 1) norm = 1;
const y = paddingTop + innerH - norm * innerH;
ctx.beginPath();
ctx.moveTo(marginLeft, y);
ctx.lineTo(w - marginRight, y);
ctx.stroke();
const label = (db > 0 ? '+' : '') + db + ' dB';
ctx.textAlign = 'right';
ctx.fillText(label, marginLeft - 4, y);
ctx.textAlign = 'left';
ctx.fillText(label, w - marginRight + 4, y);
});
if (!levels || !levels.length) return;
const n = levels.length;
if (peakLevels.length !== n) {
peakLevels = new Array(n).fill(0);
peakHold = new Array(n).fill(0);
peakAlpha = new Array(n).fill(0);
}
const barGap = 3;
const totalGap = barGap * (n + 1);
const barWidth = Math.max(3, (innerW - totalGap) / n);
const steps = 18;
for (let i = 0; i < n; i++) {
// z firmware dostajemy 0..1 odpowiadaj ce -60..0 dB
const vIn = levels[i];
let dbVal = vIn * 60 - 60; // -60..0
if (dbVal < -60) dbVal = -60;
if (dbVal > 0) dbVal = 0;
// przeskalowanie do zakresu -60..+6 (wizualnie)
let v = (dbVal - DISPLAY_MIN_DB) / (DISPLAY_MAX_DB - DISPLAY_MIN_DB);
if (v < 0) v = 0;
if (v > 1) v = 1;
// peak-hold
if (v >= peakLevels[i]) {
peakLevels[i] = v;
peakHold[i] = 0;
peakAlpha[i] = 1.0;
} else {
if (peakHold[i] < PEAK_HOLD_FRAMES) {
peakHold[i]++;
} else {
peakLevels[i] = Math.max(0, peakLevels[i] - PEAK_FALL_STEP);
peakAlpha[i] = Math.max(0, peakAlpha[i] * PEAK_FADE_FACTOR);
}
}
const activeSteps = Math.round(v * steps);
const x = marginLeft + barGap + i * (barWidth + barGap);
const stepH = innerH / steps;
// kostki s upka
for (let s = 0; s < steps; s++) {
const y = paddingTop + innerH - (s + 1) * stepH;
if (s < activeSteps) {
const frac = (s + 1) / steps;
if (frac < 0.6) {
ctx.fillStyle = '#2e7d32';
} else if (frac < 0.85) {
ctx.fillStyle = '#f9a825';
} else {
ctx.fillStyle = '#e53935';
}
} else {
ctx.fillStyle = '#111';
}
ctx.fillRect(x, y, barWidth, stepH - 1);
}
// niebieski peak z wygaszaniem
const peakV = peakLevels[i];
const alpha = peakAlpha[i];
if (peakV > 0.001 && alpha > 0.01) {
const peakY = paddingTop + innerH - peakV * innerH;
ctx.save();
ctx.globalAlpha = alpha;
ctx.fillStyle = '#2196f3';
ctx.fillRect(x, peakY - 2, barWidth, 3);
ctx.restore();
}
}
ctx.fillStyle = '#1b5e20';
ctx.fillRect(marginLeft, h - paddingBottom + 4, innerW, 2);
}
async function spectrumLoop() {
const POLL_MS = 120;
while (true) {
let bands = [];
try {
const resp = await fetch('/api/eq/spectrum');
if (resp.ok) {
const data = await resp.json();
if (data && Array.isArray(data.bands)) {
bands = data.bands;
}
}
} catch (e) {
// brak endpointu – nic nie rysujemy
}
drawSpectrum(bands);
await new Promise(r => setTimeout(r, POLL_MS));
}
}
spectrumLoop();
// ------- EQ16 + cz stotliwo ci -------
const EQ_BANDS = 16;
const freqs = [];
(function computeFreqs() {
const f0 = 20, f1 = 20000;
for (let i = 0; i < EQ_BANDS; i++) {
const t = i / (EQ_BANDS - 1);
const f = f0 * Math.pow(f1 / f0, t);
freqs.push(f);
}
})();
const anFreqLabelsDiv = document.getElementById('anFreqLabels');
function createAnalyzerLabels() {
anFreqLabelsDiv.innerHTML = '';
for (let i = 0; i < EQ_BANDS; i++) {
const div = document.createElement('div');
div.className = 'an-freq-label';
const f = freqs[i];
div.textContent = f < 1000 ? Math.round(f) + ' Hz'
: (f / 1000).toFixed(1) + ' k';
anFreqLabelsDiv.appendChild(div);
}
}
const eqSlidersBox = document.getElementById('eqSliders');
const eqEnabledCheck = document.getElementById('eqEnabled');
const anEnabledCheck = document.getElementById('anEnabled');
let eqGains = new Array(EQ_BANDS).fill(0);
function createEqUI() {
eqSlidersBox.innerHTML = '';
for (let i = 0; i < EQ_BANDS; i++) {
const bandDiv = document.createElement('div');
bandDiv.className = 'eq-band';
const wrap = document.createElement('div');
wrap.className = 'eq-slider-wrap';
const slider = document.createElement('input');
slider.type = 'range';
slider.min = '-18';
slider.max = '18';
slider.step = '0.5';
slider.value = '0';
slider.className = 'eq-slider';
slider.dataset.band = String(i);
slider.addEventListener('input', onSliderInput);
slider.addEventListener('change', onSliderChange);
wrap.appendChild(slider);
bandDiv.appendChild(wrap);
const label = document.createElement('div');
label.className = 'eq-band-label';
const f = freqs[i];
label.textContent = f < 1000 ? Math.round(f) + ' Hz'
: (f/1000).toFixed(1) + ' k';
bandDiv.appendChild(label);
const gain = document.createElement('div');
gain.className = 'eq-band-gain';
gain.id = 'eqGain' + i;
gain.textContent = '0 dB';
bandDiv.appendChild(gain);
eqSlidersBox.appendChild(bandDiv);
}
}
function onSliderInput(e) {
const band = parseInt(e.target.dataset.band, 10);
const val = parseFloat(e.target.value);
const gainLabel = document.getElementById('eqGain' + band);
gainLabel.textContent = (val > 0 ? '+' : '') + val.toFixed(1) + ' dB';
}
function onSliderChange(e) {
const band = parseInt(e.target.dataset.band, 10);
const val = parseFloat(e.target.value);
eqGains[band] = val;
sendBandToDevice(band, val);
}
function setAll(v) {
eqGains = eqGains.map(() => v);
const sliders = eqSlidersBox.querySelectorAll('.eq-slider');
sliders.forEach((sl, i) => {
sl.value = v;
const lbl = document.getElementById('eqGain' + i);
lbl.textContent = (v > 0 ? '+' : '') + Number(v).toFixed(1) + ' dB';
});
sendAllToDevice();
}
// Presety ROCK / DISCO / POP
function applyPreset(name) {
let curve = new Array(EQ_BANDS).fill(0);
if (name === 'rock') {
curve = [5, 4, 3, 2, 1, 0, -1, -2, -2, -1, 0, 1, 2, 3, 4, 5];
} else if (name === 'disco') {
curve = [6, 5, 4, 3, 2, 1, 0, -1, -1, 0, 1, 2, 3, 4, 5, 6];
} else if (name === 'pop') {
curve = [3, 2, 1, 0, 0, 0, -1, -1, -1, 0, 1, 2, 3, 3, 3, 3];
}
eqGains = curve.slice(0, EQ_BANDS);
const sliders = eqSlidersBox.querySelectorAll('.eq-slider');
sliders.forEach((sl, i) => {
const g = eqGains[i] || 0;
sl.value = g;
const lbl = document.getElementById('eqGain' + i);
lbl.textContent = (g > 0 ? '+' : '') + g.toFixed(1) + ' dB';
});
sendAllToDevice();
saveEQToDevice();
}
async function reloadFromDevice() {
try {
const resp = await fetch('/api/eq/state');
if (!resp.ok) throw new Error('HTTP ' + resp.status);
const data = await resp.json();
eqEnabledCheck.checked = !!data.enabled;
if ('analyzer' in data) {
anEnabledCheck.checked = !!data.analyzer;
}
if (Array.isArray(data.gains)) {
eqGains = data.gains.slice(0, EQ_BANDS).map(g => parseFloat(g) || 0);
const sliders = eqSlidersBox.querySelectorAll('.eq-slider');
sliders.forEach((sl, i) => {
const g = eqGains[i] || 0;
sl.value = g;
const lbl = document.getElementById('eqGain' + i);
lbl.textContent = (g > 0 ? '+' : '') + g.toFixed(1) + ' dB';
});
}
} catch (e) {
alert('Nie uda o si odczyta EQ z /api/eq/state');
}
}
function sendBandToDevice(band, gain) {
const url = '/api/eq/band?band=' + encodeURIComponent(band) +
'&gain=' + encodeURIComponent(gain.toFixed(2));
fetch(url).catch(() => {});
}
function sendAllToDevice() {
const params = new URLSearchParams();
eqGains.forEach((g, i) => {
params.append('g' + i, g.toFixed(2));
});
fetch('/api/eq/set?' + params.toString()).catch(() => {});
}
async function saveEQToDevice() {
try {
const resp = await fetch('/api/eq/save');
if (!resp.ok) throw new Error('HTTP ' + resp.status);
alert('EQ zapisany do pliku.');
} catch (e) {
alert('B d zapisu EQ: ' + e.message);
}
}
eqEnabledCheck.addEventListener('change', () => {
const on = eqEnabledCheck.checked ? 1 : 0;
fetch('/api/eq/enable?on=' + on).catch(() => {});
});
anEnabledCheck.addEventListener('change', () => {
const on = anEnabledCheck.checked ? 1 : 0;
fetch('/api/eq/analyzer?on=' + on).catch(() => {});
});
createAnalyzerLabels();
createEqUI();
reloadFromDevice();
// ------- VOLUME przez WebSocket -------
const volSlider = document.getElementById('volSlider');
const volValue = document.getElementById('volValue');
let websocket = null;
function setVolumeLabel(v) {
volValue.textContent = v;
}
function updateSliderVolume() {
const sliderValue = volSlider.value;
setVolumeLabel(sliderValue);
if (websocket && websocket.readyState === WebSocket.OPEN) {
websocket.send("volume:" + sliderValue);
} else {
console.warn("WebSocket niepo czony");
}
}
function connectWebSocket() {
websocket = new WebSocket('ws://' + window.location.hostname + '/ws');
websocket.onopen = function () {
console.log("WebSocket po czony (EQ panel)");
};
websocket.onclose = function () {
console.log("WebSocket zamkni ty – reconnect za 3s");
setTimeout(connectWebSocket, 3000);
};
websocket.onerror = function (error) {
console.error("B d WebSocket:", error);
websocket.close();
};
websocket.onmessage = function (event) {
if (event.data.startsWith("volume:")) {
const vol = parseInt(event.data.split(":")[1]);
if (!isNaN(vol)) {
volSlider.value = vol;
setVolumeLabel(vol);
}
}
};
}
volSlider.addEventListener('input', () => {
setVolumeLabel(volSlider.value);
});
volSlider.addEventListener('change', () => {
updateSliderVolume();
});
volSlider.addEventListener("wheel", function (event) {
event.preventDefault();
let currentValue = parseInt(volSlider.value);
const step = parseInt(volSlider.step) || 1;
const max = parseInt(volSlider.max);
const min = parseInt(volSlider.min);
if (event.deltaY < 0) {
volSlider.value = Math.min(currentValue + step, max);
} else {
volSlider.value = Math.max(currentValue - step, min);
}
updateSliderVolume();
});
connectWebSocket();
// ------- Radiobrowser -------
const statusEl = document.getElementById('status');
const resultsEl = document.getElementById('results');
const API_RB = 'https://de1.api.radio-browser.info/json/stations/search';
document.getElementById('btnSearch').addEventListener('click', searchStations);
document.getElementById('q').addEventListener('keydown', e => {
if (e.key === 'Enter') searchStations();
});
async function searchStations() {
const q = document.getElementById('q').value.trim();
const tag = document.getElementById('tag').value.trim();
const minBr = document.getElementById('minBr').value.trim();
const limit = document.getElementById('limit').value;
if (!q && !tag) {
statusEl.textContent = 'Podaj przynajmniej nazw lub tag.';
return;
}
statusEl.textContent = 'Szukam...';
resultsEl.innerHTML = '';
const params = new URLSearchParams();
if (q) params.append('name', q);
if (tag) params.append('tag', tag);
params.append('hidebroken', 'true');
params.append('limit', limit);
if (minBr) params.append('bitrate_min', minBr);
try {
const resp = await fetch(API_RB + '?' + params.toString());
if (!resp.ok) throw new Error('HTTP ' + resp.status);
const data = await resp.json();
if (!Array.isArray(data) || data.length === 0) {
statusEl.textContent = 'Brak wynik w.';
return;
}
statusEl.textContent = 'Znaleziono ' + data.length + ' stacji.';
data.forEach(addStationRow);
} catch (e) {
statusEl.textContent = 'B d pobierania: ' + e.message;
}
}
function addStationRow(s) {
const div = document.createElement('div');
div.className = 'station';
const metaBox = document.createElement('div');
metaBox.className = 'station-meta-box';
const name = document.createElement('div');
name.className = 'station-name';
name.textContent = s.name || '(bez nazwy)';
const meta = document.createElement('div');
meta.className = 'station-meta';
const country = s.country || '';
const br = s.bitrate ? s.bitrate + ' kbps' : '';
const codec = s.codec || '';
meta.textContent = [country, br, codec].filter(Boolean).join(' • ');
const url = document.createElement('div');
url.className = 'station-url';
url.textContent = s.url_resolved || s.url || '';
metaBox.appendChild(name);
metaBox.appendChild(meta);
metaBox.appendChild(url);
const actions = document.createElement('div');
actions.className = 'station-actions';
const btnSet = document.createElement('button');
btnSet.className = 'btn-small';
btnSet.textContent = 'Ustaw w EVO';
btnSet.onclick = () => sendToEvo(s);
actions.appendChild(btnSet);
const btnCopy = document.createElement('button');
btnCopy.className = 'btn-small';
btnCopy.textContent = 'Kopiuj URL';
btnCopy.onclick = () => copyUrl(url.textContent);
actions.appendChild(btnCopy);
div.appendChild(metaBox);
div.appendChild(actions);
resultsEl.appendChild(div);
}
function copyUrl(url) {
if (!url) return;
navigator.clipboard.writeText(url)
.then(() => statusEl.textContent = 'Skopiowano URL.')
.catch(() => statusEl.textContent = 'Nie uda o si skopiowa .');
}
function sendToEvo(st) {
const url = st.url_resolved || st.url || '';
if (!url) return;
const name = st.name || 'Radio';
const href = '/update?url=' + encodeURIComponent(url) +
'&name=' + encodeURIComponent(name);
statusEl.textContent = 'Wysy am URL do EVO...';
fetch(href)
.then(() => statusEl.textContent = 'URL wys any.')
.catch(() => statusEl.textContent = 'B d wysy ania URL.');
}
</script>
</body>
</html>
DJCheester wrote:No is![]()
But tell me, because I use low volume because I have a PAM8403 audio amplifier on my PCB so the volume is from 3-6 to 21 and the analyzer shows poorly, ie low bars can you somehow raise it?
And the refresh rate, but I don't think there is much I can do about that...
I'll play around with it...
Quote:Avoid using floating point arithmetic float. Even though ESP32-S3 has a single precision hardware floating point unit, floating point calculations are always slower than integer calculations. If possible then use fixed point representations, a different method of integer representation, or convert part of the calculation to be integer only before switching to floating point.
efi222 wrote:In the pictures a little data from the radio.
DJCheester wrote:Master I have a couple of questions, is it possible to generally make the option in the MP3 player to make the file time count from the end ? Or do you just let the timer go as it starts playing and reset when it goes to the next file ?
Is it possible in the ESP to scroll through a file, i.e. when you hold down, for example, the next song, so that it does not skip to the next one but skips forward every, say, 5 seconds, something like a "seek" ?
And the last thing that bothers me is that when I don't have a hotspot on (I use my phone and for example forget my phone), the radio is useless. Is it possible to run an MP3 player in this mode when there is no net?
On Monday I will upload a new version of your code and test.
ejcon wrote:- what's that because I don't get it ?Thought for 17s
ejcon wrote:Example of test set:
static const float ANALYZER_MAX_INPUT_LEVEL = 0.45f;
static const float ANALYZER_AGC_TARGET = 0.50f;
static constat ANALYZER_AGC_SPEED = 0.95f;