@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/store.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { mkdir,
|
|
1
|
+
import { mkdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { durableWrite } from "../fs/durable.ts";
|
|
2
3
|
import { dirname, join } from "node:path";
|
|
3
4
|
import { randomUUID } from "node:crypto";
|
|
4
5
|
import { acquireSessionLock } from "./lock.ts";
|
|
@@ -20,9 +21,13 @@ const MAX_EVENT_BYTES = 64 * 1024;
|
|
|
20
21
|
/** A rewind larger than this (a wholesale import) is not kept; the edit still lands. */
|
|
21
22
|
const MAX_REWIND_BYTES = 1024 * 1024;
|
|
22
23
|
const MAX_EVENTS = 2_000;
|
|
24
|
+
/**
|
|
25
|
+
* When the log is full, this many of the oldest events are folded away so a
|
|
26
|
+
* long session keeps accepting edits. The composition is the checkpoint;
|
|
27
|
+
* undo still reaches back through the events that remain.
|
|
28
|
+
*/
|
|
29
|
+
const FOLD_BATCH = 200;
|
|
23
30
|
export const MAX_RECORD_BYTES = 4 * 1024 * 1024;
|
|
24
|
-
/** Oldest rewinds are dropped in batches of this size when the record fills up. */
|
|
25
|
-
const COMPACT_BATCH = 64;
|
|
26
31
|
const MAX_SESSION_ID_LENGTH = 64;
|
|
27
32
|
const MAX_EVENT_KIND_LENGTH = 128;
|
|
28
33
|
const MAX_TIMESTAMP_LENGTH = 64;
|
|
@@ -34,12 +39,36 @@ export class SessionValidationError extends Error {
|
|
|
34
39
|
}
|
|
35
40
|
}
|
|
36
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Who made an event. Stamped by the authority from the connection's hello,
|
|
44
|
+
* never trusted from the intent: `actorId` is the person (absent for v1
|
|
45
|
+
* clients and the file fallback), `clientId` the window, `seq` the
|
|
46
|
+
* per-(actor, client) intent number used for retry dedupe.
|
|
47
|
+
*/
|
|
48
|
+
export type EventActor = {
|
|
49
|
+
actorId?: string;
|
|
50
|
+
clientId: string;
|
|
51
|
+
seq?: number;
|
|
52
|
+
};
|
|
53
|
+
|
|
37
54
|
export type SessionEvent = {
|
|
38
55
|
id: string;
|
|
39
56
|
revision: number;
|
|
57
|
+
/**
|
|
58
|
+
* Opaque to the store: any bounded string is kept and passed through, so a
|
|
59
|
+
* newer peer's event kinds survive an older reader.
|
|
60
|
+
*/
|
|
40
61
|
kind: string;
|
|
41
62
|
payload: unknown;
|
|
42
63
|
at: string;
|
|
64
|
+
actor?: EventActor;
|
|
65
|
+
/**
|
|
66
|
+
* The score operations this event applied: sent by an ops intent, or
|
|
67
|
+
* derived by the authority from a composition intent. Folding `ops` from
|
|
68
|
+
* the previous composition reproduces this one. Events without `ops` are
|
|
69
|
+
* snapshots (too large to diff, or not expressible as operations).
|
|
70
|
+
*/
|
|
71
|
+
ops?: unknown[];
|
|
43
72
|
/**
|
|
44
73
|
* How to recover the composition this event replaced from the one it
|
|
45
74
|
* produced (see `delta.ts`). Absent once compacted away: the store drops
|
|
@@ -56,6 +85,13 @@ export type SessionRecord<T> = {
|
|
|
56
85
|
updatedAt: string;
|
|
57
86
|
composition: T;
|
|
58
87
|
events: SessionEvent[];
|
|
88
|
+
/**
|
|
89
|
+
* Optional (0.7): how many of the oldest events were folded away when the
|
|
90
|
+
* log filled up. `revision === (folded ?? 0) + events.length`, and
|
|
91
|
+
* `events[i].revision === (folded ?? 0) + i + 1`. Absent on records that
|
|
92
|
+
* never filled, so older records load and print unchanged.
|
|
93
|
+
*/
|
|
94
|
+
folded?: number;
|
|
59
95
|
/** Name and lineage. Versioned separately so renames keep the revision. */
|
|
60
96
|
meta: SessionMeta;
|
|
61
97
|
};
|
|
@@ -192,7 +228,10 @@ export async function appendSessionEvent<T>(
|
|
|
192
228
|
// History is the store's job: a `before` composition in the payload (the
|
|
193
229
|
// pre-rewind convention) is dropped rather than persisted in full.
|
|
194
230
|
const payload = stripBefore(event.payload);
|
|
195
|
-
const payloadJson = stringifyJson(
|
|
231
|
+
const payloadJson = stringifyJson(
|
|
232
|
+
event.ops === undefined ? payload : [payload, event.ops],
|
|
233
|
+
"session event payload",
|
|
234
|
+
);
|
|
196
235
|
const payloadBytes = Buffer.byteLength(payloadJson, "utf8");
|
|
197
236
|
if (payloadBytes > MAX_EVENT_BYTES)
|
|
198
237
|
throw new Error(`session event exceeds ${MAX_EVENT_BYTES} bytes`);
|
|
@@ -207,10 +246,9 @@ export async function appendSessionEvent<T>(
|
|
|
207
246
|
try {
|
|
208
247
|
const disk = await readRecord<T>(paths.record);
|
|
209
248
|
if (disk.revision !== current.revision) throw new SessionConflictError();
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
throw new Error(`session event limit ${MAX_EVENTS} reached`);
|
|
249
|
+
// A full log folds its oldest events away instead of refusing the edit.
|
|
250
|
+
const fold = disk.events.length >= MAX_EVENTS ? FOLD_BATCH : 0;
|
|
251
|
+
const folded = (disk.folded ?? 0) + fold;
|
|
214
252
|
const revision = disk.revision + 1;
|
|
215
253
|
const at = new Date().toISOString();
|
|
216
254
|
const appended: SessionEvent = {
|
|
@@ -220,6 +258,8 @@ export async function appendSessionEvent<T>(
|
|
|
220
258
|
revision,
|
|
221
259
|
at,
|
|
222
260
|
};
|
|
261
|
+
if (event.actor !== undefined) appended.actor = { ...event.actor };
|
|
262
|
+
if (event.ops !== undefined) appended.ops = [...event.ops];
|
|
223
263
|
const rewind = diffRewind(disk.composition, composition);
|
|
224
264
|
if (Buffer.byteLength(JSON.stringify(rewind), "utf8") <= MAX_REWIND_BYTES)
|
|
225
265
|
appended.rewind = rewind;
|
|
@@ -228,7 +268,8 @@ export async function appendSessionEvent<T>(
|
|
|
228
268
|
revision,
|
|
229
269
|
updatedAt: at,
|
|
230
270
|
composition,
|
|
231
|
-
events: [...disk.events, appended],
|
|
271
|
+
events: [...disk.events.slice(fold), appended],
|
|
272
|
+
...(folded > 0 ? { folded } : {}),
|
|
232
273
|
// Metadata always comes from disk so a rename written by another window
|
|
233
274
|
// between this caller's read and its write is never reverted.
|
|
234
275
|
meta: disk.meta,
|
|
@@ -245,8 +286,10 @@ export async function appendSessionEvent<T>(
|
|
|
245
286
|
/**
|
|
246
287
|
* Keeps the record under its size cap by dropping the oldest events'
|
|
247
288
|
* rewinds, oldest first, so the rewinds that remain are always the newest
|
|
248
|
-
* contiguous run.
|
|
249
|
-
*
|
|
289
|
+
* contiguous run. The appended (last) event's rewind is dropped only when
|
|
290
|
+
* nothing else is left to drop, so undo of the edit just made keeps working
|
|
291
|
+
* whenever its rewind fits on its own. Only the composition and the bounded
|
|
292
|
+
* payloads are irreducible; a record that still does not fit is rejected.
|
|
250
293
|
*/
|
|
251
294
|
function compactRecord<T>(record: SessionRecord<T>): {
|
|
252
295
|
record: SessionRecord<T>;
|
|
@@ -254,23 +297,27 @@ function compactRecord<T>(record: SessionRecord<T>): {
|
|
|
254
297
|
} {
|
|
255
298
|
let current = record;
|
|
256
299
|
let json = JSON.stringify(current);
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
current = { ...current, events };
|
|
272
|
-
json = JSON.stringify(current);
|
|
300
|
+
let bytes = Buffer.byteLength(json, "utf8");
|
|
301
|
+
if (bytes <= MAX_RECORD_BYTES) return { record: current, json };
|
|
302
|
+
const events = [...current.events];
|
|
303
|
+
const last = events.length - 1;
|
|
304
|
+
// Measured sizes let the loop strip exactly as many rewinds as needed with
|
|
305
|
+
// one final stringify instead of one per batch.
|
|
306
|
+
// The appended event comes last, after every older rewind is gone.
|
|
307
|
+
for (let index = 0; index <= last && bytes > MAX_RECORD_BYTES; index += 1) {
|
|
308
|
+
const event = events[index]!;
|
|
309
|
+
if (event.rewind === undefined) continue;
|
|
310
|
+
const { rewind, ...rest } = event;
|
|
311
|
+
// `,"rewind":<json>` is what dropping the field saves.
|
|
312
|
+
bytes -= Buffer.byteLength(JSON.stringify(rewind), "utf8") + 10;
|
|
313
|
+
events[index] = rest;
|
|
273
314
|
}
|
|
315
|
+
current = { ...current, events };
|
|
316
|
+
json = JSON.stringify(current);
|
|
317
|
+
if (Buffer.byteLength(json, "utf8") > MAX_RECORD_BYTES)
|
|
318
|
+
throw new SessionValidationError(
|
|
319
|
+
`session record exceeds ${MAX_RECORD_BYTES} bytes`,
|
|
320
|
+
);
|
|
274
321
|
return { record: current, json };
|
|
275
322
|
}
|
|
276
323
|
|
|
@@ -383,8 +430,13 @@ export async function inheritedEvents(
|
|
|
383
430
|
} catch {
|
|
384
431
|
break;
|
|
385
432
|
}
|
|
386
|
-
if (!parent || fork.revision > parent.
|
|
387
|
-
|
|
433
|
+
if (!parent || fork.revision > parent.revision) break;
|
|
434
|
+
// Events folded out of the parent are gone; what remains still ends at
|
|
435
|
+
// the fork point.
|
|
436
|
+
const prefix = parent.events.slice(
|
|
437
|
+
0,
|
|
438
|
+
Math.max(0, fork.revision - (parent.folded ?? 0)),
|
|
439
|
+
);
|
|
388
440
|
chunks.unshift(prefix.slice(-(MAX_INHERITED_EVENTS - total)));
|
|
389
441
|
total += prefix.length;
|
|
390
442
|
fork = parent.meta.forkOf;
|
|
@@ -413,20 +465,10 @@ async function writeAtomic(path: string, contents: string): Promise<void> {
|
|
|
413
465
|
throw new SessionValidationError(
|
|
414
466
|
`session record exceeds ${MAX_RECORD_BYTES} bytes`,
|
|
415
467
|
);
|
|
416
|
-
const temporary = `${path}.${process.pid}.tmp`;
|
|
417
468
|
// Durable before it is visible: the data reaches the disk before the
|
|
418
|
-
// rename publishes it,
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
await handle.writeFile(`${contents}\n`, "utf8");
|
|
422
|
-
await handle.sync();
|
|
423
|
-
} catch (error) {
|
|
424
|
-
await handle.close().catch(() => undefined);
|
|
425
|
-
await rm(temporary, { force: true }).catch(() => undefined);
|
|
426
|
-
throw error;
|
|
427
|
-
}
|
|
428
|
-
await handle.close();
|
|
429
|
-
await rename(temporary, path);
|
|
469
|
+
// rename publishes it, and the directory is synced so the rename survives
|
|
470
|
+
// a power loss too.
|
|
471
|
+
await durableWrite(path, `${contents}\n`, { mkdir: false });
|
|
430
472
|
}
|
|
431
473
|
|
|
432
474
|
async function readRecord<T>(path: string): Promise<SessionRecord<T>> {
|
|
@@ -472,7 +514,15 @@ function validateSessionRecord<T>(value: unknown): SessionRecord<T> {
|
|
|
472
514
|
throw new SessionValidationError(
|
|
473
515
|
`session has more than ${MAX_EVENTS} events`,
|
|
474
516
|
);
|
|
475
|
-
|
|
517
|
+
const folded = record.folded ?? 0;
|
|
518
|
+
if (
|
|
519
|
+
!Number.isSafeInteger(folded) ||
|
|
520
|
+
(folded as number) < 0 ||
|
|
521
|
+
(record.folded !== undefined && folded === 0)
|
|
522
|
+
)
|
|
523
|
+
throw new SessionValidationError("session folded count is invalid");
|
|
524
|
+
const base = folded as number;
|
|
525
|
+
if (record.revision !== base + record.events.length)
|
|
476
526
|
throw new SessionValidationError(
|
|
477
527
|
"session revision does not match its event log",
|
|
478
528
|
);
|
|
@@ -487,7 +537,7 @@ function validateSessionRecord<T>(value: unknown): SessionRecord<T> {
|
|
|
487
537
|
assertSessionId(candidate.id);
|
|
488
538
|
if (
|
|
489
539
|
!Number.isSafeInteger(candidate.revision) ||
|
|
490
|
-
candidate.revision !== index + 1
|
|
540
|
+
candidate.revision !== base + index + 1
|
|
491
541
|
)
|
|
492
542
|
throw new SessionValidationError("session event revisions are invalid");
|
|
493
543
|
if (
|
|
@@ -502,7 +552,9 @@ function validateSessionRecord<T>(value: unknown): SessionRecord<T> {
|
|
|
502
552
|
)
|
|
503
553
|
throw new SessionValidationError("session event timestamp is invalid");
|
|
504
554
|
const payloadJson = stringifyJson(
|
|
505
|
-
candidate.
|
|
555
|
+
candidate.ops === undefined
|
|
556
|
+
? candidate.payload
|
|
557
|
+
: [candidate.payload, candidate.ops],
|
|
506
558
|
"session event payload",
|
|
507
559
|
);
|
|
508
560
|
if (Buffer.byteLength(payloadJson, "utf8") > MAX_EVENT_BYTES)
|
|
@@ -527,6 +579,13 @@ function validateSessionRecord<T>(value: unknown): SessionRecord<T> {
|
|
|
527
579
|
at: candidate.at,
|
|
528
580
|
};
|
|
529
581
|
if (rewind !== undefined) parsed.rewind = rewind;
|
|
582
|
+
if (candidate.actor !== undefined)
|
|
583
|
+
parsed.actor = parseEventActor(candidate.actor);
|
|
584
|
+
if (candidate.ops !== undefined) {
|
|
585
|
+
if (!Array.isArray(candidate.ops))
|
|
586
|
+
throw new SessionValidationError("session event ops are invalid");
|
|
587
|
+
parsed.ops = candidate.ops;
|
|
588
|
+
}
|
|
530
589
|
return parsed;
|
|
531
590
|
});
|
|
532
591
|
let meta: SessionMeta;
|
|
@@ -546,6 +605,7 @@ function validateSessionRecord<T>(value: unknown): SessionRecord<T> {
|
|
|
546
605
|
updatedAt: record.updatedAt,
|
|
547
606
|
composition: record.composition as T,
|
|
548
607
|
events,
|
|
608
|
+
...(base > 0 ? { folded: base } : {}),
|
|
549
609
|
meta,
|
|
550
610
|
};
|
|
551
611
|
}
|
|
@@ -579,3 +639,24 @@ function stringifyJson(value: unknown, label: string): string {
|
|
|
579
639
|
throw new SessionValidationError(`${label} must be JSON data`);
|
|
580
640
|
}
|
|
581
641
|
}
|
|
642
|
+
|
|
643
|
+
function parseEventActor(value: unknown): EventActor {
|
|
644
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
645
|
+
throw new SessionValidationError("session event actor is invalid");
|
|
646
|
+
const actor = value as Record<string, unknown>;
|
|
647
|
+
const valid =
|
|
648
|
+
typeof actor.clientId === "string" &&
|
|
649
|
+
actor.clientId.length > 0 &&
|
|
650
|
+
actor.clientId.length <= 64 &&
|
|
651
|
+
(actor.actorId === undefined ||
|
|
652
|
+
(typeof actor.actorId === "string" &&
|
|
653
|
+
/^a_[a-z2-7]{22}$/.test(actor.actorId))) &&
|
|
654
|
+
(actor.seq === undefined ||
|
|
655
|
+
(Number.isSafeInteger(actor.seq) && (actor.seq as number) >= 1));
|
|
656
|
+
if (!valid)
|
|
657
|
+
throw new SessionValidationError("session event actor is invalid");
|
|
658
|
+
const parsed: EventActor = { clientId: actor.clientId as string };
|
|
659
|
+
if (actor.actorId !== undefined) parsed.actorId = actor.actorId as string;
|
|
660
|
+
if (actor.seq !== undefined) parsed.seq = actor.seq as number;
|
|
661
|
+
return parsed;
|
|
662
|
+
}
|
package/src/tui/arrange-menu.ts
CHANGED
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
import type { Section } from "../../core/score.ts";
|
|
8
8
|
import { formatForm } from "../../core/sections.ts";
|
|
9
9
|
import { barsLabel } from "../commands/arrange.ts";
|
|
10
|
+
import { rangeLabel } from "../../core/range.ts";
|
|
10
11
|
import type { MenuContext, MenuNode } from "./menu.ts";
|
|
12
|
+
import { styleMenuDetail, styleMenuNodes } from "./style-menu.ts";
|
|
11
13
|
|
|
12
14
|
/** `3 sections · form A×2 B · loop chorus`, or how to start. */
|
|
13
15
|
export function arrangeDetail(context: MenuContext): string {
|
|
@@ -20,41 +22,171 @@ export function arrangeDetail(context: MenuContext): string {
|
|
|
20
22
|
return parts.join(" · ");
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
/** `loop 5–6 · 16 bars`, or the song length alone. */
|
|
26
|
+
export function rangeDetail(context: MenuContext): string {
|
|
24
27
|
const score = context.score;
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}));
|
|
28
|
+
const bars = `${score.bars} bar${score.bars === 1 ? "" : "s"}`;
|
|
29
|
+
return score.loop ? `loop ${rangeLabel(score.loop)} · ${bars}` : bars;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A bar range typed as `5-6` or `5`: kept as typed, else undefined. */
|
|
33
|
+
function barsText(text: string): string | undefined {
|
|
34
|
+
const value = text.trim().replace(/\s*(?:–|\.\.)\s*/u, "-");
|
|
35
|
+
return /^\d{1,4}(?:-\d{1,4})?$/u.test(value) ? value : undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Arrange › range (op1-ux §6.4): each row runs the typed range command on
|
|
40
|
+
* the focused track, so the menu, the prompt and the agent share one path.
|
|
41
|
+
*/
|
|
42
|
+
export function rangeNodes(context: MenuContext): MenuNode[] {
|
|
43
|
+
const score = context.score;
|
|
44
|
+
const track = context.trackId;
|
|
45
|
+
const nodes: MenuNode[] = [
|
|
46
|
+
{
|
|
47
|
+
kind: "entry",
|
|
48
|
+
label: "loop bars",
|
|
49
|
+
value: score.loop ? rangeLabel(score.loop) : "the song",
|
|
50
|
+
placeholder: "bars, e.g. 5-6",
|
|
51
|
+
example: "loop 5-6",
|
|
52
|
+
help: "loop these bars in playback; empty plays the song",
|
|
53
|
+
command: (text) => {
|
|
54
|
+
if (!text.trim()) return "loop off";
|
|
55
|
+
const bars = barsText(text);
|
|
56
|
+
return bars ? `loop ${bars}` : undefined;
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
if (score.loop)
|
|
61
|
+
nodes.push(
|
|
62
|
+
{
|
|
63
|
+
kind: "action",
|
|
64
|
+
label: "loop next",
|
|
65
|
+
command: "loop next",
|
|
66
|
+
help: "step the loop one length later",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
kind: "action",
|
|
70
|
+
label: "loop prev",
|
|
71
|
+
command: "loop prev",
|
|
72
|
+
help: "step the loop one length earlier",
|
|
73
|
+
},
|
|
74
|
+
);
|
|
33
75
|
nodes.push(
|
|
34
76
|
{
|
|
35
77
|
kind: "entry",
|
|
36
|
-
label: "
|
|
78
|
+
label: "copy bars",
|
|
37
79
|
value: "",
|
|
38
|
-
placeholder: "
|
|
39
|
-
example:
|
|
40
|
-
help: "
|
|
80
|
+
placeholder: "bars to bar, e.g. 5-6 to 7 x2",
|
|
81
|
+
example: `copy ${track} 5-6 to 7 x2`,
|
|
82
|
+
help: "copy this track's bars; x2 tiles, insert shifts later music",
|
|
83
|
+
command: (text) =>
|
|
84
|
+
text.trim() ? `copy ${track} ${text.trim()}` : undefined,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
kind: "entry",
|
|
88
|
+
label: "move bars",
|
|
89
|
+
value: "",
|
|
90
|
+
placeholder: "bars to bar, e.g. 5-6 to 9",
|
|
91
|
+
example: `move ${track} 5-6 to 9`,
|
|
92
|
+
help: "move this track's bars; the source empties",
|
|
93
|
+
command: (text) =>
|
|
94
|
+
text.trim() ? `move ${track} ${text.trim()}` : undefined,
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
kind: "entry",
|
|
98
|
+
label: "clear bars",
|
|
99
|
+
value: "",
|
|
100
|
+
placeholder: "bars, e.g. 5-6",
|
|
101
|
+
example: `clear ${track} 5-6`,
|
|
102
|
+
help: "empty this track's bars; the bars stay",
|
|
41
103
|
command: (text) => {
|
|
42
|
-
const
|
|
43
|
-
return
|
|
44
|
-
? `section ${trimmed}`
|
|
45
|
-
: undefined;
|
|
104
|
+
const bars = barsText(text);
|
|
105
|
+
return bars ? `clear ${track} ${bars}` : undefined;
|
|
46
106
|
},
|
|
47
107
|
},
|
|
48
108
|
{
|
|
49
109
|
kind: "entry",
|
|
50
|
-
label: "
|
|
110
|
+
label: "reverse bars",
|
|
51
111
|
value: "",
|
|
52
|
-
placeholder: "
|
|
53
|
-
example:
|
|
54
|
-
help: "
|
|
55
|
-
command: (text) =>
|
|
112
|
+
placeholder: "bars, e.g. 5-6",
|
|
113
|
+
example: `reverse ${track} 5-6`,
|
|
114
|
+
help: "mirror this track's bars in time",
|
|
115
|
+
command: (text) => {
|
|
116
|
+
const bars = barsText(text);
|
|
117
|
+
return bars ? `reverse ${track} ${bars}` : undefined;
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
kind: "entry",
|
|
122
|
+
label: "paste at",
|
|
123
|
+
value: "",
|
|
124
|
+
placeholder: "a bar, e.g. 9",
|
|
125
|
+
example: "paste at 9",
|
|
126
|
+
help: "lay the clipboard down (copy bars without `to` fills it)",
|
|
127
|
+
command: (text) =>
|
|
128
|
+
/^\d{1,4}$/u.test(text.trim()) ? `paste at ${text.trim()}` : undefined,
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
kind: "entry",
|
|
132
|
+
label: "insert bars",
|
|
133
|
+
value: "",
|
|
134
|
+
placeholder: "count at bar, e.g. 2 at 3",
|
|
135
|
+
example: "bars insert 2 at 3",
|
|
136
|
+
help: "add empty bars; later sections, clips, automation and tempo move right",
|
|
137
|
+
command: (text) =>
|
|
138
|
+
/^\d{1,3}\s+at\s+\d{1,4}$/u.test(text.trim())
|
|
139
|
+
? `bars insert ${text.trim().replace(/\s+/gu, " ")}`
|
|
140
|
+
: undefined,
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
kind: "entry",
|
|
144
|
+
label: "remove bars",
|
|
145
|
+
value: "",
|
|
146
|
+
placeholder: "bars, e.g. 5-6",
|
|
147
|
+
example: "bars remove 5-6",
|
|
148
|
+
help: "cut the bars out; later music moves left",
|
|
149
|
+
command: (text) => {
|
|
150
|
+
const bars = barsText(text);
|
|
151
|
+
return bars ? `bars remove ${bars}` : undefined;
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
kind: "entry",
|
|
156
|
+
label: "jump",
|
|
157
|
+
value: "",
|
|
158
|
+
placeholder: "bar or bar.beat, e.g. 5.3",
|
|
159
|
+
example: "jump 5.3",
|
|
160
|
+
help: "move the playhead",
|
|
161
|
+
command: (text) =>
|
|
162
|
+
/^\d{1,4}(?:\.\d{1,2})?$/u.test(text.trim())
|
|
163
|
+
? `jump ${text.trim()}`
|
|
164
|
+
: undefined,
|
|
56
165
|
},
|
|
57
166
|
);
|
|
167
|
+
return nodes;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Arrange's rows after tracks (§4a): sections (each section, mark bars,
|
|
172
|
+
* add section), then form and the style browser.
|
|
173
|
+
*/
|
|
174
|
+
export function arrangeNodes(context: MenuContext): MenuNode[] {
|
|
175
|
+
const score = context.score;
|
|
176
|
+
const count = score.sections.length;
|
|
177
|
+
const nodes: MenuNode[] = [
|
|
178
|
+
{
|
|
179
|
+
kind: "menu",
|
|
180
|
+
id: "sections",
|
|
181
|
+
label: "sections",
|
|
182
|
+
detail:
|
|
183
|
+
count === 0
|
|
184
|
+
? "none yet · mark bars"
|
|
185
|
+
: `${count} · ${score.sections.map((section) => section.name).join(", ")}`,
|
|
186
|
+
help: "mark bars, add a section, then loop, jump, mute or vary one",
|
|
187
|
+
build: sectionListNodes,
|
|
188
|
+
},
|
|
189
|
+
];
|
|
58
190
|
if (score.sections.length > 0) {
|
|
59
191
|
nodes.push({
|
|
60
192
|
kind: "entry",
|
|
@@ -68,7 +200,7 @@ export function arrangeNodes(context: MenuContext): MenuNode[] {
|
|
|
68
200
|
if (score.form.length)
|
|
69
201
|
nodes.push({
|
|
70
202
|
kind: "action",
|
|
71
|
-
label: "
|
|
203
|
+
label: "print form to tape",
|
|
72
204
|
command: "form bake",
|
|
73
205
|
help: "write the form out as plain bars, then clear it",
|
|
74
206
|
});
|
|
@@ -80,6 +212,37 @@ export function arrangeNodes(context: MenuContext): MenuNode[] {
|
|
|
80
212
|
help: "play the whole song (or form) again",
|
|
81
213
|
});
|
|
82
214
|
}
|
|
215
|
+
// TAPE (op1-ux §6): every track across the bars, copy/cut/paste keys.
|
|
216
|
+
nodes.push({
|
|
217
|
+
kind: "action",
|
|
218
|
+
label: "tape",
|
|
219
|
+
command: "/tape",
|
|
220
|
+
help: "every track across the bars · c copy, x cut, v paste · ctrl-t",
|
|
221
|
+
});
|
|
222
|
+
// §4.7: record the loop in PLAY, one undo step per pass (r / R on TAPE).
|
|
223
|
+
nodes.push({
|
|
224
|
+
kind: "action",
|
|
225
|
+
label: "record the loop",
|
|
226
|
+
command: "keys record",
|
|
227
|
+
help: "play mode over the loop · each pass is one undo step · r on tape",
|
|
228
|
+
});
|
|
229
|
+
nodes.push({
|
|
230
|
+
kind: "menu",
|
|
231
|
+
id: "range",
|
|
232
|
+
label: "range",
|
|
233
|
+
detail: rangeDetail(context),
|
|
234
|
+
help: "loop bars, copy, move, clear, paste or reverse them, insert or remove bars",
|
|
235
|
+
build: rangeNodes,
|
|
236
|
+
});
|
|
237
|
+
// The style browser: a whole song from a style (src/tui/style-menu.ts).
|
|
238
|
+
nodes.push({
|
|
239
|
+
kind: "menu",
|
|
240
|
+
id: "style",
|
|
241
|
+
label: "style",
|
|
242
|
+
detail: styleMenuDetail(context),
|
|
243
|
+
help: "make a whole song from a style: the style tree, find and blend",
|
|
244
|
+
build: styleMenuNodes,
|
|
245
|
+
});
|
|
83
246
|
return nodes;
|
|
84
247
|
}
|
|
85
248
|
|
|
@@ -388,3 +551,42 @@ export function fillNodes(section: Section): MenuNode[] {
|
|
|
388
551
|
},
|
|
389
552
|
];
|
|
390
553
|
}
|
|
554
|
+
|
|
555
|
+
/** Arrange › sections: one row per section, then mark bars and add section. */
|
|
556
|
+
function sectionListNodes(context: MenuContext): MenuNode[] {
|
|
557
|
+
const score = context.score;
|
|
558
|
+
const nodes: MenuNode[] = score.sections.map((section) => ({
|
|
559
|
+
kind: "menu",
|
|
560
|
+
id: `section:${section.name}`,
|
|
561
|
+
label: section.name,
|
|
562
|
+
detail: sectionDetail(context, section),
|
|
563
|
+
help: "loop, jump, mute, vary, move, rename, transitions",
|
|
564
|
+
build: (next) => sectionNodes(next, section.name),
|
|
565
|
+
}));
|
|
566
|
+
nodes.push(
|
|
567
|
+
{
|
|
568
|
+
kind: "entry",
|
|
569
|
+
label: "mark bars",
|
|
570
|
+
value: "",
|
|
571
|
+
placeholder: "name and bars, e.g. verse 1-8",
|
|
572
|
+
example: "section verse 1-8",
|
|
573
|
+
help: "name a bar range; re-marking a name moves its marker",
|
|
574
|
+
command: (text) => {
|
|
575
|
+
const trimmed = text.trim();
|
|
576
|
+
return /\S\s+\d{1,4}(?:[-–]\d{1,4})?$/u.test(trimmed)
|
|
577
|
+
? `section ${trimmed}`
|
|
578
|
+
: undefined;
|
|
579
|
+
},
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
kind: "entry",
|
|
583
|
+
label: "add section",
|
|
584
|
+
value: "",
|
|
585
|
+
placeholder: "name and bars, e.g. chorus 8 (empty: next name, 8 bars)",
|
|
586
|
+
example: "section add chorus 8",
|
|
587
|
+
help: "a new section after the last one (extends the song)",
|
|
588
|
+
command: (text) => `section add ${text.trim()}`.trim(),
|
|
589
|
+
},
|
|
590
|
+
);
|
|
591
|
+
return nodes;
|
|
592
|
+
}
|
package/src/tui/audition.ts
CHANGED
|
@@ -528,7 +528,7 @@ export function auditionKey(value: string): AuditionKey | undefined {
|
|
|
528
528
|
* before.
|
|
529
529
|
*/
|
|
530
530
|
const STAGEABLE =
|
|
531
|
-
/^\/?(?:(fx|effects|filter|lowpass|synth|string|bowed|modal|wind|wt|wavetable|grain|kit|instrument|vol|volume|pan|gain|speed|warpmode|root|pattern|euclid|art|articulation|bend|vibrato|glide|portamento|velcurve|vel-curve|pedal|sustain|humanize|tuning|tune|master|keys|piano|epiano|rhodes|wurli|wurlitzer|clav|clavinet)\s+\S|pack\s+use\s+\S|(?:piano|grand|upright|felt|honkytonk|prepared|ballad)(?:\s+piano)?\s*$|(?:epiano|rhodes|suitcase|dyno|wurli|wurlitzer|clav|clavinet|funkclav)\s*$|(?:tonewheel|hammond|b3|gospel|jazzorgan|combo|farfisa|vox|pipe|church|pipeorgan|churchorgan|flutes|cornet|reeds|celeste)(?:\s+\S.*)?$|rotary\s+(?:slow|fast|stop)\s*$)/i;
|
|
531
|
+
/^\/?(?:(clip|fx|effects|filter|lowpass|vocoder(?!\s+new\s*$)|formant|vowel|synth|string|bowed|modal|wind|sing|note\s+vowel|wt|wavetable|grain|kit|instrument|vol|volume|pan|gain|speed|warpmode|root|pattern|euclid|art|articulation|bend|vibrato|glide|portamento|velcurve|vel-curve|pedal|sustain|humanize|tuning|tune|master|keys|piano|epiano|rhodes|wurli|wurlitzer|clav|clavinet)\s+\S|pack\s+use\s+\S|(?:piano|grand|upright|felt|honkytonk|prepared|ballad)(?:\s+piano)?\s*$|(?:epiano|rhodes|suitcase|dyno|wurli|wurlitzer|clav|clavinet|funkclav)\s*$|(?:tonewheel|hammond|b3|gospel|jazzorgan|combo|farfisa|vox|pipe|church|pipeorgan|churchorgan|flutes|cornet|reeds|celeste)(?:\s+\S.*)?$|rotary\s+(?:slow|fast|stop)\s*$)/i;
|
|
532
532
|
/** Subcommands that list or show instead of changing the sound. */
|
|
533
533
|
const READ_ONLY = /^\/?\S+\s+(list|show|info|help|measure|meter|presets?)\s*$/i;
|
|
534
534
|
/** `master <unit>` alone only shows the unit. */
|