Max/MSP Controller
The documentation here covers the main functions in two javascript objects:
testmanager.js- The main controller script, responsible for loading and controlling the flow of a test.module_osc_handler.js- The communication module, responsible for handling the sending and receiving messages, to and and from the Unity Agent.
testmanager.js
my-qexe-project
└─ qexe_controller
└─ src
└─ code
├── ...
├── testmanager.js
└── ...
The testmanager.js script is the main controller script. The key functions are described below.
loadTest()
filePath- File path to the .json configuration file to load. The json is loaded as a javascript dictionary object, which is then saved as a string in _objtxt. This can be accessed throughouttestmanager.js.
function loadTest(filePath)
{
var dict = new Dict;
dict.import_json(filePath);
// Output the dict to a dict.view class for viewing in the visual environment.
outlet(outlet_JSON, "json", 'clear');
outlet(outlet_JSON, "json", "dictionary", dict.name);
// load the JSON file as a String.
var fileString = utilsmodule.loadFile(filePath);
SEND_PANEL_DIRECTORY_INFORMATION.message("DisplayTestFile", filePath);
// Removes any dynamically created objects in max patches.
// Objects created dynamically should be instantiated as global variables to be accessed in all functions.
clearPatchDynamicObjects();
// Convert the string to a javascript object.
try{
_objtxt = JSON.parse(fileString);
}catch(e){
error("Error loading file:", e);
}
// Set the incoming and outgoing UPD port information for osc messages.
setNetwork();
// Load in the correct questionnaire class.
setQuestionnaire(_objtxt.testSettings.questionnaireType,
_objtxt.testSettings.questionnaireIntegration);
// Load in the correct method class and configure the test.
numberOfAudioRenderingVSTs = _objtxt.testSettings.audioRendering.audioVSTConditions.length;
setTestMethod(_objtxt.testSettings.methodType);
// Calculate the paradigm based of off the set variables in the JSON.
calculateParadigm();
// Randomize the order of test items.
randomizeSequence();
// Set directories.
SEND_MODULE_RESULTS_WRITER.message("OpenFolder", RESULTSDIRECTORY);
CONTENTDIRECTORY = _objtxt.testSettings.pathToAudioVideoScenesContent;
SEND_PANEL_DIRECTORY_INFORMATION.message("DisplayContentDirectory", CONTENTDIRECTORY);
// Reset the test back to the _Main scene if a new file is opened.
UpdateScenePanelInformation(-1);
// Set the information in the OSC manager.
SetOSCManager();
// Set the condition info to resultsWriter and Method.
SetaudioConditions(_objtxt.testSettings.audioRendering);
// Generic test information.
DisplayTestInformation();
}
setQuestionnaire()
questionnaireType- String sent to the questionnaire container to load the corresponding .maxpat file. Also send to the results module to write to results files.questionnaireIntegration- String sent to the results module to ensure test metadata is stored in the results files.
Customization options
- To include a custom questionnaire .maxpat file, add a new case entry that replaces
bpatcherQuestionnairewith the name of your.maxpatfile. Ensure your.maxpatsatisfies the communication requirements expected by the rest of the controller.
... show code ...
function setQuestionnaire(questionnaireType, integrationType) {
if(questionnaireType === null){
SEND_MODULE_RESULTS_WRITER.message("SetInfoForQuestionnaire", 0, "null", "None");
return;
}
switch (questionnaireType){
case 'SSQ':
SEND_MODULE_METHOD_QUESTIONNAIRE.message("patch", "script sendbox bpatcherQuestionnaire replace questionnaire_SSQ");
break;
case "NASA-TLX":
SEND_MODULE_METHOD_QUESTIONNAIRE.message("patch", "script sendbox bpatcherQuestionnaire replace questionnaire_NASA-TLX");
break;
default :
error("Error: questionnaireType : ", questionnaireType, " is not recognized in .json file.", "\n");
}
post("Importing questionnaire:", questionnaireType, '\n');
SEND_MODULE_RESULTS_WRITER.message("SetInfoForQuestionnaire", 1, questionnaireType, integrationType);
}
setTestMethod()
methodTypeThe ID used to load the correct method logic and interfaces. A string is sent to the method container to load a certain.maxpatfile. For certain methods, the number of audio renderers is also sent to the method container to initialize the test via thesetTest()function.
Customization options
- To include your method
.maxpat, add another case entry here that replaces thebpatcherQuestionnairewith the name of your.maxpatfile. Note that additional code is required for test item construction — seecalculateParadigm()below.
... show code ...
function setTestMethod(method) {
switch (method)
{
case 'ACR':
post("Importing test methodology: ", method, "\n");
SEND_MODULE_METHOD_QUESTIONNAIRE.message("patch", "script sendbox bpatcherMethod replace method_ACR");
break;
case 'MS':
post("Importing test methodology: ", method, "\n");
SEND_MODULE_METHOD_QUESTIONNAIRE.message("patch", "script sendbox bpatcherMethod replace method_MS");
SEND_MODULE_METHOD_QUESTIONNAIRE.message("setTest", numberOfAudioRenderingVSTs);
break;
case 'PC':
post("Importing test methodology: ", method, "\n");
SEND_MODULE_METHOD_QUESTIONNAIRE.message("patch", "script sendbox bpatcherMethod replace method_PC");
SEND_MODULE_METHOD_QUESTIONNAIRE.message("setTest",numberOfAudioRenderingVSTs);
break;
case 'MUSHRA':
SEND_MODULE_METHOD_QUESTIONNAIRE.message("patch", "script sendbox bpatcherMethod replace method_MUSHRA");
SEND_MODULE_METHOD_QUESTIONNAIRE.message("setTest", numberOfAudioRenderingVSTs);
break;
case 'EBA':
SEND_MODULE_METHOD_QUESTIONNAIRE.message("patch", "script sendbox bpatcherMethod replace method_EBA");
SEND_MODULE_METHOD_QUESTIONNAIRE.message("setTest", numberOfAudioRenderingVSTs);
break;
default:
error("Error: methodType : ", method, " is not recognized in .json file.", "\n");
}
}
calculateParadigm()
Description
methodTypeis used to search for the the correct entry. The method loaded is then used to calculate a set of items.- The calculated set of test items is then saved into a dict called
objtmptxt. This is used throughout the test to access the correct variables.
Customization options
- To include a custom test method, a corresponding paradigm calculation function must also be provided. As an example, the
ACRParadigmfunction is contained within the_method_ACR_calcmodule, imported at the top oftestmanager.js.
... show code ...
function calculateParadigm() {
var objtmptxt = "";
var thisTest = new Dict("thisTest"); // Dictionary for the test.
switch(_objtxt.testSettings.methodType) // Create the paradigm.
{
case 'ACR':
thisTest = _method_ACR_calc.ACRParadigm(_objtxt);
break;
case 'MS':
thisTest = _method_EBA_calc.EBAParadigm(_objtxt);
break;
case 'PC':
thisTest = _method_PC_calc.PCParadigm(_objtxt);
break;
case 'MUSHRA':
thisTest = _method_MUSHRA_calc.MUSHRAParadigm(_objtxt);
break;
case 'EBA':
thisTest = _method_EBA_calc.EBAParadigm(_objtxt);
break;
default :
error("Test method does not currently exist. Cannot construct test items. '\n' ");
break;
}
outlet(outlet_JSON, "paradigm", 'clear');
outlet(outlet_JSON, "paradigm", "dictionary", thisTest.name);
thisTest.export_json("jsontmp.json"); // Export the paradigm to a json file.
var objtmptxt = utilsmodule.loadFile("jsontmp.json"); // Reimport the paradigm json.
try {
_objParadigm = JSON.parse(objtmptxt); // Parse into a javascript object.
} catch (e) {
post("Error parsing jsontmp.json");
return false;
}
SEND_PANEL_CURRENT_ITEM_SIMPLE.message("numberOfItems",
_objParadigm.tmpItem.length);
SEND_MODULE_RESULTS_WRITER.message("SetInfoForResults",
_objtxt.testSettings.methodType,
_objtxt.testSettings.modalityRatio,
_objParadigm.tmpItem.length);
SENDTO_PANEL_SCENE_INFORMATION.message("DisplayNumberOfItems",
_objParadigm.tmpItem.length);
}
setOSCManager()
- Sends the OSC information to the OSC manager regarding the ip address and port numbers.
- Sends the test information to the OSC manager such that the variables can be quickly sent to the connection unity agent upon request.
Customization options
- To send additional variables to the Unity Agent, add them here and extend the
set_paradigm_info()function inmodule_osc_handler.js. Make sure to accommodate these new incoming messages in the Unity Agent.
... show code ...
function SetOSCManager() {
SEND_MODULE_OSC_MANAGER.message("setOSC",
_objtxt.testSettings.ip, // set the ip address
_objtxt.testSettings.udpPortOut, // set the outgoing OSC port number
_objtxt.testSettings.udpPortIn); // set the incoming OSC port number
SEND_MODULE_OSC_MANAGER.message("SetLocalVars",
"set_paradigm_info",
_objtxt.testSettings.methodType, // set method.
numberOfAudioRenderingVSTs, // set number of VSTs loaded.
_objtxt.testSettings.questionnaireType, // set type of questionnaire.
_objtxt.testSettings.questionnaireIntegration); // set the integration mode of the questionnaire.
}
SetaudioConditions()
audioRendering- multiple key and values entires from the JSON configuration file used to load the relevant VSTs.- Initially creates the interface buttons needed to be able to open the VSTs GUIs.
- To load the actual audio rendering VSTs, a loop will iterate through the number of VSTs, and provide the path for a VST maxMSP object to be loaded, and plugged. Information in this loop is then collected and finally sent to the results writer so that all results files have the required test data.
... see code ...
function SetaudioConditions(AudioRendering) {
if (AudioRendering.active === 1){
SENDTO_PANEL_AUDIO_INFORMATION.message("SetButtons",
AudioRendering.renderingPipeline,
AudioRendering.audioVSTConditions.length);
var ConditionIDArray = []; // Array of dll id's for results.
var ConditionPathArray = []; // Array of paths/to/dlls for results.
var HRTFInfoArray = []; // HRTF info for the results.
var ParameterMap = []; // Parameter maps used for the conversion of coordinate systems for results.
vstindex = 0; // Int for connecting the VST outlets.
for(i=0; i<AudioRendering.audioVSTConditions.length; i++){
// Set the poly~ VST target (needs to be done in separate message)
SENDTO_PANEL_AUDIO_INFORMATION.message("VSTContainer",
AudioRendering.renderingPipeline,
"target", (i+1));
// Load the VST to the correct poly~ VST object.
SENDTO_PANEL_AUDIO_INFORMATION.message("VSTContainer",
AudioRendering.renderingPipeline,
"load",
AudioRendering.pathToVSTs + "/" + AudioRendering.audioVSTConditions[i].vst,
AudioRendering.pathToVSTs + "/" + AudioRendering.audioVSTConditions[i].vstParameterMap,
vstindex);
vstindex = vstindex + 2;
// Push information to ConditionIDArray for the results file.
ConditionIDArray.push(AudioRendering.audioVSTConditions[i].conditionID);
ConditionPathArray.push(AudioRendering.audioVSTConditions[i].vst);
HRTFInfoArray.push(AudioRendering.audioVSTConditions[i].hrtf)
ParameterMap.push(AudioRendering.audioVSTConditions[i].vstParameterMap ? AudioRendering.audioVSTConditions[i].vstParameterMap : "null");
}
SEND_MODULE_RESULTS_WRITER.message("SetConditionID", ConditionIDArray);
SEND_MODULE_RESULTS_WRITER.message("SetConditionPath", ConditionPathArray);
SEND_MODULE_RESULTS_WRITER.message("SetConditionHRTF", HRTFInfoArray);
SEND_MODULE_RESULTS_WRITER.message("SetConditionParameterMap", ParameterMap);
}
}
module_osc_handler.js
my-qexe-project
└─ qexe_controller
└─ src
└─ code
├── ...
├── module_osc_handler.js
└── ...
SetLocalVars(...args)
- Function called from the
testmanager.jssetOSCManager()method to set local variables.
... see code ...
max.addHandler('SetLocalVars', (...args) => {
var msg = args[0]; // identify the message.
switch (msg){
case 'set_paradigm_info':
method = args[1]; // set method
numberOfConditions = args[2]; // set number of conditions
questionnaire = args[3]; // set questionnaire
questionnaireIntegration = args[4]; // set questionnaire integration type
break;
case 'set_subjects_results_directory':
subjectResultsDirectory = args[1];
break;
default:
max.post('/control/testmanager has no handler for value', ...args);
break;
}
});
TestManager(...args)
- Handles all incoming OSC calls from Unity — either retrieving information from
testmanager.jsor responding directly with locally stored variables. The first argument inmax.outlet()is either"testmanager"(routes totestmanager.js) or"toclient"(routes to the outgoing OSC port).
Customization options
- Additional cases can be added here to handle new incoming OSC messages from Unity. Incoming calls should follow the format
"TestManager, msg, ...".
... see code ...
max.addHandler('TestManager', (...args) => {handle_TestManager(...args)});
function handle_TestManager(...args){
var msg = args[0];
if(jsonLoaded===0){
max.post('WARNING: no json test loaded', jsonLoaded);
return;
}
switch (msg){
case 'get_next_item':
max.post('Unity requesting next item information');
max.outlet('testmanager', 'OSCCall', 'getitem');
break;
case 'set_next_item':
max.post('Unity setting next item');
max.outlet('testmanager', 'OSCCall', 'setitem');
break;
case 'client_is_active':
max.post('Client is active:', args[1], max.POST_LEVELS.WARN);
max.outlet('toclient', '/client/configuration/', 'json_loaded', 1);
break;
case 'get_paradigm_information':
max.post('Unity requesting method information');
handle_SendToUnity('give_paradigm_information');
break;
case 'get_results_directory':
max.outlet('testmanager', 'OSCCall', 'getdirectory');
default:
max.post('/control/testmanager has no handler for msg value');
break;
}
}
SendToUnity(...args)
- Handles all outgoing calls to the Unity Agent. The first argument in max.outlet() is
"toclient", routing data to the outgoing OSC port.
Customization options
- To send additional information to Unity, add a new case here. Incoming calls should follow the format
"SendToUnity, msg, ...".
... see code ...
max.addHandler('TestManager', (...args) => {handle_TestManager(...args)});
function handle_TestManager(...args){
var msg = args[0];
if(jsonLoaded===0){
max.post('WARNING: no json test loaded', jsonLoaded);
return;
}
switch (msg){
case 'get_next_item':
//max.post('Unity requesting next item information');
max.outlet('testmanager', 'OSCCall', 'getitem');
break;
case 'set_next_item':
max.post('Unity setting next item');
max.outlet('testmanager', 'OSCCall', 'setitem');
break;
case 'client_is_active':
max.post('Client is active:', args[1], max.POST_LEVELS.WARN);
max.outlet('toclient', '/client/configuration/', 'json_loaded', 1);
break;
case 'get_paradigm_information':
//max.post('Unity requesting method information');
handle_SendToUnity('give_paradigm_information');
break;
case 'get_results_directory':
max.outlet('testmanager', 'OSCCall', 'getdirectory');
default:
max.post('/control/testmanager has no handler for msg value');
break;
}
}
Max Patches
module_MethodQuestionnaireContainer.maxpat
my-qexe-project
└─ qexe_controller
└─ src
└─ patchers
├── ...
├── module_MethodQuestionnaireContainer.maxpat
└── ...
The patch below shows how messages sent into the thispatcher object can load new Max patches at runtime. Both the method container (left) and the questionnaire container (right) start with a default empty patch. The setQuestionnaire() and setTestMethod() functions above load the correct patches into each container respectively.

Patcher view of the method and questionnaire container. Incoming messages from the testManager.js are routed to predefined messages that are used to dynamically load sub-patches. Incoming OSC messages can be sent directly to the sub-patches and are handled on a per-method / per-questionnaire basis.

Presentation view of the method and questionnaire container (shown above). The example shows the .maxpat modules for ARC method and SSQ questionnaire loaded. Note - the interfaces here are for visual feedback, reflecting what happens in the Unity Agent.
module_OSCManager.maxpat
The max patch below shows where the module_osc_handler.js code is used.
Incoming OSC messages that do not have a specific address in the max route object are fed into module_osc_handler.js.
Outgoing messages are either routed to the testmanager.js script, or to the outgoing UDP port.
Location
my-qexe-project
└─ qexe_controller
└─ src
└─ patchers
├── ...
├── module_OSCManager.maxpat
└── ...

Patcher view of the OSC module. The route object filters three different types of incoming OSC message that need to be mapped to any loaded VST input channel index. Control OSC messages are sent directly into the OSC module. Messages from the testManager.js object are sent into the module (purple).