beats 1.2.3 → 1.2.4

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