beats 1.2.4 → 1.2.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +1 -1
- data/README.markdown +24 -4
- data/Rakefile +1 -1
- data/bin/beats +10 -15
- data/ext/mkrf_conf.rb +28 -0
- data/lib/beats.rb +12 -76
- data/lib/beats/audioengine.rb +163 -0
- data/lib/beats/audioutils.rb +74 -0
- data/lib/beats/beatsrunner.rb +76 -0
- data/lib/beats/kit.rb +187 -0
- data/lib/beats/pattern.rb +88 -0
- data/lib/beats/song.rb +162 -0
- data/lib/beats/songoptimizer.rb +162 -0
- data/lib/beats/songparser.rb +217 -0
- data/lib/beats/track.rb +57 -0
- data/lib/wavefile/cachingwriter.rb +1 -1
- data/test/audioengine_test.rb +5 -5
- data/test/cachingwriter_test.rb +1 -1
- data/test/fixtures/valid/foo.txt +18 -0
- data/test/includes.rb +2 -9
- data/test/integration_test.rb +20 -20
- data/test/kit_test.rb +28 -28
- data/test/pattern_test.rb +13 -13
- data/test/song_test.rb +23 -23
- data/test/songoptimizer_test.rb +25 -25
- data/test/songparser_test.rb +11 -11
- data/test/sounds/bass.wav +0 -0
- data/test/sounds/bass2.wav +0 -0
- data/test/track_test.rb +12 -12
- metadata +27 -22
- data/lib/audioengine.rb +0 -161
- data/lib/audioutils.rb +0 -73
- data/lib/kit.rb +0 -185
- data/lib/pattern.rb +0 -86
- data/lib/song.rb +0 -160
- data/lib/songoptimizer.rb +0 -160
- data/lib/songparser.rb +0 -216
- data/lib/track.rb +0 -55
data/lib/songoptimizer.rb
DELETED
@@ -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
|
data/lib/songparser.rb
DELETED
@@ -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
|
data/lib/track.rb
DELETED
@@ -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
|