Example: A Simple Auditory Experiment
To help you understand how all the pieces of psiexperiment fit together, let’s walk through the creation of a simple Tone-in-Noise detection experiment. In this experiment: 1. A continuous background noise is played. 2. At random intervals, a brief tone pip is presented. 3. The user can adjust the frequency and level of the tone pip via the GUI. 4. Data (both the raw signal and experimental events) is saved to disk.
1. The IO Manifest
First, we define our hardware configuration in my_io.enaml. We’ll assume a National Instruments DAQ with one output for the speaker and one input for a monitoring microphone.
from psi.controller.api import IOManifest, NIEngine, AnalogOutput, AnalogInput
enamldef MyIO(IOManifest):
id = 'my_hardware'
NIEngine:
name = 'NI-DAQmx'
dev_name = 'Dev1'
master_clock = True
AnalogOutput: speaker:
name = 'speaker'
channel = 'ao0'
fs = 100000.0
AnalogInput: microphone:
name = 'microphone'
channel = 'ai0'
fs = 100000.0
2. The Experiment Manifest
Now, we define the logic in tone_in_noise.enaml. We’ll contribute to several core plugins.
Defining the Stimuli
We contribute to psi.token.tokens to define our noise and tone.
Extension:
point = 'psi.token.tokens'
# Continuous background noise
Noise:
name = 'background_noise'
# Brief tone pip
Token:
name = 'tone_pip'
Tone:
freq = C.tone_frequency
level = C.tone_level
CosineEnvelope:
duration = 0.05
rise_time = 0.005
Connecting to Hardware
We contribute to psi.controller.io to wire these signals to our speaker.
Extension:
point = 'psi.controller.io'
# Continuous background masker
ContinuousOutput: masker:
name = 'masker_output'
source = 'background_noise'
target_name = 'speaker'
# On-demand target tone
EpochOutput: target:
name = 'target_output'
source = 'tone_pip'
target_name = 'speaker'
Defining Parameters
We contribute to psi.context.items to expose settings to the GUI.
Extension:
point = 'psi.context.items'
Parameter:
name = 'tone_frequency'
label = 'Frequency (Hz)'
default = 4000.0
Parameter:
name = 'tone_level'
label = 'Level (dB SPL)'
default = 60.0
Controlling the Flow
We use psi.controller.actions to automate the experiment.
Extension:
point = 'psi.controller.actions'
# Initialize context on start
ExperimentAction:
event = 'experiment_initialize'
command = 'psi.context.initialize'
# Play a tone when a trial starts
ExperimentAction:
event = 'trial_start'
command = 'target_output.start'
3. The Paradigm Description
Finally, we bundle these into a paradigm in descriptions.py.
from psi.experiment.api import ParadigmDescription
ParadigmDescription(
'tone_noise', 'Tone-in-Noise Experiment', 'auditory', [
{'manifest': 'tone_in_noise.MyExperimentManifest'},
],
)
How It Works Together
Launch: Run
psi tone_noise --io my_hardware.Initialize: When you click Start, the
experiment_initializeevent fires, callingpsi.context.initialize.IO Setup: The controller plugin sees the
ContinuousOutputand immediately starts streaming thebackground_noiseto the speaker.Running: When your code (or a timer) fires the
trial_startevent, thetarget_output.startcommand is called, which pulls a 50ms tone from thetone_pipgenerator and mixes it into the speaker stream.Data: Any configured
Sink(inpsi.data.sinks) will see the events and data streams and write them to the data folder you selected.
This simple example demonstrates how psiexperiment handles the complex task of multi-signal mixing, hardware synchronization, and parameter management through a few lines of declarative code.