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