Skip to content

Commit a823485

Browse files
committed
feat(decoration): 添加装饰符支持及解析逻辑
- 新增装饰符类型定义和装饰符对象接口,支持 ABC 标准 v2.1/v2.2 的装饰符解析。 - 引入 DecorationParser 工具类,提供从字符串中提取装饰符的功能,支持符号形式和长格式的装饰符。 - 更新相关解析器以处理音符中的装饰符,确保装饰符与音符的正确关联。 - 在 ABC 解析器中增强对装饰符的支持,提升乐谱解析的准确性和灵活性。 该变更提升了 ABC 解析器对装饰符的处理能力,确保乐谱信息的准确解析和存储。
1 parent df0eff6 commit a823485

File tree

13 files changed

+729
-88
lines changed

13 files changed

+729
-88
lines changed

.cursor/rules/lib.mdc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,7 @@ alwaysApply: true
2323
- 布局层(layout目录)要尽可能保证不干涉数据,是基于数据进行布局计算,并且不侵入渲染
2424
- 布局层一定要注意是自底向上构建
2525
- 渲染层(render目录)要尽可能保证不干涉数据和布局,基于布局层进行渲染,基于数据层进行显示
26+
27+
## 工作习惯
28+
29+
- 除非明确要求,否则不需要输出md文档
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
/**
2+
* 装饰符(Decoration/Ornament)类型定义
3+
*
4+
* 根据 ABC 标准 v2.1/v2.2
5+
* @see https://abcnotation.com/wiki/abc:standard:v2.1#decorations
6+
*/
7+
8+
/**
9+
* 装饰符类型
10+
*
11+
* ABC 支持两种表示方式:
12+
* 1. 符号形式:. ~ H L M O P S T u v 等
13+
* 2. 长格式:!trill!, !fermata!, !marcato! 等
14+
*/
15+
export enum SNDecorationType {
16+
// 基本装饰符
17+
STACCATO = 'staccato', // . 断奏
18+
TENUTO = 'tenuto', // - 保持音
19+
ACCENT = 'accent', // > < 重音
20+
MARCATO = 'marcato', // ^ 强音
21+
22+
// 颤音和回音
23+
TRILL = 'trill', // T !trill! 颤音
24+
MORDENT = 'mordent', // M !mordent! 波音
25+
LOWER_MORDENT = 'lower-mordent', // !lowermordent! 下波音
26+
UPPER_MORDENT = 'upper-mordent', // !uppermordent! 上波音
27+
PRALLTRILLER = 'pralltriller', // P !pralltriller! 回音
28+
TURN = 'turn', // !turn! 回音
29+
30+
// 滑音和装饰音
31+
SLIDE = 'slide', // !slide! 滑音
32+
ROLL = 'roll', // R !roll! 滚奏
33+
34+
// 延音和休止
35+
FERMATA = 'fermata', // H !fermata! 延音
36+
FERMATA_BELOW = 'fermata-below', // !fermata-below! 下延音
37+
FERMATA_ABOVE = 'fermata-above', // !fermata-above! 上延音
38+
BREATH = 'breath', // !breath! 换气记号
39+
40+
// 琶音和和弦装饰
41+
ARPEGGIO = 'arpeggio', // !arpeggio! 琶音
42+
43+
// 弓法(弦乐器)
44+
UPBOW = 'upbow', // u !upbow! 上弓
45+
DOWNBOW = 'downbow', // v !downbow! 下弓
46+
47+
// 力度记号
48+
CRESCENDO = 'crescendo', // !crescendo(! 渐强开始
49+
CRESCENDO_END = 'crescendo-end', // !crescendo)! 渐强结束
50+
DIMINUENDO = 'diminuendo', // !diminuendo(! 渐弱开始
51+
DIMINUENDO_END = 'diminuendo-end', // !diminuendo)! 渐弱结束
52+
53+
// 踏板记号(钢琴)
54+
PEDAL_DOWN = 'pedal-down', // !ped! 踏板按下
55+
PEDAL_UP = 'pedal-up', // !ped-up! 踏板抬起
56+
57+
// 特殊记号
58+
SEGNO = 'segno', // !segno! 反复记号
59+
CODA = 'coda', // !coda! 尾声记号
60+
D_C = 'd.c.', // !D.C.! 从头反复
61+
D_S = 'd.s.', // !D.S.! 从记号反复
62+
FINE = 'fine', // !fine! 终止
63+
64+
// 编辑性记号
65+
EDITORIAL = 'editorial', // !editorial! 编辑性记号
66+
COURTESY = 'courtesy', // !courtesy! 提示性记号
67+
68+
// 其他
69+
PLUS = 'plus', // + !plus! 加号
70+
EMPHASIS = 'emphasis', // !emphasis! 强调
71+
FINGERING = 'fingering', // 指法记号
72+
73+
// 自定义
74+
CUSTOM = 'custom', // 自定义装饰符
75+
}
76+
77+
/**
78+
* 装饰符对象
79+
*/
80+
export interface SNDecoration {
81+
/** 装饰符类型 */
82+
type: SNDecorationType;
83+
/** 原始文本(如 "!trill!", "T", "." 等) */
84+
text: string;
85+
/** 位置(above 或 below,某些装饰符支持) */
86+
position?: 'above' | 'below';
87+
/** 参数(如指法数字、自定义内容) */
88+
parameter?: string;
89+
}
90+
91+
/**
92+
* 装饰符符号到类型的映射
93+
*
94+
* 根据 ABC 标准定义
95+
*/
96+
export const DECORATION_SYMBOL_MAP: Record<string, SNDecorationType> = {
97+
// 基本符号形式
98+
'.': SNDecorationType.STACCATO,
99+
'~': SNDecorationType.ROLL,
100+
H: SNDecorationType.FERMATA,
101+
L: SNDecorationType.ACCENT,
102+
M: SNDecorationType.MORDENT,
103+
O: SNDecorationType.CODA,
104+
P: SNDecorationType.PRALLTRILLER,
105+
S: SNDecorationType.SEGNO,
106+
T: SNDecorationType.TRILL,
107+
u: SNDecorationType.UPBOW,
108+
v: SNDecorationType.DOWNBOW,
109+
};
110+
111+
/**
112+
* 装饰符长格式到类型的映射
113+
*
114+
* 支持 !decoration! 格式
115+
*/
116+
export const DECORATION_LONG_MAP: Record<string, SNDecorationType> = {
117+
// 颤音和回音
118+
trill: SNDecorationType.TRILL,
119+
mordent: SNDecorationType.MORDENT,
120+
lowermordent: SNDecorationType.LOWER_MORDENT,
121+
uppermordent: SNDecorationType.UPPER_MORDENT,
122+
pralltriller: SNDecorationType.PRALLTRILLER,
123+
turn: SNDecorationType.TURN,
124+
125+
// 滑音和装饰音
126+
slide: SNDecorationType.SLIDE,
127+
roll: SNDecorationType.ROLL,
128+
129+
// 延音
130+
fermata: SNDecorationType.FERMATA,
131+
'fermata-above': SNDecorationType.FERMATA_ABOVE,
132+
'fermata-below': SNDecorationType.FERMATA_BELOW,
133+
breath: SNDecorationType.BREATH,
134+
135+
// 琶音
136+
arpeggio: SNDecorationType.ARPEGGIO,
137+
138+
// 弓法
139+
upbow: SNDecorationType.UPBOW,
140+
downbow: SNDecorationType.DOWNBOW,
141+
142+
// 力度
143+
'crescendo(': SNDecorationType.CRESCENDO,
144+
'crescendo)': SNDecorationType.CRESCENDO_END,
145+
'diminuendo(': SNDecorationType.DIMINUENDO,
146+
'diminuendo)': SNDecorationType.DIMINUENDO_END,
147+
'<(': SNDecorationType.CRESCENDO,
148+
'<)': SNDecorationType.CRESCENDO_END,
149+
'>(': SNDecorationType.DIMINUENDO,
150+
'>)': SNDecorationType.DIMINUENDO_END,
151+
152+
// 踏板
153+
ped: SNDecorationType.PEDAL_DOWN,
154+
'ped-up': SNDecorationType.PEDAL_UP,
155+
156+
// 特殊记号
157+
segno: SNDecorationType.SEGNO,
158+
coda: SNDecorationType.CODA,
159+
'D.C.': SNDecorationType.D_C,
160+
'D.S.': SNDecorationType.D_S,
161+
fine: SNDecorationType.FINE,
162+
163+
// 编辑性记号
164+
editorial: SNDecorationType.EDITORIAL,
165+
courtesy: SNDecorationType.COURTESY,
166+
167+
// 基本装饰
168+
staccato: SNDecorationType.STACCATO,
169+
tenuto: SNDecorationType.TENUTO,
170+
accent: SNDecorationType.ACCENT,
171+
marcato: SNDecorationType.MARCATO,
172+
emphasis: SNDecorationType.EMPHASIS,
173+
174+
// 其他
175+
'+': SNDecorationType.PLUS,
176+
plus: SNDecorationType.PLUS,
177+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './music';
22
export * from './ticks';
3+
export * from './decoration';

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

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ export interface SNRootMeta {
2525
* 2. ABC 特有的元数据仅存储在 meta 中(不参与布局渲染):
2626
* - origin, area, notes, copyright, noteLength 等
2727
*
28-
* 根据 ABC 标准 v2.1
28+
* 根据 ABC 标准 v2.1/v2.2
2929
* @see https://abcnotation.com/wiki/abc:standard:v2.1
30+
* @see https://abcnotation.com/wiki/abc:standard:v2.2
3031
*/
3132
export interface SNScoreMeta {
3233
/** 来源/国家(O: 字段)- ABC 特有 */
@@ -35,11 +36,27 @@ export interface SNScoreMeta {
3536
area?: string;
3637
/** 注释(N: 字段)- ABC 特有 */
3738
notes?: string;
38-
/** 版权信息(S: 字段在头部时)- ABC 特有 */
39+
/** 版权信息/来源(S: 字段在头部时)- ABC 特有 */
3940
copyright?: string;
41+
/** 节奏类型(R: 字段,如 "Jig", "Reel", "Waltz")- ABC 特有 */
42+
rhythm?: string;
43+
/** 转录者信息(Z: 字段)- ABC 特有 */
44+
transcription?: string;
45+
/** 历史信息(H: 字段)- ABC 特有 */
46+
history?: string;
47+
/** 唱片信息(D: 字段)- ABC 特有 */
48+
discography?: string;
49+
/** 书籍来源(B: 字段)- ABC 特有 */
50+
book?: string;
51+
/** 文件 URL(F: 字段)- ABC 特有 */
52+
fileUrl?: string;
53+
/** 组/集合(G: 字段)- ABC 特有 */
54+
group?: string;
55+
/** 乐曲结构(P: 字段,如 "AABB", "ABAC")- ABC 特有 */
56+
parts?: string;
4057
/** 默认音符长度(L: 字段,如 "1/4", "1/8")- ABC 特有,已转换为 timeUnit 存储在 props 中 */
4158
noteLength?: string;
42-
/** 其他 ABC 特有的字段(H:, G:, R:, Z:, D:, F:, B: 等) */
59+
/** 其他 ABC 特有的字段 */
4360
[key: string]: unknown;
4461
}
4562

packages/simple-notation/src/data/node/note.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { SNPitch } from '@core/model/music';
1+
import { SNPitch, SNDecoration } from '@core/model';
22
import { SNParserNode, SNParserChord } from '@data/node';
33

44
/**
@@ -11,6 +11,7 @@ import { SNParserNode, SNParserChord } from '@data/node';
1111
* - duration: 时值(以 ticks 为单位)
1212
* - articulation: 演奏方式(断奏、连奏、保持音等)
1313
* - chords: 关联的和弦标记(可选)
14+
* - decorations: 装饰符列表(颤音、波音、延音等)
1415
*/
1516
export class SNParserNote extends SNParserNode {
1617
/** 音高信息(音名、八度、变音记号) */
@@ -19,6 +20,8 @@ export class SNParserNote extends SNParserNode {
1920
articulation?: 'staccato' | 'legato' | 'tenuto';
2021
/** 关联的和弦标记(可选,用于显示和弦符号) */
2122
chords?: SNParserChord;
23+
/** 装饰符列表(颤音、波音、延音等) */
24+
decorations?: SNDecoration[];
2225

2326
/**
2427
* 创建音符节点

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

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,8 @@ export class AbcParser extends BaseParser<SNAbcInput> {
304304
const musicMeasures = musicContent
305305
.split('|')
306306
.map((measure) => measure.trim())
307-
.filter(Boolean);
307+
.filter(Boolean)
308+
.filter((measure) => this.isValidMeasure(measure));
308309

309310
// 解析歌词
310311
const lyricsMap = this.lyricParser.parseLyrics(lyricLines, musicMeasures);
@@ -326,6 +327,33 @@ export class AbcParser extends BaseParser<SNAbcInput> {
326327
);
327328
}
328329

330+
/**
331+
* 判断是否为有效的小节
332+
*
333+
* 有效的小节应该包含实际的音乐内容(音符、休止符等),
334+
* 而不是只包含行内标记(如 [K:C]、[V:1]、[M:4/4] 等)
335+
*
336+
* @param measureData - 小节数据字符串
337+
* @returns 是否为有效小节
338+
*/
339+
private isValidMeasure(measureData: string): boolean {
340+
// 移除所有行内标记
341+
const withoutInlineFields = measureData
342+
.replace(/\[[A-Za-z]:[^\]]*\]/g, '')
343+
.trim();
344+
345+
// 移除小节线标记
346+
const withoutBarlines = withoutInlineFields.replace(/^:|:$/g, '').trim();
347+
348+
// 如果移除行内标记和小节线后还有内容,说明包含实际音乐内容
349+
if (withoutBarlines.length > 0) {
350+
return true;
351+
}
352+
353+
// 如果只剩空白,说明这不是有效的小节
354+
return false;
355+
}
356+
329357
/**
330358
* 从父节点获取时间单位
331359
*/

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

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
SNParserNode,
1010
} from '@data/node';
1111
import { noteValueToDuration } from '@core/utils/time-unit';
12-
import { AbcTokenizer } from '../utils';
12+
import { AbcTokenizer, DecorationParser } from '../utils';
1313
import { ElementParseError } from '../errors';
1414

1515
/**
@@ -69,8 +69,12 @@ export class AbcElementParser {
6969
throw new ElementParseError(elementData);
7070
}
7171

72+
// 0. 提取装饰符(如果有)
73+
const { decorations, noteStr } =
74+
DecorationParser.separateDecorations(trimmed);
75+
7276
// 1. 解析连音线
73-
if (trimmed === '-') {
77+
if (noteStr === '-') {
7478
return new SNParserTie({
7579
id: this.getNextId('tie'),
7680
style: 'slur',
@@ -79,7 +83,7 @@ export class AbcElementParser {
7983
}
8084

8185
// 2. 解析连音(Tuplet)
82-
const tupletMatch = trimmed.match(/^\((\d+)([\s\S]*?)\)?$/);
86+
const tupletMatch = noteStr.match(/^\((\d+)([\s\S]*?)\)?$/);
8387
if (tupletMatch) {
8488
return this.parseTuplet(
8589
tupletMatch,
@@ -90,16 +94,26 @@ export class AbcElementParser {
9094
}
9195

9296
// 3. 解析休止符
93-
if (trimmed.startsWith('z')) {
94-
return this.parseRest(trimmed, elementData, timeUnit, defaultNoteLength);
97+
if (noteStr.startsWith('z')) {
98+
return this.parseRest(noteStr, elementData, timeUnit, defaultNoteLength);
9599
}
96100

97-
// 4. 解析音符
98-
const noteMatch = trimmed.match(
101+
// 4. 解析音符(带装饰符)
102+
const noteMatch = noteStr.match(
99103
/^(\^+\/?|_+\/?|=?)([A-Ga-g])([,']*)(\d*)(\.*)$/,
100104
);
101105
if (noteMatch) {
102-
return this.parseNote(noteMatch, trimmed, timeUnit, defaultNoteLength);
106+
const note = this.parseNote(
107+
noteMatch,
108+
noteStr,
109+
timeUnit,
110+
defaultNoteLength,
111+
);
112+
// 将装饰符附加到音符
113+
if (decorations.length > 0) {
114+
note.decorations = decorations;
115+
}
116+
return note;
103117
}
104118

105119
throw new ElementParseError(elementData);

0 commit comments

Comments
 (0)