(function(context,$) { "use strict"; context.JK = context.JK || {}; context.JK.SessionScreen = function(app) { var TEMPOS = context.JK.TEMPOS; var EVENTS = context.JK.EVENTS; var MIX_MODES = context.JK.MIX_MODES; var NAMED_MESSAGES = context.JK.NAMED_MESSAGES; var gearUtils = context.JK.GearUtils; var sessionUtils = context.JK.SessionUtils; var modUtils = context.JK.ModUtils; var logger = context.JK.logger; var self = this; var webcamViewer = new context.JK.WebcamViewer() var ChannelGroupIds = context.JK.ChannelGroupIds; var defaultParticipant = { tracks: [{ instrument_id: "unknown" }], user: { first_name: 'Unknown', last_name: 'User', photo_url: null } }; // Be sure to copy/extend these instead of modifying in place var trackVuOpts = { vuType: "vertical", lightCount: 13, lightWidth: 3, lightHeight: 17 }; // Must add faderId key to this var trackFaderOpts = { faderType: "vertical", height: 83 }; var METRO_SOUND_LOOKUP = { 0 : "BuiltIn", 1 : "SineWave", 2 : "Beep", 3 : "Click", 4 : "Kick", 5 : "Snare", 6 : "MetroFile" } var sessionModel = null; var sessionId; var tracks = {}; var myTracks = []; var masterMixers = []; var personalMixers = []; var allMixers = {}; var mixersByResourceId = {}; var mixersByTrackId = {}; var configureTrackDialog; var addNewGearDialog; var localRecordingsDialog = null; var recordingFinishedDialog = null; var friendSelectorDialog = null; var inviteMusiciansUtil = null; var screenActive = false; var currentMixerRangeMin = null; var currentMixerRangeMax = null; var lookingForMixersCount = 0; var lookingForMixersTimer = null; var lookingForMixers = []; var $recordingTimer = null; var recordingTimerInterval = null; var startTimeDate = null; var startingRecording = false; // double-click guard var claimedRecording = null; var backing_track_path = null; var jamTrack = null; var musicianAccessOnJoin; // was this a private or public session when the user tried to joined? var metronomeMixer = null; var playbackControls = null; var promptLeave = false; var rateSessionDialog = null; var friendInput = null; var sessionPageDone = null; var metroTempo = 120; var metroCricket = false; var metroSound = "Beep"; var $recordingManagerViewer = null; var $screen = null; var $mixModeDropdown = null; var $templateMixerModeChange = null; var $myTracksNoTracks = null; var $otherAudioContainer = null; var $myTracksContainer = null; var $liveTracksContainer = null; var downloadJamTrack = null; var $closePlaybackRecording = null; var $openBackingTrack = null; var $metronomePlaybackSelect = null; var $metronomePlaybackHelp = null; var $templatePendingMetronome = null; var $myTracks = null; var $liveTracks = null; var $audioTracks = null; var $fluidTracks = null; var $voiceChat = null; var $openFtue = null; var $tracksHolder = null; var mediaTrackGroups = [ChannelGroupIds.MediaTrackGroup, ChannelGroupIds.JamTrackGroup, ChannelGroupIds.MetronomeGroup]; var muteBothMasterAndPersonalGroups = [ChannelGroupIds.AudioInputMusicGroup, ChannelGroupIds.MediaTrackGroup, ChannelGroupIds.JamTrackGroup, ChannelGroupIds.MetronomeGroup]; var rest = context.JK.Rest(); var RENDER_SESSION_DELAY = 750; // When I need to render a session, I have to wait a bit for the mixers to be there. function beforeShow(data) { sessionId = data.id; if(!sessionId) { window.location = '/client#/home'; } promptLeave = true; $myTracksContainer.empty(); displayDoneRecording(); // assumption is that you can't join a recording session, so this should be safe var shareDialog = new JK.ShareDialog(context.JK.app, sessionId, "session"); shareDialog.initialize(context.JK.FacebookHelperInstance); if(gon.global.video_available && gon.global.video_available!="none") { webcamViewer.beforeShow() } } function beforeDisconnect() { return { freezeInteraction: true }; } function initializeSession() { // Subscribe for callbacks on audio events context.jamClient.SessionRegisterCallback("JK.HandleBridgeCallback"); context.jamClient.RegisterRecordingCallbacks("JK.HandleRecordingStartResult", "JK.HandleRecordingStopResult", "JK.HandleRecordingStarted", "JK.HandleRecordingStopped", "JK.HandleRecordingAborted"); context.jamClient.SessionSetConnectionStatusRefreshRate(1000); // If you load this page directly, the loading of the current user // is happening in parallel. We can't join the session until the // current user has been completely loaded. Poll for the current user // before proceeding with session joining. function checkForCurrentUser() { if (context.JK.userMe) { afterCurrentUserLoaded(); } else { context.setTimeout(checkForCurrentUser, 100); } } checkForCurrentUser(); context.JK.HelpBubbleHelper.jamtrackGuideSession($screen.find('li.open-a-jamtrack'), $screen) } function afterShow(data) { $fluidTracks.addClass('showing'); $openBackingTrack.removeClass('disabled'); if(!context.JK.JamServer.connected) { promptLeave = false; app.notifyAlert("Not Connected", 'To create or join a session, you must be connected to the server.'); window.location = '/client#/home' return; } // The SessionModel is a singleton. // a client can only be in one session at a time, // and other parts of the code want to know at any certain times // about the current session, if any (for example, reconnect logic) if(context.JK.CurrentSessionModel) { context.JK.CurrentSessionModel.ensureEnded(); } context.JK.CurrentSessionModel = sessionModel = new context.JK.SessionModel( context.JK.app, context.JK.JamServer, context.jamClient, self ); sessionModel.start(sessionId); // indicate that the screen is active, so that // body-scoped drag handlers can go active screenActive = true; rest.getSessionHistory(data.id) .done(function(musicSession) { musicianAccessOnJoin = musicSession.musician_access;; var shouldVerifyNetwork = musicSession.musician_access; gearUtils.guardAgainstInvalidConfiguration(app, shouldVerifyNetwork) .fail(function() { promptLeave = false; window.location = '/client#/home' }) .done(function(){ var result = sessionUtils.SessionPageEnter(); gearUtils.guardAgainstActiveProfileMissing(app, result) .fail(function(data) { promptLeave = false; if(data && data.reason == 'handled') { if(data.nav == 'BACK') { window.history.go(-1); } else { window.location = data.nav; } } else { window.location = '/client#/home'; } }) .done(function(){ sessionModel.waitForSessionPageEnterDone() .done(function(userTracks) { context.JK.CurrentSessionModel.setUserTracks(userTracks); initializeSession(); }) .fail(function(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 } else { context.JK.alertSupportedNeeded('Unable to determine configured tracks due to reason: ' + data) } promptLeave = false; window.location = '/client#/home' }); }) }) }) .fail(function() { }) } function notifyWithUserInfo(title , text, clientId) { sessionModel.findUserBy({clientId: clientId}) .done(function(user) { app.notify({ "title": title, "text": user.name + " " + text, "icon_url": context.JK.resolveAvatarUrl(user.photo_url) }); }) .fail(function() { app.notify({ "title": title, "text": 'Someone ' + text, "icon_url": "/assets/content/icon_alert_big.png" }); }); } function afterCurrentUserLoaded() { // now check if the user can play in a session with others var deferred = new $.Deferred(); if(musicianAccessOnJoin) { deferred = context.JK.guardAgainstSinglePlayerProfile(app, function () { promptLeave = false; }); } else { deferred.resolve(); } deferred.fail(function(result) { if(!result.controlled_location) { window.location="/client#/home" } }) .done(function() { logger.debug("user has passed all session guards") promptLeave = true; var sessionModel = context.JK.CurrentSessionModel; $(sessionModel.recordingModel) .on('startingRecording', function(e, data) { displayStartingRecording(); lockControlsforJamTrackRecording(); }) .on('startedRecording', function(e, data) { if(data.reason) { var reason = data.reason; var detail = data.detail; var title = "Could Not Start Recording"; if(data.reason == 'client-no-response') { notifyWithUserInfo(title, 'did not respond to the start signal.', detail); } else if(data.reason == 'empty-recording-id') { app.notifyAlert(title, "No recording ID specified."); } else if(data.reason == 'missing-client') { notifyWithUserInfo(title, 'could not be signalled to start recording.', detail); } else if(data.reason == 'already-recording') { app.notifyAlert(title, 'Already recording. If this appears incorrect, try restarting JamKazam.'); } else if(data.reason == 'recording-engine-unspecified') { notifyWithUserInfo(title, 'had a problem writing recording data to disk.', detail); } else if(data.reason == 'recording-engine-create-directory') { notifyWithUserInfo(title, 'had a problem creating a recording folder.', detail); } else if(data.reason == 'recording-engine-create-file') { notifyWithUserInfo(title, 'had a problem creating a recording file.', detail); } else if(data.reason == 'recording-engine-sample-rate') { notifyWithUserInfo(title, 'had a problem recording at the specified sample rate.', detail); } else if(data.reason == 'rest') { var jqXHR = detail[0]; app.notifyServerError(jqXHR); } else { notifyWithUserInfo(title, 'Error Reason: ' + reason); } displayDoneRecording(); } else { displayStartedRecording(); displayWhoCreated(data.clientId); lockControlsforJamTrackRecording(); } }) .on('stoppingRecording', function(e, data) { displayStoppingRecording(data); unlockControlsforJamTrackRecording(); }) .on('stoppedRecording', function(e, data) { unlockControlsforJamTrackRecording(); if(sessionModel.selfOpenedJamTracks()) { var timeline = context.jamClient.GetJamTrackTimeline(); rest.addRecordingTimeline(data.recordingId, timeline) .fail(function(){ app.notify( { title: "Unable to Add JamTrack Volume Data", text: "The volume of the JamTrack will not be correct in the recorded mix." }, null, true); }) } if(data.reason) { logger.warn("Recording Discarded: ", data); var reason = data.reason; var detail = data.detail; var title = "Recording Discarded"; if(data.reason == 'client-no-response') { notifyWithUserInfo(title, 'did not respond to the stop signal.', detail); } else if(data.reason == 'missing-client') { notifyWithUserInfo(title, 'could not be signalled to stop recording.', detail); } else if(data.reason == 'empty-recording-id') { app.notifyAlert(title, "No recording ID specified."); } else if(data.reason == 'wrong-recording-id') { app.notifyAlert(title, "Wrong recording ID specified."); } else if(data.reason == 'not-recording') { app.notifyAlert(title, "Not currently recording."); } else if(data.reason == 'already-stopping') { app.notifyAlert(title, "Already stopping the current recording."); } else if(data.reason == 'start-before-stop') { notifyWithUserInfo(title, 'asked that we start a new recording; cancelling the current one.', detail); } else { app.notifyAlert(title, "Error reason: " + reason); } displayDoneRecording(); } else { displayDoneRecording(); promptUserToSave(data.recordingId, timeline); } }) .on('abortedRecording', function(e, data) { var reason = data.reason; var detail = data.detail; var title = "Recording Cancelled"; if(data.reason == 'client-no-response') { notifyWithUserInfo(title, 'did not respond to the start signal.', detail); } else if(data.reason == 'missing-client') { notifyWithUserInfo(title, 'could not be signalled to start recording.', detail); } else if(data.reason == 'populate-recording-info') { notifyWithUserInfo(title, 'could not synchronize with the server.', detail); } else if(data.reason == 'recording-engine-unspecified') { notifyWithUserInfo(title, 'had a problem writing recording data to disk.', detail); } else if(data.reason == 'recording-engine-create-directory') { notifyWithUserInfo(title, 'had a problem creating a recording folder.', detail); } else if(data.reason == 'recording-engine-create-file') { notifyWithUserInfo(title, 'had a problem creating a recording file.', detail); } else if(data.reason == 'recording-engine-sample-rate') { notifyWithUserInfo(title, 'had a problem recording at the specified sample rate.', detail); } else { app.notifyAlert(title, "Error reason: " + reason); } displayDoneRecording(); }) sessionModel.subscribe('sessionScreen', sessionChanged); sessionModel.joinSession(sessionId) .fail(function(xhr, textStatus, errorMessage) { if(xhr.status == 404) { // we tried to join the session, but it's already gone. kick user back to join session screen promptLeave = false; context.window.location = "/client#/findSession"; app.notify( { title: "Unable to Join Session", text: "The session you attempted to join is over." }, null, true); } else if(xhr.status == 422) { var 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"]) { promptLeave = false; context.window.location = "/client#/findSession"; app.notify( { title: "Unable to Join Session", text: "The session is currently recording." }, null, true); } else { app.notifyServerError(xhr, 'Unable to Join Session'); } } else { app.notifyServerError(xhr, 'Unable to Join Session'); } }) .done(function() { // check if this is a auto-load jamtrack situation (came from account jamtrack screen) var jamTrack = sessionUtils.grabAutoOpenJamTrack(); if(jamTrack) { // give the session to settle just a little (call a timeout of 1 second) setTimeout(function() { // tell the server we are about to open a jamtrack rest.openJamTrack({id: context.JK.CurrentSessionModel.id(), jam_track_id: jamTrack.id}) .done(function(response) { // now actually load the jamtrack context.JK.CurrentSessionModel.updateSession(response); loadJamTrack(jamTrack); }) .fail(function(jqXHR) { app.notifyServerError(jqXHR, "Unable to Open JamTrack For Playback"); }) }, 1000) } }) }) } // not leave session but leave screen function beforeLeave(data) { if(promptLeave) { var leaveSessionWarningDialog = new context.JK.LeaveSessionWarningDialog(context.JK.app, function() { promptLeave = false; context.location.hash = data.hash }); leaveSessionWarningDialog.initialize(); app.layout.showDialog('leave-session-warning'); return false; } return true; } function beforeHide(data) { context.JK.HelpBubbleHelper.clearJamTrackGuide(); if(gon.global.video_available && gon.global.video_available!="none") { webcamViewer.setVideoOff() } $fluidTracks.removeClass('showing'); if(screenActive) { // this path is possible if FTUE is invoked on session page, and they cancel sessionModel.leaveCurrentSession() .fail(function(jqXHR) { if(jqXHR.status != 404) { logger.debug("leave session failed"); app.ajaxError(arguments) } }); } screenActive = false; sessionUtils.SessionPageLeave(); } function getMetronomeMasterMixers() { return _mixersForGroupId(ChannelGroupIds.MetronomeGroup, MIX_MODES.MASTER); } function checkMetronomeTransition() { // trust backend over server if(sessionModel.jamTracks() !== null || sessionModel.recordedJamTracks() !== null) { // ignore all metronome events when jamtracks are open, because backend opens metronome mixer to play jamtrack tap-ins logger.debug("ignore checkMetronomeTransition because JamTrack is open") return; } var metronomeMasterMixers = getMetronomeMasterMixers(); if (metronomeMixer == null && metronomeMasterMixers.length > 0) { logger.debug("monitoring metronome") playbackControls.startMonitor(context.JK.PLAYBACK_MONITOR_MODE.METRONOME) } else if (metronomeMixer != null && metronomeMasterMixers.length == 0) { playbackControls.stopMonitor(); } metronomeMixer = metronomeMasterMixers.length > 0 ? metronomeMasterMixers : null; } function checkJamTrackTransition(currentSession) { // handle jam tracks // if we have a recording open, then don't go into JamTrack monitor mode even if a JamTrack is open if (jamTrack == null && (currentSession && currentSession.jam_track != null && currentSession.claimed_recording == null)) { logger.debug("monitoring jamtrack") playbackControls.startMonitor(context.JK.PLAYBACK_MONITOR_MODE.JAMTRACK); } else if (jamTrack && (currentSession == null || (currentSession.jam_track == null && currentSession.claimed_recording == null))) { logger.debug("stop monitoring jamtrack") playbackControls.stopMonitor(); } jamTrack = currentSession == null ? null : currentSession.jam_track; } function checkBackingTrackTransition(currentSession) { // handle backing tracks if (backing_track_path == null && (currentSession && currentSession.backing_track_path != null)) { logger.debug("monitoring backing track") playbackControls.startMonitor(); } else if (backing_track_path && (currentSession == null || currentSession.backing_track_path == null)) { logger.debug("stop monitoring backing track") playbackControls.stopMonitor(); } backing_track_path = currentSession == null ? null : currentSession.backing_track_path; } function checkRecordingTransition(currentSession) { // handle claimed recordings if (claimedRecording == null && (currentSession && currentSession.claimed_recording != null)) { // this is a 'started with a claimed_recording' transition. // we need to start a timer to watch for the state of the play session logger.debug("monitoring recording") playbackControls.startMonitor(); } else if (claimedRecording && (currentSession == null || currentSession.claimed_recording == null)) { logger.debug("stop monitoring recording") playbackControls.stopMonitor(); } claimedRecording = currentSession == null ? null : currentSession.claimed_recording; } function handleTransitionsInRecordingPlayback() { // let's see if we detect a transition to start playback or stop playback var currentSession = sessionModel.getCurrentSession(); checkRecordingTransition(currentSession); checkBackingTrackTransition(currentSession); checkJamTrackTransition(currentSession); checkMetronomeTransition(); } function sessionChanged() { handleTransitionsInRecordingPlayback(); // TODO - in the specific case of a user changing their tracks using the configureTrack dialog, // this event appears to fire before the underlying mixers have updated. I have no event to // know definitively when the underlying mixers are up to date, so for now, we just delay slightly. // This obviously has the possibility of introducing time-based bugs. context.setTimeout(renderSession, RENDER_SESSION_DELAY); } /** * the mixers object is a list. In order to find one by key, * you must iterate. Convenience method to locate a particular * mixer by id. */ function getMixer(mixerId, mode) { if(mode === undefined) { mode = sessionModel.getMixMode(); } return allMixers[(mode ? 'M' : 'P') + mixerId]; } function getMixerByResourceId(resourceId, mode) { var mixerPair = mixersByResourceId[resourceId]; if(!mixerPair) {return null;} if(mode === undefined) { return mixerPair; } else { if(mode == MIX_MODES.MASTER) { return mixerPair.master; } else { return mixerPair.personal; } } } function getMixerByTrackId(trackId, mode) { var mixerPair = mixersByTrackId[trackId]; if(!mixerPair) {return null;} if(mode === undefined) { return mixerPair; } else { if(mode == MIX_MODES.MASTER) { return mixerPair.master; } else { return mixerPair.personal; } } } function resetOtherAudioContent() { if ($('.session-recordings .track').length === 0 && $('.session-recordings .download-jamtrack').length === 0 && $('.session-recordings .pending-metronome').length === 0) { $('.session-recordings .when-empty').show(); $('.session-recording-name-wrapper').hide(); $('.session-recordings .recording-controls').hide(); $closePlaybackRecording.show(); $('.session-recordings .session-recording-name').text('(No audio loaded)') $('.session-recordings').attr('media-state', 'closed') $('.session-livetracks').attr('media-state', 'closed') } } function didSelfOpenMedia() { var localMediaMixers = _mixersForGroupIds([ChannelGroupIds.MediaTrackGroup, ChannelGroupIds.JamTrackGroup, ChannelGroupIds.MetronomeGroup], MIX_MODES.MASTER); // if we find any local media mixers, then we are the opener of media return localMediaMixers.length > 0; } function checkShowCloseControl() { didSelfOpenMedia() ? $closePlaybackRecording.show() : $closePlaybackRecording.hide(); } function renderSession() { $myTracksContainer.empty(); $('.session-track').remove(); // Remove previous tracks $voiceChat.hide(); _updateMixers(); _renderTracks(); _renderLocalMediaTracks(); _wireTopVolume(); _wireTopMix(); _addVoiceChat(); _initDialogs(); if ($('.session-livetracks .track').length === 0) { $('.session-livetracks .when-empty').show(); } checkPendingMetronome(); resetOtherAudioContent(); resizeFluid(); /** if ($('.session-recordings .track').length === 0) { $('.session-recordings .when-empty').show(); $('.session-recording-name-wrapper').hide(); $('.session-recordings .recording-controls').hide(); // should we show the close button? Only if the user opened the media checkShowCloseControl(); } else { $('.session-recordings .when-empty').hide(); $('.session-recording-name-wrapper').show(); $('.session-recordings .recording-controls').show(); checkShowCloseControl(); } */ // Handle long labels: $(".track-label").dotdotdot() $(".session-recording-name").dotdotdot() } // renderSession function _initDialogs() { configureTrackDialog.initialize(); addNewGearDialog.initialize(); } // Get the latest list of underlying audio mixer channels, and populates: // * mixersByResourceId - a hash of resourceId / { master: mixer, personal: mixer } personal: can be null in case of PeerAudioInputMusicGroup // * mixersByTrackId - a hash of track id / {master: mixer, personal: mixer}. // * allMixers - a hash of mixer.id / mixer // * masterMixers - array of master mode mixers // * personalMixers - array of personal mode mixers function _updateMixers() { masterMixers = context.jamClient.SessionGetAllControlState(true); personalMixers = context.jamClient.SessionGetAllControlState(false); //logger.debug("masterMixers", masterMixers) //logger.debug("personalMixers", personalMixers) mixersByResourceId = {} mixersByTrackId = {} allMixers = {} var i; for(i = 0; i < masterMixers.length; i++) { var masterMixer = masterMixers[i]; allMixers['M' + masterMixer.id] = masterMixer; // populate allMixers by mixer.id // populate mixer pair var mixerPair = {} mixersByResourceId[masterMixer.rid] = mixerPair mixersByTrackId[masterMixer.id] = mixerPair mixerPair.master = masterMixer; } for(i = 0; i < personalMixers.length; i++) { var personalMixer = personalMixers[i]; allMixers['P' + personalMixer.id] = personalMixer // populate other side of mixer pair var mixerPair = mixersByResourceId[personalMixer.rid] if(!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; } // Always add a hard-coded simplified 'mixer' for the L2M mix /** var l2m_mixer = { id: '__L2M__', range_low: -80, range_high: 20, volume_left: context.jamClient.SessionGetMasterLocalMix() }; mixers.push(l2m_mixer);*/ } function _mixersForGroupId(groupId, mixMode) { var foundMixers = []; var mixers = mixMode == MIX_MODES.MASTER ? masterMixers : personalMixers; $.each(mixers, function(index, mixer) { if ( mixer.group_id === groupId) { foundMixers.push(mixer); } }); return foundMixers; } function _mixersForGroupIds(groupIds, mixMode) { var foundMixers = []; var mixers = mixMode == MIX_MODES.MASTER ? masterMixers : personalMixers; $.each(mixers, function(index, mixer) { var groupIdLen = groupIds.length; for (var i = 0; i < groupIdLen; i++) { if ( mixer.group_id === groupIds[i]) { foundMixers.push(mixer); } } }); return foundMixers; } function _getMyVoiceChatMixers() { var mixers = _mixersForGroupId(ChannelGroupIds.AudioInputChatGroup, sessionModel.getMixMode()); if (mixers.length == 0) { return null; } var oppositeMixers = _mixersForGroupId(ChannelGroupIds.AudioInputChatGroup, !sessionModel.getMixMode()); if(oppositeMixers.length == 0) { logger.warn("unable to find opposite mixer for voice chat"); return null; } var mixer = mixers[0]; var oppositeMixer = oppositeMixers[0]; return { mixer: mixer, oppositeMixer: oppositeMixer, vuMixer: mixer, muteMixer: mixer } } function _clientIdForUserInputMixer(mixerId, mixMode) { var found = null; var mixers = mixMode == MIX_MODES.MASTER ? masterMixers : personalMixers; $.each(mixers, function(index, mixer) { if (mixer.group_id === ChannelGroupIds.UserMusicInputGroup && mixer.id == mixerId) { found = mixer.client_id; return false; } }); return found; } // TODO FIXME - This needs to support multiple tracks for an individual // client id and group. function _mixerForClientId(clientId, groupIds, usedMixers) { //logger.debug("clientId", clientId, "groupIds", groupIds, "mixers", mixers) var foundMixer = null; $.each(mixers, function(index, mixer) { if (mixer.client_id === clientId) { for (var i=0; i 0) { renderRecordingTracks(recordingTrackMixers) } if(backingTrackMixers.length > 0) { renderBackingTracks(backingTrackMixers) } if(jamTrackMixers.length > 0) { renderJamTracks(jamTrackMixers); } if(metronomeTrackMixers.length > 0 && sessionModel.jamTracks() === null && sessionModel.recordedJamTracks() == null) { renderMetronomeTracks(metronomeTrackMixers); } if(adhocTrackMixers.length > 0) { logger.warn("some tracks are open that we don't know how to show") } checkMetronomeTransition(); } // this method is pretty complicated because it forks on a key bit of state: // sessionModel.isPlayingRecording() // a backing track opened as part of a recording has a different behavior and presence on the server (recording.recorded_backing_tracks) // than a backing track opend ad-hoc (connection.backing_tracks) function renderBackingTracks(backingTrackMixers) { var backingTracks = [] if(sessionModel.isPlayingRecording()) { // only return managed mixers for recorded backing tracks backingTrackMixers = context._.filter(backingTrackMixers, function(mixer){return mixer.managed || mixer.managed === undefined}) backingTracks = sessionModel.recordedBackingTracks(); } else { // only return un-managed (ad-hoc) mixers for normal backing tracks backingTracks = sessionModel.backingTracks(); backingTrackMixers = context._.filter(backingTrackMixers, function(mixer){return !mixer.managed}) if(backingTrackMixers.length > 1) { logger.error("multiple, managed backing track mixers encountered", backingTrackMixers) app.notify({ title: "Multiple Backing Tracks Encountered", text: "Only one backing track can be open a time.", icon_url: "/assets/content/icon_alert_big.png" }); return false; } } var noCorrespondingTracks = false; $.each(backingTrackMixers, function(index, mixer) { // find the track or tracks that correspond to the mixer var correspondingTracks = [] var noCorrespondingTracks = false; if(sessionModel.isPlayingRecording()) { $.each(backingTracks, function (i, backingTrack) { if(mixer.persisted_track_id == backingTrack.client_track_id || // occurs if this client is the one that opened the track mixer.id == 'L' + backingTrack.client_track_id) { // occurs if this client is a remote participant correspondingTracks.push(backingTrack) } }); } else { // if this is just an open backing track, then we can assume that the 1st backingTrackMixer is ours correspondingTracks.push(backingTracks[0]) } if (correspondingTracks.length == 0) { noCorrespondingTracks = true; logger.debug("renderBackingTracks: could not map backing tracks") app.notify({ title: "Unable to Open Backing Track", text: "Could not correlate server and client tracks", icon_url: "/assets/content/icon_alert_big.png" }); return false; } // now we have backing track and mixer in hand; we can render var backingTrack = correspondingTracks[0] // pluck the 1st mixer, and assume that all other mixers in this group are of the same type (between JamTrack vs Peer) // if it's a locally opened track (MediaTrackGroup), then we can say this person is the opener var isOpener = mixer.group_id == ChannelGroupIds.MediaTrackGroup; if(isOpener) { var oppositeMixer = getMixerByResourceId(mixer.rid, MIX_MODES.PERSONAL); var mixerId = mixer.id + "," + oppositeMixer.id } else { var mixerId = mixer.id; } var shortFilename = context.JK.getNameOfFile(backingTrack.filename); if(!sessionModel.isPlayingRecording()) { // if a recording is being played back, do not set this header, because renderRecordedTracks already did // ugly. $('.session-recording-name').text(shortFilename); } var instrumentIcon = context.JK.getInstrumentIcon45(backingTrack.instrument_id); var photoUrl = "/assets/content/icon_recording.png"; // Default trackData to participant + no Mixer state. var trackData = { type: 'backing_track', trackId: backingTrack.id, clientId: backingTrack.client_id, name: 'Backing', filename: backingTrack.filename, instrumentIcon: instrumentIcon, avatar: photoUrl, latency: "good", gainPercent: 0, muteClass: 'muted', showLoop: isOpener && !sessionModel.isPlayingRecording(), loopState: mixer.loop, mixerId: "", avatarClass: 'avatar-recording', preMasteredClass: "" }; var gainPercent = percentFromMixerValue( mixer.range_low, mixer.range_high, mixer.volume_left); var muteClass = "enabled"; if (mixer.mute) { muteClass = "muted"; } trackData.gainPercent = gainPercent; trackData.muteClass = muteClass; trackData.mixerId = mixerId; // the master mixer controls the volume control for recordings (no personal controls in either master or personal mode) trackData.vuMixerId = mixer.id; // the master mixer controls the VUs for recordings (no personal controls in either master or personal mode) trackData.muteMixerId = mixer.id; // the master mixer controls the mute for recordings (no personal controls in either master or personal mode) trackData.mediaTrackOpener = isOpener; trackData.mediaControlsDisabled = !isOpener; trackData.showHelpAboutMediaMixers = sessionModel.isPersonalMixMode() && isOpener; _addRecordingTrack(trackData, mixer, oppositeMixer); }); } function renderJamTracks(jamTrackMixersOrig) { logger.debug("rendering jam tracks", jamTrackMixersOrig); var jamTrackMixers = jamTrackMixersOrig.slice(); var jamTracks = [] var jamTrackName = 'JamTrack'; if(sessionModel.isPlayingRecording()) { // only return managed mixers for recorded backing tracks jamTracks = sessionModel.recordedJamTracks(); jamTrackName = sessionModel.recordedJamTrackName(); } else { // only return un-managed (ad-hoc) mixers for normal backing tracks jamTracks = sessionModel.jamTracks(); jamTrackName = sessionModel.jamTrackName(); } // pluck the 1st mixer, and assume that all other mixers in this group are of the same type (between JamTrack vs Peer) // if it's a locally opened track (JamTrackGroup), then we can say this person is the opener var isOpener = jamTrackMixers[0].group_id == ChannelGroupIds.JamTrackGroup; // using the server's info in conjuction with the client's, draw the recording tracks if(jamTracks) { $('.session-recording-name').text(jamTrackName); var noCorrespondingTracks = false; $.each(jamTracks, function(index, jamTrack) { var mixer = null; var preMasteredClass = ""; // find the track or tracks that correspond to the mixer var correspondingTracks = [] $.each(jamTrackMixersOrig, function(i, matchMixer) { if(matchMixer.id == jamTrack.id) { correspondingTracks.push(jamTrack); mixer = matchMixer; } }); if(correspondingTracks.length == 0) { noCorrespondingTracks = true; logger.error("could not correlate jam tracks", jamTrackMixers, jamTracks) app.notify({ title: "Unable to Open JamTrack", text: "Could not correlate server and client tracks", icon_url: "/assets/content/icon_alert_big.png"}); return false; } // prune found recorded tracks jamTracks = $.grep(jamTracks, function(value) { return $.inArray(value, correspondingTracks) < 0; }); // prune found mixers jamTrackMixers.splice(mixer); var oneOfTheTracks = correspondingTracks[0]; var instrumentIcon = context.JK.getInstrumentIcon45(oneOfTheTracks.instrument.id); var photoUrl = "/assets/content/icon_recording.png"; var name = oneOfTheTracks.part if (!name) { name = ''; } if(isOpener) { var oppositeMixer = getMixerByResourceId(mixer.rid, MIX_MODES.PERSONAL); var mixerId = mixer.id + "," + oppositeMixer.id } else { var mixerId = mixer.id; } // Default trackData to participant + no Mixer state. var trackData = { type: 'jam_track', trackId: oneOfTheTracks.id, clientId: oneOfTheTracks.client_id, name: name, instrumentIcon: instrumentIcon, avatar: photoUrl, latency: "good", gainPercent: 0, muteClass: 'muted', mixerId: "", avatarClass : 'avatar-recording', preMasteredClass: "" }; var gainPercent = percentFromMixerValue( mixer.range_low, mixer.range_high, mixer.volume_left); var muteClass = "enabled"; if (mixer.mute) { muteClass = "muted"; } trackData.gainPercent = gainPercent; trackData.muteClass = muteClass; trackData.mixerId = mixerId; // the master mixer controls the volume control for recordings (no personal controls in either master or personal mode) trackData.vuMixerId = mixer.id; // the master mixer controls the VUs for recordings (no personal controls in either master or personal mode) trackData.muteMixerId = mixer.id; // the master mixer controls the mute for recordings (no personal controls in either master or personal mode) trackData.mediaTrackOpener = isOpener; trackData.mediaControlsDisabled = !isOpener; trackData.showHelpAboutMediaMixers = sessionModel.isPersonalMixMode() && isOpener; _addRecordingTrack(trackData, mixer, oppositeMixer); }); if(!noCorrespondingTracks && jamTracks.length > 0) { logger.error("unable to find all jam tracks against client tracks"); app.notify({title:"All tracks not found", text: "Some tracks in the jam tracks are not present in the playback", icon_url: "/assets/content/icon_alert_big.png"}) } } } function renderMetronomeTracks(metronomeTrackMixers) { logger.debug("rendering metronome track") // pluck the 1st mixer, and assume that all other mixers in this group are of the same type (between JamTrack vs Peer) // if it's a locally opened track (MediaTrackGroup), then we can say this person is the opener var name = "Metronome" // using the server's info in conjuction with the client's, draw the recording tracks if(metronomeTrackMixers.length > 0) { var metronome = {} $('.session-recording-name').text(name);//sessionModel.getCurrentSession().backing_track_path); var noCorrespondingTracks = false; var mixer = metronomeTrackMixers[0] var preMasteredClass = ""; // find the track or tracks that correspond to the mixer var correspondingTracks = [] correspondingTracks.push(metronome); if(correspondingTracks.length == 0) { noCorrespondingTracks = true; app.notify({ title: "Unable to Open Metronome", text: "Could not correlate server and client tracks", icon_url: "/assets/content/icon_metronome_small.png"}); return false; } // prune found recorded tracks // Metronomes = $.grep(Metronomes, function(value) { // return $.inArray(value, correspondingTracks) < 0; // }); var oneOfTheTracks = correspondingTracks[0]; var instrumentIcon = context.JK.getInstrumentIcon45(oneOfTheTracks.instrument_id); var photoUrl = "/assets/content/icon_metronome_small.png"; var oppositeMixer = getMixerByResourceId(mixer.rid, MIX_MODES.PERSONAL); var mixerId = mixer.id + "," + oppositeMixer.id // Default trackData to participant + no Mixer state. var trackData = { type: 'metronome', trackId: "MS" + oneOfTheTracks.id, clientId: oneOfTheTracks.client_id, name: "Metronome", instrumentIcon: photoUrl, avatar: instrumentIcon, latency: "good", gainPercent: 0, muteClass: 'muted', mixerId: "", avatarClass : 'avatar-recording', preMasteredClass: "", showMetronomeControls: true }; var gainPercent = percentFromMixerValue( mixer.range_low, mixer.range_high, mixer.volume_left); var muteClass = "enabled"; if (mixer.mute) { muteClass = "muted"; } trackData.gainPercent = gainPercent; trackData.muteClass = muteClass; trackData.mixerId = mixerId; // the master mixer controls the volume control for recordings (no personal controls in either master or personal mode) trackData.vuMixerId = mixer.id; // the master mixer controls the VUs for recordings (no personal controls in either master or personal mode) trackData.muteMixerId = mixer.id; // the master mixer controls the mute for recordings (no personal controls in either master or personal mode) trackData.mediaTrackOpener = true trackData.mediaControlsDisabled = false trackData.showHelpAboutMediaMixers = false _addRecordingTrack(trackData, mixer, oppositeMixer); }// if setFormFromMetronome() metroCricket = context.jamClient.getMetronomeCricketTestState(); setMetronomePlaybackMode() $closePlaybackRecording.show(); } function renderRecordingTracks(recordingMixers) { // get the server's info for the recording var recordedTracks = sessionModel.recordedTracks(); var recordedBackingTracks = sessionModel.recordedBackingTracks(); // pluck the 1st mixer, and assume that all other mixers in this group are of the same type (between Local vs Peer) // if it's a locally opened track (MediaTrackGroup), then we can say this person is the opener var isOpener = recordingMixers[0].group_id == ChannelGroupIds.MediaTrackGroup; // using the server's info in conjuction with the client's, draw the recording tracks if(recordedTracks) { $('.session-recording-name').text(sessionModel.getCurrentSession().claimed_recording.name); var noCorrespondingTracks = false; $.each(recordingMixers, function(index, mixer) { var preMasteredClass = ""; // find the track or tracks that correspond to the mixer var correspondingTracks = [] $.each(recordedTracks, function(i, recordedTrack) { if(mixer.id.indexOf("L") == 0) { if(mixer.id.substring(1) == recordedTrack.client_track_id) { correspondingTracks.push(recordedTrack); } } else if(mixer.id.indexOf("C") == 0) { if(mixer.id.substring(1) == recordedTrack.client_id) { correspondingTracks.push(recordedTrack); preMasteredClass = "pre-mastered-track"; } } else { // this should not be possible alert("Invalid state: the recorded track had neither persisted_track_id or persisted_client_id"); } }); if(correspondingTracks.length == 0) { noCorrespondingTracks = true; logger.debug("unable to correlate all recorded tracks", recordingMixers, recordedTracks) app.notify({ title: "Unable to Open Recording", text: "Could not correlate server and client tracks", icon_url: "/assets/content/icon_alert_big.png"}); return false; } // prune found recorded tracks recordedTracks = $.grep(recordedTracks, function(value) { return $.inArray(value, correspondingTracks) < 0; }); var oneOfTheTracks = correspondingTracks[0]; var instrumentIcon = context.JK.getInstrumentIcon45(oneOfTheTracks.instrument_id); var photoUrl = "/assets/content/icon_recording.png"; var name = oneOfTheTracks.user.name; if (!(name)) { name = oneOfTheTracks.user.first_name + ' ' + oneOfTheTracks.user.last_name; } if(isOpener) { var oppositeMixer = getMixerByResourceId(mixer.rid, MIX_MODES.PERSONAL); var mixerId = mixer.id + "," + oppositeMixer.id } else { var mixerId = mixer.id; } // Default trackData to participant + no Mixer state. var trackData = { type: 'recorded_track', trackId: oneOfTheTracks.id, clientId: oneOfTheTracks.client_id, name: name, instrumentIcon: instrumentIcon, avatar: photoUrl, latency: "good", gainPercent: 0, muteClass: 'muted', mixerId: "", avatarClass : 'avatar-recording', preMasteredClass: preMasteredClass }; var gainPercent = percentFromMixerValue( mixer.range_low, mixer.range_high, mixer.volume_left); var muteClass = "enabled"; if (mixer.mute) { muteClass = "muted"; } trackData.gainPercent = gainPercent; trackData.muteClass = muteClass; trackData.mixerId = mixerId; // the master mixer controls the volume control for recordings (no personal controls in either master or personal mode) trackData.vuMixerId = mixer.id; // the master mixer controls the VUs for recordings (no personal controls in either master or personal mode) trackData.muteMixerId = mixer.id; // the master mixer controls the mute for recordings (no personal controls in either master or personal mode) trackData.mediaControlsDisabled = !isOpener; trackData.mediaTrackOpener = isOpener; trackData.showHelpAboutMediaMixers = sessionModel.isPersonalMixMode() && isOpener; _addRecordingTrack(trackData, mixer, oppositeMixer); }); if(!noCorrespondingTracks && recordedTracks.length > 0) { logger.error("unable to find all recorded tracks against client tracks"); app.notify({title:"All tracks not found", text: "Some tracks in the recording are not present in the playback", icon_url: "/assets/content/icon_alert_big.png"}) } } } function trackMuteSelected(e, data) { var muteOption = data.muteOption; // muteOption is going to be either 'master' or 'personal'. We mute the correct one, based on track info var $muteControl = $(this); // mixer is the mixer object returned from the backend corresponding to the mixer in this particular mode // oppositeMixer is the mixer correspond to the opposite mode. // Note that oppositeMixer is not ever set for ChannelGroupIds.AudioInputMusicGroup or ChannelGroupIds.MediaTrackGroup var mixer = $muteControl.data('mixer') var oppositeMixer = $muteControl.data('opposite-mixer') logger.debug("muting tracks. current mixer id=" + mixer.id + ", opposite mixer id=" + oppositeMixer.id) var mixerPair = {} if(sessionModel.isMasterMixMode()) { mixerPair.master = mixer; mixerPair.personal = oppositeMixer; } else { mixerPair.master = oppositeMixer; mixerPair.personal = mixer; } if(muteOption == 'master') { _toggleAudioMute(mixerPair.master.id, true, mixerPair.master.mode); _toggleAudioMute(mixerPair.personal.id, true, mixerPair.personal.mode); } else { _toggleAudioMute(mixerPair.personal.id, true, mixerPair.personal.mode); _toggleAudioMute(mixerPair.master.id, false, mixerPair.master.mode); } _toggleVisualMuteControl($muteControl, true); } // find backend mixer based on track data, and target client_id function findMixerForTrack(client_id, track, myTrack) { var mixer = null; // what is the best mixer for this track/client ID? var oppositeMixer = null; // what is the corresponding mixer in the opposite mode? var vuMixer = null; var muteMixer = null; var mixMode = sessionModel.getMixMode(); 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) } } else { if(mixMode === 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) 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 var 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 ) } } } else { // when it's a remote track and in personal mode, we want the 'Peer Stream', which is UserMusicInputGroup // this spans N tracks for the remote user var 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 } } } return { mixer: mixer, oppositeMixer: oppositeMixer, vuMixer: vuMixer, muteMixer: muteMixer } } function _renderTracks() { myTracks = []; // Participants are here now, but the mixers don't update right away. // Draw tracks from participants, then setup timers to look for the // mixers that go with those participants, if they're missing. lookingForMixers = [] // clear this back out as we are restarting from scratch lookingForMixersCount = 0; $.each(sessionModel.participants(), function(index, participant) { var name = participant.user.name; if (!(name)) { name = participant.user.first_name + ' ' + participant.user.last_name; } var myTrack = app.clientId == participant.client_id; // special case; if it's me and I have no tracks, show info about this sort of use of the app if (myTrack && participant.tracks.length == 0) { $tracksHolder.addClass('no-local-tracks') $liveTracksContainer.addClass('no-local-tracks') } else { $tracksHolder.removeClass('no-local-tracks') $liveTracksContainer.removeClass('no-local-tracks') } // loop through all tracks for each participant $.each(participant.tracks, function (index, track) { var instrumentIcon = context.JK.getInstrumentIcon45(track.instrument_id); var photoUrl = context.JK.resolveAvatarUrl(participant.user.photo_url); // Default trackData to participant + no Mixer state. var trackData = { trackId: track.id, connection_id: track.connection_id, client_track_id: track.client_track_id, client_resource_id: track.client_resource_id, clientId: participant.client_id, name: name, instrumentIcon: instrumentIcon, avatar: photoUrl, latency: "good", gainPercent: 0, muteClass: 'muted', mixerId: "", avatarClass: 'avatar-med', preMasteredClass: "", myTrack: myTrack }; var mixerData = findMixerForTrack(participant.client_id, track, myTrack) var mixer = mixerData.mixer; var vuMixer = mixerData.vuMixer; var muteMixer = mixerData.muteMixer; var oppositeMixer = mixerData.oppositeMixer; if (mixer && oppositeMixer) { myTrack = (mixer.group_id === ChannelGroupIds.AudioInputMusicGroup); if (!myTrack) { // it only makes sense to track 'audio established' for tracks that don't belong to you sessionModel.setAudioEstablished(participant.client_id, true); } var gainPercent = percentFromMixerValue( mixer.range_low, mixer.range_high, mixer.volume_left); var muteClass = "enabled"; if (mixer.mute) { muteClass = "muted"; } trackData.gainPercent = gainPercent; trackData.muteClass = muteClass; trackData.mixerId = mixer.id; trackData.vuMixerId = vuMixer.id; trackData.oppositeMixer = oppositeMixer; trackData.muteMixerId = muteMixer.id; trackData.noaudio = false; trackData.group_id = mixer.group_id; context.jamClient.SessionSetUserName(participant.client_id, name); } else { // No mixer to match, yet lookingForMixers.push({track: track, clientId: participant.client_id}) trackData.noaudio = true; if (!(lookingForMixersTimer)) { logger.debug("waiting for mixer to show up for track: " + track.id) lookingForMixersTimer = context.setInterval(lookForMixers, 500); } } var allowDelete = myTrack && index > 0; _addTrack(allowDelete, trackData, mixer, oppositeMixer); // Show settings icons only for my tracks if (myTrack) { myTracks.push(trackData); } }); }); configureTrackDialog = new context.JK.ConfigureTrackDialog(app, myTracks, sessionId, sessionModel); addNewGearDialog = new context.JK.AddNewGearDialog(app, self); } function connectTrackToMixer(trackSelector, track, mixerId, gainPercent, groupId, mixer, oppositeMixer) { var vuOpts = $.extend({}, trackVuOpts); var faderOpts = $.extend({}, trackFaderOpts); faderOpts.faderId = mixerId; var vuLeftSelector = trackSelector + " .track-vu-left"; var vuRightSelector = trackSelector + " .track-vu-right"; var faderSelector = trackSelector + " .track-gain"; var $fader = $(faderSelector).attr('mixer-id', mixerId).data('groupId', groupId).data('mixer', mixer).data('opposite-mixer', oppositeMixer); if(track.mediaControlsDisabled) { $fader.data('media-controls-disabled', true).data('media-track-opener', track.mediaTrackOpener) // this we be applied later to the fader handle $element } $fader.data('showHelpAboutMediaMixers', track.showHelpAboutMediaMixers) var $track = $(trackSelector); // Set mixer-id attributes and render VU/Fader if (!track.hideVU) { context.JK.VuHelpers.renderVU(vuLeftSelector, vuOpts); $track.find('.track-vu-left').attr('mixer-id', track.vuMixerId + '_vul').data('groupId', groupId).data('mixer', mixer).data('opposite-mixer', oppositeMixer) context.JK.VuHelpers.renderVU(vuRightSelector, vuOpts); $track.find('.track-vu-right').attr('mixer-id', track.vuMixerId + '_vur').data('groupId', groupId).data('mixer', mixer).data('opposite-mixer', oppositeMixer) } if (track.showMetronomeControls) { $track.find('.metronome-selects').removeClass("hidden") } else { $track.find('.metronome-selects').addClass("hidden") } // if (track.showMetroSound) { // $track.find('.metro-sound-select').removeClass("hidden") // } context.JK.FaderHelpers.renderFader($fader, faderOpts); // Set gain position context.JK.FaderHelpers.setFaderValue(mixerId, gainPercent); if(track.faderChanged) { $fader.on('fader_change', track.faderChanged); } else { $fader.on('fader_change', faderChanged); } return $track; } // Function called on an interval when participants change. Mixers seem to // show up later, so we render the tracks from participants, but keep track // of the ones there weren't any mixers for, and continually try to find them // and get them connected to the mixers underneath. function lookForMixers() { lookingForMixersCount++; _updateMixers(); var usedMixers = {}; var keysToDelete = []; context._.each(lookingForMixers, function(data) { var clientId = data.clientId; var track = data.track; var myTrack = app.clientId == clientId; var mixerData = findMixerForTrack(clientId, track, myTrack) var mixer = mixerData.mixer; var oppositeMixer = mixerData.oppositeMixer; var vuMixer = mixerData.vuMixer; var muteMixer = mixerData.muteMixer; if (mixer && oppositeMixer) { if(!myTrack) { // it only makes sense to track 'audio established' for tracks that don't belong to you sessionModel.setAudioEstablished(clientId, true); } var participant = (sessionModel.getParticipant(clientId) || {name:'unknown'}).name; logger.debug("found mixer=" + mixer.id + ", participant=" + participant) usedMixers[mixer.id] = true; keysToDelete.push(data); var gainPercent = percentFromMixerValue( mixer.range_low, mixer.range_high, mixer.volume_left); var trackSelector = 'div.track[track-id="' + track.id + '"]'; connectTrackToMixer(trackSelector, track, mixer.id, gainPercent, mixer.group_id, mixer, oppositeMixer); var $track = $('div.track[client-id="' + clientId + '"]'); var $trackIconMute = $track.find('.track-icon-mute') $trackIconMute.attr('mixer-id', muteMixer.id).data('mixer', mixer).data('opposite-mixer', oppositeMixer) $trackIconMute.muteSelector().on(EVENTS.MUTE_SELECTED, trackMuteSelected) // hide overlay for all tracks associated with this client id (if one mixer is present, then all tracks are valid) $('.disabled-track-overlay', $track).hide(); $('.track-connection', $track).removeClass('red yellow green').addClass('grey'); // Set mute state _toggleVisualMuteControl($trackIconMute, mixer.mute || oppositeMixer.mute); } else { // if 1 second has gone by and still no mixer, then we gray the participant's tracks if(lookingForMixersCount == 2) { var $track = $('div.track[client-id="' + clientId + '"]'); $('.disabled-track-overlay', $track).show(); $('.track-connection', $track).removeClass('red yellow green').addClass('red'); } // if 5 seconds have gone by and no mixer, then we tell the server failed to establish audio else if(lookingForMixersCount == 10) { if(!myTrack) { // it only makes sense to track 'audio established' for tracks that don't belong to you sessionModel.setAudioEstablished(clientId, false); } } var participant = (sessionModel.getParticipant(clientId) || { user: {name: 'unknown'}}).user.name; logger.debug("still looking for mixer for participant=" + participant + ", clientId=" + clientId) } }) for (var i=0; i 20) { lookingForMixersCount = 0; lookingForMixers = [] context.clearTimeout(lookingForMixersTimer); lookingForMixersTimer = null; } } // Given a mixerID and a value between 0.0-1.0, // light up the proper VU lights. function _updateVU(mixerId, value, isClipping) { // Special-case for mono tracks. If mono, and it's a _vul id, // update both sides, otherwise do nothing. // If it's a stereo track, just do the normal thing. var selector; var pureMixerId = mixerId.replace("_vul", ""); pureMixerId = pureMixerId.replace("_vur", ""); var mixer = getMixer(pureMixerId, sessionModel.getMixMode()); if(!mixer) { // try again, in the opposite mode (awful that this is necessary) mixer = getMixer(pureMixerId, !sessionModel.getMixMode()); } if (mixer) { if (!(mixer.stereo)) { // mono track if (mixerId.substr(-4) === "_vul") { // Do the left selector = $tracksHolder.find('[mixer-id="' + pureMixerId + '_vul"]'); context.JK.VuHelpers.updateVU(selector, value); // Do the right selector = $tracksHolder.find('[mixer-id="' + pureMixerId + '_vur"]'); context.JK.VuHelpers.updateVU(selector, value); } // otherwise, it's a mono track, _vur event - ignore. } else { // stereo track selector = $tracksHolder.find('[mixer-id="' + mixerId + '"]'); context.JK.VuHelpers.updateVU(selector, value); } } } function _addTrack(allowDelete, trackData, mixer, oppositeMixer) { var $destination = $myTracksContainer; if (trackData.clientId !== app.clientId) { $destination = $liveTracksContainer $('.session-livetracks .when-empty').hide(); } var template = $('#template-session-track').html(); var newTrack = $(context.JK.fillTemplate(template, trackData)); newTrack.data('track_data', trackData) var audioOverlay = $('.disabled-track-overlay', newTrack); var $trackIconMute = newTrack.find('.track-icon-mute') $trackIconMute.muteSelector().on(EVENTS.MUTE_SELECTED, trackMuteSelected) $trackIconMute.data('mixer', mixer).data('opposite-mixer', oppositeMixer) audioOverlay.hide(); // always start with overlay hidden, and only show if no audio persists $destination.append(newTrack); // Render VU meters and gain fader var trackSelector = $destination.selector + ' .session-track[track-id="' + trackData.trackId + '"]'; var gainPercent = trackData.gainPercent || 0; connectTrackToMixer(trackSelector, trackData, trackData.mixerId, gainPercent, trackData.group_id, mixer, oppositeMixer); var $closeButton = $('#div-track-close', 'div[track-id="' + trackData.trackId + '"]'); if (!allowDelete) { $closeButton.hide(); } else { $closeButton.click(deleteTrack); } // is this used? tracks[trackData.trackId] = new context.JK.SessionTrack(trackData.clientId); } // something is being shown now in the other audio area function otherAudioFilled() { $('.session-recordings .when-empty').hide(); $('.session-recording-name-wrapper').show(); $('.session-recordings').attr('media-state', 'open'); $('.session-livetracks').attr('media-state', 'open'); } function resizeFluid() { var trackWidth = 78; // 70 width + 8 margin var trackPadding = 30; // 15px left and right var numLiveTracks = $liveTracks.find('.track').length; var numAudioTracks = $audioTracks.find('.track').length; var totalWidth = $fluidTracks.width(); // calculate desired audio tracks width var minimumLiveTrackWidth = numLiveTracks * trackWidth + trackPadding; var otherAudioWidth = numAudioTracks * trackWidth + trackPadding; var liveTrackWidth = totalWidth - otherAudioWidth; // live tracks get precedence over audio tracks, if there is a content over width usage if(liveTrackWidth < minimumLiveTrackWidth) { logger.debug("live track width trumping mode") liveTrackWidth = minimumLiveTrackWidth; otherAudioWidth = totalWidth - liveTrackWidth; } var otherAudioWidthPct = Math.floor(100 * otherAudioWidth/totalWidth); var liveTrackWidthPct = Math.ceil(100 * liveTrackWidth/totalWidth); //logger.debug("resizeFluid: ", minimumLiveTrackWidth, otherAudioWidth, otherAudioWidthPct, liveTrackWidthPct, liveTrackWidthPct) $audioTracks.css('width', otherAudioWidthPct + '%'); $liveTracks.css('width', liveTrackWidthPct + '%'); } function _addRecordingTrack(trackData, mixer, oppositeMixer) { otherAudioFilled(); $('.session-recordings .recording-controls').show(); var parentSelector = '#session-recordedtracks-container'; var $destination = $(parentSelector); var template = $('#template-session-track').html(); var newTrack = $(context.JK.fillTemplate(template, trackData)); newTrack.data('track_data', trackData); $otherAudioContainer.append(newTrack); if(trackData.preMasteredClass) { context.JK.helpBubble($('.track-instrument', newTrack), 'pre-processed-track', {}, {offsetParent: newTrack.closest('.content-body')}); } // Render VU meters and gain fader var trackSelector = $otherAudioContainer.selector + ' .session-track[track-id="' + trackData.trackId + '"]'; var gainPercent = trackData.gainPercent || 0; var $track = connectTrackToMixer(trackSelector, trackData, trackData.mixerId, gainPercent, null); var $trackIconMute = $track.find('.track-icon-mute') if(trackData.mediaControlsDisabled) { $trackIconMute.data('media-controls-disabled', true).data('media-track-opener', trackData.mediaTrackOpener) } $trackIconMute.data('mixer', mixer).data('opposite-mixer', oppositeMixer) $trackIconMute.data('showHelpAboutMediaMixers', trackData.showHelpAboutMediaMixers) if(trackData.showLoop) { var $trackIconLoop = $track.find('.track-icon-loop') var $trackIconLoopCheckbox = $trackIconLoop.find('input'); $trackIconLoopCheckbox.prop('checked', trackData.loopState); context.JK.checkbox($trackIconLoopCheckbox) $trackIconLoopCheckbox.on('ifChanged', function() { var loop = $trackIconLoopCheckbox.is(':checked') _toggleAudioLoop(mixer.id, loop, getMixer(mixer.id).mode) }); $trackIconLoop.show() } // is this used? tracks[trackData.trackId] = new context.JK.SessionTrack(trackData.clientId); } /** * Will be called when fader changes. The fader id (provided at subscribe time), * the new value (0-100) and whether the fader is still being dragged are passed. */ function faderChanged(e, data) { var $target = $(this); var faderId = $target.attr('mixer-id'); var groupId = $target.data('groupId'); var mixerIds = faderId.split(','); // media tracks are the only controls that sometimes set two mixers right now var hasMasterAndPersonalControls = mixerIds.length == 2; $.each(mixerIds, function(i,v) { var broadcast = !(data.dragging); // If fader is still dragging, don't broadcast var mode = undefined; if(hasMasterAndPersonalControls) { mode = i == 0 ? MIX_MODES.MASTER : MIX_MODES.PERSONAL; } var mixer = fillTrackVolumeObject(v, mode, broadcast); setMixerVolume(mixer, data.percentage); 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(v, data.percentage); } }); } // function tempoFaderChanged(e, data) { // var $target = $(this); // var faderId = $target.attr('mixer-id'); // var groupId = $target.data('groupId'); // var mixerIds = faderId.split(','); // $.each(mixerIds, function(i,v) { // // TODO Interpolate tempo values if we decide to go this way: // 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 // } // }); // } function handleMetronomeCallback(args) { logger.debug("MetronomeCallback: ", args) metroTempo = args.bpm metroCricket = args.cricket; metroSound = METRO_SOUND_LOOKUP[args.sound]; setMetronomePlaybackMode(); setFormFromMetronome(); // This isn't actually there, so we rely on the metroSound as set from select on form: // metroSound = args.sound context.JK.CurrentSessionModel.refreshCurrentSession(true); } function handleVolumeChangeCallback(mixerId, isLeft, value, isMuted) { // Visually update mixer // There is no need to actually set the back-end mixer value as the // back-end will already have updated the audio mixer directly prior to sending // me this event. I simply need to visually show the new fader position. // TODO: Use mixer's range var faderValue = percentFromMixerValue(-80, 20, value); context.JK.FaderHelpers.setFaderValue(mixerId, faderValue); var $muteControl = $('[control="mute"][mixer-id="' + mixerId + '"]'); _toggleVisualMuteControl($muteControl, isMuted); } function handleBridgeCallback(vuData) { var j; var eventName = null; var mixerId = null; var value = null; var vuInfo = null; for (j = 0; j < vuData.length; j++) { vuInfo = vuData[j]; var eventName = vuInfo[0]; var vuVal = 0.0; if(eventName === "vu") { var mixerId = vuInfo[1]; var leftValue = vuInfo[2]; var leftClipping = vuInfo[3]; var rightValue = vuInfo[4]; var 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 _updateVU(mixerId + "_vul", (leftValue + 80) / 80, leftClipping); _updateVU(mixerId + "_vur", (rightValue + 80) / 80, rightClipping); } else if(eventName === 'connection_status') { var mixerId = vuInfo[1]; var value = vuInfo[2]; // Connection Quality Change var connectionClass = 'green'; if (value < 7) { connectionClass = 'yellow'; } if (value < 4) { connectionClass = 'red'; } var mixerPair = getMixerByTrackId(mixerId); var clientId = mixerPair ? mixerPair.master.client_id : null; if(clientId) { var $connection = $('.session-track[client-id="' + clientId + '"] .track-connection'); if($connection.length == 0) { logger.debug("connection status: looking for clientId: " + clientId + ", mixer: " + mixerId) } else { $connection.removeClass('red yellow green grey'); $connection.addClass(connectionClass); } } } else if(eventName === 'add' || eventName === 'remove') { // TODO - _renderSession. Note I get streams of these in // sequence, so have Nat fix, or buffer/spam protect // Note - this is already handled from websocket events. // However, there may be use of these two events to avoid // the polling-style check for when a mixer has been added // to match a participant track. } else { logger.debug('non-vu event: ' + JSON.stringify(vuInfo)); } } } function handleBackingTrackSelectedCallback(result) { $openBackingTrack.removeClass('disabled'); if(!sessionModel.inSession()) { return; } if(result.success) { logger.debug("backing track selected: " + result.file); rest.openBackingTrack({id: context.JK.CurrentSessionModel.id(), backing_track_path: result.file}) .done(function(response) { var openResult = context.jamClient.SessionOpenBackingTrackFile(result.file, false); if(openResult) { sessionModel.setBackingTrack(result.file); } else { app.notify({ "title": "Couldn't Open Backing Track", "text": "Is the file a valid audio file?", "icon_url": "/assets/content/icon_alert_big.png" }); closeBackingTrack(); } }) .fail(function(jqXHR) { app.notifyServerError(jqXHR, "Unable to Open Backing Track For Playback"); }) } else { logger.debug("no backing track selected") } } function deleteSession(evt) { var sessionId = $(evt.currentTarget).attr("action-id"); if (sessionId) { $.ajax({ type: "DELETE", url: "/api/sessions/" + sessionId, success: function(response) { context.location="/client#/home"; }, error: function(jqXHR, textStatus, errorThrown) { logger.error("Error deleting session " + sessionId); } }); } } function deleteTrack(evt) { var trackId = $(evt.currentTarget).attr("track-id"); sessionModel.deleteTrack(sessionId, trackId); } function _toggleVisualMuteControl($control, mute) { if (mute) { $control.removeClass('enabled'); $control.addClass('muted'); } else { $control.removeClass('muted'); $control.addClass('enabled'); } } function _toggleAudioMute(mixerId, muting, mode) { fillTrackVolumeObject(mixerId, mode); context.trackVolumeObject.mute = muting; if(mode === undefined) { mode = sessionModel.getMixMode(); } context.jamClient.SessionSetControlState(mixerId, mode); } function _toggleAudioLoop(mixerId, loop, mode) { fillTrackVolumeObject(mixerId, mode); context.trackVolumeObject.loop = loop; if(mode === undefined) { mode = sessionModel.getMixMode(); } context.jamClient.SessionSetControlState(mixerId, mode); } function showMuteDropdowns($control) { $control.btOn(); } function toggleMute(evt) { var $control = $(evt.currentTarget); var muting = ($control.hasClass('enabled')); var mixerIds = $control.attr('mixer-id').split(','); // track icons have a special mute behavior if($control.is('.track-icon-mute')) { var mediaControlsDisabled = $control.data('media-controls-disabled'); if(mediaControlsDisabled) { var mediaTrackOpener = $control.data('media-track-opener'); context.JK.prodBubble($control, 'media-controls-disabled', {mediaTrackOpener:mediaTrackOpener}, {positions:['bottom'], offsetParent: $control.closest('.screen')}) return false; } if(sessionModel.areControlsLockedForJamTrackRecording() && $control.closest('.session-track').data('track_data').type == 'jam_track') { context.JK.prodBubble($control, 'jamtrack-controls-disabled', {}, {positions:['bottom'], offsetParent: $control.closest('.screen')}) return false; } if($control.data('showHelpAboutMediaMixers')) { if(!sessionModel.hasShownAudioMediaMixerHelp()) { context.JK.prodBubble($control, 'volume-media-mixers', {}, {positions:['bottom'], offsetParent: $control.closest('.screen')}) sessionModel.markShownAudioMediaMixerHelp() } } $.each(mixerIds, function(i,v) { var mixerId = v; // behavior: if this is the user's track in personal mode, then we mute the track globally // otherwise, for any other track (user+master mode, or remote track in any mode) // we just mute the type of track for that mode var mixer = $control.data('mixer'); var oppositeMixer = $control.data('opposite-mixer') if(mixer && oppositeMixer && (muteBothMasterAndPersonalGroups.indexOf(mixer.group_id) > -1)) { // this is the user's local track; mute both personal and master mode logger.debug("muting both master and personal mode mixers") _toggleAudioMute(mixer.id, muting, mixer.mode) _toggleAudioMute(oppositeMixer.id, muting, oppositeMixer.mode) } else { logger.debug("muting mixer") _toggleAudioMute(mixer.id, muting, mixer.mode) } // look for all controls matching this mixer id (important when it's personal mode + UserMusicInputGroup) var $controls = $screen.find('.track-icon-mute[mixer-id="' + mixerId +'"]'); _toggleVisualMuteControl($controls, muting); }); } else { // this path is taken for voice chat, but maybe others eventually $.each(mixerIds, function(i,v) { var mixerId = v; var mixer = $control.data('mixer'); var oppositeMixer = $control.data('opposite-mixer') if(mixer && oppositeMixer && mixer.group_id == ChannelGroupIds.AudioInputChatGroup) { _toggleAudioMute(mixer.id, muting, mixer.mode); _toggleAudioMute(oppositeMixer.id, muting, oppositeMixer.mode); } else { _toggleAudioMute(mixerId, muting); } }); _toggleVisualMuteControl($control, muting); } } function fillTrackVolumeObject(mixerId, mode, broadcast) { _updateMixers(); var _broadcast = true; if (broadcast !== undefined) { _broadcast = broadcast; } var 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; // 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; return mixer; } // Given a mixer's min/max and current value, return it as // a percent from 0-100. Return an integer. function percentFromMixerValue(min, max, value) { try { var range = Math.abs(max - min); var magnitude = value - min; var percent = Math.round(100*(magnitude/range)); return percent; } catch(err) { return 0; } } // Given a mixer's min/max and a percent value, return it as // the mixer's value. Returns an integer. function percentToMixerValue(min, max, percent) { var range = Math.abs(max - min); var multiplier = percent/100; // Change 85 into 0.85 var value = min + (multiplier * range); // Protect against percents < 0 and > 100 if (value < min) { value = min; } if (value > max) { value = max; } return value; } // Given a volume percent (0-100), set the underlying // audio volume level of the passed mixerId to the correct // value. function 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. var sliderValue = percentToMixerValue( currentMixerRangeMin, currentMixerRangeMax, volumePercent); context.trackVolumeObject.volL = context.JK.FaderHelpers.convertPercentToAudioTaper(volumePercent); context.trackVolumeObject.volR = context.JK.FaderHelpers.convertPercentToAudioTaper(volumePercent); // Special case for L2M mix: if (mixer.id === '__L2M__') { logger.debug("L2M volumePercent=" + volumePercent); var dbValue = context.JK.FaderHelpers.convertLinearToDb(volumePercent); context.jamClient.SessionSetMasterLocalMix(dbValue); // context.jamClient.SessionSetMasterLocalMix(sliderValue); } else { //var isMediaMixer = mediaTrackGroups.indexOf(mixer.group_id) > -1; // if this is a media file (Metronome, JamTrack, BackingTrack, RecordedTrack), then we only modify master //var mixMode = isMediaMixer ? MIX_MODES.MASTER : sessionModel.getMixMode(); context.jamClient.SessionSetControlState(mixer.id, mixer.mode); } } function bailOut() { promptLeave = false; context.window.location = '/client#/home'; } function sessionLeave(evt) { evt.preventDefault(); rateSession(); bailOut(); return false; } function rateSession() { if (rateSessionDialog === null) { rateSessionDialog = new context.JK.RateSessionDialog(context.JK.app); rateSessionDialog.initialize(); } rateSessionDialog.showDialog(); return true; } function sessionResync(evt) { evt.preventDefault(); var response = context.jamClient.SessionAudioResync(); if (response) { app.notify({ "title": "Error", "text": response, "icon_url": "/assets/content/icon_alert_big.png"}); } return false; } function sessionWebCam(e) { e.preventDefault(); if(webcamViewer.isVideoShared()) { $('#session-webcam').removeClass("selected") } else { $('#session-webcam').addClass("selected") } webcamViewer.toggleWebcam() return false; } // http://stackoverflow.com/questions/2604450/how-to-create-a-jquery-clock-timer function updateRecordingTimer() { function pretty_time_string(num) { return ( num < 10 ? "0" : "" ) + num; } var total_seconds = (new Date - startTimeDate) / 1000; var hours = Math.floor(total_seconds / 3600); total_seconds = total_seconds % 3600; var minutes = Math.floor(total_seconds / 60); total_seconds = total_seconds % 60; var seconds = Math.floor(total_seconds); hours = pretty_time_string(hours); minutes = pretty_time_string(minutes); seconds = pretty_time_string(seconds); if(hours > 0) { var currentTimeString = hours + ":" + minutes + ":" + seconds; } else { var currentTimeString = minutes + ":" + seconds; } $recordingTimer.text('(' + currentTimeString + ')'); } function displayStartingRecording() { $('#recording-start-stop').addClass('currently-recording'); $('#recording-status').text("Starting...") } function displayStartedRecording() { // the commented out code reflects dropping the counter as your recording to save space startTimeDate = new Date; //$recordingTimer = $("(0:00)"); var $recordingStatus = $('').append("Stop Recording")//.append($recordingTimer); $('#recording-status').html( $recordingStatus ); //recordingTimerInterval = setInterval(updateRecordingTimer, 1000); } function displayStoppingRecording(data) { if(data) { if(data.reason) { app.notify({ "title": "Recording Aborted", "text": "The recording was aborted due to '" + data.reason + '"', "icon_url": "/assets/content/icon_alert_big.png" }); } } $('#recording-status').text("Stopping..."); } function displayDoneRecording() { if(recordingTimerInterval) { clearInterval(recordingTimerInterval); recordingTimerInterval = null; startTimeDate = null; } $recordingTimer = null; $('#recording-start-stop').removeClass('currently-recording'); $('#recording-status').text("Make Recording"); } function lockControlsforJamTrackRecording() { sessionModel.lockControlsforJamTrackRecording(); } function unlockControlsforJamTrackRecording() { sessionModel.unlockControlsforJamTrackRecording(); } function displayWhoCreated(clientId) { if(app.clientId != clientId) { // don't show to creator sessionModel.findUserBy({clientId: clientId}) .done(function(user) { app.notify({ "title": "Recording Started", "text": user.name + " started a recording", "icon_url": context.JK.resolveAvatarUrl(user.photo_url) }); }) .fail(function() { app.notify({ "title": "Recording Started", "text": "Oops! Can't determine who started this recording", "icon_url": "/assets/content/icon_alert_big.png" }); }) } } function promptUserToSave(recordingId, timeline) { rest.getRecording( {id: recordingId} ) .done(function(recording) { if(timeline) { recording.timeline = timeline.global } recordingFinishedDialog.setRecording(recording); app.layout.showDialog('recordingFinished').one(EVENTS.DIALOG_CLOSED, function(e, data) { if(data.result && data.result.keep){ context.JK.prodBubble($recordingManagerViewer, 'file-manager-poke', {}, {positions:['top', 'left', 'right', 'bottom'], offsetParent: $screen.parent()}) } }) }) .fail(app.ajaxError); } function checkPendingMetronome() { if(sessionModel.jamTracks() !== null || sessionModel.recordedJamTracks() !== null) { // ignore all metronome events when jamtracks are open, because backend opens metronome mixer to play jamtrack tap-ins logger.debug("ignore checkPendingMetronome because JamTrack is open") return; } //logger.debug("checkPendingMetronome", sessionModel.isMetronomeOpen(), getMetronomeMasterMixers().length) if(sessionModel.isMetronomeOpen() && getMetronomeMasterMixers().length == 0) { var pendingMetronome = $($templatePendingMetronome.html()) // hide the open options otherAudioFilled(); // fill out the 'media' name $('.session-recordings .session-recording-name').text('Metronome') // and hide the close button $closePlaybackRecording.hide(); // avoid double addition of pending metronome if($otherAudioContainer.find('.pending-metronome').length === 0) { $otherAudioContainer.append(pendingMetronome) } } else { $('.session-recordings .pending-metronome').remove() } } function openBackingTrack(e) { if($openBackingTrack.is('.disabled')) { logger.debug("backing track dialog already open") return false; } // just ignore the click if they are currently recording for now if(sessionModel.recordingModel.isRecording()) { app.notify({ "title": "Currently Recording", "text": "You can't open a backing track while creating a recording.", "icon_url": "/assets/content/icon_alert_big.png" }); return false; } $openBackingTrack.addClass('disabled'); context.jamClient.ShowSelectBackingTrackDialog("window.JK.HandleBackingTrackSelectedCallback"); return false; } function openJamTrack(e) { // just ignore the click if they are currently recording for now if(sessionModel.recordingModel.isRecording()) { app.notify({ "title": "Currently Recording", "text": "You can't open a jam track while creating a recording.", "icon_url": "/assets/content/icon_alert_big.png" }); return false; } app.layout.showDialog('open-jam-track-dialog').one(EVENTS.DIALOG_CLOSED, function(e, data) { // once the dialog is closed, see if the user has a jamtrack selected if(!data.canceled && data.result.jamTrack) { loadJamTrack(data.result.jamTrack); } else { logger.debug("OpenJamTrack dialog closed with no selection; ignoring", data) } }) return false; } function loadJamTrack(jamTrack) { $('.session-recording-name').text(''); // hide 'other audio' placeholder otherAudioFilled(); if(downloadJamTrack) { // if there was one showing before somehow, destroy it. logger.warn("destroying existing JamTrack") downloadJamTrack.root.remove(); downloadJamTrack.destroy(); downloadJamTrack = null } downloadJamTrack = new context.JK.DownloadJamTrack(app, jamTrack, 'large'); // the widget indicates when it gets to any transition; we can hide it once it reaches completion $(downloadJamTrack).on(EVENTS.JAMTRACK_DOWNLOADER_STATE_CHANGED, function(e, data) { if(data.state == downloadJamTrack.states.synchronized) { logger.debug("jamtrack synchronized; hide widget and show tracks") downloadJamTrack.root.remove(); downloadJamTrack.destroy(); downloadJamTrack = null; // XXX: test with this removed; it should be unnecessary context.jamClient.JamTrackStopPlay(); var sampleRate = context.jamClient.GetSampleRate() var sampleRateForFilename = sampleRate == 48 ? '48' : '44' var fqId = jamTrack.id + '-' + sampleRateForFilename if(jamTrack.jmep) { logger.debug("setting jmep data") context.jamClient.JamTrackLoadJmep(fqId, jamTrack.jmep) } else { logger.debug("no jmep data for jamtrack") } // JamTrackPlay means 'load' var result = context.jamClient.JamTrackPlay(fqId); if(!result) { app.notify( { title: "JamTrack Can Not Open", text: "Unable to open your JamTrack. Please contact support@jamkazam.com" }, null, true); } else { playJamTrack(jamTrack.id); } } }) // show it on the page $otherAudioContainer.append(downloadJamTrack.root) // kick off the download JamTrack process downloadJamTrack.init() } function playJamTrack(jamTrackId) { var participantCnt=sessionModel.participants().length rest.playJamTrack(jamTrackId) .done(function() { app.refreshUser(); }) context.stats.write('web.jamtrack.open', { value: 1, session_size: participantCnt, user_id: context.JK.currentUserId, user_name: context.JK.currentUserName }) }// function function openBackingTrackFile(e) { // just ignore the click if they are currently recording for now if(sessionModel.recordingModel.isRecording()) { app.notify({ "title": "Currently Recording", "text": "You can't open a backing track while creating a recording.", "icon_url": "/assets/content/icon_alert_big.png" }); return false; } else { context.jamClient.openBackingTrackFile(sessionModel.backing_track) context.stats.write('web.backingtrack.open', { value: 1, session_size: participantCnt, user_id: context.JK.currentUserId, user_name: context.JK.currentUserName }) //context.JK.CurrentSessionModel.refreshCurrentSession(true); } return false; } function unstableNTPClocks() { var unstable = [] // This should be handled in the below loop, actually: var myState = context.jamClient.getMyNetworkState() var map; $.each(sessionModel.participants(), function(index, participant) { var isSelf = participant.client_id == app.clientId; if(isSelf) { var isStable = myState.ntp_stable; } else { map = context.jamClient.getPeerState(participant.client_id) var isStable = map.ntp_stable; } if (!isStable) { var name = participant.user.name; if (!(name)) { name = participant.user.first_name + ' ' + participant.user.last_name; } if (isSelf) { name += " (this computer)" } unstable.push(name) } }); return unstable } function openMetronome(e) { // just ignore the click if they are currently recording for now if(sessionModel.recordingModel.isRecording()) { app.notify({ "title": "Currently Recording", "text": "You can't open a metronome while creating a recording.", "icon_url": "/assets/content/icon_alert_big.png" }); return false; } else { var unstable = unstableNTPClocks() if (sessionModel.participants().length > 1 && unstable.length > 0) { var names = unstable.join(", ") logger.debug("Unstable clocks: ", names, unstable) context.JK.Banner.showAlert("Couldn't open metronome", context._.template($('#template-help-metronome-unstable').html(), {names: names}, { variable: 'data' })); } else { var data = { value: 1, session_size: sessionModel.participants().length, user_id: context.JK.currentUserId, user_name: context.JK.currentUserName } context.stats.write('web.metronome.open', data) var bpm = 120; logger.debug("opening the metronome with bpm: " + bpm + ", sound:" + metroSound) rest.openMetronome({id: sessionModel.id()}) .done(function() { context.jamClient.SessionStopPlay(); context.jamClient.SessionOpenMetronome(bpm, metroSound, 1, 0); }) .fail(function(jqXHR) { logger.debug(jqXHR, jqXHR) app.notify({ "title": "Couldn't open metronome", "text": "Couldn't inform the server to open metronome. msg=" + jqXHR.responseText, "icon_url": "/assets/content/icon_alert_big.png" }); }); } return false; } } function openRecording(e) { // just ignore the click if they are currently recording for now if(sessionModel.recordingModel.isRecording()) { app.notify({ "title": "Currently Recording", "text": "You can't open a recording while creating a recording.", "icon_url": "/assets/content/icon_alert_big.png" }); return false; } if(!localRecordingsDialog.isShowing()) { app.layout.showDialog('localRecordings'); } return false; } function closeOpenMedia() { if(sessionModel.recordedTracks()) { closeRecording(); } else if(sessionModel.jamTracks() || downloadJamTrack) { closeJamTrack(); } else if(sessionModel.backingTrack() && sessionModel.backingTrack().path) { closeBackingTrack(); } else if(getMetronomeMasterMixers().length > 0) { closeMetronomeTrack(); } else { logger.error("don't know how to close open media"); } return false; } function closeBackingTrack() { if (sessionModel.recordingModel.isRecording()) { logger.debug("can't close backing track while recording") return false; } rest.closeBackingTrack({id: sessionModel.id()}) .done(function() { //sessionModel.refreshCurrentSession(true); }) .fail(function(jqXHR) { app.notify({ "title": "Couldn't Close Backing Track", "text": "Couldn't inform the server to close Backing Track. msg=" + jqXHR.responseText, "icon_url": "/assets/content/icon_alert_big.png" }); }); // '' closes all open backing tracks context.jamClient.SessionStopPlay(); context.jamClient.SessionCloseBackingTrackFile(''); return false; } function closeJamTrack() { logger.debug("closing jam track"); if (sessionModel.recordingModel.isRecording()) { logger.debug("can't close jamtrack while recording") app.notify({title: 'Can Not Close JamTrack', text: 'A JamTrack can not be closed while recording.'}) return false; } if(!sessionModel.selfOpenedJamTracks()) { logger.debug("can't close jamtrack if not the opener") app.notify({title: 'Can Not Close JamTrack', text: 'Only the person who opened the JamTrack can close it.'}) return false; } if(!sessionModel.selfOpenedJamTracks()) { logger.debug("can't close jamtrack if not the opener") return false; } if(downloadJamTrack) { logger.debug("closing DownloadJamTrack widget") downloadJamTrack.root.remove(); downloadJamTrack.destroy(); downloadJamTrack = null; // this is necessary because a syncing widget means no jamtracks are loaded; // so removing the widget will not cause a backend media change event (and so renderSession will not be called, ultimately) resetOtherAudioContent(); } rest.closeJamTrack({id: sessionModel.id()}) .done(function() { sessionModel.refreshCurrentSession(true); }) .fail(function(jqXHR) { app.notify({ "title": "Couldn't Close JamTrack", "text": "Couldn't inform the server to close JamTrack. msg=" + jqXHR.responseText, "icon_url": "/assets/content/icon_alert_big.png" }); }); context.jamClient.JamTrackStopPlay(); return false; } function closeMetronomeTrack() { rest.closeMetronome({id: sessionModel.id()}) .done(function() { logger.debug("session: SessionCloseMetronome") context.jamClient.SessionCloseMetronome(); sessionModel.refreshCurrentSession(true); }) .fail(function(jqXHR) { app.notify({ "title": "Couldn't Close MetronomeTrack", "text": "Couldn't inform the server to close MetronomeTrack. msg=" + jqXHR.responseText, "icon_url": "/assets/content/icon_alert_big.png" }); }); return false; } function closeRecording() { logger.debug("closing recording"); rest.stopPlayClaimedRecording({id: sessionModel.id(), claimed_recording_id: sessionModel.getCurrentSession().claimed_recording.id}) .done(function(response) { //sessionModel.refreshCurrentSession(true); // update session info context.JK.CurrentSessionModel.updateSession(response); }) .fail(function(jqXHR) { app.notify({ "title": "Couldn't Stop Recording Playback", "text": "Couldn't inform the server to stop playback. msg=" + jqXHR.responseText, "icon_url": "/assets/content/icon_alert_big.png" }); }); context.jamClient.CloseRecording(); return false; } function onPause(e, data) { // if a JamTrack is open, and the user hits 'pause' or 'stop', we need to automatically stop the recording if(sessionModel.jamTracks() && sessionModel.recordingModel.isRecording()) { logger.debug("preemptive jamtrack stop") startStopRecording(); } if(!data.endReached) { logger.debug("calling jamClient.SessionPausePlay. endReached:", data.endReached); context.jamClient.SessionPausePlay(); } } function onStop(e, data) { // if a JamTrack is open, and the user hits 'pause' or 'stop', we need to automatically stop the recording if(sessionModel.jamTracks() && sessionModel.recordingModel.isRecording()) { logger.debug("preemptive jamtrack stop") startStopRecording(); } if(!data.endReached) { logger.debug("calling jamClient.SessionStopPlay. endReached:", data.endReached); context.jamClient.SessionStopPlay(); } } function onPlay(e, data) { logger.debug("calling jamClient.SessionStartPlay"); context.jamClient.SessionStartPlay(data.playbackMode); } function onChangePlayPosition(e, data){ var seek = data.positionMs; if(data.playbackMonitorMode == context.JK.PLAYBACK_MONITOR_MODE.JAMTRACK) { // if positionMs == 0, then seek it back to whatever the earliest play start is to catch all the prelude if(seek == 0) { var duration = context.jamClient.SessionGetJamTracksPlayDurationMs(); seek = duration.start; } } logger.debug("calling jamClient.SessionTrackSeekMs(" + seek + ")"); if(data.playbackMonitorMode == context.JK.PLAYBACK_MONITOR_MODE.JAMTRACK) { // this doesn't ever show anything, because of blocking nature of the seek call //var $mediaSeeking = $screen.find('.media-seeking') //$mediaSeeking.attr('data-mode', 'SEEKING') context.jamClient.SessionJamTrackSeekMs(seek); //$mediaSeeking.attr('data-mode', '') } else { context.jamClient.SessionTrackSeekMs(seek); } } function startStopRecording() { // check first if a jamtrack is loaded, and playing; if so, tell user to stop the play /**if(sessionModel.jamTracks() && context.jamClient.isSessionTrackPlaying()) { app.notify( { title: "Can't Recording a Play JamTrack", text: "Stop the JamTrack before trying to recording." }, null, true); return; }*/ if(sessionModel.recordingModel.isRecording()) { sessionModel.recordingModel.stopRecording(); } else { sessionModel.recordingModel.startRecording(); } } function inviteMusicians() { friendInput = inviteMusiciansUtil.inviteSessionUpdate('#update-session-invite-musicians', sessionId); inviteMusiciansUtil.loadFriends(); $(friendInput).show(); } function setFormFromMetronome() { $("select.metro-tempo").val(metroTempo) $("select.metro-sound").val(metroSound) } function setMetronomePlaybackMode() { $metronomePlaybackSelect.metronomeSetPlaybackMode(metroCricket ? 'cricket' : 'self') } function setMetronomeFromForm() { var tempo = $("select.metro-tempo:visible option:selected").val() var sound = $("select.metro-sound:visible option:selected").val() var t = parseInt(tempo) var s if (tempo==NaN || tempo==0 || tempo==null) { t = 120 } if (sound==null || typeof(sound)=='undefined' || sound=="") { s = "Beep" } else { s = sound } logger.debug("Setting tempo and sound:", t, s) metroTempo = t metroSound = s context.jamClient.SessionSetMetronome(t, s, 1, 0); } function onMetronomeChanged(e, data) { setMetronomeFromForm() } function metronomePlaybackModeChanged(e, data) { var mode = data.playbackMode; // will be either 'self' or 'cricket' logger.debug("setting metronome playback mode: ", mode) var isCricket = mode == 'cricket'; context.jamClient.setMetronomeCricketTestState(isCricket); } function onMixerModeChanged(e, data) { $mixModeDropdown.easyDropDown('select', data.mode, true); setTimeout(renderSession, 1); } function onUserChangeMixMode(e) { var mode = $mixModeDropdown.val() == "master" ? MIX_MODES.MASTER : MIX_MODES.PERSONAL; context.jamClient.SetMixerMode(mode) modUtils.shouldShow(NAMED_MESSAGES.MASTER_VS_PERSONAL_MIX).done(function(shouldShow) { if(shouldShow) { var modeChangeHtml = $($templateMixerModeChange.html()); context.JK.Banner.show({title: 'Master vs. Personal Mix', text: modeChangeHtml, no_show: NAMED_MESSAGES.MASTER_VS_PERSONAL_MIX}); } }) return true; } function showFTUEWhenNoInputs( ) { //app.afterFtue = function() { window.location.reload }; //app.layout.startNewFtue(); window.location = '/client#/account/audio' } function events() { $('#session-leave').on('click', sessionLeave); $('#session-resync').on('click', sessionResync); $('#session-webcam').on('click', sessionWebCam); $('#session-contents').on("click", '[action="delete"]', deleteSession); $tracksHolder.on('click', 'div[control="mute"]', toggleMute); $('#recording-start-stop').on('click', startStopRecording); $('#open-a-recording').on('click', openRecording); $('#open-a-jamtrack').on('click', openJamTrack); $openBackingTrack.on('click', openBackingTrack); $('#open-a-metronome').on('click', openMetronome); $('#session-invite-musicians').on('click', inviteMusicians); $('#session-invite-musicians2').on('click', inviteMusicians); $('#track-settings').click(function() { if(gearUtils.isNoInputProfile()) { // show FTUE // showFTUEWhenNoInputs(); app.notify({title:'Settings Disabled', text:'You can not alter any settings for the System Default playback device.'}) return false; } else { configureTrackDialog.refresh(); configureTrackDialog.showVoiceChatPanel(true); configureTrackDialog.showMusicAudioPanel(true); } }); $openFtue.click(function() { showFTUEWhenNoInputs(); return false; }) $closePlaybackRecording.on('click', closeOpenMedia); $(playbackControls) .on('pause', onPause) .on('stop', onStop) .on('play', onPlay) .on('change-position', onChangePlayPosition); $(friendInput).focus(function() { $(this).val(''); }) $(document).on(EVENTS.MIXER_MODE_CHANGED, onMixerModeChanged) $mixModeDropdown.change(onUserChangeMixMode) $(document).on("change", ".metronome-select", onMetronomeChanged) $metronomePlaybackSelect.metronomePlaybackMode().on(EVENTS.METRONOME_PLAYBACK_MODE_SELECTED, metronomePlaybackModeChanged) context.JK.helpBubble($metronomePlaybackHelp, 'metromone-playback-modes', {} , {offsetParent: $screen, width:'400px'}); $(document).on('layout_resized', function() { console.log("RESIZE FLUID") resizeFluid(); }); } this.initialize = function(localRecordingsDialogInstance, recordingFinishedDialogInstance, friendSelectorDialog) { inviteMusiciansUtil = new JK.InviteMusiciansUtil(JK.app); inviteMusiciansUtil.initialize(friendSelectorDialog); localRecordingsDialog = localRecordingsDialogInstance; recordingFinishedDialog = recordingFinishedDialogInstance; context.jamClient.SetVURefreshRate(150); context.jamClient.RegisterVolChangeCallBack("JK.HandleVolumeChangeCallback"); playbackControls = new context.JK.PlaybackControls($('.session-recordings .recording-controls')); context.jamClient.setMetronomeOpenCallback("JK.HandleMetronomeCallback") var screenBindings = { 'beforeShow': beforeShow, 'afterShow': afterShow, 'beforeHide': beforeHide, 'beforeLeave' : beforeLeave, 'beforeDisconnect' : beforeDisconnect, }; //app.bindScreen('session', screenBindings); $recordingManagerViewer = $('#recording-manager-viewer'); $screen = $('#session-screen-old'); $mixModeDropdown = $screen.find('select.monitor-mode') $templateMixerModeChange = $('#template-mixer-mode-change'); $otherAudioContainer = $('#session-recordedtracks-container'); $myTracksNoTracks = $('#session-mytracks-notracks') $openFtue = $screen.find('.open-ftue-no-tracks') $myTracksContainer = $('#session-mytracks-container') $liveTracksContainer = $('#session-livetracks-container'); $closePlaybackRecording = $('#close-playback-recording') $openBackingTrack = $('#open-a-backingtrack'); $metronomePlaybackSelect = $('#metronome-playback-select') $metronomePlaybackHelp = $('#metronome-playback-help') $templatePendingMetronome = $('#template-pending-metronome'); $myTracks = $screen.find('.session-mytracks'); $liveTracks = $screen.find('.session-livetracks'); $audioTracks = $screen.find('.session-recordings'); $fluidTracks = $screen.find('.session-fluidtracks'); $voiceChat = $screen.find('#voice-chat'); $tracksHolder = $screen.find('#tracks') if(gon.global.video_available && gon.global.video_available!="none") { webcamViewer.init($(".webcam-container")) webcamViewer.setVideoOff() } events(); // make sure no previous plays are still going on by accident context.jamClient.SessionStopPlay(); if(context.jamClient.SessionRemoveAllPlayTracks) { // upgrade guard context.jamClient.SessionRemoveAllPlayTracks(); } }; this.tracks = tracks; this.getCurrentSession = function() { return sessionModel.getCurrentSession(); }; this.refreshCurrentSession = function(force) { sessionModel.refreshCurrentSession(force); }; this.setPromptLeave = function(_promptLeave) { promptLeave = _promptLeave; } this.onPlaybackStateChange = function(change_type){ // if it's play_stop or play_start, poke the playControls if(change_type == 'play_start') { playbackControls.onPlayStartEvent(); } else if(change_type == 'play_stop'){ playbackControls.onPlayStopEvent(); } else if(change_type == 'play_pause'){ playbackControls.onPlayPauseEvent(); } } context.JK.HandleVolumeChangeCallback = handleVolumeChangeCallback; context.JK.HandleMetronomeCallback = handleMetronomeCallback; context.JK.HandleBridgeCallback = handleBridgeCallback; context.JK.HandleBackingTrackSelectedCallback = handleBackingTrackSelectedCallback; }; })(window,jQuery);