OSMDDataParser.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832
  1. /**
  2. * OSMD数据解析器
  3. *
  4. * @description 从OpenSheetMusicDisplay对象中解析简谱所需的数据
  5. *
  6. * 核心功能:
  7. * 1. 解析曲谱元数据(标题、作曲家等)
  8. * 2. 解析小节信息(拍号、调号等)
  9. * 3. 解析音符数据(音高、时值、八度等)
  10. * 4. 保持OSMD原始数据引用(用于兼容层)
  11. */
  12. import { JianpuScore } from '../../models/JianpuScore';
  13. import { JianpuMeasure, createDefaultMeasure } from '../../models/JianpuMeasure';
  14. import { JianpuNote, createDefaultNote } from '../../models/JianpuNote';
  15. import { DivisionsHandler } from './DivisionsHandler';
  16. import { DEFAULT_RENDER_CONFIG } from '../config/RenderConfig';
  17. // ==================== 常量定义 ====================
  18. /** 音名到简谱数字的映射 */
  19. const STEP_TO_JIANPU: Record<string, number> = {
  20. 'C': 1, 'D': 2, 'E': 3, 'F': 4, 'G': 5, 'A': 6, 'B': 7
  21. };
  22. /** 基准八度(中央C所在的八度) */
  23. const BASE_OCTAVE = 4;
  24. /** 音符类型到时值的映射 */
  25. const TYPE_TO_DURATION: Record<string, number> = {
  26. '1024th': 1/256,
  27. '512th': 1/128,
  28. '256th': 1/64,
  29. '128th': 1/32,
  30. '64th': 1/16,
  31. '32nd': 1/8,
  32. '16th': 0.25,
  33. 'eighth': 0.5,
  34. 'quarter': 1.0,
  35. 'half': 2.0,
  36. 'whole': 4.0,
  37. 'breve': 8.0,
  38. 'long': 16.0,
  39. 'maxima': 32.0,
  40. };
  41. /** 升降号数量到调名的映射 */
  42. const FIFTHS_TO_KEY: Record<number, { key: string; mode: string }> = {
  43. '-7': { key: 'Cb', mode: 'major' },
  44. '-6': { key: 'Gb', mode: 'major' },
  45. '-5': { key: 'Db', mode: 'major' },
  46. '-4': { key: 'Ab', mode: 'major' },
  47. '-3': { key: 'Eb', mode: 'major' },
  48. '-2': { key: 'Bb', mode: 'major' },
  49. '-1': { key: 'F', mode: 'major' },
  50. '0': { key: 'C', mode: 'major' },
  51. '1': { key: 'G', mode: 'major' },
  52. '2': { key: 'D', mode: 'major' },
  53. '3': { key: 'A', mode: 'major' },
  54. '4': { key: 'E', mode: 'major' },
  55. '5': { key: 'B', mode: 'major' },
  56. '6': { key: 'F#', mode: 'major' },
  57. '7': { key: 'C#', mode: 'major' },
  58. };
  59. // ==================== 类型定义 ====================
  60. /** OSMD实例类型(简化定义,避免依赖OSMD类型) */
  61. interface OSMDInstance {
  62. GraphicSheet?: {
  63. MeasureList?: any[][];
  64. };
  65. Sheet?: {
  66. Title?: { text?: string };
  67. Subtitle?: { text?: string };
  68. Composer?: { text?: string };
  69. Lyricist?: { text?: string };
  70. Instruments?: any[];
  71. SourceMeasures?: any[];
  72. };
  73. cursor?: {
  74. Iterator?: any;
  75. reset?: () => void;
  76. next?: () => void;
  77. };
  78. }
  79. /** 解析选项 */
  80. export interface ParseOptions {
  81. /** 是否跳过装饰音 */
  82. skipGraceNotes?: boolean;
  83. /** 是否包含多声部 */
  84. includeMultiVoice?: boolean;
  85. /** 默认速度(BPM) */
  86. defaultTempo?: number;
  87. }
  88. /** 解析结果统计 */
  89. export interface ParseStats {
  90. measureCount: number;
  91. noteCount: number;
  92. voiceCount: number;
  93. restCount: number;
  94. graceNoteCount: number;
  95. parseTime: number;
  96. }
  97. // ==================== 主类 ====================
  98. /**
  99. * OSMD数据解析器
  100. */
  101. export class OSMDDataParser {
  102. /** Divisions处理器 */
  103. private divisionsHandler: DivisionsHandler;
  104. /** 解析选项 */
  105. private options: Required<ParseOptions>;
  106. /** 解析统计 */
  107. private stats: ParseStats = {
  108. measureCount: 0,
  109. noteCount: 0,
  110. voiceCount: 0,
  111. restCount: 0,
  112. graceNoteCount: 0,
  113. parseTime: 0,
  114. };
  115. /** 音符ID计数器 */
  116. private noteIdCounter: number = 0;
  117. /**
  118. * 构造函数
  119. * @param options 解析选项
  120. */
  121. constructor(options: ParseOptions = {}) {
  122. this.divisionsHandler = new DivisionsHandler();
  123. this.options = {
  124. skipGraceNotes: options.skipGraceNotes ?? false,
  125. includeMultiVoice: options.includeMultiVoice ?? true,
  126. defaultTempo: options.defaultTempo ?? 120,
  127. };
  128. }
  129. /**
  130. * 从OSMD对象解析简谱数据
  131. *
  132. * @param osmd OpenSheetMusicDisplay实例
  133. * @returns 简谱数据结构
  134. */
  135. parse(osmd: OSMDInstance): JianpuScore {
  136. const startTime = performance.now();
  137. console.log('[OSMDDataParser] 开始解析OSMD数据');
  138. // 验证OSMD对象
  139. if (!osmd) {
  140. throw new Error('[OSMDDataParser] OSMD实例不能为空');
  141. }
  142. // 重置状态
  143. this.resetState();
  144. // 1. 提取元数据
  145. const metadata = this.extractMetadata(osmd);
  146. console.log(`[OSMDDataParser] 元数据: 标题="${metadata.title}", 作曲="${metadata.composer}"`);
  147. // 2. 解析小节
  148. const measures = this.parseMeasures(osmd);
  149. console.log(`[OSMDDataParser] 解析了 ${measures.length} 个小节`);
  150. // 3. 解析音符(填充到小节中)
  151. this.parseNotes(osmd, measures);
  152. console.log(`[OSMDDataParser] 解析了 ${this.stats.noteCount} 个音符`);
  153. // 4. 构建JianpuScore
  154. const score: JianpuScore = {
  155. title: metadata.title,
  156. subtitle: metadata.subtitle,
  157. composer: metadata.composer,
  158. lyricist: metadata.lyricist,
  159. systems: [], // 布局引擎会填充
  160. measures,
  161. tempo: metadata.tempo,
  162. initialTimeSignature: measures[0]?.timeSignature ?? { beats: 4, beatType: 4 },
  163. initialKeySignature: measures[0]?.keySignature ?? { key: 'C', mode: 'major' },
  164. config: { ...DEFAULT_RENDER_CONFIG },
  165. totalMeasures: measures.length,
  166. voiceCount: this.stats.voiceCount,
  167. metadata: {
  168. parseStats: { ...this.stats },
  169. },
  170. };
  171. // 计算总时长
  172. score.duration = this.calculateTotalDuration(measures, metadata.tempo);
  173. this.stats.parseTime = performance.now() - startTime;
  174. console.log(`[OSMDDataParser] 解析完成,耗时 ${this.stats.parseTime.toFixed(2)}ms`);
  175. return score;
  176. }
  177. /**
  178. * 获取解析统计
  179. */
  180. getStats(): ParseStats {
  181. return { ...this.stats };
  182. }
  183. // ==================== 私有方法 ====================
  184. /**
  185. * 重置解析状态
  186. */
  187. private resetState(): void {
  188. this.divisionsHandler.reset();
  189. this.noteIdCounter = 0;
  190. this.stats = {
  191. measureCount: 0,
  192. noteCount: 0,
  193. voiceCount: 0,
  194. restCount: 0,
  195. graceNoteCount: 0,
  196. parseTime: 0,
  197. };
  198. }
  199. /**
  200. * 生成唯一音符ID
  201. */
  202. private generateNoteId(): string {
  203. return `note-${++this.noteIdCounter}`;
  204. }
  205. /**
  206. * 提取曲谱元数据
  207. */
  208. private extractMetadata(osmd: OSMDInstance): {
  209. title: string;
  210. subtitle?: string;
  211. composer?: string;
  212. lyricist?: string;
  213. tempo: number;
  214. } {
  215. const sheet = osmd.Sheet;
  216. let tempo = this.options.defaultTempo;
  217. // 尝试从第一个小节获取速度
  218. if (sheet?.SourceMeasures?.[0]) {
  219. const firstMeasure = sheet.SourceMeasures[0];
  220. if (firstMeasure.tempoInBPM) {
  221. tempo = firstMeasure.tempoInBPM;
  222. } else if (firstMeasure.TempoExpressions?.[0]?.InstantaneousTempo?.tempoInBpm) {
  223. tempo = firstMeasure.TempoExpressions[0].InstantaneousTempo.tempoInBpm;
  224. }
  225. }
  226. return {
  227. title: sheet?.Title?.text ?? 'Untitled',
  228. subtitle: sheet?.Subtitle?.text,
  229. composer: sheet?.Composer?.text,
  230. lyricist: sheet?.Lyricist?.text,
  231. tempo,
  232. };
  233. }
  234. /**
  235. * 解析小节信息
  236. */
  237. private parseMeasures(osmd: OSMDInstance): JianpuMeasure[] {
  238. const measures: JianpuMeasure[] = [];
  239. // 优先使用SourceMeasures(更准确)
  240. const sourceMeasures = osmd.Sheet?.SourceMeasures;
  241. if (sourceMeasures && sourceMeasures.length > 0) {
  242. for (let i = 0; i < sourceMeasures.length; i++) {
  243. const srcMeasure = sourceMeasures[i];
  244. const measure = this.parseSourceMeasure(srcMeasure, i);
  245. measures.push(measure);
  246. }
  247. } else {
  248. // 回退到GraphicSheet.MeasureList
  249. const measureList = osmd.GraphicSheet?.MeasureList;
  250. if (measureList && measureList.length > 0) {
  251. for (let i = 0; i < measureList.length; i++) {
  252. const graphicalMeasures = measureList[i];
  253. if (graphicalMeasures && graphicalMeasures.length > 0) {
  254. const measure = this.parseGraphicalMeasure(graphicalMeasures[0], i);
  255. measures.push(measure);
  256. }
  257. }
  258. }
  259. }
  260. this.stats.measureCount = measures.length;
  261. return measures;
  262. }
  263. /**
  264. * 解析SourceMeasure
  265. */
  266. private parseSourceMeasure(srcMeasure: any, index: number): JianpuMeasure {
  267. // 获取拍号
  268. const timeSig = srcMeasure.ActiveTimeSignature;
  269. const timeSignature = {
  270. beats: timeSig?.numerator ?? timeSig?.Numerator ?? 4,
  271. beatType: timeSig?.denominator ?? timeSig?.Denominator ?? 4,
  272. };
  273. // 获取调号
  274. const keySig = srcMeasure.ActiveKeySignature;
  275. const keySignature = this.parseKeySignature(keySig);
  276. // 获取速度
  277. let tempo: number | undefined;
  278. if (srcMeasure.tempoInBPM) {
  279. tempo = srcMeasure.tempoInBPM;
  280. } else if (srcMeasure.TempoExpressions?.[0]?.InstantaneousTempo?.tempoInBpm) {
  281. tempo = srcMeasure.TempoExpressions[0].InstantaneousTempo.tempoInBpm;
  282. }
  283. // 创建小节
  284. const measure = createDefaultMeasure(index + 1, timeSignature);
  285. measure.keySignature = keySignature;
  286. if (tempo !== undefined) {
  287. measure.tempo = tempo;
  288. }
  289. // 小节线类型
  290. measure.barlineType = this.getBarlineType(srcMeasure);
  291. // 是否显示拍号/调号(第一小节或变化时显示)
  292. measure.showTimeSignature = index === 0;
  293. measure.showKeySignature = index === 0;
  294. return measure;
  295. }
  296. /**
  297. * 解析GraphicalMeasure
  298. */
  299. private parseGraphicalMeasure(graphicalMeasure: any, index: number): JianpuMeasure {
  300. const srcMeasure = graphicalMeasure.parentSourceMeasure;
  301. if (srcMeasure) {
  302. return this.parseSourceMeasure(srcMeasure, index);
  303. }
  304. // 如果没有SourceMeasure,创建默认小节
  305. return createDefaultMeasure(index + 1);
  306. }
  307. /**
  308. * 解析调号
  309. */
  310. private parseKeySignature(keySig: any): {
  311. key: string;
  312. mode: 'major' | 'minor';
  313. alterations?: number;
  314. } {
  315. if (!keySig) {
  316. return { key: 'C', mode: 'major', alterations: 0 };
  317. }
  318. const fifths = keySig.keyTypeOriginal ?? keySig.Key ?? 0;
  319. const mode = keySig.Mode ?? 0; // 0 = major, 1 = minor
  320. const keyInfo = FIFTHS_TO_KEY[fifths.toString()] ?? { key: 'C', mode: 'major' };
  321. return {
  322. key: keyInfo.key,
  323. mode: mode === 1 ? 'minor' : 'major',
  324. alterations: fifths,
  325. };
  326. }
  327. /**
  328. * 获取小节线类型
  329. */
  330. private getBarlineType(srcMeasure: any): JianpuMeasure['barlineType'] {
  331. const instructions = srcMeasure.lastRepetitionInstructions;
  332. if (instructions && instructions.length > 0) {
  333. for (const inst of instructions) {
  334. const type = inst.type;
  335. if (type === 'StartLine' || type === 2) return 'repeat-start';
  336. if (type === 'BackJumpLine' || type === 3) return 'repeat-end';
  337. if (type === 'Ending') return 'double';
  338. }
  339. }
  340. // 检查是否是最后一个小节
  341. if (srcMeasure.endingBarStyleEnum === 'light-heavy' ||
  342. srcMeasure.endingBarStyle === 'light-heavy') {
  343. return 'final';
  344. }
  345. return 'single';
  346. }
  347. /**
  348. * 解析音符信息
  349. */
  350. private parseNotes(osmd: OSMDInstance, measures: JianpuMeasure[]): void {
  351. const cursor = osmd.cursor;
  352. if (!cursor?.Iterator) {
  353. console.warn('[OSMDDataParser] 无法获取cursor.Iterator,尝试从MeasureList解析');
  354. this.parseNotesFromMeasureList(osmd, measures);
  355. return;
  356. }
  357. // 使用cursor遍历
  358. cursor.reset?.();
  359. const iterator = cursor.Iterator;
  360. let maxVoiceIndex = 0;
  361. while (!iterator.EndReached) {
  362. const voiceEntries = iterator.currentVoiceEntries ?? iterator.CurrentVoiceEntries ?? [];
  363. const measureIndex = iterator.currentMeasureIndex ?? 0;
  364. if (measureIndex >= 0 && measureIndex < measures.length) {
  365. const measure = measures[measureIndex];
  366. const timestamp = iterator.currentTimeStamp?.RealValue ??
  367. iterator.currentTimeStamp?.realValue ?? 0;
  368. for (const voiceEntry of voiceEntries) {
  369. const voiceIndex = voiceEntry.ParentVoice?.VoiceId ?? 0;
  370. maxVoiceIndex = Math.max(maxVoiceIndex, voiceIndex);
  371. // 确保声部数组存在
  372. while (measure.voices.length <= voiceIndex) {
  373. measure.voices.push([]);
  374. }
  375. const notes = voiceEntry.Notes ?? voiceEntry.notes ?? [];
  376. for (const note of notes) {
  377. const jianpuNote = this.parseNote(note, measureIndex, voiceIndex, timestamp);
  378. if (jianpuNote) {
  379. measure.voices[voiceIndex].push(jianpuNote);
  380. this.stats.noteCount++;
  381. if (jianpuNote.isRest) this.stats.restCount++;
  382. if (jianpuNote.isGraceNote) this.stats.graceNoteCount++;
  383. }
  384. }
  385. }
  386. }
  387. // 移动到下一个
  388. try {
  389. if (iterator.moveToNextVisibleVoiceEntry) {
  390. iterator.moveToNextVisibleVoiceEntry(this.options.skipGraceNotes);
  391. } else {
  392. cursor.next?.();
  393. }
  394. } catch (e) {
  395. console.warn('[OSMDDataParser] Iterator移动失败:', e);
  396. break;
  397. }
  398. }
  399. this.stats.voiceCount = maxVoiceIndex + 1;
  400. // 同步notes和voices引用
  401. for (const measure of measures) {
  402. measure.notes = measure.voices;
  403. }
  404. }
  405. /**
  406. * 从MeasureList解析音符(备用方案)
  407. */
  408. private parseNotesFromMeasureList(osmd: OSMDInstance, measures: JianpuMeasure[]): void {
  409. const measureList = osmd.GraphicSheet?.MeasureList;
  410. if (!measureList) {
  411. console.warn('[OSMDDataParser] 无法获取MeasureList');
  412. return;
  413. }
  414. let maxVoiceIndex = 0;
  415. for (let measureIdx = 0; measureIdx < measureList.length && measureIdx < measures.length; measureIdx++) {
  416. const graphicalMeasures = measureList[measureIdx];
  417. const measure = measures[measureIdx];
  418. if (!graphicalMeasures) continue;
  419. for (const gMeasure of graphicalMeasures) {
  420. if (!gMeasure?.staffEntries) continue;
  421. for (const staffEntry of gMeasure.staffEntries) {
  422. const timestamp = staffEntry.relInMeasureTimestamp?.RealValue ?? 0;
  423. const voiceEntries = staffEntry.graphicalVoiceEntries ?? [];
  424. for (const gve of voiceEntries) {
  425. const voiceIndex = gve.parentVoiceEntry?.ParentVoice?.VoiceId ?? 0;
  426. maxVoiceIndex = Math.max(maxVoiceIndex, voiceIndex);
  427. // 确保声部数组存在
  428. while (measure.voices.length <= voiceIndex) {
  429. measure.voices.push([]);
  430. }
  431. const notes = gve.notes ?? [];
  432. for (const graphicalNote of notes) {
  433. const srcNote = graphicalNote.sourceNote;
  434. if (!srcNote) continue;
  435. const jianpuNote = this.parseNote(srcNote, measureIdx, voiceIndex, timestamp);
  436. if (jianpuNote) {
  437. // 保存图形化音符的SVG引用
  438. if (graphicalNote.vfnote?.[0]?.attrs?.id) {
  439. jianpuNote.osmdCompatible.svgElement.attrs.id = graphicalNote.vfnote[0].attrs.id;
  440. }
  441. measure.voices[voiceIndex].push(jianpuNote);
  442. this.stats.noteCount++;
  443. if (jianpuNote.isRest) this.stats.restCount++;
  444. if (jianpuNote.isGraceNote) this.stats.graceNoteCount++;
  445. }
  446. }
  447. }
  448. }
  449. }
  450. }
  451. this.stats.voiceCount = maxVoiceIndex + 1;
  452. // 同步notes和voices引用
  453. for (const measure of measures) {
  454. measure.notes = measure.voices;
  455. }
  456. }
  457. /**
  458. * 解析单个音符
  459. */
  460. private parseNote(
  461. note: any,
  462. measureIndex: number,
  463. voiceIndex: number,
  464. timestamp: number
  465. ): JianpuNote | null {
  466. // 跳过装饰音(如果配置了)
  467. if (this.options.skipGraceNotes && note.IsGraceNote) {
  468. return null;
  469. }
  470. const isRest = note.isRestFlag ?? note.IsRest ?? note.isRest ?? false;
  471. const isGraceNote = note.IsGraceNote ?? note.isGraceNote ?? false;
  472. // 获取音高信息
  473. let pitch = 0;
  474. let octave = 0;
  475. let accidental: JianpuNote['accidental'] = undefined;
  476. let halfTone = 0;
  477. let frequency = 0;
  478. if (!isRest && note.pitch) {
  479. pitch = this.getPitchNumber(note);
  480. octave = this.getOctaveOffset(note);
  481. accidental = this.getAccidental(note);
  482. halfTone = note.halfTone ?? this.calculateHalfTone(note);
  483. frequency = note.pitch?.frequency ?? this.halfToneToFrequency(halfTone);
  484. }
  485. // 获取时值
  486. const duration = this.getNoteDuration(note);
  487. const dots = this.getDotCount(note);
  488. // 创建音符
  489. const jianpuNote = createDefaultNote({
  490. pitch,
  491. octave,
  492. duration,
  493. accidental,
  494. dots,
  495. timestamp,
  496. voiceIndex,
  497. measureIndex,
  498. isRest,
  499. isGraceNote,
  500. isStaccato: note.isStaccato ?? false,
  501. });
  502. // 设置ID
  503. jianpuNote.id = this.generateNoteId();
  504. // 设置OSMD兼容数据
  505. jianpuNote.osmdCompatible = {
  506. noteElement: note,
  507. svgElement: {
  508. attrs: { id: `vf-${jianpuNote.id}` },
  509. modifiers: note.modifiers,
  510. },
  511. halfTone,
  512. frequency,
  513. realKey: pitch > 0 ? pitch + octave * 7 : 0,
  514. };
  515. return jianpuNote;
  516. }
  517. /**
  518. * 获取简谱音高(1-7)
  519. */
  520. private getPitchNumber(note: any): number {
  521. // 方式1:从pitch.step获取
  522. const step = note.pitch?.step ?? note.pitch?.Step;
  523. if (step !== undefined) {
  524. const pitch = STEP_TO_JIANPU[step.toString().toUpperCase()];
  525. if (pitch !== undefined) {
  526. return pitch;
  527. }
  528. }
  529. // 方式2:从halfTone计算
  530. const halfTone = note.halfTone ?? note.HalfTone;
  531. if (halfTone !== undefined) {
  532. // halfTone % 12 得到音级,映射到1-7
  533. const noteInOctave = ((halfTone % 12) + 12) % 12;
  534. 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
  535. return halfToneToJianpu[noteInOctave];
  536. }
  537. console.warn('[OSMDDataParser] 无法获取音高信息,返回默认值1');
  538. return 1;
  539. }
  540. /**
  541. * 获取八度偏移
  542. */
  543. private getOctaveOffset(note: any): number {
  544. // 方式1:从pitch.octave获取
  545. const octave = note.pitch?.octave ?? note.pitch?.Octave;
  546. if (octave !== undefined) {
  547. return octave - BASE_OCTAVE;
  548. }
  549. // 方式2:从halfTone计算
  550. const halfTone = note.halfTone ?? note.HalfTone;
  551. if (halfTone !== undefined) {
  552. const octaveFromHalfTone = Math.floor(halfTone / 12) - 1; // MIDI标准
  553. return octaveFromHalfTone - BASE_OCTAVE;
  554. }
  555. return 0;
  556. }
  557. /**
  558. * 获取升降号
  559. */
  560. private getAccidental(note: any): JianpuNote['accidental'] | undefined {
  561. // 方式1:从pitch.alter获取
  562. const alter = note.pitch?.alter ?? note.pitch?.Alter;
  563. if (alter === 1 || alter === 2) return 'sharp';
  564. if (alter === -1 || alter === -2) return 'flat';
  565. if (alter === 0) return 'natural';
  566. // 方式2:从accidental属性获取
  567. const accidental = note.accidental ?? note.Accidental;
  568. if (accidental) {
  569. const accStr = accidental.toString().toLowerCase();
  570. if (accStr.includes('sharp')) return 'sharp';
  571. if (accStr.includes('flat')) return 'flat';
  572. if (accStr.includes('natural')) return 'natural';
  573. }
  574. return undefined;
  575. }
  576. /**
  577. * 获取音符时值
  578. */
  579. private getNoteDuration(note: any): number {
  580. // 方式1:从length.realValue获取(最准确)
  581. const realValue = note.length?.RealValue ?? note.length?.realValue;
  582. if (realValue !== undefined && realValue > 0) {
  583. return realValue;
  584. }
  585. // 方式2:从noteType获取
  586. const noteType = note.noteTypeXml ?? note.TypeLength?.toString() ?? note.type;
  587. if (noteType) {
  588. const baseDuration = TYPE_TO_DURATION[noteType.toLowerCase()];
  589. if (baseDuration !== undefined) {
  590. // 应用附点
  591. const dots = this.getDotCount(note);
  592. return this.divisionsHandler.applyDots(baseDuration, dots);
  593. }
  594. }
  595. // 方式3:从duration和divisions计算
  596. const duration = note.duration ?? note.Duration;
  597. if (duration !== undefined) {
  598. const sourceMeasure = note.sourceMeasure ?? note.SourceMeasure;
  599. const divisions = sourceMeasure?.divisions ?? 256;
  600. this.divisionsHandler.setDivisions(divisions);
  601. return this.divisionsHandler.toRealValue(duration);
  602. }
  603. console.warn('[OSMDDataParser] 无法获取音符时值,返回默认值1.0');
  604. return 1.0;
  605. }
  606. /**
  607. * 获取附点数量
  608. */
  609. private getDotCount(note: any): number {
  610. // 方式1:直接获取dots数量
  611. const dots = note.dots ?? note.Dots ?? note.DotsXml;
  612. if (typeof dots === 'number') {
  613. return dots;
  614. }
  615. // 方式2:从数组长度获取
  616. if (Array.isArray(dots)) {
  617. return dots.length;
  618. }
  619. return 0;
  620. }
  621. /**
  622. * 计算半音值(从音高信息)
  623. */
  624. private calculateHalfTone(note: any): number {
  625. const step = note.pitch?.step ?? note.pitch?.Step;
  626. const octave = note.pitch?.octave ?? note.pitch?.Octave ?? 4;
  627. const alter = note.pitch?.alter ?? note.pitch?.Alter ?? 0;
  628. if (!step) return 60; // 默认C4
  629. const stepToSemitone: Record<string, number> = {
  630. 'C': 0, 'D': 2, 'E': 4, 'F': 5, 'G': 7, 'A': 9, 'B': 11
  631. };
  632. const semitone = stepToSemitone[step.toString().toUpperCase()] ?? 0;
  633. return (octave + 1) * 12 + semitone + alter;
  634. }
  635. /**
  636. * 半音值转频率
  637. */
  638. private halfToneToFrequency(halfTone: number): number {
  639. // A4 = 440Hz, MIDI note 69
  640. return 440 * Math.pow(2, (halfTone - 69) / 12);
  641. }
  642. /**
  643. * 计算总时长
  644. */
  645. private calculateTotalDuration(measures: JianpuMeasure[], tempo: number): number {
  646. let totalBeats = 0;
  647. for (const measure of measures) {
  648. const { beats, beatType } = measure.timeSignature;
  649. // 将节拍数转换为四分音符数量
  650. const quarterNotesInMeasure = beats * (4 / beatType);
  651. totalBeats += quarterNotesInMeasure;
  652. }
  653. // 四分音符时长 = 60 / BPM
  654. const quarterNoteDuration = 60 / tempo;
  655. return totalBeats * quarterNoteDuration;
  656. }
  657. }
  658. // ==================== 工厂函数 ====================
  659. /**
  660. * 创建OSMD数据解析器
  661. */
  662. export function createOSMDDataParser(options?: ParseOptions): OSMDDataParser {
  663. return new OSMDDataParser(options);
  664. }
  665. // ==================== 工具函数(导出供其他模块使用) ====================
  666. /**
  667. * 音名转简谱数字
  668. */
  669. export function stepToJianpu(step: string): number {
  670. const pitch = STEP_TO_JIANPU[step.toUpperCase()];
  671. if (pitch === undefined) {
  672. throw new Error(`无效的音名: ${step}`);
  673. }
  674. return pitch;
  675. }
  676. /**
  677. * 八度转偏移
  678. */
  679. export function octaveToOffset(octave: number): number {
  680. return octave - BASE_OCTAVE;
  681. }
  682. /**
  683. * 升降号值转类型
  684. */
  685. export function alterToAccidental(alter: number | null): 'sharp' | 'flat' | 'natural' | null {
  686. if (alter === null || alter === undefined) return null;
  687. if (alter > 0) return 'sharp';
  688. if (alter < 0) return 'flat';
  689. return 'natural';
  690. }
  691. /**
  692. * 音符类型转时值
  693. */
  694. export function noteTypeToRealValue(noteType: string): number {
  695. const duration = TYPE_TO_DURATION[noteType.toLowerCase()];
  696. if (duration === undefined) {
  697. console.warn(`未知的音符类型: ${noteType}`);
  698. return 1.0;
  699. }
  700. return duration;
  701. }