Spaces:
Sleeping
Sleeping
File size: 7,338 Bytes
b0d63f4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | 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); }
}
|