Psychtoolbox sound latencies

Hi everyone,

I have two questions, but the first one is the most important:

  1. Does checking the status of a sound object (created with the preferences for audioLib set to ‘ptb’) give me an indication of the lag of this sound. This depends on how the status of a sound component is set under the hood and whether the time point when the status attribute is set to -1 is linked to the offset of the sound. It would be very helpful, if I could use this knowledge of the lag for every sound to correct for it in my reaction time measurements.
from psychopy import prefs
prefs.hardware['audioLib'] = 'ptb'
prefs.hardware['audioLatencyMode'] = '4'
import psychtoolbox as ptb
from psychopy import sound
from psychopy.constants import PLAYING
from psychopy.hardware import keyboard
from psychopy import core

# create a default keyboard 
defaultKeyboard = keyboard.Keyboard()

# create sound and get time
s_len       = 0.01
s1_sound    = sound.Sound('G', secs=s_len, stereo=True, hamming=True, name='s1_sound')
s1_onset    = 2.5
s1_offset   = s1_onset + s_len
PTB_start = ptb.GetSecs()

# play the sound
s1_sound.play(when=PTB_start + s1_onset)
while s1_sound.status == PLAYING:
        if defaultKeyboard.getKeys(keyList=["escape"]):
            core.quit()

# print the delay
print('Delay: {}'.format( ptb.GetSecs() - PTB_start - s1_offset) )

core.wait(2)
  1. In my experiment, I play pairs of sounds. If I check the offset of the sounds, I see a systematic difference in lag for the first and the second sound. The first sound seems to finish earlier, the second seems to finish later than scheduled. Should I account for this systematic difference in lags by adjusting the scheduled sound onset times?

I know that I could improve latencies with a better sound card, and I will run the actual experiment on a PC with a better sound card, but I think my questions are valid irrespective of the sound card.
Here a simplified version of the relevant part of the experiment:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from psychopy import prefs
prefs.hardware['audioLib'] = 'ptb'
prefs.hardware['audioLatencyMode'] = '4'
import psychtoolbox as ptb
from psychopy import sound, visual, core
from psychopy.constants import PLAYING
from psychopy.hardware import keyboard
import numpy as np
from scipy import stats
import matplotlib.pyplot as plt
from os import getcwd

# Setup the Window
win = visual.Window(
    size=[1536, 864], fullscr=True, screen=0, 
    winType='pyglet', allowGUI=False, allowStencil=False,
    monitor='testMonitor', color=[0,0,0], colorSpace='rgb',
    blendMode='avg', useFBO=True, 
    units='height')

# create a default keyboard 
defaultKeyboard = keyboard.Keyboard()

# Initialize sounds
s_len = 0.01
s1_sound = sound.Sound('G', secs=s_len, stereo=True, hamming=True, name='s1_sound')
s2_sound = sound.Sound('A', secs=s_len, stereo=True, hamming=True, name='sound_2')
s1_onset = 2.5; s1_offset = s1_onset + s_len
s2_onset = 3;   s2_offset = s2_onset + s_len
s1_delay = []
s2_delay = []

PTB_start = ptb.GetSecs()
for thisTrial in range(30):
    
    s1_sound.stop()
    s2_sound.stop()
    
    s1_sound.play(when=PTB_start + s1_onset)
    s2_sound.play(when=PTB_start+ s2_onset)
    
    while s1_sound.status == PLAYING:
        if defaultKeyboard.getKeys(keyList=["escape"]):
            core.quit()
    s1_delay.append(ptb.GetSecs() - PTB_start - s1_offset)
    
    while s2_sound.status == PLAYING:
        if defaultKeyboard.getKeys(keyList=["escape"]):
            core.quit()
    s2_delay.append(ptb.GetSecs() - PTB_start - s2_offset)
    
    PTB_start = ptb.GetSecs()

# print descriptive statistics of offset latencies
for delayList, stim in [(s1_delay, 's1'), (s2_delay, 's2')]:
    print('{}: mean={:.0f} ms, sd={:.0f} ms'.format(
            stim,
            sum(delayList)/len(delayList)*1000, 
            np.std(delayList)*1000))

# compare offset latencies
ttest = stats.ttest_ind(s1_delay, s2_delay)
print('t-test: t={:.3f}, p={:.3f}'.format(ttest.statistic, ttest.pvalue))

# boxplots
fig1, ax1 = plt.subplots()
ax1.boxplot([np.array(s1_delay)*1000, np.array(s2_delay)*1000])
plt.ylabel('lag in ms')
plt.xticks([0, 1, 2], ['', 's1', 's2'])
figPath = getcwd() + '\\compare_latencies_soundPTB.png' 
plt.savefig(figPath)
print('Figure saved to: {}'.format(figPath))

core.quit()

The output of my script:
s1: mean=-19 ms, sd=3 ms
s2: mean=26 ms, sd=3 ms
t-test: t=-51.632, p=0.000

compare_latencies_soundPTB

Hi Lukas,

  1. Not really. Currently the status becomes PLAYING as soon as the play() method is called although you’re right that it probably should take into account the option that the sound is queued but not yet playing*. You can manually check the status returned by PTB which is s1_sound.statusDetailed and has more info for you.
  2. The issue with durations is something I’ll have to look in to. I’m puzzled by the result myself

cheers,
Jon

*Of course in your case, if status takes into account the pre-playing lag then your code won’t work as expected because the while mySound.status==PLAYING loop will never execute (it will be false and abort on the first pass). You’ll need to change it to while mySound.status!=FINISHED or similar instead

Thank you for your answer.

I checked, and status doesn’t seem to take the pre-playing lag into account. The sound plays and my results are the same with mySound.status!=FINISHED.

Thank you for that hint! I will look into that. For future reference, the command is s1_sound.statusDetailed.

Yes, exactly. For now that is true. What I meant was that if we change the status to have, say, a SCHEDULED period before it becomes PLAYING then you would need to change you code. But right now PLAYING becomes true as soon as play() is called

Right, yes, my bad. I’ll fix that in the post

Dear @jon and @LukasPsy,

I know this is an old thread but I believe it is still valuable to discuss.

I am having similar issues. I won’t share my code just yet but what I have experienced so far (using the PTB sound engine and 4 as latency mode) is that the offset of 5 consecutive tones (over 100 trials) seems to systematically increase from tone 1 to 5, with some resulting in even 30ms latency (the sounds ends 30ms after is meant to).

What I seem to find very reliable (assuming that the tone plays exactly when we specify using ‘when=PTB_start+s1_onset’) is the onset latency. This seems to be below 1ms for every tone. However, as it is mentioned in the mega-time study, it is actually very difficult to measure the absolute onset of sound stimuli.

Therefore, I am a bit puzzled. If the offset of sound stimuli shows such a large variability and we cannot really measure the absolute onset of sounds, where is this sub-millisecond precision?

kind regards

Giuseppe

That isn’t something we intend to convey in the manuscript, at least for lab-based (i.e. Python) studies. This can be measured rather precisely with a parallel port a mic and an oscilloscope (or BBTK) and we measured sub-millisecond precision as well as sub-ms accuracy in these audio onsets. It is hard to measure the absolute onset online because of the lack of parallel port, so maybe that’s the text that has been confusing.

Regarding the specific issue you refer to, with consecutive sounds having longer durations, it would certainly help if you could provide us a Minimal Working Example of the error in code so that we can test it.

thanks
Jon

Hi Jon,

thank you very much for your response.

For simplicity, I have used the same example in the code provided by @LukasPsy. After more experiments and tests, I can confirm that the large offset latency occurs with the PTB sound engine and the latency mode set to 4. In some rare cases, the latency can be even up to 50ms.

I was able to fix the issue by using an external sound card (Roland Rubix22). Doing so, the latency drops significantly (2-3 ms average across 5 tones over 100 trials). However, combining the PTB and the external audio card gives rise to a weird phenomenon also described in this thread: Crackling sound with PTB and focusrite audio card - crackling sound not due to the tone volume.

This crackling sound issue can be fixed if one sets the latency mode down to 1 or 2. When the latency mode is back to 3 or 4, the crackling sound occurs again. I could see this as a trade-off and the average offset latency - in my case - is reduced down to an average of 2-3 ms for every sound.

Considering the onset of sounds, I can confirm the sub-millisecond precision (apologies).

In conclusion, at least in my case, the significantly lower offset latencies between tones are provided by the external audio card, whereas the sub-millisecond precision in tones’ onset is provided by the PTB.

kind regards

Giuseppe