diff --git a/web/app/assets/javascripts/JamServer.js b/web/app/assets/javascripts/JamServer.js index d1126adf1..aaf2cfdd6 100644 --- a/web/app/assets/javascripts/JamServer.js +++ b/web/app/assets/javascripts/JamServer.js @@ -17,7 +17,8 @@ // heartbeat var heartbeatInterval = null; var heartbeatMS = null; - var heartbeatMissedMS = 10000; // if 5 seconds go by and we haven't seen a heartbeat ack, get upset + var heartbeatMissedMS = 10000; // if 10 seconds go by and we haven't seen a heartbeat ack, get upset + var lastHeartbeatSentTime = null; var lastHeartbeatAckTime = null; var lastHeartbeatFound = false; var heartbeatAckCheckInterval = null; @@ -140,6 +141,16 @@ var message = context.JK.MessageFactory.heartbeat(notificationLastSeen, notificationLastSeenAt); notificationLastSeenAt = undefined; notificationLastSeen = undefined; + // for debugging purposes, see if the last time we've sent a heartbeat is way off (500ms) of the target interval + var now = new Date(); + + if(lastHeartbeatSentTime) { + var drift = new Date().getTime() - lastHeartbeatSentTime.getTime() - heartbeatMS; + if(drift > 500) { + logger.error("significant drift between heartbeats: " + drift + 'ms beyond target interval') + } + } + lastHeartbeatSentTime = now; context.JK.JamServer.send(message); lastHeartbeatFound = false; } @@ -417,6 +428,7 @@ connectTimeout = setTimeout(function() { connectTimeout = null; if(connectDeferred.state() === 'pending') { + server.close(true); connectDeferred.reject(); } }, 4000); diff --git a/web/app/assets/javascripts/configureTrack.js b/web/app/assets/javascripts/configureTrack.js index d3bfb0eb2..344c99302 100644 --- a/web/app/assets/javascripts/configureTrack.js +++ b/web/app/assets/javascripts/configureTrack.js @@ -7,18 +7,8 @@ var logger = context.JK.logger; var myTrackCount; - var ASSIGNMENT = { - CHAT: -2, - OUTPUT: -1, - UNASSIGNED: 0, - TRACK1: 1, - TRACK2: 2 - }; - - var VOICE_CHAT = { - NO_CHAT: "0", - CHAT: "1" - }; + var ASSIGNMENT = context.JK.ASSIGNMENT; + var VOICE_CHAT = context.JK.VOICE_CHAT; var instrument_array = []; diff --git a/web/app/assets/javascripts/gear_wizard.js b/web/app/assets/javascripts/gear_wizard.js index 4926c6777..9012c91be 100644 --- a/web/app/assets/javascripts/gear_wizard.js +++ b/web/app/assets/javascripts/gear_wizard.js @@ -6,18 +6,27 @@ 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; - // SELECT DEVICE STATE - var validScore = false; + // populated by loadDevices + var deviceInformation = null; + var musicPorts = null; + + + var validLatencyScore = false; + var validIOScore = false; // SELECT TRACKS STATE @@ -30,20 +39,33 @@ var STEP_ROUTER_NETWORK = 5; var STEP_SUCCESS = 6; + var PROFILE_DEV_SEP_TOKEN = '^'; + + var iCheckIgnore = false; + var audioDeviceBehavior = { - MacOSX_builtin : { + MacOSX_builtin: { + display: 'MacOSX Built-In', videoURL: undefined }, - MACOSX_interface : { + MacOSX_interface: { + display: 'MacOSX external interface', videoURL: undefined }, - Win32_wdm : { + Win32_wdm: { + display: 'Windows WDM', videoURL: undefined }, - Win32_asio : { + Win32_asio: { + display: 'Windows ASIO', videoURL: undefined }, - Win32_asio4all : { + Win32_asio4all: { + display: 'Windows ASIO4ALL', + videoURL: undefined + }, + Linux: { + display: 'Linux', videoURL: undefined } } @@ -51,7 +73,7 @@ function beforeShowIntro() { var $watchVideo = $currentWizardStep.find('.watch-video'); var videoUrl = 'https://www.youtube.com/watch?v=VexH4834o9I'; - if(operatingSystem == "Win32") { + if (operatingSystem == "Win32") { $watchVideo.attr('href', 'https://www.youtube.com/watch?v=VexH4834o9I'); } $watchVideo.attr('href', videoUrl); @@ -65,12 +87,90 @@ var $audioOutput = $currentWizardStep.find('.select-audio-output-device'); var $bufferIn = $currentWizardStep.find('.select-buffer-in'); var $bufferOut = $currentWizardStep.find('.select-buffer-out'); - var $nextButton = $ftueButtons.find('.btn-next'); 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') - // returns a deviceInfo hash for the device matching the deviceId, or null. + // 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 {}; + return deviceInformation[deviceId]; } function selectedAudioInput() { @@ -81,31 +181,369 @@ 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(validScore) $nextButton.addClass('button-orange'); + if (validLatencyScore) $nextButton.addClass('button-orange'); else $nextButton.addClass('button-grey'); } - function audioDeviceUnselected() { - validScore = false; + 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() { + $watchVideoInput.unbind('click').click(function () { var audioDevice = findDevice(selectedAudioInput()); - if(!audioDevice) { + 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]; + var videoURL = audioDeviceBehavior[audioDevice.type].videoURL; - if(videoURL) { + if (videoURL) { $(this).attr('href', videoURL); return true; } @@ -117,16 +555,16 @@ return false; }); - $watchVideoOutput.unbind('click').click(function() { + $watchVideoOutput.unbind('click').click(function () { var audioDevice = findDevice(selectedAudioOutput()); - if(!audioDevice) { + if (!audioDevice) { throw "this button should be hidden"; } else { - var videoURL = audioDeviceBehavior[audioDevice.type]; + var videoURL = audioDeviceBehavior[audioDevice.type].videoURL; - if(videoURL) { + if (videoURL) { $(this).attr('href', videoURL); return true; } @@ -139,106 +577,131 @@ }); } - function initializeAudioInputChanged() { - $audioInput.unbind('change').change(function(evt) { - - var audioDeviceId = selectedAudioInput(); - - if(!audioDeviceId) { - audioDeviceUnselected(); - return false; - } - - var audioDevice = findDevice(selectedAudioInput()); - - if(!audioDevice) { - context.JK.alertSupportedNeeded('Unable to find device information for: ' + audioDevice); - } - - releaseDropdown(function () { - renderStartNewFtueLatencyTesting(); - var $select = $(evt.currentTarget); - - var $audioSelect = $('.ftue-new .settings-2-device select'); - var audioDriverId = $audioSelect.val(); - - if (!audioDriverId) { - context.JK.alertSupportNeeded(); - - renderStopNewFtueLatencyTesting(); - // reset back to 'Choose...' - newFtueEnableControls(false); - return; - } - jamClient.FTUESetMusicDevice(audioDriverId); - - var musicInputs = jamClient.FTUEGetMusicInputs(); - var musicOutputs = jamClient.FTUEGetMusicOutputs(); - - // set the music input to the first available input, - // and output to the first available output - var kin = null, kout = null, k = null; - // TODO FIXME - this jamClient call returns a dictionary. - // It's difficult to know what to auto-choose. - // For example, with my built-in audio, the keys I get back are - // digital in, line in, mic in and stereo mix. Which should we pick for them? - for (k in musicInputs) { - kin = k; - break; - } - for (k in musicOutputs) { - kout = k; - break; - } - var result; - if (kin && kout) { - jamClient.FTUESetMusicInput(kin); - jamClient.FTUESetMusicOutput(kout); - } else { - // TODO FIXME - how to handle a driver selection where we are unable to - // autoset both inputs and outputs? (I'd think this could happen if either - // the input or output side returned no values) - renderStopNewFtueLatencyTesting(); - console.log("invalid kin/kout %o/%o", kin, kout); - return; - } - - newFtueUpdateLatencyView('loading'); - - newFtueEnableControls(true); - newFtueOsSpecificSettings(); - // make sure whatever the user sees in the frontend is what the backend thinks - // this is necesasry because if you do a FTUE pass, close the client, and pick the same device - // the backend will *silently* use values from before, because the frontend does not query the backend - // for these values anywhere. - - // setting all 3 of these cause 3 FTUE's. Instead, we set batchModify to true so that they don't call FtueSave(false), and call later ourselves - batchModify = true; - newFtueAsioFrameSizeToBackend($('#ftue-2-asio-framesize')); - newFtueAsioInputLatencyToBackend($('#ftue-2-asio-input-latency')); - newFtueAsioOutputLatencyToBackend($('#ftue-2-asio-output-latency')); - batchModify = false; - - //setLevels(0); - renderVolumes(); - - logger.debug("Calling FTUESave(" + false + ")"); - jamClient.FTUESave(false) - pendingFtueSave = false; // this is not really used in any real fashion. just setting back to false due to batch modify above - - setVuCallbacks(); - - var latency = jamClient.FTUEGetExpectedLatency(); - console.log("FTUEGetExpectedLatency: %o", latency); - newFtueUpdateLatencyView(latency); - }); - - }) + 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(); - initializeAudioInputChanged(); } initializeStep(); @@ -291,7 +754,9 @@ function beforeShowStep($step) { var stepInfo = STEPS[step]; - if(!stepInfo) {throw "unknown step: " + step;} + if (!stepInfo) { + throw "unknown step: " + step; + } stepInfo.beforeShow.call(self); } @@ -304,7 +769,7 @@ $currentWizardStep = $nextWizardStep; var $ftueSteps = $(context._.template($templateSteps.html(), {}, { variable: 'data' })); - var $activeStep = $ftueSteps.find('.ftue-stepnumber[data-step-number="'+ step +'"]'); + 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); @@ -321,11 +786,11 @@ var $btnCancel = $ftueButtonsContent.find('.btn-cancel'); // hide back button if 1st step or last step - if(step == 0 && step == TOTAL_STEPS - 1) { + if (step == 0 && step == TOTAL_STEPS - 1) { $btnBack.hide(); } // hide next button if not on last step - if (step == TOTAL_STEPS - 1 ) { + if (step == TOTAL_STEPS - 1) { $btnNext.hide(); } // hide close if on last step @@ -350,9 +815,33 @@ $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; + if (!step) step = 0; step = parseInt(step); moveToStep(); } @@ -362,18 +851,18 @@ } function afterHide() { - + context.jamClient.FTUECancel(); } function back() { - if($(this).is('.button-grey')) return; + if ($(this).is('.button-grey')) return; step = step - 1; moveToStep(); return false; } function next() { - if($(this).is('.button-grey')) return; + if ($(this).is('.button-grey')) return; step = step + 1; @@ -392,6 +881,7 @@ function route() { } + function initialize() { var dialogBindings = { beforeShow: beforeShow, afterShow: afterShow, afterHide: afterHide }; @@ -402,6 +892,7 @@ $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(); diff --git a/web/app/assets/javascripts/globals.js b/web/app/assets/javascripts/globals.js index 49341c071..7c3a37ec3 100644 --- a/web/app/assets/javascripts/globals.js +++ b/web/app/assets/javascripts/globals.js @@ -14,7 +14,20 @@ UNIX: "Unix" }; - // TODO: store these client_id values in instruments table, or store + context.JK.ASSIGNMENT = { + CHAT: -2, + OUTPUT: -1, + UNASSIGNED: 0, + TRACK1: 1, + TRACK2: 2 + }; + + context.JK.VOICE_CHAT = { + NO_CHAT: "0", + CHAT: "1" + }; + + // TODO: store these client_id values in instruments table, or store // server_id as the client_id to prevent maintenance nightmares. As it's // set up now, we will have to deploy each time we add new instruments. context.JK.server_to_client_instrument_map = { diff --git a/web/app/assets/stylesheets/client/gearWizard.css.scss b/web/app/assets/stylesheets/client/gearWizard.css.scss index 49dc7f34b..aa2ac4bf6 100644 --- a/web/app/assets/stylesheets/client/gearWizard.css.scss +++ b/web/app/assets/stylesheets/client/gearWizard.css.scss @@ -184,8 +184,63 @@ width:45%; } + + .buffers { + .easydropdown-wrapper:nth-of-type(1) { + left:5px; + } + .easydropdown-wrapper:nth-of-type(2) { + left:35px; + } + .easydropdown, .easydropdown-wrapper { + width:15px; + } + } + + + .ftue-box.results { height: 230px !important; + padding:0; + + .scoring-section { + font-size:15px; + @include border_box_sizing; + height:64px; + + &.good { + background-color:#72a43b; + } + &.acceptable { + background-color:#cc9900; + } + &.bad, &.skip { + background-color:#660000; + } + &.unknown { + background-color:#999; + } + } + + .io-countdown { + display:none; + padding-left:19px; + position:relative; + + .secs { + position:absolute; + width:19px; + left:0; + } + } + + .io-skip-msg { + display:none; + + .scoring-section.skip & { + display:inline; + } + } } .audio-output { @@ -510,8 +565,8 @@ .subcolumn.third { right:0px; } - } + .settings-controls { clear:both; diff --git a/web/app/views/clients/gear/_gear_wizard.html.haml b/web/app/views/clients/gear/_gear_wizard.html.haml index 09d99a642..57f0ef1f8 100644 --- a/web/app/views/clients/gear/_gear_wizard.html.haml +++ b/web/app/views/clients/gear/_gear_wizard.html.haml @@ -38,7 +38,7 @@ %select.w100.select-audio-input-device %option None %h2 Audio Input Ports - .ftue-box.list.ports.output-ports + .ftue-box.list.ports.input-ports %a.button-orange.asio-settings-btn ASIO SETTINGS... %a.button-orange.resync-btn RESYNC .wizard-step-column @@ -46,7 +46,7 @@ %select.w100.select-audio-output-device %option Same as input %h2 Audio Output Ports - .ftue-box.list.ports.input-ports + .ftue-box.list.ports.output-ports .frame-and-buffers .framesize %h2 Frame @@ -83,6 +83,20 @@ .wizard-step-column %h2 Test Results .ftue-box.results + .left.w50.center.white.scoring-section.latency-score-section + .p5 + .latency LATENCY + %span.latency-score + .left.w50.center.white.scoring-section.io-score-section + .p5 + .io I/O + %span.io-skip-msg + Skipped + %span.io-countdown + %span.secs + seconds left + %span.io-rate-score + %span.io-var-score .clearall @@ -195,5 +209,11 @@ %a.button-orange.btn-next{href:'#'} NEXT %a.button-orange.btn-close{href:'#', 'layout-action' => 'close'} CLOSE +%script{type: 'text/template', id: 'template-audio-port'} + .audio-port + %input{ type: 'checkbox', 'data-id' => '{{data.id}}' } + %span + = '{{data.name}}' + diff --git a/web/vendor/assets/javascripts/jquery.icheck.js b/web/vendor/assets/javascripts/jquery.icheck.js index d7d819da3..4e0cffeb9 100644 --- a/web/vendor/assets/javascripts/jquery.icheck.js +++ b/web/vendor/assets/javascripts/jquery.icheck.js @@ -285,10 +285,12 @@ // Check, disable or indeterminate if (/^(ch|di|in)/.test(method) && !active) { + console.log("TAKING ROUTE: ", state); on(input, state); // Uncheck, enable or determinate } else if (/^(un|en|de)/.test(method) && active) { + console.log("TAKING ROUTE2: ", state); off(input, state); // Update @@ -322,7 +324,7 @@ }; // Add checked, disabled or indeterminate state - function on(input, state, keep) { + function on(input, state, keep) { var node = input[0], parent = input.parent(), checked = state == _checked, diff --git a/websocket-gateway/lib/jam_websockets/router.rb b/websocket-gateway/lib/jam_websockets/router.rb index 0f3c8b0e0..1a0bf2e32 100644 --- a/websocket-gateway/lib/jam_websockets/router.rb +++ b/websocket-gateway/lib/jam_websockets/router.rb @@ -65,20 +65,12 @@ module JamWebsockets @client_lookup[client_id] = client_context end - def remove_client(client_id, client) + def remove_client(client_id) deleted = @client_lookup.delete(client_id) if deleted.nil? - @log.warn "unable to delete #{client_id} from client_lookup" - elsif deleted.client != client - # put it back--this is only possible if add_client hit the 'old connection' path - # so in other words if this happens: - # add_client(1, clientX) - # add_client(1, clientY) # but clientX is essentially defunct - this could happen due to a bug in client, or EM doesn't notify always of connection close in time - # remove_client(1, clientX) -- this check maintains that clientY stays as the current client in the hash - @client_lookup[client_id] = deleted - @log.debug "putting back client into @client_lookup for #{client_id} #{client.inspect}" - else + @log.warn "unable to delete #{client_id} from client_lookup because it's already gone" + else @log.debug "cleaned up @client_lookup for #{client_id}" end @@ -377,7 +369,7 @@ module JamWebsockets @log.debug "cleanup up logged-in client #{client}" - remove_client(client.client_id, client) + remove_client(client.client_id) context = @clients.delete(client) @@ -462,6 +454,13 @@ module JamWebsockets user = valid_login(username, password, token, client_id) + # kill any websocket connections that have this same client_id, which can happen in race conditions + existing_client = @client_lookup[client_id] + if existing_client + remove_client(client_id) + existing_client.client.close_websocket + end + connection = JamRuby::Connection.find_by_client_id(client_id) # if this connection is reused by a different user, then whack the connection # because it will recreate a new connection lower down