@hraness/dawg 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/DAWG.md +269 -13
- package/README.md +4 -4
- package/core/diff.ts +4 -0
- package/core/fx.ts +437 -2
- package/core/granular.ts +528 -0
- package/core/instruments.ts +281 -0
- package/core/keys.ts +386 -0
- package/core/resonators.ts +574 -0
- package/core/score.ts +181 -6
- package/core/sdk/eval-child.ts +2 -0
- package/core/sdk/print.ts +169 -13
- package/core/sdk/sync-instruments.ts +58 -0
- package/core/sdk/v1.ts +1114 -21
- package/core/sections.ts +34 -8
- package/core/strings.ts +845 -0
- package/guides/effects.md +3 -1
- package/guides/sounds.md +4 -0
- package/guides/tempo.md +1 -0
- package/package.json +1 -1
- package/src/agent/agent.ts +7 -0
- package/src/agent/granular-tools.ts +138 -0
- package/src/agent/models.ts +4 -4
- package/src/agent/ops.ts +39 -2
- package/src/agent/preview-tool.ts +6 -1
- package/src/agent/tools.ts +538 -14
- package/src/agent/xcb-agent.ts +7 -0
- package/src/audio/arrange.ts +21 -3
- package/src/audio/dsp/bank.ts +233 -0
- package/src/audio/dsp/fft.ts +6 -0
- package/src/audio/dsp/filters.ts +57 -0
- package/src/audio/dsp/interp.ts +112 -0
- package/src/audio/dsp/modal.ts +684 -0
- package/src/audio/dsp/onset.ts +197 -0
- package/src/audio/dsp/oversample.ts +202 -0
- package/src/audio/dsp/rng.ts +37 -0
- package/src/audio/dsp/shape.ts +81 -0
- package/src/audio/dsp/stft.ts +73 -0
- package/src/audio/dsp/window.ts +77 -0
- package/src/audio/effects/chain.ts +6 -1
- package/src/audio/effects/rig/cab.ts +99 -0
- package/src/audio/effects/rig/filters.ts +152 -0
- package/src/audio/effects/rig/gate.ts +39 -0
- package/src/audio/effects/rig/head.ts +487 -0
- package/src/audio/effects/rig/index.ts +170 -0
- package/src/audio/effects/rig/section.ts +78 -0
- package/src/audio/effects/rig/stomp.ts +238 -0
- package/src/audio/engine.ts +10 -1
- package/src/audio/fit.ts +402 -0
- package/src/audio/granular.ts +664 -0
- package/src/audio/instrument-check.ts +133 -0
- package/src/audio/instruments.ts +111 -0
- package/src/audio/keys/dsp.ts +323 -0
- package/src/audio/keys/engine.ts +361 -0
- package/src/audio/keys/piano.ts +432 -0
- package/src/audio/live-worker.ts +54 -0
- package/src/audio/live.ts +231 -12
- package/src/audio/loudness.ts +65 -20
- package/src/audio/preview.ts +2 -0
- package/src/audio/resonators.ts +287 -0
- package/src/audio/sampler.ts +163 -17
- package/src/audio/samples.ts +30 -7
- package/src/audio/strings/body.ts +250 -0
- package/src/audio/strings/engine.ts +369 -0
- package/src/audio/strings/loop.ts +119 -0
- package/src/audio/strings/measure.test-helpers.ts +198 -0
- package/src/audio/strings/pluck.ts +354 -0
- package/src/audio/warp.ts +61 -0
- package/src/audio/wav.ts +78 -6
- package/src/commands/expression.ts +9 -2
- package/src/commands/fit.ts +135 -0
- package/src/commands/fx.ts +31 -0
- package/src/commands/granular.ts +427 -0
- package/src/commands/help.ts +74 -3
- package/src/commands/keys.ts +353 -0
- package/src/commands/modal.ts +247 -0
- package/src/commands/rig.ts +260 -0
- package/src/commands/sample.ts +3 -0
- package/src/commands/string.ts +175 -0
- package/src/commands/time.ts +4 -1
- package/src/main.ts +210 -8
- package/src/project/check.ts +13 -0
- package/src/render.ts +33 -4
- package/src/tui/audition.ts +2 -2
- package/src/tui/granular-menu.ts +278 -0
- package/src/tui/menu.ts +332 -9
- package/src/tui/modal-menu.ts +118 -0
- package/src/tui/play-session.ts +45 -2
package/core/score.ts
CHANGED
|
@@ -20,7 +20,19 @@ import {
|
|
|
20
20
|
type FxLane,
|
|
21
21
|
type TrackFx,
|
|
22
22
|
} from "./fx.ts";
|
|
23
|
+
import {
|
|
24
|
+
MODAL_INSTRUMENT,
|
|
25
|
+
normalizeModal,
|
|
26
|
+
type TrackModal,
|
|
27
|
+
} from "./resonators.ts";
|
|
23
28
|
import { normalizeSynth, type TrackSynth } from "./synth.ts";
|
|
29
|
+
import { normalizeString, type TrackString } from "./strings.ts";
|
|
30
|
+
import {
|
|
31
|
+
isGranularInstrument,
|
|
32
|
+
normalizeGranular,
|
|
33
|
+
type TrackGranular,
|
|
34
|
+
} from "./granular.ts";
|
|
35
|
+
import { normalizeKeys, type TrackKeys } from "./keys.ts";
|
|
24
36
|
import {
|
|
25
37
|
checkSongTime,
|
|
26
38
|
normalizeSongTime,
|
|
@@ -99,6 +111,11 @@ export const SCORE_LIMITS = Object.freeze({
|
|
|
99
111
|
maxSampleAccelerate: 8,
|
|
100
112
|
/** Sampler `squiz` ratio bound. */
|
|
101
113
|
maxSampleSquiz: 32,
|
|
114
|
+
/** Sampler `bpm` (source tempo) bounds. */
|
|
115
|
+
minSampleBpm: 20,
|
|
116
|
+
maxSampleBpm: 400,
|
|
117
|
+
/** Sampler `len` (window length in beats) bound. */
|
|
118
|
+
maxSampleLenBeats: 1024,
|
|
102
119
|
/** Per sample file, enforced by the decoder and the import tool. */
|
|
103
120
|
maxSampleFileBytes: 50 * 1024 * 1024,
|
|
104
121
|
maxSampleSeconds: 600,
|
|
@@ -373,6 +390,29 @@ export type Track = Readonly<{
|
|
|
373
390
|
* `ref`/`root` over the song tuning. Absent follows the song tuning.
|
|
374
391
|
*/
|
|
375
392
|
tuning?: Tuning;
|
|
393
|
+
/**
|
|
394
|
+
* String engine settings (`core/strings.ts`): a preset plus overrides.
|
|
395
|
+
* Played only when `instrument` is "string"; absent keeps today's voice.
|
|
396
|
+
*/
|
|
397
|
+
string?: TrackString;
|
|
398
|
+
/**
|
|
399
|
+
* Granular instrument settings (0.6, `core/granular.ts`). Played when
|
|
400
|
+
* `instrument` is `"granular"`; kept when the instrument changes.
|
|
401
|
+
*/
|
|
402
|
+
granular?: TrackGranular;
|
|
403
|
+
/**
|
|
404
|
+
* Optional (0.6): modelled piano settings (`core/keys.ts`), `{ preset?,
|
|
405
|
+
* ...overrides }`. The modelled piano plays only when `instrument` is a
|
|
406
|
+
* piano family (grand upright felt honkytonk prepared) and this is
|
|
407
|
+
* present; `{}` is the family's defaults.
|
|
408
|
+
*/
|
|
409
|
+
keys?: TrackKeys;
|
|
410
|
+
/**
|
|
411
|
+
* Optional (0.6): modal percussion (`core/resonators.ts`), a preset plus
|
|
412
|
+
* overrides. Plays only when `instrument` is `"modal"`; any other
|
|
413
|
+
* instrument with this field is rejected. Absent keeps today's tone.
|
|
414
|
+
*/
|
|
415
|
+
modal?: TrackModal;
|
|
376
416
|
}> &
|
|
377
417
|
/**
|
|
378
418
|
* Performance (`core/expression.ts`): glide default, sustain pedal
|
|
@@ -449,8 +489,35 @@ export type SampleRef = Readonly<{
|
|
|
449
489
|
* (1 is off, up to 32).
|
|
450
490
|
*/
|
|
451
491
|
squiz?: number;
|
|
492
|
+
/**
|
|
493
|
+
* Optional: the sample's own tempo (20..400). The window then follows
|
|
494
|
+
* the song's tempo map (ramps included): a 174 BPM break in a 128 BPM
|
|
495
|
+
* song plays 128/174 as fast. `fitmode` picks how.
|
|
496
|
+
*/
|
|
497
|
+
bpm?: number;
|
|
498
|
+
/**
|
|
499
|
+
* Optional: how a fitted sample changes its time. `repitch` (default)
|
|
500
|
+
* changes speed and pitch together like tape; `beats` cuts the window at
|
|
501
|
+
* its onsets and places each slice on its new time, unstretched (drums,
|
|
502
|
+
* speech); `tones` time-stretches with a phase vocoder and keeps the
|
|
503
|
+
* pitch (pads, loops, vocals). Needs `bpm`, `len` or `fit`.
|
|
504
|
+
*/
|
|
505
|
+
fitmode?: SampleFitMode;
|
|
506
|
+
/**
|
|
507
|
+
* Optional: the window's length in beats (0 < len ≤ 1024), followed
|
|
508
|
+
* through the song's tempo map. `fit` wins over `bpm`, `bpm` over `len`.
|
|
509
|
+
*/
|
|
510
|
+
len?: number;
|
|
452
511
|
}>;
|
|
453
512
|
|
|
513
|
+
/** Sampler `fitmode`: how a fitted window changes its time. */
|
|
514
|
+
export type SampleFitMode = "repitch" | "beats" | "tones";
|
|
515
|
+
export const SAMPLE_FIT_MODES: readonly SampleFitMode[] = Object.freeze([
|
|
516
|
+
"repitch",
|
|
517
|
+
"beats",
|
|
518
|
+
"tones",
|
|
519
|
+
]);
|
|
520
|
+
|
|
454
521
|
/** Sample `unit`: rate, cycles (bars) or seconds. */
|
|
455
522
|
export type SampleUnit = "r" | "c" | "s";
|
|
456
523
|
export const SAMPLE_UNITS: readonly SampleUnit[] = Object.freeze([
|
|
@@ -683,6 +750,10 @@ export type TrackPatch = Readonly<
|
|
|
683
750
|
velocityCurve?: Track["velocityCurve"] | null;
|
|
684
751
|
humanize?: Track["humanize"] | null;
|
|
685
752
|
tuning?: Tuning | null;
|
|
753
|
+
string?: TrackString | null;
|
|
754
|
+
granular?: TrackGranular | null;
|
|
755
|
+
keys?: TrackKeys | null;
|
|
756
|
+
modal?: TrackModal | null;
|
|
686
757
|
}
|
|
687
758
|
>;
|
|
688
759
|
|
|
@@ -727,6 +798,10 @@ export type TrackInput = Readonly<
|
|
|
727
798
|
| "velocityCurve"
|
|
728
799
|
| "humanize"
|
|
729
800
|
| "tuning"
|
|
801
|
+
| "string"
|
|
802
|
+
| "granular"
|
|
803
|
+
| "keys"
|
|
804
|
+
| "modal"
|
|
730
805
|
> &
|
|
731
806
|
Pick<Track, "id"> & {
|
|
732
807
|
filter?: TrackFilter | null;
|
|
@@ -745,6 +820,10 @@ export type TrackInput = Readonly<
|
|
|
745
820
|
velocityCurve?: Track["velocityCurve"] | string | null;
|
|
746
821
|
humanize?: Track["humanize"] | null;
|
|
747
822
|
tuning?: Tuning | null;
|
|
823
|
+
string?: TrackString | null;
|
|
824
|
+
granular?: TrackGranular | null;
|
|
825
|
+
keys?: TrackKeys | null;
|
|
826
|
+
modal?: TrackModal | null;
|
|
748
827
|
}
|
|
749
828
|
>;
|
|
750
829
|
|
|
@@ -1134,9 +1213,19 @@ export function updateTrack(
|
|
|
1134
1213
|
): TrackScore {
|
|
1135
1214
|
if (!score.tracks.some((track) => track.id === trackId)) return score;
|
|
1136
1215
|
return score.withTracks(
|
|
1137
|
-
score.tracks.map((track) =>
|
|
1138
|
-
track.id
|
|
1139
|
-
|
|
1216
|
+
score.tracks.map((track) => {
|
|
1217
|
+
if (track.id !== trackId) return track;
|
|
1218
|
+
const next: Record<string, unknown> = { ...track, ...patch };
|
|
1219
|
+
// A modal field only belongs to a modal track: switching the
|
|
1220
|
+
// instrument away drops it unless the patch sets it too.
|
|
1221
|
+
if (
|
|
1222
|
+
patch.instrument !== undefined &&
|
|
1223
|
+
patch.modal === undefined &&
|
|
1224
|
+
patch.instrument !== MODAL_INSTRUMENT
|
|
1225
|
+
)
|
|
1226
|
+
delete next.modal;
|
|
1227
|
+
return next as Track;
|
|
1228
|
+
}),
|
|
1140
1229
|
);
|
|
1141
1230
|
}
|
|
1142
1231
|
|
|
@@ -1717,7 +1806,14 @@ function normalizeTrack(input: unknown): Track {
|
|
|
1717
1806
|
const fx = fxOrThrow(() => normalizeFx(input.fx));
|
|
1718
1807
|
const fxAutomation = normalizeFxAutomation(input.fxAutomation);
|
|
1719
1808
|
const synth = fxOrThrow(() => normalizeSynth(input.synth));
|
|
1809
|
+
const keys = fxOrThrow(() => normalizeKeys(input.keys));
|
|
1720
1810
|
const wavetable = normalizeWavetable(input.wavetable);
|
|
1811
|
+
const modal = fxOrThrow(() => normalizeModal(input.modal));
|
|
1812
|
+
if (modal && instrument !== MODAL_INSTRUMENT)
|
|
1813
|
+
throw new ScoreValidationError(
|
|
1814
|
+
`track ${id} has modal settings but its instrument is "${instrument}"`,
|
|
1815
|
+
"invalid-track",
|
|
1816
|
+
);
|
|
1721
1817
|
const sampler = normalizeSampler(input.sampler);
|
|
1722
1818
|
const rhythm = normalizeRhythm(input.rhythm, id);
|
|
1723
1819
|
const time = timeOrThrow(
|
|
@@ -1739,6 +1835,15 @@ function normalizeTrack(input: unknown): Track {
|
|
|
1739
1835
|
() => normalizeTuning(input.tuning, `track ${id} tuning`),
|
|
1740
1836
|
"invalid-track",
|
|
1741
1837
|
);
|
|
1838
|
+
const string = fxOrThrow(() => normalizeString(input.string));
|
|
1839
|
+
// A bare `granular` instrument (set_instrument, `instrument granular`)
|
|
1840
|
+
// gets an empty object, so the engine plays its defaults.
|
|
1841
|
+
const granular =
|
|
1842
|
+
fxOrThrow(() =>
|
|
1843
|
+
normalizeGranular(input.granular, (ref) =>
|
|
1844
|
+
normalizeSampleRef(ref, "granular src"),
|
|
1845
|
+
),
|
|
1846
|
+
) ?? (isGranularInstrument(instrument) ? Object.freeze({}) : undefined);
|
|
1742
1847
|
let kit: string | undefined;
|
|
1743
1848
|
if (input.kit !== undefined && input.kit !== null) {
|
|
1744
1849
|
const found =
|
|
@@ -1760,7 +1865,12 @@ function normalizeTrack(input: unknown): Track {
|
|
|
1760
1865
|
`track ${id} instrument "sampler" needs a sampler`,
|
|
1761
1866
|
"invalid-track",
|
|
1762
1867
|
);
|
|
1763
|
-
|
|
1868
|
+
// A granular track keeps the sampler it grains (`grain off` goes back).
|
|
1869
|
+
if (
|
|
1870
|
+
sampler &&
|
|
1871
|
+
!isSamplerInstrument(instrument) &&
|
|
1872
|
+
!isGranularInstrument(instrument)
|
|
1873
|
+
)
|
|
1764
1874
|
throw new ScoreValidationError(
|
|
1765
1875
|
`track ${id} has a sampler but its instrument is "${instrument}"`,
|
|
1766
1876
|
"invalid-track",
|
|
@@ -1793,6 +1903,10 @@ function normalizeTrack(input: unknown): Track {
|
|
|
1793
1903
|
...(time ? { time } : {}),
|
|
1794
1904
|
...performance,
|
|
1795
1905
|
...(tuning ? { tuning } : {}),
|
|
1906
|
+
...(string ? { string } : {}),
|
|
1907
|
+
...(granular ? { granular } : {}),
|
|
1908
|
+
...(keys ? { keys } : {}),
|
|
1909
|
+
...(modal ? { modal } : {}),
|
|
1796
1910
|
});
|
|
1797
1911
|
}
|
|
1798
1912
|
|
|
@@ -2000,6 +2114,9 @@ export function normalizeSampleRef(input: unknown, name: string): SampleRef {
|
|
|
2000
2114
|
fit?: boolean;
|
|
2001
2115
|
accelerate?: number;
|
|
2002
2116
|
squiz?: number;
|
|
2117
|
+
bpm?: number;
|
|
2118
|
+
fitmode?: SampleFitMode;
|
|
2119
|
+
len?: number;
|
|
2003
2120
|
} = { src };
|
|
2004
2121
|
if (input.sha256 !== undefined) {
|
|
2005
2122
|
if (typeof input.sha256 !== "string" || !SHA256_HEX.test(input.sha256))
|
|
@@ -2163,6 +2280,46 @@ export function normalizeSampleRef(input: unknown, name: string): SampleRef {
|
|
|
2163
2280
|
1,
|
|
2164
2281
|
SCORE_LIMITS.maxSampleSquiz,
|
|
2165
2282
|
);
|
|
2283
|
+
if (input.bpm !== undefined)
|
|
2284
|
+
ref.bpm = boundedNumber(
|
|
2285
|
+
input.bpm,
|
|
2286
|
+
`${label} bpm`,
|
|
2287
|
+
SCORE_LIMITS.minSampleBpm,
|
|
2288
|
+
SCORE_LIMITS.maxSampleBpm,
|
|
2289
|
+
);
|
|
2290
|
+
if (input.fitmode !== undefined) {
|
|
2291
|
+
if (!SAMPLE_FIT_MODES.includes(input.fitmode as SampleFitMode))
|
|
2292
|
+
throw new ScoreValidationError(
|
|
2293
|
+
`${label} fitmode must be "repitch", "beats" or "tones"`,
|
|
2294
|
+
"invalid-track",
|
|
2295
|
+
);
|
|
2296
|
+
ref.fitmode = input.fitmode as SampleFitMode;
|
|
2297
|
+
}
|
|
2298
|
+
if (input.len !== undefined) {
|
|
2299
|
+
const len = boundedNumber(
|
|
2300
|
+
input.len,
|
|
2301
|
+
`${label} len`,
|
|
2302
|
+
0,
|
|
2303
|
+
SCORE_LIMITS.maxSampleLenBeats,
|
|
2304
|
+
);
|
|
2305
|
+
if (len === 0)
|
|
2306
|
+
throw new ScoreValidationError(
|
|
2307
|
+
`${label} len must be greater than 0`,
|
|
2308
|
+
"invalid-track",
|
|
2309
|
+
);
|
|
2310
|
+
ref.len = len;
|
|
2311
|
+
}
|
|
2312
|
+
if (
|
|
2313
|
+
ref.fitmode !== undefined &&
|
|
2314
|
+
ref.fitmode !== "repitch" &&
|
|
2315
|
+
ref.bpm === undefined &&
|
|
2316
|
+
ref.len === undefined &&
|
|
2317
|
+
ref.fit !== true
|
|
2318
|
+
)
|
|
2319
|
+
throw new ScoreValidationError(
|
|
2320
|
+
`${label} fitmode ${ref.fitmode} needs bpm, len or fit`,
|
|
2321
|
+
"invalid-track",
|
|
2322
|
+
);
|
|
2166
2323
|
return Object.freeze(ref);
|
|
2167
2324
|
}
|
|
2168
2325
|
|
|
@@ -2403,10 +2560,28 @@ function normalizeAutomation(
|
|
|
2403
2560
|
return freezeArray(points);
|
|
2404
2561
|
}
|
|
2405
2562
|
|
|
2563
|
+
/** The note cap `normalizeNotes` enforces; see `withNoteCap`. */
|
|
2564
|
+
let noteCap: number = SCORE_LIMITS.maxNotes;
|
|
2565
|
+
|
|
2566
|
+
/**
|
|
2567
|
+
* Runs `fn` with the note cap raised to `cap`. Stored scores keep
|
|
2568
|
+
* SCORE_LIMITS.maxNotes; the arranged renderer bakes timed tracks (cycle,
|
|
2569
|
+
* rate) into plain notes and may hold more than a stored score allows.
|
|
2570
|
+
*/
|
|
2571
|
+
export function withNoteCap<T>(cap: number, fn: () => T): T {
|
|
2572
|
+
const previous = noteCap;
|
|
2573
|
+
noteCap = Math.max(previous, cap);
|
|
2574
|
+
try {
|
|
2575
|
+
return fn();
|
|
2576
|
+
} finally {
|
|
2577
|
+
noteCap = previous;
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2406
2581
|
function normalizeNotes(inputs: readonly unknown[]): Note[] {
|
|
2407
|
-
if (!Array.isArray(inputs) || inputs.length >
|
|
2582
|
+
if (!Array.isArray(inputs) || inputs.length > noteCap) {
|
|
2408
2583
|
throw new ScoreValidationError(
|
|
2409
|
-
`score cannot contain more than ${
|
|
2584
|
+
`score cannot contain more than ${noteCap} notes`,
|
|
2410
2585
|
"score-limit",
|
|
2411
2586
|
);
|
|
2412
2587
|
}
|
package/core/sdk/eval-child.ts
CHANGED
|
@@ -104,6 +104,8 @@ async function withSampleHashes(
|
|
|
104
104
|
const wavetable = track.wavetable;
|
|
105
105
|
if (isObject(wavetable) && isObject(wavetable.table))
|
|
106
106
|
refs.push(wavetable.table);
|
|
107
|
+
const granular = track.granular;
|
|
108
|
+
if (isObject(granular) && isObject(granular.src)) refs.push(granular.src);
|
|
107
109
|
}
|
|
108
110
|
for (const ref of refs) {
|
|
109
111
|
if (
|
package/core/sdk/print.ts
CHANGED
|
@@ -8,11 +8,25 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { DRUM_VOICES, isDrumInstrument } from "../drums.ts";
|
|
11
|
+
import type { TrackGranular } from "../granular.ts";
|
|
11
12
|
import { DEFAULT_FIXED_VELOCITY } from "../expression.ts";
|
|
12
13
|
import type { RhythmRow } from "../euclid.ts";
|
|
13
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
FX_LANES,
|
|
16
|
+
RIG_STAGES,
|
|
17
|
+
fxSpec,
|
|
18
|
+
rigPresetKeys,
|
|
19
|
+
rigPresetOf,
|
|
20
|
+
type FxName,
|
|
21
|
+
type FxValues,
|
|
22
|
+
} from "../fx.ts";
|
|
14
23
|
import { MASTER_SPECS, MASTER_UNITS, type SongMaster } from "../master.ts";
|
|
15
24
|
import { midiToPitch } from "../pitch.ts";
|
|
25
|
+
import {
|
|
26
|
+
MODAL_INSTRUMENT,
|
|
27
|
+
MODAL_PARAMS,
|
|
28
|
+
type TrackModal,
|
|
29
|
+
} from "../resonators.ts";
|
|
16
30
|
import { rhythmVoicePitch, rowInSync } from "../rhythm.ts";
|
|
17
31
|
import {
|
|
18
32
|
BUILTIN_TABLE_PREFIX,
|
|
@@ -35,6 +49,7 @@ import {
|
|
|
35
49
|
import { trackSlug } from "../slug.ts";
|
|
36
50
|
import { barStartTick } from "../tempo.ts";
|
|
37
51
|
import type { Tuning } from "../tuning.ts";
|
|
52
|
+
import { DEFAULT_STRING_PRESET, type TrackString } from "../strings.ts";
|
|
38
53
|
import { DEFAULT_HIT_LENGTH, DEFAULT_VELOCITY } from "./v1.ts";
|
|
39
54
|
|
|
40
55
|
const WIDTH = 80;
|
|
@@ -137,6 +152,9 @@ const RESERVED = new Set([
|
|
|
137
152
|
"every",
|
|
138
153
|
"sampler",
|
|
139
154
|
"wavetable",
|
|
155
|
+
"rig",
|
|
156
|
+
"granular",
|
|
157
|
+
"modal",
|
|
140
158
|
"slices",
|
|
141
159
|
"euclid",
|
|
142
160
|
"euclidRot",
|
|
@@ -381,22 +399,55 @@ export function printTrack(score: TrackScore, track: Track): string {
|
|
|
381
399
|
used.add("note");
|
|
382
400
|
return printNote(score, note, !kit && !slots, INDENT + INDENT);
|
|
383
401
|
});
|
|
402
|
+
const grained =
|
|
403
|
+
track.instrument === "granular" && track.granular !== undefined;
|
|
404
|
+
if (grained) used.add("granular");
|
|
384
405
|
if (track.sampler) used.add("sampler");
|
|
385
406
|
const wavetable = isWavetableInstrument(track.instrument)
|
|
386
407
|
? wavetableOf(track)
|
|
387
408
|
: undefined;
|
|
388
409
|
if (wavetable) used.add("wavetable");
|
|
410
|
+
const stringed = track.instrument === "string" && track.string !== undefined;
|
|
411
|
+
if (stringed) used.add("stringed");
|
|
412
|
+
const modalCall =
|
|
413
|
+
track.instrument === MODAL_INSTRUMENT && track.modal
|
|
414
|
+
? printModal(track.modal, INDENT)
|
|
415
|
+
: undefined;
|
|
416
|
+
if (modalCall?.startsWith("modal(")) used.add("modal");
|
|
389
417
|
|
|
390
418
|
const entries: string[] = [
|
|
391
419
|
`id: ${str(track.id)}`,
|
|
392
420
|
`name: ${str(track.name)}`,
|
|
393
421
|
];
|
|
394
|
-
if (
|
|
422
|
+
if (grained) {
|
|
423
|
+
entries.push(`instrument: ${printGranular(track.granular!, INDENT)}`);
|
|
424
|
+
// The sampler a granular track keeps for `grain off`.
|
|
425
|
+
if (track.sampler)
|
|
426
|
+
entries.push(`sampler: ${printSampler(track.sampler, INDENT)}`);
|
|
427
|
+
} else if (track.sampler) {
|
|
395
428
|
entries.push(`instrument: ${printSampler(track.sampler, INDENT)}`);
|
|
396
429
|
} else if (wavetable) {
|
|
397
430
|
entries.push(`instrument: ${printWavetable(wavetable, INDENT)}`);
|
|
431
|
+
} else if (stringed) {
|
|
432
|
+
entries.push(`instrument: ${printStringed(track.string!, INDENT)}`);
|
|
433
|
+
} else if (modalCall) {
|
|
434
|
+
entries.push(`instrument: ${modalCall}`);
|
|
398
435
|
} else entries.push(`instrument: ${str(track.instrument)}`);
|
|
399
436
|
if (track.kit) entries.push(`kit: ${str(track.kit)}`);
|
|
437
|
+
if (track.granular && !grained)
|
|
438
|
+
entries.push(
|
|
439
|
+
`granular: ${obj(
|
|
440
|
+
[
|
|
441
|
+
...(track.granular.preset
|
|
442
|
+
? [["preset", str(track.granular.preset)] as const]
|
|
443
|
+
: []),
|
|
444
|
+
...granularEntries(track.granular, INDENT),
|
|
445
|
+
],
|
|
446
|
+
INDENT,
|
|
447
|
+
"granular: ".length,
|
|
448
|
+
1,
|
|
449
|
+
)}`,
|
|
450
|
+
);
|
|
400
451
|
if (track.time) {
|
|
401
452
|
const beats = (ticks: number) => num(ticks / score.ticksPerBeat);
|
|
402
453
|
const fields: (readonly [string, string])[] = [];
|
|
@@ -480,18 +531,31 @@ export function printTrack(score: TrackScore, track: Track): string {
|
|
|
480
531
|
)}`,
|
|
481
532
|
);
|
|
482
533
|
if (track.fx) {
|
|
483
|
-
|
|
534
|
+
// A whole rig preset prints as `...rig("crunch")` (SDK 1.22.0).
|
|
535
|
+
const rigName = rigPresetOf(track.fx);
|
|
536
|
+
if (rigName) used.add("rig");
|
|
537
|
+
const effects = (Object.entries(track.fx) as [FxName, FxValues][]).filter(
|
|
538
|
+
([effect]) =>
|
|
539
|
+
!rigName ||
|
|
540
|
+
!(
|
|
541
|
+
(RIG_STAGES as readonly string[]).includes(effect) ||
|
|
542
|
+
rigPresetKeys(rigName).includes(effect)
|
|
543
|
+
),
|
|
544
|
+
);
|
|
484
545
|
const inner = INDENT + INDENT;
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
.
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
546
|
+
const rigLine = rigName ? [`${inner}...rig(${str(rigName)}),`] : [];
|
|
547
|
+
const body = rigLine.concat(
|
|
548
|
+
effects.map(([effect, values]) => {
|
|
549
|
+
// Only values that differ from the default: decoding fills the rest.
|
|
550
|
+
const params = Object.entries(fxSpec(effect).params)
|
|
551
|
+
.filter(
|
|
552
|
+
([key, spec]) =>
|
|
553
|
+
values[key] !== undefined && values[key] !== spec.default,
|
|
554
|
+
)
|
|
555
|
+
.map(([key]) => [key, value(values[key]!)] as const);
|
|
556
|
+
return `${inner}${effect}: ${params.length === 0 ? "{}" : obj(params, inner, `${effect}: `.length, 1)},`;
|
|
557
|
+
}),
|
|
558
|
+
);
|
|
495
559
|
entries.push(`fx: {\n${body.join("\n")}\n${INDENT}}`);
|
|
496
560
|
}
|
|
497
561
|
if (track.synth) {
|
|
@@ -506,6 +570,21 @@ export function printTrack(score: TrackScore, track: Track): string {
|
|
|
506
570
|
);
|
|
507
571
|
entries.push(`synth: ${obj(params, INDENT, "synth: ".length, 1)}`);
|
|
508
572
|
}
|
|
573
|
+
if (track.string && !stringed) {
|
|
574
|
+
const params = Object.entries(track.string).map(
|
|
575
|
+
([key, v]) => [key, value(v!)] as const,
|
|
576
|
+
);
|
|
577
|
+
entries.push(`string: ${obj(params, INDENT, "string: ".length, 1)}`);
|
|
578
|
+
}
|
|
579
|
+
if (track.keys) {
|
|
580
|
+
// `{}` is meaningful: it turns the modelled piano on.
|
|
581
|
+
const params = Object.entries(track.keys).map(
|
|
582
|
+
([key, v]) => [key, value(v)] as const,
|
|
583
|
+
);
|
|
584
|
+
entries.push(
|
|
585
|
+
`keys: ${params.length === 0 ? "{}" : obj(params, INDENT, "keys: ".length, 1)}`,
|
|
586
|
+
);
|
|
587
|
+
}
|
|
509
588
|
entries.push(...performanceEntries(score, track));
|
|
510
589
|
const lanes: [string, readonly AutomationPoint[] | undefined][] = [
|
|
511
590
|
["volume", track.volumeAutomation],
|
|
@@ -575,6 +654,10 @@ export function printTrack(score: TrackScore, track: Track): string {
|
|
|
575
654
|
"every",
|
|
576
655
|
"sampler",
|
|
577
656
|
"wavetable",
|
|
657
|
+
"stringed",
|
|
658
|
+
"rig",
|
|
659
|
+
"granular",
|
|
660
|
+
"modal",
|
|
578
661
|
"euclid",
|
|
579
662
|
"grid",
|
|
580
663
|
]
|
|
@@ -859,6 +942,76 @@ function printSampler(sampler: Sampler, indent: string): string {
|
|
|
859
942
|
return `sampler(\n${inner}${shifted},\n${inner}{ mode: ${str(sampler.mode)} },\n${indent})`;
|
|
860
943
|
}
|
|
861
944
|
|
|
945
|
+
/** `stringed("sitar", { buzz: 0.8 })`: the preset, then its overrides. */
|
|
946
|
+
function printStringed(settings: TrackString, indent: string): string {
|
|
947
|
+
const name = str(settings.preset ?? DEFAULT_STRING_PRESET);
|
|
948
|
+
const params = Object.entries(settings)
|
|
949
|
+
.filter(([key]) => key !== "preset")
|
|
950
|
+
.map(([key, v]) => [key, value(v!)] as const);
|
|
951
|
+
if (params.length === 0) return `stringed(${name})`;
|
|
952
|
+
const inline = `stringed(${name}, { ${params.map(([k, v]) => `${k}: ${v}`).join(", ")} })`;
|
|
953
|
+
if (indent.length + "instrument: ".length + inline.length + 1 <= WIDTH)
|
|
954
|
+
return inline;
|
|
955
|
+
const inner = indent + INDENT;
|
|
956
|
+
return `stringed(${name}, {\n${params.map(([k, v]) => `${inner}${k}: ${v},`).join("\n")}\n${indent}})`;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
/** `[key, literal]` for each stored granular field but the preset. */
|
|
960
|
+
function granularEntries(
|
|
961
|
+
settings: TrackGranular,
|
|
962
|
+
indent: string,
|
|
963
|
+
): (readonly [string, string])[] {
|
|
964
|
+
return Object.entries(settings)
|
|
965
|
+
.filter(([key]) => key !== "preset")
|
|
966
|
+
.map(([key, v]) => [
|
|
967
|
+
key,
|
|
968
|
+
typeof v === "object"
|
|
969
|
+
? printSample(v as SampleRef, indent + INDENT, `${key}: `.length)
|
|
970
|
+
: value(v as number | string | boolean),
|
|
971
|
+
]);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
/** `granular("cloud", { scan: 0.1 })`: the preset, then its overrides. */
|
|
975
|
+
function printGranular(settings: TrackGranular, indent: string): string {
|
|
976
|
+
const inner = indent + INDENT;
|
|
977
|
+
const params = granularEntries(settings, indent);
|
|
978
|
+
const name = settings.preset ? str(settings.preset) : undefined;
|
|
979
|
+
if (params.length === 0) return name ? `granular(${name})` : "granular()";
|
|
980
|
+
const head = name ? `granular(${name}, ` : "granular(";
|
|
981
|
+
const inline = `${head}{ ${params.map(([k, v]) => `${k}: ${v}`).join(", ")} })`;
|
|
982
|
+
if (
|
|
983
|
+
!inline.includes("\n") &&
|
|
984
|
+
indent.length + "instrument: ".length + inline.length + 1 <= WIDTH
|
|
985
|
+
)
|
|
986
|
+
return inline;
|
|
987
|
+
return `${head}{\n${params.map(([k, v]) => property(k, v, inner)).join("\n")}\n${indent}})`;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* A modal track's instrument: the preset word alone (`"vibes"`) when there
|
|
992
|
+
* are no overrides, else `modal("vibes", { motor: 4 })` with keys in
|
|
993
|
+
* `MODAL_PARAMS` order. `marimba` is a legacy word, so the modal marimba
|
|
994
|
+
* always prints as `modal("marimba")`.
|
|
995
|
+
*/
|
|
996
|
+
function printModal(settings: TrackModal, indent: string): string {
|
|
997
|
+
const params: [string, string][] = [];
|
|
998
|
+
for (const key of Object.keys(MODAL_PARAMS) as (keyof TrackModal)[]) {
|
|
999
|
+
const value = settings[key];
|
|
1000
|
+
if (value === undefined) continue;
|
|
1001
|
+
params.push([key, typeof value === "number" ? num(value) : str(value)]);
|
|
1002
|
+
}
|
|
1003
|
+
const preset = settings.preset;
|
|
1004
|
+
if (params.length === 0 && preset && preset !== "marimba") return str(preset);
|
|
1005
|
+
const head = preset ? str(preset) : "";
|
|
1006
|
+
if (params.length === 0) return `modal(${head})`;
|
|
1007
|
+
const lead = head ? `${head}, ` : "";
|
|
1008
|
+
const inline = `modal(${lead}{ ${params.map(([k, v]) => `${k}: ${v}`).join(", ")} })`;
|
|
1009
|
+
if (indent.length + "instrument: ".length + inline.length + 1 <= WIDTH)
|
|
1010
|
+
return inline;
|
|
1011
|
+
const inner = indent + INDENT;
|
|
1012
|
+
return `modal(${lead}{\n${params.map(([k, v]) => `${inner}${k}: ${v},`).join("\n")}\n${indent}})`;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
862
1015
|
/** `wavetable("basic", { wt: 0.5 })`; pack tables keep their pin. */
|
|
863
1016
|
function printWavetable(settings: TrackWavetable, indent: string): string {
|
|
864
1017
|
const src = settings.table.src;
|
|
@@ -915,6 +1068,9 @@ function printSample(ref: SampleRef, indent: string, prefix: number): string {
|
|
|
915
1068
|
if (ref.accelerate !== undefined)
|
|
916
1069
|
entries.push(["accelerate", num(ref.accelerate)]);
|
|
917
1070
|
if (ref.squiz !== undefined) entries.push(["squiz", num(ref.squiz)]);
|
|
1071
|
+
if (ref.bpm !== undefined) entries.push(["bpm", num(ref.bpm)]);
|
|
1072
|
+
if (ref.fitmode !== undefined) entries.push(["fitmode", str(ref.fitmode)]);
|
|
1073
|
+
if (ref.len !== undefined) entries.push(["len", num(ref.len)]);
|
|
918
1074
|
if (entries.length === 1) return str(ref.src);
|
|
919
1075
|
return obj(entries, indent, prefix, 1);
|
|
920
1076
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SDK is one vendored file with no imports, so `core/sdk/v1.ts` carries
|
|
3
|
+
* a copy of the instrument word resolver (`core/instruments.ts`) between two
|
|
4
|
+
* marker lines. `bun core/sdk/sync-instruments.ts` rewrites it and
|
|
5
|
+
* `sync-instruments.test.ts` fails when it is stale. Same scheme as
|
|
6
|
+
* `sync-chords.ts`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { format } from "prettier";
|
|
12
|
+
|
|
13
|
+
export const BEGIN_MARKER =
|
|
14
|
+
"// BEGIN instrument words: generated from core/instruments.ts by core/sdk/sync-instruments.ts";
|
|
15
|
+
export const END_MARKER = "// END instrument words";
|
|
16
|
+
|
|
17
|
+
const ROOT = join(import.meta.dir, "..", "..");
|
|
18
|
+
export const INSTRUMENTS_PATH = join(ROOT, "core", "instruments.ts");
|
|
19
|
+
export const SDK_PATH = join(ROOT, "core", "sdk", "v1.ts");
|
|
20
|
+
|
|
21
|
+
/** The resolver block (markers included) for a `core/instruments.ts` source. */
|
|
22
|
+
export function instrumentBlock(source: string): string {
|
|
23
|
+
const start = source.indexOf("// ---- instrument words");
|
|
24
|
+
if (start < 0) throw new Error("core/instruments.ts layout changed");
|
|
25
|
+
const body = source
|
|
26
|
+
.slice(source.indexOf("\n", start) + 1)
|
|
27
|
+
.replace(/^export /gm, "")
|
|
28
|
+
.trim();
|
|
29
|
+
return `${BEGIN_MARKER}\n${body}\n${END_MARKER}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** `sdk` with its resolver block replaced, formatted like the repo. */
|
|
33
|
+
export async function syncedSdk(
|
|
34
|
+
sdk: string,
|
|
35
|
+
instruments: string,
|
|
36
|
+
): Promise<string> {
|
|
37
|
+
const from = sdk.indexOf(BEGIN_MARKER);
|
|
38
|
+
const to = sdk.indexOf(END_MARKER);
|
|
39
|
+
if (from < 0 || to < 0)
|
|
40
|
+
throw new Error("v1.ts has no instrument word markers");
|
|
41
|
+
const next =
|
|
42
|
+
sdk.slice(0, from) +
|
|
43
|
+
instrumentBlock(instruments) +
|
|
44
|
+
sdk.slice(to + END_MARKER.length);
|
|
45
|
+
return format(next, { parser: "typescript", filepath: SDK_PATH });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (import.meta.main) {
|
|
49
|
+
const [sdk, instruments] = await Promise.all([
|
|
50
|
+
readFile(SDK_PATH, "utf8"),
|
|
51
|
+
readFile(INSTRUMENTS_PATH, "utf8"),
|
|
52
|
+
]);
|
|
53
|
+
const next = await syncedSdk(sdk, instruments);
|
|
54
|
+
if (next !== sdk) await writeFile(SDK_PATH, next);
|
|
55
|
+
console.log(
|
|
56
|
+
next === sdk ? "v1.ts instrument words up to date" : "v1.ts updated",
|
|
57
|
+
);
|
|
58
|
+
}
|