class Musicality::CounterpointGenerator

Attributes

palette[R]
rhythm[R]
solution_classes[R]
solutions[R]
total_dur[R]

Public Class Methods

new(rhythm, palette, max_fact = 5) click to toggle source
# File lib/musicality/composition/generation/counterpoint_generator.rb, line 5
def initialize rhythm, palette, max_fact = 5
  raise ArgumentError, "max_fact must be >= palette size" if max_fact < palette.size
  
  @rhythm = rhythm
  @palette = palette
  @total_dur = rhythm.map {|dur| dur.abs }.inject(0,:+)
  @solution_classes = self.class.solution_classes(@total_dur, @palette)
  @solution_classes.keep_if do |sc|
    sc.map {|k,v| k*v}.inject(0,:+) == @total_dur
  end
  @solutions = figure_solutions(max_fact)
end
rhythm_to_computer(rhythm) click to toggle source
# File lib/musicality/composition/generation/counterpoint_generator.rb, line 18
def self.rhythm_to_computer rhythm
  rhythm_accum = AddingSequence.new(rhythm).take(rhythm.size+1).to_a
  x = rhythm_accum[0,rhythm.size]
  y = rhythm_accum[1,rhythm.size].map {|y_| Change::Immediate.new(y_) }
  value_changes = Hash[ [x,y].transpose ]
  ValueComputer.new(0, value_changes)
end

Private Class Methods

limited_solution_classes(durs, counts, max_fact) click to toggle source
# File lib/musicality/composition/generation/counterpoint_generator.rb, line 75
def self.limited_solution_classes durs, counts, max_fact
  n_counts = counts.size
  if n_counts > max_fact
    raise ArgumentError, "counts.size (#{n_counts}) is > max_fact (#{max_fact})"
  end
  
  total_count_rem = counts.inject(0,:+)
  if total_count_rem < max_fact
    raise ArgumentError, "total_count_rem (#{total_count_rem}) <= max_fact (#{max_fact})"
  end
  
  ltd_solns = []
  counts.size.times do |i|
    count, dur = counts.first, durs.first
    counts_rem = counts.drop(1)
    
    adj_count = [ count, max_fact - counts_rem.size ].min
    
    positive_integer_combinations_with_sum(count,adj_count).each do |comb|
      if counts_rem.empty?
        ltd_solns.push(dur => comb)
      else
        max_fact_rem = max_fact - adj_count
        limited_solution_classes(durs.drop(1), counts_rem, max_fact_rem).each do |ltd_soln2|
          ltd_solns.push(ltd_soln2.merge(dur => comb))
        end
      end
    end
    
    counts.rotate!
    durs.rotate!
  end
  return ltd_solns.uniq  
end
positive_integer_combinations_with_sum(sum, n_pos_ints) click to toggle source
# File lib/musicality/composition/generation/counterpoint_generator.rb, line 110
def self.positive_integer_combinations_with_sum sum, n_pos_ints
  raise ArgumentError, "sum must be <= number of positive integers" if n_pos_ints > sum
  
  if n_pos_ints == 1
    return [[sum]]
  elsif sum == n_pos_ints
    return [[1]*n_pos_ints]
  else
    return (1...sum).to_a.combination(n_pos_ints-1).map do |comb|
      prev_post = 0
      diffs = comb.map do |post|
        diff = post - prev_post
        prev_post = post
        diff
      end
      (diffs + [sum-prev_post]).sort
    end.uniq
  end
end
solution_classes(total_dur, palette) click to toggle source
# File lib/musicality/composition/generation/counterpoint_generator.rb, line 130
def self.solution_classes total_dur, palette
  dur = palette.first
  n = (total_dur / dur).to_i
  
  if palette.size == 1
    return [ n > 0 ? {dur => n} : {} ]
  else
    new_palette = palette.drop(1)
    return Array.new(n+1) do |i|
      subs = solution_classes(total_dur - i*dur, new_palette)
      if i > 0
        subs.each{|soln_class| soln_class[dur] = i }
      end
      subs
    end.flatten
  end
end

Public Instance Methods

best_solution(ideal_overlap, sample_rate) click to toggle source
# File lib/musicality/composition/generation/counterpoint_generator.rb, line 42
def best_solution ideal_overlap, sample_rate
  @solutions.min_by {|sol| evaluate(sol,ideal_overlap,sample_rate) }
end
best_solutions(n, ideal_overlap, sample_rate) click to toggle source
# File lib/musicality/composition/generation/counterpoint_generator.rb, line 46
def best_solutions n, ideal_overlap, sample_rate
  @solutions.sort_by {|sol| evaluate(sol,ideal_overlap,sample_rate) }.take(n)
end
evaluate(solution, ideal_overlap, sample_rate) click to toggle source
# File lib/musicality/composition/generation/counterpoint_generator.rb, line 26
def evaluate solution, ideal_overlap, sample_rate
  if solution.inject(0,:+) != @total_dur
    raise ArgumentError, "Given solution #{solution} does not have same duration as rhythm"
  end
  
  rhythm_comp = self.class.rhythm_to_computer(@rhythm)
  solution_comp = self.class.rhythm_to_computer(solution)
  
  r = rhythm_comp.sample(0...@total_dur, sample_rate)
  s = solution_comp.sample(0...@total_dur, sample_rate)
  n_same = [r,s].transpose.count {|pair| pair[0] == pair[1] }
  n_samples = (@total_dur*sample_rate).to_i
  percent_overlap = (n_same/n_samples).to_f
  return (ideal_overlap - percent_overlap).abs
end

Private Instance Methods

figure_solutions(max_factorial) click to toggle source
# File lib/musicality/composition/generation/counterpoint_generator.rb, line 52
def figure_solutions max_factorial
  solutions = []
  @solution_classes.each do |sc|
    tot = sc.values.inject(0,:+)
    if tot <= max_factorial
      proto = sc.map {|dur,n| [dur]*n }.flatten
      solutions += proto.permutation(proto.size).to_a.uniq
    else
      durs, counts = sc.sort_by {|k,v| v }.transpose
      self.class.limited_solution_classes(durs,counts,max_factorial).each do |ltd_sc|
        ltd_proto = ltd_sc.map do |dur,ns|
          ns.map {|n| [dur]*n }
        end.flatten(1)
        solutions += ltd_proto.permutation(max_factorial).map do |perm|
          perm.flatten
        end.to_a.uniq
      end
    end
  end
  
  return solutions.uniq
end