/* vim: set path+=/usr/X11R6/include: */
/**
* Copyright (c) 2024, SWGY, Inc. <ron@sw.gy>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or (at
* your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include <err.h>
#include <fcntl.h>
#include <getopt.h>
#include <math.h> /* modf */
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/time.h> /* timespeccmp, etc */
#define XK_LATIN1
#define XK_MISCELLANY
/* Must be defined to pick up keycode definitions */
#include <X11/Xlib.h>
#include <X11/keysymdef.h>
#include "xutils.h"
#include "utils.h"
#include "swgysnd.h"
#define MAXLINE 1000
#define PRIVSEP 0
static const struct timespec quick_sleep = {
.tv_sec = 0,
.tv_nsec = 100
/* This works ok I guess...
.tv_nsec = 275000000 */
};
struct stroop_scenario {
/* indicates the index of the task being shown. */
int cur_task;
/* total number of tasks */
size_t tasksz;
/* description of the tasks (see TT_* defines above) */
int *task_types;
/************************************
* Performance data collected below *
************************************/
/* per-task delta from task presentation to response collection */
double *response_speed;
/* per-task answers. 0 = incorrect, 1 = correct, -1 = unanswered */
int *correct;
};
int load_scenario(int fd, struct stroop_scenario *s);
void free_scenario(struct stroop_scenario *s);
/* Return 1 for left, 2 for right for a given task */
int answer(int task_type);
/*
* Given a task type, display the selected stimulus word (1 = RED, 2 = BLUE)
* in the appropriate color (same as the word if congruent, opposite otherwise)
* centered in large text on a black background. To the lower left, display the
* word RED in medium text within a rounded rectangle border. If the task type
* is "response congruent", the word "RED" is colored red, otherwise it is blue.
* On the lower left, display the word "BLUE" in medium text within a rounded
* rectangle border. If the task type is "response congruent", the word "BLUE"
* should be colored blue, otherwise it will be red.
*/
int show_task(int task_type, struct render_ctx *c);
/*
* Display the word "BLUE" in a large font, centered, rendered in the color
* red. On the bottom left, render the word "RED" in the color blue, on the
* bottom right, render the word "BLUE" in the color red. In small text along
* the top of the screen, wrapping the text, "select the response option with
* the word meaning that matches the display color of the target"
* Overlaid
*/
int show_instructions(struct render_ctx *c);
int show_thanks(struct render_ctx *c);
int run_practice_scenario(struct stroop_scenario *s, double duration,
struct render_ctx *c);
int run_scenario(struct stroop_scenario *s, double duration,
int output_fd, int p_id, struct render_ctx *c);
#define AUDIO_NO_REQ 0
#define AUDIO_REQ_CORRECT 1
#define AUDIO_REQ_WRONG 2
static const char *path_correct_sound =
"/home/rdahlgren/rsync-destination/Data/Sounds/ding.wav";
static const char *path_wrong_sound =
"/home/rdahlgren/rsync-destination/Data/Sounds/wrong.wav";
static volatile int sound_request = AUDIO_NO_REQ;
static volatile int kill_audio = 0;
static pthread_mutex_t audiolock = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t audiocond = PTHREAD_COND_INITIALIZER;
void request_sound(int sound);
void kill_sound(void);
void *audio_thr(void *arg);
/*
* Given an open file, write out the CSV file header:
* "participant id","stimulus word","stimulus color","left option word",
* "left option color","right option word", "right option color","response time",
* "response choice","task type", "correct"
*/
void write_output_header(FILE *fout);
/*
* Given an open file at fout, a task_type (TT_RSC_RC_1, TT_RSI_RC_1, etc),
* a participant ID, a selection (1 for left, 2 for right), and a duration,
* write a one line entry to the file of the form:
*
* p_id,red,red,blue,blue,red,red,0.384,left,fully congruent,incorrect
*/
void write_entry(FILE *fout, int task_type, int p_id, int selection,
struct timespec *d);
int verbose = 0;
static struct option longopts[] = {
{ "part-id", required_argument, NULL, 'p' },
{ "scenario", required_argument, NULL, 's' },
{ "output", required_argument, NULL, 'o' },
{ "verbose", no_argument, NULL, 'v' },
{ NULL, 0, NULL, 0 }
};
static __dead void
usage(const char *n)
{
fprintf(stderr, "usage: %s [-vvv] --part-id ID --scenario SCENARIO_FILE"
" --output OUTPUT_FILE\n", n);
exit(1);
}
int main(int argc, char **argv) {
struct stroop_scenario scen;
struct render_ctx c;
struct geom g;
struct style s;
int ch;
int scenario_fd, output_fd;
unsigned int p_id; /* participant id */
#if PRIVSEP
char *xauth_path;
#endif
const char *errstr;
pthread_t audio_thread;
p_id = 1000000;
scenario_fd = -1;
output_fd = STDIN_FILENO;
/* Read CLI options */
while ((ch = getopt_long(argc, argv, "s:o:v", longopts, NULL)) != -1) {
switch (ch) {
case 's':
if (scenario_fd != -1) /* file already specified */
usage(*argv); /* does not return */
#if PRIVSEP
if (unveil(optarg, "r") == -1)
err(1, "unveil");
#endif
if ((scenario_fd = open(optarg, O_RDONLY)) == -1)
err(1, "open %s", optarg);
break;
case 'o':
if (output_fd != STDIN_FILENO)
usage(*argv); /* does not return */
#if PRIVSEP
if (unveil(optarg, "wc") == -1)
err(1, "unveil");
#endif
if ((output_fd = open(optarg, O_WRONLY | O_CREAT,
S_IRUSR | S_IWUSR)) == -1)
err(1, "open %s", optarg);
break;
case 'p':
p_id = strtonum(optarg, 1, 999999, &errstr);
if (errstr != NULL)
err(1, "Problem parsing participant id %s: %s",
errstr, optarg);
break;
case 'v':
verbose++;
break;
default:
usage(*argv); /* does not return */
}
}
if (scenario_fd == -1)
usage(*argv); /* does not return */
if (p_id == 1000000) {
p_id = arc4random_uniform(999999);
if (verbose)
fprintf(stderr, "Assigned random participant id: %d\n",
p_id);
}
#if PRIVSEP
/* TODO: Sort out the privileges for using swgysnd */
/* drop privileges */
/* X11 requires inet and unix; getpw required to locate xauthority */
if (pledge("stdio rpath cpath unveil inet unix getpw", NULL) == -1 ||
pledge(NULL, NULL) == -1)
err(1, "pledge");
/* X11 requires file access... */
if (locate_xauthority_file(getlogin(), &xauth_path) == -1)
errx(1, "Failed to locate Xauthority file.");
if (unveil("/tmp/.X11-unix/X0", "rw") == -1 ||
unveil(xauth_path, "r") == -1)
err(1, "unveil");
if (unveil(NULL, NULL) == -1)
err(1, "unveil");
#endif
/* initialize audio system */
if (pthread_create(&audio_thread, NULL, audio_thr, NULL) != 0)
errx(1, "pthread_create");
/* Load scenario file */
if (load_scenario(scenario_fd, &scen) == -1)
errx(1, "load_scenario");
/* init X window */
g.x = 0; g.y = 0;
g.w = 1024; g.h = 768;
if (render_init(&g, &c, &s) == -1)
errx(1, "render_init failed");
/* show instructions */
if (verbose)
fprintf(stderr, "Showing instructions.\n");
if (show_instructions(&c) == -1)
errx(1, "show_instructions");
/* 30 seconds of practice */
if (verbose)
fprintf(stderr, "Running practice.\n");
if (run_practice_scenario(&scen, 30, &c) == -1)
errx(1, "run_practice_scenario");
/* show instructions again */
if (verbose)
fprintf(stderr, "Showing instructions again.\n");
if (show_instructions(&c) == -1)
errx(1, "show_instructions");
/* 90 seconds graded */
if (verbose)
fprintf(stderr, "90 seconds of exam.\n");
if (run_scenario(&scen, 90, output_fd, p_id, &c) == -1)
errx(1, "run_practice_scenario");
show_thanks(&c);
free_scenario(&scen);
/* close audio system */
kill_sound();
pthread_join(audio_thread, NULL);
exit(0);
}
int
run_scenario(struct stroop_scenario *s, double seconds, int output_fd,
int p_id, struct render_ctx *c)
{
struct geom space_text_geom;
struct style space_text_style;
struct geom actual_text_geom;
struct style actual_text_style;
struct geom timer_geom;
struct style timer_style;
struct geom score_geom;
struct style score_style;
double int_part, frac_part;
struct timespec now, start, answer_stop, stop, remaining, duration;
FILE *fout;
int t;
int current_score;
if (verbose)
fprintf(stderr, "Running %zu tasks in %f seconds.\n", s->tasksz,
seconds);
if ((fout = fdopen(output_fd, "w")) == NULL)
err(1, "fdopen");
write_output_header(fout);
if (setvbuf(fout, NULL, _IONBF, 0) != 0)
err(1, "setvbuf");
actual_text_geom.x = c->width / 2;
actual_text_geom.y = c->height / 2;
actual_text_geom.w = get_str_width(TXT_SZ_LG, STR_ACTUAL);
actual_text_geom.h = get_str_height(TXT_SZ_LG);
actual_text_style.txt_sz = TXT_SZ_LG;
actual_text_style.fg = PAL_WHT;
actual_text_style.bg = PAL_BLK;
space_text_geom.h = get_str_height(TXT_SZ_SM);
space_text_geom.w = get_str_width(TXT_SZ_SM, STR_SPACE);
space_text_geom.x = c->width / 2;
space_text_geom.y = c->height - 2*WIN_MARGIN;
space_text_style.txt_sz = TXT_SZ_SM;
space_text_style.fg = PAL_GRN;
space_text_style.bg = PAL_BLK;
timer_geom.x = WIN_MARGIN/2;
timer_geom.y = WIN_MARGIN/2;
timer_geom.w = 25;
timer_geom.h = 25;
timer_style.txt_sz = TXT_SZ_SM;
timer_style.fg = PAL_WHT;
timer_style.bg = PAL_BLK;
score_geom.x = c->width - WIN_MARGIN/2;
score_geom.y = WIN_MARGIN/2;
score_geom.w = 25;
score_geom.h = 25;
score_style.txt_sz = TXT_SZ_SM;
score_style.fg = PAL_WHT;
score_style.bg = PAL_BLK;
current_score = 0;
s->cur_task = 0;
t = s->task_types[s->cur_task];
XClearWindow(c->d, c->w);
draw_text(&actual_text_geom, &actual_text_style, STR_ACTUAL, c);
draw_text(&space_text_geom, &space_text_style, STR_SPACE, c);
XFlush(c->d);
for (;;) {
XNextEvent(c->d, &c->event);
if (c->event.type == KeyPress) {
break;
}
}
if (verbose)
fprintf(stderr, "Actual: %zu tasks in %f seconds.\n", s->tasksz,
seconds);
XClearWindow(c->d, c->w);
frac_part = modf(seconds, &int_part);
duration.tv_sec = (time_t) int_part;
duration.tv_nsec = (long)(frac_part * 1e9);
clock_gettime(CLOCK_MONOTONIC, &now);
timespecadd(&now, &duration, &stop);
clock_gettime(CLOCK_MONOTONIC, &start);
while (timespeccmp(&now, &stop, <)) {
clock_gettime(CLOCK_MONOTONIC, &now);
timespecsub(&stop, &now, &remaining);
XClearWindow(c->d, c->w);
draw_timer(&timer_geom, &timer_style, &remaining, c);
while (XPending(c->d)) {
XNextEvent(c->d, &c->event);
if (c->event.type == KeyPress) {
KeySym key = XLookupKeysym(&c->event.xkey, 0);
if (key == XK_q)
return 0;
if (key == XK_Shift_L || key == XK_Shift_R) {
/* capture response time */
clock_gettime(CLOCK_MONOTONIC,
&answer_stop);
timespecsub(&answer_stop, &start,
&duration);
/* award a point if applicable */
if (key == XK_Shift_L &&
answer(t) == 1) {
request_sound(
AUDIO_REQ_CORRECT);
current_score++;
} else if (key == XK_Shift_R && answer(t)
== 2) {
request_sound(
AUDIO_REQ_CORRECT);
current_score++;
} else {
request_sound(AUDIO_REQ_WRONG);
}
/* write out the information */
write_entry(fout, t, p_id,
(key == XK_Shift_L) ? 1 : 2, &duration);
t = s->task_types[++s->cur_task];
/* nanosleep for 0.1 seconds ? */
nanosleep(&quick_sleep, NULL);
/* start the clock on the next one */
clock_gettime(CLOCK_MONOTONIC,
&start);
}
}
}
draw_score(&score_geom, &score_style, current_score, c);
show_task(t, c);
XFlush(c->d);
usleep(16000); /* Document this 16 ms delay */
}
XClearWindow(c->d, c->w);
/* show final score and total tasks*/
score_style.txt_sz = TXT_SZ_LG;
score_geom.x = c->width / 2;
score_geom.y = c->height / 2;
draw_final_score(&score_geom, &score_style, current_score, s->cur_task,
c);
draw_text(&space_text_geom, &space_text_style, STR_SPACE, c);
XFlush(c->d);
for (;;) {
XNextEvent(c->d, &c->event);
if (c->event.type == KeyPress) {
KeySym key = XLookupKeysym(&c->event.xkey, 0);
if (key == XK_q || key == XK_space)
break;
}
}
return 0;
return 0;
}
int
run_practice_scenario(struct stroop_scenario *s, double seconds,
struct render_ctx *c)
{
struct geom space_text_geom;
struct style space_text_style;
struct geom practice_text_geom;
struct style practice_text_style;
struct geom timer_geom;
struct style timer_style;
struct geom score_geom;
struct style score_style;
double int_part, frac_part;
struct timespec now, stop, remaining, duration;
int practice_tasks[32] = {
TT_RSC_RC_1, /* Stim red, red on left, blue on right, 1 */
TT_BSC_RI_2, /* stim blue, blue on left, red on right, 1 */
TT_RSC_RI_2, /* Stim red, blue on left, red on right, 2 */
TT_RSI_RI_2, /* Stim red, blue on left, red on right, 2 */
TT_RSI_RI_1, /* Stim red, red on left, blue on right, 1 */
TT_BSC_RI_1, /* stim blue, red on left, blue on right, 2 */
TT_BSI_RI_1, /* stim blue, red on left, blue on right, 2 */
TT_RSC_RC_2, /* Stim red, blue on left, red on right, 2 */
TT_BSC_RC_2, /* stim blue, blue on left, red on right, 1 */
TT_BSC_RC_1, /* stim blue, red on left, blue on right, 2 */
TT_BSI_RC_2, /* stim blue, blue on left, red on right, 1 */
TT_RSI_RC_1, /* Stim red, red on left, blue on right, 1 */
TT_BSI_RI_2, /* stim blue, blue on left, red on right, 1 */
TT_RSC_RI_1, /* Stim red, red on left, blue on right, 1 */
TT_BSC_RI_1, /* stim blue, red on left, blue on right, 2 */
TT_BSI_RC_1, /* stim blue, red on left, blue on right, 2 */
TT_RSI_RC_2, /* Stim red, blue on left, red on right, 2 */
TT_RSC_RI_1, /* Stim red, red on left, blue on right, 1 */
TT_RSI_RI_1, /* Stim red, red on left, blue on right, 1 */
TT_RSC_RC_2, /* Stim red, blue on left, red on right, 2 */
TT_BSC_RI_2, /* stim blue, blue on left, red on right, 1 */
TT_BSI_RI_1, /* stim blue, red on left, blue on right, 2 */
TT_RSI_RI_2, /* Stim red, blue on left, red on right, 2 */
TT_BSC_RC_1, /* stim blue, red on left, blue on right, 2 */
TT_RSI_RC_1, /* Stim red, red on left, blue on right, 1 */
TT_BSC_RC_2, /* stim blue, blue on left, red on right, 1 */
TT_BSI_RC_1, /* stim blue, red on left, blue on right, 2 */
TT_RSI_RC_2, /* Stim red, blue on left, red on right, 2 */
TT_BSI_RC_2, /* stim blue, blue on left, red on right, 1 */
TT_RSC_RC_1, /* Stim red, red on left, blue on right, 1 */
TT_RSC_RI_2, /* Stim red, blue on left, red on right, 2 */
TT_BSI_RI_2 /* stim blue, blue on left, red on right, 1 */
};
int t;
size_t current_task, tasksz;
int current_score, total_tasks;
practice_text_geom.x = c->width / 2;
practice_text_geom.y = c->height / 2;
practice_text_geom.w = get_str_width(TXT_SZ_LG, STR_PRACTICE);
practice_text_geom.h = get_str_height(TXT_SZ_LG);
practice_text_style.txt_sz = TXT_SZ_LG;
practice_text_style.fg = PAL_WHT;
practice_text_style.bg = PAL_BLK;
space_text_geom.h = get_str_height(TXT_SZ_SM);
space_text_geom.w = get_str_width(TXT_SZ_SM, STR_SPACE);
space_text_geom.x = c->width / 2;
space_text_geom.y = c->height - 2*WIN_MARGIN;
space_text_style.txt_sz = TXT_SZ_SM;
space_text_style.fg = PAL_GRN;
space_text_style.bg = PAL_BLK;
timer_geom.x = WIN_MARGIN/2;
timer_geom.y = WIN_MARGIN/2;
timer_geom.w = 25;
timer_geom.h = 25;
timer_style.txt_sz = TXT_SZ_SM;
timer_style.fg = PAL_WHT;
timer_style.bg = PAL_BLK;
score_geom.x = c->width - WIN_MARGIN/2;
score_geom.y = WIN_MARGIN/2;
score_geom.w = 25;
score_geom.h = 25;
score_style.txt_sz = TXT_SZ_SM;
score_style.fg = PAL_WHT;
score_style.bg = PAL_BLK;
XClearWindow(c->d, c->w);
draw_text(&practice_text_geom, &practice_text_style, STR_PRACTICE, c);
draw_text(&space_text_geom, &space_text_style, STR_SPACE, c);
XFlush(c->d);
frac_part = modf(seconds, &int_part);
duration.tv_sec = (time_t) int_part;
duration.tv_nsec = (long)(frac_part * 1e9);
current_score = 0;
total_tasks = 0;
current_task = 0;
tasksz = 16;
for (;;) {
XNextEvent(c->d, &c->event);
if (c->event.type == KeyPress) {
break;
}
}
fprintf(stderr, "Practice: %zu tasks in %f seconds.\n", s->tasksz,
seconds);
XClearWindow(c->d, c->w);
clock_gettime(CLOCK_MONOTONIC, &now);
timespecadd(&now, &duration, &stop);
t = practice_tasks[current_task];
while (timespeccmp(&now, &stop, <)) {
clock_gettime(CLOCK_MONOTONIC, &now);
timespecsub(&stop, &now, &remaining);
XClearWindow(c->d, c->w);
draw_timer(&timer_geom, &timer_style, &remaining, c);
/* sleep for 1/100 of a second */
while (XPending(c->d)) {
XNextEvent(c->d, &c->event);
if (c->event.type == KeyPress) {
KeySym key = XLookupKeysym(&c->event.xkey, 0);
if (key == XK_q) {
return 0; // Exit loop on 'q' keypress
}
if (key == XK_Shift_L) {
if (answer(t) == 1) {
request_sound(
AUDIO_REQ_CORRECT);
current_score++;
} else {
request_sound(AUDIO_REQ_WRONG);
}
current_task++;
current_task %= tasksz;
total_tasks++;
t = practice_tasks[current_task];
/* nanosleep for 0.1 seconds ? */
nanosleep(&quick_sleep, NULL);
} else if (key == XK_Shift_R) {
if (answer(t) == 2) {
request_sound(
AUDIO_REQ_CORRECT);
current_score++;
} else {
request_sound(AUDIO_REQ_WRONG);
}
current_task++;
current_task %= tasksz;
total_tasks++;
t = practice_tasks[current_task];
/* nanosleep for 0.1 seconds ? */
nanosleep(&quick_sleep, NULL);
}
}
}
draw_score(&score_geom, &score_style, current_score, c);
show_task(t, c);
XFlush(c->d);
usleep(16000); /* Document this 16 ms delay */
}
XClearWindow(c->d, c->w);
/* show final score and total tasks*/
score_style.txt_sz = TXT_SZ_LG;
score_geom.x = c->width / 2;
score_geom.y = c->height / 2;
draw_final_score(&score_geom, &score_style, current_score, total_tasks,
c);
draw_text(&space_text_geom, &space_text_style, STR_SPACE, c);
XFlush(c->d);
for (;;) {
XNextEvent(c->d, &c->event);
if (c->event.type == KeyPress) {
KeySym key = XLookupKeysym(&c->event.xkey, 0);
if (key == XK_q || key == XK_space)
break;
}
}
return 0;
}
static void draw_red_x(struct geom *g, struct render_ctx *ctx) {
const int d = 15;
int p1_x, p1_y;
int p2_x, p2_y;
int p3_x, p3_y;
int p4_x, p4_y;
XSetForeground(ctx->d, ctx->gc, ctx->colors[PAL_RED]);
XSetLineAttributes(ctx->d, ctx->gc, 2, LineSolid, CapButt, JoinMiter);
p1_x = g->x - d;
p1_y = g->y - d;
p2_x = g->x + d;
p2_y = g->y + d;
p3_x = g->x - d;
p3_y = g->y + d;
p4_x = g->x + d;
p4_y = g->y - d;
XDrawLine(ctx->d, ctx->w, ctx->gc, p1_x, p1_y, p2_x, p2_y);
XDrawLine(ctx->d, ctx->w, ctx->gc, p3_x, p3_y, p4_x, p4_y);
}
static void draw_green_check(struct geom *g, struct render_ctx *ctx) {
const int d = 10;
int p1_x, p1_y;
int p2_x, p2_y;
int p3_x, p3_y;
p1_x = g->x - d;
p1_y = g->y - d;
p2_x = g->x;
p2_y = g->y;
p3_x = g->x + 2*d;
p3_y = g->y - 2*d;
XSetForeground(ctx->d, ctx->gc, ctx->colors[PAL_GRN]);
XSetLineAttributes(ctx->d, ctx->gc, 2, LineSolid, CapButt, JoinMiter);
XDrawLine(ctx->d, ctx->w, ctx->gc, p1_x, p1_y, p2_x, p2_y);
XDrawLine(ctx->d, ctx->w, ctx->gc, p2_x, p2_y, p3_x, p3_y);
}
int show_instructions(struct render_ctx *ctx)
{
/* Variable declarations */
struct geom space_text_geom;
struct style space_text_style;
struct geom top_description;
struct geom top_description_2;
struct geom stimulus_geom;
struct geom stimulus_caption_geom;
struct geom red_geom;
struct geom blue_geom;
struct geom wrong_label_geom;
struct geom correct_label_geom;
struct geom red_x_geom;
struct geom green_check_geom;
struct geom lshift_text_geom;
struct geom rshift_text_geom;
struct style red_style;
struct style blue_style;
struct style small_text_style;
struct style med_text_style;
struct style stimulus_style;
KeySym key;
/* Clear the window */
XClearWindow(ctx->d, ctx->w);
small_text_style.txt_sz = TXT_SZ_SM;
small_text_style.fg = PAL_WHT;
small_text_style.bg = PAL_BLK;
med_text_style.txt_sz = TXT_SZ_MD;
med_text_style.fg = PAL_WHT;
med_text_style.bg = PAL_BLK;
space_text_geom.h = get_str_height(TXT_SZ_SM);
space_text_geom.w = get_str_width(TXT_SZ_SM, STR_SPACE);
space_text_geom.x = ctx->width / 2;
space_text_geom.y = ctx->height - 2*WIN_MARGIN;
space_text_style.txt_sz = TXT_SZ_SM;
space_text_style.fg = PAL_GRN;
space_text_style.bg = PAL_BLK;
top_description.x = ctx->width / 2;
top_description.y = WIN_MARGIN;
top_description.w = get_str_width(TXT_SZ_MD, STR_DETAIL);
top_description.h = get_str_height(TXT_SZ_MD);
draw_text(&top_description, &med_text_style, STR_DETAIL, ctx);
top_description_2.x = ctx->width / 2;
top_description_2.y = WIN_MARGIN + 30 + top_description.h;
top_description_2.w = get_str_width(TXT_SZ_MD, STR_DESC_2);
top_description_2.h = get_str_height(TXT_SZ_MD);
draw_text(&top_description_2, &med_text_style, STR_DESC_2, ctx);
stimulus_style.txt_sz = TXT_SZ_LG;
stimulus_style.fg = PAL_BLU;
stimulus_style.bg = PAL_BLK;
stimulus_geom.x = ctx->width / 2;
stimulus_geom.y = ctx->height / 2;
stimulus_geom.w = get_str_width(TXT_SZ_LG, STR_RED);
stimulus_geom.h = get_str_height(TXT_SZ_LG);
stimulus_caption_geom.w =
get_str_width(TXT_SZ_SM, STR_WORD_DESC);
stimulus_caption_geom.h = get_str_height(TXT_SZ_SM);
stimulus_caption_geom.x = ctx->width / 2;
stimulus_caption_geom.y = stimulus_geom.y + stimulus_geom.h +
(4 * TXT_MARGIN) + stimulus_geom.h +
get_str_height(TXT_SZ_SM);
/* Draw small white text above the stimulus */
draw_text(&stimulus_caption_geom, &small_text_style, STR_WORD_DESC,
ctx);
/* Draw the stimulus text "RED" in blue color */
draw_text_with_border(&stimulus_geom, &stimulus_style, STR_RED, ctx);
/* Geometry for the red and blue words with their borders */
red_geom.w = get_str_width(TXT_SZ_MD, STR_RED);
red_geom.h = get_str_height(TXT_SZ_MD);
red_geom.x = WIN_MARGIN + red_geom.w / 2;
red_geom.y = ctx->height - WIN_MARGIN - red_geom.h - TXT_MARGIN;
lshift_text_geom.x = red_geom.x;
lshift_text_geom.y = red_geom.y - 50;
blue_geom.w = get_str_width(TXT_SZ_MD, STR_BLU);
fprintf(stderr, "blue_geom.w: %d\n", blue_geom.w);
blue_geom.h = get_str_height(TXT_SZ_MD);
blue_geom.x = ctx->width - WIN_MARGIN - TXT_MARGIN - blue_geom.w / 2;
blue_geom.y = ctx->height - WIN_MARGIN - blue_geom.h - TXT_MARGIN;
rshift_text_geom.x = blue_geom.x;
rshift_text_geom.y = blue_geom.y - 50;
/* Style for red and blue options */
red_style.txt_sz = TXT_SZ_MD;
red_style.bg = PAL_BLK;
red_style.fg = PAL_BLU; /* incongruent */
blue_style.txt_sz = TXT_SZ_MD;
blue_style.bg = PAL_BLK;
blue_style.fg = PAL_RED; /* incongruent */
/* Draw the red and blue words with rounded borders */
draw_text_with_border(&red_geom, &red_style, STR_RED, ctx);
draw_text_with_border(&blue_geom, &blue_style, STR_BLU, ctx);
draw_text(&lshift_text_geom, &small_text_style, STR_LSHIFT, ctx);
draw_text(&rshift_text_geom, &small_text_style, STR_RSHIFT, ctx);
/* Draw labels for correct/incorrect answer under the options */
wrong_label_geom.x = red_geom.x;
wrong_label_geom.y = red_geom.y + red_geom.h + 4 * TXT_MARGIN;
wrong_label_geom.w = get_str_width(TXT_SZ_SM, STR_INCORRECT);
wrong_label_geom.h = get_str_height(TXT_SZ_SM);
correct_label_geom.x = blue_geom.x;
correct_label_geom.y = blue_geom.y + blue_geom.h + 4 * TXT_MARGIN;
correct_label_geom.w = get_str_width(TXT_SZ_SM, STR_CORRECT);
correct_label_geom.h = get_str_height(TXT_SZ_SM);
draw_text(&wrong_label_geom, &small_text_style, STR_INCORRECT, ctx);
draw_text(&correct_label_geom, &small_text_style, STR_CORRECT, ctx);
/* Geometry for the small red X and small green checkmark */
red_x_geom.x = red_geom.x + red_geom.w;
red_x_geom.y = red_geom.y - 2*TXT_MARGIN;
green_check_geom.x = blue_geom.x + blue_geom.w - TXT_MARGIN;
green_check_geom.y = blue_geom.y - TXT_MARGIN;
/* Draw the small red X and green checkmark based on correct choice */
draw_green_check(&green_check_geom, ctx);
draw_red_x(&red_x_geom, ctx);
draw_text(&space_text_geom, &space_text_style, STR_SPACE, ctx);
/* Flush the updates */
XFlush(ctx->d);
/* Event loop to listen for key presses */
for (;;) {
XNextEvent(ctx->d, &ctx->event);
if (ctx->event.type == KeyPress) {
key = XLookupKeysym(&ctx->event.xkey, 0);
if (key == XK_q || key == XK_space)
break;
}
}
return 0;
}
int
show_thanks(struct render_ctx *c)
{
struct geom space_text_geom;
struct style space_text_style;
struct geom thanks_text_geom;
struct style thanks_text_style;
KeySym key;
thanks_text_style.txt_sz = TXT_SZ_LG;
thanks_text_style.fg = PAL_WHT;
thanks_text_style.bg = PAL_BLK;
thanks_text_geom.w = get_str_width(TXT_SZ_LG, STR_THANKS);
thanks_text_geom.h = get_str_height(TXT_SZ_LG);
thanks_text_geom.x = c->width / 2;
thanks_text_geom.y = c->height / 3;
space_text_geom.h = get_str_height(TXT_SZ_SM);
space_text_geom.w = get_str_width(TXT_SZ_SM, STR_SPACE);
space_text_geom.x = c->width / 2;
space_text_geom.y = c->height - 2*WIN_MARGIN;
space_text_style.txt_sz = TXT_SZ_SM;
space_text_style.fg = PAL_GRN;
space_text_style.bg = PAL_BLK;
XClearWindow(c->d, c->w);
draw_text(&space_text_geom, &space_text_style, STR_SPACE, c);
draw_text(&thanks_text_geom, &thanks_text_style, STR_THANKS, c);
fprintf(stderr, "Showing thanks to screen %d\n", c->screen_num);
XFlush(c->d);
for (;;) {
XNextEvent(c->d, &c->event);
if (c->event.type == KeyPress) {
key = XLookupKeysym(&c->event.xkey, 0);
if (key == XK_q || key == XK_space)
break;
}
}
return 0;
}
int show_task(int task_type, struct render_ctx *ctx)
{
/* Variable declarations */
struct geom stimulus_geom;
struct geom red_geom;
struct geom blue_geom;
struct style red_style;
struct style blue_style;
struct style stimulus_style;
int is_blue_left;
int is_red_stimulus;
int is_stimulus_incongruent;
int is_response_incongruent;
int stim_text;
is_blue_left = task_type & 8;
is_red_stimulus = task_type & 4;
is_stimulus_incongruent = task_type & 2;
is_response_incongruent = task_type & 1;
if (is_red_stimulus) {
stimulus_geom.x = ctx->width / 2;
stimulus_geom.y = ctx->height / 2;
stimulus_geom.w = is_stimulus_incongruent ?
get_str_width(TXT_SZ_LG, STR_BLU) :
get_str_width(TXT_SZ_LG, STR_RED);
stimulus_geom.h = get_str_height(TXT_SZ_LG);
stimulus_style.txt_sz = TXT_SZ_LG;
stimulus_style.fg = PAL_RED;
stimulus_style.bg = PAL_BLK;
stim_text = is_stimulus_incongruent ? STR_BLU : STR_RED;
} else {
stimulus_geom.x = ctx->width / 2;
stimulus_geom.y = ctx->height / 2;
stimulus_geom.w = is_stimulus_incongruent ?
get_str_width(TXT_SZ_LG, STR_RED) :
get_str_width(TXT_SZ_LG, STR_BLU);
stimulus_geom.h = get_str_height(TXT_SZ_LG);
stimulus_style.txt_sz = TXT_SZ_LG;
stimulus_style.fg = PAL_BLU;
stimulus_style.bg = PAL_BLK;
stim_text = is_stimulus_incongruent ? STR_RED : STR_BLU;
}
draw_text_with_border(&stimulus_geom, &stimulus_style, stim_text, ctx);
if (is_response_incongruent) {
/* Style for red and blue options */
red_style.txt_sz = TXT_SZ_MD;
red_style.bg = PAL_BLK;
red_style.fg = PAL_BLU; /* incongruent */
blue_style.txt_sz = TXT_SZ_MD;
blue_style.bg = PAL_BLK;
blue_style.fg = PAL_RED; /* incongruent */
blue_geom.w = get_str_width(TXT_SZ_MD, STR_RED);
} else {
/* Style for red and blue options */
red_style.txt_sz = TXT_SZ_MD;
red_style.bg = PAL_BLK;
red_style.fg = PAL_RED; /* congruent */
red_geom.w = get_str_width(TXT_SZ_MD, STR_RED);
blue_style.txt_sz = TXT_SZ_MD;
blue_style.bg = PAL_BLK;
blue_style.fg = PAL_BLU; /* congruent */
}
if (is_blue_left) {
/* Geometry for the red and blue words with their borders */
blue_geom.w = get_str_width(TXT_SZ_MD, STR_BLU);
blue_geom.h = get_str_height(TXT_SZ_MD);
blue_geom.x = WIN_MARGIN + blue_geom.w / 2;
blue_geom.y = ctx->height - WIN_MARGIN - blue_geom.h -
TXT_MARGIN;
red_geom.w = get_str_width(TXT_SZ_MD, STR_RED);
red_geom.h = get_str_height(TXT_SZ_MD);
red_geom.x = ctx->width - WIN_MARGIN - TXT_MARGIN -
red_geom.w / 2;
red_geom.y = ctx->height - WIN_MARGIN - red_geom.h - TXT_MARGIN;
} else {
/* Geometry for the red and blue words with their borders */
red_geom.w = get_str_width(TXT_SZ_MD, STR_RED);
red_geom.h = get_str_height(TXT_SZ_MD);
red_geom.x = WIN_MARGIN + red_geom.w / 2;
red_geom.y = ctx->height - WIN_MARGIN - red_geom.h - TXT_MARGIN;
blue_geom.w = get_str_width(TXT_SZ_MD, STR_BLU);
blue_geom.h = get_str_height(TXT_SZ_MD);
blue_geom.x = ctx->width - WIN_MARGIN - TXT_MARGIN -
blue_geom.w / 2;
blue_geom.y = ctx->height - WIN_MARGIN - blue_geom.h -
TXT_MARGIN;
}
/* Draw the red and blue words with rounded borders */
draw_text_with_border(&red_geom, &red_style, STR_RED, ctx);
draw_text_with_border(&blue_geom, &blue_style, STR_BLU, ctx);
/* Flush the updates */
XFlush(ctx->d);
return 0;
}
int
load_scenario(int fd, struct stroop_scenario *s)
{
char line[MAXLINE];
FILE *f;
int retval;
int *loaded_tasks;
size_t line_no, cur_task, i;
size_t loaded_tasksz;
if ((f = fdopen(fd, "r")) == NULL)
err(1, "fdopen");
if (verbose)
fprintf(stderr, "Loading scenario from %d to %p\n", fd,
(void *)s);
/* 1SI 2RC*/
retval = 0;
line_no = i = cur_task = 0;
loaded_tasksz = 128;
loaded_tasks = malloc(sizeof(int) * loaded_tasksz);
while (fgets(line, MAXLINE, f) != NULL) {
line[strcspn(line, "\n")] = '\0';
line_no++;
if (strncmp("#", line, 1) == 0) /* comment */
continue;
/* load the task definition */
loaded_tasks[cur_task] = 0;
if (strlen(line) != 7) {
if (verbose)
fprintf(stderr, "l: %zu bad length: %zu\n",
line_no, strlen(line));
retval = -1;
goto cleanup;
}
if (line[0] == '1') /* 1 = red */
loaded_tasks[cur_task] = 4;
else if (line[0] == '2') /* 2 = blue */
loaded_tasks[cur_task] = 0;
else {
if (verbose)
fprintf(stderr, "l: %zu bad start\n", line_no);
retval = -1;
goto cleanup;
}
if (line[2] == 'I') /* stimulus incongruent */
loaded_tasks[cur_task] += 2;
else if (line[2] != 'C') {
if (verbose)
fprintf(stderr, "l: %zu bad SI/SC\n", line_no);
retval = -1;
goto cleanup;
}
if (line[4] == '2') { /* blue on left */
loaded_tasks[cur_task] += 8;
} else if (line[4] != '1') {
if (verbose)
fprintf(stderr, "l: %zu bad resp\n", line_no);
retval = -1;
goto cleanup;
}
if (line[6] == 'I') /* response incongruent */
loaded_tasks[cur_task] += 1;
else if (line[6] != 'C') {
if (verbose)
fprintf(stderr, "line %zu bad RI/RC\n", line_no);
retval = -1;
goto cleanup;
}
if (verbose)
fprintf(stderr, "Loaded %zu task %d\n", cur_task,
loaded_tasks[cur_task]);
cur_task++;
if (cur_task == loaded_tasksz) {
loaded_tasksz *= 2;
if ((loaded_tasks = reallocarray(loaded_tasks,
loaded_tasksz, sizeof(int))) == NULL)
err(1, "reallocarray");
}
}
if (cur_task == 0)
goto cleanup;
s->cur_task = 0;
s->tasksz = cur_task;
s->task_types = malloc(sizeof(int) * s->tasksz);
memcpy(s->task_types, loaded_tasks, sizeof(int) * s->tasksz);
s->response_speed = malloc(sizeof(double) * s->tasksz);
s->correct = malloc(sizeof(int) * s->tasksz);
for (i = 0; i < s->tasksz; ++i) {
s->response_speed[i] = 0.0;
s->correct[i] = -1;
}
cleanup:
free(loaded_tasks);
fclose(f);
return retval;
}
void free_scenario(struct stroop_scenario *s)
{
fprintf(stderr, "Freeing scenario %p\n", (void *)s);
free(s->correct); s->correct = NULL;
free(s->response_speed); s->response_speed = NULL;
free(s->task_types); s->task_types = NULL;
}
int
answer(int task_type)
{
int retval;
switch (task_type) {
/* Red on left, blue on right */
case TT_RSC_RC_1: /* Stim red */
retval = 1;
break;
case TT_BSC_RC_1: /* stim blue */
retval = 2;
break;
case TT_RSI_RC_1: /* Stim red */
retval = 1;
break;
case TT_BSI_RC_1: /* stim blue */
retval = 2;
break;
case TT_RSC_RI_1: /* Stim red */
retval = 1;
break;
case TT_BSC_RI_1: /* stim blue */
retval = 2;
break;
case TT_RSI_RI_1: /* Stim red */
retval = 1;
break;
case TT_BSI_RI_1: /* stim blue */
retval = 2;
break;
/* Blue on left, red on right */
case TT_RSC_RC_2: /* Stim red */
retval = 2;
break;
case TT_BSC_RC_2: /* stim blue */
retval = 1;
break;
case TT_RSI_RC_2: /* Stim red */
retval = 2;
break;
case TT_BSI_RC_2: /* stim blue */
retval = 1;
break;
case TT_RSC_RI_2: /* Stim red */
retval = 2;
break;
case TT_BSC_RI_2: /* stim blue */
retval = 1;
break;
case TT_RSI_RI_2: /* Stim red */
retval = 2;
break;
case TT_BSI_RI_2: /* stim blue */
retval = 1;
break;
default:
retval = -1;
break;
}
return retval;
}
/* Function to write the CSV file header */
void
write_output_header(FILE *fout)
{
if (fout == NULL)
errx(1, "output file not open");
// Write the CSV header to the file
fprintf(fout, "\"participant id\",\"stimulus word\",\"stimulus color\","
"\"left option word\",\"left option color\",\"right option word\","
"\"right option color\",\"response time\",\"response choice\","
"\"task type\",\"correct\"\n");
}
/* Function to write a single entry to the CSV file */
void
write_entry(FILE *fout, int task_type, int p_id, int selection,
struct timespec *d)
{
const char *stimulus_word, *stimulus_color;
const char *left_option_word, *left_option_color;
const char *right_option_word, *right_option_color;
const char *problem_type;
const char *correct_or_not;
const char *response_choice;
double response_time;
if (fout == NULL)
errx(1, "File is not open for writing.");
if (selection == answer(task_type))
correct_or_not = "correct";
else
correct_or_not = "incorrect";
if (task_type & 2 && task_type & 1) {
problem_type = "fully incongruent";
} else if (!(task_type & 2) && !(task_type & 1)) {
problem_type = "fully congruent";
} else if (task_type & 2 && !(task_type & 1)) {
problem_type = "stimulus incongruent";
} else {
problem_type = "response incongruent";
}
/* bit 4, Blue response option on left y/n
* bit 3, Red stimulus color y/n
* bit 2, stimulus incongruent y/n
* bit 1, response incongruent y/n
*/
if (task_type & 4) {
stimulus_word = "red";
stimulus_color = (task_type & 2) ? "blue" : "red";
} else {
stimulus_word = "blue";
stimulus_color = (task_type & 2) ? "red" : "blue";
}
// Left and right options (response congruent if bit 1 is unset)
if (task_type & 8) { /* blue word on left */
left_option_word = "blue";
left_option_color = (task_type & 1) ? "red" : "blue";
right_option_word = "red";
right_option_color = (task_type & 1) ? "blue" : "red";
} else { /* red word on right */
left_option_word = "red";
left_option_color = (task_type & 1) ? "blue" : "red";
right_option_word = "blue";
right_option_color = (task_type & 1) ? "red" : "blue";
}
/* Determine if the participant selected the left or right option */
response_choice = (selection == 1) ? "left" : "right";
response_time = d->tv_sec + (d->tv_nsec / 1e9);
fprintf(fout, "%d,%s,%s,%s,%s,%s,%s,%.5f,%s,%s,%s\n", p_id,
stimulus_word, stimulus_color, left_option_word, left_option_color,
right_option_word, right_option_color, response_time,
response_choice, problem_type, correct_or_not);
}
void *
audio_thr(__attribute__((unused)) void *arg)
{
int correct_snd, wrong_snd;
int snd_req;
/* init audio system */
if (swgysnd_init(256) != 0) {
fprintf(stderr, "Failed to initialize swgysnd\n");
goto audio_exit;
}
if (swgysnd_loadwav(path_correct_sound, &correct_snd) == -1) {
fprintf(stderr, "Failed to load 'correct' sound\n");
goto audio_exit;
}
if (swgysnd_loadwav(path_wrong_sound, &wrong_snd) == -1) {
fprintf(stderr, "Failed to load 'wrong' sound\n");
goto audio_exit;
}
for (;;) {
pthread_mutex_lock(&audiolock);
while (sound_request == AUDIO_NO_REQ && !kill_audio) {
pthread_cond_wait(&audiocond, &audiolock);
}
if (kill_audio) {
pthread_mutex_unlock(&audiolock);
goto audio_exit;
}
snd_req = sound_request;
sound_request = AUDIO_NO_REQ;
pthread_mutex_unlock(&audiolock);
/* check for sound play request */
switch (snd_req) {
/* handle request and unset the sound request flag */
case AUDIO_REQ_CORRECT:
if (verbose)
fprintf(stderr, "playing correct\n");
swgysnd_playwav(correct_snd, 0);
break;
case AUDIO_REQ_WRONG:
if (verbose)
fprintf(stderr, "playing wrong\n");
swgysnd_playwav(wrong_snd, 0);
break;
default:
break;
}
}
audio_exit:
/* shutdown */
swgysnd_shutdown();
return NULL;
}
void
request_sound(int sound)
{
pthread_mutex_lock(&audiolock);
sound_request = sound;
pthread_cond_signal(&audiocond);
pthread_mutex_unlock(&audiolock);
}
void
kill_sound(void)
{
pthread_mutex_lock(&audiolock);
kill_audio = 1;
pthread_cond_signal(&audiocond);
pthread_mutex_unlock(&audiolock);
}