head_music 0.26.3 → 0.28.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +1 -1
  3. data/.rubocop.yml +11 -37
  4. data/.ruby-version +1 -1
  5. data/.travis.yml +1 -1
  6. data/Gemfile +8 -6
  7. data/Rakefile +5 -4
  8. data/bin/console +3 -3
  9. data/head_music.gemspec +21 -22
  10. data/lib/head_music/chromatic_interval.rb +3 -3
  11. data/lib/head_music/circle.rb +18 -14
  12. data/lib/head_music/clef.rb +3 -3
  13. data/lib/head_music/content/bar.rb +1 -1
  14. data/lib/head_music/content/composition.rb +1 -1
  15. data/lib/head_music/content/note.rb +1 -1
  16. data/lib/head_music/content/placement.rb +5 -5
  17. data/lib/head_music/content/position.rb +3 -3
  18. data/lib/head_music/content/rhythmic_value.rb +8 -8
  19. data/lib/head_music/content/voice.rb +1 -1
  20. data/lib/head_music/diatonic_interval.rb +41 -43
  21. data/lib/head_music/grand_staff.rb +8 -8
  22. data/lib/head_music/instrument.rb +8 -8
  23. data/lib/head_music/interval_cycle.rb +10 -12
  24. data/lib/head_music/key_signature.rb +12 -13
  25. data/lib/head_music/letter_name.rb +7 -7
  26. data/lib/head_music/melodic_interval.rb +1 -1
  27. data/lib/head_music/meter.rb +17 -12
  28. data/lib/head_music/named.rb +3 -3
  29. data/lib/head_music/pitch/enharmonic_equivalence.rb +2 -2
  30. data/lib/head_music/pitch/octave_equivalence.rb +1 -1
  31. data/lib/head_music/pitch.rb +24 -9
  32. data/lib/head_music/pitch_class.rb +18 -4
  33. data/lib/head_music/pitch_class_set.rb +5 -5
  34. data/lib/head_music/pitch_set.rb +9 -6
  35. data/lib/head_music/quality.rb +15 -14
  36. data/lib/head_music/reference_pitch.rb +23 -23
  37. data/lib/head_music/register.rb +13 -0
  38. data/lib/head_music/rhythmic_unit.rb +9 -9
  39. data/lib/head_music/scale.rb +4 -4
  40. data/lib/head_music/scale_degree.rb +2 -2
  41. data/lib/head_music/scale_type.rb +13 -13
  42. data/lib/head_music/sign.rb +12 -11
  43. data/lib/head_music/solmization.rb +3 -3
  44. data/lib/head_music/sonority.rb +2 -2
  45. data/lib/head_music/spelling.rb +4 -6
  46. data/lib/head_music/staff.rb +1 -1
  47. data/lib/head_music/style/analysis.rb +1 -1
  48. data/lib/head_music/style/annotation.rb +7 -7
  49. data/lib/head_music/style/guidelines/always_move.rb +4 -4
  50. data/lib/head_music/style/guidelines/approach_perfection_contrarily.rb +1 -1
  51. data/lib/head_music/style/guidelines/at_least_eight_notes.rb +3 -3
  52. data/lib/head_music/style/guidelines/avoid_crossing_voices.rb +6 -6
  53. data/lib/head_music/style/guidelines/avoid_overlapping_voices.rb +1 -1
  54. data/lib/head_music/style/guidelines/consonant_climax.rb +1 -1
  55. data/lib/head_music/style/guidelines/consonant_downbeats.rb +1 -1
  56. data/lib/head_music/style/guidelines/diatonic.rb +2 -2
  57. data/lib/head_music/style/guidelines/end_on_perfect_consonance.rb +1 -1
  58. data/lib/head_music/style/guidelines/end_on_tonic.rb +1 -1
  59. data/lib/head_music/style/guidelines/frequent_direction_changes.rb +1 -1
  60. data/lib/head_music/style/guidelines/limit_octave_leaps.rb +1 -1
  61. data/lib/head_music/style/guidelines/moderate_direction_changes.rb +1 -1
  62. data/lib/head_music/style/guidelines/mostly_conjunct.rb +4 -4
  63. data/lib/head_music/style/guidelines/no_rests.rb +1 -1
  64. data/lib/head_music/style/guidelines/no_unisons_in_middle.rb +1 -1
  65. data/lib/head_music/style/guidelines/notes_same_length.rb +4 -4
  66. data/lib/head_music/style/guidelines/one_to_one.rb +1 -1
  67. data/lib/head_music/style/guidelines/prefer_contrary_motion.rb +1 -1
  68. data/lib/head_music/style/guidelines/prefer_imperfect.rb +1 -1
  69. data/lib/head_music/style/guidelines/prepare_octave_leaps.rb +1 -1
  70. data/lib/head_music/style/guidelines/recover_large_leaps.rb +1 -1
  71. data/lib/head_music/style/guidelines/singable_intervals.rb +1 -1
  72. data/lib/head_music/style/guidelines/singable_range.rb +1 -1
  73. data/lib/head_music/style/guidelines/single_large_leaps.rb +1 -1
  74. data/lib/head_music/style/guidelines/start_on_perfect_consonance.rb +1 -1
  75. data/lib/head_music/style/guidelines/start_on_tonic.rb +1 -1
  76. data/lib/head_music/style/guidelines/step_down_to_final_note.rb +2 -2
  77. data/lib/head_music/style/guidelines/step_out_of_unison.rb +1 -1
  78. data/lib/head_music/style/guidelines/step_to_final_note.rb +2 -2
  79. data/lib/head_music/style/guidelines/step_up_to_final_note.rb +2 -2
  80. data/lib/head_music/style/guidelines/up_to_fourteen_notes.rb +2 -2
  81. data/lib/head_music/style/guides/first_species_harmony.rb +9 -1
  82. data/lib/head_music/style/guides/first_species_melody.rb +1 -1
  83. data/lib/head_music/style/guides/fux_cantus_firmus.rb +1 -1
  84. data/lib/head_music/style/guides/modern_cantus_firmus.rb +1 -1
  85. data/lib/head_music/style/mark.rb +1 -1
  86. data/lib/head_music/utilities/hash_key.rb +1 -1
  87. data/lib/head_music/version.rb +1 -1
  88. data/lib/head_music.rb +91 -91
  89. metadata +6 -20
@@ -4,7 +4,7 @@
4
4
  class HeadMusic::Instrument
5
5
  include HeadMusic::Named
6
6
 
7
- INSTRUMENTS = YAML.load_file(File.expand_path('data/instruments.yml', __dir__)).freeze
7
+ INSTRUMENTS = YAML.load_file(File.expand_path("data/instruments.yml", __dir__)).freeze
8
8
 
9
9
  def self.get(name)
10
10
  return get_by_name(name) if get_by_name(name)
@@ -59,20 +59,20 @@ class HeadMusic::Instrument
59
59
 
60
60
  def record_for_key(key)
61
61
  INSTRUMENTS.each do |name_key, data|
62
- return data.merge!('name_key' => name_key) if name_key.to_s == key.to_s
62
+ return data.merge!("name_key" => name_key) if name_key.to_s == key.to_s
63
63
  end
64
64
  nil
65
65
  end
66
66
 
67
67
  def initialize_data_from_record(record)
68
- @name_key = record['name_key'].to_sym
69
- @family = record['family']
70
- @standard_staves = record['standard_staves'] || []
71
- @classifications = record['classifications'] || []
72
- self.name = I18n.translate(name_key, scope: 'instruments', locale: 'en', default: inferred_name)
68
+ @name_key = record["name_key"].to_sym
69
+ @family = record["family"]
70
+ @standard_staves = record["standard_staves"] || []
71
+ @classifications = record["classifications"] || []
72
+ self.name = I18n.translate(name_key, scope: "instruments", locale: "en", default: inferred_name)
73
73
  end
74
74
 
75
75
  def inferred_name
76
- name_key.to_s.gsub(/_/, ' ')
76
+ name_key.to_s.tr("_", " ")
77
77
  end
78
78
  end
@@ -6,12 +6,12 @@ class HeadMusic::IntervalCycle
6
6
 
7
7
  def self.get(interval = 7)
8
8
  @interval_cycles ||= {}
9
- interval = interval.to_s.gsub(/^C/i, '').to_i
9
+ interval = interval.to_s.gsub(/^C/i, "").to_i
10
10
  interval = HeadMusic::ChromaticInterval.get(interval)
11
- @interval_cycles[interval.to_i] ||= new(interval: interval, starting_pitch: 'C4')
11
+ @interval_cycles[interval.to_i] ||= new(interval: interval, starting_pitch: "C4")
12
12
  end
13
13
 
14
- def initialize(interval:, starting_pitch: 'C4')
14
+ def initialize(interval:, starting_pitch: "C4")
15
15
  @interval = interval if interval.is_a?(HeadMusic::DiatonicInterval)
16
16
  @interval ||= interval if interval.is_a?(HeadMusic::ChromaticInterval)
17
17
  @interval ||= HeadMusic::ChromaticInterval.get(interval) if interval.to_s.match?(/\d/)
@@ -38,15 +38,13 @@ class HeadMusic::IntervalCycle
38
38
  protected
39
39
 
40
40
  def pitches_up
41
- @pitches_up ||= begin
42
- [starting_pitch].tap do |list|
43
- loop do
44
- next_pitch = list.last + interval
45
- next_pitch -= octave while next_pitch - starting_pitch > 12
46
- break if next_pitch.pitch_class == list.first.pitch_class
47
-
48
- list << next_pitch
49
- end
41
+ @pitches_up ||= [starting_pitch].tap do |list|
42
+ loop do
43
+ next_pitch = list.last + interval
44
+ next_pitch -= octave while next_pitch - starting_pitch > 12
45
+ break if next_pitch.pitch_class == list.first.pitch_class
46
+
47
+ list << next_pitch
50
48
  end
51
49
  end
52
50
  end
@@ -1,16 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Represents a key signature.
4
+ # In French, sharps and flats in the key signature are called "altérations".
4
5
  class HeadMusic::KeySignature
5
- attr_reader :tonic_spelling
6
- attr_reader :scale_type
7
- attr_reader :scale
6
+ attr_reader :tonic_spelling, :scale_type, :scale
8
7
 
9
8
  ORDERED_LETTER_NAMES_OF_SHARPS = %w[F C G D A E B].freeze
10
9
  ORDERED_LETTER_NAMES_OF_FLATS = ORDERED_LETTER_NAMES_OF_SHARPS.reverse.freeze
11
10
 
12
11
  def self.default
13
- @default ||= new('C', :major)
12
+ @default ||= new("C", :major)
14
13
  end
15
14
 
16
15
  def self.get(identifier)
@@ -18,7 +17,7 @@ class HeadMusic::KeySignature
18
17
 
19
18
  @key_signatures ||= {}
20
19
  tonic_spelling, scale_type_name = identifier.strip.split(/\s/)
21
- hash_key = HeadMusic::Utilities::HashKey.for(identifier.gsub(/#|♯/, ' sharp').gsub(/(\w)[b♭]/, '\\1 flat'))
20
+ hash_key = HeadMusic::Utilities::HashKey.for(identifier.gsub(/#|♯/, " sharp").gsub(/(\w)[b♭]/, '\\1 flat'))
22
21
  @key_signatures[hash_key] ||= new(tonic_spelling, scale_type_name)
23
22
  end
24
23
 
@@ -74,11 +73,11 @@ class HeadMusic::KeySignature
74
73
  flats.any? ? flats : sharps
75
74
  end
76
75
 
77
- alias sharps_and_flats signs
78
- alias accidentals signs
76
+ alias_method :sharps_and_flats, :signs
77
+ alias_method :accidentals, :signs
79
78
 
80
79
  def name
81
- [tonic_spelling, scale_type].join(' ')
80
+ [tonic_spelling, scale_type].join(" ")
82
81
  end
83
82
 
84
83
  def ==(other)
@@ -87,11 +86,11 @@ class HeadMusic::KeySignature
87
86
 
88
87
  def to_s
89
88
  if sharps.any?
90
- sharps.length == 1 ? '1 sharp' : "#{sharps.length} sharps"
89
+ (sharps.length == 1) ? "1 sharp" : "#{sharps.length} sharps"
91
90
  elsif flats.any?
92
- flats.length == 1 ? '1 flat' : "#{flats.length} flats"
91
+ (flats.length == 1) ? "1 flat" : "#{flats.length} flats"
93
92
  else
94
- 'no sharps or flats'
93
+ "no sharps or flats"
95
94
  end
96
95
  end
97
96
 
@@ -125,8 +124,8 @@ class HeadMusic::KeySignature
125
124
  (key_signature.signs | other.signs).map(&:to_s).uniq.length == 12
126
125
  end
127
126
 
128
- alias enharmonic? enharmonic_equivalent?
129
- alias equivalent? enharmonic_equivalent?
127
+ alias_method :enharmonic?, :enharmonic_equivalent?
128
+ alias_method :equivalent?, :enharmonic_equivalent?
130
129
 
131
130
  private_class_method :new
132
131
  end
@@ -5,13 +5,13 @@ class HeadMusic::LetterName
5
5
  NAMES = %w[C D E F G A B].freeze
6
6
 
7
7
  NATURAL_PITCH_CLASS_NUMBERS = {
8
- 'C' => 0,
9
- 'D' => 2,
10
- 'E' => 4,
11
- 'F' => 5,
12
- 'G' => 7,
13
- 'A' => 9,
14
- 'B' => 11,
8
+ "C" => 0,
9
+ "D" => 2,
10
+ "E" => 4,
11
+ "F" => 5,
12
+ "G" => 7,
13
+ "A" => 9,
14
+ "B" => 11
15
15
  }.freeze
16
16
 
17
17
  def self.all
@@ -38,7 +38,7 @@ class HeadMusic::MelodicInterval
38
38
  end
39
39
 
40
40
  def to_s
41
- [direction, diatonic_interval].join(' ')
41
+ [direction, diatonic_interval].join(" ")
42
42
  end
43
43
 
44
44
  def ascending?
@@ -5,8 +5,8 @@ class HeadMusic::Meter
5
5
  attr_reader :top_number, :bottom_number
6
6
 
7
7
  NAMED = {
8
- common_time: '4/4',
9
- cut_time: '2/2',
8
+ common_time: "4/4",
9
+ cut_time: "2/2"
10
10
  }.freeze
11
11
 
12
12
  def self.get(identifier)
@@ -14,11 +14,11 @@ class HeadMusic::Meter
14
14
  hash_key = HeadMusic::Utilities::HashKey.for(identifier)
15
15
  time_signature_string = NAMED[hash_key] || identifier
16
16
  @meters ||= {}
17
- @meters[hash_key] ||= new(*time_signature_string.split('/').map(&:to_i))
17
+ @meters[hash_key] ||= new(*time_signature_string.split("/").map(&:to_i))
18
18
  end
19
19
 
20
20
  def self.default
21
- get('4/4')
21
+ get("4/4")
22
22
  end
23
23
 
24
24
  def self.common_time
@@ -39,7 +39,7 @@ class HeadMusic::Meter
39
39
  end
40
40
 
41
41
  def compound?
42
- top_number > 3 && top_number / 3 == top_number / 3.0
42
+ top_number > 3 && (top_number % 3).zero?
43
43
  end
44
44
 
45
45
  def duple?
@@ -89,7 +89,7 @@ class HeadMusic::Meter
89
89
  end
90
90
 
91
91
  def to_s
92
- [top_number, bottom_number].join('/')
92
+ [top_number, bottom_number].join("/")
93
93
  end
94
94
 
95
95
  def ==(other)
@@ -97,10 +97,8 @@ class HeadMusic::Meter
97
97
  end
98
98
 
99
99
  def strong_counts
100
- @strong_counts ||= begin
101
- (1..counts_per_bar).select do |count|
102
- downbeat?(count) || strong_beat_in_duple?(count) || strong_beat_in_triple?(count)
103
- end
100
+ @strong_counts ||= (1..counts_per_bar).select do |count|
101
+ downbeat?(count) || strong_beat_in_duple?(count) || strong_beat_in_triple?(count)
104
102
  end
105
103
  end
106
104
 
@@ -119,11 +117,18 @@ class HeadMusic::Meter
119
117
  end
120
118
 
121
119
  def strong_beat_in_duple?(count, tick = 0)
122
- beat?(tick) && (count == counts_per_bar / 2.0 + 1)
120
+ return false unless beat?(tick)
121
+ return false unless counts_per_bar.even?
122
+
123
+ (count - 1) == counts_per_bar / 2
123
124
  end
124
125
 
125
126
  def strong_beat_in_triple?(count, tick = 0)
126
- beat?(tick) && (counts_per_bar % 3).zero? && counts_per_bar > 6 && count % 3 == 1
127
+ return false unless beat?(tick)
128
+ return false unless (counts_per_bar % 3).zero?
129
+ return false if counts_per_bar < 6
130
+
131
+ count % 3 == 1
127
132
  end
128
133
 
129
134
  def beat?(tick)
@@ -16,10 +16,10 @@ module HeadMusic::Named
16
16
 
17
17
  def self.get(code)
18
18
  @locales ||= {}
19
- parts = code.to_s.split(/[_\-]/)
19
+ parts = code.to_s.split(/[_-]/)
20
20
  language = parts[0].downcase
21
21
  region = parts[1]&.upcase
22
- key = [language, region].compact.join('_').to_sym
22
+ key = [language, region].compact.join("_").to_sym
23
23
  @locales[key] ||= new(language: language, region: region)
24
24
  end
25
25
 
@@ -29,7 +29,7 @@ module HeadMusic::Named
29
29
  end
30
30
 
31
31
  def code
32
- @code ||= [@language, @region].compact.join('_')
32
+ @code ||= [@language, @region].compact.join("_")
33
33
  end
34
34
 
35
35
  private_class_method :new
@@ -21,8 +21,8 @@ class HeadMusic::Pitch::EnharmonicEquivalence
21
21
  pitch.midi_note_number == other.midi_note_number && pitch.spelling != other.spelling
22
22
  end
23
23
 
24
- alias enharmonic? enharmonic_equivalent?
25
- alias equivalent? enharmonic_equivalent?
24
+ alias_method :enharmonic?, :enharmonic_equivalent?
25
+ alias_method :equivalent?, :enharmonic_equivalent?
26
26
 
27
27
  private_class_method :new
28
28
  end
@@ -18,7 +18,7 @@ class HeadMusic::Pitch::OctaveEquivalence
18
18
  pitch.spelling == other.spelling && pitch.register != other.register
19
19
  end
20
20
 
21
- alias equivalent? octave_equivalent?
21
+ alias_method :equivalent?, :octave_equivalent?
22
22
 
23
23
  private_class_method :new
24
24
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # A pitch is a named frequency represented by a spelling and an octive.
3
+ # A pitch is a named frequency represented by a spelling and a register.
4
4
  class HeadMusic::Pitch
5
5
  include Comparable
6
6
 
@@ -34,11 +34,11 @@ class HeadMusic::Pitch
34
34
  end
35
35
 
36
36
  def self.middle_c
37
- get('C4')
37
+ get("C4")
38
38
  end
39
39
 
40
40
  def self.concert_a
41
- get('A4')
41
+ get("A4")
42
42
  end
43
43
 
44
44
  def self.from_pitch_class(pitch_class)
@@ -97,8 +97,8 @@ class HeadMusic::Pitch
97
97
  (register + 1) * 12 + letter_name.pitch_class.to_i + sign_semitones.to_i
98
98
  end
99
99
 
100
- alias midi midi_note_number
101
- alias number midi_note_number
100
+ alias_method :midi, :midi_note_number
101
+ alias_method :number, :midi_note_number
102
102
 
103
103
  def to_s
104
104
  name
@@ -108,8 +108,12 @@ class HeadMusic::Pitch
108
108
  midi_note_number
109
109
  end
110
110
 
111
+ def helmholtz_notation
112
+ helmholtz_letter_name + helmholtz_marks
113
+ end
114
+
111
115
  def natural
112
- HeadMusic::Pitch.get(to_s.gsub(HeadMusic::Sign.matcher, ''))
116
+ HeadMusic::Pitch.get(to_s.gsub(HeadMusic::Sign.matcher, ""))
113
117
  end
114
118
 
115
119
  def +(other)
@@ -123,10 +127,11 @@ class HeadMusic::Pitch
123
127
  end
124
128
 
125
129
  def -(other)
126
- if other.is_a?(HeadMusic::DiatonicInterval)
130
+ case other
131
+ when HeadMusic::DiatonicInterval
127
132
  # return a pitch
128
133
  other.below(self)
129
- elsif other.is_a?(HeadMusic::Pitch)
134
+ when HeadMusic::Pitch
130
135
  # return an interval
131
136
  HeadMusic::ChromaticInterval.get(to_i - other.to_i)
132
137
  else
@@ -190,7 +195,7 @@ class HeadMusic::Pitch
190
195
  end
191
196
 
192
197
  def octaves_delta(num_steps)
193
- octaves_delta = (num_steps.abs / 7) * (num_steps >= 0 ? 1 : -1)
198
+ octaves_delta = (num_steps.abs / 7) * ((num_steps >= 0) ? 1 : -1)
194
199
  if wrapped_down?(num_steps)
195
200
  octaves_delta -= 1
196
201
  elsif wrapped_up?(num_steps)
@@ -211,4 +216,14 @@ class HeadMusic::Pitch
211
216
  @target_letter_name ||= {}
212
217
  @target_letter_name[num_steps] ||= letter_name.steps_up(num_steps)
213
218
  end
219
+
220
+ def helmholtz_letter_name
221
+ return spelling.to_s.downcase if HeadMusic::Register.get(register).helmholtz_case == :lower
222
+
223
+ spelling.to_s
224
+ end
225
+
226
+ def helmholtz_marks
227
+ HeadMusic::Register.get(register).helmholtz_marks
228
+ end
214
229
  end
@@ -4,11 +4,11 @@
4
4
  class HeadMusic::PitchClass
5
5
  include Comparable
6
6
 
7
- attr_reader :number
8
- attr_reader :spelling
7
+ attr_reader :number, :spelling
9
8
 
10
9
  SHARP_SPELLINGS = %w[C C♯ D D♯ E F F♯ G G♯ A A♯ B].freeze
11
10
  FLAT_SPELLINGS = %w[C D♭ D E♭ E F G♭ G A♭ A B♭ B].freeze
11
+ FLATTER_SPELLINGS = %w[C D♭ D E♭ F♭ F G♭ G A♭ A B♭ C♭].freeze
12
12
  INTEGER_NOTATION = %w[0 1 2 3 4 5 6 7 8 9 t e].freeze
13
13
 
14
14
  def self.get(identifier)
@@ -22,7 +22,7 @@ class HeadMusic::PitchClass
22
22
  end
23
23
 
24
24
  class << self
25
- alias [] get
25
+ alias_method :[], :get
26
26
  end
27
27
 
28
28
  def initialize(pitch_class_or_midi_number)
@@ -45,6 +45,20 @@ class HeadMusic::PitchClass
45
45
  FLAT_SPELLINGS[number]
46
46
  end
47
47
 
48
+ def flatter_spelling
49
+ FLATTER_SPELLINGS[number]
50
+ end
51
+
52
+ def smart_spelling(max_sharps_in_major_key_signature: 6)
53
+ sharp_key = HeadMusic::KeySignature.get(sharp_spelling)
54
+ return HeadMusic::Spelling.get(sharp_spelling) if sharp_key.num_sharps <= max_sharps_in_major_key_signature
55
+
56
+ flat_key = HeadMusic::KeySignature.get(flat_spelling)
57
+ return HeadMusic::Spelling.get(flat_spelling) if flat_key.num_sharps <= max_sharps_in_major_key_signature
58
+
59
+ HeadMusic::Spelling.get(flatter_spelling)
60
+ end
61
+
48
62
  # Pass in the number of semitones
49
63
  def +(other)
50
64
  HeadMusic::PitchClass.get(to_i + other.to_i)
@@ -58,7 +72,7 @@ class HeadMusic::PitchClass
58
72
  def ==(other)
59
73
  to_i == other.to_i
60
74
  end
61
- alias enharmonic? ==
75
+ alias_method :enharmonic?, :==
62
76
 
63
77
  def <=>(other)
64
78
  to_i <=> other.to_i
@@ -6,18 +6,18 @@ class HeadMusic::PitchClassSet
6
6
  attr_reader :pitch_classes
7
7
 
8
8
  delegate :empty?, to: :pitch_classes
9
- alias empty_set? empty?
9
+ alias_method :empty_set?, :empty?
10
10
 
11
11
  def initialize(identifiers)
12
12
  @pitch_classes = identifiers.map { |identifier| HeadMusic::PitchClass.get(identifier) }.uniq.sort
13
13
  end
14
14
 
15
15
  def inspect
16
- pitch_classes.map(&:to_s).join(' ')
16
+ pitch_classes.map(&:to_s).join(" ")
17
17
  end
18
18
 
19
19
  def to_s
20
- pitch_classes.map(&:to_s).join(' ')
20
+ pitch_classes.map(&:to_s).join(" ")
21
21
  end
22
22
 
23
23
  def ==(other)
@@ -35,12 +35,12 @@ class HeadMusic::PitchClassSet
35
35
  def monochord?
36
36
  pitch_classes.length == 1
37
37
  end
38
- alias monad? monochord?
38
+ alias_method :monad?, :monochord?
39
39
 
40
40
  def dichord?
41
41
  pitch_classes.length == 2
42
42
  end
43
- alias dyad? dichord?
43
+ alias_method :dyad?, :dichord?
44
44
 
45
45
  def trichord?
46
46
  pitch_classes.length == 3
@@ -9,7 +9,7 @@ class HeadMusic::PitchSet
9
9
  seventh_chord: [3, 5, 7],
10
10
  ninth_chord: [2, 3, 5, 7],
11
11
  eleventh_chord: [2, 3, 4, 5, 7],
12
- thirteenth_chord: [2, 3, 4, 5, 6, 7], # a.k.a. diatonic scale
12
+ thirteenth_chord: [2, 3, 4, 5, 6, 7] # a.k.a. diatonic scale
13
13
  }.freeze
14
14
 
15
15
  attr_reader :pitches
@@ -54,20 +54,23 @@ class HeadMusic::PitchSet
54
54
  end
55
55
 
56
56
  def integer_notation
57
+ # questions:
58
+ # - should this be absolute? 0 = C?
59
+ # - should there be both absolute and relative versions?
57
60
  @integer_notation ||= begin
58
61
  return [] if pitches.empty?
59
- diatonic_intervals_above_bass_pitch.map { |interval| interval.semitones % 12 }.flatten.sort.unshift(0)
62
+ diatonic_intervals_above_bass_pitch.map { |interval| interval.semitones % 12 }.flatten.sort.unshift(0).uniq
60
63
  end
61
64
  end
62
65
 
63
66
  def invert
64
- inverted_pitch = pitches[0] + HeadMusic::DiatonicInterval.get('perfect octave')
67
+ inverted_pitch = pitches[0] + HeadMusic::DiatonicInterval.get("perfect octave")
65
68
  new_pitches = pitches.drop(1) + [inverted_pitch]
66
69
  HeadMusic::PitchSet.new(new_pitches)
67
70
  end
68
71
 
69
72
  def uninvert
70
- inverted_pitch = pitches[-1] - HeadMusic::DiatonicInterval.get('perfect octave')
73
+ inverted_pitch = pitches[-1] - HeadMusic::DiatonicInterval.get("perfect octave")
71
74
  new_pitches = [inverted_pitch] + pitches[0..-2]
72
75
  HeadMusic::PitchSet.new(new_pitches)
73
76
  end
@@ -77,11 +80,11 @@ class HeadMusic::PitchSet
77
80
  end
78
81
 
79
82
  def inspect
80
- pitches.map(&:to_s).join(' ')
83
+ pitches.map(&:to_s).join(" ")
81
84
  end
82
85
 
83
86
  def to_s
84
- pitches.map(&:to_s).join(' ')
87
+ pitches.map(&:to_s).join(" ")
85
88
  end
86
89
 
87
90
  def ==(other)
@@ -3,13 +3,13 @@
3
3
  # A quality is a categorization of an interval.
4
4
  class HeadMusic::Quality
5
5
  SHORTHAND = {
6
- perfect: 'P',
7
- major: 'M',
8
- minor: 'm',
9
- diminished: 'd',
10
- augmented: 'A',
11
- doubly_diminished: 'dd',
12
- doubly_augmented: 'AA',
6
+ perfect: "P",
7
+ major: "M",
8
+ minor: "m",
9
+ diminished: "d",
10
+ augmented: "A",
11
+ doubly_diminished: "dd",
12
+ doubly_augmented: "AA"
13
13
  }.freeze
14
14
  NAMES = SHORTHAND.keys
15
15
 
@@ -18,7 +18,7 @@ class HeadMusic::Quality
18
18
  -1 => :diminished,
19
19
  0 => :perfect,
20
20
  1 => :augmented,
21
- 2 => :doubly_augmented,
21
+ 2 => :doubly_augmented
22
22
  }.freeze
23
23
 
24
24
  MAJOR_INTERVAL_MODIFICATION = {
@@ -26,7 +26,7 @@ class HeadMusic::Quality
26
26
  -1 => :minor,
27
27
  0 => :major,
28
28
  1 => :augmented,
29
- 2 => :doubly_augmented,
29
+ 2 => :doubly_augmented
30
30
  }.freeze
31
31
 
32
32
  def self.get(identifier)
@@ -36,10 +36,11 @@ class HeadMusic::Quality
36
36
  end
37
37
 
38
38
  def self.from(starting_quality, delta)
39
- if starting_quality == :perfect
40
- PERFECT_INTERVAL_MODIFICATION[delta].to_s.gsub(/_+/, ' ')
41
- elsif starting_quality == :major
42
- MAJOR_INTERVAL_MODIFICATION[delta].to_s.gsub(/_+/, ' ')
39
+ case starting_quality
40
+ when :perfect
41
+ PERFECT_INTERVAL_MODIFICATION[delta].to_s.gsub(/_+/, " ")
42
+ when :major
43
+ MAJOR_INTERVAL_MODIFICATION[delta].to_s.gsub(/_+/, " ")
43
44
  end
44
45
  end
45
46
 
@@ -60,7 +61,7 @@ class HeadMusic::Quality
60
61
  end
61
62
 
62
63
  def article
63
- %w[a e i o u h].include?(name.to_s.first) ? 'an' : 'a'
64
+ %w[a e i o u h].include?(name.to_s.first) ? "an" : "a"
64
65
  end
65
66
 
66
67
  NAMES.each do |method_name|