@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/session/rebase.ts
CHANGED
|
@@ -2,17 +2,20 @@
|
|
|
2
2
|
* Server-side rebase of agent operation intents.
|
|
3
3
|
*
|
|
4
4
|
* An agent validates its operations against the score at `base`. When other
|
|
5
|
-
*
|
|
5
|
+
* panes committed in the meantime, dawgd replays the operations on the
|
|
6
6
|
* current score instead of rejecting, but only when nothing they touch
|
|
7
|
-
* changed since `base`: the notes they update or remove, the
|
|
8
|
-
*
|
|
7
|
+
* changed since `base`: the notes they update or remove, the track
|
|
8
|
+
* properties they patch (per track and field, so two panes can set
|
|
9
|
+
* different properties of one track), the tracks whose contents they clear, the tracks they add notes to, and the
|
|
9
10
|
* tempo or length they set. Anything else is a conflict and the caller gets
|
|
10
11
|
* the usual `rebase` reply. The reducer still validates every operation.
|
|
11
12
|
*/
|
|
12
13
|
import {
|
|
13
14
|
applyScoreOperation,
|
|
15
|
+
automationPoints,
|
|
14
16
|
scoreFromJSON,
|
|
15
17
|
type ScoreOperation,
|
|
18
|
+
type Track,
|
|
16
19
|
type TrackScore,
|
|
17
20
|
} from "../../core/score.ts";
|
|
18
21
|
import { rewindComposition, type Rewind } from "./delta.ts";
|
|
@@ -83,11 +86,43 @@ export function rebaseOperations(
|
|
|
83
86
|
return { ok: false, reason: `note ${operation.noteId} changed` };
|
|
84
87
|
break;
|
|
85
88
|
}
|
|
86
|
-
|
|
89
|
+
// Track settings conflict per (track, property): another pane's
|
|
90
|
+
// volume change never blocks this pane's pan, only its volume.
|
|
91
|
+
case "updateTrack": {
|
|
92
|
+
const id = operation.trackId;
|
|
93
|
+
if (added.tracks.has(id)) break;
|
|
94
|
+
const before = trackSettings(base, id);
|
|
95
|
+
const now = trackSettings(current, id);
|
|
96
|
+
if (!now) return { ok: false, reason: `track ${id} was removed` };
|
|
97
|
+
for (const field of Object.keys(operation.patch) as (keyof Track)[])
|
|
98
|
+
if (!same(before?.[field] ?? null, now[field] ?? null))
|
|
99
|
+
return { ok: false, reason: `${id} ${String(field)} changed` };
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
case "setClips": {
|
|
103
|
+
const id = operation.trackId;
|
|
104
|
+
if (added.tracks.has(id)) break;
|
|
105
|
+
const now = trackSettings(current, id);
|
|
106
|
+
if (!now) return { ok: false, reason: `track ${id} was removed` };
|
|
107
|
+
if (!same(trackSettings(base, id)?.clips ?? null, now.clips ?? null))
|
|
108
|
+
return { ok: false, reason: `${id} clips changed` };
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
87
111
|
case "setAutomation": {
|
|
88
|
-
|
|
89
|
-
if (
|
|
90
|
-
|
|
112
|
+
const id = operation.trackId;
|
|
113
|
+
if (added.tracks.has(id)) break;
|
|
114
|
+
const now = trackSettings(current, id);
|
|
115
|
+
if (!now) return { ok: false, reason: `track ${id} was removed` };
|
|
116
|
+
if (
|
|
117
|
+
!same(
|
|
118
|
+
automationPoints(trackSettings(base, id), operation.parameter),
|
|
119
|
+
automationPoints(now, operation.parameter),
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
return {
|
|
123
|
+
ok: false,
|
|
124
|
+
reason: `${id} ${operation.parameter} automation changed`,
|
|
125
|
+
};
|
|
91
126
|
break;
|
|
92
127
|
}
|
|
93
128
|
case "clearTrack": {
|
|
@@ -123,10 +158,18 @@ export function rebaseOperations(
|
|
|
123
158
|
if (!same(base.tuning ?? null, current.tuning ?? null))
|
|
124
159
|
return { ok: false, reason: "tuning changed" };
|
|
125
160
|
break;
|
|
161
|
+
case "setCalibration":
|
|
162
|
+
if ((base.calibration ?? 0) !== (current.calibration ?? 0))
|
|
163
|
+
return { ok: false, reason: "calibration changed" };
|
|
164
|
+
break;
|
|
126
165
|
case "setMaster":
|
|
127
166
|
if (!deepEqual(base.master, current.master))
|
|
128
167
|
return { ok: false, reason: "master changed" };
|
|
129
168
|
break;
|
|
169
|
+
case "setStyle":
|
|
170
|
+
if (!deepEqual(base.style, current.style))
|
|
171
|
+
return { ok: false, reason: "style changed" };
|
|
172
|
+
break;
|
|
130
173
|
case "setSections":
|
|
131
174
|
if (
|
|
132
175
|
JSON.stringify(base.sections) !== JSON.stringify(current.sections) ||
|
|
@@ -135,6 +178,10 @@ export function rebaseOperations(
|
|
|
135
178
|
)
|
|
136
179
|
return { ok: false, reason: "sections changed" };
|
|
137
180
|
break;
|
|
181
|
+
case "setLoop":
|
|
182
|
+
if (!deepEqual(base.loop, current.loop))
|
|
183
|
+
return { ok: false, reason: "loop changed" };
|
|
184
|
+
break;
|
|
138
185
|
case "removeTrack": {
|
|
139
186
|
if (added.tracks.has(operation.trackId)) break;
|
|
140
187
|
if (
|
|
@@ -177,13 +224,17 @@ export function compositionAt(
|
|
|
177
224
|
record: {
|
|
178
225
|
composition: unknown;
|
|
179
226
|
events: readonly { revision: number; rewind?: Rewind | undefined }[];
|
|
227
|
+
/** Events folded out of the front of the log (see `SessionRecord`). */
|
|
228
|
+
folded?: number | undefined;
|
|
180
229
|
},
|
|
181
230
|
revision: number,
|
|
182
231
|
): unknown {
|
|
183
232
|
const { events } = record;
|
|
184
|
-
|
|
185
|
-
if (revision
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
233
|
+
const base = record.folded ?? 0;
|
|
234
|
+
if (!Number.isSafeInteger(revision) || revision < base) return undefined;
|
|
235
|
+
const start = revision - base;
|
|
236
|
+
if (start >= events.length) return undefined;
|
|
237
|
+
for (let index = start; index < events.length; index += 1)
|
|
238
|
+
if (events[index]!.revision !== base + index + 1) return undefined;
|
|
239
|
+
return rewindComposition(record.composition, events, start);
|
|
189
240
|
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Musical receipts: one line that says what changed in the song, built from
|
|
3
|
+
* the score operations a change applied (`diffScores(before, after)`).
|
|
4
|
+
*
|
|
5
|
+
* ◆ 96 BPM · +2 notes on bass (C4 E4) · reverb 0.4
|
|
6
|
+
*
|
|
7
|
+
* Receipts name tempo in BPM, notes by pitch, and track settings by their
|
|
8
|
+
* musical value; revision numbers and validator counts stay in the ctrl-o
|
|
9
|
+
* log. The builder is pure, so the agent turn, undo and redo share it.
|
|
10
|
+
*/
|
|
11
|
+
import { diffScores } from "../../core/diff.ts";
|
|
12
|
+
import { drumVoiceForPitch, isDrumInstrument } from "../../core/drums.ts";
|
|
13
|
+
import { midiToPitch } from "../../core/pitch.ts";
|
|
14
|
+
import type { ScoreOperation, TrackScore } from "../../core/score.ts";
|
|
15
|
+
|
|
16
|
+
/** Parts kept before the rest collapse to `+N more`. */
|
|
17
|
+
const MAX_PARTS = 5;
|
|
18
|
+
/** Pitch names listed after a note count. */
|
|
19
|
+
const MAX_PITCHES = 4;
|
|
20
|
+
|
|
21
|
+
function plural(count: number, word: string): string {
|
|
22
|
+
return `${count} ${word}${count === 1 ? "" : "s"}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function round(value: number, places = 2): string {
|
|
26
|
+
const scale = 10 ** places;
|
|
27
|
+
return String(Math.round(value * scale) / scale);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function noteName(pitch: number, drum: boolean): string {
|
|
31
|
+
return drum ? drumVoiceForPitch(pitch) : midiToPitch(pitch);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type NoteTally = { added: number[]; removed: number[]; changed: number };
|
|
35
|
+
|
|
36
|
+
/** One field of a track patch as a musical phrase, or undefined to skip. */
|
|
37
|
+
function trackField(field: string, value: unknown): string | undefined {
|
|
38
|
+
if (value === null || value === undefined) {
|
|
39
|
+
if (field === "volumeAutomation" || field.endsWith("Automation"))
|
|
40
|
+
return `${field.replace(/Automation$/, "")} automation off`;
|
|
41
|
+
return `${field} off`;
|
|
42
|
+
}
|
|
43
|
+
switch (field) {
|
|
44
|
+
case "volume":
|
|
45
|
+
return typeof value === "number" ? `volume ${round(value)}` : undefined;
|
|
46
|
+
case "pan":
|
|
47
|
+
return typeof value === "number" ? `pan ${round(value)}` : undefined;
|
|
48
|
+
case "muted":
|
|
49
|
+
return value === true ? "muted" : "unmuted";
|
|
50
|
+
case "solo":
|
|
51
|
+
return value === true ? "solo" : "solo off";
|
|
52
|
+
case "instrument":
|
|
53
|
+
return typeof value === "string" ? `→ ${value}` : undefined;
|
|
54
|
+
case "name":
|
|
55
|
+
return typeof value === "string" ? `renamed ${value}` : undefined;
|
|
56
|
+
case "reverb":
|
|
57
|
+
case "delay": {
|
|
58
|
+
const mix = (value as { mix?: unknown }).mix;
|
|
59
|
+
return typeof mix === "number" ? `${field} ${round(mix)}` : field;
|
|
60
|
+
}
|
|
61
|
+
case "filter": {
|
|
62
|
+
const cutoff = (value as { cutoff?: unknown }).cutoff;
|
|
63
|
+
return typeof cutoff === "number"
|
|
64
|
+
? `filter ${Math.round(cutoff)} Hz`
|
|
65
|
+
: "filter";
|
|
66
|
+
}
|
|
67
|
+
case "fx": {
|
|
68
|
+
const names = Object.keys(value as Record<string, unknown>);
|
|
69
|
+
return names.length ? `fx ${names.join(" ")}` : "fx off";
|
|
70
|
+
}
|
|
71
|
+
default:
|
|
72
|
+
if (Array.isArray(value) && field.endsWith("Automation"))
|
|
73
|
+
return value.length === 0
|
|
74
|
+
? `${field.replace(/Automation$/, "")} automation off`
|
|
75
|
+
: `${field.replace(/Automation$/, "")} automation`;
|
|
76
|
+
return field;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The receipt parts for `operations` applied to `before`. Tracks are named
|
|
82
|
+
* only when the song has more than one, so a single-track edit reads
|
|
83
|
+
* `reverb 0.4` and a band edit `bass reverb 0.4`.
|
|
84
|
+
*/
|
|
85
|
+
export function receiptParts(
|
|
86
|
+
before: TrackScore,
|
|
87
|
+
operations: readonly ScoreOperation[],
|
|
88
|
+
): string[] {
|
|
89
|
+
const parts: string[] = [];
|
|
90
|
+
// Track settings follow the notes: what you hear first reads first.
|
|
91
|
+
const settings: string[] = [];
|
|
92
|
+
const instruments = new Map(
|
|
93
|
+
before.tracks.map((track) => [track.id, track.instrument]),
|
|
94
|
+
);
|
|
95
|
+
const noteTrack = new Map(
|
|
96
|
+
before.notes.map((note) => [note.id, note.trackId]),
|
|
97
|
+
);
|
|
98
|
+
const notePitch = new Map(
|
|
99
|
+
before.notes.map((note) => [
|
|
100
|
+
note.id,
|
|
101
|
+
typeof note.pitch === "number" ? note.pitch : Number.NaN,
|
|
102
|
+
]),
|
|
103
|
+
);
|
|
104
|
+
const tallies = new Map<string, NoteTally>();
|
|
105
|
+
const tally = (trackId: string): NoteTally => {
|
|
106
|
+
let found = tallies.get(trackId);
|
|
107
|
+
if (!found) {
|
|
108
|
+
found = { added: [], removed: [], changed: 0 };
|
|
109
|
+
tallies.set(trackId, found);
|
|
110
|
+
}
|
|
111
|
+
return found;
|
|
112
|
+
};
|
|
113
|
+
const added = new Set<string>();
|
|
114
|
+
const removed = new Set<string>();
|
|
115
|
+
const trackCount = new Set([
|
|
116
|
+
...before.tracks.map((track) => track.id),
|
|
117
|
+
...operations.flatMap((op) =>
|
|
118
|
+
op.type === "addTrack" ? [op.track.id ?? "track"] : [],
|
|
119
|
+
),
|
|
120
|
+
]).size;
|
|
121
|
+
const prefix = (trackId: string) => (trackCount > 1 ? `${trackId} ` : "");
|
|
122
|
+
// Notes are summarised per track after the song-level parts.
|
|
123
|
+
for (const op of operations) {
|
|
124
|
+
switch (op.type) {
|
|
125
|
+
case "setTempo":
|
|
126
|
+
parts.push(`${round(op.tempoBpm, 1)} BPM`);
|
|
127
|
+
break;
|
|
128
|
+
case "setKey":
|
|
129
|
+
parts.push(op.key ? `key ${op.key}` : "no key");
|
|
130
|
+
break;
|
|
131
|
+
case "setBars":
|
|
132
|
+
parts.push(plural(op.bars, "bar"));
|
|
133
|
+
break;
|
|
134
|
+
case "setMeter":
|
|
135
|
+
parts.push(`${op.beatsPerBar} beats a bar`);
|
|
136
|
+
break;
|
|
137
|
+
case "setTime":
|
|
138
|
+
parts.push(op.time ? "tempo map" : "steady tempo");
|
|
139
|
+
break;
|
|
140
|
+
case "setTuning":
|
|
141
|
+
parts.push(
|
|
142
|
+
op.tuning ? `tuning ${op.tuning.name ?? ""}`.trim() : "12-TET",
|
|
143
|
+
);
|
|
144
|
+
break;
|
|
145
|
+
case "setMaster":
|
|
146
|
+
parts.push(op.master ? "master" : "master off");
|
|
147
|
+
break;
|
|
148
|
+
case "setStyle":
|
|
149
|
+
if (op.style) parts.push(op.style.id);
|
|
150
|
+
break;
|
|
151
|
+
case "setSections":
|
|
152
|
+
parts.push(plural(op.sections.length, "section"));
|
|
153
|
+
break;
|
|
154
|
+
case "setLoop":
|
|
155
|
+
parts.push(
|
|
156
|
+
op.loop
|
|
157
|
+
? `loop ${op.loop.startBar + 1}–${op.loop.startBar + op.loop.bars}`
|
|
158
|
+
: "loop off",
|
|
159
|
+
);
|
|
160
|
+
break;
|
|
161
|
+
case "setCalibration":
|
|
162
|
+
break;
|
|
163
|
+
case "addTrack": {
|
|
164
|
+
const id = op.track.id ?? "track";
|
|
165
|
+
const instrument = op.track.instrument;
|
|
166
|
+
added.add(id);
|
|
167
|
+
instruments.set(id, instrument ?? "");
|
|
168
|
+
parts.push(
|
|
169
|
+
`+${id}${instrument && instrument !== id ? ` (${instrument})` : ""}`,
|
|
170
|
+
);
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
case "removeTrack":
|
|
174
|
+
removed.add(op.trackId);
|
|
175
|
+
parts.push(`−${op.trackId}`);
|
|
176
|
+
break;
|
|
177
|
+
case "moveTrack":
|
|
178
|
+
parts.push(`moved ${op.trackId}`);
|
|
179
|
+
break;
|
|
180
|
+
case "clearTrack":
|
|
181
|
+
parts.push(`cleared ${op.trackId}`);
|
|
182
|
+
break;
|
|
183
|
+
case "setClips":
|
|
184
|
+
settings.push(
|
|
185
|
+
`${prefix(op.trackId)}${op.clips && op.clips.length ? plural(op.clips.length, "clip") : "no clips"}`,
|
|
186
|
+
);
|
|
187
|
+
break;
|
|
188
|
+
case "setAutomation":
|
|
189
|
+
settings.push(`${prefix(op.trackId)}${op.parameter} automation`);
|
|
190
|
+
break;
|
|
191
|
+
case "updateTrack": {
|
|
192
|
+
if (added.has(op.trackId)) break;
|
|
193
|
+
const fields = Object.entries(op.patch)
|
|
194
|
+
.map(([field, value]) => trackField(field, value))
|
|
195
|
+
.filter((text): text is string => text !== undefined);
|
|
196
|
+
if (fields.length === 0) break;
|
|
197
|
+
const name = op.patch.name;
|
|
198
|
+
const label =
|
|
199
|
+
typeof name === "string" ? op.trackId : prefix(op.trackId).trim();
|
|
200
|
+
const text = fields.slice(0, 2).join(" ");
|
|
201
|
+
settings.push(label ? `${label} ${text}` : text);
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
case "addNote": {
|
|
205
|
+
const entry = tally(op.note.trackId);
|
|
206
|
+
entry.added.push(
|
|
207
|
+
typeof op.note.pitch === "number" ? op.note.pitch : Number.NaN,
|
|
208
|
+
);
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
case "removeNote": {
|
|
212
|
+
const trackId = noteTrack.get(op.noteId);
|
|
213
|
+
if (trackId && !removed.has(trackId))
|
|
214
|
+
tally(trackId).removed.push(notePitch.get(op.noteId) ?? Number.NaN);
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
case "updateNote": {
|
|
218
|
+
const trackId = noteTrack.get(op.noteId);
|
|
219
|
+
if (trackId) tally(trackId).changed += 1;
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
for (const [trackId, entry] of tallies) {
|
|
225
|
+
const drum = isDrumInstrument(instruments.get(trackId));
|
|
226
|
+
const listed = (pitches: readonly number[]): string => {
|
|
227
|
+
const names = pitches
|
|
228
|
+
.filter((pitch) => Number.isFinite(pitch))
|
|
229
|
+
.slice(0, MAX_PITCHES)
|
|
230
|
+
.map((pitch) => noteName(pitch, drum));
|
|
231
|
+
const more = pitches.length > MAX_PITCHES ? " …" : "";
|
|
232
|
+
return names.length ? ` (${names.join(" ")}${more})` : "";
|
|
233
|
+
};
|
|
234
|
+
if (entry.added.length > 0)
|
|
235
|
+
parts.push(
|
|
236
|
+
`+${plural(entry.added.length, "note")} on ${trackId}${listed(entry.added)}`,
|
|
237
|
+
);
|
|
238
|
+
if (entry.removed.length > 0)
|
|
239
|
+
parts.push(
|
|
240
|
+
`−${plural(entry.removed.length, "note")} on ${trackId}${listed(entry.removed)}`,
|
|
241
|
+
);
|
|
242
|
+
if (entry.changed > 0)
|
|
243
|
+
parts.push(`${plural(entry.changed, "note")} edited on ${trackId}`);
|
|
244
|
+
}
|
|
245
|
+
parts.push(...settings);
|
|
246
|
+
if (parts.length > MAX_PARTS)
|
|
247
|
+
return [
|
|
248
|
+
...parts.slice(0, MAX_PARTS - 1),
|
|
249
|
+
`+${parts.length - (MAX_PARTS - 1)} more`,
|
|
250
|
+
];
|
|
251
|
+
return parts;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** `96 BPM · +2 notes on bass (C4 E4) · reverb 0.4`, or "" for no change. */
|
|
255
|
+
export function musicalReceipt(before: TrackScore, after: TrackScore): string {
|
|
256
|
+
if (before === after) return "";
|
|
257
|
+
let operations: readonly ScoreOperation[];
|
|
258
|
+
try {
|
|
259
|
+
operations = diffScores(before, after);
|
|
260
|
+
} catch {
|
|
261
|
+
// A resolution change cannot diff: say the song changed.
|
|
262
|
+
return "song changed";
|
|
263
|
+
}
|
|
264
|
+
return receiptParts(before, operations).join(" · ");
|
|
265
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A pane's handle on the machine's one live engine (design §12.3). It looks
|
|
3
|
+
* like the local `LiveEngine` to play mode, but notes go to dawgd as note
|
|
4
|
+
* events (`remoteNote`) and the click as a structured request
|
|
5
|
+
* (`remoteClick`): no audio crosses the socket. dawgd renders each voice
|
|
6
|
+
* from the score it holds and mixes every pane into one stream and one
|
|
7
|
+
* click.
|
|
8
|
+
*
|
|
9
|
+
* Times cross the seam on the authority's clock: local monotonic ms plus
|
|
10
|
+
* `performance.timeOrigin` plus the port's clock offset (0 on one machine).
|
|
11
|
+
*/
|
|
12
|
+
import type { ClickBus } from "../audio/engine.ts";
|
|
13
|
+
import type { LiveNotePcm } from "../audio/live.ts";
|
|
14
|
+
import type { LiveClick, LiveMessage, LiveStatus } from "./protocol.ts";
|
|
15
|
+
|
|
16
|
+
/** What the shared engine needs from the session connection. */
|
|
17
|
+
export interface LiveLink {
|
|
18
|
+
liveMonitor(on: boolean): Promise<LiveStatus>;
|
|
19
|
+
live(message: Exclude<LiveMessage, { action: "monitor" }>): void;
|
|
20
|
+
/** Authority clock minus this machine's, in ms. */
|
|
21
|
+
readonly clockOffsetMs: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A played note as play mode hands it to a shared engine. */
|
|
25
|
+
export type RemoteNote = Readonly<{
|
|
26
|
+
trackId: string;
|
|
27
|
+
pitch: number;
|
|
28
|
+
/** 0..1. */
|
|
29
|
+
velocity: number;
|
|
30
|
+
seconds: number;
|
|
31
|
+
/** Transport beat of the press (unwrapped). */
|
|
32
|
+
beat: number;
|
|
33
|
+
/** Monotonic ms of the press. */
|
|
34
|
+
atMs: number;
|
|
35
|
+
}>;
|
|
36
|
+
|
|
37
|
+
/** A structured click request (a shared engine cannot take closures). */
|
|
38
|
+
export type RemoteClick = Readonly<{
|
|
39
|
+
on: boolean;
|
|
40
|
+
volume: number;
|
|
41
|
+
countIn?: Readonly<{
|
|
42
|
+
/** Monotonic ms the count-in starts. */
|
|
43
|
+
startMs: number;
|
|
44
|
+
startBeat: number;
|
|
45
|
+
beats: number;
|
|
46
|
+
barBeats: number;
|
|
47
|
+
clickBeats: number;
|
|
48
|
+
bpm: number;
|
|
49
|
+
}>;
|
|
50
|
+
}>;
|
|
51
|
+
|
|
52
|
+
export class SharedLiveEngine {
|
|
53
|
+
public sampleRate = 48_000;
|
|
54
|
+
public canMonitor = true;
|
|
55
|
+
public leadMs = 15;
|
|
56
|
+
public playLeadMs: number | undefined;
|
|
57
|
+
public audioNote: string | undefined = "shared engine";
|
|
58
|
+
/** Notes sent (tests and the status line read it). */
|
|
59
|
+
public sent = 0;
|
|
60
|
+
|
|
61
|
+
public constructor(private readonly link: LiveLink) {}
|
|
62
|
+
|
|
63
|
+
public async monitor(on: boolean): Promise<void> {
|
|
64
|
+
try {
|
|
65
|
+
const status = await this.link.liveMonitor(on);
|
|
66
|
+
this.sampleRate = status.sampleRate;
|
|
67
|
+
this.canMonitor = status.canMonitor;
|
|
68
|
+
this.leadMs = status.leadMs;
|
|
69
|
+
this.playLeadMs = status.leadMs;
|
|
70
|
+
this.audioNote = status.note ?? "shared engine";
|
|
71
|
+
} catch {
|
|
72
|
+
// dawgd is reconnecting; the link resends monitor once it is back.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The lead is dawgd's: a pane never retimes the shared engine. */
|
|
77
|
+
public setLeadMs(_ms: number | undefined): void {}
|
|
78
|
+
|
|
79
|
+
/** Local PCM never reaches a shared engine; play mode uses `remoteNote`. */
|
|
80
|
+
public noteOn(_id: number, _note: LiveNotePcm): number {
|
|
81
|
+
return performance.now() + this.leadMs;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
public remoteNote(id: number, note: RemoteNote): number {
|
|
85
|
+
this.sent += 1;
|
|
86
|
+
this.link.live({
|
|
87
|
+
v: 1,
|
|
88
|
+
type: "live",
|
|
89
|
+
action: "on",
|
|
90
|
+
voice: id,
|
|
91
|
+
trackId: note.trackId,
|
|
92
|
+
pitch: note.pitch,
|
|
93
|
+
velocity: Math.max(0, Math.min(1, note.velocity)),
|
|
94
|
+
seconds: Math.max(0, Math.min(600, note.seconds)),
|
|
95
|
+
beat: note.beat,
|
|
96
|
+
atMs: this.authority(note.atMs),
|
|
97
|
+
});
|
|
98
|
+
return performance.now() + this.leadMs;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
public noteOff(id: number): void {
|
|
102
|
+
this.link.live({
|
|
103
|
+
v: 1,
|
|
104
|
+
type: "live",
|
|
105
|
+
action: "off",
|
|
106
|
+
voice: id,
|
|
107
|
+
atMs: this.authority(performance.now()),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** A pane cannot hand dawgd a click closure; see `remoteClick`. */
|
|
112
|
+
public setClick(click: ClickBus | undefined): void {
|
|
113
|
+
if (!click) this.remoteClick({ on: false, volume: 0 });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
public remoteClick(click: RemoteClick): void {
|
|
117
|
+
const message: LiveClick = {
|
|
118
|
+
on: click.on,
|
|
119
|
+
volume: Math.max(0, Math.min(1, click.volume)),
|
|
120
|
+
};
|
|
121
|
+
if (click.countIn) {
|
|
122
|
+
const { startMs, ...rest } = click.countIn;
|
|
123
|
+
message.countIn = { ...rest, startAtMs: this.authority(startMs) };
|
|
124
|
+
}
|
|
125
|
+
this.link.live({ v: 1, type: "live", action: "click", ...message });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private authority(monotonicMs: number): number {
|
|
129
|
+
return performance.timeOrigin + monotonicMs + this.link.clockOffsetMs;
|
|
130
|
+
}
|
|
131
|
+
}
|