beats 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (158) hide show
  1. data/README.markdown +29 -4
  2. data/bin/beats +36 -71
  3. data/lib/beats.rb +57 -0
  4. data/lib/beatswavefile.rb +93 -0
  5. data/lib/kit.rb +67 -11
  6. data/lib/pattern.rb +131 -86
  7. data/lib/song.rb +145 -114
  8. data/lib/songoptimizer.rb +154 -0
  9. data/lib/songparser.rb +40 -28
  10. data/lib/songsplitter.rb +38 -0
  11. data/lib/track.rb +33 -31
  12. data/lib/wavefile.rb +475 -0
  13. data/test/examples/combined.wav +0 -0
  14. data/test/examples/split-agogo_high.wav +0 -0
  15. data/test/examples/split-bass.wav +0 -0
  16. data/test/examples/split-hh_closed.wav +0 -0
  17. data/test/examples/split-snare.wav +0 -0
  18. data/test/examples/split-tom2.wav +0 -0
  19. data/test/examples/split-tom4.wav +0 -0
  20. data/test/fixtures/expected_output/example_combined_mono_16.wav +0 -0
  21. data/test/fixtures/expected_output/example_combined_mono_8.wav +0 -0
  22. data/test/fixtures/expected_output/example_combined_stereo_16.wav +0 -0
  23. data/test/fixtures/expected_output/example_combined_stereo_8.wav +0 -0
  24. data/test/fixtures/expected_output/example_split_mono_16-agogo.wav +0 -0
  25. data/test/fixtures/expected_output/example_split_mono_16-bass.wav +0 -0
  26. data/test/fixtures/expected_output/example_split_mono_16-hh_closed.wav +0 -0
  27. data/test/fixtures/expected_output/example_split_mono_16-snare.wav +0 -0
  28. data/test/fixtures/expected_output/example_split_mono_16-tom2_mono_16.wav +0 -0
  29. data/test/fixtures/expected_output/example_split_mono_16-tom4_mono_16.wav +0 -0
  30. data/test/fixtures/expected_output/example_split_mono_8-agogo.wav +0 -0
  31. data/test/fixtures/expected_output/example_split_mono_8-bass.wav +0 -0
  32. data/test/fixtures/expected_output/example_split_mono_8-hh_closed.wav +0 -0
  33. data/test/fixtures/expected_output/example_split_mono_8-snare.wav +0 -0
  34. data/test/fixtures/expected_output/example_split_mono_8-tom2_mono_8.wav +0 -0
  35. data/test/fixtures/expected_output/example_split_mono_8-tom4_mono_8.wav +0 -0
  36. data/test/fixtures/expected_output/example_split_stereo_16-agogo.wav +0 -0
  37. data/test/fixtures/expected_output/example_split_stereo_16-bass.wav +0 -0
  38. data/test/fixtures/expected_output/example_split_stereo_16-hh_closed.wav +0 -0
  39. data/test/fixtures/expected_output/example_split_stereo_16-snare.wav +0 -0
  40. data/test/fixtures/expected_output/example_split_stereo_16-tom2_stereo_16.wav +0 -0
  41. data/test/fixtures/expected_output/example_split_stereo_16-tom4_stereo_16.wav +0 -0
  42. data/test/fixtures/expected_output/example_split_stereo_8-agogo.wav +0 -0
  43. data/test/fixtures/expected_output/example_split_stereo_8-bass.wav +0 -0
  44. data/test/fixtures/expected_output/example_split_stereo_8-hh_closed.wav +0 -0
  45. data/test/fixtures/expected_output/example_split_stereo_8-snare.wav +0 -0
  46. data/test/fixtures/expected_output/example_split_stereo_8-tom2_stereo_8.wav +0 -0
  47. data/test/fixtures/expected_output/example_split_stereo_8-tom4_stereo_8.wav +0 -0
  48. data/test/fixtures/invalid/bad_repeat_count.txt +8 -0
  49. data/test/fixtures/invalid/bad_rhythm.txt +9 -0
  50. data/test/fixtures/invalid/bad_structure.txt +9 -0
  51. data/test/fixtures/invalid/bad_tempo.txt +8 -0
  52. data/test/fixtures/invalid/no_header.txt +3 -0
  53. data/test/fixtures/invalid/no_structure.txt +6 -0
  54. data/test/fixtures/invalid/pattern_with_no_tracks.txt +12 -0
  55. data/test/fixtures/invalid/sound_in_kit_not_found.txt +10 -0
  56. data/test/fixtures/invalid/sound_in_track_not_found.txt +8 -0
  57. data/test/fixtures/invalid/template.txt +31 -0
  58. data/test/fixtures/valid/example_mono_16.txt +28 -0
  59. data/test/fixtures/valid/example_mono_8.txt +28 -0
  60. data/test/fixtures/valid/example_no_kit.txt +30 -0
  61. data/test/fixtures/valid/example_stereo_16.txt +28 -0
  62. data/test/fixtures/valid/example_stereo_8.txt +28 -0
  63. data/test/fixtures/valid/example_with_empty_track.txt +10 -0
  64. data/test/fixtures/valid/example_with_kit.txt +34 -0
  65. data/test/fixtures/valid/no_tempo.txt +8 -0
  66. data/test/fixtures/valid/pattern_with_overflow.txt +9 -0
  67. data/test/fixtures/valid/repeats_not_specified.txt +10 -0
  68. data/test/fixtures/yaml/song_yaml.txt +30 -0
  69. data/test/includes.rb +11 -4
  70. data/test/integration.rb +100 -0
  71. data/test/kit_test.rb +39 -39
  72. data/test/pattern_test.rb +119 -71
  73. data/test/song_test.rb +87 -62
  74. data/test/songoptimizer_test.rb +162 -0
  75. data/test/songparser_test.rb +36 -165
  76. data/test/sounds/agogo_high_mono_16.wav +0 -0
  77. data/test/sounds/agogo_high_mono_8.wav +0 -0
  78. data/test/sounds/agogo_high_stereo_16.wav +0 -0
  79. data/test/sounds/agogo_high_stereo_8.wav +0 -0
  80. data/test/sounds/agogo_low_mono_16.wav +0 -0
  81. data/test/sounds/agogo_low_mono_8.wav +0 -0
  82. data/test/sounds/agogo_low_stereo_16.wav +0 -0
  83. data/test/sounds/agogo_low_stereo_8.wav +0 -0
  84. data/test/sounds/bass2_mono_16.wav +0 -0
  85. data/test/sounds/bass2_mono_8.wav +0 -0
  86. data/test/sounds/bass2_stereo_16.wav +0 -0
  87. data/test/sounds/bass2_stereo_8.wav +0 -0
  88. data/test/sounds/bass_mono_8.wav +0 -0
  89. data/test/sounds/bass_stereo_16.wav +0 -0
  90. data/test/sounds/bass_stereo_8.wav +0 -0
  91. data/test/sounds/clave_high_mono_16.wav +0 -0
  92. data/test/sounds/clave_high_mono_8.wav +0 -0
  93. data/test/sounds/clave_high_stereo_16.wav +0 -0
  94. data/test/sounds/clave_high_stereo_8.wav +0 -0
  95. data/test/sounds/clave_low_mono_16.wav +0 -0
  96. data/test/sounds/clave_low_mono_8.wav +0 -0
  97. data/test/sounds/clave_low_stereo_16.wav +0 -0
  98. data/test/sounds/clave_low_stereo_8.wav +0 -0
  99. data/test/sounds/conga_high_mono_16.wav +0 -0
  100. data/test/sounds/conga_high_mono_8.wav +0 -0
  101. data/test/sounds/conga_high_stereo_16.wav +0 -0
  102. data/test/sounds/conga_high_stereo_8.wav +0 -0
  103. data/test/sounds/conga_low_mono_16.wav +0 -0
  104. data/test/sounds/conga_low_mono_8.wav +0 -0
  105. data/test/sounds/conga_low_stereo_16.wav +0 -0
  106. data/test/sounds/conga_low_stereo_8.wav +0 -0
  107. data/test/sounds/cowbell_high_mono_16.wav +0 -0
  108. data/test/sounds/cowbell_high_mono_8.wav +0 -0
  109. data/test/sounds/cowbell_high_stereo_16.wav +0 -0
  110. data/test/sounds/cowbell_high_stereo_8.wav +0 -0
  111. data/test/sounds/cowbell_low_mono_16.wav +0 -0
  112. data/test/sounds/cowbell_low_mono_8.wav +0 -0
  113. data/test/sounds/cowbell_low_stereo_16.wav +0 -0
  114. data/test/sounds/cowbell_low_stereo_8.wav +0 -0
  115. data/test/sounds/hh_closed_mono_16.wav +0 -0
  116. data/test/sounds/hh_closed_mono_8.wav +0 -0
  117. data/test/sounds/hh_closed_stereo_16.wav +0 -0
  118. data/test/sounds/hh_closed_stereo_8.wav +0 -0
  119. data/test/sounds/hh_open_mono_16.wav +0 -0
  120. data/test/sounds/hh_open_mono_8.wav +0 -0
  121. data/test/sounds/hh_open_stereo_16.wav +0 -0
  122. data/test/sounds/hh_open_stereo_8.wav +0 -0
  123. data/test/sounds/ride_mono_16.wav +0 -0
  124. data/test/sounds/ride_mono_8.wav +0 -0
  125. data/test/sounds/ride_stereo_16.wav +0 -0
  126. data/test/sounds/ride_stereo_8.wav +0 -0
  127. data/test/sounds/rim_mono_16.wav +0 -0
  128. data/test/sounds/rim_mono_8.wav +0 -0
  129. data/test/sounds/rim_stereo_16.wav +0 -0
  130. data/test/sounds/rim_stereo_8.wav +0 -0
  131. data/test/sounds/sine-mono-8bit.wav +0 -0
  132. data/test/sounds/snare2_mono_16.wav +0 -0
  133. data/test/sounds/snare2_mono_8.wav +0 -0
  134. data/test/sounds/snare2_stereo_16.wav +0 -0
  135. data/test/sounds/snare2_stereo_8.wav +0 -0
  136. data/test/sounds/snare_mono_16.wav +0 -0
  137. data/test/sounds/snare_mono_8.wav +0 -0
  138. data/test/sounds/snare_stereo_16.wav +0 -0
  139. data/test/sounds/snare_stereo_8.wav +0 -0
  140. data/test/sounds/tom1_mono_16.wav +0 -0
  141. data/test/sounds/tom1_mono_8.wav +0 -0
  142. data/test/sounds/tom1_stereo_16.wav +0 -0
  143. data/test/sounds/tom1_stereo_8.wav +0 -0
  144. data/test/sounds/tom2_mono_16.wav +0 -0
  145. data/test/sounds/tom2_mono_8.wav +0 -0
  146. data/test/sounds/tom2_stereo_16.wav +0 -0
  147. data/test/sounds/tom2_stereo_8.wav +0 -0
  148. data/test/sounds/tom3_mono_16.wav +0 -0
  149. data/test/sounds/tom3_mono_8.wav +0 -0
  150. data/test/sounds/tom3_stereo_16.wav +0 -0
  151. data/test/sounds/tom3_stereo_8.wav +0 -0
  152. data/test/sounds/tom4_mono_16.wav +0 -0
  153. data/test/sounds/tom4_mono_8.wav +0 -0
  154. data/test/sounds/tom4_stereo_16.wav +0 -0
  155. data/test/sounds/tom4_stereo_8.wav +0 -0
  156. data/test/sounds/tone.wav +0 -0
  157. data/test/track_test.rb +78 -72
  158. metadata +277 -15
@@ -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
- def sample_length()
22
- @structure.inject(0) {|sum, pattern_name|
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
- def sample_length_with_overflow()
28
- if(@structure.length == 0)
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
- def total_tracks()
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 sample_data(pattern_name, split)
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
- fill_value = (@kit.num_channels == 1) ? 0 : [].fill(0, 0, @kit.num_channels)
47
-
48
- if(pattern_name == "")
49
- if(split)
50
- return sample_data_split_all_patterns(fill_value, num_tracks_in_song)
51
- else
52
- return sample_data_combined_all_patterns(fill_value, num_tracks_in_song)
53
- end
54
- else
55
- pattern = @patterns[pattern_name.downcase.to_sym]
56
-
57
- if(pattern == nil)
58
- raise StandardError, "Pattern '#{pattern_name}' not found in song."
59
- else
60
- primary_sample_length = pattern.sample_length(@tick_sample_length)
61
-
62
- if(split)
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
- return sample_data_combined_single_pattern(fill_value, num_tracks_in_song, pattern, primary_sample_length)
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
- if(new_tempo.class != Fixnum || new_tempo <= 0)
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
- if(overflow != {})
207
+ unless overflow == {}
101
208
  longest_overflow = overflow[overflow.keys.first]
102
- overflow.keys.each {|track_name|
103
- if(overflow[track_name].length > longest_overflow.length)
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
- final_overflow_pattern.track "", [], "."
110
- final_overflow_sample_data = final_overflow_pattern.sample_data(longest_overflow.length, @kit.num_channels, num_tracks_in_song, overflow, false)
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