1171 lines
39 KiB
JavaScript
1171 lines
39 KiB
JavaScript
(function (context, $) {
|
|
|
|
"use strict";
|
|
|
|
context.JK = context.JK || {};
|
|
context.JK.StepSelectGear = function (app, $dialog) {
|
|
|
|
var ASSIGNMENT = context.JK.ASSIGNMENT;
|
|
var VOICE_CHAT = context.JK.VOICE_CHAT;
|
|
var AUDIO_DEVICE_BEHAVIOR = context.JK.AUDIO_DEVICE_BEHAVIOR;
|
|
|
|
var self = null;
|
|
var $step = null;
|
|
var logger = context.JK.logger;
|
|
var rest = context.JK.Rest();
|
|
var $watchVideoInput = null;
|
|
var $watchVideoOutput = null;
|
|
var $audioInput = null;
|
|
var $audioOutput = null;
|
|
var $bufferIn = null;
|
|
var $bufferOut = null;
|
|
var $frameSize = null;
|
|
var $inputChannels = null;
|
|
var $outputChannels = null;
|
|
var $knobs = null;
|
|
var $scoreReport = null;
|
|
var $latencyScoreSection = null;
|
|
var $latencyScore = null;
|
|
var $latencyHeader = null;
|
|
var $ioHeader = null;
|
|
var $ioScoreSection = null;
|
|
var $ioRate = null;
|
|
var $ioRateScore = null;
|
|
var $ioVar = null;
|
|
var $ioVarScore = null;
|
|
var $ioCountdown = null;
|
|
var $ioCountdownSecs = null;
|
|
var $resultsText = null;
|
|
var $unknownText = null;
|
|
var $asioInputControlBtn = null;
|
|
var $asioOutputControlBtn = null;
|
|
var $resyncBtn = null;
|
|
var $templateAudioPort = null;
|
|
var $launchLoopbackBtn = null;
|
|
var $instructions = null;
|
|
|
|
var operatingSystem = null;
|
|
var iCheckIgnore = false;
|
|
var scoring = false; // are we currently scoring
|
|
var validDevice = false; // do we currently have a device selected that we can score against?
|
|
|
|
// cached values between
|
|
var deviceInformation = null;
|
|
var lastSelectedDeviceInfo = null;
|
|
var shownOutputProdOnce = false;
|
|
var shownInputProdOnce = false;
|
|
|
|
var selectedDeviceInfo = null;
|
|
var musicPorts = null;
|
|
var validLatencyScore = false;
|
|
var validIOScore = false;
|
|
var lastLatencyScore = null;
|
|
var ioScore = null;
|
|
var latencyScore = null;
|
|
|
|
var savedProfile = false;
|
|
|
|
|
|
|
|
function isGoodFtue() {
|
|
return validLatencyScore && validIOScore;
|
|
}
|
|
|
|
// returns a deviceInfo hash for the device matching the deviceId, or undefined.
|
|
function findDevice(deviceId) {
|
|
return deviceInformation[deviceId];
|
|
}
|
|
|
|
function selectedAudioInput() {
|
|
return $audioInput.val();
|
|
}
|
|
|
|
function selectedAudioOutput() {
|
|
return $audioOutput.val();
|
|
}
|
|
|
|
function selectedFramesize() {
|
|
return parseFloat($frameSize.val());
|
|
}
|
|
|
|
function selectedBufferIn() {
|
|
return parseFloat($bufferIn.val());
|
|
}
|
|
|
|
function selectedBufferOut() {
|
|
return parseFloat($bufferOut.val());
|
|
}
|
|
|
|
function setFramesize(value) {
|
|
context.JK.dropdown($frameSize.val(value).easyDropDown('select', value.toString(), true))
|
|
}
|
|
|
|
function setBufferIn(value) {
|
|
context.JK.dropdown($bufferIn.val(value).easyDropDown('select', value.toString(), true));
|
|
}
|
|
|
|
function setBufferOut(value) {
|
|
context.JK.dropdown($bufferOut.val(value).easyDropDown('select', value.toString(), true))
|
|
}
|
|
|
|
function setInputAudioDevice(value) {
|
|
context.JK.dropdown($audioInput.val(value).easyDropDown('select', value.toString(), true))
|
|
}
|
|
|
|
function setOutputAudioDevice(value) {
|
|
context.JK.dropdown($audioOutput.val(value).easyDropDown('select', value.toString(), true))
|
|
}
|
|
|
|
function initializeNextButtonState() {
|
|
$dialog.setNextState(isGoodFtue());
|
|
}
|
|
|
|
function initializeBackButtonState() {
|
|
$dialog.setBackState(!scoring);
|
|
}
|
|
|
|
function initializeAudioInput() {
|
|
var optionsHtml = '';
|
|
optionsHtml = '<option selected="selected" value="">Choose...</option>';
|
|
context._.each(deviceInformation, function (deviceInfo, deviceId) {
|
|
if(deviceInfo.inputCount > 0) {
|
|
optionsHtml += '<option title="' + deviceInfo.displayName + '" value="' + deviceId + '">' + deviceInfo.displayName + '</option>';
|
|
}
|
|
});
|
|
|
|
console.log("INITIALIZE AUDIO INPUT: " + optionsHtml)
|
|
$audioInput.html(optionsHtml);
|
|
context.JK.dropdown($audioInput);
|
|
$audioInput.easyDropDown('enable')
|
|
|
|
initializeAudioInputChanged();
|
|
}
|
|
|
|
function initializeAudioOutput() {
|
|
var optionsHtml = '';
|
|
optionsHtml = '<option selected="selected" value="">Same as Input</option>';
|
|
context._.each(deviceInformation, function (deviceInfo, deviceId) {
|
|
if(deviceInfo.outputCount > 0) {
|
|
optionsHtml += '<option title="' + deviceInfo.displayName + '" value="' + deviceId + '">' + deviceInfo.displayName + '</option>';
|
|
}
|
|
});
|
|
$audioOutput.html(optionsHtml);
|
|
context.JK.dropdown($audioOutput);
|
|
$audioOutput.easyDropDown('disable'); // enable once they pick something in input
|
|
|
|
initializeAudioOutputChanged();
|
|
}
|
|
|
|
function initializeFramesize() {
|
|
context.JK.dropdown($frameSize);
|
|
}
|
|
|
|
function initializeBuffers() {
|
|
context.JK.dropdown($bufferIn);
|
|
context.JK.dropdown($bufferOut);
|
|
}
|
|
|
|
|
|
// reloads the backend's channel state for the currently selected audio devices,
|
|
// and update's the UI accordingly
|
|
function initializeChannels() {
|
|
musicPorts = jamClient.FTUEGetChannels();
|
|
|
|
initializeInputPorts(musicPorts);
|
|
initializeOutputPorts(musicPorts);
|
|
}
|
|
|
|
// select 2 (or 1) inputs and 2 outputs for the user. required to get a latency score
|
|
// also, arguably convenient
|
|
function autoSelectMinimumValidChannels() {
|
|
|
|
var audioInputDeviceId = selectedAudioInput();
|
|
var audioOutputDeviceId = selectedAudioOutput();
|
|
|
|
var $allInputs = $inputChannels.find('input[type="checkbox"]');
|
|
|
|
if ($allInputs.length == 0) {
|
|
// ERROR: not enough channels
|
|
if(!audioInputDeviceId || audioInputDeviceId == '') {
|
|
context.JK.prodBubble($audioInput.closest('.easydropdown-wrapper'), 'select-input', {}, {positions:['right', 'top']});
|
|
}
|
|
//context.JK.Banner.showAlert('To be a valid input audio device, the device must have at least 1 input channel.');
|
|
return false;
|
|
}
|
|
|
|
var $allOutputs = $outputChannels.find('input[type="checkbox"]');
|
|
if ($allOutputs.length < 2) {
|
|
if(!audioOutputDeviceId || audioOutputDeviceId == '') {
|
|
context.JK.prodBubble($audioOutput.closest('.easydropdown-wrapper'), 'select-output', {}, {positions:['right', 'top'], duration:7000});
|
|
}
|
|
// ERROR: not enough channels
|
|
//context.JK.Banner.showAlert('To be a valid output audio device, the device must have at least 2 output channels.');
|
|
return false;
|
|
}
|
|
|
|
// ensure 1, or preferably 2, input channels are selected
|
|
var $assignedInputs = $inputChannels.find('input[type="checkbox"]:checked');
|
|
var $unassignedInputs = $inputChannels.find('input[type="checkbox"]:not(:checked)');
|
|
if ($assignedInputs.length == 0) {
|
|
if ($allInputs.length >= 2) {
|
|
logger.debug("selecting 2 inputs")
|
|
$unassignedInputs.eq(0).iCheck('check').attr('checked', 'checked');
|
|
// this is required because iCheck change handler re-writes the inputs. So we have to refetch unassigned outputs
|
|
$unassignedInputs = $inputChannels.find('input[type="checkbox"]:not(:checked)');
|
|
$unassignedInputs.eq(0).iCheck('check').attr('checked', 'checked');
|
|
}
|
|
else {
|
|
logger.debug("selecting 1 inputs")
|
|
$unassignedInputs.eq(0).iCheck('check').attr('checked', 'checked');
|
|
}
|
|
}
|
|
|
|
// ensure 2 outputs are selected
|
|
var $assignedOutputs = $outputChannels.find('input[type="checkbox"]:checked');
|
|
var $unassignedOutputs = $outputChannels.find('input[type="checkbox"]:not(:checked)');
|
|
|
|
if ($assignedOutputs.length == 0) {
|
|
logger.debug("selecting both outputs")
|
|
$unassignedOutputs.eq(0).iCheck('check').attr('checked', 'checked');
|
|
// this is required because iCheck change handler re-writes the inputs. So we have to refetch unassigned outputs
|
|
$unassignedOutputs = $outputChannels.find('input[type="checkbox"]:not(:checked)');
|
|
$unassignedOutputs.eq(0).iCheck('check').attr('checked', 'checked');
|
|
}
|
|
else if ($assignedOutputs.length == 1) {
|
|
logger.debug("selecting 1 output to round out 2 total")
|
|
$unassignedOutputs.eq(0).iCheck('check').attr('checked', 'checked');
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// during this phase of the FTUE, we have to assign selected input channels
|
|
// to tracks. The user, however, does not have a way to indicate which channel
|
|
// goes to which track (that's not until the next step of the wizard).
|
|
// so, we just auto-generate a valid assignment
|
|
function newInputAssignment() {
|
|
var assigned = 0;
|
|
context._.each(musicPorts.inputs, function (inputChannel) {
|
|
if (isChannelAssigned(inputChannel)) {
|
|
assigned += 1;
|
|
}
|
|
});
|
|
|
|
var newAssignment = Math.floor(assigned / 2) + 1;
|
|
return newAssignment;
|
|
}
|
|
|
|
function inputChannelChanged() {
|
|
if (iCheckIgnore) return;
|
|
|
|
var $checkbox = $(this);
|
|
var channelId = $checkbox.attr('data-id');
|
|
var isChecked = $checkbox.is(':checked');
|
|
|
|
if (isChecked) {
|
|
var newAssignment = newInputAssignment();
|
|
logger.debug("assigning input channel %o to track: %o", channelId, newAssignment);
|
|
context.jamClient.TrackSetAssignment(channelId, true, newAssignment);
|
|
}
|
|
else {
|
|
logger.debug("unassigning input channel %o", channelId);
|
|
context.jamClient.TrackSetAssignment(channelId, true, ASSIGNMENT.UNASSIGNED);
|
|
// unassigning creates a hole in our auto-assigned tracks. reassign them all to keep it consistent
|
|
var $assignedInputs = $inputChannels.find('input[type="checkbox"]:checked');
|
|
var assigned = 0;
|
|
context._.each($assignedInputs, function (assignedInput) {
|
|
var $assignedInput = $(assignedInput);
|
|
var assignedChannelId = $assignedInput.attr('data-id');
|
|
var newAssignment = Math.floor(assigned / 2) + 1;
|
|
logger.debug("re-assigning input channel %o to track: %o", assignedChannelId, newAssignment);
|
|
context.jamClient.TrackSetAssignment(assignedChannelId, true, newAssignment);
|
|
assigned += 1;
|
|
});
|
|
}
|
|
|
|
initializeChannels();
|
|
}
|
|
|
|
// should be called in a ifChanged callback if you want to cancel.
|
|
// you have to use this instead of 'return false' like a typical input 'change' event.
|
|
function cancelICheckChange($checkbox) {
|
|
iCheckIgnore = true;
|
|
var checked = $checkbox.is(':checked');
|
|
setTimeout(function () {
|
|
if (checked) $checkbox.iCheck('uncheck').removeAttr('checked');
|
|
else $checkbox.iCheck('check').attr('checked', 'checked');
|
|
iCheckIgnore = false;
|
|
}, 1);
|
|
}
|
|
|
|
function outputChannelChanged() {
|
|
if (iCheckIgnore) return;
|
|
var $checkbox = $(this);
|
|
var channelId = $checkbox.attr('data-id');
|
|
var isChecked = $checkbox.is(':checked');
|
|
|
|
// don't allow more than 2 output channels selected at once
|
|
if ($outputChannels.find('input[type="checkbox"]:checked').length > 2) {
|
|
context.JK.Banner.showAlert('You can only have a maximum of 2 output ports selected.');
|
|
// can't allow uncheck of last output
|
|
cancelICheckChange($checkbox);
|
|
return;
|
|
}
|
|
|
|
if (isChecked) {
|
|
logger.debug("assigning output channel %o", channelId);
|
|
context.jamClient.TrackSetAssignment(channelId, true, ASSIGNMENT.OUTPUT);
|
|
}
|
|
else {
|
|
logger.debug("unassigning output channel %o", channelId);
|
|
context.jamClient.TrackSetAssignment(channelId, true, ASSIGNMENT.UNASSIGNED);
|
|
}
|
|
|
|
initializeChannels();
|
|
}
|
|
|
|
// checks if it's an assigned OUTPUT or ASSIGNED CHAT
|
|
function isChannelAssigned(channel) {
|
|
return channel.assignment == ASSIGNMENT.CHAT || channel.assignment == ASSIGNMENT.OUTPUT || channel.assignment > 0;
|
|
}
|
|
|
|
function initializeInputPorts(musicPorts) {
|
|
$inputChannels.empty();
|
|
var inputPorts = musicPorts.inputs;
|
|
context._.each(inputPorts, function (inputChannel) {
|
|
var $inputChannel = $(context._.template($templateAudioPort.html(), inputChannel, { variable: 'data' }));
|
|
var $checkbox = $inputChannel.find('input');
|
|
if (isChannelAssigned(inputChannel)) {
|
|
$checkbox.attr('checked', 'checked');
|
|
}
|
|
context.JK.checkbox($checkbox);
|
|
$checkbox.on('ifChanged', inputChannelChanged);
|
|
$inputChannels.append($inputChannel);
|
|
});
|
|
}
|
|
|
|
function initializeOutputPorts(musicPorts) {
|
|
$outputChannels.empty();
|
|
var outputChannels = musicPorts.outputs;
|
|
context._.each(outputChannels, function (outputChannel) {
|
|
var $outputPort = $(context._.template($templateAudioPort.html(), outputChannel, { variable: 'data' }));
|
|
var $checkbox = $outputPort.find('input');
|
|
if (isChannelAssigned(outputChannel)) {
|
|
$checkbox.attr('checked', 'checked');
|
|
}
|
|
context.JK.checkbox($checkbox);
|
|
$checkbox.on('ifChanged', outputChannelChanged);
|
|
$outputChannels.append($outputPort);
|
|
});
|
|
}
|
|
|
|
function initializeLoopback() {
|
|
$launchLoopbackBtn.unbind('click').click(function() {
|
|
app.setWizardStep(1);
|
|
app.layout.showDialog('ftue');
|
|
return false;
|
|
})
|
|
}
|
|
|
|
function initializeFormElements() {
|
|
if (!deviceInformation) throw "devices are not initialized";
|
|
|
|
initializeAudioInput();
|
|
initializeAudioOutput();
|
|
initializeFramesize();
|
|
initializeBuffers();
|
|
initializeLoopback();
|
|
}
|
|
|
|
function resetFrameBuffers() {
|
|
$frameSize.val('2.5');
|
|
$bufferIn.val('0');
|
|
$bufferOut.val('0');
|
|
}
|
|
|
|
function clearInputPorts() {
|
|
$inputChannels.empty();
|
|
}
|
|
|
|
function clearOutputPorts() {
|
|
$outputChannels.empty();
|
|
}
|
|
|
|
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();
|
|
$ioScoreSection.removeClass('good acceptable bad unknown starting skip');
|
|
$latencyScoreSection.removeClass('good acceptable bad unknown starting')
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
// 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 updateScoreReport(latencyResult) {
|
|
var latencyClass = "neutral";
|
|
var latencyValue = null;
|
|
var validLatency = false;
|
|
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';
|
|
}
|
|
|
|
validLatencyScore = validLatency;
|
|
|
|
renderLatencyScore(latencyValue, latencyClass);
|
|
}
|
|
|
|
function audioInputDeviceUnselected() {
|
|
validDevice = false;
|
|
setOutputAudioDevice('');
|
|
resetState();
|
|
}
|
|
|
|
function renderScoringStarted() {
|
|
resetScoreReport();
|
|
initializeNextButtonState();
|
|
freezeAudioInteraction();
|
|
renderLatencyScore(null, 'starting');
|
|
renderIOScore(null, null, null, null, null, null);
|
|
}
|
|
|
|
function renderScoringStopped() {
|
|
initializeNextButtonState();
|
|
unfreezeAudioInteraction();
|
|
$resultsText.attr('scored', 'complete');
|
|
scoring = false;
|
|
initializeBackButtonState();
|
|
}
|
|
|
|
function freezeAudioInteraction() {
|
|
logger.debug("freezing audio interaction");
|
|
$audioInput.attr("disabled", "disabled").easyDropDown('disable');
|
|
$audioOutput.attr("disabled", "disabled").easyDropDown('disable');
|
|
$frameSize.attr("disabled", "disabled").easyDropDown('disable');
|
|
$bufferIn.attr("disabled", "disabled").easyDropDown('disable');
|
|
$bufferOut.attr("disabled", "disabled").easyDropDown('disable');
|
|
$asioInputControlBtn.on("click", false);
|
|
$asioOutputControlBtn.on("click", false);
|
|
$resyncBtn.on('click', false);
|
|
iCheckIgnore = true;
|
|
$inputChannels.find('input[type="checkbox"]').iCheck('disable');
|
|
$outputChannels.find('input[type="checkbox"]').iCheck('disable');
|
|
}
|
|
|
|
function unfreezeAudioInteraction() {
|
|
logger.debug("unfreezing audio interaction");
|
|
$audioInput.removeAttr("disabled").easyDropDown('enable');
|
|
$audioOutput.removeAttr("disabled").easyDropDown('enable');
|
|
$frameSize.removeAttr("disabled").easyDropDown('enable');
|
|
$bufferIn.removeAttr("disabled").easyDropDown('enable');
|
|
$bufferOut.removeAttr("disabled").easyDropDown('enable');
|
|
$asioInputControlBtn.off("click", false);
|
|
$asioOutputControlBtn.off("click", false);
|
|
$resyncBtn.off('click', false);
|
|
$inputChannels.find('input[type="checkbox"]').iCheck('enable');
|
|
$outputChannels.find('input[type="checkbox"]').iCheck('enable');
|
|
iCheckIgnore = false;
|
|
}
|
|
|
|
function initializeWatchVideo() {
|
|
$watchVideoInput.unbind('click').click(function () {
|
|
|
|
var audioDevice = findDevice(selectedAudioInput());
|
|
if (!audioDevice) {
|
|
context.JK.Banner.showAlert('You must first choose an Audio Input Device so that we can determine which video to show you.');
|
|
}
|
|
else {
|
|
var videoURL = AUDIO_DEVICE_BEHAVIOR[audioDevice.type].videoURL;
|
|
|
|
if (videoURL) {
|
|
$(this).attr('href', videoURL);
|
|
return true;
|
|
}
|
|
else {
|
|
context.JK.Banner.showAlert('No help video for this type of device (' + audioDevice.displayType + ')');
|
|
}
|
|
}
|
|
|
|
return false;
|
|
});
|
|
|
|
$watchVideoOutput.unbind('click').click(function () {
|
|
|
|
var audioDevice = findDevice(selectedAudioOutput());
|
|
if (!audioDevice) {
|
|
throw "this button should be hidden";
|
|
}
|
|
else {
|
|
var videoURL = AUDIO_DEVICE_BEHAVIOR[audioDevice.type].videoURL;
|
|
|
|
if (videoURL) {
|
|
$(this).attr('href', videoURL);
|
|
return true;
|
|
}
|
|
else {
|
|
context.JK.Banner.showAlert('No help video for this type of device (' + audioDevice.displayType + ')');
|
|
}
|
|
}
|
|
|
|
return false;
|
|
});
|
|
}
|
|
|
|
function invalidateScore() {
|
|
validLatencyScore = false;
|
|
validIOScore = false;
|
|
resetScoreReport();
|
|
initializeNextButtonState();
|
|
}
|
|
|
|
function initializeASIOButtons() {
|
|
$asioInputControlBtn.unbind('click').click(function () {
|
|
context.jamClient.FTUEOpenControlPanel(selectedAudioInput());
|
|
});
|
|
$asioOutputControlBtn.unbind('click').click(function () {
|
|
context.jamClient.FTUEOpenControlPanel(selectedAudioOutput());
|
|
});
|
|
}
|
|
|
|
function initializeKnobs() {
|
|
$frameSize.unbind('change').change(function () {
|
|
logger.debug("frameize changed: " + selectedFramesize());
|
|
context.JK.prodBubble($resyncBtn, 'push-resync-when-done', {}, {positions:['top']});
|
|
updateDefaultBuffers();
|
|
jamClient.FTUESetFrameSize(selectedFramesize());
|
|
invalidateScore();
|
|
});
|
|
|
|
$bufferIn.unbind('change').change(function () {
|
|
logger.debug("buffer-in changed: " + selectedBufferIn());
|
|
context.JK.prodBubble($resyncBtn, 'push-resync-when-done', {}, {positions:['top']});
|
|
jamClient.FTUESetInputLatency(selectedBufferIn());
|
|
invalidateScore();
|
|
});
|
|
|
|
$bufferOut.unbind('change').change(function () {
|
|
logger.debug("buffer-out changed: " + selectedBufferOut());
|
|
context.JK.prodBubble($resyncBtn, 'push-resync-when-done', {}, {positions:['top']});
|
|
jamClient.FTUESetOutputLatency(selectedBufferOut());
|
|
invalidateScore();
|
|
});
|
|
}
|
|
|
|
function ftueSummary() {
|
|
return {
|
|
os: operatingSystem,
|
|
version: context.jamClient.ClientUpdateVersion(),
|
|
success: isGoodFtue(),
|
|
score: {
|
|
validLatencyScore: validLatencyScore,
|
|
validIOScore: validIOScore,
|
|
latencyScore: latencyScore,
|
|
ioScore : ioScore,
|
|
},
|
|
audioParameters: {
|
|
frameSize: selectedFramesize(),
|
|
bufferIn: selectedBufferIn(),
|
|
bufferOut: selectedBufferOut(),
|
|
},
|
|
devices: deviceInformation,
|
|
selectedDevice: selectedDeviceInfo
|
|
}
|
|
}
|
|
|
|
function postDiagnostic() {
|
|
rest.createDiagnostic({
|
|
type: 'GEAR_SELECTION',
|
|
data: {
|
|
logs: logger.logCache,
|
|
client_type: context.JK.clientType(),
|
|
client_id:
|
|
context.JK.JamServer.clientID,
|
|
summary:ftueSummary()}
|
|
});
|
|
}
|
|
|
|
function getSelectedInputs() {
|
|
return $inputChannels.find('input[type="checkbox"]:checked');
|
|
}
|
|
|
|
function getSelectedOutputs() {
|
|
return $outputChannels.find('input[type="checkbox"]:checked');
|
|
}
|
|
|
|
function initializeResync() {
|
|
$resyncBtn.unbind('click').click(function () {
|
|
|
|
if (getSelectedInputs().length == 0) {
|
|
context.JK.Banner.showAlert("You must have at least one input port to resync.");
|
|
return false;
|
|
}
|
|
|
|
if (getSelectedOutputs().length < 2) {
|
|
context.JK.Banner.showAlert("You must have exactly two output ports to resync.");
|
|
return false;
|
|
}
|
|
|
|
attemptScore();
|
|
return false;
|
|
})
|
|
}
|
|
|
|
function renderIOScoringStarted(secondsLeft) {
|
|
$ioCountdownSecs.text(secondsLeft);
|
|
$ioCountdown.show();
|
|
}
|
|
|
|
function renderIOScoringStopped() {
|
|
$ioCountdown.hide();
|
|
}
|
|
|
|
function renderIOCountdown(secondsLeft) {
|
|
$ioCountdownSecs.text(secondsLeft);
|
|
}
|
|
|
|
// sets selectedDeviceInfo, which contains id, behavior, and info for input and output device
|
|
function cacheCurrentAudioInfo() {
|
|
|
|
var audioInputDeviceId = selectedAudioInput();
|
|
var audioOutputDeviceId = selectedAudioOutput();
|
|
if (!audioInputDeviceId) {
|
|
audioInputDeviceUnselected();
|
|
return false;
|
|
}
|
|
|
|
var audioInputDevice = findDevice(audioInputDeviceId);
|
|
if (!audioInputDevice) {
|
|
context.JK.alertSupportedNeeded('Unable to find information for input device: ' + audioInputDeviceId);
|
|
return false;
|
|
}
|
|
|
|
if (!audioOutputDeviceId) {
|
|
audioOutputDeviceId = audioInputDeviceId;
|
|
}
|
|
var audioOutputDevice = findDevice(audioOutputDeviceId);
|
|
if (!audioInputDevice) {
|
|
context.JK.alertSupportedNeeded('Unable to find information for output device: ' + audioOutputDeviceId);
|
|
return false;
|
|
}
|
|
|
|
if(savedProfile && jamClient.FTUEGetInputMusicDevice() != audioInputDeviceId || jamClient.FTUEGetOutputMusicDevice() != audioOutputDeviceId) {
|
|
// if they want to change an input or output device, but we have a saved (named) profile
|
|
// then we need to delete the current profile and create a new one
|
|
// because there is no way to rename a profile, and the profile name has the device's name in it
|
|
var profileName = context.jamClient.FTUEGetMusicProfileName();
|
|
logger.debug("invaliding previously saved profile: " + profileName);
|
|
|
|
$dialog.createFTUEProfile();
|
|
// restore user selections because newSession is called by createFTUEProfile(), invalidating dropdowns
|
|
setInputAudioDevice(audioInputDeviceId);
|
|
setOutputAudioDevice(audioOutputDeviceId);
|
|
|
|
context.jamClient.TrackDeleteProfile(profileName);
|
|
|
|
}
|
|
|
|
shownOutputProdOnce = false;
|
|
shownInputProdOnce = false;
|
|
|
|
lastSelectedDeviceInfo = selectedDeviceInfo;
|
|
|
|
selectedDeviceInfo = context.JK.selectedDeviceInfo(audioInputDeviceId, audioOutputDeviceId, deviceInformation)
|
|
|
|
return true;
|
|
}
|
|
|
|
function changeDevice() {
|
|
|
|
var audioInputDeviceId = selectedDeviceInfo.input.id;
|
|
var audioOutputDeviceId = selectedDeviceInfo.output.id;
|
|
|
|
if(audioInputDeviceId) {
|
|
$audioOutput.easyDropDown('enable');
|
|
}
|
|
|
|
// don't re-assign input/output audio devices because it disturbs input/output track association
|
|
if (jamClient.FTUEGetInputMusicDevice() != audioInputDeviceId) {
|
|
jamClient.FTUESetInputMusicDevice(audioInputDeviceId);
|
|
}
|
|
if (jamClient.FTUEGetOutputMusicDevice() != audioOutputDeviceId) {
|
|
jamClient.FTUESetOutputMusicDevice(audioOutputDeviceId);
|
|
}
|
|
|
|
initializeChannels();
|
|
|
|
validDevice = autoSelectMinimumValidChannels();
|
|
|
|
if (!validDevice) {
|
|
return false;
|
|
}
|
|
|
|
jamClient.FTUESetInputLatency(selectedBufferIn());
|
|
jamClient.FTUESetOutputLatency(selectedBufferOut());
|
|
jamClient.FTUESetFrameSize(selectedFramesize());
|
|
|
|
return true;
|
|
}
|
|
|
|
function audioDeviceChanged() {
|
|
if(!cacheCurrentAudioInfo()) {
|
|
return;
|
|
}
|
|
updateDialogForCurrentDevices();
|
|
if (changeDevice()) {
|
|
attemptScore();
|
|
}
|
|
}
|
|
|
|
function isInputAudioTypeDifferentFromLastTime() {
|
|
return lastSelectedDeviceInfo && (lastSelectedDeviceInfo.input.info.type != selectedDeviceInfo.input.info.type);
|
|
}
|
|
|
|
function isOutputAudioTypeDifferentFromLastTime() {
|
|
return lastSelectedDeviceInfo && isInputOutputDifferentTypes() && (lastSelectedDeviceInfo.output.info.type != selectedDeviceInfo.output.info.type)
|
|
}
|
|
|
|
function isInputOutputDifferentTypes() {
|
|
return selectedDeviceInfo && (selectedDeviceInfo.input.info.type != selectedDeviceInfo.output.info.type);
|
|
}
|
|
|
|
function updateDialogForCurrentDevices() {
|
|
if(selectedDeviceInfo) {
|
|
var inputBehavior = selectedDeviceInfo.input.behavior;
|
|
var outputBehavior = selectedDeviceInfo.output.behavior;
|
|
}
|
|
else {
|
|
var inputBehavior = null;
|
|
var outputBehavior = null;
|
|
|
|
}
|
|
|
|
// deal with watch video
|
|
if(isInputOutputDifferentTypes()) {
|
|
// if we have two types of devices, you need two different videos
|
|
$watchVideoOutput.show().find('.video-type').show();
|
|
$watchVideoInput.addClass('audio-output-showing').find('.video-type').show();
|
|
}
|
|
else {
|
|
$watchVideoOutput.hide();
|
|
$watchVideoInput.removeClass('audio-output-showing').find('.video-type').hide();
|
|
}
|
|
|
|
// handle framesize/buffers
|
|
if (inputBehavior && (inputBehavior.showKnobs || outputBehavior.showKnobs)) {
|
|
$knobs.show();
|
|
}
|
|
else {
|
|
$knobs.hide();
|
|
}
|
|
|
|
// handle ASIO
|
|
if (inputBehavior) {
|
|
if (inputBehavior.showASIO) {
|
|
$asioInputControlBtn.show();
|
|
}
|
|
else {
|
|
$asioInputControlBtn.hide();
|
|
}
|
|
if(outputBehavior.showASIO && (selectedDeviceInfo.input.id != selectedDeviceInfo.output.id)) {
|
|
$asioOutputControlBtn.show();
|
|
}
|
|
else {
|
|
$asioOutputControlBtn.hide();
|
|
}
|
|
}
|
|
else {
|
|
// show no ASIO buttons
|
|
$asioInputControlBtn.hide();
|
|
$asioOutputControlBtn.hide();
|
|
}
|
|
|
|
// handle resync button
|
|
if (inputBehavior) {
|
|
$resyncBtn.css('visibility', 'visible');
|
|
}
|
|
else {
|
|
$resyncBtn.css('visibility', 'hidden');
|
|
}
|
|
|
|
updateDefaultFrameSize();
|
|
|
|
updateDefaultBuffers();
|
|
}
|
|
|
|
function updateDefaultFrameSize() {
|
|
if(selectedDeviceInfo && (selectedDeviceInfo.input.info.type == 'Win32_wdm' || selectedDeviceInfo.output.info.type == 'Win32_wdm')) {
|
|
setFramesize('10');
|
|
}
|
|
else {
|
|
setFramesize('2.5')
|
|
}
|
|
|
|
jamClient.FTUESetFrameSize(selectedFramesize());
|
|
}
|
|
|
|
function updateDefaultBuffers() {
|
|
|
|
// handle specific framesize settings
|
|
if(selectedDeviceInfo && (selectedDeviceInfo.input.info.type == 'Win32_wdm' || selectedDeviceInfo.output.info.type == 'Win32_wdm')) {
|
|
var framesize = selectedFramesize();
|
|
|
|
if(framesize == 2.5) {
|
|
logger.debug("setting default buffers to 1/1");
|
|
setBufferIn('1');
|
|
setBufferOut('1');
|
|
}
|
|
else if(framesize == 5) {
|
|
logger.debug("setting default buffers to 3/2");
|
|
setBufferIn('3');
|
|
setBufferOut('2');
|
|
}
|
|
else {
|
|
logger.debug("setting default buffers to 6/5");
|
|
setBufferIn('6');
|
|
setBufferOut('5');
|
|
}
|
|
}
|
|
else {
|
|
logger.debug("setting default buffers to 0/0");
|
|
setBufferIn(0);
|
|
setBufferOut(0);
|
|
}
|
|
|
|
jamClient.FTUESetInputLatency(selectedBufferIn());
|
|
jamClient.FTUESetOutputLatency(selectedBufferOut());
|
|
}
|
|
|
|
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]);
|
|
|
|
var stdIOClass = 'bad';
|
|
if (std <= 0.50) {
|
|
stdIOClass = 'good';
|
|
}
|
|
else if (std <= 1.00) {
|
|
stdIOClass = 'acceptable';
|
|
}
|
|
|
|
var medianIOClass = 'bad';
|
|
if (Math.abs(median - medianTarget) <= 1) {
|
|
medianIOClass = 'good';
|
|
}
|
|
else if (Math.abs(median - medianTarget) <= 2) {
|
|
medianIOClass = 'acceptable';
|
|
}
|
|
|
|
// 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.
|
|
renderIOScore(std, median, io, aggregrateIOClass, medianIOClass, stdIOClass);
|
|
|
|
if(aggregrateIOClass == "bad") {
|
|
validIOScore = false;
|
|
}
|
|
else {
|
|
validIOScore = true;
|
|
}
|
|
|
|
if(isGoodFtue()) {
|
|
onSuccessfulScore();
|
|
}
|
|
else {
|
|
onFailedScore();
|
|
}
|
|
|
|
renderScoringStopped();
|
|
postDiagnostic();
|
|
}
|
|
|
|
function onFailedScore() {
|
|
rest.userCertifiedGear({success: false});
|
|
}
|
|
|
|
function onSuccessfulScore() {
|
|
rest.userCertifiedGear({success: true});
|
|
}
|
|
|
|
// 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(refocused) {
|
|
if(scoring) {return;}
|
|
scoring = true;
|
|
initializeBackButtonState();
|
|
validLatencyScore = false;
|
|
latencyScore = null;
|
|
if(!refocused) {
|
|
// don't reset a valid IO score on refocus
|
|
validIOScore = false;
|
|
ioScore = null;
|
|
}
|
|
|
|
renderScoringStarted();
|
|
|
|
// this timer exists to give UI time to update for renderScoringStarted before blocking nature of jamClient.FTUESave(save) kicks in
|
|
setTimeout(function () {
|
|
logger.debug("Calling FTUESave(false)");
|
|
jamClient.FTUESave(false);
|
|
|
|
var latency = jamClient.FTUEGetExpectedLatency();
|
|
latencyScore = latency;
|
|
|
|
// prod user to watch video if the previous type and new type changed
|
|
if(!shownInputProdOnce && isInputAudioTypeDifferentFromLastTime()) {
|
|
context.JK.prodBubble($watchVideoInput, 'ftue-watch-video', {}, {positions:['top', 'right']});
|
|
shownInputProdOnce = true;
|
|
}
|
|
|
|
// prod user to watch video if the previous type and new type changed
|
|
if(!shownOutputProdOnce && isOutputAudioTypeDifferentFromLastTime()) {
|
|
context.JK.prodBubble($watchVideoOutput, 'ftue-watch-video', {}, {positions:['top', 'right']});
|
|
shownOutputProdOnce = true;
|
|
}
|
|
|
|
updateScoreReport(latency);
|
|
|
|
if(refocused) {
|
|
context.JK.prodBubble($scoreReport, 'refocus-rescore', {validIOScore: validIOScore}, {positions:['top', 'left']});
|
|
}
|
|
|
|
// if there was a valid latency score, go on to the next step
|
|
if (validLatencyScore) {
|
|
// reuse valid IO score if this is on refocus
|
|
if(refocused && validIOScore) {
|
|
renderIOScore(null, null, null, 'starting', 'starting', 'starting');
|
|
processIOScore(ioScore);
|
|
}
|
|
else {
|
|
renderIOScore(null, null, null, 'starting', 'starting', 'starting');
|
|
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
|
|
renderIOScoringStarted(testTimeSeconds);
|
|
renderIOCountdown(testTimeSeconds);
|
|
var interval = setInterval(function () {
|
|
testTimeSeconds -= 1;
|
|
renderIOCountdown(testTimeSeconds);
|
|
|
|
if(testTimeSeconds == startTime) {
|
|
logger.debug("Starting IO Perf Test starting at " + startTime + "s in")
|
|
context.jamClient.FTUEStartIoPerfTest();
|
|
}
|
|
|
|
if (testTimeSeconds == 0) {
|
|
clearInterval(interval);
|
|
renderIOScoringStopped();
|
|
logger.debug("Ending IO Perf Test at " + testTimeSeconds + "s in")
|
|
var io = context.jamClient.FTUEGetIoPerfData();
|
|
ioScore = io;
|
|
processIOScore(io);
|
|
}
|
|
}, 1000);
|
|
}
|
|
}
|
|
else {
|
|
onFailedScore();
|
|
renderIOScore(null, null, null, 'skip', 'skip', 'skip');
|
|
renderScoringStopped();
|
|
postDiagnostic();
|
|
}
|
|
}, 250);
|
|
}
|
|
|
|
function initializeAudioInputChanged() {
|
|
$audioInput.unbind('change').change(audioDeviceChanged);
|
|
}
|
|
|
|
function initializeAudioOutputChanged() {
|
|
$audioOutput.unbind('change').change(audioDeviceChanged);
|
|
}
|
|
|
|
function handleNext() {
|
|
|
|
if(!savedProfile) {
|
|
var $assignedInputs = $inputChannels.find('input[type="checkbox"]:checked');
|
|
var $assignedOutputs = $outputChannels.find('input[type="checkbox"]:checked');
|
|
|
|
var errors = [];
|
|
if($assignedInputs.length == 0) {
|
|
errors.push("There must be at least one selected input ports.");
|
|
}
|
|
if($assignedInputs.length > 12) {
|
|
errors.push("There can only be up to 12 selected inputs ports.");
|
|
}
|
|
if($assignedOutputs.length != 2) {
|
|
errors.push("There must be exactly 2 selected output ports.");
|
|
}
|
|
var $errors = $('<ul></ul>');
|
|
context._.each(errors, function(error) {
|
|
$errors.append('<li>' + error + '</li>');
|
|
});
|
|
|
|
if(errors.length > 0) {
|
|
context.JK.Banner.showAlert({html:$errors});
|
|
return false;
|
|
}
|
|
else {
|
|
context.jamClient.FTUESetMusicProfileName(context.JK.createProfileName(selectedDeviceInfo));
|
|
context.jamClient.FTUESave(true);
|
|
savedProfile = true;
|
|
context.JK.GA.trackAudioTestCompletion(context.JK.detectOS());
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
function onFocus() {
|
|
if(!scoring && validDevice && getSelectedInputs().length > 0 && getSelectedOutputs().length == 2 ) {
|
|
// in the case the user has been unselecting ports, re-enforce minimum viable channels
|
|
validDevice = autoSelectMinimumValidChannels();
|
|
attemptScore(true);
|
|
}
|
|
}
|
|
|
|
function newSession() {
|
|
savedProfile = false;
|
|
deviceInformation = context.JK.loadDeviceInfo();
|
|
resetState();
|
|
initializeFormElements();
|
|
initializeNextButtonState();
|
|
initializeWatchVideo();
|
|
initializeASIOButtons();
|
|
initializeKnobs();
|
|
initializeResync();
|
|
}
|
|
|
|
function beforeShow() {
|
|
$(window).on('focus', onFocus);
|
|
}
|
|
|
|
function beforeHide() {
|
|
logger.debug("unregistering focus watch")
|
|
$(window).off('focus', onFocus);
|
|
}
|
|
|
|
function resetState() {
|
|
selectedDeviceInfo = null;
|
|
invalidateScore();
|
|
clearInputPorts();
|
|
clearOutputPorts();
|
|
resetFrameBuffers();
|
|
updateDialogForCurrentDevices();
|
|
}
|
|
|
|
function initialize(_$step) {
|
|
$step = _$step;
|
|
|
|
$watchVideoInput = $step.find('.watch-video.audio-input');
|
|
$watchVideoOutput = $step.find('.watch-video.audio-output');
|
|
$audioInput = $step.find('.select-audio-input-device');
|
|
$audioOutput = $step.find('.select-audio-output-device');
|
|
$bufferIn = $step.find('.select-buffer-in');
|
|
$bufferOut = $step.find('.select-buffer-out');
|
|
$frameSize = $step.find('.select-frame-size');
|
|
$inputChannels = $step.find('.input-ports');
|
|
$outputChannels = $step.find('.output-ports');
|
|
$knobs = $step.find('.frame-and-buffers');
|
|
$scoreReport = $step.find('.results');
|
|
$latencyScoreSection = $scoreReport.find('.latency-score-section');
|
|
$latencyScore = $scoreReport.find('.latency-score');
|
|
$latencyHeader = $scoreReport.find('.latency');
|
|
$ioHeader = $scoreReport.find('.io');
|
|
$ioScoreSection = $scoreReport.find('.io-score-section');
|
|
$ioRate = $scoreReport.find('.io-rate');
|
|
$ioRateScore = $scoreReport.find('.io-rate-score');
|
|
$ioVar = $scoreReport.find('.io-var');
|
|
$ioVarScore = $scoreReport.find('.io-var-score');
|
|
$ioCountdown = $scoreReport.find('.io-countdown');
|
|
$ioCountdownSecs = $scoreReport.find('.io-countdown .secs');
|
|
$resultsText = $scoreReport.find('.results-text');
|
|
$unknownText = $scoreReport.find('.unknown-text');
|
|
$asioInputControlBtn = $step.find('.asio-settings-input-btn');
|
|
$asioOutputControlBtn = $step.find('.asio-settings-output-btn');
|
|
$resyncBtn = $step.find('.resync-btn');
|
|
$templateAudioPort = $('#template-audio-port');
|
|
$launchLoopbackBtn = $('.loopback-test');
|
|
$instructions = $('.instructions');
|
|
operatingSystem = context.JK.GetOSAsString();
|
|
}
|
|
|
|
this.handleNext = handleNext;
|
|
this.newSession = newSession;
|
|
this.beforeShow = beforeShow;
|
|
this.beforeHide = beforeHide;
|
|
this.initialize = initialize;
|
|
|
|
self = this;
|
|
return this;
|
|
};
|
|
|
|
})(window, jQuery); |