Merge branch 'develop' into feature/musician_profile_enhancements

This commit is contained in:
Brian Smith 2015-03-04 02:03:22 -05:00
commit 12b610d2cb
25 changed files with 539 additions and 189 deletions

View File

@ -2,20 +2,16 @@
= f.semantic_errors *f.object.errors.keys
= f.inputs name: 'JamTrack fields' do
= f.input :name, :input_html => { :rows=>1, :maxlength=>200 }
b style='margin-left:10px'
i
| JamTrack should only be made available (to end users) if all its sub-component are in place:
= f.input :available, as: :boolean
= f.input :description, :input_html => { :rows=>5, :maxlength=>1000 }
= f.input :plan_code, :label=>'Recurly Plan Code', :required=>true, :hint => 'Must match plan code in Recurly'
= f.input :version, :label => 'Version', :hint => 'Increment this value whenever you invalidate (update) the definition of this JamTrack'
= f.input :version, :label => 'Version', :hint => 'Increment this value whenever you invalidate (update) the media in the JamTrack. Changing JMEP does not count as a version change; changing anything about a track (audio, instrument, part) does.'
//= f.input :initial_play_silence, :label => 'Initial Play Silence (seconds)'
= f.input :time_signature, collection: JamRuby::JamTrack::TIME_SIGNATURES, include_blank: false
= f.input :status, collection: JamRuby::JamTrack::STATUS, include_blank: false
= f.input :status, collection: JamRuby::JamTrack::STATUS, include_blank: false, hint: 'Only set to Production when end users should be able to purchase this JamTrack'
= f.input :recording_type, collection: JamRuby::JamTrack::RECORDING_TYPE, include_blank: false
= f.input :original_artist, :input_html => { :rows=>2, :maxlength=>200 }
= f.input :songwriter, :input_html => { :rows=>5, :maxlength=>1000 }
= f.input :publisher, :input_html => { :rows=>5, :maxlength=>1000 }
= f.input :original_artist, :input_html => { :rows=>1, :maxlength=>1000 }
= f.input :songwriter, :input_html => { :rows=>1, :maxlength=>1000 }
= f.input :publisher, :input_html => { :rows=>1, :maxlength=>1000 }
= f.input :licensor, collection: JamRuby::JamTrackLicensor.all, include_blank: false
= f.input :pro, collection: JamRuby::JamTrack::PRO, include_blank: false
= f.input :genre, collection: JamRuby::Genre.all, include_blank: false
@ -26,16 +22,13 @@
= f.input :reproduction_royalty_amount, :required=>true, :input_html=>{type:'numeric'}
= f.input :licensor_royalty_amount, :required=>true, :input_html=>{type:'numeric'}
= f.input :pro_royalty_amount, :required=>true, :input_html=>{type:'numeric'}
= f.input :url, :as => :file, :label => 'Audio File'
= f.input :jmep_text, :as => :text, :label => "JMEP Text", :input_html => {:rows => 5 }
= f.input :jmep_json, :as => :text, :label => "JMEP Json", :input_html => {:rows => 5, :readonly=>true }, :hint => 'readonly'
//= f.input :url, :as => :file, :label => 'Audio File'
= f.input :jmep_text, :as => :text, :label => "JMEP Text", :input_html => {:rows => 5 }, :hint => 'Tap-Ins & Lead Silence. Examples: https://jamkazam.atlassian.net/wiki/pages/viewpage.action?pageId=39289025#JamKazamMeta-EventProcessor(JMEP)-CommonExamples'
= f.input :jmep_json, :as => :text, :label => "JMEP Json", :input_html => {:rows => 5, :readonly => true }, :hint => 'Readonly field. This is shown here just so you can see what your JMEP got converted to readily'
= f.semantic_fields_for :jam_track_tracks do |track|
= render 'jam_track_track_fields', f: track
= f.semantic_fields_for :jam_track_tap_ins do |tap_in|
= render 'jam_track_tap_in_fields', f: tap_in
.links
= link_to_add_association 'Add Track', f, :jam_track_tracks, class: 'button', style: 'margin:20px;padding:10px 20px'
= link_to_add_association 'Add Tap In', f, :jam_track_tap_ins, class: 'button', style: 'margin:20px;padding:10px 20px'
= f.actions

View File

@ -1,7 +1,7 @@
= f.inputs name: 'Track fields' do
ol.nested-fields
= f.input :track_type, :as => :select, collection: JamRuby::JamTrackTrack::TRACK_TYPE, include_blank: false
= f.input :track_type, :as => :select, collection: ['Track', 'Master'], include_blank: false
= f.input :instrument, collection: Instrument.all, include_blank: false
= f.input :part, :required=>true, :input_html => { :rows=>1, :maxlength=>20, :type=>'numeric' }

View File

@ -31,6 +31,8 @@ module JamRuby
validate :validate_opening_recording, :if => :opening_recording
validate :validate_opening_jam_track, :if => :opening_jam_track
validate :validate_opening_backing_track, :if => :opening_backing_track
# not sure if this is helpful since if one opens, it always stays open
validate :validate_opening_metronome, :if => :opening_metronome
after_create :started_session
@ -101,11 +103,6 @@ module JamRuby
end
def validate_other_audio(error_key)
# validate that there is no metronome already open in this session
if metronome_active_was
errors.add(error_key, ValidationMessages::METRONOME_ALREADY_OPEN)
end
# validate that there is no backing track already open in this session
if backing_track_path_was.present?
errors.add(error_key, ValidationMessages::BACKING_TRACK_ALREADY_OPEN)

View File

@ -73,7 +73,7 @@ module JamRuby
query = query.where("jam_track_rights.user_id = ?", user.id)
end
query = query.where("jam_tracks.available = ?", true) unless user.admin
query = query.where("jam_tracks.status = ?", 'Production') unless user.admin
query = query.where("jam_tracks.genre_id = '#{options[:genre]}'") unless options[:genre].blank?
query = query.where("jam_track_tracks.instrument_id = '#{options[:instrument]}'") unless options[:instrument].blank?
query = query.where("jam_tracks.sales_region = '#{options[:availability]}'") unless options[:availability].blank?

View File

@ -5,7 +5,7 @@ module JamRuby
include JamRuby::S3ManagerMixin
# there should only be one Master per JamTrack, but there can be N Track per JamTrack
TRACK_TYPE = %w{Master Track}
TRACK_TYPE = %w{Track Master}
mount_uploader :url, JamTrackTrackUploader

View File

@ -955,15 +955,6 @@ describe ActiveMusicSession do
@music_session.metronome_initiator.should be_nil
end
it "disallow a metronome to be opened when another is already opened" do
# if a metronome is open, don't allow another to be opened
@music_session.open_metronome(@user1)
@music_session.errors.any?.should be_false
@music_session.open_metronome(@user1)
@music_session.errors.any?.should be_true
@music_session.errors[:metronome] == [ValidationMessages::METRONOME_ALREADY_OPEN]
end
it "disallow a metronome to be opened when recording is ongoing" do
@recording = Recording.start(@music_session, @user1)
@music_session.errors.any?.should be_false

View File

@ -46,9 +46,15 @@
CONNECTION_UP: 'connection_up',
CONNECTION_DOWN: 'connection_down',
SCREEN_CHANGED: 'screen_changed',
JAMTRACK_DOWNLOADER_STATE_CHANGED: 'jamtrack_downloader_state'
JAMTRACK_DOWNLOADER_STATE_CHANGED: 'jamtrack_downloader_state',
METRONOME_PLAYBACK_MODE_SELECTED: 'metronome_playback_mode_selected'
};
context.JK.PLAYBACK_MONITOR_MODE = {
MEDIA_FILE: 'MEDIA_FILE',
JAMTRACK: 'JAMTRACK',
METRONOME: 'METRONOME'
}
context.JK.ALERT_NAMES = {
NO_EVENT : 0,
BACKEND_ERROR : 1, //generic error - eg P2P message error

View File

@ -0,0 +1,96 @@
(function(context, $) {
"use strict";
context.JK = context.JK || {};
// creates an iconic/graphical instrument selector. useful when there is minimal real-estate
function setValue(val, $target) {
if(val == "cricket") {
$target.html("<span class='metronome-state'>Play cluster test<span class='down-arrow'></span></span>")
}
else {
$target.html("<span class='metronome-state'>Play metronome<span class='down-arrow'></span></span>")
}
}
$.fn.metronomeSetPlaybackMode = function(val) {
return this.each(function (index) {
setValue(val, $(this))
});
};
$.fn.metronomePlaybackMode = function(options) {
options = options || {mode: 'self'}
return this.each(function(index) {
function close() {
$parent.btOff();
$parent.focus();
}
var $parent = $(this);
var value = options.mode;
setValue(options.mode, $parent)
function onModeSelected() {
var $li = $(this);
var playbackMode = $li.attr('data-playback-option');
value = playbackMode;
close();
$parent.triggerHandler(context.JK.EVENTS.METRONOME_PLAYBACK_MODE_SELECTED, {playbackMode: playbackMode});
return false;
};
// if the user goes into the bubble, remove
function waitForBubbleHover($bubble) {
$bubble.hoverIntent({
over: function() {
if(timeout) {
clearTimeout(timeout);
timeout = null;
}
},
out: function() {
$parent.btOff();
}});
}
var timeout = null;
context.JK.hoverBubble($parent, $('#template-metronome-playback-mode').html(), {
trigger:'click',
cssClass: 'metronome-playback-mode-selector-popup',
spikeGirth:0,
spikeLength:0,
width:160,
closeWhenOthersOpen: true,
offsetParent: $parent.offsetParent(),
positions:['top'],
preShow: function() {
$parent.find('.down-arrow').removeClass('down-arrow').addClass('up-arrow')
},
postShow:function(container) {
$(container).find('li').click(onModeSelected)
if(timeout) {
clearTimeout(timeout);
timeout = null;
}
//waitForBubbleHover($(container))
//timeout = setTimeout(function() {$parent.btOff()}, 3000)
},
postHide:function() {
setValue(value, $parent)
}
});
});
}
})(window, jQuery);

View File

@ -25,6 +25,8 @@
logger.debug("no $parentElement specified in PlaybackControls");
}
var PLAYBACK_MONITOR_MODE = context.JK.PLAYBACK_MONITOR_MODE;
var $playButton = $('.play-button img.playbutton', $parentElement);
var $pauseButton = $('.play-button img.pausebutton', $parentElement);
var $currentTime = $('.recording-current', $parentElement);
@ -47,19 +49,19 @@
var canUpdateBackend = false;
var playbackMode = PlaybackMode.EveryWhere;
var monitorPlaybackTimeout = null;
var jamTrackMode = false; // if true, we use different APIs to determine playback info
var playbackMonitorMode = PLAYBACK_MONITOR_MODE.MEDIA_FILE;
function startPlay() {
updateIsPlaying(true);
if(endReached) {
update(0, playbackDurationMs, playbackPlaying);
}
$self.triggerHandler('play', {playbackMode: playbackMode});
$self.triggerHandler('play', {playbackMode: playbackMode, playbackMonitorMode: playbackMonitorMode});
}
function stopPlay() {
function stopPlay(endReached) {
updateIsPlaying(false);
$self.triggerHandler('pause');
$self.triggerHandler('pause', {playbackMode: playbackMode, playbackMonitorMode: playbackMonitorMode, endReached : endReached});
}
function updateOffsetBasedOnPosition(offsetLeft) {
@ -68,7 +70,7 @@
playbackPositionMs = parseInt((offsetLeft / sliderBarWidth) * playbackDurationMs);
updateCurrentTimeText(playbackPositionMs);
if(canUpdateBackend) {
$self.triggerHandler('change-position', {positionMs: playbackPositionMs, jamTrackMode: jamTrackMode});
$self.triggerHandler('change-position', {positionMs: playbackPositionMs, playbackMonitorMode: playbackMonitorMode});
canUpdateBackend = false;
}
}
@ -156,8 +158,24 @@
setPlaybackMode(playmode);
});
function styleControls( ) {
$parentElement.removeClass('mediafile-mode jamtrack-mode metronome-mode');
if(playbackMonitorMode == PLAYBACK_MONITOR_MODE.MEDIA_FILE) {
$parentElement.addClass('mediafile-mode');
}
else if(playbackMonitorMode == PLAYBACK_MONITOR_MODE.JAMTRACK) {
$parentElement.addClass('jamtrack-mode');
}
else if(playbackMonitorMode == PLAYBACK_MONITOR_MODE.METRONOME) {
$parentElement.addClass('metronome-mode');
}
else
{
throw "unknown playbackMonitorMode: " + playbackMonitorMode;
}
}
function monitorRecordingPlayback() {
if(jamTrackMode) {
if(playbackMonitorMode == PLAYBACK_MONITOR_MODE.JAMTRACK) {
var positionMs = context.jamClient.SessionCurrrentJamTrackPlayPosMs();
var duration = context.jamClient.SessionGetJamTracksPlayDurationMs();
var durationMs = duration.media_len;
@ -176,7 +194,13 @@
positionMs = 0;
}
update(positionMs, durationMs, isPlaying);
if(playbackMonitorMode == PLAYBACK_MONITOR_MODE.METRONOME) {
updateIsPlaying(isPlaying);
}
else {
update(positionMs, durationMs, isPlaying);
}
monitorPlaybackTimeout = setTimeout(monitorRecordingPlayback, 500);
}
@ -194,7 +218,7 @@
isPlaying = false;
durationTimeMs = playbackDurationMs;
currentTimeMs = playbackDurationMs;
stopPlay();
stopPlay(true);
endReached = true;
logger.debug("end reached");
}
@ -279,14 +303,23 @@
}
}
function startMonitor(_jamTrackMode) {
function startMonitor(_playbackMonitorMode) {
jamTrackMode = !!_jamTrackMode;
if(_playbackMonitorMode === undefined || _playbackMonitorMode === null) {
playbackMonitorMode = PLAYBACK_MONITOR_MODE.MEDIA_FILE;
}
else {
playbackMonitorMode = _playbackMonitorMode;
}
logger.debug("playbackControl.startMonitor " + playbackMonitorMode + "")
styleControls();
monitorRecordingPlayback();
}
function stopMonitor() {
logger.debug("playbackControl.stopMonitor")
if(monitorPlaybackTimeout!= null) {
clearTimeout(monitorPlaybackTimeout);
monitorPlaybackTimeout = null;

View File

@ -55,6 +55,16 @@
"MetronomeGroup": 12
};
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 = {};
@ -83,12 +93,14 @@
var claimedRecording = null;
var backing_track_path = null;
var jamTrack = null;
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;
@ -100,7 +112,10 @@
var downloadJamTrack = null;
var $closePlaybackRecording = null;
var $openBackingTrack = null;
var $metronomePlaybackSelect = null;
var $metronomePlaybackHelp = null;
var mediaTrackGroups = [ChannelGroupIds.MediaTrackGroup, ChannelGroupIds.JamTrackGroup, ChannelGroupIds.MetronomeGroup];
var muteBothMasterAndPersonalGroups = [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.
@ -461,39 +476,69 @@
sessionUtils.SessionPageLeave();
}
function handleTransitionsInRecordingPlayback() {
// let's see if we detect a transition to start playback or stop playback
function getMetronomeMasterMixers() {
return _mixersForGroupId(ChannelGroupIds.MetronomeGroup, MIX_MODES.MASTER);
}
var currentSession = sessionModel.getCurrentSession();
function checkMetronomeTransition() {
// trust backend over server
var metronomeMasterMixers = getMetronomeMasterMixers();
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
playbackControls.startMonitor();
}
else if(claimedRecording && (currentSession == null || currentSession.claimed_recording == null)) {
playbackControls.stopMonitor();
}
claimedRecording = currentSession == null ? null : currentSession.claimed_recording;
if(backing_track_path == null && (currentSession && currentSession.backing_track_path != null)) {
playbackControls.startMonitor();
}
else if(backing_track_path && (currentSession == null || currentSession.backing_track_path == null)) {
playbackControls.stopMonitor();
}
backing_track_path = currentSession == null ? null : currentSession.backing_track_path;
if(jamTrack == null && (currentSession && currentSession.jam_track != null)) {
playbackControls.startMonitor(true);
}
else if(jamTrack && (currentSession == null || currentSession.jam_track == null)) {
playbackControls.stopMonitor();
}
jamTrack = currentSession == null ? null : currentSession.jam_track;
if (metronomeMixer == null && metronomeMasterMixers.length > 0) {
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 (jamTrack == null && (currentSession && currentSession.jam_track != null)) {
playbackControls.startMonitor(context.JK.PLAYBACK_MONITOR_MODE.JAMTRACK);
}
else if (jamTrack && (currentSession == null || currentSession.jam_track == null)) {
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)) {
playbackControls.startMonitor();
}
else if (backing_track_path && (currentSession == null || currentSession.backing_track_path == null)) {
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
playbackControls.startMonitor();
}
else if (claimedRecording && (currentSession == null || currentSession.claimed_recording == null)) {
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() {
@ -510,8 +555,13 @@
* you must iterate. Convenience method to locate a particular
* mixer by id.
*/
function getMixer(mixerId) {
return allMixers[mixerId];
function getMixer(mixerId, mode) {
if(mode === undefined) {
mode = sessionModel.getMixMode();
}
return allMixers[(mode ? 'M' : 'P') + mixerId];
}
function getMixerByResourceId(resourceId, mode) {
@ -632,7 +682,7 @@
var i;
for(i = 0; i < masterMixers.length; i++) {
var masterMixer = masterMixers[i];
allMixers[masterMixer.id] = masterMixer; // populate allMixers by mixer.id
allMixers['M' + masterMixer.id] = masterMixer; // populate allMixers by mixer.id
// populate mixer pair
var mixerPair = {}
@ -643,16 +693,7 @@
for(i = 0; i < personalMixers.length; i++) {
var personalMixer = personalMixers[i];
if(personalMixer.group_id == ChannelGroupIds.MediaTrackGroup) {
// the reason we do this is because some media tracks have same ID in both master and personal moe
personalMixer.uniqueId = 'P--' + personalMixer.id
allMixers[personalMixer.uniqueId] = personalMixer
}
else {
allMixers[personalMixer.id] = personalMixer
}
allMixers['P' + personalMixer.id] = personalMixer
// populate other side of mixer pair
@ -1000,6 +1041,7 @@
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:
@ -1070,7 +1112,7 @@
if(isOpener) {
var oppositeMixer = getMixerByResourceId(mixer.rid, MIX_MODES.PERSONAL);
var mixerId = mixer.id + "," + oppositeMixer.uniqueId
var mixerId = mixer.id + "," + oppositeMixer.id
}
else {
var mixerId = mixer.id;
@ -1176,7 +1218,7 @@
if(isOpener) {
var oppositeMixer = getMixerByResourceId(mixer.rid, MIX_MODES.PERSONAL);
var mixerId = mixer.id + "," + oppositeMixer.uniqueId
var mixerId = mixer.id + "," + oppositeMixer.id
}
else {
var mixerId = mixer.id;
@ -1226,17 +1268,15 @@
}
function renderMetronomeTracks(metronomeTrackMixers) {
var metronomeActive = sessionModel.metronomeActive();
logger.debug("rendering metronome track",metronomeActive)
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 isOpener = metronomeTrackMixers[0].group_id == ChannelGroupIds.MediaTrackGroup;
var name = "Metronome"
// using the server's info in conjuction with the client's, draw the recording tracks
if(metronomeActive && metronomeTrackMixers.length > 0) {
var metronome = {active: metronomeActive}
if(metronomeTrackMixers.length > 0) {
var metronome = {}
$('.session-recording-name').text(name);//sessionModel.getCurrentSession().backing_track_path);
var noCorrespondingTracks = false;
@ -1264,33 +1304,8 @@
var instrumentIcon = context.JK.getInstrumentIcon45(oneOfTheTracks.instrument_id);
var photoUrl = "/assets/content/icon_metronome_small.png";
// var trackData = {
// trackId: oneOfTheTracks.id,
// clientId: oneOfTheTracks.client_id,
// name: "Tempo",
// instrumentIcon: photoUrl,
// avatar: instrumentIcon,
// latency: "good",
// gainPercent: 0,
// muteClass: 'hidden',
// mixerId: "",
// avatarClass : 'avatar-recording',
// preMasteredClass: "",
// hideVU: true,
// faderChanged : tempoFaderChanged,
// showMetronomeControls: true
// };
// _addRecordingTrack(trackData);
if(isOpener) {
var oppositeMixer = getMixerByResourceId(mixer.rid, MIX_MODES.PERSONAL);
var mixerId = mixer.id + "," + oppositeMixer.uniqueId
}
else {
var mixerId = mixer.id;
}
var oppositeMixer = getMixerByResourceId(mixer.rid, MIX_MODES.PERSONAL);
var mixerId = mixer.id + "," + oppositeMixer.id
// Default trackData to participant + no Mixer state.
var trackData = {
@ -1320,14 +1335,15 @@
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;
trackData.mediaTrackOpener = true
trackData.mediaControlsDisabled = false
trackData.showHelpAboutMediaMixers = false
_addRecordingTrack(trackData, mixer, oppositeMixer);
}// if
setFormFromMetronome()
metroCricket = context.jamClient.getMetronomeCricketTestState();
setMetronomePlaybackMode()
}
@ -1394,7 +1410,7 @@
if(isOpener) {
var oppositeMixer = getMixerByResourceId(mixer.rid, MIX_MODES.PERSONAL);
var mixerId = mixer.id + "," + oppositeMixer.uniqueId
var mixerId = mixer.id + "," + oppositeMixer.id
}
else {
var mixerId = mixer.id;
@ -1455,7 +1471,6 @@
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 = {}
@ -1656,14 +1671,14 @@
addNewGearDialog = new context.JK.AddNewGearDialog(app, self);
}
function connectTrackToMixer(trackSelector, track, mixerId, gainPercent, groupId) {
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)
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
}
@ -1675,9 +1690,9 @@
if (!track.hideVU) {
context.JK.VuHelpers.renderVU(vuLeftSelector, vuOpts);
$track.find('.track-vu-left').attr('mixer-id', track.vuMixerId + '_vul').data('groupId', groupId)
$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)
$track.find('.track-vu-right').attr('mixer-id', track.vuMixerId + '_vur').data('groupId', groupId).data('mixer', mixer).data('opposite-mixer', oppositeMixer)
}
if (track.showMetronomeControls) {
@ -1738,7 +1753,7 @@
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);
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)
@ -1832,7 +1847,7 @@
// 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);
connectTrackToMixer(trackSelector, trackData, trackData.mixerId, gainPercent, trackData.group_id, mixer, oppositeMixer);
var $closeButton = $('#div-track-close', 'div[track-id="' + trackData.trackId + '"]');
if (!allowDelete) {
@ -1907,9 +1922,18 @@
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 mixer = fillTrackVolumeObject(v, 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);
@ -1936,6 +1960,11 @@
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
@ -2085,7 +2114,7 @@
}
function _toggleAudioMute(mixerId, muting, mode) {
fillTrackVolumeObject(mixerId);
fillTrackVolumeObject(mixerId, mode);
context.trackVolumeObject.mute = muting;
if(mode === undefined) {
@ -2095,7 +2124,7 @@
}
function _toggleAudioLoop(mixerId, loop, mode) {
fillTrackVolumeObject(mixerId);
fillTrackVolumeObject(mixerId, mode);
context.trackVolumeObject.loop = loop;
if(mode === undefined) {
@ -2135,8 +2164,6 @@
}
}
$.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
@ -2145,15 +2172,15 @@
var mixer = $control.data('mixer');
var oppositeMixer = $control.data('opposite-mixer')
if(mixer && oppositeMixer && (mixer.group_id == ChannelGroupIds.AudioInputMusicGroup || mediaTrackGroups.indexOf(mixer.group_id) > -1)) {
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, getMixer(mixer.id).mode)
_toggleAudioMute(oppositeMixer.id, muting, getMixer(oppositeMixer.uniqueId || oppositeMixer.id).mode)
_toggleAudioMute(mixer.id, muting, mixer.mode)
_toggleAudioMute(oppositeMixer.id, muting, oppositeMixer.mode)
}
else {
logger.debug("muting mixer")
_toggleAudioMute(mixer.id, muting, getMixer(mixer.id).mode)
_toggleAudioMute(mixer.id, muting, mixer.mode)
}
// look for all controls matching this mixer id (important when it's personal mode + UserMusicInputGroup)
@ -2183,13 +2210,13 @@
}
function fillTrackVolumeObject(mixerId, broadcast) {
function fillTrackVolumeObject(mixerId, mode, broadcast) {
_updateMixers();
var _broadcast = true;
if (broadcast !== undefined) {
_broadcast = broadcast;
}
var mixer = getMixer(mixerId);
var mixer = getMixer(mixerId, mode);
context.trackVolumeObject.clientID = mixer.client_id;
context.trackVolumeObject.broadcast = _broadcast;
context.trackVolumeObject.master = mixer.master;
@ -2536,23 +2563,30 @@
var unstable = []
// This should be handled in the below loop, actually:
// var map = context.jamClient.getMyNetworkState()
// if (!map.ntp_stable) {
// unstable.push("self");
// }
var myState = context.jamClient.getMyNetworkState()
var map;
$.each(sessionModel.participants(), function(index, participant) {
map = context.jamClient.getPeerState(participant.client_id)
if (!map.ntp_stable) {
$.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 (app.clientId == participant.client_id) {
name += " (This computer)"
if (isSelf) {
name += " (this computer)"
}
unstable.push(name)
@ -2574,18 +2608,17 @@
return false;
} else {
var unstable = unstableNTPClocks()
if (unstable.length > 0) {
if (sessionModel.participants().length > 1 && unstable.length > 0) {
var names = unstable.join(", ")
logger.debug("Unstable clocks: ", names, unstable)
app.notify({
"title": "Couldn't open metronome",
"text": "The metronome feature requires that every user's computer in the session must agree on the current time. The computers of " + names + " have not successfully synchronized to the current time. The JamKazam service is trying to automatically correct this error condition. Please close this message, wait about 10 seconds, and then try opening the metronome again. If this problem persists after a couple of attempts, we recommend that the unsynchronized users restart the JamKazam application. If this error persists after a restart, please have the users with the issue contact support@jamkazam.com.",
"icon_url": "/assets/content/icon_alert_big.png"
});
context.JK.Banner.showAlert("Couldn't open metronome", context._.template($('#template-help-metronome-unstable').html(), {names: names}, { variable: 'data' }));
} else {
var bpm = 120;
logger.debug("opening the metronome with bpm: " + bpm + ", sound:" + metroSound)
rest.openMetronome({id: sessionModel.id()})
.done(function() {
context.jamClient.SessionOpenMetronome(120, "Click", 1, 0)
context.jamClient.SessionStopPlay();
context.jamClient.SessionOpenMetronome(bpm, metroSound, 1, 0);
})
.fail(function(jqXHR) {
logger.debug(jqXHR, jqXHR)
@ -2631,7 +2664,7 @@
else if(sessionModel.backingTrack() && sessionModel.backingTrack().path) {
closeBackingTrack();
}
else if(sessionModel.metronomeActive()) {
else if(getMetronomeMasterMixers().length > 0) {
closeMetronomeTrack();
}
else {
@ -2730,9 +2763,12 @@
return false;
}
function onPause() {
logger.debug("calling jamClient.SessionStopPlay");
context.jamClient.SessionStopPlay();
function onPause(e, data) {
logger.debug("calling jamClient.SessionStopPlay. endReached:", data.endReached);
if(!data.endReached) {
context.jamClient.SessionStopPlay();
}
}
function onPlay(e, data) {
@ -2743,7 +2779,7 @@
function onChangePlayPosition(e, data){
logger.debug("calling jamClient.SessionTrackSeekMs(" + data.positionMs + ")");
if(data.jamTrackMode) {
if(data.playbackMonitorMode == context.JK.PLAYBACK_MONITOR_MODE.JAMTRACK) {
context.jamClient.SessionJamTrackSeekMs(data.positionMs);
}
else {
@ -2784,6 +2820,10 @@
$("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()
@ -2795,7 +2835,7 @@
}
if (sound==null || typeof(sound)=='undefined' || sound=="") {
s = "click"
s = "Beep"
} else {
s = sound
}
@ -2803,13 +2843,23 @@
logger.debug("Setting tempo and sound:", t, s)
metroTempo = t
metroSound = s
context.jamClient.SessionSetMetronome(t, s, 1, 0)
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);
@ -2857,6 +2907,8 @@
$(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'});
}
this.initialize = function(localRecordingsDialogInstance, recordingFinishedDialogInstance, friendSelectorDialog) {
@ -2887,6 +2939,9 @@
$liveTracksContainer = $('#session-livetracks-container');
$closePlaybackRecording = $('#close-playback-recording')
$openBackingTrack = $('#open-a-backingtrack');
$metronomePlaybackSelect = $('#metronome-playback-select')
$metronomePlaybackHelp = $('#metronome-playback-help')
events();

View File

@ -131,15 +131,6 @@
}
}
function metronomeActive() {
if(currentSession) {
return currentSession.metronome_active
}
else {
return null;
}
}
function creatorId() {
if(!currentSession) {
throw "creator is not known"
@ -806,8 +797,8 @@
// the way we know if backing tracks changes, or recordings are opened, is via this event.
// but we want to report to the user when backing tracks change; so we need to detect change on our own
if(previousBackingTracks != backingTracks) {
logger.debug("backing tracks changed")
if(!(previousBackingTracks.length == 0 && backingTracks.length == 0) && previousBackingTracks != backingTracks) {
logger.debug("backing tracks changed", previousBackingTracks, backingTracks)
syncTracks(backingTracks);
}
else {
@ -828,7 +819,6 @@
this.backingTrack = backingTrack;
this.backingTracks = backingTracks;
this.recordedBackingTracks = recordedBackingTracks;
this.metronomeActive = metronomeActive;
this.setUserTracks = setUserTracks;
this.recordedTracks = recordedTracks;
this.jamTracks = jamTracks;

View File

@ -33,8 +33,6 @@
getBackingTracks: function(jamClient) {
var mediaTracks = context.JK.TrackHelpers.getTracks(jamClient, 4);
console.log("mediaTracks", mediaTracks)
var backingTracks = []
context._.each(mediaTracks, function(mediaTrack) {
// the check for 'not managed' means this is not a track opened by a recording, basically

View File

@ -46,6 +46,7 @@
*= require dialogs/dialog
*= require ./iconInstrumentSelect
*= require ./muteSelect
*= require ./metronomePlaybackModeSelect
*= require ./terms
*= require ./createSession
*= require ./feed

View File

@ -0,0 +1,66 @@
@import "client/common";
.metronome-playback-mode-selector-popup {
.bt-content {
width:160px;
background-color:#333;
overflow:auto;
border:1px solid #ED3618;
text-align:left;
font-family: Raleway, Arial, Helvetica, sans-serif;
ul {
height:100%;
margin-left:20px;
}
li {
font-size:12px;
margin-left:0;
list-style-type: none;
margin-bottom:5px;
}
p.please-select {
font-size:14px;
text-align:left;
margin-bottom:10px;
}
}
}
#metronome-playback-select {
margin-top:-10px;
span.metronome-state {
position:relative;
}
a {
color:#ffcc00 !important;
position:absolute;
top:4px;
right:-35px;
}
.down-arrow {
cursor:pointer;
width: 0;
height: 0;
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-top: 8px solid #fff;
position:absolute;
top:4px;
right:-20px;
}
.up-arrow {
cursor:pointer;
width: 0;
height: 0;
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-bottom: 8px solid #fff;
position:absolute;
top:2px;
right:-20px;
}
}

View File

@ -23,6 +23,9 @@
margin-right:8px;
position:relative;
background-color:#242323;
-webkit-border-radius:4px;
-moz-border-radius:4px;
border-radius:4px;
.disabled-track-overlay {
width:100%;
@ -159,6 +162,27 @@
.download-jamtrack {
margin-top:50px;
}
.metronome-playback-options {
height:100%;
line-height:100%;
vertical-align:middle;
span.metronome-state {
cursor:pointer;
height:100%;
line-height:100%;
vertical-align:middle;
}
}
#metronome-playback-help {
position:absolute;
right:5px;
top:5px;
color:#ffcc00;
cursor:help;
}
}

View File

@ -11,6 +11,54 @@ body.jam {
}
}
.dropdown-wrapper.black-flat {
li {
color:white;
&.focus {
background-color: #ed3618;
color:white;
}
}
ul {
background-color:#242323;
}
div.dropdown-container {
border-width:0;
border-radius:0;
}
div.dropdown {
background-color: #242323;
box-shadow: none;
border-radius:0;
border-width:0;
.selected {
color:white;
}
.carat {
border-top: 8px solid white;
}
li {
color:white;
&.focus {
background-color: #ed3618;
color:white;
}
}
div.after {
box-shadow:none;
}
}
}
.dropdown-wrappper div.dropdown-container {
width:auto;
}
@ -174,4 +222,11 @@ body.jam div.dropdown {
border-style:solid;
border-width: 1px 0 0 1px;
}
.black-flat {
ul {
border-width:0;
background-color:black;
}
}
}

View File

@ -1,5 +1,4 @@
.recording-controls {
.playback-mode-buttons {
display:none;
}
}

View File

@ -28,6 +28,19 @@
display:inline-block;
white-space:nowrap;
&.metronome-mode {
width:200px;
.recording-position, .recording-current, .playback-mode-buttons {
display:none;
}
}
&.jamtrack-mode, &.mediafile-mode {
.metronome-playback-options {
display:none;
}
}
.recording-status {
font-size:15px;
}

View File

@ -211,3 +211,19 @@ script type="text/template" id="template-help-downloaded-jamtrack"
p When a JamTrack is first purchased, a user-specific version of it is created on the server. Once it's ready, it's then downloaded to the client.
p However, in some cases, you may need to download the JamTrack again (if you change machines, for instance).
p If you do not currently have it and try to open it now, we will try to download it immediately.
script type="text/template" id="template-help-metronome-unstable"
.metronome-unstable
span.definition Background
p The metronome feature requires that every user's computer in the session must agree on the current time.
span.definition The Problem
p The computers of {{data.names}} have not successfully synchronized to the current time.
span.definition Solution
p The JamKazam service is trying to automatically correct this error condition. Please close this message, wait about 10 seconds, and then try opening the metronome again. If this problem persists after a couple of attempts, we recommend that the unsynchronized users restart the JamKazam application.
p If this error persists after a restart, please have the users with the issue contact support@jamkazam.com.
script type="text/template" id="template-help-metromone-playback-modes"
.metromone-playback-modes
p The metronome plays a local metronome back to each musician locally, with the local metronomes all synchronized to a global clock with high precision.
p The cluster test mixes playback of your local metronome with the streamed audio of all other musician metronomes. This will give you a good sense of the audible latency in your session. If all the metronome sounds are tightly clustered, there is low latency. If not, it will be more difficult to play in sync.

View File

@ -0,0 +1,8 @@
script type='text/template' id='template-metronome-playback-mode'
p.please-select Please select one:
ul
li data-playback-option="self"
a href='#' - Play metronome
li data-playback-option="cricket"
a href='#' - Play cluster test

View File

@ -31,5 +31,9 @@
<input type="radio" name="playback-mode" value="preview-to-me" class="preview-to-me" /><label for="playback-mode-preview-me" class="radio-text">Preview Only to Me</label>
</div>
<div class="metronome-playback-options">
<span id="metronome-playback-select"></span>
<span id="metronome-playback-help">?</span>
</div>
</div>
<!-- end recording play controls -->

View File

@ -127,9 +127,10 @@ script#template-session-track[type="text/template"]
.disabled-track-overlay
.metronome-selects.hidden
select.metronome-select.metro-sound title="Metronome Sound"
option.label value="Beep" Bleep
option.label value="Click" Click
option.label value="Snare" Drum
option.label value="Beep" Knock
option.label value="Click" Tap
option.label value="Snare" Snare
option.label value="Kick" Kick
br
select.metronome-select.metro-tempo title="Metronome Tempo"
- metronome_tempos.each do |t|

View File

@ -19,6 +19,7 @@
<%= render "jamServer" %>
<%= render "iconInstrumentSelect" %>
<%= render "muteSelect" %>
<%= render "metronome_playback_mode" %>
<%= render "clients/wizard/buttons" %>
<%= render "clients/wizard/gear/gear_wizard" %>
<%= render "clients/wizard/loopback/loopback_wizard" %>

View File

@ -17,7 +17,7 @@ describe ApiJamTracksController do
JamTrack.destroy_all
@user = FactoryGirl.create(:user)
@jam_track = FactoryGirl.create(:jam_track)
@jam_track_unavailable = FactoryGirl.create(:jam_track, :available=>false)
@jam_track_unavailable = FactoryGirl.create(:jam_track, :status=>'Staging')
controller.current_user = @user
end
@ -35,7 +35,7 @@ describe ApiJamTracksController do
json["jamtracks"].length.should == 2
# Create another unavailable track and see:
jam_track2 = FactoryGirl.create(:jam_track, :available=>false)
jam_track2 = FactoryGirl.create(:jam_track, :status => 'Staging')
get :index
response.should be_success
json = JSON.parse(response.body)

View File

@ -90,9 +90,12 @@
self.$container = self.$select.wrap('<div class="'+self.wrapperClass+touchClass+disabledClass+'"><span class="old"/></div>').parent().parent();
self.$containerWrapper = self.$container.wrap('<div class="dropdown-wrapper ' + self.wrapperWrapperClass + '"></div>').parent();
self.$containerWrapper = self.$container.wrap('<div class="dropdown-wrapper ' + self.wrapperWrapperClass + '"></div>').parent();
self.$active = $('<span class="selected">'+self.selected.title+'</span>').appendTo(self.$container);
// copy forward special classes on the easydropdown wrapper
self.$containerWrapper.addClass(self.$select.attr('data-style'))
self.$active = $('<span class="selected">'+self.selected.title+'</span>').appendTo(self.$container);
self.$carat = $('<span class="carat"/>').appendTo(self.$container);
//self.$scrollWrapper = $('<div><ul/></div>').appendTo(self.$container);
self.$scrollWrapper = $('<div class="dropdown-container"><ul/></div>').appendTo(self.$containerWrapper);