@hraness/dawg 0.4.1 → 0.6.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 +99 -0
- package/DAWG.md +551 -33
- package/README.md +4 -4
- package/core/chords.ts +288 -7
- package/core/diff.ts +41 -12
- package/core/expression.ts +1241 -0
- package/core/fx.ts +437 -2
- package/core/granular.ts +528 -0
- package/core/instruments.ts +281 -0
- package/core/keys.ts +386 -0
- package/core/loop.ts +23 -0
- package/core/master.ts +455 -0
- package/core/midi.ts +452 -0
- package/core/resonators.ts +574 -0
- package/core/rhythm.ts +7 -2
- package/core/score.ts +717 -62
- package/core/sdk/eval-child.ts +40 -3
- package/core/sdk/print.ts +614 -27
- package/core/sdk/sync-instruments.ts +58 -0
- package/core/sdk/v1.ts +2824 -45
- package/core/sections.ts +2072 -0
- package/core/strings.ts +845 -0
- package/core/synth.ts +11 -1
- package/core/tempo.ts +1318 -0
- package/core/tuning.ts +1180 -0
- package/guides/audition.md +26 -0
- package/guides/automation.md +26 -0
- package/guides/chords.md +28 -0
- package/guides/effects.md +29 -0
- package/guides/faders.md +29 -0
- package/guides/files.md +28 -0
- package/guides/getting-started.md +26 -0
- package/guides/index.ts +75 -0
- package/guides/keys.md +27 -0
- package/guides/media.md +27 -0
- package/guides/mix.md +23 -0
- package/guides/music.md +14 -0
- package/guides/notes.md +26 -0
- package/guides/performance.md +26 -0
- package/guides/play.md +24 -0
- package/guides/project.md +13 -0
- package/guides/providers.md +25 -0
- package/guides/rhythm.md +26 -0
- package/guides/sessions.md +21 -0
- package/guides/sound.md +15 -0
- package/guides/sounds.md +29 -0
- package/guides/tempo.md +27 -0
- package/guides/tracks.md +25 -0
- package/guides/web-search.md +21 -0
- package/package.json +3 -1
- package/src/agent/agent.ts +9 -0
- package/src/agent/brief.ts +77 -3
- package/src/agent/chord-tools.ts +9 -1
- package/src/agent/expression-tools.ts +336 -0
- package/src/agent/granular-tools.ts +138 -0
- package/src/agent/master-tools.ts +299 -0
- package/src/agent/models.ts +4 -4
- package/src/agent/ops.ts +68 -6
- package/src/agent/planner.ts +45 -2
- package/src/agent/preview-tool.ts +27 -4
- package/src/agent/section-tools.ts +411 -0
- package/src/agent/time-tools.ts +290 -0
- package/src/agent/tools.ts +574 -16
- package/src/agent/tuning-tools.ts +301 -0
- package/src/agent/xcb-agent.ts +9 -0
- package/src/audio/arrange.ts +489 -0
- package/src/audio/audition.ts +16 -3
- package/src/audio/click.ts +113 -1
- package/src/audio/clock.ts +71 -5
- package/src/audio/dsp/bank.ts +233 -0
- package/src/audio/dsp/fft.ts +6 -0
- package/src/audio/dsp/filters.ts +57 -0
- package/src/audio/dsp/interp.ts +112 -0
- package/src/audio/dsp/modal.ts +684 -0
- package/src/audio/dsp/onset.ts +197 -0
- package/src/audio/dsp/oversample.ts +202 -0
- package/src/audio/dsp/rng.ts +37 -0
- package/src/audio/dsp/shape.ts +81 -0
- package/src/audio/dsp/stft.ts +73 -0
- package/src/audio/dsp/window.ts +77 -0
- package/src/audio/effects/bus.ts +5 -4
- package/src/audio/effects/chain.ts +6 -1
- package/src/audio/effects/common.ts +30 -1
- package/src/audio/effects/dynamics.ts +2 -1
- package/src/audio/effects/filter.ts +5 -3
- package/src/audio/effects/modulation.ts +5 -1
- package/src/audio/effects/rig/cab.ts +99 -0
- package/src/audio/effects/rig/filters.ts +152 -0
- package/src/audio/effects/rig/gate.ts +39 -0
- package/src/audio/effects/rig/head.ts +487 -0
- package/src/audio/effects/rig/index.ts +170 -0
- package/src/audio/effects/rig/section.ts +78 -0
- package/src/audio/effects/rig/stomp.ts +238 -0
- package/src/audio/effects/space.ts +92 -2
- package/src/audio/engine.ts +100 -21
- package/src/audio/fit.ts +402 -0
- package/src/audio/granular.ts +664 -0
- package/src/audio/instrument-check.ts +133 -0
- package/src/audio/instruments.ts +111 -0
- package/src/audio/keys/dsp.ts +323 -0
- package/src/audio/keys/engine.ts +361 -0
- package/src/audio/keys/piano.ts +432 -0
- package/src/audio/live-worker.ts +54 -0
- package/src/audio/live.ts +263 -17
- package/src/audio/loudness.ts +596 -0
- package/src/audio/master.ts +660 -0
- package/src/audio/measure-worker.ts +45 -0
- package/src/audio/measure.ts +110 -0
- package/src/audio/player.ts +11 -6
- package/src/audio/preview.ts +76 -14
- package/src/audio/render-worker.ts +6 -1
- package/src/audio/renderer.ts +7 -1
- package/src/audio/resonators.ts +287 -0
- package/src/audio/sampler.ts +269 -34
- package/src/audio/samples.ts +30 -7
- package/src/audio/strings/body.ts +250 -0
- package/src/audio/strings/engine.ts +369 -0
- package/src/audio/strings/loop.ts +119 -0
- package/src/audio/strings/measure.test-helpers.ts +198 -0
- package/src/audio/strings/pluck.ts +354 -0
- package/src/audio/synth/voice.ts +60 -13
- package/src/audio/synth/zzfx.ts +10 -4
- package/src/audio/warp.ts +147 -0
- package/src/audio/wav.ts +377 -30
- package/src/commands/arrange.ts +949 -0
- package/src/commands/expression.ts +941 -0
- package/src/commands/fit.ts +135 -0
- package/src/commands/fx.ts +31 -0
- package/src/commands/granular.ts +427 -0
- package/src/commands/help.ts +355 -27
- package/src/commands/keys.ts +353 -0
- package/src/commands/master.ts +361 -0
- package/src/commands/modal.ts +247 -0
- package/src/commands/music.ts +6 -1
- package/src/commands/rig.ts +260 -0
- package/src/commands/sample.ts +3 -0
- package/src/commands/string.ts +175 -0
- package/src/commands/synth.ts +11 -1
- package/src/commands/time.ts +967 -0
- package/src/commands/tuning.ts +490 -0
- package/src/main.ts +919 -41
- package/src/project/check.ts +13 -0
- package/src/render.ts +139 -8
- package/src/session/daemon.ts +18 -8
- package/src/session/naming.ts +8 -1
- package/src/session/rebase.ts +21 -0
- package/src/tui/arrange-menu.ts +390 -0
- package/src/tui/audition.ts +38 -5
- package/src/tui/fader.ts +409 -0
- package/src/tui/granular-menu.ts +278 -0
- package/src/tui/menu-time.ts +401 -0
- package/src/tui/menu.ts +967 -23
- package/src/tui/modal-menu.ts +118 -0
- package/src/tui/performance-menu.ts +235 -0
- package/src/tui/play-chords.ts +3 -1
- package/src/tui/play-mode.ts +150 -6
- package/src/tui/play-session.ts +459 -43
- package/tui/app.ts +262 -13
- package/tui/arrange-strip.ts +174 -0
- package/tui/drawer.ts +478 -0
- package/tui/grammar.ts +63 -1
- package/tui/guide.ts +351 -0
- package/tui/highway.ts +87 -4
- package/tui/hits.ts +68 -0
- package/tui/input.ts +7 -0
- package/tui/keys.ts +72 -0
package/src/project/check.ts
CHANGED
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
formatDiagnostic,
|
|
9
9
|
type Diagnostic,
|
|
10
10
|
} from "../../core/sdk/eval.ts";
|
|
11
|
+
import { legacyResonatorWarnings } from "../../core/resonators.ts";
|
|
12
|
+
import { plainSineWarnings } from "../audio/instrument-check.ts";
|
|
11
13
|
import { isProject } from "./init.ts";
|
|
12
14
|
import { typecheckProject } from "./typecheck.ts";
|
|
13
15
|
|
|
@@ -23,6 +25,8 @@ export type CheckReport = Readonly<{
|
|
|
23
25
|
evalMs: number;
|
|
24
26
|
tracks: number;
|
|
25
27
|
notes: number;
|
|
28
|
+
/** Advice that does not fail the check (0.6: legacy instrument words). */
|
|
29
|
+
warnings: readonly string[];
|
|
26
30
|
}>;
|
|
27
31
|
|
|
28
32
|
/** Runs both checks on `project`; never throws for project problems. */
|
|
@@ -45,6 +49,14 @@ export async function checkProject(
|
|
|
45
49
|
evalMs: evaluated.ms,
|
|
46
50
|
tracks: evaluated.ok ? evaluated.score.tracks.length : 0,
|
|
47
51
|
notes: evaluated.ok ? evaluated.score.notes.length : 0,
|
|
52
|
+
warnings: Object.freeze(
|
|
53
|
+
evaluated.ok
|
|
54
|
+
? [
|
|
55
|
+
...legacyResonatorWarnings(evaluated.score.tracks),
|
|
56
|
+
...plainSineWarnings(evaluated.score.tracks),
|
|
57
|
+
]
|
|
58
|
+
: [],
|
|
59
|
+
),
|
|
48
60
|
});
|
|
49
61
|
}
|
|
50
62
|
|
|
@@ -65,6 +77,7 @@ export async function runCheckCommand(
|
|
|
65
77
|
const report = await checkProject(project, deps);
|
|
66
78
|
for (const diagnostic of report.diagnostics)
|
|
67
79
|
stderr.write(`${formatDiagnostic(diagnostic)}\n`);
|
|
80
|
+
for (const warning of report.warnings) stderr.write(`warning: ${warning}\n`);
|
|
68
81
|
if (report.ok) {
|
|
69
82
|
stdout.write(
|
|
70
83
|
`ok · ${report.tracks} track${report.tracks === 1 ? "" : "s"}, ${report.notes} note${
|
package/src/render.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `dawg render <out.wav
|
|
2
|
+
* `dawg render <out.wav>` (or `<out.mid>`): renders a session, the project files (`song.ts`,
|
|
3
3
|
* in a project with no `--session`), or a `track.loop/v1` file to
|
|
4
4
|
* a stereo 16-bit PCM WAV through the same deterministic renderer playback
|
|
5
5
|
* uses, so two renders of one score are byte-identical. It reads the session
|
|
@@ -8,9 +8,33 @@
|
|
|
8
8
|
import { createHash } from "node:crypto";
|
|
9
9
|
import { readFile, rename, writeFile } from "node:fs/promises";
|
|
10
10
|
import { resolve } from "node:path";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
scoreFromJSON,
|
|
13
|
+
ScoreValidationError,
|
|
14
|
+
withNoteCap,
|
|
15
|
+
type TrackScore,
|
|
16
|
+
} from "../core/score.ts";
|
|
12
17
|
import { decodeLoop } from "../core/loop.ts";
|
|
13
|
-
import {
|
|
18
|
+
import { scoreToMidi } from "../core/midi.ts";
|
|
19
|
+
import { RENDER_CHANNELS, encodeWav, withWavCues } from "./audio/wav.ts";
|
|
20
|
+
import { exportSampleRate, measureRendered } from "./audio/measure.ts";
|
|
21
|
+
import {
|
|
22
|
+
applyMasterCommand,
|
|
23
|
+
loudnessLine,
|
|
24
|
+
measurementLine,
|
|
25
|
+
parseMasterCommand,
|
|
26
|
+
} from "./commands/master.ts";
|
|
27
|
+
import {
|
|
28
|
+
BAKED_NOTE_CAP,
|
|
29
|
+
bakeTrackTime,
|
|
30
|
+
findSection,
|
|
31
|
+
sectionScore,
|
|
32
|
+
} from "../core/sections.ts";
|
|
33
|
+
import {
|
|
34
|
+
exportScore,
|
|
35
|
+
renderArrangedPcm,
|
|
36
|
+
sectionCues,
|
|
37
|
+
} from "./audio/arrange.ts";
|
|
14
38
|
import { SampleLibrary, hasSamplerTracks } from "./audio/samples.ts";
|
|
15
39
|
import {
|
|
16
40
|
PackStore,
|
|
@@ -29,7 +53,7 @@ import {
|
|
|
29
53
|
} from "./session/store.ts";
|
|
30
54
|
|
|
31
55
|
export const RENDER_USAGE =
|
|
32
|
-
"usage: dawg render <out.wav> [--session <name|id>] [--import <file.track.json>]";
|
|
56
|
+
"usage: dawg render <out.wav> [--session <name|id>] [--import <file.track.json>] [--section <name>] [--normalize <lufs|streaming|club|loud|…>] [--measure] [--rate <hz>] · <out.mid> writes MIDI";
|
|
33
57
|
|
|
34
58
|
const MAX_LOOP_FILE_BYTES = 512 * 1024;
|
|
35
59
|
|
|
@@ -50,7 +74,14 @@ export async function runRenderCommand(
|
|
|
50
74
|
const positional: string[] = [];
|
|
51
75
|
for (let index = 0; index < rest.length; index += 1) {
|
|
52
76
|
const arg = rest[index]!;
|
|
53
|
-
if (arg === "--
|
|
77
|
+
if (arg === "--measure") options.set(arg, "");
|
|
78
|
+
else if (
|
|
79
|
+
arg === "--session" ||
|
|
80
|
+
arg === "--import" ||
|
|
81
|
+
arg === "--normalize" ||
|
|
82
|
+
arg === "--rate" ||
|
|
83
|
+
arg === "--section"
|
|
84
|
+
) {
|
|
54
85
|
const value = rest[index + 1];
|
|
55
86
|
if (value === undefined || value.startsWith("--")) {
|
|
56
87
|
stderr.write(`${arg} needs a value · ${RENDER_USAGE}\n`);
|
|
@@ -64,7 +95,7 @@ export async function runRenderCommand(
|
|
|
64
95
|
} else positional.push(arg);
|
|
65
96
|
}
|
|
66
97
|
const target = positional[0];
|
|
67
|
-
if (positional.length !== 1 || !target || !/\.wav$/i.test(target)) {
|
|
98
|
+
if (positional.length !== 1 || !target || !/\.(wav|midi?)$/i.test(target)) {
|
|
68
99
|
stderr.write(`${RENDER_USAGE}\n`);
|
|
69
100
|
return 2;
|
|
70
101
|
}
|
|
@@ -77,13 +108,104 @@ export async function runRenderCommand(
|
|
|
77
108
|
);
|
|
78
109
|
return 1;
|
|
79
110
|
}
|
|
111
|
+
const only = options.get("--section");
|
|
112
|
+
if (only !== undefined) {
|
|
113
|
+
const section = findSection(score, only);
|
|
114
|
+
if (!section) {
|
|
115
|
+
const names = score.sections.map((entry) => entry.name).join(", ");
|
|
116
|
+
stderr.write(
|
|
117
|
+
`render failed · no section "${only}"${names ? ` · sections: ${names}` : ""}\n`,
|
|
118
|
+
);
|
|
119
|
+
return 1;
|
|
120
|
+
}
|
|
121
|
+
// One section as it plays when looped: its mutes and variations, no form.
|
|
122
|
+
score = withNoteCap(BAKED_NOTE_CAP, () =>
|
|
123
|
+
sectionScore(bakeTrackTime(score), section),
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
if (/\.midi?$/i.test(target)) {
|
|
127
|
+
// MIDI carries notes, not audio: the master and loudness flags do not apply.
|
|
128
|
+
const audioOnly = ["--normalize", "--measure", "--rate"].find((flag) =>
|
|
129
|
+
options.has(flag),
|
|
130
|
+
);
|
|
131
|
+
if (audioOnly) {
|
|
132
|
+
stderr.write(`render failed · ${audioOnly} applies to .wav only\n`);
|
|
133
|
+
return 2;
|
|
134
|
+
}
|
|
135
|
+
const midi = scoreToMidi(exportScore(score));
|
|
136
|
+
const midiPath = resolve(workspace, target);
|
|
137
|
+
const midiTemporary = `${midiPath}.${process.pid}.tmp`;
|
|
138
|
+
await writeFile(midiTemporary, midi);
|
|
139
|
+
await rename(midiTemporary, midiPath);
|
|
140
|
+
const midiSha = createHash("sha256").update(midi).digest("hex");
|
|
141
|
+
stdout.write(
|
|
142
|
+
`rendered · ${target} · ${midi.byteLength} bytes · ${midiSha}\n`,
|
|
143
|
+
);
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
// `--normalize` is the song master's loudness target for this export only:
|
|
147
|
+
// a named target also brings the limiter in at that target's ceiling.
|
|
148
|
+
const normalize = options.get("--normalize");
|
|
149
|
+
if (normalize !== undefined) {
|
|
150
|
+
const command = parseMasterCommand(`master target ${normalize}`);
|
|
151
|
+
const result = command && applyMasterCommand(score, command);
|
|
152
|
+
if (!result?.ok) {
|
|
153
|
+
stderr.write(
|
|
154
|
+
`render failed · --normalize takes LUFS (-40 to -3) or a target name · ${RENDER_USAGE}\n`,
|
|
155
|
+
);
|
|
156
|
+
return 2;
|
|
157
|
+
}
|
|
158
|
+
score = result.next ?? score;
|
|
159
|
+
const flipped =
|
|
160
|
+
command?.type === "master-target" ? command.flipped : undefined;
|
|
161
|
+
if (flipped !== undefined)
|
|
162
|
+
stderr.write(
|
|
163
|
+
`render · --normalize ${normalize} read as ${-flipped} LUFS\n`,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
80
166
|
let samples;
|
|
81
167
|
if (hasSamplerTracks(score)) {
|
|
82
168
|
samples = await new SampleLibrary({ projectRoot: workspace }).load(score);
|
|
83
169
|
for (const problem of samples.problems)
|
|
84
170
|
stderr.write(`sample ${problem.level} · ${problem.message}\n`);
|
|
171
|
+
// A voice that cannot load (a missing file, a pin that no longer
|
|
172
|
+
// matches) would render as silence: fail rather than write a song with
|
|
173
|
+
// a part missing.
|
|
174
|
+
const failed = samples.problems.filter((p) => p.level === "error").length;
|
|
175
|
+
if (failed > 0) {
|
|
176
|
+
stderr.write(
|
|
177
|
+
`render failed · ${failed} sample ${failed === 1 ? "voice" : "voices"} could not load · fix or re-pick ${failed === 1 ? "it" : "them"} and render again\n`,
|
|
178
|
+
);
|
|
179
|
+
return 1;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// A song with a master is a deliverable: 48 kHz unless --rate says
|
|
183
|
+
// otherwise; a plain song keeps the engine's rate, as dawg 0.4 wrote it.
|
|
184
|
+
const rateArg = options.get("--rate");
|
|
185
|
+
const sampleRate =
|
|
186
|
+
rateArg === undefined ? exportSampleRate(score) : Number(rateArg);
|
|
187
|
+
if (
|
|
188
|
+
!Number.isInteger(sampleRate) ||
|
|
189
|
+
sampleRate < 8_000 ||
|
|
190
|
+
sampleRate > 48_000
|
|
191
|
+
) {
|
|
192
|
+
stderr.write(
|
|
193
|
+
`render failed · --rate takes 8000 to 48000 Hz · ${RENDER_USAGE}\n`,
|
|
194
|
+
);
|
|
195
|
+
return 2;
|
|
85
196
|
}
|
|
86
|
-
let
|
|
197
|
+
let audio;
|
|
198
|
+
try {
|
|
199
|
+
audio = renderArrangedPcm(score, { samples, sampleRate });
|
|
200
|
+
} catch (error) {
|
|
201
|
+
if (!(error instanceof ScoreValidationError)) throw error;
|
|
202
|
+
stderr.write(`render failed · ${error.message}\n`);
|
|
203
|
+
return 1;
|
|
204
|
+
}
|
|
205
|
+
let wav = encodeWav(audio.pcm, audio.sampleRate, RENDER_CHANNELS);
|
|
206
|
+
// Section starts as cue points (a --section render is one section).
|
|
207
|
+
if (only === undefined)
|
|
208
|
+
wav = withWavCues(wav, sectionCues(score, audio.sampleRate));
|
|
87
209
|
// Pack sounds: name the packs (and CC-BY attributions) in the WAV's INFO
|
|
88
210
|
// comment and on stdout; CREDITS.md in a project keeps the attributions.
|
|
89
211
|
const refs = score.tracks.flatMap((track) =>
|
|
@@ -102,8 +224,17 @@ export async function runRenderCommand(
|
|
|
102
224
|
await writeFile(temporary, wav);
|
|
103
225
|
await rename(temporary, path);
|
|
104
226
|
const sha = createHash("sha256").update(wav).digest("hex");
|
|
105
|
-
|
|
227
|
+
const seconds = audio.frames / audio.sampleRate;
|
|
228
|
+
stdout.write(
|
|
229
|
+
`rendered · ${target} · ${seconds.toFixed(2)} s · ${wav.byteLength} bytes · ${sha}\n`,
|
|
230
|
+
);
|
|
106
231
|
if (credits) stdout.write(`credits · ${credits}\n`);
|
|
232
|
+
if (options.has("--measure"))
|
|
233
|
+
stdout.write(
|
|
234
|
+
`loudness · ${measurementLine(measureRendered(audio, false).mix, audio.master)}\n`,
|
|
235
|
+
);
|
|
236
|
+
else if (audio.master)
|
|
237
|
+
stdout.write(`loudness · ${loudnessLine(audio.master)}\n`);
|
|
107
238
|
return 0;
|
|
108
239
|
}
|
|
109
240
|
|
package/src/session/daemon.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
type ScoreOperation,
|
|
7
7
|
type TrackScore,
|
|
8
8
|
} from "../../core/score.ts";
|
|
9
|
-
import { TransportClock } from "../audio/clock.ts";
|
|
9
|
+
import { TransportClock, transportMapFor } from "../audio/clock.ts";
|
|
10
10
|
import { AudioEngine } from "../audio/engine.ts";
|
|
11
11
|
import { acquireSessionLock } from "./lock.ts";
|
|
12
12
|
import {
|
|
@@ -127,7 +127,7 @@ export class DawgDaemon {
|
|
|
127
127
|
}
|
|
128
128
|
try {
|
|
129
129
|
await this.reload();
|
|
130
|
-
this.clock.
|
|
130
|
+
this.clock.follow(this.score);
|
|
131
131
|
// We hold the daemon lock, so any socket file left here belongs to a
|
|
132
132
|
// crashed daemon and is safe to reclaim.
|
|
133
133
|
await rm(this.socketPath, { force: true });
|
|
@@ -340,7 +340,7 @@ export class DawgDaemon {
|
|
|
340
340
|
);
|
|
341
341
|
if (result.record.revision !== this.record.revision) {
|
|
342
342
|
// A file-fallback window wrote the score too; adopt everything.
|
|
343
|
-
const previousTempo = this.
|
|
343
|
+
const previousTempo = this.timeKey();
|
|
344
344
|
await this.reload();
|
|
345
345
|
this.broadcast({
|
|
346
346
|
v: 1,
|
|
@@ -454,7 +454,7 @@ export class DawgDaemon {
|
|
|
454
454
|
rebasedFrom: message.base,
|
|
455
455
|
}
|
|
456
456
|
: message.payload;
|
|
457
|
-
const previousTempo = this.
|
|
457
|
+
const previousTempo = this.timeKey();
|
|
458
458
|
try {
|
|
459
459
|
this.record = await appendSessionEvent(
|
|
460
460
|
this.paths,
|
|
@@ -528,9 +528,14 @@ export class DawgDaemon {
|
|
|
528
528
|
}
|
|
529
529
|
}
|
|
530
530
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
531
|
+
/** Tempo and tempo map, compared to tell when the transport must follow. */
|
|
532
|
+
private timeKey(): string {
|
|
533
|
+
return JSON.stringify([this.score.tempoBpm, this.score.time ?? null]);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
private afterScoreChange(previousTempo: string): void {
|
|
537
|
+
if (this.timeKey() !== previousTempo) {
|
|
538
|
+
this.clock.follow(this.score);
|
|
534
539
|
this.broadcastTransport();
|
|
535
540
|
}
|
|
536
541
|
// Gapless: a streaming engine swaps the loop at the current beat
|
|
@@ -560,6 +565,7 @@ export class DawgDaemon {
|
|
|
560
565
|
this.audio.seek(this.clock.beatAt());
|
|
561
566
|
} else if (action === "tempo" && bpm !== undefined) {
|
|
562
567
|
this.clock.setTempo(bpm);
|
|
568
|
+
this.clock.setTimeMap(transportMapFor(this.score));
|
|
563
569
|
}
|
|
564
570
|
this.broadcastTransport();
|
|
565
571
|
}
|
|
@@ -641,7 +647,7 @@ export class DawgDaemon {
|
|
|
641
647
|
}
|
|
642
648
|
return;
|
|
643
649
|
}
|
|
644
|
-
const previousTempo = this.
|
|
650
|
+
const previousTempo = this.timeKey();
|
|
645
651
|
await this.reload();
|
|
646
652
|
this.broadcast({
|
|
647
653
|
v: 1,
|
|
@@ -695,6 +701,10 @@ const OPERATION_TYPES = new Set([
|
|
|
695
701
|
"moveTrack",
|
|
696
702
|
"setKey",
|
|
697
703
|
"setMeter",
|
|
704
|
+
"setTime",
|
|
705
|
+
"setTuning",
|
|
706
|
+
"setMaster",
|
|
707
|
+
"setSections",
|
|
698
708
|
]);
|
|
699
709
|
|
|
700
710
|
/** Shape gate before the reducer, which validates every field it reads. */
|
package/src/session/naming.ts
CHANGED
|
@@ -107,7 +107,14 @@ export function musicalFingerprint(score: TrackScore): Fingerprint {
|
|
|
107
107
|
: `${role}(${instrument},${register},${density})`,
|
|
108
108
|
);
|
|
109
109
|
}
|
|
110
|
-
|
|
110
|
+
// A tuned song is named by its tuning (`pelog`), not a guessed 12-TET key.
|
|
111
|
+
const tuned = score.tuning
|
|
112
|
+
? (score.tuning.name ??
|
|
113
|
+
(score.tuning.edo ? `${score.tuning.edo}-edo` : "tuned"))
|
|
114
|
+
: undefined;
|
|
115
|
+
const key = score.key
|
|
116
|
+
? score.key.toLowerCase()
|
|
117
|
+
: (tuned?.toLowerCase() ?? estimateKey(histogram));
|
|
111
118
|
const line = [
|
|
112
119
|
`${bpm} bpm`,
|
|
113
120
|
key ?? "no key",
|
package/src/session/rebase.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
type TrackScore,
|
|
17
17
|
} from "../../core/score.ts";
|
|
18
18
|
import { rewindComposition, type Rewind } from "./delta.ts";
|
|
19
|
+
import { deepEqual } from "../../core/diff.ts";
|
|
19
20
|
|
|
20
21
|
/** How far behind a base may be and still be rebased. */
|
|
21
22
|
export const MAX_REBASE_DISTANCE = 64;
|
|
@@ -114,6 +115,26 @@ export function rebaseOperations(
|
|
|
114
115
|
if (base.beatsPerBar !== current.beatsPerBar)
|
|
115
116
|
return { ok: false, reason: "meter changed" };
|
|
116
117
|
break;
|
|
118
|
+
case "setTime":
|
|
119
|
+
if (!same(base.time, current.time))
|
|
120
|
+
return { ok: false, reason: "tempo map changed" };
|
|
121
|
+
break;
|
|
122
|
+
case "setTuning":
|
|
123
|
+
if (!same(base.tuning ?? null, current.tuning ?? null))
|
|
124
|
+
return { ok: false, reason: "tuning changed" };
|
|
125
|
+
break;
|
|
126
|
+
case "setMaster":
|
|
127
|
+
if (!deepEqual(base.master, current.master))
|
|
128
|
+
return { ok: false, reason: "master changed" };
|
|
129
|
+
break;
|
|
130
|
+
case "setSections":
|
|
131
|
+
if (
|
|
132
|
+
JSON.stringify(base.sections) !== JSON.stringify(current.sections) ||
|
|
133
|
+
JSON.stringify(base.form) !== JSON.stringify(current.form) ||
|
|
134
|
+
base.loopSection !== current.loopSection
|
|
135
|
+
)
|
|
136
|
+
return { ok: false, reason: "sections changed" };
|
|
137
|
+
break;
|
|
117
138
|
case "removeTrack": {
|
|
118
139
|
if (added.tracks.has(operation.trackId)) break;
|
|
119
140
|
if (
|