VulpesOS/gaia/shared/js/dialer/tone_player.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

217 lines
7.4 KiB
JavaScript
Executable file

/* globals AudioContext */
/* exported TonePlayer */
'use strict';
const kMasterVolume = 0.5;
const kToneVolume = 0.7;
const kShortPressDuration = 0.15;
const kAttackDuration = 0.025;
const kDecayDuration = 0.025;
const kReleaseDuration = 0.05;
var TonePlayer = {
_audioContext: null,
_channel: null,
_gainNode: null,
_playingNodes: [],
_maybeTrashAudio: null,
_initialized: false,
/**
* Initializes the tone player by specifying which channel will be used to
* play sounds. The TonePlayer will lazily create an AudioContext to play
* sounds when needed and automatically dispose of it when the application is
* hidden. However if the 'telephony' channel is used then we'll keep the
* AudioContext around as long as there's an active call.
*
* @param channel {String} The default channel used to play sounds.
*/
init: function tp_init(channel) {
var telephony = navigator.mozTelephony;
this._reset();
this._channel = channel;
this._maybeTrashAudio = (function tp_maybeTrashAudio() {
var callIsActive = telephony && (telephony.calls.length ||
telephony.conferenceGroup.calls.length);
/* If the application is hidden dispose of the audio context unless we're
* in a call and we're using the 'telephony' channel. */
if (document.hidden &&
!((this._channel === 'telephony') && callIsActive)) {
this._trashAudio();
}
}).bind(this);
window.addEventListener('visibilitychange', this._maybeTrashAudio);
telephony && telephony.addEventListener('callschanged',
this._maybeTrashAudio);
this._initialized = true;
},
/**
* Tears down the tone player and removes the registered event listeners,
* mostly used for unit-testing.
*/
teardown: function tp_teardown() {
var telephony = navigator.mozTelephony;
telephony && telephony.removeEventListener('callschanged',
this._maybeTrashAudio);
window.removeEventListener('visibilitychange', this._maybeTrashAudio);
this._reset();
this._initialized = false;
},
/**
* Reset all internal state to its default value.
*/
_reset: function tp_reset() {
this._audioContext = null;
this._channel = null;
this._gainNode = null;
this._playingNodes = [];
this._maybeTrashAudio = null;
},
_ensureAudio: function tp_ensureAudio() {
if (this._audioContext || !this._initialized) {
return;
}
this._audioContext = new AudioContext();
},
_trashAudio: function tp_trashAudio() {
this.stop();
this._audioContext = null;
},
// Pass 0.0 for |when| to play as soon as possible.
// Pass 0.0 for |duration| to make the tone play until stop is called.
_startAt: function tp_startAt(frequencies, when, duration) {
var context = this._audioContext;
var sampleRate = context.sampleRate;
var envelope =
context.createBuffer(1, (duration ? duration : 0.05) * sampleRate,
sampleRate);
// ADSR
for (var i = 0; i < envelope.length; i++) {
var factor = kToneVolume;
var t = i / sampleRate;
if (t <= kAttackDuration) {
factor = t / kAttackDuration;
} else if (t - kAttackDuration <= kDecayDuration) {
factor = 1.0 - (1.0 - kToneVolume) *
(t - kAttackDuration) / kDecayDuration;
}
if (!duration) {
// The envelope buffer contains the difference from the sustain value
factor -= kToneVolume;
} else if (t > duration - kReleaseDuration) {
factor *= (duration - t) / kReleaseDuration;
}
envelope.getChannelData(0)[i] = factor * kMasterVolume;
}
var gainNode = context.createGain();
gainNode.connect(context.destination);
if (!duration) {
// For long presses, the gainNode will be used to release the tone.
this._gainNode = gainNode;
}
var envelopeNode = context.createBufferSource();
envelopeNode.buffer = envelope;
envelopeNode.start(when);
envelopeNode.connect(gainNode.gain);
// Set the gain which will be summed with the envelope buffer values
// and will be the constant gain at the end of the tone envelope. For
// tones with duration, the envelope covers the entire tone, so the gain
// at the end is zero. For tones with no duration, the envelope covers
// only the attack and delay phases after which the gain is the sustain
// value.
gainNode.gain.setValueAtTime(duration ? 0.0 : kToneVolume * kMasterVolume,
0.0);
for (i = 0; i < frequencies.length; ++i) {
var oscNode = this._audioContext.createOscillator();
oscNode.type = 'sine';
oscNode.frequency.value = frequencies[i];
oscNode.start(when);
if (duration) {
// If starting immediately, then add some extra time to allow the tone
// to start, so that the tone doesn't stop short of the end of the
// envelope.
oscNode.stop(Math.max(when, context.currentTime + 0.5) + duration);
} else {
this._playingNodes.push(oscNode);
}
oscNode.connect(gainNode);
}
},
start: function tp_start(frequencies, shortPress) {
this._ensureAudio();
this._startAt(frequencies, 0, shortPress ? kShortPressDuration : 0);
},
stop: function tp_stop() {
if (!this._gainNode) {
return;
}
var context = this._audioContext;
var sampleRate = context.sampleRate;
var gain = this._gainNode.gain;
this._gainNode = null;
var ramp =
context.createBuffer(1, kReleaseDuration * sampleRate, sampleRate);
for (var i = 0; i < ramp.length; i++) {
ramp.getChannelData(0)[i] =
(ramp.length - i - 1) / ramp.length * kToneVolume * kMasterVolume;
}
var rampNode = context.createBufferSource();
rampNode.buffer = ramp;
rampNode.start();
rampNode.connect(gain);
// Change the current base gain from kToneVolume * kMasterVolume to 0,
// cancelling the initial change from adding the ramp.
gain.setValueAtTime(0.0, 0.0);
// Stop the oscillators some time after the release ramp reaches 0.
// Some extra time is included to allow the release to start.
while (this._playingNodes.length) {
this._playingNodes.pop().
stop(context.currentTime + kReleaseDuration + 0.5);
}
},
// Takes an array of steps, a step being:
// - frequency for channel 1
// - frequency for channel 2
// - duration
playSequence: function tp_playSequence(sequence) {
this._ensureAudio();
// AudioContext.currentTime is the last time received on the main thread
// from the audio graph. Trying to start a tone at currentTime will not
// start playing until this main thread returns to the event loop and
// sends a message to the audio graph thread. Allow a little time for
// this. The way Gecko processes audio in chunks of 30ms means this
// should be at least 60 ms. Experiments on Buri indicate that in
// practice this delay is 165 to 190 ms, much of which is the code that
// runs before returning to the event loop.
var when = this._audioContext.currentTime + 0.2;
for (var index = 0; index < sequence.length; ++index) {
var step = sequence[index];
var frequencies = step.slice(0, 2);
var duration = step[2] / 1000;
this._startAt(frequencies, when, duration);
when += duration;
}
}
};