Fix dance-loop tremor (set_target) and raspotify restart lockup (#1130)
#3
by MickaelBourgois - opened
- spotify_dancer/main.py +31 -3
spotify_dancer/main.py
CHANGED
|
@@ -432,6 +432,23 @@ def check_and_fix_alsa_config():
|
|
| 432 |
logger.info(f"Detected: speaker=card {speaker_card}, loopback=card {loopback_card}")
|
| 433 |
|
| 434 |
config = generate_alsa_config(speaker_card, loopback_card)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 435 |
try:
|
| 436 |
with open('/tmp/asound.conf', 'w') as f:
|
| 437 |
f.write(config)
|
|
@@ -499,6 +516,12 @@ class SpotifyDancer(ReachyMiniApp):
|
|
| 499 |
self.latest_features = self.analyzer.latest_features
|
| 500 |
movement = self.controller.update(self.latest_features, dt)
|
| 501 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 502 |
try:
|
| 503 |
head_pose = create_head_pose(
|
| 504 |
z=movement['head_z'],
|
|
@@ -506,12 +529,17 @@ class SpotifyDancer(ReachyMiniApp):
|
|
| 506 |
mm=True,
|
| 507 |
degrees=True
|
| 508 |
)
|
| 509 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 510 |
head=head_pose,
|
| 511 |
antennas=[movement['antenna_left'], movement['antenna_right']],
|
| 512 |
body_yaw=np.deg2rad(movement['body_yaw']),
|
| 513 |
-
duration=0.1,
|
| 514 |
-
method="minjerk"
|
| 515 |
)
|
| 516 |
except Exception as e:
|
| 517 |
logger.debug(f"Movement error: {e}")
|
|
|
|
| 432 |
logger.info(f"Detected: speaker=card {speaker_card}, loopback=card {loopback_card}")
|
| 433 |
|
| 434 |
config = generate_alsa_config(speaker_card, loopback_card)
|
| 435 |
+
|
| 436 |
+
# Idempotency guard: only rewrite /etc/asound.conf and restart raspotify when
|
| 437 |
+
# the config actually changes. Restarting raspotify while the audio analyzer
|
| 438 |
+
# holds the ALSA loopback capture (loopback_in) wedges ALSA in an EINVAL state
|
| 439 |
+
# ("Slave PCM not usable") that survives until a reboot. The aplay probe above
|
| 440 |
+
# can fail transiently for that exact reason while the on-disk config is
|
| 441 |
+
# already correct, so a byte-identical config must not trigger a restart.
|
| 442 |
+
try:
|
| 443 |
+
with open('/etc/asound.conf', 'r') as f:
|
| 444 |
+
existing_config = f.read()
|
| 445 |
+
except OSError:
|
| 446 |
+
existing_config = None
|
| 447 |
+
|
| 448 |
+
if existing_config == config:
|
| 449 |
+
logger.info("ALSA config already up to date, skipping rewrite and restart")
|
| 450 |
+
return True
|
| 451 |
+
|
| 452 |
try:
|
| 453 |
with open('/tmp/asound.conf', 'w') as f:
|
| 454 |
f.write(config)
|
|
|
|
| 516 |
self.latest_features = self.analyzer.latest_features
|
| 517 |
movement = self.controller.update(self.latest_features, dt)
|
| 518 |
|
| 519 |
+
# Deadzone: when there is effectively no audio, hold a neutral
|
| 520 |
+
# pose instead of reacting to numeric noise. Without this the
|
| 521 |
+
# robot twitches continuously even when nothing is playing.
|
| 522 |
+
if self.latest_features.rms < 0.02:
|
| 523 |
+
movement = {key: 0.0 for key in movement}
|
| 524 |
+
|
| 525 |
try:
|
| 526 |
head_pose = create_head_pose(
|
| 527 |
z=movement['head_z'],
|
|
|
|
| 529 |
mm=True,
|
| 530 |
degrees=True
|
| 531 |
)
|
| 532 |
+
# Use set_target (not goto_target) inside this ~20 Hz control
|
| 533 |
+
# loop. goto_target starts a fresh min-jerk profile from zero
|
| 534 |
+
# velocity on every call and blocks until it completes; calling
|
| 535 |
+
# it every 50 ms makes the profile restart 20x/s, producing
|
| 536 |
+
# visible head/antenna tremor independent of the music.
|
| 537 |
+
# AGENTS.md: set_target for loops >=10 Hz, goto_target only for
|
| 538 |
+
# one-shot gestures >=0.5 s.
|
| 539 |
+
reachy_mini.set_target(
|
| 540 |
head=head_pose,
|
| 541 |
antennas=[movement['antenna_left'], movement['antenna_right']],
|
| 542 |
body_yaw=np.deg2rad(movement['body_yaw']),
|
|
|
|
|
|
|
| 543 |
)
|
| 544 |
except Exception as e:
|
| 545 |
logger.debug(f"Movement error: {e}")
|