add sounds
This commit is contained in:
parent
1f87caa5ef
commit
b42c8e035b
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,2 +1,2 @@
|
|||||||
*.pyc
|
*.pyc
|
||||||
__pycache__
|
*__pycache__
|
Binary file not shown.
Binary file not shown.
@ -1,6 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
import glob
|
import glob
|
||||||
from PyQt5.QtGui import QIcon
|
from PyQt5.QtGui import QIcon
|
||||||
|
from PyQt5.QtMultimedia import QSound
|
||||||
|
|
||||||
organization = "bendalab"
|
organization = "bendalab"
|
||||||
application = "blipblop"
|
application = "blipblop"
|
||||||
@ -9,14 +10,34 @@ version = 0.1
|
|||||||
PACKAGE_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
|
PACKAGE_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
|
||||||
ICONS_FOLDER = os.path.join(PACKAGE_ROOT, "icons")
|
ICONS_FOLDER = os.path.join(PACKAGE_ROOT, "icons")
|
||||||
DOCS_ROOT_FILE = os.path.join(PACKAGE_ROOT, "docs", "index.md")
|
DOCS_ROOT_FILE = os.path.join(PACKAGE_ROOT, "docs", "index.md")
|
||||||
|
SNDS_FOLDER = os.path.join(PACKAGE_ROOT, "sounds")
|
||||||
|
|
||||||
ICONS_PATHS = glob.glob(os.path.join(ICONS_FOLDER, "*.png"))
|
ICONS_PATHS = glob.glob(os.path.join(ICONS_FOLDER, "*.png"))
|
||||||
ICONS_PATHS.extend(glob.glob(os.path.join(ICONS_FOLDER, "*.icns")))
|
ICONS_PATHS.extend(glob.glob(os.path.join(ICONS_FOLDER, "*.icns")))
|
||||||
ICONS_PATHS = sorted(ICONS_PATHS)
|
ICONS_PATHS = sorted(ICONS_PATHS)
|
||||||
ICON_DICT = {}
|
ICON_DICT = {}
|
||||||
|
|
||||||
|
SNDS_PATHS = glob.glob(os.path.join(SNDS_FOLDER, "*.wav"))
|
||||||
|
SNDS_PATHS = sorted(SNDS_PATHS)
|
||||||
|
SNDS_DICT = {}
|
||||||
|
|
||||||
for icon in ICONS_PATHS:
|
for icon in ICONS_PATHS:
|
||||||
ICON_DICT[icon.split(os.sep)[-1].split(".")[0]] = icon
|
ICON_DICT[icon.split(os.sep)[-1].split(".")[0]] = icon
|
||||||
|
|
||||||
|
for snd in SNDS_PATHS:
|
||||||
|
SNDS_DICT[snd.split(os.sep)[-1].split(".")[0]] = snd
|
||||||
|
|
||||||
|
|
||||||
|
def get_sound(name):
|
||||||
|
if name in SNDS_DICT.keys():
|
||||||
|
print(name)
|
||||||
|
print(SNDS_DICT[name])
|
||||||
|
return QSound(SNDS_DICT[name])
|
||||||
|
else:
|
||||||
|
print("Sound %s not found!" % name)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_icon(name):
|
def get_icon(name):
|
||||||
if name in ICON_DICT.keys():
|
if name in ICON_DICT.keys():
|
||||||
return QIcon(ICON_DICT[name])
|
return QIcon(ICON_DICT[name])
|
||||||
|
@ -1,20 +1,241 @@
|
|||||||
from PyQt5.QtWidgets import QComboBox, QFrame, QGroupBox, QHBoxLayout, QLabel, QSplitter, QTextEdit, QVBoxLayout, QWidget
|
from PyQt5.QtWidgets import QAction, QFormLayout, QGridLayout, QLabel, QLineEdit, QSizePolicy, QSlider, QSpinBox, QTextEdit, QWidget
|
||||||
from PyQt5.QtCore import QItemSelectionModel, Qt
|
from PyQt5.QtCore import QPoint, QTimer, Qt, pyqtSignal, QSettings
|
||||||
|
from PyQt5.QtGui import QColor, QFont, QKeySequence, QPainter, QBrush, QPen, QPixmap
|
||||||
|
from PyQt5.QtMultimedia import QSound
|
||||||
|
|
||||||
|
import os
|
||||||
import blipblop.constants as cnst
|
import blipblop.constants as cnst
|
||||||
|
import numpy as np
|
||||||
|
import datetime as dt
|
||||||
|
|
||||||
|
class SettingsPanel(QWidget):
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent=parent)
|
||||||
|
|
||||||
|
self._trial_spinner = QSpinBox()
|
||||||
|
self._trial_spinner.setMinimum(5)
|
||||||
|
self._trial_spinner.setMaximum(25)
|
||||||
|
self._trial_spinner.setValue(10)
|
||||||
|
|
||||||
|
self._min_delay_edit = QLineEdit()
|
||||||
|
self._min_delay_edit.setText(str("1000"))
|
||||||
|
self._min_delay_edit.setToolTip("Minimum delay between start of trial and stimulus display")
|
||||||
|
self._min_delay_edit.setEnabled(False)
|
||||||
|
|
||||||
|
self._max_delay_edit = QLineEdit()
|
||||||
|
self._max_delay_edit.setText(str("5000"))
|
||||||
|
self._max_delay_edit.setToolTip("Maximum delay between start of trial and stimulus display")
|
||||||
|
self._max_delay_edit.setEnabled(False)
|
||||||
|
|
||||||
|
self._saliency_slider = QSlider(Qt.Horizontal)
|
||||||
|
self._saliency_slider.setMinimum(0)
|
||||||
|
self._saliency_slider.setMaximum(100)
|
||||||
|
self._saliency_slider.setSliderPosition(100)
|
||||||
|
self._saliency_slider.setTickInterval(25)
|
||||||
|
self._saliency_slider.setTickPosition(QSlider.TicksBelow)
|
||||||
|
self._saliency_slider.setToolTip("Saliency of the stimulus, i.e. its opacity")
|
||||||
|
|
||||||
|
self._size_slider = QSlider(Qt.Horizontal)
|
||||||
|
self._size_slider.setMinimum(0)
|
||||||
|
self._size_slider.setMaximum(200)
|
||||||
|
self._size_slider.setSliderPosition(100)
|
||||||
|
self._size_slider.setTickInterval(25)
|
||||||
|
self._size_slider.setTickPosition(QSlider.TicksBelow)
|
||||||
|
self._size_slider.setToolTip("Diameter of the stimulus in pixel")
|
||||||
|
|
||||||
|
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.setMinimumHeight(200)
|
||||||
|
self._instructions.setReadOnly(True)
|
||||||
|
|
||||||
|
form_layout = QFormLayout()
|
||||||
|
form_layout.addRow("Settings", None)
|
||||||
|
form_layout.addRow("number of trials", self._trial_spinner)
|
||||||
|
form_layout.addRow("minimum delay [ms]", self._min_delay_edit)
|
||||||
|
form_layout.addRow("maximum delay [ms]", self._max_delay_edit)
|
||||||
|
form_layout.addRow("stimulus saliency", self._saliency_slider)
|
||||||
|
form_layout.addRow("stimulus size", self._size_slider)
|
||||||
|
form_layout.addRow("instructions", self._instructions)
|
||||||
|
self.setLayout(form_layout)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def trials(self):
|
||||||
|
return self._trial_spinner.value()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def saliency(self):
|
||||||
|
return self._saliency_slider.sliderPosition()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def size(self):
|
||||||
|
return self._size_slider.sliderPosition()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def min_delay(self):
|
||||||
|
return int(self._min_delay_edit.text())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_delay(self):
|
||||||
|
return int(self._max_delay_edit.text())
|
||||||
|
|
||||||
|
def set_enabled(self, enabled):
|
||||||
|
self._trial_spinner.setEnabled(enabled)
|
||||||
|
self._saliency_slider.setEnabled(enabled)
|
||||||
|
|
||||||
|
|
||||||
class AudioBlop(QWidget):
|
class AudioBlop(QWidget):
|
||||||
|
task_done = pyqtSignal()
|
||||||
|
task_aborted = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, parent=None) -> None:
|
def __init__(self, parent=None) -> None:
|
||||||
super().__init__(parent=parent)
|
super().__init__(parent=parent)
|
||||||
|
|
||||||
vbox = QVBoxLayout()
|
grid = QGridLayout()
|
||||||
|
grid.setColumnStretch(0, 1)
|
||||||
l = QLabel("Auditory task")
|
grid.setColumnStretch(3, 1)
|
||||||
vbox.addWidget(l)
|
grid.setRowStretch(1, 1)
|
||||||
|
grid.setRowStretch(3, 1)
|
||||||
|
self.setLayout(grid)
|
||||||
|
|
||||||
|
l = QLabel("Auditory reaction test")
|
||||||
|
l.setPixmap(QPixmap(os.path.join(cnst.ICONS_FOLDER, "auditory_task.png")))
|
||||||
|
grid.addWidget(l, 0, 0, Qt.AlignLeft)
|
||||||
|
|
||||||
|
self._status_label = QLabel("Ready to start, press enter ...")
|
||||||
|
QFont
|
||||||
|
grid.addWidget(self._status_label, 3, 4, Qt.AlignBaseline)
|
||||||
|
|
||||||
def reset(self):
|
self._draw_area = QLabel()
|
||||||
pass
|
self._draw_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||||
|
grid.addWidget(self._draw_area, 2, 1)
|
||||||
|
|
||||||
|
self._settings = SettingsPanel()
|
||||||
|
grid.addWidget(self._settings, 2, 4)
|
||||||
|
|
||||||
|
self.reset_canvas()
|
||||||
|
self.create_actions()
|
||||||
|
|
||||||
|
self._start_time = None
|
||||||
|
self._response_time = None
|
||||||
|
self._reaction_times = []
|
||||||
|
self._trial_counter = 0
|
||||||
|
self._session_running = False
|
||||||
|
self._trial_running = False
|
||||||
|
|
||||||
|
self.setFocus()
|
||||||
|
|
||||||
|
def create_actions(self):
|
||||||
|
self._start_action = QAction("start trial")
|
||||||
|
self._start_action.setShortcuts([QKeySequence("enter"), QKeySequence("return")])
|
||||||
|
self._start_action.triggered.connect(self.on_trial_start)
|
||||||
|
|
||||||
|
self._reaction = QAction("reaction")
|
||||||
|
self._reaction.setShortcut(QKeySequence("space"))
|
||||||
|
self._reaction.triggered.connect(self.on_reaction)
|
||||||
|
|
||||||
|
self._abort = QAction("abort")
|
||||||
|
self._abort.setShortcut(QKeySequence("escape"))
|
||||||
|
self._abort.triggered.connect(self.on_abort)
|
||||||
|
|
||||||
|
self.addAction(self._start_action)
|
||||||
|
self.addAction(self._reaction)
|
||||||
|
self.addAction(self._abort)
|
||||||
|
|
||||||
|
def on_reaction(self):
|
||||||
|
if not self._session_running or not self._trial_running:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._response_time = dt.datetime.now()
|
||||||
|
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))
|
||||||
|
if self._start_time is None:
|
||||||
|
self._reaction_times.append(-1000)
|
||||||
|
else:
|
||||||
|
reaction_time = self._response_time - self._start_time
|
||||||
|
self._reaction_times.append(reaction_time.total_seconds())
|
||||||
|
self.reset_canvas()
|
||||||
|
self._trial_running = False
|
||||||
|
|
||||||
|
def reset_canvas(self):
|
||||||
|
bkg_color = QColor()
|
||||||
|
bkg_color.setAlphaF(0.0)
|
||||||
|
canvas = QPixmap(400, 400)
|
||||||
|
self._canvas_center = QPoint(200, 200)
|
||||||
|
canvas.fill(bkg_color)
|
||||||
|
self.draw_fixation(canvas)
|
||||||
|
self._draw_area.setPixmap(canvas)
|
||||||
|
self._draw_area.update()
|
||||||
|
|
||||||
|
def draw_fixation(self, pixmap):
|
||||||
|
left = QPoint(175, 200)
|
||||||
|
right = QPoint(225, 200)
|
||||||
|
top = QPoint(200, 175)
|
||||||
|
bottom = QPoint(200, 225)
|
||||||
|
painter = QPainter(pixmap)
|
||||||
|
painter.setPen(QPen(Qt.red, 2, Qt.SolidLine))
|
||||||
|
painter.drawLine(left, right)
|
||||||
|
painter.drawLine(top, bottom)
|
||||||
|
painter.end()
|
||||||
|
self._canvas = QPixmap(400, 400)
|
||||||
|
self._canvas_center = QPoint(200, 200)
|
||||||
|
self._draw_area.setPixmap(self._canvas)
|
||||||
|
|
||||||
|
def blip(self):
|
||||||
|
bells = cnst.get_sound("message")
|
||||||
|
bells.setLoops(10)
|
||||||
|
#QSound("mysounds/bells.wav");
|
||||||
|
bells.play();
|
||||||
|
stim_size = self._settings.size
|
||||||
|
painter = QPainter(self._draw_area.pixmap())
|
||||||
|
painter.setPen(QPen(Qt.red, 1, Qt.SolidLine))
|
||||||
|
color = QColor(Qt.red)
|
||||||
|
color.setAlphaF(self._settings.saliency/100)
|
||||||
|
painter.setBrush(QBrush(color, Qt.SolidPattern))
|
||||||
|
painter.drawEllipse(self._canvas_center, stim_size, stim_size)
|
||||||
|
painter.end()
|
||||||
|
self._start_time = dt.datetime.now()
|
||||||
|
self._draw_area.update()
|
||||||
|
|
||||||
|
def on_trial_start(self):
|
||||||
|
print("start trial", self._trial_running)
|
||||||
|
if self._trial_running:
|
||||||
|
return
|
||||||
|
print("start trial")
|
||||||
|
if not self._session_running:
|
||||||
|
self._settings.set_enabled(False)
|
||||||
|
self._session_running = True
|
||||||
|
self._trial_running = True
|
||||||
|
if self._trial_counter >= self._settings.trials:
|
||||||
|
self.task_done.emit
|
||||||
|
return
|
||||||
|
self._trial_counter += 1
|
||||||
|
self._status_label.setText("Trial %i of %i running" % (self._trial_counter, self._settings.trials))
|
||||||
|
self.setStatusTip("Test")
|
||||||
|
min_interval = int(self._settings.min_delay / 100)
|
||||||
|
max_interval = int(self._settings.max_delay / 100)
|
||||||
|
interval = np.random.randint(min_interval, max_interval, 1) * 100
|
||||||
|
self._start_time = None
|
||||||
|
timer = QTimer(self)
|
||||||
|
timer.setSingleShot(True)
|
||||||
|
timer.setInterval(int(interval))
|
||||||
|
timer.timeout.connect(self.blip)
|
||||||
|
timer.start()
|
||||||
|
|
||||||
|
def on_abort(self):
|
||||||
|
self.reset()
|
||||||
|
self.task_aborted.emit()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def results(self):
|
||||||
|
return self._reaction_times()
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self.reset_canvas()
|
||||||
|
self._trial_counter = 0
|
||||||
|
self._session_running = 0
|
||||||
|
self._reaction_times = []
|
||||||
|
self._trial_running = False
|
||||||
|
self._session_running = False
|
||||||
|
self._status_label.setText("Ready to start...")
|
||||||
|
self._settings.set_enabled(True)
|
||||||
|
|
1
setup.py
1
setup.py
@ -31,6 +31,7 @@ install_req = ["PyQt5", "numpy"]
|
|||||||
|
|
||||||
data_files = [("icons", glob.glob(os.path.join("icons", "*.png"))),
|
data_files = [("icons", glob.glob(os.path.join("icons", "*.png"))),
|
||||||
("icons", glob.glob(os.path.join("icons", "*.ic*"))),
|
("icons", glob.glob(os.path.join("icons", "*.ic*"))),
|
||||||
|
("sounds", glob.glob(os.path.join("sounds", "*.wav"))),
|
||||||
(".", ["LICENSE"]),
|
(".", ["LICENSE"]),
|
||||||
("docs", glob.glob(os.path.join("docs", "*.md")))
|
("docs", glob.glob(os.path.join("docs", "*.md")))
|
||||||
]
|
]
|
||||||
|
BIN
sounds/bell.wav
Normal file
BIN
sounds/bell.wav
Normal file
Binary file not shown.
BIN
sounds/complete.wav
Normal file
BIN
sounds/complete.wav
Normal file
Binary file not shown.
BIN
sounds/message.wav
Normal file
BIN
sounds/message.wav
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user