Class: HeadMusic::Notation::ABC::DurationResolver

Inherits:
Object
  • Object
show all
Defined in:
lib/head_music/notation/abc/duration_resolver.rb

Overview

Converts the tune’s unit note length and a per-note multiplier string (e.g. “2”, “3/2”, “/”, “//”) into a HeadMusic::Rudiment::RhythmicValue.

Constant Summary collapse

MAX_FRACTION =

Longest supported duration: a maxima (8 whole notes).

Rational(8)
DOTS_BY_ODD_FACTOR =

A reduced binary fraction’s odd factor determines the dot count: 1 -> plain, 3 -> dotted, 7 -> double-dotted, 15 -> triple-dotted.

{1 => 0, 3 => 1, 7 => 2, 15 => 3}.freeze
UNIT_NAMES_BY_MULTIPLE =
{1 => "whole", 2 => "double whole", 4 => "longa", 8 => "maxima"}.freeze
MULTIPLIER_PATTERN =
%r{\A(\d+)?(?:(/+)(\d+)?)?\z}

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(unit_note_length) ⇒ DurationResolver

Returns a new instance of DurationResolver.



19
20
21
# File 'lib/head_music/notation/abc/duration_resolver.rb', line 19

def initialize(unit_note_length)
  @unit_note_length = Rational(unit_note_length)
end

Instance Attribute Details

#unit_note_lengthObject (readonly)

Returns the value of attribute unit_note_length.



17
18
19
# File 'lib/head_music/notation/abc/duration_resolver.rb', line 17

def unit_note_length
  @unit_note_length
end

Instance Method Details

#build_rhythmic_value(fraction, source) ⇒ Object (private)

Fractions whose odd factor is one less than a power of two map onto a single (possibly dotted) note; anything else becomes a chain of tied notes, peeling off the largest dotted-expressible head each pass.



61
62
63
64
65
66
67
68
# File 'lib/head_music/notation/abc/duration_resolver.rb', line 61

def build_rhythmic_value(fraction, source)
  dots = DOTS_BY_ODD_FACTOR[odd_factor(fraction.numerator)]
  return single_value(fraction, dots, source) if dots

  head = greedy_head(fraction)
  tail = build_rhythmic_value(fraction - head, source)
  single_value(head, DOTS_BY_ODD_FACTOR.fetch(odd_factor(head.numerator)), source, tied_value: tail)
end

#greedy_head(fraction) ⇒ Object (private)

The largest leading run of set bits (capped at four, i.e. triple-dotted) forms a dotted-expressible head for the tied-value decomposition.



78
79
80
81
82
83
84
# File 'lib/head_music/notation/abc/duration_resolver.rb', line 78

def greedy_head(fraction)
  numerator = fraction.numerator
  bits = numerator.bit_length
  run = 0
  run += 1 while run < 4 && run < bits && numerator[bits - 1 - run] == 1
  Rational(((1 << run) - 1) << (bits - run), fraction.denominator)
end

#multiplier(multiplier_string) ⇒ Object (private)



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/head_music/notation/abc/duration_resolver.rb', line 33

def multiplier(multiplier_string)
  source = multiplier_string.to_s
  match = MULTIPLIER_PATTERN.match(source)
  raise_error("malformed note length multiplier", source) unless match

  numerator = (match[1] || 1).to_i
  slashes = match[2]
  denominator = match[3]
  return Rational(numerator) unless slashes
  return Rational(numerator, 2**slashes.length) unless denominator

  # An explicit denominator pairs with exactly one slash ("3/2", not "3//2").
  raise_error("malformed note length multiplier", source) if slashes.length > 1
  raise_error("note length denominator cannot be zero", source) if denominator.to_i.zero?
  Rational(numerator, denominator.to_i)
end

#odd_factor(integer) ⇒ Object (private)



97
98
99
100
# File 'lib/head_music/notation/abc/duration_resolver.rb', line 97

def odd_factor(integer)
  integer >>= 1 while integer.even?
  integer
end

#power_of_two?(integer) ⇒ Boolean (private)

Returns:

  • (Boolean)


102
103
104
# File 'lib/head_music/notation/abc/duration_resolver.rb', line 102

def power_of_two?(integer)
  (integer & (integer - 1)).zero?
end

#raise_error(message, source) ⇒ Object (private)



106
107
108
# File 'lib/head_music/notation/abc/duration_resolver.rb', line 106

def raise_error(message, source)
  raise HeadMusic::Notation::ABC::ParseError.new(message, snippet: source)
end

#rhythmic_value(multiplier_string, scale: Rational(1)) ⇒ Object

scale: an extra multiplier applied outside the note’s own length string, used for broken-rhythm pairs (3/2 and 1/2).



25
26
27
28
29
# File 'lib/head_music/notation/abc/duration_resolver.rb', line 25

def rhythmic_value(multiplier_string, scale: Rational(1))
  fraction = unit_note_length * multiplier(multiplier_string) * scale
  validate_fraction!(fraction, multiplier_string)
  build_rhythmic_value(fraction, multiplier_string)
end

#single_value(fraction, dots, source, tied_value: nil) ⇒ Object (private)



70
71
72
73
74
# File 'lib/head_music/notation/abc/duration_resolver.rb', line 70

def single_value(fraction, dots, source, tied_value: nil)
  # A value with d dots spans (2^(d+1) - 1) / 2^d of its unit.
  unit_fraction = fraction * Rational(2**dots, (2**(dots + 1)) - 1)
  HeadMusic::Rudiment::RhythmicValue.new(unit_for(unit_fraction, source), dots: dots, tied_value: tied_value)
end

#unit_for(unit_fraction, source) ⇒ Object (private)



86
87
88
89
90
91
92
93
94
95
# File 'lib/head_music/notation/abc/duration_resolver.rb', line 86

def unit_for(unit_fraction, source)
  unit = if unit_fraction >= 1
    name = UNIT_NAMES_BY_MULTIPLE[unit_fraction.numerator]
    name && HeadMusic::Rudiment::RhythmicUnit.get(name)
  else
    HeadMusic::Rudiment::RhythmicUnit.for_denominator_value(unit_fraction.denominator)
  end
  raise_error("no rhythmic unit for a note length of #{unit_fraction}", source) unless unit
  unit
end

#validate_fraction!(fraction, source) ⇒ Object (private)



50
51
52
53
54
55
56
# File 'lib/head_music/notation/abc/duration_resolver.rb', line 50

def validate_fraction!(fraction, source)
  raise_error("note length must be positive", source) if fraction <= 0
  raise_error("note length exceeds #{MAX_FRACTION.to_i} whole notes", source) if fraction > MAX_FRACTION
  return if power_of_two?(fraction.denominator)

  raise_error("note length #{fraction} is not expressible in binary note values", source)
end