Roving oddball#

Here we will follow the methods section from Canales-Johnson et al. (2021). We chose this because it’s recent research, because it was one of the first hits on Scopus, and because it’s open access. We will go over this section bit by bit and recreate the stimuli.

Here’s the relevant section:

“We adopted a roving oddball paradigm (Cowan et al., 1993; Haenschel et al., 2005; Garrido et al., 2008). The trains of 3, 5, or 11 repetitive single tones of 20 different frequencies (250–6727 Hz with intervals of one-quarter octave) were pseudorandomly presented. Tones were identical within each tone train but differed between tone trains (Fig. 1A). Because tone trains followed on from one another continuously, the first tone of a train was considered to be an unexpected deviant tone, because it was of a different frequency than that of the preceding train. The final tone was considered to be an expected standard tone because it was preceded by several repetitions of this same tone. To avoid analytical artifacts stemming from differences in the number of standard and deviant stimuli, we considered only the last tone of a train as standard. There were 240 changes from standard to deviant tones in a single recording session. Pure sinusoidal tones lasted 64 ms (7 ms rise/fall), and stimulus onset asynchrony was 503 ms. Stimulus presentation was controlled by MATLAB (MathWorks) using the Psychophysics Toolbox extensions (Brainard, 1997; Pelli, 1997; Kleiner et al., 2007). Tones were presented through two audio speakers (Fostex) with an average intensity of 60 dB SPL around the ear of the animal.”

Imports and random number generator#

Before we start, let’s import the necessary classes from thebeat and NumPy, and make a numpy.random.Generator object with a seed. If you are not familiar with NumPy random generators and they confuse you, please refer to the NumPy manual.

We use a chosen seed so you we will get the same output as we.

[1]:
from thebeat import Sequence, SoundStimulus, SoundSequence
import numpy as np

rng = np.random.default_rng(seed=123)

Summary#

“The trains of 3, 5, or 11 repetitive single tones of 20 different frequencies (250–6727 Hz with intervals of one-quarter octave) were pseudorandomly presented. Tones were identical within each tone train but differed between tone trains (Fig. 1A). […] Pure sinusoidal tones lasted 64 ms (7 ms rise/fall), and stimulus onset asynchrony was 503 ms.”

So, we create 20 stimuli with the given frequencies, and make trains with them that are either 3, 5, or 11 tones long.


Creating the Sequences#

Most of the time, it’s conceptually the easiest to start with the Sequence object(s). Here, there will be three (with 3, 5, and 11 events). The sequences are isochronous and the inter-onset interval is 503 milliseconds.

Importantly, we will want to be able to join the sequences together at the end so we get one long train of sounds. Normally, a sequence of 3 events will have 2 inter-onset intervals (IOIs). Why that is we’ll quickly visualize by plotting a simple sequence:

[3]:
seq = Sequence.generate_isochronous(n_events=3, ioi=500)
seq.plot_sequence(figsize=(4, 2));
../../_images/examples_creating_stimuli_oddball_6_0.png

As you can see, the IOIs are the intervals between the events, meaning that for n events we have n-1 IOIs.

If we were to join sequences like these together, the final sound of a sequence and the first sound of the next sequence would coincide. To fix this, we can use the end_with_interval=True flag.

So let’s create the sequences:

[4]:
seq_3 = Sequence.generate_isochronous(n_events=3, ioi=503, end_with_interval=True)
seq_5 = Sequence.generate_isochronous(n_events=5, ioi=503, end_with_interval=True)
seq_11 = Sequence.generate_isochronous(n_events=11, ioi=503, end_with_interval=True)
# And add them to a list we'll call sequences
sequences = [seq_3, seq_5, seq_11]

Now, these sequences look like this, and can thus be joined together later:

[5]:
seq_3.plot_sequence(figsize=(4, 2));
../../_images/examples_creating_stimuli_oddball_10_0.png

Creating the stimuli#

Next, we’ll create the SoundStimulus objects, which will contain the sounds.

[6]:
# Make an array that contains the 20 frequencies we'll use. We'll use numpy.linspace for that.
freqs = np.linspace(start=250, stop=6727, num=20)
print(freqs)
[ 250.          590.89473684  931.78947368 1272.68421053 1613.57894737
 1954.47368421 2295.36842105 2636.26315789 2977.15789474 3318.05263158
 3658.94736842 3999.84210526 4340.73684211 4681.63157895 5022.52631579
 5363.42105263 5704.31578947 6045.21052632 6386.10526316 6727.        ]
[7]:
# Loop over those frequencies, and create a list with generated SoundStimulus sound objects
stimuli = []
for freq in freqs:
    stim = SoundStimulus.generate(freq=freq, duration_ms=64, onramp_ms=7, offramp_ms=7)
    stimuli.append(stim)

# We now have a list of Stimulus objects. Remember that they all have different frequencies
print(stimuli)
[SoundStimulus(duration_ms=64.0), SoundStimulus(duration_ms=64.0), SoundStimulus(duration_ms=64.0), SoundStimulus(duration_ms=64.0), SoundStimulus(duration_ms=64.0), SoundStimulus(duration_ms=64.0), SoundStimulus(duration_ms=64.0), SoundStimulus(duration_ms=64.0), SoundStimulus(duration_ms=64.0), SoundStimulus(duration_ms=64.0), SoundStimulus(duration_ms=64.0), SoundStimulus(duration_ms=64.0), SoundStimulus(duration_ms=64.0), SoundStimulus(duration_ms=64.0), SoundStimulus(duration_ms=64.0), SoundStimulus(duration_ms=64.0), SoundStimulus(duration_ms=64.0), SoundStimulus(duration_ms=64.0), SoundStimulus(duration_ms=64.0), SoundStimulus(duration_ms=64.0)]

Creating the SoundSequences#

We will now create SoundSequence objects, which will basically be the trials.

So, following the method section we need to combine the 20 stimuli we created above with the 3 different sequences, i.e. 60 combinations. That’s easily done using a nested for-loop:

[8]:
trials = []

for seq in sequences:
    for stim in stimuli:
        trial = SoundSequence(stim, seq)
        trials.append(trial)

# Confirm there's 60:
print(f"We have {len(trials)} trials")

# Let's plot one of the trials to see what they look like:
trials[2].plot_waveform();
We have 60 trials
../../_images/examples_creating_stimuli_oddball_15_1.png

Creating the trains#

Finally, we shuffle all combinations and join them using the plus operator to form one (very) long train of trials. For this example, we are not going to create a train using all trials; that would be a bit too long to plot or save etc. So we here only join the first 10 trials.

[9]:
# Shuffle the trials (we created the rng object all the way at the beginning of this tutorial)
rng.shuffle(trials)

# Initialize the train by getting the first Sequence
train = trials[0]

# Then we add to that train the next 9
for i in range(1,10):
    train = train + trials[i]

# Let's see what it looks like
train.plot_sequence(title="Stimulus train event plot", figsize=(12, 2));
train.plot_waveform(title="Stimulus train waveform", figsize=(12, 2));

# If you want, you can save the wav or play it (both of which we'll not do here)

#train.write_wav('train.wav')
#train.play()
../../_images/examples_creating_stimuli_oddball_17_0.png
../../_images/examples_creating_stimuli_oddball_17_1.png
[10]:
# You can listen to the sound here. You can ignore this code, it's only for this website.
# In your Python editor you would simply use train.play()
from IPython.display import Audio
Audio(data=train.samples, rate=train.fs)
[10]:

Code summary#

[11]:
from thebeat.core import Sequence, SoundStimulus, SoundSequence
import numpy as np

rng = np.random.default_rng(seed=123)

seq_3 = Sequence.generate_isochronous(n_events=3, ioi=503, end_with_interval=True)
seq_5 = Sequence.generate_isochronous(n_events=5, ioi=503, end_with_interval=True)
seq_11 = Sequence.generate_isochronous(n_events=11, ioi=503, end_with_interval=True)
sequences = [seq_3, seq_5, seq_11]

freqs = np.linspace(start=250, stop=6727, num=20)

trials = []

for seq in sequences:
    for stim in stimuli:
        trial = SoundSequence(stim, seq)
        trials.append(trial)

rng.shuffle(trials)

train = trials[0]
for i in range(1, 60):
    train = train + trials[i]

#train.write_wav('train.wav')
#train.play()