beats 1.2.0 → 1.2.1

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