import * as Tone from 'tone'; class DawdlehornApp { constructor() { this.audioInitialized = false; this.gamepadConnected = false; this.gamepadIndex = null; // Audio components this.synth = null; this.filter = null; this.volume = null; // State this.currentFreq = 440; this.currentVolume = 0.5; this.currentFilter = 1000; this.isPlaying = false; // UI elements this.elements = { gamepadStatus: document.getElementById('gamepad-status'), audioStatus: document.getElementById('audio-status'), freqDisplay: document.getElementById('freq-display'), volumeDisplay: document.getElementById('volume-display'), filterDisplay: document.getElementById('filter-display'), leftStickDisplay: document.getElementById('left-stick-display'), rightStickDisplay: document.getElementById('right-stick-display'), triggersDisplay: document.getElementById('triggers-display'), buttonsDisplay: document.getElementById('buttons-display'), visualizer: document.getElementById('visualizer') }; this.init(); } async init() { console.log('🎮 Initializing Dawdlehorn...'); // Set up gamepad detection this.setupGamepadDetection(); // Set up audio (will be initialized on first user interaction) this.setupAudioComponents(); // Start gamepad polling this.startGamepadPolling(); // Set up visualizer this.setupVisualizer(); console.log('✅ Dawdlehorn initialized'); } setupGamepadDetection() { window.addEventListener('gamepadconnected', (e) => { console.log('🎮 Gamepad connected:', e.gamepad.id); this.gamepadConnected = true; this.gamepadIndex = e.gamepad.index; this.elements.gamepadStatus.textContent = `Connected: ${e.gamepad.id}`; this.elements.gamepadStatus.style.color = '#00ff00'; }); window.addEventListener('gamepaddisconnected', (e) => { console.log('🎮 Gamepad disconnected'); this.gamepadConnected = false; this.gamepadIndex = null; this.elements.gamepadStatus.textContent = 'Not Connected'; this.elements.gamepadStatus.style.color = '#ff6600'; }); } async setupAudioComponents() { try { // Create audio chain: Synth -> Filter -> Volume -> Destination this.synth = new Tone.Oscillator(440, 'sawtooth'); this.filter = new Tone.Filter(1000, 'lowpass'); this.volume = new Tone.Volume(-20); // Connect the audio chain this.synth.connect(this.filter); this.filter.connect(this.volume); this.volume.toDestination(); this.elements.audioStatus.textContent = 'Ready (Click to start)'; this.elements.audioStatus.style.color = '#ffff00'; // Set up click-to-start audio document.addEventListener('click', this.initializeAudio.bind(this), { once: true }); } catch (error) { console.error('❌ Audio setup failed:', error); this.elements.audioStatus.textContent = 'Setup Failed'; this.elements.audioStatus.style.color = '#ff0000'; } } async initializeAudio() { try { await Tone.start(); console.log('🔊 Audio context started'); this.audioInitialized = true; this.elements.audioStatus.textContent = 'Active'; this.elements.audioStatus.style.color = '#00ff00'; } catch (error) { console.error('❌ Audio initialization failed:', error); this.elements.audioStatus.textContent = 'Failed'; this.elements.audioStatus.style.color = '#ff0000'; } } startGamepadPolling() { const pollGamepad = () => { if (this.gamepadConnected && this.gamepadIndex !== null) { const gamepad = navigator.getGamepads()[this.gamepadIndex]; if (gamepad) { this.handleGamepadInput(gamepad); } } requestAnimationFrame(pollGamepad); }; pollGamepad(); } handleGamepadInput(gamepad) { // Update debug displays FIRST - this shows raw input immediately const leftStickX = gamepad.axes[0] || 0; const leftStickY = gamepad.axes[1] || 0; const rightStickX = gamepad.axes[2] || 0; const rightStickY = gamepad.axes[3] || 0; this.elements.leftStickDisplay.textContent = `${leftStickX.toFixed(2)}, ${leftStickY.toFixed(2)}`; this.elements.rightStickDisplay.textContent = `${rightStickX.toFixed(2)}, ${rightStickY.toFixed(2)}`; const leftTrigger = gamepad.buttons[6] ? gamepad.buttons[6].value : 0; const rightTrigger = gamepad.buttons[7] ? gamepad.buttons[7].value : 0; this.elements.triggersDisplay.textContent = `L: ${leftTrigger.toFixed(2)} R: ${rightTrigger.toFixed(2)}`; const buttonA = gamepad.buttons[0] && gamepad.buttons[0].pressed; const buttonB = gamepad.buttons[1] && gamepad.buttons[1].pressed; const buttonX = gamepad.buttons[2] && gamepad.buttons[2].pressed; const buttonY = gamepad.buttons[3] && gamepad.buttons[3].pressed; let buttonDisplay = ''; buttonDisplay += buttonA ? '[A]' : 'A'; buttonDisplay += ' '; buttonDisplay += buttonB ? '[B]' : 'B'; buttonDisplay += ' '; buttonDisplay += buttonX ? '[X]' : 'X'; buttonDisplay += ' '; buttonDisplay += buttonY ? '[Y]' : 'Y'; this.elements.buttonsDisplay.textContent = buttonDisplay; // Early return if audio not ready if (!this.audioInitialized) return; // Left stick X-axis controls frequency (200Hz - 2000Hz) const newFreq = 440 + (leftStickX * 560); // 440Hz ± 560Hz if (Math.abs(newFreq - this.currentFreq) > 5) { this.currentFreq = newFreq; this.synth.frequency.setValueAtTime(this.currentFreq, Tone.now()); this.elements.freqDisplay.textContent = `${Math.round(this.currentFreq)} Hz`; } // Right stick Y-axis controls filter frequency (100Hz - 5000Hz) const newFilter = 1000 + (rightStickY * -2000); // Inverted Y, 1000Hz ± 2000Hz if (Math.abs(newFilter - this.currentFilter) > 50) { this.currentFilter = Math.max(100, Math.min(5000, newFilter)); this.filter.frequency.setValueAtTime(this.currentFilter, Tone.now()); this.elements.filterDisplay.textContent = `${Math.round(this.currentFilter)} Hz`; } // Triggers control volume const triggerVolume = (leftTrigger + rightTrigger) / 2; if (Math.abs(triggerVolume - this.currentVolume) > 0.05) { this.currentVolume = triggerVolume; const dbVolume = -40 + (this.currentVolume * 40); // -40dB to 0dB this.volume.volume.setValueAtTime(dbVolume, Tone.now()); this.elements.volumeDisplay.textContent = `${Math.round(this.currentVolume * 100)}%`; } // Face buttons control note playing const anyButtonPressed = buttonA || buttonB || buttonX || buttonY; if (anyButtonPressed && !this.isPlaying) { this.synth.start(); this.isPlaying = true; } else if (!anyButtonPressed && this.isPlaying) { this.synth.stop(); this.isPlaying = false; } // Different buttons trigger different note frequencies if (buttonA) this.synth.frequency.setValueAtTime(261.63, Tone.now()); // C4 if (buttonB) this.synth.frequency.setValueAtTime(293.66, Tone.now()); // D4 if (buttonX) this.synth.frequency.setValueAtTime(329.63, Tone.now()); // E4 if (buttonY) this.synth.frequency.setValueAtTime(349.23, Tone.now()); // F4 } setupVisualizer() { const canvas = this.elements.visualizer; const ctx = canvas.getContext('2d'); // Set canvas size const resizeCanvas = () => { canvas.width = canvas.offsetWidth; canvas.height = canvas.offsetHeight; }; resizeCanvas(); window.addEventListener('resize', resizeCanvas); // Simple visualizer animation const animate = () => { ctx.fillStyle = '#000'; ctx.fillRect(0, 0, canvas.width, canvas.height); if (this.isPlaying) { const time = Date.now() * 0.01; const centerY = canvas.height / 2; ctx.strokeStyle = '#00ff00'; ctx.lineWidth = 2; ctx.beginPath(); for (let x = 0; x < canvas.width; x += 2) { const freq = this.currentFreq / 1000; const y = centerY + Math.sin((x * 0.02) + time * freq) * (this.currentVolume * 30); if (x === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.stroke(); } requestAnimationFrame(animate); }; animate(); } } // Initialize the app when the page loads document.addEventListener('DOMContentLoaded', () => { new DawdlehornApp(); });