diff --git a/admin/app/admin/crash_dumps.rb b/admin/app/admin/crash_dumps.rb index bcfcd9106..4d85ae8ae 100644 --- a/admin/app/admin/crash_dumps.rb +++ b/admin/app/admin/crash_dumps.rb @@ -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 diff --git a/admin/app/admin/download_tracker.rb b/admin/app/admin/download_tracker.rb index 2577736e5..6d0daa013 100644 --- a/admin/app/admin/download_tracker.rb +++ b/admin/app/admin/download_tracker.rb @@ -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 diff --git a/ruby/lib/jam_ruby/models/crash_dump.rb b/ruby/lib/jam_ruby/models/crash_dump.rb index 13087fed0..22874a299 100644 --- a/ruby/lib/jam_ruby/models/crash_dump.rb +++ b/ruby/lib/jam_ruby/models/crash_dump.rb @@ -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 diff --git a/ruby/lib/jam_ruby/models/jam_track_right.rb b/ruby/lib/jam_ruby/models/jam_track_right.rb index 63558f84a..b400771a5 100644 --- a/ruby/lib/jam_ruby/models/jam_track_right.rb +++ b/ruby/lib/jam_ruby/models/jam_track_right.rb @@ -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! diff --git a/web/app/assets/javascripts/backend_alerts.js b/web/app/assets/javascripts/backend_alerts.js index 2bd2b3737..705f9ec29 100644 --- a/web/app/assets/javascripts/backend_alerts.js +++ b/web/app/assets/javascripts/backend_alerts.js @@ -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)) { diff --git a/web/app/assets/javascripts/configureTracksHelper2.js b/web/app/assets/javascripts/configureTracksHelper2.js index 7c4f07128..d87319a53 100644 --- a/web/app/assets/javascripts/configureTracksHelper2.js +++ b/web/app/assets/javascripts/configureTracksHelper2.js @@ -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 = $('
'); - $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; diff --git a/web/app/assets/javascripts/dialog/configureTrackDialog.js b/web/app/assets/javascripts/dialog/configureTrackDialog.js index 20801cfa6..17989b205 100644 --- a/web/app/assets/javascripts/dialog/configureTrackDialog.js +++ b/web/app/assets/javascripts/dialog/configureTrackDialog.js @@ -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); diff --git a/web/app/assets/javascripts/fakeJamClient.js b/web/app/assets/javascripts/fakeJamClient.js index 077ad4ced..8b1b8477a 100644 --- a/web/app/assets/javascripts/fakeJamClient.js +++ b/web/app/assets/javascripts/fakeJamClient.js @@ -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"; }; diff --git a/web/app/assets/javascripts/globals.js b/web/app/assets/javascripts/globals.js index 8c5895a7f..148b5ecc5 100644 --- a/web/app/assets/javascripts/globals.js +++ b/web/app/assets/javascripts/globals.js @@ -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 = { diff --git a/web/app/assets/javascripts/jquery.manageVsts.js b/web/app/assets/javascripts/jquery.manageVsts.js new file mode 100644 index 000000000..a14cec2e1 --- /dev/null +++ b/web/app/assets/javascripts/jquery.manageVsts.js @@ -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); \ No newline at end of file diff --git a/web/app/assets/javascripts/jquery.trackEffects.js b/web/app/assets/javascripts/jquery.trackEffects.js new file mode 100644 index 000000000..b0a15c74e --- /dev/null +++ b/web/app/assets/javascripts/jquery.trackEffects.js @@ -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); \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components.js b/web/app/assets/javascripts/react-components.js index ff86ff8e6..cfc88f9ff 100644 --- a/web/app/assets/javascripts/react-components.js +++ b/web/app/assets/javascripts/react-components.js @@ -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 diff --git a/web/app/assets/javascripts/react-components/ConfigureAudioTrack.js.jsx.coffee b/web/app/assets/javascripts/react-components/ConfigureAudioTrack.js.jsx.coffee new file mode 100644 index 000000000..e69de29bb diff --git a/web/app/assets/javascripts/react-components/ConfigureLiveTracksDialog.js.jsx.coffee b/web/app/assets/javascripts/react-components/ConfigureLiveTracksDialog.js.jsx.coffee new file mode 100644 index 000000000..3bc426721 --- /dev/null +++ b/web/app/assets/javascripts/react-components/ConfigureLiveTracksDialog.js.jsx.coffee @@ -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 = `` + defaultSelectionTwo = `` + + inputOneOptions.push(defaultSelectionOne) + inputTwoOptions.push(defaultSelectionTwo) + inputOneValue = '' + inputTwoValue = '' + selectedInstrument = '' + selectedVst = 'NONE' + + + instruments = [] + instruments.push(``) + for displayName, value of context.JK.server_to_client_instrument_map + instruments.push(``) + + vsts = [] + + instrumentDisabled = true + vstDisabled = true + + + if @state.configureTracks? + + if @state.configureTracks.scanningVsts + scan = + `
+
Scanning your system
for VST & AU plug-ins...
+
` + + 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 = `` + inputOneOptions.push(item) + inputTwoOptions.push(item) + + + for plugin in @state.configureTracks.vstPluginList.vsts + if plugin.isInstrument == false && plugin.category == 'Effect' + vsts.push(``) + else if plugin.category == 'NONE' + vsts.push(``) + + if @state.configureTracks.editingTrack.vst? + vstAssignedThisTrack = true + selectedVst = @state.configureTracks.editingTrack.vst.file + + vstSettingBtnClasses = classNames({'button-orange': vstAssignedThisTrack, 'button-grey': !vstAssignedThisTrack}) + `
+
+

Audio Input Ports

+

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.

+ + +
+
+

Instrument

+ +
+
+

Audio Effects (optional)

+ + manage audio plugins
+
+ SETTINGS . . . +
+ {scan} +
+
` + + renderMidi: () -> + midiInterfaces = [] + midiInterfaces.push(``) + midiInstruments = [] + + instruments = [] + for displayName, value of context.JK.server_to_client_instrument_map + instruments.push(``) + + 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(``) + + for plugin in @state.configureTracks.vstPluginList.vsts + if plugin.isInstrument == true + midiInstruments.push(``) + else if plugin.category == 'NONE' + midiInstruments.push(``) + + `
+
+

MIDI Interface

+ + scan for connected MIDI interfaces +
+
+

Instrument

+ +
+
+

MIDI Instrument (VST or AU Plugin)

+ + manage audio plugins
+
+ SETTING . . . +
+
+
` + + 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 = `CANCEL` + + `
+
+ +

{header}

+
+
+
+

Track Type

+
+
+
+ + {activeElement} + +
+ {cancelBtn} + {action} +
+
+
` + + 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}) + +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/ConfigureOutputsDialog.js.jsx.coffee b/web/app/assets/javascripts/react-components/ConfigureOutputsDialog.js.jsx.coffee new file mode 100644 index 000000000..41affdad2 --- /dev/null +++ b/web/app/assets/javascripts/react-components/ConfigureOutputsDialog.js.jsx.coffee @@ -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(``) + + if @state.configureTracks? + for output in @state.configureTracks.musicPorts.outputs + outputs.push(``) + + `
+
+ +

session audio outputs

+
+
+

Select two audio output ports that will be used to deliver the stereo audio of your sessions to your headphones or monitor.

+ + +
+ CANCEL + UPDATE PORTS +
+
+
` + + 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) + } +) diff --git a/web/app/assets/javascripts/react-components/ConfigureTracks.js.jsx.coffee b/web/app/assets/javascripts/react-components/ConfigureTracks.js.jsx.coffee new file mode 100644 index 000000000..24cd6a140 --- /dev/null +++ b/web/app/assets/javascripts/react-components/ConfigureTracks.js.jsx.coffee @@ -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(`
{input.name}
`) + + if !inputsForTrack.instrument_id? + instrument = `?` + else + instrument = `` + + trackTypeLabel = 'AUDIO' + + vstName = 'None' + if inputsForTrack.vst? && inputsForTrack.vst != 'NONE' + vstName = "#{inputsForTrack.vst.name} by #{inputsForTrack.vst.manuf}" + + liveTracks.push( + `
+
{candidate.assignment}:{trackTypeLabel}{inputs}
+
{vstName}
+
{instrument}
+
+ update + delete +
+
`) + + for output, i in trackAssignments.outputs.assigned + outputs.push( + `
+
{i + 1}:
{output.name}
+
`) + + `
+ +
+
+

Session Audio Inputs (Live Performance Tracks)

+

Plugin

+

Instrument

+
+
+ {liveTracks} +
+
+ ADD TRACK . . . +
+
+
+
+

Session Audio Outputs (Requires 2 Ports)

+
+ {outputs} +
+ UPDATE OUTPUTS . . . +
+
+
+
` + + 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() +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionMyTrack.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMyTrack.js.jsx.coffee index e36502a3c..4f46ca51d 100644 --- a/web/app/assets/javascripts/react-components/SessionMyTrack.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/SessionMyTrack.js.jsx.coffee @@ -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)" } - #
+ classification = @state.stats?.classification + + if !classification? + classification = 'unknown' + + connectionStateClasses = { 'track-connection-state': true} + connectionStateClasses[classification] = true + + if @props.associatedVst? + @equalizerSet = true + vst = `
` `
@@ -51,15 +74,15 @@ MixerActions = @MixerActions
- +
+ {vst} +

- -
` @@ -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) + }) diff --git a/web/app/assets/javascripts/react-components/SessionMyTracks.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMyTracks.js.jsx.coffee index 97323d6f8..017255665 100644 --- a/web/app/assets/javascripts/react-components/SessionMyTracks.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/SessionMyTracks.js.jsx.coffee @@ -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() diff --git a/web/app/assets/javascripts/react-components/SessionOtherTrack.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionOtherTrack.js.jsx.coffee index 68a6ccf70..0460c9819 100644 --- a/web/app/assets/javascripts/react-components/SessionOtherTrack.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/SessionOtherTrack.js.jsx.coffee @@ -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 + `
@@ -59,6 +80,7 @@ MixerActions = @MixerActions
+

@@ -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) diff --git a/web/app/assets/javascripts/react-components/SessionOtherTracks.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionOtherTracks.js.jsx.coffee index 311f0402a..c78245c1a 100644 --- a/web/app/assets/javascripts/react-components/SessionOtherTracks.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/SessionOtherTracks.js.jsx.coffee @@ -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] diff --git a/web/app/assets/javascripts/react-components/SessionStatsHover.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionStatsHover.js.jsx.coffee new file mode 100644 index 000000000..d0120d481 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionStatsHover.js.jsx.coffee @@ -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 + `
- {name}
{value}
` + + 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 = + `
+

Internet

+ {networkStats} +
` + + `
+

Session Diagnostics & Stats: {this.props.participant.user.name}

+ +
+
+

Computer

+ {computerStats} +
+
+

Audio Interface

+ {audioStats} +
+ {networkTag} +
+
+ {extraInfo} +
+
` + + 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() +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/actions/ConfigureTracksActions.js.coffee b/web/app/assets/javascripts/react-components/actions/ConfigureTracksActions.js.coffee index 4a2890385..7b5b3aa2d 100644 --- a/web/app/assets/javascripts/react-components/actions/ConfigureTracksActions.js.coffee +++ b/web/app/assets/javascripts/react-components/actions/ConfigureTracksActions.js.coffee @@ -19,4 +19,5 @@ context = window associateVSTWithTrack: {} associateMIDIWithTrack: {} desiredTrackType: {} + vstChanged: {} }) diff --git a/web/app/assets/javascripts/react-components/actions/SessionStatsActions.js.coffee b/web/app/assets/javascripts/react-components/actions/SessionStatsActions.js.coffee new file mode 100644 index 000000000..23dd5ef48 --- /dev/null +++ b/web/app/assets/javascripts/react-components/actions/SessionStatsActions.js.coffee @@ -0,0 +1,5 @@ +context = window + +@SessionStatsActions = Reflux.createActions({ + pushStats: {} +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/mixins/SessionMyTracksMixin.js.coffee b/web/app/assets/javascripts/react-components/mixins/SessionMyTracksMixin.js.coffee index 9d808485c..7fc97a850 100644 --- a/web/app/assets/javascripts/react-components/mixins/SessionMyTracksMixin.js.coffee +++ b/web/app/assets/javascripts/react-components/mixins/SessionMyTracksMixin.js.coffee @@ -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) + + } \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/stores/ConfigureTracksStore.js.coffee b/web/app/assets/javascripts/react-components/stores/ConfigureTracksStore.js.coffee new file mode 100644 index 000000000..b3c2b23a0 --- /dev/null +++ b/web/app/assets/javascripts/react-components/stores/ConfigureTracksStore.js.coffee @@ -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.

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) + + } +) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/stores/SessionStatsStore.js.coffee b/web/app/assets/javascripts/react-components/stores/SessionStatsStore.js.coffee new file mode 100644 index 000000000..a59d3fa99 --- /dev/null +++ b/web/app/assets/javascripts/react-components/stores/SessionStatsStore.js.coffee @@ -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) + } +) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee b/web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee index b0bbb0ac7..780126b38 100644 --- a/web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee +++ b/web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee @@ -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}}) diff --git a/web/app/assets/javascripts/utils.js b/web/app/assets/javascripts/utils.js index 68aa5b692..e4d626e0e 100644 --- a/web/app/assets/javascripts/utils.js +++ b/web/app/assets/javascripts/utils.js @@ -284,7 +284,8 @@ options.cssStyles = {} options.padding = 0; - context.JK.hoverBubble($element, '
', options) + var extra = options.extraClasses || '' + context.JK.hoverBubble($element, '
', 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); diff --git a/web/app/assets/javascripts/wizard/gear/step_configure_tracks.js b/web/app/assets/javascripts/wizard/gear/step_configure_tracks.js index 81c330620..72e97aad3 100644 --- a/web/app/assets/javascripts/wizard/gear/step_configure_tracks.js +++ b/web/app/assets/javascripts/wizard/gear/step_configure_tracks.js @@ -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) { diff --git a/web/app/assets/stylesheets/client/client.css b/web/app/assets/stylesheets/client/client.css index 277f98cdb..1c0385193 100644 --- a/web/app/assets/stylesheets/client/client.css +++ b/web/app/assets/stylesheets/client/client.css @@ -53,6 +53,8 @@ *= require dialogs/dialog *= require ./iconInstrumentSelect *= require ./muteSelect + *= require ./manageVsts + *= require ./vstEffects *= require ./metronomePlaybackModeSelect *= require ./terms *= require ./createSession diff --git a/web/app/assets/stylesheets/client/manageVsts.css.scss b/web/app/assets/stylesheets/client/manageVsts.css.scss new file mode 100644 index 000000000..a7063e390 --- /dev/null +++ b/web/app/assets/stylesheets/client/manageVsts.css.scss @@ -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; + } + } +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/react-components/ConfigureTracks.css.scss b/web/app/assets/stylesheets/client/react-components/ConfigureTracks.css.scss new file mode 100644 index 000000000..72d90891a --- /dev/null +++ b/web/app/assets/stylesheets/client/react-components/ConfigureTracks.css.scss @@ -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; + } + } +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/react-components/SessionScreen.css.scss b/web/app/assets/stylesheets/client/react-components/SessionScreen.css.scss index bea31605f..89ee81304 100644 --- a/web/app/assets/stylesheets/client/react-components/SessionScreen.css.scss +++ b/web/app/assets/stylesheets/client/react-components/SessionScreen.css.scss @@ -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; diff --git a/web/app/assets/stylesheets/client/react-components/SessionTrack.css.scss b/web/app/assets/stylesheets/client/react-components/SessionTrack.css.scss index 82614b21e..af76f6614 100644 --- a/web/app/assets/stylesheets/client/react-components/SessionTrack.css.scss +++ b/web/app/assets/stylesheets/client/react-components/SessionTrack.css.scss @@ -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; diff --git a/web/app/assets/stylesheets/client/vstEffects.css.scss b/web/app/assets/stylesheets/client/vstEffects.css.scss new file mode 100644 index 000000000..945e49b10 --- /dev/null +++ b/web/app/assets/stylesheets/client/vstEffects.css.scss @@ -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;; + } + } + } +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/wizard/gearWizard.css.scss b/web/app/assets/stylesheets/client/wizard/gearWizard.css.scss index 5e6084efc..d7bbb1f80 100644 --- a/web/app/assets/stylesheets/client/wizard/gearWizard.css.scss +++ b/web/app/assets/stylesheets/client/wizard/gearWizard.css.scss @@ -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"] { diff --git a/web/app/assets/stylesheets/dialogs/configureLiveTracksDialog.css.scss b/web/app/assets/stylesheets/dialogs/configureLiveTracksDialog.css.scss new file mode 100644 index 000000000..212e6869d --- /dev/null +++ b/web/app/assets/stylesheets/dialogs/configureLiveTracksDialog.css.scss @@ -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; + } + } +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/dialogs/configureOutputsDialog.css.scss b/web/app/assets/stylesheets/dialogs/configureOutputsDialog.css.scss new file mode 100644 index 000000000..b0e9e28a4 --- /dev/null +++ b/web/app/assets/stylesheets/dialogs/configureOutputsDialog.css.scss @@ -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; + } + } +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/dialogs/configureTracksDialog.css.scss b/web/app/assets/stylesheets/dialogs/configureTracksDialog.css.scss index 46b1bd830..aa776ede7 100644 --- a/web/app/assets/stylesheets/dialogs/configureTracksDialog.css.scss +++ b/web/app/assets/stylesheets/dialogs/configureTracksDialog.css.scss @@ -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; + } } \ No newline at end of file diff --git a/web/app/helpers/client_helper.rb b/web/app/helpers/client_helper.rb index f6febfb1d..d9dba9952 100644 --- a/web/app/helpers/client_helper.rb +++ b/web/app/helpers/client_helper.rb @@ -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? diff --git a/web/app/views/clients/_manageVsts.html.slim b/web/app/views/clients/_manageVsts.html.slim new file mode 100644 index 000000000..d78dcc704 --- /dev/null +++ b/web/app/views/clients/_manageVsts.html.slim @@ -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 \ No newline at end of file diff --git a/web/app/views/clients/_vstEffects.html.slim b/web/app/views/clients/_vstEffects.html.slim new file mode 100644 index 000000000..4085f9ecc --- /dev/null +++ b/web/app/views/clients/_vstEffects.html.slim @@ -0,0 +1,9 @@ +script type='text/template' id='template-vst-effects' + ul + li data-manage-vst-option="open-vst" + a href='#' + | Open  + span.vst-name + + li data-manage-vst-option="update-track" + a href='#' Update Track . . . \ No newline at end of file diff --git a/web/app/views/clients/index.html.erb b/web/app/views/clients/index.html.erb index 0dd83c6ae..5bbd11c83 100644 --- a/web/app/views/clients/index.html.erb +++ b/web/app/views/clients/index.html.erb @@ -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" %> diff --git a/web/app/views/clients/wizard/gear/_gear_wizard.html.haml b/web/app/views/clients/wizard/gear/_gear_wizard.html.haml index c2c1d9b50..4f8f9ab87 100644 --- a/web/app/views/clients/wizard/gear/_gear_wizard.html.haml +++ b/web/app/views/clients/wizard/gear/_gear_wizard.html.haml @@ -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 diff --git a/web/app/views/dialogs/_configureLiveTracksDialog.html.slim b/web/app/views/dialogs/_configureLiveTracksDialog.html.slim new file mode 100644 index 000000000..2c488ddaa --- /dev/null +++ b/web/app/views/dialogs/_configureLiveTracksDialog.html.slim @@ -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', {} diff --git a/web/app/views/dialogs/_configureOutputsDialog.html.slim b/web/app/views/dialogs/_configureOutputsDialog.html.slim new file mode 100644 index 000000000..bb6f10e7c --- /dev/null +++ b/web/app/views/dialogs/_configureOutputsDialog.html.slim @@ -0,0 +1,2 @@ +.dialog.dialog-overlay-sm.top-parent layout='dialog' layout-id='configure-outputs-dialog' id='configure-outputs-dialog' + = react_component 'ConfigureOutputsDialog', {} diff --git a/web/app/views/dialogs/_configure_tracks_dialog.html.haml b/web/app/views/dialogs/_configure_tracks_dialog.html.haml index 5611533d1..42d6d1d2e 100644 --- a/web/app/views/dialogs/_configure_tracks_dialog.html.haml +++ b/web/app/views/dialogs/_configure_tracks_dialog.html.haml @@ -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 + diff --git a/web/app/views/dialogs/_dialogs.html.haml b/web/app/views/dialogs/_dialogs.html.haml index 45db4cb3f..2c45d7df2 100644 --- a/web/app/views/dialogs/_dialogs.html.haml +++ b/web/app/views/dialogs/_dialogs.html.haml @@ -44,3 +44,5 @@ = render 'dialogs/recordingSelectorDialog' = render 'dialogs/soundCloudPlayerDialog' = render 'dialogs/deleteVideoConfirmDialog' += render 'dialogs/configureLiveTracksDialog' += render 'dialogs/configureOutputsDialog' \ No newline at end of file diff --git a/web/config/application.rb b/web/config/application.rb index 5d9fc97d6..f89f8250d 100644 --- a/web/config/application.rb +++ b/web/config/application.rb @@ -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 diff --git a/web/config/environments/development.rb b/web/config/environments/development.rb index dbc9da309..fdfbb2d01 100644 --- a/web/config/environments/development.rb +++ b/web/config/environments/development.rb @@ -102,4 +102,6 @@ SampleApp::Application.configure do config.react.variant = :development config.time_shift_style = :sox # or sbsms + + config.vst_enabled = true end diff --git a/web/config/initializers/gon.rb b/web/config/initializers/gon.rb index ba60612db..d58d5fa10 100644 --- a/web/config/initializers/gon.rb +++ b/web/config/initializers/gon.rb @@ -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 diff --git a/web/lib/tasks/jam_tracks.rake b/web/lib/tasks/jam_tracks.rake index e764a6de2..88bf63d04 100644 --- a/web/lib/tasks/jam_tracks.rake +++ b/web/lib/tasks/jam_tracks.rake @@ -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