<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! 🎵*