Spaces:
Sleeping
Sleeping
| 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); } | |
| } | |