为了账号安全,请及时绑定邮箱和手机立即绑定

为节拍器在浏览器中准确计时声音

为节拍器在浏览器中准确计时声音

潇潇雨雨 2022-10-13 19:13:52
我想创建一个虚拟节拍器并在每一步播放节拍器声音。setTimeout()/等常用方法setInterval()不够准确。我怎样才能让它在不延迟或丢失节拍的情况下工作?
查看完整描述

1 回答

?
缥缈止盈

TA贡献2041条经验 获得超4个赞

这是一个每 500 毫秒发出一次哔声的函数。

function beep() {

    var snd = new Audio("data:audio/wav;base64,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qJrx4KOQjahZxWbcZgztj2c49nKmkId44S71j0c8eV9yDK6uPRzx5X18eDvjvQ6yKo9ZSS6l//8elePK/Lf//IInrOF/FvDoADYAGBMGb7FtErm5MXMlmPAJQVgWta7Zx2go+8xJ0UiCb8LHHdftWyLJE0QIAIsI+UbXu67dZMjmgDGCGl1H+vpF4NSDckSIkk7Vd+sxEhBQMRU8j/12UIRhzSaUdQ+rQU5kGeFxm+hb1oh6pWWmv3uvmReDl0UnvtapVaIzo1jZbf/pD6ElLqSX+rUmOQNpJFa/r+sa4e/pBlAABoAAAAA3CUgShLdGIxsY7AUABPRrgCABdDuQ5GC7DqPQCgbbJUAoRSUj+NIEig0YfyWUho1VBBBA//uQZB4ABZx5zfMakeAAAAmwAAAAF5F3P0w9GtAAACfAAAAAwLhMDmAYWMgVEG1U0FIGCBgXBXAtfMH10000EEEEEECUBYln03TTTdNBDZopopYvrTTdNa325mImNg3TTPV9q3pmY0xoO6bv3r00y+IDGid/9aaaZTGMuj9mpu9Mpio1dXrr5HERTZSmqU36A3CumzN/9Robv/Xx4v9ijkSRSNLQhAWumap82WRSBUqXStV/YcS+XVLnSS+WLDroqArFkMEsAS+eWmrUzrO0oEmE40RlMZ5+ODIkAyKAGUwZ3mVKmcamcJnMW26MRPgUw6j+LkhyHGVGYjSUUKNpuJUQoOIAyDvEyG8S5yfK6dhZc0Tx1KI/gviKL6qvvFs1+bWtaz58uUNnryq6kt5RzOCkPWlVqVX2a/EEBUdU1KrXLf40GoiiFXK///qpoiDXrOgqDR38JB0bw7SoL+ZB9o1RCkQjQ2CBYZKd/+VJxZRRZlqSkKiws0WFxUyCwsKiMy7hUVFhIaCrNQsKkTIsLivwKKigsj8XYlwt/WKi2N4d//uQRCSAAjURNIHpMZBGYiaQPSYyAAABLAAAAAAAACWAAAAApUF/Mg+0aohSIRobBAsMlO//Kk4soosy1JSFRYWaLC4qZBYWFRGZdwqKiwkNBVmoWFSJkWFxX4FFRQWR+LsS4W/rFRb/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////VEFHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAU291bmRib3kuZGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMjAwNGh0dHA6Ly93d3cuc291bmRib3kuZGUAAAAAAAAAACU=");  

    snd.play();

}


setInterval(beep, 500)

然而,这不是很精确。回调可能会延迟触发(取决于很多,这可以从几毫秒到超过 1000 毫秒)。要解决这个问题,您可以使用AudioContext. 我们将使用一个简单Oscillator的节拍器声音。

您可以一次安排多个音符,也可以setInterval用来运行一个安排下一个应该播放的音符的功能。

function noteDurationToMs (bpm, dur, type) {

    return 60000 * 4 * dur * type / bpm

}


function scheduleNote(ac, time, dur) {

    var osc = ac.createOscillator();

    osc.connect( ac.destination );

    osc.start(time);

    osc.stop(time + dur);

}


const ac = new AudioContext();

let lastNote= ac.currentTime;

const step = noteDurationToMs(120, 1 / 4, 1) / 1000;

const lookAhead = step / 2;


let id, timer = ()=> {

  const diff = ac.currentTime - lastNote;

  if (diff >= lookAhead) {

   const nextNote = lastNote + step;

   scheduleNote(ac, nextNote, 0.025)

   lastNote = nextNote;

  }

}

a.addEventListener('click', () => {

    ac.resume();

    id = setInterval(timer, 15)

})

b.addEventListener('click', () => clearInterval(id))

<button id="a">Start</button>

<button id="b">Stop</button>


注意:由于某种原因AudioContext,Stack Snippets 似乎没有工作。这是第一个代码片段的jsfiddle。这是第二个片段的一个。


function noteDurationToMs (bpm, dur, type) {

    return 60000 * 4 * dur * type / bpm

}


function scheduleNote(ac, time, dur) {

    var osc = ac.createOscillator();

    osc.connect( ac.destination );

    

    

    osc.start(time);

    osc.stop(time + dur);

}


const ac = new AudioContext();

let to, lastNote = 0;


const Metronome = () => {

  const [bpm, setBpm] = React.useState(100);

  const [dur, setDur] = React.useState(1);  

  const [type, setType] = React.useState(1);

    const [run, setRun] = React.useState(false);    

  

  const handleChangeBPM = (e) => {

    setBpm(e.target.value)

  }

  

  const handleChangeDur = (e) => {

    setDur(e.target.value)

  }

  

  const handleChangeType = (e) => {

    setType(e.target.value)

  }

  

  const step = noteDurationToMs(bpm, dur, type) / 1000;

    const lookAhead = step / 2;

  

  const timer = () => {

    const diff = ac.currentTime - lastNote;

    if (diff >= lookAhead) {

     const nextNote = lastNote + step;

     scheduleNote(ac, nextNote, 0.025)

     lastNote = nextNote;

    }

  }

  

  const start = () => {

    ac.resume()

    setRun(true);

  }

  

  const stop = () => {

    clearInterval(to);

    setRun(false);

  }

  

  const toggle = () => {

    run?stop():start();

  }


    React.useEffect(() => {

    if (run) {

        clearInterval(to);

      to = setInterval(timer, step / 4);

    }

  });

  

    return <div>

    <label for="bpm">BPM:</label>


    

      <input id="bpm" type="number" onChange={handleChangeBPM} value={bpm} />

    

    <label for="dur">Duration:</label>

    

    <select id="dur" onChange={handleChangeDur} value={dur}>

      <option value={1}>Whole</option>

      <option value={1 / 2}>Half</option>

      <option value={1 / 4}>Quarter</option>

      <option value={1 / 8}>Eigth </option>

      <option value={1 / 16}>Sixteenth</option>

      <option value={1 / 32}>Thirtysecond</option>

    </select>

    

    <label for="typ">Type:</label>

    <select id="typ" onChange={handleChangeType} value={type}>

      <option value={1}>Regular</option>

      <option value={3 / 2}>Dotted</option>

      <option value={2 / 3}>Triplet</option>

    </select>

    <button onClick={toggle}>{!run?'Start':'Stop'}</button>

    <br />

    <label for="ms">MS:</label>

    <output id="ms">{noteDurationToMs(bpm, dur, type)}</output>

    </div>

}


ReactDOM.render(<Metronome />, document.querySelector("#app"))

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>

<div id="app"></div>

要包含您自己的编辑: 您还可以使用AudioBufferSourceNode和调用.start(),when参数来准确地播放音频,但您将无法使用它运行函数。


AudioBufferSourceNode.start([when]);


查看完整回答
反对 回复 2022-10-13
  • 1 回答
  • 0 关注
  • 52 浏览
慕课专栏
更多

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信