liquidsoap-prettier 1.8.2 → 1.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/.github/workflows/check-formatting.yml +32 -0
  2. package/README.md +31 -5
  3. package/package.json +1 -1
  4. package/src/cli.js +104 -9
  5. package/tests/liq/audio.liq +460 -0
  6. package/tests/liq/autocue.liq +1081 -0
  7. package/tests/liq/clock.liq +14 -0
  8. package/tests/liq/cron.liq +74 -0
  9. package/tests/liq/error.liq +48 -0
  10. package/tests/liq/extra/audio.liq +677 -0
  11. package/tests/liq/extra/audioscrobbler.liq +482 -0
  12. package/tests/liq/extra/deprecations.liq +976 -0
  13. package/tests/liq/extra/externals.liq +196 -0
  14. package/tests/liq/extra/fades.liq +260 -0
  15. package/tests/liq/extra/file.liq +66 -0
  16. package/tests/liq/extra/http.liq +160 -0
  17. package/tests/liq/extra/interactive.liq +917 -0
  18. package/tests/liq/extra/metadata.liq +75 -0
  19. package/tests/liq/extra/native.liq +201 -0
  20. package/tests/liq/extra/openai.liq +150 -0
  21. package/tests/liq/extra/server.liq +177 -0
  22. package/tests/liq/extra/source.liq +476 -0
  23. package/tests/liq/extra/spinitron.liq +272 -0
  24. package/tests/liq/extra/telnet.liq +266 -0
  25. package/tests/liq/extra/video.liq +59 -0
  26. package/tests/liq/extra/visualization.liq +68 -0
  27. package/tests/liq/fades.liq +941 -0
  28. package/tests/liq/ffmpeg.liq +605 -0
  29. package/tests/liq/file.liq +387 -0
  30. package/tests/liq/getter.liq +74 -0
  31. package/tests/liq/hls.liq +329 -0
  32. package/tests/liq/http.liq +1048 -0
  33. package/tests/liq/http_codes.liq +447 -0
  34. package/tests/liq/icecast.liq +58 -0
  35. package/tests/liq/io.liq +106 -0
  36. package/tests/liq/liquidsoap.liq +31 -0
  37. package/tests/liq/list.liq +440 -0
  38. package/tests/liq/log.liq +47 -0
  39. package/tests/liq/lufs.liq +295 -0
  40. package/tests/liq/math.liq +23 -0
  41. package/tests/liq/medialib.liq +752 -0
  42. package/tests/liq/metadata.liq +253 -0
  43. package/tests/liq/nfo.liq +258 -0
  44. package/tests/liq/null.liq +71 -0
  45. package/tests/liq/playlist.liq +1347 -0
  46. package/tests/liq/predicate.liq +106 -0
  47. package/tests/liq/process.liq +93 -0
  48. package/tests/liq/profiler.liq +5 -0
  49. package/tests/liq/protocols.liq +1139 -0
  50. package/tests/liq/ref.liq +28 -0
  51. package/tests/liq/replaygain.liq +135 -0
  52. package/tests/liq/request.liq +467 -0
  53. package/tests/liq/resolvers.liq +33 -0
  54. package/tests/liq/runtime.liq +70 -0
  55. package/tests/liq/server.liq +99 -0
  56. package/tests/liq/settings.liq +41 -0
  57. package/tests/liq/socket.liq +33 -0
  58. package/tests/liq/source.liq +362 -0
  59. package/tests/liq/sqlite.liq +161 -0
  60. package/tests/liq/stdlib.liq +172 -0
  61. package/tests/liq/string.liq +476 -0
  62. package/tests/liq/switches.liq +197 -0
  63. package/tests/liq/testing.liq +37 -0
  64. package/tests/liq/thread.liq +161 -0
  65. package/tests/liq/tracks.liq +100 -0
  66. package/tests/liq/utils.liq +81 -0
  67. package/tests/liq/video.liq +918 -0
@@ -0,0 +1,32 @@
1
+ name: Check formatting
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ pull_request:
8
+ merge_group:
9
+
10
+ concurrency:
11
+ group: ${{ github.workflow }}-${{ github.ref }}
12
+ cancel-in-progress: true
13
+
14
+ jobs:
15
+ check:
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+
20
+ - uses: actions/setup-node@v4
21
+ with:
22
+ node-version: lts/*
23
+
24
+ - uses: pnpm/action-setup@v4
25
+ with:
26
+ version: 10
27
+ run_install: true
28
+
29
+ - run: pnpm dev:prepare
30
+
31
+ - name: Check that stdlib files are formatted
32
+ run: node src/cli.js --check 'tests/liq/**/*.liq'
package/README.md CHANGED
@@ -11,18 +11,44 @@ The `liquidsoap-prettier` command-line utility should be installed with this
11
11
  package and should be available following the usual node package binary
12
12
  conventions.
13
13
 
14
- It works as follows:
14
+ Format one or more files in place:
15
15
 
16
16
  ```shell
17
- $ liquidsoap-prettier [-w|--write] path/to/file.liq "path/with/glob/pattern/**/*.liq"
17
+ $ liquidsoap-prettier -w path/to/file.liq "path/with/glob/pattern/**/*.liq"
18
18
  ```
19
19
 
20
- You can also simply check the script without formatting it:
20
+ Print formatted output to stdout (single file only):
21
+
22
+ ```shell
23
+ $ liquidsoap-prettier path/to/file.liq
24
+ ```
25
+
26
+ Read from stdin and write to stdout:
27
+
28
+ ```shell
29
+ $ cat path/to/file.liq | liquidsoap-prettier --stdin-filepath file.liq
30
+ ```
31
+
32
+ Check whether files are already formatted (useful in CI):
33
+
21
34
  ```shell
22
- $ liquidsoap-prettier [-c|--check] path/to/file.liq "path/with/glob/pattern/**/*.liq"
35
+ $ liquidsoap-prettier -c path/to/file.liq "path/with/glob/pattern/**/*.liq"
23
36
  ```
24
37
 
25
- The program returns with exit code `0` when the script is already pretty-printed and `2` otherwise.
38
+ Returns exit code `0` when all files are already formatted, `2` otherwise.
39
+
40
+ Dump the parsed AST as JSON (useful when debugging the printer):
41
+
42
+ ```shell
43
+ $ liquidsoap-prettier --dump-ast path/to/file.liq
44
+ ```
45
+
46
+ Print the version or show all options:
47
+
48
+ ```shell
49
+ $ liquidsoap-prettier --version
50
+ $ liquidsoap-prettier --help
51
+ ```
26
52
 
27
53
  ### Prettier plugin
28
54
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "liquidsoap-prettier",
3
- "version": "1.8.2",
3
+ "version": "1.8.3",
4
4
  "description": "Liquidsoap language prettier CLI and plugin",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/cli.js CHANGED
@@ -5,38 +5,130 @@ import prettier from "prettier";
5
5
  import fs from "node:fs";
6
6
  import { glob } from "glob";
7
7
  import BluebirdPromise from "bluebird";
8
+ import { createRequire } from "node:module";
8
9
  import * as prettierPluginLiquidsoap from "./index.js";
9
10
 
11
+ const require = createRequire(import.meta.url);
12
+ const { version } = require("../package.json");
13
+
14
+ const HELP = `\
15
+ Usage: liquidsoap-prettier [options] <file> [<file> ...]
16
+ liquidsoap-prettier [options] --stdin-filepath <name>
17
+
18
+ Format Liquidsoap (.liq) files.
19
+
20
+ Arguments:
21
+ <file> One or more files or glob patterns to format.
22
+
23
+ Options:
24
+ -w, --write Write formatted output back to each file in place.
25
+ -c, --check Check if files are already formatted (exit 2 if not).
26
+ --print-width Maximum line width (default: 80).
27
+ --stdin-filepath Read from stdin; use <name> in error messages.
28
+ --dump-ast Parse and dump the AST as JSON (for debugging).
29
+ -v, --version Print the version and exit.
30
+ -h, --help Show this help message and exit.
31
+
32
+ Examples:
33
+ liquidsoap-prettier script.liq # print formatted output to stdout
34
+ liquidsoap-prettier -w script.liq # format file in place
35
+ liquidsoap-prettier -w '**/*.liq' # format all .liq files in place
36
+ liquidsoap-prettier -c '**/*.liq' # check formatting of all .liq files
37
+ liquidsoap-prettier -c script.liq # check formatting (CI mode)
38
+ cat script.liq | liquidsoap-prettier --stdin-filepath script.liq
39
+ liquidsoap-prettier --dump-ast script.liq # dump parsed AST as JSON
40
+ `;
41
+
42
+ const readStdin = () =>
43
+ new Promise((resolve, reject) => {
44
+ let data = "";
45
+ process.stdin.setEncoding("utf8");
46
+ process.stdin.on("data", (chunk) => (data += chunk));
47
+ process.stdin.on("end", () => resolve(data));
48
+ process.stdin.on("error", reject);
49
+ });
50
+
10
51
  const run = async () => {
11
52
  try {
12
53
  const {
13
- _: [filename],
54
+ _: patterns,
14
55
  write,
15
56
  w,
16
57
  check,
17
58
  c,
59
+ version: showVersion,
60
+ v,
61
+ help,
62
+ h,
18
63
  "print-width": printWidth = 80,
64
+ "stdin-filepath": stdinFilepath,
65
+ "dump-ast": dumpAst,
19
66
  } = parseArgs(process.argv.slice(2), {
20
- boolean: ["w", "write", "c", "check"],
67
+ boolean: ["w", "write", "c", "check", "v", "version", "h", "help", "dump-ast"],
68
+ string: ["stdin-filepath"],
21
69
  number: ["print-width"],
22
70
  });
23
71
 
24
- if (!filename) {
72
+ if (help || h) {
73
+ process.stdout.write(HELP);
74
+ process.exit(0);
75
+ }
76
+
77
+ if (showVersion || v) {
78
+ console.log(version);
79
+ process.exit(0);
80
+ }
81
+
82
+ if (stdinFilepath) {
83
+ if (write || w) {
84
+ console.error("--write cannot be used with --stdin-filepath");
85
+ process.exit(1);
86
+ }
87
+ const code = await readStdin();
88
+ try {
89
+ if (dumpAst) {
90
+ const ast = prettierPluginLiquidsoap.parsers.liquidsoap.parse(code);
91
+ process.stdout.write(JSON.stringify(ast, null, 2) + "\n");
92
+ } else if (check || c) {
93
+ const isFormatted = await prettier.check(code, {
94
+ parser: "liquidsoap",
95
+ plugins: [prettierPluginLiquidsoap],
96
+ printWidth,
97
+ });
98
+ process.exit(isFormatted ? 0 : 2);
99
+ } else {
100
+ const formattedCode = await prettier.format(code, {
101
+ parser: "liquidsoap",
102
+ plugins: [prettierPluginLiquidsoap],
103
+ printWidth,
104
+ });
105
+ process.stdout.write(formattedCode);
106
+ }
107
+ } catch (err) {
108
+ console.error(`Error while processing ${stdinFilepath}: ${err}`);
109
+ process.exit(1);
110
+ }
111
+ process.exit(0);
112
+ }
113
+
114
+ if (patterns.length === 0) {
25
115
  console.error("No filename passed!");
26
116
  process.exit(1);
27
117
  }
28
118
 
29
- const files = await glob(filename);
119
+ const files = (
120
+ await BluebirdPromise.map(patterns, (pattern) => glob(pattern))
121
+ ).flat();
30
122
 
31
- if (files.length > 1 && !w && !write) {
123
+ if (files.length > 1 && !w && !write && !dumpAst && !c && !check) {
32
124
  console.error(
33
- "-w|--write must be used when formatting more than one file at a time!",
125
+ "-w|--write or -c|--check must be used when passing more than one file!",
34
126
  );
35
127
  process.exit(1);
36
128
  }
37
129
 
38
130
  if (files.length === 0) {
39
- console.error("No file passed!");
131
+ console.error("No file found matching the given pattern(s)!");
40
132
  process.exit(1);
41
133
  }
42
134
 
@@ -51,7 +143,10 @@ const run = async () => {
51
143
  const code = "" + fs.readFileSync(file);
52
144
 
53
145
  try {
54
- if (check || c) {
146
+ if (dumpAst) {
147
+ const ast = prettierPluginLiquidsoap.parsers.liquidsoap.parse(code);
148
+ process.stdout.write(JSON.stringify(ast, null, 2) + "\n");
149
+ } else if (check || c) {
55
150
  const isFormatted = await prettier.check(code, {
56
151
  parser: "liquidsoap",
57
152
  plugins: [prettierPluginLiquidsoap],
@@ -66,7 +161,7 @@ const run = async () => {
66
161
  });
67
162
 
68
163
  if (write || w) {
69
- console.log(`Writting formatted ${file}`);
164
+ console.log(`Writing formatted ${file}`);
70
165
  fs.writeFileSync(file, formattedCode);
71
166
  } else {
72
167
  process.stdout.write(formattedCode);
@@ -0,0 +1,460 @@
1
+ audio = ()
2
+ let audio.encode = ()
3
+
4
+ # Encode audio track to pcm_s16
5
+ # @category Source / Audio processing
6
+ def audio.encode.pcm_s16(~id=null("audio.encode.pcm_s16"), s) =
7
+ let {audio, ...tracks} = source.tracks(s)
8
+ source(id=id, tracks.{audio = track.encode.audio.pcm_s16(audio)})
9
+ end
10
+
11
+ # Encode audio track to pcm_f32
12
+ # @category Source / Audio processing
13
+ def audio.encode.pcm_f32(~id=null("audio.encode.pcm_f32"), s) =
14
+ let {audio, ...tracks} = source.tracks(s)
15
+ source(id=id, tracks.{audio = track.encode.audio.pcm_f32(audio)})
16
+ end
17
+
18
+ let audio.decode = ()
19
+
20
+ # Decode audio track to pcm_s16
21
+ # @category Source / Audio processing
22
+ def audio.decode.pcm_s16(~id=null("audio.decode.pcm_s16"), s) =
23
+ let {audio, ...tracks} = source.tracks(s)
24
+ source(id=id, tracks.{audio = track.decode.audio.pcm_s16(audio)})
25
+ end
26
+
27
+ # Decode audio track to pcm_f32
28
+ # @category Source / Audio processing
29
+ def audio.decode.pcm_f32(~id=null("audio.decode.pcm_f32"), s) =
30
+ let {audio, ...tracks} = source.tracks(s)
31
+ source(id=id, tracks.{audio = track.decode.audio.pcm_f32(audio)})
32
+ end
33
+
34
+ # Samplerate for audio.
35
+ # @category Settings
36
+ def audio.samplerate =
37
+ settings.frame.audio.samplerate
38
+ end
39
+
40
+ # Channels for audio.
41
+ # @category Settings
42
+ def audio.channels =
43
+ settings.frame.audio.channels
44
+ end
45
+
46
+ # Multiply the amplitude of the signal.
47
+ # @category Source / Audio processing
48
+ # @param f Multiplicative factor.
49
+ # @argsof track.audio.amplify
50
+ def amplify(~id=null("amplify"), %argsof(track.audio.amplify[!id]), f, s) =
51
+ tracks = source.tracks(s)
52
+ source(
53
+ id=id,
54
+ tracks.{
55
+ audio =
56
+ track.audio.amplify(
57
+ id=null("track.audio.amplify"),
58
+ %argsof(track.audio.amplify[!id]),
59
+ f,
60
+ tracks.audio
61
+ )
62
+ }
63
+ )
64
+ end
65
+
66
+ # Clip samples, i.e. ensure that all values are between
67
+ # `-1` and `1`: values lower than `-1` become `-1` and
68
+ # values higher than `1` become `1`. `nan` values become `0`.
69
+ # @category Source / Audio processing
70
+ # @argsof track.audio.clip
71
+ def clip(~id=null("clip"), %argsof(track.audio.clip[!id]), s) =
72
+ tracks = source.tracks(s)
73
+ source(
74
+ id=id,
75
+ tracks.{audio = track.audio.clip(%argsof(track.audio.clip), tracks.audio)}
76
+ )
77
+ end
78
+
79
+ lufs_builtin = lufs
80
+
81
+ # Normalization the volume of a stream (this is also called _automatic gain
82
+ # control_). Dynamic normalization of the signal is sometimes the only option
83
+ # (for instance, for live sources), and can make a listening experience much
84
+ # nicer. However, its dynamic aspect implies some limitations which can go as
85
+ # far as creating saturation in some extreme cases. If possible, consider using
86
+ # some track-based normalization techniques such as those based on
87
+ # ReplayGain. The implementation of Liquidsoap < 2.0 was renamed to
88
+ # `normalize.old`.
89
+ # @category Source / Audio processing
90
+ # @param ~id Force the value of the source ID.
91
+ # @param ~gain_max Maximal gain value (dB).
92
+ # @param ~gain_min Minimal gain value (dB).
93
+ # @param ~down Characteristic time to go down.
94
+ # @param ~up Characteristic time to go up.
95
+ # @param ~lookahead How much time to look ahead of the signal (second). Setting a positive value delays the output by the corresponding amount of time.
96
+ # @param ~lufs Use LUFS instead of RMS to compute intensity.
97
+ # @param ~target Desired RMS (dB).
98
+ # @param ~threshold Minimal RMS for activaing gain control (dB).
99
+ # @param ~window Duration of the window used to compute the current RMS power (second).
100
+ # @param ~enabled Whether normalization is enabled or not.
101
+ # @param ~debug How often to print debug messages, in seconds, useful to finetune the parameters. You should set `set("log.level", 5)` to see them.
102
+ # @param s Source to normalize.
103
+ # @method gain Current amplification coefficient (in linear scale).
104
+ # @method target_gain Current target amplification coefficient (in linear scale).
105
+ # @method rms Current rms (in linear scale).
106
+ def replaces normalize(
107
+ ~id=null,
108
+ ~target=getter(-13.),
109
+ ~up=getter(10.),
110
+ ~down=getter(.1),
111
+ ~gain_min=-12.,
112
+ ~gain_max=12.,
113
+ ~lufs=false,
114
+ ~lookahead=getter(0.),
115
+ ~window=getter(.5),
116
+ ~threshold=getter(-40.),
117
+ ~track_sensitive=true,
118
+ ~enabled=getter(true),
119
+ ~debug=null,
120
+ s
121
+ ) =
122
+ let (s, rms) =
123
+ if
124
+ lufs
125
+ then
126
+ s = lufs_builtin(id=id, window=window, s)
127
+ (s, {lin_of_dB(s.lufs())})
128
+ else
129
+ s = rms.smooth(id=id, duration=window, s)
130
+ (s, s.rms)
131
+ end
132
+
133
+ v = ref(1.)
134
+ frame = frame.duration()
135
+ gain_min = lin_of_dB(gain_min)
136
+ gain_max = lin_of_dB(gain_max)
137
+
138
+ def update() =
139
+ if
140
+ not (getter.get(enabled))
141
+ then
142
+ v := 1.
143
+ else
144
+ target = lin_of_dB(getter.get(target))
145
+ threshold = lin_of_dB(getter.get(threshold))
146
+ rms = rms()
147
+ if
148
+ rms >= threshold
149
+ then
150
+ if
151
+ v() * rms <= target
152
+ then
153
+ up = 1. - exp(0. - frame / getter.get(up))
154
+ v := v() + up * ((target / rms) - v())
155
+ else
156
+ down = 1. - exp(0. - frame / getter.get(down))
157
+ v := v() + down * ((target / rms) - v())
158
+ end
159
+
160
+ v := max(gain_min, min(gain_max, v()))
161
+ end
162
+ end
163
+ end
164
+
165
+ def target_gain() =
166
+ lin_of_dB(getter.get(target)) / rms()
167
+ end
168
+
169
+ s =
170
+ if
171
+ null.defined(debug)
172
+ then
173
+ source.run(
174
+ s,
175
+ every=null.get(debug),
176
+ {
177
+ log.debug(
178
+ "rms: #{rms()} / #{lin_of_dB(getter.get(target))}\tgain: #{v()} / #{
179
+ target_gain()
180
+ }"
181
+ )
182
+ }
183
+ )
184
+ else
185
+ s
186
+ end
187
+
188
+ s = source.methods(s)
189
+ s.on_frame(synchronous=true, update)
190
+ if track_sensitive then s.on_track(synchronous=true, fun (_) -> v := 1.) end
191
+ amplify(id=id, {v()}, delay_line(lookahead, s)).{
192
+ rms = rms,
193
+ gain = {v()},
194
+ target_gain = target_gain
195
+ }
196
+ end
197
+
198
+ # Swap two channels of a stereo source.
199
+ # @category Source / Conversion
200
+ def swap(id=null("swap"), s) =
201
+ tracks = source.tracks(s)
202
+ source(id=id, tracks.{audio = track.audio.swap(tracks.audio)})
203
+ end
204
+
205
+ # Produce mono audio by taking the mean of all audio channels.
206
+ # @category Source / Conversion
207
+ # @argsof track.audio.mean
208
+ def mean(~id=null("mean"), %argsof(track.audio.mean[!id]), s) =
209
+ tracks = source.tracks(s)
210
+ source(
211
+ id=id,
212
+ tracks.{audio = track.audio.mean(%argsof(track.audio.mean), tracks.audio)}
213
+ )
214
+ end
215
+
216
+ # Convert any pcm audio source into a stereo source.
217
+ # @category Source / Conversion
218
+ def stereo(~id=null("stereo"), s) =
219
+ tracks = source.tracks(s)
220
+ source(id=id, tracks.{audio = track.audio.stereo(tracks.audio)})
221
+ end
222
+
223
+ # Extract the left channel of a stereo track
224
+ # @category Source / Conversion
225
+ # @param t Track to extract from
226
+ def track.audio.stereo.left(~id=null("track.audio.stereo.left"), t) =
227
+ track.audio.amplify(
228
+ id=id,
229
+ override=null,
230
+ 2.,
231
+ track.audio.mean(track.audio.stereo.pan(-1., t))
232
+ )
233
+ end
234
+
235
+ # Extract the left channel of a stereo source
236
+ # @category Source / Conversion
237
+ # @param s Source to extract from
238
+ def stereo.left(~id=null("stereo.left"), s) =
239
+ tracks = source.tracks(s)
240
+ source(id=id, tracks.{audio = track.audio.stereo.left(tracks.audio)})
241
+ end
242
+
243
+ # Extract the right channel of a stereo track
244
+ # @category Source / Conversion
245
+ # @param s Track to extract from
246
+ def track.audio.stereo.right(~id=null("track.audio.stereo.right"), t) =
247
+ track.audio.amplify(
248
+ id=id,
249
+ override=null,
250
+ 2.,
251
+ track.audio.mean(track.audio.stereo.pan(1., t))
252
+ )
253
+ end
254
+
255
+ # Extract the right channel of a stereo source
256
+ # @category Source / Conversion
257
+ # @param s Source to extract from
258
+ def stereo.right(~id=null("stereo.right"), s) =
259
+ tracks = source.tracks(s)
260
+ source(id=id, tracks.{audio = track.audio.stereo.right(tracks.audio)})
261
+ end
262
+
263
+ # Spacializer which allows controlling the width of the signal.
264
+ # @category Source / Audio processing
265
+ # @param w Width of the signal (-1: mono, 0.: original, 1.: wide stereo).
266
+ def stereo.width(~id=null("stereo.width"), w=getter(0.), (s:source)) =
267
+ tracks = source.tracks(s)
268
+ source(id=id, tracks.{audio = track.audio.stereo.width(w, tracks.audio)})
269
+ end
270
+
271
+ # Pan a stereo sound.
272
+ # @category Source / Audio processing
273
+ # @argsof track.audio.stereo.pan
274
+ # @param pan Pan value. Should be between `-1` (left side) and `1` (right side).
275
+ def stereo.pan(
276
+ ~id=null("stereo.pan"),
277
+ %argsof(track.audio.stereo.pan[!id]),
278
+ pan,
279
+ (s:source)
280
+ ) =
281
+ tracks = source.tracks(s)
282
+ source(
283
+ id=id,
284
+ tracks.{
285
+ audio =
286
+ track.audio.stereo.pan(
287
+ %argsof(track.audio.stereo.pan),
288
+ pan,
289
+ tracks.audio
290
+ )
291
+ }
292
+ )
293
+ end
294
+
295
+ # Slow down or accelerate an audio stream by stretching (sounds lower) or squeezing it (sounds higher).
296
+ # @category Source / Audio processing
297
+ # @argsof track.audio.stretch
298
+ def stretch(
299
+ ~id=null("stretch"),
300
+ %argsof(track.audio.stretch[!id]),
301
+ (s:source)
302
+ ) =
303
+ tracks = source.tracks(s)
304
+ source.audio(
305
+ id=id,
306
+ track.audio.stretch(%argsof(track.audio.stretch), tracks.audio)
307
+ )
308
+ end
309
+
310
+ let stereo.ms = ()
311
+
312
+ # Decode mid+side stereo (M/S) to left+right stereo.
313
+ # @category Source / Audio processing
314
+ # @argsof track.audio.stereo.ms.decode
315
+ def stereo.ms.decode(
316
+ ~id=null("stereo.ms.decode"),
317
+ %argsof(track.audio.stereo.ms.decode[!id]),
318
+ (s:source)
319
+ ) =
320
+ tracks = source.tracks(s)
321
+ source(
322
+ id=id,
323
+ tracks.{
324
+ audio =
325
+ track.audio.stereo.ms.decode(
326
+ %argsof(track.audio.stereo.ms.decode),
327
+ tracks.audio
328
+ )
329
+ }
330
+ )
331
+ end
332
+
333
+ # Encode left+right stereo to mid+side stereo (M/S).
334
+ # @category Source / Audio processing
335
+ # @argsof track.audio.stereo.ms.encode
336
+ def stereo.ms.encode(
337
+ ~id=null("stereo.ms.encode"),
338
+ %argsof(track.audio.stereo.ms.encode[!id]),
339
+ (s:source)
340
+ ) =
341
+ tracks = source.tracks(s)
342
+ source(
343
+ id=id,
344
+ tracks.{
345
+ audio =
346
+ track.audio.stereo.ms.encode(
347
+ %argsof(track.audio.stereo.ms.encode),
348
+ tracks.audio
349
+ )
350
+ }
351
+ )
352
+ end
353
+
354
+ %ifdef track.audio.stereotool
355
+ # Process an audio source using stereotool
356
+ # @argsof track.audio.stereotool
357
+ # @category Source / Audio processing
358
+ def stereotool(
359
+ ~id=null("stereotool"),
360
+ %argsof(track.audio.stereotool[!id]),
361
+ s
362
+ ) =
363
+ let {audio, metadata, track_marks, ..._} = source.tracks(s)
364
+ s = track.audio.stereotool(%argsof(track.audio.stereotool[!id]), audio)
365
+ let replaces s =
366
+ source(
367
+ id=id,
368
+ {audio = (s : pcm), metadata = metadata, track_marks = track_marks}
369
+ )
370
+
371
+ s
372
+ end
373
+
374
+ # Output an audio source using stereotool
375
+ # @argsof track.audio.stereotool[!active]
376
+ # @argsof output.dummy[!id]
377
+ # @category Source / Output
378
+ def output.stereotool(
379
+ ~id=null("output.stereotool"),
380
+ %argsof(track.audio.stereotool[!id,!active]),
381
+ %argsof(output.dummy[!id]),
382
+ s
383
+ ) =
384
+ s = stereotool(%argsof(track.audio.stereotool[!id,!active]), active=false, s)
385
+ let replaces s = output.dummy(id=id, %argsof(output.dummy[!id]), s)
386
+
387
+ s
388
+ end
389
+ %endif
390
+
391
+ # Defer the source's audio track by a given amount of time. Source will be
392
+ # available when the given `delay` has been fully buffered. Use this operator
393
+ # instead of `buffer` when buffering large amount of data as initial delay.
394
+ #
395
+ # This operator encodes and decodes the audio content. See `defer.pcm_s16` for
396
+ # a low-level operator using directly the `pcm_s16` format.
397
+ # @category Source / Audio processing
398
+ # @argsof track.audio.defer[!id]
399
+ # @param ~id Force the source's ID
400
+ def defer(~id=null("defer"), %argsof(track.audio.defer[!id]), s) =
401
+ let {audio = a} = source.tracks(s)
402
+ a = track.encode.audio.pcm_s16(a)
403
+ a = track.audio.defer(%argsof(track.audio.defer[!id]), a)
404
+ a = track.decode.audio.pcm_s16(a)
405
+
406
+ source(
407
+ id=id,
408
+ {
409
+ audio = a,
410
+ metadata = track.metadata(a),
411
+ track_marks = track.track_marks(a)
412
+ }
413
+ )
414
+ end
415
+
416
+ # Defer the source's audio track by a given amount of time. Source will be
417
+ # available when the given `delay` has been fully buffered. Use this operator
418
+ # instead of `buffer` when buffering large amount of data as initial delay.
419
+ #
420
+ # This operator uses a source already using `pcm_s16` audio data. It can
421
+ # be used to prevent unneeded data copy. Typically, decoders that know
422
+ # how to decode to `pcm_s16` (like `ffmpeg`) will decode directly
423
+ # into the format and encoders who support it (also `%ffmpeg`)
424
+ # will encoder directly from the `pcm_s16` data. Use `defer` if you
425
+ # prefer a more user-friendly operator.
426
+ # @category Source / Audio processing
427
+ # @argsof track.audio.defer[!id]
428
+ # @param ~id Force the source's ID
429
+ def defer.pcm_s16(
430
+ ~id=null("defer.pcm_s16"),
431
+ %argsof(track.audio.defer[!id]),
432
+ (s:source(audio=pcm_s16))
433
+ ) =
434
+ let {audio = a} = source.tracks(s)
435
+ a = track.audio.defer(%argsof(track.audio.defer[!id]), a)
436
+
437
+ source(
438
+ id=id,
439
+ {
440
+ audio = a,
441
+ metadata = track.metadata(a),
442
+ track_marks = track.track_marks(a)
443
+ }
444
+ )
445
+ end
446
+
447
+ let settings.normalize_track_gain_metadata =
448
+ settings.make(
449
+ description="Metadata used to store track gain normalization metadata",
450
+ "liq_normalize_track_gain"
451
+ )
452
+
453
+ # Amplify source tracks according to track gain normalization metadata. This operator does not
454
+ # compute that value. You can use integrated LUFS track gain or ReplayGain to compute it.
455
+ # @category Source / Audio processing
456
+ # @param ~id Force the value of the source ID.
457
+ # @param s Source to be amplified.
458
+ def normalize_track_gain(~id=null, s) =
459
+ amplify(id=id, override=settings.normalize_track_gain_metadata(), 1., s)
460
+ end