sonolus.py 0.1.3__py3-none-any.whl → 0.1.5__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 sonolus.py might be problematic. Click here for more details.

Files changed (90) hide show
  1. sonolus/backend/blocks.py +756 -756
  2. sonolus/backend/excepthook.py +37 -37
  3. sonolus/backend/finalize.py +77 -69
  4. sonolus/backend/interpret.py +7 -7
  5. sonolus/backend/ir.py +29 -3
  6. sonolus/backend/mode.py +24 -24
  7. sonolus/backend/node.py +40 -40
  8. sonolus/backend/ops.py +197 -197
  9. sonolus/backend/optimize/__init__.py +0 -0
  10. sonolus/backend/optimize/allocate.py +126 -0
  11. sonolus/backend/optimize/constant_evaluation.py +374 -0
  12. sonolus/backend/optimize/copy_coalesce.py +85 -0
  13. sonolus/backend/optimize/dead_code.py +185 -0
  14. sonolus/backend/optimize/dominance.py +96 -0
  15. sonolus/backend/{flow.py → optimize/flow.py} +122 -92
  16. sonolus/backend/optimize/inlining.py +137 -0
  17. sonolus/backend/optimize/liveness.py +177 -0
  18. sonolus/backend/optimize/optimize.py +44 -0
  19. sonolus/backend/optimize/passes.py +52 -0
  20. sonolus/backend/optimize/simplify.py +191 -0
  21. sonolus/backend/optimize/ssa.py +200 -0
  22. sonolus/backend/place.py +17 -25
  23. sonolus/backend/utils.py +58 -48
  24. sonolus/backend/visitor.py +1151 -882
  25. sonolus/build/cli.py +7 -1
  26. sonolus/build/compile.py +88 -90
  27. sonolus/build/engine.py +10 -5
  28. sonolus/build/level.py +24 -23
  29. sonolus/build/node.py +43 -43
  30. sonolus/script/archetype.py +438 -139
  31. sonolus/script/array.py +27 -10
  32. sonolus/script/array_like.py +297 -0
  33. sonolus/script/bucket.py +253 -191
  34. sonolus/script/containers.py +257 -51
  35. sonolus/script/debug.py +26 -10
  36. sonolus/script/easing.py +365 -0
  37. sonolus/script/effect.py +191 -131
  38. sonolus/script/engine.py +71 -4
  39. sonolus/script/globals.py +303 -269
  40. sonolus/script/instruction.py +205 -151
  41. sonolus/script/internal/__init__.py +5 -5
  42. sonolus/script/internal/builtin_impls.py +255 -144
  43. sonolus/script/{callbacks.py → internal/callbacks.py} +127 -127
  44. sonolus/script/internal/constant.py +139 -0
  45. sonolus/script/internal/context.py +26 -9
  46. sonolus/script/internal/descriptor.py +17 -17
  47. sonolus/script/internal/dict_impl.py +65 -0
  48. sonolus/script/internal/generic.py +6 -9
  49. sonolus/script/internal/impl.py +38 -13
  50. sonolus/script/internal/introspection.py +17 -14
  51. sonolus/script/internal/math_impls.py +121 -0
  52. sonolus/script/internal/native.py +40 -38
  53. sonolus/script/internal/random.py +67 -0
  54. sonolus/script/internal/range.py +81 -0
  55. sonolus/script/internal/transient.py +51 -0
  56. sonolus/script/internal/tuple_impl.py +113 -0
  57. sonolus/script/internal/value.py +3 -3
  58. sonolus/script/interval.py +338 -112
  59. sonolus/script/iterator.py +167 -214
  60. sonolus/script/level.py +24 -0
  61. sonolus/script/num.py +80 -48
  62. sonolus/script/options.py +257 -191
  63. sonolus/script/particle.py +190 -157
  64. sonolus/script/pointer.py +30 -30
  65. sonolus/script/print.py +102 -81
  66. sonolus/script/project.py +8 -0
  67. sonolus/script/quad.py +263 -0
  68. sonolus/script/record.py +47 -16
  69. sonolus/script/runtime.py +52 -1
  70. sonolus/script/sprite.py +418 -333
  71. sonolus/script/text.py +409 -407
  72. sonolus/script/timing.py +114 -42
  73. sonolus/script/transform.py +332 -48
  74. sonolus/script/ui.py +216 -160
  75. sonolus/script/values.py +6 -13
  76. sonolus/script/vec.py +196 -78
  77. {sonolus_py-0.1.3.dist-info → sonolus_py-0.1.5.dist-info}/METADATA +1 -1
  78. sonolus_py-0.1.5.dist-info/RECORD +89 -0
  79. {sonolus_py-0.1.3.dist-info → sonolus_py-0.1.5.dist-info}/WHEEL +1 -1
  80. {sonolus_py-0.1.3.dist-info → sonolus_py-0.1.5.dist-info}/licenses/LICENSE +21 -21
  81. sonolus/backend/allocate.py +0 -51
  82. sonolus/backend/optimize.py +0 -9
  83. sonolus/backend/passes.py +0 -6
  84. sonolus/backend/simplify.py +0 -30
  85. sonolus/script/comptime.py +0 -160
  86. sonolus/script/graphics.py +0 -150
  87. sonolus/script/math.py +0 -92
  88. sonolus/script/range.py +0 -58
  89. sonolus_py-0.1.3.dist-info/RECORD +0 -75
  90. {sonolus_py-0.1.3.dist-info → sonolus_py-0.1.5.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,365 @@
1
+ # ruff: noqa: E501
2
+ import math
3
+
4
+ from sonolus.backend.ops import Op
5
+ from sonolus.script.internal.native import native_function
6
+ from sonolus.script.interval import clamp
7
+
8
+
9
+ @native_function(Op.EaseInBack)
10
+ def ease_in_back(x: float) -> float:
11
+ """Interpolate between 0 and 1, starting slow and ending fast, overshooting below 0 at the start."""
12
+ x = clamp(x, 0, 1)
13
+ c1 = 1.70158
14
+ c3 = c1 + 1
15
+ return c3 * x**3 - c1 * x**2
16
+
17
+
18
+ @native_function(Op.EaseOutBack)
19
+ def ease_out_back(x: float) -> float:
20
+ """Interpolate between 0 and 1, starting fast and ending slow, overshooting above 1 at the end."""
21
+ x = clamp(x, 0, 1)
22
+ c1 = 1.70158
23
+ c3 = c1 + 1
24
+ return 1 + c3 * (x - 1) ** 3 + c1 * (x - 1) ** 2
25
+
26
+
27
+ @native_function(Op.EaseInOutBack)
28
+ def ease_in_out_back(x: float) -> float:
29
+ """Interpolate between 0 and 1, starting and ending slow with overshooting, fast in the middle."""
30
+ x = clamp(x, 0, 1)
31
+ c1 = 1.70158
32
+ c2 = c1 * 1.525
33
+ if x < 0.5:
34
+ return ((2 * x) ** 2 * ((c2 + 1) * 2 * x - c2)) / 2
35
+ else:
36
+ return ((2 * x - 2) ** 2 * ((c2 + 1) * (2 * x - 2) + c2) + 2) / 2
37
+
38
+
39
+ @native_function(Op.EaseOutInBack)
40
+ def ease_out_in_back(x: float) -> float:
41
+ """Interpolate between 0 and 1, fast at the start and end, slow in the middle with overshooting."""
42
+ x = clamp(x, 0, 1)
43
+ c1 = 1.70158
44
+ c3 = c1 + 1
45
+ if x < 0.5:
46
+ return (1 + c3 * (2 * x - 1) ** 3 + c1 * (2 * x - 1) ** 2) / 2
47
+ else:
48
+ return (c3 * (2 * x - 1) ** 3 - c1 * (2 * x - 1) ** 2) / 2 + 0.5
49
+
50
+
51
+ @native_function(Op.EaseInCirc)
52
+ def ease_in_circ(x: float) -> float:
53
+ """Interpolate between 0 and 1, starting slow and ending very fast."""
54
+ x = clamp(x, 0, 1)
55
+ return 1 - math.sqrt(1 - x**2)
56
+
57
+
58
+ @native_function(Op.EaseOutCirc)
59
+ def ease_out_circ(x: float) -> float:
60
+ """Interpolate between 0 and 1, starting very fast and ending slow."""
61
+ x = clamp(x, 0, 1)
62
+ return math.sqrt(1 - (x - 1) ** 2)
63
+
64
+
65
+ @native_function(Op.EaseInOutCirc)
66
+ def ease_in_out_circ(x: float) -> float:
67
+ """Interpolate between 0 and 1, starting and ending slow, very fast in the middle."""
68
+ x = clamp(x, 0, 1)
69
+ if x < 0.5:
70
+ return (1 - math.sqrt(1 - (2 * x) ** 2)) / 2
71
+ else:
72
+ return (math.sqrt(1 - (2 * x - 2) ** 2) + 1) / 2
73
+
74
+
75
+ @native_function(Op.EaseOutInCirc)
76
+ def ease_out_in_circ(x: float) -> float:
77
+ """Interpolate between 0 and 1, very fast at the start and end, slow in the middle."""
78
+ x = clamp(x, 0, 1)
79
+ if x < 0.5:
80
+ return math.sqrt(1 - (2 * x - 1) ** 2) / 2
81
+ else:
82
+ return (1 - math.sqrt(1 - (2 * x - 1) ** 2)) / 2 + 0.5
83
+
84
+
85
+ @native_function(Op.EaseInCubic)
86
+ def ease_in_cubic(x: float) -> float:
87
+ """Interpolate between 0 and 1, starting slow and ending fast with cubic easing."""
88
+ x = clamp(x, 0, 1)
89
+ return x**3
90
+
91
+
92
+ @native_function(Op.EaseOutCubic)
93
+ def ease_out_cubic(x: float) -> float:
94
+ """Interpolate between 0 and 1, starting fast and ending slow with cubic easing."""
95
+ x = clamp(x, 0, 1)
96
+ return 1 - (1 - x) ** 3
97
+
98
+
99
+ @native_function(Op.EaseInOutCubic)
100
+ def ease_in_out_cubic(x: float) -> float:
101
+ """Interpolate between 0 and 1, starting and ending slow with cubic easing, fast in the middle."""
102
+ x = clamp(x, 0, 1)
103
+ if x < 0.5:
104
+ return 4 * x**3
105
+ else:
106
+ return 1 - (-2 * x + 2) ** 3 / 2
107
+
108
+
109
+ @native_function(Op.EaseOutInCubic)
110
+ def ease_out_in_cubic(x: float) -> float:
111
+ """Interpolate between 0 and 1, fast at the start and end, slow in the middle with cubic easing."""
112
+ x = clamp(x, 0, 1)
113
+ if x < 0.5:
114
+ return (1 - (1 - 2 * x) ** 3) / 2
115
+ else:
116
+ return ((2 * x - 1) ** 3) / 2 + 0.5
117
+
118
+
119
+ @native_function(Op.EaseInElastic)
120
+ def ease_in_elastic(x: float) -> float:
121
+ """Interpolate between 0 and 1 with oscillations, starting slow and ending fast."""
122
+ x = clamp(x, 0, 1)
123
+ c4 = (2 * math.pi) / 3
124
+ if x in {0, 1}:
125
+ return x
126
+ else:
127
+ return -(2 ** (10 * x - 10)) * math.sin((x * 10 - 10.75) * c4)
128
+
129
+
130
+ @native_function(Op.EaseOutElastic)
131
+ def ease_out_elastic(x: float) -> float:
132
+ """Interpolate between 0 and 1 with oscillations, starting fast and ending slow."""
133
+ x = clamp(x, 0, 1)
134
+ c4 = (2 * math.pi) / 3
135
+ if x in {0, 1}:
136
+ return x
137
+ else:
138
+ return 2 ** (-10 * x) * math.sin((x * 10 - 0.75) * c4) + 1
139
+
140
+
141
+ @native_function(Op.EaseInOutElastic)
142
+ def ease_in_out_elastic(x: float) -> float:
143
+ """Interpolate between 0 and 1 with oscillations, slow at the start and end, fast in the middle."""
144
+ x = clamp(x, 0, 1)
145
+ c5 = (2 * math.pi) / 4.5
146
+ if x in {0, 1}:
147
+ return x
148
+ elif x < 0.5:
149
+ return -(2 ** (20 * x - 10) * math.sin((20 * x - 11.125) * c5)) / 2
150
+ else:
151
+ return (2 ** (-20 * x + 10) * math.sin((20 * x - 11.125) * c5)) / 2 + 1
152
+
153
+
154
+ @native_function(Op.EaseOutInElastic)
155
+ def ease_out_in_elastic(x: float) -> float:
156
+ """Interpolate between 0 and 1 with oscillations, fast at the start and end, slow in the middle."""
157
+ x = clamp(x, 0, 1)
158
+ c4 = (2 * math.pi) / 3
159
+ if x < 0.5:
160
+ if x == 0:
161
+ return 0
162
+ else:
163
+ return (2 ** (-20 * x + 10) * math.sin((20 * x - 0.75) * c4)) / 2 + 0.5
164
+ elif x == 1:
165
+ return 1
166
+ else:
167
+ return (-(2 ** (10 * (2 * x - 1) - 10)) * math.sin((20 * x - 10.75) * c4)) / 2 + 0.5
168
+
169
+
170
+ @native_function(Op.EaseInExpo)
171
+ def ease_in_expo(x: float) -> float:
172
+ """Interpolate between 0 and 1, starting extremely slow and ending extremely fast."""
173
+ x = clamp(x, 0, 1)
174
+ return 0 if x == 0 else 2 ** (10 * x - 10)
175
+
176
+
177
+ @native_function(Op.EaseOutExpo)
178
+ def ease_out_expo(x: float) -> float:
179
+ """Interpolate between 0 and 1, starting extremely fast and ending extremely slow."""
180
+ x = clamp(x, 0, 1)
181
+ return 1 if x == 1 else 1 - 2 ** (-10 * x)
182
+
183
+
184
+ @native_function(Op.EaseInOutExpo)
185
+ def ease_in_out_expo(x: float) -> float:
186
+ """Interpolate between 0 and 1, starting and ending extremely slow, fast in the middle."""
187
+ x = clamp(x, 0, 1)
188
+ if x in {0, 1}:
189
+ return x
190
+ elif x < 0.5:
191
+ return 2 ** (20 * x - 10) / 2
192
+ else:
193
+ return (2 - 2 ** (-20 * x + 10)) / 2
194
+
195
+
196
+ @native_function(Op.EaseOutInExpo)
197
+ def ease_out_in_expo(x: float) -> float:
198
+ """Interpolate between 0 and 1, extremely fast at the start and end, extremely slow in the middle."""
199
+ x = clamp(x, 0, 1)
200
+ if x in {0, 1}:
201
+ return x
202
+ elif x < 0.5:
203
+ return (1 - 2 ** (-20 * x)) / 2
204
+ else:
205
+ return (2 ** (20 * x - 20)) / 2 + 0.5
206
+
207
+
208
+ @native_function(Op.EaseInQuad)
209
+ def ease_in_quad(x: float) -> float:
210
+ """Interpolate between 0 and 1, starting slow and ending fast with quadratic easing."""
211
+ x = clamp(x, 0, 1)
212
+ return x**2
213
+
214
+
215
+ @native_function(Op.EaseOutQuad)
216
+ def ease_out_quad(x: float) -> float:
217
+ """Interpolate between 0 and 1, starting fast and ending slow with quadratic easing."""
218
+ x = clamp(x, 0, 1)
219
+ return 1 - (1 - x) ** 2
220
+
221
+
222
+ @native_function(Op.EaseInOutQuad)
223
+ def ease_in_out_quad(x: float) -> float:
224
+ """Interpolate between 0 and 1, starting and ending slow with quadratic easing, fast in the middle."""
225
+ x = clamp(x, 0, 1)
226
+ if x < 0.5:
227
+ return 2 * x**2
228
+ else:
229
+ return 1 - (-2 * x + 2) ** 2 / 2
230
+
231
+
232
+ @native_function(Op.EaseOutInQuad)
233
+ def ease_out_in_quad(x: float) -> float:
234
+ """Interpolate between 0 and 1, fast at the start and end, slow in the middle with quadratic easing."""
235
+ x = clamp(x, 0, 1)
236
+ if x < 0.5:
237
+ return (1 - (1 - 2 * x) ** 2) / 2
238
+ else:
239
+ return ((2 * x - 1) ** 2) / 2 + 0.5
240
+
241
+
242
+ @native_function(Op.EaseInQuart)
243
+ def ease_in_quart(x: float) -> float:
244
+ """Interpolate between 0 and 1, starting very slow and ending very fast with quartic easing."""
245
+ x = clamp(x, 0, 1)
246
+ return x**4
247
+
248
+
249
+ @native_function(Op.EaseOutQuart)
250
+ def ease_out_quart(x: float) -> float:
251
+ """Interpolate between 0 and 1, starting very fast and ending very slow with quartic easing."""
252
+ x = clamp(x, 0, 1)
253
+ return 1 - (1 - x) ** 4
254
+
255
+
256
+ @native_function(Op.EaseInOutQuart)
257
+ def ease_in_out_quart(x: float) -> float:
258
+ """Interpolate between 0 and 1, starting and ending very slow with quartic easing, very fast in the middle."""
259
+ x = clamp(x, 0, 1)
260
+ if x < 0.5:
261
+ return 8 * x**4
262
+ else:
263
+ return 1 - (-2 * x + 2) ** 4 / 2
264
+
265
+
266
+ @native_function(Op.EaseOutInQuart)
267
+ def ease_out_in_quart(x: float) -> float:
268
+ """Interpolate between 0 and 1, very fast at the start and end, very slow in the middle with quartic easing."""
269
+ x = clamp(x, 0, 1)
270
+ if x < 0.5:
271
+ return (1 - (1 - 2 * x) ** 4) / 2
272
+ else:
273
+ return ((2 * x - 1) ** 4) / 2 + 0.5
274
+
275
+
276
+ @native_function(Op.EaseInQuint)
277
+ def ease_in_quint(x: float) -> float:
278
+ """Interpolate between 0 and 1, starting extremely slow and ending extremely fast with quintic easing."""
279
+ x = clamp(x, 0, 1)
280
+ return x**5
281
+
282
+
283
+ @native_function(Op.EaseOutQuint)
284
+ def ease_out_quint(x: float) -> float:
285
+ """Interpolate between 0 and 1, starting extremely fast and ending extremely slow with quintic easing."""
286
+ x = clamp(x, 0, 1)
287
+ return 1 - (1 - x) ** 5
288
+
289
+
290
+ @native_function(Op.EaseInOutQuint)
291
+ def ease_in_out_quint(x: float) -> float:
292
+ """Interpolate between 0 and 1, starting and ending extremely slow with quintic easing, extremely fast in the middle."""
293
+ x = clamp(x, 0, 1)
294
+ if x < 0.5:
295
+ return 16 * x**5
296
+ else:
297
+ return 1 - (-2 * x + 2) ** 5 / 2
298
+
299
+
300
+ @native_function(Op.EaseOutInQuint)
301
+ def ease_out_in_quint(x: float) -> float:
302
+ """Interpolate between 0 and 1, extremely fast at the start and end, extremely slow in the middle with quintic easing."""
303
+ x = clamp(x, 0, 1)
304
+ if x < 0.5:
305
+ return (1 - (1 - 2 * x) ** 5) / 2
306
+ else:
307
+ return ((2 * x - 1) ** 5) / 2 + 0.5
308
+
309
+
310
+ @native_function(Op.EaseInSine)
311
+ def ease_in_sine(x: float) -> float:
312
+ """Interpolate between 0 and 1, starting slow and ending fast with sine easing."""
313
+ x = clamp(x, 0, 1)
314
+ return 1 - math.cos((x * math.pi) / 2)
315
+
316
+
317
+ @native_function(Op.EaseOutSine)
318
+ def ease_out_sine(x: float) -> float:
319
+ """Interpolate between 0 and 1, starting fast and ending slow with sine easing."""
320
+ x = clamp(x, 0, 1)
321
+ return math.sin((x * math.pi) / 2)
322
+
323
+
324
+ @native_function(Op.EaseInOutSine)
325
+ def ease_in_out_sine(x: float) -> float:
326
+ """Interpolate between 0 and 1, starting and ending slow with sine easing, fast in the middle."""
327
+ x = clamp(x, 0, 1)
328
+ return -(math.cos(math.pi * x) - 1) / 2
329
+
330
+
331
+ @native_function(Op.EaseOutInSine)
332
+ def ease_out_in_sine(x: float) -> float:
333
+ """Interpolate between 0 and 1, fast at the start and end, slow in the middle with sine easing."""
334
+ x = clamp(x, 0, 1)
335
+ if x < 0.5:
336
+ return math.sin(math.pi * x) / 2
337
+ else:
338
+ return (1 - math.cos(math.pi * x)) / 2
339
+
340
+
341
+ def linstep(x: float) -> float:
342
+ """Linear interpolation between 0 and 1."""
343
+ return clamp(x, 0.0, 1.0)
344
+
345
+
346
+ def smoothstep(x: float) -> float:
347
+ """Interpolate between 0 and 1 using smoothstep."""
348
+ x = clamp(x, 0.0, 1.0)
349
+ return x * x * (3 - 2 * x)
350
+
351
+
352
+ def smootherstep(x: float) -> float:
353
+ """Interpolate between 0 and 1 using smootherstep."""
354
+ x = clamp(x, 0.0, 1.0)
355
+ return x * x * x * (x * (x * 6 - 15) + 10)
356
+
357
+
358
+ def step_start(x: float) -> float:
359
+ """Step function returning 1.0 if x > 0, otherwise 0.0."""
360
+ return 1.0 if x > 0 else 0.0
361
+
362
+
363
+ def step_end(x: float) -> float:
364
+ """Step function returning 1.0 if x >= 1, otherwise 0.0."""
365
+ return 1.0 if x >= 1 else 0.0
sonolus/script/effect.py CHANGED
@@ -1,131 +1,191 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass
4
- from typing import Annotated, Any, NewType, dataclass_transform, get_origin
5
-
6
- from sonolus.backend.ops import Op
7
- from sonolus.script.internal.introspection import get_field_specifiers
8
- from sonolus.script.internal.native import native_function
9
- from sonolus.script.record import Record
10
-
11
-
12
- class Effect(Record):
13
- id: int
14
-
15
- def is_available(self) -> bool:
16
- return _has_effect_clip(self.id)
17
-
18
- def play(self, distance: float) -> None:
19
- _play(self.id, distance)
20
-
21
- def schedule(self, time: float, distance: float) -> None:
22
- _play_scheduled(self.id, time, distance)
23
-
24
- def loop(self) -> LoopedEffectHandle:
25
- return LoopedEffectHandle(_play_looped(self.id))
26
-
27
- def schedule_loop(self, start_time: float) -> ScheduledLoopedEffectHandle:
28
- return ScheduledLoopedEffectHandle(_play_looped_scheduled(self.id, start_time))
29
-
30
-
31
- class LoopedEffectHandle(Record):
32
- id: int
33
-
34
- def stop(self) -> None:
35
- _stop_looped(self.id)
36
-
37
-
38
- class ScheduledLoopedEffectHandle(Record):
39
- id: int
40
-
41
- def stop(self, end_time: float) -> None:
42
- _stop_looped_scheduled(self.id, end_time)
43
-
44
-
45
- @native_function(Op.HasEffectClip)
46
- def _has_effect_clip(effect_id: int) -> bool:
47
- raise NotImplementedError
48
-
49
-
50
- @native_function(Op.Play)
51
- def _play(effect_id: int, distance: float) -> None:
52
- raise NotImplementedError
53
-
54
-
55
- @native_function(Op.PlayLooped)
56
- def _play_looped(effect_id: int) -> int:
57
- raise NotImplementedError
58
-
59
-
60
- @native_function(Op.PlayLoopedScheduled)
61
- def _play_looped_scheduled(effect_id: int, start_time: float) -> int:
62
- raise NotImplementedError
63
-
64
-
65
- @native_function(Op.PlayScheduled)
66
- def _play_scheduled(effect_id: int, time: float, distance: float) -> None:
67
- raise NotImplementedError
68
-
69
-
70
- @native_function(Op.StopLooped)
71
- def _stop_looped(handle: int) -> None:
72
- raise NotImplementedError
73
-
74
-
75
- @native_function(Op.StopLoopedScheduled)
76
- def _stop_looped_scheduled(handle: int, end_time: float) -> None:
77
- raise NotImplementedError
78
-
79
-
80
- @dataclass
81
- class EffectInfo:
82
- name: str
83
-
84
-
85
- def effect(name: str) -> Any:
86
- return EffectInfo(name)
87
-
88
-
89
- type Effects = NewType("Effects", Any)
90
-
91
-
92
- @dataclass_transform()
93
- def effects[T](cls: type[T]) -> T | Effects:
94
- if len(cls.__bases__) != 1:
95
- raise ValueError("Effects class must not inherit from any class (except object)")
96
- instance = cls()
97
- names = []
98
- for i, (name, annotation) in enumerate(get_field_specifiers(cls).items()):
99
- if get_origin(annotation) is not Annotated:
100
- raise TypeError(f"Invalid annotation for effects: {annotation}")
101
- annotation_type = annotation.__args__[0]
102
- annotation_values = annotation.__metadata__
103
- if annotation_type is not Effect:
104
- raise TypeError(f"Invalid annotation for effects: {annotation}, expected annotation of type Effect")
105
- if len(annotation_values) != 1 or not isinstance(annotation_values[0], EffectInfo):
106
- raise TypeError(f"Invalid annotation for effects: {annotation}, expected a single string annotation value")
107
- effect_name = annotation_values[0].name
108
- names.append(effect_name)
109
- setattr(instance, name, Effect(i))
110
- instance._effects_ = names
111
- instance._is_comptime_value_ = True
112
- return instance
113
-
114
-
115
- class StandardEffect:
116
- MISS = Annotated[Effect, effect("#MISS")]
117
- PERFECT = Annotated[Effect, effect("#PERFECT")]
118
- GREAT = Annotated[Effect, effect("#GREAT")]
119
- GOOD = Annotated[Effect, effect("#GOOD")]
120
- HOLD = Annotated[Effect, effect("#HOLD")]
121
- MISS_ALTERNATIVE = Annotated[Effect, effect("#MISS_ALTERNATIVE")]
122
- PERFECT_ALTERNATIVE = Annotated[Effect, effect("#PERFECT_ALTERNATIVE")]
123
- GREAT_ALTERNATIVE = Annotated[Effect, effect("#GREAT_ALTERNATIVE")]
124
- GOOD_ALTERNATIVE = Annotated[Effect, effect("#GOOD_ALTERNATIVE")]
125
- HOLD_ALTERNATIVE = Annotated[Effect, effect("#HOLD_ALTERNATIVE")]
126
- STAGE = Annotated[Effect, effect("#STAGE")]
127
-
128
-
129
- @effects
130
- class EmptyEffects:
131
- pass
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Annotated, Any, NewType, dataclass_transform, get_origin
5
+
6
+ from sonolus.backend.ops import Op
7
+ from sonolus.script.internal.introspection import get_field_specifiers
8
+ from sonolus.script.internal.native import native_function
9
+ from sonolus.script.record import Record
10
+
11
+
12
+ class Effect(Record):
13
+ """Sound effect clip.
14
+
15
+ Usage:
16
+ ```python
17
+ Effect(id: int)
18
+ ```
19
+ """
20
+
21
+ id: int
22
+ """Effect ID."""
23
+
24
+ def is_available(self) -> bool:
25
+ """Return whether the effect clip is available."""
26
+ return _has_effect_clip(self.id)
27
+
28
+ def play(self, distance: float) -> None:
29
+ """Play the effect clip.
30
+
31
+ If the clip was already played within the specified distance, it will be skipped.
32
+
33
+ Arguments:
34
+ distance: Minimum time in seconds since the last play for the effect to play.
35
+ """
36
+ _play(self.id, distance)
37
+
38
+ def schedule(self, time: float, distance: float) -> None:
39
+ """Schedule the effect clip to play at a specific time.
40
+
41
+ This is not suitable for real-time effects such as responses to user input. Use `play` instead.
42
+
43
+ This may be called in preprocess to schedule effects upfront.
44
+
45
+ If the clip would play within the specified distance of another play, it will be skipped.
46
+
47
+ Arguments:
48
+ time: Time in seconds when the effect should play.
49
+ distance: Minimum time in seconds after a previous play for the effect to play.
50
+ """
51
+ _play_scheduled(self.id, time, distance)
52
+
53
+ def loop(self) -> LoopedEffectHandle:
54
+ """Play the effect clip in a loop until stopped.
55
+
56
+ Returns:
57
+ A handle to stop the loop.
58
+ """
59
+ return LoopedEffectHandle(_play_looped(self.id))
60
+
61
+ def schedule_loop(self, start_time: float) -> ScheduledLoopedEffectHandle:
62
+ """Schedule the effect clip to play in a loop until stopped.
63
+
64
+ This is not suitable for real-time effects such as responses to user input. Use `loop` instead.
65
+
66
+ Returns:
67
+ A handle to stop the loop.
68
+ """
69
+ return ScheduledLoopedEffectHandle(_play_looped_scheduled(self.id, start_time))
70
+
71
+
72
+ class LoopedEffectHandle(Record):
73
+ """Handle to stop a looped effect."""
74
+
75
+ id: int
76
+
77
+ def stop(self) -> None:
78
+ """Stop the looped effect."""
79
+ _stop_looped(self.id)
80
+
81
+
82
+ class ScheduledLoopedEffectHandle(Record):
83
+ """Handle to stop a scheduled looped effect."""
84
+
85
+ id: int
86
+
87
+ def stop(self, end_time: float) -> None:
88
+ """Stop the scheduled looped effect."""
89
+ _stop_looped_scheduled(self.id, end_time)
90
+
91
+
92
+ @native_function(Op.HasEffectClip)
93
+ def _has_effect_clip(effect_id: int) -> bool:
94
+ raise NotImplementedError
95
+
96
+
97
+ @native_function(Op.Play)
98
+ def _play(effect_id: int, distance: float) -> None:
99
+ raise NotImplementedError
100
+
101
+
102
+ @native_function(Op.PlayLooped)
103
+ def _play_looped(effect_id: int) -> int:
104
+ raise NotImplementedError
105
+
106
+
107
+ @native_function(Op.PlayLoopedScheduled)
108
+ def _play_looped_scheduled(effect_id: int, start_time: float) -> int:
109
+ raise NotImplementedError
110
+
111
+
112
+ @native_function(Op.PlayScheduled)
113
+ def _play_scheduled(effect_id: int, time: float, distance: float) -> None:
114
+ raise NotImplementedError
115
+
116
+
117
+ @native_function(Op.StopLooped)
118
+ def _stop_looped(handle: int) -> None:
119
+ raise NotImplementedError
120
+
121
+
122
+ @native_function(Op.StopLoopedScheduled)
123
+ def _stop_looped_scheduled(handle: int, end_time: float) -> None:
124
+ raise NotImplementedError
125
+
126
+
127
+ @dataclass
128
+ class EffectInfo:
129
+ name: str
130
+
131
+
132
+ def effect(name: str) -> Any:
133
+ """Define a sound effect clip with the given name."""
134
+ return EffectInfo(name)
135
+
136
+
137
+ type Effects = NewType("Effects", Any)
138
+
139
+
140
+ @dataclass_transform()
141
+ def effects[T](cls: type[T]) -> T | Effects:
142
+ """Decorator to define effect clips.
143
+
144
+ Usage:
145
+ ```python
146
+ @effects
147
+ class Effects:
148
+ miss: StandardEffect.MISS
149
+ other: Effect = effect("other")
150
+ ```
151
+ """
152
+ if len(cls.__bases__) != 1:
153
+ raise ValueError("Effects class must not inherit from any class (except object)")
154
+ instance = cls()
155
+ names = []
156
+ for i, (name, annotation) in enumerate(get_field_specifiers(cls).items()):
157
+ if get_origin(annotation) is not Annotated:
158
+ raise TypeError(f"Invalid annotation for effects: {annotation}")
159
+ annotation_type = annotation.__args__[0]
160
+ annotation_values = annotation.__metadata__
161
+ if annotation_type is not Effect:
162
+ raise TypeError(f"Invalid annotation for effects: {annotation}, expected annotation of type Effect")
163
+ if len(annotation_values) != 1 or not isinstance(annotation_values[0], EffectInfo):
164
+ raise TypeError(f"Invalid annotation for effects: {annotation}, expected a single string annotation value")
165
+ effect_name = annotation_values[0].name
166
+ names.append(effect_name)
167
+ setattr(instance, name, Effect(i))
168
+ instance._effects_ = names
169
+ instance._is_comptime_value_ = True
170
+ return instance
171
+
172
+
173
+ class StandardEffect:
174
+ """Standard sound effect clips."""
175
+
176
+ MISS = Annotated[Effect, effect("#MISS")]
177
+ PERFECT = Annotated[Effect, effect("#PERFECT")]
178
+ GREAT = Annotated[Effect, effect("#GREAT")]
179
+ GOOD = Annotated[Effect, effect("#GOOD")]
180
+ HOLD = Annotated[Effect, effect("#HOLD")]
181
+ MISS_ALTERNATIVE = Annotated[Effect, effect("#MISS_ALTERNATIVE")]
182
+ PERFECT_ALTERNATIVE = Annotated[Effect, effect("#PERFECT_ALTERNATIVE")]
183
+ GREAT_ALTERNATIVE = Annotated[Effect, effect("#GREAT_ALTERNATIVE")]
184
+ GOOD_ALTERNATIVE = Annotated[Effect, effect("#GOOD_ALTERNATIVE")]
185
+ HOLD_ALTERNATIVE = Annotated[Effect, effect("#HOLD_ALTERNATIVE")]
186
+ STAGE = Annotated[Effect, effect("#STAGE")]
187
+
188
+
189
+ @effects
190
+ class EmptyEffects:
191
+ pass