beats 1.2.3 → 1.2.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. data/LICENSE +1 -1
  2. data/README.markdown +30 -32
  3. data/bin/beats +2 -1
  4. data/lib/audioengine.rb +13 -32
  5. data/lib/beats.rb +1 -1
  6. data/lib/kit.rb +15 -14
  7. data/lib/pattern.rb +1 -1
  8. data/lib/song.rb +3 -7
  9. data/lib/songoptimizer.rb +1 -1
  10. data/lib/songparser.rb +1 -1
  11. data/lib/track.rb +1 -1
  12. data/lib/wavefile/cachingwriter.rb +36 -0
  13. data/test/cachingwriter_test.rb +63 -0
  14. data/test/fixtures/expected_output/example_combined_mono_8.wav +0 -0
  15. data/test/fixtures/expected_output/example_combined_stereo_8.wav +0 -0
  16. data/test/fixtures/expected_output/example_split_mono_8-agogo.wav +0 -0
  17. data/test/fixtures/expected_output/example_split_mono_8-bass.wav +0 -0
  18. data/test/fixtures/expected_output/example_split_mono_8-hh_closed.wav +0 -0
  19. data/test/fixtures/expected_output/example_split_mono_8-hh_closed2.wav +0 -0
  20. data/test/fixtures/expected_output/example_split_mono_8-snare.wav +0 -0
  21. data/test/fixtures/expected_output/example_split_mono_8-tom2_mono_8.wav +0 -0
  22. data/test/fixtures/expected_output/example_split_mono_8-tom4_mono_8.wav +0 -0
  23. data/test/fixtures/expected_output/example_split_stereo_8-agogo.wav +0 -0
  24. data/test/fixtures/expected_output/example_split_stereo_8-bass.wav +0 -0
  25. data/test/fixtures/expected_output/example_split_stereo_8-hh_closed.wav +0 -0
  26. data/test/fixtures/expected_output/example_split_stereo_8-hh_closed2.wav +0 -0
  27. data/test/fixtures/expected_output/example_split_stereo_8-snare.wav +0 -0
  28. data/test/fixtures/expected_output/example_split_stereo_8-tom2_stereo_8.wav +0 -0
  29. data/test/fixtures/expected_output/example_split_stereo_8-tom4_stereo_8.wav +0 -0
  30. data/test/includes.rb +4 -2
  31. data/test/integration_test.rb +6 -2
  32. data/test/sounds/agogo_high_mono_8.wav +0 -0
  33. data/test/sounds/agogo_high_stereo_8.wav +0 -0
  34. data/test/sounds/agogo_low_mono_8.wav +0 -0
  35. data/test/sounds/agogo_low_stereo_8.wav +0 -0
  36. data/test/sounds/bass2_mono_8.wav +0 -0
  37. data/test/sounds/bass2_stereo_8.wav +0 -0
  38. data/test/sounds/bass_mono_8.wav +0 -0
  39. data/test/sounds/bass_stereo_8.wav +0 -0
  40. data/test/sounds/clave_high_mono_8.wav +0 -0
  41. data/test/sounds/clave_high_stereo_8.wav +0 -0
  42. data/test/sounds/clave_low_mono_8.wav +0 -0
  43. data/test/sounds/clave_low_stereo_8.wav +0 -0
  44. data/test/sounds/conga_high_mono_8.wav +0 -0
  45. data/test/sounds/conga_high_stereo_8.wav +0 -0
  46. data/test/sounds/conga_low_mono_8.wav +0 -0
  47. data/test/sounds/conga_low_stereo_8.wav +0 -0
  48. data/test/sounds/cowbell_high_mono_8.wav +0 -0
  49. data/test/sounds/cowbell_high_stereo_8.wav +0 -0
  50. data/test/sounds/cowbell_low_mono_8.wav +0 -0
  51. data/test/sounds/cowbell_low_stereo_8.wav +0 -0
  52. data/test/sounds/hh_closed_mono_8.wav +0 -0
  53. data/test/sounds/hh_closed_stereo_8.wav +0 -0
  54. data/test/sounds/hh_open_mono_8.wav +0 -0
  55. data/test/sounds/hh_open_stereo_8.wav +0 -0
  56. data/test/sounds/ride_mono_8.wav +0 -0
  57. data/test/sounds/ride_stereo_8.wav +0 -0
  58. data/test/sounds/rim_mono_8.wav +0 -0
  59. data/test/sounds/rim_stereo_8.wav +0 -0
  60. data/test/sounds/snare2_mono_8.wav +0 -0
  61. data/test/sounds/snare2_stereo_8.wav +0 -0
  62. data/test/sounds/snare_mono_8.wav +0 -0
  63. data/test/sounds/snare_stereo_8.wav +0 -0
  64. data/test/sounds/tom1_mono_8.wav +0 -0
  65. data/test/sounds/tom1_stereo_8.wav +0 -0
  66. data/test/sounds/tom2_mono_8.wav +0 -0
  67. data/test/sounds/tom2_stereo_8.wav +0 -0
  68. data/test/sounds/tom3_mono_8.wav +0 -0
  69. data/test/sounds/tom3_stereo_8.wav +0 -0
  70. data/test/sounds/tom4_mono_8.wav +0 -0
  71. data/test/sounds/tom4_stereo_8.wav +0 -0
  72. metadata +41 -43
  73. data/bin/example_song.wav +0 -0
  74. data/lib/beatswavefile.rb +0 -73
  75. data/lib/patternexpander.rb +0 -111
  76. data/test/patternexpander_test.rb +0 -140
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  == BEATS
2
2
 
3
- # Copyright (c) 2010-11 Joel Strait
3
+ # Copyright (c) 2010-12 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
@@ -1,66 +1,64 @@
1
- BEATS Drum Machine
1
+ Beats Drum Machine
2
2
  ------------------
3
3
 
4
- BEATS is a command-line drum machine written in pure Ruby. Feed it a song notated in YAML, and it will produce a precision-milled *.wav file of impeccable timing and feel. Here's an example song:
4
+ Beats is a command-line drum machine written in pure Ruby. Feed it a song notated in YAML, and it will produce a precision-milled *.wav file of impeccable timing and feel. Here's an example song:
5
5
 
6
6
  Song:
7
- Tempo: 120
7
+ Tempo: 105
8
8
  Flow:
9
- - Verse: x2
10
- - Chorus: x4
11
- - Verse: x2
12
- - Chorus: x4
9
+ - Verse: x4
10
+ - Chorus: x4
13
11
  Kit:
14
- - bass: sounds/bass.wav
15
- - snare: sounds/snare.wav
16
- - hh_closed: sounds/hh_closed.wav
17
- - agogo: sounds/agogo_high.wav
18
-
12
+ - bass: house_2_1.wav
13
+ - snare: roland_tr_909_2.wav
14
+ - hihat: house_2_5.wav
15
+ - cowbell: big_beat_5.wav
16
+ - deep: house_2_2.wav
17
+
19
18
  Verse:
20
- - bass: X...X...X...X...
21
- - snare: ..............X.
22
- - hh_closed: X.XXX.XXX.X.X.X.
23
- - agogo: ..............XX
24
-
19
+ - bass: X..X...X..X.....
20
+ - snare: ....X.......X...
21
+ - hihat: ..X...X...X...X.
22
+
25
23
  Chorus:
26
- - bass: X...X...X...X...
27
- - snare: ....X.......X...
28
- - hh_closed: X.XXX.XXX.XX..X.
29
- - sounds/tom4.wav: ...........X....
30
- - sounds/tom2.wav: ..............X.
24
+ - bass: X..X...X..X.....
25
+ - snare: ....X.......X...
26
+ - hihat: XXXXXXXXXXXXX...
27
+ - cowbell: ....XX.X..X.X...
28
+ - deep: .............X..
31
29
 
32
- And [here's what it sounds like](http://beatsdrummachine.com/beat.mp3) after getting the BEATS treatment. What a glorious groove!
30
+ And [here's what it sounds like](http://beatsdrummachine.com/media/beat.mp3) after getting the Beats treatment. What a glorious groove!
33
31
 
34
32
 
35
33
  Current Status
36
34
  --------------
37
35
 
38
- The latest stable version of BEATS is 1.2.3, released on December 31, 2011. This is a minor release which includes two improvements:
36
+ The latest stable version of Beats is 1.2.4, released on December 22, 2012. This is a minor release which includes two improvements:
39
37
 
40
- * Bug fix: You can now use `~` in sound file paths, and it will correctly expand to your home folder. (At least on UNIX OSes, I'm not sure if that works on Windows).
41
- * The new `--path` option allows setting the base path from which relative sound file paths are searched for.
38
+ * Now works in MRI 1.9.3
39
+ * Now supports 32-bit PCM Wave files, due to upgrading to WaveFile 0.4.0. Previously, only 8-bit and 16-bit PCM files were supported.
42
40
 
43
41
 
44
42
  Installation
45
43
  ------------
46
44
 
47
- To install the latest stable version (1.2.3) from [rubygems.org](http://rubygems.org/gems/beats), run the following from the command line:
45
+ To install the latest stable version (1.2.4) from [rubygems.org](http://rubygems.org/gems/beats), run the following from the command line:
48
46
 
49
47
  gem install beats
50
48
 
51
49
  Note that if you are installing using the default version of Ruby that comes with MacOS X, you might get a file permission error. If that happens, use `sudo gem install beats` instead. If you are using RVM, plain `gem install beats` should work fine.
52
50
 
53
- Once installed, you can then run BEATS from the command-line using the `beats` command.
51
+ Once installed, you can then run Beats from the command-line using the `beats` command.
54
52
 
55
- BEATS is not very useful unless you have some sounds to use with it. You can download some example sounds from [http://beatsdrummachine.com](http://beatsdrummachine.com/download#drum-kits).
53
+ Beats is not very useful unless you have some sounds to use with it. You can download some example sounds from [http://beatsdrummachine.com](http://beatsdrummachine.com/download#drum-kits).
56
54
 
57
55
 
58
56
  Usage
59
57
  -----
60
58
 
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).
59
+ 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
60
 
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.
61
+ 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
62
 
65
63
 
66
64
  Found a Bug? Have a Suggestion? Want to Contribute?
@@ -71,5 +69,5 @@ Contact me (Joel Strait) by sending a GitHub message or opening a GitHub issue.
71
69
 
72
70
  License
73
71
  -------
74
- BEATS is released under the MIT license.
72
+ Beats is released under the MIT license.
75
73
 
data/bin/beats CHANGED
@@ -9,7 +9,7 @@ require "wavefile"
9
9
  require "lib/audioengine"
10
10
  require "lib/audioutils"
11
11
  require "lib/beats"
12
- require "lib/beatswavefile"
12
+ require "lib/wavefile/cachingwriter"
13
13
  require "lib/kit"
14
14
  require "lib/pattern"
15
15
  require "lib/song"
@@ -18,6 +18,7 @@ require "lib/songparser"
18
18
  require "lib/track"
19
19
 
20
20
  USAGE_INSTRUCTIONS = ""
21
+ YAML::ENGINE.yamler = 'syck' if defined?(YAML::ENGINE)
21
22
 
22
23
  def parse_options()
23
24
  options = {:split => false}
@@ -24,11 +24,10 @@ class AudioEngine
24
24
  def write_to_file(output_file_name)
25
25
  packed_pattern_cache = {}
26
26
  num_tracks_in_song = @song.total_tracks
27
- samples_written = 0
28
-
29
- # Open output wave file and preparing it for writing sample data.
30
- wave_file = BeatsWaveFile.new(@kit.num_channels, SAMPLE_RATE, @kit.bits_per_sample)
31
- file = wave_file.open_for_appending(output_file_name)
27
+
28
+ # Open output wave file and prepare it for writing sample data.
29
+ format = WaveFile::Format.new(@kit.num_channels, @kit.bits_per_sample, SAMPLE_RATE)
30
+ writer = WaveFile::CachingWriter.new(output_file_name, format)
32
31
 
33
32
  # Generate each pattern's sample data, or pull it from cache, and append it to the wave file.
34
33
  incoming_overflow = {}
@@ -37,40 +36,22 @@ class AudioEngine
37
36
  unless packed_pattern_cache.member?(key)
38
37
  sample_data = generate_pattern_sample_data(@song.patterns[pattern_name], incoming_overflow)
39
38
 
40
- if @kit.num_channels == 1
41
- # Don't flatten the sample data Array, since it is already flattened. That would be a waste of time, yo.
42
- packed_pattern_cache[key] = {:primary => sample_data[:primary].pack(PACK_CODE),
43
- :overflow => sample_data[:overflow],
44
- :primary_length => sample_data[:primary].length}
45
- else
46
- packed_pattern_cache[key] = {:primary => sample_data[:primary].flatten.pack(PACK_CODE),
47
- :overflow => sample_data[:overflow],
48
- :primary_length => sample_data[:primary].length}
49
- end
39
+ packed_pattern_cache[key] = { :primary => WaveFile::Buffer.new(sample_data[:primary], format),
40
+ :overflow => WaveFile::Buffer.new(sample_data[:overflow], format) }
50
41
  end
51
42
 
52
- file.syswrite(packed_pattern_cache[key][:primary])
53
- incoming_overflow = packed_pattern_cache[key][:overflow]
54
- samples_written += packed_pattern_cache[key][:primary_length]
43
+ writer.write(packed_pattern_cache[key][:primary])
44
+ incoming_overflow = packed_pattern_cache[key][:overflow].samples
55
45
  end
56
46
 
57
47
  # Write any remaining overflow from the final pattern
58
- final_overflow_composite = AudioUtils.composite(incoming_overflow.values, @kit.num_channels)
59
- final_overflow_composite = AudioUtils.scale(final_overflow_composite, @kit.num_channels, num_tracks_in_song)
60
- if @kit.num_channels == 1
61
- file.syswrite(final_overflow_composite.pack(PACK_CODE))
62
- else
63
- file.syswrite(final_overflow_composite.flatten.pack(PACK_CODE))
64
- end
65
- samples_written += final_overflow_composite.length
66
-
67
- # Now that we know how many samples have been written, go back and re-write the correct header.
68
- file.sysseek(0)
69
- wave_file.write_header(file, samples_written)
48
+ final_overflow_composite = AudioUtils.composite(incoming_overflow.values, format.channels)
49
+ final_overflow_composite = AudioUtils.scale(final_overflow_composite, format.channels, num_tracks_in_song)
50
+ writer.write(WaveFile::Buffer.new(final_overflow_composite, format))
70
51
 
71
- file.close()
52
+ writer.close()
72
53
 
73
- return wave_file.calculate_duration(SAMPLE_RATE, samples_written)
54
+ return WaveFile::Reader.info(output_file_name).duration
74
55
  end
75
56
 
76
57
  attr_reader :step_sample_length
@@ -1,5 +1,5 @@
1
1
  class Beats
2
- BEATS_VERSION = "1.2.3"
2
+ BEATS_VERSION = "1.2.4"
3
3
 
4
4
  # Each pattern in the song will be split up into sub patterns that have at most this many steps.
5
5
  # In general, audio for several shorter patterns can be generated more quickly than for one long
data/lib/kit.rb CHANGED
@@ -37,7 +37,7 @@ class Kit
37
37
  @bits_per_sample = 16 # Only use 16-bit files as output. Supporting 8-bit output
38
38
  # means extra complication for no real gain (I'm skeptical
39
39
  # anyone would explicitly want 8-bit output instead of 16-bit).
40
-
40
+
41
41
  load_sounds(base_path, kit_items)
42
42
  end
43
43
 
@@ -123,17 +123,16 @@ private
123
123
  end
124
124
 
125
125
  kit_items = make_file_names_absolute(kit_items)
126
- raw_sounds = load_raw_sounds(kit_items)
126
+ sound_buffers = load_raw_sounds(kit_items)
127
127
 
128
+ canonical_format = WaveFile::Format.new(@num_channels, @bits_per_sample, 44100)
129
+
128
130
  # Convert each sound to a common format
129
- raw_sounds.values.each do |wavefile|
130
- wavefile.num_channels = @num_channels
131
- wavefile.bits_per_sample = @bits_per_sample
132
- end
133
-
131
+ sound_buffers.each {|file_name, buffer| sound_buffers[file_name] = buffer.convert(canonical_format) }
132
+
134
133
  # If necessary, mix component sounds into a composite
135
134
  kit_items.each do |label, sound_file_names|
136
- @sound_bank[label] = mixdown(sound_file_names, raw_sounds)
135
+ @sound_bank[label] = mixdown(sound_file_names, sound_buffers)
137
136
  end
138
137
  end
139
138
 
@@ -157,24 +156,26 @@ private
157
156
  raw_sounds = {}
158
157
  kit_items.values.flatten.each do |sound_file_name|
159
158
  begin
160
- wavefile = WaveFile.open(sound_file_name)
159
+ info = WaveFile::Reader.info(sound_file_name)
160
+ WaveFile::Reader.new(sound_file_name).each_buffer(info.sample_count) do |buffer|
161
+ raw_sounds[sound_file_name] = buffer
162
+ @num_channels = [@num_channels, buffer.channels].max
163
+ end
161
164
  rescue Errno::ENOENT
162
165
  raise SoundFileNotFoundError, "Sound file #{sound_file_name} not found."
163
166
  rescue StandardError
164
167
  raise InvalidSoundFormatError, "Sound file #{sound_file_name} is either not a sound file, " +
165
- "or is in an unsupported format. BEATS can handle 8 or 16-bit *.wav files."
168
+ "or is in an unsupported format. BEATS can handle 8, 16, or 32-bit PCM *.wav files."
166
169
  end
167
- @num_channels = [@num_channels, wavefile.num_channels].max
168
- raw_sounds[sound_file_name] = wavefile
169
170
  end
170
171
 
171
172
  return raw_sounds
172
173
  end
173
174
 
174
175
  def mixdown(sound_file_names, raw_sounds)
175
- sample_arrays = []
176
+ sample_arrays = []
176
177
  sound_file_names.each do |sound_file_name|
177
- sample_arrays << raw_sounds[sound_file_name].sample_data
178
+ sample_arrays << raw_sounds[sound_file_name].samples
178
179
  end
179
180
 
180
181
  composited_sample_data = AudioUtils.composite(sample_arrays, @num_channels)
@@ -31,7 +31,7 @@ class Pattern
31
31
  end
32
32
 
33
33
  def step_count
34
- return @tracks.values.collect {|track| track.rhythm.length }.max || 0
34
+ @tracks.values.collect {|track| track.rhythm.length }.max || 0
35
35
  end
36
36
 
37
37
  # Returns whether or not this pattern has the same number of tracks as other_pattern, and that
@@ -11,7 +11,7 @@ class InvalidTempoError < RuntimeError; end
11
11
  class Song
12
12
  DEFAULT_TEMPO = 120
13
13
 
14
- def initialize()
14
+ def initialize
15
15
  self.tempo = DEFAULT_TEMPO
16
16
  @patterns = {}
17
17
  @flow = []
@@ -50,10 +50,6 @@ class Song
50
50
  @patterns.values.inject([]) {|track_names, pattern| track_names | pattern.tracks.keys }.sort
51
51
  end
52
52
 
53
- def tempo
54
- return @tempo
55
- end
56
-
57
53
  def tempo=(new_tempo)
58
54
  unless new_tempo.class == Fixnum && new_tempo > 0
59
55
  raise InvalidTempoError, "Invalid tempo: '#{new_tempo}'. Tempo must be a number greater than 0."
@@ -73,7 +69,7 @@ class Song
73
69
  # Splits a Song object into multiple Song objects, where each new
74
70
  # Song only has 1 track. For example, if a Song has 5 tracks, this will return
75
71
  # a hash of 5 songs, each with one of the original Song's tracks.
76
- def split()
72
+ def split
77
73
  split_songs = {}
78
74
  track_names = track_names()
79
75
 
@@ -122,7 +118,7 @@ class Song
122
118
  return yaml_output
123
119
  end
124
120
 
125
- attr_reader :patterns
121
+ attr_reader :patterns, :tempo
126
122
  attr_accessor :flow
127
123
 
128
124
  private
@@ -12,7 +12,7 @@
12
12
  # Note that step #1 actually performs double duty, because breaking Patterns into smaller
13
13
  # pieces increases the likelihood there will be duplicates that can be combined.
14
14
  class SongOptimizer
15
- def initialize()
15
+ def initialize
16
16
  end
17
17
 
18
18
  # Returns a Song that will produce the same output as original_song, but should be
@@ -23,7 +23,7 @@ class SongParser
23
23
  - Verse: x2
24
24
  - Chorus: x2"
25
25
 
26
- def initialize()
26
+ def initialize
27
27
  end
28
28
 
29
29
 
@@ -47,7 +47,7 @@ class Track
47
47
  end
48
48
 
49
49
  def step_count
50
- return @rhythm.length
50
+ @rhythm.length
51
51
  end
52
52
 
53
53
  attr_accessor :name
@@ -0,0 +1,36 @@
1
+ module WaveFile
2
+ # Implementation of Writer that caches the raw wave data for each buffer that it has written.
3
+ # If the Buffer is written again, it will write the version from cache instead of re-doing
4
+ # a String.pack() call.
5
+ class CachingWriter < Writer
6
+ def initialize(file_name, format)
7
+ super
8
+
9
+ @buffer_cache = {}
10
+ end
11
+
12
+ def write(buffer)
13
+ packed_buffer_data = {}
14
+
15
+ key = buffer.hash
16
+ if @buffer_cache.member?(key)
17
+ packed_buffer_data = @buffer_cache[key]
18
+ else
19
+ samples = buffer.convert(@format).samples
20
+
21
+ if @format.channels > 1
22
+ data = samples.flatten.pack(@pack_code)
23
+ else
24
+ # Flattening an already flat array is a waste of time.
25
+ data = samples.pack(@pack_code)
26
+ end
27
+
28
+ packed_buffer_data = { :data => data, :sample_count => samples.length }
29
+ @buffer_cache[key] = packed_buffer_data
30
+ end
31
+
32
+ @file.syswrite(packed_buffer_data[:data])
33
+ @samples_written += packed_buffer_data[:sample_count]
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,63 @@
1
+ require 'includes'
2
+
3
+ # Makes @file writable so it can be replaced with StringIO for testing
4
+ class StringCachingWriter < WaveFile::CachingWriter
5
+ attr_writer :file
6
+ end
7
+
8
+ # Basic tests for CachingWriter; the integration tests test it more thoroughly.
9
+ class CachingWriterTest < Test::Unit::TestCase
10
+ def test_mono
11
+ buffer1_bytes = [0, 0, 1, 0, 2, 0]
12
+ buffer2_bytes = [3, 0, 4, 0, 5, 0]
13
+
14
+ format = WaveFile::Format.new(:mono, 16, 44100)
15
+ writer = StringCachingWriter.new("does_not_matter", format)
16
+ string_io = StringIO.new
17
+ writer.file = string_io
18
+
19
+ assert_equal(format, writer.format)
20
+
21
+ buffer = WaveFile::Buffer.new([0, 1, 2], format)
22
+ writer.write(buffer)
23
+ assert_equal(buffer1_bytes, get_bytes(string_io))
24
+
25
+ buffer = WaveFile::Buffer.new([3, 4, 5], format)
26
+ writer.write(buffer)
27
+ assert_equal(buffer1_bytes + buffer2_bytes, get_bytes(string_io))
28
+
29
+ buffer = WaveFile::Buffer.new([0, 1, 2], format)
30
+ writer.write(buffer)
31
+ assert_equal(buffer1_bytes + buffer2_bytes + buffer1_bytes, get_bytes(string_io))
32
+ end
33
+
34
+ def test_stereo
35
+ buffer1_bytes = [0, 0, 3, 0, 1, 0, 2, 0]
36
+ buffer2_bytes = [9, 0, 6, 0, 8, 0, 7, 0]
37
+
38
+ format = WaveFile::Format.new(:stereo, 16, 44100)
39
+ writer = StringCachingWriter.new("does_not_matter", format)
40
+ string_io = StringIO.new
41
+ writer.file = string_io
42
+
43
+ assert_equal(format, writer.format)
44
+
45
+ buffer = WaveFile::Buffer.new([[0, 3], [1, 2]], format)
46
+ writer.write(buffer)
47
+ assert_equal(buffer1_bytes, get_bytes(string_io))
48
+
49
+ buffer = WaveFile::Buffer.new([[9, 6], [8, 7]], format)
50
+ writer.write(buffer)
51
+ assert_equal(buffer1_bytes + buffer2_bytes, get_bytes(string_io))
52
+
53
+ buffer = WaveFile::Buffer.new([[0, 3], [1, 2]], format)
54
+ writer.write(buffer)
55
+ assert_equal(buffer1_bytes + buffer2_bytes + buffer1_bytes, get_bytes(string_io))
56
+ end
57
+
58
+ private
59
+
60
+ def get_bytes(string_io)
61
+ string_io.string.each_byte.inject([]) {|arr, element| arr << element }
62
+ end
63
+ end
@@ -4,17 +4,19 @@ require 'yaml'
4
4
  require 'rubygems'
5
5
 
6
6
  # External gems
7
+ gem 'wavefile', "=0.4.0"
7
8
  require 'wavefile'
8
9
 
9
10
  # BEATS classes
10
11
  require 'audioengine'
11
12
  require 'audioutils'
12
13
  require 'beats'
13
- require 'beatswavefile'
14
+ require 'wavefile/cachingwriter'
14
15
  require 'kit'
15
16
  require 'pattern'
16
- require 'patternexpander'
17
17
  require 'song'
18
18
  require 'songparser'
19
19
  require 'songoptimizer'
20
20
  require 'track'
21
+
22
+ YAML::ENGINE.yamler = 'syck' if defined?(YAML::ENGINE)
@@ -44,7 +44,7 @@ class IntegrationTest < Test::Unit::TestCase
44
44
 
45
45
  def run_combined_test(num_channels, bits_per_sample, suffix="", base_path=nil)
46
46
  # Make sure no output from previous tests is still around
47
- assert_equal([".", ".."], Dir.new(OUTPUT_FOLDER).entries)
47
+ assert_directory_is_empty OUTPUT_FOLDER
48
48
 
49
49
  song_fixture = "test/fixtures/valid/example_#{num_channels}_#{bits_per_sample}#{suffix}.txt"
50
50
  actual_output_file = "#{OUTPUT_FOLDER}/example_combined_#{num_channels}_#{bits_per_sample}#{suffix}.wav"
@@ -77,7 +77,7 @@ class IntegrationTest < Test::Unit::TestCase
77
77
 
78
78
  def run_split_test(num_channels, bits_per_sample, suffix="", base_path=nil)
79
79
  # Make sure no output from previous tests is still around
80
- assert_equal([".", ".."], Dir.new(OUTPUT_FOLDER).entries)
80
+ assert_directory_is_empty OUTPUT_FOLDER
81
81
 
82
82
  song_fixture = "test/fixtures/valid/example_#{num_channels}_#{bits_per_sample}#{suffix}.txt"
83
83
  actual_output_prefix = "#{OUTPUT_FOLDER}/example_split_#{num_channels}_#{bits_per_sample}#{suffix}"
@@ -107,6 +107,10 @@ class IntegrationTest < Test::Unit::TestCase
107
107
  File.delete(actual_output_file)
108
108
  end
109
109
  end
110
+
111
+ def assert_directory_is_empty dir
112
+ assert_equal([".", ".."].sort, Dir.new(dir).entries.sort)
113
+ end
110
114
 
111
115
  def clean_output_folder()
112
116
  # Make the folder if it doesn't already exist
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
metadata CHANGED
@@ -1,55 +1,57 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: beats
3
- version: !ruby/object:Gem::Version
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.2.4
4
5
  prerelease:
5
- version: 1.2.3
6
6
  platform: ruby
7
- authors:
7
+ authors:
8
8
  - Joel Strait
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
-
13
- date: 2012-01-01 00:00:00 Z
14
- dependencies:
15
- - !ruby/object:Gem::Dependency
12
+ date: 2012-12-23 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
16
15
  name: wavefile
17
- prerelease: false
18
- requirement: &id001 !ruby/object:Gem::Requirement
16
+ requirement: !ruby/object:Gem::Requirement
19
17
  none: false
20
- requirements:
21
- - - "="
22
- - !ruby/object:Gem::Version
23
- version: 0.3.0
18
+ requirements:
19
+ - - '='
20
+ - !ruby/object:Gem::Version
21
+ version: 0.4.0
24
22
  type: :runtime
25
- version_requirements: *id001
26
- description: A command-line drum machine. Feed it a song notated in YAML, and it will produce a precision-milled Wave file of impeccable timing and feel.
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - '='
28
+ - !ruby/object:Gem::Version
29
+ version: 0.4.0
30
+ description: A command-line drum machine. Feed it a song notated in YAML, and it will
31
+ produce a precision-milled Wave file of impeccable timing and feel.
27
32
  email: joel dot strait at Google's popular web mail service
28
- executables:
33
+ executables:
29
34
  - beats
30
35
  extensions: []
31
-
32
36
  extra_rdoc_files: []
33
-
34
- files:
37
+ files:
35
38
  - LICENSE
36
39
  - README.markdown
37
40
  - Rakefile
38
41
  - lib/audioengine.rb
39
42
  - lib/audioutils.rb
40
43
  - lib/beats.rb
41
- - lib/beatswavefile.rb
42
44
  - lib/kit.rb
43
45
  - lib/pattern.rb
44
- - lib/patternexpander.rb
45
46
  - lib/song.rb
46
47
  - lib/songoptimizer.rb
47
48
  - lib/songparser.rb
48
49
  - lib/track.rb
50
+ - lib/wavefile/cachingwriter.rb
49
51
  - bin/beats
50
- - bin/example_song.wav
51
52
  - test/audioengine_test.rb
52
53
  - test/audioutils_test.rb
54
+ - test/cachingwriter_test.rb
53
55
  - test/fixtures/expected_output/example_combined_mono_16.wav
54
56
  - test/fixtures/expected_output/example_combined_mono_8.wav
55
57
  - test/fixtures/expected_output/example_combined_stereo_16.wav
@@ -113,7 +115,6 @@ files:
113
115
  - test/integration_test.rb
114
116
  - test/kit_test.rb
115
117
  - test/pattern_test.rb
116
- - test/patternexpander_test.rb
117
118
  - test/song_test.rb
118
119
  - test/songoptimizer_test.rb
119
120
  - test/songparser_test.rb
@@ -206,34 +207,33 @@ files:
206
207
  - test/track_test.rb
207
208
  homepage: http://beatsdrummachine.com/
208
209
  licenses: []
209
-
210
210
  post_install_message:
211
211
  rdoc_options: []
212
-
213
- require_paths:
212
+ require_paths:
214
213
  - lib
215
- required_ruby_version: !ruby/object:Gem::Requirement
214
+ required_ruby_version: !ruby/object:Gem::Requirement
216
215
  none: false
217
- requirements:
218
- - - ">="
219
- - !ruby/object:Gem::Version
220
- version: "0"
221
- required_rubygems_version: !ruby/object:Gem::Requirement
216
+ requirements:
217
+ - - ! '>='
218
+ - !ruby/object:Gem::Version
219
+ version: '0'
220
+ required_rubygems_version: !ruby/object:Gem::Requirement
222
221
  none: false
223
- requirements:
224
- - - ">="
225
- - !ruby/object:Gem::Version
226
- version: "0"
222
+ requirements:
223
+ - - ! '>='
224
+ - !ruby/object:Gem::Version
225
+ version: '0'
227
226
  requirements: []
228
-
229
227
  rubyforge_project:
230
- rubygems_version: 1.8.13
228
+ rubygems_version: 1.8.24
231
229
  signing_key:
232
230
  specification_version: 3
233
- summary: A command-line drum machine. Feed it a song notated in YAML, and it will produce a precision-milled Wave file of impeccable timing and feel.
234
- test_files:
231
+ summary: A command-line drum machine. Feed it a song notated in YAML, and it will
232
+ produce a precision-milled Wave file of impeccable timing and feel.
233
+ test_files:
235
234
  - test/audioengine_test.rb
236
235
  - test/audioutils_test.rb
236
+ - test/cachingwriter_test.rb
237
237
  - test/fixtures/expected_output/example_combined_mono_16.wav
238
238
  - test/fixtures/expected_output/example_combined_mono_8.wav
239
239
  - test/fixtures/expected_output/example_combined_stereo_16.wav
@@ -297,7 +297,6 @@ test_files:
297
297
  - test/integration_test.rb
298
298
  - test/kit_test.rb
299
299
  - test/pattern_test.rb
300
- - test/patternexpander_test.rb
301
300
  - test/song_test.rb
302
301
  - test/songoptimizer_test.rb
303
302
  - test/songparser_test.rb
@@ -388,4 +387,3 @@ test_files:
388
387
  - test/sounds/tom4_stereo_8.wav
389
388
  - test/sounds/tone.wav
390
389
  - test/track_test.rb
391
- has_rdoc:
Binary file
@@ -1,73 +0,0 @@
1
- # Adds some functionality to the WaveFile gem that allows for improved performance. The
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
- #
7
- # I'm not sure these methods in their current form are suitable for the WaveFile gem.
8
- # If I figure out a better API I might add it to the WaveFile gem in the future, but until
9
- # then I'm just putting it here. Since BEATS is a stand-alone app and not a re-usable library,
10
- # I don't think this should be a problem.
11
- class BeatsWaveFile < WaveFile
12
-
13
- # Writes the header for the wave file to path, and returns an open File object that
14
- # can be used outside the method to append the sample data. WARNING: The header contains
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, "wb")
20
- write_header(file, 0)
21
-
22
- return file
23
- end
24
-
25
- def write_header(file, sample_length)
26
- bytes_per_sample = (@bits_per_sample / 8)
27
- sample_data_size = sample_length * bytes_per_sample * @num_channels
28
-
29
- # Write the header
30
- header = CHUNK_ID
31
- header += [HEADER_SIZE + sample_data_size].pack("V")
32
- header += FORMAT
33
- header += FORMAT_CHUNK_ID
34
- header += [SUB_CHUNK1_SIZE].pack("V")
35
- header += [PCM].pack("v")
36
- header += [@num_channels].pack("v")
37
- header += [@sample_rate].pack("V")
38
- header += [@byte_rate].pack("V")
39
- header += [@block_align].pack("v")
40
- header += [@bits_per_sample].pack("v")
41
- header += DATA_CHUNK_ID
42
- header += [sample_data_size].pack("V")
43
-
44
- file.syswrite(header)
45
- end
46
-
47
- def calculate_duration(sample_rate, total_samples)
48
- samples_per_millisecond = sample_rate / 1000.0
49
- samples_per_second = sample_rate
50
- samples_per_minute = samples_per_second * 60
51
- samples_per_hour = samples_per_minute * 60
52
- hours, minutes, seconds, milliseconds = 0, 0, 0, 0
53
-
54
- if total_samples >= samples_per_hour
55
- hours = total_samples / samples_per_hour
56
- total_samples -= samples_per_hour * hours
57
- end
58
-
59
- if total_samples >= samples_per_minute
60
- minutes = total_samples / samples_per_minute
61
- total_samples -= samples_per_minute * minutes
62
- end
63
-
64
- if total_samples >= samples_per_second
65
- seconds = total_samples / samples_per_second
66
- total_samples -= samples_per_second * seconds
67
- end
68
-
69
- milliseconds = (total_samples / samples_per_millisecond).floor
70
-
71
- return { :hours => hours, :minutes => minutes, :seconds => seconds, :milliseconds => milliseconds }
72
- end
73
- end
@@ -1,111 +0,0 @@
1
- class InvalidFlowError < RuntimeError; end
2
-
3
- # This class is used for an experimental feature that allows specifying repeats inside of
4
- # individual patterns, instead of the song flow. This feature is currently disabled, so for
5
- # the time being this class is dead code.
6
- #
7
- # TODO: The expand_pattern method in this class should probably be moved to the Pattern class.
8
- # This class would then go away.
9
- class PatternExpander
10
- BARLINE = "|"
11
- TICK = "-"
12
- REPEAT_FRAME_REGEX = /:[-]*:[0-9]*/
13
- NUMBER_REGEX = /[0-9]+/
14
-
15
- # TODO: What should happen if flow is longer than pattern?
16
- # Either ignore extra flow, or add trailing .... to each track to match up?
17
- def self.expand_pattern(flow, pattern)
18
- unless self.valid_flow? flow
19
- raise InvalidFlowError, "Invalid flow"
20
- end
21
-
22
- flow = flow.delete(BARLINE)
23
-
24
- # Count number of :
25
- # If odd, then there's an implicit : at the beginning of the pattern.
26
- # TODO: What if the first character in the flow is already :
27
- # That means repeat the first step twice, right?
28
- number_of_colons = flow.scan(/:/).length
29
- if number_of_colons % 2 == 1
30
- # TODO: What if flow[0] is not '-'
31
- flow[0] = ":" # Make the implicit : at the beginning explicit
32
- end
33
-
34
- repeat_frames = parse_flow_for_repeat_frames(flow)
35
-
36
- repeat_frames.reverse.each do |frame|
37
- pattern.tracks.each do |name, track|
38
- range = frame[:range]
39
-
40
- # WARNING: Don't change the three lines below to:
41
- # track.rhythm[range] = whatever
42
- # When changing the rhythm like this, rhythm=() won't be called,
43
- # and Track.beats won't be updated as a result.
44
- new_rhythm = track.rhythm
45
- new_rhythm[range] = new_rhythm[range] * frame[:repeats]
46
- track.rhythm = new_rhythm
47
- end
48
- end
49
-
50
- return pattern
51
- end
52
-
53
- # TODO: Return more specific info on why flow isn't valid
54
- def self.valid_flow?(flow)
55
- flow = flow.delete(BARLINE)
56
- flow = flow.delete(TICK)
57
-
58
- # If flow contains any characters other than : and [0-9], it's invalid.
59
- if flow.match(/[^:0-9]/) != nil
60
- return false
61
- end
62
-
63
- # If flow contains nothing but :, it's always valid.
64
- if flow == ":" * flow.length
65
- return true
66
- end
67
-
68
- # If flow DOESN'T contain a :, it's not valid.
69
- if flow.match(/:/) == nil
70
- return false
71
- end
72
-
73
- segments = flow.split(/[0-9]+/)
74
-
75
- # Ignore first segment
76
- segments[1...segments.length].each do |segment|
77
- if segment.length % 2 == 1
78
- return false
79
- end
80
- end
81
-
82
- return true
83
- end
84
-
85
- private
86
-
87
- def self.parse_flow_for_repeat_frames(flow)
88
- repeat_frames = []
89
- lower_bound = 0
90
- frame_start_index = flow[lower_bound...flow.length] =~ REPEAT_FRAME_REGEX
91
- while frame_start_index != nil do
92
- str = flow[lower_bound...flow.length].match(REPEAT_FRAME_REGEX).to_s
93
-
94
- range_start = lower_bound + frame_start_index
95
- range_end = range_start + str.length - 1
96
-
97
- num_repeats = str.match(NUMBER_REGEX).to_s
98
- num_repeats = (num_repeats == "") ? 2 : num_repeats.to_i
99
-
100
- repeat_frame = {}
101
- repeat_frame[:range] = range_start..range_end
102
- repeat_frame[:repeats] = num_repeats
103
- repeat_frames << repeat_frame
104
-
105
- lower_bound += frame_start_index + str.length
106
- frame_start_index = flow[lower_bound...flow.length] =~ REPEAT_FRAME_REGEX
107
- end
108
-
109
- return repeat_frames
110
- end
111
- end
@@ -1,140 +0,0 @@
1
- require 'includes'
2
-
3
- class PatternExpanderTest < Test::Unit::TestCase
4
- def test_expand_pattern_no_repeats
5
- expected_pattern = Pattern.new :verse
6
- expected_pattern.track "bass", "X...X.X."
7
- expected_pattern.track "snare", "....X..."
8
-
9
- # All of these should result in no expansion, since there are no repeats.
10
- # In other words, the pattern shouldn't change.
11
- # TODO: Add test for when flow is longer than longest track in pattern
12
- ["", "|----|----|", "|----", "----:-:1"].each do |flow|
13
- actual_pattern = Pattern.new :verse
14
- actual_pattern.track "bass", "|X...|X.X.|"
15
- actual_pattern.track "snare", "|....|X...|"
16
- actual_pattern = PatternExpander.expand_pattern(flow, actual_pattern)
17
- assert(expected_pattern.same_tracks_as?(actual_pattern))
18
- end
19
- end
20
-
21
- def test_expand_pattern_single_repeats
22
- expected_pattern = Pattern.new :verse
23
- expected_pattern.track "bass", "X...X.X.X...X.X."
24
- expected_pattern.track "snare", "....X.......X..."
25
-
26
- ["|----|---:|", "|----|---:2|", ":------:"].each do |flow|
27
- actual_pattern = Pattern.new :verse
28
- actual_pattern.track "bass", "|X...|X.X.|"
29
- actual_pattern.track "snare", "|....|X...|"
30
- actual_pattern = PatternExpander.expand_pattern(flow, actual_pattern)
31
- assert(expected_pattern.same_tracks_as?(actual_pattern))
32
- end
33
-
34
-
35
- expected_pattern = Pattern.new :verse
36
- expected_pattern.track "bass", "X...X.X.X.X.X.X.X.X...X."
37
- expected_pattern.track "snare", "....X...X...X...X...XXXX"
38
-
39
- ["|----|:-:4|----|"].each do |flow|
40
- actual_pattern = Pattern.new :verse
41
- actual_pattern.track "bass", "|X...|X.X.|..X.|"
42
- actual_pattern.track "snare", "|....|X...|XXXX|"
43
- actual_pattern = PatternExpander.expand_pattern(flow, actual_pattern)
44
- assert(expected_pattern.same_tracks_as?(actual_pattern))
45
- end
46
-
47
-
48
- # Zero repeats, so section gets removed from pattern
49
- expected_pattern = Pattern.new :verse
50
- expected_pattern.track "bass", "X.....X."
51
- expected_pattern.track "snare", "....XXXX"
52
-
53
- ["|----|:-:0|----|"].each do |flow|
54
- actual_pattern = Pattern.new :verse
55
- actual_pattern.track "bass", "|X...|X.X.|..X.|"
56
- actual_pattern.track "snare", "|....|X...|XXXX|"
57
- actual_pattern = PatternExpander.expand_pattern(flow, actual_pattern)
58
- assert(expected_pattern.same_tracks_as?(actual_pattern))
59
- end
60
- end
61
-
62
- def test_expand_pattern_multiple_repeats
63
- expected_pattern = Pattern.new :verse
64
- expected_pattern.track "bass", "X...X...X.X.X.X."
65
- expected_pattern.track "snare", "........X...X..."
66
-
67
- [":--::--:", ":-:2:--:", ":-:2:-:2"].each do |flow|
68
- actual_pattern = Pattern.new :verse
69
- actual_pattern.track "bass", "|X...|X.X.|"
70
- actual_pattern.track "snare", "|....|X...|"
71
- actual_pattern = PatternExpander.expand_pattern(flow, actual_pattern)
72
- assert(expected_pattern.same_tracks_as?(actual_pattern))
73
- end
74
-
75
-
76
- expected_pattern = Pattern.new :verse
77
- expected_pattern.track "bass", "X...X.X.X.X.X.X..XXX..XX.."
78
- expected_pattern.track "snare", "....X...X...X...X...XX..XX"
79
-
80
- ["----:-:3--:--:", "|----|:-:3|--|:-:2|"].each do |flow|
81
- actual_pattern = Pattern.new :verse
82
- actual_pattern.track "bass", "|X...|X.X.|.X|XX..|"
83
- actual_pattern.track "snare", "|....|X...|X.|..XX|"
84
- actual_pattern = PatternExpander.expand_pattern(flow, actual_pattern)
85
- assert(expected_pattern.same_tracks_as?(actual_pattern))
86
- end
87
- end
88
-
89
- def test_expand_pattern_invalid
90
- pattern = Pattern.new :verse
91
- pattern.track "bass", "|X...|X.X.|"
92
- pattern.track "snare", "|....|X...|"
93
-
94
- # Patterns with an invalid character
95
- ["a", "|---!---|", "|....|....|"].each do |invalid_flow|
96
- assert_raise(InvalidFlowError) do
97
- PatternExpander.expand_pattern(invalid_flow, pattern)
98
- end
99
- end
100
-
101
- ["----4:--", ":-:-:4-:", ":4-:-:-:"].each do |invalid_flow|
102
- assert_raise(InvalidFlowError) do
103
- PatternExpander.expand_pattern(invalid_flow, pattern)
104
- end
105
- end
106
- end
107
-
108
- def test_valid_flow?
109
- # Contains nothing but :, always valid
110
- assert_equal(true, PatternExpander.valid_flow?(""))
111
- assert_equal(true, PatternExpander.valid_flow?(":"))
112
- assert_equal(true, PatternExpander.valid_flow?("::"))
113
- assert_equal(true, PatternExpander.valid_flow?(":::"))
114
- assert_equal(true, PatternExpander.valid_flow?("::::"))
115
- assert_equal(true, PatternExpander.valid_flow?("|:--:|----|:--:|"))
116
-
117
- # Contains characters other than :|- and [0-9]
118
- assert_equal(false, PatternExpander.valid_flow?("a"))
119
- assert_equal(false, PatternExpander.valid_flow?("1"))
120
- assert_equal(false, PatternExpander.valid_flow?(":--:z---"))
121
- assert_equal(false, PatternExpander.valid_flow?(": :"))
122
-
123
- assert_equal(true, PatternExpander.valid_flow?(":0"))
124
- assert_equal(true, PatternExpander.valid_flow?(":1"))
125
- assert_equal(true, PatternExpander.valid_flow?(":4"))
126
- assert_equal(true, PatternExpander.valid_flow?(":4"))
127
- assert_equal(true, PatternExpander.valid_flow?(":16"))
128
- assert_equal(true, PatternExpander.valid_flow?("::4"))
129
- assert_equal(true, PatternExpander.valid_flow?(":::4"))
130
- assert_equal(true, PatternExpander.valid_flow?(":2::4"))
131
- assert_equal(true, PatternExpander.valid_flow?("::2::4"))
132
-
133
- assert_equal(false, PatternExpander.valid_flow?(":4:"))
134
- assert_equal(false, PatternExpander.valid_flow?("::4:"))
135
- assert_equal(false, PatternExpander.valid_flow?(":4:4"))
136
- assert_equal(false, PatternExpander.valid_flow?("::2:4"))
137
- assert_equal(false, PatternExpander.valid_flow?("::2:"))
138
- assert_equal(false, PatternExpander.valid_flow?("::2:::"))
139
- end
140
- end