Repository: psychopy/psychopy

Issue:
mouseMoveTime() returns negative values
...at least on Macs. Likely an easy fix.



Discussion:

Resolving patch:
diff --git a/psychopy/event.py b/psychopy/event.py
--- a/psychopy/event.py
+++ b/psychopy/event.py
@@ -436,7 +436,7 @@ def mouseMoved(self, distance=None, reset=False):
     def mouseMoveTime(self):
         global mouseMove
         if mouseMove:
-                return psychopy.core.getTime()-mouseMove.getLastResetTime()
+                return mouseMove.getTime()
         else: return 0 # mouseMove clock not started
 
     def getRel(self):
diff --git a/psychopy/sound.py b/psychopy/sound.py
--- a/psychopy/sound.py
+++ b/psychopy/sound.py
@@ -303,29 +303,27 @@ class SoundPyo(_SoundBase):
     """Create a sound object, from one of MANY ways.
     """
     def __init__(self, value="C", secs=0.5, octave=4, stereo=True, volume=1.0,
-                 loops=0, sampleRate=44100, bits=16, hamming=True,
+                 loops=0, sampleRate=44100, bits=16, hamming=True, start=0, stop=-1,
                  name='', autoLog=True):
         """
-        value: can be a number, string or an array.
-
-            If it's a number between 37 and 32767 then a tone will be generated at
-            that frequency in Hz.
-            -----------------------------
-            It could be a string for a note ('A','Bfl','B','C','Csh'...)
-            - you may want to specify which octave as well
-            -----------------------------
-            Or a string could represent a filename in the current
-            location, or mediaLocation, or a full path combo
-            -----------------------------
-            Or by giving an Nx2 numpy array of floats (-1:1) you
-            can specify the sound yourself as a waveform
-
-            By default, a Hamming window (8ms duration) will be applied to the
+        value: can be a number, string or an array:
+            * If it's a number between 37 and 32767 then a tone will be generated at that frequency in Hz.
+            * It could be a string for a note ('A','Bfl','B','C','Csh'...). Then you may want to specify which octave as well
+            * Or a string could represent a filename in the current location, or mediaLocation, or a full path combo
+            * Or by giving an Nx2 numpy array of floats (-1:1) you can specify the sound yourself as a waveform
+
+            By default, a Hamming window (5ms duration) will be applied to a
             generated tone, so that onset and offset are smoother (to avoid
             clicking). To disable the Hamming window, set `hamming=False`.
 
-        secs: is only relevant if the value is a note name or
-            a frequency value
+        secs:
+            Duration of a tone. Not used for sounds from a file.
+
+        start : float
+            Where to start playing a sound file; default = 0s (start of the file).
+
+        stop : float
+            Where to stop playing a sound file; default = end of file.
 
         octave: is only relevant if the value is a note name.
             Middle octave of a piano is 4. Most computers won't
@@ -347,8 +345,8 @@ def __init__(self, value="C", secs=0.5, octave=4, stereo=True, volume=1.0,
 
         bits: has no effect for the pyo backend
 
-        hamming: whether to apply a Hamming window for generated tones. Has no
-            effect on sounds from files.
+        hamming: whether to apply a Hamming window (5ms) for generated tones.
+            Not applied to sounds from files.
         """
         global pyoSndServer
         if pyoSndServer==None or pyoSndServer.getIsBooted()==0:
@@ -359,6 +357,8 @@ def __init__(self, value="C", secs=0.5, octave=4, stereo=True, volume=1.0,
         self.isStereo = stereo
         self.channels = 1 + int(stereo)
         self.secs=secs
+        self.startTime = start
+        self.stopTime = stop
         self.autoLog=autoLog
         self.name=name
 
@@ -370,16 +370,18 @@ def __init__(self, value="C", secs=0.5, octave=4, stereo=True, volume=1.0,
         self.setSound(value=value, secs=secs, octave=octave, hamming=hamming)
         self.needsUpdate = False
 
-    def play(self, fromStart=True, loops=None, autoStop=True, log=True):
+    def play(self, loops=None, autoStop=True, log=True):
         """Starts playing the sound on an available channel.
-        If no sound channels are available, it will not play and return None.
 
         loops : int
             (same as above)
 
-        This runs off a separate thread i.e. your code won't wait for the
-        sound to finish before continuing. You need to use a
-        `psychopy.core.wait(mySound.getDuration())` if you want things to pause.
+        For playing a sound file, you cannot specify the start and stop times when
+        playing the sound, only when creating the sound initially.
+
+        Playing a sound runs in a separate thread i.e. your code won't wait for the
+        sound to finish before continuing. To pause while playing, you need to use a
+        `psychopy.core.wait(mySound.getDuration())`.
         If you call `play()` while something is already playing the sounds will
         be played over each other.
         """
@@ -454,7 +456,9 @@ def _setSndFromFile(self, fileName):
         # want mono sound file played to both speakers, not just left / 0
         self.fileName = fileName
         self._sndTable = pyo.SndTable(initchnls=self.channels)
-        self._sndTable.setSound(self.fileName)  # mono file loaded to all chnls
+        # mono file loaded to all chnls:
+        self._sndTable.setSound(self.fileName,
+                                start=self.startTime, stop=self.stopTime)
         self._updateSnd()
         self.duration = self._sndTable.getDur()
 


Test patch:
diff --git a/psychopy/tests/test_misc/test_event.py b/psychopy/tests/test_misc/test_event.py
--- a/psychopy/tests/test_misc/test_event.py
+++ b/psychopy/tests/test_misc/test_event.py
@@ -66,6 +66,12 @@ def test_mouse_clock(self):
         event.stopMoveClock()
         event.resetMoveClock()
 
+        m = event.Mouse()
+        assert m.mouseMoveTime() >= 0
+        t = 0.05
+        core.wait(t, 0)
+        assert t + 0.01 > m.mouseMoveTime() >= t
+
     def test_clearEvents(self):
         for t in ['mouse', 'joystick', 'keyboard', None]:
             event.clearEvents(t)
diff --git a/psychopy/tests/test_sound/test_sound_pyo.py b/psychopy/tests/test_sound/test_sound_pyo.py
--- a/psychopy/tests/test_sound/test_sound_pyo.py
+++ b/psychopy/tests/test_sound/test_sound_pyo.py
@@ -36,6 +36,8 @@ def setup_class(self):
         assert r == 48000
         assert len(d) == 92160
 
+        self.testFile = os.path.join(self.tmp, 'green_48000.wav')
+
     @classmethod
     def teardown_class(self):
         if hasattr(self, 'tmp'):
@@ -58,8 +60,7 @@ def test_init(self):
         points = 100
         snd = numpy.ones(points) / 20
 
-        testFile = os.path.join(self.tmp, 'green_48000.wav')
-        s = sound.Sound(testFile)
+        s = sound.Sound(self.testFile)
 
     def test_play(self):
         s = sound.Sound(secs=0.1)
@@ -70,6 +71,17 @@ def test_play(self):
         s.play(loops=-1)
         s.stop()
 
+    def test_start_stop(self):
+        """only relevant for sound from files"""
+        s1 = sound.Sound(self.testFile, start=0.5, stop=1.5)
+        assert s1.getDuration() == 1
+        s2 = sound.Sound(self.testFile, start=0.5)
+        s3 = sound.Sound(self.testFile)
+        assert s3.getDuration() > s2.getDuration() > s1.getDuration()
+
+        s4 = sound.Sound(self.testFile, start=-1, stop=10000)
+        assert s4.getDuration() == s3.getDuration()
+
     def test_methods(self):
         s = sound.Sound(secs=0.1)
         v = s.getVolume()