@hraness/dawg 0.5.0 → 0.6.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 +29 -0
- package/DAWG.md +269 -13
- package/README.md +4 -4
- package/core/diff.ts +4 -0
- package/core/fx.ts +437 -2
- package/core/granular.ts +528 -0
- package/core/instruments.ts +281 -0
- package/core/keys.ts +386 -0
- package/core/resonators.ts +574 -0
- package/core/score.ts +181 -6
- package/core/sdk/eval-child.ts +2 -0
- package/core/sdk/print.ts +169 -13
- package/core/sdk/sync-instruments.ts +58 -0
- package/core/sdk/v1.ts +1114 -21
- package/core/sections.ts +34 -8
- package/core/strings.ts +845 -0
- package/guides/effects.md +3 -1
- package/guides/sounds.md +4 -0
- package/guides/tempo.md +1 -0
- package/package.json +1 -1
- package/src/agent/agent.ts +7 -0
- package/src/agent/granular-tools.ts +138 -0
- package/src/agent/models.ts +4 -4
- package/src/agent/ops.ts +39 -2
- package/src/agent/preview-tool.ts +6 -1
- package/src/agent/tools.ts +538 -14
- package/src/agent/xcb-agent.ts +7 -0
- package/src/audio/arrange.ts +21 -3
- package/src/audio/dsp/bank.ts +233 -0
- package/src/audio/dsp/fft.ts +6 -0
- package/src/audio/dsp/filters.ts +57 -0
- package/src/audio/dsp/interp.ts +112 -0
- package/src/audio/dsp/modal.ts +684 -0
- package/src/audio/dsp/onset.ts +197 -0
- package/src/audio/dsp/oversample.ts +202 -0
- package/src/audio/dsp/rng.ts +37 -0
- package/src/audio/dsp/shape.ts +81 -0
- package/src/audio/dsp/stft.ts +73 -0
- package/src/audio/dsp/window.ts +77 -0
- package/src/audio/effects/chain.ts +6 -1
- package/src/audio/effects/rig/cab.ts +99 -0
- package/src/audio/effects/rig/filters.ts +152 -0
- package/src/audio/effects/rig/gate.ts +39 -0
- package/src/audio/effects/rig/head.ts +487 -0
- package/src/audio/effects/rig/index.ts +170 -0
- package/src/audio/effects/rig/section.ts +78 -0
- package/src/audio/effects/rig/stomp.ts +238 -0
- package/src/audio/engine.ts +10 -1
- package/src/audio/fit.ts +402 -0
- package/src/audio/granular.ts +664 -0
- package/src/audio/instrument-check.ts +133 -0
- package/src/audio/instruments.ts +111 -0
- package/src/audio/keys/dsp.ts +323 -0
- package/src/audio/keys/engine.ts +361 -0
- package/src/audio/keys/piano.ts +432 -0
- package/src/audio/live-worker.ts +54 -0
- package/src/audio/live.ts +231 -12
- package/src/audio/loudness.ts +65 -20
- package/src/audio/preview.ts +2 -0
- package/src/audio/resonators.ts +287 -0
- package/src/audio/sampler.ts +163 -17
- package/src/audio/samples.ts +30 -7
- package/src/audio/strings/body.ts +250 -0
- package/src/audio/strings/engine.ts +369 -0
- package/src/audio/strings/loop.ts +119 -0
- package/src/audio/strings/measure.test-helpers.ts +198 -0
- package/src/audio/strings/pluck.ts +354 -0
- package/src/audio/warp.ts +61 -0
- package/src/audio/wav.ts +78 -6
- package/src/commands/expression.ts +9 -2
- package/src/commands/fit.ts +135 -0
- package/src/commands/fx.ts +31 -0
- package/src/commands/granular.ts +427 -0
- package/src/commands/help.ts +74 -3
- package/src/commands/keys.ts +353 -0
- package/src/commands/modal.ts +247 -0
- package/src/commands/rig.ts +260 -0
- package/src/commands/sample.ts +3 -0
- package/src/commands/string.ts +175 -0
- package/src/commands/time.ts +4 -1
- package/src/main.ts +210 -8
- package/src/project/check.ts +13 -0
- package/src/render.ts +33 -4
- package/src/tui/audition.ts +2 -2
- package/src/tui/granular-menu.ts +278 -0
- package/src/tui/menu.ts +332 -9
- package/src/tui/modal-menu.ts +118 -0
- package/src/tui/play-session.ts +45 -2
package/core/sdk/v1.ts
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
29
|
/** SDK release; dawg refreshes the vendored copy when its own is newer. */
|
|
30
|
-
export const SDK_VERSION = "1.
|
|
30
|
+
export const SDK_VERSION = "1.25.0";
|
|
31
31
|
/** Major of `SDK_VERSION`; `dawg.json` records it as `sdk`. */
|
|
32
32
|
export const SDK_MAJOR = 1;
|
|
33
33
|
|
|
@@ -1277,6 +1277,12 @@ export type SampleSpec = Readonly<{
|
|
|
1277
1277
|
accelerate?: number;
|
|
1278
1278
|
/** Like Tidal `squiz`: pitch-raise ratio per zero-crossing cycle (1..32). */
|
|
1279
1279
|
squiz?: number;
|
|
1280
|
+
/** The file's own tempo (20..400, SDK 1.20.0): the window follows the song's tempo map. */
|
|
1281
|
+
bpm?: number;
|
|
1282
|
+
/** How a fitted window changes time (SDK 1.20.0): `"repitch"` (tape), `"beats"` (onset slices), `"tones"` (keeps pitch). */
|
|
1283
|
+
fitmode?: "repitch" | "beats" | "tones";
|
|
1284
|
+
/** Window length in beats (SDK 1.20.0); `fit` wins over `bpm`, `bpm` over `len`. */
|
|
1285
|
+
len?: number;
|
|
1280
1286
|
}>;
|
|
1281
1287
|
|
|
1282
1288
|
/** Result of `sampler()`; pass it as a track's `instrument`. */
|
|
@@ -1317,7 +1323,7 @@ export function sampler(
|
|
|
1317
1323
|
throw new DawgSdkError(
|
|
1318
1324
|
`sampler voice "${name}" must be a short identifier`,
|
|
1319
1325
|
);
|
|
1320
|
-
out[name] =
|
|
1326
|
+
out[name] = sampleSpec(voices[name]!, name);
|
|
1321
1327
|
}
|
|
1322
1328
|
return Object.freeze({ kind: "sampler", voices: Object.freeze(out), mode });
|
|
1323
1329
|
}
|
|
@@ -1433,6 +1439,314 @@ export function wavetable(
|
|
|
1433
1439
|
return Object.freeze(out) as WavetableSpec;
|
|
1434
1440
|
}
|
|
1435
1441
|
|
|
1442
|
+
/** Instrument name of the 0.6 string engine (`Track.string`). */
|
|
1443
|
+
export const STRING_INSTRUMENT = "string";
|
|
1444
|
+
|
|
1445
|
+
/**
|
|
1446
|
+
* String engine settings (SDK 1.21.0): a `preset` (`nylon`, `steel`,
|
|
1447
|
+
* `electric`, `jangle`, `ebass`, `slap`, `upright`, `sitar`, `tanpura`,
|
|
1448
|
+
* `harpsichord`, `lute`, `oud`, `setar`, `tar`, `santur`, `dulcimer`, `koto`,
|
|
1449
|
+
* `harp`, `banjo`, `tres`, `requinto`) plus any parameter to override
|
|
1450
|
+
* (`ring`, `bright`, `damp`, `pos`, `mute`, `buzz`, `body`, `sym`, ...).
|
|
1451
|
+
* dawg validates names and ranges; see **Strings** in DAWG.md.
|
|
1452
|
+
*/
|
|
1453
|
+
export type StringInput = Readonly<
|
|
1454
|
+
{ preset?: string } & Record<string, number | string | undefined>
|
|
1455
|
+
>;
|
|
1456
|
+
|
|
1457
|
+
/** Result of `stringed()`; pass it as a track's `instrument`. */
|
|
1458
|
+
export type StringSpec = Readonly<{ kind: "string" } & StringInput>;
|
|
1459
|
+
|
|
1460
|
+
/**
|
|
1461
|
+
* A plucked string instrument (SDK 1.21.0): a preset and overrides.
|
|
1462
|
+
*
|
|
1463
|
+
* instrument: stringed("nylon")
|
|
1464
|
+
* instrument: stringed("sitar", { buzz: 0.8, sym: 0.5 })
|
|
1465
|
+
*/
|
|
1466
|
+
export function stringed(
|
|
1467
|
+
preset = "nylon",
|
|
1468
|
+
params: Readonly<Record<string, number | string>> = {},
|
|
1469
|
+
): StringSpec {
|
|
1470
|
+
if (typeof preset !== "string" || preset.length === 0)
|
|
1471
|
+
throw new DawgSdkError("stringed needs a preset name");
|
|
1472
|
+
if (!isRecord(params))
|
|
1473
|
+
throw new DawgSdkError("stringed params must be an object");
|
|
1474
|
+
const out: Record<string, number | string> = { kind: "string", preset };
|
|
1475
|
+
for (const [key, value] of Object.entries(params)) {
|
|
1476
|
+
if (value === undefined || key === "kind" || key === "preset") continue;
|
|
1477
|
+
out[key] =
|
|
1478
|
+
typeof value === "string" ? value : finite(value, `string ${key}`);
|
|
1479
|
+
}
|
|
1480
|
+
return Object.freeze(out) as StringSpec;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
/** Instrument name of the 0.6 granular engine (`Track.granular`). */
|
|
1484
|
+
export const GRANULAR_INSTRUMENT = "granular";
|
|
1485
|
+
|
|
1486
|
+
/**
|
|
1487
|
+
* Granular settings (SDK 1.23.0). `src` is a sample (`sample(...)` shape,
|
|
1488
|
+
* pinned like a sampler voice) or a built-in synth render
|
|
1489
|
+
* `"synth:<preset>[@note]"` (default `synth:pad`, nothing to download);
|
|
1490
|
+
* `preset` is `cloud`, `hold`, `sparkle`, `swarm`, `stutter`, `microloop`,
|
|
1491
|
+
* `backwards` or `dust`; every other key overrides one parameter (`grain`
|
|
1492
|
+
* seconds, `overlap`, `scan`, `pos`, `begin`, `end`, `spray`, `jitter`,
|
|
1493
|
+
* `pitch`, `detune`, `shimmer`, `shimint`, `spread`, `window`, `reverse`,
|
|
1494
|
+
* `freeze`, `repeat`, `hold`, `drift`, `drate`, `attack`, `release`,
|
|
1495
|
+
* `veltone`, `gain`, `seed`, `root`). dawg validates names and ranges; see
|
|
1496
|
+
* **Granular** in DAWG.md.
|
|
1497
|
+
*/
|
|
1498
|
+
export type GranularInput = Readonly<
|
|
1499
|
+
{
|
|
1500
|
+
src?: string | SampleSpec;
|
|
1501
|
+
preset?: string;
|
|
1502
|
+
} & Record<string, number | string | boolean | SampleSpec | undefined>
|
|
1503
|
+
>;
|
|
1504
|
+
|
|
1505
|
+
/** Result of `granular()`; pass it as a track's `instrument`. */
|
|
1506
|
+
export type GranularSpec = Readonly<{ kind: "granular" } & GranularInput>;
|
|
1507
|
+
|
|
1508
|
+
/**
|
|
1509
|
+
* A granular instrument (SDK 1.23.0): an optional preset, then overrides.
|
|
1510
|
+
*
|
|
1511
|
+
* ```ts
|
|
1512
|
+
* instrument: granular("cloud")
|
|
1513
|
+
* instrument: granular("hold", { src: "samples/choir.wav", scan: 0 })
|
|
1514
|
+
* instrument: granular({ src: "synth:bell@72", grain: 0.08, overlap: 6 })
|
|
1515
|
+
* ```
|
|
1516
|
+
*/
|
|
1517
|
+
export function granular(
|
|
1518
|
+
preset?: string | GranularInput,
|
|
1519
|
+
params: GranularInput = {},
|
|
1520
|
+
): GranularSpec {
|
|
1521
|
+
const fields =
|
|
1522
|
+
typeof preset === "object" && preset !== null ? preset : params;
|
|
1523
|
+
if (!isRecord(fields))
|
|
1524
|
+
throw new DawgSdkError("granular params must be an object");
|
|
1525
|
+
const out: Record<string, unknown> = { kind: "granular" };
|
|
1526
|
+
if (typeof preset === "string") {
|
|
1527
|
+
if (preset.length === 0)
|
|
1528
|
+
throw new DawgSdkError("granular needs a preset name");
|
|
1529
|
+
out.preset = preset;
|
|
1530
|
+
} else if (preset !== undefined && (typeof preset !== "object" || !preset))
|
|
1531
|
+
throw new DawgSdkError("granular takes a preset name or params");
|
|
1532
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
1533
|
+
if (value === undefined || key === "kind") continue;
|
|
1534
|
+
if (key === "src")
|
|
1535
|
+
out.src =
|
|
1536
|
+
typeof value === "string" && value.startsWith("synth:")
|
|
1537
|
+
? value
|
|
1538
|
+
: sampleSpec(value as string | SampleSpec, "granular src");
|
|
1539
|
+
else if (key === "preset" && typeof preset === "string") continue;
|
|
1540
|
+
else if (key === "root" && typeof value === "string")
|
|
1541
|
+
out.root = midi(value as Pitch);
|
|
1542
|
+
else if (
|
|
1543
|
+
typeof value === "number" ||
|
|
1544
|
+
typeof value === "string" ||
|
|
1545
|
+
typeof value === "boolean"
|
|
1546
|
+
)
|
|
1547
|
+
out[key] =
|
|
1548
|
+
typeof value === "number" ? finite(value, `granular ${key}`) : value;
|
|
1549
|
+
else throw new DawgSdkError(`granular ${key} must be a value`);
|
|
1550
|
+
}
|
|
1551
|
+
return Object.freeze(out) as GranularSpec;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
/** Instrument name that selects the modal mallet-and-bell engine (SDK 1.25.0). */
|
|
1555
|
+
export const MODAL_INSTRUMENT = "modal";
|
|
1556
|
+
|
|
1557
|
+
/** Modal presets (dawg's core/resonators.ts). */
|
|
1558
|
+
export type ModalPresetName =
|
|
1559
|
+
| "marimba"
|
|
1560
|
+
| "vibes"
|
|
1561
|
+
| "xylophone"
|
|
1562
|
+
| "glock"
|
|
1563
|
+
| "celesta"
|
|
1564
|
+
| "chimes"
|
|
1565
|
+
| "kalimba"
|
|
1566
|
+
| "mbira"
|
|
1567
|
+
| "steelpan"
|
|
1568
|
+
| "bowl"
|
|
1569
|
+
| "gong"
|
|
1570
|
+
| "timpani";
|
|
1571
|
+
|
|
1572
|
+
/** Modal overrides; omitted means the preset's value. dawg validates ranges. */
|
|
1573
|
+
export type ModalParams = Readonly<{
|
|
1574
|
+
/** `yarn` `cord` `rubber` `plastic` `brass`: sets hardness. */
|
|
1575
|
+
mallet?: "yarn" | "cord" | "rubber" | "plastic" | "brass";
|
|
1576
|
+
/** Mallet hardness 0..1: brighter, shorter contact. */
|
|
1577
|
+
hardness?: number;
|
|
1578
|
+
/** Strike position 0..1 (0.5 is the bar's centre). */
|
|
1579
|
+
position?: number;
|
|
1580
|
+
/** Fundamental ring time (T60 seconds). */
|
|
1581
|
+
ring?: number;
|
|
1582
|
+
/** How much faster upper modes die (octaves of decay per octave). */
|
|
1583
|
+
tilt?: number;
|
|
1584
|
+
/** Damping on note-off 0..1 (0 lets the bar ring). */
|
|
1585
|
+
damp?: number;
|
|
1586
|
+
/** Choke time after note-off, seconds. */
|
|
1587
|
+
release?: number;
|
|
1588
|
+
/** Vibraphone motor rate Hz and depth 0..1. */
|
|
1589
|
+
motor?: number;
|
|
1590
|
+
motordepth?: number;
|
|
1591
|
+
/** Beat between paired gamelan modes, Hz. */
|
|
1592
|
+
ombak?: number;
|
|
1593
|
+
/** Mbira buzz 0..1 and mallet click 0..1. */
|
|
1594
|
+
buzz?: number;
|
|
1595
|
+
click?: number;
|
|
1596
|
+
/** Strike pitch bend in semitones and its decay seconds (Strudel penv/pdecay). */
|
|
1597
|
+
strikebend?: number;
|
|
1598
|
+
strikedecay?: number;
|
|
1599
|
+
/** Output level 0..2 (1 is the preset level). */
|
|
1600
|
+
gain?: number;
|
|
1601
|
+
/** Mode table override (`marimba`, `bell`, `gong`, …). */
|
|
1602
|
+
body?: string;
|
|
1603
|
+
}>;
|
|
1604
|
+
|
|
1605
|
+
const MODAL_KEYS = Object.freeze([
|
|
1606
|
+
"mallet",
|
|
1607
|
+
"hardness",
|
|
1608
|
+
"position",
|
|
1609
|
+
"ring",
|
|
1610
|
+
"tilt",
|
|
1611
|
+
"damp",
|
|
1612
|
+
"release",
|
|
1613
|
+
"motor",
|
|
1614
|
+
"motordepth",
|
|
1615
|
+
"ombak",
|
|
1616
|
+
"buzz",
|
|
1617
|
+
"click",
|
|
1618
|
+
"strikebend",
|
|
1619
|
+
"strikedecay",
|
|
1620
|
+
"gain",
|
|
1621
|
+
"body",
|
|
1622
|
+
] as const);
|
|
1623
|
+
|
|
1624
|
+
/** Mallet words (core/resonators.ts MODAL_MALLETS). */
|
|
1625
|
+
const MODAL_MALLET_WORDS: readonly string[] = Object.freeze([
|
|
1626
|
+
"yarn",
|
|
1627
|
+
"cord",
|
|
1628
|
+
"rubber",
|
|
1629
|
+
"plastic",
|
|
1630
|
+
"brass",
|
|
1631
|
+
]);
|
|
1632
|
+
|
|
1633
|
+
/** Mode tables a `body` override may name (core/resonators.ts MODAL_BODIES). */
|
|
1634
|
+
const MODAL_BODY_WORDS: readonly string[] = Object.freeze([
|
|
1635
|
+
"marimba",
|
|
1636
|
+
"vibraphone",
|
|
1637
|
+
"xylophone",
|
|
1638
|
+
"glockenspiel",
|
|
1639
|
+
"celesta",
|
|
1640
|
+
"chimes",
|
|
1641
|
+
"crotale",
|
|
1642
|
+
"mbira",
|
|
1643
|
+
"kalimba",
|
|
1644
|
+
"musicbox",
|
|
1645
|
+
"toypiano",
|
|
1646
|
+
"saron",
|
|
1647
|
+
"bonang",
|
|
1648
|
+
"gender",
|
|
1649
|
+
"kempul",
|
|
1650
|
+
"gong",
|
|
1651
|
+
"bell",
|
|
1652
|
+
"steelpan",
|
|
1653
|
+
"bowl",
|
|
1654
|
+
"timpani",
|
|
1655
|
+
"tabla",
|
|
1656
|
+
"frame",
|
|
1657
|
+
]);
|
|
1658
|
+
|
|
1659
|
+
/** Numeric ranges (core/resonators.ts MODAL_PARAMS min..max). */
|
|
1660
|
+
const MODAL_RANGES: Readonly<Record<string, readonly [number, number]>> =
|
|
1661
|
+
Object.freeze({
|
|
1662
|
+
hardness: [0, 1],
|
|
1663
|
+
position: [0, 1],
|
|
1664
|
+
ring: [0.05, 30],
|
|
1665
|
+
tilt: [0, 2],
|
|
1666
|
+
damp: [0, 1],
|
|
1667
|
+
release: [0.005, 2],
|
|
1668
|
+
motor: [0, 12],
|
|
1669
|
+
motordepth: [0, 1],
|
|
1670
|
+
ombak: [0, 12],
|
|
1671
|
+
buzz: [0, 1],
|
|
1672
|
+
click: [0, 1],
|
|
1673
|
+
strikebend: [-24, 24],
|
|
1674
|
+
strikedecay: [0.001, 2],
|
|
1675
|
+
gain: [0, 2],
|
|
1676
|
+
});
|
|
1677
|
+
|
|
1678
|
+
const MODAL_PRESET_WORDS: readonly string[] = Object.freeze([
|
|
1679
|
+
"marimba",
|
|
1680
|
+
"vibes",
|
|
1681
|
+
"xylophone",
|
|
1682
|
+
"glock",
|
|
1683
|
+
"celesta",
|
|
1684
|
+
"chimes",
|
|
1685
|
+
"kalimba",
|
|
1686
|
+
"mbira",
|
|
1687
|
+
"steelpan",
|
|
1688
|
+
"bowl",
|
|
1689
|
+
"gong",
|
|
1690
|
+
"timpani",
|
|
1691
|
+
]);
|
|
1692
|
+
|
|
1693
|
+
/** Result of `modal()`; pass it as a track's `instrument`. */
|
|
1694
|
+
export type ModalSpec = Readonly<
|
|
1695
|
+
{ kind: "modal"; preset?: ModalPresetName } & ModalParams
|
|
1696
|
+
>;
|
|
1697
|
+
|
|
1698
|
+
/**
|
|
1699
|
+
* Mallets and bells on the modal engine (SDK 1.25.0): a preset and
|
|
1700
|
+
* optional overrides. A preset word alone (`instrument: "vibes"`) is the
|
|
1701
|
+
* same as `modal("vibes")`, except `"marimba"`, which stays the legacy
|
|
1702
|
+
* marimba voice; `modal("marimba")` is the modal one.
|
|
1703
|
+
*
|
|
1704
|
+
* ```ts
|
|
1705
|
+
* instrument: modal("vibes", { motor: 4, hardness: 0.6 })
|
|
1706
|
+
* instrument: modal("marimba", { mallet: "rubber" })
|
|
1707
|
+
* instrument: modal({ ring: 2 }) // default preset (marimba)
|
|
1708
|
+
* ```
|
|
1709
|
+
*/
|
|
1710
|
+
export function modal(
|
|
1711
|
+
preset?: ModalPresetName | ModalParams,
|
|
1712
|
+
params: ModalParams = {},
|
|
1713
|
+
): ModalSpec {
|
|
1714
|
+
const overrides = isRecord(preset) ? preset : params;
|
|
1715
|
+
const name = isRecord(preset) ? undefined : preset;
|
|
1716
|
+
if (!isRecord(overrides))
|
|
1717
|
+
throw new DawgSdkError("modal params must be an object");
|
|
1718
|
+
const out: Record<string, unknown> = { kind: "modal" };
|
|
1719
|
+
if (name !== undefined) {
|
|
1720
|
+
if (typeof name !== "string" || !MODAL_PRESET_WORDS.includes(name))
|
|
1721
|
+
throw new DawgSdkError(
|
|
1722
|
+
`modal preset "${String(name).slice(0, 32)}" is not one of ${MODAL_PRESET_WORDS.join(" ")}`,
|
|
1723
|
+
);
|
|
1724
|
+
out.preset = name;
|
|
1725
|
+
}
|
|
1726
|
+
for (const key of Object.keys(overrides)) {
|
|
1727
|
+
const value = (overrides as Record<string, unknown>)[key];
|
|
1728
|
+
if (value === undefined) continue;
|
|
1729
|
+
if (key === "mallet" || key === "body") {
|
|
1730
|
+
const words = key === "mallet" ? MODAL_MALLET_WORDS : MODAL_BODY_WORDS;
|
|
1731
|
+
if (typeof value !== "string" || !words.includes(value))
|
|
1732
|
+
throw new DawgSdkError(
|
|
1733
|
+
`modal ${key} "${String(value).slice(0, 32)}" is not one of ${words.join(" ")}`,
|
|
1734
|
+
);
|
|
1735
|
+
out[key] = value;
|
|
1736
|
+
} else if ((MODAL_KEYS as readonly string[]).includes(key)) {
|
|
1737
|
+
const number = finite(value, `modal ${key}`);
|
|
1738
|
+
const [min, max] = MODAL_RANGES[key]!;
|
|
1739
|
+
if (number < min || number > max)
|
|
1740
|
+
throw new DawgSdkError(`modal ${key} must be ${min}..${max}`);
|
|
1741
|
+
out[key] = number;
|
|
1742
|
+
} else
|
|
1743
|
+
throw new DawgSdkError(
|
|
1744
|
+
`modal has no parameter "${key.slice(0, 32)}" (${MODAL_KEYS.join(" ")})`,
|
|
1745
|
+
);
|
|
1746
|
+
}
|
|
1747
|
+
return Object.freeze(out) as ModalSpec;
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1436
1750
|
/**
|
|
1437
1751
|
* `count` equal slices of one file as voices `prefix0 … prefixN-1`, for
|
|
1438
1752
|
* chopped breaks: `sampler(slices("samples/break.wav", 8, "brk"))`, then
|
|
@@ -1445,7 +1759,7 @@ export function slices(
|
|
|
1445
1759
|
): Record<string, SampleSpec> {
|
|
1446
1760
|
if (!Number.isInteger(count) || count < 1 || count > 64)
|
|
1447
1761
|
throw new DawgSdkError("slices count must be an integer 1..64");
|
|
1448
|
-
const base =
|
|
1762
|
+
const base = sampleSpec(src, prefix);
|
|
1449
1763
|
const begin = base.begin ?? 0;
|
|
1450
1764
|
const end = base.end ?? 1;
|
|
1451
1765
|
const span = (end - begin) / count;
|
|
@@ -1460,7 +1774,7 @@ export function slices(
|
|
|
1460
1774
|
return voices;
|
|
1461
1775
|
}
|
|
1462
1776
|
|
|
1463
|
-
function
|
|
1777
|
+
function sampleSpec(value: string | SampleSpec, name: string): SampleSpec {
|
|
1464
1778
|
const spec = typeof value === "string" ? { src: value } : value;
|
|
1465
1779
|
if (!isRecord(spec) || typeof spec.src !== "string" || spec.src.length === 0)
|
|
1466
1780
|
throw new DawgSdkError(`sampler voice ${name} needs a src path`);
|
|
@@ -1483,6 +1797,9 @@ function sample(value: string | SampleSpec, name: string): SampleSpec {
|
|
|
1483
1797
|
fit?: boolean;
|
|
1484
1798
|
accelerate?: number;
|
|
1485
1799
|
squiz?: number;
|
|
1800
|
+
bpm?: number;
|
|
1801
|
+
fitmode?: "repitch" | "beats" | "tones";
|
|
1802
|
+
len?: number;
|
|
1486
1803
|
} = { src: spec.src };
|
|
1487
1804
|
if (spec.src.startsWith("pack:")) {
|
|
1488
1805
|
if (spec.sha256 !== undefined)
|
|
@@ -1532,9 +1849,36 @@ function sample(value: string | SampleSpec, name: string): SampleSpec {
|
|
|
1532
1849
|
if (spec.accelerate !== undefined)
|
|
1533
1850
|
out.accelerate = finite(spec.accelerate, `${name} accelerate`);
|
|
1534
1851
|
if (spec.squiz !== undefined) out.squiz = finite(spec.squiz, `${name} squiz`);
|
|
1852
|
+
if (spec.bpm !== undefined) out.bpm = finite(spec.bpm, `${name} bpm`);
|
|
1853
|
+
if (spec.fitmode !== undefined) {
|
|
1854
|
+
if (
|
|
1855
|
+
spec.fitmode !== "repitch" &&
|
|
1856
|
+
spec.fitmode !== "beats" &&
|
|
1857
|
+
spec.fitmode !== "tones"
|
|
1858
|
+
)
|
|
1859
|
+
throw new DawgSdkError(
|
|
1860
|
+
`${name} fitmode must be "repitch", "beats" or "tones"`,
|
|
1861
|
+
);
|
|
1862
|
+
out.fitmode = spec.fitmode;
|
|
1863
|
+
}
|
|
1864
|
+
if (spec.len !== undefined) out.len = finite(spec.len, `${name} len`);
|
|
1535
1865
|
return Object.freeze(out);
|
|
1536
1866
|
}
|
|
1537
1867
|
|
|
1868
|
+
/**
|
|
1869
|
+
* One sample file with options (SDK 1.20.0), for `sampler({ brk: ... })`:
|
|
1870
|
+
* `sample("samples/break.wav", { bpm: 174, fitmode: "beats" })` plays a
|
|
1871
|
+
* 174 BPM break in time with the song, cut at its hits.
|
|
1872
|
+
*/
|
|
1873
|
+
export function sample(
|
|
1874
|
+
src: string,
|
|
1875
|
+
options: Omit<SampleSpec, "src"> = {},
|
|
1876
|
+
): SampleSpec {
|
|
1877
|
+
if (typeof src !== "string" || src.length === 0)
|
|
1878
|
+
throw new DawgSdkError("sample() needs a src path");
|
|
1879
|
+
return Object.freeze({ ...options, src });
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1538
1882
|
// ---------------------------------------------------------------------------
|
|
1539
1883
|
// Tracks
|
|
1540
1884
|
|
|
@@ -1564,14 +1908,177 @@ export type AutomationInput = Readonly<{
|
|
|
1564
1908
|
wt?: readonly Point[];
|
|
1565
1909
|
}>;
|
|
1566
1910
|
|
|
1911
|
+
/**
|
|
1912
|
+
* Every rig preset (SDK 1.22.0), kept equal to `RIG_PRESETS` in
|
|
1913
|
+
* core/fx.ts by `print-rig.test.ts`: the stomp, head and cab stages plus
|
|
1914
|
+
* the companion effects a few rigs need (funk's and wah's autofilter,
|
|
1915
|
+
* bachata's chorus, jangle's compressor). `spring`'s short room is the
|
|
1916
|
+
* track's `reverb`, not `fx`: give it as
|
|
1917
|
+
* `reverb: { mix: 0.3, size: 0.35, fade: 1.5, predelay: 0, dim: 3500 }`.
|
|
1918
|
+
*/
|
|
1919
|
+
export const RIG_PRESETS: Readonly<
|
|
1920
|
+
Record<
|
|
1921
|
+
string,
|
|
1922
|
+
Readonly<
|
|
1923
|
+
Record<"stomp" | "head" | "cab", EffectParams | undefined> &
|
|
1924
|
+
Readonly<Record<string, EffectParams | undefined>>
|
|
1925
|
+
>
|
|
1926
|
+
>
|
|
1927
|
+
> = Object.freeze({
|
|
1928
|
+
clean: {
|
|
1929
|
+
stomp: undefined,
|
|
1930
|
+
head: { type: "clean", gain: 3, treble: 6 },
|
|
1931
|
+
cab: { type: "1x12" },
|
|
1932
|
+
},
|
|
1933
|
+
crunch: {
|
|
1934
|
+
stomp: undefined,
|
|
1935
|
+
head: { type: "crunch", gain: 5 },
|
|
1936
|
+
cab: { type: "4x12" },
|
|
1937
|
+
},
|
|
1938
|
+
punk: {
|
|
1939
|
+
stomp: undefined,
|
|
1940
|
+
head: { type: "crunch", gain: 7, mid: 6, master: 6 },
|
|
1941
|
+
cab: { type: "4x12", mic: 0.2 },
|
|
1942
|
+
},
|
|
1943
|
+
ragged: {
|
|
1944
|
+
stomp: { type: "face", gain: 6, tone: 0.6 },
|
|
1945
|
+
head: { type: "chime", gain: 4 },
|
|
1946
|
+
cab: { type: "2x12" },
|
|
1947
|
+
},
|
|
1948
|
+
lead: {
|
|
1949
|
+
stomp: { type: "od", gain: 3, tone: 0.5, level: 3 },
|
|
1950
|
+
head: { type: "lead", gain: 6, mid: 6 },
|
|
1951
|
+
cab: { type: "4x12" },
|
|
1952
|
+
},
|
|
1953
|
+
metal: {
|
|
1954
|
+
stomp: { type: "od", gain: 0, tone: 0.6, level: 6 },
|
|
1955
|
+
head: { type: "high", gain: 7, bass: 6, mid: 3, treble: 7, gate: -55 },
|
|
1956
|
+
cab: { type: "4x12", mic: 0.2 },
|
|
1957
|
+
},
|
|
1958
|
+
fuzz: {
|
|
1959
|
+
stomp: { type: "fuzz", gain: 7, tone: 0.5 },
|
|
1960
|
+
head: { type: "clean", gain: 4 },
|
|
1961
|
+
cab: { type: "2x12" },
|
|
1962
|
+
},
|
|
1963
|
+
octave: {
|
|
1964
|
+
stomp: { type: "octave", gain: 6, tone: 0.6, octave: 0.8 },
|
|
1965
|
+
head: { type: "clean", gain: 3 },
|
|
1966
|
+
cab: { type: "1x12" },
|
|
1967
|
+
},
|
|
1968
|
+
funk: {
|
|
1969
|
+
stomp: undefined,
|
|
1970
|
+
head: { type: "clean", gain: 2, treble: 7, presence: 6 },
|
|
1971
|
+
cab: { type: "2x12" },
|
|
1972
|
+
autofilter: {
|
|
1973
|
+
type: "bpf",
|
|
1974
|
+
sync: 0,
|
|
1975
|
+
rate: 0.01,
|
|
1976
|
+
depth: 0,
|
|
1977
|
+
follow: 3,
|
|
1978
|
+
cutoff: 500,
|
|
1979
|
+
resonance: 0.6,
|
|
1980
|
+
},
|
|
1981
|
+
},
|
|
1982
|
+
wah: {
|
|
1983
|
+
stomp: undefined,
|
|
1984
|
+
head: { type: "crunch", gain: 4 },
|
|
1985
|
+
cab: { type: "2x12" },
|
|
1986
|
+
autofilter: {
|
|
1987
|
+
type: "bpf",
|
|
1988
|
+
sync: 0.5,
|
|
1989
|
+
depth: 2,
|
|
1990
|
+
shape: "sine",
|
|
1991
|
+
cutoff: 700,
|
|
1992
|
+
resonance: 0.6,
|
|
1993
|
+
},
|
|
1994
|
+
},
|
|
1995
|
+
bachata: {
|
|
1996
|
+
stomp: undefined,
|
|
1997
|
+
head: { type: "clean", gain: 2, mid: 6, treble: 7 },
|
|
1998
|
+
cab: { type: "1x12", mic: 0.2 },
|
|
1999
|
+
chorus: { rate: 0.8, depth: 0.25, mix: 0.3 },
|
|
2000
|
+
},
|
|
2001
|
+
spring: {
|
|
2002
|
+
stomp: undefined,
|
|
2003
|
+
head: { type: "clean", gain: 3, treble: 6 },
|
|
2004
|
+
cab: { type: "open" },
|
|
2005
|
+
},
|
|
2006
|
+
bassdrive: {
|
|
2007
|
+
stomp: { type: "od", gain: 4, tone: 0.5, mix: 0.6 },
|
|
2008
|
+
head: { type: "bass", gain: 4 },
|
|
2009
|
+
cab: { type: "8x10" },
|
|
2010
|
+
},
|
|
2011
|
+
reese: {
|
|
2012
|
+
stomp: { type: "rat", gain: 3, tone: 0.3, mix: 0.5 },
|
|
2013
|
+
head: { type: "bass", gain: 6, master: 6 },
|
|
2014
|
+
cab: { type: "1x15" },
|
|
2015
|
+
},
|
|
2016
|
+
jangle: {
|
|
2017
|
+
stomp: undefined,
|
|
2018
|
+
head: { type: "chime", gain: 3, treble: 7 },
|
|
2019
|
+
cab: { type: "2x12", mic: 0.2 },
|
|
2020
|
+
compressor: { threshold: -20, ratio: 4, attack: 0.01, release: 0.15 },
|
|
2021
|
+
},
|
|
2022
|
+
alt: {
|
|
2023
|
+
stomp: { type: "rat", gain: 6, tone: 0.4 },
|
|
2024
|
+
head: { type: "crunch", gain: 4 },
|
|
2025
|
+
cab: { type: "4x12" },
|
|
2026
|
+
},
|
|
2027
|
+
});
|
|
2028
|
+
|
|
2029
|
+
/**
|
|
2030
|
+
* A guitar rig for a track's `fx` (SDK 1.22.0): the stomp → head → cab
|
|
2031
|
+
* stages of rig preset `name` and its companion effects (as the `rig`
|
|
2032
|
+
* command sets them), with optional per-stage overrides. Spread it
|
|
2033
|
+
* into `fx` next to other effects:
|
|
2034
|
+
*
|
|
2035
|
+
* ```ts
|
|
2036
|
+
* fx: { ...rig("crunch"), chorus: {} }
|
|
2037
|
+
* fx: { ...rig("metal", { head: { gain: 9 } }) }
|
|
2038
|
+
* ```
|
|
2039
|
+
*/
|
|
2040
|
+
export function rig(
|
|
2041
|
+
name: string,
|
|
2042
|
+
overrides: Readonly<
|
|
2043
|
+
Partial<Record<"stomp" | "head" | "cab", EffectParams>>
|
|
2044
|
+
> = {},
|
|
2045
|
+
): FxInput {
|
|
2046
|
+
if (
|
|
2047
|
+
typeof name !== "string" ||
|
|
2048
|
+
!Object.prototype.hasOwnProperty.call(RIG_PRESETS, name)
|
|
2049
|
+
)
|
|
2050
|
+
throw new DawgSdkError(
|
|
2051
|
+
`unknown rig "${String(name)}" (rigs: ${Object.keys(RIG_PRESETS).join(", ")})`,
|
|
2052
|
+
);
|
|
2053
|
+
if (!isRecord(overrides))
|
|
2054
|
+
throw new DawgSdkError("rig overrides must be an object");
|
|
2055
|
+
const out: Record<string, EffectParams> = {};
|
|
2056
|
+
// Companion effects first, then the stages in chain order.
|
|
2057
|
+
for (const [effect, values] of Object.entries(RIG_PRESETS[name]!))
|
|
2058
|
+
if (values && effect !== "stomp" && effect !== "head" && effect !== "cab")
|
|
2059
|
+
out[effect] = Object.freeze({ ...values });
|
|
2060
|
+
for (const stage of ["stomp", "head", "cab"] as const) {
|
|
2061
|
+
const base = RIG_PRESETS[name]![stage];
|
|
2062
|
+
const extra = overrides[stage];
|
|
2063
|
+
if (extra !== undefined && !isRecord(extra))
|
|
2064
|
+
throw new DawgSdkError(`rig ${stage} overrides must be an object`);
|
|
2065
|
+
if (base || extra)
|
|
2066
|
+
out[stage] = Object.freeze({ ...(base ?? {}), ...(extra ?? {}) });
|
|
2067
|
+
}
|
|
2068
|
+
return Object.freeze(out);
|
|
2069
|
+
}
|
|
2070
|
+
|
|
1567
2071
|
/** One effect's parameters; omitted ones take dawg's defaults. */
|
|
1568
2072
|
export type EffectParams = Readonly<Record<string, number | string | boolean>>;
|
|
1569
2073
|
|
|
1570
2074
|
/**
|
|
1571
2075
|
* Insert effects by name, rendered in the fixed chain order
|
|
1572
|
-
* filter → djf → autofilter → vowel → crush → distort →
|
|
1573
|
-
* compressor → pan → phaser → chorus → leslie → postgain →
|
|
1574
|
-
*
|
|
2076
|
+
* filter → djf → autofilter → vowel → crush → distort → stomp → head →
|
|
2077
|
+
* cab → tremolo → compressor → pan → phaser → chorus → leslie → postgain →
|
|
2078
|
+
* delay → reverb. The guitar rig (stomp, head, cab; SDK 1.22.0) is easiest
|
|
2079
|
+
* as `...rig("crunch")`.
|
|
2080
|
+
* Keys here: djf, autofilter, vowel, crush, distort, stomp, head, cab,
|
|
2081
|
+
* tremolo, compressor,
|
|
1575
2082
|
* phaser, chorus, leslie, postgain, plus the mix-bus keys `orbit`
|
|
1576
2083
|
* (`{ orbit: 2 }`, SDK 1.9.0) and `duck` (`{ orbit: 2, depth: 0.85 }`:
|
|
1577
2084
|
* this track's onsets duck every other track on that orbit). See
|
|
@@ -1585,6 +2092,53 @@ export type FxInput = Readonly<Record<string, EffectParams>>;
|
|
|
1585
2092
|
* `"synth-<param>"` (e.g. `"synth-lpf"`), read at each note's onset.
|
|
1586
2093
|
* Every parameter, range and default: **Synth** in DAWG.md.
|
|
1587
2094
|
*/
|
|
2095
|
+
/**
|
|
2096
|
+
* Modelled piano settings (SDK 1.24.0), for a track whose instrument is a
|
|
2097
|
+
* piano family (`"grand"`, `"upright"`, `"felt"`, `"honkytonk"`,
|
|
2098
|
+
* `"prepared"`; the words `"ballad"` and `"lofi"` pick presets). Every field
|
|
2099
|
+
* is optional; `{}` is the family's own sound. Automate a parameter with
|
|
2100
|
+
* `automation.fx["keys-<param>"]` (hardness, touch, decay, release, knock,
|
|
2101
|
+
* noise, felt), read at each note's onset. Ranges: **Keys** in DAWG.md.
|
|
2102
|
+
*/
|
|
2103
|
+
export type KeysInput = Readonly<{
|
|
2104
|
+
/** A named preset: grand ballad upright felt lofi honkytonk prepared. */
|
|
2105
|
+
preset?: string;
|
|
2106
|
+
/** Hammer hardness 0..1: brightness at a given velocity (0.5). */
|
|
2107
|
+
hardness?: number;
|
|
2108
|
+
/** Velocity sensitivity 0..1 (1). */
|
|
2109
|
+
touch?: number;
|
|
2110
|
+
/** Inharmonicity multiplier 0..4 (1 grand, 2.5 upright, 0 harmonic). */
|
|
2111
|
+
inharm?: number;
|
|
2112
|
+
/** Unison detune in cents 0..30 (0.7; honkytonk 16). */
|
|
2113
|
+
unison?: number;
|
|
2114
|
+
/** Sustain time multiplier 0.1..4 (1). */
|
|
2115
|
+
decay?: number;
|
|
2116
|
+
/** Damper time multiplier 0.1..4 (1). */
|
|
2117
|
+
release?: number;
|
|
2118
|
+
/** Hammer position along the string 0.04..0.3 (0.12). */
|
|
2119
|
+
strike?: number;
|
|
2120
|
+
/** Aftersound share 0..1 (0.3). */
|
|
2121
|
+
after?: number;
|
|
2122
|
+
/** Soundboard knock 0..1 (0.5). */
|
|
2123
|
+
knock?: number;
|
|
2124
|
+
/** Key and damper mechanics 0..1 (0.25). */
|
|
2125
|
+
noise?: number;
|
|
2126
|
+
/** Felt strip 0..1 (0; felt family 1). */
|
|
2127
|
+
felt?: number;
|
|
2128
|
+
/** Share of prepared keys 0..1 (0; prepared family 0.6). */
|
|
2129
|
+
prep?: number;
|
|
2130
|
+
/** Keyboard stereo width 0..1 (0.6). */
|
|
2131
|
+
width?: number;
|
|
2132
|
+
/** Octave stretch 0..1 (1); 0 keeps every key exactly on its tuning. */
|
|
2133
|
+
stretch?: number;
|
|
2134
|
+
/** Body EQ: grand upright felt honkytonk prepared (the family's own). */
|
|
2135
|
+
body?: string;
|
|
2136
|
+
/** Pitch wobble rate in Hz (tape wow), 0 off. */
|
|
2137
|
+
vib?: number;
|
|
2138
|
+
/** Pitch wobble depth in semitones (0.5). */
|
|
2139
|
+
vibmod?: number;
|
|
2140
|
+
}>;
|
|
2141
|
+
|
|
1588
2142
|
export type SynthInput = Readonly<{
|
|
1589
2143
|
attack?: number;
|
|
1590
2144
|
decay?: number;
|
|
@@ -1668,9 +2222,26 @@ export type TrackInput = Readonly<{
|
|
|
1668
2222
|
* `triangle`, and Strudel's `sawtooth`, `supersaw`, `pulse`, `user`,
|
|
1669
2223
|
* `white`, `pink`, `brown`, `crackle`, and the ZzFX sounds `z_sine`,
|
|
1670
2224
|
* `z_triangle`, `z_sawtooth`, `z_square`, `z_tan`, `z_noise`), `kit` for drums,
|
|
1671
|
-
* `sampler(...)` or `wavetable(...)
|
|
2225
|
+
* `sampler(...)` or `wavetable(...)`, or a mallet or bell (`vibes`,
|
|
2226
|
+
* `glock`, `gong`, … or `modal(...)`, SDK 1.25.0). Default `sine`.
|
|
1672
2227
|
*/
|
|
1673
|
-
instrument?:
|
|
2228
|
+
instrument?:
|
|
2229
|
+
| string
|
|
2230
|
+
| SamplerSpec
|
|
2231
|
+
| WavetableSpec
|
|
2232
|
+
| StringSpec
|
|
2233
|
+
| GranularSpec
|
|
2234
|
+
| ModalSpec;
|
|
2235
|
+
/**
|
|
2236
|
+
* The sampler a `granular(...)` track keeps while it grains one of its
|
|
2237
|
+
* voices (SDK 1.23.0); `grain off` plays it again.
|
|
2238
|
+
*/
|
|
2239
|
+
sampler?: SamplerSpec | null;
|
|
2240
|
+
/**
|
|
2241
|
+
* Granular engine (SDK 1.23.0) for an `instrument: "granular"` track, or
|
|
2242
|
+
* use `instrument: granular("cloud", {...})` or a word (`"cloud"`).
|
|
2243
|
+
*/
|
|
2244
|
+
granular?: GranularInput | null;
|
|
1674
2245
|
/**
|
|
1675
2246
|
* Synthesized drum kit for an `instrument: "kit"` track: `syn808`,
|
|
1676
2247
|
* `syn909`, `acoustic`, `lofi`, `electro` or `trap`. Omit for the default
|
|
@@ -1694,6 +2265,17 @@ export type TrackInput = Readonly<{
|
|
|
1694
2265
|
tuning?: TuningInput | null;
|
|
1695
2266
|
/** Synth voice parameters, Strudel names (`{ attack: 0.01, lpf: 800 }`). */
|
|
1696
2267
|
synth?: SynthInput;
|
|
2268
|
+
/**
|
|
2269
|
+
* String engine (SDK 1.21.0) for an `instrument: "string"` track, or use
|
|
2270
|
+
* `instrument: stringed("sitar", {...})` or a preset word (`"nylon"`).
|
|
2271
|
+
*/
|
|
2272
|
+
string?: StringInput | null;
|
|
2273
|
+
/**
|
|
2274
|
+
* Modelled piano settings (SDK 1.24.0) for `instrument: "grand"` and the
|
|
2275
|
+
* other piano families; `{}` is the family's sound. The word `"piano"`
|
|
2276
|
+
* keeps the classic 0.4 tone; use `"grand"` for the modelled piano.
|
|
2277
|
+
*/
|
|
2278
|
+
keys?: KeysInput;
|
|
1697
2279
|
muted?: boolean;
|
|
1698
2280
|
/** When any track is soloed only soloed tracks play. */
|
|
1699
2281
|
solo?: boolean;
|
|
@@ -1833,6 +2415,10 @@ export type TrackSpec = Readonly<{
|
|
|
1833
2415
|
synth: SynthInput | null;
|
|
1834
2416
|
sampler: SamplerSpec | null;
|
|
1835
2417
|
wavetable: WavetableSpec | null;
|
|
2418
|
+
/** String engine settings (SDK 1.21.0); present only when set. */
|
|
2419
|
+
string?: StringInput;
|
|
2420
|
+
/** Granular engine settings (SDK 1.23.0); null when not granular. */
|
|
2421
|
+
granular?: GranularInput | null;
|
|
1836
2422
|
automation: Readonly<Required<AutomationInput>>;
|
|
1837
2423
|
/** Every hit resolved to its pitch slot. */
|
|
1838
2424
|
notes: readonly NoteSpec[];
|
|
@@ -1855,6 +2441,10 @@ export type TrackSpec = Readonly<{
|
|
|
1855
2441
|
seed: number;
|
|
1856
2442
|
}>;
|
|
1857
2443
|
tuning: ScoreTuning | null;
|
|
2444
|
+
/** Modelled piano settings (SDK 1.24.0); present only when set. */
|
|
2445
|
+
keys?: KeysInput;
|
|
2446
|
+
/** Modal settings (SDK 1.25.0); present only on a modal track. */
|
|
2447
|
+
modal?: Readonly<{ preset?: ModalPresetName } & ModalParams>;
|
|
1858
2448
|
}>;
|
|
1859
2449
|
|
|
1860
2450
|
export type GlideMode = "legato" | "mono" | "poly";
|
|
@@ -2056,34 +2646,77 @@ export function track(input: TrackInput): TrackSpec {
|
|
|
2056
2646
|
if (typeof id !== "string" || id.length === 0 || id.length > 64)
|
|
2057
2647
|
throw new DawgSdkError(`track ${name}: id must be 1..64 characters`);
|
|
2058
2648
|
const rawInstrument = input.instrument ?? "sine";
|
|
2649
|
+
// A granular track may keep the sampler it grains (`grain off` goes back).
|
|
2650
|
+
const keptSampler =
|
|
2651
|
+
isRecord(input.sampler) &&
|
|
2652
|
+
input.sampler.kind === "sampler" &&
|
|
2653
|
+
isRecord(rawInstrument) &&
|
|
2654
|
+
rawInstrument.kind === "granular"
|
|
2655
|
+
? localizeSampler(input.sampler as SamplerSpec, slug)
|
|
2656
|
+
: null;
|
|
2657
|
+
if (input.sampler !== undefined && input.sampler !== null && !keptSampler)
|
|
2658
|
+
throw new DawgSdkError(
|
|
2659
|
+
`track ${name}: sampler: is only for a granular(...) track; use instrument: sampler({...})`,
|
|
2660
|
+
);
|
|
2059
2661
|
const samplerSpec =
|
|
2060
2662
|
isRecord(rawInstrument) && rawInstrument.kind === "sampler"
|
|
2061
2663
|
? localizeSampler(rawInstrument as SamplerSpec, slug)
|
|
2062
|
-
:
|
|
2664
|
+
: keptSampler;
|
|
2063
2665
|
const wavetableSpec =
|
|
2064
2666
|
isRecord(rawInstrument) && rawInstrument.kind === "wavetable"
|
|
2065
2667
|
? localizeWavetable(rawInstrument as WavetableSpec, slug)
|
|
2066
2668
|
: null;
|
|
2067
|
-
const
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2669
|
+
const stringFromInstrument =
|
|
2670
|
+
isRecord(rawInstrument) && rawInstrument.kind === "string"
|
|
2671
|
+
? stringInput(rawInstrument, name)
|
|
2672
|
+
: null;
|
|
2673
|
+
const granularFromInstrument =
|
|
2674
|
+
isRecord(rawInstrument) && rawInstrument.kind === "granular"
|
|
2675
|
+
? granularInput(rawInstrument, name, slug)
|
|
2676
|
+
: null;
|
|
2677
|
+
const word =
|
|
2678
|
+
typeof rawInstrument === "string"
|
|
2679
|
+
? resolveInstrumentWord(rawInstrument)
|
|
2680
|
+
: undefined;
|
|
2681
|
+
const modalSpec = trackModal(rawInstrument);
|
|
2682
|
+
const instrument = granularFromInstrument
|
|
2683
|
+
? GRANULAR_INSTRUMENT
|
|
2684
|
+
: samplerSpec
|
|
2685
|
+
? SAMPLER_INSTRUMENT
|
|
2686
|
+
: wavetableSpec
|
|
2687
|
+
? WAVETABLE_INSTRUMENT
|
|
2688
|
+
: stringFromInstrument
|
|
2689
|
+
? STRING_INSTRUMENT
|
|
2690
|
+
: modalSpec
|
|
2691
|
+
? MODAL_INSTRUMENT
|
|
2692
|
+
: typeof rawInstrument === "string"
|
|
2693
|
+
? (word?.instrument ?? rawInstrument)
|
|
2694
|
+
: undefined;
|
|
2695
|
+
// A granular word (`"cloud"`) turns the engine on with its preset.
|
|
2696
|
+
const granularSpec =
|
|
2697
|
+
granularInput(input.granular, name, slug) ??
|
|
2698
|
+
granularFromInstrument ??
|
|
2699
|
+
(word?.field === "granular" && word.preset
|
|
2700
|
+
? Object.freeze({ preset: word.preset })
|
|
2701
|
+
: instrument === GRANULAR_INSTRUMENT
|
|
2702
|
+
? Object.freeze({})
|
|
2703
|
+
: null);
|
|
2074
2704
|
if (
|
|
2075
2705
|
instrument === undefined ||
|
|
2076
2706
|
instrument.length === 0 ||
|
|
2077
2707
|
instrument.length > 64
|
|
2078
2708
|
)
|
|
2079
2709
|
throw new DawgSdkError(
|
|
2080
|
-
`track ${name}: instrument must be a voice name, "kit", sampler(...) or
|
|
2710
|
+
`track ${name}: instrument must be a voice name, "kit", sampler(...), wavetable(...), stringed(...), granular(...) or modal(...)`,
|
|
2081
2711
|
);
|
|
2082
2712
|
if (instrument === SAMPLER_INSTRUMENT && !samplerSpec)
|
|
2083
2713
|
throw new DawgSdkError(
|
|
2084
2714
|
`track ${name}: use instrument: sampler({...}) for a sampler track`,
|
|
2085
2715
|
);
|
|
2086
|
-
const slots =
|
|
2716
|
+
const slots =
|
|
2717
|
+
samplerSpec && instrument === SAMPLER_INSTRUMENT
|
|
2718
|
+
? voiceSlots(samplerSpec)
|
|
2719
|
+
: undefined;
|
|
2087
2720
|
const kit = KIT_INSTRUMENTS.includes(instrument.trim().toLowerCase());
|
|
2088
2721
|
const notes = (input.notes ?? []).map((item, index) => {
|
|
2089
2722
|
if (!isRecord(item) || (item.kind !== "note" && item.kind !== "hit"))
|
|
@@ -2158,6 +2791,23 @@ export function track(input: TrackInput): TrackSpec {
|
|
|
2158
2791
|
throw new DawgSdkError(
|
|
2159
2792
|
`track ${name}: unknown automation lane "${key}" (${AUTOMATION_KEYS.join(" ")})`,
|
|
2160
2793
|
);
|
|
2794
|
+
// A keys preset word (`"lofi"`, `"ballad"`) brings its preset's effects,
|
|
2795
|
+
// as the prompt does; explicit filter, fx and reverb win.
|
|
2796
|
+
const presetFx = keysPresetFx(input.keys, rawInstrument);
|
|
2797
|
+
if (presetFx) {
|
|
2798
|
+
input = {
|
|
2799
|
+
...input,
|
|
2800
|
+
...(input.filter === undefined && presetFx.filter
|
|
2801
|
+
? { filter: presetFx.filter }
|
|
2802
|
+
: {}),
|
|
2803
|
+
...(input.reverb === undefined && presetFx.reverb
|
|
2804
|
+
? { reverb: presetFx.reverb }
|
|
2805
|
+
: {}),
|
|
2806
|
+
...(presetFx.fx && input.fx !== null
|
|
2807
|
+
? { fx: { ...presetFx.fx, ...(isRecord(input.fx) ? input.fx : {}) } }
|
|
2808
|
+
: {}),
|
|
2809
|
+
};
|
|
2810
|
+
}
|
|
2161
2811
|
const filter =
|
|
2162
2812
|
input.filter === undefined || input.filter === null
|
|
2163
2813
|
? null
|
|
@@ -2200,8 +2850,26 @@ export function track(input: TrackInput): TrackSpec {
|
|
|
2200
2850
|
? {}
|
|
2201
2851
|
: { ir: reverbIr(input.reverb.ir, name, slug) }),
|
|
2202
2852
|
});
|
|
2203
|
-
|
|
2853
|
+
// A guitar alias (`instrument: "jangle"`) also loads its rig; stages and
|
|
2854
|
+
// effects the track's own `fx` names win.
|
|
2855
|
+
const aliasRig =
|
|
2856
|
+
typeof rawInstrument === "string"
|
|
2857
|
+
? resolveInstrumentWord(rawInstrument)?.fx
|
|
2858
|
+
: undefined;
|
|
2859
|
+
const fx = fxInput(
|
|
2860
|
+
aliasRig === undefined
|
|
2861
|
+
? input.fx
|
|
2862
|
+
: { ...rig(aliasRig), ...(isRecord(input.fx) ? input.fx : {}) },
|
|
2863
|
+
name,
|
|
2864
|
+
);
|
|
2204
2865
|
const synth = synthInput(input.synth, name);
|
|
2866
|
+
// A string preset word (`"nylon"`) turns the engine on with its preset.
|
|
2867
|
+
const string =
|
|
2868
|
+
stringInput(input.string, name) ??
|
|
2869
|
+
stringFromInstrument ??
|
|
2870
|
+
(word?.field === "string" && word.preset
|
|
2871
|
+
? Object.freeze({ preset: word.preset })
|
|
2872
|
+
: null);
|
|
2205
2873
|
return Object.freeze({
|
|
2206
2874
|
kind: "track",
|
|
2207
2875
|
id,
|
|
@@ -2219,6 +2887,8 @@ export function track(input: TrackInput): TrackSpec {
|
|
|
2219
2887
|
synth,
|
|
2220
2888
|
sampler: samplerSpec,
|
|
2221
2889
|
wavetable: wavetableSpec,
|
|
2890
|
+
...(string ? { string } : {}),
|
|
2891
|
+
...(granularSpec ? { granular: granularSpec } : {}),
|
|
2222
2892
|
automation: Object.freeze({
|
|
2223
2893
|
volume: lane("volume"),
|
|
2224
2894
|
pan: lane("pan"),
|
|
@@ -2235,9 +2905,100 @@ export function track(input: TrackInput): TrackSpec {
|
|
|
2235
2905
|
...trackTime(input.time, name),
|
|
2236
2906
|
...trackPerformance(input, name),
|
|
2237
2907
|
tuning: tuningSpec(input.tuning, `track ${name}`),
|
|
2908
|
+
...keysSpec(input.keys, rawInstrument, name),
|
|
2909
|
+
...(modalSpec ? { modal: modalSpec } : {}),
|
|
2238
2910
|
});
|
|
2239
2911
|
}
|
|
2240
2912
|
|
|
2913
|
+
/**
|
|
2914
|
+
* Effects the keys presets set with the voice: a copy of `KEYS_PRESETS`
|
|
2915
|
+
* filter, fx and reverb in core/keys.ts (core/keys.test.ts checks they
|
|
2916
|
+
* match the prompt's `piano <preset>`).
|
|
2917
|
+
*/
|
|
2918
|
+
const KEYS_PRESET_FX: Readonly<
|
|
2919
|
+
Record<
|
|
2920
|
+
string,
|
|
2921
|
+
Readonly<{
|
|
2922
|
+
filter?: FilterInput;
|
|
2923
|
+
fx?: FxInput;
|
|
2924
|
+
reverb?: ReverbInput;
|
|
2925
|
+
}>
|
|
2926
|
+
>
|
|
2927
|
+
> = Object.freeze({
|
|
2928
|
+
ballad: { reverb: { mix: 0.25, size: 0.7 } },
|
|
2929
|
+
felt: { reverb: { mix: 0.2, size: 0.5 } },
|
|
2930
|
+
lofi: {
|
|
2931
|
+
filter: { cutoff: 3500, resonance: 0.1 },
|
|
2932
|
+
fx: { crush: { bits: 10 } },
|
|
2933
|
+
},
|
|
2934
|
+
});
|
|
2935
|
+
|
|
2936
|
+
/**
|
|
2937
|
+
* The preset effects an instrument word brings: a preset word that is not
|
|
2938
|
+
* also its family (`"lofi"`, `"ballad"`), or any preset word without
|
|
2939
|
+
* `keys` (`"felt"`). A printed track names its family and always prints
|
|
2940
|
+
* `keys`, so print → eval never adds them twice.
|
|
2941
|
+
*/
|
|
2942
|
+
function keysPresetFx(
|
|
2943
|
+
keys: unknown,
|
|
2944
|
+
word: unknown,
|
|
2945
|
+
): (typeof KEYS_PRESET_FX)[string] | undefined {
|
|
2946
|
+
if (typeof word !== "string") return undefined;
|
|
2947
|
+
const meaning = resolveInstrumentWord(word);
|
|
2948
|
+
if (meaning?.field !== "keys" || !meaning.preset) return undefined;
|
|
2949
|
+
if (keys !== undefined && keys !== null && word === meaning.instrument)
|
|
2950
|
+
return undefined;
|
|
2951
|
+
return KEYS_PRESET_FX[meaning.preset];
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2954
|
+
/**
|
|
2955
|
+
* `keys` for `track()`: the input as given, or `{ preset }` when the
|
|
2956
|
+
* instrument word names a keys preset (`"grand"`, `"lofi"`), so the word
|
|
2957
|
+
* alone plays the modelled piano. dawg validates the values.
|
|
2958
|
+
*/
|
|
2959
|
+
function keysSpec(
|
|
2960
|
+
input: unknown,
|
|
2961
|
+
word: unknown,
|
|
2962
|
+
name: string,
|
|
2963
|
+
): { keys?: KeysInput } {
|
|
2964
|
+
const meaning =
|
|
2965
|
+
typeof word === "string" ? resolveInstrumentWord(word) : undefined;
|
|
2966
|
+
const preset = meaning?.field === "keys" ? meaning.preset : undefined;
|
|
2967
|
+
if (input === undefined || input === null)
|
|
2968
|
+
return preset ? { keys: Object.freeze({ preset }) } : {};
|
|
2969
|
+
if (!isRecord(input))
|
|
2970
|
+
throw new DawgSdkError(`track ${name}: keys must be an object`);
|
|
2971
|
+
const out: Record<string, EffectValue> = {};
|
|
2972
|
+
// A preset word (`"lofi"`) keeps its preset under given overrides; a
|
|
2973
|
+
// family word (`"upright"`) with `keys` is exactly the given keys.
|
|
2974
|
+
if (preset && input.preset === undefined && word !== meaning?.instrument)
|
|
2975
|
+
out.preset = preset;
|
|
2976
|
+
for (const [key, value] of Object.entries(input)) {
|
|
2977
|
+
if (value === undefined) continue;
|
|
2978
|
+
out[key] = effectValue(value, `${name} keys.${key}`);
|
|
2979
|
+
}
|
|
2980
|
+
return { keys: Object.freeze(out) };
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
/**
|
|
2984
|
+
* The modal field an instrument makes: `modal(...)`, or a modal preset
|
|
2985
|
+
* word (`"vibes"`, `"glockenspiel"`). The bare words `"modal"` and
|
|
2986
|
+
* `"marimba"` keep their pre-0.6 meaning and make none.
|
|
2987
|
+
*/
|
|
2988
|
+
function trackModal(
|
|
2989
|
+
raw: unknown,
|
|
2990
|
+
): Readonly<{ preset?: ModalPresetName } & ModalParams> | undefined {
|
|
2991
|
+
if (isRecord(raw) && raw.kind === "modal") {
|
|
2992
|
+
const { kind: _kind, ...fields } = raw as ModalSpec;
|
|
2993
|
+
return Object.freeze(fields);
|
|
2994
|
+
}
|
|
2995
|
+
if (typeof raw !== "string" || raw === MODAL_INSTRUMENT) return undefined;
|
|
2996
|
+
const meaning = resolveInstrumentWord(raw);
|
|
2997
|
+
if (meaning?.instrument !== MODAL_INSTRUMENT || !meaning.preset)
|
|
2998
|
+
return undefined;
|
|
2999
|
+
return Object.freeze({ preset: meaning.preset as ModalPresetName });
|
|
3000
|
+
}
|
|
3001
|
+
|
|
2241
3002
|
function trackTime(input: unknown, name: string): { time?: TrackTimeInput } {
|
|
2242
3003
|
if (input === undefined || input === null) return {};
|
|
2243
3004
|
if (!isRecord(input))
|
|
@@ -2348,6 +3109,21 @@ function fxInput(input: unknown, name: string): FxInput | null {
|
|
|
2348
3109
|
return Object.keys(out).length > 0 ? Object.freeze(out) : null;
|
|
2349
3110
|
}
|
|
2350
3111
|
|
|
3112
|
+
function stringInput(input: unknown, name: string): StringInput | null {
|
|
3113
|
+
if (input === undefined || input === null) return null;
|
|
3114
|
+
if (!isRecord(input))
|
|
3115
|
+
throw new DawgSdkError(`track ${name}: string must be an object`);
|
|
3116
|
+
const out: Record<string, number | string> = {};
|
|
3117
|
+
for (const [key, value] of Object.entries(input)) {
|
|
3118
|
+
if (value === undefined || key === "kind") continue;
|
|
3119
|
+
out[key] =
|
|
3120
|
+
typeof value === "string"
|
|
3121
|
+
? value
|
|
3122
|
+
: finite(value, `${name} string.${key}`);
|
|
3123
|
+
}
|
|
3124
|
+
return Object.freeze(out);
|
|
3125
|
+
}
|
|
3126
|
+
|
|
2351
3127
|
function synthInput(input: unknown, name: string): SynthInput | null {
|
|
2352
3128
|
if (input === undefined || input === null) return null;
|
|
2353
3129
|
if (!isRecord(input))
|
|
@@ -2433,7 +3209,7 @@ function reverbIr(
|
|
|
2433
3209
|
throw new DawgSdkError(`track ${name}: reverb.ir needs a src`);
|
|
2434
3210
|
const src = spec.src.trim().replace(/^\.\//, "");
|
|
2435
3211
|
if (src.startsWith("pack:")) {
|
|
2436
|
-
const ref =
|
|
3212
|
+
const ref = sampleSpec(spec as SampleSpec, `${name} reverb.ir`);
|
|
2437
3213
|
return Object.freeze({
|
|
2438
3214
|
src: ref.src,
|
|
2439
3215
|
...(ref.sha256 ? { sha256: ref.sha256 } : {}),
|
|
@@ -2445,6 +3221,41 @@ function reverbIr(
|
|
|
2445
3221
|
return src.startsWith("tracks/") ? src : `tracks/${slug}/${src}`;
|
|
2446
3222
|
}
|
|
2447
3223
|
|
|
3224
|
+
/** Validates `granular` input and localizes a sample source like a voice. */
|
|
3225
|
+
function granularInput(
|
|
3226
|
+
input: unknown,
|
|
3227
|
+
name: string,
|
|
3228
|
+
slug: string,
|
|
3229
|
+
): GranularInput | null {
|
|
3230
|
+
if (input === undefined || input === null) return null;
|
|
3231
|
+
if (!isRecord(input))
|
|
3232
|
+
throw new DawgSdkError(`track ${name}: granular must be an object`);
|
|
3233
|
+
const out: Record<string, unknown> = {};
|
|
3234
|
+
for (const [key, value] of Object.entries(input)) {
|
|
3235
|
+
if (value === undefined || key === "kind") continue;
|
|
3236
|
+
if (key === "src" && isRecord(value)) {
|
|
3237
|
+
const ref = sampleSpec(value as SampleSpec, `${name} granular src`);
|
|
3238
|
+
const src = ref.src.replace(/^\.\//, "");
|
|
3239
|
+
out.src = Object.freeze({
|
|
3240
|
+
...ref,
|
|
3241
|
+
src:
|
|
3242
|
+
src.startsWith("tracks/") || src.startsWith("pack:")
|
|
3243
|
+
? src
|
|
3244
|
+
: `tracks/${slug}/${src}`,
|
|
3245
|
+
});
|
|
3246
|
+
} else if (key === "src" && typeof value === "string")
|
|
3247
|
+
out.src = value.startsWith("synth:")
|
|
3248
|
+
? value
|
|
3249
|
+
: granularInput({ src: { src: value } }, name, slug)!.src;
|
|
3250
|
+
else if (typeof value === "number")
|
|
3251
|
+
out[key] = finite(value, `${name} granular.${key}`);
|
|
3252
|
+
else if (typeof value === "string" || typeof value === "boolean")
|
|
3253
|
+
out[key] = value;
|
|
3254
|
+
else throw new DawgSdkError(`${name} granular.${key} must be a value`);
|
|
3255
|
+
}
|
|
3256
|
+
return Object.freeze(out) as GranularInput;
|
|
3257
|
+
}
|
|
3258
|
+
|
|
2448
3259
|
/** `./wavetables/x.wav` → `tracks/<slug>/wavetables/x.wav`, like sampler files. */
|
|
2449
3260
|
function localizeWavetable(spec: WavetableSpec, slug: string): WavetableSpec {
|
|
2450
3261
|
const src = spec.table.src;
|
|
@@ -2617,6 +3428,9 @@ export type ScoreSampleRef = Readonly<{
|
|
|
2617
3428
|
fit?: boolean;
|
|
2618
3429
|
accelerate?: number;
|
|
2619
3430
|
squiz?: number;
|
|
3431
|
+
bpm?: number;
|
|
3432
|
+
fitmode?: "repitch" | "beats" | "tones";
|
|
3433
|
+
len?: number;
|
|
2620
3434
|
}>;
|
|
2621
3435
|
|
|
2622
3436
|
/** A stored track; optional fields are present only when set. */
|
|
@@ -2640,6 +3454,7 @@ export type ScoreTrack = Readonly<{
|
|
|
2640
3454
|
fx?: FxInput;
|
|
2641
3455
|
fxAutomation?: Readonly<Record<string, readonly ScorePoint[]>>;
|
|
2642
3456
|
synth?: SynthInput;
|
|
3457
|
+
keys?: KeysInput;
|
|
2643
3458
|
sampler?: Readonly<{
|
|
2644
3459
|
voices: Readonly<Record<string, ScoreSampleRef>>;
|
|
2645
3460
|
mode: "oneshot" | "keyed";
|
|
@@ -2659,6 +3474,12 @@ export type ScoreTrack = Readonly<{
|
|
|
2659
3474
|
tuning?: ScoreTuning;
|
|
2660
3475
|
wavetable?: Readonly<{ table: ScoreSampleRef } & WavetableParams>;
|
|
2661
3476
|
wtAutomation?: readonly ScorePoint[];
|
|
3477
|
+
/** String engine settings; dawg validates them (SDK 1.21.0). */
|
|
3478
|
+
string?: StringInput;
|
|
3479
|
+
/** Granular settings (SDK 1.23.0); present only when set. */
|
|
3480
|
+
granular?: GranularInput;
|
|
3481
|
+
/** Modal settings (SDK 1.25.0). */
|
|
3482
|
+
modal?: TrackSpec["modal"];
|
|
2662
3483
|
glide?: TrackSpec["glide"];
|
|
2663
3484
|
pedal?: readonly Readonly<{ tick: number; state: PedalState }>[];
|
|
2664
3485
|
velocityCurve?: TrackSpec["velocityCurve"];
|
|
@@ -2926,6 +3747,7 @@ export function song(input: SongInput): Song {
|
|
|
2926
3747
|
stored.wavetable = Object.freeze(fields);
|
|
2927
3748
|
}
|
|
2928
3749
|
if (wtAutomation.length > 0) stored.wtAutomation = wtAutomation;
|
|
3750
|
+
if (t.granular) stored.granular = t.granular;
|
|
2929
3751
|
if (t.sampler)
|
|
2930
3752
|
stored.sampler = Object.freeze({
|
|
2931
3753
|
voices: t.sampler.voices,
|
|
@@ -2962,6 +3784,9 @@ export function song(input: SongInput): Song {
|
|
|
2962
3784
|
if (t.velocityCurve) stored.velocityCurve = t.velocityCurve;
|
|
2963
3785
|
if (t.humanize) stored.humanize = t.humanize;
|
|
2964
3786
|
if (t.tuning) stored.tuning = t.tuning;
|
|
3787
|
+
if (t.string) stored.string = t.string;
|
|
3788
|
+
if (t.keys) stored.keys = t.keys;
|
|
3789
|
+
if (t.modal) stored.modal = t.modal;
|
|
2965
3790
|
if (t.rhythm && t.rhythm.length > 0)
|
|
2966
3791
|
stored.rhythm = Object.freeze(
|
|
2967
3792
|
t.rhythm.map((row) => {
|
|
@@ -3834,6 +4659,274 @@ export function progression(
|
|
|
3834
4659
|
);
|
|
3835
4660
|
}
|
|
3836
4661
|
|
|
4662
|
+
// BEGIN instrument words: generated from core/instruments.ts by core/sdk/sync-instruments.ts
|
|
4663
|
+
/** What an instrument word stores on a track. */
|
|
4664
|
+
type InstrumentWord = Readonly<{
|
|
4665
|
+
/** The `Track.instrument` value. */
|
|
4666
|
+
instrument: string;
|
|
4667
|
+
/** The optional Track field the engine reads (created with defaults). */
|
|
4668
|
+
field?: string;
|
|
4669
|
+
/** A preset of that engine to apply. */
|
|
4670
|
+
preset?: string;
|
|
4671
|
+
/** An insert-effect preset to apply with it (rig aliases). */
|
|
4672
|
+
fx?: string;
|
|
4673
|
+
}>;
|
|
4674
|
+
|
|
4675
|
+
/** A word and what it means. */
|
|
4676
|
+
type InstrumentWordRow = Readonly<{
|
|
4677
|
+
word: string;
|
|
4678
|
+
/**
|
|
4679
|
+
* Borrow the voice (instrument, field, preset) of this other word's row
|
|
4680
|
+
* when it exists; `instrument` is the fallback when it does not.
|
|
4681
|
+
*/
|
|
4682
|
+
voice?: string;
|
|
4683
|
+
}> &
|
|
4684
|
+
InstrumentWord;
|
|
4685
|
+
|
|
4686
|
+
/**
|
|
4687
|
+
* Words that keep their pre-0.6 meaning forever: they resolve to
|
|
4688
|
+
* themselves, whatever rows the lanes add.
|
|
4689
|
+
*/
|
|
4690
|
+
const LEGACY_WORDS: readonly string[] = Object.freeze([
|
|
4691
|
+
"piano",
|
|
4692
|
+
"pluck",
|
|
4693
|
+
"bass",
|
|
4694
|
+
"saw",
|
|
4695
|
+
"square",
|
|
4696
|
+
"triangle",
|
|
4697
|
+
"marimba",
|
|
4698
|
+
"wind",
|
|
4699
|
+
"cello",
|
|
4700
|
+
"contrabass",
|
|
4701
|
+
"ebass",
|
|
4702
|
+
"sitar",
|
|
4703
|
+
"organ",
|
|
4704
|
+
"strings",
|
|
4705
|
+
"bell",
|
|
4706
|
+
"keys",
|
|
4707
|
+
"lead",
|
|
4708
|
+
]);
|
|
4709
|
+
|
|
4710
|
+
/** 0.6 instrument words; each lane appends its own block. */
|
|
4711
|
+
const INSTRUMENT_WORDS: readonly InstrumentWordRow[] = Object.freeze([
|
|
4712
|
+
// strings (f06-strings): plucked presets of the string engine. Legacy
|
|
4713
|
+
// sitar/ebass keep today's voice (`string preset sitar` reaches the
|
|
4714
|
+
// engine), jangle is the rig alias (the guitar lane maps its 12string to the
|
|
4715
|
+
// preset; `string jangle` reaches it) and
|
|
4716
|
+
// upright is the keys lane's piano (doublebass reaches the preset).
|
|
4717
|
+
{ word: "nylon", instrument: "string", field: "string", preset: "nylon" },
|
|
4718
|
+
{ word: "steel", instrument: "string", field: "string", preset: "steel" },
|
|
4719
|
+
{
|
|
4720
|
+
word: "electric",
|
|
4721
|
+
instrument: "string",
|
|
4722
|
+
field: "string",
|
|
4723
|
+
preset: "electric",
|
|
4724
|
+
},
|
|
4725
|
+
{ word: "slap", instrument: "string", field: "string", preset: "slap" },
|
|
4726
|
+
{ word: "motown", instrument: "string", field: "string", preset: "motown" },
|
|
4727
|
+
{ word: "tanpura", instrument: "string", field: "string", preset: "tanpura" },
|
|
4728
|
+
{
|
|
4729
|
+
word: "harpsichord",
|
|
4730
|
+
instrument: "string",
|
|
4731
|
+
field: "string",
|
|
4732
|
+
preset: "harpsichord",
|
|
4733
|
+
},
|
|
4734
|
+
{ word: "lute", instrument: "string", field: "string", preset: "lute" },
|
|
4735
|
+
{ word: "oud", instrument: "string", field: "string", preset: "oud" },
|
|
4736
|
+
{ word: "setar", instrument: "string", field: "string", preset: "setar" },
|
|
4737
|
+
{ word: "tar", instrument: "string", field: "string", preset: "tar" },
|
|
4738
|
+
{ word: "santur", instrument: "string", field: "string", preset: "santur" },
|
|
4739
|
+
{
|
|
4740
|
+
word: "dulcimer",
|
|
4741
|
+
instrument: "string",
|
|
4742
|
+
field: "string",
|
|
4743
|
+
preset: "dulcimer",
|
|
4744
|
+
},
|
|
4745
|
+
{ word: "koto", instrument: "string", field: "string", preset: "koto" },
|
|
4746
|
+
{ word: "harp", instrument: "string", field: "string", preset: "harp" },
|
|
4747
|
+
{ word: "banjo", instrument: "string", field: "string", preset: "banjo" },
|
|
4748
|
+
{ word: "tres", instrument: "string", field: "string", preset: "tres" },
|
|
4749
|
+
{
|
|
4750
|
+
word: "requinto",
|
|
4751
|
+
instrument: "string",
|
|
4752
|
+
field: "string",
|
|
4753
|
+
preset: "requinto",
|
|
4754
|
+
},
|
|
4755
|
+
{ word: "acoustic", instrument: "string", field: "string", preset: "steel" },
|
|
4756
|
+
{ word: "classical", instrument: "string", field: "string", preset: "nylon" },
|
|
4757
|
+
{
|
|
4758
|
+
word: "bassguitar",
|
|
4759
|
+
instrument: "string",
|
|
4760
|
+
field: "string",
|
|
4761
|
+
preset: "ebass",
|
|
4762
|
+
},
|
|
4763
|
+
{ word: "fender", instrument: "string", field: "string", preset: "ebass" },
|
|
4764
|
+
{
|
|
4765
|
+
word: "doublebass",
|
|
4766
|
+
instrument: "string",
|
|
4767
|
+
field: "string",
|
|
4768
|
+
preset: "upright",
|
|
4769
|
+
},
|
|
4770
|
+
{
|
|
4771
|
+
word: "cembalo",
|
|
4772
|
+
instrument: "string",
|
|
4773
|
+
field: "string",
|
|
4774
|
+
preset: "harpsichord",
|
|
4775
|
+
},
|
|
4776
|
+
{
|
|
4777
|
+
word: "hammered",
|
|
4778
|
+
instrument: "string",
|
|
4779
|
+
field: "string",
|
|
4780
|
+
preset: "dulcimer",
|
|
4781
|
+
},
|
|
4782
|
+
{ word: "sehtar", instrument: "string", field: "string", preset: "setar" },
|
|
4783
|
+
// f06-rig: guitar track aliases, a guitar voice plus a whole rig. The
|
|
4784
|
+
// voice is the strings lane's `electric` row (jangle: its 12-string
|
|
4785
|
+
// `jangle` preset); the pluck only while that row is absent. Never `lead`
|
|
4786
|
+
// or `bass`.
|
|
4787
|
+
{
|
|
4788
|
+
word: "jangle",
|
|
4789
|
+
instrument: "string",
|
|
4790
|
+
field: "string",
|
|
4791
|
+
preset: "jangle",
|
|
4792
|
+
fx: "jangle",
|
|
4793
|
+
},
|
|
4794
|
+
{ word: "punk", instrument: "pluck", voice: "electric", fx: "punk" },
|
|
4795
|
+
{ word: "funk", instrument: "pluck", voice: "electric", fx: "funk" },
|
|
4796
|
+
{ word: "ragged", instrument: "pluck", voice: "electric", fx: "ragged" },
|
|
4797
|
+
{ word: "gtr-lead", instrument: "pluck", voice: "electric", fx: "lead" },
|
|
4798
|
+
{ word: "gtr-metal", instrument: "pluck", voice: "electric", fx: "metal" },
|
|
4799
|
+
{ word: "bachata", instrument: "pluck", voice: "electric", fx: "bachata" },
|
|
4800
|
+
// granular (f06-granular): the instrument and its texture presets. Each
|
|
4801
|
+
// starts from a built-in synth source, so nothing downloads.
|
|
4802
|
+
{
|
|
4803
|
+
word: "granular",
|
|
4804
|
+
instrument: "granular",
|
|
4805
|
+
field: "granular",
|
|
4806
|
+
preset: "cloud",
|
|
4807
|
+
},
|
|
4808
|
+
{
|
|
4809
|
+
word: "grains",
|
|
4810
|
+
instrument: "granular",
|
|
4811
|
+
field: "granular",
|
|
4812
|
+
preset: "cloud",
|
|
4813
|
+
},
|
|
4814
|
+
{ word: "cloud", instrument: "granular", field: "granular", preset: "cloud" },
|
|
4815
|
+
{
|
|
4816
|
+
word: "sparkle",
|
|
4817
|
+
instrument: "granular",
|
|
4818
|
+
field: "granular",
|
|
4819
|
+
preset: "sparkle",
|
|
4820
|
+
},
|
|
4821
|
+
{ word: "swarm", instrument: "granular", field: "granular", preset: "swarm" },
|
|
4822
|
+
{
|
|
4823
|
+
word: "microloop",
|
|
4824
|
+
instrument: "granular",
|
|
4825
|
+
field: "granular",
|
|
4826
|
+
preset: "microloop",
|
|
4827
|
+
},
|
|
4828
|
+
// keys (f06-piano): modelled pianos. `piano` stays legacy here; the typed
|
|
4829
|
+
// surfaces store a new `piano` as `grand` (core/keys.ts `pianoWrite`).
|
|
4830
|
+
{ word: "grand", instrument: "grand", field: "keys", preset: "grand" },
|
|
4831
|
+
{ word: "ballad", instrument: "grand", field: "keys", preset: "ballad" },
|
|
4832
|
+
{ word: "upright", instrument: "upright", field: "keys", preset: "upright" },
|
|
4833
|
+
{ word: "felt", instrument: "felt", field: "keys", preset: "felt" },
|
|
4834
|
+
{ word: "lofi", instrument: "felt", field: "keys", preset: "lofi" },
|
|
4835
|
+
{
|
|
4836
|
+
word: "honkytonk",
|
|
4837
|
+
instrument: "honkytonk",
|
|
4838
|
+
field: "keys",
|
|
4839
|
+
preset: "honkytonk",
|
|
4840
|
+
},
|
|
4841
|
+
{
|
|
4842
|
+
word: "prepared",
|
|
4843
|
+
instrument: "prepared",
|
|
4844
|
+
field: "keys",
|
|
4845
|
+
preset: "prepared",
|
|
4846
|
+
},
|
|
4847
|
+
// f06-modal: mallets and bells (core/resonators.ts). `marimba` is legacy;
|
|
4848
|
+
// `modal` alone gives the modal marimba.
|
|
4849
|
+
{ word: "modal", instrument: "modal", field: "modal", preset: "marimba" },
|
|
4850
|
+
{ word: "vibes", instrument: "modal", field: "modal", preset: "vibes" },
|
|
4851
|
+
{ word: "vibraphone", instrument: "modal", field: "modal", preset: "vibes" },
|
|
4852
|
+
{
|
|
4853
|
+
word: "xylophone",
|
|
4854
|
+
instrument: "modal",
|
|
4855
|
+
field: "modal",
|
|
4856
|
+
preset: "xylophone",
|
|
4857
|
+
},
|
|
4858
|
+
{ word: "glock", instrument: "modal", field: "modal", preset: "glock" },
|
|
4859
|
+
{
|
|
4860
|
+
word: "glockenspiel",
|
|
4861
|
+
instrument: "modal",
|
|
4862
|
+
field: "modal",
|
|
4863
|
+
preset: "glock",
|
|
4864
|
+
},
|
|
4865
|
+
{ word: "celesta", instrument: "modal", field: "modal", preset: "celesta" },
|
|
4866
|
+
{ word: "chimes", instrument: "modal", field: "modal", preset: "chimes" },
|
|
4867
|
+
{ word: "tubular", instrument: "modal", field: "modal", preset: "chimes" },
|
|
4868
|
+
{ word: "kalimba", instrument: "modal", field: "modal", preset: "kalimba" },
|
|
4869
|
+
{
|
|
4870
|
+
word: "thumbpiano",
|
|
4871
|
+
instrument: "modal",
|
|
4872
|
+
field: "modal",
|
|
4873
|
+
preset: "kalimba",
|
|
4874
|
+
},
|
|
4875
|
+
{ word: "mbira", instrument: "modal", field: "modal", preset: "mbira" },
|
|
4876
|
+
{ word: "steelpan", instrument: "modal", field: "modal", preset: "steelpan" },
|
|
4877
|
+
{ word: "bowl", instrument: "modal", field: "modal", preset: "bowl" },
|
|
4878
|
+
{ word: "gong", instrument: "modal", field: "modal", preset: "gong" },
|
|
4879
|
+
{ word: "gongageng", instrument: "modal", field: "modal", preset: "gong" },
|
|
4880
|
+
{ word: "timpani", instrument: "modal", field: "modal", preset: "timpani" },
|
|
4881
|
+
{
|
|
4882
|
+
word: "steeldrum",
|
|
4883
|
+
instrument: "modal",
|
|
4884
|
+
field: "modal",
|
|
4885
|
+
preset: "steelpan",
|
|
4886
|
+
},
|
|
4887
|
+
{ word: "singingbowl", instrument: "modal", field: "modal", preset: "bowl" },
|
|
4888
|
+
{
|
|
4889
|
+
word: "kettledrum",
|
|
4890
|
+
instrument: "modal",
|
|
4891
|
+
field: "modal",
|
|
4892
|
+
preset: "timpani",
|
|
4893
|
+
},
|
|
4894
|
+
{
|
|
4895
|
+
word: "tubularbells",
|
|
4896
|
+
instrument: "modal",
|
|
4897
|
+
field: "modal",
|
|
4898
|
+
preset: "chimes",
|
|
4899
|
+
},
|
|
4900
|
+
]);
|
|
4901
|
+
|
|
4902
|
+
/**
|
|
4903
|
+
* What an instrument word means: a legacy word is itself, a row word is its
|
|
4904
|
+
* row, anything else is undefined (callers keep the word as typed).
|
|
4905
|
+
*/
|
|
4906
|
+
function resolveInstrumentWord(word: string): InstrumentWord | undefined {
|
|
4907
|
+
if (LEGACY_WORDS.includes(word)) return Object.freeze({ instrument: word });
|
|
4908
|
+
const row = INSTRUMENT_WORDS.find((entry) => entry.word === word);
|
|
4909
|
+
if (!row) return undefined;
|
|
4910
|
+
const { word: _word, voice, ...meaning } = row;
|
|
4911
|
+
const borrowed =
|
|
4912
|
+
voice === undefined
|
|
4913
|
+
? undefined
|
|
4914
|
+
: INSTRUMENT_WORDS.find((entry) => entry.word === voice && !entry.voice);
|
|
4915
|
+
if (!borrowed) return Object.freeze(meaning);
|
|
4916
|
+
return Object.freeze({
|
|
4917
|
+
instrument: borrowed.instrument,
|
|
4918
|
+
...(borrowed.field === undefined ? {} : { field: borrowed.field }),
|
|
4919
|
+
...(borrowed.preset === undefined ? {} : { preset: borrowed.preset }),
|
|
4920
|
+
...(meaning.fx === undefined ? {} : { fx: meaning.fx }),
|
|
4921
|
+
});
|
|
4922
|
+
}
|
|
4923
|
+
|
|
4924
|
+
/** The `Track.instrument` value a word stores (the word itself if unknown). */
|
|
4925
|
+
function instrumentForWord(word: string): string {
|
|
4926
|
+
return resolveInstrumentWord(word)?.instrument ?? word;
|
|
4927
|
+
}
|
|
4928
|
+
// END instrument words
|
|
4929
|
+
|
|
3837
4930
|
// BEGIN chord engine: generated from core/chords.ts by core/sdk/sync-chords.ts
|
|
3838
4931
|
// ---------------------------------------------------------------------------
|
|
3839
4932
|
// Vocabulary
|