jam-cloud/web/app/assets/javascripts/wizard/gear_test.js

554 lines
19 KiB
JavaScript

(function (context, $) {
"use strict";
context.JK = context.JK || {};
context.JK.GearTest = function (app) {
var logger = context.JK.logger;
var isAutomated = false;
var drawUI = false;
var scoring = false;
var validLatencyScore = false;
var validIOScore = false;
var latencyScore = null;
var ioScore = null;
var lastSavedTime = new Date();
// this should be marked TRUE when the backend sends an invalid_audio_device alert
var asynchronousInvalidDevice = false;
var selectedDeviceInfo = null;
var $scoreReport = null;
var $ioHeader = null;
var $latencyHeader = null;
var $ioRate = null;
var $ioRateScore = null;
var $ioVar = null;
var $ioVarScore = null;
var $ioCountdown = null;
var $ioCountdownSecs = null;
var $latencyScore = null;
var $resultsText = null;
var $unknownText = null;
var $loopbackCompleted = null;
var $adjustGearSpeedCompleted = null;
var $adjustGearForIoFail = null;
var $ioScoreSection = null;
var $latencyScoreSection = null;
var $self = $(this);
var GEAR_TEST_START = "gear_test.start";
var GEAR_TEST_IO_START = "gear_test.io_start";
var GEAR_TEST_IO_DONE = "gear_test.io_done";
var GEAR_TEST_LATENCY_START = "gear_test.latency_start";
var GEAR_TEST_LATENCY_DONE = "gear_test.latency_done";
var GEAR_TEST_DONE = "gear_test.done";
var GEAR_TEST_FAIL = "gear_test.fail";
var GEAR_TEST_IO_PROGRESS = "gear_test.io_progress";
var GEAR_TEST_INVALIDATED_ASYNC = "gear_test.async_invalidated"; // happens when backend alerts us device is invalid
function isWdm() {
return selectedDeviceInfo && (selectedDeviceInfo.input.info.type == 'Win32_wdm' || selectedDeviceInfo.output.info.type == 'Win32_wdm')
}
function isGoodFtue() {
return (isWdm() || validIOScore) && !asynchronousInvalidDevice;
}
function processIOScore(io) {
// take the higher variance, which is apparently actually std dev
var std = io.in_var > io.out_var ? io.in_var : io.out_var;
std = Math.round(std * 100) / 100;
// take the furthest-off-from-target io rate
var median = Math.abs(io.in_median - io.in_target) > Math.abs(io.out_median - io.out_target) ? [io.in_median, io.in_target] : [io.out_median, io.out_target];
var medianTarget = median[1];
median = Math.round(median[0]);
console.log("io", io, std, median)
var stdIOClass = 'bad';
if (std <= 0.50) {
stdIOClass = 'good';
}
else if (std <= 1.00) {
stdIOClass = 'acceptable';
}
var medianIOClass = 'bad';
if (Math.abs(median - medianTarget) <= ((io.in_target / 100) * .5)) {
medianIOClass = 'good';
}
else if (Math.abs(median - medianTarget) <= ((io.in_target / 100))) {
medianIOClass = 'acceptable';
}
// uncomment one to force a particular type of I/O failure
// medianIOClass = "bad";
// stdIOClass = "bad"
// take worst between median or std
var ioClassToNumber = {bad: 2, acceptable: 1, good: 0}
var aggregrateIOClass = ioClassToNumber[stdIOClass] > ioClassToNumber[medianIOClass] ? stdIOClass : medianIOClass;
// now base the overall IO score based on both values.
$self.triggerHandler(GEAR_TEST_IO_DONE, {std:std, median:median, io:io, aggregrateIOClass: aggregrateIOClass, medianIOClass : medianIOClass, stdIOClass: stdIOClass, validLatencyScore: validLatencyScore})
//renderIOScore(std, median, io, aggregrateIOClass, medianIOClass, stdIOClass);
if(aggregrateIOClass == "bad") {
validIOScore = false;
}
else {
validIOScore = true;
}
scoring = false;
if(isGoodFtue()) {
$self.triggerHandler(GEAR_TEST_DONE, {validLatencyScore: validLatencyScore})
}
else {
$self.triggerHandler(GEAR_TEST_FAIL, {reason:'io', ioTarget: medianIOClass, ioTargetScore: median, ioVariance: stdIOClass, ioVarianceScore: std, validLatencyScore: validLatencyScore});
}
}
function automaticScore() {
logger.debug("automaticScore: calling FTUESave(false)");
lastSavedTime = new Date(); // save before and after FTUESave, because the event happens in a multithreaded way
var result = context.jamClient.FTUESave(false);
lastSavedTime = new Date();
if(result && result.error) {
logger.debug("unable to FTUESave(false). reason=" + result.detail);
context.JK.GearTest.testDeferred.reject(result);
return false;
}
else {
var latency = context.jamClient.FTUEGetExpectedLatency();
context.JK.GearTest.testDeferred.resolve(latency);
}
return true;
}
function loopbackScore() {
var cbFunc = 'JK.loopbackLatencyCallback';
logger.debug("Registering loopback latency callback: " + cbFunc);
context.jamClient.FTUERegisterLatencyCallback('JK.loopbackLatencyCallback');
var now = new Date();
logger.debug("Starting Latency Test..." + now);
context.jamClient.FTUEStartLatency();
}
// refocused affects how IO testing occurs.
// on refocus=true:
// * reuse IO score if it was good/acceptable
// * rescore IO if it was bad or skipped from previous try
function attemptScore(_selectedDeviceInfo, refocused) {
if(scoring) {
logger.debug("gear-test: already scoring");
return;
}
selectedDeviceInfo = _selectedDeviceInfo;
scoring = true;
asynchronousInvalidDevice = false;
$self.triggerHandler(GEAR_TEST_START);
$self.triggerHandler(GEAR_TEST_LATENCY_START);
validLatencyScore = false;
latencyScore = null;
if(!refocused) {
// don't reset a valid IO score on refocus
validIOScore = false;
ioScore = null;
}
// this timer exists to give UI time to update for renderScoringStarted before blocking nature of jamClient.FTUESave(save) kicks in
setTimeout(function () {
context.JK.GearTest.testDeferred = new $.Deferred();
if(isAutomated) {
// use automated latency score mechanism
automaticScore()
}
else {
// use loopback score mechanism
loopbackScore();
}
context.JK.GearTest.testDeferred.done(function(latency) {
latencyScore = latency;
if(isAutomated) {
// uncomment to do a manual loopback test
//latency.latencyknown = false;
}
updateScoreReport(latency, refocused);
if (true || validLatencyScore) {
$self.triggerHandler(GEAR_TEST_IO_START);
// reuse valid IO score if this is on refocus
if(false && (refocused && validIOScore)) {
processIOScore(ioScore);
}
else {
var testTimeSeconds = gon.ftue_io_wait_time; // allow time for IO to establish itself
var startTime = testTimeSeconds / 2; // start measuring half way through the test, to get past IO oddities
$self.trigger(GEAR_TEST_IO_PROGRESS, {countdown:testTimeSeconds, first:true})
var interval = setInterval(function () {
testTimeSeconds -= 1;
$self.trigger(GEAR_TEST_IO_PROGRESS, {countdown:testTimeSeconds, first:false})
if(testTimeSeconds == startTime) {
logger.debug("Starting IO Perf Test starting at " + startTime + "s in")
context.jamClient.FTUEStartIoPerfTest();
}
if (testTimeSeconds == 0) {
clearInterval(interval);
logger.debug("Ending IO Perf Test at " + testTimeSeconds + "s in")
var io = context.jamClient.FTUEGetIoPerfData();
ioScore = io;
processIOScore(io);
}
}, 1000);
}
}
else {
scoring = false;
$self.triggerHandler(GEAR_TEST_FAIL, {reason:'latency', validLatencyScore: validLatencyScore, latencyScore: latencyScore.latency})
}
})
.fail(function(ftueSaveResult) {
scoring = false;
$self.triggerHandler(GEAR_TEST_FAIL, {reason:'invalid_configuration', validLatencyScore: validLatencyScore, data: ftueSaveResult})
})
}, 250);
}
function updateScoreReport(latencyResult, refocused) {
var latencyClass = "neutral";
var latencyValue = null;
var validLatency = false;
console.log("FTUEGetExpectedLatency", latencyResult)
if (latencyResult && latencyResult.latencyknown) {
var latencyValue = latencyResult.latency;
latencyValue = Math.round(latencyValue * 100) / 100;
if (latencyValue <= 10) {
latencyClass = "good";
validLatency = true;
} else if (latencyValue <= gon.ftue_maximum_gear_latency) {
latencyClass = "acceptable";
validLatency = true;
} else {
latencyClass = "bad";
}
}
else {
latencyClass = 'unknown';
}
// uncomment these two lines to fail test due to latency
// latencyClass = "bad";
// validLatency = false;
validLatencyScore = validLatency;
if(refocused) {
context.JK.prodBubble($scoreReport, 'refocus-rescore', {validIOScore: validIOScore}, {positions:['top', 'left']});
}
$self.triggerHandler(GEAR_TEST_LATENCY_DONE, {latencyValue: latencyValue, latencyClass: latencyClass, refocused: refocused});
}
// std deviation is the worst value between in/out
// media is the worst value between in/out
// io is the value returned by the backend, which has more info
// ioClass is the pre-computed rollup class describing the result in simple terms of 'good', 'acceptable', bad'
function renderIOScore(std, median, ioData, ioClass, ioRateClass, ioVarClass) {
$ioRateScore.text(median !== null ? median : '');
$ioVarScore.text(std !== null ? std : '');
if (ioClass && ioClass != "starting" && ioClass != "skip") {
$ioRate.show();
$ioVar.show();
}
if(ioClass == 'starting' || ioClass == 'skip') {
$ioHeader.show();
}
$resultsText.attr('io-rate-score', ioRateClass);
$resultsText.attr('io-var-score', ioVarClass);
$ioScoreSection.removeClass('good acceptable bad unknown starting skip')
if (ioClass) {
$ioScoreSection.addClass(ioClass);
}
// TODO: show help bubble of all data in IO data
}
function isScoring() {
return scoring;
}
function isValidLatencyScore() {
return validLatencyScore;
}
function isValidIOScore() {
return validIOScore;
}
function getLatencyScore() {
return latencyScore;
}
function getIOScore() {
return ioScore;
}
function getLastSavedTime() {
return lastSavedTime;
}
function onInvalidAudioDevice() {
logger.debug("gear_test: onInvalidAudioDevice")
asynchronousInvalidDevice = true;
$self.triggerHandler(GEAR_TEST_INVALIDATED_ASYNC);
context.JK.Banner.showAlert('Invalid Audio Device', 'It appears this audio device is not currently connected. Attach the device to your computer and restart the application, or select a different device.<br/><br/>If you think your gear is connected and working:<br/><br/>Try a different sample rate')
}
function showLoopbackDone() {
$loopbackCompleted.show();
}
function showGearAdjustmentDone() {
$adjustGearSpeedCompleted.show();
}
function resetScoreReport() {
$ioHeader.hide();
$latencyHeader.hide();
$ioRate.hide();
$ioRateScore.empty();
$ioVar.hide();
$ioVarScore.empty();
$latencyScore.empty();
$resultsText.removeAttr('latency-score');
$resultsText.removeAttr('io-var-score');
$resultsText.removeAttr('io-rate-score');
$resultsText.removeAttr('scored');
$unknownText.hide();
$loopbackCompleted.hide();
$adjustGearSpeedCompleted.hide();
$ioScoreSection.removeClass('good acceptable bad unknown starting skip');
$latencyScoreSection.removeClass('good acceptable bad unknown starting')
}
function invalidateScore() {
validLatencyScore = false;
validIOScore = false;
asynchronousInvalidDevice = false;
resetScoreReport();
}
function renderLatencyScore(latencyValue, latencyClass) {
// latencyValue == null implies starting condition
if (latencyValue) {
$latencyScore.text(latencyValue + ' ms');
}
else {
$latencyScore.text('');
}
if(latencyClass == 'unknown') {
$latencyScore.text('Unknown');
$unknownText.show();
}
$latencyHeader.show();
$resultsText.attr('latency-score', latencyClass);
$latencyScoreSection.removeClass('good acceptable bad unknown starting').addClass(latencyClass);
}
function renderIOScoringStarted(secondsLeft) {
$ioCountdownSecs.text(secondsLeft);
$ioCountdown.show();
}
function renderIOScoringStopped() {
$ioCountdown.hide();
}
function renderIOCountdown(secondsLeft) {
$ioCountdownSecs.text(secondsLeft);
}
function uniqueDeviceName() {
try {
return selectedDeviceInfo.input.info.displayName + '(' + selectedDeviceInfo.input.behavior.shortName + ')' + '-' +
selectedDeviceInfo.output.info.displayName + '(' + selectedDeviceInfo.output.behavior.shortName + ')' + '-' +
context.JK.GetOSAsString();
}
catch(e){
logger.error("unable to devise unique device name for stats: " + e.toString());
return "Unknown";
}
}
function handleUI($testResults) {
if(!$testResults.is('.ftue-box.results')) {
throw "GearTest != .ftue-box.results"
}
$scoreReport = $testResults;
$ioHeader = $scoreReport.find('.io');;
$latencyHeader = $scoreReport.find('.latency');
$ioRate = $scoreReport.find('.io-rate');
$ioRateScore = $scoreReport.find('.io-rate-score');
$ioVar = $scoreReport.find('.io-var');
$ioVarScore = $scoreReport.find('.io-var-score');
$ioScoreSection = $scoreReport.find('.io-score-section');
$latencyScore = $scoreReport.find('.latency-score');
$ioCountdown = $scoreReport.find('.io-countdown');
$ioCountdownSecs = $scoreReport.find('.io-countdown .secs');
$resultsText = $scoreReport.find('.results-text');
$unknownText = $scoreReport.find('.unknown-text');
$loopbackCompleted = $scoreReport.find('.loopback-completed')
$adjustGearSpeedCompleted = $scoreReport.find('.adjust-gear-speed-completed');
$adjustGearForIoFail = $scoreReport.find(".adjust-gear-for-io-fail")
$latencyScoreSection = $scoreReport.find('.latency-score-section');
function onGearTestStart(e, data) {
renderIOScore(null, null, null, null, null, null);
}
function onGearTestIOStart(e, data) {
renderIOScore(null, null, null, 'starting', 'starting', 'starting');
}
function onGearTestLatencyStart(e, data) {
resetScoreReport();
renderLatencyScore(null, 'starting');
}
function onGearTestLatencyDone(e, data) {
renderLatencyScore(data.latencyValue, data.latencyClass);
}
function onGearTestIOProgress(e, data) {
if(data.first) {
renderIOScoringStarted(data.countdown);
}
renderIOCountdown(data.countdown);
if(data.countdown == 0) {
renderIOScoringStopped();
}
}
function onGearTestIODone(e, data) {
renderIOScore(data.std, data.median, data.io, data.aggregrateIOClass, data.medianIOClass, data.stdIOClass);
}
function onGearTestDone(e, data) {
$resultsText.attr('scored', 'complete');
context.JK.GA.trackAudioTestData(uniqueDeviceName(), context.JK.GA.AudioTestDataReasons.pass, latencyScore);
rest.userCertifiedGear({success: true, client_id: app.clientId, audio_latency: getLatencyScore().latency});
}
function onGearTestFail(e, data) {
$resultsText.attr('scored', 'complete');
if(data.reason == "latency") {
renderIOScore(null, null, null, 'skip', 'skip', 'skip');
}
rest.userCertifiedGear({success: false, client_id: app.clientId});
if(data.reason == "latency") {
context.JK.GA.trackAudioTestData(uniqueDeviceName(), context.JK.GA.AudioTestDataReasons.latencyFail, data.latencyScore);
}
else if(data.reason = "io") {
if(data.ioTarget == 'bad') {
context.JK.GA.trackAudioTestData(uniqueDeviceName(), context.JK.GA.AudioTestDataReasons.ioTargetFail, data.ioTargetScore);
}
else {
context.JK.GA.trackAudioTestData(uniqueDeviceName(), context.JK.GA.AudioTestDataReasons.ioVarianceFail, data.ioVarianceScore);
}
}
else if(data.reason == 'invalid_configuration') {
logger.error("invalid configuration returned by gearTest." + data.data.detail)
}
else {
logger.error("unknown reason in onGearTestFail: " + data.reason)
}
}
$self
.on(GEAR_TEST_START, onGearTestStart)
.on(GEAR_TEST_IO_START, onGearTestIOStart)
.on(GEAR_TEST_LATENCY_START, onGearTestLatencyStart)
.on(GEAR_TEST_LATENCY_DONE, onGearTestLatencyDone)
.on(GEAR_TEST_IO_PROGRESS, onGearTestIOProgress)
.on(GEAR_TEST_IO_DONE, onGearTestIODone)
.on(GEAR_TEST_DONE, onGearTestDone)
.on(GEAR_TEST_FAIL, onGearTestFail);
}
function initialize($testResults, automated, noUI) {
isAutomated = automated;
drawUI = !noUI;
if(drawUI) {
handleUI($testResults);
}
}
// Latency Test Callback
context.JK.loopbackLatencyCallback = function (latencyMS) {
// Unregister callback:
context.jamClient.FTUERegisterLatencyCallback('');
logger.debug("loopback test done: " + latencyMS);
context.JK.GearTest.testDeferred.resolve({latency: latencyMS, latencyknown:true})
};
this.GEAR_TEST_START = GEAR_TEST_START;
this.GEAR_TEST_IO_START = GEAR_TEST_IO_START;
this.GEAR_TEST_IO_DONE = GEAR_TEST_IO_DONE;
this.GEAR_TEST_LATENCY_START = GEAR_TEST_LATENCY_START;
this.GEAR_TEST_LATENCY_DONE = GEAR_TEST_LATENCY_DONE;
this.GEAR_TEST_DONE = GEAR_TEST_DONE;
this.GEAR_TEST_FAIL = GEAR_TEST_FAIL;
this.GEAR_TEST_IO_PROGRESS = GEAR_TEST_IO_PROGRESS;
this.GEAR_TEST_INVALIDATED_ASYNC = GEAR_TEST_INVALIDATED_ASYNC;
this.initialize = initialize;
this.isScoring = isScoring;
this.attemptScore = attemptScore;
this.resetScoreReport = resetScoreReport;
this.showLoopbackDone = showLoopbackDone;
this.showGearAdjustmentDone = showGearAdjustmentDone;
this.invalidateScore = invalidateScore;
this.isValidLatencyScore = isValidLatencyScore;
this.isValidIOScore = isValidIOScore;
this.isGoodFtue = isGoodFtue;
this.getLatencyScore = getLatencyScore;
this.getIOScore = getIOScore;
this.getLastSavedTime = getLastSavedTime;
this.onInvalidAudioDevice = onInvalidAudioDevice;
return this;
}
})(window, jQuery);