Thanks for your help so far (Doesn't play midi files).
So, it turns you can play midi files (.mid) in PsychoPy using pygame (see below), However, if you just set pygame first in audio library and call a file as follows…
from psychopy import sound audio_stimulus = sound.Sound('/Users/sam/Downloads/Experiment/sounds/Sound1.mid', secs=-1) audio_stimulus.play()
…then you just hear a click and receive the following feedback:
<psychopy.sound.backend_pygame.SoundPygame object at 0x112edcb10>
However if you call a midi file like this:
import pygame pygame.mixer.music.load("/Users/sam/Downloads/Experiment/sounds/Sound1.mid") pygame.mixer.music.play("/Users/sam/Downloads/Experiment/sounds/Sound1.mid")
Then it plays fine.
The thing is, in my code (which presently calls .wav files) I currently use all of the following, which I’ve inherited from the builder-generated code:
from psychopy import sound audio_stimulus = sound.Sound('A', secs=-1) audio_stimulus.setVolume(1) audio_stimulus.setSound(audioStim, secs=-1) audio_stimulus.tStart = t audio_stimulus.frameNStart = frameN audio_stimulus.play() audio_stimulus.stop() audio_stimulus.status
Could someone smarter than me help me to write a class that I could seamlessly set audio_stimulus to, that won’t otherwise affect how my code runs (i.e. it will also need to have the statuses (NOT_STARTED, STARTED, PLAYING, PAUSED,STOPPED, FINISHED, PRESSED, RELEASED, FOREVER), setSound() using the appropriate column of the conditions excel file etc…
I note that pygame has these options already:
pygame.mixer.music.set_volume(1) pygame.mixer.music.get_busy # this checks if the music is still running pygame.mixer.music.stop() pygame.mixer.music.play()
Sorry if this seems lazy… basically out of interest I had a look at the code for backend_pyo.py and it all looks incredibly complicated. Seems more sensible to ask if any of you can help me, before embarking on something like that…
Here’s the class creation for Sound using pyo (I think)…
class SoundPyo(_SoundBase): def __init__(self, value="C", secs=0.5, octave=4, stereo=True, volume=1.0, loops=0, sampleRate=44100, bits=16, hamming=True, start=0, stop=-1, name='', autoLog=True): """ value: can be a number, string or an array: * If it's a number between 37 and 32767 then a tone will be generated at that frequency in Hz. * It could be a string for a note ('A', 'Bfl', 'B', 'C', 'Csh', ...). Then you may want to specify which octave as well * Or a string could represent a filename in the current location, or mediaLocation, or a full path combo * Or by giving an Nx2 numpy array of floats (-1:1) you can specify the sound yourself as a waveform By default, a Hamming window (5ms duration) will be applied to a generated tone, so that onset and offset are smoother (to avoid clicking). To disable the Hamming window, set `hamming=False`. secs: Duration of a tone. Not used for sounds from a file. start : float Where to start playing a sound file; default = 0s (start of the file). stop : float Where to stop playing a sound file; default = end of file. octave: is only relevant if the value is a note name. Middle octave of a piano is 4. Most computers won't output sounds in the bottom octave (1) and the top octave (8) is generally painful stereo: True (= default, two channels left and right), False (one channel) volume: loudness to play the sound, from 0.0 (silent) to 1.0 (max). Adjustments are not possible during playback, only before. loops : int How many times to repeat the sound after it plays once. If `loops` == -1, the sound will repeat indefinitely until stopped. sampleRate (= 44100): if the psychopy.sound.init() function has been called or if another sound has already been created then this argument will be ignored and the previous setting will be used bits: has no effect for the pyo backend hamming: whether to apply a Hamming window (5ms) for generated tones. Not applied to sounds from files. """ global pyoSndServer if pyoSndServer is None or pyoSndServer.getIsBooted() == 0: init(rate=sampleRate) self.sampleRate = pyoSndServer.getSamplingRate() self.format = bits self.isStereo = stereo self.channels = 1 + int(stereo) self.secs = secs self.startTime = start self.stopTime = stop self.autoLog = autoLog self.name = name # try to create sound; set volume and loop before setSound (else # needsUpdate=True) self._snd = None self.volume = min(1.0, max(0.0, volume)) # distinguish the loops requested from loops actual because of # infinite tones (which have many loops but none requested) # -1 for infinite or a number of loops self.requestedLoops = self.loops = int(loops) self.setSound(value=value, secs=secs, octave=octave, hamming=hamming) self.needsUpdate = False def play(self, loops=None, autoStop=True, log=True): """Starts playing the sound on an available channel. loops : int (same as above) For playing a sound file, you cannot specify the start and stop times when playing the sound, only when creating the sound initially. Playing a sound runs in a separate thread i.e. your code won't wait for the sound to finish before continuing. To pause while playing, you need to use a `psychopy.core.wait(mySound.getDuration())`. If you call `play()` while something is already playing the sounds will be played over each other. """ if loops is not None and self.loops != loops: self.setLoops(loops) if self.needsUpdate: # ~0.00015s, regardless of the size of self._sndTable self._updateSnd() self._snd.out() self.status = STARTED if autoStop or self.loops != 0: # pyo looping is boolean: loop forever or not at all # so track requested loops using time; limitations: not # sample-accurate if self.loops >= 0: duration = self.getDuration() * (self.loops + 1) else: duration = FOREVER self.terminator = threading.Timer(duration, self._onEOS) self.terminator.start() if log and self.autoLog: logging.exp("Sound %s started" % (self.name), obj=self) return self def _onEOS(self): # call _onEOS from a thread based on time, enables loop termination if self.loops != 0: # then its looping forever as a pyo object self._snd.stop() if self.status != NOT_STARTED: # in case of multiple successive trials self.status = FINISHED return True def stop(self, log=True): """Stops the sound immediately""" self._snd.stop() try: self.terminator.cancel() except Exception: # pragma: no cover pass self.status = STOPPED if log and self.autoLog: logging.exp("Sound %s stopped" % (self.name), obj=self) def _updateSnd(self): self.needsUpdate = False doLoop = bool(self.loops != 0) # if True, end it via threading.Timer self._snd = pyo.TableRead(self._sndTable, freq=self._sndTable.getRate(), loop=doLoop, mul=self.volume) def _setSndFromFile(self, fileName): # want mono sound file played to both speakers, not just left / 0 self.fileName = fileName self._sndTable = pyo.SndTable(initchnls=self.channels) # in case a tone with inf loops had been used before self.loops = self.requestedLoops # mono file loaded to all chnls: try: self._sndTable.setSound(self.fileName, start=self.startTime, stop=self.stopTime) except Exception: msg = ('Could not open sound file `%s` using pyo; not found ' 'or format not supported.') logging.error(msg % fileName) raise TypeError(msg % fileName) self._updateSnd() self.duration = self._sndTable.getDur() def _setSndFromArray(self, thisArray): self._sndTable = pyo.DataTable(size=len(thisArray), init=thisArray.T.tolist(), chnls=self.channels) self._updateSnd() # a DataTable has no .getDur() method, so just store the duration: self.duration = float(len(thisArray)) / self.sampleRate