beats 1.2.0 → 1.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +1 -1
- data/README.markdown +28 -10
- data/bin/beats +9 -7
- data/lib/audioengine.rb +172 -0
- data/lib/audioutils.rb +73 -0
- data/lib/beats.rb +14 -15
- data/lib/beatswavefile.rb +17 -37
- data/lib/kit.rb +148 -71
- data/lib/pattern.rb +20 -117
- data/lib/patternexpander.rb +111 -0
- data/lib/song.rb +78 -132
- data/lib/songoptimizer.rb +29 -33
- data/lib/songparser.rb +70 -45
- data/lib/track.rb +11 -82
- data/test/audioengine_test.rb +261 -0
- data/test/audioutils_test.rb +45 -0
- data/test/fixtures/expected_output/example_split_mono_16-hh_closed.wav +0 -0
- data/test/{examples/split-agogo_high.wav → fixtures/expected_output/example_split_mono_16-hh_closed2.wav} +0 -0
- data/test/fixtures/expected_output/example_split_mono_8-hh_closed.wav +0 -0
- data/test/{examples/split-tom4.wav → fixtures/expected_output/example_split_mono_8-hh_closed2.wav} +0 -0
- data/test/fixtures/expected_output/example_split_stereo_16-hh_closed.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_16-hh_closed2.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_8-hh_closed.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_8-hh_closed2.wav +0 -0
- data/test/fixtures/invalid/{bad_structure.txt → bad_flow.txt} +2 -2
- data/test/fixtures/invalid/bad_repeat_count.txt +1 -1
- data/test/fixtures/invalid/bad_rhythm.txt +1 -1
- data/test/fixtures/invalid/bad_tempo.txt +1 -1
- data/test/fixtures/invalid/{no_structure.txt → no_flow.txt} +1 -1
- data/test/fixtures/invalid/pattern_with_no_tracks.txt +1 -1
- data/test/fixtures/invalid/sound_in_kit_not_found.txt +1 -1
- data/test/fixtures/invalid/sound_in_kit_wrong_format.txt +10 -0
- data/test/fixtures/invalid/sound_in_track_not_found.txt +1 -1
- data/test/fixtures/invalid/sound_in_track_wrong_format.txt +8 -0
- data/test/fixtures/invalid/template.txt +1 -1
- data/test/fixtures/valid/example_mono_16.txt +5 -3
- data/test/fixtures/valid/example_mono_8.txt +5 -3
- data/test/fixtures/valid/example_no_kit.txt +1 -1
- data/test/fixtures/valid/example_stereo_16.txt +7 -4
- data/test/fixtures/valid/example_stereo_8.txt +5 -3
- data/test/fixtures/valid/example_with_empty_track.txt +1 -1
- data/test/fixtures/valid/example_with_kit.txt +1 -1
- data/test/fixtures/valid/multiple_tracks_same_sound.txt +33 -0
- data/test/fixtures/valid/no_tempo.txt +1 -1
- data/test/fixtures/valid/optimize_pattern_collision.txt +28 -0
- data/test/fixtures/valid/pattern_with_overflow.txt +1 -1
- data/test/fixtures/valid/repeats_not_specified.txt +2 -2
- data/test/fixtures/valid/with_structure.txt +10 -0
- data/test/fixtures/yaml/song_yaml.txt +5 -5
- data/test/includes.rb +4 -2
- data/test/integration.rb +3 -3
- data/test/kit_test.rb +136 -109
- data/test/pattern_test.rb +31 -131
- data/test/patternexpander_test.rb +142 -0
- data/test/song_test.rb +104 -102
- data/test/songoptimizer_test.rb +52 -38
- data/test/songparser_test.rb +79 -46
- data/test/sounds/composite_snare_mono_8_tom3_mono_16_mono_16.wav +0 -0
- data/test/sounds/composite_snare_mono_8_tom3_mono_8_mono_16.wav +0 -0
- data/test/sounds/composite_snare_stereo_16_tom3_mono_16_stereo_16.wav +0 -0
- data/test/sounds/composite_snare_stereo_8_tom3_mono_16_stereo_16.wav +0 -0
- data/test/track_test.rb +30 -185
- metadata +56 -24
- data/lib/songsplitter.rb +0 -38
- data/test/examples/combined.wav +0 -0
- data/test/examples/split-bass.wav +0 -0
- data/test/examples/split-hh_closed.wav +0 -0
- data/test/examples/split-snare.wav +0 -0
- data/test/examples/split-tom2.wav +0 -0
data/lib/song.rb
CHANGED
@@ -1,16 +1,12 @@
|
|
1
1
|
class InvalidTempoError < RuntimeError; end
|
2
2
|
|
3
3
|
class Song
|
4
|
-
SAMPLE_RATE = 44100
|
5
|
-
SECONDS_PER_MINUTE = 60.0
|
6
|
-
SAMPLES_PER_MINUTE = SAMPLE_RATE * SECONDS_PER_MINUTE
|
7
4
|
DEFAULT_TEMPO = 120
|
8
5
|
|
9
|
-
def initialize(
|
6
|
+
def initialize()
|
10
7
|
self.tempo = DEFAULT_TEMPO
|
11
|
-
@kit = Kit.new(base_path)
|
12
8
|
@patterns = {}
|
13
|
-
@
|
9
|
+
@flow = []
|
14
10
|
end
|
15
11
|
|
16
12
|
# Adds a new pattern to the song, with the specified name.
|
@@ -19,30 +15,6 @@ class Song
|
|
19
15
|
return @patterns[name]
|
20
16
|
end
|
21
17
|
|
22
|
-
# Returns the number of samples required for the entire song at the current tempo.
|
23
|
-
# (Assumes a sample rate of 44100). Does NOT include samples required for sound
|
24
|
-
# overflow from the last pattern.
|
25
|
-
def sample_length
|
26
|
-
@structure.inject(0) do |sum, pattern_name|
|
27
|
-
sum + @patterns[pattern_name].sample_length(@tick_sample_length)
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
# Returns the number of samples required for the entire song at the current tempo.
|
32
|
-
# (Assumes a sample rate of 44100). Includes samples required for sound overflow
|
33
|
-
# from the last pattern.
|
34
|
-
def sample_length_with_overflow
|
35
|
-
if @structure.length == 0
|
36
|
-
return 0
|
37
|
-
end
|
38
|
-
|
39
|
-
full_sample_length = self.sample_length
|
40
|
-
last_pattern_sample_length = @patterns[@structure.last].sample_length(@tick_sample_length)
|
41
|
-
last_pattern_overflow_length = @patterns[@structure.last].sample_length_with_overflow(@tick_sample_length)
|
42
|
-
overflow = last_pattern_overflow_length - last_pattern_sample_length
|
43
|
-
|
44
|
-
return sample_length + overflow
|
45
|
-
end
|
46
18
|
|
47
19
|
# The number of tracks that the pattern with the greatest number of tracks has.
|
48
20
|
# TODO: Is it a problem that an optimized song can have a different total_tracks() value than
|
@@ -52,58 +24,22 @@ class Song
|
|
52
24
|
def total_tracks
|
53
25
|
@patterns.keys.collect {|pattern_name| @patterns[pattern_name].tracks.length }.max || 0
|
54
26
|
end
|
55
|
-
|
56
|
-
def track_names
|
57
|
-
track_names = {}
|
58
|
-
@patterns.values.each do |pattern|
|
59
|
-
pattern.tracks.values.each {|track| track_names[track.name] = nil}
|
60
|
-
end
|
61
|
-
|
62
|
-
return track_names.keys.sort
|
63
|
-
end
|
64
27
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
num_tracks_in_song,
|
81
|
-
incoming_overflow)
|
82
|
-
|
83
|
-
if @kit.num_channels == 1
|
84
|
-
# Don't flatten the sample data Array, since it is already flattened. That would be a waste of time, yo.
|
85
|
-
cache[key] = {:primary => sample_data[:primary].pack(pack_code), :overflow => sample_data[:overflow]}
|
86
|
-
else
|
87
|
-
cache[key] = {:primary => sample_data[:primary].flatten.pack(pack_code), :overflow => sample_data[:overflow]}
|
88
|
-
end
|
89
|
-
end
|
90
|
-
|
91
|
-
file.syswrite(cache[key][:primary])
|
92
|
-
incoming_overflow = cache[key][:overflow]
|
93
|
-
end
|
94
|
-
|
95
|
-
wave_file.write_snippet(file, merge_overflow(incoming_overflow, num_tracks_in_song))
|
96
|
-
file.close()
|
97
|
-
|
98
|
-
return wave_file.calculate_duration(SAMPLE_RATE, sample_length)
|
99
|
-
end
|
100
|
-
|
101
|
-
def num_channels
|
102
|
-
return @kit.num_channels
|
103
|
-
end
|
104
|
-
|
105
|
-
def bits_per_sample
|
106
|
-
return @kit.bits_per_sample
|
28
|
+
# The unique track names used in each of the song's patterns. Sorted in alphabetical order.
|
29
|
+
# For example calling this method for this song:
|
30
|
+
#
|
31
|
+
# Verse:
|
32
|
+
# - bass: X...
|
33
|
+
# - snare: ..X.
|
34
|
+
#
|
35
|
+
# Chorus:
|
36
|
+
# - bass: X.X.
|
37
|
+
# - snare: X.X.
|
38
|
+
# - hihat: XXXX
|
39
|
+
#
|
40
|
+
# Will return: ["bass", "hihat", "snare"]
|
41
|
+
def track_names
|
42
|
+
@patterns.values.inject([]) {|track_names, pattern| track_names | pattern.tracks.keys }.sort
|
107
43
|
end
|
108
44
|
|
109
45
|
def tempo
|
@@ -116,66 +52,98 @@ class Song
|
|
116
52
|
end
|
117
53
|
|
118
54
|
@tempo = new_tempo
|
119
|
-
@tick_sample_length = SAMPLES_PER_MINUTE / new_tempo / 4.0
|
120
55
|
end
|
121
56
|
|
122
|
-
# Returns a new Song that is identical but with no patterns or
|
123
|
-
def
|
124
|
-
copy = Song.new(
|
57
|
+
# Returns a new Song that is identical but with no patterns or flow.
|
58
|
+
def copy_ignoring_patterns_and_flow
|
59
|
+
copy = Song.new()
|
125
60
|
copy.tempo = @tempo
|
126
|
-
copy.kit = @kit
|
127
61
|
|
128
62
|
return copy
|
129
63
|
end
|
64
|
+
|
65
|
+
# Changes the song flow to consist of playing the specified pattern once. All other patterns will
|
66
|
+
# be removed from the song as a side effect.
|
67
|
+
#
|
68
|
+
# pattern_to_keep - The Symbol name of the pattern to preserve.
|
69
|
+
#
|
70
|
+
# Returns nothing.
|
71
|
+
def remove_patterns_except(pattern_to_keep)
|
72
|
+
unless @patterns.has_key?(pattern_to_keep)
|
73
|
+
raise StandardError, "The song does not include a pattern called #{pattern_to_keep}"
|
74
|
+
end
|
75
|
+
|
76
|
+
@flow = [pattern_to_keep]
|
77
|
+
remove_unused_patterns()
|
78
|
+
end
|
130
79
|
|
131
|
-
#
|
80
|
+
# Splits a Song object into multiple Song objects, where each new
|
81
|
+
# Song only has 1 track. For example, if a Song has 5 tracks, this will return
|
82
|
+
# a hash of 5 songs, each with one of the original Song's tracks.
|
83
|
+
def split()
|
84
|
+
split_songs = {}
|
85
|
+
track_names = track_names()
|
86
|
+
|
87
|
+
track_names.each do |track_name|
|
88
|
+
new_song = copy_ignoring_patterns_and_flow()
|
89
|
+
|
90
|
+
@patterns.each do |name, original_pattern|
|
91
|
+
new_pattern = new_song.pattern(name)
|
92
|
+
|
93
|
+
if original_pattern.tracks.has_key?(track_name)
|
94
|
+
original_track = original_pattern.tracks[track_name]
|
95
|
+
new_pattern.track(original_track.name, original_track.rhythm)
|
96
|
+
else
|
97
|
+
new_pattern.track(track_name, "." * original_pattern.step_count)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
new_song.flow = @flow
|
102
|
+
|
103
|
+
split_songs[track_name] = new_song
|
104
|
+
end
|
105
|
+
|
106
|
+
return split_songs
|
107
|
+
end
|
108
|
+
|
109
|
+
# Removes any patterns that aren't referenced in the flow.
|
132
110
|
def remove_unused_patterns
|
133
|
-
# Using reject() here
|
134
|
-
@patterns
|
111
|
+
# Using reject() here because for some reason select() returns an Array not a Hash.
|
112
|
+
@patterns.reject! {|k, pattern| !@flow.member?(pattern.name) }
|
135
113
|
end
|
136
114
|
|
137
115
|
# Serializes the current Song to a YAML string. This string can then be used to construct a new Song
|
138
116
|
# using the SongParser class. This lets you save a Song to disk, to be re-loaded later. Produces nicer
|
139
117
|
# looking output than the default version of to_yaml().
|
140
|
-
def to_yaml
|
118
|
+
def to_yaml(kit)
|
141
119
|
# This implementation intentionally builds up a YAML string manually instead of using YAML::dump().
|
142
120
|
# Ruby 1.8 makes it difficult to ensure a consistent ordering of hash keys, which makes the output ugly
|
143
121
|
# and also hard to test.
|
144
122
|
|
145
123
|
yaml_output = "Song:\n"
|
146
124
|
yaml_output += " Tempo: #{@tempo}\n"
|
147
|
-
yaml_output +=
|
148
|
-
yaml_output +=
|
125
|
+
yaml_output += flow_to_yaml()
|
126
|
+
yaml_output += kit.to_yaml(2)
|
149
127
|
yaml_output += patterns_to_yaml()
|
150
128
|
|
151
129
|
return yaml_output
|
152
130
|
end
|
153
131
|
|
154
|
-
attr_reader :
|
155
|
-
attr_accessor :
|
132
|
+
attr_reader :patterns
|
133
|
+
attr_accessor :flow
|
156
134
|
|
157
135
|
private
|
158
136
|
|
159
|
-
def pack_code
|
160
|
-
if @kit.bits_per_sample == 8
|
161
|
-
return "C*"
|
162
|
-
elsif @kit.bits_per_sample == 16
|
163
|
-
return "s*"
|
164
|
-
else
|
165
|
-
raise StandardError, "Invalid bits per sample of #{@kit.bits_per_sample}"
|
166
|
-
end
|
167
|
-
end
|
168
|
-
|
169
137
|
def longest_length_in_array(arr)
|
170
|
-
return arr.inject(0) {|max_length, name|
|
138
|
+
return arr.inject(0) {|max_length, name| [name.to_s.length, max_length].max }
|
171
139
|
end
|
172
140
|
|
173
|
-
def
|
174
|
-
yaml_output = "
|
175
|
-
ljust_amount = longest_length_in_array(@
|
141
|
+
def flow_to_yaml
|
142
|
+
yaml_output = " Flow:\n"
|
143
|
+
ljust_amount = longest_length_in_array(@flow) + 1 # The +1 is for the trailing ":"
|
176
144
|
previous = nil
|
177
145
|
count = 0
|
178
|
-
@
|
146
|
+
@flow.each do |pattern_name|
|
179
147
|
if pattern_name == previous || previous == nil
|
180
148
|
count += 1
|
181
149
|
else
|
@@ -200,26 +168,4 @@ private
|
|
200
168
|
|
201
169
|
return yaml_output
|
202
170
|
end
|
203
|
-
|
204
|
-
def merge_overflow(overflow, num_tracks_in_song)
|
205
|
-
merged_sample_data = []
|
206
|
-
|
207
|
-
unless overflow == {}
|
208
|
-
longest_overflow = overflow[overflow.keys.first]
|
209
|
-
overflow.keys.each do |track_name|
|
210
|
-
if overflow[track_name].length > longest_overflow.length
|
211
|
-
longest_overflow = overflow[track_name]
|
212
|
-
end
|
213
|
-
end
|
214
|
-
|
215
|
-
# TODO: What happens if final overflow is really long, and extends past single '.' rhythm?
|
216
|
-
final_overflow_pattern = Pattern.new(:overflow)
|
217
|
-
wave_data = @kit.num_channels == 1 ? [] : [[]]
|
218
|
-
final_overflow_pattern.track "", wave_data, "."
|
219
|
-
final_overflow_sample_data = final_overflow_pattern.sample_data(longest_overflow.length, @kit.num_channels, num_tracks_in_song, overflow)
|
220
|
-
merged_sample_data = final_overflow_sample_data[:primary]
|
221
|
-
end
|
222
|
-
|
223
|
-
return merged_sample_data
|
224
|
-
end
|
225
|
-
end
|
171
|
+
end
|
data/lib/songoptimizer.rb
CHANGED
@@ -1,14 +1,13 @@
|
|
1
|
-
# This class is used to transform a Song object into an equivalent Song object
|
2
|
-
# will be generated faster by the
|
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
3
|
#
|
4
4
|
# The primary method is optimize(). Currently, it performs two optimizations:
|
5
5
|
#
|
6
6
|
# 1.) Breaks patterns into shorter patterns. Generating one long Pattern is generally
|
7
7
|
# slower than generating several short Patterns with the same combined length.
|
8
8
|
# 2.) Replaces Patterns which are equivalent (i.e. they have the same tracks with the
|
9
|
-
# same rhythms)
|
10
|
-
# preventing the
|
11
|
-
# same sample data has already been generated for a different Pattern.
|
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.
|
12
11
|
#
|
13
12
|
# Note that step #1 actually performs double duty, because breaking Patterns into smaller
|
14
13
|
# pieces increases the likelihood there will be duplicates that can be combined.
|
@@ -20,7 +19,7 @@ class SongOptimizer
|
|
20
19
|
# generated faster.
|
21
20
|
def optimize(original_song, max_pattern_length)
|
22
21
|
# 1.) Create a new song, cloned from the original
|
23
|
-
optimized_song = original_song.
|
22
|
+
optimized_song = original_song.copy_ignoring_patterns_and_flow()
|
24
23
|
|
25
24
|
# 2.) Subdivide patterns
|
26
25
|
optimized_song = subdivide_song_patterns(original_song, optimized_song, max_pattern_length)
|
@@ -59,45 +58,42 @@ protected
|
|
59
58
|
def subdivide_song_patterns(original_song, optimized_song, max_pattern_length)
|
60
59
|
blank_track_pattern = '.' * max_pattern_length
|
61
60
|
|
62
|
-
#
|
63
|
-
|
61
|
+
# For each pattern, add a new pattern to new song every max_pattern_length steps
|
62
|
+
optimized_flow = {}
|
64
63
|
original_song.patterns.values.each do |pattern|
|
65
|
-
|
66
|
-
|
64
|
+
step_index = 0
|
65
|
+
optimized_flow[pattern.name] = []
|
67
66
|
|
68
|
-
while(pattern.tracks.values.first.rhythm[
|
69
|
-
|
70
|
-
|
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
|
71
72
|
pattern.tracks.values.each do |track|
|
72
|
-
sub_track_pattern = track.rhythm[
|
73
|
+
sub_track_pattern = track.rhythm[step_index...(step_index + max_pattern_length)]
|
73
74
|
|
74
75
|
if sub_track_pattern != blank_track_pattern
|
75
|
-
new_pattern.track(track.name,
|
76
|
+
new_pattern.track(track.name, sub_track_pattern)
|
76
77
|
end
|
77
78
|
end
|
78
79
|
|
79
80
|
# If no track has a trigger during this step pattern, add a blank track.
|
80
|
-
# Otherwise, this pattern will have no
|
81
|
+
# Otherwise, this pattern will have no steps, and no sound will be generated,
|
81
82
|
# causing the pattern to be "compacted away".
|
82
83
|
if new_pattern.tracks.empty?
|
83
|
-
|
84
|
-
# mono or stereo. If the first item in the sample data Array is an Array,
|
85
|
-
# it decides stereo. That's what the [] vs. [[]] is about.
|
86
|
-
placeholder_wave_data = (optimized_song.kit.num_channels == 1) ? [] : [[]]
|
87
|
-
|
88
|
-
new_pattern.track("placeholder", placeholder_wave_data, blank_track_pattern)
|
84
|
+
new_pattern.track("placeholder", blank_track_pattern)
|
89
85
|
end
|
90
86
|
|
91
|
-
|
87
|
+
step_index += max_pattern_length
|
92
88
|
end
|
93
89
|
end
|
94
90
|
|
95
|
-
#
|
91
|
+
# Replace the Song's flow to reference the new sub-divided patterns
|
96
92
|
# instead of the old patterns.
|
97
|
-
|
98
|
-
|
93
|
+
optimized_flow = original_song.flow.map do |original_pattern|
|
94
|
+
optimized_flow[original_pattern]
|
99
95
|
end
|
100
|
-
optimized_song.
|
96
|
+
optimized_song.flow = optimized_flow.flatten
|
101
97
|
|
102
98
|
return optimized_song
|
103
99
|
end
|
@@ -116,7 +112,7 @@ protected
|
|
116
112
|
seen_patterns = []
|
117
113
|
replacements = {}
|
118
114
|
|
119
|
-
# Pattern names are sorted to ensure
|
115
|
+
# Pattern names are sorted to ensure predictable pattern replacement. Makes tests easier to write.
|
120
116
|
# Sort function added manually because Ruby 1.8 doesn't know how to sort symbols...
|
121
117
|
pattern_names = song.patterns.keys.sort {|x, y| x.to_s <=> y.to_s }
|
122
118
|
|
@@ -136,14 +132,14 @@ protected
|
|
136
132
|
end
|
137
133
|
end
|
138
134
|
|
139
|
-
# Update
|
140
|
-
|
135
|
+
# Update flow to remove references to duplicates
|
136
|
+
new_flow = song.flow
|
141
137
|
replacements.each do |duplicate, replacement|
|
142
|
-
|
138
|
+
new_flow = new_flow.map do |pattern_name|
|
143
139
|
(pattern_name == duplicate) ? replacement : pattern_name
|
144
140
|
end
|
145
141
|
end
|
146
|
-
song.
|
142
|
+
song.flow = new_flow
|
147
143
|
|
148
144
|
# Remove unused Patterns. Not strictly necessary, but makes resulting songs
|
149
145
|
# easier to read for debugging purposes.
|
@@ -151,4 +147,4 @@ protected
|
|
151
147
|
|
152
148
|
return song
|
153
149
|
end
|
154
|
-
end
|
150
|
+
end
|
data/lib/songparser.rb
CHANGED
@@ -1,23 +1,37 @@
|
|
1
1
|
class SongParseError < RuntimeError; end
|
2
2
|
|
3
|
+
# This class is used to parse a raw YAML song definition into domain objects. These
|
4
|
+
# domain objects can then be used by AudioEngine to generate the output sample data
|
5
|
+
# for the song.
|
3
6
|
class SongParser
|
7
|
+
DONT_USE_STRUCTURE_WARNING =
|
8
|
+
"\n" +
|
9
|
+
"WARNING! This song contains a 'Structure' section in the header.\n" +
|
10
|
+
"As of BEATS 1.2.1, the 'Structure' section should be renamed 'Flow'.\n" +
|
11
|
+
"You should change your song file, in a future version using 'Structure' will cause an error.\n"
|
12
|
+
|
4
13
|
NO_SONG_HEADER_ERROR_MSG =
|
5
14
|
"Song must have a header. Here's an example:
|
6
15
|
|
7
16
|
Song:
|
8
17
|
Tempo: 120
|
9
|
-
|
18
|
+
Flow:
|
10
19
|
- Verse: x2
|
11
20
|
- Chorus: x2"
|
12
21
|
|
13
22
|
def initialize
|
14
23
|
end
|
15
24
|
|
16
|
-
|
17
|
-
|
18
|
-
raw_song_components =
|
25
|
+
# Parses a raw YAML song definition and converts it into a Song and Kit object.
|
26
|
+
def parse(base_path, raw_yaml_string)
|
27
|
+
raw_song_components = hashify_raw_yaml(raw_yaml_string)
|
28
|
+
|
29
|
+
# This will be coming in a future version...
|
30
|
+
#unless raw_song_components[:folder] == nil
|
31
|
+
# base_path = raw_song_components[:folder]
|
32
|
+
#end
|
19
33
|
|
20
|
-
song = Song.new(
|
34
|
+
song = Song.new()
|
21
35
|
|
22
36
|
# 1.) Set tempo
|
23
37
|
begin
|
@@ -31,45 +45,36 @@ class SongParser
|
|
31
45
|
# 2.) Build the kit
|
32
46
|
begin
|
33
47
|
kit = build_kit(base_path, raw_song_components[:kit], raw_song_components[:patterns])
|
34
|
-
rescue
|
48
|
+
rescue SoundFileNotFoundError => detail
|
49
|
+
raise SongParseError, "#{detail}"
|
50
|
+
rescue InvalidSoundFormatError => detail
|
35
51
|
raise SongParseError, "#{detail}"
|
36
52
|
end
|
37
|
-
song.kit = kit
|
38
53
|
|
39
54
|
# 3.) Load patterns
|
40
55
|
add_patterns_to_song(song, raw_song_components[:patterns])
|
41
56
|
|
42
|
-
# 4.) Set
|
43
|
-
if raw_song_components[:
|
44
|
-
raise SongParseError, "Song must have a
|
57
|
+
# 4.) Set flow
|
58
|
+
if raw_song_components[:flow] == nil
|
59
|
+
raise SongParseError, "Song must have a Flow section in the header."
|
45
60
|
else
|
46
|
-
|
61
|
+
set_song_flow(song, raw_song_components[:flow])
|
47
62
|
end
|
48
63
|
|
49
|
-
return song
|
64
|
+
return song, kit
|
50
65
|
end
|
51
66
|
|
52
67
|
private
|
53
68
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
rescue ArgumentError => detail
|
60
|
-
raise SongParseError, "Syntax error in YAML file"
|
61
|
-
end
|
62
|
-
elsif definition.class == Hash
|
63
|
-
raw_song_definition = definition
|
64
|
-
else
|
65
|
-
raise SongParseError, "Invalid song input"
|
69
|
+
def hashify_raw_yaml(raw_yaml_string)
|
70
|
+
begin
|
71
|
+
raw_song_definition = YAML.load(raw_yaml_string)
|
72
|
+
rescue ArgumentError => detail
|
73
|
+
raise SongParseError, "Syntax error in YAML file"
|
66
74
|
end
|
67
|
-
|
68
|
-
return raw_song_definition
|
69
|
-
end
|
70
75
|
|
71
|
-
def split_raw_yaml_into_components(raw_song_definition)
|
72
76
|
raw_song_components = {}
|
77
|
+
warnings = []
|
73
78
|
raw_song_components[:full_definition] = downcase_hash_keys(raw_song_definition)
|
74
79
|
|
75
80
|
if raw_song_components[:full_definition]["song"] != nil
|
@@ -78,20 +83,34 @@ private
|
|
78
83
|
raise SongParseError, NO_SONG_HEADER_ERROR_MSG
|
79
84
|
end
|
80
85
|
raw_song_components[:tempo] = raw_song_components[:header]["tempo"]
|
86
|
+
raw_song_components[:folder] = raw_song_components[:header]["folder"]
|
81
87
|
raw_song_components[:kit] = raw_song_components[:header]["kit"]
|
82
|
-
|
88
|
+
|
89
|
+
raw_flow = raw_song_components[:header]["flow"]
|
90
|
+
raw_structure = raw_song_components[:header]["structure"]
|
91
|
+
if raw_flow != nil
|
92
|
+
raw_song_components[:flow] = raw_flow
|
93
|
+
else
|
94
|
+
if raw_structure != nil
|
95
|
+
puts DONT_USE_STRUCTURE_WARNING
|
96
|
+
end
|
97
|
+
|
98
|
+
raw_song_components[:flow] = raw_structure
|
99
|
+
end
|
100
|
+
|
83
101
|
raw_song_components[:patterns] = raw_song_components[:full_definition].reject {|k, v| k == "song"}
|
84
|
-
|
102
|
+
|
85
103
|
return raw_song_components
|
86
104
|
end
|
87
105
|
|
88
106
|
def build_kit(base_path, raw_kit, raw_patterns)
|
89
|
-
|
107
|
+
kit_items = {}
|
90
108
|
|
91
109
|
# Add sounds defined in the Kit section of the song header
|
110
|
+
# TODO: Raise error is same name is defined more than once in the Kit
|
92
111
|
unless raw_kit == nil
|
93
112
|
raw_kit.each do |kit_item|
|
94
|
-
|
113
|
+
kit_items[kit_item.keys.first] = kit_item.values.first
|
95
114
|
end
|
96
115
|
end
|
97
116
|
|
@@ -107,11 +126,14 @@ private
|
|
107
126
|
track_name = track_definition.keys.first
|
108
127
|
track_path = track_name
|
109
128
|
|
110
|
-
|
129
|
+
if track_name != Pattern::FLOW_TRACK_NAME && kit_items[track_name] == nil
|
130
|
+
kit_items[track_name] = track_path
|
131
|
+
end
|
111
132
|
end
|
112
133
|
end
|
113
134
|
end
|
114
135
|
|
136
|
+
kit = Kit.new(base_path, kit_items)
|
115
137
|
return kit
|
116
138
|
end
|
117
139
|
|
@@ -120,27 +142,29 @@ private
|
|
120
142
|
new_pattern = song.pattern key.to_sym
|
121
143
|
|
122
144
|
track_list = raw_patterns[key]
|
145
|
+
# TODO Also raise error if only there is only 1 track and it's a flow track
|
123
146
|
if track_list == nil
|
124
147
|
# TODO: Use correct capitalization of pattern name in error message
|
125
148
|
# TODO: Possibly allow if pattern not referenced in the Structure, or has 0 repeats?
|
126
149
|
raise SongParseError, "Pattern '#{key}' has no tracks. It needs at least one."
|
127
150
|
end
|
128
151
|
|
152
|
+
# TODO: What if there is more than one flow? Raise error, or have last one win?
|
129
153
|
track_list.each do |track_definition|
|
130
154
|
track_name = track_definition.keys.first
|
131
155
|
|
132
|
-
# Handle case where no track
|
156
|
+
# Handle case where no track rhythm is specified (i.e. "- foo.wav:" instead of "- foo.wav: X.X.X.X.")
|
133
157
|
track_definition[track_name] ||= ""
|
134
158
|
|
135
|
-
new_pattern.track track_name,
|
159
|
+
new_pattern.track track_name, track_definition[track_name]
|
136
160
|
end
|
137
161
|
end
|
138
162
|
end
|
139
163
|
|
140
|
-
def
|
141
|
-
|
164
|
+
def set_song_flow(song, raw_flow)
|
165
|
+
flow = []
|
142
166
|
|
143
|
-
|
167
|
+
raw_flow.each{|pattern_item|
|
144
168
|
if pattern_item.class == String
|
145
169
|
pattern_item = {pattern_item => "x1"}
|
146
170
|
end
|
@@ -154,21 +178,22 @@ private
|
|
154
178
|
multiples = multiples_str.to_i
|
155
179
|
|
156
180
|
unless multiples_str.match(/[^0-9]/) == nil
|
157
|
-
raise SongParseError,
|
181
|
+
raise SongParseError,
|
182
|
+
"'#{multiples_str}' is an invalid number of repeats for pattern '#{pattern_name}'. Number of repeats should be a whole number."
|
158
183
|
else
|
159
184
|
if multiples < 0
|
160
185
|
raise SongParseError, "'#{multiples_str}' is an invalid number of repeats for pattern '#{pattern_name}'. Must be 0 or greater."
|
161
186
|
elsif multiples > 0 && !song.patterns.has_key?(pattern_name_sym)
|
162
187
|
# This test is purposefully designed to only throw an error if the number of repeats is greater
|
163
|
-
# than 0. This allows you to specify an undefined pattern in the
|
164
|
-
# This can be convenient for defining the
|
165
|
-
raise SongParseError, "Song
|
188
|
+
# than 0. This allows you to specify an undefined pattern in the flow with "x0" repeats.
|
189
|
+
# This can be convenient for defining the flow before all patterns have been added to the song file.
|
190
|
+
raise SongParseError, "Song flow includes non-existent pattern: #{pattern_name}."
|
166
191
|
end
|
167
192
|
end
|
168
193
|
|
169
|
-
multiples.times {
|
194
|
+
multiples.times { flow << pattern_name_sym }
|
170
195
|
}
|
171
|
-
song.
|
196
|
+
song.flow = flow
|
172
197
|
end
|
173
198
|
|
174
199
|
# Converts all hash keys to be lowercase
|
@@ -178,4 +203,4 @@ private
|
|
178
203
|
new_hash
|
179
204
|
end
|
180
205
|
end
|
181
|
-
end
|
206
|
+
end
|