diff --git a/web/app/assets/javascripts/application.js b/web/app/assets/javascripts/application.js index f9d230ae6..cd7ff04cb 100644 --- a/web/app/assets/javascripts/application.js +++ b/web/app/assets/javascripts/application.js @@ -39,3 +39,4 @@ //= require utils //= require custom_controls //= require_directory . +//= require_directory ./gear diff --git a/web/app/assets/javascripts/fakeJamClient.js b/web/app/assets/javascripts/fakeJamClient.js index af10cb53b..1175d9b28 100644 --- a/web/app/assets/javascripts/fakeJamClient.js +++ b/web/app/assets/javascripts/fakeJamClient.js @@ -38,7 +38,15 @@ function RestartApplication() { } + function FTUECancel() { + } + function FTUEGetMusicProfileName() { + return "default" + } + function FTUESetMusicProfileName() { + + } function FTUEGetInputLatency() { dbg("FTUEGetInputLatency"); return 2; @@ -85,6 +93,9 @@ function FTUEGetStatus() { return ftueStatus; } function FTUESetStatus(b) { ftueStatus = b; } function FTUESetMusicDevice(id) { dbg("FTUESetMusicDevice"); } + function FTUEGetAudioDevices() { + return {"devices":[{"display_name":"Built-in","guid":"Built-in","input_count":1,"name":"Built-in","output_count":1,"port_audio_name":"Built-in"},{"display_name":"JamKazam Virtual Monitor","guid":"JamKazam Virtual Monitor","input_count":0,"name":"JamKazam Virtual Monitor","output_count":1,"port_audio_name":"JamKazam Virtual Monitor"}]} + } function FTUEGetDevices() { dbg('FTUEGetMusicDevices'); return { @@ -113,6 +124,11 @@ "Multichannel (FW AP Multi) - Channel 1/Multichannel (FW AP Multi) - Channel 2" }; } + function FTUESetInputMusicDevice() { } + function FTUESetOutputMusicDevice() { } + function FTUEGetInputMusicDevice() { return null; } + function FTUEGetOutputMusicDevice() { return null; } + function FTUESetMusicInput() { dbg('FTUESetMusicInput'); } function FTUESetChatInput() { dbg('FTUESetChatInput'); } function FTUESetMusicOutput() { dbg('FTUESetMusicOutput'); } @@ -656,10 +672,17 @@ this.connected = true; // FTUE (round 3) + this.FTUESetInputMusicDevice = FTUESetInputMusicDevice; + this.FTUESetOutputMusicDevice = FTUESetOutputMusicDevice; + this.FTUEGetInputMusicDevice = FTUEGetInputMusicDevice; + this.FTUEGetOutputMusicDevice = FTUEGetOutputMusicDevice; this.FTUEGetChatInputVolume = FTUEGetChatInputVolume; this.FTUEGetChatInputs = FTUEGetChatInputs; this.FTUEGetDevices = FTUEGetDevices; this.FTUEGetFrameSize = FTUEGetFrameSize; + this.FTUECancel = FTUECancel; + this.FTUEGetMusicProfileName = FTUEGetMusicProfileName; + this.FTUESetMusicProfileName = FTUESetMusicProfileName; this.FTUEGetInputLatency = FTUEGetInputLatency; this.FTUEGetInputVolume = FTUEGetInputVolume; this.FTUEGetMusicInputs = FTUEGetMusicInputs; diff --git a/web/app/assets/javascripts/gear/gear_wizard.js b/web/app/assets/javascripts/gear/gear_wizard.js new file mode 100644 index 000000000..bdffef0dc --- /dev/null +++ b/web/app/assets/javascripts/gear/gear_wizard.js @@ -0,0 +1,237 @@ +(function (context, $) { + + "use strict"; + + + context.JK = context.JK || {}; + context.JK.GearWizard = function (app) { + + var $dialog = null; + var $wizardSteps = null; + var $currentWizardStep = null; + var step = 0; + var $templateSteps = null; + var $templateButtons = null; + var $templateAudioPort = null; + var $ftueButtons = null; + var $btnBack = null; + var $btnNext = null; + var $btnClose = null; + var $btnCancel = null; + + var self = this; + + var TOTAL_STEPS = 7; + var STEP_INTRO = 0; + var STEP_SELECT_DEVICE = 1; + var STEP_SELECT_TRACKS = 2; + var STEP_SELECT_CHAT = 3; + var STEP_DIRECT_MONITOR = 4; + var STEP_ROUTER_NETWORK = 5; + var STEP_SUCCESS = 6; + + var stepUnderstandGear = new context.JK.StepUnderstandGear(app, this); + var stepSelectGear = new context.JK.StepSelectGear(app, this); + var stepConfigureTracks = new context.JK.StepConfigureTracks(app, this); + var stepConfigureVoiceChat = new context.JK.StepConfigureVoiceChat(app, this); + var stepDirectMonitoring = new context.JK.StepDirectMonitoring(app, this); + var stepNetworkTest = new context.JK.StepNetworkTest(app, this); + var stepSuccess = new context.JK.StepSuccess(app, this); + + var STEPS = { + 0: stepUnderstandGear, + 1: stepSelectGear, + 2: stepConfigureTracks, + 3: stepConfigureVoiceChat, + 4: stepDirectMonitoring, + 5: stepNetworkTest, + 6: stepSuccess + } + + function beforeShowStep($step) { + var stepInfo = STEPS[step]; + + if (!stepInfo) { + throw "unknown step: " + step; + } + + stepInfo.beforeShow.call(stepInfo); + } + + function moveToStep() { + var $nextWizardStep = $wizardSteps.filter($('[layout-wizard-step=' + step + ']')); + + $wizardSteps.hide(); + + $currentWizardStep = $nextWizardStep; + + var $ftueSteps = $(context._.template($templateSteps.html(), {}, { variable: 'data' })); + var $activeStep = $ftueSteps.find('.ftue-stepnumber[data-step-number="' + step + '"]'); + $activeStep.addClass('.active'); + $activeStep.next().show(); // show the .ftue-step-title + $currentWizardStep.find('.ftuesteps').replaceWith($ftueSteps); + + // update buttons + var $ftueButtonsContent = $(context._.template($templateButtons.html(), {}, {variable: 'data'})); + + + $btnBack = $ftueButtonsContent.find('.btn-back'); + $btnNext = $ftueButtonsContent.find('.btn-next'); + $btnClose = $ftueButtonsContent.find('.btn-close'); + $btnCancel = $ftueButtonsContent.find('.btn-cancel'); + + // hide back button if 1st step or last step + if (step == 0 && step == TOTAL_STEPS - 1) { + $btnBack.hide(); + } + // hide next button if not on last step + if (step == TOTAL_STEPS - 1) { + $btnNext.hide(); + } + // hide close if on last step + if (step != TOTAL_STEPS - 1) { + $btnClose.hide(); + } + // hide cancel if not on last step + if (step == TOTAL_STEPS - 1) { + $btnCancel.hide(); + } + + $btnNext.on('click', next); + $btnBack.on('click', back); + $btnClose.on('click', closeDialog); + $btnCancel.on('click', closeDialog); + + $ftueButtons.empty(); + $ftueButtons.append($ftueButtonsContent); + + beforeShowStep($currentWizardStep); + $currentWizardStep.show(); + + } + + function reset() { + $currentWizardStep = null; + } + + // checks if we already have a profile called 'FTUE...'; if not, create one. if so, re-use it. + function findOrCreateFTUEProfile() { + var profileName = context.jamClient.FTUEGetMusicProfileName(); + + logger.debug("current profile name: " + profileName); + + if(profileName && profileName.indexOf('FTUE') == 0) { + + } + else { + var newProfileName = 'FTUEAttempt-' + new Date().getTime().toString(); + logger.debug("setting FTUE-prefixed profile name to: " + newProfileName); + context.jamClient.FTUESetMusicProfileName(newProfileName); + } + + var profileName = context.jamClient.FTUEGetMusicProfileName(); + + logger.debug("name on exit: " + profileName); + + } + + function beforeShow(args) { + context.jamClient.FTUECancel(); + context.jamClient.FTUESetStatus(false); + findOrCreateFTUEProfile(); + + step = args.d1; + if (!step) step = 0; + step = parseInt(step); + moveToStep(); + } + + function afterShow() { + + } + + function afterHide() { + context.jamClient.FTUESetStatus(true); + context.jamClient.FTUECancel(); + } + + function back() { + if ($(this).is('.button-grey')) return false; + step = step - 1; + moveToStep(); + return false; + } + + function next() { + if ($(this).is('.button-grey')) return false; + + step = step + 1; + + moveToStep(); + return false; + } + + function closeDialog() { + app.layout.closeDialog('gear-wizard'); + return false; + } + + function events() { + + } + + function route() { + + } + + function setNextState(enabled) { + + if(!$btnNext) return; + + $btnNext.removeClass('button-orange button-grey'); + + if (enabled) { + $btnNext.addClass('button-orange'); + } + else { + $btnNext.addClass('button-grey'); + } + } + + function initialize() { + + // on initial page load, we are not in the FTUE. so cancel the FTUE and call FTUESetStatus(true) if needed + if(context.jamClient.FTUEGetStatus() == false) { + context.jamClient.FTUESetStatus(true); + } + context.jamClient.FTUECancel(); + + var dialogBindings = { beforeShow: beforeShow, afterShow: afterShow, afterHide: afterHide }; + + app.bindDialog('gear-wizard', dialogBindings); + + $dialog = $('#gear-wizard-dialog'); + $wizardSteps = $dialog.find('.wizard-step'); + $templateSteps = $('#template-ftuesteps'); + $templateButtons = $('#template-ftue-buttons'); + $ftueButtons = $dialog.find('.ftue-buttons'); + + stepUnderstandGear.initialize($wizardSteps.filter($('[layout-wizard-step=0]'))); + stepSelectGear.initialize($wizardSteps.filter($('[layout-wizard-step=1]'))); + stepConfigureTracks.initialize($wizardSteps.filter($('[layout-wizard-step=2]'))); + stepConfigureVoiceChat.initialize($wizardSteps.filter($('[layout-wizard-step=3]'))); + stepDirectMonitoring.initialize($wizardSteps.filter($('[layout-wizard-step=4]'))); + stepNetworkTest.initialize($wizardSteps.filter($('[layout-wizard-step=5]'))); + stepSuccess.initialize($wizardSteps.filter($('[layout-wizard-step=6]'))); + + events(); + } + + this.setNextState = setNextState; + this.initialize = initialize; + + self = this; + return this; + }; + +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/gear/step_configure_tracks.js b/web/app/assets/javascripts/gear/step_configure_tracks.js new file mode 100644 index 000000000..ab0be8b4b --- /dev/null +++ b/web/app/assets/javascripts/gear/step_configure_tracks.js @@ -0,0 +1,18 @@ +(function (context, $) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.StepConfigureTracks = function (app) { + + var $step = null; + + function initialize(_$step) { + $step = _$step; + } + + this.initialize = initialize; + + return this; + } +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/gear/step_configure_voice_chat.js b/web/app/assets/javascripts/gear/step_configure_voice_chat.js new file mode 100644 index 000000000..46d9c9d6a --- /dev/null +++ b/web/app/assets/javascripts/gear/step_configure_voice_chat.js @@ -0,0 +1,18 @@ +(function (context, $) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.StepConfigureVoiceChat = function (app) { + + var $step = null; + + function initialize(_$step) { + $step = _$step; + } + + this.initialize = initialize; + + return this; + } +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/gear/step_direct_monitoring.js b/web/app/assets/javascripts/gear/step_direct_monitoring.js new file mode 100644 index 000000000..a8e6aa1ac --- /dev/null +++ b/web/app/assets/javascripts/gear/step_direct_monitoring.js @@ -0,0 +1,18 @@ +(function (context, $) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.StepDirectMonitoring = function (app) { + + var $step = null; + + function initialize(_$step) { + $step = _$step; + } + + this.initialize = initialize; + + return this; + } +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/gear/step_network_test.js b/web/app/assets/javascripts/gear/step_network_test.js new file mode 100644 index 000000000..d04e73343 --- /dev/null +++ b/web/app/assets/javascripts/gear/step_network_test.js @@ -0,0 +1,18 @@ +(function (context, $) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.StepNetworkTest = function (app) { + + var $step = null; + + function initialize(_$step) { + $step = _$step; + } + + this.initialize = initialize; + + return this; + } +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/gear/step_select_gear.js b/web/app/assets/javascripts/gear/step_select_gear.js new file mode 100644 index 000000000..ec4db1c6d --- /dev/null +++ b/web/app/assets/javascripts/gear/step_select_gear.js @@ -0,0 +1,952 @@ +(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 self = null; + var $step = null; + 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 $asioInputControlBtn = null; + var $asioOutputControlBtn = null; + var $resyncBtn = null; + var $templateAudioPort = null; + + var operatingSystem = null; + var iCheckIgnore = false; + + // cached values between + var deviceInformation = null; + var selectedDeviceInfo = null; + var musicPorts = null; + var validLatencyScore = false; + var validIOScore = false; + + var audioDeviceBehavior = { + MacOSX_builtin: { + display: 'MacOSX Built-In', + videoURL: undefined, + showKnobs: false, + showASIO: false + }, + MacOSX_interface: { + display: 'MacOSX external interface', + videoURL: undefined, + showKnobs: false, + showASIO: false + }, + Win32_wdm: { + display: 'Windows WDM', + videoURL: undefined, + showKnobs: true, + showASIO: false + }, + Win32_asio: { + display: 'Windows ASIO', + videoURL: undefined, + showKnobs: true, + showASIO: false + }, + Win32_asio4all: { + display: 'Windows ASIO4ALL', + videoURL: undefined, + showKnobs: false, + showASIO: true + }, + Linux: { + display: 'Linux', + videoURL: undefined, + showKnobs: true, + showASIO: false + } + } + + var ASIO_SETTINGS_DEFAULT_TEXT = 'ASIO SETTINGS...'; + var ASIO_SETTINGS_INPUT_TEXT = 'ASIO INPUT SETTINGS...'; + var ASIO_SETTINGS_OUTPUT_TEXT = 'ASIO OUTPUT SETTINGS...'; + + // should return one of: + // * MacOSX_builtin + // * MACOSX_interface + // * Win32_wdm + // * Win32_asio + // * Win32_asio4all + // * Linux + function determineDeviceType(deviceId, displayName) { + if (operatingSystem == "MacOSX") { + if (displayName.toLowerCase().trim() == "built-in") { + return "MacOSX_builtin"; + } + else { + return "MacOSX_interface"; + } + } + else if (operatingSystem == "Win32") { + if (context.jamClient.FTUEIsMusicDeviceWDM(deviceId)) { + return "Win32_wdm"; + } + else if (displayName.toLowerCase().indexOf("asio4all") > -1) { + return "Win32_asio4all" + } + else { + return "Win32_asio"; + } + } + else { + return "Linux"; + } + } + + function loadDevices() { + + var oldDevices = context.jamClient.FTUEGetDevices(false); + var devices = context.jamClient.FTUEGetAudioDevices(); + console.log("oldDevices: " + JSON.stringify(oldDevices)); + console.log("devices: " + JSON.stringify(devices)); + + var loadedDevices = {}; + + // augment these devices by determining their type + context._.each(devices.devices, function (device) { + + if (device.name == "JamKazam Virtual Monitor") { + return; + } + + var deviceInfo = {}; + + deviceInfo.id = device.guid; + deviceInfo.type = determineDeviceType(device.guid, device.display_name); + console.log("deviceInfo.type: " + deviceInfo.type) + deviceInfo.displayType = audioDeviceBehavior[deviceInfo.type].display; + deviceInfo.displayName = device.display_name; + + loadedDevices[device.guid] = deviceInfo; + + logger.debug("loaded device: ", deviceInfo); + }) + + deviceInformation = loadedDevices; + + logger.debug(context.JK.dlen(deviceInformation) + " devices loaded.", deviceInformation); + } + + // 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($frameSize.val()); + } + + function selectedBufferOut() { + return parseFloat($frameSize.val()); + } + + function initializeNextButtonState() { + $dialog.setNextState(validLatencyScore && validIOScore); + } + + function initializeAudioInput() { + var optionsHtml = ''; + optionsHtml = ''; + context._.each(deviceInformation, function (deviceInfo, deviceId) { + + console.log(arguments) + optionsHtml += ''; + }); + $audioInput.html(optionsHtml); + context.JK.dropdown($audioInput); + + initializeAudioInputChanged(); + } + + function initializeAudioOutput() { + var optionsHtml = ''; + optionsHtml = ''; + context._.each(deviceInformation, function (deviceInfo, deviceId) { + optionsHtml += ''; + }); + $audioOutput.html(optionsHtml); + context.JK.dropdown($audioOutput); + + 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(); + console.log("musicPorts: %o", JSON.stringify(musicPorts)); + + 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 $allInputs = $inputChannels.find('input[type="checkbox"]'); + + if ($allInputs.length == 0) { + // ERROR: not enough channels + 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) { + // 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) { + $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 { + $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)'); + + console.log("outputs:", $assignedOutputs, $unassignedOutputs); + if ($assignedOutputs.length == 0) { + console.log("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) { + $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 initializeFormElements() { + if (!deviceInformation) throw "devices are not initialized"; + + initializeAudioInput(); + initializeAudioOutput(); + initializeFramesize(); + initializeBuffers(); + } + + 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'); + } + + function renderLatencyScore(latencyValue, latencyClass) { + // latencyValue == null implies starting condition + if (latencyValue) { + $latencyScore.text(latencyValue + ' ms'); + } + else { + $latencyScore.text(''); + } + $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") { + $ioRate.show(); + $ioVar.show(); + } + if(ioClass == 'starting') { + $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 = 'N/A'; + 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 <= 20) { + latencyClass = "acceptable"; + validLatency = true; + } else { + latencyClass = "bad"; + } + } + else { + latencyClass = 'unknown'; + } + + validLatencyScore = validLatency; + + renderLatencyScore(latencyValue, latencyClass); + } + + function audioInputDeviceUnselected() { + validLatencyScore = false; + validIOScore = false; + initializeNextButtonState(); + resetFrameBuffers(); + clearInputPorts(); + } + + function renderScoringStarted() { + validLatencyScore = false; + validIOScore = false; + initializeNextButtonState(); + resetScoreReport(); + freezeAudioInteraction(); + renderLatencyScore(null, 'starting'); + renderIOScore(null, null, null, null, null, null); + } + + function renderScoringStopped() { + initializeNextButtonState(); + unfreezeAudioInteraction(); + } + + + function freezeAudioInteraction() { + $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() { + $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; + } + + // Given a latency structure, update the view. + function newFtueUpdateLatencyView(latency) { + var $report = $('.ftue-new .latency .report'); + var $instructions = $('.ftue-new .latency .instructions'); + var latencyClass = "neutral"; + var latencyValue = "N/A"; + var $saveButton = $('#btn-ftue-2-save'); + if (latency && latency.latencyknown) { + latencyValue = latency.latency; + // Round latency to two decimal places. + latencyValue = Math.round(latencyValue * 100) / 100; + if (latency.latency <= 10) { + latencyClass = "good"; + setSaveButtonState($saveButton, true); + } else if (latency.latency <= 20) { + latencyClass = "acceptable"; + setSaveButtonState($saveButton, true); + } else { + latencyClass = "bad"; + setSaveButtonState($saveButton, false); + } + } else { + latencyClass = "unknown"; + setSaveButtonState($saveButton, false); + } + + $('.ms-label', $report).html(latencyValue); + $('p', $report).html('milliseconds'); + + $report.removeClass('good acceptable bad unknown'); + $report.addClass(latencyClass); + + var instructionClasses = ['neutral', 'good', 'acceptable', 'unknown', 'bad', 'start', 'loading']; + $.each(instructionClasses, function (idx, val) { + $('p.' + val, $instructions).hide(); + }); + if (latency === 'loading') { + $('p.loading', $instructions).show(); + } else { + $('p.' + latencyClass, $instructions).show(); + renderStopNewFtueLatencyTesting(); + } + } + + 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 = audioDeviceBehavior[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 = audioDeviceBehavior[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 initializeASIOButtons() { + $asioInputControlBtn.unbind('click').click(function () { + context.jamClient.FTUEOpenControlPanel(); // TODO: supply with ID when VRFS-1707 is done + }); + $asioOutputControlBtn.unbind('click').click(function () { + context.jamClient.FTUEOpenControlPanel(); // TODO: supply with ID when VRFS-1707 is done + }); + } + + function initializeKnobs() { + $frameSize.unbind('change').change(function () { + jamClient.FTUESetFrameSize(selectedFramesize()); + }); + + $bufferIn.unbind('change').change(function () { + jamClient.FTUESetInputLatency(selectedBufferIn()); + }); + + $bufferOut.unbind('change').change(function () { + jamClient.FTUESetOutputLatency(selectedBufferOut()); + }); + } + + function initializeResync() { + $resyncBtn.unbind('click').click(function () { + attemptScore(); + return false; + }) + } + + function renderIOScoringStarted(secondsLeft) { + $ioCountdownSecs.text(secondsLeft); + $ioCountdown.show(); + } + + function renderIOScoringStopped() { + $ioCountdown.hide(); + } + + function renderIOCountdown(secondsLeft) { + $ioCountdownSecs.text(secondsLeft); + } + + // sets currentlySelectedDeviceInfo, 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; + } + + var input = findDevice(audioInputDeviceId); + var output = findDevice(audioOutputDeviceId); + + var inputBehavior = audioDeviceBehavior[input.type]; + var outputBehavior = audioDeviceBehavior[output.type]; + + selectedDeviceInfo = { + input: { + id: audioInputDeviceId, + info: input, + behavior: inputBehavior + }, + output: { + id: audioOutputDeviceId, + info: output, + behavior: outputBehavior + } + } + console.log("selectedDeviceInfo", selectedDeviceInfo); + } + + function changeDevice() { + + var audioInputDeviceId = selectedDeviceInfo.input.id; + var audioOutputDeviceId = selectedDeviceInfo.output.id; + + // 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(); + + var validDevice = autoSelectMinimumValidChannels(); + + if (!validDevice) { + return false; + } + + jamClient.FTUESetInputLatency(selectedBufferIn()); + jamClient.FTUESetOutputLatency(selectedBufferOut()); + jamClient.FTUESetFrameSize(selectedFramesize()); + + return true; + } + + function audioDeviceChanged() { + cacheCurrentAudioInfo(); + updateDialogForCurrentDevices(); + if (changeDevice()) { + attemptScore(); + } + } + + function updateDialogForCurrentDevices() { + var inputBehavior = selectedDeviceInfo.input.behavior; + var outputBehavior = selectedDeviceInfo.output.behavior; + + // handle framesize/buffers + if (inputBehavior && (inputBehavior.showKnobs || outputBehavior.showKnobs)) { + $knobs.css('visibility', 'visible') + } + else { + $knobs.css('visibility', 'hidden') + } + + // handle ASIO + if (inputBehavior) { + if (inputBehavior.showASIO && !outputBehavior.showASIO) { + // show single ASIO button + $asioInputControlBtn.text(ASIO_SETTINGS_DEFAULT_TEXT).show(); + $asioOutputControlBtn.hide(); + } + else if (!inputBehavior.showASIO && outputBehavior.showASIO) { + // show single ASIO button + $asioInputControlBtn.text(ASIO_SETTINGS_DEFAULT_TEXT).show(); + $asioOutputControlBtn.hide(); + } + else if (inputBehavior.showASIO && outputBehavior.showASIO) { + // show two ASIO buttons + $asioInputControlBtn.text(ASIO_SETTINGS_INPUT_TEXT).show(); + $asioOutputControlBtn.text(ASIO_SETTINGS_OUTPUT_TEXT).show(); + } + else { + // show no ASIO buttons + $asioInputControlBtn.hide(); + $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'); + } + } + + function attemptScore() { + renderScoringStarted(); + + // 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(); + console.log("FTUEGetExpectedLatency: %o", latency); + + updateScoreReport(latency); + + // if there was a valid latency score, go on to the next step + if (validLatencyScore) { + renderIOScore(null, null, null, 'starting', 'starting', 'starting'); + var testTimeSeconds = 10; // allow 10 seconds for IO to establish itself + context.jamClient.FTUEStartIoPerfTest(); + renderIOScoringStarted(testTimeSeconds); + renderIOCountdown(testTimeSeconds); + var interval = setInterval(function () { + testTimeSeconds -= 1; + renderIOCountdown(testTimeSeconds); + if (testTimeSeconds == 0) { + clearInterval(interval); + renderIOScoringStopped(); + var io = context.jamClient.FTUEGetIoPerfData(); + + // 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); + + // lie for now until IO questions finalize + validIOScore = true; + + renderScoringStopped(); + } + }, 1000); + } + else { + renderIOScore(null, null, null, 'skip', 'skip', 'skip'); + renderScoringStopped(); + } + }, 250); + } + + function initializeAudioInputChanged() { + $audioInput.unbind('change').change(audioDeviceChanged); + } + + function initializeAudioOutputChanged() { + $audioOutput.unbind('change').change(audioDeviceChanged); + } + + function beforeShow() { + loadDevices(); + initializeFormElements(); + initializeNextButtonState(); + initializeWatchVideo(); + initializeASIOButtons(); + initializeKnobs(); + initializeResync(); + } + + 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'); + $asioInputControlBtn = $step.find('.asio-settings-input-btn'); + $asioOutputControlBtn = $step.find('.asio-settings-output-btn'); + $resyncBtn = $step.find('.resync-btn'); + $templateAudioPort = $('#template-audio-port'); + + operatingSystem = context.jamClient.GetOSAsString(); + } + + this.beforeShow = beforeShow; + this.initialize = initialize; + + self = this; + return this; + }; + +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/gear/step_success.js b/web/app/assets/javascripts/gear/step_success.js new file mode 100644 index 000000000..3dc049d2c --- /dev/null +++ b/web/app/assets/javascripts/gear/step_success.js @@ -0,0 +1,18 @@ +(function (context, $) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.StepSuccess = function (app) { + + var $step = null; + + function initialize(_$step) { + $step = _$step; + } + + this.initialize = initialize; + + return this; + } +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/gear/step_understand_gear.js b/web/app/assets/javascripts/gear/step_understand_gear.js new file mode 100644 index 000000000..d579e84f2 --- /dev/null +++ b/web/app/assets/javascripts/gear/step_understand_gear.js @@ -0,0 +1,27 @@ +(function (context, $) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.StepUnderstandGear = function (app) { + + var $step = null; + + function beforeShow() { + var $watchVideo = $step.find('.watch-video'); + var videoUrl = 'https://www.youtube.com/watch?v=VexH4834o9I'; + if (operatingSystem == "Win32") { + $watchVideo.attr('href', 'https://www.youtube.com/watch?v=VexH4834o9I'); + } + $watchVideo.attr('href', videoUrl); + } + + function initialize(_$step) { + $step = _$step; + } + + this.initialize = initialize; + + return this; + } +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/gear_wizard.js b/web/app/assets/javascripts/gear_wizard.js deleted file mode 100644 index 9012c91be..000000000 --- a/web/app/assets/javascripts/gear_wizard.js +++ /dev/null @@ -1,909 +0,0 @@ -(function (context, $) { - - "use strict"; - - - context.JK = context.JK || {}; - context.JK.GearWizard = function (app) { - - var ASSIGNMENT = context.JK.ASSIGNMENT; - var VOICE_CHAT = context.JK.VOICE_CHAT; - - var $dialog = null; - var $wizardSteps = null; - var $currentWizardStep = null; - var step = 0; - var $templateSteps = null; - var $templateButtons = null; - var $templateAudioPort = null; - var $ftueButtons = null; - var self = null; - var operatingSystem = null; - - // populated by loadDevices - var deviceInformation = null; - var musicPorts = null; - - - var validLatencyScore = false; - var validIOScore = false; - - // SELECT TRACKS STATE - - var TOTAL_STEPS = 7; - var STEP_INTRO = 0; - var STEP_SELECT_DEVICE = 1; - var STEP_SELECT_TRACKS = 2; - var STEP_SELECT_CHAT = 3; - var STEP_DIRECT_MONITOR = 4; - var STEP_ROUTER_NETWORK = 5; - var STEP_SUCCESS = 6; - - var PROFILE_DEV_SEP_TOKEN = '^'; - - var iCheckIgnore = false; - - var audioDeviceBehavior = { - MacOSX_builtin: { - display: 'MacOSX Built-In', - videoURL: undefined - }, - MacOSX_interface: { - display: 'MacOSX external interface', - videoURL: undefined - }, - Win32_wdm: { - display: 'Windows WDM', - videoURL: undefined - }, - Win32_asio: { - display: 'Windows ASIO', - videoURL: undefined - }, - Win32_asio4all: { - display: 'Windows ASIO4ALL', - videoURL: undefined - }, - Linux: { - display: 'Linux', - videoURL: undefined - } - } - - function beforeShowIntro() { - var $watchVideo = $currentWizardStep.find('.watch-video'); - var videoUrl = 'https://www.youtube.com/watch?v=VexH4834o9I'; - if (operatingSystem == "Win32") { - $watchVideo.attr('href', 'https://www.youtube.com/watch?v=VexH4834o9I'); - } - $watchVideo.attr('href', videoUrl); - } - - function beforeSelectDevice() { - - var $watchVideoInput = $currentWizardStep.find('.watch-video.audio-input'); - var $watchVideoOutput = $currentWizardStep.find('.watch-video.audio-output'); - var $audioInput = $currentWizardStep.find('.select-audio-input-device'); - var $audioOutput = $currentWizardStep.find('.select-audio-output-device'); - var $bufferIn = $currentWizardStep.find('.select-buffer-in'); - var $bufferOut = $currentWizardStep.find('.select-buffer-out'); - var $frameSize = $currentWizardStep.find('.select-frame-size'); - var $inputChannels = $currentWizardStep.find('.input-ports'); - var $outputChannels = $currentWizardStep.find('.output-ports'); - var $scoreReport = $currentWizardStep.find('.results'); - var $latencyScoreSection = $scoreReport.find('.latency-score-section'); - var $latencyScore = $scoreReport.find('.latency-score'); - var $ioScoreSection = $scoreReport.find('.io-score-section'); - var $ioRateScore = $scoreReport.find('.io-rate-score'); - var $ioVarScore = $scoreReport.find('.io-var-score'); - var $ioCountdown = $scoreReport.find('.io-countdown'); - var $ioCountdownSecs = $scoreReport.find('.io-countdown .secs'); - var $nextButton = $ftueButtons.find('.btn-next'); - var $asioControlPanelBtn = $currentWizardStep.find('.asio-settings-btn'); - var $resyncBtn = $currentWizardStep.find('resync-btn') - - // should return one of: - // * MacOSX_builtin - // * MACOSX_interface - // * Win32_wdm - // * Win32_asio - // * Win32_asio4all - // * Linux - function determineDeviceType(deviceId, displayName) { - if (operatingSystem == "MacOSX") { - if (displayName.toLowerCase().trim() == "built-in") { - return "MacOSX_builtin"; - } - else { - return "MacOSX_interface"; - } - } - else if (operatingSystem == "Win32") { - if (context.jamClient.FTUEIsMusicDeviceWDM(deviceId)) { - return "Win32_wdm"; - } - else if (displayName.toLowerCase().indexOf("asio4all") > -1) { - return "Win32_asio4all" - } - else { - return "Win32_asio"; - } - } - else { - return "Linux"; - } - } - - function loadDevices() { - - var oldDevices = context.jamClient.FTUEGetDevices(false); - var devices = context.jamClient.FTUEGetAudioDevices(); - console.log("oldDevices: " + JSON.stringify(oldDevices)); - console.log("devices: " + JSON.stringify(devices)); - - var loadedDevices = {}; - - // augment these devices by determining their type - context._.each(devices.devices, function (device) { - - if(device.name == "JamKazam Virtual Monitor") { - return; - } - - var deviceInfo = {}; - - deviceInfo.id = device.guid; - deviceInfo.type = determineDeviceType(device.guid, device.display_name); - console.log("deviceInfo.type: " + deviceInfo.type) - deviceInfo.displayType = audioDeviceBehavior[deviceInfo.type].display; - deviceInfo.displayName = device.display_name; - - loadedDevices[device.guid] = deviceInfo; - - logger.debug("loaded device: ", deviceInfo); - }) - - deviceInformation = loadedDevices; - - logger.debug(context.JK.dlen(deviceInformation) + " devices loaded.", deviceInformation); - } - - // 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($frameSize.val()); - } - - function selectedBufferOut() { - return parseFloat($frameSize.val()); - } - - function initializeNextButtonState() { - $nextButton.removeClass('button-orange button-grey'); - - if (validLatencyScore) $nextButton.addClass('button-orange'); - else $nextButton.addClass('button-grey'); - } - - function initializeAudioInput() { - var optionsHtml = ''; - optionsHtml = ''; - context._.each(deviceInformation, function (deviceInfo, deviceId) { - - console.log(arguments) - optionsHtml += ''; - }); - $audioInput.html(optionsHtml); - context.JK.dropdown($audioInput); - - initializeAudioInputChanged(); - } - - function initializeAudioOutput() { - var optionsHtml = ''; - optionsHtml = ''; - context._.each(deviceInformation, function (deviceInfo, deviceId) { - optionsHtml += ''; - }); - $audioOutput.html(optionsHtml); - context.JK.dropdown($audioOutput); - - 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(); - console.log("musicPorts: %o", JSON.stringify(musicPorts)); - - initializeInputPorts(musicPorts); - initializeOutputPorts(musicPorts); - } - - // 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 initializeFormElements() { - if (!deviceInformation) throw "devices are not initialized"; - - initializeAudioInput(); - initializeAudioOutput(); - initializeFramesize(); - initializeBuffers(); - } - - function resetFrameBuffers() { - $frameSize.val('2.5'); - $bufferIn.val('0'); - $bufferOut.val('0'); - } - - function clearInputPorts() { - $inputChannels.empty(); - } - - function clearOutputPorts() { - $outputChannels.empty(); - } - - function resetScoreReport() { - $ioRateScore.empty(); - $ioVarScore.empty(); - $latencyScore.empty(); - } - - function renderLatencyScore(latencyValue, latencyClass) { - if(latencyValue) { - $latencyScore.text(latencyValue + ' ms'); - } - else { - $latencyScore.text(''); - } - $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) { - $ioRateScore.text(median ? median : ''); - $ioVarScore.text(std ? std : ''); - $ioScoreSection.removeClass('good acceptable bad unknown starting skip').addClass(ioClass); - // TODO: show help bubble of all data in IO data - } - - function updateScoreReport(latencyResult) { - var latencyClass = "neutral"; - var latencyValue = 'N/A'; - 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 <= 20) { - latencyClass = "acceptable"; - validLatency = true; - } else { - latencyClass = "bad"; - } - } - else { - latencyClass = 'unknown'; - } - - validLatencyScore = validLatency; - - renderLatencyScore(latencyValue, latencyClass); - } - - function audioInputDeviceUnselected() { - validLatencyScore = false; - initializeNextButtonState(); - resetFrameBuffers(); - clearInputPorts(); - } - - function renderScoringStarted() { - validLatencyScore = false; - initializeNextButtonState(); - resetScoreReport(); - freezeAudioInteraction(); - renderLatencyScore(null, 'starting'); - } - - function renderScoringStopped() { - initializeNextButtonState(); - unfreezeAudioInteraction(); - } - - - function freezeAudioInteraction() { - $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'); - $asioControlPanelBtn.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() { - $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'); - $asioControlPanelBtn.off("click", false); - $resyncBtn.off('click', false); - $inputChannels.find('input[type="checkbox"]').iCheck('enable'); - $outputChannels.find('input[type="checkbox"]').iCheck('enable'); - iCheckIgnore = false; - } - - // Given a latency structure, update the view. - function newFtueUpdateLatencyView(latency) { - var $report = $('.ftue-new .latency .report'); - var $instructions = $('.ftue-new .latency .instructions'); - var latencyClass = "neutral"; - var latencyValue = "N/A"; - var $saveButton = $('#btn-ftue-2-save'); - if (latency && latency.latencyknown) { - latencyValue = latency.latency; - // Round latency to two decimal places. - latencyValue = Math.round(latencyValue * 100) / 100; - if (latency.latency <= 10) { - latencyClass = "good"; - setSaveButtonState($saveButton, true); - } else if (latency.latency <= 20) { - latencyClass = "acceptable"; - setSaveButtonState($saveButton, true); - } else { - latencyClass = "bad"; - setSaveButtonState($saveButton, false); - } - } else { - latencyClass = "unknown"; - setSaveButtonState($saveButton, false); - } - - $('.ms-label', $report).html(latencyValue); - $('p', $report).html('milliseconds'); - - $report.removeClass('good acceptable bad unknown'); - $report.addClass(latencyClass); - - var instructionClasses = ['neutral', 'good', 'acceptable', 'unknown', 'bad', 'start', 'loading']; - $.each(instructionClasses, function (idx, val) { - $('p.' + val, $instructions).hide(); - }); - if (latency === 'loading') { - $('p.loading', $instructions).show(); - } else { - $('p.' + latencyClass, $instructions).show(); - renderStopNewFtueLatencyTesting(); - } - } - - 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 = audioDeviceBehavior[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 = audioDeviceBehavior[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 renderIOScoringStarted(secondsLeft) { - $ioCountdownSecs.text(secondsLeft); - $ioCountdown.show(); - } - - function renderIOScoringStopped() { - $ioCountdown.hide(); - } - - function renderIOCountdown(secondsLeft) { - $ioCountdownSecs.text(secondsLeft); - } - - function attemptScore() { - 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; - } - - jamClient.FTUESetInputMusicDevice(audioInputDeviceId); - jamClient.FTUESetOutputMusicDevice(audioOutputDeviceId); - - initializeChannels(); - - jamClient.FTUESetInputLatency(selectedBufferIn()); - jamClient.FTUESetOutputLatency(selectedBufferOut()); - jamClient.FTUESetFrameSize(selectedFramesize()); - - renderScoringStarted(); - logger.debug("Calling FTUESave(false)"); - jamClient.FTUESave(false); - - var latency = jamClient.FTUEGetExpectedLatency(); - console.log("FTUEGetExpectedLatency: %o", latency); - - updateScoreReport(latency); - - // if there was a valid latency score, go on to the next step - if(validLatencyScore) { - renderIOScore(null, null, null, 'starting'); - var testTimeSeconds = 10; // allow 10 seconds for IO to establish itself - context.jamClient.FTUEStartIoPerfTest(); - renderIOScoringStarted(testTimeSeconds); - renderIOCountdown(testTimeSeconds); - var interval = setInterval(function() { - testTimeSeconds -= 1; - renderIOCountdown(testTimeSeconds); - if(testTimeSeconds == 0) { - clearInterval(interval); - renderIOScoringStopped(); - var io = context.jamClient.FTUEGetIoPerfData(); - - console.log("io: ", 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'; - } - - // now base the overall IO score based on both values. - renderIOScore(std, median, io, ioClass); - - // lie for now until IO questions finalize - validIOScore = true; - - renderScoringStopped(); - } - }, 1000); - } - else { - renderIOScore(null, null, null, 'skip'); - renderScoringStopped(); - } - - } - - function initializeAudioInputChanged() { - $audioInput.unbind('change').change(attemptScore); - } - - function initializeAudioOutputChanged() { - - } - - function initializeStep() { - loadDevices(); - initializeFormElements(); - initializeNextButtonState(); - initializeWatchVideo(); - } - - initializeStep(); - } - - function beforeSelectTracks() { - - } - - function beforeSelectChat() { - - } - - function beforeDirectMonitor() { - - } - - function beforeTestNetwork() { - - } - - function beforeSuccess() { - - } - - var STEPS = { - 0: { - beforeShow: beforeShowIntro - }, - 1: { - beforeShow: beforeSelectDevice - }, - 2: { - beforeShow: beforeSelectTracks - }, - 3: { - beforeShow: beforeSelectChat - }, - 4: { - beforeShow: beforeDirectMonitor - }, - 5: { - beforeShow: beforeTestNetwork - }, - 6: { - beforeShow: beforeSuccess - } - } - - function beforeShowStep($step) { - var stepInfo = STEPS[step]; - - if (!stepInfo) { - throw "unknown step: " + step; - } - - stepInfo.beforeShow.call(self); - } - - function moveToStep() { - var $nextWizardStep = $wizardSteps.filter($('[layout-wizard-step=' + step + ']')); - - $wizardSteps.hide(); - - $currentWizardStep = $nextWizardStep; - - var $ftueSteps = $(context._.template($templateSteps.html(), {}, { variable: 'data' })); - var $activeStep = $ftueSteps.find('.ftue-stepnumber[data-step-number="' + step + '"]'); - $activeStep.addClass('.active'); - $activeStep.next().show(); // show the .ftue-step-title - $currentWizardStep.find('.ftuesteps').replaceWith($ftueSteps); - beforeShowStep($currentWizardStep); - $currentWizardStep.show(); - - // update buttons - var $ftueButtonsContent = $(context._.template($templateButtons.html(), {}, {variable: 'data'})); - - - var $btnBack = $ftueButtonsContent.find('.btn-back'); - var $btnNext = $ftueButtonsContent.find('.btn-next'); - var $btnClose = $ftueButtonsContent.find('.btn-close'); - var $btnCancel = $ftueButtonsContent.find('.btn-cancel'); - - // hide back button if 1st step or last step - if (step == 0 && step == TOTAL_STEPS - 1) { - $btnBack.hide(); - } - // hide next button if not on last step - if (step == TOTAL_STEPS - 1) { - $btnNext.hide(); - } - // hide close if on last step - if (step != TOTAL_STEPS - 1) { - $btnClose.hide(); - } - // hide cancel if not on last step - if (step == TOTAL_STEPS - 1) { - $btnCancel.hide(); - } - - $btnNext.on('click', next); - $btnBack.on('click', back); - $btnClose.on('click', closeDialog); - $btnCancel.on('click', closeDialog); - - $ftueButtons.empty(); - $ftueButtons.append($ftueButtonsContent); - } - - function reset() { - $currentWizardStep = null; - } - - // checks if we already have a profile called 'FTUE...'; if not, create one. if so, re-use it. - function findOrCreateFTUEProfile() { - var profileName = context.jamClient.FTUEGetMusicProfileName(); - - logger.debug("current profile name: " + profileName); - - if(profileName && profileName.indexOf('FTUE') == 0) { - - } - else { - var newProfileName = 'FTUEAttempt-' + new Date().getTime().toString(); - logger.debug("setting FTUE-prefixed profile name to: " + newProfileName); - context.jamClient.FTUESetMusicProfileName(newProfileName); - } - - var profileName = context.jamClient.FTUEGetMusicProfileName(); - - logger.debug("name on exit: " + profileName); - - } - - function beforeShow(args) { - context.jamClient.FTUECancel(); - findOrCreateFTUEProfile(); - - step = args.d1; - if (!step) step = 0; - step = parseInt(step); - moveToStep(); - } - - function afterShow() { - - } - - function afterHide() { - context.jamClient.FTUECancel(); - } - - function back() { - if ($(this).is('.button-grey')) return; - step = step - 1; - moveToStep(); - return false; - } - - function next() { - if ($(this).is('.button-grey')) return; - - step = step + 1; - - moveToStep(); - return false; - } - - function closeDialog() { - app.layout.closeDialog('gear-wizard'); - return false; - } - - function events() { - } - - function route() { - - } - - function initialize() { - - var dialogBindings = { beforeShow: beforeShow, afterShow: afterShow, afterHide: afterHide }; - - app.bindDialog('gear-wizard', dialogBindings); - - $dialog = $('#gear-wizard-dialog'); - $wizardSteps = $dialog.find('.wizard-step'); - $templateSteps = $('#template-ftuesteps'); - $templateButtons = $('#template-ftue-buttons'); - $templateAudioPort = $('#template-audio-port'); - $ftueButtons = $dialog.find('.ftue-buttons'); - - operatingSystem = context.jamClient.GetOSAsString(); - - events(); - } - - this.initialize = initialize; - - self = this; - return this; - }; - -})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/gearWizard.css.scss b/web/app/assets/stylesheets/client/gearWizard.css.scss index aa2ac4bf6..bf8e1384c 100644 --- a/web/app/assets/stylesheets/client/gearWizard.css.scss +++ b/web/app/assets/stylesheets/client/gearWizard.css.scss @@ -115,6 +115,7 @@ &.list.ports { height:100px; + overflow:auto; } &.instructions { @@ -151,18 +152,32 @@ margin-bottom:20px; } - .asio-settings-btn, .resync-btn { + .asio-settings-input-btn, .asio-settings-output-btn, .resync-btn { width:80%; display:inline-block; text-align:center; } - .asio-settings-btn { + .asio-settings-input-btn, .asio-settings-output-btn { margin-top:10px; } + + .asio-settings-input-btn { + display:none; + } + + .asio-settings-output-btn { + display:none; + } + .resync-btn { margin-top:10px; + visibility:hidden; + } + + .frame-and-buffers { + display:none; } .framesize { @@ -197,12 +212,24 @@ } } + .audio-port { + white-space: nowrap; + } + + .audio-channels { + margin-top:15px; + } + .ftue-box.results { height: 230px !important; padding:0; + .io, .latency { + display:none; + } + .scoring-section { font-size:15px; @include border_box_sizing; @@ -220,6 +247,11 @@ &.unknown { background-color:#999; } + &.skip { + .io-skip-msg { + display:inline; + } + } } .io-countdown { @@ -236,9 +268,48 @@ .io-skip-msg { display:none; + } - .scoring-section.skip & { - display:inline; + .io-rate { + display:none; + } + .io-var { + display:none; + } + + ul.results-text { + padding:10px 8px; + + li { + display:none + } + + &[latency-score="good"] li.latency-good { + display:list-item; + } + &[latency-score="acceptable"] li.latency-acceptable { + display:list-item; + } + &[latency-score="bad"] li.latency-bad { + display:list-item; + } + &[io-var-score="good"] li.io-var-good { + display:list-item; + } + &[io-var-score="acceptable"] li.io-var-acceptable { + display:list-item; + } + &[io-var-score="bad"] li.io-var-bad { + display:list-item; + } + &[io-rate-score="good"] li.io-rate-good { + display:list-item; + } + &[io-rate-score="acceptable"] li.io-rate-acceptable { + display:list-item; + } + &[io-rate-score="bad"] li.io-rate-bad { + display:list-item; } } } diff --git a/web/app/views/clients/gear/_gear_wizard.html.haml b/web/app/views/clients/gear/_gear_wizard.html.haml index 57f0ef1f8..01b4637de 100644 --- a/web/app/views/clients/gear/_gear_wizard.html.haml +++ b/web/app/views/clients/gear/_gear_wizard.html.haml @@ -37,15 +37,16 @@ %h2 Audio Input Device %select.w100.select-audio-input-device %option None - %h2 Audio Input Ports + %h2.audio-channels Audio Input Ports .ftue-box.list.ports.input-ports - %a.button-orange.asio-settings-btn ASIO SETTINGS... + %a.button-orange.asio-settings-input-btn ASIO SETTINGS... + %a.button-orange.asio-settings-output-btn ASIO SETTINGS... %a.button-orange.resync-btn RESYNC .wizard-step-column %h2 Audio Output Device %select.w100.select-audio-output-device %option Same as input - %h2 Audio Output Ports + %h2.audio-channels Audio Output Ports .ftue-box.list.ports.output-ports .frame-and-buffers .framesize @@ -91,13 +92,27 @@ .p5 .io I/O %span.io-skip-msg - Skipped + Not Tested %span.io-countdown %span.secs seconds left - %span.io-rate-score - %span.io-var-score - + %span.io-rate< + Rate= + %span.io-rate-score> + %span.io-var< + Var= + %span.io-var-score> + .clearall + %ul.results-text + %li.latency-good Your latency is good. + %li.latency-acceptable Your latency is acceptable. + %li.latency-bad Your latency is poor. + %li.io-rate-good Your I/O rate is good. + %li.io-rate-acceptable Your I/O rate is acceptable. + %li.io-rate-bad Your I/O rate is poor. + %li.io-var-good Your I/O variance is good. + %li.io-var-acceptable Your I/O variance is acceptable. + %li.io-var-bad Your I/O variance is poor. .clearall .wizard-step{ 'layout-wizard-step' => "2", 'dialog-title' => "Configure Tracks", 'dialog-purpose' => "ConfigureTracks" }