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,677 @@
1
+ # Compand the signal.
2
+ # @category Source / Audio processing
3
+ # @flag extra
4
+ # @argsof track.audio.compand
5
+ def compand(~id=null("compand"), %argsof(track.audio.compand[!id]), s) =
6
+ tracks = source.tracks(s)
7
+ source(
8
+ id=id,
9
+ tracks.{
10
+ audio = track.audio.compand(%argsof(track.audio.compand), tracks.audio)
11
+ }
12
+ )
13
+ end
14
+
15
+ # Comb filter
16
+ # @category Source / Audio processing
17
+ # @argsof track.audio.comb
18
+ # @flag extra
19
+ def comb(~id=null("comb"), %argsof(track.audio.comb[!id]), s) =
20
+ tracks = source.tracks(s)
21
+ source(
22
+ id=id,
23
+ tracks.{audio = track.audio.comb(%argsof(track.audio.comb), tracks.audio)}
24
+ )
25
+ end
26
+
27
+ # Compress the signal.
28
+ # @category Source / Audio processing
29
+ # @argsof track.audio.compress
30
+ # @flag extra
31
+ def compress(%argsof(track.audio.compress), s) =
32
+ tracks = source.tracks(s)
33
+ let {gain, rms, ...audio} =
34
+ track.audio.compress(%argsof(track.audio.compress), tracks.audio)
35
+
36
+ source(id=id, tracks.{audio = audio}).{gain = gain, rms = rms}
37
+ end
38
+
39
+ # Exponential compressor.
40
+ # @category Source / Audio processing
41
+ # @argsof track.audio.compress.exponential
42
+ # @flag extra
43
+ def compress.exponential(%argsof(track.audio.compress.exponential), s) =
44
+ tracks = source.tracks(s)
45
+ source(
46
+ id=id,
47
+ tracks.{
48
+ audio =
49
+ track.audio.compress.exponential(
50
+ %argsof(track.audio.compress.exponential),
51
+ tracks.audio
52
+ )
53
+ }
54
+ )
55
+ end
56
+
57
+ # A limiter. This is a `compress` with tweaked parameters.
58
+ # @category Source / Audio processing
59
+ # @flag extra
60
+ def limit(
61
+ ~id=null,
62
+ ~attack=getter(50.),
63
+ ~release=getter(200.),
64
+ ~ratio=getter(20.),
65
+ ~threshold=getter(-2.),
66
+ ~pre_gain=getter(0.),
67
+ ~gain=getter(0.),
68
+ s
69
+ ) =
70
+ compress(
71
+ id=id,
72
+ attack=attack,
73
+ release=release,
74
+ ratio=ratio,
75
+ threshold=threshold,
76
+ pre_gain=pre_gain,
77
+ gain=gain,
78
+ s
79
+ )
80
+ end
81
+
82
+ let limiter = limit
83
+
84
+ # A bandpass filter obtained by chaining a low-pass and a high-pass filter.
85
+ # @category Source / Audio processing
86
+ # @flag extra
87
+ # @param id Force the value of the source ID.
88
+ # @param ~low Lower frequency of the bandpass filter.
89
+ # @param ~high Higher frequency of the bandpass filter.
90
+ # @param ~q Q factor.
91
+ def filter.iir.eq.low_high(~id=null, ~low, ~high, ~q=1., s) =
92
+ s =
93
+ if
94
+ not (getter.is_constant(high) and getter.get(high) == infinity)
95
+ then
96
+ filter.iir.eq.low(id=id, frequency=high, q=q, s)
97
+ else
98
+ s
99
+ end
100
+
101
+ s =
102
+ if
103
+ not (getter.is_constant(low) and getter.get(low) == 0.)
104
+ then
105
+ filter.iir.eq.high(id=id, frequency=low, q=q, s)
106
+ else
107
+ s
108
+ end
109
+
110
+ s
111
+ end
112
+
113
+ # Multiband compression. The list in argument specifies
114
+ # - the `frequency` below which we should apply compression (it is above previous band)
115
+ # - the `attack` time (ms)
116
+ # - the `release` time (ms)
117
+ # - the compression `ratio`
118
+ # - the `threshold` for compression
119
+ # - the `gain` for the band
120
+ # @category Source / Audio processing
121
+ # @param ~limit Also apply limiting to bands.
122
+ # @param l Parameters for compression bands.
123
+ # @param s Source on which multiband compression should be applied.
124
+ # @flag extra
125
+ def compress.multiband(~limit=true, ~wet=getter(1.), s, l) =
126
+ # Check that the bands are with increasing frequencies.
127
+ for i = 0 to list.length(l) - 2 do
128
+ if
129
+ getter.get(list.nth(l, i + 1).frequency) <
130
+ getter.get(list.nth(l, i).frequency)
131
+ then
132
+ failwith(
133
+ "Bands should be sorted."
134
+ )
135
+ end
136
+
137
+ end
138
+
139
+ # Process a band
140
+ def band(low, band) =
141
+ high =
142
+ if
143
+ getter.is_constant(band.frequency)
144
+ and getter.get(band.frequency) >= float_of_int(audio.samplerate()) / 2.
145
+ then
146
+ infinity
147
+ else
148
+ band.frequency
149
+ end
150
+
151
+ s = filter.iir.eq.low_high(low=low, high=high, s)
152
+ s =
153
+ compress(
154
+ attack=band.attack,
155
+ release=band.release,
156
+ threshold=band.threshold,
157
+ ratio=band.ratio,
158
+ gain=band.gain,
159
+ s
160
+ )
161
+
162
+ if limit then limiter(s) else s end
163
+ end
164
+
165
+ ls =
166
+ list.mapi(
167
+ fun (i, b) ->
168
+ band(if i == 0 then 0. else list.nth(l, i - 1).frequency end, b),
169
+ l
170
+ )
171
+
172
+ c = add(normalize=false, ls)
173
+ s =
174
+ if
175
+ not getter.is_constant(wet) or getter.get(wet) != 1.
176
+ then
177
+ add(
178
+ normalize=false,
179
+ [amplify({1. - getter.get(wet)}, s), amplify(wet, c)]
180
+ )
181
+ else
182
+ c
183
+ end
184
+
185
+ # Seal l element type
186
+ if false then () else list.hd(l) end
187
+
188
+ # Limit to avoid bad surprises
189
+ limiter(s)
190
+ end
191
+
192
+ # Compress and normalize, producing a more uniform and "full" sound.
193
+ # @category Source / Audio processing
194
+ # @flag extra
195
+ # @param s The input source.
196
+ def nrj(s) =
197
+ compress(threshold=-15., ratio=3., gain=3., normalize(s))
198
+ end
199
+
200
+ # Multiband-compression.
201
+ # @category Source / Audio processing
202
+ # @flag extra
203
+ # @param s The input source.
204
+ def sky(s) =
205
+ # 3-band crossover
206
+ low = fun (s) -> filter.iir.eq.low(frequency=168., s)
207
+ mh = fun (s) -> filter.iir.eq.high(frequency=100., s)
208
+ mid = fun (s) -> filter.iir.eq.low(frequency=1800., s)
209
+ high = fun (s) -> filter.iir.eq.high(frequency=1366., s)
210
+
211
+ # Add back
212
+ add(
213
+ normalize=false,
214
+ [
215
+ compress(
216
+ attack=100.,
217
+ release=200.,
218
+ threshold=-20.,
219
+ ratio=6.,
220
+ gain=6.7,
221
+ knee=0.3,
222
+ low(s)
223
+ ),
224
+ compress(
225
+ attack=100.,
226
+ release=200.,
227
+ threshold=-20.,
228
+ ratio=6.,
229
+ gain=6.7,
230
+ knee=0.3,
231
+ mid(mh(s))
232
+ ),
233
+ compress(
234
+ attack=100.,
235
+ release=200.,
236
+ threshold=-20.,
237
+ ratio=6.,
238
+ gain=6.7,
239
+ knee=0.3,
240
+ high(s)
241
+ )
242
+ ]
243
+ )
244
+ end
245
+
246
+ # Add some bass to the sound.
247
+ # @category Source / Audio processing
248
+ # @param ~frequency Frequency below which sound is considered as bass.
249
+ # @param ~gain Amount of boosting (dB).
250
+ # @param s Source whose bass should be boosted
251
+ # @flag extra
252
+ def bass_boost(~frequency=getter(200.), ~gain=getter(10.), s) =
253
+ bass = limit(pre_gain=gain, filter.iir.eq.low(frequency=frequency, s))
254
+ add([s, bass])
255
+ end
256
+
257
+ %ifdef soundtouch
258
+ # Increases the pitch, making voices sound like on helium.
259
+ # @category Source / Audio processing
260
+ # @flag extra
261
+ # @param s The input source.
262
+ def helium(s) =
263
+ soundtouch(pitch=1.5, s)
264
+ end
265
+ %endif
266
+
267
+ # Remove low frequencies often produced by microphones.
268
+ # @flag extra
269
+ # @category Source / Audio processing
270
+ # @param ~frequency Frequency under which sound should be lowered.
271
+ # @param s The input source.
272
+ def mic_filter(~frequency=200., s) =
273
+ filter(freq=frequency, q=1., mode="high", s)
274
+ end
275
+
276
+ # Mix between dry and wet sources. Useful for testing effects. Typically:
277
+ # ```
278
+ # c = interactive.float("wetness", min=0., max=1., 1.)
279
+ # s = dry_wet(c, s, effect(s))
280
+ # ```
281
+ # and vary `c` to hear the difference between the source without and with
282
+ # the effect.
283
+ # @flag extra
284
+ # @category Source / Audio processing
285
+ # @param ~power If `true` use constant power mixing.
286
+ # @param wetness Wetness coefficient, from 0 (fully dry) to 1 (fully wet).
287
+ # @param dry Dry source.
288
+ # @param wet Wet source.
289
+ def dry_wet(~power=false, wetness, dry, wet) =
290
+ add(
291
+ power=power,
292
+ weights=[getter.map(fun (x) -> 1. - x, wetness), wetness],
293
+ [dry, wet]
294
+ )
295
+ end
296
+
297
+ # Generate DTMF tones.
298
+ # @flag extra
299
+ # @category Source / Sound synthesis
300
+ # @param ~duration Duration of a tone (in seconds).
301
+ # @param ~delay Dealy between two successive tones (in seconds).
302
+ # @param dtmf String describing DTMF tones to generates: it should contains characters 0 to 9, A to D, or * or #.
303
+ def replaces dtmf(~duration=0.1, ~delay=0.05, dtmf) =
304
+ l = ref([])
305
+ for i = 0 to string.bytes.length(dtmf) - 1 do
306
+ c = string.sub(encoding="ascii", dtmf, start=i, length=1)
307
+ let (row, col) =
308
+ if
309
+ c == "1"
310
+ then
311
+ (697., 1209.)
312
+ elsif
313
+ c == "2"
314
+ then
315
+ (697., 1336.)
316
+ elsif
317
+ c == "3"
318
+ then
319
+ (697., 1477.)
320
+ elsif
321
+ c == "A"
322
+ then
323
+ (697., 1633.)
324
+ elsif
325
+ c == "4"
326
+ then
327
+ (770., 1209.)
328
+ elsif
329
+ c == "5"
330
+ then
331
+ (770., 1336.)
332
+ elsif
333
+ c == "6"
334
+ then
335
+ (770., 1477.)
336
+ elsif
337
+ c == "B"
338
+ then
339
+ (770., 1633.)
340
+ elsif
341
+ c == "7"
342
+ then
343
+ (852., 1209.)
344
+ elsif
345
+ c == "8"
346
+ then
347
+ (852., 1336.)
348
+ elsif
349
+ c == "9"
350
+ then
351
+ (852., 1477.)
352
+ elsif
353
+ c == "C"
354
+ then
355
+ (852., 1633.)
356
+ elsif
357
+ c == "*"
358
+ then
359
+ (941., 1209.)
360
+ elsif
361
+ c == "0"
362
+ then
363
+ (941., 1336.)
364
+ elsif
365
+ c == "#"
366
+ then
367
+ (941., 1477.)
368
+ elsif
369
+ c == "D"
370
+ then
371
+ (941., 1633.)
372
+ else
373
+ (0., 0.)
374
+ end
375
+
376
+ s = add([sine(row, duration=duration), sine(col, duration=duration)])
377
+ l := blank(duration=delay)::l()
378
+ l := s::l()
379
+
380
+ end
381
+
382
+ l = list.rev(l())
383
+ sequence(l)
384
+ end
385
+
386
+ # Mixing table controllable via source methods and optional
387
+ # server/telnet commands.
388
+ # @flag extra
389
+ # @category Source / Audio processing
390
+ # @param ~id Force the value of the source ID.
391
+ # @param ~register_server_commands Register corresponding server commands
392
+ # @param ~normalize Normalize source's volume by the number of mixed sources.
393
+ def mix(~id=null, ~register_server_commands=true, ~normalize=false, sources) =
394
+ id = string.id.default(default="mixer", id)
395
+ inputs =
396
+ list.map(
397
+ fun (s) ->
398
+ begin
399
+ volume = ref(1.)
400
+ is_selected = ref(false)
401
+ is_single = ref(false)
402
+ {
403
+ volume = volume,
404
+ selected = is_selected,
405
+ single = is_single,
406
+ source = s
407
+ }
408
+ end,
409
+ sources
410
+ )
411
+
412
+ insert_metadata_fn = ref(fun (_) -> ())
413
+ sources =
414
+ list.map(
415
+ fun (input) ->
416
+ begin
417
+ s = amplify(input.volume, input.source)
418
+ s.on_track(
419
+ synchronous=true,
420
+ fun (_) -> if input.single() then input.selected := false end
421
+ )
422
+
423
+ s.on_metadata(
424
+ synchronous=true,
425
+ fun (m) ->
426
+ begin
427
+ fn = insert_metadata_fn()
428
+ fn(m)
429
+ end
430
+ )
431
+
432
+ switch([(input.selected, s)])
433
+ end,
434
+ inputs
435
+ )
436
+
437
+ s = add(normalize=normalize, sources)
438
+ let {metadata = _, ...tracks} = source.tracks(s)
439
+ s = source(tracks)
440
+ insert_metadata_fn := s.insert_metadata
441
+ let {track_marks = _, ...tracks} = source.tracks(s)
442
+ s = source(id=id, tracks)
443
+ if
444
+ register_server_commands
445
+ then
446
+ def status(input) =
447
+ "ready=#{source.is_ready(input.source)} selected=#{input.selected()} \
448
+ single=#{input.single()} volume=#{int_of_float(input.volume() * 100.)}% \
449
+ remaining=#{source.remaining(input.source)}"
450
+ end
451
+
452
+ s.register_command(
453
+ description="Skip current track on all enabled sources.",
454
+ "skip",
455
+ fun (_) ->
456
+ begin
457
+ list.iter(
458
+ fun (input) ->
459
+ if input.selected() then source.skip(input.source) end,
460
+ inputs
461
+ )
462
+
463
+ "OK"
464
+ end
465
+ )
466
+
467
+ s.register_command(
468
+ description="Set volume for a given source.",
469
+ usage="volume <source nb> <vol%>",
470
+ "volume",
471
+ fun (v) ->
472
+ begin
473
+ try
474
+ let [i, v] = r/\s/.split(v)
475
+ input = list.nth(inputs, int_of_string(i))
476
+ input.volume := float_of_string(v)
477
+ status(input)
478
+ catch _ do
479
+ "Usage: volume <source nb> <vol%>"
480
+ end
481
+ end
482
+ )
483
+
484
+ s.register_command(
485
+ description="Enable/disable a source.",
486
+ usage="select <source nb> <true|false>",
487
+ "select",
488
+ fun (arg) ->
489
+ begin
490
+ try
491
+ let [i, b] = r/\s/.split(arg)
492
+ input = list.nth(inputs, int_of_string(i))
493
+ input.selected := (b == "true")
494
+ status(input)
495
+ catch _ do
496
+ "Usage: select <source nb> <true|false>"
497
+ end
498
+ end
499
+ )
500
+
501
+ s.register_command(
502
+ description="Enable/disable automatic stop at the end of track.",
503
+ usage="single <source nb> <true|false>",
504
+ "single",
505
+ fun (arg) ->
506
+ begin
507
+ try
508
+ let [i, b] = r/\s/.split(arg)
509
+ input = list.nth(inputs, int_of_string(i))
510
+ input.single := (b == "true")
511
+ status(input)
512
+ catch _ do
513
+ "Usage: single <source nb> <true|false>"
514
+ end
515
+ end
516
+ )
517
+
518
+ s.register_command(
519
+ description="Display current status.",
520
+ "status",
521
+ fun (i) ->
522
+ begin
523
+ try
524
+ status(list.nth(inputs, int_of_string(i)))
525
+ catch _ do
526
+ "Usage: status <source nb>"
527
+ end
528
+ end
529
+ )
530
+
531
+ s.register_command(
532
+ description="Print the list of input sources.",
533
+ "inputs",
534
+ fun (_) ->
535
+ string.concat(
536
+ separator=" ",
537
+ list.map(fun (input) -> source.id(input.source), inputs)
538
+ )
539
+ )
540
+ end
541
+
542
+ s.{inputs = inputs}
543
+ end
544
+
545
+ # Indicate beats.
546
+ # @category Source / Sound synthesis
547
+ # @param ~frequency Frequency of the sound.
548
+ # @param bpm Number of beats per minute.
549
+ # @flag extra
550
+ def metronome(~frequency=440., bpm=60.) =
551
+ volume_down = 0.
552
+ beat_duration = 0.1
553
+ s = sine(frequency)
554
+
555
+ def f() =
556
+ if s.time() mod (60. / bpm) <= beat_duration then 1. else volume_down end
557
+ end
558
+
559
+ amplify(f, s)
560
+ end
561
+
562
+ # Mixes two streams, with faded transitions between the state when only the
563
+ # normal stream is available and when the special stream gets added on top of
564
+ # it.
565
+ # @category Source / Fade
566
+ # @flag extra
567
+ # @param ~duration Duration of the fade in seconds.
568
+ # @param ~p Portion of amplitude of the normal source in the mix.
569
+ # @param ~normal The normal source, which could be called the carrier too.
570
+ # @param ~special The special source.
571
+ def smooth_add(~duration=1., ~p=getter(0.2), ~normal, ~special) =
572
+ p = getter.function(p)
573
+ last_p = ref(p())
574
+
575
+ def c(fn, s) =
576
+ def v() =
577
+ fn = fn()
578
+ fn()
579
+ end
580
+
581
+ fade.scale(v, s)
582
+ end
583
+
584
+ special_volume = ref(fun () -> 0.)
585
+ special = c(special_volume, special)
586
+ normal_volume = ref(fun () -> 1.)
587
+ normal = c(normal_volume, normal)
588
+
589
+ def to_special({starting}) =
590
+ last_p := p()
591
+ q = 1. - last_p()
592
+ normal_volume := mkfade(start=1., stop=last_p(), duration=duration, normal)
593
+ special_volume := mkfade(stop=q, duration=duration, starting)
594
+ starting
595
+ end
596
+
597
+ def to_blank(x) =
598
+ b = x.starting
599
+ normal_volume := mkfade(start=last_p(), duration=duration, normal)
600
+ if
601
+ null.defined(x?.ending)
602
+ then
603
+ special_volume :=
604
+ mkfade(start=1. - last_p(), duration=duration, null.get(x?.ending))
605
+ sequence([null.get(x?.ending), b])
606
+ else
607
+ b
608
+ end
609
+ end
610
+
611
+ special =
612
+ fallback([special.{on_select = to_special}, blank().{on_select = to_blank}])
613
+
614
+ add(normalize=false, [normal, special])
615
+ end
616
+
617
+ %ifencoder %ffmpeg
618
+ # Output an MPEG-DASH playlist.
619
+ # @category Source / Output
620
+ # @flag extra
621
+ # @param ~id Force the value of the source ID.
622
+ # @param ~codec Codec to use for audio (following FFmpeg's conventions).
623
+ # @param ~fallible Allow the child source to fail, in which case the output will be (temporarily) stopped.
624
+ # @param ~start Automatically start outputting whenever possible. If true, an infallible (normal) output will start outputting as soon as it is created, and a fallible output will (re)start as soon as its source becomes available for streaming.
625
+ # @param ~playlist Playlist name
626
+ # @param ~directory Directory to write to
627
+ def output.file.dash(
628
+ ~id=null,
629
+ ~fallible=false,
630
+ ~codec="libmp3lame",
631
+ ~bitrate=128,
632
+ ~start=true,
633
+ ~playlist="stream.mpd",
634
+ ~directory,
635
+ s
636
+ ) =
637
+ enc = %ffmpeg(format = "dash", %audio(codec = codec, b = "#{bitrate}k"))
638
+ output.file(
639
+ id=id,
640
+ fallible=fallible,
641
+ start=start,
642
+ enc,
643
+ "#{(directory : string)}/#{playlist}",
644
+ s
645
+ )
646
+ end
647
+ %endif
648
+
649
+ # Return a source with a `set_volume` method.
650
+ # This method takes the same arguments as `mkfade`
651
+ # and updates the source volume with a corresponding
652
+ # fade curve
653
+ # @category Source / Audio processing
654
+ # @flag extra
655
+ # @param ~id Force the value of the source ID.
656
+ def smooth_volume(~id=null("smooth_volume"), s) =
657
+ last_volume = ref(1.)
658
+ volume_ref = ref(fun () -> 1.)
659
+
660
+ def volume() =
661
+ fn = volume_ref()
662
+ fn()
663
+ end
664
+
665
+ def set_volume(%argsof(mkfade[!start,!stop,!delay,!on_done]), v) =
666
+ volume_ref :=
667
+ mkfade(
668
+ %argsof(mkfade[!start,!stop,!delay,!on_done]),
669
+ start=last_volume(),
670
+ stop=v,
671
+ s
672
+ )
673
+ last_volume := v
674
+ end
675
+
676
+ amplify(id=id, volume, s).{set_volume = set_volume}
677
+ end