Skip to content

Commit 75f28d5

Browse files
committed
cava
1 parent 5b9f34b commit 75f28d5

File tree

2 files changed

+96
-28
lines changed

2 files changed

+96
-28
lines changed

modules/cavalcade.py

Lines changed: 94 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ def __init__(self, mainapp):
4949
self.data_handler = mainapp.draw.update
5050
self.command = ["cava", "-p", self.cava_config_file]
5151
self.state = self.NONE
52+
self.process = None
5253

5354
self.env = dict(os.environ)
5455
self.env["LC_ALL"] = "en_US.UTF-8" # not sure if it's necessary
@@ -64,7 +65,6 @@ def __init__(self, mainapp):
6465
self.io_watch_id = None
6566

6667
def _run_process(self):
67-
logger.debug("Launching cava process...")
6868
try:
6969
self.process = subprocess.Popen(
7070
self.command,
@@ -73,13 +73,11 @@ def _run_process(self):
7373
env=self.env,
7474
preexec_fn=set_death_signal # Ensure cava gets killed when the parent dies.
7575
)
76-
logger.debug("cava successfully launched!")
7776
self.state = self.RUNNING
7877
except Exception:
7978
logger.exception("Fail to launch cava")
8079

8180
def _start_io_reader(self):
82-
logger.debug("Activating GLib IO watch for cava stream handler")
8381
# Open FIFO in non-blocking mode for reading
8482
self.fifo_fd = os.open(self.path, os.O_RDONLY | os.O_NONBLOCK)
8583
# Open dummy write end to prevent getting an EOF on our FIFO
@@ -89,31 +87,43 @@ def _start_io_reader(self):
8987
def _io_callback(self, source, condition):
9088
chunk = self.byte_size * self.bars # number of bytes for given format
9189
try:
90+
if self.fifo_fd is None:
91+
return False
92+
9293
data = os.read(self.fifo_fd, chunk)
9394
except OSError as e:
94-
# logger.error("Error reading FIFO: {}".format(e))
95+
if e.errno == 11: # EAGAIN - would block, normal for non-blocking
96+
return True
97+
elif e.errno == 9: # EBADF - bad file descriptor
98+
GLib.idle_add(self.restart)
99+
return False
100+
else:
101+
return False
102+
except Exception:
95103
return False
96104

97105
# When no data is read, do not remove the IO watch immediately.
98106
if len(data) < chunk:
99-
# Instead of closing the FIFO, we log a warning and continue.
100-
# logger.warning("Incomplete data packet received (expected {} bytes, got {}). Waiting for more data...".format(chunk, len(data)))
101-
# Returning True keeps the IO watch active. A real EOF will only occur when the writer closes.
102-
return True
107+
if len(data) == 0:
108+
# No data available, continue watching
109+
return True
110+
else:
111+
return True
103112

104-
fmt = self.byte_type * self.bars # format string for struct.unpack
105-
sample = [i / self.byte_norm for i in struct.unpack(fmt, data)]
106-
GLib.idle_add(self.data_handler, sample)
113+
try:
114+
fmt = self.byte_type * self.bars # format string for struct.unpack
115+
sample = [i / self.byte_norm for i in struct.unpack(fmt, data)]
116+
GLib.idle_add(self.data_handler, sample)
117+
except (struct.error, Exception):
118+
return True
119+
107120
return True
108121

109122
def _on_stop(self):
110-
logger.debug("Cava stream handler deactivated")
111123
if self.state == self.RESTARTING:
112124
self.start()
113125
elif self.state == self.RUNNING:
114126
self.state = self.NONE
115-
logger.error("Cava process was unexpectedly terminated.")
116-
# self.restart() # May cause infinity loop, need more check
117127

118128
def start(self):
119129
"""Launch cava"""
@@ -123,27 +133,54 @@ def start(self):
123133
def restart(self):
124134
"""Restart cava process"""
125135
if self.state == self.RUNNING:
126-
logger.debug("Restarting cava process (normal mode) ...")
127136
self.state = self.RESTARTING
128-
if self.process.poll() is None:
137+
if self.process and self.process.poll() is None:
129138
self.process.kill()
130139
elif self.state == self.NONE:
131-
logger.warning("Restarting cava process (after crash) ...")
132140
self.start()
133141

134142
def close(self):
135143
"""Stop cava process"""
136144
self.state = self.CLOSING
137-
if self.process.poll() is None:
138-
self.process.kill()
145+
146+
# Stop IO watch first
139147
if self.io_watch_id:
140148
GLib.source_remove(self.io_watch_id)
141-
if self.fifo_fd:
142-
os.close(self.fifo_fd)
143-
if self.fifo_dummy_fd:
144-
os.close(self.fifo_dummy_fd)
149+
self.io_watch_id = None
150+
151+
# Close file descriptors safely
152+
if self.fifo_fd is not None:
153+
try:
154+
os.close(self.fifo_fd)
155+
except OSError:
156+
pass
157+
finally:
158+
self.fifo_fd = None
159+
160+
if self.fifo_dummy_fd is not None:
161+
try:
162+
os.close(self.fifo_dummy_fd)
163+
except OSError:
164+
pass
165+
finally:
166+
self.fifo_dummy_fd = None
167+
168+
# Kill process if still running
169+
if self.process and self.process.poll() is None:
170+
try:
171+
self.process.kill()
172+
self.process.wait(timeout=2.0) # Wait up to 2 seconds
173+
except subprocess.TimeoutExpired:
174+
self.process.kill()
175+
except Exception:
176+
pass
177+
178+
# Remove FIFO file
145179
if os.path.exists(self.path):
146-
os.remove(self.path)
180+
try:
181+
os.remove(self.path)
182+
except OSError:
183+
pass
147184

148185
class AttributeDict(dict):
149186
"""Dictionary with keys as attributes. Does nothing but easy reading"""
@@ -159,6 +196,8 @@ def __init__(self):
159196
self.silence_value = 0
160197
self.audio_sample = []
161198
self.color = None
199+
self._cached_color = None
200+
self._color_file_mtime = 0
162201

163202
self.area = Gtk.DrawingArea()
164203
self.area.connect("draw", self.redraw)
@@ -181,7 +220,7 @@ def is_silence(self, value):
181220

182221
def update(self, data):
183222
"""Audio data processing"""
184-
self.color_update()
223+
self.color_update_cached()
185224
self.audio_sample = data
186225
if not self.is_silence(self.audio_sample[0]):
187226
self.area.queue_draw()
@@ -226,6 +265,34 @@ def size_update(self, *args):
226265
self.sizes.bar.width = max(int(tw / self.sizes.number), 1)
227266
self.sizes.bar.height = self.sizes.area.height
228267

268+
def color_update_cached(self):
269+
"""Set drawing color with caching to avoid file reads on every frame"""
270+
color_file = get_relative_path("../styles/colors.css")
271+
try:
272+
# Check if file has been modified
273+
current_mtime = os.path.getmtime(color_file)
274+
if current_mtime != self._color_file_mtime or self._cached_color is None:
275+
self._color_file_mtime = current_mtime
276+
277+
color = "#a5c8ff" # default value
278+
with open(color_file, "r") as f:
279+
content = f.read()
280+
m = re.search(r"--primary:\s*(#[0-9a-fA-F]{6})", content)
281+
if m:
282+
color = m.group(1)
283+
284+
red = int(color[1:3], 16) / 255
285+
green = int(color[3:5], 16) / 255
286+
blue = int(color[5:7], 16) / 255
287+
self._cached_color = Gdk.RGBA(red=red, green=green, blue=blue, alpha=1.0)
288+
289+
self.color = self._cached_color
290+
except Exception:
291+
if self._cached_color is None:
292+
# Fallback to default color
293+
self._cached_color = Gdk.RGBA(red=0.647, green=0.784, blue=1.0, alpha=1.0)
294+
self.color = self._cached_color
295+
229296
def color_update(self):
230297
"""Set drawing color according to current settings by reading primary color from CSS"""
231298
color = "#a5c8ff" # default value
@@ -235,8 +302,8 @@ def color_update(self):
235302
m = re.search(r"--primary:\s*(#[0-9a-fA-F]{6})", content)
236303
if m:
237304
color = m.group(1)
238-
except Exception as e:
239-
logger.error("Failed to read primary color: {}".format(e))
305+
except Exception:
306+
pass
240307
red = int(color[1:3], 16) / 255
241308
green = int(color[3:5], 16) / 255
242309
blue = int(color[5:7], 16) / 255

version.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"version": "0.0.41",
33
"pkg_update": false,
44
"changelog": [
5-
"<b>fix:</b> Hover to show notch not working"
5+
"<b>fix:</b> Hover to show notch not working",
6+
"<b>tweak:</b> Optimized cavalcade"
67
]
78
}

0 commit comments

Comments
 (0)