@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.
- package/CHANGELOG.md +63 -0
- package/DAWG.md +592 -49
- package/README.md +4 -4
- package/core/chords.ts +492 -4
- package/core/diff.ts +9 -0
- package/core/expression.ts +143 -6
- package/core/fx.ts +689 -2
- package/core/granular.ts +619 -0
- package/core/instruments.ts +442 -0
- package/core/keys.ts +955 -0
- package/core/resonators.ts +862 -0
- package/core/score.ts +529 -8
- package/core/sdk/eval-child.ts +2 -0
- package/core/sdk/print.ts +247 -16
- package/core/sdk/sync-instruments.ts +58 -0
- package/core/sdk/v1.ts +2417 -40
- package/core/sections.ts +44 -10
- package/core/strings.ts +1080 -0
- package/core/winds.ts +652 -0
- package/guides/automation.md +1 -0
- package/guides/chords.md +3 -1
- package/guides/effects.md +5 -2
- package/guides/media.md +1 -1
- package/guides/performance.md +3 -0
- package/guides/resample.md +26 -0
- package/guides/sounds.md +7 -2
- package/guides/tempo.md +1 -0
- package/package.json +1 -1
- package/src/agent/agent.ts +13 -0
- package/src/agent/chord-tools.ts +153 -1
- package/src/agent/expression-tools.ts +91 -0
- package/src/agent/granular-tools.ts +138 -0
- package/src/agent/models.ts +4 -4
- package/src/agent/ops.ts +42 -2
- package/src/agent/preview-tool.ts +7 -1
- package/src/agent/resample-tool.ts +131 -0
- package/src/agent/tools.ts +726 -17
- package/src/agent/xcb-agent.ts +13 -0
- package/src/audio/arrange.ts +42 -3
- package/src/audio/dsp/bank.ts +233 -0
- package/src/audio/dsp/envelope.ts +161 -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/shift.ts +256 -0
- package/src/audio/dsp/stft.ts +73 -0
- package/src/audio/dsp/window.ts +77 -0
- package/src/audio/effects/chain.ts +23 -3
- package/src/audio/effects/common.ts +14 -0
- package/src/audio/effects/convolution.ts +106 -1
- package/src/audio/effects/gaze.ts +354 -0
- 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 +56 -6
- package/src/audio/fit.ts +447 -0
- package/src/audio/granular.ts +995 -0
- package/src/audio/instrument-check.ts +137 -0
- package/src/audio/instruments.ts +140 -0
- package/src/audio/keys/dsp.ts +323 -0
- package/src/audio/keys/electric.ts +420 -0
- package/src/audio/keys/engine.ts +485 -0
- package/src/audio/keys/organ.ts +1335 -0
- package/src/audio/keys/piano.ts +476 -0
- package/src/audio/keys/sympathetic.ts +127 -0
- package/src/audio/live-worker.ts +56 -0
- package/src/audio/live.ts +361 -15
- package/src/audio/loudness.ts +65 -20
- package/src/audio/preview.ts +17 -1
- package/src/audio/resample.ts +285 -0
- package/src/audio/resonators.ts +295 -0
- package/src/audio/sampler.ts +328 -35
- package/src/audio/samples.ts +56 -8
- package/src/audio/strings/body.ts +263 -0
- package/src/audio/strings/bow.ts +656 -0
- package/src/audio/strings/engine.ts +615 -0
- package/src/audio/strings/loop.ts +119 -0
- package/src/audio/strings/measure.test-helpers.ts +200 -0
- package/src/audio/strings/pluck.ts +354 -0
- package/src/audio/warp.ts +61 -0
- package/src/audio/wav.ts +191 -19
- package/src/audio/winds/engine.ts +302 -0
- package/src/audio/winds/filters.ts +153 -0
- package/src/audio/winds/pitch.ts +104 -0
- package/src/audio/winds/trim.ts +80 -0
- package/src/audio/winds/trims.ts +917 -0
- package/src/audio/winds/voice.ts +479 -0
- package/src/commands/expression.ts +111 -23
- package/src/commands/fit.ts +135 -0
- package/src/commands/fx.ts +35 -1
- package/src/commands/granular.ts +432 -0
- package/src/commands/help.ts +159 -5
- package/src/commands/keys.ts +629 -0
- package/src/commands/modal.ts +310 -0
- package/src/commands/resample.ts +281 -0
- package/src/commands/rig.ts +264 -0
- package/src/commands/sample.ts +52 -2
- package/src/commands/shift.ts +119 -0
- package/src/commands/string.ts +201 -0
- package/src/commands/strum.ts +473 -0
- package/src/commands/time.ts +4 -1
- package/src/commands/wind.ts +244 -0
- package/src/main.ts +339 -15
- package/src/project/check.ts +17 -0
- package/src/render.ts +49 -4
- package/src/session/presence.ts +16 -3
- package/src/tui/audition.ts +2 -2
- package/src/tui/granular-menu.ts +278 -0
- package/src/tui/menu.ts +934 -9
- package/src/tui/modal-menu.ts +145 -0
- package/src/tui/performance-menu.ts +42 -0
- package/src/tui/play-chords.ts +104 -3
- package/src/tui/play-mode.ts +1 -0
- package/src/tui/play-session.ts +70 -2
- package/src/tui/wind-menu.ts +144 -0
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.31.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,36 @@ 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;
|
|
1286
|
+
/** Pitch shift in semitones at the same length (−24..24, SDK 1.29.0). */
|
|
1287
|
+
shift?: number;
|
|
1288
|
+
/** Formant shift in semitones with `shift` (SDK 1.29.0): absent follows the pitch, 0 keeps the formants. */
|
|
1289
|
+
formant?: number;
|
|
1290
|
+
/** Like Strudel `fadeTime`: release fade in seconds (0..2, SDK 1.29.0). */
|
|
1291
|
+
fadeTime?: number;
|
|
1292
|
+
/** Like Strudel `fadeInTime`: attack fade in seconds (0..2, SDK 1.29.0). */
|
|
1293
|
+
fadeInTime?: number;
|
|
1294
|
+
/** Where `resample` rendered the file from (informational, SDK 1.29.0). */
|
|
1295
|
+
from?: SampleProvenanceSpec;
|
|
1296
|
+
/** Velocity layer (SDK 1.31.0, SFZ lovel/hivel): MIDI velocities `[lo, hi]` this voice plays. */
|
|
1297
|
+
vel?: readonly [number, number];
|
|
1298
|
+
/** Round-robin group (SDK 1.31.0, SFZ seq_length): voices in a group take turns, A B A B. */
|
|
1299
|
+
rr?: string;
|
|
1300
|
+
}>;
|
|
1301
|
+
|
|
1302
|
+
/** `SampleSpec.from` (SDK 1.29.0): the source of a resampled file. */
|
|
1303
|
+
export type SampleProvenanceSpec = Readonly<{
|
|
1304
|
+
/** `track:<id>`, `orbit:<n>` or `master`. */
|
|
1305
|
+
source: string;
|
|
1306
|
+
section?: string;
|
|
1307
|
+
bars?: readonly [number, number];
|
|
1308
|
+
/** sha256 of the score the file was rendered from. */
|
|
1309
|
+
score: string;
|
|
1280
1310
|
}>;
|
|
1281
1311
|
|
|
1282
1312
|
/** Result of `sampler()`; pass it as a track's `instrument`. */
|
|
@@ -1317,7 +1347,7 @@ export function sampler(
|
|
|
1317
1347
|
throw new DawgSdkError(
|
|
1318
1348
|
`sampler voice "${name}" must be a short identifier`,
|
|
1319
1349
|
);
|
|
1320
|
-
out[name] =
|
|
1350
|
+
out[name] = sampleSpec(voices[name]!, name);
|
|
1321
1351
|
}
|
|
1322
1352
|
return Object.freeze({ kind: "sampler", voices: Object.freeze(out), mode });
|
|
1323
1353
|
}
|
|
@@ -1433,6 +1463,551 @@ export function wavetable(
|
|
|
1433
1463
|
return Object.freeze(out) as WavetableSpec;
|
|
1434
1464
|
}
|
|
1435
1465
|
|
|
1466
|
+
/** Instrument name of the 0.6 string engine (`Track.string`). */
|
|
1467
|
+
export const STRING_INSTRUMENT = "string";
|
|
1468
|
+
|
|
1469
|
+
/**
|
|
1470
|
+
* String engine settings (SDK 1.21.0): a `preset` (`nylon`, `steel`,
|
|
1471
|
+
* `electric`, `jangle`, `ebass`, `slap`, `upright`, `sitar`, `tanpura`,
|
|
1472
|
+
* `harpsichord`, `lute`, `oud`, `setar`, `tar`, `santur`, `dulcimer`, `koto`,
|
|
1473
|
+
* `harp`, `banjo`, `tres`, `requinto`; bowed since SDK 1.31.0: `violin`,
|
|
1474
|
+
* `viola`, `cello`, `contrabass`, `fiddle`, `erhu`, `kamancheh`, `violins`,
|
|
1475
|
+
* `violas`, `cellos`, `contrabasses`, `pizz`, `trem`) plus any parameter to
|
|
1476
|
+
* override (`ring`, `bright`, `damp`, `pos`, `mute`, `buzz`, `body`, `sym`,
|
|
1477
|
+
* bow: `exciter: "bow"`, `pressure`, `speed`, `attack`, `vib`, `vibmod`,
|
|
1478
|
+
* `vibdelay`, `tremhz`, `sord`, `dyn`). dawg validates names and ranges;
|
|
1479
|
+
* see **Strings** in DAWG.md.
|
|
1480
|
+
*/
|
|
1481
|
+
export type StringInput = Readonly<
|
|
1482
|
+
{ preset?: string } & Record<string, number | string | undefined>
|
|
1483
|
+
>;
|
|
1484
|
+
|
|
1485
|
+
/** Result of `stringed()`; pass it as a track's `instrument`. */
|
|
1486
|
+
export type StringSpec = Readonly<{ kind: "string" } & StringInput>;
|
|
1487
|
+
|
|
1488
|
+
/**
|
|
1489
|
+
* A plucked or bowed string instrument (SDK 1.21.0; bowed and the object
|
|
1490
|
+
* form 1.31.0): a preset and overrides.
|
|
1491
|
+
*
|
|
1492
|
+
* instrument: stringed("nylon")
|
|
1493
|
+
* instrument: stringed("sitar", { buzz: 0.8, sym: 0.5 })
|
|
1494
|
+
* instrument: stringed({ preset: "cello", vib: 6 })
|
|
1495
|
+
*/
|
|
1496
|
+
export function stringed(
|
|
1497
|
+
preset: string | StringInput = "nylon",
|
|
1498
|
+
params: Readonly<Record<string, number | string>> = {},
|
|
1499
|
+
): StringSpec {
|
|
1500
|
+
if (isRecord(preset)) {
|
|
1501
|
+
const { preset: name, ...rest } = preset as Record<string, unknown>;
|
|
1502
|
+
return stringed(name === undefined ? "nylon" : (name as string), {
|
|
1503
|
+
...(rest as Record<string, number | string>),
|
|
1504
|
+
...params,
|
|
1505
|
+
});
|
|
1506
|
+
}
|
|
1507
|
+
if (typeof preset !== "string" || preset.length === 0)
|
|
1508
|
+
throw new DawgSdkError("stringed needs a preset name");
|
|
1509
|
+
if (!isRecord(params))
|
|
1510
|
+
throw new DawgSdkError("stringed params must be an object");
|
|
1511
|
+
const out: Record<string, number | string> = { kind: "string", preset };
|
|
1512
|
+
for (const [key, value] of Object.entries(params)) {
|
|
1513
|
+
if (value === undefined || key === "kind" || key === "preset") continue;
|
|
1514
|
+
out[key] =
|
|
1515
|
+
typeof value === "string" ? value : finite(value, `string ${key}`);
|
|
1516
|
+
}
|
|
1517
|
+
return Object.freeze(out) as StringSpec;
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
/** Instrument name of the 0.6 granular engine (`Track.granular`). */
|
|
1521
|
+
export const GRANULAR_INSTRUMENT = "granular";
|
|
1522
|
+
|
|
1523
|
+
/**
|
|
1524
|
+
* Granular settings (SDK 1.23.0). `src` is a sample (`sample(...)` shape,
|
|
1525
|
+
* pinned like a sampler voice) or a built-in synth render
|
|
1526
|
+
* `"synth:<preset>[@note]"` (default `synth:pad`, nothing to download);
|
|
1527
|
+
* `preset` is `cloud`, `hold`, `sparkle`, `swarm`, `stutter`, `microloop`,
|
|
1528
|
+
* `backwards` or `dust`; every other key overrides one parameter (`grain`
|
|
1529
|
+
* seconds, `overlap`, `scan`, `pos`, `begin`, `end`, `spray`, `jitter`,
|
|
1530
|
+
* `pitch`, `detune`, `shimmer`, `shimint`, `spread`, `window`, `reverse`,
|
|
1531
|
+
* `freeze`, `repeat`, `hold`, `drift`, `drate`, `attack`, `release`,
|
|
1532
|
+
* `veltone`, `gain`, `seed`, `root`). dawg validates names and ranges; see
|
|
1533
|
+
* **Granular** in DAWG.md.
|
|
1534
|
+
*/
|
|
1535
|
+
export type GranularInput = Readonly<
|
|
1536
|
+
{
|
|
1537
|
+
src?: string | SampleSpec;
|
|
1538
|
+
preset?: string;
|
|
1539
|
+
} & Record<string, number | string | boolean | SampleSpec | undefined>
|
|
1540
|
+
>;
|
|
1541
|
+
|
|
1542
|
+
/** Result of `granular()`; pass it as a track's `instrument`. */
|
|
1543
|
+
export type GranularSpec = Readonly<{ kind: "granular" } & GranularInput>;
|
|
1544
|
+
|
|
1545
|
+
/**
|
|
1546
|
+
* A granular instrument (SDK 1.23.0): an optional preset, then overrides.
|
|
1547
|
+
*
|
|
1548
|
+
* ```ts
|
|
1549
|
+
* instrument: granular("cloud")
|
|
1550
|
+
* instrument: granular("hold", { src: "samples/choir.wav", scan: 0 })
|
|
1551
|
+
* instrument: granular({ src: "synth:bell@72", grain: 0.08, overlap: 6 })
|
|
1552
|
+
* ```
|
|
1553
|
+
*/
|
|
1554
|
+
export function granular(
|
|
1555
|
+
preset?: string | GranularInput,
|
|
1556
|
+
params: GranularInput = {},
|
|
1557
|
+
): GranularSpec {
|
|
1558
|
+
const fields =
|
|
1559
|
+
typeof preset === "object" && preset !== null ? preset : params;
|
|
1560
|
+
if (!isRecord(fields))
|
|
1561
|
+
throw new DawgSdkError("granular params must be an object");
|
|
1562
|
+
const out: Record<string, unknown> = { kind: "granular" };
|
|
1563
|
+
if (typeof preset === "string") {
|
|
1564
|
+
if (preset.length === 0)
|
|
1565
|
+
throw new DawgSdkError("granular needs a preset name");
|
|
1566
|
+
out.preset = preset;
|
|
1567
|
+
} else if (preset !== undefined && (typeof preset !== "object" || !preset))
|
|
1568
|
+
throw new DawgSdkError("granular takes a preset name or params");
|
|
1569
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
1570
|
+
if (value === undefined || key === "kind") continue;
|
|
1571
|
+
if (key === "src")
|
|
1572
|
+
out.src =
|
|
1573
|
+
typeof value === "string" && value.startsWith("synth:")
|
|
1574
|
+
? value
|
|
1575
|
+
: sampleSpec(value as string | SampleSpec, "granular src");
|
|
1576
|
+
else if (key === "preset" && typeof preset === "string") continue;
|
|
1577
|
+
else if (key === "root" && typeof value === "string")
|
|
1578
|
+
out.root = midi(value as Pitch);
|
|
1579
|
+
else if (
|
|
1580
|
+
typeof value === "number" ||
|
|
1581
|
+
typeof value === "string" ||
|
|
1582
|
+
typeof value === "boolean"
|
|
1583
|
+
)
|
|
1584
|
+
out[key] =
|
|
1585
|
+
typeof value === "number" ? finite(value, `granular ${key}`) : value;
|
|
1586
|
+
else throw new DawgSdkError(`granular ${key} must be a value`);
|
|
1587
|
+
}
|
|
1588
|
+
return Object.freeze(out) as GranularSpec;
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
/** Instrument name that selects the modal mallet-and-bell engine (SDK 1.25.0). */
|
|
1592
|
+
export const MODAL_INSTRUMENT = "modal";
|
|
1593
|
+
|
|
1594
|
+
/** Modal presets (dawg's core/resonators.ts). */
|
|
1595
|
+
export type ModalPresetName =
|
|
1596
|
+
| "marimba"
|
|
1597
|
+
| "vibes"
|
|
1598
|
+
| "xylophone"
|
|
1599
|
+
| "glock"
|
|
1600
|
+
| "celesta"
|
|
1601
|
+
| "chimes"
|
|
1602
|
+
| "kalimba"
|
|
1603
|
+
| "mbira"
|
|
1604
|
+
| "steelpan"
|
|
1605
|
+
| "bowl"
|
|
1606
|
+
| "gong"
|
|
1607
|
+
| "timpani"
|
|
1608
|
+
// SDK 1.30.0: gamelan, small bells and frame drums.
|
|
1609
|
+
| "crotales"
|
|
1610
|
+
| "musicbox"
|
|
1611
|
+
| "toypiano"
|
|
1612
|
+
| "saron"
|
|
1613
|
+
| "demung"
|
|
1614
|
+
| "slenthem"
|
|
1615
|
+
| "gangsa"
|
|
1616
|
+
| "gender"
|
|
1617
|
+
| "bonang"
|
|
1618
|
+
| "kenong"
|
|
1619
|
+
| "kethuk"
|
|
1620
|
+
| "kempul"
|
|
1621
|
+
| "daf"
|
|
1622
|
+
| "bodhran"
|
|
1623
|
+
| "tabla";
|
|
1624
|
+
|
|
1625
|
+
/** Modal overrides; omitted means the preset's value. dawg validates ranges. */
|
|
1626
|
+
export type ModalParams = Readonly<{
|
|
1627
|
+
/** `yarn` `cord` `rubber` `plastic` `brass`: sets hardness. */
|
|
1628
|
+
mallet?: "yarn" | "cord" | "rubber" | "plastic" | "brass";
|
|
1629
|
+
/** Mallet hardness 0..1: brighter, shorter contact. */
|
|
1630
|
+
hardness?: number;
|
|
1631
|
+
/** Strike position 0..1 (0.5 is the bar's centre). */
|
|
1632
|
+
position?: number;
|
|
1633
|
+
/** Fundamental ring time (T60 seconds). */
|
|
1634
|
+
ring?: number;
|
|
1635
|
+
/** How much faster upper modes die (octaves of decay per octave). */
|
|
1636
|
+
tilt?: number;
|
|
1637
|
+
/** Damping on note-off 0..1 (0 lets the bar ring). */
|
|
1638
|
+
damp?: number;
|
|
1639
|
+
/** Choke time after note-off, seconds. */
|
|
1640
|
+
release?: number;
|
|
1641
|
+
/** Vibraphone motor rate Hz and depth 0..1. */
|
|
1642
|
+
motor?: number;
|
|
1643
|
+
motordepth?: number;
|
|
1644
|
+
/** Beat between paired gamelan modes, Hz. */
|
|
1645
|
+
ombak?: number;
|
|
1646
|
+
/** Mbira buzz 0..1 and mallet click 0..1. */
|
|
1647
|
+
buzz?: number;
|
|
1648
|
+
click?: number;
|
|
1649
|
+
/** Strike pitch bend in semitones and its decay seconds (Strudel penv/pdecay). */
|
|
1650
|
+
strikebend?: number;
|
|
1651
|
+
strikedecay?: number;
|
|
1652
|
+
/** Output level 0..2 (1 is the preset level). */
|
|
1653
|
+
gain?: number;
|
|
1654
|
+
/** Mode table override (`marimba`, `bell`, `gong`, …). */
|
|
1655
|
+
body?: string;
|
|
1656
|
+
/**
|
|
1657
|
+
* The partner track id of an ombak pair (SDK 1.30.0): this track (the
|
|
1658
|
+
* pengisep) sounds `ombak` Hz above it. song() sets the partner's ombak
|
|
1659
|
+
* to 0 (one straight voice) unless the partner gives its own.
|
|
1660
|
+
*/
|
|
1661
|
+
pair?: string;
|
|
1662
|
+
}>;
|
|
1663
|
+
|
|
1664
|
+
const MODAL_KEYS = Object.freeze([
|
|
1665
|
+
"mallet",
|
|
1666
|
+
"hardness",
|
|
1667
|
+
"position",
|
|
1668
|
+
"ring",
|
|
1669
|
+
"tilt",
|
|
1670
|
+
"damp",
|
|
1671
|
+
"release",
|
|
1672
|
+
"motor",
|
|
1673
|
+
"motordepth",
|
|
1674
|
+
"ombak",
|
|
1675
|
+
"buzz",
|
|
1676
|
+
"click",
|
|
1677
|
+
"strikebend",
|
|
1678
|
+
"strikedecay",
|
|
1679
|
+
"gain",
|
|
1680
|
+
"body",
|
|
1681
|
+
] as const);
|
|
1682
|
+
|
|
1683
|
+
/** Mallet words (core/resonators.ts MODAL_MALLETS). */
|
|
1684
|
+
const MODAL_MALLET_WORDS: readonly string[] = Object.freeze([
|
|
1685
|
+
"yarn",
|
|
1686
|
+
"cord",
|
|
1687
|
+
"rubber",
|
|
1688
|
+
"plastic",
|
|
1689
|
+
"brass",
|
|
1690
|
+
]);
|
|
1691
|
+
|
|
1692
|
+
/** Mode tables a `body` override may name (core/resonators.ts MODAL_BODIES). */
|
|
1693
|
+
const MODAL_BODY_WORDS: readonly string[] = Object.freeze([
|
|
1694
|
+
"marimba",
|
|
1695
|
+
"vibraphone",
|
|
1696
|
+
"xylophone",
|
|
1697
|
+
"glockenspiel",
|
|
1698
|
+
"celesta",
|
|
1699
|
+
"chimes",
|
|
1700
|
+
"crotale",
|
|
1701
|
+
"mbira",
|
|
1702
|
+
"kalimba",
|
|
1703
|
+
"musicbox",
|
|
1704
|
+
"toypiano",
|
|
1705
|
+
"saron",
|
|
1706
|
+
"bonang",
|
|
1707
|
+
"gender",
|
|
1708
|
+
"kempul",
|
|
1709
|
+
"gong",
|
|
1710
|
+
"bell",
|
|
1711
|
+
"steelpan",
|
|
1712
|
+
"bowl",
|
|
1713
|
+
"timpani",
|
|
1714
|
+
"tabla",
|
|
1715
|
+
"frame",
|
|
1716
|
+
]);
|
|
1717
|
+
|
|
1718
|
+
/** Numeric ranges (core/resonators.ts MODAL_PARAMS min..max). */
|
|
1719
|
+
const MODAL_RANGES: Readonly<Record<string, readonly [number, number]>> =
|
|
1720
|
+
Object.freeze({
|
|
1721
|
+
hardness: [0, 1],
|
|
1722
|
+
position: [0, 1],
|
|
1723
|
+
ring: [0.05, 30],
|
|
1724
|
+
tilt: [0, 2],
|
|
1725
|
+
damp: [0, 1],
|
|
1726
|
+
release: [0.005, 2],
|
|
1727
|
+
motor: [0, 12],
|
|
1728
|
+
motordepth: [0, 1],
|
|
1729
|
+
ombak: [0, 12],
|
|
1730
|
+
buzz: [0, 1],
|
|
1731
|
+
click: [0, 1],
|
|
1732
|
+
strikebend: [-24, 24],
|
|
1733
|
+
strikedecay: [0.001, 2],
|
|
1734
|
+
gain: [0, 2],
|
|
1735
|
+
});
|
|
1736
|
+
|
|
1737
|
+
/** Modal presets whose default carries an ombak twin bank (core/resonators.ts). */
|
|
1738
|
+
const MODAL_TWIN_PRESETS: readonly string[] = Object.freeze(["gangsa"]);
|
|
1739
|
+
|
|
1740
|
+
const MODAL_PRESET_WORDS: readonly string[] = Object.freeze([
|
|
1741
|
+
"marimba",
|
|
1742
|
+
"vibes",
|
|
1743
|
+
"xylophone",
|
|
1744
|
+
"glock",
|
|
1745
|
+
"celesta",
|
|
1746
|
+
"chimes",
|
|
1747
|
+
"kalimba",
|
|
1748
|
+
"mbira",
|
|
1749
|
+
"steelpan",
|
|
1750
|
+
"bowl",
|
|
1751
|
+
"gong",
|
|
1752
|
+
"timpani",
|
|
1753
|
+
"crotales",
|
|
1754
|
+
"musicbox",
|
|
1755
|
+
"toypiano",
|
|
1756
|
+
"saron",
|
|
1757
|
+
"demung",
|
|
1758
|
+
"slenthem",
|
|
1759
|
+
"gangsa",
|
|
1760
|
+
"gender",
|
|
1761
|
+
"bonang",
|
|
1762
|
+
"kenong",
|
|
1763
|
+
"kethuk",
|
|
1764
|
+
"kempul",
|
|
1765
|
+
"daf",
|
|
1766
|
+
"bodhran",
|
|
1767
|
+
"tabla",
|
|
1768
|
+
]);
|
|
1769
|
+
|
|
1770
|
+
/** Result of `modal()`; pass it as a track's `instrument`. */
|
|
1771
|
+
export type ModalSpec = Readonly<
|
|
1772
|
+
{ kind: "modal"; preset?: ModalPresetName } & ModalParams
|
|
1773
|
+
>;
|
|
1774
|
+
|
|
1775
|
+
/**
|
|
1776
|
+
* Mallets and bells on the modal engine (SDK 1.25.0): a preset and
|
|
1777
|
+
* optional overrides. A preset word alone (`instrument: "vibes"`) is the
|
|
1778
|
+
* same as `modal("vibes")`, except `"marimba"`, which stays the legacy
|
|
1779
|
+
* marimba voice; `modal("marimba")` is the modal one.
|
|
1780
|
+
*
|
|
1781
|
+
* ```ts
|
|
1782
|
+
* instrument: modal("vibes", { motor: 4, hardness: 0.6 })
|
|
1783
|
+
* instrument: modal("marimba", { mallet: "rubber" })
|
|
1784
|
+
* instrument: modal({ ring: 2 }) // default preset (marimba)
|
|
1785
|
+
* ```
|
|
1786
|
+
*/
|
|
1787
|
+
export function modal(
|
|
1788
|
+
preset?: ModalPresetName | ModalParams,
|
|
1789
|
+
params: ModalParams = {},
|
|
1790
|
+
): ModalSpec {
|
|
1791
|
+
const overrides = isRecord(preset) ? preset : params;
|
|
1792
|
+
const name = isRecord(preset) ? undefined : preset;
|
|
1793
|
+
if (!isRecord(overrides))
|
|
1794
|
+
throw new DawgSdkError("modal params must be an object");
|
|
1795
|
+
const out: Record<string, unknown> = { kind: "modal" };
|
|
1796
|
+
if (name !== undefined) {
|
|
1797
|
+
if (typeof name !== "string" || !MODAL_PRESET_WORDS.includes(name))
|
|
1798
|
+
throw new DawgSdkError(
|
|
1799
|
+
`modal preset "${String(name).slice(0, 32)}" is not one of ${MODAL_PRESET_WORDS.join(" ")}`,
|
|
1800
|
+
);
|
|
1801
|
+
out.preset = name;
|
|
1802
|
+
}
|
|
1803
|
+
for (const key of Object.keys(overrides)) {
|
|
1804
|
+
const value = (overrides as Record<string, unknown>)[key];
|
|
1805
|
+
if (value === undefined) continue;
|
|
1806
|
+
if (key === "mallet" || key === "body") {
|
|
1807
|
+
const words = key === "mallet" ? MODAL_MALLET_WORDS : MODAL_BODY_WORDS;
|
|
1808
|
+
if (typeof value !== "string" || !words.includes(value))
|
|
1809
|
+
throw new DawgSdkError(
|
|
1810
|
+
`modal ${key} "${String(value).slice(0, 32)}" is not one of ${words.join(" ")}`,
|
|
1811
|
+
);
|
|
1812
|
+
out[key] = value;
|
|
1813
|
+
} else if (key === "pair") {
|
|
1814
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9_-]{1,64}$/.test(value))
|
|
1815
|
+
throw new DawgSdkError("modal pair must be a track id");
|
|
1816
|
+
out.pair = value;
|
|
1817
|
+
} else if ((MODAL_KEYS as readonly string[]).includes(key)) {
|
|
1818
|
+
const number = finite(value, `modal ${key}`);
|
|
1819
|
+
const [min, max] = MODAL_RANGES[key]!;
|
|
1820
|
+
if (number < min || number > max)
|
|
1821
|
+
throw new DawgSdkError(`modal ${key} must be ${min}..${max}`);
|
|
1822
|
+
out[key] = number;
|
|
1823
|
+
} else
|
|
1824
|
+
throw new DawgSdkError(
|
|
1825
|
+
`modal has no parameter "${key.slice(0, 32)}" (${MODAL_KEYS.join(" ")} pair)`,
|
|
1826
|
+
);
|
|
1827
|
+
}
|
|
1828
|
+
return Object.freeze(out) as ModalSpec;
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
// ---- winds (f061-gamelan-winds, SDK 1.30.0) ----
|
|
1832
|
+
|
|
1833
|
+
/** The instrument value of the wind engine (core/winds.ts). */
|
|
1834
|
+
export const WIND_INSTRUMENT = "wind";
|
|
1835
|
+
|
|
1836
|
+
/** Wind presets (core/winds.ts WIND_PRESET_NAMES). */
|
|
1837
|
+
export type WindPresetName =
|
|
1838
|
+
| "flute"
|
|
1839
|
+
| "recorder"
|
|
1840
|
+
| "whistle"
|
|
1841
|
+
| "ney"
|
|
1842
|
+
| "shakuhachi"
|
|
1843
|
+
| "panpipe"
|
|
1844
|
+
| "suling"
|
|
1845
|
+
| "bansuri"
|
|
1846
|
+
| "clarinet"
|
|
1847
|
+
| "bassclarinet"
|
|
1848
|
+
| "oboe"
|
|
1849
|
+
| "bassoon"
|
|
1850
|
+
| "sax"
|
|
1851
|
+
| "altosax"
|
|
1852
|
+
| "barisax"
|
|
1853
|
+
| "trumpet"
|
|
1854
|
+
| "harmon"
|
|
1855
|
+
| "plunger"
|
|
1856
|
+
| "trombone"
|
|
1857
|
+
| "tuba"
|
|
1858
|
+
| "horn";
|
|
1859
|
+
|
|
1860
|
+
const WIND_PRESET_WORDS: readonly string[] = Object.freeze([
|
|
1861
|
+
"flute",
|
|
1862
|
+
"recorder",
|
|
1863
|
+
"whistle",
|
|
1864
|
+
"ney",
|
|
1865
|
+
"shakuhachi",
|
|
1866
|
+
"panpipe",
|
|
1867
|
+
"suling",
|
|
1868
|
+
"bansuri",
|
|
1869
|
+
"clarinet",
|
|
1870
|
+
"bassclarinet",
|
|
1871
|
+
"oboe",
|
|
1872
|
+
"bassoon",
|
|
1873
|
+
"sax",
|
|
1874
|
+
"altosax",
|
|
1875
|
+
"barisax",
|
|
1876
|
+
"trumpet",
|
|
1877
|
+
"harmon",
|
|
1878
|
+
"plunger",
|
|
1879
|
+
"trombone",
|
|
1880
|
+
"tuba",
|
|
1881
|
+
"horn",
|
|
1882
|
+
]);
|
|
1883
|
+
|
|
1884
|
+
/** Wind overrides (core/winds.ts WIND_PARAMS). */
|
|
1885
|
+
export type WindParams = Readonly<{
|
|
1886
|
+
model?: "jet" | "reed" | "sax" | "lips";
|
|
1887
|
+
breath?: number;
|
|
1888
|
+
noise?: number;
|
|
1889
|
+
attack?: number;
|
|
1890
|
+
release?: number;
|
|
1891
|
+
vib?: number;
|
|
1892
|
+
vibmod?: number;
|
|
1893
|
+
reed?: number;
|
|
1894
|
+
bright?: number;
|
|
1895
|
+
stopped?: boolean;
|
|
1896
|
+
mute?: "open" | "straight" | "cup" | "harmon" | "plunger";
|
|
1897
|
+
wah?: number;
|
|
1898
|
+
wahenv?: number;
|
|
1899
|
+
growl?: number;
|
|
1900
|
+
flutter?: number;
|
|
1901
|
+
players?: number;
|
|
1902
|
+
gain?: number;
|
|
1903
|
+
}>;
|
|
1904
|
+
|
|
1905
|
+
/** Enum words per wind parameter (core/winds.ts WIND_PARAMS). */
|
|
1906
|
+
const WIND_ENUMS: Readonly<Record<string, readonly string[]>> = Object.freeze({
|
|
1907
|
+
model: Object.freeze(["jet", "reed", "sax", "lips"]),
|
|
1908
|
+
mute: Object.freeze(["open", "straight", "cup", "harmon", "plunger"]),
|
|
1909
|
+
});
|
|
1910
|
+
|
|
1911
|
+
/** Numeric ranges (core/winds.ts WIND_PARAMS min..max). */
|
|
1912
|
+
const WIND_RANGES: Readonly<Record<string, readonly [number, number]>> =
|
|
1913
|
+
Object.freeze({
|
|
1914
|
+
breath: [0, 1],
|
|
1915
|
+
noise: [0, 1],
|
|
1916
|
+
attack: [0.001, 2],
|
|
1917
|
+
release: [0.005, 2],
|
|
1918
|
+
vib: [0, 12],
|
|
1919
|
+
vibmod: [0, 1],
|
|
1920
|
+
reed: [0, 1],
|
|
1921
|
+
bright: [0, 1],
|
|
1922
|
+
wah: [0, 1],
|
|
1923
|
+
wahenv: [0, 1],
|
|
1924
|
+
growl: [0, 1],
|
|
1925
|
+
flutter: [0, 1],
|
|
1926
|
+
players: [1, 8],
|
|
1927
|
+
gain: [0, 2],
|
|
1928
|
+
});
|
|
1929
|
+
|
|
1930
|
+
/** Result of `wind()`; pass it as a track's `instrument`. */
|
|
1931
|
+
export type WindSpec = Readonly<
|
|
1932
|
+
{ kind: "wind"; preset?: WindPresetName } & WindParams
|
|
1933
|
+
>;
|
|
1934
|
+
|
|
1935
|
+
/**
|
|
1936
|
+
* Winds and brass on the wind engine (SDK 1.30.0): a preset and optional
|
|
1937
|
+
* overrides. A preset word alone (`instrument: "flute"`) is the same as
|
|
1938
|
+
* `wind("flute")`; the bare word `"wind"` stays the legacy wind tone.
|
|
1939
|
+
*
|
|
1940
|
+
* ```ts
|
|
1941
|
+
* instrument: wind("sax", { breath: 0.8, growl: 0.2 })
|
|
1942
|
+
* instrument: wind("trumpet", { mute: "harmon", players: 3 })
|
|
1943
|
+
* instrument: wind({ bright: 0.3 }) // default preset (flute)
|
|
1944
|
+
* ```
|
|
1945
|
+
*/
|
|
1946
|
+
export function wind(
|
|
1947
|
+
preset?: WindPresetName | WindParams,
|
|
1948
|
+
params: WindParams = {},
|
|
1949
|
+
): WindSpec {
|
|
1950
|
+
const overrides = isRecord(preset) ? preset : params;
|
|
1951
|
+
const name = isRecord(preset) ? undefined : preset;
|
|
1952
|
+
if (!isRecord(overrides))
|
|
1953
|
+
throw new DawgSdkError("wind params must be an object");
|
|
1954
|
+
const out: Record<string, unknown> = { kind: "wind" };
|
|
1955
|
+
if (name !== undefined) {
|
|
1956
|
+
if (typeof name !== "string" || !WIND_PRESET_WORDS.includes(name))
|
|
1957
|
+
throw new DawgSdkError(
|
|
1958
|
+
`wind preset "${String(name).slice(0, 32)}" is not one of ${WIND_PRESET_WORDS.join(" ")}`,
|
|
1959
|
+
);
|
|
1960
|
+
out.preset = name;
|
|
1961
|
+
}
|
|
1962
|
+
for (const key of Object.keys(overrides)) {
|
|
1963
|
+
const value = (overrides as Record<string, unknown>)[key];
|
|
1964
|
+
if (value === undefined) continue;
|
|
1965
|
+
const words = WIND_ENUMS[key];
|
|
1966
|
+
if (words) {
|
|
1967
|
+
if (typeof value !== "string" || !words.includes(value))
|
|
1968
|
+
throw new DawgSdkError(
|
|
1969
|
+
`wind ${key} "${String(value).slice(0, 32)}" is not one of ${words.join(" ")}`,
|
|
1970
|
+
);
|
|
1971
|
+
out[key] = value;
|
|
1972
|
+
} else if (key === "stopped") {
|
|
1973
|
+
if (typeof value !== "boolean")
|
|
1974
|
+
throw new DawgSdkError("wind stopped must be true or false");
|
|
1975
|
+
out.stopped = value;
|
|
1976
|
+
} else if (WIND_RANGES[key]) {
|
|
1977
|
+
const number = finite(value, `wind ${key}`);
|
|
1978
|
+
const [min, max] = WIND_RANGES[key]!;
|
|
1979
|
+
if (number < min || number > max)
|
|
1980
|
+
throw new DawgSdkError(`wind ${key} must be ${min}..${max}`);
|
|
1981
|
+
if (key === "players" && !Number.isInteger(number))
|
|
1982
|
+
throw new DawgSdkError("wind players must be a whole number");
|
|
1983
|
+
out[key] = number;
|
|
1984
|
+
} else
|
|
1985
|
+
throw new DawgSdkError(
|
|
1986
|
+
`wind has no parameter "${key.slice(0, 32)}" (${[...Object.keys(WIND_ENUMS), "stopped", ...Object.keys(WIND_RANGES)].join(" ")})`,
|
|
1987
|
+
);
|
|
1988
|
+
}
|
|
1989
|
+
return Object.freeze(out) as WindSpec;
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
/**
|
|
1993
|
+
* The wind field an instrument makes: `wind(...)`, or a wind preset word
|
|
1994
|
+
* (`"flute"`, `"saxophone"`). The bare word `"wind"` keeps its pre-0.6.1
|
|
1995
|
+
* meaning (the legacy tone) and makes none.
|
|
1996
|
+
*/
|
|
1997
|
+
function trackWind(
|
|
1998
|
+
raw: unknown,
|
|
1999
|
+
): Readonly<{ preset?: WindPresetName } & WindParams> | undefined {
|
|
2000
|
+
if (isRecord(raw) && raw.kind === "wind") {
|
|
2001
|
+
const { kind: _kind, ...fields } = raw as WindSpec;
|
|
2002
|
+
return Object.freeze(fields);
|
|
2003
|
+
}
|
|
2004
|
+
if (typeof raw !== "string" || raw === WIND_INSTRUMENT) return undefined;
|
|
2005
|
+
const meaning = resolveInstrumentWord(raw);
|
|
2006
|
+
if (meaning?.instrument !== WIND_INSTRUMENT || !meaning.preset)
|
|
2007
|
+
return undefined;
|
|
2008
|
+
return Object.freeze({ preset: meaning.preset as WindPresetName });
|
|
2009
|
+
}
|
|
2010
|
+
|
|
1436
2011
|
/**
|
|
1437
2012
|
* `count` equal slices of one file as voices `prefix0 … prefixN-1`, for
|
|
1438
2013
|
* chopped breaks: `sampler(slices("samples/break.wav", 8, "brk"))`, then
|
|
@@ -1445,7 +2020,7 @@ export function slices(
|
|
|
1445
2020
|
): Record<string, SampleSpec> {
|
|
1446
2021
|
if (!Number.isInteger(count) || count < 1 || count > 64)
|
|
1447
2022
|
throw new DawgSdkError("slices count must be an integer 1..64");
|
|
1448
|
-
const base =
|
|
2023
|
+
const base = sampleSpec(src, prefix);
|
|
1449
2024
|
const begin = base.begin ?? 0;
|
|
1450
2025
|
const end = base.end ?? 1;
|
|
1451
2026
|
const span = (end - begin) / count;
|
|
@@ -1460,7 +2035,7 @@ export function slices(
|
|
|
1460
2035
|
return voices;
|
|
1461
2036
|
}
|
|
1462
2037
|
|
|
1463
|
-
function
|
|
2038
|
+
function sampleSpec(value: string | SampleSpec, name: string): SampleSpec {
|
|
1464
2039
|
const spec = typeof value === "string" ? { src: value } : value;
|
|
1465
2040
|
if (!isRecord(spec) || typeof spec.src !== "string" || spec.src.length === 0)
|
|
1466
2041
|
throw new DawgSdkError(`sampler voice ${name} needs a src path`);
|
|
@@ -1483,6 +2058,16 @@ function sample(value: string | SampleSpec, name: string): SampleSpec {
|
|
|
1483
2058
|
fit?: boolean;
|
|
1484
2059
|
accelerate?: number;
|
|
1485
2060
|
squiz?: number;
|
|
2061
|
+
bpm?: number;
|
|
2062
|
+
fitmode?: "repitch" | "beats" | "tones";
|
|
2063
|
+
len?: number;
|
|
2064
|
+
shift?: number;
|
|
2065
|
+
formant?: number;
|
|
2066
|
+
fadeTime?: number;
|
|
2067
|
+
fadeInTime?: number;
|
|
2068
|
+
from?: SampleProvenanceSpec;
|
|
2069
|
+
vel?: readonly [number, number];
|
|
2070
|
+
rr?: string;
|
|
1486
2071
|
} = { src: spec.src };
|
|
1487
2072
|
if (spec.src.startsWith("pack:")) {
|
|
1488
2073
|
if (spec.sha256 !== undefined)
|
|
@@ -1532,9 +2117,57 @@ function sample(value: string | SampleSpec, name: string): SampleSpec {
|
|
|
1532
2117
|
if (spec.accelerate !== undefined)
|
|
1533
2118
|
out.accelerate = finite(spec.accelerate, `${name} accelerate`);
|
|
1534
2119
|
if (spec.squiz !== undefined) out.squiz = finite(spec.squiz, `${name} squiz`);
|
|
2120
|
+
if (spec.bpm !== undefined) out.bpm = finite(spec.bpm, `${name} bpm`);
|
|
2121
|
+
if (spec.fitmode !== undefined) {
|
|
2122
|
+
if (
|
|
2123
|
+
spec.fitmode !== "repitch" &&
|
|
2124
|
+
spec.fitmode !== "beats" &&
|
|
2125
|
+
spec.fitmode !== "tones"
|
|
2126
|
+
)
|
|
2127
|
+
throw new DawgSdkError(
|
|
2128
|
+
`${name} fitmode must be "repitch", "beats" or "tones"`,
|
|
2129
|
+
);
|
|
2130
|
+
out.fitmode = spec.fitmode;
|
|
2131
|
+
}
|
|
2132
|
+
if (spec.len !== undefined) out.len = finite(spec.len, `${name} len`);
|
|
2133
|
+
if (spec.shift !== undefined) out.shift = finite(spec.shift, `${name} shift`);
|
|
2134
|
+
if (spec.formant !== undefined)
|
|
2135
|
+
out.formant = finite(spec.formant, `${name} formant`);
|
|
2136
|
+
if (spec.fadeTime !== undefined)
|
|
2137
|
+
out.fadeTime = finite(spec.fadeTime, `${name} fadeTime`);
|
|
2138
|
+
if (spec.fadeInTime !== undefined)
|
|
2139
|
+
out.fadeInTime = finite(spec.fadeInTime, `${name} fadeInTime`);
|
|
2140
|
+
if (spec.from !== undefined) {
|
|
2141
|
+
if (typeof spec.from !== "object" || spec.from === null)
|
|
2142
|
+
throw new DawgSdkError(`${name} from must be an object`);
|
|
2143
|
+
out.from = spec.from;
|
|
2144
|
+
}
|
|
2145
|
+
if (spec.vel !== undefined) {
|
|
2146
|
+
if (!Array.isArray(spec.vel) || spec.vel.length !== 2)
|
|
2147
|
+
throw new DawgSdkError(`${name} vel must be [lo, hi]`);
|
|
2148
|
+
out.vel = Object.freeze([
|
|
2149
|
+
finite(spec.vel[0], `${name} vel`),
|
|
2150
|
+
finite(spec.vel[1], `${name} vel`),
|
|
2151
|
+
] as const);
|
|
2152
|
+
}
|
|
2153
|
+
if (spec.rr !== undefined) out.rr = text(spec.rr, `${name} rr`);
|
|
1535
2154
|
return Object.freeze(out);
|
|
1536
2155
|
}
|
|
1537
2156
|
|
|
2157
|
+
/**
|
|
2158
|
+
* One sample file with options (SDK 1.20.0), for `sampler({ brk: ... })`:
|
|
2159
|
+
* `sample("samples/break.wav", { bpm: 174, fitmode: "beats" })` plays a
|
|
2160
|
+
* 174 BPM break in time with the song, cut at its hits.
|
|
2161
|
+
*/
|
|
2162
|
+
export function sample(
|
|
2163
|
+
src: string,
|
|
2164
|
+
options: Omit<SampleSpec, "src"> = {},
|
|
2165
|
+
): SampleSpec {
|
|
2166
|
+
if (typeof src !== "string" || src.length === 0)
|
|
2167
|
+
throw new DawgSdkError("sample() needs a src path");
|
|
2168
|
+
return Object.freeze({ ...options, src });
|
|
2169
|
+
}
|
|
2170
|
+
|
|
1538
2171
|
// ---------------------------------------------------------------------------
|
|
1539
2172
|
// Tracks
|
|
1540
2173
|
|
|
@@ -1564,14 +2197,248 @@ export type AutomationInput = Readonly<{
|
|
|
1564
2197
|
wt?: readonly Point[];
|
|
1565
2198
|
}>;
|
|
1566
2199
|
|
|
2200
|
+
/**
|
|
2201
|
+
* Every rig preset (SDK 1.22.0), kept equal to `RIG_PRESETS` in
|
|
2202
|
+
* core/fx.ts by `print-rig.test.ts`: the stomp, head and cab stages plus
|
|
2203
|
+
* the companion effects a few rigs need (funk's and wah's autofilter,
|
|
2204
|
+
* bachata's chorus, jangle's compressor). `spring`'s short room is the
|
|
2205
|
+
* track's `reverb`, not `fx`: give it as
|
|
2206
|
+
* `reverb: { mix: 0.3, size: 0.35, fade: 1.5, predelay: 0, dim: 3500 }`.
|
|
2207
|
+
*/
|
|
2208
|
+
export const RIG_PRESETS: Readonly<
|
|
2209
|
+
Record<
|
|
2210
|
+
string,
|
|
2211
|
+
Readonly<
|
|
2212
|
+
Record<"stomp" | "head" | "cab", EffectParams | undefined> &
|
|
2213
|
+
Readonly<Record<string, EffectParams | undefined>>
|
|
2214
|
+
>
|
|
2215
|
+
>
|
|
2216
|
+
> = Object.freeze({
|
|
2217
|
+
clean: {
|
|
2218
|
+
stomp: undefined,
|
|
2219
|
+
head: { type: "clean", gain: 3, treble: 6 },
|
|
2220
|
+
cab: { type: "1x12" },
|
|
2221
|
+
},
|
|
2222
|
+
crunch: {
|
|
2223
|
+
stomp: undefined,
|
|
2224
|
+
head: { type: "crunch", gain: 5 },
|
|
2225
|
+
cab: { type: "4x12" },
|
|
2226
|
+
},
|
|
2227
|
+
punk: {
|
|
2228
|
+
stomp: undefined,
|
|
2229
|
+
head: { type: "crunch", gain: 7, mid: 6, master: 6 },
|
|
2230
|
+
cab: { type: "4x12", mic: 0.2 },
|
|
2231
|
+
},
|
|
2232
|
+
ragged: {
|
|
2233
|
+
stomp: { type: "face", gain: 6, tone: 0.6 },
|
|
2234
|
+
head: { type: "chime", gain: 4 },
|
|
2235
|
+
cab: { type: "2x12" },
|
|
2236
|
+
},
|
|
2237
|
+
lead: {
|
|
2238
|
+
stomp: { type: "od", gain: 3, tone: 0.5, level: 3 },
|
|
2239
|
+
head: { type: "lead", gain: 6, mid: 6 },
|
|
2240
|
+
cab: { type: "4x12" },
|
|
2241
|
+
},
|
|
2242
|
+
metal: {
|
|
2243
|
+
stomp: { type: "od", gain: 0, tone: 0.6, level: 6 },
|
|
2244
|
+
head: { type: "high", gain: 7, bass: 6, mid: 3, treble: 7, gate: -55 },
|
|
2245
|
+
cab: { type: "4x12", mic: 0.2 },
|
|
2246
|
+
},
|
|
2247
|
+
fuzz: {
|
|
2248
|
+
stomp: { type: "fuzz", gain: 7, tone: 0.5 },
|
|
2249
|
+
head: { type: "clean", gain: 4 },
|
|
2250
|
+
cab: { type: "2x12" },
|
|
2251
|
+
},
|
|
2252
|
+
octave: {
|
|
2253
|
+
stomp: { type: "octave", gain: 6, tone: 0.6, octave: 0.8 },
|
|
2254
|
+
head: { type: "clean", gain: 3 },
|
|
2255
|
+
cab: { type: "1x12" },
|
|
2256
|
+
},
|
|
2257
|
+
funk: {
|
|
2258
|
+
stomp: undefined,
|
|
2259
|
+
head: { type: "clean", gain: 2, treble: 7, presence: 6 },
|
|
2260
|
+
cab: { type: "2x12" },
|
|
2261
|
+
autofilter: {
|
|
2262
|
+
type: "bpf",
|
|
2263
|
+
sync: 0,
|
|
2264
|
+
rate: 0.01,
|
|
2265
|
+
depth: 0,
|
|
2266
|
+
follow: 3,
|
|
2267
|
+
cutoff: 500,
|
|
2268
|
+
resonance: 0.6,
|
|
2269
|
+
},
|
|
2270
|
+
},
|
|
2271
|
+
wah: {
|
|
2272
|
+
stomp: undefined,
|
|
2273
|
+
head: { type: "crunch", gain: 4 },
|
|
2274
|
+
cab: { type: "2x12" },
|
|
2275
|
+
autofilter: {
|
|
2276
|
+
type: "bpf",
|
|
2277
|
+
sync: 0.5,
|
|
2278
|
+
depth: 2,
|
|
2279
|
+
shape: "sine",
|
|
2280
|
+
cutoff: 700,
|
|
2281
|
+
resonance: 0.6,
|
|
2282
|
+
},
|
|
2283
|
+
},
|
|
2284
|
+
bachata: {
|
|
2285
|
+
stomp: undefined,
|
|
2286
|
+
head: { type: "clean", gain: 2, mid: 6, treble: 7 },
|
|
2287
|
+
cab: { type: "1x12", mic: 0.2 },
|
|
2288
|
+
chorus: { rate: 0.8, depth: 0.25, mix: 0.3 },
|
|
2289
|
+
},
|
|
2290
|
+
spring: {
|
|
2291
|
+
stomp: undefined,
|
|
2292
|
+
head: { type: "clean", gain: 3, treble: 6 },
|
|
2293
|
+
cab: { type: "open" },
|
|
2294
|
+
},
|
|
2295
|
+
bassdrive: {
|
|
2296
|
+
stomp: { type: "od", gain: 4, tone: 0.5, mix: 0.6 },
|
|
2297
|
+
head: { type: "bass", gain: 4 },
|
|
2298
|
+
cab: { type: "8x10" },
|
|
2299
|
+
},
|
|
2300
|
+
reese: {
|
|
2301
|
+
stomp: { type: "rat", gain: 3, tone: 0.3, mix: 0.5 },
|
|
2302
|
+
head: { type: "bass", gain: 6, master: 6 },
|
|
2303
|
+
cab: { type: "1x15" },
|
|
2304
|
+
},
|
|
2305
|
+
jangle: {
|
|
2306
|
+
stomp: undefined,
|
|
2307
|
+
head: { type: "chime", gain: 3, treble: 7 },
|
|
2308
|
+
cab: { type: "2x12", mic: 0.2 },
|
|
2309
|
+
compressor: { threshold: -20, ratio: 4, attack: 0.01, release: 0.15 },
|
|
2310
|
+
// The `rig jangle` command adds `double` since 0.6.1; rig("jangle")
|
|
2311
|
+
// keeps its 0.6.0 stages so older song.ts files sound the same. Add
|
|
2312
|
+
// `double: { time: 12, drift: 1.5, width: 0.5 }` for the new one.
|
|
2313
|
+
},
|
|
2314
|
+
alt: {
|
|
2315
|
+
stomp: { type: "rat", gain: 6, tone: 0.4 },
|
|
2316
|
+
head: { type: "crunch", gain: 4 },
|
|
2317
|
+
cab: { type: "4x12" },
|
|
2318
|
+
},
|
|
2319
|
+
// SDK 1.27.0 shoegaze rigs. Their long wash is the track's `reverb`
|
|
2320
|
+
// (see `rigReverb` in the `rig` command), not part of `rig()`.
|
|
2321
|
+
shoegaze: {
|
|
2322
|
+
stomp: { type: "fuzz", gain: 7, tone: 0.45 },
|
|
2323
|
+
head: { type: "chime", gain: 4 },
|
|
2324
|
+
cab: { type: "2x12" },
|
|
2325
|
+
wobble: { depth: 25, rate: 0.4, drift: 0.4 },
|
|
2326
|
+
bloom: { amount: 0.4, harm: 2, delay: 0.6, time: 1 },
|
|
2327
|
+
double: { time: 18, drift: 2.5, width: 0.4 },
|
|
2328
|
+
postgain: { gain: 0.86 },
|
|
2329
|
+
},
|
|
2330
|
+
glide: {
|
|
2331
|
+
stomp: { type: "face", gain: 5, tone: 0.5 },
|
|
2332
|
+
head: { type: "clean", gain: 4, treble: 5 },
|
|
2333
|
+
cab: { type: "2x12" },
|
|
2334
|
+
wobble: { depth: 35, rate: 0.4, drift: 0.4 },
|
|
2335
|
+
postgain: { gain: 0.8 },
|
|
2336
|
+
},
|
|
2337
|
+
dreampop: {
|
|
2338
|
+
stomp: undefined,
|
|
2339
|
+
head: { type: "clean", gain: 2, treble: 6 },
|
|
2340
|
+
cab: { type: "1x12" },
|
|
2341
|
+
chorus: { rate: 0.6, depth: 0.3, mix: 0.35 },
|
|
2342
|
+
double: { time: 14, drift: 2, width: 0.4 },
|
|
2343
|
+
postgain: { gain: 1.45 },
|
|
2344
|
+
},
|
|
2345
|
+
swell: {
|
|
2346
|
+
stomp: undefined,
|
|
2347
|
+
head: { type: "clean", gain: 3, treble: 5 },
|
|
2348
|
+
cab: { type: "1x12" },
|
|
2349
|
+
swell: { time: 0.5 },
|
|
2350
|
+
postgain: { gain: 1.25 },
|
|
2351
|
+
},
|
|
2352
|
+
ebow: {
|
|
2353
|
+
stomp: { type: "od", gain: 3, tone: 0.5, level: 3 },
|
|
2354
|
+
head: { type: "lead", gain: 5, mid: 6 },
|
|
2355
|
+
cab: { type: "4x12" },
|
|
2356
|
+
swell: { time: 0.25 },
|
|
2357
|
+
bloom: { amount: 0.8, harm: 1, delay: 0.1, time: 0.6 },
|
|
2358
|
+
},
|
|
2359
|
+
});
|
|
2360
|
+
|
|
2361
|
+
/**
|
|
2362
|
+
* The long wash a rig preset sets as the track's `reverb` (SDK 1.27.0),
|
|
2363
|
+
* as the `rig` command stores it. `rig()` returns `fx` only, so give it
|
|
2364
|
+
* as `reverb: rigReverb("shoegaze")`; `instrument: "shoegaze"` adds it
|
|
2365
|
+
* when the track has no `reverb` of its own.
|
|
2366
|
+
*/
|
|
2367
|
+
const RIG_REVERBS: Readonly<Record<string, ReverbInput & { mix: number }>> =
|
|
2368
|
+
Object.freeze({
|
|
2369
|
+
spring: { mix: 0.3, size: 0.35, fade: 1.5, predelay: 0, dim: 3500 },
|
|
2370
|
+
shoegaze: { mix: 0.35, size: 0.9, fade: 4, predelay: 0.02, dim: 6000 },
|
|
2371
|
+
glide: { mix: 0.35, size: 0.7, fade: 2.5, predelay: 0.01, dim: 6000 },
|
|
2372
|
+
dreampop: { mix: 0.4, size: 0.85, fade: 5, predelay: 0.03, dim: 7000 },
|
|
2373
|
+
swell: { mix: 0.45, size: 0.8, fade: 4, predelay: 0.02, dim: 6500 },
|
|
2374
|
+
});
|
|
2375
|
+
|
|
2376
|
+
/** A rig preset's track reverb (`spring`, the shoegaze rigs), if any. */
|
|
2377
|
+
export function rigReverb(name: string): ReverbInput | undefined {
|
|
2378
|
+
if (
|
|
2379
|
+
typeof name !== "string" ||
|
|
2380
|
+
!Object.prototype.hasOwnProperty.call(RIG_PRESETS, name)
|
|
2381
|
+
)
|
|
2382
|
+
throw new DawgSdkError(
|
|
2383
|
+
`unknown rig "${String(name)}" (rigs: ${Object.keys(RIG_PRESETS).join(", ")})`,
|
|
2384
|
+
);
|
|
2385
|
+
const values = RIG_REVERBS[name];
|
|
2386
|
+
return values === undefined ? undefined : Object.freeze({ ...values });
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
/**
|
|
2390
|
+
* A guitar rig for a track's `fx` (SDK 1.22.0): the stomp → head → cab
|
|
2391
|
+
* stages of rig preset `name` and its companion effects (as the `rig`
|
|
2392
|
+
* command sets them), with optional per-stage overrides. Spread it
|
|
2393
|
+
* into `fx` next to other effects:
|
|
2394
|
+
*
|
|
2395
|
+
* ```ts
|
|
2396
|
+
* fx: { ...rig("crunch"), chorus: {} }
|
|
2397
|
+
* fx: { ...rig("metal", { head: { gain: 9 } }) }
|
|
2398
|
+
* ```
|
|
2399
|
+
*/
|
|
2400
|
+
export function rig(
|
|
2401
|
+
name: string,
|
|
2402
|
+
overrides: Readonly<
|
|
2403
|
+
Partial<Record<"stomp" | "head" | "cab", EffectParams>>
|
|
2404
|
+
> = {},
|
|
2405
|
+
): FxInput {
|
|
2406
|
+
if (
|
|
2407
|
+
typeof name !== "string" ||
|
|
2408
|
+
!Object.prototype.hasOwnProperty.call(RIG_PRESETS, name)
|
|
2409
|
+
)
|
|
2410
|
+
throw new DawgSdkError(
|
|
2411
|
+
`unknown rig "${String(name)}" (rigs: ${Object.keys(RIG_PRESETS).join(", ")})`,
|
|
2412
|
+
);
|
|
2413
|
+
if (!isRecord(overrides))
|
|
2414
|
+
throw new DawgSdkError("rig overrides must be an object");
|
|
2415
|
+
const out: Record<string, EffectParams> = {};
|
|
2416
|
+
// Companion effects first, then the stages in chain order.
|
|
2417
|
+
for (const [effect, values] of Object.entries(RIG_PRESETS[name]!))
|
|
2418
|
+
if (values && effect !== "stomp" && effect !== "head" && effect !== "cab")
|
|
2419
|
+
out[effect] = Object.freeze({ ...values });
|
|
2420
|
+
for (const stage of ["stomp", "head", "cab"] as const) {
|
|
2421
|
+
const base = RIG_PRESETS[name]![stage];
|
|
2422
|
+
const extra = overrides[stage];
|
|
2423
|
+
if (extra !== undefined && !isRecord(extra))
|
|
2424
|
+
throw new DawgSdkError(`rig ${stage} overrides must be an object`);
|
|
2425
|
+
if (base || extra)
|
|
2426
|
+
out[stage] = Object.freeze({ ...(base ?? {}), ...(extra ?? {}) });
|
|
2427
|
+
}
|
|
2428
|
+
return Object.freeze(out);
|
|
2429
|
+
}
|
|
2430
|
+
|
|
1567
2431
|
/** One effect's parameters; omitted ones take dawg's defaults. */
|
|
1568
2432
|
export type EffectParams = Readonly<Record<string, number | string | boolean>>;
|
|
1569
2433
|
|
|
1570
2434
|
/**
|
|
1571
2435
|
* 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
|
-
*
|
|
2436
|
+
* filter → djf → autofilter → vowel → crush → distort → stomp → head →
|
|
2437
|
+
* cab → tremolo → compressor → pan → phaser → chorus → leslie → postgain →
|
|
2438
|
+
* delay → reverb. The guitar rig (stomp, head, cab; SDK 1.22.0) is easiest
|
|
2439
|
+
* as `...rig("crunch")`.
|
|
2440
|
+
* Keys here: djf, autofilter, vowel, crush, distort, stomp, head, cab,
|
|
2441
|
+
* tremolo, compressor,
|
|
1575
2442
|
* phaser, chorus, leslie, postgain, plus the mix-bus keys `orbit`
|
|
1576
2443
|
* (`{ orbit: 2 }`, SDK 1.9.0) and `duck` (`{ orbit: 2, depth: 0.85 }`:
|
|
1577
2444
|
* this track's onsets duck every other track on that orbit). See
|
|
@@ -1585,6 +2452,106 @@ export type FxInput = Readonly<Record<string, EffectParams>>;
|
|
|
1585
2452
|
* `"synth-<param>"` (e.g. `"synth-lpf"`), read at each note's onset.
|
|
1586
2453
|
* Every parameter, range and default: **Synth** in DAWG.md.
|
|
1587
2454
|
*/
|
|
2455
|
+
/**
|
|
2456
|
+
* Modelled keys settings (SDK 1.24.0), for a track whose instrument is a
|
|
2457
|
+
* piano family (`"grand"`, `"upright"`, `"felt"`, `"honkytonk"`,
|
|
2458
|
+
* `"prepared"`; the words `"ballad"` and `"lofi"` pick presets) or, from
|
|
2459
|
+
* SDK 1.26.0, an electric family (`"epiano"`, `"wurli"`, `"clav"`; presets
|
|
2460
|
+
* epiano suitcase dyno wurli clav funkclav). Every field is optional; `{}`
|
|
2461
|
+
* is the family's own sound. Automate a parameter with
|
|
2462
|
+
* `automation.fx["keys-<param>"]` (hardness, touch, decay, release, knock,
|
|
2463
|
+
* noise, felt; electric tone, vibe, trem), read at each note's onset.
|
|
2464
|
+
* Ranges: **Keys** in DAWG.md.
|
|
2465
|
+
*/
|
|
2466
|
+
export type KeysInput = Readonly<{
|
|
2467
|
+
/**
|
|
2468
|
+
* A named preset: grand ballad upright felt lofi honkytonk prepared, or
|
|
2469
|
+
* electric epiano suitcase dyno wurli clav funkclav.
|
|
2470
|
+
*/
|
|
2471
|
+
preset?: string;
|
|
2472
|
+
/** Hammer hardness 0..1: brightness at a given velocity (0.5). */
|
|
2473
|
+
hardness?: number;
|
|
2474
|
+
/** Velocity sensitivity 0..1 (1). */
|
|
2475
|
+
touch?: number;
|
|
2476
|
+
/** Inharmonicity multiplier 0..4 (1 grand, 2.5 upright, 0 harmonic). */
|
|
2477
|
+
inharm?: number;
|
|
2478
|
+
/** Unison detune in cents 0..30 (0.7; honkytonk 16). */
|
|
2479
|
+
unison?: number;
|
|
2480
|
+
/** Sustain time multiplier 0.1..4 (1). */
|
|
2481
|
+
decay?: number;
|
|
2482
|
+
/** Damper time multiplier 0.1..4 (1). */
|
|
2483
|
+
release?: number;
|
|
2484
|
+
/** Hammer position along the string 0.04..0.3 (0.12). */
|
|
2485
|
+
strike?: number;
|
|
2486
|
+
/** Aftersound share 0..1 (0.3). */
|
|
2487
|
+
after?: number;
|
|
2488
|
+
/** Soundboard knock 0..1 (0.5). */
|
|
2489
|
+
knock?: number;
|
|
2490
|
+
/** Key and damper mechanics 0..1 (0.25). */
|
|
2491
|
+
noise?: number;
|
|
2492
|
+
/** Felt strip 0..1 (0; felt family 1). */
|
|
2493
|
+
felt?: number;
|
|
2494
|
+
/** Share of prepared keys 0..1 (0; prepared family 0.6). */
|
|
2495
|
+
prep?: number;
|
|
2496
|
+
/** Keyboard stereo width 0..1 (0.6). */
|
|
2497
|
+
width?: number;
|
|
2498
|
+
/** Octave stretch 0..1 (1); 0 keeps every key exactly on its tuning. */
|
|
2499
|
+
stretch?: number;
|
|
2500
|
+
/** Body EQ: grand upright felt honkytonk prepared (the family's own). */
|
|
2501
|
+
body?: string;
|
|
2502
|
+
/** Pitch wobble rate in Hz (tape wow), 0 off. */
|
|
2503
|
+
vib?: number;
|
|
2504
|
+
/** Pitch wobble depth in semitones (0.5). */
|
|
2505
|
+
vibmod?: number;
|
|
2506
|
+
// keys-electric (SDK 1.26.0)
|
|
2507
|
+
/** Sympathetic string resonance 0..1 under the sustain pedal (0; pianos). */
|
|
2508
|
+
sym?: number;
|
|
2509
|
+
/** Pickup drive 0..1: growl when played hard (0.35; epiano, wurli). */
|
|
2510
|
+
bark?: number;
|
|
2511
|
+
/** Tine or reed bell ping 0..1 (0.5; epiano, wurli). */
|
|
2512
|
+
bell?: number;
|
|
2513
|
+
/** Output low-pass in Hz, 0 off (electric keys). */
|
|
2514
|
+
tone?: number;
|
|
2515
|
+
/** Clav pickup switch: neck bridge both out ("both"). */
|
|
2516
|
+
pickup?: "neck" | "bridge" | "both" | "out";
|
|
2517
|
+
/** Clav mute slider 0..1 (0). */
|
|
2518
|
+
mute?: number;
|
|
2519
|
+
/** Suitcase stereo vibrato depth 0..1 (0; epiano). */
|
|
2520
|
+
vibe?: number;
|
|
2521
|
+
/** Suitcase vibrato rate in Hz 0.5..12 (4; epiano). */
|
|
2522
|
+
vibehz?: number;
|
|
2523
|
+
// Organs (SDK 1.28.0), for instrument `"tonewheel"`, `"combo"` or
|
|
2524
|
+
// `"pipe"` (presets tonewheel gospel jazzorgan combo vox pipe flutes
|
|
2525
|
+
// cornet reeds celeste). `"organ"` stays the legacy sine voice.
|
|
2526
|
+
/** Tonewheel drawbars, nine digits 0-8, 16' to 1' (`"888000000"`). */
|
|
2527
|
+
drawbars?: string;
|
|
2528
|
+
/** Tonewheel percussion: off 2nd 3rd. */
|
|
2529
|
+
perc?: string;
|
|
2530
|
+
/** Percussion decay: fast slow. */
|
|
2531
|
+
percdecay?: string;
|
|
2532
|
+
/** Key click 0..1. */
|
|
2533
|
+
click?: number;
|
|
2534
|
+
/** Scanner vibrato/chorus: off v1 v2 v3 c1 c2 c3. */
|
|
2535
|
+
scanner?: string;
|
|
2536
|
+
/** Preamp drive 0..1 (lane `keys-drive`). */
|
|
2537
|
+
drive?: number;
|
|
2538
|
+
/** Rotary speaker: slow fast stop (lane `keys-rotary`). */
|
|
2539
|
+
rotary?: string;
|
|
2540
|
+
/** Combo registers, five digits 0-8, 16' 8' 4' 2⅔' 2' (`"08800"`). */
|
|
2541
|
+
registers?: string;
|
|
2542
|
+
/**
|
|
2543
|
+
* Pipe stops: names or a registration (plenum flutes cornet reeds strings
|
|
2544
|
+
* full), as one string or a list (`["principal8", "octave4"]`).
|
|
2545
|
+
*/
|
|
2546
|
+
stops?: string | readonly string[];
|
|
2547
|
+
/** Pipe chiff 0..1. */
|
|
2548
|
+
chiff?: number;
|
|
2549
|
+
/** Pipe wind unsteadiness 0..1. */
|
|
2550
|
+
wind?: number;
|
|
2551
|
+
/** Tremolo depth 0..1: reed piano (0; wurli), pipe tremulant (0; pipe). */
|
|
2552
|
+
trem?: number;
|
|
2553
|
+
}>;
|
|
2554
|
+
|
|
1588
2555
|
export type SynthInput = Readonly<{
|
|
1589
2556
|
attack?: number;
|
|
1590
2557
|
decay?: number;
|
|
@@ -1668,9 +2635,27 @@ export type TrackInput = Readonly<{
|
|
|
1668
2635
|
* `triangle`, and Strudel's `sawtooth`, `supersaw`, `pulse`, `user`,
|
|
1669
2636
|
* `white`, `pink`, `brown`, `crackle`, and the ZzFX sounds `z_sine`,
|
|
1670
2637
|
* `z_triangle`, `z_sawtooth`, `z_square`, `z_tan`, `z_noise`), `kit` for drums,
|
|
1671
|
-
* `sampler(...)` or `wavetable(...)
|
|
2638
|
+
* `sampler(...)` or `wavetable(...)`, or a mallet or bell (`vibes`,
|
|
2639
|
+
* `glock`, `gong`, … or `modal(...)`, SDK 1.25.0). Default `sine`.
|
|
1672
2640
|
*/
|
|
1673
|
-
instrument?:
|
|
2641
|
+
instrument?:
|
|
2642
|
+
| string
|
|
2643
|
+
| SamplerSpec
|
|
2644
|
+
| WavetableSpec
|
|
2645
|
+
| StringSpec
|
|
2646
|
+
| GranularSpec
|
|
2647
|
+
| ModalSpec
|
|
2648
|
+
| WindSpec;
|
|
2649
|
+
/**
|
|
2650
|
+
* The sampler a `granular(...)` track keeps while it grains one of its
|
|
2651
|
+
* voices (SDK 1.23.0); `grain off` plays it again.
|
|
2652
|
+
*/
|
|
2653
|
+
sampler?: SamplerSpec | null;
|
|
2654
|
+
/**
|
|
2655
|
+
* Granular engine (SDK 1.23.0) for an `instrument: "granular"` track, or
|
|
2656
|
+
* use `instrument: granular("cloud", {...})` or a word (`"cloud"`).
|
|
2657
|
+
*/
|
|
2658
|
+
granular?: GranularInput | null;
|
|
1674
2659
|
/**
|
|
1675
2660
|
* Synthesized drum kit for an `instrument: "kit"` track: `syn808`,
|
|
1676
2661
|
* `syn909`, `acoustic`, `lofi`, `electro` or `trap`. Omit for the default
|
|
@@ -1694,6 +2679,17 @@ export type TrackInput = Readonly<{
|
|
|
1694
2679
|
tuning?: TuningInput | null;
|
|
1695
2680
|
/** Synth voice parameters, Strudel names (`{ attack: 0.01, lpf: 800 }`). */
|
|
1696
2681
|
synth?: SynthInput;
|
|
2682
|
+
/**
|
|
2683
|
+
* String engine (SDK 1.21.0) for an `instrument: "string"` track, or use
|
|
2684
|
+
* `instrument: stringed("sitar", {...})` or a preset word (`"nylon"`).
|
|
2685
|
+
*/
|
|
2686
|
+
string?: StringInput | null;
|
|
2687
|
+
/**
|
|
2688
|
+
* Modelled piano settings (SDK 1.24.0) for `instrument: "grand"` and the
|
|
2689
|
+
* other piano families; `{}` is the family's sound. The word `"piano"`
|
|
2690
|
+
* keeps the classic 0.4 tone; use `"grand"` for the modelled piano.
|
|
2691
|
+
*/
|
|
2692
|
+
keys?: KeysInput;
|
|
1697
2693
|
muted?: boolean;
|
|
1698
2694
|
/** When any track is soloed only soloed tracks play. */
|
|
1699
2695
|
solo?: boolean;
|
|
@@ -1720,6 +2716,16 @@ export type TrackInput = Readonly<{
|
|
|
1720
2716
|
glide?: number | Readonly<{ time?: number; mode?: GlideMode }>;
|
|
1721
2717
|
/** Sustain pedal changes as `[beat, "down" | "half" | "up"]` (SDK 1.15.0). */
|
|
1722
2718
|
pedal?: readonly (readonly [number, PedalState])[];
|
|
2719
|
+
/**
|
|
2720
|
+
* Una corda on a modelled piano (SDK 1.26.0): `[beat, "down" | "half" |
|
|
2721
|
+
* "up"]`; while down each note strikes fewer strings, softer and darker.
|
|
2722
|
+
*/
|
|
2723
|
+
softPedal?: readonly (readonly [number, PedalState])[];
|
|
2724
|
+
/**
|
|
2725
|
+
* Sostenuto on a modelled piano (SDK 1.26.0): `[beat, "down" | "up"]`;
|
|
2726
|
+
* holds only the keys already down when it presses.
|
|
2727
|
+
*/
|
|
2728
|
+
sostenuto?: readonly (readonly [number, "down" | "up"])[];
|
|
1723
2729
|
/**
|
|
1724
2730
|
* Velocity response (SDK 1.15.0): `soft` (quiet notes louder), `hard`
|
|
1725
2731
|
* (needs a firm touch), `fixed` (every note at 0.8, like an organ) or
|
|
@@ -1738,6 +2744,12 @@ export type TrackInput = Readonly<{
|
|
|
1738
2744
|
length?: number;
|
|
1739
2745
|
seed?: number;
|
|
1740
2746
|
}>;
|
|
2747
|
+
/**
|
|
2748
|
+
* How chords are fretted on this track (SDK 1.27.0): `{ tune: "dadgad",
|
|
2749
|
+
* capo: 2 }`. dawg's `strum` command and perform mode `guitar` use it;
|
|
2750
|
+
* it changes no sound by itself.
|
|
2751
|
+
*/
|
|
2752
|
+
guitar?: GuitarInput;
|
|
1741
2753
|
/** `note()`/`seq()` for pitched tracks, `hit()`/`hits()` for kits and one-shot samplers. */
|
|
1742
2754
|
notes?: readonly (NoteSpec | HitSpec)[];
|
|
1743
2755
|
/**
|
|
@@ -1833,6 +2845,10 @@ export type TrackSpec = Readonly<{
|
|
|
1833
2845
|
synth: SynthInput | null;
|
|
1834
2846
|
sampler: SamplerSpec | null;
|
|
1835
2847
|
wavetable: WavetableSpec | null;
|
|
2848
|
+
/** String engine settings (SDK 1.21.0); present only when set. */
|
|
2849
|
+
string?: StringInput;
|
|
2850
|
+
/** Granular engine settings (SDK 1.23.0); null when not granular. */
|
|
2851
|
+
granular?: GranularInput | null;
|
|
1836
2852
|
automation: Readonly<Required<AutomationInput>>;
|
|
1837
2853
|
/** Every hit resolved to its pitch slot. */
|
|
1838
2854
|
notes: readonly NoteSpec[];
|
|
@@ -1844,6 +2860,9 @@ export type TrackSpec = Readonly<{
|
|
|
1844
2860
|
/** Performance (SDK 1.15.0); present only when set. */
|
|
1845
2861
|
glide?: Readonly<{ time: number; mode: GlideMode }>;
|
|
1846
2862
|
pedal?: readonly (readonly [number, PedalState])[];
|
|
2863
|
+
/** Soft and sostenuto pedals (SDK 1.26.0); present only when set. */
|
|
2864
|
+
softPedal?: readonly (readonly [number, PedalState])[];
|
|
2865
|
+
sostenuto?: readonly (readonly [number, PedalState])[];
|
|
1847
2866
|
velocityCurve?: Readonly<{
|
|
1848
2867
|
curve: Exclude<VelocityCurveName, "linear">;
|
|
1849
2868
|
fixed?: number;
|
|
@@ -1855,6 +2874,14 @@ export type TrackSpec = Readonly<{
|
|
|
1855
2874
|
seed: number;
|
|
1856
2875
|
}>;
|
|
1857
2876
|
tuning: ScoreTuning | null;
|
|
2877
|
+
/** Modelled piano settings (SDK 1.24.0); present only when set. */
|
|
2878
|
+
keys?: KeysInput;
|
|
2879
|
+
/** Modal settings (SDK 1.25.0); present only on a modal track. */
|
|
2880
|
+
modal?: Readonly<{ preset?: ModalPresetName } & ModalParams>;
|
|
2881
|
+
/** Guitar fretting (SDK 1.27.0); present only when set. */
|
|
2882
|
+
guitar?: GuitarSetup;
|
|
2883
|
+
/** Wind settings (SDK 1.30.0); present only on a wind-engine track. */
|
|
2884
|
+
wind?: Readonly<{ preset?: WindPresetName } & WindParams>;
|
|
1858
2885
|
}>;
|
|
1859
2886
|
|
|
1860
2887
|
export type GlideMode = "legato" | "mono" | "poly";
|
|
@@ -1868,10 +2895,17 @@ const DEFAULT_FIXED_VELOCITY = 0.8;
|
|
|
1868
2895
|
function trackPerformance(
|
|
1869
2896
|
input: TrackInput,
|
|
1870
2897
|
name: string,
|
|
1871
|
-
): Partial<
|
|
2898
|
+
): Partial<
|
|
2899
|
+
Pick<
|
|
2900
|
+
TrackSpec,
|
|
2901
|
+
"glide" | "pedal" | "softPedal" | "sostenuto" | "velocityCurve" | "humanize"
|
|
2902
|
+
>
|
|
2903
|
+
> {
|
|
1872
2904
|
const out: {
|
|
1873
2905
|
glide?: TrackSpec["glide"];
|
|
1874
2906
|
pedal?: TrackSpec["pedal"];
|
|
2907
|
+
softPedal?: TrackSpec["softPedal"];
|
|
2908
|
+
sostenuto?: TrackSpec["sostenuto"];
|
|
1875
2909
|
velocityCurve?: TrackSpec["velocityCurve"];
|
|
1876
2910
|
humanize?: TrackSpec["humanize"];
|
|
1877
2911
|
} = {};
|
|
@@ -1892,29 +2926,38 @@ function trackPerformance(
|
|
|
1892
2926
|
mode,
|
|
1893
2927
|
});
|
|
1894
2928
|
}
|
|
1895
|
-
|
|
1896
|
-
|
|
2929
|
+
const pedalLane = (
|
|
2930
|
+
key: "pedal" | "softPedal" | "sostenuto",
|
|
2931
|
+
states: readonly PedalState[],
|
|
2932
|
+
) => {
|
|
2933
|
+
const value: unknown = input[key];
|
|
2934
|
+
if (value === undefined) return;
|
|
2935
|
+
const shape = `[beat, ${states.map((state) => `"${state}"`).join(" | ")}]`;
|
|
2936
|
+
if (!Array.isArray(value) || value.length > 1024)
|
|
1897
2937
|
throw new DawgSdkError(
|
|
1898
|
-
`track ${name}:
|
|
2938
|
+
`track ${name}: ${key} must be at most 1024 ${shape} events`,
|
|
1899
2939
|
);
|
|
1900
|
-
if (
|
|
1901
|
-
out
|
|
1902
|
-
|
|
2940
|
+
if (value.length > 0)
|
|
2941
|
+
out[key] = Object.freeze(
|
|
2942
|
+
value.map((event: unknown, index: number) => {
|
|
1903
2943
|
if (
|
|
1904
2944
|
!Array.isArray(event) ||
|
|
1905
2945
|
event.length !== 2 ||
|
|
1906
|
-
!
|
|
2946
|
+
!states.includes(event[1] as PedalState)
|
|
1907
2947
|
)
|
|
1908
2948
|
throw new DawgSdkError(
|
|
1909
|
-
`track ${name}:
|
|
2949
|
+
`track ${name}: ${key}[${index}] must be ${shape}`,
|
|
1910
2950
|
);
|
|
1911
2951
|
return Object.freeze([
|
|
1912
|
-
beat(event[0], `${name}
|
|
2952
|
+
beat(event[0], `${name} ${key}[${index}] beat`),
|
|
1913
2953
|
event[1] as PedalState,
|
|
1914
2954
|
] as const);
|
|
1915
2955
|
}),
|
|
1916
2956
|
);
|
|
1917
|
-
}
|
|
2957
|
+
};
|
|
2958
|
+
pedalLane("pedal", ["down", "half", "up"]);
|
|
2959
|
+
pedalLane("softPedal", ["down", "half", "up"]);
|
|
2960
|
+
pedalLane("sostenuto", ["down", "up"]);
|
|
1918
2961
|
if (input.velocityCurve !== undefined) {
|
|
1919
2962
|
const raw =
|
|
1920
2963
|
typeof input.velocityCurve === "string"
|
|
@@ -2056,34 +3099,81 @@ export function track(input: TrackInput): TrackSpec {
|
|
|
2056
3099
|
if (typeof id !== "string" || id.length === 0 || id.length > 64)
|
|
2057
3100
|
throw new DawgSdkError(`track ${name}: id must be 1..64 characters`);
|
|
2058
3101
|
const rawInstrument = input.instrument ?? "sine";
|
|
3102
|
+
// A granular track may keep the sampler it grains (`grain off` goes back).
|
|
3103
|
+
const keptSampler =
|
|
3104
|
+
isRecord(input.sampler) &&
|
|
3105
|
+
input.sampler.kind === "sampler" &&
|
|
3106
|
+
isRecord(rawInstrument) &&
|
|
3107
|
+
rawInstrument.kind === "granular"
|
|
3108
|
+
? localizeSampler(input.sampler as SamplerSpec, slug)
|
|
3109
|
+
: null;
|
|
3110
|
+
if (input.sampler !== undefined && input.sampler !== null && !keptSampler)
|
|
3111
|
+
throw new DawgSdkError(
|
|
3112
|
+
`track ${name}: sampler: is only for a granular(...) track; use instrument: sampler({...})`,
|
|
3113
|
+
);
|
|
2059
3114
|
const samplerSpec =
|
|
2060
3115
|
isRecord(rawInstrument) && rawInstrument.kind === "sampler"
|
|
2061
3116
|
? localizeSampler(rawInstrument as SamplerSpec, slug)
|
|
2062
|
-
:
|
|
3117
|
+
: keptSampler;
|
|
2063
3118
|
const wavetableSpec =
|
|
2064
3119
|
isRecord(rawInstrument) && rawInstrument.kind === "wavetable"
|
|
2065
3120
|
? localizeWavetable(rawInstrument as WavetableSpec, slug)
|
|
2066
3121
|
: null;
|
|
2067
|
-
const
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
3122
|
+
const stringFromInstrument =
|
|
3123
|
+
isRecord(rawInstrument) && rawInstrument.kind === "string"
|
|
3124
|
+
? stringInput(rawInstrument, name)
|
|
3125
|
+
: null;
|
|
3126
|
+
const granularFromInstrument =
|
|
3127
|
+
isRecord(rawInstrument) && rawInstrument.kind === "granular"
|
|
3128
|
+
? granularInput(rawInstrument, name, slug)
|
|
3129
|
+
: null;
|
|
3130
|
+
const word =
|
|
3131
|
+
typeof rawInstrument === "string"
|
|
3132
|
+
? resolveInstrumentWord(rawInstrument)
|
|
3133
|
+
: undefined;
|
|
3134
|
+
const modalSpec = trackModal(rawInstrument);
|
|
3135
|
+
const guitarSpec = guitarInput(input.guitar, `track ${name}`);
|
|
3136
|
+
const windSpec = trackWind(rawInstrument);
|
|
3137
|
+
const instrument = granularFromInstrument
|
|
3138
|
+
? GRANULAR_INSTRUMENT
|
|
3139
|
+
: samplerSpec
|
|
3140
|
+
? SAMPLER_INSTRUMENT
|
|
3141
|
+
: wavetableSpec
|
|
3142
|
+
? WAVETABLE_INSTRUMENT
|
|
3143
|
+
: stringFromInstrument
|
|
3144
|
+
? STRING_INSTRUMENT
|
|
3145
|
+
: modalSpec
|
|
3146
|
+
? MODAL_INSTRUMENT
|
|
3147
|
+
: windSpec
|
|
3148
|
+
? WIND_INSTRUMENT
|
|
3149
|
+
: typeof rawInstrument === "string"
|
|
3150
|
+
? (word?.instrument ?? rawInstrument)
|
|
3151
|
+
: undefined;
|
|
3152
|
+
// A granular word (`"cloud"`) turns the engine on with its preset.
|
|
3153
|
+
const granularSpec =
|
|
3154
|
+
granularInput(input.granular, name, slug) ??
|
|
3155
|
+
granularFromInstrument ??
|
|
3156
|
+
(word?.field === "granular" && word.preset
|
|
3157
|
+
? Object.freeze({ preset: word.preset })
|
|
3158
|
+
: instrument === GRANULAR_INSTRUMENT
|
|
3159
|
+
? Object.freeze({})
|
|
3160
|
+
: null);
|
|
2074
3161
|
if (
|
|
2075
3162
|
instrument === undefined ||
|
|
2076
3163
|
instrument.length === 0 ||
|
|
2077
3164
|
instrument.length > 64
|
|
2078
3165
|
)
|
|
2079
3166
|
throw new DawgSdkError(
|
|
2080
|
-
`track ${name}: instrument must be a voice name, "kit", sampler(...) or
|
|
3167
|
+
`track ${name}: instrument must be a voice name, "kit", sampler(...), wavetable(...), stringed(...), granular(...) or modal(...)`,
|
|
2081
3168
|
);
|
|
2082
3169
|
if (instrument === SAMPLER_INSTRUMENT && !samplerSpec)
|
|
2083
3170
|
throw new DawgSdkError(
|
|
2084
3171
|
`track ${name}: use instrument: sampler({...}) for a sampler track`,
|
|
2085
3172
|
);
|
|
2086
|
-
const slots =
|
|
3173
|
+
const slots =
|
|
3174
|
+
samplerSpec && instrument === SAMPLER_INSTRUMENT
|
|
3175
|
+
? voiceSlots(samplerSpec)
|
|
3176
|
+
: undefined;
|
|
2087
3177
|
const kit = KIT_INSTRUMENTS.includes(instrument.trim().toLowerCase());
|
|
2088
3178
|
const notes = (input.notes ?? []).map((item, index) => {
|
|
2089
3179
|
if (!isRecord(item) || (item.kind !== "note" && item.kind !== "hit"))
|
|
@@ -2158,6 +3248,23 @@ export function track(input: TrackInput): TrackSpec {
|
|
|
2158
3248
|
throw new DawgSdkError(
|
|
2159
3249
|
`track ${name}: unknown automation lane "${key}" (${AUTOMATION_KEYS.join(" ")})`,
|
|
2160
3250
|
);
|
|
3251
|
+
// A keys preset word (`"lofi"`, `"ballad"`) brings its preset's effects,
|
|
3252
|
+
// as the prompt does; explicit filter, fx and reverb win.
|
|
3253
|
+
const presetFx = keysPresetFx(input.keys, rawInstrument);
|
|
3254
|
+
if (presetFx) {
|
|
3255
|
+
input = {
|
|
3256
|
+
...input,
|
|
3257
|
+
...(input.filter === undefined && presetFx.filter
|
|
3258
|
+
? { filter: presetFx.filter }
|
|
3259
|
+
: {}),
|
|
3260
|
+
...(input.reverb === undefined && presetFx.reverb
|
|
3261
|
+
? { reverb: presetFx.reverb }
|
|
3262
|
+
: {}),
|
|
3263
|
+
...(presetFx.fx && input.fx !== null
|
|
3264
|
+
? { fx: { ...presetFx.fx, ...(isRecord(input.fx) ? input.fx : {}) } }
|
|
3265
|
+
: {}),
|
|
3266
|
+
};
|
|
3267
|
+
}
|
|
2161
3268
|
const filter =
|
|
2162
3269
|
input.filter === undefined || input.filter === null
|
|
2163
3270
|
? null
|
|
@@ -2185,6 +3292,13 @@ export function track(input: TrackInput): TrackSpec {
|
|
|
2185
3292
|
`${name} delay`,
|
|
2186
3293
|
),
|
|
2187
3294
|
});
|
|
3295
|
+
// A rig alias with a wash (`instrument: "shoegaze"`) sets the track
|
|
3296
|
+
// reverb the `track shoegaze` prompt sets, unless `reverb` is given.
|
|
3297
|
+
if (input.reverb === undefined && typeof rawInstrument === "string") {
|
|
3298
|
+
const rigName = resolveInstrumentWord(rawInstrument)?.fx;
|
|
3299
|
+
const wash = rigName === undefined ? undefined : RIG_REVERBS[rigName];
|
|
3300
|
+
if (wash) input = { ...input, reverb: wash };
|
|
3301
|
+
}
|
|
2188
3302
|
const reverb =
|
|
2189
3303
|
input.reverb === undefined || input.reverb === null
|
|
2190
3304
|
? null
|
|
@@ -2200,8 +3314,26 @@ export function track(input: TrackInput): TrackSpec {
|
|
|
2200
3314
|
? {}
|
|
2201
3315
|
: { ir: reverbIr(input.reverb.ir, name, slug) }),
|
|
2202
3316
|
});
|
|
2203
|
-
|
|
3317
|
+
// A guitar alias (`instrument: "jangle"`) also loads its rig; stages and
|
|
3318
|
+
// effects the track's own `fx` names win.
|
|
3319
|
+
const aliasRig =
|
|
3320
|
+
typeof rawInstrument === "string"
|
|
3321
|
+
? resolveInstrumentWord(rawInstrument)?.fx
|
|
3322
|
+
: undefined;
|
|
3323
|
+
const fx = fxInput(
|
|
3324
|
+
aliasRig === undefined
|
|
3325
|
+
? input.fx
|
|
3326
|
+
: { ...rig(aliasRig), ...(isRecord(input.fx) ? input.fx : {}) },
|
|
3327
|
+
name,
|
|
3328
|
+
);
|
|
2204
3329
|
const synth = synthInput(input.synth, name);
|
|
3330
|
+
// A string preset word (`"nylon"`) turns the engine on with its preset.
|
|
3331
|
+
const string =
|
|
3332
|
+
stringInput(input.string, name) ??
|
|
3333
|
+
stringFromInstrument ??
|
|
3334
|
+
(word?.field === "string" && word.preset
|
|
3335
|
+
? Object.freeze({ preset: word.preset })
|
|
3336
|
+
: null);
|
|
2205
3337
|
return Object.freeze({
|
|
2206
3338
|
kind: "track",
|
|
2207
3339
|
id,
|
|
@@ -2219,6 +3351,8 @@ export function track(input: TrackInput): TrackSpec {
|
|
|
2219
3351
|
synth,
|
|
2220
3352
|
sampler: samplerSpec,
|
|
2221
3353
|
wavetable: wavetableSpec,
|
|
3354
|
+
...(string ? { string } : {}),
|
|
3355
|
+
...(granularSpec ? { granular: granularSpec } : {}),
|
|
2222
3356
|
automation: Object.freeze({
|
|
2223
3357
|
volume: lane("volume"),
|
|
2224
3358
|
pan: lane("pan"),
|
|
@@ -2235,9 +3369,115 @@ export function track(input: TrackInput): TrackSpec {
|
|
|
2235
3369
|
...trackTime(input.time, name),
|
|
2236
3370
|
...trackPerformance(input, name),
|
|
2237
3371
|
tuning: tuningSpec(input.tuning, `track ${name}`),
|
|
3372
|
+
...keysSpec(input.keys, rawInstrument, name),
|
|
3373
|
+
...(modalSpec ? { modal: modalSpec } : {}),
|
|
3374
|
+
...(guitarSpec ? { guitar: guitarSpec } : {}),
|
|
3375
|
+
...(windSpec ? { wind: windSpec } : {}),
|
|
2238
3376
|
});
|
|
2239
3377
|
}
|
|
2240
3378
|
|
|
3379
|
+
/**
|
|
3380
|
+
* Effects the keys presets set with the voice: a copy of `KEYS_PRESETS`
|
|
3381
|
+
* filter, fx and reverb in core/keys.ts (core/keys.test.ts checks they
|
|
3382
|
+
* match the prompt's `piano <preset>`).
|
|
3383
|
+
*/
|
|
3384
|
+
const KEYS_PRESET_FX: Readonly<
|
|
3385
|
+
Record<
|
|
3386
|
+
string,
|
|
3387
|
+
Readonly<{
|
|
3388
|
+
filter?: FilterInput;
|
|
3389
|
+
fx?: FxInput;
|
|
3390
|
+
reverb?: ReverbInput;
|
|
3391
|
+
}>
|
|
3392
|
+
>
|
|
3393
|
+
> = Object.freeze({
|
|
3394
|
+
ballad: { reverb: { mix: 0.25, size: 0.7 } },
|
|
3395
|
+
felt: { reverb: { mix: 0.2, size: 0.5 } },
|
|
3396
|
+
lofi: {
|
|
3397
|
+
filter: { cutoff: 3500, resonance: 0.1 },
|
|
3398
|
+
fx: { crush: { bits: 10 } },
|
|
3399
|
+
},
|
|
3400
|
+
// f061-organ: the pipe presets sound in a church.
|
|
3401
|
+
pipe: { reverb: { mix: 0.35, size: 0.9 } },
|
|
3402
|
+
flutes: { reverb: { mix: 0.3, size: 0.8 } },
|
|
3403
|
+
cornet: { reverb: { mix: 0.3, size: 0.8 } },
|
|
3404
|
+
reeds: { reverb: { mix: 0.3, size: 0.85 } },
|
|
3405
|
+
celeste: { reverb: { mix: 0.35, size: 0.9 } },
|
|
3406
|
+
});
|
|
3407
|
+
|
|
3408
|
+
/**
|
|
3409
|
+
* The preset effects an instrument word brings: a preset word that is not
|
|
3410
|
+
* also its family (`"lofi"`, `"ballad"`), or any preset word without
|
|
3411
|
+
* `keys` (`"felt"`). A printed track names its family and always prints
|
|
3412
|
+
* `keys`, so print → eval never adds them twice.
|
|
3413
|
+
*/
|
|
3414
|
+
function keysPresetFx(
|
|
3415
|
+
keys: unknown,
|
|
3416
|
+
word: unknown,
|
|
3417
|
+
): (typeof KEYS_PRESET_FX)[string] | undefined {
|
|
3418
|
+
if (typeof word !== "string") return undefined;
|
|
3419
|
+
const meaning = resolveInstrumentWord(word);
|
|
3420
|
+
if (meaning?.field !== "keys" || !meaning.preset) return undefined;
|
|
3421
|
+
if (keys !== undefined && keys !== null && word === meaning.instrument)
|
|
3422
|
+
return undefined;
|
|
3423
|
+
return KEYS_PRESET_FX[meaning.preset];
|
|
3424
|
+
}
|
|
3425
|
+
|
|
3426
|
+
/**
|
|
3427
|
+
* `keys` for `track()`: the input as given, or `{ preset }` when the
|
|
3428
|
+
* instrument word names a keys preset (`"grand"`, `"lofi"`), so the word
|
|
3429
|
+
* alone plays the modelled piano. dawg validates the values.
|
|
3430
|
+
*/
|
|
3431
|
+
function keysSpec(
|
|
3432
|
+
input: unknown,
|
|
3433
|
+
word: unknown,
|
|
3434
|
+
name: string,
|
|
3435
|
+
): { keys?: KeysInput } {
|
|
3436
|
+
const meaning =
|
|
3437
|
+
typeof word === "string" ? resolveInstrumentWord(word) : undefined;
|
|
3438
|
+
const preset = meaning?.field === "keys" ? meaning.preset : undefined;
|
|
3439
|
+
if (input === undefined || input === null)
|
|
3440
|
+
return preset ? { keys: Object.freeze({ preset }) } : {};
|
|
3441
|
+
if (!isRecord(input))
|
|
3442
|
+
throw new DawgSdkError(`track ${name}: keys must be an object`);
|
|
3443
|
+
const out: Record<string, EffectValue> = {};
|
|
3444
|
+
// A preset word (`"lofi"`) keeps its preset under given overrides; a
|
|
3445
|
+
// family word (`"upright"`) with `keys` is exactly the given keys.
|
|
3446
|
+
if (preset && input.preset === undefined && word !== meaning?.instrument)
|
|
3447
|
+
out.preset = preset;
|
|
3448
|
+
for (const [key, value] of Object.entries(input)) {
|
|
3449
|
+
if (value === undefined) continue;
|
|
3450
|
+
// Pipe stops may be a list: stored as one space-separated string.
|
|
3451
|
+
if (key === "stops" && Array.isArray(value)) {
|
|
3452
|
+
if (!value.every((stop) => typeof stop === "string"))
|
|
3453
|
+
throw new DawgSdkError(`track ${name}: keys.stops must be stop names`);
|
|
3454
|
+
out[key] = value.join(" ");
|
|
3455
|
+
continue;
|
|
3456
|
+
}
|
|
3457
|
+
out[key] = effectValue(value, `${name} keys.${key}`);
|
|
3458
|
+
}
|
|
3459
|
+
return { keys: Object.freeze(out) };
|
|
3460
|
+
}
|
|
3461
|
+
|
|
3462
|
+
/**
|
|
3463
|
+
* The modal field an instrument makes: `modal(...)`, or a modal preset
|
|
3464
|
+
* word (`"vibes"`, `"glockenspiel"`). The bare words `"modal"` and
|
|
3465
|
+
* `"marimba"` keep their pre-0.6 meaning and make none.
|
|
3466
|
+
*/
|
|
3467
|
+
function trackModal(
|
|
3468
|
+
raw: unknown,
|
|
3469
|
+
): Readonly<{ preset?: ModalPresetName } & ModalParams> | undefined {
|
|
3470
|
+
if (isRecord(raw) && raw.kind === "modal") {
|
|
3471
|
+
const { kind: _kind, ...fields } = raw as ModalSpec;
|
|
3472
|
+
return Object.freeze(fields);
|
|
3473
|
+
}
|
|
3474
|
+
if (typeof raw !== "string" || raw === MODAL_INSTRUMENT) return undefined;
|
|
3475
|
+
const meaning = resolveInstrumentWord(raw);
|
|
3476
|
+
if (meaning?.instrument !== MODAL_INSTRUMENT || !meaning.preset)
|
|
3477
|
+
return undefined;
|
|
3478
|
+
return Object.freeze({ preset: meaning.preset as ModalPresetName });
|
|
3479
|
+
}
|
|
3480
|
+
|
|
2241
3481
|
function trackTime(input: unknown, name: string): { time?: TrackTimeInput } {
|
|
2242
3482
|
if (input === undefined || input === null) return {};
|
|
2243
3483
|
if (!isRecord(input))
|
|
@@ -2348,6 +3588,21 @@ function fxInput(input: unknown, name: string): FxInput | null {
|
|
|
2348
3588
|
return Object.keys(out).length > 0 ? Object.freeze(out) : null;
|
|
2349
3589
|
}
|
|
2350
3590
|
|
|
3591
|
+
function stringInput(input: unknown, name: string): StringInput | null {
|
|
3592
|
+
if (input === undefined || input === null) return null;
|
|
3593
|
+
if (!isRecord(input))
|
|
3594
|
+
throw new DawgSdkError(`track ${name}: string must be an object`);
|
|
3595
|
+
const out: Record<string, number | string> = {};
|
|
3596
|
+
for (const [key, value] of Object.entries(input)) {
|
|
3597
|
+
if (value === undefined || key === "kind") continue;
|
|
3598
|
+
out[key] =
|
|
3599
|
+
typeof value === "string"
|
|
3600
|
+
? value
|
|
3601
|
+
: finite(value, `${name} string.${key}`);
|
|
3602
|
+
}
|
|
3603
|
+
return Object.freeze(out);
|
|
3604
|
+
}
|
|
3605
|
+
|
|
2351
3606
|
function synthInput(input: unknown, name: string): SynthInput | null {
|
|
2352
3607
|
if (input === undefined || input === null) return null;
|
|
2353
3608
|
if (!isRecord(input))
|
|
@@ -2433,7 +3688,7 @@ function reverbIr(
|
|
|
2433
3688
|
throw new DawgSdkError(`track ${name}: reverb.ir needs a src`);
|
|
2434
3689
|
const src = spec.src.trim().replace(/^\.\//, "");
|
|
2435
3690
|
if (src.startsWith("pack:")) {
|
|
2436
|
-
const ref =
|
|
3691
|
+
const ref = sampleSpec(spec as SampleSpec, `${name} reverb.ir`);
|
|
2437
3692
|
return Object.freeze({
|
|
2438
3693
|
src: ref.src,
|
|
2439
3694
|
...(ref.sha256 ? { sha256: ref.sha256 } : {}),
|
|
@@ -2445,6 +3700,41 @@ function reverbIr(
|
|
|
2445
3700
|
return src.startsWith("tracks/") ? src : `tracks/${slug}/${src}`;
|
|
2446
3701
|
}
|
|
2447
3702
|
|
|
3703
|
+
/** Validates `granular` input and localizes a sample source like a voice. */
|
|
3704
|
+
function granularInput(
|
|
3705
|
+
input: unknown,
|
|
3706
|
+
name: string,
|
|
3707
|
+
slug: string,
|
|
3708
|
+
): GranularInput | null {
|
|
3709
|
+
if (input === undefined || input === null) return null;
|
|
3710
|
+
if (!isRecord(input))
|
|
3711
|
+
throw new DawgSdkError(`track ${name}: granular must be an object`);
|
|
3712
|
+
const out: Record<string, unknown> = {};
|
|
3713
|
+
for (const [key, value] of Object.entries(input)) {
|
|
3714
|
+
if (value === undefined || key === "kind") continue;
|
|
3715
|
+
if (key === "src" && isRecord(value)) {
|
|
3716
|
+
const ref = sampleSpec(value as SampleSpec, `${name} granular src`);
|
|
3717
|
+
const src = ref.src.replace(/^\.\//, "");
|
|
3718
|
+
out.src = Object.freeze({
|
|
3719
|
+
...ref,
|
|
3720
|
+
src:
|
|
3721
|
+
src.startsWith("tracks/") || src.startsWith("pack:")
|
|
3722
|
+
? src
|
|
3723
|
+
: `tracks/${slug}/${src}`,
|
|
3724
|
+
});
|
|
3725
|
+
} else if (key === "src" && typeof value === "string")
|
|
3726
|
+
out.src = value.startsWith("synth:")
|
|
3727
|
+
? value
|
|
3728
|
+
: granularInput({ src: { src: value } }, name, slug)!.src;
|
|
3729
|
+
else if (typeof value === "number")
|
|
3730
|
+
out[key] = finite(value, `${name} granular.${key}`);
|
|
3731
|
+
else if (typeof value === "string" || typeof value === "boolean")
|
|
3732
|
+
out[key] = value;
|
|
3733
|
+
else throw new DawgSdkError(`${name} granular.${key} must be a value`);
|
|
3734
|
+
}
|
|
3735
|
+
return Object.freeze(out) as GranularInput;
|
|
3736
|
+
}
|
|
3737
|
+
|
|
2448
3738
|
/** `./wavetables/x.wav` → `tracks/<slug>/wavetables/x.wav`, like sampler files. */
|
|
2449
3739
|
function localizeWavetable(spec: WavetableSpec, slug: string): WavetableSpec {
|
|
2450
3740
|
const src = spec.table.src;
|
|
@@ -2617,6 +3907,14 @@ export type ScoreSampleRef = Readonly<{
|
|
|
2617
3907
|
fit?: boolean;
|
|
2618
3908
|
accelerate?: number;
|
|
2619
3909
|
squiz?: number;
|
|
3910
|
+
bpm?: number;
|
|
3911
|
+
fitmode?: "repitch" | "beats" | "tones";
|
|
3912
|
+
len?: number;
|
|
3913
|
+
shift?: number;
|
|
3914
|
+
formant?: number;
|
|
3915
|
+
fadeTime?: number;
|
|
3916
|
+
fadeInTime?: number;
|
|
3917
|
+
from?: SampleProvenanceSpec;
|
|
2620
3918
|
}>;
|
|
2621
3919
|
|
|
2622
3920
|
/** A stored track; optional fields are present only when set. */
|
|
@@ -2640,6 +3938,7 @@ export type ScoreTrack = Readonly<{
|
|
|
2640
3938
|
fx?: FxInput;
|
|
2641
3939
|
fxAutomation?: Readonly<Record<string, readonly ScorePoint[]>>;
|
|
2642
3940
|
synth?: SynthInput;
|
|
3941
|
+
keys?: KeysInput;
|
|
2643
3942
|
sampler?: Readonly<{
|
|
2644
3943
|
voices: Readonly<Record<string, ScoreSampleRef>>;
|
|
2645
3944
|
mode: "oneshot" | "keyed";
|
|
@@ -2659,8 +3958,20 @@ export type ScoreTrack = Readonly<{
|
|
|
2659
3958
|
tuning?: ScoreTuning;
|
|
2660
3959
|
wavetable?: Readonly<{ table: ScoreSampleRef } & WavetableParams>;
|
|
2661
3960
|
wtAutomation?: readonly ScorePoint[];
|
|
3961
|
+
/** String engine settings; dawg validates them (SDK 1.21.0). */
|
|
3962
|
+
string?: StringInput;
|
|
3963
|
+
/** Granular settings (SDK 1.23.0); present only when set. */
|
|
3964
|
+
granular?: GranularInput;
|
|
3965
|
+
/** Modal settings (SDK 1.25.0). */
|
|
3966
|
+
modal?: TrackSpec["modal"];
|
|
3967
|
+
/** Guitar fretting (SDK 1.27.0). */
|
|
3968
|
+
guitar?: GuitarSetup;
|
|
3969
|
+
/** Wind settings (SDK 1.30.0). */
|
|
3970
|
+
wind?: TrackSpec["wind"];
|
|
2662
3971
|
glide?: TrackSpec["glide"];
|
|
2663
3972
|
pedal?: readonly Readonly<{ tick: number; state: PedalState }>[];
|
|
3973
|
+
softPedal?: readonly Readonly<{ tick: number; state: PedalState }>[];
|
|
3974
|
+
sostenuto?: readonly Readonly<{ tick: number; state: PedalState }>[];
|
|
2664
3975
|
velocityCurve?: TrackSpec["velocityCurve"];
|
|
2665
3976
|
humanize?: TrackSpec["humanize"];
|
|
2666
3977
|
}>;
|
|
@@ -2926,6 +4237,7 @@ export function song(input: SongInput): Song {
|
|
|
2926
4237
|
stored.wavetable = Object.freeze(fields);
|
|
2927
4238
|
}
|
|
2928
4239
|
if (wtAutomation.length > 0) stored.wtAutomation = wtAutomation;
|
|
4240
|
+
if (t.granular) stored.granular = t.granular;
|
|
2929
4241
|
if (t.sampler)
|
|
2930
4242
|
stored.sampler = Object.freeze({
|
|
2931
4243
|
voices: t.sampler.voices,
|
|
@@ -2949,11 +4261,13 @@ export function song(input: SongInput): Song {
|
|
|
2949
4261
|
if (Object.keys(time).length > 0) stored.time = Object.freeze(time);
|
|
2950
4262
|
}
|
|
2951
4263
|
if (t.glide) stored.glide = t.glide;
|
|
2952
|
-
|
|
4264
|
+
for (const key of ["pedal", "softPedal", "sostenuto"] as const) {
|
|
4265
|
+
const lane = t[key];
|
|
4266
|
+
if (!lane || lane.length === 0) continue;
|
|
2953
4267
|
// One event per tick (the last wins), in tick order, as dawg stores it.
|
|
2954
4268
|
const byTick = new Map<number, PedalState>();
|
|
2955
|
-
for (const [at, state] of
|
|
2956
|
-
stored
|
|
4269
|
+
for (const [at, state] of lane) byTick.set(ticks(at), state);
|
|
4270
|
+
stored[key] = Object.freeze(
|
|
2957
4271
|
[...byTick.entries()]
|
|
2958
4272
|
.sort((a, b) => a[0] - b[0])
|
|
2959
4273
|
.map(([tick, state]) => Object.freeze({ tick, state })),
|
|
@@ -2962,6 +4276,11 @@ export function song(input: SongInput): Song {
|
|
|
2962
4276
|
if (t.velocityCurve) stored.velocityCurve = t.velocityCurve;
|
|
2963
4277
|
if (t.humanize) stored.humanize = t.humanize;
|
|
2964
4278
|
if (t.tuning) stored.tuning = t.tuning;
|
|
4279
|
+
if (t.string) stored.string = t.string;
|
|
4280
|
+
if (t.keys) stored.keys = t.keys;
|
|
4281
|
+
if (t.modal) stored.modal = t.modal;
|
|
4282
|
+
if (t.guitar) stored.guitar = t.guitar;
|
|
4283
|
+
if (t.wind) stored.wind = t.wind;
|
|
2965
4284
|
if (t.rhythm && t.rhythm.length > 0)
|
|
2966
4285
|
stored.rhythm = Object.freeze(
|
|
2967
4286
|
t.rhythm.map((row) => {
|
|
@@ -3006,6 +4325,25 @@ export function song(input: SongInput): Song {
|
|
|
3006
4325
|
);
|
|
3007
4326
|
}
|
|
3008
4327
|
});
|
|
4328
|
+
// An ombak pair (SDK 1.30.0): a partner (pengumbang) whose preset carries
|
|
4329
|
+
// its own ombak twin bank plays one straight voice instead, as `modal
|
|
4330
|
+
// pair` and set_modal leave it, unless it sets ombak itself.
|
|
4331
|
+
for (const track of [...tracks]) {
|
|
4332
|
+
const pair = (track as { modal?: { pair?: string } }).modal?.pair;
|
|
4333
|
+
if (pair === undefined) continue;
|
|
4334
|
+
const at = tracks.findIndex((candidate) => candidate.id === pair);
|
|
4335
|
+
const partner = tracks[at] as
|
|
4336
|
+
(ScoreTrack & { modal?: Readonly<Record<string, unknown>> }) | undefined;
|
|
4337
|
+
if (
|
|
4338
|
+
partner?.modal &&
|
|
4339
|
+
partner.modal.ombak === undefined &&
|
|
4340
|
+
MODAL_TWIN_PRESETS.includes(String(partner.modal.preset))
|
|
4341
|
+
)
|
|
4342
|
+
tracks[at] = Object.freeze({
|
|
4343
|
+
...partner,
|
|
4344
|
+
modal: Object.freeze({ ...partner.modal, ombak: 0 }),
|
|
4345
|
+
}) as ScoreTrack;
|
|
4346
|
+
}
|
|
3009
4347
|
const arrangement: {
|
|
3010
4348
|
sections?: readonly SongSection[];
|
|
3011
4349
|
form?: readonly SongFormEntry[];
|
|
@@ -3670,7 +5008,22 @@ export type ChordOptions = Readonly<{
|
|
|
3670
5008
|
| "arp-random"
|
|
3671
5009
|
| "harp"
|
|
3672
5010
|
| "slop"
|
|
3673
|
-
| "pattern"
|
|
5011
|
+
| "pattern"
|
|
5012
|
+
| "guitar";
|
|
5013
|
+
/**
|
|
5014
|
+
* With `perform: "guitar"` (SDK 1.27.0): the stroke grid, a name (`down`
|
|
5015
|
+
* `folk` `pop` `punk` `funk` `reggae` `waltz` `jangle` `island`) or
|
|
5016
|
+
* characters D (down) U (up) d u (light) x (muted chuck) - . (rest).
|
|
5017
|
+
*/
|
|
5018
|
+
strokes?: string;
|
|
5019
|
+
/** Guitar: grid step in beats, default 0.5 (8ths). */
|
|
5020
|
+
step?: number;
|
|
5021
|
+
/** Guitar: milliseconds a full six-string down stroke takes, default 22. */
|
|
5022
|
+
speed?: number;
|
|
5023
|
+
/** Guitar: the song tempo `speed` converts with, default 120. */
|
|
5024
|
+
tempo?: number;
|
|
5025
|
+
/** Guitar: tuning, capo, hand, ring and position (as a track's `guitar`). */
|
|
5026
|
+
guitar?: GuitarInput;
|
|
3674
5027
|
/**
|
|
3675
5028
|
* With `perform: "pattern"`: a rhythm pattern by name or 1-based number
|
|
3676
5029
|
* (SDK 1.7.0): `eighths`, `sixteenths`, `offbeat`, `pop`, `charleston`,
|
|
@@ -3821,6 +5174,7 @@ export function progression(
|
|
|
3821
5174
|
seed: finite(options.seed ?? 0, "chord seed"),
|
|
3822
5175
|
...(options.pattern !== undefined ? { pattern: options.pattern } : {}),
|
|
3823
5176
|
velocity: vel,
|
|
5177
|
+
...(mode === "guitar" ? guitarPerform(options) : {}),
|
|
3824
5178
|
},
|
|
3825
5179
|
});
|
|
3826
5180
|
const out = [
|
|
@@ -3834,6 +5188,543 @@ export function progression(
|
|
|
3834
5188
|
);
|
|
3835
5189
|
}
|
|
3836
5190
|
|
|
5191
|
+
/** A track's guitar setup (SDK 1.27.0): see `TrackInput.guitar`. */
|
|
5192
|
+
export type GuitarInput = Readonly<{
|
|
5193
|
+
/** `standard` `dropd` `doubledropd` `dadgad` `openg` `opend` `opene` `halfdown` `nashville` `bass` `ukulele` `requinto`, or open-string pitches low to high. */
|
|
5194
|
+
tune?: string | readonly Pitch[];
|
|
5195
|
+
/** Capo fret 0..12. */
|
|
5196
|
+
capo?: number;
|
|
5197
|
+
/** Hand stretch in frets 3..6, default 4. */
|
|
5198
|
+
hand?: number;
|
|
5199
|
+
/** 0 closed shapes .. 1 ringing open strings, default 0.5. */
|
|
5200
|
+
ring?: number;
|
|
5201
|
+
/** Preferred fret position 0..12. */
|
|
5202
|
+
position?: number;
|
|
5203
|
+
}>;
|
|
5204
|
+
|
|
5205
|
+
function guitarInput(
|
|
5206
|
+
input: GuitarInput | undefined,
|
|
5207
|
+
where: string,
|
|
5208
|
+
): GuitarSetup | undefined {
|
|
5209
|
+
if (input === undefined) return undefined;
|
|
5210
|
+
if (!isRecord(input))
|
|
5211
|
+
throw new DawgSdkError(`${where} guitar must be an object`);
|
|
5212
|
+
const out: Record<string, unknown> = {};
|
|
5213
|
+
for (const key of Object.keys(input))
|
|
5214
|
+
if (!["tune", "capo", "hand", "ring", "position"].includes(key))
|
|
5215
|
+
throw new DawgSdkError(
|
|
5216
|
+
`${where} guitar has no field "${key.slice(0, 32)}" (tune capo hand ring position)`,
|
|
5217
|
+
);
|
|
5218
|
+
if (input.tune !== undefined) {
|
|
5219
|
+
if (typeof input.tune === "string") {
|
|
5220
|
+
const name = input.tune.toLowerCase().replace(/[\s_-]/g, "");
|
|
5221
|
+
if (!(GUITAR_TUNING_NAMES as readonly string[]).includes(name))
|
|
5222
|
+
throw new DawgSdkError(
|
|
5223
|
+
`${where} guitar tune must be one of ${GUITAR_TUNING_NAMES.join(" ")} or a list of pitches`,
|
|
5224
|
+
);
|
|
5225
|
+
out.tune = name;
|
|
5226
|
+
} else if (
|
|
5227
|
+
Array.isArray(input.tune) &&
|
|
5228
|
+
input.tune.length >= 3 &&
|
|
5229
|
+
input.tune.length <= 12
|
|
5230
|
+
)
|
|
5231
|
+
out.tune = Object.freeze(input.tune.map((p) => midi(p)));
|
|
5232
|
+
else
|
|
5233
|
+
throw new DawgSdkError(
|
|
5234
|
+
`${where} guitar tune must be a name or 3..12 pitches`,
|
|
5235
|
+
);
|
|
5236
|
+
}
|
|
5237
|
+
const int = (key: "capo" | "hand" | "position", min: number, max: number) => {
|
|
5238
|
+
const value = input[key];
|
|
5239
|
+
if (value === undefined) return;
|
|
5240
|
+
if (!Number.isInteger(value) || value < min || value > max)
|
|
5241
|
+
throw new DawgSdkError(
|
|
5242
|
+
`${where} guitar ${key} must be an integer ${min}..${max}`,
|
|
5243
|
+
);
|
|
5244
|
+
out[key] = value;
|
|
5245
|
+
};
|
|
5246
|
+
int("capo", 0, 12);
|
|
5247
|
+
int("hand", 3, 6);
|
|
5248
|
+
if (input.ring !== undefined)
|
|
5249
|
+
out.ring =
|
|
5250
|
+
Math.round(unit(input.ring, `${where} guitar ring`) * 1000) / 1000;
|
|
5251
|
+
int("position", 0, 12);
|
|
5252
|
+
return Object.keys(out).length > 0 ? Object.freeze(out) : undefined;
|
|
5253
|
+
}
|
|
5254
|
+
|
|
5255
|
+
function guitarPerform(options: ChordOptions): Partial<PerformOptions> {
|
|
5256
|
+
if (options.strokes !== undefined && !strokeGrid(options.strokes))
|
|
5257
|
+
throw new DawgSdkError(
|
|
5258
|
+
`chord strokes must be one of ${STROKE_PATTERN_NAMES.join(" ")} or a grid of D U d u x - .`,
|
|
5259
|
+
);
|
|
5260
|
+
const setup = guitarInput(options.guitar, "chord");
|
|
5261
|
+
return {
|
|
5262
|
+
...(options.strokes !== undefined ? { strokes: options.strokes } : {}),
|
|
5263
|
+
...(options.step !== undefined
|
|
5264
|
+
? { step: positive(options.step, "chord step") }
|
|
5265
|
+
: {}),
|
|
5266
|
+
...(options.speed !== undefined
|
|
5267
|
+
? {
|
|
5268
|
+
speed:
|
|
5269
|
+
Math.min(200, Math.max(0, finite(options.speed, "chord speed"))) /
|
|
5270
|
+
1000,
|
|
5271
|
+
}
|
|
5272
|
+
: {}),
|
|
5273
|
+
...(options.tempo !== undefined
|
|
5274
|
+
? { tempo: positive(options.tempo, "chord tempo") }
|
|
5275
|
+
: {}),
|
|
5276
|
+
...(setup ? { guitar: setup } : {}),
|
|
5277
|
+
};
|
|
5278
|
+
}
|
|
5279
|
+
|
|
5280
|
+
/**
|
|
5281
|
+
* Strummed guitar chords (SDK 1.27.0): `progression()` with
|
|
5282
|
+
* `perform: "guitar"`. Each chord is fretted on the guitar (`tune`, `capo`,
|
|
5283
|
+
* no barre over an open string) and strummed with a stroke grid; a down
|
|
5284
|
+
* stroke sweeps the strings in `speed` ms. Pair with `guitar` on the track
|
|
5285
|
+
* so dawg's prompt strums the same shapes.
|
|
5286
|
+
*
|
|
5287
|
+
* ```ts
|
|
5288
|
+
* notes: strum("G D Em C", { strokes: "folk", speed: 25, tempo: 96 })
|
|
5289
|
+
* notes: strum("i VI III VII", { key: "e minor", strokes: "D-DU-UDU", guitar: { capo: 2 } })
|
|
5290
|
+
* ```
|
|
5291
|
+
*/
|
|
5292
|
+
export function strum(
|
|
5293
|
+
chords: string | readonly string[],
|
|
5294
|
+
options: Omit<ProgressionOptions, "perform"> = {},
|
|
5295
|
+
): readonly NoteSpec[] {
|
|
5296
|
+
return progression(chords, { ...options, perform: "guitar" });
|
|
5297
|
+
}
|
|
5298
|
+
|
|
5299
|
+
// BEGIN instrument words: generated from core/instruments.ts by core/sdk/sync-instruments.ts
|
|
5300
|
+
/** What an instrument word stores on a track. */
|
|
5301
|
+
type InstrumentWord = Readonly<{
|
|
5302
|
+
/** The `Track.instrument` value. */
|
|
5303
|
+
instrument: string;
|
|
5304
|
+
/** The optional Track field the engine reads (created with defaults). */
|
|
5305
|
+
field?: string;
|
|
5306
|
+
/** A preset of that engine to apply. */
|
|
5307
|
+
preset?: string;
|
|
5308
|
+
/** An insert-effect preset to apply with it (rig aliases). */
|
|
5309
|
+
fx?: string;
|
|
5310
|
+
}>;
|
|
5311
|
+
|
|
5312
|
+
/** A word and what it means. */
|
|
5313
|
+
type InstrumentWordRow = Readonly<{
|
|
5314
|
+
word: string;
|
|
5315
|
+
/**
|
|
5316
|
+
* Borrow the voice (instrument, field, preset) of this other word's row
|
|
5317
|
+
* when it exists; `instrument` is the fallback when it does not.
|
|
5318
|
+
*/
|
|
5319
|
+
voice?: string;
|
|
5320
|
+
}> &
|
|
5321
|
+
InstrumentWord;
|
|
5322
|
+
|
|
5323
|
+
/**
|
|
5324
|
+
* Words that keep their pre-0.6 meaning forever: they resolve to
|
|
5325
|
+
* themselves, whatever rows the lanes add.
|
|
5326
|
+
*/
|
|
5327
|
+
const LEGACY_WORDS: readonly string[] = Object.freeze([
|
|
5328
|
+
"piano",
|
|
5329
|
+
"pluck",
|
|
5330
|
+
"bass",
|
|
5331
|
+
"saw",
|
|
5332
|
+
"square",
|
|
5333
|
+
"triangle",
|
|
5334
|
+
"marimba",
|
|
5335
|
+
"wind",
|
|
5336
|
+
"cello",
|
|
5337
|
+
"contrabass",
|
|
5338
|
+
"ebass",
|
|
5339
|
+
"sitar",
|
|
5340
|
+
"organ",
|
|
5341
|
+
"strings",
|
|
5342
|
+
"bell",
|
|
5343
|
+
"keys",
|
|
5344
|
+
"lead",
|
|
5345
|
+
]);
|
|
5346
|
+
|
|
5347
|
+
/** 0.6 instrument words; each lane appends its own block. */
|
|
5348
|
+
const INSTRUMENT_WORDS: readonly InstrumentWordRow[] = Object.freeze([
|
|
5349
|
+
// strings (f06-strings): plucked presets of the string engine. Legacy
|
|
5350
|
+
// sitar/ebass keep today's voice (`string preset sitar` reaches the
|
|
5351
|
+
// engine), jangle is the rig alias (the guitar lane maps its 12string to the
|
|
5352
|
+
// preset; `string jangle` reaches it) and
|
|
5353
|
+
// upright is the keys lane's piano (doublebass reaches the preset).
|
|
5354
|
+
{ word: "nylon", instrument: "string", field: "string", preset: "nylon" },
|
|
5355
|
+
{ word: "steel", instrument: "string", field: "string", preset: "steel" },
|
|
5356
|
+
{
|
|
5357
|
+
word: "electric",
|
|
5358
|
+
instrument: "string",
|
|
5359
|
+
field: "string",
|
|
5360
|
+
preset: "electric",
|
|
5361
|
+
},
|
|
5362
|
+
{ word: "slap", instrument: "string", field: "string", preset: "slap" },
|
|
5363
|
+
{ word: "motown", instrument: "string", field: "string", preset: "motown" },
|
|
5364
|
+
{ word: "tanpura", instrument: "string", field: "string", preset: "tanpura" },
|
|
5365
|
+
{
|
|
5366
|
+
word: "harpsichord",
|
|
5367
|
+
instrument: "string",
|
|
5368
|
+
field: "string",
|
|
5369
|
+
preset: "harpsichord",
|
|
5370
|
+
},
|
|
5371
|
+
{ word: "lute", instrument: "string", field: "string", preset: "lute" },
|
|
5372
|
+
{ word: "oud", instrument: "string", field: "string", preset: "oud" },
|
|
5373
|
+
{ word: "setar", instrument: "string", field: "string", preset: "setar" },
|
|
5374
|
+
{ word: "tar", instrument: "string", field: "string", preset: "tar" },
|
|
5375
|
+
{ word: "santur", instrument: "string", field: "string", preset: "santur" },
|
|
5376
|
+
{
|
|
5377
|
+
word: "dulcimer",
|
|
5378
|
+
instrument: "string",
|
|
5379
|
+
field: "string",
|
|
5380
|
+
preset: "dulcimer",
|
|
5381
|
+
},
|
|
5382
|
+
{ word: "koto", instrument: "string", field: "string", preset: "koto" },
|
|
5383
|
+
{ word: "harp", instrument: "string", field: "string", preset: "harp" },
|
|
5384
|
+
{ word: "banjo", instrument: "string", field: "string", preset: "banjo" },
|
|
5385
|
+
{ word: "tres", instrument: "string", field: "string", preset: "tres" },
|
|
5386
|
+
{
|
|
5387
|
+
word: "requinto",
|
|
5388
|
+
instrument: "string",
|
|
5389
|
+
field: "string",
|
|
5390
|
+
preset: "requinto",
|
|
5391
|
+
},
|
|
5392
|
+
{ word: "acoustic", instrument: "string", field: "string", preset: "steel" },
|
|
5393
|
+
{ word: "classical", instrument: "string", field: "string", preset: "nylon" },
|
|
5394
|
+
{
|
|
5395
|
+
word: "bassguitar",
|
|
5396
|
+
instrument: "string",
|
|
5397
|
+
field: "string",
|
|
5398
|
+
preset: "ebass",
|
|
5399
|
+
},
|
|
5400
|
+
{ word: "fender", instrument: "string", field: "string", preset: "ebass" },
|
|
5401
|
+
{
|
|
5402
|
+
word: "doublebass",
|
|
5403
|
+
instrument: "string",
|
|
5404
|
+
field: "string",
|
|
5405
|
+
preset: "upright",
|
|
5406
|
+
},
|
|
5407
|
+
{
|
|
5408
|
+
word: "cembalo",
|
|
5409
|
+
instrument: "string",
|
|
5410
|
+
field: "string",
|
|
5411
|
+
preset: "harpsichord",
|
|
5412
|
+
},
|
|
5413
|
+
{
|
|
5414
|
+
word: "hammered",
|
|
5415
|
+
instrument: "string",
|
|
5416
|
+
field: "string",
|
|
5417
|
+
preset: "dulcimer",
|
|
5418
|
+
},
|
|
5419
|
+
{ word: "sehtar", instrument: "string", field: "string", preset: "setar" },
|
|
5420
|
+
// bowed (f061-bowed): bowed presets of the string engine. `cello`,
|
|
5421
|
+
// `contrabass` and `strings` stay legacy words (today's voice);
|
|
5422
|
+
// `bowed-cello`, `string cello` or `bowed cello` reach the engine.
|
|
5423
|
+
{ word: "violin", instrument: "string", field: "string", preset: "violin" },
|
|
5424
|
+
{ word: "viola", instrument: "string", field: "string", preset: "viola" },
|
|
5425
|
+
{ word: "fiddle", instrument: "string", field: "string", preset: "fiddle" },
|
|
5426
|
+
{ word: "erhu", instrument: "string", field: "string", preset: "erhu" },
|
|
5427
|
+
{
|
|
5428
|
+
word: "kamancheh",
|
|
5429
|
+
instrument: "string",
|
|
5430
|
+
field: "string",
|
|
5431
|
+
preset: "kamancheh",
|
|
5432
|
+
},
|
|
5433
|
+
{
|
|
5434
|
+
word: "kemence",
|
|
5435
|
+
instrument: "string",
|
|
5436
|
+
field: "string",
|
|
5437
|
+
preset: "kamancheh",
|
|
5438
|
+
},
|
|
5439
|
+
{ word: "violins", instrument: "string", field: "string", preset: "violins" },
|
|
5440
|
+
{ word: "violas", instrument: "string", field: "string", preset: "violas" },
|
|
5441
|
+
{ word: "cellos", instrument: "string", field: "string", preset: "cellos" },
|
|
5442
|
+
{
|
|
5443
|
+
word: "contrabasses",
|
|
5444
|
+
instrument: "string",
|
|
5445
|
+
field: "string",
|
|
5446
|
+
preset: "contrabasses",
|
|
5447
|
+
},
|
|
5448
|
+
{ word: "pizzicato", instrument: "string", field: "string", preset: "pizz" },
|
|
5449
|
+
{ word: "tremolo", instrument: "string", field: "string", preset: "trem" },
|
|
5450
|
+
{
|
|
5451
|
+
word: "bowed-cello",
|
|
5452
|
+
instrument: "string",
|
|
5453
|
+
field: "string",
|
|
5454
|
+
preset: "cello",
|
|
5455
|
+
},
|
|
5456
|
+
// f06-rig: guitar track aliases, a guitar voice plus a whole rig. The
|
|
5457
|
+
// voice is the strings lane's `electric` row (jangle: its 12-string
|
|
5458
|
+
// `jangle` preset); the pluck only while that row is absent. Never `lead`
|
|
5459
|
+
// or `bass`.
|
|
5460
|
+
{
|
|
5461
|
+
word: "jangle",
|
|
5462
|
+
instrument: "string",
|
|
5463
|
+
field: "string",
|
|
5464
|
+
preset: "jangle",
|
|
5465
|
+
fx: "jangle",
|
|
5466
|
+
},
|
|
5467
|
+
{ word: "punk", instrument: "pluck", voice: "electric", fx: "punk" },
|
|
5468
|
+
{ word: "funk", instrument: "pluck", voice: "electric", fx: "funk" },
|
|
5469
|
+
{ word: "ragged", instrument: "pluck", voice: "electric", fx: "ragged" },
|
|
5470
|
+
{ word: "gtr-lead", instrument: "pluck", voice: "electric", fx: "lead" },
|
|
5471
|
+
{ word: "gtr-metal", instrument: "pluck", voice: "electric", fx: "metal" },
|
|
5472
|
+
{ word: "bachata", instrument: "pluck", voice: "electric", fx: "bachata" },
|
|
5473
|
+
// granular (f06-granular): the instrument and its texture presets. Each
|
|
5474
|
+
// starts from a built-in synth source, so nothing downloads.
|
|
5475
|
+
{
|
|
5476
|
+
word: "granular",
|
|
5477
|
+
instrument: "granular",
|
|
5478
|
+
field: "granular",
|
|
5479
|
+
preset: "cloud",
|
|
5480
|
+
},
|
|
5481
|
+
{
|
|
5482
|
+
word: "grains",
|
|
5483
|
+
instrument: "granular",
|
|
5484
|
+
field: "granular",
|
|
5485
|
+
preset: "cloud",
|
|
5486
|
+
},
|
|
5487
|
+
{ word: "cloud", instrument: "granular", field: "granular", preset: "cloud" },
|
|
5488
|
+
{
|
|
5489
|
+
word: "sparkle",
|
|
5490
|
+
instrument: "granular",
|
|
5491
|
+
field: "granular",
|
|
5492
|
+
preset: "sparkle",
|
|
5493
|
+
},
|
|
5494
|
+
{ word: "swarm", instrument: "granular", field: "granular", preset: "swarm" },
|
|
5495
|
+
{
|
|
5496
|
+
word: "microloop",
|
|
5497
|
+
instrument: "granular",
|
|
5498
|
+
field: "granular",
|
|
5499
|
+
preset: "microloop",
|
|
5500
|
+
},
|
|
5501
|
+
// keys (f06-piano): modelled pianos. `piano` stays legacy here; the typed
|
|
5502
|
+
// surfaces store a new `piano` as `grand` (core/keys.ts `pianoWrite`).
|
|
5503
|
+
{ word: "grand", instrument: "grand", field: "keys", preset: "grand" },
|
|
5504
|
+
{ word: "ballad", instrument: "grand", field: "keys", preset: "ballad" },
|
|
5505
|
+
{ word: "upright", instrument: "upright", field: "keys", preset: "upright" },
|
|
5506
|
+
{ word: "felt", instrument: "felt", field: "keys", preset: "felt" },
|
|
5507
|
+
{ word: "lofi", instrument: "felt", field: "keys", preset: "lofi" },
|
|
5508
|
+
{
|
|
5509
|
+
word: "honkytonk",
|
|
5510
|
+
instrument: "honkytonk",
|
|
5511
|
+
field: "keys",
|
|
5512
|
+
preset: "honkytonk",
|
|
5513
|
+
},
|
|
5514
|
+
{
|
|
5515
|
+
word: "prepared",
|
|
5516
|
+
instrument: "prepared",
|
|
5517
|
+
field: "keys",
|
|
5518
|
+
preset: "prepared",
|
|
5519
|
+
},
|
|
5520
|
+
// keys (f061-organ): tonewheel, combo and pipe organs on the keys
|
|
5521
|
+
// engine. `organ` stays legacy (the sine voice).
|
|
5522
|
+
{
|
|
5523
|
+
word: "tonewheel",
|
|
5524
|
+
instrument: "tonewheel",
|
|
5525
|
+
field: "keys",
|
|
5526
|
+
preset: "tonewheel",
|
|
5527
|
+
},
|
|
5528
|
+
{
|
|
5529
|
+
word: "hammond",
|
|
5530
|
+
instrument: "tonewheel",
|
|
5531
|
+
field: "keys",
|
|
5532
|
+
preset: "tonewheel",
|
|
5533
|
+
},
|
|
5534
|
+
{ word: "b3", instrument: "tonewheel", field: "keys", preset: "tonewheel" },
|
|
5535
|
+
{ word: "gospel", instrument: "tonewheel", field: "keys", preset: "gospel" },
|
|
5536
|
+
{
|
|
5537
|
+
word: "jazzorgan",
|
|
5538
|
+
instrument: "tonewheel",
|
|
5539
|
+
field: "keys",
|
|
5540
|
+
preset: "jazzorgan",
|
|
5541
|
+
},
|
|
5542
|
+
{ word: "combo", instrument: "combo", field: "keys", preset: "combo" },
|
|
5543
|
+
{ word: "farfisa", instrument: "combo", field: "keys", preset: "combo" },
|
|
5544
|
+
{ word: "vox", instrument: "combo", field: "keys", preset: "vox" },
|
|
5545
|
+
{ word: "pipe", instrument: "pipe", field: "keys", preset: "pipe" },
|
|
5546
|
+
{ word: "church", instrument: "pipe", field: "keys", preset: "pipe" },
|
|
5547
|
+
{ word: "pipeorgan", instrument: "pipe", field: "keys", preset: "pipe" },
|
|
5548
|
+
{ word: "churchorgan", instrument: "pipe", field: "keys", preset: "pipe" },
|
|
5549
|
+
{ word: "flutes", instrument: "pipe", field: "keys", preset: "flutes" },
|
|
5550
|
+
{ word: "cornet", instrument: "pipe", field: "keys", preset: "cornet" },
|
|
5551
|
+
{ word: "reeds", instrument: "pipe", field: "keys", preset: "reeds" },
|
|
5552
|
+
{ word: "celeste", instrument: "pipe", field: "keys", preset: "celeste" },
|
|
5553
|
+
// f06-modal: mallets and bells (core/resonators.ts). `marimba` is legacy;
|
|
5554
|
+
// `modal` alone gives the modal marimba.
|
|
5555
|
+
{ word: "modal", instrument: "modal", field: "modal", preset: "marimba" },
|
|
5556
|
+
{ word: "vibes", instrument: "modal", field: "modal", preset: "vibes" },
|
|
5557
|
+
{ word: "vibraphone", instrument: "modal", field: "modal", preset: "vibes" },
|
|
5558
|
+
{
|
|
5559
|
+
word: "xylophone",
|
|
5560
|
+
instrument: "modal",
|
|
5561
|
+
field: "modal",
|
|
5562
|
+
preset: "xylophone",
|
|
5563
|
+
},
|
|
5564
|
+
{ word: "glock", instrument: "modal", field: "modal", preset: "glock" },
|
|
5565
|
+
{
|
|
5566
|
+
word: "glockenspiel",
|
|
5567
|
+
instrument: "modal",
|
|
5568
|
+
field: "modal",
|
|
5569
|
+
preset: "glock",
|
|
5570
|
+
},
|
|
5571
|
+
{ word: "celesta", instrument: "modal", field: "modal", preset: "celesta" },
|
|
5572
|
+
{ word: "chimes", instrument: "modal", field: "modal", preset: "chimes" },
|
|
5573
|
+
{ word: "tubular", instrument: "modal", field: "modal", preset: "chimes" },
|
|
5574
|
+
{ word: "kalimba", instrument: "modal", field: "modal", preset: "kalimba" },
|
|
5575
|
+
{
|
|
5576
|
+
word: "thumbpiano",
|
|
5577
|
+
instrument: "modal",
|
|
5578
|
+
field: "modal",
|
|
5579
|
+
preset: "kalimba",
|
|
5580
|
+
},
|
|
5581
|
+
{ word: "mbira", instrument: "modal", field: "modal", preset: "mbira" },
|
|
5582
|
+
{ word: "steelpan", instrument: "modal", field: "modal", preset: "steelpan" },
|
|
5583
|
+
{ word: "bowl", instrument: "modal", field: "modal", preset: "bowl" },
|
|
5584
|
+
{ word: "gong", instrument: "modal", field: "modal", preset: "gong" },
|
|
5585
|
+
{ word: "gongageng", instrument: "modal", field: "modal", preset: "gong" },
|
|
5586
|
+
{ word: "timpani", instrument: "modal", field: "modal", preset: "timpani" },
|
|
5587
|
+
{
|
|
5588
|
+
word: "steeldrum",
|
|
5589
|
+
instrument: "modal",
|
|
5590
|
+
field: "modal",
|
|
5591
|
+
preset: "steelpan",
|
|
5592
|
+
},
|
|
5593
|
+
{ word: "singingbowl", instrument: "modal", field: "modal", preset: "bowl" },
|
|
5594
|
+
{
|
|
5595
|
+
word: "kettledrum",
|
|
5596
|
+
instrument: "modal",
|
|
5597
|
+
field: "modal",
|
|
5598
|
+
preset: "timpani",
|
|
5599
|
+
},
|
|
5600
|
+
{
|
|
5601
|
+
word: "tubularbells",
|
|
5602
|
+
instrument: "modal",
|
|
5603
|
+
field: "modal",
|
|
5604
|
+
preset: "chimes",
|
|
5605
|
+
},
|
|
5606
|
+
// keys-electric (0.6.1): electric pianos and clavinet. `keys` stays legacy.
|
|
5607
|
+
{ word: "epiano", instrument: "epiano", field: "keys", preset: "epiano" },
|
|
5608
|
+
{ word: "rhodes", instrument: "epiano", field: "keys", preset: "epiano" },
|
|
5609
|
+
{
|
|
5610
|
+
word: "suitcase",
|
|
5611
|
+
instrument: "epiano",
|
|
5612
|
+
field: "keys",
|
|
5613
|
+
preset: "suitcase",
|
|
5614
|
+
},
|
|
5615
|
+
{ word: "dyno", instrument: "epiano", field: "keys", preset: "dyno" },
|
|
5616
|
+
{ word: "wurli", instrument: "wurli", field: "keys", preset: "wurli" },
|
|
5617
|
+
{ word: "wurlitzer", instrument: "wurli", field: "keys", preset: "wurli" },
|
|
5618
|
+
{ word: "clav", instrument: "clav", field: "keys", preset: "clav" },
|
|
5619
|
+
{ word: "clavinet", instrument: "clav", field: "keys", preset: "clav" },
|
|
5620
|
+
{ word: "funkclav", instrument: "clav", field: "keys", preset: "funkclav" },
|
|
5621
|
+
// f061-guitar: the shoegaze alias, an electric guitar voice plus the
|
|
5622
|
+
// shoegaze rig and its long wash.
|
|
5623
|
+
{ word: "shoegaze", instrument: "pluck", voice: "electric", fx: "shoegaze" },
|
|
5624
|
+
// f061 integration: the two rig names that are free as instrument words
|
|
5625
|
+
// (glide and swell are taken by commands; reach them with `rig glide`).
|
|
5626
|
+
{ word: "dreampop", instrument: "pluck", voice: "electric", fx: "dreampop" },
|
|
5627
|
+
{ word: "ebow", instrument: "pluck", voice: "electric", fx: "ebow" },
|
|
5628
|
+
// f061-gamelan-winds: gamelan, small bells and frame drums.
|
|
5629
|
+
{ word: "crotales", instrument: "modal", field: "modal", preset: "crotales" },
|
|
5630
|
+
{ word: "crotale", instrument: "modal", field: "modal", preset: "crotales" },
|
|
5631
|
+
{ word: "musicbox", instrument: "modal", field: "modal", preset: "musicbox" },
|
|
5632
|
+
{ word: "toypiano", instrument: "modal", field: "modal", preset: "toypiano" },
|
|
5633
|
+
{ word: "saron", instrument: "modal", field: "modal", preset: "saron" },
|
|
5634
|
+
{ word: "demung", instrument: "modal", field: "modal", preset: "demung" },
|
|
5635
|
+
{ word: "slenthem", instrument: "modal", field: "modal", preset: "slenthem" },
|
|
5636
|
+
{ word: "gangsa", instrument: "modal", field: "modal", preset: "gangsa" },
|
|
5637
|
+
{ word: "gender", instrument: "modal", field: "modal", preset: "gender" },
|
|
5638
|
+
{ word: "bonang", instrument: "modal", field: "modal", preset: "bonang" },
|
|
5639
|
+
{ word: "kenong", instrument: "modal", field: "modal", preset: "kenong" },
|
|
5640
|
+
{ word: "kethuk", instrument: "modal", field: "modal", preset: "kethuk" },
|
|
5641
|
+
{ word: "kempul", instrument: "modal", field: "modal", preset: "kempul" },
|
|
5642
|
+
{ word: "daf", instrument: "modal", field: "modal", preset: "daf" },
|
|
5643
|
+
{ word: "bodhran", instrument: "modal", field: "modal", preset: "bodhran" },
|
|
5644
|
+
{ word: "framedrum", instrument: "modal", field: "modal", preset: "bodhran" },
|
|
5645
|
+
{ word: "tabla", instrument: "modal", field: "modal", preset: "tabla" },
|
|
5646
|
+
// f061-gamelan-winds: blown waveguides (core/winds.ts). The legacy word
|
|
5647
|
+
// `wind` keeps its tone; these words and their aliases pick a wind preset.
|
|
5648
|
+
{ word: "flute", instrument: "wind", field: "wind", preset: "flute" },
|
|
5649
|
+
{ word: "recorder", instrument: "wind", field: "wind", preset: "recorder" },
|
|
5650
|
+
{ word: "whistle", instrument: "wind", field: "wind", preset: "whistle" },
|
|
5651
|
+
{ word: "ney", instrument: "wind", field: "wind", preset: "ney" },
|
|
5652
|
+
{
|
|
5653
|
+
word: "shakuhachi",
|
|
5654
|
+
instrument: "wind",
|
|
5655
|
+
field: "wind",
|
|
5656
|
+
preset: "shakuhachi",
|
|
5657
|
+
},
|
|
5658
|
+
{ word: "panpipe", instrument: "wind", field: "wind", preset: "panpipe" },
|
|
5659
|
+
{ word: "suling", instrument: "wind", field: "wind", preset: "suling" },
|
|
5660
|
+
{ word: "bansuri", instrument: "wind", field: "wind", preset: "bansuri" },
|
|
5661
|
+
{ word: "clarinet", instrument: "wind", field: "wind", preset: "clarinet" },
|
|
5662
|
+
{
|
|
5663
|
+
word: "bassclarinet",
|
|
5664
|
+
instrument: "wind",
|
|
5665
|
+
field: "wind",
|
|
5666
|
+
preset: "bassclarinet",
|
|
5667
|
+
},
|
|
5668
|
+
{ word: "oboe", instrument: "wind", field: "wind", preset: "oboe" },
|
|
5669
|
+
{ word: "bassoon", instrument: "wind", field: "wind", preset: "bassoon" },
|
|
5670
|
+
{ word: "sax", instrument: "wind", field: "wind", preset: "sax" },
|
|
5671
|
+
{ word: "altosax", instrument: "wind", field: "wind", preset: "altosax" },
|
|
5672
|
+
{ word: "barisax", instrument: "wind", field: "wind", preset: "barisax" },
|
|
5673
|
+
{ word: "trumpet", instrument: "wind", field: "wind", preset: "trumpet" },
|
|
5674
|
+
{ word: "harmon", instrument: "wind", field: "wind", preset: "harmon" },
|
|
5675
|
+
{ word: "plunger", instrument: "wind", field: "wind", preset: "plunger" },
|
|
5676
|
+
{ word: "trombone", instrument: "wind", field: "wind", preset: "trombone" },
|
|
5677
|
+
{ word: "tuba", instrument: "wind", field: "wind", preset: "tuba" },
|
|
5678
|
+
{ word: "horn", instrument: "wind", field: "wind", preset: "horn" },
|
|
5679
|
+
{ word: "tinwhistle", instrument: "wind", field: "wind", preset: "whistle" },
|
|
5680
|
+
{
|
|
5681
|
+
word: "pennywhistle",
|
|
5682
|
+
instrument: "wind",
|
|
5683
|
+
field: "wind",
|
|
5684
|
+
preset: "whistle",
|
|
5685
|
+
},
|
|
5686
|
+
{ word: "nay", instrument: "wind", field: "wind", preset: "ney" },
|
|
5687
|
+
{ word: "panflute", instrument: "wind", field: "wind", preset: "panpipe" },
|
|
5688
|
+
{ word: "panpipes", instrument: "wind", field: "wind", preset: "panpipe" },
|
|
5689
|
+
{ word: "saxophone", instrument: "wind", field: "wind", preset: "sax" },
|
|
5690
|
+
{ word: "tenorsax", instrument: "wind", field: "wind", preset: "sax" },
|
|
5691
|
+
{ word: "tenor", instrument: "wind", field: "wind", preset: "sax" },
|
|
5692
|
+
{ word: "alto", instrument: "wind", field: "wind", preset: "altosax" },
|
|
5693
|
+
{ word: "bari", instrument: "wind", field: "wind", preset: "barisax" },
|
|
5694
|
+
{ word: "baritonesax", instrument: "wind", field: "wind", preset: "barisax" },
|
|
5695
|
+
{ word: "frenchhorn", instrument: "wind", field: "wind", preset: "horn" },
|
|
5696
|
+
{ word: "mutedtrumpet", instrument: "wind", field: "wind", preset: "harmon" },
|
|
5697
|
+
{ word: "wahtrumpet", instrument: "wind", field: "wind", preset: "plunger" },
|
|
5698
|
+
]);
|
|
5699
|
+
|
|
5700
|
+
/**
|
|
5701
|
+
* What an instrument word means: a legacy word is itself, a row word is its
|
|
5702
|
+
* row, anything else is undefined (callers keep the word as typed).
|
|
5703
|
+
*/
|
|
5704
|
+
function resolveInstrumentWord(word: string): InstrumentWord | undefined {
|
|
5705
|
+
if (LEGACY_WORDS.includes(word)) return Object.freeze({ instrument: word });
|
|
5706
|
+
const row = INSTRUMENT_WORDS.find((entry) => entry.word === word);
|
|
5707
|
+
if (!row) return undefined;
|
|
5708
|
+
const { word: _word, voice, ...meaning } = row;
|
|
5709
|
+
const borrowed =
|
|
5710
|
+
voice === undefined
|
|
5711
|
+
? undefined
|
|
5712
|
+
: INSTRUMENT_WORDS.find((entry) => entry.word === voice && !entry.voice);
|
|
5713
|
+
if (!borrowed) return Object.freeze(meaning);
|
|
5714
|
+
return Object.freeze({
|
|
5715
|
+
instrument: borrowed.instrument,
|
|
5716
|
+
...(borrowed.field === undefined ? {} : { field: borrowed.field }),
|
|
5717
|
+
...(borrowed.preset === undefined ? {} : { preset: borrowed.preset }),
|
|
5718
|
+
...(meaning.fx === undefined ? {} : { fx: meaning.fx }),
|
|
5719
|
+
});
|
|
5720
|
+
}
|
|
5721
|
+
|
|
5722
|
+
/** The `Track.instrument` value a word stores (the word itself if unknown). */
|
|
5723
|
+
function instrumentForWord(word: string): string {
|
|
5724
|
+
return resolveInstrumentWord(word)?.instrument ?? word;
|
|
5725
|
+
}
|
|
5726
|
+
// END instrument words
|
|
5727
|
+
|
|
3837
5728
|
// BEGIN chord engine: generated from core/chords.ts by core/sdk/sync-chords.ts
|
|
3838
5729
|
// ---------------------------------------------------------------------------
|
|
3839
5730
|
// Vocabulary
|
|
@@ -3921,6 +5812,12 @@ type Chord = Readonly<{
|
|
|
3921
5812
|
extensions: readonly Extension[];
|
|
3922
5813
|
/** Slash bass pitch class, when not the root. */
|
|
3923
5814
|
bass?: number | undefined;
|
|
5815
|
+
/**
|
|
5816
|
+
* Upper tensions beyond the four extension buttons, as semitones above
|
|
5817
|
+
* the root (13 b9, 15 #9, 17 11, 18 #11, 21 13): only typed symbols
|
|
5818
|
+
* such as `C11`, `G13` or `A7b9` carry them (0.6.1).
|
|
5819
|
+
*/
|
|
5820
|
+
tensions?: readonly number[] | undefined;
|
|
3924
5821
|
}>;
|
|
3925
5822
|
|
|
3926
5823
|
function makeChord(
|
|
@@ -3928,6 +5825,7 @@ function makeChord(
|
|
|
3928
5825
|
quality: Quality,
|
|
3929
5826
|
extensions: Iterable<Extension> = [],
|
|
3930
5827
|
bass?: number,
|
|
5828
|
+
tensions: readonly number[] = [],
|
|
3931
5829
|
): Chord {
|
|
3932
5830
|
const held = new Set(extensions);
|
|
3933
5831
|
const own = SECRET_EXTENSION[quality];
|
|
@@ -3939,6 +5837,11 @@ function makeChord(
|
|
|
3939
5837
|
quality,
|
|
3940
5838
|
extensions: Object.freeze(ext),
|
|
3941
5839
|
...(slash !== undefined && slash !== pc ? { bass: slash } : {}),
|
|
5840
|
+
...(tensions.length > 0
|
|
5841
|
+
? {
|
|
5842
|
+
tensions: Object.freeze([...new Set(tensions)].sort((a, b) => a - b)),
|
|
5843
|
+
}
|
|
5844
|
+
: {}),
|
|
3942
5845
|
});
|
|
3943
5846
|
}
|
|
3944
5847
|
|
|
@@ -3946,6 +5849,7 @@ function makeChord(
|
|
|
3946
5849
|
function chordIntervals(chord: Chord): number[] {
|
|
3947
5850
|
const set = new Set(QUALITY_INTERVALS[chord.quality]);
|
|
3948
5851
|
for (const ext of chord.extensions) set.add(EXTENSION_INTERVAL[ext]);
|
|
5852
|
+
for (const step of chord.tensions ?? []) set.add(step);
|
|
3949
5853
|
// m7 and M7 together keep both; 6 with m7 on a dim triad is the dim7's bb7.
|
|
3950
5854
|
return [...set].sort((a, b) => a - b);
|
|
3951
5855
|
}
|
|
@@ -3998,6 +5902,18 @@ const SECRET_SUFFIX: Readonly<Partial<Record<Quality, string>>> = Object.freeze(
|
|
|
3998
5902
|
|
|
3999
5903
|
/** Chord symbol suffix: `m7`, `maj9`, `7sus4`, `dim7`, `m7b5`, `6/9`. */
|
|
4000
5904
|
function chordSuffix(chord: Chord): string {
|
|
5905
|
+
if (chord.tensions?.length) {
|
|
5906
|
+
// A typed extended chord keeps the symbol it was typed as.
|
|
5907
|
+
const typed = TENSION_SUFFIXES.find(
|
|
5908
|
+
([, quality, ext, tensions]) =>
|
|
5909
|
+
quality === chord.quality &&
|
|
5910
|
+
ext.join() === chord.extensions.join() &&
|
|
5911
|
+
tensions.join() === chord.tensions!.join(),
|
|
5912
|
+
);
|
|
5913
|
+
if (typed) return typed[0];
|
|
5914
|
+
const names = chord.tensions.map((step) => TENSION_NAMES[step] ?? step);
|
|
5915
|
+
return `${chordSuffix({ ...chord, tensions: undefined })}(${names.join(",")})`;
|
|
5916
|
+
}
|
|
4001
5917
|
const ext = new Set(chord.extensions);
|
|
4002
5918
|
const b7 = ext.has("m7");
|
|
4003
5919
|
const M7 = ext.has("M7");
|
|
@@ -4161,9 +6077,51 @@ const SUFFIXES: readonly (readonly [string, Quality, readonly Extension[]])[] =
|
|
|
4161
6077
|
["addb6", "b6", []],
|
|
4162
6078
|
["7#9", "7#9", []],
|
|
4163
6079
|
];
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
6080
|
+
|
|
6081
|
+
/** Typed upper-tension chords (0.6.1): suffix, quality, buttons, tensions. */
|
|
6082
|
+
const TENSION_SUFFIXES: readonly (readonly [
|
|
6083
|
+
string,
|
|
6084
|
+
Quality,
|
|
6085
|
+
readonly Extension[],
|
|
6086
|
+
readonly number[],
|
|
6087
|
+
])[] = [
|
|
6088
|
+
["11", "maj", ["m7", "9"], [17]],
|
|
6089
|
+
["m11", "min", ["m7", "9"], [17]],
|
|
6090
|
+
["maj11", "maj", ["M7", "9"], [17]],
|
|
6091
|
+
["add11", "maj", [], [17]],
|
|
6092
|
+
["madd11", "min", [], [17]],
|
|
6093
|
+
["13", "maj", ["m7", "9"], [21]],
|
|
6094
|
+
["m13", "min", ["m7", "9"], [21]],
|
|
6095
|
+
["maj13", "maj", ["M7", "9"], [21]],
|
|
6096
|
+
["7b9", "maj", ["m7"], [13]],
|
|
6097
|
+
["7#11", "maj", ["m7"], [18]],
|
|
6098
|
+
["maj7#11", "maj", ["M7"], [18]],
|
|
6099
|
+
["M7#11", "maj", ["M7"], [18]],
|
|
6100
|
+
["7b13", "maj", ["m7"], [20]],
|
|
6101
|
+
["13b9", "maj", ["m7"], [13, 21]],
|
|
6102
|
+
];
|
|
6103
|
+
|
|
6104
|
+
const TENSION_NAMES: Readonly<Record<number, string>> = Object.freeze({
|
|
6105
|
+
13: "b9",
|
|
6106
|
+
15: "#9",
|
|
6107
|
+
17: "11",
|
|
6108
|
+
18: "#11",
|
|
6109
|
+
20: "b13",
|
|
6110
|
+
21: "13",
|
|
6111
|
+
});
|
|
6112
|
+
|
|
6113
|
+
const SUFFIX_TABLE = new Map<
|
|
6114
|
+
string,
|
|
6115
|
+
{ quality: Quality; ext: readonly Extension[]; tensions?: readonly number[] }
|
|
6116
|
+
>([
|
|
6117
|
+
...SUFFIXES.map(
|
|
6118
|
+
([suffix, quality, ext]) => [suffix, { quality, ext }] as const,
|
|
6119
|
+
),
|
|
6120
|
+
...TENSION_SUFFIXES.map(
|
|
6121
|
+
([suffix, quality, ext, tensions]) =>
|
|
6122
|
+
[suffix, { quality, ext, tensions }] as const,
|
|
6123
|
+
),
|
|
6124
|
+
]);
|
|
4167
6125
|
|
|
4168
6126
|
const LETTER: Readonly<Record<string, number>> = Object.freeze({
|
|
4169
6127
|
c: 0,
|
|
@@ -4205,7 +6163,7 @@ function parseChord(symbol: string): Chord | undefined {
|
|
|
4205
6163
|
bass = parsePitchClass(match[4]);
|
|
4206
6164
|
if (bass === undefined) return undefined;
|
|
4207
6165
|
}
|
|
4208
|
-
return makeChord(root, entry.quality, entry.ext, bass);
|
|
6166
|
+
return makeChord(root, entry.quality, entry.ext, bass, entry.tensions);
|
|
4209
6167
|
}
|
|
4210
6168
|
|
|
4211
6169
|
// ---------------------------------------------------------------------------
|
|
@@ -5003,6 +6961,8 @@ const PERFORM_MODES = [
|
|
|
5003
6961
|
"harp",
|
|
5004
6962
|
"slop",
|
|
5005
6963
|
"pattern",
|
|
6964
|
+
// 0.6.1: fretboard-voiced strokes (see `voiceGuitar`, `strokeVoicing`).
|
|
6965
|
+
"guitar",
|
|
5006
6966
|
] as const;
|
|
5007
6967
|
type PerformMode = (typeof PERFORM_MODES)[number];
|
|
5008
6968
|
|
|
@@ -5022,6 +6982,23 @@ type PerformOptions = Readonly<{
|
|
|
5022
6982
|
pattern?: string | number;
|
|
5023
6983
|
/** 0..1. */
|
|
5024
6984
|
velocity?: number;
|
|
6985
|
+
/** Guitar mode: the stroke grid (a STROKE_PATTERNS name or D U d u x - .). */
|
|
6986
|
+
strokes?: string;
|
|
6987
|
+
/** Guitar mode: grid step in beats, default 1/2. */
|
|
6988
|
+
step?: number;
|
|
6989
|
+
/** Guitar mode: seconds a full six-string down stroke takes, default 0.022. */
|
|
6990
|
+
speed?: number;
|
|
6991
|
+
/** Guitar mode: tempo in BPM that `speed` converts with, default 120. */
|
|
6992
|
+
tempo?: number;
|
|
6993
|
+
/** Guitar mode: the guitar's tuning, capo, hand, ring and position. */
|
|
6994
|
+
guitar?: GuitarSetup;
|
|
6995
|
+
/** Guitar mode: the chord's root pitch class (default: the lowest note). */
|
|
6996
|
+
root?: number;
|
|
6997
|
+
/**
|
|
6998
|
+
* Guitar mode: a slash bass pitch class that must sound lowest, even when
|
|
6999
|
+
* it is not a chord tone (B/E keeps its E pedal). Default: the lowest note.
|
|
7000
|
+
*/
|
|
7001
|
+
slash?: number;
|
|
5025
7002
|
}>;
|
|
5026
7003
|
|
|
5027
7004
|
type PerformedNote = Readonly<{
|
|
@@ -5338,6 +7315,24 @@ function perform(
|
|
|
5338
7315
|
}
|
|
5339
7316
|
return out.sort((a, b) => a.start - b.start || a.pitch - b.pitch);
|
|
5340
7317
|
}
|
|
7318
|
+
case "guitar": {
|
|
7319
|
+
const bass = options.slash ?? notes[0]!;
|
|
7320
|
+
const voicing = voiceGuitar(
|
|
7321
|
+
notes.map(mod12),
|
|
7322
|
+
mod12(bass),
|
|
7323
|
+
options.root ?? mod12(bass),
|
|
7324
|
+
options.guitar,
|
|
7325
|
+
);
|
|
7326
|
+
if (!voicing) return notes.map((pitch) => at(pitch, start, end));
|
|
7327
|
+
return strokeVoicing(voicing, start, length, {
|
|
7328
|
+
...(options.strokes !== undefined ? { strokes: options.strokes } : {}),
|
|
7329
|
+
...(options.step !== undefined ? { step: options.step } : {}),
|
|
7330
|
+
...(options.speed !== undefined ? { speed: options.speed } : {}),
|
|
7331
|
+
...(options.tempo !== undefined ? { tempo: options.tempo } : {}),
|
|
7332
|
+
velocity,
|
|
7333
|
+
strings: guitarStrings(options.guitar?.tune).length,
|
|
7334
|
+
});
|
|
7335
|
+
}
|
|
5341
7336
|
case "harp": {
|
|
5342
7337
|
const gap = Math.max(0, options.strum ?? DEFAULT_STRUM * 2);
|
|
5343
7338
|
return spanned.map((pitch, index) =>
|
|
@@ -5372,6 +7367,380 @@ function perform(
|
|
|
5372
7367
|
}
|
|
5373
7368
|
}
|
|
5374
7369
|
|
|
7370
|
+
// ---------------------------------------------------------------------------
|
|
7371
|
+
// Fretboard (0.6.1): guitar voicings and strokes
|
|
7372
|
+
|
|
7373
|
+
/**
|
|
7374
|
+
* Open-string MIDI pitches, low string to high, for `guitar tune <name>`.
|
|
7375
|
+
* Ported from the guitar design lane (proto/guitar/strum.ts TUNINGS).
|
|
7376
|
+
*/
|
|
7377
|
+
const GUITAR_TUNINGS = Object.freeze({
|
|
7378
|
+
standard: [40, 45, 50, 55, 59, 64],
|
|
7379
|
+
dropd: [38, 45, 50, 55, 59, 64],
|
|
7380
|
+
doubledropd: [38, 45, 50, 55, 59, 62],
|
|
7381
|
+
dadgad: [38, 45, 50, 55, 57, 62],
|
|
7382
|
+
openg: [38, 43, 50, 55, 59, 62],
|
|
7383
|
+
opend: [38, 45, 50, 54, 57, 62],
|
|
7384
|
+
opene: [40, 47, 52, 56, 59, 64],
|
|
7385
|
+
halfdown: [39, 44, 49, 54, 58, 63],
|
|
7386
|
+
nashville: [52, 57, 62, 67, 59, 64],
|
|
7387
|
+
bass: [28, 33, 38, 43],
|
|
7388
|
+
ukulele: [67, 60, 64, 69],
|
|
7389
|
+
requinto: [45, 50, 55, 60, 64, 69],
|
|
7390
|
+
} as const satisfies Record<string, readonly number[]>);
|
|
7391
|
+
type GuitarTuningName = keyof typeof GUITAR_TUNINGS;
|
|
7392
|
+
const GUITAR_TUNING_NAMES = Object.keys(GUITAR_TUNINGS) as GuitarTuningName[];
|
|
7393
|
+
|
|
7394
|
+
/** How a guitar is set up for voicing chords (`Track.guitar`). */
|
|
7395
|
+
type GuitarSetup = Readonly<{
|
|
7396
|
+
/** A GUITAR_TUNINGS name or open-string pitches low to high. */
|
|
7397
|
+
tune?: string | readonly number[];
|
|
7398
|
+
/** Capo fret 0..12. */
|
|
7399
|
+
capo?: number;
|
|
7400
|
+
/** Hand stretch in frets 3..6 (default 4: one fret per finger). */
|
|
7401
|
+
hand?: number;
|
|
7402
|
+
/** 0 closed shapes (no open strings) .. 1 prefer ringing open strings. */
|
|
7403
|
+
ring?: number;
|
|
7404
|
+
/** Preferred fret position 0..12 (0 open position). */
|
|
7405
|
+
position?: number;
|
|
7406
|
+
}>;
|
|
7407
|
+
|
|
7408
|
+
/** One fretted chord: frets per string low to high (-1 muted). */
|
|
7409
|
+
type GuitarVoicing = Readonly<{
|
|
7410
|
+
frets: readonly number[];
|
|
7411
|
+
/** Sounding pitches, low string to high. */
|
|
7412
|
+
pitches: readonly number[];
|
|
7413
|
+
/** The string each pitch sounds on. */
|
|
7414
|
+
strings: readonly number[];
|
|
7415
|
+
}>;
|
|
7416
|
+
|
|
7417
|
+
/** Open-string pitches for a tuning name or list (standard when unknown). */
|
|
7418
|
+
function guitarStrings(tune: GuitarSetup["tune"]): readonly number[] {
|
|
7419
|
+
if (Array.isArray(tune)) return tune as readonly number[];
|
|
7420
|
+
const name = String(tune ?? "standard")
|
|
7421
|
+
.toLowerCase()
|
|
7422
|
+
.replace(/[\s_-]/g, "");
|
|
7423
|
+
return (
|
|
7424
|
+
(GUITAR_TUNINGS as Record<string, readonly number[]>)[name] ??
|
|
7425
|
+
GUITAR_TUNINGS.standard
|
|
7426
|
+
);
|
|
7427
|
+
}
|
|
7428
|
+
|
|
7429
|
+
/**
|
|
7430
|
+
* Fingers a fretting uses. Strings at the lowest fret count as one barre
|
|
7431
|
+
* only when no open string lies between them (a barre cannot skip an open
|
|
7432
|
+
* string); otherwise one finger each.
|
|
7433
|
+
*/
|
|
7434
|
+
function guitarFingers(frets: readonly number[]): number {
|
|
7435
|
+
const fretted = frets.filter((f) => f > 0);
|
|
7436
|
+
if (fretted.length === 0) return 0;
|
|
7437
|
+
const min = Math.min(...fretted);
|
|
7438
|
+
const at = frets.flatMap((f, i) => (f === min ? [i] : []));
|
|
7439
|
+
const between = frets.slice(at[0]!, at[at.length - 1]! + 1);
|
|
7440
|
+
const barre = at.length > 1 && !between.includes(0);
|
|
7441
|
+
return (barre ? 1 : at.length) + fretted.filter((f) => f > min).length;
|
|
7442
|
+
}
|
|
7443
|
+
|
|
7444
|
+
/**
|
|
7445
|
+
* The chord tones a voicing must keep, most required first, after dropping
|
|
7446
|
+
* `drops` tones in the guitarist's order: the 5th, then the 9th; the root,
|
|
7447
|
+
* the 3rd and the 7th always stay (after the design lane's review).
|
|
7448
|
+
*/
|
|
7449
|
+
function requiredTones(tones: readonly number[], root: number, drops: number) {
|
|
7450
|
+
// The 5th, then the 11th (only beside a 3rd: a sus4 keeps its 4th), then
|
|
7451
|
+
// the 9th. dawg's symbols reach an 11th through m(add4)/madd4 (+9).
|
|
7452
|
+
const third = tones.some((pc) => [3, 4].includes(mod12(pc - root)));
|
|
7453
|
+
const order = (third ? [7, 5, 2] : [7, 2]).map((step) => mod12(root + step));
|
|
7454
|
+
const dropped = order.filter((pc) => tones.includes(pc)).slice(0, drops);
|
|
7455
|
+
return tones.filter((pc) => !dropped.includes(pc));
|
|
7456
|
+
}
|
|
7457
|
+
|
|
7458
|
+
/**
|
|
7459
|
+
* The most playable fretting of pitch classes `tones` over `bass` on a
|
|
7460
|
+
* guitar `setup`: the bass is the lowest sounding note, every required
|
|
7461
|
+
* tone sounds, at most four fingers within a `hand`-fret stretch, mutes
|
|
7462
|
+
* only under the bass or one inside, and no barre over an open string.
|
|
7463
|
+
* Cost prefers more strings, low positions, open strings (by `ring`), a
|
|
7464
|
+
* fifth present, no doubled third, and small moves from `previous`.
|
|
7465
|
+
* Undefined when nothing fits. Ported from proto/guitar/strum.ts.
|
|
7466
|
+
*/
|
|
7467
|
+
function voiceGuitar(
|
|
7468
|
+
tones: readonly number[],
|
|
7469
|
+
bass: number,
|
|
7470
|
+
root: number,
|
|
7471
|
+
setup: GuitarSetup = {},
|
|
7472
|
+
previous?: readonly number[],
|
|
7473
|
+
): GuitarVoicing | undefined {
|
|
7474
|
+
// Play mode voices every pad press; dense chords at a wide hand take
|
|
7475
|
+
// ~15 ms to search, so repeats come from a small LRU (results are frozen).
|
|
7476
|
+
const key = JSON.stringify([
|
|
7477
|
+
[...new Set([...tones, bass].map(mod12))],
|
|
7478
|
+
mod12(bass),
|
|
7479
|
+
mod12(root),
|
|
7480
|
+
setup.tune ?? null,
|
|
7481
|
+
setup.capo ?? null,
|
|
7482
|
+
setup.hand ?? null,
|
|
7483
|
+
setup.ring ?? null,
|
|
7484
|
+
setup.position ?? null,
|
|
7485
|
+
previous ?? null,
|
|
7486
|
+
]);
|
|
7487
|
+
if (voicingCache.has(key)) {
|
|
7488
|
+
const hit = voicingCache.get(key);
|
|
7489
|
+
voicingCache.delete(key);
|
|
7490
|
+
voicingCache.set(key, hit);
|
|
7491
|
+
return hit;
|
|
7492
|
+
}
|
|
7493
|
+
const voiced = searchGuitar(tones, bass, root, setup, previous);
|
|
7494
|
+
voicingCache.set(key, voiced);
|
|
7495
|
+
if (voicingCache.size > VOICING_CACHE_SIZE)
|
|
7496
|
+
voicingCache.delete(voicingCache.keys().next().value!);
|
|
7497
|
+
return voiced;
|
|
7498
|
+
}
|
|
7499
|
+
|
|
7500
|
+
const VOICING_CACHE_SIZE = 256;
|
|
7501
|
+
const voicingCache = new Map<string, GuitarVoicing | undefined>();
|
|
7502
|
+
|
|
7503
|
+
function searchGuitar(
|
|
7504
|
+
tones: readonly number[],
|
|
7505
|
+
bass: number,
|
|
7506
|
+
root: number,
|
|
7507
|
+
setup: GuitarSetup,
|
|
7508
|
+
previous?: readonly number[],
|
|
7509
|
+
): GuitarVoicing | undefined {
|
|
7510
|
+
const pcs = [...new Set([...tones, bass].map(mod12))];
|
|
7511
|
+
const capo = clampInt(setup.capo ?? 0, 0, 12);
|
|
7512
|
+
const open = guitarStrings(setup.tune).map((pitch) => pitch + capo);
|
|
7513
|
+
const n = open.length;
|
|
7514
|
+
const ring = Math.min(1, Math.max(0, setup.ring ?? 0.5));
|
|
7515
|
+
const stretch = clampInt(setup.hand ?? 4, 3, 6);
|
|
7516
|
+
const fifth = mod12(root + 7);
|
|
7517
|
+
const third = pcs.find((pc) => [3, 4].includes(mod12(pc - root)));
|
|
7518
|
+
const prev = previous ? [...previous].sort((a, b) => a - b) : undefined;
|
|
7519
|
+
for (let drops = 0; drops <= 3; drops += 1) {
|
|
7520
|
+
const required = requiredTones(pcs, root, drops);
|
|
7521
|
+
let best: { frets: number[]; cost: number } | undefined;
|
|
7522
|
+
const choose = (frets: readonly number[]) => {
|
|
7523
|
+
const idx = frets.flatMap((f, i) => (f < 0 ? [] : [i]));
|
|
7524
|
+
if (idx.length < Math.min(n, n >= 6 ? 4 : 3)) return;
|
|
7525
|
+
const interior = frets.slice(idx[0]).filter((f) => f < 0).length;
|
|
7526
|
+
if (interior > (n >= 6 ? 1 : 0)) return;
|
|
7527
|
+
const pitches = idx.map((i) => open[i]! + frets[i]!);
|
|
7528
|
+
if (mod12(Math.min(...pitches)) !== mod12(bass)) return;
|
|
7529
|
+
const sounding = new Set(pitches.map(mod12));
|
|
7530
|
+
if (required.some((pc) => !sounding.has(pc))) return;
|
|
7531
|
+
const fingers = guitarFingers(frets);
|
|
7532
|
+
if (fingers > 4) return;
|
|
7533
|
+
const fretted = frets.filter((f) => f > 0);
|
|
7534
|
+
const span = fretted.length
|
|
7535
|
+
? Math.max(...fretted) - Math.min(...fretted)
|
|
7536
|
+
: 0;
|
|
7537
|
+
if (span > stretch - 1) return;
|
|
7538
|
+
const opens = frets.filter((f) => f === 0).length;
|
|
7539
|
+
if (ring === 0 && opens > 0) return;
|
|
7540
|
+
const pos = fretted.length ? Math.min(...fretted) : 0;
|
|
7541
|
+
let cost =
|
|
7542
|
+
span * 0.6 +
|
|
7543
|
+
pos * 0.25 +
|
|
7544
|
+
fingers * 0.4 -
|
|
7545
|
+
idx.length -
|
|
7546
|
+
opens * 0.7 * ring;
|
|
7547
|
+
cost += interior * 1.5;
|
|
7548
|
+
if (!sounding.has(fifth) && pcs.includes(fifth)) cost += 0.6;
|
|
7549
|
+
if (
|
|
7550
|
+
third !== undefined &&
|
|
7551
|
+
pitches.filter((p) => mod12(p) === third).length > 1
|
|
7552
|
+
)
|
|
7553
|
+
cost += 0.5;
|
|
7554
|
+
if (setup.position !== undefined)
|
|
7555
|
+
cost += Math.abs(pos - setup.position) * 0.5;
|
|
7556
|
+
if (prev && prev.length) {
|
|
7557
|
+
const cur = [...pitches].sort((a, b) => a - b);
|
|
7558
|
+
let move = 0;
|
|
7559
|
+
for (let i = 0; i < cur.length; i += 1)
|
|
7560
|
+
move += Math.abs(cur[i]! - prev[Math.min(i, prev.length - 1)]!);
|
|
7561
|
+
cost += move * 0.05;
|
|
7562
|
+
}
|
|
7563
|
+
if (
|
|
7564
|
+
!best ||
|
|
7565
|
+
cost < best.cost - 1e-9 ||
|
|
7566
|
+
(Math.abs(cost - best.cost) <= 1e-9 && frets.join() < best.frets.join())
|
|
7567
|
+
)
|
|
7568
|
+
best = { frets: [...frets], cost };
|
|
7569
|
+
};
|
|
7570
|
+
for (let p = 1; p <= 12; p += 1) {
|
|
7571
|
+
const options = open.map((o) => {
|
|
7572
|
+
const list = [-1];
|
|
7573
|
+
if (pcs.includes(mod12(o))) list.push(0);
|
|
7574
|
+
for (let f = p; f < p + stretch; f += 1)
|
|
7575
|
+
if (pcs.includes(mod12(o + f))) list.push(f);
|
|
7576
|
+
return list;
|
|
7577
|
+
});
|
|
7578
|
+
const frets = new Array<number>(n).fill(-1);
|
|
7579
|
+
const walk = (s: number): void => {
|
|
7580
|
+
if (s === n) return choose(frets);
|
|
7581
|
+
for (const f of options[s]!) {
|
|
7582
|
+
frets[s] = f;
|
|
7583
|
+
walk(s + 1);
|
|
7584
|
+
}
|
|
7585
|
+
};
|
|
7586
|
+
walk(0);
|
|
7587
|
+
}
|
|
7588
|
+
if (best) {
|
|
7589
|
+
const { frets } = best as { frets: number[] };
|
|
7590
|
+
const strings = frets.flatMap((f, i) => (f < 0 ? [] : [i]));
|
|
7591
|
+
return Object.freeze({
|
|
7592
|
+
frets: Object.freeze(frets),
|
|
7593
|
+
pitches: Object.freeze(strings.map((i) => open[i]! + frets[i]!)),
|
|
7594
|
+
strings: Object.freeze(strings),
|
|
7595
|
+
});
|
|
7596
|
+
}
|
|
7597
|
+
}
|
|
7598
|
+
return undefined;
|
|
7599
|
+
}
|
|
7600
|
+
|
|
7601
|
+
/** A voicing as tab, low string first: `x 3 2 0 1 0`. */
|
|
7602
|
+
function guitarTab(frets: readonly number[]): string {
|
|
7603
|
+
return frets.map((f) => (f < 0 ? "x" : String(f))).join(" ");
|
|
7604
|
+
}
|
|
7605
|
+
|
|
7606
|
+
/**
|
|
7607
|
+
* Stroke grids for perform mode `guitar`, one character per step: D down
|
|
7608
|
+
* (all strings), U up (top four), d light down (top four), u light up (top
|
|
7609
|
+
* three), x muted chuck, `-` or `.` rest (strings ring on). After the
|
|
7610
|
+
* design lane's STRUM_PATTERNS.
|
|
7611
|
+
*/
|
|
7612
|
+
const STROKE_PATTERNS = Object.freeze({
|
|
7613
|
+
down: "D",
|
|
7614
|
+
folk: "D-DU-UDU",
|
|
7615
|
+
pop: "D-DU-UD-",
|
|
7616
|
+
punk: "DDDDDDDD",
|
|
7617
|
+
funk: "xUxUDUxUxUDUxUxU",
|
|
7618
|
+
reggae: "-D-D",
|
|
7619
|
+
waltz: "Ddd",
|
|
7620
|
+
jangle: "D-DUDUDU",
|
|
7621
|
+
island: "D-DU-UDU",
|
|
7622
|
+
} as const satisfies Record<string, string>);
|
|
7623
|
+
const STROKE_PATTERN_NAMES = Object.keys(STROKE_PATTERNS);
|
|
7624
|
+
const STROKE_CHARS = /^[DUdux.\-]+$/;
|
|
7625
|
+
|
|
7626
|
+
/** Default seconds a full six-string down stroke takes (22 ms). */
|
|
7627
|
+
const DEFAULT_STROKE_SPEED = 0.022;
|
|
7628
|
+
/** Default stroke grid step in beats (8ths). */
|
|
7629
|
+
const DEFAULT_STROKE_STEP = 0.5;
|
|
7630
|
+
|
|
7631
|
+
/** A stroke grid by name, or a literal grid of D U d u x - .; undefined if bad. */
|
|
7632
|
+
function strokeGrid(text: string | undefined): string | undefined {
|
|
7633
|
+
const raw = (text ?? "down").trim();
|
|
7634
|
+
const named = (STROKE_PATTERNS as Record<string, string>)[raw.toLowerCase()];
|
|
7635
|
+
if (named) return named;
|
|
7636
|
+
const grid = raw.replace(/[|\s]/g, "");
|
|
7637
|
+
return grid.length > 0 && grid.length <= 64 && STROKE_CHARS.test(grid)
|
|
7638
|
+
? grid
|
|
7639
|
+
: undefined;
|
|
7640
|
+
}
|
|
7641
|
+
|
|
7642
|
+
/**
|
|
7643
|
+
* Strum fretted `voicing` from `start` for `length` beats with stroke grid
|
|
7644
|
+
* `strokes` (repeats every grid length, one step per `step` beats). A full
|
|
7645
|
+
* down stroke sweeps the six strings in `speed` seconds at `tempo` BPM;
|
|
7646
|
+
* each struck string rings until it is struck again or the chord ends; a
|
|
7647
|
+
* chuck (x) is a short muted hit that stops the strings. Velocity accents
|
|
7648
|
+
* downbeats and lightens upstrokes.
|
|
7649
|
+
*/
|
|
7650
|
+
function strokeVoicing(
|
|
7651
|
+
voicing: GuitarVoicing,
|
|
7652
|
+
start: number,
|
|
7653
|
+
length: number,
|
|
7654
|
+
options: Readonly<{
|
|
7655
|
+
strokes?: string;
|
|
7656
|
+
step?: number;
|
|
7657
|
+
speed?: number;
|
|
7658
|
+
tempo?: number;
|
|
7659
|
+
velocity?: number;
|
|
7660
|
+
strings?: number;
|
|
7661
|
+
}> = {},
|
|
7662
|
+
): PerformedNote[] {
|
|
7663
|
+
const grid = strokeGrid(options.strokes) ?? "D";
|
|
7664
|
+
const step =
|
|
7665
|
+
options.step && options.step > 0 ? options.step : DEFAULT_STROKE_STEP;
|
|
7666
|
+
const tempo = options.tempo && options.tempo > 0 ? options.tempo : 120;
|
|
7667
|
+
const sweep =
|
|
7668
|
+
(Math.max(0, options.speed ?? DEFAULT_STROKE_SPEED) * tempo) / 60;
|
|
7669
|
+
const base = options.velocity ?? 0.8;
|
|
7670
|
+
const total = Math.max(2, options.strings ?? 6);
|
|
7671
|
+
const sounding = voicing.strings;
|
|
7672
|
+
const end = start + length;
|
|
7673
|
+
type Open = { note: PerformedNote; index: number };
|
|
7674
|
+
const ringing = new Map<number, Open>();
|
|
7675
|
+
const out: PerformedNote[] = [];
|
|
7676
|
+
const stop = (string: number, at: number) => {
|
|
7677
|
+
const held = ringing.get(string);
|
|
7678
|
+
if (!held) return;
|
|
7679
|
+
const cut = round6(
|
|
7680
|
+
Math.max(1e-3, Math.min(held.note.length, at - held.note.start)),
|
|
7681
|
+
);
|
|
7682
|
+
out[held.index] = { ...held.note, length: cut };
|
|
7683
|
+
ringing.delete(string);
|
|
7684
|
+
};
|
|
7685
|
+
const steps = Math.max(1, Math.round(length / step));
|
|
7686
|
+
for (let s = 0; s < steps; s += 1) {
|
|
7687
|
+
const ch = grid[s % grid.length]!;
|
|
7688
|
+
if (ch === "-" || ch === ".") continue;
|
|
7689
|
+
const t0 = start + s * step;
|
|
7690
|
+
if (t0 >= end - 1e-9) break;
|
|
7691
|
+
const beat = s * step;
|
|
7692
|
+
const accent =
|
|
7693
|
+
Math.abs(beat - Math.round(beat)) < 1e-9
|
|
7694
|
+
? Math.round(beat) % 2 === 0
|
|
7695
|
+
? 1
|
|
7696
|
+
: 0.92
|
|
7697
|
+
: 0.82;
|
|
7698
|
+
let order: number[];
|
|
7699
|
+
let velocity = base * accent;
|
|
7700
|
+
let ring = end - t0;
|
|
7701
|
+
const all = sounding.map((_, k) => k);
|
|
7702
|
+
switch (ch) {
|
|
7703
|
+
case "D":
|
|
7704
|
+
order = all;
|
|
7705
|
+
break;
|
|
7706
|
+
case "U":
|
|
7707
|
+
order = all.slice(-4).reverse();
|
|
7708
|
+
velocity *= 0.85;
|
|
7709
|
+
break;
|
|
7710
|
+
case "d":
|
|
7711
|
+
order = all.slice(-4);
|
|
7712
|
+
velocity *= 0.62;
|
|
7713
|
+
break;
|
|
7714
|
+
case "u":
|
|
7715
|
+
order = all.slice(-3).reverse();
|
|
7716
|
+
velocity *= 0.55;
|
|
7717
|
+
break;
|
|
7718
|
+
default:
|
|
7719
|
+
order = all;
|
|
7720
|
+
velocity *= 0.5;
|
|
7721
|
+
ring = Math.min(ring, 0.03 * (tempo / 60));
|
|
7722
|
+
}
|
|
7723
|
+
const first = sounding[order[0]!]!;
|
|
7724
|
+
order.forEach((k, position) => {
|
|
7725
|
+
const string = sounding[k]!;
|
|
7726
|
+
const at = t0 + (Math.abs(string - first) / (total - 1)) * sweep;
|
|
7727
|
+
if (at >= end - 1e-9) return;
|
|
7728
|
+
stop(string, at);
|
|
7729
|
+
const note: PerformedNote = {
|
|
7730
|
+
pitch: voicing.pitches[k]!,
|
|
7731
|
+
start: round6(at),
|
|
7732
|
+
length: round6(Math.max(1e-3, Math.min(ring, end - at))),
|
|
7733
|
+
velocity: round6(
|
|
7734
|
+
Math.max(0.05, Math.min(1, velocity * (1 - 0.04 * position))),
|
|
7735
|
+
),
|
|
7736
|
+
};
|
|
7737
|
+
out.push(note);
|
|
7738
|
+
if (ch !== "x") ringing.set(string, { note, index: out.length - 1 });
|
|
7739
|
+
});
|
|
7740
|
+
}
|
|
7741
|
+
return out.sort((a, b) => a.start - b.start || a.pitch - b.pitch);
|
|
7742
|
+
}
|
|
7743
|
+
|
|
5375
7744
|
// ---------------------------------------------------------------------------
|
|
5376
7745
|
// Progressions
|
|
5377
7746
|
|
|
@@ -5755,6 +8124,14 @@ function renderProgression(options: RenderOptions): RenderedProgression {
|
|
|
5755
8124
|
...perform(chord.pitches, at, span, {
|
|
5756
8125
|
...options.perform,
|
|
5757
8126
|
seed: (options.perform?.seed ?? 0) + index,
|
|
8127
|
+
...(options.perform?.mode === "guitar"
|
|
8128
|
+
? {
|
|
8129
|
+
root: options.chords[index]!.root,
|
|
8130
|
+
...(options.chords[index]!.bass !== undefined
|
|
8131
|
+
? { slash: options.chords[index]!.bass }
|
|
8132
|
+
: {}),
|
|
8133
|
+
}
|
|
8134
|
+
: {}),
|
|
5758
8135
|
}),
|
|
5759
8136
|
);
|
|
5760
8137
|
const source = options.chords[index]!;
|