SoundEd Logo
  • Home
  • About
  • Events / Get Connected
    • Get Connected
    • Coaching Growth
    • Title I Music Ed Conference
  • Session Topics
  • Contact
  • Facebook
  • Instagram
SoundEd – Drone Tuner Pro body { font-family: ‘Segoe UI’, system-ui, sans-serif; background: #121212; color: white; text-align: center; margin: 0; padding: 20px; overflow-x: hidden;} .container { max-width: 600px; margin: 0 auto; background: #1e1e1e; padding: 20px; border-radius: 20px; box-shadow: 0 10px 40px rgba(0,0,0,0.6); position: relative; z-index: 5; } .header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 10px; } .title-img { max-width: 250px; height: auto; display: block; margin: 0 auto; } .settings-btn { background: transparent; color: #888; font-size: 1.8rem; padding: 0 10px; width: auto; cursor: pointer; border: none; transition: 0.2s; } .settings-btn:hover { color: white; transform: rotate(90deg); } #note-name { font-size: 7rem; font-weight: 900; color: #333; margin: 0; line-height: 1; transition: color 0.1s; } #cents-display { color: #888; height: 1.5rem; margin-bottom: 10px; font-variant-numeric: tabular-nums; transition: opacity 0.2s; font-size: 1.2rem; } #cents-display.hidden { opacity: 0; } .visualizer-box { position: relative; width: 300px; height: 300px; margin: 10px auto; } #ears-mask { display: none; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: #2a2a2a; z-index: 10; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; color: #888; font-size: 1.8rem; opacity: 0; transition: opacity 0.3s; pointer-events: none; } #ears-mask.active { opacity: 1; pointer-events: auto; } canvas { background: #000; border-radius: 50%; width: 100%; height: 100%; } /* FOCUS STREAK BAR */ .streak-container { width: 100%; height: 20px; background: #111; border-radius: 10px; margin: 20px 0 10px; overflow: hidden; border: 1px solid #333; opacity: 0; transition: opacity 0.3s; } .streak-container.visible { opacity: 1; } #streak-fill { height: 100%; width: 0%; background: linear-gradient(90deg, #00ff00, #adff2f); box-shadow: 0 0 15px rgba(0,255,0,0.5); } .controls { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 20px; } button, select { padding: 12px; border-radius: 8px; border: none; font-size: 14px; cursor: pointer; background: #333; color: white; transition: 0.2s; } #toggleSystemBtn { grid-column: span 2; font-size: 1.3rem; font-weight: bold; padding: 15px; } .btn-start { background: #00ff00; color: black; } .btn-stop { background: #ff4444; color: white; } .label { font-size: 11px; color: #888; text-transform: uppercase; margin-top: 5px; text-align: left; padding-left: 5px;} .diff-group { grid-column: span 2; display: flex; gap: 5px; background: #252525; padding: 5px; border-radius: 8px; } .diff-btn { flex: 1; background: transparent; color: #888; font-size: 0.8rem; padding: 8px; font-weight: bold; border: none; } .diff-btn.selected { background: #444; color: white; box-shadow: 0 2px 5px rgba(0,0,0,0.3); border-radius: 6px; } .modal-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.9); z-index: 100; justify-content: center; align-items: center; } .modal { background: #222; padding: 30px; border-radius: 15px; width: 90%; max-width: 400px; text-align: left; border: 1px solid #444; } #run-warning { color: #ff4444; font-weight: bold; background: rgba(255,68,68,0.1); padding: 10px; border-radius: 5px; margin-bottom: 15px; font-size: 0.8rem; border: 1px solid rgba(255,68,68,0.3); } .meter-container { width: 100%; height: 12px; background: #111; border-radius: 6px; overflow: hidden; margin-bottom: 20px; border: 1px solid #333; } .meter-fill { height: 100%; width: 0%; background: #00ff00; transition: width 0.05s; } input[type=range] { width: 100%; accent-color: #00ff00; margin: 10px 0; cursor: pointer; } /* FIXED LOGO CSS: Added relative positioning and high z-index */ .footer-logo { max-width: 300px; height: auto; margin: 30px auto 10px; display: block; opacity: 0.9; position: relative; z-index: 10; } /* Confetti Canvas stays in background layer */ #confetti-canvas { position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 1; }

System Settings

⚠️ SYSTEM STOPPED. Start to test mic levels.

Microphone Input Level

Noise Gate (Sensitivity)

Drone Tuner
—
0 cents
EARS ONLY
Key Transposition
Key of C Key of Bb Key of Eb Key of F
Drone Tone
Rich (Triangle) Bright (Saw) Pure (Sine)
SoundEd.org let audioCtx, analyser, droneOsc, droneGain, micStream; let isRunning = false, smoothedCents = 0, strobeAngle = 0, showStrobe = true, animationId, noiseGate = 0.015; let streakStartTime = null; let streakActive = false; const STREAK_THRESHOLD_TIME = 1000; const STREAK_TOTAL_TIME = 6000; let difficulty = { smoothing: 0.96, greenZone: 10, yellowZone: 20, strobeSpeed: 0.005, streakRange: 5 }; const notes = [“C”, “C#”, “D”, “D#”, “E”, “F”, “F#”, “G”, “G#”, “A”, “A#”, “B”]; const canvas = document.getElementById(‘tunerCanvas’), ctx = canvas.getContext(‘2d’), toggleBtn = document.getElementById(‘toggleSystemBtn’); const streakContainer = document.getElementById(‘streak-container’), streakFill = document.getElementById(‘streak-fill’); function toggleModal() { const modal = document.getElementById(‘settingsModal’); modal.style.display = modal.style.display === ‘flex’ ? ‘none’ : ‘flex’; document.getElementById(‘run-warning’).style.display = isRunning ? ‘none’ : ‘block’; } function playTestTone() { if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)(); const osc = audioCtx.createOscillator(), gain = audioCtx.createGain(); osc.connect(gain); gain.connect(audioCtx.destination); osc.frequency.value = 440; gain.gain.setValueAtTime(0.3, audioCtx.currentTime); gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.5); osc.start(); osc.stop(audioCtx.currentTime + 0.5); } document.getElementById(‘gateSlider’).oninput = function() { noiseGate = parseFloat(this.value); } function setDifficulty(level) { document.querySelectorAll(‘.diff-btn’).forEach(b => b.classList.remove(‘selected’)); event.target.classList.add(‘selected’); if (level === ‘student’) difficulty = { smoothing: 0.96, greenZone: 10, yellowZone: 20, strobeSpeed: 0.005, streakRange: 5 }; else if (level === ‘advanced’) difficulty = { smoothing: 0.90, greenZone: 6, yellowZone: 12, strobeSpeed: 0.015, streakRange: 3 }; else difficulty = { smoothing: 0.80, greenZone: 3, yellowZone: 7, strobeSpeed: 0.04, streakRange: 1 }; resetStreak(); } async function startSystem() { if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)(); try { micStream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: false, noiseSuppression: false }}); const source = audioCtx.createMediaStreamSource(micStream); analyser = audioCtx.createAnalyser(); analyser.fftSize = 4096; source.connect(analyser); droneOsc = audioCtx.createOscillator(); droneGain = audioCtx.createGain(); droneGain.gain.setValueAtTime(0, audioCtx.currentTime); droneOsc.connect(droneGain); droneGain.connect(audioCtx.destination); droneOsc.start(); isRunning = true; render(); toggleBtn.innerText = “STOP SYSTEM”; toggleBtn.className = “btn-stop”; } catch (e) { alert(“Mic Access Required.”); } } function stopSystem() { isRunning = false; if (droneGain) droneGain.gain.setTargetAtTime(0, audioCtx.currentTime, 0.05); if (micStream) micStream.getTracks().forEach(track => track.stop()); cancelAnimationFrame(animationId); resetStreak(); ctx.clearRect(0, 0, 300, 300); document.getElementById(‘note-name’).innerText = “–“; document.getElementById(‘note-name’).style.color = “#333”; document.getElementById(‘cents-display’).innerText = “0 cents”; toggleBtn.innerText = “START SYSTEM”; toggleBtn.className = “btn-start”; } toggleBtn.onclick = () => isRunning ? stopSystem() : startSystem(); function resetStreak() { streakStartTime = null; streakActive = false; streakContainer.classList.remove(‘visible’); streakFill.style.width = “0%”; particles = []; } function autoCorrelate(buf, sampleRate) { let rms = 0; for (let i=0; i<buf.length; i++) rms += buf[i]*buf[i]; rms = Math.sqrt(rms/buf.length); document.getElementById('micMeter').style.width = Math.min(rms * 1000, 100) + "%"; if (rms < noiseGate) return -1; let r1=0, r2=buf.length-1, thres=0.2; for (let i=0; i<buf.length/2; i++) if (Math.abs(buf[i])<thres) { r1=i; break; } for (let i=1; i<buf.length/2; i++) if (Math.abs(buf[buf.length-i])<thres) { r2=buf.length-i; break; } buf = buf.slice(r1,r2); let c = new Float32Array(buf.length).fill(0); for (let i=0; i<buf.length; i++) for (let j=0; jc[d+1]) d++; let maxval=-1, maxpos=-1; for (let i=d; i maxval) { maxval = c[i]; maxpos = i; } return sampleRate/maxpos; } function render() { if (!isRunning) return; const buffer = new Float32Array(analyser.fftSize); analyser.getFloatTimeDomainData(buffer); const freq = autoCorrelate(buffer, audioCtx.sampleRate); ctx.clearRect(0, 0, 300, 300); if (freq !== -1 && freq > 50 && freq 0 ? “+” : “”) + Math.round(smoothedCents) + ” cents”; if (Math.abs(smoothedCents) = STREAK_THRESHOLD_TIME) { streakContainer.classList.add(‘visible’); let progress = ((elapsed – STREAK_THRESHOLD_TIME) / (STREAK_TOTAL_TIME – STREAK_THRESHOLD_TIME)) * 100; streakFill.style.width = Math.min(progress, 100) + “%”; if (progress >= 100) { streakActive = true; addFlowConfetti(); } } } else { resetStreak(); } drawVisuals(smoothedCents); } else { droneGain.gain.setTargetAtTime(0, audioCtx.currentTime, 0.2); resetStreak(); } updateConfetti(); animationId = requestAnimationFrame(render); } function drawVisuals(cents) { const cx = 150, cy = 150, absCents = Math.abs(cents); let color = “#ff4444”; if (absCents <= difficulty.greenZone) color = "#00ff00"; else if (absCents <= difficulty.yellowZone) color = "#ffcc00"; document.getElementById('note-name').style.color = (absCents < difficulty.yellowZone + 10) ? color : "#333"; ctx.strokeStyle = color; ctx.lineWidth = 6; ctx.lineCap = "round"; ctx.beginPath(); const rad = (cents * 1.5 – 90) * (Math.PI / 180); ctx.moveTo(cx, cy); ctx.lineTo(cx + 120 * Math.cos(rad), cy + 120 * Math.sin(rad)); ctx.stroke(); if (showStrobe) { let strobeMovement = absCents 1 || color === “#00ff00”) { ctx.strokeStyle = color; ctx.lineWidth = 12; ctx.setLineDash([8, 25]); ctx.beginPath(); ctx.arc(0, 0, 140, 0, Math.PI * 2); ctx.stroke(); } ctx.restore(); } } const confettiCanvas = document.getElementById(‘confetti-canvas’); const confCtx = confettiCanvas.getContext(‘2d’); let particles = []; function addFlowConfetti() { confettiCanvas.width = window.innerWidth; confettiCanvas.height = window.innerHeight; for (let i = 0; i = 0; i–) { let p = particles[i]; p.x += p.dirX; p.y += p.dirY; p.rot += p.rotV; confCtx.save(); confCtx.translate(p.x, p.y); confCtx.rotate(p.rot); confCtx.fillStyle = p.color; confCtx.fillRect(-p.size/2, -p.size/2, p.size, p.size); confCtx.restore(); if (p.y > window.innerHeight) particles.splice(i, 1); } } document.getElementById(‘earsToggle’).onclick = function() { const isActive = document.getElementById(‘ears-mask’).classList.toggle(‘active’); this.innerText = isActive ? “Ears Only: ON” : “Ears Only: OFF”; document.getElementById(‘cents-display’).classList.toggle(‘hidden’, isActive); this.classList.toggle(‘active’); }; document.getElementById(‘strobeToggle’).onclick = function() { showStrobe = !showStrobe; this.innerText = showStrobe ? “Strobe: ON” : “Strobe: OFF”; this.classList.toggle(‘active’); };

Share this:

  • Share on X (Opens in new window) X
  • Share on Facebook (Opens in new window) Facebook
Like Loading…
 

Loading Comments...
 

    • Subscribe Subscribed
      • SoundEd
      • Already have a WordPress.com account? Log in now.
      • SoundEd
      • Subscribe Subscribed
      • Sign up
      • Log in
      • Copy shortlink
      • Report this content
      • View post in Reader
      • Manage subscriptions
      • Collapse this bar
    %d