| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832 |
- /**
- * 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<string, number> = {
- 'C': 1, 'D': 2, 'E': 3, 'F': 4, 'G': 5, 'A': 6, 'B': 7
- };
- /** 基准八度(中央C所在的八度) */
- const BASE_OCTAVE = 4;
- /** 音符类型到时值的映射 */
- const TYPE_TO_DURATION: Record<string, number> = {
- '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<number, { key: string; mode: string }> = {
- '-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<ParseOptions>;
-
- /** 解析统计 */
- 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<string, number> = {
- '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;
- }
|