Bläddra i källkod

use joystick for scale

Billie Hilton 4 månader sedan
förälder
incheckning
d1c8bc49ce
2 ändrade filer med 156 tillägg och 72 borttagningar
  1. 30 27
      memory-bank/current.md
  2. 126 45
      src/index.js

+ 30 - 27
memory-bank/current.md

@@ -1,17 +1,16 @@
 # Dawdlehorn - Current State
 
-## Current Task: DPAD Axis Mapping and Dead Zone Immunity (COMPLETED)
-Implemented proper DPAD axis handling for Nintendo controllers, making the DPAD axis immune to dead zone filtering and mapping raw values to degrees for better usability.
+## Current Task: Enhanced Gamepad Controls with C-Major Scale and Pitch Bending (COMPLETED)
+Implemented advanced gamepad controls with musical scale mapping and pitch bending functionality for more intuitive music creation.
 
 ## Current Task Checklist
-- [x] Read current project state from memory bank
-- [x] Analyze DPAD axis implementation and dead zone filtering
-- [x] Implement DPAD axis immunity to dead zone filtering
-- [x] Map DPAD values according to specification
+- [x] Analyze current project state and requirements
+- [x] Understand existing gamepad mapping implementation
+- [x] Implement C-Major scale mapping for right axis based on angle
+- [x] Add volume control based on right axis magnitude
+- [x] Implement pitch bending with left stick up/down
 - [x] Test the implementation
-- [x] Update DPAD mapping with correct raw values and degrees
-- [x] Update display to show DPAD directions in user-friendly format
-- [x] Update memory bank with changes
+- [x] Update documentation
 
 ## Previous Task: Custom Gamepad Mapping Implementation (COMPLETED)
 - [x] Research gamepad API limitations and solutions
@@ -94,24 +93,28 @@ Implemented proper DPAD axis handling for Nintendo controllers, making the DPAD
 - **Trigger Names**: ZL (left trigger), ZR (right trigger), L (left bumper), R (right bumper)
 
 ## Recent Changes
-- **DPAD Axis Mapping Implementation**: Made DPAD axis immune to dead zone filtering
-- **Raw Value Mapping**: Mapped Nintendo controller DPAD raw values to degrees:
-  - 1.29 = neutral (not pressed) → null
-  - 0.14 = down → 270°
-  - -0.14 = down right → 315°
-  - -0.43 = right → 0°
-  - -0.71 = up right → 45°
-  - -1 = up → 90°
-  - 1 = up left → 135°
-  - 0.71 = left → 180°
-  - 0.43 = down left → 225°
-- **User-Friendly Display**: Updated DPAD display to show directional names (e.g., "Up-Right (45°)")
-- **Dead Zone Immunity**: DPAD axis now bypasses dead zone filtering for precise directional input
-- Fixed triggers display in gamepad debug interface
-- Now shows proper button names based on controller type:
-  - Nintendo: ZL/ZR (triggers), L/R (bumpers)
-  - PlayStation: L2/R2 (triggers), L1/R1 (bumpers)  
-  - Xbox/Standard: LT/RT (triggers), LB/RB (bumpers)
+- **C-Major Scale Mapping**: Right stick now maps to C-Major scale notes based on angle:
+  - Down (270°) = C4 (261.63 Hz)
+  - Down-right (315°) = D4 (293.66 Hz)
+  - Right (0°) = E4 (329.63 Hz)
+  - Up-right (45°) = F4 (349.23 Hz)
+  - Up (90°) = G4 (392.00 Hz)
+  - Up-left (135°) = A4 (440.00 Hz)
+  - Left (180°) = B4 (493.88 Hz)
+  - Down-left (225°) = C5 (523.25 Hz) - octave
+- **Volume Control**: Right stick magnitude now controls volume (0-100%)
+- **Pitch Bending**: Left stick Y-axis now controls pitch bending:
+  - Up = sharp (positive pitch bend up to +100 cents)
+  - Down = flat (negative pitch bend down to -100 cents)
+  - Range: ±1 semitone (±100 cents)
+- **Enhanced Audio Processing**: 
+  - Separated base frequency from pitch-bent frequency
+  - Real-time pitch bend calculation using semitone ratios
+  - Visual feedback showing pitch bend amount in cents with ♯/♭ symbols
+- **Improved UI Display**:
+  - Filter display now shows selected note and angle
+  - Frequency display shows pitch bend indicators
+  - Volume display reflects right stick magnitude
 
 ## Immediate Next Steps
 1. Test the fixed triggers display with the Hori Nintendo controller

+ 126 - 45
src/index.js

@@ -14,9 +14,11 @@ class DawdlehornApp {
 
         // State
         this.currentFreq = 440;
+        this.baseFreq = 440; // Base frequency before pitch bending
         this.currentVolume = 0.5;
         this.currentFilter = 1000;
         this.isPlaying = false;
+        this.pitchBend = 0; // -1 to 1, where -1 is flat, 1 is sharp
 
         // Gamepad state tracking
         this.gamepadState = {
@@ -26,6 +28,18 @@ class DawdlehornApp {
             buttons: new Set()
         };
 
+        // C-Major scale frequencies (C4 to C5)
+        this.cMajorScale = {
+            'C': 261.63,   // C4
+            'D': 293.66,   // D4
+            'E': 329.63,   // E4
+            'F': 349.23,   // F4
+            'G': 392.00,   // G4
+            'A': 440.00,   // A4
+            'B': 493.88,   // B4
+            'C5': 523.25   // C5 (octave)
+        };
+
         // UI elements
         this.elements = {
             gamepadStatus: document.getElementById('gamepad-status'),
@@ -225,26 +239,10 @@ class DawdlehornApp {
 
             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);
-                }
+            // Face buttons (A, B, X, Y) are reserved for future functionality
+            // Add vibration feedback for any button press
+            if (this.gamepadIndex !== null) {
+                this.gamepadMapper.vibrate(this.gamepadIndex, 100, 0.3, 0.1);
             }
 
         } catch (error) {
@@ -259,15 +257,8 @@ class DawdlehornApp {
 
             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;
-            }
+            // Face buttons (A, B, X, Y) no longer control audio
+            // Audio is now controlled solely by right stick movement
 
         } catch (error) {
             console.error('❌ Button release error:', error);
@@ -285,17 +276,18 @@ class DawdlehornApp {
             switch (axis) {
                 case 'LEFT_STICK_X':
                     this.gamepadState.leftStick.x = value;
-                    this.updateFrequency();
                     break;
                 case 'LEFT_STICK_Y':
                     this.gamepadState.leftStick.y = value;
+                    this.updatePitchBend();
                     break;
                 case 'RIGHT_STICK_X':
                     this.gamepadState.rightStick.x = value;
+                    this.updateRightStickControls();
                     break;
                 case 'RIGHT_STICK_Y':
                     this.gamepadState.rightStick.y = value;
-                    this.updateFilter();
+                    this.updateRightStickControls();
                     break;
                 case 'DPAD_AXIS':
                     this.gamepadState.dpadAxis = value;
@@ -308,27 +300,116 @@ class DawdlehornApp {
         }
     }
 
-    updateFrequency() {
+    updatePitchBend() {
         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`;
+        // Left stick Y-axis controls pitch bend (-1 to 1, where -1 is flat, 1 is sharp)
+        // Invert Y axis so up is positive (sharp), down is negative (flat)
+        this.pitchBend = -this.gamepadState.leftStick.y;
+
+        // Apply pitch bend to current frequency
+        this.updateFrequency();
+    }
+
+    updateRightStickControls() {
+        if (!this.audioInitialized) return;
+
+        const x = this.gamepadState.rightStick.x;
+        const y = this.gamepadState.rightStick.y;
+
+        // Calculate magnitude (0 to 1) for volume control
+        const magnitude = Math.sqrt(x * x + y * y);
+        this.currentVolume = Math.min(1, magnitude);
+
+        // Calculate angle for C-Major scale selection
+        // Convert from gamepad coordinates to degrees (0° = right, 90° = up, etc.)
+        let angle = Math.atan2(-y, x) * (180 / Math.PI); // -y to match screen coordinates
+        if (angle < 0) angle += 360; // Normalize to 0-360°
+
+        // Map angles to C-Major scale notes
+        // Down = 270° = C, Down-left = 225° = D, Left = 180° = E, etc.
+        const noteFromAngle = this.getNoteFromAngle(angle, magnitude);
+
+        if (noteFromAngle && magnitude > 0.1) { // Only play if stick is moved significantly
+            this.baseFreq = this.cMajorScale[noteFromAngle];
+            this.updateFrequency();
+
+            // Start playing if not already playing
+            if (!this.isPlaying) {
+                this.synth.start();
+                this.isPlaying = true;
+            }
+        } else if (magnitude <= 0.1 && this.isPlaying) {
+            // Stop playing if stick is back to neutral
+            this.synth.stop();
+            this.isPlaying = false;
+        }
+
+        // Update volume
+        this.updateVolume();
+
+        // Update displays
+        this.elements.volumeDisplay.textContent = `${Math.round(this.currentVolume * 100)}%`;
+
+        if (magnitude > 0.1) {
+            const noteDisplay = noteFromAngle ? `${noteFromAngle} (${Math.round(angle)}°)` : `${Math.round(angle)}°`;
+            this.elements.filterDisplay.textContent = noteDisplay;
+        } else {
+            this.elements.filterDisplay.textContent = 'Neutral';
         }
     }
 
-    updateFilter() {
+    getNoteFromAngle(angle, magnitude) {
+        if (magnitude < 0.1) return null; // Dead zone
+
+        // Map 8 directions to C-Major scale
+        // Each direction covers 45° (360° / 8 = 45°)
+        const directions = [
+            { min: 337.5, max: 22.5, note: 'E' },    // Right (0°) = E
+            { min: 22.5, max: 67.5, note: 'F' },     // Up-right (45°) = F
+            { min: 67.5, max: 112.5, note: 'G' },    // Up (90°) = G
+            { min: 112.5, max: 157.5, note: 'A' },   // Up-left (135°) = A
+            { min: 157.5, max: 202.5, note: 'B' },   // Left (180°) = B
+            { min: 202.5, max: 247.5, note: 'C5' },  // Down-left (225°) = C5 (octave)
+            { min: 247.5, max: 292.5, note: 'C' },   // Down (270°) = C
+            { min: 292.5, max: 337.5, note: 'D' }    // Down-right (315°) = D
+        ];
+
+        for (const dir of directions) {
+            if (dir.min > dir.max) { // Handle wrap-around (e.g., 337.5° to 22.5°)
+                if (angle >= dir.min || angle <= dir.max) {
+                    return dir.note;
+                }
+            } else {
+                if (angle >= dir.min && angle <= dir.max) {
+                    return dir.note;
+                }
+            }
+        }
+
+        return null;
+    }
+
+    updateFrequency() {
         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`;
+        // Apply pitch bend to base frequency
+        // Pitch bend range: ±1 semitone (±100 cents)
+        const bendFactor = Math.pow(2, this.pitchBend / 12); // 1 semitone = 1/12 octave
+        const newFreq = this.baseFreq * bendFactor;
+
+        if (Math.abs(newFreq - this.currentFreq) > 1) {
+            this.currentFreq = newFreq;
+            this.synth.frequency.setValueAtTime(this.currentFreq, Tone.now());
+
+            // Display frequency with pitch bend indicator
+            let freqText = `${Math.round(this.currentFreq)} Hz`;
+            if (Math.abs(this.pitchBend) > 0.05) {
+                const bendCents = Math.round(this.pitchBend * 100);
+                const bendSymbol = bendCents > 0 ? '♯' : '♭';
+                freqText += ` ${bendSymbol}${Math.abs(bendCents)}¢`;
+            }
+            this.elements.freqDisplay.textContent = freqText;
         }
     }