diff --git a/README.md b/README.md index da29616..45fba69 100644 --- a/README.md +++ b/README.md @@ -35,44 +35,74 @@ We use the [MISS_HIT linter](https://florianschanda.github.io/miss_hit/style_che ## How to install -### Use the matlab package manager +### Download with git + +``` bash +cd fullpath_to_directory_where_to_install +# use git to download the code +git clone https://github.com/cpp-lln-lab/CPP_PTB.git +# move into the folder you have just created +cd CPP_PTB +# add the src folder to the matlab path and save the path +matlab -nojvm -nosplash -r "addpath(fullfile(pwd)); savepath ();" +``` -This repository can be added as a dependencies by listing it in a [mpm-requirements.txt file](.mpm-requirements.txt) -as follows: +Then get the latest commit: +```bash +# from the directory where you downloaded the code +git pull origin master +``` - CPP_PTB -u https://github.com/cpp-lln-lab/CPP_PTB.git +To work with a specific version, create a branch at a specific version tag number +```bash +# creating and checking out a branch that will be calle version1 at the version tag v0.0.1 +git checkout -b version1 v0.0.1 +``` -You can then use the [matlab package manager](https://github.com/mobeets/mpm), to simply download the appropriate version of those dependencies and add them to your path by running a `getDependencies` function like the one below where you just need to replace `YOUR_EXPERIMENT_NAME` by the name of your experiment. +### Add as a submodule -```matlab - function getDependencies(action) - % Will install on your computer the matlab dependencies specified in the mpm-requirements.txt - % and add them to the matlab path. The path is never saved so you need to run getDependencies() when - % you start matlab. - % - % getDependencies('update') will force the update and overwrite previous version of the dependencies. - % - % getDependencies() If you only already have the appropriate version but just want to add them to the matlab path. - - experimentName = YOUR_EXPERIMENT_NAME; - - if nargin<1 - action = ''; - end - - switch action - case 'update' - % install dependencies - mpm install -i mpm-requirements.txt -f -c YOUR_EXPERIMENT_NAME - end - - % adds them to the path - mpm_folder = fileparts(which('mpm')); - addpath(genpath(fullfile(mpm_folder, 'mpm-packages', 'mpm-collections', experimentName))); - - end +Add it as a submodule in the repo you are working on. + +``` bash +cd fullpath_to_directory_where_to_install +# use git to download the code +git submodule add https://github.com/cpp-lln-lab/CPP_PTB.git +# move into the folder you have just created +cd CPP_PTB +# add the src folder to the matlab path and save the path +matlab -nojvm -nosplash -r "addpath(fullfile(pwd))" +``` + +To get the latest commit you then need to update the submodule with the information +on its remote repository and then merge those locally. +```bash +git submodule update --remote --merge ``` +Remember that updates to submodules need to be commited as well. + +**TO DO** + + +### Direct download + +Download the code. Unzip. And add to the matlab path. + +Pick a specific version: + +https://github.com/cpp-lln-lab/CPP_PTB/releases + +Or take the latest commit (NOT RECOMMENDED): + +https://github.com/cpp-lln-lab/CPP_PTB/archive/master.zip + +**TO DO** + + ## Setting up keyboards To select a specific keyboard to be used by the experimenter or the participant, you need to know diff --git a/apertureTexture.m b/apertureTexture.m new file mode 100644 index 0000000..a695637 --- /dev/null +++ b/apertureTexture.m @@ -0,0 +1,55 @@ +function cfg = apertureTexture(action, cfg, thisEvent) + + transparent = [0 0 0 0]; + + switch action + + case 'init' + + % we take the screen height as maximum aperture width if not + % specified. + if ~isfield(cfg.aperture, 'width') || isempty(cfg.aperture.width) + cfg.aperture.width = cfg.screen.winRect(4) / cfg.screen.ppd; + end + cfg.aperture = degToPix('width', cfg.aperture, cfg); + + cfg.aperture.texture = Screen('MakeTexture', cfg.screen.win, ... + cfg.color.background(1) * ones(cfg.screen.winRect([4 3]))); + + case 'make' + + switch cfg.aperture.type + + case 'none' + + Screen('Fillrect', cfg.aperture.texture, transparent); + + case 'circle' + + diameter = cfg.aperture.widthPix; + + xPos = cfg.screen.center(1); + yPos = cfg.screen.center(2); + if isfield(cfg.aperture, 'xPosPix') + xPos = cfg.screen.center(1) + cfg.aperture.xPosPix; + end + if isfield(cfg.aperture, 'yPosPix') + yPos = cfg.screen.center(2) + cfg.aperture.yPosPix; + end + + Screen('FillOval', cfg.aperture.texture, transparent, ... + CenterRectOnPoint([0 0 repmat(diameter, 1, 2)], ... + xPos, yPos)); + + end + + case 'draw' + + Screen('DrawTexture', cfg.screen.win, cfg.aperture.texture); + + % Screen('DrawTexture', cfg.screen.win, apertureTexture, ... + % cfg.screen.winRect, cfg.screen.winRect, current.apertureAngle - 90); + + end + +end diff --git a/computeFOV.m b/computeFOV.m index 1d12fef..2f3cdf9 100644 --- a/computeFOV.m +++ b/computeFOV.m @@ -3,8 +3,15 @@ % % computes the number of degrees of visual angle in the whole field of view % + % δ = 2 arctan ( d / 2D ) + % + % δ is the angular diameter, and d is the actual diameter of the object, + % and D is the distance to the object. + % The result obtained is in radians. + % - FOV = 2 * ... - (180 * (atan(cfg.screen.monitorWidth / (2 * cfg.screen.monitorDistance)) / pi)); + FOV = ... + 180 / pi * ... + 2 * atan(cfg.screen.monitorWidth / (2 * cfg.screen.monitorDistance)); end diff --git a/decompMotion.m b/decompMotion.m new file mode 100644 index 0000000..bf6283b --- /dev/null +++ b/decompMotion.m @@ -0,0 +1,5 @@ +function [horVector, vertVector] = decompMotion(angleMotion) + % decompose angle of start motion into horizontal and vertical vector + horVector = cos(pi * angleMotion / 180); + vertVector = -sin(pi * angleMotion / 180); +end diff --git a/degToPix.m b/degToPix.m index 9dbf832..367d07a 100644 --- a/degToPix.m +++ b/degToPix.m @@ -8,6 +8,6 @@ deg = getfield(structure, fieldName); %#ok structure = setfield(structure, [fieldName 'Pix'], ... - floor(cfg.screen.ppd * deg)) ; %#ok + floor(deg * cfg.screen.ppd)) ; %#ok end diff --git a/dotTexture.m b/dotTexture.m new file mode 100644 index 0000000..1d5ff2c --- /dev/null +++ b/dotTexture.m @@ -0,0 +1,27 @@ +function cfg = dotTexture(action, cfg, thisEvent) + + switch action + + case 'init' + cfg.dot.texture = Screen('MakeTexture', cfg.screen.win, ... + cfg.color.background(1) * ones(cfg.screen.winRect([4 3]))); + + case 'make' + + dotType = 2; + + Screen('FillRect', cfg.dot.texture, cfg.color.background); + Screen('DrawDots', cfg.dot.texture, ... + thisEvent.dot.positions, ... + cfg.dot.sizePix, ... + cfg.dot.color, ... + cfg.screen.center, ... + dotType); + + case 'draw' + + Screen('DrawTexture', cfg.screen.win, cfg.dot.texture); + + end + +end diff --git a/drawFixation.m b/drawFixation.m new file mode 100644 index 0000000..ce9d3f0 --- /dev/null +++ b/drawFixation.m @@ -0,0 +1,16 @@ +function drawFixation(cfg) + % Define the parameters of the fixation cross in `cfg` and `expParameters` + + if strcmp(cfg.fixation.type, 'cross') + + smooth = 1; + + Screen('DrawLines', ... + cfg.screen.win, ... + cfg.fixation.allCoords, ... + cfg.fixation.lineWidthPix, ... + cfg.fixation.color, ... + [cfg.screen.center(1) cfg.screen.center(2)], smooth); + end + +end diff --git a/drawFixationCross.m b/drawFixationCross.m deleted file mode 100644 index 5ce4a2f..0000000 --- a/drawFixationCross.m +++ /dev/null @@ -1,11 +0,0 @@ -function drawFixationCross(cfg, color) - % Define the parameters of the fixation cross in `cfg` and `expParameters` - - Screen('DrawLines', ... - cfg.screen.win, ... - cfg.allCoords, ... - cfg.fixation.lineWidthPix, ... - color, ... - [cfg.screen.center(1) cfg.screen.center(2)], 1); - -end diff --git a/farewellScreen.m b/farewellScreen.m new file mode 100644 index 0000000..37e0ad2 --- /dev/null +++ b/farewellScreen.m @@ -0,0 +1,8 @@ +function farewellScreen(cfg) + + Screen('FillRect', cfg.screen.win, cfg.color.background, cfg.screen.winRect); + DrawFormattedText(cfg.screen.win, 'Thank you!', 'center', 'center', cfg.text.color); + Screen('Flip', cfg.screen.win); + WaitSecs(cfg.mri.repetitionTime * 2); + +end diff --git a/getExperimentEnd.m b/getExperimentEnd.m new file mode 100644 index 0000000..19d0ea7 --- /dev/null +++ b/getExperimentEnd.m @@ -0,0 +1,15 @@ +function cfg = getExperimentEnd(cfg) + + drawFixation(cfg); + endExpmt = Screen('Flip', cfg.screen.win); + + disp(' '); + ExpmtDur = endExpmt - cfg.experimentStart; + ExpmtDurMin = floor(ExpmtDur / 60); + ExpmtDurSec = mod(ExpmtDur, 60); + disp(['Experiment lasted ', ... + num2str(ExpmtDurMin), ' minutes ', ... + num2str(ExpmtDurSec), ' seconds']); + disp(' '); + +end diff --git a/getExperimentStart.m b/getExperimentStart.m new file mode 100644 index 0000000..3d64ffd --- /dev/null +++ b/getExperimentStart.m @@ -0,0 +1,6 @@ +function cfg = getExperimentStart(cfg) + % Show the fixation cross + drawFixation(cfg); + vbl = Screen('Flip', cfg.screen.win); + cfg.experimentStart = vbl; +end diff --git a/initFixation.m b/initFixation.m new file mode 100644 index 0000000..e85d7b3 --- /dev/null +++ b/initFixation.m @@ -0,0 +1,19 @@ +function cfg = initFixation(cfg) + + if strcmp(cfg.fixation.type, 'cross') + + % Convert some values from degrees to pixels + cfg.fixation = degToPix('width', cfg.fixation, cfg); + cfg.fixation = degToPix('xDisplacement', cfg.fixation, cfg); + cfg.fixation = degToPix('yDisplacement', cfg.fixation, cfg); + + % Prepare fixation cross + cfg.fixation.xCoords = [-cfg.fixation.widthPix cfg.fixation.widthPix 0 0] / 2 + ... + cfg.fixation.xDisplacementPix; + cfg.fixation.yCoords = [0 0 -cfg.fixation.widthPix cfg.fixation.widthPix] / 2 + ... + cfg.fixation.yDisplacementPix; + cfg.fixation.allCoords = [cfg.fixation.xCoords; cfg.fixation.yCoords]; + + end + +end diff --git a/initPTB.m b/initPTB.m index 0a15c59..5364207 100644 --- a/initPTB.m +++ b/initPTB.m @@ -59,7 +59,10 @@ % monitor width % This assumes that the window fills the whole screen cfg.screen.FOV = computeFOV(cfg); - cfg.screen.ppd = cfg.screen.winRect(3) / cfg.screen.FOV; + cfg.screen.ppd = cfg.screen.winWidth / cfg.screen.FOV; + + % Initialize visual parmaters for fixation cross or dot + cfg = initFixation(cfg); %% Select specific text font, style and size initText(cfg); @@ -179,6 +182,7 @@ function initDebug(cfg) % Enable alpha-blending, set it to a blend equation useable for linear % superposition with alpha-weighted source. + % Required for drwing smooth lines and screen('DrawDots') Screen('BlendFunction', cfg.screen.win, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); end diff --git a/initializeDots.m b/initializeDots.m new file mode 100644 index 0000000..0ddcbce --- /dev/null +++ b/initializeDots.m @@ -0,0 +1,49 @@ +function [dots] = initializeDots(cfg, thisEvent) + + direction = thisEvent.direction(1); + + dots.lifeTime = cfg.dot.lifeTime; + + speedPixPerFrame = thisEvent.speed(1); + + % decide which dots are signal dots (1) and those are noise dots (0) + dots.isSignal = rand(cfg.dot.number, 1) < cfg.dot.coherence; + + % for static dots + if direction == -1 + speedPixPerFrame = 0; + dots.lifeTime = cfg.eventDuration; + dots.isSignal = ones(cfg.dot.number, 1); + end + + % Convert from seconds to frames + dots.lifeTime = ceil(dots.lifeTime / cfg.screen.ifi); + + % Set an array of dot positions [xposition, yposition] + % These can never be bigger than 1 or lower than 0 + % [0,0] is the top / left of the square + % [1,1] is the bottom / right of the square + dots.positions = rand(cfg.dot.number, 2) * cfg.screen.winWidth; + + % Set a N x 2 matrix that speed in X and Y + dots.speeds = nan(cfg.dot.number, 2); + + % Coherent dots + [horVector, vertVector] = decompMotion(direction); + dots.speeds(dots.isSignal, :) = ... + repmat([horVector, vertVector], sum(dots.isSignal), 1); + + % If not 100% coherence, we get new random direction for the other dots + direction = rand(sum(~dots.isSignal), 1) * 360; + [horVector, vertVector] = decompMotion(direction); + dots.speeds(~dots.isSignal, :) = [horVector, vertVector]; + + % So far we were working wiht unit vectors convert that speed in pixels per + % frame + dots.speeds = dots.speeds * speedPixPerFrame; + + % Create a vector to update to dotlife time of each dot + % Not all set to one so the dots will die at different times + dots.time = floor(rand(cfg.dot.number, 1) * cfg.eventDuration / cfg.screen.ifi); + +end diff --git a/setDefaultsPTB.m b/setDefaultsPTB.m index abddbd5..5a172d6 100644 --- a/setDefaultsPTB.m +++ b/setDefaultsPTB.m @@ -30,6 +30,17 @@ fieldsToSet.screen.monitorWidth = 42; fieldsToSet.screen.monitorDistance = 134; + % fixation cross or dot + fieldsToSet.fixation.type = 'cross'; + fieldsToSet.fixation.xDisplacement = 0; + fieldsToSet.fixation.yDisplacement = 0; + fieldsToSet.fixation.color = [255 255 255]; + fieldsToSet.fixation.width = 1; + fieldsToSet.fixation.lineWidthPix = 5; + + % define visual apperture field + fieldsToSet.aperture.type = 'none'; + if isfield(cfg, 'audio') && cfg.audio.do fieldsToSet.audio.fs = 44800; diff --git a/standByScreen.m b/standByScreen.m new file mode 100644 index 0000000..8df6d23 --- /dev/null +++ b/standByScreen.m @@ -0,0 +1,14 @@ +function standByScreen(cfg) + + Screen('FillRect', cfg.screen.win, cfg.color.background, cfg.screen.winRect); + + DrawFormattedText(cfg.screen.win, ... + cfg.task.instruction, ... + 'center', 'center', cfg.text.color); + + Screen('Flip', cfg.screen.win); + + % Wait for space key to be pressed + pressSpaceForMe(); + +end diff --git a/tests/test_setDefaultsPTB.m b/tests/test_setDefaultsPTB.m index fa19428..9370eb6 100644 --- a/tests/test_setDefaultsPTB.m +++ b/tests/test_setDefaultsPTB.m @@ -61,6 +61,17 @@ function test_setDefaultsPTB() 'monitorWidth', 42, ... 'monitorDistance', 134)); + % fixation cross or dot + expectedCFG.fixation.type = 'cross'; + expectedCFG.fixation.xDisplacement = 0; + expectedCFG.fixation.yDisplacement = 0; + expectedCFG.fixation.color = [255 255 255]; + expectedCFG.fixation.width = 1; + expectedCFG.fixation.lineWidthPix = 5; + + % define visual apperture field + expectedCFG.aperture.type = 'none'; + expectedCFG.keyboard.keyboard = []; expectedCFG.keyboard.responseBox = []; expectedCFG.keyboard.responseKey = {}; diff --git a/updateDots.m b/updateDots.m new file mode 100644 index 0000000..719ad64 --- /dev/null +++ b/updateDots.m @@ -0,0 +1,26 @@ +function [dots] = updateDots(dots, cfg) + + % Move the selected dots + dots.positions = dots.positions + dots.speeds; + + % Create a logical vector to detect any dot that has: + % - an xy position inferior to 0 + % - an x position superior to winWidth + % - an x position superior to winHeight + % - has exceeded its liftime + N = any([ ... + dots.positions > cfg.screen.winWidth, ... + dots.positions < 0, ... + dots.time > dots.lifeTime, ... + rand(cfg.dot.number, 1) < cfg.dot.proportionKilledPerFrame], 2) ; + + % If there is any such dot we relocate it to a new random position + % and change its lifetime to 1 + if any(N) + dots.positions(N, :) = rand(sum(N), 2) * cfg.screen.winWidth; + dots.time(N, 1) = 1; + end + + % Add one frame to the dot lifetime to each dot + dots.time = dots.time + 1; +end diff --git a/waitForTrigger.m b/waitForTrigger.m index f4ccede..54729fe 100644 --- a/waitForTrigger.m +++ b/waitForTrigger.m @@ -27,7 +27,7 @@ function waitForTrigger(cfg, deviceNumber) if strcmpi(cfg.testingDevice, 'mri') - msg = 'Waiting for trigger'; + msg = 'Waiting for trigger...'; talkToMe(cfg, msg); while triggerCounter < cfg.mri.triggerNb