[tasks] automatic countdown trial start

This commit is contained in:
Jan Grewe 2021-03-06 12:41:25 +01:00
parent 3680178129
commit 08696619f1
3 changed files with 128 additions and 40 deletions

View File

@ -5,6 +5,7 @@ from PyQt5.QtMultimedia import QMediaPlayer
import os import os
import blipblop.constants as cnst import blipblop.constants as cnst
from blipblop.ui.countdownlabel import CountdownLabel
import datetime as dt import datetime as dt
class SettingsPanel(QWidget): class SettingsPanel(QWidget):
@ -14,7 +15,8 @@ class SettingsPanel(QWidget):
self._trial_spinner = QSpinBox() self._trial_spinner = QSpinBox()
self._trial_spinner.setMinimum(5) self._trial_spinner.setMinimum(5)
self._trial_spinner.setMaximum(25) self._trial_spinner.setMaximum(25)
self._trial_spinner.setValue(3) self._trial_spinner.setValue(5)
self._trial_spinner.setToolTip("Number of consecutive trials (5 - 25)")
self._min_delay_spinner = QSpinBox() self._min_delay_spinner = QSpinBox()
self._min_delay_spinner.setMinimum(1) self._min_delay_spinner.setMinimum(1)
@ -28,11 +30,11 @@ class SettingsPanel(QWidget):
self._max_delay_spinner.setValue(5) self._max_delay_spinner.setValue(5)
self._max_delay_spinner.setToolTip("Maximum delay between start of trial and stimulus display [s]") self._max_delay_spinner.setToolTip("Maximum delay between start of trial and stimulus display [s]")
self._pause_spinner = QSpinBox() self._countdown_spinner = QSpinBox()
self._pause_spinner.setMinimum(1) self._countdown_spinner.setMinimum(1)
self._pause_spinner.setMaximum(10) self._countdown_spinner.setMaximum(10)
self._pause_spinner.setValue(3) self._countdown_spinner.setValue(3)
self._pause_spinner.setToolTip("Pause between trials [s]") self._countdown_spinner.setToolTip("Pause between trials [s]")
self._saliency_slider = QSlider(Qt.Horizontal) self._saliency_slider = QSlider(Qt.Horizontal)
self._saliency_slider.setMinimum(0) self._saliency_slider.setMinimum(0)
@ -40,7 +42,7 @@ class SettingsPanel(QWidget):
self._saliency_slider.setSliderPosition(100) self._saliency_slider.setSliderPosition(100)
self._saliency_slider.setTickInterval(25) self._saliency_slider.setTickInterval(25)
self._saliency_slider.setTickPosition(QSlider.TicksBelow) self._saliency_slider.setTickPosition(QSlider.TicksBelow)
self._saliency_slider.setToolTip("Saliency of the stimulus, i.e. its opacity") self._saliency_slider.setToolTip("Saliency of the stimulus, i.e. its loudness")
self._sound_combo = QComboBox() self._sound_combo = QComboBox()
for k in cnst.SNDS_DICT.keys(): for k in cnst.SNDS_DICT.keys():
@ -56,7 +58,7 @@ class SettingsPanel(QWidget):
form_layout.addRow("number of trials", self._trial_spinner) form_layout.addRow("number of trials", self._trial_spinner)
form_layout.addRow("minimum delay [s]", self._min_delay_spinner) form_layout.addRow("minimum delay [s]", self._min_delay_spinner)
form_layout.addRow("maximum delay [s]", self._max_delay_spinner) form_layout.addRow("maximum delay [s]", self._max_delay_spinner)
form_layout.addRow("pause [s]", self._pause_spinner) form_layout.addRow("pause [s]", self._countdown_spinner)
form_layout.addRow("stimulus saliency", self._saliency_slider) form_layout.addRow("stimulus saliency", self._saliency_slider)
form_layout.addRow("stimulus sound", self._sound_combo) form_layout.addRow("stimulus sound", self._sound_combo)
form_layout.addRow("instructions", self._instructions) form_layout.addRow("instructions", self._instructions)
@ -83,8 +85,8 @@ class SettingsPanel(QWidget):
return self._max_delay_spinner.value() return self._max_delay_spinner.value()
@property @property
def pause(self): def countdown(self):
return self._pause_spinner.value() return self._countdown_spinner.value()
@property @property
def sound(self): def sound(self):
@ -93,7 +95,7 @@ class SettingsPanel(QWidget):
def set_enabled(self, enabled): def set_enabled(self, enabled):
self._trial_spinner.setEnabled(enabled) self._trial_spinner.setEnabled(enabled)
self._saliency_slider.setEnabled(enabled) self._saliency_slider.setEnabled(enabled)
self._pause_spinner.setEnabled(enabled) self._countdown_spinner.setEnabled(enabled)
self._min_delay_spinner.setEnabled(enabled) self._min_delay_spinner.setEnabled(enabled)
self._max_delay_spinner.setEnabled(enabled) self._max_delay_spinner.setEnabled(enabled)
self._sound_combo.setEnabled(False) self._sound_combo.setEnabled(False)
@ -135,6 +137,10 @@ class AudioBlop(QWidget):
self._status_label = QLabel("Ready to start, press enter ...") self._status_label = QLabel("Ready to start, press enter ...")
grid.addWidget(self._status_label, 3, 0, Qt.AlignLeft) grid.addWidget(self._status_label, 3, 0, Qt.AlignLeft)
self._countdown_label = CountdownLabel(text="Next trial in:")
grid.addWidget(self._countdown_label, 3, 1, Qt.AlignCenter)
self._countdown_label.countdown_done.connect(self.run_trial)
self._draw_area = QLabel() self._draw_area = QLabel()
self._draw_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self._draw_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
grid.addWidget(self._draw_area, 2, 1) grid.addWidget(self._draw_area, 2, 1)
@ -186,15 +192,20 @@ class AudioBlop(QWidget):
self._response_time = dt.datetime.now() self._response_time = dt.datetime.now()
if self._trial_counter < self._settings.trials: if self._trial_counter < self._settings.trials:
self._status_label.setText("Trial %i of %i, press enter for next trial" % (self._trial_counter, self._settings.trials)) self._status_label.setText("Trial %i of %i" % (self._trial_counter, self._settings.trials))
if self._start_time is None: if self._start_time is None:
self._reaction_times.append(-1000) self._reaction_times.append(-1000)
else: else:
reaction_time = self._response_time - self._start_time reaction_time = self._response_time - self._start_time
self._reaction_times.append(reaction_time.total_seconds()) self._reaction_times.append(reaction_time.total_seconds())
self._trial_running = False self._trial_running = False
if self._timer.isActive():
self._timer.stop()
if self._trial_counter >= self._settings.trials: if self._trial_counter >= self._settings.trials:
self.task_done.emit() self.task_done.emit()
return
self._countdown_label.start(self._settings.countdown)
def reset_canvas(self): def reset_canvas(self):
bkg_color = QColor() bkg_color = QColor()
@ -230,6 +241,9 @@ class AudioBlop(QWidget):
if not self._session_running: if not self._session_running:
self._settings.set_enabled(False) self._settings.set_enabled(False)
self._session_running = True self._session_running = True
self._countdown_label.start(time=self._settings.countdown)
def run_trial(self):
self._trial_running = True self._trial_running = True
if self._trial_counter >= self._settings.trials: if self._trial_counter >= self._settings.trials:
self.task_done.emit self.task_done.emit
@ -244,11 +258,11 @@ class AudioBlop(QWidget):
max_interval = int(self._settings.max_delay * 10) max_interval = int(self._settings.max_delay * 10)
interval = self._random_generator.bounded(min_interval, max_interval) * 100 interval = self._random_generator.bounded(min_interval, max_interval) * 100
self._start_time = None self._start_time = None
timer = QTimer(self) self._timer = QTimer(self)
timer.setSingleShot(True) self._timer.setSingleShot(True)
timer.setInterval(int(interval)) self._timer.setInterval(int(interval))
timer.timeout.connect(self.blip) self._timer.timeout.connect(self.blip)
timer.start() self._timer.start()
def on_abort(self): def on_abort(self):
self.reset() self.reset()
@ -266,6 +280,7 @@ class AudioBlop(QWidget):
self._session_running = False self._session_running = False
self._status_label.setText("Ready to start...") self._status_label.setText("Ready to start...")
self._settings.set_enabled(True) self._settings.set_enabled(True)
self._countdown_label.stop()
def on_toggle_settings(self): def on_toggle_settings(self):
if self._splitter.sizes()[1] > 0: if self._splitter.sizes()[1] > 0:

View File

@ -0,0 +1,42 @@
from PyQt5.QtWidgets import QLabel
from PyQt5.QtCore import QTimer, pyqtSignal
from PyQt5.QtGui import QFont
class CountdownLabel(QLabel):
countdown_done = pyqtSignal()
def __init__(self, parent=None, text=""):
super().__init__(parent=parent)
self._text = text
self._count = 0
self._timer = QTimer()
self._timer.timeout.connect(self.on_timeout)
font = QFont()
font.setBold(True)
font.setPointSize(20)
self.setFont(font)
self.setStyleSheet("color: #2D4B9A")
def start(self, time=3):
if time < 1:
time = 1
self._count = time
self.setText("%s %i" % (self._text, self._count))
self.update()
self._timer.start(1000)
def stop(self):
if self._timer.isActive():
self._timer.stop()
self.setText("")
def on_timeout(self):
self._count -= 1
self.setText("%s %i" % (self._text, self._count))
self.update()
if self._count <= 0:
self._timer.stop()
self.setText("")
self.update()
self.countdown_done.emit()

View File

@ -4,6 +4,7 @@ from PyQt5.QtGui import QColor, QFont, QKeySequence, QPainter, QBrush, QPen, QPi
import os import os
import blipblop.constants as cnst import blipblop.constants as cnst
from blipblop.ui.countdownlabel import CountdownLabel
import datetime as dt import datetime as dt
class SettingsPanel(QWidget): class SettingsPanel(QWidget):
@ -13,17 +14,20 @@ class SettingsPanel(QWidget):
self._trial_spinner = QSpinBox() self._trial_spinner = QSpinBox()
self._trial_spinner.setMinimum(5) self._trial_spinner.setMinimum(5)
self._trial_spinner.setMaximum(25) self._trial_spinner.setMaximum(25)
self._trial_spinner.setValue(3) self._trial_spinner.setValue(5)
self._trial_spinner.setToolTip("Number of consecutive trials (5 - 25)")
self._min_delay_edit = QLineEdit() self._min_delay_spinner = QSpinBox()
self._min_delay_edit.setText(str("1")) self._min_delay_spinner.setMinimum(1)
self._min_delay_edit.setToolTip("Minimum delay between start of trial and stimulus display [s]") self._min_delay_spinner.setMaximum(10)
self._min_delay_edit.setEnabled(False) self._min_delay_spinner.setValue(1)
self._min_delay_spinner.setToolTip("Minimum delay between start of trial and stimulus display [s]")
self._max_delay_edit = QLineEdit() self._max_delay_spinner = QSpinBox()
self._max_delay_edit.setText(str("5")) self._max_delay_spinner.setMinimum(1)
self._max_delay_edit.setToolTip("Maximum delay between start of trial and stimulus display [s]") self._max_delay_spinner.setMaximum(10)
self._max_delay_edit.setEnabled(False) self._max_delay_spinner.setValue(5)
self._max_delay_spinner.setToolTip("Maximum delay between start of trial and stimulus display [s]")
self._saliency_slider = QSlider(Qt.Horizontal) self._saliency_slider = QSlider(Qt.Horizontal)
self._saliency_slider.setMinimum(0) self._saliency_slider.setMinimum(0)
@ -41,6 +45,12 @@ class SettingsPanel(QWidget):
self._size_slider.setTickPosition(QSlider.TicksBelow) self._size_slider.setTickPosition(QSlider.TicksBelow)
self._size_slider.setToolTip("Diameter of the stimulus in pixel") self._size_slider.setToolTip("Diameter of the stimulus in pixel")
self._countdown_spinner = QSpinBox()
self._countdown_spinner.setMinimum(2)
self._countdown_spinner.setMaximum(30)
self._countdown_spinner.setValue(5)
self._countdown_spinner.setToolTip("Pause/countdown for next trial")
self._instructions = QTextEdit() self._instructions = QTextEdit()
self._instructions.setMarkdown("* fixate central cross\n * press start (enter) when ready\n * press space bar as soon as the stimulus occurs") self._instructions.setMarkdown("* fixate central cross\n * press start (enter) when ready\n * press space bar as soon as the stimulus occurs")
self._instructions.setMinimumHeight(200) self._instructions.setMinimumHeight(200)
@ -49,8 +59,9 @@ class SettingsPanel(QWidget):
form_layout = QFormLayout() form_layout = QFormLayout()
form_layout.addRow("Settings", None) form_layout.addRow("Settings", None)
form_layout.addRow("number of trials", self._trial_spinner) form_layout.addRow("number of trials", self._trial_spinner)
form_layout.addRow("minimum delay [s]", self._min_delay_edit) form_layout.addRow("pause until next trial [s]", self._countdown_spinner)
form_layout.addRow("maximum delay [s]", self._max_delay_edit) form_layout.addRow("minimum delay [s]", self._min_delay_spinner)
form_layout.addRow("maximum delay [s]", self._max_delay_spinner)
form_layout.addRow("stimulus saliency", self._saliency_slider) form_layout.addRow("stimulus saliency", self._saliency_slider)
form_layout.addRow("stimulus size", self._size_slider) form_layout.addRow("stimulus size", self._size_slider)
form_layout.addRow("instructions", self._instructions) form_layout.addRow("instructions", self._instructions)
@ -70,15 +81,23 @@ class SettingsPanel(QWidget):
@property @property
def min_delay(self): def min_delay(self):
return int(self._min_delay_edit.text()) return self._min_delay_spinner.value()
@property @property
def max_delay(self): def max_delay(self):
return int(self._max_delay_edit.text()) return self._max_delay_spinner.value()
@property
def countdown(self):
return self._countdown_spinner.value()
def set_enabled(self, enabled): def set_enabled(self, enabled):
self._trial_spinner.setEnabled(enabled) self._trial_spinner.setEnabled(enabled)
self._saliency_slider.setEnabled(enabled) self._saliency_slider.setEnabled(enabled)
self._size_slider.setEnabled(enabled)
self._countdown_spinner.setEnabled(enabled)
self._min_delay_spinner.setEnabled(enabled)
self._max_delay_spinner.setEnabled(enabled)
class VisualBlip(QWidget): class VisualBlip(QWidget):
@ -117,6 +136,10 @@ class VisualBlip(QWidget):
self._status_label = QLabel("Ready to start, press enter ...") self._status_label = QLabel("Ready to start, press enter ...")
grid.addWidget(self._status_label, 3, 0, Qt.AlignBaseline) grid.addWidget(self._status_label, 3, 0, Qt.AlignBaseline)
self._countdown_label = CountdownLabel(text="Next trial in:")
grid.addWidget(self._countdown_label, 3, 1, Qt.AlignCenter)
self._countdown_label.countdown_done.connect(self.run_trial)
self._draw_area = QLabel() self._draw_area = QLabel()
self._draw_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self._draw_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
grid.addWidget(self._draw_area, 2, 1) grid.addWidget(self._draw_area, 2, 1)
@ -168,7 +191,7 @@ class VisualBlip(QWidget):
self._response_time = dt.datetime.now() self._response_time = dt.datetime.now()
if self._trial_counter < self._settings.trials: if self._trial_counter < self._settings.trials:
self._status_label.setText("Trial %i of %i, press enter for next trial" % (self._trial_counter, self._settings.trials)) self._status_label.setText("Trial %i of %i" % (self._trial_counter, self._settings.trials))
if self._start_time is None: if self._start_time is None:
self._reaction_times.append(-1000) self._reaction_times.append(-1000)
else: else:
@ -176,8 +199,12 @@ class VisualBlip(QWidget):
self._reaction_times.append(reaction_time.total_seconds()) self._reaction_times.append(reaction_time.total_seconds())
self.reset_canvas() self.reset_canvas()
self._trial_running = False self._trial_running = False
if self._timer.isActive():
self._timer.stop()
if self._trial_counter >= self._settings.trials: if self._trial_counter >= self._settings.trials:
self.task_done.emit() self.task_done.emit()
return
self._countdown_label.start(self._settings.countdown)
def reset_canvas(self): def reset_canvas(self):
bkg_color = QColor() bkg_color = QColor()
@ -221,6 +248,9 @@ class VisualBlip(QWidget):
if not self._session_running: if not self._session_running:
self._settings.set_enabled(False) self._settings.set_enabled(False)
self._session_running = True self._session_running = True
self._countdown_label.start(time=self._settings.countdown)
def run_trial(self):
self._trial_running = True self._trial_running = True
if self._trial_counter >= self._settings.trials: if self._trial_counter >= self._settings.trials:
self.task_done.emit self.task_done.emit
@ -232,11 +262,11 @@ class VisualBlip(QWidget):
max_interval = int(self._settings.max_delay * 10) max_interval = int(self._settings.max_delay * 10)
interval = self._random_generator.bounded(min_interval, max_interval) * 100 interval = self._random_generator.bounded(min_interval, max_interval) * 100
self._start_time = None self._start_time = None
timer = QTimer(self) self._timer = QTimer(self)
timer.setSingleShot(True) self._timer.setSingleShot(True)
timer.setInterval(int(interval)) self._timer.setInterval(int(interval))
timer.timeout.connect(self.blip) self._timer.timeout.connect(self.blip)
timer.start() self._timer.start()
def on_abort(self): def on_abort(self):
self.reset() self.reset()
@ -254,6 +284,7 @@ class VisualBlip(QWidget):
self._trial_running = False self._trial_running = False
self._session_running = False self._session_running = False
self._status_label.setText("Ready to start...") self._status_label.setText("Ready to start...")
self._countdown_label.stop()
self._settings.set_enabled(True) self._settings.set_enabled(True)
def on_toggle_settings(self): def on_toggle_settings(self):