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
}
}
};
});
})();