@hraness/dawg 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -0
- package/DAWG.md +592 -49
- package/README.md +4 -4
- package/core/chords.ts +492 -4
- package/core/diff.ts +9 -0
- package/core/expression.ts +143 -6
- package/core/fx.ts +689 -2
- package/core/granular.ts +619 -0
- package/core/instruments.ts +442 -0
- package/core/keys.ts +955 -0
- package/core/resonators.ts +862 -0
- package/core/score.ts +529 -8
- package/core/sdk/eval-child.ts +2 -0
- package/core/sdk/print.ts +247 -16
- package/core/sdk/sync-instruments.ts +58 -0
- package/core/sdk/v1.ts +2417 -40
- package/core/sections.ts +44 -10
- package/core/strings.ts +1080 -0
- package/core/winds.ts +652 -0
- package/guides/automation.md +1 -0
- package/guides/chords.md +3 -1
- package/guides/effects.md +5 -2
- package/guides/media.md +1 -1
- package/guides/performance.md +3 -0
- package/guides/resample.md +26 -0
- package/guides/sounds.md +7 -2
- package/guides/tempo.md +1 -0
- package/package.json +1 -1
- package/src/agent/agent.ts +13 -0
- package/src/agent/chord-tools.ts +153 -1
- package/src/agent/expression-tools.ts +91 -0
- package/src/agent/granular-tools.ts +138 -0
- package/src/agent/models.ts +4 -4
- package/src/agent/ops.ts +42 -2
- package/src/agent/preview-tool.ts +7 -1
- package/src/agent/resample-tool.ts +131 -0
- package/src/agent/tools.ts +726 -17
- package/src/agent/xcb-agent.ts +13 -0
- package/src/audio/arrange.ts +42 -3
- package/src/audio/dsp/bank.ts +233 -0
- package/src/audio/dsp/envelope.ts +161 -0
- package/src/audio/dsp/fft.ts +6 -0
- package/src/audio/dsp/filters.ts +57 -0
- package/src/audio/dsp/interp.ts +112 -0
- package/src/audio/dsp/modal.ts +684 -0
- package/src/audio/dsp/onset.ts +197 -0
- package/src/audio/dsp/oversample.ts +202 -0
- package/src/audio/dsp/rng.ts +37 -0
- package/src/audio/dsp/shape.ts +81 -0
- package/src/audio/dsp/shift.ts +256 -0
- package/src/audio/dsp/stft.ts +73 -0
- package/src/audio/dsp/window.ts +77 -0
- package/src/audio/effects/chain.ts +23 -3
- package/src/audio/effects/common.ts +14 -0
- package/src/audio/effects/convolution.ts +106 -1
- package/src/audio/effects/gaze.ts +354 -0
- package/src/audio/effects/rig/cab.ts +99 -0
- package/src/audio/effects/rig/filters.ts +152 -0
- package/src/audio/effects/rig/gate.ts +39 -0
- package/src/audio/effects/rig/head.ts +487 -0
- package/src/audio/effects/rig/index.ts +170 -0
- package/src/audio/effects/rig/section.ts +78 -0
- package/src/audio/effects/rig/stomp.ts +238 -0
- package/src/audio/engine.ts +56 -6
- package/src/audio/fit.ts +447 -0
- package/src/audio/granular.ts +995 -0
- package/src/audio/instrument-check.ts +137 -0
- package/src/audio/instruments.ts +140 -0
- package/src/audio/keys/dsp.ts +323 -0
- package/src/audio/keys/electric.ts +420 -0
- package/src/audio/keys/engine.ts +485 -0
- package/src/audio/keys/organ.ts +1335 -0
- package/src/audio/keys/piano.ts +476 -0
- package/src/audio/keys/sympathetic.ts +127 -0
- package/src/audio/live-worker.ts +56 -0
- package/src/audio/live.ts +361 -15
- package/src/audio/loudness.ts +65 -20
- package/src/audio/preview.ts +17 -1
- package/src/audio/resample.ts +285 -0
- package/src/audio/resonators.ts +295 -0
- package/src/audio/sampler.ts +328 -35
- package/src/audio/samples.ts +56 -8
- package/src/audio/strings/body.ts +263 -0
- package/src/audio/strings/bow.ts +656 -0
- package/src/audio/strings/engine.ts +615 -0
- package/src/audio/strings/loop.ts +119 -0
- package/src/audio/strings/measure.test-helpers.ts +200 -0
- package/src/audio/strings/pluck.ts +354 -0
- package/src/audio/warp.ts +61 -0
- package/src/audio/wav.ts +191 -19
- package/src/audio/winds/engine.ts +302 -0
- package/src/audio/winds/filters.ts +153 -0
- package/src/audio/winds/pitch.ts +104 -0
- package/src/audio/winds/trim.ts +80 -0
- package/src/audio/winds/trims.ts +917 -0
- package/src/audio/winds/voice.ts +479 -0
- package/src/commands/expression.ts +111 -23
- package/src/commands/fit.ts +135 -0
- package/src/commands/fx.ts +35 -1
- package/src/commands/granular.ts +432 -0
- package/src/commands/help.ts +159 -5
- package/src/commands/keys.ts +629 -0
- package/src/commands/modal.ts +310 -0
- package/src/commands/resample.ts +281 -0
- package/src/commands/rig.ts +264 -0
- package/src/commands/sample.ts +52 -2
- package/src/commands/shift.ts +119 -0
- package/src/commands/string.ts +201 -0
- package/src/commands/strum.ts +473 -0
- package/src/commands/time.ts +4 -1
- package/src/commands/wind.ts +244 -0
- package/src/main.ts +339 -15
- package/src/project/check.ts +17 -0
- package/src/render.ts +49 -4
- package/src/session/presence.ts +16 -3
- package/src/tui/audition.ts +2 -2
- package/src/tui/granular-menu.ts +278 -0
- package/src/tui/menu.ts +934 -9
- package/src/tui/modal-menu.ts +145 -0
- package/src/tui/performance-menu.ts +42 -0
- package/src/tui/play-chords.ts +104 -3
- package/src/tui/play-mode.ts +1 -0
- package/src/tui/play-session.ts +70 -2
- package/src/tui/wind-menu.ts +144 -0
package/core/score.ts
CHANGED
|
@@ -20,7 +20,23 @@ import {
|
|
|
20
20
|
type FxLane,
|
|
21
21
|
type TrackFx,
|
|
22
22
|
} from "./fx.ts";
|
|
23
|
+
import {
|
|
24
|
+
MODAL_INSTRUMENT,
|
|
25
|
+
normalizeModal,
|
|
26
|
+
normalizeWind,
|
|
27
|
+
WIND_INSTRUMENT,
|
|
28
|
+
type TrackModal,
|
|
29
|
+
type TrackWind,
|
|
30
|
+
} from "./resonators.ts";
|
|
23
31
|
import { normalizeSynth, type TrackSynth } from "./synth.ts";
|
|
32
|
+
import { normalizeString, type TrackString } from "./strings.ts";
|
|
33
|
+
import {
|
|
34
|
+
isGranularInstrument,
|
|
35
|
+
normalizeGranular,
|
|
36
|
+
type TrackGranular,
|
|
37
|
+
} from "./granular.ts";
|
|
38
|
+
import { isKeysFamily, normalizeKeys, type TrackKeys } from "./keys.ts";
|
|
39
|
+
import { GUITAR_TUNING_NAMES } from "./chords.ts";
|
|
24
40
|
import {
|
|
25
41
|
checkSongTime,
|
|
26
42
|
normalizeSongTime,
|
|
@@ -33,8 +49,11 @@ import {
|
|
|
33
49
|
import {
|
|
34
50
|
ExpressionValidationError,
|
|
35
51
|
normalizeNoteExpression,
|
|
52
|
+
normalizePedal,
|
|
36
53
|
normalizeTrackPerformance,
|
|
54
|
+
SOSTENUTO_STATES,
|
|
37
55
|
type NoteExpression,
|
|
56
|
+
type PedalEvent,
|
|
38
57
|
type NoteExpressionPatch,
|
|
39
58
|
type TrackPerformance,
|
|
40
59
|
} from "./expression.ts";
|
|
@@ -99,6 +118,17 @@ export const SCORE_LIMITS = Object.freeze({
|
|
|
99
118
|
maxSampleAccelerate: 8,
|
|
100
119
|
/** Sampler `squiz` ratio bound. */
|
|
101
120
|
maxSampleSquiz: 32,
|
|
121
|
+
/** Sampler `bpm` (source tempo) bounds. */
|
|
122
|
+
minSampleBpm: 20,
|
|
123
|
+
maxSampleBpm: 400,
|
|
124
|
+
/** Sampler `len` (window length in beats) bound. */
|
|
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,
|
|
102
132
|
/** Per sample file, enforced by the decoder and the import tool. */
|
|
103
133
|
maxSampleFileBytes: 50 * 1024 * 1024,
|
|
104
134
|
maxSampleSeconds: 600,
|
|
@@ -373,6 +403,54 @@ export type Track = Readonly<{
|
|
|
373
403
|
* `ref`/`root` over the song tuning. Absent follows the song tuning.
|
|
374
404
|
*/
|
|
375
405
|
tuning?: Tuning;
|
|
406
|
+
/**
|
|
407
|
+
* String engine settings (`core/strings.ts`): a preset plus overrides.
|
|
408
|
+
* Played only when `instrument` is "string"; absent keeps today's voice.
|
|
409
|
+
*/
|
|
410
|
+
string?: TrackString;
|
|
411
|
+
/**
|
|
412
|
+
* Granular instrument settings (0.6, `core/granular.ts`). Played when
|
|
413
|
+
* `instrument` is `"granular"`; kept when the instrument changes.
|
|
414
|
+
*/
|
|
415
|
+
granular?: TrackGranular;
|
|
416
|
+
/**
|
|
417
|
+
* Optional (0.6): modelled piano settings (`core/keys.ts`), `{ preset?,
|
|
418
|
+
* ...overrides }`. The modelled piano plays only when `instrument` is a
|
|
419
|
+
* piano family (grand upright felt honkytonk prepared) and this is
|
|
420
|
+
* present; `{}` is the family's defaults.
|
|
421
|
+
*/
|
|
422
|
+
keys?: TrackKeys;
|
|
423
|
+
/**
|
|
424
|
+
* Optional (0.6): modal percussion (`core/resonators.ts`), a preset plus
|
|
425
|
+
* overrides. Plays only when `instrument` is `"modal"`; any other
|
|
426
|
+
* instrument with this field is rejected. Absent keeps today's tone.
|
|
427
|
+
*/
|
|
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;
|
|
376
454
|
}> &
|
|
377
455
|
/**
|
|
378
456
|
* Performance (`core/expression.ts`): glide default, sustain pedal
|
|
@@ -449,8 +527,80 @@ export type SampleRef = Readonly<{
|
|
|
449
527
|
* (1 is off, up to 32).
|
|
450
528
|
*/
|
|
451
529
|
squiz?: number;
|
|
530
|
+
/**
|
|
531
|
+
* Optional: the sample's own tempo (20..400). The window then follows
|
|
532
|
+
* the song's tempo map (ramps included): a 174 BPM break in a 128 BPM
|
|
533
|
+
* song plays 128/174 as fast. `fitmode` picks how.
|
|
534
|
+
*/
|
|
535
|
+
bpm?: number;
|
|
536
|
+
/**
|
|
537
|
+
* Optional: how a fitted sample changes its time. `repitch` (default)
|
|
538
|
+
* changes speed and pitch together like tape; `beats` cuts the window at
|
|
539
|
+
* its onsets and places each slice on its new time, unstretched (drums,
|
|
540
|
+
* speech); `tones` time-stretches with a phase vocoder and keeps the
|
|
541
|
+
* pitch (pads, loops, vocals). Needs `bpm`, `len` or `fit`.
|
|
542
|
+
*/
|
|
543
|
+
fitmode?: SampleFitMode;
|
|
544
|
+
/**
|
|
545
|
+
* Optional: the window's length in beats (0 < len ≤ 1024), followed
|
|
546
|
+
* through the song's tempo map. `fit` wins over `bpm`, `bpm` over `len`.
|
|
547
|
+
*/
|
|
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;
|
|
452
594
|
}>;
|
|
453
595
|
|
|
596
|
+
/** Sampler `fitmode`: how a fitted window changes its time. */
|
|
597
|
+
export type SampleFitMode = "repitch" | "beats" | "tones";
|
|
598
|
+
export const SAMPLE_FIT_MODES: readonly SampleFitMode[] = Object.freeze([
|
|
599
|
+
"repitch",
|
|
600
|
+
"beats",
|
|
601
|
+
"tones",
|
|
602
|
+
]);
|
|
603
|
+
|
|
454
604
|
/** Sample `unit`: rate, cycles (bars) or seconds. */
|
|
455
605
|
export type SampleUnit = "r" | "c" | "s";
|
|
456
606
|
export const SAMPLE_UNITS: readonly SampleUnit[] = Object.freeze([
|
|
@@ -474,14 +624,22 @@ export type TrackReverb = Readonly<{
|
|
|
474
624
|
predelay?: number;
|
|
475
625
|
/**
|
|
476
626
|
* Optional (Strudel `iresponse`/`ir`): convolve with this impulse instead
|
|
477
|
-
* 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
|
|
478
628
|
* or a project file; `size`, `fade` and `dim` then do nothing.
|
|
479
629
|
*/
|
|
480
630
|
ir?: SampleRef;
|
|
481
631
|
}>;
|
|
482
632
|
|
|
483
633
|
/** Generated impulse responses `reverb.ir` can name as `builtin:<name>`. */
|
|
484
|
-
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
|
+
]);
|
|
485
643
|
|
|
486
644
|
/**
|
|
487
645
|
* Validates `reverb.ir`: a string or `{ src, … }`. Bare built-in names and
|
|
@@ -683,6 +841,14 @@ export type TrackPatch = Readonly<
|
|
|
683
841
|
velocityCurve?: Track["velocityCurve"] | null;
|
|
684
842
|
humanize?: Track["humanize"] | null;
|
|
685
843
|
tuning?: Tuning | null;
|
|
844
|
+
string?: TrackString | null;
|
|
845
|
+
granular?: TrackGranular | null;
|
|
846
|
+
keys?: TrackKeys | null;
|
|
847
|
+
modal?: TrackModal | null;
|
|
848
|
+
softPedal?: Track["softPedal"] | null;
|
|
849
|
+
sostenuto?: Track["sostenuto"] | null;
|
|
850
|
+
guitar?: TrackGuitar | null;
|
|
851
|
+
wind?: TrackWind | null;
|
|
686
852
|
}
|
|
687
853
|
>;
|
|
688
854
|
|
|
@@ -727,6 +893,14 @@ export type TrackInput = Readonly<
|
|
|
727
893
|
| "velocityCurve"
|
|
728
894
|
| "humanize"
|
|
729
895
|
| "tuning"
|
|
896
|
+
| "string"
|
|
897
|
+
| "granular"
|
|
898
|
+
| "keys"
|
|
899
|
+
| "modal"
|
|
900
|
+
| "softPedal"
|
|
901
|
+
| "sostenuto"
|
|
902
|
+
| "guitar"
|
|
903
|
+
| "wind"
|
|
730
904
|
> &
|
|
731
905
|
Pick<Track, "id"> & {
|
|
732
906
|
filter?: TrackFilter | null;
|
|
@@ -745,6 +919,14 @@ export type TrackInput = Readonly<
|
|
|
745
919
|
velocityCurve?: Track["velocityCurve"] | string | null;
|
|
746
920
|
humanize?: Track["humanize"] | null;
|
|
747
921
|
tuning?: Tuning | null;
|
|
922
|
+
string?: TrackString | null;
|
|
923
|
+
granular?: TrackGranular | null;
|
|
924
|
+
keys?: TrackKeys | null;
|
|
925
|
+
modal?: TrackModal | null;
|
|
926
|
+
softPedal?: Track["softPedal"] | null;
|
|
927
|
+
sostenuto?: Track["sostenuto"] | null;
|
|
928
|
+
guitar?: TrackGuitar | null;
|
|
929
|
+
wind?: TrackWind | null;
|
|
748
930
|
}
|
|
749
931
|
>;
|
|
750
932
|
|
|
@@ -1134,9 +1316,31 @@ export function updateTrack(
|
|
|
1134
1316
|
): TrackScore {
|
|
1135
1317
|
if (!score.tracks.some((track) => track.id === trackId)) return score;
|
|
1136
1318
|
return score.withTracks(
|
|
1137
|
-
score.tracks.map((track) =>
|
|
1138
|
-
track.id
|
|
1139
|
-
|
|
1319
|
+
score.tracks.map((track) => {
|
|
1320
|
+
if (track.id !== trackId) return track;
|
|
1321
|
+
const next: Record<string, unknown> = { ...track, ...patch };
|
|
1322
|
+
// A modal field only belongs to a modal track: switching the
|
|
1323
|
+
// instrument away drops it unless the patch sets it too.
|
|
1324
|
+
if (
|
|
1325
|
+
patch.instrument !== undefined &&
|
|
1326
|
+
patch.modal === undefined &&
|
|
1327
|
+
patch.instrument !== MODAL_INSTRUMENT
|
|
1328
|
+
)
|
|
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;
|
|
1342
|
+
return next as Track;
|
|
1343
|
+
}),
|
|
1140
1344
|
);
|
|
1141
1345
|
}
|
|
1142
1346
|
|
|
@@ -1717,7 +1921,21 @@ function normalizeTrack(input: unknown): Track {
|
|
|
1717
1921
|
const fx = fxOrThrow(() => normalizeFx(input.fx));
|
|
1718
1922
|
const fxAutomation = normalizeFxAutomation(input.fxAutomation);
|
|
1719
1923
|
const synth = fxOrThrow(() => normalizeSynth(input.synth));
|
|
1924
|
+
const keys = fxOrThrow(() => normalizeKeys(input.keys));
|
|
1720
1925
|
const wavetable = normalizeWavetable(input.wavetable);
|
|
1926
|
+
const modal = fxOrThrow(() => normalizeModal(input.modal));
|
|
1927
|
+
if (modal && instrument !== MODAL_INSTRUMENT)
|
|
1928
|
+
throw new ScoreValidationError(
|
|
1929
|
+
`track ${id} has modal settings but its instrument is "${instrument}"`,
|
|
1930
|
+
"invalid-track",
|
|
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
|
+
);
|
|
1721
1939
|
const sampler = normalizeSampler(input.sampler);
|
|
1722
1940
|
const rhythm = normalizeRhythm(input.rhythm, id);
|
|
1723
1941
|
const time = timeOrThrow(
|
|
@@ -1739,6 +1957,37 @@ function normalizeTrack(input: unknown): Track {
|
|
|
1739
1957
|
() => normalizeTuning(input.tuning, `track ${id} tuning`),
|
|
1740
1958
|
"invalid-track",
|
|
1741
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
|
+
}
|
|
1982
|
+
const string = fxOrThrow(() => normalizeString(input.string));
|
|
1983
|
+
// A bare `granular` instrument (set_instrument, `instrument granular`)
|
|
1984
|
+
// gets an empty object, so the engine plays its defaults.
|
|
1985
|
+
const granular =
|
|
1986
|
+
fxOrThrow(() =>
|
|
1987
|
+
normalizeGranular(input.granular, (ref) =>
|
|
1988
|
+
normalizeSampleRef(ref, "granular src"),
|
|
1989
|
+
),
|
|
1990
|
+
) ?? (isGranularInstrument(instrument) ? Object.freeze({}) : undefined);
|
|
1742
1991
|
let kit: string | undefined;
|
|
1743
1992
|
if (input.kit !== undefined && input.kit !== null) {
|
|
1744
1993
|
const found =
|
|
@@ -1760,7 +2009,12 @@ function normalizeTrack(input: unknown): Track {
|
|
|
1760
2009
|
`track ${id} instrument "sampler" needs a sampler`,
|
|
1761
2010
|
"invalid-track",
|
|
1762
2011
|
);
|
|
1763
|
-
|
|
2012
|
+
// A granular track keeps the sampler it grains (`grain off` goes back).
|
|
2013
|
+
if (
|
|
2014
|
+
sampler &&
|
|
2015
|
+
!isSamplerInstrument(instrument) &&
|
|
2016
|
+
!isGranularInstrument(instrument)
|
|
2017
|
+
)
|
|
1764
2018
|
throw new ScoreValidationError(
|
|
1765
2019
|
`track ${id} has a sampler but its instrument is "${instrument}"`,
|
|
1766
2020
|
"invalid-track",
|
|
@@ -1793,9 +2047,93 @@ function normalizeTrack(input: unknown): Track {
|
|
|
1793
2047
|
...(time ? { time } : {}),
|
|
1794
2048
|
...performance,
|
|
1795
2049
|
...(tuning ? { tuning } : {}),
|
|
2050
|
+
...(string ? { string } : {}),
|
|
2051
|
+
...(granular ? { granular } : {}),
|
|
2052
|
+
...(keys ? { keys } : {}),
|
|
2053
|
+
...(modal ? { modal } : {}),
|
|
2054
|
+
...(softPedal ? { softPedal } : {}),
|
|
2055
|
+
...(sostenuto ? { sostenuto } : {}),
|
|
2056
|
+
...(guitar ? { guitar } : {}),
|
|
2057
|
+
...(wind ? { wind } : {}),
|
|
1796
2058
|
});
|
|
1797
2059
|
}
|
|
1798
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
|
+
|
|
1799
2137
|
export function normalizeRhythm(
|
|
1800
2138
|
input: unknown,
|
|
1801
2139
|
trackId: string,
|
|
@@ -1918,6 +2256,8 @@ function trackEffectExtras(
|
|
|
1918
2256
|
}
|
|
1919
2257
|
|
|
1920
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}$/;
|
|
1921
2261
|
const SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
1922
2262
|
|
|
1923
2263
|
/** Validates a `Sampler` from unknown; `undefined`/`null` means none. */
|
|
@@ -2000,6 +2340,16 @@ export function normalizeSampleRef(input: unknown, name: string): SampleRef {
|
|
|
2000
2340
|
fit?: boolean;
|
|
2001
2341
|
accelerate?: number;
|
|
2002
2342
|
squiz?: number;
|
|
2343
|
+
bpm?: number;
|
|
2344
|
+
fitmode?: SampleFitMode;
|
|
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;
|
|
2003
2353
|
} = { src };
|
|
2004
2354
|
if (input.sha256 !== undefined) {
|
|
2005
2355
|
if (typeof input.sha256 !== "string" || !SHA256_HEX.test(input.sha256))
|
|
@@ -2163,9 +2513,162 @@ export function normalizeSampleRef(input: unknown, name: string): SampleRef {
|
|
|
2163
2513
|
1,
|
|
2164
2514
|
SCORE_LIMITS.maxSampleSquiz,
|
|
2165
2515
|
);
|
|
2516
|
+
if (input.bpm !== undefined)
|
|
2517
|
+
ref.bpm = boundedNumber(
|
|
2518
|
+
input.bpm,
|
|
2519
|
+
`${label} bpm`,
|
|
2520
|
+
SCORE_LIMITS.minSampleBpm,
|
|
2521
|
+
SCORE_LIMITS.maxSampleBpm,
|
|
2522
|
+
);
|
|
2523
|
+
if (input.fitmode !== undefined) {
|
|
2524
|
+
if (!SAMPLE_FIT_MODES.includes(input.fitmode as SampleFitMode))
|
|
2525
|
+
throw new ScoreValidationError(
|
|
2526
|
+
`${label} fitmode must be "repitch", "beats" or "tones"`,
|
|
2527
|
+
"invalid-track",
|
|
2528
|
+
);
|
|
2529
|
+
ref.fitmode = input.fitmode as SampleFitMode;
|
|
2530
|
+
}
|
|
2531
|
+
if (input.len !== undefined) {
|
|
2532
|
+
const len = boundedNumber(
|
|
2533
|
+
input.len,
|
|
2534
|
+
`${label} len`,
|
|
2535
|
+
0,
|
|
2536
|
+
SCORE_LIMITS.maxSampleLenBeats,
|
|
2537
|
+
);
|
|
2538
|
+
if (len === 0)
|
|
2539
|
+
throw new ScoreValidationError(
|
|
2540
|
+
`${label} len must be greater than 0`,
|
|
2541
|
+
"invalid-track",
|
|
2542
|
+
);
|
|
2543
|
+
ref.len = len;
|
|
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
|
+
}
|
|
2567
|
+
if (
|
|
2568
|
+
ref.fitmode !== undefined &&
|
|
2569
|
+
ref.fitmode !== "repitch" &&
|
|
2570
|
+
ref.bpm === undefined &&
|
|
2571
|
+
ref.len === undefined &&
|
|
2572
|
+
ref.fit !== true
|
|
2573
|
+
)
|
|
2574
|
+
throw new ScoreValidationError(
|
|
2575
|
+
`${label} fitmode ${ref.fitmode} needs bpm, len or fit`,
|
|
2576
|
+
"invalid-track",
|
|
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`);
|
|
2166
2608
|
return Object.freeze(ref);
|
|
2167
2609
|
}
|
|
2168
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
|
+
|
|
2169
2672
|
/** Prefix of a pack sound reference in `SampleRef.src`. */
|
|
2170
2673
|
export const PACK_PREFIX = "pack:";
|
|
2171
2674
|
|
|
@@ -2403,10 +2906,28 @@ function normalizeAutomation(
|
|
|
2403
2906
|
return freezeArray(points);
|
|
2404
2907
|
}
|
|
2405
2908
|
|
|
2909
|
+
/** The note cap `normalizeNotes` enforces; see `withNoteCap`. */
|
|
2910
|
+
let noteCap: number = SCORE_LIMITS.maxNotes;
|
|
2911
|
+
|
|
2912
|
+
/**
|
|
2913
|
+
* Runs `fn` with the note cap raised to `cap`. Stored scores keep
|
|
2914
|
+
* SCORE_LIMITS.maxNotes; the arranged renderer bakes timed tracks (cycle,
|
|
2915
|
+
* rate) into plain notes and may hold more than a stored score allows.
|
|
2916
|
+
*/
|
|
2917
|
+
export function withNoteCap<T>(cap: number, fn: () => T): T {
|
|
2918
|
+
const previous = noteCap;
|
|
2919
|
+
noteCap = Math.max(previous, cap);
|
|
2920
|
+
try {
|
|
2921
|
+
return fn();
|
|
2922
|
+
} finally {
|
|
2923
|
+
noteCap = previous;
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
|
|
2406
2927
|
function normalizeNotes(inputs: readonly unknown[]): Note[] {
|
|
2407
|
-
if (!Array.isArray(inputs) || inputs.length >
|
|
2928
|
+
if (!Array.isArray(inputs) || inputs.length > noteCap) {
|
|
2408
2929
|
throw new ScoreValidationError(
|
|
2409
|
-
`score cannot contain more than ${
|
|
2930
|
+
`score cannot contain more than ${noteCap} notes`,
|
|
2410
2931
|
"score-limit",
|
|
2411
2932
|
);
|
|
2412
2933
|
}
|
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 (
|