/** * OSMD数据解析器 * * @description 从OpenSheetMusicDisplay对象中解析简谱所需的数据 * * 核心功能: * 1. 解析曲谱元数据(标题、作曲家等) * 2. 解析小节信息(拍号、调号等) * 3. 解析音符数据(音高、时值、八度等) * 4. 保持OSMD原始数据引用(用于兼容层) */ import { JianpuScore } from '../../models/JianpuScore'; import { JianpuMeasure, createDefaultMeasure } from '../../models/JianpuMeasure'; import { JianpuNote, createDefaultNote } from '../../models/JianpuNote'; import { DivisionsHandler } from './DivisionsHandler'; import { DEFAULT_RENDER_CONFIG } from '../config/RenderConfig'; // ==================== 常量定义 ==================== /** 音名到简谱数字的映射 */ const STEP_TO_JIANPU: Record = { 'C': 1, 'D': 2, 'E': 3, 'F': 4, 'G': 5, 'A': 6, 'B': 7 }; /** 基准八度(中央C所在的八度) */ const BASE_OCTAVE = 4; /** 音符类型到时值的映射 */ const TYPE_TO_DURATION: Record = { '1024th': 1/256, '512th': 1/128, '256th': 1/64, '128th': 1/32, '64th': 1/16, '32nd': 1/8, '16th': 0.25, 'eighth': 0.5, 'quarter': 1.0, 'half': 2.0, 'whole': 4.0, 'breve': 8.0, 'long': 16.0, 'maxima': 32.0, }; /** 升降号数量到调名的映射 */ const FIFTHS_TO_KEY: Record = { '-7': { key: 'Cb', mode: 'major' }, '-6': { key: 'Gb', mode: 'major' }, '-5': { key: 'Db', mode: 'major' }, '-4': { key: 'Ab', mode: 'major' }, '-3': { key: 'Eb', mode: 'major' }, '-2': { key: 'Bb', mode: 'major' }, '-1': { key: 'F', mode: 'major' }, '0': { key: 'C', mode: 'major' }, '1': { key: 'G', mode: 'major' }, '2': { key: 'D', mode: 'major' }, '3': { key: 'A', mode: 'major' }, '4': { key: 'E', mode: 'major' }, '5': { key: 'B', mode: 'major' }, '6': { key: 'F#', mode: 'major' }, '7': { key: 'C#', mode: 'major' }, }; // ==================== 类型定义 ==================== /** OSMD实例类型(简化定义,避免依赖OSMD类型) */ interface OSMDInstance { GraphicSheet?: { MeasureList?: any[][]; }; Sheet?: { Title?: { text?: string }; Subtitle?: { text?: string }; Composer?: { text?: string }; Lyricist?: { text?: string }; Instruments?: any[]; SourceMeasures?: any[]; }; cursor?: { Iterator?: any; reset?: () => void; next?: () => void; }; } /** 解析选项 */ export interface ParseOptions { /** 是否跳过装饰音 */ skipGraceNotes?: boolean; /** 是否包含多声部 */ includeMultiVoice?: boolean; /** 默认速度(BPM) */ defaultTempo?: number; } /** 解析结果统计 */ export interface ParseStats { measureCount: number; noteCount: number; voiceCount: number; restCount: number; graceNoteCount: number; parseTime: number; } // ==================== 主类 ==================== /** * OSMD数据解析器 */ export class OSMDDataParser { /** Divisions处理器 */ private divisionsHandler: DivisionsHandler; /** 解析选项 */ private options: Required; /** 解析统计 */ private stats: ParseStats = { measureCount: 0, noteCount: 0, voiceCount: 0, restCount: 0, graceNoteCount: 0, parseTime: 0, }; /** 音符ID计数器 */ private noteIdCounter: number = 0; /** * 构造函数 * @param options 解析选项 */ constructor(options: ParseOptions = {}) { this.divisionsHandler = new DivisionsHandler(); this.options = { skipGraceNotes: options.skipGraceNotes ?? false, includeMultiVoice: options.includeMultiVoice ?? true, defaultTempo: options.defaultTempo ?? 120, }; } /** * 从OSMD对象解析简谱数据 * * @param osmd OpenSheetMusicDisplay实例 * @returns 简谱数据结构 */ parse(osmd: OSMDInstance): JianpuScore { const startTime = performance.now(); console.log('[OSMDDataParser] 开始解析OSMD数据'); // 验证OSMD对象 if (!osmd) { throw new Error('[OSMDDataParser] OSMD实例不能为空'); } // 重置状态 this.resetState(); // 1. 提取元数据 const metadata = this.extractMetadata(osmd); console.log(`[OSMDDataParser] 元数据: 标题="${metadata.title}", 作曲="${metadata.composer}"`); // 2. 解析小节 const measures = this.parseMeasures(osmd); console.log(`[OSMDDataParser] 解析了 ${measures.length} 个小节`); // 3. 解析音符(填充到小节中) this.parseNotes(osmd, measures); console.log(`[OSMDDataParser] 解析了 ${this.stats.noteCount} 个音符`); // 4. 构建JianpuScore const score: JianpuScore = { title: metadata.title, subtitle: metadata.subtitle, composer: metadata.composer, lyricist: metadata.lyricist, systems: [], // 布局引擎会填充 measures, tempo: metadata.tempo, initialTimeSignature: measures[0]?.timeSignature ?? { beats: 4, beatType: 4 }, initialKeySignature: measures[0]?.keySignature ?? { key: 'C', mode: 'major' }, config: { ...DEFAULT_RENDER_CONFIG }, totalMeasures: measures.length, voiceCount: this.stats.voiceCount, metadata: { parseStats: { ...this.stats }, }, }; // 计算总时长 score.duration = this.calculateTotalDuration(measures, metadata.tempo); this.stats.parseTime = performance.now() - startTime; console.log(`[OSMDDataParser] 解析完成,耗时 ${this.stats.parseTime.toFixed(2)}ms`); return score; } /** * 获取解析统计 */ getStats(): ParseStats { return { ...this.stats }; } // ==================== 私有方法 ==================== /** * 重置解析状态 */ private resetState(): void { this.divisionsHandler.reset(); this.noteIdCounter = 0; this.stats = { measureCount: 0, noteCount: 0, voiceCount: 0, restCount: 0, graceNoteCount: 0, parseTime: 0, }; } /** * 生成唯一音符ID */ private generateNoteId(): string { return `note-${++this.noteIdCounter}`; } /** * 提取曲谱元数据 */ private extractMetadata(osmd: OSMDInstance): { title: string; subtitle?: string; composer?: string; lyricist?: string; tempo: number; } { const sheet = osmd.Sheet; let tempo = this.options.defaultTempo; // 尝试从第一个小节获取速度 if (sheet?.SourceMeasures?.[0]) { const firstMeasure = sheet.SourceMeasures[0]; if (firstMeasure.tempoInBPM) { tempo = firstMeasure.tempoInBPM; } else if (firstMeasure.TempoExpressions?.[0]?.InstantaneousTempo?.tempoInBpm) { tempo = firstMeasure.TempoExpressions[0].InstantaneousTempo.tempoInBpm; } } return { title: sheet?.Title?.text ?? 'Untitled', subtitle: sheet?.Subtitle?.text, composer: sheet?.Composer?.text, lyricist: sheet?.Lyricist?.text, tempo, }; } /** * 解析小节信息 */ private parseMeasures(osmd: OSMDInstance): JianpuMeasure[] { const measures: JianpuMeasure[] = []; // 优先使用SourceMeasures(更准确) const sourceMeasures = osmd.Sheet?.SourceMeasures; if (sourceMeasures && sourceMeasures.length > 0) { for (let i = 0; i < sourceMeasures.length; i++) { const srcMeasure = sourceMeasures[i]; const measure = this.parseSourceMeasure(srcMeasure, i); measures.push(measure); } } else { // 回退到GraphicSheet.MeasureList const measureList = osmd.GraphicSheet?.MeasureList; if (measureList && measureList.length > 0) { for (let i = 0; i < measureList.length; i++) { const graphicalMeasures = measureList[i]; if (graphicalMeasures && graphicalMeasures.length > 0) { const measure = this.parseGraphicalMeasure(graphicalMeasures[0], i); measures.push(measure); } } } } this.stats.measureCount = measures.length; return measures; } /** * 解析SourceMeasure */ private parseSourceMeasure(srcMeasure: any, index: number): JianpuMeasure { // 获取拍号 const timeSig = srcMeasure.ActiveTimeSignature; const timeSignature = { beats: timeSig?.numerator ?? timeSig?.Numerator ?? 4, beatType: timeSig?.denominator ?? timeSig?.Denominator ?? 4, }; // 获取调号 const keySig = srcMeasure.ActiveKeySignature; const keySignature = this.parseKeySignature(keySig); // 获取速度 let tempo: number | undefined; if (srcMeasure.tempoInBPM) { tempo = srcMeasure.tempoInBPM; } else if (srcMeasure.TempoExpressions?.[0]?.InstantaneousTempo?.tempoInBpm) { tempo = srcMeasure.TempoExpressions[0].InstantaneousTempo.tempoInBpm; } // 创建小节 const measure = createDefaultMeasure(index + 1, timeSignature); measure.keySignature = keySignature; if (tempo !== undefined) { measure.tempo = tempo; } // 小节线类型 measure.barlineType = this.getBarlineType(srcMeasure); // 是否显示拍号/调号(第一小节或变化时显示) measure.showTimeSignature = index === 0; measure.showKeySignature = index === 0; return measure; } /** * 解析GraphicalMeasure */ private parseGraphicalMeasure(graphicalMeasure: any, index: number): JianpuMeasure { const srcMeasure = graphicalMeasure.parentSourceMeasure; if (srcMeasure) { return this.parseSourceMeasure(srcMeasure, index); } // 如果没有SourceMeasure,创建默认小节 return createDefaultMeasure(index + 1); } /** * 解析调号 */ private parseKeySignature(keySig: any): { key: string; mode: 'major' | 'minor'; alterations?: number; } { if (!keySig) { return { key: 'C', mode: 'major', alterations: 0 }; } const fifths = keySig.keyTypeOriginal ?? keySig.Key ?? 0; const mode = keySig.Mode ?? 0; // 0 = major, 1 = minor const keyInfo = FIFTHS_TO_KEY[fifths.toString()] ?? { key: 'C', mode: 'major' }; return { key: keyInfo.key, mode: mode === 1 ? 'minor' : 'major', alterations: fifths, }; } /** * 获取小节线类型 */ private getBarlineType(srcMeasure: any): JianpuMeasure['barlineType'] { const instructions = srcMeasure.lastRepetitionInstructions; if (instructions && instructions.length > 0) { for (const inst of instructions) { const type = inst.type; if (type === 'StartLine' || type === 2) return 'repeat-start'; if (type === 'BackJumpLine' || type === 3) return 'repeat-end'; if (type === 'Ending') return 'double'; } } // 检查是否是最后一个小节 if (srcMeasure.endingBarStyleEnum === 'light-heavy' || srcMeasure.endingBarStyle === 'light-heavy') { return 'final'; } return 'single'; } /** * 解析音符信息 */ private parseNotes(osmd: OSMDInstance, measures: JianpuMeasure[]): void { const cursor = osmd.cursor; if (!cursor?.Iterator) { console.warn('[OSMDDataParser] 无法获取cursor.Iterator,尝试从MeasureList解析'); this.parseNotesFromMeasureList(osmd, measures); return; } // 使用cursor遍历 cursor.reset?.(); const iterator = cursor.Iterator; let maxVoiceIndex = 0; while (!iterator.EndReached) { const voiceEntries = iterator.currentVoiceEntries ?? iterator.CurrentVoiceEntries ?? []; const measureIndex = iterator.currentMeasureIndex ?? 0; if (measureIndex >= 0 && measureIndex < measures.length) { const measure = measures[measureIndex]; const timestamp = iterator.currentTimeStamp?.RealValue ?? iterator.currentTimeStamp?.realValue ?? 0; for (const voiceEntry of voiceEntries) { const voiceIndex = voiceEntry.ParentVoice?.VoiceId ?? 0; maxVoiceIndex = Math.max(maxVoiceIndex, voiceIndex); // 确保声部数组存在 while (measure.voices.length <= voiceIndex) { measure.voices.push([]); } const notes = voiceEntry.Notes ?? voiceEntry.notes ?? []; for (const note of notes) { const jianpuNote = this.parseNote(note, measureIndex, voiceIndex, timestamp); if (jianpuNote) { measure.voices[voiceIndex].push(jianpuNote); this.stats.noteCount++; if (jianpuNote.isRest) this.stats.restCount++; if (jianpuNote.isGraceNote) this.stats.graceNoteCount++; } } } } // 移动到下一个 try { if (iterator.moveToNextVisibleVoiceEntry) { iterator.moveToNextVisibleVoiceEntry(this.options.skipGraceNotes); } else { cursor.next?.(); } } catch (e) { console.warn('[OSMDDataParser] Iterator移动失败:', e); break; } } this.stats.voiceCount = maxVoiceIndex + 1; // 同步notes和voices引用 for (const measure of measures) { measure.notes = measure.voices; } } /** * 从MeasureList解析音符(备用方案) */ private parseNotesFromMeasureList(osmd: OSMDInstance, measures: JianpuMeasure[]): void { const measureList = osmd.GraphicSheet?.MeasureList; if (!measureList) { console.warn('[OSMDDataParser] 无法获取MeasureList'); return; } let maxVoiceIndex = 0; for (let measureIdx = 0; measureIdx < measureList.length && measureIdx < measures.length; measureIdx++) { const graphicalMeasures = measureList[measureIdx]; const measure = measures[measureIdx]; if (!graphicalMeasures) continue; for (const gMeasure of graphicalMeasures) { if (!gMeasure?.staffEntries) continue; for (const staffEntry of gMeasure.staffEntries) { const timestamp = staffEntry.relInMeasureTimestamp?.RealValue ?? 0; const voiceEntries = staffEntry.graphicalVoiceEntries ?? []; for (const gve of voiceEntries) { const voiceIndex = gve.parentVoiceEntry?.ParentVoice?.VoiceId ?? 0; maxVoiceIndex = Math.max(maxVoiceIndex, voiceIndex); // 确保声部数组存在 while (measure.voices.length <= voiceIndex) { measure.voices.push([]); } const notes = gve.notes ?? []; for (const graphicalNote of notes) { const srcNote = graphicalNote.sourceNote; if (!srcNote) continue; const jianpuNote = this.parseNote(srcNote, measureIdx, voiceIndex, timestamp); if (jianpuNote) { // 保存图形化音符的SVG引用 if (graphicalNote.vfnote?.[0]?.attrs?.id) { jianpuNote.osmdCompatible.svgElement.attrs.id = graphicalNote.vfnote[0].attrs.id; } measure.voices[voiceIndex].push(jianpuNote); this.stats.noteCount++; if (jianpuNote.isRest) this.stats.restCount++; if (jianpuNote.isGraceNote) this.stats.graceNoteCount++; } } } } } } this.stats.voiceCount = maxVoiceIndex + 1; // 同步notes和voices引用 for (const measure of measures) { measure.notes = measure.voices; } } /** * 解析单个音符 */ private parseNote( note: any, measureIndex: number, voiceIndex: number, timestamp: number ): JianpuNote | null { // 跳过装饰音(如果配置了) if (this.options.skipGraceNotes && note.IsGraceNote) { return null; } const isRest = note.isRestFlag ?? note.IsRest ?? note.isRest ?? false; const isGraceNote = note.IsGraceNote ?? note.isGraceNote ?? false; // 获取音高信息 let pitch = 0; let octave = 0; let accidental: JianpuNote['accidental'] = undefined; let halfTone = 0; let frequency = 0; if (!isRest && note.pitch) { pitch = this.getPitchNumber(note); octave = this.getOctaveOffset(note); accidental = this.getAccidental(note); halfTone = note.halfTone ?? this.calculateHalfTone(note); frequency = note.pitch?.frequency ?? this.halfToneToFrequency(halfTone); } // 获取时值 const duration = this.getNoteDuration(note); const dots = this.getDotCount(note); // 创建音符 const jianpuNote = createDefaultNote({ pitch, octave, duration, accidental, dots, timestamp, voiceIndex, measureIndex, isRest, isGraceNote, isStaccato: note.isStaccato ?? false, }); // 设置ID jianpuNote.id = this.generateNoteId(); // 设置OSMD兼容数据 jianpuNote.osmdCompatible = { noteElement: note, svgElement: { attrs: { id: `vf-${jianpuNote.id}` }, modifiers: note.modifiers, }, halfTone, frequency, realKey: pitch > 0 ? pitch + octave * 7 : 0, }; return jianpuNote; } /** * 获取简谱音高(1-7) */ private getPitchNumber(note: any): number { // 方式1:从pitch.step获取 const step = note.pitch?.step ?? note.pitch?.Step; if (step !== undefined) { const pitch = STEP_TO_JIANPU[step.toString().toUpperCase()]; if (pitch !== undefined) { return pitch; } } // 方式2:从halfTone计算 const halfTone = note.halfTone ?? note.HalfTone; if (halfTone !== undefined) { // halfTone % 12 得到音级,映射到1-7 const noteInOctave = ((halfTone % 12) + 12) % 12; const halfToneToJianpu = [1, 1, 2, 2, 3, 4, 4, 5, 5, 6, 6, 7]; // C C# D D# E F F# G G# A A# B return halfToneToJianpu[noteInOctave]; } console.warn('[OSMDDataParser] 无法获取音高信息,返回默认值1'); return 1; } /** * 获取八度偏移 */ private getOctaveOffset(note: any): number { // 方式1:从pitch.octave获取 const octave = note.pitch?.octave ?? note.pitch?.Octave; if (octave !== undefined) { return octave - BASE_OCTAVE; } // 方式2:从halfTone计算 const halfTone = note.halfTone ?? note.HalfTone; if (halfTone !== undefined) { const octaveFromHalfTone = Math.floor(halfTone / 12) - 1; // MIDI标准 return octaveFromHalfTone - BASE_OCTAVE; } return 0; } /** * 获取升降号 */ private getAccidental(note: any): JianpuNote['accidental'] | undefined { // 方式1:从pitch.alter获取 const alter = note.pitch?.alter ?? note.pitch?.Alter; if (alter === 1 || alter === 2) return 'sharp'; if (alter === -1 || alter === -2) return 'flat'; if (alter === 0) return 'natural'; // 方式2:从accidental属性获取 const accidental = note.accidental ?? note.Accidental; if (accidental) { const accStr = accidental.toString().toLowerCase(); if (accStr.includes('sharp')) return 'sharp'; if (accStr.includes('flat')) return 'flat'; if (accStr.includes('natural')) return 'natural'; } return undefined; } /** * 获取音符时值 */ private getNoteDuration(note: any): number { // 方式1:从length.realValue获取(最准确) const realValue = note.length?.RealValue ?? note.length?.realValue; if (realValue !== undefined && realValue > 0) { return realValue; } // 方式2:从noteType获取 const noteType = note.noteTypeXml ?? note.TypeLength?.toString() ?? note.type; if (noteType) { const baseDuration = TYPE_TO_DURATION[noteType.toLowerCase()]; if (baseDuration !== undefined) { // 应用附点 const dots = this.getDotCount(note); return this.divisionsHandler.applyDots(baseDuration, dots); } } // 方式3:从duration和divisions计算 const duration = note.duration ?? note.Duration; if (duration !== undefined) { const sourceMeasure = note.sourceMeasure ?? note.SourceMeasure; const divisions = sourceMeasure?.divisions ?? 256; this.divisionsHandler.setDivisions(divisions); return this.divisionsHandler.toRealValue(duration); } console.warn('[OSMDDataParser] 无法获取音符时值,返回默认值1.0'); return 1.0; } /** * 获取附点数量 */ private getDotCount(note: any): number { // 方式1:直接获取dots数量 const dots = note.dots ?? note.Dots ?? note.DotsXml; if (typeof dots === 'number') { return dots; } // 方式2:从数组长度获取 if (Array.isArray(dots)) { return dots.length; } return 0; } /** * 计算半音值(从音高信息) */ private calculateHalfTone(note: any): number { const step = note.pitch?.step ?? note.pitch?.Step; const octave = note.pitch?.octave ?? note.pitch?.Octave ?? 4; const alter = note.pitch?.alter ?? note.pitch?.Alter ?? 0; if (!step) return 60; // 默认C4 const stepToSemitone: Record = { 'C': 0, 'D': 2, 'E': 4, 'F': 5, 'G': 7, 'A': 9, 'B': 11 }; const semitone = stepToSemitone[step.toString().toUpperCase()] ?? 0; return (octave + 1) * 12 + semitone + alter; } /** * 半音值转频率 */ private halfToneToFrequency(halfTone: number): number { // A4 = 440Hz, MIDI note 69 return 440 * Math.pow(2, (halfTone - 69) / 12); } /** * 计算总时长 */ private calculateTotalDuration(measures: JianpuMeasure[], tempo: number): number { let totalBeats = 0; for (const measure of measures) { const { beats, beatType } = measure.timeSignature; // 将节拍数转换为四分音符数量 const quarterNotesInMeasure = beats * (4 / beatType); totalBeats += quarterNotesInMeasure; } // 四分音符时长 = 60 / BPM const quarterNoteDuration = 60 / tempo; return totalBeats * quarterNoteDuration; } } // ==================== 工厂函数 ==================== /** * 创建OSMD数据解析器 */ export function createOSMDDataParser(options?: ParseOptions): OSMDDataParser { return new OSMDDataParser(options); } // ==================== 工具函数(导出供其他模块使用) ==================== /** * 音名转简谱数字 */ export function stepToJianpu(step: string): number { const pitch = STEP_TO_JIANPU[step.toUpperCase()]; if (pitch === undefined) { throw new Error(`无效的音名: ${step}`); } return pitch; } /** * 八度转偏移 */ export function octaveToOffset(octave: number): number { return octave - BASE_OCTAVE; } /** * 升降号值转类型 */ export function alterToAccidental(alter: number | null): 'sharp' | 'flat' | 'natural' | null { if (alter === null || alter === undefined) return null; if (alter > 0) return 'sharp'; if (alter < 0) return 'flat'; return 'natural'; } /** * 音符类型转时值 */ export function noteTypeToRealValue(noteType: string): number { const duration = TYPE_TO_DURATION[noteType.toLowerCase()]; if (duration === undefined) { console.warn(`未知的音符类型: ${noteType}`); return 1.0; } return duration; }