beats 1.2.4 → 1.2.5

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