Note

An online, interactive version of this example is available at Binder: binder

PsychoPy experiments

Parselmouth also allows Praat functionality to be included in an interactive PsychoPy experiment (refer to the subsection on installing Parselmouth for PsychoPy for detailed installation instructions for the PsychoPy graphical interface, the PsychoPy Builder). The following example shows how easily Python code that uses Parselmouth can be injected in such an experiment; following an adaptive staircase experimental design, at each trial of the experiment a new stimulus is generated based on the responses of the participant. See e.g. Kaernbach, C. (2001). Adaptive threshold estimation with unforced-choice tasks. Attention, Perception, & Psychophysics, 63, 1377–1388., or the PsychoPy tutorial at https://www.psychopy.org/coder/tutorial2.html.

In this example, we use an adaptive staircase experiment to determine the minimal amount of noise that makes the participant unable to distinguish between two audio fragments, “bat” and “bet” (bat.wav, bet.wav). At every iteration of the experiment, we want to generate a version of these audio files with a specific signal-to-noise ratio, of course using Parselmouth to do so. Depending on whether the participant correctly identifies whether the noisy stimulus was “bat” or “bet”, the noise level is then either increased or decreased.

As Parselmouth is just another Python library, using it from the PsychoPy Coder interface or from a standard Python script that imports the psychopy module is quite straightforward. However, PsychoPy also features a so-called Builder interface, which is a graphical interface to set up experiments with minimal or no coding. In this Builder, a user can create multiple experimental ‘routines’ out of different ‘components’ and combine them through ‘loops’, that can all be configured graphically:

PsychoPy Builder interface

For our simple example, we create a single routine trial, with a Sound, a Keyboard, and a Text component. We also insert a loop around this routine of the type staircase, such that PsychoPy will take care of the actual implementation of the loop in adaptive staircase design. The full PsychoPy experiment which can be opened in the Builder can be downloaded here: adaptive_listening.psyexp

Finally, to customize the behavior of the trial routine and to be able to use Parselmouth inside the PsychoPy experiment, we still add a Code component to the routine. This component will allow us to write Python code that interacts with the rest of the components and with the adaptive staircase loop. The Code components has different tabs, that allow us to insert custom code at different points during the execution of our trial.

First, there is the Begin Experiment tab. The code in this tab is executed only once, at the start of the experiment. We use this to set up the Python environment, importing modules and initializing variables, and defining constants:

[1]:
# ** Begin Experiment **

import parselmouth
import numpy as np
import random

conditions = ['a', 'e']
stimulus_files = {'a': "audio/bat.wav", 'e': "audio/bet.wav"}

STANDARD_INTENSITY = 70.
stimuli = {}
for condition in conditions:
    stimulus = parselmouth.Sound(stimulus_files[condition])
    stimulus.scale_intensity(STANDARD_INTENSITY)
    stimuli[condition] = stimulus

The code in the Begin Routine tab is executed before the routine, so in our example, for every iteration of the surrounding staircase loop. This allows us to actually use Parselmouth to generate the stimulus that should be played to the participant during this iteration of the routine. To do this, we need to access the current value of the adaptive staircase algorithm: PsychoPy stores this in the Python variable level. For example, at some point during the experiment, this could be 10 (representing a signal-to-noise ratio of 10 dB):

[2]:
level = 10

To execute the code we want to put in the Begin Routine tab, we need to add a few variables that would be made available by the PsychoPy Builder, normally:

[3]:
# 'filename' variable is also set by PsychoPy and contains base file name of saved log/output files
filename = "data/participant_staircase_23032017"

# PsychoPy also create a Trials object, containing e.g. information about the current iteration of the loop
# So let's quickly fake this, in this example, such that the code can be executed without errors
# In PsychoPy this would be a `psychopy.data.TrialHandler` (https://www.psychopy.org/api/data.html#psychopy.data.TrialHandler)
class MockTrials:
    def addResponse(self, response):
        print("Registering that this trial was {}successful".format("" if response else "un"))
trials = MockTrials()
trials.thisTrialN = 5 # We only need the 'thisTrialN' attribute of the 'trials' variable

# The Sound component can also be accessed by it's name, so let's quickly mock that as well
# In PsychoPy this would be a `psychopy.sound.Sound` (https://www.psychopy.org/api/sound.html#psychopy.sound.Sound)
class MockSound:
    def setSound(self, file_name):
        print("Setting audio file of Sound component to '{}'".format(file_name))
sound_1 = MockSound()

# And the same for our Keyboard component, `key_resp_2`:
class MockKeyboard:
    pass
key_resp_2 = MockKeyboard()

# Finally, let's also seed the random module to have a consistent output across different runs
random.seed(42)
[4]:
# Let's also create the directory where we will store our example output
!mkdir data

Now, we can execute the code that would be in the Begin Routine tab:

[5]:
# ** Begin Routine **

random_condition = random.choice(conditions)
random_stimulus = stimuli[random_condition]

noise_samples = np.random.normal(size=random_stimulus.n_samples)
noisy_stimulus = parselmouth.Sound(noise_samples,
                     sampling_frequency=random_stimulus.sampling_frequency)
noisy_stimulus.scale_intensity(STANDARD_INTENSITY - level)
noisy_stimulus.values += random_stimulus.values
noisy_stimulus.scale_intensity(STANDARD_INTENSITY)

# use 'filename' to save our custom stimuli
stimulus_file_name = filename + "_stimulus_" + str(trials.thisTrialN) + ".wav"
noisy_stimulus.resample(44100).save(stimulus_file_name, 'WAV')
sound_1.setSound(stimulus_file_name)
Setting audio file of Sound component to 'data/participant_staircase_23032017_stimulus_5.wav'

Let’s listen to the file we have just generated and that we would play to the participant:

[6]:
from IPython.display import Audio
Audio(filename="data/participant_staircase_23032017_stimulus_5.wav")
[6]:

In this example, we do not really need to have code executed during the trial (i.e., in the Each Frame tab). However, at the end of the trial, we need to inform the PsychoPy staircase loop whether the participant was correct or not, because this will affect the further execution the adaptive staircase, and thus value of the level variable set by PsychoPy. For this we add a final line in the End Routine tab. Let’s say the participant guessed “bat” and pressed the a key:

[7]:
key_resp_2.keys = 'a'

The End Routine tab then contains the following code to check the participant’s answer against the randomly chosen condition, and to inform the trials object of whether the participant was correct:

[8]:
# ** End Routine **

trials.addResponse(key_resp_2.keys == random_condition)
Registering that this trial was successful
[9]:
# Clean up the output directory again
!rm -r data