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