beats 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (158) hide show
  1. data/README.markdown +29 -4
  2. data/bin/beats +36 -71
  3. data/lib/beats.rb +57 -0
  4. data/lib/beatswavefile.rb +93 -0
  5. data/lib/kit.rb +67 -11
  6. data/lib/pattern.rb +131 -86
  7. data/lib/song.rb +145 -114
  8. data/lib/songoptimizer.rb +154 -0
  9. data/lib/songparser.rb +40 -28
  10. data/lib/songsplitter.rb +38 -0
  11. data/lib/track.rb +33 -31
  12. data/lib/wavefile.rb +475 -0
  13. data/test/examples/combined.wav +0 -0
  14. data/test/examples/split-agogo_high.wav +0 -0
  15. data/test/examples/split-bass.wav +0 -0
  16. data/test/examples/split-hh_closed.wav +0 -0
  17. data/test/examples/split-snare.wav +0 -0
  18. data/test/examples/split-tom2.wav +0 -0
  19. data/test/examples/split-tom4.wav +0 -0
  20. data/test/fixtures/expected_output/example_combined_mono_16.wav +0 -0
  21. data/test/fixtures/expected_output/example_combined_mono_8.wav +0 -0
  22. data/test/fixtures/expected_output/example_combined_stereo_16.wav +0 -0
  23. data/test/fixtures/expected_output/example_combined_stereo_8.wav +0 -0
  24. data/test/fixtures/expected_output/example_split_mono_16-agogo.wav +0 -0
  25. data/test/fixtures/expected_output/example_split_mono_16-bass.wav +0 -0
  26. data/test/fixtures/expected_output/example_split_mono_16-hh_closed.wav +0 -0
  27. data/test/fixtures/expected_output/example_split_mono_16-snare.wav +0 -0
  28. data/test/fixtures/expected_output/example_split_mono_16-tom2_mono_16.wav +0 -0
  29. data/test/fixtures/expected_output/example_split_mono_16-tom4_mono_16.wav +0 -0
  30. data/test/fixtures/expected_output/example_split_mono_8-agogo.wav +0 -0
  31. data/test/fixtures/expected_output/example_split_mono_8-bass.wav +0 -0
  32. data/test/fixtures/expected_output/example_split_mono_8-hh_closed.wav +0 -0
  33. data/test/fixtures/expected_output/example_split_mono_8-snare.wav +0 -0
  34. data/test/fixtures/expected_output/example_split_mono_8-tom2_mono_8.wav +0 -0
  35. data/test/fixtures/expected_output/example_split_mono_8-tom4_mono_8.wav +0 -0
  36. data/test/fixtures/expected_output/example_split_stereo_16-agogo.wav +0 -0
  37. data/test/fixtures/expected_output/example_split_stereo_16-bass.wav +0 -0
  38. data/test/fixtures/expected_output/example_split_stereo_16-hh_closed.wav +0 -0
  39. data/test/fixtures/expected_output/example_split_stereo_16-snare.wav +0 -0
  40. data/test/fixtures/expected_output/example_split_stereo_16-tom2_stereo_16.wav +0 -0
  41. data/test/fixtures/expected_output/example_split_stereo_16-tom4_stereo_16.wav +0 -0
  42. data/test/fixtures/expected_output/example_split_stereo_8-agogo.wav +0 -0
  43. data/test/fixtures/expected_output/example_split_stereo_8-bass.wav +0 -0
  44. data/test/fixtures/expected_output/example_split_stereo_8-hh_closed.wav +0 -0
  45. data/test/fixtures/expected_output/example_split_stereo_8-snare.wav +0 -0
  46. data/test/fixtures/expected_output/example_split_stereo_8-tom2_stereo_8.wav +0 -0
  47. data/test/fixtures/expected_output/example_split_stereo_8-tom4_stereo_8.wav +0 -0
  48. data/test/fixtures/invalid/bad_repeat_count.txt +8 -0
  49. data/test/fixtures/invalid/bad_rhythm.txt +9 -0
  50. data/test/fixtures/invalid/bad_structure.txt +9 -0
  51. data/test/fixtures/invalid/bad_tempo.txt +8 -0
  52. data/test/fixtures/invalid/no_header.txt +3 -0
  53. data/test/fixtures/invalid/no_structure.txt +6 -0
  54. data/test/fixtures/invalid/pattern_with_no_tracks.txt +12 -0
  55. data/test/fixtures/invalid/sound_in_kit_not_found.txt +10 -0
  56. data/test/fixtures/invalid/sound_in_track_not_found.txt +8 -0
  57. data/test/fixtures/invalid/template.txt +31 -0
  58. data/test/fixtures/valid/example_mono_16.txt +28 -0
  59. data/test/fixtures/valid/example_mono_8.txt +28 -0
  60. data/test/fixtures/valid/example_no_kit.txt +30 -0
  61. data/test/fixtures/valid/example_stereo_16.txt +28 -0
  62. data/test/fixtures/valid/example_stereo_8.txt +28 -0
  63. data/test/fixtures/valid/example_with_empty_track.txt +10 -0
  64. data/test/fixtures/valid/example_with_kit.txt +34 -0
  65. data/test/fixtures/valid/no_tempo.txt +8 -0
  66. data/test/fixtures/valid/pattern_with_overflow.txt +9 -0
  67. data/test/fixtures/valid/repeats_not_specified.txt +10 -0
  68. data/test/fixtures/yaml/song_yaml.txt +30 -0
  69. data/test/includes.rb +11 -4
  70. data/test/integration.rb +100 -0
  71. data/test/kit_test.rb +39 -39
  72. data/test/pattern_test.rb +119 -71
  73. data/test/song_test.rb +87 -62
  74. data/test/songoptimizer_test.rb +162 -0
  75. data/test/songparser_test.rb +36 -165
  76. data/test/sounds/agogo_high_mono_16.wav +0 -0
  77. data/test/sounds/agogo_high_mono_8.wav +0 -0
  78. data/test/sounds/agogo_high_stereo_16.wav +0 -0
  79. data/test/sounds/agogo_high_stereo_8.wav +0 -0
  80. data/test/sounds/agogo_low_mono_16.wav +0 -0
  81. data/test/sounds/agogo_low_mono_8.wav +0 -0
  82. data/test/sounds/agogo_low_stereo_16.wav +0 -0
  83. data/test/sounds/agogo_low_stereo_8.wav +0 -0
  84. data/test/sounds/bass2_mono_16.wav +0 -0
  85. data/test/sounds/bass2_mono_8.wav +0 -0
  86. data/test/sounds/bass2_stereo_16.wav +0 -0
  87. data/test/sounds/bass2_stereo_8.wav +0 -0
  88. data/test/sounds/bass_mono_8.wav +0 -0
  89. data/test/sounds/bass_stereo_16.wav +0 -0
  90. data/test/sounds/bass_stereo_8.wav +0 -0
  91. data/test/sounds/clave_high_mono_16.wav +0 -0
  92. data/test/sounds/clave_high_mono_8.wav +0 -0
  93. data/test/sounds/clave_high_stereo_16.wav +0 -0
  94. data/test/sounds/clave_high_stereo_8.wav +0 -0
  95. data/test/sounds/clave_low_mono_16.wav +0 -0
  96. data/test/sounds/clave_low_mono_8.wav +0 -0
  97. data/test/sounds/clave_low_stereo_16.wav +0 -0
  98. data/test/sounds/clave_low_stereo_8.wav +0 -0
  99. data/test/sounds/conga_high_mono_16.wav +0 -0
  100. data/test/sounds/conga_high_mono_8.wav +0 -0
  101. data/test/sounds/conga_high_stereo_16.wav +0 -0
  102. data/test/sounds/conga_high_stereo_8.wav +0 -0
  103. data/test/sounds/conga_low_mono_16.wav +0 -0
  104. data/test/sounds/conga_low_mono_8.wav +0 -0
  105. data/test/sounds/conga_low_stereo_16.wav +0 -0
  106. data/test/sounds/conga_low_stereo_8.wav +0 -0
  107. data/test/sounds/cowbell_high_mono_16.wav +0 -0
  108. data/test/sounds/cowbell_high_mono_8.wav +0 -0
  109. data/test/sounds/cowbell_high_stereo_16.wav +0 -0
  110. data/test/sounds/cowbell_high_stereo_8.wav +0 -0
  111. data/test/sounds/cowbell_low_mono_16.wav +0 -0
  112. data/test/sounds/cowbell_low_mono_8.wav +0 -0
  113. data/test/sounds/cowbell_low_stereo_16.wav +0 -0
  114. data/test/sounds/cowbell_low_stereo_8.wav +0 -0
  115. data/test/sounds/hh_closed_mono_16.wav +0 -0
  116. data/test/sounds/hh_closed_mono_8.wav +0 -0
  117. data/test/sounds/hh_closed_stereo_16.wav +0 -0
  118. data/test/sounds/hh_closed_stereo_8.wav +0 -0
  119. data/test/sounds/hh_open_mono_16.wav +0 -0
  120. data/test/sounds/hh_open_mono_8.wav +0 -0
  121. data/test/sounds/hh_open_stereo_16.wav +0 -0
  122. data/test/sounds/hh_open_stereo_8.wav +0 -0
  123. data/test/sounds/ride_mono_16.wav +0 -0
  124. data/test/sounds/ride_mono_8.wav +0 -0
  125. data/test/sounds/ride_stereo_16.wav +0 -0
  126. data/test/sounds/ride_stereo_8.wav +0 -0
  127. data/test/sounds/rim_mono_16.wav +0 -0
  128. data/test/sounds/rim_mono_8.wav +0 -0
  129. data/test/sounds/rim_stereo_16.wav +0 -0
  130. data/test/sounds/rim_stereo_8.wav +0 -0
  131. data/test/sounds/sine-mono-8bit.wav +0 -0
  132. data/test/sounds/snare2_mono_16.wav +0 -0
  133. data/test/sounds/snare2_mono_8.wav +0 -0
  134. data/test/sounds/snare2_stereo_16.wav +0 -0
  135. data/test/sounds/snare2_stereo_8.wav +0 -0
  136. data/test/sounds/snare_mono_16.wav +0 -0
  137. data/test/sounds/snare_mono_8.wav +0 -0
  138. data/test/sounds/snare_stereo_16.wav +0 -0
  139. data/test/sounds/snare_stereo_8.wav +0 -0
  140. data/test/sounds/tom1_mono_16.wav +0 -0
  141. data/test/sounds/tom1_mono_8.wav +0 -0
  142. data/test/sounds/tom1_stereo_16.wav +0 -0
  143. data/test/sounds/tom1_stereo_8.wav +0 -0
  144. data/test/sounds/tom2_mono_16.wav +0 -0
  145. data/test/sounds/tom2_mono_8.wav +0 -0
  146. data/test/sounds/tom2_stereo_16.wav +0 -0
  147. data/test/sounds/tom2_stereo_8.wav +0 -0
  148. data/test/sounds/tom3_mono_16.wav +0 -0
  149. data/test/sounds/tom3_mono_8.wav +0 -0
  150. data/test/sounds/tom3_stereo_16.wav +0 -0
  151. data/test/sounds/tom3_stereo_8.wav +0 -0
  152. data/test/sounds/tom4_mono_16.wav +0 -0
  153. data/test/sounds/tom4_mono_8.wav +0 -0
  154. data/test/sounds/tom4_stereo_16.wav +0 -0
  155. data/test/sounds/tom4_stereo_8.wav +0 -0
  156. data/test/sounds/tone.wav +0 -0
  157. data/test/track_test.rb +78 -72
  158. metadata +277 -15
@@ -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(raw_song_components[:structure] == nil)
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
- # This is basically a factory. Don't see a benefit to extracting to a full class.
55
- # Also, is "canonicalize" a word?
54
+ # Is "canonicalize" a word?
56
55
  def canonicalize_definition(definition)
57
- if(definition.class == String)
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(definition.class == Hash)
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(raw_song_components[:full_definition]["song"] != nil)
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
- if(raw_kit != nil)
94
- raw_kit.each {|kit_item|
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{|key|
102
+ raw_patterns.keys.each do |key|
104
103
  track_list = raw_patterns[key]
105
- track_list.each{|track_definition|
106
- track_name = track_definition.keys.first
107
- track_path = track_name
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
- kit.add(track_name, track_path)
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{|key|
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.each{|track_definition|
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
- new_pattern.track track_name, song.kit.get_sample_data(track_name), track_definition[track_name]
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(pattern_item.class == String)
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
- if(multiples_str.match(/[^0-9]/) != nil)
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(multiples < 0)
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(multiples > 0 && !song.patterns.has_key?(pattern_name_sym))
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({}) {|new_hash, pair|
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
@@ -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
@@ -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, pattern)
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.pattern = pattern
14
+ self.rhythm = rhythm
11
15
  end
12
16
 
13
- def pattern=(pattern)
14
- @pattern = pattern
17
+ def rhythm=(rhythm)
18
+ @rhythm = rhythm
15
19
  beats = []
16
20
 
17
21
  beat_length = 0
18
- #pattern.each_char{|ch|
19
- pattern.each_byte{|ch|
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
- else
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(beat_length > 0)
35
+ if beat_length > 0
30
36
  beats << beat_length
31
37
  end
32
- if(beats == [])
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
- if(@beats != [0])
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 sample_data(tick_sample_length, incoming_overflow = nil)
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(@sample_data == nil)
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(full_sample_length > 0)
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 {|beat_length|
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(remainder >= 1.0)
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(full_sample_length > actual_sample_length)
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 :pattern
119
+ attr_reader :rhythm
118
120
  end
@@ -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