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.

Files changed (226) hide show
  1. psychopy/VERSION +1 -1
  2. psychopy/alerts/alertsCatalogue/4810.yaml +19 -0
  3. psychopy/alerts/alertsCatalogue/alertCategories.yaml +4 -0
  4. psychopy/alerts/alertsCatalogue/alertmsg.py +15 -1
  5. psychopy/alerts/alertsCatalogue/generateAlertmsg.py +2 -2
  6. psychopy/app/Resources/classic/add_many.png +0 -0
  7. psychopy/app/Resources/classic/add_many@2x.png +0 -0
  8. psychopy/app/Resources/classic/devices.png +0 -0
  9. psychopy/app/Resources/classic/devices@2x.png +0 -0
  10. psychopy/app/Resources/classic/photometer.png +0 -0
  11. psychopy/app/Resources/classic/photometer@2x.png +0 -0
  12. psychopy/app/Resources/dark/add_many.png +0 -0
  13. psychopy/app/Resources/dark/add_many@2x.png +0 -0
  14. psychopy/app/Resources/dark/devices.png +0 -0
  15. psychopy/app/Resources/dark/devices@2x.png +0 -0
  16. psychopy/app/Resources/dark/photometer.png +0 -0
  17. psychopy/app/Resources/dark/photometer@2x.png +0 -0
  18. psychopy/app/Resources/light/add_many.png +0 -0
  19. psychopy/app/Resources/light/add_many@2x.png +0 -0
  20. psychopy/app/Resources/light/devices.png +0 -0
  21. psychopy/app/Resources/light/devices@2x.png +0 -0
  22. psychopy/app/Resources/light/photometer.png +0 -0
  23. psychopy/app/Resources/light/photometer@2x.png +0 -0
  24. psychopy/app/_psychopyApp.py +35 -13
  25. psychopy/app/builder/builder.py +88 -35
  26. psychopy/app/builder/dialogs/__init__.py +69 -220
  27. psychopy/app/builder/dialogs/dlgsCode.py +29 -8
  28. psychopy/app/builder/dialogs/paramCtrls.py +1468 -904
  29. psychopy/app/builder/validators.py +25 -17
  30. psychopy/app/coder/coder.py +12 -1
  31. psychopy/app/coder/repl.py +5 -2
  32. psychopy/app/colorpicker/__init__.py +1 -1
  33. psychopy/app/deviceManager/__init__.py +1 -0
  34. psychopy/app/deviceManager/addDialog.py +218 -0
  35. psychopy/app/deviceManager/dialog.py +185 -0
  36. psychopy/app/deviceManager/panel.py +191 -0
  37. psychopy/app/deviceManager/utils.py +60 -0
  38. psychopy/app/idle.py +7 -0
  39. psychopy/app/locale/ar_001/LC_MESSAGE/messages.mo +0 -0
  40. psychopy/app/locale/ar_001/LC_MESSAGE/messages.po +12695 -10592
  41. psychopy/app/locale/cs_CZ/LC_MESSAGE/messages.mo +0 -0
  42. psychopy/app/locale/cs_CZ/LC_MESSAGE/messages.po +10199 -24
  43. psychopy/app/locale/da_DK/LC_MESSAGE/messages.mo +0 -0
  44. psychopy/app/locale/da_DK/LC_MESSAGE/messages.po +10199 -24
  45. psychopy/app/locale/de_DE/LC_MESSAGE/messages.mo +0 -0
  46. psychopy/app/locale/de_DE/LC_MESSAGE/messages.po +11221 -9712
  47. psychopy/app/locale/el_GR/LC_MESSAGE/messages.mo +0 -0
  48. psychopy/app/locale/el_GR/LC_MESSAGE/messages.po +10200 -25
  49. psychopy/app/locale/en_NZ/LC_MESSAGE/messages.mo +0 -0
  50. psychopy/app/locale/en_NZ/LC_MESSAGE/messages.po +10200 -25
  51. psychopy/app/locale/en_US/LC_MESSAGE/messages.mo +0 -0
  52. psychopy/app/locale/en_US/LC_MESSAGE/messages.po +10195 -18
  53. psychopy/app/locale/es_CO/LC_MESSAGE/messages.mo +0 -0
  54. psychopy/app/locale/es_CO/LC_MESSAGE/messages.po +11917 -9101
  55. psychopy/app/locale/es_ES/LC_MESSAGE/messages.mo +0 -0
  56. psychopy/app/locale/es_ES/LC_MESSAGE/messages.po +11924 -9103
  57. psychopy/app/locale/es_US/LC_MESSAGE/messages.mo +0 -0
  58. psychopy/app/locale/es_US/LC_MESSAGE/messages.po +11917 -9101
  59. psychopy/app/locale/et_EE/LC_MESSAGE/messages.mo +0 -0
  60. psychopy/app/locale/et_EE/LC_MESSAGE/messages.po +11084 -9569
  61. psychopy/app/locale/fa_IR/LC_MESSAGE/messages.mo +0 -0
  62. psychopy/app/locale/fa_IR/LC_MESSAGE/messages.po +11590 -5806
  63. psychopy/app/locale/fi_FI/LC_MESSAGE/messages.mo +0 -0
  64. psychopy/app/locale/fi_FI/LC_MESSAGE/messages.po +10199 -24
  65. psychopy/app/locale/fr_FR/LC_MESSAGE/messages.mo +0 -0
  66. psychopy/app/locale/fr_FR/LC_MESSAGE/messages.po +11091 -9577
  67. psychopy/app/locale/he_IL/LC_MESSAGE/messages.mo +0 -0
  68. psychopy/app/locale/he_IL/LC_MESSAGE/messages.po +11072 -9549
  69. psychopy/app/locale/hi_IN/LC_MESSAGE/messages.mo +0 -0
  70. psychopy/app/locale/hi_IN/LC_MESSAGE/messages.po +11071 -9559
  71. psychopy/app/locale/hu_HU/LC_MESSAGE/messages.mo +0 -0
  72. psychopy/app/locale/hu_HU/LC_MESSAGE/messages.po +10200 -25
  73. psychopy/app/locale/it_IT/LC_MESSAGE/messages.mo +0 -0
  74. psychopy/app/locale/it_IT/LC_MESSAGE/messages.po +11072 -9560
  75. psychopy/app/locale/ja_JP/LC_MESSAGE/messages.mo +0 -0
  76. psychopy/app/locale/ja_JP/LC_MESSAGE/messages.po +1485 -1137
  77. psychopy/app/locale/ko_KR/LC_MESSAGE/messages.mo +0 -0
  78. psychopy/app/locale/ko_KR/LC_MESSAGE/messages.po +10199 -24
  79. psychopy/app/locale/ms_MY/LC_MESSAGE/messages.mo +0 -0
  80. psychopy/app/locale/ms_MY/LC_MESSAGE/messages.po +11463 -8757
  81. psychopy/app/locale/nl_NL/LC_MESSAGE/messages.mo +0 -0
  82. psychopy/app/locale/nl_NL/LC_MESSAGE/messages.po +10200 -25
  83. psychopy/app/locale/nn_NO/LC_MESSAGE/messages.mo +0 -0
  84. psychopy/app/locale/nn_NO/LC_MESSAGE/messages.po +10200 -25
  85. psychopy/app/locale/pl_PL/LC_MESSAGE/messages.mo +0 -0
  86. psychopy/app/locale/pl_PL/LC_MESSAGE/messages.po +10200 -25
  87. psychopy/app/locale/pt_PT/LC_MESSAGE/messages.mo +0 -0
  88. psychopy/app/locale/pt_PT/LC_MESSAGE/messages.po +11288 -9434
  89. psychopy/app/locale/ro_RO/LC_MESSAGE/messages.mo +0 -0
  90. psychopy/app/locale/ro_RO/LC_MESSAGE/messages.po +10200 -25
  91. psychopy/app/locale/ru_RU/LC_MESSAGE/messages.mo +0 -0
  92. psychopy/app/locale/ru_RU/LC_MESSAGE/messages.po +10199 -24
  93. psychopy/app/locale/sv_SE/LC_MESSAGE/messages.mo +0 -0
  94. psychopy/app/locale/sv_SE/LC_MESSAGE/messages.po +11441 -8747
  95. psychopy/app/locale/tr_TR/LC_MESSAGE/messages.mo +0 -0
  96. psychopy/app/locale/tr_TR/LC_MESSAGE/messages.po +11069 -9545
  97. psychopy/app/locale/zh_CN/LC_MESSAGE/messages.mo +0 -0
  98. psychopy/app/locale/zh_CN/LC_MESSAGE/messages.po +12085 -8268
  99. psychopy/app/locale/zh_TW/LC_MESSAGE/messages.mo +0 -0
  100. psychopy/app/locale/zh_TW/LC_MESSAGE/messages.po +11929 -8022
  101. psychopy/app/plugin_manager/dialog.py +12 -3
  102. psychopy/app/plugin_manager/packageIndex.py +303 -0
  103. psychopy/app/plugin_manager/packages.py +203 -63
  104. psychopy/app/plugin_manager/plugins.py +120 -240
  105. psychopy/app/preferencesDlg.py +6 -1
  106. psychopy/app/psychopyApp.py +16 -4
  107. psychopy/app/runner/runner.py +10 -2
  108. psychopy/app/runner/scriptProcess.py +8 -3
  109. psychopy/app/stdout/stdOutRich.py +11 -4
  110. psychopy/app/themes/icons.py +3 -0
  111. psychopy/app/utils.py +61 -0
  112. psychopy/colors.py +10 -5
  113. psychopy/data/experiment.py +133 -23
  114. psychopy/data/routine.py +12 -0
  115. psychopy/data/staircase.py +42 -20
  116. psychopy/data/trial.py +20 -12
  117. psychopy/data/utils.py +43 -3
  118. psychopy/demos/builder/Experiments/dragAndDrop/drag_and_drop.psyexp +22 -5
  119. psychopy/demos/builder/Experiments/dragAndDrop/stimuli/solutions.xlsx +0 -0
  120. psychopy/demos/builder/Experiments/stroopVoice/stroopVoice.psyexp +2 -12
  121. psychopy/demos/builder/Feature Demos/buttonBox/buttonBoxDemo.psyexp +3 -8
  122. psychopy/demos/builder/Feature Demos/movies/movie.psyexp +220 -0
  123. psychopy/demos/builder/Feature Demos/movies/readme.md +3 -0
  124. psychopy/demos/builder/Feature Demos/visualValidator/visualValidator.psyexp +1 -2
  125. psychopy/demos/builder/Hardware/camera/camera.psyexp +3 -16
  126. psychopy/demos/builder/Hardware/microphone/microphone.psyexp +3 -16
  127. psychopy/demos/coder/hardware/hdf5_extract.py +133 -0
  128. psychopy/event.py +20 -15
  129. psychopy/experiment/_experiment.py +86 -10
  130. psychopy/experiment/components/__init__.py +3 -10
  131. psychopy/experiment/components/_base.py +9 -20
  132. psychopy/experiment/components/button/__init__.py +1 -1
  133. psychopy/experiment/components/buttonBox/__init__.py +50 -54
  134. psychopy/experiment/components/camera/__init__.py +137 -359
  135. psychopy/experiment/components/keyboard/__init__.py +17 -24
  136. psychopy/experiment/components/microphone/__init__.py +61 -110
  137. psychopy/experiment/components/movie/__init__.py +2 -3
  138. psychopy/experiment/components/serialOut/__init__.py +192 -93
  139. psychopy/experiment/components/settings/__init__.py +45 -27
  140. psychopy/experiment/components/sound/__init__.py +82 -73
  141. psychopy/experiment/components/soundsensor/__init__.py +43 -80
  142. psychopy/experiment/devices.py +303 -0
  143. psychopy/experiment/exports.py +20 -18
  144. psychopy/experiment/flow.py +7 -0
  145. psychopy/experiment/loops.py +47 -29
  146. psychopy/experiment/monitor.py +74 -0
  147. psychopy/experiment/params.py +48 -10
  148. psychopy/experiment/plugins.py +28 -108
  149. psychopy/experiment/py2js_transpiler.py +1 -1
  150. psychopy/experiment/routines/__init__.py +1 -1
  151. psychopy/experiment/routines/_base.py +59 -24
  152. psychopy/experiment/routines/audioValidator/__init__.py +19 -155
  153. psychopy/experiment/routines/visualValidator/__init__.py +25 -25
  154. psychopy/hardware/__init__.py +20 -57
  155. psychopy/hardware/button.py +15 -2
  156. psychopy/hardware/camera/__init__.py +2237 -1394
  157. psychopy/hardware/joystick/__init__.py +1 -1
  158. psychopy/hardware/keyboard.py +5 -8
  159. psychopy/hardware/listener.py +4 -1
  160. psychopy/hardware/manager.py +75 -35
  161. psychopy/hardware/microphone.py +53 -7
  162. psychopy/hardware/monitor.py +144 -0
  163. psychopy/hardware/photometer/__init__.py +156 -117
  164. psychopy/hardware/serialdevice.py +16 -2
  165. psychopy/hardware/soundsensor.py +4 -1
  166. psychopy/iohub/devices/deviceConfigValidation.py +2 -1
  167. psychopy/iohub/devices/eyetracker/hw/gazepoint/__init__.py +2 -2
  168. psychopy/iohub/devices/eyetracker/hw/gazepoint/gp3/__init__.py +1 -0
  169. psychopy/iohub/devices/eyetracker/hw/gazepoint/gp3/eyetracker.py +10 -0
  170. psychopy/iohub/devices/keyboard/darwin.py +8 -5
  171. psychopy/iohub/util/__init__.py +7 -8
  172. psychopy/localization/generateTranslationTemplate.py +208 -116
  173. psychopy/localization/messages.pot +4305 -3502
  174. psychopy/monitors/MonitorCenter.py +174 -74
  175. psychopy/plugins/__init__.py +6 -4
  176. psychopy/preferences/devices.py +80 -0
  177. psychopy/preferences/generateHints.py +2 -1
  178. psychopy/preferences/preferences.py +35 -11
  179. psychopy/scripts/psychopy-pkgutil.py +969 -0
  180. psychopy/scripts/psyexpCompile.py +1 -1
  181. psychopy/session.py +34 -38
  182. psychopy/sound/__init__.py +6 -260
  183. psychopy/sound/audioclip.py +164 -0
  184. psychopy/sound/backend_ptb.py +8 -0
  185. psychopy/sound/backend_pygame.py +10 -0
  186. psychopy/sound/backend_pysound.py +9 -0
  187. psychopy/sound/backends/__init__.py +0 -0
  188. psychopy/sound/microphone.py +3 -0
  189. psychopy/sound/sound.py +58 -0
  190. psychopy/tests/data/correctScript/python/correctNoiseStimComponent.py +1 -1
  191. psychopy/tests/data/duplicateHeaders.csv +2 -0
  192. psychopy/tests/test_app/test_builder/test_BuilderFrame.py +22 -7
  193. psychopy/tests/test_app/test_builder/test_CompileFromBuilder.py +0 -2
  194. psychopy/tests/test_data/test_utils.py +5 -1
  195. psychopy/tests/test_experiment/test_components/test_ButtonBoxComponent.py +22 -2
  196. psychopy/tests/test_hardware/test_ports.py +0 -12
  197. psychopy/tests/test_tools/test_stringtools.py +1 -1
  198. psychopy/tools/attributetools.py +12 -5
  199. psychopy/tools/fontmanager.py +17 -14
  200. psychopy/tools/gltools.py +4 -2
  201. psychopy/tools/movietools.py +43 -2
  202. psychopy/tools/stringtools.py +33 -8
  203. psychopy/tools/versionchooser.py +1 -1
  204. psychopy/validation/audio.py +5 -1
  205. psychopy/validation/visual.py +5 -1
  206. psychopy/visual/basevisual.py +8 -7
  207. psychopy/visual/circle.py +2 -2
  208. psychopy/visual/helpers.py +3 -1
  209. psychopy/visual/image.py +29 -109
  210. psychopy/visual/movies/__init__.py +1800 -313
  211. psychopy/visual/polygon.py +4 -0
  212. psychopy/visual/shape.py +2 -2
  213. psychopy/visual/window.py +35 -12
  214. psychopy/voicekey/__init__.py +41 -669
  215. psychopy/voicekey/labjack_vks.py +7 -48
  216. psychopy/voicekey/parallel_vks.py +7 -42
  217. psychopy/voicekey/vk_tools.py +114 -263
  218. {psychopy-2025.1.0.dist-info → psychopy-2025.2.1.dist-info}/METADATA +20 -13
  219. {psychopy-2025.1.0.dist-info → psychopy-2025.2.1.dist-info}/RECORD +222 -190
  220. {psychopy-2025.1.0.dist-info → psychopy-2025.2.1.dist-info}/WHEEL +1 -1
  221. psychopy/visual/movies/players/__init__.py +0 -62
  222. psychopy/visual/movies/players/ffpyplayer_player.py +0 -1401
  223. psychopy/voicekey/demo_vks.py +0 -12
  224. psychopy/voicekey/signal.py +0 -42
  225. {psychopy-2025.1.0.dist-info → psychopy-2025.2.1.dist-info}/entry_points.txt +0 -0
  226. {psychopy-2025.1.0.dist-info → psychopy-2025.2.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,684 +1,56 @@
1
- #!/usr/bin/env python
2
- # -*- coding: utf-8 -*-
1
+ from psychopy.localization import _translate
2
+ from psychopy.plugins import PluginStub
3
+ from psychopy import logging
3
4
 
4
- """voicekey: A toolkit for programming virtual voice-keys.
5
5
 
6
- Copyright (c) Jeremy R. Gray, 2015
7
- License: Distributed under the terms of the GPLv3
8
- Dev status: beta. Can work well in some circumstances, not widely tested.
6
+ logging.warning(_translate(
7
+ "psychopy.voicekey is no longer maintained, please use psychopy.hardware.soundSensor instead"
8
+ ))
9
9
 
10
- _BaseVoiceKey is the main abstract class. Subclass and override the detect()
11
- method. See SimpleThresholdVoiceKey or OnsetVoiceKey for examples.
12
- """
13
10
 
14
- __version__ = 0.5
15
-
16
- # pyo: see http://ajaxsoundstudio.com/pyodoc
17
- try:
18
- import pyo64 as pyo
19
- have_pyo64 = True
20
- except Exception:
21
- import pyo
22
- have_pyo64 = False
23
-
24
- # pyo_server will point to a booted pyo server once pyo_init() is called:
25
- pyo_server = None
26
-
27
- # helper functions for time, signal processing, and file I/O:
28
- from . vk_tools import *
29
-
30
- # Constants:
31
- T_BASELINE_PERIOD = 0.200 # sec; time assumed not to contain any speech
32
- T_BASELINE_ON = 0.035 # sec; self.baseline is between T_BASELINE_ON ..OFF
33
- T_BASELINE_OFF = 0.180 # sec
34
- TOO_LOUD = 0.01
35
- TOO_QUIET = 10 ** -7
36
- RATE = 44100 # default sampling rate
37
-
38
- # max recording time 30 minutes; longer is ok but not tested, lots of lag:
39
- MAX_RECORDING_SEC = 1800
40
-
41
-
42
- class VoiceKeyException(Exception):
11
+ class VoiceKeyException(
12
+ PluginStub,
13
+ plugin="psychopy-legacy",
14
+ docsHome="https://psychopy.github.io/psychopy-legacy"
15
+ ):
43
16
  pass
44
17
 
45
18
 
46
- class _BaseVoiceKey:
47
- """Abstract base class for virtual voice-keys.
48
-
49
- Accepts data as real-time input (from a microphone by default) or off-line
50
- (if `file_in` is a valid file).
51
- Over-ride detect() and other methods as needed. See examples.
52
- """
53
-
54
- def __init__(self, sec=0, file_out='', file_in='', **config):
55
- """
56
- :Parameters:
57
-
58
- sec:
59
- duration to record in seconds
60
-
61
- file_out:
62
- name for output filename (for microphone input)
63
-
64
- file_in:
65
- name of input file for sound source (not microphone)
66
-
67
- config: kwargs dict of parameters for configuration. defaults are:
68
-
69
- 'msPerChunk': 2; duration of each real-time analysis chunk, in ms
70
-
71
- 'signaler': default None
72
-
73
- 'autosave': True; False means manual saving to a file is still
74
- possible (by calling .save() but not called automatically upon
75
- stopping
76
-
77
- 'chnl_in' : microphone channel;
78
- see psychopy.sound.backend.get_input_devices()
79
-
80
- 'chnl_out': not implemented; output device to use
81
-
82
- 'start': 0, select section from a file based on (start, stop) time
83
-
84
- 'stop': -1, end of file (default)
85
-
86
- 'vol': 0.99, volume 0..1
87
-
88
- 'low': 100, Hz, low end of bandpass; can vary for M/F speakers
89
-
90
- 'high': 3000, Hz, high end of bandpass
91
-
92
- 'threshold': 10
93
-
94
- 'baseline': 0; 0 = auto-detect; give a non-zero value to use that
95
-
96
- 'more_processing': True; compute more stats per chunk including
97
- bandpass; try False if 32-bit python can't keep up
98
-
99
- 'zero_crossings': True
100
- """
101
- if not (pyo_server and pyo_server.getIsBooted() and
102
- pyo_server.getIsStarted()):
103
- msg = 'Need a running pyo server: call voicekey.pyo_init()'
104
- raise VoiceKeyException(msg)
105
- self.rate = pyo_server.getSamplingRate() # pyo_init enforces 16000+ Hz
106
- self.sec = float(sec)
107
- if self.sec > MAX_RECORDING_SEC:
108
- msg = 'for recording, time in seconds cannot be longer than {0}'
109
- raise VoiceKeyException(msg.format(MAX_RECORDING_SEC))
110
-
111
- # detect whether given a numpy array directly
112
- # TO-DO: self.array_in handling needs code review
113
- source = file_in
114
- self.array_in = []
115
- if type(source) in [np.ndarray]:
116
- self.array_in = source
117
- file_in = '<array len={0}>'.format(len(source))
118
- self.file_in, self.file_out = file_in, file_out
119
-
120
- # Configuration defaults:
121
- self.config = {'msPerChunk': 2,
122
- 'signaler': None,
123
- 'autosave': True,
124
- 'chnl_in': 0, # pyo.pa_get_default_input()
125
- # 'chnl_out': 2, # pyo.pa_get_default_output() no go
126
- 'start': 0,
127
- 'stop': -1,
128
- 'vol': 0.99,
129
- 'low': 100,
130
- 'high': 3000,
131
- 'threshold': 10,
132
- 'baseline': 0,
133
- 'more_processing': True,
134
- 'zero_crossings': True}
135
- self.config.update(config)
136
- self.baseline = self.config['baseline']
137
- self.bad_baseline = False
138
- self.stopped = False
139
- self.msPerChunk = float(self.config['msPerChunk'])
140
- if not 0.65 <= self.msPerChunk <= 32:
141
- msg = 'msPerChunk should be 0.65 to 32; suggested = 2'
142
- raise ValueError(msg)
143
-
144
- self._set_source()
145
- self._set_defaults()
146
- self._set_signaler()
147
- self._set_tables()
148
-
149
- def _set_source(self):
150
- """Data source: file_in, array, or microphone
151
- """
152
- if os.path.isfile(self.file_in):
153
- _rate, self._sndTable = table_from_file(self.file_in,
154
- start=self.config['start'],
155
- stop=self.config['stop'])
156
- if _rate != self.rate:
157
- print('file sample rate differs from the voice-key rate.')
158
- self._source = pyo.TableRead(self._sndTable,
159
- freq=self._sndTable.getRate(),
160
- mul=self.config['vol'])
161
- self.sec = self._sndTable.getDur()
162
- elif len(self.array_in):
163
- self._sndTable = table_from_samples(self.array_in,
164
- start=self.config['start'],
165
- stop=self.config['stop'],
166
- rate=self.rate)
167
- self._source = pyo.TableRead(self._sndTable,
168
- freq=self._sndTable.getRate(),
169
- mul=self.config['vol'])
170
- self.sec = self._sndTable.size / self.rate
171
- else:
172
- # fall through to source = microphone
173
- ch = self.config['chnl_in']
174
- self._source = pyo.Input(chnl=ch, mul=self.config['vol'])
175
-
176
- def _set_defaults(self):
177
- """Set remaining defaults, initialize lists to hold summary stats
178
- """
179
- # adjust self.sec based on start, stop times:
180
- if (self.config['start'], self.config['stop']) != (0, -1):
181
- if self.config['stop'] > self.config['start']:
182
- self.sec = self.config['stop'] - self.config['start']
183
- elif self.config['start'] <= self.sec:
184
- self.sec = self.sec - self.config['start']
185
- self.chunks = int(self.sec * 1000. / self.msPerChunk) # ideal no slip
186
- # total chunk count and current-chunk index:
187
- self.count = 0
188
-
189
- self.filename = self.file_out or 'rec.wav'
190
- self.filesize = None
191
-
192
- # timing data for diagnostics
193
- self.elapsed = 0
194
- self.t_enter = [] # time at chunk entry
195
- self.t_exit = [] # time at chunk exit
196
- self.t_proc = [] # proportion of chunk-time spent doing _do_chunk
197
-
198
- # data cache:
199
- self.data = [] # raw unprocessed data, in chunks
200
- self.power = []
201
- self.power_bp = []
202
- self.power_above = []
203
- self.zcross = []
204
- self.max_bp = 0
205
- self.max_bp_chunk = None
206
- bandpass_pre_cache(rate=self.rate) # for faster bandpass filtering
207
-
208
- # default event parameters:
209
- self.event_detected = False
210
- self.event_lag = 0 # lag required to detect the event prior to trip()
211
- self.event_time = 0 # becomes time of detected event = time at trip()
212
- self.event_onset = 0 # best estimate of the onset of the event
213
-
214
- def _set_signaler(self):
215
- """Set the signaler to be called by trip()
216
- """
217
- if not self.config['signaler']:
218
- self.config['signaler'] = None # _BaseVoiceKeySignal()
219
- self.event_signaler = self.config['signaler']
220
-
221
- def _set_tables(self):
222
- """Set up the pyo tables (allocate memory, etc).
223
-
224
- One source -> three pyo tables: chunk=short, whole=all, baseline.
225
- triggers fill tables from self._source; make triggers in .start()
226
- """
227
- sec_per_chunk = self.msPerChunk / 1000.
228
- self._chunktable = pyo.NewTable(length=sec_per_chunk)
229
- self._wholetable = pyo.NewTable(length=self.sec)
230
- if self.baseline < TOO_QUIET:
231
- self._baselinetable = pyo.NewTable(length=T_BASELINE_OFF)
232
-
233
- def _set_baseline(self):
234
- """Set self.baseline = rms(silent period) using _baselinetable data.
235
-
236
- Called automatically (via pyo trigger) when the baseline table
237
- is full. This is better than using chunks (which have gaps between
238
- them) or the whole table (which can be very large = slow to work
239
- with).
240
- """
241
- data = np.array(self._baselinetable.getTable())
242
- tstart = int(T_BASELINE_ON * self.rate)
243
- segment_power = rms(data[tstart:])
244
-
245
- # Look for bad baseline period:
246
- if self.baseline > TOO_LOUD:
247
- self.bad_baseline = True
248
-
249
- # Dubiously quiet is bad too:
250
- if segment_power < TOO_QUIET:
251
- self.stop()
252
- msg = ('Baseline period is TOO quiet\nwrong input '
253
- 'channel selected? device-related initial delay?')
254
- raise ValueError(msg)
255
-
256
- self.baseline = max(segment_power, 1)
257
-
258
- def _process(self, chunk):
259
- """Calculate and store basic stats about the current chunk.
260
-
261
- This gets called every chunk -- keep it efficient, esp 32-bit python
262
- """
263
- # band-pass filtering:
264
- if self.config['more_processing']:
265
- bp_chunk = bandpass(chunk, self.config['low'],
266
- self.config['high'], self.rate)
267
- else:
268
- bp_chunk = chunk
269
-
270
- # loudness after bandpass filtering:
271
- self.power_bp.append(rms(bp_chunk))
272
-
273
- _mx = max(bp_chunk)
274
- if _mx > self.max_bp:
275
- self.max_bp = _mx
276
- self.max_bp_chunk = self.count # chunk containing the max
277
-
278
- if self.config['more_processing']:
279
- # more bandpass
280
- bp3k_chunk = bandpass(chunk, self.config['low'], 3000, self.rate)
281
- bp8k_chunk = bandpass(chunk, self.config['low'], 8000, self.rate)
282
- # "content filtered speech" (~ affect only):
283
- bp2k8k_chunk = bandpass(chunk, 2000, 8000, self.rate)
284
-
285
- # basic loudness:
286
- self.power.append(rms(chunk))
287
-
288
- # above a threshold or not:
289
- above_01 = int(self.power[self.count] > self.config['threshold'])
290
- self.power_above.append(above_01)
291
-
292
- if self.config['zero_crossings']:
293
- # zero-crossings per ms:
294
- zx = zero_crossings(bp_chunk)
295
- self.zcross.append(np.sum(zx) / self.msPerChunk)
296
-
297
- def detect(self):
298
- """Override to define a detection algorithm.
299
- if condition:
300
- self.trip()
301
-
302
- See SimpleThresholdVoiceKey for a minimal usage example, or
303
- VoicelessPlosiveVoiceKey for a more involved one.
304
- """
305
- raise NotImplementedError('override; see SimpleThresholdVoiceKey')
306
-
307
- def trip(self):
308
- """Trip the voice-key; does not stop recording.
309
- """
310
- # calls .start() on the event-signaler thread. Only `detect()` should
311
- # call `trip()`. Customize `.detect()` rather than the logic here.
312
-
313
- self.event_detected = True
314
- self.event_time = self.elapsed
315
- if hasattr(self, 'event_signaler') and self.event_signaler:
316
- self.event_signaler.start()
317
-
318
- def _do_chunk(self):
319
- """Core function to handle a chunk (= a few ms) of input.
320
-
321
- There can be small temporal gaps between or within chunks, i.e.,
322
- `slippage`. Adjust several parameters until this is small: msPerChunk,
323
- and what processing is done within ._process().
324
-
325
- A trigger (`_chunktrig`) signals that `_chunktable` has been filled
326
- and has set `_do_chunk` as the function to call upon triggering.
327
- `.play()` the trigger again to start recording the next chunk.
328
- """
329
- if self.stopped:
330
- return
331
-
332
- self.t_enter.append(get_time())
333
- self.elapsed = self.t_enter[-1] - self.t_enter[0]
334
- self.t_baseline_has_elapsed = bool(self.elapsed > T_BASELINE_PERIOD)
335
-
336
- # Get the table content as np.array
337
- chunk = np.asarray(self._chunktable.getTable())
338
- chunk = np.int16(chunk * 2 ** 15)
339
- self.data.append(chunk)
340
-
341
- # Calc basic stats, then use to detect features
342
- self._process(chunk)
343
- self.detect() # conditionally call trip()
344
-
345
- # Trigger a new chunk recording, or stop if stopped or time is up:
346
- t_end = get_time()
347
- if t_end - self.t_enter[0] < self.sec:
348
- if not self.stopped:
349
- self._chunktrig.play() # *** triggers the next chunk ***
350
- self.count += 1
351
- else:
352
- self.stop()
353
- self.t_exit.append(t_end)
354
-
355
- def start(self, silent=False):
356
- """Start reading and processing audio data from a file or microphone.
357
- """
358
- if self.stopped:
359
- raise VoiceKeyException('cannot start a stopped recording')
360
- self.t_start = get_time()
361
-
362
- # triggers: fill tables, call _do_chunk & _set_baseline:
363
- self._chunktrig = pyo.Trig()
364
- self._chunkrec = pyo.TrigTableRec(self._source, self._chunktrig,
365
- self._chunktable)
366
- self._chunklooper = pyo.TrigFunc(self._chunkrec["trig"],
367
- self._do_chunk)
368
- self._wholetrig = pyo.Trig()
369
- self._wholerec = pyo.TrigTableRec(self._source, self._wholetrig,
370
- self._wholetable)
371
- self._wholestopper = pyo.TrigFunc(self._wholerec["trig"], self.stop)
372
-
373
- # skip if a baseline value was given in config:
374
- if not self.baseline:
375
- self._baselinetrig = pyo.Trig()
376
- self._baselinerec = pyo.TrigTableRec(self._source,
377
- self._baselinetrig,
378
- self._baselinetable)
379
- self._calc_baseline = pyo.TrigFunc(self._baselinerec["trig"],
380
- self._set_baseline)
381
-
382
- # send _source to sound-output (speakers etc) as well:
383
- if self.file_in and not silent:
384
- self._source.out()
385
-
386
- # start calling self._do_chunk by flipping its trigger;
387
- # _do_chunk then triggers itself via _chunktrigger until done:
388
- self._chunktrig.play()
389
- self._wholetrig.play()
390
- self._baselinetrig.play()
391
-
392
- return self
393
-
394
- @property
395
- def slippage(self):
396
- """Diagnostic: Ratio of the actual (elapsed) time to the ideal time.
397
-
398
- Ideal ratio = 1 = sample-perfect acquisition of msPerChunk, without
399
- any gaps between or within chunks. 1. / slippage is the proportion of
400
- samples contributing to chunk stats.
401
- """
402
- if len(self.t_enter) > 1:
403
- diffs = np.array(self.t_enter[1:]) - np.array(self.t_enter[:-1])
404
- ratio = np.mean(diffs) * 1000. / self.msPerChunk
405
- else:
406
- ratio = 0
407
- return ratio
408
-
409
- @property
410
- def started(self):
411
- """Boolean property, whether `.start()` has been called.
412
- """
413
- return bool(hasattr(self, '_chunklooper')) # .start() has been called
414
-
415
- def stop(self):
416
- """Stop a voice-key in progress.
417
-
418
- Ends and saves the recording if using microphone input.
419
- """
420
- # Will be stopped at self.count (= the chunk index), but that is less
421
- # reliable than self.elapsed due to any slippage.
422
-
423
- if self.stopped:
424
- return
425
- self.stopped = True
426
- self.t_stop = get_time()
427
- self._source.stop()
428
- self._chunktrig.stop()
429
- self._wholetrig.stop()
430
-
431
- if self.config['autosave']:
432
- self.save()
433
-
434
- # Calc the proportion of the available time spent doing _do_chunk:
435
- for ch in range(len(self.t_exit)):
436
- t_diff = self.t_exit[ch] - self.t_enter[ch]
437
- self.t_proc.append(t_diff * 1000 / self.msPerChunk)
438
-
439
- def join(self, sec=None):
440
- """Sleep for `sec` or until end-of-input, and then call stop().
441
- """
442
- sleep(sec or self.sec - self.elapsed)
443
- self.stop()
444
-
445
- def wait_for_event(self, plus=0):
446
- """Start, join, and wait until the voice-key trips, or it times out.
447
-
448
- Optionally wait for some extra time, `plus`, before calling `stop()`.
449
- """
450
- if not self.started:
451
- self.start()
452
- while not self.event_time and not self.stopped:
453
- sleep(self.msPerChunk / 1000.)
454
- if not self.stopped:
455
- naptime = min(plus, self.sec - self.elapsed) # approx...
456
- if naptime > 0:
457
- sleep(naptime)
458
- self.stop()
459
- # next sleep() helps avoid pyo error:
460
- # "ReferenceError: weakly-referenced object no longer exists"
461
- sleep(1.5 * self.msPerChunk / 1000.)
462
-
463
- return self.elapsed
464
-
465
- def save(self, ftype='', dtype='int16'):
466
- """Save new data to file, return the size of the saved file (or None).
467
-
468
- The file format is inferred from the filename extension, e.g., `flac`.
469
- This will be overridden by the `ftype` if one is provided; defaults to
470
- `wav` if nothing else seems reasonable. The optional `dtype` (e.g.,
471
- `int16`) can be any of the sample types supported by `pyo`.
472
- """
473
- if self.file_in or not self.count:
474
- return
475
-
476
- self.save_fmt = os.path.splitext(self.filename)[1].lstrip('.')
477
- fmt = ftype or self.save_fmt or 'wav'
478
- if not self.filename.endswith('.' + fmt):
479
- self.filename += '.' + fmt
480
-
481
- # Save the recording (continuous, non-chunked):
482
- end_index = int(self.elapsed * self.rate) # ~samples
483
- if end_index < self._wholetable.size:
484
- dataf = np.asarray(self._wholetable.getTable()[:end_index])
485
- samples_to_file(dataf, self.rate, self.filename,
486
- fmt=fmt, dtype=dtype)
487
- self.sec = pyo.sndinfo(self.filename)[1]
488
- else:
489
- table_to_file(self._wholetable, self.filename,
490
- fmt=fmt, dtype=dtype)
491
- self.filesize = os.path.getsize(self.filename)
492
- return self.filesize
493
-
494
-
495
- class SimpleThresholdVoiceKey(_BaseVoiceKey):
496
- """Class for simple threshold voice key (loudness-based onset detection).
497
-
498
- The "hello world" of voice-keys.
499
- """
500
-
501
- def detect(self):
502
- """Trip if the current chunk's audio power > 10 * baseline loudness.
503
- """
504
- if self.event_detected or not self.baseline:
505
- return
506
- current = self.power[-1]
507
- threshold = 10 * self.baseline
508
- if current > threshold:
509
- self.trip()
510
-
511
-
512
- class OnsetVoiceKey(_BaseVoiceKey):
513
- """Class for speech onset detection.
514
-
515
- Uses bandpass-filtered signal (100-3000Hz). When the voice key trips,
516
- the best voice-onset RT estimate is saved as `self.event_onset`, in sec.
517
-
518
- """
519
-
520
- def detect(self):
521
- """Trip if recent audio power is greater than the baseline.
522
- """
523
- if self.event_detected or not self.baseline:
524
- return
525
- window = 5 # recent hold duration window, in chunks
526
- threshold = 10 * self.baseline
527
- conditions = all([x > threshold for x in self.power_bp[-window:]])
528
- if conditions:
529
- self.event_lag = window * self.msPerChunk / 1000.
530
- self.event_onset = self.elapsed - self.event_lag
531
- self.trip()
532
- self.event_time = self.event_onset
533
-
534
-
535
- class OffsetVoiceKey(_BaseVoiceKey):
536
- """Class to detect the offset of a single-word utterance.
537
- """
538
-
539
- def __init__(self, sec=10, file_out='', file_in='', delay=0.3, **kwargs):
540
- """Record and ends the recording after speech offset. When the voice
541
- key trips, the best voice-offset RT estimate is saved as
542
- `self.event_offset`, in seconds.
543
-
544
- :Parameters:
545
-
546
- `sec`: duration of recording in the absence of speech or
547
- other sounds.
548
-
549
- `delay`: extra time to record after speech offset, default 0.3s.
550
-
551
- The same methods are available as for class OnsetVoiceKey.
552
- """
553
- config = {'sec': sec,
554
- 'file_out': file_out,
555
- 'file_in': file_in,
556
- 'delay': delay}
557
- kwargs.update(config)
558
- super(OffsetVoiceKey, self).__init__(**kwargs)
559
-
560
- def detect(self):
561
- """Listen for onset, offset, delay, then end the recording.
562
- """
563
- if self.event_detected or not self.baseline:
564
- return
565
- if not self.event_onset:
566
- window = 5 # chunks
567
- threshold = 10 * self.baseline
568
- conditions = all([x > threshold for x in self.power_bp[-window:]])
569
- if conditions:
570
- self.event_lag = window * self.msPerChunk / 1000.
571
- self.event_onset = self.elapsed - self.event_lag
572
- self.event_offset = 0
573
- elif not self.event_offset:
574
- window = 25
575
- threshold = 10 * self.baseline
576
- # segment = np.array(self.power_bp[-window:])
577
- conditions = all([x < threshold for x in self.power_bp[-window:]])
578
- # conditions = np.all(segment < threshold)
579
- if conditions:
580
- self.event_lag = window * self.msPerChunk / 1000.
581
- self.event_offset = self.elapsed - self.event_lag
582
- self.event_time = self.event_offset # for plotting
583
- elif self.elapsed > self.event_offset + self.config['delay']:
584
- self.trip()
585
- self.stop()
586
-
587
-
588
- # ----- Convenience classes -------------------------------------------------
589
-
590
- class Recorder(_BaseVoiceKey):
591
- """Convenience class: microphone input only (no real-time analysis).
592
-
593
- Using `record()` is like `.join()`: it will block execution. But it will
594
- also try to save the recording automatically even if interrupted (whereas
595
- `.start().join()` will not do so). This might be especially useful when
596
- making long recordings.
597
- """
598
-
599
- def __init__(self, sec=2, filename='rec.wav'):
600
- super(Recorder, self).__init__(sec, file_out=filename)
601
- # def _set_defaults(self):
602
- # pass
603
-
604
- def __del__(self):
605
- if hasattr(self, 'filename') and not os.path.isfile(self.filename):
606
- self.save()
607
-
608
- def _set_baseline(self):
609
- pass
610
-
611
- def detect(self):
612
- pass
613
-
614
- def _process(self, *args, **kwargs):
615
- pass
616
-
617
- def record(self, sec=None):
618
- try:
619
- self.start().join(sec)
620
- except Exception:
621
- self.save()
622
- raise
623
-
624
-
625
- class Player(_BaseVoiceKey):
626
- """Convenience class: sound output only (no real-time analysis).
627
- """
628
-
629
- def __init__(self, sec=None, source='rec.wav',
630
- start=0, stop=-1, rate=44100):
631
- if type(source) in [np.ndarray]:
632
- sec = len(source) / rate
633
- elif os.path.isfile(source):
634
- sec = pyo.sndinfo(source)[1]
635
- config = {'start': start,
636
- 'stop': stop}
637
- super(Player, self).__init__(sec, file_in=source, **config)
638
- # def _set_defaults(self): # ideally override but need more refactoring
639
- # pass
640
-
641
- def _set_baseline(self):
642
- pass
19
+ class SimpleThresholdVoiceKey(
20
+ PluginStub,
21
+ plugin="psychopy-legacy",
22
+ docsHome="https://psychopy.github.io/psychopy-legacy"
23
+ ):
24
+ pass
643
25
 
644
- def detect(self):
645
- pass
646
26
 
647
- def _process(self, *args, **kwargs):
648
- pass
27
+ class OnsetVoiceKey(
28
+ PluginStub,
29
+ plugin="psychopy-legacy",
30
+ docsHome="https://psychopy.github.io/psychopy-legacy"
31
+ ):
32
+ pass
649
33
 
650
- def play(self, sec=None):
651
- self.start().join(sec)
652
34
 
35
+ class OffsetVoiceKey(
36
+ PluginStub,
37
+ plugin="psychopy-legacy",
38
+ docsHome="https://psychopy.github.io/psychopy-legacy"
39
+ ):
40
+ pass
653
41
 
654
- # ----- pyo initialization (essential) -------------------------------------
655
42
 
656
- def pyo_init(rate=44100, nchnls=1, buffersize=32, duplex=1):
657
- """Start and boot a global pyo server, restarting if needed.
658
- """
659
- global pyo_server
660
- if rate < 16000:
661
- raise ValueError('sample rate must be 16000 or higher')
43
+ class Recorder(
44
+ PluginStub,
45
+ plugin="psychopy-legacy",
46
+ docsHome="https://psychopy.github.io/psychopy-legacy"
47
+ ):
48
+ pass
662
49
 
663
- # re-init
664
- if hasattr(pyo_server, 'shutdown'):
665
- pyo_server.stop()
666
- sleep(0.25) # make sure enough time passes for the server to shutdown
667
- pyo_server.shutdown()
668
- sleep(0.25)
669
- pyo_server.reinit(sr=rate, nchnls=nchnls,
670
- buffersize=buffersize, duplex=duplex)
671
- else:
672
- pyo_server = pyo.Server(sr=rate,
673
- nchnls=nchnls, # 1 = mono
674
- buffersize=buffersize, # ideal = 64 or higher
675
- duplex=duplex) # 1 = input + output
676
- pyo_server.boot().start()
677
50
 
678
- # avoid mac issue of losing first 0.5s if no sound played for ~1 minute:
679
- if sys.platform == 'darwin':
680
- z2 = np.zeros(2)
681
- _sndTable = pyo.DataTable(size=2, init=z2.T.tolist(), chnls=nchnls)
682
- _snd = pyo.TableRead(_sndTable, freq=rate, mul=0)
683
- _snd.play()
684
- time.sleep(0.510)
51
+ class Player(
52
+ PluginStub,
53
+ plugin="psychopy-legacy",
54
+ docsHome="https://psychopy.github.io/psychopy-legacy"
55
+ ):
56
+ pass