@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/src/tui/modal-menu.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { EnumParam, NumberParam } from "../../core/params.ts";
|
|
7
7
|
import {
|
|
8
|
+
GAMELAN_PRESETS,
|
|
8
9
|
MODAL_INSTRUMENT,
|
|
9
10
|
MODAL_MALLET_NAMES,
|
|
10
11
|
MODAL_PARAMS,
|
|
@@ -12,6 +13,7 @@ import {
|
|
|
12
13
|
MODAL_PRESET_NAMES,
|
|
13
14
|
MODAL_SIMPLE,
|
|
14
15
|
modalSettings,
|
|
16
|
+
type ModalPresetName,
|
|
15
17
|
} from "../../core/resonators.ts";
|
|
16
18
|
import type { Track } from "../../core/score.ts";
|
|
17
19
|
import { num, specStep, type MenuNode } from "./menu.ts";
|
|
@@ -24,13 +26,38 @@ export function malletsMenu(): MenuNode {
|
|
|
24
26
|
label: "Mallets and bells",
|
|
25
27
|
detail: `${MODAL_PRESET_NAMES.length} modal presets · marimba, vibes, gong …`,
|
|
26
28
|
help: "struck bars, bells and bowls on the modal resonator engine",
|
|
27
|
-
build: () =>
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
build: () => [
|
|
30
|
+
// The core presets keep their rows (gong too); the gamelan bronzes
|
|
31
|
+
// added in 0.6.1 sit in their own sub-group.
|
|
32
|
+
...MODAL_PRESET_NAMES.filter(
|
|
33
|
+
(name) => name === "gong" || !GAMELAN_PRESETS.includes(name),
|
|
34
|
+
).map(presetRow),
|
|
35
|
+
gamelanMenu(),
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function presetRow(name: ModalPresetName): MenuNode {
|
|
41
|
+
return {
|
|
42
|
+
kind: "action",
|
|
43
|
+
label: `${name} ${MODAL_PRESETS[name].doc}`,
|
|
44
|
+
command: `modal ${name}`,
|
|
45
|
+
help: MODAL_PRESETS[name].styles,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Mallets and bells › Gamelan (f061-gamelan-winds): the Javanese and
|
|
51
|
+
* Balinese bronzes. `modal gamelan` points here.
|
|
52
|
+
*/
|
|
53
|
+
export function gamelanMenu(): MenuNode {
|
|
54
|
+
return {
|
|
55
|
+
kind: "menu",
|
|
56
|
+
id: "gamelan",
|
|
57
|
+
label: "Gamelan",
|
|
58
|
+
detail: `${GAMELAN_PRESETS.length} bronzes · saron, gangsa (ombak), gong …`,
|
|
59
|
+
help: "Javanese and Balinese gamelan; pair two gangsa tracks with modal pair, tune with tuning slendro or pelog",
|
|
60
|
+
build: () => GAMELAN_PRESETS.map(presetRow),
|
|
34
61
|
};
|
|
35
62
|
}
|
|
36
63
|
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
VELOCITY_CURVES,
|
|
13
13
|
} from "../../core/expression.ts";
|
|
14
14
|
import type { Note, Track } from "../../core/score.ts";
|
|
15
|
+
import { isPianoFamily } from "../../core/keys.ts";
|
|
15
16
|
import {
|
|
16
17
|
BEND_SHAPES,
|
|
17
18
|
DEFAULT_HUMANIZE,
|
|
@@ -50,12 +51,50 @@ export function performanceDetail(track: Track | undefined): string {
|
|
|
50
51
|
const parts = [
|
|
51
52
|
track.glide ? `glide ${track.glide.mode}` : "",
|
|
52
53
|
track.pedal?.length ? "pedal" : "",
|
|
54
|
+
track.softPedal?.length ? "soft" : "",
|
|
55
|
+
track.sostenuto?.length ? "sost" : "",
|
|
53
56
|
track.velocityCurve ? `vel ${track.velocityCurve.curve}` : "",
|
|
54
57
|
track.humanize ? "humanized" : "",
|
|
55
58
|
].filter(Boolean);
|
|
56
59
|
return parts.length ? parts.join(" · ") : "as written";
|
|
57
60
|
}
|
|
58
61
|
|
|
62
|
+
/** Soft pedal and Sostenuto rows: off, the current lane, or the whole loop. */
|
|
63
|
+
function pianoPedalNodes(track: Track): MenuNode[] {
|
|
64
|
+
const rows = [
|
|
65
|
+
{
|
|
66
|
+
lane: "soft",
|
|
67
|
+
label: "soft pedal",
|
|
68
|
+
events: track.softPedal,
|
|
69
|
+
help: "una corda: the hammers shift to strike fewer strings, softer and darker; read at each note's onset",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
lane: "sost",
|
|
73
|
+
label: "sostenuto",
|
|
74
|
+
events: track.sostenuto,
|
|
75
|
+
help: "holds only the keys already down when it presses (the middle pedal); later notes damp normally",
|
|
76
|
+
},
|
|
77
|
+
] as const;
|
|
78
|
+
return rows
|
|
79
|
+
.filter((row) => isPianoFamily(track.instrument) || row.events?.length)
|
|
80
|
+
.map((row): MenuNode => {
|
|
81
|
+
const value = row.events?.length ? `${row.events.length} events` : "off";
|
|
82
|
+
return {
|
|
83
|
+
kind: "choice",
|
|
84
|
+
label: row.label,
|
|
85
|
+
value,
|
|
86
|
+
options: ["off", ...(value === "off" ? [] : [value]), "bars"],
|
|
87
|
+
command: (option) =>
|
|
88
|
+
option === "bars"
|
|
89
|
+
? `pedal ${row.lane} bars`
|
|
90
|
+
: option === "off"
|
|
91
|
+
? `pedal ${row.lane} off`
|
|
92
|
+
: `pedal ${row.lane}`,
|
|
93
|
+
help: row.help,
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
59
98
|
export function performanceNodes(context: MenuContext): MenuNode[] {
|
|
60
99
|
const track = context.score.tracks.find(
|
|
61
100
|
(item) => item.id === context.trackId,
|
|
@@ -148,6 +187,9 @@ export function performanceNodes(context: MenuContext): MenuNode[] {
|
|
|
148
187
|
: "pedal",
|
|
149
188
|
help: "bars replaces the lane with a re-pedal on each downbeat; in play mode press Tab (latch) or hold Shift while recording",
|
|
150
189
|
},
|
|
190
|
+
// keys-electric (0.6.1): the piano's other two pedals, on modelled piano
|
|
191
|
+
// tracks (or wherever a lane is already set).
|
|
192
|
+
...pianoPedalNodes(track),
|
|
151
193
|
{
|
|
152
194
|
kind: "choice",
|
|
153
195
|
label: "velocity curve",
|
package/src/tui/play-chords.ts
CHANGED
|
@@ -35,6 +35,10 @@ import {
|
|
|
35
35
|
EXTENSIONS,
|
|
36
36
|
MAX_VOICING_STEP,
|
|
37
37
|
PERFORM_MODES,
|
|
38
|
+
DEFAULT_STROKE_SPEED,
|
|
39
|
+
STROKE_PATTERN_NAMES,
|
|
40
|
+
strokeGrid,
|
|
41
|
+
type PerformOptions,
|
|
38
42
|
PROGRESSION_PRESETS,
|
|
39
43
|
PROGRESSION_STYLES,
|
|
40
44
|
SPREADS,
|
|
@@ -100,6 +104,10 @@ export type ChordSettings = {
|
|
|
100
104
|
/** `none` or a PROGRESSION_PRESETS name; drives the "next" suggestion. */
|
|
101
105
|
preset: string;
|
|
102
106
|
style: ProgressionStyle;
|
|
107
|
+
/** Guitar perform mode: a STROKE_PATTERNS name or a D U d u x - . grid. */
|
|
108
|
+
strokes: string;
|
|
109
|
+
/** Guitar perform mode: ms a full six-string down stroke takes. */
|
|
110
|
+
speed: number;
|
|
103
111
|
};
|
|
104
112
|
|
|
105
113
|
export function defaultChordSettings(): ChordSettings {
|
|
@@ -116,9 +124,45 @@ export function defaultChordSettings(): ChordSettings {
|
|
|
116
124
|
sevenths: false,
|
|
117
125
|
preset: "none",
|
|
118
126
|
style: "pop",
|
|
127
|
+
strokes: "down",
|
|
128
|
+
speed: DEFAULT_STROKE_SPEED * 1000,
|
|
119
129
|
};
|
|
120
130
|
}
|
|
121
131
|
|
|
132
|
+
/** Play mode's `[` / `]` strum speed step in ms. */
|
|
133
|
+
export const STROKE_SPEED_STEP = 5;
|
|
134
|
+
|
|
135
|
+
/** Strum speed bounds in ms (0 = every string at once). */
|
|
136
|
+
export const STROKE_SPEED_MS = Object.freeze({ min: 0, max: 200 });
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* A strum speed from `/chords speed` text: milliseconds (`22`, `22ms`) or
|
|
140
|
+
* beats (`1/32b`, `0.05b`) at `bpm`. Undefined when not a speed.
|
|
141
|
+
*/
|
|
142
|
+
export function parseStrokeSpeed(
|
|
143
|
+
text: string | undefined,
|
|
144
|
+
bpm: number,
|
|
145
|
+
): number | undefined {
|
|
146
|
+
const raw = (text ?? "").trim().toLowerCase();
|
|
147
|
+
const beats = /^(\d+(?:\.\d+)?)(?:\/(\d+))?\s*(?:b|beats?)$/.exec(raw);
|
|
148
|
+
let ms: number;
|
|
149
|
+
if (beats) {
|
|
150
|
+
const value = Number(beats[1]) / (beats[2] ? Number(beats[2]) : 1);
|
|
151
|
+
ms = (value * 60_000) / bpm;
|
|
152
|
+
} else {
|
|
153
|
+
const plain = /^(\d+(?:\.\d+)?)\s*(?:ms)?$/.exec(raw);
|
|
154
|
+
if (!plain) return undefined;
|
|
155
|
+
ms = Number(plain[1]);
|
|
156
|
+
}
|
|
157
|
+
if (
|
|
158
|
+
!Number.isFinite(ms) ||
|
|
159
|
+
ms < STROKE_SPEED_MS.min ||
|
|
160
|
+
ms > STROKE_SPEED_MS.max
|
|
161
|
+
)
|
|
162
|
+
return undefined;
|
|
163
|
+
return Math.round(ms * 10) / 10;
|
|
164
|
+
}
|
|
165
|
+
|
|
122
166
|
/**
|
|
123
167
|
* Tracks where auto chords are the default: anything pitched that is not a
|
|
124
168
|
* bass, a drum kit or a one-shot sampler.
|
|
@@ -236,6 +280,16 @@ export class ChordPad {
|
|
|
236
280
|
s.perform = PERFORM_MODES[(index + 1) % PERFORM_MODES.length]!;
|
|
237
281
|
return { type: "status", status: `perform ${s.perform}` };
|
|
238
282
|
}
|
|
283
|
+
if ((value === "[" || value === "]") && s.perform === "guitar") {
|
|
284
|
+
// Strum speed: slower ([) or faster (]) in 5 ms steps.
|
|
285
|
+
const wanted =
|
|
286
|
+
s.speed + (value === "[" ? STROKE_SPEED_STEP : -STROKE_SPEED_STEP);
|
|
287
|
+
s.speed = Math.max(
|
|
288
|
+
STROKE_SPEED_MS.min,
|
|
289
|
+
Math.min(STROKE_SPEED_MS.max, Math.round(wanted)),
|
|
290
|
+
);
|
|
291
|
+
return { type: "status", status: `strum speed ${s.speed}ms` };
|
|
292
|
+
}
|
|
239
293
|
if (value === "b") {
|
|
240
294
|
const index = BASS_MODES.indexOf(s.bass);
|
|
241
295
|
s.bass = BASS_MODES[(index + 1) % BASS_MODES.length]!;
|
|
@@ -392,6 +446,9 @@ export class ChordPad {
|
|
|
392
446
|
{ key: "0", label: "clear", on: false },
|
|
393
447
|
{ key: "-=", label: `voicing ${signed(s.inversion)}`, on: false },
|
|
394
448
|
{ key: "9", label: perform, on: false },
|
|
449
|
+
...(s.perform === "guitar"
|
|
450
|
+
? [{ key: "[]", label: `speed ${s.speed}ms`, on: false }]
|
|
451
|
+
: []),
|
|
395
452
|
{ key: "b", label: `bass ${s.bass}`, on: false },
|
|
396
453
|
{ key: "n", label: "next", on: false },
|
|
397
454
|
{ key: "q", label: s.mode === "auto" ? "auto" : "manual", on: false },
|
|
@@ -412,6 +469,8 @@ export class ChordPad {
|
|
|
412
469
|
if (s.spread !== "close") parts.push(s.spread);
|
|
413
470
|
if (s.perform === "pattern")
|
|
414
471
|
parts.push(`pattern ${patternLabel(s.pattern)}`);
|
|
472
|
+
else if (s.perform === "guitar")
|
|
473
|
+
parts.push(`guitar ${s.strokes} ${s.speed}ms`);
|
|
415
474
|
else if (s.perform !== "block") parts.push(s.perform);
|
|
416
475
|
if (s.bass !== "off") parts.push(`bass ${s.bass}`);
|
|
417
476
|
return parts.join(" · ");
|
|
@@ -435,15 +494,18 @@ function signed(value: number): string {
|
|
|
435
494
|
/**
|
|
436
495
|
* `/chords …`: `auto|manual|off`, `voicing <-12..12>`, `spread <close|open|
|
|
437
496
|
* wide>`, `bass off|chords|unison|single|solo` (`on` = chords), `perform
|
|
438
|
-
* <mode>`, `pattern <1..13|name>`, `
|
|
497
|
+
* <mode>`, `pattern <1..13|name>`, `strokes <name|D-DU-UDU>` (guitar
|
|
498
|
+
* mode), `speed <ms|1/32b>`, `rate <grid|1/8…>`, `octaves
|
|
439
499
|
* <1..4>`, `sevenths on|off`, `preset <name|none>`, `style <pop|jazz|…>`.
|
|
440
500
|
* Mutates `settings`; returns the receipt or an error.
|
|
441
501
|
*/
|
|
442
502
|
export function applyChordsCommand(
|
|
443
503
|
settings: ChordSettings,
|
|
444
504
|
argument: string,
|
|
505
|
+
bpm = 120,
|
|
445
506
|
): { ok: boolean; message: string } {
|
|
446
|
-
const
|
|
507
|
+
const raw = argument.trim().split(/\s+/).filter(Boolean);
|
|
508
|
+
const words = raw.map((word) => word.toLowerCase());
|
|
447
509
|
const [head, value] = words;
|
|
448
510
|
const done = (message: string) => ({ ok: true, message });
|
|
449
511
|
const bad = (message: string) => ({ ok: false, message });
|
|
@@ -505,6 +567,28 @@ export function applyChordsCommand(
|
|
|
505
567
|
return bad(`rate takes ${ARP_RATES.join("|")}`);
|
|
506
568
|
settings.rate = value as ArpRate;
|
|
507
569
|
return done(`chords rate ${value}`);
|
|
570
|
+
case "strokes": {
|
|
571
|
+
// Case matters: D is a full down stroke, d a light one.
|
|
572
|
+
const grid = raw.slice(1).join("");
|
|
573
|
+
if (!strokeGrid(grid))
|
|
574
|
+
return bad(
|
|
575
|
+
`strokes takes ${STROKE_PATTERN_NAMES.join("|")} or a grid of D U d u x - .`,
|
|
576
|
+
);
|
|
577
|
+
settings.strokes = (STROKE_PATTERN_NAMES as readonly string[]).includes(
|
|
578
|
+
grid.toLowerCase(),
|
|
579
|
+
)
|
|
580
|
+
? grid.toLowerCase()
|
|
581
|
+
: grid;
|
|
582
|
+
settings.perform = "guitar";
|
|
583
|
+
return done(`chords strokes ${settings.strokes}`);
|
|
584
|
+
}
|
|
585
|
+
case "speed": {
|
|
586
|
+
const ms = parseStrokeSpeed(words.slice(1).join(""), bpm);
|
|
587
|
+
if (ms === undefined)
|
|
588
|
+
return bad("speed takes 0..200 ms (22, 22ms) or beats (1/32b)");
|
|
589
|
+
settings.speed = ms;
|
|
590
|
+
return done(`chords speed ${ms}ms`);
|
|
591
|
+
}
|
|
508
592
|
case "octaves": {
|
|
509
593
|
const n = Number(value);
|
|
510
594
|
if (!Number.isInteger(n) || n < 1 || n > 4)
|
|
@@ -526,7 +610,7 @@ export function applyChordsCommand(
|
|
|
526
610
|
return done(`chords style ${value}`);
|
|
527
611
|
default:
|
|
528
612
|
return bad(
|
|
529
|
-
"/chords auto|manual|off · voicing · spread · bass · perform · pattern · rate · octaves · sevenths · preset · style",
|
|
613
|
+
"/chords auto|manual|off · voicing · spread · bass · perform · pattern · strokes · speed · rate · octaves · sevenths · preset · style",
|
|
530
614
|
);
|
|
531
615
|
}
|
|
532
616
|
}
|
|
@@ -539,6 +623,8 @@ export function chordsSummary(settings: ChordSettings): string {
|
|
|
539
623
|
s.spread,
|
|
540
624
|
s.perform,
|
|
541
625
|
`pattern ${patternLabel(s.pattern)}`,
|
|
626
|
+
`strokes ${s.strokes}`,
|
|
627
|
+
`speed ${s.speed}ms`,
|
|
542
628
|
`rate ${s.rate}`,
|
|
543
629
|
`octaves ${s.octaves}`,
|
|
544
630
|
`bass ${s.bass}`,
|
|
@@ -603,6 +689,7 @@ export function chordPhrase(
|
|
|
603
689
|
seed: 1,
|
|
604
690
|
velocity: 0.8,
|
|
605
691
|
...(mode === "pattern" ? { pattern: settings.pattern } : {}),
|
|
692
|
+
...(mode === "guitar" ? guitarPerform(settings, score, track) : {}),
|
|
606
693
|
},
|
|
607
694
|
});
|
|
608
695
|
// With chords off a key plays one note: the root, in the chord's octave.
|
|
@@ -630,3 +717,17 @@ export function chordPhrase(
|
|
|
630
717
|
}
|
|
631
718
|
return notes;
|
|
632
719
|
}
|
|
720
|
+
|
|
721
|
+
/** Perform options for the guitar mode: strokes, speed, tempo and fretting. */
|
|
722
|
+
export function guitarPerform(
|
|
723
|
+
settings: Pick<ChordSettings, "strokes" | "speed">,
|
|
724
|
+
score: Pick<TrackScore, "tempoBpm">,
|
|
725
|
+
track: Pick<Track, "guitar"> | undefined,
|
|
726
|
+
): Partial<PerformOptions> {
|
|
727
|
+
return {
|
|
728
|
+
strokes: settings.strokes,
|
|
729
|
+
speed: settings.speed / 1000,
|
|
730
|
+
tempo: score.tempoBpm,
|
|
731
|
+
...(track?.guitar ? { guitar: track.guitar } : {}),
|
|
732
|
+
};
|
|
733
|
+
}
|
package/src/tui/play-mode.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* Shift sustain while held Tab sustain latch Esc leave the mode
|
|
13
13
|
* (while recording, sustain changes record as track pedal events)
|
|
14
14
|
* I scale degrees: the home row plays the key's scale (any tuning)
|
|
15
|
+
* [ / ] strum slower / faster (chords perform guitar, 5 ms steps)
|
|
15
16
|
*
|
|
16
17
|
* Terminals report key-down only, so a held key is synthesized from the
|
|
17
18
|
* keyboard's auto-repeat: a press sounds for the gate (one grid step by
|
package/src/tui/play-session.ts
CHANGED
|
@@ -59,12 +59,14 @@ import {
|
|
|
59
59
|
DEFAULT_STRUM,
|
|
60
60
|
perform,
|
|
61
61
|
type PerformMode,
|
|
62
|
+
type PerformOptions,
|
|
62
63
|
type PerformedNote,
|
|
63
64
|
} from "../../core/chords.ts";
|
|
64
65
|
import {
|
|
65
66
|
ChordPad,
|
|
66
67
|
chordCapable,
|
|
67
68
|
defaultChordSettings,
|
|
69
|
+
guitarPerform,
|
|
68
70
|
songKey,
|
|
69
71
|
type ChordSettings,
|
|
70
72
|
type PlayedChord,
|
|
@@ -181,6 +183,8 @@ export type RecordedChord = Readonly<{
|
|
|
181
183
|
seed: number;
|
|
182
184
|
/** CHORD_PATTERNS name when `mode` is `pattern`. */
|
|
183
185
|
pattern?: string;
|
|
186
|
+
/** Guitar mode: strokes, speed, tempo, fretting and the chord root. */
|
|
187
|
+
guitar?: Partial<PerformOptions>;
|
|
184
188
|
}>;
|
|
185
189
|
|
|
186
190
|
/** A chord sounding live: its voices start and stop from `tick()`. */
|
|
@@ -643,16 +647,21 @@ export class PlaySession {
|
|
|
643
647
|
// The full pass runs on a worker, in key order, so a strummed chord or a
|
|
644
648
|
// fast run never stalls key handling behind an oversampled render.
|
|
645
649
|
if (pcm.partial)
|
|
646
|
-
void this.fullRenderer
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
this.windows.get(id) === false &&
|
|
651
|
-
engine.voicePosition?.(id) !== undefined
|
|
650
|
+
void this.fullRenderer
|
|
651
|
+
.full(
|
|
652
|
+
synth,
|
|
653
|
+
pcm.clock === undefined ? request : { ...request, clock: pcm.clock },
|
|
652
654
|
)
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
655
|
+
.then((full) => {
|
|
656
|
+
// Not after note-off: a swap would cancel the release fade.
|
|
657
|
+
if (
|
|
658
|
+
full &&
|
|
659
|
+
this.windows.get(id) === false &&
|
|
660
|
+
engine.voicePosition?.(id) !== undefined
|
|
661
|
+
)
|
|
662
|
+
engine.noteOn(id, full);
|
|
663
|
+
this.windows.delete(id);
|
|
664
|
+
});
|
|
656
665
|
}
|
|
657
666
|
|
|
658
667
|
private release(ids: readonly number[], atMs: number): void {
|
|
@@ -729,6 +738,20 @@ export class PlaySession {
|
|
|
729
738
|
octaves: settings.octaves,
|
|
730
739
|
seed,
|
|
731
740
|
...(mode === "pattern" ? { pattern: settings.pattern } : {}),
|
|
741
|
+
...(mode === "guitar" ? { guitar: this.guitarOptions(played) } : {}),
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/** Guitar-mode perform options for a played chord (strokes, speed, frets). */
|
|
746
|
+
private guitarOptions(played: PlayedChord): Partial<PerformOptions> {
|
|
747
|
+
return {
|
|
748
|
+
...guitarPerform(
|
|
749
|
+
this.chords.settings,
|
|
750
|
+
{ tempoBpm: this.currentBpm() },
|
|
751
|
+
this.trackData(),
|
|
752
|
+
),
|
|
753
|
+
root: played.chord.root,
|
|
754
|
+
...(played.chord.bass !== undefined ? { slash: played.chord.bass } : {}),
|
|
732
755
|
};
|
|
733
756
|
}
|
|
734
757
|
|
|
@@ -749,6 +772,7 @@ export class PlaySession {
|
|
|
749
772
|
seed: note.id,
|
|
750
773
|
pattern: settings.pattern,
|
|
751
774
|
velocity: 1,
|
|
775
|
+
...(mode === "guitar" ? this.guitarOptions(played) : {}),
|
|
752
776
|
});
|
|
753
777
|
if (played.bass !== undefined)
|
|
754
778
|
plan.push({
|
|
@@ -1213,6 +1237,7 @@ export function chordNotes(
|
|
|
1213
1237
|
seed: chord.seed,
|
|
1214
1238
|
...(chord.pattern !== undefined ? { pattern: chord.pattern } : {}),
|
|
1215
1239
|
velocity: 1,
|
|
1240
|
+
...chord.guitar,
|
|
1216
1241
|
}).map((note) => ({
|
|
1217
1242
|
pitch: note.pitch,
|
|
1218
1243
|
velocity: scaleVelocity(press.velocity, note.velocity),
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ctrl-k menu rows for the wind engine (0.6.1): the "Winds and brass" group
|
|
3
|
+
* under Sound › browse sounds, and the WIND_PARAMS rows under Sound ›
|
|
4
|
+
* Parameters on a wind-engine track. Every row runs a `wind …` command.
|
|
5
|
+
*/
|
|
6
|
+
import type { EnumParam, NumberParam } from "../../core/params.ts";
|
|
7
|
+
import type { Track } from "../../core/score.ts";
|
|
8
|
+
import {
|
|
9
|
+
DEFAULT_WIND_PRESET,
|
|
10
|
+
WIND_GROUPS,
|
|
11
|
+
WIND_INSTRUMENT,
|
|
12
|
+
WIND_PARAMS,
|
|
13
|
+
WIND_PRESETS,
|
|
14
|
+
WIND_PRESET_NAMES,
|
|
15
|
+
WIND_SIMPLE,
|
|
16
|
+
windSettings,
|
|
17
|
+
type WindPresetName,
|
|
18
|
+
} from "../../core/winds.ts";
|
|
19
|
+
import { num, specStep, type MenuNode } from "./menu.ts";
|
|
20
|
+
|
|
21
|
+
function presetRow(name: WindPresetName): MenuNode {
|
|
22
|
+
return {
|
|
23
|
+
kind: "action",
|
|
24
|
+
label: `${name.padEnd(13)} ${WIND_PRESETS[name].doc}`,
|
|
25
|
+
command: `wind ${name}`,
|
|
26
|
+
help: WIND_PRESETS[name].styles,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Sound › browse sounds › Winds and brass: flutes, reeds, brass. */
|
|
31
|
+
export function windsMenu(): MenuNode {
|
|
32
|
+
return {
|
|
33
|
+
kind: "menu",
|
|
34
|
+
id: "winds",
|
|
35
|
+
label: "Winds and brass",
|
|
36
|
+
detail: `${WIND_PRESET_NAMES.length} blown · flute, sax, trumpet …`,
|
|
37
|
+
help: "breath-driven waveguides: flutes, reeds and brass; wind players 4 for a section",
|
|
38
|
+
build: () =>
|
|
39
|
+
WIND_GROUPS.map((group): MenuNode => ({
|
|
40
|
+
kind: "menu",
|
|
41
|
+
id: `winds:${group.label.toLowerCase()}`,
|
|
42
|
+
label: group.label,
|
|
43
|
+
detail: group.presets.slice(0, 3).join(", ") + " …",
|
|
44
|
+
help: `${group.label.toLowerCase()} on the wind engine`,
|
|
45
|
+
build: () => group.presets.map(presetRow),
|
|
46
|
+
})),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function numberRow(
|
|
51
|
+
name: string,
|
|
52
|
+
spec: NumberParam,
|
|
53
|
+
value: number,
|
|
54
|
+
fallback: number,
|
|
55
|
+
): MenuNode {
|
|
56
|
+
return {
|
|
57
|
+
kind: "number",
|
|
58
|
+
label: name,
|
|
59
|
+
value,
|
|
60
|
+
min: spec.min,
|
|
61
|
+
max: spec.max,
|
|
62
|
+
step: specStep(spec),
|
|
63
|
+
format: (v) => (spec.unit ? `${num(v)} ${spec.unit}` : num(v)),
|
|
64
|
+
command: (v) => `wind ${name} ${num(v)}`,
|
|
65
|
+
reset: `wind ${name} off`,
|
|
66
|
+
help: `${spec.doc} · preset ${num(fallback)} · x resets`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Sound › Parameters rows on a wind-engine track. */
|
|
71
|
+
export function windParameterNodes(track: Track): MenuNode[] {
|
|
72
|
+
if (track.instrument !== WIND_INSTRUMENT || !track.wind) return [];
|
|
73
|
+
const wind = track.wind;
|
|
74
|
+
const preset = WIND_PRESETS[wind.preset ?? DEFAULT_WIND_PRESET].settings;
|
|
75
|
+
const resolved = windSettings(wind) as Readonly<
|
|
76
|
+
Record<string, number | string | boolean>
|
|
77
|
+
>;
|
|
78
|
+
const fallback = preset as Readonly<
|
|
79
|
+
Record<string, number | string | boolean>
|
|
80
|
+
>;
|
|
81
|
+
const row = (name: string): MenuNode | undefined => {
|
|
82
|
+
const spec = WIND_PARAMS[name]!;
|
|
83
|
+
const own = (wind as Record<string, unknown>)[name];
|
|
84
|
+
if (spec.kind === "enum")
|
|
85
|
+
return {
|
|
86
|
+
kind: "choice",
|
|
87
|
+
label: name,
|
|
88
|
+
value: own === undefined ? "preset" : String(own),
|
|
89
|
+
options: ["preset", ...(spec as EnumParam).values],
|
|
90
|
+
command: (option) =>
|
|
91
|
+
option === "preset" ? `wind ${name} unset` : `wind ${name} ${option}`,
|
|
92
|
+
help: `${spec.doc} · preset ${String(fallback[name])}`,
|
|
93
|
+
};
|
|
94
|
+
if (spec.kind === "boolean")
|
|
95
|
+
return {
|
|
96
|
+
kind: "choice",
|
|
97
|
+
label: name,
|
|
98
|
+
value: own === undefined ? "preset" : own ? "on" : "off",
|
|
99
|
+
options: ["preset", "on", "off"],
|
|
100
|
+
command: (option) =>
|
|
101
|
+
option === "preset" ? `wind ${name} unset` : `wind ${name} ${option}`,
|
|
102
|
+
help: `${spec.doc} · preset ${fallback[name] ? "on" : "off"}`,
|
|
103
|
+
};
|
|
104
|
+
return numberRow(
|
|
105
|
+
name,
|
|
106
|
+
spec,
|
|
107
|
+
Number(resolved[name]),
|
|
108
|
+
Number(fallback[name]),
|
|
109
|
+
);
|
|
110
|
+
};
|
|
111
|
+
const nodes: MenuNode[] = [
|
|
112
|
+
{
|
|
113
|
+
kind: "choice",
|
|
114
|
+
label: "preset",
|
|
115
|
+
value: wind.preset ?? DEFAULT_WIND_PRESET,
|
|
116
|
+
options: WIND_PRESET_NAMES,
|
|
117
|
+
command: (option) => `wind preset ${option}`,
|
|
118
|
+
help: "the instrument: bore, exciter, range and mute",
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
for (const name of WIND_SIMPLE) {
|
|
122
|
+
const node = row(name);
|
|
123
|
+
if (node) nodes.push(node);
|
|
124
|
+
}
|
|
125
|
+
const rest = Object.keys(WIND_PARAMS).filter(
|
|
126
|
+
(name) => !(WIND_SIMPLE as readonly string[]).includes(name),
|
|
127
|
+
);
|
|
128
|
+
nodes.push({
|
|
129
|
+
kind: "menu",
|
|
130
|
+
id: "wind:advanced",
|
|
131
|
+
label: "advanced",
|
|
132
|
+
detail: rest.join(", "),
|
|
133
|
+
help: "every wind parameter",
|
|
134
|
+
build: () => rest.flatMap((name) => row(name) ?? []),
|
|
135
|
+
});
|
|
136
|
+
if (Object.keys(wind).some((key) => key !== "preset"))
|
|
137
|
+
nodes.push({
|
|
138
|
+
kind: "action",
|
|
139
|
+
label: "reset to preset",
|
|
140
|
+
command: "wind reset",
|
|
141
|
+
help: "clear overrides, keep the preset",
|
|
142
|
+
});
|
|
143
|
+
return nodes;
|
|
144
|
+
}
|