@sudobility/music_types 0.11.20 → 0.11.22
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/CLAUDE.md +9 -0
- package/dist/domain/commands/edit-commands.d.ts +66 -0
- package/dist/domain/commands/edit-commands.d.ts.map +1 -0
- package/dist/domain/commands/edit-commands.js +185 -0
- package/dist/domain/commands/edit-commands.js.map +1 -0
- package/dist/domain/commands/note-commands.d.ts +73 -0
- package/dist/domain/commands/note-commands.d.ts.map +1 -0
- package/dist/domain/commands/note-commands.js +292 -0
- package/dist/domain/commands/note-commands.js.map +1 -0
- package/dist/domain/commands/reflow.d.ts +69 -0
- package/dist/domain/commands/reflow.d.ts.map +1 -0
- package/dist/domain/commands/reflow.js +395 -0
- package/dist/domain/commands/reflow.js.map +1 -0
- package/dist/domain/commands/region-commands.d.ts +21 -0
- package/dist/domain/commands/region-commands.d.ts.map +1 -0
- package/dist/domain/commands/region-commands.js +47 -0
- package/dist/domain/commands/region-commands.js.map +1 -0
- package/dist/domain/commands/relocate-commands.d.ts +18 -0
- package/dist/domain/commands/relocate-commands.d.ts.map +1 -0
- package/dist/domain/commands/relocate-commands.js +108 -0
- package/dist/domain/commands/relocate-commands.js.map +1 -0
- package/dist/domain/commands/ripple-commands.d.ts +44 -0
- package/dist/domain/commands/ripple-commands.d.ts.map +1 -0
- package/dist/domain/commands/ripple-commands.js +104 -0
- package/dist/domain/commands/ripple-commands.js.map +1 -0
- package/dist/domain/commands/snapshot.d.ts +38 -0
- package/dist/domain/commands/snapshot.d.ts.map +1 -0
- package/dist/domain/commands/snapshot.js +44 -0
- package/dist/domain/commands/snapshot.js.map +1 -0
- package/dist/domain/commands/structure-commands.d.ts +38 -0
- package/dist/domain/commands/structure-commands.d.ts.map +1 -0
- package/dist/domain/commands/structure-commands.js +220 -0
- package/dist/domain/commands/structure-commands.js.map +1 -0
- package/dist/domain/commands/track-commands.d.ts +15 -0
- package/dist/domain/commands/track-commands.d.ts.map +1 -0
- package/dist/domain/commands/track-commands.js +56 -0
- package/dist/domain/commands/track-commands.js.map +1 -0
- package/dist/domain/commands/types.d.ts +31 -0
- package/dist/domain/commands/types.d.ts.map +1 -0
- package/dist/domain/commands/types.js +2 -0
- package/dist/domain/commands/types.js.map +1 -0
- package/dist/domain/instruments/gm-kit.d.ts +45 -0
- package/dist/domain/instruments/gm-kit.d.ts.map +1 -0
- package/dist/domain/instruments/gm-kit.js +59 -0
- package/dist/domain/instruments/gm-kit.js.map +1 -0
- package/dist/domain/instruments/gm-percussion.d.ts +42 -0
- package/dist/domain/instruments/gm-percussion.d.ts.map +1 -0
- package/dist/domain/instruments/gm-percussion.js +85 -0
- package/dist/domain/instruments/gm-percussion.js.map +1 -0
- package/dist/domain/instruments/gm-polyphony.d.ts +13 -0
- package/dist/domain/instruments/gm-polyphony.d.ts.map +1 -0
- package/dist/domain/instruments/gm-polyphony.js +76 -0
- package/dist/domain/instruments/gm-polyphony.js.map +1 -0
- package/dist/domain/instruments/gm-range.d.ts +15 -0
- package/dist/domain/instruments/gm-range.d.ts.map +1 -0
- package/dist/domain/instruments/gm-range.js +97 -0
- package/dist/domain/instruments/gm-range.js.map +1 -0
- package/dist/domain/instruments/gm-transposition.d.ts +11 -0
- package/dist/domain/instruments/gm-transposition.d.ts.map +1 -0
- package/dist/domain/instruments/gm-transposition.js +69 -0
- package/dist/domain/instruments/gm-transposition.js.map +1 -0
- package/dist/domain/instruments/gm.d.ts +28 -0
- package/dist/domain/instruments/gm.d.ts.map +1 -0
- package/dist/domain/instruments/gm.js +217 -0
- package/dist/domain/instruments/gm.js.map +1 -0
- package/dist/domain/pitch/pitch.d.ts +15 -0
- package/dist/domain/pitch/pitch.d.ts.map +1 -0
- package/dist/domain/pitch/pitch.js +81 -0
- package/dist/domain/pitch/pitch.js.map +1 -0
- package/dist/domain/pitch/transpose.d.ts +37 -0
- package/dist/domain/pitch/transpose.d.ts.map +1 -0
- package/dist/domain/pitch/transpose.js +65 -0
- package/dist/domain/pitch/transpose.js.map +1 -0
- package/dist/domain/quantization/options.d.ts +55 -0
- package/dist/domain/quantization/options.d.ts.map +1 -0
- package/dist/domain/quantization/options.js +2 -0
- package/dist/domain/quantization/options.js.map +1 -0
- package/dist/domain/quantization/quantize.d.ts +13 -0
- package/dist/domain/quantization/quantize.d.ts.map +1 -0
- package/dist/domain/quantization/quantize.js +199 -0
- package/dist/domain/quantization/quantize.js.map +1 -0
- package/dist/domain/score/factory.d.ts +39 -0
- package/dist/domain/score/factory.d.ts.map +1 -0
- package/dist/domain/score/factory.js +148 -0
- package/dist/domain/score/factory.js.map +1 -0
- package/dist/domain/score/fragment.d.ts +28 -0
- package/dist/domain/score/fragment.d.ts.map +1 -0
- package/dist/domain/score/fragment.js +68 -0
- package/dist/domain/score/fragment.js.map +1 -0
- package/dist/domain/score/ids.d.ts +8 -0
- package/dist/domain/score/ids.d.ts.map +1 -0
- package/dist/domain/score/ids.js +38 -0
- package/dist/domain/score/ids.js.map +1 -0
- package/dist/domain/score/queries.d.ts +34 -0
- package/dist/domain/score/queries.d.ts.map +1 -0
- package/dist/domain/score/queries.js +132 -0
- package/dist/domain/score/queries.js.map +1 -0
- package/dist/domain/score/ties.d.ts +73 -0
- package/dist/domain/score/ties.d.ts.map +1 -0
- package/dist/domain/score/ties.js +200 -0
- package/dist/domain/score/ties.js.map +1 -0
- package/dist/domain/selection/selection.d.ts +63 -0
- package/dist/domain/selection/selection.d.ts.map +1 -0
- package/dist/domain/selection/selection.js +129 -0
- package/dist/domain/selection/selection.js.map +1 -0
- package/dist/domain/selection/types.d.ts +10 -0
- package/dist/domain/selection/types.d.ts.map +1 -0
- package/dist/domain/selection/types.js +5 -0
- package/dist/domain/selection/types.js.map +1 -0
- package/dist/domain/time/durations.d.ts +21 -0
- package/dist/domain/time/durations.d.ts.map +1 -0
- package/dist/domain/time/durations.js +53 -0
- package/dist/domain/time/durations.js.map +1 -0
- package/dist/domain/time/fraction.d.ts +19 -0
- package/dist/domain/time/fraction.d.ts.map +1 -0
- package/dist/domain/time/fraction.js +60 -0
- package/dist/domain/time/fraction.js.map +1 -0
- package/dist/domain/time/tempo-map.d.ts +24 -0
- package/dist/domain/time/tempo-map.d.ts.map +1 -0
- package/dist/domain/time/tempo-map.js +85 -0
- package/dist/domain/time/tempo-map.js.map +1 -0
- package/dist/domain/time/ticks.d.ts +34 -0
- package/dist/domain/time/ticks.d.ts.map +1 -0
- package/dist/domain/time/ticks.js +93 -0
- package/dist/domain/time/ticks.js.map +1 -0
- package/dist/domain/validation/issues.d.ts +44 -0
- package/dist/domain/validation/issues.d.ts.map +1 -0
- package/dist/domain/validation/issues.js +34 -0
- package/dist/domain/validation/issues.js.map +1 -0
- package/dist/domain/validation/validator.d.ts +11 -0
- package/dist/domain/validation/validator.d.ts.map +1 -0
- package/dist/domain/validation/validator.js +375 -0
- package/dist/domain/validation/validator.js.map +1 -0
- package/dist/domain/voicing/allocate.d.ts +19 -0
- package/dist/domain/voicing/allocate.d.ts.map +1 -0
- package/dist/domain/voicing/allocate.js +100 -0
- package/dist/domain/voicing/allocate.js.map +1 -0
- package/dist/index.d.ts +46 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +50 -2
- package/dist/index.js.map +1 -1
- package/dist/services/regeneration/controller.d.ts +60 -0
- package/dist/services/regeneration/controller.d.ts.map +1 -0
- package/dist/services/regeneration/controller.js +202 -0
- package/dist/services/regeneration/controller.js.map +1 -0
- package/dist/test/fixtures.d.ts +45 -0
- package/dist/test/fixtures.d.ts.map +1 -0
- package/dist/test/fixtures.js +420 -0
- package/dist/test/fixtures.js.map +1 -0
- package/package.json +2 -1
package/CLAUDE.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# @sudobility/music_types
|
|
2
2
|
|
|
3
|
+
> **Git policy — never auto-commit or auto-push.** Leave your work in the working tree.
|
|
4
|
+
> Run `git commit`, `git push`, `gh pr create`, or `scripts/push_all.sh` **only when the user
|
|
5
|
+
> explicitly asks in that turn**. Approval for an earlier change does not carry forward, and
|
|
6
|
+
> finishing a task is not permission to commit it.
|
|
7
|
+
|
|
3
8
|
TypeScript types and Zod schemas for the ScoreSmith music family (music_api, music_client, music_lib, music_app).
|
|
4
9
|
|
|
5
10
|
## Tech Stack
|
|
@@ -45,3 +50,7 @@ Everything exports from a single sectioned `src/index.ts`:
|
|
|
45
50
|
- `music_client` — typed network client + React Query hooks
|
|
46
51
|
- `music_lib` — domain logic, adapters, store
|
|
47
52
|
- `music_app` — web app (UI/routing only)
|
|
53
|
+
|
|
54
|
+
## Git Workflow
|
|
55
|
+
|
|
56
|
+
- Do not use feature branches for code changes. Always stay on the current branch.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { MusicalEvent, NoteEvent, Score, UUID } from '../../index.js';
|
|
2
|
+
import type { QuantizeOptions } from '../quantization/options.js';
|
|
3
|
+
import type { ScoreCommand } from './types.js';
|
|
4
|
+
export type PasteDestination = {
|
|
5
|
+
trackId: UUID;
|
|
6
|
+
voiceIndex: number;
|
|
7
|
+
anchorTick: number;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Pastes a copied list of note events (e.g. from a prior "copy" of a
|
|
11
|
+
* selection) into `destination.trackId`/`voiceIndex`, anchored so the
|
|
12
|
+
* earliest-starting pasted note lands at `destination.anchorTick` and
|
|
13
|
+
* every other note keeps its relative offset. A pasted note that ends up
|
|
14
|
+
* crossing a measure boundary is split into tied segments, matching
|
|
15
|
+
* `moveNotesCommand`. Ids are regenerated so pasted notes never collide
|
|
16
|
+
* with their source.
|
|
17
|
+
*/
|
|
18
|
+
export declare function pasteEventsCommand(notes: NoteEvent[], destination: PasteDestination, label: string): ScoreCommand;
|
|
19
|
+
/**
|
|
20
|
+
* One voice (identified by `trackId`/`measureId`/`voiceId`) touched by a
|
|
21
|
+
* quantize action, with its *note* events already extracted (not the
|
|
22
|
+
* synthetic rests `reflowVoice` fills gaps with — feeding those into
|
|
23
|
+
* `quantizeEvents` alongside real notes could snap a filler rest's start
|
|
24
|
+
* independently of its neighboring note and corrupt the measure). Building
|
|
25
|
+
* this list is O(score) but doesn't itself call `quantizeEvents` — see
|
|
26
|
+
* `collectQuantizeTargets`'s doc comment for why it's split out as its own
|
|
27
|
+
* step (Task 17).
|
|
28
|
+
*/
|
|
29
|
+
export type QuantizeTarget = {
|
|
30
|
+
trackId: UUID;
|
|
31
|
+
measureId: UUID;
|
|
32
|
+
voiceId: UUID;
|
|
33
|
+
notes: NoteEvent[];
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* The cheap, non-quantizing "which voices does this selection touch, and
|
|
37
|
+
* what are their current notes" pass of quantization: walks the score once
|
|
38
|
+
* and collects one `QuantizeTarget` per voice containing at least one of
|
|
39
|
+
* `eventIds`. Split out from `quantizeCommand`'s transform so the
|
|
40
|
+
* intent layer (`editing.ts`'s `quantizeSelection`, `interactions.ts`'s
|
|
41
|
+
* `commitQuantize`) can run this synchronously to decide *how much* work a
|
|
42
|
+
* quantize action actually implies (spec §29: route a manual quantize of
|
|
43
|
+
* >2000 events through `services/quantization/quantize-service.ts`'s
|
|
44
|
+
* worker) before committing to the expensive `quantizeEvents` call either
|
|
45
|
+
* on the main thread or off it — without duplicating this measure/voice
|
|
46
|
+
* walk in two places.
|
|
47
|
+
*/
|
|
48
|
+
export declare function collectQuantizeTargets(score: Score, eventIds: readonly UUID[]): QuantizeTarget[];
|
|
49
|
+
/**
|
|
50
|
+
* Splices each target's already-quantized notes (`quantizedByVoiceId`,
|
|
51
|
+
* keyed by `QuantizeTarget.voiceId` — computed by the caller via
|
|
52
|
+
* `quantizeEvents`) back into `score`: clips each note back to its measure's span, then
|
|
53
|
+
* `reflowVoice` regenerates that voice's rests around the result. This
|
|
54
|
+
* keeps quantization measure-local (no cross-measure re-splitting), a
|
|
55
|
+
* deliberate Task 5 scope limitation documented in the original brief.
|
|
56
|
+
* A target whose `voiceId` has no entry in `quantizedByVoiceId` is treated
|
|
57
|
+
* as "nothing computed for it" (splices in an empty note list) — the
|
|
58
|
+
* sole caller (`quantize`, below) always populates every target's key
|
|
59
|
+
* before calling this.
|
|
60
|
+
*/
|
|
61
|
+
export declare function applyQuantizedGroups(score: Score, targets: readonly QuantizeTarget[], quantizedByVoiceId: ReadonlyMap<UUID, MusicalEvent[]>): Score;
|
|
62
|
+
/** Quantizes every voice containing at least one of `eventIds`, per Task 4's reusable quantization engine. */
|
|
63
|
+
export declare function quantizeCommand(eventIds: UUID[], options: QuantizeOptions, label: string): ScoreCommand;
|
|
64
|
+
/** Transposes the given notes by `semitones`, re-spelling each per its own measure's key signature. */
|
|
65
|
+
export declare function transposeCommand(eventIds: UUID[], semitones: number, label: string): ScoreCommand;
|
|
66
|
+
//# sourceMappingURL=edit-commands.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"edit-commands.d.ts","sourceRoot":"","sources":["../../../src/domain/commands/edit-commands.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,YAAY,EACZ,SAAS,EACT,KAAK,EACL,IAAI,EACL,MAAM,gBAAgB,CAAC;AAIxB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAM/C,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,EAAE,IAAI,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAkCF;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,SAAS,EAAE,EAClB,WAAW,EAAE,gBAAgB,EAC7B,KAAK,EAAE,MAAM,GACZ,YAAY,CAId;AAgBD;;;;;;;;;GASG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,EAAE,IAAI,CAAC;IACd,SAAS,EAAE,IAAI,CAAC;IAChB,OAAO,EAAE,IAAI,CAAC;IACd,KAAK,EAAE,SAAS,EAAE,CAAC;CACpB,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,KAAK,EACZ,QAAQ,EAAE,SAAS,IAAI,EAAE,GACxB,cAAc,EAAE,CAiBlB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE,SAAS,cAAc,EAAE,EAClC,kBAAkB,EAAE,WAAW,CAAC,IAAI,EAAE,YAAY,EAAE,CAAC,GACpD,KAAK,CAqCP;AAuBD,8GAA8G;AAC9G,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,IAAI,EAAE,EAChB,OAAO,EAAE,eAAe,EACxB,KAAK,EAAE,MAAM,GACZ,YAAY,CAEd;AAuCD,uGAAuG;AACvG,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,IAAI,EAAE,EAChB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GACZ,YAAY,CAId"}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copy/paste, quantize, and transpose command factories (spec §7, §14,
|
|
3
|
+
* §24). Each factory wraps a pure `(Score) => Score` transform via
|
|
4
|
+
* `transformCommand`.
|
|
5
|
+
*/
|
|
6
|
+
import { createId } from '../score/ids.js';
|
|
7
|
+
import { splitNoteAcrossMeasures } from '../score/ties.js';
|
|
8
|
+
import { isNoteEvent } from '../../index.js';
|
|
9
|
+
import { transposePitch } from '../pitch/transpose.js';
|
|
10
|
+
import { quantizeEvents } from '../quantization/quantize.js';
|
|
11
|
+
import { transformCommand } from './snapshot.js';
|
|
12
|
+
import { insertNoteIntoTrack, reflowVoice, withTracks } from './reflow.js';
|
|
13
|
+
function pasteEvents(score, notes, destination) {
|
|
14
|
+
const track = score.tracks.find(t => t.id === destination.trackId);
|
|
15
|
+
if (!track || notes.length === 0)
|
|
16
|
+
return score;
|
|
17
|
+
const originStart = Math.min(...notes.map(n => n.startTick));
|
|
18
|
+
const boundaries = track.measures.map(m => m.startTick);
|
|
19
|
+
let working = track;
|
|
20
|
+
for (const note of notes) {
|
|
21
|
+
const startTick = destination.anchorTick + (note.startTick - originStart);
|
|
22
|
+
const placeholder = {
|
|
23
|
+
...note,
|
|
24
|
+
id: createId(),
|
|
25
|
+
startTick,
|
|
26
|
+
trackId: destination.trackId,
|
|
27
|
+
tieStart: undefined,
|
|
28
|
+
tieStop: undefined,
|
|
29
|
+
};
|
|
30
|
+
const segments = splitNoteAcrossMeasures(placeholder, boundaries);
|
|
31
|
+
for (const segment of segments) {
|
|
32
|
+
working = insertNoteIntoTrack(working, segment, destination.voiceIndex);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const tracks = score.tracks.map(t => (t.id === track.id ? working : t));
|
|
36
|
+
return withTracks(score, tracks);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Pastes a copied list of note events (e.g. from a prior "copy" of a
|
|
40
|
+
* selection) into `destination.trackId`/`voiceIndex`, anchored so the
|
|
41
|
+
* earliest-starting pasted note lands at `destination.anchorTick` and
|
|
42
|
+
* every other note keeps its relative offset. A pasted note that ends up
|
|
43
|
+
* crossing a measure boundary is split into tied segments, matching
|
|
44
|
+
* `moveNotesCommand`. Ids are regenerated so pasted notes never collide
|
|
45
|
+
* with their source.
|
|
46
|
+
*/
|
|
47
|
+
export function pasteEventsCommand(notes, destination, label) {
|
|
48
|
+
return transformCommand(label, score => pasteEvents(score, notes, destination));
|
|
49
|
+
}
|
|
50
|
+
// ---- quantizeCommand -----------------------------------------------------------
|
|
51
|
+
/** Clips `event` to fit within `measure`'s span, or returns `null` if that leaves no positive duration. */
|
|
52
|
+
function clipToMeasure(event, measureStart, measureEnd) {
|
|
53
|
+
const start = Math.max(event.startTick, measureStart);
|
|
54
|
+
const end = Math.min(event.startTick + event.durationTicks, measureEnd);
|
|
55
|
+
if (end <= start)
|
|
56
|
+
return null;
|
|
57
|
+
return { ...event, startTick: start, durationTicks: end - start };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The cheap, non-quantizing "which voices does this selection touch, and
|
|
61
|
+
* what are their current notes" pass of quantization: walks the score once
|
|
62
|
+
* and collects one `QuantizeTarget` per voice containing at least one of
|
|
63
|
+
* `eventIds`. Split out from `quantizeCommand`'s transform so the
|
|
64
|
+
* intent layer (`editing.ts`'s `quantizeSelection`, `interactions.ts`'s
|
|
65
|
+
* `commitQuantize`) can run this synchronously to decide *how much* work a
|
|
66
|
+
* quantize action actually implies (spec §29: route a manual quantize of
|
|
67
|
+
* >2000 events through `services/quantization/quantize-service.ts`'s
|
|
68
|
+
* worker) before committing to the expensive `quantizeEvents` call either
|
|
69
|
+
* on the main thread or off it — without duplicating this measure/voice
|
|
70
|
+
* walk in two places.
|
|
71
|
+
*/
|
|
72
|
+
export function collectQuantizeTargets(score, eventIds) {
|
|
73
|
+
const idSet = new Set(eventIds);
|
|
74
|
+
const targets = [];
|
|
75
|
+
for (const track of score.tracks) {
|
|
76
|
+
for (const measure of track.measures) {
|
|
77
|
+
for (const voice of measure.voices) {
|
|
78
|
+
if (!voice.events.some(e => idSet.has(e.id)))
|
|
79
|
+
continue;
|
|
80
|
+
targets.push({
|
|
81
|
+
trackId: track.id,
|
|
82
|
+
measureId: measure.id,
|
|
83
|
+
voiceId: voice.id,
|
|
84
|
+
notes: voice.events.filter(isNoteEvent),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return targets;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Splices each target's already-quantized notes (`quantizedByVoiceId`,
|
|
93
|
+
* keyed by `QuantizeTarget.voiceId` — computed by the caller via
|
|
94
|
+
* `quantizeEvents`) back into `score`: clips each note back to its measure's span, then
|
|
95
|
+
* `reflowVoice` regenerates that voice's rests around the result. This
|
|
96
|
+
* keeps quantization measure-local (no cross-measure re-splitting), a
|
|
97
|
+
* deliberate Task 5 scope limitation documented in the original brief.
|
|
98
|
+
* A target whose `voiceId` has no entry in `quantizedByVoiceId` is treated
|
|
99
|
+
* as "nothing computed for it" (splices in an empty note list) — the
|
|
100
|
+
* sole caller (`quantize`, below) always populates every target's key
|
|
101
|
+
* before calling this.
|
|
102
|
+
*/
|
|
103
|
+
export function applyQuantizedGroups(score, targets, quantizedByVoiceId) {
|
|
104
|
+
const targetsByTrackMeasure = new Map();
|
|
105
|
+
for (const target of targets) {
|
|
106
|
+
const key = `${target.trackId}::${target.measureId}`;
|
|
107
|
+
const list = targetsByTrackMeasure.get(key);
|
|
108
|
+
if (list)
|
|
109
|
+
list.push(target);
|
|
110
|
+
else
|
|
111
|
+
targetsByTrackMeasure.set(key, [target]);
|
|
112
|
+
}
|
|
113
|
+
const tracks = score.tracks.map(track => {
|
|
114
|
+
const measures = track.measures.map(measure => {
|
|
115
|
+
const measureTargets = targetsByTrackMeasure.get(`${track.id}::${measure.id}`);
|
|
116
|
+
if (!measureTargets)
|
|
117
|
+
return measure;
|
|
118
|
+
let nextMeasure = measure;
|
|
119
|
+
const measureEnd = measure.startTick + measure.durationTicks;
|
|
120
|
+
for (const target of measureTargets) {
|
|
121
|
+
const quantizedNotes = (quantizedByVoiceId.get(target.voiceId) ?? [])
|
|
122
|
+
.map(e => clipToMeasure(e, measure.startTick, measureEnd))
|
|
123
|
+
.filter((e) => e !== null);
|
|
124
|
+
const withQuantized = {
|
|
125
|
+
...nextMeasure,
|
|
126
|
+
voices: nextMeasure.voices.map(v => v.id === target.voiceId ? { ...v, events: quantizedNotes } : v),
|
|
127
|
+
};
|
|
128
|
+
nextMeasure = reflowVoice(withQuantized, target.voiceId, track.id);
|
|
129
|
+
}
|
|
130
|
+
return nextMeasure;
|
|
131
|
+
});
|
|
132
|
+
const trackChanged = measures.some((m, i) => m !== track.measures[i]);
|
|
133
|
+
return trackChanged ? { ...track, measures } : track;
|
|
134
|
+
});
|
|
135
|
+
return withTracks(score, tracks);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Quantizes the voice(s) containing any of `eventIds`, calling
|
|
139
|
+
* `quantizeEvents` synchronously for every touched voice.
|
|
140
|
+
*
|
|
141
|
+
* This is the only path. A >2000-event selection used to be routed through a
|
|
142
|
+
* worker instead, via a precomputed-groups variant of this command; that was
|
|
143
|
+
* removed once the offload was measured at 0.57ms of saved work against a
|
|
144
|
+
* structured clone of the whole event array in each direction.
|
|
145
|
+
*/
|
|
146
|
+
function quantize(score, eventIds, options) {
|
|
147
|
+
const targets = collectQuantizeTargets(score, eventIds);
|
|
148
|
+
const quantizedByVoiceId = new Map(targets.map(t => [t.voiceId, quantizeEvents(t.notes, options)]));
|
|
149
|
+
return applyQuantizedGroups(score, targets, quantizedByVoiceId);
|
|
150
|
+
}
|
|
151
|
+
/** Quantizes every voice containing at least one of `eventIds`, per Task 4's reusable quantization engine. */
|
|
152
|
+
export function quantizeCommand(eventIds, options, label) {
|
|
153
|
+
return transformCommand(label, score => quantize(score, eventIds, options));
|
|
154
|
+
}
|
|
155
|
+
// ---- transposeCommand -----------------------------------------------------------
|
|
156
|
+
function transpose(score, eventIds, semitones) {
|
|
157
|
+
const idSet = new Set(eventIds);
|
|
158
|
+
const tracks = score.tracks.map(track => {
|
|
159
|
+
const measures = track.measures.map(measure => {
|
|
160
|
+
const voices = measure.voices.map(voice => {
|
|
161
|
+
if (!voice.events.some(e => idSet.has(e.id)))
|
|
162
|
+
return voice;
|
|
163
|
+
return {
|
|
164
|
+
...voice,
|
|
165
|
+
events: voice.events.map(event => isNoteEvent(event) && idSet.has(event.id)
|
|
166
|
+
? {
|
|
167
|
+
...event,
|
|
168
|
+
pitch: transposePitch(event.pitch, semitones, measure.keySignature),
|
|
169
|
+
}
|
|
170
|
+
: event),
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
const measureChanged = voices.some((v, i) => v !== measure.voices[i]);
|
|
174
|
+
return measureChanged ? { ...measure, voices } : measure;
|
|
175
|
+
});
|
|
176
|
+
const trackChanged = measures.some((m, i) => m !== track.measures[i]);
|
|
177
|
+
return trackChanged ? { ...track, measures } : track;
|
|
178
|
+
});
|
|
179
|
+
return withTracks(score, tracks);
|
|
180
|
+
}
|
|
181
|
+
/** Transposes the given notes by `semitones`, re-spelling each per its own measure's key signature. */
|
|
182
|
+
export function transposeCommand(eventIds, semitones, label) {
|
|
183
|
+
return transformCommand(label, score => transpose(score, eventIds, semitones));
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=edit-commands.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"edit-commands.js","sourceRoot":"","sources":["../../../src/domain/commands/edit-commands.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { Accidental, Articulation, DurationName, Pitch, UUID } from '../../index.js';
|
|
2
|
+
import type { ScoreCommand } from './types.js';
|
|
3
|
+
export type AddNoteParams = {
|
|
4
|
+
trackId: UUID;
|
|
5
|
+
measureId: UUID;
|
|
6
|
+
voiceIndex: number;
|
|
7
|
+
pitch: Pitch;
|
|
8
|
+
startTick: number;
|
|
9
|
+
durationTicks: number;
|
|
10
|
+
velocity?: number;
|
|
11
|
+
articulation?: Articulation;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Adds a new note into `params.measureId`'s voice at `params.voiceIndex`,
|
|
15
|
+
* backfilling rests around it. If the new note overlaps an existing note
|
|
16
|
+
* in that voice, the overlap is resolved deterministically by
|
|
17
|
+
* `reflowVoice`'s "replace-on-overlap" rule (see `reflow.ts`): the new
|
|
18
|
+
* note wins, trimming or dropping whatever it overlaps.
|
|
19
|
+
*
|
|
20
|
+
* Only inserted into `params.measureId` itself: if `startTick +
|
|
21
|
+
* durationTicks` extends past that measure's end, the note is silently
|
|
22
|
+
* truncated to fit (by `reflowVoice`'s measure-clipping), not split into
|
|
23
|
+
* tied segments the way `moveNotesCommand`/`pasteEventsCommand` split a
|
|
24
|
+
* note that crosses a boundary. This is a deliberate, narrower behavior
|
|
25
|
+
* for `addNoteCommand` specifically (an add always targets one named
|
|
26
|
+
* measure, unlike a move/paste's computed destination) — callers that
|
|
27
|
+
* want boundary-spanning insertion should add the note measure-local and
|
|
28
|
+
* then use `moveNotesCommand`/`resizeNotesCommand` if it needs to extend
|
|
29
|
+
* further, or split it into per-measure `addNoteCommand` calls themselves.
|
|
30
|
+
*/
|
|
31
|
+
export declare function addNoteCommand(params: AddNoteParams, label: string): ScoreCommand;
|
|
32
|
+
/**
|
|
33
|
+
* Deletes the given note events, backfilling rests so every affected
|
|
34
|
+
* measure stays full. If a deleted note was tied to a partner that isn't
|
|
35
|
+
* also being deleted, that partner's now-dangling tie flag is cleared.
|
|
36
|
+
*/
|
|
37
|
+
export declare function deleteEventsCommand(eventIds: UUID[], label: string): ScoreCommand;
|
|
38
|
+
export type MoveNotesParams = {
|
|
39
|
+
deltaTicks: number;
|
|
40
|
+
deltaSemitones: number;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Moves the given notes by `deltaTicks` (clamped within their track's
|
|
44
|
+
* existing tick span) and transposes them by `deltaSemitones`. A note
|
|
45
|
+
* whose new position crosses a measure boundary is split into tied
|
|
46
|
+
* segments (Task 3's `splitNoteAcrossMeasures`); any pre-existing tie
|
|
47
|
+
* flags on the moved note are cleared first, since its former tie
|
|
48
|
+
* partner(s) are no longer necessarily adjacent after the move. If a
|
|
49
|
+
* moved note was tied to a partner that isn't also being moved, that
|
|
50
|
+
* partner's now-dangling tie flag is cleared too.
|
|
51
|
+
*/
|
|
52
|
+
export declare function moveNotesCommand(eventIds: UUID[], params: MoveNotesParams, label: string): ScoreCommand;
|
|
53
|
+
/**
|
|
54
|
+
* Sets the given notes' duration to `durationTicks` (a raw tick length,
|
|
55
|
+
* e.g. from a piano-roll drag-resize), clamped to fit within each note's
|
|
56
|
+
* current measure (no cross-measure re-splitting), then reflows.
|
|
57
|
+
*/
|
|
58
|
+
export declare function resizeNotesCommand(eventIds: UUID[], durationTicks: number, label: string): ScoreCommand;
|
|
59
|
+
/** Sets the given notes' duration to a named value (e.g. from the inspector), converted to ticks via the score's ppq. */
|
|
60
|
+
export declare function changeDurationCommand(eventIds: UUID[], duration: DurationName, label: string): ScoreCommand;
|
|
61
|
+
/** Sets the given notes' pitch to an absolute `Pitch`. */
|
|
62
|
+
export declare function changePitchCommand(eventIds: UUID[], pitch: Pitch, label: string): ScoreCommand;
|
|
63
|
+
/** Sets the given notes' velocity (0-127). */
|
|
64
|
+
export declare function changeVelocityCommand(eventIds: UUID[], velocity: number, label: string): ScoreCommand;
|
|
65
|
+
/** Sets (or clears, when `articulation` is `undefined`) the given notes' articulation. */
|
|
66
|
+
export declare function changeArticulationCommand(eventIds: UUID[], articulation: Articulation | undefined, label: string): ScoreCommand;
|
|
67
|
+
/** Sets the given notes' pitch accidental, keeping their step/octave. */
|
|
68
|
+
export declare function changeAccidentalCommand(eventIds: UUID[], accidental: Accidental, label: string): ScoreCommand;
|
|
69
|
+
/** Toggles `tieStart` or `tieStop` on the given notes. */
|
|
70
|
+
export declare function toggleTieCommand(eventIds: UUID[], which: 'tieStart' | 'tieStop', label: string): ScoreCommand;
|
|
71
|
+
/** Moves the given notes to the voice at ordinal position `targetVoiceIndex` within their current measure (created if absent). */
|
|
72
|
+
export declare function changeVoiceCommand(eventIds: UUID[], targetVoiceIndex: number, label: string): ScoreCommand;
|
|
73
|
+
//# sourceMappingURL=note-commands.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"note-commands.d.ts","sourceRoot":"","sources":["../../../src/domain/commands/note-commands.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EACV,UAAU,EACV,YAAY,EACZ,YAAY,EAEZ,KAAK,EAGL,IAAI,EACL,MAAM,gBAAgB,CAAC;AAIxB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AA+C/C,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE,IAAI,CAAC;IACd,SAAS,EAAE,IAAI,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,KAAK,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B,CAAC;AAwBF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE,MAAM,GACZ,YAAY,CAEd;AAcD;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,IAAI,EAAE,EAChB,KAAK,EAAE,MAAM,GACZ,YAAY,CAEd;AAID,MAAM,MAAM,eAAe,GAAG;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,CAAC;AAiF7E;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,IAAI,EAAE,EAChB,MAAM,EAAE,eAAe,EACvB,KAAK,EAAE,MAAM,GACZ,YAAY,CAEd;AA0CD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,IAAI,EAAE,EAChB,aAAa,EAAE,MAAM,EACrB,KAAK,EAAE,MAAM,GACZ,YAAY,CAId;AAED,yHAAyH;AACzH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,IAAI,EAAE,EAChB,QAAQ,EAAE,YAAY,EACtB,KAAK,EAAE,MAAM,GACZ,YAAY,CAId;AAID,0DAA0D;AAC1D,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,IAAI,EAAE,EAChB,KAAK,EAAE,KAAK,EACZ,KAAK,EAAE,MAAM,GACZ,YAAY,CAId;AAED,8CAA8C;AAC9C,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,IAAI,EAAE,EAChB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,GACZ,YAAY,CAId;AAED,0FAA0F;AAC1F,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,IAAI,EAAE,EAChB,YAAY,EAAE,YAAY,GAAG,SAAS,EACtC,KAAK,EAAE,MAAM,GACZ,YAAY,CASd;AAED,yEAAyE;AACzE,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,IAAI,EAAE,EAChB,UAAU,EAAE,UAAU,EACtB,KAAK,EAAE,MAAM,GACZ,YAAY,CAOd;AAED,0DAA0D;AAC1D,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,IAAI,EAAE,EAChB,KAAK,EAAE,UAAU,GAAG,SAAS,EAC7B,KAAK,EAAE,MAAM,GACZ,YAAY,CAId;AA0DD,kIAAkI;AAClI,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,IAAI,EAAE,EAChB,gBAAgB,EAAE,MAAM,EACxB,KAAK,EAAE,MAAM,GACZ,YAAY,CAId"}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Note-editing command factories (spec §7, §14). Each factory returns a
|
|
3
|
+
* `ScoreCommand` built via `transformCommand`, wrapping a pure
|
|
4
|
+
* `(Score) => Score` transform so undo (via Immer patches) is free and
|
|
5
|
+
* structurally guaranteed to be a deep-equal inverse of execute.
|
|
6
|
+
*
|
|
7
|
+
* Every transform here keeps measures valid: `reflowVoice` (see
|
|
8
|
+
* `reflow.ts`) fills gaps with rests and trims overflow after any note is
|
|
9
|
+
* added, removed, resized, or relocated, and `moveNotesCommand` splits a
|
|
10
|
+
* note that crosses a measure boundary into tied segments via Task 3's
|
|
11
|
+
* `splitNoteAcrossMeasures`, per the Task 5 brief.
|
|
12
|
+
*/
|
|
13
|
+
import { createId } from '../score/ids.js';
|
|
14
|
+
import { splitNoteAcrossMeasures } from '../score/ties.js';
|
|
15
|
+
import { isNoteEvent } from '../../index.js';
|
|
16
|
+
import { ticksFor } from '../time/ticks.js';
|
|
17
|
+
import { transposePitch } from '../pitch/transpose.js';
|
|
18
|
+
import { transformCommand } from './snapshot.js';
|
|
19
|
+
import { clearDanglingTies, ensureVoiceAtIndex, insertNoteIntoTrack, removeNotesFromTrack, reflowVoice, withTracks, } from './reflow.js';
|
|
20
|
+
// ---- shared traversal helpers ------------------------------------------------
|
|
21
|
+
/**
|
|
22
|
+
* Applies `updater` to every note event in `score` whose id is in
|
|
23
|
+
* `eventIds`; other events pass through unchanged. Preserves referential
|
|
24
|
+
* equality at every level (voice/measure/track) that has no matching
|
|
25
|
+
* event, so a call that matches nothing is a true no-op (see `withTracks`).
|
|
26
|
+
*/
|
|
27
|
+
function mapNotes(score, eventIds, updater) {
|
|
28
|
+
const idSet = new Set(eventIds);
|
|
29
|
+
const tracks = score.tracks.map(track => {
|
|
30
|
+
const measures = track.measures.map(measure => {
|
|
31
|
+
const voices = measure.voices.map(voice => {
|
|
32
|
+
if (!voice.events.some(e => idSet.has(e.id)))
|
|
33
|
+
return voice;
|
|
34
|
+
return {
|
|
35
|
+
...voice,
|
|
36
|
+
events: voice.events.map(event => isNoteEvent(event) && idSet.has(event.id) ? updater(event) : event),
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
const measureChanged = voices.some((v, i) => v !== measure.voices[i]);
|
|
40
|
+
return measureChanged ? { ...measure, voices } : measure;
|
|
41
|
+
});
|
|
42
|
+
const trackChanged = measures.some((m, i) => m !== track.measures[i]);
|
|
43
|
+
return trackChanged ? { ...track, measures } : track;
|
|
44
|
+
});
|
|
45
|
+
return withTracks(score, tracks);
|
|
46
|
+
}
|
|
47
|
+
function addNote(score, params) {
|
|
48
|
+
const tracks = score.tracks.map(track => {
|
|
49
|
+
if (track.id !== params.trackId)
|
|
50
|
+
return track;
|
|
51
|
+
const measureExists = track.measures.some(m => m.id === params.measureId);
|
|
52
|
+
if (!measureExists)
|
|
53
|
+
return track;
|
|
54
|
+
const note = {
|
|
55
|
+
id: createId(),
|
|
56
|
+
pitch: params.pitch,
|
|
57
|
+
startTick: params.startTick,
|
|
58
|
+
durationTicks: params.durationTicks,
|
|
59
|
+
velocity: params.velocity ?? 80,
|
|
60
|
+
voiceId: '', // renumbered by insertNoteIntoTrack to the destination voice's id
|
|
61
|
+
trackId: track.id,
|
|
62
|
+
...(params.articulation ? { articulation: params.articulation } : {}),
|
|
63
|
+
};
|
|
64
|
+
return insertNoteIntoTrack(track, note, params.voiceIndex);
|
|
65
|
+
});
|
|
66
|
+
return withTracks(score, tracks);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Adds a new note into `params.measureId`'s voice at `params.voiceIndex`,
|
|
70
|
+
* backfilling rests around it. If the new note overlaps an existing note
|
|
71
|
+
* in that voice, the overlap is resolved deterministically by
|
|
72
|
+
* `reflowVoice`'s "replace-on-overlap" rule (see `reflow.ts`): the new
|
|
73
|
+
* note wins, trimming or dropping whatever it overlaps.
|
|
74
|
+
*
|
|
75
|
+
* Only inserted into `params.measureId` itself: if `startTick +
|
|
76
|
+
* durationTicks` extends past that measure's end, the note is silently
|
|
77
|
+
* truncated to fit (by `reflowVoice`'s measure-clipping), not split into
|
|
78
|
+
* tied segments the way `moveNotesCommand`/`pasteEventsCommand` split a
|
|
79
|
+
* note that crosses a boundary. This is a deliberate, narrower behavior
|
|
80
|
+
* for `addNoteCommand` specifically (an add always targets one named
|
|
81
|
+
* measure, unlike a move/paste's computed destination) — callers that
|
|
82
|
+
* want boundary-spanning insertion should add the note measure-local and
|
|
83
|
+
* then use `moveNotesCommand`/`resizeNotesCommand` if it needs to extend
|
|
84
|
+
* further, or split it into per-measure `addNoteCommand` calls themselves.
|
|
85
|
+
*/
|
|
86
|
+
export function addNoteCommand(params, label) {
|
|
87
|
+
return transformCommand(label, score => addNote(score, params));
|
|
88
|
+
}
|
|
89
|
+
// ---- deleteEventsCommand -------------------------------------------------------
|
|
90
|
+
function deleteEvents(score, eventIds) {
|
|
91
|
+
const idSet = new Set(eventIds);
|
|
92
|
+
// Clear tie flags left dangling on any surviving partner of a deleted
|
|
93
|
+
// note *before* actually removing the notes (clearDanglingTies needs
|
|
94
|
+
// the deleted notes still in place to find their chain partners).
|
|
95
|
+
const detied = clearDanglingTies(score, idSet);
|
|
96
|
+
const tracks = detied.tracks.map(track => removeNotesFromTrack(track, idSet));
|
|
97
|
+
return withTracks(detied, tracks);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Deletes the given note events, backfilling rests so every affected
|
|
101
|
+
* measure stays full. If a deleted note was tied to a partner that isn't
|
|
102
|
+
* also being deleted, that partner's now-dangling tie flag is cleared.
|
|
103
|
+
*/
|
|
104
|
+
export function deleteEventsCommand(eventIds, label) {
|
|
105
|
+
return transformCommand(label, score => deleteEvents(score, eventIds));
|
|
106
|
+
}
|
|
107
|
+
function moveNotesOnTrack(track, eventIds, params) {
|
|
108
|
+
const toMove = [];
|
|
109
|
+
track.measures.forEach(measure => {
|
|
110
|
+
measure.voices.forEach((voice, voiceIndex) => {
|
|
111
|
+
voice.events.forEach(event => {
|
|
112
|
+
if (isNoteEvent(event) && eventIds.has(event.id))
|
|
113
|
+
toMove.push({ note: event, voiceIndex });
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
if (toMove.length === 0)
|
|
118
|
+
return track;
|
|
119
|
+
let working = removeNotesFromTrack(track, new Set(toMove.map(m => m.note.id)));
|
|
120
|
+
const boundaries = working.measures.map(m => m.startTick);
|
|
121
|
+
const lastMeasure = working.measures[working.measures.length - 1];
|
|
122
|
+
const trackEnd = lastMeasure
|
|
123
|
+
? lastMeasure.startTick + lastMeasure.durationTicks
|
|
124
|
+
: 0;
|
|
125
|
+
for (const { note, voiceIndex } of toMove) {
|
|
126
|
+
const maxStart = Math.max(0, trackEnd - note.durationTicks);
|
|
127
|
+
const clampedStart = Math.max(0, Math.min(note.startTick + params.deltaTicks, maxStart));
|
|
128
|
+
const destMeasure = working.measures.find(m => clampedStart >= m.startTick &&
|
|
129
|
+
clampedStart < m.startTick + m.durationTicks) ?? lastMeasure;
|
|
130
|
+
const newPitch = params.deltaSemitones !== 0
|
|
131
|
+
? transposePitch(note.pitch, params.deltaSemitones, destMeasure?.keySignature)
|
|
132
|
+
: note.pitch;
|
|
133
|
+
const movedNote = {
|
|
134
|
+
...note,
|
|
135
|
+
startTick: clampedStart,
|
|
136
|
+
pitch: newPitch,
|
|
137
|
+
tieStart: undefined,
|
|
138
|
+
tieStop: undefined,
|
|
139
|
+
};
|
|
140
|
+
const segments = splitNoteAcrossMeasures(movedNote, boundaries);
|
|
141
|
+
for (const segment of segments) {
|
|
142
|
+
working = insertNoteIntoTrack(working, segment, voiceIndex);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return working;
|
|
146
|
+
}
|
|
147
|
+
function moveNotes(score, eventIds, params) {
|
|
148
|
+
const idSet = new Set(eventIds);
|
|
149
|
+
// As in deleteEvents: clear dangling tie flags on any surviving partner
|
|
150
|
+
// of a moved note before moveNotesOnTrack relocates it away.
|
|
151
|
+
const detied = clearDanglingTies(score, idSet);
|
|
152
|
+
const tracks = detied.tracks.map(track => moveNotesOnTrack(track, idSet, params));
|
|
153
|
+
return withTracks(detied, tracks);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Moves the given notes by `deltaTicks` (clamped within their track's
|
|
157
|
+
* existing tick span) and transposes them by `deltaSemitones`. A note
|
|
158
|
+
* whose new position crosses a measure boundary is split into tied
|
|
159
|
+
* segments (Task 3's `splitNoteAcrossMeasures`); any pre-existing tie
|
|
160
|
+
* flags on the moved note are cleared first, since its former tie
|
|
161
|
+
* partner(s) are no longer necessarily adjacent after the move. If a
|
|
162
|
+
* moved note was tied to a partner that isn't also being moved, that
|
|
163
|
+
* partner's now-dangling tie flag is cleared too.
|
|
164
|
+
*/
|
|
165
|
+
export function moveNotesCommand(eventIds, params, label) {
|
|
166
|
+
return transformCommand(label, score => moveNotes(score, eventIds, params));
|
|
167
|
+
}
|
|
168
|
+
// ---- resizeNotesCommand / changeDurationCommand ----------------------------------
|
|
169
|
+
function resizeNotes(score, eventIds, durationTicks) {
|
|
170
|
+
const idSet = new Set(eventIds);
|
|
171
|
+
const tracks = score.tracks.map(track => {
|
|
172
|
+
const measures = track.measures.map(measure => {
|
|
173
|
+
let nextMeasure = measure;
|
|
174
|
+
let changed = false;
|
|
175
|
+
for (const voice of measure.voices) {
|
|
176
|
+
if (!voice.events.some(e => idSet.has(e.id)))
|
|
177
|
+
continue;
|
|
178
|
+
changed = true;
|
|
179
|
+
const measureEnd = measure.startTick + measure.durationTicks;
|
|
180
|
+
const updatedNotes = voice.events.filter(isNoteEvent).map(e => {
|
|
181
|
+
if (!idSet.has(e.id))
|
|
182
|
+
return e;
|
|
183
|
+
const clamped = Math.max(1, Math.min(durationTicks, measureEnd - e.startTick));
|
|
184
|
+
return { ...e, durationTicks: clamped };
|
|
185
|
+
});
|
|
186
|
+
const withResized = {
|
|
187
|
+
...nextMeasure,
|
|
188
|
+
voices: nextMeasure.voices.map(v => v.id === voice.id ? { ...v, events: updatedNotes } : v),
|
|
189
|
+
};
|
|
190
|
+
nextMeasure = reflowVoice(withResized, voice.id, track.id);
|
|
191
|
+
}
|
|
192
|
+
return changed ? nextMeasure : measure;
|
|
193
|
+
});
|
|
194
|
+
const trackChanged = measures.some((m, i) => m !== track.measures[i]);
|
|
195
|
+
return trackChanged ? { ...track, measures } : track;
|
|
196
|
+
});
|
|
197
|
+
return withTracks(score, tracks);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Sets the given notes' duration to `durationTicks` (a raw tick length,
|
|
201
|
+
* e.g. from a piano-roll drag-resize), clamped to fit within each note's
|
|
202
|
+
* current measure (no cross-measure re-splitting), then reflows.
|
|
203
|
+
*/
|
|
204
|
+
export function resizeNotesCommand(eventIds, durationTicks, label) {
|
|
205
|
+
return transformCommand(label, score => resizeNotes(score, eventIds, durationTicks));
|
|
206
|
+
}
|
|
207
|
+
/** Sets the given notes' duration to a named value (e.g. from the inspector), converted to ticks via the score's ppq. */
|
|
208
|
+
export function changeDurationCommand(eventIds, duration, label) {
|
|
209
|
+
return transformCommand(label, score => resizeNotes(score, eventIds, ticksFor(duration, score.ppq)));
|
|
210
|
+
}
|
|
211
|
+
// ---- simple per-note field commands ------------------------------------------
|
|
212
|
+
/** Sets the given notes' pitch to an absolute `Pitch`. */
|
|
213
|
+
export function changePitchCommand(eventIds, pitch, label) {
|
|
214
|
+
return transformCommand(label, score => mapNotes(score, eventIds, note => ({ ...note, pitch })));
|
|
215
|
+
}
|
|
216
|
+
/** Sets the given notes' velocity (0-127). */
|
|
217
|
+
export function changeVelocityCommand(eventIds, velocity, label) {
|
|
218
|
+
return transformCommand(label, score => mapNotes(score, eventIds, note => ({ ...note, velocity })));
|
|
219
|
+
}
|
|
220
|
+
/** Sets (or clears, when `articulation` is `undefined`) the given notes' articulation. */
|
|
221
|
+
export function changeArticulationCommand(eventIds, articulation, label) {
|
|
222
|
+
return transformCommand(label, score => mapNotes(score, eventIds, note => {
|
|
223
|
+
if (articulation)
|
|
224
|
+
return { ...note, articulation };
|
|
225
|
+
const updated = { ...note };
|
|
226
|
+
delete updated.articulation;
|
|
227
|
+
return updated;
|
|
228
|
+
}));
|
|
229
|
+
}
|
|
230
|
+
/** Sets the given notes' pitch accidental, keeping their step/octave. */
|
|
231
|
+
export function changeAccidentalCommand(eventIds, accidental, label) {
|
|
232
|
+
return transformCommand(label, score => mapNotes(score, eventIds, note => ({
|
|
233
|
+
...note,
|
|
234
|
+
pitch: { ...note.pitch, accidental },
|
|
235
|
+
})));
|
|
236
|
+
}
|
|
237
|
+
/** Toggles `tieStart` or `tieStop` on the given notes. */
|
|
238
|
+
export function toggleTieCommand(eventIds, which, label) {
|
|
239
|
+
return transformCommand(label, score => mapNotes(score, eventIds, note => ({ ...note, [which]: !note[which] })));
|
|
240
|
+
}
|
|
241
|
+
// ---- changeVoiceCommand -----------------------------------------------------------
|
|
242
|
+
function changeVoice(score, eventIds, targetVoiceIndex) {
|
|
243
|
+
const idSet = new Set(eventIds);
|
|
244
|
+
const tracks = score.tracks.map(track => {
|
|
245
|
+
const measures = track.measures.map(measure => {
|
|
246
|
+
const matches = [];
|
|
247
|
+
measure.voices.forEach(voice => {
|
|
248
|
+
voice.events.forEach(e => {
|
|
249
|
+
if (isNoteEvent(e) && idSet.has(e.id))
|
|
250
|
+
matches.push(e);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
if (matches.length === 0)
|
|
254
|
+
return measure;
|
|
255
|
+
let nextMeasure = measure;
|
|
256
|
+
for (const voice of measure.voices) {
|
|
257
|
+
if (!voice.events.some(e => idSet.has(e.id)))
|
|
258
|
+
continue;
|
|
259
|
+
const remaining = voice.events
|
|
260
|
+
.filter(isNoteEvent)
|
|
261
|
+
.filter(e => !idSet.has(e.id));
|
|
262
|
+
const withRemoved = {
|
|
263
|
+
...nextMeasure,
|
|
264
|
+
voices: nextMeasure.voices.map(v => v.id === voice.id ? { ...v, events: remaining } : v),
|
|
265
|
+
};
|
|
266
|
+
nextMeasure = reflowVoice(withRemoved, voice.id, track.id);
|
|
267
|
+
}
|
|
268
|
+
nextMeasure = ensureVoiceAtIndex(nextMeasure, targetVoiceIndex, track.id);
|
|
269
|
+
const targetVoice = nextMeasure.voices[targetVoiceIndex];
|
|
270
|
+
const existingNotes = targetVoice.events.filter(isNoteEvent);
|
|
271
|
+
const movedNotes = matches.map(n => ({
|
|
272
|
+
...n,
|
|
273
|
+
voiceId: targetVoice.id,
|
|
274
|
+
}));
|
|
275
|
+
nextMeasure = {
|
|
276
|
+
...nextMeasure,
|
|
277
|
+
voices: nextMeasure.voices.map((v, i) => i === targetVoiceIndex
|
|
278
|
+
? { ...v, events: [...existingNotes, ...movedNotes] }
|
|
279
|
+
: v),
|
|
280
|
+
};
|
|
281
|
+
return reflowVoice(nextMeasure, targetVoice.id, track.id);
|
|
282
|
+
});
|
|
283
|
+
const trackChanged = measures.some((m, i) => m !== track.measures[i]);
|
|
284
|
+
return trackChanged ? { ...track, measures } : track;
|
|
285
|
+
});
|
|
286
|
+
return withTracks(score, tracks);
|
|
287
|
+
}
|
|
288
|
+
/** Moves the given notes to the voice at ordinal position `targetVoiceIndex` within their current measure (created if absent). */
|
|
289
|
+
export function changeVoiceCommand(eventIds, targetVoiceIndex, label) {
|
|
290
|
+
return transformCommand(label, score => changeVoice(score, eventIds, targetVoiceIndex));
|
|
291
|
+
}
|
|
292
|
+
//# sourceMappingURL=note-commands.js.map
|