psychopy 2024.1.4__py3-none-any.whl → 2024.2.0__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 (325) hide show
  1. psychopy/.DS_Store +0 -0
  2. psychopy/CHANGELOG.txt +206 -0
  3. psychopy/GIT_SHA +1 -0
  4. psychopy/VERSION +1 -0
  5. psychopy/__init__.py +77 -15
  6. psychopy/app/Resources/classic/plugin16.png +0 -0
  7. psychopy/app/Resources/classic/plugin16@2x.png +0 -0
  8. psychopy/app/Resources/dark/plugin16.png +0 -0
  9. psychopy/app/Resources/dark/plugin16@2x.png +0 -0
  10. psychopy/app/Resources/light/plugin16.png +0 -0
  11. psychopy/app/Resources/light/plugin16@2x.png +0 -0
  12. psychopy/app/__init__.py +76 -2
  13. psychopy/app/_psychopyApp.py +126 -101
  14. psychopy/app/builder/builder.py +14 -10
  15. psychopy/app/builder/dialogs/__init__.py +8 -8
  16. psychopy/app/builder/dialogs/dlgsConditions.py +12 -13
  17. psychopy/app/builder/dialogs/paramCtrls.py +24 -57
  18. psychopy/app/builder/validators.py +2 -2
  19. psychopy/app/coder/codeEditorBase.py +8 -8
  20. psychopy/app/coder/coder.py +4 -4
  21. psychopy/app/connections/sendusage.py +2 -2
  22. psychopy/app/connections/updates.py +9 -9
  23. psychopy/app/dialogs.py +34 -2
  24. psychopy/app/idle.py +31 -0
  25. psychopy/app/jobs.py +21 -3
  26. psychopy/app/linuxconfig/__init__.py +9 -0
  27. psychopy/app/locale/ar_001/LC_MESSAGE/messages.mo +0 -0
  28. psychopy/app/locale/ar_001/LC_MESSAGE/messages.po +4602 -2540
  29. psychopy/app/locale/es_CO/LC_MESSAGE/messages.mo +0 -0
  30. psychopy/app/locale/es_CO/LC_MESSAGE/messages.po +56 -54
  31. psychopy/app/locale/es_ES/LC_MESSAGE/messages.po +53 -43
  32. psychopy/app/locale/es_US/LC_MESSAGE/messages.mo +0 -0
  33. psychopy/app/locale/es_US/LC_MESSAGE/messages.po +56 -54
  34. psychopy/app/locale/ja_JP/LC_MESSAGE/messages.mo +0 -0
  35. psychopy/app/locale/ja_JP/LC_MESSAGE/messages.po +1011 -942
  36. psychopy/app/locale/pt_PT/LC_MESSAGE/messages.po +9415 -5
  37. psychopy/app/pavlovia_ui/_base.py +33 -3
  38. psychopy/app/pavlovia_ui/search.py +0 -1
  39. psychopy/app/plugin_manager/dialog.py +104 -51
  40. psychopy/app/plugin_manager/packages.py +5 -0
  41. psychopy/app/plugin_manager/plugins.py +145 -67
  42. psychopy/app/preferencesDlg.py +8 -8
  43. psychopy/app/psychopyApp.py +11 -5
  44. psychopy/app/ribbon.py +124 -14
  45. psychopy/app/runner/runner.py +6 -1
  46. psychopy/app/stdout/stdOutRich.py +27 -11
  47. psychopy/app/themes/icons.py +52 -2
  48. psychopy/assets/__init__.py +0 -0
  49. psychopy/assets/click.png +0 -0
  50. psychopy/assets/clicknext.png +0 -0
  51. psychopy/assets/next.png +0 -0
  52. psychopy/assets/psychopy.ico +0 -0
  53. psychopy/assets/psychopy.png +0 -0
  54. psychopy/assets/templates/__init__.py +0 -0
  55. psychopy/assets/touch.png +0 -0
  56. psychopy/assets/touchnext.png +0 -0
  57. psychopy/assets/window.ico +0 -0
  58. psychopy/changes/2023.1.0.md +9 -0
  59. psychopy/changes/2024.1.0.md +16 -0
  60. psychopy/changes/__init__.py +0 -0
  61. psychopy/clock.py +2 -2
  62. psychopy/colors.py +2 -1
  63. psychopy/compatibility.py +53 -1
  64. psychopy/contrib/.DS_Store +0 -0
  65. psychopy/contrib/configobj/__init__.py +10 -8
  66. psychopy/data/__init__.py +3 -2
  67. psychopy/data/base.py +5 -5
  68. psychopy/data/experiment.py +130 -4
  69. psychopy/data/routine.py +56 -0
  70. psychopy/data/staircase.py +2 -2
  71. psychopy/data/trial.py +559 -97
  72. psychopy/data/utils.py +56 -21
  73. psychopy/demos/.DS_Store +0 -0
  74. psychopy/demos/builder/.DS_Store +0 -0
  75. psychopy/demos/builder/Design Templates/.DS_Store +0 -0
  76. psychopy/demos/builder/Experiments/.DS_Store +0 -0
  77. psychopy/demos/builder/Feature Demos/.DS_Store +0 -0
  78. psychopy/demos/builder/Feature Demos/buttonBox/buttonBoxDemo.psyexp +375 -0
  79. psychopy/demos/builder/Feature Demos/buttonBox/readme.md +5 -0
  80. psychopy/demos/builder/Feature Demos/pilotMode/pilotMode.psyexp +433 -0
  81. psychopy/demos/builder/Feature Demos/pilotMode/readme.md +7 -0
  82. psychopy/demos/builder/Hardware/.DS_Store +0 -0
  83. psychopy/demos/builder/Helper Tools/.DS_Store +0 -0
  84. psychopy/demos/coder/.DS_Store +0 -0
  85. psychopy/demos/coder/hardware/testSoundLatency.py +2 -2
  86. psychopy/demos/coder/iohub/.DS_Store +0 -0
  87. psychopy/demos/coder/misc/hdf5_2_csv +33 -0
  88. psychopy/event.py +30 -29
  89. psychopy/experiment/.DS_Store +0 -0
  90. psychopy/experiment/_experiment.py +6 -6
  91. psychopy/experiment/components/.DS_Store +0 -0
  92. psychopy/experiment/components/__init__.py +6 -3
  93. psychopy/experiment/components/_base.py +286 -131
  94. psychopy/experiment/components/aperture/.DS_Store +0 -0
  95. psychopy/experiment/components/brush/.DS_Store +0 -0
  96. psychopy/experiment/components/button/.DS_Store +0 -0
  97. psychopy/experiment/components/button/__init__.py +5 -1
  98. psychopy/experiment/components/buttonBox/.DS_Store +0 -0
  99. psychopy/experiment/components/camera/.DS_Store +0 -0
  100. psychopy/experiment/components/code/.DS_Store +0 -0
  101. psychopy/experiment/components/dots/.DS_Store +0 -0
  102. psychopy/experiment/components/eyetracker_record/.DS_Store +0 -0
  103. psychopy/experiment/components/eyetracker_record/__init__.py +92 -30
  104. psychopy/experiment/components/form/.DS_Store +0 -0
  105. psychopy/experiment/components/form/__init__.py +6 -2
  106. psychopy/experiment/components/grating/.DS_Store +0 -0
  107. psychopy/experiment/components/grating/__init__.py +14 -3
  108. psychopy/experiment/components/image/.DS_Store +0 -0
  109. psychopy/experiment/components/image/__init__.py +14 -3
  110. psychopy/experiment/components/joyButtons/.DS_Store +0 -0
  111. psychopy/experiment/components/joystick/.DS_Store +0 -0
  112. psychopy/experiment/components/keyboard/.DS_Store +0 -0
  113. psychopy/experiment/components/keyboard/__init__.py +22 -10
  114. psychopy/experiment/components/microphone/.DS_Store +0 -0
  115. psychopy/experiment/components/microphone/__init__.py +59 -39
  116. psychopy/experiment/components/mouse/.DS_Store +0 -0
  117. psychopy/experiment/components/mouse/__init__.py +44 -29
  118. psychopy/experiment/components/movie/.DS_Store +0 -0
  119. psychopy/experiment/components/movie/__init__.py +1 -1
  120. psychopy/experiment/components/panorama/.DS_Store +0 -0
  121. psychopy/experiment/components/parallelOut/.DS_Store +0 -0
  122. psychopy/experiment/components/patch/.DS_Store +0 -0
  123. psychopy/experiment/components/polygon/.DS_Store +0 -0
  124. psychopy/experiment/components/polygon/__init__.py +26 -6
  125. psychopy/experiment/components/progress/.DS_Store +0 -0
  126. psychopy/experiment/components/ratingScale/.DS_Store +0 -0
  127. psychopy/experiment/components/resourceManager/.DS_Store +0 -0
  128. psychopy/experiment/components/roi/.DS_Store +0 -0
  129. psychopy/experiment/components/roi/__init__.py +5 -0
  130. psychopy/experiment/components/routineSettings/.DS_Store +0 -0
  131. psychopy/experiment/components/routineSettings/__init__.py +57 -10
  132. psychopy/experiment/components/serialOut/.DS_Store +0 -0
  133. psychopy/experiment/components/settings/.DS_Store +0 -0
  134. psychopy/experiment/components/settings/__init__.py +117 -42
  135. psychopy/experiment/components/slider/.DS_Store +0 -0
  136. psychopy/experiment/components/sound/.DS_Store +0 -0
  137. psychopy/experiment/components/sound/__init__.py +54 -19
  138. psychopy/experiment/components/static/.DS_Store +0 -0
  139. psychopy/experiment/components/static/__init__.py +1 -1
  140. psychopy/experiment/components/text/.DS_Store +0 -0
  141. psychopy/experiment/components/text/__init__.py +28 -3
  142. psychopy/experiment/components/textbox/.DS_Store +0 -0
  143. psychopy/experiment/components/textbox/__init__.py +12 -2
  144. psychopy/experiment/components/unknown/.DS_Store +0 -0
  145. psychopy/experiment/components/unknown/__init__.py +1 -2
  146. psychopy/experiment/components/unknownPlugin/.DS_Store +0 -0
  147. psychopy/experiment/components/unknownPlugin/__init__.py +2 -2
  148. psychopy/experiment/components/variable/.DS_Store +0 -0
  149. psychopy/experiment/flow.py +11 -4
  150. psychopy/experiment/loops.py +85 -37
  151. psychopy/experiment/params.py +74 -32
  152. psychopy/experiment/py2js_transpiler.py +8 -1
  153. psychopy/experiment/routines/.DS_Store +0 -0
  154. psychopy/experiment/routines/_base.py +102 -22
  155. psychopy/experiment/routines/counterbalance/.DS_Store +0 -0
  156. psychopy/experiment/routines/counterbalance/__init__.py +5 -1
  157. psychopy/experiment/routines/eyetracker_calibrate/.DS_Store +0 -0
  158. psychopy/experiment/routines/eyetracker_validate/.DS_Store +0 -0
  159. psychopy/experiment/routines/pavlovia_survey/.DS_Store +0 -0
  160. psychopy/experiment/routines/photodiodeValidator/.DS_Store +0 -0
  161. psychopy/experiment/routines/photodiodeValidator/__init__.py +6 -5
  162. psychopy/experiment/routines/unknown/.DS_Store +0 -0
  163. psychopy/gui/wxgui.py +4 -4
  164. psychopy/hardware/.DS_Store +0 -0
  165. psychopy/hardware/__init__.py +1 -1
  166. psychopy/hardware/base.py +12 -0
  167. psychopy/hardware/camera/__init__.py +1 -15
  168. psychopy/hardware/cedrus.py +10 -11
  169. psychopy/hardware/crs/colorcal.py +13 -22
  170. psychopy/hardware/crs/optical.py +10 -20
  171. psychopy/hardware/emulator.py +17 -14
  172. psychopy/hardware/eyetracker.py +42 -118
  173. psychopy/hardware/gammasci.py +4 -15
  174. psychopy/hardware/keyboard.py +102 -10
  175. psychopy/hardware/listener.py +3 -0
  176. psychopy/hardware/microphone.py +148 -18
  177. psychopy/hardware/minolta.py +8 -15
  178. psychopy/hardware/photodiode.py +191 -16
  179. psychopy/hardware/photometer/__init__.py +11 -19
  180. psychopy/hardware/pr.py +8 -15
  181. psychopy/hardware/speaker.py +39 -4
  182. psychopy/info.py +0 -71
  183. psychopy/iohub/.DS_Store +0 -0
  184. psychopy/iohub/__init__.py +1 -1
  185. psychopy/iohub/client/__init__.py +30 -20
  186. psychopy/iohub/client/keyboard.py +24 -24
  187. psychopy/iohub/datastore/__init__.py +2 -2
  188. psychopy/iohub/datastore/util.py +2 -2
  189. psychopy/iohub/default_config.yaml +1 -1
  190. psychopy/iohub/devices/.DS_Store +0 -0
  191. psychopy/iohub/devices/__init__.py +112 -25
  192. psychopy/iohub/devices/deviceConfigValidation.py +2 -1
  193. psychopy/iohub/devices/experiment/default_experiment.yaml +12 -1
  194. psychopy/iohub/devices/experiment/supported_config_settings.yaml +5 -1
  195. psychopy/iohub/devices/eyetracker/.DS_Store +0 -0
  196. psychopy/iohub/devices/eyetracker/__init__.py +46 -0
  197. psychopy/iohub/devices/eyetracker/calibration/procedure.py +2 -2
  198. psychopy/iohub/devices/eyetracker/hw/gazepoint/__init__.py +14 -2
  199. psychopy/iohub/devices/eyetracker/hw/mouse/eyetracker.py +3 -4
  200. psychopy/iohub/server.py +2 -2
  201. psychopy/iohub/start_iohub_process.py +3 -0
  202. psychopy/iohub/util/__init__.py +62 -70
  203. psychopy/layout.py +5 -5
  204. psychopy/logging.py +8 -1
  205. psychopy/microphone.py +10 -37
  206. psychopy/platform_specific/__init__.py +0 -2
  207. psychopy/platform_specific/darwin.py +1 -3
  208. psychopy/platform_specific/linux.py +31 -33
  209. psychopy/platform_specific/win32.py +38 -13
  210. psychopy/plugins/__init__.py +148 -116
  211. psychopy/plugins/util.py +39 -0
  212. psychopy/preferences/Darwin.spec +4 -2
  213. psychopy/preferences/FreeBSD.spec +4 -2
  214. psychopy/preferences/Linux.spec +4 -2
  215. psychopy/preferences/Windows.spec +4 -2
  216. psychopy/preferences/baseNoArch.spec +4 -2
  217. psychopy/preferences/preferences.py +47 -24
  218. psychopy/projects/pavlovia.py +47 -4
  219. psychopy/scripts/psyexpCompile.py +0 -4
  220. psychopy/session.py +153 -21
  221. psychopy/sound/__init__.py +31 -21
  222. psychopy/sound/_base.py +20 -3
  223. psychopy/sound/audioclip.py +320 -33
  224. psychopy/sound/backend_ptb.py +47 -58
  225. psychopy/sound/backend_pygame.py +1 -1
  226. psychopy/sound/backend_pysound.py +6 -15
  227. psychopy/sound/transcribe.py +53 -0
  228. psychopy/tests/.DS_Store +0 -0
  229. psychopy/tests/data/.DS_Store +0 -0
  230. psychopy/tests/data/TestUnknownPluginComponent_load_resave.psyexp +135 -0
  231. psychopy/tests/data/Test_textbox/test_ori_0_bottom right.png +0 -0
  232. psychopy/tests/data/Test_textbox/test_ori_0_center.png +0 -0
  233. psychopy/tests/data/Test_textbox/test_ori_0_top left.png +0 -0
  234. psychopy/tests/data/Test_textbox/test_ori_120_bottom right.png +0 -0
  235. psychopy/tests/data/Test_textbox/test_ori_120_center.png +0 -0
  236. psychopy/tests/data/Test_textbox/test_ori_120_top left.png +0 -0
  237. psychopy/tests/data/Test_textbox/test_ori_180_bottom right.png +0 -0
  238. psychopy/tests/data/Test_textbox/test_ori_180_center.png +0 -0
  239. psychopy/tests/data/Test_textbox/test_ori_180_top left.png +0 -0
  240. psychopy/tests/data/Test_textbox/test_ori_240_bottom right.png +0 -0
  241. psychopy/tests/data/Test_textbox/test_ori_240_center.png +0 -0
  242. psychopy/tests/data/Test_textbox/test_ori_240_top left.png +0 -0
  243. psychopy/tests/data/correctScript/.DS_Store +0 -0
  244. psychopy/tests/data/test_components/testClearKeyboard/testClearKeyboard.psyexp +200 -0
  245. psychopy/tests/data/test_session/.DS_Store +0 -0
  246. psychopy/tests/data/test_session/root/testFutureTrials/testFutureTrials.psyexp +155 -0
  247. psychopy/tests/data/test_session/root/testTrialNav/trialNav.psyexp +158 -0
  248. psychopy/tests/test_app/.DS_Store +0 -0
  249. psychopy/tests/test_app/conftest.py +2 -2
  250. psychopy/tests/test_app/test_speed.py +4 -1
  251. psychopy/tests/test_data/test_TrialHandler2.py +146 -1
  252. psychopy/tests/test_experiment/.DS_Store +0 -0
  253. psychopy/tests/test_experiment/needs_wx/genComponsTemplate.py +3 -3
  254. psychopy/tests/test_experiment/needs_wx/test_components.py +2 -2
  255. psychopy/tests/test_experiment/test_components/test_KeyboardComponent.py +28 -0
  256. psychopy/tests/test_experiment/test_components/test_UnknownPluginComponent.py +27 -0
  257. psychopy/tests/test_experiment/test_components/test_base_components.py +58 -0
  258. psychopy/tests/test_experiment/test_py2js.py +1 -1
  259. psychopy/tests/test_hardware/test_keyboard.py +31 -0
  260. psychopy/tests/test_hardware/test_ports.py +1 -11
  261. psychopy/tests/test_liaison/test_Liaison.py +47 -0
  262. psychopy/tests/test_misc/test_core.py +5 -0
  263. psychopy/tests/test_session/test_Session.py +5 -1
  264. psychopy/tests/test_tools/test_versionchooser.py +39 -8
  265. psychopy/tests/test_visual/test_all_stimuli.py +0 -97
  266. psychopy/tests/test_visual/test_image.py +6 -5
  267. psychopy/tests/test_visual/test_textbox.py +36 -0
  268. psychopy/tests/utils.py +4 -0
  269. psychopy/tools/filetools.py +1 -1
  270. psychopy/tools/pkgtools.py +160 -137
  271. psychopy/tools/versionchooser.py +10 -10
  272. psychopy/tools/wizard.py +3 -3
  273. psychopy/visual/.DS_Store +0 -0
  274. psychopy/visual/backends/pygletbackend.py +24 -13
  275. psychopy/visual/basevisual.py +5 -11
  276. psychopy/visual/button.py +2 -14
  277. psychopy/visual/helpers.py +5 -5
  278. psychopy/visual/line.py +1 -2
  279. psychopy/visual/movie2.py +7 -816
  280. psychopy/visual/movie3.py +7 -589
  281. psychopy/visual/movies/__init__.py +8 -11
  282. psychopy/visual/movies/frame.py +5 -2
  283. psychopy/visual/movies/players/ffpyplayer_player.py +5 -2
  284. psychopy/visual/noise.py +8 -7
  285. psychopy/visual/patch.py +7 -16
  286. psychopy/visual/radial.py +9 -7
  287. psychopy/visual/ratingscale.py +8 -1415
  288. psychopy/visual/secondorder.py +10 -9
  289. psychopy/visual/shape.py +7 -2
  290. psychopy/visual/text.py +1 -1
  291. psychopy/visual/textbox2/textbox2.py +28 -5
  292. {psychopy-2024.1.4.dist-info → psychopy-2024.2.0.dist-info}/METADATA +8 -13
  293. {psychopy-2024.1.4.dist-info → psychopy-2024.2.0.dist-info}/RECORD +307 -213
  294. {psychopy-2024.1.4.dist-info → psychopy-2024.2.0.dist-info}/WHEEL +1 -1
  295. psychopy/app/Resources/click.png +0 -0
  296. psychopy/app/Resources/next.png +0 -0
  297. psychopy/experiment/components/patch/__init__.py +0 -121
  298. psychopy/experiment/components/patch/classic/patch.png +0 -0
  299. psychopy/experiment/components/patch/dark/patch.png +0 -0
  300. psychopy/experiment/components/patch/dark/patch@2x.png +0 -0
  301. psychopy/experiment/components/patch/light/patch.png +0 -0
  302. psychopy/experiment/components/patch/light/patch@2x.png +0 -0
  303. psychopy/experiment/components/ratingScale/__init__.py +0 -337
  304. psychopy/experiment/components/ratingScale/classic/ratingscale.png +0 -0
  305. psychopy/experiment/components/ratingScale/classic/ratingscale@2x.png +0 -0
  306. psychopy/experiment/components/ratingScale/dark/ratingScale@2x.png +0 -0
  307. psychopy/experiment/components/ratingScale/dark/ratingscale.png +0 -0
  308. psychopy/experiment/components/ratingScale/light/ratingScale@2x.png +0 -0
  309. psychopy/experiment/components/ratingScale/light/ratingscale.png +0 -0
  310. psychopy/platform_specific/posix.py +0 -16
  311. psychopy/tests/test_sound/test_microphone.py +0 -217
  312. psychopy/tests/test_visual/test_ratingScale.py +0 -299
  313. /psychopy/{app/Resources → assets}/Psychopy Window Favicon@16w.png +0 -0
  314. /psychopy/{app/Resources → assets}/Psychopy Window Favicon@32w.png +0 -0
  315. /psychopy/{app/Resources → assets}/USB-C.png +0 -0
  316. /psychopy/{app/Resources → assets}/USB.png +0 -0
  317. /psychopy/{app/Resources → assets}/creditCard.png +0 -0
  318. /psychopy/{app/Resources → assets}/default.mp3 +0 -0
  319. /psychopy/{app/Resources → assets}/default.mp4 +0 -0
  320. /psychopy/{app/Resources → assets}/default.png +0 -0
  321. /psychopy/{app/Resources → assets/templates}/instruct1.png +0 -0
  322. /psychopy/{app/Resources → assets/templates}/instruct2.png +0 -0
  323. {psychopy-2024.1.4.dist-info → psychopy-2024.2.0.dist-info}/entry_points.txt +0 -0
  324. {psychopy-2024.1.4.dist-info → psychopy-2024.2.0.dist-info}/licenses/AUTHORS.md +0 -0
  325. {psychopy-2024.1.4.dist-info → psychopy-2024.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -6,7 +6,7 @@ from psychtoolbox import audio as audio
6
6
  from psychopy import logging as logging, prefs
7
7
  from psychopy.localization import _translate
8
8
  from psychopy.constants import NOT_STARTED
9
- from psychopy.hardware import BaseDevice
9
+ from psychopy.hardware import BaseDevice, BaseResponse, BaseResponseDevice
10
10
  from psychopy.sound.audiodevice import AudioDeviceInfo, AudioDeviceStatus
11
11
  from psychopy.sound.audioclip import AudioClip
12
12
  from psychopy.sound.exceptions import AudioInvalidCaptureDeviceError, AudioInvalidDeviceError, \
@@ -27,6 +27,10 @@ except (ImportError, ModuleNotFoundError):
27
27
  _hasPTB = False
28
28
 
29
29
 
30
+ class MicrophoneResponse(BaseResponse):
31
+ pass
32
+
33
+
30
34
  class MicrophoneDevice(BaseDevice, aliases=["mic", "microphone"]):
31
35
  """Class for recording audio from a microphone or input stream.
32
36
 
@@ -144,11 +148,42 @@ class MicrophoneDevice(BaseDevice, aliases=["mic", "microphone"]):
144
148
  # if there are none, error
145
149
  if not len(_devices):
146
150
  raise AudioInvalidCaptureDeviceError(_translate(
147
- "Could not choose default recording device as no recording devices are "
148
- "connected."
151
+ "Could not choose default recording device as no recording "
152
+ "devices are connected."
149
153
  ))
150
- # use first device
151
- self._device = _devices[0]
154
+
155
+ # Try and get the best match which are compatible with the user's
156
+ # specified settings.
157
+ if sampleRateHz is not None or channels is not None:
158
+ self._device = self.findBestDevice(
159
+ index=_devices[0].deviceIndex, # use first that shows up
160
+ sampleRateHz=sampleRateHz,
161
+ channels=channels
162
+ )
163
+ else:
164
+ self._device = _devices[0]
165
+
166
+ # Check if the default device settings are differnt than the ones
167
+ # specified by the user, if so, warn them that the default device
168
+ # settings are overwriting their settings.
169
+ if channels is None:
170
+ channels = self._device.inputChannels
171
+ elif channels != self._device.inputChannels:
172
+ logging.warning(
173
+ "Number of channels specified ({}) does not match the "
174
+ "default device's number of input channels ({}).".format(
175
+ channels, self._device.inputChannels))
176
+ channels = self._device.inputChannels
177
+
178
+ if sampleRateHz is None:
179
+ sampleRateHz = self._device.defaultSampleRate
180
+ elif sampleRateHz != self._device.defaultSampleRate:
181
+ logging.warning(
182
+ "Sample rate specified ({}) does not match the default "
183
+ "device's sample rate ({}).".format(
184
+ sampleRateHz, self._device.defaultSampleRate))
185
+ sampleRateHz = self._device.defaultSampleRate
186
+
152
187
  elif isinstance(index, str):
153
188
  # if given a str that's a name from DeviceManager, get info from device
154
189
  device = DeviceManager.getDevice(index)
@@ -167,8 +202,13 @@ class MicrophoneDevice(BaseDevice, aliases=["mic", "microphone"]):
167
202
  channels=channels
168
203
  )
169
204
 
170
- logging.info('Using audio device #{} ({}) for audio capture. Full spec: {}'.format(
171
- self._device.deviceIndex, self._device.deviceName, self._device))
205
+ devInfoText = ('Using audio device #{} ({}) for audio capture. '
206
+ 'Full spec: {}').format(
207
+ self._device.deviceIndex,
208
+ self._device.deviceName,
209
+ self._device)
210
+
211
+ logging.info(devInfoText)
172
212
 
173
213
  # error if specified device is not suitable for capture
174
214
  if not self._device.isCapture:
@@ -176,11 +216,12 @@ class MicrophoneDevice(BaseDevice, aliases=["mic", "microphone"]):
176
216
  'Specified audio device not suitable for audio recording. '
177
217
  'Has no input channels.')
178
218
 
179
- # get the sample rate
180
- self._sampleRateHz = \
181
- self._device.defaultSampleRate if sampleRateHz is None else int(
182
- sampleRateHz)
219
+ # get these values from the configured device
220
+ self._channels = self._device.inputChannels
221
+ logging.debug('Set recording channels to {} ({})'.format(
222
+ self._channels, 'stereo' if self._channels > 1 else 'mono'))
183
223
 
224
+ self._sampleRateHz = self._device.defaultSampleRate
184
225
  logging.debug('Set stream sample rate to {} Hz'.format(
185
226
  self._sampleRateHz))
186
227
 
@@ -195,13 +236,6 @@ class MicrophoneDevice(BaseDevice, aliases=["mic", "microphone"]):
195
236
 
196
237
  assert 0 <= self._audioLatencyMode <= 4 # sanity check for pref
197
238
 
198
- # set the number of recording channels
199
- self._channels = \
200
- self._device.inputChannels if channels is None else int(channels)
201
-
202
- logging.debug('Set recording channels to {} ({})'.format(
203
- self._channels, 'stereo' if self._channels > 1 else 'mono'))
204
-
205
239
  # internal recording buffer size in seconds
206
240
  assert isinstance(streamBufferSecs, (float, int))
207
241
  self._streamBufferSecs = float(streamBufferSecs)
@@ -268,6 +302,9 @@ class MicrophoneDevice(BaseDevice, aliases=["mic", "microphone"]):
268
302
  logging.debug('Audio capture device #{} ready'.format(
269
303
  self._device.deviceIndex))
270
304
 
305
+ # list to store listeners in
306
+ self.listeners = []
307
+
271
308
  def findBestDevice(self, index, sampleRateHz, channels):
272
309
  """
273
310
  Find the closest match among the microphone profiles listed by psychtoolbox as valid.
@@ -797,6 +834,99 @@ class MicrophoneDevice(BaseDevice, aliases=["mic", "microphone"]):
797
834
 
798
835
  return self._recording.getSegment() # full recording
799
836
 
837
+ def getCurrentVolume(self, timeframe=0.2):
838
+ """
839
+ Get the current volume measured by the mic.
840
+
841
+ Parameters
842
+ ----------
843
+ timeframe : float
844
+ Time frame (s) over which to take samples from. Default is 0.1s.
845
+
846
+ Returns
847
+ -------
848
+ float
849
+ Current volume registered by the mic, will depend on relative volume of the mic but
850
+ should mostly be between 0 (total silence) and 1 (very loud).
851
+ """
852
+ # if mic hasn't started yet, return 0 as it's recorded nothing
853
+ if not self.isStarted:
854
+ return 0
855
+ # poll most recent samples
856
+ self.poll()
857
+ # get last 0.1sas a clip
858
+ clip = self._recording.getSegment(
859
+ max(self._recording.lastSample / self._sampleRateHz - timeframe, 0)
860
+ )
861
+
862
+ return clip.rms() * 10
863
+
864
+ def addListener(self, listener, startLoop=False):
865
+ """
866
+ Add a listener, which will receive all the same messages as this device.
867
+
868
+ Parameters
869
+ ----------
870
+ listener : str or psychopy.hardware.listener.BaseListener
871
+ Either a Listener object, or use one of the following strings to create one:
872
+ - "liaison": Create a LiaisonListener with DeviceManager.liaison as the server
873
+ - "print": Create a PrintListener with default settings
874
+ - "log": Create a LoggingListener with default settings
875
+ startLoop : bool
876
+ If True, then upon adding the listener, start up an asynchronous loop to dispatch messages.
877
+ """
878
+ # add listener as normal
879
+ BaseResponseDevice.addListener(self, listener, startLoop=startLoop)
880
+ # if we're starting a listener loop, start recording
881
+ if startLoop:
882
+ self.start()
883
+
884
+ def clearListeners(self):
885
+ """
886
+ Remove any listeners from this device.
887
+
888
+ Returns
889
+ -------
890
+ bool
891
+ True if completed successfully
892
+ """
893
+ # clear listeners as normal
894
+ BaseResponseDevice.clearListeners(self)
895
+ # stop recording
896
+ self.stop()
897
+
898
+ def dispatchMessages(self, clear=True):
899
+ """
900
+ Dispatch current volume as a MicrophoneResponse object to any attached listeners.
901
+
902
+ Parameters
903
+ ----------
904
+ clear : bool
905
+ If True, will clear the recording up until now after dispatching the volume. This is
906
+ useful if you're just sampling volume and aren't wanting to store the recording.
907
+ """
908
+ # create a response object
909
+ message = MicrophoneResponse(
910
+ logging.defaultClock.getTime(),
911
+ self.getCurrentVolume()
912
+ )
913
+ # clear recording if requested (helps with continuous running)
914
+ if clear and self.isRecBufferFull:
915
+ # work out how many samples is 0.1s
916
+ toSave = min(
917
+ int(0.2 * self._sampleRateHz),
918
+ int(self.maxRecordingSize / 2)
919
+ )
920
+ # get last 0.1s so we still have enough for volume measurement
921
+ savedSamples = self._recording._samples[-toSave:, :]
922
+ # clear samples
923
+ self._recording.clear()
924
+ # reassign saved samples
925
+ self._recording.write(savedSamples)
926
+ # dispatch to listeners
927
+ for listener in self.listeners:
928
+ listener.receiveMessage(message)
929
+
800
930
 
801
931
  class RecordingBuffer:
802
932
  """Class for a storing a recording from a stream.
@@ -12,19 +12,12 @@ These are optional components that can be obtained by installing the
12
12
 
13
13
  """
14
14
 
15
- import psychopy.logging as logging
16
-
17
- try:
18
- from psychopy_minolta import CS100A, LS100
19
- except (ModuleNotFoundError, ImportError):
20
- logging.error(
21
- "Support for Konica Minolta hardware is not available this session. "
22
- "Please install `psychopy-minolta` and restart the session to enable "
23
- "support.")
24
- except Exception as e:
25
- logging.error(
26
- "Error encountered while loading `psychopy-minolta`. Check logs for "
27
- "more information.")
28
-
29
- if __name__ == "__main__":
15
+ from psychopy.tools.pkgtools import PluginStub
16
+
17
+
18
+ class CS100A(PluginStub, plugin="psychopy-minolta", doclink="https://psychopy.github.io/psychopy-minolta/coder/CS100A"):
19
+ pass
20
+
21
+
22
+ class LS100(PluginStub, plugin="psychopy-minolta", doclink="https://psychopy.github.io/psychopy-minolta/coder/LS100"):
30
23
  pass
@@ -1,5 +1,5 @@
1
1
  import json
2
- from psychopy import layout, logging
2
+ from psychopy import core, layout, logging
3
3
  from psychopy.hardware import base, DeviceManager
4
4
  from psychopy.localization import _translate
5
5
  from psychopy.hardware import keyboard
@@ -100,8 +100,62 @@ class BasePhotodiodeGroup(base.BaseResponseDevice):
100
100
  matches.append(resp)
101
101
 
102
102
  return matches
103
+
104
+ def findChannels(self, win):
105
+ """
106
+ Flash the entire window white to check which channels are detecting light from the given
107
+ window.
103
108
 
104
- def findPhotodiode(self, win, channel):
109
+ Parameters
110
+ ----------
111
+ win : psychopy.visual.Window
112
+ Window to flash white.
113
+ """
114
+ from psychopy import visual
115
+ # clock for timeouts
116
+ timeoutClock = core.Clock()
117
+ # box to cover screen
118
+ rect = visual.Rect(
119
+ win,
120
+ size=(2, 2), pos=(0, 0), units="norm",
121
+ autoDraw=False
122
+ )
123
+ win.flip()
124
+ # show black
125
+ rect.fillColor = "black"
126
+ rect.draw()
127
+ win.flip()
128
+ # wait 250ms for flip to happen and photodiode to catch it
129
+ timeoutClock.reset()
130
+ while timeoutClock.getTime() < 0.25:
131
+ self.dispatchMessages()
132
+ # finish dispatching any messages which are only partially received
133
+ while self.hasUnfinishedMessage():
134
+ self.dispatchMessages()
135
+ # clear caught messages so we're starting afresh
136
+ self.clearResponses()
137
+ # show white
138
+ rect.fillColor = "white"
139
+ rect.draw()
140
+ win.flip()
141
+ # wait 250ms for flip to happen and photodiode to catch it
142
+ timeoutClock.reset()
143
+ while timeoutClock.getTime() < 0.25:
144
+ self.dispatchMessages()
145
+ # finish dispatching any messages which are only partially received
146
+ while self.hasUnfinishedMessage():
147
+ self.dispatchMessages()
148
+ # start off with no channels
149
+ channels = []
150
+ # iterate through potential channels
151
+ for i, state in enumerate(self.state):
152
+ # if any detected the flash, append it
153
+ if state:
154
+ channels.append(i)
155
+
156
+ return channels
157
+
158
+ def findPhotodiode(self, win, channel=None):
105
159
  """
106
160
  Draws rectangles on the screen and records photodiode responses to recursively find the location of the diode.
107
161
 
@@ -114,6 +168,8 @@ class BasePhotodiodeGroup(base.BaseResponseDevice):
114
168
  Size of the area of certainty. Essentially, the size of the last (smallest) rectangle which the photodiode
115
169
  was able to detect.
116
170
  """
171
+ # timeout clock
172
+ timeoutClock = core.Clock()
117
173
  # keyboard to check for escape
118
174
  kb = keyboard.Keyboard(deviceName="photodiodeValidatorKeyboard")
119
175
  # stash autodraw
@@ -130,7 +186,7 @@ class BasePhotodiodeGroup(base.BaseResponseDevice):
130
186
  # add low opacity label
131
187
  label = visual.TextBox2(
132
188
  win,
133
- text="Finding photodiode...",
189
+ text=f"Finding photodiode...",
134
190
  fillColor=(0, 0, 0), color=(80, 80, 80), colorSpace="rgb255",
135
191
  pos=(0, 0), size=(2, 2), units="norm",
136
192
  alignment="center",
@@ -144,9 +200,29 @@ class BasePhotodiodeGroup(base.BaseResponseDevice):
144
200
  autoDraw=False
145
201
  )
146
202
 
147
- def scanQuadrants():
203
+ # if not given a channel, use first one which is responsive to the win
204
+ if channel is None:
205
+ # get responsive channels
206
+ responsiveChannels = self.findChannels(win=win)
207
+ # use first responsive channel
208
+ if responsiveChannels:
209
+ channel = responsiveChannels[0]
210
+ else:
211
+ # if no channels are responsive, use 0th channel and let scanQuadrants fail cleanly
212
+ channel = 0
213
+ # update label text once we have a channel
214
+ label.text = f"Finding photodiode {channel}..."
215
+
216
+ def scanQuadrants(responsive=False):
148
217
  """
149
- Recursively shrink the rectangle around the position of the photodiode until it's too small to detect.
218
+ Recursively shrink the rectangle around the position of the photodiode until it's too
219
+ small to detect.
220
+
221
+ Parameters
222
+ ----------
223
+ responsive : bool
224
+ When calling manually, this should always be left as False! Will be set to True if
225
+ any response was received from the photodiode.
150
226
  """
151
227
  # work out width and height of area
152
228
  w, h = rect.size
@@ -162,6 +238,11 @@ class BasePhotodiodeGroup(base.BaseResponseDevice):
162
238
  (r - w / 4, t - h / 4), # top right
163
239
  (l + w / 4, b + h / 4), # bottom left
164
240
  (r - w / 4, b + h / 4), # bottom right
241
+ rect.pos, # center
242
+ (l + w / 2, t - h / 4), # top center
243
+ (l + w / 2, b + h / 4), # bottom center
244
+ (l + w / 4, b + h / 2), # center left
245
+ (r - w / 4, b + h / 2), # center right
165
246
  ]:
166
247
  # position rect
167
248
  rect.pos = (x, y)
@@ -170,24 +251,97 @@ class BasePhotodiodeGroup(base.BaseResponseDevice):
170
251
  label.draw()
171
252
  rect.draw()
172
253
  win.flip()
173
- # dispatch parent messages
174
- self.dispatchMessages()
254
+ # wait for flip to happen and photodiode to catch it (max 250ms)
255
+ timeoutClock.reset()
256
+ self.clearResponses()
257
+ while not self.responses and timeoutClock.getTime() < 0.25:
258
+ self.dispatchMessages()
259
+ # finish dispatching any messages which are only partially received
260
+ while self.hasUnfinishedMessage():
261
+ self.dispatchMessages()
175
262
  # check for escape before entering recursion
176
263
  if kb.getKeys(['escape']):
177
- return
264
+ return None
178
265
  # poll photodiode
179
266
  if self.getState(channel):
267
+ # mark that we've got a response
268
+ responsive = True
180
269
  # if it detected this rectangle, recur
181
- return scanQuadrants()
270
+ return scanQuadrants(responsive=responsive)
182
271
  # if none of these have returned, rect is too small to cover the whole photodiode, so return
183
- return
272
+ return responsive
184
273
 
185
274
  # reset state
186
275
  self.state = [None] * self.channels
187
276
  self.dispatchMessages()
188
277
  self.clearResponses()
189
278
  # recursively shrink rect around the photodiode
190
- scanQuadrants()
279
+ responsive = scanQuadrants()
280
+ # if cancelled, warn and continue
281
+ if responsive is None:
282
+ logging.warn(
283
+ "`findPhotodiode` procedure cancelled by user."
284
+ )
285
+ return (
286
+ layout.Position(self.pos, units="norm", win=win),
287
+ layout.Position(self.size, units="norm", win=win),
288
+ )
289
+ # if we didn't get any responses at all, prompt to try again
290
+ if not responsive:
291
+ # set label text to alert user
292
+ label.text = (
293
+ "Received no responses from photodiode during `findPhotodiode`. Photodiode may not "
294
+ "be connected or may be configured incorrectly.\n"
295
+ "\n"
296
+ "To continue, use the arrow keys to move the photodiode patch and use the "
297
+ "plus/minus keys to resize it.\n"
298
+ "\n"
299
+ "Press ENTER when finished."
300
+ )
301
+ label.foreColor = "red"
302
+ # revert to defaults
303
+ self.units = rect.units = "norm"
304
+ self.size = rect.size = (0.1, 0.1)
305
+ self.pos = rect.pos = (0.9, -0.9)
306
+ # start a frame loop until they press enter
307
+ keys = []
308
+ res = 0.05
309
+ while "return" not in keys:
310
+ # get keys
311
+ keys = kb.getKeys()
312
+ # skip if escape pressed
313
+ if "escape" in keys:
314
+ return None
315
+ # move rect according to arrow keys
316
+ pos = list(rect.pos)
317
+ if "left" in keys:
318
+ pos[0] -= res
319
+ if "right" in keys:
320
+ pos[0] += res
321
+ if "up" in keys:
322
+ pos[1] += res
323
+ if "down" in keys:
324
+ pos[1] -= res
325
+ rect.pos = self.pos = pos
326
+ # resize rect according to +- keys
327
+ size = rect.size
328
+ if "equal" in keys:
329
+ size = [sz * 2 for sz in size]
330
+ if "minus" in keys:
331
+ size = [sz / 2 for sz in size]
332
+ rect.size = self.size = size
333
+ # show label and square
334
+ label.draw()
335
+ rect.draw()
336
+ # flip
337
+ win.flip()
338
+ # wait for a keypress
339
+ kb.waitKeys()
340
+ # return defaults
341
+ return (
342
+ layout.Position(self.pos, units="norm", win=win),
343
+ layout.Position(self.size, units="norm", win=win),
344
+ )
191
345
  # clear all the events created by this process
192
346
  self.state = [None] * self.channels
193
347
  self.dispatchMessages()
@@ -207,7 +361,17 @@ class BasePhotodiodeGroup(base.BaseResponseDevice):
207
361
  layout.Position(self.size, units="norm", win=win),
208
362
  )
209
363
 
210
- def findThreshold(self, win, channel):
364
+ def findThreshold(self, win, channel=None):
365
+ # if not given a channel, find for all channels
366
+ if channel is None:
367
+ thresholds = []
368
+ # iterate through channels
369
+ for channel in range(self.channels):
370
+ thresholds.append(
371
+ self.findThreshold(win, channel=channel)
372
+ )
373
+ # return array of thresholds
374
+ return thresholds
211
375
  # keyboard to check for escape/continue
212
376
  kb = keyboard.Keyboard(deviceName="photodiodeValidatorKeyboard")
213
377
  # stash autodraw
@@ -223,7 +387,7 @@ class BasePhotodiodeGroup(base.BaseResponseDevice):
223
387
  # add low opacity label
224
388
  label = visual.TextBox2(
225
389
  win,
226
- text="Finding best threshold for photodiode...",
390
+ text=f"Finding best threshold for photodiode {channel}...",
227
391
  fillColor=None, color=(0, 0, 0), colorSpace="rgb",
228
392
  pos=(0, 0), size=(2, 2), units="norm",
229
393
  alignment="center",
@@ -294,8 +458,10 @@ class BasePhotodiodeGroup(base.BaseResponseDevice):
294
458
  win.retrieveAutoDraw()
295
459
  # flip
296
460
  win.flip()
461
+ # set to found threshold
462
+ self._setThreshold(int(threshold), channel=channel)
297
463
 
298
- return threshold
464
+ return int(threshold)
299
465
 
300
466
  def setThreshold(self, threshold, channel):
301
467
  if isinstance(channel, (list, tuple)):
@@ -485,6 +651,9 @@ class ScreenBufferSampler(BasePhotodiodeGroup):
485
651
  win = self.win
486
652
  else:
487
653
  self.win = win
654
+ # handle None
655
+ if channel is None:
656
+ channel = 0
488
657
  # there's no physical photodiode, so just pick a reasonable place for it
489
658
  self._pos = layout.Position((0.95, -0.95), units="norm", win=win)
490
659
  self._size = layout.Size((0.05, 0.05), units="norm", win=win)
@@ -493,7 +662,13 @@ class ScreenBufferSampler(BasePhotodiodeGroup):
493
662
  return self._pos, self._size
494
663
 
495
664
  def findThreshold(self, win=None, channel=0):
496
- self.win = win
665
+ if win is None:
666
+ win = self.win
667
+ else:
668
+ self.win = win
669
+ # handle None
670
+ if channel is None:
671
+ channel = 0
497
672
  # there's no physical photodiode, so just pick a reasonable threshold
498
673
  self.setThreshold(127, channel=channel)
499
674
 
@@ -503,7 +678,7 @@ class ScreenBufferSampler(BasePhotodiodeGroup):
503
678
  class PhotodiodeValidator:
504
679
 
505
680
  def __init__(
506
- self, win, diode, channel,
681
+ self, win, diode, channel=None,
507
682
  variability=1/60,
508
683
  report="log",
509
684
  autoLog=False):
@@ -18,34 +18,23 @@ __all__ = [
18
18
  'getAllPhotometerClasses'
19
19
  ]
20
20
 
21
- import sys
21
+ from psychopy.tools.pkgtools import PluginStub
22
22
 
23
23
  # Special handling for legacy classes which have been offloaded to optional
24
24
  # packages. This will change to allow more flexibility in the future to avoid
25
25
  # updating this package for additions to these sub-packages. We'll need a
26
26
  # photometer type to do that, but for now we're doing it like this.
27
- try:
28
- from ..crs import ColorCAL, OptiCAL
29
- except Exception:
30
- ColorCAL = OptiCAL = None
27
+ from psychopy.hardware.crs.colorcal import ColorCAL
28
+ from psychopy.hardware.crs.optical import OptiCAL
31
29
 
32
30
  # Photo Resaerch Inc. spectroradiometers
33
- try:
34
- from ..pr import PR655, PR650
35
- except Exception:
36
- PR655 = PR650 = None
31
+ from psychopy.hardware.pr import PR655, PR650
37
32
 
38
33
  # Konica Minolta light-measuring devices
39
- try:
40
- from ..minolta import LS100, CS100A
41
- except Exception:
42
- LS100 = CS100A = None
34
+ from psychopy.hardware.minolta import LS100, CS100A
43
35
 
44
36
  # Gamma scientific devices
45
- try:
46
- from ..gammasci import S470
47
- except Exception:
48
- S470 = None
37
+ from psychopy.hardware.gammasci import S470
49
38
 
50
39
  # photometer interfaces will be stored here after being registered
51
40
  photometerInterfaces = {}
@@ -123,8 +112,11 @@ def getAllPhotometers():
123
112
  'ColorCAL', 'OptiCAL', 'S470', 'PR650', 'PR655', 'LS100', 'CS100A')
124
113
  incPhotomList = []
125
114
  for photName in optionalPhotometers:
126
- photClass = getattr(sys.modules[__name__], photName)
127
- if photClass is None: # not loaded if `None`
115
+ try:
116
+ photClass = globals()[photName]
117
+ except (ImportError, AttributeError):
118
+ continue
119
+ if issubclass(photClass, PluginStub):
128
120
  continue
129
121
  incPhotomList.append(photClass)
130
122
 
psychopy/hardware/pr.py CHANGED
@@ -12,19 +12,12 @@ These are optional components that can be obtained by installing the
12
12
 
13
13
  """
14
14
 
15
- import psychopy.logging as logging
16
-
17
- try:
18
- from psychopy_photoresearch import PR650, PR655
19
- except (ModuleNotFoundError, ImportError):
20
- logging.error(
21
- "Support for Photo Research Inc. hardware is not available this "
22
- "session. Please install `psychopy-photoresearch` and restart the "
23
- "session to enable support.")
24
- except Exception as e:
25
- logging.error(
26
- "Error encountered while loading `psychopy-photoresearch`. Check logs "
27
- "for more information.")
28
-
29
- if __name__ == "__main__":
15
+ from psychopy.tools.pkgtools import PluginStub
16
+
17
+
18
+ class PR650(PluginStub, plugin="psychopy-photoresearch", doclink="https://psychopy.github.io/psychopy-photoresearch/coder/PR650"):
19
+ pass
20
+
21
+
22
+ class PR655(PluginStub, plugin="psychopy-photoresearch", doclink="https://psychopy.github.io/psychopy-photoresearch/coder/PR655"):
30
23
  pass