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,1139 @@
1
+ # @flag hidden
2
+ def settings.make.protocol(name) =
3
+ settings.make.void(
4
+ "Settings for the #{name} protocol"
5
+ )
6
+ end
7
+
8
+ let settings.protocol =
9
+ settings.make.void(
10
+ "Settings for registered protocols"
11
+ )
12
+
13
+ # Register the lufs_track_gain protocol.
14
+ # @flag hidden
15
+ def protocol.lufs_track_gain(~rlog:_, ~maxtime:_, arg) =
16
+ gain = file.lufs(arg)
17
+ if
18
+ null.defined(gain)
19
+ then
20
+ "annotate:#{settings.normalize_track_gain_metadata()}=\"#{
21
+ settings.lufs.track_gain_target() - null.get(gain)
22
+ } dB\":#{arg}"
23
+ else
24
+ arg
25
+ end
26
+ end
27
+
28
+ protocol.add(
29
+ "lufs_track_gain",
30
+ protocol.lufs_track_gain,
31
+ syntax="lufs_track_gain:uri",
32
+ doc="Compute LUFS track gain correction and add it as metadata"
33
+ )
34
+
35
+ # Register the replaygain protocol.
36
+ # @flag hidden
37
+ def protocol.replaygain(~rlog:_, ~maxtime:_, arg) =
38
+ gain = file.replaygain(arg)
39
+ tag = settings.normalize_track_gain_metadata()
40
+ if
41
+ null.defined(gain)
42
+ then
43
+ "annotate:#{tag}=\"#{null.get(gain)} dB\":#{arg}"
44
+ else
45
+ arg
46
+ end
47
+ end
48
+
49
+ protocol.add(
50
+ "replaygain",
51
+ protocol.replaygain,
52
+ syntax="replaygain:uri",
53
+ doc="Compute ReplayGain value. Adds returned value as \
54
+ `\"replaygain_track_gain\"` metadata"
55
+ )
56
+
57
+ let settings.protocol.process = settings.make.protocol("process")
58
+ let settings.protocol.process.env =
59
+ settings.make(
60
+ description="List of environment variables passed down to the executed \
61
+ process.",
62
+ []
63
+ )
64
+
65
+ let settings.protocol.process.inherit_env =
66
+ settings.make(
67
+ description="Inherit calling process's environment when `env` parameter is \
68
+ empty.",
69
+ true
70
+ )
71
+
72
+ let protocol.process = ()
73
+
74
+ # Parse process protocol arguments
75
+ # @flag hidden
76
+ def protocol.process.parse(~default_timeout, arg) =
77
+ let [args, ...uri] = r/:/.split(arg)
78
+ uri = string.concat(separator=":", uri)
79
+ args = r/,/.split(args)
80
+ let args =
81
+ if
82
+ string.contains(prefix="timeout=", list.hd(args))
83
+ then
84
+ let [timeout, extname, ...cmd] = args
85
+ timeout = string.residual(prefix="timeout=", timeout)
86
+ timeout = null.map(string.to_float, timeout) ?? default_timeout
87
+ timeout = min(default_timeout, timeout)
88
+ {timeout = timeout, extname = extname, cmd = cmd}
89
+ else
90
+ let [extname, ...cmd] = args
91
+ {timeout = default_timeout, extname = extname, cmd = cmd}
92
+ end
93
+
94
+ args.{uri = uri, cmd = string.concat(separator=",", args.cmd)}
95
+ end
96
+
97
+ # Register the process protocol. Syntax:
98
+ # process:[timeout=<seconds>],<output ext>,<cmd>:uri where `timeout` argument is optional and
99
+ # cannot exceed the underlying time and <cmd> is interpolated with:
100
+ # [("input",<input file>),("output",<output file>),("colon",":")]
101
+ # See say: protocol for an example.
102
+ # @flag hidden
103
+ def replaces protocol.process(~rlog:_, ~maxtime, arg) =
104
+ log.info(
105
+ "Processing #{arg}"
106
+ )
107
+ let {uri, timeout, extname, cmd} =
108
+ protocol.process.parse(default_timeout=maxtime - time(), arg)
109
+
110
+ output = file.temp("liq-process", ".#{extname}")
111
+
112
+ def resolve(input) =
113
+ cmd =
114
+ cmd %
115
+ [
116
+ ("input", process.quote(input)),
117
+ ("output", process.quote(output)),
118
+ ("colon", ":")
119
+ ]
120
+
121
+ log.info(
122
+ "Executing #{cmd}"
123
+ )
124
+ env_vars = settings.protocol.process.env()
125
+ env = environment()
126
+
127
+ def get_env(k) =
128
+ (k, env[k])
129
+ end
130
+
131
+ env = list.map(get_env, env_vars)
132
+ inherit_env = settings.protocol.process.inherit_env()
133
+ p = process.run(timeout=timeout, env=env, inherit_env=inherit_env, cmd)
134
+ if
135
+ p.status == "exit" and p.status.code == 0
136
+ then
137
+ output
138
+ else
139
+ log.important(
140
+ "Failed to execute #{cmd}: #{p.status} (#{p.status.code})"
141
+ )
142
+ log.info(
143
+ "Standard output:\n#{p.stdout}"
144
+ )
145
+ log.info(
146
+ "Error output:\n#{p.stderr}"
147
+ )
148
+ log.info(
149
+ "Removing #{output}."
150
+ )
151
+ file.remove(output)
152
+ null
153
+ end
154
+ end
155
+
156
+ if
157
+ uri == ""
158
+ then
159
+ resolve("")
160
+ else
161
+ r = request.create(uri)
162
+ delay = maxtime - time()
163
+ if
164
+ request.resolve(timeout=delay, r)
165
+ then
166
+ res = resolve(request.filename(r))
167
+ request.destroy(r)
168
+ res
169
+ else
170
+ log(
171
+ level=3,
172
+ "Failed to resolve #{uri}"
173
+ )
174
+ null
175
+ end
176
+ end
177
+ end
178
+
179
+ protocol.add(
180
+ temporary=true,
181
+ "process",
182
+ protocol.process,
183
+ doc="Resolve a request using an arbitrary process. `<cmd>` is interpolated \
184
+ with: `[(\"input\",<input>),(\"output\",<output>),(\"colon\",\":\")]`. `uri` \
185
+ is an optional child request, `<output>` is the name of a fresh temporary \
186
+ file and has extension `.<extname>`. `<input>` is an optional input file name \
187
+ as returned while resolving `uri`.",
188
+ syntax="process:<extname>,<cmd>[:uri]"
189
+ )
190
+
191
+ # Create a process: uri, replacing `:` with `$(colon)`.
192
+ # @category Liquidsoap
193
+ # @param cmd Command line to execute
194
+ # @param ~extname Output file extension (with no leading '.')
195
+ # @param ~uri Input uri
196
+ def process.uri(~timeout=null, ~extname, ~uri="", cmd) =
197
+ timeout = null.case(timeout, {""}, fun (t) -> "timeout=" ^ string(t) ^ ",")
198
+ cmd = r/:/g.replace(fun (_) -> "$(colon)", cmd)
199
+ uri = if uri != "" then ":#{uri}" else "" end
200
+ "process:#{timeout}#{extname},#{cmd}#{uri}"
201
+ end
202
+
203
+ # Resolve http(s) URLs using curl
204
+ # @flag hidden
205
+ def protocol.http(proto, ~rlog, ~maxtime, arg) =
206
+ uri = "#{proto}:#{arg}"
207
+
208
+ def log(~level, s) =
209
+ rlog(s)
210
+ log(label="procol.external", level=level, s)
211
+ end
212
+
213
+ timeout = maxtime - time()
214
+ ret = http.head(timeout=timeout, uri)
215
+ code = ret.status_code ?? 999
216
+ extname =
217
+ 200 <= code and code < 300 ? http.headers.extname(ret.headers) : null
218
+
219
+ extname =
220
+ if
221
+ null.defined(extname)
222
+ then
223
+ null.get(extname)
224
+ else
225
+ begin
226
+ content_type = http.headers.content_type(ret.headers)
227
+ extra_log =
228
+ if
229
+ null.defined(content_type) and null.get(content_type).mime != ""
230
+ then
231
+ begin
232
+ content_type = null.get(content_type).mime
233
+ " Response has unknown mime-type: #{string.quote(content_type)} \
234
+ you may want to add it to `settings.http.mime.extnames` and \
235
+ report to us if it is a common one."
236
+ end
237
+ else
238
+ ""
239
+ end
240
+
241
+ log(
242
+ level=3,
243
+ "Failed to find a file extension for #{string.quote(uri)}.#{
244
+ extra_log
245
+ }"
246
+ )
247
+
248
+ ".osb"
249
+ end
250
+ end
251
+
252
+ output = file.temp("liq-process", extname)
253
+ file_writer = file.write.stream(output)
254
+ timeout = maxtime - time()
255
+ try
256
+ response = http.get.stream(on_body_data=file_writer, timeout=timeout, uri)
257
+ if
258
+ response.status_code < 400
259
+ then
260
+ output
261
+ else
262
+ log(
263
+ level=3,
264
+ "Error while fetching http data: #{response.status_code} - #{
265
+ response.status_message
266
+ }"
267
+ )
268
+
269
+ null
270
+ end
271
+ catch err do
272
+ file_writer(null)
273
+ log(
274
+ level=3,
275
+ "Error while fetching http data: #{err}"
276
+ )
277
+ null
278
+ end
279
+ end
280
+
281
+ # Register download protocol.
282
+ # @flag hidden
283
+ def protocol.add.http(proto) =
284
+ def protocol.http(~rlog, ~maxtime, arg) =
285
+ protocol.http(proto, rlog=rlog, maxtime=maxtime, arg)
286
+ end
287
+
288
+ protocol.add(
289
+ temporary=true,
290
+ syntax="#{proto}://...",
291
+ doc="Download http URLs using curl",
292
+ proto,
293
+ protocol.http
294
+ )
295
+ end
296
+
297
+ list.iter(protocol.add.http, ["http", "https"])
298
+ let settings.protocol.youtube_dl = settings.make.protocol("youtube-dl")
299
+ let settings.protocol.youtube_dl.path =
300
+ settings.make(
301
+ description="Path of the youtube-dl (or yt-dlp) binary.",
302
+ "yt-dlp"
303
+ )
304
+
305
+ let settings.protocol.youtube_dl.timeout =
306
+ settings.make(
307
+ description="Timeout (in seconds) for youtube-dl executions.",
308
+ 300.
309
+ )
310
+
311
+ # Register the youtube-dl protocol, using youtube-dl.
312
+ # Syntax: youtube-dl:<ID>
313
+ # @flag hidden
314
+ def protocol.youtube_dl(~rlog, ~maxtime, arg) =
315
+ binary = settings.protocol.youtube_dl.path()
316
+ timeout = settings.protocol.youtube_dl.timeout()
317
+
318
+ def log(~level, s) =
319
+ rlog(s)
320
+ log(label="protocol.youtube-dl", level=level, s)
321
+ end
322
+
323
+ delay = maxtime - time()
324
+ cmd =
325
+ "#{binary} --get-title --get-filename -- #{process.quote(arg)}"
326
+ log(
327
+ level=4,
328
+ "Executing #{cmd}"
329
+ )
330
+ x = process.read.lines(timeout=delay, cmd)
331
+ x = if list.length(x) >= 2 then x else ["", ".osb"] end
332
+ title = list.hd(default="", x)
333
+ ext = file.extension(leading_dot=false, list.nth(default="", x, 1))
334
+ cmd =
335
+ "#{binary} -q -f best --no-continue --no-playlist -o $(output) -- #{
336
+ process.quote(arg)
337
+ }"
338
+
339
+ cmd = process.uri(timeout=timeout, extname=ext, cmd)
340
+ if
341
+ title != ""
342
+ then
343
+ "annotate:title=#{string.quote(title)}:#{cmd}"
344
+ else
345
+ cmd
346
+ end
347
+ end
348
+
349
+ protocol.add(
350
+ "youtube-dl",
351
+ protocol.youtube_dl,
352
+ doc="Resolve a request using youtube-dl.",
353
+ syntax="youtube-dl:uri"
354
+ )
355
+
356
+ # Register the youtube-pl protocol.
357
+ # Syntax: youtube-pl:<ID>
358
+ # @flag hidden
359
+ def protocol.youtube_pl(~rlog:_, ~maxtime, arg) =
360
+ binary = settings.protocol.youtube_dl.path()
361
+ delay = maxtime - time()
362
+ cmd =
363
+ "#{binary} -i -s --get-id --flat-playlist -- #{process.quote(arg)}"
364
+ log(
365
+ level=4,
366
+ "Executing #{cmd}"
367
+ )
368
+ l = process.read.lines(timeout=delay, cmd)
369
+ l = list.map(fun (s) -> "youtube-dl:https://www.youtube.com/watch?v=" ^ s, l)
370
+ l = string.concat(separator="\n", l) ^ "\n"
371
+ tmp = file.temp("youtube-pl", "")
372
+ file.write(data=l, tmp)
373
+ tmp
374
+ end
375
+
376
+ protocol.add(
377
+ "youtube-pl",
378
+ protocol.youtube_pl,
379
+ doc="Resolve a request as a youtube playlist using youtube-dl. You typically \
380
+ want to use this as `playlist(\"youtube-pl:...\")`.",
381
+ temporary=true,
382
+ syntax="youtube-pl:uri"
383
+ )
384
+
385
+ # Register tmp
386
+ # @flag hidden
387
+ def protocol.tmp(~rlog, ~maxtime, arg) =
388
+ r = request.create(arg)
389
+ delay = maxtime - time()
390
+ if
391
+ request.resolve(timeout=delay, r)
392
+ then
393
+ request.filename(r)
394
+ else
395
+ rlog(
396
+ "Failed to resolve #{arg}"
397
+ )
398
+ log(
399
+ level=3,
400
+ "Failed to resolve #{arg}"
401
+ )
402
+ null
403
+ end
404
+ end
405
+
406
+ protocol.add(
407
+ "tmp",
408
+ protocol.tmp,
409
+ doc="Mark the given uri as temporary. Useful when chaining protocols",
410
+ temporary=true,
411
+ syntax="tmp:uri"
412
+ )
413
+
414
+ # Register fallible
415
+ # @flag hidden
416
+ def protocol.fallible(~rlog:_, ~maxtime:_, arg) =
417
+ arg
418
+ end
419
+
420
+ protocol.add(
421
+ "fallible",
422
+ protocol.fallible,
423
+ doc="Mark the given uri as being fallible. This can be used to prevent a \
424
+ request or source from being resolved once and for all and considered \
425
+ infallible for the duration of the script, typically when debugging.",
426
+ static=fun (_) -> false,
427
+ syntax="fallible:uri"
428
+ )
429
+
430
+ let settings.protocol.ffmpeg = settings.make.protocol("FFmpeg")
431
+ let settings.protocol.ffmpeg.path =
432
+ settings.make(
433
+ description="Path to the ffmpeg binary",
434
+ "ffmpeg"
435
+ )
436
+
437
+ let settings.protocol.ffmpeg.metadata =
438
+ settings.make(
439
+ description="Should the protocol extract metadata",
440
+ true
441
+ )
442
+
443
+ let settings.protocol.ffmpeg.replaygain =
444
+ settings.make(
445
+ description="Should the protocol adjust ReplayGain",
446
+ false
447
+ )
448
+
449
+ # Register ffmpeg
450
+ # @flag hidden
451
+ def protocol.ffmpeg(~rlog, ~maxtime, arg) =
452
+ ffmpeg = settings.protocol.ffmpeg.path()
453
+ metadata = settings.protocol.ffmpeg.metadata()
454
+ replaygain = settings.protocol.ffmpeg.replaygain()
455
+
456
+ def log(~level, s) =
457
+ rlog(s)
458
+ log(label="protocol.ffmpeg", level=level, s)
459
+ end
460
+
461
+ def annotate(m) =
462
+ def f(x) =
463
+ let (key, value) = x
464
+ "#{key}=#{string.quote(value)}"
465
+ end
466
+
467
+ m = string.concat(separator=",", list.map(f, m))
468
+ if string.bytes.length(m) > 0 then "annotate:#{m}:" else "" end
469
+ end
470
+
471
+ def parse_metadata(file) =
472
+ cmd =
473
+ "#{ffmpeg} -i #{process.quote(file)} -f ffmetadata - 2>/dev/null | grep -v \
474
+ '^;'"
475
+
476
+ delay = maxtime - time()
477
+ log(
478
+ level=4,
479
+ "Executing #{cmd}"
480
+ )
481
+ lines = process.read.lines(timeout=delay, cmd)
482
+
483
+ def f(cur, line) =
484
+ m = r/=/.split(line)
485
+ if
486
+ list.length(m) >= 2
487
+ then
488
+ key = list.hd(default="", m)
489
+ value = string.concat(separator="=", list.tl(m))
490
+ (key, value)::cur
491
+ else
492
+ cur
493
+ end
494
+ end
495
+
496
+ list.fold(f, [], lines)
497
+ end
498
+
499
+ def replaygain_filter(fname) =
500
+ if
501
+ replaygain
502
+ then
503
+ gain = file.replaygain(fname)
504
+ if
505
+ null.defined(gain)
506
+ then
507
+ "-af \"volume=#{null.get(gain)} dB\""
508
+ else
509
+ ""
510
+ end
511
+ else
512
+ ""
513
+ end
514
+ end
515
+
516
+ def cue_points(m) =
517
+ cue_in =
518
+ float_of_string(default=0., list.assoc(default="0.", "liq_cue_in", m))
519
+
520
+ cue_out =
521
+ float_of_string(default=0., list.assoc(default="", "liq_cue_out", m))
522
+
523
+ args =
524
+ if
525
+ cue_in > 0.
526
+ then
527
+ "-ss #{cue_in}"
528
+ else
529
+ ""
530
+ end
531
+ if
532
+ cue_out > cue_in
533
+ then
534
+ "#{args} -t #{cue_out - cue_in}"
535
+ else
536
+ args
537
+ end
538
+ end
539
+
540
+ def fades(r) =
541
+ m = request.metadata(r)
542
+ fade_type = list.assoc(default="", "liq_fade_type", m)
543
+ fade_in = list.assoc(default="", "liq_fade_in", m)
544
+ cue_in = list.assoc(default="", "liq_cue_in", m)
545
+ fade_out = list.assoc(default="", "liq_fade_out", m)
546
+ cue_out = list.assoc(default="", "liq_cue_out", m)
547
+ curve =
548
+ if
549
+ fade_type == "lin"
550
+ then
551
+ ":curve=tri"
552
+ elsif
553
+ fade_type == "sin"
554
+ then
555
+ ":curve=qsin"
556
+ elsif
557
+ fade_type == "log"
558
+ then
559
+ ":curve=log"
560
+ elsif
561
+ fade_type == "exp"
562
+ then
563
+ ":curve=exp"
564
+ else
565
+ ""
566
+ end
567
+
568
+ args =
569
+ if
570
+ fade_in != ""
571
+ then
572
+ fade_in = float_of_string(default=0., fade_in)
573
+ start_time =
574
+ if cue_in != "" then float_of_string(default=0., cue_in) else 0. end
575
+
576
+ if
577
+ fade_in > 0.
578
+ then
579
+ ["afade=in:st=#{start_time}:d=#{fade_in}#{curve}"]
580
+ else
581
+ []
582
+ end
583
+ else
584
+ []
585
+ end
586
+
587
+ args =
588
+ if
589
+ fade_out != ""
590
+ then
591
+ fade_out = float_of_string(default=0., fade_out)
592
+ end_time =
593
+ if
594
+ cue_out != ""
595
+ then
596
+ float_of_string(default=0., cue_out)
597
+ else
598
+ null.get(request.duration(request.filename(r)))
599
+ end
600
+
601
+ if
602
+ fade_out > 0.
603
+ then
604
+ list.append(
605
+ args,
606
+ ["afade=out:st=#{end_time - fade_out}:d=#{fade_out}#{curve}"]
607
+ )
608
+ else
609
+ args
610
+ end
611
+ else
612
+ args
613
+ end
614
+
615
+ if
616
+ list.length(args) > 0
617
+ then
618
+ args = string.concat(separator=",", args)
619
+ "-af #{args}"
620
+ else
621
+ ""
622
+ end
623
+ end
624
+
625
+ r = request.create(arg)
626
+ delay = maxtime - time()
627
+ if
628
+ request.resolve(timeout=delay, r)
629
+ then
630
+ filename = request.filename(r)
631
+ m = request.metadata(r)
632
+ m = if metadata then list.append(m, parse_metadata(filename)) else m end
633
+ annotate = annotate(m)
634
+ request.destroy(r)
635
+
636
+ # Now parse the audio
637
+ wav = file.temp("liq-process", ".wav")
638
+ cue_points = cue_points(request.metadata(r))
639
+ fades = fades(r)
640
+ replaygain_filter = replaygain_filter(filename)
641
+ cmd =
642
+ "#{ffmpeg} -y -i $(input) #{cue_points} #{fades} #{replaygain_filter} #{
643
+ process.quote(wav)
644
+ }"
645
+
646
+ uri = process.uri(extname="wav", uri=filename, cmd)
647
+ wav_r = request.create(uri)
648
+ delay = maxtime - time()
649
+ if
650
+ request.resolve(timeout=delay, wav_r)
651
+ then
652
+ request.destroy(wav_r)
653
+ "#{annotate}tmp:#{wav}"
654
+ else
655
+ log(
656
+ level=3,
657
+ "Failed to resolve #{uri}"
658
+ )
659
+ null
660
+ end
661
+ else
662
+ log(
663
+ level=3,
664
+ "Failed to resolve #{arg}"
665
+ )
666
+ null
667
+ end
668
+ end
669
+
670
+ protocol.add(
671
+ "ffmpeg",
672
+ protocol.ffmpeg,
673
+ doc="Decode any file to wave using ffmpeg",
674
+ syntax="ffmpeg:uri"
675
+ )
676
+
677
+ # Register stereo protocol which converts a file to stereo (currently decodes as
678
+ # wav).
679
+ # @flag hidden
680
+ def protocol.stereo(~rlog:_, ~maxtime:_, arg) =
681
+ file = file.temp("liq-stereo", ".wav")
682
+ r = request.create(arg)
683
+ if
684
+ not request.resolve(r)
685
+ then
686
+ log.info(
687
+ "Stereo: failed to resolve request #{arg}"
688
+ )
689
+ null
690
+ else
691
+ request.dump(%wav, file, request.create(arg))
692
+ file
693
+ end
694
+ end
695
+
696
+ protocol.add(
697
+ static=fun (_) -> true,
698
+ temporary=true,
699
+ "stereo",
700
+ protocol.stereo,
701
+ doc="Convert a file to stereo (currently decodes to wav).",
702
+ syntax="stereo:<uri>"
703
+ )
704
+
705
+ # Copy
706
+
707
+ # @flag hidden
708
+ def protocol.copy(~rlog:_, ~maxtime:_, arg) =
709
+ extname = file.extension(arg)
710
+ tmpfile = file.temp("tmp", extname)
711
+ file.copy(force=true, arg, tmpfile)
712
+ tmpfile
713
+ end
714
+
715
+ protocol.add(
716
+ static=fun (_) -> true,
717
+ "copy",
718
+ protocol.copy,
719
+ doc="Copy file to a temporary destination",
720
+ syntax="copy:/path/to/file.extname"
721
+ )
722
+
723
+ # Text2wave
724
+ # @category Settings
725
+ let settings.protocol.text2wave = settings.make.protocol("text2wave")
726
+ let settings.protocol.text2wave.path =
727
+ settings.make(
728
+ description="Path to the text2wave binary",
729
+ "text2wave"
730
+ )
731
+
732
+ # Register the text2wave: protocol using text2wave
733
+ # @flag hidden
734
+ def protocol.text2wave(~rlog:_, ~maxtime:_, arg) =
735
+ binary = settings.protocol.text2wave.path()
736
+ process.uri(
737
+ extname="wav",
738
+ "echo #{process.quote(arg)} | #{binary} -scale 1.9 > $(output)"
739
+ )
740
+ end
741
+
742
+ protocol.add(
743
+ static=fun (_) -> true,
744
+ "text2wave",
745
+ protocol.text2wave,
746
+ doc="Generate speech synthesis using text2wave. Result may be mono.",
747
+ syntax="text2wave:Text to read"
748
+ )
749
+
750
+ # Pico2wave
751
+ # @category Settings
752
+ let settings.protocol.pico2wave = settings.make.protocol("pico2wave")
753
+ let settings.protocol.pico2wave.path =
754
+ settings.make(
755
+ description="Path to the pico2wave binary",
756
+ "pico2wave"
757
+ )
758
+
759
+ let settings.protocol.pico2wave.lang =
760
+ settings.make(
761
+ description="pico2wave language. One of: `\"en-US\"`, `\"en-GB\"`, \
762
+ `\"es-ES\"`, `\"de-DE\"`, `\"fr-FR\"` or `\"it-IT\"`.",
763
+ "en-US"
764
+ )
765
+
766
+ # @flag hidden
767
+ def protocol.pico2wave(~rlog:_, ~maxtime:_, arg) =
768
+ binary = settings.protocol.pico2wave.path()
769
+ lang = settings.protocol.pico2wave.lang()
770
+ process.uri(
771
+ extname="wav",
772
+ "#{binary} -l #{lang} -w $(output) #{process.quote(arg)}"
773
+ )
774
+ end
775
+
776
+ protocol.add(
777
+ static=fun (_) -> true,
778
+ "pico2wave",
779
+ protocol.pico2wave,
780
+ doc="Generate speech synthesis using pico2wave. Result may be mono.",
781
+ syntax="pico2wave:Text to read"
782
+ )
783
+
784
+ # GTTS
785
+ # @category Settings
786
+ let settings.protocol.gtts = settings.make.protocol("gtts")
787
+ let settings.protocol.gtts.path =
788
+ settings.make(
789
+ description="Path to the gtts binary",
790
+ "gtts-cli"
791
+ )
792
+
793
+ let settings.protocol.gtts.lang =
794
+ settings.make(
795
+ description="Language to speak in.",
796
+ "en"
797
+ )
798
+
799
+ let settings.protocol.gtts.options =
800
+ settings.make(
801
+ description="Command line options.",
802
+ ""
803
+ )
804
+
805
+ # Register the gtts: protocol using gtts
806
+ # @flag hidden
807
+ def protocol.gtts(~rlog:_, ~maxtime:_, arg) =
808
+ binary = settings.protocol.gtts.path()
809
+ lang = settings.protocol.gtts.lang()
810
+ options = settings.protocol.gtts.options()
811
+ process.uri(
812
+ extname="mp3",
813
+ "#{binary} --lang #{lang} #{options} -o $(output) #{process.quote(arg)}"
814
+ )
815
+ end
816
+
817
+ protocol.add(
818
+ static=fun (_) -> true,
819
+ "gtts",
820
+ protocol.gtts,
821
+ doc="Generate speech synthesis using Google translate's text-to-speech API. \
822
+ This requires the `gtts-cli` binary. Result may be mono.",
823
+ syntax="gtts:Text to read"
824
+ )
825
+
826
+ # MacOS say
827
+ # @category Settings
828
+ let settings.protocol.macos_say = settings.make.protocol("macos_say")
829
+ let settings.protocol.macos_say.path =
830
+ settings.make(
831
+ description="Path to the say binary",
832
+ "say"
833
+ )
834
+
835
+ let settings.protocol.macos_say.options =
836
+ settings.make(
837
+ description="Command line options.",
838
+ ""
839
+ )
840
+
841
+ # Register the macos_say: protocol using the say command available on macos
842
+ # @flag hidden
843
+ def protocol.macos_say(~rlog:_, ~maxtime:_, arg) =
844
+ binary = settings.protocol.macos_say.path()
845
+ options = settings.protocol.macos_say.options()
846
+ process.uri(
847
+ extname="aiff",
848
+ "#{binary} #{options} -o $(output) #{process.quote(arg)}"
849
+ )
850
+ end
851
+
852
+ protocol.add(
853
+ static=fun (_) -> true,
854
+ "macos_say",
855
+ protocol.macos_say,
856
+ doc="Generate speech synthesis using the `say` command available on macos.",
857
+ syntax="macos_say:Text to read"
858
+ )
859
+
860
+ # Say
861
+ # @category Settings
862
+ let settings.protocol.say = settings.make.protocol("say")
863
+ let settings.protocol.say.implementation =
864
+ settings.make(
865
+ description="Implementation to use. One of: \"pico2wave\", \"gtts\", \
866
+ \"text2wave\" or \"macos_say\".",
867
+ liquidsoap.build_config.system == "macosx" ? "macos_say" : "pico2wave"
868
+ )
869
+
870
+ # Register the legacy say: protocol
871
+ # @flag hidden
872
+ def protocol.say(~rlog:_, ~maxtime:_, arg) =
873
+ "#{settings.protocol.say.implementation()}:#{arg}"
874
+ end
875
+
876
+ protocol.add(
877
+ static=fun (_) -> true,
878
+ "say",
879
+ protocol.say,
880
+ doc="Generate speech synthesis using text2wave. Result is always stereo.",
881
+ syntax="say:Text to read"
882
+ )
883
+
884
+ let settings.protocol.aws = settings.make.protocol("AWS")
885
+ let settings.protocol.aws.profile =
886
+ settings.make(
887
+ description="Use a specific profile from your credential file.",
888
+ null
889
+ )
890
+
891
+ let settings.protocol.aws.endpoint =
892
+ settings.make(
893
+ description="Alternative endpoint URL (useful for other S3 \
894
+ implementations).",
895
+ null
896
+ )
897
+
898
+ let settings.protocol.aws.region =
899
+ settings.make(
900
+ description="AWS Region",
901
+ null
902
+ )
903
+
904
+ let settings.protocol.aws.path =
905
+ settings.make(
906
+ description="Path to aws CLI binary",
907
+ "aws"
908
+ )
909
+
910
+ let settings.protocol.aws.polly = settings.make.protocol("polly")
911
+ let settings.protocol.aws.polly.format =
912
+ settings.make(
913
+ description="Output format",
914
+ "mp3"
915
+ )
916
+
917
+ let settings.protocol.aws.polly.voice =
918
+ settings.make(
919
+ description="Voice ID",
920
+ "Joanna"
921
+ )
922
+
923
+ let settings.protocol.aws.polly.extra_args =
924
+ settings.make(
925
+ description="Extra command line arguments",
926
+ ([] : [string])
927
+ )
928
+
929
+ # Build a aws base call
930
+ # @flag hidden
931
+ def aws_base() =
932
+ aws = settings.protocol.aws.path()
933
+ region = settings.protocol.aws.region()
934
+ aws =
935
+ if
936
+ null.defined(region)
937
+ then
938
+ "#{aws} --region #{null.get(region)}"
939
+ else
940
+ aws
941
+ end
942
+
943
+ endpoint = settings.protocol.aws.endpoint()
944
+ aws =
945
+ if
946
+ null.defined(endpoint)
947
+ then
948
+ "#{aws} --endpoint-url #{process.quote(null.get(endpoint))}"
949
+ else
950
+ aws
951
+ end
952
+
953
+ profile = settings.protocol.aws.profile()
954
+ if
955
+ null.defined(profile)
956
+ then
957
+ "#{aws} --profile #{process.quote(null.get(profile))}"
958
+ else
959
+ aws
960
+ end
961
+ end
962
+
963
+ # Register the s3:// protocol
964
+ # @flag hidden
965
+ def s3_protocol(~rlog:_, ~maxtime:_, arg) =
966
+ extname = file.extension(leading_dot=false, dir_sep="/", arg)
967
+ arg = process.quote("s3:#{arg}")
968
+ process.uri(
969
+ extname=extname,
970
+ "#{aws_base()} s3 cp #{arg} $(output)"
971
+ )
972
+ end
973
+
974
+ protocol.add(
975
+ "s3",
976
+ s3_protocol,
977
+ doc="Fetch files from s3 using the AWS CLI",
978
+ syntax="s3://uri"
979
+ )
980
+
981
+ # Register the polly: protocol using AWS Polly
982
+ # speech synthesis services. Syntax: polly:<text>
983
+ # @flag hidden
984
+ def polly_protocol(~rlog:_, ~maxtime:_, text) =
985
+ aws = aws_base()
986
+ format = settings.protocol.aws.polly.format()
987
+ extname =
988
+ if
989
+ format == "mp3"
990
+ then
991
+ "mp3"
992
+ elsif
993
+ format == "ogg_vorbis"
994
+ then
995
+ "ogg"
996
+ else
997
+ "wav"
998
+ end
999
+
1000
+ aws =
1001
+ "#{aws} polly synthesize-speech --output-format #{format}"
1002
+ voice_id = settings.protocol.aws.polly.voice()
1003
+ extra_args =
1004
+ string.concat(
1005
+ separator=" ",
1006
+ settings.protocol.aws.polly.extra_args()
1007
+ )
1008
+ cmd =
1009
+ "#{aws} --text #{process.quote(text)} --voice-id #{
1010
+ process.quote(voice_id)
1011
+ } #{extra_args} $(output)"
1012
+
1013
+ process.uri(extname=extname, cmd)
1014
+ end
1015
+
1016
+ protocol.add(
1017
+ static=fun (_) -> true,
1018
+ "polly",
1019
+ polly_protocol,
1020
+ doc="Generate speech synthesis using AWS polly service. Result might be mono, \
1021
+ needs aws binary in the path.",
1022
+ syntax="polly:Text to read"
1023
+ )
1024
+
1025
+ # Protocol to synthesize audio.
1026
+ # @flag hidden
1027
+ def synth_protocol(~rlog:_, ~maxtime:_, text) =
1028
+ log.debug(
1029
+ label="synth",
1030
+ "Synthesizing request: #{text}"
1031
+ )
1032
+ args = r/,/.split(text)
1033
+ args = list.map(r/=/.split, args)
1034
+ if
1035
+ list.exists(fun (l) -> list.length(l) != 2, args)
1036
+ then
1037
+ null
1038
+ else
1039
+ args =
1040
+ list.map(
1041
+ fun (l) -> (list.hd(default="", l), list.hd(default="", list.tl(l))),
1042
+ args
1043
+ )
1044
+
1045
+ shape = ref("sine")
1046
+ duration = ref(10.)
1047
+ frequency = ref(440.)
1048
+
1049
+ def set(p) =
1050
+ let (k, v) = p
1051
+ if
1052
+ k == "d" or k == "duration"
1053
+ then
1054
+ duration := float_of_string(v)
1055
+ elsif
1056
+ k == "f" or k == "freq" or k == "frequency"
1057
+ then
1058
+ frequency := float_of_string(v)
1059
+ elsif
1060
+ k == "s" or k == "shape"
1061
+ then
1062
+ shape := v
1063
+ end
1064
+ end
1065
+
1066
+ list.iter(set, args)
1067
+
1068
+ def synth(s) =
1069
+ file = file.temp("liq-synth", ".wav")
1070
+ log.info(
1071
+ label="synth",
1072
+ "Synthesizing #{shape()} in #{file}."
1073
+ )
1074
+
1075
+ clock.assign_new(sync="passive", [s])
1076
+
1077
+ stopped = ref(false)
1078
+ o = output.file(fallible=true, %wav, file, once(s))
1079
+ o.on_stop(synchronous=true, {stopped.set(true)})
1080
+
1081
+ c = clock(s.clock)
1082
+ c.start()
1083
+ while not stopped() do c.tick() end
1084
+ c.stop()
1085
+
1086
+ file
1087
+ end
1088
+
1089
+ if
1090
+ shape() == "sine"
1091
+ then
1092
+ synth(sine(duration=duration(), frequency()))
1093
+ elsif
1094
+ shape() == "saw"
1095
+ then
1096
+ synth(saw(duration=duration(), frequency()))
1097
+ elsif
1098
+ shape() == "square"
1099
+ then
1100
+ synth(square(duration=duration(), frequency()))
1101
+ elsif
1102
+ shape() == "blank"
1103
+ then
1104
+ synth(blank(duration=duration()))
1105
+ else
1106
+ null
1107
+ end
1108
+ end
1109
+ end
1110
+
1111
+ protocol.add(
1112
+ static=fun (_) -> true,
1113
+ temporary=true,
1114
+ "synth",
1115
+ synth_protocol,
1116
+ doc="Synthesize audio. Parameters are optional.",
1117
+ syntax="synth:shape=sine,frequency=440.,duration=10."
1118
+ )
1119
+
1120
+ # File protocol
1121
+ # @flag hidden
1122
+ def file_protocol(~rlog:_, ~maxtime:_, arg) =
1123
+ if
1124
+ not r/^file:/.test(arg)
1125
+ then
1126
+ null
1127
+ else
1128
+ url.decode(r/^file:(?:\/\/)?/.replace(fun (_) -> "", arg))
1129
+ end
1130
+ end
1131
+
1132
+ protocol.add(
1133
+ static=fun (_) -> true,
1134
+ temporary=false,
1135
+ "file",
1136
+ file_protocol,
1137
+ doc="File protocol. Only local files are supported",
1138
+ syntax="file:///path/to/file"
1139
+ )