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.
*