From c28d8f3e7f03d86821530dc2ad7bedcda6e90a91 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Mon, 15 Jun 2015 13:44:23 -0500 Subject: [PATCH] * wip --- web/app/assets/javascripts/addTrack.js | 2 +- web/app/assets/javascripts/application.js | 2 +- web/app/assets/javascripts/backend_alerts.js | 3 + .../assets/javascripts/client_init.js.coffee | 2 +- web/app/assets/javascripts/faderHelpers.js | 116 ++++- web/app/assets/javascripts/fakeJamClient.js | 50 +- web/app/assets/javascripts/globals.js | 23 +- web/app/assets/javascripts/jam_rest.js | 9 + .../assets/javascripts/panHelpers.js.coffee | 34 ++ .../assets/javascripts/react-components.js | 5 + .../SessionMyTrack.js.jsx.coffee | 74 +++ .../SessionMyTracks.js.jsx.coffee | 86 +++- .../SessionScreen.js.jsx.coffee | 12 +- .../SessionTrackGain.js.jsx.coffee | 49 ++ .../SessionTrackPan.js.jsx.coffee | 51 ++ .../SessionTrackPanHover.js.jsx.coffee | 37 ++ .../SessionTrackSettingsBtn.js.jsx.coffee | 19 +- .../SessionTrackVU.js.jsx.coffee | 44 ++ .../SessionTrackVolumeHover.js.jsx.coffee | 91 ++++ .../react-components/Test.js.jsx.coffee | 19 + .../actions/AppActions.js.coffee | 2 - .../actions/BroadcastActions.js.coffee | 2 +- .../actions/MixerActions.js.coffee | 10 + .../actions/SessionActions.js.coffee | 8 + .../actions/SessionMyTracksActions.js.coffee | 5 + .../helpers/MixerHelper.js.coffee | 461 ++++++++++++++++++ .../helpers/SessionHelper.js.coffee | 80 +++ .../stores/AppStore.js.coffee | 3 - .../stores/MixerStore.js.coffee | 109 +++++ .../stores/SessionMyTracksStore.js.coffee | 21 + .../stores/SessionStore.js.coffee | 423 ++++++++++++++++ web/app/assets/javascripts/session.js | 6 +- web/app/assets/javascripts/trackHelpers.js | 8 +- web/app/assets/javascripts/utils.js | 64 +++ web/app/assets/javascripts/vuHelpers.js | 47 ++ .../assets/stylesheets/client/common.css.scss | 8 + .../react-components/SessionScreen.css.scss | 317 +++++++++++- web/app/views/clients/_faders.html.erb | 8 +- web/app/views/clients/index.html.erb | 2 +- 39 files changed, 2209 insertions(+), 103 deletions(-) create mode 100644 web/app/assets/javascripts/panHelpers.js.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionMyTrack.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionTrackGain.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionTrackPan.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionTrackPanHover.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionTrackVU.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionTrackVolumeHover.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/Test.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/actions/MixerActions.js.coffee create mode 100644 web/app/assets/javascripts/react-components/actions/SessionActions.js.coffee create mode 100644 web/app/assets/javascripts/react-components/actions/SessionMyTracksActions.js.coffee create mode 100644 web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee create mode 100644 web/app/assets/javascripts/react-components/helpers/SessionHelper.js.coffee create mode 100644 web/app/assets/javascripts/react-components/stores/MixerStore.js.coffee create mode 100644 web/app/assets/javascripts/react-components/stores/SessionMyTracksStore.js.coffee create mode 100644 web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee diff --git a/web/app/assets/javascripts/addTrack.js b/web/app/assets/javascripts/addTrack.js index ba6ccd459..00dd29aa6 100644 --- a/web/app/assets/javascripts/addTrack.js +++ b/web/app/assets/javascripts/addTrack.js @@ -161,7 +161,7 @@ /** setTimeout(function() { - var inputTracks = context.JK.TrackHelpers.getTracks(context.jamClient, 2); + var inputTracks = context.JK.TrackHelpers.getTracks(context.jamClient, 4); // this is some ugly logic coming up, here's why: // we need the id (guid) that the backend generated for the new track we just added diff --git a/web/app/assets/javascripts/application.js b/web/app/assets/javascripts/application.js index e77a3a58f..8fa2e95ef 100644 --- a/web/app/assets/javascripts/application.js +++ b/web/app/assets/javascripts/application.js @@ -54,11 +54,11 @@ //= require react //= require react_ujs //= require react-init -//= require react-components //= require web/signup_helper //= require web/signin_helper //= require web/signin //= require web/tracking +//= require react-components //= require_directory . //= require_directory ./dialog //= require_directory ./wizard diff --git a/web/app/assets/javascripts/backend_alerts.js b/web/app/assets/javascripts/backend_alerts.js index da48d9138..189fab1e1 100644 --- a/web/app/assets/javascripts/backend_alerts.js +++ b/web/app/assets/javascripts/backend_alerts.js @@ -77,6 +77,9 @@ } if (type === 2) { // BACKEND_MIXER_CHANGE + + context.MixerActions.mixersChanged(type, text) + if(context.JK.CurrentSessionModel) context.JK.CurrentSessionModel.onBackendMixerChanged(type, text) } diff --git a/web/app/assets/javascripts/client_init.js.coffee b/web/app/assets/javascripts/client_init.js.coffee index 9da3d933e..bc20a8015 100644 --- a/web/app/assets/javascripts/client_init.js.coffee +++ b/web/app/assets/javascripts/client_init.js.coffee @@ -3,7 +3,7 @@ $ = jQuery context = window context.JK ||= {}; -broadcastActions = context.JK.Actions.Broadcast +broadcastActions = @BroadcastActions context.JK.ClientInit = class ClientInit constructor: () -> diff --git a/web/app/assets/javascripts/faderHelpers.js b/web/app/assets/javascripts/faderHelpers.js index ba66a44ca..6d573a2c8 100644 --- a/web/app/assets/javascripts/faderHelpers.js +++ b/web/app/assets/javascripts/faderHelpers.js @@ -11,6 +11,7 @@ var $draggingFaderHandle = null; var $draggingFader = null; + var $floater = null; var draggingOrientation = null; var logger = g.JK.logger; @@ -20,6 +21,7 @@ e.stopPropagation(); var $fader = $(this); + var floaterConvert = $fader.data('floaterConverter') var sessionModel = window.JK.CurrentSessionModel || null; var mediaControlsDisabled = $fader.data('media-controls-disabled'); @@ -43,7 +45,7 @@ } } - draggingOrientation = $fader.attr('orientation'); + draggingOrientation = $fader.attr('data-orientation'); var offset = $fader.offset(); var position = { top: e.pageY - offset.top, left: e.pageX - offset.left} @@ -53,6 +55,10 @@ return false; } + if(floaterConvert) { + window.JK.FaderHelpers.setFloaterValue($fader.find('.floater'), floaterConvert(faderPct)) + } + $fader.parent().triggerHandler('fader_change', {percentage: faderPct, dragging: false}) setHandlePosition($fader, faderPct); @@ -61,9 +67,9 @@ function setHandlePosition($fader, value) { var ratio, position; - var $handle = $fader.find('div[control="fader-handle"]'); + var $handle = $fader.find('div[data-control="fader-handle"]'); - var orientation = $fader.attr('orientation'); + var orientation = $fader.attr('data-orientation'); var handleCssAttribute = getHandleCssAttribute($fader); // required because this method is entered directly when from a callback @@ -81,7 +87,7 @@ } function faderValue($fader, e, offset) { - var orientation = $fader.attr('orientation'); + var orientation = $fader.attr('data-orientation'); var getPercentFunction = getVerticalFaderPercent; var relativePosition = offset.top; if (orientation && orientation == 'horizontal') { @@ -92,7 +98,7 @@ } function getHandleCssAttribute($fader) { - var orientation = $fader.attr('orientation'); + var orientation = $fader.attr('data-orientation'); return (orientation === 'horizontal') ? 'left' : 'top'; } @@ -134,12 +140,34 @@ return false; } + // simple snap feature to stick to the mid point + if(faderPct > 46 && faderPct < 54 && $draggingFader.data('snap')) { + faderPct = 50 + var orientation = $draggingFader.attr('data-orientation'); + if(orientation == 'horizontal') { + var width = $draggingFader.width() + var left = width / 2 + ui.position.left = left + } + else { + var height = $draggingFader.height() + var top = height / 2 + ui.position.top = top + } + } + + var floaterConvert = $draggingFaderHandle.data('floaterConverter') + + if(floaterConvert && $floater) { + window.JK.FaderHelpers.setFloaterValue($floater, floaterConvert(faderPct)) + } $draggingFader.parent().triggerHandler('fader_change', {percentage: faderPct, dragging: true}) } function onFaderDragStart(e, ui) { $draggingFaderHandle = $(this); - $draggingFader = $draggingFaderHandle.closest('div[control="fader"]'); + $draggingFader = $draggingFaderHandle.closest('div[data-control="fader"]'); + $floater = $draggingFaderHandle.find('.floater') draggingOrientation = $draggingFader.attr('orientation'); var mediaControlsDisabled = $draggingFaderHandle.data('media-controls-disabled'); @@ -210,12 +238,12 @@ selector.html(g._.template(templateSource, options)); - selector.find('div[control="fader"]') + selector.find('div[data-control="fader"]') .data('media-controls-disabled', selector.data('media-controls-disabled')) .data('media-track-opener', selector.data('media-track-opener')) .data('showHelpAboutMediaMixers', selector.data('showHelpAboutMediaMixers')) - selector.find('div[control="fader-handle"]').draggable({ + selector.find('div[data-control="fader-handle"]').draggable({ drag: onFaderDrag, start: onFaderDragStart, stop: onFaderDragStop, @@ -233,6 +261,43 @@ } }, + renderFader2: function (selector, userOptions, floaterConverter) { + selector = $(selector); + if (userOptions === undefined) { + throw ("renderFader: userOptions is required"); + } + var renderDefaults = { + faderType: "vertical" + }; + var options = $.extend({}, renderDefaults, userOptions); + + selector.find('div[data-control="fader"]') + .data('media-controls-disabled', selector.data('media-controls-disabled')) + .data('media-track-opener', selector.data('media-track-opener')) + .data('showHelpAboutMediaMixers', selector.data('showHelpAboutMediaMixers')) + .data('floaterConverter', floaterConverter) + .data('snap', userOptions.snap) + + selector.find('div[data-control="fader-handle"]').draggable({ + drag: onFaderDrag, + start: onFaderDragStart, + stop: onFaderDragStop, + containment: "parent", + axis: options.faderType === 'horizontal' ? 'x' : 'y' + }).data('media-controls-disabled', selector.data('media-controls-disabled')) + .data('media-track-opener', selector.data('media-track-opener')) + .data('showHelpAboutMediaMixers', selector.data('showHelpAboutMediaMixers')) + .data('floaterConverter', floaterConverter) + .data('snap', userOptions.snap) + + // Embed any custom styles, applied to the .fader below selector + if ("style" in options) { + for (var key in options.style) { + selector.find(' .fader').css(key, options.style[key]); + } + } + }, + convertLinearToDb: function (input) { // deal with extremes better @@ -263,27 +328,48 @@ // composite function resembling audio taper if (input <= 1) { return -80; } - if (input <= 28) { return (2 * input - 80); } - if (input <= 79) { return (0.5 * input - 38); } - if (input < 99) { return (0.875 * input - 67.5); } + if (input <= 28) { return Math.round((2 * input - 80)); } // -78 to -24 db + if (input <= 79) { return Math.round((0.5 * input - 38)); } // -24 to 1.5 db + if (input < 99) { return Math.round((0.875 * input - 67.5)); } // 1.625 - 19.125 db if (input >= 99) { return 20; } }, + convertAudioTaperToPercent: function(db) { + if(db <= -78) { return 0} + if(db <= -24) { return (db + 80) / 2 } + if(db <= 1.5) { return (db + 38) / .5 } + if(db <= 19.125) { return (db + 67.5) / 0.875 } + return 100; + }, - setFaderValue: function (faderId, faderValue) { - var $fader = $('[fader-id="' + faderId + '"]'); + + setFaderValue: function (faderId, faderValue, floaterValue) { + var $fader = $('[data-fader-id="' + faderId + '"]'); this.setHandlePosition($fader, faderValue); + if(floaterValue !== undefined) { + var $floater = $fader.find('.floater') + this.setFloaterValue($floater, floaterValue) + } + }, + + showFader: function(faderId) { + var $fader = $('[data-fader-id="' + faderId + '"]'); + $fader.find('div[data-control="fader-handle"]').show() }, setHandlePosition: function ($fader, faderValue) { - draggingOrientation = $fader.attr('orientation'); + draggingOrientation = $fader.attr('data-orientation'); setHandlePosition($fader, faderValue); draggingOrientation = null; }, + setFloaterValue: function($floater, floaterValue) { + $floater.text(floaterValue) + }, + initialize: function () { - $('body').on('click', 'div[control="fader"]', faderClick); + $('body').on('click', 'div[data-control="fader"]', faderClick); } }; diff --git a/web/app/assets/javascripts/fakeJamClient.js b/web/app/assets/javascripts/fakeJamClient.js index e571976b2..81826b5eb 100644 --- a/web/app/assets/javascripts/fakeJamClient.js +++ b/web/app/assets/javascripts/fakeJamClient.js @@ -4,6 +4,7 @@ context.JK = context.JK || {}; context.JK.FakeJamClient = function(app, p2pMessageFactory) { + var ChannelGroupIds = context.JK.ChannelGroupIds var logger = context.JK.logger; logger.info("*** Fake JamClient instance initialized. ***"); @@ -169,22 +170,22 @@ function FTUEGetMusicInputs() { dbg('FTUEGetMusicInputs'); return { - "i~11~MultiChannel (FW AP Multi)~0^i~11~Multichannel (FW AP Multi)~1": - "Multichannel (FW AP Multi) - Channel 1/Multichannel (FW AP Multi) - Channel 2" + "i~11~MultiChannel (FWAPMulti)~0^i~11~Multichannel (FWAPMulti)~1": + "Multichannel (FWAPMulti) - Channel 1/Multichannel (FWAPMulti) - Channel 2" }; } function FTUEGetMusicOutputs() { dbg('FTUEGetMusicOutputs'); return { - "o~11~Multichannel (FW AP Multi)~0^o~11~Multichannel (FW AP Multi)~1": - "Multichannel (FW AP Multi) - Channel 1/Multichannel (FW AP Multi) - Channel 2" + "o~11~Multichannel (FWAPMulti)~0^o~11~Multichannel (FWAPMulti)~1": + "Multichannel (FWAPMulti) - Channel 1/Multichannel (FWAPMulti) - Channel 2" }; } function FTUEGetChatInputs() { dbg('FTUEGetChatInputs'); return { - "i~11~MultiChannel (FW AP Multi)~0^i~11~Multichannel (FW AP Multi)~1": - "Multichannel (FW AP Multi) - Channel 1/Multichannel (FW AP Multi) - Channel 2" + "i~11~MultiChannel (FWAPMulti)~0^i~11~Multichannel (FWAPMulti)~1": + "Multichannel (FWAPMulti) - Channel 1/Multichannel (FWAPMulti) - Channel 2" }; } function FTUEGetChannels() { @@ -449,7 +450,7 @@ } function GetASIODevices() { - var response =[{"device_id":0,"device_name":"Realtek High Definition Audio","device_type": 0,"interfaces":[{"interface_id":0,"interface_name":"Realtek HDA SPDIF Out","pins":[{"is_input":false,"pin_id":0,"pin_name":"PC Speaker"}]},{"interface_id":1,"interface_name":"Realtek HD Audio rear output","pins":[{"is_input":false,"pin_id":0,"pin_name":"PC Speaker"}]},{"interface_id":2,"interface_name":"Realtek HD Audio Mic input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"}]},{"interface_id":3,"interface_name":"Realtek HD Audio Line input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"}]},{"interface_id":4,"interface_name":"Realtek HD Digital input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Capture"}]},{"interface_id":5,"interface_name":"Realtek HD Audio Stereo input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"}]}],"wavert_supported":false},{"device_id":1,"device_name":"M-Audio FW Audiophile","device_type": 1,"interfaces":[{"interface_id":0,"interface_name":"FW AP Multi","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"},{"is_input":true,"pin_id":1,"pin_name":"Input"}]},{"interface_id":1,"interface_name":"FW AP 1/2","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"},{"is_input":true,"pin_id":1,"pin_name":"Input"}]},{"interface_id":2,"interface_name":"FW AP SPDIF","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"},{"is_input":true,"pin_id":1,"pin_name":"Input"}]},{"interface_id":3,"interface_name":"FW AP 3/4","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"}]}],"wavert_supported":false},{"device_id":2,"device_name":"Virtual Audio Cable","device_type": 2,"interfaces":[{"interface_id":0,"interface_name":"Virtual Cable 2","pins":[{"is_input":true,"pin_id":0,"pin_name":"Capture"},{"is_input":false,"pin_id":1,"pin_name":"Output"}]},{"interface_id":1,"interface_name":"Virtual Cable 1","pins":[{"is_input":true,"pin_id":0,"pin_name":"Capture"},{"is_input":false,"pin_id":1,"pin_name":"Output"}]}],"wavert_supported":false},{"device_id":3,"device_name":"WebCamDV WDM Audio Capture","device_type": 3,"interfaces":[{"interface_id":0,"interface_name":"WebCamDV Audio","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"},{"is_input":false,"pin_id":1,"pin_name":"Volume Control"}]}],"wavert_supported":false}]; + var response =[{"device_id":0,"device_name":"Realtek High Definition Audio","device_type": 0,"interfaces":[{"interface_id":0,"interface_name":"Realtek HDA SPDIF Out","pins":[{"is_input":false,"pin_id":0,"pin_name":"PC Speaker"}]},{"interface_id":1,"interface_name":"Realtek HD Audio rear output","pins":[{"is_input":false,"pin_id":0,"pin_name":"PC Speaker"}]},{"interface_id":2,"interface_name":"Realtek HD Audio Mic input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"}]},{"interface_id":3,"interface_name":"Realtek HD Audio Line input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"}]},{"interface_id":4,"interface_name":"Realtek HD Digital input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Capture"}]},{"interface_id":5,"interface_name":"Realtek HD Audio Stereo input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"}]}],"wavert_supported":false},{"device_id":1,"device_name":"M-Audio FW Audiophile","device_type": 1,"interfaces":[{"interface_id":0,"interface_name":"FWAPMulti","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"},{"is_input":true,"pin_id":1,"pin_name":"Input"}]},{"interface_id":1,"interface_name":"FW AP 1/2","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"},{"is_input":true,"pin_id":1,"pin_name":"Input"}]},{"interface_id":2,"interface_name":"FW AP SPDIF","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"},{"is_input":true,"pin_id":1,"pin_name":"Input"}]},{"interface_id":3,"interface_name":"FW AP 3/4","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"}]}],"wavert_supported":false},{"device_id":2,"device_name":"Virtual Audio Cable","device_type": 2,"interfaces":[{"interface_id":0,"interface_name":"Virtual Cable 2","pins":[{"is_input":true,"pin_id":0,"pin_name":"Capture"},{"is_input":false,"pin_id":1,"pin_name":"Output"}]},{"interface_id":1,"interface_name":"Virtual Cable 1","pins":[{"is_input":true,"pin_id":0,"pin_name":"Capture"},{"is_input":false,"pin_id":1,"pin_name":"Output"}]}],"wavert_supported":false},{"device_id":3,"device_name":"WebCamDV WDM Audio Capture","device_type": 3,"interfaces":[{"interface_id":0,"interface_name":"WebCamDV Audio","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"},{"is_input":false,"pin_id":1,"pin_name":"Volume Control"}]}],"wavert_supported":false}]; return response; } @@ -474,12 +475,22 @@ } function SessionGetControlState(mixerIds, isMasterOrPersonal) { dbg("SessionGetControlState"); - var groups = [0, 1, 2, 3, 3, 7, 8, 10, 11, 12]; + var groups = + [ChannelGroupIds.MasterGroup, + ChannelGroupIds.MonitorGroup, + ChannelGroupIds.AudioInputMusicGroup, + ChannelGroupIds.AudioInputChatGroup, + ChannelGroupIds.AudioInputChatGroup, + ChannelGroupIds.UserMusicInputGroup, + ChannelGroupIds.UserChatInputGroup, + ChannelGroupIds.PeerMediaTrackGroup, + ChannelGroupIds.JamTrackGroup, + ChannelGroupIds.MetronomeGroup]; var names = [ - "FW AP Multi", - "FW AP Multi", - "FW AP Multi", - "FW AP Multi", + "FWAPMulti", + "FWAPMulti", + "FWAPMulti", + "FWAPMulti", "", "", "", @@ -533,6 +544,7 @@ stereo: true, volume_left: -40, volume_right:-40, + pan: 0, instrument_id:50, // see globals.js mode: isMasterOrPersonal, rid: mixerIds[i] @@ -542,10 +554,10 @@ } function SessionGetIDs() { return [ - "FW AP Multi_0_10000", - "FW AP Multi_1_10100", - "FW AP Multi_2_10200", - "FW AP Multi_3_10500", + "FWAPMulti_0_10000", + "FWAPMulti_1_10100", + "FWAPMulti_2_10200", + "FWAPMulti_3_10500", "User@208.191.152.98#", "User@208.191.152.98_*" ]; @@ -612,9 +624,9 @@ function doCallbacks() { var names = ["vu"]; - //var ids = ["FW AP Multi_2_10200", "FW AP Multi_0_10000"]; - var ids= ["i~11~MultiChannel (FW AP Multi)~0^i~11~Multichannel (FW AP Multi)~1", - "i~11~MultiChannel (FW AP Multi)~0^i~11~Multichannel (FW AP Multi)~2"]; + //var ids = ["FWAPMulti_2_10200", "FWAPMulti_0_10000"]; + var ids= ["i~11~MultiChannel (FWAPMulti)~0^i~11~Multichannel (FWAPMulti)~1", + "i~11~MultiChannel (FWAPMulti)~0^i~11~Multichannel (FWAPMulti)~2"]; var args = []; for (var i=0; i + value = (((mixerPan + 90) / 90) * 100) / 2 + + if value < 0 + 0 + else if value > 100 + 100 + else + value + + ### + Convert the % value of a draggable panner element + to a mixer-ready pan value + ### + convertPercentToPan: (percent) -> + value = 2 * percent / 100 * 90 - 90 + + if value < -90 + -90 + else if value > 90 + 90 + else + Math.round(value) + +context.JK.PanHelpers = new panHelper() \ 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 05600e7b5..43fbc0d08 100644 --- a/web/app/assets/javascripts/react-components.js +++ b/web/app/assets/javascripts/react-components.js @@ -1,3 +1,8 @@ +//= require_directory ./react-components/helpers //= require_directory ./react-components/actions +//= require ./react-components/stores/AppStore +//= require ./react-components/stores/SessionStore +//= require ./react-components/stores/MixerStore +//= require ./react-components/stores/SessionMyTracksStore //= require_directory ./react-components/stores //= require_directory ./react-components \ 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 new file mode 100644 index 000000000..f6d4963e7 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionMyTrack.js.jsx.coffee @@ -0,0 +1,74 @@ +context = window + +MixerActions = @MixerActions + +@SessionMyTrack = React.createClass({ + + mixins: [Reflux.listenTo(@SessionMyTracksStore,"onInputsChanged")] + + onInputsChanged: (sessionMixers) -> + + mixers = sessionMixers.mixers + newMixers = mixers.findMixerForTrack.apply(mixers, this.props.mixerFinder) + + this.setState({mixers: newMixers}) + + + getInitialState: () -> + {mixers: this.props.mixers} + + handleMute: (e) -> + e.preventDefault() + + muting = $(e.currentTarget).is('.enabled') + + MixerActions.mute([this.state.mixers.mixer, this.state.mixers.oppositeMixer], muting) + + render: () -> + + muteMixer = this.state.mixers.muteMixer + vuMixer = this.state.mixers.vuMixer + + classes = React.addons.classSet({ + 'track-icon-mute': true + 'enabled' : !muteMixer.mute + 'muted' : muteMixer.mute + }) + + `
+
{this.props.name}
+
+
+
+ +
+
+
+
+
+
+
+ + +
+
` + + componentDidMount: () -> + $root = jQuery(this.getDOMNode()) + $mute = $root.find('.track-icon-mute') + $pan = $root.find('.track-icon-pan') + + context.JK.interactReactBubble( + $mute, + 'SessionTrackVolumeHover', + {mixers:this.state.mixers, mixerFinder: this.props.mixerFinder}, + {width:235, positions:['right', 'left'], offsetParent:$root.closest('.screen')}) + + context.JK.interactReactBubble( + $pan, + 'SessionTrackPanHover', + {mixers:this.state.mixers, mixerFinder: this.props.mixerFinder}, + {width:331, positions:['right', 'left'], offsetParent:$root.closest('.screen')}) + + +}) 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 7d13fc896..b5b254ba7 100644 --- a/web/app/assets/javascripts/react-components/SessionMyTracks.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/SessionMyTracks.js.jsx.coffee @@ -1,23 +1,75 @@ context = window + +ReactCSSTransitionGroup = React.addons.CSSTransitionGroup; + + @SessionMyTracks = React.createClass({ - render: () -> + mixins: [Reflux.listenTo(@SessionMyTracksStore,"onInputsChanged"), Reflux.listenTo(@AppStore,"onAppInit")] - noTracksNotice = `
-

- You have not set up any inputs for your instrument or vocals. - If you want to hear yourself play through the JamKazam app, - and let the app mix your live playing with JamTracks, or with other musicians in online sessions, - click here now. -

-
` + onInputsChanged: (sessionMixers) -> - `
-

my live tracks

- -
- {noTracksNotice} -
-
` - }) + session = sessionMixers.session + mixers = sessionMixers.mixers + + tracks = [] + + if session.inSession() + participant = session.getParticipant(@app.clientId) + + name = participant.user.name; + + for track in participant.tracks + # try to find mixer info for this track + mixerFinder = [participant.client_id, track, true] + mixerData = mixers.findMixerForTrack(participant.client_id, track, true) + mixerData.mixerFinder = mixerFinder # so that other callers can re-find their mixer data + + # todo: sessionModel.setAudioEstablished + + instrumentIcon = context.JK.getInstrumentIcon45(track.instrument_id); + photoUrl = context.JK.resolveAvatarUrl(participant.user.photo_url); + + tracks.push({track: track, mixerFinder: mixerFinder, mixers: mixerData, name: name, instrumentIcon: instrumentIcon, photoUrl: photoUrl}) + + # TODO: also deal with chat + + this.setState(tracks: tracks, session:session) + + render: () -> + + content = null + tracks = [] + + if this.state.tracks.length > 0 + for track in this.state.tracks + tracks.push(``) + + else if this.state.session? && this.state.session.inSession() + content = `
+

+ You have not set up any inputs for your instrument or vocals. + If you want to hear yourself play through the JamKazam app, + and let the app mix your live playing with JamTracks, or with other musicians in online sessions, + click here now. +

+
` + + `
+

my live tracks

+ +
+ {content} + + {tracks} + +
+
` + + getInitialState:() -> + {tracks:[], session: null} + + onAppInit: (app) -> + @app = app +}) diff --git a/web/app/assets/javascripts/react-components/SessionScreen.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionScreen.js.jsx.coffee index 8a1443d6d..1b3685bcb 100644 --- a/web/app/assets/javascripts/react-components/SessionScreen.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/SessionScreen.js.jsx.coffee @@ -1,5 +1,7 @@ context = window +SessionActions = @SessionActions + @SessionScreen = React.createClass({ mixins: [Reflux.listenTo(@AppStore,"onAppInit")] @@ -16,7 +18,7 @@ context = window
-
+
@@ -27,12 +29,14 @@ context = window componentDidMount: () -> @logger = context.JK.logger - beforeShow: () -> + beforeShow: (data) => @logger.debug("session beforeShow") - afterShow: () -> + afterShow: (data) -> @logger.debug("session afterShow") + SessionActions.joinSession.trigger(data.id) + beforeHide: () -> @logger.debug("session beforeHide") @@ -44,7 +48,6 @@ context = window onAppInit: (@app) -> - @logger.debug("oh hai") screenBindings = { 'beforeShow': @beforeShow, 'afterShow': @afterShow, @@ -54,4 +57,5 @@ context = window }; @app.bindScreen('session2', screenBindings); + }) diff --git a/web/app/assets/javascripts/react-components/SessionTrackGain.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionTrackGain.js.jsx.coffee new file mode 100644 index 000000000..99ea912e8 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionTrackGain.js.jsx.coffee @@ -0,0 +1,49 @@ +context = window +logger = context.JK.logger + +@SessionTrackGain = React.createClass({ + + getInitialState: () -> + { + mixers: this.props.mixers, + behaviors: this.props.behaviors || {} + } + + faderChanged: (e, data) -> + $target = $(this) + groupId = $target.data('groupId') + mixerIds = [this.state.mixers.mixer.id] + + MixerActions.faderChanged(data, mixerIds, groupId) + + render: () -> + `
+
+
+ +
+
+
` + + componentDidMount: () -> + $root = jQuery(this.getDOMNode()) + if !$root.is('.track-gain') + logger.error("unknown root node") + + $fader = $root.attr('data-mixer-id', this.state.mixers.mixer.id).data('groupId', this.state.mixers.mixer.groupId).data('mixer', this.state.mixers.mixer).data('opposite-mixer', this.state.mixers.oppositeMixer) + + if this.state.behaviors.mediaControlsDisabled + $fader.data('media-controls-disabled', true).data('media-track-opener', this.state.behaviors.mediaTrackOpener) # this we be applied later to the fader handle $element + + $fader.data('showHelpAboutMediaMixers', this.state.behaviors.showHelpAboutMediaMixers) + + context.JK.FaderHelpers.renderFader2($fader, {faderType: 'vertical'}); + + # Initialize gain position + MixerActions.initGain(this.state.mixers.mixer) + + # watch for fader change events + $fader.on('fader_change', this.faderChanged); + + +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionTrackPan.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionTrackPan.js.jsx.coffee new file mode 100644 index 000000000..09963272b --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionTrackPan.js.jsx.coffee @@ -0,0 +1,51 @@ +context = window +logger = context.JK.logger + +@SessionTrackPan = React.createClass({ + + getInitialState: () -> + { + mixers: this.props.mixers, + behaviors: this.props.behaviors || {} + } + + panChanged: (e, data) -> + $target = $(this) + groupId = $target.data('groupId') + mixerIds = [this.state.mixers.mixer.id] + + MixerActions.panChanged(data, mixerIds, groupId) + + render: () -> + `
+
Left
+
Right
+
+
+
+ +
+
+
` + + componentDidMount: () -> + $root = jQuery(this.getDOMNode()) + if !$root.is('.track-pan') + logger.error("unknown root node") + + $fader = $root.attr('data-mixer-id', this.state.mixers.mixer.id).data('groupId', this.state.mixers.mixer.groupId).data('mixer', this.state.mixers.mixer).data('opposite-mixer', this.state.mixers.oppositeMixer) + + if this.state.behaviors.mediaControlsDisabled + $fader.data('media-controls-disabled', true).data('media-track-opener', this.state.behaviors.mediaTrackOpener) # this we be applied later to the fader handle $element + + $fader.data('showHelpAboutMediaMixers', this.state.behaviors.showHelpAboutMediaMixers) + + context.JK.FaderHelpers.renderFader2($fader, {faderType: 'horizontal', snap:true}, context.JK.PanHelpers.convertPercentToPan) + + # Initialize gain position + MixerActions.initPan(this.state.mixers.mixer) + + # watch for fader change events + $fader.on('fader_change', this.panChanged) + +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionTrackPanHover.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionTrackPanHover.js.jsx.coffee new file mode 100644 index 000000000..d0b26da67 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionTrackPanHover.js.jsx.coffee @@ -0,0 +1,37 @@ +context = window + +MixerActions = @MixerActions + +@SessionTrackPanHover = React.createClass({ + + mixins: [Reflux.listenTo(@SessionMyTracksStore, "onInputsChanged")] + + onInputsChanged: (sessionMixers) -> + mixers = sessionMixers.mixers + newMixers = mixers.findMixerForTrack.apply(mixers, this.props.mixerFinder) + + this.setState({mixers: newMixers}) + + + getInitialState: () -> + {mixers: this.props.mixers} + + render: () -> + + + + `
+
+

+ Use this slider to pan the audio of this track left or right in your personal mix. + This will not pan audio for other musicians in the session. + To pan audio in the master mix for recordings and broadcasts, use the Mixer button in the toolbar. +

+
+ +
+ +
+
` + +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionTrackSettingsBtn.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionTrackSettingsBtn.js.jsx.coffee index 2f1468006..ab73b0c57 100644 --- a/web/app/assets/javascripts/react-components/SessionTrackSettingsBtn.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/SessionTrackSettingsBtn.js.jsx.coffee @@ -1,9 +1,22 @@ context = window +logger = context.JK.logger + @SessionTrackSettingsBtn = React.createClass({ + mixins: [Reflux.listenTo(@AppStore,"onAppInit")] + + onConfigureSettings: (e) -> + e.preventDefault(); + + @app.layout.showDialog('configure-tracks') + + onAppInit: (app) -> + @app = app + render: () -> - ` - Settings - ` + `
+ + Settings +
` }) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionTrackVU.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionTrackVU.js.jsx.coffee new file mode 100644 index 000000000..5374a6956 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionTrackVU.js.jsx.coffee @@ -0,0 +1,44 @@ +context = window + +@SessionTrackVU = React.createClass({ + + getInitialState: () -> + {mixers: this.props.mixers} + + + render: () -> + lights = [] + redSwitch = Math.round(this.props.lightCount * 0.66); + lightClass = 'vu-red-off' + + if this.props.orientation == 'horizontal' + + for i in [0..this.props.lightCount-1] + lightClass = if i >= redSwitch then 'vu-red-off' else 'vu-green-off' + + lightClasses = React.addons.classSet('vulight', 'vu' + i, lightClass) + + lights.push(``) + + tableClasses = React.addons.classSet('vu', 'horizontal', this.props.side + '-' + this.state.mixers.mixer.mixerId) + + ` + + {lights} + +
` + else + + for i in [0..this.props.lightCount-1].reverse() + lightClass = if (i >= redSwitch) then "vu-red-off" else "vu-green-off" + + lightClasses = React.addons.classSet('vulight', 'vu' + i, lightClass) + + lights.push(``) + + tableClasses = React.addons.classSet('vu', 'vertical', this.props.side + '-' + this.state.mixers.mixer.mixerId) + + ` + {lights} +
` +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionTrackVolumeHover.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionTrackVolumeHover.js.jsx.coffee new file mode 100644 index 000000000..1778c6d2c --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionTrackVolumeHover.js.jsx.coffee @@ -0,0 +1,91 @@ +context = window + +MixerActions = @MixerActions + +@SessionTrackVolumeHover = React.createClass({ + + mixins: [Reflux.listenTo(@SessionMyTracksStore,"onInputsChanged")] + + onInputsChanged: (sessionMixers) -> + + mixers = sessionMixers.mixers + newMixers = mixers.findMixerForTrack.apply(mixers, this.props.mixerFinder) + + this.setState({mixers: newMixers}) + + getInitialState: () -> + {mixers: this.props.mixers} + + handleMute: (e) -> + e.preventDefault() + + muting = $(e.currentTarget).is('.enabled') + + MixerActions.mute([this.state.mixers.mixer, this.state.mixers.oppositeMixer], muting) + + handleMuteCheckbox: (e) -> + muting = $(e.target).is(':checked') + + MixerActions.mute([this.state.mixers.mixer, this.state.mixers.oppositeMixer], muting) + + render: () -> + + muteMixer = this.state.mixers.muteMixer + + classes = React.addons.classSet({ + 'track-icon-mute': true + 'enabled' : !muteMixer.mute + 'muted' : muteMixer.mute + }) + + + `
+
+
+ +
+
+ +
+
+
Volume
+
{this.state.mixers.mixer.volume_left}dB
+
+ +
+ + + +
+ +
+

Use this slider to control the volume of this track in your personal mix.

+

This will not affect the volume of this track for other musicians in the session.

+

To adjust master levels for all musicians for recordings and broadcasts, use Mixer button in the toolbar.

+
+
` + + componentDidMount: () -> + $root = jQuery(this.getDOMNode()) + + # initialize icheck + $checkbox = $root.find('input') + context.JK.checkbox($checkbox) + $checkbox.on('ifChanged', this.handleMuteCheckbox); + + if this.state.mixers.muteMixer.mute + $checkbox.iCheck('check').attr('checked', true) + else + $checkbox.iCheck('uncheck').attr('checked', false) + + componentWillUpdate: (nextProps, nextState) -> + $root = jQuery(this.getDOMNode()) + + # re-initialize icheck + $checkbox = $root.find('input') + + if nextState.mixers.muteMixer.mute + $checkbox.iCheck('check').attr('checked', true) + else + $checkbox.iCheck('uncheck').attr('checked', false) +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/Test.js.jsx.coffee b/web/app/assets/javascripts/react-components/Test.js.jsx.coffee new file mode 100644 index 000000000..fbdc502a7 --- /dev/null +++ b/web/app/assets/javascripts/react-components/Test.js.jsx.coffee @@ -0,0 +1,19 @@ +context = window + +@TestComponent = React.createClass({ + + getInitialState: () -> + {something: 1} + + tick: () -> + console.log("tick") + this.setState({something: this.state.something + 1}) + + componentDidMount: () -> + console.log("here") + setInterval(@tick, 1000) + + render: () -> + console.log("render") + `
{this.state.something}
` +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/actions/AppActions.js.coffee b/web/app/assets/javascripts/react-components/actions/AppActions.js.coffee index b5dd77192..6c054e3ec 100644 --- a/web/app/assets/javascripts/react-components/actions/AppActions.js.coffee +++ b/web/app/assets/javascripts/react-components/actions/AppActions.js.coffee @@ -3,5 +3,3 @@ context = window @AppActions = Reflux.createActions({ appInit: {} }) - -context.JK.Actions.App = @AppActions diff --git a/web/app/assets/javascripts/react-components/actions/BroadcastActions.js.coffee b/web/app/assets/javascripts/react-components/actions/BroadcastActions.js.coffee index 7761257d2..e4bc43707 100644 --- a/web/app/assets/javascripts/react-components/actions/BroadcastActions.js.coffee +++ b/web/app/assets/javascripts/react-components/actions/BroadcastActions.js.coffee @@ -1,6 +1,6 @@ context = window -BroadcastActions = Reflux.createActions({ +@BroadcastActions = Reflux.createActions({ load: {asyncResult: true}, hide: {} }) diff --git a/web/app/assets/javascripts/react-components/actions/MixerActions.js.coffee b/web/app/assets/javascripts/react-components/actions/MixerActions.js.coffee new file mode 100644 index 000000000..1f23bfb1b --- /dev/null +++ b/web/app/assets/javascripts/react-components/actions/MixerActions.js.coffee @@ -0,0 +1,10 @@ +context = window + +@MixerActions = Reflux.createActions({ + mute: {} + faderChanged: {} + initGain: {} + panChanged: {} + initPan: {} + mixersChanged: {} +}) diff --git a/web/app/assets/javascripts/react-components/actions/SessionActions.js.coffee b/web/app/assets/javascripts/react-components/actions/SessionActions.js.coffee new file mode 100644 index 000000000..d815e60c0 --- /dev/null +++ b/web/app/assets/javascripts/react-components/actions/SessionActions.js.coffee @@ -0,0 +1,8 @@ +context = window + +@SessionActions = Reflux.createActions({ + joinSession: {} + leaveSession: {} + resyncServer: {asyncResult: true} + myTracksChanged: {} +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/actions/SessionMyTracksActions.js.coffee b/web/app/assets/javascripts/react-components/actions/SessionMyTracksActions.js.coffee new file mode 100644 index 000000000..568ba3b29 --- /dev/null +++ b/web/app/assets/javascripts/react-components/actions/SessionMyTracksActions.js.coffee @@ -0,0 +1,5 @@ +context = window + +@SessionMyTracksActions = Reflux.createActions({ + +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee b/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee new file mode 100644 index 000000000..b92e0055c --- /dev/null +++ b/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee @@ -0,0 +1,461 @@ +context = window + +ChannelGroupIds = context.JK.ChannelGroupIds +MIX_MODES = context.JK.MIX_MODES; + + +@MixerHelper = class MixerHelper + + constructor: (@session, @masterMixers, @personalMixers, @mixMode) -> + @mixersByResourceId = {} + @mixersByTrackId = {} + @allMixers = {} + @currentMixerRangeMin = null + @currentMixerRangeMax = null + @mediaTrackGroups = [ChannelGroupIds.MediaTrackGroup, ChannelGroupIds.JamTrackGroup, + ChannelGroupIds.MetronomeGroup] + @muteBothMasterAndPersonalGroups = [ChannelGroupIds.AudioInputMusicGroup, ChannelGroupIds.MediaTrackGroup, + ChannelGroupIds.JamTrackGroup, ChannelGroupIds.MetronomeGroup] + + + @organize() + + updateMixers: (type, text, @masterMixers, @personalMixers) -> + + @organize() + + if @session.inSession() && text == 'RebuildAudioIoControl' + SessionActions.myTracksChanged.trigger() + + + + organize: () -> + for masterMixer, i in @masterMixers + @allMixers['M' + masterMixer.id] = masterMixer; # populate allMixers by mixer.id + + # populate mixer pair + mixerPair = {} + @mixersByResourceId[masterMixer.rid] = mixerPair + @mixersByTrackId[masterMixer.id] = mixerPair + mixerPair.master = masterMixer; + + for personalMixer, i in @personalMixers + + @allMixers['P' + personalMixer.id] = personalMixer + + # populate other side of mixer pair + + mixerPair = @mixersByResourceId[personalMixer.rid] + unless mixerPair + if personalMixer.group_id != ChannelGroupIds.MonitorGroup + logger.warn("there is no master version of ", personalMixer) + + mixerPair = {} + @mixersByResourceId[personalMixer.rid] = mixerPair + + @mixersByTrackId[personalMixer.id] = mixerPair; + mixerPair.personal = personalMixer; + + @groupTypes() + + groupTypes: () -> + localMediaMixers = @mixersForGroupIds(@mediaTrackGroups, MIX_MODES.MASTER) + peerLocalMediaMixers = @mixersForGroupId(ChannelGroupIds.PeerMediaTrackGroup, MIX_MODES.MASTER) + + logger.debug("localMediaMixers", localMediaMixers) + #logger.debug("peerLocalMediaMixers", peerLocalMediaMixers) + + # get the server data regarding various media tracks + recordedBackingTracks = @session.recordedBackingTracks() + backingTracks = @session.backingTracks() + recordedJamTracks = @session.recordedJamTracks() + jamTracks = @session.jamTracks() + + ### + with mixer info, we use these to decide what kind of tracks are open in the backend + + each mixer has a media_type field, which describes the type of media track it is. + * JamTrack + * BackingTrack + * RecordingTrack + * MetronomeTrack + * "" - adhoc track (not supported visually) + + it is supposed to be the case that there are only one type of track open at a time, however, that's a business policy/logic + constraint; and may be buggy. **So, we should render whatever we have, so that it's obvious what's really going on.** + + so, let's group up all mixers by type, and then ask them to be rendered + ### + + recordingTrackMixers = [] + backingTrackMixers = [] + jamTrackMixers = [] + metronomeTrackMixers = [] + adhocTrackMixers = [] + + groupByType = (mixers, isLocalMixer) -> + for mixer in mixers + mediaType = mixer.media_type + groupId = mixer.group_id + + if mediaType == 'MetronomeTrack' || groupId == ChannelGroupIds.MetronomeGroup + # Metronomes come across with a blank media type, so check group_id: + metronomeTrackMixers.push(mixer) + else if mediaType == null || mediaType == "" || mediaType == 'RecordingTrack' + # additional check; if we can match an id in backing tracks or recorded backing track, + # we need to remove it from the recorded track set, but move it to the backing track set + + isJamTrack = false; + + if jamTracks + # check if the ID matches that of an open jam track + for jamTrack in jamTracks + if mixer.id == jamTrack.id + isJamTrack = true; + break + + if !isJamTrack && recordedJamTracks + # then check if the ID matches that of a open, recorded jam track + for recordedJamTrack in recordedJamTracks + if mixer.id == recordedJamTrack.id + isJamTrack = true + break + + if isJamTrack + jamTrackMixers.push(mixer) + else + isBackingTrack = false + if recordedBackingTracks + for recordedBackingTrack in recordedBackingTracks + if mixer.id == 'L' + recordedBackingTrack.client_track_id + isBackingTrack = true + break + + if backingTracks + for backingTrack in backingTracks + if mixer.id == 'L' + backingTrack.client_track_id + isBackingTrack = true + break + + if isBackingTrack + backingTrackMixers.push(mixer) + else + # couldn't resolve this as a JamTrack or Backing track, must be a normal recorded file + recordingTrackMixers.push(mixer) + + else if mediaType == 'PeerMediaTrack' || mediaType == 'BackingTrack' + backingTrackMixers.push(mixer) + else if mediaType == 'JamTrack' + jamTrackMixers.push(mixer); + else if mediaType == null || mediaType == "" || mediaType == 'RecordingTrack' + # mediaType == null is for backwards compat with older clients. Can be removed soon + recordingTrackMixers.push(mixer) + else + logger.warn("Unknown track type: " + mediaType) + adhocTrackMixers.push(mixer) + + groupByType(localMediaMixers, true); + groupByType(peerLocalMediaMixers, false); + + ### + if recordingTrackMixers.length > 0 + renderRecordingTracks(recordingTrackMixers) + + if backingTrackMixers.length > 0 + renderBackingTracks(backingTrackMixers) + + if jamTrackMixers.length > 0 + renderJamTracks(jamTrackMixers); + + if metronomeTrackMixers.length > 0 && @session.jamTracks() == null && @session.recordedJamTracks() == null + renderMetronomeTracks(metronomeTrackMixers); + + checkMetronomeTransition(); + ### + + if adhocTrackMixers.length > 0 + logger.warn("some tracks are open that we don't know how to show") + + mixersForGroupIds: (groupIds, mixMode) -> + foundMixers = [] + mixers = if mixMode == MIX_MODES.MASTER then @masterMixers else @personalMixers; + + for mixer in mixers + groupIdLen = groupIds.length + for i in groupIdLen + if mixer.group_id == groupIds[i] + foundMixers.push(mixer) + + foundMixers + + mixersForGroupId: (groupId, mixMode) -> + foundMixers = []; + mixers = if mixMode == MIX_MODES.MASTER then @masterMixers else @personalMixers; + for mixer in mixers + if mixer.group_id == groupId + foundMixers.push(mixer) + + foundMixers + + getMixer: (mixerId, mode) -> + mode = @mixMode unless mode? + + @allMixers[(if mode then 'M' else 'P') + mixerId] + + getMixerByTrackId: (trackId, mode) -> + mixerPair = @mixersByTrackId[trackId] + + return null unless mixerPair + + if mode == undefined + return mixerPair + + else + if mode == MIX_MODES.MASTER + return mixerPair.master + else + return mixerPair.personal + + + groupedMixersForClientId: (clientId, groupIds, usedMixers, mixMode) -> + foundMixers = {}; + mixers = if mixMode == MIX_MODES.MASTER then @masterMixers else @personalMixers; + + for mixer in mixers + if mixer.client_id == clientId + for groupId in groupIds + if mixer.group_id == groupId + if (mixer.groupId != ChannelGroupIds.UserMusicInputGroup) && !(mixer.id in usedMixers) + mixers = foundMixers[mixer.group_id] + if !mixers + mixers = [] + foundMixers[mixer.group_id] = mixers + mixers.push(mixer) + + foundMixers + + findMixerForTrack: (client_id, track, myTrack) -> + mixer = null # what is the best mixer for this track/client ID? + oppositeMixer = null # what is the corresponding mixer in the opposite mode? + vuMixer = null + muteMixer = null + + if myTrack + # when it's your track, look it up by the backend resource ID + mixer = @getMixerByTrackId(track.client_track_id, @mixMode) + vuMixer = mixer + muteMixer = mixer + + # sanity checks + if mixer && mixer.group_id != ChannelGroupIds.AudioInputMusicGroup + logger.error("found local mixer that was not of groupID: AudioInputMusicGroup", mixer) + + if mixer + # find the matching AudioInputMusicGroup for the opposite mode + oppositeMixer = @getMixerByTrackId(track.client_track_id, !@mixMode) + + if @mixMode == MIX_MODES.PERSONAL + muteMixer = oppositeMixer; # make the master mixer the mute mixer + + # sanity checks + if !oppositeMixer + logger.error("unable to find opposite mixer for local mixer", mixer) + else if oppositeMixer.group_id != ChannelGroupIds.AudioInputMusicGroup + logger.error("found local mixer in opposite mode that was not of groupID: AudioInputMusicGroup", mixer, oppositeMixer) + else + logger.debug("local track is not present: ", track, mixer) + else + switch @mixMode + when MIX_MODES.MASTER + + # when it's a remote track and in master mode, we should find the PeerAudioInputMusicGroup + mixer = @getMixerByTrackId(track.client_track_id, MIX_MODES.MASTER) + + # sanity check + if mixer && mixer.group_id != ChannelGroupIds.PeerAudioInputMusicGroup + logger.error("found remote mixer that was not of groupID: PeerAudioInputMusicGroup", mixer) + + vuMixer = mixer + muteMixer = mixer + + if mixer + # we should be able to find a UserMusicInputGroup for this clientId in personal mode + oppositeMixers = @groupedMixersForClientId(client_id, [ ChannelGroupIds.UserMusicInputGroup], {}, MIX_MODES.PERSONAL) + if oppositeMixers[ChannelGroupIds.UserMusicInputGroup] + oppositeMixer = oppositeMixers[ChannelGroupIds.UserMusicInputGroup][0] + + if !oppositeMixer + logger.error("unable to find UserMusicInputGroup corresponding to PeerAudioInputMusicGroup mixer", mixer ) + + when MIX_MODES.PERSONAL + mixers = @groupedMixersForClientId(client_id, [ ChannelGroupIds.UserMusicInputGroup], {}, MIX_MODES.PERSONAL) + if mixers[ChannelGroupIds.UserMusicInputGroup] + mixer = mixers[ChannelGroupIds.UserMusicInputGroup][0] + + vuMixer = mixer + muteMixer = mixer + + if mixer + # now grab the PeerAudioInputMusicGroup in master mode to satisfy the 'opposite' mixer + oppositeMixer = @getMixerByTrackId(track.client_track_id, MIX_MODES.MASTER) + if !oppositeMixer + logger.debug("unable to find a PeerAudioInputMusicGroup master mixer matching a UserMusicInput", track.client_track_id, @mixersByTrackId) + else if oppositeMixer.group_id != ChannelGroupIds.PeerAudioInputMusicGroup + logger.error("found remote mixer that was not of groupID: PeerAudioInputMusicGroup", mixer) + + vuMixer = oppositeMixer; # for personal mode, use the PeerAudioInputMusicGroup's VUs + + { + mixer: mixer, + oppositeMixer: oppositeMixer, + vuMixer: vuMixer, + muteMixer: muteMixer + } + + mute: (mixerId, mode, muting) -> + + mode = @mixMode unless mode? + + @fillTrackVolumeObject(mixerId, mode) + + context.trackVolumeObject.mute = muting + + context.jamClient.SessionSetControlState(mixerId, mode) + + # keep state of mixer in sync with backend + mixer = @getMixer(mixerId, mode) + mixer.mute = muting + + faderChanged: (data, mixerIds, groupId) -> + # media tracks are the only controls that sometimes set two mixers right now + hasMasterAndPersonalControls = mixerIds.length == 2 + + for mixerId, i in mixerIds + broadcast = !(data.dragging) # If fader is still dragging, don't broadcast + mode = undefined + if hasMasterAndPersonalControls + mode = if i == 0 then MIX_MODES.MASTER else MIX_MODES.PERSONAL + + mixer = @fillTrackVolumeObject(mixerId, mode, broadcast) + + @setMixerVolume(mixer, data.percentage) + + # keep state of mixer in sync with backend + mixer = @getMixer(mixer.id, mixer.mode) + mixer.volume_left = context.trackVolumeObject.volL + + if groupId == ChannelGroupIds.UserMusicInputGroup + # there may be other mixers with this same ID in the case of a Peer Music Stream, so update them as well + context.JK.FaderHelpers.setFaderValue(mixerId, data.percentage) + + initGain: (mixer) -> + gainPercent = context.JK.FaderHelpers.convertAudioTaperToPercent(mixer.volume_left) + context.JK.FaderHelpers.setFaderValue(mixer.id, gainPercent) + context.JK.FaderHelpers.showFader(mixer.id) + + panChanged: (data, mixerIds, groupId) -> + # media tracks are the only controls that sometimes set two mixers right now + for mixerId, i in mixerIds + broadcast = !(data.dragging) # If fader is still dragging, don't broadcast + mode = undefined + mixer = @fillTrackVolumeObject(mixerId, mode, broadcast) + + @setMixerPan(mixer, data.percentage) + + # keep state of mixer in sync with backend + mixer = @getMixer(mixer.id, mixer.mode) + mixer.pan = context.trackVolumeObject.pan + + initPan: (mixer) -> + panPercent= context.JK.PanHelpers.convertPanToPercent(mixer.pan) + context.JK.FaderHelpers.setFaderValue(mixer.id, panPercent, mixer.pan) + context.JK.FaderHelpers.showFader(mixer.id) + + setMixerPan: (mixer, panPercent) -> + + context.trackVolumeObject.pan = context.JK.PanHelpers.convertPercentToPan(panPercent); + context.jamClient.SessionSetControlState(mixer.id, mixer.mode); + + setMixerVolume: (mixer, volumePercent) -> + ### + // The context.trackVolumeObject has been filled with the mixer values + // that go with mixerId, and the range of that mixer + // has been set in currentMixerRangeMin-Max. + // All that needs doing is to translate the incoming percent + // into the real value ont the sliders range. Set Left/Right + // volumes on trackVolumeObject, and call SetControlState to stick. + ### + + context.trackVolumeObject.volL = context.JK.FaderHelpers.convertPercentToAudioTaper(volumePercent); + context.trackVolumeObject.volR = context.JK.FaderHelpers.convertPercentToAudioTaper(volumePercent); + + context.jamClient.SessionSetControlState(mixer.id, mixer.mode); + + percentFromMixerValue: (min, max, value) -> + try + range = Math.abs(max - min) + magnitude = value - min + percent = Math.round(100*(magnitude/range)) + percent + catch err + 0 + + + percentToMixerValue:(min, max, percent) -> + range = Math.abs(max - min); + multiplier = percent/100; # Change 85 into 0.85 + value = min + (multiplier * range); + + # Protect against percents < 0 and > 100 + if value < min + value = min; + + if value > max + value = max; + + return value; + + fillTrackVolumeObject: (mixerId, mode, broadcast) -> + _broadcast = true + if broadcast? + _broadcast = broadcast + + mixer = @getMixer(mixerId, mode) + context.trackVolumeObject.clientID = mixer.client_id + context.trackVolumeObject.broadcast = _broadcast + context.trackVolumeObject.master = mixer.master + context.trackVolumeObject.monitor = mixer.monitor + context.trackVolumeObject.mute = mixer.mute + context.trackVolumeObject.name = mixer.name + context.trackVolumeObject.record = mixer.record + context.trackVolumeObject.volL = mixer.volume_left + context.trackVolumeObject.pan = mixer.pan + + # today we treat all tracks as mono, but this is required to make a stereo track happy + # context.trackVolumeObject.volR = mixer.volume_right; + context.trackVolumeObject.volR = mixer.volume_left; + + context.trackVolumeObject.loop = mixer.loop; + # trackVolumeObject doesn't have a place for range min/max + @currentMixerRangeMin = mixer.range_low; + @currentMixerRangeMax = mixer.range_high; + mixer + + updateVU: (mixerId, value, isClipping) -> + selector = null + pureMixerId = mixerId.replace("_vul", "") + pureMixerId = pureMixerId.replace("_vur", "") + mixer = @getMixer(pureMixerId, @mixMode) + unless mixer + # try again, in the opposite mode (awful that this is necessary) + mixer = @getMixer(pureMixerId, !@mixMode) + + if mixer + if mixer.stereo # // stereo track + context.JK.VuHelpers.updateVU2('vul', mixer, value) + else + if mixerId.substr(-4) == "_vul" + # Do the left + context.JK.VuHelpers.updateVU2('vul', mixer, value) + # Do the right + context.JK.VuHelpers.updateVU2('vur', mixer, value) diff --git a/web/app/assets/javascripts/react-components/helpers/SessionHelper.js.coffee b/web/app/assets/javascripts/react-components/helpers/SessionHelper.js.coffee new file mode 100644 index 000000000..1d7a82f0d --- /dev/null +++ b/web/app/assets/javascripts/react-components/helpers/SessionHelper.js.coffee @@ -0,0 +1,80 @@ +context = window + +@SessionHelper = class SessionHelper + + constructor: (app, session) -> + @app = app + @session = session + + inSession: () -> + @session? + + participants: () -> + if @session + return @session.participants + else + [] + + # if any participant has the metronome open, then we say this session has the metronome open + isMetronomeOpen: () -> + metronomeOpen = false; + for participant in @participants() + if participant.metronome_open + metronomeOpen = true + break + + metronomeOpen + + isPlayingRecording: () -> + # this is the server's state; there is no guarantee that the local tracks + # requested from the backend will have corresponding track information + return !!(@session && @session.claimed_recording); + + recordedTracks: () -> + if @session && @session.claimed_recording + @session.claimed_recording.recording.recorded_tracks + else + null + + recordedBackingTracks: () -> + if @session && @session.claimed_recording + @session.claimed_recording.recording.recorded_backing_tracks + else + null + + backingTracks: () -> + backingTracks = [] + # this may be wrong if we loosen the idea that only one person can have a backing track open. + # but for now, the 1st person we find with a backing track open is all there is to find... + for participant in @participants() + if participant.backing_tracks.length > 0 + backingTracks = participant.backing_tracks + break + + backingTracks + + jamTracks: () -> + if @session && @session.jam_track + @session.jam_track.tracks.filter((track)-> + track.track_type == 'Track' + ) + else + null + + recordedJamTracks:() -> + if @session && @session.claimed_recording + @session.claimed_recording.recording.recorded_jam_track_tracks + else + null + + + getParticipant: (clientId) -> + found = null + for participant in @participants() + if participant.client_id == clientId + found = participant + break + + logger.warn('unable to find participant with clientId: ' + clientId) unless found + found + diff --git a/web/app/assets/javascripts/react-components/stores/AppStore.js.coffee b/web/app/assets/javascripts/react-components/stores/AppStore.js.coffee index 08b3b117c..bb5a11d98 100644 --- a/web/app/assets/javascripts/react-components/stores/AppStore.js.coffee +++ b/web/app/assets/javascripts/react-components/stores/AppStore.js.coffee @@ -10,6 +10,3 @@ logger = context.JK.logger @trigger(app) } ) - -context.JK.Stores.App = @AppStore - diff --git a/web/app/assets/javascripts/react-components/stores/MixerStore.js.coffee b/web/app/assets/javascripts/react-components/stores/MixerStore.js.coffee new file mode 100644 index 000000000..829f9c29c --- /dev/null +++ b/web/app/assets/javascripts/react-components/stores/MixerStore.js.coffee @@ -0,0 +1,109 @@ +context = window +logger = context.JK.logger +MIX_MODES = context.JK.MIX_MODES; + +@MixerStore = Reflux.createStore( + { + init: -> + # Register with the app store to get @app + this.listenTo(context.AppStore, this.onAppInit); + this.listenTo(context.SessionStore, this.onSessionChange) + this.listenTo(context.MixerActions.mute, this.onMute) + this.listenTo(context.MixerActions.faderChanged, this.onFaderChanged) + this.listenTo(context.MixerActions.initGain, this.onInitGain) + this.listenTo(context.MixerActions.initPan, this.onInitPan) + this.listenTo(context.MixerActions.panChanged, this.onPanChanged) + this.listenTo(context.MixerActions.mixersChanged, this.onMixersChanged) + + context.JK.HandleVolumeChangeCallback2 = @handleVolumeChangeCallback + context.JK.HandleMetronomeCallback2 = @handleMetronomeCallback + context.JK.HandleBridgeCallback2 = @handleBridgeCallback + context.JK.HandleBackingTrackSelectedCallback2 = @handleBackingTrackSelectedCallback + + handleVolumeChangeCallback: () -> + logger.debug("volume change") + + handleMetronomeCallback: () -> + logger.debug("metronome callback") + + handleBridgeCallback: (vuData) -> + + eventName = null + mixerId = null + value = null + vuInfo = null + + for vuInfo in vuData + eventName = vuInfo[0]; + vuVal = 0.0; + if eventName == "vu" + mixerId = vuInfo[1]; + leftValue = vuInfo[2]; + leftClipping = vuInfo[3]; + rightValue = vuInfo[4]; + rightClipping = vuInfo[5]; + # TODO - no guarantee range will be -80 to 20. Get from the + # GetControlState for this mixer which returns min/max + # value is a DB value from -80 to 20. Convert to float from 0.0-1.0 + + @mixers.updateVU(mixerId + "_vul", (leftValue + 80) / 80, leftClipping) + @mixers.updateVU(mixerId + "_vur", (rightValue + 80) / 80, rightClipping) + + + handleBackingTrackSelectedCallback: () -> + logger.debug("backing track selected") + + onAppInit: (@app) -> + @gearUtils = context.JK.GearUtilsInstance + @sessionUtils = context.JK.SessionUtils + + onSessionChange: (session) -> + + @session = session + + masterMixers = context.jamClient.SessionGetAllControlState(true); + personalMixers = context.jamClient.SessionGetAllControlState(false); + + # TODO: grab correct mix mode , line 870 sessionModel.js + @mixers = new context.MixerHelper(session, masterMixers, personalMixers, MIX_MODES.PERSONAL) + + this.trigger({session: @session, mixers: @mixers}) + + + onMute: (mixers, muting) -> + + for mixer in mixers + @mixers.mute(mixer.id, mixer.mode, muting); + + # simulate a state change to cause a UI redraw + this.trigger({session: @session, mixers: @mixers}) + + onFaderChanged: (data, mixerIds, groupId) -> + + @mixers.faderChanged(data, mixerIds, groupId) + + this.trigger({session: @session, mixers: @mixers}) + + onPanChanged: (data, mixerIds, groupId) -> + @mixers.panChanged(data, mixerIds, groupId) + + this.trigger({session: @session, mixers: @mixers}) + + onInitGain: (mixer) -> + @mixers.initGain(mixer) + + onInitPan: (mixer) -> + @mixers.initPan(mixer) + + onMixersChanged: (type, text) -> + + if @mixers + masterMixers = context.jamClient.SessionGetAllControlState(true); + personalMixers = context.jamClient.SessionGetAllControlState(false); + + # TODO: grab correct mix mode , line 870 sessionModel.js + @mixers.updateMixers(type, text, masterMixers, personalMixers) + + this.trigger({session: @session, mixers: @mixers}) + } +) diff --git a/web/app/assets/javascripts/react-components/stores/SessionMyTracksStore.js.coffee b/web/app/assets/javascripts/react-components/stores/SessionMyTracksStore.js.coffee new file mode 100644 index 000000000..d50dce041 --- /dev/null +++ b/web/app/assets/javascripts/react-components/stores/SessionMyTracksStore.js.coffee @@ -0,0 +1,21 @@ +$ = jQuery +context = window +logger = context.JK.logger + +@SessionMyTracksStore = Reflux.createStore( + { + + init: -> + # Register with the app store to get @app + this.listenTo(context.AppStore, this.onAppInit); + this.listenTo(context.MixerStore, this.onSessionMixerChange) + + onAppInit: (@app) -> + @gearUtils = context.JK.GearUtilsInstance + @sessionUtils = context.JK.SessionUtils + + onSessionMixerChange: (sessionMixers) -> + + this.trigger(sessionMixers) + } +) diff --git a/web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee b/web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee new file mode 100644 index 000000000..da5fa7642 --- /dev/null +++ b/web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee @@ -0,0 +1,423 @@ +$ = jQuery +context = window +logger = context.JK.logger +rest = context.JK.Rest() +EVENTS = context.JK.EVENTS; + +SessionActions = @SessionActions + +@SessionStore = Reflux.createStore( + { + listenables: SessionActions + + userTracks: null # comes from the backend + currentSessionId: null + currentSession: null + currentOrLastSession: null + startTime: null + currentParticipants: {} + participantsEverSeen: {} + users: {} # // User info for session participants + requestingSessionRefresh: false + pendingSessionRefresh: false + sessionPageEnterTimeout: null + sessionPageEnterDeferred: null + gearUtils: null + sessionUtils: null + + + init: -> + # Register with the app store to get @app + this.listenTo(context.AppStore, this.onAppInit); + + onAppInit: (@app) -> + @gearUtils = context.JK.GearUtilsInstance + @sessionUtils = context.JK.SessionUtils + + + onJoinSession: (sessionId) -> + + # initialize webcamViewer + if gon.global.video_available && gon.global.video_available != "none" + webcamViewer.beforeShow() + + # double-check that we are connected to the server via websocket + + return unless @ensureConnected() + + # update the session data to be empty + @updateCurrentSession(null) + + # start setting data for this new session + @currentSessionId = sessionId + @startTime = new Date().getTime() + + # let's find out the public/private nature of this session, + # so that we can decide whether we need to validate the audio profile more aggressively + rest.getSessionHistory(@currentSessionId) + .done((musicSession)=> + musicianAccessOnJoin = musicSession.musician_access + + shouldVerifyNetwork = musicSession.musician_access; + + @gearUtils.guardAgainstInvalidConfiguration(@app, shouldVerifyNetwork).fail(() => + SessionActions.leaveSession.trigger({location: '/client#/home'}) + ).done(() => + result = @sessionUtils.SessionPageEnter(); + + @gearUtils.guardAgainstActiveProfileMissing(@app, result) + .fail((data) => + leaveBehavior = {} + + if data && data.reason == 'handled' + if data.nav == 'BACK' + leaveBehavior.location = -1 + else + leaveBehavior.location = data.nav + else + leaveBehavior.location = '/client#/home'; + + SessionActions.leaveSession.trigger(leaveBehavior) + ).done(() => + @waitForSessionPageEnterDone() + .done((userTracks) => + @userTracks = userTracks + + @ensureAppropriateProfile(musicianAccessOnJoin) + .done(() => + logger.debug("user has passed all session guards") + @joinSession() + ) + .fail((result) => + unless result.controlled_location + SessionActions.leaveSession.trigger({location: "/client#/home"}) + ) + ).fail((data) => + if data == "timeout" + context.JK.alertSupportedNeeded('The audio system has not reported your configured tracks in a timely fashion.') + else if data == 'session_over' + # do nothing; session ended before we got the user track info. just bail + logger.debug("session is over; bailing") + else + context.JK.alertSupportedNeeded('Unable to determine configured tracks due to reason: ' + data) + + SessionActions.leaveSession.trigger({location: '/client#/home'}) + ) + ) + ) + ) + .fail(() => + logger.error("unable to fetch session history") + ) + + waitForSessionPageEnterDone: () -> + @sessionPageEnterDeferred = $.Deferred() + + # see if we already have tracks; if so, we need to run with these + inputTracks = context.JK.TrackHelpers.getUserTracks(context.jamClient) + + logger.debug("isNoInputProfile", @gearUtils.isNoInputProfile()) + if inputTracks.length > 0 || @gearUtils.isNoInputProfile() + logger.debug("on page enter, tracks are already available") + @sessionPageEnterDeferred.resolve(inputTracks) + deferred = @sessionPageEnterDeferred + @sessionPageEnterDeferred = null + return deferred + + @sessionPageEnterTimeout = setTimeout(()=> + if @sessionPageEnterTimeout + if @sessionPageEnterDeferred + @sessionPageEnterDeferred.reject('timeout') + @sessionPageEnterDeferred = null + @sessionPageEnterTimeout = null + , 5000) + + @sessionPageEnterDeferred + + ensureAppropriateProfile: (musicianAccess) -> + deferred = new $.Deferred(); + if musicianAccess + deferred = context.JK.guardAgainstSinglePlayerProfile(@app) + else + deferred.resolve(); + deferred + + joinSession: () -> + context.jamClient.SessionRegisterCallback("JK.HandleBridgeCallback2"); + #context.jamClient.RegisterRecordingCallbacks("JK.HandleRecordingStartResult", "JK.HandleRecordingStopResult", "JK.HandleRecordingStarted", "JK.HandleRecordingStopped", "JK.HandleRecordingAborted"); + context.jamClient.SessionSetConnectionStatusRefreshRate(1000); + #context.JK.HelpBubbleHelper.jamtrackGuideSession($screen.find('li.open-a-jamtrack'), $screen) + + # subscribe to events from the recording model + @recordingRegistration() + + # tell the server we want to join + + rest.joinSession({ + client_id: @app.clientId, + ip_address: context.JK.JamServer.publicIP, + as_musician: true, + tracks: @userTracks, + session_id: @currentSessionId, + audio_latency: context.jamClient.FTUEGetExpectedLatency().latency + }) + .done((response) => + unless @inSession() + # the user has left the session before they got joined. We need to issue a leave again to the server to make sure they are out + logger.debug("user left before fully joined to session. telling server again that they have left") + @leaveSessionRest(response.id) + return + + logger.debug("calling jamClient.JoinSession"); + # on temporary disconnect scenarios, a user may already be in a session when they enter this path + # so we avoid double counting + unless @alreadyInSession() + if response.music_session.participant_count == 1 + context.JK.GA.trackSessionMusicians(context.JK.GA.SessionCreationTypes.create); + else + context.JK.GA.trackSessionMusicians(context.JK.GA.SessionCreationTypes.join); + + #recordingModel.reset(); + + context.jamClient.JoinSession({sessionID: response.id}); + + @refreshCurrentSession(true); + + context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SESSION_JOIN, @trackChanges); + context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SESSION_DEPART, @trackChanges); + context.JK.JamServer.registerMessageCallback(context.JK.MessageType.TRACKS_CHANGED, @trackChanges); + context.JK.JamServer.registerMessageCallback(context.JK.MessageType.HEARTBEAT_ACK, @trackChanges); + + $(document).trigger(EVENTS.SESSION_STARTED, {session: {id: @currentSessionId}}) if document + + @handleAutoOpenJamTrack() + ) + .fail((xhr) => + @updateCurrentSession(null) + + if xhr.status == 404 + # we tried to join the session, but it's already gone. kick user back to join session screen + leaveBehavior = + location: "/client#/findSession" + notify: + title: "Unable to Join Session", + text: " The session you attempted to join is over." + SessionActions.leaveSession.trigger(leaveBehavior) + else if xhr.status == 422 + response = JSON.parse(xhr.responseText); + if response["errors"] && response["errors"]["tracks"] && response["errors"]["tracks"][0] == "Please select at least one track" + @app.notifyAlert("No Inputs Configured", $('You will need to reconfigure your audio device.')) + + else if response["errors"] && response["errors"]["music_session"] && response["errors"]["music_session"][0] == ["is currently recording"] + + leaveBehavior = + location: "/client#/findSession" + notify: + title: "Unable to Join Session" + text: "The session is currently recording." + SessionActions.leaveSession.trigger(leaveBehavior) + else + @app.notifyServerError(xhr, 'Unable to Join Session'); + else + @app.notifyServerError(xhr, 'Unable to Join Session'); + ) + + trackChanges: (header, payload) -> + if @currentTrackChanges < payload.track_changes_counter + # we don't have the latest info. try and go get it + logger.debug("track_changes_counter = stale. refreshing...") + @refreshCurrentSession(); + + else + if header.type != 'HEARTBEAT_ACK' + # don't log if HEARTBEAT_ACK, or you will see this log all the time + logger.info("track_changes_counter = fresh. skipping refresh...", header, payload) + + handleAutoOpenJamTrack: () -> + jamTrack = @sessionUtils.grabAutoOpenJamTrack(); + if jamTrack + # give the session to settle just a little (call a timeout of 1 second) + setTimeout(()=> + # tell the server we are about to open a jamtrack + rest.openJamTrack({id: context.JK.CurrentSessionModel.id(), jam_track_id: jamTrack.id}) + .done((response) => + logger.debug("jamtrack opened") + # now actually load the jamtrack + # TODO + # context.JK.CurrentSessionModel.updateSession(response); + # loadJamTrack(jamTrack); + ) + .fail((jqXHR) => + @app.notifyServerError(jqXHR, "Unable to Open JamTrack For Playback") + ) + , 1000) + + inSession: () -> + !!@currentSessionId + + alreadyInSession: () -> + inSession = false + for participant in @participants() + if participant.user.id == context.JK.currentUserId + inSession = true + break + + participants: () -> + if @currentSession + @currentSession.participants; + else + [] + + refreshCurrentSession: (force) -> + logger.debug("refreshCurrentSession(force=true)") if force + + @refreshCurrentSessionRest(force) + + refreshCurrentSessionRest: (force) -> + unless @inSession() + logger.debug("refreshCurrentSession skipped: ") + return + + if @requestingSessionRefresh + # if someone asks for a refresh while one is going on, we ask for another to queue up + logger.debug("queueing refresh") + @pendingSessionRefresh = true; + else + @requestingSessionRefresh = true + rest.getSession(@currentSessionId) + .done((response) => + @updateSessionInfo(response, force) + ) + .fail((jqXHR) => + if jqXHR.status != 404 + @app.notifyServerError(jqXHR, "Unable to refresh session data") + else + logger.debug("refreshCurrentSessionRest: could not refresh data for session because it's gone") + ) + .always(() => + @requestingSessionRefresh = false + if @pendingSessionRefresh + # and when the request is done, if we have a pending, fire it off again + pendingSessionRefresh = false + @refreshCurrentSessionRest(force) + ) + + updateSessionInfo: (session, force) -> + if force == true || @currentTrackChanges < session.track_changes_counter + logger.debug("updating current track changes from %o to %o", @currentTrackChanges, session.track_changes_counter) + @currentTrackChanges = session.track_changes_counter; + @sendClientParticipantChanges(@currentSession, session); + @updateCurrentSession(session); + #if(callback != null) { + # callback(); + #} + else + logger.info("ignoring refresh because we already have current: " + @currentTrackChanges + ", seen: " + session.track_changes_counter); + + + leaveSessionRest: () -> + rest.deleteParticipant(@app.clientId); + + sendClientParticipantChanges: (oldSession, newSession) -> + joins = [] + leaves = [] + leaveJoins = []; # Will hold JamClientParticipants + + oldParticipants = []; # will be set to session.participants if session + oldParticipantIds = {}; + newParticipants = []; + newParticipantIds = {}; + + if oldSession && oldSession.participants + for oldParticipant in oldSession.participants + oldParticipantIds[oldParticipant.client_id] = oldParticipant + + if newSession && newSession.participants + for newParticipant in newSession.participants + newParticipantIds[newParticipant.client_id] = newParticipant + + for client_id, participant of newParticipantIds + # grow the 'all participants seen' list + unless (client_id in @participantsEverSeen) + @participantsEverSeen[client_id] = participant; + + + if client_id in oldParticipantIds + # if the participant is here now, and here before, there is still a chance we missed a + # very fast leave/join. So check if joined_session_at is different + if oldParticipantIds[client_id].joined_session_at != participant.joined_session_at + leaveJoins.push(participant) + else + # new participant id that's not in old participant ids: Join + joins.push(participant); + + for client_id, participant of oldParticipantIds + unless (client_id in newParticipantIds) + # old participant id that's not in new participant ids: Leave + leaves.push(participant); + + for i, v of joins + if v.client_id != @app.clientId + @participantJoined(newSession, v) + + for i,v of leaves + if v.client_id != @app.clientId + @participantLeft(newSession, v) + + for i,v of leaveJoins + if v.client_id != @app.clientId + logger.debug("participant had a rapid leave/join") + @participantLeft(newSession, v) + @participantJoined(newSession, v) + + participantJoined: (newSession, participant) -> + logger.debug("jamClient.ParticipantJoined", participant.client_id) + context.jamClient.ParticipantJoined(newSession, @toJamClientParticipant(participant)); + @currentParticipants[participant.client_id] = {server: participant, client: {audio_established: null}} + + participantLeft: (newSession, participant) -> + logger.debug("jamClient.ParticipantLeft", participant.client_id) + context.jamClient.ParticipantLeft(newSession, @toJamClientParticipant(participant)); + delete @currentParticipants[participant.client_id] + + toJamClientParticipant: (participant) -> + { + userID: "", + clientID: participant.client_id, + tcpPort: 0, + udpPort: 0, + localIPAddress: participant.ip_address, # ? + globalIPAddress: participant.ip_address, # ? + latency: 0, + natType: "" + } + + recordingRegistration: () -> + logger.debug("recording registration not hooked up yet") + + updateCurrentSession: (sessionData) -> + if sessionData != null + @currentOrLastSession = sessionData + + @currentSession = sessionData + + console.log("SESSION CHANGED", sessionData) + + this.trigger(new context.SessionHelper(@app, @currentSession)) + + ensureConnected: () -> + unless context.JK.JamServer.connected + leaveBehavior = + location: '/client#/home' + notify: + title: "Not Connected" + text: 'To create or join a session, you must be connected to the server.' + + SessionActions.leaveSession.trigger(leaveBehavior) + + context.JK.JamServer.connected + + + + } +) \ No newline at end of file diff --git a/web/app/assets/javascripts/session.js b/web/app/assets/javascripts/session.js index a00991840..1175e8ad8 100644 --- a/web/app/assets/javascripts/session.js +++ b/web/app/assets/javascripts/session.js @@ -1957,7 +1957,7 @@ // Given a mixerID and a value between 0.0-1.0, // light up the proper VU lights. - function _updateVU(mixerId, value, isClipping) { + function _updateVU(mixerId, value, isClipping) { // Special-case for mono tracks. If mono, and it's a _vul id, // update both sides, otherwise do nothing. @@ -2193,8 +2193,8 @@ // TODO - no guarantee range will be -80 to 20. Get from the // GetControlState for this mixer which returns min/max // value is a DB value from -80 to 20. Convert to float from 0.0-1.0 - _updateVU(mixerId + "_vul", (leftValue + 80) / 100, leftClipping); - _updateVU(mixerId + "_vur", (rightValue + 80) / 100, rightClipping); + _updateVU(mixerId + "_vul", (leftValue + 80) / 80, leftClipping); + _updateVU(mixerId + "_vur", (rightValue + 80) / 80, rightClipping); } else if(eventName === 'connection_status') { var mixerId = vuInfo[1]; diff --git a/web/app/assets/javascripts/trackHelpers.js b/web/app/assets/javascripts/trackHelpers.js index b434d80a9..ec9c8771d 100644 --- a/web/app/assets/javascripts/trackHelpers.js +++ b/web/app/assets/javascripts/trackHelpers.js @@ -7,6 +7,8 @@ "use strict"; + var ChannelGroupIds = context.JK.ChannelGroupIds + context.JK = context.JK || {}; // As these are helper functions, just have a single @@ -20,7 +22,7 @@ var userTracks = context.JK.TrackHelpers.getUserTracks(jamClient, allTracks); var backingTracks = context.JK.TrackHelpers.getBackingTracks(jamClient, allTracks); - var metronomeTracks = context.JK.TrackHelpers.getTracks(jamClient, 12); + var metronomeTracks = context.JK.TrackHelpers.getTracks(jamClient, ChannelGroupIds.MetronomeGroup); return { userTracks: userTracks, @@ -51,7 +53,7 @@ // allTracks is the result of SessionGetAllControlState; as an optimization getBackingTracks: function(jamClient, allTracks) { - var mediaTracks = context.JK.TrackHelpers.getTracks(jamClient, 4, allTracks); + var mediaTracks = context.JK.TrackHelpers.getTracks(jamClient, ChannelGroupIds.MediaTrackGroup, allTracks); var backingTracks = [] context._.each(mediaTracks, function(mediaTrack) { @@ -80,7 +82,7 @@ var localMusicTracks = []; var i; - localMusicTracks = context.JK.TrackHelpers.getTracks(jamClient, 2, allTracks); + localMusicTracks = context.JK.TrackHelpers.getTracks(jamClient, ChannelGroupIds.AudioInputMusicGroup, allTracks); var trackObjects = []; diff --git a/web/app/assets/javascripts/utils.js b/web/app/assets/javascripts/utils.js index cd44c1a76..af46ad1b1 100644 --- a/web/app/assets/javascripts/utils.js +++ b/web/app/assets/javascripts/utils.js @@ -206,6 +206,70 @@ }) return $element; } + + /** Creates a hover element that does not dissappear when the user mouses over the hover. + * + * @param $element + * @param text + * @param options + */ + context.JK.interactReactBubble = function($element, reactElementName, reactProps, options) { + + if(!options) options = {}; + + function waitForBubbleHover($bubble) { + $bubble.hoverIntent({ + over: function() { + if(timeout) { + clearTimeout(timeout); + timeout = null; + } + }, + out: function() { + //$element.btOff(); + }}); + } + + var timeout = null; + + + options.trigger = 'none' + options.clickAnywhereToClose = true + options.preShow = function(container) { + var reactElement = context[reactElementName] + if(!reactElementName) { + throw "unknown react element" + reactElementName + } + var element = React.createElement(reactElement, reactProps); + + var $container = $(container) + + React.render(element, $container.find('.react-holder').get(0)) + } + options.postShow = function(container) { + + if(timeout) { + clearTimeout(timeout); + timeout = null; + } + waitForBubbleHover($(container)) + timeout = setTimeout(function() {/**$element.btOff()*/}, 3000) + } + + $element.hoverIntent({ + over: function() { + $element.btOn(); + }, + out: function() { + + }}); + + options.cssStyles = {} + options.padding = 0; + context.JK.hoverBubble($element, '
', options) + return $element; + } + /** * Associates a bubble on hover (by default) with the specified $element, using jquery.bt.js (BeautyTips) * @param $element The element that should show the bubble when hovered diff --git a/web/app/assets/javascripts/vuHelpers.js b/web/app/assets/javascripts/vuHelpers.js index e9b4c70ce..07e47d7e1 100644 --- a/web/app/assets/javascripts/vuHelpers.js +++ b/web/app/assets/javascripts/vuHelpers.js @@ -93,6 +93,53 @@ } }) + }, + + /** + * Given a selector representing a container for a VU meter and + * a value between 0.0 and 1.0, light the appropriate lights. + */ + updateVU2: function (side, mixer, value) { + // There are 13 VU lights. Figure out how many to + // light based on the incoming value. + + var $selector = $('.' + side + '-' + mixer.id) + + $selector.each(function() { + var $table = $(this) + var horizontal = $table.is('.horizontal') + + var lightCount = Number($table.attr('data-light-count')) + + var i = 0; + var state = 'on'; + var lights = Math.round(value * lightCount); + var redSwitch = Math.round(lightCount * 0.6666667); + + var $light = null; + var colorClass = 'vu-green-'; + var thisLightSelector = null; + + // Remove all light classes from all lights + var allLightsSelector = $table.find('td'); + $(allLightsSelector).removeClass('vu-green-off vu-green-on vu-red-off vu-red-on'); + + // Set the lights + for (i = 0; i < lightCount; i++) { + colorClass = 'vu-green-'; + state = 'on'; + if (i >= redSwitch) { + colorClass = 'vu-red-'; + } + if (i >= lights) { + state = 'off'; + } + + var lightIndex = horizontal ? i : lightCount - i - 1; + allLightsSelector.eq(lightIndex).addClass(colorClass + state); + } + }) + } }; diff --git a/web/app/assets/stylesheets/client/common.css.scss b/web/app/assets/stylesheets/client/common.css.scss index 75edd1bc1..2f242f0b0 100644 --- a/web/app/assets/stylesheets/client/common.css.scss +++ b/web/app/assets/stylesheets/client/common.css.scss @@ -55,6 +55,14 @@ $poor: #980006; $error: #980006; $fair: #cc9900; +$labelFontFamily: Arial, Helvetica, sans-serif; +$labelFontSize: 12px; + +@mixin labelFont { + font-family: $labelFontFamily; + font-size: $labelFontSize; +} + @mixin border_box_sizing { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; 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 f8cce28ea..796e053e2 100644 --- a/web/app/assets/stylesheets/client/react-components/SessionScreen.css.scss +++ b/web/app/assets/stylesheets/client/react-components/SessionScreen.css.scss @@ -7,10 +7,11 @@ color: #fff; font-weight: 600; font-size: 24px; + margin-bottom: 15px; } .tracks { - position:absolute; + position: absolute; @include border_box_sizing; top: 71px; bottom: 0; @@ -19,58 +20,326 @@ .session-my-tracks, .session-other-tracks, .session-media-tracks, .session-notifications { @include border_box_sizing; - float:left; - width:25%; + float: left; + width: 25%; border-right: 1px solid #4c4c4c; - padding:15px; - height:100%; + padding: 15px; + height: 100%; + margin-bottom: 15px; } .session-notifications { - border-right-width:0; + border-right-width: 0; } - .in-session-controls { - width:100%; - padding:11px 0px 11px 0px; - background-color:#4c4c4c; - min-height:20px; - position:relative; - min-width:690px; + width: 100%; + padding: 11px 0px 11px 0px; + background-color: #4c4c4c; + min-height: 20px; + position: relative; + min-width: 690px; .label { - float:left; - font-size:12px; - color:#ccc; + float: left; + font-size: 12px; + color: #ccc; margin: 0px 0px 0px 4px; } .block { - float:left; + float: left; margin: 6px 8px 0px 8px; } a { img { - margin-right:3px; + margin-right: 3px; } } } .session-tracks-scroller { - position:relative; - overflow-x:hidden; - overflow-y:auto; - width:100%; + position: relative; + overflow-x: hidden; + overflow-y: auto; + width: 100%; } p { - line-height:125%; - margin:0; + line-height: 125%; + margin: 0; + } + + .session-my-tracks { + .session-track { + float:left; + margin: 10px 0; + padding: 6px 6px 6px 10px; + color: $ColorTextTypical; + background-color: #242323; + border-radius: 6px; + min-height: 76px; + max-width: 210px; + @include border_box_sizing; + } } + .react-holder { + &.SessionTrackVolumeHover { + height:331px; + width:235px; + + .session-track { + float:left; + background-color: #242323; + border-radius: 4px; + display: inline-block; + height: 300px; + margin-right: 14px; + position: relative; + width: 70px; + margin-top:19px; + margin-left:24px; + } + + .track-icon-mute { + float:none; + position: absolute; + top: 246px; + left: 29px; + } + + .track-gain { + position:absolute; + width:28px; + height:209px; + top:32px; + left:23px; + } + + .fader { + height:209px; + } + + .handle { + bottom:0%; + display:none; + } + + .textual-help { + float:left; + width:100px; + } + + p { + font-size:12px; + padding:0; + margin:16px 0 0; + line-height:125%; + + &:nth-child(1) { + margin-top:19px; + } + } + + .icheckbox_minimal { + position:absolute; + top: 271px; + left: 12px; + } + + input { + position:absolute; + top: 271px; + left: 12px; + } + + label { + @include labelFont; + position:absolute; + top:273px; + left:34px + } + } + &.SessionTrackPanHover { + width:331px; + height:197px; + padding:15px; + @include border_box_sizing; + + .session-pan { + .textual-help { + float:left; + width:100px; + } + } + + p { + font-size:12px; + padding:0; + line-height:125%; + } + .track-pan { + background-color: #242323; + border-radius: 4px; + display: inline-block; + height: 70px; + position: relative; + width: 300px; + margin-top:15px; + } + .fader { + position:absolute; + width:205px; + height:24px; + top:34px; + left:44px; + background-image: url('/assets/content/bkg_slider_gain_horiz_24.png'); + } + .handle { + display:none; + + img { + position:absolute; + left:-5px; + } + } + .left-label { + @include labelFont; + position:absolute; + left:13px; + top:40px; + } + .right-label { + @include labelFont; + position:absolute; + right:12px; + top:40px; + } + .floater { + width:20px; + text-align:center; + top:-22px; + left:-8px; + @include labelFont; + position:absolute; + } + } + } + + + .session-track { + + .name { + width: 100%; + margin-bottom: 6px; + @include labelFont; + } + + .track-avatar { + float: left; + padding: 1px; + width: 44px; + height: 44px; + background-color: #ed3618; + -webkit-border-radius: 22px; + -moz-border-radius: 22px; + border-radius: 22px; + + img { + width: 44px; + height: 44px; + -webkit-border-radius: 22px; + -moz-border-radius: 22px; + border-radius: 22px; + } + } + + .track-instrument { + float: left; + padding: 1px; + margin-left: 5px; + } + } + + table.vu { + float: left; + + td { + border: 3px solid #242323; + } + } + + .track-controls { + margin-top: 2px; + margin-left: 10px; + float:left + } + + .track-buttons { + margin-top:22px; + padding:0 0 0 3px; + } + + .track-icon-mute { + float:left; + position:relative; + top:0; + left:0; + } + + .track-icon-pan { + float:left; + cursor: pointer; + width: 20px; + height: 20px; + background-image:url('/assets/content/icon_pan.png'); + background-repeat:no-repeat; + text-align: center; + margin-left:10px; + } + + .track-icon-equalizer { + float:left; + cursor: pointer; + width: 20px; + height: 20px; + background-image:url('/assets/content/icon_equalizer.png'); + background-repeat:no-repeat; + text-align: center; + margin-left:7px; + } + + .session-track-list-enter { + opacity: 0.01; + transition: opacity .5s ease-in; + + &.session-track-list-enter-active { + opacity: 1; + } + } + + .session-track-list-leave { + opacity:1; + transition: opacity .5s ease-in; + + &.session-track-list-leave-active { + opacity: 0.01; + } + } + + .session-track-settings { + height:18px; + cursor:pointer; + + span { + top: -4px; + position: relative; + left:3px; + } + } } \ No newline at end of file diff --git a/web/app/views/clients/_faders.html.erb b/web/app/views/clients/_faders.html.erb index fd4a33046..0f3fab350 100644 --- a/web/app/views/clients/_faders.html.erb +++ b/web/app/views/clients/_faders.html.erb @@ -2,8 +2,8 @@