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.

Files changed (220) hide show
  1. psychopy/VERSION +1 -1
  2. psychopy/alerts/alertsCatalogue/4810.yaml +19 -0
  3. psychopy/alerts/alertsCatalogue/alertCategories.yaml +4 -0
  4. psychopy/alerts/alertsCatalogue/alertmsg.py +15 -1
  5. psychopy/alerts/alertsCatalogue/generateAlertmsg.py +2 -2
  6. psychopy/app/Resources/classic/add_many.png +0 -0
  7. psychopy/app/Resources/classic/add_many@2x.png +0 -0
  8. psychopy/app/Resources/classic/devices.png +0 -0
  9. psychopy/app/Resources/classic/devices@2x.png +0 -0
  10. psychopy/app/Resources/classic/photometer.png +0 -0
  11. psychopy/app/Resources/classic/photometer@2x.png +0 -0
  12. psychopy/app/Resources/dark/add_many.png +0 -0
  13. psychopy/app/Resources/dark/add_many@2x.png +0 -0
  14. psychopy/app/Resources/dark/devices.png +0 -0
  15. psychopy/app/Resources/dark/devices@2x.png +0 -0
  16. psychopy/app/Resources/dark/photometer.png +0 -0
  17. psychopy/app/Resources/dark/photometer@2x.png +0 -0
  18. psychopy/app/Resources/light/add_many.png +0 -0
  19. psychopy/app/Resources/light/add_many@2x.png +0 -0
  20. psychopy/app/Resources/light/devices.png +0 -0
  21. psychopy/app/Resources/light/devices@2x.png +0 -0
  22. psychopy/app/Resources/light/photometer.png +0 -0
  23. psychopy/app/Resources/light/photometer@2x.png +0 -0
  24. psychopy/app/_psychopyApp.py +35 -13
  25. psychopy/app/builder/builder.py +88 -35
  26. psychopy/app/builder/dialogs/__init__.py +69 -220
  27. psychopy/app/builder/dialogs/dlgsCode.py +29 -8
  28. psychopy/app/builder/dialogs/paramCtrls.py +1468 -904
  29. psychopy/app/builder/validators.py +25 -17
  30. psychopy/app/coder/coder.py +12 -1
  31. psychopy/app/coder/repl.py +5 -2
  32. psychopy/app/colorpicker/__init__.py +1 -1
  33. psychopy/app/deviceManager/__init__.py +1 -0
  34. psychopy/app/deviceManager/addDialog.py +218 -0
  35. psychopy/app/deviceManager/dialog.py +185 -0
  36. psychopy/app/deviceManager/panel.py +191 -0
  37. psychopy/app/deviceManager/utils.py +60 -0
  38. psychopy/app/idle.py +7 -0
  39. psychopy/app/locale/ar_001/LC_MESSAGE/messages.mo +0 -0
  40. psychopy/app/locale/ar_001/LC_MESSAGE/messages.po +12695 -10592
  41. psychopy/app/locale/cs_CZ/LC_MESSAGE/messages.mo +0 -0
  42. psychopy/app/locale/cs_CZ/LC_MESSAGE/messages.po +10199 -24
  43. psychopy/app/locale/da_DK/LC_MESSAGE/messages.mo +0 -0
  44. psychopy/app/locale/da_DK/LC_MESSAGE/messages.po +10199 -24
  45. psychopy/app/locale/de_DE/LC_MESSAGE/messages.mo +0 -0
  46. psychopy/app/locale/de_DE/LC_MESSAGE/messages.po +11221 -9712
  47. psychopy/app/locale/el_GR/LC_MESSAGE/messages.mo +0 -0
  48. psychopy/app/locale/el_GR/LC_MESSAGE/messages.po +10200 -25
  49. psychopy/app/locale/en_NZ/LC_MESSAGE/messages.mo +0 -0
  50. psychopy/app/locale/en_NZ/LC_MESSAGE/messages.po +10200 -25
  51. psychopy/app/locale/en_US/LC_MESSAGE/messages.mo +0 -0
  52. psychopy/app/locale/en_US/LC_MESSAGE/messages.po +10195 -18
  53. psychopy/app/locale/es_CO/LC_MESSAGE/messages.mo +0 -0
  54. psychopy/app/locale/es_CO/LC_MESSAGE/messages.po +11917 -9101
  55. psychopy/app/locale/es_ES/LC_MESSAGE/messages.mo +0 -0
  56. psychopy/app/locale/es_ES/LC_MESSAGE/messages.po +11924 -9103
  57. psychopy/app/locale/es_US/LC_MESSAGE/messages.mo +0 -0
  58. psychopy/app/locale/es_US/LC_MESSAGE/messages.po +11917 -9101
  59. psychopy/app/locale/et_EE/LC_MESSAGE/messages.mo +0 -0
  60. psychopy/app/locale/et_EE/LC_MESSAGE/messages.po +11084 -9569
  61. psychopy/app/locale/fa_IR/LC_MESSAGE/messages.mo +0 -0
  62. psychopy/app/locale/fa_IR/LC_MESSAGE/messages.po +11590 -5806
  63. psychopy/app/locale/fi_FI/LC_MESSAGE/messages.mo +0 -0
  64. psychopy/app/locale/fi_FI/LC_MESSAGE/messages.po +10199 -24
  65. psychopy/app/locale/fr_FR/LC_MESSAGE/messages.mo +0 -0
  66. psychopy/app/locale/fr_FR/LC_MESSAGE/messages.po +11091 -9577
  67. psychopy/app/locale/he_IL/LC_MESSAGE/messages.mo +0 -0
  68. psychopy/app/locale/he_IL/LC_MESSAGE/messages.po +11072 -9549
  69. psychopy/app/locale/hi_IN/LC_MESSAGE/messages.mo +0 -0
  70. psychopy/app/locale/hi_IN/LC_MESSAGE/messages.po +11071 -9559
  71. psychopy/app/locale/hu_HU/LC_MESSAGE/messages.mo +0 -0
  72. psychopy/app/locale/hu_HU/LC_MESSAGE/messages.po +10200 -25
  73. psychopy/app/locale/it_IT/LC_MESSAGE/messages.mo +0 -0
  74. psychopy/app/locale/it_IT/LC_MESSAGE/messages.po +11072 -9560
  75. psychopy/app/locale/ja_JP/LC_MESSAGE/messages.mo +0 -0
  76. psychopy/app/locale/ja_JP/LC_MESSAGE/messages.po +1485 -1137
  77. psychopy/app/locale/ko_KR/LC_MESSAGE/messages.mo +0 -0
  78. psychopy/app/locale/ko_KR/LC_MESSAGE/messages.po +10199 -24
  79. psychopy/app/locale/ms_MY/LC_MESSAGE/messages.mo +0 -0
  80. psychopy/app/locale/ms_MY/LC_MESSAGE/messages.po +11463 -8757
  81. psychopy/app/locale/nl_NL/LC_MESSAGE/messages.mo +0 -0
  82. psychopy/app/locale/nl_NL/LC_MESSAGE/messages.po +10200 -25
  83. psychopy/app/locale/nn_NO/LC_MESSAGE/messages.mo +0 -0
  84. psychopy/app/locale/nn_NO/LC_MESSAGE/messages.po +10200 -25
  85. psychopy/app/locale/pl_PL/LC_MESSAGE/messages.mo +0 -0
  86. psychopy/app/locale/pl_PL/LC_MESSAGE/messages.po +10200 -25
  87. psychopy/app/locale/pt_PT/LC_MESSAGE/messages.mo +0 -0
  88. psychopy/app/locale/pt_PT/LC_MESSAGE/messages.po +11288 -9434
  89. psychopy/app/locale/ro_RO/LC_MESSAGE/messages.mo +0 -0
  90. psychopy/app/locale/ro_RO/LC_MESSAGE/messages.po +10200 -25
  91. psychopy/app/locale/ru_RU/LC_MESSAGE/messages.mo +0 -0
  92. psychopy/app/locale/ru_RU/LC_MESSAGE/messages.po +10199 -24
  93. psychopy/app/locale/sv_SE/LC_MESSAGE/messages.mo +0 -0
  94. psychopy/app/locale/sv_SE/LC_MESSAGE/messages.po +11441 -8747
  95. psychopy/app/locale/tr_TR/LC_MESSAGE/messages.mo +0 -0
  96. psychopy/app/locale/tr_TR/LC_MESSAGE/messages.po +11069 -9545
  97. psychopy/app/locale/zh_CN/LC_MESSAGE/messages.mo +0 -0
  98. psychopy/app/locale/zh_CN/LC_MESSAGE/messages.po +12085 -8268
  99. psychopy/app/locale/zh_TW/LC_MESSAGE/messages.mo +0 -0
  100. psychopy/app/locale/zh_TW/LC_MESSAGE/messages.po +11929 -8022
  101. psychopy/app/plugin_manager/dialog.py +12 -3
  102. psychopy/app/plugin_manager/packageIndex.py +303 -0
  103. psychopy/app/plugin_manager/packages.py +203 -63
  104. psychopy/app/plugin_manager/plugins.py +120 -240
  105. psychopy/app/preferencesDlg.py +6 -1
  106. psychopy/app/psychopyApp.py +16 -4
  107. psychopy/app/runner/runner.py +10 -2
  108. psychopy/app/runner/scriptProcess.py +8 -3
  109. psychopy/app/stdout/stdOutRich.py +11 -4
  110. psychopy/app/themes/icons.py +3 -0
  111. psychopy/app/utils.py +61 -0
  112. psychopy/data/experiment.py +133 -23
  113. psychopy/data/routine.py +12 -0
  114. psychopy/data/staircase.py +42 -20
  115. psychopy/data/trial.py +20 -12
  116. psychopy/data/utils.py +42 -2
  117. psychopy/demos/builder/Experiments/dragAndDrop/drag_and_drop.psyexp +22 -5
  118. psychopy/demos/builder/Experiments/dragAndDrop/stimuli/solutions.xlsx +0 -0
  119. psychopy/demos/builder/Experiments/stroopVoice/stroopVoice.psyexp +2 -12
  120. psychopy/demos/builder/Feature Demos/buttonBox/buttonBoxDemo.psyexp +3 -8
  121. psychopy/demos/builder/Feature Demos/movies/movie.psyexp +220 -0
  122. psychopy/demos/builder/Feature Demos/movies/readme.md +3 -0
  123. psychopy/demos/builder/Feature Demos/visualValidator/visualValidator.psyexp +1 -2
  124. psychopy/demos/builder/Hardware/camera/camera.psyexp +3 -16
  125. psychopy/demos/builder/Hardware/microphone/microphone.psyexp +3 -16
  126. psychopy/demos/coder/hardware/hdf5_extract.py +133 -0
  127. psychopy/event.py +20 -15
  128. psychopy/experiment/_experiment.py +86 -10
  129. psychopy/experiment/components/__init__.py +3 -10
  130. psychopy/experiment/components/_base.py +9 -20
  131. psychopy/experiment/components/button/__init__.py +1 -1
  132. psychopy/experiment/components/buttonBox/__init__.py +50 -54
  133. psychopy/experiment/components/camera/__init__.py +137 -359
  134. psychopy/experiment/components/keyboard/__init__.py +17 -24
  135. psychopy/experiment/components/microphone/__init__.py +61 -110
  136. psychopy/experiment/components/movie/__init__.py +2 -3
  137. psychopy/experiment/components/serialOut/__init__.py +192 -93
  138. psychopy/experiment/components/settings/__init__.py +45 -27
  139. psychopy/experiment/components/sound/__init__.py +82 -73
  140. psychopy/experiment/components/soundsensor/__init__.py +43 -80
  141. psychopy/experiment/devices.py +303 -0
  142. psychopy/experiment/exports.py +20 -18
  143. psychopy/experiment/flow.py +7 -0
  144. psychopy/experiment/loops.py +47 -29
  145. psychopy/experiment/monitor.py +74 -0
  146. psychopy/experiment/params.py +48 -10
  147. psychopy/experiment/plugins.py +28 -108
  148. psychopy/experiment/py2js_transpiler.py +1 -1
  149. psychopy/experiment/routines/__init__.py +1 -1
  150. psychopy/experiment/routines/_base.py +59 -24
  151. psychopy/experiment/routines/audioValidator/__init__.py +19 -155
  152. psychopy/experiment/routines/visualValidator/__init__.py +25 -25
  153. psychopy/hardware/__init__.py +20 -57
  154. psychopy/hardware/button.py +15 -2
  155. psychopy/hardware/camera/__init__.py +2237 -1394
  156. psychopy/hardware/joystick/__init__.py +1 -1
  157. psychopy/hardware/keyboard.py +5 -8
  158. psychopy/hardware/listener.py +4 -1
  159. psychopy/hardware/manager.py +75 -35
  160. psychopy/hardware/microphone.py +52 -6
  161. psychopy/hardware/monitor.py +144 -0
  162. psychopy/hardware/photometer/__init__.py +156 -117
  163. psychopy/hardware/serialdevice.py +16 -2
  164. psychopy/hardware/soundsensor.py +4 -1
  165. psychopy/iohub/devices/deviceConfigValidation.py +2 -1
  166. psychopy/iohub/devices/keyboard/darwin.py +8 -5
  167. psychopy/iohub/util/__init__.py +7 -8
  168. psychopy/localization/generateTranslationTemplate.py +208 -116
  169. psychopy/localization/messages.pot +4305 -3502
  170. psychopy/monitors/MonitorCenter.py +174 -74
  171. psychopy/plugins/__init__.py +6 -4
  172. psychopy/preferences/devices.py +80 -0
  173. psychopy/preferences/generateHints.py +2 -1
  174. psychopy/preferences/preferences.py +35 -11
  175. psychopy/scripts/psychopy-pkgutil.py +969 -0
  176. psychopy/scripts/psyexpCompile.py +1 -1
  177. psychopy/session.py +34 -38
  178. psychopy/sound/__init__.py +6 -260
  179. psychopy/sound/audioclip.py +164 -0
  180. psychopy/sound/backend_ptb.py +8 -0
  181. psychopy/sound/backend_pygame.py +10 -0
  182. psychopy/sound/backend_pysound.py +9 -0
  183. psychopy/sound/backends/__init__.py +0 -0
  184. psychopy/sound/microphone.py +3 -0
  185. psychopy/sound/sound.py +58 -0
  186. psychopy/tests/data/correctScript/python/correctNoiseStimComponent.py +1 -1
  187. psychopy/tests/data/duplicateHeaders.csv +2 -0
  188. psychopy/tests/test_app/test_builder/test_BuilderFrame.py +22 -7
  189. psychopy/tests/test_app/test_builder/test_CompileFromBuilder.py +0 -2
  190. psychopy/tests/test_data/test_utils.py +5 -1
  191. psychopy/tests/test_experiment/test_components/test_ButtonBoxComponent.py +22 -2
  192. psychopy/tests/test_hardware/test_ports.py +0 -12
  193. psychopy/tests/test_tools/test_stringtools.py +1 -1
  194. psychopy/tools/attributetools.py +12 -5
  195. psychopy/tools/fontmanager.py +17 -14
  196. psychopy/tools/movietools.py +43 -2
  197. psychopy/tools/stringtools.py +33 -8
  198. psychopy/tools/versionchooser.py +1 -1
  199. psychopy/validation/audio.py +5 -1
  200. psychopy/validation/visual.py +5 -1
  201. psychopy/visual/basevisual.py +8 -7
  202. psychopy/visual/circle.py +2 -2
  203. psychopy/visual/image.py +29 -109
  204. psychopy/visual/movies/__init__.py +1800 -313
  205. psychopy/visual/polygon.py +4 -0
  206. psychopy/visual/shape.py +2 -2
  207. psychopy/visual/window.py +34 -11
  208. psychopy/voicekey/__init__.py +41 -669
  209. psychopy/voicekey/labjack_vks.py +7 -48
  210. psychopy/voicekey/parallel_vks.py +7 -42
  211. psychopy/voicekey/vk_tools.py +114 -263
  212. {psychopy-2025.1.1.dist-info → psychopy-2025.2.1.dist-info}/METADATA +17 -11
  213. {psychopy-2025.1.1.dist-info → psychopy-2025.2.1.dist-info}/RECORD +216 -184
  214. {psychopy-2025.1.1.dist-info → psychopy-2025.2.1.dist-info}/WHEEL +1 -1
  215. psychopy/visual/movies/players/__init__.py +0 -62
  216. psychopy/visual/movies/players/ffpyplayer_player.py +0 -1401
  217. psychopy/voicekey/demo_vks.py +0 -12
  218. psychopy/voicekey/signal.py +0 -42
  219. {psychopy-2025.1.1.dist-info → psychopy-2025.2.1.dist-info}/entry_points.txt +0 -0
  220. {psychopy-2025.1.1.dist-info → psychopy-2025.2.1.dist-info}/licenses/LICENSE +0 -0
@@ -16,12 +16,13 @@ The code that writes out a *_lastrun.py experiment file is (in order):
16
16
  settings.SettingsComponent.writeEndCode()
17
17
  """
18
18
  import functools
19
+ import json
19
20
  from xml.etree.ElementTree import Element
20
21
 
21
22
  import re
22
23
  from pathlib import Path
23
24
 
24
- from psychopy import logging
25
+ from psychopy import data, logging
25
26
  from . import utils
26
27
  from . import py2js
27
28
 
@@ -55,6 +56,8 @@ inputDefaults = {
55
56
  # these are parameters which once existed but are no longer needed, so inclusion in this list will
56
57
  # silence any "future version" warnings
57
58
  legacyParams = [
59
+ # settings params from the early days of PsychoJS
60
+ "JS libs", "OSF Project ID"
58
61
  # in 2021.1, we standardised colorSpace to be object-wide rather than param-specific
59
62
  "lineColorSpace", "borderColorSpace", "fillColorSpace", "foreColorSpace",
60
63
  # in 2024.2.0, we removed some superfluous params from the pupil labs backend
@@ -230,7 +233,7 @@ class Param():
230
233
  return "%i" % self.val # int and float -> str(int)
231
234
  except TypeError:
232
235
  return "%s" % self.val # try array of float instead?
233
- elif self.valType in ['extendedStr','str', 'file', 'table']:
236
+ elif self.valType in ['extendedStr', 'str', 'file', 'table', 'device']:
234
237
  # at least 1 non-escaped '$' anywhere --> code wanted
235
238
  # return str if code wanted
236
239
  # return repr if str wanted; this neatly handles "it's" and 'He
@@ -300,13 +303,19 @@ class Param():
300
303
  # Otherwise, treat as string
301
304
  return repr(val)
302
305
  elif self.valType == 'list':
303
- valid, val = self.dollarSyntax()
304
- val = toList(val)
305
- return "{}".format(val)
306
+ if self.inputType == "fileList":
307
+ # treat each item as a string-type param
308
+ output = []
309
+ for item in data.utils.listFromString(self.val):
310
+ item = str(Param(item, "file"))
311
+ output.append(item)
312
+ return "[{}]".format(",".join(output))
313
+ else:
314
+ valid, val = self.dollarSyntax()
315
+ val = toList(val)
316
+ return "{}".format(val)
306
317
  elif self.valType == 'fixedList':
307
318
  return "{}".format(self.val)
308
- elif self.valType == 'fileList':
309
- return "{}".format(self.val)
310
319
  elif self.valType == 'bool':
311
320
  if utils.scriptTarget == "PsychoJS":
312
321
  return ("%s" % self.val).lower() # make True -> "true"
@@ -372,12 +381,41 @@ class Param():
372
381
  allowedLabels=self.allowedLabels,
373
382
  direct=self.direct,
374
383
  canBePath=self.canBePath,
375
- categ=self.categ
384
+ categ=self.categ,
385
+ ctrlParams=self.ctrlParams
376
386
  )
377
387
 
378
388
  def __deepcopy__(self, memo):
379
389
  return self.copy()
380
-
390
+
391
+ @classmethod
392
+ def fromJSON(cls, data):
393
+ # initialise
394
+ param = Param(
395
+ "",
396
+ "code",
397
+ )
398
+ # apply
399
+ param.applyJSON(data)
400
+
401
+ def applyJSON(self, data):
402
+ if "val" in data:
403
+ self.val = data['val']
404
+ if "valType" in data:
405
+ self.valType = data['valType']
406
+ if "updates" in data:
407
+ self.updates = "{}".format(data['updates'])
408
+ if "plugin" in data:
409
+ self.plugin = "{}".format(data['plugin'])
410
+
411
+ def toJSON(self):
412
+ return {
413
+ 'val': self.val,
414
+ 'valType': self.valType,
415
+ 'updates': "{}".format(self.updates),
416
+ 'plugin': "{}".format(self.plugin)
417
+ }
418
+
381
419
  @property
382
420
  def _xml(self):
383
421
  # Make root element
@@ -425,7 +463,7 @@ class Param():
425
463
  return True, val
426
464
  else:
427
465
  # If value does not begin with an unescaped $, treat it as a string
428
- if not re.findall(r"(?<!\\)\$", val):
466
+ if not re.findall(r"(?<!\\)\$", str(val)):
429
467
  # Return if all $ are escaped (\$)
430
468
  return True, val
431
469
  else:
@@ -3,120 +3,40 @@ from psychopy import logging
3
3
 
4
4
  class PluginDevicesMixin:
5
5
  """
6
- Mixin for Components and Routines which adds behaviour to get parameters and values from
7
- plugins and use them to create different devices for different plugin backends.
6
+ Legacy placeholder class - used to handle device backends added by plugins before Builder had a dedicated device manager.
8
7
  """
9
-
10
- def __init_subclass__(cls):
11
- # list of backends for this component - each should be a subclass of DeviceBackend
12
- cls.backends = []
13
-
14
- def loadBackends(self):
15
- # add params from backends
16
- for backend in self.backends:
17
- # get params using backend's method
18
- params, order = backend.getParams(self)
19
- # get reference to package
20
- plugin = None
21
- if hasattr(backend, "__module__") and backend.__module__:
22
- pkg = backend.__module__.split(".")[0]
23
- if pkg != "psychopy":
24
- plugin = pkg
25
- # add order
26
- self.order.extend(order)
27
- # add any params
28
- for key, param in params.items():
29
- if key in self.params:
30
- # if this param already exists (i.e. from saved data), get the saved val
31
- param.val = self.params[key].val
32
- param.updates = self.params[key].updates
33
- # add param
34
- self.params[key] = param
35
- # store plugin reference
36
- self.params[key].plugin = plugin
37
-
38
- # add dependencies so that backend params are only shown for this backend
39
- for name in params:
40
- self.depends.append(
41
- {
42
- "dependsOn": "deviceBackend", # if...
43
- "condition": f"== '{backend.key}'", # meets...
44
- "param": name, # then...
45
- "true": "show", # should...
46
- "false": "hide", # otherwise...
47
- }
48
- )
49
- # add requirements
50
- backend.addRequirements(self)
51
-
52
- def getBackendKeys(self):
53
- keys = []
54
- for backend in self.backends:
55
- keys.append(backend.key)
56
-
57
- return keys
58
-
59
- def getBackendLabels(self):
60
- labels = []
61
- for backend in self.backends:
62
- labels.append(backend.label)
63
-
64
- return labels
65
-
66
- def writeDeviceCode(self, buff):
67
- # write init code from backend
68
- for backend in self.backends:
69
- if backend.key == self.params['deviceBackend']:
70
- backend.writeDeviceCode(self, buff)
8
+ pass
71
9
 
72
10
 
73
- class DeviceBackend:
74
- # which component is this backend for?
11
+ from .devices import DeviceBackend
12
+ class DeviceBackend(DeviceBackend):
13
+ """
14
+ Legacy wrapper to convert plugins.DeviceBackend classes to the newer devices.DeviceBackend class.
15
+ """
16
+ key = "microphone"
17
+ label = ""
18
+ component = None
19
+ deviceClasses = ["psychopy.hardware.soundsensor.MicrophoneSoundSensor"]
75
20
  component = PluginDevicesMixin
76
- # what value should Builder use for this backend?
77
21
  key = ""
78
- # what label should be displayed by Builder for this backend?
79
22
  label = ""
80
- # values to append to the component's deviceClasses array
81
23
  deviceClasses = []
82
24
 
83
- def __init_subclass__(cls):
84
- logging.debug(
85
- f"Registered backend for {cls.component.__name__}: {cls.label} ({cls.key}) from "
86
- f"{cls.__module__}:{cls.__name__}"
87
- )
88
- # add class to list of backends for ButtonBoxComponent
89
- cls.component.backends = cls.component.backends.copy()
90
- cls.component.backends.append(cls)
91
- # add device classes to component
92
- if cls is not PluginDevicesMixin and hasattr(cls.component, "deviceClasses"):
93
- for deviceClass in cls.deviceClasses:
94
- if deviceClass not in cls.component.deviceClasses:
95
- cls.component.deviceClasses = cls.component.deviceClasses.copy()
96
- cls.component.deviceClasses.append(deviceClass)
97
-
98
- def getParams(self):
99
- """
100
- Get parameters from this backend to add to each new instance of ButtonBoxComponent
101
-
102
- Returns
103
- -------
104
- dict[str:Param]
105
- Dict of Param objects, which will be added to any Button Box Component's params, along
106
- with a dependency to only show them when this backend is selected
107
- list[str]
108
- List of param names, defining the order in which params should appear
109
- """
110
- raise NotImplementedError()
111
25
 
112
- def addRequirements(self):
113
- """
114
- Add any required module/package imports for this backend
115
- """
116
- return
117
-
118
- def writeDeviceCode(self, buff):
119
- """
120
- Write the code to create a device for this backend
121
- """
122
- raise NotImplementedError()
26
+ def __init_subclass__(cls):
27
+ # if component was specified by class attribute, register it
28
+ if cls.component is not PluginDevicesMixin:
29
+ cls.component.registerBackend(cls)
30
+ # if we can get params without initialising, add them to the Component's legacy list
31
+ # (as they're now parameters of the device rather than the Component)
32
+ try:
33
+ for name in cls.getParams(None)[0]:
34
+ cls.component.legacyParams.append(name)
35
+ except AttributeError:
36
+ pass
37
+ # placeholder value for icon
38
+ cls.icon = None
39
+ # use label for backendLabel
40
+ cls.backendLabel = cls.label
41
+ # a backend only has 1 device now
42
+ cls.deviceClass = cls.deviceClasses[0]
@@ -12,7 +12,7 @@ import re
12
12
  try:
13
13
  from metapensiero.pj.api import translates
14
14
  except ImportError:
15
- pass # metapensiero not installed
15
+ translates = None # metapensiero not installed
16
16
 
17
17
  import astunparse
18
18
 
@@ -8,7 +8,7 @@
8
8
  # Distributed under the terms of the GNU General Public License (GPL).
9
9
 
10
10
  from importlib import import_module
11
- from ._base import BaseStandaloneRoutine, BaseValidatorRoutine, Routine
11
+ from ._base import BaseStandaloneRoutine, BaseDeviceRoutine, BaseValidatorRoutine, Routine
12
12
  from .unknown import UnknownRoutine
13
13
  from pathlib import Path
14
14
  from psychopy import logging
@@ -16,6 +16,7 @@ from pathlib import Path
16
16
 
17
17
  from psychopy.experiment.components.static import StaticComponent
18
18
  from psychopy.experiment.components.routineSettings import RoutineSettingsComponent
19
+ from psychopy.experiment.devices import DeviceMixin
19
20
  from psychopy.localization import _translate
20
21
  from psychopy.experiment import Param
21
22
 
@@ -32,6 +33,9 @@ class BaseStandaloneRoutine:
32
33
  beta = False
33
34
  # hide this Component in Builder view?
34
35
  hidden = False
36
+ # are there any known legacy params for this Routine?
37
+ # these will be removed & warnings ignored on experiment load
38
+ legacyParams = []
35
39
 
36
40
  def __init__(self, exp, name='',
37
41
  stopType='duration (s)', stopVal='',
@@ -47,7 +51,7 @@ class BaseStandaloneRoutine:
47
51
  msg = _translate(
48
52
  "Name of this Routine (alphanumeric or _, no spaces)")
49
53
  self.params['name'] = Param(name,
50
- valType='code', inputType="single", categ='Basic',
54
+ valType='code', inputType="name", categ='Basic',
51
55
  hint=msg,
52
56
  label=_translate('Name'))
53
57
 
@@ -374,14 +378,41 @@ class BaseStandaloneRoutine:
374
378
  self.params['disabled'].val = value
375
379
 
376
380
 
377
- class BaseValidatorRoutine(BaseStandaloneRoutine):
381
+ class BaseDeviceRoutine(BaseStandaloneRoutine, DeviceMixin):
382
+ """
383
+ Base class for most routines which interface with a hardware device.
384
+ """
385
+ def __init__(
386
+ self, exp,
387
+ # basic
388
+ name='',
389
+ stopType='duration (s)', stopVal='',
390
+ # device
391
+ deviceLabel="",
392
+ # testing
393
+ disabled=False
394
+ ):
395
+ # initialise base component
396
+ BaseStandaloneRoutine.__init__(
397
+ self, exp,
398
+ # basic
399
+ name=name,
400
+ stopType=stopType, stopVal=stopVal,
401
+ # testing
402
+ disabled=disabled
403
+ )
404
+ # add device stuff
405
+ self.addDeviceParams(
406
+ defaultLabel=deviceLabel
407
+ )
408
+
409
+
410
+ class BaseValidatorRoutine(BaseDeviceRoutine):
378
411
  """
379
412
  Subcategory of Standalone Routine, which sets up a "validator" - an object which is linked to in the Testing tab
380
413
  of another Component and validates that the component behaved as expected. Any validator Routines should subclass
381
414
  this rather than BaseStandaloneRoutine.
382
415
  """
383
- # list of class strings (readable by DeviceManager) which this component's device could be
384
- deviceClasses = []
385
416
 
386
417
  def writeRoutineStartValidationCode(self, buff, stim):
387
418
  """
@@ -667,19 +698,23 @@ class Routine(list):
667
698
  )
668
699
  buff.writeIndentedLines(code % self.params)
669
700
 
670
- code = ("for thisComponent in {name}.components:\n"
671
- " thisComponent.tStart = None\n"
672
- " thisComponent.tStop = None\n"
673
- " thisComponent.tStartRefresh = None\n"
674
- " thisComponent.tStopRefresh = None\n"
675
- " if hasattr(thisComponent, 'status'):\n"
676
- " thisComponent.status = NOT_STARTED\n"
677
- "# reset timers\n"
678
- 't = 0\n'
679
- '_timeToFirstFrame = win.getFutureFlipTime(clock="now")\n'
680
- # '{clockName}.reset(-_timeToFirstFrame) # t0 is time of first possible flip\n'
681
- 'frameN = -1\n'
682
- '\n# --- Run Routine "{name}" ---\n')
701
+ code = (
702
+ "for thisComponent in {name}.components:\n"
703
+ " thisComponent.tStart = None\n"
704
+ " thisComponent.tStop = None\n"
705
+ " thisComponent.tStartRefresh = None\n"
706
+ " thisComponent.tStopRefresh = None\n"
707
+ " if hasattr(thisComponent, 'status'):\n"
708
+ " thisComponent.status = NOT_STARTED\n"
709
+ "# reset timers\n"
710
+ 't = 0\n'
711
+ '_timeToFirstFrame = win.getFutureFlipTime(clock="now")\n'
712
+ # '{clockName}.reset(-_timeToFirstFrame) # t0 is time of first possible flip\n'
713
+ 'frameN = -1\n'
714
+ '\n'
715
+ '# --- Run Routine "{name}" ---\n'
716
+ 'thisExp.currentRoutine = {name}\n'
717
+ )
683
718
  buff.writeIndentedLines(code.format(name=self.name,
684
719
  clockName=self._clockName))
685
720
 
@@ -760,16 +795,16 @@ class Routine(list):
760
795
  # are we done yet?
761
796
  code = (
762
797
  '\n'
763
- '# check if all components have finished\n'
764
- 'if not continueRoutine: # a component has requested a '
765
- 'forced-end of Routine\n'
798
+ '# has a Component requested the Routine to end?\n'
799
+ 'if not continueRoutine:\n'
766
800
  ' %(name)s.forceEnded = routineForceEnded = True\n'
801
+ '# has the Routine been forcibly ended?\n'
802
+ 'if %(name)s.forceEnded or routineForceEnded:\n'
767
803
  ' break\n'
768
- 'continueRoutine = False # will revert to True if at least '
769
- 'one component still running\n'
804
+ '# has every Component finished?\n'
805
+ 'continueRoutine = False\n'
770
806
  'for thisComponent in %(name)s.components:\n'
771
- ' if hasattr(thisComponent, "status") and '
772
- 'thisComponent.status != FINISHED:\n'
807
+ ' if hasattr(thisComponent, "status") and thisComponent.status != FINISHED:\n'
773
808
  ' continueRoutine = True\n'
774
809
  ' break # at least one component has not yet finished\n')
775
810
  buff.writeIndentedLines(code % self.params)
@@ -2,15 +2,16 @@
2
2
  # -*- coding: utf-8 -*-
3
3
 
4
4
  from pathlib import Path
5
+ from psychopy.preferences import prefs
5
6
  from psychopy.alerts._alerts import alert
6
7
  from psychopy.experiment import Param
7
- from psychopy.experiment.plugins import PluginDevicesMixin, DeviceBackend
8
+ from psychopy.experiment.plugins import DeviceBackend
8
9
  from psychopy.experiment.components import getInitVals
9
- from psychopy.experiment.routines import Routine, BaseValidatorRoutine
10
+ from psychopy.experiment.routines import Routine, BaseDeviceRoutine
10
11
  from psychopy.localization import _translate
11
12
 
12
13
 
13
- class AudioValidatorRoutine(BaseValidatorRoutine, PluginDevicesMixin):
14
+ class AudioValidatorRoutine(BaseDeviceRoutine):
14
15
  """
15
16
  Use a sound sensor (voicekey or microphone) to confirm that audio stimuli are presented when they should be.
16
17
  """
@@ -22,16 +23,24 @@ class AudioValidatorRoutine(BaseValidatorRoutine, PluginDevicesMixin):
22
23
  "Use a sound sensor to confirm that audio stimuli are presented when they should "
23
24
  "be."
24
25
  )
25
- deviceClasses = ["psychopy.validation.voicekey.AudioValidator"]
26
26
  version = "2025.1.0"
27
+ legacyParams = [
28
+ # old device setup params, no longer needed as this is handled by DeviceManager
29
+ "deviceBackend",
30
+ "meMicrophone",
31
+ "meThreshold",
32
+ "meRange",
33
+ "meSamplingWindow",
34
+ ]
27
35
 
28
36
  def __init__(
29
37
  self,
30
38
  # basic
31
39
  exp, name='audioVal',
32
- threshold=0.5,
33
40
  # device
34
- deviceLabel="", deviceBackend="microphone", channel="0",
41
+ deviceLabel="", channel="0",
42
+ # legacy
43
+ threshold=0.5, deviceBackend="microphone",
35
44
  ):
36
45
 
37
46
  self.exp = exp # so we can access the experiment if necess
@@ -43,44 +52,13 @@ class AudioValidatorRoutine(BaseValidatorRoutine, PluginDevicesMixin):
43
52
 
44
53
  exp.requirePsychopyLibs(["validation"])
45
54
 
46
- # --- Basic ---
47
- self.order += [
48
- "threshold",
49
- ]
50
- self.params['threshold'] = Param(
51
- threshold, valType="code", inputType="single", categ="Basic",
52
- label=_translate("Threshold"),
53
- hint=_translate(
54
- "Arbitrary volume threshold at which the sound sensor should register a positive, units go from 0 (least volume) to 1 (most volume)."
55
- )
56
- )
57
55
  del self.params['stopType']
58
56
  del self.params['stopVal']
59
57
 
60
58
  # --- Device ---
61
59
  self.order += [
62
- "deviceLabel",
63
- "deviceBackend",
64
60
  "channel",
65
61
  ]
66
- self.params['deviceLabel'] = Param(
67
- deviceLabel, valType="str", inputType="single", categ="Device",
68
- label=_translate("Device name"),
69
- hint=_translate(
70
- "A name to refer to this Component's associated hardware device by. If using the "
71
- "same device for multiple components, be sure to use the same name here."
72
- )
73
- )
74
- self.params['deviceBackend'] = Param(
75
- deviceBackend, valType="code", inputType="choice", categ="Device",
76
- allowedVals=self.getBackendKeys,
77
- allowedLabels=self.getBackendLabels,
78
- label=_translate("Sound sensor type"),
79
- hint=_translate(
80
- "Type of sound sensor to use."
81
- ),
82
- direct=False
83
- )
84
62
  self.params['channel'] = Param(
85
63
  channel, valType="code", inputType="single", categ="Device",
86
64
  label=_translate("Sound sensor channel"),
@@ -91,47 +69,14 @@ class AudioValidatorRoutine(BaseValidatorRoutine, PluginDevicesMixin):
91
69
  )
92
70
  )
93
71
 
94
- self.loadBackends()
95
-
96
- def writeDeviceCode(self, buff):
97
- """
98
- Code to setup the CameraDevice for this component.
99
-
100
- Parameters
101
- ----------
102
- buff : io.StringIO
103
- Text buffer to write code to.
104
- """
105
- # do usual backend-specific device code writing
106
- PluginDevicesMixin.writeDeviceCode(self, buff)
107
- # get inits
108
- inits = getInitVals(self.params)
109
- # get device handle
110
- code = (
111
- "%(deviceLabelCode)s = deviceManager.getDevice(%(deviceLabel)s)\n"
112
- "%(deviceLabelCode)s.setThreshold(%(threshold)s, channel=%(channel)s)\n"
113
- )
114
- buff.writeOnceIndentedLines(code % inits)
115
-
116
72
  def writeMainCode(self, buff):
117
73
  inits = getInitVals(self.params)
118
- # get diode
119
- code = (
120
- "# diode object for %(name)s\n"
121
- "%(name)sDevice = deviceManager.getDevice(%(deviceLabel)s)\n"
122
- )
123
- buff.writeIndentedLines(code % inits)
124
-
125
- if self.params['threshold']:
126
- code = (
127
- "%(name)sDevice.setThreshold(%(threshold)s, channel=%(channel)s)\n"
128
- )
129
- buff.writeIndentedLines(code % inits)
130
74
  # create validator object
131
75
  code = (
132
76
  "# validator object for %(name)s\n"
133
77
  "%(name)s = validation.AudioValidator(\n"
134
- " %(name)sDevice, %(channel)s,\n"
78
+ " deviceManager.getDevice(%(deviceLabel)s), \n"
79
+ " %(channel)s,\n"
135
80
  ")\n"
136
81
  )
137
82
  buff.writeIndentedLines(code % inits)
@@ -258,86 +203,5 @@ class AudioValidatorRoutine(BaseValidatorRoutine, PluginDevicesMixin):
258
203
  return stims
259
204
 
260
205
 
261
- class MicrophoneSoundSensorValidatorBackend(DeviceBackend):
262
- """
263
- Adds a microphone sound sensor emulation backend for AudioValidator, as well as acting as an
264
- example for implementing other sound sensor device backends.
265
- """
266
-
267
- key = "microphone"
268
- label = _translate("Microphone")
269
- component = AudioValidatorRoutine
270
- deviceClasses = ["psychopy.hardware.soundsensor.MicrophoneSoundSensor"]
271
-
272
- def getParams(self: AudioValidatorRoutine):
273
- # define order
274
- order = [
275
- 'microphone',
276
- 'dbRange',
277
- 'samplingWindow'
278
- ]
279
- # define params
280
- params = {}
281
- def getDeviceIndices():
282
- from psychopy.hardware.microphone import MicrophoneDevice
283
- profiles = MicrophoneDevice.getAvailableDevices()
284
-
285
- return [None] + [profile['index'] for profile in profiles]
286
-
287
- def getDeviceNames():
288
- from psychopy.hardware.microphone import MicrophoneDevice
289
- profiles = MicrophoneDevice.getAvailableDevices()
290
-
291
- return ["default"] + [profile['deviceName'] for profile in profiles]
292
-
293
- params['microphone'] = Param(
294
- None, valType='str', inputType="choice", categ="Device",
295
- allowedVals=getDeviceIndices,
296
- allowedLabels=getDeviceNames,
297
- label=_translate("Microphone"),
298
- hint=_translate(
299
- "What microphone device to use?"
300
- )
301
- )
302
- params['dbRange'] = Param(
303
- (0, 1), valType="list", inputType="single", categ="Device",
304
- label=_translate("Decibel range"),
305
- hint=_translate(
306
- "Range of possible decibels to expect mic responses to be in, by default (0, 1)"
307
- )
308
- )
309
- params['samplingWindow'] = Param(
310
- 0.03, valType="code", inputType="single", categ="Device",
311
- label=_translate("Sampling window"),
312
- hint=_translate(
313
- "How long (s) to average samples from the microphone across? Larger sampling "
314
- "windows reduce the chance of random spikes, but also reduce sensitivity."
315
- )
316
- )
317
-
318
- return params, order
319
-
320
- def addRequirements(self):
321
- # needs microphone
322
- self.exp.requireImport(
323
- importName="MicrophoneDevice",
324
- importFrom="psychopy.hardware.microphone"
325
- )
326
-
327
- def writeDeviceCode(self: AudioValidatorRoutine, buff):
328
- # get inits
329
- inits = getInitVals(self.params)
330
- # make MicrophoneVoiceKey object
331
- code = (
332
- "%(name)sDevice = MicrophoneDevice(\n"
333
- " index=%(microphone)s\n"
334
- ")\n"
335
- "deviceManager.addDevice(\n"
336
- " deviceClass='psychopy.hardware.soundsensor.MicrophoneSoundSensor',\n"
337
- " deviceName=%(deviceLabel)s,\n"
338
- " device=%(name)sDevice, \n"
339
- " dbRange=%(dbRange)s, \n"
340
- " samplingWindow=%(samplingWindow)s, \n"
341
- ")\n"
342
- )
343
- buff.writeOnceIndentedLines(code % inits)
206
+ from ...components.soundsensor import MicrophoneSoundSensorBackend
207
+ AudioValidatorRoutine.registerBackend(MicrophoneSoundSensorBackend)