-
Notifications
You must be signed in to change notification settings - Fork 28
/
songparser.rb
216 lines (170 loc) · 7.2 KB
/
songparser.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
class SongParseError < RuntimeError; end
# This class is used to parse a raw YAML song definition into domain objects (i.e.
# Song, Pattern, Track, and Kit). These domain objects can then be used by AudioEngine
# to generate the actual audio data that is saved to disk.
#
# The sole public method is parse(). It takes a raw YAML string and returns a Song and
# Kit object (or raises an error if the YAML string couldn't be parsed correctly).
class SongParser
DONT_USE_STRUCTURE_WARNING =
"\n" +
"WARNING! This song contains a 'Structure' section in the header.\n" +
"As of BEATS 1.2.1, the 'Structure' section should be renamed 'Flow'.\n" +
"You should change your song file, in a future version using 'Structure' will cause an error.\n"
NO_SONG_HEADER_ERROR_MSG =
"Song must have a header. Here's an example:
Song:
Tempo: 120
Flow:
- Verse: x2
- Chorus: x2"
def initialize
end
# Parses a raw YAML song definition and converts it into a Song and Kit object.
def parse(base_path, raw_yaml_string)
raw_song_components = hashify_raw_yaml(raw_yaml_string)
unless raw_song_components[:folder] == nil
base_path = raw_song_components[:folder]
end
song = Song.new()
# 1.) Set tempo
begin
if raw_song_components[:tempo] != nil
song.tempo = raw_song_components[:tempo]
end
rescue InvalidTempoError => detail
raise SongParseError, "#{detail}"
end
# 2.) Build the kit
begin
kit = build_kit(base_path, raw_song_components[:kit], raw_song_components[:patterns])
rescue SoundFileNotFoundError => detail
raise SongParseError, "#{detail}"
rescue InvalidSoundFormatError => detail
raise SongParseError, "#{detail}"
end
# 3.) Load patterns
add_patterns_to_song(song, raw_song_components[:patterns])
# 4.) Set flow
if raw_song_components[:flow] == nil
raise SongParseError, "Song must have a Flow section in the header."
else
set_song_flow(song, raw_song_components[:flow])
end
return song, kit
end
private
def hashify_raw_yaml(raw_yaml_string)
begin
raw_song_definition = YAML.load(raw_yaml_string)
rescue ArgumentError => detail
raise SongParseError, "Syntax error in YAML file"
end
raw_song_components = {}
raw_song_components[:full_definition] = downcase_hash_keys(raw_song_definition)
if raw_song_components[:full_definition]["song"] != nil
raw_song_components[:header] = downcase_hash_keys(raw_song_components[:full_definition]["song"])
else
raise SongParseError, NO_SONG_HEADER_ERROR_MSG
end
raw_song_components[:tempo] = raw_song_components[:header]["tempo"]
raw_song_components[:folder] = raw_song_components[:header]["folder"]
raw_song_components[:kit] = raw_song_components[:header]["kit"]
raw_flow = raw_song_components[:header]["flow"]
raw_structure = raw_song_components[:header]["structure"]
if raw_flow != nil
raw_song_components[:flow] = raw_flow
else
if raw_structure != nil
puts DONT_USE_STRUCTURE_WARNING
end
raw_song_components[:flow] = raw_structure
end
raw_song_components[:patterns] = raw_song_components[:full_definition].reject {|k, v| k == "song"}
return raw_song_components
end
def build_kit(base_path, raw_kit, raw_patterns)
kit_items = {}
# Add sounds defined in the Kit section of the song header
# Converts [{a=>1}, {b=>2}, {c=>3}] from raw YAML to {a=>1, b=>2, c=>3}
# TODO: Raise error is same name is defined more than once in the Kit
unless raw_kit == nil
raw_kit.each do |kit_item|
kit_items[kit_item.keys.first] = kit_item.values.first
end
end
# Add sounds not defined in Kit section, but used in individual tracks
# TODO Investigate detecting duplicate keys already defined in the Kit section, as this could possibly
# result in a performance improvement when the sound has to be converted to a different bit rate/num channels,
# as well as use less memory.
raw_patterns.keys.each do |key|
track_list = raw_patterns[key]
unless track_list == nil
track_list.each do |track_definition|
track_name = track_definition.keys.first
track_path = track_name
if track_name != Pattern::FLOW_TRACK_NAME && kit_items[track_name] == nil
kit_items[track_name] = track_path
end
end
end
end
kit = Kit.new(base_path, kit_items)
return kit
end
def add_patterns_to_song(song, raw_patterns)
raw_patterns.keys.each do |key|
new_pattern = song.pattern key.to_sym
track_list = raw_patterns[key]
# TODO Also raise error if only there is only 1 track and it's a flow track
if track_list == nil
# TODO: Use correct capitalization of pattern name in error message
# TODO: Possibly allow if pattern not referenced in the Flow, or has 0 repeats?
raise SongParseError, "Pattern '#{key}' has no tracks. It needs at least one."
end
# TODO: What if there is more than one flow? Raise error, or have last one win?
track_list.each do |track_definition|
track_name = track_definition.keys.first
# Handle case where no track rhythm is specified (i.e. "- foo.wav:" instead of "- foo.wav: X.X.X.X.")
track_definition[track_name] ||= ""
new_pattern.track track_name, track_definition[track_name]
end
end
end
def set_song_flow(song, raw_flow)
flow = []
raw_flow.each{|pattern_item|
if pattern_item.class == String
pattern_item = {pattern_item => "x1"}
end
pattern_name = pattern_item.keys.first
pattern_name_sym = pattern_name.downcase.to_sym
# Convert the number of repeats from a String such as "x4" into an integer such as 4.
multiples_str = pattern_item[pattern_name]
multiples_str.slice!(0)
multiples = multiples_str.to_i
unless multiples_str.match(/[^0-9]/) == nil
raise SongParseError,
"'#{multiples_str}' is an invalid number of repeats for pattern '#{pattern_name}'. Number of repeats should be a whole number."
else
if multiples < 0
raise SongParseError, "'#{multiples_str}' is an invalid number of repeats for pattern '#{pattern_name}'. Must be 0 or greater."
elsif multiples > 0 && !song.patterns.has_key?(pattern_name_sym)
# This test is purposefully designed to only throw an error if the number of repeats is greater
# than 0. This allows you to specify an undefined pattern in the flow with "x0" repeats.
# This can be convenient for defining the flow before all patterns have been added to the song file.
raise SongParseError, "Song flow includes non-existent pattern: #{pattern_name}."
end
end
multiples.times { flow << pattern_name_sym }
}
song.flow = flow
end
# Converts all hash keys to be lowercase
def downcase_hash_keys(hash)
return hash.inject({}) do |new_hash, pair|
new_hash[pair.first.downcase] = pair.last
new_hash
end
end
end