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