395 lines
13 KiB
JavaScript
395 lines
13 KiB
JavaScript
/*------------------------------------------------------------------------------
|
|
* Various tools for experiments
|
|
*------------------------------------------------------------------------------
|
|
* Requires jQuery, Semantic UI modal and icon, and the dbsplab.fun DataHandler
|
|
* (only to check if the sound level adjustment has been done already).
|
|
*----------------------------------------------------------------------------*/
|
|
|
|
// Latest version at https://git.web.rug.nl/dBSPL/jspsych-plugins/src/branch/main/js/tools.js
|
|
|
|
function is_browser_compatible(){
|
|
// Add here everything that needs to be tested for browser compatibility
|
|
if( (new Audio()).canPlayType('audio/mp3') != 'probably' )
|
|
return false;
|
|
|
|
/*
|
|
if( (new Audio()).canPlayType('audio/mpeg') != 'probably' )
|
|
return false;
|
|
*/
|
|
|
|
/*
|
|
if( (new Audio()).canPlayType('audio/wav') != 'probably' )
|
|
return false;
|
|
if( (new Audio()).canPlayType('audio/flac') != 'probably' )
|
|
return false;
|
|
*/
|
|
return true;
|
|
|
|
// for of
|
|
}
|
|
|
|
function show_error(msg, to="body", after=false)
|
|
{
|
|
if(after)
|
|
$("<div class='ui error message'>"+msg+"</div>").appendTo(to);
|
|
else
|
|
$("<div class='ui error message'>"+msg+"</div>").prependTo(to);
|
|
}
|
|
|
|
function sound_level_adjustment(sound_file, after_cb)
|
|
{
|
|
// Checks in the session if sound level adjustment has been performed for this
|
|
// experiment, and if not, shows a sound level adjustment dialog.
|
|
|
|
if(typeof after_cb==='undefined')
|
|
after_cb = function(){};
|
|
|
|
window.DataHandler.get_sound_level_adj(
|
|
// success
|
|
function(is_adjusted){
|
|
if(is_adjusted)
|
|
after_cb();
|
|
else
|
|
_make_sound_level_adjustment(sound_file, after_cb);
|
|
},
|
|
// error
|
|
show_error
|
|
);
|
|
}
|
|
|
|
// Sound level adjustment dialog internationalisation
|
|
var SLADi18n = {};
|
|
SLADi18n['title'] = {};
|
|
SLADi18n['title']['fr'] = "Réglage du volume";
|
|
SLADi18n['title']['en'] = "Sound level adjustment";
|
|
SLADi18n['title']['nl'] = "Geluidsvolume";
|
|
SLADi18n['title']['de'] = "Lautstärkeanpassung";
|
|
SLADi18n['intro'] = {};
|
|
SLADi18n['intro']['fr'] = "Il est conseillé de completer cette expérience dans un <b>environnement calme</b>, et de préférence en utilisant un <b>casque de bonne qualité</b>. Ajustez le volume de votre ordinateur de façon à ce que le son soit présenté à un niveau confortable, et gardez le volume identique pendant toute la durée de l'expérience.";
|
|
SLADi18n['intro']['en'] = "You are kindly asked to perform this experiment in a <b>calm environment</b>, and preferably using <b>good quality headphones</b>. Adjust the sound level on your computer so that the sound plays at a comfortable level, and keep the volume the same during the whole experiment.";
|
|
SLADi18n['intro']['nl'] = "U wordt vriendelijk verzocht om dit experiment in een <b>stille omgeving</b> uit te voeren en bij voorkeur een <b>koptelefoon van goede kwaliteit</b> te gebruiken. Pas het geluidsvolume op uw computer aan zodat het geluid op een comfortabel niveau wordt afgespeeld, en verander het geluidsniveau verder niet meer gedurende het experiment.";
|
|
SLADi18n['intro']['de'] = "Wir bitten Sie freundlich, dieses Experiment in einer ruhigen Umgebung und möglichst mit hochwertigen Kopfhörern durchzuführen. Stellen Sie die Lautstärke Ihres Computers so ein, dass der Klang in einer angenehmen Lautstärke wiedergegeben wird, und behalten Sie diese Einstellung während des gesamten Experiments bei.";
|
|
SLADi18n['loading'] = {};
|
|
SLADi18n['loading']['fr'] = "Chargement...";
|
|
SLADi18n['loading']['en'] = "Loading...";
|
|
SLADi18n['loading']['nl'] = "Bezig met laden...";
|
|
SLADi18n['loading']['de'] = "Wird geladen...";
|
|
SLADi18n['continue'] = {};
|
|
SLADi18n['continue']['fr'] = "Continuer";
|
|
SLADi18n['continue']['en'] = "Continue";
|
|
SLADi18n['continue']['nl'] = "Doorgaan";
|
|
SLADi18n['continue']['de'] = "Weiter";
|
|
SLADi18n['when-ready'] = {};
|
|
SLADi18n['when-ready']['fr'] = `Quand vous êtes prêt.e, cliquez sur "${SLADi18n['continue']['fr']}".`;
|
|
SLADi18n['when-ready']['en'] = `When you are ready, click on "${SLADi18n['continue']['en']}".`;
|
|
SLADi18n['when-ready']['nl'] = `Als u klaar bent, klik je op "${SLADi18n['continue']['nl']}".`;
|
|
SLADi18n['when-ready']['de'] = `Um fortzufahren, klicken Sie bitte auf "${SLADi18n['continue']['de']}".`;
|
|
|
|
|
|
function _make_sound_level_adjustment(sound_file, after_cb)
|
|
{
|
|
// The global LANG has to be defined
|
|
|
|
var eLANG = LANG;
|
|
// Fallback to English if lanugage is not supported
|
|
if(typeof SLADi18n['intro'][eLANG] === 'undefined'){
|
|
eLANG = 'en';
|
|
}
|
|
|
|
var snd;
|
|
var dialog = $(
|
|
"<div class='ui modal' id='sound_adjustment' style='max-width: 20em;'>"+
|
|
"<div class='header'>"+
|
|
SLADi18n['title'][eLANG]+
|
|
"</div>"+
|
|
"<div class='content'>"+
|
|
"<p>"+SLADi18n['intro'][eLANG]+"</p>"+
|
|
"<p style='text-align: center;'>"+
|
|
"<button class='ui huge icon button' id='play-pause'>"+
|
|
"<i class='asterisk loading icon'></i>"+
|
|
"</button>"+
|
|
"</p>"+
|
|
"</div>"+
|
|
"<div class='actions'>"+
|
|
"<button class='ui right labeled icon disabled ok button'>"+
|
|
"<i class='right arrow icon'></i>"+
|
|
SLADi18n['continue'][eLANG]+
|
|
"</button>"+
|
|
"</div>"+
|
|
"</div>").appendTo("body");
|
|
dialog.modal({
|
|
closable: false,
|
|
onApprove: function(elmt){
|
|
snd.pause();
|
|
snd = null;
|
|
},
|
|
onHidden: function(){
|
|
window.DataHandler.set_sound_level_adj(after_cb, show_error);
|
|
}
|
|
}).modal('show');
|
|
$("#sound_adjustment").css("max-width", "25em");
|
|
$("#sound_adjustment .content").css("box-sizing", "border-box");
|
|
|
|
function load_sound(snd_file)
|
|
{
|
|
snd = new Audio(snd_file);
|
|
snd.loop = true;
|
|
snd.autoplay = false;
|
|
snd.volume = 1;
|
|
|
|
snd.canplaythrough_1st = true;
|
|
|
|
snd.addEventListener("canplaythrough", function(){
|
|
if(this.canplaythrough_1st){
|
|
$('#play-pause i.icon').removeClass('asterisk loading').addClass("play");
|
|
$('#sound_adjustment').find(".ok.button").removeClass('disabled');
|
|
$('#play-pause').click(function() {
|
|
if($(this).children("i.icon").hasClass("play"))
|
|
snd.play();
|
|
else
|
|
snd.pause();
|
|
$(this).children("i.icon").toggleClass("play pause");
|
|
});
|
|
this.canplaythrough_1st = false;
|
|
}
|
|
});
|
|
|
|
snd.load();
|
|
}
|
|
|
|
if(typeof sound_file === 'string' || sound_file instanceof String)
|
|
{
|
|
load_sound(sound_file);
|
|
}
|
|
else if(typeof sound_file === 'object')
|
|
{
|
|
// This must be VT query
|
|
vt(sound_file, load_sound, show_error);
|
|
}
|
|
else
|
|
{
|
|
show_error("The sound that was passed for adjustment is not valid...: "+sound_file);
|
|
}
|
|
|
|
}
|
|
|
|
// Some polyfills for IE
|
|
|
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith
|
|
if(!String.prototype.startsWith) {
|
|
Object.defineProperty(String.prototype, 'startsWith', {
|
|
value: function(search, rawPos) {
|
|
var pos = rawPos > 0 ? rawPos|0 : 0;
|
|
return this.substring(pos, pos + search.length) === search;
|
|
}
|
|
});
|
|
}
|
|
|
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sign
|
|
if(!Math.sign) {
|
|
Math.sign = function(x) {
|
|
return ((x > 0) - (x < 0)) || +x;
|
|
};
|
|
}
|
|
|
|
if(!Array.prototype.fill) {
|
|
Object.defineProperty(Array.prototype, 'fill', {
|
|
value: function(value) {
|
|
|
|
// Steps 1-2.
|
|
if(this == null) {
|
|
throw new TypeError('this is null or not defined');
|
|
}
|
|
|
|
var O = Object(this);
|
|
|
|
// Steps 3-5.
|
|
var len = O.length >>> 0;
|
|
|
|
// Steps 6-7.
|
|
var start = arguments[1];
|
|
var relativeStart = start >> 0;
|
|
|
|
// Step 8.
|
|
var k = relativeStart < 0 ?
|
|
Math.max(len + relativeStart, 0) :
|
|
Math.min(relativeStart, len);
|
|
|
|
// Steps 9-10.
|
|
var end = arguments[2];
|
|
var relativeEnd = end === undefined ?
|
|
len : end >> 0;
|
|
|
|
// Step 11.
|
|
var finalValue = relativeEnd < 0 ?
|
|
Math.max(len + relativeEnd, 0) :
|
|
Math.min(relativeEnd, len);
|
|
|
|
// Step 12.
|
|
while(k < finalValue) {
|
|
O[k] = value;
|
|
k++;
|
|
}
|
|
|
|
// Step 13.
|
|
return O;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Some utility functions
|
|
|
|
function getRandomIntInclusive(min, max) {
|
|
min = Math.ceil(min);
|
|
max = Math.floor(max);
|
|
return Math.floor(Math.random() * (max - min + 1)) + min; //The maximum is inclusive and the minimum is inclusive
|
|
}
|
|
|
|
if(!Array.prototype.min){
|
|
Array.prototype.min = function() {
|
|
if(this.length==0)
|
|
return null;
|
|
return this.reduce(function(m,v){ return (v<m)?v:m; }, +Infinity);
|
|
};
|
|
} else {
|
|
console.warn("Array.prototype.min already exists! Its definition may not correspond to what was intended.");
|
|
}
|
|
|
|
if(!Array.prototype.max){
|
|
Array.prototype.max = function() {
|
|
if(this.length==0)
|
|
return null;
|
|
return this.reduce(function(m,v){ return (v>m)?v:m; }, -Infinity);
|
|
};
|
|
} else {
|
|
console.warn("Array.prototype.max already exists! Its definition may not correspond to what was intended.");
|
|
}
|
|
|
|
if(!Array.prototype.diff){
|
|
Array.prototype.diff = function() {
|
|
var a = [];
|
|
for(var i = 0; i < this.length - 1; i++) {
|
|
a.push(this[i + 1] - this[i])
|
|
}
|
|
return a;
|
|
};
|
|
} else {
|
|
console.warn("Array.prototype.diff already exists! Its definition may not correspond to what was intended.");
|
|
}
|
|
|
|
if(!Array.prototype.sum){
|
|
Array.prototype.sum = function() {
|
|
return this.reduce(function(S, v) {
|
|
return S + v;
|
|
}, 0);
|
|
};
|
|
} else {
|
|
console.warn("Array.prototype.sum already exists! Its definition may not correspond to what was intended.");
|
|
}
|
|
|
|
if(!Array.prototype.mean){
|
|
Array.prototype.mean = function() {
|
|
return this.sum() / this.length;
|
|
};
|
|
} else {
|
|
console.warn("Array.prototype.mean already exists! Its definition may not correspond to what was intended.");
|
|
}
|
|
|
|
if(!Array.prototype.findIndices){
|
|
Array.prototype.findIndices = function(cnd) {
|
|
return this.reduce(function(a, v, i) {
|
|
if(cnd(v)) {
|
|
a.push(i);
|
|
};
|
|
return a;
|
|
}, []);
|
|
};
|
|
} else {
|
|
console.warn("Array.prototype.findIndices already exists! Its definition may not correspond to what was intended.");
|
|
}
|
|
|
|
if(!Array.prototype.select){
|
|
Array.prototype.select = function(idx) {
|
|
var a = []
|
|
for(var i of idx){
|
|
a.push(this[i]);
|
|
}
|
|
return a;
|
|
};
|
|
} else {
|
|
console.warn("Array.prototype.select already exists! Its definition may not correspond to what was intended.");
|
|
}
|
|
|
|
if(!Array.prototype.non_zero){
|
|
Array.prototype.non_zero = function() {
|
|
return this.filter(function(x) { return x != 0; });
|
|
};
|
|
} else {
|
|
console.warn("Array.prototype.non_zero already exists! Its definition may not correspond to what was intended.");
|
|
}
|
|
|
|
if(!Array.prototype.frequencies){
|
|
// Adapted from jsPsych DataColumn
|
|
Array.prototype.frequencies = function() {
|
|
return this.reduce(function(unique, v){
|
|
if(typeof unique[v] == 'undefined') {
|
|
unique[v] = 1;
|
|
} else {
|
|
unique[v]++;
|
|
}
|
|
return unique;
|
|
}, {});
|
|
};
|
|
} else {
|
|
console.warn("Array.prototype.frequencies already exists! Its definition may not correspond to what was intended.");
|
|
}
|
|
|
|
if(!Array.range) {
|
|
Array.range = function(arg1, arg2=null, step = 1) {
|
|
if(arg2===null) {
|
|
start = 0;
|
|
stop = arg1;
|
|
} else {
|
|
start = arg1;
|
|
stop = arg2;
|
|
}
|
|
return Array(Math.ceil((stop - start) / step)).fill(start).map((x, y) => x + y * step);
|
|
}
|
|
} else {
|
|
console.warn("Array.range already exists! Its definition may not correspond to what was intended.");
|
|
}
|
|
|
|
if(!Array.linspace) {
|
|
Array.linspace = function(start, end, n){
|
|
var a = [];
|
|
var step = (end-start)/(n-1);
|
|
for(var i=0; i<n; i++){
|
|
a.push(start+step*i);
|
|
}
|
|
return a;
|
|
};
|
|
} else {
|
|
console.warn("Array.range already exists! Its definition may not correspond to what was intended.");
|
|
}
|
|
|
|
|
|
if(!Array.prototype.keys){
|
|
Array.prototype.keys = function() {
|
|
// Not an iterator, but good enough
|
|
return Array.range(this.length);
|
|
};
|
|
}
|
|
|
|
function stringToIntArray(s){
|
|
var a = new Array();
|
|
for(var i=0; i<s.length; i++){
|
|
a.push(s.charCodeAt(i));
|
|
}
|
|
return a;
|
|
}
|
|
|
|
if(!console) {
|
|
console.log = function(){};
|
|
console.warn = function(){};
|
|
}
|