Multiple fixes. SNR calculation still needs fixing
This commit is contained in:
@@ -22,6 +22,7 @@ import scipy.signal as signal
|
|||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
def asl_P56(x, fs, nbits):
|
def asl_P56(x, fs, nbits):
|
||||||
|
nbits = int(nbits)
|
||||||
eps = np.finfo(float).eps
|
eps = np.finfo(float).eps
|
||||||
x = x[:] # make sure x is column vector
|
x = x[:] # make sure x is column vector
|
||||||
if len(x.shape) < 2:
|
if len(x.shape) < 2:
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ g = exp( -1/( fs* T)); % smoothing factor in envelop detection
|
|||||||
c( 1: thres_no)= 2.^ (-15: thres_no- 16);
|
c( 1: thres_no)= 2.^ (-15: thres_no- 16);
|
||||||
% vector with thresholds from one quantizing level up to half the maximum
|
% vector with thresholds from one quantizing level up to half the maximum
|
||||||
% code, at a step of 2, in the case of 16bit samples, from 2^-15 to 0.5;
|
% code, at a step of 2, in the case of 16bit samples, from 2^-15 to 0.5;
|
||||||
|
|
||||||
a( 1: thres_no) = 0; % activity counter for each level threshold
|
a( 1: thres_no) = 0; % activity counter for each level threshold
|
||||||
hang( 1: thres_no) = I; % hangover counter for each level threshold
|
hang( 1: thres_no) = I; % hangover counter for each level threshold
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
"fileversion" : 1,
|
"fileversion" : 1,
|
||||||
"appversion" : {
|
"appversion" : {
|
||||||
"major" : 8,
|
"major" : 8,
|
||||||
"minor" : 0,
|
"minor" : 1,
|
||||||
"revision" : 8,
|
"revision" : 1,
|
||||||
"architecture" : "x64",
|
"architecture" : "x64",
|
||||||
"modernui" : 1
|
"modernui" : 1
|
||||||
}
|
}
|
||||||
@@ -159,11 +159,12 @@
|
|||||||
"fontname" : "Arial",
|
"fontname" : "Arial",
|
||||||
"fontsize" : 12.0,
|
"fontsize" : 12.0,
|
||||||
"id" : "obj-18",
|
"id" : "obj-18",
|
||||||
|
"linecount" : 2,
|
||||||
"maxclass" : "comment",
|
"maxclass" : "comment",
|
||||||
"numinlets" : 1,
|
"numinlets" : 1,
|
||||||
"numoutlets" : 0,
|
"numoutlets" : 0,
|
||||||
"patching_rect" : [ 408.0, 215.0, 81.75, 20.0 ],
|
"patching_rect" : [ 408.0, 215.0, 81.75, 33.0 ],
|
||||||
"text" : "70 dB SPL"
|
"text" : "70 dB Peak SPL"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -684,8 +685,8 @@
|
|||||||
"followglobaltempo" : 0,
|
"followglobaltempo" : 0,
|
||||||
"formantcorrection" : 0,
|
"formantcorrection" : 0,
|
||||||
"mode" : "basic",
|
"mode" : "basic",
|
||||||
"originallength" : [ 866265.382312924717553, "ticks" ],
|
"originallength" : [ 866265.382312924601138, "ticks" ],
|
||||||
"originaltempo" : 119.999999999999943,
|
"originaltempo" : 119.999999999999929,
|
||||||
"pitchcorrection" : 0,
|
"pitchcorrection" : 0,
|
||||||
"quality" : "basic",
|
"quality" : "basic",
|
||||||
"timestretch" : [ 0 ]
|
"timestretch" : [ 0 ]
|
||||||
@@ -969,8 +970,8 @@
|
|||||||
"followglobaltempo" : 0,
|
"followglobaltempo" : 0,
|
||||||
"formantcorrection" : 0,
|
"formantcorrection" : 0,
|
||||||
"mode" : "basic",
|
"mode" : "basic",
|
||||||
"originallength" : [ 137594.144217686989577, "ticks" ],
|
"originallength" : [ 137594.144217686960474, "ticks" ],
|
||||||
"originaltempo" : 119.999999999999929,
|
"originaltempo" : 119.999999999999915,
|
||||||
"pitchcorrection" : 0,
|
"pitchcorrection" : 0,
|
||||||
"quality" : "basic",
|
"quality" : "basic",
|
||||||
"timestretch" : [ 0 ]
|
"timestretch" : [ 0 ]
|
||||||
|
|||||||
Executable
+38
@@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, "../matrix_test/helper_modules")
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from pathops import dir_must_exist
|
||||||
|
from filesystem import globDir
|
||||||
|
from pysndfile import sndio
|
||||||
|
import os
|
||||||
|
from signalops import block_process_wav
|
||||||
|
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
def main():
|
||||||
|
'''
|
||||||
|
'''
|
||||||
|
fs = 44100
|
||||||
|
f = 1000.0
|
||||||
|
n = np.arange(fs * 60 * 5)
|
||||||
|
y = np.sin(2*np.pi*f*n/fs)
|
||||||
|
coef = np.load('./out/calibration_coefficients/click_cal_coef.npy')
|
||||||
|
y *= coef
|
||||||
|
dir_must_exist('./out/calibrated_stim/')
|
||||||
|
sndio.write("./out/calibrated_stim/1k_tone.wav", y, fs, format='wav', enc='pcm16')
|
||||||
|
coef = np.load('./out/calibration_coefficients/da_cal_coef.npy')
|
||||||
|
y, fs, enc = sndio.read('./out/stimulus/da_cal_stim.wav')
|
||||||
|
sndio.write('./out/calibrated_stim/da_cal_stim.wav', y*coef, fs, format='wav', enc='pcm16')
|
||||||
|
coef = np.load('./out/calibration_coefficients/mat_cal_coef.npy')
|
||||||
|
y, fs, enc = sndio.read('./out/stimulus/mat_cal_stim.wav')
|
||||||
|
sndio.write('./out/calibrated_stim/mat_cal_stim.wav', y*coef, fs, format='wav', enc='pcm16')
|
||||||
|
coef = np.load('./out/calibration_coefficients/story_cal_coef.npy')
|
||||||
|
y, fs, enc = sndio.read('./out/stimulus/story_cal_stim.wav')
|
||||||
|
sndio.write('./out/calibrated_stim/story_cal_stim.wav', y*coef, fs, format='wav', enc='pcm16')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Executable
+74
@@ -0,0 +1,74 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, "../matrix_test/helper_modules")
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from pathops import dir_must_exist
|
||||||
|
from filesystem import globDir
|
||||||
|
from pysndfile import sndio
|
||||||
|
import os
|
||||||
|
from signalops import block_process_wav
|
||||||
|
from shutil import copyfile
|
||||||
|
|
||||||
|
def calc_potential_max(wavs, noise_filepath, out_dir, out_name):
|
||||||
|
max_wav_samp = 0
|
||||||
|
max_wav_rms = 0
|
||||||
|
for wav in wavs:
|
||||||
|
x, fs, enc = sndio.read(wav)
|
||||||
|
max_wav_samp = np.max([max_wav_samp, np.max(np.abs(x))])
|
||||||
|
max_wav_rms = np.max([max_wav_rms, np.sqrt(np.mean(x**2))])
|
||||||
|
x, fs, enc = sndio.read(noise_filepath)
|
||||||
|
noise_rms = np.sqrt(np.mean(x**2))
|
||||||
|
max_noise_samp = max(np.abs(x))
|
||||||
|
|
||||||
|
snr = -5.
|
||||||
|
snr_fs = 10**(-snr/20)
|
||||||
|
max_noise_samp *= max_wav_rms/noise_rms
|
||||||
|
max_sampl = max_wav_samp+(max_noise_samp*snr_fs)
|
||||||
|
reduction_coef = 1.0/max_sampl
|
||||||
|
np.save(os.path.join(out_dir, "{}.npy".format(out_name)), reduction_coef)
|
||||||
|
return reduction_coef
|
||||||
|
|
||||||
|
def main():
|
||||||
|
'''
|
||||||
|
'''
|
||||||
|
da_files = ["../tone_stim/stimulus/tone_2000/tone_3000_2000Hz.wav", "../tone_stim/stimulus/tone_500/tone_3000_500Hz.wav"]
|
||||||
|
story_dir = "../eeg_story_stim/stimulus"
|
||||||
|
mat_dir = "../matrix_test/speech_components"
|
||||||
|
noise_file = "../matrix_test/behavioural_stim/stimulus/wav/noise/noise_norm.wav"
|
||||||
|
da_noise_file = "../da_stim/noise/wav/noise/noise_norm.wav"
|
||||||
|
|
||||||
|
story_wavs = globDir(story_dir, '*.wav')
|
||||||
|
mat_wavs = globDir(mat_dir, '*.wav')
|
||||||
|
|
||||||
|
out_dir = "./out"
|
||||||
|
out_red_dir = os.path.join(out_dir, 'reduction_coefficients')
|
||||||
|
out_stim_dir = os.path.join(out_dir, 'stimulus')
|
||||||
|
dir_must_exist(out_dir)
|
||||||
|
dir_must_exist(out_red_dir)
|
||||||
|
dir_must_exist(out_stim_dir)
|
||||||
|
|
||||||
|
story_coef = calc_potential_max(story_wavs, noise_file, out_red_dir, "story_red_coef")
|
||||||
|
mat_coef = calc_potential_max(mat_wavs, noise_file, out_red_dir, "mat_red_coef")
|
||||||
|
da_coef = calc_potential_max(da_files, da_noise_file, out_red_dir, "da_red_coef")
|
||||||
|
|
||||||
|
mat_cal_stim = "../matrix_test/long_concat_stim/out/stim/stim_0.wav"
|
||||||
|
da_cal_stim = "./out/stimulus/1k_tone.wav"
|
||||||
|
# click_cal_stim = "../tone_stim/stimulus/tone_2000/tone_3000_2000Hz.wav"
|
||||||
|
story_cal_stim = "../eeg_story_stim/stimulus/odin_1_1.wav"
|
||||||
|
|
||||||
|
mat_out_stim = os.path.join(out_stim_dir, "mat_cal_stim.wav")
|
||||||
|
# click_out_stim = os.path.join(out_stim_dir, "click_cal_stim.wav")
|
||||||
|
da_out_stim = os.path.join(out_stim_dir, "1k_cal_stim.wav")
|
||||||
|
story_out_stim = os.path.join(out_stim_dir, "story_cal_stim.wav")
|
||||||
|
|
||||||
|
block_process_wav(mat_cal_stim, mat_out_stim, lambda x: x * mat_coef)
|
||||||
|
block_process_wav(story_cal_stim, story_out_stim, lambda x: x * story_coef)
|
||||||
|
block_process_wav(da_cal_stim, da_out_stim, lambda x: x * da_coef)
|
||||||
|
# block_process_wav(click_cal_stim, click_out_stim, lambda x: x * click_coef)
|
||||||
|
#copyfile(click_cal_stim, click_out_stim)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Executable
+27
@@ -0,0 +1,27 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, "../matrix_test/helper_modules")
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from pathops import dir_must_exist
|
||||||
|
from filesystem import globDir
|
||||||
|
from pysndfile import sndio
|
||||||
|
import os
|
||||||
|
from signalops import block_process_wav
|
||||||
|
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
def main():
|
||||||
|
'''
|
||||||
|
'''
|
||||||
|
fs = 44100
|
||||||
|
f = 1000.0
|
||||||
|
n = np.arange(fs * 60 * 5)
|
||||||
|
y = np.sin(2*np.pi*f*n/fs)
|
||||||
|
y = np.array([y, y]).T
|
||||||
|
sndio.write("./out/stimulus/1k_tone.wav", y, fs, format='wav', enc='pcm16')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
+1
-1
@@ -44,7 +44,7 @@ class DaTestThread(BaseThread):
|
|||||||
'''
|
'''
|
||||||
def __init__(self, sessionFilepath=None,
|
def __init__(self, sessionFilepath=None,
|
||||||
stimFolder='./tone_stim/stimulus',
|
stimFolder='./tone_stim/stimulus',
|
||||||
noiseFilepath="./tone_stim/noise/wav/noise/noise.wav",
|
noiseFilepath="./tone_stim/noise/wav/noise/noise_norm.wav",
|
||||||
noiseintensityFilepath="./tone_stim/noise/intensity/noise_intensity.npy",
|
noiseintensityFilepath="./tone_stim/noise/intensity/noise_intensity.npy",
|
||||||
red_coef="./calibration/out/reduction_coefficients/tone_red_coef.npy",
|
red_coef="./calibration/out/reduction_coefficients/tone_red_coef.npy",
|
||||||
cal_coef="./calibration/out/calibration_coefficients/tone_cal_coef.npy",
|
cal_coef="./calibration/out/calibration_coefficients/tone_cal_coef.npy",
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ def main():
|
|||||||
wavs = globDir("./stimulus", "*.wav")
|
wavs = globDir("./stimulus", "*.wav")
|
||||||
for wav in wavs:
|
for wav in wavs:
|
||||||
x, fs, enc, fmt = sndio.read(wav, return_format=True)
|
x, fs, enc, fmt = sndio.read(wav, return_format=True)
|
||||||
y_r = np.insert(x, 0, np.zeros(fs))
|
|
||||||
idx = np.arange(x.shape[0])
|
idx = np.arange(x.shape[0])
|
||||||
|
breakpoint()
|
||||||
y = np.vstack([x, x, np.zeros(x.shape[0])]).T
|
y = np.vstack([x, x, np.zeros(x.shape[0])]).T
|
||||||
trigger = gen_trigger(idx, 2., 0.01, fs)
|
trigger = gen_trigger(idx, 2., 0.01, fs)
|
||||||
y[:, 2] = trigger
|
y[:, 2] = trigger
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
1, Who are the characters of this story?
|
1, Who are the characters of this story?
|
||||||
2, What are they up to?
|
2, What are the characters doing?
|
||||||
3, What did they wager?
|
3, What did the characters wager?
|
||||||
|
@@ -19,6 +19,9 @@ import csv
|
|||||||
import pdb
|
import pdb
|
||||||
import dill
|
import dill
|
||||||
|
|
||||||
|
import sounddevice as sd
|
||||||
|
from hearing_loss_sim import apply_hearing_loss_sim
|
||||||
|
|
||||||
symb_dict = {
|
symb_dict = {
|
||||||
True: 10003,
|
True: 10003,
|
||||||
False: 10007
|
False: 10007
|
||||||
@@ -38,7 +41,7 @@ class EEGStoryTrainThread(BaseThread):
|
|||||||
Thread for running server side matrix test operations
|
Thread for running server side matrix test operations
|
||||||
'''
|
'''
|
||||||
def __init__(self, sessionFilepath=None,
|
def __init__(self, sessionFilepath=None,
|
||||||
stimFolder='./eeg_story_stim/', nTrials=2,
|
stimFolder='./eeg_story_stim/stimulus/', nTrials=2,
|
||||||
socketio=None, participant=None, srt_50=None, s_50=None):
|
socketio=None, participant=None, srt_50=None, s_50=None):
|
||||||
self.test_name = 'eeg_story_train'
|
self.test_name = 'eeg_story_train'
|
||||||
self.stimDir = stimFolder
|
self.stimDir = stimFolder
|
||||||
@@ -90,6 +93,7 @@ class EEGStoryTrainThread(BaseThread):
|
|||||||
if self._stopevent.isSet() or self.finishTest:
|
if self._stopevent.isSet() or self.finishTest:
|
||||||
break
|
break
|
||||||
# Play concatenated matrix sentences at set SNR
|
# Play concatenated matrix sentences at set SNR
|
||||||
|
|
||||||
self.playStimulus(wav)
|
self.playStimulus(wav)
|
||||||
self.waitForResponse()
|
self.waitForResponse()
|
||||||
if self._stopevent.isSet() or self.finishTest:
|
if self._stopevent.isSet() or self.finishTest:
|
||||||
@@ -142,21 +146,38 @@ class EEGStoryTrainThread(BaseThread):
|
|||||||
def displayInstructions(self):
|
def displayInstructions(self):
|
||||||
self.socketio.emit('display_instructions', namespace='/main')
|
self.socketio.emit('display_instructions', namespace='/main')
|
||||||
|
|
||||||
|
def playStimulus(self, wav):
|
||||||
def playStimulus(self, wav_file, replay=False):
|
'''
|
||||||
|
Output audio stimulus from numpy array
|
||||||
|
'''
|
||||||
self.newResp = False
|
self.newResp = False
|
||||||
self.socketio.emit("stim_playing", namespace="/main")
|
self.socketio.emit("stim_playing", namespace="/main")
|
||||||
# if not replay:
|
x, fs, _ = sndio.read(wav)
|
||||||
# self.y = self.generateTrial(self.snr)
|
if self.participant.parameters['hl_sim_active']:
|
||||||
|
y = apply_hearing_loss_sim(x, fs)
|
||||||
# Play audio
|
# Play audio
|
||||||
# sd.play(self.y, self.fs, blocking=True)
|
|
||||||
if not self.dev_mode:
|
if not self.dev_mode:
|
||||||
self.play_wav(wav_file, 'finish_test')
|
sd.play(y, fs, blocking=True)
|
||||||
else:
|
else:
|
||||||
self.play_wav('./da_stim/DA_170.wav', 'finish_test')
|
self.play_wav('./da_stim/DA_170.wav', '')
|
||||||
|
|
||||||
self.socketio.emit("stim_done", namespace="/main")
|
self.socketio.emit("stim_done", namespace="/main")
|
||||||
|
|
||||||
|
# def playStimulus(self, wav_file, replay=False):
|
||||||
|
|
||||||
|
# x, fs, _ = sndio.read(wav_file)
|
||||||
|
# self.newResp = False
|
||||||
|
# self.socketio.emit("stim_playing", namespace="/main")
|
||||||
|
# # if not replay:
|
||||||
|
# # self.y = self.generateTrial(self.snr)
|
||||||
|
# # Play audio
|
||||||
|
# # sd.play(self.y, self.fs, blocking=True)
|
||||||
|
# if not self.dev_mode:
|
||||||
|
# self.play_wav(wav_file, 'finish_test')
|
||||||
|
# else:
|
||||||
|
# self.play_wav('./da_stim/DA_170.wav', 'finish_test')
|
||||||
|
|
||||||
|
# self.socketio.emit("stim_done", namespace="/main")
|
||||||
|
|
||||||
|
|
||||||
def saveState(self, out="test_state.pkl"):
|
def saveState(self, out="test_state.pkl"):
|
||||||
saveDict = {k:self.__dict__[k] for k in self.toSave}
|
saveDict = {k:self.__dict__[k] for k in self.toSave}
|
||||||
|
|||||||
+28
-7
@@ -10,7 +10,10 @@ import numpy as np
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
from shutil import copy2
|
from shutil import copy2
|
||||||
import re
|
import re
|
||||||
|
import sounddevice as sd
|
||||||
|
from ITU_P56 import asl_P56
|
||||||
|
|
||||||
|
from hearing_loss_sim import apply_hearing_loss_sim
|
||||||
from test_base import BaseThread, run_test_thread
|
from test_base import BaseThread, run_test_thread
|
||||||
from scipy.special import logit
|
from scipy.special import logit
|
||||||
from config import socketio
|
from config import socketio
|
||||||
@@ -52,7 +55,7 @@ class EEGTestThread(BaseThread):
|
|||||||
'''
|
'''
|
||||||
def __init__(self, sessionFilepath=None,
|
def __init__(self, sessionFilepath=None,
|
||||||
listFolder="./matrix_test/short_concat_stim/out",
|
listFolder="./matrix_test/short_concat_stim/out",
|
||||||
noiseFilepath="./matrix_test/behavioural_stim/stimulus/wav/noise/noise.wav",
|
noiseFilepath="./matrix_test/behavioural_stim/stimulus/wav/noise/noise_norm.wav",
|
||||||
red_coef="./calibration/out/reduction_coefficients/mat_red_coef.npy",
|
red_coef="./calibration/out/reduction_coefficients/mat_red_coef.npy",
|
||||||
cal_coef="./calibration/out/calibration_coefficients/mat_cal_coef.npy",
|
cal_coef="./calibration/out/calibration_coefficients/mat_cal_coef.npy",
|
||||||
socketio=None, participant=None, srt_50=None, s_50=None):
|
socketio=None, participant=None, srt_50=None, s_50=None):
|
||||||
@@ -116,7 +119,7 @@ class EEGTestThread(BaseThread):
|
|||||||
logger.info("{0:<25}".format("Current question 2:") + f"{' '.join(q[1][:-1])} | Answer: {q[1][-1]}")
|
logger.info("{0:<25}".format("Current question 2:") + f"{' '.join(q[1][:-1])} | Answer: {q[1][-1]}")
|
||||||
logger.info("{0:<25}".format("Current SNR(-srt):") + f"{snr}")
|
logger.info("{0:<25}".format("Current SNR(-srt):") + f"{snr}")
|
||||||
# Play concatenated matrix sentences at set SNR
|
# Play concatenated matrix sentences at set SNR
|
||||||
self.playStimulusWav(wav)
|
self.playStimulus(wav)
|
||||||
self.setMatrix(q)
|
self.setMatrix(q)
|
||||||
self.saveState(out=self.backupFilepath)
|
self.saveState(out=self.backupFilepath)
|
||||||
logger.info("-"*78)
|
logger.info("-"*78)
|
||||||
@@ -171,8 +174,8 @@ class EEGTestThread(BaseThread):
|
|||||||
def finaliseResults(self):
|
def finaliseResults(self):
|
||||||
toSave = ['marker_files', 'clinPageLoaded', 'wav_files', 'participant',
|
toSave = ['marker_files', 'clinPageLoaded', 'wav_files', 'participant',
|
||||||
'response', 'backupFilepath', 'noise_path', 'question_files',
|
'response', 'backupFilepath', 'noise_path', 'question_files',
|
||||||
'si', 'question', 'answers', 'trial_ind']
|
'question', 'answers', 'trial_ind']
|
||||||
saveDict = {k:self.__dict__[k] for k in toSave}
|
saveDict = {k:self.__dict__[k] for k in toSave if k in self.__dict__.keys()}
|
||||||
self.participant['eeg_test'].update(saveDict)
|
self.participant['eeg_test'].update(saveDict)
|
||||||
self.participant.save("eeg_test")
|
self.participant.save("eeg_test")
|
||||||
backup_path = os.path.join(self.participant.data_paths['eeg_test'],
|
backup_path = os.path.join(self.participant.data_paths['eeg_test'],
|
||||||
@@ -240,20 +243,22 @@ class EEGTestThread(BaseThread):
|
|||||||
csv_files = natsorted(globDir(stim_dir, "*.csv"))
|
csv_files = natsorted(globDir(stim_dir, "*.csv"))
|
||||||
marker_file = csv_files[0]
|
marker_file = csv_files[0]
|
||||||
question_files = csv_files[1:]
|
question_files = csv_files[1:]
|
||||||
rms_file = globDir(stim_dir, "*.npy")[0]
|
# rms_file = globDir(stim_dir, "*.npy")[0]
|
||||||
speech_rms = float(np.load(rms_file))
|
# speech_rms = float(np.load(rms_file))
|
||||||
snr = snrs[:, ind]
|
snr = snrs[:, ind]
|
||||||
audio, fs, enc, fmt = sndio.read(wav, return_format=True)
|
audio, fs, enc, fmt = sndio.read(wav, return_format=True)
|
||||||
|
|
||||||
speech = audio[:, :2]
|
speech = audio[:, :2]
|
||||||
triggers = audio[:, 2]
|
triggers = audio[:, 2]
|
||||||
|
speech_rms, _, _ = asl_P56(speech, fs, 16.)
|
||||||
wf = []
|
wf = []
|
||||||
wm = []
|
wm = []
|
||||||
for ind2, s in enumerate(snr):
|
for ind2, s in enumerate(snr):
|
||||||
start = randint(0, noise_file.frames()-speech.shape[0])
|
start = randint(0, noise_file.frames()-speech.shape[0])
|
||||||
noise_file.seek(start)
|
noise_file.seek(start)
|
||||||
noise = noise_file.read_frames(speech.shape[0])
|
noise = noise_file.read_frames(speech.shape[0])
|
||||||
noise_rms = np.sqrt(np.mean(noise**2))
|
#noise_rms = np.sqrt(np.mean(noise**2))
|
||||||
|
noise_rms = asl_P56(noise, fs, 16)
|
||||||
snr_fs = 10**(-s/20)
|
snr_fs = 10**(-s/20)
|
||||||
if snr_fs == np.inf:
|
if snr_fs == np.inf:
|
||||||
snr_fs = 0.
|
snr_fs = 0.
|
||||||
@@ -302,6 +307,22 @@ class EEGTestThread(BaseThread):
|
|||||||
self.answers[:] = np.nan
|
self.answers[:] = np.nan
|
||||||
|
|
||||||
|
|
||||||
|
def playStimulus(self, wav):
|
||||||
|
'''
|
||||||
|
Output audio stimulus from numpy array
|
||||||
|
'''
|
||||||
|
self.newResp = False
|
||||||
|
self.socketio.emit("stim_playing", namespace="/main")
|
||||||
|
x, fs, _ = sndio.read(wav)
|
||||||
|
if self.participant.parameters['hl_sim_active']:
|
||||||
|
y = apply_hearing_loss_sim(x, fs)
|
||||||
|
# Play audio
|
||||||
|
if not self.dev_mode:
|
||||||
|
sd.play(y, fs, blocking=True)
|
||||||
|
else:
|
||||||
|
self.play_wav('./da_stim/DA_170.wav', '')
|
||||||
|
self.socketio.emit("{}_stim_done".format(self.test_name), namespace="/main")
|
||||||
|
|
||||||
def submitTestResponse(self, msg):
|
def submitTestResponse(self, msg):
|
||||||
'''
|
'''
|
||||||
Get and store participant response for current trial
|
Get and store participant response for current trial
|
||||||
|
|||||||
+3
-1
@@ -18,6 +18,7 @@ import os
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import re
|
import re
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
nowtime = datetime.now()
|
nowtime = datetime.now()
|
||||||
@@ -42,7 +43,7 @@ def find_participants(folder='./participant_data/'):
|
|||||||
if os.path.isdir(os.path.join(folder,o))]
|
if os.path.isdir(os.path.join(folder,o))]
|
||||||
for path in part_folder:
|
for path in part_folder:
|
||||||
part_key = os.path.basename(path)
|
part_key = os.path.basename(path)
|
||||||
participants[part_key] = Participant(participant_dir=path)
|
participants[part_key] = deepcopy(Participant(participant_dir=path))
|
||||||
participants[part_key].load('info')
|
participants[part_key].load('info')
|
||||||
participants[part_key].load('parameters')
|
participants[part_key].load('parameters')
|
||||||
return participants
|
return participants
|
||||||
@@ -144,6 +145,7 @@ class Participant:
|
|||||||
'''
|
'''
|
||||||
'''
|
'''
|
||||||
folder = os.path.join(self.participant_dir, data_key)
|
folder = os.path.join(self.participant_dir, data_key)
|
||||||
|
# print(f"Participant {self.data['info']['number']}: {folder}")
|
||||||
with open(os.path.join(folder, "{}.pkl".format(data_key)), 'rb') as f:
|
with open(os.path.join(folder, "{}.pkl".format(data_key)), 'rb') as f:
|
||||||
self.data[data_key].update(dill.load(f))
|
self.data[data_key].update(dill.load(f))
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
from scipy import signal
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
def apply_hearing_loss_sim(x, fs, channels=[0, 1]):
|
||||||
|
b, a = signal.butter(4, 1170.0/(fs/2.), 'low')
|
||||||
|
if len(x.shape) < 2:
|
||||||
|
x = x[:, np.newaxis]
|
||||||
|
for channel in channels:
|
||||||
|
x[:, channel] = signal.filtfilt(b, a, x[:, channel])
|
||||||
|
return x
|
||||||
|
# w, h = signal.freqs(b, a)
|
||||||
|
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
import sys
|
import sys
|
||||||
#sys.path.insert(0, "../helper_modules")
|
#sys.path.insert(0, "../helper_modules")
|
||||||
#sys.path.insert(0, "../matrix_test/helper_modules/")
|
#sys.path.insert(0, "../matrix_test/helper_modules/")
|
||||||
|
sys.path.insert(0, "../../")
|
||||||
|
sys.path.insert(0, "../helper_modules")
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
@@ -16,6 +18,11 @@ from natsort import natsorted
|
|||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from pysndfile import PySndfile, sndio
|
from pysndfile import PySndfile, sndio
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
|
from ITU_P56 import asl_P56
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from multiprocessing.dummy import Pool as ThreadPool
|
||||||
|
import multiprocessing
|
||||||
|
|
||||||
from pathops import dir_must_exist
|
from pathops import dir_must_exist
|
||||||
from signalops import rolling_window_lastaxis, block_lfilter, calc_rms, block_process_wav
|
from signalops import rolling_window_lastaxis, block_lfilter, calc_rms, block_process_wav
|
||||||
@@ -191,7 +198,7 @@ def calc_spectrum(files, silences, fs=44100, plot=False):
|
|||||||
return b
|
return b
|
||||||
|
|
||||||
|
|
||||||
def gen_noise(OutDir, b, fs, s_rms):
|
def gen_noise(OutDir, b, fs):
|
||||||
print("Generating noise...")
|
print("Generating noise...")
|
||||||
# Generate 10 minutes of white noise
|
# Generate 10 minutes of white noise
|
||||||
x = np.random.randn(int(fs*60.*5.))
|
x = np.random.randn(int(fs*60.*5.))
|
||||||
@@ -203,9 +210,14 @@ def gen_noise(OutDir, b, fs, s_rms):
|
|||||||
dir_must_exist(noiseDir)
|
dir_must_exist(noiseDir)
|
||||||
y, y_max = block_lfilter_wav(b, [1.0], x, os.path.join(noiseDir, 'noise.wav'), 65538, 44100)
|
y, y_max = block_lfilter_wav(b, [1.0], x, os.path.join(noiseDir, 'noise.wav'), 65538, 44100)
|
||||||
block_process_wav(os.path.join(noiseDir, 'noise.wav'), os.path.join(noiseDir, 'noise_norm.wav'), lambda x: x / (y_max * 1.05))
|
block_process_wav(os.path.join(noiseDir, 'noise.wav'), os.path.join(noiseDir, 'noise_norm.wav'), lambda x: x / (y_max * 1.05))
|
||||||
|
noise_norm_wav = PySndfile(os.path.join(noiseDir, 'noise_norm.wav'), 'r')
|
||||||
noise_rms_path = os.path.join(noiseRMSDir, 'noise_rms.npy')
|
noise_rms_path = os.path.join(noiseRMSDir, 'noise_rms.npy')
|
||||||
|
y = noise_norm_wav.read_frames(fs*60)
|
||||||
y = y/(np.abs(y).max() * 0.95)
|
y = y/(np.abs(y).max() * 0.95)
|
||||||
rms = np.sqrt(np.mean(y**2))
|
# rms = np.sqrt(np.mean(y**2))
|
||||||
|
rms, _, _ = asl_P56(y, fs, 16)
|
||||||
|
print(f"Noise level: {rms}")
|
||||||
|
|
||||||
peak = np.abs(y).max()
|
peak = np.abs(y).max()
|
||||||
np.save(noise_rms_path, rms)
|
np.save(noise_rms_path, rms)
|
||||||
np.save('./stimulus/peak/noise_peak.npy', peak)
|
np.save('./stimulus/peak/noise_peak.npy', peak)
|
||||||
@@ -215,21 +227,43 @@ def gen_noise(OutDir, b, fs, s_rms):
|
|||||||
def calc_speech_rms(files, silences, rmsDir, fs=44100, plot=False):
|
def calc_speech_rms(files, silences, rmsDir, fs=44100, plot=False):
|
||||||
'''
|
'''
|
||||||
'''
|
'''
|
||||||
|
files = files[:3]
|
||||||
|
#silences = silences[:3]
|
||||||
f = sum(files, [])
|
f = sum(files, [])
|
||||||
sumsqrd = 0.0
|
n_files = len(f)
|
||||||
n = 0
|
#for ind, (wavfile, sil) in enumerate(zip(f, silences)):
|
||||||
for wavfile, sil in zip(f, silences):
|
def level_calc(args):
|
||||||
y, fs, _ = sndio.read(wavfile)
|
ind, wavfile = args
|
||||||
t = np.arange(y.size)
|
x, fs, _ = sndio.read(wavfile)
|
||||||
sTemp = np.zeros((sil.shape[0], t.size), dtype=bool)
|
level = asl_P56(x, fs, 16.)[0]
|
||||||
for ind3, s in enumerate(sil):
|
print(f"Calculated level of {Path(wavfile).name} ({ind+1}/{n_files}): {level}")
|
||||||
sTemp[ind3, :] = np.logical_and(t > s[0], t < s[1])
|
return level
|
||||||
silentSamples = np.any(sTemp, axis=0)
|
|
||||||
y_temp = y[~silentSamples]
|
# Make the Pool of workers
|
||||||
sumsqrd += np.sum(y_temp**2)
|
pool = ThreadPool(multiprocessing.cpu_count()-1)
|
||||||
n += y_temp.size
|
# Open the urls in their own threads
|
||||||
rms = np.sqrt(sumsqrd/n)
|
# and return the results
|
||||||
np.save(os.path.join(rmsDir, 'overall_speech_rms.npy'), rms)
|
levels = pool.map(level_calc, enumerate(f))
|
||||||
|
#close the pool and wait for the work to finish
|
||||||
|
pool.close()
|
||||||
|
pool.join()
|
||||||
|
rms = np.mean(levels)
|
||||||
|
|
||||||
|
# f = sum(files, [])
|
||||||
|
# sumsqrd = 0.0
|
||||||
|
# n = 0
|
||||||
|
# for wavfile, sil in zip(f, silences):
|
||||||
|
# y, fs, _ = sndio.read(wavfile)
|
||||||
|
# t = np.arange(y.size)
|
||||||
|
# sTemp = np.zeros((sil.shape[0], t.size), dtype=bool)
|
||||||
|
# for ind3, s in enumerate(sil):
|
||||||
|
# sTemp[ind3, :] = np.logical_and(t > s[0], t < s[1])
|
||||||
|
# silentSamples = np.any(sTemp, axis=0)
|
||||||
|
# y_temp = y[~silentSamples]
|
||||||
|
# sumsqrd += np.sum(y_temp**2)
|
||||||
|
# n += y_temp.size
|
||||||
|
# rms = np.sqrt(sumsqrd/n)
|
||||||
|
#np.save(os.path.join(rmsDir, 'overall_speech_rms.npy'), rms)
|
||||||
return rms
|
return rms
|
||||||
#sentenceFFT.append(np.abs(Zxx[:, ~np.any(sTemp, axis=0)]))
|
#sentenceFFT.append(np.abs(Zxx[:, ~np.any(sTemp, axis=0)]))
|
||||||
|
|
||||||
@@ -268,4 +302,4 @@ if __name__ == "__main__":
|
|||||||
silences = detect_silences(rmsFiles, 44100)
|
silences = detect_silences(rmsFiles, 44100)
|
||||||
s_rms = calc_speech_rms(wavFiles, silences, rmsDir)
|
s_rms = calc_speech_rms(wavFiles, silences, rmsDir)
|
||||||
b = calc_spectrum(wavFiles, silences)
|
b = calc_spectrum(wavFiles, silences)
|
||||||
y = gen_noise(args['OutDir'], b, 44100, s_rms)
|
y = gen_noise(args['OutDir'], b, 44100)
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ def block_process_wav(wavpath, out_wavpath, func, block_size=4096, **args):
|
|||||||
y = func(x, **args)
|
y = func(x, **args)
|
||||||
out_wav.write_frames(y)
|
out_wav.write_frames(y)
|
||||||
i += block_size
|
i += block_size
|
||||||
|
del out_wav
|
||||||
|
|
||||||
def window_rms(a, window_size):
|
def window_rms(a, window_size):
|
||||||
print("Squaring...")
|
print("Squaring...")
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import sys
|
import sys
|
||||||
sys.path.insert(0, "../helper_modules/")
|
sys.path.insert(0, "../helper_modules/")
|
||||||
|
sys.path.insert(0, "../../")
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from filesystem import globDir
|
from filesystem import globDir
|
||||||
@@ -17,17 +18,27 @@ import csv
|
|||||||
from copy import copy
|
from copy import copy
|
||||||
from contextlib import ExitStack
|
from contextlib import ExitStack
|
||||||
from scipy.signal import square
|
from scipy.signal import square
|
||||||
|
from ITU_P56 import asl_P56
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
def calc_potential_max(stim_folder, noise_filepath, out_dir):
|
def calc_potential_max(stim_folder, noise_filepath, out_dir):
|
||||||
max_wav_samp = 0
|
max_wav_samp = 0
|
||||||
max_wav_rms = 0
|
max_wav_rms = 0
|
||||||
wavs = globDir(stim_folder, '*.wav')
|
wavs = globDir(stim_folder, '*.wav')
|
||||||
for wav in wavs:
|
n_files = len(wavs)
|
||||||
|
for ind, wav in enumerate(wavs):
|
||||||
x, fs, enc = sndio.read(wav)
|
x, fs, enc = sndio.read(wav)
|
||||||
max_wav_samp = np.max([max_wav_samp, np.max(np.abs(x))])
|
max_wav_samp = np.max([max_wav_samp, np.max(np.abs(x))])
|
||||||
max_wav_rms = np.max([max_wav_rms, np.sqrt(np.mean(x**2))])
|
#max_wav_rms = np.max([max_wav_rms, np.sqrt(np.mean(x**2))])
|
||||||
|
level = asl_P56(x, fs, 16.)[0]
|
||||||
|
max_wav_rms = np.max([max_wav_rms, ])
|
||||||
|
print(f"Calculated level of {Path(wav).name} ({ind+1}/{n_files}): {level}")
|
||||||
x, fs, enc = sndio.read(noise_filepath)
|
x, fs, enc = sndio.read(noise_filepath)
|
||||||
noise_rms = np.sqrt(np.mean(x**2))
|
# noise_rms = np.sqrt(np.mean(x**2))
|
||||||
|
print(f"Calculating level of {Path(noise_filepath).name}")
|
||||||
|
noise_rms, _, _ = asl_P56(x, fs, 16.)
|
||||||
|
print(f"Calculated level of {Path(noise_filepath).name}: {noise_rms}")
|
||||||
max_noise_samp = max(np.abs(x))
|
max_noise_samp = max(np.abs(x))
|
||||||
|
|
||||||
snr = -15.0
|
snr = -15.0
|
||||||
@@ -56,7 +67,7 @@ def main():
|
|||||||
dir_must_exist(wav_dir)
|
dir_must_exist(wav_dir)
|
||||||
dir_must_exist(noise_dir)
|
dir_must_exist(noise_dir)
|
||||||
|
|
||||||
noise_filepath = "../behavioural_stim/stimulus/wav/noise/noise.wav"
|
noise_filepath = "../behavioural_stim/stimulus/wav/noise/noise_norm.wav"
|
||||||
|
|
||||||
folders = os.listdir(base_dir)
|
folders = os.listdir(base_dir)
|
||||||
folders = natsorted(folders)[1:15]
|
folders = natsorted(folders)[1:15]
|
||||||
|
|||||||
+14
-4
@@ -21,6 +21,9 @@ import sounddevice as sd
|
|||||||
import pdb
|
import pdb
|
||||||
|
|
||||||
from config import socketio
|
from config import socketio
|
||||||
|
from hearing_loss_sim import apply_hearing_loss_sim
|
||||||
|
from ITU_P56 import asl_P56
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -143,6 +146,7 @@ class MatTestThread(BaseThread):
|
|||||||
self.loadNoise(noiseFilepath, noiseRMSFilepath)
|
self.loadNoise(noiseFilepath, noiseRMSFilepath)
|
||||||
|
|
||||||
self.dev_mode = False
|
self.dev_mode = False
|
||||||
|
self.audio_cal = False
|
||||||
|
|
||||||
|
|
||||||
def displayInstructions(self):
|
def displayInstructions(self):
|
||||||
@@ -185,6 +189,8 @@ class MatTestThread(BaseThread):
|
|||||||
self.lists[0][currentSentenceInd],
|
self.lists[0][currentSentenceInd],
|
||||||
self.listsRMS[0][currentSentenceInd]
|
self.listsRMS[0][currentSentenceInd]
|
||||||
)
|
)
|
||||||
|
if self.participant.parameters['hl_sim_active']:
|
||||||
|
self.y = apply_hearing_loss_sim(self.y, self.fs, channels=[0])
|
||||||
# Define words presented in the current trial
|
# Define words presented in the current trial
|
||||||
self.currentWords = self.listsString[0][currentSentenceInd]
|
self.currentWords = self.listsString[0][currentSentenceInd]
|
||||||
|
|
||||||
@@ -193,7 +199,11 @@ class MatTestThread(BaseThread):
|
|||||||
logger.info("{0:<25}".format("Current track index:") + f"{self.adTrInd}")
|
logger.info("{0:<25}".format("Current track index:") + f"{self.adTrInd}")
|
||||||
logger.info("{0:<25}".format("Current trial number:") + f"{self.trialN}")
|
logger.info("{0:<25}".format("Current trial number:") + f"{self.trialN}")
|
||||||
logger.info("{0:<25}".format("Current SNR:") + f"{self.adaptiveTracks[self.adTrInd].snr}")
|
logger.info("{0:<25}".format("Current SNR:") + f"{self.adaptiveTracks[self.adTrInd].snr}")
|
||||||
self.playStimulus(self.y, self.fs)
|
if self.audio_cal:
|
||||||
|
y, fs, fmt = sndio.read('./calibration/out/stimulus/mat_cal_stim.wav')
|
||||||
|
self.playStimulus(y, fs)
|
||||||
|
else:
|
||||||
|
self.playStimulus(self.y, self.fs)
|
||||||
self.waitForResponse()
|
self.waitForResponse()
|
||||||
self.checkSentencesAvailable()
|
self.checkSentencesAvailable()
|
||||||
if self.finishTest:
|
if self.finishTest:
|
||||||
@@ -388,9 +398,9 @@ class MatTestThread(BaseThread):
|
|||||||
# Get data for each sentence
|
# Get data for each sentence
|
||||||
for fp, words, level_file in zip(listAudiofiles, csv_reader, levels):
|
for fp, words, level_file in zip(listAudiofiles, csv_reader, levels):
|
||||||
# Read in audio file and calculate it's RMS
|
# Read in audio file and calculate it's RMS
|
||||||
level = loadmat(level_file)
|
|
||||||
x, self.fs, _ = sndio.read(fp)
|
x, self.fs, _ = sndio.read(fp)
|
||||||
x_rms = np.sqrt(np.mean(x**2))
|
logger.info(f"Calculating level for {Path(fp).name}")
|
||||||
|
x_rms, _, _ = asl_P56(x, self.fs, 16.)
|
||||||
self.lists[-1].append(x)
|
self.lists[-1].append(x)
|
||||||
self.listsRMS[-1].append(x_rms)
|
self.listsRMS[-1].append(x_rms)
|
||||||
self.listsString[-1].append(words)
|
self.listsString[-1].append(words)
|
||||||
@@ -531,7 +541,7 @@ class AdaptiveTrack():
|
|||||||
x_noise *= x_rms/self.noise_rms
|
x_noise *= x_rms/self.noise_rms
|
||||||
y = x_noise
|
y = x_noise
|
||||||
# Set speech to start 500ms after the noise, scaled to the desired SNR
|
# Set speech to start 500ms after the noise, scaled to the desired SNR
|
||||||
sigStart = random.randint(self.fs, round(2*self.fs))
|
sigStart = random.randint(round(self.fs/2.), round(2*self.fs))
|
||||||
y[sigStart:sigStart+x.size] += x*snr_fs
|
y[sigStart:sigStart+x.size] += x*snr_fs
|
||||||
y *= self.reduction_coef
|
y *= self.reduction_coef
|
||||||
return y
|
return y
|
||||||
|
|||||||
@@ -20,13 +20,13 @@
|
|||||||
<div class="form-group container-fluid">
|
<div class="form-group container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col text-center mb-3">
|
<div class="col text-center mb-3">
|
||||||
<button type="button" id="start_mat_train" class="btn btn-primary mx-3">Start matrix training data collection</button>
|
<button type="button" id="start_story_train" class="btn btn-primary mx-3">Start story training data collection</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="col text-center mb-3">
|
<div class="col text-center mb-3">
|
||||||
<button type="button" id="load_mat_train_saved" class="btn btn-primary mx-3">Load saved session</button>
|
<button type="button" id="load_train_story_saved" class="btn btn-primary mx-3">Load saved session</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="col text-center mb-3">
|
<div class="col text-center mb-3">
|
||||||
<button type="button" id="load_mat_train_backup" class="btn btn-primary mx-3">Load previous automatic backup</button>
|
<button type="button" id="load_train_story_backup" class="btn btn-primary mx-3">Load previous automatic backup</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|||||||
@@ -74,7 +74,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
|
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
|
||||||
<a class="dropdown-item" href="/pta_test">PTA</a>
|
<a class="dropdown-item" href="/pta_test">PTA</a>
|
||||||
<a class="dropdown-item" href="/tympanometry">Tympanometry</a>
|
<!--<a class="dropdown-item" href="/tympanometry">Tympanometry</a>-->
|
||||||
<a class="dropdown-item" href="/da/setup">Tone EEG recording</a>
|
<a class="dropdown-item" href="/da/setup">Tone EEG recording</a>
|
||||||
<a class="dropdown-item" href="/matrix_test">Behavioral Matrix Test</a>
|
<a class="dropdown-item" href="/matrix_test">Behavioral Matrix Test</a>
|
||||||
<a class="dropdown-item" href="/eeg">Decoder EEG recording</a>
|
<a class="dropdown-item" href="/eeg">Decoder EEG recording</a>
|
||||||
|
|||||||
Reference in New Issue
Block a user