diff --git a/examples/sync/sync_test.py b/examples/sync/sync_test.py index f59ef418..dcab80c6 100644 --- a/examples/sync/sync_test.py +++ b/examples/sync/sync_test.py @@ -40,6 +40,8 @@ from expyfun import ExperimentController, building_doc from expyfun.visual import Circle, Rectangle +USE_VPIXX = False +VPIXX_COLOR = [0, 1, 2, 3, 4, 5, 6, 7] # full red n_channels = 2 click_idx = [0] with ExperimentController( @@ -64,9 +66,12 @@ circle = Circle(ec, 1, units="deg", fill_color="k", line_color="w") # Make a rectangle that is the standard credit card size rect = Rectangle(ec, [0, 0, 8.56, 5.398], "cm", None, "#AA3377") + # set the vpixx trigger pixel + if USE_VPIXX: + ec.set_vpixx_color(VPIXX_COLOR) while pressed != "8": # enable a clean quit if required ec.set_background_color("white") - t1 = ec.start_stimulus(start_of_trial=False) # skip checks + t1 = ec.start_stimulus(start_of_trial=False, vpixx=USE_VPIXX) # skip checks ec.set_background_color("black") t2 = ec.flip() diff = round(1000 * (t2 - t1), 2) diff --git a/examples/sync/sync_test_dualmon.py b/examples/sync/sync_test_dualmon.py index 42cbda96..f2689dcd 100644 --- a/examples/sync/sync_test_dualmon.py +++ b/examples/sync/sync_test_dualmon.py @@ -39,11 +39,11 @@ from expyfun import ExperimentController, building_doc from expyfun.visual import Circle, Rectangle -print(__doc__) - SCREEN = 0 FULL = False WIN_SIZE = (1000, 1000) # or None for full screen if size matches config +USE_VPIXX = True +VPIXX_COLOR = [0, 1, 2, 3, 4, 5, 6, 7] # full red n_channels = 2 click_idx = [0] @@ -71,9 +71,12 @@ circle = Circle(ec, 1, units="deg", fill_color="k", line_color="w") # Make a rectangle that is the standard credit card size rect = Rectangle(ec, [0, 0, 8.56, 5.398], "cm", None, "#AA3377") + # set the vpixx trigger pixel + if USE_VPIXX: + ec.set_vpixx_color(VPIXX_COLOR) while pressed != "8": # enable a clean quit if required ec.set_background_color("white") - t1 = ec.start_stimulus(start_of_trial=False) # skip checks + t1 = ec.start_stimulus(start_of_trial=False, vpixx=USE_VPIXX) # skip checks ec.set_background_color("black") t2 = ec.flip() diff = round(1000 * (t2 - t1), 2) @@ -86,5 +89,3 @@ ec.refocus() pressed = ec.wait_one_press(0.5)[0] if not building_doc else "8" ec.stop() - -# ea.plot_screen(screenshot) diff --git a/expyfun/_experiment_controller.py b/expyfun/_experiment_controller.py index 42c8a0bb..5e12c0b0 100644 --- a/expyfun/_experiment_controller.py +++ b/expyfun/_experiment_controller.py @@ -581,6 +581,7 @@ def __init__( car = sum([np.sin(2 * np.pi * f * t) for f in [800, 1000, 1200]]) self._beep = None self._beep_data = np.tile(car * np.exp(-t * 10) / 4, (2, 3)) + self.vpixx_color = () # finish initialization logger.info("Expyfun: Initialization complete") @@ -794,7 +795,31 @@ def set_background_color(self, color="black"): self._bgcolor = _convert_color(color) gl.glClearColor(*[c / 255.0 for c in self._bgcolor]) - def start_stimulus(self, start_of_trial=True, flip=True, when=None): + def set_vpixx_color(self, bits=()): + """Calculate color for vpixx "pixel mode" triggering, from the desired bits. + + Parameters + ---------- + bits: array-like of int + The bits to be set high (0-indexed). Values between 0 and 23 are valid. + Note that these are the RGB bits *not the DSUB pins*; pins 1-4 and 14-17 + control the red value, pins 5-8 and 18-21 control the green value, and pins + 9-12 and 22-25 control the blue value (13 is ground). See + https://docs.vpixx.com/vocal/sending-triggers-with-pixel-mode for details. + """ + if not len(bits): + self.vpixx_color = () + return + bits = np.array(bits, dtype=int) + assert bits.min() >= 0 and bits.max() < 24, ( + "Vpixx color bits must be between 0 and 23" + ) + trig_out = np.exp2(bits).sum(dtype=int).item() + col = (trig_out % 256, (trig_out >> 8) % 256, (trig_out >> 16) % 256) + # this gets (unavoidably) passed to _convert_color later; must be in range [0,1] + self.vpixx_color = np.array(col) / 255 + + def start_stimulus(self, start_of_trial=True, flip=True, when=None, vpixx=False): """Play audio, (optionally) flip screen, run any "on_flip" functions. Parameters @@ -812,6 +837,9 @@ def start_stimulus(self, start_of_trial=True, flip=True, when=None): flip completes (if `flip` is ``True``). As a result, in some cases `when` should be set to a value smaller than your true intended flip time. + vpixx : bool + Whether to display the vpixx trigger pixel on flip. See + :meth:`ExperimentController.flip` for details. Ignored if ``flip`` is False. Returns ------- @@ -844,7 +872,7 @@ def start_stimulus(self, start_of_trial=True, flip=True, when=None): self._on_next_flip = ( [self._play] + self._ofp_critical_funs + self._on_next_flip ) - stimulus_time = self.flip(when) + stimulus_time = self.flip(when, vpixx=vpixx) else: if when is not None: self.wait_until(when) @@ -1150,7 +1178,7 @@ def _setup_window(self, window_size, exp_name, full_screen, screen): % (window_size, screen, self.dpi) ) - def flip(self, when=None): + def flip(self, when=None, vpixx=False): """Flip screen, then run any "on-flip" functions. Parameters @@ -1161,6 +1189,11 @@ def flip(self, when=None): absolute) wait time before the flip completes. As a result, in some cases `when` should be set to a value smaller than your true intended flip time. + vpixx : bool + Whether to display the vpixx trigger pixel on this flip. Useful only with + Vpixx projectors set in "pixel mode". Will use the value of + ``ExperimentController.vpixx_color``, which can be computed from the desired + output trigger values with :meth:`~ExperimentController.set_vpixx_color`. Returns ------- @@ -1191,6 +1224,21 @@ def flip(self, when=None): if self.safe_flipping: # On NVIDIA Linux these calls cause a 2x delay (33ms instead of 16) gl.glFinish() + if vpixx: + if not len(self.vpixx_color): + raise RuntimeError( + "Vpixx pixel color not set; did you forget to call " + "ec.set_vpixx_color() ?" + ) + rect = Rectangle( + ec=self, + pos=(0.5, self.window_size_pix[1] - 0.5, 1, 1), + units="pix", + fill_color=self.vpixx_color, + line_color=None, + line_width=0.0, + ) + rect.draw() self._win.flip() # this waits until everything is called, including last draw self._clear_rect.draw()