|
@@ -14,9 +14,11 @@ class DawdlehornApp {
|
|
|
|
|
|
|
|
// State
|
|
// State
|
|
|
this.currentFreq = 440;
|
|
this.currentFreq = 440;
|
|
|
|
|
+ this.baseFreq = 440; // Base frequency before pitch bending
|
|
|
this.currentVolume = 0.5;
|
|
this.currentVolume = 0.5;
|
|
|
this.currentFilter = 1000;
|
|
this.currentFilter = 1000;
|
|
|
this.isPlaying = false;
|
|
this.isPlaying = false;
|
|
|
|
|
+ this.pitchBend = 0; // -1 to 1, where -1 is flat, 1 is sharp
|
|
|
|
|
|
|
|
// Gamepad state tracking
|
|
// Gamepad state tracking
|
|
|
this.gamepadState = {
|
|
this.gamepadState = {
|
|
@@ -26,6 +28,18 @@ class DawdlehornApp {
|
|
|
buttons: new Set()
|
|
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
|
|
// UI elements
|
|
|
this.elements = {
|
|
this.elements = {
|
|
|
gamepadStatus: document.getElementById('gamepad-status'),
|
|
gamepadStatus: document.getElementById('gamepad-status'),
|
|
@@ -225,26 +239,10 @@ class DawdlehornApp {
|
|
|
|
|
|
|
|
if (!this.audioInitialized) return;
|
|
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) {
|
|
} catch (error) {
|
|
@@ -259,15 +257,8 @@ class DawdlehornApp {
|
|
|
|
|
|
|
|
if (!this.audioInitialized) return;
|
|
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) {
|
|
} catch (error) {
|
|
|
console.error('❌ Button release error:', error);
|
|
console.error('❌ Button release error:', error);
|
|
@@ -285,17 +276,18 @@ class DawdlehornApp {
|
|
|
switch (axis) {
|
|
switch (axis) {
|
|
|
case 'LEFT_STICK_X':
|
|
case 'LEFT_STICK_X':
|
|
|
this.gamepadState.leftStick.x = value;
|
|
this.gamepadState.leftStick.x = value;
|
|
|
- this.updateFrequency();
|
|
|
|
|
break;
|
|
break;
|
|
|
case 'LEFT_STICK_Y':
|
|
case 'LEFT_STICK_Y':
|
|
|
this.gamepadState.leftStick.y = value;
|
|
this.gamepadState.leftStick.y = value;
|
|
|
|
|
+ this.updatePitchBend();
|
|
|
break;
|
|
break;
|
|
|
case 'RIGHT_STICK_X':
|
|
case 'RIGHT_STICK_X':
|
|
|
this.gamepadState.rightStick.x = value;
|
|
this.gamepadState.rightStick.x = value;
|
|
|
|
|
+ this.updateRightStickControls();
|
|
|
break;
|
|
break;
|
|
|
case 'RIGHT_STICK_Y':
|
|
case 'RIGHT_STICK_Y':
|
|
|
this.gamepadState.rightStick.y = value;
|
|
this.gamepadState.rightStick.y = value;
|
|
|
- this.updateFilter();
|
|
|
|
|
|
|
+ this.updateRightStickControls();
|
|
|
break;
|
|
break;
|
|
|
case 'DPAD_AXIS':
|
|
case 'DPAD_AXIS':
|
|
|
this.gamepadState.dpadAxis = value;
|
|
this.gamepadState.dpadAxis = value;
|
|
@@ -308,27 +300,116 @@ class DawdlehornApp {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- updateFrequency() {
|
|
|
|
|
|
|
+ updatePitchBend() {
|
|
|
if (!this.audioInitialized) return;
|
|
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;
|
|
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;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|