Changing the playback rate of the movies by key presses

Hello everyone,

I am trying to write a code where participants can adjust the speed of a video by pressing the keys. For example, for a small decrease in the speed (let’s say 0.1 unit), they should press ‘y’, or for a large increment (0.3), they should press ‘a’. After each key press, I want the newly adjusted video to be seen on the screen so that participants can decide if it is proper or if they want to change the speed again and again. After they are sure, they should press the right arrow to move to the next video. The trick is that I want to use the same video, but each time, they will see it with a different playback speed/rate. So, each time when the next video is shown on the screen, it should be the same one with a different playback rate. Then, I need to save the initial playback rate and the final one only. I want to keep the playback rate between 0.1 and 5.0. So far, I have not been able to refresh the video with the key press.

Here is some part of my code

— Initialize components for Routine “exp_trial” —

movie_files = visual.MovieStim(
    win, name='movie_files',
    filename=None, movieLib='ffpyplayer',
    loop=True, volume=1.0, noAudio=False,
    pos=(0, 0), size=(43.46, 24.45), units='deg',
    ori=0.0, anchor='center',opacity=None, contrast=1.0,
    depth=0
)
trial_key_resp = keyboard.Keyboard(deviceName='trial_key_resp')
trial_inst_text = visual.TextStim(win=win, name='trial_inst_text',
    text='Press A or S for large decrement or increment. \nPress Y or X for a small decrement or increment.',
    font='Arial',
    pos=(0, 0.40), height=0.03, wrapWidth=None, ori=0.0, 
    color='white', colorSpace='rgb', opacity=None, 
    languageStyle='LTR',
    depth=-2.0);
answer_key_resp = keyboard.Keyboard(deviceName='answer_key_resp')

set up handler to look after randomisation of conditions etc

trial_loop = data.TrialHandler(nReps=1.0, method='random', 
    extraInfo=expInfo, originPath=-1,
    trialList=data.importConditions('trialExp.xlsx'),
    seed=None, name='trial_loop')
thisExp.addLoop(trial_loop)  # add the loop to the experiment
thisTrial_loop = trial_loop.trialList[0]  # so we can initialise stimuli with some values
# abbreviate parameter names if possible (e.g. rgb = thisTrial_loop.rgb)
if thisTrial_loop != None:
    for paramName in thisTrial_loop:
        globals()[paramName] = thisTrial_loop[paramName]

for thisTrial_loop in trial_loop:
    currentLoop = trial_loop
    thisExp.timestampOnFlip(win, 'thisRow.t', format=globalClock.format)
    # pause experiment here if requested
    if thisExp.status == PAUSED:
        pauseExperiment(
            thisExp=thisExp, 
            win=win, 
            timers=[routineTimer], 
            playbackComponents=[]
    )
    # abbreviate parameter names if possible (e.g. rgb = thisTrial_loop.rgb)
    if thisTrial_loop != None:
        for paramName in thisTrial_loop:
            globals()[paramName] = thisTrial_loop[paramName]
            
    # Initialize playback speed
    speeds = np.arange(0.1,5.1,0.1)
    video_file = trial_videos  # Assuming trial_videos is a single video file path string
    
    large_step = 3
    small_step = 1
    
    # --- Prepare to start Routine "exp_trial" ---
    continueRoutine = True
    # update component parameters for each repeat
    thisExp.addData('exp_trial.started', globalClock.getTime(format='float'))
    # Load the movie file and set playback rate
    playback_speed = np.random.choice(speeds)
    movie_files.setMovie(video_file)
    movie_files.rate = playback_speed
    movie_files.play()
    
    trial_key_resp.keys = []
    trial_key_resp.rt = []
    _trial_key_resp_allKeys = []
    answer_key_resp.keys = []
    answer_key_resp.rt = []
    _answer_key_resp_allKeys = []
    # keep track of which components have finished
    exp_trialComponents = [movie_files, trial_key_resp, trial_inst_text, answer_key_resp]
    for thisComponent in exp_trialComponents:
        thisComponent.tStart = None
        thisComponent.tStop = None
        thisComponent.tStartRefresh = None
        thisComponent.tStopRefresh = None
        if hasattr(thisComponent, 'status'):
            thisComponent.status = NOT_STARTED
    # reset timers
    t = 0
    _timeToFirstFrame = win.getFutureFlipTime(clock="now")
    frameN = -1
    
    # --- Run Routine "exp_trial" ---
    routineForceEnded = not continueRoutine
    while continueRoutine:
        # get current time
        t = routineTimer.getTime()
        tThisFlip = win.getFutureFlipTime(clock=routineTimer)
        tThisFlipGlobal = win.getFutureFlipTime(clock=None)
        frameN = frameN + 1  # number of completed frames (so 0 is the first frame)
        # update/draw components on each frame
        
        # *movie_files* updates
        
        # if movie_files is starting this frame...
        if movie_files.status == NOT_STARTED and tThisFlip >= 0.0-frameTolerance:
            # keep track of start time/frame for later
            movie_files.frameNStart = frameN  # exact frame index
            movie_files.tStart = t  # local t and not account for scr refresh
            movie_files.tStartRefresh = tThisFlipGlobal  # on global time
            win.timeOnFlip(movie_files, 'tStartRefresh')  # time at next scr refresh
            # add timestamp to datafile
            thisExp.timestampOnFlip(win, 'movie_files.started')
            # update status
            movie_files.status = STARTED
            movie_files.setAutoDraw(True)
            movie_files.play()
        
        # *trial_key_resp* updates
        waitOnFlip = False
        
        # if trial_key_resp is starting this frame...
        if trial_key_resp.status == NOT_STARTED and tThisFlip >= 2.0-frameTolerance:
            # keep track of start time/frame for later
            trial_key_resp.frameNStart = frameN  # exact frame index
            trial_key_resp.tStart = t  # local t and not account for scr refresh
            trial_key_resp.tStartRefresh = tThisFlipGlobal  # on global time
            win.timeOnFlip(trial_key_resp, 'tStartRefresh')  # time at next scr refresh
            # update status
            trial_key_resp.status = STARTED
            # keyboard checking is just starting
            waitOnFlip = True
            win.callOnFlip(trial_key_resp.clock.reset)  # t=0 on next screen flip
            win.callOnFlip(trial_key_resp.clearEvents, eventType='keyboard')  # clear events on next screen flip
        if trial_key_resp.status == STARTED and not waitOnFlip:
            theseKeys = trial_key_resp.getKeys(keyList=['right'], ignoreKeys=["escape"], waitRelease=False)
            _trial_key_resp_allKeys.extend(theseKeys)
            if len(_trial_key_resp_allKeys):
                trial_key_resp.keys = _trial_key_resp_allKeys[-1].name  # just the last key pressed
                trial_key_resp.rt = _trial_key_resp_allKeys[-1].rt
                trial_key_resp.duration = _trial_key_resp_allKeys[-1].duration
                # a response ends the routine
                continueRoutine = False
        
        # *trial_inst_text* updates
        
        # if trial_inst_text is starting this frame...
        if trial_inst_text.status == NOT_STARTED and tThisFlip >= 0.0-frameTolerance:
            # keep track of start time/frame for later
            trial_inst_text.frameNStart = frameN  # exact frame index
            trial_inst_text.tStart = t  # local t and not account for scr refresh
            trial_inst_text.tStartRefresh = tThisFlipGlobal  # on global time
            win.timeOnFlip(trial_inst_text, 'tStartRefresh')  # time at next scr refresh
            # add timestamp to datafile
            thisExp.timestampOnFlip(win, 'trial_inst_text.started')
            # update status
            trial_inst_text.status = STARTED
            trial_inst_text.setAutoDraw(True)
        
        # if trial_inst_text is active this frame...
        if trial_inst_text.status == STARTED:
            # update params
            pass
        
        # *answer_key_resp* updates
        waitOnFlip = False
        
        # if answer_key_resp is starting this frame...
        if answer_key_resp.status == NOT_STARTED and tThisFlip >= 0.0 - frameTolerance:
            answer_key_resp.frameNStart = frameN
            answer_key_resp.tStart = t
            answer_key_resp.tStartRefresh = tThisFlipGlobal
            win.timeOnFlip(answer_key_resp, 'tStartRefresh')
            thisExp.timestampOnFlip(win, 'answer_key_resp.started')
            answer_key_resp.status = STARTED
            waitOnFlip = True
            win.callOnFlip(answer_key_resp.clock.reset)
            win.callOnFlip(answer_key_resp.clearEvents, eventType='keyboard')
        if trial_key_resp.status == STARTED and not waitOnFlip:
            keys = event.getKeys(['a', 's', 'y', 'x', 'right'])
            if keys:
                key = keys[0]  # just the first key pressed
                if key == 'a':  # large decrement
                    playback_speed = max(0, playback_speed - large_step)
                elif key == 's':  # large increment
                    playback_speed = min(5.1, playback_speed + large_step)
                elif key == 'y':  # small decrement
                    playback_speed = max(0, playback_speed - small_step)
                elif key == 'x':  # small increment
                    playback_speed = min(5.1, playback_speed + small_step)
                elif key == 'right':  # Proceed to next video
                    continueRoutine = False
                
                # Update the playback speed immediately
                playback_rate = playback_speed
                
                
                # Start playing the video
                movie_files.setMovie(video_file)
                movie_files.rate = playback_rate
                movie_files.play()
                
                # Refresh the screen
                win.flip()
             
        # check for quit (typically the Esc key)
        if defaultKeyboard.getKeys(keyList=["escape"]):
            thisExp.status = FINISHED
        if thisExp.status == FINISHED or endExpNow:
            endExperiment(thisExp, win=win)
            return
        
        # check if all components have finished
        if not continueRoutine:  # a component has requested a forced-end of Routine
            routineForceEnded = True
            break
        continueRoutine = False  # will revert to True if at least one component still running
        for thisComponent in exp_trialComponents:
            if hasattr(thisComponent, "status") and thisComponent.status != FINISHED:
                continueRoutine = True
                break  # at least one component has not yet finished
        
        # refresh the screen
        if continueRoutine:  # don't flip if this routine is over or we'll get a blank screen
            win.flip()
    
    # --- Ending Routine "exp_trial" ---
    for thisComponent in exp_trialComponents:
        if hasattr(thisComponent, "setAutoDraw"):
            thisComponent.setAutoDraw(False)
    thisExp.addData('exp_trial.stopped', globalClock.getTime(format='float'))
    movie_files.stop()  # ensure movie has stopped at end of Routine


    # check responses
    if trial_key_resp.keys in ['', [], None]:  # No response was made
        trial_key_resp.keys = None
    trial_loop.addData('trial_key_resp.keys',trial_key_resp.keys)
    if trial_key_resp.keys != None:  # we had a response
        trial_loop.addData('trial_key_resp.rt', trial_key_resp.rt)
        trial_loop.addData('trial_key_resp.duration', trial_key_resp.duration)
    if answer_key_resp.keys in ['', [], None]:  # No response was made
        answer_key_resp.keys = None
    trial_loop.addData('answer_key_resp.keys',answer_key_resp.keys)
    trial_loop.addData('updated_speed', playback_speed)
    if answer_key_resp.keys != None:  # we had a response
        trial_loop.addData('answer_key_resp.rt', answer_key_resp.rt)
        trial_loop.addData('answer_key_resp.duration', answer_key_resp.duration)
    # the Routine "exp_trial" was not non-slip safe, so reset the non-slip timer
    routineTimer.reset()

Hi ! I am trying do to something similar in a study, but the values of speed are reported in an excel file. I was wondering if you could share any solutions ?

Best,
Sarah

So it depends exactly how you want this to work.

  1. Changing the movie playback rate of an actual movie file: This is not possible in PsychoPy at the moment. The playback rate is locked to the movie file’s framerate, and can’t be changed.
  2. Skipping forward and backward in time in a paused movie file: Technically possible but tricky. There is a “seek” function, but there are going to be a number of failure cases.
  3. Creating an effect that looks like you’re changing the playback rate of a movie file but not actually using MovieStim: Entirely doable if you code the experiment from scratch, not sure if it’s practical in the builder.

In short, unless the movie file you are using has audio, I would strongly recommend having a list of individual frames (images) and changing the rate at which you advance through them. The only challenge is getting PsychoPy to load all of the individual images ahead of time so it looks smooth. If you’re just doing the whole thing with code that’s not hard, you create a list of ImageStim objects from a list of filenames. I’m just not sure if there’s a practical way to do it in the builder.