@leafo/lml 0.1.1 → 0.3.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/dist/song.d.ts CHANGED
@@ -1,50 +1,255 @@
1
+ import { type ChordShapeName } from "./music.js";
2
+ /**
3
+ * Represents a single musical note with pitch, timing, and duration.
4
+ * @example
5
+ * const note = new SongNote("C4", 0, 1) // Middle C at beat 0, duration 1 beat
6
+ * const note2 = new SongNote("D#5", 4, 0.5) // D#5 at beat 4, half beat duration
7
+ */
1
8
  export declare class SongNote {
9
+ /** Unique identifier for this note instance */
2
10
  id: symbol;
11
+ /** Note name with octave (e.g., "C4", "D#5", "Bb3") */
3
12
  note: string;
13
+ /** Start position in beats */
4
14
  start: number;
15
+ /** Duration in beats */
5
16
  duration: number;
6
- constructor(note: string, start: number, duration: number);
17
+ /** Source location in LML text as [startOffset, endOffset], used for editor features */
18
+ sourceLocation?: [number, number];
19
+ /**
20
+ * Creates a new SongNote.
21
+ * @param note - Note name with octave (e.g., "C4", "D#5")
22
+ * @param start - Start position in beats
23
+ * @param duration - Duration in beats
24
+ * @param sourceLocation - Optional source location in LML text [startOffset, endOffset]
25
+ */
26
+ constructor(note: string, start: number, duration: number, sourceLocation?: [number, number]);
27
+ /**
28
+ * Creates a copy of this note.
29
+ * @returns A new SongNote with the same properties
30
+ */
7
31
  clone(): SongNote;
32
+ /**
33
+ * Checks if this note overlaps with a time range.
34
+ * @param min - Start of the range in beats
35
+ * @param max - End of the range in beats
36
+ * @returns True if the note overlaps with the range
37
+ */
8
38
  inRange(min: number, max: number): boolean;
39
+ /**
40
+ * Creates a new note transposed by a number of semitones.
41
+ * @param semitones - Number of semitones to transpose (positive = up, negative = down)
42
+ * @returns A new SongNote with the transposed pitch
43
+ * @example
44
+ * const c4 = new SongNote("C4", 0, 1)
45
+ * const d4 = c4.transpose(2) // D4
46
+ * const b3 = c4.transpose(-1) // B3
47
+ */
9
48
  transpose(semitones: number): SongNote;
49
+ /** Returns the start position in beats. */
10
50
  getStart(): number;
51
+ /** Returns the end position in beats (start + duration). */
11
52
  getStop(): number;
53
+ /** Returns the end position for rendering purposes. */
12
54
  getRenderStop(): number;
55
+ /** Returns a string representation of the note. */
13
56
  toString(): string;
14
57
  }
58
+ /**
59
+ * Metadata associated with a parsed song.
60
+ */
15
61
  export interface SongMetadata {
62
+ /** Key signature as number of accidentals (positive = sharps, negative = flats) */
16
63
  keySignature?: number;
64
+ /** Number of beats per measure */
17
65
  beatsPerMeasure?: number;
66
+ /** Frontmatter key-value pairs from the LML source */
67
+ frontmatter?: Record<string, string>;
18
68
  }
69
+ /**
70
+ * A list of notes representing a song or track, with associated metadata.
71
+ * Extends Array<SongNote> to allow direct indexing and array methods.
72
+ * @example
73
+ * const song = SongNoteList.newSong([
74
+ * ["C4", 0, 1],
75
+ * ["E4", 1, 1],
76
+ * ["G4", 2, 1],
77
+ * ])
78
+ * song.transpose(2) // Transpose up a whole step
79
+ */
19
80
  export declare class SongNoteList extends Array<SongNote> {
81
+ /** Bucket size in beats for spatial indexing optimization */
20
82
  static bucketSize: number;
83
+ /** Song metadata including key signature and time signature */
21
84
  metadata?: SongMetadata;
22
- autoChords?: [number, [string, string]][];
85
+ /** Auto chord markers as [beat_position, [root, chord_shape]][] */
86
+ autoChords?: [number, [string, ChordShapeName]][];
87
+ /** Clef changes as [beat_position, clef_name][] */
23
88
  clefs?: [number, string][];
89
+ /** String annotations as [beat_position, text][] */
90
+ strings?: [number, string][];
91
+ /** Time signature changes as [beat_position, beats_per_measure][] */
92
+ timeSignatures?: [number, number][];
93
+ /** Key signature changes as [beat_position, accidental_count, source_location][] */
94
+ keySignatures?: [number, number, [number, number]][];
95
+ /** Optional track name */
24
96
  trackName?: string;
97
+ /** Cached spatial index buckets for fast note lookup */
25
98
  private buckets?;
26
99
  constructor();
100
+ /**
101
+ * Creates a new SongNoteList from an array of note tuples.
102
+ * @param noteTuples - Array of [note, start, duration] tuples
103
+ * @returns A new SongNoteList containing the notes
104
+ * @example
105
+ * const song = SongNoteList.newSong([
106
+ * ["C4", 0, 1],
107
+ * ["D4", 1, 1],
108
+ * ])
109
+ */
27
110
  static newSong(noteTuples: [string, number, number][]): SongNoteList;
111
+ /**
112
+ * Creates a deep copy of this song (notes are cloned).
113
+ * @returns A new SongNoteList with cloned notes
114
+ */
28
115
  clone(): SongNoteList;
116
+ /**
117
+ * Clears the spatial index cache. Call this after modifying notes.
118
+ */
29
119
  clearCache(): void;
120
+ /**
121
+ * Creates a new song with all notes and key signatures transposed.
122
+ * Uses circle of fifths for key signature transposition.
123
+ * Returns `this` if amount is 0.
124
+ * @param amount - Number of semitones to transpose (positive = up, negative = down)
125
+ * @returns A new SongNoteList with transposed notes, or `this` if amount is 0
126
+ * @example
127
+ * const song = parser.compile(parser.parse("ks0 c d e"))
128
+ * const transposed = song.transpose(1) // Transposes to Db major
129
+ * // transposed.metadata.keySignature === -5
130
+ */
30
131
  transpose(amount?: number): SongNoteList;
132
+ /**
133
+ * Transposes notes from this song into the target song.
134
+ * Override in subclasses for custom note handling (e.g., multi-track).
135
+ * @param target - The song to add transposed notes to
136
+ * @param amount - Number of semitones to transpose
137
+ */
138
+ protected transposeNotesInto(target: SongNoteList, amount: number): void;
139
+ /**
140
+ * Finds all notes that overlap with a time range.
141
+ * @param start - Start of the range in beats
142
+ * @param stop - End of the range in beats
143
+ * @returns Array of notes that overlap with the range
144
+ */
31
145
  notesInRange(start: number, stop: number): SongNote[];
146
+ /**
147
+ * Finds indices of notes whose source location overlaps with a text selection.
148
+ * Used for editor features like transposition of selected notes.
149
+ * @param start - Start offset in source text
150
+ * @param end - End offset in source text
151
+ * @returns Set of note indices that overlap with the selection
152
+ */
153
+ findNotesForSelection(start: number, end: number): Set<number>;
154
+ /**
155
+ * Finds key signatures whose source location overlaps with a text selection.
156
+ * Used for editor features like transposition of selected key signatures.
157
+ * @param start - Start offset in source text
158
+ * @param end - End offset in source text
159
+ * @returns Array of key signature entries that overlap with the selection
160
+ */
161
+ findKeySignaturesForSelection(start: number, end: number): [number, number, [number, number]][];
162
+ /**
163
+ * Returns the end position of the last note in beats.
164
+ * @returns End position in beats, or 0 if empty
165
+ */
32
166
  getStopInBeats(): number;
167
+ /**
168
+ * Returns the start position of the first note in beats.
169
+ * @returns Start position in beats, or 0 if empty
170
+ */
33
171
  getStartInBeats(): number;
172
+ /**
173
+ * Calculates measure boundaries based on time signatures.
174
+ * @returns Array of measure objects with start position and beat count
175
+ */
176
+ getMeasures(): {
177
+ start: number;
178
+ beats: number;
179
+ }[];
180
+ /**
181
+ * Returns the pitch range of all notes in the song.
182
+ * @returns Tuple of [lowest, highest] note names, or undefined if empty
183
+ */
34
184
  noteRange(): [string, string] | undefined;
185
+ /**
186
+ * Determines the best staff type for displaying this song.
187
+ * @returns "treble", "bass", or "grand" based on note range and clef settings
188
+ */
35
189
  fittingStaff(): "treble" | "bass" | "grand";
190
+ /** Calculates the bucket range for a time span. */
36
191
  private getBucketRange;
192
+ /** Builds the spatial index buckets for fast note lookup. */
37
193
  private buildBuckets;
194
+ /** Gets the buckets to scan when matching notes near a beat. */
38
195
  private adjacentBuckets;
196
+ /** Gets or builds the spatial index buckets. */
39
197
  private getBuckets;
198
+ /**
199
+ * Finds the note closest to a beat position using spatial indexing.
200
+ * Faster than matchNote for large songs.
201
+ * @param findNote - Note name to search for (e.g., "C4")
202
+ * @param beat - Beat position to search near
203
+ * @param wrapRight - Optional right boundary for wrap-around search
204
+ * @param wrapLeft - Optional left boundary for wrap-around search
205
+ * @returns Index of the matching note, or null if not found
206
+ */
40
207
  matchNoteFast(findNote: string, beat: number, wrapRight?: number, wrapLeft?: number): number | null;
208
+ /**
209
+ * Finds the note closest to a beat position using linear search.
210
+ * Use matchNoteFast for better performance on large songs.
211
+ * @param findNote - Note name to search for (e.g., "C4")
212
+ * @param beat - Beat position to search near
213
+ * @returns Index of the matching note, or null if not found
214
+ */
41
215
  matchNote(findNote: string, beat: number): number | null;
42
216
  }
217
+ /**
218
+ * A song containing multiple tracks, where each track is a SongNoteList.
219
+ * Notes are stored both in the main list and in their respective tracks.
220
+ * @example
221
+ * const song = new MultiTrackSong()
222
+ * song.pushWithTrack(new SongNote("C4", 0, 1), 0) // Track 0
223
+ * song.pushWithTrack(new SongNote("E4", 0, 1), 1) // Track 1
224
+ */
43
225
  export declare class MultiTrackSong extends SongNoteList {
226
+ /** Array of individual tracks */
44
227
  tracks: SongNoteList[];
45
228
  constructor();
229
+ /**
230
+ * Adds a note to both the main song and a specific track.
231
+ * @param note - The note to add
232
+ * @param trackIdx - The track index to add the note to
233
+ * @returns The added note
234
+ */
46
235
  pushWithTrack(note: SongNote, trackIdx: number): SongNote;
236
+ /**
237
+ * Finds an unused track index for auto-generated content like chords.
238
+ * @returns The next available track index
239
+ */
47
240
  findEmptyTrackIdx(): number;
241
+ /**
242
+ * Gets a track by index, creating it if it doesn't exist.
243
+ * @param idx - The track index
244
+ * @returns The SongNoteList for the track
245
+ */
48
246
  getTrack(idx: number): SongNoteList;
247
+ /**
248
+ * Transposes notes from this song into the target song, handling tracks.
249
+ * Notes are transposed per-track and added to both the track and main list.
250
+ * @param target - The MultiTrackSong to add transposed notes to
251
+ * @param amount - Number of semitones to transpose
252
+ */
253
+ protected transposeNotesInto(target: SongNoteList, amount: number): void;
49
254
  }
50
255
  //# sourceMappingURL=song.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"song.d.ts","sourceRoot":"","sources":["../src/song.ts"],"names":[],"mappings":"AAKA,qBAAa,QAAQ;IACnB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;gBAEJ,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;IAOzD,KAAK,IAAI,QAAQ;IAMjB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO;IAS1C,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,QAAQ;IAMtC,QAAQ,IAAI,MAAM;IAIlB,OAAO,IAAI,MAAM;IAIjB,aAAa,IAAI,MAAM;IAIvB,QAAQ,IAAI,MAAM;CAGnB;AAED,MAAM,WAAW,YAAY;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB;AAGD,qBAAa,YAAa,SAAQ,KAAK,CAAC,QAAQ,CAAC;IAC/C,MAAM,CAAC,UAAU,SAAI;IAErB,QAAQ,CAAC,EAAE,YAAY,CAAA;IACvB,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,CAAA;IACzC,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAA;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAA;IAElB,OAAO,CAAC,OAAO,CAAC,CAA0B;;IAO1C,MAAM,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,YAAY;IAYpE,KAAK,IAAI,YAAY;IAUrB,UAAU,IAAI,IAAI;IAIlB,SAAS,CAAC,MAAM,SAAI,GAAG,YAAY;IAenC,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,QAAQ,EAAE;IAIrD,cAAc,IAAI,MAAM;IAKxB,eAAe,IAAI,MAAM;IAKzB,SAAS,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS;IAoBzC,YAAY,IAAI,QAAQ,GAAG,MAAM,GAAG,OAAO;IAyC3C,OAAO,CAAC,cAAc;IAQtB,OAAO,CAAC,YAAY;IAcpB,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,UAAU;IAQlB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAqDnG,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;CAsBzD;AAED,qBAAa,cAAe,SAAQ,YAAY;IAC9C,MAAM,EAAE,YAAY,EAAE,CAAK;;IAO3B,aAAa,CAAC,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,GAAG,QAAQ;IAQzD,iBAAiB,IAAI,MAAM;IAI3B,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY;CAOpC"}
1
+ {"version":3,"file":"song.d.ts","sourceRoot":"","sources":["../src/song.ts"],"names":[],"mappings":"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"}
package/dist/song.js CHANGED
@@ -1,17 +1,39 @@
1
- import { parseNote, noteName, MIDDLE_C_PITCH } from "./music.js";
2
- // note: C4, D#5, etc...
3
- // start: when note begins in beats
4
- // duration: how long note is in beats
1
+ import { parseNote, noteName, MIDDLE_C_PITCH, transposeKeySignature } from "./music.js";
2
+ /**
3
+ * Represents a single musical note with pitch, timing, and duration.
4
+ * @example
5
+ * const note = new SongNote("C4", 0, 1) // Middle C at beat 0, duration 1 beat
6
+ * const note2 = new SongNote("D#5", 4, 0.5) // D#5 at beat 4, half beat duration
7
+ */
5
8
  export class SongNote {
6
- constructor(note, start, duration) {
9
+ /**
10
+ * Creates a new SongNote.
11
+ * @param note - Note name with octave (e.g., "C4", "D#5")
12
+ * @param start - Start position in beats
13
+ * @param duration - Duration in beats
14
+ * @param sourceLocation - Optional source location in LML text [startOffset, endOffset]
15
+ */
16
+ constructor(note, start, duration, sourceLocation) {
7
17
  this.id = Symbol();
8
18
  this.note = note;
9
19
  this.start = start;
10
20
  this.duration = duration;
21
+ this.sourceLocation = sourceLocation;
11
22
  }
23
+ /**
24
+ * Creates a copy of this note.
25
+ * @returns A new SongNote with the same properties
26
+ */
12
27
  clone() {
13
- return new SongNote(this.note, this.start, this.duration);
28
+ const cloned = new SongNote(this.note, this.start, this.duration, this.sourceLocation);
29
+ return cloned;
14
30
  }
31
+ /**
32
+ * Checks if this note overlaps with a time range.
33
+ * @param min - Start of the range in beats
34
+ * @param max - End of the range in beats
35
+ * @returns True if the note overlaps with the range
36
+ */
15
37
  inRange(min, max) {
16
38
  const stop = this.start + this.duration;
17
39
  if (min >= stop) {
@@ -22,28 +44,61 @@ export class SongNote {
22
44
  }
23
45
  return true;
24
46
  }
47
+ /**
48
+ * Creates a new note transposed by a number of semitones.
49
+ * @param semitones - Number of semitones to transpose (positive = up, negative = down)
50
+ * @returns A new SongNote with the transposed pitch
51
+ * @example
52
+ * const c4 = new SongNote("C4", 0, 1)
53
+ * const d4 = c4.transpose(2) // D4
54
+ * const b3 = c4.transpose(-1) // B3
55
+ */
25
56
  transpose(semitones) {
26
- return new SongNote(noteName(parseNote(this.note) + semitones), this.start, this.duration);
57
+ return new SongNote(noteName(parseNote(this.note) + semitones), this.start, this.duration, this.sourceLocation);
27
58
  }
59
+ /** Returns the start position in beats. */
28
60
  getStart() {
29
61
  return this.start;
30
62
  }
63
+ /** Returns the end position in beats (start + duration). */
31
64
  getStop() {
32
65
  return this.start + this.duration;
33
66
  }
67
+ /** Returns the end position for rendering purposes. */
34
68
  getRenderStop() {
35
69
  return this.start + this.duration;
36
70
  }
71
+ /** Returns a string representation of the note. */
37
72
  toString() {
38
73
  return `${this.note},${this.start},${this.duration}`;
39
74
  }
40
75
  }
41
- // like note list but notes in time
76
+ /**
77
+ * A list of notes representing a song or track, with associated metadata.
78
+ * Extends Array<SongNote> to allow direct indexing and array methods.
79
+ * @example
80
+ * const song = SongNoteList.newSong([
81
+ * ["C4", 0, 1],
82
+ * ["E4", 1, 1],
83
+ * ["G4", 2, 1],
84
+ * ])
85
+ * song.transpose(2) // Transpose up a whole step
86
+ */
42
87
  export class SongNoteList extends Array {
43
88
  constructor() {
44
89
  super();
45
90
  Object.setPrototypeOf(this, SongNoteList.prototype);
46
91
  }
92
+ /**
93
+ * Creates a new SongNoteList from an array of note tuples.
94
+ * @param noteTuples - Array of [note, start, duration] tuples
95
+ * @returns A new SongNoteList containing the notes
96
+ * @example
97
+ * const song = SongNoteList.newSong([
98
+ * ["C4", 0, 1],
99
+ * ["D4", 1, 1],
100
+ * ])
101
+ */
47
102
  static newSong(noteTuples) {
48
103
  const notes = noteTuples.map(([note, start, duration]) => new SongNote(note, start, duration));
49
104
  const song = new SongNoteList();
@@ -52,38 +107,157 @@ export class SongNoteList extends Array {
52
107
  }
53
108
  return song;
54
109
  }
110
+ /**
111
+ * Creates a deep copy of this song (notes are cloned).
112
+ * @returns A new SongNoteList with cloned notes
113
+ */
55
114
  clone() {
56
115
  const song = new SongNoteList();
57
116
  this.forEach(note => song.push(note.clone()));
58
117
  return song;
59
118
  }
119
+ /**
120
+ * Clears the spatial index cache. Call this after modifying notes.
121
+ */
60
122
  clearCache() {
61
123
  delete this.buckets;
62
124
  }
125
+ /**
126
+ * Creates a new song with all notes and key signatures transposed.
127
+ * Uses circle of fifths for key signature transposition.
128
+ * Returns `this` if amount is 0.
129
+ * @param amount - Number of semitones to transpose (positive = up, negative = down)
130
+ * @returns A new SongNoteList with transposed notes, or `this` if amount is 0
131
+ * @example
132
+ * const song = parser.compile(parser.parse("ks0 c d e"))
133
+ * const transposed = song.transpose(1) // Transposes to Db major
134
+ * // transposed.metadata.keySignature === -5
135
+ */
63
136
  transpose(amount = 0) {
64
137
  if (amount == 0) {
65
138
  return this;
66
139
  }
67
- const song = new SongNoteList();
68
- this.forEach(note => song.push(note.transpose(amount)));
140
+ const song = new this.constructor();
141
+ this.transposeNotesInto(song, amount);
142
+ // Copy metadata, transposing key signature
143
+ if (this.metadata) {
144
+ song.metadata = {
145
+ ...this.metadata,
146
+ keySignature: this.metadata.keySignature !== undefined
147
+ ? transposeKeySignature(this.metadata.keySignature, amount)
148
+ : undefined
149
+ };
150
+ }
151
+ // Transpose key signatures array
152
+ if (this.keySignatures) {
153
+ song.keySignatures = this.keySignatures.map(([beat, count, loc]) => [beat, transposeKeySignature(count, amount), loc]);
154
+ }
155
+ // Reuse references to unchanged properties
156
+ song.timeSignatures = this.timeSignatures;
157
+ song.clefs = this.clefs;
158
+ song.strings = this.strings;
159
+ song.autoChords = this.autoChords;
160
+ song.trackName = this.trackName;
69
161
  return song;
70
162
  }
71
- // find the notes that fall in the time range
163
+ /**
164
+ * Transposes notes from this song into the target song.
165
+ * Override in subclasses for custom note handling (e.g., multi-track).
166
+ * @param target - The song to add transposed notes to
167
+ * @param amount - Number of semitones to transpose
168
+ */
169
+ transposeNotesInto(target, amount) {
170
+ this.forEach(note => target.push(note.transpose(amount)));
171
+ }
172
+ /**
173
+ * Finds all notes that overlap with a time range.
174
+ * @param start - Start of the range in beats
175
+ * @param stop - End of the range in beats
176
+ * @returns Array of notes that overlap with the range
177
+ */
72
178
  notesInRange(start, stop) {
73
179
  return [...this.filter((n) => n.inRange(start, stop))];
74
180
  }
181
+ /**
182
+ * Finds indices of notes whose source location overlaps with a text selection.
183
+ * Used for editor features like transposition of selected notes.
184
+ * @param start - Start offset in source text
185
+ * @param end - End offset in source text
186
+ * @returns Set of note indices that overlap with the selection
187
+ */
188
+ findNotesForSelection(start, end) {
189
+ const result = new Set();
190
+ this.forEach((note, index) => {
191
+ if (!note.sourceLocation)
192
+ return;
193
+ const [noteStart, noteEnd] = note.sourceLocation;
194
+ if (start <= noteEnd && end >= noteStart) {
195
+ result.add(index);
196
+ }
197
+ });
198
+ return result;
199
+ }
200
+ /**
201
+ * Finds key signatures whose source location overlaps with a text selection.
202
+ * Used for editor features like transposition of selected key signatures.
203
+ * @param start - Start offset in source text
204
+ * @param end - End offset in source text
205
+ * @returns Array of key signature entries that overlap with the selection
206
+ */
207
+ findKeySignaturesForSelection(start, end) {
208
+ if (!this.keySignatures)
209
+ return [];
210
+ return this.keySignatures.filter(([, , [ksStart, ksEnd]]) => start <= ksEnd && end >= ksStart);
211
+ }
212
+ /**
213
+ * Returns the end position of the last note in beats.
214
+ * @returns End position in beats, or 0 if empty
215
+ */
75
216
  getStopInBeats() {
76
217
  if (this.length == 0) {
77
218
  return 0;
78
219
  }
79
220
  return Math.max.apply(null, this.map((n) => n.getStop()));
80
221
  }
222
+ /**
223
+ * Returns the start position of the first note in beats.
224
+ * @returns Start position in beats, or 0 if empty
225
+ */
81
226
  getStartInBeats() {
82
227
  if (this.length == 0) {
83
228
  return 0;
84
229
  }
85
230
  return Math.min.apply(null, this.map((n) => n.getStart()));
86
231
  }
232
+ /**
233
+ * Calculates measure boundaries based on time signatures.
234
+ * @returns Array of measure objects with start position and beat count
235
+ */
236
+ getMeasures() {
237
+ const measures = [];
238
+ const songEnd = this.getStopInBeats();
239
+ if (songEnd === 0)
240
+ return measures;
241
+ // Default to 4 beats per measure if no time signatures
242
+ const timeSigs = this.timeSignatures ?? [[0, 4]];
243
+ let position = 0;
244
+ let sigIndex = 0;
245
+ let currentBeats = timeSigs[0]?.[1] ?? 4;
246
+ while (position < songEnd) {
247
+ // Check if time signature changes at or before current position
248
+ while (sigIndex < timeSigs.length && timeSigs[sigIndex][0] <= position) {
249
+ currentBeats = timeSigs[sigIndex][1];
250
+ sigIndex++;
251
+ }
252
+ measures.push({ start: position, beats: currentBeats });
253
+ position += currentBeats;
254
+ }
255
+ return measures;
256
+ }
257
+ /**
258
+ * Returns the pitch range of all notes in the song.
259
+ * @returns Tuple of [lowest, highest] note names, or undefined if empty
260
+ */
87
261
  noteRange() {
88
262
  if (!this.length) {
89
263
  return undefined;
@@ -101,6 +275,10 @@ export class SongNoteList extends Array {
101
275
  }
102
276
  return [noteName(min), noteName(max)];
103
277
  }
278
+ /**
279
+ * Determines the best staff type for displaying this song.
280
+ * @returns "treble", "bass", or "grand" based on note range and clef settings
281
+ */
104
282
  fittingStaff() {
105
283
  if (this.clefs && this.clefs.length == 1) {
106
284
  const firstNote = this[0];
@@ -138,12 +316,14 @@ export class SongNoteList extends Array {
138
316
  return "treble";
139
317
  }
140
318
  }
319
+ /** Calculates the bucket range for a time span. */
141
320
  getBucketRange(start, stop) {
142
321
  const bucketSize = SongNoteList.bucketSize;
143
322
  const left = Math.floor(start / bucketSize);
144
323
  const right = Math.ceil(stop / bucketSize);
145
324
  return [left, right];
146
325
  }
326
+ /** Builds the spatial index buckets for fast note lookup. */
147
327
  buildBuckets() {
148
328
  const buckets = {};
149
329
  this.forEach((songNote, idx) => {
@@ -156,16 +336,26 @@ export class SongNoteList extends Array {
156
336
  });
157
337
  return buckets;
158
338
  }
159
- // get the buckets to scan to match notes for beat
339
+ /** Gets the buckets to scan when matching notes near a beat. */
160
340
  adjacentBuckets(beat) {
161
341
  return this.getBucketRange(beat - 1, beat + 1);
162
342
  }
343
+ /** Gets or builds the spatial index buckets. */
163
344
  getBuckets() {
164
345
  if (!this.buckets) {
165
346
  this.buckets = this.buildBuckets();
166
347
  }
167
348
  return this.buckets;
168
349
  }
350
+ /**
351
+ * Finds the note closest to a beat position using spatial indexing.
352
+ * Faster than matchNote for large songs.
353
+ * @param findNote - Note name to search for (e.g., "C4")
354
+ * @param beat - Beat position to search near
355
+ * @param wrapRight - Optional right boundary for wrap-around search
356
+ * @param wrapLeft - Optional left boundary for wrap-around search
357
+ * @returns Index of the matching note, or null if not found
358
+ */
169
359
  matchNoteFast(findNote, beat, wrapRight, wrapLeft) {
170
360
  const buckets = this.getBuckets();
171
361
  const [left, right] = this.adjacentBuckets(beat);
@@ -213,7 +403,13 @@ export class SongNoteList extends Array {
213
403
  }
214
404
  return foundIdx;
215
405
  }
216
- // see if we're hitting a valid note
406
+ /**
407
+ * Finds the note closest to a beat position using linear search.
408
+ * Use matchNoteFast for better performance on large songs.
409
+ * @param findNote - Note name to search for (e.g., "C4")
410
+ * @param beat - Beat position to search near
411
+ * @returns Index of the matching note, or null if not found
412
+ */
217
413
  matchNote(findNote, beat) {
218
414
  let foundIdx = null;
219
415
  for (let idx = 0; idx < this.length; idx++) {
@@ -234,28 +430,68 @@ export class SongNoteList extends Array {
234
430
  return foundIdx;
235
431
  }
236
432
  }
237
- SongNoteList.bucketSize = 8; // bucket size in beats
433
+ /** Bucket size in beats for spatial indexing optimization */
434
+ SongNoteList.bucketSize = 8;
435
+ /**
436
+ * A song containing multiple tracks, where each track is a SongNoteList.
437
+ * Notes are stored both in the main list and in their respective tracks.
438
+ * @example
439
+ * const song = new MultiTrackSong()
440
+ * song.pushWithTrack(new SongNote("C4", 0, 1), 0) // Track 0
441
+ * song.pushWithTrack(new SongNote("E4", 0, 1), 1) // Track 1
442
+ */
238
443
  export class MultiTrackSong extends SongNoteList {
239
444
  constructor() {
240
445
  super();
446
+ /** Array of individual tracks */
241
447
  this.tracks = [];
242
448
  Object.setPrototypeOf(this, MultiTrackSong.prototype);
243
449
  }
450
+ /**
451
+ * Adds a note to both the main song and a specific track.
452
+ * @param note - The note to add
453
+ * @param trackIdx - The track index to add the note to
454
+ * @returns The added note
455
+ */
244
456
  pushWithTrack(note, trackIdx) {
245
457
  this.push(note);
246
458
  const track = this.getTrack(trackIdx);
247
459
  track.push(note);
248
460
  return note;
249
461
  }
250
- // find an empty track to put autochords in
462
+ /**
463
+ * Finds an unused track index for auto-generated content like chords.
464
+ * @returns The next available track index
465
+ */
251
466
  findEmptyTrackIdx() {
252
467
  return this.tracks.length + 1;
253
468
  }
469
+ /**
470
+ * Gets a track by index, creating it if it doesn't exist.
471
+ * @param idx - The track index
472
+ * @returns The SongNoteList for the track
473
+ */
254
474
  getTrack(idx) {
255
475
  if (!this.tracks[idx]) {
256
476
  this.tracks[idx] = new SongNoteList();
257
477
  }
258
478
  return this.tracks[idx];
259
479
  }
480
+ /**
481
+ * Transposes notes from this song into the target song, handling tracks.
482
+ * Notes are transposed per-track and added to both the track and main list.
483
+ * @param target - The MultiTrackSong to add transposed notes to
484
+ * @param amount - Number of semitones to transpose
485
+ */
486
+ transposeNotesInto(target, amount) {
487
+ const multiTarget = target;
488
+ this.tracks.forEach((track, idx) => {
489
+ if (track) {
490
+ const transposedTrack = track.transpose(amount);
491
+ multiTarget.tracks[idx] = transposedTrack;
492
+ transposedTrack.forEach(note => multiTarget.push(note));
493
+ }
494
+ });
495
+ }
260
496
  }
261
497
  //# sourceMappingURL=song.js.map