robot-dance-party / js /dance_engine.js
autonomous-ai's picture
Upload folder using huggingface_hub
61bd138 verified
Raw
History Blame Contribute Delete
5.42 kB
// 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 }));
}
}