beats 1.2.0 → 1.2.1
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.
- 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
|