class InevitableCacophony::OctaveStructure

Constants

OCTAVE_RATIO

Frequency scaling for a difference of one whole octave

OCTAVE_STRUCTURE_SENTENCE

Regular expressions used in parsing

Attributes

chords[R]
scales[R]

Public Class Methods

new(scale_text) click to toggle source

@param scale_text [String] Dwarf Fortress musical form description including scale information. TODO: Allow contructing these without parsing text

# File lib/inevitable_cacophony/octave_structure.rb, line 65
def initialize(scale_text)
        description = Parser::SectionedText.new(scale_text)
        octave_description = description.find_paragraph(OCTAVE_STRUCTURE_SENTENCE)
        @octave_divisions = parse_octave_structure(octave_description)
        
        @chords = parse_chords(description)
        @scales = parse_scales(description, chords)
end

Public Instance Methods

chromatic_scale() click to toggle source

@return [Scale] A scale including all available notes in the octave.

(As the chromatic scale does for well-tempered Western instruments)
# File lib/inevitable_cacophony/octave_structure.rb, line 78
def chromatic_scale
        Scale.new([], @octave_divisions + [2])
end

Private Instance Methods

parse_chord(degrees) click to toggle source

@param degrees The list of degrees used by this particular scale

# File lib/inevitable_cacophony/octave_structure.rb, line 148
def parse_chord(degrees)
        ordinals = degrees.split(/(?:,| and) the/)
        
        chord_notes = ordinals.map do |degree_ordinal|
                # degree_ordinal is like "4th",
                # or may be like "13th (completing the octave)"
                # in which case it's not in our list of notes, but always has a factor of 2
                # (the tonic, an octave higher)
        
                if degree_ordinal.include?('(completing the octave)')
                        2
                else
                        index = degree_ordinal.strip.to_i
                        @octave_divisions[index - 1]
                end
        end
        
        Chord.new(chord_notes)
end
parse_chords(description) click to toggle source

@param description [Parser::SectionedText] The description text from which to extract chord data.

# File lib/inevitable_cacophony/octave_structure.rb, line 130
def parse_chords(description)
        
        # TODO: extract to constant
        chord_paragraph_regex = /The ([^ ]+) [a-z]*chord is/
        
        {}.tap do |chords|
                chord_paragraphs = description.find_all_paragraphs(chord_paragraph_regex)
        
                chord_paragraphs.each do |paragraph|
                        degrees_sentence = paragraph.find(chord_paragraph_regex)
        
                        name, degrees = degrees_sentence.match(/The ([^ ]+) [a-z]*chord is the (.*) degrees of the .* scale/).captures
                        chords[name.to_sym] = parse_chord(degrees)
                end
        end
end
parse_disjoint_chords_scale(scale_paragraph, chords) click to toggle source
# File lib/inevitable_cacophony/octave_structure.rb, line 188
def parse_disjoint_chords_scale(scale_paragraph, chords)
        chords_sentence = scale_paragraph.find(/These chords are/)
        chord_list = chords_sentence.match(/These chords are named ([^.]+)\.?/).captures.first
        chord_names = chord_list.split(/,|and/).map(&:strip).map(&:to_sym)
        
        Scale.new(chords.values_at(*chord_names))
end
parse_exact_notes(octave_paragraph) click to toggle source
# File lib/inevitable_cacophony/octave_structure.rb, line 99
def parse_exact_notes(octave_paragraph)
        exact_spacing_sentence = octave_paragraph.find(/their spacing is roughly/)
        spacing_match = exact_spacing_sentence.match(/In quartertones, their spacing is roughly 1((-|x){23})0/)
        
        if spacing_match
                # Always include the tonic
                note_scalings = [1]
        
                note_positions = spacing_match.captures.first
                step_size = 2**(1.0 / note_positions.length.succ)
                ratio = 1
                note_positions.each_char do |pos|
                        ratio *= step_size
                        
                        case pos
                        when 'x'
                                note_scalings << ratio
                        when '-'
                                # Do nothing; no note here
                        else
                                raise "Unexpected note position symbol #{pos.inspect}"
                        end
                end
        
                note_scalings
        else
                raise "Cannot parse octave text"
        end
end
parse_number_word(word) click to toggle source

Convert a number word to text – rough approximation for now. TODO: Rails or something may do this.

@param word [String] @return [Fixnum]

# File lib/inevitable_cacophony/octave_structure.rb, line 201
def parse_number_word(word)
        words_to_numbers = {
                'one' => 1,
                'two' => 2,
                'three' => 3,
                'four' => 4,
                'five' => 5,
                'six' => 6,
                'seven' => 7,
                'eight' => 8,
                'nine' => 9,
                'ten' => 10,
                'eleven' => 11,
                'twelve' => 12,
                'thirteen' => 13,
                'fourteen' => 14,
                'fifteen' => 15,
                'sixteen' => 16,
                'seventeen' => 17,
                'eighteen' => 18,
                'nineteen' => 19,
        }
        
        if words_to_numbers[word]
                words_to_numbers[word]
        elsif word.start_with?('twenty-')
                words_to_numbers[word.delete_prefix('twenty-')] + 20
        else
                "Unsupported number name #{word}"
        end
end
parse_octave_structure(octave_paragraph) click to toggle source
# File lib/inevitable_cacophony/octave_structure.rb, line 84
def parse_octave_structure(octave_paragraph)
        octave_sentence = octave_paragraph.find(OCTAVE_STRUCTURE_SENTENCE)
        note_count_match = octave_sentence.match(/Scales are constructed from ([-a-z ]+) notes spaced evenly throughout the octave/)
        
        if note_count_match
                note_count_word = note_count_match.captures.first
                divisions = parse_number_word(note_count_word)
                numerator = divisions.to_f
        
                (0...divisions).map { |index| 2 ** (index/numerator) }
        else
                parse_exact_notes(octave_paragraph)
        end
end
parse_scales(description, chords) click to toggle source

@param description [Parser::SectionedText] @param chords [Hash{Symbol,Chord}]

# File lib/inevitable_cacophony/octave_structure.rb, line 170
def parse_scales(description, chords)
        scale_topic_regex = /The [^ ]+ [^ ]+ scale is/
        
        {}.tap do |scales|
                description.find_all_paragraphs(scale_topic_regex).each do |scale_paragraph|
                        scale_sentence = scale_paragraph.find(scale_topic_regex)
                        name, scale_type = scale_sentence.match(/The ([^ ]+) [a-z]+tonic scale is (thought of as .*|constructed by)/).captures
        
                        case scale_type
                        when /thought of as ([a-z]+ )?(disjoint|joined) chords/
                                scales[name.to_sym] = parse_disjoint_chords_scale(scale_paragraph, chords)
                        else
                                raise "Unknown scale type #{scale_type} in #{scale_sentence}"
                        end
                end
        end
end