@hraness/dawg 0.6.0 → 0.7.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 +126 -0
- package/DAWG.md +655 -141
- package/README.md +27 -25
- package/core/autotune.ts +1119 -0
- package/core/chords.ts +759 -23
- package/core/clips.ts +499 -0
- package/core/diff.ts +182 -99
- package/core/expression.ts +158 -6
- package/core/fx.ts +354 -32
- package/core/granular.ts +91 -0
- package/core/instruments.ts +180 -0
- package/core/keys.ts +573 -4
- package/core/loop.ts +5 -0
- package/core/lyrics.ts +297 -0
- package/core/master.ts +3 -3
- package/core/resonators.ts +306 -4
- package/core/routing.ts +165 -0
- package/core/score.ts +1181 -16
- package/core/sdk/eval-child.ts +7 -2
- package/core/sdk/eval.ts +35 -6
- package/core/sdk/print.ts +354 -6
- package/core/sdk/sync-lyrics.ts +49 -0
- package/core/sdk/v1.ts +3160 -69
- package/core/sections.ts +298 -24
- package/core/sing.ts +815 -0
- package/core/strings.ts +237 -2
- 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/core/winds.ts +652 -0
- package/guides/agent.md +29 -0
- package/guides/arrange.md +30 -0
- package/guides/audition.md +20 -14
- package/guides/automation.md +12 -6
- package/guides/chords.md +16 -14
- package/guides/effects.md +17 -15
- 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 +12 -6
- package/guides/music.md +23 -8
- package/guides/notes.md +15 -9
- package/guides/performance.md +15 -10
- package/guides/play.md +21 -13
- package/guides/project.md +25 -8
- package/guides/providers.md +20 -14
- package/guides/resample.md +32 -0
- package/guides/rhythm.md +17 -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 -12
- package/guides/styles.md +31 -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 +5 -2
- package/src/agent/agent.ts +131 -13
- package/src/agent/calibration-tools.ts +53 -0
- package/src/agent/chord-tools.ts +153 -1
- package/src/agent/clip-tools.ts +453 -0
- package/src/agent/command-agent.ts +369 -0
- package/src/agent/drum-tools.ts +2 -2
- package/src/agent/expression-tools.ts +91 -0
- package/src/agent/gateway.ts +246 -60
- package/src/agent/models.ts +53 -12
- package/src/agent/ops.ts +15 -1
- package/src/agent/pack-tools.ts +1 -1
- package/src/agent/planner.ts +13 -0
- package/src/agent/portable-schema.ts +80 -0
- package/src/agent/preview-tool.ts +5 -1
- package/src/agent/provider.ts +22 -8
- package/src/agent/resample-tool.ts +131 -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 +307 -37
- package/src/agent/usage.ts +2 -2
- package/src/agent/voice-tools.ts +925 -0
- package/src/agent/xcb-agent.ts +16 -6
- package/src/argv.ts +38 -0
- package/src/audio/analysis.ts +253 -0
- package/src/audio/arrange.ts +58 -3
- 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/doctor.ts +86 -0
- package/src/audio/dsp/bandbank.ts +138 -0
- package/src/audio/dsp/envelope.ts +171 -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/dsp/shift.ts +256 -0
- package/src/audio/effects/chain.ts +21 -4
- package/src/audio/effects/common.ts +57 -0
- package/src/audio/effects/convolution.ts +113 -5
- package/src/audio/effects/filter.ts +48 -69
- package/src/audio/effects/formant.ts +263 -0
- package/src/audio/effects/gaze.ts +354 -0
- package/src/audio/engine.ts +206 -33
- package/src/audio/fit.ts +80 -3
- package/src/audio/granular.ts +352 -21
- package/src/audio/instrument-check.ts +67 -47
- package/src/audio/instruments.ts +36 -3
- package/src/audio/keys/calibration.ts +56 -0
- package/src/audio/keys/electric.ts +427 -0
- package/src/audio/keys/engine.ts +156 -20
- package/src/audio/keys/organ.ts +1335 -0
- package/src/audio/keys/piano.ts +72 -8
- package/src/audio/keys/sympathetic.ts +127 -0
- package/src/audio/kits.ts +135 -6
- package/src/audio/live-worker.ts +3 -1
- package/src/audio/live.ts +247 -26
- package/src/audio/native.ts +615 -0
- package/src/audio/preview.ts +45 -3
- package/src/audio/render-worker.ts +2 -0
- package/src/audio/renderer.ts +2 -0
- package/src/audio/resample.ts +286 -0
- package/src/audio/resonators.ts +10 -2
- package/src/audio/sampler.ts +252 -24
- package/src/audio/samples.ts +46 -3
- package/src/audio/sing/analysis.ts +193 -0
- package/src/audio/sing/engine.ts +949 -0
- package/src/audio/strings/body.ts +29 -16
- package/src/audio/strings/bow.ts +699 -0
- package/src/audio/strings/engine.ts +274 -22
- package/src/audio/strings/measure.test-helpers.ts +2 -0
- 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 +608 -85
- package/src/audio/winds/engine.ts +306 -0
- package/src/audio/winds/filters.ts +153 -0
- package/src/audio/winds/pitch.ts +104 -0
- package/src/audio/winds/trim.ts +104 -0
- package/src/audio/winds/trims.ts +917 -0
- package/src/audio/winds/trims1.ts +297 -0
- package/src/audio/winds/voice.ts +492 -0
- 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 +44 -29
- 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 +104 -23
- package/src/commands/formant.ts +221 -0
- package/src/commands/fx.ts +105 -33
- package/src/commands/grammar.ts +558 -0
- package/src/commands/granular.ts +7 -2
- package/src/commands/help.ts +652 -339
- package/src/commands/history.ts +18 -0
- package/src/commands/keys.ts +293 -17
- package/src/commands/modal.ts +67 -4
- 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 +130 -0
- package/src/commands/progression.ts +170 -0
- package/src/commands/resample.ts +281 -0
- package/src/commands/rhythm.ts +3 -0
- package/src/commands/rig.ts +8 -25
- package/src/commands/sample.ts +49 -2
- package/src/commands/shift.ts +119 -0
- package/src/commands/sing.ts +478 -0
- package/src/commands/string.ts +28 -2
- package/src/commands/strum.ts +485 -0
- 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 +616 -0
- package/src/commands/vocal.ts +147 -0
- package/src/commands/vocoder.ts +627 -0
- package/src/commands/wind.ts +244 -0
- package/src/fs/durable.ts +50 -0
- package/src/lang/glossary.ts +493 -0
- package/src/launch-args.ts +163 -0
- package/src/main.ts +1521 -255
- package/src/media/cli.ts +20 -3
- package/src/media/import.ts +3 -1
- package/src/project/check.ts +26 -3
- 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 +36 -1
- package/src/session/daemon.ts +3 -0
- package/src/session/meta.ts +14 -0
- package/src/session/origin.ts +154 -0
- package/src/session/port.ts +22 -4
- package/src/session/presence.ts +50 -7
- package/src/session/protocol.ts +5 -1
- package/src/session/rebase.ts +18 -5
- package/src/session/receipt.ts +258 -0
- package/src/session/store.ts +65 -43
- package/src/tui/arrange-menu.ts +65 -31
- package/src/tui/audition.ts +1 -1
- package/src/tui/euclid.ts +18 -13
- package/src/tui/fader.ts +228 -41
- package/src/tui/granular-menu.ts +2 -4
- 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 +1377 -193
- package/src/tui/modal-menu.ts +38 -11
- package/src/tui/performance-menu.ts +46 -1
- package/src/tui/play-chords.ts +108 -5
- package/src/tui/play-mode.ts +16 -1
- package/src/tui/play-session.ts +81 -16
- package/src/tui/sing-menu.ts +278 -0
- package/src/tui/style-menu.ts +104 -0
- package/src/tui/vocoder-menu.ts +244 -0
- package/src/tui/wind-menu.ts +144 -0
- package/src/version.ts +8 -0
- package/src/web/fetch.ts +115 -29
- package/tui/activity.ts +180 -9
- package/tui/app.ts +274 -40
- package/tui/clip-row.ts +132 -0
- package/tui/delight.ts +144 -0
- package/tui/drawer.ts +70 -22
- package/tui/frame-gate.ts +76 -0
- package/tui/grammar.ts +112 -63
- package/tui/guide.ts +42 -4
- package/tui/highway.ts +269 -25
- package/tui/hints.ts +192 -0
- package/tui/input.ts +60 -9
- package/tui/keys.ts +1 -1
- package/tui/play-strip.ts +68 -14
- package/tui/prompt.ts +1 -1
- package/tui/screen.ts +144 -14
- package/tui/theme.ts +27 -0
package/core/diff.ts
CHANGED
|
@@ -10,10 +10,6 @@
|
|
|
10
10
|
* canonical order, so insertion order never matters).
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {
|
|
14
|
-
NOTE_EXPRESSION_FIELDS,
|
|
15
|
-
TRACK_PERFORMANCE_FIELDS,
|
|
16
|
-
} from "./expression.ts";
|
|
17
13
|
import {
|
|
18
14
|
applyScoreOperation,
|
|
19
15
|
TrackScore,
|
|
@@ -31,29 +27,109 @@ export class DiffError extends Error {
|
|
|
31
27
|
}
|
|
32
28
|
}
|
|
33
29
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
|
|
30
|
+
/**
|
|
31
|
+
* How each field diffs: `same` is identity (never patched), `set` patches
|
|
32
|
+
* the new value, `clear` patches the new value or null, `zero` treats
|
|
33
|
+
* absent as 0, `flag` treats absent as false and `lane` treats absent as
|
|
34
|
+
* []. The records are keyed by every field of `Note` and `Track`, so a new
|
|
35
|
+
* field fails typechecking until it is classified here.
|
|
36
|
+
*/
|
|
37
|
+
type FieldRule = "same" | "set" | "clear" | "zero" | "flag" | "lane";
|
|
38
|
+
|
|
39
|
+
const NOTE_FIELDS: { readonly [K in keyof Note]-?: FieldRule } = {
|
|
40
|
+
id: "same",
|
|
41
|
+
trackId: "same",
|
|
42
|
+
startTick: "set",
|
|
43
|
+
durationTicks: "set",
|
|
44
|
+
pitch: "set",
|
|
45
|
+
velocity: "set",
|
|
46
|
+
cents: "zero",
|
|
47
|
+
lyric: "clear",
|
|
48
|
+
vowel: "clear",
|
|
49
|
+
articulation: "clear",
|
|
50
|
+
glide: "clear",
|
|
51
|
+
bend: "clear",
|
|
52
|
+
vibrato: "clear",
|
|
53
|
+
humanize: "clear",
|
|
54
|
+
drift: "clear",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const TRACK_FIELDS: { readonly [K in keyof Track]-?: FieldRule } = {
|
|
58
|
+
id: "same",
|
|
59
|
+
name: "set",
|
|
60
|
+
instrument: "set",
|
|
61
|
+
muted: "set",
|
|
62
|
+
volume: "set",
|
|
63
|
+
pan: "set",
|
|
64
|
+
solo: "flag",
|
|
65
|
+
volumeAutomation: "lane",
|
|
66
|
+
panAutomation: "lane",
|
|
67
|
+
filterAutomation: "lane",
|
|
68
|
+
resonanceAutomation: "lane",
|
|
69
|
+
delayFeedbackAutomation: "lane",
|
|
70
|
+
delayMixAutomation: "lane",
|
|
71
|
+
wtAutomation: "lane",
|
|
72
|
+
filter: "clear",
|
|
73
|
+
delay: "clear",
|
|
74
|
+
reverb: "clear",
|
|
75
|
+
fx: "clear",
|
|
76
|
+
fxAutomation: "clear",
|
|
77
|
+
synth: "clear",
|
|
78
|
+
wavetable: "clear",
|
|
79
|
+
sampler: "clear",
|
|
80
|
+
rhythm: "clear",
|
|
81
|
+
kit: "clear",
|
|
82
|
+
time: "clear",
|
|
83
|
+
tuning: "clear",
|
|
84
|
+
string: "clear",
|
|
85
|
+
granular: "clear",
|
|
86
|
+
keys: "clear",
|
|
87
|
+
modal: "clear",
|
|
88
|
+
softPedal: "clear",
|
|
89
|
+
sostenuto: "clear",
|
|
90
|
+
guitar: "clear",
|
|
91
|
+
wind: "clear",
|
|
92
|
+
sing: "clear",
|
|
93
|
+
vocoder: "clear",
|
|
94
|
+
clips: "clear",
|
|
95
|
+
takes: "clear",
|
|
96
|
+
autotune: "clear",
|
|
97
|
+
glide: "clear",
|
|
98
|
+
pedal: "clear",
|
|
99
|
+
velocityCurve: "clear",
|
|
100
|
+
humanize: "clear",
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/** Field names of `Note` and `Track`, for tests that check completeness. */
|
|
104
|
+
export const NOTE_FIELD_NAMES = Object.freeze(
|
|
105
|
+
Object.keys(NOTE_FIELDS),
|
|
106
|
+
) as readonly (keyof Note)[];
|
|
107
|
+
export const TRACK_FIELD_NAMES = Object.freeze(
|
|
108
|
+
Object.keys(TRACK_FIELDS),
|
|
109
|
+
) as readonly (keyof Track)[];
|
|
110
|
+
|
|
111
|
+
function fieldPatch(
|
|
112
|
+
rules: Readonly<Record<string, FieldRule>>,
|
|
113
|
+
a: Readonly<Record<string, unknown>>,
|
|
114
|
+
b: Readonly<Record<string, unknown>>,
|
|
115
|
+
): Record<string, unknown> | undefined {
|
|
116
|
+
const patch: Record<string, unknown> = {};
|
|
117
|
+
for (const [key, rule] of Object.entries(rules)) {
|
|
118
|
+
const before = a[key];
|
|
119
|
+
const after = b[key];
|
|
120
|
+
if (rule === "same") continue;
|
|
121
|
+
if (rule === "set") {
|
|
122
|
+
if (!deepEqual(before, after)) patch[key] = after;
|
|
123
|
+
} else if (rule === "zero") {
|
|
124
|
+
if (!deepEqual(before ?? 0, after ?? 0)) patch[key] = after ?? 0;
|
|
125
|
+
} else if (rule === "flag") {
|
|
126
|
+
if ((before ?? false) !== (after ?? false)) patch[key] = after ?? false;
|
|
127
|
+
} else if (rule === "lane") {
|
|
128
|
+
if (!deepEqual(before ?? [], after ?? [])) patch[key] = after ?? [];
|
|
129
|
+
} else if (!deepEqual(before, after)) patch[key] = after ?? null;
|
|
130
|
+
}
|
|
131
|
+
return Object.keys(patch).length > 0 ? patch : undefined;
|
|
132
|
+
}
|
|
57
133
|
|
|
58
134
|
/** Operations turning `a` into `b`; empty when they are equal. */
|
|
59
135
|
export function diffScores(
|
|
@@ -64,31 +140,7 @@ export function diffScores(
|
|
|
64
140
|
throw new DiffError(
|
|
65
141
|
`ticksPerBeat differs (${a.ticksPerBeat} → ${b.ticksPerBeat}); the session resolution is fixed`,
|
|
66
142
|
);
|
|
67
|
-
const ops: ScoreOperation[] = [];
|
|
68
|
-
if (a.beatsPerBar !== b.beatsPerBar)
|
|
69
|
-
ops.push({ type: "setMeter", beatsPerBar: b.beatsPerBar });
|
|
70
|
-
if (a.bars !== b.bars) ops.push({ type: "setBars", bars: b.bars });
|
|
71
|
-
if (a.tempoBpm !== b.tempoBpm)
|
|
72
|
-
ops.push({ type: "setTempo", tempoBpm: b.tempoBpm });
|
|
73
|
-
if (a.key !== b.key) ops.push({ type: "setKey", key: b.key });
|
|
74
|
-
if (!deepEqual(a.time, b.time))
|
|
75
|
-
ops.push({ type: "setTime", time: b.time ?? null });
|
|
76
|
-
if (!deepEqual(a.tuning, b.tuning))
|
|
77
|
-
ops.push({ type: "setTuning", tuning: b.tuning ?? null });
|
|
78
|
-
if (!deepEqual(a.master, b.master))
|
|
79
|
-
ops.push({ type: "setMaster", master: b.master ?? null });
|
|
80
|
-
if (
|
|
81
|
-
!deepEqual(a.sections, b.sections) ||
|
|
82
|
-
!deepEqual(a.form, b.form) ||
|
|
83
|
-
a.loopSection !== b.loopSection
|
|
84
|
-
)
|
|
85
|
-
ops.push({
|
|
86
|
-
type: "setSections",
|
|
87
|
-
sections: b.sections,
|
|
88
|
-
form: b.form,
|
|
89
|
-
...(b.loopSection === undefined ? {} : { loopSection: b.loopSection }),
|
|
90
|
-
});
|
|
91
|
-
|
|
143
|
+
const ops: ScoreOperation[] = [...songOperations(a, b)];
|
|
92
144
|
const aTracks = new Map(a.tracks.map((track) => [track.id, track]));
|
|
93
145
|
const bTracks = new Map(b.tracks.map((track) => [track.id, track]));
|
|
94
146
|
const aNotes = new Map(a.notes.map((note) => [note.id, note]));
|
|
@@ -96,32 +148,26 @@ export function diffScores(
|
|
|
96
148
|
|
|
97
149
|
const updates: ScoreOperation[] = [];
|
|
98
150
|
const additions: ScoreOperation[] = [];
|
|
151
|
+
// Notes that leave `a`: dropped with their track, or removed one by one
|
|
152
|
+
// (a note may name a track the score lacks, so check `a` too).
|
|
153
|
+
const gone = new Set<string>();
|
|
99
154
|
for (const note of a.notes) {
|
|
100
|
-
if (!bTracks.has(note.trackId))
|
|
155
|
+
if (aTracks.has(note.trackId) && !bTracks.has(note.trackId)) {
|
|
156
|
+
gone.add(note.id); // removed with its track
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
101
159
|
const next = bNotes.get(note.id);
|
|
102
160
|
if (!next || next.trackId !== note.trackId) {
|
|
103
161
|
ops.push({ type: "removeNote", noteId: note.id });
|
|
162
|
+
gone.add(note.id);
|
|
104
163
|
continue;
|
|
105
164
|
}
|
|
106
165
|
const patch = notePatch(note, next);
|
|
107
166
|
if (patch) updates.push({ type: "updateNote", noteId: note.id, patch });
|
|
108
167
|
}
|
|
109
|
-
for (const note of b.notes)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
previous &&
|
|
113
|
-
previous.trackId === note.trackId &&
|
|
114
|
-
bTracks.has(note.trackId)
|
|
115
|
-
)
|
|
116
|
-
continue;
|
|
117
|
-
if (
|
|
118
|
-
previous &&
|
|
119
|
-
previous.trackId === note.trackId &&
|
|
120
|
-
!aTracks.has(note.trackId)
|
|
121
|
-
)
|
|
122
|
-
continue;
|
|
123
|
-
additions.push({ type: "addNote", note });
|
|
124
|
-
}
|
|
168
|
+
for (const note of b.notes)
|
|
169
|
+
if (!aNotes.has(note.id) || gone.has(note.id))
|
|
170
|
+
additions.push({ type: "addNote", note });
|
|
125
171
|
|
|
126
172
|
for (const track of a.tracks)
|
|
127
173
|
if (!bTracks.has(track.id))
|
|
@@ -151,6 +197,69 @@ export function diffScores(
|
|
|
151
197
|
return Object.freeze(ops);
|
|
152
198
|
}
|
|
153
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Song settings, ordered so every step is valid on its own: constraints
|
|
202
|
+
* that go away are dropped first (sections before meter changes, fermatas
|
|
203
|
+
* before the tempo or meter that time them) and constraints that arrive
|
|
204
|
+
* are added last. Steps are checked on a copy without tracks or notes,
|
|
205
|
+
* because song settings never depend on them.
|
|
206
|
+
*/
|
|
207
|
+
function songOperations(a: TrackScore, b: TrackScore): ScoreOperation[] {
|
|
208
|
+
const ops: ScoreOperation[] = [];
|
|
209
|
+
let song = new TrackScore({ ...a.toJSON(), tracks: [], notes: [] });
|
|
210
|
+
const emit = (operation: ScoreOperation): void => {
|
|
211
|
+
song = applyScoreOperation(song, operation);
|
|
212
|
+
ops.push(operation);
|
|
213
|
+
};
|
|
214
|
+
const sectionsDiffer = (): boolean =>
|
|
215
|
+
!deepEqual(song.sections, b.sections) ||
|
|
216
|
+
!deepEqual(song.form, b.form) ||
|
|
217
|
+
song.loopSection !== b.loopSection;
|
|
218
|
+
const setSections = (): void =>
|
|
219
|
+
emit({
|
|
220
|
+
type: "setSections",
|
|
221
|
+
sections: b.sections,
|
|
222
|
+
form: b.form,
|
|
223
|
+
...(b.loopSection === undefined ? {} : { loopSection: b.loopSection }),
|
|
224
|
+
});
|
|
225
|
+
if (b.sections.length === 0 && sectionsDiffer()) setSections();
|
|
226
|
+
const retimed =
|
|
227
|
+
song.beatsPerBar !== b.beatsPerBar ||
|
|
228
|
+
song.bars !== b.bars ||
|
|
229
|
+
song.tempoBpm !== b.tempoBpm;
|
|
230
|
+
if (retimed && song.time?.fermatas && !deepEqual(song.time, b.time)) {
|
|
231
|
+
const { fermatas: _fermatas, ...rest } = b.time ?? {};
|
|
232
|
+
const interim = Object.keys(rest).length > 0 ? rest : null;
|
|
233
|
+
if (!deepEqual(song.time, interim ?? undefined))
|
|
234
|
+
emit({ type: "setTime", time: interim });
|
|
235
|
+
} else if (retimed && song.time?.fermatas) {
|
|
236
|
+
// Same fermatas before and after, but timed by a new tempo or meter.
|
|
237
|
+
const { fermatas: _fermatas, ...rest } = song.time;
|
|
238
|
+
emit({
|
|
239
|
+
type: "setTime",
|
|
240
|
+
time: Object.keys(rest).length > 0 ? rest : null,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
if (song.beatsPerBar !== b.beatsPerBar)
|
|
244
|
+
emit({ type: "setMeter", beatsPerBar: b.beatsPerBar });
|
|
245
|
+
if (song.bars !== b.bars) emit({ type: "setBars", bars: b.bars });
|
|
246
|
+
if (song.tempoBpm !== b.tempoBpm)
|
|
247
|
+
emit({ type: "setTempo", tempoBpm: b.tempoBpm });
|
|
248
|
+
if (song.key !== b.key) emit({ type: "setKey", key: b.key });
|
|
249
|
+
if (!deepEqual(song.time, b.time))
|
|
250
|
+
emit({ type: "setTime", time: b.time ?? null });
|
|
251
|
+
if (!deepEqual(song.tuning, b.tuning))
|
|
252
|
+
emit({ type: "setTuning", tuning: b.tuning ?? null });
|
|
253
|
+
if (!deepEqual(song.master, b.master))
|
|
254
|
+
emit({ type: "setMaster", master: b.master ?? null });
|
|
255
|
+
if ((song.calibration ?? 0) !== (b.calibration ?? 0))
|
|
256
|
+
emit({ type: "setCalibration", calibration: b.calibration ?? null });
|
|
257
|
+
if (!deepEqual(song.style, b.style))
|
|
258
|
+
emit({ type: "setStyle", style: b.style ?? null });
|
|
259
|
+
if (sectionsDiffer()) setSections();
|
|
260
|
+
return ops;
|
|
261
|
+
}
|
|
262
|
+
|
|
154
263
|
/** Applies operations in order; the inverse of `diffScores`. */
|
|
155
264
|
export function applyScoreOperations(
|
|
156
265
|
score: TrackScore,
|
|
@@ -160,43 +269,17 @@ export function applyScoreOperations(
|
|
|
160
269
|
}
|
|
161
270
|
|
|
162
271
|
function notePatch(a: Note, b: Note): NotePatch | undefined {
|
|
163
|
-
|
|
164
|
-
for (const key of [
|
|
165
|
-
"startTick",
|
|
166
|
-
"durationTicks",
|
|
167
|
-
"pitch",
|
|
168
|
-
"velocity",
|
|
169
|
-
] as const)
|
|
170
|
-
if (a[key] !== b[key]) patch[key] = b[key];
|
|
171
|
-
for (const key of NOTE_EXPRESSION_FIELDS)
|
|
172
|
-
if (!deepEqual(a[key], b[key])) patch[key] = b[key] ?? null;
|
|
173
|
-
if ((a.cents ?? 0) !== (b.cents ?? 0)) patch.cents = b.cents ?? 0;
|
|
174
|
-
return Object.keys(patch).length > 0 ? (patch as NotePatch) : undefined;
|
|
272
|
+
return fieldPatch(NOTE_FIELDS, a, b) as NotePatch | undefined;
|
|
175
273
|
}
|
|
176
274
|
|
|
177
275
|
function trackPatch(a: Track, b: Track): TrackPatch | undefined {
|
|
178
|
-
|
|
179
|
-
for (const key of ["name", "instrument", "muted", "volume", "pan"] as const)
|
|
180
|
-
if (a[key] !== b[key]) patch[key] = b[key];
|
|
181
|
-
if ((a.solo ?? false) !== (b.solo ?? false)) patch.solo = b.solo ?? false;
|
|
182
|
-
for (const key of EFFECTS)
|
|
183
|
-
if (!deepEqual(a[key], b[key])) patch[key] = b[key] ?? null;
|
|
184
|
-
for (const key of [
|
|
185
|
-
"rhythm",
|
|
186
|
-
"kit",
|
|
187
|
-
"time",
|
|
188
|
-
"tuning",
|
|
189
|
-
...TRACK_PERFORMANCE_FIELDS,
|
|
190
|
-
] as const)
|
|
191
|
-
if (!deepEqual(a[key], b[key])) patch[key] = b[key] ?? null;
|
|
192
|
-
for (const key of LANES)
|
|
193
|
-
if (!deepEqual(a[key] ?? [], b[key] ?? [])) patch[key] = b[key] ?? [];
|
|
194
|
-
return Object.keys(patch).length > 0 ? (patch as TrackPatch) : undefined;
|
|
276
|
+
return fieldPatch(TRACK_FIELDS, a, b) as TrackPatch | undefined;
|
|
195
277
|
}
|
|
196
278
|
|
|
197
279
|
/** Structural equality for plain JSON-like data. */
|
|
198
280
|
export function deepEqual(a: unknown, b: unknown): boolean {
|
|
199
|
-
|
|
281
|
+
// === so -0 equals 0: a printed song.ts reads -0 back as 0.
|
|
282
|
+
if (a === b) return true;
|
|
200
283
|
if (
|
|
201
284
|
typeof a !== "object" ||
|
|
202
285
|
typeof b !== "object" ||
|
package/core/expression.ts
CHANGED
|
@@ -125,6 +125,11 @@ export type NoteExpression = Readonly<{
|
|
|
125
125
|
* track's humanize seed, 1 when it has none). `{}` keeps the note exact.
|
|
126
126
|
*/
|
|
127
127
|
humanize?: NoteHumanize;
|
|
128
|
+
/**
|
|
129
|
+
* Optional (0.7): on an autotune guide note, the share of slow pitch drift
|
|
130
|
+
* removed (0..1), overriding the track's `autotune.drift` for this note.
|
|
131
|
+
*/
|
|
132
|
+
drift?: number;
|
|
128
133
|
}>;
|
|
129
134
|
|
|
130
135
|
/** Per-note humanize: amounts as in `Humanize`, absent ones 0. */
|
|
@@ -141,6 +146,7 @@ export type NoteExpressionPatch = Readonly<{
|
|
|
141
146
|
bend?: readonly BendPoint[] | null;
|
|
142
147
|
vibrato?: NoteVibrato | null;
|
|
143
148
|
humanize?: NoteHumanize | null;
|
|
149
|
+
drift?: number | null;
|
|
144
150
|
}>;
|
|
145
151
|
|
|
146
152
|
export const NOTE_EXPRESSION_FIELDS = [
|
|
@@ -149,6 +155,7 @@ export const NOTE_EXPRESSION_FIELDS = [
|
|
|
149
155
|
"bend",
|
|
150
156
|
"vibrato",
|
|
151
157
|
"humanize",
|
|
158
|
+
"drift",
|
|
152
159
|
] as const;
|
|
153
160
|
|
|
154
161
|
export const GLIDE_MODES = ["legato", "mono", "poly"] as const;
|
|
@@ -167,6 +174,8 @@ export type TrackGlide = Readonly<{ time: number; mode: GlideMode }>;
|
|
|
167
174
|
export const DEFAULT_GLIDE_SECONDS = 0.06;
|
|
168
175
|
|
|
169
176
|
export const PEDAL_STATES = ["down", "half", "up"] as const;
|
|
177
|
+
/** Sostenuto has no half position (0.6.1). */
|
|
178
|
+
export const SOSTENUTO_STATES = ["down", "up"] as const;
|
|
170
179
|
export type PedalState = (typeof PEDAL_STATES)[number];
|
|
171
180
|
|
|
172
181
|
/** A sustain pedal (MIDI CC64) change at a tick. */
|
|
@@ -323,6 +332,12 @@ export function normalizeVibrato(value: unknown): NoteVibrato | undefined {
|
|
|
323
332
|
return Object.freeze({ rate, depth, ...(delay > 0 ? { delay } : {}) });
|
|
324
333
|
}
|
|
325
334
|
|
|
335
|
+
/** A guide note's autotune drift share (0.7), 0..1. */
|
|
336
|
+
export function normalizeNoteDrift(value: unknown): number | undefined {
|
|
337
|
+
if (value === undefined || value === null) return undefined;
|
|
338
|
+
return finite(value, "note drift", 0, 1);
|
|
339
|
+
}
|
|
340
|
+
|
|
326
341
|
/** Validates a note's expression fields; only the set ones are returned. */
|
|
327
342
|
export function normalizeNoteExpression(
|
|
328
343
|
input: Readonly<Record<string, unknown>>,
|
|
@@ -332,12 +347,14 @@ export function normalizeNoteExpression(
|
|
|
332
347
|
const bend = normalizeBend(input.bend);
|
|
333
348
|
const vibrato = normalizeVibrato(input.vibrato);
|
|
334
349
|
const humanize = normalizeNoteHumanize(input.humanize);
|
|
350
|
+
const drift = normalizeNoteDrift(input.drift);
|
|
335
351
|
return {
|
|
336
352
|
...(articulation ? { articulation } : {}),
|
|
337
353
|
...(glide !== undefined ? { glide } : {}),
|
|
338
354
|
...(bend ? { bend } : {}),
|
|
339
355
|
...(vibrato ? { vibrato } : {}),
|
|
340
356
|
...(humanize ? { humanize } : {}),
|
|
357
|
+
...(drift !== undefined ? { drift } : {}),
|
|
341
358
|
};
|
|
342
359
|
}
|
|
343
360
|
|
|
@@ -386,16 +403,18 @@ export function normalizeTrackGlide(value: unknown): TrackGlide | undefined {
|
|
|
386
403
|
export function normalizePedal(
|
|
387
404
|
value: unknown,
|
|
388
405
|
maxTick: number,
|
|
406
|
+
label = "pedal",
|
|
407
|
+
states: readonly PedalState[] = PEDAL_STATES,
|
|
389
408
|
): readonly PedalEvent[] | undefined {
|
|
390
409
|
if (value === undefined || value === null) return undefined;
|
|
391
410
|
if (!Array.isArray(value) || value.length > EXPRESSION_LIMITS.maxPedalEvents)
|
|
392
411
|
throw new ExpressionValidationError(
|
|
393
|
-
|
|
412
|
+
`${label} must be an array of at most ${EXPRESSION_LIMITS.maxPedalEvents} events`,
|
|
394
413
|
);
|
|
395
414
|
const byTick = new Map<number, PedalEvent>();
|
|
396
415
|
for (const item of value as unknown[]) {
|
|
397
|
-
const event = record(item,
|
|
398
|
-
onlyKeys(event, ["tick", "state"],
|
|
416
|
+
const event = record(item, `${label} event`);
|
|
417
|
+
onlyKeys(event, ["tick", "state"], `${label} event`);
|
|
399
418
|
const tick = event.tick;
|
|
400
419
|
if (
|
|
401
420
|
typeof tick !== "number" ||
|
|
@@ -404,11 +423,11 @@ export function normalizePedal(
|
|
|
404
423
|
tick > maxTick
|
|
405
424
|
)
|
|
406
425
|
throw new ExpressionValidationError(
|
|
407
|
-
|
|
426
|
+
`${label} tick must be an integer 0..${maxTick}`,
|
|
408
427
|
);
|
|
409
|
-
if (!
|
|
428
|
+
if (!states.includes(event.state as PedalState))
|
|
410
429
|
throw new ExpressionValidationError(
|
|
411
|
-
|
|
430
|
+
`${label} state must be one of ${states.join(", ")}`,
|
|
412
431
|
);
|
|
413
432
|
// The last event at a tick wins.
|
|
414
433
|
byTick.set(tick, Object.freeze({ tick, state: event.state as PedalState }));
|
|
@@ -551,6 +570,80 @@ export function pedalStateAt(
|
|
|
551
570
|
return state;
|
|
552
571
|
}
|
|
553
572
|
|
|
573
|
+
/** A stretch of song ticks `[from, to)` placed at `offset` in a slice. */
|
|
574
|
+
export type PedalPiece = Readonly<{ from: number; to: number; offset: number }>;
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Pedal events for a slice made of `pieces` (a render window, a section
|
|
578
|
+
* pass, an audition region): the state in force at each piece's start is
|
|
579
|
+
* restated at its offset (`carry`, for sustain and una corda; sostenuto
|
|
580
|
+
* passes false, since a pedal already down latches no key struck later),
|
|
581
|
+
* events inside the piece move by `offset - from`, and a pedal still down
|
|
582
|
+
* at the piece's end is lifted there. Undefined when nothing is left.
|
|
583
|
+
*/
|
|
584
|
+
export function slicePedal(
|
|
585
|
+
events: readonly PedalEvent[] | undefined,
|
|
586
|
+
pieces: readonly PedalPiece[],
|
|
587
|
+
carry = true,
|
|
588
|
+
): readonly PedalEvent[] | undefined {
|
|
589
|
+
if (!events || events.length === 0) return events;
|
|
590
|
+
const byTick = new Map<number, PedalEvent>();
|
|
591
|
+
const put = (tick: number, state: PedalState) =>
|
|
592
|
+
byTick.set(tick, Object.freeze({ tick, state }));
|
|
593
|
+
for (const { from, to, offset } of pieces) {
|
|
594
|
+
let state: PedalState = "up";
|
|
595
|
+
if (carry)
|
|
596
|
+
for (const event of events) {
|
|
597
|
+
if (event.tick >= from) break;
|
|
598
|
+
state = event.state;
|
|
599
|
+
}
|
|
600
|
+
// Restate up too: the previous piece may have ended with it down.
|
|
601
|
+
if (state !== "up" || byTick.size > 0) put(offset, state);
|
|
602
|
+
for (const event of events) {
|
|
603
|
+
if (event.tick < from || event.tick >= to) continue;
|
|
604
|
+
put(event.tick - from + offset, event.state);
|
|
605
|
+
state = event.state;
|
|
606
|
+
}
|
|
607
|
+
if (state !== "up" && Number.isFinite(to)) put(offset + to - from, "up");
|
|
608
|
+
}
|
|
609
|
+
const out = [...byTick.values()].sort((a, b) => a.tick - b.tick);
|
|
610
|
+
// Drop events that restate the state already in force.
|
|
611
|
+
const kept: PedalEvent[] = [];
|
|
612
|
+
let last: PedalState = "up";
|
|
613
|
+
for (const event of out)
|
|
614
|
+
if (event.state !== last) {
|
|
615
|
+
kept.push(event);
|
|
616
|
+
last = event.state;
|
|
617
|
+
}
|
|
618
|
+
return kept.length > 0 ? Object.freeze(kept) : undefined;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/** The track with its three pedal lanes cut to `pieces` (see slicePedal). */
|
|
622
|
+
export function slicePedals<T extends PedalTrack>(
|
|
623
|
+
track: T,
|
|
624
|
+
pieces: readonly PedalPiece[],
|
|
625
|
+
): T {
|
|
626
|
+
if (!track.pedal && !track.softPedal && !track.sostenuto) return track;
|
|
627
|
+
const out: Record<string, unknown> = { ...track };
|
|
628
|
+
for (const [field, carry] of [
|
|
629
|
+
["pedal", true],
|
|
630
|
+
["softPedal", true],
|
|
631
|
+
["sostenuto", false],
|
|
632
|
+
] as const) {
|
|
633
|
+
if (!track[field]) continue;
|
|
634
|
+
const next = slicePedal(track[field], pieces, carry);
|
|
635
|
+
if (next) out[field] = next;
|
|
636
|
+
else delete out[field];
|
|
637
|
+
}
|
|
638
|
+
return out as T;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
type PedalTrack = Readonly<{
|
|
642
|
+
pedal?: readonly PedalEvent[];
|
|
643
|
+
softPedal?: readonly PedalEvent[];
|
|
644
|
+
sostenuto?: readonly PedalEvent[];
|
|
645
|
+
}>;
|
|
646
|
+
|
|
554
647
|
// ---------------------------------------------------------------------------
|
|
555
648
|
// Performance
|
|
556
649
|
|
|
@@ -674,6 +767,7 @@ export function hasTrackPerformance(track: Track | undefined): boolean {
|
|
|
674
767
|
track !== undefined &&
|
|
675
768
|
(track.glide !== undefined ||
|
|
676
769
|
track.pedal !== undefined ||
|
|
770
|
+
track.sostenuto !== undefined ||
|
|
677
771
|
track.velocityCurve !== undefined ||
|
|
678
772
|
track.humanize !== undefined)
|
|
679
773
|
);
|
|
@@ -869,6 +963,64 @@ export function performNotes(
|
|
|
869
963
|
};
|
|
870
964
|
});
|
|
871
965
|
}
|
|
966
|
+
// 3b. Sostenuto (0.6.1): a key held (written length after articulation)
|
|
967
|
+
// when the sostenuto goes down keeps its damper up until the sostenuto
|
|
968
|
+
// lifts; keys struck later damp at their own release (or the sustain
|
|
969
|
+
// pedal's). The rod keeps a caught damper raised, so the same pitch
|
|
970
|
+
// struck again while that sostenuto is down rings to the lift too; the
|
|
971
|
+
// earlier instance stops at the restrike so voices do not pile up. A key
|
|
972
|
+
// still held through a lift and a new press is caught again.
|
|
973
|
+
const sostenuto = track?.sostenuto;
|
|
974
|
+
if (sostenuto) {
|
|
975
|
+
const spans: { down: number; lift: number; pitches: Set<number> }[] = [];
|
|
976
|
+
let open: number | undefined;
|
|
977
|
+
for (const event of sostenuto) {
|
|
978
|
+
if (event.state === "down" && open === undefined) open = event.tick;
|
|
979
|
+
else if (event.state === "up" && open !== undefined) {
|
|
980
|
+
spans.push({ down: open, lift: event.tick, pitches: new Set() });
|
|
981
|
+
open = undefined;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
if (open !== undefined)
|
|
985
|
+
spans.push({ down: open, lift: timing.endTick, pitches: new Set() });
|
|
986
|
+
const caughtBy = working.map((item) => {
|
|
987
|
+
const keyUp = item.start + item.bendLength;
|
|
988
|
+
return spans.filter((span) => {
|
|
989
|
+
const held = item.start <= span.down && span.down < keyUp;
|
|
990
|
+
if (held) span.pitches.add(item.note.pitch);
|
|
991
|
+
return held;
|
|
992
|
+
});
|
|
993
|
+
});
|
|
994
|
+
working = working.map((item, index) => {
|
|
995
|
+
const keyUp = item.start + item.bendLength;
|
|
996
|
+
let lift = -Infinity;
|
|
997
|
+
for (const span of caughtBy[index]!) lift = Math.max(lift, span.lift);
|
|
998
|
+
for (const span of spans)
|
|
999
|
+
if (
|
|
1000
|
+
span.pitches.has(item.note.pitch) &&
|
|
1001
|
+
span.down < item.start &&
|
|
1002
|
+
item.start < span.lift
|
|
1003
|
+
)
|
|
1004
|
+
lift = Math.max(lift, span.lift);
|
|
1005
|
+
if (lift === -Infinity) return item;
|
|
1006
|
+
const current = item.start + item.duration;
|
|
1007
|
+
let end = Math.max(current, Math.min(lift, timing.endTick));
|
|
1008
|
+
if (end <= current) return item;
|
|
1009
|
+
const restrike = working.find(
|
|
1010
|
+
(other) =>
|
|
1011
|
+
other !== item &&
|
|
1012
|
+
other.note.pitch === item.note.pitch &&
|
|
1013
|
+
other.start > item.start &&
|
|
1014
|
+
other.start < end,
|
|
1015
|
+
);
|
|
1016
|
+
if (restrike) end = Math.max(current, restrike.start);
|
|
1017
|
+
return {
|
|
1018
|
+
...item,
|
|
1019
|
+
duration: end - item.start,
|
|
1020
|
+
pedalled: end > keyUp || item.pedalled === true,
|
|
1021
|
+
};
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
872
1024
|
// 4. Glide and monophony (on the written timing).
|
|
873
1025
|
let performed = glideAndMono(
|
|
874
1026
|
track,
|