1
0
mirror of https://github.com/psychopy/psychojs.git synced 2025-05-10 18:50:54 +00:00

Merge pull request #529 from apitiot/2022.2.4

ENH Sound values can be changed without instantiating a new SoundPlayer
This commit is contained in:
Alain Pitiot 2022-09-15 13:35:32 +02:00 committed by GitHub
commit f8a2b10e88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 212 additions and 101 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "psychojs", "name": "psychojs",
"version": "2022.2.3", "version": "2022.2.4",
"private": true, "private": true,
"description": "Helps run in-browser neuroscience, psychology, and psychophysics experiments", "description": "Helps run in-browser neuroscience, psychology, and psychophysics experiments",
"license": "MIT", "license": "MIT",

View File

@ -170,7 +170,7 @@ export class PsychoJS
} }
this.logger.info("[PsychoJS] Initialised."); this.logger.info("[PsychoJS] Initialised.");
this.logger.info("[PsychoJS] @version 2022.2.1"); this.logger.info("[PsychoJS] @version 2022.2.4");
// hide the initialisation message: // hide the initialisation message:
const root = document.getElementById("root"); const root = document.getElementById("root");

View File

@ -53,28 +53,21 @@ export class AudioClipPlayer extends SoundPlayer
/** /**
* Determine whether this player can play the given sound. * Determine whether this player can play the given sound.
* *
* @param {module:sound.Sound} sound - the sound object, which should be an AudioClip * @param {module:core.PsychoJS} psychoJS - the PsychoJS instance
* @return {Object|undefined} an instance of AudioClipPlayer if sound is an AudioClip or undefined otherwise * @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: return { audioClip: value };
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;
} }
// AudioClipPlayer is not an appropriate player for the given sound: // AudioClipPlayer is not an appropriate player for the given sound:
return undefined; return false;
} }
/** /**
@ -129,6 +122,23 @@ export class AudioClipPlayer extends SoundPlayer
// TODO // 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. * Start playing the sound.
* *

View File

@ -2,7 +2,7 @@
/** /**
* Sound stimulus. * Sound stimulus.
* *
* @author Alain Pitiot * @author Alain Pitiot, Nikita Agafonov
* @version 2022.2.3 * @version 2022.2.3
* @copyright (c) 2017-2020 Ilixa Ltd. (http://ilixa.com) (c) 2020-2022 Open Science Tools Ltd. (https://opensciencetools.org) * @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 * @license Distributed under the terms of the MIT License
@ -74,7 +74,6 @@ export class Sound extends PsychObject
this._player = undefined; this._player = undefined;
this._addAttribute("win", win); this._addAttribute("win", win);
this._addAttribute("value", value);
this._addAttribute("octave", octave); this._addAttribute("octave", octave);
this._addAttribute("secs", secs); this._addAttribute("secs", secs);
this._addAttribute("startTime", startTime); this._addAttribute("startTime", startTime);
@ -84,8 +83,9 @@ export class Sound extends PsychObject
this._addAttribute("loops", loops); this._addAttribute("loops", loops);
this._addAttribute("autoLog", autoLog); this._addAttribute("autoLog", autoLog);
// identify an appropriate player: // note: setValue will identify the appropriate SoundPlayer and possibly instantiate it
this._getPlayer(); // 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; 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.</p> * Repeat calls to play may results in the sounds being played on top of each other.</p>
* *
* @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 {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) play(loops, log = true)
{ {
@ -109,7 +109,7 @@ export class Sound extends PsychObject
* Stop playing the sound immediately. * Stop playing the sound immediately.
* *
* @param {Object} options * @param {Object} options
* @param {boolean} [options.log= true] - whether or not to log * @param {boolean} [options.log= true] - whether to log
*/ */
stop({ stop({
log = true, log = true,
@ -134,7 +134,7 @@ export class Sound extends PsychObject
* *
* @param {number} volume - the volume (values should be between 0 and 1) * @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} [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) 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 {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) 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? playerArgs = TrackPlayer.accept(this._psychoJS, value);
return this; 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 { throw {
origin: "Sound.setSound", origin: "Sound.setValue",
context: "when replacing the current sound", context: "when setting the sound value",
error: "invalid input, need an instance of the Sound class.", error: "could not find an appropriate player.",
}; };
} }
/** /**
* Set the number of loops. * 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 {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) setLoops(loops = 0, log = true)
{ {
@ -194,7 +264,7 @@ export class Sound extends PsychObject
* Set the duration (in seconds) * 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 {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) setSecs(secs = 0.5, log = true)
{ {

View File

@ -26,22 +26,6 @@ export class SoundPlayer extends PsychObject
super(psychoJS); 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. * Start playing the sound.
* *

View File

@ -24,7 +24,7 @@ export class TonePlayer extends SoundPlayer
* @memberOf module:sound * @memberOf module:sound
* @param {Object} options * @param {Object} options
* @param {module:core.PsychoJS} options.psychoJS - the PsychoJS instance * @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 {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.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. * @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({ constructor({
psychoJS, psychoJS,
note = "C4", note = "C4",
duration_s = 0.5, secs = 0.5,
volume = 1.0, volume = 1.0,
loops = 0, loops = 0,
soundLibrary = TonePlayer.SoundLibrary.TONE_JS, soundLibrary = TonePlayer.SoundLibrary.TONE_JS,
@ -42,7 +42,7 @@ export class TonePlayer extends SoundPlayer
super(psychoJS); super(psychoJS);
this._addAttribute("note", note); this._addAttribute("note", note);
this._addAttribute("duration_s", duration_s); this._addAttribute("duration_s", secs);
this._addAttribute("volume", volume); this._addAttribute("volume", volume);
this._addAttribute("loops", loops); this._addAttribute("loops", loops);
this._addAttribute("soundLibrary", soundLibrary); this._addAttribute("soundLibrary", soundLibrary);
@ -66,25 +66,21 @@ export class TonePlayer extends SoundPlayer
* <p>Note: if TonePlayer accepts the sound but Tone.js is not available, e.g. if the browser is IE11, * <p>Note: if TonePlayer accepts the sound but Tone.js is not available, e.g. if the browser is IE11,
* we throw an exception.</p> * we throw an exception.</p>
* *
* @param {module:sound.Sound} sound - the sound * @param {string|number} value - potential frequency or note
* @return {Object|undefined} an instance of TonePlayer that can play the given sound or undefined otherwise * @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 the sound's value is an integer, we interpret it as a frequency:
if (isNumeric(sound.value)) if (isNumeric(value))
{ {
return new TonePlayer({ return { note: value }
psychoJS: sound.psychoJS,
note: sound.value,
duration_s: sound.secs,
volume: sound.volume,
loops: sound.loops,
});
} }
// if the sound's value is a string, we check whether it is a note: // 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: // mapping between the PsychoPY notes and the standard ones:
let psychopyToToneMap = new Map(); let psychopyToToneMap = new Map();
@ -96,21 +92,15 @@ export class TonePlayer extends SoundPlayer
} }
// check whether the sound's value is a recognised note: // 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") if (typeof note !== "undefined")
{ {
return new TonePlayer({ return { note: note + octave };
psychoJS: sound.psychoJS,
note: note + sound.octave,
duration_s: sound.secs,
volume: sound.volume,
loops: sound.loops,
});
} }
} }
// TonePlayer is not an appropriate player for the given sound: // the value does not seem to correspond to a tone we can play:
return undefined; return false;
} }
/** /**
@ -126,11 +116,11 @@ export class TonePlayer extends SoundPlayer
/** /**
* Set the duration of the tone. * 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. * Start playing the sound.
* *

View File

@ -8,6 +8,7 @@
*/ */
import { SoundPlayer } from "./SoundPlayer.js"; import { SoundPlayer } from "./SoundPlayer.js";
import { Howl } from "howler";
/** /**
* <p>This class handles the playback of sound tracks.</p> * <p>This class handles the playback of sound tracks.</p>
@ -54,34 +55,43 @@ export class TrackPlayer extends SoundPlayer
/** /**
* Determine whether this player can play the given sound. * 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 * @param {string} value - the sound, which should be the name of an audio resource file
* file * @return {boolean} whether or not value is supported
* @return {Object|undefined} an instance of TrackPlayer that can play the given track or undefined otherwise
*/ */
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 value === "string")
if (typeof sound.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") if (typeof howl !== "undefined")
{ {
// build the player: return { howl };
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;
} }
} }
// TonePlayer is not an appropriate player for the given sound: // 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. * Start playing the sound.
* *