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