beats 1.2.3 → 1.2.4
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +1 -1
- data/README.markdown +30 -32
- data/bin/beats +2 -1
- data/lib/audioengine.rb +13 -32
- data/lib/beats.rb +1 -1
- data/lib/kit.rb +15 -14
- data/lib/pattern.rb +1 -1
- data/lib/song.rb +3 -7
- data/lib/songoptimizer.rb +1 -1
- data/lib/songparser.rb +1 -1
- data/lib/track.rb +1 -1
- data/lib/wavefile/cachingwriter.rb +36 -0
- data/test/cachingwriter_test.rb +63 -0
- data/test/fixtures/expected_output/example_combined_mono_8.wav +0 -0
- data/test/fixtures/expected_output/example_combined_stereo_8.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_8-agogo.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_8-bass.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_8-hh_closed.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_8-hh_closed2.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_8-snare.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_8-tom2_mono_8.wav +0 -0
- data/test/fixtures/expected_output/example_split_mono_8-tom4_mono_8.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_8-agogo.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_8-bass.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_8-hh_closed.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_8-hh_closed2.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_8-snare.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_8-tom2_stereo_8.wav +0 -0
- data/test/fixtures/expected_output/example_split_stereo_8-tom4_stereo_8.wav +0 -0
- data/test/includes.rb +4 -2
- data/test/integration_test.rb +6 -2
- data/test/sounds/agogo_high_mono_8.wav +0 -0
- data/test/sounds/agogo_high_stereo_8.wav +0 -0
- data/test/sounds/agogo_low_mono_8.wav +0 -0
- data/test/sounds/agogo_low_stereo_8.wav +0 -0
- data/test/sounds/bass2_mono_8.wav +0 -0
- data/test/sounds/bass2_stereo_8.wav +0 -0
- data/test/sounds/bass_mono_8.wav +0 -0
- data/test/sounds/bass_stereo_8.wav +0 -0
- data/test/sounds/clave_high_mono_8.wav +0 -0
- data/test/sounds/clave_high_stereo_8.wav +0 -0
- data/test/sounds/clave_low_mono_8.wav +0 -0
- data/test/sounds/clave_low_stereo_8.wav +0 -0
- data/test/sounds/conga_high_mono_8.wav +0 -0
- data/test/sounds/conga_high_stereo_8.wav +0 -0
- data/test/sounds/conga_low_mono_8.wav +0 -0
- data/test/sounds/conga_low_stereo_8.wav +0 -0
- data/test/sounds/cowbell_high_mono_8.wav +0 -0
- data/test/sounds/cowbell_high_stereo_8.wav +0 -0
- data/test/sounds/cowbell_low_mono_8.wav +0 -0
- data/test/sounds/cowbell_low_stereo_8.wav +0 -0
- data/test/sounds/hh_closed_mono_8.wav +0 -0
- data/test/sounds/hh_closed_stereo_8.wav +0 -0
- data/test/sounds/hh_open_mono_8.wav +0 -0
- data/test/sounds/hh_open_stereo_8.wav +0 -0
- data/test/sounds/ride_mono_8.wav +0 -0
- data/test/sounds/ride_stereo_8.wav +0 -0
- data/test/sounds/rim_mono_8.wav +0 -0
- data/test/sounds/rim_stereo_8.wav +0 -0
- data/test/sounds/snare2_mono_8.wav +0 -0
- data/test/sounds/snare2_stereo_8.wav +0 -0
- data/test/sounds/snare_mono_8.wav +0 -0
- data/test/sounds/snare_stereo_8.wav +0 -0
- data/test/sounds/tom1_mono_8.wav +0 -0
- data/test/sounds/tom1_stereo_8.wav +0 -0
- data/test/sounds/tom2_mono_8.wav +0 -0
- data/test/sounds/tom2_stereo_8.wav +0 -0
- data/test/sounds/tom3_mono_8.wav +0 -0
- data/test/sounds/tom3_stereo_8.wav +0 -0
- data/test/sounds/tom4_mono_8.wav +0 -0
- data/test/sounds/tom4_stereo_8.wav +0 -0
- metadata +41 -43
- data/bin/example_song.wav +0 -0
- data/lib/beatswavefile.rb +0 -73
- data/lib/patternexpander.rb +0 -111
- data/test/patternexpander_test.rb +0 -140
data/LICENSE
CHANGED
data/README.markdown
CHANGED
@@ -1,66 +1,64 @@
|
|
1
|
-
|
1
|
+
Beats Drum Machine
|
2
2
|
------------------
|
3
3
|
|
4
|
-
|
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:
|
7
|
+
Tempo: 105
|
8
8
|
Flow:
|
9
|
-
- Verse:
|
10
|
-
- Chorus:
|
11
|
-
- Verse: x2
|
12
|
-
- Chorus: x4
|
9
|
+
- Verse: x4
|
10
|
+
- Chorus: x4
|
13
11
|
Kit:
|
14
|
-
- bass:
|
15
|
-
- snare:
|
16
|
-
-
|
17
|
-
-
|
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:
|
21
|
-
- snare:
|
22
|
-
-
|
23
|
-
|
24
|
-
|
19
|
+
- bass: X..X...X..X.....
|
20
|
+
- snare: ....X.......X...
|
21
|
+
- hihat: ..X...X...X...X.
|
22
|
+
|
25
23
|
Chorus:
|
26
|
-
- bass:
|
27
|
-
- snare:
|
28
|
-
-
|
29
|
-
-
|
30
|
-
-
|
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
|
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
|
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
|
-
*
|
41
|
-
*
|
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.
|
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
|
51
|
+
Once installed, you can then run Beats from the command-line using the `beats` command.
|
54
52
|
|
55
|
-
|
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
|
-
|
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
|
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
|
-
|
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/
|
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}
|
data/lib/audioengine.rb
CHANGED
@@ -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
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
-
|
41
|
-
|
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
|
-
|
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,
|
59
|
-
final_overflow_composite = AudioUtils.scale(final_overflow_composite,
|
60
|
-
|
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
|
-
|
52
|
+
writer.close()
|
72
53
|
|
73
|
-
return
|
54
|
+
return WaveFile::Reader.info(output_file_name).duration
|
74
55
|
end
|
75
56
|
|
76
57
|
attr_reader :step_sample_length
|
data/lib/beats.rb
CHANGED
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
|
-
|
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
|
-
|
130
|
-
|
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,
|
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
|
-
|
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
|
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].
|
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)
|
data/lib/pattern.rb
CHANGED
@@ -31,7 +31,7 @@ class Pattern
|
|
31
31
|
end
|
32
32
|
|
33
33
|
def step_count
|
34
|
-
|
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
|
data/lib/song.rb
CHANGED
@@ -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
|
data/lib/songoptimizer.rb
CHANGED
@@ -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
|
data/lib/songparser.rb
CHANGED
data/lib/track.rb
CHANGED
@@ -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
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
data/test/includes.rb
CHANGED
@@ -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 '
|
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)
|
data/test/integration_test.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
data/test/sounds/bass_mono_8.wav
CHANGED
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
data/test/sounds/ride_mono_8.wav
CHANGED
Binary file
|
Binary file
|
data/test/sounds/rim_mono_8.wav
CHANGED
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
data/test/sounds/tom1_mono_8.wav
CHANGED
Binary file
|
Binary file
|
data/test/sounds/tom2_mono_8.wav
CHANGED
Binary file
|
Binary file
|
data/test/sounds/tom3_mono_8.wav
CHANGED
Binary file
|
Binary file
|
data/test/sounds/tom4_mono_8.wav
CHANGED
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
|
-
|
14
|
-
|
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
|
-
|
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.
|
18
|
+
requirements:
|
19
|
+
- - '='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 0.4.0
|
24
22
|
type: :runtime
|
25
|
-
|
26
|
-
|
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:
|
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:
|
222
|
+
requirements:
|
223
|
+
- - ! '>='
|
224
|
+
- !ruby/object:Gem::Version
|
225
|
+
version: '0'
|
227
226
|
requirements: []
|
228
|
-
|
229
227
|
rubyforge_project:
|
230
|
-
rubygems_version: 1.8.
|
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
|
234
|
-
|
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:
|
data/bin/example_song.wav
DELETED
Binary file
|
data/lib/beatswavefile.rb
DELETED
@@ -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
|
data/lib/patternexpander.rb
DELETED
@@ -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
|