TabSINT

TabSINT

  • Docs
  • Forum
  • Releases
  • Changelog
  • About

›User Guide

Quick Start

  • TabSINT Quick Start
  • WAHTS Quick Start
  • Run an Exam

User Guide

  • Introduction
  • Background
  • Tablet Setup
  • Configuration
  • Data Interface
  • Results Analysis
  • WAHTS
  • Dosimeter
  • Protocols
  • Response Areas
  • WAHTS Response Areas
  • Advanced Protocols
  • Example Protocols
  • Generate Configuration Code
  • FAQ

References

  • References

Example Protocols

Three Digit Test Example

Description

The three digit test plays a series of recordings of a speaker saying three numbers, such as '4, 1, 9', and the subject tries to mark the correct three digits after hearing each recording. A noisy masker is typically played simultaneously, and the signal-to-noise level (SNR) is adjusted up or down depending on the subjects ability to hear the numbers correctly.

Protocol

Below is an example protocol.json. The protocol includes:

  • A warm-up sub-protocol and a full-exam sub-protocol.

  • A main menu, where the administrator can select a warm-up, the full exam, or to submit results.

  • Both sub-protocols use a preProcessFunction to select random three-digit wav files, add a masker, and adaptively set the SNR

{
  "title": "Sample Protocol for OMT Exam",
  "subtitle": "For demonstration purposes only.",
  "instructionText": "Press one button in each column corresponding to the sentence.",
  "helpText": "Contact the exam administrator for assistance.",
  "calibration": [
    {
      "wavfiles": [
        "126.wav",
        "128.wav",
        // ...
        "neg.wav",
        "pos.wav"
      ]
    }
  ],
  "pages": [
    {
      "id": "MainMenu",
      "reference": "MainMenu"
    }
  ],
  "subProtocols": [
    {
      "protocolId":"MainMenu",
      "title":"Main Menu",
      "pages":[
        {
          "id":"Menu",
          "title":"Main Menu",
          "questionMainText":"Select a Three Digit Test option.",
          "hideProgressBar" : true,
          "responseArea":{
            "type":"multipleChoiceResponseArea",
            "choices":[
              {
                "id":"Warm Up"
              },
              {
                "id":"Full Exam"
              },
              {
                "id":"Finish Exam and Submit Results"
              }
            ]
          },
          "followOns":[
            {
              "conditional":"result.response === 'Warm Up' ",
              "target":{
                "reference":"warmUp"
              }
            },
            {
              "conditional":"result.response === 'Full Exam' ",
              "target":{
                "reference":"fullExam"
              }
            },
            {
              "conditional":"result.response === 'Finish Exam and Submit Results' ",
              "target":{
                "reference":"@END_ALL"
              }
            }
          ]
        }
      ]
    },
    {
      "protocolId":"warmUp",
      "hideProgressBar" : false,
      "title": "Three Digit Test Warm-up",
      "pages": [
        {
          "id":"warmup_intructions",
          "title":"Instructions",
          "questionMainText":"Mark the three numbers you hear for each recording.",
          "hideProgressBar" : true
        },
        {
          "id": "tdt_warmup",
          "preProcessFunction":"warmUpProcessor",
          "responseArea": {
            "type": "threeDigitTestResponseArea"
          },
          "repeatPage":{
            "nRepeats":4
          },
          "wavfiles": [
            { "path": "126.wav" },
            { "path": "128.wav" },
            //...
            { "path": "pos.wav" },
            { "path": "neg.wav" }

          ]
        },
        {
          "id":"menu",
          "reference":"MainMenu"
        }
      ]
    },
    {
      "protocolId":"fullExam",
      "hideProgressBar" : false,
      "title": "Three Digit Test",
      "pages": [
        {
          "id":"tdt_intructions",
          "title":"Instructions",
          "questionMainText":"Mark the three numbers you hear for each recording.",
          "hideProgressBar" : true
        },
        {
          "id": "tdt_exam",
          "preProcessFunction":"fullExamProcessor",
          "responseArea": {
            "type": "threeDigitTestResponseArea"
          },
          "repeatPage":{
            "nRepeats":10
          },
          "wavfiles": [
            { "path": "126.wav" },
            { "path": "128.wav" },
            //...
            { "path": "pos.wav" },
            { "path": "neg.wav" }

          ]
        },
        {
          "id":"menu",
          "reference":"MainMenu"
        }
      ]
    }
  ]
}

Javascript

Below is an example customJs.js to accompany the protocol.json.

(function() {

    /**********************************************************
     This is a container for protocol specific functions.

     Note - these functions are re-initialized for each page prior to application.

     To add a function, use the following notation:

     tabsint.register('newFunctionName', function(dm) {

         do stuff using dm fields:
         dm.flags - directly modifiable
         dm.page - the current page
         dm.testRestults - all previous responses
         dm.result - the most recent response
         _ - convenience handle to Underscore.js
         Math - convenience handle to javascript Math methods

         Note the return structure mimics the structure outlined in the protocol_schema.json:
         var returnObject = {
            fieldName: newVal,
            title: dm.page.title+' Number '+1),
            wavfiles:[
              wavefile, masker
            ],
            responseArea: {
              type: 'threeDigitTestResponseArea',
              correct: [correct[0],correct[1],correct[2]]
            }
         };
         return returnObject;
     });

   */

    /***********************************************************************************************************
     * Convenience Functions - these functions are used in multiple custom functions
     *
     * They have been moved up here to avoid repeating code
     *********************************************************************************************************/

    // Returns the current number of presentations for a given Id.
    // Useful if using repeatPage to know what the current iteration number is.
    // The function starts at the most recent result, and works backward until the pageId no longer matches.
  function getCurrentN(responses,currentId){
      var currentN = 0;               // initialize counter
      var index = responses.length-1; // start index at most recent response

      // Loop through responses, starting with most recent, until another section (pageId) is found
      while ( index >= 0 && (responses[index].presentationId.indexOf(currentId) > -1)){
          currentN ++; // add 1 to counter
          index --;    // decrease index to previous response
      }
      return currentN;
  }

  // convenience function for getting an item randomly from an array
  function getRandomItem(arr) {
      var randomIndex = Math.random();              // random number, range [0, 1), meaning 0 inclusive to 1 exclusive
      var randomIndex = randomIndex * arr.length;   // now [0, length)
      var randomIndex = Math.floor(randomIndex);    // now [0, length-1] integer
      return arr[randomIndex];
  }

  // select a random three-digit wav file
  // note - the wavfile must be listed int the page's wavfiles block, so that the path is handled correctly
  function getRandomWav(pageWavfiles) {
      // All wavfiles and maskers should be listed in the page wavfiles block
      var wavfiles = [];
      // build an array of three-digit wavfiles
      _.each(pageWavfiles, function(w){
          var ind = w.path.indexOf('.wav'); // finding index of '.' in 'xyz.wav'
          var d = w.path.substring(ind-3,ind); // get 'xyz' part of wavfile name, leave in string format
          if (!isNaN(parseInt(d[0])) && !isNaN(parseInt(d[1])) && !isNaN(parseInt(d[2]))) {
              wavfiles.push(w);
          }
      });

      return getRandomItem(wavfiles); // return one wavfile, selected randomly
  }

  // find the correct answer for a three-digit wavfile, assuming name is of type xyz.wav, where x, y, and z are the digits in order
  function getCorrect(selectedWavfile) {
      var ind = selectedWavfile.path.indexOf('.wav'); // finding index of '.' in 'xyz.wav'
      var correct = selectedWavfile.path.substring(ind - 3, ind); // get 'xyz' part of wavfile name, leave in string format
      return correct;
  }

  // select a masker wavfile.
  // note - the wavfile must be listed int the page's wavfiles block, so that the path is handled correctly
  function selectMasker(pageWavfiles, maskerName) {
      var ret = {};
    // using underscore library for the for loop
      _.each(pageWavfiles, function(w){
          if (w.path.indexOf(maskerName) >= 0){ // maskers MUST have 'masker' in the filename.  All else are three-digit wavs
              ret = w;
          }
      });
      return ret;
  }

  // odd/even checking
  function isEven(x) { return (x%2)==0; }
  function isOdd(x) { return !isEven(x); }


  /******************************************************************************************************
   * Custom Functions - these functions can be assigned to pages using the preProcessFunction field.
   * ****************************************************************************************************/

  tabsint.register('warmUpProcessor', function(dm) {
    /*
     In this example preprocessor, the goals are:
     1.  present a random 3-digit wavfile each time
     2.  adjust the SNR each time based on the number of correct digits last time

     This logic requires knowing what SNR was used last time.  flags (dm.flags.variablename)
     are used to store those values.

     Several functions (getCorrect, getCurrentN) are used in multiple custom functions,
     and have been moved above to avoid repeating code.
     */

    /******************************  Constants  ************************************************/
    var correctStep = -2; // multiplier for # correct out of 3
    var incorrectStep = 2; // multiplier for # incorrect out of 3
    var fixedMaterial = 'target'; // target or masker
    var fixedLevel = 65; // the fixed material will remain at this level while the other changes to reach specified SNRs
    var initialSNR = 0; // starting point for signal-to-noise ratio
    var maskerList = ['pos','neg'];

    /****************   Initialization / Update from last presentation  ************************/
    // variables to be saved in flags
    var snr = undefined;

    // find presentation number for current section (by counting matching pageId's)
    var currentN = getCurrentN(dm.examResults.testResults.responses, dm.page.id);
    console.log('INFO: Presenting '+dm.page.id+' # '+currentN);

    // If first presentation, use initialSNR, otherwise, grab snr (from flags set in previous presentation) and update
    if (currentN === 0){
        snr = initialSNR;
    } else {
        snr = dm.flags.snr;
        snr += correctStep*dm.result.numberCorrect + incorrectStep*dm.result.numberIncorrect; // update stored value
    }

    // grab a random wavfile from the list of wavfiles
    var selectedWavfile = getRandomWav(dm.page.wavfiles);

    // find the correct answer, assuming filename is xyz.wav, where x, y, z are the digits in order
    var correct = getCorrect(selectedWavfile); // get 'xyz' part of wavfile name, leave in string format

    //progressbar

    var selectedMasker = selectMasker(dm.page.wavfiles, maskerList[0]);

    // adjust the SPL based on fixedMaterial and updated snr
    if (fixedMaterial === 'target'){
        selectedWavfile.targetSPL = fixedLevel;
        selectedMasker.targetSPL = fixedLevel - snr; // masker must go down to increase SNR
    } else if (fixedMaterial === 'masker'){
        selectedWavfile.targetSPL = fixedLevel + snr; // target must go up to increase SNR
        selectedMasker.targetSPL = fixedLevel;
    }

    // update progressbar
    var progress = 100* (currentN) / (dm.page.repeatPage.nRepeats+1);

    // set snr in flags for next calculation
    dm.flags.snr = snr;

    // build the return object with page fields to update
    var returnObject = {
        id: dm.page.id+'_'+currentN+'_snr'+dm.flags.snr, // update page id
        title: dm.page.title+' '+(currentN+1), //
      progressBarVal: progress,
        wavfiles:[
            selectedWavfile, selectedMasker
        ],
        responseArea: {
            type: 'threeDigitTestResponseArea',
            correct: [correct[0],correct[1],correct[2]] // array of strings, i.e. ['3','8','1']
        }
    };
    return returnObject;
  });

  tabsint.register('fullExamProcessor', function(dm) {
    /*
     In this example preprocessor, the goals are:
     1.  present a random 3-digit wavfile each time
     2.  if presentation # is even, add a random masker
     3.  if presentation # is odd, add a different random masker
     4.  if presentation # is odd, update SNR based on number of correct digits last presentation

     This logic requires knowing what SNR and masker were used last time.  Flags (dm.flags.variablename)
     are used to store those values.

     Several functions (getCorrect, getCurrentN) are used in multiple custom functions,
     and have been moved above to avoid repeating code.
     */

    /******************************  Constants  ************************************************/
    var correctStep = -2; // multiplier for # correct out of 3
    var incorrectStep = 2; // multiplier for # incorrect out of 3
    var fixedMaterial = 'target'; // target or masker
    var fixedLevel = 65; // the fixed material will remain at this level while the other changes to reach specified SNRs
    var initialSNR = 0; // starting point for signal-to-noise ratio
    var maskerList = ['pos','neg'];

    /****************   Initialization / Update from last presentation  ************************/
    // variables to be saved in flags
    var snr = undefined, maskerName = undefined;

    // find presentation number for current section (by counting matching pageId's)
    var currentN = getCurrentN(dm.examResults.testResults.responses, dm.page.id);
    console.log('INFO: Presenting '+dm.page.id+' # '+currentN);

    // If first presentation, use initialSNR, otherwise, grab snr (from flags set in previous presentation) and update
    if (currentN === 0){
        snr = initialSNR;
    } else {
        snr = dm.flags.snr;
    }

    // grab a random wavfile from the list of wavfiles
    var selectedWavfile = getRandomWav(dm.page.wavfiles);

    // find the correct answer, assuming filename is xyz.wav, where x, y, z are the digits in order
    var correct = getCorrect(selectedWavfile); // get 'xyz' part of wavfile name, leave in string format

    //progressbar
    var selectedMasker;
    // Select masker.  If even, randomize.  If odd, use a different masker than last time AND update SNR
    if (isEven(currentN)){ // handles 0
        maskerName = getRandomItem(maskerList);  // get a random masker
        selectedMasker = selectMasker(dm.page.wavfiles, maskerName);
    } else if (isOdd(currentN)) {
        var maskerIndex = maskerList.indexOf(dm.flags.masker); // get index of masker used last time
        var tmpMaskerList = maskerList; // copy list so original is not changed
        tmpMaskerList.splice(maskerIndex,1); // remove the masker used last time
        maskerName = getRandomItem(tmpMaskerList); // get different random masker using modified list
        selectedMasker = selectMasker(dm.page.wavfiles, maskerName);

        snr += correctStep*dm.result.numberCorrect + incorrectStep*dm.result.numberIncorrect; // update stored value
    }

    // adjust the SPL based on fixedMaterial and updated snr
    if (fixedMaterial === 'target'){
        selectedWavfile.targetSPL = fixedLevel;
        selectedMasker.targetSPL = fixedLevel - snr; // masker must go down to increase SNR
    } else if (fixedMaterial === 'masker'){
        selectedWavfile.targetSPL = fixedLevel + snr; // target must go up to increase SNR
        selectedMasker.targetSPL = fixedLevel;
    }

    // update progressbar
    var progress = 100* (currentN) / (dm.page.repeatPage.nRepeats+1);

    // set snr in flags for next calculation
    dm.flags.snr = snr;
    dm.flags.masker = maskerName;

    // build the return object with page fields to update
    var returnObject = {
        id: dm.page.id+'_'+currentN+'_snr'+dm.flags.snr, // update page id
        title: dm.page.title+' '+(currentN+1), //
      progressBarVal: progress,
        wavfiles:[
            selectedWavfile, selectedMasker
        ],
        responseArea: {
            type: 'threeDigitTestResponseArea',
            correct: [correct[0],correct[1],correct[2]] // array of strings, i.e. ['3','8','1']
        }
    };
    return returnObject;
  });

});

Feedback Example

Description

See Feedback for detailed description of feature.

Protocol

{
  "title":"A Simple OMT Exam With Feedback",
  "pages":[
    {
      "id":"omt001",
      "title":"OMT",
      "wavfiles":[
        {
          "path":"Ta_ll3cm.wav",
          "targetSPL":"65.0"
        }
      ],
      "responseArea":{
        "type":"omtResponseArea",
        "correct":"Lucy likes three cheap mugs.",
        "feedback":"gradeResponse"
      }
    }
  ]
}

Repeats Example

Description

See Repeats for detailed description of feature.

Protocol

{
  "title":"A Simple Exam With Repeated Question",
  "pages":[
    {
      "id":"repeat01",
      "title":"Multiple Choice 1 With Repeats",
      "instructionText":"The question will repeat (up to 2 repeats) if you choose A.",
      "responseArea":{
        "type":"multipleChoiceResponseArea",
        "choices":[
          {
            "id":"A",
            "text":"Choice A"
          },
          {
            "id":"B",
            "text":"Choice B"
          }
        ]
      },
      "repeatPage":{
        "nRepeats":2,
        "repeatIf":"result.response !== 'B'"
      }
    }
  ]
}

Subject History Example

Description

This short protocol runs a Hughson-Westlake audiometry exam at a Frequency specified by either a subject's history or by a user input.

Protocol

Below is an example protocol.json. The protocol includes:

  • A subject id response area, where the user can input a subject id

  • If subject history is available for the subject id entered, the user will go directory to the audiometry exam

  • If subject history is not available, the user will be directed to an input response area to enter it

  • The preprocessing function on the audiometry exam handles how to specify the frequency input

{
  "title": "Demonstration of Subject History",
  "pages": [
    {
      "id": "subjectId",
      "title": "Subject ID",
      "questionMainText": "Enter the Subject ID",
      "responseArea": {
        "type": "subjectIdResponseArea"
      },
      "followOns":[
        {
          "conditional":"_.has(flags.subjectHistory, result.response)",
          "target": {
            "reference": "HW_demonstration"
          }
        },
        {
          "conditional": "!_.has(flags.subjectHistory, result.response)",
          "target": {
            "reference":"inputF"
          }
        }
      ]
    }
  ],
  "subProtocols": [
    {
      "protocolId": "inputF",
      "pages":[
        {
          "id": "inputF",
          "title": "Input F Left",
          "questionMainText": "Input Frequency",
          "responseArea": {
            "type": "integerResponseArea"
          }
        },
        {
          "reference" : "HW_demonstration"
        }
      ]
    },
    {
      "protocolId": "HW_demonstration",
      "pages": [
        {
          "id": "HW",
          "title": "Hughson-Westlake Level Exam",
          "preProcessFunction": "retrieveF",
          "responseArea": {
            "type": "chaHughsonWestlake",
            "examInstructions": "Tap the button once for each set of sounds you hear",
            "examProperties": {
              "LevelUnits": "dB SPL",
              "F": "F",
              "OutputChannel": "HPL0",
              "UseSoftwareButton": false
            }
          }
        }
      ]
    }
  ]
}

Javascript

Below is an example customJs.js to accompany the protocol.json.

(function() {

  tabsint.register('retrieveHAF', function(api) {

    var Freq;
    var history = api.flags.subjectHistory;
    var subject = api.examResults.subjectId;

    // Try to retrieve HAF for either left of right ear
    try {
      Freq = retrieveF();
    } catch (e) {
      console.log('WARNING: Failed to retrieve HAF data from history. Error: ' + angular.toJson(e));
      Freq = undefined;   // This will throw an error in the exam to alert us
    }

    // Find Frequency in subject history or from Input Response Area
    function retrieveF() {
      var F;

      // if there is no history entry for this subject, try get input from previous integer response area
      if (_.isUndefined(history[subject])) {

        // look through all previous responses, find the one with the presentationId = 'inputF'
        _.each(api.examResults.testResults.responses, function(response) {
          if (response.presentationId === 'inputF'){
            F = parseInt(response.response);   // make an integer out the response
          }
        });
      }

      // otherwise, get F from subject history
      else {
        F = _.last(history[subject]).F
      }

      return F;
    }

    return {
      responseArea: {
        examProperties: {
          F: Freq
        }
      }
    };
  });


})();
Last updated on 3/26/2019
← Advanced ProtocolsGenerate Configuration Code →
  • Three Digit Test Example
    • Description
    • Protocol
    • Javascript
  • Feedback Example
    • Description
    • Protocol
  • Repeats Example
    • Description
    • Protocol
  • Subject History Example
    • Description
    • Protocol
    • Javascript
TabSINT
Docs
IntroductionQuick StartUser Guide
Source
GitLabIssue Tracker
Community
YouTube
Copyright © 2023 Creare