summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBrianna Rainey2026-01-22 16:40:37 -0500
committerBrianna Rainey2026-01-22 16:40:37 -0500
commit801777e5348ed5e5665d2472f14f36673c253d66 (patch)
tree93e56e1d9ddf39d3b0374782303ec7b2f1a3ed84
parenta12be862e22bdec6a243a3f0b5f4f28d69084a2a (diff)
make Life component respond to audio
also adds a dissolve effect between frames and a kaleidoscope effect the fancier shape types ignore audio for now. Fixes #91
-rw-r--r--src/avp/components/life.py263
-rw-r--r--src/avp/components/life.ui82
2 files changed, 267 insertions, 78 deletions
diff --git a/src/avp/components/life.py b/src/avp/components/life.py
index 5b719d1..9e5e202 100644
--- a/src/avp/components/life.py
+++ b/src/avp/components/life.py
@@ -1,13 +1,15 @@
1from PyQt6 import QtGui, QtCore, QtWidgets 1from PyQt6 import QtCore, QtWidgets
2from PyQt6.QtGui import QUndoCommand 2from PyQt6.QtGui import QUndoCommand
3from PIL import Image, ImageDraw, ImageEnhance, ImageChops, ImageFilter 3from PIL import Image, ImageDraw, ImageEnhance, ImageChops, ImageFilter, ImageOps
4import os 4import os
5from copy import copy
5import math 6import math
6import logging 7import logging
7 8
8 9
9from ..component import Component 10from ..component import Component
10from ..toolkit.frame import BlankFrame, scale 11from ..toolkit.frame import BlankFrame, scale
12from .original import Component as Visualizer
11 13
12 14
13log = logging.getLogger("AVP.Component.Life") 15log = logging.getLogger("AVP.Component.Life")
@@ -15,7 +17,7 @@ log = logging.getLogger("AVP.Component.Life")
15 17
16class Component(Component): 18class Component(Component):
17 name = "Conway's Game of Life" 19 name = "Conway's Game of Life"
18 version = "1.0.0" 20 version = "2.0.0"
19 21
20 def widget(self, *args): 22 def widget(self, *args):
21 super().widget(*args) 23 super().widget(*args)
@@ -62,6 +64,8 @@ class Component(Component):
62 "customImg": self.page.checkBox_customImg, 64 "customImg": self.page.checkBox_customImg,
63 "showGrid": self.page.checkBox_showGrid, 65 "showGrid": self.page.checkBox_showGrid,
64 "image": self.page.lineEdit_image, 66 "image": self.page.lineEdit_image,
67 "kaleidoscope": self.page.checkBox_kaleidoscope,
68 "sensitivity": self.page.spinBox_sensitivity,
65 }, 69 },
66 colorWidgets={ 70 colorWidgets={
67 "color": self.page.pushButton_color, 71 "color": self.page.pushButton_color,
@@ -106,6 +110,8 @@ class Component(Component):
106 110
107 def update(self): 111 def update(self):
108 self.updateGridSize() 112 self.updateGridSize()
113
114 # Hide/show widgets depending on state of "custom image" checkbox
109 if self.page.checkBox_customImg.isChecked(): 115 if self.page.checkBox_customImg.isChecked():
110 self.page.label_color.setVisible(False) 116 self.page.label_color.setVisible(False)
111 self.page.lineEdit_color.setVisible(False) 117 self.page.lineEdit_color.setVisible(False)
@@ -124,6 +130,17 @@ class Component(Component):
124 self.page.label_image.setVisible(False) 130 self.page.label_image.setVisible(False)
125 self.page.lineEdit_image.setVisible(False) 131 self.page.lineEdit_image.setVisible(False)
126 self.page.pushButton_pickImage.setVisible(False) 132 self.page.pushButton_pickImage.setVisible(False)
133
134 # Disable audio sensitivity spinbox if not relevant
135 if (
136 self.page.comboBox_shapeType.currentIndex() < 4
137 or self.page.checkBox_customImg.isChecked()
138 ):
139 self.page.spinBox_sensitivity.setEnabled(True)
140 else:
141 self.page.spinBox_sensitivity.setEnabled(False)
142
143 # Disable arrow buttons to shift the grid if the grid is empty
127 enabled = len(self.startingGrid) > 0 144 enabled = len(self.startingGrid) > 0
128 for widget in self.shiftButtons: 145 for widget in self.shiftButtons:
129 widget.setEnabled(enabled) 146 widget.setEnabled(enabled)
@@ -144,16 +161,48 @@ class Component(Component):
144 self.pxHeight = math.ceil(self.height / self.gridHeight) 161 self.pxHeight = math.ceil(self.height / self.gridHeight)
145 162
146 def previewRender(self): 163 def previewRender(self):
147 return self.drawGrid(self.startingGrid) 164 image = self.drawGrid(self.startingGrid, self.color)
165 image = self.addKaleidoscopeEffect(image)
166 image = self.addShadow(image)
167 image = self.addGridLines(image)
168 return image
148 169
149 def preFrameRender(self, *args, **kwargs): 170 def preFrameRender(self, *args, **kwargs):
150 super().preFrameRender(*args, **kwargs) 171 super().preFrameRender(*args, **kwargs)
151 self.tickGrids = {0: self.startingGrid} 172 self.tickGrids = {0: self.startingGrid}
152 173
174 smoothConstantDown = 0.08 + 0
175 smoothConstantUp = 0.8 - 0
176 self.lastSpectrum = None
177 self.spectrumArray = {}
178 if self.sensitivity == 0:
179 return
180
181 for i in range(0, len(self.completeAudioArray), self.sampleSize):
182 if self.canceled:
183 break
184 self.lastSpectrum = Visualizer.transformData(
185 i,
186 self.completeAudioArray,
187 self.sampleSize,
188 smoothConstantDown,
189 smoothConstantUp,
190 self.lastSpectrum,
191 self.sensitivity,
192 )
193 self.spectrumArray[i] = copy(self.lastSpectrum)
194
195 progress = int(100 * (i / len(self.completeAudioArray)))
196 if progress >= 100:
197 progress = 100
198 pStr = "Analyzing audio: " + str(progress) + "%"
199 self.progressBarSetText.emit(pStr)
200 self.progressBarUpdate.emit(int(progress))
201
153 def properties(self): 202 def properties(self):
154 if self.customImg and (not self.image or not os.path.exists(self.image)): 203 if self.customImg and (not self.image or not os.path.exists(self.image)):
155 return ["error"] 204 return ["error"]
156 return [] 205 return ["pcm"] if self.sensitivity > 0 else []
157 206
158 def error(self): 207 def error(self):
159 return "No image selected to represent life." 208 return "No image selected to represent life."
@@ -169,65 +218,181 @@ class Component(Component):
169 # Delete old evolution data which we shouldn't need anymore 218 # Delete old evolution data which we shouldn't need anymore
170 if tick - 60 in self.tickGrids: 219 if tick - 60 in self.tickGrids:
171 del self.tickGrids[tick - 60] 220 del self.tickGrids[tick - 60]
172 return self.drawGrid(grid)
173 221
174 def drawGrid(self, grid): 222 # Fade difference between previous and current grid
223 previousGrid = self.tickGrids.get(tick - 1, set())
224 newColor = self.color
225 if not self.customImg:
226 r, g, b = self.color
227 decay = 255 / self.tickRate
228 decayAmount = int(decay * (frameNo % self.tickRate))
229 decayColor = (
230 r,
231 g,
232 b,
233 255 - decayAmount,
234 )
235 newColor = (r, g, b, min(255, decayAmount * 2))
236 previousGridImage = self.drawGrid(
237 previousGrid,
238 decayColor,
239 (
240 None
241 if (not self.customImg and self.shapeType > 3)
242 or self.sensitivity < 1
243 else self.spectrumArray[frameNo * self.sampleSize]
244 ),
245 )
246 image = self.drawGrid(
247 grid,
248 newColor,
249 (
250 None
251 if (not self.customImg and self.shapeType > 3) or self.sensitivity < 1
252 else self.spectrumArray[frameNo * self.sampleSize]
253 ),
254 grid.intersection(previousGrid),
255 )
256 if not self.customImg:
257 image = Image.alpha_composite(previousGridImage, image)
258 image = self.addKaleidoscopeEffect(image)
259 image = self.addShadow(image)
260 image = self.addGridLines(image)
261 return image
262
263 def addShadow(self, frame):
264 if not self.shadow:
265 return frame
266
267 shadImg = ImageEnhance.Contrast(frame).enhance(0.0)
268 shadImg = shadImg.filter(ImageFilter.GaussianBlur(5.00))
269 shadImg = ImageChops.offset(shadImg, -2, 2)
270 shadImg.paste(frame, box=(0, 0), mask=frame)
271 frame = shadImg
272 return frame
273
274 def addGridLines(self, frame):
275 if not self.showGrid:
276 return frame
277
278 drawer = ImageDraw.Draw(frame)
279 w, h = scale(0.05, self.width, self.height, int)
280 for x in range(self.pxWidth, self.width, self.pxWidth):
281 drawer.rectangle(
282 ((x, 0), (x + w, self.height)),
283 fill=self.color,
284 )
285 for y in range(self.pxHeight, self.height, self.pxHeight):
286 drawer.rectangle(
287 ((0, y), (self.width, y + h)),
288 fill=self.color,
289 )
290 return frame
291
292 def addKaleidoscopeEffect(self, frame):
293 if not self.kaleidoscope:
294 return frame
295
296 flippedImage = frame.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
297 frame.paste(flippedImage, (0, 0), mask=flippedImage)
298
299 flippedImage = frame.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
300 frame.paste(flippedImage, (0, 0), mask=flippedImage)
301
302 flippedImage = frame.transpose(Image.Transpose.ROTATE_90)
303 frame.paste(flippedImage, (0, 0), mask=flippedImage)
304
305 flippedImage = frame.transpose(Image.Transpose.ROTATE_270)
306 frame.paste(flippedImage, (0, 0), mask=flippedImage)
307 return frame
308
309 def drawGrid(self, grid, color, spectrumData=None, didntChange=None):
175 frame = BlankFrame(self.width, self.height) 310 frame = BlankFrame(self.width, self.height)
311 if didntChange is None:
312 # this set would contain cell coords that did not change
313 # between the previous grid tick and this one
314 didntChange = set()
176 315
177 def drawCustomImg(): 316 def drawCustomImg():
178 try: 317 try:
179 img = Image.open(self.image) 318 img = Image.open(self.image)
180 except Exception: 319 except Exception:
181 return 320 return
182 img = img.resize((self.pxWidth, self.pxHeight), Image.Resampling.LANCZOS) 321 img = img.resize(
183 frame.paste(img, box=(drawPtX, drawPtY)) 322 (
323 (self.pxWidth + audioMorphWidth),
324 (self.pxHeight + audioMorphHeight),
325 ),
326 Image.Resampling.LANCZOS,
327 )
328 frame.paste(
329 img,
330 box=(
331 (drawPtX - (audioMorphWidth * 2)),
332 (drawPtY - (audioMorphHeight * 2)),
333 ),
334 )
184 335
185 def drawShape(): 336 def drawShape(x, y):
186 drawer = ImageDraw.Draw(frame) 337 drawer = ImageDraw.Draw(frame)
187 rect = ( 338 rect = (
188 (drawPtX, drawPtY), 339 (drawPtX - audioMorphWidth, drawPtY - audioMorphHeight),
189 (drawPtX + self.pxWidth, drawPtY + self.pxHeight), 340 (
341 drawPtX + self.pxWidth + audioMorphWidth,
342 drawPtY + self.pxHeight + audioMorphHeight,
343 ),
190 ) 344 )
191 shape = self.page.comboBox_shapeType.currentText().lower() 345 shape = self.page.comboBox_shapeType.currentText().lower()
346 thisCellColor = color if (x, y) not in didntChange else (*color[:3], 255)
192 347
193 # Rectangle 348 # Rectangle
194 if shape == "rectangle": 349 if shape == "rectangle":
195 drawer.rectangle(rect, fill=self.color) 350 drawer.rectangle(rect, fill=thisCellColor)
196 351
197 # Elliptical 352 # Elliptical
198 elif shape == "elliptical": 353 elif shape == "elliptical":
199 drawer.ellipse(rect, fill=self.color) 354 drawer.ellipse(rect, fill=thisCellColor)
200 355
201 tenthX, tenthY = scale(10, self.pxWidth, self.pxHeight, int) 356 tenthX, tenthY = scale(10, self.pxWidth, self.pxHeight, int)
202 smallerShape = ( 357 smallerShape = (
203 ( 358 (
204 drawPtX + tenthX + int(tenthX / 4), 359 drawPtX + tenthX + int(tenthX / 4) - int(audioMorphWidth / 2),
205 drawPtY + tenthY + int(tenthY / 2), 360 drawPtY + tenthY + int(tenthY / 2) - int(audioMorphHeight / 2),
206 ), 361 ),
207 ( 362 (
208 drawPtX + self.pxWidth - tenthX - int(tenthX / 4), 363 drawPtX
209 drawPtY + self.pxHeight - (tenthY + int(tenthY / 2)), 364 + self.pxWidth
365 - tenthX
366 - int(tenthX / 4)
367 + int(audioMorphWidth / 2),
368 drawPtY
369 + self.pxHeight
370 - (tenthY + int(tenthY / 2))
371 + int(audioMorphHeight / 2),
210 ), 372 ),
211 ) 373 )
212 outlineShape = ( 374 outlineShape = (
213 (drawPtX + int(tenthX / 4), drawPtY + int(tenthY / 2)),
214 ( 375 (
215 drawPtX + self.pxWidth - int(tenthX / 4), 376 drawPtX + int(tenthX / 4) - audioMorphWidth,
216 drawPtY + self.pxHeight - int(tenthY / 2), 377 drawPtY + int(tenthY / 2) - audioMorphHeight,
378 ),
379 (
380 drawPtX + self.pxWidth - int(tenthX / 4) + audioMorphWidth,
381 drawPtY + self.pxHeight - int(tenthY / 2) + audioMorphHeight,
217 ), 382 ),
218 ) 383 )
219 # Circle 384 # Circle
220 if shape == "circle": 385 if shape == "circle":
221 drawer.ellipse(outlineShape, fill=self.color) 386 drawer.ellipse(outlineShape, fill=thisCellColor)
222 drawer.ellipse(smallerShape, fill=(0, 0, 0, 0)) 387 drawer.ellipse(smallerShape, fill=(0, 0, 0, 0))
223 388
224 # Lilypad 389 # Lilypad
225 elif shape == "lilypad": 390 elif shape == "lilypad":
226 drawer.pieslice(smallerShape, 290, 250, fill=self.color) 391 drawer.pieslice(smallerShape, 290, 250, fill=thisCellColor)
227 392
228 # Pie 393 # Pie
229 elif shape == "pie": 394 elif shape == "pie":
230 drawer.pieslice(outlineShape, 35, 320, fill=self.color) 395 drawer.pieslice(outlineShape, 35, 320, fill=thisCellColor)
231 396
232 hX, hY = scale(50, self.pxWidth, self.pxHeight, int) # halfline 397 hX, hY = scale(50, self.pxWidth, self.pxHeight, int) # halfline
233 tX, tY = scale(33, self.pxWidth, self.pxHeight, int) # thirdline 398 tX, tY = scale(33, self.pxWidth, self.pxHeight, int) # thirdline
@@ -235,7 +400,7 @@ class Component(Component):
235 400
236 # Path 401 # Path
237 if shape == "path": 402 if shape == "path":
238 drawer.ellipse(rect, fill=self.color) 403 drawer.ellipse(rect, fill=thisCellColor)
239 rects = { 404 rects = {
240 direction: False 405 direction: False
241 for direction in ( 406 for direction in (
@@ -287,7 +452,7 @@ class Component(Component):
287 drawPtY + self.pxHeight, 452 drawPtY + self.pxHeight,
288 ), 453 ),
289 ) 454 )
290 drawer.rectangle(sect, fill=self.color) 455 drawer.rectangle(sect, fill=thisCellColor)
291 456
292 # Duck 457 # Duck
293 elif shape == "duck": 458 elif shape == "duck":
@@ -304,10 +469,10 @@ class Component(Component):
304 (drawPtX + int(qX / 4), drawPtY + int(qY * 3)), 469 (drawPtX + int(qX / 4), drawPtY + int(qY * 3)),
305 (drawPtX + int(tX * 2), drawPtY + self.pxHeight), 470 (drawPtX + int(tX * 2), drawPtY + self.pxHeight),
306 ) 471 )
307 drawer.ellipse(duckBody, fill=self.color) 472 drawer.ellipse(duckBody, fill=thisCellColor)
308 drawer.ellipse(duckHead, fill=self.color) 473 drawer.ellipse(duckHead, fill=thisCellColor)
309 drawer.pieslice(duckWing, 130, 200, fill=self.color) 474 drawer.pieslice(duckWing, 130, 200, fill=thisCellColor)
310 drawer.pieslice(duckBeak, 145, 200, fill=self.color) 475 drawer.pieslice(duckBeak, 145, 200, fill=thisCellColor)
311 476
312 # Peace 477 # Peace
313 elif shape == "peace": 478 elif shape == "peace":
@@ -321,9 +486,9 @@ class Component(Component):
321 drawPtY + self.pxHeight - int(tenthY / 2), 486 drawPtY + self.pxHeight - int(tenthY / 2),
322 ), 487 ),
323 ) 488 )
324 drawer.ellipse(outlineShape, fill=self.color) 489 drawer.ellipse(outlineShape, fill=thisCellColor)
325 drawer.ellipse(smallerShape, fill=(0, 0, 0, 0)) 490 drawer.ellipse(smallerShape, fill=(0, 0, 0, 0))
326 drawer.rectangle(line, fill=self.color) 491 drawer.rectangle(line, fill=thisCellColor)
327 492
328 def slantLine(difference): 493 def slantLine(difference):
329 return ( 494 return (
@@ -334,8 +499,10 @@ class Component(Component):
334 (drawPtY + hY), 499 (drawPtY + hY),
335 ) 500 )
336 501
337 drawer.line(slantLine(qX), fill=self.color, width=tenthX) 502 drawer.line(slantLine(qX), fill=thisCellColor, width=tenthX)
338 drawer.line(slantLine(self.pxWidth - qX), fill=self.color, width=tenthX) 503 drawer.line(
504 slantLine(self.pxWidth - qX), fill=thisCellColor, width=tenthX
505 )
339 506
340 for x, y in grid: 507 for x, y in grid:
341 drawPtX = x * self.pxWidth 508 drawPtX = x * self.pxWidth
@@ -345,30 +512,16 @@ class Component(Component):
345 if drawPtY > self.height: 512 if drawPtY > self.height:
346 continue 513 continue
347 514
515 audioMorphWidth = (
516 0 if spectrumData is None else int(spectrumData[(drawPtX % 63) * 4] / 4)
517 )
518 audioMorphHeight = (
519 0 if spectrumData is None else int(spectrumData[(drawPtY % 63) * 4] / 4)
520 )
348 if self.customImg: 521 if self.customImg:
349 drawCustomImg() 522 drawCustomImg()
350 else: 523 else:
351 drawShape() 524 drawShape(x, y)
352
353 if self.shadow:
354 shadImg = ImageEnhance.Contrast(frame).enhance(0.0)
355 shadImg = shadImg.filter(ImageFilter.GaussianBlur(5.00))
356 shadImg = ImageChops.offset(shadImg, -2, 2)
357 shadImg.paste(frame, box=(0, 0), mask=frame)
358 frame = shadImg
359 if self.showGrid:
360 drawer = ImageDraw.Draw(frame)
361 w, h = scale(0.05, self.width, self.height, int)
362 for x in range(self.pxWidth, self.width, self.pxWidth):
363 drawer.rectangle(
364 ((x, 0), (x + w, self.height)),
365 fill=self.color,
366 )
367 for y in range(self.pxHeight, self.height, self.pxHeight):
368 drawer.rectangle(
369 ((0, y), (self.width, y + h)),
370 fill=self.color,
371 )
372 525
373 return frame 526 return frame
374 527
diff --git a/src/avp/components/life.ui b/src/avp/components/life.ui
index 30cf9d0..a0c8999 100644
--- a/src/avp/components/life.ui
+++ b/src/avp/components/life.ui
@@ -7,7 +7,7 @@
7 <x>0</x> 7 <x>0</x>
8 <y>0</y> 8 <y>0</y>
9 <width>586</width> 9 <width>586</width>
10 <height>197</height> 10 <height>206</height>
11 </rect> 11 </rect>
12 </property> 12 </property>
13 <property name="windowTitle"> 13 <property name="windowTitle">
@@ -29,24 +29,27 @@
29 </item> 29 </item>
30 <item> 30 <item>
31 <widget class="QSpinBox" name="spinBox_tickRate"> 31 <widget class="QSpinBox" name="spinBox_tickRate">
32 <property name="toolTip">
33 <string>increase number for slower animation</string>
34 </property>
32 <property name="suffix"> 35 <property name="suffix">
33 <string> frames per tick</string> 36 <string> frames per tick</string>
34 </property> 37 </property>
35 <property name="minimum"> 38 <property name="minimum">
36 <number>1</number> 39 <number>10</number>
37 </property> 40 </property>
38 <property name="maximum"> 41 <property name="maximum">
39 <number>30</number> 42 <number>240</number>
40 </property> 43 </property>
41 <property name="value"> 44 <property name="value">
42 <number>5</number> 45 <number>60</number>
43 </property> 46 </property>
44 </widget> 47 </widget>
45 </item> 48 </item>
46 <item> 49 <item>
47 <spacer name="horizontalSpacer"> 50 <spacer name="horizontalSpacer">
48 <property name="orientation"> 51 <property name="orientation">
49 <enum>Qt::Horizontal</enum> 52 <enum>Qt::Orientation::Horizontal</enum>
50 </property> 53 </property>
51 <property name="sizeHint" stdset="0"> 54 <property name="sizeHint" stdset="0">
52 <size> 55 <size>
@@ -103,7 +106,7 @@
103 <item> 106 <item>
104 <spacer name="horizontalSpacer_5"> 107 <spacer name="horizontalSpacer_5">
105 <property name="orientation"> 108 <property name="orientation">
106 <enum>Qt::Horizontal</enum> 109 <enum>Qt::Orientation::Horizontal</enum>
107 </property> 110 </property>
108 <property name="sizeHint" stdset="0"> 111 <property name="sizeHint" stdset="0">
109 <size> 112 <size>
@@ -194,7 +197,7 @@
194 <item> 197 <item>
195 <spacer name="horizontalSpacer_2"> 198 <spacer name="horizontalSpacer_2">
196 <property name="orientation"> 199 <property name="orientation">
197 <enum>Qt::Horizontal</enum> 200 <enum>Qt::Orientation::Horizontal</enum>
198 </property> 201 </property>
199 <property name="sizeHint" stdset="0"> 202 <property name="sizeHint" stdset="0">
200 <size> 203 <size>
@@ -258,7 +261,7 @@
258 <item> 261 <item>
259 <spacer name="horizontalSpacer_8"> 262 <spacer name="horizontalSpacer_8">
260 <property name="orientation"> 263 <property name="orientation">
261 <enum>Qt::Horizontal</enum> 264 <enum>Qt::Orientation::Horizontal</enum>
262 </property> 265 </property>
263 <property name="sizeHint" stdset="0"> 266 <property name="sizeHint" stdset="0">
264 <size> 267 <size>
@@ -273,10 +276,23 @@
273 <item> 276 <item>
274 <layout class="QHBoxLayout" name="horizontalLayout_6"> 277 <layout class="QHBoxLayout" name="horizontalLayout_6">
275 <item> 278 <item>
279 <widget class="QCheckBox" name="checkBox_kaleidoscope">
280 <property name="text">
281 <string>Kaleidoscope</string>
282 </property>
283 <property name="checked">
284 <bool>true</bool>
285 </property>
286 </widget>
287 </item>
288 <item>
276 <widget class="QCheckBox" name="checkBox_shadow"> 289 <widget class="QCheckBox" name="checkBox_shadow">
277 <property name="text"> 290 <property name="text">
278 <string>Shadow</string> 291 <string>Shadow</string>
279 </property> 292 </property>
293 <property name="checked">
294 <bool>true</bool>
295 </property>
280 </widget> 296 </widget>
281 </item> 297 </item>
282 <item> 298 <item>
@@ -289,7 +305,7 @@
289 <item> 305 <item>
290 <spacer name="horizontalSpacer_6"> 306 <spacer name="horizontalSpacer_6">
291 <property name="orientation"> 307 <property name="orientation">
292 <enum>Qt::Horizontal</enum> 308 <enum>Qt::Orientation::Horizontal</enum>
293 </property> 309 </property>
294 <property name="sizeHint" stdset="0"> 310 <property name="sizeHint" stdset="0">
295 <size> 311 <size>
@@ -309,7 +325,7 @@
309 <string>Up</string> 325 <string>Up</string>
310 </property> 326 </property>
311 <property name="arrowType"> 327 <property name="arrowType">
312 <enum>Qt::UpArrow</enum> 328 <enum>Qt::ArrowType::UpArrow</enum>
313 </property> 329 </property>
314 </widget> 330 </widget>
315 </item> 331 </item>
@@ -319,7 +335,7 @@
319 <string>Down</string> 335 <string>Down</string>
320 </property> 336 </property>
321 <property name="arrowType"> 337 <property name="arrowType">
322 <enum>Qt::DownArrow</enum> 338 <enum>Qt::ArrowType::DownArrow</enum>
323 </property> 339 </property>
324 </widget> 340 </widget>
325 </item> 341 </item>
@@ -329,7 +345,7 @@
329 <string>Left</string> 345 <string>Left</string>
330 </property> 346 </property>
331 <property name="arrowType"> 347 <property name="arrowType">
332 <enum>Qt::LeftArrow</enum> 348 <enum>Qt::ArrowType::LeftArrow</enum>
333 </property> 349 </property>
334 </widget> 350 </widget>
335 </item> 351 </item>
@@ -339,14 +355,14 @@
339 <string>Right</string> 355 <string>Right</string>
340 </property> 356 </property>
341 <property name="arrowType"> 357 <property name="arrowType">
342 <enum>Qt::RightArrow</enum> 358 <enum>Qt::ArrowType::RightArrow</enum>
343 </property> 359 </property>
344 </widget> 360 </widget>
345 </item> 361 </item>
346 <item> 362 <item>
347 <spacer name="horizontalSpacer_9"> 363 <spacer name="horizontalSpacer_9">
348 <property name="orientation"> 364 <property name="orientation">
349 <enum>Qt::Horizontal</enum> 365 <enum>Qt::Orientation::Horizontal</enum>
350 </property> 366 </property>
351 <property name="sizeHint" stdset="0"> 367 <property name="sizeHint" stdset="0">
352 <size> 368 <size>
@@ -356,6 +372,23 @@
356 </property> 372 </property>
357 </spacer> 373 </spacer>
358 </item> 374 </item>
375 <item>
376 <widget class="QLabel" name="label_sensitivity">
377 <property name="text">
378 <string>Audio Sensitivity</string>
379 </property>
380 </widget>
381 </item>
382 <item>
383 <widget class="QSpinBox" name="spinBox_sensitivity">
384 <property name="maximum">
385 <number>40</number>
386 </property>
387 <property name="value">
388 <number>20</number>
389 </property>
390 </widget>
391 </item>
359 </layout> 392 </layout>
360 </item> 393 </item>
361 </layout> 394 </layout>
@@ -364,19 +397,22 @@
364 <widget class="QTextBrowser" name="textBrowser"> 397 <widget class="QTextBrowser" name="textBrowser">
365 <property name="html"> 398 <property name="html">
366 <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt; 399 <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
367&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt; 400&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;meta charset=&quot;utf-8&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
368p, li { white-space: pre-wrap; } 401p, li { white-space: pre-wrap; }
369&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Ubuntu'; font-size:11pt; font-weight:400; font-style:normal;&quot;&gt; 402hr { height: 1px; border-width: 0; }
370&lt;p style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Click the preview window to place a cell. Right-click to remove.&lt;/span&gt;&lt;/p&gt; 403li.unchecked::marker { content: &quot;\2610&quot;; }
371&lt;p style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;- A cell with less than 2 neighbours will die from underpopulation&lt;/p&gt; 404li.checked::marker { content: &quot;\2612&quot;; }
372&lt;p style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;- A cell with more than 3 neighbours will die from overpopulation.&lt;/p&gt; 405&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Noto Sans'; font-size:10pt; font-weight:400; font-style:normal;&quot;&gt;
373&lt;p style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;- An empty space surrounded by 3 live cells will cause reproduction.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> 406&lt;p style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:'Ubuntu'; font-size:11pt; font-weight:600;&quot;&gt;Click the preview window to place a cell. Right-click to remove.&lt;/span&gt;&lt;/p&gt;
407&lt;p style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:'Ubuntu'; font-size:11pt;&quot;&gt;- A cell with less than 2 neighbours will die from underpopulation&lt;/span&gt;&lt;/p&gt;
408&lt;p style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:'Ubuntu'; font-size:11pt;&quot;&gt;- A cell with more than 3 neighbours will die from overpopulation.&lt;/span&gt;&lt;/p&gt;
409&lt;p style=&quot; margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:'Ubuntu'; font-size:11pt;&quot;&gt;- An empty space surrounded by 3 live cells will cause reproduction.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
374 </property> 410 </property>
375 <property name="tabStopDistance"> 411 <property name="tabStopDistance">
376 <number>80</number> 412 <double>80.000000000000000</double>
377 </property> 413 </property>
378 <property name="textInteractionFlags"> 414 <property name="textInteractionFlags">
379 <set>Qt::NoTextInteraction</set> 415 <set>Qt::TextInteractionFlag::NoTextInteraction</set>
380 </property> 416 </property>
381 <property name="openLinks"> 417 <property name="openLinks">
382 <bool>false</bool> 418 <bool>false</bool>
@@ -388,7 +424,7 @@ p, li { white-space: pre-wrap; }
388 <item> 424 <item>
389 <spacer name="verticalSpacer"> 425 <spacer name="verticalSpacer">
390 <property name="orientation"> 426 <property name="orientation">
391 <enum>Qt::Vertical</enum> 427 <enum>Qt::Orientation::Vertical</enum>
392 </property> 428 </property>
393 <property name="sizeHint" stdset="0"> 429 <property name="sizeHint" stdset="0">
394 <size> 430 <size>