Skip to content

Commit 2725898

Browse files
committed
feat(parser): 优化 ABC 解析器以增强乐谱内容处理
- 重构乐谱体解析逻辑,分离 V: 定义和乐谱内容,提升解析准确性。 - 增强对声部定义的处理,支持多个声部的内容分割和映射。 - 更新乐谱体内容的处理,移除不必要的标记,确保歌词与音符的对齐效果。 该变更提升了 ABC 解析器的灵活性和可维护性,确保乐谱内容的准确解析。
1 parent 8609c91 commit 2725898

File tree

2 files changed

+160
-8
lines changed

2 files changed

+160
-8
lines changed

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

Lines changed: 126 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -398,11 +398,127 @@ export class AbcParser extends BaseParser<SNAbcInput> {
398398
const { headerFields, content } = this.splitSectionHeaderAndContent(rest);
399399
const { props, meta } = this.parseSectionHeader(headerFields, sMetaValue);
400400

401-
const voiceMatch = content.trim().match(/(?<voice>V:.*?(?=\s*V:|$))/gs);
402-
const voices = voiceMatch?.map((v) => v.trim()) || [];
401+
// 第一步:从 content 中分离出 V: 定义和乐谱体
402+
// V: 定义是行首的 V: 开头的行,乐谱体是包含 [V:1] 标记和音符的内容
403+
const lines = content.split(/\r?\n/);
404+
const voiceHeaders: string[] = [];
405+
const musicBody: string[] = [];
406+
let foundMusicContent = false;
403407

404-
if (voices.length === 0 && content.trim()) {
405-
voices.push(content.trim());
408+
for (const line of lines) {
409+
const trimmedLine = line.trim();
410+
// 如果遇到行首的 V: 定义(不是 [V:1] 这样的标记),收集为声部定义
411+
if (/^\s*V:\s*\d+/.test(trimmedLine) && !foundMusicContent) {
412+
voiceHeaders.push(trimmedLine);
413+
} else {
414+
// 一旦遇到非 V: 定义的行,后续所有内容都是乐谱体
415+
foundMusicContent = true;
416+
musicBody.push(line);
417+
}
418+
}
419+
420+
const musicBodyContent = musicBody.join('\n');
421+
422+
// 建立声部编号到元数据的映射
423+
const voiceMetadataMap = new Map<
424+
string,
425+
{ voiceNumber: string; metaLine: string; fullLine: string }
426+
>();
427+
for (const voiceHeader of voiceHeaders) {
428+
const match = voiceHeader.match(/^\s*V:\s*(\d+)\s*(.*)$/);
429+
if (match) {
430+
const [, voiceNumber, metaLine] = match;
431+
voiceMetadataMap.set(voiceNumber, {
432+
voiceNumber,
433+
metaLine: metaLine || '',
434+
fullLine: voiceHeader,
435+
});
436+
}
437+
}
438+
439+
// 第二步:解析乐谱体,按 [V:1] 标记分割内容
440+
// 如果没有任何 V: 定义,则整个内容作为一个默认声部
441+
if (voiceMetadataMap.size === 0 && musicBodyContent.trim()) {
442+
const defaultVoice = this.parseVoice(musicBodyContent.trim());
443+
return new SNParserSection({
444+
id: sMetaValue || this.getNextId('section'),
445+
originStr: sectionData,
446+
})
447+
.setMeta(meta)
448+
.setProps(props)
449+
.addChildren([defaultVoice]);
450+
}
451+
452+
// 按 [V:1] 标记分割乐谱体内容
453+
// 匹配 [V:数字] 标记,将标记后的内容收集到对应的声部
454+
const voiceContentMap = new Map<string, string[]>();
455+
456+
// 处理第一个 [V:1] 之前的内容(如果有)
457+
const firstVoiceMatch = musicBodyContent.match(/\[\s*V:\s*(\d+)\s*\]/);
458+
if (
459+
firstVoiceMatch &&
460+
firstVoiceMatch.index !== undefined &&
461+
firstVoiceMatch.index > 0
462+
) {
463+
const beforeFirstVoice = musicBodyContent
464+
.substring(0, firstVoiceMatch.index)
465+
.trim();
466+
if (beforeFirstVoice) {
467+
// 如果第一个声部定义存在,将之前的内容分配给第一个声部
468+
if (voiceMetadataMap.size > 0) {
469+
const firstVoiceNumber = Array.from(voiceMetadataMap.keys())[0];
470+
if (!voiceContentMap.has(firstVoiceNumber)) {
471+
voiceContentMap.set(firstVoiceNumber, []);
472+
}
473+
voiceContentMap.get(firstVoiceNumber)!.push(beforeFirstVoice);
474+
}
475+
}
476+
}
477+
478+
// 处理所有 [V:数字] 标记后的内容
479+
// 使用正则表达式匹配 [V:数字] 标记,并捕获标记后的内容(直到下一个 [V:数字] 或文件结尾)
480+
const voiceBlockRegex =
481+
/\[\s*V:\s*(\d+)\s*\]([^[]*?)(?=\[\s*V:\s*\d+\s*\]|$)/gs;
482+
let match;
483+
while ((match = voiceBlockRegex.exec(musicBodyContent)) !== null) {
484+
const voiceNumber = match[1];
485+
const blockContent = match[2].trim();
486+
487+
if (!voiceContentMap.has(voiceNumber)) {
488+
voiceContentMap.set(voiceNumber, []);
489+
}
490+
if (blockContent) {
491+
voiceContentMap.get(voiceNumber)!.push(blockContent);
492+
}
493+
}
494+
495+
// 如果没有找到任何 [V:数字] 标记,但有 V: 定义,则将整个乐谱体分配给第一个声部
496+
if (
497+
voiceContentMap.size === 0 &&
498+
voiceMetadataMap.size > 0 &&
499+
musicBodyContent.trim()
500+
) {
501+
const firstVoiceNumber = Array.from(voiceMetadataMap.keys())[0];
502+
voiceContentMap.set(firstVoiceNumber, [musicBodyContent.trim()]);
503+
}
504+
505+
// 第三步:为每个声部创建解析节点
506+
const voices: SNParserVoice[] = [];
507+
for (const [voiceNumber, contents] of voiceContentMap.entries()) {
508+
const metadata = voiceMetadataMap.get(voiceNumber);
509+
if (metadata) {
510+
// 合并该声部的所有内容块
511+
const combinedContent = contents.join('\n');
512+
// 构建完整的声部数据:V:定义 + 乐谱内容
513+
const fullVoiceData = `${metadata.fullLine}\n${combinedContent}`;
514+
const voice = this.parseVoice(fullVoiceData);
515+
voices.push(voice);
516+
}
517+
}
518+
519+
// 如果没有任何声部内容,创建一个默认声部
520+
if (voices.length === 0 && musicBodyContent.trim()) {
521+
voices.push(this.parseVoice(musicBodyContent.trim()));
406522
}
407523

408524
return new SNParserSection({
@@ -411,7 +527,7 @@ export class AbcParser extends BaseParser<SNAbcInput> {
411527
})
412528
.setMeta(meta)
413529
.setProps(props)
414-
.addChildren(voices.map((voiceData) => this.parseVoice(voiceData)));
530+
.addChildren(voices);
415531
}
416532

417533
private splitSectionHeaderAndContent(sectionContent: string): {
@@ -591,8 +707,12 @@ export class AbcParser extends BaseParser<SNAbcInput> {
591707
}> = [];
592708
let verseNumber = 0;
593709

710+
// 移除乐谱体中的 [V:1] 这样的声部标记,这些标记不应该被处理
711+
// 同时移除歌词行,以便后续处理
712+
// 注意:需要移除 [V:1] 但保留其他元数据标记如 [K:C]
594713
const musicContent = measuresContent
595-
.replace(/^\s*[wW]:\s*.*$/gim, '')
714+
.replace(/\[\s*V:\s*\d+\s*\]/g, '') // 移除 [V:1] 这样的标记
715+
.replace(/^\s*[wW]:\s*.*$/gim, '') // 移除歌词行
596716
.trim();
597717

598718
const rawMeasures = musicContent

packages/web/src/pages/LayoutTest.vue

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,53 @@ const container = ref<HTMLDivElement | null>(null);
2121
let sn: SimpleNotation | null = null;
2222
2323
const abcData: string = `
24+
%%abc-2.1
25+
%%encoding utf-8
26+
% 这是一个测试乐谱,用于验证各种 ABC Notation 功能
27+
28+
X:1
2429
T:小星星
2530
T:Twinkle Twinkle Little Star
26-
C:Traditional
31+
C:作词:佚名
32+
C:作曲:Traditional
2733
M:4/4
2834
L:1/4
2935
Q:1/4=100
3036
K:C major
3137
S:1
3238
V:1 name="Melody" clef=treble
33-
[K:C] |: C C G G | A A G2 | F F E E | D D C2 |
39+
[V:1] [K:C] |: C C G G | A A G2 | F F E E | D D C2 |
3440
w:一 闪 一 闪 | 亮 晶 晶 | 满 天 都 是 | 小 星 星 |
3541
G G F F | E E D2 | G G F F | E E D2 :|
3642
w:挂 在 天 空 | 放 光 明 | 好 像 许 多 | 小 眼 睛 |
3743
`;
3844
45+
// % 第二段:行内歌词(英文)
46+
// |: C C G G | A A G2 | F F E E | D D C2 |
47+
// w:Twinkle twinkle | little star | how I wonder | what you are |
48+
// G G F F | E E D2 | G G F F | E E D2 :|
49+
// w:Up above the | world so high | like a diamond | in the sky |
50+
51+
// % 第三段:段落歌词(中文)
52+
// |: C C G G | A A G2 | F F E E | D D C2 |
53+
// W: 一闪一闪亮晶晶
54+
// W: 满天都是小星星
55+
// W: 挂在天空放光明
56+
// W: 好像许多小眼睛
57+
// G G F F | E E D2 | C C G G | A A G2 :|
58+
// W: 一闪一闪亮晶晶
59+
// W: 满天都是小星星
60+
61+
// % 第四段:段落歌词(英文)
62+
// |: C C G G | A A G2 | F F E E | D D C2 |
63+
// W: Twinkle twinkle little star
64+
// W: How I wonder what you are
65+
// W: Up above the world so high
66+
// W: Like a diamond in the sky
67+
// G G F F | E E D2 | C C G G | A A G2 :|
68+
// W: Twinkle twinkle little star
69+
// W: How I wonder what you are
70+
3971
onMounted(() => {
4072
if (!container.value) {
4173
console.error('Container element not found');

0 commit comments

Comments
 (0)