Skip to content

Commit d9353ba

Browse files
committed
feat(parser): 增强音符解析以支持升降号和装饰符的处理
- 更新 SNPitch 接口,允许 accidental 属性为可选,支持未标记的音符。 - 修改音符解析逻辑,合并升降号与音符,确保解析时的准确性。 - 增强装饰符提取逻辑,确保升降号不被误识别为装饰符。 - 在渲染过程中添加升降号的绘制逻辑,确保视觉表现的准确性。 该变更提升了音符解析的灵活性和准确性,确保乐谱信息的完整性和可视化效果。
1 parent 147a20f commit d9353ba

File tree

7 files changed

+428
-17
lines changed

7 files changed

+428
-17
lines changed

packages/simple-notation/src/core/model/music.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ export interface SNPitch {
3232
letter: string;
3333
/** 八度(例如:4 表示中央 C) */
3434
octave: number;
35-
/** 变音记号 */
36-
accidental: SNAccidental;
35+
/** 变音记号(undefined 表示没有升降号标记,NATURAL 表示明确写了还原号 =) */
36+
accidental?: SNAccidental;
3737
}
3838

3939
/**
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import type { SNKeySignature } from '@core/model/music';
2+
import { SNAccidental } from '@core/model/music';
3+
4+
/**
5+
* 调号到音符升降号的映射工具
6+
*
7+
* 根据调号计算每个音名字母应该的升降号
8+
*/
9+
10+
/**
11+
* 大调调号的升降号序列
12+
* 升号调:F C G D A E B(顺序)
13+
* 降号调:B E A D G C F(顺序,与升号相反)
14+
*/
15+
const SHARP_ORDER = ['F', 'C', 'G', 'D', 'A', 'E', 'B'];
16+
const FLAT_ORDER = ['B', 'E', 'A', 'D', 'G', 'C', 'F'];
17+
18+
/**
19+
* 根据调号主音计算调号类型和升降号数量
20+
*
21+
* @param keySignature - 调号对象
22+
* @returns 调号类型和升降号数量
23+
*/
24+
function calculateKeySignatureInfo(keySignature: SNKeySignature): {
25+
type: 'sharp' | 'flat' | 'natural';
26+
count: number;
27+
} {
28+
const { letter, symbol } = keySignature;
29+
30+
// 如果 symbol 明确指定了 sharp 或 flat,直接使用
31+
if (symbol === 'sharp') {
32+
// 升号调:根据 letter 计算升号数量
33+
const sharpKeys: Record<string, number> = {
34+
G: 1,
35+
D: 2,
36+
A: 3,
37+
E: 4,
38+
B: 5,
39+
'F#': 6,
40+
'C#': 7,
41+
};
42+
const count = sharpKeys[letter] || 0;
43+
return { type: 'sharp', count };
44+
}
45+
46+
if (symbol === 'flat') {
47+
// 降号调:根据 letter 计算降号数量
48+
const flatKeys: Record<string, number> = {
49+
F: 1,
50+
Bb: 2,
51+
Eb: 3,
52+
Ab: 4,
53+
Db: 5,
54+
Gb: 6,
55+
Cb: 7,
56+
};
57+
const count = flatKeys[letter] || 0;
58+
return { type: 'flat', count };
59+
}
60+
61+
// symbol === 'natural' 的情况,需要根据 letter 判断调号类型
62+
// C大调或A小调(无升降号)
63+
if (letter === 'C' || letter === 'A') {
64+
return { type: 'natural', count: 0 };
65+
}
66+
67+
// 根据 letter 判断是大调还是小调,然后确定调号类型
68+
// 大调升号调:G, D, A, E, B, F#, C#
69+
// 大调降号调:F, Bb, Eb, Ab, Db, Gb, Cb
70+
// 小调升号调:E, B, F#, C#, G#, D#, A#(关系小调)
71+
// 小调降号调:D, G, C, F, Bb, Eb, Ab(关系小调)
72+
73+
const majorSharpKeys: Record<string, number> = {
74+
G: 1,
75+
D: 2,
76+
A: 3,
77+
E: 4,
78+
B: 5,
79+
'F#': 6,
80+
'C#': 7,
81+
};
82+
83+
const majorFlatKeys: Record<string, number> = {
84+
F: 1,
85+
Bb: 2,
86+
Eb: 3,
87+
Ab: 4,
88+
Db: 5,
89+
Gb: 6,
90+
Cb: 7,
91+
};
92+
93+
// 优先检查大调
94+
if (majorSharpKeys[letter]) {
95+
return { type: 'sharp', count: majorSharpKeys[letter] };
96+
}
97+
if (majorFlatKeys[letter]) {
98+
return { type: 'flat', count: majorFlatKeys[letter] };
99+
}
100+
101+
// 小调(关系小调)
102+
const minorSharpKeys: Record<string, number> = {
103+
E: 1, // E小调(G大调的关系小调)
104+
B: 2, // B小调(D大调的关系小调)
105+
'F#': 3, // F#小调(A大调的关系小调)
106+
'C#': 4, // C#小调(E大调的关系小调)
107+
'G#': 5, // G#小调(B大调的关系小调)
108+
'D#': 6, // D#小调(F#大调的关系小调)
109+
'A#': 7, // A#小调(C#大调的关系小调)
110+
};
111+
112+
const minorFlatKeys: Record<string, number> = {
113+
D: 1, // D小调(F大调的关系小调)
114+
G: 2, // G小调(Bb大调的关系小调)
115+
C: 3, // C小调(Eb大调的关系小调)
116+
F: 4, // F小调(Ab大调的关系小调)
117+
Bb: 5, // Bb小调(Db大调的关系小调)
118+
Eb: 6, // Eb小调(Gb大调的关系小调)
119+
Ab: 7, // Ab小调(Cb大调的关系小调)
120+
};
121+
122+
if (minorSharpKeys[letter]) {
123+
return { type: 'sharp', count: minorSharpKeys[letter] };
124+
}
125+
if (minorFlatKeys[letter]) {
126+
return { type: 'flat', count: minorFlatKeys[letter] };
127+
}
128+
129+
// 默认无升降号
130+
return { type: 'natural', count: 0 };
131+
}
132+
133+
/**
134+
* 根据调号获取指定音名字母的升降号
135+
*
136+
* @param keySignature - 调号对象
137+
* @param noteLetter - 音名字母(A-G)
138+
* @returns 该音在调号中应该的升降号
139+
*/
140+
export function getAccidentalFromKeySignature(
141+
keySignature: SNKeySignature,
142+
noteLetter: string,
143+
): SNAccidental {
144+
const { type, count } = calculateKeySignatureInfo(keySignature);
145+
146+
if (type === 'natural' || count === 0) {
147+
return SNAccidental.NATURAL;
148+
}
149+
150+
if (type === 'sharp') {
151+
// 升号调:取前count个音
152+
const sharpNotes = SHARP_ORDER.slice(0, count);
153+
if (sharpNotes.includes(noteLetter)) {
154+
return SNAccidental.SHARP;
155+
}
156+
} else if (type === 'flat') {
157+
// 降号调:取前count个音
158+
const flatNotes = FLAT_ORDER.slice(0, count);
159+
if (flatNotes.includes(noteLetter)) {
160+
return SNAccidental.FLAT;
161+
}
162+
}
163+
164+
return SNAccidental.NATURAL;
165+
}

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

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -127,13 +127,22 @@ function parseElementInternal(
127127

128128
// 4. 解析音符(带装饰符)
129129
// 支持整数(C4)、分数(C/2, C3/2)和简写(C/)三种时值表示
130+
// 升降号格式:^ (升号), ^^ (重升号), _ (降号), __ (重降号), = (还原号)
130131
const noteMatch = noteStr.match(
131-
/^(\^+\/?|_+\/?|=?)([A-Ga-g])([,']*)(\d+\/\d+|\/\d*|\d*)(\.*)$/,
132+
/^(\^+|_+|=?)([A-Ga-g])([,']*)(\d+\/\d+|\/\d*|\d*)(\.*)$/,
132133
);
133134
if (noteMatch) {
135+
// 构建完整的 originStr,包含装饰符(如果有)和音符
136+
// 装饰符在音符前面,所以格式是:装饰符 + 音符
137+
const fullOriginStr =
138+
decorations.length > 0
139+
? decorations.map((d) => d.text).join('') + noteStr
140+
: noteStr;
141+
134142
const note = parseNote(
135143
noteMatch,
136144
noteStr,
145+
fullOriginStr, // 传入完整的 originStr(包含装饰符和音符)
137146
timeUnit,
138147
defaultNoteLength,
139148
getNextId,
@@ -212,10 +221,18 @@ function parseRest(
212221

213222
/**
214223
* 解析音符
224+
*
225+
* @param match - 正则匹配结果
226+
* @param noteStr - 音符字符串(用于解析,不包含装饰符)
227+
* @param originStr - 完整的原始字符串(包含装饰符和音符,用于 originStr)
228+
* @param timeUnit - 时间单位
229+
* @param defaultNoteLength - 默认音符长度
230+
* @param getNextId - ID 生成器
215231
*/
216232
function parseNote(
217233
match: RegExpMatchArray,
218-
trimmed: string,
234+
noteStr: string,
235+
originStr: string,
219236
timeUnit: SNTimeUnit | undefined,
220237
defaultNoteLength: number | undefined,
221238
getNextId: (prefix: string) => string,
@@ -238,18 +255,18 @@ function parseNote(
238255
if (timeUnit) {
239256
const noteValue = parseDurationString(durationStr, defaultNoteLength);
240257

241-
dotCount = (trimmed.match(/\./g) || []).length;
258+
dotCount = (noteStr.match(/\./g) || []).length;
242259
const dottedNoteValue = calculateDottedNoteValue(noteValue, dotCount);
243260
duration = noteValueToDuration(dottedNoteValue, timeUnit);
244261
} else {
245262
duration = durationStr ? parseInt(durationStr, 10) : 1;
246-
// 即使没有 timeUnit,也尝试从 originStr 中提取附点数量
247-
dotCount = (trimmed.match(/\./g) || []).length;
263+
// 即使没有 timeUnit,也尝试从 noteStr 中提取附点数量
264+
dotCount = (noteStr.match(/\./g) || []).length;
248265
}
249266

250267
return new SNParserNote({
251268
id: getNextId('note'),
252-
originStr: trimmed,
269+
originStr, // 使用完整的 originStr(包含装饰符和音符)
253270
pitch: {
254271
letter: letter.toUpperCase(),
255272
octave,
@@ -264,10 +281,11 @@ function parseNote(
264281
* 解析变音记号
265282
*
266283
* @param accidentalStr - 变音记号字符串
267-
* @returns 变音记号枚举值
284+
* @returns 变音记号枚举值(undefined 表示没有升降号标记,NATURAL 表示明确写了还原号 =)
268285
*/
269-
function parseAccidental(accidentalStr: string): SNAccidental {
270-
if (!accidentalStr) return SNAccidental.NATURAL;
286+
function parseAccidental(accidentalStr: string): SNAccidental | undefined {
287+
// 如果没有升降号标记,返回 undefined
288+
if (!accidentalStr) return undefined;
271289

272290
switch (accidentalStr) {
273291
case '^':
@@ -279,9 +297,11 @@ function parseAccidental(accidentalStr: string): SNAccidental {
279297
case '__':
280298
return SNAccidental.DOUBLE_FLAT;
281299
case '=':
300+
// 明确写了还原号,返回 NATURAL
282301
return SNAccidental.NATURAL;
283302
default:
284-
return SNAccidental.NATURAL;
303+
// 未知格式,返回 undefined(不显示升降号)
304+
return undefined;
285305
}
286306
}
287307

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,14 @@ export class DecorationParser {
5757
}
5858

5959
// 2. 尝试提取符号形式装饰符
60+
// 注意:升降号符号(^, _, =)不应该被识别为装饰符
61+
// 它们应该在音符解析时处理
62+
const firstChar = remaining[0];
63+
if (firstChar === '^' || firstChar === '_' || firstChar === '=') {
64+
// 遇到升降号符号,停止提取装饰符
65+
break;
66+
}
67+
6068
const symbolResult = this.extractSymbolDecoration(remaining);
6169
if (symbolResult) {
6270
decorations.push(symbolResult.decoration);

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

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,17 +86,52 @@ export class AbcTokenizer {
8686
continue;
8787
}
8888

89-
// 2. 捕获普通音符/休止符(A-G/z开头,含变音、八度等)
89+
// 2. 捕获升降号符号(^, _, =),它们应该和后面的音符一起提取
90+
if (
91+
measureData[pos] === '^' ||
92+
measureData[pos] === '_' ||
93+
measureData[pos] === '='
94+
) {
95+
// 查找后面的音符字母
96+
let notePos = pos + 1;
97+
// 跳过连续的升降号符号(如 ^^, __)
98+
while (
99+
notePos < len &&
100+
(measureData[notePos] === '^' ||
101+
measureData[notePos] === '_' ||
102+
measureData[notePos] === '=')
103+
) {
104+
notePos++;
105+
}
106+
// 如果后面是音符字母,一起提取
107+
if (notePos < len && noteRegex.test(measureData[notePos])) {
108+
const note = this.extractNote(measureData, notePos);
109+
// 将升降号符号和音符合并
110+
const accidentalPart = measureData.slice(pos, notePos);
111+
tokens.push(accidentalPart + note.token);
112+
pos = note.endPos;
113+
continue;
114+
}
115+
// 如果后面不是音符字母,将升降号符号作为独立 token(这种情况不应该发生,但为了安全)
116+
tokens.push(measureData[pos]);
117+
pos++;
118+
continue;
119+
}
120+
121+
// 3. 捕获普通音符/休止符(A-G/z开头,含变音、八度等)
90122
if (noteRegex.test(measureData[pos])) {
91123
const note = this.extractNote(measureData, pos);
92124
tokens.push(note.token);
93125
pos = note.endPos;
94126
continue;
95127
}
96128

97-
// 3. 捕获其他符号(小节线等)
129+
// 4. 捕获其他符号(小节线等)
98130
let tokenEnd = pos;
99-
while (tokenEnd < len && !/\s|[A-Ga-gz([]/.test(measureData[tokenEnd])) {
131+
while (
132+
tokenEnd < len &&
133+
!/\s|[A-Ga-gz([^_=]/.test(measureData[tokenEnd])
134+
) {
100135
tokenEnd++;
101136
}
102137

0 commit comments

Comments
 (0)