@scorelabs/core 1.0.5 → 1.0.7
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 +1 -1
- package/dist/importers/MusicXMLParser.d.ts +2 -1
- package/dist/importers/MusicXMLParser.js +28 -13
- package/dist/models/Measure.d.ts +1 -1
- package/dist/models/Measure.js +1 -1
- package/dist/models/Note.d.ts +1 -1
- package/dist/models/Note.js +1 -1
- package/dist/models/Part.d.ts +2 -2
- package/dist/models/Part.js +1 -1
- package/dist/models/Score.d.ts +3 -3
- package/dist/models/Score.js +6 -3
- package/dist/models/Staff.d.ts +2 -2
- package/dist/models/types.d.ts +1 -1
- package/package.json +5 -4
- package/dist/utils/tier.d.ts +0 -36
- package/dist/utils/tier.js +0 -112
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ console.log(`Number of Parts: ${score.parts.length}`);
|
|
|
36
36
|
The core model uses `NoteSet` to represent a collection of notes at a specific point data (e.g., a chord or a single note).
|
|
37
37
|
|
|
38
38
|
```typescript
|
|
39
|
-
import { Note, NoteSet, Pitch
|
|
39
|
+
import { Duration, Note, NoteSet, Pitch } from '@scorelabs/core';
|
|
40
40
|
|
|
41
41
|
// Create a C4 quarter note
|
|
42
42
|
const c4 = new Note(new Pitch('C', 4), Duration.Quarter);
|
|
@@ -12,8 +12,9 @@ export declare class MusicXMLParser {
|
|
|
12
12
|
private currentSymbol;
|
|
13
13
|
private instrumentPitchMap;
|
|
14
14
|
private _domParser;
|
|
15
|
-
constructor(domParserInstance?: any);
|
|
15
|
+
constructor(domParserInstance?: DOMParser | any);
|
|
16
16
|
private parseFromString;
|
|
17
|
+
static getXMLFromBinary(data: ArrayBuffer, domParser?: DOMParser | any): Promise<string>;
|
|
17
18
|
parseBinary(data: ArrayBuffer): Promise<ScoreJSON>;
|
|
18
19
|
parse(xmlString: string): ScoreJSON;
|
|
19
20
|
private parseSubtitle;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import JSZip from 'jszip';
|
|
2
2
|
import { getInstrumentByProgram } from '../models/Instrument';
|
|
3
|
-
import {
|
|
3
|
+
import { Accidental, Arpeggio, Articulation, BarlineStyle, Bowing, Clef, Duration, GlissandoType, HairpinType, NoteheadShape, Ornament, OttavaType, StemDirection, } from '../models/types';
|
|
4
4
|
/**
|
|
5
5
|
* MusicXML Parser
|
|
6
6
|
*
|
|
@@ -24,14 +24,17 @@ export class MusicXMLParser {
|
|
|
24
24
|
}
|
|
25
25
|
return this._domParser.parseFromString(xmlString, 'application/xml');
|
|
26
26
|
}
|
|
27
|
-
async
|
|
27
|
+
static async getXMLFromBinary(data, domParser) {
|
|
28
28
|
const uint8 = new Uint8Array(data);
|
|
29
29
|
if (uint8[0] === 0x50 && uint8[1] === 0x4b) {
|
|
30
30
|
const zip = await JSZip.loadAsync(data);
|
|
31
31
|
const containerXml = await zip.file('META-INF/container.xml')?.async('text');
|
|
32
32
|
if (!containerXml)
|
|
33
33
|
throw new Error('Invalid MXL: Missing META-INF/container.xml');
|
|
34
|
-
const
|
|
34
|
+
const _parser = domParser || (typeof DOMParser !== 'undefined' ? new DOMParser() : null);
|
|
35
|
+
if (!_parser)
|
|
36
|
+
throw new Error('No DOMParser available');
|
|
37
|
+
const containerDoc = _parser.parseFromString(containerXml, 'application/xml');
|
|
35
38
|
const rootfile = containerDoc.querySelector('rootfile');
|
|
36
39
|
const fullPath = rootfile?.getAttribute('full-path');
|
|
37
40
|
if (!fullPath)
|
|
@@ -39,13 +42,17 @@ export class MusicXMLParser {
|
|
|
39
42
|
const scoreXml = await zip.file(fullPath)?.async('text');
|
|
40
43
|
if (!scoreXml)
|
|
41
44
|
throw new Error(`Invalid MXL: Could not find file ${fullPath}`);
|
|
42
|
-
return
|
|
45
|
+
return scoreXml;
|
|
43
46
|
}
|
|
44
47
|
else {
|
|
45
48
|
const decoder = new TextDecoder();
|
|
46
|
-
return
|
|
49
|
+
return decoder.decode(data);
|
|
47
50
|
}
|
|
48
51
|
}
|
|
52
|
+
async parseBinary(data) {
|
|
53
|
+
const scoreXml = await MusicXMLParser.getXMLFromBinary(data, this._domParser);
|
|
54
|
+
return this.parse(scoreXml);
|
|
55
|
+
}
|
|
49
56
|
parse(xmlString) {
|
|
50
57
|
this.instrumentPitchMap.clear();
|
|
51
58
|
// Strip potential BOM and leading garbage
|
|
@@ -788,22 +795,30 @@ export class MusicXMLParser {
|
|
|
788
795
|
else if (barStyle === 'dotted')
|
|
789
796
|
barlineStyle = BarlineStyle.Dotted;
|
|
790
797
|
}
|
|
791
|
-
|
|
798
|
+
const repeatEl = barline.querySelector('repeat');
|
|
799
|
+
if (repeatEl) {
|
|
800
|
+
const direction = repeatEl.getAttribute('direction');
|
|
801
|
+
const times = repeatEl.getAttribute('times');
|
|
792
802
|
repeats.push({
|
|
793
|
-
type:
|
|
794
|
-
|
|
795
|
-
: 'end',
|
|
803
|
+
type: direction === 'forward' ? 'start' : 'end',
|
|
804
|
+
times: times ? parseInt(times) : undefined,
|
|
796
805
|
});
|
|
806
|
+
}
|
|
797
807
|
const ending = barline.querySelector('ending');
|
|
798
808
|
if (ending) {
|
|
799
809
|
const type = ending.getAttribute('type');
|
|
800
|
-
const
|
|
810
|
+
const numberAttr = ending.getAttribute('number') || '1';
|
|
811
|
+
// Handle "1,2" or "1 2"
|
|
812
|
+
const numbers = numberAttr
|
|
813
|
+
.split(/[,\s]+/)
|
|
814
|
+
.map((n) => parseInt(n))
|
|
815
|
+
.filter((n) => !isNaN(n));
|
|
801
816
|
if (type === 'start')
|
|
802
|
-
volta = { type: 'start',
|
|
817
|
+
volta = { type: 'start', numbers };
|
|
803
818
|
else if (type === 'stop')
|
|
804
|
-
volta = { type: 'stop',
|
|
819
|
+
volta = { type: 'stop', numbers };
|
|
805
820
|
else if (type === 'discontinue')
|
|
806
|
-
volta = { type: 'both',
|
|
821
|
+
volta = { type: 'both', numbers };
|
|
807
822
|
}
|
|
808
823
|
});
|
|
809
824
|
return { repeats, volta, barlineStyle };
|
package/dist/models/Measure.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Note, NoteJSON } from './Note';
|
|
2
2
|
import { NoteSet, NoteSetJSON } from './NoteSet';
|
|
3
|
-
import {
|
|
3
|
+
import { BarlineStyle, Clef, Duration, KeySignature, Repeat, Tempo, TimeSignature, Volta } from './types';
|
|
4
4
|
/**
|
|
5
5
|
* Represents a single measure containing note sets, potentially in multiple voices.
|
|
6
6
|
*/
|
package/dist/models/Measure.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Note } from './Note';
|
|
2
2
|
import { NoteSet } from './NoteSet';
|
|
3
|
-
import { DURATION_VALUES, decomposeDuration,
|
|
3
|
+
import { BarlineStyle, DURATION_VALUES, decomposeDuration, } from './types';
|
|
4
4
|
/**
|
|
5
5
|
* Represents a single measure containing note sets, potentially in multiple voices.
|
|
6
6
|
*/
|
package/dist/models/Note.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Duration, Accidental, Articulation, Dynamic, Slur, Tuplet, Hairpin, Glissando, Arpeggio, Ottava, Pedal, Ornament, FretboardDiagram, NoteheadShape, Bowing, Lyric, StemDirection } from './types';
|
|
2
1
|
import { Pitch } from './Pitch';
|
|
2
|
+
import { Accidental, Arpeggio, Articulation, Bowing, Duration, Dynamic, FretboardDiagram, Glissando, Hairpin, Lyric, NoteheadShape, Ornament, Ottava, Pedal, Slur, StemDirection, Tuplet } from './types';
|
|
3
3
|
/**
|
|
4
4
|
* Represents a single note or rest in a measure.
|
|
5
5
|
*/
|
package/dist/models/Note.js
CHANGED
package/dist/models/Part.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Staff, StaffJSON } from './Staff';
|
|
2
1
|
import { Instrument } from './Instrument';
|
|
3
|
-
import { Note } from './Note';
|
|
4
2
|
import { Measure } from './Measure';
|
|
3
|
+
import { Note } from './Note';
|
|
4
|
+
import { Staff, StaffJSON } from './Staff';
|
|
5
5
|
import { Duration } from './types';
|
|
6
6
|
/**
|
|
7
7
|
* Represents a musical part (instrument/voice) which can have multiple staves.
|
package/dist/models/Part.js
CHANGED
package/dist/models/Score.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Measure } from './Measure';
|
|
2
|
+
import { NoteSet } from './NoteSet';
|
|
2
3
|
import { Part, PartJSON } from './Part';
|
|
3
4
|
import { Staff } from './Staff';
|
|
4
|
-
import {
|
|
5
|
-
import { Measure } from './Measure';
|
|
5
|
+
import { Duration, Genre, KeySignature, TimeSignature } from './types';
|
|
6
6
|
/**
|
|
7
7
|
* Represents a complete musical score.
|
|
8
8
|
*/
|
package/dist/models/Score.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Duration, decomposeDuration, Genre } from './types';
|
|
2
1
|
import { Part } from './Part';
|
|
2
|
+
import { Duration, Genre, decomposeDuration } from './types';
|
|
3
3
|
/**
|
|
4
4
|
* Represents a complete musical score.
|
|
5
5
|
*/
|
|
@@ -275,7 +275,10 @@ export class Score {
|
|
|
275
275
|
let i = 0;
|
|
276
276
|
let blockStart = 0;
|
|
277
277
|
const repeatCount = new Map();
|
|
278
|
-
|
|
278
|
+
let safetyCounter = 0;
|
|
279
|
+
const MAX_SEQUENCE = 10000;
|
|
280
|
+
while (i < measures.length && safetyCounter < MAX_SEQUENCE) {
|
|
281
|
+
safetyCounter++;
|
|
279
282
|
const m = measures[i];
|
|
280
283
|
if (m.repeats.some((r) => r.type === 'start'))
|
|
281
284
|
blockStart = i;
|
|
@@ -294,7 +297,7 @@ export class Score {
|
|
|
294
297
|
if (nextEndIdx !== -1)
|
|
295
298
|
iteration = (repeatCount.get(nextEndIdx) || 0) + 1;
|
|
296
299
|
}
|
|
297
|
-
if (m.volta && m.volta.
|
|
300
|
+
if (m.volta && !m.volta.numbers.includes(iteration)) {
|
|
298
301
|
i++;
|
|
299
302
|
continue;
|
|
300
303
|
}
|
package/dist/models/Staff.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Clef, Duration } from './types';
|
|
2
1
|
import { Measure, MeasureJSON } from './Measure';
|
|
3
|
-
import { Pitch } from './Pitch';
|
|
4
2
|
import { Note } from './Note';
|
|
3
|
+
import { Pitch } from './Pitch';
|
|
4
|
+
import { Clef, Duration } from './types';
|
|
5
5
|
/**
|
|
6
6
|
* Represents a single staff (one set of 5 lines with a clef).
|
|
7
7
|
*/
|
package/dist/models/types.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scorelabs/core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "Core logic and models for ScoreLabs music notation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
"types": "./dist/index.d.ts",
|
|
12
12
|
"exports": {
|
|
13
13
|
".": {
|
|
14
|
-
"
|
|
15
|
-
"
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js"
|
|
16
16
|
}
|
|
17
17
|
},
|
|
18
18
|
"scripts": {
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
"release:minor": "npm version minor && npm publish --access public",
|
|
24
24
|
"release:major": "npm version major && npm publish --access public",
|
|
25
25
|
"release": "npm run release:patch",
|
|
26
|
-
"test": "vitest"
|
|
26
|
+
"test": "vitest",
|
|
27
|
+
"lint": "eslint ."
|
|
27
28
|
},
|
|
28
29
|
"dependencies": {
|
|
29
30
|
"jszip": "^3.10.1"
|
package/dist/utils/tier.d.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
export declare const TOKEN_MODULUS = 17;
|
|
2
|
-
export declare const EXPECTED_REMAINDER = 2;
|
|
3
|
-
export declare const EXPIRATION_MS: number;
|
|
4
|
-
export type UserTier = 'free' | 'premium';
|
|
5
|
-
/**
|
|
6
|
-
* Obfuscates the user's tier into a numeric token for the viewer.
|
|
7
|
-
* The viewer expects token % 17 === 2 for premium access.
|
|
8
|
-
*/
|
|
9
|
-
export declare function generateObfuscatedToken(tier: UserTier): number;
|
|
10
|
-
/**
|
|
11
|
-
* Packs token and timestamp into an obfuscated string.
|
|
12
|
-
*/
|
|
13
|
-
export declare function packToken(token: number, timestamp: number, salt: string): string;
|
|
14
|
-
/**
|
|
15
|
-
* Unpacks the token string and validates the signature.
|
|
16
|
-
*/
|
|
17
|
-
export declare function unpackToken(packed: string, salt: string): {
|
|
18
|
-
token: number;
|
|
19
|
-
timestamp: number;
|
|
20
|
-
} | null;
|
|
21
|
-
/**
|
|
22
|
-
* Returns the effective numeric token, checking for expiration.
|
|
23
|
-
*/
|
|
24
|
-
export declare function getEffectiveToken(user: any): number;
|
|
25
|
-
/**
|
|
26
|
-
* Returns the effective tier from the obfuscated token
|
|
27
|
-
*/
|
|
28
|
-
export declare function getTierFromUser(user: any): UserTier;
|
|
29
|
-
/**
|
|
30
|
-
* Returns true only if the user has a valid, non-expired premium token.
|
|
31
|
-
*/
|
|
32
|
-
export declare function isUserPremium(user: any): boolean;
|
|
33
|
-
/**
|
|
34
|
-
* Returns true only if the user has a valid, non-expired free token.
|
|
35
|
-
*/
|
|
36
|
-
export declare function isUserFree(user: any): boolean;
|
package/dist/utils/tier.js
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
export const TOKEN_MODULUS = 17;
|
|
2
|
-
export const EXPECTED_REMAINDER = 2;
|
|
3
|
-
export const EXPIRATION_MS = 24 * 60 * 60 * 1000;
|
|
4
|
-
/**
|
|
5
|
-
* Obfuscates the user's tier into a numeric token for the viewer.
|
|
6
|
-
* The viewer expects token % 17 === 2 for premium access.
|
|
7
|
-
*/
|
|
8
|
-
export function generateObfuscatedToken(tier) {
|
|
9
|
-
const isPremium = tier === 'premium';
|
|
10
|
-
let base = Math.floor(Math.random() * 8999) + 1000;
|
|
11
|
-
if (isPremium) {
|
|
12
|
-
// Ensure token % 17 === 2
|
|
13
|
-
return base - (base % TOKEN_MODULUS) + EXPECTED_REMAINDER;
|
|
14
|
-
}
|
|
15
|
-
else {
|
|
16
|
-
// Ensure token % 17 !== 2
|
|
17
|
-
const result = base;
|
|
18
|
-
return result % TOKEN_MODULUS === EXPECTED_REMAINDER ? result + 1 : result;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Packs token and timestamp into an obfuscated string.
|
|
23
|
-
*/
|
|
24
|
-
export function packToken(token, timestamp, salt) {
|
|
25
|
-
// Use a more robust numeric salt
|
|
26
|
-
const saltNum = salt
|
|
27
|
-
.split('')
|
|
28
|
-
.reduce((acc, char, i) => acc + char.charCodeAt(0) * (i + 1), 0);
|
|
29
|
-
// Pack components
|
|
30
|
-
const tHex = token.toString(16);
|
|
31
|
-
// Obfuscate time using XOR with salt and token
|
|
32
|
-
const timeKey = (saltNum ^ token) % 1000000;
|
|
33
|
-
const oTime = (BigInt(timestamp) ^ BigInt(timeKey)).toString(16);
|
|
34
|
-
// Create a signature that binds token, time and salt
|
|
35
|
-
const sigPayload = `${token}:${timestamp}:${salt}`;
|
|
36
|
-
let sig = 0;
|
|
37
|
-
for (let i = 0; i < sigPayload.length; i++) {
|
|
38
|
-
sig = (sig << 5) - sig + sigPayload.charCodeAt(i);
|
|
39
|
-
sig |= 0; // Convert to 32bit integer
|
|
40
|
-
}
|
|
41
|
-
const sHex = Math.abs(sig).toString(16);
|
|
42
|
-
return `${tHex}-${oTime}-${sHex}`;
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Unpacks the token string and validates the signature.
|
|
46
|
-
*/
|
|
47
|
-
export function unpackToken(packed, salt) {
|
|
48
|
-
try {
|
|
49
|
-
const [tHex, oTime, sHex] = packed.split('-');
|
|
50
|
-
if (!tHex || !oTime || !sHex)
|
|
51
|
-
return null;
|
|
52
|
-
const token = parseInt(tHex, 16);
|
|
53
|
-
const saltNum = salt
|
|
54
|
-
.split('')
|
|
55
|
-
.reduce((acc, char, i) => acc + char.charCodeAt(0) * (i + 1), 0);
|
|
56
|
-
const timeKey = (saltNum ^ token) % 1000000;
|
|
57
|
-
const timestamp = Number(BigInt(parseInt(oTime, 16)) ^ BigInt(timeKey));
|
|
58
|
-
// Validate signature
|
|
59
|
-
const sigPayload = `${token}:${timestamp}:${salt}`;
|
|
60
|
-
let sig = 0;
|
|
61
|
-
for (let i = 0; i < sigPayload.length; i++) {
|
|
62
|
-
sig = (sig << 5) - sig + sigPayload.charCodeAt(i);
|
|
63
|
-
sig |= 0;
|
|
64
|
-
}
|
|
65
|
-
const expectedSig = Math.abs(sig).toString(16);
|
|
66
|
-
if (sHex !== expectedSig)
|
|
67
|
-
return null;
|
|
68
|
-
return { token, timestamp };
|
|
69
|
-
}
|
|
70
|
-
catch (e) {
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Returns the effective numeric token, checking for expiration.
|
|
76
|
-
*/
|
|
77
|
-
export function getEffectiveToken(user) {
|
|
78
|
-
if (!user)
|
|
79
|
-
return 0;
|
|
80
|
-
const accessToken = user.accessToken || user.access_token;
|
|
81
|
-
if (!accessToken) {
|
|
82
|
-
return 0;
|
|
83
|
-
}
|
|
84
|
-
const salt = (user.uuid || user.id || 'default-salt').toString();
|
|
85
|
-
const unpacked = unpackToken(accessToken, salt);
|
|
86
|
-
if (!unpacked)
|
|
87
|
-
return 0;
|
|
88
|
-
const now = Date.now();
|
|
89
|
-
if (now - unpacked.timestamp >= EXPIRATION_MS || now < unpacked.timestamp) {
|
|
90
|
-
return 0; // Expired
|
|
91
|
-
}
|
|
92
|
-
return unpacked.token;
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* Returns the effective tier from the obfuscated token
|
|
96
|
-
*/
|
|
97
|
-
export function getTierFromUser(user) {
|
|
98
|
-
const token = getEffectiveToken(user);
|
|
99
|
-
return token % TOKEN_MODULUS === EXPECTED_REMAINDER ? 'premium' : 'free';
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Returns true only if the user has a valid, non-expired premium token.
|
|
103
|
-
*/
|
|
104
|
-
export function isUserPremium(user) {
|
|
105
|
-
return getTierFromUser(user) === 'premium';
|
|
106
|
-
}
|
|
107
|
-
/**
|
|
108
|
-
* Returns true only if the user has a valid, non-expired free token.
|
|
109
|
-
*/
|
|
110
|
-
export function isUserFree(user) {
|
|
111
|
-
return getTierFromUser(user) === 'free';
|
|
112
|
-
}
|