psychopy 2025.1.1__py3-none-any.whl → 2025.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of psychopy might be problematic. Click here for more details.
- psychopy/VERSION +1 -1
- psychopy/alerts/alertsCatalogue/4810.yaml +19 -0
- psychopy/alerts/alertsCatalogue/alertCategories.yaml +4 -0
- psychopy/alerts/alertsCatalogue/alertmsg.py +15 -1
- psychopy/alerts/alertsCatalogue/generateAlertmsg.py +2 -2
- psychopy/app/Resources/classic/add_many.png +0 -0
- psychopy/app/Resources/classic/add_many@2x.png +0 -0
- psychopy/app/Resources/classic/devices.png +0 -0
- psychopy/app/Resources/classic/devices@2x.png +0 -0
- psychopy/app/Resources/classic/photometer.png +0 -0
- psychopy/app/Resources/classic/photometer@2x.png +0 -0
- psychopy/app/Resources/dark/add_many.png +0 -0
- psychopy/app/Resources/dark/add_many@2x.png +0 -0
- psychopy/app/Resources/dark/devices.png +0 -0
- psychopy/app/Resources/dark/devices@2x.png +0 -0
- psychopy/app/Resources/dark/photometer.png +0 -0
- psychopy/app/Resources/dark/photometer@2x.png +0 -0
- psychopy/app/Resources/light/add_many.png +0 -0
- psychopy/app/Resources/light/add_many@2x.png +0 -0
- psychopy/app/Resources/light/devices.png +0 -0
- psychopy/app/Resources/light/devices@2x.png +0 -0
- psychopy/app/Resources/light/photometer.png +0 -0
- psychopy/app/Resources/light/photometer@2x.png +0 -0
- psychopy/app/_psychopyApp.py +35 -13
- psychopy/app/builder/builder.py +88 -35
- psychopy/app/builder/dialogs/__init__.py +69 -220
- psychopy/app/builder/dialogs/dlgsCode.py +29 -8
- psychopy/app/builder/dialogs/paramCtrls.py +1468 -904
- psychopy/app/builder/validators.py +25 -17
- psychopy/app/coder/coder.py +12 -1
- psychopy/app/coder/repl.py +5 -2
- psychopy/app/colorpicker/__init__.py +1 -1
- psychopy/app/deviceManager/__init__.py +1 -0
- psychopy/app/deviceManager/addDialog.py +218 -0
- psychopy/app/deviceManager/dialog.py +185 -0
- psychopy/app/deviceManager/panel.py +191 -0
- psychopy/app/deviceManager/utils.py +60 -0
- psychopy/app/idle.py +7 -0
- psychopy/app/locale/ar_001/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ar_001/LC_MESSAGE/messages.po +12695 -10592
- psychopy/app/locale/cs_CZ/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/cs_CZ/LC_MESSAGE/messages.po +10199 -24
- psychopy/app/locale/da_DK/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/da_DK/LC_MESSAGE/messages.po +10199 -24
- psychopy/app/locale/de_DE/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/de_DE/LC_MESSAGE/messages.po +11221 -9712
- psychopy/app/locale/el_GR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/el_GR/LC_MESSAGE/messages.po +10200 -25
- psychopy/app/locale/en_NZ/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/en_NZ/LC_MESSAGE/messages.po +10200 -25
- psychopy/app/locale/en_US/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/en_US/LC_MESSAGE/messages.po +10195 -18
- psychopy/app/locale/es_CO/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/es_CO/LC_MESSAGE/messages.po +11917 -9101
- psychopy/app/locale/es_ES/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/es_ES/LC_MESSAGE/messages.po +11924 -9103
- psychopy/app/locale/es_US/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/es_US/LC_MESSAGE/messages.po +11917 -9101
- psychopy/app/locale/et_EE/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/et_EE/LC_MESSAGE/messages.po +11084 -9569
- psychopy/app/locale/fa_IR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/fa_IR/LC_MESSAGE/messages.po +11590 -5806
- psychopy/app/locale/fi_FI/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/fi_FI/LC_MESSAGE/messages.po +10199 -24
- psychopy/app/locale/fr_FR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/fr_FR/LC_MESSAGE/messages.po +11091 -9577
- psychopy/app/locale/he_IL/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/he_IL/LC_MESSAGE/messages.po +11072 -9549
- psychopy/app/locale/hi_IN/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/hi_IN/LC_MESSAGE/messages.po +11071 -9559
- psychopy/app/locale/hu_HU/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/hu_HU/LC_MESSAGE/messages.po +10200 -25
- psychopy/app/locale/it_IT/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/it_IT/LC_MESSAGE/messages.po +11072 -9560
- psychopy/app/locale/ja_JP/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ja_JP/LC_MESSAGE/messages.po +1485 -1137
- psychopy/app/locale/ko_KR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ko_KR/LC_MESSAGE/messages.po +10199 -24
- psychopy/app/locale/ms_MY/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ms_MY/LC_MESSAGE/messages.po +11463 -8757
- psychopy/app/locale/nl_NL/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/nl_NL/LC_MESSAGE/messages.po +10200 -25
- psychopy/app/locale/nn_NO/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/nn_NO/LC_MESSAGE/messages.po +10200 -25
- psychopy/app/locale/pl_PL/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/pl_PL/LC_MESSAGE/messages.po +10200 -25
- psychopy/app/locale/pt_PT/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/pt_PT/LC_MESSAGE/messages.po +11288 -9434
- psychopy/app/locale/ro_RO/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ro_RO/LC_MESSAGE/messages.po +10200 -25
- psychopy/app/locale/ru_RU/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ru_RU/LC_MESSAGE/messages.po +10199 -24
- psychopy/app/locale/sv_SE/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/sv_SE/LC_MESSAGE/messages.po +11441 -8747
- psychopy/app/locale/tr_TR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/tr_TR/LC_MESSAGE/messages.po +11069 -9545
- psychopy/app/locale/zh_CN/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/zh_CN/LC_MESSAGE/messages.po +12085 -8268
- psychopy/app/locale/zh_TW/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/zh_TW/LC_MESSAGE/messages.po +11929 -8022
- psychopy/app/plugin_manager/dialog.py +12 -3
- psychopy/app/plugin_manager/packageIndex.py +303 -0
- psychopy/app/plugin_manager/packages.py +203 -63
- psychopy/app/plugin_manager/plugins.py +120 -240
- psychopy/app/preferencesDlg.py +6 -1
- psychopy/app/psychopyApp.py +16 -4
- psychopy/app/runner/runner.py +10 -2
- psychopy/app/runner/scriptProcess.py +8 -3
- psychopy/app/stdout/stdOutRich.py +11 -4
- psychopy/app/themes/icons.py +3 -0
- psychopy/app/utils.py +61 -0
- psychopy/data/experiment.py +133 -23
- psychopy/data/routine.py +12 -0
- psychopy/data/staircase.py +42 -20
- psychopy/data/trial.py +20 -12
- psychopy/data/utils.py +42 -2
- psychopy/demos/builder/Experiments/dragAndDrop/drag_and_drop.psyexp +22 -5
- psychopy/demos/builder/Experiments/dragAndDrop/stimuli/solutions.xlsx +0 -0
- psychopy/demos/builder/Experiments/stroopVoice/stroopVoice.psyexp +2 -12
- psychopy/demos/builder/Feature Demos/buttonBox/buttonBoxDemo.psyexp +3 -8
- psychopy/demos/builder/Feature Demos/movies/movie.psyexp +220 -0
- psychopy/demos/builder/Feature Demos/movies/readme.md +3 -0
- psychopy/demos/builder/Feature Demos/visualValidator/visualValidator.psyexp +1 -2
- psychopy/demos/builder/Hardware/camera/camera.psyexp +3 -16
- psychopy/demos/builder/Hardware/microphone/microphone.psyexp +3 -16
- psychopy/demos/coder/hardware/hdf5_extract.py +133 -0
- psychopy/event.py +20 -15
- psychopy/experiment/_experiment.py +86 -10
- psychopy/experiment/components/__init__.py +3 -10
- psychopy/experiment/components/_base.py +9 -20
- psychopy/experiment/components/button/__init__.py +1 -1
- psychopy/experiment/components/buttonBox/__init__.py +50 -54
- psychopy/experiment/components/camera/__init__.py +137 -359
- psychopy/experiment/components/keyboard/__init__.py +17 -24
- psychopy/experiment/components/microphone/__init__.py +61 -110
- psychopy/experiment/components/movie/__init__.py +2 -3
- psychopy/experiment/components/serialOut/__init__.py +192 -93
- psychopy/experiment/components/settings/__init__.py +45 -27
- psychopy/experiment/components/sound/__init__.py +82 -73
- psychopy/experiment/components/soundsensor/__init__.py +43 -80
- psychopy/experiment/devices.py +303 -0
- psychopy/experiment/exports.py +20 -18
- psychopy/experiment/flow.py +7 -0
- psychopy/experiment/loops.py +47 -29
- psychopy/experiment/monitor.py +74 -0
- psychopy/experiment/params.py +48 -10
- psychopy/experiment/plugins.py +28 -108
- psychopy/experiment/py2js_transpiler.py +1 -1
- psychopy/experiment/routines/__init__.py +1 -1
- psychopy/experiment/routines/_base.py +59 -24
- psychopy/experiment/routines/audioValidator/__init__.py +19 -155
- psychopy/experiment/routines/visualValidator/__init__.py +25 -25
- psychopy/hardware/__init__.py +20 -57
- psychopy/hardware/button.py +15 -2
- psychopy/hardware/camera/__init__.py +2237 -1394
- psychopy/hardware/joystick/__init__.py +1 -1
- psychopy/hardware/keyboard.py +5 -8
- psychopy/hardware/listener.py +4 -1
- psychopy/hardware/manager.py +75 -35
- psychopy/hardware/microphone.py +52 -6
- psychopy/hardware/monitor.py +144 -0
- psychopy/hardware/photometer/__init__.py +156 -117
- psychopy/hardware/serialdevice.py +16 -2
- psychopy/hardware/soundsensor.py +4 -1
- psychopy/iohub/devices/deviceConfigValidation.py +2 -1
- psychopy/iohub/devices/keyboard/darwin.py +8 -5
- psychopy/iohub/util/__init__.py +7 -8
- psychopy/localization/generateTranslationTemplate.py +208 -116
- psychopy/localization/messages.pot +4305 -3502
- psychopy/monitors/MonitorCenter.py +174 -74
- psychopy/plugins/__init__.py +6 -4
- psychopy/preferences/devices.py +80 -0
- psychopy/preferences/generateHints.py +2 -1
- psychopy/preferences/preferences.py +35 -11
- psychopy/scripts/psychopy-pkgutil.py +969 -0
- psychopy/scripts/psyexpCompile.py +1 -1
- psychopy/session.py +34 -38
- psychopy/sound/__init__.py +6 -260
- psychopy/sound/audioclip.py +164 -0
- psychopy/sound/backend_ptb.py +8 -0
- psychopy/sound/backend_pygame.py +10 -0
- psychopy/sound/backend_pysound.py +9 -0
- psychopy/sound/backends/__init__.py +0 -0
- psychopy/sound/microphone.py +3 -0
- psychopy/sound/sound.py +58 -0
- psychopy/tests/data/correctScript/python/correctNoiseStimComponent.py +1 -1
- psychopy/tests/data/duplicateHeaders.csv +2 -0
- psychopy/tests/test_app/test_builder/test_BuilderFrame.py +22 -7
- psychopy/tests/test_app/test_builder/test_CompileFromBuilder.py +0 -2
- psychopy/tests/test_data/test_utils.py +5 -1
- psychopy/tests/test_experiment/test_components/test_ButtonBoxComponent.py +22 -2
- psychopy/tests/test_hardware/test_ports.py +0 -12
- psychopy/tests/test_tools/test_stringtools.py +1 -1
- psychopy/tools/attributetools.py +12 -5
- psychopy/tools/fontmanager.py +17 -14
- psychopy/tools/movietools.py +43 -2
- psychopy/tools/stringtools.py +33 -8
- psychopy/tools/versionchooser.py +1 -1
- psychopy/validation/audio.py +5 -1
- psychopy/validation/visual.py +5 -1
- psychopy/visual/basevisual.py +8 -7
- psychopy/visual/circle.py +2 -2
- psychopy/visual/image.py +29 -109
- psychopy/visual/movies/__init__.py +1800 -313
- psychopy/visual/polygon.py +4 -0
- psychopy/visual/shape.py +2 -2
- psychopy/visual/window.py +34 -11
- psychopy/voicekey/__init__.py +41 -669
- psychopy/voicekey/labjack_vks.py +7 -48
- psychopy/voicekey/parallel_vks.py +7 -42
- psychopy/voicekey/vk_tools.py +114 -263
- {psychopy-2025.1.1.dist-info → psychopy-2025.2.1.dist-info}/METADATA +17 -11
- {psychopy-2025.1.1.dist-info → psychopy-2025.2.1.dist-info}/RECORD +216 -184
- {psychopy-2025.1.1.dist-info → psychopy-2025.2.1.dist-info}/WHEEL +1 -1
- psychopy/visual/movies/players/__init__.py +0 -62
- psychopy/visual/movies/players/ffpyplayer_player.py +0 -1401
- psychopy/voicekey/demo_vks.py +0 -12
- psychopy/voicekey/signal.py +0 -42
- {psychopy-2025.1.1.dist-info → psychopy-2025.2.1.dist-info}/entry_points.txt +0 -0
- {psychopy-2025.1.1.dist-info → psychopy-2025.2.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -143,7 +143,7 @@ def compileScript(infile=None, version=None, outfile=None):
|
|
|
143
143
|
The experiment object used for generating the experiment script
|
|
144
144
|
"""
|
|
145
145
|
# import PsychoPy experiment and write script with useVersion active
|
|
146
|
-
from psychopy
|
|
146
|
+
from psychopy import experiment
|
|
147
147
|
# Check infile type
|
|
148
148
|
if isinstance(infile, experiment.Experiment):
|
|
149
149
|
thisExp = infile
|
psychopy/session.py
CHANGED
|
@@ -256,6 +256,8 @@ class Session:
|
|
|
256
256
|
):
|
|
257
257
|
# Store root and add to Python path
|
|
258
258
|
self.root = Path(root)
|
|
259
|
+
if not self.root.is_absolute():
|
|
260
|
+
self.root = self.root.absolute()
|
|
259
261
|
sys.path.insert(1, str(self.root))
|
|
260
262
|
# store rest message
|
|
261
263
|
self.restMsg = restMsg
|
|
@@ -962,44 +964,7 @@ class Session:
|
|
|
962
964
|
f"Device names are not available for experiments added to Session directly as a "
|
|
963
965
|
f".py file."
|
|
964
966
|
)
|
|
965
|
-
|
|
966
|
-
usages = {}
|
|
967
|
-
|
|
968
|
-
def _process(name, emt):
|
|
969
|
-
"""
|
|
970
|
-
Process an element (Component or Routine) for device names and append them to the
|
|
971
|
-
usages dict.
|
|
972
|
-
|
|
973
|
-
Parameters
|
|
974
|
-
----------
|
|
975
|
-
name : str
|
|
976
|
-
Name of this element in Builder
|
|
977
|
-
emt : Component or Routine
|
|
978
|
-
Element to process
|
|
979
|
-
"""
|
|
980
|
-
# if we have a device name for this element...
|
|
981
|
-
if "deviceLabel" in emt.params:
|
|
982
|
-
# get init value so it lines up with boilerplate code
|
|
983
|
-
inits = experiment.getInitVals(emt.params)
|
|
984
|
-
# get value
|
|
985
|
-
deviceName = inits['deviceLabel'].val
|
|
986
|
-
# if deviceName exists from other elements, add usage to it
|
|
987
|
-
if deviceName in usages:
|
|
988
|
-
usages[deviceName].append(name)
|
|
989
|
-
else:
|
|
990
|
-
usages[deviceName] = [name]
|
|
991
|
-
|
|
992
|
-
# iterate through routines
|
|
993
|
-
for rtName, rt in exp.routines.items():
|
|
994
|
-
if isinstance(rt, experiment.routines.BaseStandaloneRoutine):
|
|
995
|
-
# for standalone routines, get device names from params
|
|
996
|
-
_process(rtName, rt)
|
|
997
|
-
else:
|
|
998
|
-
# for regular routines, get device names from each component
|
|
999
|
-
for comp in rt:
|
|
1000
|
-
_process(comp.name, comp)
|
|
1001
|
-
|
|
1002
|
-
return list(usages)
|
|
967
|
+
return exp.getRequiredDeviceNames()
|
|
1003
968
|
|
|
1004
969
|
def runExperiment(self, key, expInfo=None, blocking=True):
|
|
1005
970
|
"""
|
|
@@ -1264,6 +1229,13 @@ class Session:
|
|
|
1264
1229
|
|
|
1265
1230
|
# set ExperimentHandler status to PAUSED
|
|
1266
1231
|
self.currentExperiment.pause()
|
|
1232
|
+
# update Liaison if needed
|
|
1233
|
+
if self.liaison is not None:
|
|
1234
|
+
self.sendToLiaison({
|
|
1235
|
+
'type': "experiment_status",
|
|
1236
|
+
'name': self.currentExperiment.name,
|
|
1237
|
+
'status': self.currentExperiment.status,
|
|
1238
|
+
})
|
|
1267
1239
|
|
|
1268
1240
|
return True
|
|
1269
1241
|
|
|
@@ -1285,6 +1257,13 @@ class Session:
|
|
|
1285
1257
|
return False
|
|
1286
1258
|
# set ExperimentHandler status to STARTED
|
|
1287
1259
|
self.currentExperiment.resume()
|
|
1260
|
+
# update Liaison if needed
|
|
1261
|
+
if self.liaison is not None:
|
|
1262
|
+
self.sendToLiaison({
|
|
1263
|
+
'type': "experiment_status",
|
|
1264
|
+
'name': self.currentExperiment.name,
|
|
1265
|
+
'status': self.currentExperiment.status,
|
|
1266
|
+
})
|
|
1288
1267
|
|
|
1289
1268
|
return True
|
|
1290
1269
|
|
|
@@ -1305,8 +1284,25 @@ class Session:
|
|
|
1305
1284
|
)
|
|
1306
1285
|
return False
|
|
1307
1286
|
self.currentExperiment.stop()
|
|
1287
|
+
# update Liaison if needed
|
|
1288
|
+
if self.liaison is not None:
|
|
1289
|
+
self.sendToLiaison({
|
|
1290
|
+
'type': "experiment_status",
|
|
1291
|
+
'name': self.currentExperiment.name,
|
|
1292
|
+
'status': self.currentExperiment.status,
|
|
1293
|
+
})
|
|
1308
1294
|
|
|
1309
1295
|
return True
|
|
1296
|
+
|
|
1297
|
+
def next(self):
|
|
1298
|
+
"""
|
|
1299
|
+
Move on to either the next trial (if in a trials loop) or the next Routine.
|
|
1300
|
+
"""
|
|
1301
|
+
# return if there's no current experiment
|
|
1302
|
+
if self.currentExperiment is None:
|
|
1303
|
+
return
|
|
1304
|
+
# skip trials in current loop
|
|
1305
|
+
return self.currentExperiment.next()
|
|
1310
1306
|
|
|
1311
1307
|
def skipTrials(self, n=1):
|
|
1312
1308
|
"""
|
psychopy/sound/__init__.py
CHANGED
|
@@ -34,271 +34,17 @@ After importing sound, the sound lib and driver being used will be stored as::
|
|
|
34
34
|
# Distributed under the terms of the GNU General Public License (GPL).
|
|
35
35
|
|
|
36
36
|
import sys
|
|
37
|
-
import os
|
|
38
|
-
import traceback
|
|
39
|
-
from psychopy import logging, prefs, constants
|
|
40
|
-
from psychopy.tools import systemtools
|
|
41
|
-
from .exceptions import DependencyError, SoundFormatError
|
|
42
37
|
from .audiodevice import *
|
|
43
38
|
from .audioclip import * # import objects related to AudioClip
|
|
44
|
-
from . import microphone
|
|
39
|
+
from . import microphone, sound
|
|
40
|
+
from .sound import Sound
|
|
45
41
|
|
|
46
|
-
__all__ = [
|
|
42
|
+
__all__ = [
|
|
43
|
+
"microphone",
|
|
44
|
+
"Sound"
|
|
45
|
+
]
|
|
47
46
|
|
|
48
|
-
# # import transcription if possible
|
|
49
|
-
# try:
|
|
50
|
-
# from .transcribe import * # import transcription engine stuff
|
|
51
|
-
# except ImportError as err:
|
|
52
|
-
# formatted_tb = ''.join(
|
|
53
|
-
# traceback.format_exception(type(err), err, err.__traceback__))
|
|
54
|
-
# logging.error(
|
|
55
|
-
# "Failed to import psychopy.sound.transcribe. Transcription will not be"
|
|
56
|
-
# "possible on this machine. For details see stack trace below:\n"
|
|
57
|
-
# f"{formatted_tb}")
|
|
58
47
|
|
|
59
48
|
# used to check if we are on 64-bit Python
|
|
60
49
|
bits32 = sys.maxsize == 2 ** 32
|
|
61
50
|
|
|
62
|
-
# Globals for the sound library. We can only load one audio library at a time,
|
|
63
|
-
# so once these values are populated they cannot be changed without restarting
|
|
64
|
-
# Python.
|
|
65
|
-
pyoSndServer = None
|
|
66
|
-
Sound = None
|
|
67
|
-
audioLib = None
|
|
68
|
-
audioDriver = None
|
|
69
|
-
backend = None
|
|
70
|
-
|
|
71
|
-
# These are the names that can be used in the prefs to specifiy audio libraries.
|
|
72
|
-
# The available libraries are hard-coded at this point until we can overhaul
|
|
73
|
-
# the sound library to be more modular.
|
|
74
|
-
_audioLibs = ['ptb', 'sounddevice', 'pyo', 'pysoundcard', 'pygame']
|
|
75
|
-
failed = [] # keep track of audio libs that failed to load
|
|
76
|
-
|
|
77
|
-
# check if this is being imported on Travis/Github (has no audio card)
|
|
78
|
-
if systemtools.isVM_CI():
|
|
79
|
-
# for sounddevice we built in some VM protection but not in pyo
|
|
80
|
-
prefs.hardware['audioLib'] = ['ptb', 'sounddevice']
|
|
81
|
-
|
|
82
|
-
# ensure that the value for `audioLib` is a list
|
|
83
|
-
if isinstance(prefs.hardware['audioLib'], str):
|
|
84
|
-
prefs.hardware['audioLib'] = [prefs.hardware['audioLib']]
|
|
85
|
-
|
|
86
|
-
thisLibName = None # name of the library we are trying to load
|
|
87
|
-
|
|
88
|
-
# selection and fallback mechanism for audio libraries
|
|
89
|
-
for thisLibName in prefs.hardware['audioLib']:
|
|
90
|
-
# Tell the user we are trying to load the specifeid audio library
|
|
91
|
-
logging.info(f"Trying to load audio library: {thisLibName}")
|
|
92
|
-
|
|
93
|
-
# Iterate over the list of audioLibs and try to load the first one that
|
|
94
|
-
# is supported. If none are supported, load PTB as a fallback. If PTB isn't
|
|
95
|
-
# installed, raise an error.
|
|
96
|
-
thisLibName = thisLibName.lower()
|
|
97
|
-
|
|
98
|
-
# lowercased list of valid audio libraries for safe comparisons
|
|
99
|
-
validLibs = [libName.lower() for libName in _audioLibs]
|
|
100
|
-
|
|
101
|
-
# check if `thisLibName` is a valid audio library
|
|
102
|
-
if thisLibName not in validLibs:
|
|
103
|
-
failed.append(thisLibName)
|
|
104
|
-
logging.warning(f"Invalid audioLib pref: {thisLibName}. "
|
|
105
|
-
f"Valid options are: {_audioLibs}")
|
|
106
|
-
continue
|
|
107
|
-
|
|
108
|
-
# select the backend and set the Sound class
|
|
109
|
-
if thisLibName == 'ptb':
|
|
110
|
-
# The Psychtoolbox backend is preferred, provides the best performance
|
|
111
|
-
# and is the only one that supports low-latency scheduling. If no other
|
|
112
|
-
# audio backend can be loaded, we will use PTB.
|
|
113
|
-
if not bits32:
|
|
114
|
-
try:
|
|
115
|
-
from . import backend_ptb as backend
|
|
116
|
-
Sound = backend.SoundPTB
|
|
117
|
-
audioDriver = backend.audioDriver
|
|
118
|
-
except Exception as err:
|
|
119
|
-
failed.append(thisLibName)
|
|
120
|
-
continue
|
|
121
|
-
else:
|
|
122
|
-
break
|
|
123
|
-
else:
|
|
124
|
-
logging.warning("PTB backend is not supported on 32-bit Python. "
|
|
125
|
-
"Trying another backend...")
|
|
126
|
-
continue
|
|
127
|
-
elif thisLibName == 'pyo':
|
|
128
|
-
# pyo is a wrapper around PortAudio, which is a cross-platform audio
|
|
129
|
-
# library. It is the recommended backend for Windows and Linux.
|
|
130
|
-
try:
|
|
131
|
-
# Caution: even import failed inside, we still get a module object.
|
|
132
|
-
# This is not the case for other backends and may not be desired.
|
|
133
|
-
from . import backend_pyo as backend
|
|
134
|
-
Sound = backend.SoundPyo
|
|
135
|
-
pyoSndServer = backend.pyoSndServer
|
|
136
|
-
audioDriver = backend.audioDriver
|
|
137
|
-
except Exception:
|
|
138
|
-
failed.append(thisLibName)
|
|
139
|
-
continue
|
|
140
|
-
else:
|
|
141
|
-
break
|
|
142
|
-
elif thisLibName == 'sounddevice':
|
|
143
|
-
# sounddevice is a wrapper around PortAudio, which is a cross-platform
|
|
144
|
-
# audio library. It is the recommended backend for Windows and Linux.
|
|
145
|
-
try:
|
|
146
|
-
# Caution: even import failed inside, we still get a module object.
|
|
147
|
-
# This is not the case for other backends and may not be desired.
|
|
148
|
-
from . import backend_sounddevice as backend
|
|
149
|
-
Sound = backend.SoundDeviceSound
|
|
150
|
-
except Exception:
|
|
151
|
-
failed.append(thisLibName)
|
|
152
|
-
continue
|
|
153
|
-
else:
|
|
154
|
-
break
|
|
155
|
-
elif thisLibName == 'pygame':
|
|
156
|
-
# pygame is a cross-platform audio library. It is no longer supported by
|
|
157
|
-
# PsychoPy, but we keep it here for backwards compatibility until
|
|
158
|
-
# something breaks.
|
|
159
|
-
try:
|
|
160
|
-
from . import backend_pygame as backend
|
|
161
|
-
Sound = backend.SoundPygame
|
|
162
|
-
except Exception:
|
|
163
|
-
failed.append(thisLibName)
|
|
164
|
-
continue
|
|
165
|
-
else:
|
|
166
|
-
break
|
|
167
|
-
elif thisLibName == 'pysoundcard':
|
|
168
|
-
# pysoundcard is a wrapper around PortAudio, which is a cross-platform
|
|
169
|
-
# audio library.
|
|
170
|
-
try:
|
|
171
|
-
from . import backend_pysound as backend
|
|
172
|
-
Sound = backend.SoundPySoundCard
|
|
173
|
-
except Exception:
|
|
174
|
-
failed.append(thisLibName)
|
|
175
|
-
continue
|
|
176
|
-
else:
|
|
177
|
-
break
|
|
178
|
-
else:
|
|
179
|
-
# Catch-all for invalid audioLib prefs.
|
|
180
|
-
msg = ("audioLib pref should be one of {!r}, not {!r}"
|
|
181
|
-
.format(_audioLibs, thisLibName))
|
|
182
|
-
raise ValueError(msg)
|
|
183
|
-
else:
|
|
184
|
-
# if we get here, there is no audioLib that is supported, try for PTB
|
|
185
|
-
msg = ("Failed to load any of the audioLibs: {!r}. Falling back to "
|
|
186
|
-
"PsychToolbox ('ptb') backend for sound. Be sure to add 'ptb' to "
|
|
187
|
-
"preferences to avoid seeing this message again.".format(failed))
|
|
188
|
-
logging.error(msg)
|
|
189
|
-
try:
|
|
190
|
-
from . import backend_ptb as backend
|
|
191
|
-
Sound = backend.SoundPTB
|
|
192
|
-
audioDriver = backend.audioDriver
|
|
193
|
-
except Exception:
|
|
194
|
-
failed.append(thisLibName)
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
# we successfully loaded a backend if `Sound` is not None
|
|
198
|
-
if Sound is not None:
|
|
199
|
-
audioLib = thisLibName
|
|
200
|
-
init = backend.init
|
|
201
|
-
getDevices = None
|
|
202
|
-
if audioLib != 'ptb' and hasattr(backend, 'getDevices'):
|
|
203
|
-
getDevices = backend.getDevices
|
|
204
|
-
else:
|
|
205
|
-
import psychopy.hardware.speaker as speaker
|
|
206
|
-
|
|
207
|
-
def _getDevices(*args, **kwargs): # for matching function signiture
|
|
208
|
-
allDevices = speaker.SpeakerDevice.getAvailableDevices()
|
|
209
|
-
return [dev['name'] for dev in allDevices]
|
|
210
|
-
|
|
211
|
-
getDevices = _getDevices
|
|
212
|
-
|
|
213
|
-
logging.info('sound is using audioLib: %s' % audioLib)
|
|
214
|
-
else:
|
|
215
|
-
# if we get here, there is no audioLib that is supported
|
|
216
|
-
logging.error(
|
|
217
|
-
"No audioLib could be loaded. Tried: {}\n Check whether the necessary "
|
|
218
|
-
"audioLibs are installed.".format(prefs.hardware['audioLib']))
|
|
219
|
-
|
|
220
|
-
# warn the user
|
|
221
|
-
if audioLib is not None:
|
|
222
|
-
if audioLib.lower() != 'ptb':
|
|
223
|
-
# Could be running PTB, just aren't?
|
|
224
|
-
logging.warning("We strongly recommend you activate the PTB sound "
|
|
225
|
-
"engine in PsychoPy prefs as the preferred audio "
|
|
226
|
-
"engine. Its timing is vastly superior. Your prefs "
|
|
227
|
-
"are currently set to use {} (in that order)."
|
|
228
|
-
.format(prefs.hardware['audioLib']))
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
# function to set the device (if current lib allows it)
|
|
232
|
-
def setDevice(dev, kind=None):
|
|
233
|
-
"""Sets the device to be used for new streams being created.
|
|
234
|
-
|
|
235
|
-
Parameters
|
|
236
|
-
----------
|
|
237
|
-
dev: str or dict
|
|
238
|
-
Name of the device to be used (name, index or sounddevice.device)
|
|
239
|
-
kind: str
|
|
240
|
-
One of [None, 'output', 'input']
|
|
241
|
-
|
|
242
|
-
"""
|
|
243
|
-
if dev is None:
|
|
244
|
-
# if given None, do nothing
|
|
245
|
-
return
|
|
246
|
-
|
|
247
|
-
global backend # pull from module namespace
|
|
248
|
-
if not hasattr(backend, 'defaultOutput'):
|
|
249
|
-
raise IOError("Attempting to SetDevice (audio) but not supported by "
|
|
250
|
-
"the current audio library ({!r})".format(audioLib))
|
|
251
|
-
|
|
252
|
-
if hasattr(dev, 'name'):
|
|
253
|
-
dev = dev['name']
|
|
254
|
-
|
|
255
|
-
if kind is None:
|
|
256
|
-
backend.defaultInput = backend.defaultOutput = dev
|
|
257
|
-
elif kind == 'input':
|
|
258
|
-
backend.defaultInput = dev
|
|
259
|
-
elif kind == 'output':
|
|
260
|
-
backend.defaultOutput = dev
|
|
261
|
-
else:
|
|
262
|
-
if systemtools.isVM_CI(): # no audio device on CI, ignore
|
|
263
|
-
return
|
|
264
|
-
else:
|
|
265
|
-
raise TypeError("`kind` should be one of [None, 'output', 'input'] "
|
|
266
|
-
"not {!r}".format(kind))
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
# Set the device according to user prefs (if current lib allows it)
|
|
270
|
-
deviceNames = []
|
|
271
|
-
if backend is None:
|
|
272
|
-
raise ImportError("None of the audio library backends could be imported. "
|
|
273
|
-
"Tried: {}\n Check whether the necessary audioLibs are "
|
|
274
|
-
"installed and can be imported successfully."
|
|
275
|
-
.format(prefs.hardware['audioLib']))
|
|
276
|
-
elif hasattr(backend, 'defaultOutput'):
|
|
277
|
-
pref = prefs.hardware['audioDevice']
|
|
278
|
-
# is it a list or a simple string?
|
|
279
|
-
if isinstance(pref, list):
|
|
280
|
-
# multiple options so use zeroth
|
|
281
|
-
dev = pref[0]
|
|
282
|
-
else:
|
|
283
|
-
# a single option
|
|
284
|
-
dev = pref
|
|
285
|
-
# is it simply "default" (do nothing)
|
|
286
|
-
if dev == 'default' or systemtools.isVM_CI():
|
|
287
|
-
pass # do nothing
|
|
288
|
-
elif getDevices is not None:
|
|
289
|
-
devices = getDevices(kind='output')
|
|
290
|
-
if type(devices) is dict:
|
|
291
|
-
deviceNames = sorted(devices.keys())
|
|
292
|
-
else:
|
|
293
|
-
deviceNames = sorted(devices)
|
|
294
|
-
if dev not in devices:
|
|
295
|
-
logging.warn(
|
|
296
|
-
u"Requested audio device '{}' that is not available on "
|
|
297
|
-
"this hardware. The 'audioDevice' preference should be one of "
|
|
298
|
-
"{}".format(dev, deviceNames))
|
|
299
|
-
else:
|
|
300
|
-
setDevice(dev, kind='output')
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
if __name__ == "__main__":
|
|
304
|
-
pass
|
psychopy/sound/audioclip.py
CHANGED
|
@@ -812,7 +812,165 @@ class AudioClip:
|
|
|
812
812
|
self._sampleRateHz = targetSampleRateHz
|
|
813
813
|
|
|
814
814
|
return self
|
|
815
|
+
|
|
816
|
+
def pad(self, direction='start', duration=1.0, units='seconds'):
|
|
817
|
+
"""Pad the audio clip with silence at the start or end.
|
|
818
|
+
|
|
819
|
+
This method will modify the audio clip inplace, adding samples of
|
|
820
|
+
silence to the specified location.
|
|
821
|
+
|
|
822
|
+
Parameters
|
|
823
|
+
----------
|
|
824
|
+
direction : str
|
|
825
|
+
Where to add the padding. Can be 'start' or 'end'. Default is 'start'.
|
|
826
|
+
duration : float or int
|
|
827
|
+
Duration of the padding in seconds. Default is `1.0`.
|
|
828
|
+
units : str
|
|
829
|
+
Units to interpret the `duration` parameter as. Can be 'seconds' or
|
|
830
|
+
'samples'. Default is 'seconds'.
|
|
831
|
+
|
|
832
|
+
Returns
|
|
833
|
+
-------
|
|
834
|
+
AudioClip
|
|
835
|
+
This audio clip object with padding added at the specified location.
|
|
836
|
+
|
|
837
|
+
"""
|
|
838
|
+
# convert duration to samples
|
|
839
|
+
if units == 'seconds':
|
|
840
|
+
padSamples = int(duration * self.sampleRateHz)
|
|
841
|
+
elif units == 'samples':
|
|
842
|
+
padSamples = int(duration)
|
|
843
|
+
else:
|
|
844
|
+
raise ValueError(
|
|
845
|
+
"Invalid value for `units`. Must be 'seconds' or 'samples'.")
|
|
846
|
+
|
|
847
|
+
# create padding samples
|
|
848
|
+
padding = np.zeros((padSamples, self.channels), dtype=np.float32)
|
|
849
|
+
|
|
850
|
+
if direction == 'start':
|
|
851
|
+
self._samples = np.vstack((padding, self._samples))
|
|
852
|
+
elif direction == 'end':
|
|
853
|
+
self._samples = np.vstack((self._samples, padding))
|
|
854
|
+
else:
|
|
855
|
+
raise ValueError(
|
|
856
|
+
"Invalid value for `direction`. Must be 'start' or 'end'.")
|
|
857
|
+
|
|
858
|
+
# ensure the samples are contiguous and right dtype
|
|
859
|
+
self._samples = np.ascontiguousarray(self._samples, dtype=np.float32)
|
|
860
|
+
|
|
861
|
+
# recompute the duration of the new clip
|
|
862
|
+
self._duration = len(self.samples) / float(self.sampleRateHz)
|
|
863
|
+
|
|
864
|
+
return self
|
|
865
|
+
|
|
866
|
+
def padded(self, direction='start', duration=1.0, units='seconds'):
|
|
867
|
+
"""Return a new audio clip with padding added at the start or end.
|
|
868
|
+
|
|
869
|
+
This method will return a new audio clip with samples of silence added
|
|
870
|
+
to the specified location. The original audio clip will not be modified.
|
|
871
|
+
|
|
872
|
+
Parameters
|
|
873
|
+
----------
|
|
874
|
+
direction : str
|
|
875
|
+
Where to add the padding. Can be 'start' or 'end'. Default is 'start'.
|
|
876
|
+
duration : float or int
|
|
877
|
+
Duration of the padding in seconds. Default is `1.0`.
|
|
878
|
+
units : str
|
|
879
|
+
Units to interpret the `duration` parameter as. Can be 'seconds' or
|
|
880
|
+
'samples'. Default is 'seconds'.
|
|
881
|
+
|
|
882
|
+
Returns
|
|
883
|
+
-------
|
|
884
|
+
AudioClip
|
|
885
|
+
A new audio clip with padding added at the specified location.
|
|
886
|
+
|
|
887
|
+
"""
|
|
888
|
+
# create a copy of the audio clip
|
|
889
|
+
newClip = self.copy()
|
|
890
|
+
|
|
891
|
+
# pad the copy
|
|
892
|
+
return newClip.pad(direction, duration, units)
|
|
893
|
+
|
|
894
|
+
def trim(self, direction='start', duration=1.0, units='seconds'):
|
|
895
|
+
"""Trim the audio clip by removing samples from the start or end.
|
|
896
|
+
|
|
897
|
+
This method will modify the audio clip inplace, trimming off samples
|
|
898
|
+
from the specfied direction.
|
|
815
899
|
|
|
900
|
+
Parameters
|
|
901
|
+
----------
|
|
902
|
+
direction : str
|
|
903
|
+
Where to remove audio samples from. Can be 'start' or 'end'. Default
|
|
904
|
+
is 'start'.
|
|
905
|
+
duration : float or int
|
|
906
|
+
How many samples to remove from the start or end of the audio clip.
|
|
907
|
+
units : str
|
|
908
|
+
Units to interpret the `duration` parameter as. Can be 'seconds' or
|
|
909
|
+
'samples'. Default is 'seconds'.
|
|
910
|
+
|
|
911
|
+
Returns
|
|
912
|
+
-------
|
|
913
|
+
AudioClip
|
|
914
|
+
This audio clip object with padding removed at the specified
|
|
915
|
+
location.
|
|
916
|
+
|
|
917
|
+
"""
|
|
918
|
+
# convert duration to samples
|
|
919
|
+
if units == 'seconds':
|
|
920
|
+
trimSamples = int(duration * self.sampleRateHz)
|
|
921
|
+
elif units == 'samples':
|
|
922
|
+
trimSamples = int(duration)
|
|
923
|
+
else:
|
|
924
|
+
raise ValueError(
|
|
925
|
+
"Invalid value for `units`. Must be 'seconds' or 'samples'.")
|
|
926
|
+
|
|
927
|
+
if direction == 'start':
|
|
928
|
+
self._samples = self._samples[trimSamples:, :]
|
|
929
|
+
elif direction == 'end':
|
|
930
|
+
self._samples = self._samples[:-trimSamples, :]
|
|
931
|
+
else:
|
|
932
|
+
raise ValueError(
|
|
933
|
+
"Invalid value for `direction`. Must be 'start' or 'end'.")
|
|
934
|
+
|
|
935
|
+
# ensure the samples are contiguous and right dtype
|
|
936
|
+
self._samples = np.ascontiguousarray(self._samples, dtype=np.float32)
|
|
937
|
+
|
|
938
|
+
# recompute the duration of the new clip
|
|
939
|
+
self._duration = len(self.samples) / float(self.sampleRateHz)
|
|
940
|
+
|
|
941
|
+
return self
|
|
942
|
+
|
|
943
|
+
def trimmed(self, direction='start', duration=1.0, units='seconds'):
|
|
944
|
+
"""Return a new audio clip with samples removed from the start or end.
|
|
945
|
+
|
|
946
|
+
This method will return a new audio clip with samples of silence removed
|
|
947
|
+
from the specified location. The original audio clip will not be
|
|
948
|
+
modified.
|
|
949
|
+
|
|
950
|
+
Parameters
|
|
951
|
+
----------
|
|
952
|
+
direction : str
|
|
953
|
+
Where to remove audio samples from. Can be 'start' or 'end'. Default
|
|
954
|
+
is 'start'.
|
|
955
|
+
duration : float or int
|
|
956
|
+
How many samples to remove from the start or end of the audio clip.
|
|
957
|
+
units : str
|
|
958
|
+
Units to interpret the `duration` parameter as. Can be 'seconds' or
|
|
959
|
+
'samples'. Default is 'seconds'.
|
|
960
|
+
|
|
961
|
+
Returns
|
|
962
|
+
-------
|
|
963
|
+
AudioClip
|
|
964
|
+
A new audio clip with padding removed at the specified location.
|
|
965
|
+
|
|
966
|
+
"""
|
|
967
|
+
# create a copy of the audio clip
|
|
968
|
+
newClip = self.copy()
|
|
969
|
+
|
|
970
|
+
# trim the copy
|
|
971
|
+
return newClip.trim(direction, duration, units)
|
|
972
|
+
|
|
973
|
+
|
|
816
974
|
# --------------------------------------------------------------------------
|
|
817
975
|
# Audio analysis methods
|
|
818
976
|
#
|
|
@@ -884,6 +1042,12 @@ class AudioClip:
|
|
|
884
1042
|
# recompute duration after updating sample rate
|
|
885
1043
|
self._duration = len(self._samples) / float(self._sampleRateHz)
|
|
886
1044
|
|
|
1045
|
+
@property
|
|
1046
|
+
def totalSamples(self):
|
|
1047
|
+
"""Total number of audio samples stored in this object (`int`).
|
|
1048
|
+
"""
|
|
1049
|
+
return self._samples.shape[0] # number of rows
|
|
1050
|
+
|
|
887
1051
|
@property
|
|
888
1052
|
def duration(self):
|
|
889
1053
|
"""The duration of the audio in seconds (`float`).
|
psychopy/sound/backend_ptb.py
CHANGED
|
@@ -34,6 +34,11 @@ except Exception:
|
|
|
34
34
|
|
|
35
35
|
import numpy as np
|
|
36
36
|
|
|
37
|
+
__all__ = [
|
|
38
|
+
"SoundPTB",
|
|
39
|
+
"Sound"
|
|
40
|
+
]
|
|
41
|
+
|
|
37
42
|
|
|
38
43
|
defaultLatencyClass = 1
|
|
39
44
|
# suggestedLatency = 0.005 ## Not currently used. Keep < 1 scr refresh
|
|
@@ -396,3 +401,6 @@ class SoundPTB(_SoundBase):
|
|
|
396
401
|
self.__dict__['track'] = None
|
|
397
402
|
else:
|
|
398
403
|
self.__dict__['track'] = weakref.ref(track)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
Sound = SoundPTB
|
psychopy/sound/backend_pygame.py
CHANGED
|
@@ -19,6 +19,13 @@ except ImportError as err:
|
|
|
19
19
|
raise DependencyError(repr(err))
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"SoundPygame",
|
|
25
|
+
"Sound"
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
22
29
|
def getDevices(kind=None):
|
|
23
30
|
"""Get audio playback and recording devices via the backend's audio API.
|
|
24
31
|
|
|
@@ -311,3 +318,6 @@ class SoundPygame(_SoundBase):
|
|
|
311
318
|
thisArray = ((thisArray + 1) * 2**7).astype(numpy.uint8)
|
|
312
319
|
|
|
313
320
|
self._snd = sndarray.make_sound(thisArray)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
Sound = SoundPygame
|
|
@@ -21,6 +21,12 @@ from os import path
|
|
|
21
21
|
import weakref
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
__all__ = [
|
|
25
|
+
"SoundPySoundCard",
|
|
26
|
+
"Sound"
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
24
30
|
def init(rate=44100, stereo=True, buffer=128):
|
|
25
31
|
pass
|
|
26
32
|
# for compatibility with other backends but not needed
|
|
@@ -313,3 +319,6 @@ class SoundPySoundCard(_SoundBase):
|
|
|
313
319
|
def __del__(self):
|
|
314
320
|
if hasattr(self, "_stream"):
|
|
315
321
|
self._stream.close()
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
Sound = SoundPySoundCard
|
|
File without changes
|