beats 1.2.4 → 1.2.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,162 @@
1
+ module Beats
2
+ # This class is used to transform a Song object into an equivalent Song object whose
3
+ # sample data will be generated faster by the audio engine.
4
+ #
5
+ # The primary method is optimize(). Currently, it performs two optimizations:
6
+ #
7
+ # 1.) Breaks patterns into shorter patterns. Generating one long Pattern is generally
8
+ # slower than generating several short Patterns with the same combined length.
9
+ # 2.) Replaces Patterns which are equivalent (i.e. they have the same tracks with the
10
+ # same rhythms) with one canonical Pattern. This allows for better caching, by
11
+ # preventing the audio engine from generating the same sample data more than once.
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_flow()
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
+ 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
+ # For each pattern, add a new pattern to new song every max_pattern_length steps
63
+ optimized_flow = {}
64
+ original_song.patterns.values.each do |pattern|
65
+ step_index = 0
66
+ optimized_flow[pattern.name] = []
67
+
68
+ while(pattern.tracks.values.first.rhythm[step_index] != nil) do
69
+ # TODO: Is this pattern 100% sufficient to prevent collisions between subdivided
70
+ # pattern names and existing patterns with numeric suffixes?
71
+ new_pattern = optimized_song.pattern("#{pattern.name}_#{step_index}".to_sym)
72
+ optimized_flow[pattern.name] << new_pattern.name
73
+ pattern.tracks.values.each do |track|
74
+ sub_track_pattern = track.rhythm[step_index...(step_index + max_pattern_length)]
75
+
76
+ if sub_track_pattern != blank_track_pattern
77
+ new_pattern.track(track.name, sub_track_pattern)
78
+ end
79
+ end
80
+
81
+ # If no track has a trigger during this step pattern, add a blank track.
82
+ # Otherwise, this pattern will have no steps, and no sound will be generated,
83
+ # causing the pattern to be "compacted away".
84
+ if new_pattern.tracks.empty?
85
+ new_pattern.track("placeholder", blank_track_pattern)
86
+ end
87
+
88
+ step_index += max_pattern_length
89
+ end
90
+ end
91
+
92
+ # Replace the Song's flow to reference the new sub-divided patterns
93
+ # instead of the old patterns.
94
+ optimized_flow = original_song.flow.map do |original_pattern|
95
+ optimized_flow[original_pattern]
96
+ end
97
+ optimized_song.flow = optimized_flow.flatten
98
+
99
+ optimized_song
100
+ end
101
+
102
+
103
+ # Replaces any Patterns that are duplicates (i.e., each track uses the same sound and has
104
+ # the same rhythm) with a single canonical pattern.
105
+ #
106
+ # The benefit of this is that it allows more effective caching. For example, suppose Pattern A
107
+ # and Pattern B are equivalent. If Pattern A gets generated first, it will be cached. When
108
+ # Pattern B gets generated, it will be generated from scratch instead of using Pattern A's
109
+ # cached data. Consolidating duplicates into one prevents this from happening.
110
+ #
111
+ # Duplicate Patterns are more likely to occur after calling subdivide_song_patterns().
112
+ def prune_duplicate_patterns(song)
113
+ pattern_replacements = determine_pattern_replacements(song.patterns)
114
+
115
+ # Update flow to remove references to duplicates
116
+ new_flow = song.flow
117
+ pattern_replacements.each do |duplicate, replacement|
118
+ new_flow = new_flow.map do |pattern_name|
119
+ (pattern_name == duplicate) ? replacement : pattern_name
120
+ end
121
+ end
122
+ song.flow = new_flow
123
+
124
+ # This isn't strictly necessary, but makes resulting songs easier to read for debugging purposes.
125
+ song.remove_unused_patterns()
126
+
127
+ song
128
+ end
129
+
130
+
131
+ # Examines a set of patterns definitions, determining which ones have the same tracks with the same
132
+ # rhythms. Then constructs a hash of pattern => pattern indicating that all occurances in the flow
133
+ # of the key should be replaced with the value, so that the other equivalent definitions can be pruned
134
+ # from the song (and hence their sample data doesn't need to be generated).
135
+ def determine_pattern_replacements(patterns)
136
+ seen_patterns = []
137
+ replacements = {}
138
+
139
+ # Pattern names are sorted to ensure predictable pattern replacement. Makes tests easier to write.
140
+ # Sort function added manually because Ruby 1.8 doesn't know how to sort symbols...
141
+ pattern_names = patterns.keys.sort {|x, y| x.to_s <=> y.to_s }
142
+
143
+ # Detect duplicates
144
+ pattern_names.each do |pattern_name|
145
+ pattern = patterns[pattern_name]
146
+ found_duplicate = false
147
+ seen_patterns.each do |seen_pattern|
148
+ if !found_duplicate && pattern.same_tracks_as?(seen_pattern)
149
+ replacements[pattern.name.to_sym] = seen_pattern.name.to_sym
150
+ found_duplicate = true
151
+ end
152
+ end
153
+
154
+ if !found_duplicate
155
+ seen_patterns << pattern
156
+ end
157
+ end
158
+
159
+ replacements
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,217 @@
1
+ module Beats
2
+ class SongParseError < RuntimeError; end
3
+
4
+
5
+ # This class is used to parse a raw YAML song definition into domain objects (i.e.
6
+ # Song, Pattern, Track, and Kit). These domain objects can then be used by AudioEngine
7
+ # to generate the actual audio data that is saved to disk.
8
+ #
9
+ # The sole public method is parse(). It takes a raw YAML string and returns a Song and
10
+ # Kit object (or raises an error if the YAML string couldn't be parsed correctly).
11
+ class SongParser
12
+ DONT_USE_STRUCTURE_WARNING =
13
+ "\n" +
14
+ "WARNING! This song contains a 'Structure' section in the header.\n" +
15
+ "As of BEATS 1.2.1, the 'Structure' section should be renamed 'Flow'.\n" +
16
+ "You should change your song file, in a future version using 'Structure' will cause an error.\n"
17
+
18
+ NO_SONG_HEADER_ERROR_MSG =
19
+ "Song must have a header. Here's an example:
20
+
21
+ Song:
22
+ Tempo: 120
23
+ Flow:
24
+ - Verse: x2
25
+ - Chorus: x2"
26
+
27
+ def initialize
28
+ end
29
+
30
+
31
+ # Parses a raw YAML song definition and converts it into a Song and Kit object.
32
+ def parse(base_path, raw_yaml_string)
33
+ raw_song_components = hashify_raw_yaml(raw_yaml_string)
34
+
35
+ unless raw_song_components[:folder].nil?
36
+ base_path = raw_song_components[:folder]
37
+ end
38
+
39
+ song = Song.new()
40
+
41
+ # 1.) Set tempo
42
+ begin
43
+ unless raw_song_components[:tempo].nil?
44
+ song.tempo = raw_song_components[:tempo]
45
+ end
46
+ rescue InvalidTempoError => detail
47
+ raise SongParseError, "#{detail}"
48
+ end
49
+
50
+ # 2.) Build the kit
51
+ begin
52
+ kit = build_kit(base_path, raw_song_components[:kit], raw_song_components[:patterns])
53
+ rescue SoundFileNotFoundError => detail
54
+ raise SongParseError, "#{detail}"
55
+ rescue InvalidSoundFormatError => detail
56
+ raise SongParseError, "#{detail}"
57
+ end
58
+
59
+ # 3.) Load patterns
60
+ add_patterns_to_song(song, raw_song_components[:patterns])
61
+
62
+ # 4.) Set flow
63
+ if raw_song_components[:flow].nil?
64
+ raise SongParseError, "Song must have a Flow section in the header."
65
+ else
66
+ set_song_flow(song, raw_song_components[:flow])
67
+ end
68
+
69
+ return song, kit
70
+ end
71
+
72
+
73
+ private
74
+
75
+
76
+ def hashify_raw_yaml(raw_yaml_string)
77
+ begin
78
+ raw_song_definition = YAML.load(raw_yaml_string)
79
+ rescue ArgumentError => detail
80
+ raise SongParseError, "Syntax error in YAML file"
81
+ end
82
+
83
+ raw_song_components = {}
84
+ raw_song_components[:full_definition] = downcase_hash_keys(raw_song_definition)
85
+
86
+ unless raw_song_components[:full_definition]["song"].nil?
87
+ raw_song_components[:header] = downcase_hash_keys(raw_song_components[:full_definition]["song"])
88
+ else
89
+ raise SongParseError, NO_SONG_HEADER_ERROR_MSG
90
+ end
91
+ raw_song_components[:tempo] = raw_song_components[:header]["tempo"]
92
+ raw_song_components[:folder] = raw_song_components[:header]["folder"]
93
+ raw_song_components[:kit] = raw_song_components[:header]["kit"]
94
+
95
+ raw_flow = raw_song_components[:header]["flow"]
96
+ raw_structure = raw_song_components[:header]["structure"]
97
+ unless raw_flow.nil?
98
+ raw_song_components[:flow] = raw_flow
99
+ else
100
+ unless raw_structure.nil?
101
+ puts DONT_USE_STRUCTURE_WARNING
102
+ end
103
+
104
+ raw_song_components[:flow] = raw_structure
105
+ end
106
+
107
+ raw_song_components[:patterns] = raw_song_components[:full_definition].reject {|k, v| k == "song"}
108
+
109
+ return raw_song_components
110
+ end
111
+
112
+
113
+ def build_kit(base_path, raw_kit, raw_patterns)
114
+ kit_items = {}
115
+
116
+ # Add sounds defined in the Kit section of the song header
117
+ # Converts [{a=>1}, {b=>2}, {c=>3}] from raw YAML to {a=>1, b=>2, c=>3}
118
+ # TODO: Raise error is same name is defined more than once in the Kit
119
+ unless raw_kit.nil?
120
+ raw_kit.each do |kit_item|
121
+ kit_items[kit_item.keys.first] = kit_item.values.first
122
+ end
123
+ end
124
+
125
+ # Add sounds not defined in Kit section, but used in individual tracks
126
+ # TODO Investigate detecting duplicate keys already defined in the Kit section, as this could possibly
127
+ # result in a performance improvement when the sound has to be converted to a different bit rate/num channels,
128
+ # as well as use less memory.
129
+ raw_patterns.keys.each do |key|
130
+ track_list = raw_patterns[key]
131
+
132
+ unless track_list.nil?
133
+ track_list.each do |track_definition|
134
+ track_name = track_definition.keys.first
135
+ track_path = track_name
136
+
137
+ if track_name != Pattern::FLOW_TRACK_NAME && kit_items[track_name].nil?
138
+ kit_items[track_name] = track_path
139
+ end
140
+ end
141
+ end
142
+ end
143
+
144
+ Kit.new(base_path, kit_items)
145
+ end
146
+
147
+
148
+ def add_patterns_to_song(song, raw_patterns)
149
+ raw_patterns.keys.each do |key|
150
+ new_pattern = song.pattern key.to_sym
151
+
152
+ track_list = raw_patterns[key]
153
+ # TODO Also raise error if only there is only 1 track and it's a flow track
154
+ if track_list.nil?
155
+ # TODO: Use correct capitalization of pattern name in error message
156
+ # TODO: Possibly allow if pattern not referenced in the Flow, or has 0 repeats?
157
+ raise SongParseError, "Pattern '#{key}' has no tracks. It needs at least one."
158
+ end
159
+
160
+ # TODO: What if there is more than one flow? Raise error, or have last one win?
161
+ track_list.each do |track_definition|
162
+ track_name = track_definition.keys.first
163
+
164
+ # Handle case where no track rhythm is specified (i.e. "- foo.wav:" instead of "- foo.wav: X.X.X.X.")
165
+ track_definition[track_name] ||= ""
166
+
167
+ new_pattern.track track_name, track_definition[track_name]
168
+ end
169
+ end
170
+ end
171
+
172
+
173
+ def set_song_flow(song, raw_flow)
174
+ flow = []
175
+
176
+ raw_flow.each{|pattern_item|
177
+ if pattern_item.class == String
178
+ pattern_item = {pattern_item => "x1"}
179
+ end
180
+
181
+ pattern_name = pattern_item.keys.first
182
+ pattern_name_sym = pattern_name.downcase.to_sym
183
+
184
+ # Convert the number of repeats from a String such as "x4" into an integer such as 4.
185
+ multiples_str = pattern_item[pattern_name]
186
+ multiples_str.slice!(0)
187
+ multiples = multiples_str.to_i
188
+
189
+ unless multiples_str.match(/[^0-9]/).nil?
190
+ raise SongParseError,
191
+ "'#{multiples_str}' is an invalid number of repeats for pattern '#{pattern_name}'. Number of repeats should be a whole number."
192
+ else
193
+ if multiples < 0
194
+ raise SongParseError, "'#{multiples_str}' is an invalid number of repeats for pattern '#{pattern_name}'. Must be 0 or greater."
195
+ elsif multiples > 0 && !song.patterns.has_key?(pattern_name_sym)
196
+ # This test is purposefully designed to only throw an error if the number of repeats is greater
197
+ # than 0. This allows you to specify an undefined pattern in the flow with "x0" repeats.
198
+ # This can be convenient for defining the flow before all patterns have been added to the song file.
199
+ raise SongParseError, "Song flow includes non-existent pattern: #{pattern_name}."
200
+ end
201
+ end
202
+
203
+ multiples.times { flow << pattern_name_sym }
204
+ }
205
+ song.flow = flow
206
+ end
207
+
208
+
209
+ # Converts all hash keys to be lowercase
210
+ def downcase_hash_keys(hash)
211
+ return hash.inject({}) do |new_hash, pair|
212
+ new_hash[pair.first.downcase] = pair.last
213
+ new_hash
214
+ end
215
+ end
216
+ end
217
+ end
@@ -0,0 +1,57 @@
1
+ module Beats
2
+ class InvalidRhythmError < RuntimeError; end
3
+
4
+
5
+ # Domain object which models a kit sound playing a rhythm. For example,
6
+ # a bass drum playing every quarter note for two measures.
7
+ #
8
+ # This object is like sheet music; the AudioEngine is responsible creating actual
9
+ # audio data for a Track (with the help of a Kit).
10
+ class Track
11
+ REST = "."
12
+ BEAT = "X"
13
+ BARLINE = "|"
14
+
15
+ def initialize(name, rhythm)
16
+ # TODO: Add validation for input parameters
17
+ @name = name
18
+ self.rhythm = rhythm
19
+ end
20
+
21
+ # TODO: What to have this invoked when setting like this?
22
+ # track.rhythm[x..y] = whatever
23
+ def rhythm=(rhythm)
24
+ @rhythm = rhythm.delete(BARLINE)
25
+ beats = []
26
+
27
+ beat_length = 0
28
+ #rhythm.each_char{|ch|
29
+ @rhythm.each_byte do |ch|
30
+ ch = ch.chr
31
+ if ch == BEAT
32
+ beats << beat_length
33
+ beat_length = 1
34
+ elsif ch == REST
35
+ beat_length += 1
36
+ else
37
+ raise InvalidRhythmError, "Track #{@name} has an invalid rhythm: '#{rhythm}'. Can only contain '#{BEAT}', '#{REST}' or '#{BARLINE}'"
38
+ end
39
+ end
40
+
41
+ if beat_length > 0
42
+ beats << beat_length
43
+ end
44
+ if beats == []
45
+ beats = [0]
46
+ end
47
+ @beats = beats
48
+ end
49
+
50
+ def step_count
51
+ @rhythm.length
52
+ end
53
+
54
+ attr_accessor :name
55
+ attr_reader :rhythm, :beats
56
+ end
57
+ end
@@ -30,7 +30,7 @@ module WaveFile
30
30
  end
31
31
 
32
32
  @file.syswrite(packed_buffer_data[:data])
33
- @samples_written += packed_buffer_data[:sample_count]
33
+ @total_sample_frames += packed_buffer_data[:sample_count]
34
34
  end
35
35
  end
36
36
  end
@@ -28,15 +28,15 @@ class AudioEngineTest < Test::Unit::TestCase
28
28
  test_engines = {}
29
29
  base_path = File.dirname(__FILE__) + "/.."
30
30
  song_parser = SongParser.new()
31
-
31
+
32
32
  test_engines[:blank] = AudioEngine.new(Song.new(), Kit.new(base_path, {}))
33
33
 
34
34
  FIXTURES.each do |fixture_name|
35
35
  song, kit = song_parser.parse(base_path, File.read("test/fixtures/valid/#{fixture_name}.txt"))
36
36
  test_engines[fixture_name] = AudioEngine.new(song, kit)
37
37
  end
38
-
39
- return test_engines
38
+
39
+ test_engines
40
40
  end
41
41
 
42
42
  def test_initialize
@@ -142,7 +142,7 @@ class AudioEngineTest < Test::Unit::TestCase
142
142
  #helper_generate_track_sample_data kit, "XX", 1, [-100, -100], [200, 300, -400]
143
143
 
144
144
  # 3C.) Tick sample length is shorter than the sound sample data, but not by an integer amount.
145
- #
145
+ #
146
146
  # Each step of 1.83 samples should end on the following boundaries:
147
147
  # Tick: 1, 2, 3, 4, 5, 6
148
148
  # Raw: 0.0, 1.83, 3.66, 5.49, 7.32, 9.15, 10.98
@@ -162,7 +162,7 @@ class AudioEngineTest < Test::Unit::TestCase
162
162
  engine = MockAudioEngine.new(Song.new(), kit)
163
163
  engine.step_sample_length = step_sample_length
164
164
  actual = engine.generate_track_sample_data(track, kit.get_sample_data("S"))
165
-
165
+
166
166
  assert_equal(Hash, actual.class)
167
167
  assert_equal(["overflow", "primary"], actual.keys.map{|key| key.to_s}.sort)
168
168
  assert_equal(expected_primary, actual[:primary])