Forum Replies Created
-
AuthorPosts
-
Jeremy
KeymasterHello,
I’m glad that you find the sample experiment interesting, since I came up with its design as I was working on the tutorial for PCIbex 🙂
There is no actual study behind it, but I’d be more than happy to see someone explore the online timecourse of semantic processing of verbal agreement in English!
Jeremy
Jeremy
KeymasterHello,
I think there are Praat scripts that will do what you describe, and generate an annex file reporting the corresponding timecodes. However, I don’t know exactly how you would integrate that within PennController, or any other experiemental software for the matter.
If your goal is simply to skip any silence at the beginning of your audio files, you’d be better off using a script that automatically truncate your audio streams at the beginning and at the end.
Jeremy
Jeremy
KeymasterHello Callie,
Sorry for the late reply, I was away from the office last week. What you can do is generate the random number at the beginning of the experiment, but present it only at the end. Here’s a dummy experiment for illustration purposes:
PennController.ResetPrefix(null) // Shorten command names (keep this line here) uniqueID = [1,2,3,4].map(v=>Math.floor((1+Math.random())*0x10000).toString(16).substring(1)).join('-'); Header( // void ) .log( "ID" , uniqueID ) newTrial("intro", newButton("Hello").print().wait() ) newTrial("experiment", newButton("World").print().wait() ) SendResults() newTrial("end", newText("your id: "+uniqueID).print(),newButton().wait() )
Let me know if you need assistance adapting this to your case
Jeremy
Jeremy
KeymasterHi Peiyao,
Apologies for the late reply, I was away from the office last week.
1) The only way to hide the cursor is to change the cursor CSS property to use a custom empty image. Keep in mind that hiding the cursor (and directly manipulating the mouse in any way more generally) is considered a security threat.
2) There is no type of PennController element, as of now, that easily lets you detect mouse hovering, so you’ll have to use javascript
Here is a trial whose behavior gets pretty close to what you describe:
PennController.ResetPrefix(null) // Shorten command names (keep this line here) newTrial( newButton("Start") .print("center at 50vw", "middle at 50vh") .wait() .remove() , newCanvas('left', 200,200).css('background','red').print("center at 25vw", "middle at 50vh"), newCanvas('right', 200,200).css('background','green').print("center at 75vw", "middle at 50vh") , newFunction( ()=>{ $("body").css({ width: '100vw', height: '100vh', cursor: 'url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="), auto' }); setTimeout(()=>$("body").bind('mousemove', ()=>$('body').css('cursor','unset')),1000); }).call() , newVar("choice", 'none').log() , newFunction( ()=>new Promise(r=>$(".PennController-left, .PennController-right").bind('mouseenter', e=>getVar("choice").set( e.target.className.replace(/^.*(left|right).*$/,"$1") )._runPromises().then(r) )) ).call() )
You’ll notice the two Function elements: the first one replaces the cursor with a custom inline (empty) image for the whole page’s body (while making it occupy 100% of the space on the page) and also listens for any movement so it can restore the cursor. Some caveat about cursor and mouse movements: it appears the cursor property is only updated if the mouse moves, so you actually want the mouse to move immediately after the button is clicked. This will happen naturally with an external mouse, but maybe not with a trackpad. But also because of that, you want to add a delay before mouse movements will restore the cursor, which is why I added a setTimeout of 1000ms.
The second Function element tells the script to listen to mouse movements that land on either the element named left or the element named right (the two Canvases) and set the value of the choice Var element to “left” or “right” (looking up the element’s class name and deleting everything that’s not it). Because the call command on the Function element is asynchronous, I embedded all that into a Promise that’s only resolved when the mouse lands on left or right, which has the consequence of waiting until that event before reaching the end of the script (and therefore validating the trial).
Let me know if you need assistance adapting this to your case
Jeremy
Jeremy
KeymasterHi Anna,
There is, but things are getting a little messy. You’ll need to execute some plain javascript code just before repeating the calls to truly randomize their order. This is precisely what the newFunction element is for. Here’s a possible implementation:
newTrial("flapTest", newFunction("init", function(){ this.progress = -1; this.indices = [... new Array(8)].map((v,i)=>i).sort(v=>Math.random()-0.5); }).call() , newFunction("getNextIndex", function(){ this.progress++; if (this.progress>=this.indices.length) return "stop"; return this.indices[this.progress]; }) , newButton("carryOn").log() , newButton("launch").callback( newVar("whichIndex").set( getFunction("getNextIndex").call() ) .test.is(0).success( ...launch("1", "adequateT.mp3", "<p>Correct!</p>", "<p>Incorrect.</p>", "a flap") ) .test.is(1).success( ...launch("2", "bitterT.mp3", "<p>Correct!</p>", "<p>Incorrect.</p>", "a flap") ) .test.is(2).success( ...launch("3", "daddyT.mp3", "<p>Correct!</p>", "<p>Incorrect.</p>", "a flap") ) .test.is(3).success( ...launch("4", "mottoT.mp3", "<p>Correct!</p>", "<p>Incorrect.</p>", "a flap") ) .test.is(4).success( ...launch("5", "italicsT.mp3", "<p>Incorrect.</p>", "<p>Correct!</p>", "not a flap") ) .test.is(5).success( ...launch("6", "planetaryT.mp3", "<p>Incorrect.</p>", "<p>Correct!</p>", "not a flap") ) .test.is(6).success( ...launch("7", "producingT.mp3", "<p>Incorrect.</p>", "<p>Correct!</p>", "not a flap") ) .test.is(7).success( ...launch("8", "traditionT.mp3", "<p>Incorrect.</p>", "<p>Correct!</p>", "not a flap") ) .test.is("stop").success( getButton("carryOn").click() ) .testNot.is("stop").success( getButton("launch").click() ) ).click() , getButton("carryOn").wait() , newText("evaluation", "Your number of correct answers: ").after(newText("").text(getVar("FlapScore"))) .print() , getVar("FlapScore") .test.is(v=>v>=5) .success( newText("<p>Good job! When you're ready, press the button below to proceed.</p>").print(), newButton("Continue").print().wait() ) .failure( newText("nope1", "<p>Given your score, we would like you to take the quiz again. Press the button below to proceed.</p>").print(), newButton("toNext1", "Continue").print().wait(), getFunction("init").call(), getText("nope1").remove(), getButton("toNext1").remove(), getText("evaluation").remove(), getButton("launch").click(), getButton("carryOn").wait() ) )
The init function is what takes care of randomly ordering the indices, and (re)setting the pointer (this.progress) to the first index in the random list (this.indices). The getNextIndex function, which is called whenever the launch button is clicked (manually or by simulation) will increment the pointer and return the next index in the list. By assigning that value to a Var element, we can test it and run the corresponding launch sequence. If the indices have been exhausted, the value is "stop", in which case we simulate a click on carryOn, otherwise we simulate a new click on launch which will run the next sequence from the randomized list of indices.
Note the line getFunction("init").call() just before repeating the training, so as to reinitialize the pointer and re-randomize the list of indices
Let me know if you have questions
Jeremy
Jeremy
KeymasterThe problem is coming from the newButton commands being rerun the second time, which effectively erases the history of clicks from the element. I’ll have to work on that for future releases, but in the meantime here is a revised version of your script that should work:
const launch = (num, audio, flapChooseT, notFlapChooseT, status) => [ newAudio("flapAud" + num, audio) .play() .wait() , newText("prompt" + num, "This was...<br><br>") .settings.center() .print() , getButton("carryOn") .test.clicked() .success( getButton("flap"+num).enable().print(), getText("spacer"+num).print(), getButton("notFlap"+num).enable().print() ) .failure( newButton("flap" + num, "a flap") .print() .center() .log() .callback( getText("response" + num).text(flapChooseT), getVar("FlapScore").set(v => v + Number(status=="a flap")), getButton("flap" + num).disable(), getButton("notFlap" + num).disable(), getButton("moveOn" + num).visible() ) , newText("spacer" + num, "<br>").print() , newButton("notFlap" + num, "not a flap") .print() .center() .log() .callback( getText("response" + num).text(notFlapChooseT), getVar("FlapScore").set(v => v + Number(status == "not a flap")), getButton("flap" + num).disable(), getButton("notFlap" + num).disable(), getButton("moveOn" + num).visible() ) ) , newText("response" + num, "<p></p>") .print() , newButton("moveOn" + num, "Next") .hidden() .print() .wait() , getText("prompt" + num).remove(), getText("spacer" + num).remove(), getButton("flap" + num).remove(), getButton("notFlap" + num).remove(), getText("response" + num).remove(), getButton("moveOn" + num).remove(), ] newTrial("flapTest", newButton("carryOn").log() , newButton("launch").callback( ...launch("1", "adequateT.mp3", "<p>Correct!</p>", "<p>Incorrect.</p>", "a flap"), ...launch("7", "producingT.mp3", "<p>Incorrect.</p>", "<p>Correct!</p>", "not a flap"), ...launch("2", "bitterT.mp3", "<p>Correct!</p>", "<p>Incorrect.</p>", "a flap"), ...launch("3", "daddyT.mp3", "<p>Correct!</p>", "<p>Incorrect.</p>", "a flap"), ...launch("8", "traditionT.mp3", "<p>Incorrect.</p>", "<p>Correct!</p>", "not a flap"), ...launch("4", "mottoT.mp3", "<p>Correct!</p>", "<p>Incorrect.</p>", "a flap"), ...launch("5", "italicsT.mp3", "<p>Incorrect.</p>", "<p>Correct!</p>", "not a flap"), ...launch("6", "planetaryT.mp3", "<p>Incorrect.</p>", "<p>Correct!</p>", "not a flap"), getButton("carryOn").click() ).click() , getButton("carryOn").wait() , newText("evaluation", "Your number of correct answers: ").after(newText("").text(getVar("FlapScore"))) .print() , getVar("FlapScore") .test.is(v=>v>=5) .success( newText("<p>Good job! When you're ready, press the button below to proceed.</p>").print(), newButton("Continue").print().wait() ) .failure( newText("nope1", "<p>Given your score, we would like you to take the quiz again. Press the button below to proceed.</p>").print(), newButton("toNext1", "Continue").print().wait(), getText("nope1").remove(), getButton("toNext1").remove(), getText("evaluation").remove(), getButton("launch").click(), getButton("carryOn").wait() ) )
You’ll notice that I changed a few other things:
- I used a function in the Var’s test, which allowed me to concisely apply a “greater than” test
- I created the carryOn button first and got rid of “finally”
- I test in launch whether carryOn was clicked before, so I don’t recreate the buttons (which would erase their click history) the second time around
- I also got rid of once on the buttons because you’ll need to have them clickable again if you repeat the training phase
One note about the results file: you’ll get only one line for buttons that were never clicked, with “Never” as their timestamp, and one or two lines for the other buttons depending on how often they were clicked. Unfortunately the results lines don’t seem to appear in chronological order (despite my attempt at coding that feature, another thing I’ll need to fix for the next release) so you’ll have to reorder them when running your analyses, using the timestamps. I logged the carryOn button to make it easier to identify first-round vs second-round clicks by comparing timestamps.
Let me know if you have questions
Jeremy
Jeremy
KeymasterHi Anna,
How do you handle training repetition? Is it all part of one big newTrial that you reset if accuracy is low?
Jeremy
Jeremy
KeymasterHi Emiel,
Yes, it would be along the lines of the solutions you point to. So something like:
const replaceUploadingMessage = ()=>{ const uploadingMessage = $(".PennController-PennController > p"); if (uploadingMessage.length > 0 && uploadingMessage[0].innerHTML.match(/^Please wait while the archive of your recordings is being uploaded to the server/)) uploadingMessage.html("Please wait"); window.requestAnimationFrame( replaceUploadingMessage ); }; window.requestAnimationFrame( replaceUploadingMessage );
Also consider uploading recordings trial by trial
Jeremy
Jeremy
KeymasterSo I came up with a new function that I think is better suited to your needs: it simply picks a number of trials from a set, so in your case you can pick 40 trials from the randomized set of all the trials labeled ListeW (randomize("ListeW")), and then pick 40 other trials from that same set. The trick here is that you’ll store the set in a variable, so you can reference it when you call pick again, instead of recreating a brand new set with randomize("ListeW").
Define the pick/Pick functions the way you defined subsequence:
function Pick(set,n) { assert(set instanceof Object, "First argument of pick cannot be a plain string" ); n = Number(n); if (isNaN(n) || n<0) n = 0; this.args = [set]; set.remainingSet = null; this.run = function(arrays){ if (set.remainingSet===null) set.remainingSet = arrays[0]; const newArray = []; for (let i = 0; i < n && set.remainingSet.length; i++) newArray.push( set.remainingSet.shift() ); return newArray; } } function pick(set, n) { return new Pick(set,n); }
Then you can use it like this:
Sequence( "intro", pick(liste=randomize("ListeW"),40),"attentionCheck", pick(liste,40),"break", pick(liste,40),"attentionCheck", pick(liste,40), "end" )
As you can see I generate a randomized set of trials in the first pick function, and I store it in a javascript variable named liste, so I can reference it in the next three pick's. You could actually even create the set above the Sequence command and only reference it using the variable in all the pick functions. You just want to always use a unique javascript variable name (liste is not ideal because the generic English word list is likely to be used elsewhere, but hopefully the final e will help avoid any conflicts).
Let me know if you have questions
Jeremy
Jeremy
KeymasterHi Kathy,
EDIT: Oops, looks like I misremembered how the subsequence function works. See message below for an alternative solution
Since you’re already using the subsequence function, why don’t you just tell it exactly that? Here’s what your whole Sequence command could look like:
Sequence( "intro" , subsequence( repeat(randomize("ListeW"), 40) , "attentionCheck" , repeat(randomize("ListeW"), 40) , "break" , repeat(randomize("ListeW"), 40) , "attentionCheck" , repeat(randomize("ListeW"), 40) ) , "end" )
I see that you named your Scale element “slider,” if you want to make it a “real” slider, you could consider using the slider command, like this:
newScale("slider", 50) .before( newText("0 ") ) .after( newText(" 50") ) .slider() .print() .log() .wait()
In any case, you don’t need to manually specify all 50 labels, you can just do this:
newScale("slider", 50).labelsPosition("bottom")
Jeremy
Jeremy
KeymasterHi,
The callback command is a non-blocking command, meaning that everything after the closing parenthesis of callback will be executed immediately, with no further delay. You probably want to use the blocking command wait instead:
newTrial("test_audio", newAudio("test", "2fishRoundTank.mp3") .play() .log() // .print() // Do you really want to show the player? , newTimer("reg1", 800).start().wait(), getAudio("test").pause(), newKey("next", " ").log().wait() , getAudio("test").play(), newTimer("reg2", 400).start().wait(), // I assume you want to play the next 400ms getAudio("test").pause(), getKey("next").wait() , getAudio("test").play(), newTimer("reg3", 400).start().wait(), getAudio("test").pause(), getKey("next").wait() , newButton("Next") .print() .wait() )
Jeremy
Jeremy
KeymasterHi,
You are using a plain javascript ternary conditionals where you should be using a PennController test command. I briefly explain this point below the code in this message.
Your trial should look like this:
newTrial("feedback", newVar("Acc", 0.8).global() , newText("low_acc", "Low accuracy") , newText("high_acc", "High accuracy") , getVar("Acc").test.is( v => v > 0.5 ) .success( getText("high_acc").print() ) .failure( getText("low_acc").print() ) , newButton("Exit") .center() .print() .wait() )
Jeremy
Jeremy
KeymasterHi,
That’s a need I hadn’t anticipated, though it makes total sense that some designs would need that.
Add this at the top of your script, below PennController.ResetPrefix(null):
_AddStandardCommands(function(PennEngine){ this.actions = { duration: function(resolve, duration){ const nduration = Number(duration); if (isNaN(nduration) || nduration < 0) PennEngine.debug.error("Invalid duration for timer "+this.id+" (""+duration+"")"); else this.duration = nduration; resolve(); } } });
Then you can use the command .duration to (re)set the duration of your Timer, for example:
newTrial( newVar("duration") , newTextInput("input","Enter a duration in ms").print().wait().remove().setVar("duration") , newTimer("timeout",1).duration( getVar("duration") ).start().wait() , newText("Timeout elapsed").print() , newButton().wait() )
Jeremy
Jeremy
KeymasterHi,
There is no selected test for Key elements, what you are looking for is test.pressed
Example:
newTrial( newKey("test", "ArrowLeft", "ArrowRight") .wait() .test.pressed( "ArrowLeft" ) .success( newText("Left").print() ) .failure( newText("Right").print() ) , newButton().wait() )
Jeremy
Jeremy
KeymasterHello Miriam,
Both options should normally work. Here is an example script, switch the commenting // to test out the two options:
PennController.ResetPrefix(null) AddTable("myTable", "Correct\nYes\nNo\nYes\nNo" ) Header( // newVar("Acc", []).global() ) newTrial( "intro" , newVar("Acc", []).global(), newButton("Start").print().wait() ) Template( "myTable" , row => newTrial( "exp" , newVar("computedAcc").set(getVar("Acc")).set(v=>v.filter(a=>a===true).length/v.length), newText("accuracy").text(getVar("computedAcc")).print("left at 5px", "top at 5px") , newScale("answer", "Yes","No").button().print().wait() .test.selected(row.Correct) .success( getVar("Acc").set(v=>[...v,true]) ) .failure( getVar("Acc").set(v=>[...v,false]) ) )) newTrial( "end" , newVar("computedAcc").set(getVar("Acc")).set(v=>v.filter(a=>a===true).length/v.length), newText("accuracy").text(getVar("computedAcc")) , newText("Final accuracy: ").after(getText("accuracy")).print() , newButton().wait() )
In this example I set the global Var element to an array of true‘s and false‘s so I can then compute the current accuracy on each trial by dividing the number of true‘s by the length of the array.
Note that some Var-related bugs were fixed with PennController 1.8, so make sure to update your version of PennController if the script above does not work for you
Jeremy
-
AuthorPosts