@sudobility/music_drawing 0.0.1

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.
Files changed (57) hide show
  1. package/dist/canvas-renderer.d.ts +180 -0
  2. package/dist/canvas-renderer.d.ts.map +1 -0
  3. package/dist/canvas-renderer.js +831 -0
  4. package/dist/canvas-renderer.js.map +1 -0
  5. package/dist/convert.d.ts +122 -0
  6. package/dist/convert.d.ts.map +1 -0
  7. package/dist/convert.js +426 -0
  8. package/dist/convert.js.map +1 -0
  9. package/dist/display-timing.d.ts +68 -0
  10. package/dist/display-timing.d.ts.map +1 -0
  11. package/dist/display-timing.js +192 -0
  12. package/dist/display-timing.js.map +1 -0
  13. package/dist/icon-canvas.d.ts +12 -0
  14. package/dist/icon-canvas.d.ts.map +1 -0
  15. package/dist/icon-canvas.js +62 -0
  16. package/dist/icon-canvas.js.map +1 -0
  17. package/dist/index.d.ts +30 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +30 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/layout.d.ts +159 -0
  22. package/dist/layout.d.ts.map +1 -0
  23. package/dist/layout.js +362 -0
  24. package/dist/layout.js.map +1 -0
  25. package/dist/measure-content.d.ts +114 -0
  26. package/dist/measure-content.d.ts.map +1 -0
  27. package/dist/measure-content.js +553 -0
  28. package/dist/measure-content.js.map +1 -0
  29. package/dist/note-color.d.ts +48 -0
  30. package/dist/note-color.d.ts.map +1 -0
  31. package/dist/note-color.js +81 -0
  32. package/dist/note-color.js.map +1 -0
  33. package/dist/pagination.d.ts +57 -0
  34. package/dist/pagination.d.ts.map +1 -0
  35. package/dist/pagination.js +152 -0
  36. package/dist/pagination.js.map +1 -0
  37. package/dist/percussion.d.ts +47 -0
  38. package/dist/percussion.d.ts.map +1 -0
  39. package/dist/percussion.js +172 -0
  40. package/dist/percussion.js.map +1 -0
  41. package/dist/playhead.d.ts +37 -0
  42. package/dist/playhead.d.ts.map +1 -0
  43. package/dist/playhead.js +89 -0
  44. package/dist/playhead.js.map +1 -0
  45. package/dist/test/canvas-stub.d.ts +21 -0
  46. package/dist/test/canvas-stub.d.ts.map +1 -0
  47. package/dist/test/canvas-stub.js +51 -0
  48. package/dist/test/canvas-stub.js.map +1 -0
  49. package/dist/test/fixtures.d.ts +47 -0
  50. package/dist/test/fixtures.d.ts.map +1 -0
  51. package/dist/test/fixtures.js +433 -0
  52. package/dist/test/fixtures.js.map +1 -0
  53. package/dist/types.d.ts +74 -0
  54. package/dist/types.d.ts.map +1 -0
  55. package/dist/types.js +9 -0
  56. package/dist/types.js.map +1 -0
  57. package/package.json +62 -0
@@ -0,0 +1,89 @@
1
+ import { measureAtXInSystem, systemAtY } from './layout.js';
2
+ /** The score's shared measure grid (first track — every track shares it once `rebuildMeasureTicks` has run; same convention as `store/selectors.ts`). */
3
+ function measureTimings(score) {
4
+ return score.tracks[0]?.measures ?? [];
5
+ }
6
+ /** Binary search over ascending `startTick`s for the measure containing `tick`; clamps below/above the score to the first/last measure. */
7
+ function measureIndexForTick(timings, tick) {
8
+ if (tick < timings[0].startTick)
9
+ return 0;
10
+ let lo = 0;
11
+ let hi = timings.length - 1;
12
+ while (lo <= hi) {
13
+ const mid = (lo + hi) >> 1;
14
+ const m = timings[mid];
15
+ if (tick < m.startTick)
16
+ hi = mid - 1;
17
+ else if (tick >= m.startTick + m.durationTicks)
18
+ lo = mid + 1;
19
+ else
20
+ return mid;
21
+ }
22
+ return timings.length - 1;
23
+ }
24
+ /** Binary search over systems by their (ascending) measure-index ranges. */
25
+ function systemForMeasureIndex(plan, measureIndex) {
26
+ const systems = plan.systems;
27
+ let lo = 0;
28
+ let hi = systems.length - 1;
29
+ while (lo <= hi) {
30
+ const mid = (lo + hi) >> 1;
31
+ const s = systems[mid];
32
+ if (measureIndex < s.measureIndices[0])
33
+ hi = mid - 1;
34
+ else if (measureIndex > s.measureIndices[s.measureIndices.length - 1])
35
+ lo = mid + 1;
36
+ else
37
+ return s;
38
+ }
39
+ return null;
40
+ }
41
+ /**
42
+ * Where the caret for `tick` sits on the canvas, or `null` when there is
43
+ * nothing to draw against (empty score / no layout). Ticks past the end of
44
+ * the score clamp to the final measure's right edge.
45
+ */
46
+ export function caretPositionForTick(plan, score, tick) {
47
+ const timings = measureTimings(score);
48
+ if (timings.length === 0)
49
+ return null;
50
+ const measureIndex = measureIndexForTick(timings, tick);
51
+ const timing = timings[measureIndex];
52
+ const layout = plan.trackLayouts[0]?.measures[measureIndex];
53
+ const system = systemForMeasureIndex(plan, measureIndex);
54
+ if (!layout || !system)
55
+ return null;
56
+ const fraction = timing.durationTicks > 0
57
+ ? Math.min(1, Math.max(0, (tick - timing.startTick) / timing.durationTicks))
58
+ : 0;
59
+ return {
60
+ x: layout.box.x + fraction * layout.box.width,
61
+ yTop: system.yTop,
62
+ yBottom: system.yBottom,
63
+ };
64
+ }
65
+ /**
66
+ * The tick a canvas click at logical `(x, y)` should seek to, or `null`
67
+ * when the point is in dead space between/outside systems. Horizontal
68
+ * positions left/right of a system's measures clamp to that system's
69
+ * first/last measure, so clicking the clef area seeks to the system start.
70
+ */
71
+ export function tickForPoint(plan, score, x, y) {
72
+ const timings = measureTimings(score);
73
+ if (timings.length === 0)
74
+ return null;
75
+ const system = systemAtY(plan, y);
76
+ if (!system)
77
+ return null;
78
+ const hit = measureAtXInSystem(plan, system, x);
79
+ if (!hit)
80
+ return null;
81
+ const timing = timings[hit.measureIndex];
82
+ if (!timing)
83
+ return null;
84
+ const fraction = hit.box.width > 0
85
+ ? Math.min(1, Math.max(0, (x - hit.box.x) / hit.box.width))
86
+ : 0;
87
+ return Math.round(timing.startTick + fraction * timing.durationTicks);
88
+ }
89
+ //# sourceMappingURL=playhead.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playhead.js","sourceRoot":"","sources":["../src/playhead.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * A recording stand-in for `CanvasRenderingContext2D`: every method call is
3
+ * appended to `ops`; every property write is accepted; `measureText`
4
+ * returns a deterministic width (8px/char) so VexFlow text layout math
5
+ * stays finite. Proxy-based so any method VexFlow's `CanvasContext` calls
6
+ * is recorded without this double having to enumerate the whole 2D API —
7
+ * new methods VexFlow starts calling never require test-double updates.
8
+ *
9
+ * Exported from the package root (like `testStoreContext`) so downstream
10
+ * suites — music_app's jsdom setup stubs `HTMLCanvasElement.getContext`
11
+ * with it — share one implementation.
12
+ */
13
+ export type MockOp = {
14
+ method: string;
15
+ args: unknown[];
16
+ };
17
+ export type Mock2DContext = CanvasRenderingContext2D & {
18
+ ops: MockOp[];
19
+ };
20
+ export declare function createMock2DContext(width?: number, height?: number): Mock2DContext;
21
+ //# sourceMappingURL=canvas-stub.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canvas-stub.d.ts","sourceRoot":"","sources":["../../src/test/canvas-stub.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,MAAM,MAAM,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,EAAE,CAAA;CAAE,CAAC;AACzD,MAAM,MAAM,aAAa,GAAG,wBAAwB,GAAG;IAAE,GAAG,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAEzE,wBAAgB,mBAAmB,CAAC,KAAK,SAAM,EAAE,MAAM,SAAM,GAAG,aAAa,CAoC5E"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * A recording stand-in for `CanvasRenderingContext2D`: every method call is
3
+ * appended to `ops`; every property write is accepted; `measureText`
4
+ * returns a deterministic width (8px/char) so VexFlow text layout math
5
+ * stays finite. Proxy-based so any method VexFlow's `CanvasContext` calls
6
+ * is recorded without this double having to enumerate the whole 2D API —
7
+ * new methods VexFlow starts calling never require test-double updates.
8
+ *
9
+ * Exported from the package root (like `testStoreContext`) so downstream
10
+ * suites — music_app's jsdom setup stubs `HTMLCanvasElement.getContext`
11
+ * with it — share one implementation.
12
+ */
13
+ export function createMock2DContext(width = 800, height = 600) {
14
+ const ops = [];
15
+ const state = {
16
+ ops,
17
+ canvas: { width, height },
18
+ measureText: (text) => ({
19
+ width: String(text).length * 8,
20
+ actualBoundingBoxAscent: 8,
21
+ actualBoundingBoxDescent: 2,
22
+ fontBoundingBoxAscent: 8,
23
+ fontBoundingBoxDescent: 2,
24
+ }),
25
+ getTransform: () => ({ a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 }),
26
+ createLinearGradient: () => ({ addColorStop: () => undefined }),
27
+ createRadialGradient: () => ({ addColorStop: () => undefined }),
28
+ getImageData: (_x, _y, w, h) => ({
29
+ width: w,
30
+ height: h,
31
+ data: new Uint8ClampedArray(w * h * 4),
32
+ }),
33
+ };
34
+ return new Proxy(state, {
35
+ get(target, prop) {
36
+ if (prop in target)
37
+ return target[prop];
38
+ const fn = (...args) => {
39
+ ops.push({ method: String(prop), args });
40
+ return undefined;
41
+ };
42
+ target[prop] = fn;
43
+ return fn;
44
+ },
45
+ set(target, prop, value) {
46
+ target[prop] = value;
47
+ return true;
48
+ },
49
+ });
50
+ }
51
+ //# sourceMappingURL=canvas-stub.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canvas-stub.js","sourceRoot":"","sources":["../../src/test/canvas-stub.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,MAAM,UAAU,mBAAmB,CAAC,KAAK,GAAG,GAAG,EAAE,MAAM,GAAG,GAAG;IAC3D,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,MAAM,KAAK,GAAqC;QAC9C,GAAG;QACH,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE;QACzB,WAAW,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC,CAAC;YAC9B,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC;YAC9B,uBAAuB,EAAE,CAAC;YAC1B,wBAAwB,EAAE,CAAC;YAC3B,qBAAqB,EAAE,CAAC;YACxB,sBAAsB,EAAE,CAAC;SAC1B,CAAC;QACF,YAAY,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;QAC5D,oBAAoB,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,YAAY,EAAE,GAAG,EAAE,CAAC,SAAS,EAAE,CAAC;QAC/D,oBAAoB,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,YAAY,EAAE,GAAG,EAAE,CAAC,SAAS,EAAE,CAAC;QAC/D,YAAY,EAAE,CAAC,EAAU,EAAE,EAAU,EAAE,CAAS,EAAE,CAAS,EAAE,EAAE,CAAC,CAAC;YAC/D,KAAK,EAAE,CAAC;YACR,MAAM,EAAE,CAAC;YACT,IAAI,EAAE,IAAI,iBAAiB,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;SACvC,CAAC;KACH,CAAC;IACF,OAAO,IAAI,KAAK,CAAC,KAAK,EAAE;QACtB,GAAG,CAAC,MAAM,EAAE,IAAI;YACd,IAAI,IAAI,IAAI,MAAM;gBAAE,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC;YACxC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAe,EAAE,EAAE;gBAChC,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;gBACzC,OAAO,SAAS,CAAC;YACnB,CAAC,CAAC;YACF,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;YAClB,OAAO,EAAE,CAAC;QACZ,CAAC;QACD,GAAG,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK;YACrB,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;YACrB,OAAO,IAAI,CAAC;QACd,CAAC;KACF,CAA6B,CAAC;AACjC,CAAC"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Deterministic score fixtures shared across the test suite (Task 3 brief).
3
+ * No randomness: ids are generated by a simple per-call counter (not
4
+ * `createId`/`crypto.randomUUID`) and timestamps are a fixed constant, so
5
+ * every call with the same arguments produces a structurally identical
6
+ * `Score`.
7
+ */
8
+ import type { Score } from '@sudobility/music_types';
9
+ import type { RenderTheme } from '../types.js';
10
+ /** An 8-measure C-major "Twinkle Twinkle Little Star" melody on a single piano track. */
11
+ export declare function twinkleScore(): Score;
12
+ /** A 4-measure two-track score: a treble melody and a supporting bass line. */
13
+ export declare function twoTrackScore(): Score;
14
+ /**
15
+ * Three tracks over 2 measures, deliberately plain.
16
+ *
17
+ * For tests about *which* tracks are involved — visibility, active-track
18
+ * resolution, per-track filtering — where two tracks cannot tell "the first
19
+ * one" apart from "the first visible one", and the music itself is beside the
20
+ * point.
21
+ */
22
+ export declare function threeTrackScore(): Score;
23
+ /**
24
+ * Two tracks with sharply different densities: a treble line of 16
25
+ * sixteenth notes per measure over a bass of one whole note per measure.
26
+ * Exercises cross-track timeline sync — the dense track needs a wider
27
+ * measure than `BASE_MEASURE_WIDTH` and must still share barlines and
28
+ * tick-aligned x positions with the sparse track.
29
+ */
30
+ export declare function denseVsSparseScore(): Score;
31
+ /** A 4-measure I-IV-V-I block-chord progression on a single piano track. */
32
+ export declare function chordScore(): Score;
33
+ /**
34
+ * Generates a score with `trackCount` tracks of `measureCount` measures
35
+ * each (4 quarter notes per measure, cycling through a C-major scale), for
36
+ * performance/stress tests. Deterministic and allocation-light.
37
+ */
38
+ export declare function stressScore(trackCount: number, measureCount: number): Score;
39
+ /**
40
+ * A `RenderTheme` for tests: every role gets a distinct, obviously-fake
41
+ * value so an assertion that the wrong role was used fails loudly instead of
42
+ * matching a lookalike hex. Shared here (rather than redeclared per suite)
43
+ * so adding a role to `RenderTheme` breaks in one place, and so consuming
44
+ * apps' jsdom suites can import the same object.
45
+ */
46
+ export declare function testRenderTheme(): RenderTheme;
47
+ //# sourceMappingURL=fixtures.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fixtures.d.ts","sourceRoot":"","sources":["../../src/test/fixtures.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAMV,KAAK,EAGN,MAAM,yBAAyB,CAAC;AAEjC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAqG/C,yFAAyF;AACzF,wBAAgB,YAAY,IAAI,KAAK,CA0DpC;AAED,+EAA+E;AAC/E,wBAAgB,aAAa,IAAI,KAAK,CAkFrC;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,IAAI,KAAK,CAiDvC;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,IAAI,KAAK,CAgF1C;AAED,4EAA4E;AAC5E,wBAAgB,UAAU,IAAI,KAAK,CAsClC;AAID;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,KAAK,CA+D3E;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,IAAI,WAAW,CAY7C"}
@@ -0,0 +1,433 @@
1
+ import { measureDurationTicks, ticksFor } from '@sudobility/music_types';
2
+ const FIXED_TIMESTAMP = '2024-01-01T00:00:00.000Z';
3
+ /** A per-call, per-prefix incrementing id generator (deterministic, cheap). */
4
+ function makeIdFactory() {
5
+ const counters = new Map();
6
+ return {
7
+ next(prefix) {
8
+ const n = counters.get(prefix) ?? 0;
9
+ counters.set(prefix, n + 1);
10
+ return `${prefix}-${n}`;
11
+ },
12
+ };
13
+ }
14
+ /** Builds consecutive single-voice melody measures from a per-measure list of note specs. */
15
+ function buildMelodyMeasures(measuresOfNotes, ppq, timeSignature, keySignature, trackId, ids) {
16
+ const measureTicks = measureDurationTicks(timeSignature, ppq);
17
+ return measuresOfNotes.map((notes, index) => {
18
+ const startTick = index * measureTicks;
19
+ const voiceId = ids.next('voice');
20
+ let cursor = startTick;
21
+ const events = notes.map(spec => {
22
+ const durationTicks = ticksFor(spec.duration, ppq);
23
+ const event = {
24
+ id: ids.next('note'),
25
+ pitch: spec.pitch,
26
+ startTick: cursor,
27
+ durationTicks,
28
+ velocity: 80,
29
+ voiceId,
30
+ trackId,
31
+ };
32
+ cursor += durationTicks;
33
+ return event;
34
+ });
35
+ return {
36
+ id: ids.next('measure'),
37
+ index,
38
+ startTick,
39
+ durationTicks: measureTicks,
40
+ timeSignature,
41
+ keySignature,
42
+ voices: [{ id: voiceId, name: 'Voice 1', events }],
43
+ };
44
+ });
45
+ }
46
+ /** Builds consecutive single-voice measures, each a block chord spanning the full measure. */
47
+ function buildChordMeasures(chordsByMeasure, ppq, timeSignature, keySignature, trackId, ids) {
48
+ const measureTicks = measureDurationTicks(timeSignature, ppq);
49
+ return chordsByMeasure.map((pitches, index) => {
50
+ const startTick = index * measureTicks;
51
+ const voiceId = ids.next('voice');
52
+ const events = pitches.map(pitch => ({
53
+ id: ids.next('note'),
54
+ pitch,
55
+ startTick,
56
+ durationTicks: measureTicks,
57
+ velocity: 80,
58
+ voiceId,
59
+ trackId,
60
+ }));
61
+ return {
62
+ id: ids.next('measure'),
63
+ index,
64
+ startTick,
65
+ durationTicks: measureTicks,
66
+ timeSignature,
67
+ keySignature,
68
+ voices: [{ id: voiceId, name: 'Voice 1', events }],
69
+ };
70
+ });
71
+ }
72
+ const C_MAJOR = { fifths: 0, mode: 'major' };
73
+ const FOUR_FOUR = { numerator: 4, denominator: 4 };
74
+ function naturalPitch(step, octave) {
75
+ return { step, accidental: 0, octave };
76
+ }
77
+ /** An 8-measure C-major "Twinkle Twinkle Little Star" melody on a single piano track. */
78
+ export function twinkleScore() {
79
+ const ids = makeIdFactory();
80
+ const ppq = 480;
81
+ const q = (step, octave = 4) => ({
82
+ pitch: naturalPitch(step, octave),
83
+ duration: 'quarter',
84
+ });
85
+ const h = (step, octave = 4) => ({
86
+ pitch: naturalPitch(step, octave),
87
+ duration: 'half',
88
+ });
89
+ const measures = [
90
+ [q('C'), q('C'), q('G'), q('G')],
91
+ [q('A'), q('A'), h('G')],
92
+ [q('F'), q('F'), q('E'), q('E')],
93
+ [q('D'), q('D'), h('C')],
94
+ [q('G'), q('G'), q('F'), q('F')],
95
+ [q('E'), q('E'), h('D')],
96
+ [q('G'), q('G'), q('F'), q('F')],
97
+ [q('E'), q('E'), h('D')],
98
+ ];
99
+ const trackId = ids.next('track');
100
+ const track = {
101
+ id: trackId,
102
+ name: 'Piano',
103
+ instrumentName: 'Piano',
104
+ midiProgram: 0,
105
+ midiChannel: 0,
106
+ clef: 'treble',
107
+ volume: 1,
108
+ pan: 0,
109
+ muted: false,
110
+ solo: false,
111
+ measures: buildMelodyMeasures(measures, ppq, FOUR_FOUR, C_MAJOR, trackId, ids),
112
+ };
113
+ return {
114
+ id: ids.next('score'),
115
+ version: 1,
116
+ ppq,
117
+ metadata: {
118
+ title: 'Twinkle Twinkle Little Star',
119
+ createdAt: FIXED_TIMESTAMP,
120
+ updatedAt: FIXED_TIMESTAMP,
121
+ },
122
+ tempoMap: [{ id: ids.next('tempo'), tick: 0, bpm: 120 }],
123
+ tracks: [track],
124
+ };
125
+ }
126
+ /** A 4-measure two-track score: a treble melody and a supporting bass line. */
127
+ export function twoTrackScore() {
128
+ const ids = makeIdFactory();
129
+ const ppq = 480;
130
+ const q = (step, octave) => ({
131
+ pitch: naturalPitch(step, octave),
132
+ duration: 'quarter',
133
+ });
134
+ const w = (step, octave) => ({
135
+ pitch: naturalPitch(step, octave),
136
+ duration: 'whole',
137
+ });
138
+ const trebleMeasures = [
139
+ [q('C', 4), q('D', 4), q('E', 4), q('F', 4)],
140
+ [q('G', 4), q('F', 4), q('E', 4), q('D', 4)],
141
+ [q('C', 4), q('D', 4), q('E', 4), q('F', 4)],
142
+ [q('G', 4), q('F', 4), q('E', 4), q('D', 4)],
143
+ ];
144
+ const bassMeasures = [
145
+ [w('C', 2)],
146
+ [w('G', 2)],
147
+ [w('C', 2)],
148
+ [w('G', 2)],
149
+ ];
150
+ const trebleId = ids.next('track');
151
+ const bassId = ids.next('track');
152
+ const treble = {
153
+ id: trebleId,
154
+ name: 'Treble',
155
+ instrumentName: 'Piano',
156
+ midiProgram: 0,
157
+ midiChannel: 0,
158
+ clef: 'treble',
159
+ volume: 1,
160
+ pan: 0,
161
+ muted: false,
162
+ solo: false,
163
+ measures: buildMelodyMeasures(trebleMeasures, ppq, FOUR_FOUR, C_MAJOR, trebleId, ids),
164
+ };
165
+ const bass = {
166
+ id: bassId,
167
+ name: 'Bass',
168
+ instrumentName: 'Acoustic Bass',
169
+ midiProgram: 32,
170
+ midiChannel: 1,
171
+ clef: 'bass',
172
+ volume: 1,
173
+ pan: 0,
174
+ muted: false,
175
+ solo: false,
176
+ measures: buildMelodyMeasures(bassMeasures, ppq, FOUR_FOUR, C_MAJOR, bassId, ids),
177
+ };
178
+ return {
179
+ id: ids.next('score'),
180
+ version: 1,
181
+ ppq,
182
+ metadata: {
183
+ title: 'Two Track Demo',
184
+ createdAt: FIXED_TIMESTAMP,
185
+ updatedAt: FIXED_TIMESTAMP,
186
+ },
187
+ tempoMap: [{ id: ids.next('tempo'), tick: 0, bpm: 120 }],
188
+ tracks: [treble, bass],
189
+ };
190
+ }
191
+ /**
192
+ * Three tracks over 2 measures, deliberately plain.
193
+ *
194
+ * For tests about *which* tracks are involved — visibility, active-track
195
+ * resolution, per-track filtering — where two tracks cannot tell "the first
196
+ * one" apart from "the first visible one", and the music itself is beside the
197
+ * point.
198
+ */
199
+ export function threeTrackScore() {
200
+ const ids = makeIdFactory();
201
+ const ppq = 480;
202
+ const q = (step, octave) => ({
203
+ pitch: naturalPitch(step, octave),
204
+ duration: 'quarter',
205
+ });
206
+ const measures = [
207
+ [q('C', 4), q('D', 4), q('E', 4), q('F', 4)],
208
+ [q('G', 4), q('F', 4), q('E', 4), q('D', 4)],
209
+ ];
210
+ const tracks = ['Alpha', 'Beta', 'Gamma'].map((name, index) => {
211
+ const trackId = ids.next('track');
212
+ return {
213
+ id: trackId,
214
+ name,
215
+ instrumentName: 'Piano',
216
+ midiProgram: 0,
217
+ midiChannel: index,
218
+ clef: 'treble',
219
+ volume: 1,
220
+ pan: 0,
221
+ muted: false,
222
+ solo: false,
223
+ measures: buildMelodyMeasures(measures, ppq, FOUR_FOUR, C_MAJOR, trackId, ids),
224
+ };
225
+ });
226
+ return {
227
+ id: ids.next('score'),
228
+ version: 1,
229
+ ppq,
230
+ metadata: {
231
+ title: 'Three Track Demo',
232
+ createdAt: FIXED_TIMESTAMP,
233
+ updatedAt: FIXED_TIMESTAMP,
234
+ },
235
+ tempoMap: [{ id: ids.next('tempo'), tick: 0, bpm: 120 }],
236
+ tracks,
237
+ };
238
+ }
239
+ /**
240
+ * Two tracks with sharply different densities: a treble line of 16
241
+ * sixteenth notes per measure over a bass of one whole note per measure.
242
+ * Exercises cross-track timeline sync — the dense track needs a wider
243
+ * measure than `BASE_MEASURE_WIDTH` and must still share barlines and
244
+ * tick-aligned x positions with the sparse track.
245
+ */
246
+ export function denseVsSparseScore() {
247
+ const ids = makeIdFactory();
248
+ const ppq = 480;
249
+ const s = (step, octave) => ({
250
+ pitch: naturalPitch(step, octave),
251
+ duration: 'sixteenth',
252
+ });
253
+ const w = (step, octave) => ({
254
+ pitch: naturalPitch(step, octave),
255
+ duration: 'whole',
256
+ });
257
+ const runUp = ['C', 'D', 'E', 'F', 'G', 'A', 'B']
258
+ .flatMap(step => [s(step, 4), s(step, 4)])
259
+ .concat([s('C', 5), s('C', 5)]);
260
+ const denseMeasures = [runUp, runUp, runUp, runUp];
261
+ const sparseMeasures = [
262
+ [w('C', 2)],
263
+ [w('G', 2)],
264
+ [w('C', 2)],
265
+ [w('G', 2)],
266
+ ];
267
+ const denseId = ids.next('track');
268
+ const sparseId = ids.next('track');
269
+ const dense = {
270
+ id: denseId,
271
+ name: 'Dense',
272
+ instrumentName: 'Piano',
273
+ midiProgram: 0,
274
+ midiChannel: 0,
275
+ clef: 'treble',
276
+ volume: 1,
277
+ pan: 0,
278
+ muted: false,
279
+ solo: false,
280
+ measures: buildMelodyMeasures(denseMeasures, ppq, FOUR_FOUR, C_MAJOR, denseId, ids),
281
+ };
282
+ const sparse = {
283
+ id: sparseId,
284
+ name: 'Sparse',
285
+ instrumentName: 'Acoustic Bass',
286
+ midiProgram: 32,
287
+ midiChannel: 1,
288
+ clef: 'bass',
289
+ volume: 1,
290
+ pan: 0,
291
+ muted: false,
292
+ solo: false,
293
+ measures: buildMelodyMeasures(sparseMeasures, ppq, FOUR_FOUR, C_MAJOR, sparseId, ids),
294
+ };
295
+ return {
296
+ id: ids.next('score'),
297
+ version: 1,
298
+ ppq,
299
+ metadata: {
300
+ title: 'Dense vs Sparse',
301
+ createdAt: FIXED_TIMESTAMP,
302
+ updatedAt: FIXED_TIMESTAMP,
303
+ },
304
+ tempoMap: [{ id: ids.next('tempo'), tick: 0, bpm: 120 }],
305
+ tracks: [dense, sparse],
306
+ };
307
+ }
308
+ /** A 4-measure I-IV-V-I block-chord progression on a single piano track. */
309
+ export function chordScore() {
310
+ const ids = makeIdFactory();
311
+ const ppq = 480;
312
+ const chords = [
313
+ [naturalPitch('C', 4), naturalPitch('E', 4), naturalPitch('G', 4)], // I
314
+ [naturalPitch('F', 4), naturalPitch('A', 4), naturalPitch('C', 5)], // IV
315
+ [naturalPitch('G', 4), naturalPitch('B', 4), naturalPitch('D', 5)], // V
316
+ [naturalPitch('C', 4), naturalPitch('E', 4), naturalPitch('G', 4)], // I
317
+ ];
318
+ const trackId = ids.next('track');
319
+ const track = {
320
+ id: trackId,
321
+ name: 'Piano',
322
+ instrumentName: 'Piano',
323
+ midiProgram: 0,
324
+ midiChannel: 0,
325
+ clef: 'treble',
326
+ volume: 1,
327
+ pan: 0,
328
+ muted: false,
329
+ solo: false,
330
+ measures: buildChordMeasures(chords, ppq, FOUR_FOUR, C_MAJOR, trackId, ids),
331
+ };
332
+ return {
333
+ id: ids.next('score'),
334
+ version: 1,
335
+ ppq,
336
+ metadata: {
337
+ title: 'Chord Progression Demo',
338
+ createdAt: FIXED_TIMESTAMP,
339
+ updatedAt: FIXED_TIMESTAMP,
340
+ },
341
+ tempoMap: [{ id: ids.next('tempo'), tick: 0, bpm: 90 }],
342
+ tracks: [track],
343
+ };
344
+ }
345
+ const STRESS_SCALE = ['C', 'D', 'E', 'F', 'G', 'A', 'B'];
346
+ /**
347
+ * Generates a score with `trackCount` tracks of `measureCount` measures
348
+ * each (4 quarter notes per measure, cycling through a C-major scale), for
349
+ * performance/stress tests. Deterministic and allocation-light.
350
+ */
351
+ export function stressScore(trackCount, measureCount) {
352
+ const ids = makeIdFactory();
353
+ const ppq = 480;
354
+ const measureTicks = measureDurationTicks(FOUR_FOUR, ppq);
355
+ const quarterTicks = ticksFor('quarter', ppq);
356
+ const tracks = [];
357
+ for (let t = 0; t < trackCount; t += 1) {
358
+ const trackId = ids.next('track');
359
+ const measures = [];
360
+ for (let m = 0; m < measureCount; m += 1) {
361
+ const startTick = m * measureTicks;
362
+ const voiceId = ids.next('voice');
363
+ const events = [];
364
+ for (let beat = 0; beat < 4; beat += 1) {
365
+ const step = STRESS_SCALE[(t + m + beat) % STRESS_SCALE.length];
366
+ events.push({
367
+ id: ids.next('note'),
368
+ pitch: naturalPitch(step, 4),
369
+ startTick: startTick + beat * quarterTicks,
370
+ durationTicks: quarterTicks,
371
+ velocity: 80,
372
+ voiceId,
373
+ trackId,
374
+ });
375
+ }
376
+ measures.push({
377
+ id: ids.next('measure'),
378
+ index: m,
379
+ startTick,
380
+ durationTicks: measureTicks,
381
+ timeSignature: FOUR_FOUR,
382
+ keySignature: C_MAJOR,
383
+ voices: [{ id: voiceId, name: 'Voice 1', events }],
384
+ });
385
+ }
386
+ tracks.push({
387
+ id: trackId,
388
+ name: `Track ${t + 1}`,
389
+ instrumentName: 'Piano',
390
+ midiProgram: 0,
391
+ midiChannel: t % 16,
392
+ clef: 'treble',
393
+ volume: 1,
394
+ pan: 0,
395
+ muted: false,
396
+ solo: false,
397
+ measures,
398
+ });
399
+ }
400
+ return {
401
+ id: ids.next('score'),
402
+ version: 1,
403
+ ppq,
404
+ metadata: {
405
+ title: `Stress ${trackCount}x${measureCount}`,
406
+ createdAt: FIXED_TIMESTAMP,
407
+ updatedAt: FIXED_TIMESTAMP,
408
+ },
409
+ tempoMap: [{ id: ids.next('tempo'), tick: 0, bpm: 120 }],
410
+ tracks,
411
+ };
412
+ }
413
+ /**
414
+ * A `RenderTheme` for tests: every role gets a distinct, obviously-fake
415
+ * value so an assertion that the wrong role was used fails loudly instead of
416
+ * matching a lookalike hex. Shared here (rather than redeclared per suite)
417
+ * so adding a role to `RenderTheme` breaks in one place, and so consuming
418
+ * apps' jsdom suites can import the same object.
419
+ */
420
+ export function testRenderTheme() {
421
+ return {
422
+ foreground: '#111111',
423
+ noteNormal: '#222222',
424
+ noteInactive: '#888888',
425
+ noteSelected: '#333333',
426
+ noteRegenerated: '#444444',
427
+ notePlaying: '#555555',
428
+ staveActive: '#666666',
429
+ staveInactive: '#777777',
430
+ caret: '#888888',
431
+ };
432
+ }
433
+ //# sourceMappingURL=fixtures.js.map