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