psychopy.org | Reference | Downloads | Github

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