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,272 @@
|
|
|
1
|
+
let spinitron = {submit = ()}
|
|
2
|
+
|
|
3
|
+
# Submit a track to the spinitron track system
|
|
4
|
+
# and return the raw response.
|
|
5
|
+
# @category Interaction
|
|
6
|
+
# @flag extra
|
|
7
|
+
# @param ~api_key API key
|
|
8
|
+
def spinitron.submit.raw(
|
|
9
|
+
~host="https://spinitron.com/api",
|
|
10
|
+
~api_key,
|
|
11
|
+
~live=false,
|
|
12
|
+
~start=null,
|
|
13
|
+
~duration=null,
|
|
14
|
+
~artist,
|
|
15
|
+
~release=null,
|
|
16
|
+
~label=null,
|
|
17
|
+
~genre=null,
|
|
18
|
+
~song,
|
|
19
|
+
~composer=null,
|
|
20
|
+
~isrc=null
|
|
21
|
+
) =
|
|
22
|
+
params = [("song", song), ("artist", artist)]
|
|
23
|
+
|
|
24
|
+
def fold_optional_string_params(params, param) =
|
|
25
|
+
let (label, param) = param
|
|
26
|
+
if
|
|
27
|
+
null.defined(param)
|
|
28
|
+
then
|
|
29
|
+
[(label, null.get(param)), ...params]
|
|
30
|
+
else
|
|
31
|
+
params
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
params =
|
|
36
|
+
list.fold(
|
|
37
|
+
fold_optional_string_params,
|
|
38
|
+
params,
|
|
39
|
+
[
|
|
40
|
+
("live", null.map(fun (b) -> b ? "1" : "0", (live : bool?))),
|
|
41
|
+
("start", start),
|
|
42
|
+
("duration", null.map(string, (duration : int?))),
|
|
43
|
+
("release", release),
|
|
44
|
+
("label", label),
|
|
45
|
+
("genre", genre),
|
|
46
|
+
("composer", composer),
|
|
47
|
+
("isrc", isrc)
|
|
48
|
+
]
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def encode_param(param) =
|
|
52
|
+
let (label, param) = param
|
|
53
|
+
"#{label}=#{url.encode(param)}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
params = string.concat(separator="&", list.map(encode_param, params))
|
|
57
|
+
|
|
58
|
+
http.post(
|
|
59
|
+
data=params,
|
|
60
|
+
headers=[
|
|
61
|
+
("Accept", "application/json"),
|
|
62
|
+
("Content-Type", "application/x-www-form-urlencoded"),
|
|
63
|
+
(
|
|
64
|
+
"Authorization",
|
|
65
|
+
"Bearer #{(api_key : string)}"
|
|
66
|
+
)
|
|
67
|
+
],
|
|
68
|
+
"#{host}/spins"
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Submit a track to the spinitron track system
|
|
73
|
+
# and return the parsed response
|
|
74
|
+
# @category Interaction
|
|
75
|
+
# @flag extra
|
|
76
|
+
# @param ~api_key API key
|
|
77
|
+
def replaces spinitron.submit(%argsof(spinitron.submit.raw)) =
|
|
78
|
+
resp = spinitron.submit.raw(%argsof(spinitron.submit.raw))
|
|
79
|
+
|
|
80
|
+
if
|
|
81
|
+
resp.status_code == 201
|
|
82
|
+
then
|
|
83
|
+
let json.parse (resp :
|
|
84
|
+
{
|
|
85
|
+
id: int,
|
|
86
|
+
playlist_id: int,
|
|
87
|
+
"start" as spin_start: string,
|
|
88
|
+
"end" as spin_end: string?,
|
|
89
|
+
duration: int?,
|
|
90
|
+
timezone: string?,
|
|
91
|
+
image: string?,
|
|
92
|
+
classical: bool?,
|
|
93
|
+
artist: string,
|
|
94
|
+
"artist-custom" as artist_custom: string?,
|
|
95
|
+
composer: string?,
|
|
96
|
+
release: string?,
|
|
97
|
+
"release-custom" as release_custom: string?,
|
|
98
|
+
va: bool?,
|
|
99
|
+
label: string?,
|
|
100
|
+
"label-custom" as label_custom: string?,
|
|
101
|
+
released: int?,
|
|
102
|
+
medium: string?,
|
|
103
|
+
genre: string?,
|
|
104
|
+
song: string,
|
|
105
|
+
note: string?,
|
|
106
|
+
request: bool?,
|
|
107
|
+
local: bool?,
|
|
108
|
+
new: bool?,
|
|
109
|
+
work: string?,
|
|
110
|
+
conductor: string?,
|
|
111
|
+
performers: string?,
|
|
112
|
+
ensemble: string?,
|
|
113
|
+
"catalog-number" as catalog_number: string?,
|
|
114
|
+
isrc: string?,
|
|
115
|
+
upc: string?,
|
|
116
|
+
iswc: string?,
|
|
117
|
+
"_links" as links: {self: {href: string}?, playlist: {href: string}?}?
|
|
118
|
+
}
|
|
119
|
+
) = resp
|
|
120
|
+
|
|
121
|
+
resp
|
|
122
|
+
elsif
|
|
123
|
+
resp.status_code == 422
|
|
124
|
+
then
|
|
125
|
+
let json.parse (errors : [{field: string, message: string}]) = resp
|
|
126
|
+
|
|
127
|
+
errors =
|
|
128
|
+
list.map(
|
|
129
|
+
fun (p) ->
|
|
130
|
+
begin
|
|
131
|
+
let {field, message} = p
|
|
132
|
+
"#{field}: #{message}"
|
|
133
|
+
end,
|
|
134
|
+
errors
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
errors =
|
|
138
|
+
string.concat(
|
|
139
|
+
separator=", ",
|
|
140
|
+
errors
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
error.raise(
|
|
144
|
+
error.raise(
|
|
145
|
+
error.http,
|
|
146
|
+
"Invalid fields: #{errors}"
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
else
|
|
150
|
+
let json.parse ({name, message, code, status, type} :
|
|
151
|
+
{name: string, message: string, code: int, status: int, type: string?}
|
|
152
|
+
) = resp
|
|
153
|
+
|
|
154
|
+
type = type ?? "undefined"
|
|
155
|
+
|
|
156
|
+
error.raise(
|
|
157
|
+
error.raise(
|
|
158
|
+
error.http,
|
|
159
|
+
"#{name}: #{message} (code: #{code}, status: #{status}, type: #{type})"
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Submit a spin using the given metadata to the spinitron track system
|
|
166
|
+
# and return the parsed response. `artist` and `song` (or `title`) must
|
|
167
|
+
# be present either as metadata or as optional argument.
|
|
168
|
+
# @category Interaction
|
|
169
|
+
# @flag extra
|
|
170
|
+
# @param m Metadata to submit. Overrides optional arguments when present.
|
|
171
|
+
# @param ~mapper Metadata mapper that can be used to map metadata fields to spinitron's expected. \
|
|
172
|
+
# Returned metadata are added to the submitted metadata. By default, `title` is \
|
|
173
|
+
# mapped to `song` and `album` to `release` if neither of those passed otherwise.
|
|
174
|
+
# @param ~api_key API key
|
|
175
|
+
def spinitron.submit.metadata(
|
|
176
|
+
%argsof(spinitron.submit[!artist,!song]),
|
|
177
|
+
~mapper=(
|
|
178
|
+
fun (m) ->
|
|
179
|
+
[
|
|
180
|
+
...(m["song"] != "" or m["title"] == "" ? [] : [("song", m["title"])]),
|
|
181
|
+
...(
|
|
182
|
+
m["release"] != "" or m["album"] == ""
|
|
183
|
+
? []
|
|
184
|
+
: [("release", m["album"])]
|
|
185
|
+
)
|
|
186
|
+
]
|
|
187
|
+
),
|
|
188
|
+
~artist=null,
|
|
189
|
+
~song=null,
|
|
190
|
+
m
|
|
191
|
+
) =
|
|
192
|
+
m = [...m, ...mapper(m)]
|
|
193
|
+
|
|
194
|
+
def conv_opt_arg(convert, label, default) =
|
|
195
|
+
list.assoc.mem(label, m) ? convert(m[label]) : default
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
opt_arg =
|
|
199
|
+
fun (label, default) -> conv_opt_arg(fun (x) -> null(x), label, default)
|
|
200
|
+
|
|
201
|
+
live = conv_opt_arg(bool_of_string, "live", live)
|
|
202
|
+
start = opt_arg("start", start)
|
|
203
|
+
duration = conv_opt_arg(int_of_string, "duration", duration)
|
|
204
|
+
artist = opt_arg("artist", artist)
|
|
205
|
+
release = opt_arg("release", release)
|
|
206
|
+
label = opt_arg("label", label)
|
|
207
|
+
genre = opt_arg("genre", genre)
|
|
208
|
+
song = opt_arg("song", song)
|
|
209
|
+
composer = opt_arg("composer", composer)
|
|
210
|
+
isrc = opt_arg("isrc", isrc)
|
|
211
|
+
|
|
212
|
+
if
|
|
213
|
+
artist == null or song == null
|
|
214
|
+
then
|
|
215
|
+
error.raise(
|
|
216
|
+
error.invalid,
|
|
217
|
+
"Both \"artist\" and \"song\" (or \"title\" metadata) must be provided!"
|
|
218
|
+
)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
artist = null.get(artist)
|
|
222
|
+
song = null.get(song)
|
|
223
|
+
|
|
224
|
+
res = spinitron.submit(%argsof(spinitron.submit))
|
|
225
|
+
|
|
226
|
+
print(res)
|
|
227
|
+
|
|
228
|
+
res
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Specialized version of `s.on_metadata` that submits spins using
|
|
232
|
+
# the source's metadata to the spinitron track system. `artist` and `song`
|
|
233
|
+
# (or `title`) must be present either as metadata or as optional argument.
|
|
234
|
+
# @category Interaction
|
|
235
|
+
# @flag extra
|
|
236
|
+
# @param m Metadata to submit. Overrides optional arguments when present.
|
|
237
|
+
# @param ~api_key API key
|
|
238
|
+
def spinitron.submit.on_metadata(%argsof(spinitron.submit.metadata), s) =
|
|
239
|
+
def on_metadata(m) =
|
|
240
|
+
if
|
|
241
|
+
m["title"] == "" and m["song"] == ""
|
|
242
|
+
then
|
|
243
|
+
log.severe(
|
|
244
|
+
label=source.id(s),
|
|
245
|
+
"Field \"song\" or \"title\" missing, skipping metadata spinitron \
|
|
246
|
+
submission."
|
|
247
|
+
)
|
|
248
|
+
elsif
|
|
249
|
+
m["artist"] == ""
|
|
250
|
+
then
|
|
251
|
+
log.severe(
|
|
252
|
+
label=source.id(s),
|
|
253
|
+
"Field \"artist\" missing, skipping metadata spinitron submission."
|
|
254
|
+
)
|
|
255
|
+
else
|
|
256
|
+
try
|
|
257
|
+
ignore(spinitron.submit.metadata(%argsof(spinitron.submit.metadata), m))
|
|
258
|
+
log.important(
|
|
259
|
+
label=source.id(s),
|
|
260
|
+
"Successfully submitted spin from metadata"
|
|
261
|
+
)
|
|
262
|
+
catch err do
|
|
263
|
+
log.severe(
|
|
264
|
+
label=source.id(s),
|
|
265
|
+
"Error while submitting spin from metadata: #{err}"
|
|
266
|
+
)
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
source.methods(s).on_metadata(synchronous=false, on_metadata)
|
|
272
|
+
end
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# @docof request.dynamic
|
|
2
|
+
def replaces request.dynamic(%argsof(request.dynamic), fn) =
|
|
3
|
+
s = request.dynamic(%argsof(request.dynamic), fn)
|
|
4
|
+
s.on_wake_up(
|
|
5
|
+
synchronous=false,
|
|
6
|
+
memoize(
|
|
7
|
+
{
|
|
8
|
+
server.register(
|
|
9
|
+
namespace=source.id(s),
|
|
10
|
+
description="Flush the queue and skip the current track",
|
|
11
|
+
"flush_and_skip",
|
|
12
|
+
fun (_) ->
|
|
13
|
+
try
|
|
14
|
+
s.set_queue([])
|
|
15
|
+
s.skip()
|
|
16
|
+
"Done."
|
|
17
|
+
catch err do
|
|
18
|
+
"Error while flushing and skipping source: #{err}"
|
|
19
|
+
end
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
)
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
s
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @docof blank.strip
|
|
29
|
+
def replaces blank.strip(%argsof(blank.strip), s) =
|
|
30
|
+
s = blank.strip(%argsof(blank.strip), s)
|
|
31
|
+
s.on_wake_up(
|
|
32
|
+
synchronous=false,
|
|
33
|
+
memoize(
|
|
34
|
+
{
|
|
35
|
+
server.register(
|
|
36
|
+
namespace=source.id(s),
|
|
37
|
+
description="Check if the source is stripping.",
|
|
38
|
+
"is_stripping",
|
|
39
|
+
fun (_) -> begin "#{s.is_blank()}" end
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
s
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @docof output.external
|
|
49
|
+
def replaces output.external(%argsof(output.external), f, p, s) =
|
|
50
|
+
s = output.external(%argsof(output.external), f, p, s)
|
|
51
|
+
s.on_wake_up(
|
|
52
|
+
synchronous=false,
|
|
53
|
+
memoize(
|
|
54
|
+
{
|
|
55
|
+
server.register(
|
|
56
|
+
namespace=s.id(),
|
|
57
|
+
description="Re-open the output.",
|
|
58
|
+
"reopen",
|
|
59
|
+
fun (_) ->
|
|
60
|
+
begin
|
|
61
|
+
s.reopen()
|
|
62
|
+
"Done."
|
|
63
|
+
end
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
s
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# @docof input.external.avi
|
|
73
|
+
def replaces input.external.avi(%argsof(input.external.avi), s) =
|
|
74
|
+
s = input.external.avi(%argsof(input.external.avi), s)
|
|
75
|
+
s.on_wake_up(
|
|
76
|
+
synchronous=false,
|
|
77
|
+
memoize(
|
|
78
|
+
{
|
|
79
|
+
server.register(
|
|
80
|
+
namespace=source.id(s),
|
|
81
|
+
description="Show internal buffer length (in seconds).",
|
|
82
|
+
"buffer_length",
|
|
83
|
+
fun (_) ->
|
|
84
|
+
begin
|
|
85
|
+
buffered = s.buffered()
|
|
86
|
+
audio = list.assoc(default=0., "audio", buffered)
|
|
87
|
+
video = list.assoc(default=0., "video", buffered)
|
|
88
|
+
total = min(audio, video)
|
|
89
|
+
"audio buffer length: #{audio}\nvideo buffer length: #{
|
|
90
|
+
video
|
|
91
|
+
}\ntotal buffer length: #{total}"
|
|
92
|
+
end
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
s
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# @docof input.harbor
|
|
102
|
+
def replaces input.harbor(%argsof(input.harbor), s) =
|
|
103
|
+
s = input.harbor(%argsof(input.harbor), s)
|
|
104
|
+
s.on_wake_up(
|
|
105
|
+
synchronous=false,
|
|
106
|
+
memoize(
|
|
107
|
+
{
|
|
108
|
+
begin
|
|
109
|
+
server.register(
|
|
110
|
+
namespace=source.id(s),
|
|
111
|
+
description="Stop current source client, if connected.",
|
|
112
|
+
"stop",
|
|
113
|
+
fun (_) ->
|
|
114
|
+
begin
|
|
115
|
+
s.stop()
|
|
116
|
+
"Done"
|
|
117
|
+
end
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
server.register(
|
|
121
|
+
namespace=source.id(s),
|
|
122
|
+
description="Display current status.",
|
|
123
|
+
"status",
|
|
124
|
+
fun (_) -> begin s.status() end
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
server.register(
|
|
128
|
+
namespace=source.id(s),
|
|
129
|
+
description="Get the buffer's length, in seconds.",
|
|
130
|
+
"buffer_length",
|
|
131
|
+
fun (_) -> begin "#{s.buffer_length()}" end
|
|
132
|
+
)
|
|
133
|
+
end
|
|
134
|
+
}
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
s
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
%ifdef input.http
|
|
142
|
+
# @docof input.http
|
|
143
|
+
def replaces input.http(%argsof(input.http), s) =
|
|
144
|
+
s = input.http(%argsof(input.http), s)
|
|
145
|
+
s.on_wake_up(
|
|
146
|
+
synchronous=false,
|
|
147
|
+
memoize(
|
|
148
|
+
{
|
|
149
|
+
begin
|
|
150
|
+
server.register(
|
|
151
|
+
namespace=source.id(s),
|
|
152
|
+
description="Start the source, if needed.",
|
|
153
|
+
"start",
|
|
154
|
+
fun (_) ->
|
|
155
|
+
begin
|
|
156
|
+
s.start()
|
|
157
|
+
"Done!"
|
|
158
|
+
end
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
server.register(
|
|
162
|
+
namespace=source.id(s),
|
|
163
|
+
description="Stop the source if connected.",
|
|
164
|
+
"stop",
|
|
165
|
+
fun (_) ->
|
|
166
|
+
begin
|
|
167
|
+
s.stop()
|
|
168
|
+
"Done!"
|
|
169
|
+
end
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
server.register(
|
|
173
|
+
namespace=source.id(s),
|
|
174
|
+
description="Get or set the stream's HTTP URL. Setting a new URL \
|
|
175
|
+
will not affect an ongoing connection.",
|
|
176
|
+
usage="url [url]",
|
|
177
|
+
"url",
|
|
178
|
+
fun (url) ->
|
|
179
|
+
begin
|
|
180
|
+
if
|
|
181
|
+
url == ""
|
|
182
|
+
then
|
|
183
|
+
s.url()
|
|
184
|
+
else
|
|
185
|
+
begin
|
|
186
|
+
s.set_url({url})
|
|
187
|
+
"Done!"
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
server.register(
|
|
194
|
+
namespace=source.id(s),
|
|
195
|
+
description="Return the current status of the source, either \
|
|
196
|
+
\"stopped\" (the source isn't trying to relay the HTTP stream), \
|
|
197
|
+
\"polling\" (attempting to connect to the HTTP stream) or \
|
|
198
|
+
\"connected <url>\" (connected to <url>, buffering or playing back \
|
|
199
|
+
the stream).",
|
|
200
|
+
"status",
|
|
201
|
+
fun (_) -> begin s.status() end
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
server.register(
|
|
205
|
+
namespace=source.id(s),
|
|
206
|
+
description="Get the buffer's length, in seconds.",
|
|
207
|
+
"buffer_length",
|
|
208
|
+
fun (_) ->
|
|
209
|
+
"#{list.fold(fun (l, (_, l')) -> max(l, l'), 0., s.buffered())}"
|
|
210
|
+
)
|
|
211
|
+
end
|
|
212
|
+
}
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
s
|
|
217
|
+
end
|
|
218
|
+
%endif
|
|
219
|
+
|
|
220
|
+
# @docof lufs
|
|
221
|
+
def replaces lufs(%argsof(lufs), s) =
|
|
222
|
+
s = lufs(%argsof(lufs), s)
|
|
223
|
+
s.on_wake_up(
|
|
224
|
+
synchronous=false,
|
|
225
|
+
memoize(
|
|
226
|
+
{
|
|
227
|
+
server.register(
|
|
228
|
+
namespace=source.id(s),
|
|
229
|
+
description="Current value for the LUFS (short-term value computed \
|
|
230
|
+
over the duration specified by the `window` parameter).",
|
|
231
|
+
"lufs",
|
|
232
|
+
fun (_) ->
|
|
233
|
+
"#{s.lufs()} LUFS"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
server.register(
|
|
237
|
+
namespace=source.id(s),
|
|
238
|
+
description="Average LUFS value over the current track.",
|
|
239
|
+
"lufs_integrated",
|
|
240
|
+
fun (_) ->
|
|
241
|
+
"#{s.lufs_integrated()} LUFS"
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
server.register(
|
|
245
|
+
namespace=source.id(s),
|
|
246
|
+
description="Momentary LUFS (over a 400ms window).",
|
|
247
|
+
"lufs_momentary",
|
|
248
|
+
fun (_) ->
|
|
249
|
+
"#{s.lufs_momentary()} LUFS"
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
)
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
s
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
server.register(
|
|
259
|
+
description="Shutdown the running liquidsoap instance",
|
|
260
|
+
"shutdown",
|
|
261
|
+
fun (_) ->
|
|
262
|
+
begin
|
|
263
|
+
shutdown()
|
|
264
|
+
"OK"
|
|
265
|
+
end
|
|
266
|
+
)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Extract cover from the source's metadata and add it as a static image.
|
|
2
|
+
# @category Track / Video processing
|
|
3
|
+
# @flag extra
|
|
4
|
+
# @param ~id Force the value of the source ID.
|
|
5
|
+
# @param ~fallible Whether we are allowed to fail (in case the file is non-existent or invalid).
|
|
6
|
+
# @param ~width Scale to width
|
|
7
|
+
# @param ~height Scale to height
|
|
8
|
+
# @param ~x x position.
|
|
9
|
+
# @param ~y y position.
|
|
10
|
+
# @param ~default Default cover file when no cover is available
|
|
11
|
+
# @param ~mime_types Recognized mime types and their corresponding file extensions.
|
|
12
|
+
def video.add_cover(
|
|
13
|
+
~fallible=false,
|
|
14
|
+
~width=null,
|
|
15
|
+
~height=null,
|
|
16
|
+
~x=getter(0),
|
|
17
|
+
~y=getter(0),
|
|
18
|
+
~mime_types=[
|
|
19
|
+
("image/gif", "gif"),
|
|
20
|
+
("image/jpg", "jpeg"),
|
|
21
|
+
("image/jpeg", "jpeg"),
|
|
22
|
+
("image/png", "png"),
|
|
23
|
+
("image/webp", "webp")
|
|
24
|
+
],
|
|
25
|
+
~default,
|
|
26
|
+
s
|
|
27
|
+
) =
|
|
28
|
+
cover_file = file.cover.manager(mime_types=mime_types, default=default)
|
|
29
|
+
s.on_metadata(synchronous=true, cover_file.set)
|
|
30
|
+
video.add_request(
|
|
31
|
+
fallible=fallible,
|
|
32
|
+
x=x,
|
|
33
|
+
y=y,
|
|
34
|
+
width=width,
|
|
35
|
+
height=height,
|
|
36
|
+
request=cover_file,
|
|
37
|
+
s
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
let video.ffmpeg = ()
|
|
42
|
+
|
|
43
|
+
%ifdef input.ffmpeg
|
|
44
|
+
# ffmpeg's test source video (useful for testing and debugging).
|
|
45
|
+
# @category Source / Video processing
|
|
46
|
+
# @flag extra
|
|
47
|
+
def video.ffmpeg.testsrc(~id="video.testsrc") =
|
|
48
|
+
video =
|
|
49
|
+
"testsrc=size=#{video.frame.width()}x#{video.frame.height()}:rate=#{
|
|
50
|
+
video.frame.rate()
|
|
51
|
+
}"
|
|
52
|
+
audio = "sine=frequency=440:beep_factor=2:sample_rate=#{audio.samplerate()}"
|
|
53
|
+
input.ffmpeg(
|
|
54
|
+
id=id,
|
|
55
|
+
format="lavfi",
|
|
56
|
+
"#{video} [out0]; #{audio} [out1]"
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
%endif
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# VU meter: display the audio volume (RMS in dB) on the standard output.
|
|
2
|
+
# @category Source / Visualization
|
|
3
|
+
# @flag extra
|
|
4
|
+
# @param ~rms_min Minimal volume (dB).
|
|
5
|
+
# @param ~rms_max Maximal volume (dB).
|
|
6
|
+
# @param ~scroll Update the display in the same line.
|
|
7
|
+
# @param ~window Duration in seconds of volume computation.
|
|
8
|
+
def vumeter(~rms_min=-25., ~rms_max=-5., ~window=0.5, ~scroll=false, s) =
|
|
9
|
+
screen_width = 80
|
|
10
|
+
bar_width = screen_width
|
|
11
|
+
let s = rms(duration=window, s)
|
|
12
|
+
|
|
13
|
+
def display() =
|
|
14
|
+
v = dB_of_lin(s.rms())
|
|
15
|
+
x = (v - rms_min) / (rms_max - rms_min)
|
|
16
|
+
x = if x < 0. then 0. else x end
|
|
17
|
+
x = if x > 1. then 1. else x end
|
|
18
|
+
n = int_of_float(x * float_of_int(bar_width))
|
|
19
|
+
bar = ref("")
|
|
20
|
+
if not scroll then bar := "\r" end
|
|
21
|
+
for _ = 0 to n - 1 do bar := bar() ^ "=" end
|
|
22
|
+
for _ = 0 to bar_width - n - 1 do bar := bar() ^ "." end
|
|
23
|
+
bar :=
|
|
24
|
+
bar() ^
|
|
25
|
+
" " ^
|
|
26
|
+
string(v)
|
|
27
|
+
if scroll then bar := bar() ^ "\n" end
|
|
28
|
+
print(newline=false, bar())
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
thread.run(fast=true, every=window, display)
|
|
32
|
+
s
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# VU meter: display the audio volume (RMS in dB). This adds a video track to the
|
|
36
|
+
# source.
|
|
37
|
+
# @category Source / Visualization
|
|
38
|
+
# @flag extra
|
|
39
|
+
# @param ~rms_min Minimal volume (dB).
|
|
40
|
+
# @param ~rms_max Maximal volume (dB).
|
|
41
|
+
# @param ~window Duration in seconds of volume computation.
|
|
42
|
+
# @param ~color Color of the display (0xRRGGBB).
|
|
43
|
+
# @param ~persistence Persistence of the display (s).
|
|
44
|
+
def video.vumeter(
|
|
45
|
+
~rms_min=-35.,
|
|
46
|
+
~rms_max=0.,
|
|
47
|
+
~window=0.1,
|
|
48
|
+
~color=0xff0000,
|
|
49
|
+
~persistence=0.,
|
|
50
|
+
s
|
|
51
|
+
) =
|
|
52
|
+
s = source(s.{video = source.tracks(blank()).video})
|
|
53
|
+
s = rms(duration=window, s)
|
|
54
|
+
height = video.frame.height()
|
|
55
|
+
width = ref(0)
|
|
56
|
+
|
|
57
|
+
def update() =
|
|
58
|
+
v = dB_of_lin(s.rms())
|
|
59
|
+
x = (v - rms_min) / (rms_max - rms_min)
|
|
60
|
+
x = if x < 0. then 0. else x end
|
|
61
|
+
x = if x > 1. then 1. else x end
|
|
62
|
+
width := int_of_float(x * float_of_int(video.frame.width()))
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
thread.run(fast=true, every=window, update)
|
|
66
|
+
s = video.add_rectangle(width=width, height=height, color=color, s)
|
|
67
|
+
video.persistence(duration=persistence, s)
|
|
68
|
+
end
|