@hraness/dawg 0.4.0 → 0.4.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 +16 -0
- package/DAWG.md +7 -4
- package/README.md +4 -4
- package/package.json +1 -1
- package/src/audio/effects/chain.ts +12 -2
- package/src/audio/effects/convolution.ts +43 -5
- package/src/audio/effects/space.ts +105 -48
- package/src/audio/engine.ts +85 -0
- package/src/audio/preview.ts +13 -2
- package/src/audio/wav.ts +148 -34
- package/src/main.ts +117 -14
- package/src/tui/audition.ts +21 -1
- package/src/tui/euclid.ts +50 -6
- package/src/tui/menu.ts +21 -2
- package/src/tui/play-chords.ts +93 -1
- package/tui/grammar.ts +24 -19
package/src/tui/menu.ts
CHANGED
|
@@ -119,6 +119,13 @@ export type MenuAudition = Readonly<{
|
|
|
119
119
|
hint: string;
|
|
120
120
|
/** `♪ solo · B staged 2 · 42 ms`, shown in the title. */
|
|
121
121
|
status?: string | undefined;
|
|
122
|
+
/**
|
|
123
|
+
* Which commands stage while auditioning (default `isStageable`); the
|
|
124
|
+
* chord settings screen also stages `/chords …` and `key …`.
|
|
125
|
+
*/
|
|
126
|
+
stageable?: (command: string) => boolean;
|
|
127
|
+
/** The committed chord settings, for `staged ← committed` on Chords rows. */
|
|
128
|
+
committedChords?: ChordSettings;
|
|
122
129
|
}>;
|
|
123
130
|
|
|
124
131
|
type NumberField = Readonly<{
|
|
@@ -1543,6 +1550,8 @@ function transportNodes(context: MenuContext): MenuNode[] {
|
|
|
1543
1550
|
|
|
1544
1551
|
type Frame = {
|
|
1545
1552
|
title: string;
|
|
1553
|
+
/** The submenu's node id (`chords`), for `EditMenu.section`. */
|
|
1554
|
+
id?: string;
|
|
1546
1555
|
build: (context: MenuContext) => MenuNode[];
|
|
1547
1556
|
index: number;
|
|
1548
1557
|
query: string;
|
|
@@ -1573,6 +1582,7 @@ function childFrame(
|
|
|
1573
1582
|
): Frame {
|
|
1574
1583
|
return {
|
|
1575
1584
|
title: node.label,
|
|
1585
|
+
id: node.id,
|
|
1576
1586
|
index: 0,
|
|
1577
1587
|
query: "",
|
|
1578
1588
|
hover: `menu:${node.id}`,
|
|
@@ -1603,6 +1613,11 @@ export class EditMenu {
|
|
|
1603
1613
|
return this.filtering || this.entry !== undefined;
|
|
1604
1614
|
}
|
|
1605
1615
|
|
|
1616
|
+
/** The root section open now (`chords`), or undefined at the root. */
|
|
1617
|
+
get section(): string | undefined {
|
|
1618
|
+
return this.stack[1]?.id;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1606
1621
|
/** Open at the root, or at a section id (`effects`, `automation`). */
|
|
1607
1622
|
show(context: MenuContext, section?: string): void {
|
|
1608
1623
|
this.stack = [{ title: "menu", build: rootNodes, index: 0, query: "" }];
|
|
@@ -1777,7 +1792,7 @@ export class EditMenu {
|
|
|
1777
1792
|
if (node.kind === "action")
|
|
1778
1793
|
return frame.hover &&
|
|
1779
1794
|
context.audition?.looping &&
|
|
1780
|
-
isStageable(node.command)
|
|
1795
|
+
(context.audition.stageable ?? isStageable)(node.command)
|
|
1781
1796
|
? { type: "choose", command: node.command, key: frame.hover }
|
|
1782
1797
|
: { type: "run", command: node.command };
|
|
1783
1798
|
if (node.kind === "choice") {
|
|
@@ -1822,7 +1837,8 @@ export class EditMenu {
|
|
|
1822
1837
|
private hovered(frame: Frame, context: MenuContext): MenuResult {
|
|
1823
1838
|
if (!frame.hover || !context.audition?.looping) return { type: "handled" };
|
|
1824
1839
|
const node = this.selected(context);
|
|
1825
|
-
|
|
1840
|
+
const stageable = context.audition?.stageable ?? isStageable;
|
|
1841
|
+
if (node?.kind !== "action" || !stageable(node.command))
|
|
1826
1842
|
return { type: "handled" };
|
|
1827
1843
|
frame.hovering = true;
|
|
1828
1844
|
return { type: "hover", command: node.command, key: frame.hover };
|
|
@@ -1932,6 +1948,9 @@ export class EditMenu {
|
|
|
1932
1948
|
for (const node of frame.build({
|
|
1933
1949
|
...context,
|
|
1934
1950
|
score: audition.committed,
|
|
1951
|
+
...(audition.committedChords
|
|
1952
|
+
? { chords: audition.committedChords }
|
|
1953
|
+
: {}),
|
|
1935
1954
|
}))
|
|
1936
1955
|
before.set(node.label, valueText(node));
|
|
1937
1956
|
const items = nodes.map((node, at) => {
|
package/src/tui/play-chords.ts
CHANGED
|
@@ -25,6 +25,11 @@
|
|
|
25
25
|
import {
|
|
26
26
|
BASS_MODES,
|
|
27
27
|
CHORD_PATTERNS,
|
|
28
|
+
DEFAULT_ARP_RATE,
|
|
29
|
+
DEFAULT_STRUM,
|
|
30
|
+
generateProgression,
|
|
31
|
+
presetChords,
|
|
32
|
+
renderProgression,
|
|
28
33
|
CHORD_TYPES,
|
|
29
34
|
EXTENSIONS,
|
|
30
35
|
MAX_VOICING_STEP,
|
|
@@ -56,7 +61,12 @@ import {
|
|
|
56
61
|
type ProgressionStyle,
|
|
57
62
|
type Spread,
|
|
58
63
|
} from "../../core/chords.ts";
|
|
59
|
-
import {
|
|
64
|
+
import {
|
|
65
|
+
isSamplerInstrument,
|
|
66
|
+
type NoteInput,
|
|
67
|
+
type Track,
|
|
68
|
+
type TrackScore,
|
|
69
|
+
} from "../../core/score.ts";
|
|
60
70
|
|
|
61
71
|
export const CHORD_MODES = ["auto", "manual", "off"] as const;
|
|
62
72
|
export type ChordMode = (typeof CHORD_MODES)[number];
|
|
@@ -536,3 +546,85 @@ export function chordsSummary(settings: ChordSettings): string {
|
|
|
536
546
|
`style ${s.style}`,
|
|
537
547
|
].join(" · ");
|
|
538
548
|
}
|
|
549
|
+
|
|
550
|
+
/** Chords in the chord settings screen's audition phrase. */
|
|
551
|
+
const PHRASE_CHORDS = 4;
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* What the chord settings sound like, for the audition loop: the chosen
|
|
555
|
+
* progression (the preset, or four chords of the style's seeded walk) in the
|
|
556
|
+
* song key, voiced and performed with `settings` (voicing, spread, sevenths,
|
|
557
|
+
* perform mode, pattern, arp rate and octaves, bass mode), one chord per bar
|
|
558
|
+
* or two per bar in a one-bar loop. With chords `off` it plays each chord's
|
|
559
|
+
* root alone, as play mode would. Deterministic: the same settings and key
|
|
560
|
+
* always give the same notes.
|
|
561
|
+
*/
|
|
562
|
+
export function chordPhrase(
|
|
563
|
+
settings: ChordSettings,
|
|
564
|
+
score: TrackScore,
|
|
565
|
+
track: Track,
|
|
566
|
+
bars: number,
|
|
567
|
+
): NoteInput[] {
|
|
568
|
+
const key = parseKey(score.key) ?? { tonic: 0, mode: "major" as const };
|
|
569
|
+
const preset =
|
|
570
|
+
settings.preset === "none" ? undefined : findPreset(settings.preset);
|
|
571
|
+
const progression = preset
|
|
572
|
+
? presetChords(key, preset)
|
|
573
|
+
: generateProgression({
|
|
574
|
+
key,
|
|
575
|
+
length: PHRASE_CHORDS,
|
|
576
|
+
style: settings.style,
|
|
577
|
+
seed: 1,
|
|
578
|
+
sevenths: settings.sevenths,
|
|
579
|
+
});
|
|
580
|
+
const tpb = score.ticksPerBeat;
|
|
581
|
+
const totalBeats = Math.max(1, bars) * score.beatsPerBar;
|
|
582
|
+
const count = Math.max(2, Math.max(1, bars));
|
|
583
|
+
const span = totalBeats / count;
|
|
584
|
+
const chords = Array.from(
|
|
585
|
+
{ length: count },
|
|
586
|
+
(_, index) => progression[index % progression.length]!,
|
|
587
|
+
);
|
|
588
|
+
const mode = settings.perform;
|
|
589
|
+
const rendered = renderProgression({
|
|
590
|
+
key,
|
|
591
|
+
chords,
|
|
592
|
+
beatsPerChord: span,
|
|
593
|
+
inversion: settings.inversion,
|
|
594
|
+
spread: settings.spread,
|
|
595
|
+
bassMode: settings.mode === "off" ? "solo" : settings.bass,
|
|
596
|
+
perform: {
|
|
597
|
+
mode,
|
|
598
|
+
rate:
|
|
599
|
+
settings.rate === "grid" ? DEFAULT_ARP_RATE : RATE_BEATS[settings.rate],
|
|
600
|
+
octaves: settings.octaves,
|
|
601
|
+
strum: mode === "harp" ? DEFAULT_STRUM * 2 : DEFAULT_STRUM,
|
|
602
|
+
seed: 1,
|
|
603
|
+
velocity: 0.8,
|
|
604
|
+
...(mode === "pattern" ? { pattern: settings.pattern } : {}),
|
|
605
|
+
},
|
|
606
|
+
});
|
|
607
|
+
// With chords off a key plays one note: the root, in the chord's octave.
|
|
608
|
+
const performed =
|
|
609
|
+
settings.mode === "off"
|
|
610
|
+
? rendered.bass.map((note) => ({ ...note, pitch: note.pitch + 24 }))
|
|
611
|
+
: [...rendered.notes, ...rendered.bass];
|
|
612
|
+
const end = totalBeats * tpb;
|
|
613
|
+
const notes: NoteInput[] = [];
|
|
614
|
+
for (const note of performed) {
|
|
615
|
+
const startTick = Math.round(note.start * tpb);
|
|
616
|
+
if (startTick >= end) continue;
|
|
617
|
+
notes.push({
|
|
618
|
+
id: `chords-${notes.length + 1}`,
|
|
619
|
+
trackId: track.id,
|
|
620
|
+
startTick,
|
|
621
|
+
durationTicks: Math.max(
|
|
622
|
+
1,
|
|
623
|
+
Math.min(Math.round(note.length * tpb), end - startTick),
|
|
624
|
+
),
|
|
625
|
+
pitch: note.pitch,
|
|
626
|
+
velocity: note.velocity,
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
return notes;
|
|
630
|
+
}
|
package/tui/grammar.ts
CHANGED
|
@@ -80,7 +80,7 @@ export const HINTS = {
|
|
|
80
80
|
action: " ↑↓ move · enter apply · / filter · esc back · ? keys ",
|
|
81
81
|
typing: " type a value · enter apply · esc cancel ",
|
|
82
82
|
euclid:
|
|
83
|
-
" ↑↓ voice · ←→ adjust · tab field · space
|
|
83
|
+
" ↑↓ voice · ←→ adjust · tab field · space loop · x off · esc back · ? keys ",
|
|
84
84
|
text: " ↑↓ scroll · pgup pgdn page · esc back · ? keys ",
|
|
85
85
|
log: " ↑↓ scroll · / filter · esc back · ? keys ",
|
|
86
86
|
keys: " any key closes ",
|
|
@@ -88,6 +88,27 @@ export const HINTS = {
|
|
|
88
88
|
|
|
89
89
|
// ── the `?` panel ─────────────────────────────────────────────────────
|
|
90
90
|
|
|
91
|
+
/** The audition loop's keys, shared by the menu and the rhythm editor. */
|
|
92
|
+
const AUDITIONING: readonly KeySection[] = [
|
|
93
|
+
{
|
|
94
|
+
title: "auditioning",
|
|
95
|
+
rows: [
|
|
96
|
+
["space", "loop the focused track · again stops"],
|
|
97
|
+
["c", "solo ↔ in context (the whole mix)"],
|
|
98
|
+
["a", "A/B: committed ↔ staged"],
|
|
99
|
+
["enter", "keep staged changes (one undo step)"],
|
|
100
|
+
["esc", "revert staged changes"],
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
title: "every change",
|
|
105
|
+
rows: [
|
|
106
|
+
["", "while looping, stages; otherwise runs the command shown"],
|
|
107
|
+
["ctrl-z", "undoes it"],
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
];
|
|
111
|
+
|
|
91
112
|
const LIST: readonly KeyRow[] = [
|
|
92
113
|
["↑ ↓ j k", "move"],
|
|
93
114
|
["pgup pgdn", "page"],
|
|
@@ -166,23 +187,7 @@ export const KEYS = {
|
|
|
166
187
|
["x delete", "reset to default (deletes an automation point)"],
|
|
167
188
|
],
|
|
168
189
|
},
|
|
169
|
-
|
|
170
|
-
title: "auditioning",
|
|
171
|
-
rows: [
|
|
172
|
-
["space", "loop the focused track · again stops"],
|
|
173
|
-
["c", "solo ↔ in context (the whole mix)"],
|
|
174
|
-
["a", "A/B: committed ↔ staged"],
|
|
175
|
-
["enter", "keep staged changes (one undo step)"],
|
|
176
|
-
["esc", "revert staged changes"],
|
|
177
|
-
],
|
|
178
|
-
},
|
|
179
|
-
{
|
|
180
|
-
title: "every change",
|
|
181
|
-
rows: [
|
|
182
|
-
["", "while looping, stages; otherwise runs the command shown"],
|
|
183
|
-
["ctrl-z", "undoes it"],
|
|
184
|
-
],
|
|
185
|
-
},
|
|
190
|
+
...AUDITIONING,
|
|
186
191
|
],
|
|
187
192
|
euclid: [
|
|
188
193
|
{
|
|
@@ -193,12 +198,12 @@ export const KEYS = {
|
|
|
193
198
|
["tab shift-tab ] [", "next / previous field"],
|
|
194
199
|
["0-9", "type a value, enter applies"],
|
|
195
200
|
["enter", "add a row (or type a value)"],
|
|
196
|
-
["space", "hear the voice"],
|
|
197
201
|
["x", "turn the row off"],
|
|
198
202
|
["f", "freeze into plain hits"],
|
|
199
203
|
["esc", "back"],
|
|
200
204
|
],
|
|
201
205
|
},
|
|
206
|
+
...AUDITIONING,
|
|
202
207
|
],
|
|
203
208
|
text: [
|
|
204
209
|
{
|