| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>简谱渲染引擎 - 在线测试</title>
- <style>
- :root {
- --primary: #4f46e5;
- --primary-light: #818cf8;
- --success: #10b981;
- --warning: #f59e0b;
- --error: #ef4444;
- --bg: #f8fafc;
- --bg-card: #ffffff;
- --text: #1e293b;
- --border: #e2e8f0;
- }
-
- * { box-sizing: border-box; margin: 0; padding: 0; }
-
- body {
- font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
- background: var(--bg);
- color: var(--text);
- min-height: 100vh;
- padding: 20px;
- }
-
- .container {
- max-width: 1400px;
- margin: 0 auto;
- }
-
- header {
- text-align: center;
- padding: 24px;
- background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
- border-radius: 12px;
- margin-bottom: 20px;
- color: white;
- }
-
- header h1 {
- font-size: 1.8rem;
- margin-bottom: 8px;
- }
-
- .control-panel {
- background: var(--bg-card);
- border-radius: 12px;
- padding: 20px;
- margin-bottom: 20px;
- box-shadow: 0 2px 4px rgba(0,0,0,0.05);
- border: 1px solid var(--border);
- }
-
- .control-panel h3 {
- margin-bottom: 16px;
- font-size: 1rem;
- color: var(--primary);
- }
-
- .control-group {
- display: flex;
- flex-wrap: wrap;
- gap: 16px;
- align-items: flex-end;
- }
-
- .control-item {
- display: flex;
- flex-direction: column;
- gap: 4px;
- }
-
- .control-item label {
- font-size: 0.85rem;
- color: #64748b;
- }
-
- input, select, button {
- padding: 10px 14px;
- border-radius: 8px;
- border: 1px solid var(--border);
- background: white;
- color: var(--text);
- font-size: 0.9rem;
- }
-
- input:focus, select:focus {
- outline: none;
- border-color: var(--primary);
- box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
- }
-
- button {
- background: var(--primary);
- color: white;
- border: none;
- cursor: pointer;
- font-weight: 500;
- transition: all 0.2s;
- }
-
- button:hover {
- background: var(--primary-light);
- transform: translateY(-1px);
- }
-
- button:active {
- transform: translateY(0);
- }
-
- button.success {
- background: var(--success);
- }
-
- button.warning {
- background: var(--warning);
- }
-
- #xml-url {
- flex: 1;
- min-width: 300px;
- }
-
- .render-panel {
- background: var(--bg-card);
- border-radius: 12px;
- padding: 30px;
- min-height: 400px;
- margin-bottom: 20px;
- box-shadow: 0 2px 4px rgba(0,0,0,0.05);
- border: 1px solid var(--border);
- }
-
- #score-container {
- width: 100%;
- overflow-x: auto;
- min-height: 300px;
- }
-
- #score-container svg {
- display: block;
- margin: 0 auto;
- }
-
- .stats-panel {
- background: var(--bg-card);
- border-radius: 12px;
- padding: 20px;
- margin-bottom: 20px;
- box-shadow: 0 2px 4px rgba(0,0,0,0.05);
- border: 1px solid var(--border);
- }
-
- .stats-panel h3 {
- font-size: 1rem;
- margin-bottom: 12px;
- color: var(--primary);
- }
-
- .stats-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
- gap: 12px;
- }
-
- .stat-item {
- background: var(--bg);
- border-radius: 8px;
- padding: 12px;
- text-align: center;
- }
-
- .stat-value {
- font-size: 1.3rem;
- font-weight: 600;
- color: var(--primary);
- }
-
- .stat-label {
- font-size: 0.75rem;
- color: #64748b;
- margin-top: 4px;
- }
-
- .log-panel {
- background: var(--bg-card);
- border-radius: 12px;
- padding: 20px;
- box-shadow: 0 2px 4px rgba(0,0,0,0.05);
- border: 1px solid var(--border);
- }
-
- .log-panel h3 {
- font-size: 1rem;
- margin-bottom: 12px;
- color: var(--primary);
- }
-
- #log-output {
- background: #0f172a;
- border-radius: 8px;
- padding: 16px;
- font-family: 'Consolas', 'Monaco', monospace;
- font-size: 0.85rem;
- max-height: 200px;
- overflow-y: auto;
- color: #10b981;
- }
-
- .log-entry {
- margin-bottom: 4px;
- padding: 2px 0;
- }
-
- .log-entry.error { color: var(--error); }
- .log-entry.warn { color: var(--warning); }
- .log-entry.success { color: var(--success); }
- .log-entry.info { color: #60a5fa; }
-
- .placeholder {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- height: 300px;
- color: #94a3b8;
- }
-
- .placeholder svg {
- width: 60px;
- height: 60px;
- margin-bottom: 16px;
- }
-
- .loading {
- display: inline-block;
- width: 20px;
- height: 20px;
- border: 2px solid #f3f3f3;
- border-top: 2px solid var(--primary);
- border-radius: 50%;
- animation: spin 1s linear infinite;
- margin-right: 8px;
- }
-
- @keyframes spin {
- 0% { transform: rotate(0deg); }
- 100% { transform: rotate(360deg); }
- }
-
- .compare-panel {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 20px;
- }
-
- .compare-panel .panel {
- background: var(--bg-card);
- border-radius: 12px;
- padding: 20px;
- box-shadow: 0 2px 4px rgba(0,0,0,0.05);
- border: 1px solid var(--border);
- }
-
- .compare-panel .panel h4 {
- margin-bottom: 12px;
- color: var(--primary);
- }
-
- /* 全屏样式 */
- .render-panel {
- position: relative;
- }
-
- .render-panel.fullscreen {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- z-index: 9999;
- margin: 0;
- border-radius: 0;
- padding: 20px;
- display: flex;
- flex-direction: column;
- }
-
- .render-panel.fullscreen #score-container {
- flex: 1;
- min-height: auto;
- height: 100%;
- }
-
- .fullscreen-btn {
- position: absolute;
- top: 10px;
- right: 10px;
- z-index: 10;
- padding: 8px 16px !important;
- font-size: 0.85rem !important;
- display: flex;
- align-items: center;
- gap: 6px;
- }
-
- .fullscreen-btn svg {
- width: 16px;
- height: 16px;
- }
-
- .render-panel.fullscreen .fullscreen-btn {
- position: fixed;
- top: 20px;
- right: 20px;
- }
-
- /* 全屏时的缩放控制 */
- .zoom-controls {
- position: absolute;
- top: 10px;
- left: 10px;
- z-index: 10;
- display: flex;
- gap: 8px;
- align-items: center;
- background: rgba(255,255,255,0.95);
- padding: 6px 12px;
- border-radius: 8px;
- box-shadow: 0 2px 8px rgba(0,0,0,0.1);
- }
-
- .render-panel.fullscreen .zoom-controls {
- position: fixed;
- top: 20px;
- left: 20px;
- }
-
- .zoom-controls button {
- padding: 6px 12px !important;
- font-size: 0.8rem !important;
- min-width: 36px;
- }
-
- .zoom-controls span {
- font-size: 0.85rem;
- color: var(--text);
- min-width: 50px;
- text-align: center;
- }
- </style>
- </head>
- <body>
- <div class="container">
- <header>
- <h1>🎵 简谱渲染引擎测试</h1>
- <p>JianpuRenderer - 自主开发的简谱渲染引擎</p>
- </header>
-
- <!-- 控制面板 -->
- <div class="control-panel">
- <h3>📦 MusicXML 数据源</h3>
- <div class="control-group">
- <div class="control-item" style="flex: 1;">
- <label>MusicXML URL(可从接口获取 xmlFileUrl)</label>
- <input type="text" id="xml-url" placeholder="请输入MusicXML文件的URL地址...">
- </div>
- <button onclick="fetchFromCurrentPage()" class="warning">📋 从当前曲谱获取URL</button>
- <button onclick="loadAndRender()" class="success">🎨 渲染简谱</button>
- </div>
- </div>
-
- <div class="control-panel">
- <h3>⚙️ 渲染配置</h3>
- <div class="control-group">
- <div class="control-item">
- <label>四分音符间距</label>
- <input type="number" id="spacing" value="50" min="30" max="100">
- </div>
- <div class="control-item">
- <label>字体大小</label>
- <input type="number" id="font-size" value="20" min="14" max="32">
- </div>
- <div class="control-item">
- <label>行宽度</label>
- <input type="number" id="system-width" value="1200" min="400" max="1600">
- </div>
- <div class="control-item">
- <label>显示歌词</label>
- <select id="show-lyrics">
- <option value="true">显示</option>
- <option value="false">隐藏</option>
- </select>
- </div>
- <div class="control-item">
- <label>调试模式</label>
- <select id="debug-mode">
- <option value="false">关闭</option>
- <option value="true">开启</option>
- </select>
- </div>
- <button onclick="clearOutput()">🗑️ 清除</button>
- </div>
- </div>
-
- <!-- 渲染面板 -->
- <div class="render-panel" id="render-panel">
- <!-- 缩放控制 -->
- <div class="zoom-controls">
- <button onclick="zoomOut()">➖</button>
- <span id="zoom-level">100%</span>
- <button onclick="zoomIn()">➕</button>
- <button onclick="zoomReset()">重置</button>
- </div>
- <!-- 全屏按钮 -->
- <button class="fullscreen-btn" onclick="toggleFullscreen()">
- <svg id="fullscreen-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
- <path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/>
- </svg>
- <span id="fullscreen-text">全屏</span>
- </button>
- <div id="score-container">
- <div class="placeholder">
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
- <path d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3"/>
- </svg>
- <p>请输入MusicXML URL,然后点击"渲染简谱"</p>
- <p style="font-size: 0.8rem; margin-top: 8px; color: #94a3b8;">
- 或者从当前打开的曲谱页面点击"从当前曲谱获取URL"自动填充
- </p>
- </div>
- </div>
- </div>
-
- <!-- 统计面板 -->
- <div class="stats-panel">
- <h3>📊 渲染统计</h3>
- <div class="stats-grid">
- <div class="stat-item">
- <div class="stat-value" id="stat-measures">-</div>
- <div class="stat-label">小节数</div>
- </div>
- <div class="stat-item">
- <div class="stat-value" id="stat-notes">-</div>
- <div class="stat-label">音符数</div>
- </div>
- <div class="stat-item">
- <div class="stat-value" id="stat-systems">-</div>
- <div class="stat-label">行数</div>
- </div>
- <div class="stat-item">
- <div class="stat-value" id="stat-parse">-</div>
- <div class="stat-label">解析时间</div>
- </div>
- <div class="stat-item">
- <div class="stat-value" id="stat-layout">-</div>
- <div class="stat-label">布局时间</div>
- </div>
- <div class="stat-item">
- <div class="stat-value" id="stat-draw">-</div>
- <div class="stat-label">绘制时间</div>
- </div>
- <div class="stat-item">
- <div class="stat-value" id="stat-total">-</div>
- <div class="stat-label">总耗时</div>
- </div>
- </div>
- </div>
-
- <!-- 日志面板 -->
- <div class="log-panel">
- <h3>📋 控制台日志</h3>
- <div id="log-output">
- <div class="log-entry info">等待加载MusicXML...</div>
- </div>
- </div>
- </div>
-
- <script type="module">
- import { JianpuRenderer } from '/src/jianpu-renderer/JianpuRenderer.ts';
- import { OSMDCompatibilityAdapter } from '/src/jianpu-renderer/adapters/OSMDCompatibilityAdapter.ts';
-
- // ========== 全屏和缩放功能 ==========
- let currentZoom = 100;
- let isFullscreen = false;
-
- // 全屏切换
- window.toggleFullscreen = function() {
- const panel = document.getElementById('render-panel');
- const icon = document.getElementById('fullscreen-icon');
- const text = document.getElementById('fullscreen-text');
-
- isFullscreen = !isFullscreen;
-
- if (isFullscreen) {
- panel.classList.add('fullscreen');
- text.textContent = '退出';
- icon.innerHTML = '<path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"/>';
- document.body.style.overflow = 'hidden';
- } else {
- panel.classList.remove('fullscreen');
- text.textContent = '全屏';
- icon.innerHTML = '<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/>';
- document.body.style.overflow = '';
- }
- };
-
- // ESC键退出全屏
- document.addEventListener('keydown', (e) => {
- if (e.key === 'Escape' && isFullscreen) {
- window.toggleFullscreen();
- }
- });
-
- // 缩放功能
- function updateZoom() {
- const container = document.getElementById('score-container');
- const svg = container.querySelector('svg');
- if (svg) {
- svg.style.transform = `scale(${currentZoom / 100})`;
- svg.style.transformOrigin = 'top center';
- }
- document.getElementById('zoom-level').textContent = `${currentZoom}%`;
- }
-
- window.zoomIn = function() {
- currentZoom = Math.min(300, currentZoom + 20);
- updateZoom();
- };
-
- window.zoomOut = function() {
- currentZoom = Math.max(20, currentZoom - 20);
- updateZoom();
- };
-
- window.zoomReset = function() {
- currentZoom = 100;
- updateZoom();
- };
-
- // ========== 日志和工具函数 ==========
-
- // 日志函数
- function log(message, type = 'info') {
- const logOutput = document.getElementById('log-output');
- const entry = document.createElement('div');
- entry.className = `log-entry ${type}`;
- entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
- logOutput.appendChild(entry);
- logOutput.scrollTop = logOutput.scrollHeight;
- console.log(`[${type}]`, message);
- }
-
- // 清除输出
- window.clearOutput = function() {
- document.getElementById('score-container').innerHTML = `
- <div class="placeholder">
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
- <path d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3"/>
- </svg>
- <p>请输入MusicXML URL,然后点击"渲染简谱"</p>
- </div>
- `;
- document.getElementById('log-output').innerHTML = '<div class="log-entry info">等待加载MusicXML...</div>';
-
- ['measures', 'notes', 'systems', 'parse', 'layout', 'draw', 'total'].forEach(id => {
- document.getElementById(`stat-${id}`).textContent = '-';
- });
- };
-
- // 从当前页面获取XML URL
- window.fetchFromCurrentPage = function() {
- // 尝试从 window.state 获取 xmlUrl
- if (window.opener && window.opener.state && window.opener.state.xmlUrl) {
- document.getElementById('xml-url').value = window.opener.state.xmlUrl;
- log('从父窗口获取到xmlUrl', 'success');
- return;
- }
-
- // 提示用户
- const example = 'https://oss.dayaedu.com/daya/music/1706164502047.xml';
- document.getElementById('xml-url').value = example;
- log('已填入示例URL,请替换为实际的xmlFileUrl', 'warn');
- };
-
- // 加载并渲染
- window.loadAndRender = async function() {
- const xmlUrl = document.getElementById('xml-url').value.trim();
-
- if (!xmlUrl) {
- log('请输入MusicXML URL', 'error');
- return;
- }
-
- const spacing = parseInt(document.getElementById('spacing').value);
- const fontSize = parseInt(document.getElementById('font-size').value);
- const systemWidth = parseInt(document.getElementById('system-width').value);
- const showLyrics = document.getElementById('show-lyrics').value === 'true';
- const debug = document.getElementById('debug-mode').value === 'true';
-
- log(`开始加载: ${xmlUrl}`);
-
- // 显示加载状态
- document.getElementById('score-container').innerHTML = `
- <div class="placeholder">
- <div class="loading"></div>
- <p>正在加载MusicXML...</p>
- </div>
- `;
-
- try {
- // 加载XML
- const response = await fetch(xmlUrl);
- if (!response.ok) {
- throw new Error(`无法加载文件: ${response.status} ${response.statusText}`);
- }
- const xmlContent = await response.text();
- log(`XML加载成功 (${(xmlContent.length / 1024).toFixed(1)} KB)`, 'success');
-
- // 清空容器
- const container = document.getElementById('score-container');
- container.innerHTML = '';
-
- // 创建JianpuRenderer
- const startTime = performance.now();
- const renderer = new JianpuRenderer(container, {
- quarterNoteSpacing: spacing,
- noteFontSize: fontSize,
- systemWidth: systemWidth,
- drawLyrics: showLyrics,
- debug: debug,
- });
-
- // 由于JianpuRenderer.load()需要OSMD对象,这里我们需要先创建一个简单的解析适配
- // 直接使用MusicXML字符串
- log('开始解析MusicXML...');
-
- // 注意:当前JianpuRenderer.load()需要OSMD对象或者类似结构
- // 这里我们创建一个简单的OSMD模拟对象用于测试
- const osmdObject = await createOSMDLikeObject(xmlContent);
-
- const parseStartTime = performance.now();
- await renderer.load(osmdObject);
- const parseTime = performance.now() - parseStartTime;
- log(`解析完成,耗时 ${parseTime.toFixed(2)}ms`, 'success');
-
- // 渲染
- const renderStartTime = performance.now();
- renderer.render();
- const renderTime = performance.now() - renderStartTime;
- log(`渲染完成,耗时 ${renderTime.toFixed(2)}ms`, 'success');
-
- // 获取统计信息
- const stats = renderer.getStats();
- const totalTime = performance.now() - startTime;
-
- // 更新统计
- document.getElementById('stat-measures').textContent = stats.measureCount;
- document.getElementById('stat-notes').textContent = stats.noteCount;
- document.getElementById('stat-systems').textContent = stats.systemCount;
- document.getElementById('stat-parse').textContent = `${stats.parseTime.toFixed(1)}ms`;
- document.getElementById('stat-layout').textContent = `${stats.layoutTime.toFixed(1)}ms`;
- document.getElementById('stat-draw').textContent = `${stats.drawTime.toFixed(1)}ms`;
- document.getElementById('stat-total').textContent = `${totalTime.toFixed(1)}ms`;
-
- log(`✅ 渲染完成! ${stats.measureCount}小节, ${stats.noteCount}音符, ${stats.systemCount}行`, 'success');
-
- // 生成times数组测试兼容性
- const adapter = new OSMDCompatibilityAdapter(renderer, debug);
- const times = adapter.generateTimesArray();
- log(`兼容层: 生成了 ${times.length} 个times项`, 'info');
-
- // 将renderer和times暴露到全局用于调试
- window.jianpuRenderer = renderer;
- window.jianpuTimes = times;
- log('💡 调试: window.jianpuRenderer / window.jianpuTimes 可用', 'info');
-
- // 重置缩放
- currentZoom = 100;
- document.getElementById('zoom-level').textContent = '100%';
-
- } catch (error) {
- log(`❌ 错误: ${error.message}`, 'error');
- console.error(error);
- document.getElementById('score-container').innerHTML = `
- <div class="placeholder" style="color: #ef4444;">
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
- <circle cx="12" cy="12" r="10"/>
- <line x1="15" y1="9" x2="9" y2="15"/>
- <line x1="9" y1="9" x2="15" y2="15"/>
- </svg>
- <p>渲染失败: ${error.message}</p>
- </div>
- `;
- }
- };
-
- /**
- * 创建类似OSMD的对象结构
- * JianpuRenderer需要这种格式的输入
- */
- async function createOSMDLikeObject(xmlContent) {
- // 导入解析器
- const { DivisionsHandler } = await import('/src/jianpu-renderer/core/parser/DivisionsHandler.ts');
-
- const parser = new DOMParser();
- const doc = parser.parseFromString(xmlContent, 'text/xml');
-
- const parseError = doc.querySelector('parsererror');
- if (parseError) {
- throw new Error(`XML解析错误: ${parseError.textContent}`);
- }
-
- const divisionsHandler = new DivisionsHandler();
- const title = doc.querySelector('work-title')?.textContent ?? 'Untitled';
- const measureElements = doc.querySelectorAll('measure');
- const sourceMeasures = [];
- const notes = [];
-
- let currentDivisions = 256;
- let currentTempo = 120;
- let currentTimeSignature = { numerator: 4, denominator: 4 };
- let currentKeySignature = { keyTypeOriginal: 0, Mode: 0 };
-
- measureElements.forEach((measureEl, measureIndex) => {
- const attributes = measureEl.querySelector('attributes');
- if (attributes) {
- const divisions = attributes.querySelector('divisions');
- if (divisions) {
- currentDivisions = parseInt(divisions.textContent ?? '256');
- divisionsHandler.setDivisions(currentDivisions);
- }
-
- const time = attributes.querySelector('time');
- if (time) {
- currentTimeSignature = {
- numerator: parseInt(time.querySelector('beats')?.textContent ?? '4'),
- denominator: parseInt(time.querySelector('beat-type')?.textContent ?? '4'),
- };
- }
-
- const key = attributes.querySelector('key');
- if (key) {
- const fifths = parseInt(key.querySelector('fifths')?.textContent ?? '0');
- currentKeySignature = { keyTypeOriginal: fifths, Mode: 0 };
- }
- }
-
- const sound = measureEl.querySelector('sound[tempo]');
- if (sound) {
- currentTempo = parseInt(sound.getAttribute('tempo') ?? '120');
- }
-
- const sourceMeasure = {
- MeasureNumberXML: measureIndex + 1,
- measureListIndex: measureIndex,
- tempoInBPM: currentTempo,
- ActiveTimeSignature: currentTimeSignature,
- ActiveKeySignature: currentKeySignature,
- Duration: { RealValue: currentTimeSignature.numerator / currentTimeSignature.denominator },
- lastRepetitionInstructions: [],
- verticalMeasureList: [],
- };
-
- sourceMeasures.push(sourceMeasure);
-
- const noteElements = measureEl.querySelectorAll('note');
- let timestamp = 0;
-
- noteElements.forEach((noteEl) => {
- const note = parseNote(noteEl, measureIndex, sourceMeasure, timestamp, divisionsHandler);
- if (note) {
- notes.push({ note, measureIndex, timestamp });
- if (!noteEl.querySelector('chord')) {
- timestamp += note.length.realValue;
- }
- }
- });
- });
-
- // 构建cursor迭代器
- let noteIndex = 0;
- const iterator = {
- EndReached: notes.length === 0,
- currentVoiceEntries: notes.length > 0 ? [{
- Notes: [notes[0]?.note],
- notes: [notes[0]?.note],
- ParentVoice: { VoiceId: 0 },
- }] : [],
- CurrentVoiceEntries: [],
- currentMeasureIndex: 0,
- currentTimeStamp: { RealValue: 0, realValue: 0 },
- moveToNextVisibleVoiceEntry: () => {
- noteIndex++;
- if (noteIndex >= notes.length) {
- iterator.EndReached = true;
- } else {
- const { note, measureIndex, timestamp } = notes[noteIndex];
- iterator.currentVoiceEntries = [{
- Notes: [note],
- notes: [note],
- ParentVoice: { VoiceId: 0 },
- }];
- iterator.CurrentVoiceEntries = iterator.currentVoiceEntries;
- iterator.currentMeasureIndex = measureIndex;
- iterator.currentTimeStamp = { RealValue: timestamp, realValue: timestamp };
- }
- },
- };
- iterator.CurrentVoiceEntries = iterator.currentVoiceEntries;
-
- return {
- Sheet: {
- Title: { text: title },
- SourceMeasures: sourceMeasures,
- userStartTempoInBPM: currentTempo,
- },
- GraphicSheet: {
- MeasureList: sourceMeasures.map(m => [{ parentSourceMeasure: m }]),
- },
- cursor: {
- Iterator: iterator,
- reset: () => {
- noteIndex = 0;
- iterator.EndReached = notes.length === 0;
- if (notes.length > 0) {
- const { note, measureIndex, timestamp } = notes[0];
- iterator.currentVoiceEntries = [{
- Notes: [note],
- notes: [note],
- ParentVoice: { VoiceId: 0 },
- }];
- iterator.CurrentVoiceEntries = iterator.currentVoiceEntries;
- iterator.currentMeasureIndex = measureIndex;
- iterator.currentTimeStamp = { RealValue: timestamp, realValue: timestamp };
- }
- },
- next: () => iterator.moveToNextVisibleVoiceEntry(),
- },
- };
- }
-
- function parseNote(noteEl, measureIndex, sourceMeasure, timestamp, divisionsHandler) {
- const isRest = noteEl.querySelector('rest') !== null;
- const durationEl = noteEl.querySelector('duration');
- const duration = parseInt(durationEl?.textContent ?? '256');
- const realValue = divisionsHandler.toRealValue(duration);
-
- let pitch = null;
- let halfTone = 60;
-
- if (!isRest) {
- const pitchEl = noteEl.querySelector('pitch');
- if (pitchEl) {
- const step = pitchEl.querySelector('step')?.textContent ?? 'C';
- const octave = parseInt(pitchEl.querySelector('octave')?.textContent ?? '4');
- const alter = parseInt(pitchEl.querySelector('alter')?.textContent ?? '0');
-
- pitch = { step, octave, alter };
-
- const stepToSemitone = { 'C': 0, 'D': 2, 'E': 4, 'F': 5, 'G': 7, 'A': 9, 'B': 11 };
- halfTone = (octave + 1) * 12 + (stepToSemitone[step] ?? 0) + alter;
- }
- }
-
- const dots = noteEl.querySelectorAll('dot').length;
- const typeEl = noteEl.querySelector('type');
- const noteType = typeEl?.textContent ?? 'quarter';
-
- const lyrics = [];
- const lyricElements = noteEl.querySelectorAll('lyric');
- lyricElements.forEach((lyricEl) => {
- const number = parseInt(lyricEl.getAttribute('number') ?? '1');
- const text = lyricEl.querySelector('text')?.textContent ?? '';
- lyrics.push({ number, text });
- });
-
- return {
- pitch,
- halfTone,
- length: { realValue, RealValue: realValue },
- isRestFlag: isRest,
- IsRest: isRest,
- IsGraceNote: noteEl.querySelector('grace') !== null,
- IsChordNote: noteEl.querySelector('chord') !== null,
- dots,
- DotsXml: dots,
- noteTypeXml: noteType,
- sourceMeasure,
- duration,
- lyrics,
- };
- }
-
- // 初始化
- log('简谱渲染引擎测试页面已加载', 'success');
- log('请输入MusicXML URL并点击"渲染简谱"', 'info');
- </script>
- </body>
- </html>
|