Search code examples
javascriptaudioweb-audio-api

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

  • 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>