'Fading in and out Web Audio Loop

I'm recording some audio in the browser and then want to loop it seamlessly, avoiding clicks etc when starting. This means fading it and out.

I can ramp the volume up and down once, but I can't find anyway to trigger Web Audio's 'ramp to value at time' every time the loop starts again.

Is there an easy way to do this? I've got 10 of these buffers looping so I'd like to avoid lots of costly setinterval checks if possible...

            let source = audioContext.createBufferSource();
            let gain = audioContext.createGain();
            gain.gain.value = 0.01;

            source.buffer = decodedData;
            songLength = decodedData.duration;

            source.loop = true;
            source.connect(gain);
            gain.connect(audioContext.destination);
            source.start(0);
        
            // fade in and out
            gain.gain.exponentialRampToValueAtTime(0.2, audioContext.currentTime + 1);
            gain.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + songLength);


Solution 1:[1]

Consider listening to the ended event and re-trigger the playback:

class FadeInLoop {
  ctx
  audioBuffer
  gainNode
  isPlaying = true

  constructor(ctx, url) {
    this.ctx = ctx
    this.audioBuffer = fetch(url)
      .then(response => response.arrayBuffer())
      .then(arrayBuffer => ctx.decodeAudioData(arrayBuffer))

    this.gainNode = ctx.createGain()
    this.gainNode.connect(ctx.destination)
  }

  async start() {
    this.isPlaying = true
    const source = ctx.createBufferSource()
    this.source = source
    source.addEventListener('ended', e => {
      if (this.isPlaying) { // repeat unless stop() was called
        this.start()
      }
    })

    source.connect(this.gainNode)
    source.buffer = await this.audioBuffer
    const now = this.ctx.currentTime
    this.gainNode.gain.setValueAtTime(Number.EPSILON, now);
    this.gainNode.gain.exponentialRampToValueAtTime(1, now + 0.055)
    source.start(0)
  }

  stop() {
    this.isPlaying = false
    this.source?.stop()
  }
}

const ctx = new AudioContext({ latencyHint: 'interactive' })
const loop = new FadeInLoop(ctx, 'https://batman.dev/static/71474264/loop.mp3')
<button onclick="loop.start()">Start</button>
<button onclick="loop.stop()">Stop</button>

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1