psychopy 2024.2.5__py3-none-any.whl → 2025.1.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/CHANGELOG.txt +4 -4
- psychopy/GIT_SHA +1 -1
- psychopy/LICENSE.txt +1 -1
- psychopy/VERSION +1 -1
- psychopy/__init__.py +10 -7
- psychopy/alerts/__init__.py +1 -1
- psychopy/alerts/_alerts.py +53 -17
- psychopy/alerts/_errorHandler.py +3 -4
- psychopy/alerts/alertsCatalogue/3210.yaml +27 -0
- psychopy/alerts/alertsCatalogue/3610.yaml +19 -0
- psychopy/alerts/alertsCatalogue/4130.yaml +19 -0
- psychopy/alerts/alertsCatalogue/alertCategories.yaml +8 -1
- psychopy/alerts/alertsCatalogue/alertmsg.py +1 -1
- psychopy/alerts/alerttools.py +0 -16
- psychopy/app/Resources/betasplash.png +0 -0
- psychopy/app/Resources/betasplash@2x.png +0 -0
- psychopy/app/Resources/classic/case.png +0 -0
- psychopy/app/Resources/classic/case@2x.png +0 -0
- psychopy/app/Resources/classic/fileaudio.png +0 -0
- psychopy/app/Resources/classic/fileaudio@2x.png +0 -0
- psychopy/app/Resources/classic/filecss.png +0 -0
- psychopy/app/Resources/classic/filecss@2x.png +0 -0
- psychopy/app/Resources/classic/filecsv.png +0 -0
- psychopy/app/Resources/classic/filecsv@2x.png +0 -0
- psychopy/app/Resources/classic/filedesign.png +0 -0
- psychopy/app/Resources/classic/filedesign@2x.png +0 -0
- psychopy/app/Resources/classic/filefont.png +0 -0
- psychopy/app/Resources/classic/filefont@2x.png +0 -0
- psychopy/app/Resources/classic/filegit.png +0 -0
- psychopy/app/Resources/classic/filegit@2x.png +0 -0
- psychopy/app/Resources/classic/filehtml.png +0 -0
- psychopy/app/Resources/classic/filehtml@2x.png +0 -0
- psychopy/app/Resources/classic/fileimage.png +0 -0
- psychopy/app/Resources/classic/fileimage@2x.png +0 -0
- psychopy/app/Resources/classic/fileinfo.png +0 -0
- psychopy/app/Resources/classic/fileinfo@2x.png +0 -0
- psychopy/app/Resources/classic/filejs.png +0 -0
- psychopy/app/Resources/classic/filejs@2x.png +0 -0
- psychopy/app/Resources/classic/filejson.png +0 -0
- psychopy/app/Resources/classic/filejson@2x.png +0 -0
- psychopy/app/Resources/classic/filepkg.png +0 -0
- psychopy/app/Resources/classic/filepkg@2x.png +0 -0
- psychopy/app/Resources/classic/filepsyexp.png +0 -0
- psychopy/app/Resources/classic/filepsyexp@2x.png +0 -0
- psychopy/app/Resources/classic/filepy.png +0 -0
- psychopy/app/Resources/classic/filepy@2x.png +0 -0
- psychopy/app/Resources/classic/filetxt.png +0 -0
- psychopy/app/Resources/classic/filetxt@2x.png +0 -0
- psychopy/app/Resources/classic/fileunknown.png +0 -0
- psychopy/app/Resources/classic/fileunknown@2x.png +0 -0
- psychopy/app/Resources/classic/filevideo.png +0 -0
- psychopy/app/Resources/classic/filevideo@2x.png +0 -0
- psychopy/app/Resources/classic/find.png +0 -0
- psychopy/app/Resources/classic/find@2x.png +0 -0
- psychopy/app/Resources/classic/loop.png +0 -0
- psychopy/app/Resources/classic/loop@2x.png +0 -0
- psychopy/app/Resources/classic/regex.png +0 -0
- psychopy/app/Resources/classic/regex@2x.png +0 -0
- psychopy/app/Resources/dark/case.png +0 -0
- psychopy/app/Resources/dark/case@2x.png +0 -0
- psychopy/app/Resources/dark/fileaudio.png +0 -0
- psychopy/app/Resources/dark/fileaudio@2x.png +0 -0
- psychopy/app/Resources/dark/filecss.png +0 -0
- psychopy/app/Resources/dark/filecss@2x.png +0 -0
- psychopy/app/Resources/dark/filecsv.png +0 -0
- psychopy/app/Resources/dark/filecsv@2x.png +0 -0
- psychopy/app/Resources/dark/filedesign.png +0 -0
- psychopy/app/Resources/dark/filedesign@2x.png +0 -0
- psychopy/app/Resources/dark/filefont.png +0 -0
- psychopy/app/Resources/dark/filefont@2x.png +0 -0
- psychopy/app/Resources/dark/filegit.png +0 -0
- psychopy/app/Resources/dark/filegit@2x.png +0 -0
- psychopy/app/Resources/dark/filehtml.png +0 -0
- psychopy/app/Resources/dark/filehtml@2x.png +0 -0
- psychopy/app/Resources/dark/fileimage.png +0 -0
- psychopy/app/Resources/dark/fileimage@2x.png +0 -0
- psychopy/app/Resources/dark/fileinfo.png +0 -0
- psychopy/app/Resources/dark/fileinfo@2x.png +0 -0
- psychopy/app/Resources/dark/filejs.png +0 -0
- psychopy/app/Resources/dark/filejs@2x.png +0 -0
- psychopy/app/Resources/dark/filejson.png +0 -0
- psychopy/app/Resources/dark/filejson@2x.png +0 -0
- psychopy/app/Resources/dark/filepkg.png +0 -0
- psychopy/app/Resources/dark/filepkg@2x.png +0 -0
- psychopy/app/Resources/dark/filepsyexp.png +0 -0
- psychopy/app/Resources/dark/filepsyexp@2x.png +0 -0
- psychopy/app/Resources/dark/filepy.png +0 -0
- psychopy/app/Resources/dark/filepy@2x.png +0 -0
- psychopy/app/Resources/dark/filetxt.png +0 -0
- psychopy/app/Resources/dark/filetxt@2x.png +0 -0
- psychopy/app/Resources/dark/fileunknown.png +0 -0
- psychopy/app/Resources/dark/fileunknown@2x.png +0 -0
- psychopy/app/Resources/dark/filevideo.png +0 -0
- psychopy/app/Resources/dark/filevideo@2x.png +0 -0
- psychopy/app/Resources/dark/find.png +0 -0
- psychopy/app/Resources/dark/find@2x.png +0 -0
- psychopy/app/Resources/dark/loop.png +0 -0
- psychopy/app/Resources/dark/loop@2x.png +0 -0
- psychopy/app/Resources/dark/regex.png +0 -0
- psychopy/app/Resources/dark/regex@2x.png +0 -0
- psychopy/app/Resources/light/case.png +0 -0
- psychopy/app/Resources/light/case@2x.png +0 -0
- psychopy/app/Resources/light/fileaudio.png +0 -0
- psychopy/app/Resources/light/fileaudio@2x.png +0 -0
- psychopy/app/Resources/light/filecss.png +0 -0
- psychopy/app/Resources/light/filecss@2x.png +0 -0
- psychopy/app/Resources/light/filecsv.png +0 -0
- psychopy/app/Resources/light/filecsv@2x.png +0 -0
- psychopy/app/Resources/light/filedesign.png +0 -0
- psychopy/app/Resources/light/filedesign@2x.png +0 -0
- psychopy/app/Resources/light/filefont.png +0 -0
- psychopy/app/Resources/light/filefont@2x.png +0 -0
- psychopy/app/Resources/light/filegit.png +0 -0
- psychopy/app/Resources/light/filegit@2x.png +0 -0
- psychopy/app/Resources/light/filehtml.png +0 -0
- psychopy/app/Resources/light/filehtml@2x.png +0 -0
- psychopy/app/Resources/light/fileimage.png +0 -0
- psychopy/app/Resources/light/fileimage@2x.png +0 -0
- psychopy/app/Resources/light/fileinfo.png +0 -0
- psychopy/app/Resources/light/fileinfo@2x.png +0 -0
- psychopy/app/Resources/light/filejs.png +0 -0
- psychopy/app/Resources/light/filejs@2x.png +0 -0
- psychopy/app/Resources/light/filejson.png +0 -0
- psychopy/app/Resources/light/filejson@2x.png +0 -0
- psychopy/app/Resources/light/filepkg.png +0 -0
- psychopy/app/Resources/light/filepkg@2x.png +0 -0
- psychopy/app/Resources/light/filepsyexp.png +0 -0
- psychopy/app/Resources/light/filepsyexp@2x.png +0 -0
- psychopy/app/Resources/light/filepy.png +0 -0
- psychopy/app/Resources/light/filepy@2x.png +0 -0
- psychopy/app/Resources/light/filetxt.png +0 -0
- psychopy/app/Resources/light/filetxt@2x.png +0 -0
- psychopy/app/Resources/light/fileunknown.png +0 -0
- psychopy/app/Resources/light/fileunknown@2x.png +0 -0
- psychopy/app/Resources/light/filevideo.png +0 -0
- psychopy/app/Resources/light/filevideo@2x.png +0 -0
- psychopy/app/Resources/light/find.png +0 -0
- psychopy/app/Resources/light/find@2x.png +0 -0
- psychopy/app/Resources/light/loop.png +0 -0
- psychopy/app/Resources/light/loop@2x.png +0 -0
- psychopy/app/Resources/light/regex.png +0 -0
- psychopy/app/Resources/light/regex@2x.png +0 -0
- psychopy/app/Resources/routine_templates/Basic.psyexp +0 -1
- psychopy/app/Resources/routine_templates/Misc.psyexp +0 -1
- psychopy/app/Resources/routine_templates/Online.psyexp +0 -2
- psychopy/app/Resources/routine_templates/Trials.psyexp +0 -1
- psychopy/app/Resources/splash.png +0 -0
- psychopy/app/Resources/splash@2x.png +0 -0
- psychopy/app/__init__.py +49 -8
- psychopy/app/__main__.py +3 -0
- psychopy/app/_psychopyApp.py +134 -125
- psychopy/app/builder/builder.py +42 -22
- psychopy/app/builder/dialogs/__init__.py +44 -11
- psychopy/app/builder/dialogs/dlgsCode.py +1 -1
- psychopy/app/builder/dialogs/dlgsConditions.py +1 -1
- psychopy/app/builder/dialogs/findDlg.py +106 -20
- psychopy/app/builder/dialogs/paramCtrls.py +42 -1
- psychopy/app/builder/validators.py +1 -1
- psychopy/app/coder/codeEditorBase.py +8 -8
- psychopy/app/coder/coder.py +32 -29
- psychopy/app/coder/fileBrowser.py +68 -22
- psychopy/app/coder/folding.py +1 -1
- psychopy/app/coder/psychoParser.py +1 -1
- psychopy/app/coder/repl.py +1 -1
- psychopy/app/coder/sourceTree.py +1 -1
- psychopy/app/connections/__init__.py +1 -1
- psychopy/app/connections/news.py +1 -1
- psychopy/app/connections/sendusage.py +1 -1
- psychopy/app/connections/updates.py +1 -1
- psychopy/app/console.py +1 -1
- psychopy/app/errorDlg.py +1 -1
- psychopy/app/frametracker.py +1 -1
- psychopy/app/idle.py +1 -1
- psychopy/app/jobs.py +5 -2
- psychopy/app/linuxconfig/__init__.py +1 -1
- psychopy/app/locale/ar_001/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ar_001/LC_MESSAGE/messages.po +4 -4
- psychopy/app/locale/cs_CZ/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/da_DK/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/de_DE/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/de_DE/LC_MESSAGE/messages.po +3 -3
- psychopy/app/locale/el_GR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/en_NZ/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/en_US/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/es_CO/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/es_CO/LC_MESSAGE/messages.po +4 -4
- psychopy/app/locale/es_ES/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/es_ES/LC_MESSAGE/messages.po +4 -4
- psychopy/app/locale/es_US/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/es_US/LC_MESSAGE/messages.po +4 -4
- psychopy/app/locale/et_EE/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/et_EE/LC_MESSAGE/messages.po +2 -2
- psychopy/app/locale/fa_IR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/fa_IR/LC_MESSAGE/messages.po +1 -1
- psychopy/app/locale/fi_FI/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/fr_FR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/fr_FR/LC_MESSAGE/messages.po +2 -2
- psychopy/app/locale/he_IL/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/he_IL/LC_MESSAGE/messages.po +2 -2
- psychopy/app/locale/hi_IN/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/hi_IN/LC_MESSAGE/messages.po +2 -2
- psychopy/app/locale/hu_HU/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/it_IT/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/it_IT/LC_MESSAGE/messages.po +2 -2
- psychopy/app/locale/ja_JP/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ja_JP/LC_MESSAGE/messages.po +3421 -2396
- psychopy/app/locale/ko_KR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ms_MY/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ms_MY/LC_MESSAGE/messages.po +2 -2
- psychopy/app/locale/nl_NL/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/nn_NO/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/pl_PL/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/pt_PT/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/pt_PT/LC_MESSAGE/messages.po +4 -4
- psychopy/app/locale/ro_RO/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/ru_RU/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/sv_SE/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/sv_SE/LC_MESSAGE/messages.po +2 -2
- psychopy/app/locale/tr_TR/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/tr_TR/LC_MESSAGE/messages.po +2 -2
- psychopy/app/locale/zh_CN/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/zh_CN/LC_MESSAGE/messages.po +3 -3
- psychopy/app/locale/zh_TW/LC_MESSAGE/messages.mo +0 -0
- psychopy/app/locale/zh_TW/LC_MESSAGE/messages.po +2 -2
- psychopy/app/{builder/localizedStrings.py → localizedStrings.py} +173 -26
- psychopy/app/pavlovia_ui/__init__.py +1 -1
- psychopy/app/pavlovia_ui/_base.py +1 -1
- psychopy/app/pavlovia_ui/functions.py +1 -1
- psychopy/app/pavlovia_ui/menu.py +1 -1
- psychopy/app/pavlovia_ui/project.py +1 -1
- psychopy/app/pavlovia_ui/search.py +1 -1
- psychopy/app/pavlovia_ui/sync.py +1 -1
- psychopy/app/pavlovia_ui/user.py +1 -1
- psychopy/app/plugin_manager/dialog.py +34 -96
- psychopy/app/plugin_manager/packages.py +10 -14
- psychopy/app/plugin_manager/plugins.py +64 -4
- psychopy/app/preferencesDlg.py +12 -37
- psychopy/app/psychopyApp.py +130 -44
- psychopy/app/ribbon.py +1 -0
- psychopy/app/runner/runner.py +19 -7
- psychopy/app/runner/scriptProcess.py +11 -6
- psychopy/app/stdout/stdOutRich.py +9 -2
- psychopy/app/themes/fonts.py +1 -1
- psychopy/app/themes/icons.py +2 -38
- psychopy/app/ui/__init__.py +1 -1
- psychopy/app/utils.py +3 -3
- psychopy/assets/default.mp3 +0 -0
- psychopy/assets/fonts/NotoSans-Bold.ttf +0 -0
- psychopy/assets/fonts/NotoSans-BoldItalic.ttf +0 -0
- psychopy/assets/fonts/NotoSans-Italic.ttf +0 -0
- psychopy/assets/fonts/NotoSans-Regular.ttf +0 -0
- psychopy/assets/voicekeyThresholdStim.wav +0 -0
- psychopy/clock.py +4 -1
- psychopy/colors.py +2 -0
- psychopy/core.py +1 -1
- psychopy/data/base.py +1 -1
- psychopy/data/experiment.py +151 -46
- psychopy/data/routine.py +27 -1
- psychopy/data/staircase.py +2 -1
- psychopy/data/trial.py +62 -14
- psychopy/data/utils.py +1 -1
- psychopy/demos/builder/Design Templates/branchedExperiment/branchedExperiment.psyexp +333 -218
- psychopy/demos/builder/Design Templates/psychophysicsStaircase/psychophysicsStaircase.psyexp +261 -239
- psychopy/demos/builder/Design Templates/psychophysicsStairsInterleaved/psychophysicsStaircaseInterleaved.psyexp +319 -180
- psychopy/demos/builder/Design Templates/randomisedBlocks/randomisedBlocks.psyexp +204 -116
- psychopy/demos/builder/Experiments/BART/assets/background.jpg +0 -0
- psychopy/demos/builder/Experiments/BART/assets/blueBalloon.png +0 -0
- psychopy/demos/builder/Experiments/BART/assets/greenBalloon.png +0 -0
- psychopy/demos/builder/Experiments/BART/assets/redBalloon.png +0 -0
- psychopy/demos/builder/Experiments/BART/bart.psyexp +779 -866
- psychopy/demos/builder/Experiments/BigFiveInventory/BFI.psyexp +242 -180
- psychopy/demos/builder/Experiments/GoNoGo/gng.psyexp +419 -406
- psychopy/demos/builder/Experiments/dragAndDrop/README.md +2 -37
- psychopy/demos/builder/Experiments/dragAndDrop/drag_and_drop.psyexp +460 -1204
- psychopy/demos/builder/Experiments/dragAndDrop/stimuli/blank_grid.png +0 -0
- psychopy/demos/builder/Experiments/dragAndDrop/stimuli/make_shapes.psyexp +221 -0
- psychopy/demos/builder/Experiments/dragAndDrop/stimuli/readme.md +4 -0
- psychopy/demos/builder/Experiments/dragAndDrop/stimuli/solution_1.png +0 -0
- psychopy/demos/builder/Experiments/dragAndDrop/stimuli/solution_10.png +0 -0
- psychopy/demos/builder/Experiments/dragAndDrop/stimuli/solution_2.png +0 -0
- psychopy/demos/builder/Experiments/dragAndDrop/stimuli/solution_3.png +0 -0
- psychopy/demos/builder/Experiments/dragAndDrop/stimuli/solution_4.png +0 -0
- psychopy/demos/builder/Experiments/dragAndDrop/stimuli/solution_5.png +0 -0
- psychopy/demos/builder/Experiments/dragAndDrop/stimuli/solution_6.png +0 -0
- psychopy/demos/builder/Experiments/dragAndDrop/stimuli/solution_7.png +0 -0
- psychopy/demos/builder/Experiments/dragAndDrop/stimuli/solution_8.png +0 -0
- psychopy/demos/builder/Experiments/dragAndDrop/stimuli/solution_9.png +0 -0
- psychopy/demos/builder/Experiments/dragAndDrop/stimuli/solutions.xlsx +0 -0
- psychopy/demos/builder/Experiments/mentalRotation/MentalRotation.psyexp +583 -542
- psychopy/demos/builder/Experiments/navon/NavonTask.psyexp +458 -427
- psychopy/demos/builder/Experiments/sternberg/sternberg.psyexp +588 -550
- psychopy/demos/builder/Experiments/stroop/stroop.psyexp +303 -207
- psychopy/demos/builder/Experiments/stroopExtended/stroop.psyexp +390 -215
- psychopy/demos/builder/Experiments/stroopExtended/stroopReverse.psyexp +390 -215
- psychopy/demos/builder/Experiments/stroopVoice/stroopVoice.psyexp +357 -331
- psychopy/demos/builder/Feature Demos/buttonBox/buttonBoxDemo.psyexp +3 -2
- psychopy/demos/builder/Feature Demos/counterbalance/counterbalance.psyexp +287 -277
- psychopy/demos/builder/Feature Demos/gratings/gratings.psyexp +370 -320
- psychopy/demos/builder/Feature Demos/noise/noise.psyexp +452 -399
- psychopy/demos/builder/Feature Demos/panorama/panorama.psyexp +168 -133
- psychopy/demos/builder/Feature Demos/pilotMode/pilotMode.psyexp +4 -3
- psychopy/demos/builder/Feature Demos/progress/progressBar.psyexp +420 -392
- psychopy/demos/builder/Feature Demos/sliders/sliders.psyexp +917 -871
- psychopy/demos/builder/Feature Demos/visualValidator/readme.md +7 -0
- psychopy/demos/builder/Feature Demos/visualValidator/visualValidator.psyexp +200 -0
- psychopy/demos/builder/Hardware/EEG_parallel_component/EEG_triggers_parallel_comp.psyexp +25 -9
- psychopy/demos/builder/Hardware/EEG_serial_code/EEG_triggers_serial_code.psyexp +372 -361
- psychopy/demos/builder/Hardware/EEG_serial_component/EEG_triggers_serial_comp.psyexp +25 -9
- psychopy/demos/builder/Hardware/EGI_netstation/stroop.psyexp +320 -235
- psychopy/demos/builder/Hardware/Eyetracking_visual_search/visualSearch.psyexp +790 -651
- psychopy/demos/builder/Hardware/camera/camera.psyexp +326 -246
- psychopy/demos/builder/Hardware/eyetracking/eyetracking.psyexp +432 -327
- psychopy/demos/builder/Hardware/eyetracking_custom_cal/eyetracking_custom_cal.psyexp +440 -321
- psychopy/demos/builder/Hardware/fMRI/fMRI_demo.psyexp +190 -181
- psychopy/demos/builder/Hardware/lab_streaming_layer/lsl_triggers_demo.psyexp +323 -312
- psychopy/demos/builder/Hardware/lab_streaming_layer/lsl_triggers_demo_legacy.psyexp +360 -0
- psychopy/demos/builder/Hardware/lab_streaming_layer/lsl_triggers_demo_legacy_legacy.psyexp +312 -0
- psychopy/demos/builder/Hardware/lab_streaming_layer_legacy/lsl_triggers_demo_legacy.psyexp +329 -273
- psychopy/demos/builder/Hardware/lab_streaming_layer_legacy/lsl_triggers_demo_legacy_legacy.psyexp +360 -0
- psychopy/demos/builder/Hardware/lab_streaming_layer_legacy/lsl_triggers_demo_legacy_legacy_legacy.psyexp +312 -0
- psychopy/demos/builder/Hardware/microphone/microphone.psyexp +450 -404
- psychopy/demos/builder/Hardware/pump/pump.psyexp +593 -329
- psychopy/demos/builder/Helper Tools/achorVSalignment/FlowCircular-Regular.ttf +0 -0
- psychopy/demos/builder/Helper Tools/achorVSalignment/anchorAlignment.psyexp +336 -274
- psychopy/demos/builder/Helper Tools/clockFace/clockFace.psyexp +200 -149
- psychopy/demos/builder/Helper Tools/colors/colors.psyexp +300 -279
- psychopy/demos/builder/Helper Tools/drawPolygon/drawPolygon.psyexp +677 -564
- psychopy/demos/builder/Helper Tools/keyNameFinder/keyNameFinder.psyexp +214 -158
- psychopy/demos/builder/Helper Tools/spatialUnits/unitDemo.psyexp +195 -146
- psychopy/demos/coder/experiment control/runtimeInfo.py +1 -1
- psychopy/devices/__init__.py +1 -1
- psychopy/event.py +1 -1
- psychopy/exceptions.py +1 -1
- psychopy/experiment/__init__.py +1 -1
- psychopy/experiment/_experiment.py +30 -9
- psychopy/experiment/components/__init__.py +1 -6
- psychopy/experiment/components/_base.py +44 -19
- psychopy/experiment/components/aperture/__init__.py +1 -1
- psychopy/experiment/components/brush/__init__.py +2 -2
- psychopy/experiment/components/button/__init__.py +24 -28
- psychopy/experiment/components/camera/__init__.py +38 -39
- psychopy/experiment/components/code/__init__.py +1 -1
- psychopy/experiment/components/dots/__init__.py +1 -1
- psychopy/experiment/components/eyetracker_record/__init__.py +7 -3
- psychopy/experiment/components/form/__init__.py +3 -3
- psychopy/experiment/components/grating/__init__.py +1 -1
- psychopy/experiment/components/image/__init__.py +1 -1
- psychopy/experiment/components/joyButtons/__init__.py +2 -2
- psychopy/experiment/components/joyButtons/virtualJoyButtons.py +1 -1
- psychopy/experiment/components/joystick/__init__.py +3 -3
- psychopy/experiment/components/joystick/virtualJoystick.py +1 -1
- psychopy/experiment/components/keyboard/__init__.py +98 -122
- psychopy/experiment/components/microphone/__init__.py +54 -98
- psychopy/experiment/components/mouse/__init__.py +92 -93
- psychopy/experiment/components/movie/__init__.py +28 -50
- psychopy/experiment/components/parallelOut/__init__.py +3 -3
- psychopy/experiment/components/polygon/__init__.py +2 -3
- psychopy/experiment/components/roi/__init__.py +2 -2
- psychopy/experiment/components/routineSettings/__init__.py +2 -0
- psychopy/experiment/components/serialOut/__init__.py +7 -7
- psychopy/experiment/components/settings/__init__.py +317 -313
- psychopy/experiment/components/settings/eyetracking.py +108 -0
- psychopy/experiment/components/slider/__init__.py +5 -5
- psychopy/experiment/components/sound/__init__.py +168 -78
- psychopy/experiment/components/soundsensor/__init__.py +361 -0
- psychopy/experiment/components/soundsensor/classic/soundsensor.png +0 -0
- psychopy/experiment/components/soundsensor/classic/soundsensor@2x.png +0 -0
- psychopy/experiment/components/soundsensor/dark/soundsensor.png +0 -0
- psychopy/experiment/components/soundsensor/dark/soundsensor@2x.png +0 -0
- psychopy/experiment/components/soundsensor/light/soundsensor.png +0 -0
- psychopy/experiment/components/soundsensor/light/soundsensor@2x.png +0 -0
- psychopy/experiment/components/static/__init__.py +59 -33
- psychopy/experiment/components/text/__init__.py +2 -6
- psychopy/experiment/components/textbox/__init__.py +3 -7
- psychopy/experiment/components/unknown/__init__.py +2 -0
- psychopy/experiment/components/unknownPlugin/__init__.py +2 -0
- psychopy/experiment/exports.py +1 -1
- psychopy/experiment/flow.py +2 -2
- psychopy/experiment/localization.py +1 -1
- psychopy/experiment/loops.py +43 -10
- psychopy/experiment/params.py +6 -4
- psychopy/experiment/plugins.py +8 -1
- psychopy/experiment/py2js.py +1 -1
- psychopy/experiment/py2js_transpiler.py +1 -1
- psychopy/experiment/routines/__init__.py +1 -1
- psychopy/experiment/routines/_base.py +23 -24
- psychopy/experiment/routines/audioValidator/__init__.py +343 -0
- psychopy/experiment/routines/audioValidator/classic/audio_validator.png +0 -0
- psychopy/experiment/routines/audioValidator/classic/audio_validator@2x.png +0 -0
- psychopy/experiment/routines/audioValidator/dark/audio_validator.png +0 -0
- psychopy/experiment/routines/audioValidator/dark/audio_validator@2x.png +0 -0
- psychopy/experiment/routines/audioValidator/light/audio_validator.png +0 -0
- psychopy/experiment/routines/audioValidator/light/audio_validator@2x.png +0 -0
- psychopy/experiment/routines/eyetracker_calibrate/__init__.py +76 -17
- psychopy/experiment/routines/eyetracker_validate/__init__.py +1 -1
- psychopy/experiment/routines/unknown/__init__.py +2 -0
- psychopy/experiment/routines/{photodiodeValidator → visualValidator}/__init__.py +89 -120
- psychopy/experiment/routines/visualValidator/classic/visual_validator.png +0 -0
- psychopy/experiment/routines/visualValidator/classic/visual_validator@2x.png +0 -0
- psychopy/experiment/routines/visualValidator/dark/visual_validator.png +0 -0
- psychopy/experiment/routines/visualValidator/dark/visual_validator@2x.png +0 -0
- psychopy/experiment/routines/visualValidator/light/visual_validator.png +0 -0
- psychopy/experiment/routines/visualValidator/light/visual_validator@2x.png +0 -0
- psychopy/experiment/utils.py +1 -1
- psychopy/gui/__init__.py +1 -1
- psychopy/gui/qtgui.py +15 -6
- psychopy/gui/wxgui.py +1 -1
- psychopy/hardware/__init__.py +0 -1
- psychopy/hardware/base.py +147 -16
- psychopy/hardware/bbtk/__init__.py +10 -16
- psychopy/hardware/brainproducts.py +7 -13
- psychopy/hardware/button.py +21 -2
- psychopy/hardware/buttonbox/__init__.py +1 -1
- psychopy/hardware/camera/__init__.py +16 -3
- psychopy/hardware/cedrus.py +5 -16
- psychopy/hardware/crs/__init__.py +1 -1
- psychopy/hardware/crs/bits.py +36 -33
- psychopy/hardware/crs/colorcal.py +8 -11
- psychopy/hardware/crs/optical.py +7 -10
- psychopy/hardware/crs/shaders.py +18 -5
- psychopy/hardware/emotiv.py +1 -1
- psychopy/hardware/emulator.py +11 -5
- psychopy/hardware/exceptions.py +86 -0
- psychopy/hardware/forp.py +18 -23
- psychopy/hardware/gammasci.py +8 -3
- psychopy/hardware/iolab.py +8 -19
- psychopy/hardware/joystick/__init__.py +865 -266
- psychopy/hardware/joystick/_base.py +251 -0
- psychopy/hardware/joystick/backend_glfw.py +306 -0
- psychopy/hardware/joystick/backend_pyglet.py +309 -0
- psychopy/hardware/joystick/mappings.py +287 -0
- psychopy/hardware/keyboard.py +4 -2
- psychopy/hardware/labhackers.py +1 -1
- psychopy/hardware/labjacks.py +9 -13
- psychopy/hardware/{photodiode.py → lightsensor.py} +146 -203
- psychopy/hardware/listener.py +9 -8
- psychopy/hardware/manager.py +24 -35
- psychopy/hardware/microphone.py +534 -154
- psychopy/hardware/minolta.py +14 -4
- psychopy/hardware/mouse/__init__.py +1 -1
- psychopy/hardware/photometer/__init__.py +2 -2
- psychopy/hardware/pr.py +14 -4
- psychopy/hardware/qmix.py +18 -27
- psychopy/hardware/serialdevice.py +43 -12
- psychopy/hardware/soundsensor.py +473 -0
- psychopy/hardware/spatial/__init__.py +231 -0
- psychopy/hardware/speaker.py +298 -36
- psychopy/hardware/triggerbox/__init__.py +1 -1
- psychopy/hardware/triggerbox/base.py +1 -1
- psychopy/hardware/triggerbox/parallel.py +1 -1
- psychopy/info.py +1 -1
- psychopy/iohub/devices/eyetracker/__init__.py +10 -18
- psychopy/iohub/devices/eyetracker/calibration/procedure.py +15 -33
- psychopy/iohub/devices/eyetracker/hw/pupil_labs/neon/__init__.py +1 -0
- psychopy/iohub/devices/mouse/linux2.py +2 -1
- psychopy/layout.py +1 -1
- psychopy/liaison.py +91 -39
- psychopy/locale_setup.py +11 -1
- psychopy/localization/__init__.py +1 -1
- psychopy/localization/_localization.py +1 -1
- psychopy/localization/messages.pot +2 -2
- psychopy/logging.py +14 -12
- psychopy/microphone.py +4 -3
- psychopy/misc.py +1 -1
- psychopy/monitors/MonitorCenter.py +3 -3
- psychopy/monitors/__init__.py +1 -1
- psychopy/monitors/calibData.py +1 -1
- psychopy/monitors/calibTools.py +3 -2
- psychopy/platform_specific/__init__.py +1 -1
- psychopy/platform_specific/darwin.py +1 -1
- psychopy/platform_specific/linux.py +1 -1
- psychopy/platform_specific/win32.py +1 -1
- psychopy/plugins/__init__.py +26 -17
- psychopy/plugins/util.py +65 -0
- psychopy/preferences/Darwin.spec +11 -3
- psychopy/preferences/FreeBSD.spec +11 -3
- psychopy/preferences/Linux.spec +11 -3
- psychopy/preferences/Windows.spec +11 -3
- psychopy/preferences/__init__.py +1 -1
- psychopy/preferences/baseNoArch.spec +11 -3
- psychopy/preferences/hints.py +78 -61
- psychopy/preferences/preferences.py +82 -13
- psychopy/projects/__init__.py +1 -1
- psychopy/projects/pavlovia.py +29 -13
- psychopy/scripts/psyexpCompile.py +1 -1
- psychopy/session.py +81 -8
- psychopy/sound/__init__.py +25 -10
- psychopy/sound/_base.py +134 -34
- psychopy/sound/audioclip.py +38 -15
- psychopy/sound/audiodevice.py +1 -1
- psychopy/sound/backend_ptb.py +53 -321
- psychopy/sound/backend_pygame.py +7 -3
- psychopy/sound/backend_pyo.py +53 -22
- psychopy/sound/backend_pysound.py +10 -27
- psychopy/sound/backend_sounddevice.py +33 -21
- psychopy/sound/exceptions.py +1 -1
- psychopy/sound/microphone.py +83 -5
- psychopy/sound/transcribe.py +3 -3
- psychopy/tests/data/test_basic_run.py +1 -0
- psychopy/tests/data/test_sounds/default_16000.wav +0 -0
- psychopy/tests/data/test_sounds/default_192000.wav +0 -0
- psychopy/tests/data/test_sounds/default_22050.wav +0 -0
- psychopy/tests/data/test_sounds/default_32000.wav +0 -0
- psychopy/tests/data/test_sounds/default_44100.wav +0 -0
- psychopy/tests/data/test_sounds/default_48000.wav +0 -0
- psychopy/tests/data/test_sounds/default_8000.wav +0 -0
- psychopy/tests/data/test_sounds/default_96000.wav +0 -0
- psychopy/tests/test_alerts/test_alerttools.py +2 -1
- psychopy/tests/test_app/test_command_line.py +65 -0
- psychopy/tests/test_data/test_TrialHandler2.py +18 -7
- psychopy/tests/test_demos/test_builder_demos.py +1 -1
- psychopy/tests/test_experiment/needs_wx/componsTemplate.txt +5238 -4188
- psychopy/tests/test_experiment/needs_wx/test_components.py +1 -1
- psychopy/tests/test_experiment/test_components/test_RoutineSettingsComponent.py +16 -0
- psychopy/tests/test_experiment/test_components/test_all_components.py +1 -1
- psychopy/tests/test_experiment/test_components/test_base_components.py +26 -17
- psychopy/tests/test_experiment/test_loops.py +12 -4
- psychopy/tests/test_experiment/test_params.py +17 -4
- psychopy/tests/test_experiment/test_routines/test_PhotodiodeValidationRoutine.py +2 -2
- psychopy/tests/test_hardware/test_photodiode.py +66 -0
- psychopy/tests/test_liaison/test_Liaison.py +4 -4
- psychopy/tests/test_misc/test_clock.py +2 -0
- psychopy/tests/test_misc/test_color.py +2 -0
- psychopy/tests/test_misc/test_event.py +1 -0
- psychopy/tests/test_plugins/__init__.py +0 -0
- psychopy/tests/test_plugins/test_plugin_stubs.py +125 -0
- psychopy/tests/test_sound/test_sound.py +111 -40
- psychopy/tests/test_tools/test_colorspacetools.py +24 -23
- psychopy/tests/test_tools/test_mathtools.py +9 -9
- psychopy/tests/test_tools/test_viewtools.py +101 -0
- psychopy/tests/test_validators/__init__.py +0 -0
- psychopy/tests/test_validators/test_voicekeyValidator.py +95 -0
- psychopy/tests/test_visual/test_all_stimuli.py +2 -2
- psychopy/tests/test_visual/test_basevisual.py +37 -7
- psychopy/tests/test_visual/test_button.py +2 -2
- psychopy/tests/test_visual/test_circle.py +10 -2
- psychopy/tests/test_visual/test_dots.py +1 -1
- psychopy/tests/test_visual/test_form.py +13 -13
- psychopy/tests/test_visual/test_gamma.py +3 -3
- psychopy/tests/test_visual/test_image.py +2 -2
- psychopy/tests/test_visual/test_progress.py +2 -2
- psychopy/tests/test_visual/test_roi.py +8 -2
- psychopy/tests/test_visual/test_shape.py +2 -2
- psychopy/tests/test_visual/test_slider.py +2 -2
- psychopy/tests/test_visual/test_target.py +2 -2
- psychopy/tests/test_visual/test_textbox.py +6 -8
- psychopy/tests/test_visual/test_winScalePos.py +6 -5
- psychopy/tests/test_visual/test_window.py +17 -0
- psychopy/tests/utils.py +51 -1
- psychopy/tools/__init__.py +1 -1
- psychopy/tools/arraytools.py +2 -2
- psychopy/tools/attributetools.py +52 -1
- psychopy/tools/audiotools.py +4 -1
- psychopy/tools/colorspacetools.py +6 -4
- psychopy/tools/coordinatetools.py +1 -1
- psychopy/tools/fileerrortools.py +21 -9
- psychopy/tools/filetools.py +2 -2
- psychopy/tools/fontmanager.py +47 -28
- psychopy/tools/gltools.py +2964 -558
- psychopy/tools/imagetools.py +1 -1
- psychopy/tools/mathtools.py +997 -127
- psychopy/tools/monitorunittools.py +7 -1
- psychopy/tools/movietools.py +1 -2
- psychopy/tools/pkgtools.py +157 -127
- psychopy/tools/plottools.py +1 -1
- psychopy/tools/rifttools.py +1 -1
- psychopy/tools/stereotools.py +1 -1
- psychopy/tools/stimulustools.py +172 -2
- psychopy/tools/stringtools.py +22 -2
- psychopy/tools/systemtools.py +1 -1
- psychopy/tools/typetools.py +1 -1
- psychopy/tools/unittools.py +1 -1
- psychopy/tools/versionchooser.py +3 -1
- psychopy/tools/viewtools.py +54 -70
- psychopy/tools/wizard.py +2 -2
- psychopy/validation/__init__.py +6 -0
- psychopy/validation/audio.py +74 -0
- psychopy/validation/visual.py +115 -0
- psychopy/visual/__init__.py +1 -4
- psychopy/visual/aperture.py +9 -6
- psychopy/visual/backends/__init__.py +1 -1
- psychopy/visual/backends/_base.py +1 -1
- psychopy/visual/backends/gamma.py +1 -1
- psychopy/visual/backends/glfwbackend.py +8 -12
- psychopy/visual/backends/pygamebackend.py +1 -1
- psychopy/visual/backends/pygletbackend.py +32 -11
- psychopy/visual/basevisual.py +93 -13
- psychopy/visual/brush.py +1 -1
- psychopy/visual/bufferimage.py +90 -10
- psychopy/visual/button.py +1 -1
- psychopy/visual/circle.py +12 -20
- psychopy/visual/custommouse.py +1 -1
- psychopy/visual/dot.py +80 -14
- psychopy/visual/elementarray.py +84 -10
- psychopy/visual/filters.py +1 -1
- psychopy/visual/form.py +43 -28
- psychopy/visual/grating.py +105 -25
- psychopy/visual/helpers.py +1 -1
- psychopy/visual/image.py +98 -20
- psychopy/visual/line.py +15 -25
- psychopy/visual/movie.py +1 -1
- psychopy/visual/movie2.py +4 -3
- psychopy/visual/movie3.py +4 -3
- psychopy/visual/movies/__init__.py +1 -1
- psychopy/visual/movies/frame.py +1 -1
- psychopy/visual/movies/metadata.py +1 -1
- psychopy/visual/movies/players/__init__.py +1 -1
- psychopy/visual/movies/players/_base.py +1 -1
- psychopy/visual/movies/players/ffpyplayer_player.py +2 -3
- psychopy/visual/nnlvs.py +9 -6
- psychopy/visual/noise.py +4 -16
- psychopy/visual/patch.py +4 -3
- psychopy/visual/pie.py +3 -14
- psychopy/visual/polygon.py +21 -28
- psychopy/visual/radial.py +4 -18
- psychopy/visual/ratingscale.py +4 -3
- psychopy/visual/rect.py +16 -25
- psychopy/visual/rift.py +8 -2
- psychopy/visual/secondorder.py +4 -16
- psychopy/visual/shaders.py +620 -286
- psychopy/visual/shape.py +281 -90
- psychopy/visual/simpleimage.py +1 -1
- psychopy/visual/slider.py +78 -25
- psychopy/visual/stim3d.py +5 -608
- psychopy/visual/text.py +13 -3
- psychopy/visual/textbox2/textbox2.py +188 -56
- psychopy/visual/vlcmoviestim.py +1 -1
- psychopy/visual/window.py +373 -200
- psychopy/web.py +1 -1
- {psychopy-2024.2.5.dist-info → psychopy-2025.1.0.dist-info}/METADATA +9 -9
- {psychopy-2024.2.5.dist-info → psychopy-2025.1.0.dist-info}/RECORD +644 -621
- psychopy/.DS_Store +0 -0
- psychopy/__init__.py.orig +0 -65
- psychopy/app/.DS_Store +0 -0
- psychopy/app/Resources/.DS_Store +0 -0
- psychopy/app/Resources/classic/filecsv16.png +0 -0
- psychopy/app/Resources/classic/fileimage16.png +0 -0
- psychopy/app/Resources/classic/fileunknown16.png +0 -0
- psychopy/app/Resources/dark/filecsv16.png +0 -0
- psychopy/app/Resources/dark/filecsv16@2x.png +0 -0
- psychopy/app/Resources/dark/fileimage16.png +0 -0
- psychopy/app/Resources/dark/fileimage16@2x.png +0 -0
- psychopy/app/Resources/dark/fileunknown16.png +0 -0
- psychopy/app/Resources/dark/fileunknown16@2x.png +0 -0
- psychopy/app/Resources/fonts/OpenSans-Bold.ttf +0 -0
- psychopy/app/Resources/fonts/OpenSans-BoldItalic.ttf +0 -0
- psychopy/app/Resources/fonts/OpenSans-ExtraBold.ttf +0 -0
- psychopy/app/Resources/fonts/OpenSans-ExtraBoldItalic.ttf +0 -0
- psychopy/app/Resources/fonts/OpenSans-Italic.ttf +0 -0
- psychopy/app/Resources/fonts/OpenSans-Light.ttf +0 -0
- psychopy/app/Resources/fonts/OpenSans-LightItalic.ttf +0 -0
- psychopy/app/Resources/fonts/OpenSans-Regular.ttf +0 -0
- psychopy/app/Resources/fonts/OpenSans-SemiBold.ttf +0 -0
- psychopy/app/Resources/fonts/OpenSans-SemiBoldItalic.ttf +0 -0
- psychopy/app/Resources/light/filecsv16.png +0 -0
- psychopy/app/Resources/light/filecsv16@2x.png +0 -0
- psychopy/app/Resources/light/fileimage16.png +0 -0
- psychopy/app/Resources/light/fileimage16@2x.png +0 -0
- psychopy/app/Resources/light/fileunknown16.png +0 -0
- psychopy/app/Resources/light/fileunknown16@2x.png +0 -0
- psychopy/app/Resources/psychopySplash.png +0 -0
- psychopy/app/Resources/psychopySplash@2x.png +0 -0
- psychopy/app/builder/builder.py.orig +0 -3932
- psychopy/app/builder/dialogs/__init__.py.orig +0 -1679
- psychopy/app/builder/dialogs/paramCtrls.py.orig +0 -713
- psychopy/app/colorpicker/__init__.py.orig +0 -411
- psychopy/app/locale/ar_001/.DS_Store +0 -0
- psychopy/app/locale/ar_001/LC_MESSAGE/.DS_Store +0 -0
- psychopy/core.py.orig +0 -169
- psychopy/demos/builder/.DS_Store +0 -0
- psychopy/demos/builder/Design Templates/randomisedBlocks/randomisedBlocks_lastrun.py +0 -330
- psychopy/demos/builder/Experiments/.DS_Store +0 -0
- psychopy/demos/builder/Experiments/dragAndDrop/archived_conditions.xlsx +0 -0
- psychopy/demos/builder/Experiments/dragAndDrop/draw grid stim.py +0 -61
- psychopy/demos/builder/Experiments/dragAndDrop/shapeMaker.psyexp +0 -91
- psychopy/demos/builder/Experiments/dragAndDrop/stimuli/grid_image_1.png +0 -0
- psychopy/demos/builder/Experiments/dragAndDrop/stimuli/grid_image_10.png +0 -0
- psychopy/demos/builder/Experiments/dragAndDrop/stimuli/grid_image_2.png +0 -0
- psychopy/demos/builder/Experiments/dragAndDrop/stimuli/grid_image_3.png +0 -0
- psychopy/demos/builder/Experiments/dragAndDrop/stimuli/grid_image_4.png +0 -0
- psychopy/demos/builder/Experiments/dragAndDrop/stimuli/grid_image_5.png +0 -0
- psychopy/demos/builder/Experiments/dragAndDrop/stimuli/grid_image_6.png +0 -0
- psychopy/demos/builder/Experiments/dragAndDrop/stimuli/grid_image_7.png +0 -0
- psychopy/demos/builder/Experiments/dragAndDrop/stimuli/grid_image_8.png +0 -0
- psychopy/demos/builder/Experiments/dragAndDrop/stimuli/grid_image_9.png +0 -0
- psychopy/demos/builder/Experiments/dragAndDrop/updated_conditions.xlsx +0 -0
- psychopy/demos/builder/Tools/.DS_Store +0 -0
- psychopy/demos/builder/Tools/gammaCalibration/.DS_Store +0 -0
- psychopy/demos/builder/Tools/gammaCalibration/data/.DS_Store +0 -0
- psychopy/demos/builder/Tools/gammaCalibration/data/_gamma_correction_visual_2022-05-18_14h18.29.439.csv +0 -38
- psychopy/demos/builder/Tools/gammaCalibration/data/_gamma_correction_visual_2022-05-18_14h18.29.439.log +0 -3418
- psychopy/demos/builder/Tools/gammaCalibration/data/_gamma_correction_visual_2022-05-18_14h18.29.439.psydat +0 -0
- psychopy/demos/builder/Tools/gammaCalibration/data/x1_gamma_correction_visual_2022-05-17_13h59.42.928.csv +0 -2
- psychopy/demos/builder/Tools/gammaCalibration/data/x1_gamma_correction_visual_2022-05-17_13h59.42.928.log +0 -15
- psychopy/demos/builder/Tools/gammaCalibration/data/x1_gamma_correction_visual_2022-05-17_13h59.42.928.psydat +0 -0
- psychopy/demos/builder/Tools/gammaCalibration/gamma_correction_visual_lastrun.py +0 -562
- psychopy/demos/coder/.DS_Store +0 -0
- psychopy/demos/coder/experiment control/info_gamma.pickle +0 -0
- psychopy/demos/coder/iohub/.iohpid +0 -1
- psychopy/demos/coder/iohub/eyetracking/.iohpid +0 -1
- psychopy/demos/coder/iohub/wintab/.DS_Store +0 -0
- psychopy/demos/coder/stimuli/.DS_Store +0 -0
- psychopy/experiment/_experiment.py.orig +0 -1032
- psychopy/experiment/components/.DS_Store +0 -0
- psychopy/experiment/components/_base.py.orig +0 -823
- psychopy/experiment/components/form/.DS_Store +0 -0
- psychopy/experiment/components/microphone/__init__.py.orig +0 -490
- psychopy/experiment/components/settings/__init__.py.orig +0 -1337
- psychopy/experiment/components/textbox/__init__.py.orig +0 -310
- psychopy/experiment/components/webcam/.DS_Store +0 -0
- psychopy/experiment/components/webcam/light/.DS_Store +0 -0
- psychopy/experiment/loops.py.orig +0 -829
- psychopy/experiment/params.py.orig +0 -408
- psychopy/experiment/routine.py.orig +0 -503
- psychopy/experiment/routines/photodiodeValidator/classic/photodiode_validator.png +0 -0
- psychopy/experiment/routines/photodiodeValidator/classic/photodiode_validator@2x.png +0 -0
- psychopy/experiment/routines/photodiodeValidator/dark/photodiode_validator.png +0 -0
- psychopy/experiment/routines/photodiodeValidator/dark/photodiode_validator@2x.png +0 -0
- psychopy/experiment/routines/photodiodeValidator/light/photodiode_validator.png +0 -0
- psychopy/experiment/routines/photodiodeValidator/light/photodiode_validator@2x.png +0 -0
- psychopy/hardware/.DS_Store +0 -0
- psychopy/hardware/brainproducts.py.orig +0 -680
- psychopy/hardware/iolab.py.orig +0 -238
- psychopy/iohub/datastore/__init__.py.orig +0 -443
- psychopy/iohub/datastore/util.py.orig +0 -692
- psychopy/iohub/devices/mouse/darwin.py.orig +0 -427
- psychopy/iohub/devices/mouse/linux2.py.orig +0 -198
- psychopy/preferences/.DS_Store +0 -0
- psychopy/projects/pavlovia.py.orig +0 -1295
- psychopy/tests/.DS_Store +0 -0
- psychopy/tests/data/.DS_Store +0 -0
- psychopy/tests/data/TestCircle_fill_local.png +0 -0
- psychopy/tests/data/aperture1_normHexbackground_local.png +0 -0
- psychopy/tests/data/aperture1_norm_local.png +0 -0
- psychopy/tests/data/aperture2_normHexbackground_local.png +0 -0
- psychopy/tests/data/beatandrcos_height_local.png +0 -0
- psychopy/tests/data/beatandrcos_normAddBlend_local.png +0 -0
- psychopy/tests/data/beatandrcos_normHexbackground_local.png +0 -0
- psychopy/tests/data/beatandrcos_norm_local.png +0 -0
- psychopy/tests/data/beatandrcos_stencil_local.png +0 -0
- psychopy/tests/data/blend_add_height_local.png +0 -0
- psychopy/tests/data/blend_add_normAddBlend_local.png +0 -0
- psychopy/tests/data/blend_add_normHexbackground_local.png +0 -0
- psychopy/tests/data/blend_add_normNoShade_local.png +0 -0
- psychopy/tests/data/blend_add_norm_local.png +0 -0
- psychopy/tests/data/blend_add_stencil_local.png +0 -0
- psychopy/tests/data/bufferimg_gabor_height_local.png +0 -0
- psychopy/tests/data/bufferimg_gabor_normAddBlend_local.png +0 -0
- psychopy/tests/data/bufferimg_gabor_normHexbackground_local.png +0 -0
- psychopy/tests/data/bufferimg_gabor_normNoShade_local.png +0 -0
- psychopy/tests/data/bufferimg_gabor_norm_local.png +0 -0
- psychopy/tests/data/bufferimg_gabor_stencil_local.png +0 -0
- psychopy/tests/data/circleHex_height_local.png +0 -0
- psychopy/tests/data/circleHex_normAddBlend_local.png +0 -0
- psychopy/tests/data/circleHex_normHexbackground_local.png +0 -0
- psychopy/tests/data/circleHex_normNoShade_local.png +0 -0
- psychopy/tests/data/circleHex_norm_local.png +0 -0
- psychopy/tests/data/circleHex_stencil_local.png +0 -0
- psychopy/tests/data/color_comparison_local.png +0 -0
- psychopy/tests/data/correctScript/.DS_Store +0 -0
- psychopy/tests/data/dots_height_local.png +0 -0
- psychopy/tests/data/dots_normAddBlend_local.png +0 -0
- psychopy/tests/data/dots_normHexbackground_local.png +0 -0
- psychopy/tests/data/dots_normNoShade_local.png +0 -0
- psychopy/tests/data/dots_norm_local.png +0 -0
- psychopy/tests/data/dots_stencil_local.png +0 -0
- psychopy/tests/data/elarray1_height_local.png +0 -0
- psychopy/tests/data/elarray1_normAddBlend_local.png +0 -0
- psychopy/tests/data/elarray1_normHexbackground_local.png +0 -0
- psychopy/tests/data/elarray1_norm_local.png +0 -0
- psychopy/tests/data/elarray1_stencil_local.png +0 -0
- psychopy/tests/data/envelopeandrcos_height_local.png +0 -0
- psychopy/tests/data/envelopeandrcos_normAddBlend_local.png +0 -0
- psychopy/tests/data/envelopeandrcos_normHexbackground_local.png +0 -0
- psychopy/tests/data/envelopeandrcos_norm_local.png +0 -0
- psychopy/tests/data/envelopeandrcos_stencil_local.png +0 -0
- psychopy/tests/data/envelopepowerandrcos_height_local.png +0 -0
- psychopy/tests/data/envelopepowerandrcos_normAddBlend_local.png +0 -0
- psychopy/tests/data/envelopepowerandrcos_normHexbackground_local.png +0 -0
- psychopy/tests/data/envelopepowerandrcos_norm_local.png +0 -0
- psychopy/tests/data/envelopepowerandrcos_stencil_local.png +0 -0
- psychopy/tests/data/gabor1_height_local.png +0 -0
- psychopy/tests/data/gabor1_normAddBlend_local.png +0 -0
- psychopy/tests/data/gabor1_normHexbackground_local.png +0 -0
- psychopy/tests/data/gabor1_normNoShade_local.png +0 -0
- psychopy/tests/data/gabor1_norm_local.png +0 -0
- psychopy/tests/data/gabor1_stencil_local.png +0 -0
- psychopy/tests/data/greyscale_normHexbackground_local.png +0 -0
- psychopy/tests/data/imageAndGauss_height_local.png +0 -0
- psychopy/tests/data/imageAndGauss_normAddBlend_local.png +0 -0
- psychopy/tests/data/imageAndGauss_normHexbackground_local.png +0 -0
- psychopy/tests/data/imageAndGauss_normNoShade_local.png +0 -0
- psychopy/tests/data/imageAndGauss_norm_local.png +0 -0
- psychopy/tests/data/imageAndGauss_stencil_local.png +0 -0
- psychopy/tests/data/movFrame1_stencil_local.png +0 -0
- psychopy/tests/data/noiseAndRcos_height_local.png +0 -0
- psychopy/tests/data/noiseAndRcos_normAddBlend_local.png +0 -0
- psychopy/tests/data/noiseAndRcos_normHexbackground_local.png +0 -0
- psychopy/tests/data/noiseAndRcos_normNoShade_local.png +0 -0
- psychopy/tests/data/noiseAndRcos_norm_local.png +0 -0
- psychopy/tests/data/noiseAndRcos_stencil_local.png +0 -0
- psychopy/tests/data/noiseFiltersAndRcos_height_local.png +0 -0
- psychopy/tests/data/noiseFiltersAndRcos_normAddBlend_local.png +0 -0
- psychopy/tests/data/noiseFiltersAndRcos_normHexbackground_local.png +0 -0
- psychopy/tests/data/noiseFiltersAndRcos_normNoShade_local.png +0 -0
- psychopy/tests/data/noiseFiltersAndRcos_norm_local.png +0 -0
- psychopy/tests/data/noiseFiltersAndRcos_stencil_local.png +0 -0
- psychopy/tests/data/numpyImage_height_local.png +0 -0
- psychopy/tests/data/numpyImage_normAddBlend_local.png +0 -0
- psychopy/tests/data/numpyImage_normHexbackground_local.png +0 -0
- psychopy/tests/data/numpyImage_normNoShade_local.png +0 -0
- psychopy/tests/data/numpyImage_norm_local.png +0 -0
- psychopy/tests/data/numpyImage_stencil_local.png +0 -0
- psychopy/tests/data/shape2_1_normAddBlend_local.png +0 -0
- psychopy/tests/data/shape2_1_normHexbackground_local.png +0 -0
- psychopy/tests/data/shape2_1_normNoShade_local.png +0 -0
- psychopy/tests/data/shape2_1_norm_local.png +0 -0
- psychopy/tests/data/shape2_1_stencil_local.png +0 -0
- psychopy/tests/data/text1_height_local.png +0 -0
- psychopy/tests/data/text1_normAddBlend_local.png +0 -0
- psychopy/tests/data/text1_normHexbackground_local.png +0 -0
- psychopy/tests/data/text1_norm_local.png +0 -0
- psychopy/tests/data/text1_stencil_local.png +0 -0
- psychopy/tests/data/wedge1_height_local.png +0 -0
- psychopy/tests/data/wedge1_normAddBlend_local.png +0 -0
- psychopy/tests/data/wedge1_normHexbackground_local.png +0 -0
- psychopy/tests/data/wedge1_normNoShade_local.png +0 -0
- psychopy/tests/data/wedge1_norm_local.png +0 -0
- psychopy/tests/data/wedge1_stencil_local.png +0 -0
- psychopy/tests/test_app/.DS_Store +0 -0
- psychopy/tests/test_app/test_builder/.DS_Store +0 -0
- psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1206.log +0 -177
- psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1206.psydat +0 -0
- psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1206.xlsx +0 -0
- psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1324.log +0 -168
- psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1324.psydat +0 -0
- psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1324.xlsx +0 -0
- psychopy/tests/test_data/.DS_Store +0 -0
- psychopy/tests/test_hardware/test_CRS_BitsSharp.py +0 -67
- psychopy/tests/test_hardware/test_CRS_BitsSharp.py.orig +0 -68
- psychopy/tests/test_hardware/test_CRS_bitsShaders.py +0 -110
- psychopy/tests/test_visual/test_image.py.orig +0 -219
- psychopy/visual/basevisual.py.orig +0 -1723
- psychopy/visual/form.py.orig +0 -1181
- psychopy/visual/text.py.orig +0 -752
- psychopy/visual/textbox2/textbox2.py.orig +0 -1315
- psychopy/visual/windowwarp.py.orig +0 -463
- /psychopy/{app/cortex.log → alerts/alertsCatalogue/3600.yaml} +0 -0
- /psychopy/{app/Resources → assets}/fonts/Arvo-Bold.ttf +0 -0
- /psychopy/{app/Resources → assets}/fonts/Arvo-BoldItalic.ttf +0 -0
- /psychopy/{app/Resources → assets}/fonts/Arvo-Italic.ttf +0 -0
- /psychopy/{app/Resources → assets}/fonts/Arvo-Regular.ttf +0 -0
- /psychopy/{app/Resources → assets}/fonts/DejaVuSerif.ttf +0 -0
- /psychopy/{app/Resources → assets}/fonts/IndieFlower-Regular.ttf +0 -0
- /psychopy/{app/Resources → assets}/fonts/JetBrainsMono-Italic-VariableFont_wght.ttf +0 -0
- /psychopy/{app/Resources → assets}/fonts/JetBrainsMono-VariableFont_wght.ttf +0 -0
- /psychopy/demos/builder/Experiments/{GoNoGo → goNoGo}/readme.md +0 -0
- {psychopy-2024.2.5.dist-info → psychopy-2025.1.0.dist-info}/WHEEL +0 -0
- {psychopy-2024.2.5.dist-info → psychopy-2025.1.0.dist-info}/entry_points.txt +0 -0
- {psychopy-2024.2.5.dist-info → psychopy-2025.1.0.dist-info}/licenses/LICENSE +0 -0
psychopy/hardware/microphone.py
CHANGED
|
@@ -3,7 +3,8 @@ import time
|
|
|
3
3
|
|
|
4
4
|
import numpy as np
|
|
5
5
|
from psychtoolbox import audio as audio
|
|
6
|
-
from psychopy import logging as logging, prefs
|
|
6
|
+
from psychopy import logging as logging, prefs, core
|
|
7
|
+
from psychopy.hardware.exceptions import DeviceNotConnectedError
|
|
7
8
|
from psychopy.localization import _translate
|
|
8
9
|
from psychopy.constants import NOT_STARTED
|
|
9
10
|
from psychopy.hardware import BaseDevice, BaseResponse, BaseResponseDevice
|
|
@@ -60,7 +61,14 @@ class MicrophoneDevice(BaseDevice, aliases=["mic", "microphone"]):
|
|
|
60
61
|
size of the recording buffer to ensure that the application does not run
|
|
61
62
|
out of memory. By default, the recording buffer is set to 24000 KB (or
|
|
62
63
|
24 MB). At a sample rate of 48kHz, this will result in 62.5 seconds of
|
|
63
|
-
continuous audio being recorded before the buffer is full.
|
|
64
|
+
continuous audio being recorded before the buffer is full. You may
|
|
65
|
+
specify how to handle the buffer when it is full using the
|
|
66
|
+
`policyWhenFull` parameter.
|
|
67
|
+
policyWhenFull : str
|
|
68
|
+
Policy to use when the recording buffer is full. Options are:
|
|
69
|
+
- "ignore": When full, just don't record any new samples
|
|
70
|
+
- "warn"/"warning": Same as ignore, but will log a warning
|
|
71
|
+
- "error": When full, will raise an error
|
|
64
72
|
audioLatencyMode : int or None
|
|
65
73
|
Audio latency mode to use, values range between 0-4. If `None`, the
|
|
66
74
|
setting from preferences will be used. Using `3` (exclusive mode) is
|
|
@@ -114,15 +122,19 @@ class MicrophoneDevice(BaseDevice, aliases=["mic", "microphone"]):
|
|
|
114
122
|
# other instances of MicrophoneDevice, stored by index
|
|
115
123
|
_streams = {}
|
|
116
124
|
|
|
117
|
-
def __init__(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
def __init__(
|
|
126
|
+
self,
|
|
127
|
+
index=None,
|
|
128
|
+
sampleRateHz=None,
|
|
129
|
+
channels=None,
|
|
130
|
+
streamBufferSecs=2.0,
|
|
131
|
+
maxRecordingSize=-1,
|
|
132
|
+
policyWhenFull='warn',
|
|
133
|
+
exclusive=False,
|
|
134
|
+
audioRunMode=1,
|
|
135
|
+
# legacy
|
|
136
|
+
audioLatencyMode=None,
|
|
137
|
+
):
|
|
126
138
|
|
|
127
139
|
if not _hasPTB: # fail if PTB is not installed
|
|
128
140
|
raise ModuleNotFoundError(
|
|
@@ -147,10 +159,13 @@ class MicrophoneDevice(BaseDevice, aliases=["mic", "microphone"]):
|
|
|
147
159
|
_devices = MicrophoneDevice.getDevices()
|
|
148
160
|
# if there are none, error
|
|
149
161
|
if not len(_devices):
|
|
150
|
-
raise
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
162
|
+
raise DeviceNotConnectedError(
|
|
163
|
+
_translate(
|
|
164
|
+
"Could not choose default recording device as no recording "
|
|
165
|
+
"devices are connected."
|
|
166
|
+
),
|
|
167
|
+
deviceClass=MicrophoneDevice
|
|
168
|
+
)
|
|
154
169
|
|
|
155
170
|
# Try and get the best match which are compatible with the user's
|
|
156
171
|
# specified settings.
|
|
@@ -191,8 +206,11 @@ class MicrophoneDevice(BaseDevice, aliases=["mic", "microphone"]):
|
|
|
191
206
|
if isinstance(device, MicrophoneDevice):
|
|
192
207
|
self._device = device._device
|
|
193
208
|
else:
|
|
194
|
-
|
|
195
|
-
|
|
209
|
+
# if not found, find best match
|
|
210
|
+
self._device = self.findBestDevice(
|
|
211
|
+
index=index,
|
|
212
|
+
sampleRateHz=sampleRateHz,
|
|
213
|
+
channels=channels
|
|
196
214
|
)
|
|
197
215
|
else:
|
|
198
216
|
# get best match
|
|
@@ -226,15 +244,13 @@ class MicrophoneDevice(BaseDevice, aliases=["mic", "microphone"]):
|
|
|
226
244
|
self._sampleRateHz))
|
|
227
245
|
|
|
228
246
|
# set the audio latency mode
|
|
229
|
-
if
|
|
230
|
-
self._audioLatencyMode =
|
|
247
|
+
if exclusive:
|
|
248
|
+
self._audioLatencyMode = 2
|
|
231
249
|
else:
|
|
232
|
-
self._audioLatencyMode =
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
assert 0 <= self._audioLatencyMode <= 4 # sanity check for pref
|
|
250
|
+
self._audioLatencyMode = 1
|
|
251
|
+
logging.debug(
|
|
252
|
+
'Set audio latency mode to {}'.format(self._audioLatencyMode)
|
|
253
|
+
)
|
|
238
254
|
|
|
239
255
|
# internal recording buffer size in seconds
|
|
240
256
|
assert isinstance(streamBufferSecs, (float, int))
|
|
@@ -243,60 +259,28 @@ class MicrophoneDevice(BaseDevice, aliases=["mic", "microphone"]):
|
|
|
243
259
|
# PTB specific stuff
|
|
244
260
|
self._mode = 2 # open a stream in capture mode
|
|
245
261
|
|
|
246
|
-
#
|
|
247
|
-
# session
|
|
248
|
-
logging.debug('Opening audio stream for device #{}'.format(
|
|
249
|
-
self._device.deviceIndex))
|
|
250
|
-
if self._device.deviceIndex not in MicrophoneDevice._streams:
|
|
251
|
-
MicrophoneDevice._streams[self._device.deviceIndex] = audio.Stream(
|
|
252
|
-
device_id=self._device.deviceIndex,
|
|
253
|
-
latency_class=self._audioLatencyMode,
|
|
254
|
-
mode=self._mode,
|
|
255
|
-
freq=self._device.defaultSampleRate,
|
|
256
|
-
channels=self._device.inputChannels)
|
|
257
|
-
logging.debug('Stream opened')
|
|
258
|
-
else:
|
|
259
|
-
logging.debug(
|
|
260
|
-
"Stream already created for device at index {}, using created stream.".format(
|
|
261
|
-
self._device.deviceIndex
|
|
262
|
-
)
|
|
263
|
-
)
|
|
264
|
-
|
|
265
|
-
# store reference to stream in this instance
|
|
266
|
-
self._stream = MicrophoneDevice._streams[self._device.deviceIndex]
|
|
267
|
-
|
|
262
|
+
# get audio run mode
|
|
268
263
|
assert isinstance(audioRunMode, (float, int)) and \
|
|
269
264
|
(audioRunMode == 0 or audioRunMode == 1)
|
|
270
265
|
self._audioRunMode = int(audioRunMode)
|
|
271
|
-
self._stream.run_mode = self._audioRunMode
|
|
272
|
-
|
|
273
|
-
logging.debug('Set run mode to `{}`'.format(
|
|
274
|
-
self._audioRunMode))
|
|
275
|
-
|
|
276
|
-
# set latency bias
|
|
277
|
-
self._stream.latency_bias = 0.0
|
|
278
|
-
|
|
279
|
-
logging.debug('Set stream latency bias to {} ms'.format(
|
|
280
|
-
self._stream.latency_bias))
|
|
281
|
-
|
|
282
|
-
# pre-allocate recording buffer, called once
|
|
283
|
-
self._stream.get_audio_data(self._streamBufferSecs)
|
|
284
266
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
267
|
+
# open stream
|
|
268
|
+
self._stream = None
|
|
269
|
+
self._opening = self._closing = False
|
|
270
|
+
self.open()
|
|
288
271
|
|
|
289
272
|
# status flag for Builder
|
|
290
273
|
self._statusFlag = NOT_STARTED
|
|
291
274
|
|
|
292
|
-
#
|
|
293
|
-
self._recording =
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
maxRecordingSize
|
|
297
|
-
|
|
298
|
-
)
|
|
275
|
+
# recording buffer information
|
|
276
|
+
self._recording = [] # use a list
|
|
277
|
+
self._totalSamples = 0
|
|
278
|
+
self._maxRecordingSize = (
|
|
279
|
+
-1 if maxRecordingSize is None else int(maxRecordingSize))
|
|
280
|
+
self._policyWhenFull = policyWhenFull
|
|
299
281
|
|
|
282
|
+
# internal state
|
|
283
|
+
self._possiblyAsleep = False
|
|
300
284
|
self._isStarted = False # internal state
|
|
301
285
|
|
|
302
286
|
logging.debug('Audio capture device #{} ready'.format(
|
|
@@ -304,6 +288,46 @@ class MicrophoneDevice(BaseDevice, aliases=["mic", "microphone"]):
|
|
|
304
288
|
|
|
305
289
|
# list to store listeners in
|
|
306
290
|
self.listeners = []
|
|
291
|
+
|
|
292
|
+
@property
|
|
293
|
+
def maxRecordingSize(self):
|
|
294
|
+
"""
|
|
295
|
+
Until a file is saved, the audio data from a Microphone needs to be stored in RAM. To avoid
|
|
296
|
+
a memory leak, we limit the amount which can be stored by a single Microphone object. The
|
|
297
|
+
`maxRecordingSize` parameter defines what this limit is.
|
|
298
|
+
|
|
299
|
+
Parameters
|
|
300
|
+
----------
|
|
301
|
+
value : int
|
|
302
|
+
How much data (in kb) to allow, default is 24mb (so 24,000kb)
|
|
303
|
+
"""
|
|
304
|
+
return self._maxRecordingSize
|
|
305
|
+
|
|
306
|
+
@maxRecordingSize.setter
|
|
307
|
+
def maxRecordingSize(self, value):
|
|
308
|
+
self._maxRecordingSize = int(value)
|
|
309
|
+
|
|
310
|
+
@property
|
|
311
|
+
def policyWhenFull(self):
|
|
312
|
+
"""
|
|
313
|
+
Until a file is saved, the audio data from a Microphone needs to be stored in RAM. To avoid
|
|
314
|
+
a memory leak, we limit the amount which can be stored by a single Microphone object. The
|
|
315
|
+
`policyWhenFull` parameter tells the Microphone what to do when it's reached that limit.
|
|
316
|
+
|
|
317
|
+
Parameters
|
|
318
|
+
----------
|
|
319
|
+
value : str
|
|
320
|
+
One of:
|
|
321
|
+
- "ignore": When full, just don't record any new samples
|
|
322
|
+
- "warn"/"warning": Same as ignore, but will log a warning
|
|
323
|
+
- "error": When full, will raise an error
|
|
324
|
+
- "roll"/"rolling": When full, clears the start of the buffer to make room for new samples
|
|
325
|
+
"""
|
|
326
|
+
return self._policyWhenFull
|
|
327
|
+
|
|
328
|
+
@policyWhenFull.setter
|
|
329
|
+
def policyWhenFull(self, value):
|
|
330
|
+
self._policyWhenFull = value
|
|
307
331
|
|
|
308
332
|
def findBestDevice(self, index, sampleRateHz, channels):
|
|
309
333
|
"""
|
|
@@ -337,11 +361,11 @@ class MicrophoneDevice(BaseDevice, aliases=["mic", "microphone"]):
|
|
|
337
361
|
# iterate through device profiles
|
|
338
362
|
for profile in self.getDevices():
|
|
339
363
|
# if same index, keep as fallback
|
|
340
|
-
if profile.deviceIndex
|
|
364
|
+
if index in (profile.deviceIndex, profile.deviceName):
|
|
341
365
|
fallbackDevice = profile
|
|
342
366
|
# if same everything, we got it!
|
|
343
367
|
if all((
|
|
344
|
-
profile.deviceIndex
|
|
368
|
+
index in (profile.deviceIndex, profile.deviceName),
|
|
345
369
|
profile.defaultSampleRate == sampleRateHz,
|
|
346
370
|
profile.inputChannels == channels,
|
|
347
371
|
)):
|
|
@@ -353,14 +377,18 @@ class MicrophoneDevice(BaseDevice, aliases=["mic", "microphone"]):
|
|
|
353
377
|
f"Could not find exact match for specified parameters (index={index}, sampleRateHz="
|
|
354
378
|
f"{sampleRateHz}, channels={channels}), falling back to best approximation ("
|
|
355
379
|
f"index={fallbackDevice.deviceIndex}, "
|
|
380
|
+
f"name={fallbackDevice.deviceName},"
|
|
356
381
|
f"sampleRateHz={fallbackDevice.defaultSampleRate}, "
|
|
357
382
|
f"channels={fallbackDevice.inputChannels})"
|
|
358
383
|
)
|
|
359
384
|
chosenDevice = fallbackDevice
|
|
360
385
|
elif chosenDevice is None:
|
|
361
386
|
# if no index match found, raise error
|
|
362
|
-
raise
|
|
363
|
-
|
|
387
|
+
raise DeviceNotConnectedError(
|
|
388
|
+
_translate(
|
|
389
|
+
"Could not find any audio recording device with index {index}",
|
|
390
|
+
).format(index=index),
|
|
391
|
+
deviceClass=MicrophoneDevice
|
|
364
392
|
)
|
|
365
393
|
|
|
366
394
|
return chosenDevice
|
|
@@ -391,7 +419,7 @@ class MicrophoneDevice(BaseDevice, aliases=["mic", "microphone"]):
|
|
|
391
419
|
# if the other object is the wrong type or doesn't have an index, it's not this
|
|
392
420
|
return False
|
|
393
421
|
|
|
394
|
-
return self.index
|
|
422
|
+
return index in (self.index, self._device.deviceName)
|
|
395
423
|
|
|
396
424
|
@staticmethod
|
|
397
425
|
def getDevices():
|
|
@@ -429,9 +457,13 @@ class MicrophoneDevice(BaseDevice, aliases=["mic", "microphone"]):
|
|
|
429
457
|
def getAvailableDevices():
|
|
430
458
|
devices = []
|
|
431
459
|
for profile in st.getAudioCaptureDevices():
|
|
460
|
+
# get index as a name if possible
|
|
461
|
+
index = profile.get('device_name', None)
|
|
462
|
+
if index is None:
|
|
463
|
+
index = profile.get('index', None)
|
|
432
464
|
device = {
|
|
433
465
|
'deviceName': profile.get('device_name', "Unknown Microphone"),
|
|
434
|
-
'index':
|
|
466
|
+
'index': index,
|
|
435
467
|
'sampleRateHz': profile.get('defaultSampleRate', None),
|
|
436
468
|
'channels': profile.get('inputChannels', None),
|
|
437
469
|
}
|
|
@@ -454,6 +486,24 @@ class MicrophoneDevice(BaseDevice, aliases=["mic", "microphone"]):
|
|
|
454
486
|
# self._stream.start()
|
|
455
487
|
# self._stream.stop()
|
|
456
488
|
|
|
489
|
+
@property
|
|
490
|
+
def channels(self):
|
|
491
|
+
"""Number of audio channels to record samples to (`int`).
|
|
492
|
+
"""
|
|
493
|
+
return self._channels
|
|
494
|
+
|
|
495
|
+
@property
|
|
496
|
+
def sampleRateHz(self):
|
|
497
|
+
"""Sampling rate for audio recording in Hertz (`int`).
|
|
498
|
+
"""
|
|
499
|
+
return self._sampleRateHz
|
|
500
|
+
|
|
501
|
+
@property
|
|
502
|
+
def device(self):
|
|
503
|
+
"""Audio device descriptor (`AudioDeviceInfo`).
|
|
504
|
+
"""
|
|
505
|
+
return self._device
|
|
506
|
+
|
|
457
507
|
@property
|
|
458
508
|
def recording(self):
|
|
459
509
|
"""Reference to the current recording buffer (`RecordingBuffer`)."""
|
|
@@ -462,28 +512,7 @@ class MicrophoneDevice(BaseDevice, aliases=["mic", "microphone"]):
|
|
|
462
512
|
@property
|
|
463
513
|
def recBufferSecs(self):
|
|
464
514
|
"""Capacity of the recording buffer in seconds (`float`)."""
|
|
465
|
-
return self.
|
|
466
|
-
|
|
467
|
-
@property
|
|
468
|
-
def maxRecordingSize(self):
|
|
469
|
-
"""Maximum recording size in kilobytes (`int`).
|
|
470
|
-
|
|
471
|
-
Since audio recordings tend to consume a large amount of system memory,
|
|
472
|
-
one might want to limit the size of the recording buffer to ensure that
|
|
473
|
-
the application does not run out. By default, the recording buffer is
|
|
474
|
-
set to 64000 KB (or 64 MB). At a sample rate of 48kHz, this will result
|
|
475
|
-
in about. Using stereo audio (``nChannels == 2``) requires twice the
|
|
476
|
-
buffer over mono (``nChannels == 2``) for the same length clip.
|
|
477
|
-
|
|
478
|
-
Setting this value will allocate another recording buffer of appropriate
|
|
479
|
-
size. Avoid doing this in any time sensitive parts of your application.
|
|
480
|
-
|
|
481
|
-
"""
|
|
482
|
-
return self._recording.maxRecordingSize
|
|
483
|
-
|
|
484
|
-
@maxRecordingSize.setter
|
|
485
|
-
def maxRecordingSize(self, value):
|
|
486
|
-
self._recording.maxRecordingSize = value
|
|
515
|
+
return self._totalSamples / float(self._sampleRateHz)
|
|
487
516
|
|
|
488
517
|
@property
|
|
489
518
|
def latencyBias(self):
|
|
@@ -550,7 +579,7 @@ class MicrophoneDevice(BaseDevice, aliases=["mic", "microphone"]):
|
|
|
550
579
|
lost.
|
|
551
580
|
|
|
552
581
|
"""
|
|
553
|
-
return self.
|
|
582
|
+
return self._totalSamples >= self._maxRecordingSize
|
|
554
583
|
|
|
555
584
|
@property
|
|
556
585
|
def isStarted(self):
|
|
@@ -645,9 +674,11 @@ class MicrophoneDevice(BaseDevice, aliases=["mic", "microphone"]):
|
|
|
645
674
|
|
|
646
675
|
if self._stream is None:
|
|
647
676
|
raise AudioStreamError("Stream not ready.")
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
677
|
+
# reset timer for possibly asleep
|
|
678
|
+
self._possiblyAsleep = False
|
|
679
|
+
# reset the recording buffer
|
|
680
|
+
self._recording = []
|
|
681
|
+
self._totalSamples = 0
|
|
651
682
|
|
|
652
683
|
# reset warnings
|
|
653
684
|
# self._warnedRecBufferFull = False
|
|
@@ -722,12 +753,11 @@ class MicrophoneDevice(BaseDevice, aliases=["mic", "microphone"]):
|
|
|
722
753
|
"""
|
|
723
754
|
# This function must be idempotent since it can be invoked at any time
|
|
724
755
|
# whether a stream is started or not.
|
|
725
|
-
if not self.isStarted:
|
|
756
|
+
if not self.isStarted or self._stream._closed:
|
|
726
757
|
return
|
|
727
758
|
|
|
728
759
|
# poll remaining samples, if any
|
|
729
|
-
|
|
730
|
-
self.poll()
|
|
760
|
+
self.poll()
|
|
731
761
|
|
|
732
762
|
startTime, endPositionSecs, xruns, estStopTime = self._stream.stop(
|
|
733
763
|
block_until_stopped=int(blockUntilStopped),
|
|
@@ -766,16 +796,103 @@ class MicrophoneDevice(BaseDevice, aliases=["mic", "microphone"]):
|
|
|
766
796
|
"""
|
|
767
797
|
return self.stop(blockUntilStopped=blockUntilStopped, stopTime=stopTime)
|
|
768
798
|
|
|
799
|
+
def open(self):
|
|
800
|
+
"""
|
|
801
|
+
Open the audio stream.
|
|
802
|
+
"""
|
|
803
|
+
# do nothing if stream is already open
|
|
804
|
+
if self._stream is not None and not self._stream._closed:
|
|
805
|
+
return
|
|
806
|
+
# set flag that it's mid-open
|
|
807
|
+
self._opening = True
|
|
808
|
+
# search for open streams and if there is one, use it
|
|
809
|
+
if self._device.deviceIndex in MicrophoneDevice._streams:
|
|
810
|
+
logging.debug(
|
|
811
|
+
f"Assigning audio stream for device #{self._device.deviceIndex} to a new "
|
|
812
|
+
f"MicrophoneDevice object."
|
|
813
|
+
)
|
|
814
|
+
self._stream = MicrophoneDevice._streams[self._device.deviceIndex]
|
|
815
|
+
return
|
|
816
|
+
|
|
817
|
+
# if no open streams, make one
|
|
818
|
+
logging.debug(
|
|
819
|
+
f"Opening new audio stream for device #{self._device.deviceIndex}."
|
|
820
|
+
)
|
|
821
|
+
self._stream = MicrophoneDevice._streams[self._device.deviceIndex] = audio.Stream(
|
|
822
|
+
device_id=self._device.deviceIndex,
|
|
823
|
+
latency_class=self._audioLatencyMode,
|
|
824
|
+
mode=self._mode,
|
|
825
|
+
freq=self._device.defaultSampleRate,
|
|
826
|
+
channels=self._device.inputChannels
|
|
827
|
+
)
|
|
828
|
+
# set run mode
|
|
829
|
+
self._stream.run_mode = self._audioRunMode
|
|
830
|
+
logging.debug('Set run mode to `{}`'.format(
|
|
831
|
+
self._audioRunMode))
|
|
832
|
+
# set latency bias
|
|
833
|
+
self._stream.latency_bias = 0.0
|
|
834
|
+
logging.debug('Set stream latency bias to {} ms'.format(
|
|
835
|
+
self._stream.latency_bias))
|
|
836
|
+
# pre-allocate recording buffer, called once
|
|
837
|
+
self._stream.get_audio_data(self._streamBufferSecs)
|
|
838
|
+
logging.debug(
|
|
839
|
+
'Allocated stream buffer to hold {} seconds of data'.format(
|
|
840
|
+
self._streamBufferSecs))
|
|
841
|
+
# set flag that it's done opening
|
|
842
|
+
self._opening = False
|
|
843
|
+
|
|
769
844
|
def close(self):
|
|
770
|
-
"""Close the stream.
|
|
771
|
-
|
|
772
|
-
Should not be called until you are certain you're done with it. Ideally,
|
|
773
|
-
you should never close and reopen the same stream within a single
|
|
774
|
-
session.
|
|
775
|
-
|
|
776
845
|
"""
|
|
846
|
+
Close the audio stream.
|
|
847
|
+
"""
|
|
848
|
+
# clear any attached listeners
|
|
849
|
+
self.clearListeners()
|
|
850
|
+
# do nothing further if already closed
|
|
851
|
+
if self._stream._closed:
|
|
852
|
+
return
|
|
853
|
+
# set flag that it's mid-close
|
|
854
|
+
self._closing = True
|
|
855
|
+
# remove ref to stream
|
|
856
|
+
if self._device.deviceIndex in MicrophoneDevice._streams:
|
|
857
|
+
MicrophoneDevice._streams.pop(self._device.deviceIndex)
|
|
858
|
+
# close stream
|
|
777
859
|
self._stream.close()
|
|
778
860
|
logging.debug('Stream closed')
|
|
861
|
+
# set flag that it's done closing
|
|
862
|
+
self._closing = False
|
|
863
|
+
|
|
864
|
+
def reopen(self):
|
|
865
|
+
"""
|
|
866
|
+
Calls self.close() then self.open() to reopen the stream.
|
|
867
|
+
"""
|
|
868
|
+
# get status at close
|
|
869
|
+
status = self.isStarted
|
|
870
|
+
# start timer
|
|
871
|
+
start = time.time()
|
|
872
|
+
# close then open
|
|
873
|
+
self.close()
|
|
874
|
+
self.open()
|
|
875
|
+
# log time it took
|
|
876
|
+
logging.info(
|
|
877
|
+
f"Reopened microphone #{self.index}, took {time.time() - start:.3f}s"
|
|
878
|
+
)
|
|
879
|
+
# if mic was running beforehand, start it back up again now
|
|
880
|
+
if status:
|
|
881
|
+
self.start()
|
|
882
|
+
|
|
883
|
+
@property
|
|
884
|
+
def recordingEmpty(self):
|
|
885
|
+
"""`True` if the recording buffer is empty (`bool`).
|
|
886
|
+
"""
|
|
887
|
+
return len(self._recording) == 0
|
|
888
|
+
|
|
889
|
+
@property
|
|
890
|
+
def recordingFull(self):
|
|
891
|
+
"""`True` if the recording buffer is full (`bool`).
|
|
892
|
+
"""
|
|
893
|
+
if self._maxRecordingSize < 0:
|
|
894
|
+
return False
|
|
895
|
+
return self._totalSamples >= self._maxRecordingSize
|
|
779
896
|
|
|
780
897
|
def poll(self):
|
|
781
898
|
"""Poll audio samples.
|
|
@@ -796,12 +913,46 @@ class MicrophoneDevice(BaseDevice, aliases=["mic", "microphone"]):
|
|
|
796
913
|
|
|
797
914
|
"""
|
|
798
915
|
if not self.isStarted:
|
|
799
|
-
|
|
800
|
-
"
|
|
916
|
+
logging.warning(
|
|
917
|
+
"Attempted to poll samples from mic which hasn't started."
|
|
918
|
+
)
|
|
919
|
+
return
|
|
920
|
+
if self._stream._closed:
|
|
921
|
+
logging.warning(
|
|
922
|
+
"Attempted to poll samples from mic which has been closed."
|
|
923
|
+
)
|
|
924
|
+
return
|
|
925
|
+
if self._opening or self._closing:
|
|
926
|
+
action = "opening" if self._opening else "closing"
|
|
927
|
+
logging.warning(
|
|
928
|
+
f"Attempted to poll microphone while the stream was still {action}. Samples will be "
|
|
929
|
+
f"lost."
|
|
930
|
+
)
|
|
931
|
+
return
|
|
801
932
|
|
|
802
933
|
# figure out what to do with this other information
|
|
803
934
|
audioData, absRecPosition, overflow, cStartTime = \
|
|
804
935
|
self._stream.get_audio_data()
|
|
936
|
+
|
|
937
|
+
if len(audioData):
|
|
938
|
+
# if we got samples, the device is awake, so stop figuring out if it's asleep
|
|
939
|
+
self._possiblyAsleep = False
|
|
940
|
+
elif self._possiblyAsleep is False:
|
|
941
|
+
# if it was awake and now we've got no samples, store the time
|
|
942
|
+
self._possiblyAsleep = time.time()
|
|
943
|
+
elif self._possiblyAsleep + 1 < time.time():
|
|
944
|
+
# if we've not had any evidence of it being awake for 1s, reopen
|
|
945
|
+
logging.error(
|
|
946
|
+
f"Microphone device appears to have gone to sleep, reopening to wake it up."
|
|
947
|
+
)
|
|
948
|
+
# mark as stopped so we don't recursively poll forever when stopping
|
|
949
|
+
self._isStarted = False
|
|
950
|
+
# reopen
|
|
951
|
+
self.reopen()
|
|
952
|
+
# start again
|
|
953
|
+
self.start()
|
|
954
|
+
# mark as not asleep so we don't restart again if the first poll is empty
|
|
955
|
+
self._possiblyAsleep = False
|
|
805
956
|
|
|
806
957
|
if overflow:
|
|
807
958
|
logging.warning(
|
|
@@ -810,9 +961,102 @@ class MicrophoneDevice(BaseDevice, aliases=["mic", "microphone"]):
|
|
|
810
961
|
"called often enough, or increase the size of the audio buffer "
|
|
811
962
|
"with `bufferSecs`.")
|
|
812
963
|
|
|
813
|
-
|
|
964
|
+
# add samples to recording buffer
|
|
965
|
+
if len(audioData):
|
|
966
|
+
# add samples to recording buffer
|
|
967
|
+
self._recording.append(
|
|
968
|
+
AudioClip(audioData, sampleRateHz=self._sampleRateHz))
|
|
969
|
+
self._totalSamples += audioData.shape[0]
|
|
970
|
+
|
|
971
|
+
if self.recordingFull and not self._policyWhenFull == 'ignore':
|
|
972
|
+
if self._policyWhenFull == 'warn':
|
|
973
|
+
logging.warning(
|
|
974
|
+
"Recording buffer is full, no more samples will be added.")
|
|
975
|
+
elif self._policyWhenFull == 'error':
|
|
976
|
+
raise AudioStreamError(
|
|
977
|
+
"Recording buffer is full, no more samples will be added.")
|
|
978
|
+
|
|
979
|
+
return 0
|
|
980
|
+
|
|
981
|
+
def _mergeAudioFragments(self):
|
|
982
|
+
"""Merge audio fragments into a single segment.
|
|
983
|
+
|
|
984
|
+
This merges all audio fragments in the recoding buffer into a single
|
|
985
|
+
`AudioClip` object. The recording buffer is then cleared and the first
|
|
986
|
+
element is set to the merged segment.
|
|
987
|
+
|
|
988
|
+
Returns
|
|
989
|
+
-------
|
|
990
|
+
bool
|
|
991
|
+
`False` if the recording buffer has no fragments to merge, `True`
|
|
992
|
+
otherwise.
|
|
993
|
+
|
|
994
|
+
"""
|
|
995
|
+
if len(self._recording) < 2:
|
|
996
|
+
return False
|
|
997
|
+
|
|
998
|
+
# get the sum of all audio samples in the recording buffer
|
|
999
|
+
totalSamples = sum(
|
|
1000
|
+
[segment.samples.shape[0] for segment in self._recording])
|
|
1001
|
+
|
|
1002
|
+
# create a new array to hold all samples
|
|
1003
|
+
fullSegment = np.zeros(
|
|
1004
|
+
(totalSamples, self._recording[0].channels),
|
|
1005
|
+
dtype=np.float32,
|
|
1006
|
+
order='C')
|
|
1007
|
+
|
|
1008
|
+
# copy samples from each segment into the full segment
|
|
1009
|
+
idx = 0
|
|
1010
|
+
for segment in self._recording:
|
|
1011
|
+
nSamples = segment.samples.shape[0]
|
|
1012
|
+
fullSegment[idx:idx + nSamples, :] = segment.samples
|
|
1013
|
+
idx += nSamples
|
|
1014
|
+
|
|
1015
|
+
# set the recording
|
|
1016
|
+
self._recording = [
|
|
1017
|
+
AudioClip(fullSegment, sampleRateHz=self._sampleRateHz)]
|
|
1018
|
+
|
|
1019
|
+
return True
|
|
1020
|
+
|
|
1021
|
+
def _getSegment(self, start=0, end=None):
|
|
1022
|
+
"""Get a segment of audio samples from the recording buffer.
|
|
1023
|
+
|
|
1024
|
+
Parameters
|
|
1025
|
+
----------
|
|
1026
|
+
start : float
|
|
1027
|
+
Start time of the segment in seconds.
|
|
1028
|
+
end : float or None
|
|
1029
|
+
End time of the segment in seconds. If `None`, the segment will
|
|
1030
|
+
extend to the end of the recording buffer.
|
|
814
1031
|
|
|
815
|
-
|
|
1032
|
+
Returns
|
|
1033
|
+
-------
|
|
1034
|
+
AudioClip or None
|
|
1035
|
+
Segment of the recording buffer. Returns `None` if the recording
|
|
1036
|
+
buffer is empty.
|
|
1037
|
+
|
|
1038
|
+
"""
|
|
1039
|
+
if self.recordingEmpty:
|
|
1040
|
+
return None
|
|
1041
|
+
|
|
1042
|
+
self._mergeAudioFragments() # merge audio fragments
|
|
1043
|
+
|
|
1044
|
+
if not len(self._recording[0].samples):
|
|
1045
|
+
raise AudioStreamError(
|
|
1046
|
+
"Could not access recording as microphone has sent no samples."
|
|
1047
|
+
)
|
|
1048
|
+
|
|
1049
|
+
if start == 0 and end is None: # return full recording
|
|
1050
|
+
return self._recording[0]
|
|
1051
|
+
|
|
1052
|
+
# get a range of samples within the recording buffer
|
|
1053
|
+
idxStart = int(start * self._sampleRateHz)
|
|
1054
|
+
idxEnd = -1 if end is None else int(end * self._sampleRateHz)
|
|
1055
|
+
|
|
1056
|
+
return AudioClip(
|
|
1057
|
+
np.array(self._recording[0].samples[idxStart:idxEnd, :],
|
|
1058
|
+
dtype=np.float32, order='C'),
|
|
1059
|
+
sampleRateHz=self._sampleRateHz)
|
|
816
1060
|
|
|
817
1061
|
def getRecording(self):
|
|
818
1062
|
"""Get audio data from the last microphone recording.
|
|
@@ -828,12 +1072,15 @@ class MicrophoneDevice(BaseDevice, aliases=["mic", "microphone"]):
|
|
|
828
1072
|
|
|
829
1073
|
"""
|
|
830
1074
|
if self.isStarted:
|
|
831
|
-
|
|
832
|
-
"Cannot get audio clip
|
|
833
|
-
"
|
|
834
|
-
|
|
835
|
-
|
|
1075
|
+
logging.warn(
|
|
1076
|
+
"Cannot get audio clip while recording is in progress, so "
|
|
1077
|
+
"stopping recording now."
|
|
1078
|
+
)
|
|
1079
|
+
self.stop()
|
|
836
1080
|
|
|
1081
|
+
# get the segment
|
|
1082
|
+
return self._getSegment() # full recording
|
|
1083
|
+
|
|
837
1084
|
def getCurrentVolume(self, timeframe=0.2):
|
|
838
1085
|
"""
|
|
839
1086
|
Get the current volume measured by the mic.
|
|
@@ -846,20 +1093,45 @@ class MicrophoneDevice(BaseDevice, aliases=["mic", "microphone"]):
|
|
|
846
1093
|
Returns
|
|
847
1094
|
-------
|
|
848
1095
|
float
|
|
849
|
-
Current volume registered by the mic, will depend on relative volume
|
|
850
|
-
should mostly be between 0 (total silence) and 1
|
|
1096
|
+
Current volume registered by the mic, will depend on relative volume
|
|
1097
|
+
of the mic but should mostly be between 0 (total silence) and 1
|
|
1098
|
+
(very loud).
|
|
1099
|
+
|
|
851
1100
|
"""
|
|
852
1101
|
# if mic hasn't started yet, return 0 as it's recorded nothing
|
|
853
|
-
if not self.isStarted:
|
|
1102
|
+
if not self.isStarted or self._stream._closed:
|
|
854
1103
|
return 0
|
|
1104
|
+
|
|
855
1105
|
# poll most recent samples
|
|
856
1106
|
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
1107
|
|
|
862
|
-
|
|
1108
|
+
if self.recordingEmpty:
|
|
1109
|
+
return 0.0
|
|
1110
|
+
|
|
1111
|
+
# merge last few recording fragments into a single segment
|
|
1112
|
+
requiredSamples = int(timeframe * self._sampleRateHz)
|
|
1113
|
+
sampleBuffers = []
|
|
1114
|
+
for segment in reversed(self._recording):
|
|
1115
|
+
nSamples = segment.samples.shape[0]
|
|
1116
|
+
requiredSamples -= nSamples
|
|
1117
|
+
if requiredSamples < 0:
|
|
1118
|
+
sampleBuffers.insert(0, segment.samples[requiredSamples:, :])
|
|
1119
|
+
break
|
|
1120
|
+
sampleBuffers.insert(0, segment.samples)
|
|
1121
|
+
|
|
1122
|
+
# merge the samples
|
|
1123
|
+
sampleBuffer = np.concatenate(
|
|
1124
|
+
sampleBuffers, axis=0, dtype=np.float32)
|
|
1125
|
+
|
|
1126
|
+
clip = AudioClip(sampleBuffer, sampleRateHz=self._sampleRateHz)
|
|
1127
|
+
|
|
1128
|
+
# get average volume
|
|
1129
|
+
rms = clip.rms() * 10
|
|
1130
|
+
|
|
1131
|
+
# round
|
|
1132
|
+
rms = np.round(rms.astype(np.float64), decimals=3)
|
|
1133
|
+
|
|
1134
|
+
return rms
|
|
863
1135
|
|
|
864
1136
|
def addListener(self, listener, startLoop=False):
|
|
865
1137
|
"""
|
|
@@ -876,10 +1148,12 @@ class MicrophoneDevice(BaseDevice, aliases=["mic", "microphone"]):
|
|
|
876
1148
|
If True, then upon adding the listener, start up an asynchronous loop to dispatch messages.
|
|
877
1149
|
"""
|
|
878
1150
|
# add listener as normal
|
|
879
|
-
BaseResponseDevice.addListener(self, listener, startLoop=startLoop)
|
|
1151
|
+
listener = BaseResponseDevice.addListener(self, listener, startLoop=startLoop)
|
|
880
1152
|
# if we're starting a listener loop, start recording
|
|
881
1153
|
if startLoop:
|
|
882
1154
|
self.start()
|
|
1155
|
+
|
|
1156
|
+
return listener
|
|
883
1157
|
|
|
884
1158
|
def clearListeners(self):
|
|
885
1159
|
"""
|
|
@@ -891,10 +1165,12 @@ class MicrophoneDevice(BaseDevice, aliases=["mic", "microphone"]):
|
|
|
891
1165
|
True if completed successfully
|
|
892
1166
|
"""
|
|
893
1167
|
# clear listeners as normal
|
|
894
|
-
BaseResponseDevice.clearListeners(self)
|
|
1168
|
+
resp = BaseResponseDevice.clearListeners(self)
|
|
895
1169
|
# stop recording
|
|
896
1170
|
self.stop()
|
|
897
1171
|
|
|
1172
|
+
return resp
|
|
1173
|
+
|
|
898
1174
|
def dispatchMessages(self, clear=True):
|
|
899
1175
|
"""
|
|
900
1176
|
Dispatch current volume as a MicrophoneResponse object to any attached listeners.
|
|
@@ -905,27 +1181,112 @@ class MicrophoneDevice(BaseDevice, aliases=["mic", "microphone"]):
|
|
|
905
1181
|
If True, will clear the recording up until now after dispatching the volume. This is
|
|
906
1182
|
useful if you're just sampling volume and aren't wanting to store the recording.
|
|
907
1183
|
"""
|
|
1184
|
+
# if mic is not recording, there's nothing to dispatch
|
|
1185
|
+
if not self.isStarted:
|
|
1186
|
+
return
|
|
1187
|
+
|
|
1188
|
+
# poll the mic now
|
|
1189
|
+
self.poll()
|
|
908
1190
|
# create a response object
|
|
909
1191
|
message = MicrophoneResponse(
|
|
910
1192
|
logging.defaultClock.getTime(),
|
|
911
|
-
self.getCurrentVolume()
|
|
1193
|
+
self.getCurrentVolume(),
|
|
1194
|
+
device=self,
|
|
912
1195
|
)
|
|
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
1196
|
# dispatch to listeners
|
|
927
1197
|
for listener in self.listeners:
|
|
928
1198
|
listener.receiveMessage(message)
|
|
1199
|
+
|
|
1200
|
+
return message
|
|
1201
|
+
|
|
1202
|
+
def findSpeakers(self, allowedSpeakers=None, threshold=0.01):
|
|
1203
|
+
"""
|
|
1204
|
+
Find speakers which this microphone can hear.
|
|
1205
|
+
|
|
1206
|
+
Parameters
|
|
1207
|
+
----------
|
|
1208
|
+
allowedSpeakers : list[SpeakerDevice or dict] or None
|
|
1209
|
+
List of speakers to test, or leave as None to test all speakers. If speakers are given
|
|
1210
|
+
as a dict, SpeakerDevice objects will be created via DeviceManager.
|
|
1211
|
+
threshold : float
|
|
1212
|
+
Necessary difference in volume (dB) between sound playing and not playing to conclude
|
|
1213
|
+
that this mic can hear the given speaker.
|
|
1214
|
+
|
|
1215
|
+
Returns
|
|
1216
|
+
-------
|
|
1217
|
+
list[SpeakerDevice]
|
|
1218
|
+
List of speakers which this MicrophoneDevice can hear
|
|
1219
|
+
"""
|
|
1220
|
+
from psychopy import sound
|
|
1221
|
+
from psychopy.hardware import DeviceManager
|
|
1222
|
+
|
|
1223
|
+
def _takeReading(dur):
|
|
1224
|
+
"""
|
|
1225
|
+
Take a reading from this MicrophoneDevice and return the average volume.
|
|
1226
|
+
|
|
1227
|
+
Parameters
|
|
1228
|
+
----------
|
|
1229
|
+
dur : float
|
|
1230
|
+
Time (s) to read for
|
|
1231
|
+
|
|
1232
|
+
Returns
|
|
1233
|
+
-------
|
|
1234
|
+
float
|
|
1235
|
+
Average volume across samples and channels during the reading
|
|
1236
|
+
"""
|
|
1237
|
+
# countdown for the duration
|
|
1238
|
+
countdown = core.CountdownTimer(dur)
|
|
1239
|
+
# start recording
|
|
1240
|
+
self.start()
|
|
1241
|
+
# poll while active
|
|
1242
|
+
while countdown.getTime() > 0:
|
|
1243
|
+
self.poll()
|
|
1244
|
+
# get volume
|
|
1245
|
+
vol = self.getCurrentVolume(timeframe=dur)
|
|
1246
|
+
# if multi-channel, take the max
|
|
1247
|
+
try:
|
|
1248
|
+
vol = max(vol)
|
|
1249
|
+
except TypeError:
|
|
1250
|
+
pass
|
|
1251
|
+
# stop recording
|
|
1252
|
+
self.stop()
|
|
1253
|
+
|
|
1254
|
+
return vol
|
|
1255
|
+
|
|
1256
|
+
# if no allowed speakers given, use all
|
|
1257
|
+
if allowedSpeakers is None:
|
|
1258
|
+
allowedSpeakers = DeviceManager.getAvailableDevices(
|
|
1259
|
+
"psychopy.hardware.speaker.SpeakerDevice"
|
|
1260
|
+
)
|
|
1261
|
+
# list of found speakers
|
|
1262
|
+
foundSpeakers = []
|
|
1263
|
+
# iterate through allowed speakers
|
|
1264
|
+
for speaker in allowedSpeakers:
|
|
1265
|
+
# if given a dict, actualise it
|
|
1266
|
+
if isinstance(speaker, dict):
|
|
1267
|
+
speakerProfile = speaker
|
|
1268
|
+
speaker = DeviceManager.getDevice(speakerProfile['deviceName'])
|
|
1269
|
+
if speaker is None:
|
|
1270
|
+
speaker = DeviceManager.addDevice(**speakerProfile)
|
|
1271
|
+
# generate a sound for this speaker
|
|
1272
|
+
try:
|
|
1273
|
+
snd = sound.Sound("A", stereo=True, speaker=speaker)
|
|
1274
|
+
except:
|
|
1275
|
+
# silently skip on error
|
|
1276
|
+
continue
|
|
1277
|
+
# get a baseline volume
|
|
1278
|
+
baseline = _takeReading(1)
|
|
1279
|
+
# start playing a beep
|
|
1280
|
+
snd.play()
|
|
1281
|
+
# get an active volume
|
|
1282
|
+
active = _takeReading(1)
|
|
1283
|
+
# stop the beep
|
|
1284
|
+
snd.stop()
|
|
1285
|
+
# if the difference is above the threshold, speaker is good
|
|
1286
|
+
if active - baseline > threshold:
|
|
1287
|
+
foundSpeakers.append(speaker)
|
|
1288
|
+
|
|
1289
|
+
return foundSpeakers
|
|
929
1290
|
|
|
930
1291
|
|
|
931
1292
|
class RecordingBuffer:
|
|
@@ -962,20 +1323,16 @@ class RecordingBuffer:
|
|
|
962
1323
|
|
|
963
1324
|
"""
|
|
964
1325
|
def __init__(self, sampleRateHz=SAMPLE_RATE_48kHz, channels=2,
|
|
965
|
-
maxRecordingSize=
|
|
1326
|
+
maxRecordingSize=None, policyWhenFull='ignore'):
|
|
966
1327
|
self._channels = channels
|
|
967
1328
|
self._sampleRateHz = sampleRateHz
|
|
968
|
-
self._maxRecordingSize = maxRecordingSize
|
|
1329
|
+
self._maxRecordingSize = maxRecordingSize or 64000
|
|
969
1330
|
self._samples = None # `ndarray` created in _allocRecBuffer`
|
|
970
1331
|
self._offset = 0 # recording offset
|
|
971
1332
|
self._lastSample = 0 # offset of the last sample from stream
|
|
972
1333
|
self._spaceRemaining = None # set in `_allocRecBuffer`
|
|
973
1334
|
self._totalSamples = None # set in `_allocRecBuffer`
|
|
974
1335
|
|
|
975
|
-
# check if the value is valid
|
|
976
|
-
if policyWhenFull not in ['ignore', 'warn', 'error']:
|
|
977
|
-
raise ValueError("Invalid value for `policyWhenFull`.")
|
|
978
|
-
|
|
979
1336
|
self._policyWhenFull = policyWhenFull
|
|
980
1337
|
self._warnedRecBufferFull = False
|
|
981
1338
|
self._loops = 0
|
|
@@ -1126,9 +1483,8 @@ class RecordingBuffer:
|
|
|
1126
1483
|
"""
|
|
1127
1484
|
nSamples = len(samples)
|
|
1128
1485
|
if self.isFull:
|
|
1129
|
-
if self._policyWhenFull
|
|
1130
|
-
|
|
1131
|
-
elif self._policyWhenFull == 'warn':
|
|
1486
|
+
if self._policyWhenFull in ('warn', 'warning'):
|
|
1487
|
+
# if policy is warn, we log a warning then proceed as if ignored
|
|
1132
1488
|
if not self._warnedRecBufferFull:
|
|
1133
1489
|
logging.warning(
|
|
1134
1490
|
f"Audio recording buffer filled! This means that no "
|
|
@@ -1139,10 +1495,29 @@ class RecordingBuffer:
|
|
|
1139
1495
|
self._warnedRecBufferFull = True
|
|
1140
1496
|
return nSamples
|
|
1141
1497
|
elif self._policyWhenFull == 'error':
|
|
1498
|
+
# if policy is error, we fully error
|
|
1142
1499
|
raise AudioRecordingBufferFullError(
|
|
1143
1500
|
"Cannot write samples, recording buffer is full.")
|
|
1501
|
+
elif self._policyWhenFull == ('rolling', 'roll'):
|
|
1502
|
+
# if policy is rolling, we clear the first half of the buffer
|
|
1503
|
+
toSave = self._totalSamples - len(samples)
|
|
1504
|
+
# get last 0.1s so we still have enough for volume measurement
|
|
1505
|
+
savedSamples = self._recording._samples[-toSave:, :]
|
|
1506
|
+
# log
|
|
1507
|
+
if not self._warnedRecBufferFull:
|
|
1508
|
+
logging.warning(
|
|
1509
|
+
f"Microphone buffer reached, as policy when full is 'roll'/'rolling' the "
|
|
1510
|
+
f"oldest samples will be cleared to make room for new samples."
|
|
1511
|
+
)
|
|
1512
|
+
logging.flush()
|
|
1513
|
+
self._warnedRecBufferFull = True
|
|
1514
|
+
# clear samples
|
|
1515
|
+
self._recording.clear()
|
|
1516
|
+
# reassign saved samples
|
|
1517
|
+
self._recording.write(savedSamples)
|
|
1144
1518
|
else:
|
|
1145
|
-
|
|
1519
|
+
# if policy is to ignore, we simply don't write new samples
|
|
1520
|
+
return nSamples
|
|
1146
1521
|
|
|
1147
1522
|
if not nSamples: # no samples came out of the stream, just return
|
|
1148
1523
|
return
|
|
@@ -1197,6 +1572,11 @@ class RecordingBuffer:
|
|
|
1197
1572
|
idxStart = int(start * self._sampleRateHz)
|
|
1198
1573
|
idxEnd = self._lastSample if end is None else int(
|
|
1199
1574
|
end * self._sampleRateHz)
|
|
1575
|
+
|
|
1576
|
+
if not len(self._samples):
|
|
1577
|
+
raise AudioStreamError(
|
|
1578
|
+
"Could not access recording as microphone has sent no samples."
|
|
1579
|
+
)
|
|
1200
1580
|
|
|
1201
1581
|
return AudioClip(
|
|
1202
1582
|
np.array(self._samples[idxStart:idxEnd, :],
|