| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342 |
- import * as Tone from 'tone';
- import { GamepadMapper } from './gamepad-mapper.js';
- 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;
- // Gamepad state tracking
- this.gamepadState = {
- leftStick: { x: 0, y: 0 },
- rightStick: { x: 0, y: 0 },
- triggers: { left: 0, right: 0 },
- buttons: new Set()
- };
- // 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')
- };
- // Initialize gamepad mapper
- this.gamepadMapper = new GamepadMapper({
- deadzone: 0.15,
- debounceTime: 30,
- enableVibration: true
- });
- 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() {
- // Set up gamepad mapper event listeners
- this.gamepadMapper.on('connected', (data) => {
- console.log(`🎮 Gamepad connected: ${data.id} (Type: ${data.type})`);
- this.gamepadConnected = true;
- this.gamepadIndex = data.index;
- this.elements.gamepadStatus.textContent = `Connected: ${data.id} (${data.type})`;
- this.elements.gamepadStatus.style.color = '#00ff00';
- });
- this.gamepadMapper.on('disconnected', (data) => {
- console.log('🎮 Gamepad disconnected');
- this.gamepadConnected = false;
- this.gamepadIndex = null;
- this.elements.gamepadStatus.textContent = 'Not Connected';
- this.elements.gamepadStatus.style.color = '#ff6600';
- });
- // Handle button events
- this.gamepadMapper.on('buttondown', (data) => {
- this.handleButtonPress(data.button, data.value);
- });
- this.gamepadMapper.on('buttonup', (data) => {
- this.handleButtonRelease(data.button);
- });
- // Handle axis changes
- this.gamepadMapper.on('axischange', (data) => {
- this.handleAxisChange(data.axis, data.value, data.rawValue);
- });
- }
- 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() {
- // The GamepadMapper handles polling internally, so we just need to update the debug display
- this.updateDebugDisplay();
- }
- updateDebugDisplay() {
- const updateLoop = () => {
- if (this.gamepadConnected) {
- // Update debug displays with current gamepad state
- this.elements.leftStickDisplay.textContent = `${this.gamepadState.leftStick.x.toFixed(2)}, ${this.gamepadState.leftStick.y.toFixed(2)}`;
- this.elements.rightStickDisplay.textContent = `${this.gamepadState.rightStick.x.toFixed(2)}, ${this.gamepadState.rightStick.y.toFixed(2)}`;
- this.elements.triggersDisplay.textContent = `L: ${this.gamepadState.triggers.left.toFixed(2)} R: ${this.gamepadState.triggers.right.toFixed(2)}`;
- // Update button display
- let buttonDisplay = '';
- buttonDisplay += this.gamepadState.buttons.has('A') ? '[A]' : 'A';
- buttonDisplay += ' ';
- buttonDisplay += this.gamepadState.buttons.has('B') ? '[B]' : 'B';
- buttonDisplay += ' ';
- buttonDisplay += this.gamepadState.buttons.has('X') ? '[X]' : 'X';
- buttonDisplay += ' ';
- buttonDisplay += this.gamepadState.buttons.has('Y') ? '[Y]' : 'Y';
- this.elements.buttonsDisplay.textContent = buttonDisplay;
- }
- requestAnimationFrame(updateLoop);
- };
- updateLoop();
- }
- handleButtonPress(button, value) {
- try {
- console.log(`🎮 Button pressed: ${button} (${value})`);
- this.gamepadState.buttons.add(button);
- if (!this.audioInitialized) return;
- // Map face buttons to notes with proper debouncing
- const noteMap = {
- 'A': 261.63, // C4
- 'B': 293.66, // D4
- 'X': 329.63, // E4
- 'Y': 349.23 // F4
- };
- if (noteMap[button]) {
- if (!this.isPlaying) {
- this.synth.start();
- this.isPlaying = true;
- }
- this.synth.frequency.setValueAtTime(noteMap[button], Tone.now());
- // Add vibration feedback if available
- if (this.gamepadIndex !== null) {
- this.gamepadMapper.vibrate(this.gamepadIndex, 100, 0.3, 0.1);
- }
- }
- } catch (error) {
- console.error('❌ Button press error:', error);
- }
- }
- handleButtonRelease(button) {
- try {
- console.log(`🎮 Button released: ${button}`);
- this.gamepadState.buttons.delete(button);
- if (!this.audioInitialized) return;
- // Stop audio if no face buttons are pressed
- const faceButtons = ['A', 'B', 'X', 'Y'];
- const anyFaceButtonPressed = faceButtons.some(btn => this.gamepadState.buttons.has(btn));
- if (!anyFaceButtonPressed && this.isPlaying) {
- this.synth.stop();
- this.isPlaying = false;
- }
- } catch (error) {
- console.error('❌ Button release error:', error);
- }
- }
- handleAxisChange(axis, value, rawValue) {
- try {
- // Update gamepad state
- switch (axis) {
- case 'LEFT_STICK_X':
- this.gamepadState.leftStick.x = value;
- this.updateFrequency();
- break;
- case 'LEFT_STICK_Y':
- this.gamepadState.leftStick.y = value;
- break;
- case 'RIGHT_STICK_X':
- this.gamepadState.rightStick.x = value;
- break;
- case 'RIGHT_STICK_Y':
- this.gamepadState.rightStick.y = value;
- this.updateFilter();
- break;
- }
- // Handle analog triggers (some controllers map triggers to axes)
- if (axis === 'LEFT_TRIGGER' || axis === 'RIGHT_TRIGGER') {
- if (axis === 'LEFT_TRIGGER') {
- this.gamepadState.triggers.left = value;
- } else {
- this.gamepadState.triggers.right = value;
- }
- this.updateVolume();
- }
- } catch (error) {
- console.error('❌ Axis change error:', error);
- }
- }
- updateFrequency() {
- if (!this.audioInitialized) return;
- // Left stick X-axis controls frequency (200Hz - 2000Hz)
- const newFreq = 440 + (this.gamepadState.leftStick.x * 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`;
- }
- }
- updateFilter() {
- if (!this.audioInitialized) return;
- // Right stick Y-axis controls filter frequency (100Hz - 5000Hz)
- const newFilter = 1000 + (this.gamepadState.rightStick.y * -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`;
- }
- }
- updateVolume() {
- if (!this.audioInitialized) return;
- // Triggers control volume
- const triggerVolume = (this.gamepadState.triggers.left + this.gamepadState.triggers.right) / 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)}%`;
- }
- }
- 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();
- });
|