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.
- psychopy/.DS_Store +0 -0
- psychopy/CHANGELOG.txt +206 -0
- psychopy/GIT_SHA +1 -0
- psychopy/VERSION +1 -0
- psychopy/__init__.py +77 -15
- psychopy/app/Resources/classic/plugin16.png +0 -0
- psychopy/app/Resources/classic/plugin16@2x.png +0 -0
- psychopy/app/Resources/dark/plugin16.png +0 -0
- psychopy/app/Resources/dark/plugin16@2x.png +0 -0
- psychopy/app/Resources/light/plugin16.png +0 -0
- psychopy/app/Resources/light/plugin16@2x.png +0 -0
- psychopy/app/__init__.py +76 -2
- psychopy/app/_psychopyApp.py +126 -101
- psychopy/app/builder/builder.py +14 -10
- psychopy/app/builder/dialogs/__init__.py +8 -8
- psychopy/app/builder/dialogs/dlgsConditions.py +12 -13
- psychopy/app/builder/dialogs/paramCtrls.py +24 -57
- psychopy/app/builder/validators.py +2 -2
- psychopy/app/coder/codeEditorBase.py +8 -8
- psychopy/app/coder/coder.py +4 -4
- psychopy/app/connections/sendusage.py +2 -2
- psychopy/app/connections/updates.py +9 -9
- psychopy/app/dialogs.py +34 -2
- psychopy/app/idle.py +31 -0
- psychopy/app/jobs.py +21 -3
- psychopy/app/linuxconfig/__init__.py +9 -0
- psychopy/app/locale/ar_001/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ar_001/LC_MESSAGE/messages.po +4602 -2540
- psychopy/app/locale/es_CO/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/es_CO/LC_MESSAGE/messages.po +56 -54
- psychopy/app/locale/es_ES/LC_MESSAGE/messages.po +53 -43
- psychopy/app/locale/es_US/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/es_US/LC_MESSAGE/messages.po +56 -54
- psychopy/app/locale/ja_JP/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ja_JP/LC_MESSAGE/messages.po +1011 -942
- psychopy/app/locale/pt_PT/LC_MESSAGE/messages.po +9415 -5
- psychopy/app/pavlovia_ui/_base.py +33 -3
- psychopy/app/pavlovia_ui/search.py +0 -1
- psychopy/app/plugin_manager/dialog.py +104 -51
- psychopy/app/plugin_manager/packages.py +5 -0
- psychopy/app/plugin_manager/plugins.py +145 -67
- psychopy/app/preferencesDlg.py +8 -8
- psychopy/app/psychopyApp.py +11 -5
- psychopy/app/ribbon.py +124 -14
- psychopy/app/runner/runner.py +6 -1
- psychopy/app/stdout/stdOutRich.py +27 -11
- psychopy/app/themes/icons.py +52 -2
- psychopy/assets/__init__.py +0 -0
- psychopy/assets/click.png +0 -0
- psychopy/assets/clicknext.png +0 -0
- psychopy/assets/next.png +0 -0
- psychopy/assets/psychopy.ico +0 -0
- psychopy/assets/psychopy.png +0 -0
- psychopy/assets/templates/__init__.py +0 -0
- psychopy/assets/touch.png +0 -0
- psychopy/assets/touchnext.png +0 -0
- psychopy/assets/window.ico +0 -0
- psychopy/changes/2023.1.0.md +9 -0
- psychopy/changes/2024.1.0.md +16 -0
- psychopy/changes/__init__.py +0 -0
- psychopy/clock.py +2 -2
- psychopy/colors.py +2 -1
- psychopy/compatibility.py +53 -1
- psychopy/contrib/.DS_Store +0 -0
- psychopy/contrib/configobj/__init__.py +10 -8
- psychopy/data/__init__.py +3 -2
- psychopy/data/base.py +5 -5
- psychopy/data/experiment.py +130 -4
- psychopy/data/routine.py +56 -0
- psychopy/data/staircase.py +2 -2
- psychopy/data/trial.py +559 -97
- psychopy/data/utils.py +56 -21
- psychopy/demos/.DS_Store +0 -0
- psychopy/demos/builder/.DS_Store +0 -0
- psychopy/demos/builder/Design Templates/.DS_Store +0 -0
- psychopy/demos/builder/Experiments/.DS_Store +0 -0
- psychopy/demos/builder/Feature Demos/.DS_Store +0 -0
- psychopy/demos/builder/Feature Demos/buttonBox/buttonBoxDemo.psyexp +375 -0
- psychopy/demos/builder/Feature Demos/buttonBox/readme.md +5 -0
- psychopy/demos/builder/Feature Demos/pilotMode/pilotMode.psyexp +433 -0
- psychopy/demos/builder/Feature Demos/pilotMode/readme.md +7 -0
- psychopy/demos/builder/Hardware/.DS_Store +0 -0
- psychopy/demos/builder/Helper Tools/.DS_Store +0 -0
- psychopy/demos/coder/.DS_Store +0 -0
- psychopy/demos/coder/hardware/testSoundLatency.py +2 -2
- psychopy/demos/coder/iohub/.DS_Store +0 -0
- psychopy/demos/coder/misc/hdf5_2_csv +33 -0
- psychopy/event.py +30 -29
- psychopy/experiment/.DS_Store +0 -0
- psychopy/experiment/_experiment.py +6 -6
- psychopy/experiment/components/.DS_Store +0 -0
- psychopy/experiment/components/__init__.py +6 -3
- psychopy/experiment/components/_base.py +286 -131
- psychopy/experiment/components/aperture/.DS_Store +0 -0
- psychopy/experiment/components/brush/.DS_Store +0 -0
- psychopy/experiment/components/button/.DS_Store +0 -0
- psychopy/experiment/components/button/__init__.py +5 -1
- psychopy/experiment/components/buttonBox/.DS_Store +0 -0
- psychopy/experiment/components/camera/.DS_Store +0 -0
- psychopy/experiment/components/code/.DS_Store +0 -0
- psychopy/experiment/components/dots/.DS_Store +0 -0
- psychopy/experiment/components/eyetracker_record/.DS_Store +0 -0
- psychopy/experiment/components/eyetracker_record/__init__.py +92 -30
- psychopy/experiment/components/form/.DS_Store +0 -0
- psychopy/experiment/components/form/__init__.py +6 -2
- psychopy/experiment/components/grating/.DS_Store +0 -0
- psychopy/experiment/components/grating/__init__.py +14 -3
- psychopy/experiment/components/image/.DS_Store +0 -0
- psychopy/experiment/components/image/__init__.py +14 -3
- psychopy/experiment/components/joyButtons/.DS_Store +0 -0
- psychopy/experiment/components/joystick/.DS_Store +0 -0
- psychopy/experiment/components/keyboard/.DS_Store +0 -0
- psychopy/experiment/components/keyboard/__init__.py +22 -10
- psychopy/experiment/components/microphone/.DS_Store +0 -0
- psychopy/experiment/components/microphone/__init__.py +59 -39
- psychopy/experiment/components/mouse/.DS_Store +0 -0
- psychopy/experiment/components/mouse/__init__.py +44 -29
- psychopy/experiment/components/movie/.DS_Store +0 -0
- psychopy/experiment/components/movie/__init__.py +1 -1
- psychopy/experiment/components/panorama/.DS_Store +0 -0
- psychopy/experiment/components/parallelOut/.DS_Store +0 -0
- psychopy/experiment/components/patch/.DS_Store +0 -0
- psychopy/experiment/components/polygon/.DS_Store +0 -0
- psychopy/experiment/components/polygon/__init__.py +26 -6
- psychopy/experiment/components/progress/.DS_Store +0 -0
- psychopy/experiment/components/ratingScale/.DS_Store +0 -0
- psychopy/experiment/components/resourceManager/.DS_Store +0 -0
- psychopy/experiment/components/roi/.DS_Store +0 -0
- psychopy/experiment/components/roi/__init__.py +5 -0
- psychopy/experiment/components/routineSettings/.DS_Store +0 -0
- psychopy/experiment/components/routineSettings/__init__.py +57 -10
- psychopy/experiment/components/serialOut/.DS_Store +0 -0
- psychopy/experiment/components/settings/.DS_Store +0 -0
- psychopy/experiment/components/settings/__init__.py +117 -42
- psychopy/experiment/components/slider/.DS_Store +0 -0
- psychopy/experiment/components/sound/.DS_Store +0 -0
- psychopy/experiment/components/sound/__init__.py +54 -19
- psychopy/experiment/components/static/.DS_Store +0 -0
- psychopy/experiment/components/static/__init__.py +1 -1
- psychopy/experiment/components/text/.DS_Store +0 -0
- psychopy/experiment/components/text/__init__.py +28 -3
- psychopy/experiment/components/textbox/.DS_Store +0 -0
- psychopy/experiment/components/textbox/__init__.py +12 -2
- psychopy/experiment/components/unknown/.DS_Store +0 -0
- psychopy/experiment/components/unknown/__init__.py +1 -2
- psychopy/experiment/components/unknownPlugin/.DS_Store +0 -0
- psychopy/experiment/components/unknownPlugin/__init__.py +2 -2
- psychopy/experiment/components/variable/.DS_Store +0 -0
- psychopy/experiment/flow.py +11 -4
- psychopy/experiment/loops.py +85 -37
- psychopy/experiment/params.py +74 -32
- psychopy/experiment/py2js_transpiler.py +8 -1
- psychopy/experiment/routines/.DS_Store +0 -0
- psychopy/experiment/routines/_base.py +102 -22
- psychopy/experiment/routines/counterbalance/.DS_Store +0 -0
- psychopy/experiment/routines/counterbalance/__init__.py +5 -1
- psychopy/experiment/routines/eyetracker_calibrate/.DS_Store +0 -0
- psychopy/experiment/routines/eyetracker_validate/.DS_Store +0 -0
- psychopy/experiment/routines/pavlovia_survey/.DS_Store +0 -0
- psychopy/experiment/routines/photodiodeValidator/.DS_Store +0 -0
- psychopy/experiment/routines/photodiodeValidator/__init__.py +6 -5
- psychopy/experiment/routines/unknown/.DS_Store +0 -0
- psychopy/gui/wxgui.py +4 -4
- psychopy/hardware/.DS_Store +0 -0
- psychopy/hardware/__init__.py +1 -1
- psychopy/hardware/base.py +12 -0
- psychopy/hardware/camera/__init__.py +1 -15
- psychopy/hardware/cedrus.py +10 -11
- psychopy/hardware/crs/colorcal.py +13 -22
- psychopy/hardware/crs/optical.py +10 -20
- psychopy/hardware/emulator.py +17 -14
- psychopy/hardware/eyetracker.py +42 -118
- psychopy/hardware/gammasci.py +4 -15
- psychopy/hardware/keyboard.py +102 -10
- psychopy/hardware/listener.py +3 -0
- psychopy/hardware/microphone.py +148 -18
- psychopy/hardware/minolta.py +8 -15
- psychopy/hardware/photodiode.py +191 -16
- psychopy/hardware/photometer/__init__.py +11 -19
- psychopy/hardware/pr.py +8 -15
- psychopy/hardware/speaker.py +39 -4
- psychopy/info.py +0 -71
- psychopy/iohub/.DS_Store +0 -0
- psychopy/iohub/__init__.py +1 -1
- psychopy/iohub/client/__init__.py +30 -20
- psychopy/iohub/client/keyboard.py +24 -24
- psychopy/iohub/datastore/__init__.py +2 -2
- psychopy/iohub/datastore/util.py +2 -2
- psychopy/iohub/default_config.yaml +1 -1
- psychopy/iohub/devices/.DS_Store +0 -0
- psychopy/iohub/devices/__init__.py +112 -25
- psychopy/iohub/devices/deviceConfigValidation.py +2 -1
- psychopy/iohub/devices/experiment/default_experiment.yaml +12 -1
- psychopy/iohub/devices/experiment/supported_config_settings.yaml +5 -1
- psychopy/iohub/devices/eyetracker/.DS_Store +0 -0
- psychopy/iohub/devices/eyetracker/__init__.py +46 -0
- psychopy/iohub/devices/eyetracker/calibration/procedure.py +2 -2
- psychopy/iohub/devices/eyetracker/hw/gazepoint/__init__.py +14 -2
- psychopy/iohub/devices/eyetracker/hw/mouse/eyetracker.py +3 -4
- psychopy/iohub/server.py +2 -2
- psychopy/iohub/start_iohub_process.py +3 -0
- psychopy/iohub/util/__init__.py +62 -70
- psychopy/layout.py +5 -5
- psychopy/logging.py +8 -1
- psychopy/microphone.py +10 -37
- psychopy/platform_specific/__init__.py +0 -2
- psychopy/platform_specific/darwin.py +1 -3
- psychopy/platform_specific/linux.py +31 -33
- psychopy/platform_specific/win32.py +38 -13
- psychopy/plugins/__init__.py +148 -116
- psychopy/plugins/util.py +39 -0
- psychopy/preferences/Darwin.spec +4 -2
- psychopy/preferences/FreeBSD.spec +4 -2
- psychopy/preferences/Linux.spec +4 -2
- psychopy/preferences/Windows.spec +4 -2
- psychopy/preferences/baseNoArch.spec +4 -2
- psychopy/preferences/preferences.py +47 -24
- psychopy/projects/pavlovia.py +47 -4
- psychopy/scripts/psyexpCompile.py +0 -4
- psychopy/session.py +153 -21
- psychopy/sound/__init__.py +31 -21
- psychopy/sound/_base.py +20 -3
- psychopy/sound/audioclip.py +320 -33
- psychopy/sound/backend_ptb.py +47 -58
- psychopy/sound/backend_pygame.py +1 -1
- psychopy/sound/backend_pysound.py +6 -15
- psychopy/sound/transcribe.py +53 -0
- psychopy/tests/.DS_Store +0 -0
- psychopy/tests/data/.DS_Store +0 -0
- psychopy/tests/data/TestUnknownPluginComponent_load_resave.psyexp +135 -0
- psychopy/tests/data/Test_textbox/test_ori_0_bottom right.png +0 -0
- psychopy/tests/data/Test_textbox/test_ori_0_center.png +0 -0
- psychopy/tests/data/Test_textbox/test_ori_0_top left.png +0 -0
- psychopy/tests/data/Test_textbox/test_ori_120_bottom right.png +0 -0
- psychopy/tests/data/Test_textbox/test_ori_120_center.png +0 -0
- psychopy/tests/data/Test_textbox/test_ori_120_top left.png +0 -0
- psychopy/tests/data/Test_textbox/test_ori_180_bottom right.png +0 -0
- psychopy/tests/data/Test_textbox/test_ori_180_center.png +0 -0
- psychopy/tests/data/Test_textbox/test_ori_180_top left.png +0 -0
- psychopy/tests/data/Test_textbox/test_ori_240_bottom right.png +0 -0
- psychopy/tests/data/Test_textbox/test_ori_240_center.png +0 -0
- psychopy/tests/data/Test_textbox/test_ori_240_top left.png +0 -0
- psychopy/tests/data/correctScript/.DS_Store +0 -0
- psychopy/tests/data/test_components/testClearKeyboard/testClearKeyboard.psyexp +200 -0
- psychopy/tests/data/test_session/.DS_Store +0 -0
- psychopy/tests/data/test_session/root/testFutureTrials/testFutureTrials.psyexp +155 -0
- psychopy/tests/data/test_session/root/testTrialNav/trialNav.psyexp +158 -0
- psychopy/tests/test_app/.DS_Store +0 -0
- psychopy/tests/test_app/conftest.py +2 -2
- psychopy/tests/test_app/test_speed.py +4 -1
- psychopy/tests/test_data/test_TrialHandler2.py +146 -1
- psychopy/tests/test_experiment/.DS_Store +0 -0
- psychopy/tests/test_experiment/needs_wx/genComponsTemplate.py +3 -3
- psychopy/tests/test_experiment/needs_wx/test_components.py +2 -2
- psychopy/tests/test_experiment/test_components/test_KeyboardComponent.py +28 -0
- psychopy/tests/test_experiment/test_components/test_UnknownPluginComponent.py +27 -0
- psychopy/tests/test_experiment/test_components/test_base_components.py +58 -0
- psychopy/tests/test_experiment/test_py2js.py +1 -1
- psychopy/tests/test_hardware/test_keyboard.py +31 -0
- psychopy/tests/test_hardware/test_ports.py +1 -11
- psychopy/tests/test_liaison/test_Liaison.py +47 -0
- psychopy/tests/test_misc/test_core.py +5 -0
- psychopy/tests/test_session/test_Session.py +5 -1
- psychopy/tests/test_tools/test_versionchooser.py +39 -8
- psychopy/tests/test_visual/test_all_stimuli.py +0 -97
- psychopy/tests/test_visual/test_image.py +6 -5
- psychopy/tests/test_visual/test_textbox.py +36 -0
- psychopy/tests/utils.py +4 -0
- psychopy/tools/filetools.py +1 -1
- psychopy/tools/pkgtools.py +160 -137
- psychopy/tools/versionchooser.py +10 -10
- psychopy/tools/wizard.py +3 -3
- psychopy/visual/.DS_Store +0 -0
- psychopy/visual/backends/pygletbackend.py +24 -13
- psychopy/visual/basevisual.py +5 -11
- psychopy/visual/button.py +2 -14
- psychopy/visual/helpers.py +5 -5
- psychopy/visual/line.py +1 -2
- psychopy/visual/movie2.py +7 -816
- psychopy/visual/movie3.py +7 -589
- psychopy/visual/movies/__init__.py +8 -11
- psychopy/visual/movies/frame.py +5 -2
- psychopy/visual/movies/players/ffpyplayer_player.py +5 -2
- psychopy/visual/noise.py +8 -7
- psychopy/visual/patch.py +7 -16
- psychopy/visual/radial.py +9 -7
- psychopy/visual/ratingscale.py +8 -1415
- psychopy/visual/secondorder.py +10 -9
- psychopy/visual/shape.py +7 -2
- psychopy/visual/text.py +1 -1
- psychopy/visual/textbox2/textbox2.py +28 -5
- {psychopy-2024.1.4.dist-info → psychopy-2024.2.0.dist-info}/METADATA +8 -13
- {psychopy-2024.1.4.dist-info → psychopy-2024.2.0.dist-info}/RECORD +307 -213
- {psychopy-2024.1.4.dist-info → psychopy-2024.2.0.dist-info}/WHEEL +1 -1
- psychopy/app/Resources/click.png +0 -0
- psychopy/app/Resources/next.png +0 -0
- psychopy/experiment/components/patch/__init__.py +0 -121
- psychopy/experiment/components/patch/classic/patch.png +0 -0
- psychopy/experiment/components/patch/dark/patch.png +0 -0
- psychopy/experiment/components/patch/dark/patch@2x.png +0 -0
- psychopy/experiment/components/patch/light/patch.png +0 -0
- psychopy/experiment/components/patch/light/patch@2x.png +0 -0
- psychopy/experiment/components/ratingScale/__init__.py +0 -337
- psychopy/experiment/components/ratingScale/classic/ratingscale.png +0 -0
- psychopy/experiment/components/ratingScale/classic/ratingscale@2x.png +0 -0
- psychopy/experiment/components/ratingScale/dark/ratingScale@2x.png +0 -0
- psychopy/experiment/components/ratingScale/dark/ratingscale.png +0 -0
- psychopy/experiment/components/ratingScale/light/ratingScale@2x.png +0 -0
- psychopy/experiment/components/ratingScale/light/ratingscale.png +0 -0
- psychopy/platform_specific/posix.py +0 -16
- psychopy/tests/test_sound/test_microphone.py +0 -217
- psychopy/tests/test_visual/test_ratingScale.py +0 -299
- /psychopy/{app/Resources → assets}/Psychopy Window Favicon@16w.png +0 -0
- /psychopy/{app/Resources → assets}/Psychopy Window Favicon@32w.png +0 -0
- /psychopy/{app/Resources → assets}/USB-C.png +0 -0
- /psychopy/{app/Resources → assets}/USB.png +0 -0
- /psychopy/{app/Resources → assets}/creditCard.png +0 -0
- /psychopy/{app/Resources → assets}/default.mp3 +0 -0
- /psychopy/{app/Resources → assets}/default.mp4 +0 -0
- /psychopy/{app/Resources → assets}/default.png +0 -0
- /psychopy/{app/Resources → assets/templates}/instruct1.png +0 -0
- /psychopy/{app/Resources → assets/templates}/instruct2.png +0 -0
- {psychopy-2024.1.4.dist-info → psychopy-2024.2.0.dist-info}/entry_points.txt +0 -0
- {psychopy-2024.1.4.dist-info → psychopy-2024.2.0.dist-info}/licenses/AUTHORS.md +0 -0
- {psychopy-2024.1.4.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
|
-
#
|
|
215
|
-
self.sampleRate
|
|
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"
|
psychopy/sound/audioclip.py
CHANGED
|
@@ -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['
|
|
1116
|
+
filename = Path(prefs.paths['assets']) / ft.defaultStim[filename]
|
|
830
1117
|
return AudioClip.load(filename, codec)
|
|
831
1118
|
|
|
832
1119
|
|
psychopy/sound/backend_ptb.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
212
|
+
if isinstance(device, list) and len(device):
|
|
213
213
|
device = device[0]
|
|
214
|
-
if
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
|
247
|
+
raise e
|
|
248
248
|
except Exception as e:
|
|
249
249
|
audio.Stream.__init__(self, mode=mode+8,
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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.
|
|
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
|
|
387
|
+
if val is True:
|
|
407
388
|
self.__dict__['channels'] = 2
|
|
408
|
-
elif val
|
|
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['
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
605
|
-
|
|
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)
|
psychopy/sound/backend_pygame.py
CHANGED
|
@@ -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['
|
|
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
|