hyperspace-jam-lyria / Player2Synth.js
Solshine's picture
Upload folder using huggingface_hub
b0d63f4 verified
Raw
History Blame Contribute Delete
7.34 kB
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); }
}