beats 1.2.4 → 1.2.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,160 +0,0 @@
1
- # This class is used to transform a Song object into an equivalent Song object whose
2
- # sample data will be generated faster by the audio 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) with one canonical Pattern. This allows for better caching, by
10
- # preventing the audio engine from generating the same sample data more than once.
11
- #
12
- # Note that step #1 actually performs double duty, because breaking Patterns into smaller
13
- # pieces increases the likelihood there will be duplicates that can be combined.
14
- class SongOptimizer
15
- def initialize
16
- end
17
-
18
- # Returns a Song that will produce the same output as original_song, but should be
19
- # generated faster.
20
- def optimize(original_song, max_pattern_length)
21
- # 1.) Create a new song, cloned from the original
22
- optimized_song = original_song.copy_ignoring_patterns_and_flow()
23
-
24
- # 2.) Subdivide patterns
25
- optimized_song = subdivide_song_patterns(original_song, optimized_song, max_pattern_length)
26
-
27
- # 3.) Prune duplicate patterns
28
- optimized_song = prune_duplicate_patterns(optimized_song)
29
-
30
- return optimized_song
31
- end
32
-
33
- protected
34
-
35
- # Splits the patterns of a Song into smaller patterns, each one with at most
36
- # max_pattern_length steps. For example, if max_pattern_length is 4, then
37
- # the following pattern:
38
- #
39
- # track1: X...X...X.
40
- # track2: ..X.....X.
41
- # track3: X.X.X.X.X.
42
- #
43
- # will be converted into the following 3 patterns:
44
- #
45
- # track1: X...
46
- # track2: ..X.
47
- # track3: X.X.
48
- #
49
- # track1: X...
50
- # track3: X.X.
51
- #
52
- # track1: X.
53
- # track2: X.
54
- # track3: X.
55
- #
56
- # Note that if a track in a sub-divided pattern has no triggers (such as track2 in the
57
- # 2nd pattern above), it will not be included in the new pattern.
58
- def subdivide_song_patterns(original_song, optimized_song, max_pattern_length)
59
- blank_track_pattern = '.' * max_pattern_length
60
-
61
- # For each pattern, add a new pattern to new song every max_pattern_length steps
62
- optimized_flow = {}
63
- original_song.patterns.values.each do |pattern|
64
- step_index = 0
65
- optimized_flow[pattern.name] = []
66
-
67
- while(pattern.tracks.values.first.rhythm[step_index] != nil) do
68
- # TODO: Is this pattern 100% sufficient to prevent collisions between subdivided
69
- # pattern names and existing patterns with numeric suffixes?
70
- new_pattern = optimized_song.pattern("#{pattern.name}_#{step_index}".to_sym)
71
- optimized_flow[pattern.name] << new_pattern.name
72
- pattern.tracks.values.each do |track|
73
- sub_track_pattern = track.rhythm[step_index...(step_index + max_pattern_length)]
74
-
75
- if sub_track_pattern != blank_track_pattern
76
- new_pattern.track(track.name, sub_track_pattern)
77
- end
78
- end
79
-
80
- # If no track has a trigger during this step pattern, add a blank track.
81
- # Otherwise, this pattern will have no steps, and no sound will be generated,
82
- # causing the pattern to be "compacted away".
83
- if new_pattern.tracks.empty?
84
- new_pattern.track("placeholder", blank_track_pattern)
85
- end
86
-
87
- step_index += max_pattern_length
88
- end
89
- end
90
-
91
- # Replace the Song's flow to reference the new sub-divided patterns
92
- # instead of the old patterns.
93
- optimized_flow = original_song.flow.map do |original_pattern|
94
- optimized_flow[original_pattern]
95
- end
96
- optimized_song.flow = optimized_flow.flatten
97
-
98
- return optimized_song
99
- end
100
-
101
-
102
- # Replaces any Patterns that are duplicates (i.e., each track uses the same sound and has
103
- # the same rhythm) with a single canonical pattern.
104
- #
105
- # The benefit of this is that it allows more effective caching. For example, suppose Pattern A
106
- # and Pattern B are equivalent. If Pattern A gets generated first, it will be cached. When
107
- # Pattern B gets generated, it will be generated from scratch instead of using Pattern A's
108
- # cached data. Consolidating duplicates into one prevents this from happening.
109
- #
110
- # Duplicate Patterns are more likely to occur after calling subdivide_song_patterns().
111
- def prune_duplicate_patterns(song)
112
- pattern_replacements = determine_pattern_replacements(song.patterns)
113
-
114
- # Update flow to remove references to duplicates
115
- new_flow = song.flow
116
- pattern_replacements.each do |duplicate, replacement|
117
- new_flow = new_flow.map do |pattern_name|
118
- (pattern_name == duplicate) ? replacement : pattern_name
119
- end
120
- end
121
- song.flow = new_flow
122
-
123
- # This isn't strictly necessary, but makes resulting songs easier to read for debugging purposes.
124
- song.remove_unused_patterns()
125
-
126
- return song
127
- end
128
-
129
-
130
- # Examines a set of patterns definitions, determining which ones have the same tracks with the same
131
- # rhythms. Then constructs a hash of pattern => pattern indicating that all occurances in the flow
132
- # of the key should be replaced with the value, so that the other equivalent definitions can be pruned
133
- # from the song (and hence their sample data doesn't need to be generated).
134
- def determine_pattern_replacements(patterns)
135
- seen_patterns = []
136
- replacements = {}
137
-
138
- # Pattern names are sorted to ensure predictable pattern replacement. Makes tests easier to write.
139
- # Sort function added manually because Ruby 1.8 doesn't know how to sort symbols...
140
- pattern_names = patterns.keys.sort {|x, y| x.to_s <=> y.to_s }
141
-
142
- # Detect duplicates
143
- pattern_names.each do |pattern_name|
144
- pattern = patterns[pattern_name]
145
- found_duplicate = false
146
- seen_patterns.each do |seen_pattern|
147
- if !found_duplicate && pattern.same_tracks_as?(seen_pattern)
148
- replacements[pattern.name.to_sym] = seen_pattern.name.to_sym
149
- found_duplicate = true
150
- end
151
- end
152
-
153
- if !found_duplicate
154
- seen_patterns << pattern
155
- end
156
- end
157
-
158
- return replacements
159
- end
160
- end
@@ -1,216 +0,0 @@
1
- class SongParseError < RuntimeError; end
2
-
3
-
4
- # This class is used to parse a raw YAML song definition into domain objects (i.e.
5
- # Song, Pattern, Track, and Kit). These domain objects can then be used by AudioEngine
6
- # to generate the actual audio data that is saved to disk.
7
- #
8
- # The sole public method is parse(). It takes a raw YAML string and returns a Song and
9
- # Kit object (or raises an error if the YAML string couldn't be parsed correctly).
10
- class SongParser
11
- DONT_USE_STRUCTURE_WARNING =
12
- "\n" +
13
- "WARNING! This song contains a 'Structure' section in the header.\n" +
14
- "As of BEATS 1.2.1, the 'Structure' section should be renamed 'Flow'.\n" +
15
- "You should change your song file, in a future version using 'Structure' will cause an error.\n"
16
-
17
- NO_SONG_HEADER_ERROR_MSG =
18
- "Song must have a header. Here's an example:
19
-
20
- Song:
21
- Tempo: 120
22
- Flow:
23
- - Verse: x2
24
- - Chorus: x2"
25
-
26
- def initialize
27
- end
28
-
29
-
30
- # Parses a raw YAML song definition and converts it into a Song and Kit object.
31
- def parse(base_path, raw_yaml_string)
32
- raw_song_components = hashify_raw_yaml(raw_yaml_string)
33
-
34
- unless raw_song_components[:folder] == nil
35
- base_path = raw_song_components[:folder]
36
- end
37
-
38
- song = Song.new()
39
-
40
- # 1.) Set tempo
41
- begin
42
- if raw_song_components[:tempo] != nil
43
- song.tempo = raw_song_components[:tempo]
44
- end
45
- rescue InvalidTempoError => detail
46
- raise SongParseError, "#{detail}"
47
- end
48
-
49
- # 2.) Build the kit
50
- begin
51
- kit = build_kit(base_path, raw_song_components[:kit], raw_song_components[:patterns])
52
- rescue SoundFileNotFoundError => detail
53
- raise SongParseError, "#{detail}"
54
- rescue InvalidSoundFormatError => detail
55
- raise SongParseError, "#{detail}"
56
- end
57
-
58
- # 3.) Load patterns
59
- add_patterns_to_song(song, raw_song_components[:patterns])
60
-
61
- # 4.) Set flow
62
- if raw_song_components[:flow] == nil
63
- raise SongParseError, "Song must have a Flow section in the header."
64
- else
65
- set_song_flow(song, raw_song_components[:flow])
66
- end
67
-
68
- return song, kit
69
- end
70
-
71
-
72
- private
73
-
74
-
75
- def hashify_raw_yaml(raw_yaml_string)
76
- begin
77
- raw_song_definition = YAML.load(raw_yaml_string)
78
- rescue ArgumentError => detail
79
- raise SongParseError, "Syntax error in YAML file"
80
- end
81
-
82
- raw_song_components = {}
83
- raw_song_components[:full_definition] = downcase_hash_keys(raw_song_definition)
84
-
85
- if raw_song_components[:full_definition]["song"] != nil
86
- raw_song_components[:header] = downcase_hash_keys(raw_song_components[:full_definition]["song"])
87
- else
88
- raise SongParseError, NO_SONG_HEADER_ERROR_MSG
89
- end
90
- raw_song_components[:tempo] = raw_song_components[:header]["tempo"]
91
- raw_song_components[:folder] = raw_song_components[:header]["folder"]
92
- raw_song_components[:kit] = raw_song_components[:header]["kit"]
93
-
94
- raw_flow = raw_song_components[:header]["flow"]
95
- raw_structure = raw_song_components[:header]["structure"]
96
- if raw_flow != nil
97
- raw_song_components[:flow] = raw_flow
98
- else
99
- if raw_structure != nil
100
- puts DONT_USE_STRUCTURE_WARNING
101
- end
102
-
103
- raw_song_components[:flow] = raw_structure
104
- end
105
-
106
- raw_song_components[:patterns] = raw_song_components[:full_definition].reject {|k, v| k == "song"}
107
-
108
- return raw_song_components
109
- end
110
-
111
-
112
- def build_kit(base_path, raw_kit, raw_patterns)
113
- kit_items = {}
114
-
115
- # Add sounds defined in the Kit section of the song header
116
- # Converts [{a=>1}, {b=>2}, {c=>3}] from raw YAML to {a=>1, b=>2, c=>3}
117
- # TODO: Raise error is same name is defined more than once in the Kit
118
- unless raw_kit == nil
119
- raw_kit.each do |kit_item|
120
- kit_items[kit_item.keys.first] = kit_item.values.first
121
- end
122
- end
123
-
124
- # Add sounds not defined in Kit section, but used in individual tracks
125
- # TODO Investigate detecting duplicate keys already defined in the Kit section, as this could possibly
126
- # result in a performance improvement when the sound has to be converted to a different bit rate/num channels,
127
- # as well as use less memory.
128
- raw_patterns.keys.each do |key|
129
- track_list = raw_patterns[key]
130
-
131
- unless track_list == nil
132
- track_list.each do |track_definition|
133
- track_name = track_definition.keys.first
134
- track_path = track_name
135
-
136
- if track_name != Pattern::FLOW_TRACK_NAME && kit_items[track_name] == nil
137
- kit_items[track_name] = track_path
138
- end
139
- end
140
- end
141
- end
142
-
143
- kit = Kit.new(base_path, kit_items)
144
- return kit
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
@@ -1,55 +0,0 @@
1
- class InvalidRhythmError < RuntimeError; end
2
-
3
-
4
- # Domain object which models a kit sound playing a rhythm. For example,
5
- # a bass drum playing every quarter note for two measures.
6
- #
7
- # This object is like sheet music; the AudioEngine is responsible creating actual
8
- # audio data for a Track (with the help of a Kit).
9
- class Track
10
- REST = "."
11
- BEAT = "X"
12
- BARLINE = "|"
13
-
14
- def initialize(name, rhythm)
15
- # TODO: Add validation for input parameters
16
- @name = name
17
- self.rhythm = rhythm
18
- end
19
-
20
- # TODO: What to have this invoked when setting like this?
21
- # track.rhythm[x..y] = whatever
22
- def rhythm=(rhythm)
23
- @rhythm = rhythm.delete(BARLINE)
24
- beats = []
25
-
26
- beat_length = 0
27
- #rhythm.each_char{|ch|
28
- @rhythm.each_byte do |ch|
29
- ch = ch.chr
30
- if ch == BEAT
31
- beats << beat_length
32
- beat_length = 1
33
- elsif ch == REST
34
- beat_length += 1
35
- else
36
- raise InvalidRhythmError, "Track #{@name} has an invalid rhythm: '#{rhythm}'. Can only contain '#{BEAT}', '#{REST}' or '#{BARLINE}'"
37
- end
38
- end
39
-
40
- if beat_length > 0
41
- beats << beat_length
42
- end
43
- if beats == []
44
- beats = [0]
45
- end
46
- @beats = beats
47
- end
48
-
49
- def step_count
50
- @rhythm.length
51
- end
52
-
53
- attr_accessor :name
54
- attr_reader :rhythm, :beats
55
- end