beats 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +14 -9
- data/bin/beats +4 -8
- data/lib/kit.rb +27 -13
- data/lib/pattern.rb +15 -4
- data/lib/song.rb +10 -110
- data/lib/songparser.rb +169 -0
- data/test/includes.rb +1 -0
- data/test/kit_test.rb +22 -17
- data/test/pattern_test.rb +5 -5
- data/test/song_test.rb +13 -99
- data/test/songparser_test.rb +207 -0
- metadata +5 -2
data/README.markdown
CHANGED
@@ -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
|
16
|
-
- snare
|
17
|
-
- hh_closed
|
18
|
-
-
|
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
|
22
|
-
- snare
|
23
|
-
- hh_closed
|
24
|
-
|
25
|
-
|
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.
|
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
|
-
|
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
|
-
|
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
|
-
|
11
|
-
|
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
|
14
|
-
@num_channels =
|
27
|
+
if wavefile.num_channels > @num_channels
|
28
|
+
@num_channels = wavefile.num_channels
|
15
29
|
end
|
16
|
-
if
|
17
|
-
@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
|
-
|
37
|
+
wavefile = @sounds[name]
|
24
38
|
|
25
|
-
if
|
39
|
+
if wavefile == nil
|
26
40
|
raise StandardError, "Kit doesn't contain sound '#{name}'."
|
27
41
|
else
|
28
|
-
|
29
|
-
|
42
|
+
wavefile.num_channels = @num_channels
|
43
|
+
wavefile.bits_per_sample = @bits_per_sample
|
30
44
|
|
31
|
-
return
|
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
|
data/lib/pattern.rb
CHANGED
@@ -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]
|
68
|
-
|
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
|
-
|
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)
|
data/lib/song.rb
CHANGED
@@ -1,20 +1,16 @@
|
|
1
|
-
class
|
1
|
+
class InvalidTempoError < RuntimeError; end
|
2
2
|
|
3
3
|
class Song
|
4
4
|
SAMPLE_RATE = 44100
|
5
5
|
SECONDS_PER_MINUTE = 60.0
|
6
|
-
|
6
|
+
SAMPLES_PER_MINUTE = SAMPLE_RATE * SECONDS_PER_MINUTE
|
7
|
+
DEFAULT_TEMPO = 120
|
7
8
|
|
8
|
-
def initialize(
|
9
|
-
self.tempo =
|
10
|
-
@
|
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
|
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 =
|
89
|
+
@tick_sample_length = SAMPLES_PER_MINUTE / new_tempo / 4.0
|
94
90
|
end
|
95
91
|
|
96
|
-
attr_reader :
|
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 = {}
|
data/lib/songparser.rb
ADDED
@@ -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
|
data/test/includes.rb
CHANGED
data/test/kit_test.rb
CHANGED
@@ -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
|
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", "
|
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", "
|
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", "
|
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", "
|
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", "
|
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", "
|
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", "
|
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", "
|
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", "
|
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", "
|
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", "
|
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", "
|
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)
|
data/test/pattern_test.rb
CHANGED
@@ -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", "
|
12
|
-
kit.add("snare.wav", "
|
13
|
-
kit.add("hh_closed.wav", "
|
14
|
-
kit.add("hh_open.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
|
|
data/test/song_test.rb
CHANGED
@@ -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", "
|
16
|
-
kit.add("snare.wav", "
|
17
|
-
kit.add("hh_closed.wav", "
|
18
|
-
kit.add("ride.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] =
|
17
|
+
test_songs[:blank] = Song.new("test/sounds")
|
23
18
|
|
24
|
-
test_songs[:no_structure] =
|
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] =
|
33
|
+
test_songs[:overflow] = SongParser.new().parse(File.dirname(__FILE__) + "/..", overflow_yaml)
|
49
34
|
|
50
|
-
test_songs[:from_code] =
|
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.
|
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-
|
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
|