@hraness/dawg 0.5.0 → 0.6.1

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 (124) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/DAWG.md +592 -49
  3. package/README.md +4 -4
  4. package/core/chords.ts +492 -4
  5. package/core/diff.ts +9 -0
  6. package/core/expression.ts +143 -6
  7. package/core/fx.ts +689 -2
  8. package/core/granular.ts +619 -0
  9. package/core/instruments.ts +442 -0
  10. package/core/keys.ts +955 -0
  11. package/core/resonators.ts +862 -0
  12. package/core/score.ts +529 -8
  13. package/core/sdk/eval-child.ts +2 -0
  14. package/core/sdk/print.ts +247 -16
  15. package/core/sdk/sync-instruments.ts +58 -0
  16. package/core/sdk/v1.ts +2417 -40
  17. package/core/sections.ts +44 -10
  18. package/core/strings.ts +1080 -0
  19. package/core/winds.ts +652 -0
  20. package/guides/automation.md +1 -0
  21. package/guides/chords.md +3 -1
  22. package/guides/effects.md +5 -2
  23. package/guides/media.md +1 -1
  24. package/guides/performance.md +3 -0
  25. package/guides/resample.md +26 -0
  26. package/guides/sounds.md +7 -2
  27. package/guides/tempo.md +1 -0
  28. package/package.json +1 -1
  29. package/src/agent/agent.ts +13 -0
  30. package/src/agent/chord-tools.ts +153 -1
  31. package/src/agent/expression-tools.ts +91 -0
  32. package/src/agent/granular-tools.ts +138 -0
  33. package/src/agent/models.ts +4 -4
  34. package/src/agent/ops.ts +42 -2
  35. package/src/agent/preview-tool.ts +7 -1
  36. package/src/agent/resample-tool.ts +131 -0
  37. package/src/agent/tools.ts +726 -17
  38. package/src/agent/xcb-agent.ts +13 -0
  39. package/src/audio/arrange.ts +42 -3
  40. package/src/audio/dsp/bank.ts +233 -0
  41. package/src/audio/dsp/envelope.ts +161 -0
  42. package/src/audio/dsp/fft.ts +6 -0
  43. package/src/audio/dsp/filters.ts +57 -0
  44. package/src/audio/dsp/interp.ts +112 -0
  45. package/src/audio/dsp/modal.ts +684 -0
  46. package/src/audio/dsp/onset.ts +197 -0
  47. package/src/audio/dsp/oversample.ts +202 -0
  48. package/src/audio/dsp/rng.ts +37 -0
  49. package/src/audio/dsp/shape.ts +81 -0
  50. package/src/audio/dsp/shift.ts +256 -0
  51. package/src/audio/dsp/stft.ts +73 -0
  52. package/src/audio/dsp/window.ts +77 -0
  53. package/src/audio/effects/chain.ts +23 -3
  54. package/src/audio/effects/common.ts +14 -0
  55. package/src/audio/effects/convolution.ts +106 -1
  56. package/src/audio/effects/gaze.ts +354 -0
  57. package/src/audio/effects/rig/cab.ts +99 -0
  58. package/src/audio/effects/rig/filters.ts +152 -0
  59. package/src/audio/effects/rig/gate.ts +39 -0
  60. package/src/audio/effects/rig/head.ts +487 -0
  61. package/src/audio/effects/rig/index.ts +170 -0
  62. package/src/audio/effects/rig/section.ts +78 -0
  63. package/src/audio/effects/rig/stomp.ts +238 -0
  64. package/src/audio/engine.ts +56 -6
  65. package/src/audio/fit.ts +447 -0
  66. package/src/audio/granular.ts +995 -0
  67. package/src/audio/instrument-check.ts +137 -0
  68. package/src/audio/instruments.ts +140 -0
  69. package/src/audio/keys/dsp.ts +323 -0
  70. package/src/audio/keys/electric.ts +420 -0
  71. package/src/audio/keys/engine.ts +485 -0
  72. package/src/audio/keys/organ.ts +1335 -0
  73. package/src/audio/keys/piano.ts +476 -0
  74. package/src/audio/keys/sympathetic.ts +127 -0
  75. package/src/audio/live-worker.ts +56 -0
  76. package/src/audio/live.ts +361 -15
  77. package/src/audio/loudness.ts +65 -20
  78. package/src/audio/preview.ts +17 -1
  79. package/src/audio/resample.ts +285 -0
  80. package/src/audio/resonators.ts +295 -0
  81. package/src/audio/sampler.ts +328 -35
  82. package/src/audio/samples.ts +56 -8
  83. package/src/audio/strings/body.ts +263 -0
  84. package/src/audio/strings/bow.ts +656 -0
  85. package/src/audio/strings/engine.ts +615 -0
  86. package/src/audio/strings/loop.ts +119 -0
  87. package/src/audio/strings/measure.test-helpers.ts +200 -0
  88. package/src/audio/strings/pluck.ts +354 -0
  89. package/src/audio/warp.ts +61 -0
  90. package/src/audio/wav.ts +191 -19
  91. package/src/audio/winds/engine.ts +302 -0
  92. package/src/audio/winds/filters.ts +153 -0
  93. package/src/audio/winds/pitch.ts +104 -0
  94. package/src/audio/winds/trim.ts +80 -0
  95. package/src/audio/winds/trims.ts +917 -0
  96. package/src/audio/winds/voice.ts +479 -0
  97. package/src/commands/expression.ts +111 -23
  98. package/src/commands/fit.ts +135 -0
  99. package/src/commands/fx.ts +35 -1
  100. package/src/commands/granular.ts +432 -0
  101. package/src/commands/help.ts +159 -5
  102. package/src/commands/keys.ts +629 -0
  103. package/src/commands/modal.ts +310 -0
  104. package/src/commands/resample.ts +281 -0
  105. package/src/commands/rig.ts +264 -0
  106. package/src/commands/sample.ts +52 -2
  107. package/src/commands/shift.ts +119 -0
  108. package/src/commands/string.ts +201 -0
  109. package/src/commands/strum.ts +473 -0
  110. package/src/commands/time.ts +4 -1
  111. package/src/commands/wind.ts +244 -0
  112. package/src/main.ts +339 -15
  113. package/src/project/check.ts +17 -0
  114. package/src/render.ts +49 -4
  115. package/src/session/presence.ts +16 -3
  116. package/src/tui/audition.ts +2 -2
  117. package/src/tui/granular-menu.ts +278 -0
  118. package/src/tui/menu.ts +934 -9
  119. package/src/tui/modal-menu.ts +145 -0
  120. package/src/tui/performance-menu.ts +42 -0
  121. package/src/tui/play-chords.ts +104 -3
  122. package/src/tui/play-mode.ts +1 -0
  123. package/src/tui/play-session.ts +70 -2
  124. package/src/tui/wind-menu.ts +144 -0
package/core/fx.ts CHANGED
@@ -12,7 +12,11 @@
12
12
  * under `track.fx.<name>` with every parameter stored once enabled.
13
13
  */
14
14
 
15
+ import { MODAL_LANE_PARAMS, WIND_LANE_PARAMS } from "./resonators.ts";
15
16
  import { SYNTH_LANE_PARAMS } from "./synth.ts";
17
+ import { STRING_LANE_PARAMS } from "./strings.ts";
18
+ import { KEYS_LANE_PARAMS } from "./keys.ts";
19
+ import { GRANULAR_LANE_PARAMS } from "./granular.ts";
16
20
  import {
17
21
  FxValidationError,
18
22
  normalizeParam,
@@ -48,9 +52,20 @@ export const FX_CHAIN = Object.freeze([
48
52
  "vowel",
49
53
  "crush",
50
54
  "distort",
55
+ // 0.6 guitar rig (stomp box, amp head with noise gate, speaker cabinet).
56
+ "stomp",
57
+ "head",
58
+ "cab",
59
+ // 0.6.1 shoegaze (src/audio/effects/gaze.ts): a held tremolo arm, the
60
+ // dominant note's feedback bloom, a volume swell; double (mono in, stereo
61
+ // out) runs where the chain turns stereo.
62
+ "wobble",
63
+ "bloom",
64
+ "swell",
51
65
  "tremolo",
52
66
  "compressor",
53
67
  "pan",
68
+ "double",
54
69
  "phaser",
55
70
  "chorus",
56
71
  "leslie",
@@ -323,6 +338,300 @@ export const FX_SPECS = Object.freeze({
323
338
  },
324
339
  },
325
340
  },
341
+ stomp: {
342
+ label: "stomp box",
343
+ doc: "guitar pedal before the amp: fuzz (Big Muff), face (Fuzz Face), od (Tube Screamer), rat, octave (Octavia); oversampled",
344
+ simple: ["type", "gain", "tone", "level"],
345
+ strudel: "(no Strudel equivalent; dawg guitar rig)",
346
+ params: {
347
+ type: {
348
+ kind: "enum",
349
+ values: ["fuzz", "face", "od", "rat", "octave"],
350
+ default: "od",
351
+ doc: "circuit: fuzz (Big Muff), face (Fuzz Face), od (Tube Screamer), rat (RAT), octave (Octavia)",
352
+ },
353
+ gain: {
354
+ kind: "number",
355
+ min: 0,
356
+ max: 10,
357
+ default: 5,
358
+ step: 0.25,
359
+ automate: true,
360
+ doc: "the pedal's gain / sustain / distortion knob, 0..10",
361
+ },
362
+ tone: {
363
+ kind: "number",
364
+ min: 0,
365
+ max: 1,
366
+ default: 0.5,
367
+ step: 0.05,
368
+ automate: true,
369
+ doc: "dark 0 .. bright 1",
370
+ },
371
+ level: {
372
+ kind: "number",
373
+ min: -24,
374
+ max: 12,
375
+ default: 0,
376
+ step: 0.5,
377
+ unit: "dB",
378
+ doc: "trim over a level-matched pedal (0 = bypass loudness)",
379
+ },
380
+ octave: {
381
+ kind: "number",
382
+ min: 0,
383
+ max: 1,
384
+ default: 0.7,
385
+ step: 0.05,
386
+ doc: "octave: blend of the octave-up (rectified) path",
387
+ },
388
+ mix: mix(1),
389
+ },
390
+ },
391
+ head: {
392
+ label: "amp head",
393
+ doc: "guitar amp: preamp stages, Yeh-Smith tone stack, power amp with sag, level-matched across types; optional noise gate",
394
+ simple: ["type", "gain", "bass", "mid", "treble"],
395
+ strudel: "(no Strudel equivalent; Strudel amp is linear gain)",
396
+ params: {
397
+ type: {
398
+ kind: "enum",
399
+ values: ["clean", "chime", "crunch", "lead", "high", "solid", "bass"],
400
+ default: "crunch",
401
+ doc: "clean (blackface), chime (AC30), crunch (plexi), lead (JCM800), high (modern high gain), solid (JC-120), bass (SVT)",
402
+ },
403
+ gain: {
404
+ kind: "number",
405
+ min: 0,
406
+ max: 10,
407
+ default: 5,
408
+ step: 0.25,
409
+ automate: true,
410
+ doc: "preamp gain 0..10 (level-matched: more gain, not more volume)",
411
+ },
412
+ bass: {
413
+ kind: "number",
414
+ min: 0,
415
+ max: 10,
416
+ default: 5,
417
+ step: 0.5,
418
+ doc: "tone stack bass 0..10",
419
+ },
420
+ mid: {
421
+ kind: "number",
422
+ min: 0,
423
+ max: 10,
424
+ default: 5,
425
+ step: 0.5,
426
+ doc: "tone stack mid 0..10",
427
+ },
428
+ treble: {
429
+ kind: "number",
430
+ min: 0,
431
+ max: 10,
432
+ default: 5,
433
+ step: 0.5,
434
+ doc: "tone stack treble 0..10",
435
+ },
436
+ presence: {
437
+ kind: "number",
438
+ min: 0,
439
+ max: 10,
440
+ default: 5,
441
+ step: 0.5,
442
+ doc: "power amp presence 0..10 (5 flat)",
443
+ },
444
+ master: {
445
+ kind: "number",
446
+ min: 0,
447
+ max: 10,
448
+ default: 5,
449
+ step: 0.25,
450
+ automate: true,
451
+ doc: "power amp drive 0..10",
452
+ },
453
+ sag: {
454
+ kind: "number",
455
+ min: 0,
456
+ max: 1,
457
+ default: 0.3,
458
+ step: 0.05,
459
+ optional: true,
460
+ doc: "power supply sag 0..1 (absent: the head type's own)",
461
+ },
462
+ gate: {
463
+ kind: "number",
464
+ min: -96,
465
+ max: 0,
466
+ default: -60,
467
+ step: 1,
468
+ unit: "dB",
469
+ optional: true,
470
+ doc: "noise gate threshold before the amp (absent: no gate)",
471
+ },
472
+ level: {
473
+ kind: "number",
474
+ min: -24,
475
+ max: 12,
476
+ default: 0,
477
+ step: 0.5,
478
+ unit: "dB",
479
+ doc: "output level after level matching",
480
+ },
481
+ },
482
+ },
483
+ cab: {
484
+ label: "speaker cabinet",
485
+ doc: "speaker cabinet and microphone (biquad model, no impulse response)",
486
+ simple: ["type", "mic", "mix"],
487
+ strudel: "(no Strudel equivalent; dawg guitar rig)",
488
+ params: {
489
+ type: {
490
+ kind: "enum",
491
+ values: ["1x12", "2x12", "4x12", "1x10", "open", "8x10", "1x15", "di"],
492
+ default: "2x12",
493
+ doc: "1x12, 2x12, 4x12, 1x10, open back, 8x10 and 1x15 bass, di (no speaker)",
494
+ },
495
+ mic: {
496
+ kind: "number",
497
+ min: 0,
498
+ max: 1,
499
+ default: 0.3,
500
+ step: 0.05,
501
+ doc: "microphone position: 0 centre (bright) .. 1 edge (dark)",
502
+ },
503
+ mix: mix(1),
504
+ },
505
+ },
506
+ wobble: {
507
+ label: "wobble",
508
+ doc: "held tremolo arm: slow seeded pitch wow and flutter of the whole track",
509
+ simple: ["depth", "rate", "drift"],
510
+ strudel: "(no Strudel equivalent; dawg shoegaze)",
511
+ params: {
512
+ depth: {
513
+ kind: "number",
514
+ min: 0,
515
+ max: 100,
516
+ default: 20,
517
+ step: 1,
518
+ unit: "c",
519
+ automate: true,
520
+ doc: "peak pitch deviation in cents",
521
+ },
522
+ rate: {
523
+ kind: "number",
524
+ min: 0.05,
525
+ max: 8,
526
+ default: 0.5,
527
+ step: 0.05,
528
+ unit: "Hz",
529
+ doc: "wobble rate",
530
+ },
531
+ drift: {
532
+ kind: "number",
533
+ min: 0,
534
+ max: 1,
535
+ default: 0.3,
536
+ step: 0.05,
537
+ doc: "0 periodic .. 1 wandering (seeded, so every render matches)",
538
+ },
539
+ },
540
+ },
541
+ bloom: {
542
+ label: "bloom",
543
+ doc: "feedback bloom: the top held note grows a singing harmonic",
544
+ simple: ["amount", "harm", "delay"],
545
+ strudel: "(no Strudel equivalent; dawg shoegaze)",
546
+ params: {
547
+ amount: {
548
+ kind: "number",
549
+ min: 0,
550
+ max: 1,
551
+ default: 0.5,
552
+ step: 0.05,
553
+ doc: "level of the feedback partial",
554
+ },
555
+ harm: {
556
+ kind: "number",
557
+ min: 1,
558
+ max: 4,
559
+ default: 2,
560
+ step: 1,
561
+ doc: "harmonic that feeds back: 1 the note, 2 its octave, 3 the fifth above",
562
+ },
563
+ delay: {
564
+ kind: "number",
565
+ min: 0,
566
+ max: 4,
567
+ default: 0.6,
568
+ step: 0.05,
569
+ unit: "s",
570
+ doc: "how long a note is held before it starts to feed back",
571
+ },
572
+ time: {
573
+ kind: "number",
574
+ min: 0.05,
575
+ max: 4,
576
+ default: 1,
577
+ step: 0.05,
578
+ unit: "s",
579
+ doc: "how fast the feedback grows",
580
+ },
581
+ },
582
+ },
583
+ swell: {
584
+ label: "swell",
585
+ doc: "volume swell: each strum fades in, no pick attack",
586
+ simple: ["time", "mix"],
587
+ strudel: "(no Strudel equivalent; dawg shoegaze)",
588
+ params: {
589
+ time: {
590
+ kind: "number",
591
+ min: 0.01,
592
+ max: 4,
593
+ default: 0.4,
594
+ step: 0.01,
595
+ unit: "s",
596
+ doc: "rise time after each onset",
597
+ },
598
+ mix: mix(1),
599
+ },
600
+ },
601
+ double: {
602
+ label: "double",
603
+ doc: "automatic double tracking: a seeded second take, spread left and right",
604
+ simple: ["time", "drift", "width"],
605
+ strudel: "(no Strudel equivalent; dawg shoegaze)",
606
+ params: {
607
+ time: {
608
+ kind: "number",
609
+ min: 5,
610
+ max: 60,
611
+ default: 22,
612
+ step: 1,
613
+ unit: "ms",
614
+ doc: "how late the second take plays",
615
+ },
616
+ drift: {
617
+ kind: "number",
618
+ min: 0,
619
+ max: 10,
620
+ default: 3,
621
+ step: 0.5,
622
+ unit: "ms",
623
+ doc: "how far the second take's timing wanders",
624
+ },
625
+ width: {
626
+ kind: "number",
627
+ min: 0,
628
+ max: 1,
629
+ default: 0.6,
630
+ step: 0.05,
631
+ doc: "0 centred .. 1 the two takes hard left and right",
632
+ },
633
+ },
634
+ },
326
635
  tremolo: {
327
636
  label: "tremolo",
328
637
  doc: "volume modulation",
@@ -834,6 +1143,14 @@ export const CORE_EFFECTS: readonly EffectName[] = Object.freeze([
834
1143
  "reverb",
835
1144
  ] as const);
836
1145
 
1146
+ /** The 0.6.1 shoegaze stages (Effects > Shoegaze), in chain order. */
1147
+ export const SHOEGAZE_EFFECTS: readonly EffectName[] = Object.freeze([
1148
+ "wobble",
1149
+ "bloom",
1150
+ "swell",
1151
+ "double",
1152
+ ] as const);
1153
+
837
1154
  export function isEffectName(value: unknown): value is EffectName {
838
1155
  return (
839
1156
  typeof value === "string" &&
@@ -903,12 +1220,21 @@ export function normalizeFx(input: unknown): TrackFx | undefined {
903
1220
  }
904
1221
 
905
1222
  /** Automation lane name of an `fx` parameter, e.g. `distort-drive`. */
906
- export type FxLane = `${FxName | "reverb" | "synth"}-${string}`;
1223
+ export type FxLane =
1224
+ `${FxName | "reverb" | "synth" | "string" | "keys" | "modal" | "grain" | "wind"}-${string}`;
907
1225
 
908
1226
  /** Every automatable `fx` parameter as `{ lane, effect, param, spec }`. */
909
1227
  export const FX_LANES: readonly Readonly<{
910
1228
  lane: FxLane;
911
- effect: FxName | "reverb" | "synth";
1229
+ effect:
1230
+ | FxName
1231
+ | "reverb"
1232
+ | "synth"
1233
+ | "string"
1234
+ | "keys"
1235
+ | "modal"
1236
+ | "grain"
1237
+ | "wind";
912
1238
  param: string;
913
1239
  spec: NumberParam;
914
1240
  }>[] = Object.freeze([
@@ -936,6 +1262,53 @@ export const FX_LANES: readonly Readonly<{
936
1262
  spec,
937
1263
  }),
938
1264
  ),
1265
+ // String engine parameters (core/strings.ts), read at each note's onset.
1266
+ ...STRING_LANE_PARAMS.map(({ param, spec }) =>
1267
+ Object.freeze({
1268
+ lane: `string-${param}` as FxLane,
1269
+ effect: "string" as const,
1270
+ param,
1271
+ spec,
1272
+ }),
1273
+ ),
1274
+ // Modelled piano parameters (core/keys.ts), read at each note's onset.
1275
+ ...KEYS_LANE_PARAMS.map(({ param, spec }) =>
1276
+ Object.freeze({
1277
+ lane: `keys-${param}` as FxLane,
1278
+ effect: "keys" as const,
1279
+ param,
1280
+ spec,
1281
+ }),
1282
+ ),
1283
+ // Modal parameters (core/resonators.ts), read at each note's onset
1284
+ // (`modal-motordepth` every 32-sample control tick).
1285
+ ...MODAL_LANE_PARAMS.map(({ param, spec }) =>
1286
+ Object.freeze({
1287
+ lane: `modal-${param}` as FxLane,
1288
+ effect: "modal" as const,
1289
+ param,
1290
+ spec,
1291
+ }),
1292
+ ),
1293
+ // Granular parameters (core/granular.ts, 0.6.1), read every 32 frames;
1294
+ // grain-scoped ones are latched at each grain's onset.
1295
+ ...GRANULAR_LANE_PARAMS.map(({ param, spec }) =>
1296
+ Object.freeze({
1297
+ lane: `grain-${param}` as FxLane,
1298
+ effect: "grain" as const,
1299
+ param,
1300
+ spec,
1301
+ }),
1302
+ ),
1303
+ // Wind parameters (core/winds.ts), read every 32-sample control tick.
1304
+ ...WIND_LANE_PARAMS.map(({ param, spec }) =>
1305
+ Object.freeze({
1306
+ lane: `wind-${param}` as FxLane,
1307
+ effect: "wind" as const,
1308
+ param,
1309
+ spec,
1310
+ }),
1311
+ ),
939
1312
  ]);
940
1313
 
941
1314
  /**
@@ -973,6 +1346,14 @@ export const FX_PRESETS: Readonly<
973
1346
  cutoff: 400,
974
1347
  resonance: 0.4,
975
1348
  },
1349
+ wah: {
1350
+ type: "bpf",
1351
+ sync: 0.5,
1352
+ depth: 2,
1353
+ shape: "sine",
1354
+ cutoff: 700,
1355
+ resonance: 0.6,
1356
+ },
976
1357
  },
977
1358
  vowel: { a: { vowel: "a" }, o: { vowel: "o" }, ee: { vowel: "i" } },
978
1359
  crush: {
@@ -987,6 +1368,50 @@ export const FX_PRESETS: Readonly<
987
1368
  fold: { drive: 5, type: "fold", tone: 7000, mix: 0.7 },
988
1369
  shape: { drive: 5, type: "shape", tone: 9000 },
989
1370
  },
1371
+ stomp: {
1372
+ muff: { type: "fuzz", gain: 7, tone: 0.5 },
1373
+ face: { type: "face", gain: 6, tone: 0.5 },
1374
+ screamer: { type: "od", gain: 5, tone: 0.5, level: 3 },
1375
+ rat: { type: "rat", gain: 6, tone: 0.4 },
1376
+ octavia: { type: "octave", gain: 6, tone: 0.6, octave: 0.8 },
1377
+ boost: { type: "od", gain: 0, tone: 0.6, level: 6 },
1378
+ },
1379
+ head: {
1380
+ blackface: { type: "clean", gain: 3, treble: 6 },
1381
+ ac: { type: "chime", gain: 4, treble: 6 },
1382
+ plexi: { type: "crunch", gain: 5 },
1383
+ lead: { type: "lead", gain: 6, mid: 6 },
1384
+ modern: { type: "high", gain: 7, bass: 6, mid: 3, treble: 7, gate: -55 },
1385
+ jc: { type: "solid", gain: 2, treble: 6 },
1386
+ svt: { type: "bass", gain: 4, bass: 6 },
1387
+ },
1388
+ cab: {
1389
+ "1x12": { type: "1x12" },
1390
+ "2x12": { type: "2x12" },
1391
+ "4x12": { type: "4x12" },
1392
+ open: { type: "open" },
1393
+ "8x10": { type: "8x10" },
1394
+ "1x15": { type: "1x15" },
1395
+ di: { type: "di" },
1396
+ },
1397
+ wobble: {
1398
+ glide: { depth: 35, rate: 0.4, drift: 0.4 },
1399
+ tape: { depth: 12, rate: 0.8, drift: 0.6 },
1400
+ seasick: { depth: 60, rate: 0.25, drift: 0.2 },
1401
+ },
1402
+ bloom: {
1403
+ octave: { amount: 0.5, harm: 2, delay: 0.6, time: 1 },
1404
+ fifth: { amount: 0.4, harm: 3, delay: 0.8, time: 1.2 },
1405
+ ebow: { amount: 0.8, harm: 1, delay: 0.1, time: 0.6 },
1406
+ },
1407
+ swell: {
1408
+ slow: { time: 0.8 },
1409
+ violin: { time: 0.3 },
1410
+ },
1411
+ double: {
1412
+ tight: { time: 12, drift: 1.5, width: 0.5 },
1413
+ wide: { time: 28, drift: 3, width: 0.7 },
1414
+ },
990
1415
  tremolo: {
991
1416
  gentle: { sync: 1, depth: 0.3, shape: "sine" },
992
1417
  "eighth-chop": { sync: 0.5, depth: 0.9, shape: "square" },
@@ -1060,6 +1485,268 @@ export const FX_PRESETS: Readonly<
1060
1485
  },
1061
1486
  } satisfies Partial<Record<EffectName, Record<string, FxValues>>>);
1062
1487
 
1488
+ /** The guitar rig stages, in chain order: what `rig reset` clears. */
1489
+ export const RIG_STAGES = Object.freeze(["stomp", "head", "cab"] as const);
1490
+ export type RigStage = (typeof RIG_STAGES)[number];
1491
+
1492
+ /**
1493
+ * Whole guitar rigs (`rig <name>`): each sets the rig stages it names over
1494
+ * their defaults and clears the ones it leaves out. A few also set one
1495
+ * other effect (a wah, a spring-like reverb, a chorus) the sound needs.
1496
+ */
1497
+ export const RIG_PRESETS: Readonly<
1498
+ Record<string, Readonly<Partial<Record<FxName | "reverb", FxValues>>>>
1499
+ > = Object.freeze({
1500
+ clean: {
1501
+ head: { type: "clean", gain: 3, treble: 6 },
1502
+ cab: { type: "1x12" },
1503
+ },
1504
+ crunch: { head: { type: "crunch", gain: 5 }, cab: { type: "4x12" } },
1505
+ punk: {
1506
+ head: { type: "crunch", gain: 7, mid: 6, master: 6 },
1507
+ cab: { type: "4x12", mic: 0.2 },
1508
+ },
1509
+ ragged: {
1510
+ stomp: { type: "face", gain: 6, tone: 0.6 },
1511
+ head: { type: "chime", gain: 4 },
1512
+ cab: { type: "2x12" },
1513
+ },
1514
+ lead: {
1515
+ stomp: { type: "od", gain: 3, tone: 0.5, level: 3 },
1516
+ head: { type: "lead", gain: 6, mid: 6 },
1517
+ cab: { type: "4x12" },
1518
+ },
1519
+ metal: {
1520
+ stomp: { type: "od", gain: 0, tone: 0.6, level: 6 },
1521
+ head: { type: "high", gain: 7, bass: 6, mid: 3, treble: 7, gate: -55 },
1522
+ cab: { type: "4x12", mic: 0.2 },
1523
+ },
1524
+ fuzz: {
1525
+ stomp: { type: "fuzz", gain: 7, tone: 0.5 },
1526
+ head: { type: "clean", gain: 4 },
1527
+ cab: { type: "2x12" },
1528
+ },
1529
+ octave: {
1530
+ stomp: { type: "octave", gain: 6, tone: 0.6, octave: 0.8 },
1531
+ head: { type: "clean", gain: 3 },
1532
+ cab: { type: "1x12" },
1533
+ },
1534
+ funk: {
1535
+ head: { type: "clean", gain: 2, treble: 7, presence: 6 },
1536
+ cab: { type: "2x12" },
1537
+ autofilter: {
1538
+ type: "bpf",
1539
+ sync: 0,
1540
+ rate: 0.01,
1541
+ depth: 0,
1542
+ follow: 3,
1543
+ cutoff: 500,
1544
+ resonance: 0.6,
1545
+ },
1546
+ },
1547
+ wah: {
1548
+ head: { type: "crunch", gain: 4 },
1549
+ cab: { type: "2x12" },
1550
+ autofilter: {
1551
+ type: "bpf",
1552
+ sync: 0.5,
1553
+ depth: 2,
1554
+ shape: "sine",
1555
+ cutoff: 700,
1556
+ resonance: 0.6,
1557
+ },
1558
+ },
1559
+ bachata: {
1560
+ head: { type: "clean", gain: 2, mid: 6, treble: 7 },
1561
+ cab: { type: "1x12", mic: 0.2 },
1562
+ chorus: { rate: 0.8, depth: 0.25, mix: 0.3 },
1563
+ },
1564
+ spring: {
1565
+ head: { type: "clean", gain: 3, treble: 6 },
1566
+ cab: { type: "open" },
1567
+ reverb: { mix: 0.3, size: 0.35, fade: 1.5, predelay: 0, dim: 3500 },
1568
+ },
1569
+ bassdrive: {
1570
+ stomp: { type: "od", gain: 4, tone: 0.5, mix: 0.6 },
1571
+ head: { type: "bass", gain: 4 },
1572
+ cab: { type: "8x10" },
1573
+ },
1574
+ reese: {
1575
+ stomp: { type: "rat", gain: 3, tone: 0.3, mix: 0.5 },
1576
+ head: { type: "bass", gain: 6, master: 6 },
1577
+ cab: { type: "1x15" },
1578
+ },
1579
+ jangle: {
1580
+ head: { type: "chime", gain: 3, treble: 7 },
1581
+ cab: { type: "2x12", mic: 0.2 },
1582
+ compressor: { threshold: -20, ratio: 4, attack: 0.01, release: 0.15 },
1583
+ // 0.6.1: the double-tracked jangle of a twelve-string record. A stored
1584
+ // 0.6.0 jangle (no double) keeps rendering as it was; `rig jangle` again
1585
+ // adds it. The SDK's rig("jangle") keeps its 0.6.0 stages (see
1586
+ // RIG_ADDED), so the printer writes this double out explicitly.
1587
+ double: { time: 12, drift: 1.5, width: 0.5 },
1588
+ },
1589
+ alt: {
1590
+ stomp: { type: "rat", gain: 6, tone: 0.4 },
1591
+ head: { type: "crunch", gain: 4 },
1592
+ cab: { type: "4x12" },
1593
+ },
1594
+ // 0.6.1 shoegaze rigs (src/audio/effects/gaze.ts): fuzz into a held
1595
+ // tremolo arm, a feedback bloom, doubled takes and a long wash.
1596
+ shoegaze: {
1597
+ stomp: { type: "fuzz", gain: 7, tone: 0.45 },
1598
+ head: { type: "chime", gain: 4 },
1599
+ cab: { type: "2x12" },
1600
+ wobble: { depth: 25, rate: 0.4, drift: 0.4 },
1601
+ bloom: { amount: 0.4, harm: 2, delay: 0.6, time: 1 },
1602
+ double: { time: 18, drift: 2.5, width: 0.4 },
1603
+ postgain: { gain: 0.86 },
1604
+ reverb: { mix: 0.35, size: 0.9, fade: 4, predelay: 0.02, dim: 6000 },
1605
+ },
1606
+ glide: {
1607
+ stomp: { type: "face", gain: 5, tone: 0.5 },
1608
+ head: { type: "clean", gain: 4, treble: 5 },
1609
+ cab: { type: "2x12" },
1610
+ wobble: { depth: 35, rate: 0.4, drift: 0.4 },
1611
+ postgain: { gain: 0.8 },
1612
+ reverb: { mix: 0.35, size: 0.7, fade: 2.5, predelay: 0.01, dim: 6000 },
1613
+ },
1614
+ dreampop: {
1615
+ head: { type: "clean", gain: 2, treble: 6 },
1616
+ cab: { type: "1x12" },
1617
+ chorus: { rate: 0.6, depth: 0.3, mix: 0.35 },
1618
+ double: { time: 14, drift: 2, width: 0.4 },
1619
+ postgain: { gain: 1.45 },
1620
+ reverb: { mix: 0.4, size: 0.85, fade: 5, predelay: 0.03, dim: 7000 },
1621
+ },
1622
+ swell: {
1623
+ head: { type: "clean", gain: 3, treble: 5 },
1624
+ cab: { type: "1x12" },
1625
+ swell: { time: 0.5 },
1626
+ postgain: { gain: 1.25 },
1627
+ reverb: { mix: 0.45, size: 0.8, fade: 4, predelay: 0.02, dim: 6500 },
1628
+ },
1629
+ ebow: {
1630
+ stomp: { type: "od", gain: 3, tone: 0.5, level: 3 },
1631
+ head: { type: "lead", gain: 5, mid: 6 },
1632
+ cab: { type: "4x12" },
1633
+ swell: { time: 0.25 },
1634
+ bloom: { amount: 0.8, harm: 1, delay: 0.1, time: 0.6 },
1635
+ },
1636
+ });
1637
+
1638
+ /**
1639
+ * Effects a rig preset gained after SDK `rig(name)` was fixed. `rig(name)`
1640
+ * leaves them out, so an older song.ts spreading `...rig("jangle")` keeps
1641
+ * its sound; the printer writes them next to the spread, and a stored
1642
+ * track with or without them still reads as that rig.
1643
+ */
1644
+ export const RIG_ADDED: Readonly<Record<string, readonly FxName[]>> =
1645
+ Object.freeze({ jangle: Object.freeze(["double"] as FxName[]) });
1646
+
1647
+ export function isRigPreset(name: string): boolean {
1648
+ return Object.prototype.hasOwnProperty.call(RIG_PRESETS, name);
1649
+ }
1650
+
1651
+ /**
1652
+ * `fx` with rig preset `name` applied: rig stages it leaves out are
1653
+ * removed, every stage it names is filled over its defaults, and other
1654
+ * effects keep their values. `"reset"` removes the rig. A preset's track
1655
+ * effect (the `spring` reverb) is not part of `fx`: see `rigReverb`.
1656
+ */
1657
+ export function applyRigPreset(
1658
+ fx: TrackFx | undefined,
1659
+ name: string,
1660
+ ): TrackFx | undefined {
1661
+ const out: Record<string, unknown> = { ...(fx ?? {}) };
1662
+ for (const stage of RIG_STAGES) delete out[stage];
1663
+ // A previous rig's companion (funk's envelope filter, jangle's
1664
+ // compressor…) goes with it, unless the user has changed it since.
1665
+ for (const [effect, values] of Object.entries(out))
1666
+ if (isRigCompanion(effect, values)) delete out[effect];
1667
+ if (name !== "reset") {
1668
+ if (!isRigPreset(name))
1669
+ throw new FxValidationError(
1670
+ `unknown rig "${name}" (rigs: ${Object.keys(RIG_PRESETS).join(", ")})`,
1671
+ );
1672
+ for (const [effect, values] of Object.entries(RIG_PRESETS[name]!))
1673
+ if (isFxName(effect)) out[effect] = values;
1674
+ }
1675
+ return normalizeFx(out);
1676
+ }
1677
+
1678
+ /** Normalized companion effects (not stages, not reverb) of every rig. */
1679
+ let companions: Map<string, string[]> | undefined;
1680
+
1681
+ /** True when `values` is exactly what some rig preset writes for `effect`. */
1682
+ function isRigCompanion(effect: string, values: unknown): boolean {
1683
+ if ((RIG_STAGES as readonly string[]).includes(effect)) return false;
1684
+ if (!companions) {
1685
+ companions = new Map();
1686
+ for (const preset of Object.values(RIG_PRESETS))
1687
+ for (const [name, raw] of Object.entries(preset)) {
1688
+ if (!isFxName(name) || (RIG_STAGES as readonly string[]).includes(name))
1689
+ continue;
1690
+ const normal = normalizeFx({ [name]: raw })?.[name];
1691
+ const list = companions.get(name) ?? [];
1692
+ list.push(JSON.stringify(normal));
1693
+ companions.set(name, list);
1694
+ }
1695
+ }
1696
+ return companions.get(effect)?.includes(JSON.stringify(values)) ?? false;
1697
+ }
1698
+
1699
+ /** True when `reverb` is exactly the track reverb some rig preset sets. */
1700
+ export function isRigReverb(reverb: unknown): boolean {
1701
+ if (reverb === undefined) return false;
1702
+ const json = JSON.stringify(reverb);
1703
+ return Object.keys(RIG_PRESETS).some(
1704
+ (name) => rigReverb(name) && JSON.stringify(rigReverb(name)) === json,
1705
+ );
1706
+ }
1707
+
1708
+ /** The track reverb a rig preset sets (spring), if any. */
1709
+ export function rigReverb(name: string): FxValues | undefined {
1710
+ if (!isRigPreset(name)) return undefined;
1711
+ const values = (RIG_PRESETS[name] as Record<string, FxValues | undefined>)
1712
+ .reverb;
1713
+ return values === undefined
1714
+ ? undefined
1715
+ : normalizeParams(TRACK_EFFECT_SPECS.reverb.params, values, "reverb");
1716
+ }
1717
+
1718
+ /**
1719
+ * The rig preset whose stages and companion effects `fx` matches exactly,
1720
+ * if any (other effects may be present too).
1721
+ */
1722
+ export function rigPresetOf(fx: TrackFx | undefined): string | undefined {
1723
+ if (!fx || !RIG_STAGES.some((stage) => fx[stage])) return undefined;
1724
+ for (const name of Object.keys(RIG_PRESETS)) {
1725
+ const want: Partial<Record<FxName, FxValues>> = {
1726
+ ...(applyRigPreset(undefined, name) ?? {}),
1727
+ };
1728
+ for (const added of RIG_ADDED[name] ?? []) delete want[added];
1729
+ const keys = new Set<string>([...RIG_STAGES, ...Object.keys(want)]);
1730
+ if (
1731
+ [...keys].every(
1732
+ (key) =>
1733
+ JSON.stringify(fx[key as FxName]) ===
1734
+ JSON.stringify(want[key as FxName]),
1735
+ )
1736
+ )
1737
+ return name;
1738
+ }
1739
+ return undefined;
1740
+ }
1741
+
1742
+ /** The effects of `fx` a printed `...rig(name)` stands for. */
1743
+ export function rigPresetKeys(name: string): readonly FxName[] {
1744
+ const added = RIG_ADDED[name] ?? [];
1745
+ return Object.keys(RIG_PRESETS[name] ?? {}).filter(
1746
+ (key): key is FxName => isFxName(key) && !added.includes(key),
1747
+ );
1748
+ }
1749
+
1063
1750
  export function effectPresetNames(effect: EffectName): readonly string[] {
1064
1751
  return Object.keys(FX_PRESETS[effect] ?? {});
1065
1752
  }