Sound not playing when using psychopy constants

Hi, I’m creating a simple experiment where I want two audio tracks playing sequentially but in between, the GUI presents the word ‘open’ which requires a key press to then continue to the second audio track. However, when I use the following code, the experiment just ends and nothing plays (there is also no error message associated with it).

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Fri Jul 28 18:05:04 2022

@author: emilia
"""
  
from psychopy import visual, core, data, gui, logging, event
from psychopy.visual import TextStim, ImageStim
from psychopy.hardware import keyboard 
import psychopy.event
import psychtoolbox as ptb
from psychopy import sound
from psychopy.constants import FINISHED

import pandas as pd
from datetime import datetime
import os
import serial

def showtext(window, text, size=0.5):
    message = visual.TextStim(window, text=text, wrapWidth = 1.5)
    message.size = size
    message.draw()
    win.flip()
    psychopy.event.waitKeys()
    if kb.getKeys(keyList=['escape']):
        shutdown()

mySound_single = sound.Sound('/Users/emilia/Documents/STUDIES/Greenman/stimuli/F1.wav')
mySound_dual = sound.Sound('/Users/emilia/Documents/STUDIES/Greenman/stimuli/F1.wav')

# Set up window
win = visual.Window([600,200], fullscr=False)

nextFlip = win.getFutureFlipTime(clock='ptb')
mySound_single.play(when=nextFlip)

win.flip()

if mySound_single.status == FINISHED:
    showtext(win, 'open')

nextFlip = win.getFutureFlipTime(clock='ptb')
mySound_dual.play(when=nextFlip)

win.flip()

# make sure everything is closed down
win.close()
core.quit()

I would appreciate any help with this, thanks!

Computers are frustrating, largely because they only do exactly what we tell them to :wink:

i.e. assuming your screen has a refresh rate of 60 Hz, your script above should only run for two refresh cycles (approximately 33 ms) before quitting.

The primary issue here is that sounds play asynchronously – the rest of your code continues while the sound plays in the background. So this check will almost certainly not evaluate to True:

if mySound_single.status == FINISHED:
    showtext(win, 'open')

i.e. the sound is still playing when this check is made, so the text won’t be shown. The next sound then starts playing, and then the experiment quits almost instantly afterwards. You probably won’t even hear the second sound start, as the quit() occurs immediately after the win.flip()

The wider issue here is that you should probably be using Builder to create the bones of your experiment. It would take about two minutes in Builder to create this section of your task, and everything would run properly (plus you would get all of the data collection and saving for free, and so on).

You clearly know how to program in Python, but the limitation here is knowing the detailed ins and outs of the PsychoPy API (e.g. you also mix references to the event module and the Keyboard object in quick succession, which can also cause unexpected results). Let Builder take care of all of that, and use your Python expertise only as required (by inserting small code snippets via code components when required).

This isn’t beginner-level advice: we would advise even the most experienced Python (and PsychoPy) developers to use this hybrid approach. It’s the best way of using your time and expertise, while letting Builder take care of all the tedious stuff, while ensuring that the code you are using is best-practice in terms of timing and so on.

e.g. I do still use some hand-crafted PsychoPy scripts from a decade ago. But for any new work, I start with Builder and just insert additional code snippets via code components as required. In my case, my custom code is largely for implementing experimental design considerations that Builder can’t provide for, but I let it handle most of the stimulus display and response collection stuff.

But how should you proceed if you want to continue with your own hand-crafted script? One way would be to look at how Builder scripts work. They tend to operate on a loop structure, with one iteration per screen refresh (i.e. each iteration ends with a win.flip()). With that approach, you would be actively checking on each screen refresh whether the sound has finished playing (or the escape key has been pressed, and so on) and proceeding to the next step only when that has occurred. Having active monitoring loops like that avoids the code immediately running through to completion. e.g:

mySound_single.play(when=nextFlip)

while mySound_single.status != FINISHED:
    # should probably check for 'escape' here
    win.flip()

# now will happen, but only after the sound has finished:
showtext(win, 'open')

Using win.flip() in a loop like this seems arbitrary (i.e. what does screen drawing have to do with sound duration?), but has distinct advantages. Most particularly, it limits the rate of the loop to only once per screen refresh. Without it, the loop might iterate at millions of times per second. That would hog all the resources of your computer, and soon cause timing issues (i.e. the OS and other apps need time to “breathe” as well, and would soon start wrestling with your script for access to the CPU). Putting a pause in each loop iteration allows your script to be a good citizen, effectively sharing access to the CPU for a few milliseconds on each cycle, meaning that it won’t eventually get strangled by competing processes.

1 Like