beats 1.2.4 → 1.2.5
Sign up to get free protection for your applications and to get access to all the features.
- 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
|