head_music 0.16.2 → 0.17.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3eff7c01e7dbb6e69538d9cdcbad34c55fc9f05e
4
- data.tar.gz: e78e38ddd5ec3e8c77aae6a9dc371df7183ef29d
3
+ metadata.gz: 3ff78a490907bb272ceebc6b8d7b635ec7ddc629
4
+ data.tar.gz: a0dc85d1b1862519ea0ef6fb2469778180e86bab
5
5
  SHA512:
6
- metadata.gz: 98da361ccd151df8d8b8d31688a783cbf3586845e06bf5584e6131b0eb657f18f820f0111e86cdd6a38e65770a99a64588e5693d938aa364b317f590f01db330
7
- data.tar.gz: 67bfc70343e9c9733671aea1e9d1d80291ee4233e70fe60899e22efe61f8f6132cff06e54804aa34e3b098080db22919ba38a695a615d42219ba04ee2a3f1c74
6
+ metadata.gz: 0bd80123c3cb687b3bdf9feae1bf82bbfbe71dcfd0a40714d5e698d4859e1578db55e2cf447f0c4515547053ac0d2f46379ab7593c038a221f84679f6c7bd263
7
+ data.tar.gz: 6fd59458857b94183c868f3c303f237bab322d455b9bb485a64282e6799ac188fd35bb30329ff903ae1a8bb5bf4bddcf9d1471290570085b44875ec854236c27
@@ -45,6 +45,14 @@ class HeadMusic::Composition
45
45
  [voices.map(&:earliest_bar_number), 1].flatten.max
46
46
  end
47
47
 
48
+ def cantus_firmus_voice
49
+ voices.detect(&:cantus_firmus?)
50
+ end
51
+
52
+ def counterpoint_voice
53
+ voices.reject(&:cantus_firmus?).first
54
+ end
55
+
48
56
  private
49
57
 
50
58
  def ensure_attributes(name, key_signature, meter)
@@ -3,8 +3,8 @@ class HeadMusic::KeySignature
3
3
  attr_reader :scale_type
4
4
  attr_reader :scale
5
5
 
6
- SHARPS = %w{F# C# G# D# A# E# B#}
7
- FLATS = %w{Bb Eb Ab Db Gb Cb Fb}
6
+ SHARPS = %w{F C G D A E B}
7
+ FLATS = %w{B♭ E♭ A♭ D♭ G♭ C♭ F♭}
8
8
 
9
9
  def self.default
10
10
  @default ||= new('C', :major)
@@ -14,7 +14,7 @@ class HeadMusic::KeySignature
14
14
  return identifier if identifier.is_a?(HeadMusic::KeySignature)
15
15
  @key_signatures ||= {}
16
16
  tonic_spelling, scale_type_name = identifier.strip.split(/\s/)
17
- hash_key = HeadMusic::Utilities::HashKey.for(identifier.gsub(/#/, 'sharp').gsub(/b/, 'flat'))
17
+ hash_key = HeadMusic::Utilities::HashKey.for(identifier.gsub(/#|♯/, 'sharp').gsub(/b|♭/, 'flat'))
18
18
  @key_signatures[hash_key] ||= new(tonic_spelling, scale_type_name)
19
19
  end
20
20
 
@@ -55,7 +55,7 @@ class HeadMusic::KeySignature
55
55
  end
56
56
 
57
57
  def name
58
- [tonic_spelling.to_s, scale_type.to_s].join(' ')
58
+ [tonic_spelling, scale_type].join(' ')
59
59
  end
60
60
 
61
61
  def ==(other)
@@ -0,0 +1,54 @@
1
+ class HeadMusic::Language
2
+ include Comparable
3
+ include HeadMusic::NamedRudiment
4
+
5
+ LANGUAGES = [
6
+ { name: 'American English', short_name: 'English', abbreviation: 'en-US' },
7
+ { name: 'British English', abbreviation: 'en-GB' },
8
+ { name: 'French', native_name: 'Français', abbreviation: 'fr' },
9
+ { name: 'German', native_name: 'Deutsche', abbreviation: 'de' },
10
+ { name: 'Italian', native_name: 'Italiano', abbreviation: 'it' },
11
+ { name: 'Spanish', native_name: 'Español', abbreviation: 'es' },
12
+ { name: 'Russian', native_name: 'русский', abbreviation: 'ru' },
13
+ ]
14
+
15
+ LANGUAGES.map { |language| ::HeadMusic::Utilities::HashKey.for(language[:name]) }.each do |language_key|
16
+ define_singleton_method(language_key) { HeadMusic::Language.get(language_key) }
17
+ end
18
+
19
+ LANGUAGES.map { |language| ::HeadMusic::Utilities::HashKey.for(language[:native_name]) }.reject(&:nil?).each do |language_key|
20
+ define_singleton_method(language_key) { HeadMusic::Language.get(language_key) }
21
+ end
22
+
23
+ LANGUAGES.map { |language| ::HeadMusic::Utilities::HashKey.for(language[:short_name]) }.reject(&:nil?).each do |language_key|
24
+ define_singleton_method(language_key) { HeadMusic::Language.get(language_key) }
25
+ end
26
+
27
+ def self.default
28
+ self.english
29
+ end
30
+
31
+ def self.get(name)
32
+ get_by_name(name) || default
33
+ end
34
+
35
+ attr_accessor :name, :native_name, :short_name
36
+
37
+ def initialize(identifier)
38
+ identifier_key = HeadMusic::Utilities::HashKey.for(identifier)
39
+ language_data = LANGUAGES.detect { |data|
40
+ [:name, :native_name, :short_name].map { |key| HeadMusic::Utilities::HashKey.for(data[key]) }.include?(identifier_key)
41
+ }
42
+ @name = language_data[:name]
43
+ @native_name = language_data[:native_name]
44
+ @short_name = language_data[:short_name]
45
+ end
46
+
47
+ def <=>(other)
48
+ name.to_s <=> other.to_s
49
+ end
50
+
51
+ def inspect
52
+ [name, native_name, short_name].reject(&:nil?).join(' / ')
53
+ end
54
+ end
@@ -3,8 +3,6 @@ class HeadMusic::Octave
3
3
 
4
4
  DEFAULT = 4
5
5
 
6
- MATCHER = /(-?\d+)$/
7
-
8
6
  def self.get(identifier)
9
7
  from_number(identifier) || from_name(identifier) || default
10
8
  end
@@ -18,7 +16,7 @@ class HeadMusic::Octave
18
16
 
19
17
  def self.from_name(string)
20
18
  if string.to_s.match(HeadMusic::Spelling::MATCHER)
21
- _letter, _accidental, octave_string = string.to_s.match(HeadMusic::Spelling::MATCHER).captures
19
+ _letter, _sign, octave_string = string.to_s.match(HeadMusic::Spelling::MATCHER).captures
22
20
  @octaves ||= {}
23
21
  @octaves[octave_string.to_i] ||= new(octave_string.to_i) if octave_string
24
22
  end
@@ -5,9 +5,9 @@ class HeadMusic::Pitch
5
5
  attr_reader :octave
6
6
 
7
7
  delegate :letter_name, :letter_name_cycle, to: :spelling
8
- delegate :accidental, :sharp?, :flat?, to: :spelling
8
+ delegate :sign, :sharp?, :flat?, to: :spelling
9
9
  delegate :pitch_class, to: :spelling
10
- delegate :semitones, to: :accidental, prefix: true, allow_nil: true
10
+ delegate :semitones, to: :sign, prefix: true, allow_nil: true
11
11
 
12
12
  delegate :smallest_interval_to, to: :pitch_class
13
13
 
@@ -32,17 +32,17 @@ class HeadMusic::Pitch
32
32
  natural_letter_pitch = get(HeadMusic::LetterName.get(letter_name).pitch_class)
33
33
  natural_letter_pitch += 12 while (number - natural_letter_pitch.to_i).to_i >= 11
34
34
  natural_letter_pitch = get(natural_letter_pitch)
35
- accidental_interval = natural_letter_pitch.smallest_interval_to(HeadMusic::PitchClass.get(number))
36
- accidental = HeadMusic::Accidental.for_interval(accidental_interval)
37
- spelling = HeadMusic::Spelling.fetch_or_create(letter_name, accidental)
35
+ sign_interval = natural_letter_pitch.smallest_interval_to(HeadMusic::PitchClass.get(number))
36
+ sign = HeadMusic::Sign.by(:semitones, sign_interval) if sign_interval != 0
37
+ spelling = HeadMusic::Spelling.fetch_or_create(letter_name, sign)
38
38
  fetch_or_create(spelling, natural_letter_pitch.octave)
39
39
  end
40
40
 
41
41
  def self.fetch_or_create(spelling, octave)
42
42
  @pitches ||= {}
43
43
  if spelling && (-1..9).include?(octave)
44
- key = [spelling, octave].join
45
- @pitches[key] ||= new(spelling, octave)
44
+ hash_key = [spelling, octave].join
45
+ @pitches[hash_key] ||= new(spelling, octave)
46
46
  end
47
47
  end
48
48
 
@@ -56,7 +56,7 @@ class HeadMusic::Pitch
56
56
  end
57
57
 
58
58
  def midi_note_number
59
- (octave + 1) * 12 + letter_name.pitch_class.to_i + accidental_semitones.to_i
59
+ (octave + 1) * 12 + letter_name.pitch_class.to_i + sign_semitones.to_i
60
60
  end
61
61
 
62
62
  alias_method :midi, :midi_note_number
@@ -93,7 +93,8 @@ class HeadMusic::Pitch
93
93
  end
94
94
 
95
95
  def ==(value)
96
- to_s == value.to_s
96
+ other = HeadMusic::Pitch.get(value)
97
+ to_s == other.to_s
97
98
  end
98
99
 
99
100
  def <=>(other)
@@ -8,7 +8,7 @@ class HeadMusic::Scale
8
8
  root_pitch = HeadMusic::Pitch.get(root_pitch)
9
9
  scale_type = HeadMusic::ScaleType.get(scale_type || :major)
10
10
  @scales ||= {}
11
- name = [root_pitch.to_s, scale_type].join(' ')
11
+ name = [root_pitch, scale_type].join(' ')
12
12
  hash_key = HeadMusic::Utilities::HashKey.for(name)
13
13
  @scales[hash_key] ||= new(root_pitch, scale_type)
14
14
  end
@@ -16,21 +16,20 @@ class HeadMusic::ScaleDegree
16
16
  scale.letter_name_cycle.index(spelling.letter_name.to_s) + 1
17
17
  end
18
18
 
19
- def accidental
20
- spelling.accidental
21
- scale_degree_usual_spelling.accidental
22
- accidental_semitones = spelling.accidental && spelling.accidental.semitones || 0
23
- usual_accidental_semitones = scale_degree_usual_spelling.accidental && scale_degree_usual_spelling.accidental.semitones || 0
24
- HeadMusic::Accidental.for_interval(accidental_semitones - usual_accidental_semitones)
19
+ def sign
20
+ sign_semitones = spelling.sign && spelling.sign.semitones || 0
21
+ usual_sign_semitones = scale_degree_usual_spelling.sign && scale_degree_usual_spelling.sign.semitones || 0
22
+ delta = sign_semitones - usual_sign_semitones
23
+ HeadMusic::Sign.by(:semitones, delta) if delta != 0
25
24
  end
26
25
 
27
26
  def to_s
28
- "#{accidental}#{degree}"
27
+ "#{sign}#{degree}"
29
28
  end
30
29
 
31
30
  def <=>(other)
32
31
  if other.is_a?(HeadMusic::ScaleDegree)
33
- [degree, accidental.semitones] <=> [other.degree, other.accidental.semitones]
32
+ [degree, sign.semitones] <=> [other.degree, other.sign.semitones]
34
33
  else
35
34
  to_s <=> other.to_s
36
35
  end
@@ -39,7 +38,7 @@ class HeadMusic::ScaleDegree
39
38
  def name_for_degree
40
39
  if scale_type.diatonic?
41
40
  NAME_FOR_DIATONIC_DEGREE[degree] ||
42
- (scale_type.intervals.last == 1 || accidental == '#' ? 'leading tone' : 'subtonic')
41
+ (scale_type.intervals.last == 1 || sign == '#' ? 'leading tone' : 'subtonic')
43
42
  end
44
43
  end
45
44
 
@@ -0,0 +1,72 @@
1
+ class HeadMusic::Sign
2
+ include Comparable
3
+
4
+ attr_reader :identifier, :name, :ascii, :unicode, :html_entity, :cents
5
+
6
+ def self.all
7
+ @all ||= [
8
+ new(identifier: :sharp, name: 'sharp', ascii: '#', unicode: '♯', html_entity: '&#9839;', cents: 100),
9
+ new(identifier: :flat, name: 'flat', ascii: 'b', unicode: "♭", html_entity: '&#9837;', cents: -100),
10
+ new(identifier: :natural, name: 'natural', ascii: '', unicode: '♮', html_entity: '&#9838;', cents: 0),
11
+ new(identifier: :double_sharp, name: 'double sharp', ascii: '##', unicode: '𝄪', html_entity: '&#119082;', cents: 200),
12
+ new(identifier: :double_flat, name: 'double flat', ascii: 'bb', unicode: '𝄫', html_entity: '&#119083;', cents: -200),
13
+ ]
14
+ end
15
+
16
+ def self.symbols
17
+ @sign_symbols ||= all.map { |sign| [sign.ascii, sign.unicode] }.flatten.reject { |s| s.nil? || s.empty? }
18
+ end
19
+
20
+ def self.matcher
21
+ @matcher ||= Regexp.new symbols.join('|')
22
+ end
23
+
24
+ def self.symbol?(candidate)
25
+ /^(#{matcher})$/.match?(candidate)
26
+ end
27
+
28
+ def self.get(identifier)
29
+ return identifier if identifier.is_a?(HeadMusic::Sign)
30
+ all.detect do |sign|
31
+ sign.representions.include?(identifier)
32
+ end
33
+ end
34
+
35
+ def self.by(key, value)
36
+ all.detect do |sign|
37
+ if %i[cents semitones].include?(key.to_sym)
38
+ sign.send(key) == value
39
+ end
40
+ end
41
+ end
42
+
43
+ def representions
44
+ [identifier, identifier.to_s, name, ascii, unicode, html_entity].reject { |representation| representation.to_s.strip == '' }
45
+ end
46
+
47
+ def semitones
48
+ cents / 100.0
49
+ end
50
+
51
+ def to_s
52
+ unicode
53
+ end
54
+
55
+ def <=>(other)
56
+ other = HeadMusic::Sign.get(other)
57
+ self.cents <=> other.cents
58
+ end
59
+
60
+ private
61
+
62
+ def initialize(attributes)
63
+ @identifier = attributes[:identifier]
64
+ @name = attributes[:name]
65
+ @ascii = attributes[:ascii]
66
+ @unicode = attributes[:unicode]
67
+ @html_entity = attributes[:html_entity]
68
+ @cents = attributes[:cents]
69
+ end
70
+
71
+ private_class_method :new
72
+ end
@@ -1,9 +1,9 @@
1
1
  class HeadMusic::Spelling
2
- MATCHER = /^\s*([A-G])([b#]*)(\-?\d+)?\s*$/i
2
+ MATCHER = /^\s*([A-G])(#{HeadMusic::Sign.matcher}?)(\-?\d+)?\s*$/i
3
3
 
4
4
  attr_reader :pitch_class
5
5
  attr_reader :letter_name
6
- attr_reader :accidental
6
+ attr_reader :sign
7
7
 
8
8
  delegate :number, to: :pitch_class, prefix: true
9
9
  delegate :to_i, to: :pitch_class_number
@@ -11,6 +11,7 @@ class HeadMusic::Spelling
11
11
  delegate :enharmonic?, to: :pitch_class
12
12
 
13
13
  def self.get(identifier)
14
+ return identifier if identifier.is_a?(HeadMusic::Spelling)
14
15
  from_name(identifier) || from_number(identifier)
15
16
  end
16
17
 
@@ -20,11 +21,11 @@ class HeadMusic::Spelling
20
21
 
21
22
  def self.from_name(name)
22
23
  if match(name)
23
- letter_name, accidental_string, _octave = match(name).captures
24
+ letter_name, sign_string, _octave = match(name).captures
24
25
  letter_name = HeadMusic::LetterName.get(letter_name)
25
26
  return nil unless letter_name
26
- accidental = HeadMusic::Accidental.get(accidental_string)
27
- fetch_or_create(letter_name, accidental)
27
+ sign = HeadMusic::Sign.get(sign_string)
28
+ fetch_or_create(letter_name, sign)
28
29
  end
29
30
  end
30
31
 
@@ -38,26 +39,26 @@ class HeadMusic::Spelling
38
39
  def self.from_number_and_letter(number, letter_name)
39
40
  letter_name = HeadMusic::LetterName.get(letter_name)
40
41
  natural_letter_pitch_class = letter_name.pitch_class
41
- accidental_interval = natural_letter_pitch_class.smallest_interval_to(HeadMusic::PitchClass.get(number))
42
- accidental = HeadMusic::Accidental.for_interval(accidental_interval)
43
- fetch_or_create(letter_name, accidental)
42
+ sign_interval = natural_letter_pitch_class.smallest_interval_to(HeadMusic::PitchClass.get(number))
43
+ sign = HeadMusic::Sign.by(:semitones, sign_interval) if sign_interval != 0
44
+ fetch_or_create(letter_name, sign)
44
45
  end
45
46
 
46
- def self.fetch_or_create(letter_name, accidental)
47
+ def self.fetch_or_create(letter_name, sign)
47
48
  @spellings ||= {}
48
- key = [letter_name, accidental].join
49
- @spellings[key] ||= new(letter_name, accidental)
49
+ hash_key = [letter_name, sign].join
50
+ @spellings[hash_key] ||= new(letter_name, sign)
50
51
  end
51
52
 
52
- def initialize(letter_name, accidental = nil)
53
+ def initialize(letter_name, sign = nil)
53
54
  @letter_name = HeadMusic::LetterName.get(letter_name.to_s)
54
- @accidental = HeadMusic::Accidental.get(accidental.to_s)
55
- accidental_semitones = @accidental ? @accidental.semitones : 0
56
- @pitch_class = HeadMusic::PitchClass.get(letter_name.pitch_class + accidental_semitones)
55
+ @sign = HeadMusic::Sign.get(sign)
56
+ sign_semitones = @sign ? @sign.semitones : 0
57
+ @pitch_class = HeadMusic::PitchClass.get(letter_name.pitch_class + sign_semitones)
57
58
  end
58
59
 
59
60
  def name
60
- [letter_name, accidental].join
61
+ [letter_name, sign].join
61
62
  end
62
63
 
63
64
  def to_s
@@ -65,15 +66,16 @@ class HeadMusic::Spelling
65
66
  end
66
67
 
67
68
  def sharp?
68
- accidental && accidental == '#'
69
+ sign && sign == '#'
69
70
  end
70
71
 
71
72
  def flat?
72
- accidental && accidental == 'b'
73
+ sign && sign == 'b'
73
74
  end
74
75
 
75
76
  def ==(value)
76
- to_s == value.to_s
77
+ other = HeadMusic::Spelling.get(value)
78
+ to_s == other.to_s
77
79
  end
78
80
 
79
81
  def scale(scale_type_name = nil)
@@ -14,7 +14,7 @@ class HeadMusic::Style::Annotations::Diatonic < HeadMusic::Style::Annotation
14
14
  notes_not_in_key.reject do |note|
15
15
  penultimate_note &&
16
16
  note == penultimate_note &&
17
- HeadMusic::ScaleDegree.new(key_signature, note.pitch.spelling).accidental == '#'
17
+ HeadMusic::ScaleDegree.new(key_signature, note.pitch.spelling).sign == '#'
18
18
  end
19
19
  end
20
20
 
@@ -1,3 +1,3 @@
1
1
  module HeadMusic
2
- VERSION = "0.16.2"
2
+ VERSION = "0.17.0"
3
3
  end
data/lib/head_music.rb CHANGED
@@ -11,9 +11,13 @@ require 'active_support/core_ext/module/delegation'
11
11
  require 'active_support/core_ext/string/access'
12
12
  require 'humanize'
13
13
 
14
+ # utilities
15
+ require 'head_music/utilities/hash_key'
16
+
17
+ # modules
14
18
  require 'head_music/named_rudiment'
15
19
 
16
- require 'head_music/accidental'
20
+ # rudiments
17
21
  require 'head_music/bar'
18
22
  require 'head_music/chord'
19
23
  require 'head_music/circle'
@@ -26,6 +30,7 @@ require 'head_music/harmonic_interval'
26
30
  require 'head_music/instrument'
27
31
  require 'head_music/interval'
28
32
  require 'head_music/key_signature'
33
+ require 'head_music/language'
29
34
  require 'head_music/letter_name'
30
35
  require 'head_music/melodic_interval'
31
36
  require 'head_music/meter'
@@ -43,13 +48,17 @@ require 'head_music/rhythmic_value'
43
48
  require 'head_music/scale'
44
49
  require 'head_music/scale_degree'
45
50
  require 'head_music/scale_type'
51
+ require 'head_music/sign'
46
52
  require 'head_music/spelling'
47
53
  require 'head_music/staff'
54
+ require 'head_music/voice'
48
55
 
56
+ # analysis
49
57
  require 'head_music/style/analysis'
50
58
  require 'head_music/style/annotation'
51
59
  require 'head_music/style/mark'
52
60
 
61
+ # annotations
53
62
  require 'head_music/style/annotations/always_move'
54
63
  require 'head_music/style/annotations/approach_perfection_contrarily'
55
64
  require 'head_music/style/annotations/at_least_eight_notes'
@@ -84,10 +93,8 @@ require 'head_music/style/annotations/step_to_final_note'
84
93
  require 'head_music/style/annotations/step_up_to_final_note'
85
94
  require 'head_music/style/annotations/up_to_fourteen_notes'
86
95
 
96
+ # rulesets
87
97
  require 'head_music/style/rulesets/fux_cantus_firmus'
88
98
  require 'head_music/style/rulesets/modern_cantus_firmus'
89
99
  require 'head_music/style/rulesets/first_species_melody'
90
100
  require 'head_music/style/rulesets/first_species_harmony'
91
-
92
- require 'head_music/utilities/hash_key'
93
- require 'head_music/voice'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: head_music
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.16.2
4
+ version: 0.17.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rob Head
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-06-16 00:00:00.000000000 Z
11
+ date: 2018-02-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -116,7 +116,6 @@ files:
116
116
  - circle.yml
117
117
  - head_music.gemspec
118
118
  - lib/head_music.rb
119
- - lib/head_music/accidental.rb
120
119
  - lib/head_music/bar.rb
121
120
  - lib/head_music/chord.rb
122
121
  - lib/head_music/circle.rb
@@ -129,6 +128,7 @@ files:
129
128
  - lib/head_music/instrument.rb
130
129
  - lib/head_music/interval.rb
131
130
  - lib/head_music/key_signature.rb
131
+ - lib/head_music/language.rb
132
132
  - lib/head_music/letter_name.rb
133
133
  - lib/head_music/melodic_interval.rb
134
134
  - lib/head_music/meter.rb
@@ -147,6 +147,7 @@ files:
147
147
  - lib/head_music/scale.rb
148
148
  - lib/head_music/scale_degree.rb
149
149
  - lib/head_music/scale_type.rb
150
+ - lib/head_music/sign.rb
150
151
  - lib/head_music/spelling.rb
151
152
  - lib/head_music/staff.rb
152
153
  - lib/head_music/style/analysis.rb
@@ -1,42 +0,0 @@
1
- class HeadMusic::Accidental
2
- ACCIDENTAL_SEMITONES = {
3
- '#' => 1,
4
- '##' => 2,
5
- 'b' => -1,
6
- 'bb' => -2
7
- }
8
-
9
- attr_reader :string
10
-
11
- def self.get(identifier)
12
- for_symbol(identifier) || for_interval(identifier)
13
- end
14
-
15
- def self.for_symbol(identifier)
16
- @accidentals ||= {}
17
- @accidentals[identifier.to_s] ||= new(identifier.to_s) if ACCIDENTAL_SEMITONES.keys.include?(identifier.to_s)
18
- end
19
-
20
- def self.for_interval(semitones)
21
- @accidentals ||= {}
22
- @accidentals[semitones.to_i] ||= ACCIDENTAL_SEMITONES.key(semitones.to_i)
23
- end
24
-
25
- def initialize(string)
26
- @string = string
27
- end
28
-
29
- def to_s
30
- string
31
- end
32
-
33
- def ==(value)
34
- to_s == value.to_s
35
- end
36
-
37
- def semitones
38
- ACCIDENTAL_SEMITONES.fetch(string, 0)
39
- end
40
-
41
- private_class_method :new
42
- end