reframe-video 0.6.26 → 0.6.27

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/dist/cli.js CHANGED
@@ -6,6 +6,30 @@ import { tmpdir as tmpdir6 } from "node:os";
6
6
  import { basename, dirname as dirname7, join as join9, resolve as resolve6 } from "node:path";
7
7
 
8
8
  // ../core/src/ir.ts
9
+ var SFX_NAMES = [
10
+ "whoosh",
11
+ "swish",
12
+ "rise",
13
+ "riser",
14
+ "warp",
15
+ "tick",
16
+ "click",
17
+ "blip",
18
+ "pop",
19
+ "select",
20
+ "thud",
21
+ "boom",
22
+ "knock",
23
+ "chime",
24
+ "ding",
25
+ "coin",
26
+ "sparkle",
27
+ "shimmer",
28
+ "success",
29
+ "zap",
30
+ "error"
31
+ ];
32
+ var BGM_SYNTHS = ["ambient-pad", "lofi", "pulse", "tension", "uplift"];
9
33
  var DEFAULT_CROSSFADE = 0.5;
10
34
  var DEFAULT_TO_DURATION = 0.5;
11
35
  var DEFAULT_TWEEN_DURATION = 0.5;
@@ -656,7 +680,6 @@ function validateScene(ir) {
656
680
  }
657
681
  }
658
682
  }
659
- const SFX_NAMES = ["whoosh", "pop", "tick", "rise", "shimmer", "thud"];
660
683
  for (const [i, cue] of (ir.audio?.cues ?? []).entries()) {
661
684
  if (typeof cue.at === "string" && !labels.has(cue.at)) {
662
685
  problems.push(
@@ -692,6 +715,10 @@ function validateScene(ir) {
692
715
  if (ir.audio?.bgm?.file !== void 0 && ir.audio.bgm.synth !== void 0) {
693
716
  problems.push('audio.bgm: use either "file" or "synth", not both');
694
717
  }
718
+ const bgmSynth = ir.audio?.bgm?.synth;
719
+ if (bgmSynth !== void 0 && !BGM_SYNTHS.includes(bgmSynth)) {
720
+ problems.push(`audio.bgm.synth: unknown synth "${bgmSynth}" \u2014 valid: ${BGM_SYNTHS.join(", ")}`);
721
+ }
695
722
  if (problems.length > 0) throw new SceneValidationError(problems);
696
723
  }
697
724
  var TRANSITIONS = ["cut", "crossfade"];
@@ -991,12 +1018,32 @@ var SET = 1 / 120;
991
1018
 
992
1019
  // ../core/src/audio.ts
993
1020
  var SFX_DURATION = {
1021
+ // transition
994
1022
  whoosh: 0.35,
995
- pop: 0.12,
996
- tick: 0.03,
1023
+ swish: 0.32,
997
1024
  rise: 0.5,
1025
+ riser: 0.85,
1026
+ warp: 0.5,
1027
+ // ui
1028
+ tick: 0.03,
1029
+ click: 0.05,
1030
+ blip: 0.1,
1031
+ pop: 0.12,
1032
+ select: 0.18,
1033
+ // impact
1034
+ thud: 0.25,
1035
+ boom: 0.6,
1036
+ knock: 0.14,
1037
+ // positive
1038
+ chime: 0.7,
1039
+ ding: 0.5,
1040
+ coin: 0.3,
1041
+ sparkle: 0.6,
998
1042
  shimmer: 0.9,
999
- thud: 0.25
1043
+ success: 0.6,
1044
+ // alert
1045
+ zap: 0.22,
1046
+ error: 0.4
1000
1047
  };
1001
1048
  var FILE_CUE_DURATION = 0.4;
1002
1049
  function collectClipAudio(ir, duration, warnings) {
@@ -1056,7 +1103,11 @@ function resolveAudioPlan(compiled) {
1056
1103
  fadeIn: cue.fadeIn ?? 0,
1057
1104
  fadeOut: cue.fadeOut ?? 0,
1058
1105
  pan: cue.pan ?? 0,
1059
- source: cue.sfx ? { kind: "sfx", name: cue.sfx, params: cue.params ?? {} } : { kind: "file", path: cue.file }
1106
+ source: cue.sfx ? (
1107
+ // auto-vary: default the seed to the cue's order so repeated sfx differ
1108
+ // (pitch/texture); an explicit params.seed always wins.
1109
+ { kind: "sfx", name: cue.sfx, params: { seed: index, ...cue.params } }
1110
+ ) : { kind: "file", path: cue.file }
1060
1111
  });
1061
1112
  }
1062
1113
  cues.sort((a, b) => a.t - b.t);
@@ -1136,7 +1187,11 @@ function resolveCompositionAudioPlan(comp) {
1136
1187
  fadeIn: cue.fadeIn ?? 0,
1137
1188
  fadeOut: cue.fadeOut ?? 0,
1138
1189
  pan: cue.pan ?? 0,
1139
- source: cue.sfx ? { kind: "sfx", name: cue.sfx, params: cue.params ?? {} } : { kind: "file", path: cue.file }
1190
+ source: cue.sfx ? (
1191
+ // auto-vary: default the seed to the cue's order so repeated sfx differ
1192
+ // (pitch/texture); an explicit params.seed always wins.
1193
+ { kind: "sfx", name: cue.sfx, params: { seed: index, ...cue.params } }
1194
+ ) : { kind: "file", path: cue.file }
1140
1195
  });
1141
1196
  }
1142
1197
  if (!audio?.bgm && cues.length === 0 && clipAudio.length === 0) return null;
@@ -1240,19 +1295,23 @@ function hash01(n, seed) {
1240
1295
  var noise = (n, seed) => hash01(n, seed) * 2 - 1;
1241
1296
  var TAU = Math.PI * 2;
1242
1297
  var expDecay = (t, dur, k = 5) => Math.exp(-k * t / dur);
1298
+ var square = (ph) => (Math.sin(ph) + 0.33 * Math.sin(3 * ph) + 0.2 * Math.sin(5 * ph)) / 1.4;
1299
+ var PITCH_STEPS = [0, 2, 4, 7, 9, 12, 5, -3, 16, -5];
1300
+ function seedPitch(seed) {
1301
+ const i = (Math.round(seed) % PITCH_STEPS.length + PITCH_STEPS.length) % PITCH_STEPS.length;
1302
+ return Math.pow(2, PITCH_STEPS[i] / 12);
1303
+ }
1243
1304
  function buffer(duration) {
1244
1305
  const n = Math.round(duration * SAMPLE_RATE);
1245
1306
  return { out: new Float32Array(n), n };
1246
1307
  }
1247
- function whoosh(seed) {
1308
+ function whoosh(seed, pitch) {
1248
1309
  const dur = 0.35;
1249
1310
  const { out, n } = buffer(dur);
1250
- let lp = 0;
1251
- let lp2 = 0;
1311
+ let lp = 0, lp2 = 0;
1252
1312
  for (let i = 0; i < n; i++) {
1253
- const t = i / SAMPLE_RATE;
1254
- const u = t / dur;
1255
- const center = 1200 * Math.pow(300 / 1200, u);
1313
+ const t = i / SAMPLE_RATE, u = t / dur;
1314
+ const center = 1200 * pitch * Math.pow(0.25, u);
1256
1315
  const alpha = Math.min(1, TAU * center / SAMPLE_RATE);
1257
1316
  lp += alpha * (noise(i, seed) - lp);
1258
1317
  lp2 += alpha * 0.5 * (lp - lp2);
@@ -1261,110 +1320,392 @@ function whoosh(seed) {
1261
1320
  }
1262
1321
  return out;
1263
1322
  }
1264
- function pop(seed) {
1323
+ function swish(seed, pitch) {
1324
+ const dur = 0.32;
1325
+ const { out, n } = buffer(dur);
1326
+ let lp = 0, lp2 = 0;
1327
+ for (let i = 0; i < n; i++) {
1328
+ const t = i / SAMPLE_RATE, u = t / dur;
1329
+ const center = 2600 * pitch * Math.pow(0.2, u);
1330
+ const alpha = Math.min(1, TAU * center / SAMPLE_RATE);
1331
+ lp += alpha * (noise(i, seed) - lp);
1332
+ lp2 += alpha * 0.5 * (lp - lp2);
1333
+ const env = u < 0.15 ? u / 0.15 : expDecay(t - 0.15 * dur, dur * 0.85, 5);
1334
+ out[i] = (lp - lp2) * env * 2.4;
1335
+ }
1336
+ return out;
1337
+ }
1338
+ function rise(_seed, pitch) {
1339
+ const dur = 0.5;
1340
+ const { out, n } = buffer(dur);
1341
+ let phase = 0;
1342
+ for (let i = 0; i < n; i++) {
1343
+ const t = i / SAMPLE_RATE, u = t / dur;
1344
+ const freq = 220 * pitch * Math.pow(4, u);
1345
+ phase += TAU * freq / SAMPLE_RATE;
1346
+ const env = Math.sin(Math.PI * Math.min(1, u * 1.05)) ** 1.5;
1347
+ out[i] = (Math.sin(phase) + 0.3 * Math.sin(2 * phase)) * env * 0.45;
1348
+ }
1349
+ return out;
1350
+ }
1351
+ function riser(seed, pitch) {
1352
+ const dur = 0.85;
1353
+ const { out, n } = buffer(dur);
1354
+ let lp = 0, phase = 0;
1355
+ for (let i = 0; i < n; i++) {
1356
+ const t = i / SAMPLE_RATE, u = t / dur;
1357
+ const center = 200 * pitch * Math.pow(12, u);
1358
+ const alpha = Math.min(1, TAU * center / SAMPLE_RATE);
1359
+ lp += alpha * (noise(i, seed) - lp);
1360
+ const freq = 120 * pitch * Math.pow(6, u);
1361
+ phase += TAU * freq / SAMPLE_RATE;
1362
+ const env = Math.pow(u, 1.6);
1363
+ out[i] = (lp * 1.6 + Math.sin(phase) * 0.5) * env * 0.9;
1364
+ }
1365
+ return out;
1366
+ }
1367
+ function warp(_seed, pitch) {
1368
+ const dur = 0.5;
1369
+ const { out, n } = buffer(dur);
1370
+ let phase = 0;
1371
+ for (let i = 0; i < n; i++) {
1372
+ const t = i / SAMPLE_RATE, u = t / dur;
1373
+ const bend = Math.sin(Math.PI * u);
1374
+ const vib = 1 + 0.4 * Math.sin(TAU * 18 * t);
1375
+ const freq = 300 * pitch * (1 + 2.5 * bend) * vib;
1376
+ phase += TAU * freq / SAMPLE_RATE;
1377
+ const env = Math.sin(Math.PI * u) ** 0.8;
1378
+ out[i] = square(phase) * env * 0.5;
1379
+ }
1380
+ return out;
1381
+ }
1382
+ function tick(seed, pitch) {
1383
+ const dur = 0.03;
1384
+ const { out, n } = buffer(dur);
1385
+ for (let i = 0; i < n; i++) {
1386
+ const t = i / SAMPLE_RATE;
1387
+ const sine = t < 4e-3 ? Math.sin(TAU * 4e3 * pitch * t) : 0;
1388
+ out[i] = (sine * 0.6 + noise(i, seed) * 0.35) * expDecay(t, dur, 8);
1389
+ }
1390
+ return out;
1391
+ }
1392
+ function click(seed, pitch) {
1393
+ const dur = 0.05;
1394
+ const { out, n } = buffer(dur);
1395
+ let lp = 0;
1396
+ for (let i = 0; i < n; i++) {
1397
+ const t = i / SAMPLE_RATE;
1398
+ lp += 0.5 * (noise(i, seed) - lp);
1399
+ const sine = Math.sin(TAU * 1500 * pitch * t);
1400
+ out[i] = (sine * 0.5 + lp * 0.6) * expDecay(t, dur, 11);
1401
+ }
1402
+ return out;
1403
+ }
1404
+ function blip(_seed, pitch) {
1405
+ const dur = 0.1;
1406
+ const { out, n } = buffer(dur);
1407
+ let phase = 0;
1408
+ for (let i = 0; i < n; i++) {
1409
+ const t = i / SAMPLE_RATE, u = t / dur;
1410
+ phase += TAU * 880 * pitch / SAMPLE_RATE;
1411
+ const env = Math.min(1, u * 12) * Math.min(1, (1 - u) * 6);
1412
+ out[i] = square(phase) * env * 0.5;
1413
+ }
1414
+ return out;
1415
+ }
1416
+ function pop(seed, pitch) {
1265
1417
  const dur = 0.12;
1266
1418
  const { out, n } = buffer(dur);
1267
1419
  let phase = 0;
1268
1420
  for (let i = 0; i < n; i++) {
1269
1421
  const t = i / SAMPLE_RATE;
1270
- const freq = 600 * Math.pow(150 / 600, t / 0.08);
1422
+ const freq = 600 * pitch * Math.pow(0.25, t / 0.08);
1271
1423
  phase += TAU * freq / SAMPLE_RATE;
1272
1424
  const transient = t < 2e-3 ? noise(i, seed) * 0.5 : 0;
1273
1425
  out[i] = (Math.sin(phase) + transient) * expDecay(t, dur, 6) * 0.8;
1274
1426
  }
1275
1427
  return out;
1276
1428
  }
1277
- function tick(seed) {
1278
- const dur = 0.03;
1429
+ function select(_seed, pitch) {
1430
+ const dur = 0.18;
1431
+ const { out, n } = buffer(dur);
1432
+ let phase = 0;
1433
+ for (let i = 0; i < n; i++) {
1434
+ const t = i / SAMPLE_RATE, u = t / dur;
1435
+ const freq = (t < 0.08 ? 620 : 930) * pitch;
1436
+ phase += TAU * freq / SAMPLE_RATE;
1437
+ const env = Math.min(1, u * 16) * Math.min(1, (1 - u) * 5);
1438
+ out[i] = (Math.sin(phase) + 0.25 * Math.sin(2 * phase)) * env * 0.5;
1439
+ }
1440
+ return out;
1441
+ }
1442
+ function thud(seed, pitch) {
1443
+ const dur = 0.25;
1279
1444
  const { out, n } = buffer(dur);
1445
+ let phase = 0, lp = 0;
1280
1446
  for (let i = 0; i < n; i++) {
1281
1447
  const t = i / SAMPLE_RATE;
1282
- const sine = t < 4e-3 ? Math.sin(TAU * 4e3 * t) : 0;
1283
- out[i] = (sine * 0.6 + noise(i, seed) * 0.35) * expDecay(t, dur, 8);
1448
+ const freq = 90 * pitch * Math.pow(0.5, t / 0.15);
1449
+ phase += TAU * freq / SAMPLE_RATE;
1450
+ lp += 0.02 * (noise(i, seed) - lp);
1451
+ const attack = t < 0.01 ? lp * 3 : 0;
1452
+ out[i] = (Math.sin(phase) * 0.9 + attack) * expDecay(t, dur, 5);
1284
1453
  }
1285
1454
  return out;
1286
1455
  }
1287
- function rise(seed) {
1456
+ function boom(seed, pitch) {
1457
+ const dur = 0.6;
1458
+ const { out, n } = buffer(dur);
1459
+ let phase = 0, lp = 0;
1460
+ for (let i = 0; i < n; i++) {
1461
+ const t = i / SAMPLE_RATE;
1462
+ const freq = 70 * pitch * Math.pow(0.5, t / 0.3);
1463
+ phase += TAU * freq / SAMPLE_RATE;
1464
+ lp += 0.06 * (noise(i, seed) - lp);
1465
+ const body = t < 0.06 ? lp * 2.5 * (1 - t / 0.06) : 0;
1466
+ out[i] = (Math.sin(phase) * 1 + body) * expDecay(t, dur, 3.2);
1467
+ }
1468
+ return out;
1469
+ }
1470
+ function knock(seed, pitch) {
1471
+ const dur = 0.14;
1472
+ const { out, n } = buffer(dur);
1473
+ let phase = 0, lp = 0;
1474
+ for (let i = 0; i < n; i++) {
1475
+ const t = i / SAMPLE_RATE;
1476
+ const freq = 220 * pitch * Math.pow(0.7, t / 0.05);
1477
+ phase += TAU * freq / SAMPLE_RATE;
1478
+ lp += 0.3 * (noise(i, seed) - lp);
1479
+ const tap = t < 5e-3 ? lp : 0;
1480
+ out[i] = (Math.sin(phase) * 0.8 + tap * 0.6) * expDecay(t, dur, 9);
1481
+ }
1482
+ return out;
1483
+ }
1484
+ function chime(seed, pitch) {
1485
+ const dur = 0.7;
1486
+ const { out, n } = buffer(dur);
1487
+ const f0 = 800 * pitch;
1488
+ const partials = [
1489
+ { f: f0, a: 1, k: 4 },
1490
+ { f: f0 * 2.76, a: 0.5, k: 5.5 },
1491
+ { f: f0 * 5.4, a: 0.28, k: 7 },
1492
+ { f: f0 * 8.9, a: 0.13, k: 9 }
1493
+ ];
1494
+ for (let i = 0; i < n; i++) {
1495
+ const t = i / SAMPLE_RATE;
1496
+ let s = 0;
1497
+ for (const p of partials) s += Math.sin(TAU * p.f * t) * p.a * expDecay(t, dur, p.k);
1498
+ const strike = t < 3e-3 ? noise(i, seed) * 0.3 : 0;
1499
+ out[i] = (s / 1.9 + strike) * 0.6;
1500
+ }
1501
+ return out;
1502
+ }
1503
+ function ding(_seed, pitch) {
1288
1504
  const dur = 0.5;
1289
1505
  const { out, n } = buffer(dur);
1290
- let phase = 0;
1506
+ const f0 = 1200 * pitch;
1291
1507
  for (let i = 0; i < n; i++) {
1292
1508
  const t = i / SAMPLE_RATE;
1293
- const u = t / dur;
1294
- const freq = 220 * Math.pow(880 / 220, u);
1509
+ const s = Math.sin(TAU * f0 * t) + 0.4 * Math.sin(TAU * f0 * 2 * t) + 0.2 * Math.sin(TAU * f0 * 3.01 * t);
1510
+ out[i] = s / 1.6 * expDecay(t, dur, 4.5) * 0.6;
1511
+ }
1512
+ return out;
1513
+ }
1514
+ function coin(_seed, pitch) {
1515
+ const dur = 0.3;
1516
+ const { out, n } = buffer(dur);
1517
+ let phase = 0;
1518
+ for (let i = 0; i < n; i++) {
1519
+ const t = i / SAMPLE_RATE, u = t / dur;
1520
+ const freq = (t < 0.06 ? 988 : 1319) * pitch;
1295
1521
  phase += TAU * freq / SAMPLE_RATE;
1296
- const env = Math.sin(Math.PI * Math.min(1, u * 1.05)) ** 1.5;
1297
- out[i] = (Math.sin(phase) + 0.3 * Math.sin(2 * phase)) * env * 0.45;
1522
+ const env = Math.min(1, u * 30) * expDecay(Math.max(0, t - 0.06), dur, 3.5);
1523
+ out[i] = square(phase) * env * 0.55;
1524
+ }
1525
+ return out;
1526
+ }
1527
+ function sparkle(seed, pitch) {
1528
+ const dur = 0.6;
1529
+ const { out, n } = buffer(dur);
1530
+ const steps = [1, 1.5, 2, 3, 4, 5, 6];
1531
+ const base = 1200 * pitch;
1532
+ for (let i = 0; i < n; i++) {
1533
+ const t = i / SAMPLE_RATE, u = t / dur;
1534
+ let s = 0;
1535
+ for (let g = 0; g < steps.length; g++) {
1536
+ const on = u * steps.length - g;
1537
+ if (on > 0 && on < 3) {
1538
+ const ge = Math.exp(-on * 1.8);
1539
+ s += Math.sin(TAU * base * steps[g] * t + hash01(g, seed) * TAU) * ge;
1540
+ }
1541
+ }
1542
+ out[i] = s / 2.4 * Math.sin(Math.PI * u) ** 0.5 * 0.5;
1543
+ }
1544
+ return out;
1545
+ }
1546
+ function success(_seed, pitch) {
1547
+ const dur = 0.6;
1548
+ const { out, n } = buffer(dur);
1549
+ const notes = [523.25, 659.25, 783.99].map((f) => f * pitch);
1550
+ let phase = 0, cur = 0;
1551
+ for (let i = 0; i < n; i++) {
1552
+ const t = i / SAMPLE_RATE, u = t / dur;
1553
+ const idx = Math.min(notes.length - 1, Math.floor(u * 3.2));
1554
+ if (idx !== cur) cur = idx;
1555
+ phase += TAU * notes[cur] / SAMPLE_RATE;
1556
+ const local = u * 3.2 - idx;
1557
+ const env = Math.min(1, local * 12) * Math.min(1, (1 - Math.min(1, local)) * 4 + 0.2);
1558
+ out[i] = (Math.sin(phase) + 0.3 * Math.sin(2 * phase)) * env * 0.42;
1298
1559
  }
1299
1560
  return out;
1300
1561
  }
1301
- function shimmer(seed) {
1562
+ function shimmer(seed, pitch) {
1302
1563
  const dur = 0.9;
1303
1564
  const { out, n } = buffer(dur);
1304
1565
  const partials = Array.from({ length: 5 }, (_, p) => ({
1305
- freq: 2e3 + hash01(p, seed + 7) * 2e3,
1566
+ freq: (2e3 + hash01(p, seed + 7) * 2e3) * pitch,
1306
1567
  am: 0.5 + hash01(p, seed + 8) * 1.5,
1307
1568
  phase: hash01(p, seed + 9) * TAU
1308
1569
  }));
1309
1570
  for (let i = 0; i < n; i++) {
1310
- const t = i / SAMPLE_RATE;
1311
- const u = t / dur;
1571
+ const t = i / SAMPLE_RATE, u = t / dur;
1312
1572
  const env = Math.sin(Math.PI * u) ** 1.2;
1313
1573
  let s = 0;
1314
- for (const part of partials) {
1315
- s += Math.sin(TAU * part.freq * t + part.phase) * (0.6 + 0.4 * Math.sin(TAU * part.am * t));
1316
- }
1574
+ for (const part of partials) s += Math.sin(TAU * part.freq * t + part.phase) * (0.6 + 0.4 * Math.sin(TAU * part.am * t));
1317
1575
  out[i] = s / 5 * env * 0.5;
1318
1576
  }
1319
1577
  return out;
1320
1578
  }
1321
- function thud(seed) {
1322
- const dur = 0.25;
1579
+ function zap(seed, pitch) {
1580
+ const dur = 0.22;
1323
1581
  const { out, n } = buffer(dur);
1324
1582
  let phase = 0;
1325
- let lp = 0;
1326
1583
  for (let i = 0; i < n; i++) {
1327
- const t = i / SAMPLE_RATE;
1328
- const freq = 90 * Math.pow(45 / 90, t / 0.15);
1584
+ const t = i / SAMPLE_RATE, u = t / dur;
1585
+ const freq = 1600 * pitch * Math.pow(0.12, u);
1329
1586
  phase += TAU * freq / SAMPLE_RATE;
1330
- lp += 0.02 * (noise(i, seed) - lp);
1331
- const attack = t < 0.01 ? lp * 3 : 0;
1332
- out[i] = (Math.sin(phase) * 0.9 + attack) * expDecay(t, dur, 5);
1587
+ const grit = noise(i, seed) * 0.25;
1588
+ out[i] = (square(phase) + grit) * expDecay(t, dur, 4.5) * 0.5;
1589
+ }
1590
+ return out;
1591
+ }
1592
+ function error(_seed, pitch) {
1593
+ const dur = 0.4;
1594
+ const { out, n } = buffer(dur);
1595
+ let phase = 0;
1596
+ for (let i = 0; i < n; i++) {
1597
+ const t = i / SAMPLE_RATE, u = t / dur;
1598
+ const freq = (t < 0.16 ? 311 : 233) * pitch;
1599
+ phase += TAU * freq / SAMPLE_RATE;
1600
+ const seg = t < 0.16 ? u / 0.4 : (u - 0.4) / 0.6;
1601
+ const env = Math.min(1, seg * 18) * Math.min(1, (1 - seg) * 6);
1602
+ out[i] = square(phase) * env * 0.5;
1333
1603
  }
1334
1604
  return out;
1335
1605
  }
1336
1606
  var RECIPES = {
1337
1607
  whoosh,
1338
- pop,
1339
- tick,
1608
+ swish,
1340
1609
  rise,
1610
+ riser,
1611
+ warp,
1612
+ tick,
1613
+ click,
1614
+ blip,
1615
+ pop,
1616
+ select,
1617
+ thud,
1618
+ boom,
1619
+ knock,
1620
+ chime,
1621
+ ding,
1622
+ coin,
1623
+ sparkle,
1341
1624
  shimmer,
1342
- thud
1625
+ success,
1626
+ zap,
1627
+ error
1343
1628
  };
1344
1629
  function synthSfx(name, params = {}) {
1345
- const samples = RECIPES[name](params.seed ?? 0);
1630
+ const seed = params.seed ?? 0;
1631
+ const pitch = (params.pitch ?? 1) * seedPitch(seed);
1632
+ const samples = RECIPES[name](seed, pitch);
1346
1633
  if (params.gainDb) {
1347
1634
  const g = Math.pow(10, params.gainDb / 20);
1348
1635
  for (let i = 0; i < samples.length; i++) samples[i] *= g;
1349
1636
  }
1637
+ let peak = 0;
1638
+ for (let i = 0; i < samples.length; i++) peak = Math.max(peak, Math.abs(samples[i]));
1639
+ if (peak > 0.95) {
1640
+ const g = 0.95 / peak;
1641
+ for (let i = 0; i < samples.length; i++) samples[i] *= g;
1642
+ }
1350
1643
  return samples;
1351
1644
  }
1352
- function synthAmbientPad(duration, seed = 0) {
1645
+ function pad(freqs, duration, seed, opts) {
1353
1646
  const { out, n } = buffer(duration);
1354
- const voices = [110, 165, 220].flatMap((f, v) => [
1355
- { freq: f * (1 + (hash01(v, seed + 3) - 0.5) * 4e-3), am: 0.05 + hash01(v, seed + 4) * 0.08, phase: hash01(v, seed + 5) * TAU },
1356
- { freq: f * (1 - (hash01(v, seed + 6) - 0.5) * 4e-3), am: 0.05 + hash01(v, seed + 7) * 0.08, phase: hash01(v, seed + 8) * TAU }
1647
+ const voices = freqs.flatMap((f, v) => [
1648
+ { freq: f * (1 + (hash01(v, seed + 3) - 0.5) * 4e-3), am: opts.amBase + hash01(v, seed + 4) * 0.08, phase: hash01(v, seed + 5) * TAU },
1649
+ { freq: f * (1 - (hash01(v, seed + 6) - 0.5) * 4e-3), am: opts.amBase + hash01(v, seed + 7) * 0.08, phase: hash01(v, seed + 8) * TAU }
1357
1650
  ]);
1358
1651
  for (let i = 0; i < n; i++) {
1359
1652
  const t = i / SAMPLE_RATE;
1360
1653
  let s = 0;
1361
1654
  for (const voice of voices) {
1362
1655
  s += Math.sin(TAU * voice.freq * t + voice.phase) * (0.75 + 0.25 * Math.sin(TAU * voice.am * t));
1656
+ if (opts.bright > 0) s += opts.bright * Math.sin(TAU * voice.freq * 2 * t + voice.phase);
1363
1657
  }
1364
- out[i] = s / voices.length * 0.7;
1658
+ out[i] = s / voices.length * opts.gain;
1365
1659
  }
1366
1660
  return out;
1367
1661
  }
1662
+ function synthAmbientPad(duration, seed = 0) {
1663
+ return pad([110, 165, 220], duration, seed, { amBase: 0.05, bright: 0, gain: 0.7 });
1664
+ }
1665
+ function synthLofi(duration, seed = 0) {
1666
+ return pad([130.81, 164.81, 196, 246.94], duration, seed, { amBase: 0.04, bright: 0.04, gain: 0.62 });
1667
+ }
1668
+ function synthPulse(duration, _seed = 0) {
1669
+ const { out, n } = buffer(duration);
1670
+ const beat2 = 2.2;
1671
+ for (let i = 0; i < n; i++) {
1672
+ const t = i / SAMPLE_RATE;
1673
+ const ph = t * beat2 % 1;
1674
+ const gate = Math.exp(-ph * 5) * 0.9 + 0.1;
1675
+ const s = Math.sin(TAU * 82 * t) + 0.6 * Math.sin(TAU * 123 * t) + 0.3 * Math.sin(TAU * 246 * t);
1676
+ out[i] = s / 1.9 * gate * 0.6;
1677
+ }
1678
+ return out;
1679
+ }
1680
+ function synthTension(duration, seed = 0) {
1681
+ const { out, n } = buffer(duration);
1682
+ const base = [98, 104, 110];
1683
+ for (let i = 0; i < n; i++) {
1684
+ const t = i / SAMPLE_RATE;
1685
+ const drift = 1 + 0.03 * (t / Math.max(1e-3, duration));
1686
+ let s = 0;
1687
+ for (let v = 0; v < base.length; v++) {
1688
+ const f = base[v] * drift * (1 + (hash01(v, seed) - 0.5) * 6e-3);
1689
+ s += Math.sin(TAU * f * t + hash01(v, seed + 1) * TAU);
1690
+ }
1691
+ const swell = 0.6 + 0.4 * Math.sin(TAU * 0.08 * t);
1692
+ out[i] = s / base.length * swell * 0.6;
1693
+ }
1694
+ return out;
1695
+ }
1696
+ function synthUplift(duration, seed = 0) {
1697
+ return pad([196, 246.94, 293.66, 392], duration, seed, { amBase: 0.07, bright: 0.1, gain: 0.6 });
1698
+ }
1699
+ var BGM_RECIPES = {
1700
+ "ambient-pad": synthAmbientPad,
1701
+ lofi: synthLofi,
1702
+ pulse: synthPulse,
1703
+ tension: synthTension,
1704
+ uplift: synthUplift
1705
+ };
1706
+ function synthBgm(name, duration, seed = 0) {
1707
+ return BGM_RECIPES[name](duration, seed);
1708
+ }
1368
1709
 
1369
1710
  // ../render-cli/src/audio/sfx.ts
1370
1711
  var ROOT = true ? resolve(dirname(fileURLToPath(import.meta.url)), "..") : resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "..");
@@ -1414,7 +1755,7 @@ async function resolveBgmFile(source, duration, sceneDir) {
1414
1755
  }
1415
1756
  throw new Error(`bgm file "${p}" not found`);
1416
1757
  }
1417
- return writeCached(`ambient-pad-${duration.toFixed(2)}`, () => synthAmbientPad(duration));
1758
+ return writeCached(`${source.name}-${duration.toFixed(2)}`, () => synthBgm(source.name, duration));
1418
1759
  }
1419
1760
 
1420
1761
  // ../render-cli/src/audio/clip.ts
@@ -4,6 +4,32 @@ import { readFile } from "node:fs/promises";
4
4
  import { dirname, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
 
7
+ // ../core/src/ir.ts
8
+ var SFX_NAMES = [
9
+ "whoosh",
10
+ "swish",
11
+ "rise",
12
+ "riser",
13
+ "warp",
14
+ "tick",
15
+ "click",
16
+ "blip",
17
+ "pop",
18
+ "select",
19
+ "thud",
20
+ "boom",
21
+ "knock",
22
+ "chime",
23
+ "ding",
24
+ "coin",
25
+ "sparkle",
26
+ "shimmer",
27
+ "success",
28
+ "zap",
29
+ "error"
30
+ ];
31
+ var BGM_SYNTHS = ["ambient-pad", "lofi", "pulse", "tension", "uplift"];
32
+
7
33
  // ../core/src/interpolate.ts
8
34
  var BACK_C1 = 1.70158;
9
35
  var BACK_C2 = BACK_C1 * 1.525;
@@ -329,7 +355,6 @@ function validateScene(ir) {
329
355
  }
330
356
  }
331
357
  }
332
- const SFX_NAMES = ["whoosh", "pop", "tick", "rise", "shimmer", "thud"];
333
358
  for (const [i, cue] of (ir.audio?.cues ?? []).entries()) {
334
359
  if (typeof cue.at === "string" && !labels.has(cue.at)) {
335
360
  problems.push(
@@ -365,6 +390,10 @@ function validateScene(ir) {
365
390
  if (ir.audio?.bgm?.file !== void 0 && ir.audio.bgm.synth !== void 0) {
366
391
  problems.push('audio.bgm: use either "file" or "synth", not both');
367
392
  }
393
+ const bgmSynth = ir.audio?.bgm?.synth;
394
+ if (bgmSynth !== void 0 && !BGM_SYNTHS.includes(bgmSynth)) {
395
+ problems.push(`audio.bgm.synth: unknown synth "${bgmSynth}" \u2014 valid: ${BGM_SYNTHS.join(", ")}`);
396
+ }
368
397
  if (problems.length > 0) throw new SceneValidationError(problems);
369
398
  }
370
399
  var TRANSITIONS = ["cut", "crossfade"];
package/dist/compile.js CHANGED
@@ -9,6 +9,32 @@ import { readFile } from "node:fs/promises";
9
9
  import { dirname, resolve } from "node:path";
10
10
  import { fileURLToPath } from "node:url";
11
11
 
12
+ // ../core/src/ir.ts
13
+ var SFX_NAMES = [
14
+ "whoosh",
15
+ "swish",
16
+ "rise",
17
+ "riser",
18
+ "warp",
19
+ "tick",
20
+ "click",
21
+ "blip",
22
+ "pop",
23
+ "select",
24
+ "thud",
25
+ "boom",
26
+ "knock",
27
+ "chime",
28
+ "ding",
29
+ "coin",
30
+ "sparkle",
31
+ "shimmer",
32
+ "success",
33
+ "zap",
34
+ "error"
35
+ ];
36
+ var BGM_SYNTHS = ["ambient-pad", "lofi", "pulse", "tension", "uplift"];
37
+
12
38
  // ../core/src/interpolate.ts
13
39
  var BACK_C1 = 1.70158;
14
40
  var BACK_C2 = BACK_C1 * 1.525;
@@ -334,7 +360,6 @@ function validateScene(ir) {
334
360
  }
335
361
  }
336
362
  }
337
- const SFX_NAMES = ["whoosh", "pop", "tick", "rise", "shimmer", "thud"];
338
363
  for (const [i, cue] of (ir.audio?.cues ?? []).entries()) {
339
364
  if (typeof cue.at === "string" && !labels.has(cue.at)) {
340
365
  problems.push(
@@ -370,6 +395,10 @@ function validateScene(ir) {
370
395
  if (ir.audio?.bgm?.file !== void 0 && ir.audio.bgm.synth !== void 0) {
371
396
  problems.push('audio.bgm: use either "file" or "synth", not both');
372
397
  }
398
+ const bgmSynth = ir.audio?.bgm?.synth;
399
+ if (bgmSynth !== void 0 && !BGM_SYNTHS.includes(bgmSynth)) {
400
+ problems.push(`audio.bgm.synth: unknown synth "${bgmSynth}" \u2014 valid: ${BGM_SYNTHS.join(", ")}`);
401
+ }
373
402
  if (problems.length > 0) throw new SceneValidationError(problems);
374
403
  }
375
404