Search code examples
scheduled-tasksweb-audio-apipitch

How do you schedule a fade-out using setTargetAtTime


I am having problems with the 'windowing', as it's called, of pitch-shifted grains of sounds using the Web Audio API. When I play the sound, I successfully set the initial gain to 0, using setValueAtTime, and successfully fade the sound in using linearRampToValueAtTime. However, I am not successful in scheduling a fade-out to occur slightly before the sound ends. It may be because the sound is pitch-shifted, although in the code below, I believe I have set the parameters correctly. Though maybe the final parameter in setTargetAtTime is not suitable because I don't fully understand that parameter, despite having read Chris's remarks on the matter. How does one successfully schedule a fade-out when the sound is pitch-shifted, using setTargetAtTime? Below is my code. You can see the piece itself at https://vispo.com/animisms/enigman2/2022

            source.buffer = audioBuffer;
            source.connect(this.GrainObjectGain);
            this.GrainObjectGain.connect(app.mainGain);
            source.addEventListener('ended', this.soundEnded);
            //------------------------------------------------------------------------
            // Now we do the 'windowing', ie, when we play it, we fade it in,
            // and we set it to fade out at the end of play. The fade duration
            // in seconds is a maximum of 10 milliseconds and 
            // a minimum of 10% of the duration of the sound. This helps 
            // eliminate pops.
            source.playbackRate.value = this.playbackRate;
            var fadeDurationInSeconds = Math.min(0.01,0.1*duration*this.playbackRate);
            this.GrainObjectGain.gain.setValueAtTime(0, app.audioContext.currentTime);
            this.GrainObjectGain.gain.linearRampToValueAtTime(app.constantGrainGain, app.audioContext.currentTime+fadeDurationInSeconds);
            this.GrainObjectGain.gain.setTargetAtTime(0, app.audioContext.currentTime+duration*this.playbackRate-fadeDurationInSeconds, fadeDurationInSeconds);
            source.start(when, offset, duration);

Solution

  • Given your comment below I guess you want to schedule the fade out fadeDurationInSeconds before the sound ends.

    Since you change playbackRate you need to divide the original duration by that playbackRate to get the actual duration.

    Changing your setTargetAtTime() call as follows should schedule the fade out at the desired point in time.

    this.GrainObjectGain.gain.setTargetAtTime(
        0,
        app.audioContext.currentTime + duration / this.playbackRate - fadeDurationInSeconds,
        fadeDurationInSeconds
    );
    

    Please not that setTargetAtTime() actually never reaches the target. At least in theory. It comes reasonably close though after some time. But that time is longer as the timeConstant.

    The relevant text from the spec can be found here: https://webaudio.github.io/web-audio-api/#dom-audioparam-settargetattime

    Start exponentially approaching the target value at the given time with a rate having the given time constant. Among other uses, this is useful for implementing the "decay" and "release" portions of an ADSR envelope. Please note that the parameter value does not immediately change to the target value at the given time, but instead gradually changes to the target value.

    The timeConstant parameter roughly defines the time it takes to reach 2/3 of the desired signal attenuation.

    It's mentioned here: https://webaudio.github.io/web-audio-api/#dom-audioparam-settargetattime-timeconstant

    More precisely, timeConstant is the time it takes a first-order linear continuous time-invariant system to reach the value 1−1/𝑒 (around 63.2%) given a step input response (transition from 0 to 1 value).