katspaugh/wavesurfer.js

Region playback documentation

danijel3 opened this issue · 6 comments

The auto playback/loop feature described in the documentation sucks. It's an important feature if you want to set up the region boundaries precisely and need to playback the regions multiple times while doing fine adjustments.

The problem with the example in the demos is that it always overshoots the ending by a random amount and using region events simply doesn't work.

Looking through the source code, I found that WebAudio backend supports a slightly more accurate method of pausing the audio at region end. Granted this method doesn't work for HTML audio element backend, but I think it would be worth documenting this feature somewhere, so people don't have to look for it in the sourcecode.

My solution requires the appropriate backend in the Wavesrufer constructor:

wavesurfer = WaveSurfer.create({
    container: '#segmentation-container',
    url: audio_url,
    backend: "WebAudio",
    ...
})

Next I create a playBetween method like this:

function playBetween(start, end) {
    wavesurfer.media.currentTime = start
    wavesurfer.media.play()
    wavesurfer.media.stopAt(end)
  }

Which I can use in various contexts, for example:

    regions.on('region-clicked', (r, e) => {
      e.stopPropagation() // prevent triggering a click on the waveform
      playBetween(r.start, r.end)
    })

Thanks that was useful I was experiencing the same issue with looping regions. I'll let you know once I try it.

Yea, this isn't really perfect for looping, but you could implement something yourself. To see how, you need to look at the implementation of the stopAt function:

stopAt(timeSeconds: number) {
const delay = timeSeconds - this.currentTime
this.bufferNode?.stop(this.audioContext.currentTime + delay)
this.bufferNode?.addEventListener(
'ended',
() => {
this.bufferNode = null
this.pause()
},
{ once: true },
)
}

It works similarly to the example case, but instead of looking for wavesurfer events, it is using a slightly more accurate buffer node events. It's worth noting this is still far from perfect. The safest, most perfect approach would simply copy the portion of the buffer to a separate location (say another audio node) and simply play/loop that.

EDIT: Upon reviewing the documentation, it's actually quite accurate:

https://developer.mozilla.org/en-US/docs/Web/API/AudioScheduledSourceNode/stop

You could go with copying the buffer if you were extremely paranoid, but for most use cases, this is quite sufficient. The only small bug I notice is the playhead is sometimes a bit off graphically, but the audio is actually pretty accurate.

Has loop?

Again, this functionality (unlike stopAt) isn't implemented anywhere in the project. I got something to work, using this function:

 function loopBetween(start, end) {
  if (!wavesurfer.media.paused) return

  wavesurfer.media.paused = false

  wavesurfer.media.bufferNode?.disconnect()
  wavesurfer.media.bufferNode = wavesurfer.media.audioContext.createBufferSource()
  if (wavesurfer.media.buffer) {
    wavesurfer.media.bufferNode.buffer = wavesurfer.media.buffer
  }
  wavesurfer.media.bufferNode.playbackRate.value = wavesurfer.media._playbackRate
  wavesurfer.media.bufferNode.connect(wavesurfer.media.gainNode)

  wavesurfer.media.bufferNode.loop = true
  wavesurfer.media.bufferNode.loopStart = start
  wavesurfer.media.bufferNode.loopEnd = end

  wavesurfer.media.bufferNode.start(wavesurfer.media.audioContext.currentTime, start)
}

However, this won't advance the playhead, even if the audio does loop. You can check out the documentation here:
https://developer.mozilla.org/en-US/docs/Web/API/AudioBufferSourceNode/loop

Hello, can you try to load large files through range slicing in wavesuier?

Again, this functionality (unlike stopAt) isn't implemented anywhere in the project. I got something to work, using this function:

 function loopBetween(start, end) {
  if (!wavesurfer.media.paused) return

  wavesurfer.media.paused = false

  wavesurfer.media.bufferNode?.disconnect()
  wavesurfer.media.bufferNode = wavesurfer.media.audioContext.createBufferSource()
  if (wavesurfer.media.buffer) {
    wavesurfer.media.bufferNode.buffer = wavesurfer.media.buffer
  }
  wavesurfer.media.bufferNode.playbackRate.value = wavesurfer.media._playbackRate
  wavesurfer.media.bufferNode.connect(wavesurfer.media.gainNode)

  wavesurfer.media.bufferNode.loop = true
  wavesurfer.media.bufferNode.loopStart = start
  wavesurfer.media.bufferNode.loopEnd = end

  wavesurfer.media.bufferNode.start(wavesurfer.media.audioContext.currentTime, start)
}

However, this won't advance the playhead, even if the audio does loop. You can check out the documentation here: https://developer.mozilla.org/en-US/docs/Web/API/AudioBufferSourceNode/loop

However, the pointer of the wavesufer waveform does not run synchronously and is not linked to the waveform. Although audio playback is implemented