beats 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|