// dance_engine.js — Maps audio analysis to robot commands // // Equivalent to duo's BeatBandit._tick() control loop: // audio.currentTime → beatIndex → blockIndex → energy level // → select sequence → interpolate to target → send to robot // // Our pipeline: // audioEngine.analyze() → energy level → select sequence // → on beat: pick move → send LED + servo + emotion to robot import { selectSequence, getMoveAtBeat, classifyEnergy, spectrumColor, flashColor, dimColor } from './move_library.js'; export class DanceEngine extends EventTarget { constructor(audioEngine, connection, safety) { super(); this.audio = audioEngine; this.robot = connection; this.safety = safety; // Dance state (like duo's _currentBlock, _currentSequence) this._currentSequence = null; this._currentEnergy = 'low'; this._beatCount = 0; // Global beat counter this._blockBeat = 0; // Beat within current 8-beat block this._lastBlockEnergy = ''; // Detect energy level changes // Toggles (controlled by UI switches) this.ledEnabled = true; this.servoEnabled = true; this.emotionEnabled = false; // Animation loop this._frameId = null; this._running = false; // Bind for rAF this._tick = this._tick.bind(this); } start() { if (this._running) return; this._running = true; this._beatCount = 0; this._blockBeat = 0; this._currentSequence = selectSequence('medium'); this._frameId = requestAnimationFrame(this._tick); this._emit('started'); } stop() { this._running = false; if (this._frameId) { cancelAnimationFrame(this._frameId); this._frameId = null; } // Reset LED this.robot.ledEffectStop().catch(() => {}); this._emit('stopped'); } // --- Main control loop (like duo's _tick at 50Hz) --- // We run at display framerate (~60Hz) but throttle robot commands via safety _tick(timestamp) { if (!this._running) return; this._frameId = requestAnimationFrame(this._tick); // Get audio analysis for this frame const analysis = this.audio.analyze(timestamp); const { bass, mid, high, energy, isBeat, bpm } = analysis; // Classify energy level (like duo's sequence_assignments) const energyLevel = classifyEnergy(energy); // On energy level change: select new sequence (like duo's _selectSequence) if (energyLevel !== this._lastBlockEnergy) { this._currentSequence = selectSequence(energyLevel); this._blockBeat = 0; this._lastBlockEnergy = energyLevel; this._emit('energy-change', { level: energyLevel }); } // --- On beat: execute move from sequence --- if (isBeat) { this._beatCount++; this._blockBeat = this._beatCount % 8; // Every 8 beats: rotate to next sequence (like duo's block transitions) if (this._blockBeat === 0) { this._currentSequence = selectSequence(energyLevel); } const move = getMoveAtBeat(this._currentSequence, this._blockBeat); this._executeMove(move, analysis, timestamp); this._emit('beat', { beat: this._beatCount, move, bpm, energy: energyLevel }); } // --- Continuous: ambient LED color from spectrum --- if (this.ledEnabled) { this._sendAmbientLed(bass, mid, high, energy, isBeat, timestamp); } // --- Emit tick for visualizer --- this._emit('tick', { bass, mid, high, energy, isBeat, bpm, timestamp }); } // Execute a single move from the sequence _executeMove(move, analysis, timestamp) { // Servo: absolute joint positions via /servo/move if (this.servoEnabled && move.positions) { const moveKey = JSON.stringify(move.positions); if (this.safety.shouldSend('servo', timestamp, moveKey)) { this.robot.servoMove(move.positions, move.duration).catch(() => {}); this.safety.markSent('servo', timestamp, moveKey); const yaw = move.positions['base_yaw.pos']; const pitch = move.positions['base_pitch.pos']; this._emit('command', { type: 'servo', yaw, pitch, duration: move.duration }); } } // Emotion (sparse — only some beats have emotion triggers) if (this.emotionEnabled && move.emotion) { if (this.safety.shouldSend('emotion', timestamp, move.emotion)) { const intensity = Math.min(analysis.energy / 180, 1.0); this.robot.emotion(move.emotion, intensity).catch(() => {}); this.safety.markSent('emotion', timestamp, move.emotion); this._emit('command', { type: 'emotion', emotion: move.emotion, intensity }); } } } // Continuous LED: spectrum color between beats, flash on beats _sendAmbientLed(bass, mid, high, energy, isBeat, timestamp) { let color; if (isBeat && energy > 80) { // Beat flash: bright color from spectrum + boost const base = spectrumColor(bass, mid, high); color = flashColor(base, Math.min(energy / 200, 1)); } else { // Between beats: dim spectrum color const base = spectrumColor(bass, mid, high); color = dimColor(base, 0.3 + (energy / 255) * 0.3); } if (this.safety.shouldSend('led', timestamp, color)) { this.robot.ledSolid(color, true).catch(() => {}); this.safety.markSent('led', timestamp, color); } } _emit(type, detail = {}) { this.dispatchEvent(new CustomEvent(type, { detail })); } }