(function (context, $) { "use strict"; context.JK = context.JK || {}; context.JK.StepSelectGear = function (app, dialog) { var ALERT_NAMES = context.JK.ALERT_NAMES; var EVENTS = context.JK.EVENTS; var ASSIGNMENT = context.JK.ASSIGNMENT; var VOICE_CHAT = context.JK.VOICE_CHAT; var AUDIO_DEVICE_BEHAVIOR = context.JK.AUDIO_DEVICE_BEHAVIOR; var gearUtils = context.JK.GearUtils; var modUtils = context.JK.ModUtils; var self = null; var $step = null; var logger = context.JK.logger; var rest = context.JK.Rest(); var frameBuffers = new context.JK.FrameBuffers(app); var gearTest = new context.JK.GearTest(app); var loopbackShowing = false; var adjustGearSettingsShowing = false; var wizard = null; // the goal of lastFailureAnalytics and trackedPass are to send only a single AudioTest 'Pass' or 'Failed' event, per FTUE wizard open/close var lastFailureAnalytics = {}; var trackedPass = false; var $watchVideoInput = null; var $watchVideoOutput = null; var $audioInput = null; var $audioOutput = null; var $inputChannels = null; var $outputChannels = null; var $knobs = null; var $adjustSettingsLink = null; var $adjustGearForIoFail = null; var $scoreReport = null; var $asioInputControlBtn = null; var $asioOutputControlBtn = null; var $resyncBtn = null; var $templateAudioPort = null; var $launchLoopbackBtn = null; var $instructions = null; var $templateDeviceNotValid = null; var $resyncStatus = null; var $resyncStatusText = null; var operatingSystem = null; var iCheckIgnore = false; 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 savedProfile = false; var queueUpdateDeviceList = false; var cancelRescanFunc = null; var initialScan = false; // 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 setInputAudioDevice(value) { context.JK.dropdown($audioInput.val(value).easyDropDown('select', value.toString(), true)) } function setOutputAudioDevice(value) { if(value != "" && value == selectedAudioInput()) { value = ''; // to force Same as Input } context.JK.dropdown($audioOutput.val(value).easyDropDown('select', value.toString(), true)) } function initializeNextButtonState() { dialog.setNextState(gearTest.isGoodFtue() || dialog.getLoopbackWizard().getGearTest().isGoodFtue() || dialog.getAdjustGearSettings().getGearTest().isGoodFtue()); } function initializeBackButtonState() { dialog.setBackState(!gearTest.isScoring()); } function allInputDevices() { var allInputDevices = []; context._.each(deviceInformation, function (deviceInfo, deviceId) { if(deviceInfo.inputCount > 0) { allInputDevices.push({deviceId: deviceId, deviceInfo: deviceInfo}); } }); return allInputDevices; } function allOutputDevices() { var allOutputDevices = []; context._.each(deviceInformation, function (deviceInfo, deviceId) { if(deviceInfo.outputCount > 0) { allOutputDevices.push({deviceId: deviceId, deviceInfo: deviceInfo}); } }); return allOutputDevices; } function initializeAudioInput() { var optionsHtml = ''; optionsHtml = ''; context._.each(allInputDevices(), function (device) { optionsHtml += ''; }); $audioInput.html(optionsHtml); context.JK.dropdown($audioInput); $audioInput.easyDropDown('enable') initializeAudioInputChanged(); } function initializeAudioOutput() { var optionsHtml = ''; optionsHtml = ''; context._.each(allOutputDevices(), function (device) { optionsHtml += ''; }); $audioOutput.html(optionsHtml); context.JK.dropdown($audioOutput); $audioOutput.easyDropDown('disable'); // enable once they pick something in input initializeAudioOutputChanged(); } // reloads the backend's channel state for the currently selected audio devices, // and update's the UI accordingly function initializeChannels() { musicPorts = context.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']}); } else { // this path should be impossible because we filter output devices with 0 inputs from the input device dropdown // but we might flip that, so it's nice to leave this in to catch us later context.JK.Banner.showAlert('To be a valid input audio device, the device must have at least 1 input port.'); } 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'); } } 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}); } else { // this path indicates that the user has deliberately chosen a device, so we need to tell them that this device does not work with JamKazam context.JK.prodBubble($audioOutput.closest('.easydropdown-wrapper'), 'select-output', {}, {positions:['right', 'top'], duration:7000}); } return false; } // 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 (gearUtils.isChannelAssigned(inputChannel)) { assigned += 1; } }); var newAssignment = Math.floor(assigned / 2) + 1; return newAssignment; } function reassignInputChannels() { assertFTUEProfile(); 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; }); } function reassignOutputChannels() { assertFTUEProfile(); var $assignedOutputs = $outputChannels.find('input[type="checkbox"]:checked'); context._.each($assignedOutputs, function (assignedOutput) { var $assignedOutput = $(assignedOutput); var assignedChannelId = $assignedOutput.attr('data-id'); logger.debug("re-assigning output channel %o", assignedChannelId); context.jamClient.TrackSetAssignment(assignedChannelId, true, ASSIGNMENT.OUTPUT); }); } function inputChannelChanged() { if (iCheckIgnore) return; assertFTUEProfile(); 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 reassignInputChannels(); } 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(); } 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 (gearUtils.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 (gearUtils.isChannelAssigned(outputChannel)) { $checkbox.attr('checked', 'checked'); } context.JK.checkbox($checkbox); $checkbox.on('ifChanged', outputChannelChanged); $outputChannels.append($outputPort); }); } function initializeLoopback() { $launchLoopbackBtn.unbind('click').click(function() { $(dialog.getLoopbackWizard().getDialog()).one(EVENTS.DIALOG_CLOSED, function() { loopbackShowing = false; if(dialog.getLoopbackWizard().getGearTest().isGoodFtue()) { gearTest.resetScoreReport(); gearTest.showLoopbackDone(); setTimeout(function() { context.JK.prodBubble(dialog.getWizard().getNextButton(), 'can-move-on', {}, {positions:['top'], offsetParent: dialog.getWizard().getDialog()}); }, 300); } initializeNextButtonState(); initializeBackButtonState(); }) loopbackShowing = true; app.layout.showDialog('loopback-wizard') return false; }) } function onAdjustGearRequested() { if(gearTest.isScoring()) {logger.debug("ignoring adjust-gear request while scoring"); return false;} app.layout.showDialog('adjust-gear-speed-dialog').one(EVENTS.DIALOG_CLOSED, function(e, data) { adjustGearSettingsShowing = false; var adjustGearTest = data.result; if(!data.canceled) { if(adjustGearTest.isGoodFtue()) { // update our own frame buffers to reflect any changes made by the adjust dialog frameBuffers.setFramesize(context.jamClient.FTUEGetFrameSize()) frameBuffers.setBufferIn(context.jamClient.FTUEGetInputLatency()) frameBuffers.setBufferOut(context.jamClient.FTUEGetOutputLatency()) gearTest.resetScoreReport(); gearTest.showGearAdjustmentDone(); setTimeout(function() { context.JK.prodBubble(dialog.getWizard().getNextButton(), 'can-move-on', {}, {positions:['top'], offsetParent: dialog.getWizard().getDialog()}); }, 300); } initializeNextButtonState(); initializeBackButtonState(); } else { logger.debug("adjust-gear-speed was cancelled; ignoring") } }) adjustGearSettingsShowing = true; return false; } function initializeFormElements() { if (!deviceInformation) throw "devices are not initialized"; initializeAudioInput(); initializeAudioOutput(); initializeLoopback(); } function clearInputPorts() { $inputChannels.empty(); } function clearOutputPorts() { $outputChannels.empty(); } function audioInputDeviceUnselected() { validDevice = false; setOutputAudioDevice(''); resetState(); } function renderScoringStarted() { initializeBackButtonState(); initializeNextButtonState(); freezeAudioInteraction(); } function renderScoringStopped() { initializeNextButtonState(); initializeBackButtonState(); unfreezeAudioInteraction(); } function freezeAudioInteraction() { logger.debug("freezing audio interaction"); $audioInput.attr("disabled", "disabled").easyDropDown('disable'); $audioOutput.attr("disabled", "disabled").easyDropDown('disable'); frameBuffers.disable(); $asioInputControlBtn.on("click", false).addClass('disabled'); $asioOutputControlBtn.on("click", false).addClass('disabled'); $resyncBtn.on('click', false).addClass('disabled'); 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'); frameBuffers.enable(); $asioInputControlBtn.off("click", false).removeClass('disabled'); $asioOutputControlBtn.off("click", false).removeClass('disabled') $resyncBtn.off('click', false).removeClass('disabled') $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() { gearTest.invalidateScore(); dialog.getLoopbackWizard().getGearTest().invalidateScore(); dialog.getAdjustGearSettings().getGearTest().invalidateScore(); initializeNextButtonState(); } function initializeASIOButtons() { $asioInputControlBtn.unbind('click').click(function () { if(gearTest.isScoring()) { return false; } context.jamClient.FTUEOpenControlPanel(selectedAudioInput()); return false; }); $asioOutputControlBtn.unbind('click').click(function () { if(gearTest.isScoring()) { return false; } context.jamClient.FTUEOpenControlPanel(selectedAudioOutput()); return false; }); } function onFramesizeChanged() { context.JK.prodBubble($resyncBtn, 'push-resync-when-done', {}, {positions:['top']}); updateDefaultBuffers(); jamClient.FTUESetFrameSize(frameBuffers.selectedFramesize()); invalidateScore(); } function onBufferInChanged() { context.JK.prodBubble($resyncBtn, 'push-resync-when-done', {}, {positions:['top']}); jamClient.FTUESetInputLatency(frameBuffers.selectedBufferIn()); invalidateScore(); } function onBufferOutChanged() { context.JK.prodBubble($resyncBtn, 'push-resync-when-done', {}, {positions:['top']}); jamClient.FTUESetOutputLatency(frameBuffers.selectedBufferOut()); invalidateScore(); } 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 () { scheduleRescanSystem(function() { if (getSelectedInputs().length > 0 && getSelectedOutputs().length == 2) { logger.debug("after rescan, ready to attempt score") attemptScore(); } else { logger.debug("after rescan, not ready to attempt score") } }, 3000, false); return false; }) } function rescan() { // start audio if already running, false = don't reload last audio configuration, true = re-init tracks var result = context.jamClient.ReloadAudioSystem(context.jamClient.IsAudioStarted(), false, true); // get the current list of input devices, and output devices, in the UI // these are arrays with this structure // {deviceId: id, deviceInfo: deviceInfo} var originalInputDevices = allInputDevices(); var originalOutputDevices = allOutputDevices(); var originalInputDeviceIds = originalInputDevices.map(function(device){return device.deviceId}); var originalOutputDevicesId = originalOutputDevices.map(function(device){return device.deviceId}); var selectedInput = selectedAudioInput(); var selectedOutput = selectedAudioOutput(); // reload device info now that the scan is complete deviceInformation = gearUtils.loadDeviceInfo(); var inputDevices = allInputDevices(); var outputDevices = allOutputDevices(); var inputDeviceIds = inputDevices.map(function(device){return device.deviceId}); var outputDeviceIds = outputDevices.map(function(device){return device.deviceId}); var missingInputDeviceIds = $(originalInputDeviceIds).not(inputDeviceIds).get(); var newInputDeviceIds = $(inputDeviceIds).not(originalInputDeviceIds).get(); var missingOutputDeviceIds = $(originalOutputDevicesId).not(outputDeviceIds).get(); var newOutputDeviceIds = $(outputDeviceIds).not(originalOutputDevicesId).get(); // these are used later to show prod bubbles var missingInputDevice = null; var newInputDevice = null; var missingOutputDevice = null; var newOutputDevice = null; if(missingInputDeviceIds.length > 0 || newOutputDeviceIds.length > 0) { initializeAudioInput(); if(missingInputDeviceIds.indexOf(selectedInput) > -1) { // the user had a device selected that's no longer here. we need to notify them of this, and auto-select 'no device' audioInputDeviceUnselected(); missingInputDevice = originalInputDevices.filter(function(inputDevice) { return selectedInput == inputDevice.deviceId })[0].deviceInfo.displayName; } else { setInputAudioDevice(selectedInput); if(missingInputDeviceIds.length > 0) { missingInputDevice = originalInputDevices.filter(function(inputDevice) { return missingInputDeviceIds[0] == inputDevice.deviceId })[0].deviceInfo.displayName; } if(newInputDeviceIds.length > 0) { newInputDevice = inputDevices.filter(function(inputDevice) { return newInputDeviceIds[0] == inputDevice.deviceId })[0].deviceInfo.displayName; } } } if(missingOutputDeviceIds.length > 0 || newOutputDeviceIds.length > 0) { initializeAudioOutput(); if(missingOutputDeviceIds.indexOf(selectedOutput) > -1) { // the user had a device selected that's no longer here. we need to notify them of this, and auto-select 'no device' setOutputAudioDevice(''); missingInputDevice = originalOutputDevices.filter(function(outputDevice) { return selectedOutput == outputDevice.deviceId })[0].deviceInfo.displayName; } else { setOutputAudioDevice(selectedOutput); if(missingOutputDeviceIds.length > 0) { missingOutputDevice = originalOutputDevices.filter(function(outputDevice) { return missingOutputDeviceIds[0] == outputDevice.deviceId })[0].deviceInfo.displayName; } if(newOutputDeviceIds.length > 0) { newOutputDevice = outputDevices.filter(function(outputDevice) { return newOutputDeviceIds[0] == outputDevice.deviceId })[0].deviceInfo.displayName; } } } // handle prod bubbles in case devices are gone if(missingInputDevice || missingOutputDevice || newInputDevice || newOutputDevice) { if(missingInputDevice == missingOutputDevice || newInputDevice == newOutputDevice) { // we only notify over the input device in the case the device is new/missing from both input/output, to reduce too much bubble action context.JK.prodBubble($audioInput.closest('.easydropdown-wrapper'), 'gear-wizard-inputs-changed', {missingInputDevice: missingInputDevice, newInputDevice: newInputDevice}, {positions:['top'], duration:8000}); } else { if(missingInputDevice || newInputDevice) { context.JK.prodBubble($audioInput.closest('.easydropdown-wrapper'), 'gear-wizard-inputs-changed', {missingInputDevice: missingInputDevice, newInputDevice: newInputDevice}, {positions:['top'], duration:8000}); } if(missingOutputDevice || newOutputDevice) { context.JK.prodBubble($audioOutput.closest('.easydropdown-wrapper'), 'gear-wizard-outputs-changed', {missingOutputDevice: missingOutputDevice, newOutputDevice: newOutputDevice}, {positions:[ 'top'], duration:8000}); } } } // repair channel selections, because the backend cleared them in RebuildAudioSystem reassignInputChannels(); reassignOutputChannels(); } function cancelRescan() { if(cancelRescanFunc) { cancelRescanFunc(); cancelRescanFunc = null; } } function scheduleRescanSystem(after, time, checkingForGear) { cancelRescanFunc = gearUtils.scheduleAudioRestart('gear-wizard-select-gear', time, function() { freezeAudioInteraction(); $resyncBtn.addClass('scanning').text('RESYNC') $resyncStatusText.text(checkingForGear ? 'CHECKING GEAR' : 'RESYNCING...'); $resyncStatus.show(); }, function(cancel) { $resyncBtn.removeClass('scanning').text('RESYNC') unfreezeAudioInteraction(); $resyncStatus.hide(); if(!cancel) { rescan(); if(after) { after(); } } }); } // 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 = gearUtils.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) { $adjustSettingsLink.hide(); return false; } jamClient.FTUESetInputLatency(frameBuffers.selectedBufferIn()); jamClient.FTUESetOutputLatency(frameBuffers.selectedBufferOut()); jamClient.FTUESetFrameSize(frameBuffers.selectedFramesize()); // 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; } // further, check if we have both inputs and outputs defined; if so, show ? to allow launch of adjust gear settings dialog if(modUtils.getGear('show_frame_options')) { $adjustSettingsLink.show(); } 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 || modUtils.getGear('show_frame_options'))) { $knobs.show(); } else { $knobs.hide(); $adjustSettingsLink.hide(); } // handle ASIO visibility 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(); } updateDefaultFrameSize(); updateDefaultBuffers(); } function updateDefaultFrameSize() { if(selectedDeviceInfo && (selectedDeviceInfo.input.info.type == 'Win32_wdm' || selectedDeviceInfo.output.info.type == 'Win32_wdm')) { frameBuffers.setFramesize('10'); } else { frameBuffers.setFramesize('2.5') } jamClient.FTUESetFrameSize(frameBuffers.selectedFramesize()); } function updateDefaultBuffers() { gearUtils.updateDefaultBuffers(selectedDeviceInfo, frameBuffers) } // 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) { gearTest.attemptScore(selectedDeviceInfo, refocused); } function initializeAudioInputChanged() { $audioInput.unbind('change').change(audioDeviceChanged); } function initializeAudioOutputChanged() { $audioOutput.unbind('change').change(audioDeviceChanged); } function onGearTestStarted(e, data) { renderScoringStarted(); } function onGearTestDone(e, data) { renderScoringStopped(); gearUtils.postDiagnostic(operatingSystem, deviceInformation, selectedDeviceInfo, gearTest, frameBuffers, true); if(queueUpdateDeviceList) { queueUpdateDeviceList = false; updateDeviceList(); } } function getLastAudioTestFailAnalytics() { return lastFailureAnalytics; } function storeLastFailureForAnalytics(platform, reason, data) { if(!trackedPass) { lastFailureAnalytics = { platform: platform, reason: reason, data: data } } } function prodUserToTweakASIOSettings($btn) { setTimeout(function() { context.JK.prodBubble($btn, 'tweak-asio-settings', {}, {positions:['top']}); }, 300) } function onGearTestFail(e, data) { renderScoringStopped(); gearUtils.postDiagnostic(operatingSystem, deviceInformation, selectedDeviceInfo, gearTest, frameBuffers, true); if(data.reason == "latency") { console.log("selectedDeviceInfo", selectedDeviceInfo) if(selectedDeviceInfo.input.info.type.indexOf('Win32_asio') > -1) { prodUserToTweakASIOSettings($asioInputControlBtn) } else if(selectedDeviceInfo.output.info.type.indexOf('Win32_asio') > -1) { prodUserToTweakASIOSettings($asioOutputControlBtn) } storeLastFailureForAnalytics(context.JK.detectOS(), context.JK.GA.AudioTestFailReasons.latency, data.latencyScore); } else if(data.reason = "io") { if(data.ioTarget == 'bad') { storeLastFailureForAnalytics(context.JK.detectOS(), context.JK.GA.AudioTestFailReasons.ioTarget, data.ioTargetScore); } else { storeLastFailureForAnalytics(context.JK.detectOS(), context.JK.GA.AudioTestFailReasons.ioVariance, data.ioVarianceScore); } } else if(data.reason == 'invalid_configuration') { if(data.data.reason == 'device_failure') { var specificErrors = []; if(selectedDeviceInfo.input.id == selectedDeviceInfo.output.id && data.data.input) { specificErrors.push("The configured device (" + selectedDeviceInfo.input.info.displayName + ") is not connected or not functioning."); } else { if(data.data.input) { specificErrors.push("The configured input device (" + selectedDeviceInfo.input.info.displayName + ") is not connected or not functioning."); } if(data.data.output) { specificErrors.push("The configured output device (" + selectedDeviceInfo.output.info.displayName + ") is not connected or not functioning."); } } var specificResolutions = []; if(selectedDeviceInfo.input.info.type.indexOf('Win32_asio') > -1 || selectedDeviceInfo.output.info.type.indexOf('Win32_asio') > -1) { // specificResolutions.push("Read over this article") specificResolutions.push("Select the ASIO SETTINGS... button and try different settings."); } if(selectedDeviceInfo.input.info.type == 'Win32_wdm' || selectedDeviceInfo.output.info.type == 'Win32_wdm') { // specificResolutions.push("Read over this article") specificResolutions.push("Change the Frame, Buffer In, or Buffer Out settings."); } specificResolutions.push("Reconnect the device if possible."); specificResolutions.push("Restart the JamKazam application."); specificResolutions.push("Make sure you have the latest drivers installed for your gear."); specificResolutions.push("In rare cases, restarting your computer can help."); context.JK.Banner.showAlert('Invalid Audio Device', $(context._.template($templateDeviceNotValid.html(), {specific_errors: specificErrors, specific_resolutions: specificResolutions }, { variable: 'data' }))); } else { context.JK.alertSupportedNeeded("Unable to configure device. " + data.data) } } else { logger.error("unknown reason in onGearTestFail: " + data.reason) } if(queueUpdateDeviceList) { queueUpdateDeviceList = false; updateDeviceList(); } } function onGearTestInvalidated(e, data) { initializeNextButtonState(); } function handleHelp() { if(operatingSystem == "Win32") { return "https://jamkazam.desk.com/customer/portal/articles/1599818-first-time-setup---step-2---select-and-test-audio-gear---windows-"; } else { return "https://jamkazam.desk.com/customer/portal/articles/1599845-first-time-setup---step-2---select-and-test-audio-gear---mac"; } } function handleNext() { 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 = $(''); context._.each(errors, function(error) { $errors.append('
  • ' + error + '
  • '); }); if(errors.length > 0) { context.JK.Banner.showAlert({html:$errors}); return false; } if(!savedProfile) { context.jamClient.FTUESetMusicProfileName(gearUtils.createProfileName(selectedDeviceInfo)); var result = context.jamClient.FTUESave(true); if(result && result.error) { // failed logger.warn("unable to FTUESave(true). reason:" + result.detail); context.JK.alertSupportedNeeded("Unable to persist the audio profile. " + result.detail); return false; } else { context.JK.GA.trackAudioTestCompletion(context.JK.detectOS()); trackedPass = true; lastFailureAnalytics = null; savedProfile = true; } } // keep the shared state between step 2 and step 3 up-to-date wizard.setChosenInputs(context._.map($assignedInputs, function(input) { return $(input).attr('data-id') })); return true; } function onFocus() { if(validDevice && !loopbackShowing && !adjustGearSettingsShowing && !gearTest.isScoring() && getSelectedInputs().length > 0 && getSelectedOutputs().length == 2 ) { scheduleRescanSystem(function() { attemptScore(true); }, 3000, false) } } function newSession() { savedProfile = false; initialScan = false; deviceInformation = gearUtils.loadDeviceInfo(); resetState(); initializeFormElements(); initializeNextButtonState(); initializeWatchVideo(); initializeASIOButtons(); initializeResync(); } function beforeWizardShow() { lastFailureAnalytics = null; trackedPass = false; } function beforeShow() { $(window).on('focus', onFocus); context.JK.onBackendEvent(ALERT_NAMES.USB_CONNECTED, 'select-gear', onUsbDeviceConnected); context.JK.onBackendEvent(ALERT_NAMES.USB_DISCONNECTED, 'select-gear', onUsbDeviceDisconnected); initializeNextButtonState(); context.JK.onBackendEvent(ALERT_NAMES.AUDIO_DEVICE_NOT_PRESENT, 'gear_test', gearTest.onInvalidAudioDevice); if(!initialScan) { scheduleRescanSystem(null, 3000, true); initialScan = true; } } function beforeHide() { cancelRescan(); logger.debug("unregistering focus watch") context.JK.offBackendEvent(ALERT_NAMES.USB_CONNECTED, 'select-gear', onUsbDeviceConnected); context.JK.offBackendEvent(ALERT_NAMES.USB_DISCONNECTED, 'select-gear', onUsbDeviceDisconnected); $(window).off('focus', onFocus); context.JK.offBackendEvent(ALERT_NAMES.AUDIO_DEVICE_NOT_PRESENT, 'gear_test', gearTest.onInvalidAudioDevice); } function onUsbDeviceConnected() { if(!context.jamClient.IsFrontendVisible()) {return;} // don't handle USB events when minimized updateDeviceList(); } function onUsbDeviceDisconnected() { if(!context.jamClient.IsFrontendVisible()) {return;} // don't handle USB events when minimized updateDeviceList(); } function updateDeviceList() { if(gearTest.isScoring()) { queueUpdateDeviceList = true; } else { scheduleRescanSystem(null, 5000, false); } } function resetState() { selectedDeviceInfo = null; invalidateScore(); clearInputPorts(); clearOutputPorts(); frameBuffers.resetValues(); updateDialogForCurrentDevices(); } function assertFTUEProfile() { if(savedProfile) {return;} // once we save the profile, it's name no longer starts with FTUE var profileName = context.jamClient.FTUEGetMusicProfileName(); if(profileName && profileName.indexOf('FTUE') != 0) { logger.debug("the profile name *must* start with FTUE during step 2. name=" + profileName) context.JK.alertSupportedNeeded('The application is no longer modifying a new profile. Please restart the application and try the gear wizard again.'); } } function initialize(_$step, _wizard) { $step = _$step; wizard = _wizard; $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'); $templateDeviceNotValid = $('#device-not-valid'); $inputChannels = $step.find('.input-ports'); $outputChannels = $step.find('.output-ports'); $knobs = $step.find('.frame-and-buffers'); $adjustSettingsLink = $knobs.find('.adjust-gear-settings') $adjustGearForIoFail = $step.find(".adjust-gear-for-io-fail") $scoreReport = $step.find('.results'); $asioInputControlBtn = $step.find('.asio-settings-input-btn'); $asioOutputControlBtn = $step.find('.asio-settings-output-btn'); $resyncBtn = $step.find('.resync-btn'); $templateAudioPort = $('#template-audio-port'); $launchLoopbackBtn = $step.find('.loopback-test'); $instructions = $step.find('.instructions'); $resyncStatus = $step.find('.resync-status'); $resyncStatusText = $step.find('.resynctext'); operatingSystem = context.JK.GetOSAsString(); frameBuffers.initialize($knobs); $(frameBuffers) .on(frameBuffers.FRAMESIZE_CHANGED, onFramesizeChanged) .on(frameBuffers.BUFFER_IN_CHANGED, onBufferInChanged) .on(frameBuffers.BUFFER_OUT_CHANGED, onBufferOutChanged) .on(frameBuffers.ADJUST_GEAR_LINK_CLICKED, onAdjustGearRequested) gearTest.initialize($scoreReport, true) $(gearTest) .on(gearTest.GEAR_TEST_START, onGearTestStarted) .on(gearTest.GEAR_TEST_DONE, onGearTestDone) .on(gearTest.GEAR_TEST_FAIL, onGearTestFail) .on(gearTest.GEAR_TEST_INVALIDATED_ASYNC, onGearTestInvalidated) $adjustGearForIoFail.click(onAdjustGearRequested); } this.getLastAudioTestFailAnalytics = getLastAudioTestFailAnalytics; this.handleNext = handleNext; this.newSession = newSession; this.handleHelp = handleHelp; this.beforeShow = beforeShow; this.beforeHide = beforeHide; this.beforeWizardShow = beforeWizardShow; this.initialize = initialize; self = this; return this; }; })(window, jQuery);