beats 1.2.4 → 1.2.5
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 +7 -0
- data/LICENSE +1 -1
- data/README.markdown +24 -4
- data/Rakefile +1 -1
- data/bin/beats +10 -15
- data/ext/mkrf_conf.rb +28 -0
- data/lib/beats.rb +12 -76
- data/lib/beats/audioengine.rb +163 -0
- data/lib/beats/audioutils.rb +74 -0
- data/lib/beats/beatsrunner.rb +76 -0
- data/lib/beats/kit.rb +187 -0
- data/lib/beats/pattern.rb +88 -0
- data/lib/beats/song.rb +162 -0
- data/lib/beats/songoptimizer.rb +162 -0
- data/lib/beats/songparser.rb +217 -0
- data/lib/beats/track.rb +57 -0
- data/lib/wavefile/cachingwriter.rb +1 -1
- data/test/audioengine_test.rb +5 -5
- data/test/cachingwriter_test.rb +1 -1
- data/test/fixtures/valid/foo.txt +18 -0
- data/test/includes.rb +2 -9
- data/test/integration_test.rb +20 -20
- data/test/kit_test.rb +28 -28
- data/test/pattern_test.rb +13 -13
- data/test/song_test.rb +23 -23
- data/test/songoptimizer_test.rb +25 -25
- data/test/songparser_test.rb +11 -11
- data/test/sounds/bass.wav +0 -0
- data/test/sounds/bass2.wav +0 -0
- data/test/track_test.rb +12 -12
- metadata +27 -22
- data/lib/audioengine.rb +0 -161
- data/lib/audioutils.rb +0 -73
- data/lib/kit.rb +0 -185
- data/lib/pattern.rb +0 -86
- data/lib/song.rb +0 -160
- data/lib/songoptimizer.rb +0 -160
- data/lib/songparser.rb +0 -216
- data/lib/track.rb +0 -55
data/lib/audioutils.rb
DELETED
@@ -1,73 +0,0 @@
|
|
1
|
-
# This class contains some utility methods for working with sample data.
|
2
|
-
class AudioUtils
|
3
|
-
|
4
|
-
# Combines multiple sample arrays into one, by adding them together.
|
5
|
-
# When the sample arrays are different lengths, the output array will be the length
|
6
|
-
# of the longest input array.
|
7
|
-
# WARNING: Incoming arrays can be modified.
|
8
|
-
def self.composite(sample_arrays, num_channels)
|
9
|
-
if sample_arrays == []
|
10
|
-
return []
|
11
|
-
end
|
12
|
-
|
13
|
-
# Sort from longest to shortest
|
14
|
-
sample_arrays = sample_arrays.sort {|x, y| y.length <=> x.length}
|
15
|
-
|
16
|
-
composited_output = sample_arrays.slice!(0)
|
17
|
-
sample_arrays.each do |sample_array|
|
18
|
-
unless sample_array == []
|
19
|
-
if num_channels == 1
|
20
|
-
sample_array.length.times {|i| composited_output[i] += sample_array[i] }
|
21
|
-
elsif num_channels == 2
|
22
|
-
sample_array.length.times do |i|
|
23
|
-
composited_output[i] = [composited_output[i][0] + sample_array[i][0],
|
24
|
-
composited_output[i][1] + sample_array[i][1]]
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
return composited_output
|
31
|
-
end
|
32
|
-
|
33
|
-
|
34
|
-
# Scales the amplitude of the incoming sample array by *scale* amount. Can be used in conjunction
|
35
|
-
# with composite() to make sure composited sample arrays don't have an amplitude greater than 1.0.
|
36
|
-
def self.scale(sample_array, num_channels, scale)
|
37
|
-
if sample_array == []
|
38
|
-
return sample_array
|
39
|
-
end
|
40
|
-
|
41
|
-
if scale > 1
|
42
|
-
if num_channels == 1
|
43
|
-
sample_array = sample_array.map {|sample| sample / scale }
|
44
|
-
elsif num_channels == 2
|
45
|
-
sample_array = sample_array.map {|sample| [sample[0] / scale, sample[1] / scale]}
|
46
|
-
else
|
47
|
-
raise StandardError, "Invalid sample data array in AudioUtils.normalize()"
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
return sample_array
|
52
|
-
end
|
53
|
-
|
54
|
-
|
55
|
-
# Returns the number of samples that each step (i.e. a 'X' or a '.') lasts at a given sample
|
56
|
-
# rate and tempo. The sample length can be a non-integer value. Although there's no such
|
57
|
-
# thing as a partial sample, this is required to prevent small timing errors from creeping in.
|
58
|
-
# If they accumulate, they can cause rhythms to drift out of time.
|
59
|
-
def self.step_sample_length(samples_per_second, tempo)
|
60
|
-
samples_per_minute = samples_per_second * 60.0
|
61
|
-
samples_per_quarter_note = samples_per_minute / tempo
|
62
|
-
|
63
|
-
# Each step is equivalent to a 16th note
|
64
|
-
return samples_per_quarter_note / 4.0
|
65
|
-
end
|
66
|
-
|
67
|
-
|
68
|
-
# Returns the sample index that a given step (offset from 0) starts on.
|
69
|
-
def self.step_start_sample(step_index, step_sample_length)
|
70
|
-
return (step_index * step_sample_length).floor
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
data/lib/kit.rb
DELETED
@@ -1,185 +0,0 @@
|
|
1
|
-
# Raised when trying to load a sound file which can't be found at the path specified
|
2
|
-
class SoundFileNotFoundError < RuntimeError; end
|
3
|
-
|
4
|
-
# Raised when trying to load a sound file which either isn't actually a sound file, or
|
5
|
-
# is in an unsupported format.
|
6
|
-
class InvalidSoundFormatError < RuntimeError; end
|
7
|
-
|
8
|
-
|
9
|
-
# This class provides a repository for the sounds used in a song. Most usefully, it
|
10
|
-
# also handles converting the sounds to a common format. For example, if a song requires
|
11
|
-
# a sound that is mono/8-bit, another that is stereo/8-bit, and another that is
|
12
|
-
# stereo/16-bit, they have to be converted to a common format before they can be used
|
13
|
-
# together. Kit handles this conversion; all sounds retrieved using
|
14
|
-
# get_sample_data() will be in a common format.
|
15
|
-
#
|
16
|
-
# Sounds can only be added at initialization. During initialization, the sample data
|
17
|
-
# for each sound is loaded into memory, and converted to the common format if necessary.
|
18
|
-
# This format is:
|
19
|
-
#
|
20
|
-
# Bits per sample: 16
|
21
|
-
# Sample rate: 44100
|
22
|
-
# Channels: Stereo, unless all of the kit sounds are mono.
|
23
|
-
#
|
24
|
-
# For example if the kit has these sounds:
|
25
|
-
#
|
26
|
-
# my_sound_1.wav: mono, 16-bit
|
27
|
-
# my_sound_2.wav: stereo, 8-bit
|
28
|
-
# my_sound_3.wav: mono, 8-bit
|
29
|
-
#
|
30
|
-
# they will all be converted to stereo/16-bit during initialization.
|
31
|
-
class Kit
|
32
|
-
def initialize(base_path, kit_items)
|
33
|
-
@base_path = base_path
|
34
|
-
@label_mappings = {}
|
35
|
-
@sound_bank = {}
|
36
|
-
@num_channels = 1
|
37
|
-
@bits_per_sample = 16 # Only use 16-bit files as output. Supporting 8-bit output
|
38
|
-
# means extra complication for no real gain (I'm skeptical
|
39
|
-
# anyone would explicitly want 8-bit output instead of 16-bit).
|
40
|
-
|
41
|
-
load_sounds(base_path, kit_items)
|
42
|
-
end
|
43
|
-
|
44
|
-
# Returns the sample data for a sound contained in the Kit. If the all sounds in the
|
45
|
-
# kit are mono, then this will be a flat Array of Fixnums between -32768 and 32767.
|
46
|
-
# Otherwise, this will be an Array of Fixnums pairs between -32768 and 32767.
|
47
|
-
#
|
48
|
-
# label - The name of the sound to get sample data for. If the sound was defined in
|
49
|
-
# the Kit section of a song file, this will generally be a descriptive label
|
50
|
-
# such as "bass" or "snare". If defined in a track but not the kit, it will
|
51
|
-
# generally be a file name such as "my_sounds/hihat/hi_hat.wav".
|
52
|
-
#
|
53
|
-
# Examples
|
54
|
-
#
|
55
|
-
# # If @num_channels is 1, a flat Array of Fixnums:
|
56
|
-
# get_sample_data("bass")
|
57
|
-
# # => [154, 7023, 8132, 2622, -132, 34, ..., -6702]
|
58
|
-
#
|
59
|
-
# # If @num_channels is 2, a Array of Fixnums pairs:
|
60
|
-
# get_sample_data("snare")
|
61
|
-
# # => [[57, 1265], [-452, 10543], [-2531, 12643], [-6372, 11653], ..., [5482, 25673]]
|
62
|
-
#
|
63
|
-
# Returns the sample data Array for the sound bound to label.
|
64
|
-
def get_sample_data(label)
|
65
|
-
if label == "placeholder"
|
66
|
-
return []
|
67
|
-
end
|
68
|
-
|
69
|
-
sample_data = @sound_bank[label]
|
70
|
-
|
71
|
-
if sample_data == nil
|
72
|
-
# TODO: Should we really throw an exception here rather than just returning nil?
|
73
|
-
raise StandardError, "Kit doesn't contain sound '#{label}'."
|
74
|
-
else
|
75
|
-
return sample_data
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
def scale!(scale_factor)
|
80
|
-
@sound_bank.each do |label, sample_array|
|
81
|
-
@sound_bank[label] = AudioUtils.scale(sample_array, @num_channels, scale_factor)
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
# Returns a YAML representation of the Kit. Produces nicer looking output than the default version
|
86
|
-
# of to_yaml().
|
87
|
-
#
|
88
|
-
# indent_space_count - The number of spaces to indent each line in the output (default: 0).
|
89
|
-
#
|
90
|
-
# Returns a String representation of the Kit in YAML format.
|
91
|
-
def to_yaml(indent_space_count = 0)
|
92
|
-
yaml = ""
|
93
|
-
longest_label_mapping_length =
|
94
|
-
@label_mappings.keys.inject(0) do |max_length, name|
|
95
|
-
(name.to_s.length > max_length) ? name.to_s.length : max_length
|
96
|
-
end
|
97
|
-
|
98
|
-
if @label_mappings.length > 0
|
99
|
-
yaml += " " * indent_space_count + "Kit:\n"
|
100
|
-
ljust_amount = longest_label_mapping_length + 1 # The +1 is for the trailing ":"
|
101
|
-
@label_mappings.sort.each do |label, path|
|
102
|
-
yaml += " " * indent_space_count + " - #{(label + ":").ljust(ljust_amount)} #{path}\n"
|
103
|
-
end
|
104
|
-
end
|
105
|
-
|
106
|
-
return yaml
|
107
|
-
end
|
108
|
-
|
109
|
-
attr_reader :base_path, :label_mappings, :bits_per_sample, :num_channels
|
110
|
-
|
111
|
-
private
|
112
|
-
|
113
|
-
def load_sounds(base_path, kit_items)
|
114
|
-
# Set label mappings
|
115
|
-
kit_items.each do |label, sound_file_names|
|
116
|
-
if sound_file_names.class == Array
|
117
|
-
raise StandardError, "Composite sounds aren't allowed (yet...)"
|
118
|
-
end
|
119
|
-
|
120
|
-
unless label == sound_file_names
|
121
|
-
@label_mappings[label] = sound_file_names
|
122
|
-
end
|
123
|
-
end
|
124
|
-
|
125
|
-
kit_items = make_file_names_absolute(kit_items)
|
126
|
-
sound_buffers = load_raw_sounds(kit_items)
|
127
|
-
|
128
|
-
canonical_format = WaveFile::Format.new(@num_channels, @bits_per_sample, 44100)
|
129
|
-
|
130
|
-
# Convert each sound to a common format
|
131
|
-
sound_buffers.each {|file_name, buffer| sound_buffers[file_name] = buffer.convert(canonical_format) }
|
132
|
-
|
133
|
-
# If necessary, mix component sounds into a composite
|
134
|
-
kit_items.each do |label, sound_file_names|
|
135
|
-
@sound_bank[label] = mixdown(sound_file_names, sound_buffers)
|
136
|
-
end
|
137
|
-
end
|
138
|
-
|
139
|
-
# Converts relative paths into absolute paths. Note that this will also handle
|
140
|
-
# expanding ~ on platforms that support that.
|
141
|
-
def make_file_names_absolute(kit_items)
|
142
|
-
kit_items.each do |label, sound_file_names|
|
143
|
-
unless sound_file_names.class == Array
|
144
|
-
sound_file_names = [sound_file_names]
|
145
|
-
end
|
146
|
-
|
147
|
-
sound_file_names.map! {|sound_file_name| File.expand_path(sound_file_name, base_path) }
|
148
|
-
kit_items[label] = sound_file_names
|
149
|
-
end
|
150
|
-
|
151
|
-
return kit_items
|
152
|
-
end
|
153
|
-
|
154
|
-
# Load all sound files, bailing if any are invalid
|
155
|
-
def load_raw_sounds(kit_items)
|
156
|
-
raw_sounds = {}
|
157
|
-
kit_items.values.flatten.each do |sound_file_name|
|
158
|
-
begin
|
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
|
164
|
-
rescue Errno::ENOENT
|
165
|
-
raise SoundFileNotFoundError, "Sound file #{sound_file_name} not found."
|
166
|
-
rescue StandardError
|
167
|
-
raise InvalidSoundFormatError, "Sound file #{sound_file_name} is either not a sound file, " +
|
168
|
-
"or is in an unsupported format. BEATS can handle 8, 16, or 32-bit PCM *.wav files."
|
169
|
-
end
|
170
|
-
end
|
171
|
-
|
172
|
-
return raw_sounds
|
173
|
-
end
|
174
|
-
|
175
|
-
def mixdown(sound_file_names, raw_sounds)
|
176
|
-
sample_arrays = []
|
177
|
-
sound_file_names.each do |sound_file_name|
|
178
|
-
sample_arrays << raw_sounds[sound_file_name].samples
|
179
|
-
end
|
180
|
-
|
181
|
-
composited_sample_data = AudioUtils.composite(sample_arrays, @num_channels)
|
182
|
-
|
183
|
-
return AudioUtils.scale(composited_sample_data, @num_channels, sound_file_names.length)
|
184
|
-
end
|
185
|
-
end
|
data/lib/pattern.rb
DELETED
@@ -1,86 +0,0 @@
|
|
1
|
-
# Domain object which models one or more Tracks playing a part of the song at the same time.
|
2
|
-
# For example, a bass drum, snare drum, and hi-hat track playing the song's chorus.
|
3
|
-
#
|
4
|
-
# This object is like sheet music; the AudioEngine is responsible creating actual
|
5
|
-
# audio data for a Pattern (with the help of a Kit).
|
6
|
-
class Pattern
|
7
|
-
FLOW_TRACK_NAME = "flow"
|
8
|
-
|
9
|
-
def initialize(name)
|
10
|
-
@name = name
|
11
|
-
@tracks = {}
|
12
|
-
end
|
13
|
-
|
14
|
-
# Adds a new track to the pattern.
|
15
|
-
def track(name, rhythm)
|
16
|
-
track_key = unique_track_name(name)
|
17
|
-
new_track = Track.new(name, rhythm)
|
18
|
-
@tracks[track_key] = new_track
|
19
|
-
|
20
|
-
# If the new track is longer than any of the previously added tracks,
|
21
|
-
# pad the other tracks with trailing . to make them all the same length.
|
22
|
-
# Necessary to prevent incorrect overflow calculations for tracks.
|
23
|
-
longest_track_length = step_count()
|
24
|
-
@tracks.values.each do |track|
|
25
|
-
if track.rhythm.length < longest_track_length
|
26
|
-
track.rhythm += "." * (longest_track_length - track.rhythm.length)
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
return new_track
|
31
|
-
end
|
32
|
-
|
33
|
-
def step_count
|
34
|
-
@tracks.values.collect {|track| track.rhythm.length }.max || 0
|
35
|
-
end
|
36
|
-
|
37
|
-
# Returns whether or not this pattern has the same number of tracks as other_pattern, and that
|
38
|
-
# each of the tracks has the same name and rhythm. Ordering of tracks does not matter; will
|
39
|
-
# return true if the two patterns have the same tracks but in a different ordering.
|
40
|
-
def same_tracks_as?(other_pattern)
|
41
|
-
@tracks.keys.each do |track_name|
|
42
|
-
other_pattern_track = other_pattern.tracks[track_name]
|
43
|
-
if other_pattern_track == nil || @tracks[track_name].rhythm != other_pattern_track.rhythm
|
44
|
-
return false
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
return @tracks.length == other_pattern.tracks.length
|
49
|
-
end
|
50
|
-
|
51
|
-
# Returns a YAML representation of the Pattern. Produces nicer looking output than the default
|
52
|
-
# version of to_yaml().
|
53
|
-
def to_yaml
|
54
|
-
longest_track_name_length =
|
55
|
-
@tracks.keys.inject(0) do |max_length, name|
|
56
|
-
(name.to_s.length > max_length) ? name.to_s.length : max_length
|
57
|
-
end
|
58
|
-
ljust_amount = longest_track_name_length + 7
|
59
|
-
|
60
|
-
yaml = "#{@name.to_s.capitalize}:\n"
|
61
|
-
@tracks.keys.sort.each do |track_name|
|
62
|
-
yaml += " - #{track_name}:".ljust(ljust_amount)
|
63
|
-
yaml += "#{@tracks[track_name].rhythm}\n"
|
64
|
-
end
|
65
|
-
|
66
|
-
return yaml
|
67
|
-
end
|
68
|
-
|
69
|
-
attr_accessor :tracks, :name
|
70
|
-
|
71
|
-
private
|
72
|
-
|
73
|
-
# Returns a unique track name that is not already in use by a track in
|
74
|
-
# this pattern. Used to help support having multiple tracks with the same
|
75
|
-
# sample in a track.
|
76
|
-
def unique_track_name(name)
|
77
|
-
i = 2
|
78
|
-
name_key = name
|
79
|
-
while @tracks.has_key? name_key
|
80
|
-
name_key = "#{name}#{i.to_s}"
|
81
|
-
i += 1
|
82
|
-
end
|
83
|
-
|
84
|
-
return name_key
|
85
|
-
end
|
86
|
-
end
|
data/lib/song.rb
DELETED
@@ -1,160 +0,0 @@
|
|
1
|
-
class InvalidTempoError < RuntimeError; end
|
2
|
-
|
3
|
-
|
4
|
-
# Domain object which models the 'sheet music' for a full song. Models the Patterns
|
5
|
-
# that should be played, in which order (i.e. the flow), and at which tempo.
|
6
|
-
#
|
7
|
-
# This is the top-level model object that is used by the AudioEngine to produce
|
8
|
-
# actual audio data. A Song tells the AudioEngine what sounds to trigger and when.
|
9
|
-
# A Kit provides the sample data for each of these sounds. With a Song and a Kit
|
10
|
-
# the AudioEngine can produce the audio data that is saved to disk.
|
11
|
-
class Song
|
12
|
-
DEFAULT_TEMPO = 120
|
13
|
-
|
14
|
-
def initialize
|
15
|
-
self.tempo = DEFAULT_TEMPO
|
16
|
-
@patterns = {}
|
17
|
-
@flow = []
|
18
|
-
end
|
19
|
-
|
20
|
-
# Adds a new pattern to the song, with the specified name.
|
21
|
-
def pattern(name)
|
22
|
-
@patterns[name] = Pattern.new(name)
|
23
|
-
return @patterns[name]
|
24
|
-
end
|
25
|
-
|
26
|
-
|
27
|
-
# The number of tracks that the pattern with the greatest number of tracks has.
|
28
|
-
# TODO: Is it a problem that an optimized song can have a different total_tracks() value than
|
29
|
-
# the original? Or is that actually a good thing?
|
30
|
-
# TODO: Investigate replacing this with a method max_sounds_playing_at_once() or something
|
31
|
-
# like that. Would look each pattern along with it's incoming overflow.
|
32
|
-
def total_tracks
|
33
|
-
@patterns.keys.collect {|pattern_name| @patterns[pattern_name].tracks.length }.max || 0
|
34
|
-
end
|
35
|
-
|
36
|
-
# The unique track names used in each of the song's patterns. Sorted in alphabetical order.
|
37
|
-
# For example calling this method for this song:
|
38
|
-
#
|
39
|
-
# Verse:
|
40
|
-
# - bass: X...
|
41
|
-
# - snare: ..X.
|
42
|
-
#
|
43
|
-
# Chorus:
|
44
|
-
# - bass: X.X.
|
45
|
-
# - snare: X.X.
|
46
|
-
# - hihat: XXXX
|
47
|
-
#
|
48
|
-
# Will return: ["bass", "hihat", "snare"]
|
49
|
-
def track_names
|
50
|
-
@patterns.values.inject([]) {|track_names, pattern| track_names | pattern.tracks.keys }.sort
|
51
|
-
end
|
52
|
-
|
53
|
-
def tempo=(new_tempo)
|
54
|
-
unless new_tempo.class == Fixnum && new_tempo > 0
|
55
|
-
raise InvalidTempoError, "Invalid tempo: '#{new_tempo}'. Tempo must be a number greater than 0."
|
56
|
-
end
|
57
|
-
|
58
|
-
@tempo = new_tempo
|
59
|
-
end
|
60
|
-
|
61
|
-
# Returns a new Song that is identical but with no patterns or flow.
|
62
|
-
def copy_ignoring_patterns_and_flow
|
63
|
-
copy = Song.new()
|
64
|
-
copy.tempo = @tempo
|
65
|
-
|
66
|
-
return copy
|
67
|
-
end
|
68
|
-
|
69
|
-
# Splits a Song object into multiple Song objects, where each new
|
70
|
-
# Song only has 1 track. For example, if a Song has 5 tracks, this will return
|
71
|
-
# a hash of 5 songs, each with one of the original Song's tracks.
|
72
|
-
def split
|
73
|
-
split_songs = {}
|
74
|
-
track_names = track_names()
|
75
|
-
|
76
|
-
track_names.each do |track_name|
|
77
|
-
new_song = copy_ignoring_patterns_and_flow()
|
78
|
-
|
79
|
-
@patterns.each do |name, original_pattern|
|
80
|
-
new_pattern = new_song.pattern(name)
|
81
|
-
|
82
|
-
if original_pattern.tracks.has_key?(track_name)
|
83
|
-
original_track = original_pattern.tracks[track_name]
|
84
|
-
new_pattern.track(original_track.name, original_track.rhythm)
|
85
|
-
else
|
86
|
-
new_pattern.track(track_name, "." * original_pattern.step_count)
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
90
|
-
new_song.flow = @flow
|
91
|
-
|
92
|
-
split_songs[track_name] = new_song
|
93
|
-
end
|
94
|
-
|
95
|
-
return split_songs
|
96
|
-
end
|
97
|
-
|
98
|
-
# Removes any patterns that aren't referenced in the flow.
|
99
|
-
def remove_unused_patterns
|
100
|
-
# Using reject() here because for some reason select() returns an Array not a Hash.
|
101
|
-
@patterns.reject! {|k, pattern| !@flow.member?(pattern.name) }
|
102
|
-
end
|
103
|
-
|
104
|
-
# Serializes the current Song to a YAML string. This string can then be used to construct a new Song
|
105
|
-
# using the SongParser class. This lets you save a Song to disk, to be re-loaded later. Produces nicer
|
106
|
-
# looking output than the default version of to_yaml().
|
107
|
-
def to_yaml(kit)
|
108
|
-
# This implementation intentionally builds up a YAML string manually instead of using YAML::dump().
|
109
|
-
# Ruby 1.8 makes it difficult to ensure a consistent ordering of hash keys, which makes the output ugly
|
110
|
-
# and also hard to test.
|
111
|
-
|
112
|
-
yaml_output = "Song:\n"
|
113
|
-
yaml_output += " Tempo: #{@tempo}\n"
|
114
|
-
yaml_output += flow_to_yaml()
|
115
|
-
yaml_output += kit.to_yaml(2)
|
116
|
-
yaml_output += patterns_to_yaml()
|
117
|
-
|
118
|
-
return yaml_output
|
119
|
-
end
|
120
|
-
|
121
|
-
attr_reader :patterns, :tempo
|
122
|
-
attr_accessor :flow
|
123
|
-
|
124
|
-
private
|
125
|
-
|
126
|
-
def longest_length_in_array(arr)
|
127
|
-
return arr.inject(0) {|max_length, name| [name.to_s.length, max_length].max }
|
128
|
-
end
|
129
|
-
|
130
|
-
def flow_to_yaml
|
131
|
-
yaml_output = " Flow:\n"
|
132
|
-
ljust_amount = longest_length_in_array(@flow) + 1 # The +1 is for the trailing ":"
|
133
|
-
previous = nil
|
134
|
-
count = 0
|
135
|
-
@flow.each do |pattern_name|
|
136
|
-
if pattern_name == previous || previous == nil
|
137
|
-
count += 1
|
138
|
-
else
|
139
|
-
yaml_output += " - #{(previous.to_s.capitalize + ':').ljust(ljust_amount)} x#{count}\n"
|
140
|
-
count = 1
|
141
|
-
end
|
142
|
-
previous = pattern_name
|
143
|
-
end
|
144
|
-
yaml_output += " - #{(previous.to_s.capitalize + ':').ljust(ljust_amount)} x#{count}\n"
|
145
|
-
|
146
|
-
return yaml_output
|
147
|
-
end
|
148
|
-
|
149
|
-
def patterns_to_yaml
|
150
|
-
yaml_output = ""
|
151
|
-
|
152
|
-
# Sort to ensure a consistent order, to make testing easier
|
153
|
-
pattern_names = @patterns.keys.map {|key| key.to_s} # Ruby 1.8 can't sort symbols...
|
154
|
-
pattern_names.sort.each do |pattern_name|
|
155
|
-
yaml_output += "\n" + @patterns[pattern_name.to_sym].to_yaml()
|
156
|
-
end
|
157
|
-
|
158
|
-
return yaml_output
|
159
|
-
end
|
160
|
-
end
|