/*fastshft.c - Deliver a predetermined sequence of visual stimuli on alternating
  sides, with display updates synchronised to the refresh pulse.  Put codes on
  the parallel port for stimuli and for joystick responses.
  Copyright (c) 1996 Matthew Belmonte

  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 2
  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., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

  If you find this program useful, please send mail to Matthew Belmonte.
  <mkb4@Cornell.edu>.  If you base a publication on data processed by this
  program, please notify Matthew Belmonte and include the following citation
  in your publication:

	Matthew Belmonte, `A Software System for Analysis of
	Steady-State Evoked Potentials', Association for Computing
	Machinery SIGBIO Newsletter 17:1:9-14 (April 1997).
*/

/*For Microsoft QuickC, use QCL /G2 /Ox /Zp /F 4000 FASTSHFT.C*/

#define MAX_NUM_STIMULI 2048

#include <stdio.h>
#include <stdlib.h>
#include <graph.h>
#include <conio.h>
#include <errno.h>
#include "fastshft.h"
#define SLOW_JOYSTK_MIDPT -24 /*joystick_midpoint on a 20MHz 80386 system*/

/*port addresses*/
#define ColourSyncReg 0x3DA
#define ParallelPort 0x278 /*was 3BC on old machine*/
#define GamePort 0x201

/*game port masks*/
#define HorizontalMask 0x1      /*RC timer on pins 3 and 1*/
#define VerticalMask 0x2        /*RC timer on pins 6 and 1*/
#define ButtonMask 0x10         /*switch on pins 2 and 4*/
#define SecondButtonMask 0x20   /*switch on pins 7 and 4*/
#define ThirdButtonMask 0x80    /*switch on pins 14 and 4*/

/*parallel port control codes*/
#define CTBStrobe 7
#define CTBNormal 6

/*colours*/
#define TARGET 4        /*target square*/
#define NONTARGET 2     /*nontarget square*/
#define FIX 15          /*fixation point*/
#define BG 0            /*background*/

int joystick_midpoint,  /*negative offset s.t. joystick midpoint is 0*/
    prev_joystick;      /*last known orientation of the joystick*/

/*send an event code through the parallel port*/
void EventCode(code)
unsigned char code;
  {
  outp(ParallelPort, code);
  outp(ParallelPort+2, CTBStrobe);
  outp(ParallelPort+2, CTBNormal);
  }

/*sync to refresh pulse*/
void Sync()
  {
  while(inp(ColourSyncReg) & 8)
    ;
  while(!(inp(ColourSyncReg) & 8))
    ;
  }

/*sync to refresh pulse, and check the joystick while we're waiting*/
void SyncAndInput()
  {
  register int timer;
  int prev_timer;
  timer = joystick_midpoint;
/*get past any falling edge of the refresh pulse*/
  while(inp(ColourSyncReg) & 8) 
    ;
/*reset the timer*/
  outp(GamePort, 0);
/*wait for capacitor to discharge*/
  while((!(inp(ColourSyncReg) & 8)) && (inp(GamePort) & HorizontalMask))
    timer++;
/*give the timing value a coarser grain, to ensure proper debouncing*/
  timer = timer*SLOW_JOYSTK_MIDPT/joystick_midpoint;
/*if a refresh pulse is detected, stop this & get on w/ more timely tasks*/
  while(!(inp(ColourSyncReg) & 8))
  /*inv: 'timer' contains the most recent joystick measurement.
    'prev_joystick' is negative if the last known orientation of the joystick
    was to the left, or positive if to the right.  Event codes have been sent
    on all occasions on which the joystick has crossed the midline during the
    current screen refresh cycle.*/
    {
    prev_timer = timer;
    timer = joystick_midpoint;
  /*reset the timer*/
    outp(GamePort, 0);
  /*wait for capacitor to discharge*/
    while((!(inp(ColourSyncReg) & 8)) && (inp(GamePort) & HorizontalMask))
      timer++;
  /*give the timing value a coarser grain, to ensure proper debouncing*/
    timer = timer*SLOW_JOYSTK_MIDPT/joystick_midpoint;
  /*If the joystick has crossed the midline, send an event code.  We can't be
   sure whether a momentary decrease in the timer value is due to a leftward
   movement of the joystick or to the occurrence of an interrupt during the
   timing loop.  So we check two consecutive timer values.  (As long as
   nothing else is happening on the machine, interrupts won't occur so often
   as to clobber two successive timing attempts, so this is safe.)  If the
   last known joystick orientation was to the left, and we now find a positive
   timing value, then the joystick has definitely moved to the right.  If the
   last known joystick orientation was to the right, and we now find two
   consecutive negative timing values, then the joystick has definitely moved
   to the left.  Timing values of zero cause no action, and therefore serve to
   debounce the input.*/
    if((timer > 0) && (prev_joystick < 0))
      {
      EventCode(EV_RESPONSE+1);
      prev_joystick = timer;
      }
    else if((timer < 0) && (prev_timer < 0) && (prev_joystick > 0))
      {
      EventCode(EV_RESPONSE);
      prev_joystick = timer;
      }
    }
/*rising edge of the refresh pulse is occurring now*/
  }

/*for consistency in timing, this is similar to SyncAndInput() above*/
int Joystick()
  {
  register int timer;
  int prev_timer;
  Sync();
  timer = 0;
  while(inp(ColourSyncReg) & 8) 
    ;
  outp(GamePort, 0);
  while((!(inp(ColourSyncReg) & 8)) && (inp(GamePort) & HorizontalMask))
    timer++;
  prev_timer = timer;
  timer = 0;
  outp(GamePort, 0);
  while((!(inp(ColourSyncReg) & 8)) && (inp(GamePort) & HorizontalMask))
    timer++;
  if(inp(ColourSyncReg) & 8)
  /*this never happens*/
    {
    _setvideomode(_DEFAULTMODE);
    fprintf(stderr, "Fatal error: joystick timer outlasted refresh cycle!\n");
    exit(1);
    }
  return(timer>=prev_timer? timer: prev_timer);
  }

void main(argc, argv)
int argc;
char **argv;
  {
  register int n, trial, prev_target;
  int num_stimuli, max_num_stimuli, num_trials;
  unsigned int stim;
  FILE *stimulus_file;
  short square_hlength, square_vlength;
  struct videoconfig config;
  unsigned char stimuli[MAX_NUM_STIMULI];
  printf("Copyright (c) 1996 Matthew Belmonte <mkb4@Cornell.edu>.  Please cite.\n");
  if(argc != 2)
    {
    fprintf(stderr, "Usage: %s <stimulus file>\n", *argv);
    fprintf(stderr, "The stimulus file is a text file of integers separated by white space.\n");
    fprintf(stderr, "Stimuli in each block begin on the left, and alternate left and right.\n");
    fprintf(stderr, "Bit 0 of each stimulus code specifies target (0) or nontarget (1),\n");
    fprintf(stderr, "except for the special code %d, which specifies the end of a block.\n", EV_FINISH);
    exit(1);
    }
  if((stimulus_file = fopen(argv[1], "r")) == NULL)
    {
    fprintf(stderr, "couldn't open stimulus file %s\n", argv[1]);
    exit(errno);
    }

/*ascertain joystick midpoint*/
  joystick_midpoint = -Joystick();
  printf("Joystick midpoint is %d\n", -joystick_midpoint);

/*find out how many stimuli there are*/
  max_num_stimuli = 0;
  num_trials = 0;
  num_stimuli = 0;
  while(fscanf(stimulus_file, "%u", &stim) == 1)
  /*inv: the first num_trials blocks have been read, max_num_stimuli is the
    maximum of the lengths of those blocks (including the terminating
    EV_FINISH code), and the first num_stimuli codes have been read from
    the current block, and max_num_stimuli is the maximum of the lengths of
    the first num_trials blocks*/
    {
    if(stim == EV_FINISH)
      {
      if(num_stimuli > max_num_stimuli)
	max_num_stimuli = num_stimuli;
      num_stimuli = 0;
      num_trials++;
      }
    else
      num_stimuli++;
    }
  if(stim != EV_FINISH)
  /*if the final end-of-trial code was omitted, act as if it's present*/
    {
    if(num_stimuli > max_num_stimuli)
      max_num_stimuli = num_stimuli;
    num_trials++;
    }
  if(max_num_stimuli > MAX_NUM_STIMULI)
  /*not enough space for stimuli*/
    {
    fclose(stimulus_file);
    fprintf(stderr, "Too many stimuli - recompile %s w/ MAX_NUM_STIMULI=%d\n", *argv, max_num_stimuli);
    exit(1);
    }
  rewind(stimulus_file);

  printf("To continue, strike a key.\n");
  getch();

/*initialise video*/
  if(_setvideomode(_MRES16COLOR) == 0)
    {
    fprintf(stderr, "unsupported video mode\n");
    exit(1);
    }
  _getvideoconfig(&config);
  square_hlength = config.numypixels/4;
  square_vlength = 86*square_hlength/100; /*correct for pixel aspect ratio*/
  _setbkcolor(_BLACK);
  _clearscreen(_GCLEARSCREEN);
/*present lateral targets for HEOG calibration, & send codes on responses*/
  _setcolor(FIX);
  for(stim = 0; stim != 2; stim++)
    {
    while(!(inp(GamePort) & ButtonMask))
      ;
    Sync();
    _rectangle(_GFILLINTERIOR, stim*(config.numxpixels-1-square_hlength), config.numypixels/2-square_vlength/2, square_hlength+stim*(config.numxpixels-1-square_hlength), config.numypixels/2+square_vlength/2);
    while(inp(GamePort) & ButtonMask)
      ;
    EventCode(EV_HEOG_CALIB);
  /*debounce*/
    for(trial = 0; trial != 30; trial++)
      Sync();
    _clearscreen(_GCLEARSCREEN);
    }
/*draw fixation point*/
  _moveto(config.numxpixels/2, 3*config.numypixels/4 + 7*square_vlength/8);
  _lineto(config.numxpixels/2, 3*config.numypixels/4 + 9*square_vlength/8);
  _moveto(config.numxpixels/2 - square_hlength/8, 3*config.numypixels/4 + square_vlength);
  _lineto(config.numxpixels/2 + square_hlength/8, 3*config.numypixels/4 + square_vlength);

/*present trials*/
  for(trial = 0; trial != num_trials; trial++)
  /*inv: the first _trial_ blocks have been presented*/
    {
  /*read all stimuli into memory s.t. disk access won't alter timing*/
    num_stimuli = 0;
    while((fscanf(stimulus_file, "%u", &stim) == 1) && (stim != EV_FINISH))
      stimuli[num_stimuli++] = (unsigned char)stim;
  /*make sure the control button has been released*/
    while(!(inp(GamePort) & ButtonMask))
      ;
  /*Randomly choose an initial laterality, display a lateral cue, then wait for
    the subject to lateralise the joystick & to press the control button*/
    _setcolor(FIX);
    if(rand() < 16384)
    /*left*/
      {
      _rectangle(_GBORDER, 0, config.numypixels/2-square_vlength/2, square_hlength, config.numypixels/2+square_vlength/2);
      while((inp(GamePort) & ButtonMask) || ((prev_joystick=(Joystick()+joystick_midpoint)*SLOW_JOYSTK_MIDPT/joystick_midpoint) >= SLOW_JOYSTK_MIDPT/2))
	;
      }
    else
    /*right*/
      {
      _rectangle(_GBORDER, config.numxpixels-1-square_hlength, config.numypixels/2-square_vlength/2, config.numxpixels-1, config.numypixels/2+square_vlength/2);
      while((inp(GamePort) & ButtonMask) || ((prev_joystick=(Joystick()+joystick_midpoint)*SLOW_JOYSTK_MIDPT/joystick_midpoint) <= -SLOW_JOYSTK_MIDPT/2))
	;
      }
  /*record initial position of joystick*/
    EventCode(EV_START+(prev_joystick>0));
  /*sync to screen refresh*/
    Sync();
  /*start the trial*/
    prev_target = 0;
    for(n = 0; n < num_stimuli; n++)
    /*inv: the first _n_ stimuli in the block have been presented*/
      {
      if(stimuli[n] == 2)
	prev_target = 2;
    /*If the most recent target was also on the left, and a response to it has
      already occurred, then insert some targets on the left (currently
      unattended) side, to keep the task somewhat unpredictable.*/
      else if((prev_target == 2) && (prev_joystick > 0) && (rand() < 32767/21))
	stimuli[n] = 2;
    /*paint left-hand stimulus*/
      _setcolor((stimuli[n]&1)?NONTARGET:TARGET);
      _rectangle(_GFILLINTERIOR, 0, config.numypixels/2-square_vlength/2, square_hlength, config.numypixels/2+square_vlength/2);
    /*unpaint right-hand stimulus*/
      _setcolor(BG);
      _rectangle(_GFILLINTERIOR, config.numxpixels-1-square_hlength, config.numypixels/2-square_vlength/2, config.numxpixels-1, config.numypixels/2+square_vlength/2);
    /*send event code for left-hand stimulus*/
      EventCode(stimuli[n++]);
    /*sync*/
      SyncAndInput();
      SyncAndInput();
      SyncAndInput();
      SyncAndInput();
      if(n != num_stimuli) /*necessary in case of odd # of stimuli*/
	{
	if(stimuli[n] == 4)
	  prev_target = 4;
      /*If the most recent target was also on the right, and a response to it
	has already occurred, then insert some targets on the right (currently
	unattended) side, to keep the task somewhat unpredictable.*/
	else if((prev_target == 4) && (prev_joystick < 0) && (rand() < 32767/21))
	  stimuli[n] = 4;
      /*paint right-hand stimulus*/
	_setcolor((stimuli[n]&1)?NONTARGET:TARGET);
	_rectangle(_GFILLINTERIOR, config.numxpixels-1-square_hlength, config.numypixels/2-square_vlength/2, config.numxpixels-1, config.numypixels/2+square_vlength/2);
      /*unpaint left-hand stimulus*/
	_setcolor(BG);
	_rectangle(_GFILLINTERIOR, 0, config.numypixels/2-square_vlength/2, square_hlength, config.numypixels/2+square_vlength/2);
      /*send event code for right-hand stimulus*/
	EventCode(stimuli[n]);
      /*sync*/
	SyncAndInput();
	SyncAndInput();
	SyncAndInput();
	SyncAndInput();
	}
    /*tried to pre-draw and then use _remappalette and _remapallpalette here,
      but they're too slow - on an 80486 two calls to _remappalette() take 4
      refresh cycles, and one call to _remapallpalette takes 16 refresh cycles*/
      }
  /*unpaint both stimuli*/
    _setcolor(BG);
    _rectangle(_GFILLINTERIOR, config.numxpixels-1-square_hlength, config.numypixels/2-square_vlength/2, config.numxpixels-1, config.numypixels/2+square_vlength/2);
    _rectangle(_GFILLINTERIOR, 0, config.numypixels/2-square_vlength/2, square_hlength, config.numypixels/2+square_vlength/2);
  /*mark end of trial*/
    EventCode(EV_FINISH);
    }

  fclose(stimulus_file);
  _setvideomode(_DEFAULTMODE);
  exit(0);
  }
