beats 1.2.4 → 1.2.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +1 -1
- data/README.markdown +24 -4
- data/Rakefile +1 -1
- data/bin/beats +10 -15
- data/ext/mkrf_conf.rb +28 -0
- data/lib/beats.rb +12 -76
- data/lib/beats/audioengine.rb +163 -0
- data/lib/beats/audioutils.rb +74 -0
- data/lib/beats/beatsrunner.rb +76 -0
- data/lib/beats/kit.rb +187 -0
- data/lib/beats/pattern.rb +88 -0
- data/lib/beats/song.rb +162 -0
- data/lib/beats/songoptimizer.rb +162 -0
- data/lib/beats/songparser.rb +217 -0
- data/lib/beats/track.rb +57 -0
- data/lib/wavefile/cachingwriter.rb +1 -1
- data/test/audioengine_test.rb +5 -5
- data/test/cachingwriter_test.rb +1 -1
- data/test/fixtures/valid/foo.txt +18 -0
- data/test/includes.rb +2 -9
- data/test/integration_test.rb +20 -20
- data/test/kit_test.rb +28 -28
- data/test/pattern_test.rb +13 -13
- data/test/song_test.rb +23 -23
- data/test/songoptimizer_test.rb +25 -25
- data/test/songparser_test.rb +11 -11
- data/test/sounds/bass.wav +0 -0
- data/test/sounds/bass2.wav +0 -0
- data/test/track_test.rb +12 -12
- metadata +27 -22
- data/lib/audioengine.rb +0 -161
- data/lib/audioutils.rb +0 -73
- data/lib/kit.rb +0 -185
- data/lib/pattern.rb +0 -86
- data/lib/song.rb +0 -160
- data/lib/songoptimizer.rb +0 -160
- data/lib/songparser.rb +0 -216
- data/lib/track.rb +0 -55
@@ -0,0 +1,162 @@
|
|
1
|
+
module Beats
|
2
|
+
# This class is used to transform a Song object into an equivalent Song object whose
|
3
|
+
# sample data will be generated faster by the audio engine.
|
4
|
+
#
|
5
|
+
# The primary method is optimize(). Currently, it performs two optimizations:
|
6
|
+
#
|
7
|
+
# 1.) Breaks patterns into shorter patterns. Generating one long Pattern is generally
|
8
|
+
# slower than generating several short Patterns with the same combined length.
|
9
|
+
# 2.) Replaces Patterns which are equivalent (i.e. they have the same tracks with the
|
10
|
+
# same rhythms) with one canonical Pattern. This allows for better caching, by
|
11
|
+
# preventing the audio engine from generating the same sample data more than once.
|
12
|
+
#
|
13
|
+
# Note that step #1 actually performs double duty, because breaking Patterns into smaller
|
14
|
+
# pieces increases the likelihood there will be duplicates that can be combined.
|
15
|
+
class SongOptimizer
|
16
|
+
def initialize
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns a Song that will produce the same output as original_song, but should be
|
20
|
+
# generated faster.
|
21
|
+
def optimize(original_song, max_pattern_length)
|
22
|
+
# 1.) Create a new song, cloned from the original
|
23
|
+
optimized_song = original_song.copy_ignoring_patterns_and_flow()
|
24
|
+
|
25
|
+
# 2.) Subdivide patterns
|
26
|
+
optimized_song = subdivide_song_patterns(original_song, optimized_song, max_pattern_length)
|
27
|
+
|
28
|
+
# 3.) Prune duplicate patterns
|
29
|
+
optimized_song = prune_duplicate_patterns(optimized_song)
|
30
|
+
|
31
|
+
optimized_song
|
32
|
+
end
|
33
|
+
|
34
|
+
protected
|
35
|
+
|
36
|
+
# Splits the patterns of a Song into smaller patterns, each one with at most
|
37
|
+
# max_pattern_length steps. For example, if max_pattern_length is 4, then
|
38
|
+
# the following pattern:
|
39
|
+
#
|
40
|
+
# track1: X...X...X.
|
41
|
+
# track2: ..X.....X.
|
42
|
+
# track3: X.X.X.X.X.
|
43
|
+
#
|
44
|
+
# will be converted into the following 3 patterns:
|
45
|
+
#
|
46
|
+
# track1: X...
|
47
|
+
# track2: ..X.
|
48
|
+
# track3: X.X.
|
49
|
+
#
|
50
|
+
# track1: X...
|
51
|
+
# track3: X.X.
|
52
|
+
#
|
53
|
+
# track1: X.
|
54
|
+
# track2: X.
|
55
|
+
# track3: X.
|
56
|
+
#
|
57
|
+
# Note that if a track in a sub-divided pattern has no triggers (such as track2 in the
|
58
|
+
# 2nd pattern above), it will not be included in the new pattern.
|
59
|
+
def subdivide_song_patterns(original_song, optimized_song, max_pattern_length)
|
60
|
+
blank_track_pattern = '.' * max_pattern_length
|
61
|
+
|
62
|
+
# For each pattern, add a new pattern to new song every max_pattern_length steps
|
63
|
+
optimized_flow = {}
|
64
|
+
original_song.patterns.values.each do |pattern|
|
65
|
+
step_index = 0
|
66
|
+
optimized_flow[pattern.name] = []
|
67
|
+
|
68
|
+
while(pattern.tracks.values.first.rhythm[step_index] != nil) do
|
69
|
+
# TODO: Is this pattern 100% sufficient to prevent collisions between subdivided
|
70
|
+
# pattern names and existing patterns with numeric suffixes?
|
71
|
+
new_pattern = optimized_song.pattern("#{pattern.name}_#{step_index}".to_sym)
|
72
|
+
optimized_flow[pattern.name] << new_pattern.name
|
73
|
+
pattern.tracks.values.each do |track|
|
74
|
+
sub_track_pattern = track.rhythm[step_index...(step_index + max_pattern_length)]
|
75
|
+
|
76
|
+
if sub_track_pattern != blank_track_pattern
|
77
|
+
new_pattern.track(track.name, sub_track_pattern)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# If no track has a trigger during this step pattern, add a blank track.
|
82
|
+
# Otherwise, this pattern will have no steps, and no sound will be generated,
|
83
|
+
# causing the pattern to be "compacted away".
|
84
|
+
if new_pattern.tracks.empty?
|
85
|
+
new_pattern.track("placeholder", blank_track_pattern)
|
86
|
+
end
|
87
|
+
|
88
|
+
step_index += max_pattern_length
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Replace the Song's flow to reference the new sub-divided patterns
|
93
|
+
# instead of the old patterns.
|
94
|
+
optimized_flow = original_song.flow.map do |original_pattern|
|
95
|
+
optimized_flow[original_pattern]
|
96
|
+
end
|
97
|
+
optimized_song.flow = optimized_flow.flatten
|
98
|
+
|
99
|
+
optimized_song
|
100
|
+
end
|
101
|
+
|
102
|
+
|
103
|
+
# Replaces any Patterns that are duplicates (i.e., each track uses the same sound and has
|
104
|
+
# the same rhythm) with a single canonical pattern.
|
105
|
+
#
|
106
|
+
# The benefit of this is that it allows more effective caching. For example, suppose Pattern A
|
107
|
+
# and Pattern B are equivalent. If Pattern A gets generated first, it will be cached. When
|
108
|
+
# Pattern B gets generated, it will be generated from scratch instead of using Pattern A's
|
109
|
+
# cached data. Consolidating duplicates into one prevents this from happening.
|
110
|
+
#
|
111
|
+
# Duplicate Patterns are more likely to occur after calling subdivide_song_patterns().
|
112
|
+
def prune_duplicate_patterns(song)
|
113
|
+
pattern_replacements = determine_pattern_replacements(song.patterns)
|
114
|
+
|
115
|
+
# Update flow to remove references to duplicates
|
116
|
+
new_flow = song.flow
|
117
|
+
pattern_replacements.each do |duplicate, replacement|
|
118
|
+
new_flow = new_flow.map do |pattern_name|
|
119
|
+
(pattern_name == duplicate) ? replacement : pattern_name
|
120
|
+
end
|
121
|
+
end
|
122
|
+
song.flow = new_flow
|
123
|
+
|
124
|
+
# This isn't strictly necessary, but makes resulting songs easier to read for debugging purposes.
|
125
|
+
song.remove_unused_patterns()
|
126
|
+
|
127
|
+
song
|
128
|
+
end
|
129
|
+
|
130
|
+
|
131
|
+
# Examines a set of patterns definitions, determining which ones have the same tracks with the same
|
132
|
+
# rhythms. Then constructs a hash of pattern => pattern indicating that all occurances in the flow
|
133
|
+
# of the key should be replaced with the value, so that the other equivalent definitions can be pruned
|
134
|
+
# from the song (and hence their sample data doesn't need to be generated).
|
135
|
+
def determine_pattern_replacements(patterns)
|
136
|
+
seen_patterns = []
|
137
|
+
replacements = {}
|
138
|
+
|
139
|
+
# Pattern names are sorted to ensure predictable pattern replacement. Makes tests easier to write.
|
140
|
+
# Sort function added manually because Ruby 1.8 doesn't know how to sort symbols...
|
141
|
+
pattern_names = patterns.keys.sort {|x, y| x.to_s <=> y.to_s }
|
142
|
+
|
143
|
+
# Detect duplicates
|
144
|
+
pattern_names.each do |pattern_name|
|
145
|
+
pattern = patterns[pattern_name]
|
146
|
+
found_duplicate = false
|
147
|
+
seen_patterns.each do |seen_pattern|
|
148
|
+
if !found_duplicate && pattern.same_tracks_as?(seen_pattern)
|
149
|
+
replacements[pattern.name.to_sym] = seen_pattern.name.to_sym
|
150
|
+
found_duplicate = true
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
if !found_duplicate
|
155
|
+
seen_patterns << pattern
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
replacements
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,217 @@
|
|
1
|
+
module Beats
|
2
|
+
class SongParseError < RuntimeError; end
|
3
|
+
|
4
|
+
|
5
|
+
# This class is used to parse a raw YAML song definition into domain objects (i.e.
|
6
|
+
# Song, Pattern, Track, and Kit). These domain objects can then be used by AudioEngine
|
7
|
+
# to generate the actual audio data that is saved to disk.
|
8
|
+
#
|
9
|
+
# The sole public method is parse(). It takes a raw YAML string and returns a Song and
|
10
|
+
# Kit object (or raises an error if the YAML string couldn't be parsed correctly).
|
11
|
+
class SongParser
|
12
|
+
DONT_USE_STRUCTURE_WARNING =
|
13
|
+
"\n" +
|
14
|
+
"WARNING! This song contains a 'Structure' section in the header.\n" +
|
15
|
+
"As of BEATS 1.2.1, the 'Structure' section should be renamed 'Flow'.\n" +
|
16
|
+
"You should change your song file, in a future version using 'Structure' will cause an error.\n"
|
17
|
+
|
18
|
+
NO_SONG_HEADER_ERROR_MSG =
|
19
|
+
"Song must have a header. Here's an example:
|
20
|
+
|
21
|
+
Song:
|
22
|
+
Tempo: 120
|
23
|
+
Flow:
|
24
|
+
- Verse: x2
|
25
|
+
- Chorus: x2"
|
26
|
+
|
27
|
+
def initialize
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
# Parses a raw YAML song definition and converts it into a Song and Kit object.
|
32
|
+
def parse(base_path, raw_yaml_string)
|
33
|
+
raw_song_components = hashify_raw_yaml(raw_yaml_string)
|
34
|
+
|
35
|
+
unless raw_song_components[:folder].nil?
|
36
|
+
base_path = raw_song_components[:folder]
|
37
|
+
end
|
38
|
+
|
39
|
+
song = Song.new()
|
40
|
+
|
41
|
+
# 1.) Set tempo
|
42
|
+
begin
|
43
|
+
unless raw_song_components[:tempo].nil?
|
44
|
+
song.tempo = raw_song_components[:tempo]
|
45
|
+
end
|
46
|
+
rescue InvalidTempoError => detail
|
47
|
+
raise SongParseError, "#{detail}"
|
48
|
+
end
|
49
|
+
|
50
|
+
# 2.) Build the kit
|
51
|
+
begin
|
52
|
+
kit = build_kit(base_path, raw_song_components[:kit], raw_song_components[:patterns])
|
53
|
+
rescue SoundFileNotFoundError => detail
|
54
|
+
raise SongParseError, "#{detail}"
|
55
|
+
rescue InvalidSoundFormatError => detail
|
56
|
+
raise SongParseError, "#{detail}"
|
57
|
+
end
|
58
|
+
|
59
|
+
# 3.) Load patterns
|
60
|
+
add_patterns_to_song(song, raw_song_components[:patterns])
|
61
|
+
|
62
|
+
# 4.) Set flow
|
63
|
+
if raw_song_components[:flow].nil?
|
64
|
+
raise SongParseError, "Song must have a Flow section in the header."
|
65
|
+
else
|
66
|
+
set_song_flow(song, raw_song_components[:flow])
|
67
|
+
end
|
68
|
+
|
69
|
+
return song, kit
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
|
76
|
+
def hashify_raw_yaml(raw_yaml_string)
|
77
|
+
begin
|
78
|
+
raw_song_definition = YAML.load(raw_yaml_string)
|
79
|
+
rescue ArgumentError => detail
|
80
|
+
raise SongParseError, "Syntax error in YAML file"
|
81
|
+
end
|
82
|
+
|
83
|
+
raw_song_components = {}
|
84
|
+
raw_song_components[:full_definition] = downcase_hash_keys(raw_song_definition)
|
85
|
+
|
86
|
+
unless raw_song_components[:full_definition]["song"].nil?
|
87
|
+
raw_song_components[:header] = downcase_hash_keys(raw_song_components[:full_definition]["song"])
|
88
|
+
else
|
89
|
+
raise SongParseError, NO_SONG_HEADER_ERROR_MSG
|
90
|
+
end
|
91
|
+
raw_song_components[:tempo] = raw_song_components[:header]["tempo"]
|
92
|
+
raw_song_components[:folder] = raw_song_components[:header]["folder"]
|
93
|
+
raw_song_components[:kit] = raw_song_components[:header]["kit"]
|
94
|
+
|
95
|
+
raw_flow = raw_song_components[:header]["flow"]
|
96
|
+
raw_structure = raw_song_components[:header]["structure"]
|
97
|
+
unless raw_flow.nil?
|
98
|
+
raw_song_components[:flow] = raw_flow
|
99
|
+
else
|
100
|
+
unless raw_structure.nil?
|
101
|
+
puts DONT_USE_STRUCTURE_WARNING
|
102
|
+
end
|
103
|
+
|
104
|
+
raw_song_components[:flow] = raw_structure
|
105
|
+
end
|
106
|
+
|
107
|
+
raw_song_components[:patterns] = raw_song_components[:full_definition].reject {|k, v| k == "song"}
|
108
|
+
|
109
|
+
return raw_song_components
|
110
|
+
end
|
111
|
+
|
112
|
+
|
113
|
+
def build_kit(base_path, raw_kit, raw_patterns)
|
114
|
+
kit_items = {}
|
115
|
+
|
116
|
+
# Add sounds defined in the Kit section of the song header
|
117
|
+
# Converts [{a=>1}, {b=>2}, {c=>3}] from raw YAML to {a=>1, b=>2, c=>3}
|
118
|
+
# TODO: Raise error is same name is defined more than once in the Kit
|
119
|
+
unless raw_kit.nil?
|
120
|
+
raw_kit.each do |kit_item|
|
121
|
+
kit_items[kit_item.keys.first] = kit_item.values.first
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# Add sounds not defined in Kit section, but used in individual tracks
|
126
|
+
# TODO Investigate detecting duplicate keys already defined in the Kit section, as this could possibly
|
127
|
+
# result in a performance improvement when the sound has to be converted to a different bit rate/num channels,
|
128
|
+
# as well as use less memory.
|
129
|
+
raw_patterns.keys.each do |key|
|
130
|
+
track_list = raw_patterns[key]
|
131
|
+
|
132
|
+
unless track_list.nil?
|
133
|
+
track_list.each do |track_definition|
|
134
|
+
track_name = track_definition.keys.first
|
135
|
+
track_path = track_name
|
136
|
+
|
137
|
+
if track_name != Pattern::FLOW_TRACK_NAME && kit_items[track_name].nil?
|
138
|
+
kit_items[track_name] = track_path
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
Kit.new(base_path, kit_items)
|
145
|
+
end
|
146
|
+
|
147
|
+
|
148
|
+
def add_patterns_to_song(song, raw_patterns)
|
149
|
+
raw_patterns.keys.each do |key|
|
150
|
+
new_pattern = song.pattern key.to_sym
|
151
|
+
|
152
|
+
track_list = raw_patterns[key]
|
153
|
+
# TODO Also raise error if only there is only 1 track and it's a flow track
|
154
|
+
if track_list.nil?
|
155
|
+
# TODO: Use correct capitalization of pattern name in error message
|
156
|
+
# TODO: Possibly allow if pattern not referenced in the Flow, or has 0 repeats?
|
157
|
+
raise SongParseError, "Pattern '#{key}' has no tracks. It needs at least one."
|
158
|
+
end
|
159
|
+
|
160
|
+
# TODO: What if there is more than one flow? Raise error, or have last one win?
|
161
|
+
track_list.each do |track_definition|
|
162
|
+
track_name = track_definition.keys.first
|
163
|
+
|
164
|
+
# Handle case where no track rhythm is specified (i.e. "- foo.wav:" instead of "- foo.wav: X.X.X.X.")
|
165
|
+
track_definition[track_name] ||= ""
|
166
|
+
|
167
|
+
new_pattern.track track_name, track_definition[track_name]
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
|
173
|
+
def set_song_flow(song, raw_flow)
|
174
|
+
flow = []
|
175
|
+
|
176
|
+
raw_flow.each{|pattern_item|
|
177
|
+
if pattern_item.class == String
|
178
|
+
pattern_item = {pattern_item => "x1"}
|
179
|
+
end
|
180
|
+
|
181
|
+
pattern_name = pattern_item.keys.first
|
182
|
+
pattern_name_sym = pattern_name.downcase.to_sym
|
183
|
+
|
184
|
+
# Convert the number of repeats from a String such as "x4" into an integer such as 4.
|
185
|
+
multiples_str = pattern_item[pattern_name]
|
186
|
+
multiples_str.slice!(0)
|
187
|
+
multiples = multiples_str.to_i
|
188
|
+
|
189
|
+
unless multiples_str.match(/[^0-9]/).nil?
|
190
|
+
raise SongParseError,
|
191
|
+
"'#{multiples_str}' is an invalid number of repeats for pattern '#{pattern_name}'. Number of repeats should be a whole number."
|
192
|
+
else
|
193
|
+
if multiples < 0
|
194
|
+
raise SongParseError, "'#{multiples_str}' is an invalid number of repeats for pattern '#{pattern_name}'. Must be 0 or greater."
|
195
|
+
elsif multiples > 0 && !song.patterns.has_key?(pattern_name_sym)
|
196
|
+
# This test is purposefully designed to only throw an error if the number of repeats is greater
|
197
|
+
# than 0. This allows you to specify an undefined pattern in the flow with "x0" repeats.
|
198
|
+
# This can be convenient for defining the flow before all patterns have been added to the song file.
|
199
|
+
raise SongParseError, "Song flow includes non-existent pattern: #{pattern_name}."
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
multiples.times { flow << pattern_name_sym }
|
204
|
+
}
|
205
|
+
song.flow = flow
|
206
|
+
end
|
207
|
+
|
208
|
+
|
209
|
+
# Converts all hash keys to be lowercase
|
210
|
+
def downcase_hash_keys(hash)
|
211
|
+
return hash.inject({}) do |new_hash, pair|
|
212
|
+
new_hash[pair.first.downcase] = pair.last
|
213
|
+
new_hash
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
data/lib/beats/track.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
module Beats
|
2
|
+
class InvalidRhythmError < RuntimeError; end
|
3
|
+
|
4
|
+
|
5
|
+
# Domain object which models a kit sound playing a rhythm. For example,
|
6
|
+
# a bass drum playing every quarter note for two measures.
|
7
|
+
#
|
8
|
+
# This object is like sheet music; the AudioEngine is responsible creating actual
|
9
|
+
# audio data for a Track (with the help of a Kit).
|
10
|
+
class Track
|
11
|
+
REST = "."
|
12
|
+
BEAT = "X"
|
13
|
+
BARLINE = "|"
|
14
|
+
|
15
|
+
def initialize(name, rhythm)
|
16
|
+
# TODO: Add validation for input parameters
|
17
|
+
@name = name
|
18
|
+
self.rhythm = rhythm
|
19
|
+
end
|
20
|
+
|
21
|
+
# TODO: What to have this invoked when setting like this?
|
22
|
+
# track.rhythm[x..y] = whatever
|
23
|
+
def rhythm=(rhythm)
|
24
|
+
@rhythm = rhythm.delete(BARLINE)
|
25
|
+
beats = []
|
26
|
+
|
27
|
+
beat_length = 0
|
28
|
+
#rhythm.each_char{|ch|
|
29
|
+
@rhythm.each_byte do |ch|
|
30
|
+
ch = ch.chr
|
31
|
+
if ch == BEAT
|
32
|
+
beats << beat_length
|
33
|
+
beat_length = 1
|
34
|
+
elsif ch == REST
|
35
|
+
beat_length += 1
|
36
|
+
else
|
37
|
+
raise InvalidRhythmError, "Track #{@name} has an invalid rhythm: '#{rhythm}'. Can only contain '#{BEAT}', '#{REST}' or '#{BARLINE}'"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
if beat_length > 0
|
42
|
+
beats << beat_length
|
43
|
+
end
|
44
|
+
if beats == []
|
45
|
+
beats = [0]
|
46
|
+
end
|
47
|
+
@beats = beats
|
48
|
+
end
|
49
|
+
|
50
|
+
def step_count
|
51
|
+
@rhythm.length
|
52
|
+
end
|
53
|
+
|
54
|
+
attr_accessor :name
|
55
|
+
attr_reader :rhythm, :beats
|
56
|
+
end
|
57
|
+
end
|
data/test/audioengine_test.rb
CHANGED
@@ -28,15 +28,15 @@ class AudioEngineTest < Test::Unit::TestCase
|
|
28
28
|
test_engines = {}
|
29
29
|
base_path = File.dirname(__FILE__) + "/.."
|
30
30
|
song_parser = SongParser.new()
|
31
|
-
|
31
|
+
|
32
32
|
test_engines[:blank] = AudioEngine.new(Song.new(), Kit.new(base_path, {}))
|
33
33
|
|
34
34
|
FIXTURES.each do |fixture_name|
|
35
35
|
song, kit = song_parser.parse(base_path, File.read("test/fixtures/valid/#{fixture_name}.txt"))
|
36
36
|
test_engines[fixture_name] = AudioEngine.new(song, kit)
|
37
37
|
end
|
38
|
-
|
39
|
-
|
38
|
+
|
39
|
+
test_engines
|
40
40
|
end
|
41
41
|
|
42
42
|
def test_initialize
|
@@ -142,7 +142,7 @@ class AudioEngineTest < Test::Unit::TestCase
|
|
142
142
|
#helper_generate_track_sample_data kit, "XX", 1, [-100, -100], [200, 300, -400]
|
143
143
|
|
144
144
|
# 3C.) Tick sample length is shorter than the sound sample data, but not by an integer amount.
|
145
|
-
#
|
145
|
+
#
|
146
146
|
# Each step of 1.83 samples should end on the following boundaries:
|
147
147
|
# Tick: 1, 2, 3, 4, 5, 6
|
148
148
|
# Raw: 0.0, 1.83, 3.66, 5.49, 7.32, 9.15, 10.98
|
@@ -162,7 +162,7 @@ class AudioEngineTest < Test::Unit::TestCase
|
|
162
162
|
engine = MockAudioEngine.new(Song.new(), kit)
|
163
163
|
engine.step_sample_length = step_sample_length
|
164
164
|
actual = engine.generate_track_sample_data(track, kit.get_sample_data("S"))
|
165
|
-
|
165
|
+
|
166
166
|
assert_equal(Hash, actual.class)
|
167
167
|
assert_equal(["overflow", "primary"], actual.keys.map{|key| key.to_s}.sort)
|
168
168
|
assert_equal(expected_primary, actual[:primary])
|