logo elektroda
logo elektroda
X
logo elektroda

Internet radio and audio file player on ESP32-S3

MAJSTER XXL 162324 1755
ADVERTISEMENT
Treść została przetłumaczona polish » english Zobacz oryginalną wersję tematu
📢 Listen (AI):
  • ADVERTISEMENT
  • #1652 21759123
    MAJSTER XXL
    Level 29  
    @robgold I am personally disturbed by all distractions, and on the radio I mainly care about theme music, even news stations I skip- TV has already effectively discouraged me, so I stayed with radio.
  • #1653 21759260
    DJCheester
    Level 26  
    kacha36 wrote:
    Hi. This is probably my last attempt to program the ESP32-S3-N16R8. Something is definitely doing "wrong", I just don't know what. At the moment I have IDE v. 2.3.6, Teensy installed from within the IDE, some changes to the audio libraries and after a while of compiling the message - "Arduino\libraries\Audio_-_Adafruit_Fork/AudioStream.h:71:10: fatal error: kinetis.h: No such file or directory".
    What do I still need to install so that I can finally compile the file correctly and upload it to the ESP?


    After checking the installation instructions given to me by DJCheester, the same error with kinetis.h still occurs.


    I was the one who created this manual, I see that you want to compile the Evo radio and remove the Adafruit library because it is the library used when compiling the Majster. Robgold's soft uses u8g2lib for the screen.

    Greetings...

    Added after 5 [minutes]:

    Oh and you may have to clear the temp folder because that's where the Arduino saves temp files.

    I can't remember the paths by heart I can check tomorrow and provide.

    Greetings...

    Added after 2 [hours] 7 [minutes]:

    ejcon wrote:
    My suggestion pseudo-analyzer as style 5 and 6 For those who like gadgets.


    Hi I have uploaded this your main.cpp file via Arduino renaming it to main.ino so that it can be opened in the Arduino environment (earlier in programmer's notepad I saw that this is the same code as in Arduino)

    It successfully compiled and uploaded to the radio.

    OLED display shows audio equalizer visualization with text Radio Italo 4 You OLED display showing audio spectrum analyzer with radio playback

    I've had a bit of a look at the workings of this pseudo analyser, and yes, to my eye the bars repeat every three jumps probably more in voltage (or digital value) than in a real analyser.

    The question is whether this generally in code has a breakdown of band frequencies ?

    Because yes I can honestly say it looks very artificial, probably the best of all is the peak hold on the bars.

    I once built something like this

    DIY stereo radio with vacuum tubes and front-panel sound visualizer

    And here it is clearly divided into frequencies the first one on the left is responsible for the bass and the last one for the treble

    Is it possible to make this analyser work like this?
    For me, there doesn't have to be so many bars it could be 10 - 12, but it would work better.

    Regards...

    Added after 10 [minutes]:

    MAJSTER XXL wrote:
    From me today such a new "settings menu" for version v3, to activate it I used another button of the remote control (in my case the PROG button). And how it works:


    Majster 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?

    And also tell me me or have you changed something in the speed of connection of the radio with the wifi and the time until the stream starts playing. And kol Robgold it happens very fast, at you this waiting time sometimes is in minutes ?

    I will upload a new version of your code on Monday and test it.

    Greetings....
  • ADVERTISEMENT
  • #1654 21759382
    efi222
    Level 20  
    DJCheester wrote:
    I've had a bit of a look at the workings of this pseudo-analyser, and yes, to my eye the bars repeat every three jumps probably more in voltage (or digital value) than in a real analyser.

    And what does it look like in motion?
    In my opinion this ESP already has a lot of work to do with the radio. On the FFT, it's already rather running out of olbig power.
  • #1655 21759392
    robgold
    Level 21  
    These are simple bars calculated from VU values. For mp3 streams the real FFT was still working fine, but from what my colleague and I were discussing FLAC streams required too much power for the FFT to work. For me personally it is a radio and should work as a radio. The FLAC streams are more important than the analyser. If anyone wants to add one for themselves a colleague has provided the code.
  • #1656 21759396
    DJCheester
    Level 26  
    Well, it doesn't look like much, but could a smaller number of bars be used, e.g. 5, 7, but with audio bands?

    Greetings ...
  • #1657 21759402
    khoam
    Level 42  
    efi222 wrote:
    For the FFT, it will rather run out of calculating power.


    This can be done using fixed-point arithmetic. Then the calculations perform more than 10 times faster. GCC offers support for fixed-position arithmetic using custom types as an extension to the C language.

    https://github.com/espressif/esp-dsp
  • #1658 21759405
    efi222
    Level 20  
    DJCheester wrote:
    Well, it doesn't look interesting, and would it be possible to fire off fewer bars, e.g. 5, 7, but it would go with the audio bands ?
    There is unlikely to be a difference. You have to extract fragments from the whole band anyway.
  • ADVERTISEMENT
  • #1659 21759431
    robgold
    Level 21  
    DJCheester wrote:
    Well, it doesn't look interesting, and would fewer bars be able to fire up e.g. 5, 7, but it would go with the audio bands?

    Greetings ...


    The first version with the equalizer provided by colleague @ejcon works like this but long term this requires maintaining your own version and tweaking the audio library.
  • ADVERTISEMENT
  • #1660 21759435
    DJCheester
    Level 26  
    I think the focus should be on radio.

    This version is underdeveloped also in terms of the software, when for example selecting mute on this analyser a strange screen appears, besides that when silence on the stream 1-2 dashes light up on the screen.

    So if it's going to be a problem to get this in place and working properly then I think you'd have to drop it and focus more on the audio side of things i.e. radio and streams. I would also suggest a music player 😁

    But an interesting adventure with this analyser 😁 however, I'm going back to my friend's original software 3.19.33 on Monday.

    Greetings....
  • #1661 21759573
    ejcon
    Level 14  
    Hello,
    I have a version with a real equalizer and eqalizer but there is a problem with the AAC and FLAC standard on these programs the equalizer is turned on it cuts the transmission and jerks .
    There is a prefix for automatic operation when there is ACC and FLAC then 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 .
    <!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>
    
    Corrected version of the FLAC equaliser Correction of the auto level input analyser . Added automation with display of equalizer status .
  • #1662 21759610
    DJCheester
    Level 26  
    Fantastic, I'm going to check it out at my place tomorrow.
    Maybe they could add some conditions where on MP3 stations you can automatically turn it on and on flaac stations some screen not available or something like that.

    Greetings....
  • #1664 21759628
    DJCheester
    Level 26  
    Broken down a very nice old radio is my personal assessment.

    The design itself maybe cool but not how it fits in such a case.

    Greetings....
  • #1665 21759645
    ArturAVS
    Moderator
    Watching it, I wondered, is this restoration or profanity? As for the radio itself, I really like all those icons and their placement on the LCD. I have some vintage equipment myself for restoration and modification, however, my rule of thumb is not to interfere with the factory front panel.
  • #1666 21759773
    DJCheester
    Level 26  
    ArturAVS wrote:
    my policy is not to tamper with the factory front panel.


    I agree with this, I myself am currently assembling this radio in a housing left over from a Schneider tuner which is part of a 1980's tower set.

    Rule of thumb, no changes to the casing, only what if a hole in the back for a wifi screw-on antenna becomes necessary.

    I have attached some photos in this topic already of this equipment.

    Greetings....
  • #1667 21759845
    MAJSTER XXL
    Level 29  
    DJCheester wrote:
    And tell me if maybe you changed something in the speed of connection of the radio to the wifi and the time until the stream starts playing. And kol Robgold it happens very fast, at you this waiting time sometimes is in minutes ?

    I have described this before, with me the v3 radio starts in a few seconds, this long connection problem is caused by swapped files libesp_netif.a liblwip.a Try to restore the correct ones by e.g. uninstalling the esp32 version in the Board Manager in the Arduino IDE and reinstalling the latest version 3.3.3.


    Screenshot of Arduino IDE with esp32 3.3.3 installed and ESP32S3 code shown

    And this is what the log looks like to me from power up to running the audio from the stream, so it's in seconds:
    Code: C / C++
    Log in, to see the code


    As you can see in the logs, by the time the info from the audio appears I've loaded the calendar and weather beforehand, so it takes as long as it normally has to, well not minutes as you can see.
  • #1668 21759853
    DJCheester
    Level 26  
    MAJSTER XXL wrote:
    I have described this before, with me the v3 radio starts in a few seconds, this long connection problem is caused by substituted libesp_netif.a liblwip.a files Try to restore the correct ones by e.g. uninstalling the esp32 version in the Board Manager in the Arduino IDE and reinstalling the latest version 3.3.3.


    And somehow I missed it, ok I will try to restore core from version 3.2.0 at first

    Greetings...
  • #1669 21759860
    simw
    Level 27  
    MAJSTER XXL wrote:
    on my v3 radio starts in a few seconds, this long connection problem is caused by swapped libesp_netif.a liblwip.a files

    In my own case, I cannot confirm this.
    In the last pages there was an up-to-date description of the installation with the swapped files, which I used and the radio takes a few seconds to boot.
  • #1670 21759865
    DJCheester
    Level 26  
    simw wrote:
    MAJSTER XXL wrote:
    at my radio v3 starts in a few seconds, this long connection problem is caused by swapped libesp_netif.a liblwip.a files

    In my own case, I cannot confirm this.
    In the last tabs there was an up-to-date description of the installation with the swapped files, which I used and the radio starts a few seconds.


    Did you use this manual in pdf ?

    https://www.elektroda.pl/rtvforum/viewtopic.php?p=21752133#21752133

    And what Arduino IDE and windows do you have ?

    Greetings ...
  • #1671 21759867
    robgold
    Level 21  
    >>21759853 substituted libraries for higher WiFi throughput support core ESP up to 3.2.0 maximum. On newer they can do problems. I think Majster's v3 will run on 3.2.0 as well as on the new core. For 3.3.3 you just need to restore the two old files that were swapped (I assume everyone backed up when doing this swap) but this will block you from listening to FLAC stations.
  • #1672 21759869
    DJCheester
    Level 26  
    Well not everyone has kept the old ones 😁 I don't have a click replace.

    Send as you have back up original files.

    I will swap temporarily just to test the Majster software.

    Regards...
  • #1673 21759875
    MAJSTER XXL
    Level 29  
    I checked again just now, replacing these files results in better playback of flac streams from radio stations without the scrobbling, but unfortunately long connection after power restart even sometimes with reset and esp32 hang. Restoring these files, on the other hand, results in a problem with the flac streams (not all of them) with a grunt, but no problem with fast linking to stations after a power restart.
  • #1674 21759876
    simw
    Level 27  
    DJCheester wrote:
    Did you use this pdf manual ?

    No, this is not the pdf. A couple of tabs back is the description:
    https://www.elektroda.pl/rtvforum/viewtopic.php?p=21748612#21748612

    As for the environment, in order not to mess with the arduino configuration for version 3.18 of the radio, I installed the portable version 2.3.6.
    It's important that it's a portable version, because the current zip from the Arduino pages didn't work for me to run as a "sandbox", there are probably problems with version 2 in this topic, but somehow I didn't dig further. Grok gave up :)
    I reinstalled from scratch according to the above description as to each library version, including swapping the two libraries. It compiled without errors, but to be fair, I didn't check that exactly these lib swaps worked, in fact I don't know how to check, but the new files are in the correct directory.

    Arduino downloaded from here:
    https://portable.info.pl/arduino_portable/
    It creates the whole environment in a separate directory, so I currently have versions 3.18 and 3.19 separately.
    You can "develop" versions independently.
  • #1676 21759891
    DJCheester
    Level 26  
    simw wrote:
    Creates the entire environment in a separate directory, so I currently have versions 3.18 and 3.19 separately.
    You can independently "develop" versions for yourself.


    And don't these version portals use the Arduino15 directory where the core is downloaded ?

    Greetings ...

    Added after 1 [minute]:

    I need 3.2.0 because I also want to upload the Robgold version and this will only temporarily swap.

    Greetings ...
  • #1677 21759896
    simw
    Level 27  
    DJCheester wrote:
    Are not these version portals using the Arduino15 directory where the core is downloaded ?

    As far as I have been able to determine it does not, Arduino15 is created inside the directory.
    This is more or less what the tree looks like. The "Projects" directory I created myself.
    The whole thing takes up almost 7.4 GiB once I have adapted to compile version 3.19.33.

    Arduino folder structure with subfolders like Arduino15 and Projekty
  • #1678 21759898
    DJCheester
    Level 26  
    simw wrote:
    The whole thing takes up almost 7.4 GiB once I have adapted to compile version 3.19.33.


    Well, that's something I have to do for myself then I'll have two versions at once 😁

    Regards...
  • #1679 21759908
    robgold
    Level 21  
    Just out of curiosity you can check the stream of our native station smoothjazz.com.pl Link in flac version. I'm in contact with them and have just set up a larger buffer for the FLAC steum on the Icecast server. I'm curious if this also helps on unaltered libraries? Because on the substituted version the radio plays wonderfully today. Zero micro-breaks, zero information from the audio library about a possible weak stream. @MAJSTER XXL can you check?
  • #1680 21759926
    simw
    Level 27  
    robgold wrote:
    as a matter of curiosity you can check the stream of our home station smoothjazz.com.pl

    1411kbps yes it comes out, but somewhere my DAC got lost and I can't hear it playing :)
    Found it and it plays without any breathlessness once it gets going. The first few seconds he crouches a bit. Maybe it's because he still has to push something for the series....
📢 Listen (AI):

Topic summary

The discussion centers on the development of an internet radio and audio file player based on the ESP32-S3-WROOM-1 module, featuring a custom-designed prototype PCB with OLED display and user controls including rotary encoders and buttons. Key challenges addressed include pin spacing discrepancies in the ESP32-S3 module footprint, integration of Wi-Fi connectivity with dynamic station list updates, and handling of Polish character encoding on the OLED display. The project uses Arduino IDE (version 2.3.2) with ESP-IDF support and requires enabling PSRAM. Audio playback supports MP3, AAC, and FLAC streams, with the ESP32-audioI2S library recommended over the incompatible Audio library. Users reported issues with SPI MISO pin assignment causing bootloader conflicts, resolved by reassigning MISO to pin 35. The project incorporates WiFiManager for network configuration, EEPROM and SD card storage for saving last played station and settings, and includes plans for tone control via an external KA2107 equalizer and a CS8673 amplifier module. Problems with encoder input stability and memory limitations for Bluetooth A2DP on ESP32-S3 were noted. The community suggested alternatives like KaRadio and ESP32-MiniWebRadio projects. Debugging tips include serial terminal logs for HTTP errors and flash memory erasure to resolve boot loops. The project is open-source on GitHub, encouraging forks and modifications. Additional features under development include browser-based updates, directory navigation, and potential audio recording to SD card.
Summary generated by the language model.
ADVERTISEMENT