import * as WebAudioApiErrors from '../modules/Errors.mjs';
import { PitchShift } from './PitchShift.mjs';
import { EffectBase } from './EffectBase.mjs';
/**
* Class representing a Doppler effect.
*
* A Doppler effect performs a linear change in frequency over a specified period of time.
*
* @extends EffectBase
*/
export class Doppler extends EffectBase {
// Effect-specific private variables
/** @type {PitchShift} */
#pitchShifter;
/** @type {number} */
#initDistance;
/** @type {number} */
#finalDistance;
/** @type {number} */
#missDistance;
/** @type {number} */
#duration;
// Parameter limits
static minDistance = 0;
static maxDistance = 1000;
static minDuration = 0;
static maxDuration = 60;
/**
* Constructs a new {@link Doppler} effect object.
*/
constructor(audioContext) {
super(audioContext);
this.#pitchShifter = new PitchShift(audioContext);
}
/**
* Returns a list of all available parameters for manipulation in the `effectOptions` parameter
* of the {@link EffectBase#update update()} function for this {@link Effect}.
*
* @returns {EffectParameter[]} List of effect-specific parameters for use in the effect's {@link EffectBase#update update()} function
* @see {@link EffectParameter}
*/
static getParameters() {
return [
{ name: 'initDistance', type: 'number', validValues: [Doppler.minDistance, Doppler.maxDistance], defaultValue: 100 },
{ name: 'finalDistance', type: 'number', validValues: [Doppler.minDistance, Doppler.maxDistance], defaultValue: 100 },
{ name: 'missDistance', type: 'number', validValues: [Doppler.minDistance, Doppler.maxDistance], defaultValue: 14 },
{ name: 'duration', type: 'number', validValues: [Doppler.minDuration, Doppler.maxDuration], defaultValue: 10 }
];
}
async load() {
this.#initDistance = 100;
this.#finalDistance = 100;
this.#missDistance = 14;
this.#duration = 10;
await this.#pitchShifter.load();
}
/* eslint no-empty-pattern: "off" */
/**
* Updates the {@link Doppler} effect according to the specified parameters at the
* specified time.
*
* Note that the `updateTime` parameter can be omitted to immediately cause the requested
* changes to take effect.
*
* @param {number} initDistance - Starting distance in meters between an audio source and an observer
* @param {number} finalDistance - Final distance in meters between an audio source and an observer
* @param {number} missDistance - Distance in meters by which the audio source misses the observer
* @param {number} duration - Duration in seconds required for the audio source to travel from its starting to final location
* @param {number} [updateTime] - Global API time at which to update the effect
* @param {number} [timeConstant] - Time constant defining an exponential approach to the target
* @returns {Promise<boolean>} Whether the effect update was successfully applied
*/
async update({ initDistance, finalDistance, missDistance, duration }, updateTime) {
if ((initDistance == null) || (finalDistance == null) || (missDistance == null) || (duration == null))
throw new WebAudioApiErrors.WebAudioValueError('Cannot update the Doppler effect without all of the following parameters: "initDistance, finalDistance, missDistance, duration"');
else if (initDistance < Doppler.minDistance)
throw new WebAudioApiErrors.WebAudioValueError(`Distance value cannot be less than ${Doppler.minDistance}`);
else if (initDistance > Doppler.maxDistance)
throw new WebAudioApiErrors.WebAudioValueError(`Distance value cannot be greater than ${Doppler.maxDistance}`);
else if (finalDistance < Doppler.minDistance)
throw new WebAudioApiErrors.WebAudioValueError(`Distance value cannot be less than ${Doppler.minDistance}`);
else if (finalDistance > Doppler.maxDistance)
throw new WebAudioApiErrors.WebAudioValueError(`Distance value cannot be greater than ${Doppler.maxDistance}`);
else if (missDistance < Doppler.minDistance)
throw new WebAudioApiErrors.WebAudioValueError(`Distance value cannot be less than ${Doppler.minDistance}`);
else if (missDistance > Doppler.maxDistance)
throw new WebAudioApiErrors.WebAudioValueError(`Distance value cannot be greater than ${Doppler.maxDistance}`);
else if (duration < Doppler.minDuration)
throw new WebAudioApiErrors.WebAudioValueError(`Duration value cannot be less than ${Doppler.minDuration}`);
else if (duration > Doppler.maxDuration)
throw new WebAudioApiErrors.WebAudioValueError(`Duration value cannot be greater than ${Doppler.maxDuration}`);
else if (initDistance < missDistance)
throw new WebAudioApiErrors.WebAudioValueError('Initial distance cannot be less than the miss distance');
else if (finalDistance < missDistance)
throw new WebAudioApiErrors.WebAudioValueError('Final distance cannot be less than the miss distance');
const timeToUpdate = (updateTime == null) ? this.audioContext.currentTime : updateTime;
const approachingDistance = Math.sqrt(initDistance**2 - missDistance**2);
const departingDistance = Math.sqrt(finalDistance**2 - missDistance**2);
const totalDistance = approachingDistance + departingDistance;
const speedMetersPerCentisecond = totalDistance / (100 * duration);
const approachingDuration = duration * (approachingDistance / totalDistance);
const departingDuration = duration * (departingDistance / totalDistance);
const approachingWeights = new Float32Array(approachingDuration * 100);
const departingWeights = new Float32Array(departingDuration * 100);
for (let i = 0; i < approachingWeights.length; ++i)
approachingWeights[i] = Math.cos(Math.atan2(missDistance, approachingDistance - (i * speedMetersPerCentisecond)));
for (let i = 0; i < departingWeights.length; ++i)
departingWeights[i] = Math.cos(Math.atan2(missDistance, i * speedMetersPerCentisecond));
const approachingFrequency = 1200 * Math.log2(343.0 / (343.0 - (100 * speedMetersPerCentisecond)));
const departingFrequency = 1200 * Math.log2(343.0 / (343.0 + (100 * speedMetersPerCentisecond)));
this.#pitchShifter.updatePrivate(approachingFrequency, timeToUpdate, approachingWeights, approachingDuration);
this.#pitchShifter.updatePrivate(departingFrequency, timeToUpdate + approachingDuration, departingWeights, departingDuration);
this.#initDistance = initDistance;
this.#finalDistance = finalDistance;
this.#missDistance = missDistance;
this.#duration = duration;
return true;
}
currentParameterValues() {
return {
initDistance: this.#initDistance,
finalDistance: this.#finalDistance,
missDistance: this.#missDistance,
duration: this.#duration
};
}
getInputNode() {
return this.#pitchShifter.getInputNode();
}
getOutputNode() {
return this.#pitchShifter.getOutputNode();
}
}