Merge branch 'feature/stats' into develop

This commit is contained in:
Seth Call 2015-12-10 22:09:44 -06:00
commit f82f64f2db
52 changed files with 2639 additions and 501 deletions

View File

@ -1,28 +1,27 @@
ActiveAdmin.register JamRuby::CrashDump, :as => 'Crash Dump' do
# Note: a lame thing is it's not obvious how to make it search on email instead of user_id.
filter :timestamp
filter :user_email, :as => :string
filter :client_id
filter :user_id
menu :parent => 'Misc'
config.sort_order = 'created_at DESC'
index do
column 'User' do |oo| oo.user ? link_to(oo.user.email, oo.user.admin_url, {:title => oo.user.email}) : '' end
column "Client Version", :client_version
column "Client Type", :client_type
column "Download" do |post|
link_to 'Link', post.sign_url
end
column "Timestamp" do |post|
(post.timestamp || post.created_at).strftime('%b %d %Y, %H:%M')
end
column "Client Type", :client_type
column "Dump URL" do |post|
link_to post.uri, post.uri
column "Description" do |post|
post.description
end
column "User ID", :user_id
# FIXME (?): This isn't performant (though it likely doesn't matter). Could probably do a join.
column "User Email" do |post|
unless post.user_id.nil?
post.user_email
end
end
column "Client ID", :client_id
actions
end
end

View File

@ -10,10 +10,11 @@ ActiveAdmin.register JamRuby::DownloadTracker, :as => 'DownloadTrackers' do
index do
column 'User' do |oo| oo.user ? link_to(oo.user.email, oo.user.admin_url, {:title => oo.user.email}) : '' end
column 'Remote IP' do |oo| oo.remote_ip end
column 'Created' do |oo| oo.created_at end
column 'JamTrack' do |oo| oo.jam_track end
column 'Paid' do |oo| oo.paid end
column 'Blacklisted?' do |oo| IpBlacklist.listed(oo.remote_ip) ? 'Yes' : 'No' end
column 'Remote IP' do |oo| oo.remote_ip end
column "" do |oo|
link_to 'Blacklist This IP', "download_trackers/#{oo.id}/blacklist_by_ip"
end

View File

@ -1,6 +1,8 @@
module JamRuby
class CrashDump < ActiveRecord::Base
include JamRuby::S3ManagerMixin
self.table_name = "crash_dumps"
self.primary_key = 'id'
@ -23,5 +25,8 @@ module JamRuby
self.user.email
end
def sign_url(expiration_time = 3600 * 24 * 7, secure=true)
s3_manager.sign_url(self[:ri], {:expires => expiration_time, :secure => secure})
end
end
end

View File

@ -111,10 +111,10 @@ module JamRuby
# the idea is that this is used when a user who has the rights to this tries to download this JamTrack
# we would verify their rights (can_download?), and generates a URL in response to the click so that they can download
# but the url is short lived enough so that it wouldn't be easily shared
def sign_url(expiration_time = 120, bitrate=48, secure=true)
field_name = (bitrate==48) ? "url_48" : "url_44"
s3_manager.sign_url(self[field_name], {:expires => expiration_time, :secure => secure})
end
def sign_url(expiration_time = 120, bitrate=48, secure=true)
field_name = (bitrate==48) ? "url_48" : "url_44"
s3_manager.sign_url(self[field_name], {:expires => expiration_time, :secure => secure})
end
def delete_s3_files
remove_url_48!

View File

@ -138,7 +138,8 @@
context.VideoActions.videoWindowClosed()
}
else if (type === ALERT_NAMES.VST_CHANGED) {
context.ConfigureTracksActions.onVstChanged()
console.log("VST CHANGED!")
context.ConfigureTracksActions.vstChanged()
}
else if((!context.JK.CurrentSessionModel || !context.JK.CurrentSessionModel.inSession()) &&
(ALERT_NAMES.INPUT_IO_RATE == type || ALERT_NAMES.INPUT_IO_JTR == type || ALERT_NAMES.OUTPUT_IO_RATE == type || ALERT_NAMES.OUTPUT_IO_JTR== type)) {

View File

@ -22,136 +22,7 @@
var $instrumentsHolder = null;
var isDragging = false;
function removeHoverer($hoverChannel) {
var $channel = $hoverChannel.data('original')
$channel.data('cloned', null);
$hoverChannel.remove();
}
function hoverIn($channel) {
if(isDragging) return;
var $container = $channel.closest('.target');
var inTarget = $container.length > 0;
if(!inTarget) {
$container = $channel.closest('.channels-holder')
}
var $inputs = $container.find('.ftue-input');
var index = $inputs.index($channel);
// $channel.css('padding', '0 5px');
if(inTarget) {
$channel.data('container', $container)
$channel.addClass('hovering');
$channel.css('color', 'white')
$channel.css('background-color', '#333');
$channel.css('border', '#333');
$channel.css('border-radius', '2px');
$channel.css('min-width', '49%');
$channel.css('width', 'auto');
$channel.css('position', 'absolute');
$container.css('overflow', 'visible');
}
else {
var $offsetParent = $channel.offsetParent();
var parentOffset = $offsetParent.offset();
var hoverChannel = $(context._.template($templateAssignablePort.html(), {id: 'bogus', name: $channel.text(), direction: 'bogus'}, { variable: 'data' }));
hoverChannel
.css('position', 'absolute')
.css('color', 'white')
.css('left', $channel.position().left)
.css('top', $channel.position().top)
.css('background-color', '#333')
.css('min-width', $channel.width())
.css('min-height', $channel.height())
.css('z-index', 10000)
.css('background-position', '4px 6px')
.css('padding-left', '14px')
.data('original', $channel);
$channel.data('cloned', hoverChannel);
hoverChannel
.hover(function(e) {
var hoverCheckTimeout = hoverChannel.data('hoverCheckTimeout');
if(hoverCheckTimeout) {
clearTimeout(hoverCheckTimeout);
hoverChannel.data('hoverCheckTimeout', null);
}
}, function() { removeHoverer($(this)); })
.mousedown(function(e) {
// because we have obscured the element the user wants to drag,
// we proxy a mousedown on the hover-element to the covered .ftue-input ($channel).
// this causes jquery.drag to get going even though the user clicked a different element
$channel.trigger(e)
})
hoverChannel.data('hoverCheckTimeout', setTimeout(function() {
// check if element has already been left
hoverChannel.data('hoverCheckTimeout', null);
removeHoverer(hoverChannel);
}, 500));
hoverChannel.prependTo($offsetParent);
}
$channel.css('z-index', 10000)
if(inTarget && $inputs.length == 2) {
if(index == 0) {
$channel.css('right', '50%')
}
else {
$channel.css('left', '51%')
}
}
}
function hoverOut($channel) {
var $cloned = $channel.data('cloned');
if($cloned) {
return; // let the cloned handle the rest of hover out logic when it's hovered-out
}
$channel
.removeClass('hovering')
.css('color', '')
.css('font-weight', '')
.css('position', '')
.css('width', '')
.css('background-color', '')
.css('padding', '')
.css('padding-left', '')
.css('background-position', '')
.css('border', '')
.css('border-radius', '')
.css('right', '')
.css('left', '')
.css('min-width', '')
.css('z-index', '')
.css('margin-left', '')
.css('max-width', 'auto');
//var $container = $channel.closest('.target');
var $container = $channel.data('container');
if($container) {
$container.css('overflow', '')
}
}
function fixClone($clone) {
$clone
.css('color', '')
.css('font-weight', '')
.css('width', 'auto')
.css('background-color', '')
.css('padding', '')
.css('border', '')
.css('border-radius', '')
.css('right', '')
.css('min-width', '')
}
// inputChannelFilter is an optional argument that is used by the Gear Wizard.
// basically, if an input channel isn't in there, it's not going to be displayed
@ -176,11 +47,6 @@
var $channel = $(context._.template($templateAssignablePort.html(), $.extend({}, inputChannel, {direction:'in'}), { variable: 'data' }));
$channel.hover(
function() { hoverIn ($(this)) },
function() { hoverOut($(this)) }
);
if(forceInputsToUnassign || inputChannel.assignment == ASSIGNMENT.UNASSIGNED) {
unassignInputChannel($channel);
}
@ -205,10 +71,7 @@
context._.each(outputChannels, function (outputChannel, index) {
var $channel = $(context._.template($templateAssignablePort.html(), $.extend({}, outputChannel, {direction:'out'}), { variable: 'data' }));
$channel.hover(
function() { hoverIn ($(this)) },
function() { hoverOut($(this)) }
);
if(outputChannel.assignment == ASSIGNMENT.UNASSIGNED) {
unassignOutputChannel($channel);
@ -223,29 +86,6 @@
}
addChannelToOutput($channel, $output.find('.output-target'));
}
$channel.draggable({
helper: 'clone',
start: function(e,ui) {
isDragging = true;
var $channel = $(this);
fixClone(ui.helper);
var $output = $channel.closest('.output-target');
var isUnassigned = $output.length == 0;
if(isUnassigned) {
$outputChannelHolder.find('.output-target').addClass('possible-target');
}
else {
$outputChannelHolder.find('.output-target').addClass('possible-target');
$unassignedOutputsHolder.addClass('possible-target');
}
},
stop: function() {
isDragging = false;
$outputChannelHolder.find('.output-target').removeClass('possible-target');
$unassignedOutputsHolder.removeClass('possible-target')
}
});
});
}
@ -439,112 +279,9 @@
$originallyAssignedTrack.attr('output-count', $originallyAssignedTrack.find('.ftue-input:not(.ui-draggable-dragging)').length)
}
function initializeUnassignedOutputDroppable() {
$unassignedOutputsHolder.droppable(
{
accept: '.ftue-input[data-direction="out"]',
activeClass: 'drag-in-progress',
hoverClass: 'drag-hovering',
drop: function( event, ui ) {
var $channel = ui.draggable;
//$channel.css('left', '0').css('top', '0');
unassignOutputChannel($channel);
}
});
}
function initializeUnassignedInputDroppable() {
$unassignedInputsHolder.droppable(
{
accept: '.ftue-input[data-direction="in"]',
activeClass: 'drag-in-progress',
hoverClass: 'drag-hovering',
drop: function( event, ui ) {
var $channel = ui.draggable;
//$channel.css('left', '0').css('top', '0');
unassignInputChannel($channel);
}
});
}
function initializeOutputDroppables() {
var i;
for(i = 0; i < MAX_OUTPUTS; i++) {
var $target = $(context._.template($templateOutputTarget.html(), {num: i }, { variable: 'data' }));
$outputChannelHolder.append($target);
$target.find('.output-target').droppable(
{
accept: '.ftue-input[data-direction="out"]',
activeClass: 'drag-in-progress',
hoverClass: 'drag-hovering',
drop: function( event, ui ) {
var $slot = $(this);
if($slot.attr('output-count') == 1) {
return false; // max of 1 output per slot
}
var $channel = ui.draggable;
//$channel.css('left', '0').css('top', '0');
addChannelToOutput($channel, $slot);
}
});
}
}
function initializeTrackDroppables() {
var i;
for(i = 0; i < MAX_TRACKS; i++) {
var $target = $(context._.template($templateTrackTarget.html(), {num: i }, { variable: 'data' }));
$tracksHolder.append($target);
$target.find('.track-target').droppable(
{
accept: '.ftue-input[data-direction="in"]',
activeClass: 'drag-in-progress',
hoverClass: 'drag-hovering',
drop: function( event, ui ) {
var $track = $(this);
if($track.attr('track-count') == 2) {
return false; // max of 2 inputs per track
}
var $channel = ui.draggable;
//$channel.css('left', '0').css('top', '0');
addChannelToTrack($channel, $track);
}
});
}
}
function initializeInstrumentDropdown() {
var i;
for(i = 0; i < MAX_TRACKS; i++) {
var $root = $('<div class="track-instrument"></div>');
$root.instrumentSelector().attr('data-num', i);
$instrumentsHolder.append($root);
}
}
function initialize(_$parent) {
$parent = _$parent;
$templateAssignablePort = $('#template-assignable-port');
$templateTrackTarget = $('#template-track-target');
$templateOutputTarget = $('#template-output-target');
$unassignedInputsHolder = $parent.find('.unassigned-input-channels')
$unassignedOutputsHolder = $parent.find('.unassigned-output-channels');
$tracksHolder = $parent.find('.tracks');
$instrumentsHolder = $parent.find('.instruments');
$outputChannelHolder = $parent.find('.output-channels');
initializeUnassignedInputDroppable();
initializeTrackDroppables();
initializeInstrumentDropdown();
initializeUnassignedOutputDroppable();
initializeOutputDroppables();
}
this.initialize = initialize;

View File

@ -50,7 +50,7 @@
function setInstructions(type) {
if (type === 'audio') {
$instructions.html('Choose your audio device. Drag and drop to assign input ports to tracks, and specify the instrument for each track. Drag and drop to assign a pair of output ports for session stereo audio monitoring.')
$instructions.html("Click the 'ADD LIVE TRACK' button to add more tracks. You may set up a live track for each instrumental and/or vocal part to perform in sessions. You must also set up exactly two Session Audio Output ports to deliver the stereo audio in your sessions.")
return;
var os = context.jamClient.GetOSAsString();
$instructions.html(configure_audio_instructions[os]);
@ -91,7 +91,7 @@
}
function validateAudioSettings() {
return configureTracksHelper.trySave();
return true;
}
function showVoiceChatPanel() {
@ -103,7 +103,7 @@
$musicAudioTabSelector.click(function () {
// validate voice chat settings
if (validateVoiceChatSettings()) {
configureTracksHelper.reset();
window.ConfigureTracksActions.reset(false);
voiceChatHelper.reset();
showMusicAudioPanel();
}
@ -113,7 +113,7 @@
// validate audio settings
if (validateAudioSettings()) {
logger.debug("initializing voice chat helper")
configureTracksHelper.reset();
window.ConfigureTracksActions.reset(false);
voiceChatHelper.reset();
showVoiceChatPanel();
}
@ -133,7 +133,7 @@
//});
$btnUpdateTrackSettings.click(function() {
if(configureTracksHelper.trySave() && voiceChatHelper.trySave()) {
if(voiceChatHelper.trySave()) {
app.layout.closeDialog('configure-tracks');
}
@ -152,7 +152,7 @@
});
$certifiedAudioProfile.html(optionsHtml);
context.JK.dropdown($certifiedAudioProfile);
//context.JK.dropdown($certifiedAudioProfile);
}
function deviceChanged() {
@ -183,7 +183,7 @@
currentProfile = profile;
configureTracksHelper.reset();
window.ConfigureTracksActions.reset(false);
}
function beforeShow() {
@ -207,13 +207,16 @@
return;
}
configureTracksHelper.reset();
window.ConfigureTracksActions.reset(false);
voiceChatHelper.reset();
voiceChatHelper.beforeShow();
}
function afterShow() {
sessionUtils.SessionPageEnter();
//context.ConfigureTracksActions.vstScan();
}
function onCancel() {
@ -247,8 +250,8 @@
$btnAddNewGear = $dialog.find('.btn-add-new-audio-gear');
$btnUpdateTrackSettings = $dialog.find('.btn-update-settings');
configureTracksHelper = new context.JK.ConfigureTracksHelper(app);
configureTracksHelper.initialize($dialog);
//configureTracksHelper = new context.JK.ConfigureTracksHelper(app);
//configureTracksHelper.initialize($dialog);
voiceChatHelper = new context.JK.VoiceChatHelper(app);
voiceChatHelper.initialize($dialog, 'configure_track_dialog', true, {vuType: "vertical", lightCount: 10, lightWidth: 3, lightHeight: 17}, 191);

View File

@ -1052,6 +1052,16 @@
function GetAutoStart() { return true; }
function SaveSettings() {}
function VSTScan(callback) {setTimeout(eval(callback+ "()"), 1000)}
function hasVstHost() { return false;}
function getPluginList() { return {vsts:[]} }
function clearPluginList() {}
function listTrackAssignments() {
return {}
}
// Javascript Bridge seems to camel-case
// Set the instance functions:
this.AbortRecording = AbortRecording;
@ -1315,6 +1325,11 @@
this.StopNetworkTest = StopNetworkTest;
this.log = log;
this.getOperatingMode = getOperatingMode;
this.VSTScan = VSTScan;
this.hasVstHost = hasVstHost;
this.getPluginList = getPluginList;
this.clearPluginList = clearPluginList;
this.listTrackAssignments = listTrackAssignments;
this.clientID = "devtester";
};

View File

@ -53,7 +53,9 @@
METRONOME_PLAYBACK_MODE_SELECTED: 'metronome_playback_mode_selected',
CHECKOUT_SIGNED_IN: 'checkout_signed_in',
CHECKOUT_SKIP_SIGN_IN: 'checkout_skip_sign_in',
PREVIEW_PLAYED: 'preview_played'
PREVIEW_PLAYED: 'preview_played',
VST_OPERATION_SELECTED: 'vst_operation_selected',
VST_EFFECT_SELECTED: 'vst_effect_selected'
};
context.JK.PLAYBACK_MONITOR_MODE = {

View File

@ -0,0 +1,71 @@
(function(context, $) {
"use strict";
context.JK = context.JK || {};
// creates an iconic/graphical instrument selector. useful when there is minimal real-estate
$.fn.manageVsts = function(options) {
return this.each(function(index) {
function close() {
$parent.btOff();
$parent.focus();
}
var $parent = $(this);
function onManageVstSelected() {
var $li = $(this);
var vstOperation = $li.attr('data-manage-vst-option');
close();
$parent.triggerHandler(context.JK.EVENTS.VST_OPERATION_SELECTED, {vstOperation: vstOperation});
return false;
};
// if the user goes into the bubble, remove
function waitForBubbleHover($bubble) {
$bubble.hoverIntent({
over: function() {
if(timeout) {
clearTimeout(timeout);
timeout = null;
}
},
out: function() {
$parent.btOff();
}});
}
var timeout = null;
context.JK.hoverBubble($parent, $('#template-manage-vsts').html(), {
trigger:'none',
cssClass: 'manage-vsts-popup',
spikeGirth:0,
spikeLength:0,
width:190,
closeWhenOthersOpen: true,
offsetParent: $parent.closest('.dialog'),
positions:['bottom'],
preShow: function() {
},
postShow:function(container) {
$(container).find('li').click(onManageVstSelected)
if(timeout) {
clearTimeout(timeout);
timeout = null;
}
waitForBubbleHover($(container))
timeout = setTimeout(function() {$parent.btOff()}, 3000)
}
});
});
}
})(window, jQuery);

View File

@ -0,0 +1,75 @@
(function(context, $) {
"use strict";
context.JK = context.JK || {};
// creates an iconic/graphical instrument selector. useful when there is minimal real-estate
$.fn.trackEffects = function(options) {
return this.each(function(index) {
function close() {
$parent.btOff();
$parent.focus();
}
var $parent = $(this);
function onOptionSelected() {
var $li = $(this);
var vstOperation = $li.attr('data-manage-vst-option');
close();
$parent.triggerHandler(context.JK.EVENTS.VST_EFFECT_SELECTED, {vstOperation: vstOperation});
return false;
};
// if the user goes into the bubble, remove
function waitForBubbleHover($bubble) {
$bubble.hoverIntent({
over: function() {
if(timeout) {
clearTimeout(timeout);
timeout = null;
}
},
out: function() {
$parent.btOff();
}});
}
var timeout = null;
context.JK.hoverBubble($parent, $('#template-vst-effects').html(), {
trigger:'none',
cssClass: 'vst-effects-popup',
spikeGirth:0,
spikeLength:0,
width:220,
closeWhenOthersOpen: true,
offsetParent: $parent.closest('.screen'),
positions:['bottom'],
preShow: function() {
},
postShow:function(container) {
if (options && options['postShow']) {
options['postShow']($(container))
}
$(container).find('li').click(onOptionSelected)
if(timeout) {
clearTimeout(timeout);
timeout = null;
}
waitForBubbleHover($(container))
timeout = setTimeout(function() {$parent.btOff()}, 3000)
}
});
});
}
})(window, jQuery);

View File

@ -7,7 +7,9 @@
//= require ./react-components/stores/RecordingStore
//= require ./react-components/stores/VideoStore
//= require ./react-components/stores/SessionStore
//= require ./react-components/stores/SessionStatsStore
//= require ./react-components/stores/MixerStore
//= require ./react-components/stores/ConfigureTracksStore
//= require ./react-components/stores/JamTrackStore
//= require ./react-components/stores/SessionNotificationStore
//= require ./react-components/stores/MediaPlaybackStore

View File

@ -0,0 +1,419 @@
context = window
ConfigureTracksStore = @ConfigureTracksStore
@ConfigureLiveTracksDialog = React.createClass({
mixins: [Reflux.listenTo(@ConfigureTracksStore,"onConfigureTracksChanged"), Reflux.listenTo(@AppStore, "onAppInit")]
onConfigureTracksChanged:(configureTracks) ->
@setState({configureTracks: configureTracks})
onAppInit: (@app) ->
getInitialState: () ->
{configureTracks: null, midiInterface: null}
renderAudio: () ->
inputOneOptions = []
inputTwoOptions = []
defaultSelectionOne = `<option value="">Select an input port for this track (required)</option>`
defaultSelectionTwo = `<option value="">Select an input port for this track (optional)</option>`
inputOneOptions.push(defaultSelectionOne)
inputTwoOptions.push(defaultSelectionTwo)
inputOneValue = ''
inputTwoValue = ''
selectedInstrument = ''
selectedVst = 'NONE'
instruments = []
instruments.push(`<option value="">Select the instrument for this track</option>`)
for displayName, value of context.JK.server_to_client_instrument_map
instruments.push(`<option value={value.server_id}>{displayName}</option>`)
vsts = []
instrumentDisabled = true
vstDisabled = true
if @state.configureTracks?
if @state.configureTracks.scanningVsts
scan =
`<div className="vstScan">
<div className="spinner-small"></div><span>Scanning your system<br/>for VST &amp; AU plug-ins...</span>
</div>`
selectedInstrument = @state.configureTracks.editingTrack.instrument_id if @state.configureTracks.editingTrack.instrument_id?
if @state.configureTracks.editingTrack.length == 1
input = @state.configureTracks.editingTrack[0]
if input.number == 0
inputOneValue = input.id
else
inputTwoValue = input.id
if @state.configureTracks.editingTrack.length > 1
inputOneValue = @state.configureTracks.editingTrack[0].id
inputTwoValue = @state.configureTracks.editingTrack[1].id
instrumentDisabled = @state.configureTracks.editingTrack.length == 0
vstDisabled = @state.configureTracks.editingTrack.length == 0
for input in @state.configureTracks.musicPorts.inputs
include = false
# we need to see that this input is unassigned, or one of the two selected
for unassignedInputs in @state.configureTracks.trackAssignments.inputs.unassigned
if unassignedInputs.id == input.id
include = true
break
if !include
# not see if it's the currently edited track
for currentInput in @state.configureTracks.editingTrack
if currentInput.id == input.id
include = true
if include
item = `<option value={input.id}>{input.name}</option>`
inputOneOptions.push(item)
inputTwoOptions.push(item)
for plugin in @state.configureTracks.vstPluginList.vsts
if plugin.isInstrument == false && plugin.category == 'Effect'
vsts.push(`<option value={plugin.file}>{plugin.name} by {plugin.manuf}</option>`)
else if plugin.category == 'NONE'
vsts.push(`<option value={plugin.file}>No VST/AU plugin selected</option>`)
if @state.configureTracks.editingTrack.vst?
vstAssignedThisTrack = true
selectedVst = @state.configureTracks.editingTrack.vst.file
vstSettingBtnClasses = classNames({'button-orange': vstAssignedThisTrack, 'button-grey': !vstAssignedThisTrack})
`<div className="audio">
<div className="audio-input-ports">
<h3>Audio Input Ports</h3>
<p>Select one or two inputs ports to assign to this track. Note that if you assign a single input port, the app will automatically duplicate this port into a stereo track.</p>
<select className="input-one" name="input-one" onChange={this.inputChanged} value={inputOneValue}>
{inputOneOptions}
</select>
<select className="input-two" name="input-two" onChange={this.inputChanged} value={inputTwoValue}>
{inputTwoOptions}
</select>
</div>
<div className="instrument-selection">
<h3>Instrument</h3>
<select className="instrument-pick" name="instrument" onChange={this.instrumentSelected} value={selectedInstrument} disabled={instrumentDisabled}>
{instruments}
</select>
</div>
<div className="audio-effects">
<h3>Audio Effects (optional)</h3>
<select className="vsts" name="vsts" onChange={this.vstsChanged} value={selectedVst} disabled={vstDisabled}>
{vsts}
</select>
<a className="manage-audio-plugins" onClick={this.manageAudioPlugins}>manage audio plugins <div className="down-arrow"></div></a>
<div className="settings-holder">
<a onClick={this.vstSettings} className={vstSettingBtnClasses}>SETTINGS . . .</a>
</div>
{scan}
</div>
</div>`
renderMidi: () ->
midiInterfaces = []
midiInterfaces.push(`<option value="">Select a MIDI interface</option>`)
midiInstruments = []
instruments = []
for displayName, value of context.JK.server_to_client_instrument_map
instruments.push(`<option value={value.server_id}>{displayName}</option>`)
selectedMidiInterface = ''
selectedInstrument = context.JK.client_to_server_instrument_map[50].server_id # default to electric guitar
selectedMidiInstrument = ''
instrumentDisabled = true
midiInstrumentDisabled = true
vstAssignedThisTrack = false
if @state.configureTracks?
logger.debug("current midi device: " + @state.configureTracks.editingTrack.midiDeviceIndex)
selectedMidiInterface = @state.configureTracks.editingTrack.midiDeviceIndex
selectedInstrument = @state.configureTracks.editingTrack.instrument_id if @state.configureTracks.editingTrack.instrument_id?
instrumentDisabled = !@state.midiInterface? || !selectedMidiInterface?
midiInstrumentDisabled = !@state.midiInterface? || !selectedMidiInterface?
midiInstrumentDisabled = false
instrumentDisabled = false
if @state.configureTracks.editingTrack.vst?
vstAssignedThisTrack = true
selectedMidiInstrument = @state.configureTracks.editingTrack.vst.file
vstSettingBtnClasses = classNames({'button-orange': vstAssignedThisTrack, 'button-grey': !vstAssignedThisTrack})
for midiDevice in @state.configureTracks.attachedMidiDevices.midiDevices
midiInterfaces.push(`<option value={midiDevice.deviceIndex}>{midiDevice.deviceName}</option>`)
for plugin in @state.configureTracks.vstPluginList.vsts
if plugin.isInstrument == true
midiInstruments.push(`<option value={plugin.file}>{plugin.name} by {plugin.manuf}</option>`)
else if plugin.category == 'NONE'
midiInstruments.push(`<option value={plugin.file}>Select a VST or AU instrument</option>`)
`<div className="midi">
<div className="midi-interface">
<h3>MIDI Interface</h3>
<select className="midi-select" name="midi-select" onChange={this.midiInterfaceChanged} value={selectedMidiInterface}>
{midiInterfaces}
</select>
<a className="scan-midi" onClick={this.scanMidi}>scan for connected MIDI interfaces</a>
</div>
<div className="instrument-selection">
<h3>Instrument</h3>
<select className="instrument-pick" name="instrument" onChange={this.instrumentSelected} value={selectedInstrument} disabled={instrumentDisabled}>
{instruments}
</select>
</div>
<div className="midi-instrument">
<h3>MIDI Instrument (VST or AU Plugin)</h3>
<select className="vsts" name="midi-instrument" onChange={this.vstsChanged} value={selectedMidiInstrument} disabled={midiInstrumentDisabled}>
{midiInstruments}
</select>
<a className="manage-audio-plugins" onClick={this.manageAudioPlugins}>manage audio plugins <div className="down-arrow"></div></a>
<div className="settings-holder">
<a onClick={this.vstSettings} className={vstSettingBtnClasses}>SETTING . . .</a>
</div>
</div>
</div>`
render: () ->
action = 'ADD TRACK'
header = 'add track'
isAudio = !@state.configureTracks? || @state.configureTracks.trackType == 'audio'
isMidi = !isAudio
if isAudio
activeElement = @renderAudio()
else
activeElement = @renderMidi()
if !@state.configureTracks?.newTrack
action = 'CLOSE'
header = 'update track'
else
cancelBtn = `<a onClick={this.onCancel} className="button-grey">CANCEL</a>`
`<div>
<div className="content-head">
<img className="content-icon" src="/assets/content/icon_add.png" height={19} width={19}/>
<h1>{header}</h1>
</div>
<div className="dialog-inner">
<div className="track-type">
<h3>Track Type</h3>
<div className="track-type-option"><input type="radio" value="audio" name="track-type" checked={isAudio} /><label>Audio</label></div>
<div className="track-type-option"><input type="radio" value="midi" name="track-type" checked={isMidi} /><label>MIDI</label></div>
</div>
{activeElement}
<div className="actions">
{cancelBtn}
<a onClick={this.doClose} className="button-orange">{action}</a>
</div>
</div>
</div>`
inputChanged: (e) ->
$root = $(@getDOMNode())
$select1 = $root.find('.input-one')
$select2 = $root.find('.input-two')
audioInput1 = $select1.val()
audioInput2 = $select2.val()
if audioInput1 == ''
audioInput1 = null
if audioInput2 == ''
audioInput2 = null
if audioInput1? && audioInput1 == audioInput2
e.preventDefault()
# TODO: tell user they can't do this
return
ConfigureTracksActions.associateInputsWithTrack(audioInput1, audioInput2)
vstsChanged: (e) ->
$root = $(@getDOMNode())
$select = $root.find('select.vsts')
vstSelected = $select.val()
if vstSelected != 'NONE'
vstSelected = {file: vstSelected}
if @state.configureTracks?.trackType == 'midi'
@updateMidiAssociations()
else
ConfigureTracksActions.associateVSTWithTrack(vstSelected)
@setState({midiInterface: null})
instrumentSelected: (e) ->
$root = $(@getDOMNode())
$select = $root.find('.instrument-pick')
instrumentId = $select.val()
ConfigureTracksActions.associateInstrumentWithTrack(instrumentId)
doClose: (e) ->
e.preventDefault()
# check that instrument is selected
$root = $(@getDOMNode())
$instrument = $root.find('.instrument-pick')
instrumentId = $instrument.val()
$select1 = $root.find('.input-one')
$select2 = $root.find('.input-two')
audioInput1 = $select1.val()
audioInput2 = $select2.val()
if audioInput1 == ''
audioInput1 = null
if audioInput2 == ''
audioInput2 = null
if audioInput1 == null && audioInput2 == null
context.JK.Banner.showAlert("At least one input must be specified.")
return
if instrumentId == null || instrumentId == ''
context.JK.Banner.showAlert("Please select an instrument.")
return
@app.layout.closeDialog('configure-live-tracks-dialog', false)
onCancel: (e) ->
ConfigureTracksActions.cancelEdit()
@app.layout.closeDialog('configure-live-tracks-dialog', true)
vstSettings: (e) ->
e.preventDefault()
ConfigureTracksActions.showVstSettings()
componentDidMount: () ->
$root = $(@getDOMNode())
$radio = context.JK.checkbox($root.find('input[type="radio"]'))
$radio.on("ifChanged", @trackTypeChanged);
componentWillUpdate: () ->
@ignoreICheck = true
$root = $(@getDOMNode())
$radio = $root.find('input[type="radio"]')
#$radio.iCheck('enable')
$radio.iCheck('enable')
componentDidUpdate: () ->
$root = $(@getDOMNode())
$radio = $root.find('input[type="radio"]')
$radio = context.JK.checkbox($root.find('input[type="radio"]'))
$radio.on("ifChanged", @trackTypeChanged);
if @state.configureTracks.editingTrack.assignment == 1
$radio.iCheck('disable')
else
$radio.iCheck('enable')
@ignoreICheck = false
$manageAudioPlugins = $root.find('.manage-audio-plugins')
unless $manageAudioPlugins.data('initialized')
$manageAudioPlugins.manageVsts().on(context.JK.EVENTS.VST_OPERATION_SELECTED, @vstOperation).data('initialized', true)
trackTypeChanged: (event) ->
if @ignoreICheck
logger.debug("ignoring track type changed")
return
$checkedType = $(event.target);
value = $checkedType.val()
logger.debug("trackTypeChanged: " + value, $checkedType)
ConfigureTracksActions.desiredTrackType(value)
#@setState({trackType: value})
vstOperation: (e, data) ->
if data.vstOperation == 'scan'
ConfigureTracksActions.vstScan()
else if data.vstOperation == 'clear'
ConfigureTracksActions.clearVsts()
manageAudioPlugins: (e) ->
e.preventDefault()
$root = $(@getDOMNode())
$manageAudioPlugins = $root.find('.manage-audio-plugins')
$manageAudioPlugins.btOn()
scanMidi: (e) ->
e.preventDefault()
ConfigureTracksActions.midiScan()
midiInterfaceChanged: (e) ->
@updateMidiAssociations()
updateMidiAssociations: (e) ->
$root = $(@getDOMNode())
$select = $root.find('select.midi-select')
midiInterface = $select.val()
$select = $root.find('select.vsts')
vstSelected = $select.val()
logger.debug("updateMidiAssocations", vstSelected, midiInterface)
if vstSelected != 'NONE'
vstSelected = {file: vstSelected}
else
vstSelected = null
if midiInterface == ''
midiInterface = null
midi = @state.midiInterface || midiInterface
if vstSelected? && midi?
logger.debug("updating midi:#{midi} & vst: #{vstSelected.file}")
ConfigureTracksActions.associateVSTWithTrack(vstSelected)
setTimeout((() =>
ConfigureTracksActions.associateMIDIWithTrack(midi)
), 250)
else if midi?
logger.debug("updating midi:#{midiInterface}")
ConfigureTracksActions.associateMIDIWithTrack(midiInterface)
@setState({midiInterface: midiInterface})
})

View File

@ -0,0 +1,87 @@
context = window
ConfigureTracksStore = @ConfigureTracksStore
@ConfigureOutputsDialog = React.createClass({
mixins: [Reflux.listenTo(@ConfigureTracksStore, "onConfigureTracksChanged"), Reflux.listenTo(@AppStore, "onAppInit")]
onConfigureTracksChanged: (configureTracks) ->
@setState({configureTracks: configureTracks})
onAppInit: (@app) ->
getInitialState: () ->
{configureTracks: null}
render: () ->
outputs = []
outputs.push(`<option value="">Select an output port for session audio</option>`)
if @state.configureTracks?
for output in @state.configureTracks.musicPorts.outputs
outputs.push(`<option value={output.id}>{output.name}</option>`)
`<div>
<div className="content-head">
<img className="content-icon" src="/assets/shared/icon_session.png" height={24} width={24}/>
<h1>session audio outputs</h1>
</div>
<div className="dialog-inner">
<p>Select two audio output ports that will be used to deliver the stereo audio of your sessions to your headphones or monitor.</p>
<select className="output-1" >
{outputs}
</select>
<select className="output-2" >
{outputs}
</select>
<div className="actions">
<a onClick={this.onCancel} className="button-grey">CANCEL</a>
<a onClick={this.onClose} className="button-orange">UPDATE PORTS</a>
</div>
</div>
</div>`
componentDidUpdate: () ->
$root = $(@getDOMNode())
$output1 = $root.find('.output-1')
$output2 = $root.find('.output-2')
if @state.configureTracks? && @state.configureTracks.trackAssignments.outputs.assigned.length == 2
output1 = @state.configureTracks.trackAssignments.outputs.assigned[0].id
output2 = @state.configureTracks.trackAssignments.outputs.assigned[1].id
$output1.val(output1)
$output2.val(output2)
onClose: (e) ->
e.preventDefault()
$root = $(@getDOMNode())
$output1 = $root.find('.output-1')
$output2 = $root.find('.output-2')
output1 = $output1.val()
output2 = $output2.val()
if output1 == null || output1 == ''
context.JK.Banner.showAlert("Both output ports must have a selection.")
return
if output2 == null || output2 == ''
context.JK.Banner.showAlert("Both output ports must have a selection.")
return
if output1? && output1 != '' && output1 == output2
context.JK.Banner.showAlert("Both output ports can not be the same.")
return
ConfigureTracksActions.updateOutputs(output1, output2)
@app.layout.closeDialog('configure-outputs-dialog', false)
onCancel: (e) ->
@app.layout.closeDialog('configure-outputs-dialog', true)
}
)

View File

@ -0,0 +1,120 @@
context = window
rest = context.JK.Rest()
ReactCSSTransitionGroup = React.addons.CSSTransitionGroup
ASSIGNMENT = context.JK.ASSIGNMENT
VOICE_CHAT = context.JK.VOICE_CHAT
MAX_TRACKS = context.JK.MAX_TRACKS
MAX_OUTPUTS = context.JK.MAX_OUTPUTS
gearUtils = context.JK.GearUtils
@ConfigureTracks = React.createClass({
mixins: [Reflux.listenTo(@ConfigureTracksStore,"onConfigureTracksChanged")]
getInitialState: () ->
{configureTracks: null}
onConfigureTracksChanged: (configureTracks) ->
@setState({configureTracks: configureTracks})
render: () ->
liveTracks = []
outputs = []
trackAssignments = @state.configureTracks?.trackAssignments
if trackAssignments
for inputsForTrack in trackAssignments.inputs.assigned
candidate = inputsForTrack[0]
inputs = []
for input in inputsForTrack
inputs.push(`<div className="live-input">{input.name}</div>`)
if !inputsForTrack.instrument_id?
instrument = `<span className="none">?</span>`
else
instrument = `<span><img src={context.JK.getInstrumentIconMap24()[inputsForTrack.instrument_id].asset} /></span>`
trackTypeLabel = 'AUDIO'
vstName = 'None'
if inputsForTrack.vst? && inputsForTrack.vst != 'NONE'
vstName = "#{inputsForTrack.vst.name} by #{inputsForTrack.vst.manuf}"
liveTracks.push(
`<div key={candidate.assignment} className="live-track">
<div className={classNames({'input-track-info': true, one: inputsForTrack.length == 1, two: inputsForTrack.length == 2})}><span className="assignment">{candidate.assignment}:</span><span className="track-type-label">{trackTypeLabel}</span>{inputs}</div>
<div className="plugin-info">{vstName}</div>
<div className="plugin-instrument">{instrument}</div>
<div className="live-track-actions">
<a className="update-live-track" onClick={this.onUpdateLiveTrack.bind(this, inputsForTrack)}>update</a>
<a className="delete-live-track" onClick={this.onDeleteLiveTrack.bind(this, inputsForTrack)}>delete</a>
</div>
</div>`)
for output, i in trackAssignments.outputs.assigned
outputs.push(
`<div key={output.id} className="output-track">
<div className="output-track-info"><span className="assignment">{i + 1}:</span><div className="output">{output.name}</div></div>
</div>`)
`<div className="ConfigureTracks">
<select className="certified-audio-profile" style={{display:'none'}}></select>
<div className="inputs-view">
<div>
<h3 className="session-audio-inputs-header">Session Audio Inputs (Live Performance Tracks)</h3>
<h3 className="plugin-header">Plugin</h3>
<h3 className="instrument-header">Instrument</h3>
</div>
<div className="live-tracks">
{liveTracks}
</div>
<div className="add-track-action">
<a onClick={this.openLiveTrackDialog} className="button-orange">ADD TRACK . . . </a>
</div>
</div>
<div className="outputs-view">
<div>
<h3 className="session-audio-outputs-header">Session Audio Outputs (Requires 2 Ports)</h3>
<div className="output-tracks">
{outputs}
</div>
<a onClick={this.openOutputTrackDialog} className="button-orange">UPDATE OUTPUTS . . . </a>
</div>
</div>
<div className="clearall"></div>
</div>`
onUpdateLiveTrack:(liveTrack, e) ->
e.preventDefault()
ConfigureTracksActions.showEditTrack(liveTrack.assignment)
onDeleteLiveTrack:(liveTrack, e) ->
e.preventDefault()
if liveTrack.assignment == 1
# can't delete the last assignment
context.JK.Banner.showAlert('You can not delete the 1st audio track.')
else
context.JK.Banner.showYesNo({
title: "Confirm Deletion",
html: "Are you sure you want to delete this live track?",
yes: =>
ConfigureTracksActions.deleteTrack(liveTrack.assignment)
})
openLiveTrackDialog: (e) ->
e.preventDefault()
ConfigureTracksActions.showAddNewTrack()
openOutputTrackDialog: (e) ->
e.preventDefault()
ConfigureTracksActions.showEditOutputs()
})

View File

@ -1,9 +1,22 @@
context = window
MixerActions = @MixerActions
ConfigureTracksActions = @ConfigureTracksActions
@SessionMyTrack = React.createClass({
mixins: [Reflux.listenTo(@SessionStatsStore,"onStatsChanged")]
onStatsChanged: (stats) ->
@setState({stats: stats[@props.clientId]})
getInitialState: () ->
stats = window.SessionStatsStore.stats
if stats?
clientStats = stats[@props.clientId]
else
clientStats = null
{stats: clientStats}
handleMute: (e) ->
e.preventDefault()
@ -42,7 +55,17 @@ MixerActions = @MixerActions
WebkitTransform: "rotate(#{pan}deg)"
}
# <div className="track-icon-equalizer" />
classification = @state.stats?.classification
if !classification?
classification = 'unknown'
connectionStateClasses = { 'track-connection-state': true}
connectionStateClasses[classification] = true
if @props.associatedVst?
@equalizerSet = true
vst = `<div className="track-icon-equalizer" />`
`<div className={trackClasses}>
<div className="disabled-track-overlay" />
@ -51,15 +74,15 @@ MixerActions = @MixerActions
<div className="track-avatar"><img src={this.props.photoUrl}/></div>
<div className="track-instrument"><img height="45" src={this.props.instrumentIcon} width="45" /></div>
<div className="track-controls">
<SessionTrackVU orientation="horizontal" lightCount={4} lightWidth={17} lightHeight={3} side="best" mixers={this.props.mixers} />
<SessionTrackVU orientation="horizontal" lightCount={5} lightWidth={17} lightHeight={3} side="best" mixers={this.props.mixers} />
<div className="track-buttons">
<div className={classes} data-control="mute" data-mixer-id={muteMixerId} onClick={this.handleMute}/>
<div className="track-icon-pan" style={panStyle}/>
{vst}
<div className={classNames(connectionStateClasses)}/>
</div>
<br className="clearall"/>
</div>
<br className="clearall"/>
</div>
</div>`
@ -71,6 +94,7 @@ MixerActions = @MixerActions
$root = $(this.getDOMNode())
$mute = $root.find('.track-icon-mute')
$pan = $root.find('.track-icon-pan')
$connectionState = $root.find('.track-connection-state')
context.JK.interactReactBubble(
$mute,
@ -88,6 +112,19 @@ MixerActions = @MixerActions
,
{width:331, positions:['right', 'left'], offsetParent:$root.closest('.top-parent')})
context.JK.interactReactBubble(
$connectionState,
'SessionStatsHover',
() =>
{participant: {client_id: this.props.clientId, user: name: 'You', possessive: 'Your'}, }
,
{width:380, positions:['right', 'left'], offsetParent:$root.closest('.screen'), extraClasses: 'self'})
unless this.props.hasMixer
$mute.on("mouseenter", false)
$mute.on("mouseleave", false)
$pan.on("mouseentere", false)
$pan.on("mouseleave", false)
unless this.props.hasMixer
$mute.on("mouseenter", false)
$mute.on("mouseleave", false)
@ -96,6 +133,8 @@ MixerActions = @MixerActions
context.JK.helpBubble($root.find('.disabled-track-overlay'), 'missing-my-tracks', {}, {positions:['top'], offsetParent: $root.closest('.top-parent')})
@initializeVstEffects()
componentWillUpdate: (nextProps, nextState) ->
$root = $(this.getDOMNode())
$mute = $root.find('.track-icon-mute')
@ -116,4 +155,32 @@ MixerActions = @MixerActions
$mute.on("mouseleave", false)
$pan.on("mouseentere", false)
$pan.on("mouseleave", false)
componentDidUpdate:() ->
@initializeVstEffects()
initializeVstEffects: () ->
$root = $(this.getDOMNode())
$equalizer = $root.find('.track-icon-equalizer')
if $equalizer.length > 0 && !$equalizer.data('initialized')
logger.debug("initializing trackEffects", $equalizer)
$equalizer.trackEffects({postShow: @prepVstEffects}).on(context.JK.EVENTS.VST_EFFECT_SELECTED, @vstOperation).data('initialized', true).click(() =>
logger.debug("clicked!")
$equalizer.btOn()
)
prepVstEffects: ($container) ->
$container.find('.vst-name').text(@props.associatedVst?.name)
vstOperation: (e, data) ->
logger.debug("track effect selection: " + data.vstOperation)
if !@props.associatedVst?
logger.warn("no associated VST")
return
if data.vstOperation == 'open-vst'
ConfigureTracksActions.showVstSettings(@props.associatedVst.track)
else
ConfigureTracksActions.showEditTrack(@props.associatedVst.track + 1)
})

View File

@ -5,7 +5,7 @@ ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;
@SessionMyTracks = React.createClass({
mixins: [@SessionMyTracksMixin, Reflux.listenTo(@SessionMyTracksStore,"onInputsChanged"), Reflux.listenTo(@AppStore,"onAppInit")]
mixins: [@SessionMyTracksMixin, Reflux.listenTo(@SessionMyTracksStore,"onInputsChanged"), Reflux.listenTo(@AppStore,"onAppInit"), Reflux.listenTo(@ConfigureTracksStore, "onConfigureTracksChanged")]
goToFtue: (e) ->
e.preventDefault()

View File

@ -4,6 +4,19 @@ MixerActions = @MixerActions
@SessionOtherTrack = React.createClass({
mixins: [Reflux.listenTo(@SessionStatsStore,"onStatsChanged")]
onStatsChanged: (stats) ->
@setState({stats: stats[@props.participant.client_id]})
getInitialState: () ->
stats = window.SessionStatsStore.stats
if stats?
clientStats = stats[@props.participant.client_id]
else
clientStats = null
{stats: clientStats}
handleMute: (e) ->
e.preventDefault()
@ -48,6 +61,14 @@ MixerActions = @MixerActions
WebkitTransform: "rotate(#{pan}deg)"
}
classification = @state.stats?.classification
if !classification?
classification = 'unknown'
connectionStateClasses = { 'track-connection-state': true}
connectionStateClasses[classification] = true
`<div className={componentClasses}>
<div className="disabled-track-overlay" />
<div className="session-track-contents">
@ -59,6 +80,7 @@ MixerActions = @MixerActions
<div className="track-buttons">
<div className={classes} data-control="mute" data-mixer-id={muteMixerId} onClick={this.handleMute}/>
<div className="track-icon-pan" style={panStyle}/>
<div className={classNames(connectionStateClasses)}/>
</div>
<br className="clearall"/>
</div>
@ -77,6 +99,7 @@ MixerActions = @MixerActions
$root = $(this.getDOMNode())
$mute = $root.find('.track-icon-mute')
$pan = $root.find('.track-icon-pan')
$connectionState = $root.find('.track-connection-state')
context.JK.interactReactBubble(
$mute,
@ -96,6 +119,14 @@ MixerActions = @MixerActions
,
{width:331, positions:['right', 'left'], offsetParent:$root.closest('.screen')})
context.JK.interactReactBubble(
$connectionState,
'SessionStatsHover',
() =>
{participant: this.props.participant}
,
{width:380, positions:['right', 'left'], offsetParent:$root.closest('.screen')})
unless this.props.hasMixer
$mute.on("mouseenter", false)
$mute.on("mouseleave", false)

View File

@ -17,6 +17,11 @@ ReactCSSTransitionGroup = React.addons.CSSTransitionGroup
for participant in session.otherParticipants()
if participant.user.id == context.JK.currentUserId
participant.user.possessive = "Your"
else
participant.user.possessive = participant.user.name + "'s"
name = participant.user.name
tracks = []
firstTrack = participant.tracks[0]

View File

@ -0,0 +1,217 @@
context = window
ChannelGroupIds = context.JK.ChannelGroupIds
MixerActions = @MixerActions
ptrCount = 0
StatsInfo = {
system: {
cpu: {
good: (user, stats) -> "#{user.possessive} computer processor is not overworked by JamKazam or the your system.",
warn: (user, stats) -> "#{user.possessive} computer processor is being heavily used. There is little spare capacity, and this means your processor may not be able to handle all of it's tasks, causing audio quality, latency, and other issues.",
poor: (user, stats) -> "#{user.possessive} computer processor is being very heavily used. There is little spare capacity, and this means your processor may not be able to handle all of it's tasks, causing audio quality, latency, and other issues."
}
},
network: {
wifi: {
good: (user, stats) -> "#{user.name} is using wired ethernet.",
warn: (user, stats) -> "#{user.name} is using Wi-Fi, which will create audio quality issues and additional latency.",
poor: (user, stats) -> "#{user.name} is using Wi-Fi, which will create audio quality issues and additional latency.",
},
net_bitrate: {
good: (user, stats) -> "#{user.name} has enough bandwidth to send you a high quality audio stream.",
warn: (user, stats) -> "#{user.name} has bandwidth to send you a degraded, but sufficient, audio stream.",
poor: (user, stats) -> "#{user.name} has not enough bandwidth to send you a decent quality audio stream.",
},
ping: {
good: (user, stats) -> "The internet connection between you and #{user.name} has very low latency.",
warn: (user, stats) -> "The internet connection between you and #{user.name} has average latency, which may affect staying in sync.",
poor: (user, stats) -> "The internet connection between you and #{user.name} has high latency, making it very difficult to stay in sync.",
},
pkt_loss: {
good: (user, stats) -> "The internet connection between you and #{user.name} loses a small % of packets. It should not affect your audio quality.",
warn: (user, stats) -> "The internet connection between you and #{user.name} loses a significant % of packets. This may result in periodical audio artifacts.",
poor: (user, stats) -> "The internet connection between you and #{user.name} loses a high % of packets. This will result in frequent audio artifacts.",
},
audiojq_median: {
good: (user, stats) -> "JamKazam has to maintain a only a small buffer of audio to preserve audio quality, resulting in minimal added latency.",
warn: (user, stats) -> "JamKazam has to maintain a significant buffer of audio to preserve audio quality, resulting in potentially noticeable additional latency.",
poor: (user, stats) -> "JamKazam has to maintain a large buffer of audio to preserve audio quality, resulting in noticeabley added latency.",
}
},
audio: {
framesize: {
good: (user, stats) -> "#{user.possessive} gear is reading and writing audio data at a very high rate, keeping gear-added latency low.",
warn: (user, stats) -> "#{user.possessive} gear is reading and writing audio at a average rate, causing a few milliseconds extra latency compared to a 2.5 Frame Size.",
poor: (user, stats) -> "#{user.possessive} gear is reading and writing audio at a slow rate, causing a decent amount of latency before the internet is involved.",
},
latency: {
good: (user, stats) -> "#{user.possessive} gear has a small amount of latency.",
warn: (user, stats) -> "#{user.possessive} gear has a significant amount of latency.",
poor: (user, stats) -> "#{user.possessive} gear has a large amount of latency, making it difficult to play in time."
},
input_jitter: {
good: (user, stats) -> "#{user.possessive} gear has a small amount of input jitter, meaning it is keeping good time with JamKazam as it reads in your input signal.",
warn: (user, stats) -> "#{user.possessive} gear has a significant amount of input jitter, meaning it might be periodically adding delay or audio artifacts to your inputs.",
poor: (user, stats) -> "#{user.possessive} gear has a large amount of input jitter, meaning it likely adding delay and audio artifacts to your inputs.",
},
output_jitter: {
good: (user, stats) -> "#{user.possessive} gear has a small amount of output jitter, meaning it is keeping good time with your JamKazam as writes out your audio output.",
warn: (user, stats) -> "#{user.possessive} gear has a significant amount of output jitter, meaning it might be periodically adding delay and audio artifacts to your output.",
poor: (user, stats) -> "#{user.possessive} gear has a large amount of output jitter, meaning it likely adding delay and audio artifacts to your output.",
},
audio_in_type: {
good: (user, stats) -> "#{user.name} using an ideal driver type for #{user.possessive.toLowerCase()} gear.",
warn: (user, stats) -> "#{user.name} using a problematic driver type for #{user.possessive.toLowerCase()} gear.",
poor: (user, stats) -> "#{user.name} using a driver type considered problematic.",
}
}
}
@SessionStatsHover = React.createClass({
propTypes: {
clientId: React.PropTypes.string
}
mixins: [Reflux.listenTo(@SessionStatsStore, "onStatsChanged")]
hover: (type, field) ->
logger.debug("hover! #{type} #{field}")
@setState({hoverType: type, hoverField:field})
hoverOut: () ->
logger.debug("hover out!")
@setState({hoverType: null, hoverField: null})
stat: (properties, type, name, field, value) ->
classes = {'status-icon': true}
classifier = properties[field + '_level']
classes[classifier] = true
`<div className='stat' onMouseOver={this.hover.bind(this, type, field)}><span className="title">- {name}</span><div className={classNames(classes)}></div><span className="stat-value">{value}</span></div>`
render: () ->
extraInfo = 'Hover over a stat to learn more.'
if @state.hoverType?
type = @state.hoverType
field = @state.hoverField
extraInfo = 'No extra info for this metric.'
classifier = @state.stats?[type]?[field + '_level']
if classifier?
info = StatsInfo[type]?[field]?[classifier](@props.participant.user, @state.stats)
if info?
extraInfo = info
# "Windows WDM-KS"
computerStats = []
networkStats = []
audioStats = []
network = @state.stats?.network
system = @state.stats?.system
audio = @state.stats?.audio
if system?
if system.cpu?
computerStats.push(@stat(system, 'system', 'Processor', 'cpu', Math.round(system.cpu) + ' %'))
if audio?
if audio.audio_in_type?
audio_type = '?'
audio_long = audio.audio_in_type.toLowerCase()
if audio_long.indexOf('asio') > -1
audio_type = 'ASIO'
else if audio_long.indexOf('wdm') > -1
audio_type = 'WDM'
else if audio_long.indexOf('core') > -1
audio_type = 'CoreAudio'
audioStats.push(@stat(audio, 'audio', 'Gear Driver', 'audio_in_type', audio_type))
if audio.framesize?
framesize = '?'
if audio.framesize == 2.5
framesize = '2.5'
else if audio.framesize == 5
framesize = '5'
else if audio.framesize == 10
framesize = '10'
audioStats.push(@stat(audio, 'audio', 'Frame Size', 'framesize', framesize))
if audio.latency?
audioStats.push(@stat(audio, 'audio', 'Latency', 'latency', audio.latency.toFixed(1) + ' ms'))
if audio.input_jitter?
audioStats.push(@stat(audio, 'audio', 'Input Jitter', 'input_jitter', audio.input_jitter.toFixed(2)))
if audio.output_jitter?
audioStats.push(@stat(audio, 'audio', 'Output Jitter', 'output_jitter', audio.output_jitter.toFixed(2)))
networkTag = null
if network?
if network.net_bitrate?
networkStats.push(@stat(network, 'network', 'Bandwidth', 'net_bitrate', Math.round(network.net_bitrate) + ' k'))
if network.ping?
networkStats.push(@stat(network, 'network', 'Latency', 'ping', (network.ping / 2).toFixed(1) + ' ms'))
#if network.jitter?
if network.audiojq_median?
networkStats.push(@stat(network, 'network', 'Jitter Queue', 'audiojq_median', network.audiojq_median.toFixed(1)))
# networkStats.
if network.pkt_loss?
networkStats.push(@stat(network, 'network', 'Packet Loss', 'pkt_loss', network.pkt_loss.toFixed(1) + ' %'))
if network.wifi?
if network.wifi
value = 'Wi-Fi'
else
value = 'Ethernet'
networkStats.push(@stat(network, 'network', 'Connectivity', 'wifi', value))
networkTag =
`<div className="network-stats stats-holder">
<h3>Internet</h3>
{networkStats}
</div>`
`<div className="stats-hover">
<h3>Session Diagnostics &amp; Stats: {this.props.participant.user.name}</h3>
<div className="stats-area">
<div className="computer-stats stats-holder">
<h3>Computer</h3>
{computerStats}
</div>
<div className="audio-stats stats-holder">
<h3>Audio Interface</h3>
{audioStats}
</div>
{networkTag}
</div>
<div className="stats-info">
{extraInfo}
</div>
</div>`
onStatsChanged: (stats) ->
stats = window.SessionStatsStore.stats
if stats?
clientStats = stats[@props.participant.client_id]
else
clientStats = null
@setState({stats: clientStats})
getInitialState: () ->
stats = window.SessionStatsStore.stats
if stats?
clientStats = stats[@props.participant.client_id]
else
clientStats = null
{stats: clientStats, hoverType: null, hoverField: null}
closeHover: (e) ->
e.preventDefault()
$container = $(this.getDOMNode()).closest('.react-holder')
$container.data('bt').btOff()
})

View File

@ -19,4 +19,5 @@ context = window
associateVSTWithTrack: {}
associateMIDIWithTrack: {}
desiredTrackType: {}
vstChanged: {}
})

View File

@ -0,0 +1,5 @@
context = window
@SessionStatsActions = Reflux.createActions({
pushStats: {}
})

View File

@ -4,9 +4,21 @@ context = window
onInputsChanged: (sessionMixers) ->
@sessionMixers = sessionMixers
session = sessionMixers.session
mixers = sessionMixers.mixers
@recompute()
onConfigureTracksChanged: (configureTracks) ->
@configureTracks = configureTracks
@recompute()
recompute: () ->
return if !@sessionMixers?
session = @sessionMixers.session
mixers = @sessionMixers.mixers
tracks = []
@ -38,11 +50,30 @@ context = window
instrumentIcon = context.JK.getInstrumentIcon45(track.instrument_id);
trackName = "#{name}: #{track.instrument}"
tracks.push({track: track, mixerFinder: mixerFinder, mixers: mixerData, hasMixer:hasMixer, name: name, trackName: trackName, instrumentIcon: instrumentIcon, photoUrl: photoUrl, clientId: participant.client_id})
associatedVst = null
# find any VST info
if hasMixer && @configureTracks?
# bug in the backend; track is wrong for personal mixers (always 1), but correct for master mix
trackAssignment = -1
if @props.mode == context.JK.MIX_MODES.MASTER
trackAssignment = mixerData.mixer.track
else
trackAssignment = mixerData.oppositeMixer?.track
console.log("checking associations", @configureTracks.vstTrackAssignments.vsts, mixerData.mixer)
for vst in @configureTracks.vstTrackAssignments.vsts
if vst.track == trackAssignment - 1 && vst.name != 'NONE'
logger.debug("found VST on track", vst, track)
associatedVst = vst
break
tracks.push({track: track, mixerFinder: mixerFinder, mixers: mixerData, hasMixer:hasMixer, name: name, trackName: trackName, instrumentIcon: instrumentIcon, photoUrl: photoUrl, clientId: participant.client_id, associatedVst: associatedVst})
else
logger.warn("SessionMyTracks: unable to find participant")
this.setState(tracks: tracks, session:session, chat: chat)
}

View File

@ -0,0 +1,439 @@
$ = jQuery
context = window
logger = context.JK.logger
ASSIGNMENT = context.JK.ASSIGNMENT
VOICE_CHAT = context.JK.VOICE_CHAT
MAX_TRACKS = context.JK.MAX_TRACKS
MAX_OUTPUTS = context.JK.MAX_OUTPUTS
gearUtils = context.JK.GearUtils
###
QVariantMap scanForPlugins();
QVariantMap VSTListVsts();
void VSTClearAll();
QVariantMap VSTSetTrackAssignment(const QVariantMap vst, const QString& trackId);
QVariantMap VSTListTrackAssignments();
void VSTShowHideGui(bool show,const QString& trackId);
void VST_ScanForMidiDevices();
QVariantMap VST_GetMidiDeviceList();
bool VST_EnableMidiForTrack(const QString& trackId, bool enableMidi, int midiDeviceIndex);
###
@ConfigureTracksStore = Reflux.createStore(
{
listenables: ConfigureTracksActions
musicPorts: {inputs: [], outputs: []}
trackNumber: null
editingTrack: null
vstPluginList: {vsts: []}
vstTrackAssignments: {vsts: []}
attachedMidiDevices: {midiDevices: []}
midiTrackAssignments: {tracks: []}
scanningVsts: false
trackType: 'audio'
init: () ->
this.listenTo(context.AppStore, this.onAppInit)
this.listenTo(context.MixerStore, this.onMixersChanged)
onAppInit: (@app) ->
editingTrackValid: () ->
true
onMixersChanged: (mixers) ->
@loadChannels()
@loadTrackInstruments()
@changed()
onReset: (force) ->
logger.debug("ConfigureTracksStore:reset", this)
@trackNumber = null
@editingTrack = null
@loadChannels()
@loadTrackInstruments()
if force || context.jamClient.hasVstAssignment()
@performVstScan()
@performMidiScan()
@changed()
onTrySave: () ->
logger.debug("ConfigureTracksStore:trySave")
@trySave()
trySave: () ->
onVstScan: () ->
@performVstScan(true)
@changed()
performVstScan: (sendChanged) ->
@hasVst = gon.global.vst_enabled & context.jamClient.hasVstHost()
logger.debug("hasVst", @hasVst)
if @hasVst
logger.debug("vstScan starting")
@scanningVsts = true
result = context.jamClient.VSTScan("window.ConfigureTracksStore.onVstScanComplete")
onClearVsts: () ->
context.jamClient.VSTClearAll()
setTimeout((() =>
@listVsts()
@changed()
), 250)
onVstScanComplete: () ->
# XXX must wait a long time to get track assignments after scan/
console.log("vst scan complete")
@scanningVsts = false
setTimeout((() =>
@listVsts()
@changed()
), 100 )
onVstChanged: () ->
setTimeout()
logger.debug("vst changed")
setTimeout((() =>
@listVsts()
@changed()
), 0)
listVsts: () ->
@vstPluginList = context.jamClient.VSTListVsts()
@vstTrackAssignments = context.jamClient.VSTListTrackAssignments()
console.log("@vstTrackAssignments", @vstTrackAssignments)
onMidiScan: () ->
@performMidiScan()
@changed()
performMidiScan: () ->
if !@hasVst
logger.debug("performMidiScan skipped due to no VST")
return
context.jamClient.VST_ScanForMidiDevices();
@attachedMidiDevices = context.jamClient.VST_GetMidiDeviceList();
# trackNumber is 0-based, and optional
onShowVstSettings: (trackNumber) ->
if !@hasVst
logger.debug("onShowVstSettings skipped due to no VST")
return
if !trackNumber?
trackNumber = @trackNumber - 1 if @trackNumber?
logger.debug("show VST GUI", trackNumber)
context.jamClient.VSTShowHideGui(true, trackNumber) if trackNumber?
changed: () ->
@editingTrack = []
@editingTrack.assignment = @trackNumber
if @trackNumber?
for inputsForTrack in @trackAssignments.inputs.assigned
if inputsForTrack.assignment == @trackNumber
@editingTrack = inputsForTrack
break
# slap on vst, if any, from list of vst assignments
for vst in @vstTrackAssignments.vsts
if vst.track == @editingTrack.assignment - 1
@editingTrack.vst = vst
@editingTrack.midiDeviceIndex = vst.midiDeviceIndex
break
for inputsForTrack in @trackAssignments.inputs.assigned
if vst.track == inputsForTrack.assignment - 1
inputsForTrack.vst = vst
if @editingTrack.vst?
logger.debug("current track has a VST assigned:" + @editingTrack.vst.file)
logger.debug("trackAssignments:", @trackAssignments)
logger.debug("editingTrack:", @editingTrack)
@item = {
musicPorts: @musicPorts,
trackAssignments: @trackAssignments,
trackNumber: @trackNumber,
editingTrack: @editingTrack,
vstPluginList: @vstPluginList,
vstTrackAssignments: @vstTrackAssignments,
attachedMidiDevices: @attachedMidiDevices,
nextTrackNumber: @nextTrackNumber,
newTrack: @newTrack,
midiTrackAssignments: @midiTrackAssignments,
scanningVsts: @scanningVsts,
trackType: @trackType
}
@trigger(@item)
loadChannels: (forceInputsToUnassign, inputChannelFilter) ->
# inputChannelFilter is an optional argument that is used by the Gear Wizard.
# basically, if an input channel isn't in there, it's not going to be displayed
@musicPorts = context.jamClient.FTUEGetChannels()
# let's populate this bad boy
@trackAssignments = {inputs: {unassigned: [], assigned: [], chat: []}, outputs: {unassigned: [], assigned: []}}
nextTrackNumber = 0
for input in @musicPorts.inputs
if input.assignment == ASSIGNMENT.UNASSIGNED
@trackAssignments.inputs.unassigned.push(input)
else if input.assignment == ASSIGNMENT.CHAT
@trackAssignments.inputs.chat.push(input)
else
nextTrackNumber = input.assignment if input.assignment > nextTrackNumber
# make sure this assignment isn't already preset (you can have multiple inputs per 'track slot')
found = false
for assigned in @trackAssignments.inputs.assigned
if assigned.assignment == input.assignment
assigned.push(input)
found = true
if !found
initial = [input]
initial.assignment = input.assignment # store the assignment on the array itself, so we don't have to check inside the array for an input's assignment (which will all be the same)
@trackAssignments.inputs.assigned.push(initial)
for output in @musicPorts.outputs
if output.assignment == ASSIGNMENT.OUTPUT
@trackAssignments.outputs.assigned.push(output)
else
@trackAssignments.outputs.unassigned.push(output)
@nextTrackNumber = nextTrackNumber + 1
loadTrackInstruments: (forceInputsToUnassign) ->
for inputsForTrack in @trackAssignments.inputs.assigned
clientInstrument = context.jamClient.TrackGetInstrument(inputsForTrack.assignment)
if clientInstrument == 0
logger.debug("defaulting track instrument for assignment #{@trackNumber}")
# ensure that we always have an instrument set (50 = electric guitar
context.jamClient.TrackSetInstrument(inputsForTrack.assignment, 50)
clientInstrument = 50
instrument = context.JK.client_to_server_instrument_map[clientInstrument];
inputsForTrack.instrument_id = instrument?.server_id
onAssociateInputsWithTrack: (inputId1, inputId2) ->
return unless @trackNumber?
for inputs in @editingTrack
context.jamClient.TrackSetAssignment(inputs.id, true, ASSIGNMENT.UNASSIGNED)
if inputId1?
logger.debug("setting input1 #{inputId1} to #{@trackNumber}")
context.jamClient.TrackSetAssignment(inputId1, true, @trackNumber)
if inputId2?
logger.debug("setting input2 #{inputId2} to #{@trackNumber}")
context.jamClient.TrackSetAssignment(inputId2, true, @trackNumber)
result = context.jamClient.TrackSaveAssignments();
if(!result || result.length == 0)
else
context.JK.Banner.showAlert('Unable to save assignments. ' + result);
onAssociateInstrumentWithTrack: (instrumentId) ->
return unless @trackNumber?
logger.debug("context.jamClient.TrackSetInstrument(trackNumber, track.instrument_id)", @trackNumber, instrumentId)
if instrumentId != null && instrumentId != ''
context.jamClient.TrackSetInstrument(@trackNumber, context.JK.instrument_id_to_instrument[instrumentId].client_id)
else
context.jamClient.TrackSetInstrument(@trackNumber, 0)
if(!result || result.length == 0)
else
context.JK.Banner.showAlert('Unable to save assignments. ' + result);
result = context.jamClient.TrackSaveAssignments()
if(!result || result.length == 0)
else
context.JK.Banner.showAlert('Unable to save assignments. ' + result);
onAssociateVSTWithTrack: (vst) ->
if !@hasVst
logger.debug("onAssociateVSTWithTrack skipped due to no VST")
return
if vst?
logger.debug("associating track:#{@trackNumber - 1} with VST:#{vst.file}")
found = null
for knownVst in @vstPluginList.vsts
if knownVst.file == vst.file
found = knownVst
break
if found?
context.jamClient.VSTSetTrackAssignment(found, @trackNumber - 1)
else
logger.error("unable to locate vst for #{vst}")
else
logger.debug("unassociated track:#{@trackNumber} with VST")
# no way to unset VST assignment yet
setTimeout((() => (
@listVsts()
@changed()
)), 250)
onCancelEdit: () ->
if @newTrack
for input in @editingTrack
context.jamClient.TrackSetAssignment(input.id, true, ASSIGNMENT.UNASSIGNED)
result = context.jamClient.TrackSaveAssignments()
if(!result || result.length == 0)
else
context.JK.Banner.showAlert('Unable to save assignments. ' + result);
else
logger.error("unable to process cancel for an existing track")
onDeleteTrack: (assignment) ->
track = null
for inputsForTrack in @trackAssignments.inputs.assigned
if inputsForTrack.assignment == assignment
track = inputsForTrack
break
if track?
for input in inputsForTrack
context.jamClient.TrackSetAssignment(input.id, true, ASSIGNMENT.UNASSIGNED)
result = context.jamClient.TrackSaveAssignments()
if(!result || result.length == 0)
else
context.JK.Banner.showAlert('Unable to save assignments. ' + result);
else
logger.error("unable to find track to delete")
onShowAddNewTrack: () ->
# check if we have what we need... namely, free ports
if @trackAssignments.inputs.unassigned.length == 0
context.JK.Banner.showAlert('You have no more unassigned input ports.<br/><br/>You can free some up by editing an AUDIO track.')
return
@openLiveTrackDialog(@nextTrackNumber)
onShowEditTrack: (trackNumber) ->
@openLiveTrackDialog(trackNumber)
openLiveTrackDialog: (trackNumber) ->
@trackNumber = trackNumber
logger.debug("opening live track dialog for track #{trackNumber}")
@newTrack = true
for inputsForTrack in @trackAssignments.inputs.assigned
logger.debug("inputsForTrack.assignment @trackNumber", inputsForTrack.assignment, @trackNumber )
if inputsForTrack.assignment == @trackNumber
@newTrack = false
break
if @newTrack
@trackType = 'audio'
else
if @trackNumber == 1
@trackType = 'audio'
else
@trackType = 'audio'
for trackAssignment in @vstTrackAssignments.vsts
if trackAssignment.track == @trackNumber - 1
if trackAssignment.midiDeviceIndex > -1
logger.debug("editing midi track")
@trackType = 'midi'
break
if @newTrack
assignment = context.jamClient.TrackGetInstrument(@trackNumber)
if assignment == 0
logger.debug("defaulting track instrument for assignment #{@trackNumber}")
# ensure that we always have an instrument set (50 = electric guitar
context.jamClient.TrackSetInstrument(@trackNumber, 50)
@performVstScan()
@performMidiScan()
@changed()
@app.layout.showDialog('configure-live-tracks-dialog')
onDesiredTrackType: (trackType) ->
@trackType = trackType
if @trackType == 'midi'
@trackNumber = 100
@changed()
onUpdateOutputs: (outputId1, outputId2) ->
context.jamClient.TrackSetAssignment(outputId1, true, ASSIGNMENT.OUTPUT);
context.jamClient.TrackSetAssignment(outputId2, true, ASSIGNMENT.OUTPUT);
result = context.jamClient.TrackSaveAssignments();
if(!result || result.length == 0)
else
context.JK.Banner.showAlert('Unable to save assignments. ' + result);
onShowEditOutputs: () ->
@app.layout.showDialog('configure-outputs-dialog')
onAssociateMIDIWithTrack: (midiInterface) ->
@trackNumber = 100
if !midiInterface? || midiInterface == ''
logger.debug("disabling midiInterface:#{midiInterface}, track:#{@trackNumber - 1}")
context.jamClient.VST_EnableMidiForTrack(@trackNumber - 1, false, 0)
else
logger.debug("enabling midiInterface:#{midiInterface}, track:#{@trackNumber - 1}")
context.jamClient.VST_EnableMidiForTrack(@trackNumber - 1, true, midiInterface)
setTimeout((() => (
@listVsts()
@changed()
)), 250)
}
)

View File

@ -0,0 +1,137 @@
$ = jQuery
context = window
logger = context.JK.logger
rest = context.JK.Rest()
EVENTS = context.JK.EVENTS
MIX_MODES = context.JK.MIX_MODES
SessionActions = @SessionActions
SessionStatThresholds = gon.session_stat_thresholds
NetworkThresholds = SessionStatThresholds.network
SystemThresholds = SessionStatThresholds.system
AudioThresholds = SessionStatThresholds.audio
@SessionStatsStore = Reflux.createStore(
{
listenables: @SessionStatsActions
rawStats: null
onPushStats: (stats) ->
@rawStats = stats
@changed()
classify: (holder, field, threshold) ->
value = holder[field]
fieldLevel = field + '_level'
fieldThreshold = threshold[field]
if value? && fieldThreshold?
if fieldThreshold.inverse
if value <= fieldThreshold.poor
holder[fieldLevel] = 'poor'
@participantClassification = 3
else if value <= fieldThreshold.warn
holder[fieldLevel] = 'warn'
@participantClassification = 2 if @participantClassification == 1
else
holder[fieldLevel] = 'good'
else if fieldThreshold.eql
if value == fieldThreshold.poor
holder[fieldLevel] = 'poor'
@participantClassification = 3
else if value == fieldThreshold.warn
holder[fieldLevel] = 'warn'
@participantClassification = 2 if @participantClassification == 1
else
holder[fieldLevel] = 'good'
else
if value >= fieldThreshold.poor
holder[fieldLevel] = 'poor'
@participantClassification = 3
else if value >= fieldThreshold.warn
holder[fieldLevel] = 'warn'
@participantClassification = 2 if @participantClassification == 1
else
holder[fieldLevel] = 'good'
changed: () ->
@stats = {}
console.log("raw stats", @rawStats)
for participant in @rawStats
@participantClassification = 1 # 1=good, 2=warn, 3=poor
# CPU is 0-100
if participant.cpu?
system = {cpu: participant.cpu}
@classify(system, 'cpu', SystemThresholds)
participant.system = system
network = participant.network
if network?
# audio_bitrate: 256
# net_bitrate: 286.19244384765625
# ping: 0.08024691045284271
# ping_var: 0.6403124332427979
# pkt_loss: 100
# wifi: false
@classify(network, 'audiojq_median', NetworkThresholds)
@classify(network, 'net_bitrate', NetworkThresholds)
@classify(network, 'ping', NetworkThresholds)
@classify(network, 'pkt_loss', NetworkThresholds)
@classify(network, 'wifi', NetworkThresholds)
audio = participant.audio
if audio?
# acpu: 5.148329734802246
# audio_in: "Fast Track"
# audio_in_type: "Core Audio"
# cpu: 22.44668960571289
# framesize: 2.5
# in_latency: 5.020833492279053
# input_iio_jitter: -0.0015926361083984375
# input_jitter: 0.2977011799812317
# input_median: 400.16632080078125
# io_out_latency: "Expected Latency = 9.54 +/- 1.00 ms [Raw/PaBuff/PaRing Latency: 9.54 / 12.04 / 0.00 ms]"
# out_latency: 4.520833492279053
# output_iio_jitter: -0.07366180419921875
# output_jitter: 0.40290364623069763
# output_median: 400.0581970214844
# output_name: 4
# samplerate: 48000
if audio.cpu?
system = {cpu: audio.cpu}
@classify(system, 'cpu', SystemThresholds)
participant.system = system
if audio.in_latency? and audio.out_latency?
audio.latency = audio.in_latency + audio.out_latency
@classify(audio, 'framesize', AudioThresholds)
@classify(audio, 'latency', AudioThresholds)
@classify(audio, 'input_jitter', AudioThresholds)
@classify(audio, 'output_jitter', AudioThresholds)
@classify(audio, 'audio_in_type', AudioThresholds)
switch @participantClassification
when 1 then participant.classification = 'good'
when 2 then participant.classification = 'warn'
when 3 then participant.classification = 'poor'
else
participant.classification = 'unknown'
@stats[participant.id] = participant
@trigger(@stats)
}
)

View File

@ -10,6 +10,7 @@ SessionActions = @SessionActions
RecordingActions = @RecordingActions
NotificationActions = @NotificationActions
VideoActions = @VideoActions
ConfigureTracksActions = @ConfigureTracksActions
@SessionStore = Reflux.createStore(
{
@ -731,6 +732,9 @@ VideoActions = @VideoActions
$(document).trigger(EVENTS.SESSION_STARTED, {session: {id: @currentSessionId}}) if document
@handleAutoOpenJamTrack()
@watchBackendStats()
ConfigureTracksActions.reset(false)
)
.fail((xhr) =>
@updateCurrentSession(null)
@ -762,6 +766,13 @@ VideoActions = @VideoActions
@app.notifyServerError(xhr, 'Unable to Join Session');
)
watchBackendStats: () ->
@backendStatsInterval = window.setInterval((() => (@updateBackendStats())), 1000)
updateBackendStats: () ->
connectionStats = window.jamClient.getConnectionDetail('')
SessionStatsActions.pushStats(connectionStats)
trackChanges: (header, payload) ->
if @currentTrackChanges < payload.track_changes_counter
# we don't have the latest info. try and go get it
@ -1050,6 +1061,10 @@ VideoActions = @VideoActions
@userTracks = null;
@startTime = null;
if @backendStatsInterval?
window.clearInterval(@backendStatsInterval)
@backendStatsInterval = null
if @joinDeferred?.state() == 'resolved'
$(document).trigger(EVENTS.SESSION_ENDED, {session: {id: @currentSessionId}})

View File

@ -284,7 +284,8 @@
options.cssStyles = {}
options.padding = 0;
context.JK.hoverBubble($element, '<div class="react-holder ' + reactElementName + '"></div>', options)
var extra = options.extraClasses || ''
context.JK.hoverBubble($element, '<div class="react-holder ' + reactElementName + ' ' + extra + '"></div>', options)
return $element;
}
@ -733,6 +734,10 @@
return date.toLocaleTimeString();
}
context.JK.iconMapBase = function() {
return icon_map_base
}
context.JK.formatUtcTime = function(date, change) {
if (change) {
date.setMinutes(Math.ceil(date.getMinutes() / 30) * 30);

View File

@ -21,14 +21,14 @@
}
function handleNext() {
var saved = configureTracksHelper.trySave();
/** var saved = configureTracksHelper.trySave();
if(saved) {
context.JK.GA.trackConfigureTracksCompletion(context.JK.detectOS());
successfullyAssignedOnce = true;
}
return saved;
*/
return context.ConfigureTracksStore.editingTrackValid()
}
function newSession() {
@ -38,7 +38,8 @@
function beforeShow() {
var forceInputsToUnassigned = !successfullyAssignedOnce;
configureTracksHelper.reset(forceInputsToUnassigned, wizard.getChosenInputs())
window.ConfigureTracksActions.reset(false);
//configureTracksHelper.reset(forceInputsToUnassigned, wizard.getChosenInputs())
}
function initialize(_$step, _wizard) {

View File

@ -53,6 +53,8 @@
*= require dialogs/dialog
*= require ./iconInstrumentSelect
*= require ./muteSelect
*= require ./manageVsts
*= require ./vstEffects
*= require ./metronomePlaybackModeSelect
*= require ./terms
*= require ./createSession

View File

@ -0,0 +1,23 @@
@import "client/common";
.manage-vsts-popup {
.bt-content {
height:38px;
width:190px;
background-color:#333;
overflow:auto;
border:1px solid #ED3618;
text-align:left;
font-family: 'Raleway', Arial, Helvetica, sans-serif;
ul {
@include vertical-align-column;
height:100%;
margin-left: 0 !important;
}
li {
font-size:12px;
margin-left:0 !important;
list-style-type: none;
}
}
}

View File

@ -0,0 +1,170 @@
@import "client/common";
.ConfigureTracks {
.inputs-view {
border-width:1px 0;
border-color:$ColorText;
border-style:solid;
padding:20px 0;
height:220px;
.live-tracks {
height:165px;
overflow:auto;
a {
margin-left:3px;
}
}
.live-input {
display:inline-block;
&:before {
content: '('
}
&:after {
content: ')'
}
}
.live-track {
margin-bottom:20px;
}
a.delete-live-track {
margin-left:20px;
}
.assignment {
display:inline-block;
width:20px;
}
.input-track-info {
@include border_box_sizing;
width:60%;
display:inline-block;
&.one {
.live-input {
width:50%;
@include border_box_sizing;
}
}
&.two {
.live-input {
max-width:40%;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
@include border_box_sizing;
&:nth-of-type(1) {
padding-right:5px;
}
&:nth-of-type(2) {
padding-left:5px;
}
}
}
}
.track-type-label {
display:inline-block;
white-space:nowrap;
padding-right:7px;
}
.plugin-info {
@include border_box_sizing;
width:30%;
display:inline-block;
white-space:nowrap;
text-overflow: ellipsis;
overflow:hidden;
}
.plugin-instrument {
@include border_box_sizing;
width:10%;
display:inline-block;
text-align:center;
img {
vertical-align:middle;
}
}
}
.outputs-view {
border-width:0 0 1px;
border-color:$ColorText;
border-style:solid;
padding:20px 0;
.assignment {
display:inline-block;
width:20px;
}
.output-tracks {
height:73px;
overflow:auto;
a {
margin-left:3px;
}
}
.output-track {
margin-bottom: 20px;
}
.output-track-info {
display:inline-block;
}
.output {
display:inline-block;
margin-bottom:0 !important;
&:before {
content: '('
}
&:after {
content: ')'
}
}
}
h3 {
display:inline-block;
font-size:14px;
margin:0 0 20px;
@include border_box_sizing;
&.session-audio-inputs-header {
color:white;
font-weight:bold;
width:60%;
}
&.session-audio-outputs-header {
color:white;
font-weight:bold;
width:60%;
}
&.plugin-header {
width:30%;
}
&.instrument-header {
width:10%;
text-align:center;
}
}
.live-track-actions {
display:block;
padding-left: 17px;
margin-top: 5px;
a {
font-size:12px;
}
}
}

View File

@ -6,6 +6,7 @@ $session-screen-divider: 1190px;
@content;
}
}
@mixin session-normal {
@media (min-width: #{$session-screen-divider}) {
@content;
@ -13,97 +14,171 @@ $session-screen-divider: 1190px;
}
#session-screen {
.session-container {
overflow-x: hidden;
.session-container {
overflow-x: hidden;
}
.session-track {
@include session-small {
max-width: 120px;
}
.session-track {
&.metronome, &.jam-track, &.recorded-track, &.backing-track {
@include session-small {
max-width: 120px;
height: auto;
}
&.metronome, &.jam-track, &.recorded-track, &.backing-track {
.track-icon-pan {
@include session-small {
height:auto;
}
.track-icon-pan {
@include session-small {
margin-right:0;
}
}
.track-buttons {
@include session-small {
margin:12px 0 0;
}
}
table.vu {
@include session-small {
margin-top:5px;
}
}
.track-controls {
@include session-small {
margin-right:8px;
}
}
.track-icon-pan {
@include session-small {
margin-right:2px;
}
}
.track-instrument {
@include session-small {
margin: -4px 12px 0 0;
}
margin-right: 0;
}
}
&.jam-track-category, &.recorded-category {
.track-controls {
@include session-small {
margin-top:5px;
margin-right:8px;
}
.track-buttons {
@include session-small {
margin: 12px 0 0;
}
table.vu {
@include session-small {
margin-top:0;
}
}
table.vu {
@include session-small {
margin-top: 5px;
}
.jam-track-header {
@include session-small {
float:left;
}
}
.track-controls {
@include session-small {
margin-right: 8px;
}
.name {
@include session-small {
float:left;
}
}
.track-icon-pan {
@include session-small {
margin-right: 2px;
}
.track-buttons {
@include session-small {
margin-top:12px;
}
}
.track-instrument {
@include session-small {
margin: -4px 12px 0 0;
}
}
}
&.jam-track-category, &.recorded-category {
.track-controls {
@include session-small {
margin-top: 5px;
margin-right: 8px;
}
}
table.vu {
@include session-small {
margin-top: 0;
}
}
.jam-track-header {
@include session-small {
float: left;
}
}
.name {
@include session-small {
float: left;
}
}
.track-buttons {
@include session-small {
margin-top: 12px;
}
}
}
}
.track-controls {
@include session-small {
margin-top:8px;
.track-controls {
@include session-small {
margin-top: 8px;
}
}
.track-buttons {
@include session-small {
margin-left: 14px;
}
}
.self.SessionStatsHover .stats-info {
height:200px;
}
.stats-info {
float: left;
width: 120px;
border-radius: 15px;
border-color: #fc0;
border-width: 1px;
border-style: solid;
padding:10px;
height: 341px;
margin-top: 20px;
}
.stats-area {
float: left;
padding: 0 20px 20px 20px;
color: #cccccc;
h3 {
color: white;
}
.stat {
margin: 7px 0;
}
.title {
display: inline-block;
width: 100px;
line-height: 20px;
height: 20px;
vertical-align: middle;
}
.stats-holder {
margin-top: 20px;
h3 {
margin: 0 0 10px 0;
}
}
.status-icon {
border-radius: 10px;
width: 20px;
height: 20px;
margin-right: 10px;
display: inline-block;
vertical-align: middle;
&.poor {
background-color: $poor;
}
&.warn {
background-color: $fair;
}
&.unknown {
background-color: $unknown;
}
&.good {
background-color: $good;
}
}
.track-buttons {
@include session-small {
margin-left:14px;
}
.stat-value {
width: 50px;
display: inline-block;
height: 20px;
vertical-align: middle;
line-height: 20px;
margin-right:6px;
}
h2 {
}
h2 {
color: #fff;
font-weight: 600;
font-size: 24px;
@ -125,18 +200,18 @@ $session-screen-divider: 1190px;
padding: 10px;
height: 100%;
margin-bottom: 15px;
color:$ColorTextTypical;
overflow:hidden;
position:relative;
color: $ColorTextTypical;
overflow: hidden;
position: relative;
}
.session-media-tracks {
width:34%;
width: 34%;
}
.session-notifications {
border-right-width: 0;
display:none; //temp
display: none; //temp
}
.in-session-controls {
@ -162,20 +237,20 @@ $session-screen-divider: 1190px;
a {
img, .volume-icon {
vertical-align:top;
vertical-align: top;
margin-right: 4px;
}
span {
vertical-align:middle;
vertical-align: middle;
}
}
.button-grey {
margin:0 5px;
margin: 0 5px;
padding: 3px 7px;
&.session-leave {
margin-right:10px;
margin-right: 10px;
}
}
}
@ -192,15 +267,15 @@ $session-screen-divider: 1190px;
bottom: 0;
left: 0;
right: 0;
text-align:left;
text-align: left;
&.media-options-showing {
top:180px;
top: 180px;
}
border-right: 1px solid #4c4c4c;
margin-bottom:10px;
margin-top:10px;
margin-bottom: 10px;
margin-top: 10px;
}
p {
@ -209,57 +284,56 @@ $session-screen-divider: 1190px;
}
.download-jamtrack {
margin-top:20px;
margin-top: 20px;
}
.when-empty {
margin-top:25px;
margin-left:22px;
color:$ColorTextTypical;
overflow:hidden;
margin-top: 25px;
margin-left: 22px;
color: $ColorTextTypical;
overflow: hidden;
}
.session-track-settings {
height:20px;
cursor:pointer;
padding-bottom:1px; // to line up with SessionOtherTracks
color:$ColorTextTypical;
height: 20px;
cursor: pointer;
padding-bottom: 1px; // to line up with SessionOtherTracks
color: $ColorTextTypical;
&:hover {
color:white;
color: white;
}
span {
top: -5px;
position: relative;
left:3px;
left: 3px;
}
}
.session-invite-musicians {
height:20px;
height: 20px;
cursor: pointer;
color:$ColorTextTypical;
color: $ColorTextTypical;
&:hover {
color:white;
color: white;
}
span {
top:-5px;
position:relative;
left:3px;
top: -5px;
position: relative;
left: 3px;
}
}
.closeAudio, .session-clear-notifications {
cursor: pointer;
color:$ColorTextTypical;
height:20px;
color: $ColorTextTypical;
height: 20px;
img {
top:-2px
top: -2px
}
span {
top: -5px;
@ -268,115 +342,113 @@ $session-screen-divider: 1190px;
}
}
.open-media-file-header, .use-metronome-header {
font-size:14px;
line-height:100%;
margin:0;
font-size: 14px;
line-height: 100%;
margin: 0;
img {
position:relative;
top:3px;
position: relative;
top: 3px;
}
}
.open-media-file-header {
img {
vertical-align:middle;
vertical-align: middle;
}
.open-text {
margin-left:5px;
vertical-align:bottom;
margin-left: 5px;
vertical-align: bottom;
}
}
.use-metronome-header {
clear: both;
a {
color:$ColorTextTypical;
color: $ColorTextTypical;
&:hover {
text-decoration: underline;
color:white;
color: white;
}
}
}
.open-media-file-options {
font-size:14px;
font-size: 14px;
margin: 7px 0 0 7px !important;
color:$ColorTextTypical;
color: $ColorTextTypical;
li {
margin-bottom:5px !important;
margin-left:38px !important;
margin-bottom: 5px !important;
margin-left: 38px !important;
a {
text-decoration: none;
&:hover {
text-decoration: underline;
color:white;
color: white;
}
color:$ColorTextTypical;
color: $ColorTextTypical;
}
}
}
.open-metronome {
margin-left:5px;
margin-left: 5px;
}
.media-options {
padding-bottom:10px;
padding-bottom: 10px;
}
.session-mytracks-notracks p.notice {
font-size:14px;
font-size: 14px;
}
.session-notification {
color: white;
background-color: #666666;
border-radius: 6px;
min-height: 36px;
width:100%;
position:relative;
width: 100%;
position: relative;
@include border_box_sizing;
padding:6px;
margin:10px 0;
padding: 6px;
margin: 10px 0;
&.has-details {
cursor:pointer;
cursor: pointer;
}
.msg {
font-size:14px;
font-size: 14px;
}
.detail {
font-size:12px;
margin-top:5px;
font-size: 12px;
margin-top: 5px;
}
.notify-help {
color:#ffcc00;
color: #ffcc00;
text-decoration: none;
font-size:12px;
margin-left:5px;
font-size: 12px;
margin-left: 5px;
&:hover {
text-decoration:underline !important;
text-decoration: underline !important;
}
}
}
.close-window {
text-align:center;
clear:both;
text-align: center;
clear: both;
}
.session-volume-settings .volume-icon {
display:inline-block;
width:14px;
height:14px;
background-image:url('/assets/content/icon_mute_sm.png');
background-repeat:no-repeat;
display: inline-block;
width: 14px;
height: 14px;
background-image: url('/assets/content/icon_mute_sm.png');
background-repeat: no-repeat;
&.muted {
background-position: 0 0;

View File

@ -119,6 +119,29 @@
}
}
.track-connection-state {
width:20px;
height:20px;
float:left;
cursor:pointer;
text-align: center;
margin-left:10px;
border-radius:10px;
&.poor {
background-color:$poor;
}
&.warn {
background-color:$fair;
}
&.unknown {
background-color:$unknown;
}
&.good {
background-color:$good;
}
}
.track-icon-equalizer {
float:left;
cursor: pointer;
@ -134,6 +157,11 @@
}
}
&.my-track {
width:235px;
max-width:235px;
}
// media overrides
&.backing-track, &.recorded-track, &.jam-track, &.metronome, &.recorded-category, &.jam-track-category {
@ -357,6 +385,7 @@
}
}
&.SessionTrackVolumeHover {
.session-track {
margin-bottom:0;
@ -368,6 +397,22 @@
height:380px ! important;
}
&.SessionStatsHover {
width:380px;
height:450px;
@include border_box_sizing;
h3 {
margin-top:20px;
color: white;
margin-left:20px;
}
&.self {
height:290px;
}
}
&.SessionTrackPanHover {
width:331px;
height:197px;

View File

@ -0,0 +1,39 @@
@import "client/common";
.vst-effects-popup {
margin-top: 3px;
margin-left: 120px;
.bt-content {
width:220px;
background-color:#333;
overflow:auto;
border:1px solid #ED3618;
text-align:left;
font-family: 'Raleway', Arial, Helvetica, sans-serif;
ul {
@include vertical-align-column;
height:100%;
margin-left: 0 !important;
}
li {
font-size:12px;
margin-left:0 !important;
list-style-type: none;
margin-bottom:0 !important;
padding:7px 0 10px 0;
.vst-name {
text-overflow:ellipsis;
overflow:hidden;
}
&:nth-of-type(1) {
border-width:0 0 1px 0 !important;
border-style:solid !important;;
border-color:#ED3618 !important;;
}
}
}
}

View File

@ -197,7 +197,7 @@
width: 25%;
&:nth-of-type(2) {
width: 21%;
width: 71%;
}
&:nth-of-type(3) {
@ -217,9 +217,64 @@
margin-top: 45px;
}
.output-channels, .unassigned-output-channels {
.outputs-view {
display:none;
}
.inputs-view {
padding:0;
border-width:0;
h3.session-audio-inputs-header {
font-weight:normal;
width:70%;
font-size:14px;
color:white;
}
h3.plugin-header {
width:20%;
font-size:14px;
color:white;
}
h3.instrument-header {
width:10%;
font-size:14px;
color:white;
}
.input-track-info {
width:70%;
}
.plugin-info {
width:20%;
}
.plugin-instrument {
width:10%;
}
.live-tracks {
height:198px;
}
.live-input {
display:block;
max-width:90%;
&:before {
content: ''
}
&:after {
content: ''
}
}
.input-track-info .live-input {
padding-left:19px;
padding-right:0;
}
.add-track-action { text-align:center;}
}
}
.wizard-step[layout-wizard-step="3"] {

View File

@ -0,0 +1,184 @@
@import "client/common";
#configure-live-tracks-dialog {
width: 800px;
.dialog-inner {
width:auto;
}
h3 {
color:white;
font-weight:bold;
margin-bottom:10px;
}
.manage-audio-plugins {
font-size:12px;
}
.actions {
clear:both;
text-align:center;
}
.track-type {
width:100%;
@include border_box_sizing;
padding:10px;
.track-type-option {
margin-bottom:10px;
}
label {
vertical-align:middle;
display:inline-block;
margin-left:10px;
}
.iradio_minimal {
vertical-align:middle;
display:inline-block;
}
}
.audio-input-ports {
width:60%;
@include border_box_sizing;
float:left;
margin-bottom:40px;
padding:10px;
select {
width: 90%;
@include border_box_sizing;
margin-bottom:10px;
}
p {
margin-bottom:10px;
line-height:125%;
}
}
.audio {
.instrument-selection {
width:40%;
@include border_box_sizing;
float:left;
margin-bottom:26px;
padding:10px;
select {
width:90%;
}
}
}
.midi-interface {
@include border_box_sizing;
float:left;
margin-bottom:20px;
padding:10px;
position:relative;
width:50%;
select {
width:80%;
margin-bottom:10px;
}
a {
font-size:12px;
}
}
.midi {
.instrument-selection {
width:50%;
@include border_box_sizing;
float:left;
margin-bottom:26px;
padding:10px;
select {
width:80%;
}
}
}
.midi-instrument {
@include border_box_sizing;
float:left;
margin-bottom:20px;
padding:10px;
position:relative;
width:50%;
clear:both;
select {
width:80%;
margin-bottom:10px;
}
.down-arrow {
cursor:pointer;
width: 0;
height: 0;
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-top: 8px solid #fc0;
position: relative;
top: -8px;
right: -125px;
}
.settings-holder {
float: right;
margin-right: 65px;
margin-top: -1px;
}
}
.audio-effects {
width:40%;
@include border_box_sizing;
float:left;
margin-bottom:20px;
padding:10px;
position:relative;
select {
width:90%;
margin-bottom:20px;
}
a.manage-audio-plugins {
position:relative;
}
.down-arrow {
cursor:pointer;
width: 0;
height: 0;
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-top: 8px solid #fc0;
position: absolute;
top: 2px;
right: -20px;
}
.settings-holder {
right:10%;
text-align:right;
position:absolute;
margin-top: -27px;
@include border_box_sizing;
padding: 10px 0 10px 10px;
}
a.button-orange {
}
}
.vstScan {
margin-top:20px;
.spinner-small {
float:left;
}
span {
font-size:12px;
}
}
}

View File

@ -0,0 +1,36 @@
@import "client/common";
#configure-outputs-dialog {
width: 425px;
.dialog-inner {
width: auto;
}
h3 {
color: white;
font-weight: bold;
margin-bottom: 10px;
}
.actions {
clear: both;
text-align: center;
}
p {
margin-bottom:10px;
line-height:125%;
}
select {
width: 100%;
&.output-1 {
margin-bottom:15px;
}
&.output-2 {
margin-bottom:50px;
}
}
}

View File

@ -2,8 +2,6 @@
@charset "UTF-8";
#configure-tracks-dialog {
min-height: 700px;
max-height: 700px;
width:800px;
&[current-screen="account/audio"] {
@ -120,21 +118,21 @@
}
.buttons {
bottom: 25px;
position: absolute;
right: 25px;
left:25px;
position:static;
margin-top:20px;
text-align:center;
}
.btn-add-new-audio-gear {
float:left;
position:absolute;
left:20px;
}
.btn-cancel {
float:right;
}
.btn-update-settings {
float:right;
}
}

View File

@ -69,6 +69,7 @@ module ClientHelper
gon.ftue_maximum_gear_latency = Rails.application.config.ftue_maximum_gear_latency
gon.musician_search_meta = MusicianSearch.search_filter_meta
gon.band_search_meta = BandSearch.search_filter_meta
gon.session_stat_thresholds = Rails.application.config.session_stat_thresholds
# is this the native client or browser?
@nativeClient = is_native_client?

View File

@ -0,0 +1,7 @@
script type='text/template' id='template-manage-vsts'
ul
li data-manage-vst-option="scan"
a href='#' scan for new or updated plugins
li data-manage-vst-option="clear"
a href='#' clear plug-in list

View File

@ -0,0 +1,9 @@
script type='text/template' id='template-vst-effects'
ul
li data-manage-vst-option="open-vst"
a href='#'
| Open&nbsp;
span.vst-name
li data-manage-vst-option="update-track"
a href='#' Update Track . . .

View File

@ -20,6 +20,8 @@
<%= render "jamServer" %>
<%= render "iconInstrumentSelect" %>
<%= render "muteSelect" %>
<%= render "manageVsts" %>
<%= render "vstEffects" %>
<%= render "metronome_playback_mode" %>
<%= render "clients/wizard/buttons" %>
<%= render "clients/wizard/gear/gear_wizard" %>

View File

@ -81,16 +81,7 @@
.center
%a.button-orange.watch-video{href:'https://www.youtube.com/watch?v=SjMeMZpKNR4', rel:'external'} WATCH VIDEO
.wizard-step-column
%h2 Unassigned Ports
.unassigned-input-channels.channels-holder
.wizard-step-column
%h2 Track Input Port(s)
.tracks
.wizard-step-column
%h2 Instrument
.instruments
.output-channels
.unassigned-output-channels.channels-holder
= react_component 'ConfigureTracks', {}
.wizard-step{ 'layout-wizard-step' => "3", 'dialog-title' => "Configure Voice Chat", 'dialog-purpose' => "ConfigureVoiceChat" }
.ftuesteps

View File

@ -0,0 +1,2 @@
.dialog.dialog-overlay-sm.top-parent layout='dialog' layout-id='configure-live-tracks-dialog' id='configure-live-tracks-dialog'
= react_component 'ConfigureLiveTracksDialog', {}

View File

@ -0,0 +1,2 @@
.dialog.dialog-overlay-sm.top-parent layout='dialog' layout-id='configure-outputs-dialog' id='configure-outputs-dialog'
= react_component 'ConfigureOutputsDialog', {}

View File

@ -4,7 +4,7 @@
%h1 configure tracks
.dialog-inner
.dialog-tabs
%a.selected.tab-configure-audio Music Audio
%a.selected.tab-configure-audio Inputs & Outputs
%a.tab-configure-voice Voice Chat
.instructions
@ -16,32 +16,8 @@
.tab.no-selection-range{'tab-id' => 'music-audio'}
.column
.certified-audio-profile-section
.sub-header Certified Audio Profile
%select.certified-audio-profile
.clearall
= react_component 'ConfigureTracks', {}
.unused-audio-inputs-section
.sub-header Unused Input Ports
.unassigned-input-channels.channels-holder
.unused-audio-outputs-section
.sub-header Unused Output Ports
.unassigned-output-channels.channels-holder
.column
.input-tracks-section
.sub-column
.sub-header Track Input Port(s)
.input-tracks.tracks
.sub-column
.sub-header Instrument
.instruments
.output-channels-section
.sub-header Audio Output Port
.output-channels
.clearall
@ -72,5 +48,6 @@
.buttons
%a.btn-add-new-audio-gear.button-grey{'layout-link' => 'add-new-audio-gear'} ADD NEW AUDIO GEAR
%a.button-orange.btn-update-settings{href:'#'} UPDATE SETTINGS
%a.button-grey.btn-cancel{href:'#'} CANCEL
%a.button-orange.btn-update-settings{href:'#'} SAVE SETTINGS

View File

@ -44,3 +44,5 @@
= render 'dialogs/recordingSelectorDialog'
= render 'dialogs/soundCloudPlayerDialog'
= render 'dialogs/deleteVideoConfirmDialog'
= render 'dialogs/configureLiveTracksDialog'
= render 'dialogs/configureOutputsDialog'

View File

@ -377,5 +377,27 @@ if defined?(Bundler)
config.download_tracker_day_range = 30
config.max_user_ip_address = 10
config.max_multiple_users_same_ip = 2
config.session_stat_thresholds = {
network: {
wifi: {warn: true, poor: true, eql: true},
net_bitrate: {warn: 210, poor: 155, inverse:true},
ping: {warn: 40, poor: 70},
pkt_loss: {warn: 3, poor: 10},
audiojq_median: {warn: 2.1, poor: 5.1}
},
system: {
cpu: {warn: 70, poor:85}
},
audio: {
audio_in_type: {warn: 'Windows WDM-KS', poor: nil, eql:true},
audio_out_type: {warn: 'Windows WDM-KS', poor: nil, eql:true},
framesize: {warn: 2.6, poor: 2.6},
latency: {warn: 10, poor: 20},
input_jitter: {warn: 0.5, poor: 1},
output_jitter: {warn: 0.5, poor: 1},
}
}
config.vst_enabled = true
end
end

View File

@ -102,4 +102,6 @@ SampleApp::Application.configure do
config.react.variant = :development
config.time_shift_style = :sox # or sbsms
config.vst_enabled = true
end

View File

@ -22,4 +22,5 @@ Gon.global.jamtrack_landing_bubbles_enabled = Rails.application.config.jamtrack_
Gon.global.jamtrack_browser_bubbles_enabled = Rails.application.config.jamtrack_browser_bubbles_enabled
Gon.global.bugsnag_key = Rails.application.config.bugsnag_key
Gon.global.bugsnag_notify_release_stages = Rails.application.config.bugsnag_notify_release_stages
Gon.global.vst_enabled = Rails.application.config.vst_enabled
Gon.global.env = Rails.env

View File

@ -95,6 +95,11 @@ namespace :jam_tracks do
JamTrackImporter.synchronize_all(skip_audio_upload: false)
end
task sync_tim_tracks: :environment do |task, args|
JamTrackImporter.storage_format = 'TimTracks'
JamTrackImporter.synchronize_all(skip_audio_upload:false)
end
task tency_dups: :environment do |task, args|
end