<script src="https://unpkg.com/@strudel.cycles/core@latest/dist/index.js"></script> <script src="https://unpkg.com/@strudel.cycles/webaudio@latest/dist/index.js"></script> <script src="https://unpkg.com/@strudel.cycles/mini@latest/dist/index.js"></script> # 🎧 Interactive Frequency Trainer (Web) > Obsidian Publish compatible ear training tool with live Strudel integration <div id="trainer-controls" style="background: #f5f5f5; padding: 20px; border-radius: 8px; margin: 20px 0;"> <h3>🎛️ Controls</h3> <button onclick="initializeTrainer()" style="background: #4CAF50; color: white; padding: 10px 20px; border: none; border-radius: 4px; margin: 5px; cursor: pointer;">Initialize Audio</button> <button onclick="generateChallenge()" style="background: #2196F3; color: white; padding: 10px 20px; border: none; border-radius: 4px; margin: 5px; cursor: pointer;">New Challenge</button> <button onclick="playCurrentChallenge()" style="background: #FF9800; color: white; padding: 10px 20px; border: none; border-radius: 4px; margin: 5px; cursor: pointer;">Play Again</button> <button onclick="stopAll()" style="background: #f44336; color: white; padding: 10px 20px; border: none; border-radius: 4px; margin: 5px; cursor: pointer;">Stop All</button> <div style="margin-top: 15px;"> <label>Difficulty: </label> <select id="difficulty-select" style="padding: 5px;"> <option value="easy">Easy</option> <option value="medium" selected>Medium</option> <option value="hard">Hard</option> </select> <label style="margin-left: 15px;">Waveform: </label> <select id="waveform-select" style="padding: 5px;"> <option value="sine" selected>Sine Wave</option> <option value="square">Square Wave</option> <option value="sawtooth">Sawtooth</option> <option value="triangle">Triangle</option> </select> </div> </div> <div id="challenge-info" style="background: #e3f2fd; padding: 15px; border-radius: 8px; margin: 20px 0; display: none;"> <h4>Current Challenge</h4> <p id="challenge-text">Click "New Challenge" to start!</p> <div style="margin-top: 10px;"> <button onclick="checkGuess('subBass')" style="background: #9C27B0; color: white; padding: 8px 15px; border: none; border-radius: 3px; margin: 3px; cursor: pointer;">Sub Bass (20-60Hz)</button> <button onclick="checkGuess('bass')" style="background: #673AB7; color: white; padding: 8px 15px; border: none; border-radius: 3px; margin: 3px; cursor: pointer;">Bass (60-250Hz)</button> <button onclick="checkGuess('lowMids')" style="background: #3F51B5; color: white; padding: 8px 15px; border: none; border-radius: 3px; margin: 3px; cursor: pointer;">Low Mids (250-500Hz)</button> <button onclick="checkGuess('mids')" style="background: #2196F3; color: white; padding: 8px 15px; border: none; border-radius: 3px; margin: 3px; cursor: pointer;">Mids (500-2kHz)</button> <button onclick="checkGuess('highMids')" style="background: #00BCD4; color: white; padding: 8px 15px; border: none; border-radius: 3px; margin: 3px; cursor: pointer;">High Mids (2-4kHz)</button> <button onclick="checkGuess('presence')" style="background: #009688; color: white; padding: 8px 15px; border: none; border-radius: 3px; margin: 3px; cursor: pointer;">Presence (4-8kHz)</button> <button onclick="checkGuess('treble')" style="background: #4CAF50; color: white; padding: 8px 15px; border: none; border-radius: 3px; margin: 3px; cursor: pointer;">Treble (8-20kHz)</button> </div> </div> <div id="results" style="background: #fff3e0; padding: 15px; border-radius: 8px; margin: 20px 0; display: none;"> <h4>Results</h4> <p id="result-text"></p> <p id="score-text">Score: 0/0</p> </div> ## Live Coding Examples ### Basic Strudel Pattern ```javascript // Initialize Strudel (run this first) const { initStrudel } = strudel; initStrudel().then(() => { console.log('🎵 Strudel initialized!'); // Simple sine wave pattern stack( sine(440).gain(0.2).dur(0.5), sine(440 * 1.5).gain(0.1).dur(0.5).delay(0.25) ).play(); }); ``` ### Advanced Frequency Training Pattern ```javascript // Frequency training with Strudel patterns const freqTraining = () => { const freqs = [200, 400, 800, 1600, 3200]; return sequence(...freqs.map(f => sine(f).gain(0.15).dur(0.8).attack(0.1).release(0.3) )).slow(2); }; freqTraining().play(); ``` <script> // Global state let audioContext; let currentChallenge = null; let score = 0; let attempts = 0; let strudelInitialized = false; // Frequency bands for training const criticalBands = { subBass: { range: [20, 60], description: 'Sub Bass: Feel more than hear' }, bass: { range: [60, 250], description: 'Bass: Fundamental bass notes' }, lowMids: { range: [250, 500], description: 'Low Mids: Warmth and body' }, mids: { range: [500, 2000], description: 'Mids: Vocal presence' }, highMids: { range: [2000, 4000], description: 'High Mids: Clarity and definition' }, presence: { range: [4000, 8000], description: 'Presence: Attack and edge' }, treble: { range: [8000, 20000], description: 'Treble: Air and sparkle' } }; // Initialize audio system async function initializeTrainer() { try { // Basic Web Audio API setup audioContext = new (window.AudioContext || window.webkitAudioContext)(); // Try to initialize Strudel if available if (typeof strudel !== 'undefined' && !strudelInitialized) { await strudel.initStrudel(); strudelInitialized = true; console.log('🎵 Strudel initialized successfully'); } console.log('🎧 Audio system ready!'); document.getElementById('challenge-info').style.display = 'block'; // Test tone playFrequency(440, 0.5); } catch (error) { console.error('Audio initialization failed:', error); alert('Audio initialization failed. Please check your browser permissions.'); } } // Play a frequency using Web Audio API function playFrequency(freq, duration = 1) { if (!audioContext) { alert('Please initialize audio first!'); return; } const oscillator = audioContext.createOscillator(); const gainNode = audioContext.createGain(); oscillator.connect(gainNode); gainNode.connect(audioContext.destination); const waveform = document.getElementById('waveform-select').value; oscillator.type = waveform; oscillator.frequency.value = freq; // Envelope to prevent clicks gainNode.gain.setValueAtTime(0, audioContext.currentTime); gainNode.gain.linearRampToValueAtTime(0.15, audioContext.currentTime + 0.05); gainNode.gain.linearRampToValueAtTime(0, audioContext.currentTime + duration - 0.05); oscillator.start(); oscillator.stop(audioContext.currentTime + duration); console.log(`🔊 Playing ${freq}Hz ${waveform} wave`); } // Generate a new training challenge function generateChallenge() { if (!audioContext) { alert('Please initialize audio first!'); return; } const difficulty = document.getElementById('difficulty-select').value; const difficultySettings = { easy: { bands: ['bass', 'mids', 'treble'], tolerance: 100 }, medium: { bands: Object.keys(criticalBands), tolerance: 50 }, hard: { bands: Object.keys(criticalBands), tolerance: 25 } }; const settings = difficultySettings[difficulty]; const randomBand = settings.bands[Math.floor(Math.random() * settings.bands.length)]; const band = criticalBands[randomBand]; const frequency = Math.floor(Math.random() * (band.range[1] - band.range[0]) + band.range[0]); currentChallenge = { frequency, band: randomBand, tolerance: settings.tolerance, correctAnswer: randomBand }; document.getElementById('challenge-text').innerHTML = ` <strong>🎯 New Challenge Generated!</strong><br> Listen to the frequency and identify its range.<br> <em>Difficulty: ${difficulty} | Tolerance: ±${settings.tolerance}Hz</em> `; document.getElementById('results').style.display = 'none'; // Auto-play the challenge playCurrentChallenge(); } // Play the current challenge function playCurrentChallenge() { if (!currentChallenge) { alert('Generate a challenge first!'); return; } playFrequency(currentChallenge.frequency, 2); console.log(`🎵 Playing challenge: ${currentChallenge.frequency}Hz in ${currentChallenge.band} range`); } // Check user's guess function checkGuess(guessedBand) { if (!currentChallenge) { alert('Generate a challenge first!'); return; } attempts++; const correct = guessedBand === currentChallenge.correctAnswer; if (correct) { score++; } const resultDiv = document.getElementById('results'); const resultText = document.getElementById('result-text'); const scoreText = document.getElementById('score-text'); resultText.innerHTML = ` <strong>${correct ? '✅ Correct!' : '❌ Not quite'}</strong><br> Target: ${currentChallenge.frequency}Hz in the <strong>${currentChallenge.correctAnswer}</strong> range<br> Your guess: <strong>${guessedBand}</strong><br> ${criticalBands[currentChallenge.correctAnswer].description} `; scoreText.textContent = `Score: ${score}/${attempts} (${Math.round(score/attempts*100)}%)`; resultDiv.style.display = 'block'; // Play the frequency again for reference setTimeout(() => { playFrequency(currentChallenge.frequency, 1.5); }, 1000); } // Stop all audio function stopAll() { if (audioContext && audioContext.state !== 'closed') { // This is a simple way to stop - in production you'd track oscillators audioContext.suspend(); setTimeout(() => { audioContext.resume(); }, 100); } console.log('🛑 Audio stopped'); } // Auto-initialize when page loads (for Obsidian Publish) document.addEventListener('DOMContentLoaded', function() { console.log('🚀 Frequency Trainer loaded - click Initialize Audio to begin!'); }); </script> ## 🎓 Training Exercises ### Exercise 1: Range Identification 1. Click "New Challenge" to generate a random frequency 2. Listen carefully to the tone 3. Click the frequency range button you think it belongs to 4. Check your accuracy and learn from the feedback ### Exercise 2: Waveform Comparison 1. Change the waveform selector between sine, square, and sawtooth 2. Generate the same frequency with different waveforms 3. Notice the harmonic differences ### Exercise 3: Difficulty Progression 1. Start with "Easy" difficulty (wider frequency ranges) 2. Progress to "Medium" and then "Hard" 3. Track your accuracy improvement over time ## 🚀 Astro Site Integration Since you mentioned implementing this in your Astro site, here are the key components you'll need: ### Astro Component Structure ```typescript // src/components/FrequencyTrainer.astro --- // Component props and logic here --- <div class="frequency-trainer"> <!-- Controls and UI --> </div> <script> // Client-side JavaScript for audio </script> ``` ### Benefits of Astro Version - **Better Performance**: Static generation with hydrated interactivity - **Modern Framework**: Component-based architecture - **Better SEO**: Server-side rendering - **Enhanced Styling**: Scoped CSS and modern tooling Would you like me to create the Astro version as well? --- *This tool works in Obsidian Publish and any modern web browser with Web Audio API support! 🎵*