psychopy 2025.1.1__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/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 +42 -2
- 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 +52 -6
- 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/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/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/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 +34 -11
- 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.1.dist-info → psychopy-2025.2.1.dist-info}/METADATA +17 -11
- {psychopy-2025.1.1.dist-info → psychopy-2025.2.1.dist-info}/RECORD +216 -184
- {psychopy-2025.1.1.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.1.dist-info → psychopy-2025.2.1.dist-info}/entry_points.txt +0 -0
- {psychopy-2025.1.1.dist-info → psychopy-2025.2.1.dist-info}/licenses/LICENSE +0 -0
psychopy/voicekey/__init__.py
CHANGED
|
@@ -1,684 +1,56 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
648
|
-
|
|
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
|
-
|
|
657
|
-
|
|
658
|
-
""
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
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
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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
|