integration.test.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587
  1. /**
  2. * 解析器集成测试
  3. *
  4. * 使用真实的MusicXML文件测试完整的解析流程:
  5. * 1. MusicXML解析 → 模拟OSMD对象
  6. * 2. OSMDDataParser解析 → JianpuScore
  7. * 3. TimeCalculator计算 → 时间数据
  8. *
  9. * 测试文件:
  10. * - basic.xml: 基础简谱
  11. * - mixed-durations.xml: 混合时值
  12. */
  13. import { describe, it, expect, beforeAll, vi } from 'vitest';
  14. import { readFileSync } from 'fs';
  15. import { join } from 'path';
  16. import { OSMDDataParser } from '../core/parser/OSMDDataParser';
  17. import { TimeCalculator } from '../core/parser/TimeCalculator';
  18. import { DivisionsHandler } from '../core/parser/DivisionsHandler';
  19. // ==================== MusicXML 解析器 ====================
  20. /**
  21. * 简单的MusicXML解析器
  22. * 将MusicXML转换为模拟的OSMD对象结构
  23. */
  24. class SimpleMusicXMLParser {
  25. private divisionsHandler = new DivisionsHandler();
  26. /**
  27. * 解析MusicXML字符串
  28. */
  29. parse(xmlString: string): any {
  30. const parser = new DOMParser();
  31. const doc = parser.parseFromString(xmlString, 'text/xml');
  32. // 检查解析错误
  33. const parseError = doc.querySelector('parsererror');
  34. if (parseError) {
  35. throw new Error(`XML解析错误: ${parseError.textContent}`);
  36. }
  37. return this.buildOSMDObject(doc);
  38. }
  39. private buildOSMDObject(doc: Document): any {
  40. const title = doc.querySelector('work-title')?.textContent ?? 'Untitled';
  41. const composer = doc.querySelector('creator[type="composer"]')?.textContent ?? '';
  42. // 解析小节
  43. const measureElements = doc.querySelectorAll('measure');
  44. const sourceMeasures: any[] = [];
  45. const notes: any[] = [];
  46. let currentDivisions = 256;
  47. let currentTempo = 120;
  48. let currentTimeSignature = { numerator: 4, denominator: 4 };
  49. let currentKeySignature = { keyTypeOriginal: 0, Mode: 0 };
  50. measureElements.forEach((measureEl, measureIndex) => {
  51. // 解析attributes
  52. const attributes = measureEl.querySelector('attributes');
  53. if (attributes) {
  54. const divisions = attributes.querySelector('divisions');
  55. if (divisions) {
  56. currentDivisions = parseInt(divisions.textContent ?? '256');
  57. this.divisionsHandler.setDivisions(currentDivisions);
  58. }
  59. const time = attributes.querySelector('time');
  60. if (time) {
  61. currentTimeSignature = {
  62. numerator: parseInt(time.querySelector('beats')?.textContent ?? '4'),
  63. denominator: parseInt(time.querySelector('beat-type')?.textContent ?? '4'),
  64. };
  65. }
  66. const key = attributes.querySelector('key');
  67. if (key) {
  68. currentKeySignature = {
  69. keyTypeOriginal: parseInt(key.querySelector('fifths')?.textContent ?? '0'),
  70. Mode: key.querySelector('mode')?.textContent === 'minor' ? 1 : 0,
  71. };
  72. }
  73. }
  74. // 解析速度
  75. const sound = measureEl.querySelector('sound[tempo]');
  76. if (sound) {
  77. currentTempo = parseInt(sound.getAttribute('tempo') ?? '120');
  78. }
  79. const metronome = measureEl.querySelector('metronome per-minute');
  80. if (metronome) {
  81. currentTempo = parseInt(metronome.textContent ?? '120');
  82. }
  83. // 创建SourceMeasure
  84. const sourceMeasure: any = {
  85. MeasureNumberXML: measureIndex + 1,
  86. measureListIndex: measureIndex,
  87. tempoInBPM: currentTempo,
  88. ActiveTimeSignature: currentTimeSignature,
  89. ActiveKeySignature: currentKeySignature,
  90. Duration: { RealValue: currentTimeSignature.numerator / currentTimeSignature.denominator },
  91. lastRepetitionInstructions: [],
  92. verticalMeasureList: [],
  93. };
  94. sourceMeasures.push(sourceMeasure);
  95. // 解析音符
  96. const noteElements = measureEl.querySelectorAll('note');
  97. let timestamp = 0;
  98. noteElements.forEach((noteEl) => {
  99. const note = this.parseNote(noteEl, measureIndex, sourceMeasure, timestamp);
  100. if (note) {
  101. notes.push({
  102. note,
  103. measureIndex,
  104. timestamp,
  105. });
  106. // 更新时间戳(除非是和弦音符)
  107. if (!noteEl.querySelector('chord')) {
  108. timestamp += note.length.realValue;
  109. }
  110. }
  111. });
  112. });
  113. // 构建cursor迭代器
  114. let noteIndex = 0;
  115. const iterator = {
  116. EndReached: notes.length === 0,
  117. currentVoiceEntries: notes.length > 0 ? [{
  118. Notes: [notes[0]?.note],
  119. notes: [notes[0]?.note],
  120. ParentVoice: { VoiceId: 0 },
  121. }] : [],
  122. CurrentVoiceEntries: [],
  123. currentMeasureIndex: 0,
  124. currentTimeStamp: { RealValue: 0, realValue: 0 },
  125. moveToNextVisibleVoiceEntry: () => {
  126. noteIndex++;
  127. if (noteIndex >= notes.length) {
  128. iterator.EndReached = true;
  129. } else {
  130. const { note, measureIndex, timestamp } = notes[noteIndex];
  131. iterator.currentVoiceEntries = [{
  132. Notes: [note],
  133. notes: [note],
  134. ParentVoice: { VoiceId: 0 },
  135. }];
  136. iterator.CurrentVoiceEntries = iterator.currentVoiceEntries;
  137. iterator.currentMeasureIndex = measureIndex;
  138. iterator.currentTimeStamp = { RealValue: timestamp, realValue: timestamp };
  139. }
  140. },
  141. };
  142. iterator.CurrentVoiceEntries = iterator.currentVoiceEntries;
  143. return {
  144. Sheet: {
  145. Title: { text: title },
  146. Composer: { text: composer },
  147. SourceMeasures: sourceMeasures,
  148. },
  149. GraphicSheet: {
  150. MeasureList: sourceMeasures.map(m => [{ parentSourceMeasure: m }]),
  151. },
  152. cursor: {
  153. Iterator: iterator,
  154. reset: () => {
  155. noteIndex = 0;
  156. iterator.EndReached = notes.length === 0;
  157. if (notes.length > 0) {
  158. const { note, measureIndex, timestamp } = notes[0];
  159. iterator.currentVoiceEntries = [{
  160. Notes: [note],
  161. notes: [note],
  162. ParentVoice: { VoiceId: 0 },
  163. }];
  164. iterator.CurrentVoiceEntries = iterator.currentVoiceEntries;
  165. iterator.currentMeasureIndex = measureIndex;
  166. iterator.currentTimeStamp = { RealValue: timestamp, realValue: timestamp };
  167. }
  168. },
  169. next: () => iterator.moveToNextVisibleVoiceEntry(),
  170. },
  171. };
  172. }
  173. private parseNote(noteEl: Element, measureIndex: number, sourceMeasure: any, timestamp: number): any {
  174. const isRest = noteEl.querySelector('rest') !== null;
  175. const durationEl = noteEl.querySelector('duration');
  176. const duration = parseInt(durationEl?.textContent ?? '256');
  177. const realValue = this.divisionsHandler.toRealValue(duration);
  178. // 解析音高
  179. let pitch: any = null;
  180. let halfTone = 60; // 默认C4
  181. if (!isRest) {
  182. const pitchEl = noteEl.querySelector('pitch');
  183. if (pitchEl) {
  184. const step = pitchEl.querySelector('step')?.textContent ?? 'C';
  185. const octave = parseInt(pitchEl.querySelector('octave')?.textContent ?? '4');
  186. const alter = parseInt(pitchEl.querySelector('alter')?.textContent ?? '0');
  187. pitch = {
  188. step,
  189. octave,
  190. alter,
  191. frequency: this.calculateFrequency(step, octave, alter),
  192. };
  193. halfTone = this.calculateHalfTone(step, octave, alter);
  194. }
  195. }
  196. // 解析附点
  197. const dots = noteEl.querySelectorAll('dot').length;
  198. // 解析类型
  199. const typeEl = noteEl.querySelector('type');
  200. const noteType = typeEl?.textContent ?? 'quarter';
  201. return {
  202. pitch,
  203. halfTone,
  204. length: { realValue, RealValue: realValue },
  205. isRestFlag: isRest,
  206. IsRest: isRest,
  207. IsGraceNote: noteEl.querySelector('grace') !== null,
  208. IsChordNote: noteEl.querySelector('chord') !== null,
  209. dots,
  210. DotsXml: dots,
  211. noteTypeXml: noteType,
  212. sourceMeasure,
  213. duration,
  214. };
  215. }
  216. private calculateHalfTone(step: string, octave: number, alter: number): number {
  217. const stepToSemitone: Record<string, number> = {
  218. 'C': 0, 'D': 2, 'E': 4, 'F': 5, 'G': 7, 'A': 9, 'B': 11
  219. };
  220. return (octave + 1) * 12 + (stepToSemitone[step] ?? 0) + alter;
  221. }
  222. private calculateFrequency(step: string, octave: number, alter: number): number {
  223. const halfTone = this.calculateHalfTone(step, octave, alter);
  224. return 440 * Math.pow(2, (halfTone - 69) / 12);
  225. }
  226. }
  227. // ==================== 测试工具函数 ====================
  228. /**
  229. * 加载测试XML文件
  230. */
  231. function loadTestXML(filename: string): string {
  232. const filePath = join(__dirname, 'fixtures', filename);
  233. return readFileSync(filePath, 'utf-8');
  234. }
  235. /**
  236. * 解析MusicXML为模拟OSMD对象
  237. */
  238. function parseXMLToOSMD(xmlString: string): any {
  239. const parser = new SimpleMusicXMLParser();
  240. return parser.parse(xmlString);
  241. }
  242. // ==================== 测试用例 ====================
  243. describe('解析器集成测试', () => {
  244. let xmlParser: SimpleMusicXMLParser;
  245. let osmdParser: OSMDDataParser;
  246. let timeCalculator: TimeCalculator;
  247. beforeAll(() => {
  248. xmlParser = new SimpleMusicXMLParser();
  249. osmdParser = new OSMDDataParser();
  250. timeCalculator = new TimeCalculator();
  251. vi.spyOn(console, 'log').mockImplementation(() => {});
  252. vi.spyOn(console, 'warn').mockImplementation(() => {});
  253. });
  254. // ==================== basic.xml 测试 ====================
  255. describe('basic.xml - 基础简谱', () => {
  256. let xmlContent: string;
  257. let osmdObject: any;
  258. beforeAll(() => {
  259. xmlContent = loadTestXML('basic.xml');
  260. osmdObject = parseXMLToOSMD(xmlContent);
  261. });
  262. it('应该正确解析XML文件', () => {
  263. expect(osmdObject).toBeDefined();
  264. expect(osmdObject.Sheet).toBeDefined();
  265. expect(osmdObject.cursor).toBeDefined();
  266. });
  267. it('应该正确解析标题和作曲家', () => {
  268. expect(osmdObject.Sheet.Title.text).toBe('基础简谱测试');
  269. expect(osmdObject.Sheet.Composer.text).toBe('测试');
  270. });
  271. it('应该正确解析4个小节', () => {
  272. expect(osmdObject.Sheet.SourceMeasures.length).toBe(4);
  273. });
  274. it('应该正确解析拍号', () => {
  275. const firstMeasure = osmdObject.Sheet.SourceMeasures[0];
  276. expect(firstMeasure.ActiveTimeSignature.numerator).toBe(4);
  277. expect(firstMeasure.ActiveTimeSignature.denominator).toBe(4);
  278. });
  279. it('应该正确解析速度', () => {
  280. const firstMeasure = osmdObject.Sheet.SourceMeasures[0];
  281. expect(firstMeasure.tempoInBPM).toBe(120);
  282. });
  283. describe('OSMDDataParser解析', () => {
  284. let score: any;
  285. beforeAll(() => {
  286. osmdParser = new OSMDDataParser();
  287. score = osmdParser.parse(osmdObject);
  288. });
  289. it('应该返回JianpuScore对象', () => {
  290. expect(score).toBeDefined();
  291. expect(score.measures).toBeDefined();
  292. expect(score.tempo).toBeDefined();
  293. });
  294. it('应该解析出4个小节', () => {
  295. expect(score.measures.length).toBe(4);
  296. });
  297. it('应该解析出正确的音符数量', () => {
  298. const stats = osmdParser.getStats();
  299. // basic.xml: 4小节,每小节4个音符,共16个(包含休止符)
  300. expect(stats.noteCount).toBeGreaterThanOrEqual(14);
  301. });
  302. it('第1小节应该包含do re mi fa', () => {
  303. const notes = score.measures[0].voices[0];
  304. expect(notes.length).toBeGreaterThanOrEqual(4);
  305. // 验证音高
  306. const pitches = notes.slice(0, 4).map((n: any) => n.pitch);
  307. expect(pitches).toContain(1); // do
  308. expect(pitches).toContain(2); // re
  309. expect(pitches).toContain(3); // mi
  310. expect(pitches).toContain(4); // fa
  311. });
  312. it('第3小节应该包含休止符', () => {
  313. const notes = score.measures[2].voices[0];
  314. const hasRest = notes.some((n: any) => n.isRest);
  315. expect(hasRest).toBe(true);
  316. });
  317. it('第4小节应该包含不同八度的音符', () => {
  318. const notes = score.measures[3].voices[0];
  319. const octaves = notes.map((n: any) => n.octave);
  320. // 应该有低音(-1)、中音(0)、高音(1)
  321. expect(octaves).toContain(-1); // G3 -> octave -1
  322. expect(octaves).toContain(0); // C4 -> octave 0
  323. expect(octaves).toContain(1); // E5, G5 -> octave 1
  324. });
  325. });
  326. describe('TimeCalculator计算', () => {
  327. let score: any;
  328. beforeAll(() => {
  329. osmdParser = new OSMDDataParser();
  330. score = osmdParser.parse(osmdObject);
  331. timeCalculator.calculateTimes(score);
  332. });
  333. it('应该为所有音符计算时间', () => {
  334. for (const measure of score.measures) {
  335. for (const voice of measure.voices) {
  336. for (const note of voice) {
  337. expect(note.startTime).toBeDefined();
  338. expect(note.endTime).toBeDefined();
  339. expect(note.endTime).toBeGreaterThan(note.startTime);
  340. }
  341. }
  342. }
  343. });
  344. it('BPM=120时四分音符应该是0.5秒', () => {
  345. const firstNote = score.measures[0].voices[0][0];
  346. const duration = firstNote.endTime - firstNote.startTime;
  347. expect(duration).toBeCloseTo(0.5, 2);
  348. });
  349. it('总时长应该约为8秒(4小节×4拍×0.5秒)', () => {
  350. const result = timeCalculator.getResult();
  351. expect(result.totalDuration).toBeCloseTo(8.0, 1);
  352. });
  353. });
  354. });
  355. // ==================== mixed-durations.xml 测试 ====================
  356. describe('mixed-durations.xml - 混合时值', () => {
  357. let xmlContent: string;
  358. let osmdObject: any;
  359. beforeAll(() => {
  360. xmlContent = loadTestXML('mixed-durations.xml');
  361. osmdObject = parseXMLToOSMD(xmlContent);
  362. });
  363. it('应该正确解析XML文件', () => {
  364. expect(osmdObject).toBeDefined();
  365. expect(osmdObject.Sheet.SourceMeasures.length).toBe(6);
  366. });
  367. describe('OSMDDataParser解析', () => {
  368. let score: any;
  369. beforeAll(() => {
  370. osmdParser = new OSMDDataParser();
  371. score = osmdParser.parse(osmdObject);
  372. });
  373. it('应该解析出6个小节', () => {
  374. expect(score.measures.length).toBe(6);
  375. });
  376. it('第1小节应该包含8个八分音符', () => {
  377. const notes = score.measures[0].voices[0];
  378. expect(notes.length).toBe(8);
  379. // 验证时值都是0.5(八分音符)
  380. notes.forEach((note: any) => {
  381. expect(note.duration).toBeCloseTo(0.5, 2);
  382. });
  383. });
  384. it('第2小节应该包含2个二分音符', () => {
  385. const notes = score.measures[1].voices[0];
  386. expect(notes.length).toBe(2);
  387. notes.forEach((note: any) => {
  388. expect(note.duration).toBeCloseTo(2.0, 2);
  389. });
  390. });
  391. it('第3小节应该包含1个全音符', () => {
  392. const notes = score.measures[2].voices[0];
  393. expect(notes.length).toBe(1);
  394. expect(notes[0].duration).toBeCloseTo(4.0, 2);
  395. });
  396. it('第4小节应该包含附点音符', () => {
  397. const notes = score.measures[3].voices[0];
  398. // 查找附点四分音符(时值1.5)
  399. const dottedQuarter = notes.find((n: any) => Math.abs(n.duration - 1.5) < 0.1);
  400. expect(dottedQuarter).toBeDefined();
  401. expect(dottedQuarter.dots).toBe(1);
  402. });
  403. it('第6小节应该包含十六分音符', () => {
  404. const notes = score.measures[5].voices[0];
  405. // 查找十六分音符(时值0.25)
  406. const sixteenthNotes = notes.filter((n: any) => Math.abs(n.duration - 0.25) < 0.1);
  407. expect(sixteenthNotes.length).toBe(4);
  408. });
  409. });
  410. describe('TimeCalculator计算', () => {
  411. let score: any;
  412. beforeAll(() => {
  413. osmdParser = new OSMDDataParser();
  414. score = osmdParser.parse(osmdObject);
  415. timeCalculator.calculateTimes(score);
  416. });
  417. it('八分音符时长应该是0.3秒(BPM=100)', () => {
  418. const eighthNote = score.measures[0].voices[0][0];
  419. const duration = eighthNote.endTime - eighthNote.startTime;
  420. // BPM=100, 四分音符=0.6秒, 八分音符=0.3秒
  421. expect(duration).toBeCloseTo(0.3, 2);
  422. });
  423. it('全音符时长应该是2.4秒(BPM=100)', () => {
  424. const wholeNote = score.measures[2].voices[0][0];
  425. const duration = wholeNote.endTime - wholeNote.startTime;
  426. // BPM=100, 四分音符=0.6秒, 全音符=2.4秒
  427. expect(duration).toBeCloseTo(2.4, 2);
  428. });
  429. it('总时长应该约为14.4秒(6小节×4拍×0.6秒)', () => {
  430. const result = timeCalculator.getResult();
  431. expect(result.totalDuration).toBeCloseTo(14.4, 1);
  432. });
  433. });
  434. });
  435. // ==================== 性能测试 ====================
  436. describe('性能测试', () => {
  437. it('解析basic.xml应该在100ms内完成', () => {
  438. const xmlContent = loadTestXML('basic.xml');
  439. const startTime = performance.now();
  440. const osmdObject = parseXMLToOSMD(xmlContent);
  441. const parser = new OSMDDataParser();
  442. const score = parser.parse(osmdObject);
  443. const calc = new TimeCalculator();
  444. calc.calculateTimes(score);
  445. const endTime = performance.now();
  446. expect(endTime - startTime).toBeLessThan(100);
  447. });
  448. it('解析mixed-durations.xml应该在100ms内完成', () => {
  449. const xmlContent = loadTestXML('mixed-durations.xml');
  450. const startTime = performance.now();
  451. const osmdObject = parseXMLToOSMD(xmlContent);
  452. const parser = new OSMDDataParser();
  453. const score = parser.parse(osmdObject);
  454. const calc = new TimeCalculator();
  455. calc.calculateTimes(score);
  456. const endTime = performance.now();
  457. expect(endTime - startTime).toBeLessThan(100);
  458. });
  459. });
  460. // ==================== 边界情况测试 ====================
  461. describe('边界情况', () => {
  462. it('空XML应该抛出错误', () => {
  463. expect(() => parseXMLToOSMD('')).toThrow();
  464. });
  465. it('无效XML应该抛出错误', () => {
  466. expect(() => parseXMLToOSMD('<invalid>')).toThrow();
  467. });
  468. });
  469. });
  470. // ==================== DivisionsHandler 集成测试 ====================
  471. describe('DivisionsHandler 集成测试', () => {
  472. it('应该正确处理basic.xml的divisions=256', () => {
  473. const handler = new DivisionsHandler();
  474. handler.setDivisions(256);
  475. // 四分音符 duration=256
  476. expect(handler.toRealValue(256)).toBe(1.0);
  477. // 八分音符 duration=128
  478. expect(handler.toRealValue(128)).toBe(0.5);
  479. // 二分音符 duration=512
  480. expect(handler.toRealValue(512)).toBe(2.0);
  481. // 全音符 duration=1024
  482. expect(handler.toRealValue(1024)).toBe(4.0);
  483. // 附点四分音符 duration=384
  484. expect(handler.toRealValue(384)).toBe(1.5);
  485. // 十六分音符 duration=64
  486. expect(handler.toRealValue(64)).toBe(0.25);
  487. });
  488. });