diff --git a/app.py b/app.py index 30c1ae6..efe4a46 100644 --- a/app.py +++ b/app.py @@ -1,6 +1,6 @@ import pathops import os -from matrix_test.generate_matrix_stimulus import generateAudioStimulus +from matrix_test.long_concat_stim.gen_long_stim import generateAudioStimulus import time import config diff --git a/click_test_thread.py b/click_test_thread.py index f31824f..5c53593 100644 --- a/click_test_thread.py +++ b/click_test_thread.py @@ -1,7 +1,7 @@ from threading import Thread, Event import os import numpy as np -from matrix_test.filesystem import globDir +from matrix_test.helper_modules.filesystem import globDir from pysndfile import PySndfile, sndio from random import randint, shuffle from shutil import copyfile @@ -12,7 +12,7 @@ from shutil import copy2 from test_base import BaseThread -from matrix_test.signalops import play_wav +from matrix_test.helper_modules.signalops import play_wav from scipy.special import logit from config import socketio import csv diff --git a/da_test_thread.py b/da_test_thread.py index 2c84e6a..3dea4f8 100644 --- a/da_test_thread.py +++ b/da_test_thread.py @@ -1,7 +1,7 @@ from threading import Thread, Event import os import numpy as np -from matrix_test.filesystem import globDir +from matrix_test.helper_modules.filesystem import globDir from pysndfile import PySndfile, sndio from random import randint, shuffle from shutil import copyfile @@ -12,7 +12,7 @@ from shutil import copy2 from test_base import BaseThread -from matrix_test.signalops import play_wav +from matrix_test.helper_modules.signalops import play_wav from scipy.special import logit from config import socketio import csv diff --git a/eeg_mat_train_thread.py b/eeg_mat_train_thread.py index 8dc5f15..53f93cf 100644 --- a/eeg_mat_train_thread.py +++ b/eeg_mat_train_thread.py @@ -1,7 +1,7 @@ from threading import Thread, Event import os import numpy as np -from matrix_test.filesystem import globDir +from matrix_test.helper_modules.filesystem import globDir from pysndfile import PySndfile, sndio from random import randint, shuffle from shutil import copyfile @@ -10,9 +10,7 @@ import numpy as np import pandas as pd from shutil import copy2 -from test_base import BaseThread - -from matrix_test.signalops import play_wav +from test_base import BaseThread, run_test_thread from scipy.special import logit from config import socketio import csv @@ -24,6 +22,17 @@ symb_dict = { False: 10007 } +def roll_independant(A, r): + rows, column_indices = np.ogrid[:A.shape[0], :A.shape[1]] + + # Use always a negative shift, so that column_indices are valid. + # (could also use module operation) + r[r < 0] += A.shape[1] + column_indices = column_indices - r[:,np.newaxis] + + result = A[rows, column_indices] + return result + def set_trace(): import logging log = logging.getLogger('werkzeug') @@ -38,25 +47,128 @@ class EEGMatTrainThread(BaseThread): Thread for running server side matrix test operations ''' def __init__(self, sessionFilepath=None, - stimFolder='./da_stim/', nTrials=2, + listFolder="./matrix_test/short_concat_stim/out", + noiseFilepath="./matrix_test/behavioural_stim/stimulus/wav/noise/noise.wav", socketio=None, participant=None, srt_50=None, s_50=None): - self.wav_file = os.path.join(stimFolder, '3000_da.wav') + self.noise_path = noiseFilepath + self.listDir = listFolder - self.test_name = 'da_test' - self.nTrials = nTrials + self.wav_files = [] + self.marker_files = [] + self.question_files = [] + self.question = [] + self.response = [] + + # Percent speech inteligibility (estimated using behavioural measure) + # to present stimuli at + self.si = np.array([20.0, 35.0, 50.0, 65.0, 80.0, 90.0, 100.0]) self.trial_ind = 0 self._stopevent = Event() - super(DaTestThread, self).__init__(self.test_name, - sessionFilepath=sessionFilepath, - socketio=socketio, - participant=participant) - - self.toSave = ['trial_ind', 'nTrials', 'wav_file', 'test_name'] + super(EEGMatTrainThread, self).__init__('eeg_mat_train', + sessionFilepath=sessionFilepath, + socketio=socketio, + participant=participant) + self.socketio.on_event('submit_eeg_response', self.submitTestResponse, namespace='/main') self.socketio.on_event('finalise_results', self.finaliseResults, namespace='/main') - self.dev_mode = True + def loadStimulus(self): + ''' + ''' + self.participant.load('mat_test') + try: + srt_50=self.participant.data['mat_test']['srt_50'] + s_50=self.participant.data['mat_test']['s_50'] + except KeyError: + raise KeyError("Behavioural matrix test results not available, make " + "sure the behavioural test has been run before " + "running this test.") + # Estimate speech intelligibility thresholds using predicted + # psychometric function + reduction_coef = float(np.load(os.path.join(self.listDir, 'reduction_coef.npy'))) + s_50 *= 0.01 + x = logit(self.si * 0.01) + snrs = (x/(4*s_50))+srt_50 + snr_map = pd.DataFrame({"speech_intel" : self.si, "snr": snrs}) + save_dir = self.participant.data_paths['eeg_test/stimulus'] + snr_map_path = os.path.join(save_dir, "snr_map.csv") + snr_map.to_csv(snr_map_path) + snrs = np.repeat(snrs[np.newaxis], 4, axis=0) + snrs = roll_independant(snrs, np.array([0,-1,-2,-3])) + noise_file = PySndfile(self.noise_path, 'r') + stim_dirs = [x for x in os.listdir(self.listDir) if os.path.isdir(os.path.join(self.listDir, x))] + shuffle(stim_dirs) + wav_files = [] + question = [] + marker_files = [] + self.socketio.emit('test_stim_load', namespace='/main') + for ind, dir_name in enumerate(stim_dirs): + stim_dir = os.path.join(self.listDir, dir_name) + wav = globDir(stim_dir, "*.wav")[0] + csv_files = natsorted(globDir(stim_dir, "*.csv")) + marker_file = csv_files[0] + question_files = csv_files[1:] + rms_file = globDir(stim_dir, "*.npy")[0] + speech_rms = float(np.load(rms_file)) + snr = snrs[:, ind] + audio, fs, enc, fmt = sndio.read(wav, return_format=True) + + speech = audio[:, :2] + #triggers = audio[:, 2] + wf = [] + for ind2, s in enumerate(snr): + start = randint(0, noise_file.frames()-speech.shape[0]) + noise_file.seek(start) + noise = noise_file.read_frames(speech.shape[0]) + noise_rms = np.sqrt(np.mean(noise**2)) + snr_fs = 10**(-s/20) + if snr_fs == np.inf: + snr_fs = 0. + elif snr_fs == -np.inf: + raise ValueError("Noise infinitely louder than signal at snr: {}".format(snr)) + noise = noise*(speech_rms/noise_rms) + out_wav_path = os.path.join(save_dir, "Stim_{0}_{1}.wav".format(ind, ind2)) + out_meta_path = os.path.join(save_dir, "Stim_{0}_{1}.npy".format(ind, ind2)) + with np.errstate(divide='raise'): + try: + out_wav = (speech+(np.stack([noise, noise], axis=1)*snr_fs))*reduction_coef + except: + set_trace() + #out_wav = np.concatenate([out_wav, triggers[:, np.newaxis]], axis=1) + sndio.write(out_wav_path, out_wav, fs, fmt, enc) + np.save(out_meta_path, snr) + wf.append(out_wav_path) + wav_files.append(wf) + out_marker_path = os.path.join(save_dir, "Marker_{0}.csv".format(ind)) + marker_files.append(out_marker_path) + copyfile(marker_file, out_marker_path) + for q_file in question_files: + out_q_path = os.path.join(save_dir, "Questions_{0}_{1}.csv".format(ind, ind2)) + self.question_files.append(out_q_path) + copyfile(q_file, out_q_path) + + for q_file_path in question_files: + q = [] + with open(q_file_path, 'r') as q_file: + q_reader = csv.reader(q_file) + for line in q_reader: + q.append(line) + question.append(q) + + self.wav_files = [item for sublist in wav_files for item in sublist] + + self.question.extend(question) + + for item in marker_files: + self.marker_files.extend([item] * 4) + + c = list(zip(self.wav_files, self.marker_files, self.question)) + shuffle(c) + self.wav_files, self.marker_files, self.question = zip(*c) + + self.answers = np.empty(np.shape(self.question)[:2]) + self.answers[:] = np.nan def testLoop(self): @@ -64,22 +176,81 @@ class EEGMatTrainThread(BaseThread): Main loop for iteratively finding the SRT ''' self.waitForPageLoad() - self.socketio.emit('test_ready', namespace='/main') - for self.trial_ind in range(self.nTrials): + self.loadResponse() + self.socketio.emit( + 'test_ready', + {'sentence_1': self.question[0][0][0], 'sentence_2': + self.question[0][1][0]}, namespace='/main' + ) + # For each stimulus + trials = list(zip(self.wav_files, self.question))[self.trial_ind:] + for (wav, q) in trials: self.displayInstructions() self.waitForPartReady() if self._stopevent.isSet() or self.finishTest: break # Play concatenated matrix sentences at set SNR - self.playStimulus(self.wav_file) - self.saveState(out=self.backupFilepath) + self.playStimulus(wav) + self.setMatrix(q) + self.saveState(out=self.backupFilepath) if not self._stopevent.isSet(): self.unsetPageLoaded() self.socketio.emit('processing-complete', namespace='/main') - + self.waitForPageLoad() + self.fillTable() def displayInstructions(self): - self.socketio.emit('display_instructions', namespace='/main') + self.socketio.emit( + 'display_instructions', + {'sentence_1': self.question[self.trial_ind][0][0], 'sentence_2': + self.question[self.trial_ind][1][0]}, namespace='/main' + ) + + def fillTable(self): + ''' + ''' + symb = [[symb_dict[x], symb_dict[y]] for x, y in self.answers if not np.isnan([x, y]).any()] + self.socketio.emit('test_fill_table', {'data': symb}, namespace='/main') + + + def setMatrix(self, questions): + ''' + ''' + for self.q_ind, q in enumerate(questions): + self.answer = q[1] + question = q[0] + self.socketio.emit('set_matrix', {'data': question}, namespace='/main') + self.waitForResponse() + if self._stopevent.isSet() or self.finishTest: + return + self.processResponse() + self.trial_ind += 1 + self.saveState(out=self.backupFilepath) + + def processResponse(self): + ''' + ''' + self.newResp = False + self.answers[self.trial_ind, self.q_ind] = self.answer in self.response + symb = symb_dict[self.answers[self.trial_ind, self.q_ind]] + self.socketio.emit('test_resp', {'q_ind': self.q_ind, 'trial_ind': self.trial_ind, "ans": symb}, namespace='/main') + + def loadResponse(self): + incomplete_responses = np.isnan(self.answers).any(axis=1)[:, np.newaxis].repeat(2, axis=1) + self.answers[incomplete_responses] = np.nan + self.fillTable() + + def finaliseResults(self): + toSave = ['marker_files', 'clinPageLoaded', 'wav_files', 'participant', + 'response', 'backupFilepath', 'noise_path', 'question_files', + 'si', 'question', 'answers', 'trial_ind'] + saveDict = {k:self.__dict__[k] for k in toSave} + self.participant['eeg_test'].update(saveDict) + self.participant.save("eeg_test") + backup_path = os.path.join(self.participant.data_paths['eeg_test'], + 'finalised_backup.pkl') + copy2(self.backupFilepath, backup_path) + self.finalised = True def playStimulus(self, wav_file, replay=False): @@ -96,14 +267,18 @@ class EEGMatTrainThread(BaseThread): self.socketio.emit("stim_done", namespace="/main") - - def loadStimulus(self): + def submitTestResponse(self, msg): ''' + Get and store participant response for current trial ''' - #audio, fs, enc, fmt = sndio.read(wav, return_format=True) + self.response = [x.upper() for x in msg['resp']] + self.newResp = True - def saveState(self, out="test_state.pkl"): - saveDict = {k:self.__dict__[k] for k in self.toSave} + def saveState(self, out="eeg_test_state.pkl"): + toSave = ['marker_files', 'wav_files', 'participant', 'response', + 'backupFilepath', 'noise_path', 'question_files', 'si', + 'question', 'answers', 'trial_ind'] + saveDict = {k:self.__dict__[k] for k in toSave} with open(out, 'wb') as f: dill.dump(saveDict, f) diff --git a/eeg_story_train_thread.py b/eeg_story_train_thread.py index eacdd3c..c7d551b 100644 --- a/eeg_story_train_thread.py +++ b/eeg_story_train_thread.py @@ -1,7 +1,6 @@ from threading import Thread, Event import os import numpy as np -from matrix_test.filesystem import globDir from pysndfile import PySndfile, sndio from random import randint, shuffle from shutil import copyfile @@ -12,7 +11,8 @@ from shutil import copy2 from test_base import BaseThread -from matrix_test.signalops import play_wav +from matrix_test.helper_modules.signalops import play_wav +from matrix_test.helper_modules.filesystem import globDir from scipy.special import logit from config import socketio import csv @@ -65,11 +65,10 @@ class EEGStoryTrainThread(BaseThread): self.socketio.on_event('finalise_results', self.finaliseResults, namespace='/main') self.loadStimulus() - self.dev_mode = True + self.dev_mode = False def setQuestion(self, q): self.socketio.emit('set_question', data=q[0], namespace='/main') - set_trace() def testLoop(self): ''' diff --git a/eeg_test_thread.py b/eeg_test_thread.py index ac73c56..cd6147e 100644 --- a/eeg_test_thread.py +++ b/eeg_test_thread.py @@ -1,7 +1,7 @@ from threading import Thread, Event import os import numpy as np -from matrix_test.filesystem import globDir +from matrix_test.helper_modules.filesystem import globDir from pysndfile import PySndfile, sndio from random import randint, shuffle from shutil import copyfile @@ -47,7 +47,7 @@ class EEGTestThread(BaseThread): Thread for running server side matrix test operations ''' def __init__(self, sessionFilepath=None, - listFolder="./matrix_test/short_concat_stim/", + listFolder="./matrix_test/short_concat_stim/out", noiseFilepath="./matrix_test/stimulus/wav/noise/noise.wav", socketio=None, participant=None, srt_50=None, s_50=None): self.noise_path = noiseFilepath diff --git a/matrix_test/__init__.py b/matrix_test/__init__.py index bc7bada..e69de29 100644 --- a/matrix_test/__init__.py +++ b/matrix_test/__init__.py @@ -1 +0,0 @@ -from .pathtype import PathType diff --git a/matrix_test/long_concat_stim/gen_long_stim.py b/matrix_test/long_concat_stim/gen_long_stim.py index 354043b..60c2e85 100755 --- a/matrix_test/long_concat_stim/gen_long_stim.py +++ b/matrix_test/long_concat_stim/gen_long_stim.py @@ -22,7 +22,7 @@ from pathops import dir_must_exist try: from signalops import rolling_window_lastaxis, block_lfilter except ImportError: - from .signalops import rolling_window_lastaxis, block_lfilter + from ..helper_modules.signalops import rolling_window_lastaxis, block_lfilter import scipy.signal as sgnl from scipy.stats import pearsonr @@ -32,12 +32,12 @@ from pyswarm import pso try: from lpc import lpc except ImportError: - from .lpc import lpc + from ..helper_modules.lpc import lpc try: from filesystem import globDir, organiseWavs, prepareOutDir except ImportError: - from .filesystem import globDir, organiseWavs, prepareOutDir + from ..helper_modules.filesystem import globDir, organiseWavs, prepareOutDir def synthesizeTrial(wavFileMatrix, indexes): diff --git a/matrix_test/long_concat_stim/gen_part_words.py b/matrix_test/long_concat_stim/gen_part_words.py new file mode 100755 index 0000000..c1b9848 --- /dev/null +++ b/matrix_test/long_concat_stim/gen_part_words.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +import sys +sys.path.insert(0, "../helper_modules/") + +import csv +from tokens_to_words import tokens_to_words, load_component_map +import pdb + +def main(): + component_map_file = "../speech_components/component_map.json" + stim_parts = "./out/parts/stim_parts.csv" + stim_words = "./out/parts/stim_words.csv" + component_map = load_component_map(component_map_file) + lines = [] + with open(stim_parts, 'r') as csvfile: + for line in csv.reader(csvfile): + lines.append(tokens_to_words(line, component_map)) + with open(stim_words, 'w') as csvfile: + writer = csv.writer(csvfile) + writer.writerows(lines) + + +if __name__ == "__main__": + main() diff --git a/matrix_test_thread.py b/matrix_test_thread.py index c9d072a..495d106 100644 --- a/matrix_test_thread.py +++ b/matrix_test_thread.py @@ -11,7 +11,7 @@ import csv from shutil import copy2 from pysndfile import sndio, PySndfile -from matrix_test.filesystem import globDir +from matrix_test.helper_modules.filesystem import globDir from test_base import BaseThread import sounddevice as sd import pdb diff --git a/route.py b/route.py index a9527fe..30df1bc 100644 --- a/route.py +++ b/route.py @@ -21,7 +21,7 @@ from pysndfile import sndio from scipy.optimize import minimize from app import generate_matrix_stimulus -from matrix_test.filesystem import globDir, organiseWavs, prepareOutDir +from matrix_test.helper_modules.filesystem import globDir, organiseWavs, prepareOutDir from matrix_test_thread import MatTestThread from pathops import dir_must_exist from participant import Participant, find_participants, gen_participant_num diff --git a/server.py b/server.py index 4c37446..3ef4bfc 100755 --- a/server.py +++ b/server.py @@ -27,7 +27,7 @@ from pysndfile import sndio from scipy.optimize import minimize from app import generate_matrix_stimulus -from matrix_test.filesystem import globDir, organiseWavs, prepareOutDir +from matrix_test.helper_modules.filesystem import globDir, organiseWavs, prepareOutDir from matrix_test_thread import MatTestThread from pathops import dir_must_exist from participant import Participant diff --git a/socket_handlers.py b/socket_handlers.py index 8c6926a..8624c20 100644 --- a/socket_handlers.py +++ b/socket_handlers.py @@ -23,11 +23,11 @@ from scipy.optimize import minimize from WavPlayer import play_wav_async from app import generate_matrix_stimulus -from matrix_test.filesystem import globDir, organiseWavs, prepareOutDir +from matrix_test.helper_modules.filesystem import globDir, organiseWavs, prepareOutDir from matrix_test_thread import MatTestThread from pathops import dir_must_exist from participant import Participant -from matrix_test.signalops import play_wav +from matrix_test.helper_modules.signalops import play_wav from config import server, socketio, participants @@ -126,13 +126,13 @@ def start_test(msg): test_name = msg.pop('test_name') part_key = msg.pop('part_key') thread_type = thread_types[test_name] - socketio.emit('participant_start_{}'.format(test_name), namespace='/main') if part_key != "--": participant = participants[part_key] else: raise ValueError("Participant must be selected...") + socketio.emit('participant_start_{}'.format(test_name), namespace='/main') socketio.emit('participant_start', test_name, namespace='/main', broadcast=True) run_test_thread(test_name, thread_type, participant=participant, **msg) diff --git a/templates/participant_index.html b/templates/participant_index.html index 4d03649..52096b7 100644 --- a/templates/participant_index.html +++ b/templates/participant_index.html @@ -69,7 +69,8 @@ 'eeg_test': "/eeg/test/run", 'da_test': "/da/run", 'click_test': "/click/run", - 'eeg_story_train': "/eeg/train/story/run" + 'eeg_story_train': "/eeg/train/story/run", + 'eeg_mat_train': "/eeg/train/mat/run" } socket.on('participant_start', function(msg) {