diff --git a/ruby/lib/jam_ruby/models/jam_track.rb b/ruby/lib/jam_ruby/models/jam_track.rb index 1121b607f..d0eea16a8 100644 --- a/ruby/lib/jam_ruby/models/jam_track.rb +++ b/ruby/lib/jam_ruby/models/jam_track.rb @@ -19,7 +19,7 @@ module JamRuby :reproduction_royalty, :public_performance_royalty, :reproduction_royalty_amount, :licensor_royalty_amount, :pro_royalty_amount, :plan_code, :initial_play_silence, :jam_track_tracks_attributes, :jam_track_tap_ins_attributes, :genre_ids, :version, :jmep_json, :jmep_text, :pro_ascap, :pro_bmi, :pro_sesac, :duration, - :server_fixation_date, :hfa_license_status, :hfa_license_desired, :alternative_license_status, :hfa_license_number, :hfa_song_code, :album_title, as: :admin + :server_fixation_date, :hfa_license_status, :hfa_license_desired, :alternative_license_status, :hfa_license_number, :hfa_song_code, :album_title, :year, as: :admin validates :name, presence: true, length: {maximum: 200} validates :plan_code, presence: true, uniqueness: true, length: {maximum: 50 } @@ -451,7 +451,7 @@ module JamRuby end def stem_tracks - JamTrackTrack.where(jam_track_id: self.id).where(track_type: 'Track') + JamTrackTrack.where(jam_track_id: self.id).where("track_type = 'Track' or track_type = 'Click'") end def can_download?(user) diff --git a/ruby/lib/jam_ruby/models/jam_track_mixdown.rb b/ruby/lib/jam_ruby/models/jam_track_mixdown.rb index 3551dcdba..c1a30d073 100644 --- a/ruby/lib/jam_ruby/models/jam_track_mixdown.rb +++ b/ruby/lib/jam_ruby/models/jam_track_mixdown.rb @@ -83,6 +83,11 @@ module JamRuby end end + if parsed["count-in"] + all_quiet = false + tweaked = true + end + if all_quiet errors.add(:settings, 'are all muted') end diff --git a/ruby/lib/jam_ruby/resque/jam_track_mixdown_packager.rb b/ruby/lib/jam_ruby/resque/jam_track_mixdown_packager.rb index 32b5d51ff..191e41d33 100644 --- a/ruby/lib/jam_ruby/resque/jam_track_mixdown_packager.rb +++ b/ruby/lib/jam_ruby/resque/jam_track_mixdown_packager.rb @@ -10,9 +10,11 @@ module JamRuby include JamRuby::S3ManagerMixin + TAP_IN_PADDING = 2 MAX_PAN = 90 MIN_PAN = -90 + KNOCK_SECONDS = 0.035 attr_accessor :mixdown_package_id, :settings, :mixdown_package, :mixdown, :step @queue = :jam_track_mixdown_packager @@ -55,6 +57,8 @@ module JamRuby @mixdown = @mixdown_package.jam_track_mixdown @settings = JSON.parse(@mixdown.settings) + process_jmep + track_settings # compute the step count @@ -102,31 +106,234 @@ module JamRuby vol != 1.0 || pan != 0 end - def create_tapin_track + def process_jmep + @start_points = [] + @initial_padding = 0.0 + + speed = @settings['speed'] || 0 + + @speed_factor = 1.0 + (-speed.to_f / 100.0) + @inverse_speed_factor = 1 - (-speed.to_f / 100) + + log.info("speed factor #{@speed_factor}") + jmep = @mixdown.jam_track.jmep_json if jmep jmep = JSON.parse(jmep) end if jmep.nil? - return nil + log.debug("no jmep") + return end events = jmep["Events"] - return nil if events.nil? || events.length == 0 + return if events.nil? || events.length == 0 metronome = nil events.each do |event| - if event.has_key?("Metronome") - metronome = event + if event.has_key?("metronome") + metronome = event["metronome"] break end end - return nil if metronome.nil? || metronome.length == 0 + if metronome.nil? || metronome.length == 0 + log.debug("no metronome events for jmep", jmep) + return + end - cmd("sox -n -r 44100 -c 2 silence.wav trim 0.0 0.05", "bpm_silence") + @start_points = metronome.select { |x| puts x.inspect; x["action"] == "start" } + + log.debug("found #{@start_points.length} metronome start points") + + start_point = @start_points[0] + + if start_point + start_time = parse_time(start_point["ts"]) + + if start_time < 2.0 + padding = start_time - 2.0 + @initial_padding = padding.abs + @initial_tap_in = start_time + end + end + + if @speed_factor != 1.0 + metronome.length.times do |count| + + # we expect to find metronome start/stop grouped + if count % 2 == 0 + + start = metronome[count] + stop = metronome[count + 1] + + if start["action"] != "start" || stop["action"] != "stop" + # bail out + log.error("found de-coupled metronome events #{start.to_json} | #{stop.to_json}") + next + end + + bpm = start["bpm"].to_f + stop_time = parse_time(stop['ts']) + ticks = stop['ticks'].to_i + + + new_bpm = bpm * @inverse_speed_factor + new_stop_time = stop_time * @speed_factor + new_start_time = new_stop_time - (60.0/new_bpm * ticks) + + log.info("original bpm:#{bpm} start: #{parse_time(start["ts"])} stop: #{stop_time}") + log.info("updated bpm:#{new_bpm} start: #{new_start_time} stop: #{new_stop_time}") + + stop["ts"] = new_stop_time + start["ts"] = new_start_time + start["bpm"] = new_bpm + stop["bpm"] = new_bpm + + @tap_in_initial_silence = (@initial_tap_in + @initial_padding) * @speed_factor + + end + + end + end + + @start_points = metronome.select { |x| puts x.inspect; x["action"] == "start" } + + end + + # format like: "-0:00:02:820" + def parse_time(ts) + + if ts.is_a?(Float) + return ts + end + + time = 0.0 + negative = false + + if ts.start_with?('-') + negative = true + end + + # parse time_format + bits = ts.split(':').reverse + + bit_position = 0 + bits.each do |bit| + if bit_position == 0 + # milliseconds + milliseconds = bit.to_f + time += milliseconds/1000 + elsif bit_position == 1 + # seconds + time += bit.to_f + elsif bit_position == 2 + # minutes + time += 60 * bit.to_f + elsif bit_position == 3 + # hours + # not bothering + end + + bit_position += 1 + end + + if negative + time = 0.0 - time + end + + time + end + + def path_to_resources + File.join(File.dirname(File.expand_path(__FILE__)), '../../../lib/jam_ruby/app/assets/sounds') + end + + def knock_file + if long_sample_rate == 44100 + knock = File.join(path_to_resources, 'knock44.wav') + else + knock = File.join(path_to_resources, 'knock48.wav') + end + + log.debug("knock file path: " + knock) + knock + end + + def create_silence(tmp_dir, segment_count, duration) + file = File.join(tmp_dir, "#{segment_count}.wav") + + # -c 2 means stereo + cmd("sox -n -r #{long_sample_rate} -c 2 #{file} trim 0.0 #{duration}", "silence") + + file + end + + def create_tapin_track(tmp_dir) + + return nil if @start_points.length == 0 + + segment_count = 0 + + + #initial_silence = @initial_tap_in + @initial_padding + + initial_silence = @tap_in_initial_silence + + #log.info("tapin data: initial_tap_in: #{@initial_tap_in}, initial_padding: #{@initial_padding}, initial_silence: #{initial_silence}") + + time_points = [] + files = [] + if initial_silence > 0 + + files << create_silence(tmp_dir, segment_count, initial_silence) + + time_points << {type: :silence, ts: initial_silence} + segment_count += 1 + end + + + time_cursor = nil + @start_points.each do |start_point| + tap_time = parse_time(start_point["ts"]) + if !time_cursor.nil? + between_silence = tap_time - time_cursor + files << create_silence(tmp_dir, segment_count, between_silence) + time_points << {type: :silence, ts: between_silence} + end + time_cursor = tap_time + bpm = start_point["bpm"].to_f + + tick_silence = 60.0/bpm - KNOCK_SECONDS + + ticks = start_point["ticks"].to_i + + ticks.times do |tick| + files << knock_file + files << create_silence(tmp_dir, segment_count, tick_silence) + time_points << {type: :knock, ts: KNOCK_SECONDS} + time_points << {type: :silence, ts: tick_silence} + time_cursor + 60.0/bpm + segment_count += 1 + end + end + + log.info("time points for tap-in: #{time_points.inspect}") + # do we need to pad with time? not sure + + sequence_cmd = "sox " + files.each do |file| + sequence_cmd << "\"#{file}\" " + end + + count_in = File.join(tmp_dir, "count-in.wav") + sequence_cmd << "\"#{count_in}\"" + + cmd(sequence_cmd, "count_in") + @count_in_file = count_in + count_in end # creates a list of tracks to actually mix @@ -140,6 +347,15 @@ module JamRuby stems = @mixdown.jam_track.stem_tracks @track_count = stems.length + @include_count_in = @settings["count-in"] && @start_points.length > 0 && @mixdown_package.encrypt_type.nil? + + # temp + # @include_count_in = true + + if @include_count_in + @track_count += 1 + end + stems.each do |stem| vol = 1.0 @@ -166,10 +382,14 @@ module JamRuby # if we didn't deliberately skip this one, and if there was no 'match' (meaning user did not specify), then we leave this in unchanged if !skipped && !match - @track_settings << {stem:stem, vol:vol, pan:pan} + @track_settings << {stem: stem, vol: vol, pan: pan} end end + if @include_count_in + @track_settings << {count_in: true, vol: 1.0, pan: 0} + end + @track_settings end @@ -180,14 +400,14 @@ module JamRuby # k = f(i) = (i)/(2*MAX_PAN) + 0.5 # so f(MIN_PAN) = -0.5 + 0.5 = 0 - k = ((pan * (1.0))/ (2.0 * MAX_PAN )) + 0.5 + k = ((pan * (1.0))/ (2.0 * MAX_PAN)) + 0.5 l, r = 0 if k == 0 l = 0.0 r = 1.0 else - l = Math.sqrt(k) + l = Math.sqrt(k) r = Math.sqrt(1-k) end @@ -196,24 +416,26 @@ module JamRuby def package - puts @settings.inspect - puts @track_count - puts @track_settings - puts @track_settings.count + log.info("Settings: #{@settings.to_json}") Dir.mktmpdir do |tmp_dir| # download all files @track_settings.each do |track| - jam_track_track = track[:stem] - file = File.join(tmp_dir, jam_track_track.id + '.ogg') + if track[:count_in] + file = create_tapin_track(tmp_dir) + bump_step(@mixdown_package) + else + jam_track_track = track[:stem] - bump_step(@mixdown_package) + file = File.join(tmp_dir, jam_track_track.id + '.ogg') - # download each track needed - s3_manager.download(jam_track_track.url_by_sample_rate(@mixdown_package.sample_rate), file) + bump_step(@mixdown_package) + # download each track needed + s3_manager.download(jam_track_track.url_by_sample_rate(@mixdown_package.sample_rate), file) + end track[:file] = file end @@ -233,6 +455,8 @@ module JamRuby apply_vol_and_pan tmp_dir + create_silence_padding tmp_dir + mix tmp_dir pitch_speed tmp_dir @@ -245,6 +469,7 @@ module JamRuby @track_settings.each do |track| jam_track_track = track[:stem] + count_in = track[:count_in] file = track[:file] unless should_alter_volume? track @@ -262,7 +487,11 @@ module JamRuby # sox claps.wav claps-remixed.wav remix 1v1.0 2v1.0 - volumed_file = File.join(tmp_dir, jam_track_track.id + '-volumed.ogg') + if count_in + volumed_file = File.join(tmp_dir, 'count-in' + '-volumed.ogg') + else + volumed_file = File.join(tmp_dir, jam_track_track.id + '-volumed.ogg') + end cmd("sox \"#{file}\" \"#{volumed_file}\" remix 1v#{channel_r} 2v#{channel_l}", 'vol_pan') @@ -271,6 +500,29 @@ module JamRuby end end + def create_silence_padding(tmp_dir) + if @initial_padding > 0 && @include_count_in + + @padding_file = File.join(tmp_dir, "initial_padding.ogg") + + # -c 2 means stereo + cmd("sox -n -r #{long_sample_rate} -c 2 #{@padding_file} trim 0.0 #{@initial_padding}", "initial_padding") + + @track_settings.each do |track| + + next if track[:count_in] + + input = track[:volumed_file] + output = input[0..-5] + '-padded.ogg' + + padd_cmd = "sox '#{@padding_file}' '#{input}' '#{output}'" + + cmd(padd_cmd, "pad_track_with_silence") + track[:volumed_file] = output + end + end + end + # output is @mix_file def mix(tmp_dir) @@ -278,6 +530,11 @@ module JamRuby @mix_file = File.join(tmp_dir, "mix.ogg") + + pitch = @settings['pitch'] || 0 + speed = @settings['speed'] || 0 + + # if there is only one track to mix, we need to skip mixing (sox will barf if you try to mix one file), but still divide by number of tracks if @track_settings.count == 1 mix_divide = 1.0/@track_count @@ -290,6 +547,11 @@ module JamRuby cmd = "sox -m" mix_divide = 1.0/@track_count @track_settings.each do |track| + + # if pitch/shifted, we lay the tap-in after pitch/speed shift + # next if (pitch != 0 || speed != 0) && track[:count_in] + next if track[:count_in] + volumed_file = track[:volumed_file] cmd << " -v #{mix_divide} \"#{volumed_file}\"" end @@ -302,6 +564,13 @@ module JamRuby end + def long_sample_rate + sample_rate = 48000 + if @mixdown_package.sample_rate != 48 + sample_rate = 44100 + end + sample_rate + end # output is @speed_mix_file def pitch_speed tmp_dir @@ -327,17 +596,21 @@ module JamRuby # usage: sbsms infile<.wav|.aif|.mp3|.ogg> outfile<.ogg> rate[0.01:100] halfsteps[-48:48] outSampleRateInHz - sample_rate = 48000 - if @mixdown_package.sample_rate != 48 - sample_rate = 44100 - end + sample_rate = long_sample_rate # rate comes in as a percent (like 5, -5 for 5%, -5%). We need to change that to 1.05/ sbsms_speed = speed/100.0 sbsms_speed = 1.0 + sbsms_speed sbsms_pitch = pitch - cmd( "sbsms \"#{@mix_file}\" \"#{@speed_mix_file}\" #{sbsms_speed} #{sbsms_pitch} #{sample_rate}", 'speed_pitch_shift') + cmd("sbsms \"#{@mix_file}\" \"#{@speed_mix_file}\" #{sbsms_speed} #{sbsms_pitch} #{sample_rate}", 'speed_pitch_shift') + end + + if @include_count_in + # lay the tap-ins over the recording + layered = File.join(tmp_dir, "layered_speed_mix.ogg") + cmd("sox -m '#{@count_in_file}' '#{@speed_mix_file}' '#{layered}'", "layer_tap_in") + @speed_mix_file = layered end end @@ -364,7 +637,7 @@ module JamRuby length = File.size(output) computed_md5 = Digest::MD5.new - File.open(output, 'rb').each {|line| computed_md5.update(line)} + File.open(output, 'rb').each { |line| computed_md5.update(line) } md5 = computed_md5.to_s @mixdown_package.finish_sign(s3_url, private_key, length, md5.to_s) @@ -426,7 +699,7 @@ module JamRuby end private_key_file = File.join(tmp_dir, 'skey.pem') - File.open(private_key_file, 'w') {|f| f.write(private_key) } + File.open(private_key_file, 'w') { |f| f.write(private_key) } log.debug("PRIVATE KEY") log.debug(private_key) diff --git a/web/app/assets/javascripts/react-components/PopupJamTrackPlayer.js.jsx.coffee b/web/app/assets/javascripts/react-components/PopupJamTrackPlayer.js.jsx.coffee index bd4c34779..9f3fd2975 100644 --- a/web/app/assets/javascripts/react-components/PopupJamTrackPlayer.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/PopupJamTrackPlayer.js.jsx.coffee @@ -298,6 +298,14 @@ mixins.push(Reflux.listenTo(JamTrackPlayerStore, 'onJamTrackPlayerStoreChanged') `) + if jamTrack?.jmep?.Events? && jamTrack.jmep.Events[0].metronome? + # tap-in detected; show user tap-in option + tracks.push(` + + Count-in + + `) + stems = `
@@ -647,6 +655,7 @@ mixins.push(Reflux.listenTo(JamTrackPlayerStore, 'onJamTrackPlayerStoreChanged') else pitch = parseInt(pitch) + count_in = false # get mute state of all tracks $mutes = $(@getDOMNode()).find('.stems .stem .stem-mute') @@ -657,10 +666,13 @@ mixins.push(Reflux.listenTo(JamTrackPlayerStore, 'onJamTrackPlayerStoreChanged') stemId = $mute.attr('data-stem-id') muted = $mute.is(':checked') - tracks.push({id: stemId, mute: muted}) + if stemId == 'count-in' + count_in = !muted + else + tracks.push({id: stemId, mute: muted}) ) - mixdown = {jamTrackID: @state.jamTrackState.jamTrack.id, name: name, settings: {speed:speed, pitch: pitch, tracks:tracks}} + mixdown = {jamTrackID: @state.jamTrackState.jamTrack.id, name: name, settings: {speed:speed, pitch: pitch, "count-in": count_in, tracks:tracks}} JamTrackPlayerActions.createMixdown(mixdown, @createMixdownDone, @createMixdownFail) diff --git a/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee index ca7012871..c214aab20 100644 --- a/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee @@ -276,8 +276,7 @@ ChannelGroupIds = context.JK.ChannelGroupIds # All the JamTracks mediaTracks.push(``) - # show metronome only if it's a full jamtrack - if @state.metronome? && @state.jamTrackMixdown.id == null + if @state.metronome? # && @state.jamTrackMixdown.id == null @state.metronome.mode = MIX_MODES.PERSONAL mediaTracks.push(``) diff --git a/web/app/assets/javascripts/react-components/stores/JamTrackStore.js.coffee b/web/app/assets/javascripts/react-components/stores/JamTrackStore.js.coffee index 17c088534..46a095bb8 100644 --- a/web/app/assets/javascripts/react-components/stores/JamTrackStore.js.coffee +++ b/web/app/assets/javascripts/react-components/stores/JamTrackStore.js.coffee @@ -165,6 +165,20 @@ JamTrackActions = @JamTrackActions # JamTrackPlay means 'load' logger.debug("JamTrackStore: loading mixdown") context.jamClient.JamTrackStopPlay(); + + if @jamTrack.jmep + + if @jamTrack.activeMixdown.settings.speed? + @jamTrack.jmep.speed = @jamTrack.activeMixdown.settings.speed + else + @jamTrack.jmep.speed = 0 + + logger.debug("setting jmep data. speed:" + @jamTrack.jmep.speed) + + context.jamClient.JamTrackLoadJmep(fqId, @jamTrack.jmep) + else + logger.debug("no jmep data for jamtrack") + result = context.jamClient.JamTrackPlay(fqId); if !result @jamTrack.activeMixdown.client_state = 'cant_open' diff --git a/web/lib/tasks/jam_tracks.rake b/web/lib/tasks/jam_tracks.rake index dcc9f623a..c0add4f92 100644 --- a/web/lib/tasks/jam_tracks.rake +++ b/web/lib/tasks/jam_tracks.rake @@ -196,6 +196,12 @@ namespace :jam_tracks do mapper.correlate end + task touch: :environment do |task, arg| + JamTrack.all.each do |jt| + jt.touch # causes jmep re-spin + end + end + task generate_private_key: :environment do |task, arg| JamTrackRight.all.each do |right| if right.private_key_44.nil? || right.private_key_48.nil?