Direct, Audio & Video Functions
Your project’s code directory can hold three files that work together:
direct_functions.py— numbered, self-contained actions (play a sound, draw a stimulus, toggle a camera annotation, …) that can be triggered from several different places: a GUI button, a camera/GPIO/touchscreen trigger, a Bpod softcode, or a direct call from your task’s own code.sound_functions.pyandvideo_functions.py— small libraries of generator functions that direct functions (or tasks) call to actually produce a sound or a visual stimulus, built on top of thesound_deviceandscreenAPIs described in Sound Integration and Screen Integration.
Direct Functions
Create a file named direct_functions.py inside your project’s code directory
and define a class named DirectFunctions that inherits from DirectFunctionsBase.
Each method must be named functionN (N from 1 to 98) and take no arguments other
than self. Its first line, as a docstring, is a short label — it’s what shows up
on the corresponding button in the GUI, so keep it brief.
from village.custom_classes.direct_functions_base import DirectFunctionsBase
from village.devices.camera import cam_box
class DirectFunctions(DirectFunctionsBase):
def function1(self):
"""Camera MSG ON"""
# self.task gives access to the currently running task, so you can read
# anything it defines (settings, calibrations, custom attributes, ...)
cam_box.annotation = "ON"
def function2(self):
"""Clear Camera MSG"""
cam_box.annotation = ""
Note
The system detects the class automatically — no registration needed. See this
project’s own direct_functions.py for a fuller set of examples covering sound
playback and on-screen stimuli.
Ways to trigger a direct function
GUI — the
FUNCTIONStab shows one button per registered function, labeled with its docstring. Clicking it calls that function right away.Camera trigger — call
self.task.execute_function(N)insideCameraTriggerBase.trigger. See Camera Triggers.GPIO trigger — call
self.task.execute_function(N)insidetrigger_on/trigger_off. See Custom GPIO Interaction.Touchscreen trigger — call
self.task.execute_function(N)insideTouchTriggerBase.trigger. See Touchscreen.Bpod softcodes — if
BEHAVIOR_CONTROLLERisBPOD, functions 1-98 can be triggered automatically from inside the state machine (see below).OTHERcontroller (a microcontroller you talk to yourself, or none at all) — there is no state-machine layer to trigger functions for you, so callself.execute_function(N)directly from your task’s own code (create_trial,after_trial, or anywhere else), whenever you want it to run.
Bpod softcodes
A softcode is a short message the Bpod state machine sends back to the Raspberry
Pi while a trial is running, without waiting for the trial to end. You trigger
one by including BpodOutput.SoftCodeN in a state’s output_actions, alongside
any other output (opening a valve, turning on an LED, …). N goes from 1 to 98,
matching direct functions 1-to-1. The moment the state machine executes that
output, Village receives the message and automatically calls
execute_function(N) — this is how a Bpod task can trigger a direct function mid
-trial, from inside the state machine, instead of only between trials.
self.bpod.add_state(
state_name="reward",
state_timer=0.5,
state_change_conditions={BpodEvent.Tup: "exit"},
# opens Valve1 AND triggers function6, at the same time
output_actions=[("Valve1", 1), BpodOutput.SoftCode6],
)
This is not the only softcode channel
The example above is Bpod → Raspberry Pi. There is also a channel the other way
around — the task can send a softcode from the Raspberry Pi to Bpod
(self.task.bpod.send_softcode_to_bpod(N)), for the state machine to branch on
via state_change_conditions={"SoftCodeN": "next_state"}. That’s a different
mechanism (a Bpod input condition, not a direct-function trigger) — see
the “Example” section of Touchscreen.
Audio Functions
sound_functions.py holds two kinds of functions:
Generic generators (e.g.
tone_generator,whitenoise_generator) that take whatever arguments they need and return a numpy waveform array. Call them from anywhere — a direct function, a task, a trigger — and pass the result tosound_device.load().Calibration-sound wrappers — thin wrappers around a generic generator that fix every argument except
durationandgain, since that’s the signature the Sound Calibration panel always calls them with. They are listed insound_calibration_functions, which fills theSOUNDdropdown in the Sound Calibration panel.
def tone_generator(duration: float, gain: float, frequency: int):
... # returns a numpy array
def tone_600(duration: float, gain: float):
return tone_generator(duration=duration, gain=gain, frequency=600)
sound_calibration_functions = [tone_600, ...]
Keep names in sync
A wrapper’s __name__ ("tone_600" above) is stored as the sound’s identity in
the calibration data, and is what you pass as sound_name= to
get_sound_gain(...) everywhere else in the project. Rename a wrapper here and
every call site referencing its old name has to be updated too, or the
calibration lookup silently stops matching and raises a “not calibrated” error.
Video Functions
video_functions.py holds draw_X_generator(...) factories: each one takes the
stimulus parameters and returns a draw() closure, meant to be passed to
screen.load_draw_function(draw_fn=...) (see
Screen Integration). Qt then calls that draw()
on every frame while screen.start_drawing() is active.
from PyQt5.QtCore import QRect, Qt
from PyQt5.QtGui import QPainter
def draw_circle_generator(duration, x_pos, y_pos, diameter, color):
def draw():
with QPainter(screen) as painter:
painter.fillRect(painter.viewport(), screen.background_color)
if screen.elapsed_time < duration:
painter.setPen(Qt.NoPen)
painter.setBrush(color)
painter.drawEllipse(QRect(x_pos, y_pos, diameter, diameter))
return draw
Two things every draw() needs to do itself, since nothing does them
automatically:
Fill the background first (
screen.background_color) — Qt doesn’t clear the widget between paints, so skipping this leaves the previous frame’s stimulus on screen underneath the new one.Check
screen.elapsed_time < durationbefore painting anything — once the duration is over,draw()just stops drawing the stimulus (falling back to the plain background) until a new draw function is loaded.
Adding your own functions
sound_functions.py and video_functions.py are plain modules, not subclasses —
unlike direct_functions.py (or camera_trigger.py, gpio_trigger.py, …),
there is only ever one of them per project, so there’s nothing to discover or
swap automatically. Add a new generator alongside the existing ones and import it
directly wherever you need it, the same way direct_functions.py already does:
from sound_functions import tone_generator, whitenoise_generator
from video_functions import draw_circle_generator, draw_rectangle_generator