liquidsoap-prettier 1.8.2 → 1.8.3

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.
Files changed (67) hide show
  1. package/.github/workflows/check-formatting.yml +32 -0
  2. package/README.md +31 -5
  3. package/package.json +1 -1
  4. package/src/cli.js +104 -9
  5. package/tests/liq/audio.liq +460 -0
  6. package/tests/liq/autocue.liq +1081 -0
  7. package/tests/liq/clock.liq +14 -0
  8. package/tests/liq/cron.liq +74 -0
  9. package/tests/liq/error.liq +48 -0
  10. package/tests/liq/extra/audio.liq +677 -0
  11. package/tests/liq/extra/audioscrobbler.liq +482 -0
  12. package/tests/liq/extra/deprecations.liq +976 -0
  13. package/tests/liq/extra/externals.liq +196 -0
  14. package/tests/liq/extra/fades.liq +260 -0
  15. package/tests/liq/extra/file.liq +66 -0
  16. package/tests/liq/extra/http.liq +160 -0
  17. package/tests/liq/extra/interactive.liq +917 -0
  18. package/tests/liq/extra/metadata.liq +75 -0
  19. package/tests/liq/extra/native.liq +201 -0
  20. package/tests/liq/extra/openai.liq +150 -0
  21. package/tests/liq/extra/server.liq +177 -0
  22. package/tests/liq/extra/source.liq +476 -0
  23. package/tests/liq/extra/spinitron.liq +272 -0
  24. package/tests/liq/extra/telnet.liq +266 -0
  25. package/tests/liq/extra/video.liq +59 -0
  26. package/tests/liq/extra/visualization.liq +68 -0
  27. package/tests/liq/fades.liq +941 -0
  28. package/tests/liq/ffmpeg.liq +605 -0
  29. package/tests/liq/file.liq +387 -0
  30. package/tests/liq/getter.liq +74 -0
  31. package/tests/liq/hls.liq +329 -0
  32. package/tests/liq/http.liq +1048 -0
  33. package/tests/liq/http_codes.liq +447 -0
  34. package/tests/liq/icecast.liq +58 -0
  35. package/tests/liq/io.liq +106 -0
  36. package/tests/liq/liquidsoap.liq +31 -0
  37. package/tests/liq/list.liq +440 -0
  38. package/tests/liq/log.liq +47 -0
  39. package/tests/liq/lufs.liq +295 -0
  40. package/tests/liq/math.liq +23 -0
  41. package/tests/liq/medialib.liq +752 -0
  42. package/tests/liq/metadata.liq +253 -0
  43. package/tests/liq/nfo.liq +258 -0
  44. package/tests/liq/null.liq +71 -0
  45. package/tests/liq/playlist.liq +1347 -0
  46. package/tests/liq/predicate.liq +106 -0
  47. package/tests/liq/process.liq +93 -0
  48. package/tests/liq/profiler.liq +5 -0
  49. package/tests/liq/protocols.liq +1139 -0
  50. package/tests/liq/ref.liq +28 -0
  51. package/tests/liq/replaygain.liq +135 -0
  52. package/tests/liq/request.liq +467 -0
  53. package/tests/liq/resolvers.liq +33 -0
  54. package/tests/liq/runtime.liq +70 -0
  55. package/tests/liq/server.liq +99 -0
  56. package/tests/liq/settings.liq +41 -0
  57. package/tests/liq/socket.liq +33 -0
  58. package/tests/liq/source.liq +362 -0
  59. package/tests/liq/sqlite.liq +161 -0
  60. package/tests/liq/stdlib.liq +172 -0
  61. package/tests/liq/string.liq +476 -0
  62. package/tests/liq/switches.liq +197 -0
  63. package/tests/liq/testing.liq +37 -0
  64. package/tests/liq/thread.liq +161 -0
  65. package/tests/liq/tracks.liq +100 -0
  66. package/tests/liq/utils.liq +81 -0
  67. package/tests/liq/video.liq +918 -0
@@ -0,0 +1,476 @@
1
+ # Match a string with an expression. Perl compatible regular expressions are
2
+ # recognized. Hence, special characters should be escaped. Alternatively, one
3
+ # can use the `r/_/.test(_)` syntax for regular expressions.
4
+ # @category String
5
+ def string.match(~pattern, s) =
6
+ regexp(pattern).test(s)
7
+ end
8
+
9
+ # Extract substrings from a string. Perl compatible regular expressions are
10
+ # recognized. Hence, special characters should be escaped. Returns a list of
11
+ # (index,value). If the list does not have a pair associated to some index, it
12
+ # means that the corresponding pattern was not found. Alter natively, one can
13
+ # use the `r/_/.exec(_)` syntax for regular expressions.
14
+ # @category String
15
+ def string.extract(~pattern, s) =
16
+ regexp(pattern).exec(s)
17
+ end
18
+
19
+ # Replace all substrings matched by a pattern by another string returned by a
20
+ # function. Alternatively, one can use the `r/_/g.replace(_)` syntax for regular
21
+ # expressions.
22
+ # @category String
23
+ # @param ~pattern Pattern (regular expression) of substrings which should be replaced.
24
+ # @param f Function getting a matched substring an returning the string to replace it with.
25
+ # @param s String whose substrings should be replaced.
26
+ def string.replace(~pattern, f, s) =
27
+ regexp(flags=["g"], pattern).replace(f, s)
28
+ end
29
+
30
+ # Split a string at "separator". Perl compatible regular expressions are
31
+ # recognized. Hence, special characters should be escaped. Alternatively, one
32
+ # can use the `r/_/.split(_)` syntax for regular expressions.
33
+ # @category String
34
+ def string.split(~separator, s) =
35
+ regexp(separator).split(s)
36
+ end
37
+
38
+ # Return an array of the string's bytes.
39
+ # @category String
40
+ def string.bytes(s) =
41
+ string.split(separator="", s)
42
+ end
43
+
44
+ # Return the length of the string in bytes.
45
+ # @category String
46
+ def string.bytes.length(s) =
47
+ string.length(encoding="ascii", s)
48
+ end
49
+
50
+ # Split a string in two at first "separator".
51
+ # @category String
52
+ def string.split.first(~encoding=null, ~separator, s) =
53
+ n = string.length(encoding=encoding, s)
54
+ l = string.length(encoding=encoding, separator)
55
+ i = string.index(substring=separator, s)
56
+ if
57
+ i < 0
58
+ then
59
+ error.raise(
60
+ error.not_found,
61
+ "String does not contain the separator."
62
+ )
63
+ end
64
+
65
+ (
66
+ string.sub(encoding=encoding, s, start=0, length=i),
67
+ string.sub(encoding=encoding, s, start=i + l, length=n - (i + l))
68
+ )
69
+ end
70
+
71
+ # Test whether a string contains a given prefix, substring or suffix.
72
+ # @category String
73
+ # @param ~encoding Encoding used to split characters. Should be one of: `"utf8"` or `"ascii"`
74
+ # @param ~prefix Prefix to look for.
75
+ # @param ~substring Substring to look for.
76
+ # @param ~suffix Suffix to look for.
77
+ # @param s The string to look into.
78
+ def string.contains(~encoding=null, ~prefix="", ~substring="", ~suffix="", s) =
79
+ ans = ref(prefix == "" and substring == "" and suffix == "")
80
+ if
81
+ prefix != ""
82
+ then
83
+ ans :=
84
+ ans()
85
+ or string.sub(
86
+ encoding=encoding,
87
+ s,
88
+ start=0,
89
+ length=string.length(encoding=encoding, prefix)
90
+ ) ==
91
+ prefix
92
+ end
93
+
94
+ if
95
+ suffix != ""
96
+ then
97
+ suflen = string.length(encoding=encoding, suffix)
98
+ ans :=
99
+ ans()
100
+ or string.sub(
101
+ encoding=encoding,
102
+ s,
103
+ start=string.length(encoding=encoding, s) - suflen,
104
+ length=suflen
105
+ ) ==
106
+ suffix
107
+ end
108
+
109
+ if
110
+ substring != ""
111
+ then
112
+ sublen = string.length(encoding=encoding, substring)
113
+ for i = 0 to string.length(encoding=encoding, s) - sublen do
114
+ ans :=
115
+ ans()
116
+ or (
117
+ string.sub(encoding=encoding, s, start=i, length=sublen) == substring
118
+ )
119
+
120
+ end
121
+ end
122
+
123
+ ans()
124
+ end
125
+
126
+ # What remains of a string after a given prefix.
127
+ # @category String
128
+ # @param ~encoding Encoding used to split characters. Should be one of: `"utf8"` or `"ascii"`
129
+ # @param ~prefix Requested prefix.
130
+ def string.residual(~encoding=null, ~prefix, s) =
131
+ n = string.length(encoding=encoding, prefix)
132
+ if
133
+ string.contains(encoding=encoding, prefix=prefix, s)
134
+ then
135
+ string.sub(
136
+ encoding=encoding,
137
+ s,
138
+ start=n,
139
+ length=string.length(encoding=encoding, s) - n
140
+ )
141
+ else
142
+ null
143
+ end
144
+ end
145
+
146
+ # Test whether a string is a valid integer.
147
+ # @category String
148
+ def string.is_int(s) =
149
+ try
150
+ ignore(int_of_string(s))
151
+ true
152
+ catch _ do
153
+ false
154
+ end
155
+ end
156
+
157
+ # Convert a string to a int.
158
+ # @category String
159
+ def string.to_int(~default=0, s) =
160
+ int_of_string(default=default, s)
161
+ end
162
+
163
+ # Convert an int to string.
164
+ # @category String
165
+ # @param ~digits Minimal number of digits (pad with 0s on the left if necessary).
166
+ def string.of_int(~digits=0, n) =
167
+ s = string(n)
168
+ if
169
+ string.length(s) >= digits
170
+ then
171
+ s
172
+ else
173
+ string.make(char_code=48, digits - string.bytes.length(s)) ^ s
174
+ end
175
+ end
176
+
177
+ # Convert a string to a float.
178
+ # @category String
179
+ def string.to_float(~default=0., s) =
180
+ float_of_string(default=default, s)
181
+ end
182
+
183
+ let string.binary = ()
184
+
185
+ # Value of a positive (unsigned) integer encoded using native memory
186
+ # representation.
187
+ # @category String
188
+ # @param ~little_endian Whether the memory representation is little endian.
189
+ # @param s String containing the binary representation.
190
+ def string.binary.to_int(~little_endian=true, s) =
191
+ ans = ref(0)
192
+ n = string.bytes.length(s)
193
+ for i = 0 to n - 1 do
194
+ ans :=
195
+ lsl(ans(), 8) + string.nth(s, if little_endian then n - 1 - i else i end)
196
+
197
+ end
198
+
199
+ ans()
200
+ end
201
+
202
+ # Encode a positive (unsigned) integer using native memory representation.
203
+ # @category String
204
+ # @param ~pad Minimum length in digits (pad on the left with zeros in order to reach it)
205
+ # @param ~little_endian Whether the memory representation is little endian.
206
+ # @param s String containing the binary representation.
207
+ def string.binary.of_int(~pad=0, ~little_endian=true, d) =
208
+ def rec f(d, s) =
209
+ if
210
+ d > 0
211
+ then
212
+ c = string.hex_of_int(pad=2, (d mod 256))
213
+ if
214
+ little_endian
215
+ then
216
+ f(lsr(d, 8), "#{s}\\x#{c}")
217
+ else
218
+ f(lsr(d, 8), "\\x#{c}#{s}")
219
+ end
220
+ else
221
+ s
222
+ end
223
+ end
224
+
225
+ ret = d == 0 ? "\\x00" : f(d, "")
226
+ ret = string.unescape(ret)
227
+ len = string.bytes.length(ret)
228
+ if
229
+ len < pad
230
+ then
231
+ ans = string.make(char_code=0, pad - len)
232
+ if little_endian then "#{ret}#{ans}" else "#{ans}#{ret}" end
233
+ else
234
+ ret
235
+ end
236
+ end
237
+
238
+ # Add a null character at the end of a string.
239
+ # @category String
240
+ # @param s String.
241
+ def string.null_terminated(s) =
242
+ s ^ "\000"
243
+ end
244
+
245
+ # Generate an identifier if no identifier was provided.
246
+ # @category String
247
+ # @param ~default Name from which identifier is generated if not present.
248
+ # @param id Proposed identifier.
249
+ def string.id.default(~default, id) =
250
+ null.default(id, {string.id(default)})
251
+ end
252
+
253
+ # Return a quoted copy of the given string.
254
+ # By default, the string is assumed to be `"utf8"` encoded and is escaped
255
+ # following JSON and javascript specification.
256
+ # @category String
257
+ # @param ~encoding One of: `"ascii"` or `"utf8"`. If `null`, `utf8` is tried first and `ascii` is used as a fallback if this fails.
258
+ def string.quote(~encoding=null, s) =
259
+ def special_char(~encoding, s) =
260
+ if
261
+ s == "'"
262
+ then
263
+ false
264
+ else
265
+ string.escape.special_char(encoding=encoding, s)
266
+ end
267
+ end
268
+
269
+ s = string.escape(special_char=special_char, encoding=encoding, s)
270
+ "\"#{s}\""
271
+ end
272
+
273
+ # Return an unquoted copy of the given string.
274
+ # Quotes are removed by trying to parse the string
275
+ # following the JSON string escaping convention.
276
+ # @category String
277
+ def string.unquote(s) =
278
+ try
279
+ let json.parse (s : string) = s
280
+ s
281
+ catch _ do
282
+ s
283
+ end
284
+ end
285
+
286
+ let string.data_uri = ()
287
+
288
+ # Encode a string using the data uri format,
289
+ # i.e. `"data:<mime>[;base64],<data>"`.
290
+ # @category String
291
+ # @param ~base64 Encode data using the base64 format
292
+ # @param ~mime Data mime type
293
+ def string.data_uri.encode(~base64=true, ~(mime:string), s) =
294
+ s = base64 ? ";base64,#{string.base64.encode(s)}" : ",#{s}"
295
+ "data:#{mime}#{s}"
296
+ end
297
+
298
+ # Decode a string using the data uri format,
299
+ # i.e. `"data:<mime>[;base64],<data>"`.
300
+ # @category String
301
+ def string.data_uri.decode(s) =
302
+ captured =
303
+ r/^data:(?<mime>[\/\w]+)(?<base64>;base64)?,(?<data>.+)$/.exec(s).groups
304
+
305
+ if
306
+ list.assoc.mem("mime", captured) and list.assoc.mem("data", captured)
307
+ then
308
+ mime = list.assoc("mime", captured)
309
+ data = list.assoc("data", captured)
310
+ data =
311
+ if
312
+ list.assoc.mem("base64", captured)
313
+ then
314
+ string.base64.decode(data)
315
+ else
316
+ data
317
+ end
318
+
319
+ data.{mime = mime}
320
+ else
321
+ null
322
+ end
323
+ end
324
+
325
+ let string.getter = ()
326
+
327
+ # Create a string getter which will return once the given string and then the
328
+ # empty string.
329
+ # @category String
330
+ def string.getter.single(s) =
331
+ first = ref(true)
332
+ fun () ->
333
+ begin
334
+ if
335
+ first()
336
+ then
337
+ first := false
338
+ s
339
+ else
340
+ ""
341
+ end
342
+ end
343
+ end
344
+
345
+ # Flush all values from a string getter and return
346
+ # the concatenated result. If the getter is constant,
347
+ # return the constant string. Otherwise, call the getter
348
+ # repeatedly until it returns an empty string and return
349
+ # the concatenated result
350
+ # @category String
351
+ def string.getter.flush(~separator="", gen) =
352
+ if
353
+ getter.is_constant(gen)
354
+ then
355
+ getter.get(gen)
356
+ else
357
+ def rec f(data) =
358
+ chunk = getter.get(gen)
359
+ if
360
+ chunk == ""
361
+ then
362
+ string.concat(separator=separator, data)
363
+ else
364
+ f([...data, chunk])
365
+ end
366
+ end
367
+
368
+ f([])
369
+ end
370
+ end
371
+
372
+ # Combine a list of string getters `[g1, ...]`
373
+ # and return a single getter `g` such that:
374
+ # `string.getter.flush(separator=s, g) = string.concat(separator=s, list.filter(fun (s) -> s != "", [string.getter.flush(g1), ...]))`
375
+ # @category String
376
+ def string.getter.concat(l) =
377
+ len = list.length(l)
378
+ pos = ref(0)
379
+
380
+ def rec f() =
381
+ if
382
+ pos() == len
383
+ then
384
+ ""
385
+ else
386
+ gen = list.nth(l, pos())
387
+ ret = getter.get(gen)
388
+ if ret == "" or getter.is_constant(gen) then ref.incr(pos) end
389
+ ret == "" ? f() : ret
390
+ end
391
+ end
392
+
393
+ getter(f)
394
+ end
395
+
396
+ let string.char.ascii = ()
397
+
398
+ # All ASCII characters code
399
+ # @category String
400
+ let string.char.ascii = list.init(128, fun (c) -> c)
401
+
402
+ # All ASCII control character codes
403
+ # @category String
404
+ let string.char.ascii.control = list.init(32, fun (c) -> c)
405
+
406
+ # All ASCII printable character codes
407
+ # @category String
408
+ let string.char.ascii.printable = list.init(96, fun (c) -> c + 32)
409
+
410
+ # All ASCII alphabet character codes
411
+ # @category String
412
+ let string.char.ascii.alphabet =
413
+ [
414
+ # A-Z
415
+ ...list.init(24, fun (c) -> c + 65),
416
+ # a-z
417
+ ...list.init(24, fun (c) -> c + 97)
418
+ ]
419
+
420
+ # All ASCII number character codes
421
+ # @category String
422
+ let string.char.ascii.number = list.init(10, fun (c) -> c + 48)
423
+
424
+ # Return a random ASCII character
425
+ # @category String
426
+ def string.char.ascii.random(range=[...string.char.ascii]) =
427
+ string.char(list.nth(range, random.int(min=0, max=list.length(range) - 1)))
428
+ end
429
+
430
+ # Escape HTML entities.
431
+ # @category String
432
+ # @argsof string.escape[encoding]
433
+ def string.escape.html(%argsof(string.escape[encoding]), s) =
434
+ escaped =
435
+ [
436
+ ("&", "&amp;"),
437
+ ("<", "&lt;"),
438
+ (">", "&gt;"),
439
+ ('"', "&quot;"),
440
+ ("'", "&#39;")
441
+ ]
442
+
443
+ def special_char(~encoding:_, c) =
444
+ list.assoc.mem(c, escaped)
445
+ end
446
+
447
+ def escape_char(~encoding:_, c) =
448
+ escaped[c]
449
+ end
450
+
451
+ string.escape(
452
+ %argsof(string.escape[encoding]),
453
+ special_char=special_char,
454
+ escape_char=escape_char,
455
+ s
456
+ )
457
+ end
458
+
459
+ # Generate a given number of spaces (this can be useful for indenting).
460
+ # @category String
461
+ # @param n Number of spaces.
462
+ def string.spaces(n) =
463
+ string.make(char_code=32, n)
464
+ end
465
+
466
+ # Convert a string to uppercase.
467
+ # @category String
468
+ def string.uppercase(s) =
469
+ string.case(lower=false, s)
470
+ end
471
+
472
+ # Convert a string to lowercase.
473
+ # @category String
474
+ def string.lowercase(s) =
475
+ string.case(lower=true, s)
476
+ end
@@ -0,0 +1,197 @@
1
+ # At the beginning of each track, select the first ready child.
2
+ # @category Source / Track processing
3
+ # @param ~id Force the value of the source ID.
4
+ def fallback(~id=null, sources) =
5
+ switch(id=id, list.map(fun (s) -> ({true}, s), sources))
6
+ end
7
+
8
+ # Rotate between sources.
9
+ # @category Source / Track processing
10
+ # @param ~id Force the value of the source ID.
11
+ def rotate(~id=null, sources) =
12
+ sources =
13
+ (sources :
14
+ [
15
+ source.{
16
+ on_select?: (
17
+ {ending: source?, replay_metadata: bool, starting: source}
18
+ )->source,
19
+ on_leave?: ({source: source, track_sensitive: bool})->unit,
20
+ single?: bool,
21
+ track_sensitive?: getter(bool),
22
+ weight?: getter(int)
23
+ }
24
+ ]
25
+ )
26
+
27
+ sources = list.map(fun (s) -> s.{weight = s?.weight ?? getter(1)}, sources)
28
+ failed = (source.fail() : source)
29
+
30
+ # Currently selected index
31
+ picked_index = ref(-1)
32
+
33
+ # Number of tracks played per selected source
34
+ # source IDs can change between calls..
35
+ tracks_played =
36
+ list.map(fun (s) -> ((fun () -> source.id(s)), ref(0)), sources)
37
+
38
+ tracks_played =
39
+ fun () ->
40
+ list.map(
41
+ fun (x) ->
42
+ begin
43
+ label_fn = fst(x)
44
+ (label_fn(), snd(x))
45
+ end,
46
+ tracks_played
47
+ )
48
+
49
+ # Find index of next source to play, i.e. first ready source after currently
50
+ # selected one.
51
+ def pick() =
52
+ list.iter((fun (el) -> snd(el) := 0), tracks_played())
53
+ if
54
+ list.exists(source.is_ready, sources)
55
+ then
56
+ def rec f(index) =
57
+ s = list.nth(default=failed, sources, index)
58
+ if
59
+ source.is_ready(s)
60
+ then
61
+ picked_index := index
62
+ else
63
+ f((index + 1) mod list.length(sources))
64
+ end
65
+ end
66
+
67
+ f((picked_index() + 1) mod list.length(sources))
68
+ else
69
+ picked_index := -1
70
+ end
71
+ end
72
+
73
+ # Add condition to i-th source.
74
+ def add_condition(index, s) =
75
+ def cond() =
76
+ if picked_index() == -1 then pick() end
77
+ if
78
+ picked_index() != -1
79
+ then
80
+ picked_source = list.nth(sources, picked_index())
81
+ fn = list.assoc(source.id(picked_source), tracks_played())
82
+ if getter.get(picked_source.weight) <= fn() then pick() end
83
+ end
84
+
85
+ picked_index() == index
86
+ end
87
+
88
+ (cond, s)
89
+ end
90
+
91
+ s = switch(list.mapi(add_condition, sources))
92
+
93
+ def f(_) =
94
+ if
95
+ null.defined(s.selected())
96
+ then
97
+ selected_id = source.id(null.get(s.selected()))
98
+ if
99
+ list.assoc.mem(selected_id, tracks_played())
100
+ then
101
+ played = list.assoc(selected_id, tracks_played())
102
+ ref.incr(played)
103
+ end
104
+ end
105
+ end
106
+
107
+ s.on_track(synchronous=true, f)
108
+
109
+ def replaces s =
110
+ fallback(id=id, s::sources)
111
+ end
112
+
113
+ s
114
+ end
115
+
116
+ # At the beginning of every track, select a random ready child.
117
+ # @category Source / Track processing
118
+ # @param ~id Force the value of the source ID.
119
+ def replaces random(~id=null, sources) =
120
+ sources =
121
+ (sources :
122
+ [
123
+ source.{
124
+ on_select?: (
125
+ {ending: source?, replay_metadata: bool, starting: source}
126
+ )->source,
127
+ on_leave?: ({source: source, track_sensitive: bool})->unit,
128
+ single?: bool,
129
+ track_sensitive?: getter(bool),
130
+ weight?: getter(int)
131
+ }
132
+ ]
133
+ )
134
+
135
+ sources = list.map(fun (s) -> s.{weight = s?.weight ?? getter(1)}, sources)
136
+ next_index = ref(-1)
137
+
138
+ def pick() =
139
+ def available_weighted_sources(cur, s) =
140
+ let (index, current_weight, indexes) = cur
141
+ let (current_weight, indexes) =
142
+ if
143
+ source.is_ready((s : source.{ weight: getter(int) }))
144
+ then
145
+ weight = getter.get(s.weight)
146
+ indexes = (current_weight, current_weight + weight, index)::indexes
147
+ (current_weight + weight, indexes)
148
+ else
149
+ (current_weight, indexes)
150
+ end
151
+
152
+ (index + 1, current_weight, indexes)
153
+ end
154
+
155
+ let (_, total_weight, weighted_indexes) =
156
+ list.fold(available_weighted_sources, (0, 0, []), sources)
157
+
158
+ picked_weight =
159
+ if total_weight > 0 then random.int(min=0, max=total_weight) else 0 end
160
+
161
+ def pick_source(picked_index, el) =
162
+ let (lower_bound, upper_bound, index) = el
163
+ if
164
+ lower_bound <= picked_weight and picked_weight < upper_bound
165
+ then
166
+ index
167
+ else
168
+ picked_index
169
+ end
170
+ end
171
+
172
+ next_index := list.fold(pick_source, -1, weighted_indexes)
173
+ end
174
+
175
+ def add_condition(index, s) =
176
+ def cond() =
177
+ if next_index() == -1 then pick() end
178
+ next_index() == index
179
+ end
180
+
181
+ (cond, s)
182
+ end
183
+
184
+ s = switch(list.mapi(add_condition, sources))
185
+
186
+ def f(_) =
187
+ next_index := -1
188
+ end
189
+
190
+ s.on_track(synchronous=true, f)
191
+
192
+ def replaces s =
193
+ fallback(id=id, s::sources)
194
+ end
195
+
196
+ s
197
+ end
@@ -0,0 +1,37 @@
1
+ # Sleep regularly, thus inducing delays in the sound production. This is mainly
2
+ # useful for emulating network delays or sources which are slow to produce data,
3
+ # and thus test bufferization and robustness of scripts.
4
+ # @category Source / Testing
5
+ # @flag experimental
6
+ # @param ~every How often we should sleep (in seconds, 0 means every frame).
7
+ # @param ~delay Delay introduced (in seconds).
8
+ # @param ~delay_random Maximum amount of time randomly added to the delay (in seconds).
9
+ # @param ~on_delay Function called when a delay is introduced, with the delay as argument.
10
+ # @param s Source in which the delays should be introduced.
11
+ # @method frozen The stream production is frozen while set to `true`.
12
+ def sleeper(
13
+ ~every=1.,
14
+ ~delay=1.1,
15
+ ~delay_random=0.,
16
+ ~on_delay=fun (_) -> (),
17
+ s
18
+ ) =
19
+ last = ref(0.)
20
+ frozen = ref(false)
21
+
22
+ def f() =
23
+ while frozen() do thread.pause(0.1) end
24
+ now = source.time(s)
25
+ if
26
+ now >= last() + every
27
+ then
28
+ last := now
29
+ delay = delay + random.float(max=delay_random)
30
+ on_delay(delay)
31
+ thread.pause(delay)
32
+ end
33
+ end
34
+
35
+ source.methods(s).on_frame(synchronous=true, f)
36
+ s.{frozen = frozen}
37
+ end