@hraness/dawg 0.6.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 +34 -0
- package/DAWG.md +338 -51
- package/README.md +4 -4
- package/core/chords.ts +492 -4
- package/core/diff.ts +5 -0
- package/core/expression.ts +143 -6
- package/core/fx.ts +258 -6
- package/core/granular.ts +91 -0
- package/core/instruments.ts +161 -0
- package/core/keys.ts +573 -4
- package/core/resonators.ts +291 -3
- package/core/score.ts +349 -3
- package/core/sdk/print.ts +78 -3
- package/core/sdk/v1.ts +1320 -36
- package/core/sections.ts +12 -4
- package/core/strings.ts +237 -2
- package/core/winds.ts +652 -0
- package/guides/automation.md +1 -0
- package/guides/chords.md +3 -1
- package/guides/effects.md +3 -2
- package/guides/media.md +1 -1
- package/guides/performance.md +3 -0
- package/guides/resample.md +26 -0
- package/guides/sounds.md +7 -6
- package/package.json +1 -1
- package/src/agent/agent.ts +8 -2
- package/src/agent/chord-tools.ts +153 -1
- package/src/agent/expression-tools.ts +91 -0
- package/src/agent/ops.ts +4 -1
- package/src/agent/preview-tool.ts +2 -1
- package/src/agent/resample-tool.ts +131 -0
- package/src/agent/tools.ts +200 -15
- package/src/agent/xcb-agent.ts +8 -2
- package/src/audio/arrange.ts +21 -0
- package/src/audio/dsp/envelope.ts +161 -0
- package/src/audio/dsp/shift.ts +256 -0
- package/src/audio/effects/chain.ts +18 -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/engine.ts +46 -5
- package/src/audio/fit.ts +45 -0
- package/src/audio/granular.ts +352 -21
- package/src/audio/instrument-check.ts +9 -5
- package/src/audio/instruments.ts +32 -3
- package/src/audio/keys/electric.ts +420 -0
- package/src/audio/keys/engine.ts +143 -19
- package/src/audio/keys/organ.ts +1335 -0
- package/src/audio/keys/piano.ts +50 -6
- package/src/audio/keys/sympathetic.ts +127 -0
- package/src/audio/live-worker.ts +3 -1
- package/src/audio/live.ts +136 -9
- package/src/audio/preview.ts +15 -1
- package/src/audio/resample.ts +285 -0
- package/src/audio/resonators.ts +10 -2
- package/src/audio/sampler.ts +168 -21
- package/src/audio/samples.ts +26 -1
- package/src/audio/strings/body.ts +29 -16
- package/src/audio/strings/bow.ts +656 -0
- package/src/audio/strings/engine.ts +266 -20
- package/src/audio/strings/measure.test-helpers.ts +2 -0
- package/src/audio/wav.ts +119 -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 +103 -22
- package/src/commands/fx.ts +4 -1
- package/src/commands/granular.ts +7 -2
- package/src/commands/help.ts +91 -8
- package/src/commands/keys.ts +287 -11
- package/src/commands/modal.ts +67 -4
- package/src/commands/resample.ts +281 -0
- package/src/commands/rig.ts +5 -1
- package/src/commands/sample.ts +49 -2
- package/src/commands/shift.ts +119 -0
- package/src/commands/string.ts +28 -2
- package/src/commands/strum.ts +473 -0
- package/src/commands/wind.ts +244 -0
- package/src/main.ts +129 -7
- package/src/project/check.ts +5 -1
- package/src/render.ts +16 -0
- package/src/session/presence.ts +16 -3
- package/src/tui/audition.ts +1 -1
- package/src/tui/menu.ts +637 -35
- package/src/tui/modal-menu.ts +34 -7
- 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 +34 -9
- 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
|
|
|
@@ -1283,6 +1283,30 @@ export type SampleSpec = Readonly<{
|
|
|
1283
1283
|
fitmode?: "repitch" | "beats" | "tones";
|
|
1284
1284
|
/** Window length in beats (SDK 1.20.0); `fit` wins over `bpm`, `bpm` over `len`. */
|
|
1285
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;
|
|
1286
1310
|
}>;
|
|
1287
1311
|
|
|
1288
1312
|
/** Result of `sampler()`; pass it as a track's `instrument`. */
|
|
@@ -1446,9 +1470,13 @@ export const STRING_INSTRUMENT = "string";
|
|
|
1446
1470
|
* String engine settings (SDK 1.21.0): a `preset` (`nylon`, `steel`,
|
|
1447
1471
|
* `electric`, `jangle`, `ebass`, `slap`, `upright`, `sitar`, `tanpura`,
|
|
1448
1472
|
* `harpsichord`, `lute`, `oud`, `setar`, `tar`, `santur`, `dulcimer`, `koto`,
|
|
1449
|
-
* `harp`, `banjo`, `tres`, `requinto
|
|
1450
|
-
*
|
|
1451
|
-
*
|
|
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.
|
|
1452
1480
|
*/
|
|
1453
1481
|
export type StringInput = Readonly<
|
|
1454
1482
|
{ preset?: string } & Record<string, number | string | undefined>
|
|
@@ -1458,15 +1486,24 @@ export type StringInput = Readonly<
|
|
|
1458
1486
|
export type StringSpec = Readonly<{ kind: "string" } & StringInput>;
|
|
1459
1487
|
|
|
1460
1488
|
/**
|
|
1461
|
-
* A plucked string instrument (SDK 1.21.0
|
|
1489
|
+
* A plucked or bowed string instrument (SDK 1.21.0; bowed and the object
|
|
1490
|
+
* form 1.31.0): a preset and overrides.
|
|
1462
1491
|
*
|
|
1463
1492
|
* instrument: stringed("nylon")
|
|
1464
1493
|
* instrument: stringed("sitar", { buzz: 0.8, sym: 0.5 })
|
|
1494
|
+
* instrument: stringed({ preset: "cello", vib: 6 })
|
|
1465
1495
|
*/
|
|
1466
1496
|
export function stringed(
|
|
1467
|
-
preset = "nylon",
|
|
1497
|
+
preset: string | StringInput = "nylon",
|
|
1468
1498
|
params: Readonly<Record<string, number | string>> = {},
|
|
1469
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
|
+
}
|
|
1470
1507
|
if (typeof preset !== "string" || preset.length === 0)
|
|
1471
1508
|
throw new DawgSdkError("stringed needs a preset name");
|
|
1472
1509
|
if (!isRecord(params))
|
|
@@ -1567,7 +1604,23 @@ export type ModalPresetName =
|
|
|
1567
1604
|
| "steelpan"
|
|
1568
1605
|
| "bowl"
|
|
1569
1606
|
| "gong"
|
|
1570
|
-
| "timpani"
|
|
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";
|
|
1571
1624
|
|
|
1572
1625
|
/** Modal overrides; omitted means the preset's value. dawg validates ranges. */
|
|
1573
1626
|
export type ModalParams = Readonly<{
|
|
@@ -1600,6 +1653,12 @@ export type ModalParams = Readonly<{
|
|
|
1600
1653
|
gain?: number;
|
|
1601
1654
|
/** Mode table override (`marimba`, `bell`, `gong`, …). */
|
|
1602
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;
|
|
1603
1662
|
}>;
|
|
1604
1663
|
|
|
1605
1664
|
const MODAL_KEYS = Object.freeze([
|
|
@@ -1675,6 +1734,9 @@ const MODAL_RANGES: Readonly<Record<string, readonly [number, number]>> =
|
|
|
1675
1734
|
gain: [0, 2],
|
|
1676
1735
|
});
|
|
1677
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
|
+
|
|
1678
1740
|
const MODAL_PRESET_WORDS: readonly string[] = Object.freeze([
|
|
1679
1741
|
"marimba",
|
|
1680
1742
|
"vibes",
|
|
@@ -1688,6 +1750,21 @@ const MODAL_PRESET_WORDS: readonly string[] = Object.freeze([
|
|
|
1688
1750
|
"bowl",
|
|
1689
1751
|
"gong",
|
|
1690
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",
|
|
1691
1768
|
]);
|
|
1692
1769
|
|
|
1693
1770
|
/** Result of `modal()`; pass it as a track's `instrument`. */
|
|
@@ -1733,6 +1810,10 @@ export function modal(
|
|
|
1733
1810
|
`modal ${key} "${String(value).slice(0, 32)}" is not one of ${words.join(" ")}`,
|
|
1734
1811
|
);
|
|
1735
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;
|
|
1736
1817
|
} else if ((MODAL_KEYS as readonly string[]).includes(key)) {
|
|
1737
1818
|
const number = finite(value, `modal ${key}`);
|
|
1738
1819
|
const [min, max] = MODAL_RANGES[key]!;
|
|
@@ -1741,12 +1822,192 @@ export function modal(
|
|
|
1741
1822
|
out[key] = number;
|
|
1742
1823
|
} else
|
|
1743
1824
|
throw new DawgSdkError(
|
|
1744
|
-
`modal has no parameter "${key.slice(0, 32)}" (${MODAL_KEYS.join(" ")})`,
|
|
1825
|
+
`modal has no parameter "${key.slice(0, 32)}" (${MODAL_KEYS.join(" ")} pair)`,
|
|
1745
1826
|
);
|
|
1746
1827
|
}
|
|
1747
1828
|
return Object.freeze(out) as ModalSpec;
|
|
1748
1829
|
}
|
|
1749
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
|
+
|
|
1750
2011
|
/**
|
|
1751
2012
|
* `count` equal slices of one file as voices `prefix0 … prefixN-1`, for
|
|
1752
2013
|
* chopped breaks: `sampler(slices("samples/break.wav", 8, "brk"))`, then
|
|
@@ -1800,6 +2061,13 @@ function sampleSpec(value: string | SampleSpec, name: string): SampleSpec {
|
|
|
1800
2061
|
bpm?: number;
|
|
1801
2062
|
fitmode?: "repitch" | "beats" | "tones";
|
|
1802
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;
|
|
1803
2071
|
} = { src: spec.src };
|
|
1804
2072
|
if (spec.src.startsWith("pack:")) {
|
|
1805
2073
|
if (spec.sha256 !== undefined)
|
|
@@ -1862,6 +2130,27 @@ function sampleSpec(value: string | SampleSpec, name: string): SampleSpec {
|
|
|
1862
2130
|
out.fitmode = spec.fitmode;
|
|
1863
2131
|
}
|
|
1864
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`);
|
|
1865
2154
|
return Object.freeze(out);
|
|
1866
2155
|
}
|
|
1867
2156
|
|
|
@@ -2018,14 +2307,85 @@ export const RIG_PRESETS: Readonly<
|
|
|
2018
2307
|
head: { type: "chime", gain: 3, treble: 7 },
|
|
2019
2308
|
cab: { type: "2x12", mic: 0.2 },
|
|
2020
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.
|
|
2021
2313
|
},
|
|
2022
2314
|
alt: {
|
|
2023
2315
|
stomp: { type: "rat", gain: 6, tone: 0.4 },
|
|
2024
2316
|
head: { type: "crunch", gain: 4 },
|
|
2025
2317
|
cab: { type: "4x12" },
|
|
2026
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
|
+
},
|
|
2027
2359
|
});
|
|
2028
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
|
+
|
|
2029
2389
|
/**
|
|
2030
2390
|
* A guitar rig for a track's `fx` (SDK 1.22.0): the stomp → head → cab
|
|
2031
2391
|
* stages of rig preset `name` and its companion effects (as the `rig`
|
|
@@ -2093,15 +2453,21 @@ export type FxInput = Readonly<Record<string, EffectParams>>;
|
|
|
2093
2453
|
* Every parameter, range and default: **Synth** in DAWG.md.
|
|
2094
2454
|
*/
|
|
2095
2455
|
/**
|
|
2096
|
-
* Modelled
|
|
2456
|
+
* Modelled keys settings (SDK 1.24.0), for a track whose instrument is a
|
|
2097
2457
|
* piano family (`"grand"`, `"upright"`, `"felt"`, `"honkytonk"`,
|
|
2098
|
-
* `"prepared"`; the words `"ballad"` and `"lofi"` pick presets)
|
|
2099
|
-
*
|
|
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
|
|
2100
2462
|
* `automation.fx["keys-<param>"]` (hardness, touch, decay, release, knock,
|
|
2101
|
-
* noise, felt), read at each note's onset.
|
|
2463
|
+
* noise, felt; electric tone, vibe, trem), read at each note's onset.
|
|
2464
|
+
* Ranges: **Keys** in DAWG.md.
|
|
2102
2465
|
*/
|
|
2103
2466
|
export type KeysInput = Readonly<{
|
|
2104
|
-
/**
|
|
2467
|
+
/**
|
|
2468
|
+
* A named preset: grand ballad upright felt lofi honkytonk prepared, or
|
|
2469
|
+
* electric epiano suitcase dyno wurli clav funkclav.
|
|
2470
|
+
*/
|
|
2105
2471
|
preset?: string;
|
|
2106
2472
|
/** Hammer hardness 0..1: brightness at a given velocity (0.5). */
|
|
2107
2473
|
hardness?: number;
|
|
@@ -2137,6 +2503,53 @@ export type KeysInput = Readonly<{
|
|
|
2137
2503
|
vib?: number;
|
|
2138
2504
|
/** Pitch wobble depth in semitones (0.5). */
|
|
2139
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;
|
|
2140
2553
|
}>;
|
|
2141
2554
|
|
|
2142
2555
|
export type SynthInput = Readonly<{
|
|
@@ -2231,7 +2644,8 @@ export type TrackInput = Readonly<{
|
|
|
2231
2644
|
| WavetableSpec
|
|
2232
2645
|
| StringSpec
|
|
2233
2646
|
| GranularSpec
|
|
2234
|
-
| ModalSpec
|
|
2647
|
+
| ModalSpec
|
|
2648
|
+
| WindSpec;
|
|
2235
2649
|
/**
|
|
2236
2650
|
* The sampler a `granular(...)` track keeps while it grains one of its
|
|
2237
2651
|
* voices (SDK 1.23.0); `grain off` plays it again.
|
|
@@ -2302,6 +2716,16 @@ export type TrackInput = Readonly<{
|
|
|
2302
2716
|
glide?: number | Readonly<{ time?: number; mode?: GlideMode }>;
|
|
2303
2717
|
/** Sustain pedal changes as `[beat, "down" | "half" | "up"]` (SDK 1.15.0). */
|
|
2304
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"])[];
|
|
2305
2729
|
/**
|
|
2306
2730
|
* Velocity response (SDK 1.15.0): `soft` (quiet notes louder), `hard`
|
|
2307
2731
|
* (needs a firm touch), `fixed` (every note at 0.8, like an organ) or
|
|
@@ -2320,6 +2744,12 @@ export type TrackInput = Readonly<{
|
|
|
2320
2744
|
length?: number;
|
|
2321
2745
|
seed?: number;
|
|
2322
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;
|
|
2323
2753
|
/** `note()`/`seq()` for pitched tracks, `hit()`/`hits()` for kits and one-shot samplers. */
|
|
2324
2754
|
notes?: readonly (NoteSpec | HitSpec)[];
|
|
2325
2755
|
/**
|
|
@@ -2430,6 +2860,9 @@ export type TrackSpec = Readonly<{
|
|
|
2430
2860
|
/** Performance (SDK 1.15.0); present only when set. */
|
|
2431
2861
|
glide?: Readonly<{ time: number; mode: GlideMode }>;
|
|
2432
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])[];
|
|
2433
2866
|
velocityCurve?: Readonly<{
|
|
2434
2867
|
curve: Exclude<VelocityCurveName, "linear">;
|
|
2435
2868
|
fixed?: number;
|
|
@@ -2445,6 +2878,10 @@ export type TrackSpec = Readonly<{
|
|
|
2445
2878
|
keys?: KeysInput;
|
|
2446
2879
|
/** Modal settings (SDK 1.25.0); present only on a modal track. */
|
|
2447
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>;
|
|
2448
2885
|
}>;
|
|
2449
2886
|
|
|
2450
2887
|
export type GlideMode = "legato" | "mono" | "poly";
|
|
@@ -2458,10 +2895,17 @@ const DEFAULT_FIXED_VELOCITY = 0.8;
|
|
|
2458
2895
|
function trackPerformance(
|
|
2459
2896
|
input: TrackInput,
|
|
2460
2897
|
name: string,
|
|
2461
|
-
): Partial<
|
|
2898
|
+
): Partial<
|
|
2899
|
+
Pick<
|
|
2900
|
+
TrackSpec,
|
|
2901
|
+
"glide" | "pedal" | "softPedal" | "sostenuto" | "velocityCurve" | "humanize"
|
|
2902
|
+
>
|
|
2903
|
+
> {
|
|
2462
2904
|
const out: {
|
|
2463
2905
|
glide?: TrackSpec["glide"];
|
|
2464
2906
|
pedal?: TrackSpec["pedal"];
|
|
2907
|
+
softPedal?: TrackSpec["softPedal"];
|
|
2908
|
+
sostenuto?: TrackSpec["sostenuto"];
|
|
2465
2909
|
velocityCurve?: TrackSpec["velocityCurve"];
|
|
2466
2910
|
humanize?: TrackSpec["humanize"];
|
|
2467
2911
|
} = {};
|
|
@@ -2482,29 +2926,38 @@ function trackPerformance(
|
|
|
2482
2926
|
mode,
|
|
2483
2927
|
});
|
|
2484
2928
|
}
|
|
2485
|
-
|
|
2486
|
-
|
|
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)
|
|
2487
2937
|
throw new DawgSdkError(
|
|
2488
|
-
`track ${name}:
|
|
2938
|
+
`track ${name}: ${key} must be at most 1024 ${shape} events`,
|
|
2489
2939
|
);
|
|
2490
|
-
if (
|
|
2491
|
-
out
|
|
2492
|
-
|
|
2940
|
+
if (value.length > 0)
|
|
2941
|
+
out[key] = Object.freeze(
|
|
2942
|
+
value.map((event: unknown, index: number) => {
|
|
2493
2943
|
if (
|
|
2494
2944
|
!Array.isArray(event) ||
|
|
2495
2945
|
event.length !== 2 ||
|
|
2496
|
-
!
|
|
2946
|
+
!states.includes(event[1] as PedalState)
|
|
2497
2947
|
)
|
|
2498
2948
|
throw new DawgSdkError(
|
|
2499
|
-
`track ${name}:
|
|
2949
|
+
`track ${name}: ${key}[${index}] must be ${shape}`,
|
|
2500
2950
|
);
|
|
2501
2951
|
return Object.freeze([
|
|
2502
|
-
beat(event[0], `${name}
|
|
2952
|
+
beat(event[0], `${name} ${key}[${index}] beat`),
|
|
2503
2953
|
event[1] as PedalState,
|
|
2504
2954
|
] as const);
|
|
2505
2955
|
}),
|
|
2506
2956
|
);
|
|
2507
|
-
}
|
|
2957
|
+
};
|
|
2958
|
+
pedalLane("pedal", ["down", "half", "up"]);
|
|
2959
|
+
pedalLane("softPedal", ["down", "half", "up"]);
|
|
2960
|
+
pedalLane("sostenuto", ["down", "up"]);
|
|
2508
2961
|
if (input.velocityCurve !== undefined) {
|
|
2509
2962
|
const raw =
|
|
2510
2963
|
typeof input.velocityCurve === "string"
|
|
@@ -2679,6 +3132,8 @@ export function track(input: TrackInput): TrackSpec {
|
|
|
2679
3132
|
? resolveInstrumentWord(rawInstrument)
|
|
2680
3133
|
: undefined;
|
|
2681
3134
|
const modalSpec = trackModal(rawInstrument);
|
|
3135
|
+
const guitarSpec = guitarInput(input.guitar, `track ${name}`);
|
|
3136
|
+
const windSpec = trackWind(rawInstrument);
|
|
2682
3137
|
const instrument = granularFromInstrument
|
|
2683
3138
|
? GRANULAR_INSTRUMENT
|
|
2684
3139
|
: samplerSpec
|
|
@@ -2689,9 +3144,11 @@ export function track(input: TrackInput): TrackSpec {
|
|
|
2689
3144
|
? STRING_INSTRUMENT
|
|
2690
3145
|
: modalSpec
|
|
2691
3146
|
? MODAL_INSTRUMENT
|
|
2692
|
-
:
|
|
2693
|
-
?
|
|
2694
|
-
:
|
|
3147
|
+
: windSpec
|
|
3148
|
+
? WIND_INSTRUMENT
|
|
3149
|
+
: typeof rawInstrument === "string"
|
|
3150
|
+
? (word?.instrument ?? rawInstrument)
|
|
3151
|
+
: undefined;
|
|
2695
3152
|
// A granular word (`"cloud"`) turns the engine on with its preset.
|
|
2696
3153
|
const granularSpec =
|
|
2697
3154
|
granularInput(input.granular, name, slug) ??
|
|
@@ -2835,6 +3292,13 @@ export function track(input: TrackInput): TrackSpec {
|
|
|
2835
3292
|
`${name} delay`,
|
|
2836
3293
|
),
|
|
2837
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
|
+
}
|
|
2838
3302
|
const reverb =
|
|
2839
3303
|
input.reverb === undefined || input.reverb === null
|
|
2840
3304
|
? null
|
|
@@ -2907,6 +3371,8 @@ export function track(input: TrackInput): TrackSpec {
|
|
|
2907
3371
|
tuning: tuningSpec(input.tuning, `track ${name}`),
|
|
2908
3372
|
...keysSpec(input.keys, rawInstrument, name),
|
|
2909
3373
|
...(modalSpec ? { modal: modalSpec } : {}),
|
|
3374
|
+
...(guitarSpec ? { guitar: guitarSpec } : {}),
|
|
3375
|
+
...(windSpec ? { wind: windSpec } : {}),
|
|
2910
3376
|
});
|
|
2911
3377
|
}
|
|
2912
3378
|
|
|
@@ -2931,6 +3397,12 @@ const KEYS_PRESET_FX: Readonly<
|
|
|
2931
3397
|
filter: { cutoff: 3500, resonance: 0.1 },
|
|
2932
3398
|
fx: { crush: { bits: 10 } },
|
|
2933
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 } },
|
|
2934
3406
|
});
|
|
2935
3407
|
|
|
2936
3408
|
/**
|
|
@@ -2975,6 +3447,13 @@ function keysSpec(
|
|
|
2975
3447
|
out.preset = preset;
|
|
2976
3448
|
for (const [key, value] of Object.entries(input)) {
|
|
2977
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
|
+
}
|
|
2978
3457
|
out[key] = effectValue(value, `${name} keys.${key}`);
|
|
2979
3458
|
}
|
|
2980
3459
|
return { keys: Object.freeze(out) };
|
|
@@ -3431,6 +3910,11 @@ export type ScoreSampleRef = Readonly<{
|
|
|
3431
3910
|
bpm?: number;
|
|
3432
3911
|
fitmode?: "repitch" | "beats" | "tones";
|
|
3433
3912
|
len?: number;
|
|
3913
|
+
shift?: number;
|
|
3914
|
+
formant?: number;
|
|
3915
|
+
fadeTime?: number;
|
|
3916
|
+
fadeInTime?: number;
|
|
3917
|
+
from?: SampleProvenanceSpec;
|
|
3434
3918
|
}>;
|
|
3435
3919
|
|
|
3436
3920
|
/** A stored track; optional fields are present only when set. */
|
|
@@ -3480,8 +3964,14 @@ export type ScoreTrack = Readonly<{
|
|
|
3480
3964
|
granular?: GranularInput;
|
|
3481
3965
|
/** Modal settings (SDK 1.25.0). */
|
|
3482
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"];
|
|
3483
3971
|
glide?: TrackSpec["glide"];
|
|
3484
3972
|
pedal?: readonly Readonly<{ tick: number; state: PedalState }>[];
|
|
3973
|
+
softPedal?: readonly Readonly<{ tick: number; state: PedalState }>[];
|
|
3974
|
+
sostenuto?: readonly Readonly<{ tick: number; state: PedalState }>[];
|
|
3485
3975
|
velocityCurve?: TrackSpec["velocityCurve"];
|
|
3486
3976
|
humanize?: TrackSpec["humanize"];
|
|
3487
3977
|
}>;
|
|
@@ -3771,11 +4261,13 @@ export function song(input: SongInput): Song {
|
|
|
3771
4261
|
if (Object.keys(time).length > 0) stored.time = Object.freeze(time);
|
|
3772
4262
|
}
|
|
3773
4263
|
if (t.glide) stored.glide = t.glide;
|
|
3774
|
-
|
|
4264
|
+
for (const key of ["pedal", "softPedal", "sostenuto"] as const) {
|
|
4265
|
+
const lane = t[key];
|
|
4266
|
+
if (!lane || lane.length === 0) continue;
|
|
3775
4267
|
// One event per tick (the last wins), in tick order, as dawg stores it.
|
|
3776
4268
|
const byTick = new Map<number, PedalState>();
|
|
3777
|
-
for (const [at, state] of
|
|
3778
|
-
stored
|
|
4269
|
+
for (const [at, state] of lane) byTick.set(ticks(at), state);
|
|
4270
|
+
stored[key] = Object.freeze(
|
|
3779
4271
|
[...byTick.entries()]
|
|
3780
4272
|
.sort((a, b) => a[0] - b[0])
|
|
3781
4273
|
.map(([tick, state]) => Object.freeze({ tick, state })),
|
|
@@ -3787,6 +4279,8 @@ export function song(input: SongInput): Song {
|
|
|
3787
4279
|
if (t.string) stored.string = t.string;
|
|
3788
4280
|
if (t.keys) stored.keys = t.keys;
|
|
3789
4281
|
if (t.modal) stored.modal = t.modal;
|
|
4282
|
+
if (t.guitar) stored.guitar = t.guitar;
|
|
4283
|
+
if (t.wind) stored.wind = t.wind;
|
|
3790
4284
|
if (t.rhythm && t.rhythm.length > 0)
|
|
3791
4285
|
stored.rhythm = Object.freeze(
|
|
3792
4286
|
t.rhythm.map((row) => {
|
|
@@ -3831,6 +4325,25 @@ export function song(input: SongInput): Song {
|
|
|
3831
4325
|
);
|
|
3832
4326
|
}
|
|
3833
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
|
+
}
|
|
3834
4347
|
const arrangement: {
|
|
3835
4348
|
sections?: readonly SongSection[];
|
|
3836
4349
|
form?: readonly SongFormEntry[];
|
|
@@ -4495,7 +5008,22 @@ export type ChordOptions = Readonly<{
|
|
|
4495
5008
|
| "arp-random"
|
|
4496
5009
|
| "harp"
|
|
4497
5010
|
| "slop"
|
|
4498
|
-
| "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;
|
|
4499
5027
|
/**
|
|
4500
5028
|
* With `perform: "pattern"`: a rhythm pattern by name or 1-based number
|
|
4501
5029
|
* (SDK 1.7.0): `eighths`, `sixteenths`, `offbeat`, `pop`, `charleston`,
|
|
@@ -4646,6 +5174,7 @@ export function progression(
|
|
|
4646
5174
|
seed: finite(options.seed ?? 0, "chord seed"),
|
|
4647
5175
|
...(options.pattern !== undefined ? { pattern: options.pattern } : {}),
|
|
4648
5176
|
velocity: vel,
|
|
5177
|
+
...(mode === "guitar" ? guitarPerform(options) : {}),
|
|
4649
5178
|
},
|
|
4650
5179
|
});
|
|
4651
5180
|
const out = [
|
|
@@ -4659,6 +5188,114 @@ export function progression(
|
|
|
4659
5188
|
);
|
|
4660
5189
|
}
|
|
4661
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
|
+
|
|
4662
5299
|
// BEGIN instrument words: generated from core/instruments.ts by core/sdk/sync-instruments.ts
|
|
4663
5300
|
/** What an instrument word stores on a track. */
|
|
4664
5301
|
type InstrumentWord = Readonly<{
|
|
@@ -4780,6 +5417,42 @@ const INSTRUMENT_WORDS: readonly InstrumentWordRow[] = Object.freeze([
|
|
|
4780
5417
|
preset: "dulcimer",
|
|
4781
5418
|
},
|
|
4782
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
|
+
},
|
|
4783
5456
|
// f06-rig: guitar track aliases, a guitar voice plus a whole rig. The
|
|
4784
5457
|
// voice is the strings lane's `electric` row (jangle: its 12-string
|
|
4785
5458
|
// `jangle` preset); the pluck only while that row is absent. Never `lead`
|
|
@@ -4844,6 +5517,39 @@ const INSTRUMENT_WORDS: readonly InstrumentWordRow[] = Object.freeze([
|
|
|
4844
5517
|
field: "keys",
|
|
4845
5518
|
preset: "prepared",
|
|
4846
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" },
|
|
4847
5553
|
// f06-modal: mallets and bells (core/resonators.ts). `marimba` is legacy;
|
|
4848
5554
|
// `modal` alone gives the modal marimba.
|
|
4849
5555
|
{ word: "modal", instrument: "modal", field: "modal", preset: "marimba" },
|
|
@@ -4897,6 +5603,98 @@ const INSTRUMENT_WORDS: readonly InstrumentWordRow[] = Object.freeze([
|
|
|
4897
5603
|
field: "modal",
|
|
4898
5604
|
preset: "chimes",
|
|
4899
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" },
|
|
4900
5698
|
]);
|
|
4901
5699
|
|
|
4902
5700
|
/**
|
|
@@ -5014,6 +5812,12 @@ type Chord = Readonly<{
|
|
|
5014
5812
|
extensions: readonly Extension[];
|
|
5015
5813
|
/** Slash bass pitch class, when not the root. */
|
|
5016
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;
|
|
5017
5821
|
}>;
|
|
5018
5822
|
|
|
5019
5823
|
function makeChord(
|
|
@@ -5021,6 +5825,7 @@ function makeChord(
|
|
|
5021
5825
|
quality: Quality,
|
|
5022
5826
|
extensions: Iterable<Extension> = [],
|
|
5023
5827
|
bass?: number,
|
|
5828
|
+
tensions: readonly number[] = [],
|
|
5024
5829
|
): Chord {
|
|
5025
5830
|
const held = new Set(extensions);
|
|
5026
5831
|
const own = SECRET_EXTENSION[quality];
|
|
@@ -5032,6 +5837,11 @@ function makeChord(
|
|
|
5032
5837
|
quality,
|
|
5033
5838
|
extensions: Object.freeze(ext),
|
|
5034
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
|
+
: {}),
|
|
5035
5845
|
});
|
|
5036
5846
|
}
|
|
5037
5847
|
|
|
@@ -5039,6 +5849,7 @@ function makeChord(
|
|
|
5039
5849
|
function chordIntervals(chord: Chord): number[] {
|
|
5040
5850
|
const set = new Set(QUALITY_INTERVALS[chord.quality]);
|
|
5041
5851
|
for (const ext of chord.extensions) set.add(EXTENSION_INTERVAL[ext]);
|
|
5852
|
+
for (const step of chord.tensions ?? []) set.add(step);
|
|
5042
5853
|
// m7 and M7 together keep both; 6 with m7 on a dim triad is the dim7's bb7.
|
|
5043
5854
|
return [...set].sort((a, b) => a - b);
|
|
5044
5855
|
}
|
|
@@ -5091,6 +5902,18 @@ const SECRET_SUFFIX: Readonly<Partial<Record<Quality, string>>> = Object.freeze(
|
|
|
5091
5902
|
|
|
5092
5903
|
/** Chord symbol suffix: `m7`, `maj9`, `7sus4`, `dim7`, `m7b5`, `6/9`. */
|
|
5093
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
|
+
}
|
|
5094
5917
|
const ext = new Set(chord.extensions);
|
|
5095
5918
|
const b7 = ext.has("m7");
|
|
5096
5919
|
const M7 = ext.has("M7");
|
|
@@ -5254,9 +6077,51 @@ const SUFFIXES: readonly (readonly [string, Quality, readonly Extension[]])[] =
|
|
|
5254
6077
|
["addb6", "b6", []],
|
|
5255
6078
|
["7#9", "7#9", []],
|
|
5256
6079
|
];
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
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
|
+
]);
|
|
5260
6125
|
|
|
5261
6126
|
const LETTER: Readonly<Record<string, number>> = Object.freeze({
|
|
5262
6127
|
c: 0,
|
|
@@ -5298,7 +6163,7 @@ function parseChord(symbol: string): Chord | undefined {
|
|
|
5298
6163
|
bass = parsePitchClass(match[4]);
|
|
5299
6164
|
if (bass === undefined) return undefined;
|
|
5300
6165
|
}
|
|
5301
|
-
return makeChord(root, entry.quality, entry.ext, bass);
|
|
6166
|
+
return makeChord(root, entry.quality, entry.ext, bass, entry.tensions);
|
|
5302
6167
|
}
|
|
5303
6168
|
|
|
5304
6169
|
// ---------------------------------------------------------------------------
|
|
@@ -6096,6 +6961,8 @@ const PERFORM_MODES = [
|
|
|
6096
6961
|
"harp",
|
|
6097
6962
|
"slop",
|
|
6098
6963
|
"pattern",
|
|
6964
|
+
// 0.6.1: fretboard-voiced strokes (see `voiceGuitar`, `strokeVoicing`).
|
|
6965
|
+
"guitar",
|
|
6099
6966
|
] as const;
|
|
6100
6967
|
type PerformMode = (typeof PERFORM_MODES)[number];
|
|
6101
6968
|
|
|
@@ -6115,6 +6982,23 @@ type PerformOptions = Readonly<{
|
|
|
6115
6982
|
pattern?: string | number;
|
|
6116
6983
|
/** 0..1. */
|
|
6117
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;
|
|
6118
7002
|
}>;
|
|
6119
7003
|
|
|
6120
7004
|
type PerformedNote = Readonly<{
|
|
@@ -6431,6 +7315,24 @@ function perform(
|
|
|
6431
7315
|
}
|
|
6432
7316
|
return out.sort((a, b) => a.start - b.start || a.pitch - b.pitch);
|
|
6433
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
|
+
}
|
|
6434
7336
|
case "harp": {
|
|
6435
7337
|
const gap = Math.max(0, options.strum ?? DEFAULT_STRUM * 2);
|
|
6436
7338
|
return spanned.map((pitch, index) =>
|
|
@@ -6465,6 +7367,380 @@ function perform(
|
|
|
6465
7367
|
}
|
|
6466
7368
|
}
|
|
6467
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
|
+
|
|
6468
7744
|
// ---------------------------------------------------------------------------
|
|
6469
7745
|
// Progressions
|
|
6470
7746
|
|
|
@@ -6848,6 +8124,14 @@ function renderProgression(options: RenderOptions): RenderedProgression {
|
|
|
6848
8124
|
...perform(chord.pitches, at, span, {
|
|
6849
8125
|
...options.perform,
|
|
6850
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
|
+
: {}),
|
|
6851
8135
|
}),
|
|
6852
8136
|
);
|
|
6853
8137
|
const source = options.chords[index]!;
|