import * as Tone from 'https://esm.sh/tone'; // Player 2: Simple beatpad — each finger triggers a distinct percussion hit // Hand height controls pitch, spread controls reverb export class Player2Synth { constructor() { this.isStarted = false; this._panicMuted = false; this.activePatterns = new Map(); this.padSynths = this.activePatterns; this.currentSynthIndex = 0; this.padPresets = [{ name: 'BEATPAD' }]; this.currentStyleLabel = 'BEATPAD'; this.scale = ['C2','D2','E2','F2','G2','A2','B2','C3','D3','E3','F3','G3','A3','B3','C4','D4','E4','F4','G4','A4','B4','C5']; this._fingerStates = {}; this._drumCooldowns = {}; } async start() { if (this.isStarted) return; await Tone.start(); this.limiter = new Tone.Limiter(-3).toDestination(); this.reverb = new Tone.Reverb({ decay: 1.5, wet: 0.15 }).connect(this.limiter); // Kick — deep thump this.kick = new Tone.MembraneSynth({ pitchDecay: 0.05, octaves: 8, envelope: { attack: 0.001, decay: 0.2, sustain: 0, release: 0.3 } }).connect(this.limiter); // Snare — noise burst + body this.snare = new Tone.NoiseSynth({ noise: { type: 'white' }, envelope: { attack: 0.001, decay: 0.12, sustain: 0 } }).connect(this.reverb); // Hi-hat — short bright noise this.hihat = new Tone.NoiseSynth({ noise: { type: 'white' }, envelope: { attack: 0.001, decay: 0.04, sustain: 0 } }).connect(this.limiter); this.hihat.volume.value = -6; // Clap — layered noise this.clap = new Tone.NoiseSynth({ noise: { type: 'pink' }, envelope: { attack: 0.001, decay: 0.15, sustain: 0 } }).connect(this.reverb); // Percussion — tonal hit (tom/conga) this.tom = new Tone.MembraneSynth({ pitchDecay: 0.08, octaves: 4, envelope: { attack: 0.001, decay: 0.15, sustain: 0, release: 0.2 } }).connect(this.reverb); // Peruvian flute — breathy sine+triangle with vibrato and delay this.flute = new Tone.Synth({ oscillator: { type: 'sine' }, envelope: { attack: 0.08, decay: 0.1, sustain: 0.6, release: 0.4 } }); this.fluteVibrato = new Tone.Vibrato({ frequency: 5, depth: 0.15 }); this.fluteDelay = new Tone.PingPongDelay('4n', 0.25); this.fluteDelay.wet.value = 0.3; this.flute.connect(this.fluteVibrato); this.fluteVibrato.connect(this.fluteDelay); this.fluteDelay.connect(this.reverb); // Pentatonic flute scale (Andean) this._fluteScale = ['E4','G4','A4','B4','D5','E5','G5','A5','B5','D6','E6']; this._fluteHeld = false; this.isStarted = true; console.log('[Beatpad] Started'); } // Synth hand: height controls tom pitch, each finger triggers a pad updateGesture(handId, gestureData) { if (!this.isStarted || this._panicMuted) return; const ext = gestureData.fingerExtensions || {}; const prev = this._fingerStates; // Thumb = kick, Middle = hihat, Ring = clap, Pinky = tom this._triggerOnOpen('thumb', ext.thumb, prev.thumb, () => { const note = gestureData.rootNote || 'C2'; this.kick.triggerAttackRelease(note, '8n'); }); this._triggerOnOpen('middle', ext.middle, prev.middle, () => { this.hihat.triggerAttackRelease('32n'); }); this._triggerOnOpen('ring', ext.ring, prev.ring, () => { this.clap.triggerAttackRelease('8n'); }); this._triggerOnOpen('pinky', ext.pinky, prev.pinky, () => { const note = gestureData.rootNote || 'C3'; this.tom.triggerAttackRelease(note, '8n'); }); // Index = Peruvian flute — held while finger is up, pitch from hand height if ((ext.index || 0) > 0.45) { const handY = gestureData.rootNote ? this.scale.indexOf(gestureData.rootNote) : 10; const fluteIdx = Math.floor((handY / this.scale.length) * this._fluteScale.length); const fluteNote = this._fluteScale[Math.max(0, Math.min(this._fluteScale.length - 1, fluteIdx))]; if (!this._fluteHeld) { this.flute.triggerAttack(fluteNote); this._fluteHeld = true; } else { this.flute.frequency.rampTo(Tone.Frequency(fluteNote).toFrequency(), 0.15); } } else if (this._fluteHeld) { this.flute.triggerRelease(); this._fluteHeld = false; } // Save state for next frame this._fingerStates = { thumb: ext.thumb || 0, index: ext.index || 0, middle: ext.middle || 0, ring: ext.ring || 0, pinky: ext.pinky || 0 }; // Wrist angle controls reverb amount const wrist = Math.abs(gestureData.wristAngle || 0); if (this.reverb) this.reverb.wet.value = wrist * 0.5; } _triggerOnOpen(finger, current, previous, callback) { const now = performance.now(); const cd = this._drumCooldowns[finger] || 0; if ((current || 0) > 0.45 && (previous || 0) <= 0.45 && now - cd > 120) { this._drumCooldowns[finger] = now; callback(); } } // Drum hand: same per-finger triggers but with repeat while held updateDrumGesture(gestureData) { if (!this.isStarted || this._panicMuted) return; const ext = gestureData.fingerExtensions || {}; // Retrigger while finger is held open (every 200ms) this._retrigger('d_thumb', ext.thumb, () => this.kick.triggerAttackRelease('C1', '8n')); this._retrigger('d_index', ext.index, () => this.snare.triggerAttackRelease('16n')); this._retrigger('d_middle', ext.middle, () => this.hihat.triggerAttackRelease('32n')); this._retrigger('d_ring', ext.ring, () => this.clap.triggerAttackRelease('8n')); this._retrigger('d_pinky', ext.pinky, () => this.tom.triggerAttackRelease('G2', '16n')); } _retrigger(key, value, callback) { const now = performance.now(); const cd = this._drumCooldowns[key] || 0; if ((value || 0) > 0.5 && now - cd > 200) { this._drumCooldowns[key] = now; callback(); } } triggerFingerTouch(f1, f2) { if (!this.isStarted || this._panicMuted) return; // Any finger touch = rimshot this.tom.triggerAttackRelease('E4', '32n'); } // Stubs for game.js compatibility startArpeggio() {} stopArpeggio() {} updateArpeggio() {} updateArpeggioVolume() {} updateFingerExpression() {} setProximityFilter() {} setSynthHandOpen() {} setDrumHandFist() {} stopShapeBass() {} updateShapeBass() {} updateShapeAudio() {} cycleSynth() { console.log('[Beatpad] Cycle — only one mode'); } panic() { this._panicMuted = !this._panicMuted; } handLeft() {} ensureAudioActive() { if (Tone.context.state === 'suspended') Tone.context.resume(); } getAnalyser() { return null; } getAnalyserData() { return new Float32Array(2048); } }