@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/expression.ts
CHANGED
|
@@ -167,6 +167,8 @@ export type TrackGlide = Readonly<{ time: number; mode: GlideMode }>;
|
|
|
167
167
|
export const DEFAULT_GLIDE_SECONDS = 0.06;
|
|
168
168
|
|
|
169
169
|
export const PEDAL_STATES = ["down", "half", "up"] as const;
|
|
170
|
+
/** Sostenuto has no half position (0.6.1). */
|
|
171
|
+
export const SOSTENUTO_STATES = ["down", "up"] as const;
|
|
170
172
|
export type PedalState = (typeof PEDAL_STATES)[number];
|
|
171
173
|
|
|
172
174
|
/** A sustain pedal (MIDI CC64) change at a tick. */
|
|
@@ -386,16 +388,18 @@ export function normalizeTrackGlide(value: unknown): TrackGlide | undefined {
|
|
|
386
388
|
export function normalizePedal(
|
|
387
389
|
value: unknown,
|
|
388
390
|
maxTick: number,
|
|
391
|
+
label = "pedal",
|
|
392
|
+
states: readonly PedalState[] = PEDAL_STATES,
|
|
389
393
|
): readonly PedalEvent[] | undefined {
|
|
390
394
|
if (value === undefined || value === null) return undefined;
|
|
391
395
|
if (!Array.isArray(value) || value.length > EXPRESSION_LIMITS.maxPedalEvents)
|
|
392
396
|
throw new ExpressionValidationError(
|
|
393
|
-
|
|
397
|
+
`${label} must be an array of at most ${EXPRESSION_LIMITS.maxPedalEvents} events`,
|
|
394
398
|
);
|
|
395
399
|
const byTick = new Map<number, PedalEvent>();
|
|
396
400
|
for (const item of value as unknown[]) {
|
|
397
|
-
const event = record(item,
|
|
398
|
-
onlyKeys(event, ["tick", "state"],
|
|
401
|
+
const event = record(item, `${label} event`);
|
|
402
|
+
onlyKeys(event, ["tick", "state"], `${label} event`);
|
|
399
403
|
const tick = event.tick;
|
|
400
404
|
if (
|
|
401
405
|
typeof tick !== "number" ||
|
|
@@ -404,11 +408,11 @@ export function normalizePedal(
|
|
|
404
408
|
tick > maxTick
|
|
405
409
|
)
|
|
406
410
|
throw new ExpressionValidationError(
|
|
407
|
-
|
|
411
|
+
`${label} tick must be an integer 0..${maxTick}`,
|
|
408
412
|
);
|
|
409
|
-
if (!
|
|
413
|
+
if (!states.includes(event.state as PedalState))
|
|
410
414
|
throw new ExpressionValidationError(
|
|
411
|
-
|
|
415
|
+
`${label} state must be one of ${states.join(", ")}`,
|
|
412
416
|
);
|
|
413
417
|
// The last event at a tick wins.
|
|
414
418
|
byTick.set(tick, Object.freeze({ tick, state: event.state as PedalState }));
|
|
@@ -551,6 +555,80 @@ export function pedalStateAt(
|
|
|
551
555
|
return state;
|
|
552
556
|
}
|
|
553
557
|
|
|
558
|
+
/** A stretch of song ticks `[from, to)` placed at `offset` in a slice. */
|
|
559
|
+
export type PedalPiece = Readonly<{ from: number; to: number; offset: number }>;
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Pedal events for a slice made of `pieces` (a render window, a section
|
|
563
|
+
* pass, an audition region): the state in force at each piece's start is
|
|
564
|
+
* restated at its offset (`carry`, for sustain and una corda; sostenuto
|
|
565
|
+
* passes false, since a pedal already down latches no key struck later),
|
|
566
|
+
* events inside the piece move by `offset - from`, and a pedal still down
|
|
567
|
+
* at the piece's end is lifted there. Undefined when nothing is left.
|
|
568
|
+
*/
|
|
569
|
+
export function slicePedal(
|
|
570
|
+
events: readonly PedalEvent[] | undefined,
|
|
571
|
+
pieces: readonly PedalPiece[],
|
|
572
|
+
carry = true,
|
|
573
|
+
): readonly PedalEvent[] | undefined {
|
|
574
|
+
if (!events || events.length === 0) return events;
|
|
575
|
+
const byTick = new Map<number, PedalEvent>();
|
|
576
|
+
const put = (tick: number, state: PedalState) =>
|
|
577
|
+
byTick.set(tick, Object.freeze({ tick, state }));
|
|
578
|
+
for (const { from, to, offset } of pieces) {
|
|
579
|
+
let state: PedalState = "up";
|
|
580
|
+
if (carry)
|
|
581
|
+
for (const event of events) {
|
|
582
|
+
if (event.tick >= from) break;
|
|
583
|
+
state = event.state;
|
|
584
|
+
}
|
|
585
|
+
// Restate up too: the previous piece may have ended with it down.
|
|
586
|
+
if (state !== "up" || byTick.size > 0) put(offset, state);
|
|
587
|
+
for (const event of events) {
|
|
588
|
+
if (event.tick < from || event.tick >= to) continue;
|
|
589
|
+
put(event.tick - from + offset, event.state);
|
|
590
|
+
state = event.state;
|
|
591
|
+
}
|
|
592
|
+
if (state !== "up" && Number.isFinite(to)) put(offset + to - from, "up");
|
|
593
|
+
}
|
|
594
|
+
const out = [...byTick.values()].sort((a, b) => a.tick - b.tick);
|
|
595
|
+
// Drop events that restate the state already in force.
|
|
596
|
+
const kept: PedalEvent[] = [];
|
|
597
|
+
let last: PedalState = "up";
|
|
598
|
+
for (const event of out)
|
|
599
|
+
if (event.state !== last) {
|
|
600
|
+
kept.push(event);
|
|
601
|
+
last = event.state;
|
|
602
|
+
}
|
|
603
|
+
return kept.length > 0 ? Object.freeze(kept) : undefined;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/** The track with its three pedal lanes cut to `pieces` (see slicePedal). */
|
|
607
|
+
export function slicePedals<T extends PedalTrack>(
|
|
608
|
+
track: T,
|
|
609
|
+
pieces: readonly PedalPiece[],
|
|
610
|
+
): T {
|
|
611
|
+
if (!track.pedal && !track.softPedal && !track.sostenuto) return track;
|
|
612
|
+
const out: Record<string, unknown> = { ...track };
|
|
613
|
+
for (const [field, carry] of [
|
|
614
|
+
["pedal", true],
|
|
615
|
+
["softPedal", true],
|
|
616
|
+
["sostenuto", false],
|
|
617
|
+
] as const) {
|
|
618
|
+
if (!track[field]) continue;
|
|
619
|
+
const next = slicePedal(track[field], pieces, carry);
|
|
620
|
+
if (next) out[field] = next;
|
|
621
|
+
else delete out[field];
|
|
622
|
+
}
|
|
623
|
+
return out as T;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
type PedalTrack = Readonly<{
|
|
627
|
+
pedal?: readonly PedalEvent[];
|
|
628
|
+
softPedal?: readonly PedalEvent[];
|
|
629
|
+
sostenuto?: readonly PedalEvent[];
|
|
630
|
+
}>;
|
|
631
|
+
|
|
554
632
|
// ---------------------------------------------------------------------------
|
|
555
633
|
// Performance
|
|
556
634
|
|
|
@@ -674,6 +752,7 @@ export function hasTrackPerformance(track: Track | undefined): boolean {
|
|
|
674
752
|
track !== undefined &&
|
|
675
753
|
(track.glide !== undefined ||
|
|
676
754
|
track.pedal !== undefined ||
|
|
755
|
+
track.sostenuto !== undefined ||
|
|
677
756
|
track.velocityCurve !== undefined ||
|
|
678
757
|
track.humanize !== undefined)
|
|
679
758
|
);
|
|
@@ -869,6 +948,64 @@ export function performNotes(
|
|
|
869
948
|
};
|
|
870
949
|
});
|
|
871
950
|
}
|
|
951
|
+
// 3b. Sostenuto (0.6.1): a key held (written length after articulation)
|
|
952
|
+
// when the sostenuto goes down keeps its damper up until the sostenuto
|
|
953
|
+
// lifts; keys struck later damp at their own release (or the sustain
|
|
954
|
+
// pedal's). The rod keeps a caught damper raised, so the same pitch
|
|
955
|
+
// struck again while that sostenuto is down rings to the lift too; the
|
|
956
|
+
// earlier instance stops at the restrike so voices do not pile up. A key
|
|
957
|
+
// still held through a lift and a new press is caught again.
|
|
958
|
+
const sostenuto = track?.sostenuto;
|
|
959
|
+
if (sostenuto) {
|
|
960
|
+
const spans: { down: number; lift: number; pitches: Set<number> }[] = [];
|
|
961
|
+
let open: number | undefined;
|
|
962
|
+
for (const event of sostenuto) {
|
|
963
|
+
if (event.state === "down" && open === undefined) open = event.tick;
|
|
964
|
+
else if (event.state === "up" && open !== undefined) {
|
|
965
|
+
spans.push({ down: open, lift: event.tick, pitches: new Set() });
|
|
966
|
+
open = undefined;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
if (open !== undefined)
|
|
970
|
+
spans.push({ down: open, lift: timing.endTick, pitches: new Set() });
|
|
971
|
+
const caughtBy = working.map((item) => {
|
|
972
|
+
const keyUp = item.start + item.bendLength;
|
|
973
|
+
return spans.filter((span) => {
|
|
974
|
+
const held = item.start <= span.down && span.down < keyUp;
|
|
975
|
+
if (held) span.pitches.add(item.note.pitch);
|
|
976
|
+
return held;
|
|
977
|
+
});
|
|
978
|
+
});
|
|
979
|
+
working = working.map((item, index) => {
|
|
980
|
+
const keyUp = item.start + item.bendLength;
|
|
981
|
+
let lift = -Infinity;
|
|
982
|
+
for (const span of caughtBy[index]!) lift = Math.max(lift, span.lift);
|
|
983
|
+
for (const span of spans)
|
|
984
|
+
if (
|
|
985
|
+
span.pitches.has(item.note.pitch) &&
|
|
986
|
+
span.down < item.start &&
|
|
987
|
+
item.start < span.lift
|
|
988
|
+
)
|
|
989
|
+
lift = Math.max(lift, span.lift);
|
|
990
|
+
if (lift === -Infinity) return item;
|
|
991
|
+
const current = item.start + item.duration;
|
|
992
|
+
let end = Math.max(current, Math.min(lift, timing.endTick));
|
|
993
|
+
if (end <= current) return item;
|
|
994
|
+
const restrike = working.find(
|
|
995
|
+
(other) =>
|
|
996
|
+
other !== item &&
|
|
997
|
+
other.note.pitch === item.note.pitch &&
|
|
998
|
+
other.start > item.start &&
|
|
999
|
+
other.start < end,
|
|
1000
|
+
);
|
|
1001
|
+
if (restrike) end = Math.max(current, restrike.start);
|
|
1002
|
+
return {
|
|
1003
|
+
...item,
|
|
1004
|
+
duration: end - item.start,
|
|
1005
|
+
pedalled: end > keyUp || item.pedalled === true,
|
|
1006
|
+
};
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
872
1009
|
// 4. Glide and monophony (on the written timing).
|
|
873
1010
|
let performed = glideAndMono(
|
|
874
1011
|
track,
|
package/core/fx.ts
CHANGED
|
@@ -12,10 +12,11 @@
|
|
|
12
12
|
* under `track.fx.<name>` with every parameter stored once enabled.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { MODAL_LANE_PARAMS } from "./resonators.ts";
|
|
15
|
+
import { MODAL_LANE_PARAMS, WIND_LANE_PARAMS } from "./resonators.ts";
|
|
16
16
|
import { SYNTH_LANE_PARAMS } from "./synth.ts";
|
|
17
17
|
import { STRING_LANE_PARAMS } from "./strings.ts";
|
|
18
18
|
import { KEYS_LANE_PARAMS } from "./keys.ts";
|
|
19
|
+
import { GRANULAR_LANE_PARAMS } from "./granular.ts";
|
|
19
20
|
import {
|
|
20
21
|
FxValidationError,
|
|
21
22
|
normalizeParam,
|
|
@@ -55,9 +56,16 @@ export const FX_CHAIN = Object.freeze([
|
|
|
55
56
|
"stomp",
|
|
56
57
|
"head",
|
|
57
58
|
"cab",
|
|
59
|
+
// 0.6.1 shoegaze (src/audio/effects/gaze.ts): a held tremolo arm, the
|
|
60
|
+
// dominant note's feedback bloom, a volume swell; double (mono in, stereo
|
|
61
|
+
// out) runs where the chain turns stereo.
|
|
62
|
+
"wobble",
|
|
63
|
+
"bloom",
|
|
64
|
+
"swell",
|
|
58
65
|
"tremolo",
|
|
59
66
|
"compressor",
|
|
60
67
|
"pan",
|
|
68
|
+
"double",
|
|
61
69
|
"phaser",
|
|
62
70
|
"chorus",
|
|
63
71
|
"leslie",
|
|
@@ -495,6 +503,135 @@ export const FX_SPECS = Object.freeze({
|
|
|
495
503
|
mix: mix(1),
|
|
496
504
|
},
|
|
497
505
|
},
|
|
506
|
+
wobble: {
|
|
507
|
+
label: "wobble",
|
|
508
|
+
doc: "held tremolo arm: slow seeded pitch wow and flutter of the whole track",
|
|
509
|
+
simple: ["depth", "rate", "drift"],
|
|
510
|
+
strudel: "(no Strudel equivalent; dawg shoegaze)",
|
|
511
|
+
params: {
|
|
512
|
+
depth: {
|
|
513
|
+
kind: "number",
|
|
514
|
+
min: 0,
|
|
515
|
+
max: 100,
|
|
516
|
+
default: 20,
|
|
517
|
+
step: 1,
|
|
518
|
+
unit: "c",
|
|
519
|
+
automate: true,
|
|
520
|
+
doc: "peak pitch deviation in cents",
|
|
521
|
+
},
|
|
522
|
+
rate: {
|
|
523
|
+
kind: "number",
|
|
524
|
+
min: 0.05,
|
|
525
|
+
max: 8,
|
|
526
|
+
default: 0.5,
|
|
527
|
+
step: 0.05,
|
|
528
|
+
unit: "Hz",
|
|
529
|
+
doc: "wobble rate",
|
|
530
|
+
},
|
|
531
|
+
drift: {
|
|
532
|
+
kind: "number",
|
|
533
|
+
min: 0,
|
|
534
|
+
max: 1,
|
|
535
|
+
default: 0.3,
|
|
536
|
+
step: 0.05,
|
|
537
|
+
doc: "0 periodic .. 1 wandering (seeded, so every render matches)",
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
},
|
|
541
|
+
bloom: {
|
|
542
|
+
label: "bloom",
|
|
543
|
+
doc: "feedback bloom: the top held note grows a singing harmonic",
|
|
544
|
+
simple: ["amount", "harm", "delay"],
|
|
545
|
+
strudel: "(no Strudel equivalent; dawg shoegaze)",
|
|
546
|
+
params: {
|
|
547
|
+
amount: {
|
|
548
|
+
kind: "number",
|
|
549
|
+
min: 0,
|
|
550
|
+
max: 1,
|
|
551
|
+
default: 0.5,
|
|
552
|
+
step: 0.05,
|
|
553
|
+
doc: "level of the feedback partial",
|
|
554
|
+
},
|
|
555
|
+
harm: {
|
|
556
|
+
kind: "number",
|
|
557
|
+
min: 1,
|
|
558
|
+
max: 4,
|
|
559
|
+
default: 2,
|
|
560
|
+
step: 1,
|
|
561
|
+
doc: "harmonic that feeds back: 1 the note, 2 its octave, 3 the fifth above",
|
|
562
|
+
},
|
|
563
|
+
delay: {
|
|
564
|
+
kind: "number",
|
|
565
|
+
min: 0,
|
|
566
|
+
max: 4,
|
|
567
|
+
default: 0.6,
|
|
568
|
+
step: 0.05,
|
|
569
|
+
unit: "s",
|
|
570
|
+
doc: "how long a note is held before it starts to feed back",
|
|
571
|
+
},
|
|
572
|
+
time: {
|
|
573
|
+
kind: "number",
|
|
574
|
+
min: 0.05,
|
|
575
|
+
max: 4,
|
|
576
|
+
default: 1,
|
|
577
|
+
step: 0.05,
|
|
578
|
+
unit: "s",
|
|
579
|
+
doc: "how fast the feedback grows",
|
|
580
|
+
},
|
|
581
|
+
},
|
|
582
|
+
},
|
|
583
|
+
swell: {
|
|
584
|
+
label: "swell",
|
|
585
|
+
doc: "volume swell: each strum fades in, no pick attack",
|
|
586
|
+
simple: ["time", "mix"],
|
|
587
|
+
strudel: "(no Strudel equivalent; dawg shoegaze)",
|
|
588
|
+
params: {
|
|
589
|
+
time: {
|
|
590
|
+
kind: "number",
|
|
591
|
+
min: 0.01,
|
|
592
|
+
max: 4,
|
|
593
|
+
default: 0.4,
|
|
594
|
+
step: 0.01,
|
|
595
|
+
unit: "s",
|
|
596
|
+
doc: "rise time after each onset",
|
|
597
|
+
},
|
|
598
|
+
mix: mix(1),
|
|
599
|
+
},
|
|
600
|
+
},
|
|
601
|
+
double: {
|
|
602
|
+
label: "double",
|
|
603
|
+
doc: "automatic double tracking: a seeded second take, spread left and right",
|
|
604
|
+
simple: ["time", "drift", "width"],
|
|
605
|
+
strudel: "(no Strudel equivalent; dawg shoegaze)",
|
|
606
|
+
params: {
|
|
607
|
+
time: {
|
|
608
|
+
kind: "number",
|
|
609
|
+
min: 5,
|
|
610
|
+
max: 60,
|
|
611
|
+
default: 22,
|
|
612
|
+
step: 1,
|
|
613
|
+
unit: "ms",
|
|
614
|
+
doc: "how late the second take plays",
|
|
615
|
+
},
|
|
616
|
+
drift: {
|
|
617
|
+
kind: "number",
|
|
618
|
+
min: 0,
|
|
619
|
+
max: 10,
|
|
620
|
+
default: 3,
|
|
621
|
+
step: 0.5,
|
|
622
|
+
unit: "ms",
|
|
623
|
+
doc: "how far the second take's timing wanders",
|
|
624
|
+
},
|
|
625
|
+
width: {
|
|
626
|
+
kind: "number",
|
|
627
|
+
min: 0,
|
|
628
|
+
max: 1,
|
|
629
|
+
default: 0.6,
|
|
630
|
+
step: 0.05,
|
|
631
|
+
doc: "0 centred .. 1 the two takes hard left and right",
|
|
632
|
+
},
|
|
633
|
+
},
|
|
634
|
+
},
|
|
498
635
|
tremolo: {
|
|
499
636
|
label: "tremolo",
|
|
500
637
|
doc: "volume modulation",
|
|
@@ -1006,6 +1143,14 @@ export const CORE_EFFECTS: readonly EffectName[] = Object.freeze([
|
|
|
1006
1143
|
"reverb",
|
|
1007
1144
|
] as const);
|
|
1008
1145
|
|
|
1146
|
+
/** The 0.6.1 shoegaze stages (Effects > Shoegaze), in chain order. */
|
|
1147
|
+
export const SHOEGAZE_EFFECTS: readonly EffectName[] = Object.freeze([
|
|
1148
|
+
"wobble",
|
|
1149
|
+
"bloom",
|
|
1150
|
+
"swell",
|
|
1151
|
+
"double",
|
|
1152
|
+
] as const);
|
|
1153
|
+
|
|
1009
1154
|
export function isEffectName(value: unknown): value is EffectName {
|
|
1010
1155
|
return (
|
|
1011
1156
|
typeof value === "string" &&
|
|
@@ -1076,12 +1221,20 @@ export function normalizeFx(input: unknown): TrackFx | undefined {
|
|
|
1076
1221
|
|
|
1077
1222
|
/** Automation lane name of an `fx` parameter, e.g. `distort-drive`. */
|
|
1078
1223
|
export type FxLane =
|
|
1079
|
-
`${FxName | "reverb" | "synth" | "string" | "keys" | "modal"}-${string}`;
|
|
1224
|
+
`${FxName | "reverb" | "synth" | "string" | "keys" | "modal" | "grain" | "wind"}-${string}`;
|
|
1080
1225
|
|
|
1081
1226
|
/** Every automatable `fx` parameter as `{ lane, effect, param, spec }`. */
|
|
1082
1227
|
export const FX_LANES: readonly Readonly<{
|
|
1083
1228
|
lane: FxLane;
|
|
1084
|
-
effect:
|
|
1229
|
+
effect:
|
|
1230
|
+
| FxName
|
|
1231
|
+
| "reverb"
|
|
1232
|
+
| "synth"
|
|
1233
|
+
| "string"
|
|
1234
|
+
| "keys"
|
|
1235
|
+
| "modal"
|
|
1236
|
+
| "grain"
|
|
1237
|
+
| "wind";
|
|
1085
1238
|
param: string;
|
|
1086
1239
|
spec: NumberParam;
|
|
1087
1240
|
}>[] = Object.freeze([
|
|
@@ -1137,6 +1290,25 @@ export const FX_LANES: readonly Readonly<{
|
|
|
1137
1290
|
spec,
|
|
1138
1291
|
}),
|
|
1139
1292
|
),
|
|
1293
|
+
// Granular parameters (core/granular.ts, 0.6.1), read every 32 frames;
|
|
1294
|
+
// grain-scoped ones are latched at each grain's onset.
|
|
1295
|
+
...GRANULAR_LANE_PARAMS.map(({ param, spec }) =>
|
|
1296
|
+
Object.freeze({
|
|
1297
|
+
lane: `grain-${param}` as FxLane,
|
|
1298
|
+
effect: "grain" as const,
|
|
1299
|
+
param,
|
|
1300
|
+
spec,
|
|
1301
|
+
}),
|
|
1302
|
+
),
|
|
1303
|
+
// Wind parameters (core/winds.ts), read every 32-sample control tick.
|
|
1304
|
+
...WIND_LANE_PARAMS.map(({ param, spec }) =>
|
|
1305
|
+
Object.freeze({
|
|
1306
|
+
lane: `wind-${param}` as FxLane,
|
|
1307
|
+
effect: "wind" as const,
|
|
1308
|
+
param,
|
|
1309
|
+
spec,
|
|
1310
|
+
}),
|
|
1311
|
+
),
|
|
1140
1312
|
]);
|
|
1141
1313
|
|
|
1142
1314
|
/**
|
|
@@ -1222,6 +1394,24 @@ export const FX_PRESETS: Readonly<
|
|
|
1222
1394
|
"1x15": { type: "1x15" },
|
|
1223
1395
|
di: { type: "di" },
|
|
1224
1396
|
},
|
|
1397
|
+
wobble: {
|
|
1398
|
+
glide: { depth: 35, rate: 0.4, drift: 0.4 },
|
|
1399
|
+
tape: { depth: 12, rate: 0.8, drift: 0.6 },
|
|
1400
|
+
seasick: { depth: 60, rate: 0.25, drift: 0.2 },
|
|
1401
|
+
},
|
|
1402
|
+
bloom: {
|
|
1403
|
+
octave: { amount: 0.5, harm: 2, delay: 0.6, time: 1 },
|
|
1404
|
+
fifth: { amount: 0.4, harm: 3, delay: 0.8, time: 1.2 },
|
|
1405
|
+
ebow: { amount: 0.8, harm: 1, delay: 0.1, time: 0.6 },
|
|
1406
|
+
},
|
|
1407
|
+
swell: {
|
|
1408
|
+
slow: { time: 0.8 },
|
|
1409
|
+
violin: { time: 0.3 },
|
|
1410
|
+
},
|
|
1411
|
+
double: {
|
|
1412
|
+
tight: { time: 12, drift: 1.5, width: 0.5 },
|
|
1413
|
+
wide: { time: 28, drift: 3, width: 0.7 },
|
|
1414
|
+
},
|
|
1225
1415
|
tremolo: {
|
|
1226
1416
|
gentle: { sync: 1, depth: 0.3, shape: "sine" },
|
|
1227
1417
|
"eighth-chop": { sync: 0.5, depth: 0.9, shape: "square" },
|
|
@@ -1390,14 +1580,70 @@ export const RIG_PRESETS: Readonly<
|
|
|
1390
1580
|
head: { type: "chime", gain: 3, treble: 7 },
|
|
1391
1581
|
cab: { type: "2x12", mic: 0.2 },
|
|
1392
1582
|
compressor: { threshold: -20, ratio: 4, attack: 0.01, release: 0.15 },
|
|
1583
|
+
// 0.6.1: the double-tracked jangle of a twelve-string record. A stored
|
|
1584
|
+
// 0.6.0 jangle (no double) keeps rendering as it was; `rig jangle` again
|
|
1585
|
+
// adds it. The SDK's rig("jangle") keeps its 0.6.0 stages (see
|
|
1586
|
+
// RIG_ADDED), so the printer writes this double out explicitly.
|
|
1587
|
+
double: { time: 12, drift: 1.5, width: 0.5 },
|
|
1393
1588
|
},
|
|
1394
1589
|
alt: {
|
|
1395
1590
|
stomp: { type: "rat", gain: 6, tone: 0.4 },
|
|
1396
1591
|
head: { type: "crunch", gain: 4 },
|
|
1397
1592
|
cab: { type: "4x12" },
|
|
1398
1593
|
},
|
|
1594
|
+
// 0.6.1 shoegaze rigs (src/audio/effects/gaze.ts): fuzz into a held
|
|
1595
|
+
// tremolo arm, a feedback bloom, doubled takes and a long wash.
|
|
1596
|
+
shoegaze: {
|
|
1597
|
+
stomp: { type: "fuzz", gain: 7, tone: 0.45 },
|
|
1598
|
+
head: { type: "chime", gain: 4 },
|
|
1599
|
+
cab: { type: "2x12" },
|
|
1600
|
+
wobble: { depth: 25, rate: 0.4, drift: 0.4 },
|
|
1601
|
+
bloom: { amount: 0.4, harm: 2, delay: 0.6, time: 1 },
|
|
1602
|
+
double: { time: 18, drift: 2.5, width: 0.4 },
|
|
1603
|
+
postgain: { gain: 0.86 },
|
|
1604
|
+
reverb: { mix: 0.35, size: 0.9, fade: 4, predelay: 0.02, dim: 6000 },
|
|
1605
|
+
},
|
|
1606
|
+
glide: {
|
|
1607
|
+
stomp: { type: "face", gain: 5, tone: 0.5 },
|
|
1608
|
+
head: { type: "clean", gain: 4, treble: 5 },
|
|
1609
|
+
cab: { type: "2x12" },
|
|
1610
|
+
wobble: { depth: 35, rate: 0.4, drift: 0.4 },
|
|
1611
|
+
postgain: { gain: 0.8 },
|
|
1612
|
+
reverb: { mix: 0.35, size: 0.7, fade: 2.5, predelay: 0.01, dim: 6000 },
|
|
1613
|
+
},
|
|
1614
|
+
dreampop: {
|
|
1615
|
+
head: { type: "clean", gain: 2, treble: 6 },
|
|
1616
|
+
cab: { type: "1x12" },
|
|
1617
|
+
chorus: { rate: 0.6, depth: 0.3, mix: 0.35 },
|
|
1618
|
+
double: { time: 14, drift: 2, width: 0.4 },
|
|
1619
|
+
postgain: { gain: 1.45 },
|
|
1620
|
+
reverb: { mix: 0.4, size: 0.85, fade: 5, predelay: 0.03, dim: 7000 },
|
|
1621
|
+
},
|
|
1622
|
+
swell: {
|
|
1623
|
+
head: { type: "clean", gain: 3, treble: 5 },
|
|
1624
|
+
cab: { type: "1x12" },
|
|
1625
|
+
swell: { time: 0.5 },
|
|
1626
|
+
postgain: { gain: 1.25 },
|
|
1627
|
+
reverb: { mix: 0.45, size: 0.8, fade: 4, predelay: 0.02, dim: 6500 },
|
|
1628
|
+
},
|
|
1629
|
+
ebow: {
|
|
1630
|
+
stomp: { type: "od", gain: 3, tone: 0.5, level: 3 },
|
|
1631
|
+
head: { type: "lead", gain: 5, mid: 6 },
|
|
1632
|
+
cab: { type: "4x12" },
|
|
1633
|
+
swell: { time: 0.25 },
|
|
1634
|
+
bloom: { amount: 0.8, harm: 1, delay: 0.1, time: 0.6 },
|
|
1635
|
+
},
|
|
1399
1636
|
});
|
|
1400
1637
|
|
|
1638
|
+
/**
|
|
1639
|
+
* Effects a rig preset gained after SDK `rig(name)` was fixed. `rig(name)`
|
|
1640
|
+
* leaves them out, so an older song.ts spreading `...rig("jangle")` keeps
|
|
1641
|
+
* its sound; the printer writes them next to the spread, and a stored
|
|
1642
|
+
* track with or without them still reads as that rig.
|
|
1643
|
+
*/
|
|
1644
|
+
export const RIG_ADDED: Readonly<Record<string, readonly FxName[]>> =
|
|
1645
|
+
Object.freeze({ jangle: Object.freeze(["double"] as FxName[]) });
|
|
1646
|
+
|
|
1401
1647
|
export function isRigPreset(name: string): boolean {
|
|
1402
1648
|
return Object.prototype.hasOwnProperty.call(RIG_PRESETS, name);
|
|
1403
1649
|
}
|
|
@@ -1476,8 +1722,11 @@ export function rigReverb(name: string): FxValues | undefined {
|
|
|
1476
1722
|
export function rigPresetOf(fx: TrackFx | undefined): string | undefined {
|
|
1477
1723
|
if (!fx || !RIG_STAGES.some((stage) => fx[stage])) return undefined;
|
|
1478
1724
|
for (const name of Object.keys(RIG_PRESETS)) {
|
|
1479
|
-
const want
|
|
1480
|
-
|
|
1725
|
+
const want: Partial<Record<FxName, FxValues>> = {
|
|
1726
|
+
...(applyRigPreset(undefined, name) ?? {}),
|
|
1727
|
+
};
|
|
1728
|
+
for (const added of RIG_ADDED[name] ?? []) delete want[added];
|
|
1729
|
+
const keys = new Set<string>([...RIG_STAGES, ...Object.keys(want)]);
|
|
1481
1730
|
if (
|
|
1482
1731
|
[...keys].every(
|
|
1483
1732
|
(key) =>
|
|
@@ -1492,7 +1741,10 @@ export function rigPresetOf(fx: TrackFx | undefined): string | undefined {
|
|
|
1492
1741
|
|
|
1493
1742
|
/** The effects of `fx` a printed `...rig(name)` stands for. */
|
|
1494
1743
|
export function rigPresetKeys(name: string): readonly FxName[] {
|
|
1495
|
-
|
|
1744
|
+
const added = RIG_ADDED[name] ?? [];
|
|
1745
|
+
return Object.keys(RIG_PRESETS[name] ?? {}).filter(
|
|
1746
|
+
(key): key is FxName => isFxName(key) && !added.includes(key),
|
|
1747
|
+
);
|
|
1496
1748
|
}
|
|
1497
1749
|
|
|
1498
1750
|
export function effectPresetNames(effect: EffectName): readonly string[] {
|
package/core/granular.ts
CHANGED
|
@@ -77,6 +77,14 @@ export type TrackGranular = Readonly<{
|
|
|
77
77
|
release?: number;
|
|
78
78
|
veltone?: number;
|
|
79
79
|
gain?: number;
|
|
80
|
+
/** Optional (0.6.1): grain rate as a note value on the tempo map. */
|
|
81
|
+
sync?: GrainSync;
|
|
82
|
+
/** Optional (0.6.1): snap grain pitches to the scale or chord. */
|
|
83
|
+
quant?: GrainQuant;
|
|
84
|
+
/** Optional (0.6.1): one voice; legato notes retarget it. */
|
|
85
|
+
mono?: boolean;
|
|
86
|
+
/** Optional (0.6.1): the sustain pedal freezes the head. */
|
|
87
|
+
pedal?: boolean;
|
|
80
88
|
/**
|
|
81
89
|
* The instrument `grain off` returns to (set when granular turns on;
|
|
82
90
|
* absent: a sampler, a wavetable, or the source's synth).
|
|
@@ -102,6 +110,30 @@ const n = (
|
|
|
102
110
|
...extra,
|
|
103
111
|
});
|
|
104
112
|
|
|
113
|
+
export const GRAIN_SYNC_VALUES = Object.freeze([
|
|
114
|
+
"off",
|
|
115
|
+
"1/64",
|
|
116
|
+
"1/32",
|
|
117
|
+
"1/16t",
|
|
118
|
+
"1/16",
|
|
119
|
+
"1/16d",
|
|
120
|
+
"1/8t",
|
|
121
|
+
"1/8",
|
|
122
|
+
"1/8d",
|
|
123
|
+
"1/4t",
|
|
124
|
+
"1/4",
|
|
125
|
+
"1/4d",
|
|
126
|
+
"1/2",
|
|
127
|
+
"1/1",
|
|
128
|
+
] as const);
|
|
129
|
+
export type GrainSync = (typeof GRAIN_SYNC_VALUES)[number];
|
|
130
|
+
export const GRAIN_QUANT_VALUES = Object.freeze([
|
|
131
|
+
"off",
|
|
132
|
+
"scale",
|
|
133
|
+
"chord",
|
|
134
|
+
] as const);
|
|
135
|
+
export type GrainQuant = (typeof GRAIN_QUANT_VALUES)[number];
|
|
136
|
+
|
|
105
137
|
/**
|
|
106
138
|
* The granular parameter table: one row per stored number, enum or flag,
|
|
107
139
|
* in stored key order. It drives validation, the printer, the menu, the
|
|
@@ -223,8 +255,63 @@ export const GRANULAR_PARAMS: Readonly<Record<string, ParamSpec>> =
|
|
|
223
255
|
}),
|
|
224
256
|
veltone: n(0, 1, 0, 0.05, "soft notes are darker"),
|
|
225
257
|
gain: n(0, 4, 1, 0.05, "voice level"),
|
|
258
|
+
// 0.6.1 grainplay (appended; absent keeps 0.6.0 behaviour).
|
|
259
|
+
sync: Object.freeze({
|
|
260
|
+
kind: "enum",
|
|
261
|
+
values: GRAIN_SYNC_VALUES,
|
|
262
|
+
default: "off",
|
|
263
|
+
doc: "grain rate as a note value on the tempo map (off: grain/overlap)",
|
|
264
|
+
}),
|
|
265
|
+
quant: Object.freeze({
|
|
266
|
+
kind: "enum",
|
|
267
|
+
values: GRAIN_QUANT_VALUES,
|
|
268
|
+
default: "off",
|
|
269
|
+
doc: "snap grain pitches to the song scale or the sounding chord",
|
|
270
|
+
}),
|
|
271
|
+
mono: Object.freeze({
|
|
272
|
+
kind: "boolean",
|
|
273
|
+
default: false,
|
|
274
|
+
doc: "one voice: legato notes retarget its pitch, the cloud keeps going",
|
|
275
|
+
}),
|
|
276
|
+
pedal: Object.freeze({
|
|
277
|
+
kind: "boolean",
|
|
278
|
+
default: false,
|
|
279
|
+
doc: "the sustain pedal freezes the head while it is down",
|
|
280
|
+
}),
|
|
226
281
|
});
|
|
227
282
|
|
|
283
|
+
/** Lanes `grain-<param>` (0.6.1): every number row marked `automate`. */
|
|
284
|
+
export const GRANULAR_LANE_PARAMS: readonly Readonly<{
|
|
285
|
+
param: string;
|
|
286
|
+
spec: Extract<ParamSpec, { kind: "number" }>;
|
|
287
|
+
}>[] = Object.freeze(
|
|
288
|
+
Object.entries(GRANULAR_PARAMS)
|
|
289
|
+
.filter(
|
|
290
|
+
(entry): entry is [string, Extract<ParamSpec, { kind: "number" }>] =>
|
|
291
|
+
entry[1].kind === "number" && entry[1].automate === true,
|
|
292
|
+
)
|
|
293
|
+
.map(([param, spec]) => Object.freeze({ param, spec })),
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
/** Note values `sync` takes, with their length in beats. */
|
|
297
|
+
export const GRAIN_SYNC_BEATS: Readonly<Record<string, number>> = Object.freeze(
|
|
298
|
+
{
|
|
299
|
+
"1/64": 1 / 16,
|
|
300
|
+
"1/32": 1 / 8,
|
|
301
|
+
"1/16t": 1 / 6,
|
|
302
|
+
"1/16": 1 / 4,
|
|
303
|
+
"1/16d": 3 / 8,
|
|
304
|
+
"1/8t": 1 / 3,
|
|
305
|
+
"1/8": 1 / 2,
|
|
306
|
+
"1/8d": 3 / 4,
|
|
307
|
+
"1/4t": 2 / 3,
|
|
308
|
+
"1/4": 1,
|
|
309
|
+
"1/4d": 3 / 2,
|
|
310
|
+
"1/2": 2,
|
|
311
|
+
"1/1": 4,
|
|
312
|
+
},
|
|
313
|
+
);
|
|
314
|
+
|
|
228
315
|
export type GranularParamName = keyof typeof GRANULAR_PARAMS & string;
|
|
229
316
|
|
|
230
317
|
/** Resolved settings the voice reads (presets and defaults applied). */
|
|
@@ -256,6 +343,10 @@ export type GranularSettings = Readonly<{
|
|
|
256
343
|
release: number;
|
|
257
344
|
veltone: number;
|
|
258
345
|
gain: number;
|
|
346
|
+
sync: GrainSync;
|
|
347
|
+
quant: GrainQuant;
|
|
348
|
+
mono: boolean;
|
|
349
|
+
pedal: boolean;
|
|
259
350
|
}>;
|
|
260
351
|
|
|
261
352
|
/**
|