//===================================================================================================================== // // readcxdata.c -- Implementation of MATLAB MEX function readcxdata() which parses analog data, digital event data, // and other information from a Maestro or Cntrlx (ie, cntrlxUNIX/PC) data file. // // AUTHOR: saruffner. // // DESCRIPTION: // // // CREDITS: This MEX function is based on Justin Gardner's readTrialData(), with mods by David Schoppik. I worked on // its handling of ContMode data files, fixed some miscellaneous bugs, and modified it to handle data files with // version >= 2. Such files are generated by Maestro, a WinNT/2000/XP application that handles the functionality of the // older cntrlxUNIX, cntrlxPC, and spikesPC all on a single PC platform. The function can also read in files // generated by cntrlxUNIX/PC, which I refer to as Cntrlx for short. // // // REVISION HISTORY: // 14mar2003-- Began development, using the source code from JG's READTRIAL.* and DECODE.* as a starting point. // 29sep2003-- Modified processEdits() so that it skips over unrecognized action codes instead of aborting -- which is // what readTrialData() did. XWORK appears to write some extraneous junk in the action edit record! // 01oct2003-- Modified to support reading in "sorted spikes" channel records appended by XWORK. // 19nov2003-- Modified to read in new field added to CXFILEHDR: 'dwXYSeed' is the value used to seed the random# // generators in the XY scope controllers (applicable to trial mode only). In Cntrlx (V<=1), this seed // value was communicated via the trial code RANDOM_SEED. // 20jan2004-- Fixed bug in uncompressAIData() that caused it to fail increment the scan count for the last scan's // worth of analog data decompressed. // 10may2004-- Added fields 'patvelH' and 'patvelV' to the output field 'targets'. These hold the computed velocity // trajectories for the patterns associated with certain extended video targets on the XY scope and // framebuffer platforms. This is rather wasteful except when a target pattern is perburbed, and // readcxdata() does not yet interpret the TARGET_PERTURB trial code set! // 14may2004-- Fixed bug in processing TARGET_H/VPOSREL trial codes in processTrialCodes(). // -- Updated processTrialCodes() so that it "moves" XY scope targets that are turned off IF the data file // was created by Maestro v1.2.1 or greater. Prior to that version, XY targets did NOT move while off. // 19may2004-- Updated processTrialCodes() to account for the effects of velocity stabilization on the position // trajectory of the selected target. The target's velocity trajectory is NOT adjusted and thus will not // accurately reflect the target's motion during the "open loop" segment. // 21jun2004-- Updated processTrialCodes() to also adjust the window velocity (hvel, vvel) of the stabilized target // during velocity stabilization. This was simply a matter of adding the eye velocity to the target's // precomputed velocity. // -- WARNING messages are now written to STDOUT only if the verbose flag is set. Several users have // complained about the messages... // 08jul2004-- Minor change to uncompressAIData() to handle fact that Cntrlx uses 0xFF as the "endOfData" marker for // AI data records in a Continuous mode data file. Also discovered that Cntrlx does NOT give an accurate // count of the actual # of bytes in the compressed AI data stream stored in the data file. readcxdata() // gets the correct count. This is not an issue with Maestro-generated files. // 02oct2004-- Added endian-conversion routines so that readcxdata() will run on big-endian (Mac) machines. All // Cntrlx/Maestro data files were created on little-endian machines. Endianness is determined at runtime // rather than using a defined constant at compile time. // 15dec2004-- Fixed minor bug in processTrialCodes() that caused seg fault when data file contained no analog data. // 14jan2005-- Fixed bug in mexFunction() that caused seg fault when data file contained spike waveform data -- it // was trying to store waveform data into the 'data' field instead of the 'spikewave' field! // 26jan2005-- Updated processTrialCodes() to skip over new trial code group MIDTRIALREW introduced into Maestro with // code changes dtd 25jan2005. // 12apr2005-- Mods to process new CX_TAGSECTRECORD detailing tagged sections within a Maestro trial. // -- I think that setting up the "stimulusrun" field is causing "cache consistency error" in MATLAB after // execution of readcxdata(). Disabled code that creates this field to see if problems go away... // 01aug2005-- Adding support to account for the effects of any perturbations (TARGET_PERTURB trial codes) on trial // target trajectories, including support for new noise perturbation PERT_ISGAUSS. // 03jan2006-- Modified processTrialCodes() to handle new trial code group RPDWINDOW, introduced in Maestro v1.4.0. // 25jan2006-- Modified trial header record processing to report values in new header fields, introduced in Maestro // v1.4.2, that provide info for the R/P Distro feature (if the trial uses it). // 18mar2006-- Modified IAW changes introduced in Maestro v1.5.0: elimination of OKNDRUM tgt platform; velocity // stabilization can act over a contiguous range of trial segs instead of just one. // 07apr2006-- Fixing bug in calculation of target trajectory during velocity stabilization. // 19apr2006-- Various mods to handle changes introduced in Maestro v2.0.0 (data file version >= 8): // 1) Replacement of VSG-based FB video with RMVideo targets. A new data structure, RMVTGTDEF in // RMVIDEO_COMMON.H, replaces FBPARMS in the union U_TGPARMS (see CXOBJ_IFC_MEX.H). This change affects // the storage of individual target definitions as CXFILETGT in the data file's target record(s). We have // to be able to handle both ld (CXFILETGT_V7) and current formats. // 2) Velocity stabilization is now possible on a per-target, per-segment basis; TARGET_HOPEN replaced by // TARGET_VSTAB trial code. We have to do a bit more work to compensate the precomputed trajectories of // trial targets for the effects of velocity stabilization. Fcn processTrialCodes() updated accordingly. // 19jun2006-- Mod to expose two new XY scope tgt defn params, XYPARMS.fInnerX and .fInnerY. These were introduced in // Maestro v2.0.1 (data file version 9). // 27feb2007-- Mod processTrialCodes() to support four new trial codes, INSIDE_HACC, INSIDE_VACC, INSIDE_HSLOACC, and // INSIDE_VSLOACC. These were introduced in Maestro v2.1.0 (data file version STILL = 9) to support // per-segment pattern acceleration in H and V. // 16may2007-- Mod to synch w/ changes introduced in Maestro v2.1.1 (data file version = 10), which supports 4 // alternative response measures for an R/P Distro trial. The response type is reported in the trial data // file header. // 16jul2007-- Mod to synch w/ changes introduced in Maestro v2.1.2 (data file version = 11), which adds support for // perturbing trial target window or pattern speed while keeping direction constant. Changes isolated to // the PERTMGR module. // 06sep2007-- Mod to synch w/ changes introduced in Maestro v2.1.3 (data file version = 12), which adds support for // simultaneously perturbing trial target window and pattern speed (PERT_ON_SPD), or window and pattern // direction (PERT_ON_DIR). This version also introduced a second algorithm for per-dot speed noise for // the NOISYSPEED XYScope tgt and the RMV_RANDOMDOTS RMVideo tgt, but those changes did not require any // mod to READCXDATA. (All changes isolated to PERTMGR module.) // 09jan2008-- Mods to handle new action codes introduced in JMWork v1.0. A successor to XWork and its Matlab sister // program MWork, JMWork lets the user manually add or remove individual spikes in a sorted-spike train // channel. The original sorted spike train is stored in the appropriate records, while the individual // edits are stored as ACTION_ADDSORTSPK and ACTION_REMOVESORTSPK actions. READCXDATA does not return the // original unedited spike trains; the sorted spike trains in the output structure reflect any spike edits // defined in the data file. // 10jan2008-- Mod to ACTION_CUTIT processing. In addition to preparing the 'cut' field in the output structure, // READCXDATA now actually makes the defined cuts in the eye-velocity data channels HEVEL, VEVEL, HDVEL. // The cuts are made after processing trial codes -- so that the code which does velocity stabilization of // target trajectories works with the original uncut eye velocity data. // 22jul2008-- Added output field 'psgm'. This structure contains the parametric definition of a Pulse Stimulus // Generator Module (PSGM) sequence delivered during a Maestro trial. If the data file was not recorded // during Trial mode, or if the PSGM_TC trial code was not sent, this field will be an empty array. It // does NOT provide a definitive indication of whether or not the PSGM sequence actually occurred. // 20jan2009-- The 'marked' flag is now set if either of two "styles" of discard marks are encountered among the action // edits: the presence of at least one mark segment (XWork-style), OR a mark1 at t=-1 (MWork-style). // 20jan2009-- Added output info 'trialInfo' holding some additional information about a Maestro trial, including info // on any perturbations applied during the trial. // 17sep2009-- Mod to synch w/ changes introduced in Maestro v2.5.0 (data file version = 13), which adds support for a // video target class, RMV_MOVIE, on the RMVideo platform. This introduced changes in the RMVTGTDEF struct, // impacting the format of target definition records in the Maestro data file. Also added a flag that // changes the behavior of RMV_GRATING; if RMV_F_ORIENTADJ set, both H and V cmpts of pattern velocity are // significant during animation, specifying a standard pattern velocity vector. The grating orientation is // adjusted frame-by-frame so that it is always perpendicular to this vector. When the flag is not set, the // grating target behaves as before: grating orientation is a constant, the V cmpt of pattern velocity is // ignored and the H cmpt is the grating's drift speed. // 22feb2010-- Fixed problem with the algorithm for converting interevent intervals in 10us ticks to absolute event // arrival times in ms (in readEvents() and readSortedSpikes()). Repeated reading by readcxdata() followed // by saving with editcxdata() introduced jitter in the arrival times that worsened with repetition, due to // double=>int roundoff issues. // 29apr2010-- Mod to synch w/ changes introduced in Maestro v2.6.0 (data file version = 15). Two fields were added to // the data file header, CXFILEHDR.iStartPosH, .iStartPosV. These are exposed as like-named members of the // "key" field in the output structure generated by the MEX function. // 21may2010-- Added field "on" to "targets" structure in output. This field is a 1xM cell array, where M is the number // of targets in trial. Each cell contains a vector with ON epochs, intervals during which that target was // ON during the trial: [tOn1 tOff1 tOn2 tOff2 ... ]. Times in ms in the RECORDED timeline. Since it // includes ON epochs that may have occurred prior to start of recording, some times could be negative. // They will be in chronological order. See processTrialCodes(). // 24may2010-- Mod to synch w/ change introduced in Maestro v2.6.0 (data file version = 16). Field added to the data // file header, CXFILEHDR.dwTrialFlags. It is exposed in a like-named member of the "key" field in the // output structure generated by this MEX function. // 22sep2010-- Mods to handle new action codes introduced in JMWork v1.4.0. (1) The ACTION_DISCARD action (#codes=1) is // added to the file to indicate that it should discarded/ignored in downstream analysis. This is intended to replace // the "hacked" conventions for marking a file as discarded. The ACTION_DEFTAG action (#codes=6) attaches a labelled // "tag" to the recorded timeline. The tag label is user-defined and must contain 1-16 visible ASCII characters. A tag // is a truly general-purpose mark that the user can tailor to his/her needs. The tag's timestamp is the second code in // the code group. The remaining 4 32-bit int codes form a 16-byte label field, packed in little-endian order and // padded with zeros if the label length is less than 16. // 09mar2011-- Fixed bug in processTrialCodes() regarding computation of trial trajectories for RMVideo targets. Prior // to fix, code was erroneously using the XYScope frame update interval (typ. 4ms) for the video frame update interval // instead of the RMVideo display update interval. // -- Mod to synch w/ changes introduced in Maestro v2.6.5 (data file version = 17). Field added to the data // file header, CXFILEHDR.iSTSelected. It is exposed in a like-named member of the "key" field in the output structure // generated by this MEX function. // 12apr2011-- Mods to support emulation of XYScope noisy-dots targets in Trial mode, for data file versions >= 12. // Added module XYNOISYEM.* to implement the emulation. Two new fields in output: "xynoisytimes" and "xynoisy". These // will be empty if data file version < 12, if file was not recorded in Trial mode, or if no XYScope noisy-dots targets // participated in the trial. // 16may2011-- Added support for new implementation of XYScope noisy-dots targets introduced in Maestro v2.7.0, data // file version 18. Added support for new "sliding-window average" feature during velocity stabilization of targets // in Trial mode. See processTrialCodes(). // 20may2011-- Changed all instances of mxCreateScalarDouble to mxCreateDoubleScalar. mxCreateScalarDouble was declared // obsolete in 2006, and code support is removed as of Matlab 2011a. // 11may2012-- Began changes to replace the XYNOISYEM.* module with NOISYEM.*, which adds support for emulating RMVideo // noisy-dots targets in Trial mode; XYNOISYEM.* only supported noisy-dots targets on the XYScope display platform. The // per-dot trajectories are still stored in the same fields in the output structure, "xynoisytimes" and "xynoisy", so as // not to break existing analysis code that looked at XYScope noisy-dots targets. // 16may2012-- Finished testing/debugging changes dtd 11may2012. Decided to include per-dot position as well as velocity // trajectories in "xynoisy" -- but ONLY for RMVideo noisy-dots targets; it's not possible to keep track of per-dot // positions for XYScope. // 06sep2013-- Revised to support 50 distinct sorted-spike train data channels instead of 13. This was mostly a matter // of documenting the change and and updating a few constants. See cx_filefmt_mex.h for more details. // 12aug2014-- Bug fix in processTrialCodes(). // 24nov2015-- Revised to handle Maestro data file changes relevant to added support for Eyelink tracker, as of Maestro // version 3.2.0, data file V=20. Revised out.key to include new header fields iVStabWinLen (V=18) and iELInfo (V=20). // Added out.blinks, an array of length N*2 listing N "blink epochs" recorded on the Eyelink 1000+ tracker. Each pair // of entries in the array represent the start and end times of a blink, in ms elapsed since recording started. // 16nov2016—- MINOR revision to recognize the new RMVideo target type RMV_IMAGE, introduced in Maestro v3.3.1. Note // that data file version did NOT change. We just need to test for type RMV_IMAGE so that the ‘strFolder’ and ‘strFile’ // fields are set correctly in setTargetDefns(). // 12jun2018— Revised to handle Maestro data file format changes in Maestro V4.0.0, which migrates Maestro to the // 64-bit Win10/RTX64 platform. Data file V=21. Revised out.key to include new header fields setName[] and subsetName[] // which hold the name of a trial’s parent set and subset, as applicable. // 01oct2018-- Revised to handle another data file format change for Maestro 4.0.0. Revised out.key to include new // header fields rmvSyncSz and rmvSyncDur to hold spot size and flash duration for new RMVideo "vertical sync flash". // 04dec2018-- Revised to handle data file format change in Maestro 4.0.1 (data file version unchanged). Revised out.key // to include new header field timestampMS. // 27mar2019-- Revisions to handle data file format change for Maestro 4.0.5 (data file version 22). Revised out.key to // to include new header field rmvDupEvents[]. Also changed out.key.d_framerate to a double scalar storing the RMVideo // refresh rate in milli-Hz. As of file version 22, CXFILEHDR.d_framerate is a 32-bit integer storing the refresh rate // in micro-Hz instead of milli-Hz. To avoid impacting programs that might use out.key.d_framerate, I decided to keep // the units the same, but storing it as a floating-point keeps the extra precision available in V>=22 data files. And // since d_framerate is used within READCXDATA to calculate the RMVideo refresh period, that calculation now depends on // the file version! // 13may2019-- Revised to handle data file format change for Maestro 4.1.0 (data file version 23). Target definition // record format was altered by the addition of three int-valued parameters to the RMVTGTDEF structure defined in // rmvideo_common.h to support a new "flicker" feature for RMVideo targets. //===================================================================================================================== #include #include #include #include #include #include "mex.h" #include "pertmgr.h" // this module handles most details of processing TARGET_PERTURB trial codes #include "noisyem.h" // emulation of XYScope OR RMVideo "noisy dots" targets in trial mode #include "readcxdata.h" // look here for all relevant constants, structure definitions //===================================================================================================================== // MODULE GLOBALS, CONSTANTS //===================================================================================================================== CXFILEDATA cxData; // data & info records in Maestro/Cntrlx file are parsed into members of this data struct int iVerbose; // if nonzero, printf's inform user of progress in reading data file (for debug) BOOL isBigEndian; // TRUE if system is big-endian, in which case endian conversions are necessary! MPERTMGR G_pertMgr; // processes perturbations (TARGET_PERTURB) of trial target trajectories size_t dims[2] = {1, 1}; // to set dimensions of various fields in the output structure; the dimensions may be // altered by mx***() methods as they're filled in. const int RECORDSZ = 1024; // size of each record in a Maestro/Cntrlx data file //===================================================================================================================== // FUNCTIONS DEFINED IN THIS MODULE //===================================================================================================================== void usage(); BOOL getNumRecordsInFile( FILE* pFile ); void displayHeader(); BOOL allocBuffers( BOOL bNoHeader ); void freeBuffers(); void endianSwap( BYTE* bytes, int nBytes ); void endianSwapHeader( CXFILEHDR* pHdr ); void endianSwapTgtDefV7( CXFILETGT_V7* pTgt ); void endianSwapTgtDefV12( CXFILETGT_V12* pTgt ); void endianSwapTgtDefV22( CXFILETGT_V22* pTgt ); void endianSwapTgtDef( CXFILETGT* pTgt ); void endianSwapStimDef( CXFILESTIM_U* pStimU, BOOL isHdr ); BOOL readAI( CXFILEREC* pRec ); void uncompressAIData( double* pDst, int iDstSz, char* pSrc, int iSrcSz, int nCh, int* pNC, int* pNScans ); BOOL readEvents( CXFILEREC* pRec ); BOOL readOthers( CXFILEREC* pRec ); BOOL readSortedSpikes( CXFILEREC* pRec ); BOOL readTrialCodes( CXFILEREC* pRec ); BOOL readEdits( CXFILEREC* pRec ); BOOL readTargets( CXFILEREC* pRec ); BOOL readStims( CXFILEREC* pRec ); BOOL readTagSections( CXFILEREC* pRec ); mxArray* createUInt32Scalar( DWORD dwValue ); mxArray* createInt32Scalar( int iValue ); void setSortedSpikesOutput( mxArray* pOut ); void setHeaderOutput( mxArray* pOut ); void setTargetDefns( mxArray* pOut ); void setStimulusRunDefn( mxArray* pOut ); void setTagSections( mxArray* pOut ); void setTrialInfo(mxArray* pOut); BOOL processEdits( mxArray* pOut ); void unpackTagLabel(char* sbuf, int* pLabelInts); void removeSortedSpike(int ch, int tSpk); void addSortedSpike(int ch, int tSpk); void cutVelocityTraces( mxArray* pOut ); void initializeNoisyDotsEmulator(); BOOL shouldAdjustPatternMotionAtSegStart(int pos); BOOL shouldAdjustPatternMotionDuringVStab(int pos); BOOL processTrialCodes( mxArray* pOut ); void prepareTgtIDs(); int mapTargetID( short nID ); //=== mexFunction (readcxdata) ======================================================================================== // // This is the method called from MATLAB to read in CNTRLX data files. // // readcxdata( 'filename' [, verbose, nchans] ) // where: // 'filename' ==> pathname of the CNTRLX data file. // verbose ==> if nonzero, detailed progress msgs are written to STDOUT (for debugging). // nchans ==> for headerless ContMode data files, we need to know how many analog data channels were // recorded in order to properly parse the compressed analog data in the file. // // ARGS: nlhs, plhs -- [out] array output ("left-hand side") containing data/info in the file. We EXPECT // nlhs==1, since readcxdata() returns everything in a single data structure as described // in the outputFields structure. // nrhs, prhs -- [in] array input. See above. // // RETURNS: NONE. If a fatal error occurs while processing the file, an error message is printed to STDOUT and // a partially completed (possibly) output structure is returned. // void mexFunction( int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[] ) { int i,j; BOOL bOk, bNeedBlinkEnd; BYTE recID; // first byte of a data file record is its "ID" CXFILEREC fileRec; // a generic CNTRLX data file record char strFileName[1024]; // data file's pathname FILE* pFile; // pointer to open data file double* pdData; BOOL bHeaderless; // TRUE for headerless ContMode data file CXFILEHDR* pHdr; // pointer to stored header record plhs[0] = mxCreateStructArray(2, dims, NUMOUTFIELDS, outputFields); // create empty output structure if(sizeof(CXFILEHDR) != RECORDSZ || sizeof(CXFILEREC) != RECORDSZ) // DBG: These structs MUST be the right size! { printf( "Bad record size: hdr = %d, generic rec = %d", sizeof(CXFILEHDR), sizeof(CXFILEREC) ); return; } memset( (VOID*) &cxData, 0, sizeof(CXFILEDATA) ); // init internal representation of file content pHdr = &(cxData.fileHdr); // ptr to data file header in internal storage if( nrhs < 1 || nrhs > 3 || nlhs != 1 ) // check input/output args { usage(); return; } if( nrhs >= 2 ) // turn on verbose progress reporting? iVerbose = (int) *mxGetPr( prhs[1] ); else iVerbose = 0; i=0; // detect endianness of system; we'll have to ((BYTE*) &i)[0] = 1; // do conversions if it is big-endian isBigEndian = (i==1) ? FALSE : TRUE; if( iVerbose ) printf( "Host is %s-endian!\n", isBigEndian ? "big" : "little" ); mxGetString( prhs[0], strFileName, mxGetN(prhs[0])+1 ); // get file's pathname if( (pFile = fopen( strFileName, "rb" )) == NULL ) // open the data file { printf( "ERROR: Could not open %s\n", strFileName ); return; } if( iVerbose ) printf( "Opened %s\n", strFileName ); if( !getNumRecordsInFile( pFile ) ) // determine #records in the file { fclose( pFile ); return; } if( iVerbose ) printf( "File contains %i records.\n", cxData.nRecords ); if( fread( (VOID*) &fileRec, RECORDSZ, 1, pFile ) == 0 ) // read header record into temporary buffer { printf( "ERROR: reading header record in file %s\n", strFileName ); fclose( pFile ); return; } bHeaderless = FALSE; recID = fileRec.idTag[0]; if( (fileRec.idTag[1] == 0) && // if this is a headerless ContMode data file: (recID <= CX_XWORKACTIONREC || recID == CX_V1TGTRECORD) ) { bHeaderless = TRUE; if( iVerbose ) printf( "This is a headerless ContMode file.\n" ); if( nrhs < 3 ) // user MUST specify the # of AI channels { // that were recorded! usage(); fclose( pFile ); return; } strcpy( pHdr->name, "**continuous_mode_run**" ); // make up a fake header for it... pHdr->nchans = (short) *mxGetPr( prhs[2] ); // get #channels recorded from RHS argument for( i = 0; (i < pHdr->nchans) && (i < CXH_MAXAI); i++ ) // ordered channel list -- MAY NOT REFLECT pHdr->chlist[i] = i; // WHICH CHANNELS WERE ACTUALLY RECORDED! pHdr->iPosScale = 1000; // here we're just setting defaults pHdr->iPosTheta = 0; pHdr->iVelScale = 1000; pHdr->iVelTheta = 0; pHdr->iRewLen1 = pHdr->iRewLen2 = 25; pHdr->version = 0; // since it's headerless, it was written // before versioning was introduced pHdr->flags |= CXHF_ISCONTINUOUS; // this is a ContMode file pHdr->nScanIntvUS = 2000; pHdr->nScansSaved = 0; // these are determined AFTER we've read in pHdr->nBytesCompressed = 0; // and processed all file records } else // for data files possessing header record: { memcpy( (VOID*) pHdr, (VOID*) &fileRec, RECORDSZ ); // copy header into internal storage if( isBigEndian ) endianSwapHeader( pHdr ); // convert endianness if necessary if( pHdr->version < 1 ) // for file versions < 1, the int-valued { // counters did not exist. here we set them pHdr->nScansSaved = (int) pHdr->npdig; // IAW the short-valued counters so we can pHdr->nBytesCompressed = (int) pHdr->nchar; // ignore the short-valued ctrs henceforth. } if( iVerbose ) displayHeader(); } if( pHdr->nchans < 0 || pHdr->nchans > CXH_MAXAI ) // check for illegal # of recorded AI channels { printf( "ERROR: %i channels recorded (max = %i).\n", pHdr->nchans, CXH_MAXAI ); fclose( pFile ); return; } if( (!bHeaderless) && // check for bad data length (pHdr->nScansSaved < 0 || pHdr->nBytesCompressed < 0) ) { printf( "ERROR: bad data length (nScans = %i, nBytes = %i).\n", pHdr->nScansSaved, pHdr->nBytesCompressed ); fclose( pFile ); return; } if( !allocBuffers( bHeaderless ) ) // alloc internal bufs to receive file data { printf( "ERROR: Unable to allocate internal buffers.\n" ); freeBuffers(); fclose( pFile ); return; } for( i = (bHeaderless) ? 0 : 1; i < cxData.nRecords; i++) // read in & process one record at a time: { if( i > 0 ) // read record into byte buffer; we don't { // need to do so for the first record in if( fread( (VOID*) &fileRec, RECORDSZ, 1, pFile ) == 0 ) // a headerless file, since we already read { // it in earlier! printf( "ERROR: Reading record %i in file %s\n", i, strFileName ); freeBuffers(); fclose( pFile ); return; } } if( iVerbose ) // report record id tag (first 8 bytes) { printf( "ID tag for record %i: ", i ); for( j = 0; j < 8; j++ ) printf( "%i ", (UINT) fileRec.idTag[j] ); printf( "\n" ); } bOk = TRUE; switch( fileRec.idTag[0] ) // process record IAW data type... { case CX_AIRECORD : case CX_SPIKEWAVERECORD : bOk = readAI( &fileRec ); break; case CX_EVENT0RECORD : case CX_EVENT1RECORD : bOk = readEvents( &fileRec ); break; case CX_OTHEREVENTRECORD : bOk = readOthers( &fileRec ); break; case CX_TRIALCODERECORD : bOk = readTrialCodes( &fileRec ); break; case CX_XWORKACTIONREC : bOk = readEdits( &fileRec ); break; case CX_TGTRECORD : bOk = readTargets( &fileRec ); break; case CX_STIMRUNRECORD : bOk = readStims( &fileRec ); break; case CX_TAGSECTRECORD : bOk = readTagSections( &fileRec ); break; default : // sorted spike train records have if( fileRec.idTag[0] >= CX_SPIKESORTREC_FIRST && // a range of record id tags... fileRec.idTag[0] <= CX_SPIKESORTREC_LAST ) bOk = readSortedSpikes( &fileRec ); else if( iVerbose) printf( "Skipped record!\n" ); break; } if( !bOk ) // abort if an error (memory realloc) { // occurred while processing a record freeBuffers(); fclose( pFile ); return; } } fclose( pFile ); // close the file if( iVerbose ) // report some results { printf( "Found %i spikes on DI channel 0\n", cxData.nSpikes ); printf( "Found %i events on DI channel 1\n", cxData.nEvents ); printf( "Found %i events on DI<2..15>\n", cxData.nOthers / 2 ); printf( "Found %i blink epochs (Eyelink)\n", cxData.nBlinkEvts / 2 ); for( i=0; i < NUMSPIKESORTCH; i++ ) if( cxData.nSortedSpikes[i] > 0 ) printf("Found %i spikes on sorted spike train channel %i\n", cxData.nSortedSpikes[i], i); } // // We now start copying data from our internal buffers into the various fields of the MATLAB-compatible output // structure. Of course, the MATLAB mx**() functions must allocate memory for the various fields, some of which may // be very large matrices. To make better use of system memory resources, we free each internal buffer as we finish // with it. Note that we save spike waveform data for last because that potentially uses the most memory by far.... // strFileName[CXH_NAME_SZ] = '\0'; // trial name. it's possible that the char str strncpy( strFileName, pHdr->name, CXH_NAME_SZ ); // in hdr lacks terminal NULL, so we copy to a mxSetField( plhs[0], 0, "trialname", mxCreateString(strFileName) ); // another string w/tacked on terminal NULL if( cxData.nSpikes > 0 ) // put spike times (ch0) into output array, { // then free the assoc internal buffer. mxSetField( plhs[0], 0, "spikes", mxCreateDoubleMatrix( 1, cxData.nSpikes, mxREAL ) ); memcpy( (void*) mxGetPr( mxGetField( plhs[0], 0, "spikes" ) ), (void*) cxData.pdSpikes, sizeof(double) * cxData.nSpikes ); } free( cxData.pdSpikes ); cxData.pdSpikes = NULL; if( cxData.nEvents > 0 ) // similarly for ch1 events array... { mxSetField( plhs[0], 0, "events", mxCreateDoubleMatrix( 1, cxData.nEvents, mxREAL ) ); memcpy( (void*) mxGetPr( mxGetField( plhs[0], 0, "events" ) ), (void*) cxData.pdEvents, sizeof(double) * cxData.nEvents ); } free( cxData.pdEvents ); cxData.pdEvents = NULL; if( cxData.nOthers > 0 ) // for events on DI<2..15>, we put the (ch#, { // time)-pairs in Mx2 matrix, M = #events mxSetField( plhs[0], 0, "other", mxCreateDoubleMatrix( cxData.nOthers/2, 2, mxREAL ) ); pdData = mxGetPr( mxGetField( plhs[0], 0, "other" ) ); for( i = 0; i < cxData.nOthers/2; i++ ) { pdData[i] = cxData.pdOthers[i*2]; pdData[i+cxData.nOthers/2] = cxData.pdOthers[i*2+1]; } } free( cxData.pdOthers ); cxData.pdOthers = NULL; // store blink epochs in "blinks" field Mx2 array, where M = # blink epochs (start time in first col).... if(cxData.nBlinkEvts > 0) { // if # blink events is odd, add a "blink end" event at the end of the recorded timeline bNeedBlinkEnd = ((cxData.nBlinkEvts % 2) != 0); if(bNeedBlinkEnd) ++(cxData.nBlinkEvts); j = cxData.nBlinkEvts/2; mxSetField(plhs[0], 0, "blinks", mxCreateDoubleMatrix(j, 2, mxREAL)); pdData = mxGetPr(mxGetField(plhs[0], 0, "blinks")); for(i=0; i < j-1; i++) { pdData[i] = cxData.pdBlinks[i*2]; pdData[i+j] = cxData.pdBlinks[i*2+1]; } i = j-1; pdData[i] = cxData.pdBlinks[i*2]; if(bNeedBlinkEnd) pdData[i+j] = (cxData.fileHdr.nScansSaved * cxData.fileHdr.nScanIntvUS) / 1000.0; else pdData[i+j] = cxData.pdBlinks[i*2+1]; } if(cxData.pdBlinks != NULL) { free(cxData.pdBlinks); cxData.pdBlinks = NULL; } if( pHdr->nchans > 0 && cxData.nAIBytes == 0 ) // expected AI data in file but found none! { if( iVerbose ) printf( "WARNING: Expected but found no AI data in file!\n" ); } else if( pHdr->nchans > 0 ) // uncompress AI channel data into output array { pdData = // uncompress the data into a temp array so (double*) malloc( sizeof(double) * cxData.nAIBytes ); // we can verify #scans saved. this is esp. uncompressAIData( pdData, cxData.nAIBytes, // important for headerless files, since we cxData.pcAIData, cxData.nAIBytes, pHdr->nchans, &i, &j ); // have no idea how many scans were saved! if( bHeaderless ) // for such files we did not know #bytes { // compressed & #scans saved apriori. fill pHdr->nBytesCompressed = i; // in that info in header fields now! pHdr->nScansSaved = j; pHdr->nchar = (short) i; // (the short-valued versions of these ctrs) pHdr->npdig = (short) j; } else // for files w/headers, warn user if either { // of these values were misreported & if( pHdr->nBytesCompressed != i ) // correct them... { if( iVerbose ) printf( "WARNING: File header misreported #AI bytes compressed: reported = %i, actual = %i\n", pHdr->nBytesCompressed, i ); pHdr->nBytesCompressed = i; } if( pHdr->nScansSaved != j ) { if( iVerbose ) printf( "WARNING: File header misreported #AI scan epochs saved: reported = %i, actual = %i", pHdr->nScansSaved, j ); pHdr->nScansSaved = j; } pHdr->nchar = (short) pHdr->nBytesCompressed; pHdr->npdig = (short) pHdr->nScansSaved; } free( cxData.pcAIData ); cxData.pcAIData = NULL; // free the original compressed data buffer mxSetField( plhs[0], 0, "data", // create #ch x #scans output matrix mxCreateDoubleMatrix( pHdr->nchans, pHdr->nScansSaved, mxREAL ) ); i = ((int)pHdr->nchans) * pHdr->nScansSaved; memcpy( (void*) mxGetPr( mxGetField( plhs[0], 0, "data" ) ), // and copy data from tmp array into matrix (void*) pdData, sizeof(double) * i ); free( pdData ); // free the temp array } setHeaderOutput( plhs[0] ); // create data file hdr struct and copy data if( cxData.nEdits > 0 ) // process XWork/JMWork editing actions, { // filling out relevant fields in MATLAB output if( iVerbose ) // structure... printf( "Processing %i XWork/JMWork edit actions...\n", cxData.nEdits ); if( !processEdits( plhs[0] ) ) { freeBuffers(); return; } } // output all sorted spike train channel data. THIS MUST BE CALLED AFTER processEdits(), which will alter any sorted // spike trains found IAW any "spike-edit" actions found among the action codes read from the file!!! setSortedSpikesOutput( plhs[0] ); // before processing trial codes (if any), initialize the noisy-dots target emulator so that it is in a valid // state. If this is not a trial data file, or if the trial did not use any noisy-dots targets, then the emulator // won't be used. initializeNoisyDotsEmulator(); if( cxData.nCodes > 0 ) // process trial codes, filling out relevant { // fields in MATLAB output structure... if( iVerbose ) printf( "Processing %i trial codes...\n", cxData.nCodes ); if( !processTrialCodes( plhs[0] ) ) { freeBuffers(); return; } } // save results from noisy dots target emulation, then release memory allocated by emulator. If the emulator // wasn't used, this simply sets the relevant fields in the output structure to empty arrays. setNoisyDotsResults(plhs[0]); releaseNoisyDotsEmulator(); // alter eye velocity traces in 'data' field IAW per-channel saccade cuts listed in 'cuts' field. Should be called // AFTER processTrialCodes(), which may need the original eye velocity trajectories to do vel stab of targets! cutVelocityTraces( plhs[0] ); setTagSections( plhs[0] ); // store info on any tagged sections found setTrialInfo(plhs[0]); // store additional trial info (as of 20Jan09) setTargetDefns( plhs[0] ); // store target & stimulus run defns, if any, // setStimulusRunDefn( plhs[0] ); // in relevant fields of MATLAB output struct // TODO: MATLAB cannot handle array of structs // of different sizes?? "stimulusrun" field // currently unavailable if( cxData.nFastBytes > 0 ) // uncompress spike waveform into output array: { pdData = // uncompress the data into a temp array so (double*) malloc( sizeof(double) * cxData.nFastBytes ); // we can determin #samples saved before uncompressAIData( pdData, cxData.nFastBytes, // allocating MATLAB array... cxData.pcFastData, cxData.nFastBytes, 1, &i, &j ); free( cxData.pcFastData ); cxData.pcFastData = NULL; // free the original compressed data buffer if( pHdr->nSpikeBytesCompressed != i ) // # "fast" bytes compressed incorrect in { // header; inform user & correct. if( iVerbose ) printf( "WARNING: File header misreported # spike waveform bytes compressed: reported = %i, actual = %i\n", pHdr->nSpikeBytesCompressed, i ); pHdr->nSpikeBytesCompressed = i; } if( iVerbose ) printf( "Found %i spike waveform samples\n", j ); mxSetField( plhs[0], 0, "spikewave", // create output array mxCreateDoubleMatrix( 1, j, mxREAL ) ); memcpy( (void*) mxGetPr( mxGetField( plhs[0], 0, "spikewave" ) ), // and copy data from tmp array into it (void*) pdData, sizeof(double) * j ); free( pdData ); // free the temp array } freeBuffers(); // make sure all alloc'd memory has been freed } //=== usage =========================================================================================================== // // Prints readcxdata() usage details to STDOUT. // void usage() { printf( "USAGE: d = readcxdata( 'filename' [,verbose, nchans]) \n" ); printf( " filename --> pathname of CNTRLX data file \n" ); printf( " verbose --> if nonzero, fcn prints detailed progress messages \n" ); printf( " nchans --> #AI chans recorded [0..16]; req'd only for *headerless* ContMode files (pre-Dec2001) \n" ); } //=== getNumRecordsInFile ============================================================================================= // // Determines file length by seeking EOF and reading file position, then sets #records accordingly. Assumes file // is opened in binary (vs text) mode, and returns file ptr to the file's beginning upon successful return. // // ARGS: pFile -- [in] open file pointer. // // RETURNS: TRUE if successful, FALSE otherwise (a fatal error -- appropriate error msg printed to STDOUT). // BOOL getNumRecordsInFile( FILE* pFile ) { long nFileBytes; if( fseek( pFile, 0, SEEK_END ) != 0 ) // seek to end of file { printf( "ERROR: Could not seek to end of file.\n" ); return( FALSE ); } nFileBytes = ftell( pFile ); // end of file pos = file len in bytes if it is if( nFileBytes == -1L ) // opened in binary mode { printf( "ERROR: Unable to read file ptr position.\n" ); return( FALSE ); } if( (nFileBytes % RECORDSZ) != 0 ) // there should always be an integral # of { // data records in a CNTRLX data file! printf( "ERROR: File does not have an integral # of %i-byte records; filesize = %i.\n", RECORDSZ, nFileBytes ); return( FALSE ); } if( fseek( pFile, 0, SEEK_SET ) != 0 ) // restore file ptr to file's beginning { printf( "ERROR: Could not seek to beginning of file.\n" ); return( FALSE ); } cxData.nRecords = (int) (nFileBytes/((long)RECORDSZ)); // calc #records in file return( TRUE ); } //=== displayHeader =================================================================================================== // // Prints to STDOUT the contents of data file header record (CXFILEHDR struct) that was read into internal storage. // void displayHeader() { int i; CXFILEHDR* pHdr; char strName[CXH_NAME_SZ+1]; pHdr = &(cxData.fileHdr); strName[CXH_NAME_SZ] = '\0'; // in case trial name in header does not have terminal NULL strncpy( strName, pHdr->name, CXH_NAME_SZ ); printf( "name: %s\n", strName ); printf( "trhdir: %i\n", pHdr->trhdir ); printf( "trvdir: %i\n", pHdr->trvdir ); printf( "nchar: %i\n", pHdr->nchar ); printf( "npdig: %i\n", pHdr->npdig ); printf( "nchans: %i\n", pHdr->nchans ); printf( "chlist: " ); for( i = 0; i < pHdr->nchans; i++ ) printf( "%i ", pHdr->chlist[i] ); printf("\n"); printf( "d_rows: %i\n", pHdr->d_rows ); printf( "d_cols: %i\n", pHdr->d_cols ); printf( "d_crow: %i\n", pHdr->d_crow ); printf( "d_ccol: %i\n", pHdr->d_ccol ); printf( "d_dist: %i\n", pHdr->d_dist ); printf( "d_dwidth: %i\n", pHdr->d_dwidth ); printf( "d_dheight: %i\n", pHdr->d_dheight ); // units for d_framerate: milli-Hz for V<22, micro-Hz for V>=22 printf( "d_framerate: %i %s\n", pHdr->d_framerate, (pHdr->version < 22) ? "milli-Hz" : "micro-Hz"); printf( "iPosScale: %i\n", pHdr->iPosScale ); printf( "iPosTheta: %i\n", pHdr->iPosTheta ); printf( "iVelScale: %i\n", pHdr->iVelScale ); printf( "iVelTheta: %i\n", pHdr->iVelTheta ); printf( "iRewLen1: %i\n", pHdr->iRewLen1 ); printf( "iRewLen2: %i\n", pHdr->iRewLen2 ); if( pHdr->version > 0 ) { printf( "dayRecorded: %i\n", pHdr->dayRecorded ); printf( "monthRecorded: %i\n", pHdr->monthRecorded ); printf( "yearRecorded: %i\n", pHdr->yearRecorded ); if(pHdr->version >= 21) printf( "timestamp: %i ms\n", pHdr->timestampMS ); printf( "version: %i\n", pHdr->version ); printf( "flags: 0x%08x\n", pHdr->flags ); printf( "nScanIntvUS: %i\n", pHdr->nScanIntvUS ); printf( "nBytesCompressed: %i\n", pHdr->nBytesCompressed ); printf( "nScansSaved: %i\n", pHdr->nScansSaved ); if( pHdr->version == 1 && pHdr->spikesFName[0] != 0 ) printf( "spikes file: %s\n", pHdr->spikesFName ); } if( pHdr->version > 1 ) { printf( "nSpikeBytesCompressed: %i\n", pHdr->nSpikeBytesCompressed ); printf( "nSpikeSampIntvUS: %i\n", pHdr->nSpikeSampIntvUS ); } if( (pHdr->version > 2) && ((pHdr->flags & CXHF_ISCONTINUOUS) == 0) ) printf( "dwXYSeed: 0x%08x\n", pHdr->dwXYSeed ); if( (pHdr->version >= 6) && ((pHdr->flags & CXHF_ISRPDISTRO) != 0) ) { printf( "iRPDStart: %i\n", pHdr->iRPDStart ); printf( "iRPDDur: %i\n", pHdr->iRPDDur ); if(pHdr->version >= 10) printf("iRPDRespType: %i\n", pHdr->iRPDRespType); printf( "iRPDResponse: %i\n", pHdr->iRPDResponse ); printf( "iRPDWindows: " ); for( i = 0; i < 4; i++ ) printf( "%i ", pHdr->iRPDWindows[i] ); printf("\n"); } if( (pHdr->version >= 16) && ((pHdr->flags & CXHF_ISCONTINUOUS) == 0) ) printf( "dwTrialFlags: 0x%08x\n", pHdr->dwTrialFlags ); if( (pHdr->version >= 17) && ((pHdr->flags & CXHF_ISSEARCHTSK) != 0) ) printf( "Search task selected tgt index: %i\n", pHdr->iSTSelected ); if(pHdr->version >= 18) printf("Sliding window length to improve VStab: %i ms\n", pHdr->iVStabWinLen); if((pHdr->version >= 20) && ((pHdr->flags & CXHF_EYELINKUSED) != 0)) { printf("Eyelink 1000+ eye tracker used to monitor eye position...\n"); printf(" record type = %i\n", pHdr->iELInfo[0]); printf(" X,Y cal offset = %i, %i\n", pHdr->iELInfo[1], pHdr->iELInfo[2]); printf(" X,Y cal gain = %i, %i\n", pHdr->iELInfo[3], pHdr->iELInfo[4]); printf(" vel smoothing width = %i ms\n", pHdr->iELInfo[5]); printf(" # of repeat samples = %i\n", pHdr->iELInfo[6]); printf(" inter-sample delay: max=%i ms, avg=%.1f ms\n", pHdr->iELInfo[7], ((double) pHdr->iELInfo[8])/1000.0); } if((pHdr->version >= 21) && ((pHdr->flags & CXHF_ISCONTINUOUS) == 0)) { printf("Trial set = %s\n", pHdr->setName); printf("Trial subset = %s\n", (pHdr->subsetName[0] != 0) ? pHdr->subsetName : "N/A"); printf("RMV VSync Flash: spot size (mm) = %d, dur (#frames) = %d\n", pHdr->rmvSyncSz, pHdr->rmvSyncDur); } if((pHdr->version >= 22) && ((pHdr->flags & CXHF_DUPFRAME) != 0)) { printf("Duplicate frames detected by RMVideo during trial:\n"); for(i=0; irmvDupEvents[i] > 0) { if(pHdr->rmvDupEvents[i+1] == 0) printf(" --> Single repeat at frame index %d due to late frame update.\n", pHdr->rmvDupEvents[i]); else printf(" --> %d repeats starting at frame index %d due to render delay.\n", pHdr->rmvDupEvents[i+1], pHdr->rmvDupEvents[i]); } } } //=== allocBuffers =================================================================================================== // // Allocates internal buffers for storing the certain types of data retrieved from a CNTRLX data file. We attempt // to allocate as little memory as necessary, while trying to reduce the likelihood that we'll have to reallocate // buffers later. Reallocation may be required if there's more data than anticipated. We must unavoidably // overestimate when the data file is a headerless version=0 ContMode file. // // NOTE: We do NOT allocate any buffers for "sorted spike train" channels. Instead, these are lazily allocated as // they are needed in readSortedSpikes(). The vast majority of data files will not contain "spike sorting" records, // and those that do will typically only use a few of the channels. Analogously for blink events detected when the // Eyelink tracker is used. // // ARGS: bNoHeader -- [in] TRUE if data file lacked a header record (version 0 ContMode file). // // RETURNS: TRUE if successful, FALSE if a memory allocation failed (a fatal error). // BOOL allocBuffers( BOOL bNoHeader ) { int i; CXFILEHDR* pHdr; // ptr to data file header int nAllOtherRecs; // approx # of all records in file OTHER // THAN AI data & spike waveform records pHdr = &(cxData.fileHdr); if( bNoHeader ) // for version 0 headerless files: { if( pHdr->nchans > 0 ) // AI data recorded, but we don't know { // how much. assume 3/4 of file records cxData.nAIBufSz = 3 * cxData.nRecords * CX_RECORDBYTES / 4; // contain AI data. cxData.nAIBufSz += RECORDSZ; nAllOtherRecs = cxData.nRecords / 4; } else // no AI data recorded { cxData.nAIBufSz = 0; nAllOtherRecs = cxData.nRecords; } cxData.nFastBufSz = 0; // spk wvform data not found in v0 files } else // otherwise: { i = 0; // # AI or spike waveform recs in file if( pHdr->nBytesCompressed > 0 ) // determine approx # of AI recs in file { // and how much space we need for AI data i += pHdr->nBytesCompressed/CX_RECORDBYTES; if( (pHdr->nBytesCompressed % CX_RECORDBYTES) != 0 ) ++i; cxData.nAIBufSz = pHdr->nBytesCompressed + RECORDSZ; } if( pHdr->nSpikeBytesCompressed > 0 ) // analogously for spike waveform data... { i += pHdr->nSpikeBytesCompressed/CX_RECORDBYTES + 1; if( (pHdr->nSpikeBytesCompressed % CX_RECORDBYTES) != 0 ) ++i; cxData.nFastBufSz = pHdr->nSpikeBytesCompressed + RECORDSZ; } nAllOtherRecs = cxData.nRecords - 1 - i; // #records not containing AI or spike if( nAllOtherRecs < 0 ) nAllOtherRecs = 0; // waveform data, excluding header rec } if( cxData.nAIBufSz > 0 ) // alloc buf for compressed AI data { cxData.pcAIData = (char *) malloc( sizeof(char) * cxData.nAIBufSz ); if( cxData.pcAIData == NULL ) return( FALSE ); } if( cxData.nFastBufSz > 0 ) // alloc buf for spike waveform data { cxData.pcFastData = (char *) malloc( sizeof(char) * cxData.nFastBufSz ); if( cxData.pcFastData == NULL ) return( FALSE ); } // sorted-spike train channel data bufs are NOT pre-allocated for(i=0; i 0 ) // alloc other bufs in a sensible way... { cxData.nSpikesBufSz = nAllOtherRecs * CX_RECORDINTS; // typically, CX_EVENT0RECORD is most cxData.pdSpikes = // common one after the AI data record (double *) malloc( sizeof(double) * cxData.nSpikesBufSz ); if( cxData.pdSpikes == NULL ) return( FALSE ); if( nAllOtherRecs <= 4 ) cxData.nEventsBufSz = CX_RECORDINTS; else cxData.nEventsBufSz = nAllOtherRecs * CX_RECORDINTS / 2; cxData.pdEvents = (double *) malloc( sizeof(double) * cxData.nEventsBufSz ); if( cxData.pdEvents == NULL ) return( FALSE ); cxData.nOthersBufSz = 2 * CX_RECORDINTS; // there's usually not a lot of these cxData.pdOthers = (double *) malloc( sizeof(double) * cxData.nOthersBufSz ); if( cxData.pdOthers == NULL ) return( FALSE ); cxData.nEditsBufSz = 2 * CX_RECORDINTS; // there's usually not a lot of edits cxData.piEdits = (int *) malloc( sizeof(int) * cxData.nEditsBufSz ); if( cxData.piEdits == NULL ) return( FALSE ); if( pHdr->version >= 2 ) // detailed tgt defns only read from data { // files w/ version >=2 i = MAX_TRIALTARGS; // calc the most #tgts that would be if( i < MAX_ACTIVETGTS + MAXTGTSINXYSEQ ) // defined in a CNTRLX data file i = MAX_ACTIVETGTS + MAXTGTSINXYSEQ; cxData.nTgtsBufSz = i + 5; // plus a little extra if(pHdr->version >= 23) // current CXFILETGT as of v=23 { cxData.pTargets = (CXFILETGT*) malloc( sizeof(CXFILETGT) * cxData.nTgtsBufSz ); if( cxData.pTargets == NULL ) return( FALSE ); } else if(pHdr->version >= 13) // deprecated CXFILETGT_V22 for v=13..22 { cxData.pTargets_V22 = (CXFILETGT_V22*) malloc( sizeof(CXFILETGT_V22) * cxData.nTgtsBufSz ); if( cxData.pTargets_V22 == NULL ) return( FALSE ); } else if(pHdr->version >= 8) // deprecated CXFILETGT_V12 for v=8..12 { cxData.pTargets_V12 = (CXFILETGT_V12*) malloc( sizeof(CXFILETGT_V12) * cxData.nTgtsBufSz ); if( cxData.pTargets_V12 == NULL ) return( FALSE ); } else // deprecated CXFILETGT_V7 for v=2..7 { cxData.pTargets_V7 = (CXFILETGT_V7*) malloc( sizeof(CXFILETGT_V7) * cxData.nTgtsBufSz ); if( cxData.pTargets_V7 == NULL ) return( FALSE ); } } if( !(pHdr->flags & CXHF_ISCONTINUOUS) ) // trial codes apply only to TrialMode files { cxData.nCodesBufSz = 1000; // this should be plenty!! cxData.pCodes = (TRIALCODE*) malloc( sizeof(TRIALCODE) * cxData.nCodesBufSz ); if( cxData.pCodes == NULL ) return( FALSE ); } else if( pHdr->version >= 2 ) // stim run defn record applies only to { // ContMode files w/ version >= 2 cxData.nStimsBufSz = MAXSTIMULI + 5; // we should never need more cxData.pStimBuf = (CXFILESTIM_U*) malloc( sizeof(CXFILESTIM_U) * cxData.nStimsBufSz ); if( cxData.pStimBuf == NULL ) return( FALSE ); } } return( TRUE ); } //=== freeBuffers ==================================================================================================== // // Free all internal data buffers. // void freeBuffers() { int i; if( cxData.pcAIData != NULL ) { free( cxData.pcAIData ); cxData.pcAIData = NULL; cxData.nAIBufSz = 0; } if( cxData.pcFastData != NULL ) { free( cxData.pcFastData ); cxData.pcFastData = NULL; cxData.nFastBufSz = 0; } if( cxData.pdSpikes != NULL ) { free( cxData.pdSpikes ); cxData.pdSpikes = NULL; cxData.nSpikesBufSz = 0; } if( cxData.pdEvents != NULL ) { free( cxData.pdEvents ); cxData.pdEvents = NULL; cxData.nEventsBufSz = 0; } if( cxData.pdOthers != NULL ) { free( cxData.pdOthers ); cxData.pdOthers = NULL; cxData.nOthersBufSz = 0; } if( cxData.pdBlinks != NULL ) { free( cxData.pdBlinks ); cxData.pdBlinks = NULL; cxData.nBlinksBufSz = 0; } for( i=0; i < NUMSPIKESORTCH; i++ ) { if( cxData.pdSortedSpikes[i] != NULL ) { free( cxData.pdSortedSpikes[i] ); cxData.pdSortedSpikes[i] = NULL; cxData.nSortedBufSz[i] = 0; } } if( cxData.piEdits != NULL ) { free( cxData.piEdits ); cxData.piEdits = NULL; cxData.nEditsBufSz = 0; } // CXFILETGT was changed in data file version 8, in version 13, and in version 23 if(cxData.pTargets != NULL) { free(cxData.pTargets); cxData.pTargets = NULL; cxData.nTgtsBufSz = 0; } if(cxData.pTargets_V7 != NULL) { free(cxData.pTargets_V7); cxData.pTargets_V7 = NULL; cxData.nTgtsBufSz = 0; } if(cxData.pTargets_V12 != NULL) { free(cxData.pTargets_V12); cxData.pTargets_V12 = NULL; cxData.nTgtsBufSz = 0; } if(cxData.pTargets_V22 != NULL) { free(cxData.pTargets_V22); cxData.pTargets_V22 = NULL; cxData.nTgtsBufSz = 0; } if(cxData.pCodes != NULL) { free(cxData.pCodes); cxData.pCodes = NULL; cxData.nCodesBufSz = 0; } if(cxData.pStimBuf != NULL) { free(cxData.pStimBuf); cxData.pStimBuf = NULL; cxData.nStimsBufSz = 0; } } //=== endianSwap ====================================================================================================== // // Swaps endianness of atomic types like short, int, float, and double. // // CREDIT: Code Project article dtd 19Aug2003 by JC Cobas (http://www.codetools.com/cpp/endianness.asp#xx586418xx). // // ARGS: bytes -- [in/out] address of atomic value, cast to a byte array for access to individual bytes. // nBytes-- [in] the number of bytes in the atomic value. // void endianSwap( BYTE* bytes, int nBytes ) { register int i = 0; register int j = nBytes-1; register BYTE ucTmp = 0; while( itrhdir), nShort ); endianSwap( (BYTE*) &(pHdr->trvdir), nShort ); endianSwap( (BYTE*) &(pHdr->nchar), nShort); endianSwap( (BYTE*) &(pHdr->npdig), nShort); endianSwap( (BYTE*) &(pHdr->nchans), nShort ); for( i=0; ichlist[i]), nShort ); endianSwap( (BYTE*) &(pHdr->d_rows), nShort ); endianSwap( (BYTE*) &(pHdr->d_cols), nShort ); endianSwap( (BYTE*) &(pHdr->d_crow), nShort ); endianSwap( (BYTE*) &(pHdr->d_ccol), nShort ); endianSwap( (BYTE*) &(pHdr->d_dist), nShort ); endianSwap( (BYTE*) &(pHdr->d_dwidth), nShort ); endianSwap( (BYTE*) &(pHdr->d_dheight), nShort ); endianSwap( (BYTE*) &(pHdr->d_framerate), nInt ); endianSwap( (BYTE*) &(pHdr->iPosScale), nInt ); endianSwap( (BYTE*) &(pHdr->iPosTheta), nInt ); endianSwap( (BYTE*) &(pHdr->iVelScale), nInt ); endianSwap( (BYTE*) &(pHdr->iVelTheta), nInt ); endianSwap( (BYTE*) &(pHdr->iRewLen1), nInt ); endianSwap( (BYTE*) &(pHdr->iRewLen2), nInt ); endianSwap( (BYTE*) &(pHdr->dayRecorded), nInt ); endianSwap( (BYTE*) &(pHdr->monthRecorded), nInt ); endianSwap( (BYTE*) &(pHdr->yearRecorded), nInt ); endianSwap( (BYTE*) &(pHdr->version), nInt ); endianSwap( (BYTE*) &(pHdr->flags), sizeof(DWORD) ); endianSwap( (BYTE*) &(pHdr->nScanIntvUS), nInt ); endianSwap( (BYTE*) &(pHdr->nBytesCompressed), nInt ); endianSwap( (BYTE*) &(pHdr->nScansSaved), nInt ); endianSwap( (BYTE*) &(pHdr->nSpikeBytesCompressed), nInt ); endianSwap( (BYTE*) &(pHdr->nSpikeSampIntvUS), nInt ); endianSwap( (BYTE*) &(pHdr->dwXYSeed), sizeof(DWORD) ); endianSwap( (BYTE*) &(pHdr->iRPDStart), nInt ); endianSwap( (BYTE*) &(pHdr->iRPDDur), nInt ); endianSwap( (BYTE*) &(pHdr->iRPDResponse), nInt ); for( i=0; i<4; i++ ) endianSwap( (BYTE*) &(pHdr->iRPDWindows[i]), nInt ); endianSwap( (BYTE*) &(pHdr->iRPDRespType), nInt ); endianSwap( (BYTE*) &(pHdr->iStartPosH), nInt ); endianSwap( (BYTE*) &(pHdr->iStartPosV), nInt ); endianSwap( (BYTE*) &(pHdr->dwTrialFlags), sizeof(DWORD) ); endianSwap( (BYTE*) &(pHdr->iSTSelected), nInt ); endianSwap( (BYTE*) &(pHdr->iVStabWinLen), nInt ); for( i=0; i<9; i++ ) endianSwap( (BYTE*) &(pHdr->iELInfo[i]), nInt ); endianSwap((BYTE*) &(pHdr->rmvSyncSz), nShort); endianSwap((BYTE*) &(pHdr->rmvSyncDur), nShort); endianSwap((BYTE*) &(pHdr->timestampMS), nInt); for( i=0; irmvDupEvents[i]), nInt ); } //=== endianSwapTgtDefV7 ============================================================================================== // // Swaps endianness of the individual members in the CXFILETGT_V7 structure encapsulating definition of a Maestro // target within the data file's CX_TGTRECORD record -- for data files with version numbers in [2..7]. // // ARGS: pTgt -- [in/out] The target definition structure. Endianness of atomic members converted in place. // void endianSwapTgtDefV7( CXFILETGT_V7* pTgt ) { int i, nInt, nFloat; WORD wType; nInt = sizeof(int); nFloat = sizeof(float); endianSwap( (BYTE*) &(pTgt->dwState), sizeof(DWORD) ); endianSwap( (BYTE*) &(pTgt->fPosX), nFloat ); endianSwap( (BYTE*) &(pTgt->fPosY), nFloat ); endianSwap( (BYTE*) &(pTgt->def.wType), sizeof(WORD) ); wType = pTgt->def.wType; // correct endianness of tgt params IAW type of tgt if( wType == CX_XYTARG ) // tgt params exist only for XY & FB targets! { endianSwap( (BYTE*) &(pTgt->def.u.xy.type), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.xy.ndots), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.xy.iDotLfUnits), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.xy.fDotLife), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.xy.fRectW), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.xy.fRectH), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.xy.fInnerW), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.xy.fInnerH), nFloat ); } else if( wType == CX_FBTARG ) { endianSwap( (BYTE*) &(pTgt->def.u.fb.type), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.fb.shape), nInt ); for( i=0; i<3; i++ ) { endianSwap( (BYTE*) &(pTgt->def.u.fb.csMean[i]), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.fb.csCon[i]), nInt ); } endianSwap( (BYTE*) &(pTgt->def.u.fb.fRectW), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.fb.fRectH), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.fb.fSigma), nFloat ); for( i=0; i<2; i++ ) { endianSwap( (BYTE*) &(pTgt->def.u.fb.fGratSF[i]), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.fb.fGratAxis[i]), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.fb.fGratPhase[i]), nFloat ); } } } //=== endianSwapTgtDefV12 ============================================================================================= // // Swaps endianness of the individual members in the CXFILETGT_V12 structure encapsulating definition of a Maestro // target within the data file's CX_TGTRECORD record -- for data files with version numbers in [8..12]. // // ARGS: pTgt -- [in/out] The target definition structure. Endianness of atomic members converted in place. // void endianSwapTgtDefV12( CXFILETGT_V12* pTgt ) { int i, nInt, nFloat; WORD wType; nInt = sizeof(int); nFloat = sizeof(float); endianSwap( (BYTE*) &(pTgt->dwState), sizeof(DWORD) ); endianSwap( (BYTE*) &(pTgt->fPosX), nFloat ); endianSwap( (BYTE*) &(pTgt->fPosY), nFloat ); endianSwap( (BYTE*) &(pTgt->def.wType), sizeof(WORD) ); wType = pTgt->def.wType; // correct endianness of tgt params IAW type of tgt if( wType == CX_XYTARG ) // tgt params exist only for XY & RMVideo targets! { endianSwap( (BYTE*) &(pTgt->def.u.xy.type), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.xy.ndots), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.xy.iDotLfUnits), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.xy.fDotLife), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.xy.fRectW), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.xy.fRectH), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.xy.fInnerW), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.xy.fInnerH), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.xy.fInnerX), nFloat ); // v>=9. Won't impact processing of v=8 files. endianSwap( (BYTE*) &(pTgt->def.u.xy.fInnerY), nFloat ); } else if( wType == CX_RMVTARG ) { endianSwap( (BYTE*) &(pTgt->def.u.rmv.iType), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.iAperture), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.iFlags), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.fOuterW), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.fOuterH), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.fInnerW), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.fInnerH), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.nDots), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.nDotSize), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.iSeed), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.iPctCoherent), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.iNoiseUpdIntv), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.iNoiseLimit), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.fDotLife), nFloat ); for( i=0; i<2; i++ ) { endianSwap( (BYTE*) &(pTgt->def.u.rmv.iRGBMean[i]), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.iRGBCon[i]), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.fSpatialFreq[i]), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.fDriftAxis[i]), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.fGratPhase[i]), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.fSigma[i]), nFloat ); } } } //=== endianSwapTgtDefV22 ============================================================================================= // // Swaps endianness of the individual members in the CXFILETGT_V22 structure encapsulating definition of a Maestro // target within the data file's CX_TGTRECORD record -- for data files with version numbers in [13..22]. // // ARGS: pTgt -- [in/out] The target definition structure. Endianness of atomic members converted in place. // void endianSwapTgtDefV22( CXFILETGT_V22* pTgt ) { int i, nInt, nFloat; WORD wType; nInt = sizeof(int); nFloat = sizeof(float); endianSwap( (BYTE*) &(pTgt->dwState), sizeof(DWORD) ); endianSwap( (BYTE*) &(pTgt->fPosX), nFloat ); endianSwap( (BYTE*) &(pTgt->fPosY), nFloat ); endianSwap( (BYTE*) &(pTgt->def.wType), sizeof(WORD) ); wType = pTgt->def.wType; // correct endianness of tgt params IAW type of tgt if( wType == CX_XYTARG ) // tgt params exist only for XY & RMVideo targets! { endianSwap( (BYTE*) &(pTgt->def.u.xy.type), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.xy.ndots), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.xy.iDotLfUnits), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.xy.fDotLife), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.xy.fRectW), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.xy.fRectH), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.xy.fInnerW), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.xy.fInnerH), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.xy.fInnerX), nFloat ); // v>=9. Won't impact processing of v=8 files. endianSwap( (BYTE*) &(pTgt->def.u.xy.fInnerY), nFloat ); } else if( wType == CX_RMVTARG ) { endianSwap( (BYTE*) &(pTgt->def.u.rmv.iType), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.iAperture), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.iFlags), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.fOuterW), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.fOuterH), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.fInnerW), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.fInnerH), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.nDots), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.nDotSize), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.iSeed), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.iPctCoherent), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.iNoiseUpdIntv), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.iNoiseLimit), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.fDotLife), nFloat ); for( i=0; i<2; i++ ) { endianSwap( (BYTE*) &(pTgt->def.u.rmv.iRGBMean[i]), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.iRGBCon[i]), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.fSpatialFreq[i]), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.fDriftAxis[i]), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.fGratPhase[i]), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.fSigma[i]), nFloat ); } // !!!!!!!!!! last two members are char[] arrays. They don't need to be processed !!!!!!!!!!!!!!! } } //=== endianSwapTgtDef ================================================================================================ // // Swaps endianness of the individual members in the CXFILETGT structure encapsulating definition of a Maestro // target within the data file's CX_TGTRECORD record -- for data files with version number >= 23. // // ARGS: pTgt -- [in/out] The target definition structure. Endianness of atomic members converted in place. // void endianSwapTgtDef( CXFILETGT* pTgt ) { int i, nInt, nFloat; WORD wType; nInt = sizeof(int); nFloat = sizeof(float); endianSwap( (BYTE*) &(pTgt->dwState), sizeof(DWORD) ); endianSwap( (BYTE*) &(pTgt->fPosX), nFloat ); endianSwap( (BYTE*) &(pTgt->fPosY), nFloat ); endianSwap( (BYTE*) &(pTgt->def.wType), sizeof(WORD) ); wType = pTgt->def.wType; // correct endianness of tgt params IAW type of tgt if( wType == CX_XYTARG ) // tgt params exist only for XY & RMVideo targets! { endianSwap( (BYTE*) &(pTgt->def.u.xy.type), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.xy.ndots), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.xy.iDotLfUnits), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.xy.fDotLife), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.xy.fRectW), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.xy.fRectH), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.xy.fInnerW), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.xy.fInnerH), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.xy.fInnerX), nFloat ); // v>=9. Won't impact processing of v=8 files. endianSwap( (BYTE*) &(pTgt->def.u.xy.fInnerY), nFloat ); } else if( wType == CX_RMVTARG ) { endianSwap( (BYTE*) &(pTgt->def.u.rmv.iType), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.iAperture), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.iFlags), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.fOuterW), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.fOuterH), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.fInnerW), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.fInnerH), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.nDots), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.nDotSize), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.iSeed), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.iPctCoherent), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.iNoiseUpdIntv), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.iNoiseLimit), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.fDotLife), nFloat ); for( i=0; i<2; i++ ) { endianSwap( (BYTE*) &(pTgt->def.u.rmv.iRGBMean[i]), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.iRGBCon[i]), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.fSpatialFreq[i]), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.fDriftAxis[i]), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.fGratPhase[i]), nFloat ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.fSigma[i]), nFloat ); } // strFolder and strFile are char[] arrays. They don't need to be processed !!!!!!!!!!!!!!! endianSwap( (BYTE*) &(pTgt->def.u.rmv.iFlickerOn), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.iFlickerOff), nInt ); endianSwap( (BYTE*) &(pTgt->def.u.rmv.iFlickerDelay), nInt ); } } //=== endianSwapStimDef =============================================================================================== // // Swaps endianness of the individual members in the CXFILESTIM_U union encapsulating definition of a Maestro // stimulus run within the data file's CX_STIMRUNRECORD record. The first CXFILESTIM_U structure in the first // stimulus run record is a header encapsulated by the CXFILESTIMRUNHDR struct; the rest of the stimulus run records // are populated by STIMCHAN structs. // // ARGS: pStimU -- [in/out] A union representing the stimulus run header (CXFILESTIMRUNHDR) or a stimulus // run definition (STIMCHAN). Endianness of atomic members converted in place. // isHdr -- [in] TRUE if first argument is the header. // void endianSwapStimDef( CXFILESTIM_U* pStimU, BOOL isHdr ) { int nInt, nFloat, xySeqType, psgmType; CXFILESTIMRUNHDR* pHdr; STIMCHAN* pStim; nInt = sizeof(int); nFloat = sizeof(float); xySeqType = STIM_ISXYSEQ; // chan type consts changed in vers 7! if( cxData.fileHdr.version < 7 ) ++xySeqType; psgmType = STIM_ISPSGM; if( cxData.fileHdr.version < 7 ) ++psgmType; if( isHdr ) { pHdr = &(pStimU->hdr); endianSwap( (BYTE*) &(pHdr->bRunning), sizeof(BOOL) ); endianSwap( (BYTE*) &(pHdr->iDutyPeriod), nInt ); endianSwap( (BYTE*) &(pHdr->iDutyPulse), nInt ); endianSwap( (BYTE*) &(pHdr->nAutoStop), nInt ); endianSwap( (BYTE*) &(pHdr->fHOffset), nFloat ); endianSwap( (BYTE*) &(pHdr->fVOffset), nFloat ); endianSwap( (BYTE*) &(pHdr->nStimuli), nInt ); endianSwap( (BYTE*) &(pHdr->nXYTgts), nInt ); } else { pStim = &(pStimU->stim); endianSwap( (BYTE*) &(pStim->bOn), sizeof(BOOL) ); endianSwap( (BYTE*) &(pStim->iMarker), nInt ); endianSwap( (BYTE*) &(pStim->iType), nInt ); endianSwap( (BYTE*) &(pStim->iStdMode), nInt ); endianSwap( (BYTE*) &(pStim->tStart), nInt ); if( pStim->iType == xySeqType ) // convert union -- depends on type of stimulus run... { endianSwap( (BYTE*) &(pStim->xy.iOpMode), nInt ); endianSwap( (BYTE*) &(pStim->xy.iRefresh), nInt ); endianSwap( (BYTE*) &(pStim->xy.nSegs), nInt ); endianSwap( (BYTE*) &(pStim->xy.iSegDur), nInt ); endianSwap( (BYTE*) &(pStim->xy.iSeed), nInt ); endianSwap( (BYTE*) &(pStim->xy.nChoices), nInt ); endianSwap( (BYTE*) &(pStim->xy.fAngle), nFloat ); endianSwap( (BYTE*) &(pStim->xy.fVel), nFloat ); endianSwap( (BYTE*) &(pStim->xy.fOffsetV), nFloat ); } else if( pStim->iType == psgmType ) { endianSwap( (BYTE*) &(pStim->sgm.iOpMode), nInt ); endianSwap( (BYTE*) &(pStim->sgm.bExtTrig), sizeof(BOOL) ); endianSwap( (BYTE*) &(pStim->sgm.iAmp1), nInt ); endianSwap( (BYTE*) &(pStim->sgm.iAmp2), nInt ); endianSwap( (BYTE*) &(pStim->sgm.iPW1), nInt ); endianSwap( (BYTE*) &(pStim->sgm.iPW2), nInt ); endianSwap( (BYTE*) &(pStim->sgm.iPulseIntv), nInt ); endianSwap( (BYTE*) &(pStim->sgm.iTrainIntv), nInt ); endianSwap( (BYTE*) &(pStim->sgm.nPulses), nInt ); endianSwap( (BYTE*) &(pStim->sgm.nTrains), nInt ); } else if( pStim->iStdMode == MODE_ISSINE ) { endianSwap( (BYTE*) &(pStim->sine.iPeriod), nInt ); endianSwap( (BYTE*) &(pStim->sine.nCycles), nInt ); endianSwap( (BYTE*) &(pStim->sine.fAmp), nFloat ); endianSwap( (BYTE*) &(pStim->sine.fPhase), nFloat ); endianSwap( (BYTE*) &(pStim->sine.fDirec), nFloat ); } else if( pStim->iStdMode == MODE_ISPULSE ) { endianSwap( (BYTE*) &(pStim->pulse.bBlank), sizeof(BOOL) ); endianSwap( (BYTE*) &(pStim->pulse.iPulseDur), nInt ); endianSwap( (BYTE*) &(pStim->pulse.iRampDur), nInt ); endianSwap( (BYTE*) &(pStim->pulse.fAmp), nFloat ); endianSwap( (BYTE*) &(pStim->pulse.fDirec), nFloat ); } } } //=== readAI ========================================================================================================== // // Read compressed AI data in one CX_AIRECORD or CX_SPIKEWAVERECORD into a pre-allocated internal buffer. If we // must reallocate the internal buffer because it is full, the routine fails if we're unable to do so. // // ARGS: pRec -- [in] ptr to buffer holding a CX_AIRECORD or CX_SPIKEWAVERECORD data file record. // // RETURNS: TRUE if successful, FALSE otherwise. // BOOL readAI( CXFILEREC* pRec ) { char* pcNewBuf; // ptr to reallocated buffer, if needed int iExtra = CX_RECORDBYTES * 20; // if we must realloc, add 20 records' worth int* pnBytes; // ptrs to appropriate internal buffer and its int* pnBufSz; // associated size and current count -- b/c char** ppcBuf; // the task is identical for both record types if( pRec->idTag[0] == CX_AIRECORD ) // set up ptrs IAW record type... { pnBytes = &(cxData.nAIBytes); pnBufSz = &(cxData.nAIBufSz); ppcBuf = &(cxData.pcAIData); } else { pnBytes = &(cxData.nFastBytes); pnBufSz = &(cxData.nFastBufSz); ppcBuf = &(cxData.pcFastData); } if( *pnBytes + CX_RECORDBYTES > *pnBufSz ) // insufficient space. attempt to reallocate { // the internal buffer, aborting on failure. pcNewBuf = (char*) realloc( (void*) *ppcBuf, sizeof(char)*(iExtra + *pnBufSz) ); if( pcNewBuf == NULL ) { printf( "ERROR: Internal buffer reallocation failed!\n" ); return( FALSE ); } *ppcBuf = pcNewBuf; *pnBufSz += iExtra; } memcpy( (*ppcBuf) + (*pnBytes), pRec->u.byteData, CX_RECORDBYTES ); // copy compressed AI data from record into *pnBytes += CX_RECORDBYTES; // internal buf & update buffer count. return( TRUE ); } //=== uncompressAIData ================================================================================================ // // Uncompress a CNTRLX analog input byte stream sampling the specified number N of AI channels. When more than one // channel is recorded, the data are stored in the output buffer as [ch1(0), ..., chN(0), ch1(1), ..., ch1(N), ...]. // Uncompressed data is in the range of a 12bit analog-to-digital converter: [-2048..2047]. // // Compression algorithm: Each compressed sample represents the DIFFERENCE from the previous sample. If this // difference is in [-63..63], it is encoded as a single byte in [0x01..0x7F]. Observe that bit 7 is NOT set. To // get back the sample, subtract 64 from the encoded byte, then add the result to the value of the last sample on // the current channel. If the difference is in [-2048..-64, 64..2047], it is encoded as two bytes in the range // [0x8800..0x8FC0, 0x9040..0x97FF], with the high byte first. In this case bit 7 is set in the high byte -- that's // how we distinguish between a one-byte and two-byte compressed datum. To uncompress the two-byte datum, we pack // the two bytes into a 16bit int, clear bit 15, and subtract 4096 to recover the difference, which is then added // to the value of the last sample to get the current sample value. OBSERVE that, if a given sample is compressed // as one byte, it will never have the value 0x00; for a 2-byte compressed sample, the high byte is never 0x00. In // fact, CNTRLX uses the zero byte to mark the end of the compressed data stream. We stop as soon as we reach this // end-of-stream marker -- thus we will know exactly how many bytes were compressed and how many scans of real data // were saved. // // 20jan2004: BUG FIX. When the # of compressed bytes is an integer multiple of CX_RECORDBYTES, there is no "end of // data" marker (the zero byte) in the compressed data buffer. Prior to this fix, this method would abort // before incrementing the scan counter for the last scan's worth of data. As a result, readcxdata() warned that // there was a mismatch between the #scans reported in the header and the actual number of scans read from file. // // 08jul2004: Cntrlx uses the byte value 0xFF rather than 0 as the "endOfData" mark in Continuous mode files, while // it uses 0 for Trial mode files. Maestro uses 0 as the "endOfData" mark always. To decompress Cntrlx-generated // Continuous mode files properly, we check for the presence of either of these markers. The compression algorithm // guarantees that 0xFF will never appear as the value of a 1-byte compressed sample, nor as the first byte of a // 2-byte compressed sample. // // ARGS: pDst -- [out] pre-allocated buffer to hold the uncompressed data stream in "channel-scan order". // iDstSz -- [in] total # of samples that can be stored in uncompressed data buffer. // pSrc -- [in] compressed data stream buffer. // iSrcSz -- [in] size of compressed data buffer. // nCh -- [in] # of AI channels that were recorded. // pNC -- [out] total # of compressed bytes found (zero byte marks end of stream!) // pNScans -- [out] total # of complete scans found in uncompressed data stream. The total # of // samples is this # times the # of channels recorded. // // RETURNS: NONE. // void uncompressAIData( double* pDst, int iDstSz, char* pSrc, int iSrcSz, int nCh, int* pNC, int* pNScans ) { int i; int iLastSample[CXH_MAXAI]; char cByte; short shTemp; int nSrc; int nScans; memset( iLastSample, 0, CXH_MAXAI*sizeof(int) ); // all channels read 0 at t = 0! nScans = 0; // # of complete channel scans found nSrc = 0; // # of compressed bytes processed while( nSrc < iSrcSz ) // uncompress the data stream { if( (nScans+1)*nCh > iDstSz ) // oops, not enough room left in output buf { // for the next scan's worth of samples *pNC = nSrc; *pNScans = nScans; return; } for( i = 0; i < nCh; i++ ) // do one channel scan's worth at a time { if( nSrc == iSrcSz || pSrc[nSrc] == 0 || pSrc[nSrc] == -1 ) // oops, we've either hit end of input { // buffer or got "endOfData" mark *pNC = nSrc; *pNScans = nScans; return; } cByte = pSrc[nSrc++]; // read in the next byte if( cByte & 0x080 ) // if bit7 set, next datum is 2 bytes { if( nSrc == iSrcSz ) // should NEVER happen, but just in case... { *pNC = nSrc; *pNScans = nScans; return; } shTemp = (cByte & 0x7F); shTemp <<= 8; shTemp |= 0x00FF & ((short) pSrc[nSrc++]); shTemp -= 4096; iLastSample[i] += shTemp; // datum is difference from last sample! } else // if bit 7 clear, next datum is 1 byte { shTemp = cByte - 64; iLastSample[i] += shTemp; } pDst[nScans*nCh+i] = (double)iLastSample[i]; // save uncompressed sample in output buf } ++nScans; } *pNC = nSrc; *pNScans = nScans; } //=== readEvents ====================================================================================================== // // Read digital event data from a CX_EVENT0RECORD or CX_EVENT1RECORD data file record into the appropriate internal // buffer. Reallocate buffer as needed; abort if reallocation fails. // // In both these records, the "recorded" data is the interevent intervals between successive events (ie, occurence // of a rising edge) on DI channels 0 and 1. The first interval is the absolute time of occurrence of the first // event (where T=0 is when recording started). All interval times are in 10us units. Here we convert all event // intervals back to absolute occurrence times, and convert all times to milliseconds. // // Each record may contain up to CX_RECORDINTS interval times in CXFILEREC.u.iData[]. The last DI event record of // either type may not be full; in such a case, the remaining "garbage" times are set to the "end of data" marker // EOD_EVENTRECORD, defined in CXFILEFMT.H. All such garbage data are ignored. // // ARGS: pRec -- [in] ptr to buffer holding one of the digital event data file records. File record format // is encapsulated by the CXFILEREC structure (see CXFILEFMT.H). // // RETURNS: TRUE if successful, FALSE otherwise. // BOOL readEvents( CXFILEREC* pRec ) { int i, iTime, nInt; double* pdNewBuf; // ptr to reallocated buffer, if needed int iExtra = CX_RECORDINTS * 20; // if we must realloc, add 20 records' worth // since the task is the same for both record types, we use ptrs to the corresponding internal buffer, its size, the // current event count, and the last event time in # of 10us ticks. This last variable is needed to handle // processing over a record boundary. int* pnEvents; int* pnBufSz; double** ppdBuf; long* ptLastEvent; nInt = sizeof(int); // in case we have to convert endianness if( pRec->idTag[0] == CX_EVENT0RECORD ) // set up ptrs IAW record type. CX_EVENT0RECORD { // is reserved for recording spikes times pnEvents = &(cxData.nSpikes); pnBufSz = &(cxData.nSpikesBufSz); ppdBuf = &(cxData.pdSpikes); ptLastEvent = &(cxData.tLastSpike); } else // CX_EVENT1RECORD may be used to record a 2nd { // spike train or marker pulses pnEvents = &(cxData.nEvents); pnBufSz = &(cxData.nEventsBufSz); ppdBuf = &(cxData.pdEvents); ptLastEvent = &(cxData.tLastEvent); } if( *pnEvents + CX_RECORDINTS > *pnBufSz ) // insufficient space. attempt to reallocate { // the internal buffer, aborting on failure. pdNewBuf = (double*) realloc( (void*) *ppdBuf, sizeof(double)*(iExtra + *pnBufSz) ); if( pdNewBuf == NULL ) { printf( "ERROR: Internal buffer reallocation failed!\n" ); return( FALSE ); } *ppdBuf = pdNewBuf; *pnBufSz += iExtra; } for( i = 0; i < CX_RECORDINTS; i++ ) // copy data to internal buffer: { iTime = pRec->u.iData[i]; if( isBigEndian ) endianSwap( (BYTE*) &iTime, nInt ); // convert endianness if necessary if( iTime == EOD_EVENTRECORD ) break; // stop! reached "end-of-data" marker. // add interval time to get current elapsed time in # of 10us ticks. Then convert to msecs. *ptLastEvent += iTime; (*ppdBuf)[*pnEvents] = ((double) *ptLastEvent) / 100.0; ++(*pnEvents); } return( TRUE ); } //=== readOthers ====================================================================================================== // // Read digital event data for DI channels <2..15> from a CX_OTHEREVENTRECORD data file record into the appropriate // internal buffer. Reallocate buffer as needed; abort if reallocation fails. // // Unlike the DI<0> and DI<1> event records, the "recorded" data is a series of (event mask, event time)-pairs, // since it is possible that pulses occur simultaneously (ie, w/in a single 10us epoch) on more than one DI channel. // The event time in 10us is NOT an interevent interval, but the absolute event time relative to the start of // recording. Here convert all event times to milliseconds, AND separate out any simultaneous events into // individual (event ch#, event time)-pairs, which is what gets stored in the internal buffer. // // Each CX_OTHEREVENTRECORD record may contain up to CX_RECORDINTS/2 (event mask, event time)-pairs. The last such // record in the file may not be full; in such a case, the remaining pairs are set to (0, EOD_EVENTRECORD). All // such garbage data are ignored. // // If the data file version >= 20, the CX_OTHEREVENTRECORD may also contain "blink start" and "blink end" events // that Maestro generates when it uses the Eyelink 1000+ eye tracker and detects blink epochs in the Eyelink data // stream. The CX_EL_BLINKSTARTMASK and CX_EL_BLINKENDMASK event masks do not overlap with the digital input event // masks, and the blink event times are in milliseconds rather than 10-us ticks. It is assumed that the blink events // occur in (start, end) pairs, and that is how the event times are stored in the internal buffer. However, if the // first blink event is "blink end", then a "blink start" is added at T=0, the assumption being that the subject was // mid-blink when recording started. // // ARGS: pRec -- [in] ptr to buffer holding a CX_OTHEREVENTRECORD. File record format is encapsulated by // the CXFILEREC structure (see CXFILEFMT.H). // // RETURNS: TRUE if successful, FALSE otherwise. // BOOL readOthers( CXFILEREC* pRec ) { int i, j, iTime, iMask, nInt; double* pdNewBuf; // ptr to reallocated buffer, if needed int iExtra = CX_RECORDINTS * 20; // if we must realloc, add 20 records' worth // don't check for Eyelink blink events unless Eyelink tracker was used int iBlinkMask = 0; if((cxData.fileHdr.version >= 20) && ((cxData.fileHdr.flags & CXHF_EYELINKUSED) != 0)) iBlinkMask = (CX_EL_BLINKSTARTMASK | CX_EL_BLINKENDMASK); nInt = sizeof(int); // in case we have to convert endianness for( i = 0; i < CX_RECORDINTS; i+=2 ) // for each (event mask, time)-pair in record: { iMask = pRec->u.iData[i]; // event mask (bitN set = event on DI) iTime = pRec->u.iData[i+1]; // event time in 10us units if( isBigEndian ) // convert endianness if necessary { endianSwap( (BYTE*) &iMask, nInt ); endianSwap( (BYTE*) &iTime, nInt ); } if( iTime == EOD_EVENTRECORD ) break; // stop! reached "end-of-data" marker. if((iBlinkMask & iMask) != 0) { // Eyelink blink epochs: Add event time (already in ms) to internal buffer, assuming they're listed in // chronological order in start,end pairs. Exception: If first blink event is "blink end", first add an // event time of 0 -- under the assumption that the subject was mid-blink when recording started. // allocate initially or reallocate internal buffer (it is not pre-allocated) if(cxData.nBlinkEvts == cxData.nBlinksBufSz) { if(cxData.nBlinksBufSz == 0) pdNewBuf = (double*) malloc(sizeof(double) * 100); else pdNewBuf = (double*) realloc((void*) cxData.pdBlinks, sizeof(double)*(100 + cxData.nBlinksBufSz)); if(pdNewBuf == NULL) { printf("ERROR: Internal buffer reallocation failed!\n"); return(FALSE); } cxData.pdBlinks = pdNewBuf; cxData.nBlinksBufSz += 100; } if(cxData.nBlinkEvts == 0 && (iMask & CX_EL_BLINKSTARTMASK) == 0) cxData.pdBlinks[cxData.nBlinkEvts++] = 0.0; cxData.pdBlinks[cxData.nBlinkEvts++] = (double) iTime; } else { // digital input events: create a separate (ch#, time)-pair for each event (could be simultaneous ones!). // Reallocate internal buffer when necessary. Abort if reallocation fails. for( j = 2; j < 16; j++ ) if( (iMask & (int)(1< cxData.nOthersBufSz ) { pdNewBuf = (double*) realloc( (void*) cxData.pdOthers, sizeof(double)*(iExtra + cxData.nOthersBufSz) ); if( pdNewBuf == NULL ) { printf( "ERROR: Internal buffer reallocation failed!\n" ); return( FALSE ); } cxData.pdOthers = pdNewBuf; cxData.nOthersBufSz += iExtra; } cxData.pdOthers[cxData.nOthers++] = (double) j; // put (ch#, time)-pair into internal buf, cxData.pdOthers[cxData.nOthers++] = ((double)iTime) / 100.0; // converting time from 10us ticks to msec. } } } return( TRUE ); } //=== readSortedSpikes ================================================================================================ // // Read events from a "sorted spike train channel" record into the appropriate internal buffer. Reallocate each // buffer as needed; abort if reallocation fails. // // Analysis programs like XWork and JMWork can perform "spike sorting" on the high-resolution waveform recorded by // Maestro (at 25KHz) or the older Cntrlx partner app "spikesPC" (at 50KHz). The resulting "sorted-spike trains" are // represented by a chronological sequence of spike arrival times and are appended to the original Maestro data file // in records with tags CX_SPIKESORTREC_FIRST..CX_SPIKESORTREC_LAST. Thus, as many as // // NUMSPIKESORTCH = CX_SPIKESORTREC_LAST - CX_SPIKESORTREC_FIRST + 1 // // distinct trains can be stored in the file. This used to be 13; as of 06sep13, it is 50. This increase in capacity // makes it possible to store all of the "units" recorded on several electrodes using the Plexon system during a // Maestro experiment. Plexon spike trains are added by custom analysis code, typically using EDITCXDATA. // // In all such records, the "recorded" data are the interevent intervals between successive events (ie, spike // arrivals as determined by the sorting algorithm applied). The first interval is the absolute time of occurrence // of the spike (where T=0 is when recording started). All interval times are in 10us units. Here we convert all // event intervals back to absolute occurrence times, and convert all times to milliseconds. // // Each sorted spike train channel record may contain up to CX_RECORDINTS interval times in CXFILEREC.u.iData[]. // The last channel record may not be full; in such a case, the remaining "garbage" interevent intervals are set to // the "end of data" marker EOD_EVENTRECORD, defined in CXFILEFMT.H. All such garbage data are ignored. // // Each sorted spike train channel's sequence of spike arrival times are stored in a separate internal buffer. The // internal buffers are lazily allocated and reallocated as needed, since most Maestro data files will not contain // this kind of information. // // ARGS: pRec -- [in] ptr to buffer holding one of the sorted spike train channel records. File record // format is encapsulated by the CXFILEREC structure (see CXFILEFMT.H). // // RETURNS: TRUE if successful, FALSE otherwise. // BOOL readSortedSpikes( CXFILEREC* pRec ) { int i, iTime, nInt; double* pdNewBuf; // ptr to reallocated buffer, if needed int iExtra = CX_RECORDINTS * 2; // if we must realloc, add 2 records' worth int iSortCh; // the "sorted spike train" channel # // since the task is the same for each sorted-spike channel, we use ptrs to the corresponding internal buffer, its // size, the current event count, and the last spike arrival time in # of 10us ticks. This last variable is needed // to handle processing over a record boundary. int* pnEvents; int* pnBufSz; double** ppdBuf; long* ptLastEvent; nInt = sizeof(int); // in case we have to convert endianness iSortCh = (int) (pRec->idTag[0] - CX_SPIKESORTREC_FIRST); // set up ptrs for the channel we're looking at pnEvents = &(cxData.nSortedSpikes[iSortCh]); pnBufSz = &(cxData.nSortedBufSz[iSortCh]); ppdBuf = &(cxData.pdSortedSpikes[iSortCh]); ptLastEvent = &(cxData.tLastSortedSpike[iSortCh]); if( *pnBufSz == 0 ) // this is the first record for this channel, { // so we need to allocate the internal buffer *ppdBuf = (double *) malloc( sizeof(double) * iExtra ); if( *ppdBuf == NULL ) { printf( "ERROR: Sorted spikes buffer allocation failed!\n" ); return( FALSE ); } else *pnBufSz = iExtra; } else if( *pnEvents + CX_RECORDINTS > *pnBufSz ) // internal buffer too small. attempt to { // reallocate it, aborting on failure. pdNewBuf = (double*) realloc( (void*) *ppdBuf, sizeof(double)*(iExtra + *pnBufSz) ); if( pdNewBuf == NULL ) { printf( "ERROR: Internal buffer reallocation failed!\n" ); return( FALSE ); } *ppdBuf = pdNewBuf; *pnBufSz += iExtra; } for( i = 0; i < CX_RECORDINTS; i++ ) // copy data to internal buffer: { iTime = pRec->u.iData[i]; if( isBigEndian ) endianSwap( (BYTE*) &iTime, nInt ); // convert endianness if necessary if( iTime == EOD_EVENTRECORD ) break; // stop! reached "end-of-data" marker. // add interval time to get current elapsed time in # of 10us ticks. Then convert to msecs. *ptLastEvent += iTime; (*ppdBuf)[*pnEvents] = ((double) *ptLastEvent) / 100.0; ++(*pnEvents); } return( TRUE ); } //=== readTrialCodes ================================================================================================== // // Read trial codes from a CX_TRIALCODERECORD data file record into the appropriate internal buffer. Reallocate // buffer as needed; abort if reallocation fails. // // Each record may contain up to CX_RECORDCODES trial codes. // // ARGS: pRec -- [in] ptr to buffer holding the CX_TRIALCODERECORD record. File record fmt encapsulated by // the CXFILEREC structure (see CXFILEFMT.H). // // RETURNS: TRUE if successful, FALSE otherwise. // BOOL readTrialCodes( CXFILEREC* pRec ) { int i, nShort; TRIALCODE* pNewTCBuf; // ptr to reallocated buffer, if needed int iExtra = CX_RECORDCODES; // if we must realloc, add 1 record's worth nShort = sizeof(short); // in case we have to convert endianness if( cxData.nCodes + CX_RECORDCODES > cxData.nCodesBufSz ) // insufficient space; reallocate internal buf. { // abort if reallocation fails. pNewTCBuf = (TRIALCODE*) realloc( (void*) cxData.pCodes, sizeof(TRIALCODE)*(iExtra + cxData.nCodesBufSz) ); if( pNewTCBuf == NULL ) { printf( "ERROR: Internal buffer reallocation failed!\n" ); return( FALSE ); } cxData.pCodes = pNewTCBuf; cxData.nCodesBufSz += iExtra; } for( i = 0; i < CX_RECORDCODES; i++ ) // copy codes in record to internal buf as is { if( isBigEndian ) // convert endianness if necessary { endianSwap( (BYTE*) &(pRec->u.tc[i].code), nShort ); endianSwap( (BYTE*) &(pRec->u.tc[i].time), nShort ); } cxData.pCodes[cxData.nCodes++] = pRec->u.tc[i]; } return( TRUE ); } //=== readEdits ======================================================================================================= // // Read integer-valued data from a CX_XWORKACTIONREC data file record into the appropriate internal buffer. Realloc // buffer as needed; abort if reallocation fails. The data is merely copied as is. // // ARGS: pRec -- [in] ptr to buffer holding the CX_XWORKACTIONREC record. File record fmt encapsulated by // the CXFILEREC structure (see CXFILEFMT.H). // // RETURNS: TRUE if successful, FALSE otherwise. // BOOL readEdits( CXFILEREC* pRec ) { int i, nInt; int* piNewBuf; // ptr to reallocated buffer, if needed int iExtra = CX_RECORDINTS * 2; // if we must realloc, add 2 records' worth if( cxData.nEdits + CX_RECORDINTS > cxData.nEditsBufSz ) // insufficient space; reallocate internal buf. { // abort if reallocation fails. piNewBuf = (int*) realloc( (void*) cxData.piEdits, sizeof(int)*(iExtra + cxData.nEditsBufSz) ); if( piNewBuf == NULL ) { printf( "ERROR: Internal buffer reallocation failed!\n" ); return( FALSE ); } cxData.piEdits = piNewBuf; cxData.nEditsBufSz += iExtra; } if( isBigEndian ) // convert endianness if necessary { nInt = sizeof(int); for( i=0; iu.iData[i]), nInt ); } memcpy( (void*) (cxData.piEdits + cxData.nEdits), // copy data in record to internal buf as is (void*) &(pRec->u.iData[0]), CX_RECORDINTS*sizeof(int) ); cxData.nEdits += CX_RECORDINTS; return( TRUE ); } //=== readTargets ===================================================================================================== // // Read CNTRLX target definitions from a CX_TGTRECORD data file record into the appropriate internal buffers. Fail // if there's no more room for target definitions -- this should never happen! // // We do NOT provide support for reading in target definition records (CX_V1TGTRECORD) from data files having // vers# < 2. The tedious translation work did not seem worth the effort. // // The format in which a single target definition is stored, encapsulated by CXFILETGT, changed as of data file // version 8 (Maestro 2.0.0), again as of version 13 (Maestro 2.5.0), and yet again as of version 23 (Maestro // 4.1.0). This method handles both the current and the deprecated (CXFILETGT_V7, CXFILETGT_V12, CXFILETGT_V22) // formats. // // ARGS: pRec -- [in] ptr to buffer holding the CX_TGTRECORD record. File fmt encapsulated by the CXFILEREC // structure (see CXFILEFMT.H). // // RETURNS: TRUE if successful, FALSE otherwise. // BOOL readTargets( CXFILEREC* pRec ) { int i; CXFILETGT* pTgt; // for data file versions 23+ CXFILETGT_V22* pTgtV22; // for data file versions [13..22] CXFILETGT_V12* pTgtV12; // for data file versions [8..12] CXFILETGT_V7* pTgtV7; // for data file versions [2..7] if( cxData.fileHdr.version < 2 ) // just a consistency check { printf( "ERROR: Data file version inconsistent!\n%d\n",cxData.fileHdr.version ); return( FALSE ); } if(cxData.fileHdr.version >= 23) for(i=0; iu.tgts[i]); if( isBigEndian ) endianSwapTgtDef( pTgt ); // convert endianness if necessary if( pTgt->def.wType == 0 ) break; // "empty" tgt defn; rest of rec is junk! memcpy( (void*) &(cxData.pTargets[cxData.nTargets]), (void*) pTgt, sizeof(CXFILETGT) ); ++(cxData.nTargets); } else if(cxData.fileHdr.version >= 13) for(i=0; iu.tgtsV22[i]); if( isBigEndian ) endianSwapTgtDefV22( pTgtV22 ); if( pTgtV22->def.wType == 0 ) break; memcpy( (void*) &(cxData.pTargets_V22[cxData.nTargets]), (void*) pTgtV22, sizeof(CXFILETGT_V22) ); ++(cxData.nTargets); } else if(cxData.fileHdr.version >= 8) for(i=0; iu.tgtsV12[i]); if( isBigEndian ) endianSwapTgtDefV12( pTgtV12 ); if( pTgtV12->def.wType == 0 ) break; memcpy( (void*) &(cxData.pTargets_V12[cxData.nTargets]), (void*) pTgtV12, sizeof(CXFILETGT_V12) ); ++(cxData.nTargets); } else for(i=0; iu.tgtsV7[i]); if( isBigEndian ) endianSwapTgtDefV7( pTgtV7 ); if( pTgtV7->def.wType == 0 ) break; memcpy( (void*) &(cxData.pTargets_V7[cxData.nTargets]), (void*) pTgtV7, sizeof(CXFILETGT_V7) ); ++(cxData.nTargets); } return( TRUE ); } //=== readStims ======================================================================================================= // // Read contents of a CX_STIMRUNRECORD data file record into the appropriate internal buffer. Fail if we find more // stimulus channel definitions than we've allocated for -- which should never happen! // // The CX_STIMRUNRECORD record was introduced with CNTRLX data file version 2. One (or at most two) of these // records may appear in ContMode data files to summarize the defn of the stimulus run that was in memory (whether // or not it was actually used) WHEN DATA RECORDING BEGAN. Each record contains up to CX_RECORDSTIMS objects // defined by the CXFILESTIM_U structure. We merely copy all of the objects into the internal buffer, even though // some of them may be dummy objects filling the last partially full CX_STIMRUNRECORD in the file. // // ARGS: pRec -- [in] ptr to buffer holding the CX_STIMRUNRECORD record. File fmt encapsulated by the // CXFILEREC structure (see CXFILEFMT.H). // // RETURNS: TRUE if successful, FALSE otherwise. // BOOL readStims( CXFILEREC* pRec ) { int i; if( cxData.fileHdr.version < 2 ) // just a consistency check { printf( "ERROR: Data file version inconsistent!\n%d\n",cxData.fileHdr.version ); return( FALSE ); } if( cxData.nStims == cxData.nStimsBufSz ) // internal buffer is already full and we've { // encountered another record -- this should printf( "ERROR: No more room for stimulus run info!\n" ); // never happen! return( FALSE ); } for( i = 0; i < CX_RECORDSTIMS; i++ ) // copy stim run info into buffer: { if( isBigEndian ) // convert endianness if necessary endianSwapStimDef( &(pRec->u.stims[i]), (cxData.nStims == 0) ); memcpy( (void*) &(cxData.pStimBuf[cxData.nStims]), (void*) &(pRec->u.stims[i]), sizeof(CXFILESTIM_U) ); ++(cxData.nStims); if( cxData.nStims == cxData.nStimsBufSz ) break; // stop! internal buf now full. anything // else in record should be junk anyway. } return( TRUE ); } //=== readTagSections ================================================================================================= // // Read contents of a CX_TAGSECTRECORD data file record into the appropriate internal buffer. // // The CX_TAGSECTRECORD record was introduced with Maestro data file version 4. It contains up to CX_RECORDSECTS // instances of the TRIALSECT structure, which defines a tagged section within a Maestro trial. We merely copy all // of the non-empty TRIALSECT structures into the the internal buffer. When the character string field // TRIALSECT.tag is zero-length, we've reached the end of the list of defined sections. // // If we've already processed a CX_TAGSECTRECORD (ie, there are already some tagged sections stored in the internal // buffer), the function fails -- since a Maestro data file will contain at most ONE of these records. // // ARGS: pRec -- [in] ptr to buffer holding the CX_TAGSECTRECORD record. File fmt encapsulated by the // CXFILEREC structure (see CXFILEFMT.H). // // RETURNS: TRUE if successful, FALSE otherwise. // BOOL readTagSections( CXFILEREC* pRec ) { int i; if( cxData.nSections > 0 ) // at most one CX_TAGSECTRECORD in a file! { printf( "ERROR: Data file contains more than one tagged section record?!\n" ); return( FALSE ); } for( i = 0; i < CX_RECORDSECTS; i++ ) // copy tagged section defns into buffer { if( strlen( pRec->u.sects[i].tag ) == 0 ) break; memcpy( (void*) &(cxData.sections[cxData.nSections]), (void*) &(pRec->u.sects[i]), sizeof(TRIALSECT) ); ++(cxData.nSections); if( cxData.nSections == MAX_SEGMENTS ) break; // internal buf full; this is the max# of // tagged sections anyway! } return( TRUE ); } //=== createUInt32Scalar, createInt32Scalar =========================================================================== // // Convenience functions for filling in integer-valued scalar fields in a MATLAB structure array. // // ARGS: dwValue, iValue -- [in] the integer scalar. // // RETURNS: ptr to the numeric scalar array, initialized to the specified value. NULL if unable to alloc. // mxArray* createUInt32Scalar( DWORD dwValue ) { mxArray* pMxArr = mxCreateNumericMatrix( 1, 1, mxUINT32_CLASS, mxREAL ); if( pMxArr != NULL ) *((DWORD*) mxGetData( pMxArr )) = dwValue; return( pMxArr ); } mxArray* createInt32Scalar( int iValue ) { mxArray* pMxArr = mxCreateNumericMatrix( 1, 1, mxINT32_CLASS, mxREAL ); if( pMxArr != NULL ) *((int*) mxGetData( pMxArr )) = iValue; return( pMxArr ); } //=== setSortedSpikesOutput =========================================================================================== // // This method prepares the "sortedSpikes" field in the output structure created by readcxdata(). This field // reports any "sorted spike train channel" data culled from the data file; such data is appended to the original // Maestro data file by analysis programs like XWork and JMWork. At a minimum, it initializes the field to a // 1xNUMSPIKESORTCH MATLAB cell array, where all the cells are empty matrices. Then, for each sorted spike train // channel for which data was found in the file, the corresponding cell is set to a double array representing the // sequence of spike arrival times in the sorted spike train; all times are in milliseconds relative to the start of // recording. The spike train reported will include the effects of any manual "spike-edit" actions -- a feature // introduced in JMWork. // // Since spike-sorting is rarely done, the "sortedSpikes" field will typically be a cell array of empty matrices. // When spike-sorting is done, it's usually done on just one or a few channels. Only those cells corresponding to // the selected channels will have non-empty matrices in them. // // ARGS: pOut -- [in/out] ptr to the MATLAB structure array prepared by readcxdata(). // // RETURNS: NONE. // void setSortedSpikesOutput( mxArray* pOut ) { int i; mxArray* sortCells; mxArray* train; sortCells = mxCreateCellMatrix( 1, NUMSPIKESORTCH ); // create the cell array for( i=0; i 0 ) // train to a double array and store it in the { // appropriate cell of the cell array. train = mxCreateDoubleMatrix( 1, cxData.nSortedSpikes[i], mxREAL ); memcpy( (void*) mxGetPr( train ), (void*) (cxData.pdSortedSpikes[i]), sizeof(double) * cxData.nSortedSpikes[i] ); mxSetCell( sortCells, i, train ); } if( cxData.pdSortedSpikes[i] != NULL ) // also deallocate corres internal buffer, if { // one exists free( cxData.pdSortedSpikes[i] ); cxData.pdSortedSpikes[i] = NULL; cxData.nSortedBufSz[i] = 0; cxData.nSortedSpikes[i] = 0; } } mxSetField( pOut, 0, "sortedSpikes", sortCells ); // finally, store cell array in output field } //=== setHeaderOutput ================================================================================================= // // Copies fields from the data file header into corresponding fields in the "key" member of the output structure // created by readcxdata(). // // ARGS: pOut -- [in/out] ptr to the MATLAB structure array prepared by readcxdata(). // // RETURNS: NONE. // void setHeaderOutput( mxArray* pOut ) { int* piData; int i; CXFILEHDR* pHdr; mxArray* pMXHdr; double rate; pHdr = &(cxData.fileHdr); mxSetField( pOut, 0, "key", // create MATLAB struct to hold file hdr info mxCreateStructMatrix( 1, 1, NUMHDRFIELDS, headerFields ) ); pMXHdr = mxGetField( pOut, 0, "key" ); mxSetField( pMXHdr, 0, "trhdir", createInt32Scalar( pHdr->trhdir ) ); mxSetField( pMXHdr, 0, "trvdir", createInt32Scalar( pHdr->trvdir ) ); mxSetField( pMXHdr, 0, "nchar", createInt32Scalar( pHdr->nchar ) ); mxSetField( pMXHdr, 0, "npdig", createInt32Scalar( pHdr->npdig ) ); mxSetField( pMXHdr, 0, "nchans", createInt32Scalar( pHdr->nchans ) ); mxSetField( pMXHdr, 0, "chlist", mxCreateNumericMatrix( 1, CXH_MAXAI, mxINT32_CLASS, mxREAL ) ); piData = (int*) mxGetData( mxGetField( pMXHdr, 0, "chlist" ) ); for(i = 0; i < CXH_MAXAI; i++) piData[i] = (int) pHdr->chlist[i]; mxSetField( pMXHdr, 0, "d_rows", createInt32Scalar( pHdr->d_rows ) ); mxSetField( pMXHdr, 0, "d_cols", createInt32Scalar( pHdr->d_cols ) ); mxSetField( pMXHdr, 0, "d_crow", createInt32Scalar( pHdr->d_crow ) ); mxSetField( pMXHdr, 0, "d_ccol", createInt32Scalar( pHdr->d_ccol ) ); mxSetField( pMXHdr, 0, "d_dist", createInt32Scalar( pHdr->d_dist ) ); mxSetField( pMXHdr, 0, "d_dwidth", createInt32Scalar( pHdr->d_dwidth ) ); mxSetField( pMXHdr, 0, "d_dheight", createInt32Scalar( pHdr->d_dheight ) ); // for V<22, CXFILEHDR.d_framerate units are milli-Hz; for V>=22, micro-Hz. To avoid changing the units in // the output structure, we divide by 1000 to convert from micro-Hz to milli-Hz, storing the value as a // floating-point scalar so that the extra precision is not lost rate = (double) pHdr->d_framerate; if(pHdr->version >= 22) rate /= 1000.0; mxSetField( pMXHdr, 0, "d_framerate", mxCreateDoubleScalar(rate) ); mxSetField( pMXHdr, 0, "iPosScale", createInt32Scalar( pHdr->iPosScale ) ); mxSetField( pMXHdr, 0, "iPosTheta", createInt32Scalar( pHdr->iPosTheta ) ); mxSetField( pMXHdr, 0, "iVelScale", createInt32Scalar( pHdr->iVelScale ) ); mxSetField( pMXHdr, 0, "iVelTheta", createInt32Scalar( pHdr->iVelTheta ) ); mxSetField( pMXHdr, 0, "iRewLen1", createInt32Scalar( pHdr->iRewLen1 ) ); mxSetField( pMXHdr, 0, "iRewLen2", createInt32Scalar( pHdr->iRewLen2 ) ); mxSetField( pMXHdr, 0, "dayRecorded", createInt32Scalar( pHdr->dayRecorded ) ); mxSetField( pMXHdr, 0, "monthRecorded", createInt32Scalar( pHdr->monthRecorded ) ); mxSetField( pMXHdr, 0, "yearRecorded", createInt32Scalar( pHdr->yearRecorded ) ); mxSetField( pMXHdr, 0, "version", createInt32Scalar( pHdr->version ) ); mxSetField( pMXHdr, 0, "flags", createUInt32Scalar( pHdr->flags ) ); mxSetField( pMXHdr, 0, "nScanIntvUS", createInt32Scalar( pHdr->nScanIntvUS ) ); mxSetField( pMXHdr, 0, "nBytesCompressed", createInt32Scalar( pHdr->nBytesCompressed ) ); mxSetField( pMXHdr, 0, "nScansSaved", createInt32Scalar( pHdr->nScansSaved ) ); mxSetField( pMXHdr, 0, "spikesFName", mxCreateString( pHdr->spikesFName ) ); mxSetField( pMXHdr, 0, "nSpikeBytesCompressed", createInt32Scalar( pHdr->nSpikeBytesCompressed ) ); mxSetField( pMXHdr, 0, "nSpikeSampIntvUS", createInt32Scalar( pHdr->nSpikeSampIntvUS ) ); mxSetField( pMXHdr, 0, "dwXYSeed", createUInt32Scalar( pHdr->dwXYSeed ) ); mxSetField( pMXHdr, 0, "iRPDStart", createInt32Scalar( pHdr->iRPDStart ) ); mxSetField( pMXHdr, 0, "iRPDDur", createInt32Scalar( pHdr->iRPDDur ) ); mxSetField( pMXHdr, 0, "iRPDResponse", createInt32Scalar( pHdr->iRPDResponse ) ); mxSetField( pMXHdr, 0, "iRPDWindows", mxCreateNumericMatrix( 1, 4, mxINT32_CLASS, mxREAL ) ); piData = (int*) mxGetData( mxGetField( pMXHdr, 0, "iRPDWindows" ) ); for(i = 0; i < 4; i++) piData[i] = (int) pHdr->iRPDWindows[i]; mxSetField( pMXHdr, 0, "iRPDRespType", createInt32Scalar( pHdr->iRPDRespType ) ); mxSetField( pMXHdr, 0, "iStartPosH", createInt32Scalar( pHdr->iStartPosH ) ); mxSetField( pMXHdr, 0, "iStartPosV", createInt32Scalar( pHdr->iStartPosV ) ); mxSetField( pMXHdr, 0, "dwTrialFlags", createUInt32Scalar( pHdr->dwTrialFlags ) ); mxSetField( pMXHdr, 0, "iSTSelected", createInt32Scalar( pHdr->iSTSelected ) ); mxSetField( pMXHdr, 0, "iVStabWinLen", createInt32Scalar( pHdr->iVStabWinLen ) ); mxSetField( pMXHdr, 0, "iELInfo", mxCreateNumericMatrix( 1, 9, mxINT32_CLASS, mxREAL ) ); piData = (int*) mxGetData( mxGetField( pMXHdr, 0, "iELInfo" ) ); for(i = 0; i < 9; i++) piData[i] = (int) pHdr->iELInfo[i]; mxSetField( pMXHdr, 0, "setName", mxCreateString( pHdr->setName ) ); mxSetField( pMXHdr, 0, "subsetName", mxCreateString( pHdr->subsetName ) ); mxSetField(pMXHdr, 0, "rmvSyncSz", createInt32Scalar(pHdr->rmvSyncSz)); mxSetField(pMXHdr, 0, "rmvSyncDur", createInt32Scalar(pHdr->rmvSyncDur)); mxSetField(pMXHdr, 0, "timestampMS", createInt32Scalar(pHdr->timestampMS)); mxSetField( pMXHdr, 0, "rmvDupEvents", mxCreateNumericMatrix( 1, CXH_RMVDUPEVTSZ, mxINT32_CLASS, mxREAL ) ); piData = (int*) mxGetData( mxGetField( pMXHdr, 0, "rmvDupEvents" ) ); for(i = 0; i < CXH_RMVDUPEVTSZ; i++) piData[i] = (int) pHdr->rmvDupEvents[i]; } //=== setTargetDefns ================================================================================================== // // Creates the "tgtdefns" field in the output structure returned by readcxdata(). This field is an array of // structures containing the definitions of participating targets, and it is created only for CNTLRX data files with // version >= 2. Target information is copied from our internal CXFILEDATA structure. // // The storage format for target definitions changed with data file version 8 (introduction of RMVideo in place of // Maestro's old VSG-based FB video), in data file version 13 (introduction of RMVideo "movie" target), and in data // file version 23 (introduction of "flicker" feature for RMVideo targets). The method handles both the current // (CXFILETGT) and old (CXFILETGT_V7, CXFILETGT_V12, CXFILETGT_V22) formats. // // ARGS: pOut -- [in/out] ptr to the MATLAB structure array prepared by readcxdata(). // // RETURNS: NONE. // void setTargetDefns(mxArray* pOut) { int i, j, iType; mxArray* pMXTgt; mxArray* pMXParms; CXFILETGT* pTgt; CXFILETGT_V7* pTgtV7; CXFILETGT_V12* pTgtV12; CXFILETGT_V22* pTgtV22; double *dPtr1, *dPtr2, *dPtr3; double dTmp; if( cxData.nTargets <= 0 || cxData.fileHdr.version < 2 ) // no tgts defined, or file vers < 2 return; pMXTgt = mxCreateStructMatrix( 1, cxData.nTargets, NUMTGTFIELDS, tgtFields ); // array of tgt defn structs if(pMXTgt == NULL) return; if(cxData.fileHdr.version >= 23) for(i = 0; i < cxData.nTargets; i++) { pTgt = &(cxData.pTargets[i]); mxSetField( pMXTgt, i, "category", createUInt32Scalar( (DWORD) pTgt->def.wType ) ); mxSetField( pMXTgt, i, "name", mxCreateString( pTgt->def.name ) ); if( pTgt->def.wType == CX_XYTARG ) // create & fill in defining params { // for an XY scope tgt, or... pMXParms = mxCreateStructMatrix( 1, 1, NUMXYTGTPARMS, xyTgtParams ); mxSetField( pMXParms, 0, "type", createInt32Scalar( pTgt->def.u.xy.type ) ); mxSetField( pMXParms, 0, "ndots", createInt32Scalar( pTgt->def.u.xy.ndots ) ); mxSetField( pMXParms, 0, "iDotLfUnits", createInt32Scalar( pTgt->def.u.xy.iDotLfUnits ) ); mxSetField( pMXParms, 0, "fDotLife", mxCreateDoubleScalar( pTgt->def.u.xy.fDotLife ) ); mxSetField( pMXParms, 0, "fRectW", mxCreateDoubleScalar( pTgt->def.u.xy.fRectW ) ); mxSetField( pMXParms, 0, "fRectH", mxCreateDoubleScalar( pTgt->def.u.xy.fRectH ) ); mxSetField( pMXParms, 0, "fInnerW", mxCreateDoubleScalar( pTgt->def.u.xy.fInnerW ) ); mxSetField( pMXParms, 0, "fInnerH", mxCreateDoubleScalar( pTgt->def.u.xy.fInnerH ) ); // XYPARMS.fInnerX, .fInnerY were added for V=9. Implied value is 0 for V<9. dTmp = (cxData.fileHdr.version >= 9) ? pTgt->def.u.xy.fInnerX : 0.0; mxSetField( pMXParms, 0, "fInnerX", mxCreateDoubleScalar( dTmp ) ); dTmp = (cxData.fileHdr.version >= 9) ? pTgt->def.u.xy.fInnerY : 0.0; mxSetField( pMXParms, 0, "fInnerY", mxCreateDoubleScalar( dTmp ) ); mxSetField( pMXTgt, i, "params", pMXParms ); } else if( pTgt->def.wType == CX_RMVTARG ) // ...a RMVideo tgt. { iType = pTgt->def.u.rmv.iType; pMXParms = mxCreateStructMatrix( 1, 1, NUMRMVTGTPARMS, rmvTgtParams ); mxSetField( pMXParms, 0, "iType", createInt32Scalar( iType ) ); mxSetField( pMXParms, 0, "iAperture", createInt32Scalar( pTgt->def.u.rmv.iAperture ) ); mxSetField( pMXParms, 0, "iFlags", createInt32Scalar( pTgt->def.u.rmv.iFlags ) ); mxSetField( pMXParms, 0, "fOuterW", mxCreateDoubleScalar( pTgt->def.u.rmv.fOuterW ) ); mxSetField( pMXParms, 0, "fOuterH", mxCreateDoubleScalar( pTgt->def.u.rmv.fOuterH ) ); mxSetField( pMXParms, 0, "fInnerW", mxCreateDoubleScalar( pTgt->def.u.rmv.fInnerW ) ); mxSetField( pMXParms, 0, "fInnerH", mxCreateDoubleScalar( pTgt->def.u.rmv.fInnerH ) ); mxSetField( pMXParms, 0, "nDots", createInt32Scalar( pTgt->def.u.rmv.nDots ) ); mxSetField( pMXParms, 0, "nDotSize", createInt32Scalar( pTgt->def.u.rmv.nDotSize ) ); mxSetField( pMXParms, 0, "iSeed", createInt32Scalar( pTgt->def.u.rmv.iSeed ) ); mxSetField( pMXParms, 0, "iPctCoherent", createInt32Scalar( pTgt->def.u.rmv.iPctCoherent ) ); mxSetField( pMXParms, 0, "iNoiseUpdIntv", createInt32Scalar( pTgt->def.u.rmv.iNoiseUpdIntv ) ); mxSetField( pMXParms, 0, "iNoiseLimit", createInt32Scalar( pTgt->def.u.rmv.iNoiseLimit ) ); mxSetField( pMXParms, 0, "fDotLife", mxCreateDoubleScalar( pTgt->def.u.rmv.fDotLife ) ); mxSetField( pMXParms, 0, "iRGBMean", mxCreateDoubleMatrix(1, 2, mxREAL) ); dPtr1 = mxGetPr( mxGetField(pMXParms, 0, "iRGBMean") ); for( j=0; j<2; j++ ) dPtr1[j] = pTgt->def.u.rmv.iRGBMean[j]; mxSetField( pMXParms, 0, "iRGBCon", mxCreateDoubleMatrix(1, 2, mxREAL) ); dPtr1 = mxGetPr( mxGetField(pMXParms, 0, "iRGBCon") ); for( j=0; j<2; j++ ) dPtr1[j] = pTgt->def.u.rmv.iRGBCon[j]; mxSetField( pMXParms, 0, "fSpatialFreq", mxCreateDoubleMatrix(1, 2, mxREAL) ); dPtr1 = mxGetPr( mxGetField(pMXParms, 0, "fSpatialFreq") ); for( j=0; j<2; j++ ) dPtr1[j] = pTgt->def.u.rmv.fSpatialFreq[j]; mxSetField( pMXParms, 0, "fDriftAxis", mxCreateDoubleMatrix(1, 2, mxREAL) ); dPtr1 = mxGetPr( mxGetField(pMXParms, 0, "fDriftAxis") ); for( j=0; j<2; j++ ) dPtr1[j] = pTgt->def.u.rmv.fDriftAxis[j]; mxSetField( pMXParms, 0, "fGratPhase", mxCreateDoubleMatrix(1, 2, mxREAL) ); dPtr1 = mxGetPr( mxGetField(pMXParms, 0, "fGratPhase") ); for( j=0; j<2; j++ ) dPtr1[j] = pTgt->def.u.rmv.fGratPhase[j]; mxSetField( pMXParms, 0, "fSigma", mxCreateDoubleMatrix(1, 2, mxREAL) ); dPtr1 = mxGetPr( mxGetField(pMXParms, 0, "fSigma") ); for( j=0; j<2; j++ ) dPtr1[j] = pTgt->def.u.rmv.fSigma[j]; mxSetField( pMXParms, 0, "strFolder", mxCreateString( (iType==RMV_MOVIE || iType==RMV_IMAGE) ? pTgt->def.u.rmv.strFolder : "" ) ); mxSetField( pMXParms, 0, "strFile", mxCreateString( (iType==RMV_MOVIE || iType==RMV_IMAGE) ? pTgt->def.u.rmv.strFile : "" ) ); // target flicker parameters added in V=23 mxSetField( pMXParms, 0, "iFlickerOn", createInt32Scalar( pTgt->def.u.rmv.iFlickerOn ) ); mxSetField( pMXParms, 0, "iFlickerOff", createInt32Scalar( pTgt->def.u.rmv.iFlickerOff ) ); mxSetField( pMXParms, 0, "iFlickerDelay", createInt32Scalar( pTgt->def.u.rmv.iFlickerDelay ) ); mxSetField( pMXTgt, i, "params", pMXParms ); } if( (cxData.fileHdr.flags & CXHF_ISCONTINUOUS) != 0 ) // remaining fields only apply to { // ContMode files... mxSetField( pMXTgt, i, "dwState", createUInt32Scalar( pTgt->dwState ) ); mxSetField( pMXTgt, i, "hPos", mxCreateDoubleScalar( pTgt->fPosX ) ); mxSetField( pMXTgt, i, "vPos", mxCreateDoubleScalar( pTgt->fPosY ) ); } } else if(cxData.fileHdr.version >= 13) for(i = 0; i < cxData.nTargets; i++) { pTgtV22 = &(cxData.pTargets_V22[i]); mxSetField( pMXTgt, i, "category", createUInt32Scalar( (DWORD) pTgtV22->def.wType ) ); mxSetField( pMXTgt, i, "name", mxCreateString( pTgtV22->def.name ) ); if( pTgtV22->def.wType == CX_XYTARG ) // create & fill in defining params { // for an XY scope tgt, or... pMXParms = mxCreateStructMatrix( 1, 1, NUMXYTGTPARMS, xyTgtParams ); mxSetField( pMXParms, 0, "type", createInt32Scalar( pTgtV22->def.u.xy.type ) ); mxSetField( pMXParms, 0, "ndots", createInt32Scalar( pTgtV22->def.u.xy.ndots ) ); mxSetField( pMXParms, 0, "iDotLfUnits", createInt32Scalar( pTgtV22->def.u.xy.iDotLfUnits ) ); mxSetField( pMXParms, 0, "fDotLife", mxCreateDoubleScalar( pTgtV22->def.u.xy.fDotLife ) ); mxSetField( pMXParms, 0, "fRectW", mxCreateDoubleScalar( pTgtV22->def.u.xy.fRectW ) ); mxSetField( pMXParms, 0, "fRectH", mxCreateDoubleScalar( pTgtV22->def.u.xy.fRectH ) ); mxSetField( pMXParms, 0, "fInnerW", mxCreateDoubleScalar( pTgtV22->def.u.xy.fInnerW ) ); mxSetField( pMXParms, 0, "fInnerH", mxCreateDoubleScalar( pTgtV22->def.u.xy.fInnerH ) ); // XYPARMS.fInnerX, .fInnerY were added for V=9. Implied value is 0 for V<9. dTmp = (cxData.fileHdr.version >= 9) ? pTgtV22->def.u.xy.fInnerX : 0.0; mxSetField( pMXParms, 0, "fInnerX", mxCreateDoubleScalar( dTmp ) ); dTmp = (cxData.fileHdr.version >= 9) ? pTgtV22->def.u.xy.fInnerY : 0.0; mxSetField( pMXParms, 0, "fInnerY", mxCreateDoubleScalar( dTmp ) ); mxSetField( pMXTgt, i, "params", pMXParms ); } else if( pTgtV22->def.wType == CX_RMVTARG ) // ...a RMVideo tgt. { iType = pTgtV22->def.u.rmv.iType; pMXParms = mxCreateStructMatrix( 1, 1, NUMRMVTGTPARMS, rmvTgtParams ); mxSetField( pMXParms, 0, "iType", createInt32Scalar( iType ) ); mxSetField( pMXParms, 0, "iAperture", createInt32Scalar( pTgtV22->def.u.rmv.iAperture ) ); mxSetField( pMXParms, 0, "iFlags", createInt32Scalar( pTgtV22->def.u.rmv.iFlags ) ); mxSetField( pMXParms, 0, "fOuterW", mxCreateDoubleScalar( pTgtV22->def.u.rmv.fOuterW ) ); mxSetField( pMXParms, 0, "fOuterH", mxCreateDoubleScalar( pTgtV22->def.u.rmv.fOuterH ) ); mxSetField( pMXParms, 0, "fInnerW", mxCreateDoubleScalar( pTgtV22->def.u.rmv.fInnerW ) ); mxSetField( pMXParms, 0, "fInnerH", mxCreateDoubleScalar( pTgtV22->def.u.rmv.fInnerH ) ); mxSetField( pMXParms, 0, "nDots", createInt32Scalar( pTgtV22->def.u.rmv.nDots ) ); mxSetField( pMXParms, 0, "nDotSize", createInt32Scalar( pTgtV22->def.u.rmv.nDotSize ) ); mxSetField( pMXParms, 0, "iSeed", createInt32Scalar( pTgtV22->def.u.rmv.iSeed ) ); mxSetField( pMXParms, 0, "iPctCoherent", createInt32Scalar( pTgtV22->def.u.rmv.iPctCoherent ) ); mxSetField( pMXParms, 0, "iNoiseUpdIntv", createInt32Scalar( pTgtV22->def.u.rmv.iNoiseUpdIntv ) ); mxSetField( pMXParms, 0, "iNoiseLimit", createInt32Scalar( pTgtV22->def.u.rmv.iNoiseLimit ) ); mxSetField( pMXParms, 0, "fDotLife", mxCreateDoubleScalar( pTgtV22->def.u.rmv.fDotLife ) ); mxSetField( pMXParms, 0, "iRGBMean", mxCreateDoubleMatrix(1, 2, mxREAL) ); dPtr1 = mxGetPr( mxGetField(pMXParms, 0, "iRGBMean") ); for( j=0; j<2; j++ ) dPtr1[j] = pTgtV22->def.u.rmv.iRGBMean[j]; mxSetField( pMXParms, 0, "iRGBCon", mxCreateDoubleMatrix(1, 2, mxREAL) ); dPtr1 = mxGetPr( mxGetField(pMXParms, 0, "iRGBCon") ); for( j=0; j<2; j++ ) dPtr1[j] = pTgtV22->def.u.rmv.iRGBCon[j]; mxSetField( pMXParms, 0, "fSpatialFreq", mxCreateDoubleMatrix(1, 2, mxREAL) ); dPtr1 = mxGetPr( mxGetField(pMXParms, 0, "fSpatialFreq") ); for( j=0; j<2; j++ ) dPtr1[j] = pTgtV22->def.u.rmv.fSpatialFreq[j]; mxSetField( pMXParms, 0, "fDriftAxis", mxCreateDoubleMatrix(1, 2, mxREAL) ); dPtr1 = mxGetPr( mxGetField(pMXParms, 0, "fDriftAxis") ); for( j=0; j<2; j++ ) dPtr1[j] = pTgtV22->def.u.rmv.fDriftAxis[j]; mxSetField( pMXParms, 0, "fGratPhase", mxCreateDoubleMatrix(1, 2, mxREAL) ); dPtr1 = mxGetPr( mxGetField(pMXParms, 0, "fGratPhase") ); for( j=0; j<2; j++ ) dPtr1[j] = pTgtV22->def.u.rmv.fGratPhase[j]; mxSetField( pMXParms, 0, "fSigma", mxCreateDoubleMatrix(1, 2, mxREAL) ); dPtr1 = mxGetPr( mxGetField(pMXParms, 0, "fSigma") ); for( j=0; j<2; j++ ) dPtr1[j] = pTgtV22->def.u.rmv.fSigma[j]; mxSetField( pMXParms, 0, "strFolder", mxCreateString( (iType==RMV_MOVIE || iType==RMV_IMAGE) ? pTgtV22->def.u.rmv.strFolder : "" ) ); mxSetField( pMXParms, 0, "strFile", mxCreateString( (iType==RMV_MOVIE || iType==RMV_IMAGE) ? pTgtV22->def.u.rmv.strFile : "" ) ); // target flicker parameters added in V=23 -- set to 0 for all previous versions mxSetField( pMXParms, 0, "iFlickerOn", createInt32Scalar(0) ); mxSetField( pMXParms, 0, "iFlickerOff", createInt32Scalar(0) ); mxSetField( pMXParms, 0, "iFlickerDelay", createInt32Scalar(0) ); mxSetField( pMXTgt, i, "params", pMXParms ); } if( (cxData.fileHdr.flags & CXHF_ISCONTINUOUS) != 0 ) // remaining fields only apply to { // ContMode files... mxSetField( pMXTgt, i, "dwState", createUInt32Scalar( pTgtV22->dwState ) ); mxSetField( pMXTgt, i, "hPos", mxCreateDoubleScalar( pTgtV22->fPosX ) ); mxSetField( pMXTgt, i, "vPos", mxCreateDoubleScalar( pTgtV22->fPosY ) ); } } else if(cxData.fileHdr.version >= 8) for(i = 0; i < cxData.nTargets; i++) { pTgtV12 = &(cxData.pTargets_V12[i]); mxSetField( pMXTgt, i, "category", createUInt32Scalar( (DWORD) pTgtV12->def.wType ) ); mxSetField( pMXTgt, i, "name", mxCreateString( pTgtV12->def.name ) ); if(pTgtV12->def.wType == CX_XYTARG) { pMXParms = mxCreateStructMatrix( 1, 1, NUMXYTGTPARMS, xyTgtParams ); mxSetField( pMXParms, 0, "type", createInt32Scalar( pTgtV12->def.u.xy.type ) ); mxSetField( pMXParms, 0, "ndots", createInt32Scalar( pTgtV12->def.u.xy.ndots ) ); mxSetField( pMXParms, 0, "iDotLfUnits", createInt32Scalar( pTgtV12->def.u.xy.iDotLfUnits ) ); mxSetField( pMXParms, 0, "fDotLife", mxCreateDoubleScalar( pTgtV12->def.u.xy.fDotLife ) ); mxSetField( pMXParms, 0, "fRectW", mxCreateDoubleScalar( pTgtV12->def.u.xy.fRectW ) ); mxSetField( pMXParms, 0, "fRectH", mxCreateDoubleScalar( pTgtV12->def.u.xy.fRectH ) ); mxSetField( pMXParms, 0, "fInnerW", mxCreateDoubleScalar( pTgtV12->def.u.xy.fInnerW ) ); mxSetField( pMXParms, 0, "fInnerH", mxCreateDoubleScalar( pTgtV12->def.u.xy.fInnerH ) ); // XYPARMS.fInnerX, .fInnerY were added for V=9. Implied value is 0 for V<9. dTmp = (cxData.fileHdr.version >= 9) ? pTgtV12->def.u.xy.fInnerX : 0.0; mxSetField( pMXParms, 0, "fInnerX", mxCreateDoubleScalar( dTmp ) ); dTmp = (cxData.fileHdr.version >= 9) ? pTgtV12->def.u.xy.fInnerY : 0.0; mxSetField( pMXParms, 0, "fInnerY", mxCreateDoubleScalar( dTmp ) ); mxSetField( pMXTgt, i, "params", pMXParms ); } else if(pTgtV12->def.wType == CX_RMVTARG) // handle target defns in file versions [8..12] properly! { pMXParms = mxCreateStructMatrix( 1, 1, NUMRMVTGTPARMS, rmvTgtParams ); mxSetField( pMXParms, 0, "iType", createInt32Scalar( pTgtV12->def.u.rmv.iType ) ); mxSetField( pMXParms, 0, "iAperture", createInt32Scalar( pTgtV12->def.u.rmv.iAperture ) ); mxSetField( pMXParms, 0, "iFlags", createInt32Scalar( pTgtV12->def.u.rmv.iFlags ) ); mxSetField( pMXParms, 0, "fOuterW", mxCreateDoubleScalar( pTgtV12->def.u.rmv.fOuterW ) ); mxSetField( pMXParms, 0, "fOuterH", mxCreateDoubleScalar( pTgtV12->def.u.rmv.fOuterH ) ); mxSetField( pMXParms, 0, "fInnerW", mxCreateDoubleScalar( pTgtV12->def.u.rmv.fInnerW ) ); mxSetField( pMXParms, 0, "fInnerH", mxCreateDoubleScalar( pTgtV12->def.u.rmv.fInnerH ) ); mxSetField( pMXParms, 0, "nDots", createInt32Scalar( pTgtV12->def.u.rmv.nDots ) ); mxSetField( pMXParms, 0, "nDotSize", createInt32Scalar( pTgtV12->def.u.rmv.nDotSize ) ); mxSetField( pMXParms, 0, "iSeed", createInt32Scalar( pTgtV12->def.u.rmv.iSeed ) ); mxSetField( pMXParms, 0, "iPctCoherent", createInt32Scalar( pTgtV12->def.u.rmv.iPctCoherent ) ); mxSetField( pMXParms, 0, "iNoiseUpdIntv", createInt32Scalar( pTgtV12->def.u.rmv.iNoiseUpdIntv ) ); mxSetField( pMXParms, 0, "iNoiseLimit", createInt32Scalar( pTgtV12->def.u.rmv.iNoiseLimit ) ); mxSetField( pMXParms, 0, "fDotLife", mxCreateDoubleScalar( pTgtV12->def.u.rmv.fDotLife ) ); mxSetField( pMXParms, 0, "iRGBMean", mxCreateDoubleMatrix(1, 2, mxREAL) ); dPtr1 = mxGetPr( mxGetField(pMXParms, 0, "iRGBMean") ); for( j=0; j<2; j++ ) dPtr1[j] = pTgtV12->def.u.rmv.iRGBMean[j]; mxSetField( pMXParms, 0, "iRGBCon", mxCreateDoubleMatrix(1, 2, mxREAL) ); dPtr1 = mxGetPr( mxGetField(pMXParms, 0, "iRGBCon") ); for( j=0; j<2; j++ ) dPtr1[j] = pTgtV12->def.u.rmv.iRGBCon[j]; mxSetField( pMXParms, 0, "fSpatialFreq", mxCreateDoubleMatrix(1, 2, mxREAL) ); dPtr1 = mxGetPr( mxGetField(pMXParms, 0, "fSpatialFreq") ); for( j=0; j<2; j++ ) dPtr1[j] = pTgtV12->def.u.rmv.fSpatialFreq[j]; mxSetField( pMXParms, 0, "fDriftAxis", mxCreateDoubleMatrix(1, 2, mxREAL) ); dPtr1 = mxGetPr( mxGetField(pMXParms, 0, "fDriftAxis") ); for( j=0; j<2; j++ ) dPtr1[j] = pTgtV12->def.u.rmv.fDriftAxis[j]; mxSetField( pMXParms, 0, "fGratPhase", mxCreateDoubleMatrix(1, 2, mxREAL) ); dPtr1 = mxGetPr( mxGetField(pMXParms, 0, "fGratPhase") ); for( j=0; j<2; j++ ) dPtr1[j] = pTgtV12->def.u.rmv.fGratPhase[j]; mxSetField( pMXParms, 0, "fSigma", mxCreateDoubleMatrix(1, 2, mxREAL) ); dPtr1 = mxGetPr( mxGetField(pMXParms, 0, "fSigma") ); for( j=0; j<2; j++ ) dPtr1[j] = pTgtV12->def.u.rmv.fSigma[j]; // these next five fields were not present in target records for v=8..12 data files mxSetField( pMXParms, 0, "strFolder", mxCreateString( "" ) ); mxSetField( pMXParms, 0, "strFile", mxCreateString( "" ) ); mxSetField( pMXParms, 0, "iFlickerOn", createInt32Scalar(0) ); mxSetField( pMXParms, 0, "iFlickerOff", createInt32Scalar(0) ); mxSetField( pMXParms, 0, "iFlickerDelay", createInt32Scalar(0) ); mxSetField( pMXTgt, i, "params", pMXParms ); } if( (cxData.fileHdr.flags & CXHF_ISCONTINUOUS) != 0 ) { mxSetField( pMXTgt, i, "dwState", createUInt32Scalar( pTgtV12->dwState ) ); mxSetField( pMXTgt, i, "hPos", mxCreateDoubleScalar( pTgtV12->fPosX ) ); mxSetField( pMXTgt, i, "vPos", mxCreateDoubleScalar( pTgtV12->fPosY ) ); } } else for( i = 0; i < cxData.nTargets; i++ ) // handle target defns in file versions [2..7] properly! { pTgtV7 = &(cxData.pTargets_V7[i]); mxSetField( pMXTgt, i, "category", createUInt32Scalar( (DWORD) pTgtV7->def.wType ) ); mxSetField( pMXTgt, i, "name", mxCreateString( pTgtV7->def.name ) ); if( pTgtV7->def.wType == CX_XYTARG ) { pMXParms = mxCreateStructMatrix( 1, 1, NUMXYTGTPARMS, xyTgtParams ); mxSetField( pMXParms, 0, "type", createInt32Scalar( pTgtV7->def.u.xy.type ) ); mxSetField( pMXParms, 0, "ndots", createInt32Scalar( pTgtV7->def.u.xy.ndots ) ); mxSetField( pMXParms, 0, "iDotLfUnits", createInt32Scalar( pTgtV7->def.u.xy.iDotLfUnits ) ); mxSetField( pMXParms, 0, "fDotLife", mxCreateDoubleScalar( pTgtV7->def.u.xy.fDotLife ) ); mxSetField( pMXParms, 0, "fRectW", mxCreateDoubleScalar( pTgtV7->def.u.xy.fRectW ) ); mxSetField( pMXParms, 0, "fRectH", mxCreateDoubleScalar( pTgtV7->def.u.xy.fRectH ) ); mxSetField( pMXParms, 0, "fInnerW", mxCreateDoubleScalar( pTgtV7->def.u.xy.fInnerW ) ); mxSetField( pMXParms, 0, "fInnerH", mxCreateDoubleScalar( pTgtV7->def.u.xy.fInnerH ) ); // these two fields did not exist prior to V=9. Set to 0 always mxSetField( pMXParms, 0, "fInnerX", mxCreateDoubleScalar( 0.0 ) ); mxSetField( pMXParms, 0, "fInnerY", mxCreateDoubleScalar( 0.0 ) ); mxSetField( pMXTgt, i, "params", pMXParms ); } else if( pTgtV7->def.wType == CX_FBTARG ) { pMXParms = mxCreateStructMatrix( 1, 1, NUMFBTGTPARMS, fbTgtParams ); mxSetField( pMXParms, 0, "type", createInt32Scalar( pTgtV7->def.u.fb.type ) ); mxSetField( pMXParms, 0, "shape", createInt32Scalar( pTgtV7->def.u.fb.shape ) ); mxSetField( pMXParms, 0, "csMean", mxCreateDoubleMatrix( 1, 3, mxREAL ) ); mxSetField( pMXParms, 0, "csCon", mxCreateDoubleMatrix( 1, 3, mxREAL ) ); dPtr1 = mxGetPr( mxGetField( pMXParms, 0, "csMean" ) ); dPtr2 = mxGetPr( mxGetField( pMXParms, 0, "csCon" ) ); for( j = 0; j < 3; j++ ) { dPtr1[j] = pTgtV7->def.u.fb.csMean[j]; dPtr2[j] = pTgtV7->def.u.fb.csCon[j]; } mxSetField( pMXParms, 0, "fRectW", mxCreateDoubleScalar( pTgtV7->def.u.fb.fRectW ) ); mxSetField( pMXParms, 0, "fRectH", mxCreateDoubleScalar( pTgtV7->def.u.fb.fRectH ) ); mxSetField( pMXParms, 0, "fSigma", mxCreateDoubleScalar( pTgtV7->def.u.fb.fSigma ) ); mxSetField( pMXParms, 0, "fGratSF", mxCreateDoubleMatrix( 1, 2, mxREAL ) ); mxSetField( pMXParms, 0, "fGratAxis", mxCreateDoubleMatrix( 1, 2, mxREAL ) ); mxSetField( pMXParms, 0, "fGratPhase", mxCreateDoubleMatrix( 1, 2, mxREAL ) ); dPtr1 = mxGetPr( mxGetField( pMXParms, 0, "fGratSF" ) ); dPtr2 = mxGetPr( mxGetField( pMXParms, 0, "fGratAxis" ) ); dPtr3 = mxGetPr( mxGetField( pMXParms, 0, "fGratPhase" ) ); for( j = 0; j < 2; j++ ) { dPtr1[j] = pTgtV7->def.u.fb.fGratSF[j]; dPtr2[j] = pTgtV7->def.u.fb.fGratAxis[j]; dPtr3[j] = pTgtV7->def.u.fb.fGratPhase[j]; } mxSetField( pMXTgt, i, "params", pMXParms ); } if( (cxData.fileHdr.flags & CXHF_ISCONTINUOUS) != 0 ) { mxSetField( pMXTgt, i, "dwState", createUInt32Scalar( pTgtV7->dwState ) ); mxSetField( pMXTgt, i, "hPos", mxCreateDoubleScalar( pTgtV7->fPosX ) ); mxSetField( pMXTgt, i, "vPos", mxCreateDoubleScalar( pTgtV7->fPosY ) ); } } mxSetField( pOut, 0, "tgtdefns", pMXTgt ); // set "tgtdefns" field in output } //=== setStimulusRunDefn ============================================================================================== // // Creates the "stimulusrun" field in the output structure returned by readcxdata(). This field is a structure // containing some general run parameters and an array of N structures defining the N active stimulus channels in // the run. The field is created only for CNTRLX data files with version >= 2. Stimulus run information is copied // from our internal CXFILEDATA structure. // // ARGS: pOut -- [in/out] ptr to the MATLAB structure array prepared by readcxdata(). // // RETURNS: NONE. // void setStimulusRunDefn( mxArray* pOut ) { int i, xySeqType, psgmType; mxArray* pMXRun; mxArray* pMXStims; mxArray* pMXParms; CXFILESTIMRUNHDR* pHdr; STIMCHAN* pStim; if( cxData.nStims <= 1 || cxData.fileHdr.version < 2 ) // no run defined, or file vers < 2 return; xySeqType = STIM_ISXYSEQ; // chan type consts changed in vers 7! if( cxData.fileHdr.version < 7 ) ++xySeqType; psgmType = STIM_ISPSGM; if( cxData.fileHdr.version < 7 ) ++psgmType; pMXRun = mxCreateStructMatrix( 1, 1, NUMRUNFIELDS, runFields ); // create stimulus run output struct if( pMXRun == NULL ) return; mxSetField( pOut, 0, "stimulusrun", pMXRun ); // set "stimulusrun" field in output pHdr = &(cxData.pStimBuf[0].hdr); // copy stim run hdr params mxSetField( pMXRun, 0, "bRunning", createUInt32Scalar( (DWORD) pHdr->bRunning ) ); mxSetField( pMXRun, 0, "iDutyPeriod", createInt32Scalar( pHdr->iDutyPeriod ) ); mxSetField( pMXRun, 0, "iDutyPulse", createInt32Scalar( pHdr->iDutyPulse ) ); mxSetField( pMXRun, 0, "nAutoStop", createInt32Scalar( pHdr->nAutoStop ) ); mxSetField( pMXRun, 0, "fHOffset", mxCreateDoubleScalar( pHdr->fHOffset ) ); mxSetField( pMXRun, 0, "fVOffset", mxCreateDoubleScalar( pHdr->fVOffset ) ); mxSetField( pMXRun, 0, "nXYTgts", createInt32Scalar( pHdr->nXYTgts ) ); pMXStims=mxCreateStructMatrix(1, pHdr->nStimuli, NUMSTIMFIELDS, stimFields); // array of stimulus channel defns if( pMXStims == NULL ) return; mxSetField( pMXRun, 0, "stimuli", pMXStims ); for( i = 0; (i < pHdr->nStimuli) && (i < cxData.nStims - 1); i++ ) // copy stimulus channel defns into { // output array of structs... pStim = &(cxData.pStimBuf[i+1].stim); mxSetField( pMXStims, i, "bOn", createUInt32Scalar( (DWORD) pStim->bOn ) ); mxSetField( pMXStims, i, "iMarker", createInt32Scalar( pStim->iMarker ) ); mxSetField( pMXStims, i, "iType", createInt32Scalar( pStim->iType ) ); mxSetField( pMXStims, i, "iStdMode", createInt32Scalar( pStim->iStdMode ) ); mxSetField( pMXStims, i, "tStart", createInt32Scalar( pStim->tStart ) ); if( pStim->iType == xySeqType ) // each stimulus channel type has a { // different parameter set... pMXParms = mxCreateStructMatrix( 1, 1, NUMXYSEQFIELDS, xyseqFields ); if( pMXParms == NULL ) return; mxSetField( pMXParms, 0, "iOpMode", createInt32Scalar( pStim->xy.iOpMode ) ); mxSetField( pMXParms, 0, "iRefresh", createInt32Scalar( pStim->xy.iRefresh ) ); mxSetField( pMXParms, 0, "nSegs", createInt32Scalar( pStim->xy.nSegs ) ); mxSetField( pMXParms, 0, "iSegDur", createInt32Scalar( pStim->xy.iSegDur ) ); mxSetField( pMXParms, 0, "iSeed", createInt32Scalar( pStim->xy.iSeed ) ); mxSetField( pMXParms, 0, "nChoices", createInt32Scalar( pStim->xy.nChoices ) ); mxSetField( pMXParms, 0, "fAngle", mxCreateDoubleScalar( pStim->xy.fAngle ) ); mxSetField( pMXParms, 0, "fVel", mxCreateDoubleScalar( pStim->xy.fVel ) ); mxSetField( pMXParms, 0, "fOffsetV", mxCreateDoubleScalar( pStim->xy.fOffsetV ) ); } else if( pStim->iType == psgmType ) { pMXParms = mxCreateStructMatrix( 1, 1, NUMSGMFIELDS, sgmFields ); if( pMXParms == NULL ) return; mxSetField( pMXParms, 0, "iOpMode", createInt32Scalar( pStim->sgm.iOpMode ) ); mxSetField( pMXParms, 0, "bExtTrig", createUInt32Scalar( (DWORD) pStim->sgm.bExtTrig ) ); mxSetField( pMXParms, 0, "iAmp1", createInt32Scalar( pStim->sgm.iAmp1 ) ); mxSetField( pMXParms, 0, "iAmp2", createInt32Scalar( pStim->sgm.iAmp2 ) ); mxSetField( pMXParms, 0, "iPW1", createInt32Scalar( pStim->sgm.iPW1 ) ); mxSetField( pMXParms, 0, "iPW2", createInt32Scalar( pStim->sgm.iPW2 ) ); mxSetField( pMXParms, 0, "iPulseIntv", createInt32Scalar( pStim->sgm.iPulseIntv ) ); mxSetField( pMXParms, 0, "iTrainIntv", createInt32Scalar( pStim->sgm.iTrainIntv ) ); mxSetField( pMXParms, 0, "nPulses", createInt32Scalar( pStim->sgm.nPulses ) ); mxSetField( pMXParms, 0, "nTrains", createInt32Scalar( pStim->sgm.nTrains ) ); } else if( pStim->iStdMode == MODE_ISSINE ) { pMXParms = mxCreateStructMatrix( 1, 1, NUMSINEFIELDS, sineFields ); if( pMXParms == NULL ) return; mxSetField( pMXParms, 0, "iPeriod", createInt32Scalar( pStim->sine.iPeriod ) ); mxSetField( pMXParms, 0, "fAmp", mxCreateDoubleScalar( pStim->sine.fAmp ) ); mxSetField( pMXParms, 0, "fPhase", mxCreateDoubleScalar( pStim->sine.fPhase ) ); mxSetField( pMXParms, 0, "fDirec", mxCreateDoubleScalar( pStim->sine.fDirec ) ); } else if( pStim->iStdMode == MODE_ISPULSE ) { pMXParms = mxCreateStructMatrix( 1, 1, NUMPULSEFIELDS, pulseFields ); if( pMXParms == NULL ) return; mxSetField( pMXParms, 0, "bBlank", createUInt32Scalar( (DWORD) pStim->pulse.bBlank ) ); mxSetField( pMXParms, 0, "iPulseDur", createInt32Scalar( pStim->pulse.iPulseDur ) ); mxSetField( pMXParms, 0, "iRampDur", createInt32Scalar( pStim->pulse.iRampDur ) ); mxSetField( pMXParms, 0, "fAmp", mxCreateDoubleScalar( pStim->pulse.fAmp ) ); mxSetField( pMXParms, 0, "fDirec", mxCreateDoubleScalar( pStim->pulse.fDirec ) ); } mxSetField( pMXStims, i, "params", pMXParms ); } } //=== setTagSections ================================================================================================== // // Creates the "tagSections" field in the output structure returned by readcxdata(). This field is an array of // structures containing the definitions of any tagged sections found in a Maestro trial. The array will be empty // if the data file was collected in ContMode, if the trial contained no tagged sections, or if the file was // generated by a version of Maestro that did not support tagged sections. // // NOTE: BE SURE to call this function after processTrialCodes(), since it makes use of trial segment information // prepared by that method. // // ARGS: pOut -- [in/out] ptr to the MATLAB structure array prepared by readcxdata(). // // RETURNS: NONE. // void setTagSections( mxArray* pOut ) { int i, iFirstSeg, iLastSeg, tStart, tLen; mxArray* pMXSections; TRIALSECT* pSect; pMXSections = mxCreateStructMatrix( 1, cxData.nSections, // create array of structs defining 0 or NUMTAGSECTFIELDS, tagSectFields ); // more tagged sections if( pMXSections == NULL ) return; // unable to create struct array! for( i = 0; i < cxData.nSections; i++ ) // fill out the tagged section info { pSect = &(cxData.sections[i]); // first, the section tag and segment iFirstSeg = (int) pSect->cFirstSeg; // range spanned, as recorded in file iLastSeg = (int) pSect->cLastSeg; mxSetField( pMXSections, i, "tag", mxCreateString( pSect->tag ) ); mxSetField( pMXSections, i, "firstSeg", createInt32Scalar( iFirstSeg ) ); mxSetField( pMXSections, i, "lastSeg", createInt32Scalar( iLastSeg ) ); tStart = -1; // if trial's segment info is valid, tLen = -1; // report section start time and len if( iFirstSeg <= iLastSeg && cxData.nSegments > iLastSeg && // relative to when recording started; cxData.tRecordStarted >= 0 && cxData.tTrialLen > 0 && // otherwise, both are set to -1. !cxData.bSkipOccurred ) { tStart = cxData.segStart[iFirstSeg] - cxData.tRecordStarted; if( (iLastSeg + 1) < cxData.nSegments ) tLen = cxData.segStart[iLastSeg+1] - cxData.segStart[iFirstSeg]; else tLen = cxData.tTrialLen - cxData.segStart[iFirstSeg]; } mxSetField( pMXSections, i, "tStart", createInt32Scalar( tStart ) ); mxSetField( pMXSections, i, "tLen", createInt32Scalar( tLen ) ); } mxSetField( pOut, 0, "tagSections", pMXSections ); // set "tagSections" field in output } //=== setTrialInfo ==================================================================================================== // // Creates the "trialInfo" field in the output structure returned by readcxdata(). This structure field contains // miscellaneous additional information about a Maestro trial definition. The structure members will be initialized // to zero or empty array if the data file was collected in ContMode. // // NOTE: BE SURE to call this function after processTrialCodes(), since it accesses trial information prepared by // that method. // // ARGS: pOut -- [in/out] ptr to the MATLAB structure array prepared by readcxdata(). // // RETURNS: NONE. // void setTrialInfo(mxArray* pOut) { int i; mxArray* pMXInfo; mxArray* pMXPerts; double* pdBuf; PMPERTOBJ pPert; // if found no trial codes, do nothing! if(cxData.nCodes <= 0) return; // create structure that will hold contents of 'trialInfo' field in output pMXInfo = mxCreateStructMatrix(1, 1, NUMTRIALINFOFIELDS, trialInfoFields ); if(pMXInfo == NULL) return; // the 'segStart' field: 1xN matrix of segment start times in trial ticks. mxSetField(pMXInfo, 0, "segStart", mxCreateDoubleMatrix(1, cxData.nSegments, mxREAL)); pdBuf = (double*) mxGetPr(mxGetField(pMXInfo, 0, "segStart")); for(i = 0; i < cxData.nSegments; i++) pdBuf[i] = (double) cxData.segStart[i]; // the 'duration' field: total trial length in # of trial ticks mxSetField(pMXInfo, 0, "duration", createInt32Scalar(cxData.tTrialLen)); // the 'tRecord' field: elapsed time at which recording began, in # trial ticks mxSetField(pMXInfo, 0, "tRecord", createInt32Scalar(cxData.tRecordStarted)); // the 'perts' field: 1xM array of "perturbation info" structures pMXPerts = mxCreateStructMatrix(1, G_pertMgr.nPerts, NUMPERTINFOFIELDS, pertInfoFields); mxSetField(pMXInfo, 0, "perts", pMXPerts); if(pMXPerts != NULL) for(i = 0; i < G_pertMgr.nPerts; i++) { pPert = &(G_pertMgr.perts[i]); mxSetField(pMXPerts, i, "tgt", createInt32Scalar(pPert->iTgt)); mxSetField(pMXPerts, i, "cmpt", createInt32Scalar(pPert->idCmpt)); mxSetField(pMXPerts, i, "start", createInt32Scalar(pPert->iStart)); mxSetField(pMXPerts, i, "amp", mxCreateDoubleScalar(pPert->fAmp )); mxSetField(pMXPerts, i, "type", createInt32Scalar(pPert->def.iType)); mxSetField(pMXPerts, i, "dur", createInt32Scalar(pPert->def.iDur)); } // set 'trialInfo' field in output structure mxSetField(pOut, 0, "trialInfo", pMXInfo); } //=== processEdits ==================================================================================================== // // Process all XWORK editing actions stored in the relevant internal buffer. The following actions are parsed into // fields of the MATLAB output structure returned by readcxdata(): // // ACTION_CUTIT ==> This action code describes a "saccade cut" made on the original data trace(s) in XWORK. The // saccade cuts are stored in the MATLAB output as an Nx3 matrix, where N is the total # of saccade cuts. For // each cut we must save the beginning & end points of the cut, and the AI channel# of the affected trace. // ACTION_MARK ==> This action code describes a "mark segment" placed by the user in XWORK. The mark segments // are stored in the MATLAB output as an Nx2 matrix, where N is the total # of mark segments. For each mark // segment we must save its beginning and end points. // ACTION_SETMARK1, ACTION_SETMARK2 ==> These action codes describe two different "mark points" that can be // placed by the user in XWORK. Each mark point set (one for _SETMARK1, another for _SETMARK2) is stored in // the MATLAB output as an Nx1 matrix, where N is the total # of mark points. // ACTION_REMOVESORTSPK, ACTION_ADDSORTSPK ==> [Introduced in JMWork 1.0, circa 09jan2008] These action codes // manually remove or add individual spikes to any of the "unedited" sorted-spike train data channels. The // unedited spike trains are stored in a separate sequence of 1KB records with id tags CX_SPIKESORTREC_FIRST.. // CX_SPIKESORTREC_LAST. // These actions are processed by actually modifying the sequences of spike arrival times that should have // already been stored in CXFILEDATA.pdSortedSpikes[]. THUS, IT IS ESSENTIAL that this method be called AFTER // the entire file has been parsed and BEFORE the method setSortedSpikesOutput() is called to copy the // (possibly edited) spike trains into the output structure. See also: addSortedSpike(), removeSortedSpike(). // ACTION_DEFTAG ==> [Introduced in JMWork 1.4.0, Sep2010]. Action code group defines a "tag", ie, a user-defined // label attached to the recorded timeline. Code following action ID is the timestamp in milliseconds. This // is followed by 4 32-bit ints containing 16-byte label field, packed in little-endian order and padded with // zeros if the label length is less than 16. // ACTION_DISCARD ==> [Introduced in JMWork 1.4.0, Sep2010]. Presence of this action code indicates that file has // been marked as "discarded", to be ignored by downstream analysis. It is an explicit discard mark intended // to replace the "hacked" conventions that were historically used in XWork and MWork. // // Before processing the editing info in the internal buffer, we must allocate temporary arrays to hold the info // described above. The function will fail if any of these allocations fail. We release the memory -- including // the internal buffer that held the edit codes in the first place -- once we've copied the info into the MATLAB // output structure. // // ARGS: pOut -- [in/out] ptr to the MATLAB structure array prepared by readcxdata(). // // RETURNS: TRUE if successful; FALSE otherwise. // BOOL processEdits( mxArray* pOut ) { int i, iCh, t0, t1; double* pdOut; double* pdCutStart; // holds start points of any saccade cuts found in edit buf double* pdCutEnd; // holds end points of any saccade cuts found in edit buf double* pdCutChan; // holds affected ch# for saccade cuts found in edit buf double* pdMark1; // holds all SETMARK1 mark points found in edit buf double* pdMark2; // holds all SETMARK2 mark points found in edit buf double* pdMarks; // holds all mark segments found in edit buf PTAGMARK pdTags; // holds all labelled tags found in edit buf (ACTION_DEFTAG) int nCuts; // # of saccade cuts found in edit buf int nMark1; // # of SETMARK1 mark points found in edit buf int nMark2; // # of SETMARK2 mark points found in edit buf int nMarks; // # of mark segments found in edit buf int nTags; // # of labelled tags found in edit buf int discarded; // nonzero if we find a discard mark among edits -- any // of the three recognized discard mark styles int explicitDiscard; // nonzero if ACTION_DISCARD present: JMWork-style discard mxArray* pMXTags; // Matlab structure array to hold ACTION_DEFTAG tags // compute worst-case # of cuts, marks, or tags in edit buffer nCuts = cxData.nEdits / 5; if( nCuts < 1 ) nCuts = 1; nMark1 = cxData.nEdits / 2; if( nMark1 < 1 ) nMark1 = 1; nMark2 = nMark1; nMarks = cxData.nEdits / 3; if( nMarks < 1 ) nMarks = 1; nTags = cxData.nEdits / 6; if( nTags < 1 ) nTags = 1; // allocate temporary buffers for each annotation type based on worst-case counts pdCutStart = (double*) malloc( nCuts * sizeof(double) ); pdCutEnd = (double*) malloc( nCuts * sizeof(double) ); pdCutChan = (double*) malloc( nCuts * sizeof(double) ); pdMark1 = (double*) malloc( nMark1 * sizeof(double) ); pdMark2 = (double*) malloc( nMark2 * sizeof(double) ); pdMarks = (double*) malloc( nMarks*2 * sizeof(double) ); pdTags = (PTAGMARK) malloc( nTags * sizeof(TAGMARK) ); // abort if we failed to allocate any buffer if(pdCutStart == NULL || pdCutEnd == NULL || pdCutChan == NULL || pdMark1 == NULL || pdMark2 == NULL || pdMarks == NULL || pdTags == NULL) { free( pdCutStart ); free( pdCutEnd ); free( pdCutChan ); free( pdMark1 ); free( pdMark2 ); free( pdMarks ); free(pdTags); printf( "ERROR: Memory allocation failure.\n" ); return( FALSE ); } nCuts = nMark1 = nMark2 = nMarks = nTags = 0; discarded = 0; explicitDiscard = 0; i = 1; // very first action code = action count -- we skip it while( i < cxData.nEdits ) switch( cxData.piEdits[i] ) // process all actions in the edit buffer: { case ACTION_SACCUT: i += 10; break; // most action types are ignored here; we just skip case ACTION_RMUNIT: // over the codes defining that action... case ACTION_ADDUNIT: i += 2; break; case ACTION_RMALL: case ACTION_EDITEVENT: i += 3; break; case ACTION_ILLEGAL: // JMWork fills partial edit record with this value case 0 : // while we'll see this if file was touched by XWork. i = cxData.nEdits; break; case ACTION_CUTIT: // ACTION_CUTIT ch# refT startT endT iCh = cxData.piEdits[i+1]; t0 = cxData.piEdits[i+2] + cxData.piEdits[i+3]; t1 = cxData.piEdits[i+2] + cxData.piEdits[i+4]; if( iVerbose ) printf( "Saccade cut: ch#%i [%i %i]\n", iCh, t0, t1 ); pdCutChan[nCuts] = (double) iCh; pdCutStart[nCuts] = (double) t0; pdCutEnd[nCuts] = (double) t1; ++nCuts; i += 5; break; case ACTION_SETMARK1: // ACTION_SETMARK1 markT if( iVerbose ) printf( "Mark1 at %i\n", cxData.piEdits[i+1] ); pdMark1[nMark1] = (double) cxData.piEdits[i+1]; if(pdMark1[nMark1] == -1) discarded = 1; // mark1 = -1 is the MWork-style discard mark! ++nMark1; i += 2; break; case ACTION_SETMARK2: // ACTION_SETMARK2 markT if( iVerbose ) printf( "Mark2 at %i\n", cxData.piEdits[i+1] ); pdMark2[nMark2] = (double) cxData.piEdits[i+1]; ++nMark2; i += 2; break; case ACTION_MARK: // ACTION_MARK startT endT t0 = cxData.piEdits[i+1]; t1 = cxData.piEdits[i+2]; if( iVerbose ) printf( "Mark segment: [%i %i]\n", t0, t1 ); pdMarks[nMarks*2] = (double) t0; pdMarks[nMarks*2 + 1] = (double) t1; ++nMarks; discarded = 1; // a mark segment is XWork-style discard mark! i += 3; break; case ACTION_DEFTAG: // ACTION_DEFTAG tagT labelInt0 .. labelInt3 pdTags[nTags].time = cxData.piEdits[i+1]; unpackTagLabel(pdTags[nTags].label, &(cxData.piEdits[i+2])); if(iVerbose) printf("Tag: [%i %s]\n", pdTags[nTags].time, pdTags[nTags].label); ++nTags; i += 6; break; case ACTION_DISCARD: // ACTION_DISCARD - the JMWork-style discard mark! discarded = 1; explicitDiscard = 1; i += 1; break; case ACTION_REMOVESORTSPK: // ACTION_REMOVESORTSPK spkTrainCh# spkT_10us removeSortedSpike(cxData.piEdits[i+1], cxData.piEdits[i+2]); i += 3; break; case ACTION_ADDSORTSPK: // ACTION_ADDSORTSPK spkTrainCh# spkT_10us addSortedSpike(cxData.piEdits[i+1], cxData.piEdits[i+2]); i += 3; break; default : // skip over unrecognized action codes! ++i; break; } // free internal buf for edit actions; we no longer need it. free(cxData.piEdits); cxData.piEdits = NULL; cxData.nEdits = cxData.nEditsBufSz = 0; // store any saccade cut info in Nx3 output matrix "cut", then free the relevant temporary buffers... if(nCuts > 0) { mxSetField( pOut, 0, "cut", mxCreateDoubleMatrix( nCuts, 3, mxREAL ) ); pdOut = (double*) mxGetPr( mxGetField( pOut, 0, "cut" ) ); memcpy( (void*) pdOut, (void*) pdCutStart, nCuts*sizeof(double) ); memcpy( (void*) (pdOut + nCuts), (void*) pdCutEnd, nCuts*sizeof(double) ); memcpy( (void*) (pdOut + nCuts*2),(void*) pdCutChan, nCuts*sizeof(double) ); } free( pdCutStart ); free( pdCutEnd ); free( pdCutChan ); // store any SETMARK1 pts in 1xN output matrix "mark1", then free relevant temp buffer... if( nMark1 > 0 ) { mxSetField( pOut, 0, "mark1", mxCreateDoubleMatrix( 1, nMark1, mxREAL ) ); pdOut = (double*) mxGetPr( mxGetField( pOut, 0, "mark1" ) ); memcpy( (void*) pdOut, (void*) pdMark1, nMark1*sizeof(double) ); } free( pdMark1 ); // store any SETMARK2 pts in 1xN output matrix "mark2", then free relevant temp buffer... if( nMark2 > 0 ) { mxSetField( pOut, 0, "mark2", mxCreateDoubleMatrix( 1, nMark2, mxREAL ) ); pdOut = (double*) mxGetPr( mxGetField( pOut, 0, "mark2" ) ); memcpy( (void*) pdOut, (void*) pdMark2, nMark2*sizeof(double) ); } free( pdMark2 ); // store any mark segments in Nx2 output matrix "marks", then free relevant temp buffer... if( nMarks > 0 ) { mxSetField( pOut, 0, "marks", mxCreateDoubleMatrix( nMarks, 2, mxREAL) ); pdOut = mxGetPr( mxGetField( pOut, 0, "marks" ) ); for( i = 0; i < nMarks; i++ ) { pdOut[i] = pdMarks[i*2]; pdOut[i+nMarks] = pdMarks[i*2+1]; } } free( pdMarks ); // store any labelled tag in 1xN structure array "tags", then free relevant temp buffer... pMXTags = mxCreateStructMatrix(1, nTags, NUMTAGFIELDS, tagFields); if(pMXTags == NULL) { printf( "ERROR: Memory allocation failure.\n" ); free(pdTags); return(FALSE); } for(i=0; i> (j*8)) & 0x0FF); if(c == 0) { // got terminal null. If string is empty, set it to "!" if(idx == 0) sbuf[0] = (char) 0x21; return; } else { // make sure next char is valid. If not, set it to '!'. if(c < (char)0x21 || c > (char)0x7E) c = (char) 0x21; sbuf[idx++] = c; } } } } //=== removeSortedSpike, addSortedSpike =============================================================================== // // Helper methods that handle action codes ACTION_REMOVESORTSPK and ACTION_ADDSORTSPK for processEdits(). They // modify the corresponding sorted-spike train buffer in CXFILEDATA.pdSortedSpikes[ch], either removing a spike or // adding one -- while keeping the spikes in chronological order. // // NOTES: // 1) This is an inefficient approach, since each invocation involves searching the spike train array and copying a // portion of it shifted one element forward or backward. Spike edits are relatively rare, so we'll leave it this // way for now. // 2) IMPORTANT: While processing sorted-spike train records, interspike intervals are converted from an integer // number of 10us ticks to a double-valued elapsed time in milliseconds. Each spike edit time is saved in the action // record as an integer-valued elapsed time in 10us ticks and here it is converted to a double-value in milliseconds // for comparison with the sorted spike times that are in CXFILEDATA.pdSortedSpikes[]. These operations can // introduce a very tiny error, so that the test for equality between double values might fail even though the two // times are much less than 1us apart. (I actually encountered this problem during testing!) Therefore, both methods // assume equality if the absolute value of the difference is less than 0.001ms (ie, 1us). // // ARGS: ch -- The sorted-spike train channel, in [0..NUMSPIKESORTCH). If the corresponding buffer was not // allocated, then the file contains no data for that channel, and so no action is taken (this should not happen!). // tSpk -- The time of the spike to be added or removed, in 10us ticks elapsed since recording began. If // spike is to be added and there's already a spike there, addSortedSpike() does nothing. If spike is to be removed // and there is no spike at that time, removeSortedSpike() does nothing. // void removeSortedSpike(int ch, int tSpk) { int i, iRmv; double tSpkMS, diff; int* pnSpikes; // ptrs to appropriate internal buf, its size, int* pnBufSz; // and current count -- b/c the task is the double** ppdBuf; // same for each sorted spike channel rec type. // abort if invalid ch# or if we did not find any data in the file for the specified channel if(ch < 0 || ch >= NUMSPIKESORTCH) return; if(cxData.nSortedBufSz[ch] == 0) return; // #spikes, buffer size, and buffer for the relevant sorted-spike train. Spike times are in ms in this buffer! pnSpikes = &(cxData.nSortedSpikes[ch]); pnBufSz = &(cxData.nSortedBufSz[ch]); ppdBuf = &(cxData.pdSortedSpikes[ch]); // find index pos of spike to be removed. If not found, abort. tSpkMS = ((double)tSpk) / 100.0; iRmv = -1; i = 0; while(i < *pnSpikes) { // conversion to double causes inaccuracies. If times are less than 1us (0.001ms) apart, they're identical! diff = tSpkMS - (*ppdBuf)[i]; if(-0.001 <= diff && diff <= 0.001) { iRmv = i; break; } ++i; } if(iRmv < 0) return; // erase the removed spike by shifting later spikes back one for(i = iRmv;i < *pnSpikes - 1; i++) (*ppdBuf)[i] = (*ppdBuf)[i+1]; *pnSpikes = *pnSpikes - 1; } void addSortedSpike(int ch, int tSpk) { int i, iAdd; double tSpkMS, diff; int* pnSpikes; // ptrs to appropriate internal buf, its size, int* pnBufSz; // and current count -- b/c the task is the double** ppdBuf; // same for each sorted spike channel rec type. double* pdNewBuf; // ptr to reallocated buffer, if needed if(ch < 0 || ch >= NUMSPIKESORTCH) return; if(cxData.nSortedBufSz[ch] == 0) return; // #spikes, buffer size, and buffer for the relevant sorted-spike train. Spike times are in ms in this buffer! pnSpikes = &(cxData.nSortedSpikes[ch]); pnBufSz = &(cxData.nSortedBufSz[ch]); ppdBuf = &(cxData.pdSortedSpikes[ch]); // find insert pos of spike to be added, maintaining chronological order. If a spike is already at that time, abort. tSpkMS = ((double)tSpk) / 100.0; iAdd = 0; while((iAdd < *pnSpikes) && ((*ppdBuf)[iAdd] < tSpkMS)) ++iAdd; // ensure that spike time is not within 1us of the two times between which it is being inserted. This means that the // spike is aleady there and does not need to be added! The conversions to double values cause inaccuracies -- see // comment header. if(iAdd > 0) { diff = tSpkMS - (*ppdBuf)[iAdd-1]; if(-0.001 <= diff && diff <= 0.001) return; } if(iAdd < *pnSpikes) { diff = tSpkMS - (*ppdBuf)[iAdd]; if(-0.001 <= diff && diff <= 0.001) return; } // make room for the spike to be added by shifting later spikes forward one spot. Might have to reallocate buffer! if(*pnSpikes == *pnBufSz) { pdNewBuf = (double*) realloc( (void*) *ppdBuf, sizeof(double)*(100 + *pnBufSz) ); if(pdNewBuf == NULL) { printf( "ERROR: Internal buffer reallocation failed while adding a sorted spike; op failed!\n" ); return; } *ppdBuf = pdNewBuf; *pnBufSz += 100; } for(i = *pnSpikes; i > iAdd; i--) (*ppdBuf)[i] = (*ppdBuf)[i-1]; // insert the spike time and increment spike count (*ppdBuf)[iAdd] = tSpkMS; *pnSpikes = *pnSpikes + 1; } //=== cutEyeVelocityTraces (added 10jan2008) ========================================================================== // // Modify any eye velocity data channels (HEVEL, VEVEL, HDVEL) currently stored in the 'data' field of the output // structure IAW the per-channel saccade cuts that were processed from the data file's action records and stored in // the 'cut' field. // // For each saccade cut found, the method replaces the saccade in the trace with a straight line connecting the // cut's endpoints -- exactly as the trace is displayed in the programs that do per-channel saccade cutting, XWork // and its Java-based successor, JMWork. Any saccade cut defined on a channel other than HEVEL, VEVEL or HDVEL is // ignored, since it makes no sense to cut saccades on any other data channels collected in Maestro. // // This method must be called AFTER the 'data' and 'cut' fields have been prepared. It is also important that it // be called AFTER processTrialCodes(), since that method may access the eye trajectory data to compute target // trajectories during velocity stabilization. It is important that ORIGINAL raw trajectory data be used for that // task! // // ARGS: pOut -- [in/out] ptr to the MATLAB structure array prepared by readcxdata(). // void cutVelocityTraces(mxArray* pOut) { int i, k; int nCuts; // number of per-channel cuts defined double* pdCuts; // per-channel cuts: t0[0..N-1], t1[0..N-1], ch#[0..N-1] int t0, t1, ch; // the start tick, end tick, and affected channel for a saccade cut int toTicks; // divide cut times by this factor to convert to ticks (1 or 2ms) double* pdRecorded; // recorded AI data: ch1[0],...chM[0], ch1[1], ... chM[1], ... int chHEVEL; // if nonnegative, this is ordinal pos of HEVEL in recorded channel list int chVEVEL; // if nonnegative, this is ordinal pos of VEVEL in recorded channel list int chHDVEL; // if nonnegative, this is ordinal pos of HDVEL in recorded channel list int nChan, chPos, velAtT0, dy, dt; // these are used to linearly interpolate between saccade cut endpoints mxArray* pMxArray; // get array of all recorded analog data, plus the ordinal pos of HEVEL, VEVEL, and HDVEL in recorded channel list. // We will modify the contents of the MATLAB array containing the analog data through the pointer provided! chHEVEL = -1; chVEVEL = -1; chHDVEL = -1; pdRecorded = NULL; if(cxData.fileHdr.nBytesCompressed > 0 && cxData.fileHdr.nchans > 0) { pMxArray = mxGetField(pOut, 0, "data"); if(pMxArray != NULL) pdRecorded = mxGetPr(pMxArray); for(i = 0; i= t1 || t0 < 0 || t1 >= cxData.fileHdr.nScansSaved) continue; chPos = -1; if(ch == HEVEL) chPos = chHEVEL; else if(ch == VEVEL) chPos = chVEVEL; else if(ch == HDVEL) chPos = chHDVEL; if(chPos < 0) continue; if(iVerbose) printf("Cutting %s between t=%d and %d ms.\n", ((ch==HEVEL) ? "HEVEL" : ((ch==VEVEL) ? "VEVEL" : "HDVEL")), t0*toTicks, t1*toTicks); velAtT0 = (int) pdRecorded[t0 * nChan + chPos]; dy = ((int) pdRecorded[t1 * nChan + chPos]) - velAtT0; dt = t1 - t0; for(k=t0+1; k= 12, in which one or more XYScope or RMVideo noisy-dots targets participate. Note that there cannot be a mix of noisy-dots targets from both display platforms! This method initializes the emulator parameters and adds all participating noisy-dots targets to it. If an error occurs while initializing the emulator, then emulation is disabled. In this case a warning message is printed to STDOUT. For details, see the NOISYEM.* module */ void initializeNoisyDotsEmulator() { int len, d, w, h, i; double rmvFP_millisec; DWORD seed; BOOL ok, gotXY, gotRMV; CXFILETGT* pTgt; CXFILETGT_V12* pTgtV12; CXFILETGT_V22* pTgtV22; NOISYTGTINFO nti; len = 0; d = w = h = 100; rmvFP_millisec = 10; seed = 0; ok = (cxData.fileHdr.version >= 12) && ((cxData.fileHdr.flags & CXHF_ISCONTINUOUS) == 0) && (cxData.fileHdr.nScansSaved > 0) && (cxData.nTargets > 0); gotXY = FALSE; gotRMV = FALSE; if(ok) { // make sure trial includes at least one XYScope OR RMVideo noisy dots target, AND NOT a mix from both displays for(i=0; i= 23) { pTgt = &(cxData.pTargets[i]); if(!gotXY) { gotXY = (pTgt->def.wType == CX_XYTARG) && (pTgt->def.u.xy.type == NOISYDIR || pTgt->def.u.xy.type == NOISYSPEED); } if(!gotRMV) { gotRMV = (pTgt->def.wType == CX_RMVTARG) && (pTgt->def.u.rmv.iType == RMV_RANDOMDOTS) && (pTgt->def.u.rmv.iNoiseUpdIntv > 0) && (pTgt->def.u.rmv.iNoiseLimit > 0); } } else if(cxData.fileHdr.version >= 13) { pTgtV22 = &(cxData.pTargets_V22[i]); if(!gotXY) { gotXY = (pTgtV22->def.wType == CX_XYTARG) && (pTgtV22->def.u.xy.type == NOISYDIR || pTgtV22->def.u.xy.type == NOISYSPEED); } if(!gotRMV) { gotRMV = (pTgtV22->def.wType == CX_RMVTARG) && (pTgtV22->def.u.rmv.iType == RMV_RANDOMDOTS) && (pTgtV22->def.u.rmv.iNoiseUpdIntv > 0) && (pTgtV22->def.u.rmv.iNoiseLimit > 0); } } else { pTgtV12 = &(cxData.pTargets_V12[i]); if(!gotXY) { gotXY = (pTgtV12->def.wType == CX_XYTARG) && (pTgtV12->def.u.xy.type == NOISYDIR || pTgtV12->def.u.xy.type == NOISYSPEED); } if(!gotRMV) { gotRMV = (pTgtV12->def.wType == CX_RMVTARG) && (pTgtV12->def.u.rmv.iType == RMV_RANDOMDOTS) && (pTgtV12->def.u.rmv.iNoiseUpdIntv > 0) && (pTgtV12->def.u.rmv.iNoiseLimit > 0); } } // cannot have XYScope and RMVideo targets both participating in the same trial! if(gotXY && gotRMV) { printf("WARNING: XYScope and RMVideo target participating in the same trial?!\n"); break; } } ok = (gotXY || gotRMV) && !(gotXY && gotRMV); } if(ok) { len = cxData.fileHdr.nScansSaved; d = cxData.fileHdr.d_dist; w = cxData.fileHdr.d_dwidth; h = cxData.fileHdr.d_dheight; seed = cxData.fileHdr.dwXYSeed; // units for RMVideo frame rate: milli-Hz (V<22) or micro-Hz (V>=22): if(gotRMV) rmvFP_millisec = (cxData.fileHdr.version<22 ? 1.0e6 : 1.0e9) / ((double) cxData.fileHdr.d_framerate); } initNoisyDotsEmulator(gotXY, cxData.fileHdr.version, len, d, w, h, seed, rmvFP_millisec); if(!ok) return; // now add the noisy-dots targets to the emulator object for(i=0; ok && i= 23) { pTgt = &(cxData.pTargets[i]); if((pTgt->def.wType == CX_XYTARG) && (pTgt->def.u.xy.type == NOISYDIR || pTgt->def.u.xy.type == NOISYSPEED)) { nti.type = EMU_NOISYDIR; if(pTgt->def.u.xy.type == NOISYSPEED) nti.type = (pTgt->def.u.xy.fInnerX != 0) ? EMU_NOISYSPD_MUL : EMU_NOISYSPD_ADD; nti.level = (int) pTgt->def.u.xy.fInnerW; nti.updIntv = (int) pTgt->def.u.xy.fInnerH; nti.nDots = pTgt->def.u.xy.ndots; ok = addNoisyDotsTarget(i, &nti); } else if((pTgt->def.wType == CX_RMVTARG) && (pTgt->def.u.rmv.iType == RMV_RANDOMDOTS) && (pTgt->def.u.rmv.iNoiseUpdIntv > 0) && (pTgt->def.u.rmv.iNoiseLimit > 0)) { if((pTgt->def.u.rmv.iFlags & RMV_F_DIRNOISE) != 0) nti.type = EMU_NOISYDIR; else nti.type = ((pTgt->def.u.rmv.iFlags & RMV_F_SPDLOG2) != 0) ? EMU_NOISYSPD_MUL : EMU_NOISYSPD_ADD; nti.level = pTgt->def.u.rmv.iNoiseLimit; nti.updIntv = pTgt->def.u.rmv.iNoiseUpdIntv; nti.nDots = pTgt->def.u.rmv.nDots; nti.iFlags = pTgt->def.u.rmv.iFlags; nti.iPctCoherent = pTgt->def.u.rmv.iPctCoherent; nti.fDotLife = pTgt->def.u.rmv.fDotLife; nti.iSeed = pTgt->def.u.rmv.iSeed; nti.fOuterW = pTgt->def.u.rmv.fOuterW; nti.fOuterH = pTgt->def.u.rmv.fOuterH; ok = addNoisyDotsTarget(i, &nti); } } else if(cxData.fileHdr.version >= 13) { pTgtV22 = &(cxData.pTargets_V22[i]); if((pTgtV22->def.wType == CX_XYTARG) && (pTgtV22->def.u.xy.type == NOISYDIR || pTgtV22->def.u.xy.type == NOISYSPEED)) { nti.type = EMU_NOISYDIR; if(pTgtV22->def.u.xy.type == NOISYSPEED) nti.type = (pTgtV22->def.u.xy.fInnerX != 0) ? EMU_NOISYSPD_MUL : EMU_NOISYSPD_ADD; nti.level = (int) pTgtV22->def.u.xy.fInnerW; nti.updIntv = (int) pTgtV22->def.u.xy.fInnerH; nti.nDots = pTgtV22->def.u.xy.ndots; ok = addNoisyDotsTarget(i, &nti); } else if((pTgtV22->def.wType == CX_RMVTARG) && (pTgtV22->def.u.rmv.iType == RMV_RANDOMDOTS) && (pTgtV22->def.u.rmv.iNoiseUpdIntv > 0) && (pTgtV22->def.u.rmv.iNoiseLimit > 0)) { if((pTgtV22->def.u.rmv.iFlags & RMV_F_DIRNOISE) != 0) nti.type = EMU_NOISYDIR; else nti.type = ((pTgtV22->def.u.rmv.iFlags & RMV_F_SPDLOG2) != 0) ? EMU_NOISYSPD_MUL : EMU_NOISYSPD_ADD; nti.level = pTgtV22->def.u.rmv.iNoiseLimit; nti.updIntv = pTgtV22->def.u.rmv.iNoiseUpdIntv; nti.nDots = pTgtV22->def.u.rmv.nDots; nti.iFlags = pTgtV22->def.u.rmv.iFlags; nti.iPctCoherent = pTgtV22->def.u.rmv.iPctCoherent; nti.fDotLife = pTgtV22->def.u.rmv.fDotLife; nti.iSeed = pTgtV22->def.u.rmv.iSeed; nti.fOuterW = pTgtV22->def.u.rmv.fOuterW; nti.fOuterH = pTgtV22->def.u.rmv.fOuterH; ok = addNoisyDotsTarget(i, &nti); } } else { pTgtV12 = &(cxData.pTargets_V12[i]); if((pTgtV12->def.wType == CX_XYTARG) && (pTgtV12->def.u.xy.type == NOISYDIR || pTgtV12->def.u.xy.type == NOISYSPEED)) { nti.type = EMU_NOISYDIR; if(pTgtV12->def.u.xy.type == NOISYSPEED) nti.type = (pTgtV12->def.u.xy.fInnerX != 0) ? EMU_NOISYSPD_MUL : EMU_NOISYSPD_ADD; nti.level = (int) pTgtV12->def.u.xy.fInnerW; nti.updIntv = (int) pTgtV12->def.u.xy.fInnerH; nti.nDots = pTgtV12->def.u.xy.ndots; ok = addNoisyDotsTarget(i, &nti); } else if((pTgtV12->def.wType == CX_RMVTARG) && (pTgtV12->def.u.rmv.iType == RMV_RANDOMDOTS) && (pTgtV12->def.u.rmv.iNoiseUpdIntv > 0) && (pTgtV12->def.u.rmv.iNoiseLimit > 0)) { if((pTgtV12->def.u.rmv.iFlags & RMV_F_DIRNOISE) != 0) nti.type = EMU_NOISYDIR; else nti.type = ((pTgtV12->def.u.rmv.iFlags & RMV_F_SPDLOG2) != 0) ? EMU_NOISYSPD_MUL : EMU_NOISYSPD_ADD; nti.level = pTgtV12->def.u.rmv.iNoiseLimit; nti.updIntv = pTgtV12->def.u.rmv.iNoiseUpdIntv; nti.nDots = pTgtV12->def.u.rmv.nDots; nti.iFlags = pTgtV12->def.u.rmv.iFlags; nti.iPctCoherent = pTgtV12->def.u.rmv.iPctCoherent; nti.fDotLife = pTgtV12->def.u.rmv.fDotLife; nti.iSeed = pTgtV12->def.u.rmv.iSeed; nti.fOuterW = pTgtV12->def.u.rmv.fOuterW; nti.fOuterH = pTgtV12->def.u.rmv.fOuterH; ok = addNoisyDotsTarget(i, &nti); } } if(!ok) { releaseNoisyDotsEmulator(); printf( "WARNING: Unable to add target %d to noisy-dots emulator. Emulation unavailable.\n", i ); } } } /** For selected video target types, a sudden large "instantaneous" change in target window position (as can occur at the beginning of a new trial segment) requires a commensurate change in the target's pattern position, or the target's behavior is compromised. This is the case for the XYScope FASTCENTER, FCDOTLIFE, NOISYDIR, COHERENTFC, and NOISYSPEED targets for data files generated prior to Maestro v2.7.0 (file version 18). As of Maestro 2.7.0, XYScope target pattern motion is specified WRT the target window rather than the screen. The change still must be applied to the RMVideo RMV_RANDOMDOTS target IF its RMV_F_WRTSCREEN flag isset. This method returns TRUE if the specified target must be adjusted accordingly. @param pos Index of target in the target definitions array within CXFILEDATA. @return TRUE for the selected target types described, FALSE otherwise. */ BOOL shouldAdjustPatternMotionAtSegStart(int pos) { BOOL adjust; int t; CXFILETGT* pTgt; CXFILETGT_V22* pTgtV22; CXFILETGT_V12* pTgtV12; CXFILETGT_V7* pTgtV7; // check for invalid target index and handle changes in target definition structure over time... adjust = FALSE; if(cxData.fileHdr.version >= 2 && pos >= 0 && pos < cxData.nTargets) { if(cxData.fileHdr.version >= 23) { pTgt = &(cxData.pTargets[pos]); if(pTgt->def.wType == CX_XYTARG && cxData.fileHdr.version < 18) { t = pTgt->def.u.xy.type; adjust = (t==FASTCENTER || t==FCDOTLIFE || t==NOISYDIR || t==COHERENTFC || t==NOISYSPEED); } else if(pTgt->def.wType == CX_RMVTARG) { adjust = (pTgt->def.u.rmv.iType == RMV_RANDOMDOTS) && ((pTgt->def.u.rmv.iFlags & RMV_F_WRTSCREEN) != 0); } } else if(cxData.fileHdr.version >= 13) { pTgtV22 = &(cxData.pTargets_V22[pos]); if(pTgtV22->def.wType == CX_XYTARG && cxData.fileHdr.version < 18) { t = pTgtV22->def.u.xy.type; adjust = (t==FASTCENTER || t==FCDOTLIFE || t==NOISYDIR || t==COHERENTFC || t==NOISYSPEED); } else if(pTgtV22->def.wType == CX_RMVTARG) { adjust = (pTgtV22->def.u.rmv.iType == RMV_RANDOMDOTS) && ((pTgtV22->def.u.rmv.iFlags & RMV_F_WRTSCREEN) != 0); } } else if(cxData.fileHdr.version >= 8) { pTgtV12 = &(cxData.pTargets_V12[pos]); if(pTgtV12->def.wType == CX_XYTARG) { t = pTgtV12->def.u.xy.type; adjust = (t==FASTCENTER || t==FCDOTLIFE || t==NOISYDIR || t==COHERENTFC || t==NOISYSPEED); } else if(pTgtV12->def.wType == CX_RMVTARG) { adjust = (pTgtV12->def.u.rmv.iType == RMV_RANDOMDOTS) && ((pTgtV12->def.u.rmv.iFlags & RMV_F_WRTSCREEN) != 0); } } else { pTgtV7 = &(cxData.pTargets_V7[pos]); if(pTgtV7->def.wType == CX_XYTARG) { t = pTgtV7->def.u.xy.type; adjust = (t==FASTCENTER || t==FCDOTLIFE || t==NOISYDIR || t==COHERENTFC || t==NOISYSPEED); } } } return(adjust); } /** For all XYScope target types except FLOWFIELD, and for the RMVideo RMV_RANDOMDOTS target with the RMV_F_WRTSCREEN flag set, MaestroDRIVER adjusts the target pattern motion by the same amount that the target window motion is adjusted when that target is velocity-stabilized. This method returns TRUE if the specified target is one of these, but only if the data file was recorded on or after 20sep2005, when Maestro v1.3.3 was released. Prior to that version, MaestroDRIVER did not make this adjustment. 17may2011: As of Maestro v2.7.0 (file version 18), XYScope target pattern motion is now specified WRT the target window rather than the screen. VStab compensation is done solely by adjusting target window motion. Thus, for data file version >= 18, the only target whose pattern motion is still adjusted during VStab is the RMV_RANDOOMDOTS target when its RMV_F_WRTSCREEN flag is set. @param pos Index of target in the target definitions array within CXFILEDATA. @return TRUE if the described conditions are satisfied, FALSE otherwise. */ BOOL shouldAdjustPatternMotionDuringVStab(int pos) { BOOL adjust; CXFILETGT* pTgt; CXFILETGT_V22* pTgtV22; CXFILETGT_V12* pTgtV12; CXFILETGT_V7* pTgtV7; // the target pattern motion adjustment during VStab was not introduced until Maestro v1.3.3, dtd 20sep2005 adjust = cxData.fileHdr.version > 5 || (cxData.fileHdr.version == 5 && (cxData.fileHdr.yearRecorded > 2005 || (cxData.fileHdr.yearRecorded == 2005 && (cxData.fileHdr.monthRecorded > 9 || (cxData.fileHdr.monthRecorded == 9 && cxData.fileHdr.dayRecorded >= 20))))); // check for invalid target index and handle changes in target definition structure over time... adjust = adjust && pos >= 0 && pos < cxData.nTargets; if(adjust) { if(cxData.fileHdr.version >= 23) { pTgt = &(cxData.pTargets[pos]); adjust = (pTgt->def.wType == CX_RMVTARG) && (pTgt->def.u.rmv.iType == RMV_RANDOMDOTS) && ((pTgt->def.u.rmv.iFlags & RMV_F_WRTSCREEN) != 0); } else if(cxData.fileHdr.version >= 13) { pTgtV22 = &(cxData.pTargets_V22[pos]); if(pTgtV22->def.wType == CX_XYTARG && cxData.fileHdr.version < 18) adjust = (pTgtV22->def.u.xy.type != FLOWFIELD); else if(pTgtV22->def.wType == CX_RMVTARG) adjust = (pTgtV22->def.u.rmv.iType == RMV_RANDOMDOTS) && ((pTgtV22->def.u.rmv.iFlags & RMV_F_WRTSCREEN) != 0); } else if(cxData.fileHdr.version >= 8) { pTgtV12 = &(cxData.pTargets_V12[pos]); if(pTgtV12->def.wType == CX_XYTARG) adjust = (pTgtV12->def.u.xy.type != FLOWFIELD); else if(pTgtV12->def.wType == CX_RMVTARG) adjust = (pTgtV12->def.u.rmv.iType == RMV_RANDOMDOTS) && ((pTgtV12->def.u.rmv.iFlags & RMV_F_WRTSCREEN) != 0); } else { pTgtV7 = &(cxData.pTargets_V7[pos]); if(pTgtV7->def.wType == CX_XYTARG) adjust = (pTgtV7->def.u.xy.type != FLOWFIELD); } } return(adjust); } //=== processTrialCodes =============================================================================================== // // Process all trial codes stored in the relevant internal buffer and calculate the expected trajectories of all // targets that participated in the trial. These trial target trajectories and the old-style "target IDs" of the // participating trial targets are then stored in the "targets" field within the output structure generated by // readcxdata(). // // The old-style "target IDs" were used by cntrlxUNIX/PC and included dedicated IDs for "hard" targets such as // FIBER1, FIBER2, etc. MAESTRO (data file version >= 2) does not use these IDs -- the target number stored in the // trial codes refers to the target's position in the trial target list. To further identify the target, we must // look at the target definitions. These definitions should already have been read into CXFILEDATA. Regardless of // file version, we still prepare the list of old-style target IDs -- these have been part of the output from past // incarnations of readcxdata(), and downstream code may use them! // // The function also saves, in our global internal buffer 'cxData', the number of segments in the trial, the start // time of each segment, and the trial time at which recording began. This information is useful in preparing the // 'tagSections' output field -- see setTagSections(). // // With Maestro file version >=5, the algorithm for generating noise perturbations is reproducible. This function // now accounts for the effects of perturbation waveforms (TARGET_PERTURB code group) so long as the data file // version is >= 5. See the PERTMGR module. // // With Maestro file version >=7 (as of app v1.5.0), velocity stabilization can occur over a contiguous span of // segments instead of just one. The method takes this into account when computing the trajectory of a // velocity-stabilized target. // // With Maestro file version >=8 (as of app v2.0.0), velocity stabilization can now be turned on/off on a // per-target and per-segment basis. This method was updated accordingly. // // As of Maestro v2.1.0 (file version = 9, introduced in app v2.0.1), additional trial codes INSIDE_***ACC // support per-segment specification of pattern acceleration. Note that the INSIDE_VACC code is identical to a // very old code, VSYNC_PULSE. As long as file version is 9+, the code is safely interpreted as INSIDE_VACC. // // As of 7/22/08, output field 'psgm' is initialized IAW PSGM sequence parameters gleaned from trial code group // PSGM_TC. Retroactive to data file version 10, app v2.1.1 -- even though the PSGM hardware was only recently // installed in a lab rig. // // 13apr2011-May2011: BUG FIXES AND OTHER CHANGES // 1) In April 2011 we uncovered problems with the implementation of the XYScope NOISYDIR and NOISYSPEED targets. We // decided it would be useful to be able to replicate the erroneous implementation in READCXDATA in order to // properly evaluate recorded neural and behavioral responses to these target stimuli. The functions that do this // are in the new XYNOISYEM.* module. // // 2) In prior versions, this method used the recorded HEVEL and VEVEL to adjust the horizontal and vertical // velocity of a non-video target when that target was velocity-stabilized. I think this was an attempt to account // for the effects of velocity stabilization in the target velocity traces generated by READCXDATA, while trying to // keep them as smooth as possible. In any case, the adjusted velocity vector was fed to the perturbation manager -- // which meant that any applied velocity perturbations would be messed up. In MaestroDRIVER, all perturbations are // processed while generating the precomputed target trajectories. Furthermore, MaestroDRIVER only uses HGPOS and // VEPOS for VStab. ProcessTrialCodes() has been modified to do the same. // // 3) Prior to Maestro v2.7.0 (data file version 18), XYScope target pattern motion was specified WRT the screen, // not the target window. The same is true for the RMVideo target RMV_RANDOMDOTS when the RMV_F_WRTSCREEN flag is // set. As a consequence, MaestroDRIVER must adjust BOTH the target window displacement and the target pattern // displacement each frame to account for velocity stabilization of that target. This adjustment messed up the // intended behavior of the NOISYDIR and NOISYSPEED targets. In order to emulate the trajectories of the individual // dots in these two target types, processTrialCodes() now makes the same adjustments. This means that the // targets.patVelH/V fields will show the effects of velocity stabilization for XYScope targets and the // aforementioned RMVideo target. This change only applies to data files with version < 18 generated after 20sep2005 // (Maestro v1.3.3); prior to this date, MaestroDRIVER did not make this particular adjustment. // // 4) Prior to Maestro v2.7.0, the target pattern of certain XYScope target types (FASTCENTER, COHERENTFC, FCDOTLIFE, // NOISYDIR, and NOISYSPEED) was offset by the change in target window position AT THE START OF A SEGMENT. If this // was not done, their operation was compromised. The same applies to the RMVideo target RMV_RANDOMDOTS when the // RMV_F_WRTSCREEN flag is set. ProcessTrialCodes() now accounts for this tweak, for data files with version 2-17 // (target definitions were added to the data file as of version 2). // // 5) As of Maestro v2.7.0 (data file version = 18), all XYScope target pattern motion is specified WRT the target // window, NOT the screen. VStab is now accomplished in MaestroDRIVER by adjusting target window position only. The // tweaks described in (2) and (3) are no longer done (except for the RMVideo RMV_RANDOMDOTS target when the flag // RMV_F_WRTSCREEN is set). In addition, this change means that the output fields target.patVelH/V will contain the // target pattern velocity trajectory WRT the target window. As such these fields will not show the effects of // velocity stabilization. // // LIMITATIONS: // 1) We make a concerted effort here to accurately calculate the "expected" trajectories of tgts participating in // the trial. However, some trial operations -- such as "skipOnSaccade" -- preclude such calculations. Warning // messages are printed to STDOUT in such cases. Note that we compensate the position and velocity of a target // that is subject to velocity stabilization wrt the eye ("open loop"). // 2) We also do NOT bother to account for the effects of pixelization on the actual pos & vel of XY scope, VSG FB // video, or RMVideo targets. We DO, however, attempt to account for the frame update intervals of the video // displays; these are both longer than a single time "epoch" (1ms) in TrialMode. We assume that only one video // display is used during a trial -- not an unreasonable premise. // 3) We do NOT try to account for the effects of interleaving XY scope tgts, another special feature in TrialMode. // // ARGS: pOut -- [in/out] ptr to the MATLAB structure array prepared by readcxdata(). // // RETURNS: TRUE if successful; FALSE otherwise (encountered an unrecognized trial code). // BOOL processTrialCodes( mxArray* pOut ) { static BOOL bIsOn[MAX_TRIALTARGS]; // is trial tgt currently turned on? static int onEpochs[MAX_TRIALTARGS][MAX_SEGMENTS]; static double dhPos[MAX_TRIALTARGS]; // trial tgt pos,vel,acc at the current trial time (deg, deg/s, deg/s^2) static double dvPos[MAX_TRIALTARGS]; // (declared static to avoid possible call stack overflows) static double dhVel[MAX_TRIALTARGS]; static double dvVel[MAX_TRIALTARGS]; static double dhAcc[MAX_TRIALTARGS]; static double dvAcc[MAX_TRIALTARGS]; static double dhPatVel[MAX_TRIALTARGS]; // trial tgt pattern velocity at current trial time in deg/s static double dvPatVel[MAX_TRIALTARGS]; static double dhPatAcc[MAX_TRIALTARGS]; // trial tgt pattern acceleration at current trial time in deg/s^2 static double dvPatAcc[MAX_TRIALTARGS]; static double dhPatPos[MAX_TRIALTARGS]; // so we can calc net pattern velocity over each frame interval for any static double dvPatPos[MAX_TRIALTARGS]; // video targets static double dhPosPrev[MAX_TRIALTARGS]; // trial tgt pos during the previous trial tick (deg static double dvPosPrev[MAX_TRIALTARGS]; static double dhLastPos[MAX_TRIALTARGS]; // remember tgt window/pattern pos at last video frame update static double dvLastPos[MAX_TRIALTARGS]; static double dhLastPatPos[MAX_TRIALTARGS]; static double dvLastPatPos[MAX_TRIALTARGS]; static double dhPertD[MAX_TRIALTARGS]; // net perturbation in tgt vel at current trial time in deg/s static double dvPertD[MAX_TRIALTARGS]; static double dhPatPertD[MAX_TRIALTARGS]; // net perturbation in tgt pattern vel at current trial time in deg/s static double dvPatPertD[MAX_TRIALTARGS]; static int iLastVSMask[MAX_TRIALTARGS]; // per-target velocity stabilization mask for previous segment static int iCurrVSMask[MAX_TRIALTARGS]; // per-target velocity stabilization mask for current segment int i, j, k, m, n; double dTemp, dH, dV, dHPat, dVPat; BOOL bDone; BOOL bWasOn; int nID; // old-style target ID BOOL bXYScopeUsed; // TRUE if at least one XY scope target is used in the trial BOOL bXYMoveWhileOff; // TRUE if XY scope targets move while off (Maestro v1.2.1 dtd 10may04) int iNextXYFrame, iCurrXYFrame; // next and current XY frame update interval, in ms; we need both to // handle transition at trial segment boundaries double dCurrTimeMS; // we use these to keep track of frame updates on the XY or FB video double dLastVideoUpdateMS; // displays; since the video update intervals on these platforms is double dVideoFrameMS; // longer than a trial tick! (FB frame period may not be an integer!) int iLastVideoUpdateMS; BOOL bDoVideoUpdate; // int iTick; // the current trial time (# of AI scans elapsed) int iRecOnTick; // trial time at which data recording began (not necessarily T=0!!) double dTickDur; // the duration of one trial tick, in seconds.we double* pdTgtPosH; // ptrs to trial tgt pos & vel trajectories as stored in MATLAB matrices. double* pdTgtPosV; // storage order: hPos0(0), .., hPosM(0), hPos0(1), .., hPosM(1), ....., double* pdTgtVelH; // hPos0(N-1), .., hPosM(N-1), where N = nTrialRecordLen and M+1 = #tgts double* pdTgtVelV; // participating in the trial. Pos in deg, vel in deg/sec. double* pdPatVelH; // similarly for trial tgt pattern vel trajectory double* pdPatVelV; // double* pdTgtIDs; // ptr to MATLAB array holding old-style tgt IDs mxArray* pMXTraj; // ptr to MATLAB structure array holding trial tgt trajectory data mxArray* pMXPSGM; // ptr to MATLAB structure array holding trial's PSGM parameters mxArray* pMXOnEpochs; // ptr to MATLAB cell array holding target ON epochs [on1 off1 on2 off2 ...] mxArray* pMXEpochTimes; // ptr to MATLAB vector holding target ON epoch times for one target double* pdEpochTimes; // we need these to handle TARGET_HOPEN, in data files w/ V <= 7 int iOpenSeg; // index of first velocity-stabilization ("open loop") segment, if any int iOpenTgt; // the tgt stabilized (must be fix tgt #1) int nOpenSegs; // # of contiguous segs in which v.stab is active (file vers==7) WORD openFlags; // OPEN_SNAPTOEYE, OPEN_HONLY, OPEN_VONLY int iCurrSeg; // index of current trial segment double dLastEyePosH, dLastEyePosV; // pos of eye during previous tick double dCurrEyePosH, dCurrEyePosV; // pos of eye during current tick (HGPOS, VEPOS) BOOL bGotEye; // flag set for each tick we recorded HGPOS,VEPOS -- for VStab comp int iFix1; // index into trial tgt list of tgt designated as fixation tgt #1 BOOL bStartNewSeg; // TRUE when we're starting a new segment of the trial double* pdRecorded; // recorded AI data: ch1[0],...chM[0], ch1[1], ... chM[1], ... int chHGPOS; // if nonnegative, this is ordinal pos of HGPOS in recorded channel list int chVEPOS; // if nonnegative, this is ordinal pos of VEPOS in recorded channel list BOOL bEnaVStabComp; // flag unset if we encounter a prob and can't do VStab compensation BOOL bNoisyDotsEmuOn; // flag set if noisy-dots targets are to be emulated resetPertManager( &G_pertMgr ); // reset perturbation manager prepareTgtIDs(); // prepare list of old-style IDs for targets // participating in the trial. bEnaVStabComp = TRUE; // unset flag if can't do VStab compensation bGotEye = FALSE; chHGPOS = -1; // get ordinal pos in recorded channel list chVEPOS = -1; // of AI channels HGPOS, VEPOS. pdRecorded = NULL; if( cxData.fileHdr.nBytesCompressed > 0 && cxData.fileHdr.nchans > 0 ) { pdRecorded = mxGetPr( mxGetField( pOut, 0, "data" ) ); // so we can access recorded data! for( i = 0; i 3 || // until Maestro 1.2.1. we have to check the (cxData.fileHdr.version == 3 && // date b/c we did not update the data file (cxData.fileHdr.yearRecorded > 2004 || // version# with Maestro 1.2.1 (cxData.fileHdr.yearRecorded == 2004 && (cxData.fileHdr.monthRecorded > 3 || (cxData.fileHdr.monthRecorded == 3 && cxData.fileHdr.dayRecorded >= 10))))); bNoisyDotsEmuOn = isNoisyDotsEmulatorEnabled(); // are we emulating any noisy-dots targets? bDone = FALSE; // build trial tgt trajectories step by step, iTick = 0; // processing trial codes as we go... i = 0; while( !bDone ) { if( i= 0 ) { bIsOn[k] = (cxData.pCodes[i].code == TARGET_ON) ? TRUE : FALSE; // update onEpochs array for the target. We don't assume that target was off when TARGET_ON is delivered // and vice versa. If #times entered into array is odd, target is ON; else it is OFF. bWasOn = (onEpochs[k][0] % 2) != 0; if(bIsOn[k] != bWasOn) { j = onEpochs[k][0]; onEpochs[k][j+1] = iTick; onEpochs[k][0] = j+1; } } i += 2; break; case TARGET_HVEL : // change tgt window's horiz velocity case TARGET_HSLOVEL : if( (k = mapTargetID( cxData.pCodes[i+1].code )) >= 0 ) { dTemp = (cxData.pCodes[i].code == TARGET_HVEL) ? d_TC_STDSCALE : d_TC_SLOSCALE1; dhVel[k] = ((double) cxData.pCodes[i+1].time) / dTemp; } i += 2; break; case TARGET_VVEL : // change tgt window's verti velocity case TARGET_VSLOVEL : if( (k = mapTargetID( cxData.pCodes[i+1].code )) >= 0 ) { dTemp = (cxData.pCodes[i].code == TARGET_VVEL) ? d_TC_STDSCALE : d_TC_SLOSCALE1; dvVel[k] = ((double) cxData.pCodes[i+1].time) / dTemp; } i += 2; break; case INSIDE_HVEL : // change tgt pattern's horiz velocity case INSIDE_HSLOVEL : if( (k = mapTargetID( cxData.pCodes[i+1].code )) >= 0 ) { dTemp = (cxData.pCodes[i].code == INSIDE_HVEL) ? d_TC_STDSCALE : d_TC_SLOSCALE1; dhPatVel[k] = ((double) cxData.pCodes[i+1].time) / dTemp; } i += 2; break; case INSIDE_VVEL : // change tgt pattern's verti velocity case INSIDE_VSLOVEL : if( (k = mapTargetID( cxData.pCodes[i+1].code )) >= 0 ) { dTemp = (cxData.pCodes[i].code == INSIDE_VVEL) ? d_TC_STDSCALE : d_TC_SLOSCALE1; dvPatVel[k] = ((double) cxData.pCodes[i+1].time) / dTemp; } i += 2; break; case INSIDE_HACC : // change tgt pattern's horiz acceleration case INSIDE_HSLOACC : if( (k = mapTargetID( cxData.pCodes[i+1].code )) >= 0 ) { dhPatAcc[k] = (double) cxData.pCodes[i+1].time; if( cxData.pCodes[i].code == INSIDE_HSLOACC ) dhPatAcc[k] /= d_TC_SLOSCALE2; } i += 2; break; case INSIDE_VACC : // change tgt pattern's verti acceleration case INSIDE_VSLOACC : if(cxData.pCodes[i].code == INSIDE_VACC && cxData.fileHdr.version < 9) { // INSIDE_VACC collides w/obsolete code VSYNC_PULSE mxDestroyArray( mxGetField( pMXTraj, 0, "hpos" ) ); mxDestroyArray( mxGetField( pMXTraj, 0, "vpos" ) ); mxDestroyArray( mxGetField( pMXTraj, 0, "hvel" ) ); mxDestroyArray( mxGetField( pMXTraj, 0, "vvel" ) ); mxDestroyArray( mxGetField( pMXTraj, 0, "patvelH" ) ); mxDestroyArray( mxGetField( pMXTraj, 0, "patvelV" ) ); mxDestroyArray( mxGetField( pMXTraj, 0, "targnums" ) ); mxDestroyArray( pMXTraj ); if( iVerbose ) printf( "WARNING: Unrecognized trial code %i (%i)! Cannot process trial codes!\n", cxData.pCodes[i].code, i ); return( FALSE ); } if( (k = mapTargetID( cxData.pCodes[i+1].code )) >= 0 ) { dvPatAcc[k] = (double) cxData.pCodes[i+1].time; if( cxData.pCodes[i].code == INSIDE_VSLOACC ) dvPatAcc[k] /= d_TC_SLOSCALE2; } i += 2; break; case TARGET_HPOSREL : // relative or absolute change in tgt case TARGET_HPOSABS : // window's horiz pos if( (k = mapTargetID( cxData.pCodes[i+1].code )) >= 0 ) { dTemp = ((double) cxData.pCodes[i+1].time); if(cxData.fileHdr.version >= 2) dTemp /= d_TC_SLOSCALE2; // scale factor was changed for vers >= 2!! else dTemp /= 66.66667; nID = cxData.oldTgtIDs[k]; // the old-style target ID if( (nID != TURNTABLE) && (nID < HARDTARGS || // chair CANNOT be repos'd instantaneously bIsOn[k] || bXYMoveWhileOff || !bXYScopeUsed) ) // XY tgts could not move while OFF until { // Maestro version 1.2.1 if( cxData.pCodes[i].code == TARGET_HPOSREL ) dhPos[k] += dTemp; else dhPos[k] = dTemp; } } i += 2; break; case TARGET_VPOSREL : // relative or absolute change in tgt case TARGET_VPOSABS : // window's verti pos if( (k = mapTargetID( cxData.pCodes[i+1].code )) >= 0 ) { dTemp = ((double) cxData.pCodes[i+1].time); if(cxData.fileHdr.version >= 2) dTemp /= d_TC_SLOSCALE2; // scale factor was changed for vers >= 2!! else dTemp /= 66.66667; nID = cxData.oldTgtIDs[k]; // the old-style target ID if( (nID != TURNTABLE) && (nID < HARDTARGS || // chair CANNOT be repos'd instantaneously bIsOn[k] || bXYMoveWhileOff || !bXYScopeUsed) ) // XY tgts could not move while OFF until { // Maestro version 1.2.1 if( cxData.pCodes[i].code == TARGET_VPOSREL ) dvPos[k] += dTemp; else dvPos[k] = dTemp; } } i += 2; break; case TARGET_HACC : // change tgt window's horiz acceleration case TARGET_HSLOACC : if( (k = mapTargetID( cxData.pCodes[i+1].code )) >= 0 ) { dhAcc[k] = (double) cxData.pCodes[i+1].time; if( cxData.pCodes[i].code == TARGET_HSLOACC ) dhAcc[k] /= d_TC_SLOSCALE2; } i += 2; break; case TARGET_VACC : // change tgt window's verti acceleration case TARGET_VSLOACC : if( (k = mapTargetID( cxData.pCodes[i+1].code )) >= 0 ) { dvAcc[k] = (double) cxData.pCodes[i+1].time; if( cxData.pCodes[i].code == TARGET_VSLOACC ) dvAcc[k] /= d_TC_SLOSCALE2; } i += 2; break; case TARGET_PERTURB : // handle target velocity pertubation (N=5) if( (k = mapTargetID( cxData.pCodes[i+1].code )) >= 0 ) { if( cxData.fileHdr.version < 5 ) { if( iVerbose ) { printf( "WARNING: The %i-th trial target is perturbed during trial. ", k ); printf( "Since perts aren't supported, its calc'd trajectory will be incorrect!\n" ); } } else if( !processPertCodes( &G_pertMgr, &(cxData.pCodes[i]) ) ) { if( iVerbose ) { printf( "WARNING: Problem occurred processing TARGET_PERTURB for %i-th target", k ); printf( "Its calc'd trajectory will be inaccurate!\n" ); } } } i += 5; break; case TARGET_HOPEN : // engage velocity stabilization; initialize if( iOpenSeg < 0 ) { iOpenSeg = iCurrSeg; openFlags = (WORD) cxData.pCodes[i+1].time; if( cxData.fileHdr.version >= 7 ) // as of Maestro 1.5.0, v.stab can act over nOpenSegs = (int) cxData.pCodes[i+1].code; // a contiguous range of trial segments! else nOpenSegs = 1; } else if( iVerbose ) printf( "WARNING: Ignored extra TARGET_HOPEN code!\n" ); // should get this code just once per trial i += 2; break; case TARGET_VSTAB : // (V>=8) VStab state of tgt changed if( (k = mapTargetID( cxData.pCodes[i+1].code )) >= 0 ) { iCurrVSMask[k] = (int) cxData.pCodes[i+1].time; } i += 2; break; case XYTARGETUSED : // indicates that trial uses XY scope tgt(s) bXYScopeUsed = TRUE; // (NOTE: we do NOT attempt to account for i += 2; // XY tgt interleaving!) break; case DELTAT : // change XY frame update interval iNextXYFrame = (int) cxData.pCodes[i+1].code; if( iTick == 0 ) iCurrXYFrame = iNextXYFrame; // (special case at trial's start) i += 2; break; case SPECIALOP : // if trial includes a "skipOnSaccade" op, if( cxData.pCodes[i+1].code == SPECIAL_SKIP ) // we cannot calc any trajectories once the { // skip occurs cxData.bSkipOccurred = TRUE; // this flag indicates that segment times // in internal buf may be incorrect if( iVerbose ) { printf( "WARNING: Trial includes a 'skipOnSaccade' op. " ); printf( "All tgt trajectories will be incorrect after the skip!\n" ); } } i += 2; break; case ADCON : // FIRST such code marks start of recording; if( iRecOnTick < 0 ) iRecOnTick = cxData.pCodes[i].time; // recording continues till end of trial. ++i; // (N = 1) break; case ADCOFF : // N = 1; does not affect tgt window motion case CHECKRESPOFF : case FAILSAFE : case STARTTRIAL : ++i; break; case FIXEYE1 : // rem fix tgt 1 ID for vel stabilization. iFix1 = mapTargetID( cxData.pCodes[i+1].code ); // Cntrlx issues FIXEYE1 AFTER TARGET_HOPEN! i += 2; break; case FIXEYE2 : // N = 2; does not affect tgt window motion case FIXACCURACY : case REWARDLEN : case MIDTRIALREW : case CHECKRESPON : case RANDOM_SEED : case PULSE_ON : case TARGET_ZVEL : case TARGET_ZPOSREL : case TARGET_ZPOSABS : case TARGET_ZACC : i += 2; break; case VRORIGIN : // N = 3; does not affect tgt window motion case RPDWINDOW : i += 3; break; case PSGM_TC : // N = 6; as of data file v>=10, we save if(cxData.fileHdr.version >= 10) // PSGM seq params in an output field. No { // effect on target motion. pMXPSGM = mxCreateStructMatrix(1, 1, NUMSGMFIELDS, sgmFields); if(pMXPSGM != NULL) { mxSetField(pMXPSGM, 0, "tStart", createInt32Scalar(cxData.pCodes[i].time)); mxSetField(pMXPSGM, 0, "iOpMode", createInt32Scalar(cxData.pCodes[i+1].code) ); mxSetField(pMXPSGM, 0, "bExtTrig", createUInt32Scalar((DWORD) cxData.pCodes[i+1].time) ); mxSetField(pMXPSGM, 0, "iAmp1", createInt32Scalar((int) cxData.pCodes[i+2].code) ); mxSetField(pMXPSGM, 0, "iAmp2", createInt32Scalar((int) cxData.pCodes[i+2].time) ); mxSetField(pMXPSGM, 0, "iPW1", createInt32Scalar((int) cxData.pCodes[i+3].code) ); mxSetField(pMXPSGM, 0, "iPW2", createInt32Scalar((int) cxData.pCodes[i+3].time) ); mxSetField(pMXPSGM, 0, "iPulseIntv", createInt32Scalar((int) cxData.pCodes[i+4].code) ); mxSetField(pMXPSGM, 0, "iTrainIntv", createInt32Scalar((int) cxData.pCodes[i+4].time) ); mxSetField(pMXPSGM, 0, "nPulses", createInt32Scalar((int) cxData.pCodes[i+5].code) ); mxSetField(pMXPSGM, 0, "nTrains", createInt32Scalar((int) cxData.pCodes[i+5].time) ); mxSetField(pOut, 0, "psgm", pMXPSGM); } } i += 6; break; case ENDTRIAL : // we're done! i = cxData.nCodes + 1; bDone = TRUE; break; default : // if code not recognized, we must abort mxDestroyArray( mxGetField( pMXTraj, 0, "hpos" ) ); // destroying MEX arrays we've allocated... mxDestroyArray( mxGetField( pMXTraj, 0, "vpos" ) ); mxDestroyArray( mxGetField( pMXTraj, 0, "hvel" ) ); mxDestroyArray( mxGetField( pMXTraj, 0, "vvel" ) ); mxDestroyArray( mxGetField( pMXTraj, 0, "patvelH" ) ); mxDestroyArray( mxGetField( pMXTraj, 0, "patvelV" ) ); mxDestroyArray( mxGetField( pMXTraj, 0, "targnums" ) ); mxDestroyArray( pMXTraj ); if( iVerbose ) printf( "WARNING: Unrecognized trial code %i (%i)! Cannot process trial codes!\n", cxData.pCodes[i].code, i ); return( FALSE ); } // at the start of a new segment, target windows may be instantaneously repositioned. Prior to Maestro 2.7.0 (data // file V 18), for the XYScope targets that "recycle" dots, such a large change could cause all the dots to // recycle at once, which disrupts the intended behavior of those targets. To address this, MaestroDRIVER offset // the target pattern by the same amount. We make this adjustment here. We have to do the same for the RMVideo // RMV_RANDOMDOTS target type, BUT ONLY if the RMV_WRTSCREEN flag is set. NOTE that this causes an artifact in // targets.patVelH/V when the jump in position is large. if(bStartNewSeg) for(j=0; j= 18, support sliding window // average of eye position. NOTE that if VStab is on at the beginning of the recording, the sliding window avg // will be off at the start because we don't have access to eye position before recording was turned on! if( bEnaVStabComp ) { if( (iRecOnTick >= 0) && (iTick-iRecOnTick < cxData.fileHdr.nScansSaved) ) { if(cxData.fileHdr.version < 18 || cxData.fileHdr.iVStabWinLen <= 1 || iTick==iRecOnTick) { k = (iTick-iRecOnTick) * ((int)cxData.fileHdr.nchans); dCurrEyePosH = pdRecorded[k+chHGPOS] * POSAIRAW_TODEG; dCurrEyePosV = pdRecorded[k+chVEPOS] * POSAIRAW_TODEG; } else { // compute sliding window average: Be careful near beginning of recorded timeline! dCurrEyePosH = 0; dCurrEyePosV = 0; n = 0; for(j=0; j= iRecOnTick); j++) { k = (iTick-j-iRecOnTick) * ((int)cxData.fileHdr.nchans); dCurrEyePosH += pdRecorded[k+chHGPOS]; dCurrEyePosV += pdRecorded[k+chVEPOS]; ++n; } dCurrEyePosH /= (double) n; dCurrEyePosH *= POSAIRAW_TODEG; dCurrEyePosV /= (double) n; dCurrEyePosV *= POSAIRAW_TODEG; } bGotEye = TRUE; } else bGotEye = FALSE; } if( bEnaVStabComp ) // handle VStab compensation of all tgts { if( bStartNewSeg && (cxData.fileHdr.version <= 7) ) // translate older VStab (V<8)... { if( iOpenSeg == iCurrSeg ) // turn on VStab for whatever tgt is { // designated fix tgt#1. It is only if( iFix1 < 0 ) // tgt stabilized prior to V=8! { bEnaVStabComp = FALSE; printf( "WARNING: Cannot compensate for velocity stabilization -- " ); printf( "Did not specify fixation tgt #1! (V<=7)\n" ); iOpenSeg = MAX_SEGMENTS + 2; } else { iOpenTgt = iFix1; iCurrVSMask[iOpenTgt] |= VSTAB_ON; if( (openFlags & OPENMODE_MASK) == OPENMODE_SNAP ) iCurrVSMask[iOpenTgt] |= VSTAB_SNAP; openFlags &= ~OPENMODE_MASK; if( openFlags == 0 || openFlags == OPENENA_HONLY ) iCurrVSMask[iOpenTgt] |= VSTAB_H; if( openFlags == 0 || openFlags == OPENENA_VONLY ) iCurrVSMask[iOpenTgt] |= VSTAB_V; } } else if((iOpenSeg>-1) && (iCurrSeg == iOpenSeg+nOpenSegs)) // turn off VStab for the selected tgt. iCurrVSMask[iOpenTgt] = 0; } if( bEnaVStabComp ) for( j=0; j= dVideoFrameMS); // each time there's a video frame update, update the emulation of any noisy-dots targets. The emulation is run // over the entire trial, but dot trajectories are saved by the emulator only during the recorded portion. Nothing // happens here if the noisy-dots target emulator is not enabled. NOTE that this appears after the VStab code, so // that the effects of VStab are reflected properly in the emulation. if(bNoisyDotsEmuOn && bDoVideoUpdate) { startNoisyDotsUpdate(iTick, iRecOnTick, iCurrXYFrame); for(j=0; j= 0 && // for the *recorded* length of trial, save (iTick - iRecOnTick < cxData.fileHdr.nScansSaved) ) // trial tgt traj's for the current time... { k = (iTick - iRecOnTick) * cxData.nTrialTgts; // (offsets into traj arrays) m = (iLastVideoUpdateMS - iRecOnTick) * cxData.nTrialTgts; // start of previous video frame for( j = 0; j < cxData.nTrialTgts; j++ ) { nID = cxData.oldTgtIDs[j]; if( nID < HARDTARGS ) // non-video targets updated every tick { pdTgtPosH[k+j] = dhPos[j]; pdTgtPosV[k+j] = dvPos[j]; pdTgtVelH[k+j] = dhVel[j] + dhPertD[j]; pdTgtVelV[k+j] = dvVel[j] + dvPertD[j]; // for non-video targets that are VStab'd, also adjust target velocity by the change in eye position. // NOTE: This will make the velocity trace rather noisy! Vel is in deg/sec. 1 tick = 0.001 sec! if(bEnaVStabComp && (iCurrVSMask[j] & VSTAB_ON) != 0) { if((iCurrVSMask[j] & VSTAB_H) != 0) pdTgtVelH[k+j] += (dCurrEyePosH - dLastEyePosH) / 0.001; if((iCurrVSMask[j] & VSTAB_V) != 0) pdTgtVelV[k+j] += (dCurrEyePosV - dLastEyePosV) / 0.001; } } else if( (!bDoVideoUpdate) || // update video targets only at frame epochs !(bIsOn[j] || bXYMoveWhileOff || !bXYScopeUsed) ) // also XY targets that are off don't move { // prior to Maestro version 1.2.1 pdTgtPosH[k+j] = dhLastPos[j]; // (vel cmpts are handled below..) pdTgtPosV[k+j] = dvLastPos[j]; } else { // it's time to update video targets. We determine the target's window and pattern velocity over the // entire preceding frame, then divide by the frame period to get velocity. This velocity is copied // into each of the millisecond epochs spanned by the update frame ("sample and hold"). pdTgtPosH[k+j] = dhPos[j]; pdTgtPosV[k+j] = dvPos[j]; if( m >= 0 ) { // [12aug2014 bug FIX - next line replaced: dTemp = (dCurrTimeMS - dLastVideoUpdateMS) * 0.001;] dTemp = dVideoFrameMS * 0.001; dH = (dhPos[j] - dhLastPos[j]) / dTemp; dV = (dvPos[j] - dvLastPos[j]) / dTemp; dHPat = (dhPatPos[j] - dhLastPatPos[j]) / dTemp; dhLastPatPos[j] = dhPatPos[j]; dVPat = (dvPatPos[j] - dvLastPatPos[j]) / dTemp; dvLastPatPos[j] = dvPatPos[j]; for( n = m; n < k; n += cxData.nTrialTgts ) { pdTgtVelH[n+j] = dH; pdTgtVelV[n+j] = dV; pdPatVelH[n+j] = dHPat; pdPatVelV[n+j] = dVPat; } } } } } if( bDoVideoUpdate ) // update video frame counters and "last" { // positions of video tgts after new frame bDoVideoUpdate = FALSE; // has begun // FIX as of 15may2012: // time at which last video update happened. For XYScope, we can use the current time in ms because XYScope // updates always occur on ms boundaries. This is NOT the case for RMVideo, as the (fixed) frame period is // generally not an integer multiple of 1 ms!!!! if(bXYScopeUsed) dLastVideoUpdateMS = dCurrTimeMS; else if(iTick == 0) dLastVideoUpdateMS = 0.0; else dLastVideoUpdateMS += dVideoFrameMS; iLastVideoUpdateMS = iTick; for( j = 0; j < cxData.nTrialTgts; j++ ) { dhLastPos[j] = dhPos[j]; dvLastPos[j] = dvPos[j]; dhLastPatPos[j] = dhPatPos[j]; dvLastPatPos[j] = dvPatPos[j]; } } // when compensating for VStab, we need to remember eye pos in order to compute the change in eye pos since the // previous tick. if(bEnaVStabComp && bGotEye) { dLastEyePosH = dCurrEyePosH; dLastEyePosV = dCurrEyePosV; } // advance tgt trajectories over the current time epoch...note modulation by any defined perturbations! for( j = 0; j < cxData.nTrialTgts; j++ ) { dhPosPrev[j] = dhPos[j]; dvPosPrev[j] = dvPos[j]; dhPos[j] += (dhVel[j] + dhPertD[j]) * dTickDur; dvPos[j] += (dvVel[j] + dvPertD[j]) * dTickDur; dhPatPos[j] += (dhPatVel[j] + dhPatPertD[j]) * dTickDur; dvPatPos[j] += (dvPatVel[j] + dvPatPertD[j]) * dTickDur; dhVel[j] += dhAcc[j] * dTickDur; dvVel[j] += dvAcc[j] * dTickDur; dhPatVel[j] += dhPatAcc[j] * dTickDur; dvPatVel[j] += dvPatAcc[j] * dTickDur; } ++iTick; // advance to the next time epoch dCurrTimeMS += dTickDur * 1000.0; iCurrXYFrame = iNextXYFrame; // update XY frame period, which can change // on segment boundaries } cxData.nSegments = iCurrSeg+1; // remember #segs in trial and the time at cxData.tRecordStarted = iRecOnTick; // at which recording began cxData.tTrialLen = iTick; mxSetField( pMXTraj, 0, "nTrialLen", createInt32Scalar( iTick ) ); // store total trial length and time at which mxSetField(pMXTraj, 0, "tRecordOn", createInt32Scalar(iRecOnTick)); // recording began // store target ON epochs in "targets.on". This field is a cell array of vectors. If a target is NEVER turned on // during the trial, that entry in the cell array is an empty vector. pMXOnEpochs = mxCreateCellMatrix(1, cxData.nTrialTgts); for(i=0; i 0) { // if target was ON at the end, turn it off j = onEpochs[i][0]; if((j % 2) != 0) { ++j; onEpochs[i][j] = iTick; onEpochs[i][0] = j; } // copy epoch times into MATLAB vector and store in cell array. All times are adjusted to the RECORDED trial // timeline -- so some times could be negative. pMXEpochTimes = mxCreateDoubleMatrix(1, j, mxREAL ); pdEpochTimes = mxGetPr(pMXEpochTimes); for(k=1; k<=j; k++) pdEpochTimes[k-1] = onEpochs[i][k] - iRecOnTick; mxSetCell(pMXOnEpochs, i, pMXEpochTimes); } mxSetField(pMXTraj, 0, "on", pMXOnEpochs); // store "targets" struct in the so-named output field mxSetField( pOut, 0, "targets", pMXTraj ); return( TRUE ); } //=== prepareTgtIDs =================================================================================================== // // Prepare the list of old-style IDs for targets participating in the CNTRLX trial represented by the trial codes // in the current data file. For data file version >= 2, this is an easy task. Trial target definitions have // already been stored in CXFILEDATA.pTargets[]. We merely examine this array to determine the old-style IDs: eg, // version 2 tgt type CX_FIBER1 maps to old-style ID FIBER1, while all CX_XYTARG and CX_FBTARG types will map to // unique numbers >= HARDTARGS... For data file version < 2, we must loop through all the trial codes to obtain // the IDs of the participating targets. // // ARGS: NONE. // // RETURNS: NONE. // void prepareTgtIDs() { int i, j, nID; WORD wType; // new style tgt type (file vers >= 2) if( cxData.fileHdr.version >= 2 ) // file vers >= 2 contain tgt info from which we { // can deduce the old-style IDs... for( i = 0; (i < cxData.nTargets) && (i < MAX_TRIALTARGS); i++ ) { if( cxData.fileHdr.version <= 7 ) // tgt defn format changed as of file vers = 8 wType = cxData.pTargets_V7[i].def.wType; else if(cxData.fileHdr.version <= 12) // and again as of file vers = 13 wType = cxData.pTargets_V12[i].def.wType; else if(cxData.fileHdr.version <= 22) // and again as of file vers = 23 wType = cxData.pTargets_V22[i].def.wType; else wType = cxData.pTargets[i].def.wType; switch( wType ) { case CX_FIBER1 : cxData.oldTgtIDs[i] = FIBER1; break; case CX_FIBER2 : cxData.oldTgtIDs[i] = FIBER2; break; case CX_REDLED1 : cxData.oldTgtIDs[i] = REDLED1; break; case CX_REDLED2 : cxData.oldTgtIDs[i] = REDLED2; break; case CX_CHAIR : cxData.oldTgtIDs[i] = TURNTABLE; break; default : cxData.oldTgtIDs[i] = HARDTARGS + i; break; } } cxData.nTrialTgts = cxData.nTargets; return; } cxData.nTrialTgts = 0; // for file vers < 2, we extract old-style IDs from i = 0; // the trial codes themselves... while( i < cxData.nCodes ) { nID = -1; // extract ID from next code set, if it has one switch( cxData.pCodes[i].code ) { case TARGET_ON : // for all of these, N = #trial codes in set = 2, and case TARGET_OFF : // the target ID is found in code1... case TARGET_HVEL : case TARGET_VVEL : case TARGET_HPOSREL : case TARGET_VPOSREL : case TARGET_HPOSABS : case TARGET_VPOSABS : case FIXEYE1 : case FIXEYE2 : case TARGET_HACC : case TARGET_VACC : case TARGET_VSTAB : case TARGET_HSLOVEL : case TARGET_VSLOVEL : case TARGET_HSLOACC : case TARGET_VSLOACC : case TARGET_ZVEL : case TARGET_ZPOSREL : case TARGET_ZPOSABS : case TARGET_ZACC : case XYTARGETUSED : case INSIDE_HVEL : case INSIDE_VVEL : case INSIDE_HSLOVEL : case INSIDE_VSLOVEL : nID = (int) cxData.pCodes[i+1].code; i += 2; break; case INSIDE_HACC : // these codes introduced in Maestro v2.1.0. Should case INSIDE_VACC : // NOT be encountered if file vers < 9. Otherwise, case INSIDE_HSLOACC : // N=2, and target ID is found in code1 case INSIDE_VSLOACC : if(cxData.fileHdr.version < 9) { if( iVerbose ) printf( "WARNING: Unrecognized trial code! Cannot process trial codes!\n" ); return; } nID = (int) cxData.pCodes[i+1].code; i += 2; break; case TARGET_HOPEN : // OBSOLETE as of V=8. For V=7, no tgt ID available. if( cxData.fileHdr.version < 7 ) // For V<7, tgt ID is found in code1. nID = (int) cxData.pCodes[i+1].code; i+= 2; break; case TARGET_PERTURB : // N = 5; target ID is in code 1 nID = (int) cxData.pCodes[i+1].code; i += 5; break; case ADCON : // N = 1; does not include a target ID... case ADCOFF : case CHECKRESPOFF : case FAILSAFE : case STARTTRIAL : ++i; break; case FIXACCURACY : // N = 2; does not include a target ID... case PULSE_ON : case DELTAT : case SPECIALOP : case REWARDLEN : case CHECKRESPON : case RANDOM_SEED : i += 2; break; case VRORIGIN : // N = 3; does not include a target ID... case RPDWINDOW : i += 3; break; case PSGM_TC : // N = 6; does not include a target ID... i += 6; break; case ENDTRIAL : // we're done! i = cxData.nCodes + 1; break; default : // catch-all so we don't get stuck on bad code! if( iVerbose ) printf( "WARNING: Unrecognized trial code! Cannot process trial codes!\n" ); return; } if( nID >= 0 ) // if we found a target ID, append it to our list { // if it is not already there & list is not full for( j = 0; j < cxData.nTrialTgts; j++ ) { if( cxData.oldTgtIDs[j] == nID ) break; } if( j == cxData.nTrialTgts && cxData.nTrialTgts < MAX_TRIALTARGS ) { cxData.oldTgtIDs[j] = nID; ++(cxData.nTrialTgts); } } } } //=== mapTargetID ===================================================================================================== // // This routine maps the "target ID" that appears in a TrialMode data file's trial codes to the ordinal position of // the target in the trial target list. In data files w/ version < 2 -- created by cntrlxUNIX/PC -- the ID was an // an index into the "loaded" target list; some IDs were reserved for "hard" targets like FIBER1 and REDLED1. For // such data files we must search the target IDs stored in CXFILEDATA.oldTgtIDs[] to determine the list index of // the trial target with the specified ID. In data files w/ version >= 2 -- created by the "all-PC" Cntrlx -- the // ID is simply the ordinal pos in the trial target list; no lookup is required in this case. // // ARGS: nID -- [in] target ID as it appears in trial codes // // RETURNS: pos of corresponding target in the trial's participating target list (-1 if not found). // int mapTargetID( short nID ) { int i; int iPos = -1; if( cxData.fileHdr.version >= 2 ) iPos = (int) nID; else for( i = 0; i < cxData.nTrialTgts; i++ ) { if( cxData.oldTgtIDs[i] == (int) nID ) { iPos = i; break; } } return( iPos ); }