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