From e78cbb00421714e6873de863b57304ae6ec75433 Mon Sep 17 00:00:00 2001 From: todd Date: Wed, 31 Jul 2024 14:18:12 +0100 Subject: [PATCH] ENH: Allow Sound to play on different speakers in the same experiment --- .../experiment/components/sound/__init__.py | 2 +- psychopy/hardware/speaker.py | 5 +-- psychopy/sound/backend_ptb.py | 34 +++++++++++++------ 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/psychopy/experiment/components/sound/__init__.py b/psychopy/experiment/components/sound/__init__.py index dd8aad63b2..21589f2966 100644 --- a/psychopy/experiment/components/sound/__init__.py +++ b/psychopy/experiment/components/sound/__init__.py @@ -112,7 +112,7 @@ def getSpeakerValues(): return vals self.params['speakerIndex'] = Param( - speakerIndex, valType="code", inputType="choice", categ="Device", + speakerIndex, valType="str", inputType="choice", categ="Device", allowedVals=getSpeakerValues, allowedLabels=getSpeakerLabels, hint=_translate( diff --git a/psychopy/hardware/speaker.py b/psychopy/hardware/speaker.py index 23e35dff6c..94449b29f1 100644 --- a/psychopy/hardware/speaker.py +++ b/psychopy/hardware/speaker.py @@ -39,14 +39,11 @@ def __init__(self, index): # find profile which matches index for profile in profiles.values(): if index in (profile['index'], profile['name']): - self.index = profile['index'] + self.index = int(profile['index']) self.deviceName = profile['name'] if self.index is None: logging.error("No speaker device found with index %d" % index) - else: - # set global device (best we can do for now) - setDevice(index) def isSameDevice(self, other): """ diff --git a/psychopy/sound/backend_ptb.py b/psychopy/sound/backend_ptb.py index 56ca350bf8..e9295e9ad5 100644 --- a/psychopy/sound/backend_ptb.py +++ b/psychopy/sound/backend_ptb.py @@ -127,6 +127,9 @@ class _StreamsDict(dict): use the instance `streams` rather than creating a new instance of this """ + def __init__(self, index): + # store device index + self.index = index def getStream(self, sampleRate, channels, blockSize): """Gets a stream of exact match or returns a new one @@ -186,11 +189,11 @@ def _getStream(self, sampleRate, channels, blockSize): # create new stream self[label] = _MasterStream(sampleRate, channels, blockSize, - device=defaultOutput) + device=self.index) return label, self[label] -streams = _StreamsDict() +devices = {} class _MasterStream(audio.Stream): @@ -351,8 +354,8 @@ def isFinished(self): def _getDefaultSampleRate(self): """Check what streams are open and use one of these""" - if len(streams): - return list(streams.values())[0].sampleRate + if len(devices.get(self.speaker.index, [])): + return list(devices[self.speaker.index].values())[0].sampleRate else: return 48000 # seems most widely supported @@ -604,17 +607,26 @@ def stream(self): """Read-only property returns the stream on which the sound will be played """ + # if no stream yet, make one if not self.streamLabel: + # if no streams for current device yet, make a StreamsDict for it + if self.speaker.index not in devices: + devices[self.speaker.index] = _StreamsDict(index=self.speaker.index) + # make stream try: - label, s = streams.getStream(sampleRate=self.sampleRate, - channels=self.channels, - blockSize=self.blockSize) + label, s = devices[self.speaker.index].getStream( + sampleRate=self.sampleRate, + channels=self.channels, + blockSize=self.blockSize + ) except SoundFormatError as err: # try to use something similar (e.g. mono->stereo) # then check we have an appropriate stream open - altern = streams._getSimilar(sampleRate=self.sampleRate, - channels=-1, - blockSize=-1) + altern = devices[self.speaker.index]._getSimilar( + sampleRate=self.sampleRate, + channels=-1, + blockSize=-1 + ) if altern is None: raise SoundFormatError(err) else: # safe to extract data @@ -625,7 +637,7 @@ def stream(self): self.blockSize = s.blockSize self.streamLabel = label - return streams[self.streamLabel] + return devices[self.speaker.index][self.streamLabel] def __del__(self): if self.track: