beats 1.1.0 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +29 -4
- data/bin/beats +36 -71
- data/lib/beats.rb +57 -0
- data/lib/beatswavefile.rb +93 -0
- data/lib/kit.rb +67 -11
- data/lib/pattern.rb +131 -86
- data/lib/song.rb +145 -114
- data/lib/songoptimizer.rb +154 -0
- data/lib/songparser.rb +40 -28
- data/lib/songsplitter.rb +38 -0
- data/lib/track.rb +33 -31
- data/lib/wavefile.rb +475 -0
- data/test/examples/combined.wav +0 -0
- data/test/examples/split-agogo_high.wav +0 -0
- data/test/examples/split-bass.wav +0 -0
- data/test/examples/split-hh_closed.wav +0 -0
- data/test/examples/split-snare.wav +0 -0
- data/test/examples/split-tom2.wav +0 -0
- data/test/examples/split-tom4.wav +0 -0
- data/test/fixtures/expected_output/example_combined_mono_16.wav +0 -0
- data/test/fixtures/expected_output/example_combined_mono_8.wav +0 -0
- data/test/fixtures/expected_output/example_combined_stereo_16.wav +0 -0
- data/test/fixtures/expected_output/example_combined_stereo_8.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_16-agogo.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_16-bass.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_16-hh_closed.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_16-snare.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_16-tom2_mono_16.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_16-tom4_mono_16.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_8-agogo.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_8-bass.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_8-hh_closed.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_8-snare.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_8-tom2_mono_8.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_8-tom4_mono_8.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_16-agogo.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_16-bass.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_16-hh_closed.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_16-snare.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_16-tom2_stereo_16.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_16-tom4_stereo_16.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_8-agogo.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_8-bass.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_8-hh_closed.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_8-snare.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_8-tom2_stereo_8.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_8-tom4_stereo_8.wav +0 -0
- data/test/fixtures/invalid/bad_repeat_count.txt +8 -0
- data/test/fixtures/invalid/bad_rhythm.txt +9 -0
- data/test/fixtures/invalid/bad_structure.txt +9 -0
- data/test/fixtures/invalid/bad_tempo.txt +8 -0
- data/test/fixtures/invalid/no_header.txt +3 -0
- data/test/fixtures/invalid/no_structure.txt +6 -0
- data/test/fixtures/invalid/pattern_with_no_tracks.txt +12 -0
- data/test/fixtures/invalid/sound_in_kit_not_found.txt +10 -0
- data/test/fixtures/invalid/sound_in_track_not_found.txt +8 -0
- data/test/fixtures/invalid/template.txt +31 -0
- data/test/fixtures/valid/example_mono_16.txt +28 -0
- data/test/fixtures/valid/example_mono_8.txt +28 -0
- data/test/fixtures/valid/example_no_kit.txt +30 -0
- data/test/fixtures/valid/example_stereo_16.txt +28 -0
- data/test/fixtures/valid/example_stereo_8.txt +28 -0
- data/test/fixtures/valid/example_with_empty_track.txt +10 -0
- data/test/fixtures/valid/example_with_kit.txt +34 -0
- data/test/fixtures/valid/no_tempo.txt +8 -0
- data/test/fixtures/valid/pattern_with_overflow.txt +9 -0
- data/test/fixtures/valid/repeats_not_specified.txt +10 -0
- data/test/fixtures/yaml/song_yaml.txt +30 -0
- data/test/includes.rb +11 -4
- data/test/integration.rb +100 -0
- data/test/kit_test.rb +39 -39
- data/test/pattern_test.rb +119 -71
- data/test/song_test.rb +87 -62
- data/test/songoptimizer_test.rb +162 -0
- data/test/songparser_test.rb +36 -165
- data/test/sounds/agogo_high_mono_16.wav +0 -0
- data/test/sounds/agogo_high_mono_8.wav +0 -0
- data/test/sounds/agogo_high_stereo_16.wav +0 -0
- data/test/sounds/agogo_high_stereo_8.wav +0 -0
- data/test/sounds/agogo_low_mono_16.wav +0 -0
- data/test/sounds/agogo_low_mono_8.wav +0 -0
- data/test/sounds/agogo_low_stereo_16.wav +0 -0
- data/test/sounds/agogo_low_stereo_8.wav +0 -0
- data/test/sounds/bass2_mono_16.wav +0 -0
- data/test/sounds/bass2_mono_8.wav +0 -0
- data/test/sounds/bass2_stereo_16.wav +0 -0
- data/test/sounds/bass2_stereo_8.wav +0 -0
- data/test/sounds/bass_mono_8.wav +0 -0
- data/test/sounds/bass_stereo_16.wav +0 -0
- data/test/sounds/bass_stereo_8.wav +0 -0
- data/test/sounds/clave_high_mono_16.wav +0 -0
- data/test/sounds/clave_high_mono_8.wav +0 -0
- data/test/sounds/clave_high_stereo_16.wav +0 -0
- data/test/sounds/clave_high_stereo_8.wav +0 -0
- data/test/sounds/clave_low_mono_16.wav +0 -0
- data/test/sounds/clave_low_mono_8.wav +0 -0
- data/test/sounds/clave_low_stereo_16.wav +0 -0
- data/test/sounds/clave_low_stereo_8.wav +0 -0
- data/test/sounds/conga_high_mono_16.wav +0 -0
- data/test/sounds/conga_high_mono_8.wav +0 -0
- data/test/sounds/conga_high_stereo_16.wav +0 -0
- data/test/sounds/conga_high_stereo_8.wav +0 -0
- data/test/sounds/conga_low_mono_16.wav +0 -0
- data/test/sounds/conga_low_mono_8.wav +0 -0
- data/test/sounds/conga_low_stereo_16.wav +0 -0
- data/test/sounds/conga_low_stereo_8.wav +0 -0
- data/test/sounds/cowbell_high_mono_16.wav +0 -0
- data/test/sounds/cowbell_high_mono_8.wav +0 -0
- data/test/sounds/cowbell_high_stereo_16.wav +0 -0
- data/test/sounds/cowbell_high_stereo_8.wav +0 -0
- data/test/sounds/cowbell_low_mono_16.wav +0 -0
- data/test/sounds/cowbell_low_mono_8.wav +0 -0
- data/test/sounds/cowbell_low_stereo_16.wav +0 -0
- data/test/sounds/cowbell_low_stereo_8.wav +0 -0
- data/test/sounds/hh_closed_mono_16.wav +0 -0
- data/test/sounds/hh_closed_mono_8.wav +0 -0
- data/test/sounds/hh_closed_stereo_16.wav +0 -0
- data/test/sounds/hh_closed_stereo_8.wav +0 -0
- data/test/sounds/hh_open_mono_16.wav +0 -0
- data/test/sounds/hh_open_mono_8.wav +0 -0
- data/test/sounds/hh_open_stereo_16.wav +0 -0
- data/test/sounds/hh_open_stereo_8.wav +0 -0
- data/test/sounds/ride_mono_16.wav +0 -0
- data/test/sounds/ride_mono_8.wav +0 -0
- data/test/sounds/ride_stereo_16.wav +0 -0
- data/test/sounds/ride_stereo_8.wav +0 -0
- data/test/sounds/rim_mono_16.wav +0 -0
- data/test/sounds/rim_mono_8.wav +0 -0
- data/test/sounds/rim_stereo_16.wav +0 -0
- data/test/sounds/rim_stereo_8.wav +0 -0
- data/test/sounds/sine-mono-8bit.wav +0 -0
- data/test/sounds/snare2_mono_16.wav +0 -0
- data/test/sounds/snare2_mono_8.wav +0 -0
- data/test/sounds/snare2_stereo_16.wav +0 -0
- data/test/sounds/snare2_stereo_8.wav +0 -0
- data/test/sounds/snare_mono_16.wav +0 -0
- data/test/sounds/snare_mono_8.wav +0 -0
- data/test/sounds/snare_stereo_16.wav +0 -0
- data/test/sounds/snare_stereo_8.wav +0 -0
- data/test/sounds/tom1_mono_16.wav +0 -0
- data/test/sounds/tom1_mono_8.wav +0 -0
- data/test/sounds/tom1_stereo_16.wav +0 -0
- data/test/sounds/tom1_stereo_8.wav +0 -0
- data/test/sounds/tom2_mono_16.wav +0 -0
- data/test/sounds/tom2_mono_8.wav +0 -0
- data/test/sounds/tom2_stereo_16.wav +0 -0
- data/test/sounds/tom2_stereo_8.wav +0 -0
- data/test/sounds/tom3_mono_16.wav +0 -0
- data/test/sounds/tom3_mono_8.wav +0 -0
- data/test/sounds/tom3_stereo_16.wav +0 -0
- data/test/sounds/tom3_stereo_8.wav +0 -0
- data/test/sounds/tom4_mono_16.wav +0 -0
- data/test/sounds/tom4_mono_8.wav +0 -0
- data/test/sounds/tom4_stereo_16.wav +0 -0
- data/test/sounds/tom4_stereo_8.wav +0 -0
- data/test/sounds/tone.wav +0 -0
- data/test/track_test.rb +78 -72
- metadata +277 -15
data/lib/songparser.rb
CHANGED
@@ -10,7 +10,7 @@ class SongParser
|
|
10
10
|
- Verse: x2
|
11
11
|
- Chorus: x2"
|
12
12
|
|
13
|
-
def initialize
|
13
|
+
def initialize
|
14
14
|
end
|
15
15
|
|
16
16
|
def parse(base_path, definition = nil)
|
@@ -40,7 +40,7 @@ class SongParser
|
|
40
40
|
add_patterns_to_song(song, raw_song_components[:patterns])
|
41
41
|
|
42
42
|
# 4.) Set structure
|
43
|
-
if
|
43
|
+
if raw_song_components[:structure] == nil
|
44
44
|
raise SongParseError, "Song must have a Structure section in the header."
|
45
45
|
else
|
46
46
|
set_song_structure(song, raw_song_components[:structure])
|
@@ -51,16 +51,15 @@ class SongParser
|
|
51
51
|
|
52
52
|
private
|
53
53
|
|
54
|
-
#
|
55
|
-
# Also, is "canonicalize" a word?
|
54
|
+
# Is "canonicalize" a word?
|
56
55
|
def canonicalize_definition(definition)
|
57
|
-
if
|
56
|
+
if definition.class == String
|
58
57
|
begin
|
59
58
|
raw_song_definition = YAML.load(definition)
|
60
59
|
rescue ArgumentError => detail
|
61
60
|
raise SongParseError, "Syntax error in YAML file"
|
62
61
|
end
|
63
|
-
elsif
|
62
|
+
elsif definition.class == Hash
|
64
63
|
raw_song_definition = definition
|
65
64
|
else
|
66
65
|
raise SongParseError, "Invalid song input"
|
@@ -73,7 +72,7 @@ private
|
|
73
72
|
raw_song_components = {}
|
74
73
|
raw_song_components[:full_definition] = downcase_hash_keys(raw_song_definition)
|
75
74
|
|
76
|
-
if
|
75
|
+
if raw_song_components[:full_definition]["song"] != nil
|
77
76
|
raw_song_components[:header] = downcase_hash_keys(raw_song_components[:full_definition]["song"])
|
78
77
|
else
|
79
78
|
raise SongParseError, NO_SONG_HEADER_ERROR_MSG
|
@@ -90,46 +89,59 @@ private
|
|
90
89
|
kit = Kit.new(base_path)
|
91
90
|
|
92
91
|
# Add sounds defined in the Kit section of the song header
|
93
|
-
|
94
|
-
raw_kit.each
|
92
|
+
unless raw_kit == nil
|
93
|
+
raw_kit.each do |kit_item|
|
95
94
|
kit.add(kit_item.keys.first, kit_item.values.first)
|
96
|
-
|
95
|
+
end
|
97
96
|
end
|
98
97
|
|
99
98
|
# Add sounds not defined in Kit section, but used in individual tracks
|
100
99
|
# TODO Investigate detecting duplicate keys already defined in the Kit section, as this could possibly
|
101
100
|
# result in a performance improvement when the sound has to be converted to a different bit rate/num channels,
|
102
101
|
# as well as use less memory.
|
103
|
-
raw_patterns.keys.each
|
102
|
+
raw_patterns.keys.each do |key|
|
104
103
|
track_list = raw_patterns[key]
|
105
|
-
|
106
|
-
|
107
|
-
|
104
|
+
|
105
|
+
unless track_list == nil
|
106
|
+
track_list.each do |track_definition|
|
107
|
+
track_name = track_definition.keys.first
|
108
|
+
track_path = track_name
|
108
109
|
|
109
|
-
|
110
|
-
|
111
|
-
|
110
|
+
kit.add(track_name, track_path)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
112
114
|
|
113
115
|
return kit
|
114
116
|
end
|
115
117
|
|
116
118
|
def add_patterns_to_song(song, raw_patterns)
|
117
|
-
raw_patterns.keys.each
|
119
|
+
raw_patterns.keys.each do |key|
|
118
120
|
new_pattern = song.pattern key.to_sym
|
119
121
|
|
120
122
|
track_list = raw_patterns[key]
|
121
|
-
track_list
|
123
|
+
if track_list == nil
|
124
|
+
# TODO: Use correct capitalization of pattern name in error message
|
125
|
+
# TODO: Possibly allow if pattern not referenced in the Structure, or has 0 repeats?
|
126
|
+
raise SongParseError, "Pattern '#{key}' has no tracks. It needs at least one."
|
127
|
+
end
|
128
|
+
|
129
|
+
track_list.each do |track_definition|
|
122
130
|
track_name = track_definition.keys.first
|
123
|
-
|
124
|
-
|
125
|
-
|
131
|
+
|
132
|
+
# Handle case where no track pattern is specified (i.e. "- foo.wav:" instead of "- foo.wav: X.X.X.X.")
|
133
|
+
track_definition[track_name] ||= ""
|
134
|
+
|
135
|
+
new_pattern.track track_name, song.kit.get_sample_data(track_name), track_definition[track_name]
|
136
|
+
end
|
137
|
+
end
|
126
138
|
end
|
127
139
|
|
128
140
|
def set_song_structure(song, raw_structure)
|
129
141
|
structure = []
|
130
142
|
|
131
143
|
raw_structure.each{|pattern_item|
|
132
|
-
if
|
144
|
+
if pattern_item.class == String
|
133
145
|
pattern_item = {pattern_item => "x1"}
|
134
146
|
end
|
135
147
|
|
@@ -141,12 +153,12 @@ private
|
|
141
153
|
multiples_str.slice!(0)
|
142
154
|
multiples = multiples_str.to_i
|
143
155
|
|
144
|
-
|
156
|
+
unless multiples_str.match(/[^0-9]/) == nil
|
145
157
|
raise SongParseError, "'#{multiples_str}' is an invalid number of repeats for pattern '#{pattern_name}'. Number of repeats should be a whole number."
|
146
158
|
else
|
147
|
-
if
|
159
|
+
if multiples < 0
|
148
160
|
raise SongParseError, "'#{multiples_str}' is an invalid number of repeats for pattern '#{pattern_name}'. Must be 0 or greater."
|
149
|
-
elsif
|
161
|
+
elsif multiples > 0 && !song.patterns.has_key?(pattern_name_sym)
|
150
162
|
# This test is purposefully designed to only throw an error if the number of repeats is greater
|
151
163
|
# than 0. This allows you to specify an undefined pattern in the structure with "x0" repeats.
|
152
164
|
# This can be convenient for defining the structure before all patterns have been added to the song file.
|
@@ -161,9 +173,9 @@ private
|
|
161
173
|
|
162
174
|
# Converts all hash keys to be lowercase
|
163
175
|
def downcase_hash_keys(hash)
|
164
|
-
return hash.inject({})
|
176
|
+
return hash.inject({}) do |new_hash, pair|
|
165
177
|
new_hash[pair.first.downcase] = pair.last
|
166
178
|
new_hash
|
167
|
-
|
179
|
+
end
|
168
180
|
end
|
169
181
|
end
|
data/lib/songsplitter.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# Used to split a Song object into multiple Song objects, where each resulting
|
2
|
+
# object only has 1 track. For example, if a Song has 5 tracks, this will return
|
3
|
+
# a hash of 5 songs, each with one of the original song's tracks.
|
4
|
+
class SongSplitter
|
5
|
+
def initialize()
|
6
|
+
end
|
7
|
+
|
8
|
+
def split(original_song)
|
9
|
+
track_names = original_song.track_names()
|
10
|
+
|
11
|
+
split_songs = {}
|
12
|
+
track_names.each do |track_name|
|
13
|
+
new_song = original_song.copy_ignoring_patterns_and_structure()
|
14
|
+
|
15
|
+
if track_name == "placeholder"
|
16
|
+
track_sample_data = []
|
17
|
+
else
|
18
|
+
track_sample_data = new_song.kit.get_sample_data(track_name)
|
19
|
+
end
|
20
|
+
|
21
|
+
original_song.patterns.each do |name, original_pattern|
|
22
|
+
new_pattern = new_song.pattern name
|
23
|
+
|
24
|
+
if original_pattern.tracks.has_key?(track_name)
|
25
|
+
new_pattern.track track_name, track_sample_data, original_pattern.tracks[track_name].rhythm
|
26
|
+
else
|
27
|
+
new_pattern.track track_name, track_sample_data, "." * original_pattern.tick_count()
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
new_song.structure = original_song.structure
|
32
|
+
|
33
|
+
split_songs[track_name] = new_song
|
34
|
+
end
|
35
|
+
|
36
|
+
return split_songs
|
37
|
+
end
|
38
|
+
end
|
data/lib/track.rb
CHANGED
@@ -1,35 +1,41 @@
|
|
1
|
+
class InvalidRhythmError < RuntimeError; end
|
2
|
+
|
1
3
|
class Track
|
2
4
|
REST = "."
|
3
5
|
BEAT = "X"
|
4
6
|
|
5
|
-
def initialize(name, wave_data,
|
7
|
+
def initialize(name, wave_data, rhythm)
|
8
|
+
# TODO: Add validation for input parameters
|
9
|
+
|
6
10
|
@wave_data = wave_data
|
7
11
|
@name = name
|
8
12
|
@sample_data = nil
|
9
13
|
@overflow = nil
|
10
|
-
self.
|
14
|
+
self.rhythm = rhythm
|
11
15
|
end
|
12
16
|
|
13
|
-
def
|
14
|
-
@
|
17
|
+
def rhythm=(rhythm)
|
18
|
+
@rhythm = rhythm
|
15
19
|
beats = []
|
16
20
|
|
17
21
|
beat_length = 0
|
18
|
-
#
|
19
|
-
|
22
|
+
#rhythm.each_char{|ch|
|
23
|
+
rhythm.each_byte do |ch|
|
20
24
|
ch = ch.chr
|
21
25
|
if ch == BEAT
|
22
26
|
beats << beat_length
|
23
27
|
beat_length = 1
|
24
|
-
|
28
|
+
elsif ch == REST
|
25
29
|
beat_length += 1
|
30
|
+
else
|
31
|
+
raise InvalidRhythmError, "Track #{@name} has an invalid rhythm: '#{rhythm}'. Can only contain 'X' or '.'"
|
26
32
|
end
|
27
|
-
|
33
|
+
end
|
28
34
|
|
29
|
-
if
|
35
|
+
if beat_length > 0
|
30
36
|
beats << beat_length
|
31
37
|
end
|
32
|
-
if
|
38
|
+
if beats == []
|
33
39
|
beats = [0]
|
34
40
|
end
|
35
41
|
@beats = beats
|
@@ -39,6 +45,10 @@ class Track
|
|
39
45
|
@overflow = nil
|
40
46
|
end
|
41
47
|
|
48
|
+
def intro_sample_length(tick_sample_length)
|
49
|
+
return @beats[0] * tick_sample_length.floor
|
50
|
+
end
|
51
|
+
|
42
52
|
def sample_length(tick_sample_length)
|
43
53
|
total_ticks = @beats.inject(0) {|sum, n| sum + n}
|
44
54
|
return (total_ticks * tick_sample_length).floor
|
@@ -47,7 +57,7 @@ class Track
|
|
47
57
|
def sample_length_with_overflow(tick_sample_length)
|
48
58
|
temp_sample_length = sample_length(tick_sample_length)
|
49
59
|
|
50
|
-
|
60
|
+
unless @beats == [0]
|
51
61
|
beat_sample_length = @beats.last * tick_sample_length
|
52
62
|
if(@wave_data.length > beat_sample_length)
|
53
63
|
temp_sample_length += @wave_data.length - beat_sample_length.floor
|
@@ -57,33 +67,37 @@ class Track
|
|
57
67
|
return temp_sample_length.floor
|
58
68
|
end
|
59
69
|
|
60
|
-
def
|
70
|
+
def tick_count
|
71
|
+
return @rhythm.length
|
72
|
+
end
|
73
|
+
|
74
|
+
def sample_data(tick_sample_length)
|
61
75
|
actual_sample_length = sample_length(tick_sample_length)
|
62
76
|
full_sample_length = sample_length_with_overflow(tick_sample_length)
|
63
77
|
|
64
|
-
if
|
78
|
+
if @sample_data == nil
|
65
79
|
fill_value = (@wave_data.first.class == Array) ? [0, 0] : 0
|
66
80
|
output_data = [].fill(fill_value, 0, full_sample_length)
|
67
81
|
|
68
|
-
if
|
82
|
+
if full_sample_length > 0
|
69
83
|
remainder = 0.0
|
70
84
|
offset = @beats[0] * tick_sample_length
|
71
85
|
remainder += (@beats[0] * tick_sample_length) - (@beats[0] * tick_sample_length).floor
|
72
86
|
|
73
|
-
@beats[1...(@beats.length)].each
|
87
|
+
@beats[1...(@beats.length)].each do |beat_length|
|
74
88
|
beat_sample_length = beat_length * tick_sample_length
|
75
89
|
|
76
90
|
remainder += beat_sample_length - beat_sample_length.floor
|
77
|
-
if
|
91
|
+
if remainder >= 1.0
|
78
92
|
beat_sample_length += 1
|
79
93
|
remainder -= 1.0
|
80
94
|
end
|
81
95
|
|
82
96
|
output_data[offset...(offset + wave_data.length)] = wave_data
|
83
97
|
offset += beat_sample_length.floor
|
84
|
-
|
98
|
+
end
|
85
99
|
|
86
|
-
if
|
100
|
+
if full_sample_length > actual_sample_length
|
87
101
|
@sample_data = output_data[0...offset]
|
88
102
|
@overflow = output_data[actual_sample_length...full_sample_length]
|
89
103
|
else
|
@@ -98,21 +112,9 @@ class Track
|
|
98
112
|
|
99
113
|
primary_sample_data = @sample_data.dup
|
100
114
|
|
101
|
-
if(incoming_overflow != nil && incoming_overflow != [])
|
102
|
-
# TO DO: Add check for when incoming overflow is longer than
|
103
|
-
# track full length to prevent track from lengthening.
|
104
|
-
intro_length = @beats.first * tick_sample_length.floor
|
105
|
-
|
106
|
-
if(incoming_overflow.length <= intro_length)
|
107
|
-
primary_sample_data[0...incoming_overflow.length] = incoming_overflow
|
108
|
-
else
|
109
|
-
primary_sample_data[0...intro_length] = incoming_overflow[0...intro_length]
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
115
|
return {:primary => primary_sample_data, :overflow => @overflow}
|
114
116
|
end
|
115
117
|
|
116
118
|
attr_accessor :name, :wave_data
|
117
|
-
attr_reader :
|
119
|
+
attr_reader :rhythm
|
118
120
|
end
|
data/lib/wavefile.rb
ADDED
@@ -0,0 +1,475 @@
|
|
1
|
+
# DO NOT EDIT THIS FILE
|
2
|
+
# DO NOT EDIT THIS FILE
|
3
|
+
# DO NOT EDIT THIS FILE
|
4
|
+
#
|
5
|
+
# OK, so what's the deal here? This file contains the WaveFile class defined
|
6
|
+
# in v0.3.0 of the WaveFile gem (http://github.com/jstrait/wavefile). So why
|
7
|
+
# are we manually importing the class instead of just using the Gem? The
|
8
|
+
# reason is that (on my machine at least) it takes about 0.2 seconds
|
9
|
+
# to load RubyGems in 1.8.7. This is a non-trivial amount of time, and for
|
10
|
+
# shorter songs it can be a relatively large percentage of the total runtime.
|
11
|
+
# (In Ruby 1.9, it has no effect on performance, since RubyGems is already
|
12
|
+
# baked in anyway).
|
13
|
+
#
|
14
|
+
# So, considering that the WaveFile gem only contains one class (this file),
|
15
|
+
# I'm just moving it here. This means BEATS doesn't have to use the
|
16
|
+
# WaveFile gem, it therefore doesn't need to use RubyGems either. It's a hack,
|
17
|
+
# but a pragmatic hack.
|
18
|
+
#
|
19
|
+
# The caveat is that to make this as Gem-like as possible, this file should
|
20
|
+
# be treated as an external library, and not edited.
|
21
|
+
|
22
|
+
=begin
|
23
|
+
WAV File Specification
|
24
|
+
FROM http://ccrma.stanford.edu/courses/422/projects/WaveFormat/
|
25
|
+
The canonical WAVE format starts with the RIFF header:
|
26
|
+
0 4 ChunkID Contains the letters "RIFF" in ASCII form
|
27
|
+
(0x52494646 big-endian form).
|
28
|
+
4 4 ChunkSize 36 + SubChunk2Size, or more precisely:
|
29
|
+
4 + (8 + SubChunk1Size) + (8 + SubChunk2Size)
|
30
|
+
This is the size of the rest of the chunk
|
31
|
+
following this number. This is the size of the
|
32
|
+
entire file in bytes minus 8 bytes for the
|
33
|
+
two fields not included in this count:
|
34
|
+
ChunkID and ChunkSize.
|
35
|
+
8 4 Format Contains the letters "WAVE"
|
36
|
+
(0x57415645 big-endian form).
|
37
|
+
|
38
|
+
The "WAVE" format consists of two subchunks: "fmt " and "data":
|
39
|
+
The "fmt " subchunk describes the sound data's format:
|
40
|
+
12 4 Subchunk1ID Contains the letters "fmt "
|
41
|
+
(0x666d7420 big-endian form).
|
42
|
+
16 4 Subchunk1Size 16 for PCM. This is the size of the
|
43
|
+
rest of the Subchunk which follows this number.
|
44
|
+
20 2 AudioFormat PCM = 1 (i.e. Linear quantization)
|
45
|
+
Values other than 1 indicate some
|
46
|
+
form of compression.
|
47
|
+
22 2 NumChannels Mono = 1, Stereo = 2, etc.
|
48
|
+
24 4 SampleRate 8000, 44100, etc.
|
49
|
+
28 4 ByteRate == SampleRate * NumChannels * BitsPerSample/8
|
50
|
+
32 2 BlockAlign == NumChannels * BitsPerSample/8
|
51
|
+
The number of bytes for one sample including
|
52
|
+
all channels. I wonder what happens when
|
53
|
+
this number isn't an integer?
|
54
|
+
34 2 BitsPerSample 8 bits = 8, 16 bits = 16, etc.
|
55
|
+
|
56
|
+
The "data" subchunk contains the size of the data and the actual sound:
|
57
|
+
36 4 Subchunk2ID Contains the letters "data"
|
58
|
+
(0x64617461 big-endian form).
|
59
|
+
40 4 Subchunk2Size == NumSamples * NumChannels * BitsPerSample/8
|
60
|
+
This is the number of bytes in the data.
|
61
|
+
You can also think of this as the size
|
62
|
+
of the read of the subchunk following this
|
63
|
+
number.
|
64
|
+
44 * Data The actual sound data.
|
65
|
+
=end
|
66
|
+
|
67
|
+
class WaveFile
|
68
|
+
CHUNK_ID = "RIFF"
|
69
|
+
FORMAT = "WAVE"
|
70
|
+
FORMAT_CHUNK_ID = "fmt "
|
71
|
+
SUB_CHUNK1_SIZE = 16
|
72
|
+
PCM = 1
|
73
|
+
DATA_CHUNK_ID = "data"
|
74
|
+
HEADER_SIZE = 36
|
75
|
+
|
76
|
+
def initialize(num_channels, sample_rate, bits_per_sample, sample_data = [])
|
77
|
+
if num_channels == :mono
|
78
|
+
@num_channels = 1
|
79
|
+
elsif num_channels == :stereo
|
80
|
+
@num_channels = 2
|
81
|
+
else
|
82
|
+
@num_channels = num_channels
|
83
|
+
end
|
84
|
+
@sample_rate = sample_rate
|
85
|
+
@bits_per_sample = bits_per_sample
|
86
|
+
@sample_data = sample_data
|
87
|
+
|
88
|
+
@byte_rate = sample_rate * @num_channels * (bits_per_sample / 8)
|
89
|
+
@block_align = @num_channels * (bits_per_sample / 8)
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.open(path)
|
93
|
+
file = File.open(path, "rb")
|
94
|
+
|
95
|
+
begin
|
96
|
+
header = read_header(file)
|
97
|
+
errors = validate_header(header)
|
98
|
+
|
99
|
+
if errors == []
|
100
|
+
sample_data = read_sample_data(file,
|
101
|
+
header[:num_channels],
|
102
|
+
header[:bits_per_sample],
|
103
|
+
header[:sub_chunk2_size])
|
104
|
+
|
105
|
+
wave_file = self.new(header[:num_channels],
|
106
|
+
header[:sample_rate],
|
107
|
+
header[:bits_per_sample],
|
108
|
+
sample_data)
|
109
|
+
else
|
110
|
+
error_msg = "#{path} can't be opened, due to the following errors:\n"
|
111
|
+
errors.each {|error| error_msg += " * #{error}\n" }
|
112
|
+
raise StandardError, error_msg
|
113
|
+
end
|
114
|
+
rescue EOFError
|
115
|
+
raise StandardError, "An error occured while reading #{path}."
|
116
|
+
ensure
|
117
|
+
file.close()
|
118
|
+
end
|
119
|
+
|
120
|
+
return wave_file
|
121
|
+
end
|
122
|
+
|
123
|
+
def save(path)
|
124
|
+
# All numeric values should be saved in little-endian format
|
125
|
+
|
126
|
+
sample_data_size = @sample_data.length * @num_channels * (@bits_per_sample / 8)
|
127
|
+
|
128
|
+
# Write the header
|
129
|
+
file_contents = CHUNK_ID
|
130
|
+
file_contents += [HEADER_SIZE + sample_data_size].pack("V")
|
131
|
+
file_contents += FORMAT
|
132
|
+
file_contents += FORMAT_CHUNK_ID
|
133
|
+
file_contents += [SUB_CHUNK1_SIZE].pack("V")
|
134
|
+
file_contents += [PCM].pack("v")
|
135
|
+
file_contents += [@num_channels].pack("v")
|
136
|
+
file_contents += [@sample_rate].pack("V")
|
137
|
+
file_contents += [@byte_rate].pack("V")
|
138
|
+
file_contents += [@block_align].pack("v")
|
139
|
+
file_contents += [@bits_per_sample].pack("v")
|
140
|
+
file_contents += DATA_CHUNK_ID
|
141
|
+
file_contents += [sample_data_size].pack("V")
|
142
|
+
|
143
|
+
# Write the sample data
|
144
|
+
if !mono?
|
145
|
+
output_sample_data = []
|
146
|
+
@sample_data.each{|sample|
|
147
|
+
sample.each{|sub_sample|
|
148
|
+
output_sample_data << sub_sample
|
149
|
+
}
|
150
|
+
}
|
151
|
+
else
|
152
|
+
output_sample_data = @sample_data
|
153
|
+
end
|
154
|
+
|
155
|
+
if @bits_per_sample == 8
|
156
|
+
file_contents += output_sample_data.pack("C*")
|
157
|
+
elsif @bits_per_sample == 16
|
158
|
+
file_contents += output_sample_data.pack("s*")
|
159
|
+
else
|
160
|
+
raise StandardError, "Bits per sample is #{@bits_per_samples}, only 8 or 16 are supported"
|
161
|
+
end
|
162
|
+
|
163
|
+
file = File.open(path, "w")
|
164
|
+
file.syswrite(file_contents)
|
165
|
+
file.close
|
166
|
+
end
|
167
|
+
|
168
|
+
def sample_data()
|
169
|
+
return @sample_data
|
170
|
+
end
|
171
|
+
|
172
|
+
def normalized_sample_data()
|
173
|
+
if @bits_per_sample == 8
|
174
|
+
min_value = 128.0
|
175
|
+
max_value = 127.0
|
176
|
+
midpoint = 128
|
177
|
+
elsif @bits_per_sample == 16
|
178
|
+
min_value = 32768.0
|
179
|
+
max_value = 32767.0
|
180
|
+
midpoint = 0
|
181
|
+
else
|
182
|
+
raise StandardError, "Bits per sample is #{@bits_per_samples}, only 8 or 16 are supported"
|
183
|
+
end
|
184
|
+
|
185
|
+
if mono?
|
186
|
+
normalized_sample_data = @sample_data.map {|sample|
|
187
|
+
sample -= midpoint
|
188
|
+
if sample < 0
|
189
|
+
sample.to_f / min_value
|
190
|
+
else
|
191
|
+
sample.to_f / max_value
|
192
|
+
end
|
193
|
+
}
|
194
|
+
else
|
195
|
+
normalized_sample_data = @sample_data.map {|sample|
|
196
|
+
sample.map {|sub_sample|
|
197
|
+
sub_sample -= midpoint
|
198
|
+
if sub_sample < 0
|
199
|
+
sub_sample.to_f / min_value
|
200
|
+
else
|
201
|
+
sub_sample.to_f / max_value
|
202
|
+
end
|
203
|
+
}
|
204
|
+
}
|
205
|
+
end
|
206
|
+
|
207
|
+
return normalized_sample_data
|
208
|
+
end
|
209
|
+
|
210
|
+
def sample_data=(sample_data)
|
211
|
+
if sample_data.length > 0 && ((mono? && sample_data[0].class == Float) ||
|
212
|
+
(!mono? && sample_data[0][0].class == Float))
|
213
|
+
if @bits_per_sample == 8
|
214
|
+
# Samples in 8-bit wave files are stored as a unsigned byte
|
215
|
+
# Effective values are 0 to 255, midpoint at 128
|
216
|
+
min_value = 128.0
|
217
|
+
max_value = 127.0
|
218
|
+
midpoint = 128
|
219
|
+
elsif @bits_per_sample == 16
|
220
|
+
# Samples in 16-bit wave files are stored as a signed little-endian short
|
221
|
+
# Effective values are -32768 to 32767, midpoint at 0
|
222
|
+
min_value = 32768.0
|
223
|
+
max_value = 32767.0
|
224
|
+
midpoint = 0
|
225
|
+
else
|
226
|
+
raise StandardError, "Bits per sample is #{@bits_per_samples}, only 8 or 16 are supported"
|
227
|
+
end
|
228
|
+
|
229
|
+
if mono?
|
230
|
+
@sample_data = sample_data.map {|sample|
|
231
|
+
if(sample < 0.0)
|
232
|
+
(sample * min_value).round + midpoint
|
233
|
+
else
|
234
|
+
(sample * max_value).round + midpoint
|
235
|
+
end
|
236
|
+
}
|
237
|
+
else
|
238
|
+
@sample_data = sample_data.map {|sample|
|
239
|
+
sample.map {|sub_sample|
|
240
|
+
if(sub_sample < 0.0)
|
241
|
+
(sub_sample * min_value).round + midpoint
|
242
|
+
else
|
243
|
+
(sub_sample * max_value).round + midpoint
|
244
|
+
end
|
245
|
+
}
|
246
|
+
}
|
247
|
+
end
|
248
|
+
else
|
249
|
+
@sample_data = sample_data
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
def mono?()
|
254
|
+
return num_channels == 1
|
255
|
+
end
|
256
|
+
|
257
|
+
def stereo?()
|
258
|
+
return num_channels == 2
|
259
|
+
end
|
260
|
+
|
261
|
+
def reverse()
|
262
|
+
sample_data.reverse!()
|
263
|
+
end
|
264
|
+
|
265
|
+
def duration()
|
266
|
+
total_samples = sample_data.length
|
267
|
+
samples_per_millisecond = @sample_rate / 1000.0
|
268
|
+
samples_per_second = @sample_rate
|
269
|
+
samples_per_minute = samples_per_second * 60
|
270
|
+
samples_per_hour = samples_per_minute * 60
|
271
|
+
hours, minutes, seconds, milliseconds = 0, 0, 0, 0
|
272
|
+
|
273
|
+
if(total_samples >= samples_per_hour)
|
274
|
+
hours = total_samples / samples_per_hour
|
275
|
+
total_samples -= samples_per_hour * hours
|
276
|
+
end
|
277
|
+
|
278
|
+
if(total_samples >= samples_per_minute)
|
279
|
+
minutes = total_samples / samples_per_minute
|
280
|
+
total_samples -= samples_per_minute * minutes
|
281
|
+
end
|
282
|
+
|
283
|
+
if(total_samples >= samples_per_second)
|
284
|
+
seconds = total_samples / samples_per_second
|
285
|
+
total_samples -= samples_per_second * seconds
|
286
|
+
end
|
287
|
+
|
288
|
+
milliseconds = (total_samples / samples_per_millisecond).floor
|
289
|
+
|
290
|
+
return { :hours => hours, :minutes => minutes, :seconds => seconds, :milliseconds => milliseconds }
|
291
|
+
end
|
292
|
+
|
293
|
+
def bits_per_sample=(new_bits_per_sample)
|
294
|
+
if new_bits_per_sample != 8 && new_bits_per_sample != 16
|
295
|
+
raise StandardError, "Bits per sample of #{@bits_per_samples} is invalid, only 8 or 16 are supported"
|
296
|
+
end
|
297
|
+
|
298
|
+
if @bits_per_sample == 16 && new_bits_per_sample == 8
|
299
|
+
conversion_func = lambda {|sample|
|
300
|
+
if(sample < 0)
|
301
|
+
(sample / 256) + 128
|
302
|
+
else
|
303
|
+
# Faster to just divide by integer 258?
|
304
|
+
(sample / 258.007874015748031).round + 128
|
305
|
+
end
|
306
|
+
}
|
307
|
+
|
308
|
+
if mono?
|
309
|
+
@sample_data.map! &conversion_func
|
310
|
+
else
|
311
|
+
sample_data.map! {|sample| sample.map! &conversion_func }
|
312
|
+
end
|
313
|
+
elsif @bits_per_sample == 8 && new_bits_per_sample == 16
|
314
|
+
conversion_func = lambda {|sample|
|
315
|
+
sample -= 128
|
316
|
+
if(sample < 0)
|
317
|
+
sample * 256
|
318
|
+
else
|
319
|
+
# Faster to just multiply by integer 258?
|
320
|
+
(sample * 258.007874015748031).round
|
321
|
+
end
|
322
|
+
}
|
323
|
+
|
324
|
+
if mono?
|
325
|
+
@sample_data.map! &conversion_func
|
326
|
+
else
|
327
|
+
sample_data.map! {|sample| sample.map! &conversion_func }
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
@bits_per_sample = new_bits_per_sample
|
332
|
+
end
|
333
|
+
|
334
|
+
def num_channels=(new_num_channels)
|
335
|
+
if new_num_channels == :mono
|
336
|
+
new_num_channels = 1
|
337
|
+
elsif new_num_channels == :stereo
|
338
|
+
new_num_channels = 2
|
339
|
+
end
|
340
|
+
|
341
|
+
# The cases of mono -> stereo and vice-versa are handled in specially,
|
342
|
+
# because those conversion methods are faster than the general methods,
|
343
|
+
# and the large majority of wave files are expected to be either mono or stereo.
|
344
|
+
if @num_channels == 1 && new_num_channels == 2
|
345
|
+
sample_data.map! {|sample| [sample, sample]}
|
346
|
+
elsif @num_channels == 2 && new_num_channels == 1
|
347
|
+
sample_data.map! {|sample| (sample[0] + sample[1]) / 2}
|
348
|
+
elsif @num_channels == 1 && new_num_channels >= 2
|
349
|
+
sample_data.map! {|sample| [].fill(sample, 0, new_num_channels)}
|
350
|
+
elsif @num_channels >= 2 && new_num_channels == 1
|
351
|
+
sample_data.map! {|sample| sample.inject(0) {|sub_sample, sum| sum + sub_sample } / @num_channels }
|
352
|
+
elsif @num_channels > 2 && new_num_channels == 2
|
353
|
+
sample_data.map! {|sample| [sample[0], sample[1]]}
|
354
|
+
end
|
355
|
+
|
356
|
+
@num_channels = new_num_channels
|
357
|
+
end
|
358
|
+
|
359
|
+
def inspect()
|
360
|
+
duration = self.duration()
|
361
|
+
|
362
|
+
result = "Channels: #{@num_channels}\n" +
|
363
|
+
"Sample rate: #{@sample_rate}\n" +
|
364
|
+
"Bits per sample: #{@bits_per_sample}\n" +
|
365
|
+
"Block align: #{@block_align}\n" +
|
366
|
+
"Byte rate: #{@byte_rate}\n" +
|
367
|
+
"Sample count: #{@sample_data.length}\n" +
|
368
|
+
"Duration: #{duration[:hours]}h:#{duration[:minutes]}m:#{duration[:seconds]}s:#{duration[:milliseconds]}ms\n"
|
369
|
+
end
|
370
|
+
|
371
|
+
attr_reader :num_channels, :bits_per_sample, :byte_rate, :block_align
|
372
|
+
attr_accessor :sample_rate
|
373
|
+
|
374
|
+
private
|
375
|
+
|
376
|
+
def self.read_header(file)
|
377
|
+
header = {}
|
378
|
+
|
379
|
+
# Read RIFF header
|
380
|
+
riff_header = file.sysread(12).unpack("a4Va4")
|
381
|
+
header[:chunk_id] = riff_header[0]
|
382
|
+
header[:chunk_size] = riff_header[1]
|
383
|
+
header[:format] = riff_header[2]
|
384
|
+
|
385
|
+
# Read format subchunk
|
386
|
+
header[:sub_chunk1_id], header[:sub_chunk1_size] = self.read_to_chunk(file, FORMAT_CHUNK_ID)
|
387
|
+
format_subchunk_str = file.sysread(header[:sub_chunk1_size])
|
388
|
+
format_subchunk = format_subchunk_str.unpack("vvVVvv") # Any extra parameters are ignored
|
389
|
+
header[:audio_format] = format_subchunk[0]
|
390
|
+
header[:num_channels] = format_subchunk[1]
|
391
|
+
header[:sample_rate] = format_subchunk[2]
|
392
|
+
header[:byte_rate] = format_subchunk[3]
|
393
|
+
header[:block_align] = format_subchunk[4]
|
394
|
+
header[:bits_per_sample] = format_subchunk[5]
|
395
|
+
|
396
|
+
# Read data subchunk
|
397
|
+
header[:sub_chunk2_id], header[:sub_chunk2_size] = self.read_to_chunk(file, DATA_CHUNK_ID)
|
398
|
+
|
399
|
+
return header
|
400
|
+
end
|
401
|
+
|
402
|
+
def self.read_to_chunk(file, expected_chunk_id)
|
403
|
+
chunk_id = file.sysread(4)
|
404
|
+
chunk_size = file.sysread(4).unpack("V")[0]
|
405
|
+
|
406
|
+
while chunk_id != expected_chunk_id
|
407
|
+
# Skip chunk
|
408
|
+
file.sysread(chunk_size)
|
409
|
+
|
410
|
+
chunk_id = file.sysread(4)
|
411
|
+
chunk_size = file.sysread(4).unpack("V")[0]
|
412
|
+
end
|
413
|
+
|
414
|
+
return chunk_id, chunk_size
|
415
|
+
end
|
416
|
+
|
417
|
+
def self.validate_header(header)
|
418
|
+
errors = []
|
419
|
+
|
420
|
+
unless header[:bits_per_sample] == 8 || header[:bits_per_sample] == 16
|
421
|
+
errors << "Invalid bits per sample of #{header[:bits_per_sample]}. Only 8 and 16 are supported."
|
422
|
+
end
|
423
|
+
|
424
|
+
unless (1..65535) === header[:num_channels]
|
425
|
+
errors << "Invalid number of channels. Must be between 1 and 65535."
|
426
|
+
end
|
427
|
+
|
428
|
+
unless header[:chunk_id] == CHUNK_ID
|
429
|
+
errors << "Unsupported chunk ID: '#{header[:chunk_id]}'"
|
430
|
+
end
|
431
|
+
|
432
|
+
unless header[:format] == FORMAT
|
433
|
+
errors << "Unsupported format: '#{header[:format]}'"
|
434
|
+
end
|
435
|
+
|
436
|
+
unless header[:sub_chunk1_id] == FORMAT_CHUNK_ID
|
437
|
+
errors << "Unsupported chunk id: '#{header[:sub_chunk1_id]}'"
|
438
|
+
end
|
439
|
+
|
440
|
+
unless header[:audio_format] == PCM
|
441
|
+
errors << "Unsupported audio format code: '#{header[:audio_format]}'"
|
442
|
+
end
|
443
|
+
|
444
|
+
unless header[:sub_chunk2_id] == DATA_CHUNK_ID
|
445
|
+
errors << "Unsupported chunk id: '#{header[:sub_chunk2_id]}'"
|
446
|
+
end
|
447
|
+
|
448
|
+
return errors
|
449
|
+
end
|
450
|
+
|
451
|
+
# Assumes that file is "queued up" to the first sample
|
452
|
+
def self.read_sample_data(file, num_channels, bits_per_sample, sample_data_size)
|
453
|
+
if(bits_per_sample == 8)
|
454
|
+
data = file.sysread(sample_data_size).unpack("C*")
|
455
|
+
elsif(bits_per_sample == 16)
|
456
|
+
data = file.sysread(sample_data_size).unpack("s*")
|
457
|
+
else
|
458
|
+
data = []
|
459
|
+
end
|
460
|
+
|
461
|
+
if(num_channels > 1)
|
462
|
+
multichannel_data = []
|
463
|
+
|
464
|
+
i = 0
|
465
|
+
while i < data.length
|
466
|
+
multichannel_data << data[i...(num_channels + i)]
|
467
|
+
i += num_channels
|
468
|
+
end
|
469
|
+
|
470
|
+
data = multichannel_data
|
471
|
+
end
|
472
|
+
|
473
|
+
return data
|
474
|
+
end
|
475
|
+
end
|