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",
"version": "2022.2.3",
"version": "2022.2.4",
"private": true,
"description": "Helps run in-browser neuroscience, psychology, and psychophysics experiments",
"license": "MIT",

View File

@ -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.4");
// hide the initialisation message:
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.
*
* @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.
*

View File

@ -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.</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 {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,15 +147,22 @@ 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))
{
throw {
origin: "Sound.setSound",
context: "when setting the sound",
error: "the argument should be an instance of the Sound class.",
};
}
this._setAttribute("value", sound.value, log);
if (typeof this._player !== "undefined")
@ -163,22 +170,85 @@ export class Sound extends PsychObject
this._player = this._player.constructor.accept(this);
}
// Be fluent?
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.setTone(value, octave);
}
else
{
this._player = new TonePlayer(Object.assign(args, playerArgs));
}
return;
}
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)
{

View File

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

View File

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

View File

@ -8,6 +8,7 @@
*/
import { SoundPlayer } from "./SoundPlayer.js";
import { Howl } from "howler";
/**
* <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.
*
* @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.
*