psychopy 2024.1.3__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 (331) 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/localizedStrings.py +11 -9
  19. psychopy/app/builder/validators.py +2 -2
  20. psychopy/app/coder/codeEditorBase.py +8 -8
  21. psychopy/app/coder/coder.py +4 -4
  22. psychopy/app/connections/sendusage.py +2 -2
  23. psychopy/app/connections/updates.py +9 -9
  24. psychopy/app/dialogs.py +34 -2
  25. psychopy/app/idle.py +31 -0
  26. psychopy/app/jobs.py +21 -3
  27. psychopy/app/linuxconfig/__init__.py +9 -0
  28. psychopy/app/locale/ar_001/LC_MESSAGE/messages.mo +0 -0
  29. psychopy/app/locale/ar_001/LC_MESSAGE/messages.po +4602 -2540
  30. psychopy/app/locale/es_CO/LC_MESSAGE/messages.mo +0 -0
  31. psychopy/app/locale/es_CO/LC_MESSAGE/messages.po +56 -54
  32. psychopy/app/locale/es_ES/LC_MESSAGE/messages.po +53 -43
  33. psychopy/app/locale/es_US/LC_MESSAGE/messages.mo +0 -0
  34. psychopy/app/locale/es_US/LC_MESSAGE/messages.po +56 -54
  35. psychopy/app/locale/ja_JP/LC_MESSAGE/messages.mo +0 -0
  36. psychopy/app/locale/ja_JP/LC_MESSAGE/messages.po +1258 -1176
  37. psychopy/app/locale/pt_PT/LC_MESSAGE/messages.po +9415 -5
  38. psychopy/app/pavlovia_ui/_base.py +33 -3
  39. psychopy/app/pavlovia_ui/search.py +0 -1
  40. psychopy/app/plugin_manager/dialog.py +104 -51
  41. psychopy/app/plugin_manager/packages.py +5 -0
  42. psychopy/app/plugin_manager/plugins.py +152 -67
  43. psychopy/app/preferencesDlg.py +8 -8
  44. psychopy/app/psychopyApp.py +11 -5
  45. psychopy/app/ribbon.py +124 -14
  46. psychopy/app/runner/runner.py +6 -1
  47. psychopy/app/stdout/stdOutRich.py +27 -11
  48. psychopy/app/themes/icons.py +52 -2
  49. psychopy/assets/__init__.py +0 -0
  50. psychopy/assets/click.png +0 -0
  51. psychopy/assets/clicknext.png +0 -0
  52. psychopy/assets/next.png +0 -0
  53. psychopy/assets/psychopy.ico +0 -0
  54. psychopy/assets/psychopy.png +0 -0
  55. psychopy/assets/templates/__init__.py +0 -0
  56. psychopy/assets/touch.png +0 -0
  57. psychopy/assets/touchnext.png +0 -0
  58. psychopy/assets/window.ico +0 -0
  59. psychopy/changes/2023.1.0.md +9 -0
  60. psychopy/changes/2024.1.0.md +16 -0
  61. psychopy/changes/__init__.py +0 -0
  62. psychopy/clock.py +2 -2
  63. psychopy/colors.py +2 -1
  64. psychopy/compatibility.py +53 -1
  65. psychopy/contrib/.DS_Store +0 -0
  66. psychopy/contrib/configobj/__init__.py +10 -8
  67. psychopy/data/__init__.py +3 -2
  68. psychopy/data/base.py +5 -5
  69. psychopy/data/experiment.py +130 -4
  70. psychopy/data/routine.py +56 -0
  71. psychopy/data/staircase.py +2 -2
  72. psychopy/data/trial.py +559 -97
  73. psychopy/data/utils.py +56 -21
  74. psychopy/demos/.DS_Store +0 -0
  75. psychopy/demos/builder/.DS_Store +0 -0
  76. psychopy/demos/builder/Design Templates/.DS_Store +0 -0
  77. psychopy/demos/builder/Experiments/.DS_Store +0 -0
  78. psychopy/demos/builder/Feature Demos/.DS_Store +0 -0
  79. psychopy/demos/builder/Feature Demos/buttonBox/buttonBoxDemo.psyexp +375 -0
  80. psychopy/demos/builder/Feature Demos/buttonBox/readme.md +5 -0
  81. psychopy/demos/builder/Feature Demos/pilotMode/pilotMode.psyexp +433 -0
  82. psychopy/demos/builder/Feature Demos/pilotMode/readme.md +7 -0
  83. psychopy/demos/builder/Feature Demos/progress/progressBar.psyexp +4 -4
  84. psychopy/demos/builder/Hardware/.DS_Store +0 -0
  85. psychopy/demos/builder/Helper Tools/.DS_Store +0 -0
  86. psychopy/demos/coder/.DS_Store +0 -0
  87. psychopy/demos/coder/hardware/testSoundLatency.py +2 -2
  88. psychopy/demos/coder/iohub/.DS_Store +0 -0
  89. psychopy/demos/coder/misc/hdf5_2_csv +33 -0
  90. psychopy/event.py +30 -29
  91. psychopy/experiment/.DS_Store +0 -0
  92. psychopy/experiment/_experiment.py +6 -6
  93. psychopy/experiment/components/.DS_Store +0 -0
  94. psychopy/experiment/components/__init__.py +6 -3
  95. psychopy/experiment/components/_base.py +286 -131
  96. psychopy/experiment/components/aperture/.DS_Store +0 -0
  97. psychopy/experiment/components/brush/.DS_Store +0 -0
  98. psychopy/experiment/components/button/.DS_Store +0 -0
  99. psychopy/experiment/components/button/__init__.py +5 -1
  100. psychopy/experiment/components/buttonBox/.DS_Store +0 -0
  101. psychopy/experiment/components/buttonBox/__init__.py +21 -12
  102. psychopy/experiment/components/camera/.DS_Store +0 -0
  103. psychopy/experiment/components/code/.DS_Store +0 -0
  104. psychopy/experiment/components/dots/.DS_Store +0 -0
  105. psychopy/experiment/components/eyetracker_record/.DS_Store +0 -0
  106. psychopy/experiment/components/eyetracker_record/__init__.py +92 -30
  107. psychopy/experiment/components/form/.DS_Store +0 -0
  108. psychopy/experiment/components/form/__init__.py +6 -2
  109. psychopy/experiment/components/grating/.DS_Store +0 -0
  110. psychopy/experiment/components/grating/__init__.py +14 -3
  111. psychopy/experiment/components/image/.DS_Store +0 -0
  112. psychopy/experiment/components/image/__init__.py +14 -3
  113. psychopy/experiment/components/joyButtons/.DS_Store +0 -0
  114. psychopy/experiment/components/joystick/.DS_Store +0 -0
  115. psychopy/experiment/components/keyboard/.DS_Store +0 -0
  116. psychopy/experiment/components/keyboard/__init__.py +22 -10
  117. psychopy/experiment/components/microphone/.DS_Store +0 -0
  118. psychopy/experiment/components/microphone/__init__.py +59 -39
  119. psychopy/experiment/components/mouse/.DS_Store +0 -0
  120. psychopy/experiment/components/mouse/__init__.py +44 -29
  121. psychopy/experiment/components/movie/.DS_Store +0 -0
  122. psychopy/experiment/components/movie/__init__.py +1 -1
  123. psychopy/experiment/components/panorama/.DS_Store +0 -0
  124. psychopy/experiment/components/parallelOut/.DS_Store +0 -0
  125. psychopy/experiment/components/patch/.DS_Store +0 -0
  126. psychopy/experiment/components/polygon/.DS_Store +0 -0
  127. psychopy/experiment/components/polygon/__init__.py +26 -6
  128. psychopy/experiment/components/progress/.DS_Store +0 -0
  129. psychopy/experiment/components/progress/__init__.py +1 -1
  130. psychopy/experiment/components/ratingScale/.DS_Store +0 -0
  131. psychopy/experiment/components/resourceManager/.DS_Store +0 -0
  132. psychopy/experiment/components/roi/.DS_Store +0 -0
  133. psychopy/experiment/components/roi/__init__.py +5 -0
  134. psychopy/experiment/components/routineSettings/.DS_Store +0 -0
  135. psychopy/experiment/components/routineSettings/__init__.py +57 -10
  136. psychopy/experiment/components/serialOut/.DS_Store +0 -0
  137. psychopy/experiment/components/settings/.DS_Store +0 -0
  138. psychopy/experiment/components/settings/__init__.py +117 -42
  139. psychopy/experiment/components/slider/.DS_Store +0 -0
  140. psychopy/experiment/components/sound/.DS_Store +0 -0
  141. psychopy/experiment/components/sound/__init__.py +54 -19
  142. psychopy/experiment/components/static/.DS_Store +0 -0
  143. psychopy/experiment/components/static/__init__.py +1 -1
  144. psychopy/experiment/components/text/.DS_Store +0 -0
  145. psychopy/experiment/components/text/__init__.py +28 -3
  146. psychopy/experiment/components/textbox/.DS_Store +0 -0
  147. psychopy/experiment/components/textbox/__init__.py +12 -2
  148. psychopy/experiment/components/unknown/.DS_Store +0 -0
  149. psychopy/experiment/components/unknown/__init__.py +1 -2
  150. psychopy/experiment/components/unknownPlugin/.DS_Store +0 -0
  151. psychopy/experiment/components/unknownPlugin/__init__.py +2 -2
  152. psychopy/experiment/components/variable/.DS_Store +0 -0
  153. psychopy/experiment/flow.py +11 -4
  154. psychopy/experiment/loops.py +85 -37
  155. psychopy/experiment/params.py +74 -32
  156. psychopy/experiment/py2js_transpiler.py +8 -1
  157. psychopy/experiment/routines/.DS_Store +0 -0
  158. psychopy/experiment/routines/_base.py +102 -22
  159. psychopy/experiment/routines/counterbalance/.DS_Store +0 -0
  160. psychopy/experiment/routines/counterbalance/__init__.py +5 -1
  161. psychopy/experiment/routines/eyetracker_calibrate/.DS_Store +0 -0
  162. psychopy/experiment/routines/eyetracker_validate/.DS_Store +0 -0
  163. psychopy/experiment/routines/pavlovia_survey/.DS_Store +0 -0
  164. psychopy/experiment/routines/photodiodeValidator/.DS_Store +0 -0
  165. psychopy/experiment/routines/photodiodeValidator/__init__.py +7 -6
  166. psychopy/experiment/routines/unknown/.DS_Store +0 -0
  167. psychopy/gui/wxgui.py +4 -4
  168. psychopy/hardware/.DS_Store +0 -0
  169. psychopy/hardware/__init__.py +1 -1
  170. psychopy/hardware/base.py +12 -0
  171. psychopy/hardware/camera/__init__.py +1 -15
  172. psychopy/hardware/cedrus.py +10 -11
  173. psychopy/hardware/crs/colorcal.py +13 -22
  174. psychopy/hardware/crs/optical.py +10 -20
  175. psychopy/hardware/emulator.py +17 -14
  176. psychopy/hardware/eyetracker.py +42 -118
  177. psychopy/hardware/gammasci.py +4 -15
  178. psychopy/hardware/keyboard.py +102 -11
  179. psychopy/hardware/listener.py +3 -0
  180. psychopy/hardware/microphone.py +148 -18
  181. psychopy/hardware/minolta.py +8 -15
  182. psychopy/hardware/photodiode.py +191 -16
  183. psychopy/hardware/photometer/__init__.py +11 -19
  184. psychopy/hardware/pr.py +8 -15
  185. psychopy/hardware/speaker.py +39 -4
  186. psychopy/info.py +0 -71
  187. psychopy/iohub/.DS_Store +0 -0
  188. psychopy/iohub/__init__.py +1 -1
  189. psychopy/iohub/client/__init__.py +30 -20
  190. psychopy/iohub/client/keyboard.py +24 -24
  191. psychopy/iohub/datastore/__init__.py +2 -2
  192. psychopy/iohub/datastore/util.py +2 -2
  193. psychopy/iohub/default_config.yaml +1 -1
  194. psychopy/iohub/devices/.DS_Store +0 -0
  195. psychopy/iohub/devices/__init__.py +112 -25
  196. psychopy/iohub/devices/deviceConfigValidation.py +2 -1
  197. psychopy/iohub/devices/experiment/default_experiment.yaml +12 -1
  198. psychopy/iohub/devices/experiment/supported_config_settings.yaml +5 -1
  199. psychopy/iohub/devices/eyetracker/.DS_Store +0 -0
  200. psychopy/iohub/devices/eyetracker/__init__.py +46 -0
  201. psychopy/iohub/devices/eyetracker/calibration/procedure.py +2 -2
  202. psychopy/iohub/devices/eyetracker/hw/gazepoint/__init__.py +14 -2
  203. psychopy/iohub/devices/eyetracker/hw/mouse/eyetracker.py +3 -4
  204. psychopy/iohub/server.py +2 -2
  205. psychopy/iohub/start_iohub_process.py +3 -0
  206. psychopy/iohub/util/__init__.py +62 -70
  207. psychopy/layout.py +5 -5
  208. psychopy/logging.py +8 -1
  209. psychopy/microphone.py +10 -37
  210. psychopy/platform_specific/__init__.py +0 -2
  211. psychopy/platform_specific/darwin.py +1 -3
  212. psychopy/platform_specific/linux.py +31 -33
  213. psychopy/platform_specific/win32.py +38 -13
  214. psychopy/plugins/__init__.py +148 -116
  215. psychopy/plugins/util.py +39 -0
  216. psychopy/preferences/Darwin.spec +4 -2
  217. psychopy/preferences/FreeBSD.spec +4 -2
  218. psychopy/preferences/Linux.spec +4 -2
  219. psychopy/preferences/Windows.spec +4 -2
  220. psychopy/preferences/baseNoArch.spec +4 -2
  221. psychopy/preferences/preferences.py +47 -24
  222. psychopy/projects/pavlovia.py +47 -4
  223. psychopy/scripts/psyexpCompile.py +0 -4
  224. psychopy/session.py +153 -21
  225. psychopy/sound/__init__.py +31 -21
  226. psychopy/sound/_base.py +20 -3
  227. psychopy/sound/audioclip.py +320 -33
  228. psychopy/sound/backend_ptb.py +47 -58
  229. psychopy/sound/backend_pygame.py +1 -1
  230. psychopy/sound/backend_pysound.py +6 -15
  231. psychopy/sound/transcribe.py +53 -0
  232. psychopy/tests/.DS_Store +0 -0
  233. psychopy/tests/data/.DS_Store +0 -0
  234. psychopy/tests/data/TestUnknownPluginComponent_load_resave.psyexp +135 -0
  235. psychopy/tests/data/Test_textbox/test_ori_0_bottom right.png +0 -0
  236. psychopy/tests/data/Test_textbox/test_ori_0_center.png +0 -0
  237. psychopy/tests/data/Test_textbox/test_ori_0_top left.png +0 -0
  238. psychopy/tests/data/Test_textbox/test_ori_120_bottom right.png +0 -0
  239. psychopy/tests/data/Test_textbox/test_ori_120_center.png +0 -0
  240. psychopy/tests/data/Test_textbox/test_ori_120_top left.png +0 -0
  241. psychopy/tests/data/Test_textbox/test_ori_180_bottom right.png +0 -0
  242. psychopy/tests/data/Test_textbox/test_ori_180_center.png +0 -0
  243. psychopy/tests/data/Test_textbox/test_ori_180_top left.png +0 -0
  244. psychopy/tests/data/Test_textbox/test_ori_240_bottom right.png +0 -0
  245. psychopy/tests/data/Test_textbox/test_ori_240_center.png +0 -0
  246. psychopy/tests/data/Test_textbox/test_ori_240_top left.png +0 -0
  247. psychopy/tests/data/correctScript/.DS_Store +0 -0
  248. psychopy/tests/data/test_components/testClearKeyboard/testClearKeyboard.psyexp +200 -0
  249. psychopy/tests/data/test_session/.DS_Store +0 -0
  250. psychopy/tests/data/test_session/root/testFutureTrials/testFutureTrials.psyexp +155 -0
  251. psychopy/tests/data/test_session/root/testTrialNav/trialNav.psyexp +158 -0
  252. psychopy/tests/test_app/.DS_Store +0 -0
  253. psychopy/tests/test_app/conftest.py +2 -2
  254. psychopy/tests/test_app/test_speed.py +4 -1
  255. psychopy/tests/test_data/test_TrialHandler2.py +146 -1
  256. psychopy/tests/test_experiment/.DS_Store +0 -0
  257. psychopy/tests/test_experiment/needs_wx/genComponsTemplate.py +3 -3
  258. psychopy/tests/test_experiment/needs_wx/test_components.py +2 -2
  259. psychopy/tests/test_experiment/test_components/test_KeyboardComponent.py +28 -0
  260. psychopy/tests/test_experiment/test_components/test_UnknownPluginComponent.py +27 -0
  261. psychopy/tests/test_experiment/test_components/test_base_components.py +58 -0
  262. psychopy/tests/test_experiment/test_py2js.py +1 -1
  263. psychopy/tests/test_hardware/test_keyboard.py +184 -16
  264. psychopy/tests/test_hardware/test_ports.py +1 -11
  265. psychopy/tests/test_liaison/test_Liaison.py +47 -0
  266. psychopy/tests/test_misc/test_core.py +5 -0
  267. psychopy/tests/test_session/test_Session.py +5 -1
  268. psychopy/tests/test_tools/test_versionchooser.py +39 -8
  269. psychopy/tests/test_visual/test_all_stimuli.py +0 -97
  270. psychopy/tests/test_visual/test_image.py +6 -5
  271. psychopy/tests/test_visual/test_textbox.py +36 -0
  272. psychopy/tests/utils.py +4 -0
  273. psychopy/tools/filetools.py +1 -1
  274. psychopy/tools/pkgtools.py +160 -137
  275. psychopy/tools/versionchooser.py +10 -10
  276. psychopy/tools/wizard.py +3 -3
  277. psychopy/visual/.DS_Store +0 -0
  278. psychopy/visual/backends/pygletbackend.py +24 -13
  279. psychopy/visual/basevisual.py +5 -11
  280. psychopy/visual/button.py +2 -14
  281. psychopy/visual/helpers.py +5 -5
  282. psychopy/visual/line.py +1 -2
  283. psychopy/visual/movie2.py +7 -816
  284. psychopy/visual/movie3.py +7 -589
  285. psychopy/visual/movies/__init__.py +8 -11
  286. psychopy/visual/movies/frame.py +5 -2
  287. psychopy/visual/movies/players/ffpyplayer_player.py +5 -2
  288. psychopy/visual/noise.py +8 -7
  289. psychopy/visual/patch.py +7 -16
  290. psychopy/visual/progress.py +1 -1
  291. psychopy/visual/radial.py +9 -7
  292. psychopy/visual/ratingscale.py +8 -1415
  293. psychopy/visual/secondorder.py +10 -9
  294. psychopy/visual/shape.py +7 -2
  295. psychopy/visual/text.py +1 -1
  296. psychopy/visual/textbox2/textbox2.py +28 -5
  297. psychopy/web.py +5 -2
  298. {psychopy-2024.1.3.dist-info → psychopy-2024.2.0.dist-info}/METADATA +8 -13
  299. {psychopy-2024.1.3.dist-info → psychopy-2024.2.0.dist-info}/RECORD +313 -219
  300. {psychopy-2024.1.3.dist-info → psychopy-2024.2.0.dist-info}/WHEEL +1 -1
  301. psychopy/app/Resources/click.png +0 -0
  302. psychopy/app/Resources/next.png +0 -0
  303. psychopy/experiment/components/patch/__init__.py +0 -121
  304. psychopy/experiment/components/patch/classic/patch.png +0 -0
  305. psychopy/experiment/components/patch/dark/patch.png +0 -0
  306. psychopy/experiment/components/patch/dark/patch@2x.png +0 -0
  307. psychopy/experiment/components/patch/light/patch.png +0 -0
  308. psychopy/experiment/components/patch/light/patch@2x.png +0 -0
  309. psychopy/experiment/components/ratingScale/__init__.py +0 -337
  310. psychopy/experiment/components/ratingScale/classic/ratingscale.png +0 -0
  311. psychopy/experiment/components/ratingScale/classic/ratingscale@2x.png +0 -0
  312. psychopy/experiment/components/ratingScale/dark/ratingScale@2x.png +0 -0
  313. psychopy/experiment/components/ratingScale/dark/ratingscale.png +0 -0
  314. psychopy/experiment/components/ratingScale/light/ratingScale@2x.png +0 -0
  315. psychopy/experiment/components/ratingScale/light/ratingscale.png +0 -0
  316. psychopy/platform_specific/posix.py +0 -16
  317. psychopy/tests/test_sound/test_microphone.py +0 -217
  318. psychopy/tests/test_visual/test_ratingScale.py +0 -299
  319. /psychopy/{app/Resources → assets}/Psychopy Window Favicon@16w.png +0 -0
  320. /psychopy/{app/Resources → assets}/Psychopy Window Favicon@32w.png +0 -0
  321. /psychopy/{app/Resources → assets}/USB-C.png +0 -0
  322. /psychopy/{app/Resources → assets}/USB.png +0 -0
  323. /psychopy/{app/Resources → assets}/creditCard.png +0 -0
  324. /psychopy/{app/Resources → assets}/default.mp3 +0 -0
  325. /psychopy/{app/Resources → assets}/default.mp4 +0 -0
  326. /psychopy/{app/Resources → assets}/default.png +0 -0
  327. /psychopy/{app/Resources → assets/templates}/instruct1.png +0 -0
  328. /psychopy/{app/Resources → assets/templates}/instruct2.png +0 -0
  329. {psychopy-2024.1.3.dist-info → psychopy-2024.2.0.dist-info}/entry_points.txt +0 -0
  330. {psychopy-2024.1.3.dist-info → psychopy-2024.2.0.dist-info}/licenses/AUTHORS.md +0 -0
  331. {psychopy-2024.1.3.dist-info → psychopy-2024.2.0.dist-info}/licenses/LICENSE +0 -0
psychopy/sound/_base.py CHANGED
@@ -210,10 +210,27 @@ class _SoundBase(AttributeGetSetMixin):
210
210
  elif isinstance(value, (list, numpy.ndarray,)):
211
211
  # create a sound from the input array/list
212
212
  self._setSndFromArray(numpy.array(value))
213
- elif isinstance(value, AudioClip):
214
- # from an audio clip object
215
- self.sampleRate = value.sampleRateHz
213
+ elif isinstance(value, AudioClip): # from an audio clip object
214
+ # check if we should resample the audio clip to match the device
215
+ if self.sampleRate is None:
216
+ logging.warning(
217
+ "Sound output sample rate not set. The provided AudioClip "
218
+ "requires a sample rate of {} Hz for playback which may "
219
+ "not match the device settings.".format(value.sampleRateHz))
220
+
221
+ self.sampleRate = value.sampleRateHz
222
+
223
+ if self.sampleRate != value.sampleRateHz:
224
+ logging.warning(
225
+ "Resampling to match sound device sample rate (from {} "
226
+ "to {} Hz), distortion may occur.".format(
227
+ value.sampleRateHz, self.sampleRate))
228
+
229
+ # resample with the new sample rate using the AudioClip method
230
+ value = value.resample(self.sampleRate, copy=True)
231
+
216
232
  self._setSndFromArray(value.samples)
233
+
217
234
  # did we succeed?
218
235
  if self._snd is None:
219
236
  pass # raise ValueError, "Could not make a "+value+" sound"
@@ -24,7 +24,8 @@ __all__ = [
24
24
  ]
25
25
 
26
26
  from pathlib import Path
27
-
27
+ import shutil
28
+ import tempfile
28
29
  import numpy as np
29
30
  import soundfile as sf
30
31
  from psychopy import prefs
@@ -403,6 +404,145 @@ class AudioClip:
403
404
 
404
405
  return AudioClip(samples, sampleRateHz=sampleRateHz)
405
406
 
407
+ # --------------------------------------------------------------------------
408
+ # Speech synthesis methods
409
+ #
410
+ # These static methods are used to generate audio samples from text using
411
+ # text-to-speech (TTS) engines.
412
+ #
413
+
414
+ @staticmethod
415
+ def synthesizeSpeech(text, engine='gtts', synthConfig=None, outFile=None):
416
+ """Synthesize speech from text using a text-to-speech (TTS) engine.
417
+
418
+ This method is used to generate audio samples from text using a
419
+ text-to-speech (TTS) engine. The synthesized speech can be used for
420
+ various purposes, such as generating audio cues for experiments or
421
+ creating audio instructions for participants.
422
+
423
+ This method returns an `AudioClip` object containing the synthesized
424
+ speech. The quality and format of the retured audio may vary depending
425
+ on the TTS engine used.
426
+
427
+ Please note that online TTS engines may require an active internet
428
+ connection to work. This also may send the text to a remote server for
429
+ processing, so be mindful of privacy concerns.
430
+
431
+ Parameters
432
+ ----------
433
+ text : str
434
+ Text to synthesize into speech.
435
+ engine : str
436
+ TTS engine to use for speech synthesis. Default is 'gtts'.
437
+ synthConfig : dict or None
438
+ Additional configuration options for the specified engine. These
439
+ are specified using a dictionary (ex.
440
+ `synthConfig={'slow': False}`). These paramters vary depending on
441
+ the engine in use. Default is `None` which uses the default
442
+ configuration for the engine.
443
+ outFile : str or None
444
+ File name to save the synthesized speech to. This can be used to
445
+ save the audio to a file for later use. If `None`, the audio clip
446
+ will be returned in memory. If you plan on using the same audio
447
+ clip multiple times, it is recommended to save it to a file and load
448
+ it later.
449
+
450
+ Returns
451
+ -------
452
+ AudioClip
453
+ Audio clip containing the synthesized speech.
454
+
455
+ Examples
456
+ --------
457
+ Synthesize speech using the default gTTS engine::
458
+
459
+ import psychopy.sound as sound
460
+ voiceClip = sound.AudioClip.synthesizeSpeech(
461
+ 'How are you doing today?')
462
+
463
+ Save the synthesized speech to a file for later use::
464
+
465
+ voiceClip = sound.AudioClip.synthesizeSpeech(
466
+ 'How are you doing today?', outFile='/path/to/speech.mp3')
467
+
468
+ Synthesize speech using the gTTS engine with a specific language,
469
+ timeout, and top-level domain::
470
+
471
+ voiceClip = sound.AudioClip.synthesizeSpeech(
472
+ 'How are you doing today?',
473
+ engine='gtts',
474
+ synthConfig={'lang': 'en', 'timeout': 10, 'tld': 'us'})
475
+
476
+ """
477
+ if engine not in ['gtts']:
478
+ raise ValueError('Unsupported TTS engine specified.')
479
+
480
+ synthConfig = {} if synthConfig is None else synthConfig
481
+
482
+ if engine == 'gtts': # google's text-to-speech engine
483
+ logging.info('Using Google Text-to-Speech (gTTS) engine.')
484
+
485
+ try:
486
+ import gtts
487
+ except ImportError:
488
+ raise ImportError(
489
+ 'The gTTS package is required for speech synthesis.')
490
+
491
+ # set defaults for parameters if not specified
492
+ if 'timeout' not in synthConfig:
493
+ synthConfig['timeout'] = None
494
+ logging.warning(
495
+ 'The gTTS speech-to-text engine has been configured with '
496
+ 'an infinite timeout. The application may stall if the '
497
+ 'server is unresponsive. To set a timeout, specify the '
498
+ '`timeout` key in `synthConfig`.')
499
+
500
+ if 'lang' not in synthConfig: # language
501
+ synthConfig['lang'] = 'en'
502
+ logging.info(
503
+ "Language not specified, defaulting to '{}' for speech "
504
+ "synthesis engine.".format(synthConfig['lang']))
505
+ else:
506
+ # check if the value is a valid language code
507
+ if synthConfig['lang'] not in gtts.lang.tts_langs():
508
+ raise ValueError('Unsupported language code specified.')
509
+
510
+ if 'tld' not in synthConfig: # top-level domain
511
+ synthConfig['tld'] = 'us'
512
+ logging.info(
513
+ "Top-level domain (TLD) not specified, defaulting to '{}' "
514
+ "for synthesis engine.".format(synthConfig['tld']))
515
+
516
+ if 'slow' not in synthConfig: # slow mode
517
+ synthConfig['slow'] = False
518
+ logging.info(
519
+ "Slow mode not specified, defaulting to '{}' for synthesis "
520
+ "engine.".format(synthConfig['slow']))
521
+
522
+ try:
523
+ handle = gtts.gTTS(
524
+ text=text,
525
+ **synthConfig)
526
+ except gtts.gTTSError as e:
527
+ raise AudioSynthesisError(
528
+ 'Error occurred during speech synthesis: {}'.format(e))
529
+
530
+ # this is online and needs a download, so we'll save it to a file
531
+ with tempfile.TemporaryDirectory() as tmpdir:
532
+ # always returns an MP3 file
533
+ tmpfile = str(Path(tmpdir) / 'psychopy_tts_output.mp3')
534
+ handle.save(tmpfile)
535
+
536
+ # load audio clip samples to memory
537
+ toReturn = AudioClip.load(tmpfile)
538
+
539
+ # copy the file if we want to save it
540
+ import shutil
541
+ if outFile is not None:
542
+ shutil.copy(tmpfile, outFile)
543
+
544
+ return toReturn
545
+
406
546
  # --------------------------------------------------------------------------
407
547
  # Audio editing methods
408
548
  #
@@ -514,6 +654,154 @@ class AudioClip:
514
654
  arrview *= float(factor)
515
655
  arrview.clip(-1, 1)
516
656
 
657
+ def resample(self, targetSampleRateHz, resampleType='default',
658
+ equalEnergy=False, copy=False):
659
+ """Resample audio to another sample rate.
660
+
661
+ This method will resample the audio clip to a new sample rate. The
662
+ method used for resampling can be specified using the `method` parameter.
663
+
664
+ Parameters
665
+ ----------
666
+ targetSampleRateHz : int
667
+ New sample rate.
668
+ resampleType : str
669
+ Fitler (or method) to use for resampling. The methods available
670
+ depend on the packages installed. The 'default' method uses
671
+ `scipy.signal.resample` to resample the audio. Other methods require
672
+ the user to install `librosa` or `resampy`. Default is 'default'.
673
+ equalEnergy : bool
674
+ Make the output have similar energy to the input. Option not
675
+ available for the 'default' method. Default is `False`.
676
+ copy : bool
677
+ Return a copy of the resampled audio clip at the new sample rate.
678
+ If `False`, the audio clip will be resampled inplace. Default is
679
+ `False`.
680
+
681
+ Returns
682
+ -------
683
+ AudioClip
684
+ Resampled audio clip.
685
+
686
+ Notes
687
+ -----
688
+ * Resampling audio clip may result in distortion which is exacerbated by
689
+ successive resampling.
690
+ * When using `librosa` for resampling, the `fix` parameter is set to
691
+ `False`.
692
+ * The resampling types 'linear', 'zero_order_hold', 'sinc_best',
693
+ 'sinc_medium' and 'sinc_fastest' require the `samplerate` package to
694
+ be installed in addition to `librosa`.
695
+ * Specifying either the 'fft' or 'scipy' method will use the same
696
+ resampling method as the 'default' method, howwever it will allow for
697
+ the `equalEnergy` option to be used.
698
+
699
+ Examples
700
+ --------
701
+ Resample an audio clip to 44.1kHz::
702
+
703
+ snd.resample(44100)
704
+
705
+ Use the 'soxr_vhq' method for resampling::
706
+
707
+ snd.resample(44100, resampleType='soxr_vhq')
708
+
709
+ Create a copy of the audio clip resampled to 44.1kHz::
710
+
711
+ sndResampled = snd.resample(44100, copy=True)
712
+
713
+ Resample the audio clip to be playable on a certain device::
714
+
715
+ import psychopy.sound as sound
716
+ from psychopy.sound.audioclip import AudioClip
717
+
718
+ audioClip = sound.AudioClip.load('/path/to/audio.wav')
719
+
720
+ deviceSampleRateHz = sound.Sound().sampleRate
721
+ audioClip.resample(deviceSampleRateHz)
722
+
723
+ """
724
+ targetSampleRateHz = int(targetSampleRateHz) # ensure it's an integer
725
+
726
+ # sample rate is the same, return self
727
+ if targetSampleRateHz == self._sampleRateHz:
728
+ if copy:
729
+ return AudioClip(
730
+ self._samples.copy(),
731
+ sampleRateHz=self._sampleRateHz)
732
+
733
+ logging.info('No resampling needed, sample rate is the same.')
734
+
735
+ return self # no need to resample
736
+
737
+ if resampleType == 'default': # scipy
738
+ import scipy.signal # hard dep, so we'll import here
739
+
740
+ # the simplest method to resample audio using the libraries we have
741
+ # already
742
+ nSamp = round(
743
+ len(self._samples) * float(targetSampleRateHz) /
744
+ self.sampleRateHz)
745
+ newSamples = scipy.signal.resample(
746
+ self._samples, nSamp, axis=0)
747
+
748
+ if equalEnergy:
749
+ logging.warning(
750
+ 'The `equalEnergy` option is not available for the '
751
+ 'default resampling method.')
752
+
753
+ elif resampleType in ('kaiser_best', 'kaiser_fast'): # resampy
754
+ try:
755
+ import resampy
756
+ except ImportError:
757
+ raise ImportError(
758
+ 'The `resampy` package is required for this resampling '
759
+ 'method ({}).'.format(resampleType))
760
+
761
+ newSamples = resampy.resample(
762
+ self._samples,
763
+ self._sampleRateHz,
764
+ targetSampleRateHz,
765
+ filter=resampleType,
766
+ scale=equalEnergy,
767
+ axis=0)
768
+
769
+ elif resampleType in ('soxr_vhq', 'soxr_hq', 'soxr_mq', 'soxr_lq',
770
+ 'soxr_qq', 'polyphase', 'linear', 'zero_order_hold', 'fft',
771
+ 'scipy', 'sinc_best', 'sinc_medium', 'sinc_fastest'): # librosa
772
+ try:
773
+ import librosa
774
+ except ImportError:
775
+ raise ImportError(
776
+ 'The `librosa` package is required for this resampling '
777
+ 'method ({}).'.format(resampleType))
778
+
779
+ newSamples = librosa.resample(
780
+ self._samples,
781
+ orig_sr=self._sampleRateHz,
782
+ target_sr=targetSampleRateHz,
783
+ res_type=resampleType,
784
+ scale=equalEnergy,
785
+ fix=False,
786
+ axis=0)
787
+
788
+ else:
789
+ raise ValueError('Unsupported resampling method specified.')
790
+
791
+ logging.info(
792
+ "Resampled audio from {}Hz to {}Hz using method '{}'.".format(
793
+ self._sampleRateHz, targetSampleRateHz, resampleType))
794
+
795
+ if copy: # return a new object
796
+ return AudioClip(newSamples, sampleRateHz=targetSampleRateHz)
797
+
798
+ # inplace resampling, need to clear the old array since the shape may
799
+ # have changed
800
+ self._samples = newSamples
801
+ self._sampleRateHz = targetSampleRateHz
802
+
803
+ return self
804
+
517
805
  # --------------------------------------------------------------------------
518
806
  # Audio analysis methods
519
807
  #
@@ -583,37 +871,6 @@ class AudioClip:
583
871
  self._sampleRateHz = int(value)
584
872
  # recompute duration after updating sample rate
585
873
  self._duration = len(self._samples) / float(self._sampleRateHz)
586
-
587
- def resample(self, targetSampleRateHz, resampleType='soxr_hq',
588
- equalEnergy=False):
589
- """Resample audio to another sample rate.
590
-
591
- Parameters
592
- ----------
593
- targetSampleRateHz : int
594
- New sample rate.
595
- resampleType : str or None
596
- Method to use for resampling.
597
- equalEnergy : bool
598
- Make the output have similar energy to the input.
599
-
600
- Notes
601
- -----
602
- * Resampling audio clip may result in distortion which is exacerbated by
603
- successive resampling.
604
-
605
- """
606
- import librosa
607
-
608
- self.samples = librosa.resample(
609
- self.samples,
610
- self._sampleRateHz,
611
- targetSampleRateHz,
612
- res_type=resampleType,
613
- scale=equalEnergy,
614
- axis=0)
615
-
616
- self.sampleRateHz = targetSampleRateHz # update
617
874
 
618
875
  @property
619
876
  def duration(self):
@@ -719,6 +976,36 @@ class AudioClip:
719
976
  self._samples = samplesMixed # overwrite
720
977
 
721
978
  return self
979
+
980
+ def asStereo(self, copy=True):
981
+ """Convert the audio clip to stereo (two channel audio).
982
+
983
+ Parameters
984
+ ----------
985
+ copy : bool
986
+ If `True` an :class:`~psychopy.sound.AudioClip` containing a copy
987
+ of the samples will be returned. If `False`, channels will be
988
+ mixed inplace resulting in the same object being returned. User data
989
+ is not copied.
990
+
991
+ Returns
992
+ -------
993
+ :class:`~psychopy.sound.AudioClip`
994
+ Stereo version of this object.
995
+
996
+ """
997
+ if self.channels == 2:
998
+ return self
999
+
1000
+ samples = np.atleast_2d(self._samples) # enforce 2D
1001
+ samples = np.hstack((samples, samples))
1002
+
1003
+ if copy:
1004
+ return AudioClip(samples, self.sampleRateHz)
1005
+
1006
+ self._samples = samples # overwrite
1007
+
1008
+ return self
722
1009
 
723
1010
  def transcribe(self, engine='whisper', language='en-US', expectedWords=None,
724
1011
  config=None):
@@ -826,7 +1113,7 @@ def load(filename, codec=None):
826
1113
  """
827
1114
  # alias default names (so it always points to default.png)
828
1115
  if filename in ft.defaultStim:
829
- filename = Path(prefs.paths['resources']) / ft.defaultStim[filename]
1116
+ filename = Path(prefs.paths['assets']) / ft.defaultStim[filename]
830
1117
  return AudioClip.load(filename, codec)
831
1118
 
832
1119
 
@@ -86,7 +86,7 @@ def getDevices(kind=None):
86
86
  kind can be None, 'input' or 'output'
87
87
  The dict keys are names, and items are dicts of properties
88
88
  """
89
- if sys.platform=='win32':
89
+ if sys.platform == 'win32':
90
90
  deviceTypes = 13 # only WASAPI drivers need apply!
91
91
  else:
92
92
  deviceTypes = None
@@ -97,7 +97,7 @@ def getDevices(kind=None):
97
97
  allDevs = audio.get_devices(device_type=deviceTypes)
98
98
 
99
99
  # annoyingly query_devices is a DeviceList or a dict depending on number
100
- if type(allDevs) == dict:
100
+ if isinstance(allDevs, dict):
101
101
  allDevs = [allDevs]
102
102
 
103
103
  for ii, dev in enumerate(allDevs):
@@ -180,13 +180,13 @@ class _StreamsDict(dict):
180
180
  raise SoundFormatError(
181
181
  "Tried to create audio stream {} but {} already exists "
182
182
  "and {} doesn't support multiple portaudio streams"
183
- .format(label, list(self.keys())[0], sys.platform)
183
+ .format(label, list(self.keys())[0], sys.platform)
184
184
  )
185
185
  else:
186
186
 
187
187
  # create new stream
188
188
  self[label] = _MasterStream(sampleRate, channels, blockSize,
189
- device=defaultOutput)
189
+ device=defaultOutput)
190
190
  return label, self[label]
191
191
 
192
192
 
@@ -209,9 +209,9 @@ class _MasterStream(audio.Stream):
209
209
  self.duplex = duplex
210
210
  self.blockSize = blockSize
211
211
  self.label = getStreamLabel(sampleRate, channels, blockSize)
212
- if type(device) == list and len(device):
212
+ if isinstance(device, list) and len(device):
213
213
  device = device[0]
214
- if type(device)==str: # we need to convert name to an ID or make None
214
+ if isinstance(device, str): # we need to convert name to an ID or make None
215
215
  devs = getDevices('output')
216
216
  if device in devs:
217
217
  deviceID = devs[device]['DeviceIndex']
@@ -226,16 +226,16 @@ class _MasterStream(audio.Stream):
226
226
  if not systemtools.isVM_CI(): # Github Actions VM does not have a sound device
227
227
  try:
228
228
  audio.Stream.__init__(self, device_id=deviceID, mode=mode+8,
229
- latency_class=audioLatencyClass,
230
- freq=sampleRate,
231
- channels=channels,
232
- ) # suggested_latency=suggestedLatency
233
- except OSError as e:
229
+ latency_class=audioLatencyClass,
230
+ freq=sampleRate,
231
+ channels=channels,
232
+ ) # suggested_latency=suggestedLatency
233
+ except OSError as e: # noqa: F841
234
234
  audio.Stream.__init__(self, device_id=deviceID, mode=mode+8,
235
- latency_class=audioLatencyClass,
236
- # freq=sampleRate,
237
- channels=channels,
238
- )
235
+ latency_class=audioLatencyClass,
236
+ # freq=sampleRate,
237
+ channels=channels,
238
+ )
239
239
  self.sampleRate = self.status['SampleRate']
240
240
  print("Failed to start PTB.audio with requested rate of "
241
241
  "{} but succeeded with a default rate ({}). "
@@ -244,14 +244,14 @@ class _MasterStream(audio.Stream):
244
244
  except TypeError as e:
245
245
  print("device={}, mode={}, latency_class={}, freq={}, channels={}"
246
246
  .format(device, mode+8, audioLatencyClass, sampleRate, channels))
247
- raise(e)
247
+ raise e
248
248
  except Exception as e:
249
249
  audio.Stream.__init__(self, mode=mode+8,
250
- latency_class=audioLatencyClass,
251
- freq=sampleRate,
252
- channels=channels,
253
- )
254
-
250
+ latency_class=audioLatencyClass,
251
+ freq=sampleRate,
252
+ channels=channels,
253
+ )
254
+
255
255
  if "there isn't any audio output device" in str(e):
256
256
  print("Failed to load audio device:\n"
257
257
  " '{}'\n"
@@ -329,7 +329,7 @@ class SoundPTB(_SoundBase):
329
329
  self.sndArr = None
330
330
  self.hamming = hamming
331
331
  self._hammingWindow = None # will be created during setSound
332
- self.win=syncToWin
332
+ self.win = syncToWin
333
333
  # setSound (determines sound type)
334
334
  self.setSound(value, secs=self.secs, octave=self.octave,
335
335
  hamming=self.hamming)
@@ -340,12 +340,14 @@ class SoundPTB(_SoundBase):
340
340
  @property
341
341
  def isPlaying(self):
342
342
  """`True` if the audio playback is ongoing."""
343
+ # This will update _isPlaying if sound has stopped by _EOS()
344
+ _ = self._checkPlaybackFinished()
343
345
  return self._isPlaying
344
346
 
345
347
  @property
346
348
  def isFinished(self):
347
349
  """`True` if the audio playback has completed."""
348
- return self._isFinished
350
+ return self._checkPlaybackFinished()
349
351
 
350
352
  def _getDefaultSampleRate(self):
351
353
  """Check what streams are open and use one of these"""
@@ -360,27 +362,6 @@ class SoundPTB(_SoundBase):
360
362
  return None
361
363
  return self.track.status
362
364
 
363
- @property
364
- def status(self):
365
- """status gives a simple value from psychopy.constants to indicate
366
- NOT_STARTED, STARTED, FINISHED, PAUSED
367
-
368
- Psychtoolbox sounds also have a statusDetailed property with further info"""
369
-
370
- if self.__dict__['status']==STARTED:
371
- # check portaudio to see if still playing
372
- pa_status = self.statusDetailed
373
- if not pa_status['Active'] and pa_status['State']==0:
374
- # we were playing and now not so presumably FINISHED
375
- self._EOS()
376
-
377
- return self.__dict__['status']
378
-
379
-
380
- @status.setter
381
- def status(self, newStatus):
382
- self.__dict__['status'] = newStatus
383
-
384
365
  @property
385
366
  def volume(self):
386
367
  return self.__dict__['volume']
@@ -403,9 +384,9 @@ class SoundPTB(_SoundBase):
403
384
  @stereo.setter
404
385
  def stereo(self, val):
405
386
  self.__dict__['stereo'] = val
406
- if val == True:
387
+ if val is True:
407
388
  self.__dict__['channels'] = 2
408
- elif val == False:
389
+ elif val is False:
409
390
  self.__dict__['channels'] = 1
410
391
  elif val == -1:
411
392
  self.__dict__['channels'] = -1
@@ -444,7 +425,7 @@ class SoundPTB(_SoundBase):
444
425
  def _setSndFromFile(self, filename):
445
426
  # alias default names (so it always points to default.png)
446
427
  if filename in ft.defaultStim:
447
- filename = Path(prefs.paths['resources']) / ft.defaultStim[filename]
428
+ filename = Path(prefs.paths['assets']) / ft.defaultStim[filename]
448
429
  self.sndFile = f = sf.SoundFile(filename)
449
430
  self.sourceType = 'file'
450
431
  self.sampleRate = f.samplerate
@@ -479,7 +460,6 @@ class SoundPTB(_SoundBase):
479
460
  self._channelCheck(
480
461
  self.sndArr) # Check for fewer channels in stream vs data array
481
462
 
482
-
483
463
  def _setSndFromArray(self, thisArray):
484
464
 
485
465
  self.sndArr = np.asarray(thisArray).astype('float32')
@@ -529,12 +509,20 @@ class SoundPTB(_SoundBase):
529
509
  "experiment settings**".format(self.channels, array.shape[1]))
530
510
  logging.error(msg)
531
511
  raise ValueError(msg)
532
-
512
+
533
513
  def _checkPlaybackFinished(self):
534
514
  """Checks whether playback has finished by looking up the status.
535
515
  """
516
+ # get detailed status from backend
536
517
  pa_status = self.statusDetailed
537
- self._isFinished = not pa_status['Active'] and pa_status['State'] == 0
518
+ # was the sound already finished?
519
+ wasFinished = self._isFinished
520
+ # is it finished now?
521
+ isFinished = self._isFinished = not pa_status['Active'] and pa_status['State'] == 0
522
+ # if it wasn't finished but now is, do end of stream behaviour
523
+ if isFinished and not wasFinished:
524
+ self._EOS()
525
+
538
526
  return self._isFinished
539
527
 
540
528
  def play(self, loops=None, when=None, log=True):
@@ -546,7 +534,7 @@ class SoundPTB(_SoundBase):
546
534
  """
547
535
  if self._checkPlaybackFinished():
548
536
  self.stop(reset=True)
549
-
537
+
550
538
  if loops is not None and self.loops != loops:
551
539
  self.setLoops(loops)
552
540
 
@@ -570,8 +558,8 @@ class SoundPTB(_SoundBase):
570
558
  def pause(self, log=True):
571
559
  """Stops the sound without reset, so that play will continue from here if needed
572
560
  """
573
- if self.isPlaying:
574
- self.stop(reset=False)
561
+ if self._isPlaying:
562
+ self.stop(reset=False, log=False)
575
563
  if log and self.autoLog:
576
564
  logging.exp(u"Sound %s paused" % (self.name), obj=self)
577
565
 
@@ -579,7 +567,7 @@ class SoundPTB(_SoundBase):
579
567
  """Stop the sound and return to beginning
580
568
  """
581
569
  # this uses FINISHED for some reason, all others use STOPPED
582
- if not self.isPlaying:
570
+ if not self._isPlaying:
583
571
  return
584
572
 
585
573
  self.track.stop()
@@ -601,11 +589,12 @@ class SoundPTB(_SoundBase):
601
589
  """Function called on End Of Stream
602
590
  """
603
591
  self._loopsFinished += 1
604
- if self.loops == 0:
605
- self.stop(reset=reset, log=False)
606
- self._isFinished = True
607
- elif 0 < self.loops <= self._loopsFinished:
592
+ if self._loopsFinished >= self._loopsRequested:
593
+ # if we have finished all requested loops
608
594
  self.stop(reset=reset, log=False)
595
+ else:
596
+ # reset _isFinished back to False
597
+ self._isFinished = False
609
598
 
610
599
  if log and self.autoLog:
611
600
  logging.exp(u"Sound %s reached end of file" % self.name, obj=self)
@@ -269,7 +269,7 @@ class SoundPygame(_SoundBase):
269
269
  def _setSndFromFile(self, fileName):
270
270
  # alias default names (so it always points to default.png)
271
271
  if fileName in ft.defaultStim:
272
- fileName = Path(prefs.paths['resources']) / ft.defaultStim[fileName]
272
+ fileName = Path(prefs.paths['assets']) / ft.defaultStim[fileName]
273
273
  # load the file
274
274
  if not path.isfile(fileName):
275
275
  msg = "Sound file %s could not be found." % fileName