beats 1.3.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/LICENSE +1 -1
- data/README.markdown +22 -42
- data/bin/beats +6 -7
- data/lib/beats.rb +2 -1
- data/lib/beats/audioengine.rb +13 -13
- data/lib/beats/beatsrunner.rb +7 -8
- data/lib/beats/kit.rb +12 -156
- data/lib/beats/kit_builder.rb +74 -0
- data/lib/beats/pattern.rb +2 -22
- data/lib/beats/song.rb +5 -55
- data/lib/beats/songoptimizer.rb +3 -3
- data/lib/beats/songparser.rb +25 -46
- data/lib/beats/track.rb +20 -31
- data/lib/beats/transforms/song_swinger.rb +2 -4
- data/lib/wavefile/cachingwriter.rb +2 -2
- data/test/audioengine_test.rb +22 -24
- data/test/audioutils_test.rb +1 -1
- data/test/cachingwriter_test.rb +13 -12
- data/test/fixtures/invalid/leading_bar_line.txt +15 -0
- data/test/fixtures/{valid → invalid}/with_structure.txt +2 -2
- data/test/fixtures/valid/multiple_tracks_same_sound.txt +2 -1
- data/test/fixtures/valid/optimize_pattern_collision.txt +4 -5
- data/test/fixtures/valid/track_with_spaces.txt +13 -0
- data/test/includes.rb +1 -4
- data/test/integration_test.rb +5 -5
- data/test/kit_builder_test.rb +52 -0
- data/test/kit_test.rb +18 -141
- data/test/pattern_test.rb +66 -1
- data/test/song_swinger_test.rb +2 -2
- data/test/song_test.rb +9 -33
- data/test/songoptimizer_test.rb +18 -18
- data/test/songparser_test.rb +20 -10
- data/test/track_test.rb +23 -9
- metadata +26 -31
- data/ext/mkrf_conf.rb +0 -28
- data/test/fixtures/invalid/template.txt +0 -31
- data/test/fixtures/valid/foo.txt +0 -18
- data/test/sounds/bass.wav +0 -0
- data/test/sounds/bass2.wav +0 -0
- data/test/sounds/sine-mono-8bit.wav +0 -0
- data/test/sounds/tone.wav +0 -0
@@ -0,0 +1,74 @@
|
|
1
|
+
module Beats
|
2
|
+
class KitBuilder
|
3
|
+
# Raised when trying to load a sound file which can't be found at the path specified
|
4
|
+
class SoundFileNotFoundError < RuntimeError; end
|
5
|
+
|
6
|
+
# Raised when trying to load a sound file which either isn't actually a sound file, or
|
7
|
+
# is in an unsupported format.
|
8
|
+
class InvalidSoundFormatError < RuntimeError; end
|
9
|
+
|
10
|
+
BITS_PER_SAMPLE = 16
|
11
|
+
SAMPLE_FORMAT = "pcm_#{BITS_PER_SAMPLE}".to_sym
|
12
|
+
SAMPLE_RATE = 44100
|
13
|
+
|
14
|
+
def initialize(base_path)
|
15
|
+
@base_path = base_path
|
16
|
+
@labels_to_filenames = {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def add_item(label, filename)
|
20
|
+
@labels_to_filenames[label] = absolute_file_name(filename)
|
21
|
+
end
|
22
|
+
|
23
|
+
def has_label?(label)
|
24
|
+
@labels_to_filenames.keys.include?(label)
|
25
|
+
end
|
26
|
+
|
27
|
+
def build_kit
|
28
|
+
# Load each sample buffer
|
29
|
+
filenames_to_buffers = {}
|
30
|
+
@labels_to_filenames.values.uniq.each do |filename|
|
31
|
+
filenames_to_buffers[filename] = load_sample_buffer(filename)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Convert each buffer to the same sample format
|
35
|
+
num_channels = filenames_to_buffers.values.map(&:channels).max || 1
|
36
|
+
canonical_format = WaveFile::Format.new(num_channels, SAMPLE_FORMAT, SAMPLE_RATE)
|
37
|
+
filenames_to_buffers.values.each {|buffer| buffer.convert!(canonical_format) }
|
38
|
+
|
39
|
+
labels_to_buffers = {}
|
40
|
+
@labels_to_filenames.each do |label, filename|
|
41
|
+
labels_to_buffers[label] = filenames_to_buffers[filename].samples
|
42
|
+
end
|
43
|
+
labels_to_buffers[Kit::PLACEHOLDER_TRACK_NAME] = []
|
44
|
+
|
45
|
+
Kit.new(labels_to_buffers, num_channels, BITS_PER_SAMPLE)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
# Converts relative path into absolute path. Note that this will also handle
|
51
|
+
# expanding ~ on platforms that support that.
|
52
|
+
def absolute_file_name(filename)
|
53
|
+
File.expand_path(filename, @base_path)
|
54
|
+
end
|
55
|
+
|
56
|
+
def load_sample_buffer(filename)
|
57
|
+
sample_buffer = nil
|
58
|
+
|
59
|
+
begin
|
60
|
+
reader = WaveFile::Reader.new(filename)
|
61
|
+
reader.each_buffer(reader.total_sample_frames) do |buffer|
|
62
|
+
sample_buffer = buffer
|
63
|
+
end
|
64
|
+
rescue Errno::ENOENT
|
65
|
+
raise SoundFileNotFoundError, "Sound file #{filename} not found."
|
66
|
+
rescue StandardError
|
67
|
+
raise InvalidSoundFormatError, "Sound file #{filename} is either not a sound file, " +
|
68
|
+
"or is in an unsupported format. BEATS can handle 8, 16, 24, or 32-bit PCM *.wav files."
|
69
|
+
end
|
70
|
+
|
71
|
+
sample_buffer
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/lib/beats/pattern.rb
CHANGED
@@ -5,8 +5,6 @@ module Beats
|
|
5
5
|
# This object is like sheet music; the AudioEngine is responsible creating actual
|
6
6
|
# audio data for a Pattern (with the help of a Kit).
|
7
7
|
class Pattern
|
8
|
-
FLOW_TRACK_NAME = "flow"
|
9
|
-
|
10
8
|
def initialize(name)
|
11
9
|
@name = name
|
12
10
|
@tracks = {}
|
@@ -21,7 +19,7 @@ module Beats
|
|
21
19
|
# If the new track is longer than any of the previously added tracks,
|
22
20
|
# pad the other tracks with trailing . to make them all the same length.
|
23
21
|
# Necessary to prevent incorrect overflow calculations for tracks.
|
24
|
-
longest_track_length = step_count
|
22
|
+
longest_track_length = step_count
|
25
23
|
@tracks.values.each do |track|
|
26
24
|
if track.rhythm.length < longest_track_length
|
27
25
|
track.rhythm += "." * (longest_track_length - track.rhythm.length)
|
@@ -49,25 +47,7 @@ module Beats
|
|
49
47
|
@tracks.length == other_pattern.tracks.length
|
50
48
|
end
|
51
49
|
|
52
|
-
|
53
|
-
# version of to_yaml().
|
54
|
-
def to_yaml
|
55
|
-
longest_track_name_length =
|
56
|
-
@tracks.keys.inject(0) do |max_length, name|
|
57
|
-
(name.to_s.length > max_length) ? name.to_s.length : max_length
|
58
|
-
end
|
59
|
-
ljust_amount = longest_track_name_length + 7
|
60
|
-
|
61
|
-
yaml = "#{@name.to_s.capitalize}:\n"
|
62
|
-
@tracks.keys.sort.each do |track_name|
|
63
|
-
yaml += " - #{track_name}:".ljust(ljust_amount)
|
64
|
-
yaml += "#{@tracks[track_name].rhythm}\n"
|
65
|
-
end
|
66
|
-
|
67
|
-
yaml
|
68
|
-
end
|
69
|
-
|
70
|
-
attr_accessor :tracks, :name
|
50
|
+
attr_reader :tracks, :name
|
71
51
|
|
72
52
|
private
|
73
53
|
|
data/lib/beats/song.rb
CHANGED
@@ -1,7 +1,4 @@
|
|
1
1
|
module Beats
|
2
|
-
class InvalidTempoError < RuntimeError; end
|
3
|
-
|
4
|
-
|
5
2
|
# Domain object which models the 'sheet music' for a full song. Models the Patterns
|
6
3
|
# that should be played, in which order (i.e. the flow), and at which tempo.
|
7
4
|
#
|
@@ -10,6 +7,8 @@ module Beats
|
|
10
7
|
# A Kit provides the sample data for each of these sounds. With a Song and a Kit
|
11
8
|
# the AudioEngine can produce the audio data that is saved to disk.
|
12
9
|
class Song
|
10
|
+
class InvalidTempoError < RuntimeError; end
|
11
|
+
|
13
12
|
DEFAULT_TEMPO = 120
|
14
13
|
|
15
14
|
def initialize
|
@@ -21,7 +20,6 @@ module Beats
|
|
21
20
|
# Adds a new pattern to the song, with the specified name.
|
22
21
|
def pattern(name)
|
23
22
|
@patterns[name] = Pattern.new(name)
|
24
|
-
@patterns[name]
|
25
23
|
end
|
26
24
|
|
27
25
|
|
@@ -52,7 +50,7 @@ module Beats
|
|
52
50
|
end
|
53
51
|
|
54
52
|
def tempo=(new_tempo)
|
55
|
-
unless (new_tempo.
|
53
|
+
unless (new_tempo.is_a?(Integer) || new_tempo.class == Float) && new_tempo > 0
|
56
54
|
raise InvalidTempoError, "Invalid tempo: '#{new_tempo}'. Tempo must be a number greater than 0."
|
57
55
|
end
|
58
56
|
|
@@ -61,7 +59,7 @@ module Beats
|
|
61
59
|
|
62
60
|
# Returns a new Song that is identical but with no patterns or flow.
|
63
61
|
def copy_ignoring_patterns_and_flow
|
64
|
-
copy = Song.new
|
62
|
+
copy = Song.new
|
65
63
|
copy.tempo = @tempo
|
66
64
|
|
67
65
|
copy
|
@@ -75,7 +73,7 @@ module Beats
|
|
75
73
|
track_names = track_names()
|
76
74
|
|
77
75
|
track_names.each do |track_name|
|
78
|
-
new_song = copy_ignoring_patterns_and_flow
|
76
|
+
new_song = copy_ignoring_patterns_and_flow
|
79
77
|
|
80
78
|
@patterns.each do |name, original_pattern|
|
81
79
|
new_pattern = new_song.pattern(name)
|
@@ -102,23 +100,6 @@ module Beats
|
|
102
100
|
@patterns.reject! {|k, pattern| !@flow.member?(pattern.name) }
|
103
101
|
end
|
104
102
|
|
105
|
-
# Serializes the current Song to a YAML string. This string can then be used to construct a new Song
|
106
|
-
# using the SongParser class. This lets you save a Song to disk, to be re-loaded later. Produces nicer
|
107
|
-
# looking output than the default version of to_yaml().
|
108
|
-
def to_yaml(kit)
|
109
|
-
# This implementation intentionally builds up a YAML string manually instead of using YAML::dump().
|
110
|
-
# Ruby 1.8 makes it difficult to ensure a consistent ordering of hash keys, which makes the output ugly
|
111
|
-
# and also hard to test.
|
112
|
-
|
113
|
-
yaml_output = "Song:\n"
|
114
|
-
yaml_output += " Tempo: #{@tempo}\n"
|
115
|
-
yaml_output += flow_to_yaml()
|
116
|
-
yaml_output += kit.to_yaml(2)
|
117
|
-
yaml_output += patterns_to_yaml()
|
118
|
-
|
119
|
-
yaml_output
|
120
|
-
end
|
121
|
-
|
122
103
|
attr_reader :patterns, :tempo
|
123
104
|
attr_accessor :flow
|
124
105
|
|
@@ -127,36 +108,5 @@ module Beats
|
|
127
108
|
def longest_length_in_array(arr)
|
128
109
|
arr.inject(0) {|max_length, name| [name.to_s.length, max_length].max }
|
129
110
|
end
|
130
|
-
|
131
|
-
def flow_to_yaml
|
132
|
-
yaml_output = " Flow:\n"
|
133
|
-
ljust_amount = longest_length_in_array(@flow) + 1 # The +1 is for the trailing ":"
|
134
|
-
previous = nil
|
135
|
-
count = 0
|
136
|
-
@flow.each do |pattern_name|
|
137
|
-
if pattern_name == previous || previous.nil?
|
138
|
-
count += 1
|
139
|
-
else
|
140
|
-
yaml_output += " - #{(previous.to_s.capitalize + ':').ljust(ljust_amount)} x#{count}\n"
|
141
|
-
count = 1
|
142
|
-
end
|
143
|
-
previous = pattern_name
|
144
|
-
end
|
145
|
-
yaml_output += " - #{(previous.to_s.capitalize + ':').ljust(ljust_amount)} x#{count}\n"
|
146
|
-
|
147
|
-
yaml_output
|
148
|
-
end
|
149
|
-
|
150
|
-
def patterns_to_yaml
|
151
|
-
yaml_output = ""
|
152
|
-
|
153
|
-
# Sort to ensure a consistent order, to make testing easier
|
154
|
-
pattern_names = @patterns.keys.map {|key| key.to_s} # Ruby 1.8 can't sort symbols...
|
155
|
-
pattern_names.sort.each do |pattern_name|
|
156
|
-
yaml_output += "\n" + @patterns[pattern_name.to_sym].to_yaml()
|
157
|
-
end
|
158
|
-
|
159
|
-
yaml_output
|
160
|
-
end
|
161
111
|
end
|
162
112
|
end
|
data/lib/beats/songoptimizer.rb
CHANGED
@@ -20,7 +20,7 @@ module Beats
|
|
20
20
|
# generated faster.
|
21
21
|
def optimize(original_song, max_pattern_length)
|
22
22
|
# 1.) Create a new song, cloned from the original
|
23
|
-
optimized_song = original_song.copy_ignoring_patterns_and_flow
|
23
|
+
optimized_song = original_song.copy_ignoring_patterns_and_flow
|
24
24
|
|
25
25
|
# 2.) Subdivide patterns
|
26
26
|
optimized_song = subdivide_song_patterns(original_song, optimized_song, max_pattern_length)
|
@@ -82,7 +82,7 @@ module Beats
|
|
82
82
|
# Otherwise, this pattern will have no steps, and no sound will be generated,
|
83
83
|
# causing the pattern to be "compacted away".
|
84
84
|
if new_pattern.tracks.empty?
|
85
|
-
new_pattern.track(
|
85
|
+
new_pattern.track(Kit::PLACEHOLDER_TRACK_NAME, blank_track_pattern)
|
86
86
|
end
|
87
87
|
|
88
88
|
step_index += max_pattern_length
|
@@ -122,7 +122,7 @@ module Beats
|
|
122
122
|
song.flow = new_flow
|
123
123
|
|
124
124
|
# This isn't strictly necessary, but makes resulting songs easier to read for debugging purposes.
|
125
|
-
song.remove_unused_patterns
|
125
|
+
song.remove_unused_patterns
|
126
126
|
|
127
127
|
song
|
128
128
|
end
|
data/lib/beats/songparser.rb
CHANGED
@@ -1,7 +1,4 @@
|
|
1
1
|
module Beats
|
2
|
-
class SongParseError < RuntimeError; end
|
3
|
-
|
4
|
-
|
5
2
|
# This class is used to parse a raw YAML song definition into domain objects (i.e.
|
6
3
|
# Song, Pattern, Track, and Kit). These domain objects can then be used by AudioEngine
|
7
4
|
# to generate the actual audio data that is saved to disk.
|
@@ -9,11 +6,7 @@ module Beats
|
|
9
6
|
# The sole public method is parse(). It takes a raw YAML string and returns a Song and
|
10
7
|
# Kit object (or raises an error if the YAML string couldn't be parsed correctly).
|
11
8
|
class SongParser
|
12
|
-
|
13
|
-
"\n" +
|
14
|
-
"WARNING! This song contains a 'Structure' section in the header.\n" +
|
15
|
-
"As of BEATS 1.2.1, the 'Structure' section should be renamed 'Flow'.\n" +
|
16
|
-
"You should change your song file, in a future version using 'Structure' will cause an error.\n"
|
9
|
+
class ParseError < RuntimeError; end
|
17
10
|
|
18
11
|
NO_SONG_HEADER_ERROR_MSG =
|
19
12
|
"Song must have a header. Here's an example:
|
@@ -43,17 +36,17 @@ module Beats
|
|
43
36
|
unless raw_song_components[:tempo].nil?
|
44
37
|
song.tempo = raw_song_components[:tempo]
|
45
38
|
end
|
46
|
-
rescue InvalidTempoError => detail
|
47
|
-
raise
|
39
|
+
rescue Song::InvalidTempoError => detail
|
40
|
+
raise ParseError, "#{detail}"
|
48
41
|
end
|
49
42
|
|
50
43
|
# 2.) Build the kit
|
51
44
|
begin
|
52
45
|
kit = build_kit(base_path, raw_song_components[:kit], raw_song_components[:patterns])
|
53
|
-
rescue SoundFileNotFoundError => detail
|
54
|
-
raise
|
55
|
-
rescue InvalidSoundFormatError => detail
|
56
|
-
raise
|
46
|
+
rescue KitBuilder::SoundFileNotFoundError => detail
|
47
|
+
raise ParseError, "#{detail}"
|
48
|
+
rescue KitBuilder::InvalidSoundFormatError => detail
|
49
|
+
raise ParseError, "#{detail}"
|
57
50
|
end
|
58
51
|
|
59
52
|
# 3.) Load patterns
|
@@ -61,7 +54,7 @@ module Beats
|
|
61
54
|
|
62
55
|
# 4.) Set flow
|
63
56
|
if raw_song_components[:flow].nil?
|
64
|
-
raise
|
57
|
+
raise ParseError, "Song must have a Flow section in the header."
|
65
58
|
else
|
66
59
|
set_song_flow(song, raw_song_components[:flow])
|
67
60
|
end
|
@@ -70,8 +63,8 @@ module Beats
|
|
70
63
|
if raw_song_components[:swing]
|
71
64
|
begin
|
72
65
|
song = Transforms::SongSwinger.transform(song, raw_song_components[:swing])
|
73
|
-
rescue Transforms::InvalidSwingRateError => detail
|
74
|
-
raise
|
66
|
+
rescue Transforms::SongSwinger::InvalidSwingRateError => detail
|
67
|
+
raise ParseError, "#{detail}"
|
75
68
|
end
|
76
69
|
end
|
77
70
|
|
@@ -85,8 +78,10 @@ module Beats
|
|
85
78
|
def hashify_raw_yaml(raw_yaml_string)
|
86
79
|
begin
|
87
80
|
raw_song_definition = YAML.load(raw_yaml_string)
|
81
|
+
rescue Psych::SyntaxError => detail
|
82
|
+
raise ParseError, "Syntax error in YAML file: #{detail}"
|
88
83
|
rescue ArgumentError => detail
|
89
|
-
raise
|
84
|
+
raise ParseError, "Syntax error in YAML file: #{detail}"
|
90
85
|
end
|
91
86
|
|
92
87
|
raw_song_components = {}
|
@@ -95,23 +90,13 @@ module Beats
|
|
95
90
|
unless raw_song_components[:full_definition]["song"].nil?
|
96
91
|
raw_song_components[:header] = downcase_hash_keys(raw_song_components[:full_definition]["song"])
|
97
92
|
else
|
98
|
-
raise
|
93
|
+
raise ParseError, NO_SONG_HEADER_ERROR_MSG
|
99
94
|
end
|
100
95
|
raw_song_components[:tempo] = raw_song_components[:header]["tempo"]
|
101
96
|
raw_song_components[:folder] = raw_song_components[:header]["folder"]
|
102
97
|
raw_song_components[:kit] = raw_song_components[:header]["kit"]
|
103
98
|
|
104
|
-
|
105
|
-
raw_structure = raw_song_components[:header]["structure"]
|
106
|
-
unless raw_flow.nil?
|
107
|
-
raw_song_components[:flow] = raw_flow
|
108
|
-
else
|
109
|
-
unless raw_structure.nil?
|
110
|
-
puts DONT_USE_STRUCTURE_WARNING
|
111
|
-
end
|
112
|
-
|
113
|
-
raw_song_components[:flow] = raw_structure
|
114
|
-
end
|
99
|
+
raw_song_components[:flow] = raw_song_components[:header]["flow"]
|
115
100
|
|
116
101
|
raw_song_components[:swing] = raw_song_components[:header]["swing"]
|
117
102
|
raw_song_components[:patterns] = raw_song_components[:full_definition].reject {|k, v| k == "song"}
|
@@ -119,23 +104,19 @@ module Beats
|
|
119
104
|
return raw_song_components
|
120
105
|
end
|
121
106
|
|
122
|
-
|
123
107
|
def build_kit(base_path, raw_kit, raw_patterns)
|
124
|
-
|
108
|
+
kit_builder = KitBuilder.new(base_path)
|
125
109
|
|
126
110
|
# Add sounds defined in the Kit section of the song header
|
127
111
|
# Converts [{a=>1}, {b=>2}, {c=>3}] from raw YAML to {a=>1, b=>2, c=>3}
|
128
112
|
# TODO: Raise error is same name is defined more than once in the Kit
|
129
113
|
unless raw_kit.nil?
|
130
114
|
raw_kit.each do |kit_item|
|
131
|
-
|
115
|
+
kit_builder.add_item(kit_item.keys.first, kit_item.values.first)
|
132
116
|
end
|
133
117
|
end
|
134
118
|
|
135
119
|
# Add sounds not defined in Kit section, but used in individual tracks
|
136
|
-
# TODO Investigate detecting duplicate keys already defined in the Kit section, as this could possibly
|
137
|
-
# result in a performance improvement when the sound has to be converted to a different bit rate/num channels,
|
138
|
-
# as well as use less memory.
|
139
120
|
raw_patterns.keys.each do |key|
|
140
121
|
track_list = raw_patterns[key]
|
141
122
|
|
@@ -144,30 +125,28 @@ module Beats
|
|
144
125
|
track_name = track_definition.keys.first
|
145
126
|
track_path = track_name
|
146
127
|
|
147
|
-
if
|
148
|
-
|
128
|
+
if !kit_builder.has_label?(track_name)
|
129
|
+
kit_builder.add_item(track_name, track_path)
|
149
130
|
end
|
150
131
|
end
|
151
132
|
end
|
152
133
|
end
|
153
134
|
|
154
|
-
|
135
|
+
kit_builder.build_kit
|
155
136
|
end
|
156
137
|
|
157
|
-
|
158
138
|
def add_patterns_to_song(song, raw_patterns)
|
159
139
|
raw_patterns.keys.each do |key|
|
160
140
|
new_pattern = song.pattern key.to_sym
|
161
141
|
|
162
142
|
track_list = raw_patterns[key]
|
163
|
-
|
143
|
+
|
164
144
|
if track_list.nil?
|
165
145
|
# TODO: Use correct capitalization of pattern name in error message
|
166
146
|
# TODO: Possibly allow if pattern not referenced in the Flow, or has 0 repeats?
|
167
|
-
raise
|
147
|
+
raise ParseError, "Pattern '#{key}' has no tracks. It needs at least one."
|
168
148
|
end
|
169
149
|
|
170
|
-
# TODO: What if there is more than one flow? Raise error, or have last one win?
|
171
150
|
track_list.each do |track_definition|
|
172
151
|
track_name = track_definition.keys.first
|
173
152
|
|
@@ -197,16 +176,16 @@ module Beats
|
|
197
176
|
multiples = multiples_str.to_i
|
198
177
|
|
199
178
|
unless multiples_str.match(/[^0-9]/).nil?
|
200
|
-
raise
|
179
|
+
raise ParseError,
|
201
180
|
"'#{multiples_str}' is an invalid number of repeats for pattern '#{pattern_name}'. Number of repeats should be a whole number."
|
202
181
|
else
|
203
182
|
if multiples < 0
|
204
|
-
raise
|
183
|
+
raise ParseError, "'#{multiples_str}' is an invalid number of repeats for pattern '#{pattern_name}'. Must be 0 or greater."
|
205
184
|
elsif multiples > 0 && !song.patterns.has_key?(pattern_name_sym)
|
206
185
|
# This test is purposefully designed to only throw an error if the number of repeats is greater
|
207
186
|
# than 0. This allows you to specify an undefined pattern in the flow with "x0" repeats.
|
208
187
|
# This can be convenient for defining the flow before all patterns have been added to the song file.
|
209
|
-
raise
|
188
|
+
raise ParseError, "Song flow includes non-existent pattern: #{pattern_name}."
|
210
189
|
end
|
211
190
|
end
|
212
191
|
|
data/lib/beats/track.rb
CHANGED
@@ -1,57 +1,46 @@
|
|
1
1
|
module Beats
|
2
|
-
class InvalidRhythmError < RuntimeError; end
|
3
|
-
|
4
|
-
|
5
2
|
# Domain object which models a kit sound playing a rhythm. For example,
|
6
3
|
# a bass drum playing every quarter note for two measures.
|
7
4
|
#
|
8
5
|
# This object is like sheet music; the AudioEngine is responsible creating actual
|
9
6
|
# audio data for a Track (with the help of a Kit).
|
10
7
|
class Track
|
8
|
+
class InvalidRhythmError < RuntimeError; end
|
9
|
+
|
11
10
|
REST = "."
|
12
11
|
BEAT = "X"
|
13
12
|
BARLINE = "|"
|
13
|
+
SPACE = " "
|
14
|
+
DISALLOWED_CHARACTERS = /[^X\.]/ # I.e., anything not an 'X' or a '.'
|
14
15
|
|
15
16
|
def initialize(name, rhythm)
|
16
|
-
# TODO: Add validation for input parameters
|
17
17
|
@name = name
|
18
18
|
self.rhythm = rhythm
|
19
19
|
end
|
20
20
|
|
21
|
-
# TODO: What to have this invoked when setting like this?
|
22
|
-
# track.rhythm[x..y] = whatever
|
23
21
|
def rhythm=(rhythm)
|
24
22
|
@rhythm = rhythm.delete(BARLINE)
|
25
|
-
|
26
|
-
|
27
|
-
beat_length = 0
|
28
|
-
#rhythm.each_char{|ch|
|
29
|
-
@rhythm.each_byte do |ch|
|
30
|
-
ch = ch.chr
|
31
|
-
if ch == BEAT
|
32
|
-
beats << beat_length
|
33
|
-
beat_length = 1
|
34
|
-
elsif ch == REST
|
35
|
-
beat_length += 1
|
36
|
-
else
|
37
|
-
raise InvalidRhythmError, "Track #{@name} has an invalid rhythm: '#{rhythm}'. Can only contain '#{BEAT}', '#{REST}' or '#{BARLINE}'"
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
if beat_length > 0
|
42
|
-
beats << beat_length
|
43
|
-
end
|
44
|
-
if beats == []
|
45
|
-
beats = [0]
|
46
|
-
end
|
47
|
-
@beats = beats
|
23
|
+
@rhythm = @rhythm.delete(SPACE)
|
24
|
+
@trigger_step_lengths = calculate_trigger_step_lengths
|
48
25
|
end
|
49
26
|
|
50
27
|
def step_count
|
51
28
|
@rhythm.length
|
52
29
|
end
|
53
30
|
|
54
|
-
|
55
|
-
|
31
|
+
attr_reader :name, :rhythm, :trigger_step_lengths
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def calculate_trigger_step_lengths
|
36
|
+
if @rhythm.match(DISALLOWED_CHARACTERS)
|
37
|
+
raise InvalidRhythmError, "Track #{@name} has an invalid rhythm: '#{rhythm}'. Can only contain '#{BEAT}', '#{REST}', '#{BARLINE}', or ' '"
|
38
|
+
end
|
39
|
+
|
40
|
+
trigger_step_lengths = @rhythm.scan(/X?\.*/)[0..-2].map(&:length)
|
41
|
+
trigger_step_lengths.unshift(0) unless @rhythm.start_with?(REST)
|
42
|
+
|
43
|
+
trigger_step_lengths
|
44
|
+
end
|
56
45
|
end
|
57
46
|
end
|