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,1081 @@
1
+ # Initialize settings for autocue
2
+ # @category Settings
3
+ let settings.autocue = {internal = ()}
4
+
5
+ let settings.autocue.implementations =
6
+ settings.make(
7
+ description="All available autocue implementations",
8
+ []
9
+ )
10
+
11
+ let settings.autocue.metadata = ()
12
+
13
+ let settings.autocue.metadata.priority =
14
+ settings.make(
15
+ description="Priority for the autocue metadata resolver. Default value \
16
+ allows it to override both file and request metadata.",
17
+ 10
18
+ )
19
+
20
+ let settings.autocue.preferred =
21
+ settings.make(
22
+ description="Preferred autocue",
23
+ "internal"
24
+ )
25
+
26
+ let settings.autocue.amplify_behavior =
27
+ settings.make(
28
+ description="How to proceed with loudness adjustment. Set to `\"override\"` to always prefer
29
+ the value provided by the `autocue` provider. Set to `\"ignore\"` to ignore all
30
+ loudness correction provided via the `autocue` provider. Set to
31
+ `\"keep\"` to always prefer user-provided values (via request annotation or file tags)
32
+ over values provided by the `autocue` provider.",
33
+ "override"
34
+ )
35
+
36
+ let settings.autocue.amplify_aliases =
37
+ settings.make(
38
+ description="List of metadata to treat as amplify aliases when applying the \
39
+ `amplify_behavior` policy.",
40
+ ["replaygain_track_gain"]
41
+ )
42
+
43
+ let settings.autocue.internal.metadata_override =
44
+ settings.make(
45
+ description="Disable processing when one of these metadata is found",
46
+ [
47
+ "liq_cue_in",
48
+ "liq_cue_out",
49
+ "liq_fade_in",
50
+ "liq_fade_in_delay",
51
+ "liq_fade_out",
52
+ "liq_fade_out_delay",
53
+ "liq_disable_autocue"
54
+ ]
55
+ )
56
+
57
+ let settings.autocue.internal.lufs_target =
58
+ settings.make(
59
+ description="Loudness target",
60
+ -14.0
61
+ )
62
+
63
+ let settings.autocue.internal.cue_in_threshold =
64
+ settings.make(
65
+ description="Cue in threshold",
66
+ -34.0
67
+ )
68
+
69
+ let settings.autocue.internal.cue_out_threshold =
70
+ settings.make(
71
+ description="Cue out threshold",
72
+ -42.0
73
+ )
74
+
75
+ let settings.autocue.internal.cross_threshold =
76
+ settings.make(
77
+ description="Crossfade start threshold",
78
+ -7.0
79
+ )
80
+
81
+ let settings.autocue.internal.max_overlap =
82
+ settings.make(
83
+ description="Maximum allowed overlap/crossfade in seconds",
84
+ 6.0
85
+ )
86
+
87
+ let settings.autocue.internal.sustained_endings_enabled =
88
+ settings.make(
89
+ description="Try to optimize crossfade point on sustained endings",
90
+ true
91
+ )
92
+
93
+ let settings.autocue.internal.sustained_endings_dropoff =
94
+ settings.make(
95
+ description="Max. loudness drop off immediately after crossfade point to \
96
+ consider it as relevant ending [percentage]",
97
+ 15.0
98
+ )
99
+
100
+ let settings.autocue.internal.sustained_endings_slope =
101
+ settings.make(
102
+ description="Max. loudness difference between crossfade point and cue out to \
103
+ consider it as relevant ending [percentage]",
104
+ 20.0
105
+ )
106
+
107
+ let settings.autocue.internal.sustained_endings_min_duration =
108
+ settings.make(
109
+ description="Minimum duration to consider it the ending as sustained \
110
+ [seconds]",
111
+ 1.0
112
+ )
113
+
114
+ let settings.autocue.internal.sustained_endings_threshold_limit =
115
+ settings.make(
116
+ description="Max reduction of dB thresholds compared to initial value \
117
+ [multiplying factor]",
118
+ 2.0
119
+ )
120
+
121
+ let settings.autocue.internal.ratio =
122
+ settings.make(
123
+ description="Maximum real time ratio to control speed of LUFS data analysis",
124
+ 70.
125
+ )
126
+
127
+ let settings.autocue.internal.timeout =
128
+ settings.make(
129
+ description="Maximum allowed processing time (estimated)",
130
+ 10.
131
+ )
132
+
133
+ let autocue = {internal = ()}
134
+
135
+ # Register an `autocue` implementation.
136
+ # @category Source / Audio processing
137
+ # @param ~name Name of the implementation
138
+ def autocue.register(~name, fn) =
139
+ current_implementations = settings.autocue.implementations()
140
+ if
141
+ list.assoc.mem(name, current_implementations)
142
+ then
143
+ error.raise(
144
+ error.invalid,
145
+ "Autocue implementation #{name} already exists!"
146
+ )
147
+ end
148
+ settings.autocue.implementations := [(name, fn), ...current_implementations]
149
+ end
150
+
151
+ # Get frames from ffmpeg.filter.ebur128
152
+ # @flag hidden
153
+ def autocue.internal.ebur128(~duration, ~ratio=50., ~timeout=10., filename) =
154
+ ignore(ratio)
155
+ ignore(timeout)
156
+ ignore(filename)
157
+ ignore(duration)
158
+ %ifdef ffmpeg.filter.ebur128
159
+ estimated_processing_time = duration / ratio
160
+
161
+ if
162
+ estimated_processing_time > timeout or duration <= 0.
163
+ then
164
+ log(
165
+ level=2,
166
+ label="autocue.internal",
167
+ "Estimated processing duration is too long, autocue disabled! #{
168
+ duration
169
+ } / #{ratio} = #{estimated_processing_time} (Duration / Ratio = Processing \
170
+ duration; max. allowed: #{timeout})"
171
+ )
172
+ []
173
+ else
174
+ r = request.create(resolve_metadata=false, filename)
175
+ frames = ref([])
176
+
177
+ def process(s) =
178
+ def ebur128(s) =
179
+ def mk_filter(graph) =
180
+ let {audio = a} = source.tracks(s)
181
+ a = ffmpeg.filter.audio.input(graph, a)
182
+ let ([a], _) = ffmpeg.filter.ebur128(metadata=true, graph, a)
183
+
184
+ # ebur filter seems to generate invalid PTS.
185
+ a = ffmpeg.filter.asetpts(expr="N/SR/TB", graph, a)
186
+ a = ffmpeg.filter.audio.output(id="filter_output", graph, a)
187
+ source({audio = a, metadata = track.metadata(a)})
188
+ end
189
+
190
+ ffmpeg.filter.create(mk_filter)
191
+ end
192
+
193
+ s = ebur128(s)
194
+ s.on_metadata(synchronous=true, fun (m) -> frames := [...frames(), m])
195
+ s
196
+ end
197
+
198
+ request.process(ratio=ratio, process=process, r)
199
+
200
+ frames()
201
+ end
202
+ %else
203
+ ignore(ratio)
204
+ ignore(timeout)
205
+ ignore(filename)
206
+ log(
207
+ level=2,
208
+ label="autocue.internal",
209
+ "ffmpeg.filter.ebur128 is not available, autocue disabled!"
210
+ )
211
+ []
212
+ %endif
213
+ end
214
+
215
+ # Compute autocue data
216
+ # @flag hidden
217
+ def autocue.internal.implementation(
218
+ ~request_metadata,
219
+ ~file_metadata,
220
+ filename
221
+ ) =
222
+ lufs_target = settings.autocue.internal.lufs_target()
223
+ cue_in_threshold = settings.autocue.internal.cue_in_threshold()
224
+ cue_out_threshold = settings.autocue.internal.cue_out_threshold()
225
+ cross_threshold = settings.autocue.internal.cross_threshold()
226
+ max_overlap = settings.autocue.internal.max_overlap()
227
+ sustained_endings_enabled =
228
+ settings.autocue.internal.sustained_endings_enabled()
229
+ sustained_endings_dropoff =
230
+ settings.autocue.internal.sustained_endings_dropoff()
231
+ sustained_endings_slope = settings.autocue.internal.sustained_endings_slope()
232
+ sustained_endings_min_duration =
233
+ settings.autocue.internal.sustained_endings_min_duration()
234
+ sustained_endings_threshold_limit =
235
+ settings.autocue.internal.sustained_endings_threshold_limit()
236
+ ratio = settings.autocue.internal.ratio()
237
+ timeout = settings.autocue.internal.timeout()
238
+
239
+ metadata_overrides = settings.autocue.internal.metadata_override()
240
+
241
+ metadata = [...request_metadata, ...file_metadata]
242
+
243
+ if
244
+ list.exists(fun (el) -> list.mem(fst(el), metadata_overrides), metadata)
245
+ then
246
+ log(
247
+ level=2,
248
+ label="autocue.internal.metadata",
249
+ "Override metadata detected for #{filename}, disabling autocue!"
250
+ )
251
+ null
252
+ else
253
+ log(
254
+ level=4,
255
+ label="autocue.internal",
256
+ "Starting to process #{filename}"
257
+ )
258
+
259
+ %ifdef request.duration.ffmpeg
260
+ duration = request.duration.ffmpeg(resolve_metadata=false, filename)
261
+ %else
262
+ duration = null
263
+ %endif
264
+
265
+ if
266
+ duration == null
267
+ then
268
+ log(
269
+ level=2,
270
+ label="autocue.internal",
271
+ "Could not get request duration, internal autocue disabled!"
272
+ )
273
+ null
274
+ else
275
+ duration = null.get(duration)
276
+
277
+ frames =
278
+ autocue.internal.ebur128(
279
+ duration=duration,
280
+ ratio=ratio,
281
+ timeout=timeout,
282
+ filename
283
+ )
284
+
285
+ if
286
+ list.length(frames) < 2
287
+ then
288
+ log(
289
+ level=2,
290
+ label="autocue.internal",
291
+ "Autocue computation failed!"
292
+ )
293
+ null
294
+ else
295
+ # Get the 2nd last frame which is the last with loudness data
296
+ frame = list.nth(frames, list.length(frames) - 2)
297
+
298
+ # Get the Integrated Loudness from the last frame (overall loudness)
299
+ lufs =
300
+ float_of_string(
301
+ list.assoc(default=string(lufs_target), "lavfi.r128.I", frame)
302
+ )
303
+
304
+ # Calc LUFS difference to target for liq_amplify
305
+ lufs_correction = lufs_target - lufs
306
+
307
+ # Create dB thresholds relative to LUFS target
308
+ lufs_cue_in_threshold = lufs + cue_in_threshold
309
+ lufs_cue_out_threshold = lufs + cue_out_threshold
310
+ lufs_cross_threshold = lufs + cross_threshold
311
+
312
+ log(
313
+ level=4,
314
+ label="autocue.internal",
315
+ "Processing results for #{filename}"
316
+ )
317
+
318
+ log(
319
+ level=4,
320
+ label="autocue.internal",
321
+ "lufs_correction: #{lufs_correction}"
322
+ )
323
+ log(
324
+ level=4,
325
+ label="autocue.internal",
326
+ "lufs_cue_in_threshold: #{lufs_cue_in_threshold}"
327
+ )
328
+ log(
329
+ level=4,
330
+ label="autocue.internal",
331
+ "lufs_cue_out_threshold: #{lufs_cue_out_threshold}"
332
+ )
333
+ log(
334
+ level=4,
335
+ label="autocue.internal",
336
+ "lufs_cross_threshold: #{lufs_cross_threshold}"
337
+ )
338
+
339
+ # Set cue/fade defaults
340
+ cue_in = ref(0.)
341
+ cue_out = ref(0.)
342
+ cross_cue = ref(0.)
343
+ fade_in = ref(0.)
344
+ fade_out = ref(0.)
345
+
346
+ # Extract timestamps for cue points
347
+ # Iterate over loudness data frames and set cue points based on db thresholds
348
+ last_ts = ref(0.)
349
+ current_ts = ref(0.)
350
+ cue_found = ref(false)
351
+ cross_start_idx = ref(0.)
352
+ cross_stop_idx = ref(0.)
353
+ cross_mid_idx = ref(0.)
354
+ cross_frame_length = ref(0.)
355
+ ending_fst_db = ref(0.)
356
+ ending_snd_db = ref(0.)
357
+ reset_iter_values = ref(true)
358
+
359
+ frames_rev = list.rev(frames)
360
+ total_frames_length = float_of_int(list.length(frames))
361
+ frame_idx = ref(total_frames_length - 1.)
362
+ lufs_cross_threshold_sustained = ref(lufs_cross_threshold)
363
+ lufs_cue_out_threshold_sustained = ref(lufs_cue_out_threshold)
364
+
365
+ err = error.register("assoc")
366
+ def find_cues(
367
+ frame,
368
+ ~reverse_order=false,
369
+ ~sustained_ending_check=false,
370
+ ~sustained_ending_recalc=false
371
+ ) =
372
+ if
373
+ reset_iter_values()
374
+ then
375
+ last_ts := 0.
376
+ current_ts := 0.
377
+ cue_found := false
378
+ end
379
+
380
+ # Get current frame loudness level and timestamp
381
+ db_level = list.assoc(default="nan", string("lavfi.r128.M"), frame)
382
+ current_ts :=
383
+ float_of_string(list.assoc(default="0.", "lavfi.liq.pts", frame))
384
+
385
+ # Process only valid level values
386
+ if
387
+ db_level != "nan"
388
+ then
389
+ db_level = float_of_string(db_level)
390
+
391
+ if
392
+ not sustained_ending_check and not sustained_ending_recalc
393
+ then
394
+ # Run regular cue point calc
395
+ reset_iter_values := false
396
+ if
397
+ not reverse_order
398
+ then
399
+ # Search for cue in
400
+ if
401
+ db_level > lufs_cue_in_threshold
402
+ then
403
+ # First time exceeding threshold
404
+ cue_in := last_ts()
405
+
406
+ # Break
407
+ error.raise(
408
+ err,
409
+ "break list.iter"
410
+ )
411
+ end
412
+ else
413
+ # Search for cue out and crossfade point starting from the end (reversed)
414
+ if
415
+ db_level > lufs_cue_out_threshold and not cue_found()
416
+ then
417
+ # Cue out
418
+ cue_out := last_ts()
419
+ cross_stop_idx := frame_idx()
420
+ cue_found := true
421
+ elsif
422
+ db_level > lufs_cross_threshold
423
+ then
424
+ # Absolute crossfade cue
425
+ cross_cue := last_ts()
426
+ cross_start_idx := frame_idx()
427
+
428
+ # Break
429
+ error.raise(
430
+ err,
431
+ "break list.iter"
432
+ )
433
+ end
434
+ frame_idx := frame_idx() - 1.
435
+ end
436
+ elsif
437
+ sustained_ending_check
438
+ then
439
+ # Check regular crossfade data for sustained ending
440
+ if
441
+ reset_iter_values()
442
+ then
443
+ frame_idx := total_frames_length - 1.
444
+ cross_start_idx := cross_start_idx() + 5.
445
+ cross_stop_idx := cross_stop_idx() - 5.
446
+ cross_frame_length := cross_stop_idx() - cross_start_idx()
447
+ cross_mid_idx := cross_stop_idx() - (cross_frame_length() / 2.)
448
+ end
449
+ reset_iter_values := false
450
+
451
+ if
452
+ frame_idx() < cross_start_idx()
453
+ or cross_frame_length() < sustained_endings_min_duration * 10.
454
+ then
455
+ error.raise(
456
+ err,
457
+ "break list.iter"
458
+ )
459
+ end
460
+
461
+ if
462
+ frame_idx() < cross_stop_idx() and frame_idx() > cross_mid_idx()
463
+ then
464
+ if
465
+ ending_snd_db() < 0.
466
+ then
467
+ ending_snd_db := (ending_snd_db() + db_level) / 2.
468
+ else
469
+ ending_snd_db := db_level
470
+ end
471
+ end
472
+
473
+ if
474
+ frame_idx() > cross_start_idx()
475
+ and frame_idx() < cross_mid_idx()
476
+ then
477
+ if
478
+ ending_fst_db() < 0.
479
+ then
480
+ ending_fst_db := (ending_fst_db() + db_level) / 2.
481
+ else
482
+ ending_fst_db := db_level
483
+ end
484
+ end
485
+ frame_idx := frame_idx() - 1.
486
+ elsif
487
+ sustained_ending_recalc
488
+ then
489
+ # Recalculate crossfade on sustained ending
490
+ if
491
+ reset_iter_values()
492
+ then
493
+ cue_out := 0.
494
+ cross_cue := 0.
495
+ end
496
+ reset_iter_values := false
497
+ if
498
+ db_level > lufs_cue_out_threshold_sustained()
499
+ and not cue_found()
500
+ then
501
+ # Cue out
502
+ cue_out := last_ts()
503
+ cue_found := true
504
+ end
505
+ if
506
+ db_level > lufs_cross_threshold_sustained()
507
+ then
508
+ # Absolute crossfade cue
509
+ cross_cue := current_ts()
510
+ error.raise(
511
+ err,
512
+ "break list.iter"
513
+ )
514
+ end
515
+ end
516
+
517
+ # Update last timestamp value with current
518
+ last_ts := current_ts()
519
+ end
520
+ end
521
+
522
+ # Search for cue_in first
523
+ reset_iter_values := true
524
+ def cue_iter_fwd(frame) =
525
+ find_cues(frame)
526
+ end
527
+ try
528
+ list.iter(cue_iter_fwd, frames)
529
+ catch _ do
530
+ log(
531
+ level=4,
532
+ label="autocue.internal",
533
+ "cue_iter_fwd completed."
534
+ )
535
+ end
536
+
537
+ # Reverse frames and search in reverse order for cross_cue and cue_out
538
+ reset_iter_values := true
539
+ def cue_iter_rev(frame) =
540
+ find_cues(frame, reverse_order=true)
541
+ end
542
+ try
543
+ list.iter(cue_iter_rev, frames_rev)
544
+ catch _ do
545
+ log(
546
+ level=4,
547
+ label="autocue.internal",
548
+ "cue_iter_rev completed."
549
+ )
550
+ end
551
+
552
+ if
553
+ sustained_endings_enabled
554
+ then
555
+ # Check for sustained ending
556
+ reset_iter_values := true
557
+ def sustained_ending_check_iter(frame) =
558
+ find_cues(frame, sustained_ending_check=true)
559
+ end
560
+ try
561
+ list.iter(sustained_ending_check_iter, frames_rev)
562
+ catch _ do
563
+ log(
564
+ level=4,
565
+ label="autocue.internal.sustained_ending",
566
+ "sustained_ending_check_iter completed."
567
+ )
568
+ end
569
+
570
+ log(
571
+ level=4,
572
+ label="autocue.internal.sustained_ending",
573
+ "Analysis frame length: #{cross_frame_length()}"
574
+ )
575
+ log(
576
+ level=4,
577
+ label="autocue.internal.sustained_ending",
578
+ "Avg. ending loudness: #{ending_fst_db()} => #{ending_snd_db()}"
579
+ )
580
+
581
+ # Check whether data indicate a sustained ending
582
+ if
583
+ ending_fst_db() < 0.
584
+ then
585
+ slope = ref(0.)
586
+ dropoff = lufs_cross_threshold / ending_fst_db()
587
+
588
+ if
589
+ ending_snd_db() < 0.
590
+ then
591
+ slope := ending_fst_db() / ending_snd_db()
592
+ end
593
+
594
+ log(
595
+ level=4,
596
+ label="autocue.internal.sustained_ending",
597
+ "Drop off: #{(1. - dropoff) * 100.}%"
598
+ )
599
+ log(
600
+ level=4,
601
+ label="autocue.internal.sustained_ending",
602
+ "Slope: #{(1. - slope()) * 100.}%"
603
+ )
604
+
605
+ detect_slope = slope() > 1. - sustained_endings_slope / 100.
606
+ detect_dropoff =
607
+ ending_fst_db() >
608
+ lufs_cross_threshold * (sustained_endings_dropoff / 100. + 1.)
609
+ if
610
+ detect_slope or detect_dropoff
611
+ then
612
+ log(
613
+ level=3,
614
+ label="autocue.internal.sustained_ending",
615
+ "Sustained ending detected (drop off: #{detect_dropoff} / slope: \
616
+ #{detect_slope})"
617
+ )
618
+
619
+ if
620
+ detect_slope
621
+ then
622
+ lufs_cross_threshold_sustained :=
623
+ max(
624
+ lufs_cross_threshold * sustained_endings_threshold_limit,
625
+ ending_snd_db() - 0.5
626
+ )
627
+ else
628
+ lufs_cross_threshold_sustained :=
629
+ max(
630
+ lufs_cross_threshold * sustained_endings_threshold_limit,
631
+ ending_fst_db() - 0.5
632
+ )
633
+ end
634
+ lufs_cue_out_threshold_sustained =
635
+ ref(
636
+ max(
637
+ lufs_cue_out_threshold * sustained_endings_threshold_limit,
638
+ lufs_cue_out_threshold +
639
+ (lufs_cross_threshold_sustained() - lufs_cross_threshold)
640
+ )
641
+ )
642
+
643
+ log(
644
+ level=4,
645
+ label="autocue.internal.sustained_ending",
646
+ "Changed crossfade threshold: #{lufs_cross_threshold} => #{
647
+ lufs_cross_threshold_sustained()
648
+ }"
649
+ )
650
+ log(
651
+ level=4,
652
+ label="autocue.internal.sustained_ending",
653
+ "Changed cue out threshold: #{lufs_cue_out_threshold} => #{
654
+ lufs_cue_out_threshold_sustained()
655
+ }"
656
+ )
657
+
658
+ cross_cue_init = cross_cue()
659
+ cue_out_init = cue_out()
660
+
661
+ reset_iter_values := true
662
+ def sustained_ending_recalc_iter(frame) =
663
+ find_cues(frame, sustained_ending_recalc=true)
664
+ end
665
+ try
666
+ list.iter(sustained_ending_recalc_iter, frames_rev)
667
+ catch _ do
668
+ log(
669
+ level=4,
670
+ label="autocue.internal",
671
+ "sustained_ending_recalc_iter completed."
672
+ )
673
+ end
674
+
675
+ log(
676
+ level=4,
677
+ label="autocue.internal.sustained_ending",
678
+ "Changed crossfade point: #{cross_cue_init} => #{cross_cue()}"
679
+ )
680
+ log(
681
+ level=4,
682
+ label="autocue.internal.sustained_ending",
683
+ "Changed cue out point: #{cue_out_init} => #{cue_out()}"
684
+ )
685
+ else
686
+ log(
687
+ level=3,
688
+ label="autocue.internal.sustained_ending",
689
+ "No sustained ending detected."
690
+ )
691
+ end
692
+ else
693
+ log(
694
+ level=3,
695
+ label="autocue.internal.sustained_ending",
696
+ "No sustained ending detected."
697
+ )
698
+ end
699
+ end
700
+
701
+ # Finalize cue/cross/fade values now...
702
+ if cue_out() == 0. then cue_out := duration end
703
+
704
+ # Calc cross/overlap duration
705
+ if
706
+ cross_cue() + 0.1 < cue_out()
707
+ then
708
+ fade_out := cue_out() - cross_cue()
709
+ end
710
+
711
+ # Add some margin to cue in
712
+ cue_in := cue_in() - 0.1
713
+
714
+ # Avoid hard cuts on cue in
715
+ if
716
+ cue_in() > 0.2
717
+ then
718
+ fade_in := 0.2
719
+ cue_in := cue_in() - 0.2
720
+ end
721
+
722
+ # Ignore super short cue in
723
+ if
724
+ cue_in() <= 0.2
725
+ then
726
+ fade_in := 0.
727
+ cue_in := 0.
728
+ end
729
+
730
+ # Limit overlap duration to maximum
731
+ if max_overlap < fade_in() then fade_in := max_overlap end
732
+
733
+ if
734
+ max_overlap < fade_out()
735
+ then
736
+ cue_shift = fade_out() - max_overlap
737
+ cue_out := cue_out() - cue_shift
738
+ fade_out := max_overlap
739
+ fade_out := max_overlap
740
+ end
741
+
742
+ (
743
+ {
744
+ amplify =
745
+ "#{lufs_correction} dB",
746
+ cue_in = cue_in(),
747
+ cue_out = cue_out(),
748
+ fade_in = fade_in(),
749
+ fade_out = fade_out()
750
+ }
751
+ :
752
+ {
753
+ amplify?: string,
754
+ cue_in: float,
755
+ cue_out: float,
756
+ fade_in: float,
757
+ fade_in_type?: string,
758
+ fade_in_curve?: float,
759
+ fade_out: float,
760
+ fade_out_type?: string,
761
+ fade_out_curve?: float,
762
+ start_next?: float,
763
+ extra_metadata?: [(string * string)]
764
+ }
765
+ )
766
+ end
767
+ end
768
+ end
769
+ end
770
+
771
+ autocue.register(name="internal", autocue.internal.implementation)
772
+
773
+ # Translate autocue values into internal metadara
774
+ # @flag hidden
775
+ def autocue.metadata(~implementation, autocue) =
776
+ let {cue_in, cue_out, fade_in, fade_out} = autocue
777
+
778
+ extra_metadata = autocue.extra_metadata ?? []
779
+ amplify = autocue?.amplify
780
+
781
+ fade_in_type = autocue?.fade_in_type
782
+ fade_in_curve = autocue?.fade_in_curve
783
+ fade_out_type = autocue?.fade_out_type
784
+ fade_out_curve = autocue?.fade_out_curve
785
+
786
+ fade_out_start = cue_out - fade_out
787
+ let (fade_out, fade_out_start) =
788
+ if
789
+ fade_out_start < 0.
790
+ then
791
+ log(
792
+ level=2,
793
+ label="autocue",
794
+ "Invalid cue_out/fade_out values: #{cue_out}/#{fade_out}"
795
+ )
796
+ (0., cue_out)
797
+ else
798
+ (fade_out, fade_out_start)
799
+ end
800
+
801
+ start_next = autocue.start_next ?? fade_out_start
802
+
803
+ start_next =
804
+ if
805
+ start_next < cue_in or cue_out < start_next
806
+ then
807
+ log(
808
+ level=2,
809
+ label="autocue",
810
+ "Invalid start_next: #{start_next}"
811
+ )
812
+ fade_out_start
813
+ else
814
+ start_next
815
+ end
816
+
817
+ fade_out_start_next =
818
+ if fade_out_start < start_next then start_next - fade_out_start else 0. end
819
+
820
+ let fade_out_delay =
821
+ if start_next < fade_out_start then fade_out_start - start_next else 0. end
822
+
823
+ total_fade_out = fade_out + fade_out_delay
824
+
825
+ max_start_duration = cue_out - cue_in - total_fade_out
826
+
827
+ opt_arg = fun (lbl, v) -> null.defined(v) ? [(lbl, string(v))] : []
828
+
829
+ [
830
+ ("liq_autocue", implementation),
831
+ ...opt_arg("liq_amplify", amplify),
832
+ ("liq_cue_in", string(cue_in)),
833
+ ("liq_cue_out", string(cue_out)),
834
+ ("liq_cross_start_duration", string(fade_in)),
835
+ ("liq_cross_max_start_duration", string(max_start_duration)),
836
+ ("liq_cross_end_duration", string(total_fade_out)),
837
+ ("liq_fade_in", string(fade_in)),
838
+ ...opt_arg("liq_fade_in_type", fade_in_type),
839
+ ...opt_arg("liq_fade_in_curve", fade_in_curve),
840
+ ("liq_fade_out", string(fade_out)),
841
+ ("liq_fade_out_start_next", string(fade_out_start_next)),
842
+ ("liq_fade_out_delay", string(fade_out_delay)),
843
+ ...opt_arg("liq_fade_out_type", fade_out_type),
844
+ ...opt_arg("liq_fade_out_curve", fade_out_curve),
845
+ ...extra_metadata
846
+ ]
847
+ end
848
+
849
+ let file.autocue = ()
850
+
851
+ # Return the file's autocue values as metadata suitable for metadata override.
852
+ # @category Source / Audio processing
853
+ def file.autocue.metadata(~request_metadata, uri) =
854
+ preferred_implementation = settings.autocue.preferred()
855
+ implementations = settings.autocue.implementations()
856
+ autocue_metadata = autocue.metadata
857
+
858
+ let (implementation_name, implementation) =
859
+ if
860
+ list.assoc.mem(preferred_implementation, implementations)
861
+ then
862
+ log(
863
+ level=4,
864
+ label="autocue",
865
+ "Using preferred #{preferred_implementation} autocue implementation."
866
+ )
867
+ (
868
+ preferred_implementation,
869
+ list.assoc(preferred_implementation, implementations)
870
+ )
871
+ elsif
872
+ list.length(implementations) > 0
873
+ then
874
+ let [(name, implementation)] = implementations
875
+ log(
876
+ level=4,
877
+ label="autocue",
878
+ "Using first available #{name} autocue implementation."
879
+ )
880
+ (name, implementation)
881
+ else
882
+ error.raise(
883
+ error.not_found,
884
+ "No autocue implementation found!"
885
+ )
886
+ end
887
+
888
+ r =
889
+ request.create(
890
+ excluded_metadata_resolvers=decoder.metadata.reentrant(),
891
+ uri
892
+ )
893
+
894
+ if
895
+ not request.resolve(r)
896
+ then
897
+ request.destroy(r)
898
+ log(
899
+ level=2,
900
+ label="autocue",
901
+ "Couldn't resolve uri: #{uri}"
902
+ )
903
+ []
904
+ else
905
+ autocue =
906
+ try
907
+ autocue =
908
+ implementation(
909
+ request_metadata=request_metadata,
910
+ file_metadata=request.metadata(r),
911
+ request.filename(r)
912
+ )
913
+ request.destroy(r)
914
+ autocue
915
+ catch err do
916
+ request.destroy(r)
917
+ log(
918
+ level=2,
919
+ label="autocue",
920
+ "Error while processing autocue: #{err}"
921
+ )
922
+ error.raise(err)
923
+ end
924
+
925
+ if
926
+ null.defined(autocue)
927
+ then
928
+ autocue_metadata(implementation=implementation_name, null.get(autocue))
929
+ else
930
+ log(
931
+ level=2,
932
+ label="autocue.metadata",
933
+ "No autocue data returned for file #{uri}"
934
+ )
935
+ []
936
+ end
937
+ end
938
+ end
939
+
940
+ # Enable autocue metadata resolver. This resolver will process any file
941
+ # decoded by Liquidsoap and add cue-in/out and crossfade metadata when these
942
+ # values can be computed. This function sets `settings.request.prefetch` to `2`
943
+ # to account for the latency introduced by the `autocue` computation when resolving
944
+ # requests. For a finer-grained processing, use the `autocue:` protocol.
945
+ # @category Liquidsoap
946
+ def enable_autocue_metadata() =
947
+ if settings.request.prefetch() == 1 then settings.request.prefetch := 2 end
948
+
949
+ def autocue_metadata(~metadata, fname) =
950
+ metadata_overrides = settings.autocue.internal.metadata_override()
951
+
952
+ if
953
+ list.exists(fun (el) -> list.mem(fst(el), metadata_overrides), metadata)
954
+ then
955
+ log(
956
+ level=2,
957
+ label="autocue.metadata",
958
+ "Override metadata detected for #{fname}, disabling autocue!"
959
+ )
960
+ []
961
+ else
962
+ autocue_metadata = file.autocue.metadata(request_metadata=metadata, fname)
963
+
964
+ all_amplify = [...settings.autocue.amplify_aliases(), "liq_amplify"]
965
+
966
+ user_supplied_amplify =
967
+ list.filter_map(
968
+ fun (el) ->
969
+ if list.mem(fst(el), all_amplify) then fst(el) else null end,
970
+ metadata
971
+ )
972
+
973
+ user_supplied_amplify_labels =
974
+ string.concat(
975
+ separator=", ",
976
+ user_supplied_amplify
977
+ )
978
+
979
+ autocue_metadata =
980
+ if
981
+ settings.autocue.amplify_behavior() == "ignore"
982
+ then
983
+ [...list.assoc.remove("liq_amplify", autocue_metadata)]
984
+ else
985
+ if
986
+ user_supplied_amplify != []
987
+ then
988
+ if
989
+ settings.autocue.amplify_behavior() == "keep"
990
+ then
991
+ log(
992
+ level=3,
993
+ label="autocue.metadata",
994
+ "User-supplied amplify metadata detected: #{
995
+ user_supplied_amplify_labels
996
+ }, keeping user-provided data."
997
+ )
998
+ list.assoc.remove("liq_amplify", autocue_metadata)
999
+ elsif
1000
+ settings.autocue.amplify_behavior() == "override"
1001
+ then
1002
+ log(
1003
+ level=3,
1004
+ label="autocue.metadata",
1005
+ "User-supplied amplify metadata detected: #{
1006
+ user_supplied_amplify_labels
1007
+ }, overriding with autocue data."
1008
+ )
1009
+ [
1010
+ ...autocue_metadata,
1011
+ # This replaces all user-provided tags with the value returned by
1012
+ # the autocue implementation.
1013
+ ...list.map(
1014
+ fun (lbl) -> (lbl, autocue_metadata["liq_amplify"]),
1015
+ user_supplied_amplify
1016
+ )
1017
+ ]
1018
+ else
1019
+ log(
1020
+ level=2,
1021
+ label="autocue.metadata",
1022
+ "Invalid value for `settings.autocue.amplify_behavior`: #{
1023
+ settings.autocue.amplify_behavior()
1024
+ }"
1025
+ )
1026
+ autocue_metadata
1027
+ end
1028
+ else
1029
+ autocue_metadata
1030
+ end
1031
+ end
1032
+ log(level=4, label="autocue.metadata", "#{autocue_metadata}")
1033
+ autocue_metadata
1034
+ end
1035
+ end
1036
+
1037
+ %ifdef settings.decoder.mime_types.ffmpeg
1038
+ mime_types = settings.decoder.mime_types.ffmpeg()
1039
+ file_extensions = settings.decoder.file_extensions.ffmpeg()
1040
+ %else
1041
+ mime_types = null
1042
+ file_extensions = null
1043
+ %endif
1044
+
1045
+ decoder.metadata.add(
1046
+ mime_types=mime_types,
1047
+ file_extensions=file_extensions,
1048
+ priority=settings.autocue.metadata.priority,
1049
+ reentrant=true,
1050
+ "autocue",
1051
+ autocue_metadata
1052
+ )
1053
+ end
1054
+
1055
+ # Define autocue protocol
1056
+ # @flag hidden
1057
+ def protocol.autocue(~rlog:_, ~maxtime:_, arg) =
1058
+ cue_metadata = file.autocue.metadata(request_metadata=[], arg)
1059
+
1060
+ if
1061
+ cue_metadata != []
1062
+ then
1063
+ cue_metadata =
1064
+ list.map(fun (el) -> "#{fst(el)}=#{string.quote(snd(el))}", cue_metadata)
1065
+ cue_metadata = string.concat(separator=",", cue_metadata)
1066
+ "annotate:#{cue_metadata}:#{arg}"
1067
+ else
1068
+ log(
1069
+ level=2,
1070
+ label="autocue.protocol",
1071
+ "No autocue data returned for URI #{arg}!"
1072
+ )
1073
+ arg
1074
+ end
1075
+ end
1076
+ protocol.add(
1077
+ "autocue",
1078
+ protocol.autocue,
1079
+ doc="Adding automatically computed cues/crossfade metadata",
1080
+ syntax="autocue:uri"
1081
+ )