beats 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|