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.
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