rsgt 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +6 -0
  3. data/.rspec +2 -0
  4. data/Gemfile +6 -0
  5. data/README.markdown +139 -0
  6. data/Rakefile +10 -0
  7. data/bin/rsgt +5 -0
  8. data/lib/rsgt.rb +42 -0
  9. data/lib/rsgt/audio_extractor.rb +25 -0
  10. data/lib/rsgt/cli.rb +89 -0
  11. data/lib/rsgt/command_runner.rb +41 -0
  12. data/lib/rsgt/encrypted_steam_file.rb +44 -0
  13. data/lib/rsgt/multipacker.rb +114 -0
  14. data/lib/rsgt/multipacker/config_processor.rb +44 -0
  15. data/lib/rsgt/repacker.rb +37 -0
  16. data/lib/rsgt/rs_custom_song_toolkit.rb +41 -0
  17. data/lib/rsgt/saved_game.rb +37 -0
  18. data/lib/rsgt/saved_game/learn_a_song/song.rb +46 -0
  19. data/lib/rsgt/saved_game/score_attack/song.rb +38 -0
  20. data/lib/rsgt/saved_game/song_collection.rb +16 -0
  21. data/lib/rsgt/saved_game/statistics.rb +91 -0
  22. data/lib/rsgt/saved_game/statistics/song.rb +58 -0
  23. data/lib/rsgt/steam.rb +45 -0
  24. data/lib/rsgt/unpacked_psarc.rb +72 -0
  25. data/lib/rsgt/version.rb +3 -0
  26. data/lib/rsgt/vocals_extractor.rb +24 -0
  27. data/lib/rsgt/wav_shifter.rb +54 -0
  28. data/lib/rsgt/wwise_converter.rb +99 -0
  29. data/rsgt.gemspec +31 -0
  30. data/spec/lib/rsgt/audio_extractor_spec.rb +14 -0
  31. data/spec/lib/rsgt/command_runner_spec.rb +37 -0
  32. data/spec/lib/rsgt/repacker_spec.rb +24 -0
  33. data/spec/lib/rsgt/rs_custom_song_toolkit_spec.rb +47 -0
  34. data/spec/lib/rsgt/saved_game/learn_a_song/song_spec.rb +15 -0
  35. data/spec/lib/rsgt/saved_game_spec.rb +49 -0
  36. data/spec/lib/rsgt/unpacked_psarc_spec.rb +35 -0
  37. data/spec/lib/rsgt/vocals_extractor_spec.rb +14 -0
  38. data/spec/lib/rsgt/wav_shifter_spec.rb +30 -0
  39. data/spec/lib/rsgt/wwise_converter_spec.rb +22 -0
  40. data/spec/spec_helper.rb +11 -0
  41. metadata +168 -0
@@ -0,0 +1,72 @@
1
+ module RSGuitarTech
2
+ class UnpackedPSARC
3
+
4
+ def self.from_psarc(psarc, opts)
5
+ raise ArgumentError, "A block is required" unless block_given?
6
+ raise ArgumentError, "#{psarc} not found!" unless File.exist?(psarc)
7
+
8
+ puts "Unpacking psarc to temporary location..."
9
+ # Use pyrocksmith to unpack the file, and check that it worked..
10
+ CommandRunner.run! ["pyrocksmith", "--unpack", psarc, "--no-crypto"]
11
+ expected_extraction = File.basename psarc, ".*"
12
+ raise ArgumentError unless File.exist?(expected_extraction + "/appid.appid")
13
+
14
+ # Move it to a tmpdir, and yield
15
+ Dir.mktmpdir do |dir|
16
+ FileUtils.mv expected_extraction, dir
17
+ yield self.new(psarc, dir, opts)
18
+ end
19
+ end
20
+
21
+ attr_accessor :psarc, :tmpdir, :opts
22
+
23
+ def initialize(psarc, tmpdir, opts)
24
+ @psarc = psarc
25
+ @tmpdir = tmpdir
26
+ @opts = opts
27
+ end
28
+
29
+ def repack!
30
+ puts "Repacking altered psarc..."
31
+ CommandRunner.run! ["pyrocksmith", "--no-crypto", "--pack", "#{tmpdir}/#{expected_extraction}"]
32
+ puts "Done! ✅"
33
+ end
34
+
35
+ def expected_extraction
36
+ File.basename psarc, ".*"
37
+ end
38
+
39
+ def manifest(type)
40
+ Dir.glob("#{tmpdir}/#{expected_extraction}/manifests/*/*.json").detect { |filename| filename.include? type.to_s }
41
+ end
42
+
43
+ def sng_bin(type)
44
+ Dir.glob("#{tmpdir}/#{expected_extraction}/songs/bin/macos/*.sng").detect { |filename| filename.include? type.to_s }
45
+ end
46
+
47
+ def sng_xml
48
+ Dir.glob("#{tmpdir}/#{expected_extraction}/songs/bin/macos/*.xml").first
49
+ end
50
+
51
+ def audio_track(track: :main)
52
+ audio_tracks[track]
53
+ end
54
+
55
+ def ogg_track
56
+ audio_tracks[:main].gsub ".wem", ".ogg"
57
+ end
58
+
59
+ private
60
+
61
+ def audio_tracks
62
+ @audio_tracks ||= begin
63
+ both_wems = Dir.glob("#{tmpdir}/#{expected_extraction}/audio/mac/*.wem").sort_by { |wem| File.stat(wem).size }
64
+
65
+ {
66
+ main: both_wems.last,
67
+ preview: both_wems.first
68
+ }
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,3 @@
1
+ module RSGuitarTech
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,24 @@
1
+ module RSGuitarTech
2
+ class VocalsExtractor
3
+
4
+ attr_accessor :psarc, :opts, :unpacked
5
+
6
+ def initialize(opts)
7
+ @psarc = File.expand_path(opts.delete :psarc)
8
+ @opts = opts
9
+ end
10
+
11
+ def extract!
12
+ puts ".. Extracting vocals from sng to xml..."
13
+ UnpackedPSARC.from_psarc(psarc, opts) do |unpacked|
14
+ CommandRunner.run! RSCustomSongToolkit.sng2xml(
15
+ manifest: unpacked.manifest(:vocals),
16
+ input: unpacked.sng_bin(:vocals)
17
+ )
18
+ raise StandardError, "Failed to extract vocals" unless File.exist? unpacked.sng_xml
19
+
20
+ FileUtils.cp unpacked.sng_xml, "."
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,54 @@
1
+ module RSGuitarTech
2
+ class WavShifter
3
+
4
+ attr_accessor :wav, :dir, :amount
5
+
6
+ def initialize(opts)
7
+ @wav = File.expand_path(opts.delete :wav)
8
+ @dir = opts[:dir]
9
+ @amount = opts[:amount].to_i
10
+ end
11
+
12
+ def shift!
13
+ case dir
14
+ when "forward" then forward!
15
+ when /back/ then backward!
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def forward!
22
+ raise NotImplementedError
23
+ end
24
+
25
+ def backward!
26
+ Dir.mktmpdir do |tmpdir|
27
+ puts "Trimming #{amount}ms from beginning..."
28
+ status = CommandRunner.run! [
29
+ "ffmpeg", "-i", wav,
30
+ "-ss", start_trim_timestamp,
31
+ "-acodec", "copy",
32
+ "#{tmpdir}/shifted.wav"
33
+ ]
34
+ if status.success?
35
+ puts "Success!"
36
+ FileUtils.cp "#{tmpdir}/shifted.wav", wav
37
+ else
38
+ puts "Failed!"
39
+ end
40
+ end
41
+ end
42
+
43
+ TS_CONV = [1, 1000, 60000, 3600000]
44
+
45
+ def start_trim_timestamp
46
+ h = (amount / TS_CONV[3])
47
+ m = (amount % TS_CONV[3] / TS_CONV[2])
48
+ s = (amount % TS_CONV[2] / TS_CONV[1])
49
+ ms = (amount % TS_CONV[1])
50
+
51
+ "%02d:%02d:%02d.%03d" % [h, m, s, ms]
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,99 @@
1
+ module RSGuitarTech
2
+ class WWiseConverter
3
+
4
+ TEMPLATE_TAR_BZ = "/Applications/RocksmithCustomSongToolkit.app/Contents/Resources/WwiseYEAR.tar.bz2"
5
+
6
+ attr_accessor :source_file, :wwise_version, :quality, :chorus, :dir
7
+
8
+ def initialize(source_file, opts = {})
9
+ @source_file = source_file
10
+ @wwise_version = opts[:wwise_ver]
11
+ @quality = opts[:quality]
12
+ @chorus = opts[:chorus]
13
+ end
14
+
15
+ def convert!
16
+ Dir.mktmpdir do |dir|
17
+ @dir = dir
18
+
19
+ extract_base_template
20
+ prepare_template
21
+ copy_source
22
+ ffmpeg_preview
23
+ run_wwise
24
+ if wwise_version[0..3].to_i >= 2016
25
+ downgrade_headers "#{template_dir}/GeneratedSoundBanks/Windows/144151451.wem"
26
+ downgrade_headers "#{template_dir}/GeneratedSoundBanks/Windows/309608343.wem"
27
+ end
28
+ yield self
29
+ end
30
+ end
31
+
32
+ def extract_base_template
33
+ just_tar = template_tar_bz.gsub(".bz2", "")
34
+ CommandRunner.run! ["bunzip2", "-k", template_tar_bz] # Unzip Wwise2016.tar.bz2 to Wwise2016.tar
35
+ CommandRunner.run! ["tar", "-xzvf", just_tar, "-C", dir] # Untar Wwise2016.tar to the workdir
36
+ end
37
+
38
+ def prepare_template
39
+ CommandRunner.run! ["mkdir", "-p", "#{template_dir}/.cache/Windows/SFX"]
40
+ CommandRunner.run! ["sed", "-i.bak", "'s/%QF1%/#{quality}/'", default_work_unit.shellescape], skip_escaping: true
41
+ CommandRunner.run! ["sed", "-i.bak", "'s/%QF2%/#{quality}/'", default_work_unit.shellescape], skip_escaping: true
42
+ end
43
+
44
+ def copy_source
45
+ FileUtils.cp source_file, "#{template_dir}/Originals/SFX/Audio.wav"
46
+ end
47
+
48
+ def ffmpeg_preview
49
+ CommandRunner.run! ["ffmpeg", "-i", source_file, "-ss", chorus, "-t", "30", "#{template_dir}/Originals/SFX/Audio_preview.wav"]
50
+ end
51
+
52
+ def run_wwise
53
+ wwise_cmd = [
54
+ wwise_cli,
55
+ "#{template_dir}/Template.wproj",
56
+ "-GenerateSoundBanks",
57
+ "-Platform",
58
+ "Windows",
59
+ "-NoWwiseDat",
60
+ "-ClearAudioFileCache",
61
+ "-Save"
62
+ ]
63
+ wwise_cmd << "-Verbose" if RSGuitarTech.verbose
64
+ CommandRunner.run! wwise_cmd
65
+ end
66
+
67
+ def downgrade_headers(file)
68
+ CommandRunner.run! %Q{printf "$(printf '\\x%02X' 03)" | dd of=#{file} bs=1 seek=40 count=1 conv=notrunc}, skip_escaping: true
69
+ CommandRunner.run! %Q{printf "$(printf '\\x%02X' 00)" | dd of=#{file} bs=1 seek=41 count=1 conv=notrunc}, skip_escaping: true
70
+ CommandRunner.run! %Q{printf "$(printf '\\x%02X' 00)" | dd of=#{file} bs=1 seek=42 count=1 conv=notrunc}, skip_escaping: true
71
+ CommandRunner.run! %Q{printf "$(printf '\\x%02X' 00)" | dd of=#{file} bs=1 seek=43 count=1 conv=notrunc}, skip_escaping: true
72
+ end
73
+
74
+ def results
75
+ {
76
+ main: "#{template_dir}/GeneratedSoundBanks/Windows/144151451.wem",
77
+ preview: "#{template_dir}/GeneratedSoundBanks/Windows/309608343.wem"
78
+ }
79
+ end
80
+
81
+ private
82
+
83
+ def template_dir
84
+ "#{dir}/Template"
85
+ end
86
+
87
+ def template_tar_bz
88
+ TEMPLATE_TAR_BZ.dup.gsub "YEAR", wwise_version[0..3]
89
+ end
90
+
91
+ def wwise_cli
92
+ "/Applications/Audiokinetic/Wwise #{wwise_version}/Wwise.app/Contents/Tools/WwiseCLI.sh"
93
+ end
94
+
95
+ def default_work_unit
96
+ "#{template_dir}/Interactive Music Hierarchy/Default Work Unit.wwu"
97
+ end
98
+ end
99
+ end
data/rsgt.gemspec ADDED
@@ -0,0 +1,31 @@
1
+ lib = File.expand_path("../lib", __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "rsgt/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.platform = Gem::Platform::RUBY
7
+ s.name = "rsgt"
8
+ s.version = RSGuitarTech::VERSION
9
+ s.authors = ["Justin Aiken"]
10
+ s.email = ["60tonangel@gmail.com"]
11
+ s.license = "MIT"
12
+ s.homepage = "https://github.com/JustinAiken/rsgt"
13
+ s.summary = %q{Rocksmith}
14
+ s.description = %q{Rocksmith}
15
+
16
+ s.rubyforge_project = "rsgt"
17
+
18
+ s.files = `git ls-files`.split("\n")
19
+ s.test_files = `git ls-files -- {spec}/*`.split("\n")
20
+ s.require_paths = ["lib"]
21
+ s.bindir = "bin"
22
+ s.executables = s.files.grep(%r{^bin/}).map { |f| File.basename(f) }
23
+
24
+ s.add_dependency "bindata"
25
+ s.add_dependency "trollop", "~> 2.1"
26
+ s.add_dependency "rainbow", "~> 2.2"
27
+ s.add_dependency "activesupport", ">= 3.0"
28
+
29
+ s.add_development_dependency "rspec", "~> 3.5"
30
+ s.add_development_dependency "pry"
31
+ end
@@ -0,0 +1,14 @@
1
+ require "spec_helper"
2
+
3
+ describe RSGuitarTech::AudioExtractor do
4
+ let(:extractor) { described_class.new psarc: psarc }
5
+ let(:psarc) { "foo_m.psarc" }
6
+
7
+ describe "#extract!" do
8
+ it "extracts" do
9
+ allow(extractor).to receive :puts
10
+ expect(RSGuitarTech::UnpackedPSARC).to receive :from_psarc
11
+ extractor.extract!
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,37 @@
1
+ require "spec_helper"
2
+
3
+ describe RSGuitarTech::CommandRunner do
4
+ let(:runner) { described_class }
5
+ let(:args) { "ls" }
6
+ let(:skip_escaping) { false }
7
+ subject(:run!) { runner.run! args, skip_escaping: skip_escaping }
8
+
9
+ it { should be_success }
10
+
11
+ context "with spaces" do
12
+ let(:args) { "ls -al" }
13
+
14
+ after { run! }
15
+
16
+ it "escapes the command" do
17
+ expect(Open3).to receive(:capture3).with "ls\\ -al"
18
+ end
19
+
20
+ context "but skip_escaping: true" do
21
+ let(:skip_escaping) { true }
22
+
23
+ it "runs the command as-is" do
24
+ expect(Open3).to receive(:capture3).with "ls -al"
25
+ end
26
+ end
27
+ end
28
+
29
+ context "verbosely" do
30
+ before { allow(RSGuitarTech).to receive(:verbose).and_return true }
31
+
32
+ it "does some puts" do
33
+ expect_any_instance_of(described_class).to receive(:puts).at_least :once
34
+ run!
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,24 @@
1
+ require "spec_helper"
2
+
3
+ describe RSGuitarTech::Repacker do
4
+ let(:repacker) { described_class.new psarc: "foo_m.psarc", vocals_xml: vocals_xml, audio: audio }
5
+ let(:vocals_xml) { "foo_vocals.xml" }
6
+ let(:audio) { "audio.wav" }
7
+
8
+ let(:unpacked) { double RSGuitarTech::UnpackedPSARC, manifest: "manifest.json", audio_track: "track.wem" }
9
+ let(:converter) { double RSGuitarTech::WWiseConverter, results: {main: "main.wav"} }
10
+
11
+ describe "#repack!" do
12
+ before { allow(repacker).to receive :puts }
13
+
14
+ it "repacks" do
15
+ expect(RSGuitarTech::UnpackedPSARC).to receive(:from_psarc).and_yield unpacked
16
+ expect_any_instance_of(RSGuitarTech::WWiseConverter).to receive(:convert!).and_yield converter
17
+ expect(FileUtils).to receive(:cp).with "main.wav", "track.wem"
18
+ expect(RSGuitarTech::CommandRunner).to receive :run!
19
+ expect(unpacked).to receive :repack!
20
+
21
+ repacker.repack!
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,47 @@
1
+ require "spec_helper"
2
+
3
+ describe RSGuitarTech::RSCustomSongToolkit do
4
+ let(:rs_custom) { described_class }
5
+
6
+ it ".ww2ogg" do
7
+ expect(rs_custom.ww2ogg "foo.wem").to eq [
8
+ "wine",
9
+ "/Applications/RocksmithCustomSongToolkit.app/Contents/Resources/ww2ogg.exe",
10
+ "foo.wem",
11
+ "--pcb",
12
+ described_class::PCB_PATH
13
+ ]
14
+ end
15
+
16
+ it ".revorb" do
17
+ expect(rs_custom.revorb "foo.ogg").to eq [
18
+ "wine",
19
+ "/Applications/RocksmithCustomSongToolkit.app/Contents/Resources/revorb.exe",
20
+ "foo.ogg"
21
+ ]
22
+ end
23
+
24
+ it ".sng2xml" do
25
+ expect(rs_custom.sng2xml manifest: "manifest.json", input: "foo.sng").to eq [
26
+ "wine",
27
+ "/Applications/RocksmithCustomSongToolkit.app/Contents/Resources/sng2014.exe",
28
+ "--sng2xml",
29
+ "--manifest=manifest.json",
30
+ "--input=foo.sng",
31
+ "--arrangement=Vocal",
32
+ "--platform=Mac"
33
+ ]
34
+ end
35
+
36
+ it ".xml2sng" do
37
+ expect(rs_custom.xml2sng manifest: "manifest.json", input: "foo.xml").to eq [
38
+ "wine",
39
+ "/Applications/RocksmithCustomSongToolkit.app/Contents/Resources/sng2014.exe",
40
+ "--xml2sng",
41
+ "--manifest=manifest.json",
42
+ "--input=foo.xml",
43
+ "--arrangement=Vocal",
44
+ "--platform=Mac"
45
+ ]
46
+ end
47
+ end
@@ -0,0 +1,15 @@
1
+ require "spec_helper"
2
+
3
+ describe RSGuitarTech::SavedGame::LearnASong::Song do
4
+ pending ".from"
5
+
6
+ describe "#==" do
7
+ subject { described_class.new id: "123", timestamp: nil, dynamic_difficulty: nil, play_next_stats: nil }
8
+ let(:song_b) { described_class.new id: "123", timestamp: nil, dynamic_difficulty: nil, play_next_stats: nil }
9
+ let(:song_c) { described_class.new id: "456", timestamp: nil, dynamic_difficulty: nil, play_next_stats: nil }
10
+
11
+ it { should eq song_b }
12
+ it { should_not eq song_c }
13
+ it { should_not eq nil }
14
+ end
15
+ end
@@ -0,0 +1,49 @@
1
+ require "spec_helper"
2
+
3
+ describe RSGuitarTech::SavedGame do
4
+ let(:saved_game) { described_class.new json, :filename }
5
+
6
+ pending ".from"
7
+
8
+ describe "#options" do
9
+ subject { saved_game.options }
10
+ let(:json) { {"Options" => {"foo" => "bar"}} }
11
+ it { should eq "foo" => "bar" }
12
+ end
13
+
14
+ describe "#statistics" do
15
+ subject { saved_game.statistics }
16
+ let(:json) { {"Stats" => {"foo" => "bar"}} }
17
+ it { should be_a RSGuitarTech::SavedGame::Statistics }
18
+ end
19
+
20
+ describe "#song_statistics" do
21
+ subject(:song_statistics) { saved_game.song_statistics }
22
+ let(:json) { {"Stats" => {"Songs" => {"abc" => "songdata"}}} }
23
+ it { should be_a RSGuitarTech::SavedGame::SongCollection }
24
+
25
+ it "is a keyed collection of Statistics::Song's" do
26
+ expect(song_statistics["abc"]).to be_a RSGuitarTech::SavedGame::Statistics::Song
27
+ end
28
+ end
29
+
30
+ describe "#score_attack" do
31
+ subject(:score_attack) { saved_game.score_attack }
32
+ let(:json) { {"SongsSA" => {"abc" => "songdata"}} }
33
+ it { should be_a RSGuitarTech::SavedGame::SongCollection }
34
+
35
+ it "is a keyed collection of Statistics::Song's" do
36
+ expect(score_attack["abc"]).to be_a RSGuitarTech::SavedGame::ScoreAttack::Song
37
+ end
38
+ end
39
+
40
+ describe "#learn_a_song" do
41
+ subject(:learn_a_song) { saved_game.learn_a_song }
42
+ let(:json) { {"Songs" => {"abc" => "songdata"}} }
43
+ it { should be_a RSGuitarTech::SavedGame::SongCollection }
44
+
45
+ it "is a keyed collection of Statistics::Song's" do
46
+ expect(learn_a_song["abc"]).to be_a RSGuitarTech::SavedGame::LearnASong::Song
47
+ end
48
+ end
49
+ end