{"id":2946,"date":"2026-02-23T18:20:13","date_gmt":"2026-02-23T17:20:13","guid":{"rendered":"https:\/\/hb9id.ch\/?page_id=2946"},"modified":"2026-03-24T16:44:38","modified_gmt":"2026-03-24T15:44:38","slug":"morsetrainer","status":"publish","type":"page","link":"https:\/\/hb9id.ch\/index.php\/morsetrainer\/","title":{"rendered":"HB9ID Morsetrainer"},"content":{"rendered":"    <div id=\"hb9id-plugin-container\">\n        <link href=\"https:\/\/fonts.googleapis.com\/css2?family=Figtree:wght@400;600;700&display=swap\" rel=\"stylesheet\">\n        \n        <style>\n            \/* Base *\/\n            .hb9id-morse-wrapper { \n                font-family: 'Figtree', sans-serif; \n                background: #fff; \n                padding: 15px; \n                max-width: 1000px; \n                margin: 0 auto; \n                color: #333; \n                line-height: 1.3; \n                box-sizing: border-box; \n            }\n            .hb9id-morse-wrapper h3 { font-size: 20px; margin: 0 0 4px 0; font-weight: 700; color: #222; }\n            .hb9id-morse-wrapper h5 { font-size: 13px; color: #777; margin: 0 0 15px 0; font-weight: 400; }\n            .hb9id-morse-wrapper h4 { \n                font-size: 13px; margin: 10px 0 6px 0; color: #999; \n                text-transform: uppercase; letter-spacing: 0.5px; \n                border-bottom: 1px solid #eee; padding-bottom: 2px; \n            }\n\n            \/* Grid Layout *\/\n            .hb9id-main-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; }\n\n            \/* Cards *\/\n            .hb9id-card { background: #f9f9f9; padding: 12px; border-radius: 6px; border: 1px solid #eee; margin-bottom: 8px; }\n\n            \/* Form Elements *\/\n            .hb9id-morse-wrapper textarea, .hb9id-morse-wrapper input[type=\"text\"] { \n                font-family: 'Figtree', sans-serif; font-size: 14px; padding: 10px; \n                border: 1px solid #ccc; border-radius: 4px; width: 100%; box-sizing: border-box; resize: vertical; \n            }\n            .hb9id-morse-wrapper textarea:focus, .hb9id-morse-wrapper input[type=\"text\"]:focus { \n                border-color: #DD9933; outline: none; box-shadow: 0 0 0 2px rgba(221, 153, 51, 0.2); \n            }\n            .hb9id-morse-wrapper input[type=\"text\"] { text-transform: uppercase; font-weight: bold; }\n\n            \/* Sliders *\/\n            .hb9id-slider-group { margin-bottom: 10px; }\n            .hb9id-slider-group label { font-size: 12px; font-weight: 600; color: #444; display: flex; justify-content: space-between; margin-bottom: 4px; }\n            .hb9id-slider-group input[type=\"range\"] { -webkit-appearance: none; appearance: none; width: 100%; background: transparent; cursor: pointer; margin: 4px 0; }\n            .hb9id-slider-group input[type=\"range\"]:focus { outline: none; }\n            .hb9id-slider-group input[type=\"range\"]::-webkit-slider-runnable-track { width: 100%; height: 6px; background: #e0e0e0; border-radius: 4px; border: 1px solid #ccc; }\n            .hb9id-slider-group input[type=\"range\"]::-webkit-slider-thumb { height: 16px; width: 16px; border-radius: 50%; background: #DD9933; border: 2px solid #fff; cursor: pointer; -webkit-appearance: none; margin-top: -6px; box-shadow: 0 1px 3px rgba(0,0,0,0.3); }\n            .hb9id-slider-group input[type=\"range\"]::-moz-range-track { width: 100%; height: 6px; background: #e0e0e0; border-radius: 4px; border: 1px solid #ccc; }\n            .hb9id-slider-group input[type=\"range\"]::-moz-range-thumb { height: 16px; width: 16px; border-radius: 50%; background: #DD9933; border: 2px solid #fff; cursor: pointer; box-shadow: 0 1px 3px rgba(0,0,0,0.3); }\n\n            \/* Buttons *\/\n            .hb9id-btn-group { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; }\n            .hb9id-btn { \n                flex: 1; padding: 10px 12px; font-size: 13px; font-weight: 700; color: #fff; \n                background: #555; border: none; border-radius: 4px; cursor: pointer; transition: background 0.2s; text-align: center; \n            }\n            .hb9id-btn:hover { background: #333; }\n            .hb9id-btn-primary { background: #DD9933; }\n            .hb9id-btn-primary:hover { background: #c5862b; }\n            .hb9id-btn-danger { background: #e74c3c; }\n            .hb9id-btn-danger:hover { background: #c0392b; }\n            .hb9id-btn-success { background: #2ecc71; }\n            .hb9id-btn-success:hover { background: #27ae60; }\n            .hb9id-btn-purple { background: #9b59b6; }\n            .hb9id-btn-purple:hover { background: #8e44ad; }\n            .hb9id-btn:disabled { background: #bdc3c7; cursor: not-allowed; }\n\n            \/* Karaoke Display *\/\n            #karaokeDisplay { \n                font-family: monospace; font-size: 26px; background: #fff; padding: 12px; \n                border: 2px dashed #DD9933; border-radius: 6px; min-height: 38px; \n                text-align: center; letter-spacing: 2px; color: #333; font-weight: bold; margin-bottom: 15px; \n            }\n            .highlight { background-color: #DD9933; color: white !important; border-radius: 3px; padding: 0 2px; }\n\n\t\t\t\/* Char Grid *\/\n            .char-grid { \n                display: grid; grid-template-columns: repeat(auto-fill, minmax(55px, 1fr)); gap: 6px; \n                background: #fff; padding: 10px; border: 1px solid #ddd; border-radius: 4px; \n                font-size: 14px; \n            }\n            .char-grid label { cursor: pointer; font-family: monospace; font-size: 14px; font-weight: bold; margin: 0; }\n            .char-grid input[type=\"checkbox\"] { margin-right: 5px; accent-color: #DD9933; cursor: pointer; }\n\n            \/* Test Area *\/\n            #testArea { display: none; background: #fdfafeb3; padding: 15px; border-radius: 6px; border: 2px dashed #9b59b6; margin-top: 15px; }\n            #countdownDisplay { font-size: 42px; font-weight: bold; text-align: center; color: #9b59b6; margin: 15px 0; }\n            #testResult { margin-top: 12px; padding: 10px; border-radius: 4px; font-weight: bold; font-size: 14px; text-align: center; }\n\n            @media (max-width: 768px) {\n                .hb9id-main-grid { grid-template-columns: 1fr; }\n            }\n        <\/style>\n\n        <div class=\"hb9id-morse-wrapper\">\n            <h3>HB9ID Morsecode Trainer<\/h3>\n            <h5>von HB9INE (v.1.4-BT-Fix) \u2013 Interaktives Training & Pr\u00fcfungsmodus<\/h5>\n\n            <div id=\"karaokeDisplay\">BEREIT...<\/div>\n\n            <div class=\"hb9id-main-grid\">\n                <div>\n                    <h4>Manuelle Eingabe<\/h4>\n                    <div class=\"hb9id-card\">\n                        <textarea id=\"textInput\" rows=\"2\">CQ CQ HB9INE CQ CQ HB9INE<\/textarea>\n                        <div class=\"hb9id-btn-group\" style=\"margin-top: 8px;\">\n                            <button class=\"hb9id-btn hb9id-btn-primary\" onclick=\"playMorse()\">\u25b6 Abspielen<\/button>\n                            <button class=\"hb9id-btn hb9id-btn-danger\" onclick=\"stopMorse()\">\u25a0 Stopp<\/button>\n                        <\/div>\n                    <\/div>\n\n                    <h4>Audio Parameter<\/h4>\n                    <div class=\"hb9id-card\">\n                        <div class=\"hb9id-slider-group\">\n                            <label>Geschwindigkeit (WPM): <span id=\"wpmVal\">20<\/span><\/label>\n                            <input type=\"range\" id=\"wpm\" min=\"5\" max=\"40\" value=\"20\" oninput=\"document.getElementById('wpmVal').innerText = this.value\">\n                        <\/div>\n                        <div class=\"hb9id-slider-group\">\n                            <label>Frequenz (Hz): <span id=\"freqVal\">550<\/span><\/label>\n                            <input type=\"range\" id=\"freq\" min=\"450\" max=\"800\" value=\"550\" oninput=\"document.getElementById('freqVal').innerText = this.value\">\n                        <\/div>\n                        <div class=\"hb9id-slider-group\">\n                            <label>Zeichen-Abstand: <span id=\"charSpaceVal\">5<\/span><\/label>\n                            <input type=\"range\" id=\"charSpace\" min=\"3\" max=\"15\" value=\"5\" oninput=\"document.getElementById('charSpaceVal').innerText = this.value\">\n                        <\/div>\n                        <div class=\"hb9id-slider-group\" style=\"margin-bottom: 0;\">\n                            <label>Wort-Abstand: <span id=\"wordSpaceVal\">10<\/span><\/label>\n                            <input type=\"range\" id=\"wordSpace\" min=\"7\" max=\"25\" value=\"10\" oninput=\"document.getElementById('wordSpaceVal').innerText = this.value\">\n                        <\/div>\n                    <\/div>\n                <\/div>\n\n                <div>\n                    <h4>Trainings-Generator<\/h4>\n                    <div class=\"hb9id-card\">\n                        <div style=\"display: grid; grid-template-columns: 1fr 1fr; gap: 15px;\">\n                            <div class=\"hb9id-slider-group\">\n                                <label>Zeichen \/ Wort: <span id=\"maxCharsVal\">5<\/span><\/label>\n                                <input type=\"range\" id=\"maxChars\" min=\"1\" max=\"9\" value=\"5\" oninput=\"document.getElementById('maxCharsVal').innerText = this.value\">\n                            <\/div>\n                            <div class=\"hb9id-slider-group\">\n                                <label>Anzahl W\u00f6rter: <span id=\"numWordsVal\">8<\/span><\/label>\n                                <input type=\"range\" id=\"numWords\" min=\"1\" max=\"20\" value=\"8\" oninput=\"document.getElementById('numWordsVal').innerText = this.value\">\n                            <\/div>\n                        <\/div>\n                        \n                        <label style=\"font-size: 12px; font-weight: 600; color: #444; margin: 8px 0 4px 0; display: block;\">Zeichenauswahl:<\/label>\n                        <div class=\"char-grid\" id=\"charSelection\"><\/div>\n                        \n                        <div class=\"hb9id-btn-group\">\n                            <button class=\"hb9id-btn\" style=\"flex: 0.5;\" onclick=\"toggleAll(true)\">Alle<\/button>\n                            <button class=\"hb9id-btn\" style=\"flex: 0.5;\" onclick=\"toggleAll(false)\">Keine<\/button>\n                            <button class=\"hb9id-btn hb9id-btn-success\" onclick=\"generateAndPlayTest()\">Generieren & Start<\/button>\n                        <\/div>\n                        <div class=\"hb9id-btn-group\" style=\"margin-top: 8px;\">\n                            <button class=\"hb9id-btn hb9id-btn-purple\" id=\"startTestBtn\" onclick=\"startBlindTest()\">Blindtest starten<\/button>\n                        <\/div>\n                    <\/div>\n\n                    <div id=\"testArea\">\n                        <h4 style=\"color: #9b59b6; border-bottom: 1px solid #ebd4f4; margin-top:0;\">Pr\u00fcfungsmodus<\/h4>\n                        <div id=\"countdownDisplay\" style=\"display: none;\">7<\/div>\n                        <div id=\"testInputArea\" style=\"display: none;\">\n                            <input type=\"text\" id=\"userTestInput\" placeholder=\"Was hast du geh\u00f6rt?\" onkeypress=\"if(event.key==='Enter') checkTestAnswer()\">\n                            <div class=\"hb9id-btn-group\">\n                                <button class=\"hb9id-btn hb9id-btn-success\" onclick=\"checkTestAnswer()\">\u2714 Pr\u00fcfen<\/button>\n                                <button class=\"hb9id-btn hb9id-btn-danger\" onclick=\"stopMorse()\">\u25a0 Abbruch<\/button>\n                            <\/div>\n                            <div id=\"testResult\"><\/div>\n                        <\/div>\n                    <\/div>\n                <\/div>\n            <\/div>\n        <\/div>\n\n        <script>\n        (function() {\n            const morseDict = {\n                'A': '.-', 'B': '-...', 'C': '-.-.', 'D': '-..', 'E': '.', 'F': '..-.',\n                'G': '--.', 'H': '....', 'I': '..', 'J': '.---', 'K': '-.-', 'L': '.-..',\n                'M': '--', 'N': '-.', 'O': '---', 'P': '.--.', 'Q': '--.-', 'R': '.-.',\n                'S': '...', 'T': '-', 'U': '..-', 'V': '...-', 'W': '.--', 'X': '-..-',\n                'Y': '-.--', 'Z': '--..', '1': '.----', '2': '..---', '3': '...--',\n                '4': '....-', '5': '.....', '6': '-....', '7': '--...', '8': '---..',\n                '9': '----.', '0': '-----', '\u00c4': '.-.-', '\u00d6': '---.', '\u00dc': '..--'\n            };\n\n            const wordList = [\"DER\", \"DIE\", \"DAS\", \"UND\", \"IST\", \"FUNK\", \"CQ\", \"73\", \"DX\", \"QTH\", \"NAME\", \"WATT\", \"YL\", \"OM\"];\n\n            let audioCtx = null;\n            let activeOscillators = [];\n            let animationFrameId = null;\n            let timings = [];\n            let isPlaying = false;\n            let hiddenTestText = \"\";\n            let countdownTimer = null;\n            let isBlindTestMode = false;\n\n            \/\/ UI Init\n            window.initCharSelection = function() {\n                const container = document.getElementById('charSelection');\n                if(!container) return;\n                container.innerHTML = '';\n                Object.keys(morseDict).forEach(char => {\n                    let div = document.createElement('div');\n                    div.style.display = 'flex';\n                    div.style.alignItems = 'center';\n                    div.innerHTML = `<input type=\"checkbox\" id=\"cb-${char}\" value=\"${char}\" checked>\n                                     <label for=\"cb-${char}\">${char}<\/label>`;\n                    container.appendChild(div);\n                });\n            };\n\n            window.toggleAll = function(state) {\n                document.querySelectorAll('#charSelection input[type=\"checkbox\"]').forEach(cb => cb.checked = state);\n            };\n\n            function playTone(startTime, duration, freq) {\n                if(!audioCtx) return;\n                const osc = audioCtx.createOscillator();\n                const gain = audioCtx.createGain();\n                osc.type = 'sine';\n                osc.frequency.value = freq;\n                gain.gain.setValueAtTime(0, startTime);\n                gain.gain.linearRampToValueAtTime(1, startTime + 0.01);\n                gain.gain.setValueAtTime(1, startTime + duration - 0.01);\n                gain.gain.linearRampToValueAtTime(0, startTime + duration);\n                osc.connect(gain);\n                gain.connect(audioCtx.destination);\n                osc.start(startTime);\n                osc.stop(startTime + duration);\n                activeOscillators.push(osc);\n            }\n\n            window.stopMorse = function() {\n                isPlaying = false;\n                if(animationFrameId) cancelAnimationFrame(animationFrameId);\n                if(countdownTimer) clearInterval(countdownTimer);\n                document.getElementById('countdownDisplay').style.display = 'none';\n                document.getElementById('startTestBtn').disabled = false;\n                activeOscillators.forEach(osc => { try { osc.stop(); } catch(e){} });\n                activeOscillators = [];\n                document.querySelectorAll('#karaokeDisplay span').forEach(s => s.classList.remove('highlight'));\n            };\n\n            window.playMorse = function(customText) {\n                stopMorse();\n                if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();\n                if (audioCtx.state === 'suspended') audioCtx.resume();\n\n                isBlindTestMode = (typeof customText === 'string');\n                const rawText = isBlindTestMode ? customText.toUpperCase() : document.getElementById('textInput').value.toUpperCase();\n                \n                let text = \"\";\n                for(let char of rawText) if(morseDict[char] || char === ' ') text += char;\n                \n                const display = document.getElementById('karaokeDisplay');\n                display.innerHTML = '';\n                if(isBlindTestMode) {\n                    display.innerHTML = '<span style=\"color:#9b59b6;\">[ PR\u00dcFUNG L\u00c4UFT... ]<\/span>';\n                } else {\n                    for(let i=0; i<text.length; i++) {\n                        let span = document.createElement('span');\n                        span.innerText = text[i] === ' ' ? '\\u00A0' : text[i];\n                        span.id = 'char-' + i;\n                        display.appendChild(span);\n                    }\n                }\n\n                const wpm = parseInt(document.getElementById('wpm').value);\n                const freq = parseInt(document.getElementById('freq').value);\n                const dot = 1.2 \/ wpm;\n                const cSpace = parseInt(document.getElementById('charSpace').value);\n                const wSpace = parseInt(document.getElementById('wordSpace').value);\n\n                \/\/ --- BLUETOOTH FIX: 1.2 Sekunden Vorlaufzeit ---\n                let currentTime = audioCtx.currentTime + 1.2;\n                timings = [];\n\n                for (let i = 0; i < text.length; i++) {\n                    const char = text[i];\n                    let start = currentTime;\n                    if (char === ' ') {\n                        currentTime += dot * (wSpace - cSpace);\n                        timings.push({ index: i, start: start, end: currentTime });\n                        continue;\n                    }\n                    const code = morseDict[char];\n                    if (code) {\n                        for (let s of code) {\n                            let d = (s === '.') ? dot : dot * 3;\n                            playTone(currentTime, d, freq);\n                            currentTime += d + dot;\n                        }\n                        currentTime += dot * (cSpace - 1);\n                        timings.push({ index: i, start: start, end: currentTime });\n                    }\n                }\n                isPlaying = true;\n                updateKaraoke();\n            };\n\n            function updateKaraoke() {\n                if (!isPlaying) return;\n                let now = audioCtx.currentTime;\n                let activeIdx = -1;\n                for (let t of timings) if (now >= t.start && now <= t.end) { activeIdx = t.index; break; }\n                \n                if(!isBlindTestMode) {\n                    let spans = document.getElementById('karaokeDisplay').children;\n                    for(let i=0; i<spans.length; i++) {\n                        if(i === activeIdx) spans[i].classList.add('highlight');\n                        else spans[i].classList.remove('highlight');\n                    }\n                }\n                if (timings.length > 0 && now > timings[timings.length-1].end) { isPlaying = false; return; }\n                animationFrameId = requestAnimationFrame(updateKaraoke);\n            }\n\n            window.generateAndPlayTest = function() {\n                const boxes = document.querySelectorAll('#charSelection input:checked');\n                const selected = Array.from(boxes).map(b => b.value);\n                if(!selected.length) return alert(\"W\u00e4hle Zeichen aus!\");\n                \n                const max = parseInt(document.getElementById('maxChars').value);\n                const num = parseInt(document.getElementById('numWords').value);\n                let words = [];\n                for(let i=0; i<num; i++) {\n                    let w = \"\"; for(let j=0; j<max; j++) w += selected[Math.floor(Math.random()*selected.length)];\n                    words.push(w);\n                }\n                document.getElementById('textInput').value = words.join(\" \");\n                playMorse();\n            };\n\n            window.startBlindTest = function() {\n                const boxes = document.querySelectorAll('#charSelection input:checked');\n                const selected = Array.from(boxes).map(b => b.value);\n                if(!selected.length) return;\n                \n                const max = parseInt(document.getElementById('maxChars').value);\n                const num = parseInt(document.getElementById('numWords').value);\n                let words = [];\n                for(let i=0; i<num; i++) {\n                    let w = \"\"; for(let j=0; j<max; j++) w += selected[Math.floor(Math.random()*selected.length)];\n                    words.push(w);\n                }\n                hiddenTestText = words.join(\" \");\n                \n                document.getElementById('testArea').style.display = 'block';\n                document.getElementById('testInputArea').style.display = 'none';\n                document.getElementById('testResult').innerHTML = '';\n                document.getElementById('userTestInput').value = '';\n                document.getElementById('startTestBtn').disabled = true;\n                \n                let count = 7;\n                const cd = document.getElementById('countdownDisplay');\n                cd.style.display = 'block';\n                cd.innerText = count;\n                \n                countdownTimer = setInterval(() => {\n                    count--;\n                    if(count > 0) cd.innerText = count;\n                    else if(count === 0) {\n                        cd.innerText = \"LOS!\";\n                        document.getElementById('testInputArea').style.display = 'block';\n                        document.getElementById('userTestInput').focus();\n                        playMorse(hiddenTestText);\n                    } else { clearInterval(countdownTimer); cd.style.display = 'none'; }\n                }, 1000);\n            };\n\n            window.checkTestAnswer = function() {\n                let u = document.getElementById('userTestInput').value.toUpperCase().trim();\n                let a = hiddenTestText.toUpperCase().trim();\n                let res = document.getElementById('testResult');\n                if(u === a) {\n                    res.style.background = '#d4edda'; res.style.color = '#155724'; res.style.border = '1px solid #c3e6cb'; res.innerHTML = \"\u2705 Richtig!\";\n                } else {\n                    res.style.background = '#f8d7da'; res.style.color = '#721c24'; res.style.border = '1px solid #f5c6cb'; res.innerHTML = \"\u274c Falsch! L\u00f6sung: \" + a;\n                }\n            };\n\n            initCharSelection();\n        })();\n        <\/script>\n    <\/div>\n    \n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"has-text-align-center wp-block-paragraph\">Findest du unsere L\u00f6sungen n\u00fctzlich? Dann unterst\u00fctze uns mit einer Spende!<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"768\" height=\"350\" src=\"https:\/\/hb9id.ch\/wp-content\/uploads\/2026\/03\/TWINT_Individueller-Betrag_DE-768x350-1.png\" alt=\"\" class=\"wp-image-3053\" srcset=\"https:\/\/hb9id.ch\/wp-content\/uploads\/2026\/03\/TWINT_Individueller-Betrag_DE-768x350-1.png 768w, https:\/\/hb9id.ch\/wp-content\/uploads\/2026\/03\/TWINT_Individueller-Betrag_DE-768x350-1-300x137.png 300w\" sizes=\"auto, (max-width: 768px) 100vw, 768px\" \/><\/figure>\n","protected":false},"excerpt":{"rendered":"<p>Findest du unsere L\u00f6sungen n\u00fctzlich? Dann unterst\u00fctze uns mit einer Spende!<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":"","_members_access_role":[],"_members_access_error":""},"class_list":["post-2946","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/hb9id.ch\/index.php\/wp-json\/wp\/v2\/pages\/2946","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/hb9id.ch\/index.php\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/hb9id.ch\/index.php\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/hb9id.ch\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/hb9id.ch\/index.php\/wp-json\/wp\/v2\/comments?post=2946"}],"version-history":[{"count":5,"href":"https:\/\/hb9id.ch\/index.php\/wp-json\/wp\/v2\/pages\/2946\/revisions"}],"predecessor-version":[{"id":3119,"href":"https:\/\/hb9id.ch\/index.php\/wp-json\/wp\/v2\/pages\/2946\/revisions\/3119"}],"wp:attachment":[{"href":"https:\/\/hb9id.ch\/index.php\/wp-json\/wp\/v2\/media?parent=2946"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}