beats 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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