HearingTestGUI.py 8.34 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

"""Architecture:
GUI for user
- Start test
- Play sounds
- Register button presses

Generate library of sounds
Play sounds
Record results

Score: hearing and playability

Display score

Save score and agregate
Dump data (for backup) after each test

Display agregated scores in second window
"""

import sounddevice as sd
import numpy as np
import time
Ed Rogers's avatar
Ed Rogers committed
25
from PyQt5 import QtCore, QtWidgets, QtGui, Qt
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from PyQt5.QtWidgets import QMainWindow, QLabel, QGridLayout, QWidget, QTextEdit
from PyQt5.QtCore import QSize
import sys
from SoundLibrary import SoundLibrary
from HearingTest import HearingTest
import matplotlib
matplotlib.use('Qt5Agg')
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure


class HearingMplCanvas(FigureCanvas):
    """Ultimately, this is a QWidget (as well as a FigureCanvasAgg, etc.)."""

Ed Rogers's avatar
Ed Rogers committed
40
41
42
    false_color = np.array([0., 1., 0.])
    true_color = np.array([0., 0., 1.])

43
    def __init__(self, parent=None, width=5, height=4, dpi=100):
Ed Rogers's avatar
Ed Rogers committed
44
        self.baseline = -0.1
45
46
47
48
49
50
51
52
53
54
55
56
57
58
        fig = Figure(figsize=(width, height), dpi=dpi)
        self.axes = fig.add_subplot(111)

        # self.compute_initial_figure()

        FigureCanvas.__init__(self, fig)
        self.setParent(parent)
        fig.set_facecolor('k')
        FigureCanvas.setSizePolicy(self,
                                   QtWidgets.QSizePolicy.Expanding,
                                   QtWidgets.QSizePolicy.Expanding)
        FigureCanvas.updateGeometry(self)
        self.ul_bar = None
        self.ll_bar = None
Ed Rogers's avatar
Ed Rogers committed
59
        self.plot_result(HearingTest(parent.library, self))
60
61

    def plot_result(self, result: HearingTest):
62
        inds = np.arange(0, result.freqs.size)
63
        self.axes.set_facecolor('k')
Ed Rogers's avatar
Ed Rogers committed
64
65
66
67
        self.ul_bar = self.axes.bar(inds, result.upper_bounds - self.baseline,
                                    color=0.5*self.false_color, bottom=self.baseline)
        self.ll_bar = self.axes.bar(inds, result.lower_bounds - self.baseline,
                                    color=self.false_color, bottom=self.baseline)
68
        xlim = self.axes.get_xlim()
Ed Rogers's avatar
Ed Rogers committed
69
        for i in np.arange(result.lower_lim + self.baseline, result.upper_lim, result.block_size()):
70
            self.axes.plot(self.axes.get_xlim(), np.ones(2)*i, color='k')
71
        labels = [str(b) for b in result.freqs]
72
73
        labels.insert(0, '')
        self.axes.set_xticklabels(labels)
Ed Rogers's avatar
Ed Rogers committed
74
        self.axes.set_ylim(self.baseline, result.upper_lim - self.baseline)
75
76
77
        self.axes.set_xlim(xlim)

    def update_result(self, result: HearingTest) -> None:
Ed Rogers's avatar
Ed Rogers committed
78
79
        self.set_bar_heights(self.ul_bar, result.upper_bounds-self.baseline)
        self.set_bar_heights(self.ll_bar, result.lower_bounds-self.baseline)
Ed Rogers's avatar
Ed Rogers committed
80
        self.set_bar_colors([self.ul_bar, self.ll_bar], result.finished_freqs)
Ed Rogers's avatar
Ed Rogers committed
81
        self.draw()
82
83

    @staticmethod
Ed Rogers's avatar
Ed Rogers committed
84
    def set_bar_heights(bars: matplotlib.container.BarContainer, vals: np.ndarray) -> None:
85
86
87
        for bar, h in zip(bars, vals):
            bar.set_height(h)

Ed Rogers's avatar
Ed Rogers committed
88
89
90
91
92
93
94
95
96
97
    @staticmethod
    def set_bar_colors(bars, finished):
        depth = [0.5, 1]
        for i, bar_line in enumerate(bars):
            for bar, f in zip(bar_line, finished):
                if f:
                    bar.set_facecolor(depth[i] * HearingMplCanvas.true_color)
                else:
                    bar.set_facecolor(depth[i] * HearingMplCanvas.false_color)

98
99
100
101
    # TODO add animation


class HearingTestThread(QtCore.QThread):
102
    played_sound = QtCore.pyqtSignal(float, float, float)
Ed Rogers's avatar
Ed Rogers committed
103
    aborted = QtCore.pyqtSignal()
104
105
106
107

    def __init__(self, test: HearingTest):
        QtCore.QThread.__init__(self)
        self.test = test
Ed Rogers's avatar
Ed Rogers committed
108
        self.abort = False
109
110
111
112
113

    def __del__(self):
        self.wait()

    def run(self):
Ed Rogers's avatar
Ed Rogers committed
114
115
        finished = False
        while not finished:
Ed Rogers's avatar
Ed Rogers committed
116
117
118
            if self.abort:
                self.aborted.emit()
                break
119
120
            finished, freq, volume, played_time, next_sleep_time = self.test.play_next_sound()
            self.played_sound.emit(freq, volume, played_time)
121
122
123
124
125
126

            self.sleep(next_sleep_time)


class TestWindow(QMainWindow):

Ed Rogers's avatar
Ed Rogers committed
127
128
    start_text = "Press space to start..."

129
130
    def __init__(self, library):
        self.library = library
Ed Rogers's avatar
Ed Rogers committed
131
        self.test = None
132
133
134
135
136
137
138
        self.testing_thread = None
        self.test_running = False

        QMainWindow.__init__(self)

        self.setMinimumSize(QSize(640, 480))
        self.setWindowTitle("Hearing Test")
Ed Rogers's avatar
Ed Rogers committed
139
        self.setStyleSheet("QMainWindow { background-color: black }")
140
141
142
143
144
145
146

        central_widget = QWidget(self)
        self.setCentralWidget(central_widget)

        grid_layout = QGridLayout(self)
        central_widget.setLayout(grid_layout)

Ed Rogers's avatar
Ed Rogers committed
147
        text_style = "QLabel { background-color: black; color: white; font: 24pt}"
Ed Rogers's avatar
Ed Rogers committed
148
        self.title = QLabel(start_text, self)
Ed Rogers's avatar
Ed Rogers committed
149
        self.title.setStyleSheet(text_style)
150
151
152
153
154
        self.title.setAlignment(QtCore.Qt.AlignCenter)

        self.log = QTextEdit()
        self.log.setReadOnly(True)
        self.log.setFocusPolicy(QtCore.Qt.NoFocus)
Ed Rogers's avatar
Ed Rogers committed
155
        self.log.hide()
156
157
158

        self.graph = HearingMplCanvas(parent=self)

Ed Rogers's avatar
Ed Rogers committed
159
160
161
162
163
        self.score_label = QLabel('', self)
        self.score_label.setStyleSheet(text_style)
        self.score_label.setAlignment(QtCore.Qt.AlignLeft)
        self.score_label.hide()

164
165
166
        grid_layout.addWidget(self.title, 0, 0)
        grid_layout.addWidget(self.log, 1, 0)
        grid_layout.addWidget(self.graph, 2, 0)
Ed Rogers's avatar
Ed Rogers committed
167
        grid_layout.addWidget(self.score_label, 3, 0)
168
169

    def keyPressEvent(self, event: QtGui.QKeyEvent):
Ed Rogers's avatar
Ed Rogers committed
170
171
172
173
174
175
        if event.key() == Qt.Qt.Key_D:
            if self.log.isHidden():
                self.log.show()
            else:
                self.log.hide()
            return
Ed Rogers's avatar
Ed Rogers committed
176
177
178
        if event.key() == Qt.Qt.Key_F:
            HearingTest.max_response_time = 0.01
            return
Ed Rogers's avatar
Ed Rogers committed
179
180
181
        if event.key() == Qt.Qt.Key_S:
            HearingTest.max_response_time = 1
            return
182
        if not self.test_running:
Ed Rogers's avatar
Ed Rogers committed
183
184
185
186
187
            if event.key() == Qt.Qt.Key_Escape:
                self.close()
            else:
                self.test_running = True
                self.run_test()
188
        else:
Ed Rogers's avatar
Ed Rogers committed
189
190
            if event.key() == Qt.Qt.Key_Escape:
                self.testing_thread.abort = True
Ed Rogers's avatar
Ed Rogers committed
191
            key_press_time = time.time()
Ed Rogers's avatar
Ed Rogers committed
192
            self.test.handle_key_press(key_press_time)
Ed Rogers's avatar
Ed Rogers committed
193
            self.record_key_press(event, key_press_time)
194

Ed Rogers's avatar
Ed Rogers committed
195
196
    def record_key_press(self, event: QtGui.QKeyEvent, _time):
        self.log.append('Key {} pressed at {}'.format(event.key(), _time))
197
198
199
200
201

    def run_test(self):
        self.log.setText('')
        self.title.setText('Test running')
        self.log.append('Starting...')
Ed Rogers's avatar
Ed Rogers committed
202
        self.score_label.hide()
Ed Rogers's avatar
Ed Rogers committed
203
204
        self.test = HearingTest(self.library, self.graph)
        self.testing_thread = HearingTestThread(self.test)
205
206
        self.testing_thread.played_sound.connect(self.sound_played)
        self.testing_thread.finished.connect(self.test_finished)
Ed Rogers's avatar
Ed Rogers committed
207
        self.testing_thread.aborted.connect(self.aborted)
208
209
210
        self.testing_thread.start()
        # need to prevent another thread starting

211
212
    def sound_played(self, freq, volume, played_time):
        self.log.append('Played freq {}, at volume {} and time {}'.format(freq, volume, played_time))
213

Ed Rogers's avatar
Ed Rogers committed
214
215
    def aborted(self):
        self.log.append('Test aborted')
Ed Rogers's avatar
Ed Rogers committed
216
217
218
        self.title.setText(self.start_text)
        self.test = None
        self.test_running = False
Ed Rogers's avatar
Ed Rogers committed
219

220
    def test_finished(self):
Ed Rogers's avatar
Ed Rogers committed
221
222
        if not self.test_running:
            return
223
        self.test_running = False
Ed Rogers's avatar
Ed Rogers committed
224
        self.title.setText(self.start_text)
225
        self.log.append("Finished test")
Ed Rogers's avatar
Ed Rogers committed
226
227
228
229
230
        self.log.append("Writing test result to file")
        thresholds = self.test.thresholds
        thresholds = thresholds.reshape(thresholds.shape[0], -1)  # convert to 2d
        with open('data.csv', 'ba') as file:
            np.savetxt(file, thresholds.T, fmt='%.2f', delimiter=',')
231
        # TODO calculate score
232
        # TODO display score
Ed Rogers's avatar
Ed Rogers committed
233
234
235
        score_str = self.test.get_score_text()
        self.score_label.setText(score_str)
        self.score_label.show()
Ed Rogers's avatar
Ed Rogers committed
236
        self.test = None
237
238
239
240
241
242


def main():
    device = sd.query_devices(sd.default.device['output'])
    fs = device['default_samplerate']
    length = 0.5
Ed Rogers's avatar
Ed Rogers committed
243
    f = [125, 250, 500, 750, 1000, 1500, 2000, 5000, 8000, 12000]
244
245
246
247
248
249
250
251
252
253
254

    library = SoundLibrary(fs, length, f)

    app = QtWidgets.QApplication(sys.argv)
    main_win = TestWindow(library)
    main_win.show()
    sys.exit(app.exec_())


if __name__ == '__main__':
    main()