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.
- package/LICENSE +21 -0
- package/README.md +104 -0
- package/bin/lush +62 -0
- package/bin/lush.js +48 -0
- package/package.json +61 -0
- package/pyproject.toml +52 -0
- package/scripts/postinstall.js +58 -0
- package/scripts/sync-version.js +38 -0
- package/setup.py +23 -0
- package/src/lush/__init__.py +30 -0
- package/src/lush/__main__.py +42 -0
- package/src/lush/audio.py +222 -0
- package/src/lush/cava.py +665 -0
- package/src/lush/constants.py +686 -0
- package/src/lush/data/cava.conf +19 -0
- package/src/lush/data/stations.json +3586 -0
- package/src/lush/modals.py +1382 -0
- package/src/lush/net.py +27 -0
- package/src/lush/notify.py +154 -0
- package/src/lush/search.py +185 -0
- package/src/lush/state.py +550 -0
- package/src/lush/stats.py +555 -0
- package/src/lush/ui.py +690 -0
- package/src/lush/ui_helpers.py +569 -0
package/src/lush/cava.py
ADDED
|
@@ -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
|