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