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
data/lib/kit.rb CHANGED
@@ -1,93 +1,93 @@
1
1
  # Raised when trying to load a sound file which can't be found at the path specified
2
- class SoundNotFoundError < RuntimeError; end
2
+ class SoundFileNotFoundError < RuntimeError; end
3
3
 
4
- # This class keeps track of the sounds that are used in a song. It provides a
5
- # central place for storing sound data, and most usefully, handles converting
6
- # sounds in different formats to a standard format.
7
- #
8
- # For example, if a song requires a sound that is mono/8-bit, another that is
9
- # stereo/8-bit, and another that is stereo/16-bit, it can't mix them together
10
- # because they are in different formats. Kit however automatically handles the
11
- # details of converting them to a common format. If you add sounds to the kit
12
- # using add(), sounds that you get using get_sample_data() will be in a common
13
- # format.
4
+ # Raised when trying to load a sound file which either isn't actually a sound file, or
5
+ # is in an unsupported format.
6
+ class InvalidSoundFormatError < RuntimeError; end
7
+
8
+
9
+ # This class provides a repository for the sounds used in a song. Most usefully, it
10
+ # also handles converting the sounds to a common format. For example, if a song requires
11
+ # a sound that is mono/8-bit, another that is stereo/8-bit, and another that is
12
+ # stereo/16-bit, they have to be converted to a common format before they can be used
13
+ # together. Kit handles this conversion; all sounds retrieved using
14
+ # get_sample_data() will be in a common format.
15
+ #
16
+ # Sounds can only be added at initialization. During initialization, the sample data
17
+ # for each sound is loaded into memory, and converted to the common format if necessary.
18
+ # This format is:
19
+ #
20
+ # Bits per sample: 16
21
+ # Sample rate: 44100
22
+ # Channels: Stereo, unless all of the kit sounds are mono.
14
23
  #
15
- # All sounds returned by get_sample_data() will be 16-bit. All sounds will be
16
- # either mono or stereo; if at least one added sound is stereo then all sounds
17
- # will be stereo. So for example if a mono/8-bit, stereo/8-bit, and stereo/16-bit
18
- # sound are added, when you retrieve each one using get_sample_data() they will
19
- # be stereo/16-bit.
24
+ # For example if the kit has these sounds:
20
25
  #
21
- # Note that this means that each time a new sound is added to the Kit, the common
22
- # format might change, if the incoming sound has a greater number of channels than
23
- # any of the previously added sounds. Therefore, all of the sounds
24
- # used by a Song should be added to the Kit before generation begins. If you
25
- # create Song objects by using SongParser, this will be taken care of for you (as
26
- # long as you don't modify the Kit afterward).
26
+ # my_sound_1.wav: mono, 16-bit
27
+ # my_sound_2.wav: stereo, 8-bit
28
+ # my_sound_3.wav: mono, 8-bit
29
+ #
30
+ # they will all be converted to stereo/16-bit during initialization.
27
31
  class Kit
28
- PATH_SEPARATOR = File.const_get("SEPARATOR")
29
-
30
- # Creates a new Kit object. base_path indicates the folder from which sound files
31
- # with relative file paths will be loaded from.
32
- def initialize(base_path)
32
+ def initialize(base_path, kit_items)
33
33
  @base_path = base_path
34
34
  @label_mappings = {}
35
- @sounds = {}
35
+ @sound_bank = {}
36
36
  @num_channels = 1
37
37
  @bits_per_sample = 16 # Only use 16-bit files as output. Supporting 8-bit output
38
38
  # means extra complication for no real gain (I'm skeptical
39
- # anyone would explicitly want 8-bit output instead of 16-bit.
39
+ # anyone would explicitly want 8-bit output instead of 16-bit).
40
+
41
+ load_sounds(base_path, kit_items)
40
42
  end
41
43
 
42
- # Adds a new sound to the kit.
43
- def add(name, path)
44
- unless @sounds.has_key? name
45
- path_is_absolute = path.start_with?(PATH_SEPARATOR)
46
- if path_is_absolute
47
- full_path = path
48
- else
49
- full_path = @base_path + PATH_SEPARATOR + path
50
- end
51
-
52
- begin
53
- wavefile = WaveFile.open(full_path)
54
- rescue
55
- # TODO: Raise different error if sound is in an unsupported format
56
- raise SoundNotFoundError, "Sound file #{full_path} not found."
57
- end
58
-
59
- @sounds[name] = wavefile
60
- if name != path
61
- @label_mappings[name] = path
62
- end
63
-
64
- if wavefile.num_channels > @num_channels
65
- @num_channels = wavefile.num_channels
66
- end
44
+ # Returns the sample data for a sound contained in the Kit. If the all sounds in the
45
+ # kit are mono, then this will be a flat Array of Fixnums between -32768 and 32767.
46
+ # Otherwise, this will be an Array of Fixnums pairs between -32768 and 32767.
47
+ #
48
+ # label - The name of the sound to get sample data for. If the sound was defined in
49
+ # the Kit section of a song file, this will generally be a descriptive label
50
+ # such as "bass" or "snare". If defined in a track but not the kit, it will
51
+ # generally be a file name such as "my_sounds/hihat/hi_hat.wav".
52
+ #
53
+ # Examples
54
+ #
55
+ # # If @num_channels is 1, a flat Array of Fixnums:
56
+ # get_sample_data("bass")
57
+ # # => [154, 7023, 8132, 2622, -132, 34, ..., -6702]
58
+ #
59
+ # # If @num_channels is 2, a Array of Fixnums pairs:
60
+ # get_sample_data("snare")
61
+ # # => [[57, 1265], [-452, 10543], [-2531, 12643], [-6372, 11653], ..., [5482, 25673]]
62
+ #
63
+ # Returns the sample data Array for the sound bound to label.
64
+ def get_sample_data(label)
65
+ if label == "placeholder"
66
+ return []
67
67
  end
68
- end
69
-
70
- # Returns the sample data (as an Array) for a sound contained in the Kit.
71
- # Raises an error if the sound doesn't exist in the Kit.
72
- def get_sample_data(name)
73
- wavefile = @sounds[name]
68
+
69
+ sample_data = @sound_bank[label]
74
70
 
75
- if wavefile == nil
76
- raise StandardError, "Kit doesn't contain sound '#{name}'."
71
+ if sample_data == nil
72
+ # TODO: Should we really throw an exception here rather than just returning nil?
73
+ raise StandardError, "Kit doesn't contain sound '#{label}'."
77
74
  else
78
- wavefile.num_channels = @num_channels
79
- wavefile.bits_per_sample = @bits_per_sample
80
-
81
- return wavefile.sample_data
75
+ return sample_data
82
76
  end
83
77
  end
84
78
 
85
- # Returns the number of sounds currently contained in the kit.
86
- def size
87
- return @sounds.length
79
+ def scale!(scale_factor)
80
+ @sound_bank.each do |label, sample_array|
81
+ @sound_bank[label] = AudioUtils.scale(sample_array, @num_channels, scale_factor)
82
+ end
88
83
  end
89
-
90
- # Produces nicer looking output than the default version of to_yaml().
84
+
85
+ # Returns a YAML representation of the Kit. Produces nicer looking output than the default version
86
+ # of to_yaml().
87
+ #
88
+ # indent_space_count - The number of spaces to indent each line in the output (default: 0).
89
+ #
90
+ # Returns a String representation of the Kit in YAML format.
91
91
  def to_yaml(indent_space_count = 0)
92
92
  yaml = ""
93
93
  longest_label_mapping_length =
@@ -107,4 +107,81 @@ class Kit
107
107
  end
108
108
 
109
109
  attr_reader :base_path, :label_mappings, :bits_per_sample, :num_channels
110
+
111
+ private
112
+
113
+ def load_sounds(base_path, kit_items)
114
+ # Set label mappings
115
+ kit_items.each do |label, sound_file_names|
116
+ if sound_file_names.class == Array
117
+ raise StandardError, "Composite sounds aren't allowed (yet...)"
118
+ end
119
+
120
+ unless label == sound_file_names
121
+ @label_mappings[label] = sound_file_names
122
+ end
123
+ end
124
+
125
+ kit_items = make_file_names_absolute(kit_items)
126
+ raw_sounds = load_raw_sounds(kit_items)
127
+
128
+ # Convert each sound to a common format
129
+ raw_sounds.values.each do |wavefile|
130
+ wavefile.num_channels = @num_channels
131
+ wavefile.bits_per_sample = @bits_per_sample
132
+ end
133
+
134
+ # If necessary, mix component sounds into a composite
135
+ kit_items.each do |label, sound_file_names|
136
+ @sound_bank[label] = mixdown(sound_file_names, raw_sounds)
137
+ end
138
+ end
139
+
140
+ def get_absolute_path(base_path, sound_file_name)
141
+ path_is_absolute = sound_file_name.start_with?(File::SEPARATOR)
142
+ return path_is_absolute ? sound_file_name : (base_path + File::SEPARATOR + sound_file_name)
143
+ end
144
+
145
+ def make_file_names_absolute(kit_items)
146
+ kit_items.each do |label, sound_file_names|
147
+ unless sound_file_names.class == Array
148
+ sound_file_names = [sound_file_names]
149
+ end
150
+
151
+ sound_file_names.map! {|sound_file_name| get_absolute_path(base_path, sound_file_name)}
152
+ kit_items[label] = sound_file_names
153
+ end
154
+
155
+ return kit_items
156
+ end
157
+
158
+ # Load all sound files, bailing if any are invalid
159
+ def load_raw_sounds(kit_items)
160
+ raw_sounds = {}
161
+ kit_items.values.flatten.each do |sound_file_name|
162
+ begin
163
+ wavefile = WaveFile.open(sound_file_name)
164
+ rescue Errno::ENOENT
165
+ raise SoundFileNotFoundError, "Sound file #{sound_file_name} not found."
166
+ rescue StandardError
167
+ raise InvalidSoundFormatError, "Sound file #{sound_file_name} is either not a sound file, " +
168
+ "or is in an unsupported format. BEATS can handle 8 or 16-bit *.wav files."
169
+ end
170
+ @num_channels = [@num_channels, wavefile.num_channels].max
171
+ raw_sounds[sound_file_name] = wavefile
172
+ end
173
+
174
+ return raw_sounds
175
+ end
176
+
177
+ def mixdown(sound_file_names, raw_sounds)
178
+ sample_arrays = []
179
+ sound_file_names.each do |sound_file_name|
180
+ sample_arrays << raw_sounds[sound_file_name].sample_data
181
+ end
182
+
183
+ composited_sample_data = AudioUtils.composite(sample_arrays, @num_channels)
184
+
185
+ return AudioUtils.scale(composited_sample_data, @num_channels, sound_file_names.length)
186
+ end
110
187
  end
@@ -1,18 +1,21 @@
1
1
  class Pattern
2
+ FLOW_TRACK_NAME = "flow"
3
+
2
4
  def initialize(name)
3
5
  @name = name
4
6
  @tracks = {}
5
7
  end
6
8
 
7
9
  # Adds a new track to the pattern.
8
- def track(name, wave_data, rhythm)
9
- new_track = Track.new(name, wave_data, rhythm)
10
- @tracks[new_track.name] = new_track
11
-
10
+ def track(name, rhythm)
11
+ track_key = unique_track_name(name)
12
+ new_track = Track.new(name, rhythm)
13
+ @tracks[track_key] = new_track
14
+
12
15
  # If the new track is longer than any of the previously added tracks,
13
16
  # pad the other tracks with trailing . to make them all the same length.
14
17
  # Necessary to prevent incorrect overflow calculations for tracks.
15
- longest_track_length = tick_count()
18
+ longest_track_length = step_count()
16
19
  @tracks.values.each do |track|
17
20
  if track.rhythm.length < longest_track_length
18
21
  track.rhythm += "." * (longest_track_length - track.rhythm.length)
@@ -22,33 +25,9 @@ class Pattern
22
25
  return new_track
23
26
  end
24
27
 
25
- # The number of samples required for the pattern at the given tempo. DOES NOT include samples
26
- # necessary for sound that overflows past the last tick of the pattern.
27
- def sample_length(tick_sample_length)
28
- @tracks.keys.collect {|track_name| @tracks[track_name].sample_length(tick_sample_length) }.max || 0
29
- end
30
-
31
- # The number of samples required for the pattern at the given tempo. Include sound overflow
32
- # past the last tick of the pattern.
33
- def sample_length_with_overflow(tick_sample_length)
34
- @tracks.keys.collect {|track_name| @tracks[track_name].sample_length_with_overflow(tick_sample_length) }.max || 0
35
- end
36
-
37
- def tick_count
28
+ def step_count
38
29
  return @tracks.values.collect {|track| track.rhythm.length }.max || 0
39
30
  end
40
-
41
- def sample_data(tick_sample_length, num_channels, num_tracks_in_song, incoming_overflow)
42
- primary_sample_data, overflow_sample_data = generate_main_sample_data(tick_sample_length, num_channels)
43
- primary_sample_data, overflow_sample_data = handle_incoming_overflow(tick_sample_length,
44
- num_channels,
45
- incoming_overflow,
46
- primary_sample_data,
47
- overflow_sample_data)
48
- primary_sample_data = mixdown_sample_data(num_channels, num_tracks_in_song, primary_sample_data)
49
-
50
- return {:primary => primary_sample_data, :overflow => overflow_sample_data}
51
- end
52
31
 
53
32
  # Returns whether or not this pattern has the same number of tracks as other_pattern, and that
54
33
  # each of the tracks has the same name and rhythm. Ordering of tracks does not matter; will
@@ -86,93 +65,17 @@ class Pattern
86
65
 
87
66
  private
88
67
 
89
- def generate_main_sample_data(tick_sample_length, num_channels)
90
- track_names = @tracks.keys
91
- primary_sample_data = []
92
- overflow_sample_data = {}
93
- actual_sample_length = sample_length(tick_sample_length)
94
-
95
- if @intermediate_cache == nil
96
- track_names.each do |track_name|
97
- temp = @tracks[track_name].sample_data(tick_sample_length)
98
-
99
- if primary_sample_data == []
100
- primary_sample_data = temp[:primary]
101
- overflow_sample_data[track_name] = temp[:overflow]
102
- else
103
- track_samples = temp[:primary]
104
- if num_channels == 1
105
- track_samples.length.times {|i| primary_sample_data[i] += track_samples[i] }
106
- else
107
- track_samples.length.times do |i|
108
- primary_sample_data[i] = [primary_sample_data[i][0] + track_samples[i][0],
109
- primary_sample_data[i][1] + track_samples[i][1]]
110
- end
111
- end
112
-
113
- overflow_sample_data[track_name] = temp[:overflow]
114
- end
115
- end
116
-
117
- @intermediate_cache = {:primary => primary_sample_data.dup, :overflow => overflow_sample_data.dup}
118
- else
119
- primary_sample_data = @intermediate_cache[:primary].dup
120
- overflow_sample_data = @intermediate_cache[:overflow].dup
68
+ # Returns a unique track name that is not already in use by a track in
69
+ # this pattern. Used to help support having multiple tracks with the same
70
+ # sample in a track.
71
+ def unique_track_name(name)
72
+ i = 2
73
+ name_key = name
74
+ while @tracks.has_key? name_key
75
+ name_key = "#{name}#{i.to_s}"
76
+ i += 1
121
77
  end
122
-
123
- return primary_sample_data, overflow_sample_data
124
- end
125
-
126
- def handle_incoming_overflow(tick_sample_length, num_channels, incoming_overflow, primary_sample_data, overflow_sample_data)
127
- track_names = @tracks.keys
128
-
129
- # Add overflow from previous pattern
130
- incoming_overflow.keys.each do |track_name|
131
- num_incoming_overflow_samples = incoming_overflow[track_name].length
132
78
 
133
- if num_incoming_overflow_samples > 0
134
- if track_names.member?(track_name)
135
- # TODO: Does this handle situations where track has a .... rhythm and overflow is
136
- # longer than track length?
137
-
138
- intro_length = @tracks[track_name].intro_sample_length(tick_sample_length)
139
- if num_incoming_overflow_samples > intro_length
140
- num_incoming_overflow_samples = intro_length
141
- end
142
- else
143
- # If incoming overflow for track is longer than the pattern length, only add the first part of
144
- # the overflow to the pattern, and add the remainder to overflow_sample_data so that it gets
145
- # handled by the next pattern to be generated.
146
- if num_incoming_overflow_samples > primary_sample_data.length
147
- overflow_sample_data[track_name] = (incoming_overflow[track_name])[primary_sample_data.length...num_incoming_overflow_samples]
148
- num_incoming_overflow_samples = primary_sample_data.length
149
- end
150
- end
151
-
152
- if num_channels == 1
153
- num_incoming_overflow_samples.times {|i| primary_sample_data[i] += incoming_overflow[track_name][i]}
154
- else
155
- num_incoming_overflow_samples.times do |i|
156
- primary_sample_data[i] = [primary_sample_data[i][0] + incoming_overflow[track_name][i][0],
157
- primary_sample_data[i][1] + incoming_overflow[track_name][i][1]]
158
- end
159
- end
160
- end
161
- end
162
-
163
- return primary_sample_data, overflow_sample_data
164
- end
165
-
166
- def mixdown_sample_data(num_channels, num_tracks_in_song, primary_sample_data)
167
- # Mix down the pattern's tracks into one single track
168
- if num_tracks_in_song > 1
169
- if num_channels == 1
170
- primary_sample_data = primary_sample_data.map {|sample| sample / num_tracks_in_song }
171
- else
172
- primary_sample_data = primary_sample_data.map {|sample| [sample[0] / num_tracks_in_song, sample[1] / num_tracks_in_song]}
173
- end
174
- end
175
-
176
- return primary_sample_data
79
+ return name_key
177
80
  end
178
- end
81
+ end
@@ -0,0 +1,111 @@
1
+ class InvalidFlowError < RuntimeError; end
2
+
3
+ # This class is used for an experimental feature that allows specifying repeats inside of
4
+ # individual patterns, instead of the song flow. This feature is currently disabled, so for
5
+ # the time being this class is dead code.
6
+ #
7
+ # TODO: The expand_pattern method in this class should probably be moved to the Pattern class.
8
+ # This class would then go away.
9
+ class PatternExpander
10
+ BARLINE = "|"
11
+ TICK = "-"
12
+ REPEAT_FRAME_REGEX = /:[-]*:[0-9]*/
13
+ NUMBER_REGEX = /[0-9]+/
14
+
15
+ # TODO: What should happen if flow is longer than pattern?
16
+ # Either ignore extra flow, or add trailing .... to each track to match up?
17
+ def self.expand_pattern(flow, pattern)
18
+ unless self.valid_flow? flow
19
+ raise InvalidFlowError, "Invalid flow"
20
+ end
21
+
22
+ flow = flow.delete(BARLINE)
23
+
24
+ # Count number of :
25
+ # If odd, then there's an implicit : at the beginning of the pattern.
26
+ # TODO: What if the first character in the flow is already :
27
+ # That means repeat the first step twice, right?
28
+ number_of_colons = flow.scan(/:/).length
29
+ if number_of_colons % 2 == 1
30
+ # TODO: What if flow[0] is not '-'
31
+ flow[0] = ":" # Make the implicit : at the beginning explicit
32
+ end
33
+
34
+ repeat_frames = parse_flow_for_repeat_frames(flow)
35
+
36
+ repeat_frames.reverse.each do |frame|
37
+ pattern.tracks.each do |name, track|
38
+ range = frame[:range]
39
+
40
+ # WARNING: Don't change the three lines below to:
41
+ # track.rhythm[range] = whatever
42
+ # When changing the rhythm like this, rhythm=() won't be called,
43
+ # and Track.beats won't be updated as a result.
44
+ new_rhythm = track.rhythm
45
+ new_rhythm[range] = new_rhythm[range] * frame[:repeats]
46
+ track.rhythm = new_rhythm
47
+ end
48
+ end
49
+
50
+ return pattern
51
+ end
52
+
53
+ # TODO: Return more specific info on why flow isn't valid
54
+ def self.valid_flow?(flow)
55
+ flow = flow.delete(BARLINE)
56
+ flow = flow.delete(TICK)
57
+
58
+ # If flow contains any characters other than : and [0-9], it's invalid.
59
+ if flow.match(/[^:0-9]/) != nil
60
+ return false
61
+ end
62
+
63
+ # If flow contains nothing but :, it's always valid.
64
+ if flow == ":" * flow.length
65
+ return true
66
+ end
67
+
68
+ # If flow DOESN'T contain a :, it's not valid.
69
+ if flow.match(/:/) == nil
70
+ return false
71
+ end
72
+
73
+ segments = flow.split(/[0-9]+/)
74
+
75
+ # Ignore first segment
76
+ segments[1...segments.length].each do |segment|
77
+ if segment.length % 2 == 1
78
+ return false
79
+ end
80
+ end
81
+
82
+ return true
83
+ end
84
+
85
+ private
86
+
87
+ def self.parse_flow_for_repeat_frames(flow)
88
+ repeat_frames = []
89
+ lower_bound = 0
90
+ frame_start_index = flow[lower_bound...flow.length] =~ REPEAT_FRAME_REGEX
91
+ while frame_start_index != nil do
92
+ str = flow[lower_bound...flow.length].match(REPEAT_FRAME_REGEX).to_s
93
+
94
+ range_start = lower_bound + frame_start_index
95
+ range_end = range_start + str.length - 1
96
+
97
+ num_repeats = str.match(NUMBER_REGEX).to_s
98
+ num_repeats = (num_repeats == "") ? 2 : num_repeats.to_i
99
+
100
+ repeat_frame = {}
101
+ repeat_frame[:range] = range_start..range_end
102
+ repeat_frame[:repeats] = num_repeats
103
+ repeat_frames << repeat_frame
104
+
105
+ lower_bound += frame_start_index + str.length
106
+ frame_start_index = flow[lower_bound...flow.length] =~ REPEAT_FRAME_REGEX
107
+ end
108
+
109
+ return repeat_frames
110
+ end
111
+ end