Traducteur de code Morse
Saisissez votre texte : la traduction s’affiche en direct, et vous pouvez l’écouter à la vitesse de votre choix.
Vitesse
20 WPM
Tonalité
600 Hz
Chronologie standard PARIS : point = 1 temps, trait = 3 temps, espace entre signes = 1 temps, entre lettres = 3 temps, entre mots = 7 temps. À 20 WPM, un temps dure 60 ms.
Le code source
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="author" content="F4HXN">
<meta name="description" content="Traducteur de code Morse avec lecture audio a vitesse reglable (WPM), ponctuation et chiffres - F4HXN">
<meta name="copyright" content="CC0 1.0 Universel">
<title>Traducteur de Code Morse — F4HXN</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
<style>
:root{
--bleu:#0076a5;
--bleu-clair:#298ad4;
--encre:#0f1e28;
--gris:#5a6b75;
--trait:#e2e8ec;
--fond-doux:#f5f8fa;
--blanc:#ffffff;
}
*{box-sizing:border-box}
.fmorse{
font-family:"DM Sans",system-ui,sans-serif;
color:var(--encre);
max-width:1200px;
margin:0 auto;
line-height:1.6;
}
.fmorse .fm-wrap{
background:var(--blanc);
border:1px solid var(--trait);
border-radius:14px;
padding:28px;
}
.fmorse h2.fm-titre{
font-size:1.5rem;
font-weight:700;
margin:0 0 4px;
color:var(--encre);
letter-spacing:-.01em;
}
.fmorse .fm-sous{
margin:0 0 22px;
color:var(--gris);
font-size:.95rem;
}
.fmorse label{
display:block;
font-size:.82rem;
font-weight:600;
text-transform:uppercase;
letter-spacing:.05em;
color:var(--bleu);
margin:0 0 8px;
}
.fmorse textarea{
width:100%;
min-height:96px;
resize:vertical;
font-family:"DM Sans",sans-serif;
font-size:1.05rem;
line-height:1.5;
color:var(--encre);
background:var(--blanc);
border:1px solid var(--trait);
border-radius:10px;
padding:14px 16px;
transition:border-color .15s ease, box-shadow .15s ease;
}
.fmorse textarea:focus{
outline:none;
border-color:var(--bleu-clair);
box-shadow:0 0 0 3px rgba(41,138,212,.15);
}
.fmorse .fm-sortie-bloc{margin-top:22px}
.fmorse .fm-sortie{
font-family:"JetBrains Mono",ui-monospace,monospace;
font-size:1.2rem;
line-height:2;
letter-spacing:.02em;
color:var(--bleu);
background:var(--fond-doux);
border:1px solid var(--trait);
border-radius:10px;
padding:16px 18px;
min-height:64px;
word-break:break-word;
white-space:pre-wrap;
}
.fmorse .fm-sortie:empty::before{
content:"Le code Morse s’affichera ici.";
color:var(--gris);
font-family:"DM Sans",sans-serif;
font-size:.95rem;
letter-spacing:normal;
}
.fmorse .fm-car{
display:inline-block;
padding:0 2px;
border-radius:4px;
transition:background .08s linear, color .08s linear;
}
.fmorse .fm-car.actif{
background:var(--bleu-clair);
color:var(--blanc);
}
.fmorse .fm-controles{
display:grid;
grid-template-columns:1fr 1fr;
gap:20px;
margin-top:22px;
padding:20px;
background:var(--fond-doux);
border:1px solid var(--trait);
border-radius:10px;
}
.fmorse .fm-reglage .fm-reglage-tete{
display:flex;
justify-content:space-between;
align-items:baseline;
margin-bottom:8px;
}
.fmorse .fm-reglage-tete span:first-child{
font-size:.82rem;
font-weight:600;
text-transform:uppercase;
letter-spacing:.05em;
color:var(--bleu);
}
.fmorse .fm-valeur{
font-family:"JetBrains Mono",monospace;
font-weight:700;
font-size:.95rem;
color:var(--encre);
}
.fmorse input[type=range]{
width:100%;
height:6px;
-webkit-appearance:none;
appearance:none;
background:var(--trait);
border-radius:3px;
outline:none;
}
.fmorse input[type=range]::-webkit-slider-thumb{
-webkit-appearance:none;
appearance:none;
width:20px;height:20px;border-radius:50%;
background:var(--bleu);
cursor:pointer;
border:3px solid var(--blanc);
box-shadow:0 0 0 1px var(--trait);
}
.fmorse input[type=range]::-moz-range-thumb{
width:20px;height:20px;border-radius:50%;
background:var(--bleu);
cursor:pointer;
border:3px solid var(--blanc);
}
.fmorse input[type=range]:focus-visible::-webkit-slider-thumb{
box-shadow:0 0 0 3px rgba(41,138,212,.35);
}
.fmorse .fm-boutons{
display:flex;
flex-wrap:wrap;
gap:12px;
margin-top:22px;
}
.fmorse button{
font-family:"DM Sans",sans-serif;
font-size:1rem;
font-weight:600;
padding:12px 22px;
border-radius:10px;
cursor:pointer;
border:1px solid transparent;
transition:background .15s ease, opacity .15s ease;
}
.fmorse .fm-jouer{background:var(--bleu);color:var(--blanc)}
.fmorse .fm-jouer:hover{background:var(--bleu-clair)}
.fmorse .fm-stop{background:var(--blanc);color:var(--bleu);border-color:var(--trait)}
.fmorse .fm-stop:hover{background:var(--fond-doux)}
.fmorse .fm-copier{background:var(--blanc);color:var(--gris);border-color:var(--trait)}
.fmorse .fm-copier:hover{background:var(--fond-doux)}
.fmorse button:disabled{opacity:.45;cursor:not-allowed}
.fmorse button:focus-visible{outline:2px solid var(--bleu-clair);outline-offset:2px}
.fmorse .fm-note{
margin-top:16px;
font-size:.85rem;
color:var(--gris);
}
@media (max-width:640px){
.fmorse .fm-wrap{padding:20px}
.fmorse .fm-controles{grid-template-columns:1fr}
.fmorse .fm-boutons button{flex:1 1 auto}
}
@media (prefers-reduced-motion:reduce){
.fmorse *{transition:none!important}
}
</style>
</head>
<body>
<div class="fmorse">
<div class="fm-wrap">
<h2 class="fm-titre">Traducteur de code Morse</h2>
<p class="fm-sous">Saisissez votre texte : la traduction s’affiche en direct, et vous pouvez l’écouter à la vitesse de votre choix.</p>
<label for="fm-texte">Texte à traduire</label>
<textarea id="fm-texte" placeholder="Entrez votre texte ici — lettres, chiffres et ponctuation sont pris en charge."></textarea>
<div class="fm-sortie-bloc">
<label for="fm-sortie">Code Morse</label>
<div id="fm-sortie" class="fm-sortie" aria-live="polite"></div>
</div>
<div class="fm-controles">
<div class="fm-reglage">
<div class="fm-reglage-tete">
<span>Vitesse</span>
<span class="fm-valeur"><span id="fm-wpm-val">20</span> WPM</span>
</div>
<input type="range" id="fm-wpm" min="5" max="35" step="1" value="20" aria-label="Vitesse en mots par minute">
</div>
<div class="fm-reglage">
<div class="fm-reglage-tete">
<span>Tonalité</span>
<span class="fm-valeur"><span id="fm-freq-val">600</span> Hz</span>
</div>
<input type="range" id="fm-freq" min="400" max="900" step="10" value="600" aria-label="Fréquence de la tonalité en hertz">
</div>
</div>
<div class="fm-boutons">
<button type="button" class="fm-jouer" id="fm-jouer">Écouter</button>
<button type="button" class="fm-stop" id="fm-stop" disabled>Arrêter</button>
<button type="button" class="fm-copier" id="fm-copier">Copier le Morse</button>
</div>
<p class="fm-note">Chronologie standard PARIS : point = 1 temps, trait = 3 temps, espace entre signes = 1 temps, entre lettres = 3 temps, entre mots = 7 temps. À 20 WPM, un temps dure 60 ms.</p>
</div>
</div>
<script>
(function(){
var MORSE = {
'A':'.-','B':'-...','C':'-.-.','D':'-..','E':'.','F':'..-.','G':'--.',
'H':'....','I':'..','J':'.---','K':'-.-','L':'.-..','M':'--','N':'-.',
'O':'---','P':'.--.','Q':'--.-','R':'.-.','S':'...','T':'-','U':'..-',
'V':'...-','W':'.--','X':'-..-','Y':'-.--','Z':'--..',
'0':'-----','1':'.----','2':'..---','3':'...--','4':'....-','5':'.....',
'6':'-....','7':'--...','8':'---..','9':'----.',
'.':'.-.-.-',',':'--..--','?':'..--..',"'":'.----.','!':'-.-.--',
'/':'-..-.','(':'-.--.',')':'-.--.-','&':'.-...',':':'---...',
';':'-.-.-.','=':'-...-','+':'.-.-.','-':'-....-','_':'..--.-',
'"':'.-..-.','$':'...-..-','@':'.--.-.',
'É':'..-..','È':'.-..-','À':'.--.-','Ç':'-.-..','Ù':'..--'
};
var texte = document.getElementById('fm-texte');
var sortie = document.getElementById('fm-sortie');
var wpm = document.getElementById('fm-wpm');
var freq = document.getElementById('fm-freq');
var wpmVal = document.getElementById('fm-wpm-val');
var freqVal= document.getElementById('fm-freq-val');
var bJouer = document.getElementById('fm-jouer');
var bStop = document.getElementById('fm-stop');
var bCopier= document.getElementById('fm-copier');
var audioCtx = null;
var timers = [];
var enLecture = false;
var lettres = []; // modele : { code, span, motAvant }
// --- Traduction en direct + construction du modele ---
function traduire(){
var t = texte.value;
sortie.innerHTML = '';
lettres = [];
var motAvant = false; // un espace precede-t-il la prochaine lettre ?
var premiere = true;
for (var i=0;i<t.length;i++){
var brut = t[i];
if (/\s/.test(brut)){
if (!premiere) motAvant = true;
sortie.appendChild(document.createTextNode(' '));
continue;
}
var code = MORSE[brut.toUpperCase()];
if (!code) continue; // caractere non pris en charge : ignore
var span = document.createElement('span');
span.className = 'fm-car';
span.textContent = code;
sortie.appendChild(span);
sortie.appendChild(document.createTextNode(' '));
lettres.push({ code: code, span: span, motAvant: motAvant });
motAvant = false;
premiere = false;
}
}
function texteMorse(){
return texte.value.toUpperCase().split('').map(function(c){
if (/\s/.test(c)) return '/';
return MORSE[c] || '';
}).filter(function(x){return x!=='';}).join(' ');
}
// --- Lecture audio, timing PARIS ---
function stop(){
enLecture = false;
timers.forEach(clearTimeout);
timers = [];
if (audioCtx){ try{ audioCtx.close(); }catch(e){} audioCtx = null; }
var actifs = sortie.querySelectorAll('.fm-car.actif');
for (var i=0;i<actifs.length;i++) actifs[i].classList.remove('actif');
bStop.disabled = true;
bJouer.disabled = false;
}
function jouer(){
stop();
if (!lettres.length) return;
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
var u = 1.2 / parseInt(wpm.value,10); // duree d'un temps, en secondes
var f = parseInt(freq.value,10);
var t0 = audioCtx.currentTime + 0.15;
var curseur = t0;
var osc = audioCtx.createOscillator();
var gain = audioCtx.createGain();
osc.type = 'sine';
osc.frequency.value = f;
gain.gain.value = 0;
osc.connect(gain).connect(audioCtx.destination);
osc.start(t0);
function bip(debut, duree){
var r = 0.006; // rampe anti-clic
gain.gain.setValueAtTime(0, debut);
gain.gain.linearRampToValueAtTime(0.28, debut + r);
gain.gain.setValueAtTime(0.28, debut + duree - r);
gain.gain.linearRampToValueAtTime(0, debut + duree);
}
enLecture = true;
bJouer.disabled = true;
bStop.disabled = false;
for (var i=0;i<lettres.length;i++){
var L = lettres[i];
// espace AVANT la lettre : 3 temps entre lettres, 7 temps entre mots
if (i > 0) curseur += (L.motAvant ? 7*u : 3*u);
// surlignage synchronise
(function(span, debut){
timers.push(setTimeout(function(){
if(!enLecture) return;
var a = sortie.querySelectorAll('.fm-car.actif');
for (var k=0;k<a.length;k++) a[k].classList.remove('actif');
span.classList.add('actif');
}, (debut - t0) * 1000));
})(L.span, curseur);
// signes de la lettre, avec 1 temps entre chaque signe
for (var j=0;j<L.code.length;j++){
var d = L.code[j]==='-' ? 3*u : u;
bip(curseur, d);
curseur += d;
if (j < L.code.length - 1) curseur += u;
}
}
var finMs = (curseur - t0) * 1000 + 250;
timers.push(setTimeout(function(){ if(enLecture) stop(); }, finMs));
}
// --- Evenements ---
texte.addEventListener('input', function(){ if(enLecture) stop(); traduire(); });
wpm.addEventListener('input', function(){ wpmVal.textContent = wpm.value; });
freq.addEventListener('input', function(){ freqVal.textContent = freq.value; });
bJouer.addEventListener('click', jouer);
bStop.addEventListener('click', stop);
bCopier.addEventListener('click', function(){
var m = texteMorse();
if(!m) return;
navigator.clipboard.writeText(m).then(function(){
var ancien = bCopier.textContent;
bCopier.textContent = 'Copié';
setTimeout(function(){ bCopier.textContent = ancien; }, 1200);
});
});
traduire();
})();
</script>
</body>
</html>