Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions src/audio/Audio.js
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,35 @@ class Audio extends Object3D {

}

/**
* Fades the audio in by increasing volume gradually from `volumeNow` to `volumeThen`,
* within the passed time interval.
*
* @param {number} duration - The duration of the fade.
* @param {number} [volumeNow=0] - The volume at the start of the fade.
* @param {number} [volumeThen=1] - The volume at the end of the fade.
* @return {Audio} A reference to this instance.
*/
fadeIn( duration, volumeNow = 0, volumeThen = 1 ) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've quickly checked Unreal and they also have fadein/fadeout in their Blueprint API:

https://dev.epicgames.com/documentation/en-us/unreal-engine/BlueprintAPI/Audio/Components/Audio/FadeIn
https://dev.epicgames.com/documentation/en-us/unreal-engine/BlueprintAPI/Audio/Components/Audio/FadeOut

When checking our own methods, I think fadeIn() and fadeOut() should have an optional delay parameter similar to play() and stop(). It's in seconds and you add it currentTime when scheduling the values. New singnature:

fadeIn( duration, volumeNow = 0, volumeThen = 1, delay = 0 ) {

Also, do we need volumeNow? Can't we just use the current volume like in fadeOut(). It seems the Unreal component also just allows to define the target volume.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I think we can set the volumeNow beforehand

audio.setVolume(0.1);
audio.fadeIn(3, 0.5);

although there might be some issues because setVolume does not set the volume immediately.
it is using the same setTargetAtTime API with a small 0.01 timeConstant.
so it could be setVolume will be canceled by fadeIn

Copy link
Collaborator

@Mugen87 Mugen87 Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about we save the value passed to setVolume() is a private variable an use it in the fade methods? In this way, the value set via setVolume() isn't lost when fading in an audio like in https://github.com/mrdoob/three.js/pull/32549/files#r2622754999. The implementation would use:

return this._scheduleFading( duration, this._volume, volumeThen );

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will check if it works fine or not with the above example
it was just a hypothesis

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found this old issue #12510

do you remember why setTargetAtTime was used and not setValueAtTime ?

Screenshot 2025-12-17 at 10 22 39
  1. if possible I would replace it with setValueAtTime (instant volume update)
  2. your suggestion with this._volume
  3. this is evil but currentTime + 0.01 * 3 will make sure setVolume smoothing is complete

Copy link
Contributor Author

@tatsmaki tatsmaki Dec 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we care about cases when people access audio.gain.gain.value directly?
because in this scenario this._volume will not be set

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not ideal that gain is public but in general we expect developers work with setVolume().

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

audio.setVolume(0.1);
audio.fadeIn(3, 0.5);

I've tested the code today an realized the usage of setTargetAtTime() in setVolume() does not work with the new fading methods. In the above code example, when fadeIn() is executed, the current volume is not set to 0.1 yet which means the interpolation to the target value won't work. After some testing this can only be fixed by using setValueAtTime() in setVolume(). Or by using your original implementation. However, I'm not a fan of setting the current volume in the fade methods since we already have setVolume() for that.

So I think before we can make this change, we have to migrate from setTargetAtTime() to setValueAtTime(). But that requires good testing and some research in older issues since setTargetAtTime() was chosen on purpose, see https://github.com/mrdoob/three.js/pull/32549/files#r2631137839.

Copy link
Collaborator

@Mugen87 Mugen87 Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After some more reading I'm not feeling comfortable to make the change from setTargetAtTime() to setValueAtTime(). As explained in WebAudio/web-audio-api#76 (comment), setTargetAtTime() provides a De-zippering behavior:

De-zippering is the process of smoothly approaching a target value instead of jumping to it directly. The intention is to prevent audio "glitches" which might occur if the value is changed suddenly.

setValueAtTime() does not:

If a developer explicitly sets values (using setValueAtTime for example) then de-zippering is not performed.

So if we use setValueAtTime(), we risk to introduce (platform specific) audio regressions e.g. when changing the volume. Bugs like crackling sounds were reported in the past (I actually heard them myself) and we don't experience them since we use setTargetAtTime().

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After reconsideration it's probably better to close the PR and let fadein/fadeouts be implemented on application level. _scheduleFading() depends on setValueAtTime() and the consequences of using this method in audio animations are unclear.


return this._scheduleFading( duration, volumeNow, volumeThen );

}

/**
* Fades the audio out by decreasing volume gradually from the current volume to `volumeThen`,
* within the passed time interval.
*
* @param {number} duration - The duration of the fade.
* @param {number} [volumeThen=0] - The volume at the end of the fade.
* @return {Audio} A reference to this instance.
*/
fadeOut( duration, volumeThen = 0 ) {

return this._scheduleFading( duration, this.getVolume(), volumeThen );

}

copy( source, recursive ) {

super.copy( source, recursive );
Expand Down Expand Up @@ -773,6 +802,25 @@ class Audio extends Object3D {

}

_scheduleFading( duration, volumeNow, volumeThen ) {

const currentTime = this.listener.context.currentTime;
/**
* 95% of the duration.
*
* Reference: {@link https://developer.mozilla.org/en-US/docs/Web/API/AudioParam/setTargetAtTime#choosing_a_good_timeconstant}
*/
const timeConstant = duration / 3;

this.gain.gain
.cancelScheduledValues( currentTime )
.setValueAtTime( volumeNow, currentTime )
.setTargetAtTime( volumeThen, currentTime, timeConstant );

return this;

}

}

export { Audio };