@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/README.md +273 -40
- package/dist/auto-chords.d.ts +33 -8
- package/dist/auto-chords.d.ts.map +1 -1
- package/dist/auto-chords.js +19 -1
- package/dist/auto-chords.js.map +1 -1
- package/dist/grammar.d.ts.map +1 -1
- package/dist/grammar.js +1371 -283
- package/dist/grammar.js.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/music.d.ts +502 -5
- package/dist/music.d.ts.map +1 -1
- package/dist/music.js +500 -53
- package/dist/music.js.map +1 -1
- package/dist/note-utils.d.ts +13 -0
- package/dist/note-utils.d.ts.map +1 -0
- package/dist/note-utils.js +62 -0
- package/dist/note-utils.js.map +1 -0
- package/dist/noteUtils.d.ts +25 -0
- package/dist/noteUtils.d.ts.map +1 -0
- package/dist/noteUtils.js +86 -0
- package/dist/noteUtils.js.map +1 -0
- package/dist/parser.d.ts +137 -2
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +259 -36
- package/dist/parser.js.map +1 -1
- package/dist/song.d.ts +207 -2
- package/dist/song.d.ts.map +1 -1
- package/dist/song.js +251 -15
- package/dist/song.js.map +1 -1
- package/package.json +8 -3
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
|
-
|
|
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
|
-
|
|
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
|
package/dist/song.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"song.d.ts","sourceRoot":"","sources":["../src/song.ts"],"names":[],"mappings":"
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
68
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|