beats 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +29 -4
- data/bin/beats +36 -71
- data/lib/beats.rb +57 -0
- data/lib/beatswavefile.rb +93 -0
- data/lib/kit.rb +67 -11
- data/lib/pattern.rb +131 -86
- data/lib/song.rb +145 -114
- data/lib/songoptimizer.rb +154 -0
- data/lib/songparser.rb +40 -28
- data/lib/songsplitter.rb +38 -0
- data/lib/track.rb +33 -31
- data/lib/wavefile.rb +475 -0
- data/test/examples/combined.wav +0 -0
- data/test/examples/split-agogo_high.wav +0 -0
- data/test/examples/split-bass.wav +0 -0
- data/test/examples/split-hh_closed.wav +0 -0
- data/test/examples/split-snare.wav +0 -0
- data/test/examples/split-tom2.wav +0 -0
- data/test/examples/split-tom4.wav +0 -0
- data/test/fixtures/expected_output/example_combined_mono_16.wav +0 -0
- data/test/fixtures/expected_output/example_combined_mono_8.wav +0 -0
- data/test/fixtures/expected_output/example_combined_stereo_16.wav +0 -0
- data/test/fixtures/expected_output/example_combined_stereo_8.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_16-agogo.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_16-bass.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_16-hh_closed.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_16-snare.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_16-tom2_mono_16.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_16-tom4_mono_16.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_8-agogo.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_8-bass.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_8-hh_closed.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_8-snare.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_8-tom2_mono_8.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_8-tom4_mono_8.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_16-agogo.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_16-bass.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_16-hh_closed.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_16-snare.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_16-tom2_stereo_16.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_16-tom4_stereo_16.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_8-agogo.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_8-bass.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_8-hh_closed.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_8-snare.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_8-tom2_stereo_8.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_8-tom4_stereo_8.wav +0 -0
- data/test/fixtures/invalid/bad_repeat_count.txt +8 -0
- data/test/fixtures/invalid/bad_rhythm.txt +9 -0
- data/test/fixtures/invalid/bad_structure.txt +9 -0
- data/test/fixtures/invalid/bad_tempo.txt +8 -0
- data/test/fixtures/invalid/no_header.txt +3 -0
- data/test/fixtures/invalid/no_structure.txt +6 -0
- data/test/fixtures/invalid/pattern_with_no_tracks.txt +12 -0
- data/test/fixtures/invalid/sound_in_kit_not_found.txt +10 -0
- data/test/fixtures/invalid/sound_in_track_not_found.txt +8 -0
- data/test/fixtures/invalid/template.txt +31 -0
- data/test/fixtures/valid/example_mono_16.txt +28 -0
- data/test/fixtures/valid/example_mono_8.txt +28 -0
- data/test/fixtures/valid/example_no_kit.txt +30 -0
- data/test/fixtures/valid/example_stereo_16.txt +28 -0
- data/test/fixtures/valid/example_stereo_8.txt +28 -0
- data/test/fixtures/valid/example_with_empty_track.txt +10 -0
- data/test/fixtures/valid/example_with_kit.txt +34 -0
- data/test/fixtures/valid/no_tempo.txt +8 -0
- data/test/fixtures/valid/pattern_with_overflow.txt +9 -0
- data/test/fixtures/valid/repeats_not_specified.txt +10 -0
- data/test/fixtures/yaml/song_yaml.txt +30 -0
- data/test/includes.rb +11 -4
- data/test/integration.rb +100 -0
- data/test/kit_test.rb +39 -39
- data/test/pattern_test.rb +119 -71
- data/test/song_test.rb +87 -62
- data/test/songoptimizer_test.rb +162 -0
- data/test/songparser_test.rb +36 -165
- data/test/sounds/agogo_high_mono_16.wav +0 -0
- data/test/sounds/agogo_high_mono_8.wav +0 -0
- data/test/sounds/agogo_high_stereo_16.wav +0 -0
- data/test/sounds/agogo_high_stereo_8.wav +0 -0
- data/test/sounds/agogo_low_mono_16.wav +0 -0
- data/test/sounds/agogo_low_mono_8.wav +0 -0
- data/test/sounds/agogo_low_stereo_16.wav +0 -0
- data/test/sounds/agogo_low_stereo_8.wav +0 -0
- data/test/sounds/bass2_mono_16.wav +0 -0
- data/test/sounds/bass2_mono_8.wav +0 -0
- data/test/sounds/bass2_stereo_16.wav +0 -0
- data/test/sounds/bass2_stereo_8.wav +0 -0
- data/test/sounds/bass_mono_8.wav +0 -0
- data/test/sounds/bass_stereo_16.wav +0 -0
- data/test/sounds/bass_stereo_8.wav +0 -0
- data/test/sounds/clave_high_mono_16.wav +0 -0
- data/test/sounds/clave_high_mono_8.wav +0 -0
- data/test/sounds/clave_high_stereo_16.wav +0 -0
- data/test/sounds/clave_high_stereo_8.wav +0 -0
- data/test/sounds/clave_low_mono_16.wav +0 -0
- data/test/sounds/clave_low_mono_8.wav +0 -0
- data/test/sounds/clave_low_stereo_16.wav +0 -0
- data/test/sounds/clave_low_stereo_8.wav +0 -0
- data/test/sounds/conga_high_mono_16.wav +0 -0
- data/test/sounds/conga_high_mono_8.wav +0 -0
- data/test/sounds/conga_high_stereo_16.wav +0 -0
- data/test/sounds/conga_high_stereo_8.wav +0 -0
- data/test/sounds/conga_low_mono_16.wav +0 -0
- data/test/sounds/conga_low_mono_8.wav +0 -0
- data/test/sounds/conga_low_stereo_16.wav +0 -0
- data/test/sounds/conga_low_stereo_8.wav +0 -0
- data/test/sounds/cowbell_high_mono_16.wav +0 -0
- data/test/sounds/cowbell_high_mono_8.wav +0 -0
- data/test/sounds/cowbell_high_stereo_16.wav +0 -0
- data/test/sounds/cowbell_high_stereo_8.wav +0 -0
- data/test/sounds/cowbell_low_mono_16.wav +0 -0
- data/test/sounds/cowbell_low_mono_8.wav +0 -0
- data/test/sounds/cowbell_low_stereo_16.wav +0 -0
- data/test/sounds/cowbell_low_stereo_8.wav +0 -0
- data/test/sounds/hh_closed_mono_16.wav +0 -0
- data/test/sounds/hh_closed_mono_8.wav +0 -0
- data/test/sounds/hh_closed_stereo_16.wav +0 -0
- data/test/sounds/hh_closed_stereo_8.wav +0 -0
- data/test/sounds/hh_open_mono_16.wav +0 -0
- data/test/sounds/hh_open_mono_8.wav +0 -0
- data/test/sounds/hh_open_stereo_16.wav +0 -0
- data/test/sounds/hh_open_stereo_8.wav +0 -0
- data/test/sounds/ride_mono_16.wav +0 -0
- data/test/sounds/ride_mono_8.wav +0 -0
- data/test/sounds/ride_stereo_16.wav +0 -0
- data/test/sounds/ride_stereo_8.wav +0 -0
- data/test/sounds/rim_mono_16.wav +0 -0
- data/test/sounds/rim_mono_8.wav +0 -0
- data/test/sounds/rim_stereo_16.wav +0 -0
- data/test/sounds/rim_stereo_8.wav +0 -0
- data/test/sounds/sine-mono-8bit.wav +0 -0
- data/test/sounds/snare2_mono_16.wav +0 -0
- data/test/sounds/snare2_mono_8.wav +0 -0
- data/test/sounds/snare2_stereo_16.wav +0 -0
- data/test/sounds/snare2_stereo_8.wav +0 -0
- data/test/sounds/snare_mono_16.wav +0 -0
- data/test/sounds/snare_mono_8.wav +0 -0
- data/test/sounds/snare_stereo_16.wav +0 -0
- data/test/sounds/snare_stereo_8.wav +0 -0
- data/test/sounds/tom1_mono_16.wav +0 -0
- data/test/sounds/tom1_mono_8.wav +0 -0
- data/test/sounds/tom1_stereo_16.wav +0 -0
- data/test/sounds/tom1_stereo_8.wav +0 -0
- data/test/sounds/tom2_mono_16.wav +0 -0
- data/test/sounds/tom2_mono_8.wav +0 -0
- data/test/sounds/tom2_stereo_16.wav +0 -0
- data/test/sounds/tom2_stereo_8.wav +0 -0
- data/test/sounds/tom3_mono_16.wav +0 -0
- data/test/sounds/tom3_mono_8.wav +0 -0
- data/test/sounds/tom3_stereo_16.wav +0 -0
- data/test/sounds/tom3_stereo_8.wav +0 -0
- data/test/sounds/tom4_mono_16.wav +0 -0
- data/test/sounds/tom4_mono_8.wav +0 -0
- data/test/sounds/tom4_stereo_16.wav +0 -0
- data/test/sounds/tom4_stereo_8.wav +0 -0
- data/test/sounds/tone.wav +0 -0
- data/test/track_test.rb +78 -72
- metadata +277 -15
data/lib/song.rb
CHANGED
@@ -13,19 +13,26 @@ class Song
|
|
13
13
|
@structure = []
|
14
14
|
end
|
15
15
|
|
16
|
+
# Adds a new pattern to the song, with the specified name.
|
16
17
|
def pattern(name)
|
17
18
|
@patterns[name] = Pattern.new(name)
|
18
19
|
return @patterns[name]
|
19
20
|
end
|
20
21
|
|
21
|
-
|
22
|
-
|
22
|
+
# Returns the number of samples required for the entire song at the current tempo.
|
23
|
+
# (Assumes a sample rate of 44100). Does NOT include samples required for sound
|
24
|
+
# overflow from the last pattern.
|
25
|
+
def sample_length
|
26
|
+
@structure.inject(0) do |sum, pattern_name|
|
23
27
|
sum + @patterns[pattern_name].sample_length(@tick_sample_length)
|
24
|
-
|
28
|
+
end
|
25
29
|
end
|
26
30
|
|
27
|
-
|
28
|
-
|
31
|
+
# Returns the number of samples required for the entire song at the current tempo.
|
32
|
+
# (Assumes a sample rate of 44100). Includes samples required for sound overflow
|
33
|
+
# from the last pattern.
|
34
|
+
def sample_length_with_overflow
|
35
|
+
if @structure.length == 0
|
29
36
|
return 0
|
30
37
|
end
|
31
38
|
|
@@ -37,158 +44,182 @@ class Song
|
|
37
44
|
return sample_length + overflow
|
38
45
|
end
|
39
46
|
|
40
|
-
|
47
|
+
# The number of tracks that the pattern with the greatest number of tracks has.
|
48
|
+
# TODO: Is it a problem that an optimized song can have a different total_tracks() value than
|
49
|
+
# the original? Or is that actually a good thing?
|
50
|
+
# TODO: Investigate replacing this with a method max_sounds_playing_at_once() or something
|
51
|
+
# like that. Would look each pattern along with it's incoming overflow.
|
52
|
+
def total_tracks
|
41
53
|
@patterns.keys.collect {|pattern_name| @patterns[pattern_name].tracks.length }.max || 0
|
42
54
|
end
|
55
|
+
|
56
|
+
def track_names
|
57
|
+
track_names = {}
|
58
|
+
@patterns.values.each do |pattern|
|
59
|
+
pattern.tracks.values.each {|track| track_names[track.name] = nil}
|
60
|
+
end
|
61
|
+
|
62
|
+
return track_names.keys.sort
|
63
|
+
end
|
43
64
|
|
44
|
-
def
|
65
|
+
def write_to_file(output_file_name)
|
66
|
+
cache = {}
|
67
|
+
pack_code = pack_code()
|
45
68
|
num_tracks_in_song = self.total_tracks()
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
return sample_data_split_single_pattern(fill_value, num_tracks_in_song, pattern, primary_sample_length)
|
69
|
+
sample_length = sample_length_with_overflow()
|
70
|
+
|
71
|
+
wave_file = BeatsWaveFile.new(@kit.num_channels, SAMPLE_RATE, @kit.bits_per_sample)
|
72
|
+
file = wave_file.open_for_appending(output_file_name, sample_length)
|
73
|
+
|
74
|
+
incoming_overflow = {}
|
75
|
+
@structure.each do |pattern_name|
|
76
|
+
key = [pattern_name, incoming_overflow.hash]
|
77
|
+
unless cache.member?(key)
|
78
|
+
sample_data = @patterns[pattern_name].sample_data(@tick_sample_length,
|
79
|
+
@kit.num_channels,
|
80
|
+
num_tracks_in_song,
|
81
|
+
incoming_overflow)
|
82
|
+
|
83
|
+
if @kit.num_channels == 1
|
84
|
+
# Don't flatten the sample data Array, since it is already flattened. That would be a waste of time, yo.
|
85
|
+
cache[key] = {:primary => sample_data[:primary].pack(pack_code), :overflow => sample_data[:overflow]}
|
64
86
|
else
|
65
|
-
|
87
|
+
cache[key] = {:primary => sample_data[:primary].flatten.pack(pack_code), :overflow => sample_data[:overflow]}
|
66
88
|
end
|
67
89
|
end
|
90
|
+
|
91
|
+
file.syswrite(cache[key][:primary])
|
92
|
+
incoming_overflow = cache[key][:overflow]
|
68
93
|
end
|
94
|
+
|
95
|
+
wave_file.write_snippet(file, merge_overflow(incoming_overflow, num_tracks_in_song))
|
96
|
+
file.close()
|
97
|
+
|
98
|
+
return wave_file.calculate_duration(SAMPLE_RATE, sample_length)
|
69
99
|
end
|
70
100
|
|
71
|
-
def num_channels
|
101
|
+
def num_channels
|
72
102
|
return @kit.num_channels
|
73
103
|
end
|
74
104
|
|
75
|
-
def bits_per_sample
|
105
|
+
def bits_per_sample
|
76
106
|
return @kit.bits_per_sample
|
77
107
|
end
|
78
108
|
|
79
|
-
def tempo
|
109
|
+
def tempo
|
80
110
|
return @tempo
|
81
111
|
end
|
82
112
|
|
83
113
|
def tempo=(new_tempo)
|
84
|
-
|
114
|
+
unless new_tempo.class == Fixnum && new_tempo > 0
|
85
115
|
raise InvalidTempoError, "Invalid tempo: '#{new_tempo}'. Tempo must be a number greater than 0."
|
86
116
|
end
|
87
117
|
|
88
118
|
@tempo = new_tempo
|
89
119
|
@tick_sample_length = SAMPLES_PER_MINUTE / new_tempo / 4.0
|
90
120
|
end
|
121
|
+
|
122
|
+
# Returns a new Song that is identical but with no patterns or structure.
|
123
|
+
def copy_ignoring_patterns_and_structure
|
124
|
+
copy = Song.new(@kit.base_path)
|
125
|
+
copy.tempo = @tempo
|
126
|
+
copy.kit = @kit
|
127
|
+
|
128
|
+
return copy
|
129
|
+
end
|
130
|
+
|
131
|
+
# Removes any patterns that aren't referenced in the structure.
|
132
|
+
def remove_unused_patterns
|
133
|
+
# Using reject() here, because for some reason select() returns an Array not a Hash.
|
134
|
+
@patterns = @patterns.reject {|k, pattern| !@structure.member?(pattern.name) }
|
135
|
+
end
|
136
|
+
|
137
|
+
# Serializes the current Song to a YAML string. This string can then be used to construct a new Song
|
138
|
+
# using the SongParser class. This lets you save a Song to disk, to be re-loaded later. Produces nicer
|
139
|
+
# looking output than the default version of to_yaml().
|
140
|
+
def to_yaml
|
141
|
+
# This implementation intentionally builds up a YAML string manually instead of using YAML::dump().
|
142
|
+
# Ruby 1.8 makes it difficult to ensure a consistent ordering of hash keys, which makes the output ugly
|
143
|
+
# and also hard to test.
|
144
|
+
|
145
|
+
yaml_output = "Song:\n"
|
146
|
+
yaml_output += " Tempo: #{@tempo}\n"
|
147
|
+
yaml_output += structure_to_yaml()
|
148
|
+
yaml_output += @kit.to_yaml(2)
|
149
|
+
yaml_output += patterns_to_yaml()
|
150
|
+
|
151
|
+
return yaml_output
|
152
|
+
end
|
91
153
|
|
92
154
|
attr_reader :tick_sample_length, :patterns
|
93
155
|
attr_accessor :structure, :kit
|
94
156
|
|
95
157
|
private
|
96
158
|
|
159
|
+
def pack_code
|
160
|
+
if @kit.bits_per_sample == 8
|
161
|
+
return "C*"
|
162
|
+
elsif @kit.bits_per_sample == 16
|
163
|
+
return "s*"
|
164
|
+
else
|
165
|
+
raise StandardError, "Invalid bits per sample of #{@kit.bits_per_sample}"
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def longest_length_in_array(arr)
|
170
|
+
return arr.inject(0) {|max_length, name| (name.to_s.length > max_length) ? name.to_s.length : max_length }
|
171
|
+
end
|
172
|
+
|
173
|
+
def structure_to_yaml
|
174
|
+
yaml_output = " Structure:\n"
|
175
|
+
ljust_amount = longest_length_in_array(@structure) + 1 # The +1 is for the trailing ":"
|
176
|
+
previous = nil
|
177
|
+
count = 0
|
178
|
+
@structure.each do |pattern_name|
|
179
|
+
if pattern_name == previous || previous == nil
|
180
|
+
count += 1
|
181
|
+
else
|
182
|
+
yaml_output += " - #{(previous.to_s.capitalize + ':').ljust(ljust_amount)} x#{count}\n"
|
183
|
+
count = 1
|
184
|
+
end
|
185
|
+
previous = pattern_name
|
186
|
+
end
|
187
|
+
yaml_output += " - #{(previous.to_s.capitalize + ':').ljust(ljust_amount)} x#{count}\n"
|
188
|
+
|
189
|
+
return yaml_output
|
190
|
+
end
|
191
|
+
|
192
|
+
def patterns_to_yaml
|
193
|
+
yaml_output = ""
|
194
|
+
|
195
|
+
# Sort to ensure a consistent order, to make testing easier
|
196
|
+
pattern_names = @patterns.keys.map {|key| key.to_s} # Ruby 1.8 can't sort symbols...
|
197
|
+
pattern_names.sort.each do |pattern_name|
|
198
|
+
yaml_output += "\n" + @patterns[pattern_name.to_sym].to_yaml()
|
199
|
+
end
|
200
|
+
|
201
|
+
return yaml_output
|
202
|
+
end
|
203
|
+
|
97
204
|
def merge_overflow(overflow, num_tracks_in_song)
|
98
205
|
merged_sample_data = []
|
99
206
|
|
100
|
-
|
207
|
+
unless overflow == {}
|
101
208
|
longest_overflow = overflow[overflow.keys.first]
|
102
|
-
overflow.keys.each
|
103
|
-
if
|
209
|
+
overflow.keys.each do |track_name|
|
210
|
+
if overflow[track_name].length > longest_overflow.length
|
104
211
|
longest_overflow = overflow[track_name]
|
105
212
|
end
|
106
|
-
|
213
|
+
end
|
107
214
|
|
215
|
+
# TODO: What happens if final overflow is really long, and extends past single '.' rhythm?
|
108
216
|
final_overflow_pattern = Pattern.new(:overflow)
|
109
|
-
|
110
|
-
|
217
|
+
wave_data = @kit.num_channels == 1 ? [] : [[]]
|
218
|
+
final_overflow_pattern.track "", wave_data, "."
|
219
|
+
final_overflow_sample_data = final_overflow_pattern.sample_data(longest_overflow.length, @kit.num_channels, num_tracks_in_song, overflow)
|
111
220
|
merged_sample_data = final_overflow_sample_data[:primary]
|
112
221
|
end
|
113
222
|
|
114
223
|
return merged_sample_data
|
115
224
|
end
|
116
|
-
|
117
|
-
def sample_data_split_all_patterns(fill_value, num_tracks_in_song)
|
118
|
-
output_data = {}
|
119
|
-
|
120
|
-
offset = 0
|
121
|
-
overflow = {}
|
122
|
-
@structure.each {|pattern_name|
|
123
|
-
pattern_sample_length = @patterns[pattern_name].sample_length(@tick_sample_length)
|
124
|
-
pattern_sample_data = @patterns[pattern_name].sample_data(@tick_sample_length, @kit.num_channels, num_tracks_in_song, overflow, true)
|
125
|
-
|
126
|
-
pattern_sample_data[:primary].keys.each {|track_name|
|
127
|
-
if(output_data[track_name] == nil)
|
128
|
-
output_data[track_name] = [].fill(fill_value, 0, self.sample_length_with_overflow())
|
129
|
-
end
|
130
|
-
|
131
|
-
output_data[track_name][offset...(offset + pattern_sample_length)] = pattern_sample_data[:primary][track_name]
|
132
|
-
}
|
133
|
-
|
134
|
-
overflow.keys.each {|track_name|
|
135
|
-
if(pattern_sample_data[:primary][track_name] == nil)
|
136
|
-
output_data[track_name][offset...overflow[track_name].length] = overflow[track_name]
|
137
|
-
end
|
138
|
-
}
|
139
|
-
|
140
|
-
overflow = pattern_sample_data[:overflow]
|
141
|
-
offset += pattern_sample_length
|
142
|
-
}
|
143
|
-
|
144
|
-
overflow.keys.each {|track_name|
|
145
|
-
output_data[track_name][offset...overflow[track_name].length] = overflow[track_name]
|
146
|
-
}
|
147
|
-
|
148
|
-
return output_data
|
149
|
-
end
|
150
|
-
|
151
|
-
def sample_data_split_single_pattern(fill_value, num_tracks_in_song, pattern, primary_sample_length)
|
152
|
-
output_data = {}
|
153
|
-
|
154
|
-
pattern_sample_length = pattern.sample_length(@tick_sample_length)
|
155
|
-
pattern_sample_data = pattern.sample_data(@tick_sample_length, @kit.num_channels, num_tracks_in_song, {}, true)
|
156
|
-
|
157
|
-
pattern_sample_data[:primary].keys.each {|track_name|
|
158
|
-
overflow_sample_length = pattern_sample_data[:overflow][track_name].length
|
159
|
-
full_sample_length = pattern_sample_length + overflow_sample_length
|
160
|
-
output_data[track_name] = [].fill(fill_value, 0, full_sample_length)
|
161
|
-
output_data[track_name][0...pattern_sample_length] = pattern_sample_data[:primary][track_name]
|
162
|
-
output_data[track_name][pattern_sample_length...full_sample_length] = pattern_sample_data[:overflow][track_name]
|
163
|
-
}
|
164
|
-
|
165
|
-
return output_data
|
166
|
-
end
|
167
|
-
|
168
|
-
def sample_data_combined_all_patterns(fill_value, num_tracks_in_song)
|
169
|
-
output_data = [].fill(fill_value, 0, self.sample_length_with_overflow)
|
170
|
-
|
171
|
-
offset = 0
|
172
|
-
overflow = {}
|
173
|
-
@structure.each {|pattern_name|
|
174
|
-
pattern_sample_length = @patterns[pattern_name].sample_length(@tick_sample_length)
|
175
|
-
pattern_sample_data = @patterns[pattern_name].sample_data(@tick_sample_length, @kit.num_channels, num_tracks_in_song, overflow)
|
176
|
-
output_data[offset...offset + pattern_sample_length] = pattern_sample_data[:primary]
|
177
|
-
overflow = pattern_sample_data[:overflow]
|
178
|
-
offset += pattern_sample_length
|
179
|
-
}
|
180
|
-
|
181
|
-
# Handle overflow from final pattern
|
182
|
-
output_data[offset...output_data.length] = merge_overflow(overflow, num_tracks_in_song)
|
183
|
-
return output_data
|
184
|
-
end
|
185
|
-
|
186
|
-
def sample_data_combined_single_pattern(fill_value, num_tracks_in_song, pattern, primary_sample_length)
|
187
|
-
output_data = [].fill(fill_value, 0, pattern.sample_length_with_overflow(@tick_sample_length))
|
188
|
-
sample_data = pattern.sample_data(tick_sample_length, @kit.num_channels, num_tracks_in_song, {}, false)
|
189
|
-
output_data[0...primary_sample_length] = sample_data[:primary]
|
190
|
-
output_data[primary_sample_length...output_data.length] = merge_overflow(sample_data[:overflow], num_tracks_in_song)
|
191
|
-
|
192
|
-
return output_data
|
193
|
-
end
|
194
225
|
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
# This class is used to transform a Song object into an equivalent Song object that
|
2
|
+
# will be generated faster by the sound engine.
|
3
|
+
#
|
4
|
+
# The primary method is optimize(). Currently, it performs two optimizations:
|
5
|
+
#
|
6
|
+
# 1.) Breaks patterns into shorter patterns. Generating one long Pattern is generally
|
7
|
+
# slower than generating several short Patterns with the same combined length.
|
8
|
+
# 2.) Replaces Patterns which are equivalent (i.e. they have the same tracks with the
|
9
|
+
# same rhythms) into one canonical Pattern. This allows for better caching, by
|
10
|
+
# preventing the sound engine from generating sample data for a Pattern when the
|
11
|
+
# same sample data has already been generated for a different Pattern.
|
12
|
+
#
|
13
|
+
# Note that step #1 actually performs double duty, because breaking Patterns into smaller
|
14
|
+
# pieces increases the likelihood there will be duplicates that can be combined.
|
15
|
+
class SongOptimizer
|
16
|
+
def initialize()
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns a Song that will produce the same output as original_song, but should be
|
20
|
+
# generated faster.
|
21
|
+
def optimize(original_song, max_pattern_length)
|
22
|
+
# 1.) Create a new song, cloned from the original
|
23
|
+
optimized_song = original_song.copy_ignoring_patterns_and_structure()
|
24
|
+
|
25
|
+
# 2.) Subdivide patterns
|
26
|
+
optimized_song = subdivide_song_patterns(original_song, optimized_song, max_pattern_length)
|
27
|
+
|
28
|
+
# 3.) Prune duplicate patterns
|
29
|
+
optimized_song = prune_duplicate_patterns(optimized_song)
|
30
|
+
|
31
|
+
return optimized_song
|
32
|
+
end
|
33
|
+
|
34
|
+
protected
|
35
|
+
|
36
|
+
# Splits the patterns of a Song into smaller patterns, each one with at most
|
37
|
+
# max_pattern_length steps. For example, if max_pattern_length is 4, then
|
38
|
+
# the following pattern:
|
39
|
+
#
|
40
|
+
# track1: X...X...X.
|
41
|
+
# track2: ..X.....X.
|
42
|
+
# track3: X.X.X.X.X.
|
43
|
+
#
|
44
|
+
# will be converted into the following 3 patterns:
|
45
|
+
#
|
46
|
+
# track1: X...
|
47
|
+
# track2: ..X.
|
48
|
+
# track3: X.X.
|
49
|
+
#
|
50
|
+
# track1: X...
|
51
|
+
# track3: X.X.
|
52
|
+
#
|
53
|
+
# track1: X.
|
54
|
+
# track2: X.
|
55
|
+
# track3: X.
|
56
|
+
#
|
57
|
+
# Note that if a track in a sub-divided pattern has no triggers (such as track2 in the
|
58
|
+
# 2nd pattern above), it will not be included in the new pattern.
|
59
|
+
def subdivide_song_patterns(original_song, optimized_song, max_pattern_length)
|
60
|
+
blank_track_pattern = '.' * max_pattern_length
|
61
|
+
|
62
|
+
# 2.) For each pattern, add a new pattern to new song every max_pattern_length ticks
|
63
|
+
optimized_structure = {}
|
64
|
+
original_song.patterns.values.each do |pattern|
|
65
|
+
tick_index = 0
|
66
|
+
optimized_structure[pattern.name] = []
|
67
|
+
|
68
|
+
while(pattern.tracks.values.first.rhythm[tick_index] != nil) do
|
69
|
+
new_pattern = optimized_song.pattern("#{pattern.name}#{tick_index}".to_sym)
|
70
|
+
optimized_structure[pattern.name] << new_pattern.name
|
71
|
+
pattern.tracks.values.each do |track|
|
72
|
+
sub_track_pattern = track.rhythm[tick_index...(tick_index + max_pattern_length)]
|
73
|
+
|
74
|
+
if sub_track_pattern != blank_track_pattern
|
75
|
+
new_pattern.track(track.name, track.wave_data, sub_track_pattern)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# If no track has a trigger during this step pattern, add a blank track.
|
80
|
+
# Otherwise, this pattern will have no ticks, and no sound will be generated,
|
81
|
+
# causing the pattern to be "compacted away".
|
82
|
+
if new_pattern.tracks.empty?
|
83
|
+
# Track.sample_data() examines its sound's sample data to determine if it is
|
84
|
+
# mono or stereo. If the first item in the sample data Array is an Array,
|
85
|
+
# it decides stereo. That's what the [] vs. [[]] is about.
|
86
|
+
placeholder_wave_data = (optimized_song.kit.num_channels == 1) ? [] : [[]]
|
87
|
+
|
88
|
+
new_pattern.track("placeholder", placeholder_wave_data, blank_track_pattern)
|
89
|
+
end
|
90
|
+
|
91
|
+
tick_index += max_pattern_length
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# 3.) Replace the Song's structure to reference the new sub-divided patterns
|
96
|
+
# instead of the old patterns.
|
97
|
+
optimized_structure = original_song.structure.map do |original_pattern|
|
98
|
+
optimized_structure[original_pattern]
|
99
|
+
end
|
100
|
+
optimized_song.structure = optimized_structure.flatten
|
101
|
+
|
102
|
+
return optimized_song
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
# Replaces any Patterns that are duplicates (i.e., each track uses the same sound and has
|
107
|
+
# the same rhythm) with a single canonical pattern.
|
108
|
+
#
|
109
|
+
# The benefit of this is that it allows more effective caching. For example, suppose Pattern A
|
110
|
+
# and Pattern B are equivalent. If Pattern A gets generated first, it will be cached. When
|
111
|
+
# Pattern B gets generated, it will be generated from scratch instead of using Pattern A's
|
112
|
+
# cached data. Consolidating duplicates into one prevents this from happening.
|
113
|
+
#
|
114
|
+
# Duplicate Patterns are more likely to occur after calling subdivide_song_patterns().
|
115
|
+
def prune_duplicate_patterns(song)
|
116
|
+
seen_patterns = []
|
117
|
+
replacements = {}
|
118
|
+
|
119
|
+
# Pattern names are sorted to ensure consistent pattern replacement. Makes tests easier to write.
|
120
|
+
# Sort function added manually because Ruby 1.8 doesn't know how to sort symbols...
|
121
|
+
pattern_names = song.patterns.keys.sort {|x, y| x.to_s <=> y.to_s }
|
122
|
+
|
123
|
+
# Detect duplicates
|
124
|
+
pattern_names.each do |pattern_name|
|
125
|
+
pattern = song.patterns[pattern_name]
|
126
|
+
found_duplicate = false
|
127
|
+
seen_patterns.each do |seen_pattern|
|
128
|
+
if !found_duplicate && pattern.same_tracks_as?(seen_pattern)
|
129
|
+
replacements[pattern.name.to_sym] = seen_pattern.name.to_sym
|
130
|
+
found_duplicate = true
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
if !found_duplicate
|
135
|
+
seen_patterns << pattern
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Update structure to remove references to duplicates
|
140
|
+
new_structure = song.structure
|
141
|
+
replacements.each do |duplicate, replacement|
|
142
|
+
new_structure = new_structure.map do |pattern_name|
|
143
|
+
(pattern_name == duplicate) ? replacement : pattern_name
|
144
|
+
end
|
145
|
+
end
|
146
|
+
song.structure = new_structure
|
147
|
+
|
148
|
+
# Remove unused Patterns. Not strictly necessary, but makes resulting songs
|
149
|
+
# easier to read for debugging purposes.
|
150
|
+
song.remove_unused_patterns()
|
151
|
+
|
152
|
+
return song
|
153
|
+
end
|
154
|
+
end
|