shaderflow 0.4.0.dev1__py2.py3-none-any.whl
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.
Potentially problematic release.
This version of shaderflow might be problematic. Click here for more details.
- ShaderFlow/Common/Imgui.py +0 -0
- ShaderFlow/Common/Notes.py +134 -0
- ShaderFlow/Common/__init__.py +0 -0
- ShaderFlow/Message.py +147 -0
- ShaderFlow/Module.py +162 -0
- ShaderFlow/Modules/Audio.py +431 -0
- ShaderFlow/Modules/Bouncing.py +94 -0
- ShaderFlow/Modules/Camera.py +451 -0
- ShaderFlow/Modules/Depth.py +0 -0
- ShaderFlow/Modules/Dynamics.py +288 -0
- ShaderFlow/Modules/Frametimer.py +79 -0
- ShaderFlow/Modules/Keyboard.py +43 -0
- ShaderFlow/Modules/Noise.py +80 -0
- ShaderFlow/Modules/OpticalFlow.py +1 -0
- ShaderFlow/Modules/Piano.py +362 -0
- ShaderFlow/Modules/Spectrogram.py +315 -0
- ShaderFlow/Modules/Video.py +234 -0
- ShaderFlow/Modules/Waveform.py +100 -0
- ShaderFlow/Modules/__init__.py +0 -0
- ShaderFlow/Resources/Images/ShaderFlow.png +0 -0
- ShaderFlow/Resources/Images/ShaderFlow.svg +1 -0
- ShaderFlow/Resources/Scenes/Examples/Demo.py +284 -0
- ShaderFlow/Resources/Scenes/Examples/GLSL/Bars.frag +20 -0
- ShaderFlow/Resources/Scenes/Examples/GLSL/Bouncing.frag +50 -0
- ShaderFlow/Resources/Scenes/Examples/GLSL/Life/Simulation.glsl +49 -0
- ShaderFlow/Resources/Scenes/Examples/GLSL/Life/Visuals.glsl +31 -0
- ShaderFlow/Resources/Scenes/Examples/GLSL/MotionBlur.frag +20 -0
- ShaderFlow/Resources/Scenes/Examples/GLSL/Multipass.frag +43 -0
- ShaderFlow/Resources/Scenes/Examples/GLSL/RayMarch.frag +59 -0
- ShaderFlow/Resources/Scenes/Examples/GLSL/ShaderToy.frag +65 -0
- ShaderFlow/Resources/Scenes/Examples/GLSL/Video.frag +5 -0
- ShaderFlow/Resources/Scenes/Examples/GLSL/Visualizer.frag +75 -0
- ShaderFlow/Resources/Scenes/Examples/GLSL/Waveform.frag +18 -0
- ShaderFlow/Resources/Shaders/Fragment/Default.glsl +65 -0
- ShaderFlow/Resources/Shaders/Fragment/Final.glsl +7 -0
- ShaderFlow/Resources/Shaders/Fragment/Loading.frag +40 -0
- ShaderFlow/Resources/Shaders/Fragment/Missing.glsl +23 -0
- ShaderFlow/Resources/Shaders/Include/Camera.glsl +166 -0
- ShaderFlow/Resources/Shaders/Include/Complex.glsl +0 -0
- ShaderFlow/Resources/Shaders/Include/ShaderFlow.glsl +444 -0
- ShaderFlow/Resources/Shaders/Vertex/Default.glsl +21 -0
- ShaderFlow/Scene.py +1124 -0
- ShaderFlow/Shader.py +391 -0
- ShaderFlow/Texture.py +528 -0
- ShaderFlow/Variable.py +128 -0
- ShaderFlow/__init__.py +11 -0
- ShaderFlow/__main__.py +22 -0
- shaderflow-0.4.0.dev1.dist-info/METADATA +79 -0
- shaderflow-0.4.0.dev1.dist-info/RECORD +51 -0
- shaderflow-0.4.0.dev1.dist-info/WHEEL +5 -0
- shaderflow-0.4.0.dev1.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import time
|
|
3
|
+
import warnings
|
|
4
|
+
from collections import deque
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Deque, Generator, Iterable, List, Optional, Self, Tuple
|
|
7
|
+
|
|
8
|
+
import numpy
|
|
9
|
+
from attr import Factory, define, field
|
|
10
|
+
|
|
11
|
+
from Broken import (
|
|
12
|
+
BrokenEnum,
|
|
13
|
+
BrokenPath,
|
|
14
|
+
BrokenPlatform,
|
|
15
|
+
BrokenThread,
|
|
16
|
+
Ignore,
|
|
17
|
+
log,
|
|
18
|
+
)
|
|
19
|
+
from Broken.Externals.FFmpeg import BrokenAudioReader, BrokenFFmpeg
|
|
20
|
+
from Broken.Types import Channels, Hertz, Samples, Seconds
|
|
21
|
+
from ShaderFlow.Module import ShaderModule
|
|
22
|
+
from ShaderFlow.Modules.Dynamics import ShaderDynamics
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
import soundcard
|
|
26
|
+
except OSError as exception:
|
|
27
|
+
raise ImportError(log.error('\n'.join((
|
|
28
|
+
f"Original ImportError: {exception}\n\n",
|
|
29
|
+
"Couldn't import 'soundcard' library, probably due missing audio shared libraries (libpulse)",
|
|
30
|
+
"• If you're on Linux, consider installing 'pulseaudio' or 'pipewire-pulse' packages",
|
|
31
|
+
"• Shouldn't happen elsewhere, get support at (https://github.com/bastibe/SoundCard)"
|
|
32
|
+
))))
|
|
33
|
+
|
|
34
|
+
# Disable runtime warnings on SoundCard, it's ok to read nothing on Windows
|
|
35
|
+
if BrokenPlatform.OnWindows:
|
|
36
|
+
warnings.filterwarnings("ignore", category=soundcard.SoundcardRuntimeWarning)
|
|
37
|
+
|
|
38
|
+
# -------------------------------------------------------------------------------------------------|
|
|
39
|
+
|
|
40
|
+
def fuzzy_string_search(string: str, choices: List[str], many: int=1, minimum_score: int=0) -> list[tuple[str, int]]:
|
|
41
|
+
"""Fuzzy search a string in a list of strings, returns a list of matches"""
|
|
42
|
+
with warnings.catch_warnings():
|
|
43
|
+
warnings.filterwarnings("ignore")
|
|
44
|
+
import thefuzz.process
|
|
45
|
+
result = thefuzz.process.extract(string, choices, limit=many)
|
|
46
|
+
if many == 1:
|
|
47
|
+
return result[0]
|
|
48
|
+
return result
|
|
49
|
+
|
|
50
|
+
def root_mean_square(data) -> float:
|
|
51
|
+
return numpy.sqrt(numpy.mean(numpy.square(data)))
|
|
52
|
+
|
|
53
|
+
# -------------------------------------------------------------------------------------------------|
|
|
54
|
+
|
|
55
|
+
class BrokenAudioMode(BrokenEnum):
|
|
56
|
+
Realtime = "realtime"
|
|
57
|
+
File = "file"
|
|
58
|
+
|
|
59
|
+
@define(slots=False)
|
|
60
|
+
class BrokenAudio:
|
|
61
|
+
mode: BrokenAudioMode = BrokenAudioMode.Realtime.field()
|
|
62
|
+
dtype: numpy.dtype = numpy.float32
|
|
63
|
+
|
|
64
|
+
data: numpy.ndarray = None
|
|
65
|
+
"""Progressive audio data, shape: (channels, samples)"""
|
|
66
|
+
|
|
67
|
+
tell: int = 0
|
|
68
|
+
"""The number of samples to read from the audio so far"""
|
|
69
|
+
|
|
70
|
+
def __post__(self):
|
|
71
|
+
BrokenThread.new(self._play_thread, daemon=True)
|
|
72
|
+
BrokenThread.new(self._record_thread, daemon=True)
|
|
73
|
+
self.create_buffer()
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def buffer_size(self) -> Samples:
|
|
77
|
+
return int(self.samplerate*self.buffer_seconds)
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def shape(self) -> Tuple[Channels, Samples]:
|
|
81
|
+
return (self.channels, self.buffer_size)
|
|
82
|
+
|
|
83
|
+
def create_buffer(self):
|
|
84
|
+
self.data = numpy.zeros(self.shape, dtype=self.dtype)
|
|
85
|
+
|
|
86
|
+
def add_data(self, data: numpy.ndarray) -> Optional[numpy.ndarray]:
|
|
87
|
+
"""
|
|
88
|
+
Roll the data to the left by the length of the new data; copy new data to the end
|
|
89
|
+
Note: Channel count must match the buffer's one
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
data: The new data of shape: (channels, length)
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
The data that was written, if any
|
|
96
|
+
"""
|
|
97
|
+
if (data := numpy.array(data, dtype=self.dtype)).any():
|
|
98
|
+
length = data.shape[1]
|
|
99
|
+
self.data = numpy.roll(self.data, -length, axis=1)
|
|
100
|
+
self.data[:, -length:] = data
|
|
101
|
+
self.tell += length
|
|
102
|
+
return data
|
|
103
|
+
|
|
104
|
+
def get_data_between_samples(self, start: Samples, end: Samples) -> numpy.ndarray:
|
|
105
|
+
return self.data[:, int(start):int(end)]
|
|
106
|
+
|
|
107
|
+
def get_data_between_seconds(self, start: Seconds, end: Seconds) -> numpy.ndarray:
|
|
108
|
+
return self.get_data_between_samples(start*self.samplerate, end*self.samplerate)
|
|
109
|
+
|
|
110
|
+
def get_last_n_samples(self, n: Samples) -> numpy.ndarray:
|
|
111
|
+
return self.data[:, -int(n):]
|
|
112
|
+
|
|
113
|
+
def get_last_n_seconds(self, n: Seconds) -> numpy.ndarray:
|
|
114
|
+
return self.get_last_n_samples(n*self.samplerate)
|
|
115
|
+
|
|
116
|
+
# ------------------------------------------|
|
|
117
|
+
# Sample Rate
|
|
118
|
+
|
|
119
|
+
_samplerate: Hertz = 44100
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def samplerate(self) -> Hertz:
|
|
123
|
+
"""How many data points per second the audio is sampled at. Defaults to 44100"""
|
|
124
|
+
return self._samplerate or 44100
|
|
125
|
+
|
|
126
|
+
@samplerate.setter
|
|
127
|
+
def samplerate(self, value: Hertz):
|
|
128
|
+
self._samplerate = value
|
|
129
|
+
self.create_buffer()
|
|
130
|
+
|
|
131
|
+
# ------------------------------------------|
|
|
132
|
+
# Channels
|
|
133
|
+
|
|
134
|
+
_channels: int = 2
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def channels(self) -> int:
|
|
138
|
+
"""Number of audio streams (channels). Two is stereo, one is mono. Defaults to 2"""
|
|
139
|
+
return self._channels or 2
|
|
140
|
+
|
|
141
|
+
@channels.setter
|
|
142
|
+
def channels(self, value: int):
|
|
143
|
+
self._channels = value
|
|
144
|
+
self.create_buffer()
|
|
145
|
+
|
|
146
|
+
# ------------------------------------------|
|
|
147
|
+
# History
|
|
148
|
+
|
|
149
|
+
_buffer_seconds: Seconds = 30.0
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def buffer_seconds(self) -> Seconds:
|
|
153
|
+
"""Buffer length in seconds. Cheap on ram and fast, ideally have a decent side"""
|
|
154
|
+
# Note: To convince yourself, (48000 Hz) * (2 Channels) * (30 sec) * (f32=4 bytes) = 11 MB
|
|
155
|
+
return self._buffer_seconds
|
|
156
|
+
|
|
157
|
+
@buffer_seconds.setter
|
|
158
|
+
def buffer_seconds(self, value: Seconds):
|
|
159
|
+
self._buffer_seconds = value
|
|
160
|
+
self.create_buffer()
|
|
161
|
+
|
|
162
|
+
# ------------------------------------------|
|
|
163
|
+
# File
|
|
164
|
+
|
|
165
|
+
_file: Path = None
|
|
166
|
+
_file_reader: BrokenAudioReader = None
|
|
167
|
+
_file_stream: Generator[Tuple[Seconds, numpy.ndarray], None, Seconds] = None
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def file(self) -> Path:
|
|
171
|
+
return self._file
|
|
172
|
+
|
|
173
|
+
@file.setter
|
|
174
|
+
def file(self, value: Path):
|
|
175
|
+
self._file = BrokenPath(value)
|
|
176
|
+
if self._file and not (self._file.exists()):
|
|
177
|
+
return log.minor(f"Audio File doesn't exist ({value})")
|
|
178
|
+
self.samplerate = BrokenFFmpeg.get_audio_samplerate(self.file, echo=False)
|
|
179
|
+
self.channels = BrokenFFmpeg.get_audio_channels(self.file, echo=False)
|
|
180
|
+
self._file_reader = BrokenAudioReader(path=self.file)
|
|
181
|
+
self._file_stream = self._file_reader.stream
|
|
182
|
+
self.mode = BrokenAudioMode.File
|
|
183
|
+
self.close_recorder()
|
|
184
|
+
|
|
185
|
+
# ------------------------------------------|
|
|
186
|
+
# Soundcard
|
|
187
|
+
|
|
188
|
+
recorder_device: Any = None
|
|
189
|
+
recorder: Any = None
|
|
190
|
+
|
|
191
|
+
speaker_device: Any = None
|
|
192
|
+
speaker: Any = None
|
|
193
|
+
|
|
194
|
+
@staticmethod
|
|
195
|
+
def recorders() -> Iterable[Any]:
|
|
196
|
+
yield from soundcard.all_microphones(include_loopback=True)
|
|
197
|
+
|
|
198
|
+
@staticmethod
|
|
199
|
+
def speakers() -> Iterable[Any]:
|
|
200
|
+
yield from soundcard.all_speakers()
|
|
201
|
+
|
|
202
|
+
@staticmethod
|
|
203
|
+
def recorders_names() -> Iterable[str]:
|
|
204
|
+
yield from map(lambda device: device.name, BrokenAudio.recorders())
|
|
205
|
+
|
|
206
|
+
@staticmethod
|
|
207
|
+
def speakers_names() -> Iterable[str]:
|
|
208
|
+
yield from map(lambda device: device.name, BrokenAudio.speakers())
|
|
209
|
+
|
|
210
|
+
def list_recorders(self) -> None:
|
|
211
|
+
"""List and print all available Audio recording devices"""
|
|
212
|
+
log.info("Recording Devices:")
|
|
213
|
+
for i, device in enumerate(BrokenAudio.recorders()):
|
|
214
|
+
log.info(f"• ({i:2d}) Recorder: '{device.name}'")
|
|
215
|
+
|
|
216
|
+
def list_speakers(self) -> None:
|
|
217
|
+
"""List and print all available Audio playback devices"""
|
|
218
|
+
log.info("Playback Devices:")
|
|
219
|
+
for i, device in enumerate(BrokenAudio.speakers()):
|
|
220
|
+
log.info(f"• ({i:2d}) Speaker: '{device.name}'")
|
|
221
|
+
|
|
222
|
+
def __fuzzy__(self, name: str, devices: Iterable[str]) -> Optional[str]:
|
|
223
|
+
device_name = fuzzy_string_search(name, devices)[0]
|
|
224
|
+
return next(filter(lambda x: x.name == device_name, devices), None)
|
|
225
|
+
|
|
226
|
+
def open_speaker(self,
|
|
227
|
+
name: str=None,
|
|
228
|
+
*,
|
|
229
|
+
samplerate: Hertz=None,
|
|
230
|
+
) -> Self:
|
|
231
|
+
"""
|
|
232
|
+
Open a SoundCard device for playing real-time audio.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
name: The name of the device to open. If None, the default speaker is used. The search
|
|
236
|
+
is fuzzy, so the match does not need to be exact
|
|
237
|
+
|
|
238
|
+
samplerate: If None, gets self.samplerate
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
Self, Fluent interface
|
|
242
|
+
"""
|
|
243
|
+
(self.speaker or Ignore()).__exit__(None, None, None)
|
|
244
|
+
|
|
245
|
+
# Search for the Speaker
|
|
246
|
+
if name is None:
|
|
247
|
+
self.speaker_device = soundcard.default_speaker()
|
|
248
|
+
else:
|
|
249
|
+
self.speaker_device = self.__fuzzy__(name, self.speakers_names)
|
|
250
|
+
|
|
251
|
+
# Open the speaker
|
|
252
|
+
log.info(f"Opening Speaker with Device ({self.speaker_device})")
|
|
253
|
+
self.speaker = self.speaker_device.player(
|
|
254
|
+
samplerate=samplerate or self.samplerate,
|
|
255
|
+
).__enter__()
|
|
256
|
+
return self
|
|
257
|
+
|
|
258
|
+
def close_speaker(self) -> Self:
|
|
259
|
+
(self.speaker or Ignore()).__exit__(None, None, None)
|
|
260
|
+
self.speaker = None
|
|
261
|
+
return self
|
|
262
|
+
|
|
263
|
+
def open_recorder(self,
|
|
264
|
+
name: str=None,
|
|
265
|
+
*,
|
|
266
|
+
samplerate: Hertz=44100,
|
|
267
|
+
channels: List[int]=None,
|
|
268
|
+
blocksize: int=512,
|
|
269
|
+
) -> Self:
|
|
270
|
+
"""
|
|
271
|
+
Open a SoundCard device for recording real-time audio. Specifics implementation adapted
|
|
272
|
+
from the `soundcard` library Source Code (docstring only)
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
name: The name of the device to open. If None, the first loopback device or default
|
|
276
|
+
microphone is used. The search is fuzzy, so the match does not need to be exact
|
|
277
|
+
|
|
278
|
+
samplerate: The desired sample rate of the audio
|
|
279
|
+
|
|
280
|
+
channels: Channels to read from the device.
|
|
281
|
+
• None: Record all available channels
|
|
282
|
+
• List[int]: Record only the specified channels
|
|
283
|
+
• -1: (Linux: Mono mix of all channels) (MacOS: Silence)
|
|
284
|
+
|
|
285
|
+
blocksize: Desired minimum latency in samples, and also the number of recorded
|
|
286
|
+
samples at a time. Lower values reduces latency and increases CPU usage, which
|
|
287
|
+
funnily enough might cause latency issues
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
Self, Fluent interface
|
|
291
|
+
"""
|
|
292
|
+
self.close_recorder()
|
|
293
|
+
|
|
294
|
+
# Search for default loopback device
|
|
295
|
+
if name is None:
|
|
296
|
+
for device in self.recorders():
|
|
297
|
+
if device.isloopback:
|
|
298
|
+
self.recorder_device = device
|
|
299
|
+
break
|
|
300
|
+
self.recorder_device = (self.recorder_device or soundcard.default_microphone())
|
|
301
|
+
else:
|
|
302
|
+
self.recorder_device = self.__fuzzy__(name, self.recorders_names())
|
|
303
|
+
|
|
304
|
+
# Open the recorder
|
|
305
|
+
log.info(f"Opening Recorder with Device ({self.recorder_device})")
|
|
306
|
+
self.recorder = self.recorder_device.recorder(
|
|
307
|
+
samplerate=samplerate,
|
|
308
|
+
channels=channels,
|
|
309
|
+
blocksize=blocksize,
|
|
310
|
+
).__enter__()
|
|
311
|
+
|
|
312
|
+
# Update properties
|
|
313
|
+
self.samplerate = getattr(self.recorder, "_samplerate", samplerate)
|
|
314
|
+
self.channels = self.recorder_device.channels
|
|
315
|
+
self.mode = BrokenAudioMode.Realtime
|
|
316
|
+
return self
|
|
317
|
+
|
|
318
|
+
def close_recorder(self) -> Self:
|
|
319
|
+
(self.recorder or Ignore()).__exit__(None, None, None)
|
|
320
|
+
self.recorder = None
|
|
321
|
+
return self
|
|
322
|
+
|
|
323
|
+
def record(self, numframes: int=None) -> Optional[numpy.ndarray]:
|
|
324
|
+
"""Record a number of samples from the recorder. 'None' records all"""
|
|
325
|
+
if not self.recorder:
|
|
326
|
+
return None
|
|
327
|
+
return self.add_data(self.recorder.record(numframes=numframes).T)
|
|
328
|
+
|
|
329
|
+
def _record_thread(self) -> None:
|
|
330
|
+
while True:
|
|
331
|
+
if (self.record() is None):
|
|
332
|
+
time.sleep(0.01)
|
|
333
|
+
|
|
334
|
+
# # Playing
|
|
335
|
+
|
|
336
|
+
_play_queue: Deque[numpy.ndarray] = Factory(deque)
|
|
337
|
+
|
|
338
|
+
def play(self, data: numpy.ndarray) -> None:
|
|
339
|
+
"""Add a numpy array to the play queue. for non-blocking playback"""
|
|
340
|
+
if not self.speaker_device:
|
|
341
|
+
return None
|
|
342
|
+
self._play_queue.append(data)
|
|
343
|
+
|
|
344
|
+
def _play_thread(self) -> None:
|
|
345
|
+
while True:
|
|
346
|
+
if (self._play_queue and self.speaker):
|
|
347
|
+
self.speaker.play(self._play_queue.popleft().T)
|
|
348
|
+
continue
|
|
349
|
+
time.sleep(0.01)
|
|
350
|
+
|
|
351
|
+
# ------------------------------------------|
|
|
352
|
+
# Properties utils
|
|
353
|
+
|
|
354
|
+
@property
|
|
355
|
+
def stereo(self) -> bool:
|
|
356
|
+
"""Is this Audio object stereo?"""
|
|
357
|
+
return (self.channels == 2)
|
|
358
|
+
|
|
359
|
+
@property
|
|
360
|
+
def mono(self) -> bool:
|
|
361
|
+
"""Is this Audio object mono?"""
|
|
362
|
+
return (self.channels == 1)
|
|
363
|
+
|
|
364
|
+
@property
|
|
365
|
+
def duration(self) -> Seconds:
|
|
366
|
+
if self.mode == BrokenAudioMode.Realtime:
|
|
367
|
+
return math.inf
|
|
368
|
+
if self.mode == BrokenAudioMode.File:
|
|
369
|
+
return BrokenFFmpeg.get_audio_duration(self.file)
|
|
370
|
+
|
|
371
|
+
# -------------------------------------------------------------------------------------------------|
|
|
372
|
+
|
|
373
|
+
@define
|
|
374
|
+
class ShaderAudio(BrokenAudio, ShaderModule):
|
|
375
|
+
|
|
376
|
+
# Todo: Move to a ShaderAudioProcessing class
|
|
377
|
+
volume: ShaderDynamics = None
|
|
378
|
+
std: ShaderDynamics = None
|
|
379
|
+
final: bool = True
|
|
380
|
+
|
|
381
|
+
def __post__(self):
|
|
382
|
+
self.volume = ShaderDynamics(
|
|
383
|
+
scene=self.scene, name=f"{self.name}Volume",
|
|
384
|
+
frequency=2, zeta=1, response=0, value=0
|
|
385
|
+
)
|
|
386
|
+
self.std = ShaderDynamics(
|
|
387
|
+
scene=self.scene, name=f"{self.name}STD",
|
|
388
|
+
frequency=10, zeta=1, response=0, value=0
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
def commands(self):
|
|
392
|
+
return
|
|
393
|
+
|
|
394
|
+
# Common commands
|
|
395
|
+
self.scene.typer.command(self.list_recorders, panel=self.__class__.__name__)
|
|
396
|
+
self.scene.typer.command(self.list_speakers, panel=self.__class__.__name__)
|
|
397
|
+
|
|
398
|
+
# Proper commands
|
|
399
|
+
self.scene.typer.command(self.open_recorder, name=f"{self.name}-recorder", panel=f"{self.__class__.__name__}: {self.name}")
|
|
400
|
+
self.scene.typer.command(self.open_speaker, name=f"{self.name}-speaker", panel=f"{self.__class__.__name__}: {self.name}")
|
|
401
|
+
|
|
402
|
+
@property
|
|
403
|
+
def duration(self) -> Seconds:
|
|
404
|
+
return BrokenFFmpeg.get_audio_duration(self.file) or self.scene.duration
|
|
405
|
+
|
|
406
|
+
def setup(self):
|
|
407
|
+
self.file = self.file
|
|
408
|
+
if (self.final and self.scene.realtime):
|
|
409
|
+
if (self.mode == BrokenAudioMode.File):
|
|
410
|
+
self.open_speaker()
|
|
411
|
+
else:
|
|
412
|
+
self.open_recorder()
|
|
413
|
+
|
|
414
|
+
def ffmpeg(self, ffmpeg: BrokenFFmpeg) -> None:
|
|
415
|
+
if BrokenPath(self.file, valid=True):
|
|
416
|
+
ffmpeg.input(path=self.file)
|
|
417
|
+
|
|
418
|
+
def update(self):
|
|
419
|
+
try:
|
|
420
|
+
if self._file_stream:
|
|
421
|
+
self._file_reader.chunk = self.scene.rdt
|
|
422
|
+
data = next(self._file_stream).T
|
|
423
|
+
self.add_data(data)
|
|
424
|
+
self.play(data)
|
|
425
|
+
except StopIteration:
|
|
426
|
+
pass
|
|
427
|
+
|
|
428
|
+
self.volume.target = 2 * root_mean_square(self.get_last_n_seconds(0.1)) * (2**0.5)
|
|
429
|
+
self.std.target = numpy.std(self.get_last_n_seconds(0.1))
|
|
430
|
+
|
|
431
|
+
# -------------------------------------------------------------------------------------------------|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import random
|
|
2
|
+
from math import cos, sin
|
|
3
|
+
from typing import Iterable
|
|
4
|
+
|
|
5
|
+
import numpy
|
|
6
|
+
from attr import define
|
|
7
|
+
|
|
8
|
+
from Broken import clamp
|
|
9
|
+
from Broken.Loaders import LoadableImage, LoaderImage
|
|
10
|
+
from Broken.Types import TAU, Degrees
|
|
11
|
+
from ShaderFlow.Module import ShaderModule
|
|
12
|
+
from ShaderFlow.Texture import ShaderTexture
|
|
13
|
+
from ShaderFlow.Variable import ShaderVariable
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@define
|
|
17
|
+
class ShaderBouncing(ShaderModule):
|
|
18
|
+
name: str = "iBounce"
|
|
19
|
+
position: numpy.ndarray = None
|
|
20
|
+
velocity: numpy.ndarray = None
|
|
21
|
+
aspect_ratio: float = 1
|
|
22
|
+
|
|
23
|
+
def setup(self):
|
|
24
|
+
self.set_velocity_polar(1, random.uniform(0, TAU))
|
|
25
|
+
self.position = numpy.array((0.0, 0.0))
|
|
26
|
+
|
|
27
|
+
def update(self):
|
|
28
|
+
self.position += (self.velocity * self.scene.dt)
|
|
29
|
+
|
|
30
|
+
for i, limit in enumerate((self.aspect_ratio, 1)):
|
|
31
|
+
if abs(self.position[i]) > limit:
|
|
32
|
+
self.velocity[i] = -self.velocity[i]
|
|
33
|
+
self.position[i] = clamp(self.position[i], -limit, limit)
|
|
34
|
+
|
|
35
|
+
def pipeline(self) -> Iterable[ShaderVariable]:
|
|
36
|
+
yield ShaderVariable("uniform", "vec2", f"{self.name}Position", self.position)
|
|
37
|
+
yield ShaderVariable("uniform", "vec2", f"{self.name}Velocity", self.velocity)
|
|
38
|
+
|
|
39
|
+
# # Quality of Life
|
|
40
|
+
|
|
41
|
+
def set_velocity_polar(self, magnitude: float, angle: Degrees):
|
|
42
|
+
self.velocity = magnitude*numpy.array((cos(angle), sin(angle)))
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def x(self) -> float:
|
|
46
|
+
return self.position[0]
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def y(self) -> float:
|
|
50
|
+
return self.position[1]
|
|
51
|
+
|
|
52
|
+
@x.setter
|
|
53
|
+
def x(self, value: float):
|
|
54
|
+
self.position[0] = value
|
|
55
|
+
|
|
56
|
+
@y.setter
|
|
57
|
+
def y(self, value: float):
|
|
58
|
+
self.position[1] = value
|
|
59
|
+
|
|
60
|
+
# # Advanced
|
|
61
|
+
|
|
62
|
+
def advanced_ratios(self, image: LoadableImage, steps: int=1000) -> ShaderTexture:
|
|
63
|
+
"""Get a texture of `aspect_ratio(angle)` from linspace(0, tau, steps)"""
|
|
64
|
+
ratios = numpy.zeros((steps, 1), dtype=numpy.float32)
|
|
65
|
+
image = numpy.array(LoaderImage(image))
|
|
66
|
+
width, height, _ = image.shape
|
|
67
|
+
bigger = max(width, height)
|
|
68
|
+
|
|
69
|
+
import cv2
|
|
70
|
+
|
|
71
|
+
# image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
|
72
|
+
image = image[:, :, 3]
|
|
73
|
+
|
|
74
|
+
# Make anew image with a centered raw copy
|
|
75
|
+
square = numpy.zeros((bigger, bigger), dtype=numpy.uint8)
|
|
76
|
+
x, y = (bigger-width)//2, (bigger-height)//2
|
|
77
|
+
square[x:x+width, y:y+height] = image
|
|
78
|
+
|
|
79
|
+
# Rotate the image and find its alpha content bounding box
|
|
80
|
+
for i, angle in enumerate(numpy.linspace(0, 360, steps)):
|
|
81
|
+
rotation = cv2.getRotationMatrix2D((bigger/2, bigger/2), angle, 1)
|
|
82
|
+
rotated = cv2.warpAffine(square, rotation, (bigger, bigger))
|
|
83
|
+
thresh = cv2.threshold(rotated, 4, 255, cv2.THRESH_BINARY)[1]
|
|
84
|
+
contours = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0]
|
|
85
|
+
_, _, w, h = cv2.boundingRect(max(contours, key=cv2.contourArea))
|
|
86
|
+
ratios[i][0] = (w/h)
|
|
87
|
+
|
|
88
|
+
return ShaderTexture(
|
|
89
|
+
name=f"{self.name}AspectRatio",
|
|
90
|
+
scene=self.scene,
|
|
91
|
+
components=1,
|
|
92
|
+
width=steps,
|
|
93
|
+
height=1,
|
|
94
|
+
).from_numpy(ratios)
|