lushapp 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,665 @@
1
+ # LUSH - Live CAVA Real-Time Audio DSP Engine & Shimmer Visualizers
2
+ import subprocess
3
+ import threading
4
+ import time
5
+ import math
6
+ import random
7
+ from pathlib import Path
8
+
9
+ class CavaVisualizerEngine:
10
+ def __init__(self, config_file: Path, sensitivity: int = 180, framerate: int = 144):
11
+ self.config_file = config_file
12
+ self.sensitivity = sensitivity
13
+ self.framerate = framerate
14
+ self.bars = [0] * 32
15
+ self.running = True
16
+ self.proc = None
17
+ self.lock = threading.Lock()
18
+
19
+ self._ensure_config()
20
+ self.thread = threading.Thread(target=self._reader_loop, daemon=True)
21
+ self.thread.start()
22
+
23
+ def _ensure_config(self):
24
+ self.config_file.parent.mkdir(parents=True, exist_ok=True)
25
+ cfg_content = f"""[general]
26
+ framerate = {self.framerate}
27
+ bars = 32
28
+ sensitivity = {self.sensitivity}
29
+ autosens = 1
30
+
31
+ [input]
32
+ method = pulse
33
+ source = auto
34
+
35
+ [output]
36
+ method = raw
37
+ raw_target = /dev/stdout
38
+ data_format = ascii
39
+ ascii_max_range = 8
40
+
41
+ [smoothing]
42
+ monstercat = 1.2
43
+ noise_reduction = 20
44
+ """
45
+ with open(self.config_file, "w", encoding="utf-8") as f:
46
+ f.write(cfg_content)
47
+
48
+ def update_settings(self, sensitivity: int, framerate: int = 144):
49
+ self.sensitivity = sensitivity
50
+ self.framerate = framerate
51
+ self._ensure_config()
52
+ if self.proc:
53
+ try:
54
+ self.proc.terminate()
55
+ except Exception:
56
+ pass
57
+
58
+ def _reader_loop(self):
59
+ while self.running:
60
+ try:
61
+ self.proc = subprocess.Popen(
62
+ ["cava", "-p", str(self.config_file)],
63
+ stdout=subprocess.PIPE,
64
+ stderr=subprocess.DEVNULL,
65
+ text=True
66
+ )
67
+ while self.running:
68
+ line = self.proc.stdout.readline()
69
+ if not line: break
70
+ vals = [int(x) for x in line.strip().split(';') if x.isdigit()]
71
+ if vals:
72
+ with self.lock:
73
+ self.bars = vals[:32]
74
+ except Exception:
75
+ # Procedural harmonic wave fallback when CAVA is not installed
76
+ t0 = time.time()
77
+ for _ in range(30):
78
+ if not self.running: break
79
+ t = time.time() - t0
80
+ synth_bars = []
81
+ for i in range(32):
82
+ freq = (i + 1) * 0.8
83
+ v = (math.sin(t * 5.0 + i * 0.4) * 0.5 + 0.5) * \
84
+ (math.cos(t * 2.5 + freq) * 0.3 + 0.7) * \
85
+ (math.sin(t * 9.0 + i * 0.8) * 0.2 + 0.8)
86
+ amp = max(0, min(8, int(v * 8 * (1.0 - (i / 64.0)))))
87
+ synth_bars.append(amp)
88
+ with self.lock:
89
+ self.bars = synth_bars
90
+ time.sleep(1.0 / 30.0)
91
+
92
+ def stop(self):
93
+ self.running = False
94
+ if self.proc:
95
+ try:
96
+ if self.proc.stdout:
97
+ self.proc.stdout.close()
98
+ except Exception:
99
+ pass
100
+ try:
101
+ self.proc.terminate()
102
+ self.proc.wait(timeout=0.2)
103
+ except Exception:
104
+ try:
105
+ self.proc.kill()
106
+ self.proc.wait(timeout=0.1)
107
+ except Exception:
108
+ pass
109
+ self.proc = None
110
+ if hasattr(self, 'thread') and self.thread.is_alive() and threading.current_thread() != self.thread:
111
+ self.thread.join(timeout=0.3)
112
+
113
+ # 1. Standard Spectrum
114
+ def get_spectrum_str(self, width: int, is_playing: bool) -> str:
115
+ if not is_playing: return " " * width
116
+ with self.lock:
117
+ cur = list(self.bars)
118
+ chars = [' ', ' ', '▂', '▃', '▄', '▅', '▆', '▇', '█']
119
+ if not cur or all(b == 0 for b in cur): return " " * width
120
+
121
+ step = len(cur) / max(1, width)
122
+ out = []
123
+ for i in range(width):
124
+ src_i = min(len(cur)-1, int(i * step))
125
+ v = min(len(chars)-1, max(0, cur[src_i]))
126
+ out.append(chars[v])
127
+ return ''.join(out)
128
+
129
+ # 2. Shimmer Spectrum Bars (with dynamic white-hot tips and traveling sheen wave)
130
+ def get_shimmer_spectrum_elements(self, width: int, is_playing: bool, t_now: float):
131
+ if not is_playing:
132
+ return [(" ", 14)] * width
133
+ with self.lock:
134
+ cur = list(self.bars)
135
+ chars = [' ', ' ', '▂', '▃', '▄', '▅', '▆', '▇', '█']
136
+ if not cur or all(b == 0 for b in cur):
137
+ return [(" ", 14)] * width
138
+
139
+ wave_pos = ((t_now * 0.9) % 2.2) - 0.4
140
+ step = len(cur) / max(1, width)
141
+ out = []
142
+
143
+ for i in range(width):
144
+ src_i = min(len(cur)-1, int(i * step))
145
+ raw_v = cur[src_i]
146
+ v = min(len(chars)-1, max(0, raw_v))
147
+ ch = chars[v]
148
+
149
+ x_norm = i / max(1, width - 1)
150
+ dist = abs(x_norm - wave_pos)
151
+
152
+ # Intensity blends bar amplitude with sheen wave
153
+ amp_ratio = raw_v / 8.0
154
+ glow_boost = max(0.0, 0.45 - dist * 2.2)
155
+ intensity = amp_ratio * 0.65 + glow_boost
156
+
157
+ if intensity > 0.85:
158
+ pair = 10 # Peak White-Hot
159
+ elif intensity > 0.65:
160
+ pair = 11 # Sheen Highlight
161
+ elif intensity > 0.45:
162
+ pair = 12 # Neon Glow
163
+ elif intensity > 0.25:
164
+ pair = 13 # Mid Glow
165
+ else:
166
+ pair = 14 # Base Tone
167
+
168
+ out.append((ch, pair))
169
+ return out
170
+
171
+ # 3. Shimmer Audio String (Vibrating Braille string with sheen wave)
172
+ def get_shimmer_string_elements(self, width: int, is_playing: bool, t_now: float):
173
+ if not is_playing:
174
+ return [("─", 14)] * width
175
+ with self.lock:
176
+ cur = list(self.bars)
177
+ braille = ['⣀', '⣤', '⣶', '⣷', '⣿', '⣾', '⣴', '⣄', '⣀']
178
+ if not cur or all(b == 0 for b in cur):
179
+ return [("─", 14)] * width
180
+
181
+ wave_pos = ((t_now * 0.95) % 2.2) - 0.4
182
+ step = len(cur) / max(1, width)
183
+ out = []
184
+
185
+ for i in range(width):
186
+ src_i = min(len(cur)-1, int(i * step))
187
+ raw_v = cur[src_i]
188
+ v = min(len(braille)-1, max(0, raw_v))
189
+ ch = braille[v]
190
+
191
+ x_norm = i / max(1, width - 1)
192
+ dist = abs(x_norm - wave_pos)
193
+
194
+ amp_ratio = raw_v / 8.0
195
+ glow_boost = max(0.0, 0.5 - dist * 2.5)
196
+ intensity = amp_ratio * 0.6 + glow_boost
197
+
198
+ if intensity > 0.80:
199
+ pair = 10
200
+ elif intensity > 0.60:
201
+ pair = 11
202
+ elif intensity > 0.40:
203
+ pair = 12
204
+ elif intensity > 0.20:
205
+ pair = 13
206
+ else:
207
+ pair = 14
208
+
209
+ out.append((ch, pair))
210
+ return out
211
+
212
+
213
+ # 4. Harmonic Resonant Strings (Harmonic nodes and antinodes with traveling glints)
214
+ def get_harmonic_strings_elements(self, width: int, is_playing: bool, t_now: float):
215
+ if not is_playing:
216
+ return [("─", 14)] * width
217
+ with self.lock:
218
+ cur = list(self.bars)
219
+ if not cur or all(b == 0 for b in cur):
220
+ return [("─", 14)] * width
221
+
222
+ wave_pos = ((t_now * 1.1) % 2.2) - 0.4
223
+ step = len(cur) / max(1, width)
224
+ out = []
225
+
226
+ for i in range(width):
227
+ src_i = min(len(cur)-1, int(i * step))
228
+ raw_v = cur[src_i]
229
+ x_norm = i / max(1, width - 1)
230
+ dist = abs(x_norm - wave_pos)
231
+
232
+ # Calculate harmonic node vibration
233
+ harm_phase = math.sin(x_norm * math.pi * 4.0 + t_now * 4.0)
234
+ amp = (raw_v / 8.0) * (0.5 + 0.5 * harm_phase)
235
+
236
+ if amp > 0.75:
237
+ ch = "◈"
238
+ elif amp > 0.55:
239
+ ch = "◇"
240
+ elif amp > 0.35:
241
+ ch = "·"
242
+ elif amp > 0.18:
243
+ ch = "╌"
244
+ else:
245
+ ch = "─"
246
+
247
+ glow = max(0.0, 0.55 - dist * 2.4)
248
+ intensity = (raw_v / 8.0) * 0.6 + glow
249
+
250
+ if intensity > 0.80:
251
+ pair = 10
252
+ elif intensity > 0.60:
253
+ pair = 11
254
+ elif intensity > 0.40:
255
+ pair = 12
256
+ elif intensity > 0.20:
257
+ pair = 13
258
+ else:
259
+ pair = 14
260
+
261
+ out.append((ch, pair))
262
+ return out
263
+
264
+ # 5. Neon Laser Cords (High-voltage pulsing laser cords with tension bending)
265
+ def get_laser_cords_elements(self, width: int, is_playing: bool, t_now: float):
266
+ if not is_playing:
267
+ return [("═", 14)] * width
268
+ with self.lock:
269
+ cur = list(self.bars)
270
+ if not cur or all(b == 0 for b in cur):
271
+ return [("═", 14)] * width
272
+
273
+ spark_pos = ((t_now * 1.6) % 2.0) - 0.3
274
+ step = len(cur) / max(1, width)
275
+ out = []
276
+
277
+ for i in range(width):
278
+ src_i = min(len(cur)-1, int(i * step))
279
+ raw_v = cur[src_i]
280
+ x_norm = i / max(1, width - 1)
281
+ dist = abs(x_norm - spark_pos)
282
+
283
+ if dist < 0.05 and raw_v >= 3:
284
+ ch = "⟡"
285
+ pair = 10
286
+ elif dist < 0.12 and raw_v >= 2:
287
+ ch = "⟢"
288
+ pair = 11
289
+ elif raw_v >= 6:
290
+ ch = "═"
291
+ pair = 10 if dist < 0.3 else 11
292
+ elif raw_v >= 4:
293
+ ch = "━"
294
+ pair = 12
295
+ elif raw_v >= 2:
296
+ ch = "─"
297
+ pair = 13
298
+ else:
299
+ ch = "═"
300
+ pair = 14
301
+
302
+ out.append((ch, pair))
303
+ return out
304
+
305
+ # 6. Quantum String Shimmer (Superstring theory particle vibration)
306
+ def get_quantum_strings_elements(self, width: int, is_playing: bool, t_now: float):
307
+ if not is_playing:
308
+ return [("·", 14)] * width
309
+ with self.lock:
310
+ cur = list(self.bars)
311
+ if not cur or all(b == 0 for b in cur):
312
+ return [("·", 14)] * width
313
+
314
+ particles = ["·", "•", "○", "●", "⊛", "✦", "⊛", "●", "○", "•", "·"]
315
+ wave_pos = ((t_now * 0.85) % 2.2) - 0.4
316
+ step = len(cur) / max(1, width)
317
+ out = []
318
+
319
+ for i in range(width):
320
+ src_i = min(len(cur)-1, int(i * step))
321
+ raw_v = cur[src_i]
322
+ x_norm = i / max(1, width - 1)
323
+ dist = abs(x_norm - wave_pos)
324
+
325
+ p_idx = (int(t_now * 8) + int(raw_v * 1.5) + i) % len(particles)
326
+ ch = particles[p_idx] if raw_v > 0 else "·"
327
+
328
+ glow = max(0.0, 0.5 - dist * 2.2)
329
+ intensity = (raw_v / 8.0) * 0.65 + glow
330
+
331
+ if intensity > 0.80:
332
+ pair = 10
333
+ elif intensity > 0.60:
334
+ pair = 11
335
+ elif intensity > 0.40:
336
+ pair = 12
337
+ elif intensity > 0.20:
338
+ pair = 13
339
+ else:
340
+ pair = 14
341
+
342
+ out.append((ch, pair))
343
+ return out
344
+
345
+ # 7. Aurora Shimmer Strings (Flowing celestial ribbons with wave drift)
346
+ def get_aurora_ribbon_elements(self, width: int, is_playing: bool, t_now: float):
347
+ if not is_playing:
348
+ return [("~", 14)] * width
349
+ with self.lock:
350
+ cur = list(self.bars)
351
+ if not cur or all(b == 0 for b in cur):
352
+ return [("~", 14)] * width
353
+
354
+ aurora_chars = ["~", "∿", "≈", "≋", "∿", "≈", "~"]
355
+ wave_pos = ((t_now * 0.75) % 2.2) - 0.4
356
+ step = len(cur) / max(1, width)
357
+ out = []
358
+
359
+ for i in range(width):
360
+ src_i = min(len(cur)-1, int(i * step))
361
+ raw_v = cur[src_i]
362
+ x_norm = i / max(1, width - 1)
363
+ dist = abs(x_norm - wave_pos)
364
+
365
+ wave_offset = math.sin(x_norm * 6.0 + t_now * 3.0)
366
+ idx = int((wave_offset + 1.0) * 0.5 * (len(aurora_chars) - 1))
367
+ ch = aurora_chars[max(0, min(len(aurora_chars)-1, idx))]
368
+
369
+ glow = max(0.0, 0.55 - dist * 2.3)
370
+ intensity = (raw_v / 8.0) * 0.6 + glow
371
+
372
+ if intensity > 0.80:
373
+ pair = 10
374
+ elif intensity > 0.60:
375
+ pair = 11
376
+ elif intensity > 0.40:
377
+ pair = 12
378
+ elif intensity > 0.20:
379
+ pair = 13
380
+ else:
381
+ pair = 14
382
+
383
+ out.append((ch, pair))
384
+ return out
385
+
386
+ # 8. Stereo Harp Strings (Dual acoustic harp strings plucking towards center)
387
+ def get_stereo_harp_elements(self, width: int, is_playing: bool, t_now: float):
388
+ if not is_playing:
389
+ return [("─", 14)] * width
390
+ with self.lock:
391
+ cur = list(self.bars)
392
+ if not cur or all(b == 0 for b in cur):
393
+ return [("─", 14)] * width
394
+
395
+ half_w = max(2, (width - 1) // 2)
396
+ step = len(cur) / max(1, half_w)
397
+ left_side = []
398
+ right_side = []
399
+
400
+ for i in range(half_w):
401
+ l_src = min(len(cur)-1, int(i * step))
402
+ r_src = min(len(cur)-1, int((half_w - 1 - i) * step))
403
+ l_v = cur[l_src]
404
+ r_v = cur[r_src]
405
+
406
+ l_ch = "◈" if l_v >= 6 else ("◇" if l_v >= 4 else ("─" if l_v >= 2 else "╌"))
407
+ l_pair = 10 if l_v >= 7 else (11 if l_v >= 5 else (12 if l_v >= 3 else 14))
408
+ left_side.append((l_ch, l_pair))
409
+
410
+ r_ch = "◈" if r_v >= 6 else ("◇" if r_v >= 4 else ("─" if r_v >= 2 else "╌"))
411
+ r_pair = 10 if r_v >= 7 else (11 if r_v >= 5 else (12 if r_v >= 3 else 14))
412
+ right_side.append((r_ch, r_pair))
413
+
414
+ center_ch = ("|*|" if max(cur) >= 7 else " | ", 10 if max(cur) >= 7 else 11)
415
+ return left_side + [(center_ch[0][1], center_ch[1])] + right_side
416
+
417
+ # 9. Mirrored Butterfly Spectrum (Dual Wings)
418
+ def get_mirrored_butterfly_elements(self, width: int, is_playing: bool, t_now: float):
419
+ if not is_playing:
420
+ return [(" ", 14)] * width
421
+ with self.lock:
422
+ cur = list(self.bars)
423
+ chars = [' ', ' ', '▂', '▃', '▄', '▅', '▆', '▇', '█']
424
+ if not cur or all(b == 0 for b in cur):
425
+ return [(" ", 14)] * width
426
+
427
+ half_w = max(2, (width - 1) // 2)
428
+ step = len(cur) / max(1, half_w)
429
+ right_wing = []
430
+
431
+ for i in range(half_w):
432
+ src_i = min(len(cur)-1, int(i * step))
433
+ raw_v = cur[src_i]
434
+ v = min(len(chars)-1, max(0, raw_v))
435
+ ch = chars[v]
436
+
437
+ pair = 10 if raw_v >= 7 else (11 if raw_v >= 5 else (12 if raw_v >= 3 else 14))
438
+ right_wing.append((ch, pair))
439
+
440
+ left_wing = list(reversed(right_wing))
441
+ center_char = ("│", 11 if max(cur) >= 6 else 14)
442
+
443
+ return left_wing + [center_char] + right_wing
444
+
445
+ # 5. Cyberpunk Matrix Rain Stream
446
+ def get_matrix_rain_elements(self, width: int, is_playing: bool, t_now: float):
447
+ if not is_playing:
448
+ return [(" ", 14)] * width
449
+ with self.lock:
450
+ cur = list(self.bars)
451
+ glyphs = ['0', '1', 'ア', 'カ', 'サ', 'タ', 'ナ', 'ハ', 'ミ', 'ヒ', 'ー', ' ']
452
+ if not cur or all(b == 0 for b in cur):
453
+ return [(" ", 14)] * width
454
+
455
+ step = len(cur) / max(1, width)
456
+ out = []
457
+
458
+ for i in range(width):
459
+ src_i = min(len(cur)-1, int(i * step))
460
+ raw_v = cur[src_i]
461
+
462
+ if raw_v == 0:
463
+ out.append((" ", 14))
464
+ else:
465
+ glyph_idx = (int(t_now * 12) + i * 3) % len(glyphs)
466
+ ch = glyphs[glyph_idx]
467
+ pair = 10 if raw_v >= 7 else (11 if raw_v >= 5 else (12 if raw_v >= 3 else 13))
468
+ out.append((ch, pair))
469
+ return out
470
+
471
+ # 6. Standard Waveform Line
472
+
473
+ # 10. High-Energy Spectral Beams
474
+ def get_frequency_beams_elements(self, width: int, is_playing: bool, t_now: float):
475
+ if not is_playing:
476
+ return [("·", 14)] * width
477
+ with self.lock:
478
+ cur = list(self.bars)
479
+ if not cur or all(b == 0 for b in cur):
480
+ return [("·", 14)] * width
481
+
482
+ step = len(cur) / max(1, width)
483
+ beam_chars = [" ", "▂", "▃", "▄", "▅", "▆", "▇", "█"]
484
+ out = []
485
+ for i in range(width):
486
+ src_i = min(len(cur)-1, int(i * step))
487
+ v = cur[src_i]
488
+ ch = beam_chars[min(len(beam_chars)-1, v)]
489
+ if v >= 7: p = 10
490
+ elif v >= 5: p = 11
491
+ elif v >= 3: p = 12
492
+ elif v >= 1: p = 13
493
+ else: p = 14
494
+ out.append((ch, p))
495
+ return out
496
+
497
+ # 11. Acoustic Wave Interference
498
+ def get_sine_interference_elements(self, width: int, is_playing: bool, t_now: float):
499
+ if not is_playing:
500
+ return [("~", 14)] * width
501
+ with self.lock:
502
+ cur = list(self.bars)
503
+ if not cur or all(b == 0 for b in cur):
504
+ return [("~", 14)] * width
505
+
506
+ energy = sum(cur) / max(1, len(cur) * 8.0)
507
+ bass = max(cur[:4]) / 8.0 if len(cur) >= 4 else 0
508
+ treble = max(cur[14:]) / 8.0 if len(cur) >= 15 else 0
509
+ out = []
510
+ for i in range(width):
511
+ x_norm = i / max(1, width - 1)
512
+ w1 = math.sin(x_norm * 8.0 + t_now * (2.0 + bass * 4.0))
513
+ w2 = math.sin(x_norm * 14.0 - t_now * (3.0 + treble * 5.0))
514
+ interference = (w1 + w2) * 0.5
515
+ if abs(interference) > 0.75:
516
+ ch = "◈" if energy > 0.6 else "◇"
517
+ p = 10 if energy > 0.6 else 11
518
+ elif abs(interference) > 0.40:
519
+ ch = "≈"
520
+ p = 12
521
+ elif abs(interference) > 0.15:
522
+ ch = "∿"
523
+ p = 13
524
+ else:
525
+ ch = "·"
526
+ p = 14
527
+ out.append((ch, p))
528
+ return out
529
+
530
+ # 12. High-Res Braille Oscilloscope
531
+ def get_braille_oscilloscope_elements(self, width: int, is_playing: bool, t_now: float):
532
+ if not is_playing:
533
+ return [("⠤", 14)] * width
534
+ with self.lock:
535
+ cur = list(self.bars)
536
+ if not cur or all(b == 0 for b in cur):
537
+ return [("⠤", 14)] * width
538
+
539
+ braille_waves = ["⠤", "⠢", "⠰", "⠠", "⠤", "⠶", "⠒", "⠓", "⠚", "⠛"]
540
+ step = len(cur) / max(1, width)
541
+ out = []
542
+ for i in range(width):
543
+ src_i = min(len(cur)-1, int(i * step))
544
+ v = cur[src_i]
545
+ idx = min(len(braille_waves)-1, int((v / 8.0) * (len(braille_waves)-1)))
546
+ ch = braille_waves[idx]
547
+ p = 10 if v >= 7 else (11 if v >= 5 else (12 if v >= 3 else 13))
548
+ out.append((ch, p))
549
+ return out
550
+
551
+ # 13. Bass Transient Shockwave
552
+ def get_radial_shock_burst_elements(self, width: int, is_playing: bool, t_now: float):
553
+ if not is_playing:
554
+ return [("·", 14)] * width
555
+ with self.lock:
556
+ cur = list(self.bars)
557
+ if not cur or all(b == 0 for b in cur):
558
+ return [("·", 14)] * width
559
+
560
+ bass = max(cur[:4]) / 8.0 if len(cur) >= 4 else 0
561
+ shock_pos = (t_now * (1.5 + bass * 3.0)) % 1.2
562
+ out = []
563
+ for i in range(width):
564
+ dist_c = abs((i / max(1, width - 1)) - 0.5) * 2.0
565
+ wave_diff = abs(dist_c - shock_pos)
566
+ if wave_diff < 0.10 and bass > 0.6:
567
+ ch = "◈"
568
+ p = 10
569
+ elif wave_diff < 0.20:
570
+ ch = "•"
571
+ p = 11
572
+ elif wave_diff < 0.35:
573
+ ch = "·"
574
+ p = 12
575
+ else:
576
+ ch = "─"
577
+ p = 14
578
+ out.append((ch, p))
579
+ return out
580
+
581
+ # 14. Harmonic Constellation
582
+ def get_starlight_constellation_elements(self, width: int, is_playing: bool, t_now: float):
583
+ if not is_playing:
584
+ return [("·", 14)] * width
585
+ with self.lock:
586
+ cur = list(self.bars)
587
+ if not cur or all(b == 0 for b in cur):
588
+ return [("·", 14)] * width
589
+
590
+ step = len(cur) / max(1, width)
591
+ out = []
592
+ for i in range(width):
593
+ src_i = min(len(cur)-1, int(i * step))
594
+ v = cur[src_i]
595
+ if v >= 7:
596
+ ch, p = "◆", 10
597
+ elif v >= 5:
598
+ ch, p = "◈", 11
599
+ elif v >= 3:
600
+ ch, p = "◇", 12
601
+ elif v >= 1:
602
+ ch, p = "─", 13
603
+ else:
604
+ ch, p = "·", 14
605
+ out.append((ch, p))
606
+ return out
607
+
608
+ def get_waveform_str(self, width: int, is_playing: bool) -> str:
609
+ if not is_playing: return "─" * width
610
+ with self.lock:
611
+ cur = list(self.bars)
612
+ braille = ['⣀', '⣤', '⣶', '⣷', '⣿', '⣾', '⣴', '⣄', '⣀']
613
+ if not cur or all(b == 0 for b in cur): return "─" * width
614
+
615
+ step = len(cur) / max(1, width)
616
+ out = []
617
+ for i in range(width):
618
+ src_i = min(len(cur)-1, int(i * step))
619
+ v = min(len(braille)-1, max(0, cur[src_i]))
620
+ out.append(braille[v])
621
+ return ''.join(out)
622
+
623
+ # 7. Stereo VU Meter
624
+ def get_stereo_vu_str(self, width: int, is_playing: bool) -> str:
625
+ bar_w = max(4, (width - 10) // 2)
626
+ if not is_playing:
627
+ return f"L [{'░' * bar_w}] R [{'░' * bar_w}]"
628
+ with self.lock:
629
+ cur = list(self.bars)
630
+
631
+ half = len(cur) // 2
632
+ l_max = max(cur[:half]) if half > 0 else 0
633
+ r_max = max(cur[half:]) if half > 0 else 0
634
+ l_len = int((l_max / 8.0) * bar_w)
635
+ r_len = int((r_max / 8.0) * bar_w)
636
+ l_str = '█' * l_len + '░' * (bar_w - l_len)
637
+ r_str = '█' * r_len + '░' * (bar_w - r_len)
638
+ return f"L [{l_str}] R [{r_str}]"
639
+
640
+ # 8. Frequency Peak Dots with Shimmer Glow
641
+ def get_peak_dots_elements(self, width: int, is_playing: bool, t_now: float):
642
+ if not is_playing: return [(" ", 14)] * width
643
+ with self.lock:
644
+ cur = list(self.bars)
645
+ dot_chars = [' ', '·', '⠂', '⠒', '⠶', '⠷', '⠿', '█']
646
+ if not cur or all(b == 0 for b in cur): return [(" ", 14)] * width
647
+
648
+ step = len(cur) / max(1, width)
649
+ out = []
650
+ for i in range(width):
651
+ src_i = min(len(cur)-1, int(i * step))
652
+ raw_v = cur[src_i]
653
+ v = min(len(dot_chars)-1, max(0, raw_v))
654
+ ch = dot_chars[v]
655
+ pair = 10 if raw_v >= 7 else (11 if raw_v >= 5 else (12 if raw_v >= 3 else 14))
656
+ out.append((ch, pair))
657
+ return out
658
+
659
+ def shutdown(self):
660
+ self.running = False
661
+ if self.proc:
662
+ try:
663
+ self.proc.terminate()
664
+ except Exception:
665
+ pass