Skip to content

Commit df0eff6

Browse files
committed
feat(parser): 重构 ABC 解析器以优化解析逻辑和增强可维护性
- 移除冗余的解析函数,简化 AbcParser 的结构,提升代码的清晰度。 - 引入专门的解析器(AbcHeaderParser, AbcMeasureParser, AbcLyricParser)以分离不同解析逻辑,增强模块化。 - 更新 parseRoot 和 parseScore 方法,确保文件头和乐谱内容的解析更加准确和灵活。 - 优化声部和小节的处理逻辑,确保解析结果的准确性和一致性。 该变更提升了 ABC 解析器的可维护性和扩展性,确保乐谱解析的准确性和灵活性。
1 parent 1dc7170 commit df0eff6

13 files changed

Lines changed: 2016 additions & 1648 deletions

File tree

packages/simple-notation/src/data/parser/abc-parser.ts

Lines changed: 107 additions & 1648 deletions
Large diffs are not rendered by default.
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/**
2+
* ABC 解析错误定义
3+
*
4+
* 提供统一的错误类型体系,便于错误追踪和处理
5+
*/
6+
7+
/**
8+
* ABC 解析错误基类
9+
*
10+
* @example
11+
* ```typescript
12+
* throw new AbcParseError('解析失败', 'PARSE_ERROR', { line: 1, column: 5 });
13+
* ```
14+
*/
15+
export class AbcParseError extends Error {
16+
constructor(
17+
message: string,
18+
public readonly code: string,
19+
public readonly position?: { line?: number; column?: number },
20+
public readonly context?: string,
21+
) {
22+
super(message);
23+
this.name = 'AbcParseError';
24+
}
25+
}
26+
27+
/**
28+
* 元素解析错误
29+
*
30+
* 当无法解析音符、休止符等元素时抛出
31+
*/
32+
export class ElementParseError extends AbcParseError {
33+
/** 原始错误(如果有) */
34+
public readonly cause?: Error;
35+
36+
constructor(
37+
elementData: string,
38+
position?: { line?: number; column?: number },
39+
cause?: Error,
40+
) {
41+
super(
42+
`无法解析元素: ${elementData}`,
43+
'ELEMENT_PARSE_ERROR',
44+
position,
45+
elementData,
46+
);
47+
this.cause = cause;
48+
}
49+
}
50+
51+
/**
52+
* 头部解析错误
53+
*
54+
* 当无法解析头部字段时抛出
55+
*/
56+
export class HeaderParseError extends AbcParseError {
57+
constructor(fieldName: string, fieldValue: string, reason: string) {
58+
super(
59+
`无法解析头部字段 ${fieldName}: ${reason}`,
60+
'HEADER_PARSE_ERROR',
61+
undefined,
62+
`${fieldName}: ${fieldValue}`,
63+
);
64+
}
65+
}
66+
67+
/**
68+
* 小节解析错误
69+
*
70+
* 当小节时值不匹配等问题时抛出
71+
*/
72+
export class MeasureParseError extends AbcParseError {
73+
constructor(measureIndex: number, reason: string, context?: string) {
74+
super(
75+
`小节 ${measureIndex} 解析错误: ${reason}`,
76+
'MEASURE_PARSE_ERROR',
77+
undefined,
78+
context,
79+
);
80+
}
81+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* ABC 解析器模块
3+
*
4+
* 提供完整的 ABC 格式解析能力
5+
*/
6+
7+
export * from './errors';
8+
export * from './utils';
9+
export * from './parsers';
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
import { SNTimeUnit } from '@core/model/ticks';
2+
import { SNAccidental } from '@core/model/music';
3+
import { SNParserElement } from '@data/model';
4+
import {
5+
SNParserNote,
6+
SNParserRest,
7+
SNParserTie,
8+
SNParserTuplet,
9+
SNParserNode,
10+
} from '@data/node';
11+
import { noteValueToDuration } from '@core/utils/time-unit';
12+
import { AbcTokenizer } from '../utils';
13+
import { ElementParseError } from '../errors';
14+
15+
/**
16+
* ABC 元素解析器
17+
*
18+
* 职责:解析小节内的音乐元素
19+
* - 音符(Note)
20+
* - 休止符(Rest)
21+
* - 连音(Tuplet)
22+
* - 连音线(Tie)
23+
*/
24+
export class AbcElementParser {
25+
/**
26+
* 获取下一个ID的回调函数
27+
*/
28+
private getNextId: (prefix: string) => string;
29+
30+
constructor(getNextId: (prefix: string) => string) {
31+
this.getNextId = getNextId;
32+
}
33+
34+
/**
35+
* 解析单个元素
36+
*
37+
* @param elementData - 元素数据字符串
38+
* @param timeUnit - 时间单位(可选)
39+
* @param defaultNoteLength - 默认音符长度(可选)
40+
* @returns 解析后的元素节点
41+
* @throws {ElementParseError} 当无法解析元素时抛出
42+
*
43+
* @example
44+
* ```typescript
45+
* // 解析音符
46+
* parser.parseElement('C'); // SNParserNote
47+
* parser.parseElement('C#'); // SNParserNote (升C)
48+
* parser.parseElement('C4'); // SNParserNote (四分音符C)
49+
*
50+
* // 解析休止符
51+
* parser.parseElement('z'); // SNParserRest
52+
* parser.parseElement('z4'); // SNParserRest (四分休止符)
53+
*
54+
* // 解析连音线
55+
* parser.parseElement('-'); // SNParserTie
56+
*
57+
* // 解析连音
58+
* parser.parseElement('(3ABC'); // SNParserTuplet (三连音)
59+
* ```
60+
*/
61+
parseElement(
62+
elementData: string,
63+
timeUnit?: SNTimeUnit,
64+
defaultNoteLength?: number,
65+
): SNParserElement {
66+
const trimmed = elementData.trim();
67+
68+
if (!trimmed) {
69+
throw new ElementParseError(elementData);
70+
}
71+
72+
// 1. 解析连音线
73+
if (trimmed === '-') {
74+
return new SNParserTie({
75+
id: this.getNextId('tie'),
76+
style: 'slur',
77+
originStr: elementData,
78+
});
79+
}
80+
81+
// 2. 解析连音(Tuplet)
82+
const tupletMatch = trimmed.match(/^\((\d+)([\s\S]*?)\)?$/);
83+
if (tupletMatch) {
84+
return this.parseTuplet(
85+
tupletMatch,
86+
elementData,
87+
timeUnit,
88+
defaultNoteLength,
89+
);
90+
}
91+
92+
// 3. 解析休止符
93+
if (trimmed.startsWith('z')) {
94+
return this.parseRest(trimmed, elementData, timeUnit, defaultNoteLength);
95+
}
96+
97+
// 4. 解析音符
98+
const noteMatch = trimmed.match(
99+
/^(\^+\/?|_+\/?|=?)([A-Ga-g])([,']*)(\d*)(\.*)$/,
100+
);
101+
if (noteMatch) {
102+
return this.parseNote(noteMatch, trimmed, timeUnit, defaultNoteLength);
103+
}
104+
105+
throw new ElementParseError(elementData);
106+
}
107+
108+
/**
109+
* 解析连音(Tuplet)
110+
*/
111+
private parseTuplet(
112+
match: RegExpMatchArray,
113+
elementData: string,
114+
timeUnit?: SNTimeUnit,
115+
defaultNoteLength?: number,
116+
): SNParserTuplet {
117+
const [, , innerNotesStr] = match;
118+
const innerNotes = AbcTokenizer.tokenize(innerNotesStr);
119+
120+
return new SNParserTuplet({
121+
id: this.getNextId('tuplet'),
122+
originStr: elementData,
123+
}).addChildren(
124+
innerNotes.map(
125+
(noteStr): SNParserNote =>
126+
this.parseElement(
127+
noteStr,
128+
timeUnit,
129+
defaultNoteLength,
130+
) as SNParserNote,
131+
),
132+
);
133+
}
134+
135+
/**
136+
* 解析休止符
137+
*/
138+
private parseRest(
139+
trimmed: string,
140+
elementData: string,
141+
timeUnit?: SNTimeUnit,
142+
defaultNoteLength?: number,
143+
): SNParserRest {
144+
let duration: number;
145+
146+
if (timeUnit) {
147+
const restStr = trimmed.slice(1);
148+
const durationStr = restStr.match(/^(\d+)/)?.[1];
149+
const dotCount = (restStr.match(/\./g) || []).length;
150+
151+
const noteValue = durationStr
152+
? 1 / parseInt(durationStr, 10)
153+
: defaultNoteLength || 1 / 4;
154+
155+
const dottedNoteValue = this.calculateDottedNoteValue(
156+
noteValue,
157+
dotCount,
158+
);
159+
duration = noteValueToDuration(dottedNoteValue, timeUnit);
160+
} else {
161+
duration = parseInt(trimmed.slice(1), 10) || 1;
162+
}
163+
164+
return new SNParserRest({
165+
id: this.getNextId('rest'),
166+
duration,
167+
originStr: elementData,
168+
});
169+
}
170+
171+
/**
172+
* 解析音符
173+
*/
174+
private parseNote(
175+
match: RegExpMatchArray,
176+
trimmed: string,
177+
timeUnit?: SNTimeUnit,
178+
defaultNoteLength?: number,
179+
): SNParserNote {
180+
const [, accidentalStr, letter, octaveSymbols, durationStr] = match;
181+
182+
// 解析变音记号
183+
const accidental = this.parseAccidental(accidentalStr);
184+
185+
// 解析八度
186+
const baseOctave: number = letter === letter.toUpperCase() ? 3 : 4;
187+
const octaveOffset = octaveSymbols.split('').reduce((offset, sym) => {
188+
return sym === ',' ? offset - 1 : sym === "'" ? offset + 1 : offset;
189+
}, 0);
190+
const octave = baseOctave + octaveOffset;
191+
192+
// 解析时值
193+
let duration: number;
194+
if (timeUnit) {
195+
const noteValue = durationStr
196+
? 1 / parseInt(durationStr, 10)
197+
: defaultNoteLength || 1 / 4;
198+
199+
const dotCount = (trimmed.match(/\./g) || []).length;
200+
const dottedNoteValue = this.calculateDottedNoteValue(
201+
noteValue,
202+
dotCount,
203+
);
204+
duration = noteValueToDuration(dottedNoteValue, timeUnit);
205+
} else {
206+
duration = durationStr ? parseInt(durationStr, 10) : 1;
207+
}
208+
209+
return new SNParserNote({
210+
id: this.getNextId('note'),
211+
originStr: trimmed,
212+
pitch: {
213+
letter: letter.toUpperCase(),
214+
octave,
215+
accidental,
216+
},
217+
duration,
218+
});
219+
}
220+
221+
/**
222+
* 解析变音记号
223+
*
224+
* @param accidentalStr - 变音记号字符串
225+
* @returns 变音记号枚举值
226+
*/
227+
private parseAccidental(accidentalStr: string): SNAccidental {
228+
if (!accidentalStr) return SNAccidental.NATURAL;
229+
230+
switch (accidentalStr) {
231+
case '^':
232+
return SNAccidental.SHARP;
233+
case '^^':
234+
return SNAccidental.DOUBLE_SHARP;
235+
case '_':
236+
return SNAccidental.FLAT;
237+
case '__':
238+
return SNAccidental.DOUBLE_FLAT;
239+
case '=':
240+
return SNAccidental.NATURAL;
241+
default:
242+
return SNAccidental.NATURAL;
243+
}
244+
}
245+
246+
/**
247+
* 计算带附点的音符时值
248+
*
249+
* @param noteValue - 基础音符时值
250+
* @param dotCount - 附点数量
251+
* @returns 带附点的音符时值
252+
*/
253+
private calculateDottedNoteValue(
254+
noteValue: number,
255+
dotCount: number,
256+
): number {
257+
if (dotCount === 0) return noteValue;
258+
return noteValue * (1 + 0.5 * (1 - Math.pow(0.5, dotCount)));
259+
}
260+
261+
/**
262+
* 获取默认音符长度
263+
*
264+
* 从父节点的 meta 中查找 noteLength 字段
265+
*
266+
* @param node - 当前节点(可选)
267+
* @returns 默认音符长度
268+
*/
269+
static getDefaultNoteLength(node?: SNParserNode): number {
270+
if (node) {
271+
let current: SNParserNode | undefined = node.parent;
272+
while (current) {
273+
const meta = current.meta as { noteLength?: string } | undefined;
274+
if (meta?.noteLength) {
275+
const match = meta.noteLength.match(/^(\d+)\/(\d+)$/);
276+
if (match) {
277+
const [, num, den] = match.map(Number);
278+
return num / den;
279+
}
280+
}
281+
current = current.parent;
282+
}
283+
}
284+
return 1 / 4; // 默认四分音符
285+
}
286+
}

0 commit comments

Comments
 (0)