* VRFS-3725 - frontend support for click tracks and tap-in creation

This commit is contained in:
Seth Call 2015-12-03 14:21:07 -06:00
parent ebdde28f85
commit 54a437d6e1
7 changed files with 342 additions and 33 deletions

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -298,6 +298,14 @@ mixins.push(Reflux.listenTo(JamTrackPlayerStore, 'onJamTrackPlayerStoreChanged')
<td className="mute"><input type="checkbox" className="stem-mute" data-stem-id={track.id} defaultChecked={track.track_type == 'Click'}/></td>
</tr>`)
if jamTrack?.jmep?.Events? && jamTrack.jmep.Events[0].metronome?
# tap-in detected; show user tap-in option
tracks.push(`
<tr className="stem">
<td><img src={context.JK.getInstrumentIcon24('computer')} className="instrument-icon" /> Count-in</td>
<td className="mute"><input type="checkbox" className="stem-mute" data-stem-id="count-in" /></td>
</tr>`)
stems = `<div key="stems" className="stems">
<table>
<thead>
@ -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)

View File

@ -276,8 +276,7 @@ ChannelGroupIds = context.JK.ChannelGroupIds
# All the JamTracks
mediaTracks.push(`<SessionJamTrackCategory key="JamTrackCategory" jamTrackName={this.state.jamTrackName} mixers={this.state.mediaCategoryMixer} mode={MIX_MODES.PERSONAL} />`)
# 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(`<SessionMetronome key="JamTrackMetronome" {...this.state.metronome} location="jam-track" />`)

View File

@ -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'

View File

@ -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?