CreaturesDigital commited on
Commit
35af364
·
1 Parent(s): 2edf3f2

v1.1.0: Auto-detect and fix ALSA configuration on startup

Browse files

- Added get_alsa_card_numbers() to detect speaker and loopback cards
- Added generate_alsa_config() to create proper ALSA configuration
- Added check_and_fix_alsa_config() that runs on app startup
- Automatically fixes ALSA card numbers after reboot
- Updated README with auto-configuration feature

Files changed (3) hide show
  1. README.md +2 -0
  2. pyproject.toml +1 -1
  3. spotify_dancer/main.py +121 -0
README.md CHANGED
@@ -29,6 +29,7 @@ Make your Reachy Mini dance to Spotify music! This app analyzes audio in real-ti
29
  - **Dance Styles** - Energetic, Groovy, Chill, and Hip-Hop modes
30
  - **Stream Analysis** - Direct audio capture via ALSA loopback (best quality)
31
  - **Web Control Panel** - Live visualizer with adjustable settings
 
32
 
33
  ## Requirements
34
 
@@ -129,6 +130,7 @@ Spotify App → raspotify → ALSA multi-output
129
  - **No loopback_in device**: Run `sudo modprobe snd-aloop` and restart the app
130
  - **Poor detection**: Use loopback_in, increase sensitivity, turn up volume
131
  - **Tracks skip**: Check ALSA config and IPC permissions (see setup guide)
 
132
 
133
  ## License
134
 
 
29
  - **Dance Styles** - Energetic, Groovy, Chill, and Hip-Hop modes
30
  - **Stream Analysis** - Direct audio capture via ALSA loopback (best quality)
31
  - **Web Control Panel** - Live visualizer with adjustable settings
32
+ - **Auto-Configuration** - Automatically detects and fixes ALSA audio settings
33
 
34
  ## Requirements
35
 
 
130
  - **No loopback_in device**: Run `sudo modprobe snd-aloop` and restart the app
131
  - **Poor detection**: Use loopback_in, increase sensitivity, turn up volume
132
  - **Tracks skip**: Check ALSA config and IPC permissions (see setup guide)
133
+ - **ALSA errors after reboot**: The app auto-detects and fixes card numbers on startup. If issues persist, see the [setup guide](docs/spotify_setup.md)
134
 
135
  ## License
136
 
pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
 
5
  [project]
6
  name = "spotify_dancer"
7
- version = "1.0.0"
8
  description = "Spotify Dance Mode for Reachy Mini - Robot dances to your music"
9
  readme = "README.md"
10
  requires-python = ">=3.10"
 
4
 
5
  [project]
6
  name = "spotify_dancer"
7
+ version = "1.1.0"
8
  description = "Spotify Dance Mode for Reachy Mini - Robot dances to your music"
9
  readme = "README.md"
10
  requires-python = ">=3.10"
spotify_dancer/main.py CHANGED
@@ -335,6 +335,124 @@ def setup_loopback() -> bool:
335
  return False
336
 
337
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
338
  # =============================================================================
339
  # Main App
340
  # =============================================================================
@@ -358,6 +476,9 @@ class SpotifyDancer(ReachyMiniApp):
358
 
359
  def run(self, reachy_mini: ReachyMini, stop_event: threading.Event):
360
  """Main app loop."""
 
 
 
361
  # Start UI in background
362
  ui_thread = threading.Thread(
363
  target=self._run_ui,
 
335
  return False
336
 
337
 
338
+ def get_alsa_card_numbers() -> tuple:
339
+ """Auto-detect ALSA card numbers for speaker and loopback."""
340
+ speaker_card = None
341
+ loopback_card = None
342
+ try:
343
+ result = subprocess.run(['aplay', '-l'], capture_output=True, text=True, timeout=5)
344
+ if result.returncode == 0:
345
+ for line in result.stdout.split('\n'):
346
+ if line.startswith('card '):
347
+ parts = line.split(':')
348
+ if len(parts) >= 2:
349
+ card_num = parts[0].replace('card ', '').strip()
350
+ card_info = parts[1].lower()
351
+ if 'loopback' in card_info and loopback_card is None:
352
+ loopback_card = card_num
353
+ elif ('audio' in card_info or 'reachy' in card_info) and speaker_card is None:
354
+ speaker_card = card_num
355
+ except Exception as e:
356
+ logger.error(f"Failed to detect ALSA cards: {e}")
357
+ return speaker_card, loopback_card
358
+
359
+
360
+ def generate_alsa_config(speaker_card: str, loopback_card: str) -> str:
361
+ """Generate ALSA configuration."""
362
+ return f"""# Reachy Mini ALSA Configuration (auto-generated)
363
+ pcm.!default {{ type hw; card {speaker_card} }}
364
+ ctl.!default {{ type hw; card {speaker_card} }}
365
+
366
+ pcm.reachymini_audio_sink {{
367
+ type dmix
368
+ ipc_key 4241
369
+ ipc_perm 0777
370
+ ipc_key_add_uid false
371
+ slave {{ pcm "hw:{speaker_card},0"; channels 2; period_size 1024; buffer_size 4096; rate 16000 }}
372
+ }}
373
+
374
+ pcm.reachymini_audio_src {{
375
+ type dsnoop
376
+ ipc_key 4242
377
+ ipc_perm 0777
378
+ ipc_key_add_uid false
379
+ slave {{ pcm "hw:{speaker_card},0"; channels 2; rate 16000; period_size 1024; buffer_size 4096 }}
380
+ }}
381
+
382
+ pcm.loopback_out {{ type plug; slave.pcm "hw:{loopback_card},0" }}
383
+ pcm.loopback_in {{ type plug; slave.pcm "hw:{loopback_card},1" }}
384
+
385
+ pcm.spotify_multi {{
386
+ type plug
387
+ slave.pcm {{
388
+ type multi
389
+ slaves {{
390
+ a {{ pcm "reachymini_audio_sink" channels 2 }}
391
+ b {{ pcm "loopback_out" channels 2 }}
392
+ }}
393
+ bindings {{
394
+ 0 {{ slave a channel 0 }}
395
+ 1 {{ slave a channel 1 }}
396
+ 2 {{ slave b channel 0 }}
397
+ 3 {{ slave b channel 1 }}
398
+ }}
399
+ }}
400
+ ttable.0.0 1
401
+ ttable.1.1 1
402
+ ttable.0.2 1
403
+ ttable.1.3 1
404
+ }}
405
+
406
+ pcm.spotify {{ type plug; slave.pcm "spotify_multi" }}
407
+ """
408
+
409
+
410
+ def check_and_fix_alsa_config():
411
+ """Check if ALSA config is valid, fix if needed."""
412
+ # Test if spotify device works
413
+ try:
414
+ result = subprocess.run(
415
+ ['aplay', '-D', 'spotify', '-d', '0', '/dev/zero'],
416
+ capture_output=True, timeout=3
417
+ )
418
+ if result.returncode == 0:
419
+ logger.info("ALSA config OK")
420
+ return True
421
+ except Exception:
422
+ pass
423
+
424
+ # Config doesn't work, try to fix it
425
+ logger.warning("ALSA config invalid, attempting auto-fix...")
426
+ speaker_card, loopback_card = get_alsa_card_numbers()
427
+
428
+ if speaker_card is None or loopback_card is None:
429
+ logger.error(f"Could not detect cards: speaker={speaker_card}, loopback={loopback_card}")
430
+ return False
431
+
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)
438
+ result = subprocess.run(
439
+ ['sudo', 'cp', '/tmp/asound.conf', '/etc/asound.conf'],
440
+ capture_output=True, timeout=5
441
+ )
442
+ if result.returncode == 0:
443
+ logger.info("ALSA config updated successfully")
444
+ # Restart raspotify to use new config
445
+ subprocess.run(['sudo', 'systemctl', 'restart', 'raspotify'],
446
+ capture_output=True, timeout=10)
447
+ return True
448
+ else:
449
+ logger.error(f"Failed to save config: {result.stderr.decode()}")
450
+ except Exception as e:
451
+ logger.error(f"Error fixing config: {e}")
452
+
453
+ return False
454
+
455
+
456
  # =============================================================================
457
  # Main App
458
  # =============================================================================
 
476
 
477
  def run(self, reachy_mini: ReachyMini, stop_event: threading.Event):
478
  """Main app loop."""
479
+ # Auto-fix ALSA config if needed
480
+ check_and_fix_alsa_config()
481
+
482
  # Start UI in background
483
  ui_thread = threading.Thread(
484
  target=self._run_ui,