class Beats::Song

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.

Constants

DEFAULT_TEMPO

Attributes

flow[RW]
patterns[R]
tempo[R]

Public Class Methods

new() click to toggle source
# File lib/beats/song.rb, line 14
def initialize
  self.tempo = DEFAULT_TEMPO
  @patterns = {}
  @flow = []
end

Public Instance Methods

copy_ignoring_patterns_and_flow() click to toggle source

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

# File lib/beats/song.rb, line 61
def copy_ignoring_patterns_and_flow
  copy = Song.new
  copy.tempo = @tempo

  copy
end
pattern(name, tracks=[]) click to toggle source

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

# File lib/beats/song.rb, line 21
def pattern(name, tracks=[])
  @patterns[name] = Pattern.new(name, tracks)
end
remove_unused_patterns() click to toggle source

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

# File lib/beats/song.rb, line 97
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
split() click to toggle source

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.

# File lib/beats/song.rb, line 71
def split
  split_songs = {}
  track_names = track_names()

  track_names.each do |track_name|
    new_song = copy_ignoring_patterns_and_flow

    @patterns.each do |pattern_name, original_pattern|
      if original_pattern.tracks.has_key?(track_name)
        new_track = original_pattern.tracks[track_name]
      else
        new_track = Track.new(track_name, Track::REST * original_pattern.step_count)
      end

      new_song.pattern(pattern_name, [new_track])
    end

    new_song.flow = @flow

    split_songs[track_name] = new_song
  end

  split_songs
end
tempo=(new_tempo) click to toggle source
# File lib/beats/song.rb, line 52
def tempo=(new_tempo)
  unless (new_tempo.is_a?(Integer) || new_tempo.is_a?(Float)) && new_tempo > 0
    raise InvalidTempoError, "Invalid tempo: '#{new_tempo}'. Tempo must be a number greater than 0."
  end

  @tempo = new_tempo
end
total_tracks() click to toggle source

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.

# File lib/beats/song.rb, line 31
def total_tracks
  @patterns.keys.collect {|pattern_name| @patterns[pattern_name].tracks.length }.max || 0
end
track_names() click to toggle source

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

# File lib/beats/song.rb, line 48
def track_names
  @patterns.values.inject([]) {|track_names, pattern| track_names | pattern.tracks.keys }.sort
end

Private Instance Methods

longest_length_in_array(arr) click to toggle source
# File lib/beats/song.rb, line 107
def longest_length_in_array(arr)
  arr.inject(0) {|max_length, name| [name.to_s.length, max_length].max }
end