/
song.rb
executable file
·160 lines (130 loc) · 5.11 KB
/
song.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
class InvalidTempoError < RuntimeError; end
# Domain object which models the 'sheet music' for a full song. Models the Patterns
# that should be played, in which order (i.e. the flow), and at which tempo.
#
# This is the top-level model object that is used by the AudioEngine to produce
# actual audio data. A Song tells the AudioEngine what sounds to trigger and when.
# A Kit provides the sample data for each of these sounds. With a Song and a Kit
# the AudioEngine can produce the audio data that is saved to disk.
class Song
DEFAULT_TEMPO = 120
def initialize
self.tempo = DEFAULT_TEMPO
@patterns = {}
@flow = []
end
# Adds a new pattern to the song, with the specified name.
def pattern(name)
@patterns[name] = Pattern.new(name)
return @patterns[name]
end
# The number of tracks that the pattern with the greatest number of tracks has.
# TODO: Is it a problem that an optimized song can have a different total_tracks() value than
# the original? Or is that actually a good thing?
# TODO: Investigate replacing this with a method max_sounds_playing_at_once() or something
# like that. Would look each pattern along with it's incoming overflow.
def total_tracks
@patterns.keys.collect {|pattern_name| @patterns[pattern_name].tracks.length }.max || 0
end
# The unique track names used in each of the song's patterns. Sorted in alphabetical order.
# For example calling this method for this song:
#
# Verse:
# - bass: X...
# - snare: ..X.
#
# Chorus:
# - bass: X.X.
# - snare: X.X.
# - hihat: XXXX
#
# Will return: ["bass", "hihat", "snare"]
def track_names
@patterns.values.inject([]) {|track_names, pattern| track_names | pattern.tracks.keys }.sort
end
def tempo=(new_tempo)
unless new_tempo.class == Fixnum && new_tempo > 0
raise InvalidTempoError, "Invalid tempo: '#{new_tempo}'. Tempo must be a number greater than 0."
end
@tempo = new_tempo
end
# Returns a new Song that is identical but with no patterns or flow.
def copy_ignoring_patterns_and_flow
copy = Song.new()
copy.tempo = @tempo
return copy
end
# Splits a Song object into multiple Song objects, where each new
# Song only has 1 track. For example, if a Song has 5 tracks, this will return
# a hash of 5 songs, each with one of the original Song's tracks.
def split
split_songs = {}
track_names = track_names()
track_names.each do |track_name|
new_song = copy_ignoring_patterns_and_flow()
@patterns.each do |name, original_pattern|
new_pattern = new_song.pattern(name)
if original_pattern.tracks.has_key?(track_name)
original_track = original_pattern.tracks[track_name]
new_pattern.track(original_track.name, original_track.rhythm)
else
new_pattern.track(track_name, "." * original_pattern.step_count)
end
end
new_song.flow = @flow
split_songs[track_name] = new_song
end
return split_songs
end
# Removes any patterns that aren't referenced in the flow.
def remove_unused_patterns
# Using reject() here because for some reason select() returns an Array not a Hash.
@patterns.reject! {|k, pattern| !@flow.member?(pattern.name) }
end
# Serializes the current Song to a YAML string. This string can then be used to construct a new Song
# using the SongParser class. This lets you save a Song to disk, to be re-loaded later. Produces nicer
# looking output than the default version of to_yaml().
def to_yaml(kit)
# This implementation intentionally builds up a YAML string manually instead of using YAML::dump().
# Ruby 1.8 makes it difficult to ensure a consistent ordering of hash keys, which makes the output ugly
# and also hard to test.
yaml_output = "Song:\n"
yaml_output += " Tempo: #{@tempo}\n"
yaml_output += flow_to_yaml()
yaml_output += kit.to_yaml(2)
yaml_output += patterns_to_yaml()
return yaml_output
end
attr_reader :patterns, :tempo
attr_accessor :flow
private
def longest_length_in_array(arr)
return arr.inject(0) {|max_length, name| [name.to_s.length, max_length].max }
end
def flow_to_yaml
yaml_output = " Flow:\n"
ljust_amount = longest_length_in_array(@flow) + 1 # The +1 is for the trailing ":"
previous = nil
count = 0
@flow.each do |pattern_name|
if pattern_name == previous || previous == nil
count += 1
else
yaml_output += " - #{(previous.to_s.capitalize + ':').ljust(ljust_amount)} x#{count}\n"
count = 1
end
previous = pattern_name
end
yaml_output += " - #{(previous.to_s.capitalize + ':').ljust(ljust_amount)} x#{count}\n"
return yaml_output
end
def patterns_to_yaml
yaml_output = ""
# Sort to ensure a consistent order, to make testing easier
pattern_names = @patterns.keys.map {|key| key.to_s} # Ruby 1.8 can't sort symbols...
pattern_names.sort.each do |pattern_name|
yaml_output += "\n" + @patterns[pattern_name.to_sym].to_yaml()
end
return yaml_output
end
end