beats 1.2.0 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. data/LICENSE +1 -1
  2. data/README.markdown +28 -10
  3. data/bin/beats +9 -7
  4. data/lib/audioengine.rb +172 -0
  5. data/lib/audioutils.rb +73 -0
  6. data/lib/beats.rb +14 -15
  7. data/lib/beatswavefile.rb +17 -37
  8. data/lib/kit.rb +148 -71
  9. data/lib/pattern.rb +20 -117
  10. data/lib/patternexpander.rb +111 -0
  11. data/lib/song.rb +78 -132
  12. data/lib/songoptimizer.rb +29 -33
  13. data/lib/songparser.rb +70 -45
  14. data/lib/track.rb +11 -82
  15. data/test/audioengine_test.rb +261 -0
  16. data/test/audioutils_test.rb +45 -0
  17. data/test/fixtures/expected_output/example_split_mono_16-hh_closed.wav +0 -0
  18. data/test/{examples/split-agogo_high.wav → fixtures/expected_output/example_split_mono_16-hh_closed2.wav} +0 -0
  19. data/test/fixtures/expected_output/example_split_mono_8-hh_closed.wav +0 -0
  20. data/test/{examples/split-tom4.wav → fixtures/expected_output/example_split_mono_8-hh_closed2.wav} +0 -0
  21. data/test/fixtures/expected_output/example_split_stereo_16-hh_closed.wav +0 -0
  22. data/test/fixtures/expected_output/example_split_stereo_16-hh_closed2.wav +0 -0
  23. data/test/fixtures/expected_output/example_split_stereo_8-hh_closed.wav +0 -0
  24. data/test/fixtures/expected_output/example_split_stereo_8-hh_closed2.wav +0 -0
  25. data/test/fixtures/invalid/{bad_structure.txt → bad_flow.txt} +2 -2
  26. data/test/fixtures/invalid/bad_repeat_count.txt +1 -1
  27. data/test/fixtures/invalid/bad_rhythm.txt +1 -1
  28. data/test/fixtures/invalid/bad_tempo.txt +1 -1
  29. data/test/fixtures/invalid/{no_structure.txt → no_flow.txt} +1 -1
  30. data/test/fixtures/invalid/pattern_with_no_tracks.txt +1 -1
  31. data/test/fixtures/invalid/sound_in_kit_not_found.txt +1 -1
  32. data/test/fixtures/invalid/sound_in_kit_wrong_format.txt +10 -0
  33. data/test/fixtures/invalid/sound_in_track_not_found.txt +1 -1
  34. data/test/fixtures/invalid/sound_in_track_wrong_format.txt +8 -0
  35. data/test/fixtures/invalid/template.txt +1 -1
  36. data/test/fixtures/valid/example_mono_16.txt +5 -3
  37. data/test/fixtures/valid/example_mono_8.txt +5 -3
  38. data/test/fixtures/valid/example_no_kit.txt +1 -1
  39. data/test/fixtures/valid/example_stereo_16.txt +7 -4
  40. data/test/fixtures/valid/example_stereo_8.txt +5 -3
  41. data/test/fixtures/valid/example_with_empty_track.txt +1 -1
  42. data/test/fixtures/valid/example_with_kit.txt +1 -1
  43. data/test/fixtures/valid/multiple_tracks_same_sound.txt +33 -0
  44. data/test/fixtures/valid/no_tempo.txt +1 -1
  45. data/test/fixtures/valid/optimize_pattern_collision.txt +28 -0
  46. data/test/fixtures/valid/pattern_with_overflow.txt +1 -1
  47. data/test/fixtures/valid/repeats_not_specified.txt +2 -2
  48. data/test/fixtures/valid/with_structure.txt +10 -0
  49. data/test/fixtures/yaml/song_yaml.txt +5 -5
  50. data/test/includes.rb +4 -2
  51. data/test/integration.rb +3 -3
  52. data/test/kit_test.rb +136 -109
  53. data/test/pattern_test.rb +31 -131
  54. data/test/patternexpander_test.rb +142 -0
  55. data/test/song_test.rb +104 -102
  56. data/test/songoptimizer_test.rb +52 -38
  57. data/test/songparser_test.rb +79 -46
  58. data/test/sounds/composite_snare_mono_8_tom3_mono_16_mono_16.wav +0 -0
  59. data/test/sounds/composite_snare_mono_8_tom3_mono_8_mono_16.wav +0 -0
  60. data/test/sounds/composite_snare_stereo_16_tom3_mono_16_stereo_16.wav +0 -0
  61. data/test/sounds/composite_snare_stereo_8_tom3_mono_16_stereo_16.wav +0 -0
  62. data/test/track_test.rb +30 -185
  63. metadata +56 -24
  64. data/lib/songsplitter.rb +0 -38
  65. data/test/examples/combined.wav +0 -0
  66. data/test/examples/split-bass.wav +0 -0
  67. data/test/examples/split-hh_closed.wav +0 -0
  68. data/test/examples/split-snare.wav +0 -0
  69. data/test/examples/split-tom2.wav +0 -0
@@ -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(base_path)
6
+ def initialize()
10
7
  self.tempo = DEFAULT_TEMPO
11
- @kit = Kit.new(base_path)
12
8
  @patterns = {}
13
- @structure = []
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
- def write_to_file(output_file_name)
66
- cache = {}
67
- pack_code = pack_code()
68
- num_tracks_in_song = self.total_tracks()
69
- sample_length = sample_length_with_overflow()
70
-
71
- wave_file = BeatsWaveFile.new(@kit.num_channels, SAMPLE_RATE, @kit.bits_per_sample)
72
- file = wave_file.open_for_appending(output_file_name, sample_length)
73
-
74
- incoming_overflow = {}
75
- @structure.each do |pattern_name|
76
- key = [pattern_name, incoming_overflow.hash]
77
- unless cache.member?(key)
78
- sample_data = @patterns[pattern_name].sample_data(@tick_sample_length,
79
- @kit.num_channels,
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 structure.
123
- def copy_ignoring_patterns_and_structure
124
- copy = Song.new(@kit.base_path)
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
- # Removes any patterns that aren't referenced in the structure.
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, because for some reason select() returns an Array not a Hash.
134
- @patterns = @patterns.reject {|k, pattern| !@structure.member?(pattern.name) }
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 += structure_to_yaml()
148
- yaml_output += @kit.to_yaml(2)
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 :tick_sample_length, :patterns
155
- attr_accessor :structure, :kit
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| (name.to_s.length > max_length) ? name.to_s.length : max_length }
138
+ return arr.inject(0) {|max_length, name| [name.to_s.length, max_length].max }
171
139
  end
172
140
 
173
- def structure_to_yaml
174
- yaml_output = " Structure:\n"
175
- ljust_amount = longest_length_in_array(@structure) + 1 # The +1 is for the trailing ":"
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
- @structure.each do |pattern_name|
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
@@ -1,14 +1,13 @@
1
- # This class is used to transform a Song object into an equivalent Song object that
2
- # will be generated faster by the sound engine.
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) into one canonical Pattern. This allows for better caching, by
10
- # preventing the sound engine from generating sample data for a Pattern when 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.copy_ignoring_patterns_and_structure()
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
- # 2.) For each pattern, add a new pattern to new song every max_pattern_length ticks
63
- optimized_structure = {}
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
- tick_index = 0
66
- optimized_structure[pattern.name] = []
64
+ step_index = 0
65
+ optimized_flow[pattern.name] = []
67
66
 
68
- while(pattern.tracks.values.first.rhythm[tick_index] != nil) do
69
- new_pattern = optimized_song.pattern("#{pattern.name}#{tick_index}".to_sym)
70
- optimized_structure[pattern.name] << new_pattern.name
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[tick_index...(tick_index + max_pattern_length)]
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, track.wave_data, sub_track_pattern)
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 ticks, and no sound will be generated,
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
- # Track.sample_data() examines its sound's sample data to determine if it is
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
- tick_index += max_pattern_length
87
+ step_index += max_pattern_length
92
88
  end
93
89
  end
94
90
 
95
- # 3.) Replace the Song's structure to reference the new sub-divided patterns
91
+ # Replace the Song's flow to reference the new sub-divided patterns
96
92
  # instead of the old patterns.
97
- optimized_structure = original_song.structure.map do |original_pattern|
98
- optimized_structure[original_pattern]
93
+ optimized_flow = original_song.flow.map do |original_pattern|
94
+ optimized_flow[original_pattern]
99
95
  end
100
- optimized_song.structure = optimized_structure.flatten
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 consistent pattern replacement. Makes tests easier to write.
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 structure to remove references to duplicates
140
- new_structure = song.structure
135
+ # Update flow to remove references to duplicates
136
+ new_flow = song.flow
141
137
  replacements.each do |duplicate, replacement|
142
- new_structure = new_structure.map do |pattern_name|
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.structure = new_structure
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
@@ -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
- Structure:
18
+ Flow:
10
19
  - Verse: x2
11
20
  - Chorus: x2"
12
21
 
13
22
  def initialize
14
23
  end
15
24
 
16
- def parse(base_path, definition = nil)
17
- raw_song_definition = canonicalize_definition(definition)
18
- raw_song_components = split_raw_yaml_into_components(raw_song_definition)
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(base_path)
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 SoundNotFoundError => detail
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 structure
43
- if raw_song_components[:structure] == nil
44
- raise SongParseError, "Song must have a Structure section in the header."
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
- set_song_structure(song, raw_song_components[:structure])
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
- # Is "canonicalize" a word?
55
- def canonicalize_definition(definition)
56
- if definition.class == String
57
- begin
58
- raw_song_definition = YAML.load(definition)
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
- raw_song_components[:structure] = raw_song_components[:header]["structure"]
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
- kit = Kit.new(base_path)
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
- kit.add(kit_item.keys.first, kit_item.values.first)
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
- kit.add(track_name, track_path)
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 pattern is specified (i.e. "- foo.wav:" instead of "- foo.wav: X.X.X.X.")
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, song.kit.get_sample_data(track_name), track_definition[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 set_song_structure(song, raw_structure)
141
- structure = []
164
+ def set_song_flow(song, raw_flow)
165
+ flow = []
142
166
 
143
- raw_structure.each{|pattern_item|
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, "'#{multiples_str}' is an invalid number of repeats for pattern '#{pattern_name}'. Number of repeats should be a whole number."
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 structure with "x0" repeats.
164
- # This can be convenient for defining the structure before all patterns have been added to the song file.
165
- raise SongParseError, "Song structure includes non-existent pattern: #{pattern_name}."
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 { structure << pattern_name_sym }
194
+ multiples.times { flow << pattern_name_sym }
170
195
  }
171
- song.structure = structure
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