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,677 @@
|
|
|
1
|
+
# Compand the signal.
|
|
2
|
+
# @category Source / Audio processing
|
|
3
|
+
# @flag extra
|
|
4
|
+
# @argsof track.audio.compand
|
|
5
|
+
def compand(~id=null("compand"), %argsof(track.audio.compand[!id]), s) =
|
|
6
|
+
tracks = source.tracks(s)
|
|
7
|
+
source(
|
|
8
|
+
id=id,
|
|
9
|
+
tracks.{
|
|
10
|
+
audio = track.audio.compand(%argsof(track.audio.compand), tracks.audio)
|
|
11
|
+
}
|
|
12
|
+
)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Comb filter
|
|
16
|
+
# @category Source / Audio processing
|
|
17
|
+
# @argsof track.audio.comb
|
|
18
|
+
# @flag extra
|
|
19
|
+
def comb(~id=null("comb"), %argsof(track.audio.comb[!id]), s) =
|
|
20
|
+
tracks = source.tracks(s)
|
|
21
|
+
source(
|
|
22
|
+
id=id,
|
|
23
|
+
tracks.{audio = track.audio.comb(%argsof(track.audio.comb), tracks.audio)}
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Compress the signal.
|
|
28
|
+
# @category Source / Audio processing
|
|
29
|
+
# @argsof track.audio.compress
|
|
30
|
+
# @flag extra
|
|
31
|
+
def compress(%argsof(track.audio.compress), s) =
|
|
32
|
+
tracks = source.tracks(s)
|
|
33
|
+
let {gain, rms, ...audio} =
|
|
34
|
+
track.audio.compress(%argsof(track.audio.compress), tracks.audio)
|
|
35
|
+
|
|
36
|
+
source(id=id, tracks.{audio = audio}).{gain = gain, rms = rms}
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Exponential compressor.
|
|
40
|
+
# @category Source / Audio processing
|
|
41
|
+
# @argsof track.audio.compress.exponential
|
|
42
|
+
# @flag extra
|
|
43
|
+
def compress.exponential(%argsof(track.audio.compress.exponential), s) =
|
|
44
|
+
tracks = source.tracks(s)
|
|
45
|
+
source(
|
|
46
|
+
id=id,
|
|
47
|
+
tracks.{
|
|
48
|
+
audio =
|
|
49
|
+
track.audio.compress.exponential(
|
|
50
|
+
%argsof(track.audio.compress.exponential),
|
|
51
|
+
tracks.audio
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# A limiter. This is a `compress` with tweaked parameters.
|
|
58
|
+
# @category Source / Audio processing
|
|
59
|
+
# @flag extra
|
|
60
|
+
def limit(
|
|
61
|
+
~id=null,
|
|
62
|
+
~attack=getter(50.),
|
|
63
|
+
~release=getter(200.),
|
|
64
|
+
~ratio=getter(20.),
|
|
65
|
+
~threshold=getter(-2.),
|
|
66
|
+
~pre_gain=getter(0.),
|
|
67
|
+
~gain=getter(0.),
|
|
68
|
+
s
|
|
69
|
+
) =
|
|
70
|
+
compress(
|
|
71
|
+
id=id,
|
|
72
|
+
attack=attack,
|
|
73
|
+
release=release,
|
|
74
|
+
ratio=ratio,
|
|
75
|
+
threshold=threshold,
|
|
76
|
+
pre_gain=pre_gain,
|
|
77
|
+
gain=gain,
|
|
78
|
+
s
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
let limiter = limit
|
|
83
|
+
|
|
84
|
+
# A bandpass filter obtained by chaining a low-pass and a high-pass filter.
|
|
85
|
+
# @category Source / Audio processing
|
|
86
|
+
# @flag extra
|
|
87
|
+
# @param id Force the value of the source ID.
|
|
88
|
+
# @param ~low Lower frequency of the bandpass filter.
|
|
89
|
+
# @param ~high Higher frequency of the bandpass filter.
|
|
90
|
+
# @param ~q Q factor.
|
|
91
|
+
def filter.iir.eq.low_high(~id=null, ~low, ~high, ~q=1., s) =
|
|
92
|
+
s =
|
|
93
|
+
if
|
|
94
|
+
not (getter.is_constant(high) and getter.get(high) == infinity)
|
|
95
|
+
then
|
|
96
|
+
filter.iir.eq.low(id=id, frequency=high, q=q, s)
|
|
97
|
+
else
|
|
98
|
+
s
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
s =
|
|
102
|
+
if
|
|
103
|
+
not (getter.is_constant(low) and getter.get(low) == 0.)
|
|
104
|
+
then
|
|
105
|
+
filter.iir.eq.high(id=id, frequency=low, q=q, s)
|
|
106
|
+
else
|
|
107
|
+
s
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
s
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Multiband compression. The list in argument specifies
|
|
114
|
+
# - the `frequency` below which we should apply compression (it is above previous band)
|
|
115
|
+
# - the `attack` time (ms)
|
|
116
|
+
# - the `release` time (ms)
|
|
117
|
+
# - the compression `ratio`
|
|
118
|
+
# - the `threshold` for compression
|
|
119
|
+
# - the `gain` for the band
|
|
120
|
+
# @category Source / Audio processing
|
|
121
|
+
# @param ~limit Also apply limiting to bands.
|
|
122
|
+
# @param l Parameters for compression bands.
|
|
123
|
+
# @param s Source on which multiband compression should be applied.
|
|
124
|
+
# @flag extra
|
|
125
|
+
def compress.multiband(~limit=true, ~wet=getter(1.), s, l) =
|
|
126
|
+
# Check that the bands are with increasing frequencies.
|
|
127
|
+
for i = 0 to list.length(l) - 2 do
|
|
128
|
+
if
|
|
129
|
+
getter.get(list.nth(l, i + 1).frequency) <
|
|
130
|
+
getter.get(list.nth(l, i).frequency)
|
|
131
|
+
then
|
|
132
|
+
failwith(
|
|
133
|
+
"Bands should be sorted."
|
|
134
|
+
)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Process a band
|
|
140
|
+
def band(low, band) =
|
|
141
|
+
high =
|
|
142
|
+
if
|
|
143
|
+
getter.is_constant(band.frequency)
|
|
144
|
+
and getter.get(band.frequency) >= float_of_int(audio.samplerate()) / 2.
|
|
145
|
+
then
|
|
146
|
+
infinity
|
|
147
|
+
else
|
|
148
|
+
band.frequency
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
s = filter.iir.eq.low_high(low=low, high=high, s)
|
|
152
|
+
s =
|
|
153
|
+
compress(
|
|
154
|
+
attack=band.attack,
|
|
155
|
+
release=band.release,
|
|
156
|
+
threshold=band.threshold,
|
|
157
|
+
ratio=band.ratio,
|
|
158
|
+
gain=band.gain,
|
|
159
|
+
s
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
if limit then limiter(s) else s end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
ls =
|
|
166
|
+
list.mapi(
|
|
167
|
+
fun (i, b) ->
|
|
168
|
+
band(if i == 0 then 0. else list.nth(l, i - 1).frequency end, b),
|
|
169
|
+
l
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
c = add(normalize=false, ls)
|
|
173
|
+
s =
|
|
174
|
+
if
|
|
175
|
+
not getter.is_constant(wet) or getter.get(wet) != 1.
|
|
176
|
+
then
|
|
177
|
+
add(
|
|
178
|
+
normalize=false,
|
|
179
|
+
[amplify({1. - getter.get(wet)}, s), amplify(wet, c)]
|
|
180
|
+
)
|
|
181
|
+
else
|
|
182
|
+
c
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Seal l element type
|
|
186
|
+
if false then () else list.hd(l) end
|
|
187
|
+
|
|
188
|
+
# Limit to avoid bad surprises
|
|
189
|
+
limiter(s)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Compress and normalize, producing a more uniform and "full" sound.
|
|
193
|
+
# @category Source / Audio processing
|
|
194
|
+
# @flag extra
|
|
195
|
+
# @param s The input source.
|
|
196
|
+
def nrj(s) =
|
|
197
|
+
compress(threshold=-15., ratio=3., gain=3., normalize(s))
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Multiband-compression.
|
|
201
|
+
# @category Source / Audio processing
|
|
202
|
+
# @flag extra
|
|
203
|
+
# @param s The input source.
|
|
204
|
+
def sky(s) =
|
|
205
|
+
# 3-band crossover
|
|
206
|
+
low = fun (s) -> filter.iir.eq.low(frequency=168., s)
|
|
207
|
+
mh = fun (s) -> filter.iir.eq.high(frequency=100., s)
|
|
208
|
+
mid = fun (s) -> filter.iir.eq.low(frequency=1800., s)
|
|
209
|
+
high = fun (s) -> filter.iir.eq.high(frequency=1366., s)
|
|
210
|
+
|
|
211
|
+
# Add back
|
|
212
|
+
add(
|
|
213
|
+
normalize=false,
|
|
214
|
+
[
|
|
215
|
+
compress(
|
|
216
|
+
attack=100.,
|
|
217
|
+
release=200.,
|
|
218
|
+
threshold=-20.,
|
|
219
|
+
ratio=6.,
|
|
220
|
+
gain=6.7,
|
|
221
|
+
knee=0.3,
|
|
222
|
+
low(s)
|
|
223
|
+
),
|
|
224
|
+
compress(
|
|
225
|
+
attack=100.,
|
|
226
|
+
release=200.,
|
|
227
|
+
threshold=-20.,
|
|
228
|
+
ratio=6.,
|
|
229
|
+
gain=6.7,
|
|
230
|
+
knee=0.3,
|
|
231
|
+
mid(mh(s))
|
|
232
|
+
),
|
|
233
|
+
compress(
|
|
234
|
+
attack=100.,
|
|
235
|
+
release=200.,
|
|
236
|
+
threshold=-20.,
|
|
237
|
+
ratio=6.,
|
|
238
|
+
gain=6.7,
|
|
239
|
+
knee=0.3,
|
|
240
|
+
high(s)
|
|
241
|
+
)
|
|
242
|
+
]
|
|
243
|
+
)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Add some bass to the sound.
|
|
247
|
+
# @category Source / Audio processing
|
|
248
|
+
# @param ~frequency Frequency below which sound is considered as bass.
|
|
249
|
+
# @param ~gain Amount of boosting (dB).
|
|
250
|
+
# @param s Source whose bass should be boosted
|
|
251
|
+
# @flag extra
|
|
252
|
+
def bass_boost(~frequency=getter(200.), ~gain=getter(10.), s) =
|
|
253
|
+
bass = limit(pre_gain=gain, filter.iir.eq.low(frequency=frequency, s))
|
|
254
|
+
add([s, bass])
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
%ifdef soundtouch
|
|
258
|
+
# Increases the pitch, making voices sound like on helium.
|
|
259
|
+
# @category Source / Audio processing
|
|
260
|
+
# @flag extra
|
|
261
|
+
# @param s The input source.
|
|
262
|
+
def helium(s) =
|
|
263
|
+
soundtouch(pitch=1.5, s)
|
|
264
|
+
end
|
|
265
|
+
%endif
|
|
266
|
+
|
|
267
|
+
# Remove low frequencies often produced by microphones.
|
|
268
|
+
# @flag extra
|
|
269
|
+
# @category Source / Audio processing
|
|
270
|
+
# @param ~frequency Frequency under which sound should be lowered.
|
|
271
|
+
# @param s The input source.
|
|
272
|
+
def mic_filter(~frequency=200., s) =
|
|
273
|
+
filter(freq=frequency, q=1., mode="high", s)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Mix between dry and wet sources. Useful for testing effects. Typically:
|
|
277
|
+
# ```
|
|
278
|
+
# c = interactive.float("wetness", min=0., max=1., 1.)
|
|
279
|
+
# s = dry_wet(c, s, effect(s))
|
|
280
|
+
# ```
|
|
281
|
+
# and vary `c` to hear the difference between the source without and with
|
|
282
|
+
# the effect.
|
|
283
|
+
# @flag extra
|
|
284
|
+
# @category Source / Audio processing
|
|
285
|
+
# @param ~power If `true` use constant power mixing.
|
|
286
|
+
# @param wetness Wetness coefficient, from 0 (fully dry) to 1 (fully wet).
|
|
287
|
+
# @param dry Dry source.
|
|
288
|
+
# @param wet Wet source.
|
|
289
|
+
def dry_wet(~power=false, wetness, dry, wet) =
|
|
290
|
+
add(
|
|
291
|
+
power=power,
|
|
292
|
+
weights=[getter.map(fun (x) -> 1. - x, wetness), wetness],
|
|
293
|
+
[dry, wet]
|
|
294
|
+
)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Generate DTMF tones.
|
|
298
|
+
# @flag extra
|
|
299
|
+
# @category Source / Sound synthesis
|
|
300
|
+
# @param ~duration Duration of a tone (in seconds).
|
|
301
|
+
# @param ~delay Dealy between two successive tones (in seconds).
|
|
302
|
+
# @param dtmf String describing DTMF tones to generates: it should contains characters 0 to 9, A to D, or * or #.
|
|
303
|
+
def replaces dtmf(~duration=0.1, ~delay=0.05, dtmf) =
|
|
304
|
+
l = ref([])
|
|
305
|
+
for i = 0 to string.bytes.length(dtmf) - 1 do
|
|
306
|
+
c = string.sub(encoding="ascii", dtmf, start=i, length=1)
|
|
307
|
+
let (row, col) =
|
|
308
|
+
if
|
|
309
|
+
c == "1"
|
|
310
|
+
then
|
|
311
|
+
(697., 1209.)
|
|
312
|
+
elsif
|
|
313
|
+
c == "2"
|
|
314
|
+
then
|
|
315
|
+
(697., 1336.)
|
|
316
|
+
elsif
|
|
317
|
+
c == "3"
|
|
318
|
+
then
|
|
319
|
+
(697., 1477.)
|
|
320
|
+
elsif
|
|
321
|
+
c == "A"
|
|
322
|
+
then
|
|
323
|
+
(697., 1633.)
|
|
324
|
+
elsif
|
|
325
|
+
c == "4"
|
|
326
|
+
then
|
|
327
|
+
(770., 1209.)
|
|
328
|
+
elsif
|
|
329
|
+
c == "5"
|
|
330
|
+
then
|
|
331
|
+
(770., 1336.)
|
|
332
|
+
elsif
|
|
333
|
+
c == "6"
|
|
334
|
+
then
|
|
335
|
+
(770., 1477.)
|
|
336
|
+
elsif
|
|
337
|
+
c == "B"
|
|
338
|
+
then
|
|
339
|
+
(770., 1633.)
|
|
340
|
+
elsif
|
|
341
|
+
c == "7"
|
|
342
|
+
then
|
|
343
|
+
(852., 1209.)
|
|
344
|
+
elsif
|
|
345
|
+
c == "8"
|
|
346
|
+
then
|
|
347
|
+
(852., 1336.)
|
|
348
|
+
elsif
|
|
349
|
+
c == "9"
|
|
350
|
+
then
|
|
351
|
+
(852., 1477.)
|
|
352
|
+
elsif
|
|
353
|
+
c == "C"
|
|
354
|
+
then
|
|
355
|
+
(852., 1633.)
|
|
356
|
+
elsif
|
|
357
|
+
c == "*"
|
|
358
|
+
then
|
|
359
|
+
(941., 1209.)
|
|
360
|
+
elsif
|
|
361
|
+
c == "0"
|
|
362
|
+
then
|
|
363
|
+
(941., 1336.)
|
|
364
|
+
elsif
|
|
365
|
+
c == "#"
|
|
366
|
+
then
|
|
367
|
+
(941., 1477.)
|
|
368
|
+
elsif
|
|
369
|
+
c == "D"
|
|
370
|
+
then
|
|
371
|
+
(941., 1633.)
|
|
372
|
+
else
|
|
373
|
+
(0., 0.)
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
s = add([sine(row, duration=duration), sine(col, duration=duration)])
|
|
377
|
+
l := blank(duration=delay)::l()
|
|
378
|
+
l := s::l()
|
|
379
|
+
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
l = list.rev(l())
|
|
383
|
+
sequence(l)
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# Mixing table controllable via source methods and optional
|
|
387
|
+
# server/telnet commands.
|
|
388
|
+
# @flag extra
|
|
389
|
+
# @category Source / Audio processing
|
|
390
|
+
# @param ~id Force the value of the source ID.
|
|
391
|
+
# @param ~register_server_commands Register corresponding server commands
|
|
392
|
+
# @param ~normalize Normalize source's volume by the number of mixed sources.
|
|
393
|
+
def mix(~id=null, ~register_server_commands=true, ~normalize=false, sources) =
|
|
394
|
+
id = string.id.default(default="mixer", id)
|
|
395
|
+
inputs =
|
|
396
|
+
list.map(
|
|
397
|
+
fun (s) ->
|
|
398
|
+
begin
|
|
399
|
+
volume = ref(1.)
|
|
400
|
+
is_selected = ref(false)
|
|
401
|
+
is_single = ref(false)
|
|
402
|
+
{
|
|
403
|
+
volume = volume,
|
|
404
|
+
selected = is_selected,
|
|
405
|
+
single = is_single,
|
|
406
|
+
source = s
|
|
407
|
+
}
|
|
408
|
+
end,
|
|
409
|
+
sources
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
insert_metadata_fn = ref(fun (_) -> ())
|
|
413
|
+
sources =
|
|
414
|
+
list.map(
|
|
415
|
+
fun (input) ->
|
|
416
|
+
begin
|
|
417
|
+
s = amplify(input.volume, input.source)
|
|
418
|
+
s.on_track(
|
|
419
|
+
synchronous=true,
|
|
420
|
+
fun (_) -> if input.single() then input.selected := false end
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
s.on_metadata(
|
|
424
|
+
synchronous=true,
|
|
425
|
+
fun (m) ->
|
|
426
|
+
begin
|
|
427
|
+
fn = insert_metadata_fn()
|
|
428
|
+
fn(m)
|
|
429
|
+
end
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
switch([(input.selected, s)])
|
|
433
|
+
end,
|
|
434
|
+
inputs
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
s = add(normalize=normalize, sources)
|
|
438
|
+
let {metadata = _, ...tracks} = source.tracks(s)
|
|
439
|
+
s = source(tracks)
|
|
440
|
+
insert_metadata_fn := s.insert_metadata
|
|
441
|
+
let {track_marks = _, ...tracks} = source.tracks(s)
|
|
442
|
+
s = source(id=id, tracks)
|
|
443
|
+
if
|
|
444
|
+
register_server_commands
|
|
445
|
+
then
|
|
446
|
+
def status(input) =
|
|
447
|
+
"ready=#{source.is_ready(input.source)} selected=#{input.selected()} \
|
|
448
|
+
single=#{input.single()} volume=#{int_of_float(input.volume() * 100.)}% \
|
|
449
|
+
remaining=#{source.remaining(input.source)}"
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
s.register_command(
|
|
453
|
+
description="Skip current track on all enabled sources.",
|
|
454
|
+
"skip",
|
|
455
|
+
fun (_) ->
|
|
456
|
+
begin
|
|
457
|
+
list.iter(
|
|
458
|
+
fun (input) ->
|
|
459
|
+
if input.selected() then source.skip(input.source) end,
|
|
460
|
+
inputs
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
"OK"
|
|
464
|
+
end
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
s.register_command(
|
|
468
|
+
description="Set volume for a given source.",
|
|
469
|
+
usage="volume <source nb> <vol%>",
|
|
470
|
+
"volume",
|
|
471
|
+
fun (v) ->
|
|
472
|
+
begin
|
|
473
|
+
try
|
|
474
|
+
let [i, v] = r/\s/.split(v)
|
|
475
|
+
input = list.nth(inputs, int_of_string(i))
|
|
476
|
+
input.volume := float_of_string(v)
|
|
477
|
+
status(input)
|
|
478
|
+
catch _ do
|
|
479
|
+
"Usage: volume <source nb> <vol%>"
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
s.register_command(
|
|
485
|
+
description="Enable/disable a source.",
|
|
486
|
+
usage="select <source nb> <true|false>",
|
|
487
|
+
"select",
|
|
488
|
+
fun (arg) ->
|
|
489
|
+
begin
|
|
490
|
+
try
|
|
491
|
+
let [i, b] = r/\s/.split(arg)
|
|
492
|
+
input = list.nth(inputs, int_of_string(i))
|
|
493
|
+
input.selected := (b == "true")
|
|
494
|
+
status(input)
|
|
495
|
+
catch _ do
|
|
496
|
+
"Usage: select <source nb> <true|false>"
|
|
497
|
+
end
|
|
498
|
+
end
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
s.register_command(
|
|
502
|
+
description="Enable/disable automatic stop at the end of track.",
|
|
503
|
+
usage="single <source nb> <true|false>",
|
|
504
|
+
"single",
|
|
505
|
+
fun (arg) ->
|
|
506
|
+
begin
|
|
507
|
+
try
|
|
508
|
+
let [i, b] = r/\s/.split(arg)
|
|
509
|
+
input = list.nth(inputs, int_of_string(i))
|
|
510
|
+
input.single := (b == "true")
|
|
511
|
+
status(input)
|
|
512
|
+
catch _ do
|
|
513
|
+
"Usage: single <source nb> <true|false>"
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
s.register_command(
|
|
519
|
+
description="Display current status.",
|
|
520
|
+
"status",
|
|
521
|
+
fun (i) ->
|
|
522
|
+
begin
|
|
523
|
+
try
|
|
524
|
+
status(list.nth(inputs, int_of_string(i)))
|
|
525
|
+
catch _ do
|
|
526
|
+
"Usage: status <source nb>"
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
s.register_command(
|
|
532
|
+
description="Print the list of input sources.",
|
|
533
|
+
"inputs",
|
|
534
|
+
fun (_) ->
|
|
535
|
+
string.concat(
|
|
536
|
+
separator=" ",
|
|
537
|
+
list.map(fun (input) -> source.id(input.source), inputs)
|
|
538
|
+
)
|
|
539
|
+
)
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
s.{inputs = inputs}
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
# Indicate beats.
|
|
546
|
+
# @category Source / Sound synthesis
|
|
547
|
+
# @param ~frequency Frequency of the sound.
|
|
548
|
+
# @param bpm Number of beats per minute.
|
|
549
|
+
# @flag extra
|
|
550
|
+
def metronome(~frequency=440., bpm=60.) =
|
|
551
|
+
volume_down = 0.
|
|
552
|
+
beat_duration = 0.1
|
|
553
|
+
s = sine(frequency)
|
|
554
|
+
|
|
555
|
+
def f() =
|
|
556
|
+
if s.time() mod (60. / bpm) <= beat_duration then 1. else volume_down end
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
amplify(f, s)
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
# Mixes two streams, with faded transitions between the state when only the
|
|
563
|
+
# normal stream is available and when the special stream gets added on top of
|
|
564
|
+
# it.
|
|
565
|
+
# @category Source / Fade
|
|
566
|
+
# @flag extra
|
|
567
|
+
# @param ~duration Duration of the fade in seconds.
|
|
568
|
+
# @param ~p Portion of amplitude of the normal source in the mix.
|
|
569
|
+
# @param ~normal The normal source, which could be called the carrier too.
|
|
570
|
+
# @param ~special The special source.
|
|
571
|
+
def smooth_add(~duration=1., ~p=getter(0.2), ~normal, ~special) =
|
|
572
|
+
p = getter.function(p)
|
|
573
|
+
last_p = ref(p())
|
|
574
|
+
|
|
575
|
+
def c(fn, s) =
|
|
576
|
+
def v() =
|
|
577
|
+
fn = fn()
|
|
578
|
+
fn()
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
fade.scale(v, s)
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
special_volume = ref(fun () -> 0.)
|
|
585
|
+
special = c(special_volume, special)
|
|
586
|
+
normal_volume = ref(fun () -> 1.)
|
|
587
|
+
normal = c(normal_volume, normal)
|
|
588
|
+
|
|
589
|
+
def to_special({starting}) =
|
|
590
|
+
last_p := p()
|
|
591
|
+
q = 1. - last_p()
|
|
592
|
+
normal_volume := mkfade(start=1., stop=last_p(), duration=duration, normal)
|
|
593
|
+
special_volume := mkfade(stop=q, duration=duration, starting)
|
|
594
|
+
starting
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
def to_blank(x) =
|
|
598
|
+
b = x.starting
|
|
599
|
+
normal_volume := mkfade(start=last_p(), duration=duration, normal)
|
|
600
|
+
if
|
|
601
|
+
null.defined(x?.ending)
|
|
602
|
+
then
|
|
603
|
+
special_volume :=
|
|
604
|
+
mkfade(start=1. - last_p(), duration=duration, null.get(x?.ending))
|
|
605
|
+
sequence([null.get(x?.ending), b])
|
|
606
|
+
else
|
|
607
|
+
b
|
|
608
|
+
end
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
special =
|
|
612
|
+
fallback([special.{on_select = to_special}, blank().{on_select = to_blank}])
|
|
613
|
+
|
|
614
|
+
add(normalize=false, [normal, special])
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
%ifencoder %ffmpeg
|
|
618
|
+
# Output an MPEG-DASH playlist.
|
|
619
|
+
# @category Source / Output
|
|
620
|
+
# @flag extra
|
|
621
|
+
# @param ~id Force the value of the source ID.
|
|
622
|
+
# @param ~codec Codec to use for audio (following FFmpeg's conventions).
|
|
623
|
+
# @param ~fallible Allow the child source to fail, in which case the output will be (temporarily) stopped.
|
|
624
|
+
# @param ~start Automatically start outputting whenever possible. If true, an infallible (normal) output will start outputting as soon as it is created, and a fallible output will (re)start as soon as its source becomes available for streaming.
|
|
625
|
+
# @param ~playlist Playlist name
|
|
626
|
+
# @param ~directory Directory to write to
|
|
627
|
+
def output.file.dash(
|
|
628
|
+
~id=null,
|
|
629
|
+
~fallible=false,
|
|
630
|
+
~codec="libmp3lame",
|
|
631
|
+
~bitrate=128,
|
|
632
|
+
~start=true,
|
|
633
|
+
~playlist="stream.mpd",
|
|
634
|
+
~directory,
|
|
635
|
+
s
|
|
636
|
+
) =
|
|
637
|
+
enc = %ffmpeg(format = "dash", %audio(codec = codec, b = "#{bitrate}k"))
|
|
638
|
+
output.file(
|
|
639
|
+
id=id,
|
|
640
|
+
fallible=fallible,
|
|
641
|
+
start=start,
|
|
642
|
+
enc,
|
|
643
|
+
"#{(directory : string)}/#{playlist}",
|
|
644
|
+
s
|
|
645
|
+
)
|
|
646
|
+
end
|
|
647
|
+
%endif
|
|
648
|
+
|
|
649
|
+
# Return a source with a `set_volume` method.
|
|
650
|
+
# This method takes the same arguments as `mkfade`
|
|
651
|
+
# and updates the source volume with a corresponding
|
|
652
|
+
# fade curve
|
|
653
|
+
# @category Source / Audio processing
|
|
654
|
+
# @flag extra
|
|
655
|
+
# @param ~id Force the value of the source ID.
|
|
656
|
+
def smooth_volume(~id=null("smooth_volume"), s) =
|
|
657
|
+
last_volume = ref(1.)
|
|
658
|
+
volume_ref = ref(fun () -> 1.)
|
|
659
|
+
|
|
660
|
+
def volume() =
|
|
661
|
+
fn = volume_ref()
|
|
662
|
+
fn()
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
def set_volume(%argsof(mkfade[!start,!stop,!delay,!on_done]), v) =
|
|
666
|
+
volume_ref :=
|
|
667
|
+
mkfade(
|
|
668
|
+
%argsof(mkfade[!start,!stop,!delay,!on_done]),
|
|
669
|
+
start=last_volume(),
|
|
670
|
+
stop=v,
|
|
671
|
+
s
|
|
672
|
+
)
|
|
673
|
+
last_volume := v
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
amplify(id=id, volume, s).{set_volume = set_volume}
|
|
677
|
+
end
|