@hraness/dawg 0.6.1 → 0.8.0
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 +118 -0
- package/DAWG.md +454 -140
- package/README.md +30 -26
- package/core/autotune.ts +1119 -0
- package/core/chords.ts +271 -23
- package/core/clips.ts +499 -0
- package/core/diff.ts +184 -104
- package/core/expression.ts +15 -0
- package/core/fx.ts +99 -29
- package/core/ids.ts +52 -0
- package/core/instruments.ts +19 -0
- package/core/keys.ts +3 -3
- package/core/loop.ts +8 -0
- package/core/lyrics.ts +297 -0
- package/core/master.ts +3 -3
- package/core/range.ts +581 -0
- package/core/resonators.ts +16 -2
- package/core/routing.ts +165 -0
- package/core/score.ts +955 -14
- package/core/sdk/eval-child.ts +7 -2
- package/core/sdk/eval.ts +35 -6
- package/core/sdk/print.ts +287 -5
- package/core/sdk/sync-lyrics.ts +49 -0
- package/core/sdk/v1.ts +2132 -46
- package/core/sections.ts +480 -35
- package/core/sing.ts +815 -0
- package/core/style-provenance.ts +80 -0
- package/core/styles/africa-mena-southasia.ts +2893 -0
- package/core/styles/americas.ts +3810 -0
- package/core/styles/art.ts +4993 -0
- package/core/styles/base.ts +123 -0
- package/core/styles/cycles.ts +106 -0
- package/core/styles/electronic.ts +2723 -0
- package/core/styles/europe-asia-pacific.ts +2838 -0
- package/core/styles/excerpt.ts +29 -0
- package/core/styles/gamelan.ts +283 -0
- package/core/styles/generate.ts +2199 -0
- package/core/styles/index.ts +515 -0
- package/core/styles/parts.ts +106 -0
- package/core/styles/pop.ts +3189 -0
- package/core/styles/rock.ts +2993 -0
- package/core/styles/roots.ts +4175 -0
- package/core/styles/schema.ts +429 -0
- package/core/styles/taxonomy.ts +940 -0
- package/core/styles/validate.ts +528 -0
- package/core/tempo.ts +32 -2
- package/core/tuning.ts +19 -3
- package/core/vocoder.ts +524 -0
- package/guides/agent.md +29 -0
- package/guides/arrange.md +31 -0
- package/guides/audio.md +31 -0
- package/guides/audition.md +20 -14
- package/guides/automation.md +12 -7
- package/guides/chords.md +15 -15
- package/guides/effects.md +17 -16
- package/guides/faders.md +20 -16
- package/guides/files.md +13 -9
- package/guides/getting-started.md +15 -11
- package/guides/keys.md +19 -15
- package/guides/media.md +17 -12
- package/guides/mix.md +15 -7
- package/guides/music.md +23 -8
- package/guides/notes.md +15 -9
- package/guides/panes.md +32 -0
- package/guides/performance.md +15 -13
- package/guides/play.md +21 -13
- package/guides/project.md +26 -8
- package/guides/providers.md +20 -14
- package/guides/resample.md +15 -9
- package/guides/rhythm.md +18 -13
- package/guides/sessions.md +17 -7
- package/guides/show-me.md +31 -0
- package/guides/sound.md +25 -9
- package/guides/sounds.md +15 -13
- package/guides/styles.md +31 -0
- package/guides/tape.md +32 -0
- package/guides/tempo.md +15 -10
- package/guides/tracks.md +15 -10
- package/guides/tuning.md +32 -0
- package/guides/voice.md +31 -0
- package/guides/web-search.md +18 -8
- package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
- package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
- package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
- package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
- package/native/prebuilt/manifest.json +21 -0
- package/package.json +6 -2
- package/src/agent/agent.ts +130 -18
- package/src/agent/calibration-tools.ts +53 -0
- package/src/agent/clip-tools.ts +453 -0
- package/src/agent/command-agent.ts +378 -0
- package/src/agent/drum-tools.ts +2 -2
- package/src/agent/expression-tools.ts +1 -1
- package/src/agent/gateway.ts +246 -60
- package/src/agent/models.ts +53 -12
- package/src/agent/ops.ts +27 -1
- package/src/agent/pack-tools.ts +1 -1
- package/src/agent/planner.ts +25 -0
- package/src/agent/portable-schema.ts +80 -0
- package/src/agent/preview-tool.ts +4 -1
- package/src/agent/provider.ts +22 -8
- package/src/agent/range-tools.ts +216 -0
- package/src/agent/rhythm-tools.ts +1 -1
- package/src/agent/section-tools.ts +1 -1
- package/src/agent/show-me.ts +497 -0
- package/src/agent/steer.ts +15 -0
- package/src/agent/style-tools.ts +217 -0
- package/src/agent/tool-error.ts +12 -0
- package/src/agent/tools.ts +110 -23
- package/src/agent/usage.ts +2 -2
- package/src/agent/voice-tools.ts +925 -0
- package/src/agent/xcb-agent.ts +13 -10
- package/src/argv.ts +38 -0
- package/src/audio/analysis.ts +253 -0
- package/src/audio/arrange.ts +49 -5
- package/src/audio/audio-command.ts +218 -0
- package/src/audio/autotune-engine.ts +101 -0
- package/src/audio/autotune.ts +640 -0
- package/src/audio/clips.ts +240 -0
- package/src/audio/devices.ts +264 -0
- package/src/audio/doctor.ts +157 -0
- package/src/audio/dsp/bandbank.ts +138 -0
- package/src/audio/dsp/envelope.ts +10 -0
- package/src/audio/dsp/follow.ts +120 -0
- package/src/audio/dsp/formant.ts +427 -0
- package/src/audio/dsp/glottal.ts +243 -0
- package/src/audio/dsp/interp.ts +7 -2
- package/src/audio/dsp/lpc.ts +50 -0
- package/src/audio/dsp/periodicity.ts +59 -0
- package/src/audio/dsp/pitch.ts +995 -0
- package/src/audio/dsp/psola.ts +199 -0
- package/src/audio/effects/chain.ts +3 -1
- package/src/audio/effects/common.ts +43 -0
- package/src/audio/effects/convolution.ts +7 -4
- package/src/audio/effects/filter.ts +48 -69
- package/src/audio/effects/formant.ts +263 -0
- package/src/audio/engine.ts +360 -40
- package/src/audio/fit.ts +35 -3
- package/src/audio/instrument-check.ts +59 -43
- package/src/audio/instruments.ts +4 -0
- package/src/audio/keys/calibration.ts +56 -0
- package/src/audio/keys/electric.ts +8 -1
- package/src/audio/keys/engine.ts +13 -1
- package/src/audio/keys/piano.ts +22 -2
- package/src/audio/kits.ts +135 -6
- package/src/audio/live.ts +114 -20
- package/src/audio/native.ts +615 -0
- package/src/audio/preview.ts +30 -2
- package/src/audio/render-worker.ts +2 -0
- package/src/audio/renderer.ts +2 -0
- package/src/audio/resample.ts +2 -1
- package/src/audio/sampler.ts +85 -4
- package/src/audio/samples.ts +20 -2
- package/src/audio/sing/analysis.ts +193 -0
- package/src/audio/sing/engine.ts +949 -0
- package/src/audio/strings/bow.ts +48 -5
- package/src/audio/strings/engine.ts +8 -2
- package/src/audio/synth/oscillators.ts +31 -21
- package/src/audio/synth/voice.ts +34 -1
- package/src/audio/vocoder/bank.ts +314 -0
- package/src/audio/vocoder/carrier.ts +165 -0
- package/src/audio/vocoder/control.ts +68 -0
- package/src/audio/vocoder/detect.ts +50 -0
- package/src/audio/vocoder/index.ts +304 -0
- package/src/audio/vocoder/talkbox.ts +143 -0
- package/src/audio/wav.ts +500 -77
- package/src/audio/winds/engine.ts +5 -1
- package/src/audio/winds/trim.ts +28 -4
- package/src/audio/winds/trims1.ts +297 -0
- package/src/audio/winds/voice.ts +15 -2
- package/src/auth/cli.ts +38 -36
- package/src/auth/credentials.ts +30 -1
- package/src/auth/login.ts +15 -9
- package/src/auth/tui.ts +19 -10
- package/src/commands/arrange.ts +82 -32
- package/src/commands/autotune.ts +421 -0
- package/src/commands/calibration.ts +74 -0
- package/src/commands/clips.ts +887 -0
- package/src/commands/drums.ts +3 -2
- package/src/commands/edit.ts +11 -4
- package/src/commands/expression.ts +1 -1
- package/src/commands/formant.ts +221 -0
- package/src/commands/fx.ts +101 -32
- package/src/commands/grammar.ts +560 -0
- package/src/commands/help.ts +774 -366
- package/src/commands/history.ts +139 -10
- package/src/commands/keys.ts +25 -8
- package/src/commands/modal.ts +1 -1
- package/src/commands/music.ts +1 -1
- package/src/commands/nearest.ts +53 -0
- package/src/commands/pack.ts +9 -2
- package/src/commands/param-range.ts +56 -0
- package/src/commands/parses.ts +135 -0
- package/src/commands/progression.ts +170 -0
- package/src/commands/range.ts +763 -0
- package/src/commands/resample.ts +13 -10
- package/src/commands/rhythm.ts +3 -0
- package/src/commands/rig.ts +3 -24
- package/src/commands/sample.ts +11 -1
- package/src/commands/sing.ts +474 -0
- package/src/commands/strum.ts +13 -1
- package/src/commands/style.ts +415 -0
- package/src/commands/time.ts +6 -3
- package/src/commands/tuning.ts +3 -3
- package/src/commands/vocal-pitch.ts +617 -0
- package/src/commands/vocal.ts +147 -0
- package/src/commands/vocoder.ts +627 -0
- package/src/commands/wind.ts +2 -2
- package/src/fs/durable.ts +50 -0
- package/src/identity/actor.ts +127 -0
- package/src/lang/glossary.ts +511 -0
- package/src/launch-args.ts +267 -0
- package/src/main.ts +2508 -308
- package/src/media/cli.ts +20 -3
- package/src/media/import.ts +3 -1
- package/src/project/check.ts +21 -2
- package/src/project/clip-pins.ts +72 -0
- package/src/project/init.ts +23 -8
- package/src/project/sync.ts +418 -86
- package/src/render.ts +20 -1
- package/src/session/client.ts +107 -3
- package/src/session/clipboard.ts +79 -0
- package/src/session/daemon.ts +199 -5
- package/src/session/live-host.ts +232 -0
- package/src/session/meta.ts +14 -0
- package/src/session/origin.ts +174 -0
- package/src/session/port.ts +109 -14
- package/src/session/presence.ts +34 -4
- package/src/session/protocol.ts +365 -7
- package/src/session/rebase.ts +63 -12
- package/src/session/receipt.ts +265 -0
- package/src/session/shared-live.ts +131 -0
- package/src/session/store.ts +126 -45
- package/src/tui/arrange-menu.ts +225 -23
- package/src/tui/audition.ts +1 -1
- package/src/tui/euclid.ts +113 -27
- package/src/tui/fader.ts +282 -42
- package/src/tui/granular-menu.ts +2 -4
- package/src/tui/knob-fields.ts +107 -0
- package/src/tui/knob-map.ts +198 -0
- package/src/tui/menu-clips.ts +297 -0
- package/src/tui/menu-time.ts +20 -13
- package/src/tui/menu-voice.ts +405 -0
- package/src/tui/menu.ts +987 -179
- package/src/tui/modal-menu.ts +6 -6
- package/src/tui/performance-menu.ts +5 -2
- package/src/tui/play-chords.ts +4 -2
- package/src/tui/play-mode.ts +15 -1
- package/src/tui/play-session.ts +243 -28
- package/src/tui/sing-menu.ts +278 -0
- package/src/tui/style-menu.ts +104 -0
- package/src/tui/tape-mode.ts +580 -0
- package/src/tui/tape-view.ts +263 -0
- package/src/tui/vocoder-menu.ts +244 -0
- package/src/tui/wind-menu.ts +3 -3
- package/src/version.ts +8 -0
- package/src/web/fetch.ts +115 -29
- package/tui/activity.ts +180 -9
- package/tui/app.ts +491 -84
- package/tui/clip-row.ts +132 -0
- package/tui/delight.ts +193 -0
- package/tui/drawer.ts +233 -27
- package/tui/frame-gate.ts +76 -0
- package/tui/grammar.ts +143 -65
- package/tui/guide.ts +50 -5
- package/tui/highway.ts +309 -30
- package/tui/hints.ts +197 -0
- package/tui/hits.ts +7 -1
- package/tui/input.ts +60 -9
- package/tui/keys.ts +1 -1
- package/tui/knobs.ts +268 -0
- package/tui/play-strip.ts +80 -14
- package/tui/prompt.ts +1 -1
- package/tui/screen.ts +151 -18
- package/tui/tape.ts +439 -0
- package/tui/text.ts +35 -2
- package/tui/theme.ts +46 -1
package/src/tui/modal-menu.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Ctrl-k menu rows for modal percussion (0.6): the "
|
|
3
|
-
* group under Sound ›
|
|
2
|
+
* Ctrl-k menu rows for modal percussion (0.6): the "mallets"
|
|
3
|
+
* group under Sound › instruments, and the MODAL_PARAMS rows under
|
|
4
4
|
* Sound › Parameters on a modal track. Every row runs a `modal …` command.
|
|
5
5
|
*/
|
|
6
6
|
import type { EnumParam, NumberParam } from "../../core/params.ts";
|
|
@@ -18,12 +18,12 @@ import {
|
|
|
18
18
|
import type { Track } from "../../core/score.ts";
|
|
19
19
|
import { num, specStep, type MenuNode } from "./menu.ts";
|
|
20
20
|
|
|
21
|
-
/** Sound ›
|
|
21
|
+
/** Sound › instruments › mallets: one row per preset. */
|
|
22
22
|
export function malletsMenu(): MenuNode {
|
|
23
23
|
return {
|
|
24
24
|
kind: "menu",
|
|
25
25
|
id: "mallets",
|
|
26
|
-
label: "
|
|
26
|
+
label: "mallets",
|
|
27
27
|
detail: `${MODAL_PRESET_NAMES.length} modal presets · marimba, vibes, gong …`,
|
|
28
28
|
help: "struck bars, bells and bowls on the modal resonator engine",
|
|
29
29
|
build: () => [
|
|
@@ -47,14 +47,14 @@ function presetRow(name: ModalPresetName): MenuNode {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
|
-
*
|
|
50
|
+
* mallets › gamelan (f061-gamelan-winds): the Javanese and
|
|
51
51
|
* Balinese bronzes. `modal gamelan` points here.
|
|
52
52
|
*/
|
|
53
53
|
export function gamelanMenu(): MenuNode {
|
|
54
54
|
return {
|
|
55
55
|
kind: "menu",
|
|
56
56
|
id: "gamelan",
|
|
57
|
-
label: "
|
|
57
|
+
label: "gamelan",
|
|
58
58
|
detail: `${GAMELAN_PRESETS.length} bronzes · saron, gangsa (ombak), gong …`,
|
|
59
59
|
help: "Javanese and Balinese gamelan; pair two gangsa tracks with modal pair, tune with tuning slendro or pelog",
|
|
60
60
|
build: () => GAMELAN_PRESETS.map(presetRow),
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* runs the same prompt command a user could type (src/commands/expression.ts),
|
|
4
4
|
* so the menu, the prompt and the agent share one grammar.
|
|
5
5
|
*/
|
|
6
|
+
import { singVowelNodes } from "./sing-menu.ts";
|
|
6
7
|
import {
|
|
7
8
|
ARTICULATIONS,
|
|
8
9
|
DEFAULT_FIXED_VELOCITY,
|
|
@@ -187,7 +188,7 @@ export function performanceNodes(context: MenuContext): MenuNode[] {
|
|
|
187
188
|
: "pedal",
|
|
188
189
|
help: "bars replaces the lane with a re-pedal on each downbeat; in play mode press Tab (latch) or hold Shift while recording",
|
|
189
190
|
},
|
|
190
|
-
// keys-electric (0.6.1): the piano's other two pedals, on
|
|
191
|
+
// keys-electric (0.6.1): the piano's other two pedals, on modeled piano
|
|
191
192
|
// tracks (or wherever a lane is already set).
|
|
192
193
|
...pianoPedalNodes(track),
|
|
193
194
|
{
|
|
@@ -233,7 +234,7 @@ export function performanceNodes(context: MenuContext): MenuNode[] {
|
|
|
233
234
|
},
|
|
234
235
|
{
|
|
235
236
|
kind: "number",
|
|
236
|
-
label: "humanize
|
|
237
|
+
label: "humanize vel (%)",
|
|
237
238
|
value: humanize ? velocity : undefined,
|
|
238
239
|
min: 0,
|
|
239
240
|
max: EXPRESSION_LIMITS.maxHumanizePercent,
|
|
@@ -273,5 +274,7 @@ export function performanceNodes(context: MenuContext): MenuNode[] {
|
|
|
273
274
|
command: "humanize reseed",
|
|
274
275
|
help: "the same amounts with a different seeded performance",
|
|
275
276
|
});
|
|
277
|
+
// 0.7 sing: Vowels on a sing track.
|
|
278
|
+
nodes.push(...singVowelNodes(context));
|
|
276
279
|
return nodes;
|
|
277
280
|
}
|
package/src/tui/play-chords.ts
CHANGED
|
@@ -605,9 +605,11 @@ export function applyChordsCommand(
|
|
|
605
605
|
return done(`chords preset ${value}`);
|
|
606
606
|
case "style":
|
|
607
607
|
if (!(PROGRESSION_STYLES as readonly string[]).includes(value ?? ""))
|
|
608
|
-
return bad(
|
|
608
|
+
return bad(
|
|
609
|
+
`idiom takes ${PROGRESSION_STYLES.join("|")} · chords idiom jazz`,
|
|
610
|
+
);
|
|
609
611
|
settings.style = value as ProgressionStyle;
|
|
610
|
-
return done(`chords
|
|
612
|
+
return done(`chords idiom ${value}`);
|
|
611
613
|
default:
|
|
612
614
|
return bad(
|
|
613
615
|
"/chords auto|manual|off · voicing · spread · bass · perform · pattern · strokes · speed · rate · octaves · sevenths · preset · style",
|
package/src/tui/play-mode.ts
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
type Track,
|
|
33
33
|
} from "../../core/score.ts";
|
|
34
34
|
import type { TuningTable } from "../../core/tuning.ts";
|
|
35
|
+
import { DRUM_VOICES, isDrumInstrument } from "../../core/drums.ts";
|
|
35
36
|
import { pitchName } from "../../tui/highway.ts";
|
|
36
37
|
|
|
37
38
|
/** Semitone offset from the base C for each note key. */
|
|
@@ -112,10 +113,21 @@ export function defaultBaseFor(instrument: string | undefined): number {
|
|
|
112
113
|
*/
|
|
113
114
|
export type PlayLayout = Readonly<{
|
|
114
115
|
base: number;
|
|
115
|
-
/** Pitch → label (voice names on one-shot samplers). */
|
|
116
|
+
/** Pitch → label (voice names on one-shot samplers, drums on kits). */
|
|
116
117
|
labels: ReadonlyMap<number, string>;
|
|
118
|
+
/** A drum kit: keys name drums and the chord row stays hidden. */
|
|
119
|
+
drums?: boolean | undefined;
|
|
117
120
|
}>;
|
|
118
121
|
|
|
122
|
+
/**
|
|
123
|
+
* A kit's keys on the GM map from C2 (36), the same keys the agent's
|
|
124
|
+
* show-me gestures press: A kick, W rim, S snare, E clap, T hat, H tom,
|
|
125
|
+
* U open hat. Other keys still sound their nearest drum, unlabeled.
|
|
126
|
+
*/
|
|
127
|
+
export function drumKeyLabels(): ReadonlyMap<number, string> {
|
|
128
|
+
return new Map(DRUM_VOICES.map((info) => [info.pitch, info.label]));
|
|
129
|
+
}
|
|
130
|
+
|
|
119
131
|
export function playLayoutFor(track: Track | undefined): PlayLayout {
|
|
120
132
|
const labels = new Map<number, string>();
|
|
121
133
|
if (track && isSamplerInstrument(track.instrument) && track.sampler) {
|
|
@@ -130,6 +142,8 @@ export function playLayoutFor(track: Track | undefined): PlayLayout {
|
|
|
130
142
|
const lowest = roots.length > 0 ? Math.min(...roots) : 60;
|
|
131
143
|
return { base: clampBase(Math.floor(lowest / 12) * 12), labels };
|
|
132
144
|
}
|
|
145
|
+
if (track && isDrumInstrument(track.instrument))
|
|
146
|
+
return { base: 36, labels: drumKeyLabels(), drums: true };
|
|
133
147
|
// A plain instrument takes its range from the track's name (`bass`, `lead`).
|
|
134
148
|
const byInstrument = defaultBaseFor(track?.instrument);
|
|
135
149
|
const base =
|
package/src/tui/play-session.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
type PedalState,
|
|
30
30
|
} from "../../core/expression.ts";
|
|
31
31
|
import type { ClickBus } from "../audio/engine.ts";
|
|
32
|
+
import type { RemoteClick, RemoteNote } from "../session/shared-live.ts";
|
|
32
33
|
import {
|
|
33
34
|
DEFAULT_CLICK_VOLUME,
|
|
34
35
|
countInClicks,
|
|
@@ -50,13 +51,16 @@ import {
|
|
|
50
51
|
import {
|
|
51
52
|
LiveFullRenderer,
|
|
52
53
|
LiveSynth,
|
|
54
|
+
warmLive,
|
|
53
55
|
MAX_LIVE_NOTE_SECONDS,
|
|
54
56
|
type LiveNotePcm,
|
|
55
57
|
} from "../audio/live.ts";
|
|
56
58
|
import type { SampleBank } from "../audio/samples.ts";
|
|
59
|
+
import { loopedSection } from "../audio/arrange.ts";
|
|
57
60
|
import { fitting, onFitReady } from "../audio/fit.ts";
|
|
58
61
|
import {
|
|
59
62
|
DEFAULT_STRUM,
|
|
63
|
+
degreeOf,
|
|
60
64
|
perform,
|
|
61
65
|
type PerformMode,
|
|
62
66
|
type PerformOptions,
|
|
@@ -73,6 +77,7 @@ import {
|
|
|
73
77
|
} from "./play-chords.ts";
|
|
74
78
|
import type { PlayHeaderView, PlayStripKey } from "../../tui/play-strip.ts";
|
|
75
79
|
import { resolveTuning, type Tuning } from "../../core/tuning.ts";
|
|
80
|
+
import { singThroat } from "../../core/sing.ts";
|
|
76
81
|
import {
|
|
77
82
|
NOTE_KEYS,
|
|
78
83
|
PlayKeyboard,
|
|
@@ -91,6 +96,10 @@ export interface LiveEngine {
|
|
|
91
96
|
readonly sampleRate: number;
|
|
92
97
|
readonly canMonitor: boolean;
|
|
93
98
|
readonly leadMs: number;
|
|
99
|
+
/** The lead play mode pins (15 ms on the native sink, else 60 ms). */
|
|
100
|
+
readonly playLeadMs?: number;
|
|
101
|
+
/** How play mode sounds (`native sink`, `ffplay · no native sink`). */
|
|
102
|
+
readonly audioNote?: string;
|
|
94
103
|
monitor(on: boolean): Promise<void>;
|
|
95
104
|
setLeadMs(ms: number | undefined): void;
|
|
96
105
|
noteOn(id: number, note: LiveNotePcm): number;
|
|
@@ -98,6 +107,14 @@ export interface LiveEngine {
|
|
|
98
107
|
/** Frames of voice `id` mixed so far, or undefined once it has ended. */
|
|
99
108
|
voicePosition?(id: number): number | undefined;
|
|
100
109
|
setClick(click: ClickBus | undefined): void;
|
|
110
|
+
/**
|
|
111
|
+
* A shared engine (dawgd's, src/session/shared-live.ts) takes note events
|
|
112
|
+
* instead of PCM: it renders the voice itself, so one engine mixes every
|
|
113
|
+
* pane. Returns the monotonic ms the note is scheduled for.
|
|
114
|
+
*/
|
|
115
|
+
remoteNote?(id: number, note: RemoteNote): number;
|
|
116
|
+
/** A shared engine's click: on/volume and the count-in, not a closure. */
|
|
117
|
+
remoteClick?(click: RemoteClick): void;
|
|
101
118
|
}
|
|
102
119
|
|
|
103
120
|
export interface PlayHost {
|
|
@@ -124,6 +141,14 @@ export interface PlayHost {
|
|
|
124
141
|
startTransport(beat: number): Promise<void>;
|
|
125
142
|
stopTransport(): Promise<void>;
|
|
126
143
|
card(text: string, tone: "info" | "success" | "warning" | "error"): void;
|
|
144
|
+
/**
|
|
145
|
+
* Another pane recording on `trackId` (§12.4): its name (`pane B`), else
|
|
146
|
+
* undefined. A replace pass refuses while one is, since it would erase
|
|
147
|
+
* the other pane's notes.
|
|
148
|
+
*/
|
|
149
|
+
recordingElsewhere?(trackId: string): string | undefined;
|
|
150
|
+
/** A played key that is not recorded: the screen glows its lane. */
|
|
151
|
+
ghost?(pitch: number): void;
|
|
127
152
|
newNoteId(): string;
|
|
128
153
|
}
|
|
129
154
|
|
|
@@ -240,6 +265,8 @@ export class PlaySession {
|
|
|
240
265
|
private synth: LiveSynth | undefined;
|
|
241
266
|
/** Rig voices awaiting their full render: id → released since. */
|
|
242
267
|
private readonly windows = new Map<number, boolean>();
|
|
268
|
+
/** f07-sing: the sounding key on a throat track (one drone, mono). */
|
|
269
|
+
private throatId: number | undefined;
|
|
243
270
|
private readonly fullRenderer = new LiveFullRenderer();
|
|
244
271
|
private readonly pending = new Map<number, Pending>();
|
|
245
272
|
/** Sustain changes while recording (Tab latch or Shift), as pedal events. */
|
|
@@ -253,6 +280,10 @@ export class PlaySession {
|
|
|
253
280
|
/** Live pedal state as the playhead entered each loop bar (replace). */
|
|
254
281
|
private readonly barPedal = new Map<number, PedalState>();
|
|
255
282
|
private lastBar: number | undefined;
|
|
283
|
+
/** Loop recording: the pass (loop wrap) the playhead is in. */
|
|
284
|
+
private lastPass: number | undefined;
|
|
285
|
+
/** Passes committed this take (the header's `pass N`). */
|
|
286
|
+
private passCount = 0;
|
|
256
287
|
private countIn: CountIn | undefined;
|
|
257
288
|
private flushing: Promise<void> = Promise.resolve();
|
|
258
289
|
/** Most recent key-to-sound schedule, ms (lead before device latency). */
|
|
@@ -289,7 +320,8 @@ export class PlaySession {
|
|
|
289
320
|
settings.mode =
|
|
290
321
|
chordCapable(track) &&
|
|
291
322
|
twelveTet(this.host.score(), track) &&
|
|
292
|
-
(track?.glide === undefined || track.glide.mode === "poly")
|
|
323
|
+
(track?.glide === undefined || track.glide.mode === "poly") &&
|
|
324
|
+
!singThroat(track)
|
|
293
325
|
? "auto"
|
|
294
326
|
: "manual";
|
|
295
327
|
this.chords = new ChordPad(settings, () => this.host.score().key);
|
|
@@ -331,8 +363,12 @@ export class PlaySession {
|
|
|
331
363
|
this.active = true;
|
|
332
364
|
const engine = this.host.engine();
|
|
333
365
|
if (engine?.canMonitor) {
|
|
334
|
-
engine.setLeadMs(PLAY_LEAD_MS);
|
|
366
|
+
engine.setLeadMs(engine.playLeadMs ?? PLAY_LEAD_MS);
|
|
367
|
+
// The voice's tables and loops are built now, not on the first key.
|
|
368
|
+
warmLive(this.host.score(), this.trackId, engine.sampleRate);
|
|
335
369
|
await engine.monitor(true);
|
|
370
|
+
if (engine.audioNote)
|
|
371
|
+
this.status = `${engine.audioNote} · ${Math.round(engine.leadMs)} ms lead`;
|
|
336
372
|
} else this.status = "no audio · keys still record";
|
|
337
373
|
this.applyClick();
|
|
338
374
|
}
|
|
@@ -344,7 +380,8 @@ export class PlaySession {
|
|
|
344
380
|
this.countIn = undefined;
|
|
345
381
|
const engine = this.host.engine();
|
|
346
382
|
if (engine) {
|
|
347
|
-
engine.
|
|
383
|
+
if (engine.remoteClick) this.sendClick(engine.remoteClick.bind(engine));
|
|
384
|
+
else engine.setClick(this.clickOn ? this.clickBus() : undefined);
|
|
348
385
|
engine.setLeadMs(undefined);
|
|
349
386
|
await engine.monitor(false);
|
|
350
387
|
}
|
|
@@ -391,9 +428,34 @@ export class PlaySession {
|
|
|
391
428
|
private applyClick(): void {
|
|
392
429
|
const engine = this.host.engine();
|
|
393
430
|
if (!engine) return;
|
|
431
|
+
if (engine.remoteClick) {
|
|
432
|
+
this.sendClick(engine.remoteClick.bind(engine));
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
394
435
|
engine.setClick(this.clickOn || this.countIn ? this.clickBus() : undefined);
|
|
395
436
|
}
|
|
396
437
|
|
|
438
|
+
/** The click as a shared engine takes it: on, volume, count-in. */
|
|
439
|
+
private sendClick(send: (click: RemoteClick) => void): void {
|
|
440
|
+
const count = this.countIn;
|
|
441
|
+
send({
|
|
442
|
+
on: this.clickOn,
|
|
443
|
+
volume: this.clickVolume,
|
|
444
|
+
...(count
|
|
445
|
+
? {
|
|
446
|
+
countIn: {
|
|
447
|
+
startMs: count.startMs,
|
|
448
|
+
startBeat: count.startBeat,
|
|
449
|
+
beats: count.beats,
|
|
450
|
+
barBeats: count.barBeats,
|
|
451
|
+
clickBeats: count.clickBeats,
|
|
452
|
+
bpm: count.bpm,
|
|
453
|
+
},
|
|
454
|
+
}
|
|
455
|
+
: {}),
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
397
459
|
/** `/click on|off|<volume>`. */
|
|
398
460
|
public clickCommand(argument: string): string {
|
|
399
461
|
const parsed = parseClickArgument(argument, {
|
|
@@ -421,6 +483,54 @@ export class PlaySession {
|
|
|
421
483
|
return `grid · ${match.label}`;
|
|
422
484
|
}
|
|
423
485
|
|
|
486
|
+
/**
|
|
487
|
+
* `keys record [replace|off]` and the r / R keys: arm recording, overdub
|
|
488
|
+
* (additive) or replace (each pass erases the bars it crossed). With a
|
|
489
|
+
* loop set, every pass over it is one commit, so one undo step (§4.7).
|
|
490
|
+
* Replace refuses while another pane records this track (§12.4).
|
|
491
|
+
*/
|
|
492
|
+
public recordCommand(
|
|
493
|
+
mode: "overdub" | "replace" | "off",
|
|
494
|
+
): Readonly<{ ok: boolean; message: string }> {
|
|
495
|
+
if (mode === "off") {
|
|
496
|
+
const was = this.armed;
|
|
497
|
+
this.armed = false;
|
|
498
|
+
this.replace = false;
|
|
499
|
+
if (was) void this.stopRecording();
|
|
500
|
+
this.status = "record off";
|
|
501
|
+
return { ok: true, message: "record off" };
|
|
502
|
+
}
|
|
503
|
+
if (mode === "replace") {
|
|
504
|
+
const other = this.host.recordingElsewhere?.(this.trackId);
|
|
505
|
+
if (other) {
|
|
506
|
+
const message = `${other} is recording ${this.trackName()} · overdub instead (r)`;
|
|
507
|
+
this.status = `✗ ${message}`;
|
|
508
|
+
return { ok: false, message };
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
this.replace = mode === "replace";
|
|
512
|
+
this.armed = true;
|
|
513
|
+
const message = `record ${this.replace ? "replace" : "overdub"} · ${this.trackName()} · ${this.passWord()}`;
|
|
514
|
+
this.status = this.replace
|
|
515
|
+
? "replace · each pass overwrites its bars"
|
|
516
|
+
: "overdub";
|
|
517
|
+
return { ok: true, message };
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
private trackName(): string {
|
|
521
|
+
return this.trackData()?.name ?? this.trackId;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** `loop 5–6 · one undo step a pass`, or `one undo step a bar`. */
|
|
525
|
+
private passWord(): string {
|
|
526
|
+
const loop = loopedSection(this.host.score());
|
|
527
|
+
if (!loop) return "one undo step a bar";
|
|
528
|
+
const last = loop.startBar + loop.bars;
|
|
529
|
+
const bars =
|
|
530
|
+
loop.bars === 1 ? `${loop.startBar + 1}` : `${loop.startBar + 1}–${last}`;
|
|
531
|
+
return `loop ${bars} · one undo step a pass`;
|
|
532
|
+
}
|
|
533
|
+
|
|
424
534
|
/** Handle one decoded key; mode commands return to the caller. */
|
|
425
535
|
public press(value: string): PlayKeyResult {
|
|
426
536
|
const now = this.host.now();
|
|
@@ -483,6 +593,8 @@ export class PlaySession {
|
|
|
483
593
|
return { type: "handled" };
|
|
484
594
|
case "note": {
|
|
485
595
|
this.release(action.released, now);
|
|
596
|
+
// A key that is not recording glows its lane for a moment.
|
|
597
|
+
if (!this.recording) this.host.ghost?.(action.note.pitch);
|
|
486
598
|
const chord = this.chords.chordFor(action.note.pitch);
|
|
487
599
|
if (chord) {
|
|
488
600
|
const played = this.chords.voice(chord, action.note.pitch);
|
|
@@ -501,6 +613,12 @@ export class PlaySession {
|
|
|
501
613
|
);
|
|
502
614
|
return { type: "handled" };
|
|
503
615
|
}
|
|
616
|
+
// f07-sing: a throat track is one drone; a new key releases the last.
|
|
617
|
+
if (singThroat(this.trackData())) {
|
|
618
|
+
if (this.throatId !== undefined && this.throatId !== action.note.id)
|
|
619
|
+
this.release([this.throatId], now);
|
|
620
|
+
this.throatId = action.note.id;
|
|
621
|
+
}
|
|
504
622
|
this.sound(action.note.id, action.note);
|
|
505
623
|
this.record(action.note);
|
|
506
624
|
return { type: "handled" };
|
|
@@ -545,15 +663,12 @@ export class PlaySession {
|
|
|
545
663
|
|
|
546
664
|
private command(command: PlayCommand): PlayKeyResult {
|
|
547
665
|
if (command === "record") {
|
|
548
|
-
this.armed
|
|
549
|
-
if (!this.armed) void this.stopRecording();
|
|
550
|
-
this.status = this.armed ? "record armed" : "record off";
|
|
666
|
+
this.recordCommand(this.armed ? "off" : "overdub");
|
|
551
667
|
return { type: "handled" };
|
|
552
668
|
}
|
|
553
669
|
if (command === "replace") {
|
|
554
|
-
|
|
555
|
-
this.
|
|
556
|
-
this.status = this.replace ? "replace · bars are overwritten" : "overdub";
|
|
670
|
+
const result = this.recordCommand(this.replace ? "overdub" : "replace");
|
|
671
|
+
if (!result.ok) this.host.card(result.message, "error");
|
|
557
672
|
return { type: "handled" };
|
|
558
673
|
}
|
|
559
674
|
if (command === "click") {
|
|
@@ -608,6 +723,18 @@ export class PlaySession {
|
|
|
608
723
|
const seconds = Number.isFinite(note.releaseAtMs)
|
|
609
724
|
? (note.releaseAtMs - note.atMs) / 1000
|
|
610
725
|
: MAX_LIVE_NOTE_SECONDS;
|
|
726
|
+
if (engine.remoteNote) {
|
|
727
|
+
const scheduled = engine.remoteNote(id, {
|
|
728
|
+
trackId: this.trackId,
|
|
729
|
+
pitch: note.pitch,
|
|
730
|
+
velocity: note.velocity / 127,
|
|
731
|
+
seconds,
|
|
732
|
+
beat: this.host.beatAt(note.atMs),
|
|
733
|
+
atMs: note.atMs,
|
|
734
|
+
});
|
|
735
|
+
this.lastLatencyMs = Math.max(0, scheduled - this.host.now());
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
611
738
|
const samples = this.host.samples?.();
|
|
612
739
|
if (this.synth?.rate !== engine.sampleRate)
|
|
613
740
|
this.synth = new LiveSynth(engine.sampleRate);
|
|
@@ -925,25 +1052,50 @@ export class PlaySession {
|
|
|
925
1052
|
if (this.pending.size > 0 || this.replaceBars.size > 0)
|
|
926
1053
|
void this.stopRecording();
|
|
927
1054
|
this.lastBar = undefined;
|
|
1055
|
+
this.lastPass = undefined;
|
|
928
1056
|
return;
|
|
929
1057
|
}
|
|
930
1058
|
const score = this.host.score();
|
|
931
1059
|
const bar = transportBar(score, this.host.beatAt(now));
|
|
932
1060
|
if (bar !== this.lastBar)
|
|
933
1061
|
this.barPedal.set(this.loopBar(bar), this.pedalDown ? "down" : "up");
|
|
1062
|
+
// With a loop set, a take commits once per pass (each wrap), so one
|
|
1063
|
+
// pass is one undo step; without one, once per bar (§4.7).
|
|
1064
|
+
const loop = loopedSection(score);
|
|
1065
|
+
const pass = loop ? Math.floor(bar / loop.bars) : undefined;
|
|
934
1066
|
if (this.lastBar !== undefined && bar !== this.lastBar) {
|
|
1067
|
+
// Every bar the playhead left (a slow frame can skip one).
|
|
935
1068
|
if (this.replace)
|
|
936
|
-
|
|
937
|
-
this.
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
1069
|
+
for (
|
|
1070
|
+
let left = this.lastBar;
|
|
1071
|
+
left < bar && left < this.lastBar + 256;
|
|
1072
|
+
left += 1
|
|
1073
|
+
)
|
|
1074
|
+
this.replaceBars.add(
|
|
1075
|
+
this.loopBar(
|
|
1076
|
+
Math.floor(
|
|
1077
|
+
this.toScoreBeat(left * score.beatsPerBar) / score.beatsPerBar,
|
|
1078
|
+
),
|
|
941
1079
|
),
|
|
942
|
-
)
|
|
943
|
-
|
|
944
|
-
this.
|
|
1080
|
+
);
|
|
1081
|
+
if (pass === undefined) this.queueFlush(bar, now, false);
|
|
1082
|
+
else if (this.lastPass !== undefined && pass !== this.lastPass)
|
|
1083
|
+
this.queueFlush(bar, now, false, true);
|
|
945
1084
|
}
|
|
946
1085
|
this.lastBar = bar;
|
|
1086
|
+
this.lastPass = pass;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
/** `↻ 5–6 · pass 3` while loop recording; undefined otherwise. */
|
|
1090
|
+
public passLabel(): string | undefined {
|
|
1091
|
+
if (!this.recording) return undefined;
|
|
1092
|
+
const loop = loopedSection(this.host.score());
|
|
1093
|
+
if (!loop) return undefined;
|
|
1094
|
+
const bars =
|
|
1095
|
+
loop.bars === 1
|
|
1096
|
+
? `${loop.startBar + 1}`
|
|
1097
|
+
: `${loop.startBar + 1}–${loop.startBar + loop.bars}`;
|
|
1098
|
+
return `↻ ${bars} · pass ${this.passCount + 1}`;
|
|
947
1099
|
}
|
|
948
1100
|
|
|
949
1101
|
private toScoreBeat(beat: number): number {
|
|
@@ -962,15 +1114,27 @@ export class PlaySession {
|
|
|
962
1114
|
if (this.pedalDown && this.host.playing())
|
|
963
1115
|
this.pendingPedal.push({ beat: this.host.beatAt(now), state: "up" });
|
|
964
1116
|
this.pedalDown = false;
|
|
965
|
-
this.queueFlush(
|
|
1117
|
+
this.queueFlush(
|
|
1118
|
+
Number.POSITIVE_INFINITY,
|
|
1119
|
+
now,
|
|
1120
|
+
true,
|
|
1121
|
+
loopedSection(this.host.score()) !== undefined,
|
|
1122
|
+
);
|
|
966
1123
|
await this.flushing;
|
|
967
1124
|
this.recordedIds.clear();
|
|
968
1125
|
this.lastBar = undefined;
|
|
1126
|
+
this.lastPass = undefined;
|
|
1127
|
+
this.passCount = 0;
|
|
969
1128
|
}
|
|
970
1129
|
|
|
971
|
-
private queueFlush(
|
|
1130
|
+
private queueFlush(
|
|
1131
|
+
currentBar: number,
|
|
1132
|
+
now: number,
|
|
1133
|
+
all: boolean,
|
|
1134
|
+
pass = false,
|
|
1135
|
+
): void {
|
|
972
1136
|
this.flushing = this.flushing
|
|
973
|
-
.then(() => this.flush(currentBar, now, all))
|
|
1137
|
+
.then(() => this.flush(currentBar, now, all, pass))
|
|
974
1138
|
.catch((error: unknown) =>
|
|
975
1139
|
this.host.card(
|
|
976
1140
|
`record failed · ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -987,6 +1151,7 @@ export class PlaySession {
|
|
|
987
1151
|
currentBar: number,
|
|
988
1152
|
now: number,
|
|
989
1153
|
all: boolean,
|
|
1154
|
+
pass = false,
|
|
990
1155
|
): Promise<void> {
|
|
991
1156
|
const score = this.host.score();
|
|
992
1157
|
const ready: Pending[] = [];
|
|
@@ -1003,12 +1168,39 @@ export class PlaySession {
|
|
|
1003
1168
|
this.pendingPedal = this.pendingPedal.filter(
|
|
1004
1169
|
(event) => !pedal.includes(event),
|
|
1005
1170
|
);
|
|
1006
|
-
if (
|
|
1171
|
+
if (pass) this.passCount += 1;
|
|
1172
|
+
// §12.4: a replace pass never erases while another pane records this
|
|
1173
|
+
// track; its notes still land, as an overdub.
|
|
1174
|
+
const other =
|
|
1175
|
+
erase.length > 0
|
|
1176
|
+
? this.host.recordingElsewhere?.(this.trackId)
|
|
1177
|
+
: undefined;
|
|
1178
|
+
if (other) {
|
|
1179
|
+
erase.length = 0;
|
|
1180
|
+
this.host.card(
|
|
1181
|
+
`${other} is recording ${this.trackName()} · pass kept as overdub`,
|
|
1182
|
+
"warning",
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
if (ready.length === 0 && erase.length === 0 && pedal.length === 0) {
|
|
1186
|
+
if (pass) this.recordedIds.clear();
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1007
1189
|
const eraseStates = new Map(
|
|
1008
1190
|
erase.map((bar) => [bar, this.barPedal.get(bar) ?? "up"] as const),
|
|
1009
1191
|
);
|
|
1010
1192
|
try {
|
|
1011
|
-
await this.commitTake(
|
|
1193
|
+
await this.commitTake(
|
|
1194
|
+
score,
|
|
1195
|
+
ready,
|
|
1196
|
+
erase,
|
|
1197
|
+
eraseStates,
|
|
1198
|
+
pedal,
|
|
1199
|
+
now,
|
|
1200
|
+
pass ? this.passCount : undefined,
|
|
1201
|
+
);
|
|
1202
|
+
// The next pass may replace this one's notes: forget them.
|
|
1203
|
+
if (pass) this.recordedIds.clear();
|
|
1012
1204
|
} catch (error) {
|
|
1013
1205
|
// Nothing was committed: keep the take so the next flush retries it.
|
|
1014
1206
|
for (const pending of ready) this.pending.set(pending.id, pending);
|
|
@@ -1025,6 +1217,7 @@ export class PlaySession {
|
|
|
1025
1217
|
eraseStates: ReadonlyMap<number, PedalState>,
|
|
1026
1218
|
pedal: readonly { beat: number; state: PedalState }[],
|
|
1027
1219
|
now: number,
|
|
1220
|
+
pass?: number,
|
|
1028
1221
|
): Promise<void> {
|
|
1029
1222
|
const operations = recordOperations(score, {
|
|
1030
1223
|
trackId: this.trackId,
|
|
@@ -1079,12 +1272,17 @@ export class PlaySession {
|
|
|
1079
1272
|
trackId: this.trackId,
|
|
1080
1273
|
operations,
|
|
1081
1274
|
replace: this.replace,
|
|
1275
|
+
...(pass !== undefined ? { pass } : {}),
|
|
1082
1276
|
});
|
|
1083
1277
|
for (const pending of ready) this.pending.delete(pending.id);
|
|
1084
1278
|
for (const operation of operations)
|
|
1085
1279
|
if (operation.type === "addNote") this.recordedIds.add(operation.note.id);
|
|
1280
|
+
const head =
|
|
1281
|
+
pass !== undefined
|
|
1282
|
+
? `pass ${pass} · +${added} note${added === 1 ? "" : "s"}`
|
|
1283
|
+
: `recorded ${added} note${added === 1 ? "" : "s"}`;
|
|
1086
1284
|
this.host.card(
|
|
1087
|
-
|
|
1285
|
+
`${head}${pedal.length && !pedalNote ? ` · ${pedal.length} pedal` : ""}${removed ? ` · replaced ${removed}` : ""} · ${this.trackId}${pedalNote}`,
|
|
1088
1286
|
pedalNote ? "warning" : "success",
|
|
1089
1287
|
);
|
|
1090
1288
|
}
|
|
@@ -1143,20 +1341,25 @@ export class PlaySession {
|
|
|
1143
1341
|
};
|
|
1144
1342
|
}
|
|
1145
1343
|
return {
|
|
1146
|
-
range: this.keyboard.range,
|
|
1344
|
+
range: this.layout.drums ? "drums" : this.keyboard.range,
|
|
1147
1345
|
velocity: this.keyboard.velocity,
|
|
1148
1346
|
armed: this.armed,
|
|
1149
1347
|
recording: this.recording,
|
|
1150
1348
|
replace: this.replace,
|
|
1349
|
+
pass: this.passLabel(),
|
|
1151
1350
|
click: this.clickOn,
|
|
1152
1351
|
sustain: this.keyboard.sustain,
|
|
1153
1352
|
countIn,
|
|
1154
1353
|
beat,
|
|
1155
1354
|
grid: `grid ${this.grid}`,
|
|
1156
|
-
|
|
1355
|
+
// A kit plays drums, not harmony: no chord row and no key chip.
|
|
1356
|
+
chords: this.layout.drums
|
|
1357
|
+
? undefined
|
|
1358
|
+
: this.chords.glance(songKey(this.host.score().key).set),
|
|
1157
1359
|
status: this.status,
|
|
1158
1360
|
keys: this.strip(now),
|
|
1159
|
-
legend:
|
|
1361
|
+
legend:
|
|
1362
|
+
this.chords.on && !this.layout.drums ? this.chords.legend() : undefined,
|
|
1160
1363
|
};
|
|
1161
1364
|
}
|
|
1162
1365
|
|
|
@@ -1178,8 +1381,20 @@ export class PlaySession {
|
|
|
1178
1381
|
).map((cell) => {
|
|
1179
1382
|
const chord = this.keyboard.pitchFor(cell.key);
|
|
1180
1383
|
const name =
|
|
1181
|
-
chord === undefined
|
|
1182
|
-
|
|
1384
|
+
chord === undefined || this.layout.drums
|
|
1385
|
+
? undefined
|
|
1386
|
+
: this.chords.keyLabel(chord);
|
|
1387
|
+
// Chords in the key are the safe ones to reach for: the strip lights
|
|
1388
|
+
// them; a borrowed chord still plays but stays plain.
|
|
1389
|
+
return name && chord !== undefined
|
|
1390
|
+
? {
|
|
1391
|
+
...cell,
|
|
1392
|
+
label: name,
|
|
1393
|
+
chord: true,
|
|
1394
|
+
diatonic:
|
|
1395
|
+
degreeOf(this.chords.key, ((chord % 12) + 12) % 12) !== undefined,
|
|
1396
|
+
}
|
|
1397
|
+
: this.noteCell(cell);
|
|
1183
1398
|
});
|
|
1184
1399
|
}
|
|
1185
1400
|
|