/** * Tone Player. * * @author Alain Pitiot * @version 3.0.0b11 * @copyright (c) 2018 Ilixa Ltd. ({@link http://ilixa.com}) * @license Distributed under the terms of the MIT License */ import { SoundPlayer } from './SoundPlayer'; /** *

This class handles the playing of tones.

* * @name module:sound.TonePlayer * @class * @extends SoundPlayer * @param {Object} options * @param {PsychoJS} options.psychoJS - the PsychoJS instance * @param {number} [options.duration_s= 0.5] - duration of the tone (in seconds) * @param {string|number} [options.note= 'C4'] - note (if string) or frequency (if number) * @param {number} [options.volume= 1.0] - volume of the tone (must be between 0 and 1.0) * @param {number} [options.loops= 0] - how many times to repeat the tone after it has played once. If loops == -1, the tone will repeat indefinitely until stopped. */ export class TonePlayer extends SoundPlayer { constructor({ psychoJS, note = 'C4', duration_s = 0.5, volume = 1.0, loops = 0 } = {}) { super(psychoJS); this._addAttributes(TonePlayer, note, duration_s, volume, loops); // create a synth: we use a triangular oscillator with hardly any envelope: this._synthOtions = { oscillator: { type: 'triangle' }, envelope: { attack: 0.001, decay: 0.001, sustain: 1, release: 0.001 } }; this._synth = new Tone.Synth(this._synthOtions); // connect it to a volume node: this._volumeNode = new Tone.Volume(-60 + volume * 66); this._synth.connect(this._volumeNode); // connect the volume node to the master output: this._volumeNode.toMaster(); // tonejs Loop: this._toneLoop = null; } /** * Determine whether this player can play the given sound. * * @name module:sound.TonePlayer.accept * @function * @static * @public * @param {module:sound.Sound} - the sound * @return {Object|undefined} an instance of TonePlayer that can play the given sound or undefined otherwise */ static accept(sound) { // if the sound's value is an integer, we interpret it as a frequency: if ($.isNumeric(sound.value)) { // build the player: const player = new TonePlayer({ psychoJS: sound.psychoJS, note: sound.value, duration_s: sound.secs, volume: sound.volume, loops: sound.loops }); return player; } // if the sound's value is a string, we check whether it is a note: if (typeof sound.value === 'string') { // mapping between the PsychoPY notes and the standard ones: let psychopyToToneMap = new Map(); for (const note of ['A', 'B', 'C', 'D', 'E', 'F', 'G']) { psychopyToToneMap.set(note, note); psychopyToToneMap.set(note + 'fl', note + 'b'); psychopyToToneMap.set(note + 'sh', note + '#'); } // check whether the sound's value is a recognised note: const note = psychopyToToneMap.get(sound.value); if (typeof note !== 'undefined') { // build the player: const player = new TonePlayer({ psychoJS: sound.psychoJS, note: note + sound.octave, duration_s: sound.secs, volume: sound.volume, loops: sound.loops }); return player; } } // TonePlayer is not an appropriate player for the given sound: return undefined; } /** * Get the duration of the sound. * * @name module:sound.TonePlayer#getDuration * @function * @public * @return {number} the duration of the sound, in seconds */ getDuration() { return this.duration_s; } /** * Set the number of loops. * * @name module:sound.TonePlayer#setLoops * @function * @public * @param {number} loops - how many times to repeat the track after it has played once. If loops == -1, the track will repeat indefinitely until stopped. */ setLoops(loops) { this._loops = loops; } /** * Set the volume of the tone. * * @name module:sound.TonePlayer#setVolume * @function * @public * @param {Integer} volume - the volume of the tone * @param {booleam} [mute= false] - whether or not to mute the tone */ setVolume(volume, mute = false) { this._volume = volume; if (typeof this._volumeNode !== 'undefined') { this._volumeNode.mute = mute; this._synth.volume.value = -60 + volume * 66; } } /** * Start playing the sound. * * @name module:sound.TonePlayer#play * @function * @public * @param {boolean} [loops] - how many times to repeat the sound after it has played once. If loops == -1, the sound will repeat indefinitely until stopped. */ play(loops) { if (typeof loops !== 'undefined') this._loops = loops; const self = this; const callback = time => { self._synth.triggerAttackRelease(self._note, self.duration_s, Tone.now()); }; if (this.loops == 0) this._toneId = Tone.Transport.scheduleOnce(callback, Tone.now()); else if (this.loops == -1) this._toneId = Tone.Transport.scheduleRepeat( callback, this.duration_s, Tone.now(), Tone.Infinity ); else this._toneId = Tone.Transport.scheduleRepeat( callback, this.duration_s, Tone.now(), this.duration_s * (this._loops+1) ); } /** * Stop playing the sound immediately. * * @name module:sound.TonePlayer#stop * @function * @public */ stop() { if (this._toneId) Tone.Transport.clear(this._toneId); } } // Start the Tone Transport Tone.Transport.start(Tone.now());