// AUDIO element
/* $AC$ PennController.newAudio(name,file) Creates a new Audio element $AC$ */
/* $AC$ PennController.getAudio(name) Retrieves an existing Audio element $AC$ */
window.PennController._AddElementType("Audio", function(PennEngine) {
const RATIO_PRELOADED = 0.95;
// This is executed when Ibex runs the script in data_includes (not a promise, no need to resolve)
this.immediate = function(id, file){
if (typeof id == "string" && file===undefined)
file = id;
let addHostURLs = !file.match(/^http/i);
this.resource = PennEngine.resources.new(file, function(uri, resolve){
const object = document.createElement("audio");
object.muted = true;
let loading = false;
const checkLoaded = ()=>{
if (!loading){
console.log("Starting to preload "+file, object);
object.muted = true;
object.play();
}
loading = true;
if (object.buffered.length && object.seekable.length){
const ratio = object.buffered.end(0) / object.seekable.end(0);
if (object.currentTime == object.duration || ratio >= RATIO_PRELOADED){
object.pause();
object.currentTime = 0;
object.muted = false;
loading = false;
return resolve(object);
}
}
window.requestAnimationFrame(checkLoaded);
return true;
};
object.addEventListener("progress", ()=>loading||checkLoaded());
object.src = uri;
object.load(); // Forcing 'autopreload'
}, addHostURLs);
// this.resource = PennEngine.resources.fetch(file, function(resolve){
// this.object = new Audio(); // Creation of the audio using the resource's value
// this.object.muted = true;
// let playing = false, checking = false;
// const checkLoaded = ()=>{
// checking = true;
// if (this.object.buffered.length && this.object.seekable.length){
// if (this.object.buffered.end(0) == this.object.seekable.end(0)){
// this.object.pause();
// this.object.currentTime = 0;
// this.object.muted = false;
// resolved = true;
// return resolve();
// }
// else if (!playing){
// this.object.muted = true;
// this.object.play();
// }
// }
// window.requestAnimationFrame(checkLoaded);
// return true;
// };
// this.object.addEventListener("progress", ()=>checking||checkLoaded());
// this.object.src = this.value;
// this.object.load(); // Forcing 'autopreload'
// }, addHostURLs);
// Naming
if (id===undefined||typeof(id)!="string"||id.length==0)
id = "Audio";
this.id = id;
};
// This is executed when 'newAudio' is executed in the trial (converted into a Promise, so call resolve)
this.uponCreation = function(resolve){
this.resource.object.controls = true; // Make the controls visible
this.audio = this.resource.object; // Audio simply refers to the resource's object
this.hasPlayed = false; // Whether the audio has played before
this.disabled = false; // Whether the audio can be played
this.resource.object.style = null; // (Re)set any particular style applied to the resource's object
this.jQueryElement = $(this.audio); // The jQuery element
this.jQueryElement.removeClass();
this.jQueryDisable = null; // The 'disable' element, to be printed on top
this.playEvents = []; // List of ["play",time,position]
this.endEvents = []; // List of ["end",time,position]
this.pauseEvents = []; // List of ["pause",time,position]
this.seekEvents = []; // List of ["seek",time,position]
this.bufferEvents = []; // List of ["buffer",time,position]
this.whatToSave = []; // ["play","end","pause","seek"] (buffer logged by default)
this.resource.object.onplay = ()=>{
this.playEvents.push(["play",this.audio.currentTime,Date.now()]);
};
this.resource.object.onended = ()=>{
this.hasPlayed=true;
this.endEvents.push(["end",this.audio.currentTime,Date.now()]);
};
this.resource.object.onpause = ()=>{
this.pauseEvents.push(["pause",this.audio.currentTime,Date.now()]);
};
this.resource.object.onseeked = ()=>{
this.seekEvents.push(["seek",this.audio.currentTime,Date.now()]);
};
this.resource.object.onwaiting = ()=>{
this.bufferEvents.push(["buffer",this.audio.currentTime,Date.now()]);
};
this.printDisable = opacity=>{
if (opacity===undefined) opacity = this.disabled;
if (opacity===true||isNaN(Number(opacity))) opacity = 0.5;
if (this.jQueryDisable instanceof jQuery)
this.jQueryDisable.remove();
this.jQueryDisable = $("
").css({
position: "absolute",
display: "inline-block",
"background-color": "gray",
opacity: opacity,
width: this.jQueryElement.width(),
height: this.jQueryElement.height()
});
this.jQueryElement.before(this.jQueryDisable);
// this.jQueryElement.removeAttr("controls");
};
resolve();
};
// This is executed at the end of a trial
this.end = function(){
if (this.whatToSave && this.whatToSave.indexOf("play")>-1){
if (!this.playEvents.length)
PennEngine.controllers.running.save(this.type, this.id, "play", "NA", "Never");
for (let line in this.playEvents)
PennEngine.controllers.running.save(this.type, this.id, ...this.playEvents[line]);
}
if (this.whatToSave && this.whatToSave.indexOf("end")>-1){
if (!this.endEvents.length)
PennEngine.controllers.running.save(this.type, this.id, "end", "NA", "Never");
for (let line in this.endEvents)
PennEngine.controllers.running.save(this.type, this.id, ...this.endEvents[line]);
}
if (this.whatToSave && this.whatToSave.indexOf("pause")>-1){
if (!this.pauseEvents.length)
PennEngine.controllers.running.save(this.type, this.id, "pause", "NA", "Never");
for (let line in this.pauseEvents)
PennEngine.controllers.running.save(this.type, this.id, ...this.pauseEvents[line]);
}
if (this.whatToSave && this.whatToSave.indexOf("seek")>-1){
if (!this.seekEvents.length)
PennEngine.controllers.running.save(this.type, this.id, "seek", "NA", "Never");
for (let line in this.seekEvents)
PennEngine.controllers.running.save(this.type, this.id, ...this.seekEvents[line]);
}
if (this.bufferEvents)
for (let line in this.bufferEvents)
PennEngine.controllers.running.save(this.type, this.id, ...this.bufferEvents[line]);
this.resource.object.pause();
this.resource.object.currentTime = 0; // Reset to the beginning
if (this.jQueryDisable)
this.jQueryDisable.remove();// Remove disabler from DOM
};
this.value = function(){ // Value is timestamp of last end event
if (this.endEvents.length)
return this.endEvents[this.endEvents.length-1][2];
else
return 0;
};
this.actions = {
// Every method is converted into a Promise (so need to resolve)
play: function(resolve, loop){ /* $AC$ Audio PElement.play() Starts the audio playback $AC$ */
if (this.hasOwnProperty("audio") && this.audio instanceof Audio){
if (loop && loop=="once")
this.audio.removeAttribute("loop");
else if (loop)
this.audio.loop = true;
this.audio.play();
}
else
PennEngine.debug.error("No audio to play for element ", this.id);
resolve();
},
pause: function(resolve){ /* $AC$ Audio PElement.pause() Pauses the audio playback $AC$ */
this.audio.pause();
resolve();
}
,
print: function(resolve, ...where){ /* $AC$ Audio PElement.print() Prints an interface to control the audio playback $AC$ */
let afterPrint = ()=>{
if (this.disabled || (this.disabled!==null&&this.disabled!==false&&!isNaN(this.disabled)))
this.printDisable(this.disabled);
resolve();
};
PennEngine.elements.standardCommands.actions.print.apply(this, [afterPrint, ...where]);
},
stop: function(resolve){ /* $AC$ Audio PElement.stop() Stops the audio playback $AC$ */
// this.audio.pause();
this.audio.currentTime = this.audio.duration;
resolve();
}
,
// Here, we resolve only when the audio ends (and the test is felicitous, if provided)
wait: function(resolve, test){ /* $AC$ Audio PElement.wait() Waits until the audio playback has ended $AC$ */
if (test == "first" && this.hasPlayed) // If first and has already played, resolve already
resolve();
else { // Else, extend onend and do the checks
let resolved = false;
let originalOnended = this.audio.onended;
this.audio.onended = function(...rest){
originalOnended.apply(this, rest);
if (resolved)
return;
if (test instanceof Object && test._runPromises && test.success){
let oldDisabled = this.disabled; // Temporarilly disable
this.disabled = "tmp";
test._runPromises().then(value=>{ // If a valid test command was provided
if (value=="success"){
resolved = true;
resolve(); // resolve only if test is a success
}
if (this.disabled == "tmp") // Restore to previous setting if not modified by test
this.disabled = oldDisabled;
});
}
else{ // If no (valid) test command was provided
resolved = true;
resolve(); // resolve anyway
}
};
}
}
};
this.settings = {
disable: function(resolve, opacity){ /* $AC$ Audio PElement.disable(opacity) Disables the interface $AC$ */
this.jQueryElement.addClass("PennController-disabled");
this.jQueryContainer.addClass("PennController-disabled");
if (isNaN(opacity)||opacity===null) opacity = true;
else opacity = Number(opacity);
this.disabled = opacity;
this.printDisable(opacity);
resolve();
}
,
enable: function(resolve){ /* $AC$ Audio PElement.enable() Enables the interface $AC$ */
if (this.jQueryDisable instanceof jQuery){
this.disabled = false;
this.jQueryDisable.remove();
this.jQueryDisable = null;
this.jQueryElement.removeClass("PennController-disabled");
this.jQueryContainer.removeClass("PennController-disabled");
this.jQueryElement.attr("controls", true);
}
resolve();
}
,
// Every setting is converted into a Promise (so resolve)
once: function(resolve){ /* $AC$ Audio PElement.once() The interface will be disabled after the first playback $AC$ */
if (this.hasPlayed){
this.disabled = true;
this.printDisable();
}
else { // Extend onend
let onended = this.audio.onended, t = this;
this.audio.onended = function(...rest){
onended.apply(this, rest);
t.disabled = true;
t.printDisable();
};
}
resolve();
}
,
log: function(resolve, ...what){ /* $AC$ Audio PElement.log() Logs playback events $AC$ */
if (what.length==1 && typeof(what[0])=="string")
this.whatToSave.push(what[0]);
else if (what.length>1)
this.whatToSave = this.whatToSave.concat(what);
else
this.whatToSave = ["play","end","pause","seek"];
resolve();
}
};
this.test = {
// Every test is used within a Promise back-end, but it should simply return true/false
hasPlayed: function(){ /* $AC$ Audio PElement.test.hasPlayed() Checks whether the audio has ever been played fully $AC$ */
return this.hasPlayed;
}
,
playing: function(){ /* $AC$ Audio PElement.test.playing() Checks whether the audio is currently playing $AC$ */
return this.audio.currentTime&&!this.audio.paused;
}
};
});