You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

HTML Canvas乐谱滚动与Metronome节拍器同步问题求助

高精度同步解决方案

核心思路

放弃setTimeout这种低精度定时器,改用requestAnimationFrame做乐谱滚动渲染(和浏览器渲染帧同步,精度更高),同时用Web Audio API的AudioContext.currentTime作为统一时间基准,让节拍音效触发和乐谱滚动完全对齐。

具体实现步骤

1. 用AudioContext统一计时基准

Web Audio的时间是微秒级精度,不受主线程阻塞影响,直接用它作为节拍和滚动的核心时间源:

// 注意:必须通过用户交互(比如点击开始按钮)初始化,避免浏览器静音限制
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();

2. 计算滚动的精确速度

根据当前BPM算出每个四分音符的时长,再结合音符间的像素间距,得到精确的滚动速度(像素/毫秒):

// 示例:bpm是当前节拍速度,noteSpacing是乐谱中两个音符的垂直像素间距
const beatDurationMs = 60000 / bpm;
const scrollSpeed = noteSpacing / beatDurationMs; // 每毫秒滚动的像素数

3. 用requestAnimationFrame驱动滚动

替换原来的setTimeout滚动逻辑,用requestAnimationFrame的高精度时间戳计算滚动位置,确保每帧渲染都精准:

let startTime; // 滚动开始的时间戳
let totalScoreHeight; // 乐谱总高度,用来判断是否滚动结束

function updateScroll(timestamp) {
  if (!startTime) startTime = timestamp;
  const elapsedMs = timestamp - startTime;
  // 计算当前应该滚动的偏移量
  const scrollOffset = scrollSpeed * elapsedMs;
  
  // 这里替换成你原来的Canvas绘制逻辑,传入scrollOffset作为偏移量
  drawScore(scrollOffset);
  
  // 没滚完就继续
  if (scrollOffset < totalScoreHeight) {
    requestAnimationFrame(updateScroll);
  }
}

4. 节拍音效精准调度

用Web Audio的时间调度方法提前安排音效,完全脱离定时器的精度限制:

function scheduleMetronome() {
  const beatIntervalSec = 60 / bpm; // 每个节拍的秒数
  let nextBeatTime = audioCtx.currentTime;
  
  // 提前调度未来1秒内的所有节拍,避免临时卡顿漏拍
  while (nextBeatTime < audioCtx.currentTime + 1) {
    playBeat(nextBeatTime);
    nextBeatTime += beatIntervalSec;
  }
  
  // 每半秒重新调度一次,保证后续节拍持续精准
  setTimeout(scheduleMetronome, 500);
}

function playBeat(scheduledTime) {
  // 创建节拍音效的Web Audio节点
  const oscillator = audioCtx.createOscillator();
  const gainNode = audioCtx.createGain();
  
  oscillator.connect(gainNode);
  gainNode.connect(audioCtx.destination);
  
  // 设置音效参数,确保在指定时间触发和结束
  oscillator.frequency.value = 880;
  gainNode.gain.setValueAtTime(0.3, scheduledTime);
  gainNode.gain.exponentialRampToValueAtTime(0.01, scheduledTime + 0.1);
  
  oscillator.start(scheduledTime);
  oscillator.stop(scheduledTime + 0.1);
}

5. 同步启动滚动和节拍

在用户点击“开始”按钮时,同时启动滚动和节拍调度:

function startPractice() {
  // 先重置AudioContext状态(如果之前暂停过)
  if (audioCtx.state === 'suspended') {
    audioCtx.resume();
  }
  
  // 初始化滚动参数
  startTime = null;
  scrollSpeed = noteSpacing / (60000 / bpm);
  
  // 启动滚动和节拍
  requestAnimationFrame(updateScroll);
  scheduleMetronome();
}

关键注意点

  • 必须通过用户交互初始化AudioContext,否则浏览器会阻止音频播放
  • 滚动计算时要考虑Canvas的缩放比例,避免像素偏移导致的同步误差
  • 提前调度多个节拍音效,防止主线程临时阻塞导致漏拍

内容的提问来源于stack exchange,提问作者user30222843

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 12:17:45