Class: Song

Inherits:
Object
  • Object
show all
Defined in:
lib/song.rb

Overview

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.

Constant Summary collapse

DEFAULT_TEMPO =
120

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeSong

Returns a new instance of Song.



14
15
16
17
18
# File 'lib/song.rb', line 14

def initialize()
  self.tempo = DEFAULT_TEMPO
  @patterns = {}
  @flow = []
end

Instance Attribute Details

#flowObject

Returns the value of attribute flow.



126
127
128
# File 'lib/song.rb', line 126

def flow
  @flow
end

#patternsObject (readonly)

Returns the value of attribute patterns.



125
126
127
# File 'lib/song.rb', line 125

def patterns
  @patterns
end

Instance Method Details

#copy_ignoring_patterns_and_flowObject

Returns a new Song that is identical but with no patterns or flow.



66
67
68
69
70
71
# File 'lib/song.rb', line 66

def copy_ignoring_patterns_and_flow
  copy = Song.new()
  copy.tempo = @tempo
  
  return copy
end

#pattern(name) ⇒ Object

Adds a new pattern to the song, with the specified name.



21
22
23
24
# File 'lib/song.rb', line 21

def pattern(name)
  @patterns[name] = Pattern.new(name)
  return @patterns[name]
end

#remove_unused_patternsObject

Removes any patterns that aren’t referenced in the flow.



103
104
105
106
# File 'lib/song.rb', line 103

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

#splitObject

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.



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
# File 'lib/song.rb', line 76

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

#tempoObject



53
54
55
# File 'lib/song.rb', line 53

def tempo
  return @tempo
end

#tempo=(new_tempo) ⇒ Object



57
58
59
60
61
62
63
# File 'lib/song.rb', line 57

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

#to_yaml(kit) ⇒ Object

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().



111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/song.rb', line 111

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

#total_tracksObject

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.



32
33
34
# File 'lib/song.rb', line 32

def total_tracks
  @patterns.keys.collect {|pattern_name| @patterns[pattern_name].tracks.length }.max || 0
end

#track_namesObject

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”]



49
50
51
# File 'lib/song.rb', line 49

def track_names
  @patterns.values.inject([]) {|track_names, pattern| track_names | pattern.tracks.keys }.sort
end