Jeremy

Forum Replies Created

Viewing 15 posts - 1,141 through 1,155 (of 1,522 total)
  • Author
    Posts
  • in reply to: paper of the sample experiment #6016
    Jeremy
    Keymaster

    Hello,

    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

    in reply to: Playing audio word-by-word #6015
    Jeremy
    Keymaster

    Hello,

    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

    in reply to: Random Number Generator #6014
    Jeremy
    Keymaster

    Hello 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

    in reply to: hide cursor and select by pointing #6013
    Jeremy
    Keymaster

    Hi 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

    in reply to: Logging Results for Multiple Attempts #6000
    Jeremy
    Keymaster

    Hi 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

    in reply to: Logging Results for Multiple Attempts #5995
    Jeremy
    Keymaster

    The 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

    in reply to: Logging Results for Multiple Attempts #5993
    Jeremy
    Keymaster

    Hi Anna,

    How do you handle training repetition? Is it all part of one big newTrial that you reset if accuracy is low?

    Jeremy

    in reply to: Different message for UploadRecordings #5991
    Jeremy
    Keymaster

    Hi 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

    in reply to: including attention checks and break #5980
    Jeremy
    Keymaster

    So 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

    in reply to: including attention checks and break #5976
    Jeremy
    Keymaster

    Hi 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

    • This reply was modified 4 years, 11 months ago by Jeremy. Reason: Mistake
    • This reply was modified 4 years, 11 months ago by Jeremy. Reason: Solution in next message
    in reply to: Playing audio word-by-word #5975
    Jeremy
    Keymaster

    Hi,

    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

    in reply to: Keeping track of participant accuracy #5974
    Jeremy
    Keymaster

    Hi,

    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

    in reply to: Set newTimer() with newVar() #5970
    Jeremy
    Keymaster

    Hi,

    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+" (&quot;"+duration+"&quot;)");
                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

    in reply to: Keeping track of participant accuracy #5967
    Jeremy
    Keymaster

    Hi,

    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

    in reply to: Keeping track of participant accuracy #5906
    Jeremy
    Keymaster

    Hello 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

Viewing 15 posts - 1,141 through 1,155 (of 1,522 total)