beats 1.3.0 → 2.0.0
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.
- 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
|