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:
commit
f8a2b10e88
@ -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",
|
||||
|
@ -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");
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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.
|
||||
*
|
||||
|
Loading…
Reference in New Issue
Block a user