beats 1.0.0 → 1.1.0

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.
@@ -10,18 +10,23 @@ BEATS is a drum machine written in pure Ruby. Feed it a song notated in YAML, an
10
10
  - Chorus: x4
11
11
  - Verse: x2
12
12
  - Chorus: x4
13
+ Kit:
14
+ - bass: sounds/bass.wav
15
+ - snare: sounds/snare.wav
16
+ - hh_closed: sounds/hh_closed.wav
17
+ - agogo: sounds/agogo_high.wav
13
18
 
14
19
  Verse:
15
- - bass.wav: X...X...X...X...
16
- - snare.wav: ..............X.
17
- - hh_closed.wav: X.XXX.XXX.X.X.X.
18
- - agogo_high.wav: ..............XX
20
+ - bass: X...X...X...X...
21
+ - snare: ..............X.
22
+ - hh_closed: X.XXX.XXX.X.X.X.
23
+ - agogo: ..............XX
19
24
 
20
25
  Chorus:
21
- - bass.wav: X...X...X...X...
22
- - snare.wav: ....X.......X...
23
- - hh_closed.wav: X.XXX.XXX.XX..X.
24
- - tom4.wav: ...........X....
25
- - tom2.wav: ..............X.
26
+ - bass: X...X...X...X...
27
+ - snare: ....X.......X...
28
+ - hh_closed: X.XXX.XXX.XX..X.
29
+ - sounds/tom4.wav: ...........X....
30
+ - sounds/tom2.wav: ..............X.
26
31
 
27
32
  For installation and usage instructions, visit the BEATS website at [http://beatsdrummachine.com](http://beatsdrummachine.com).
data/bin/beats CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require File.dirname(__FILE__) + "/../lib/song"
4
+ require File.dirname(__FILE__) + "/../lib/songparser"
4
5
  require File.dirname(__FILE__) + "/../lib/kit"
5
6
  require File.dirname(__FILE__) + "/../lib/pattern"
6
7
  require File.dirname(__FILE__) + "/../lib/track"
@@ -9,7 +10,7 @@ require "optparse"
9
10
  require "yaml"
10
11
  require "wavefile"
11
12
 
12
- BEATS_VERSION = "1.0.0"
13
+ BEATS_VERSION = "1.1.0"
13
14
  SAMPLE_RATE = 44100
14
15
 
15
16
  def parse_options
@@ -63,7 +64,8 @@ end
63
64
 
64
65
  begin
65
66
  parse_start_time = Time.now
66
- song_from_file = Song.new(File.dirname(input_file), YAML.load_file(input_file))
67
+ song_parser = SongParser.new()
68
+ song_from_file = song_parser.parse(File.dirname(input_file), YAML.load_file(input_file))
67
69
 
68
70
  generate_samples_start = Time.now
69
71
  sample_data = song_from_file.sample_data(options[:pattern], options[:split])
@@ -90,12 +92,6 @@ rescue Errno::ENOENT => detail
90
92
  puts ""
91
93
  puts "Song file '#{input_file}' not found."
92
94
  puts ""
93
- rescue ArgumentError => detail
94
- puts ""
95
- puts "Song file '#{input_file}' has an error:"
96
- puts " Syntax error in YAML file:"
97
- puts " #{detail}"
98
- puts ""
99
95
  rescue SongParseError => detail
100
96
  puts ""
101
97
  puts "Song file '#{input_file}' has an error:"
data/lib/kit.rb CHANGED
@@ -1,5 +1,10 @@
1
+ class SoundNotFoundError < RuntimeError; end
2
+
1
3
  class Kit
2
- def initialize()
4
+ PATH_SEPARATOR = File.const_get("SEPARATOR")
5
+
6
+ def initialize(base_path)
7
+ @base_path = base_path
3
8
  @sounds = {}
4
9
  @num_channels = 0
5
10
  @bits_per_sample = 0
@@ -7,28 +12,37 @@ class Kit
7
12
 
8
13
  def add(name, path)
9
14
  if(!@sounds.has_key? name)
10
- w = WaveFile.open(path)
11
- @sounds[name] = w
15
+ if(!path.start_with?(PATH_SEPARATOR))
16
+ path = @base_path + PATH_SEPARATOR + path
17
+ end
18
+
19
+ begin
20
+ wavefile = WaveFile.open(path)
21
+ rescue
22
+ raise SoundNotFoundError, "Sound file #{name} not found."
23
+ end
24
+
25
+ @sounds[name] = wavefile
12
26
 
13
- if w.num_channels > @num_channels
14
- @num_channels = w.num_channels
27
+ if wavefile.num_channels > @num_channels
28
+ @num_channels = wavefile.num_channels
15
29
  end
16
- if w.bits_per_sample > @bits_per_sample
17
- @bits_per_sample = w.bits_per_sample
30
+ if wavefile.bits_per_sample > @bits_per_sample
31
+ @bits_per_sample = wavefile.bits_per_sample
18
32
  end
19
33
  end
20
34
  end
21
35
 
22
36
  def get_sample_data(name)
23
- w = @sounds[name]
37
+ wavefile = @sounds[name]
24
38
 
25
- if w == nil
39
+ if wavefile == nil
26
40
  raise StandardError, "Kit doesn't contain sound '#{name}'."
27
41
  else
28
- w.num_channels = @num_channels
29
- w.bits_per_sample = @bits_per_sample
42
+ wavefile.num_channels = @num_channels
43
+ wavefile.bits_per_sample = @bits_per_sample
30
44
 
31
- return w.sample_data
45
+ return wavefile.sample_data
32
46
  end
33
47
  end
34
48
 
@@ -36,5 +50,5 @@ class Kit
36
50
  return @sounds.length
37
51
  end
38
52
 
39
- attr_reader :bits_per_sample, :num_channels
53
+ attr_reader :base_path, :bits_per_sample, :num_channels
40
54
  end
@@ -1,6 +1,7 @@
1
1
  class Pattern
2
2
  def initialize(name)
3
3
  @name = name
4
+ @cache = {}
4
5
  @tracks = {}
5
6
  end
6
7
 
@@ -47,6 +48,12 @@ class Pattern
47
48
  private
48
49
 
49
50
  def combined_sample_data(tick_sample_length, num_channels, num_tracks_in_song, incoming_overflow)
51
+ # If we've already encountered this pattern with the same incoming overflow before,
52
+ # return the pre-mixed down version from the cache.
53
+ if(@cache.member?(incoming_overflow))
54
+ return @cache[incoming_overflow]
55
+ end
56
+
50
57
  fill_value = (num_channels == 1) ? 0 : [].fill(0, 0, num_channels)
51
58
  track_names = @tracks.keys
52
59
  primary_sample_data = []
@@ -64,8 +71,8 @@ private
64
71
  (0...track_samples.length).each {|i| primary_sample_data[i] += track_samples[i] }
65
72
  else
66
73
  (0...track_samples.length).each {|i|
67
- primary_sample_data[i] = [primary_sample_data[i][0] + track_samples[i][0],
68
- primary_sample_data[i][1] + track_samples[i][1]]
74
+ primary_sample_data[i][0] += track_samples[i][0]
75
+ primary_sample_data[i][1] += track_samples[i][1]
69
76
  }
70
77
  end
71
78
 
@@ -86,14 +93,18 @@ private
86
93
  end
87
94
  }
88
95
 
89
- # Mix down the tracks into one
96
+ # Mix down the pattern's tracks into one single track
90
97
  if(num_channels == 1)
91
98
  primary_sample_data = primary_sample_data.map {|sample| (sample / num_tracks_in_song).round }
92
99
  else
93
100
  primary_sample_data = primary_sample_data.map {|sample| [(sample[0] / num_tracks_in_song).round, (sample[1] / num_tracks_in_song).round] }
94
101
  end
95
102
 
96
- return {:primary => primary_sample_data, :overflow => overflow_sample_data}
103
+ # Add the result to the cache so we don't have to go through all of this the next time...
104
+ mixdown_sample_data = {:primary => primary_sample_data, :overflow => overflow_sample_data}
105
+ @cache[incoming_overflow] = mixdown_sample_data
106
+
107
+ return mixdown_sample_data
97
108
  end
98
109
 
99
110
  def split_sample_data(tick_sample_length, num_channels, incoming_overflow)
@@ -1,20 +1,16 @@
1
- class SongParseError < RuntimeError; end
1
+ class InvalidTempoError < RuntimeError; end
2
2
 
3
3
  class Song
4
4
  SAMPLE_RATE = 44100
5
5
  SECONDS_PER_MINUTE = 60.0
6
- PATH_SEPARATOR = File.const_get("SEPARATOR")
6
+ SAMPLES_PER_MINUTE = SAMPLE_RATE * SECONDS_PER_MINUTE
7
+ DEFAULT_TEMPO = 120
7
8
 
8
- def initialize(input_path, definition = nil)
9
- self.tempo = 120
10
- @input_path = input_path
11
- @kit = Kit.new()
9
+ def initialize(base_path)
10
+ self.tempo = DEFAULT_TEMPO
11
+ @kit = Kit.new(base_path)
12
12
  @patterns = {}
13
13
  @structure = []
14
-
15
- if(definition != nil)
16
- parse(definition)
17
- end
18
14
  end
19
15
 
20
16
  def pattern(name)
@@ -86,15 +82,15 @@ class Song
86
82
 
87
83
  def tempo=(new_tempo)
88
84
  if(new_tempo.class != Fixnum || new_tempo <= 0)
89
- raise SongParseError, "Invalid tempo: '#{new_tempo}'. Tempo must be a number greater than 0."
85
+ raise InvalidTempoError, "Invalid tempo: '#{new_tempo}'. Tempo must be a number greater than 0."
90
86
  end
91
87
 
92
88
  @tempo = new_tempo
93
- @tick_sample_length = (SAMPLE_RATE * SECONDS_PER_MINUTE) / new_tempo / 4.0
89
+ @tick_sample_length = SAMPLES_PER_MINUTE / new_tempo / 4.0
94
90
  end
95
91
 
96
- attr_reader :input_path, :tick_sample_length
97
- attr_accessor :structure
92
+ attr_reader :tick_sample_length, :patterns
93
+ attr_accessor :structure, :kit
98
94
 
99
95
  private
100
96
 
@@ -117,102 +113,6 @@ private
117
113
 
118
114
  return merged_sample_data
119
115
  end
120
-
121
- # Converts all hash keys to be lowercase
122
- def downcase_hash_keys(hash)
123
- return hash.inject({}) {|new_hash, pair|
124
- new_hash[pair.first.downcase] = pair.last
125
- new_hash
126
- }
127
- end
128
-
129
- def parse(definition)
130
- if(definition.class == String)
131
- song_definition = YAML.load(definition)
132
- elsif(definition.class == Hash)
133
- song_definition = definition
134
- else
135
- raise StandardError, "Invalid song input"
136
- end
137
-
138
- @kit = build_kit(song_definition)
139
-
140
- song_definition = downcase_hash_keys(song_definition)
141
-
142
- # Process each pattern
143
- song_definition.keys.each{|key|
144
- if(key != "song")
145
- new_pattern = self.pattern key.to_sym
146
-
147
- track_list = song_definition[key]
148
- track_list.each{|track_definition|
149
- track_name = track_definition.keys.first
150
- new_pattern.track track_name, @kit.get_sample_data(track_name), track_definition[track_name]
151
- }
152
- end
153
- }
154
-
155
- # Process song header
156
- parse_song_header(downcase_hash_keys(song_definition["song"]))
157
- end
158
-
159
- def parse_song_header(header_data)
160
- self.tempo = header_data["tempo"]
161
-
162
- pattern_list = header_data["structure"]
163
- structure = []
164
- pattern_list.each{|pattern_item|
165
- if(pattern_item.class == String)
166
- pattern_item = {pattern_item => "x1"}
167
- end
168
-
169
- pattern_name = pattern_item.keys.first
170
- pattern_name_sym = pattern_name.downcase.to_sym
171
-
172
- if(!@patterns.has_key?(pattern_name_sym))
173
- raise SongParseError, "Song structure includes non-existant pattern: #{pattern_name}."
174
- end
175
-
176
- multiples_str = pattern_item[pattern_name]
177
- multiples_str.slice!(0)
178
- multiples = multiples_str.to_i
179
-
180
- if(multiples_str.match(/[^0-9]/) != nil)
181
- raise SongParseError, "'#{multiples_str}' is an invalid number of repeats for pattern '#{pattern_name}'. Number of repeats should be a whole number."
182
- elsif(multiples < 0)
183
- raise SongParseError, "'#{multiples_str}' is an invalid number of repeats for pattern '#{pattern_name}'. Must be 0 or greater."
184
- end
185
-
186
- multiples.times { structure << pattern_name_sym }
187
- }
188
-
189
- @structure = structure
190
- end
191
-
192
- def build_kit(song_definition)
193
- kit = Kit.new()
194
-
195
- song_definition.keys.each{|key|
196
- if(key.downcase != "song")
197
- track_list = song_definition[key]
198
- track_list.each{|track_definition|
199
- track_name = track_definition.keys.first
200
- track_path = track_name
201
- if(!track_path.start_with?(PATH_SEPARATOR))
202
- track_path = @input_path + PATH_SEPARATOR + track_path
203
- end
204
-
205
- if(!File.exists? track_path)
206
- raise SongParseError, "File '#{track_name}' not found for pattern '#{key}'"
207
- end
208
-
209
- kit.add(track_name, track_path)
210
- }
211
- end
212
- }
213
-
214
- return kit
215
- end
216
116
 
217
117
  def sample_data_split_all_patterns(fill_value, num_tracks_in_song)
218
118
  output_data = {}
@@ -0,0 +1,169 @@
1
+ class SongParseError < RuntimeError; end
2
+
3
+ class SongParser
4
+ NO_SONG_HEADER_ERROR_MSG =
5
+ "Song must have a header. Here's an example:
6
+
7
+ Song:
8
+ Tempo: 120
9
+ Structure:
10
+ - Verse: x2
11
+ - Chorus: x2"
12
+
13
+ def initialize()
14
+ end
15
+
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)
19
+
20
+ song = Song.new(base_path)
21
+
22
+ # 1.) Set tempo
23
+ begin
24
+ if raw_song_components[:tempo] != nil
25
+ song.tempo = raw_song_components[:tempo]
26
+ end
27
+ rescue InvalidTempoError => detail
28
+ raise SongParseError, "#{detail}"
29
+ end
30
+
31
+ # 2.) Build the kit
32
+ begin
33
+ kit = build_kit(base_path, raw_song_components[:kit], raw_song_components[:patterns])
34
+ rescue SoundNotFoundError => detail
35
+ raise SongParseError, "#{detail}"
36
+ end
37
+ song.kit = kit
38
+
39
+ # 3.) Load patterns
40
+ add_patterns_to_song(song, raw_song_components[:patterns])
41
+
42
+ # 4.) Set structure
43
+ if(raw_song_components[:structure] == nil)
44
+ raise SongParseError, "Song must have a Structure section in the header."
45
+ else
46
+ set_song_structure(song, raw_song_components[:structure])
47
+ end
48
+
49
+ return song
50
+ end
51
+
52
+ private
53
+
54
+ # This is basically a factory. Don't see a benefit to extracting to a full class.
55
+ # Also, is "canonicalize" a word?
56
+ def canonicalize_definition(definition)
57
+ if(definition.class == String)
58
+ begin
59
+ raw_song_definition = YAML.load(definition)
60
+ rescue ArgumentError => detail
61
+ raise SongParseError, "Syntax error in YAML file"
62
+ end
63
+ elsif(definition.class == Hash)
64
+ raw_song_definition = definition
65
+ else
66
+ raise SongParseError, "Invalid song input"
67
+ end
68
+
69
+ return raw_song_definition
70
+ end
71
+
72
+ def split_raw_yaml_into_components(raw_song_definition)
73
+ raw_song_components = {}
74
+ raw_song_components[:full_definition] = downcase_hash_keys(raw_song_definition)
75
+
76
+ if(raw_song_components[:full_definition]["song"] != nil)
77
+ raw_song_components[:header] = downcase_hash_keys(raw_song_components[:full_definition]["song"])
78
+ else
79
+ raise SongParseError, NO_SONG_HEADER_ERROR_MSG
80
+ end
81
+ raw_song_components[:tempo] = raw_song_components[:header]["tempo"]
82
+ raw_song_components[:kit] = raw_song_components[:header]["kit"]
83
+ raw_song_components[:structure] = raw_song_components[:header]["structure"]
84
+ raw_song_components[:patterns] = raw_song_components[:full_definition].reject {|k, v| k == "song"}
85
+
86
+ return raw_song_components
87
+ end
88
+
89
+ def build_kit(base_path, raw_kit, raw_patterns)
90
+ kit = Kit.new(base_path)
91
+
92
+ # Add sounds defined in the Kit section of the song header
93
+ if(raw_kit != nil)
94
+ raw_kit.each {|kit_item|
95
+ kit.add(kit_item.keys.first, kit_item.values.first)
96
+ }
97
+ end
98
+
99
+ # Add sounds not defined in Kit section, but used in individual tracks
100
+ # TODO Investigate detecting duplicate keys already defined in the Kit section, as this could possibly
101
+ # result in a performance improvement when the sound has to be converted to a different bit rate/num channels,
102
+ # as well as use less memory.
103
+ raw_patterns.keys.each{|key|
104
+ track_list = raw_patterns[key]
105
+ track_list.each{|track_definition|
106
+ track_name = track_definition.keys.first
107
+ track_path = track_name
108
+
109
+ kit.add(track_name, track_path)
110
+ }
111
+ }
112
+
113
+ return kit
114
+ end
115
+
116
+ def add_patterns_to_song(song, raw_patterns)
117
+ raw_patterns.keys.each{|key|
118
+ new_pattern = song.pattern key.to_sym
119
+
120
+ track_list = raw_patterns[key]
121
+ track_list.each{|track_definition|
122
+ track_name = track_definition.keys.first
123
+ new_pattern.track track_name, song.kit.get_sample_data(track_name), track_definition[track_name]
124
+ }
125
+ }
126
+ end
127
+
128
+ def set_song_structure(song, raw_structure)
129
+ structure = []
130
+
131
+ raw_structure.each{|pattern_item|
132
+ if(pattern_item.class == String)
133
+ pattern_item = {pattern_item => "x1"}
134
+ end
135
+
136
+ pattern_name = pattern_item.keys.first
137
+ pattern_name_sym = pattern_name.downcase.to_sym
138
+
139
+ # Convert the number of repeats from a String such as "x4" into an integer such as 4.
140
+ multiples_str = pattern_item[pattern_name]
141
+ multiples_str.slice!(0)
142
+ multiples = multiples_str.to_i
143
+
144
+ if(multiples_str.match(/[^0-9]/) != nil)
145
+ raise SongParseError, "'#{multiples_str}' is an invalid number of repeats for pattern '#{pattern_name}'. Number of repeats should be a whole number."
146
+ else
147
+ if(multiples < 0)
148
+ raise SongParseError, "'#{multiples_str}' is an invalid number of repeats for pattern '#{pattern_name}'. Must be 0 or greater."
149
+ elsif(multiples > 0 && !song.patterns.has_key?(pattern_name_sym))
150
+ # This test is purposefully designed to only throw an error if the number of repeats is greater
151
+ # than 0. This allows you to specify an undefined pattern in the structure with "x0" repeats.
152
+ # This can be convenient for defining the structure before all patterns have been added to the song file.
153
+ raise SongParseError, "Song structure includes non-existent pattern: #{pattern_name}."
154
+ end
155
+ end
156
+
157
+ multiples.times { structure << pattern_name_sym }
158
+ }
159
+ song.structure = structure
160
+ end
161
+
162
+ # Converts all hash keys to be lowercase
163
+ def downcase_hash_keys(hash)
164
+ return hash.inject({}) {|new_hash, pair|
165
+ new_hash[pair.first.downcase] = pair.last
166
+ new_hash
167
+ }
168
+ end
169
+ end
@@ -1,6 +1,7 @@
1
1
  require 'test/unit'
2
2
  require 'yaml'
3
3
  require 'song'
4
+ require 'songparser'
4
5
  require 'pattern'
5
6
  require 'track'
6
7
  require 'kit'
@@ -6,50 +6,55 @@ class SongTest < Test::Unit::TestCase
6
6
  MIN_SAMPLE_8BIT = 0
7
7
  MAX_SAMPLE_8BIT = 255
8
8
 
9
- def test_add
9
+ def test_valid_add
10
10
  # Test adding sounds with progressively higher bits per sample and num channels.
11
11
  # Verify that kit.bits_per_sample and kit.num_channels is ratcheted up.
12
- kit = Kit.new()
12
+ kit = Kit.new("test/sounds")
13
13
  assert_equal(kit.bits_per_sample, 0)
14
14
  assert_equal(kit.num_channels, 0)
15
15
  assert_equal(kit.size, 0)
16
- kit.add("mono8", "test/sounds/bass_mono_8.wav")
16
+ kit.add("mono8", "bass_mono_8.wav")
17
17
  assert_equal(kit.bits_per_sample, 8)
18
18
  assert_equal(kit.num_channels, 1)
19
19
  assert_equal(kit.size, 1)
20
- kit.add("mono16", "test/sounds/bass_mono_16.wav")
20
+ kit.add("mono16", "bass_mono_16.wav")
21
21
  assert_equal(kit.bits_per_sample, 16)
22
22
  assert_equal(kit.num_channels, 1)
23
23
  assert_equal(kit.size, 2)
24
- kit.add("stereo16", "test/sounds/bass_stereo_16.wav")
24
+ kit.add("stereo16", "bass_stereo_16.wav")
25
25
  assert_equal(kit.bits_per_sample, 16)
26
26
  assert_equal(kit.num_channels, 2)
27
27
  assert_equal(kit.size, 3)
28
28
 
29
29
  # Test adding sounds with progressively lower bits per sample and num channels.
30
30
  # Verify that kit.bits_per_sample and kit.num_channels doesn't change.
31
- kit = Kit.new()
31
+ kit = Kit.new("test/sounds")
32
32
  assert_equal(kit.bits_per_sample, 0)
33
33
  assert_equal(kit.num_channels, 0)
34
- kit.add("stereo16", "test/sounds/bass_stereo_16.wav")
34
+ kit.add("stereo16", "bass_stereo_16.wav")
35
35
  assert_equal(kit.bits_per_sample, 16)
36
36
  assert_equal(kit.num_channels, 2)
37
- kit.add("mono16", "test/sounds/bass_mono_16.wav")
37
+ kit.add("mono16", "bass_mono_16.wav")
38
38
  assert_equal(kit.bits_per_sample, 16)
39
39
  assert_equal(kit.num_channels, 2)
40
- kit.add("mono8", "test/sounds/bass_mono_8.wav")
40
+ kit.add("mono8", "bass_mono_8.wav")
41
41
  assert_equal(kit.bits_per_sample, 16)
42
42
  assert_equal(kit.num_channels, 2)
43
43
  end
44
44
 
45
+ def test_invalid_add
46
+ kit = Kit.new("test/sounds")
47
+ assert_raise(SoundNotFoundError) { kit.add("i_do_not_exist", "i_do_not_exist.wav") }
48
+ end
49
+
45
50
  def test_get_sample_data
46
- kit = Kit.new()
51
+ kit = Kit.new("test/sounds")
47
52
 
48
53
  assert_raise(StandardError) { kit.get_sample_data("nonexistant") }
49
54
 
50
55
  # Test adding sounds with progressively higher bits per sample and num channels.
51
56
  # Verify that sample data bits per sample and num channels is ratcheted up.
52
- kit.add("mono8", "test/sounds/bass_mono_8.wav")
57
+ kit.add("mono8", "bass_mono_8.wav")
53
58
  sample_data = kit.get_sample_data("mono8")
54
59
  assert(sample_data.max <= MAX_SAMPLE_8BIT)
55
60
  assert(sample_data.min >= MIN_SAMPLE_8BIT)
@@ -59,7 +64,7 @@ class SongTest < Test::Unit::TestCase
59
64
  }
60
65
  assert(all_are_fixnums)
61
66
 
62
- kit.add("mono16", "test/sounds/bass_mono_16.wav")
67
+ kit.add("mono16", "bass_mono_16.wav")
63
68
  sample_data = kit.get_sample_data("mono8")
64
69
  assert(sample_data.max > MAX_SAMPLE_8BIT)
65
70
  assert(sample_data.min < MIN_SAMPLE_8BIT)
@@ -69,7 +74,7 @@ class SongTest < Test::Unit::TestCase
69
74
  }
70
75
  assert(all_are_fixnums)
71
76
 
72
- kit.add("stereo16", "test/sounds/bass_stereo_16.wav")
77
+ kit.add("stereo16", "bass_stereo_16.wav")
73
78
  sample_data = kit.get_sample_data("stereo16")
74
79
  assert(sample_data.flatten.max > MAX_SAMPLE_8BIT)
75
80
  assert(sample_data.flatten.min < MIN_SAMPLE_8BIT)
@@ -83,9 +88,9 @@ class SongTest < Test::Unit::TestCase
83
88
 
84
89
  # Test adding sounds with progressively lower bits per sample and num channels.
85
90
  # Verify that sample data bits per sample and num channels doesn't go down.
86
- kit = Kit.new()
91
+ kit = Kit.new("test/sounds")
87
92
 
88
- kit.add("stereo16", "test/sounds/bass_stereo_16.wav")
93
+ kit.add("stereo16", "bass_stereo_16.wav")
89
94
  sample_data = kit.get_sample_data("stereo16")
90
95
  assert(sample_data.flatten.max > MAX_SAMPLE_8BIT)
91
96
  assert(sample_data.flatten.min < MIN_SAMPLE_8BIT)
@@ -96,7 +101,7 @@ class SongTest < Test::Unit::TestCase
96
101
  assert(all_are_arrays)
97
102
  assert(sample_data.first.length == 2)
98
103
 
99
- kit.add("mono16", "test/sounds/bass_mono_16.wav")
104
+ kit.add("mono16", "bass_mono_16.wav")
100
105
  sample_data = kit.get_sample_data("mono16")
101
106
  assert(sample_data.flatten.max > MAX_SAMPLE_8BIT)
102
107
  assert(sample_data.flatten.min < MIN_SAMPLE_8BIT)
@@ -107,7 +112,7 @@ class SongTest < Test::Unit::TestCase
107
112
  assert(all_are_arrays)
108
113
  assert(sample_data.first.length == 2)
109
114
 
110
- kit.add("mono8", "test/sounds/bass_mono_8.wav")
115
+ kit.add("mono8", "bass_mono_8.wav")
111
116
  sample_data = kit.get_sample_data("mono8")
112
117
  assert(sample_data.flatten.max > MAX_SAMPLE_8BIT)
113
118
  assert(sample_data.flatten.min < MIN_SAMPLE_8BIT)
@@ -7,11 +7,11 @@ class PatternTest < Test::Unit::TestCase
7
7
  SECONDS_IN_MINUTE = 60.0
8
8
 
9
9
  def generate_test_data
10
- kit = Kit.new()
11
- kit.add("bass.wav", "test/sounds/bass_mono_8.wav")
12
- kit.add("snare.wav", "test/sounds/snare_mono_8.wav")
13
- kit.add("hh_closed.wav", "test/sounds/hh_closed_mono_8.wav")
14
- kit.add("hh_open.wav", "test/sounds/hh_open_mono_8.wav")
10
+ kit = Kit.new("test/sounds")
11
+ kit.add("bass.wav", "bass_mono_8.wav")
12
+ kit.add("snare.wav", "snare_mono_8.wav")
13
+ kit.add("hh_closed.wav", "hh_closed_mono_8.wav")
14
+ kit.add("hh_open.wav", "hh_open_mono_8.wav")
15
15
 
16
16
  test_patterns = []
17
17
 
@@ -2,41 +2,26 @@ $:.unshift File.join(File.dirname(__FILE__),'..','lib')
2
2
 
3
3
  require 'test/includes'
4
4
 
5
- class MockSong < Song
6
- attr_reader :patterns
7
- attr_accessor :kit
8
- end
9
-
10
5
  class SongTest < Test::Unit::TestCase
11
6
  DEFAULT_TEMPO = 120
12
7
 
13
8
  def generate_test_data
14
- kit = Kit.new()
15
- kit.add("bass.wav", "test/sounds/bass_mono_8.wav")
16
- kit.add("snare.wav", "test/sounds/snare_mono_8.wav")
17
- kit.add("hh_closed.wav", "test/sounds/hh_closed_mono_8.wav")
18
- kit.add("ride.wav", "test/sounds/ride_mono_8.wav")
9
+ kit = Kit.new("test/sounds")
10
+ kit.add("bass.wav", "bass_mono_8.wav")
11
+ kit.add("snare.wav", "snare_mono_8.wav")
12
+ kit.add("hh_closed.wav", "hh_closed_mono_8.wav")
13
+ kit.add("ride.wav", "ride_mono_8.wav")
19
14
 
20
- test_songs = {}
15
+ test_songs = SongParserTest.generate_test_data()
21
16
 
22
- test_songs[:blank] = MockSong.new(File.dirname(__FILE__) + "/..")
17
+ test_songs[:blank] = Song.new("test/sounds")
23
18
 
24
- test_songs[:no_structure] = MockSong.new(File.dirname(__FILE__) + "/..")
19
+ test_songs[:no_structure] = Song.new("test/sounds")
25
20
  verse = test_songs[:no_structure].pattern :verse
26
21
  verse.track "bass.wav", kit.get_sample_data("bass.wav"), "X.......X......."
27
22
  verse.track "snare.wav", kit.get_sample_data("snare.wav"), "....X.......X..."
28
23
  verse.track "hh_closed.wav", kit.get_sample_data("hh_closed.wav"), "X.X.X.X.X.X.X.X."
29
24
 
30
- repeats_not_specified_yaml = "
31
- Song:
32
- Tempo: 100
33
- Structure:
34
- - Verse
35
-
36
- Verse:
37
- - test/sounds/bass_mono_8.wav: X"
38
- test_songs[:repeats_not_specified] = MockSong.new(File.dirname(__FILE__) + "/..", repeats_not_specified_yaml)
39
-
40
25
  overflow_yaml = "
41
26
  Song:
42
27
  Tempo: 100
@@ -45,9 +30,9 @@ Song:
45
30
 
46
31
  Verse:
47
32
  - test/sounds/snare_mono_8.wav: ...X"
48
- test_songs[:overflow] = MockSong.new(File.dirname(__FILE__) + "/..", overflow_yaml)
33
+ test_songs[:overflow] = SongParser.new().parse(File.dirname(__FILE__) + "/..", overflow_yaml)
49
34
 
50
- test_songs[:from_code] = MockSong.new(File.dirname(__FILE__) + "/..")
35
+ test_songs[:from_code] = Song.new("test/sounds")
51
36
  verse = test_songs[:from_code].pattern :verse
52
37
  verse.track "bass.wav", kit.get_sample_data("bass.wav"), "X.......X......."
53
38
  verse.track "snare.wav", kit.get_sample_data("snare.wav"), "....X.......X..."
@@ -59,91 +44,20 @@ Verse:
59
44
  test_songs[:from_code].structure = [:verse, :chorus, :verse, :chorus, :chorus]
60
45
  test_songs[:from_code].kit = kit
61
46
 
62
- valid_yaml_string = "# An example song
63
-
64
- Song:
65
- Tempo: 99
66
- Structure:
67
- - Verse: x2
68
- - Chorus: x2
69
- - Verse: x2
70
- - Chorus: x4
71
- - Bridge: x1
72
- - Chorus: x4
73
-
74
- Verse:
75
- - test/sounds/bass_mono_8.wav: X...X...X...XX..X...X...XX..X...
76
- - test/sounds/snare_mono_8.wav: ..X...X...X...X.X...X...X...X...
77
- # Here is a comment
78
- - test/sounds/hh_closed_mono_8.wav: X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.
79
- - test/sounds/hh_open_mono_8.wav: X...............X..............X
80
- # Here is another comment
81
- Chorus:
82
- - test/sounds/bass_mono_8.wav: X...X...XXXXXXXXX...X...X...X...
83
- - test/sounds/snare_mono_8.wav: ...................X...X...X...X
84
- - test/sounds/hh_closed_mono_8.wav: X.X.XXX.X.X.XXX.X.X.XXX.X.X.XXX. # It's comment time
85
- - test/sounds/hh_open_mono_8.wav: ........X.......X.......X.......
86
- - test/sounds/ride_mono_8.wav: ....X...................X.......
87
-
88
-
89
- Bridge:
90
- - test/sounds/hh_closed_mono_8.wav: XX.XXX.XXX.XXX.XXX.XXX.XXX.XXX.X"
91
-
92
- test_songs[:from_valid_yaml_string] = MockSong.new(File.dirname(__FILE__) + "/..", valid_yaml_string)
93
-
94
47
  return test_songs
95
48
  end
96
49
 
97
50
  def test_initialize
98
- test_songs = generate_test_data
51
+ test_songs = generate_test_data()
99
52
 
100
53
  assert_equal(test_songs[:blank].structure, [])
101
54
  assert_equal(test_songs[:blank].tick_sample_length, (Song::SAMPLE_RATE * Song::SECONDS_PER_MINUTE) / DEFAULT_TEMPO / 4.0)
55
+
102
56
  assert_equal(test_songs[:no_structure].structure, [])
103
57
  assert_equal(test_songs[:no_structure].tick_sample_length, (Song::SAMPLE_RATE * Song::SECONDS_PER_MINUTE) / DEFAULT_TEMPO / 4.0)
58
+
104
59
  assert_equal(test_songs[:from_code].structure, [:verse, :chorus, :verse, :chorus, :chorus])
105
60
  assert_equal(test_songs[:from_code].tick_sample_length, (Song::SAMPLE_RATE * Song::SECONDS_PER_MINUTE) / DEFAULT_TEMPO / 4.0)
106
-
107
- assert_equal(test_songs[:from_valid_yaml_string].structure, [:verse, :verse, :chorus, :chorus, :verse, :verse, :chorus, :chorus, :chorus, :chorus, :bridge, :chorus, :chorus, :chorus, :chorus])
108
- assert_equal(test_songs[:from_valid_yaml_string].tempo, 99)
109
- assert_equal(test_songs[:from_valid_yaml_string].tick_sample_length, (Song::SAMPLE_RATE * Song::SECONDS_PER_MINUTE) / 99 / 4.0)
110
- assert_equal(test_songs[:from_valid_yaml_string].patterns.keys.map{|key| key.to_s}.sort, ["bridge", "chorus", "verse"])
111
- assert_equal(test_songs[:from_valid_yaml_string].patterns[:verse].tracks.length, 4)
112
- assert_equal(test_songs[:from_valid_yaml_string].patterns[:chorus].tracks.length, 5)
113
- assert_equal(test_songs[:from_valid_yaml_string].patterns[:bridge].tracks.length, 1)
114
- end
115
-
116
- def test_invalid_initialize
117
- invalid_tempo_yaml_string = "# Invalid tempo song
118
- Song:
119
- Tempo: 100a
120
- Structure:
121
- - Verse: x2
122
-
123
- Verse:
124
- - test/sounds/bass_mono_8.wav: X...X...X...XX..X...X...XX..X..."
125
- assert_raise(SongParseError) { song = MockSong.new(File.dirname(__FILE__) + "/..", invalid_tempo_yaml_string) }
126
-
127
- invalid_structure_yaml_string = "# Invalid structure song
128
- Song:
129
- Tempo: 100
130
- Structure:
131
- - Verse: x2
132
- - Chorus: x1
133
-
134
- Verse:
135
- - test/sounds/bass_mono_8.wav: X...X...X...XX..X...X...XX..X..."
136
- assert_raise(SongParseError) { song = MockSong.new(File.dirname(__FILE__) + "/..", invalid_structure_yaml_string) }
137
-
138
- invalid_repeats_yaml_string = " # Invalid structure song
139
- Song:
140
- Tempo: 100
141
- Structure:
142
- - Verse: x2a
143
-
144
- Verse:
145
- - test/sounds/bass_mono_8.wav: X...X...X...XX..X...X...XX..X..."
146
- assert_raise(SongParseError) { song = MockSong.new(File.dirname(__FILE__) + "/..", invalid_repeats_yaml_string) }
147
61
  end
148
62
 
149
63
  def test_total_tracks
@@ -0,0 +1,207 @@
1
+ $:.unshift File.join(File.dirname(__FILE__),'..','lib')
2
+
3
+ require 'test/includes'
4
+
5
+ class SongParserTest < Test::Unit::TestCase
6
+ def self.generate_test_data
7
+ kit = Kit.new("test/sounds")
8
+ kit.add("bass.wav", "bass_mono_8.wav")
9
+ kit.add("snare.wav", "snare_mono_8.wav")
10
+ kit.add("hh_closed.wav", "hh_closed_mono_8.wav")
11
+ kit.add("ride.wav", "ride_mono_8.wav")
12
+
13
+ test_songs = {}
14
+ base_path = File.dirname(__FILE__) + "/.."
15
+
16
+ no_tempo_yaml = "
17
+ Song:
18
+ Structure:
19
+ - Verse: x1
20
+
21
+ Verse:
22
+ - test/sounds/bass_mono_8.wav: X"
23
+ test_songs[:no_tempo] = SongParser.new().parse(base_path, no_tempo_yaml)
24
+
25
+ repeats_not_specified_yaml = "
26
+ Song:
27
+ Tempo: 100
28
+ Structure:
29
+ - Verse
30
+
31
+ Verse:
32
+ - test/sounds/bass_mono_8.wav: X"
33
+ test_songs[:repeats_not_specified] = SongParser.new().parse(base_path, repeats_not_specified_yaml)
34
+
35
+ overflow_yaml = "
36
+ Song:
37
+ Tempo: 100
38
+ Structure:
39
+ - Verse: x2
40
+
41
+ Verse:
42
+ - test/sounds/snare_mono_8.wav: ...X"
43
+ test_songs[:overflow] = SongParser.new().parse(base_path, overflow_yaml)
44
+
45
+ valid_yaml_string = "# An example song
46
+
47
+ Song:
48
+ Tempo: 99
49
+ Structure:
50
+ - Verse: x2
51
+ - Chorus: x2
52
+ - Verse: x2
53
+ - Chorus: x4
54
+ - Bridge: x1
55
+ - Undefined: x0 # This is legal as long as num repeats is 0.
56
+ - Chorus: x4
57
+
58
+ Verse:
59
+ - test/sounds/bass_mono_8.wav: X...X...X...XX..X...X...XX..X...
60
+ - test/sounds/snare_mono_8.wav: ..X...X...X...X.X...X...X...X...
61
+ # Here is a comment
62
+ - test/sounds/hh_closed_mono_8.wav: X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.
63
+ - test/sounds/hh_open_mono_8.wav: X...............X..............X
64
+ # Here is another comment
65
+ Chorus:
66
+ - test/sounds/bass_mono_8.wav: X...X...XXXXXXXXX...X...X...X...
67
+ - test/sounds/snare_mono_8.wav: ...................X...X...X...X
68
+ - test/sounds/hh_closed_mono_8.wav: X.X.XXX.X.X.XXX.X.X.XXX.X.X.XXX. # It's comment time
69
+ - test/sounds/hh_open_mono_8.wav: ........X.......X.......X.......
70
+ - test/sounds/ride_mono_8.wav: ....X...................X.......
71
+
72
+
73
+ Bridge:
74
+ - test/sounds/hh_closed_mono_8.wav: XX.XXX.XXX.XXX.XXX.XXX.XXX.XXX.X"
75
+ test_songs[:from_valid_yaml_string] = SongParser.new().parse(base_path, valid_yaml_string)
76
+
77
+ valid_yaml_string_with_kit = "# An example song
78
+
79
+ Song:
80
+ Tempo: 99
81
+ Kit:
82
+ - bass: test/sounds/bass_mono_8.wav
83
+ - snare: test/sounds/snare_mono_8.wav
84
+ - hhclosed: test/sounds/hh_closed_mono_8.wav
85
+ - hhopen: test/sounds/hh_open_mono_8.wav
86
+ Structure:
87
+ - Verse: x2
88
+ - Chorus: x2
89
+ - Verse: x2
90
+ - Chorus: x4
91
+ - Bridge: x1
92
+ - Undefined: x0 # This is legal as long as num repeats is 0.
93
+ - Chorus: x4
94
+
95
+ Verse:
96
+ - base: X...X...X...XX..X...X...XX..X...
97
+ - snare: ..X...X...X...X.X...X...X...X...
98
+ # Here is a comment
99
+ - hhclosed: X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.
100
+ - hhopen: X...............X..............X
101
+ # Here is another comment
102
+ Chorus:
103
+ - bass: X...X...XXXXXXXXX...X...X...X...
104
+ - snare: ...................X...X...X...X
105
+ - test/sounds/hh_closed_mono_8.wav: X.X.XXX.X.X.XXX.X.X.XXX.X.X.XXX. # It's comment time
106
+ - hhopen: ........X.......X.......X.......
107
+ - test/sounds/ride_mono_8.wav: ....X...................X.......
108
+
109
+ Bridge:
110
+ - hhclosed: XX.XXX.XXX.XXX.XXX.XXX.XXX.XXX.X"
111
+ test_songs[:from_valid_yaml_string_with_kit] = SongParser.new().parse(base_path, valid_yaml_string)
112
+
113
+ return test_songs
114
+ end
115
+
116
+ def test_valid_initialize
117
+ test_songs = SongParserTest.generate_test_data()
118
+
119
+ assert_equal(test_songs[:no_tempo].tempo, 120)
120
+ assert_equal(test_songs[:no_tempo].structure, [:verse])
121
+
122
+ assert_equal(test_songs[:repeats_not_specified].tempo, 100)
123
+ assert_equal(test_songs[:repeats_not_specified].structure, [:verse])
124
+
125
+ # These two songs should be the same, except that one uses a kit in the song header
126
+ # and the other doesn't.
127
+ [:from_valid_yaml_string, :from_valid_yaml_string_with_kit].each {|song_key|
128
+ song = test_songs[song_key]
129
+ assert_equal(song.structure, [:verse, :verse, :chorus, :chorus, :verse, :verse, :chorus, :chorus, :chorus, :chorus, :bridge, :chorus, :chorus, :chorus, :chorus])
130
+ assert_equal(song.tempo, 99)
131
+ assert_equal(song.tick_sample_length, (Song::SAMPLE_RATE * Song::SECONDS_PER_MINUTE) / 99 / 4.0)
132
+ assert_equal(song.patterns.keys.map{|key| key.to_s}.sort, ["bridge", "chorus", "verse"])
133
+ assert_equal(song.patterns[:verse].tracks.length, 4)
134
+ assert_equal(song.patterns[:chorus].tracks.length, 5)
135
+ assert_equal(song.patterns[:bridge].tracks.length, 1)
136
+ }
137
+ end
138
+
139
+ def test_invalid_initialize
140
+ no_header_yaml_string = "# Song with no header
141
+ Verse:
142
+ - test/sounds/bass_mono_8.wav: X...X...X...XX..X...X...XX..X..."
143
+ assert_raise(SongParseError) { song = SongParser.new().parse(File.dirname(__FILE__) + "/..", no_header_yaml_string) }
144
+
145
+ sound_doesnt_exist_yaml_string = "# Song with non-existent sound
146
+ Song:
147
+ Tempo: 100
148
+ Structure:
149
+ - Verse: x1
150
+
151
+ Verse:
152
+ - test/sounds/i_do_not_exist.wav: X...X..."
153
+ assert_raise(SongParseError) { song = SongParser.new().parse(File.dirname(__FILE__) + "/..", sound_doesnt_exist_yaml_string) }
154
+
155
+
156
+ sound_doesnt_exist_in_kit_yaml_string = "# Song with non-existent sound in Kit
157
+ Song:
158
+ Tempo: 100
159
+ Structure:
160
+ - Verse: x1
161
+ Kit:
162
+ - bad: test/sounds/i_do_not_exist.wav
163
+
164
+ Verse:
165
+ - bad: X...X..."
166
+ assert_raise(SongParseError) { song = SongParser.new().parse(File.dirname(__FILE__) + "/..", sound_doesnt_exist_in_kit_yaml_string) }
167
+
168
+ invalid_tempo_yaml_string = "# Song with invalid tempo
169
+ Song:
170
+ Tempo: 100a
171
+ Structure:
172
+ - Verse: x2
173
+
174
+ Verse:
175
+ - test/sounds/bass_mono_8.wav: X...X...X...XX..X...X...XX..X..."
176
+ assert_raise(SongParseError) { song = SongParser.new().parse(File.dirname(__FILE__) + "/..", invalid_tempo_yaml_string) }
177
+
178
+ invalid_structure_yaml_string = "# Song whose structure references non-existent pattern
179
+ Song:
180
+ Tempo: 100
181
+ Structure:
182
+ - Verse: x2
183
+ - Chorus: x1
184
+
185
+ Verse:
186
+ - test/sounds/bass_mono_8.wav: X...X...X...XX..X...X...XX..X..."
187
+ assert_raise(SongParseError) { song = SongParser.new().parse(File.dirname(__FILE__) + "/..", invalid_structure_yaml_string) }
188
+
189
+ no_structure_yaml_string = "# Song without a structure section in the header
190
+ Song:
191
+ Tempo: 100
192
+
193
+ Verse:
194
+ - test/sounds/bass_mono_8.wav: X...X...X...XX..X...X...XX..X..."
195
+ assert_raise(SongParseError) { song = SongParser.new().parse(File.dirname(__FILE__) + "/..", no_structure_yaml_string) }
196
+
197
+ invalid_repeats_yaml_string = "# Song with invalid number of repeats for pattern
198
+ Song:
199
+ Tempo: 100
200
+ Structure:
201
+ - Verse: x2a
202
+
203
+ Verse:
204
+ - test/sounds/bass_mono_8.wav: X...X...X...XX..X...X...XX..X..."
205
+ assert_raise(SongParseError) { song = SongParser.new().parse(File.dirname(__FILE__) + "/..", invalid_repeats_yaml_string) }
206
+ end
207
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: beats
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joel Strait
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2010-02-13 00:00:00 -05:00
12
+ date: 2010-04-12 00:00:00 -04:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -36,12 +36,14 @@ files:
36
36
  - lib/kit.rb
37
37
  - lib/pattern.rb
38
38
  - lib/song.rb
39
+ - lib/songparser.rb
39
40
  - lib/track.rb
40
41
  - bin/beats
41
42
  - test/includes.rb
42
43
  - test/kit_test.rb
43
44
  - test/pattern_test.rb
44
45
  - test/song_test.rb
46
+ - test/songparser_test.rb
45
47
  - test/sounds/bass_mono_16.wav
46
48
  - test/sounds/bass_mono_8.wav
47
49
  - test/sounds/bass_stereo_16.wav
@@ -83,6 +85,7 @@ test_files:
83
85
  - test/kit_test.rb
84
86
  - test/pattern_test.rb
85
87
  - test/song_test.rb
88
+ - test/songparser_test.rb
86
89
  - test/sounds/bass_mono_16.wav
87
90
  - test/sounds/bass_mono_8.wav
88
91
  - test/sounds/bass_stereo_16.wav