beats 1.2.0 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. data/LICENSE +1 -1
  2. data/README.markdown +28 -10
  3. data/bin/beats +9 -7
  4. data/lib/audioengine.rb +172 -0
  5. data/lib/audioutils.rb +73 -0
  6. data/lib/beats.rb +14 -15
  7. data/lib/beatswavefile.rb +17 -37
  8. data/lib/kit.rb +148 -71
  9. data/lib/pattern.rb +20 -117
  10. data/lib/patternexpander.rb +111 -0
  11. data/lib/song.rb +78 -132
  12. data/lib/songoptimizer.rb +29 -33
  13. data/lib/songparser.rb +70 -45
  14. data/lib/track.rb +11 -82
  15. data/test/audioengine_test.rb +261 -0
  16. data/test/audioutils_test.rb +45 -0
  17. data/test/fixtures/expected_output/example_split_mono_16-hh_closed.wav +0 -0
  18. data/test/{examples/split-agogo_high.wav → fixtures/expected_output/example_split_mono_16-hh_closed2.wav} +0 -0
  19. data/test/fixtures/expected_output/example_split_mono_8-hh_closed.wav +0 -0
  20. data/test/{examples/split-tom4.wav → fixtures/expected_output/example_split_mono_8-hh_closed2.wav} +0 -0
  21. data/test/fixtures/expected_output/example_split_stereo_16-hh_closed.wav +0 -0
  22. data/test/fixtures/expected_output/example_split_stereo_16-hh_closed2.wav +0 -0
  23. data/test/fixtures/expected_output/example_split_stereo_8-hh_closed.wav +0 -0
  24. data/test/fixtures/expected_output/example_split_stereo_8-hh_closed2.wav +0 -0
  25. data/test/fixtures/invalid/{bad_structure.txt → bad_flow.txt} +2 -2
  26. data/test/fixtures/invalid/bad_repeat_count.txt +1 -1
  27. data/test/fixtures/invalid/bad_rhythm.txt +1 -1
  28. data/test/fixtures/invalid/bad_tempo.txt +1 -1
  29. data/test/fixtures/invalid/{no_structure.txt → no_flow.txt} +1 -1
  30. data/test/fixtures/invalid/pattern_with_no_tracks.txt +1 -1
  31. data/test/fixtures/invalid/sound_in_kit_not_found.txt +1 -1
  32. data/test/fixtures/invalid/sound_in_kit_wrong_format.txt +10 -0
  33. data/test/fixtures/invalid/sound_in_track_not_found.txt +1 -1
  34. data/test/fixtures/invalid/sound_in_track_wrong_format.txt +8 -0
  35. data/test/fixtures/invalid/template.txt +1 -1
  36. data/test/fixtures/valid/example_mono_16.txt +5 -3
  37. data/test/fixtures/valid/example_mono_8.txt +5 -3
  38. data/test/fixtures/valid/example_no_kit.txt +1 -1
  39. data/test/fixtures/valid/example_stereo_16.txt +7 -4
  40. data/test/fixtures/valid/example_stereo_8.txt +5 -3
  41. data/test/fixtures/valid/example_with_empty_track.txt +1 -1
  42. data/test/fixtures/valid/example_with_kit.txt +1 -1
  43. data/test/fixtures/valid/multiple_tracks_same_sound.txt +33 -0
  44. data/test/fixtures/valid/no_tempo.txt +1 -1
  45. data/test/fixtures/valid/optimize_pattern_collision.txt +28 -0
  46. data/test/fixtures/valid/pattern_with_overflow.txt +1 -1
  47. data/test/fixtures/valid/repeats_not_specified.txt +2 -2
  48. data/test/fixtures/valid/with_structure.txt +10 -0
  49. data/test/fixtures/yaml/song_yaml.txt +5 -5
  50. data/test/includes.rb +4 -2
  51. data/test/integration.rb +3 -3
  52. data/test/kit_test.rb +136 -109
  53. data/test/pattern_test.rb +31 -131
  54. data/test/patternexpander_test.rb +142 -0
  55. data/test/song_test.rb +104 -102
  56. data/test/songoptimizer_test.rb +52 -38
  57. data/test/songparser_test.rb +79 -46
  58. data/test/sounds/composite_snare_mono_8_tom3_mono_16_mono_16.wav +0 -0
  59. data/test/sounds/composite_snare_mono_8_tom3_mono_8_mono_16.wav +0 -0
  60. data/test/sounds/composite_snare_stereo_16_tom3_mono_16_stereo_16.wav +0 -0
  61. data/test/sounds/composite_snare_stereo_8_tom3_mono_16_stereo_16.wav +0 -0
  62. data/test/track_test.rb +30 -185
  63. metadata +56 -24
  64. data/lib/songsplitter.rb +0 -38
  65. data/test/examples/combined.wav +0 -0
  66. data/test/examples/split-bass.wav +0 -0
  67. data/test/examples/split-hh_closed.wav +0 -0
  68. data/test/examples/split-snare.wav +0 -0
  69. data/test/examples/split-tom2.wav +0 -0
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  == BEATS
2
2
 
3
- # Copyright (c) 2010 Joel Strait
3
+ # Copyright (c) 2010-11 Joel Strait
4
4
  #
5
5
  # Permission is hereby granted, free of charge, to any person
6
6
  # obtaining a copy of this software and associated documentation
@@ -12,9 +12,9 @@ BEATS is a command-line drum machine written in pure Ruby. Feed it a song notate
12
12
  - Chorus: x4
13
13
  Kit:
14
14
  - bass: sounds/bass.wav
15
- - snare: sounds/snare.wav
16
- - hh_closed: sounds/hh_closed.wav
17
- - agogo: sounds/agogo_high.wav
15
+ - snare: sounds/snare.wav
16
+ - hh_closed: sounds/hh_closed.wav
17
+ - agogo: sounds/agogo_high.wav
18
18
 
19
19
  Verse:
20
20
  - bass: X...X...X...X...
@@ -26,23 +26,27 @@ BEATS is a command-line drum machine written in pure Ruby. Feed it a song notate
26
26
  - bass: X...X...X...X...
27
27
  - snare: ....X.......X...
28
28
  - hh_closed: X.XXX.XXX.XX..X.
29
- - sounds/tom4.wav: ...........X....
30
- - sounds/tom2.wav: ..............X.
29
+ - sounds/tom4.wav: ...........X....
30
+ - sounds/tom2.wav: ..............X.
31
+
32
+ And [here's what it sounds like](http://beatsdrummachine.com/beat.mp3) after getting the BEATS treatment. What a glorious groove!
31
33
 
32
- And [here is what it sounds like](http://beatsdrummachine.com/beat.mp3) after getting the BEATS treatment. What a glorious groove!
33
34
 
34
35
  Current Status
35
36
  --------------
36
37
 
37
- The latest stable version of BEATS is 1.2.0. It was just released! It brings significant performance and architectural improvements. It also contains a few bug fixes.
38
+ The latest stable version of BEATS is 1.2.1, released on March 6, 2011. This is a minor release which includes the following improvments:
38
39
 
39
- Since it was just released, I'm not sure just yet what will be coming in 1.3.0.
40
+ * You can use the | character to represent bar lines in a track rhythm. This is optional, but often makes longer rhythms easier to read.
41
+ * The "Structure" section of the song header is now called "Flow". (You can still use "Structure" for now, but you'll get a warning).
42
+ * A pattern can contain multiple tracks that use the same sound. Previously, BEATS would pick one of those tracks as the 'winner', and the other tracks wouldn't be played.
43
+ * Bug fix: A better error message is displayed if a sound file is in an unsupported format (such as MP3), or is not even a sound file.
40
44
 
41
45
 
42
46
  Installation
43
47
  ------------
44
48
 
45
- To install the latest stable version (1.2.0), run the following from the command line:
49
+ To install the latest stable version (1.2.1) from [rubygems.org](http://rubygems.org/gems/beats), run the following from the command line:
46
50
 
47
51
  sudo gem install beats
48
52
 
@@ -54,4 +58,18 @@ BEATS is not very useful unless you have some sounds to use with it. You can dow
54
58
  Usage
55
59
  -----
56
60
 
57
- BEATS runs from the command-line. Run `beats -h` to see the available options. For more detailed instructions, visit [http://beatsdrummachine.com](http://beatsdrummachine.com)
61
+ BEATS runs from the command-line. Run `beats -h` to see the available options. For more detailed instructions, visit [https://github.com/jstrait/beats/wiki/Usage](https://github.com/jstrait/beats/wiki/Usage) on the [BEATS Wiki](https://github.com/jstrait/beats/wiki).
62
+
63
+ The BEATS wiki also has a [Getting Started](https://github.com/jstrait/beats/wiki/Getting-Started) tutorial which shows how to create an example beat from scratch.
64
+
65
+
66
+ Found a Bug? Have a Suggestion? Want to Contribute?
67
+ ---------------------------------------------------
68
+
69
+ Contact me (Joel Strait) by sending a GitHub message.
70
+
71
+
72
+ License
73
+ -------
74
+ BEATS is released under the MIT license.
75
+
data/bin/beats CHANGED
@@ -5,16 +5,18 @@ start_time = Time.now
5
5
  $:.unshift File.dirname(__FILE__) + "/.."
6
6
  require "optparse"
7
7
  require "yaml"
8
+ require "lib/wavefile"
9
+ require "lib/beatswavefile"
10
+ require "lib/audioengine"
11
+ require "lib/audioutils"
8
12
  require "lib/beats"
9
- require "lib/song"
10
- require "lib/songparser"
11
- require "lib/songoptimizer"
12
- require "lib/songsplitter"
13
13
  require "lib/kit"
14
14
  require "lib/pattern"
15
+ require "lib/patternexpander"
16
+ require "lib/song"
17
+ require "lib/songoptimizer"
18
+ require "lib/songparser"
15
19
  require "lib/track"
16
- require "lib/wavefile"
17
- require "lib/beatswavefile"
18
20
 
19
21
  def parse_options
20
22
  options = {:split => false, :pattern => nil}
@@ -67,4 +69,4 @@ rescue StandardError => detail
67
69
  puts "An error occured while generating sound for '#{input_file_name}':\n"
68
70
  puts " #{detail}\n"
69
71
  puts "\n"
70
- end
72
+ end
@@ -0,0 +1,172 @@
1
+ # This class actually generates the output sound data for the performance.
2
+ # Applies a Kit to a Song (which contains sub Patterns and Tracks) to
3
+ # produce output sample data.
4
+ class AudioEngine
5
+ SAMPLE_RATE = 44100
6
+ PACK_CODE = "s*" # All output sample data is assumed to be 16-bit
7
+
8
+ def initialize(song, kit)
9
+ @song = song
10
+ @kit = kit
11
+
12
+ @step_sample_length = AudioUtils.step_sample_length(SAMPLE_RATE, @song.tempo)
13
+ @composited_pattern_cache = {}
14
+ end
15
+
16
+ def write_to_file(output_file_name)
17
+ packed_pattern_cache = {}
18
+ num_tracks_in_song = @song.total_tracks
19
+ samples_written = 0
20
+
21
+ # Open output wave file and preparing it for writing sample data.
22
+ wave_file = BeatsWaveFile.new(@kit.num_channels, SAMPLE_RATE, @kit.bits_per_sample)
23
+ file = wave_file.open_for_appending(output_file_name)
24
+
25
+ # Generate each pattern's sample data, or pull it from cache, and append it to the wave file.
26
+ incoming_overflow = {}
27
+ @song.flow.each do |pattern_name|
28
+ key = [pattern_name, incoming_overflow.hash]
29
+ unless packed_pattern_cache.member?(key)
30
+ sample_data = generate_pattern_sample_data(@song.patterns[pattern_name], incoming_overflow)
31
+
32
+ if @kit.num_channels == 1
33
+ # Don't flatten the sample data Array, since it is already flattened. That would be a waste of time, yo.
34
+ packed_pattern_cache[key] = {:primary => sample_data[:primary].pack(PACK_CODE),
35
+ :overflow => sample_data[:overflow],
36
+ :primary_length => sample_data[:primary].length}
37
+ else
38
+ packed_pattern_cache[key] = {:primary => sample_data[:primary].flatten.pack(PACK_CODE),
39
+ :overflow => sample_data[:overflow],
40
+ :primary_length => sample_data[:primary].length}
41
+ end
42
+ end
43
+
44
+ file.syswrite(packed_pattern_cache[key][:primary])
45
+ incoming_overflow = packed_pattern_cache[key][:overflow]
46
+ samples_written += packed_pattern_cache[key][:primary_length]
47
+ end
48
+
49
+ # Write any remaining overflow from the final pattern
50
+ final_overflow_composite = AudioUtils.composite(incoming_overflow.values, @kit.num_channels)
51
+ final_overflow_composite = AudioUtils.scale(final_overflow_composite, @kit.num_channels, num_tracks_in_song)
52
+ if @kit.num_channels == 1
53
+ file.syswrite(final_overflow_composite.pack(PACK_CODE))
54
+ else
55
+ file.syswrite(final_overflow_composite.flatten.pack(PACK_CODE))
56
+ end
57
+ samples_written += final_overflow_composite.length
58
+
59
+ # Now that we know how many samples have been written, go back and re-write the correct header.
60
+ file.sysseek(0)
61
+ wave_file.write_header(file, samples_written)
62
+
63
+ file.close()
64
+
65
+ return wave_file.calculate_duration(SAMPLE_RATE, samples_written)
66
+ end
67
+
68
+ attr_reader :step_sample_length
69
+
70
+ private
71
+
72
+ # Generates the sample data for a single track, using the specified sound's sample data.
73
+ def generate_track_sample_data(track, sound)
74
+ beats = track.beats
75
+ if beats == [0]
76
+ return {:primary => [], :overflow => []} # Is this really what should happen? Why throw away overflow?
77
+ end
78
+
79
+ fill_value = (@kit.num_channels == 1) ? 0 : [0, 0]
80
+ primary_sample_data = [].fill(fill_value, 0, AudioUtils.step_start_sample(track.step_count, @step_sample_length))
81
+
82
+ step_index = beats[0]
83
+ beat_sample_length = 0
84
+ beats[1...(beats.length)].each do |beat_step_length|
85
+ start_sample = AudioUtils.step_start_sample(step_index, @step_sample_length)
86
+ end_sample = [(start_sample + sound.length), primary_sample_data.length].min
87
+ beat_sample_length = end_sample - start_sample
88
+
89
+ primary_sample_data[start_sample...end_sample] = sound[0...beat_sample_length]
90
+
91
+ step_index += beat_step_length
92
+ end
93
+
94
+ overflow_sample_data = (sound == [] || beats.length == 1) ? [] : sound[beat_sample_length...(sound.length)]
95
+
96
+ return {:primary => primary_sample_data, :overflow => overflow_sample_data}
97
+ end
98
+
99
+ # Composites the sample data for each of the pattern's tracks, and returns the overflow sample data
100
+ # from tracks whose last sound trigger extends past the end of the pattern. This overflow can be
101
+ # used by the next pattern to avoid sounds cutting off when the pattern changes.
102
+ def generate_pattern_sample_data(pattern, incoming_overflow)
103
+ # Unless cached, composite each track's sample data.
104
+ if @composited_pattern_cache[pattern] == nil
105
+ primary_sample_data, overflow_sample_data = composite_pattern_tracks(pattern)
106
+ @composited_pattern_cache[pattern] = {:primary => primary_sample_data.dup, :overflow => overflow_sample_data.dup}
107
+ else
108
+ primary_sample_data = @composited_pattern_cache[pattern][:primary].dup
109
+ overflow_sample_data = @composited_pattern_cache[pattern][:overflow].dup
110
+ end
111
+
112
+ # Composite overflow from the previous pattern onto this pattern, to prevent sounds from cutting off.
113
+ primary_sample_data, overflow_sample_data = handle_incoming_overflow(pattern,
114
+ incoming_overflow,
115
+ primary_sample_data,
116
+ overflow_sample_data)
117
+ primary_sample_data = AudioUtils.scale(primary_sample_data, @kit.num_channels, @song.total_tracks)
118
+
119
+ return {:primary => primary_sample_data, :overflow => overflow_sample_data}
120
+ end
121
+
122
+ def composite_pattern_tracks(pattern)
123
+ overflow_sample_data = {}
124
+
125
+ raw_track_sample_arrays = []
126
+ pattern.tracks.each do |track_name, track|
127
+ temp = generate_track_sample_data(track, @kit.get_sample_data(track.name))
128
+ raw_track_sample_arrays << temp[:primary]
129
+ overflow_sample_data[track_name] = temp[:overflow]
130
+ end
131
+
132
+ primary_sample_data = AudioUtils.composite(raw_track_sample_arrays, @kit.num_channels)
133
+ return primary_sample_data, overflow_sample_data
134
+ end
135
+
136
+ # Applies sound overflow (i.e. long sounds such as cymbal crash which extend past the last step)
137
+ # from the previous pattern in the flow to the current pattern. This prevents sounds from being
138
+ # cut off when the pattern changes.
139
+ #
140
+ # It would probably be shorter and conceptually simpler to deal with incoming overflow in
141
+ # generate_track_sample_data() instead of this method. (In fact, this method would go away).
142
+ # However, doing it this way allows for caching composited pattern sample data, and
143
+ # applying incoming overflow to the composite. This allows each pattern to only be composited once,
144
+ # regardless of the incoming overflow that each performance of it receives. If incoming overflow
145
+ # was handled at the Track level we couldn't do that.
146
+ def handle_incoming_overflow(pattern, incoming_overflow, primary_sample_data, overflow_sample_data)
147
+ pattern_track_names = pattern.tracks.keys
148
+ sample_arrays = [primary_sample_data]
149
+
150
+ incoming_overflow.each do |incoming_track_name, incoming_sample_data|
151
+ end_sample = incoming_sample_data.length
152
+
153
+ if pattern_track_names.member?(incoming_track_name)
154
+ track = pattern.tracks[incoming_track_name]
155
+
156
+ if track.beats.length > 1
157
+ intro_length = (pattern.tracks[incoming_track_name].beats[0] * step_sample_length).floor
158
+ end_sample = [end_sample, intro_length].min
159
+ end
160
+ end
161
+
162
+ if end_sample > primary_sample_data.length
163
+ end_sample = primary_sample_data.length
164
+ overflow_sample_data[incoming_track_name] = incoming_sample_data[(primary_sample_data.length)...(incoming_sample_data.length)]
165
+ end
166
+
167
+ sample_arrays << incoming_sample_data[0...end_sample]
168
+ end
169
+
170
+ return AudioUtils.composite(sample_arrays, @kit.num_channels), overflow_sample_data
171
+ end
172
+ end
@@ -0,0 +1,73 @@
1
+ # This class contains some utility methods for working with sample data.
2
+ class AudioUtils
3
+
4
+ # Combines multiple sample arrays into one, by adding them together.
5
+ # When the sample arrays are different lengths, the output array will be the length
6
+ # of the longest input array.
7
+ # WARNING: Incoming arrays can be modified.
8
+ def self.composite(sample_arrays, num_channels)
9
+ if sample_arrays == []
10
+ return []
11
+ end
12
+
13
+ # Sort from longest to shortest
14
+ sample_arrays = sample_arrays.sort {|x, y| y.length <=> x.length}
15
+
16
+ composited_output = sample_arrays.slice!(0)
17
+ sample_arrays.each do |sample_array|
18
+ unless sample_array == []
19
+ if num_channels == 1
20
+ sample_array.length.times {|i| composited_output[i] += sample_array[i] }
21
+ elsif num_channels == 2
22
+ sample_array.length.times do |i|
23
+ composited_output[i] = [composited_output[i][0] + sample_array[i][0],
24
+ composited_output[i][1] + sample_array[i][1]]
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ return composited_output
31
+ end
32
+
33
+
34
+ # Scales the amplitude of the incoming sample array by *scale* amount. Can be used in conjunction
35
+ # with composite() to make sure composited sample arrays don't have an amplitude greater than 1.0.
36
+ def self.scale(sample_array, num_channels, scale)
37
+ if sample_array == []
38
+ return sample_array
39
+ end
40
+
41
+ if scale > 1
42
+ if num_channels == 1
43
+ sample_array = sample_array.map {|sample| sample / scale }
44
+ elsif num_channels == 2
45
+ sample_array = sample_array.map {|sample| [sample[0] / scale, sample[1] / scale]}
46
+ else
47
+ raise StandardError, "Invalid sample data array in AudioUtils.normalize()"
48
+ end
49
+ end
50
+
51
+ return sample_array
52
+ end
53
+
54
+
55
+ # Returns the number of samples that each step (i.e. a 'X' or a '.') lasts at a given sample
56
+ # rate and tempo. The sample length can be a non-integer value. Although there's no such
57
+ # thing as a partial sample, this is required to prevent small timing errors from creeping in.
58
+ # If they accumulate, they can cause rhythms to drift out of time.
59
+ def self.step_sample_length(samples_per_second, tempo)
60
+ samples_per_minute = samples_per_second * 60.0
61
+ samples_per_quarter_note = samples_per_minute / tempo
62
+
63
+ # Each step is equivalent to a 16th note
64
+ return samples_per_quarter_note / 4.0
65
+ end
66
+
67
+
68
+ # Returns the sample index that a given step (offset from 0) starts on.
69
+ def self.step_start_sample(step_index, step_sample_length)
70
+ return (step_index * step_sample_length).floor
71
+ end
72
+ end
73
+
@@ -1,6 +1,9 @@
1
1
  class Beats
2
- BEATS_VERSION = "1.2.0"
3
- SAMPLE_RATE = 44100
2
+ BEATS_VERSION = "1.2.1"
3
+
4
+ # Each pattern in the song will be split up into sub patterns that have at most this many steps.
5
+ # In general, audio for several shorter patterns can be generated more quickly than for one long
6
+ # pattern, and can also be cached more effectively.
4
7
  OPTIMIZED_PATTERN_LENGTH = 4
5
8
 
6
9
  def initialize(input_file_name, output_file_name, options)
@@ -20,23 +23,19 @@ class Beats
20
23
  end
21
24
 
22
25
  song_parser = SongParser.new()
23
- song = song_parser.parse(File.dirname(@input_file_name), YAML.load_file(@input_file_name))
26
+ song, kit = song_parser.parse(File.dirname(@input_file_name), File.read(@input_file_name))
24
27
  song_optimizer = SongOptimizer.new()
25
28
 
26
- if @options[:pattern] != nil
29
+ # If the -p option is used, transform the song into one whose flow consists of
30
+ # playing that single pattern once.
31
+ unless @options[:pattern] == nil
27
32
  pattern_name = @options[:pattern].downcase.to_sym
28
- unless song.patterns.member?(pattern_name)
29
- raise StandardError, "The song does not include a pattern called #{@options[:pattern]}"
30
- end
31
-
32
- song.structure = [pattern_name]
33
- song.remove_unused_patterns()
33
+ song.remove_patterns_except(pattern_name)
34
34
  end
35
35
 
36
36
  duration = nil
37
37
  if @options[:split]
38
- song_splitter = SongSplitter.new()
39
- split_songs = song_splitter.split(song)
38
+ split_songs = song.split()
40
39
  split_songs.each do |track_name, split_song|
41
40
  split_song = song_optimizer.optimize(split_song, OPTIMIZED_PATTERN_LENGTH)
42
41
 
@@ -45,13 +44,13 @@ class Beats
45
44
  file_name = File.dirname(@output_file_name) + "/" +
46
45
  File.basename(@output_file_name, extension) + "-" + File.basename(track_name, extension) +
47
46
  extension
48
- duration = split_song.write_to_file(file_name)
47
+ duration = AudioEngine.new(split_song, kit).write_to_file(file_name)
49
48
  end
50
49
  else
51
50
  song = song_optimizer.optimize(song, OPTIMIZED_PATTERN_LENGTH)
52
- duration = song.write_to_file(@output_file_name)
51
+ duration = AudioEngine.new(song, kit).write_to_file(@output_file_name)
53
52
  end
54
53
 
55
54
  return {:duration => duration}
56
55
  end
57
- end
56
+ end
@@ -1,15 +1,10 @@
1
1
  # Adds some functionality to the WaveFile gem that allows for improved performance. The
2
- # combo of open_for_appending() and write_snippet() allow a wave file to be written to
3
- # disk in chunks, instead of all at once. This improves performance (and I would assume
4
- # memory usage) by eliminating the need to store the entire sample data for the song in
5
- # memory in a giant (i.e. millions of elements) array.
2
+ # use of open_for_appending() allows a wave file to be written to disk in chunks, instead
3
+ # of all at once. This improves performance (and I would assume memory usage) by eliminating
4
+ # the need to store the entire sample data for the song in memory in a giant (i.e. millions
5
+ # of elements) array.
6
6
  #
7
7
  # I'm not sure these methods in their current form are suitable for the WaveFile gem.
8
- # That's a public API so I want to be careful adding to it, and these methods fall into the
9
- # category of "you should know what you're doing." In particular, open_for_appending() and
10
- # write_snippet() need to be used together, and if you don't use them right your saved wave
11
- # file will be messed up. There's probably a better API for doing this.
12
- #
13
8
  # If I figure out a better API I might add it to the WaveFile gem in the future, but until
14
9
  # then I'm just putting it here. Since BEATS is a stand-alone app and not a re-usable library,
15
10
  # I don't think this should be a problem.
@@ -17,12 +12,19 @@ class BeatsWaveFile < WaveFile
17
12
 
18
13
  # Writes the header for the wave file to path, and returns an open File object that
19
14
  # can be used outside the method to append the sample data. WARNING: The header contains
20
- # a field for the total number of samples in the file. This number of samples must be
21
- # subsequently be written to the file using write_snippet() or it won't be valid and you
22
- # won't be able to play it.
23
- def open_for_appending(path, num_samples)
15
+ # a field for the total number of samples in the file. This number of samples (and exactly
16
+ # this number of samples) must be subsequently be written to the file before it is closed
17
+ # or it won't be valid and you won't be able to play it.
18
+ def open_for_appending(path)
19
+ file = File.open(path, "w")
20
+ write_header(file, 0)
21
+
22
+ return file
23
+ end
24
+
25
+ def write_header(file, sample_length)
24
26
  bytes_per_sample = (@bits_per_sample / 8)
25
- sample_data_size = num_samples * bytes_per_sample * @num_channels
27
+ sample_data_size = sample_length * bytes_per_sample * @num_channels
26
28
 
27
29
  # Write the header
28
30
  header = CHUNK_ID
@@ -39,29 +41,7 @@ class BeatsWaveFile < WaveFile
39
41
  header += DATA_CHUNK_ID
40
42
  header += [sample_data_size].pack("V")
41
43
 
42
- file = File.open(path, "w")
43
44
  file.syswrite(header)
44
-
45
- return file
46
- end
47
-
48
- # Appending sample_data to file, which is assumed to be open. Should be used in
49
- # conjunction with open_for_appending(). The File object returned by that method
50
- # should be passed in here. WARNING: you are responsible for writing the correct
51
- # number of samples to the file, with 1 or more calls to this method. The caller
52
- # of this method is also responsible for closing the File object when finished.
53
- def write_snippet(file, sample_data)
54
- if @bits_per_sample == 8
55
- pack_code = "C*"
56
- elsif @bits_per_sample == 16
57
- pack_code = "s*"
58
- end
59
-
60
- if @num_channels == 1
61
- file.syswrite(sample_data.pack(pack_code))
62
- else
63
- file.syswrite(sample_data.flatten.pack(pack_code))
64
- end
65
45
  end
66
46
 
67
47
  def calculate_duration(sample_rate, total_samples)
@@ -90,4 +70,4 @@ class BeatsWaveFile < WaveFile
90
70
 
91
71
  return { :hours => hours, :minutes => minutes, :seconds => seconds, :milliseconds => milliseconds }
92
72
  end
93
- end
73
+ end