rvlib-shipkit 2.3.0 → 2.4.0
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/CHANGELOG.md +11 -2
- package/package.json +1 -1
- package/src/assets/kits/identity-kit.ts +5 -2
- package/src/cli/cmd_Commands.ts +4 -0
- package/src/cli/cmd_Review.ts +36 -10
- package/src/cli/cmd_Sound.ts +209 -0
- package/src/cli/cmd_Trailer.ts +7 -2
- package/src/cli/kit/verb.ts +11 -3
- package/src/cli.ts +2 -0
- package/src/core/app-icon.ts +10 -6
- package/src/repo-config.ts +3 -0
- package/src/review/format.ts +11 -3
- package/src/review/model.ts +47 -5
- package/src/review/project.ts +20 -4
- package/src/review/routes.ts +12 -1
- package/src/review/store.ts +15 -6
- package/src/review/unit.ts +19 -1
- package/src/serve/hub-app/admin-svc.ts +1 -1
- package/src/serve/hub-app/admin-view.tsx +1 -1
- package/src/serve/hub-app/hash-route.ts +12 -4
- package/src/serve/hub-app/home-st.ts +7 -1
- package/src/serve/hub-app/hub.css +38 -1
- package/src/serve/hub-app/icons.tsx +6 -0
- package/src/serve/hub-app/layout-st.ts +2 -2
- package/src/serve/hub-app/md.ts +32 -0
- package/src/serve/hub-app/project-list.css +9 -7
- package/src/serve/hub-app/project-list.tsx +5 -2
- package/src/serve/hub-app/projects-svc.ts +3 -2
- package/src/serve/hub-app/repo-st.ts +22 -7
- package/src/serve/hub-app/review/clip-st.ts +57 -7
- package/src/serve/hub-app/review/kind-view.tsx +78 -9
- package/src/serve/hub-app/review/review-answer.tsx +83 -53
- package/src/serve/hub-app/review/review-page.tsx +9 -14
- package/src/serve/hub-app/review/review-st.ts +141 -18
- package/src/serve/hub-app/review/review-views.tsx +154 -9
- package/src/serve/hub-app/review.css +70 -10
- package/src/serve/hub-app/wiki-st.ts +190 -0
- package/src/serve/hub-app/wikis-view.tsx +134 -5
- package/src/serve/hub-runtime.ts +4 -4
- package/src/serve/project-gutter-route.ts +25 -1
- package/src/serve/project-gutter.ts +1 -1
- package/src/serve/serve-hub.ts +7 -0
- package/src/shot/client.ts +17 -5
- package/src/shot/shooter.ts +19 -3
- package/src/shot/shot-ask.ts +9 -1
- package/src/shot/stepped-clip.ts +53 -0
- package/src/trailer/adapter.ts +33 -4
- package/src/trailer/build.ts +170 -29
- package/src/trailer/fonts.ts +55 -0
- package/src/trailer/footage.ts +4 -4
- package/src/trailer/grammar.ts +429 -67
- package/src/trailer/graph.ts +130 -56
- package/src/trailer/look.ts +3 -0
- package/src/trailer/motion.ts +207 -0
- package/src/trailer/plan.ts +2 -2
- package/src/trailer/prune.ts +13 -2
- package/src/trailer/score/compose.ts +18 -2
- package/src/trailer/score/gm-render.ts +43 -0
- package/src/trailer/score/gm.ts +90 -0
- package/src/trailer/score/midi.ts +143 -0
- package/src/trailer/score/pattern.ts +13 -6
- package/src/trailer/score/score.ts +28 -14
- package/src/trailer/score/sound.ts +12 -1
- package/src/trailer/score/synth.ts +24 -2
- package/src/trailer/sound/audio.ts +148 -0
- package/src/trailer/sound/brand.ts +42 -0
- package/src/trailer/sound/comfy.ts +38 -0
- package/src/trailer/sound/eleven.ts +72 -0
- package/src/trailer/sound/fluid.ts +71 -0
- package/src/trailer/sound/gen.ts +139 -0
- package/src/trailer/sound/index.ts +60 -0
- package/src/trailer/sound/presets.ts +69 -0
- package/src/trailer/sound/run.ts +189 -0
- package/src/trailer/titles.ts +451 -235
- package/src/trailer/url.ts +18 -3
- package/src/trailer/vocab.ts +12 -1
- package/src/trailer/words.ts +29 -8
- package/src/wiki/graph-mount.ts +3 -0
- package/src/wiki/graph-page.ts +45 -9
- package/src/wiki/graph-pages.ts +128 -0
package/src/trailer/graph.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// the stitch's ffmpeg arguments, as data: a piece of the cut (its shots cut to their frames, joined by their cuts, the
|
|
2
2
|
// titles laid over) and the mix of the sound. Pure: stitch.ts runs them, keyed on these very arguments
|
|
3
3
|
import type { CutKind, Shot, Trailer } from './grammar.ts'
|
|
4
|
-
import { GROW_S, musicCues, starts, total } from './grammar.ts'
|
|
4
|
+
import { CUTS, GROW_S, musicCues, starts, total } from './grammar.ts'
|
|
5
5
|
import type { TrailerLook } from './look.ts'
|
|
6
6
|
import { frameAt, frameSpans, type Piece } from './plan.ts'
|
|
7
|
+
import { inFrames, KNOCK_S, outFrames, type TitleMotion, titleMotion } from './motion.ts'
|
|
7
8
|
import type { TitleImage } from './titles.ts'
|
|
8
9
|
|
|
9
10
|
/** a wipe's edge, as xfade draws it: along x or y, starting from the far side (`from`) or the near one */
|
|
@@ -14,24 +15,6 @@ const SEAM_EDGE: Partial<Record<CutKind, { axis: 'x' | 'y'; from: boolean }>> =
|
|
|
14
15
|
'wipe-down': { axis: 'y', from: false },
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
const XFADE: Record<Exclude<CutKind, 'hard'>, string> = {
|
|
18
|
-
fade: 'fade',
|
|
19
|
-
'dip-black': 'fadeblack',
|
|
20
|
-
'dip-white': 'fadewhite',
|
|
21
|
-
'slide-left': 'slideleft',
|
|
22
|
-
'slide-right': 'slideright',
|
|
23
|
-
'slide-up': 'slideup',
|
|
24
|
-
'slide-down': 'slidedown',
|
|
25
|
-
'whip-left': 'smoothleft',
|
|
26
|
-
'whip-right': 'smoothright',
|
|
27
|
-
'wipe-left': 'wipeleft',
|
|
28
|
-
'wipe-right': 'wiperight',
|
|
29
|
-
'wipe-up': 'wipeup',
|
|
30
|
-
'wipe-down': 'wipedown',
|
|
31
|
-
circle: 'circleopen',
|
|
32
|
-
pixel: 'pixelize',
|
|
33
|
-
}
|
|
34
|
-
|
|
35
18
|
/** a grid's or a split's cell: a shot's or a footage block's take, from its `from`, cropped by its `frame` */
|
|
36
19
|
export interface GridCell {
|
|
37
20
|
footage: string
|
|
@@ -198,9 +181,107 @@ function flashF(s: Shot): string {
|
|
|
198
181
|
.join('')
|
|
199
182
|
}
|
|
200
183
|
|
|
201
|
-
/** a
|
|
202
|
-
const
|
|
203
|
-
const
|
|
184
|
+
/** a frame's own pseudo random number, 0 to 1: the same cut draws the same frames */
|
|
185
|
+
const rnd = (...xs: number[]): number => {
|
|
186
|
+
const v = Math.sin(xs.reduce((n, x, j) => n + x * [12.9898, 78.233, 37.719, 4.581][j % 4]!, 0)) * 43758.5453
|
|
187
|
+
return v - Math.floor(v)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** the cuts the core draws, frame by frame over the frames both shots hold: `a` the shot leaving, `b` the one coming,
|
|
191
|
+
* `n` frames, the result labelled `out`. A whip pans both shots past at speed, blurred along the way they fly; a
|
|
192
|
+
* glitch tears the picture (colours split, bands torn sideways, grain) round a flicker from one shot to the next; a
|
|
193
|
+
* leak burns two soft lights of the look across a dissolve */
|
|
194
|
+
export function drawnCut(kind: CutKind, a: string, b: string, out: string, n: number, i: number, [W, H]: readonly [number, number], fps: number, look: Pick<TrailerLook, 'accent' | 'light'>): string[] {
|
|
195
|
+
const d = n / fps
|
|
196
|
+
if (kind === 'leak') {
|
|
197
|
+
const big = `s=${2 * W}x${H}:r=${fps}:d=${n3(d + 0.2)}:type=radial:speed=0`
|
|
198
|
+
const [y1, y2, r1, r2] = [Math.round(H * 0.4), Math.round(H * 0.85), Math.round(W * 0.45), Math.round(W * 0.3)]
|
|
199
|
+
return [
|
|
200
|
+
`[${a}][${b}]xfade=transition=fade:duration=${n3(d)}:offset=0,format=gbrp[lx${i}]`,
|
|
201
|
+
`gradients=${big}:n=3:c0=0xffdcaa:c1=${ff(look.accent)}:c2=0x000000:x0=${W}:y0=${y1}:x1=${W + r1}:y1=${y1},format=gbrp,crop=w=${W}:h=${H}:x='${W}*(1.1-t/${n3(d)})':y=0[la${i}]`,
|
|
202
|
+
`gradients=${big}:n=2:c0=0xff5a46:c1=0x000000:x0=${W}:y0=${y2}:x1=${W + r2}:y1=${y2},format=gbrp,crop=w=${W}:h=${H}:x='${W}*(0.25+0.6*t/${n3(d)})':y=0[lb${i}]`,
|
|
203
|
+
`[la${i}][lb${i}]blend=all_mode=screen,colorchannelmixer=rr=0.8:gg=0.8:bb=0.8,fade=t=in:st=0:d=${n3(d / 2)},fade=t=out:st=${n3(d / 2)}:d=${n3(d / 2)}[ll${i}]`,
|
|
204
|
+
`[lx${i}][ll${i}]blend=all_mode=screen:shortest=1,format=yuv420p[${out}]`,
|
|
205
|
+
]
|
|
206
|
+
}
|
|
207
|
+
const ks = Array.from({ length: n }, (_, k) => k)
|
|
208
|
+
const g = [
|
|
209
|
+
`[${a}]split=${n}${ks.map((k) => `[ca${i}_${k}x]`).join('')}`,
|
|
210
|
+
`[${b}]split=${n}${ks.map((k) => `[cb${i}_${k}x]`).join('')}`,
|
|
211
|
+
...ks.flatMap((k) => [`[ca${i}_${k}x]trim=start_frame=${k}:end_frame=${k + 1},setpts=PTS-STARTPTS[ca${i}_${k}]`, `[cb${i}_${k}x]trim=start_frame=${k}:end_frame=${k + 1},setpts=PTS-STARTPTS[cb${i}_${k}]`]),
|
|
212
|
+
]
|
|
213
|
+
for (const k of ks) {
|
|
214
|
+
const [fa, fb, fo] = [`ca${i}_${k}`, `cb${i}_${k}`, `co${i}_${k}`]
|
|
215
|
+
const p = (k + 0.5) / n
|
|
216
|
+
if (kind.startsWith('whip-')) {
|
|
217
|
+
// eased in and out: the speed peaks mid way, and the blur with it (a box as wide as a frame's travel)
|
|
218
|
+
const u = p < 0.5 ? 4 * p ** 3 : 1 - (-2 * p + 2) ** 3 / 2
|
|
219
|
+
const across = kind === 'whip-left' || kind === 'whip-right'
|
|
220
|
+
const span = across ? W : H
|
|
221
|
+
const sigma = Math.min(600, (12 * Math.min(p, 1 - p) ** 2 * span * 0.35) / n)
|
|
222
|
+
const forward = kind === 'whip-left' || kind === 'whip-up'
|
|
223
|
+
const off = Math.round((forward ? u : 1 - u) * span)
|
|
224
|
+
const pair = forward ? `[${fa}][${fb}]` : `[${fb}][${fa}]`
|
|
225
|
+
g.push(`${pair}${across ? 'hstack' : 'vstack'}=inputs=2,crop=${W}:${H}:${across ? off : 0}:${across ? 0 : off},gblur=sigma=${n3(across ? sigma : 0)}:sigmaV=${n3(across ? 0 : sigma)}[${fo}]`)
|
|
226
|
+
continue
|
|
227
|
+
}
|
|
228
|
+
// glitch: the picture flickers from one shot to the other round the middle, torn hardest there
|
|
229
|
+
const env = Math.sin(Math.PI * p) ** 0.7
|
|
230
|
+
const flick = p > 0.3 && p < 0.7 && rnd(i, k, 9) < 0.45
|
|
231
|
+
const src = (p < 0.5) !== flick ? fa : fb
|
|
232
|
+
g.push(`[${src === fa ? fb : fa}]nullsink`)
|
|
233
|
+
const dx = Math.round(W * 0.016 * env * (0.4 + rnd(i, k, 1)))
|
|
234
|
+
const bands = 1 + Math.floor(rnd(i, k, 2) * 3)
|
|
235
|
+
let cur = `[${src}]format=gbrp,rgbashift=rh=${dx}:bh=${-dx}:gv=${Math.round(dx / 3)}:edge=smear`
|
|
236
|
+
const parts: string[] = []
|
|
237
|
+
for (let j = 0; j < bands; j++) {
|
|
238
|
+
const bh = 2 * Math.round((H * (0.03 + 0.1 * rnd(i, k, j, 3))) / 2)
|
|
239
|
+
const by = Math.round((H - bh) * rnd(i, k, j, 4))
|
|
240
|
+
const shift = Math.round((rnd(i, k, j, 5) - 0.5) * W * 0.16 * env)
|
|
241
|
+
parts.push(`${cur},split[gb${i}_${k}_${j}][gs${i}_${k}_${j}]`, `[gs${i}_${k}_${j}]crop=${W}:${bh}:0:${by}[gt${i}_${k}_${j}]`)
|
|
242
|
+
cur = `[gb${i}_${k}_${j}][gt${i}_${k}_${j}]overlay=x=${shift}:y=${by}`
|
|
243
|
+
}
|
|
244
|
+
g.push(...parts, `${cur},noise=alls=${Math.round(28 * env)}:allf=t+u:all_seed=${i * 977 + k},format=yuv420p[${fo}]`)
|
|
245
|
+
}
|
|
246
|
+
g.push(`${ks.map((k) => `[co${i}_${k}]`).join('')}concat=n=${n}:v=1:a=0,settb=1/${fps},setpts=N,format=yuv420p[${out}]`)
|
|
247
|
+
return g
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** each title's span on the cut, shot by shot: its first and last frame (inclusive) and its seconds, a knock's flight
|
|
251
|
+
* in them. A progress step ends the frame before the next one starts: two steps never stand on one frame */
|
|
252
|
+
export function titleSpans(t: Pick<Trailer, 'shots' | 'fps'> & Parameters<typeof starts>[0]): { ta: number; te: number; seconds: number }[][] {
|
|
253
|
+
const st = starts(t)
|
|
254
|
+
return t.shots.map((s, i) =>
|
|
255
|
+
s.titles.map((ti) => {
|
|
256
|
+
const at = st[i]! + ti.at
|
|
257
|
+
const seconds = Math.min(ti.for ?? s.length - ti.at, ti.knock === undefined ? Infinity : ti.knock - ti.at + KNOCK_S)
|
|
258
|
+
const handOff = ti.style === 'progress' && ti.step !== undefined && ti.step.i < ti.step.n ? 1 : 0
|
|
259
|
+
return { ta: frameAt(at, t.fps), te: Math.floor((at + seconds) * t.fps + 1e-6) - handOff, seconds }
|
|
260
|
+
}),
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** each title's entrance and exit in frames (motion.ts), shot by shot: what the build draws and the stitch lays */
|
|
265
|
+
export function titleMotions(t: Pick<Trailer, 'shots' | 'fps'> & Parameters<typeof starts>[0]): TitleMotion[][] {
|
|
266
|
+
const spans = titleSpans(t)
|
|
267
|
+
return t.shots.map((s, i) =>
|
|
268
|
+
s.titles.map((ti, j) => {
|
|
269
|
+
const sp = spans[i]![j]!
|
|
270
|
+
return titleMotion({
|
|
271
|
+
style: ti.style,
|
|
272
|
+
text: ti.text,
|
|
273
|
+
...(ti.in ? { in: ti.in } : {}),
|
|
274
|
+
...(ti.out ? { out: ti.out } : {}),
|
|
275
|
+
...(ti.knock !== undefined ? { knock: ti.knock - ti.at } : {}),
|
|
276
|
+
...(ti.step ? { step: ti.step } : {}),
|
|
277
|
+
seconds: sp.seconds,
|
|
278
|
+
frames: Math.max(0, sp.te - sp.ta + 1),
|
|
279
|
+
fps: t.fps,
|
|
280
|
+
cover: ti.style === 'short' && sp.ta === 0,
|
|
281
|
+
})
|
|
282
|
+
}),
|
|
283
|
+
)
|
|
284
|
+
}
|
|
204
285
|
|
|
205
286
|
/** the ffmpeg arguments that render a piece's frames, no sound, and the row of its shots' middle frames for the
|
|
206
287
|
* contact strip (`thumbs`, when a middle falls in it) */
|
|
@@ -340,7 +421,9 @@ export function pieceArgs(a: Pick<StitchInput, 't' | 'sources' | 'titles' | 'loo
|
|
|
340
421
|
const kind = t.shots[i - 1]!.cut.kind as Exclude<CutKind, 'hard'>
|
|
341
422
|
const d = head / fps
|
|
342
423
|
const edge = SEAM_EDGE[kind]
|
|
343
|
-
|
|
424
|
+
const how = CUTS[kind] as string
|
|
425
|
+
if (how === 'drawn') graph.push(...drawnCut(kind, `e${i - 1}`, `h${i}`, `j${i}`, head, i, [W, H], fps, a.look))
|
|
426
|
+
else if (edge === undefined) graph.push(`[e${i - 1}][h${i}]xfade=transition=${how}:duration=${n3(d)}:offset=0,format=yuv420p[j${i}]`)
|
|
344
427
|
else {
|
|
345
428
|
// a wipe's seam rides its edge: a soft shadow of the look's edge, then a line of its light
|
|
346
429
|
const across = edge.axis === 'x'
|
|
@@ -349,7 +432,7 @@ export function pieceArgs(a: Pick<StitchInput, 't' | 'sources' | 'titles' | 'loo
|
|
|
349
432
|
const at = (w: number): string => (across ? `x='${pos}-${w / 2}':y=0` : `x=0:y='${pos}-${w / 2}'`)
|
|
350
433
|
const [sw, lw] = [Math.round(H / 64), Math.max(4, Math.round(H / 300))]
|
|
351
434
|
graph.push(
|
|
352
|
-
`[e${i - 1}][h${i}]xfade=transition=${
|
|
435
|
+
`[e${i - 1}][h${i}]xfade=transition=${how}:duration=${n3(d)}:offset=0[jw${i}]`,
|
|
353
436
|
bar(`${ff(a.look.edge)}@0.35`, sw, 'ws'),
|
|
354
437
|
bar(ff(a.look.light), lw, 'wl'),
|
|
355
438
|
`[jw${i}][ws${i}]overlay=${at(sw)}:eval=frame:shortest=1[jv${i}]`,
|
|
@@ -373,43 +456,34 @@ export function pieceArgs(a: Pick<StitchInput, 't' | 'sources' | 'titles' | 'loo
|
|
|
373
456
|
cur = 'ends'
|
|
374
457
|
}
|
|
375
458
|
|
|
376
|
-
// the titles that show in the piece, wherever their shot is: each
|
|
459
|
+
// the titles that show in the piece, wherever their shot is: each its entrance's frames, its still held, its exit's
|
|
460
|
+
// frames (titles.ts drew them all in one box), laid at that box
|
|
461
|
+
const tspans = titleSpans(t)
|
|
377
462
|
t.shots.forEach((s, i) => {
|
|
378
|
-
s.titles.forEach((
|
|
379
|
-
const
|
|
380
|
-
const dur = Math.min(ti.for ?? s.length - ti.at, ti.knock === undefined ? Infinity : ti.knock - ti.at + KNOCK_S)
|
|
381
|
-
// a progress step ends the frame before the next one starts: two steps never stand on one frame
|
|
382
|
-
const handOff = ti.style === 'progress' && ti.step !== undefined && ti.step.i < ti.step.n ? 1 : 0
|
|
383
|
-
const [ta, te] = [frameAt(at, fps), Math.floor((at + dur) * fps + 1e-6) - handOff]
|
|
463
|
+
s.titles.forEach((_, j) => {
|
|
464
|
+
const { ta, te } = tspans[i]![j]!
|
|
384
465
|
if (te < p.from || ta >= p.to || te < ta) return
|
|
385
|
-
// a tagline and the logo breathe in slowly; the plates pop; a short's words come quick
|
|
386
|
-
const short = ti.style === 'short' || ti.style === 'progress'
|
|
387
|
-
const f = Math.min(ti.style === 'line' || ti.style === 'logo' ? 0.7 : short ? 0.15 : 0.25, dur / 3)
|
|
388
|
-
// the steps of a progress swap on their beat: the first alone fades in, the last alone fades out
|
|
389
|
-
const step = ti.style === 'progress' ? ti.step : undefined
|
|
390
|
-
const fadeIn = step === undefined || step.i === 1
|
|
391
|
-
const fadeOut = step === undefined || step.i === step.n
|
|
392
|
-
// a short's caption on the cut's first frame is there whole: frame 0 is the cover the platforms take
|
|
393
|
-
const cover = ti.style === 'short' && ta === 0
|
|
394
466
|
const img = a.titles[i]![j]!
|
|
395
|
-
const
|
|
467
|
+
const [nIn, nOut] = [img.in ?? 0, img.out ?? 0]
|
|
468
|
+
const hold = te - ta + 1 - nIn - nOut
|
|
396
469
|
const [from, to] = [Math.max(ta, base), Math.min(te, p.to - 1)]
|
|
397
|
-
const
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
`[${
|
|
402
|
-
|
|
403
|
-
)
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
470
|
+
const segs: string[] = []
|
|
471
|
+
const seq = (pattern: string, n: number, tag: string): void => {
|
|
472
|
+
const k = input('-framerate', String(fps), '-start_number', '0', '-i', pattern)
|
|
473
|
+
graph.push(`[${k}:v]format=rgba,trim=end_frame=${n},settb=1/${fps},setpts=N[t${i}_${j}${tag}]`)
|
|
474
|
+
segs.push(`[t${i}_${j}${tag}]`)
|
|
475
|
+
}
|
|
476
|
+
if (nIn > 0) seq(inFrames(img.file), nIn, 'i')
|
|
477
|
+
if (hold > 0) {
|
|
478
|
+
const k = input('-i', img.file)
|
|
479
|
+
graph.push(`[${k}:v]format=rgba,loop=loop=${hold - 1}:size=1:start=0,settb=1/${fps},setpts=N[t${i}_${j}h]`)
|
|
480
|
+
segs.push(`[t${i}_${j}h]`)
|
|
481
|
+
}
|
|
482
|
+
if (nOut > 0) seq(outFrames(img.file), nOut, 'o')
|
|
483
|
+
const join = segs.length > 1 ? `${segs.join('')}concat=n=${segs.length}:v=1:a=0,` : `${segs[0]}`
|
|
484
|
+
graph.push(`${join}${segs.length > 1 ? '' : 'null,'}trim=start_frame=${from - ta}:end_frame=${to - ta + 1},settb=1/${fps},setpts=N+${from - base}[t${i}_${j}]`)
|
|
408
485
|
const on = `between(t,${n3((ta - base - 0.5) / fps)},${n3((te - base + 0.5) / fps)})`
|
|
409
|
-
|
|
410
|
-
const x = kn === null ? `${img.x}${slide}` : `${img.x - (hyp - img.w) / 2}-${n3(W * 1.1)}*${flown}`
|
|
411
|
-
const y = kn === null ? `${img.y}+${rise}` : `${img.y - (hyp - img.h) / 2}+${rise}-${n3(H * 0.45)}*${flown}`
|
|
412
|
-
graph.push(`[${cur}][t${i}_${j}]overlay=x='${x}':y='${y}':eof_action=pass:enable='${on}'[v${i}_${j}]`)
|
|
486
|
+
graph.push(`[${cur}][t${i}_${j}]overlay=x=${img.x}:y=${img.y}:eof_action=pass:enable='${on}'[v${i}_${j}]`)
|
|
413
487
|
cur = `v${i}_${j}`
|
|
414
488
|
})
|
|
415
489
|
})
|
package/src/trailer/look.ts
CHANGED
|
@@ -22,6 +22,9 @@ export interface TrailerLookInput {
|
|
|
22
22
|
short?: { font?: { file?: string; family?: string; weight?: number }; light?: string; edge?: string; accent?: string }
|
|
23
23
|
/** the vertical short's look (a `vertical` cut), each part `false` to leave it out (vertical.ts) */
|
|
24
24
|
vertical?: VerticalLookInput
|
|
25
|
+
/** text styles by name, each a title line's words (`style big font "Bebas Neue" in letters`): a title's `style`
|
|
26
|
+
* names one; one named like a built in style gives that style its defaults (grammar.ts resolveStyles) */
|
|
27
|
+
styles?: Record<string, string>
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
/** what a vertical short wears, image paths repo relative; its colours and face are the look's */
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// a title's entrance and exit, frame by frame: what each move does to the whole title or to each of its letters at a
|
|
2
|
+
// point of its run. Pure: the grammar checks the words, titles.ts draws each state, graph.ts lays the frames. An exit
|
|
3
|
+
// is its move played backwards; the knock is the one exit of its own
|
|
4
|
+
|
|
5
|
+
export const MOVES = ['none', 'fade', 'rise', 'drop', 'slide', 'pop', 'shake', 'blur', 'wipe', 'typewriter', 'letters', 'wave'] as const
|
|
6
|
+
export type MoveKind = (typeof MOVES)[number]
|
|
7
|
+
/** the moves drawn letter by letter: the title is drawn again each frame, the rest move the drawn title */
|
|
8
|
+
export const LETTER_MOVES: ReadonlySet<MoveKind> = new Set(['typewriter', 'letters', 'wave'])
|
|
9
|
+
|
|
10
|
+
/** a move a title line names: `in pop 0.4s`; no seconds, its own default */
|
|
11
|
+
export interface Move {
|
|
12
|
+
kind: MoveKind
|
|
13
|
+
seconds?: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** the whole title at a frame: its alpha, its offset in frame pixels, its scale and turn (radians) about its box's
|
|
17
|
+
* middle, a blur in frame pixels, and the share of its width a wipe shows (1: all) */
|
|
18
|
+
export interface Whole {
|
|
19
|
+
a: number
|
|
20
|
+
dx: number
|
|
21
|
+
dy: number
|
|
22
|
+
s: number
|
|
23
|
+
rot: number
|
|
24
|
+
blur: number
|
|
25
|
+
wipe: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** one letter at a frame, about its own middle: alpha, offset in frame pixels, scale */
|
|
29
|
+
export interface Glyph {
|
|
30
|
+
a: number
|
|
31
|
+
dx: number
|
|
32
|
+
dy: number
|
|
33
|
+
s: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** what a frame draws: the whole title's state; with a letter move, each letter's and the plates' and bands' alpha */
|
|
37
|
+
export interface TitleFrame {
|
|
38
|
+
whole: Whole
|
|
39
|
+
glyph?: (i: number, n: number) => Glyph
|
|
40
|
+
decor: number
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const STILL: Whole = { a: 1, dx: 0, dy: 0, s: 1, rot: 0, blur: 0, wipe: 1 }
|
|
44
|
+
const AT_REST: TitleFrame = { whole: STILL, decor: 1 }
|
|
45
|
+
|
|
46
|
+
const clip01 = (x: number): number => Math.max(0, Math.min(1, x))
|
|
47
|
+
const BACK = 1.70158
|
|
48
|
+
/** an ease out that overshoots a little and settles */
|
|
49
|
+
export const backOut = (x: number): number => 1 + (BACK + 1) * (x - 1) ** 3 + BACK * (x - 1) ** 2
|
|
50
|
+
const inOut = (x: number): number => (x < 0.5 ? 4 * x ** 3 : 1 - (-2 * x + 2) ** 3 / 2)
|
|
51
|
+
const smooth = (x: number): number => x * x * (3 - 2 * x)
|
|
52
|
+
|
|
53
|
+
/** a knocked title tips and flies out over this long */
|
|
54
|
+
export const KNOCK_S = 0.4
|
|
55
|
+
const KNOCK_TURN = 1.4
|
|
56
|
+
|
|
57
|
+
/** the frame of `kind` at `p`, 0 (before the entrance) to 1 (at rest), on a frame W by H whose letters are `px` tall */
|
|
58
|
+
export function moveFrame(kind: MoveKind, p: number, [W, H]: readonly [number, number], px: number): TitleFrame {
|
|
59
|
+
void W
|
|
60
|
+
p = clip01(p)
|
|
61
|
+
const whole = (w: Partial<Whole>): TitleFrame => ({ whole: { ...STILL, ...w }, decor: 1 })
|
|
62
|
+
switch (kind) {
|
|
63
|
+
case 'none':
|
|
64
|
+
return AT_REST
|
|
65
|
+
case 'fade':
|
|
66
|
+
return whole({ a: p })
|
|
67
|
+
case 'rise':
|
|
68
|
+
return whole({ a: p, dy: (H / 40) * (1 - p) ** 2 })
|
|
69
|
+
case 'drop':
|
|
70
|
+
return whole({ a: p, dy: -(H / 30) * (1 - p) ** 2 })
|
|
71
|
+
case 'slide':
|
|
72
|
+
return whole({ a: p, dx: -(H / 32) * (1 - p) ** 3 })
|
|
73
|
+
case 'pop':
|
|
74
|
+
return whole({ a: clip01(p * 3), s: 0.55 + 0.45 * backOut(p) })
|
|
75
|
+
case 'shake': {
|
|
76
|
+
const amp = (H / 26) * (1 - p) ** 2
|
|
77
|
+
return whole({ a: clip01(p * 4), dx: (amp * (Math.sin(p * 53) + 0.5 * Math.sin(p * 31 + 1))) / 1.5, dy: (amp * (Math.cos(p * 47) + 0.5 * Math.sin(p * 23 + 2))) / 1.5, rot: (amp / H) * 0.6 * Math.sin(p * 41) })
|
|
78
|
+
}
|
|
79
|
+
case 'blur':
|
|
80
|
+
return whole({ a: clip01(p * 1.6), blur: (H / 40) * (1 - p) ** 2, s: 1 + 0.04 * (1 - p) })
|
|
81
|
+
case 'wipe':
|
|
82
|
+
return whole({ wipe: inOut(p) })
|
|
83
|
+
case 'typewriter':
|
|
84
|
+
return { whole: STILL, decor: clip01(p * 4), glyph: (i, n) => ({ a: p * n >= i + 1 - 1e-9 ? 1 : 0, dx: 0, dy: 0, s: 1 }) }
|
|
85
|
+
case 'letters':
|
|
86
|
+
return {
|
|
87
|
+
whole: STILL,
|
|
88
|
+
decor: clip01(p * 3),
|
|
89
|
+
glyph: (i, n) => {
|
|
90
|
+
const span = 0.45
|
|
91
|
+
const q = smooth(clip01((p - (n > 1 ? (i / (n - 1)) * (1 - span) : 0)) / span))
|
|
92
|
+
return { a: q, dx: 0, dy: px * 0.4 * (1 - q) ** 2, s: 1 }
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
case 'wave':
|
|
96
|
+
return {
|
|
97
|
+
whole: STILL,
|
|
98
|
+
decor: clip01(p * 3),
|
|
99
|
+
glyph: (i, n) => {
|
|
100
|
+
const span = 0.5
|
|
101
|
+
const q = clip01((p - (n > 1 ? (i / (n - 1)) * (1 - span) : 0)) / span)
|
|
102
|
+
return { a: clip01(q * 2.5), dx: 0, dy: px * 0.7 * (1 - backOut(q)), s: 0.7 + 0.3 * backOut(q) }
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** a knocked title at `q`, 0 (still in place) to 1 (gone): it tips and flies off left and up */
|
|
109
|
+
export function knockFrame(q: number, [W, H]: readonly [number, number]): TitleFrame {
|
|
110
|
+
const k = clip01(q) ** 2
|
|
111
|
+
return { whole: { ...STILL, rot: -KNOCK_TURN * k, dx: -W * 1.1 * k, dy: -H * 0.45 * k }, decor: 1 }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** how many letters a title's text draws: every character but spaces, the `*` marks and the `\n` breaks */
|
|
115
|
+
export const letterCount = (text: string): number => [...text.replaceAll('\\n', '').replace(/[*\s]/g, '')].length
|
|
116
|
+
|
|
117
|
+
/** a move's length when its line names none; the default entrance and exit keep the style's own (`styleFade`) */
|
|
118
|
+
export function moveSeconds(kind: MoveKind, letters: number): number {
|
|
119
|
+
switch (kind) {
|
|
120
|
+
case 'none':
|
|
121
|
+
return 0
|
|
122
|
+
case 'typewriter':
|
|
123
|
+
return 0.045 * letters
|
|
124
|
+
case 'letters':
|
|
125
|
+
return 0.3 + 0.03 * letters
|
|
126
|
+
case 'wave':
|
|
127
|
+
return 0.35 + 0.035 * letters
|
|
128
|
+
case 'pop':
|
|
129
|
+
return 0.35
|
|
130
|
+
case 'shake':
|
|
131
|
+
case 'blur':
|
|
132
|
+
case 'wipe':
|
|
133
|
+
return 0.5
|
|
134
|
+
default:
|
|
135
|
+
return 0.4
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** the style's own fade: a tagline and the logo breathe in slowly, the plates pop, a short's words come quick */
|
|
140
|
+
export const styleFade = (style: string): number => (style === 'line' || style === 'logo' ? 0.7 : style === 'short' || style === 'progress' ? 0.15 : 0.25)
|
|
141
|
+
|
|
142
|
+
/** what a title's entrance and exit are, their frames counted */
|
|
143
|
+
export interface TitleMotion {
|
|
144
|
+
in: { kind: MoveKind; frames: number }
|
|
145
|
+
out: { kind: MoveKind | 'knock'; frames: number }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** the title's span in seconds and frames, and what it shows: the words of its line, its style, its step */
|
|
149
|
+
export interface MotionInput {
|
|
150
|
+
style: string
|
|
151
|
+
text: string
|
|
152
|
+
in?: Move
|
|
153
|
+
out?: Move
|
|
154
|
+
/** seconds from the title's start to its knock */
|
|
155
|
+
knock?: number
|
|
156
|
+
step?: { i: number; n: number }
|
|
157
|
+
/** seconds it stands, a knock's flight included */
|
|
158
|
+
seconds: number
|
|
159
|
+
/** frames it stands */
|
|
160
|
+
frames: number
|
|
161
|
+
fps: number
|
|
162
|
+
/** a short's caption on the cut's first frame: there whole, the cover the platforms take */
|
|
163
|
+
cover: boolean
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** a title's entrance and exit in frames. With no `in`, a title rises in (a short slides, a progress fades) over its
|
|
167
|
+
* style's fade; with no `out`, it fades out over it; a default never takes more than a third of the title, a letter
|
|
168
|
+
* move more than half. A progress step swaps on its beat: only the first fades in, only the last fades out */
|
|
169
|
+
export function titleMotion(m: MotionInput): TitleMotion {
|
|
170
|
+
const fade = Math.min(styleFade(m.style), m.seconds / 3)
|
|
171
|
+
const letters = letterCount(m.text)
|
|
172
|
+
const own = (mv: Move | undefined, fallback: MoveKind): [MoveKind, number] => {
|
|
173
|
+
if (!mv) return [fallback, fallback === 'none' ? 0 : fade]
|
|
174
|
+
if (mv.seconds !== undefined) return [mv.kind, mv.seconds]
|
|
175
|
+
return [mv.kind, Math.min(moveSeconds(mv.kind, letters), m.seconds * (LETTER_MOVES.has(mv.kind) ? 0.5 : 1 / 3))]
|
|
176
|
+
}
|
|
177
|
+
const firstStep = m.step === undefined || m.step.i === 1
|
|
178
|
+
const lastStep = m.step === undefined || m.step.i === m.step.n
|
|
179
|
+
const defIn: MoveKind = m.style === 'short' ? 'slide' : m.style === 'progress' ? 'fade' : 'rise'
|
|
180
|
+
let [inKind, inS] = m.cover || !firstStep ? ['none' as MoveKind, 0] : own(m.in, defIn)
|
|
181
|
+
let [outKind, outS]: [MoveKind | 'knock', number] = m.knock !== undefined ? ['knock', KNOCK_S] : !lastStep ? ['none', 0] : own(m.out, 'fade')
|
|
182
|
+
if (inKind === 'none') inS = 0
|
|
183
|
+
if (outKind === 'none') outS = 0
|
|
184
|
+
let a = Math.round(inS * m.fps)
|
|
185
|
+
let b = Math.round(outS * m.fps)
|
|
186
|
+
// a title shorter than its moves shares its frames between them
|
|
187
|
+
if (a + b > m.frames) {
|
|
188
|
+
const k = m.frames / (a + b)
|
|
189
|
+
a = Math.floor(a * k)
|
|
190
|
+
b = Math.floor(b * k)
|
|
191
|
+
}
|
|
192
|
+
if (a === 0) inKind = 'none'
|
|
193
|
+
if (b === 0) outKind = 'none'
|
|
194
|
+
return { in: { kind: inKind, frames: a }, out: { kind: outKind, frames: b } }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** the frame `k` of an entrance of `n` frames: frame 0 is before it, the frame after the last at rest */
|
|
198
|
+
export const inFrame = (kind: MoveKind, k: number, n: number, size: readonly [number, number], px: number): TitleFrame => moveFrame(kind, k / n, size, px)
|
|
199
|
+
|
|
200
|
+
/** the frame `k` of an exit of `n` frames: its move played backwards, the last frame gone; a knock leaves from its
|
|
201
|
+
* first frame, still in place */
|
|
202
|
+
export const outFrame = (kind: MoveKind | 'knock', k: number, n: number, size: readonly [number, number], px: number): TitleFrame =>
|
|
203
|
+
kind === 'knock' ? knockFrame(k / n, size) : moveFrame(kind, 1 - (k + 1) / n, size, px)
|
|
204
|
+
|
|
205
|
+
/** the files of an entrance's or an exit's frames beside a title's PNG, as ffmpeg reads a numbered run */
|
|
206
|
+
export const inFrames = (file: string): string => `${file}.in-%04d.png`
|
|
207
|
+
export const outFrames = (file: string): string => `${file}.out-%04d.png`
|
package/src/trailer/plan.ts
CHANGED
|
@@ -12,8 +12,8 @@ export function footageKey(version: unknown, recipe: unknown, size: readonly [nu
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
/** a film that writes a take of its own: the others are read where they lie (a clip, an image) or drawn (a card) */
|
|
15
|
-
export type TakeFilm = Extract<Film, { kind: 'run' | 'cli' | 'url' }>
|
|
16
|
-
export const isTake = (f: Film | null): f is TakeFilm => f?.kind === 'run' || f?.kind === 'cli' || f?.kind === 'url'
|
|
15
|
+
export type TakeFilm = Extract<Film, { kind: 'run' | 'cli' | 'url' | 'page' }>
|
|
16
|
+
export const isTake = (f: Film | null): f is TakeFilm => f?.kind === 'run' || f?.kind === 'cli' || f?.kind === 'url' || f?.kind === 'page'
|
|
17
17
|
|
|
18
18
|
/** the take is one picture, held for the shot */
|
|
19
19
|
export const takeIsStill = (f: TakeFilm): boolean => (f.kind === 'cli' ? f.output === 'still' : f.still === true)
|
package/src/trailer/prune.ts
CHANGED
|
@@ -28,7 +28,7 @@ export interface Live {
|
|
|
28
28
|
/** the live set: each cut read, the name of every cache file its build reads, by the build's own key functions */
|
|
29
29
|
export async function liveSet(root: string): Promise<Live> {
|
|
30
30
|
const live: Live = { footage: new Set(), stems: new Set(), sound: new Set(), score: new Set(), titles: new Set(), broken: [] }
|
|
31
|
-
const [{ takesOfLoaded }, { trailerCache }, { scoreWavFile }, { scoreModuleFile, scoreModule }, titles, { lookOf }, { loadBrand, identityLogo }] = await Promise.all([
|
|
31
|
+
const [{ fileOf, takesOfLoaded, titleRequest }, { trailerCache }, { scoreWavFile }, { scoreModuleFile, scoreModule }, titles, { lookOf }, { loadBrand, identityLogo }, fonts, { titleMotions }, { resolveStyles }] = await Promise.all([
|
|
32
32
|
import('./build.ts'),
|
|
33
33
|
import('./footage.ts'),
|
|
34
34
|
import('./score/sound.ts'),
|
|
@@ -36,6 +36,9 @@ export async function liveSet(root: string): Promise<Live> {
|
|
|
36
36
|
import('./titles.ts'),
|
|
37
37
|
import('./look.ts'),
|
|
38
38
|
import('../assets/run-assets.ts'),
|
|
39
|
+
import('./fonts.ts'),
|
|
40
|
+
import('./graph.ts'),
|
|
41
|
+
import('./grammar.ts'),
|
|
39
42
|
])
|
|
40
43
|
const brand = await loadBrand(root)
|
|
41
44
|
const look = typeof brand === 'string' ? null : lookOf(brand, await identityLogo(root))
|
|
@@ -55,7 +58,15 @@ export async function liveSet(root: string): Promise<Live> {
|
|
|
55
58
|
if (look) {
|
|
56
59
|
const dir = trailerCache(root, 'titles')
|
|
57
60
|
names.titles.push(basename(titles.cardFile(dir, t.size, look)))
|
|
58
|
-
|
|
61
|
+
resolveStyles(t, (typeof brand === 'object' && brand?.trailer?.styles) || {})
|
|
62
|
+
const motions = titleMotions(t)
|
|
63
|
+
t.shots.forEach((s, i) =>
|
|
64
|
+
s.titles.forEach((ti, j) => {
|
|
65
|
+
const f = ti.fx?.font
|
|
66
|
+
const font = f === undefined ? null : fonts.isFontFile(f) ? fileOf(f, l) : fonts.googleFile(f, ti.fx?.weight ?? 400)
|
|
67
|
+
names.titles.push(basename(titles.titleFile(dir, { ...titleRequest(l, ti, look, motions[i]![j]!), font })))
|
|
68
|
+
}),
|
|
69
|
+
)
|
|
59
70
|
}
|
|
60
71
|
if (t.score && l.score) {
|
|
61
72
|
names.score.push(basename(scoreModuleFile(scoreModule(t.score, t.sections).text, trailerCache(root, 'score'))))
|
|
@@ -34,8 +34,9 @@ export function transpose(text: string, semis: number): string {
|
|
|
34
34
|
.map((tok) => {
|
|
35
35
|
const { mods, body } = splitMods(tok)
|
|
36
36
|
const [note = '', len] = body.split('/')
|
|
37
|
-
const
|
|
38
|
-
|
|
37
|
+
const midis = note.split('+').map(noteToMidi)
|
|
38
|
+
if (midis.some((m) => m === null)) return tok
|
|
39
|
+
return mods + midis.map((m) => midiToNote(m! + semis)).join('+') + (len === undefined ? '' : `/${len}`)
|
|
39
40
|
})
|
|
40
41
|
.join(' ')
|
|
41
42
|
}
|
|
@@ -238,3 +239,18 @@ export function voice(text: string, which: number, floor: number, bar = 16, rest
|
|
|
238
239
|
}
|
|
239
240
|
return out.join(' ')
|
|
240
241
|
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* each chord of a chart whole, as one chord token (`D4+F4+A4`): every tone placed at or above `floor` (a midi note),
|
|
245
|
+
* stacked upward, re-struck per segment and every `restrike` rows. For strings, brass, an organ or a stab.
|
|
246
|
+
*/
|
|
247
|
+
export function chords(text: string, floor: number, bar = 16, restrike = 0): string {
|
|
248
|
+
const out: string[] = []
|
|
249
|
+
for (const seg of chart(text, bar)) {
|
|
250
|
+
let m = seg.chord.root
|
|
251
|
+
while (m < floor) m += 12
|
|
252
|
+
const tok = seg.chord.tones.map((iv) => midiToNote(m + iv)).join('+')
|
|
253
|
+
for (let k = 0; k < seg.rows; k++) out.push(k === 0 || (restrike > 0 && k % restrike === 0) ? tok : '.')
|
|
254
|
+
}
|
|
255
|
+
return out.join(' ')
|
|
256
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// a score's General MIDI tracks as stems: each GM instrument's notes written as a .mid, rendered by FluidSynth with
|
|
2
|
+
// its own room off, read back for the synth to mix into the score's bus (its pan, its send to the room, the master)
|
|
3
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { fluidRender, readWav } from '../sound/fluid.ts'
|
|
7
|
+
import { stemMidi } from './midi.ts'
|
|
8
|
+
import type { CompiledScore } from './score.ts'
|
|
9
|
+
|
|
10
|
+
/** fluidsynth's gain for a stem: a GM instrument at vol 0.8 then sits near a synth one of the palette (measured on one
|
|
11
|
+
* arpeggio: GM piano -26.9 dBFS at gain 1, the palette's keys -19.5) */
|
|
12
|
+
const STEM_GAIN = 2.2
|
|
13
|
+
|
|
14
|
+
export interface Stem {
|
|
15
|
+
instrument: string
|
|
16
|
+
left: Float32Array
|
|
17
|
+
right: Float32Array
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** the score's GM instruments that play a note before `end` */
|
|
21
|
+
export const gmInstruments = (score: CompiledScore, end: number): string[] =>
|
|
22
|
+
[...new Set(score.notes.filter((n) => n.time < end && score.instruments[n.instrument]?.osc === 'gm').map((n) => n.instrument))]
|
|
23
|
+
|
|
24
|
+
/** one stem a GM instrument, `rate` samples a second, its notes up to `end` seconds of the render */
|
|
25
|
+
export function gmStems(score: CompiledScore, end: number, rate: number): Stem[] {
|
|
26
|
+
const names = gmInstruments(score, end)
|
|
27
|
+
if (names.length === 0) return []
|
|
28
|
+
const dir = mkdtempSync(join(tmpdir(), 'sk-gm-'))
|
|
29
|
+
try {
|
|
30
|
+
return names.map((name, i) => {
|
|
31
|
+
const v = score.instruments[name]!.gm!
|
|
32
|
+
const mid = join(dir, `${i}.mid`)
|
|
33
|
+
const wav = join(dir, `${i}.wav`)
|
|
34
|
+
writeFileSync(mid, stemMidi(score.notes.filter((n) => n.instrument === name), v, end))
|
|
35
|
+
fluidRender(mid, wav, { rate, room: false, gain: STEM_GAIN })
|
|
36
|
+
const w = readWav(wav)
|
|
37
|
+
if (w.rate !== rate) throw new Error(`fluidsynth rendered ${name} at ${w.rate} Hz, not ${rate}`)
|
|
38
|
+
return { instrument: name, left: w.left, right: w.right }
|
|
39
|
+
})
|
|
40
|
+
} finally {
|
|
41
|
+
rmSync(dir, { recursive: true, force: true })
|
|
42
|
+
}
|
|
43
|
+
}
|