Module: Cyclotone::Harmony

Defined in:
lib/cyclotone/harmony.rb

Constant Summary collapse

NOTE_OFFSETS =
{
  "c" => 0, "cs" => 1, "db" => 1, "d" => 2, "ds" => 3, "eb" => 3, "e" => 4,
  "f" => 5, "fs" => 6, "gb" => 6, "g" => 7, "gs" => 8, "ab" => 8, "a" => 9,
  "as" => 10, "bb" => 10, "b" => 11
}.freeze
SCALES =
{
  major: [0, 2, 4, 5, 7, 9, 11],
  minor: [0, 2, 3, 5, 7, 8, 10],
  dorian: [0, 2, 3, 5, 7, 9, 10],
  phrygian: [0, 1, 3, 5, 7, 8, 10],
  lydian: [0, 2, 4, 6, 7, 9, 11],
  mixolydian: [0, 2, 4, 5, 7, 9, 10],
  locrian: [0, 1, 3, 5, 6, 8, 10],
  harmonic_minor: [0, 2, 3, 5, 7, 8, 11],
  melodic_minor: [0, 2, 3, 5, 7, 9, 11],
  whole_tone: [0, 2, 4, 6, 8, 10],
  chromatic: (0..11).to_a,
  pentatonic_major: [0, 2, 4, 7, 9],
  pentatonic_minor: [0, 3, 5, 7, 10],
  blues: [0, 3, 5, 6, 7, 10],
  egyptian: [0, 2, 5, 7, 10],
  hirajoshi: [0, 2, 3, 7, 8],
  iwato: [0, 1, 5, 6, 10],
  enigmatic: [0, 1, 4, 6, 8, 10, 11],
  neapolitan_major: [0, 1, 3, 5, 7, 9, 11],
  neapolitan_minor: [0, 1, 3, 5, 7, 8, 11]
}.freeze
CHORDS =
{
  major: [0, 4, 7],
  minor: [0, 3, 7],
  diminished: [0, 3, 6],
  augmented: [0, 4, 8],
  sus2: [0, 2, 7],
  sus4: [0, 5, 7],
  major7: [0, 4, 7, 11],
  minor7: [0, 3, 7, 10],
  dominant7: [0, 4, 7, 10]
}.freeze

Class Method Summary collapse

Class Method Details

.arpeggiate(pattern, mode: :up) ⇒ Object



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/cyclotone/harmony.rb', line 70

def arpeggiate(pattern, mode: :up)
  Pattern.ensure_pattern(pattern).flat_map_events do |event|
    notes = extract_notes(event.value)
    next [event] if notes.empty?
    next [] if event.part.duration.zero?

    ordered = order_notes(notes, mode)
    segment_length = event.part.duration / ordered.length

    ordered.each_with_index.map do |note, index|
      part = TimeSpan.new(
        event.part.start + (segment_length * index),
        event.part.start + (segment_length * (index + 1))
      )

      Event.new(whole: part, part: part, value: arpeggiated_value(event.value, note))
    end
  end
end

.chord(name, root: 0, inversion: 0, voicing: nil, drop2: false, octave_spread: 0) ⇒ Object



57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/cyclotone/harmony.rb', line 57

def chord(name, root: 0, inversion: 0, voicing: nil, drop2: false, octave_spread: 0)
  root_note = note_number(root)
  intervals = voicing || CHORDS.fetch(name.to_sym) { raise ArgumentError, "unknown chord #{name}" }
  notes = Array(intervals).map do |interval|
    root_note + interval
  end
  notes = invert_notes(notes, inversion.to_i)
  notes = spread_octaves(notes, octave_spread.to_i)
  notes = drop_second_voice(notes) if drop2

  Pattern.pure(notes)
end

.note_number(value) ⇒ Object

Raises:

  • (ArgumentError)


90
91
92
93
94
95
96
97
98
99
# File 'lib/cyclotone/harmony.rb', line 90

def note_number(value)
  return value.to_i if value.is_a?(Numeric)

  normalized = value.to_s.strip.downcase
  match = normalized.match(/\A([a-g](?:s|b)?)(-?\d+)\z/)
  return normalized.to_i if match.nil? && normalized.match?(/\A-?\d+\z/)
  raise ArgumentError, "invalid note #{value.inspect}" if match.nil?

  NOTE_OFFSETS.fetch(match[1]) + ((match[2].to_i + 1) * 12)
end

.scale(name, pattern, root: 0) ⇒ Object



48
49
50
51
52
53
54
55
# File 'lib/cyclotone/harmony.rb', line 48

def scale(name, pattern, root: 0)
  intervals = SCALES.fetch(name.to_sym) { raise ArgumentError, "unknown scale #{name}" }
  root_note = note_number(root)

  Pattern.ensure_pattern(pattern).fmap do |value|
    apply_scale(intervals, root_note, value)
  end
end