psychopy 2025.1.0__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 (226) 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/colors.py +10 -5
  113. psychopy/data/experiment.py +133 -23
  114. psychopy/data/routine.py +12 -0
  115. psychopy/data/staircase.py +42 -20
  116. psychopy/data/trial.py +20 -12
  117. psychopy/data/utils.py +43 -3
  118. psychopy/demos/builder/Experiments/dragAndDrop/drag_and_drop.psyexp +22 -5
  119. psychopy/demos/builder/Experiments/dragAndDrop/stimuli/solutions.xlsx +0 -0
  120. psychopy/demos/builder/Experiments/stroopVoice/stroopVoice.psyexp +2 -12
  121. psychopy/demos/builder/Feature Demos/buttonBox/buttonBoxDemo.psyexp +3 -8
  122. psychopy/demos/builder/Feature Demos/movies/movie.psyexp +220 -0
  123. psychopy/demos/builder/Feature Demos/movies/readme.md +3 -0
  124. psychopy/demos/builder/Feature Demos/visualValidator/visualValidator.psyexp +1 -2
  125. psychopy/demos/builder/Hardware/camera/camera.psyexp +3 -16
  126. psychopy/demos/builder/Hardware/microphone/microphone.psyexp +3 -16
  127. psychopy/demos/coder/hardware/hdf5_extract.py +133 -0
  128. psychopy/event.py +20 -15
  129. psychopy/experiment/_experiment.py +86 -10
  130. psychopy/experiment/components/__init__.py +3 -10
  131. psychopy/experiment/components/_base.py +9 -20
  132. psychopy/experiment/components/button/__init__.py +1 -1
  133. psychopy/experiment/components/buttonBox/__init__.py +50 -54
  134. psychopy/experiment/components/camera/__init__.py +137 -359
  135. psychopy/experiment/components/keyboard/__init__.py +17 -24
  136. psychopy/experiment/components/microphone/__init__.py +61 -110
  137. psychopy/experiment/components/movie/__init__.py +2 -3
  138. psychopy/experiment/components/serialOut/__init__.py +192 -93
  139. psychopy/experiment/components/settings/__init__.py +45 -27
  140. psychopy/experiment/components/sound/__init__.py +82 -73
  141. psychopy/experiment/components/soundsensor/__init__.py +43 -80
  142. psychopy/experiment/devices.py +303 -0
  143. psychopy/experiment/exports.py +20 -18
  144. psychopy/experiment/flow.py +7 -0
  145. psychopy/experiment/loops.py +47 -29
  146. psychopy/experiment/monitor.py +74 -0
  147. psychopy/experiment/params.py +48 -10
  148. psychopy/experiment/plugins.py +28 -108
  149. psychopy/experiment/py2js_transpiler.py +1 -1
  150. psychopy/experiment/routines/__init__.py +1 -1
  151. psychopy/experiment/routines/_base.py +59 -24
  152. psychopy/experiment/routines/audioValidator/__init__.py +19 -155
  153. psychopy/experiment/routines/visualValidator/__init__.py +25 -25
  154. psychopy/hardware/__init__.py +20 -57
  155. psychopy/hardware/button.py +15 -2
  156. psychopy/hardware/camera/__init__.py +2237 -1394
  157. psychopy/hardware/joystick/__init__.py +1 -1
  158. psychopy/hardware/keyboard.py +5 -8
  159. psychopy/hardware/listener.py +4 -1
  160. psychopy/hardware/manager.py +75 -35
  161. psychopy/hardware/microphone.py +53 -7
  162. psychopy/hardware/monitor.py +144 -0
  163. psychopy/hardware/photometer/__init__.py +156 -117
  164. psychopy/hardware/serialdevice.py +16 -2
  165. psychopy/hardware/soundsensor.py +4 -1
  166. psychopy/iohub/devices/deviceConfigValidation.py +2 -1
  167. psychopy/iohub/devices/eyetracker/hw/gazepoint/__init__.py +2 -2
  168. psychopy/iohub/devices/eyetracker/hw/gazepoint/gp3/__init__.py +1 -0
  169. psychopy/iohub/devices/eyetracker/hw/gazepoint/gp3/eyetracker.py +10 -0
  170. psychopy/iohub/devices/keyboard/darwin.py +8 -5
  171. psychopy/iohub/util/__init__.py +7 -8
  172. psychopy/localization/generateTranslationTemplate.py +208 -116
  173. psychopy/localization/messages.pot +4305 -3502
  174. psychopy/monitors/MonitorCenter.py +174 -74
  175. psychopy/plugins/__init__.py +6 -4
  176. psychopy/preferences/devices.py +80 -0
  177. psychopy/preferences/generateHints.py +2 -1
  178. psychopy/preferences/preferences.py +35 -11
  179. psychopy/scripts/psychopy-pkgutil.py +969 -0
  180. psychopy/scripts/psyexpCompile.py +1 -1
  181. psychopy/session.py +34 -38
  182. psychopy/sound/__init__.py +6 -260
  183. psychopy/sound/audioclip.py +164 -0
  184. psychopy/sound/backend_ptb.py +8 -0
  185. psychopy/sound/backend_pygame.py +10 -0
  186. psychopy/sound/backend_pysound.py +9 -0
  187. psychopy/sound/backends/__init__.py +0 -0
  188. psychopy/sound/microphone.py +3 -0
  189. psychopy/sound/sound.py +58 -0
  190. psychopy/tests/data/correctScript/python/correctNoiseStimComponent.py +1 -1
  191. psychopy/tests/data/duplicateHeaders.csv +2 -0
  192. psychopy/tests/test_app/test_builder/test_BuilderFrame.py +22 -7
  193. psychopy/tests/test_app/test_builder/test_CompileFromBuilder.py +0 -2
  194. psychopy/tests/test_data/test_utils.py +5 -1
  195. psychopy/tests/test_experiment/test_components/test_ButtonBoxComponent.py +22 -2
  196. psychopy/tests/test_hardware/test_ports.py +0 -12
  197. psychopy/tests/test_tools/test_stringtools.py +1 -1
  198. psychopy/tools/attributetools.py +12 -5
  199. psychopy/tools/fontmanager.py +17 -14
  200. psychopy/tools/gltools.py +4 -2
  201. psychopy/tools/movietools.py +43 -2
  202. psychopy/tools/stringtools.py +33 -8
  203. psychopy/tools/versionchooser.py +1 -1
  204. psychopy/validation/audio.py +5 -1
  205. psychopy/validation/visual.py +5 -1
  206. psychopy/visual/basevisual.py +8 -7
  207. psychopy/visual/circle.py +2 -2
  208. psychopy/visual/helpers.py +3 -1
  209. psychopy/visual/image.py +29 -109
  210. psychopy/visual/movies/__init__.py +1800 -313
  211. psychopy/visual/polygon.py +4 -0
  212. psychopy/visual/shape.py +2 -2
  213. psychopy/visual/window.py +35 -12
  214. psychopy/voicekey/__init__.py +41 -669
  215. psychopy/voicekey/labjack_vks.py +7 -48
  216. psychopy/voicekey/parallel_vks.py +7 -42
  217. psychopy/voicekey/vk_tools.py +114 -263
  218. {psychopy-2025.1.0.dist-info → psychopy-2025.2.1.dist-info}/METADATA +20 -13
  219. {psychopy-2025.1.0.dist-info → psychopy-2025.2.1.dist-info}/RECORD +222 -190
  220. {psychopy-2025.1.0.dist-info → psychopy-2025.2.1.dist-info}/WHEEL +1 -1
  221. psychopy/visual/movies/players/__init__.py +0 -62
  222. psychopy/visual/movies/players/ffpyplayer_player.py +0 -1401
  223. psychopy/voicekey/demo_vks.py +0 -12
  224. psychopy/voicekey/signal.py +0 -42
  225. {psychopy-2025.1.0.dist-info → psychopy-2025.2.1.dist-info}/entry_points.txt +0 -0
  226. {psychopy-2025.1.0.dist-info → psychopy-2025.2.1.dist-info}/licenses/LICENSE +0 -0
@@ -1189,7 +1189,7 @@ def getAllJoysticks():
1189
1189
  joy = Joystick(joysticks[0]['index'])
1190
1190
 
1191
1191
  """
1192
- return Joystick.getAllJoysticks()
1192
+ return Joystick.getAvailableDevices()
1193
1193
 
1194
1194
 
1195
1195
  if __name__ == "__main__":
@@ -456,14 +456,11 @@ class KeyboardDevice(BaseResponseDevice, aliases=["keyboard"]):
456
456
 
457
457
  @staticmethod
458
458
  def getAvailableDevices():
459
- devices = []
460
- for profile in st.getKeyboards():
461
- devices.append({
462
- 'deviceName': profile.get('device_name', "Unknown Keyboard"),
463
- 'device': profile.get('index', -1),
464
- 'bufferSize': profile.get('bufferSize', 10000),
465
- })
466
- return devices
459
+ return [{
460
+ 'deviceName': "Keyboard",
461
+ 'device': -1,
462
+ 'bufferSize': 10000
463
+ }]
467
464
 
468
465
  def getKeys(self, keyList=None, ignoreKeys=None, waitRelease=True, clear=True):
469
466
  """
@@ -274,7 +274,7 @@ class LoggingListener(BaseListener):
274
274
  level : int
275
275
  Logging level to log messages as, can be one of the constants from psychopy.logging. Default is logging.DEBUG.
276
276
  """
277
- def __init__(self, file=logging.root, level=logging.DEBUG):
277
+ def __init__(self, file=logging.console, level=logging.DEBUG):
278
278
  # init base class
279
279
  BaseListener.__init__(self)
280
280
  # store params
@@ -287,7 +287,10 @@ class LoggingListener(BaseListener):
287
287
  """
288
288
  # append
289
289
  self.responses.append(message)
290
+ # log
290
291
  self.file.logger.log(message, level=self.level)
292
+ # flush log
293
+ logging.flush()
291
294
 
292
295
 
293
296
  class LiaisonListener(BaseListener):
@@ -152,6 +152,9 @@ class DeviceManager:
152
152
  if deviceClass in (None, "*"):
153
153
  # resolve "any" flags to BaseDevice
154
154
  deviceClass = "psychopy.hardware.base.BaseDevice"
155
+ # if it's already a type, return as is
156
+ if isinstance(deviceClass, type):
157
+ return deviceClass
155
158
  # get package and class names from deviceClass string
156
159
  parts = deviceClass.split(".")
157
160
  pkgName = ".".join(parts[:-1])
@@ -434,6 +437,52 @@ class DeviceManager:
434
437
  del DeviceManager.devices[deviceName]
435
438
 
436
439
  return True
440
+
441
+ @staticmethod
442
+ def resolveDevice(device, deviceClass):
443
+ """
444
+ Resolve a value to a device, handling strings and using None to find/create a default
445
+ device.
446
+
447
+ Parameters
448
+ ----------
449
+ device : any
450
+ Value to resolve
451
+ deviceClass : type or str
452
+ Class which the returned device should be an instance of (this can be a base class)
453
+ """
454
+ # resolve class
455
+ deviceClass = DeviceManager._resolveAlias(deviceClass)
456
+ # resolve device
457
+ if isinstance(device, deviceClass):
458
+ # if given a device directly, return it as is
459
+ return device
460
+ elif isinstance(device, str) and DeviceManager.hasDevice(device):
461
+ # if given the name of a managed device, get it
462
+ return DeviceManager.getDevice(device)
463
+ elif device is None:
464
+ # first try looking for extant devices
465
+ for cls in deviceClass.__subclasses__():
466
+ # add the first initialised device, if any
467
+ for thisDevice in DeviceManager.getInitialisedDevices(cls).values():
468
+ return thisDevice
469
+ # if that fails, make one
470
+ for cls in deviceClass.__subclasses__():
471
+ # create a device from the first available profile
472
+ for profile in cls.getAvailableDevices():
473
+ return cls(**{
474
+ key: val for key, val in profile.items() if key not in ("deviceName", "deviceClass")
475
+ })
476
+ # error if there are no available devices
477
+ raise DeviceNotConnectedError(
478
+ f"Could not find or create a default device for {deviceClass.__name__} as no "
479
+ f"devices are connected."
480
+ )
481
+ else:
482
+ raise ValueError(
483
+ f"Could not get device from '{device}', value is neither a {deviceClass.__name__} "
484
+ f"object or the name of one in DeviceManager."
485
+ )
437
486
 
438
487
  @staticmethod
439
488
  def getDevice(deviceName):
@@ -500,48 +549,15 @@ class DeviceManager:
500
549
  """
501
550
  from psychopy import experiment
502
551
 
503
- # dict in which to store usages
504
552
  usages = {}
505
553
 
506
- def _process(emt):
507
- """
508
- Process an element (Component or Routine) for device names and append them to the
509
- usages dict.
510
-
511
- Parameters
512
- ----------
513
- emt : Component or Routine
514
- Element to process
515
- """
516
- # if we have a device name for this element...
517
- if "deviceLabel" in emt.params:
518
- # get init value so it lines up with boilerplate code
519
- inits = experiment.getInitVals(emt.params)
520
- # get value
521
- deviceName = inits['deviceLabel'].val
522
- # make sure device name is in usages dict
523
- if deviceName not in usages:
524
- usages[deviceName] = []
525
- # add any new usages
526
- for cls in getattr(emt, "deviceClasses", []):
527
- if cls not in usages[deviceName]:
528
- usages[deviceName].append(cls)
529
-
530
554
  # process each experiment
531
555
  for file in experiments:
532
556
  # create experiment object
533
557
  exp = experiment.Experiment()
534
558
  exp.loadFromXML(file)
535
-
536
- # iterate through routines
537
- for rt in exp.routines.values():
538
- if isinstance(rt, experiment.routines.BaseStandaloneRoutine):
539
- # for standalone routines, get device names from params
540
- _process(rt)
541
- else:
542
- # for regular routines, get device names from each component
543
- for comp in rt:
544
- _process(comp)
559
+ # get info
560
+ usages.update(exp.getRequiredDeviceNames())
545
561
 
546
562
  return usages
547
563
 
@@ -641,6 +657,7 @@ class DeviceManager:
641
657
  """
642
658
  # if deviceClass is *, call for all types
643
659
  if deviceClass == "*":
660
+ DeviceManager.importAllComponentDeviceClasses()
644
661
  deviceClass = DeviceManager.deviceClasses
645
662
  # if given multiple types, call for each
646
663
  if isinstance(deviceClass, (list, tuple)):
@@ -782,6 +799,29 @@ class DeviceManager:
782
799
  device.clearListeners()
783
800
 
784
801
  return True
802
+
803
+ @staticmethod
804
+ def importAllComponentDeviceClasses():
805
+ """
806
+ For all known Components, import the relevant device classes so they appear in
807
+ DeviceManager.deviceClasses
808
+ """
809
+ from psychopy.experiment import getAllElements
810
+
811
+ # iterate through all detectable elements
812
+ for emt in getAllElements().values():
813
+ # if possible, get relevant device classes
814
+ if hasattr(emt, "backends"):
815
+ for cls in emt.backends:
816
+ if hasattr(cls, "deviceClass"):
817
+ # import it so we can detect it
818
+ try:
819
+ DeviceManager._resolveClassString(cls.deviceClass)
820
+ except:
821
+ logging.warn(
822
+ f"Failed to load class {cls.deviceClass} from specification in "
823
+ f"{cls.__name__} ({emt.__name__})"
824
+ )
785
825
 
786
826
  @staticmethod
787
827
  def getResponseParams(deviceClass="*"):
@@ -89,7 +89,7 @@ class MicrophoneDevice(BaseDevice, aliases=["mic", "microphone"]):
89
89
  Capture 10 seconds of audio from the primary microphone::
90
90
 
91
91
  import psychopy.core as core
92
- import psychopy.sound.Microphone as Microphone
92
+ import psychopy.sound.microphone.Microphone as Microphone
93
93
 
94
94
  mic = Microphone(bufferSecs=10.0) # open the microphone
95
95
  mic.start() # start recording
@@ -275,6 +275,9 @@ class MicrophoneDevice(BaseDevice, aliases=["mic", "microphone"]):
275
275
  # recording buffer information
276
276
  self._recording = [] # use a list
277
277
  self._totalSamples = 0
278
+ self._absRecStartTime = self._absRecStopTime = -1.0
279
+ self._recPositionSecs = 0.0
280
+
278
281
  self._maxRecordingSize = (
279
282
  -1 if maxRecordingSize is None else int(maxRecordingSize))
280
283
  self._policyWhenFull = policyWhenFull
@@ -463,6 +466,7 @@ class MicrophoneDevice(BaseDevice, aliases=["mic", "microphone"]):
463
466
  index = profile.get('index', None)
464
467
  device = {
465
468
  'deviceName': profile.get('device_name', "Unknown Microphone"),
469
+ 'deviceClass': "psychopy.hardware.microphone.MicrophoneDevice",
466
470
  'index': index,
467
471
  'sampleRateHz': profile.get('defaultSampleRate', None),
468
472
  'channels': profile.get('inputChannels', None),
@@ -513,6 +517,16 @@ class MicrophoneDevice(BaseDevice, aliases=["mic", "microphone"]):
513
517
  def recBufferSecs(self):
514
518
  """Capacity of the recording buffer in seconds (`float`)."""
515
519
  return self._totalSamples / float(self._sampleRateHz)
520
+
521
+ @property
522
+ def recSampleCount(self):
523
+ """Total number of samples in the recording buffer (`int`).
524
+
525
+ This is the total number of samples that have been recorded since the
526
+ last `start` call. If the stream is not started, this will return `0`.
527
+
528
+ """
529
+ return self._totalSamples
516
530
 
517
531
  @property
518
532
  def latencyBias(self):
@@ -642,6 +656,16 @@ class MicrophoneDevice(BaseDevice, aliases=["mic", "microphone"]):
642
656
  logging.debug(f"Microphone test failed. Error: {err}")
643
657
 
644
658
  raise err
659
+
660
+ @property
661
+ def recordingTime(self):
662
+ """Current position in the recording buffer in seconds (`float`).
663
+
664
+ This is the position of the next sample to be written to the recording
665
+ buffer. If the stream is not started, this will return `0.0`.
666
+
667
+ """
668
+ return self._recPositionSecs
645
669
 
646
670
  def start(self, when=None, waitForStart=0, stopTime=None):
647
671
  """Start an audio recording.
@@ -679,11 +703,12 @@ class MicrophoneDevice(BaseDevice, aliases=["mic", "microphone"]):
679
703
  # reset the recording buffer
680
704
  self._recording = []
681
705
  self._totalSamples = 0
706
+ self._recPositionSecs = 0.0
682
707
 
683
708
  # reset warnings
684
709
  # self._warnedRecBufferFull = False
685
710
 
686
- startTime = self._stream.start(
711
+ self._absRecStartTime = self._stream.start(
687
712
  repetitions=0,
688
713
  when=when,
689
714
  wait_for_start=int(waitForStart),
@@ -694,9 +719,9 @@ class MicrophoneDevice(BaseDevice, aliases=["mic", "microphone"]):
694
719
 
695
720
  logging.debug(
696
721
  'Scheduled start of audio capture for device #{} at t={}.'.format(
697
- self._device.deviceIndex, startTime))
722
+ self._device.deviceIndex, self._absRecStartTime))
698
723
 
699
- return startTime
724
+ return self._absRecStartTime
700
725
 
701
726
  def record(self, when=None, waitForStart=0, stopTime=None):
702
727
  """Start an audio recording (alias of `.start()`).
@@ -757,7 +782,7 @@ class MicrophoneDevice(BaseDevice, aliases=["mic", "microphone"]):
757
782
  return
758
783
 
759
784
  # poll remaining samples, if any
760
- self.poll()
785
+ _ = self.poll()
761
786
 
762
787
  startTime, endPositionSecs, xruns, estStopTime = self._stream.stop(
763
788
  block_until_stopped=int(blockUntilStopped),
@@ -893,6 +918,16 @@ class MicrophoneDevice(BaseDevice, aliases=["mic", "microphone"]):
893
918
  if self._maxRecordingSize < 0:
894
919
  return False
895
920
  return self._totalSamples >= self._maxRecordingSize
921
+
922
+ @property
923
+ def recStartTime(self):
924
+ """Absolute time when the recording started (`float`).
925
+
926
+ This is the time when the first sample was recorded. If the recording
927
+ has not started, this will be `-1.0`.
928
+
929
+ """
930
+ return self._absRecStartTime
896
931
 
897
932
  def poll(self):
898
933
  """Poll audio samples.
@@ -909,7 +944,14 @@ class MicrophoneDevice(BaseDevice, aliases=["mic", "microphone"]):
909
944
  Returns
910
945
  -------
911
946
  int
912
- Number of overruns in sampling.
947
+ Current recording position in samples (`int`) and number of
948
+ overflows (`int`). If the returned position is less than zero
949
+ (negative) then microphone is still starting up and wont be ready
950
+ until the position is greater than or equal to zero. If overflow
951
+ occurs, this means that the recording buffer is full and no more
952
+ samples can be added until polled. To prevent this, ensure that
953
+ `poll()` is called often enough or increase the size of the audio
954
+ buffer with `bufferSecs`.
913
955
 
914
956
  """
915
957
  if not self.isStarted:
@@ -975,8 +1017,12 @@ class MicrophoneDevice(BaseDevice, aliases=["mic", "microphone"]):
975
1017
  elif self._policyWhenFull == 'error':
976
1018
  raise AudioStreamError(
977
1019
  "Recording buffer is full, no more samples will be added.")
1020
+
1021
+ # update the recording position
1022
+ self._absRecStopTime = absRecPosition / self._sampleRateHz
1023
+ self._recPositionSecs = self._absRecStopTime - self._absRecStartTime
978
1024
 
979
- return 0
1025
+ return absRecPosition, overflow
980
1026
 
981
1027
  def _mergeAudioFragments(self):
982
1028
  """Merge audio fragments into a single segment.
@@ -0,0 +1,144 @@
1
+ from psychopy.monitors import Monitor, DACrange, GammaCalculator
2
+ from psychopy.hardware import DeviceManager, keyboard
3
+ import numpy as np
4
+ import time
5
+
6
+ __all__ = [
7
+ "Monitor",
8
+ "calibrateGamma"
9
+ ]
10
+
11
+
12
+ def calibrateGamma(
13
+ win,
14
+ photometer,
15
+ patchSize=0.3,
16
+ nPoints=8
17
+ ):
18
+ """
19
+ Use a photometer to calibrate the gamma for this monitor.
20
+
21
+ Parameters
22
+ ----------
23
+ win : psychopy.visual.Window
24
+ Window to run calibration in
25
+ photometer : str
26
+ Name of a photometer setup already in device manager
27
+ patchSize : float, optional
28
+ Size of the calibration patch as a proportion of the screen size (0-1), by default 0.3
29
+ nPoints : int, optional
30
+ Number of calibration points to use, by default 8
31
+ """
32
+ from psychopy import visual
33
+ from psychopy.hardware.photometer import BasePhotometerDevice
34
+
35
+ # get photometer device (if not given one)
36
+ if isinstance(photometer, BasePhotometerDevice):
37
+ phot = photometer
38
+ else:
39
+ phot = DeviceManager.getDevice(photometer)
40
+ # error if there isn't one
41
+ if phot is None:
42
+ raise ConnectionError(
43
+ "No photometer found. Try setting one up in the device manager."
44
+ )
45
+ # make sure nPoints is an integer
46
+ nPoints = int(nPoints)
47
+ # create a patch
48
+ patch = visual.GratingStim(
49
+ win,
50
+ tex="sqr",
51
+ size=(patchSize*2, patchSize*2),
52
+ units="norm",
53
+ rgb=(255, 255, 255),
54
+ colorSpace="rgb255",
55
+ )
56
+ # create instructions
57
+ instr = visual.TextBox2(
58
+ win,
59
+ text=(
60
+ "Point the photometer at the central box and press SPACE (or wait 2s) to "
61
+ "take a reading. Press ESCAPE to cancel."
62
+ ),
63
+ size=(2, 0.5),
64
+ pos=(0, 1),
65
+ padding=0.05,
66
+ anchor="top center",
67
+ alignment="top center",
68
+ letterHeight=0.05
69
+ )
70
+ # create progress indicator
71
+ prog = visual.TextBox2(
72
+ win,
73
+ text="Waiting for keypress...",
74
+ size=(1, 0.2),
75
+ pos=(-1, -1),
76
+ padding=0.05,
77
+ anchor="bottom left",
78
+ alignment="bottom left",
79
+ letterHeight=0.05
80
+ )
81
+ # do initial draw
82
+ win.flip()
83
+ patch.draw()
84
+ instr.draw()
85
+ prog.draw()
86
+ win.flip()
87
+ # this will hold the measured luminance values
88
+ lumSeries = np.zeros((4, nPoints), 'd')
89
+ # listen for keypress or 30s
90
+ kb = keyboard.Keyboard()
91
+ keys = kb.waitKeys(keyList=["escape", "space"], maxWait=30)
92
+ # abort if requested
93
+ if keys and "escape" in keys:
94
+ return
95
+ # clear instructions & update current action once responded
96
+ instr.text = ""
97
+ # iterate through levels
98
+ for lvl, dac in enumerate(DACrange(nPoints)):
99
+ # get relevant guns
100
+ if lvl == 0:
101
+ # guns are irrelevant when intensity is 0
102
+ guns = [None]
103
+ else:
104
+ guns = range(4)
105
+ # iterate through guns per level
106
+ for gun in guns:
107
+ # update progress indicator
108
+ prog.text = f"Level {lvl+1}/{nPoints} Gun {gun+1 if gun is not None else 'NA'}/4"
109
+ # set the patch color
110
+ if gun in (0, None):
111
+ # if gun is 0 (aka luminance), set as flat
112
+ patch.color = dac
113
+ else:
114
+ # otherwise, set just the relevant gun and leave the rest black
115
+ patch.color = [
116
+ dac if i == gun-1 else -1
117
+ for i in range(3)
118
+ ]
119
+ # draw all & flip
120
+ patch.draw()
121
+ prog.draw()
122
+ win.flip()
123
+ # allow the screen to settle
124
+ time.sleep(0.2)
125
+ # take reading
126
+ lum = phot.getLum()
127
+ # if no gun, set for all
128
+ if gun is None:
129
+ for thisGun in range(4):
130
+ lumSeries[thisGun, lvl] = lum
131
+ else:
132
+ lumSeries[gun, lvl] = lum
133
+ # transform lum series to a gamma grid
134
+ gammaGrid = []
135
+ for lumRow in lumSeries:
136
+ calc = GammaCalculator(
137
+ inputs=DACrange(nPoints),
138
+ lums=lumRow
139
+ )
140
+ gammaGrid.append(
141
+ [calc.min, calc.max, calc.gammaModel[0]]
142
+ )
143
+
144
+ return gammaGrid