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.
- package/.github/workflows/check-formatting.yml +32 -0
- package/README.md +31 -5
- package/package.json +1 -1
- package/src/cli.js +104 -9
- package/tests/liq/audio.liq +460 -0
- package/tests/liq/autocue.liq +1081 -0
- package/tests/liq/clock.liq +14 -0
- package/tests/liq/cron.liq +74 -0
- package/tests/liq/error.liq +48 -0
- package/tests/liq/extra/audio.liq +677 -0
- package/tests/liq/extra/audioscrobbler.liq +482 -0
- package/tests/liq/extra/deprecations.liq +976 -0
- package/tests/liq/extra/externals.liq +196 -0
- package/tests/liq/extra/fades.liq +260 -0
- package/tests/liq/extra/file.liq +66 -0
- package/tests/liq/extra/http.liq +160 -0
- package/tests/liq/extra/interactive.liq +917 -0
- package/tests/liq/extra/metadata.liq +75 -0
- package/tests/liq/extra/native.liq +201 -0
- package/tests/liq/extra/openai.liq +150 -0
- package/tests/liq/extra/server.liq +177 -0
- package/tests/liq/extra/source.liq +476 -0
- package/tests/liq/extra/spinitron.liq +272 -0
- package/tests/liq/extra/telnet.liq +266 -0
- package/tests/liq/extra/video.liq +59 -0
- package/tests/liq/extra/visualization.liq +68 -0
- package/tests/liq/fades.liq +941 -0
- package/tests/liq/ffmpeg.liq +605 -0
- package/tests/liq/file.liq +387 -0
- package/tests/liq/getter.liq +74 -0
- package/tests/liq/hls.liq +329 -0
- package/tests/liq/http.liq +1048 -0
- package/tests/liq/http_codes.liq +447 -0
- package/tests/liq/icecast.liq +58 -0
- package/tests/liq/io.liq +106 -0
- package/tests/liq/liquidsoap.liq +31 -0
- package/tests/liq/list.liq +440 -0
- package/tests/liq/log.liq +47 -0
- package/tests/liq/lufs.liq +295 -0
- package/tests/liq/math.liq +23 -0
- package/tests/liq/medialib.liq +752 -0
- package/tests/liq/metadata.liq +253 -0
- package/tests/liq/nfo.liq +258 -0
- package/tests/liq/null.liq +71 -0
- package/tests/liq/playlist.liq +1347 -0
- package/tests/liq/predicate.liq +106 -0
- package/tests/liq/process.liq +93 -0
- package/tests/liq/profiler.liq +5 -0
- package/tests/liq/protocols.liq +1139 -0
- package/tests/liq/ref.liq +28 -0
- package/tests/liq/replaygain.liq +135 -0
- package/tests/liq/request.liq +467 -0
- package/tests/liq/resolvers.liq +33 -0
- package/tests/liq/runtime.liq +70 -0
- package/tests/liq/server.liq +99 -0
- package/tests/liq/settings.liq +41 -0
- package/tests/liq/socket.liq +33 -0
- package/tests/liq/source.liq +362 -0
- package/tests/liq/sqlite.liq +161 -0
- package/tests/liq/stdlib.liq +172 -0
- package/tests/liq/string.liq +476 -0
- package/tests/liq/switches.liq +197 -0
- package/tests/liq/testing.liq +37 -0
- package/tests/liq/thread.liq +161 -0
- package/tests/liq/tracks.liq +100 -0
- package/tests/liq/utils.liq +81 -0
- 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
|
+
)
|