@@ -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 * [ w W ] : \s * .* $ / gim, '' )
714+ . replace ( / \[ \s * V : \s * \d + \s * \] / g, '' ) // 移除 [V:1] 这样的标记
715+ . replace ( / ^ \s * [ w W ] : \s * .* $ / gim, '' ) // 移除歌词行
596716 . trim ( ) ;
597717
598718 const rawMeasures = musicContent
0 commit comments