From f11568507c61308a61449ef86c5b40e59ae7bc8c Mon Sep 17 00:00:00 2001 From: Jon Peirce Date: Thu, 8 Aug 2019 14:01:02 +0100 Subject: [PATCH 1/4] FF: PTB sounds. Better option of syncing to window and better logging Now providing a default window to sync to (like the when arg in play()) Fixed issue with extra stopping logging items --- psychopy/constants.py | 1 + psychopy/sound/backend_ptb.py | 52 ++++++++++++++++++++++++----------- 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/psychopy/constants.py b/psychopy/constants.py index cc538fec18..f024c203fc 100644 --- a/psychopy/constants.py +++ b/psychopy/constants.py @@ -22,6 +22,7 @@ STOPPED = -1 FINISHED = STOPPED SKIP = -2 +STOPPING = -3 # for button box: PRESSED = 1 diff --git a/psychopy/sound/backend_ptb.py b/psychopy/sound/backend_ptb.py index 80cd37e4b4..8d88a03bcc 100644 --- a/psychopy/sound/backend_ptb.py +++ b/psychopy/sound/backend_ptb.py @@ -11,8 +11,8 @@ import time import re -from psychopy import logging, exceptions -from psychopy.constants import (PLAYING, PAUSED, FINISHED, STOPPED, +from psychopy import prefs, logging, exceptions +from psychopy.constants import (STARTED, PAUSED, FINISHED, STOPPING, NOT_STARTED) from psychopy.exceptions import SoundFormatError, DependencyError from ._base import _SoundBase, HammingWindow @@ -202,7 +202,8 @@ def __init__(self, value="C", secs=0.5, octave=4, stereo=-1, preBuffer=-1, hamming=True, startTime=0, stopTime=-1, - name='', autoLog=True): + name='', autoLog=True, + syncToWin=None): """ :param value: note name ("C","Bfl"), filename or frequency (Hz) :param secs: duration (for synthesised tones) @@ -228,12 +229,13 @@ def __init__(self, value="C", secs=0.5, octave=4, stereo=-1, :param stopTime: for sound files this controls the end of snippet :param name: string for logging purposes :param autoLog: whether to automatically log every change + :param syncToWin: if you want start/stop to sync with win flips add this """ self.sound = value self.name = name self.secs = secs # for any synthesised sounds (notesand freqs) self.octave = octave # for note name sounds - self.loops = loops + self.loops = self._loopsRequested = loops self._loopsFinished = 0 self.volume = volume self.startTime = startTime # for files @@ -253,7 +255,7 @@ def __init__(self, value="C", secs=0.5, octave=4, stereo=-1, self.sndArr = None self.hamming = hamming self._hammingWindow = None # will be created during setSound - + self.win=syncToWin # setSound (determines sound type) self.setSound(value, secs=self.secs, octave=self.octave, hamming=self.hamming) @@ -299,6 +301,8 @@ def setSound(self, value, secs=0.5, octave=4, hamming=None, log=True): output sounds in the bottom octave (1) and the top octave (8) is generally painful """ + # reset self.loops to what was requested (in case altered for infinite play of tones) + self.loops = self._loopsRequested # start with the base class method _SoundBase.setSound(self, value, secs, octave, hamming, log) @@ -396,14 +400,26 @@ def _channelCheck(self, array): logging.error(msg) raise ValueError(msg) - def play(self, loops=None, when=None): + def play(self, loops=None, when=None, log=True): """Start the sound playing """ if loops is not None and self.loops != loops: self.setLoops(loops) self.status = PLAYING self._tSoundRequestPlay = time.time() - self.track.start(repetitions=self.loops, when=when) + + if hasattr(when, 'getFutureFlipTime'): + logTime = when.getFutureFlipTime(clock=None) + when = when.getFutureFlipTime(clock='ptb') + elif when is None and hasattr(self.win, 'getFutureFlipTime'): + logTime = self.win.getFutureFlipTime(clock=None) + when = self.win.getFutureFlipTime(clock='ptb') + else: + logTime = None + self.track.start(repetitions=loops, when=when) + # time.sleep(0.) + if log and self.autoLog: + logging.exp(u"Sound %s started" % (self.name), obj=self, t=logTime) def pause(self): """Stop the sound but play will continue from here if needed @@ -411,14 +427,17 @@ def pause(self): self.status = PAUSED streams[self.streamLabel].remove(self) - def stop(self, reset=True): + def stop(self, reset=True, log=True): """Stop the sound and return to beginning """ - streams[self.streamLabel].remove(self) + if self.status == FINISHED: + return + self.track.stop() if reset: self.seek(0) - self.status = STOPPED - self.track.stop(repetitions=self.loops) + if log and self.autoLog: + logging.exp(u"Sound %s stopped" % (self.name), obj=self) + self.status = FINISHED if self._hammingWindow: thisWin = self._hammingWindow.nextBlock(self.t, self.blockSize) @@ -438,16 +457,17 @@ def seek(self, t): if self.sndFile and not self.sndFile.closed: self.sndFile.seek(self.frameN) - def _EOS(self, reset=True): + def _EOS(self, reset=True, log=True): """Function called on End Of Stream """ + self.status = STOPPING self._loopsFinished += 1 if self.loops == 0: - self.stop(reset=reset) + self.stop(reset=reset, log=False) elif self.loops > 0 and self._loopsFinished >= self.loops: - self.stop(reset=reset) - - self.status = FINISHED + self.stop(reset=reset, log=False) + if log and self.autoLog: + logging.exp(u"Sound %s reached end of file" % (self.name), obj=self) @property def stream(self): From 9aec9c9cfaa2ea2ecc99d9e3ead3019c9850d2eb Mon Sep 17 00:00:00 2001 From: Jon Peirce Date: Thu, 8 Aug 2019 14:09:22 +0100 Subject: [PATCH 2/4] BF: Window class method was using self, not Window --- psychopy/tools/attributetools.py | 2 +- psychopy/visual/window.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/psychopy/tools/attributetools.py b/psychopy/tools/attributetools.py index 4b5ac25eb4..b8192c678c 100644 --- a/psychopy/tools/attributetools.py +++ b/psychopy/tools/attributetools.py @@ -166,5 +166,5 @@ def logAttrib(obj, log, attrib, value=None): try: obj.win.logOnFlip(message, level=logging.EXP, obj=obj) except AttributeError: - # this is probably a Window, having no "win" attribute + # the "win" attribute only exists if sync-to-visual (e.g. stimuli) logging.log(message, level=logging.EXP, obj=obj) diff --git a/psychopy/visual/window.py b/psychopy/visual/window.py index 4f97c009af..c45be802e9 100644 --- a/psychopy/visual/window.py +++ b/psychopy/visual/window.py @@ -808,7 +808,7 @@ def dispatchAllWindowEvents(cls): Dispatches events for all pyglet windows. Used by iohub 2.0 psychopy kb event integration. """ - self.backend.dispatchEvents() + Window.backend.dispatchEvents() def flip(self, clearBuffer=True): """Flip the front and back buffers after drawing everything for your From 93f87c8e85769969d06a7bbe6a429d86da96b7aa Mon Sep 17 00:00:00 2001 From: Jon Peirce Date: Thu, 8 Aug 2019 14:19:02 +0100 Subject: [PATCH 3/4] ENH: time-based timing in Builder much more accurate In the past we were deciding whether to draw for one more frame based on a guess about when the next frame will occur, but not trying to determine how far we are from that flip time: ``` if tDesired > (t-frameInterval*0.5): #guess we switch ``` Window now has a method to estimate time of next flip: ``` timeNextFlip = win.getFutureFlipTime() if tDesired > timeNextFlip-tolerance: #we're more confident based on actual predicted frame time ``` On my tests this has made the stimulus timing very accurate whereas before it was often out by a frame --- psychopy/experiment/components/_base.py | 78 ++++++++----------- .../components/aperture/__init__.py | 2 +- .../components/cedrusBox/__init__.py | 2 +- .../components/envelopegrating/__init__.py | 2 +- .../components/eyetracker/__init__.py | 2 +- .../experiment/components/ioLabs/__init__.py | 2 +- .../components/joyButtons/__init__.py | 2 +- .../components/joystick/__init__.py | 2 +- .../components/keyboard/__init__.py | 2 +- .../experiment/components/mouse/__init__.py | 2 +- .../experiment/components/movie/__init__.py | 2 +- .../components/parallelOut/__init__.py | 2 +- .../experiment/components/pump/__init__.py | 2 +- .../components/settings/__init__.py | 2 + .../experiment/components/sound/__init__.py | 11 ++- psychopy/experiment/routine.py | 6 +- psychopy/visual/window.py | 37 +++++++++ 17 files changed, 94 insertions(+), 64 deletions(-) diff --git a/psychopy/experiment/components/_base.py b/psychopy/experiment/components/_base.py index d2c81fba9c..0d8c4af013 100644 --- a/psychopy/experiment/components/_base.py +++ b/psychopy/experiment/components/_base.py @@ -189,50 +189,41 @@ def writeExperimentEndCode(self, buff): """ pass - def writeTimeTestCode(self, buff): - """Original code for testing whether to draw. - All objects have now migrated to using writeStartTestCode and - writeEndTestCode - """ - # unused internally; deprecated March 2016 v1.83.x, will remove 1.85 - logging.warning( - 'Deprecation warning: writeTimeTestCode() is not supported;\n' - 'will be removed. Please use writeStartTestCode() instead') - if self.params['duration'].val == '': - code = "if %(startTime)s <= t:\n" - else: - code = "if %(startTime)s <= t < %(startTime)s + %(duration)s:\n" - buff.writeIndentedLines(code % self.params) - def writeStartTestCode(self, buff): """Test whether we need to start """ + if self.params['syncScreenRefresh']: + tCompare = 'tThisFlip' + else: + tCompare = 't' if self.params['startType'].val == 'time (s)': # if startVal is an empty string then set to be 0.0 if (isinstance(self.params['startVal'].val, basestring) and not self.params['startVal'].val.strip()): self.params['startVal'].val = '0.0' - code = ("if t >= %(startVal)s " - "and %(name)s.status == NOT_STARTED:\n") + code = ("if {params[name]}.status == NOT_STARTED and " + "{t} >= {params[startVal]}-frameTolerance:\n") elif self.params['startType'].val == 'frame N': - code = ("if frameN >= %(startVal)s " - "and %(name)s.status == NOT_STARTED:\n") + code = ("if {params[name]}.status == NOT_STARTED and " + "frameN >= {params[startVal]}:\n") elif self.params['startType'].val == 'condition': - code = ("if (%(startVal)s) " - "and %(name)s.status == NOT_STARTED:\n") + code = ("if {params[name]}.status == NOT_STARTED and " + "{params[startVal]}:\n") else: msg = "Not a known startType (%(startType)s) for %(name)s" raise CodeGenerationException(msg % self.params) - buff.writeIndented(code % self.params) + buff.writeIndented(code.format(params=self.params, t=tCompare)) buff.setIndentLevel(+1, relative=True) code = ("# keep track of start time/frame for later\n" - "%(name)s.tStart = t # not accounting for scr refresh\n" - "%(name)s.frameNStart = frameN # exact frame index\n" - "win.timeOnFlip(%(name)s, 'tStartRefresh')" - " # time at next scr refresh\n") - buff.writeIndentedLines(code % self.params) + "{params[name]}.frameNStart = frameN # exact frame index\n" + "{params[name]}.tStart = t # local t and not account for scr refresh\n" + "{params[name]}.tStartRefresh = tThisFlipGlobal # on global time\n") + if self.type != "Sound": # for sounds, don't update to actual frame time + code += ("win.timeOnFlip({params[name]}, 'tStartRefresh')" + " # time at next scr refresh\n") + buff.writeIndentedLines(code.format(params=self.params)) def writeStartTestCodeJS(self, buff): """Test whether we need to start @@ -265,29 +256,24 @@ def writeStartTestCodeJS(self, buff): def writeStopTestCode(self, buff): """Test whether we need to stop """ + buff.writeIndentedLines("if %(name)s.status == STARTED:\n" % self.params) + buff.setIndentLevel(+1, relative=True) + if self.params['stopType'].val == 'time (s)': - code = ("frameRemains = %(stopVal)s " - "- win.monitorFramePeriod * 0.75" - " # most of one frame period left\n" - "if %(name)s.status == STARTED and t >= frameRemains:\n") + code = ("# is it time to stop? (based on local clock)\n" + "if tThisFlip > %(stopVal)s-frameTolerance:\n" + ) # duration in time (s) - elif (self.params['stopType'].val == 'duration (s)' and - self.params['startType'].val == 'time (s)'): - code = ("frameRemains = %(startVal)s + %(stopVal)s" - "- win.monitorFramePeriod * 0.75" - " # most of one frame period left\n" - "if %(name)s.status == STARTED and t >= frameRemains:\n") - # start at frame and end with duration (need to use approximate) - elif self.params['stopType'].val == 'duration (s)': - code = ("if %(name)s.status == STARTED and t >= (%(name)s.tStart " - "+ %(stopVal)s):\n") + elif (self.params['stopType'].val == 'duration (s)'): + code = ("# is it time to stop? (based on global clock, using actual start)\n" + "if tThisFlipGlobal > %(name)s.tStartRefresh + %(stopVal)s-frameTolerance:\n" + ) elif self.params['stopType'].val == 'duration (frames)': - code = ("if %(name)s.status == STARTED and frameN >= " - "(%(name)s.frameNStart + %(stopVal)s):\n") + code = ("if frameN >= (%(name)s.frameNStart + %(stopVal)s):\n") elif self.params['stopType'].val == 'frame N': - code = "if %(name)s.status == STARTED and frameN >= %(stopVal)s:\n" + code = "if frameN >= %(stopVal)s:\n" elif self.params['stopType'].val == 'condition': - code = "if %(name)s.status == STARTED and bool(%(stopVal)s):\n" + code = "if bool(%(stopVal)s):\n" else: msg = ("Didn't write any stop line for startType=%(startType)s, " "stopType=%(stopType)s") @@ -626,7 +612,7 @@ def writeFrameCode(self, buff): self.writeStopTestCode(buff) buff.writeIndented("%(name)s.setAutoDraw(False)\n" % self.params) # to get out of the if statement - buff.setIndentLevel(-1, relative=True) + buff.setIndentLevel(-2, relative=True) # set parameters that need updating every frame # do any params need updating? (this method inherited from _base) diff --git a/psychopy/experiment/components/aperture/__init__.py b/psychopy/experiment/components/aperture/__init__.py index 341094c159..69c3382237 100644 --- a/psychopy/experiment/components/aperture/__init__.py +++ b/psychopy/experiment/components/aperture/__init__.py @@ -90,7 +90,7 @@ def writeFrameCode(self, buff): self.writeStopTestCode(buff) buff.writeIndented("%(name)s.enabled = False\n" % self.params) # to get out of the if statement - buff.setIndentLevel(-1, relative=True) + buff.setIndentLevel(-2, relative=True) # set parameters that need updating every frame # do any params need updating? (this method inherited from _base) if self.checkNeedToUpdate('set every frame'): diff --git a/psychopy/experiment/components/cedrusBox/__init__.py b/psychopy/experiment/components/cedrusBox/__init__.py index b7062d51a6..d7b8518d01 100755 --- a/psychopy/experiment/components/cedrusBox/__init__.py +++ b/psychopy/experiment/components/cedrusBox/__init__.py @@ -209,7 +209,7 @@ def writeFrameCode(self, buff): # writes an if statement to determine whether to draw etc self.writeStopTestCode(buff) buff.writeIndented("%(name)s.status = FINISHED\n" % self.params) - buff.setIndentLevel(-1, True) + buff.setIndentLevel(-2, True) buff.writeIndented("if %(name)s.status == STARTED:\n" % self.params) buff.setIndentLevel(1, relative=True) # to get out of if statement diff --git a/psychopy/experiment/components/envelopegrating/__init__.py b/psychopy/experiment/components/envelopegrating/__init__.py index c3769c0b8a..e2b36a9a84 100644 --- a/psychopy/experiment/components/envelopegrating/__init__.py +++ b/psychopy/experiment/components/envelopegrating/__init__.py @@ -288,7 +288,7 @@ def writeFrameCode(self, buff): #if self.params['blendmode'].val!='default': #buff.writeIndented("win.blendMode=%(name)s_SaveBlendMode\n" % self.params) # to get out of the if statement - buff.setIndentLevel(-1, relative=True) + buff.setIndentLevel(-2, relative=True) # set parameters that need updating every frame # do any params need updating? (this method inherited from _base) diff --git a/psychopy/experiment/components/eyetracker/__init__.py b/psychopy/experiment/components/eyetracker/__init__.py index 300dfaab6d..93fa63f935 100644 --- a/psychopy/experiment/components/eyetracker/__init__.py +++ b/psychopy/experiment/components/eyetracker/__init__.py @@ -148,7 +148,7 @@ def writeFrameCode(self, buff): buff.writeIndentedLines(code % self.params) # to get out of the if statement - buff.setIndentLevel(-1, relative=True) + buff.setIndentLevel(-2, relative=True) # if STARTED and not FINISHED! code = "if %(name)s.status == STARTED: # only update if started and not finished!\n" % self.params diff --git a/psychopy/experiment/components/ioLabs/__init__.py b/psychopy/experiment/components/ioLabs/__init__.py index cb81da4c4c..182bc9a8ed 100644 --- a/psychopy/experiment/components/ioLabs/__init__.py +++ b/psychopy/experiment/components/ioLabs/__init__.py @@ -171,7 +171,7 @@ def writeFrameCode(self, buff): # writes an if statement to determine whether to draw etc self.writeStopTestCode(buff) buff.writeIndented("%(name)s.status = FINISHED\n" % self.params) - buff.setIndentLevel(-1, True) + buff.setIndentLevel(-2, True) buff.writeIndented("if %(name)s.status == STARTED:\n" % self.params) buff.setIndentLevel(1, relative=True) # to get out of the if statement diff --git a/psychopy/experiment/components/joyButtons/__init__.py b/psychopy/experiment/components/joyButtons/__init__.py index f50b6c25c2..b756f654a7 100755 --- a/psychopy/experiment/components/joyButtons/__init__.py +++ b/psychopy/experiment/components/joyButtons/__init__.py @@ -298,7 +298,7 @@ def writeFrameCode(self, buff): self.writeStopTestCode(buff) buff.writeIndented("%(name)s.status = FINISHED\n" % self.params) # to get out of the if statement - buff.setIndentLevel(-1, relative=True) + buff.setIndentLevel(-2, relative=True) buff.writeIndented("if %(name)s.status == STARTED:\n" % self.params) buff.setIndentLevel(1, relative=True) # to get out of if statement diff --git a/psychopy/experiment/components/joystick/__init__.py b/psychopy/experiment/components/joystick/__init__.py index 4bc237f649..be95475be4 100755 --- a/psychopy/experiment/components/joystick/__init__.py +++ b/psychopy/experiment/components/joystick/__init__.py @@ -393,7 +393,7 @@ def writeFrameCode(self, buff): self.writeStopTestCode(buff) buff.writeIndented("%(name)s.status = FINISHED\n" % self.params) # to get out of the if statement - buff.setIndentLevel(-1, relative=True) + buff.setIndentLevel(-2, relative=True) # if STARTED and not FINISHED! code = ("if %(name)s.status == STARTED: " diff --git a/psychopy/experiment/components/keyboard/__init__.py b/psychopy/experiment/components/keyboard/__init__.py index 6db7f18a9c..82dedad5f9 100644 --- a/psychopy/experiment/components/keyboard/__init__.py +++ b/psychopy/experiment/components/keyboard/__init__.py @@ -229,7 +229,7 @@ def writeFrameCode(self, buff): self.writeStopTestCode(buff) buff.writeIndented("%(name)s.status = FINISHED\n" % self.params) # to get out of the if statement - buff.setIndentLevel(-1, relative=True) + buff.setIndentLevel(-2, relative=True) buff.writeIndented("if %s.status == STARTED%s:\n" % (self.params['name'], ['', ' and not waitOnFlip'][visualSync])) diff --git a/psychopy/experiment/components/mouse/__init__.py b/psychopy/experiment/components/mouse/__init__.py index ee26b6d1a6..ee28ab2b1c 100644 --- a/psychopy/experiment/components/mouse/__init__.py +++ b/psychopy/experiment/components/mouse/__init__.py @@ -275,7 +275,7 @@ def writeFrameCode(self, buff): self.writeStopTestCode(buff) buff.writeIndented("%(name)s.status = FINISHED\n" % self.params) # to get out of the if statement - buff.setIndentLevel(-1, relative=True) + buff.setIndentLevel(-2, relative=True) # if STARTED and not FINISHED! code = ("if %(name)s.status == STARTED: " diff --git a/psychopy/experiment/components/movie/__init__.py b/psychopy/experiment/components/movie/__init__.py index 018cf7c03a..f1bcc9b97b 100644 --- a/psychopy/experiment/components/movie/__init__.py +++ b/psychopy/experiment/components/movie/__init__.py @@ -240,7 +240,7 @@ def writeFrameCode(self, buff): self.writeStopTestCode(buff) buff.writeIndented("%(name)s.setAutoDraw(False)\n" % self.params) # to get out of the if statement - buff.setIndentLevel(-1, relative=True) + buff.setIndentLevel(-2, relative=True) # set parameters that need updating every frame # do any params need updating? (this method inherited from _base) if self.checkNeedToUpdate('set every frame'): diff --git a/psychopy/experiment/components/parallelOut/__init__.py b/psychopy/experiment/components/parallelOut/__init__.py index df3d20fc4e..43b8fbba8a 100644 --- a/psychopy/experiment/components/parallelOut/__init__.py +++ b/psychopy/experiment/components/parallelOut/__init__.py @@ -126,7 +126,7 @@ def writeFrameCode(self, buff): buff.writeIndented(code) # to get out of the if statement - buff.setIndentLevel(-1, relative=True) + buff.setIndentLevel(-2, relative=True) # dedent # buff.setIndentLevel(-dedentAtEnd, relative=True)#'if' statement of the diff --git a/psychopy/experiment/components/pump/__init__.py b/psychopy/experiment/components/pump/__init__.py index caf2270822..6723d90138 100644 --- a/psychopy/experiment/components/pump/__init__.py +++ b/psychopy/experiment/components/pump/__init__.py @@ -173,7 +173,7 @@ def writeFrameCode(self, buff): code = '%(name)s.stop()\n' % self.params buff.writeIndentedLines(code) - buff.setIndentLevel(-1, relative=True) + buff.setIndentLevel(-2, relative=True) def writeRoutineEndCode(self, buff): # Make sure that we stop the pumps even if the routine has been diff --git a/psychopy/experiment/components/settings/__init__.py b/psychopy/experiment/components/settings/__init__.py index 086c3a923d..e70f724701 100644 --- a/psychopy/experiment/components/settings/__init__.py +++ b/psychopy/experiment/components/settings/__init__.py @@ -672,6 +672,8 @@ def writeStartCode(self, buff, version): buff.writeIndentedLines("\nendExpNow = False # flag for 'escape'" " or other condition => quit the exp\n") + buff.writeIndented("frameTolerance = 0.001 # how close to onset before 'same' frame\n") + def writeWindowCode(self, buff): """Setup the window code. """ diff --git a/psychopy/experiment/components/sound/__init__.py b/psychopy/experiment/components/sound/__init__.py index 18ea4fa01d..05f5604671 100644 --- a/psychopy/experiment/components/sound/__init__.py +++ b/psychopy/experiment/components/sound/__init__.py @@ -84,8 +84,11 @@ def writeInitCode(self, buff): inits['stopVal'].val = -1 elif float(inits['stopVal'].val) > 2: inits['stopVal'].val = -1 - buff.writeIndented("%s = sound.Sound(%s, secs=%s, stereo=%s)\n" % - (inits['name'], inits['sound'], inits['stopVal'], self.exp.settings.params['Force stereo'])) + buff.writeIndented("%s = sound.Sound(%s, secs=%s, stereo=%s, hamming=%s,\n" + " name='%s')\n" % + (inits['name'], inits['sound'], inits['stopVal'], + self.exp.settings.params['Force stereo'], + inits['hamming'], inits['name'])) buff.writeIndented("%(name)s.setVolume(%(volume)s)\n" % (inits)) def writeRoutineStartCode(self, buff): @@ -154,7 +157,7 @@ def writeFrameCode(self, buff): " %(name)s.stop()\n") buff.writeIndentedLines(code % self.params) # because of the 'if' statement of the time test - buff.setIndentLevel(-1, relative=True) + buff.setIndentLevel(-2, relative=True) def writeFrameCodeJS(self, buff): """Write the code that will be called every frame @@ -199,7 +202,7 @@ def writeRoutineEndCode(self, buff): code = "%s.stop() # ensure sound has stopped at end of routine\n" buff.writeIndented(code % self.params['name']) # get parent to write code too (e.g. store onset/offset times) - super().writeRoutineEndCode(buff) + super().writeRoutineEndCode(buff) # noinspection def writeRoutineEndCodeJS(self, buff): code = "%s.stop(); // ensure sound has stopped at end of routine\n" diff --git a/psychopy/experiment/routine.py b/psychopy/experiment/routine.py index 0c8cc6f5e1..2c8f5f6ce0 100644 --- a/psychopy/experiment/routine.py +++ b/psychopy/experiment/routine.py @@ -156,10 +156,12 @@ def writeMainCode(self, buff): buff.setIndentLevel(1, True) # on each frame code = ('# get current time\n' - 't = %s.getTime()\n' + 't = {clockName}.getTime()\n' + 'tThisFlip = win.getFutureFlipTime(clock={clockName})\n' + 'tThisFlipGlobal = win.getFutureFlipTime(clock=None)\n' 'frameN = frameN + 1 # number of completed frames ' '(so 0 is the first frame)\n') - buff.writeIndentedLines(code % self._clockName) + buff.writeIndentedLines(code.format(clockName=self._clockName)) # write the code for each component during frame buff.writeIndentedLines('# update/draw components on each frame\n') diff --git a/psychopy/visual/window.py b/psychopy/visual/window.py index c45be802e9..32a151d199 100644 --- a/psychopy/visual/window.py +++ b/psychopy/visual/window.py @@ -23,6 +23,8 @@ from psychopy.contrib.lazy_import import lazy_import from psychopy import colors +from psychopy.clock import monotonicClock + # try to find avbin (we'll overload pyglet's load_library tool and then # add some paths) haveAvbin = False @@ -780,6 +782,40 @@ def timeOnFlip(self, obj, attrib): """ self.callOnFlip(self._assignFlipTime, obj, attrib) + def getFutureFlipTime(self, targetTime=0, clock=None): + """The expected time of the next screen refresh. This is currently + calculated as win._lastFrameTime + refreshInterval + + Parameters + ----------- + targetTime: float + The delay from now for which you want the flip time. 0 will give the + because that the earliest we can achieve. 0.15 will give the schedule + flip time that gets as close to 150 ms as possible + clock : None, 'ptb' or any Clock object + If True then the time returned is compatible with ptb.GetSecs() + """ + baseClock = logging.defaultClock + if not self.monitorFramePeriod: + raise AttributeError("Cannot calculate nextFlipTime due to unknown " + "monitorFramePeriod") + lastFlip = self._frameTimes[-1] # self.lastFrameTime is not always on. This is + timeNext = lastFlip + self.monitorFramePeriod + now = baseClock.getTime() + if (now + targetTime) > timeNext: + extraFrames = round((targetTime - timeNext)/self.monitorFramePeriod) + thisT = timeNext + extraFrames*self.monitorFramePeriod + else: + thisT = timeNext + # convert back to target clock timebase + if clock=='ptb': # add back the lastResetTime (that's the clock difference) + return thisT + baseClock.getLastResetTime() + elif clock: + return thisT + baseClock.getLastResetTime() - clock.getLastResetTime() + else: + return thisT + + def _assignFlipTime(self, obj, attrib): """Helper function to assign the time of last flip to the obj.attrib @@ -954,6 +990,7 @@ def flip(self, clearBuffer=True): # get timestamp self._frameTime = now = logging.defaultClock.getTime() + self._frameTimes.append(monotonicClock.getTime()) # run other functions immediately after flip completes for callEntry in self._toCall: From 12d478172be950399a3d0abe196ccfebf712dac4 Mon Sep 17 00:00:00 2001 From: Jon Peirce Date: Thu, 8 Aug 2019 14:59:42 +0100 Subject: [PATCH 4/4] TESTS: failed to update eh test for the new argument --- psychopy/tests/test_all_visual/test_winFlipTiming.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/psychopy/tests/test_all_visual/test_winFlipTiming.py b/psychopy/tests/test_all_visual/test_winFlipTiming.py index c3baa99578..a7044b81d5 100644 --- a/psychopy/tests/test_all_visual/test_winFlipTiming.py +++ b/psychopy/tests/test_all_visual/test_winFlipTiming.py @@ -22,12 +22,14 @@ def teardown_class(self): def _runSeriesOfFlips(self, usePTB): if usePTB: getTime = GetSecs + clk = 'ptb' else: getTime = clock.monotonicClock.getTime + clk = None self.win.flip() now = clock.monotonicClock.getTime() - next = self.win.getFutureFlipTime(ptb=usePTB) + next = self.win.getFutureFlipTime(clock=clk) errsNext = [] # check nextFrame against reality for 10 frames @@ -38,7 +40,7 @@ def _runSeriesOfFlips(self, usePTB): ## print('err', next-this) errsNext.append(next-this) #then update next - next = self.win.getFutureFlipTime(ptb=usePTB) + next = self.win.getFutureFlipTime(clock=clk) ## print('next', next) return errsNext