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