@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/score.ts
CHANGED
|
@@ -23,7 +23,10 @@ import {
|
|
|
23
23
|
import {
|
|
24
24
|
MODAL_INSTRUMENT,
|
|
25
25
|
normalizeModal,
|
|
26
|
+
normalizeWind,
|
|
27
|
+
WIND_INSTRUMENT,
|
|
26
28
|
type TrackModal,
|
|
29
|
+
type TrackWind,
|
|
27
30
|
} from "./resonators.ts";
|
|
28
31
|
import { normalizeSynth, type TrackSynth } from "./synth.ts";
|
|
29
32
|
import { normalizeString, type TrackString } from "./strings.ts";
|
|
@@ -32,7 +35,8 @@ import {
|
|
|
32
35
|
normalizeGranular,
|
|
33
36
|
type TrackGranular,
|
|
34
37
|
} from "./granular.ts";
|
|
35
|
-
import { normalizeKeys, type TrackKeys } from "./keys.ts";
|
|
38
|
+
import { isKeysFamily, normalizeKeys, type TrackKeys } from "./keys.ts";
|
|
39
|
+
import { GUITAR_TUNING_NAMES } from "./chords.ts";
|
|
36
40
|
import {
|
|
37
41
|
checkSongTime,
|
|
38
42
|
normalizeSongTime,
|
|
@@ -45,8 +49,11 @@ import {
|
|
|
45
49
|
import {
|
|
46
50
|
ExpressionValidationError,
|
|
47
51
|
normalizeNoteExpression,
|
|
52
|
+
normalizePedal,
|
|
48
53
|
normalizeTrackPerformance,
|
|
54
|
+
SOSTENUTO_STATES,
|
|
49
55
|
type NoteExpression,
|
|
56
|
+
type PedalEvent,
|
|
50
57
|
type NoteExpressionPatch,
|
|
51
58
|
type TrackPerformance,
|
|
52
59
|
} from "./expression.ts";
|
|
@@ -116,6 +123,12 @@ export const SCORE_LIMITS = Object.freeze({
|
|
|
116
123
|
maxSampleBpm: 400,
|
|
117
124
|
/** Sampler `len` (window length in beats) bound. */
|
|
118
125
|
maxSampleLenBeats: 1024,
|
|
126
|
+
/** Sampler `shift` and `formant` bound in semitones (0.6.1). */
|
|
127
|
+
maxSampleShift: 24,
|
|
128
|
+
/** Sampler `fadeTime`/`fadeInTime` bound in seconds (0.6.1). */
|
|
129
|
+
maxSampleFadeSeconds: 2,
|
|
130
|
+
/** Longest `resample` render in seconds (0.6.1). */
|
|
131
|
+
maxResampleSeconds: 600,
|
|
119
132
|
/** Per sample file, enforced by the decoder and the import tool. */
|
|
120
133
|
maxSampleFileBytes: 50 * 1024 * 1024,
|
|
121
134
|
maxSampleSeconds: 600,
|
|
@@ -413,6 +426,31 @@ export type Track = Readonly<{
|
|
|
413
426
|
* instrument with this field is rejected. Absent keeps today's tone.
|
|
414
427
|
*/
|
|
415
428
|
modal?: TrackModal;
|
|
429
|
+
/**
|
|
430
|
+
* Optional (0.6.1): una corda (soft pedal) events, same shape as `pedal`.
|
|
431
|
+
* Modelled pianos read it at each onset (half is half the shift); other
|
|
432
|
+
* instruments ignore it.
|
|
433
|
+
*/
|
|
434
|
+
softPedal?: readonly PedalEvent[];
|
|
435
|
+
/**
|
|
436
|
+
* Optional (0.6.1): sostenuto pedal events (down and up only). Keys held
|
|
437
|
+
* when it goes down ring until it lifts; later keys damp as usual.
|
|
438
|
+
*/
|
|
439
|
+
sostenuto?: readonly PedalEvent[];
|
|
440
|
+
/**
|
|
441
|
+
* Optional (0.6.1): how the track is fretted when chords are strummed
|
|
442
|
+
* (`strum`, perform mode `guitar`): tuning, capo, hand stretch, how much
|
|
443
|
+
* open strings ring and a preferred position. Notes only; never read at
|
|
444
|
+
* render, so it changes no sound by itself.
|
|
445
|
+
*/
|
|
446
|
+
guitar?: TrackGuitar;
|
|
447
|
+
/**
|
|
448
|
+
* Optional (0.6.1): a blown waveguide (`core/winds.ts`), a preset plus
|
|
449
|
+
* overrides. Plays only when `instrument` is `"wind"`; without it the
|
|
450
|
+
* legacy `wind` tone plays unchanged. Any other instrument with this
|
|
451
|
+
* field is rejected.
|
|
452
|
+
*/
|
|
453
|
+
wind?: TrackWind;
|
|
416
454
|
}> &
|
|
417
455
|
/**
|
|
418
456
|
* Performance (`core/expression.ts`): glide default, sustain pedal
|
|
@@ -508,6 +546,51 @@ export type SampleRef = Readonly<{
|
|
|
508
546
|
* through the song's tempo map. `fit` wins over `bpm`, `bpm` over `len`.
|
|
509
547
|
*/
|
|
510
548
|
len?: number;
|
|
549
|
+
/**
|
|
550
|
+
* Optional (0.6.1): pitch shift in semitones (−24..24) at constant
|
|
551
|
+
* length, a cached phase-vocoder preprocess (`src/audio/dsp/shift.ts`).
|
|
552
|
+
* Applied after any fit, so it follows `fitmode`.
|
|
553
|
+
*/
|
|
554
|
+
shift?: number;
|
|
555
|
+
/**
|
|
556
|
+
* Optional (0.6.1, with `shift`): formant shift in semitones (−24..24).
|
|
557
|
+
* Absent: formants follow the pitch (chipmunk); 0 keeps them in place.
|
|
558
|
+
*/
|
|
559
|
+
formant?: number;
|
|
560
|
+
/** Optional (0.6.1, Strudel `fadeTime`): release fade in seconds (0..2). */
|
|
561
|
+
fadeTime?: number;
|
|
562
|
+
/** Optional (0.6.1, Strudel `fadeInTime`): attack fade in seconds (0..2). */
|
|
563
|
+
fadeInTime?: number;
|
|
564
|
+
/**
|
|
565
|
+
* Optional (0.6.1): where `resample` rendered this file from; kept for
|
|
566
|
+
* reference only (never read by the renderer).
|
|
567
|
+
*/
|
|
568
|
+
from?: SampleProvenance;
|
|
569
|
+
/**
|
|
570
|
+
* Optional (0.6.1, SFZ `lovel`/`hivel`): the MIDI velocity range
|
|
571
|
+
* [lo, hi] (0..127, lo ≤ hi) this voice plays. Keyed voices with the same
|
|
572
|
+
* root are velocity layers; a one-shot voice outside its range is silent
|
|
573
|
+
* unless its `rr` group has a voice for that velocity.
|
|
574
|
+
*/
|
|
575
|
+
vel?: readonly [number, number];
|
|
576
|
+
/**
|
|
577
|
+
* Optional (0.6.1, SFZ `seq_length`): round-robin group name. One-shot
|
|
578
|
+
* voices in a group are one sound (any member's slot plays it); voices
|
|
579
|
+
* that match a note's velocity take turns, A B A B, in note order.
|
|
580
|
+
*/
|
|
581
|
+
rr?: string;
|
|
582
|
+
}>;
|
|
583
|
+
|
|
584
|
+
/** `SampleRef.from`: the source of a resampled file (0.6.1). */
|
|
585
|
+
export type SampleProvenance = Readonly<{
|
|
586
|
+
/** `track:<id>`, `orbit:<n>` or `master`. */
|
|
587
|
+
source: string;
|
|
588
|
+
/** Section name the render covered. */
|
|
589
|
+
section?: string;
|
|
590
|
+
/** Bars a..b (1-based, inclusive) the render covered. */
|
|
591
|
+
bars?: readonly [number, number];
|
|
592
|
+
/** sha256 of the canonical score JSON the file was rendered from. */
|
|
593
|
+
score: string;
|
|
511
594
|
}>;
|
|
512
595
|
|
|
513
596
|
/** Sampler `fitmode`: how a fitted window changes its time. */
|
|
@@ -541,14 +624,22 @@ export type TrackReverb = Readonly<{
|
|
|
541
624
|
predelay?: number;
|
|
542
625
|
/**
|
|
543
626
|
* Optional (Strudel `iresponse`/`ir`): convolve with this impulse instead
|
|
544
|
-
* of the algorithmic tail. `builtin:room|hall|plate`, a pinned pack sound
|
|
627
|
+
* of the algorithmic tail. `builtin:room|hall|plate|reverse|gate|spring`, a pinned pack sound
|
|
545
628
|
* or a project file; `size`, `fade` and `dim` then do nothing.
|
|
546
629
|
*/
|
|
547
630
|
ir?: SampleRef;
|
|
548
631
|
}>;
|
|
549
632
|
|
|
550
633
|
/** Generated impulse responses `reverb.ir` can name as `builtin:<name>`. */
|
|
551
|
-
export const REVERB_IR_BUILTINS = Object.freeze([
|
|
634
|
+
export const REVERB_IR_BUILTINS = Object.freeze([
|
|
635
|
+
"room",
|
|
636
|
+
"hall",
|
|
637
|
+
"plate",
|
|
638
|
+
// 0.6.1: a reverse (rising) wash, a gated tail, a spring tank.
|
|
639
|
+
"reverse",
|
|
640
|
+
"gate",
|
|
641
|
+
"spring",
|
|
642
|
+
]);
|
|
552
643
|
|
|
553
644
|
/**
|
|
554
645
|
* Validates `reverb.ir`: a string or `{ src, … }`. Bare built-in names and
|
|
@@ -754,6 +845,10 @@ export type TrackPatch = Readonly<
|
|
|
754
845
|
granular?: TrackGranular | null;
|
|
755
846
|
keys?: TrackKeys | null;
|
|
756
847
|
modal?: TrackModal | null;
|
|
848
|
+
softPedal?: Track["softPedal"] | null;
|
|
849
|
+
sostenuto?: Track["sostenuto"] | null;
|
|
850
|
+
guitar?: TrackGuitar | null;
|
|
851
|
+
wind?: TrackWind | null;
|
|
757
852
|
}
|
|
758
853
|
>;
|
|
759
854
|
|
|
@@ -802,6 +897,10 @@ export type TrackInput = Readonly<
|
|
|
802
897
|
| "granular"
|
|
803
898
|
| "keys"
|
|
804
899
|
| "modal"
|
|
900
|
+
| "softPedal"
|
|
901
|
+
| "sostenuto"
|
|
902
|
+
| "guitar"
|
|
903
|
+
| "wind"
|
|
805
904
|
> &
|
|
806
905
|
Pick<Track, "id"> & {
|
|
807
906
|
filter?: TrackFilter | null;
|
|
@@ -824,6 +923,10 @@ export type TrackInput = Readonly<
|
|
|
824
923
|
granular?: TrackGranular | null;
|
|
825
924
|
keys?: TrackKeys | null;
|
|
826
925
|
modal?: TrackModal | null;
|
|
926
|
+
softPedal?: Track["softPedal"] | null;
|
|
927
|
+
sostenuto?: Track["sostenuto"] | null;
|
|
928
|
+
guitar?: TrackGuitar | null;
|
|
929
|
+
wind?: TrackWind | null;
|
|
827
930
|
}
|
|
828
931
|
>;
|
|
829
932
|
|
|
@@ -1224,6 +1327,18 @@ export function updateTrack(
|
|
|
1224
1327
|
patch.instrument !== MODAL_INSTRUMENT
|
|
1225
1328
|
)
|
|
1226
1329
|
delete next.modal;
|
|
1330
|
+
// The legacy word `wind` is the legacy tone, so naming any instrument
|
|
1331
|
+
// (`wind` included) without a wind field leaves the wind engine.
|
|
1332
|
+
if (patch.instrument !== undefined && patch.wind === undefined)
|
|
1333
|
+
delete next.wind;
|
|
1334
|
+
// Keys settings only belong to a keys family (piano, electric, organ):
|
|
1335
|
+
// switching to another instrument drops them unless the patch sets them.
|
|
1336
|
+
if (
|
|
1337
|
+
patch.instrument !== undefined &&
|
|
1338
|
+
patch.keys === undefined &&
|
|
1339
|
+
!isKeysFamily(patch.instrument)
|
|
1340
|
+
)
|
|
1341
|
+
delete next.keys;
|
|
1227
1342
|
return next as Track;
|
|
1228
1343
|
}),
|
|
1229
1344
|
);
|
|
@@ -1814,6 +1929,13 @@ function normalizeTrack(input: unknown): Track {
|
|
|
1814
1929
|
`track ${id} has modal settings but its instrument is "${instrument}"`,
|
|
1815
1930
|
"invalid-track",
|
|
1816
1931
|
);
|
|
1932
|
+
const guitar = normalizeGuitar(input.guitar);
|
|
1933
|
+
const wind = fxOrThrow(() => normalizeWind(input.wind));
|
|
1934
|
+
if (wind && instrument !== WIND_INSTRUMENT)
|
|
1935
|
+
throw new ScoreValidationError(
|
|
1936
|
+
`track ${id} has wind settings but its instrument is "${instrument}"`,
|
|
1937
|
+
"invalid-track",
|
|
1938
|
+
);
|
|
1817
1939
|
const sampler = normalizeSampler(input.sampler);
|
|
1818
1940
|
const rhythm = normalizeRhythm(input.rhythm, id);
|
|
1819
1941
|
const time = timeOrThrow(
|
|
@@ -1835,6 +1957,28 @@ function normalizeTrack(input: unknown): Track {
|
|
|
1835
1957
|
() => normalizeTuning(input.tuning, `track ${id} tuning`),
|
|
1836
1958
|
"invalid-track",
|
|
1837
1959
|
);
|
|
1960
|
+
let softPedal: readonly PedalEvent[] | undefined;
|
|
1961
|
+
let sostenuto: readonly PedalEvent[] | undefined;
|
|
1962
|
+
try {
|
|
1963
|
+
softPedal = normalizePedal(
|
|
1964
|
+
input.softPedal,
|
|
1965
|
+
SCORE_LIMITS.maxTick,
|
|
1966
|
+
"softPedal",
|
|
1967
|
+
);
|
|
1968
|
+
sostenuto = normalizePedal(
|
|
1969
|
+
input.sostenuto,
|
|
1970
|
+
SCORE_LIMITS.maxTick,
|
|
1971
|
+
"sostenuto",
|
|
1972
|
+
SOSTENUTO_STATES,
|
|
1973
|
+
);
|
|
1974
|
+
} catch (error) {
|
|
1975
|
+
if (error instanceof ExpressionValidationError)
|
|
1976
|
+
throw new ScoreValidationError(
|
|
1977
|
+
`track ${id} ${error.message}`,
|
|
1978
|
+
"invalid-track",
|
|
1979
|
+
);
|
|
1980
|
+
throw error;
|
|
1981
|
+
}
|
|
1838
1982
|
const string = fxOrThrow(() => normalizeString(input.string));
|
|
1839
1983
|
// A bare `granular` instrument (set_instrument, `instrument granular`)
|
|
1840
1984
|
// gets an empty object, so the engine plays its defaults.
|
|
@@ -1907,9 +2051,89 @@ function normalizeTrack(input: unknown): Track {
|
|
|
1907
2051
|
...(granular ? { granular } : {}),
|
|
1908
2052
|
...(keys ? { keys } : {}),
|
|
1909
2053
|
...(modal ? { modal } : {}),
|
|
2054
|
+
...(softPedal ? { softPedal } : {}),
|
|
2055
|
+
...(sostenuto ? { sostenuto } : {}),
|
|
2056
|
+
...(guitar ? { guitar } : {}),
|
|
2057
|
+
...(wind ? { wind } : {}),
|
|
1910
2058
|
});
|
|
1911
2059
|
}
|
|
1912
2060
|
|
|
2061
|
+
/** Track.guitar (0.6.1): a tuning name or open-string pitches plus fretting. */
|
|
2062
|
+
export type TrackGuitar = Readonly<{
|
|
2063
|
+
/** A GUITAR_TUNINGS name or 3..12 open-string MIDI pitches, low to high. */
|
|
2064
|
+
tune?: string | readonly number[];
|
|
2065
|
+
/** Capo fret 0..12. */
|
|
2066
|
+
capo?: number;
|
|
2067
|
+
/** Hand stretch in frets 3..6 (default 4). */
|
|
2068
|
+
hand?: number;
|
|
2069
|
+
/** 0 closed shapes .. 1 ringing open strings (default 0.5). */
|
|
2070
|
+
ring?: number;
|
|
2071
|
+
/** Preferred fret position 0..12. */
|
|
2072
|
+
position?: number;
|
|
2073
|
+
}>;
|
|
2074
|
+
|
|
2075
|
+
/** Validates Track.guitar; `{}` and null normalize to absent. */
|
|
2076
|
+
export function normalizeGuitar(input: unknown): TrackGuitar | undefined {
|
|
2077
|
+
if (input === undefined || input === null) return undefined;
|
|
2078
|
+
const fail = (message: string): never => {
|
|
2079
|
+
throw new ScoreValidationError(`track guitar ${message}`, "invalid-track");
|
|
2080
|
+
};
|
|
2081
|
+
if (typeof input !== "object" || Array.isArray(input))
|
|
2082
|
+
fail("must be an object or null");
|
|
2083
|
+
const out: Record<string, unknown> = {};
|
|
2084
|
+
const record = input as Record<string, unknown>;
|
|
2085
|
+
for (const key of Object.keys(record))
|
|
2086
|
+
if (!["tune", "capo", "hand", "ring", "position"].includes(key))
|
|
2087
|
+
fail(`has no field "${key.slice(0, 32)}"`);
|
|
2088
|
+
const tune = record.tune;
|
|
2089
|
+
if (tune !== undefined && tune !== null) {
|
|
2090
|
+
if (typeof tune === "string") {
|
|
2091
|
+
const name = tune.toLowerCase().replace(/[\s_-]/g, "");
|
|
2092
|
+
if (!(GUITAR_TUNING_NAMES as readonly string[]).includes(name))
|
|
2093
|
+
fail(
|
|
2094
|
+
`tune must be one of ${GUITAR_TUNING_NAMES.join(", ")} or a list of pitches`,
|
|
2095
|
+
);
|
|
2096
|
+
out.tune = name;
|
|
2097
|
+
} else if (
|
|
2098
|
+
Array.isArray(tune) &&
|
|
2099
|
+
tune.length >= 3 &&
|
|
2100
|
+
tune.length <= 12 &&
|
|
2101
|
+
tune.every((p) => Number.isInteger(p) && p >= 0 && p <= 127)
|
|
2102
|
+
)
|
|
2103
|
+
out.tune = Object.freeze([...(tune as number[])]);
|
|
2104
|
+
else fail("tune must be a tuning name or 3..12 MIDI pitches");
|
|
2105
|
+
}
|
|
2106
|
+
const int = (key: string, min: number, max: number) => {
|
|
2107
|
+
const value = record[key];
|
|
2108
|
+
if (value === undefined || value === null) return;
|
|
2109
|
+
if (
|
|
2110
|
+
typeof value !== "number" ||
|
|
2111
|
+
!Number.isInteger(value) ||
|
|
2112
|
+
value < min ||
|
|
2113
|
+
value > max
|
|
2114
|
+
)
|
|
2115
|
+
fail(`${key} must be an integer ${min}..${max}`);
|
|
2116
|
+
out[key] = value;
|
|
2117
|
+
};
|
|
2118
|
+
int("capo", 0, 12);
|
|
2119
|
+
int("hand", 3, 6);
|
|
2120
|
+
const ring = record.ring;
|
|
2121
|
+
if (ring !== undefined && ring !== null) {
|
|
2122
|
+
if (
|
|
2123
|
+
typeof ring !== "number" ||
|
|
2124
|
+
!Number.isFinite(ring) ||
|
|
2125
|
+
ring < 0 ||
|
|
2126
|
+
ring > 1
|
|
2127
|
+
)
|
|
2128
|
+
fail("ring must be 0..1");
|
|
2129
|
+
out.ring = Math.round((ring as number) * 1000) / 1000;
|
|
2130
|
+
}
|
|
2131
|
+
int("position", 0, 12);
|
|
2132
|
+
return Object.keys(out).length > 0
|
|
2133
|
+
? (Object.freeze(out) as TrackGuitar)
|
|
2134
|
+
: undefined;
|
|
2135
|
+
}
|
|
2136
|
+
|
|
1913
2137
|
export function normalizeRhythm(
|
|
1914
2138
|
input: unknown,
|
|
1915
2139
|
trackId: string,
|
|
@@ -2032,6 +2256,8 @@ function trackEffectExtras(
|
|
|
2032
2256
|
}
|
|
2033
2257
|
|
|
2034
2258
|
const VOICE_NAME = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
2259
|
+
/** A sampler round-robin group name (`SampleRef.rr`). */
|
|
2260
|
+
const SAMPLE_RR_GROUP = /^[A-Za-z0-9_]{1,32}$/;
|
|
2035
2261
|
const SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
2036
2262
|
|
|
2037
2263
|
/** Validates a `Sampler` from unknown; `undefined`/`null` means none. */
|
|
@@ -2117,6 +2343,13 @@ export function normalizeSampleRef(input: unknown, name: string): SampleRef {
|
|
|
2117
2343
|
bpm?: number;
|
|
2118
2344
|
fitmode?: SampleFitMode;
|
|
2119
2345
|
len?: number;
|
|
2346
|
+
shift?: number;
|
|
2347
|
+
formant?: number;
|
|
2348
|
+
fadeTime?: number;
|
|
2349
|
+
fadeInTime?: number;
|
|
2350
|
+
from?: SampleProvenance;
|
|
2351
|
+
vel?: readonly [number, number];
|
|
2352
|
+
rr?: string;
|
|
2120
2353
|
} = { src };
|
|
2121
2354
|
if (input.sha256 !== undefined) {
|
|
2122
2355
|
if (typeof input.sha256 !== "string" || !SHA256_HEX.test(input.sha256))
|
|
@@ -2309,6 +2542,28 @@ export function normalizeSampleRef(input: unknown, name: string): SampleRef {
|
|
|
2309
2542
|
);
|
|
2310
2543
|
ref.len = len;
|
|
2311
2544
|
}
|
|
2545
|
+
if (input.vel !== undefined) {
|
|
2546
|
+
const vel = input.vel;
|
|
2547
|
+
if (
|
|
2548
|
+
!Array.isArray(vel) ||
|
|
2549
|
+
vel.length !== 2 ||
|
|
2550
|
+
!vel.every((v) => Number.isInteger(v) && v >= 0 && v <= 127) ||
|
|
2551
|
+
vel[0] > vel[1]
|
|
2552
|
+
)
|
|
2553
|
+
throw new ScoreValidationError(
|
|
2554
|
+
`${label} vel must be [lo, hi], MIDI velocities 0..127 with lo ≤ hi`,
|
|
2555
|
+
"invalid-track",
|
|
2556
|
+
);
|
|
2557
|
+
ref.vel = Object.freeze([vel[0] as number, vel[1] as number] as const);
|
|
2558
|
+
}
|
|
2559
|
+
if (input.rr !== undefined) {
|
|
2560
|
+
if (typeof input.rr !== "string" || !SAMPLE_RR_GROUP.test(input.rr))
|
|
2561
|
+
throw new ScoreValidationError(
|
|
2562
|
+
`${label} rr must be a round-robin group name (letters, digits, _; at most 32)`,
|
|
2563
|
+
"invalid-track",
|
|
2564
|
+
);
|
|
2565
|
+
ref.rr = input.rr;
|
|
2566
|
+
}
|
|
2312
2567
|
if (
|
|
2313
2568
|
ref.fitmode !== undefined &&
|
|
2314
2569
|
ref.fitmode !== "repitch" &&
|
|
@@ -2320,9 +2575,100 @@ export function normalizeSampleRef(input: unknown, name: string): SampleRef {
|
|
|
2320
2575
|
`${label} fitmode ${ref.fitmode} needs bpm, len or fit`,
|
|
2321
2576
|
"invalid-track",
|
|
2322
2577
|
);
|
|
2578
|
+
if (input.shift !== undefined)
|
|
2579
|
+
ref.shift = boundedNumber(
|
|
2580
|
+
input.shift,
|
|
2581
|
+
`${label} shift`,
|
|
2582
|
+
-SCORE_LIMITS.maxSampleShift,
|
|
2583
|
+
SCORE_LIMITS.maxSampleShift,
|
|
2584
|
+
);
|
|
2585
|
+
if (input.formant !== undefined)
|
|
2586
|
+
ref.formant = boundedNumber(
|
|
2587
|
+
input.formant,
|
|
2588
|
+
`${label} formant`,
|
|
2589
|
+
-SCORE_LIMITS.maxSampleShift,
|
|
2590
|
+
SCORE_LIMITS.maxSampleShift,
|
|
2591
|
+
);
|
|
2592
|
+
if (input.fadeTime !== undefined)
|
|
2593
|
+
ref.fadeTime = boundedNumber(
|
|
2594
|
+
input.fadeTime,
|
|
2595
|
+
`${label} fadeTime`,
|
|
2596
|
+
0,
|
|
2597
|
+
SCORE_LIMITS.maxSampleFadeSeconds,
|
|
2598
|
+
);
|
|
2599
|
+
if (input.fadeInTime !== undefined)
|
|
2600
|
+
ref.fadeInTime = boundedNumber(
|
|
2601
|
+
input.fadeInTime,
|
|
2602
|
+
`${label} fadeInTime`,
|
|
2603
|
+
0,
|
|
2604
|
+
SCORE_LIMITS.maxSampleFadeSeconds,
|
|
2605
|
+
);
|
|
2606
|
+
if (input.from !== undefined)
|
|
2607
|
+
ref.from = normalizeProvenance(input.from, `${label} from`);
|
|
2323
2608
|
return Object.freeze(ref);
|
|
2324
2609
|
}
|
|
2325
2610
|
|
|
2611
|
+
const PROVENANCE_SOURCE =
|
|
2612
|
+
/^(?:track:[A-Za-z0-9_-]{1,64}|orbit:[0-9]{1,2}|master)$/;
|
|
2613
|
+
|
|
2614
|
+
function normalizeProvenance(input: unknown, label: string): SampleProvenance {
|
|
2615
|
+
if (
|
|
2616
|
+
!isRecord(input) ||
|
|
2617
|
+
typeof input.source !== "string" ||
|
|
2618
|
+
!PROVENANCE_SOURCE.test(input.source) ||
|
|
2619
|
+
typeof input.score !== "string" ||
|
|
2620
|
+
!SHA256_HEX.test(input.score)
|
|
2621
|
+
)
|
|
2622
|
+
throw new ScoreValidationError(
|
|
2623
|
+
`${label} must be { source: track:<id>|orbit:<n>|master, score: <sha256> }`,
|
|
2624
|
+
"invalid-track",
|
|
2625
|
+
);
|
|
2626
|
+
const from: {
|
|
2627
|
+
source: string;
|
|
2628
|
+
section?: string;
|
|
2629
|
+
bars?: [number, number];
|
|
2630
|
+
score: string;
|
|
2631
|
+
} = {
|
|
2632
|
+
source: input.source,
|
|
2633
|
+
score: input.score,
|
|
2634
|
+
};
|
|
2635
|
+
if (input.section !== undefined) {
|
|
2636
|
+
if (
|
|
2637
|
+
typeof input.section !== "string" ||
|
|
2638
|
+
input.section.length === 0 ||
|
|
2639
|
+
input.section.length > 64
|
|
2640
|
+
)
|
|
2641
|
+
throw new ScoreValidationError(
|
|
2642
|
+
`${label} section must be a section name`,
|
|
2643
|
+
"invalid-track",
|
|
2644
|
+
);
|
|
2645
|
+
from.section = input.section;
|
|
2646
|
+
}
|
|
2647
|
+
if (input.bars !== undefined) {
|
|
2648
|
+
const bars = input.bars;
|
|
2649
|
+
if (
|
|
2650
|
+
!Array.isArray(bars) ||
|
|
2651
|
+
bars.length !== 2 ||
|
|
2652
|
+
!bars.every((b) => Number.isInteger(b) && b >= 1 && b <= 100_000) ||
|
|
2653
|
+
bars[0] > bars[1]
|
|
2654
|
+
)
|
|
2655
|
+
throw new ScoreValidationError(
|
|
2656
|
+
`${label} bars must be [a, b] with 1 ≤ a ≤ b`,
|
|
2657
|
+
"invalid-track",
|
|
2658
|
+
);
|
|
2659
|
+
from.bars = [bars[0], bars[1]];
|
|
2660
|
+
}
|
|
2661
|
+
// Key order is fixed so the printed JSON is stable.
|
|
2662
|
+
return Object.freeze({
|
|
2663
|
+
source: from.source,
|
|
2664
|
+
...(from.section !== undefined ? { section: from.section } : {}),
|
|
2665
|
+
...(from.bars !== undefined
|
|
2666
|
+
? { bars: Object.freeze(from.bars) as readonly [number, number] }
|
|
2667
|
+
: {}),
|
|
2668
|
+
score: from.score,
|
|
2669
|
+
});
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2326
2672
|
/** Prefix of a pack sound reference in `SampleRef.src`. */
|
|
2327
2673
|
export const PACK_PREFIX = "pack:";
|
|
2328
2674
|
|
package/core/sdk/print.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
MODAL_PARAMS,
|
|
28
28
|
type TrackModal,
|
|
29
29
|
} from "../resonators.ts";
|
|
30
|
+
import { WIND_INSTRUMENT, WIND_PARAMS, type TrackWind } from "../winds.ts";
|
|
30
31
|
import { rhythmVoicePitch, rowInSync } from "../rhythm.ts";
|
|
31
32
|
import {
|
|
32
33
|
BUILTIN_TABLE_PREFIX,
|
|
@@ -155,6 +156,7 @@ const RESERVED = new Set([
|
|
|
155
156
|
"rig",
|
|
156
157
|
"granular",
|
|
157
158
|
"modal",
|
|
159
|
+
"wind",
|
|
158
160
|
"slices",
|
|
159
161
|
"euclid",
|
|
160
162
|
"euclidRot",
|
|
@@ -414,6 +416,11 @@ export function printTrack(score: TrackScore, track: Track): string {
|
|
|
414
416
|
? printModal(track.modal, INDENT)
|
|
415
417
|
: undefined;
|
|
416
418
|
if (modalCall?.startsWith("modal(")) used.add("modal");
|
|
419
|
+
const windCall =
|
|
420
|
+
track.instrument === WIND_INSTRUMENT && track.wind
|
|
421
|
+
? printWind(track.wind, INDENT)
|
|
422
|
+
: undefined;
|
|
423
|
+
if (windCall?.startsWith("wind(")) used.add("wind");
|
|
417
424
|
|
|
418
425
|
const entries: string[] = [
|
|
419
426
|
`id: ${str(track.id)}`,
|
|
@@ -432,6 +439,8 @@ export function printTrack(score: TrackScore, track: Track): string {
|
|
|
432
439
|
entries.push(`instrument: ${printStringed(track.string!, INDENT)}`);
|
|
433
440
|
} else if (modalCall) {
|
|
434
441
|
entries.push(`instrument: ${modalCall}`);
|
|
442
|
+
} else if (windCall) {
|
|
443
|
+
entries.push(`instrument: ${windCall}`);
|
|
435
444
|
} else entries.push(`instrument: ${str(track.instrument)}`);
|
|
436
445
|
if (track.kit) entries.push(`kit: ${str(track.kit)}`);
|
|
437
446
|
if (track.granular && !grained)
|
|
@@ -658,6 +667,7 @@ export function printTrack(score: TrackScore, track: Track): string {
|
|
|
658
667
|
"rig",
|
|
659
668
|
"granular",
|
|
660
669
|
"modal",
|
|
670
|
+
"wind",
|
|
661
671
|
"euclid",
|
|
662
672
|
"grid",
|
|
663
673
|
]
|
|
@@ -808,13 +818,15 @@ function performanceEntries(score: TrackScore, track: Track): string[] {
|
|
|
808
818
|
1,
|
|
809
819
|
)}`,
|
|
810
820
|
);
|
|
811
|
-
|
|
812
|
-
const
|
|
821
|
+
for (const key of ["pedal", "softPedal", "sostenuto"] as const) {
|
|
822
|
+
const lane = track[key];
|
|
823
|
+
if (!lane) continue;
|
|
824
|
+
const events = lane.map(
|
|
813
825
|
(event) =>
|
|
814
826
|
`[${num(event.tick / score.ticksPerBeat)}, ${str(event.state)}]`,
|
|
815
827
|
);
|
|
816
828
|
entries.push(
|
|
817
|
-
|
|
829
|
+
`${key}: ${list(events, INDENT, `${key}: `.length, 1, events.length > 1)}`,
|
|
818
830
|
);
|
|
819
831
|
}
|
|
820
832
|
if (track.velocityCurve)
|
|
@@ -851,6 +863,20 @@ function performanceEntries(score: TrackScore, track: Track): string[] {
|
|
|
851
863
|
)}`,
|
|
852
864
|
);
|
|
853
865
|
}
|
|
866
|
+
if (track.guitar) {
|
|
867
|
+
const { tune, capo, hand, ring, position } = track.guitar;
|
|
868
|
+
const fields: [string, string][] = [];
|
|
869
|
+
if (tune !== undefined)
|
|
870
|
+
fields.push([
|
|
871
|
+
"tune",
|
|
872
|
+
typeof tune === "string" ? str(tune) : `[${tune.map(num).join(", ")}]`,
|
|
873
|
+
]);
|
|
874
|
+
if (capo !== undefined) fields.push(["capo", num(capo)]);
|
|
875
|
+
if (hand !== undefined) fields.push(["hand", num(hand)]);
|
|
876
|
+
if (ring !== undefined) fields.push(["ring", num(ring)]);
|
|
877
|
+
if (position !== undefined) fields.push(["position", num(position)]);
|
|
878
|
+
entries.push(`guitar: ${obj(fields, INDENT, "guitar: ".length, 1)}`);
|
|
879
|
+
}
|
|
854
880
|
return entries;
|
|
855
881
|
}
|
|
856
882
|
|
|
@@ -1000,6 +1026,7 @@ function printModal(settings: TrackModal, indent: string): string {
|
|
|
1000
1026
|
if (value === undefined) continue;
|
|
1001
1027
|
params.push([key, typeof value === "number" ? num(value) : str(value)]);
|
|
1002
1028
|
}
|
|
1029
|
+
if (settings.pair !== undefined) params.push(["pair", str(settings.pair)]);
|
|
1003
1030
|
const preset = settings.preset;
|
|
1004
1031
|
if (params.length === 0 && preset && preset !== "marimba") return str(preset);
|
|
1005
1032
|
const head = preset ? str(preset) : "";
|
|
@@ -1012,6 +1039,37 @@ function printModal(settings: TrackModal, indent: string): string {
|
|
|
1012
1039
|
return `modal(${lead}{\n${params.map(([k, v]) => `${inner}${k}: ${v},`).join("\n")}\n${indent}})`;
|
|
1013
1040
|
}
|
|
1014
1041
|
|
|
1042
|
+
/**
|
|
1043
|
+
* A wind track's instrument: the preset word alone (`"flute"`) when there
|
|
1044
|
+
* are no overrides, else `wind("sax", { breath: 0.8 })` with keys in
|
|
1045
|
+
* `WIND_PARAMS` order. Every wind preset word is also an instrument word.
|
|
1046
|
+
*/
|
|
1047
|
+
function printWind(settings: TrackWind, indent: string): string {
|
|
1048
|
+
const params: [string, string][] = [];
|
|
1049
|
+
for (const key of Object.keys(WIND_PARAMS)) {
|
|
1050
|
+
const value = (settings as Record<string, unknown>)[key];
|
|
1051
|
+
if (value === undefined) continue;
|
|
1052
|
+
params.push([
|
|
1053
|
+
key,
|
|
1054
|
+
typeof value === "number"
|
|
1055
|
+
? num(value)
|
|
1056
|
+
: typeof value === "boolean"
|
|
1057
|
+
? String(value)
|
|
1058
|
+
: str(String(value)),
|
|
1059
|
+
]);
|
|
1060
|
+
}
|
|
1061
|
+
const preset = settings.preset;
|
|
1062
|
+
if (params.length === 0 && preset) return str(preset);
|
|
1063
|
+
const head = preset ? str(preset) : "";
|
|
1064
|
+
if (params.length === 0) return `wind(${head})`;
|
|
1065
|
+
const lead = head ? `${head}, ` : "";
|
|
1066
|
+
const inline = `wind(${lead}{ ${params.map(([k, v]) => `${k}: ${v}`).join(", ")} })`;
|
|
1067
|
+
if (indent.length + "instrument: ".length + inline.length + 1 <= WIDTH)
|
|
1068
|
+
return inline;
|
|
1069
|
+
const inner = indent + INDENT;
|
|
1070
|
+
return `wind(${lead}{\n${params.map(([k, v]) => `${inner}${k}: ${v},`).join("\n")}\n${indent}})`;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1015
1073
|
/** `wavetable("basic", { wt: 0.5 })`; pack tables keep their pin. */
|
|
1016
1074
|
function printWavetable(settings: TrackWavetable, indent: string): string {
|
|
1017
1075
|
const src = settings.table.src;
|
|
@@ -1071,6 +1129,23 @@ function printSample(ref: SampleRef, indent: string, prefix: number): string {
|
|
|
1071
1129
|
if (ref.bpm !== undefined) entries.push(["bpm", num(ref.bpm)]);
|
|
1072
1130
|
if (ref.fitmode !== undefined) entries.push(["fitmode", str(ref.fitmode)]);
|
|
1073
1131
|
if (ref.len !== undefined) entries.push(["len", num(ref.len)]);
|
|
1132
|
+
if (ref.shift !== undefined) entries.push(["shift", num(ref.shift)]);
|
|
1133
|
+
if (ref.formant !== undefined) entries.push(["formant", num(ref.formant)]);
|
|
1134
|
+
if (ref.fadeInTime !== undefined)
|
|
1135
|
+
entries.push(["fadeInTime", num(ref.fadeInTime)]);
|
|
1136
|
+
if (ref.fadeTime !== undefined) entries.push(["fadeTime", num(ref.fadeTime)]);
|
|
1137
|
+
if (ref.from !== undefined) {
|
|
1138
|
+
const from = ref.from;
|
|
1139
|
+
const parts: [string, string][] = [["source", str(from.source)]];
|
|
1140
|
+
if (from.section !== undefined) parts.push(["section", str(from.section)]);
|
|
1141
|
+
if (from.bars !== undefined)
|
|
1142
|
+
parts.push(["bars", `[${num(from.bars[0])}, ${num(from.bars[1])}]`]);
|
|
1143
|
+
parts.push(["score", str(from.score)]);
|
|
1144
|
+
entries.push(["from", obj(parts, indent + INDENT, "from: ".length, 1)]);
|
|
1145
|
+
}
|
|
1146
|
+
if (ref.vel !== undefined)
|
|
1147
|
+
entries.push(["vel", `[${num(ref.vel[0])}, ${num(ref.vel[1])}]`]);
|
|
1148
|
+
if (ref.rr !== undefined) entries.push(["rr", str(ref.rr)]);
|
|
1074
1149
|
if (entries.length === 1) return str(ref.src);
|
|
1075
1150
|
return obj(entries, indent, prefix, 1);
|
|
1076
1151
|
}
|