From 442b9a079f3265ae0dcdba25298e83720639dd73 Mon Sep 17 00:00:00 2001 From: Alain Pitiot Date: Thu, 15 Sep 2022 10:43:42 +0200 Subject: [PATCH] ENH Sound values can be changed without instantiating a new SoundPlayer --- src/core/PsychoJS.js | 2 +- src/sound/AudioClipPlayer.js | 42 ++++++++----- src/sound/Sound.js | 110 ++++++++++++++++++++++++++++------- src/sound/SoundPlayer.js | 16 ----- src/sound/TonePlayer.js | 63 +++++++++++--------- src/sound/TrackPlayer.js | 78 +++++++++++++++++++------ 6 files changed, 211 insertions(+), 100 deletions(-) diff --git a/src/core/PsychoJS.js b/src/core/PsychoJS.js index aabd33c..65ac719 100644 --- a/src/core/PsychoJS.js +++ b/src/core/PsychoJS.js @@ -170,7 +170,7 @@ export class PsychoJS } this.logger.info("[PsychoJS] Initialised."); - this.logger.info("[PsychoJS] @version 2022.2.1"); + this.logger.info("[PsychoJS] @version 2022.2.3"); // hide the initialisation message: const root = document.getElementById("root"); diff --git a/src/sound/AudioClipPlayer.js b/src/sound/AudioClipPlayer.js index 10dd99b..81792f6 100644 --- a/src/sound/AudioClipPlayer.js +++ b/src/sound/AudioClipPlayer.js @@ -53,28 +53,21 @@ export class AudioClipPlayer extends SoundPlayer /** * Determine whether this player can play the given sound. * - * @param {module:sound.Sound} sound - the sound object, which should be an AudioClip - * @return {Object|undefined} an instance of AudioClipPlayer if sound is an AudioClip or undefined otherwise + * @param {module:core.PsychoJS} psychoJS - the PsychoJS instance + * @param {string} value - the sound value, which should be the name of an audio resource + * file + * @return {Object|boolean} argument needed to instantiate a AudioClipPlayer that can play the given sound + * or false otherwise */ - static accept(sound) + static accept(psychoJS, value) { - if (sound.value instanceof AudioClip) + if (value instanceof AudioClip) { - // build the player: - const player = new AudioClipPlayer({ - psychoJS: sound.psychoJS, - audioClip: sound.value, - startTime: sound.startTime, - stopTime: sound.stopTime, - stereo: sound.stereo, - loops: sound.loops, - volume: sound.volume, - }); - return player; + return { audioClip: value }; } // AudioClipPlayer is not an appropriate player for the given sound: - return undefined; + return false; } /** @@ -129,6 +122,23 @@ export class AudioClipPlayer extends SoundPlayer // TODO } + /** + * Set the audio clip. + * + * @param {Object} options.audioClip - the module:sound.AudioClip. + */ + setAudioClip(audioClip) + { + if (audioClip instanceof AudioClip) + { + if (this._audioClip !== undefined) + { + this.stop(); + } + this._audioClip = audioClip; + } + } + /** * Start playing the sound. * diff --git a/src/sound/Sound.js b/src/sound/Sound.js index cf4ef45..084c9d6 100644 --- a/src/sound/Sound.js +++ b/src/sound/Sound.js @@ -2,7 +2,7 @@ /** * Sound stimulus. * - * @author Alain Pitiot + * @author Alain Pitiot, Nikita Agafonov * @version 2022.2.3 * @copyright (c) 2017-2020 Ilixa Ltd. (http://ilixa.com) (c) 2020-2022 Open Science Tools Ltd. (https://opensciencetools.org) * @license Distributed under the terms of the MIT License @@ -74,7 +74,6 @@ export class Sound extends PsychObject this._player = undefined; this._addAttribute("win", win); - this._addAttribute("value", value); this._addAttribute("octave", octave); this._addAttribute("secs", secs); this._addAttribute("startTime", startTime); @@ -84,8 +83,9 @@ export class Sound extends PsychObject this._addAttribute("loops", loops); this._addAttribute("autoLog", autoLog); - // identify an appropriate player: - this._getPlayer(); + // note: setValue will identify the appropriate SoundPlayer and possibly instantiate it + // consequently _addAtribute("value") needs to be the last one so the other attributes are already set + this._addAttribute("value", value); this.status = PsychoJS.Status.NOT_STARTED; } @@ -97,7 +97,7 @@ export class Sound extends PsychObject * Repeat calls to play may results in the sounds being played on top of each other.

* * @param {number} loops how many times to repeat the sound after it plays once. If loops == -1, the sound will repeat indefinitely until stopped. - * @param {boolean} [log= true] whether or not to log + * @param {boolean} [log= true] whether to log */ play(loops, log = true) { @@ -109,7 +109,7 @@ export class Sound extends PsychObject * Stop playing the sound immediately. * * @param {Object} options - * @param {boolean} [options.log= true] - whether or not to log + * @param {boolean} [options.log= true] - whether to log */ stop({ log = true, @@ -134,7 +134,7 @@ export class Sound extends PsychObject * * @param {number} volume - the volume (values should be between 0 and 1) * @param {boolean} [mute= false] - whether or not to mute the sound - * @param {boolean} [log= true] - whether of not to log + * @param {boolean} [log= true] - whether to log */ setVolume(volume, mute = false, log = true) { @@ -147,38 +147,108 @@ export class Sound extends PsychObject } /** - * Set the sound value on demand past initialisation. + * Set the sound value. * * @param {object} sound - a sound instance to replace the current one - * @param {boolean} [log= true] - whether or not to log + * @param {boolean} [log= true] - whether to log */ setSound(sound, log = true) { - if (sound instanceof Sound) + if (!(sound instanceof Sound)) { - this._setAttribute("value", sound.value, log); + throw { + origin: "Sound.setSound", + context: "when setting the sound", + error: "the argument should be an instance of the Sound class.", + }; + } - if (typeof this._player !== "undefined") + this._setAttribute("value", sound.value, log); + + if (typeof this._player !== "undefined") + { + this._player = this._player.constructor.accept(this); + } + + return this; + } + + /** + * Set the sound value. + * + * @param {number|string} [value = "C"] - the sound value + * @param {number} [octave = 4] - the octave corresponding to the tone (if applicable) + * @param {boolean} [log=true] - whether to log + */ + setValue(value = "C", octave = 4, log = true) + { + this._setAttribute("value", value, log); + + const args = { + psychoJS: this._psychoJS, + stereo: this._stereo, + volume: this._volume, + loops: this._loops, + startTime: this._startTime, + stopTime: this._stopTime, + secs: this._secs + } + + let playerArgs = TonePlayer.accept(value, octave); + if (typeof playerArgs !== "undefined") + { + if (this._player instanceof TonePlayer) { - this._player = this._player.constructor.accept(this); + this._player.setTone(value, octave); } + else + { + this._player = new TonePlayer(Object.assign(args, playerArgs)); + } + return; + } - // Be fluent? - return this; + playerArgs = TrackPlayer.accept(this._psychoJS, value); + if (playerArgs !== false) + { + if (this._player instanceof TrackPlayer) + { + this._player.setTrack(value); + } + else + { + this._player = new TrackPlayer(Object.assign(args, playerArgs)); + } + return; + } + + playerArgs = AudioClipPlayer.accept(this._psychoJS, value); + if (typeof playerArgs !== "undefined") + { + if (this._player instanceof AudioClipPlayer) + { + this._player.setAudioClip(value); + } + else + { + this._player = new AudioClipPlayer(Object.assign(args, playerArgs)); + } + return; } throw { - origin: "Sound.setSound", - context: "when replacing the current sound", - error: "invalid input, need an instance of the Sound class.", + origin: "Sound.setValue", + context: "when setting the sound value", + error: "could not find an appropriate player.", }; + } /** * Set the number of loops. * * @param {number} [loops=0] - how many times to repeat the sound after it has played once. If loops == -1, the sound will repeat indefinitely until stopped. - * @param {boolean} [log=true] - whether of not to log + * @param {boolean} [log=true] - whether to log */ setLoops(loops = 0, log = true) { @@ -194,7 +264,7 @@ export class Sound extends PsychObject * Set the duration (in seconds) * * @param {number} [secs=0.5] - duration of the tone (in seconds) If secs == -1, the sound will play indefinitely. - * @param {boolean} [log=true] - whether or not to log + * @param {boolean} [log=true] - whether to log */ setSecs(secs = 0.5, log = true) { diff --git a/src/sound/SoundPlayer.js b/src/sound/SoundPlayer.js index 512c365..77d610c 100644 --- a/src/sound/SoundPlayer.js +++ b/src/sound/SoundPlayer.js @@ -26,22 +26,6 @@ export class SoundPlayer extends PsychObject super(psychoJS); } - /** - * Determine whether this player can play the given sound. - * - * @abstract - * @param {module:sound.Sound} - the sound - * @return {Object|undefined} an instance of the SoundPlayer that can play the sound, or undefined if none could be found - */ - static accept(sound) - { - throw { - origin: "SoundPlayer.accept", - context: "when evaluating whether this player can play a given sound", - error: "this method is abstract and should not be called.", - }; - } - /** * Start playing the sound. * diff --git a/src/sound/TonePlayer.js b/src/sound/TonePlayer.js index 488016a..75c7d08 100644 --- a/src/sound/TonePlayer.js +++ b/src/sound/TonePlayer.js @@ -24,7 +24,7 @@ export class TonePlayer extends SoundPlayer * @memberOf module:sound * @param {Object} options * @param {module:core.PsychoJS} options.psychoJS - the PsychoJS instance - * @param {number} [options.duration_s= 0.5] - duration of the tone (in seconds). If duration_s == -1, the sound will play indefinitely. + * @param {number} [options.secs= 0.5] - duration of the tone (in seconds). If secs == -1, the sound will play indefinitely. * @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. @@ -32,7 +32,7 @@ export class TonePlayer extends SoundPlayer constructor({ psychoJS, note = "C4", - duration_s = 0.5, + secs = 0.5, volume = 1.0, loops = 0, soundLibrary = TonePlayer.SoundLibrary.TONE_JS, @@ -42,7 +42,7 @@ export class TonePlayer extends SoundPlayer super(psychoJS); this._addAttribute("note", note); - this._addAttribute("duration_s", duration_s); + this._addAttribute("duration_s", secs); this._addAttribute("volume", volume); this._addAttribute("loops", loops); this._addAttribute("soundLibrary", soundLibrary); @@ -66,25 +66,21 @@ export class TonePlayer extends SoundPlayer *

Note: if TonePlayer accepts the sound but Tone.js is not available, e.g. if the browser is IE11, * we throw an exception.

* - * @param {module:sound.Sound} sound - the sound - * @return {Object|undefined} an instance of TonePlayer that can play the given sound or undefined otherwise + * @param {string|number} value - potential frequency or note + * @param {number} octave - the octave corresponding to the tone + * @return {Object|boolean} argument needed to instantiate a TonePlayer that can play the given sound + * or false otherwise */ - static accept(sound) + static accept(value, octave) { // if the sound's value is an integer, we interpret it as a frequency: - if (isNumeric(sound.value)) + if (isNumeric(value)) { - return new TonePlayer({ - psychoJS: sound.psychoJS, - note: sound.value, - duration_s: sound.secs, - volume: sound.volume, - loops: sound.loops, - }); + return { note: value } } // if the sound's value is a string, we check whether it is a note: - if (typeof sound.value === "string") + if (typeof value === "string") { // mapping between the PsychoPY notes and the standard ones: let psychopyToToneMap = new Map(); @@ -96,21 +92,15 @@ export class TonePlayer extends SoundPlayer } // check whether the sound's value is a recognised note: - const note = psychopyToToneMap.get(sound.value); + const note = psychopyToToneMap.get(value); if (typeof note !== "undefined") { - return new TonePlayer({ - psychoJS: sound.psychoJS, - note: note + sound.octave, - duration_s: sound.secs, - volume: sound.volume, - loops: sound.loops, - }); + return { note: note + octave }; } } - // TonePlayer is not an appropriate player for the given sound: - return undefined; + // the value does not seem to correspond to a tone we can play: + return false; } /** @@ -126,11 +116,11 @@ export class TonePlayer extends SoundPlayer /** * Set the duration of the tone. * - * @param {number} duration_s - the duration of the tone (in seconds) If duration_s == -1, the sound will play indefinitely. + * @param {number} secs - the duration of the tone (in seconds) If secs == -1, the sound will play indefinitely. */ - setDuration(duration_s) + setDuration(secs) { - this.duration_s = duration_s; + this.duration_s = secs; } /** @@ -172,6 +162,23 @@ export class TonePlayer extends SoundPlayer } } + /** + * Set the note for tone. + * + * @param {string|number} value - potential frequency or note + * @param {number} octave - the octave corresponding to the tone + */ + setTone(value = "C", octave = 4) + { + const args = TonePlayer.accept(value, octave); + this._note = args.note; + + if (typeof this._synth !== "undefined") + { + this._synth.setNote(this._note); + } + } + /** * Start playing the sound. * diff --git a/src/sound/TrackPlayer.js b/src/sound/TrackPlayer.js index 7451ae6..dd9774c 100644 --- a/src/sound/TrackPlayer.js +++ b/src/sound/TrackPlayer.js @@ -8,6 +8,7 @@ */ import { SoundPlayer } from "./SoundPlayer.js"; +import { Howl } from "howler"; /** *

This class handles the playback of sound tracks.

@@ -54,34 +55,43 @@ export class TrackPlayer extends SoundPlayer /** * Determine whether this player can play the given sound. * - * @param {module:sound.Sound} sound - the sound, which should be the name of an audio resource - * file - * @return {Object|undefined} an instance of TrackPlayer that can play the given track or undefined otherwise + * @param {string} value - the sound, which should be the name of an audio resource file + * @return {boolean} whether or not value is supported */ - static accept(sound) + static checkValueSupport (value) { - // if the sound's value is a string, we check whether it is the name of a resource: - if (typeof sound.value === "string") + if (typeof value === "string") { - const howl = sound.psychoJS.serverManager.getResource(sound.value); + return true; + } + + return false; + } + + /** + * Determine whether this player can play the given sound. + * + * @param {module:core.PsychoJS} psychoJS - the PsychoJS instance + * @param {string} value - the sound value, which should be the name of an audio resource + * file + * @return {Object|boolean} argument needed to instantiate a TrackPlayer that can play the given sound + * or false otherwise + */ + static accept(psychoJS, value) + { + // value should be a string: + if (typeof value === "string") + { + // check whether the value is the name of a resource: + const howl = psychoJS.serverManager.getResource(value); if (typeof howl !== "undefined") { - // build the player: - const player = new TrackPlayer({ - psychoJS: sound.psychoJS, - howl: howl, - startTime: sound.startTime, - stopTime: sound.stopTime, - stereo: sound.stereo, - loops: sound.loops, - volume: sound.volume, - }); - return player; + return { howl }; } } // TonePlayer is not an appropriate player for the given sound: - return undefined; + return false; } /** @@ -142,6 +152,36 @@ export class TrackPlayer extends SoundPlayer } } + /** + * Set new track to play. + * + * @param {Object|string} track - a track resource name or Howl object (see {@link https://howlerjs.com/}) + */ + setTrack(track) + { + let newHowl = undefined; + + if (typeof track === "string") + { + newHowl = this.psychoJS.serverManager.getResource(track); + } + else if (track instanceof Howl) + { + newHowl = track; + } + + if (newHowl !== undefined) + { + this._howl.once("fade", (id) => + { + this._howl.stop(id); + this._howl.off("end"); + this._howl = newHowl; + }); + this._howl.fade(this._howl.volume(), 0, 17, this._id); + } + } + /** * Start playing the sound. *