head_music 0.23.4 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2560dc1ecfabf9f035a6c9349f6764582dd803edaad98cf6edf70b73ba67452b
4
- data.tar.gz: 8f48d2ac8336316478854efd66eb80e2a05d47b9a015cbbebf0b25a709518c3c
3
+ metadata.gz: 46aae73acea5e3331b19a08a02ded3c00fea5731e920207fc9d2b61dd9ebc359
4
+ data.tar.gz: c2cfdbb52641ad27bd96308fe99da5f59b2e66efdf2f40a4932e8bc6552510c4
5
5
  SHA512:
6
- metadata.gz: 125db0856552fee0160105993dd57c8566d96c904ba4df58d8292cd4becc9422638bb75b1da8c25e636c59f81ba0e4b518625e57c6cffacd336ee992918735b0
7
- data.tar.gz: 55b3a99f40051c9c4f3929e0a15930c4325441a3d18c43125daae0730e718eee0f3e6b3a0fbb851e01d916c040a499d6c577638716ea61298f458f3bc3037b80
6
+ metadata.gz: 23a9cc4013e6058892effeeaf940947a90ce2e5950eb4c3c622b7036fecbaa14dd4cd4aa5d5dbe5b6ffdefe1ce8e37681b1d8d040e135b1a4c737fca23c84fa6
7
+ data.tar.gz: 15d30c56ea0b08db8bde6b0d69b38a9605751b370086e6a66db5a74029b1f4c87ab408b0e567e092f0e2418d4f65e1dfe5644358c0fd0f6f676548466b5544ec
data/.rubocop.yml CHANGED
@@ -1,6 +1,9 @@
1
1
  Layout/DotPosition:
2
2
  EnforcedStyle: trailing
3
3
 
4
+ Layout/LineLength:
5
+ Max: 120
6
+
4
7
  Metrics/BlockLength:
5
8
  Exclude:
6
9
  - 'Gemfile'
@@ -10,9 +13,6 @@ Metrics/BlockLength:
10
13
  Metrics/ClassLength:
11
14
  Max: 155
12
15
 
13
- Metrics/LineLength:
14
- Max: 120
15
-
16
16
  Style/ClassAndModuleChildren:
17
17
  EnforcedStyle: compact
18
18
 
data/Rakefile CHANGED
@@ -9,5 +9,5 @@ task default: :spec
9
9
 
10
10
  desc 'Open an irb session preloaded with this library'
11
11
  task :console do
12
- sh 'irb -rubygems -I lib -r head_music.rb'
12
+ sh 'irb -I lib -r head_music.rb'
13
13
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # An interval is the distance between two pitches.
3
+ # A chromatic interval is the distance between two pitches measured in half-steps.
4
4
  class HeadMusic::ChromaticInterval
5
5
  include Comparable
6
6
 
@@ -40,6 +40,10 @@ class HeadMusic::ChromaticInterval
40
40
  semitones
41
41
  end
42
42
 
43
+ def diatonic_name
44
+ NAMES[simple.semitones].gsub(/_/, ' ')
45
+ end
46
+
43
47
  # diatonic set theory
44
48
  alias specific_interval semitones
45
49
 
@@ -5,39 +5,50 @@ require 'head_music/interval_cycle'
5
5
  # A Circle of Fifths or Fourths shows relationships between pitch classes
6
6
  class HeadMusic::Circle < HeadMusic::IntervalCycle
7
7
  def self.of_fifths
8
- get(7)
8
+ get(:perfect_fifth)
9
9
  end
10
10
 
11
11
  def self.of_fourths
12
- get(5)
12
+ get(:perfect_fourth)
13
13
  end
14
14
 
15
- def self.get(interval = 7)
15
+ def self.get(interval = :perfect_fifth)
16
16
  @circles ||= {}
17
- @circles[interval.to_i] ||= new(interval)
18
- end
19
-
20
- attr_reader :interval, :pitch_classes
21
-
22
- # Accepts an interval (as an integer number of semitones)
23
- def initialize(interval)
24
- @interval = interval.to_i
25
- @pitch_classes = pitch_classes_by_interval
17
+ diatonic_interval = HeadMusic::DiatonicInterval.get(interval)
18
+ @circles[interval] ||= new(interval: diatonic_interval, starting_pitch: 'C4')
26
19
  end
27
20
 
28
21
  def index(pitch_class)
29
- @pitch_classes.index(HeadMusic::Spelling.get(pitch_class).pitch_class)
22
+ pitch_classes.index(HeadMusic::Spelling.get(pitch_class).pitch_class)
30
23
  end
31
24
 
32
- private_class_method :new
25
+ alias spellings_up spellings
33
26
 
34
- private
27
+ def key_signatures_up
28
+ spellings_up.map { |spelling| HeadMusic::KeySignature.new(spelling) }
29
+ end
35
30
 
36
- def interval_cycle
37
- @interval_cycle ||= HeadMusic::IntervalCycle.get(interval)
31
+ def key_signatures_down
32
+ spellings_down.map { |spelling| HeadMusic::KeySignature.new(spelling) }
38
33
  end
39
34
 
40
- def pitch_classes_by_interval
41
- interval_cycle.send(:pitch_classes_by_interval)
35
+ def spellings_down
36
+ pitches_down.map(&:spelling)
42
37
  end
38
+
39
+ def pitches_down
40
+ @pitches_down ||= begin
41
+ [starting_pitch].tap do |list|
42
+ loop do
43
+ next_pitch = list.last - interval
44
+ next_pitch += octave while starting_pitch - next_pitch > 12
45
+ break if next_pitch.pitch_class == list.first.pitch_class
46
+
47
+ list << next_pitch
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ private_class_method :new
43
54
  end
@@ -280,6 +280,8 @@ class HeadMusic::DiatonicInterval
280
280
  to: :naming
281
281
  )
282
282
 
283
+ alias to_i semitones
284
+
283
285
  # Accepts a name and returns the interval with middle c on the bottom
284
286
  def self.get(identifier)
285
287
  name = Parser.new(identifier)
@@ -2,35 +2,56 @@
2
2
 
3
3
  # An Interval Cycle is a collection of pitch classes created from a sequence of the same interval class.
4
4
  class HeadMusic::IntervalCycle
5
+ attr_reader :interval, :starting_pitch
6
+
5
7
  def self.get(interval = 7)
6
- @circles ||= {}
8
+ @interval_cycles ||= {}
7
9
  interval = interval.to_s.gsub(/^C/i, '').to_i
8
- @circles[interval.to_i] ||= new(interval)
10
+ interval = HeadMusic::ChromaticInterval.get(interval)
11
+ @interval_cycles[interval.to_i] ||= new(interval: interval, starting_pitch: 'C4')
9
12
  end
10
13
 
11
- attr_reader :interval, :pitch_classes
14
+ def initialize(interval:, starting_pitch: 'C4')
15
+ @interval = interval if interval.is_a?(HeadMusic::DiatonicInterval)
16
+ @interval ||= interval if interval.is_a?(HeadMusic::ChromaticInterval)
17
+ @interval ||= HeadMusic::ChromaticInterval.get(interval) if interval.to_s.match?(/\d/)
18
+ @interval ||= HeadMusic::DiatonicInterval.get(interval)
19
+ @starting_pitch = HeadMusic::Pitch.get(starting_pitch)
20
+ end
12
21
 
13
- def initialize(interval)
14
- @interval = interval.to_i
15
- @pitch_classes = pitch_classes_by_interval
22
+ def pitches
23
+ @pitches ||= pitches_up
16
24
  end
17
25
 
18
- def index(pitch_class)
19
- @pitch_classes.index(HeadMusic::Spelling.get(pitch_class).pitch_class)
26
+ def pitch_classes
27
+ @pitch_classes ||= pitches.map(&:pitch_class)
20
28
  end
21
29
 
22
- private_class_method :new
30
+ def pitch_class_set
31
+ @pitch_class_set ||= HeadMusic::PitchClassSet.new(pitches)
32
+ end
23
33
 
24
- private
34
+ def spellings
35
+ @spellings ||= pitches.map(&:spelling)
36
+ end
25
37
 
26
- def pitch_classes_by_interval
27
- [HeadMusic::PitchClass.get(0)].tap do |list|
28
- loop do
29
- next_pitch_class = list.last + interval
30
- break if next_pitch_class == list.first
38
+ protected
31
39
 
32
- list << next_pitch_class
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
33
50
  end
34
51
  end
35
52
  end
53
+
54
+ def octave
55
+ @octave ||= HeadMusic::DiatonicInterval.get(:perfect_octave)
56
+ end
36
57
  end
@@ -6,8 +6,8 @@ class HeadMusic::KeySignature
6
6
  attr_reader :scale_type
7
7
  attr_reader :scale
8
8
 
9
- SHARPS = %w[F C G D A E B].freeze
10
- FLATS = %w[B♭ E♭ A♭ D♭ G♭ C♭ F♭].freeze
9
+ ORDERED_LETTER_NAMES_OF_SHARPS = %w[F C G D A E B].freeze
10
+ ORDERED_LETTER_NAMES_OF_FLATS = ORDERED_LETTER_NAMES_OF_SHARPS.reverse.freeze
11
11
 
12
12
  def self.default
13
13
  @default ||= new('C', :major)
@@ -39,19 +39,35 @@ class HeadMusic::KeySignature
39
39
  end
40
40
 
41
41
  def sharps
42
- spellings.select(&:sharp?).sort_by { |sharp| SHARPS.index(sharp.to_s) }
42
+ spellings.select(&:sharp?).sort_by do |spelling|
43
+ ORDERED_LETTER_NAMES_OF_SHARPS.index(spelling.letter_name.to_s)
44
+ end
45
+ end
46
+
47
+ def double_sharps
48
+ spellings.select(&:double_sharp?).sort_by do |spelling|
49
+ ORDERED_LETTER_NAMES_OF_SHARPS.index(spelling.letter_name.to_s)
50
+ end
43
51
  end
44
52
 
45
53
  def flats
46
- spellings.select(&:flat?).sort_by { |flat| FLATS.index(flat.to_s) }
54
+ spellings.select(&:flat?).sort_by do |spelling|
55
+ ORDERED_LETTER_NAMES_OF_FLATS.index(spelling.letter_name.to_s)
56
+ end
57
+ end
58
+
59
+ def double_flats
60
+ spellings.select(&:double_flat?).sort_by do |spelling|
61
+ ORDERED_LETTER_NAMES_OF_FLATS.index(spelling.letter_name.to_s)
62
+ end
47
63
  end
48
64
 
49
65
  def num_sharps
50
- sharps.length
66
+ sharps.length + double_sharps.length * 2
51
67
  end
52
68
 
53
69
  def num_flats
54
- flats.length
70
+ flats.length + double_flats.length * 2
55
71
  end
56
72
 
57
73
  def signs
@@ -25,5 +25,21 @@ module HeadMusic::Named
25
25
  key = HeadMusic::Utilities::HashKey.for(name)
26
26
  @instances_by_name[key] ||= new(name)
27
27
  end
28
+
29
+ def aliases
30
+ {}
31
+ end
32
+ end
33
+
34
+ # An Alias encapsulates an alternative name for a rudiment.
35
+ class Alias
36
+ attr_reader :key, :name, :abbreviation, :locale
37
+
38
+ def initialize(key:, name:, abbreviation: nil, locale: nil)
39
+ @key = key
40
+ @name = name
41
+ @abbreviation = abbreviation
42
+ @locale = locale
43
+ end
28
44
  end
29
45
  end
@@ -21,8 +21,17 @@ class HeadMusic::Pitch
21
21
  delegate :enharmonic_equivalent?, :enharmonic?, to: :enharmonic_equivalence
22
22
  delegate :octave_equivalent?, to: :octave_equivalence
23
23
 
24
+ # Fetches a pitch identified by the information passed in.
25
+ #
26
+ # Accepts:
27
+ # - a Pitch instance
28
+ # - a PitchClass instance
29
+ # - a name string, such as 'Ab4'
30
+ # - a number corresponding to the midi note number
24
31
  def self.get(value)
25
- from_pitch_class(value) || from_name(value) || from_number(value)
32
+ from_pitch_class(value) ||
33
+ from_name(value) ||
34
+ from_number(value)
26
35
  end
27
36
 
28
37
  def self.middle_c
@@ -48,9 +57,7 @@ class HeadMusic::Pitch
48
57
  def self.from_number(number)
49
58
  return nil unless number == number.to_i
50
59
 
51
- spelling = HeadMusic::Spelling.from_number(number)
52
- octave = (number.to_i / 12) - 1
53
- fetch_or_create(spelling, octave)
60
+ fetch_or_create(HeadMusic::Spelling.from_number(number), (number.to_i / 12) - 1)
54
61
  end
55
62
 
56
63
  def self.from_number_and_letter(number, letter_name)
@@ -7,8 +7,8 @@ class HeadMusic::PitchClass
7
7
  attr_reader :number
8
8
  attr_reader :spelling
9
9
 
10
- SHARP_SPELLINGS = %w[C C# D D# E F F# G G# A A# B].freeze
11
- FLAT_SPELLINGS = %w[C Db D Eb E F Gb G Ab A Bb B].freeze
10
+ SHARP_SPELLINGS = %w[C C D D E F F G G A A B].freeze
11
+ FLAT_SPELLINGS = %w[C D♭ D E♭ E F G♭ G A♭ A B♭ B].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)
@@ -24,29 +24,30 @@ class HeadMusic::ReferencePitch
24
24
  { name: 'Chorton', pitch: 'A4', frequency: 466.0 },
25
25
  ].freeze
26
26
 
27
- ALIASES = {
28
- kammerton: :baroque,
29
- chamber: :baroque,
30
- haydn: :classical,
31
- mozart: :classical,
32
- philosophic: :scientific,
33
- sauveur: :scientific,
34
- schiller: :scientific,
35
- continental: :french,
36
- international: :french,
37
- low: :new_philharmonic,
38
- concert: :a440,
39
- stuttgart: :a440,
40
- scheibler: :a440,
41
- iso_16: :a440,
42
- high: :old_philharmonic,
43
- choir: :chorton,
44
- }.freeze
45
-
46
- NAMED_REFERENCE_PITCH_NAMES = NAMED_REFERENCE_PITCHES.map { |pitch_data| pitch_data[:name] }
27
+ ALIAS_DATA = [
28
+ { key: :baroque, name: 'Kammerton' },
29
+ { key: :classical, name: 'Haydn' },
30
+ { key: :classical, name: 'Mozart' },
31
+ { key: :scientific, name: 'philosophical' },
32
+ { key: :scientific, name: 'Sauveur' },
33
+ { key: :scientific, name: 'Schiller' },
34
+ { key: :french, name: 'continental' },
35
+ { key: :french, name: 'international' },
36
+ { key: :new_philharmonic, name: 'low' },
37
+ { key: :old_philharmonic, name: 'high' },
38
+ { key: :a440, name: 'concert' },
39
+ { key: :a440, name: 'Stuttgart' },
40
+ { key: :a440, name: 'Scheibler' },
41
+ { key: :a440, name: 'ISO 16' },
42
+ { key: :chorton, name: 'choir' },
43
+ ].freeze
47
44
 
48
45
  attr_reader :pitch, :frequency
49
46
 
47
+ def self.aliases
48
+ @aliases ||= ALIAS_DATA.map { |attributes| HeadMusic::Named::Alias.new(attributes) }
49
+ end
50
+
50
51
  def self.get(name)
51
52
  return name if name.is_a?(self)
52
53
  get_by_name(name)
@@ -56,7 +57,7 @@ class HeadMusic::ReferencePitch
56
57
  @name = name.to_s
57
58
  reference_pitch_data = NAMED_REFERENCE_PITCHES.detect do |candidate|
58
59
  candidate_name_key = HeadMusic::Utilities::HashKey.for(candidate[:name])
59
- [candidate_name_key, candidate_name_key.to_s.delete('_').to_sym].include?(normalized_name)
60
+ [candidate_name_key, candidate_name_key.to_s.delete('_').to_sym].include?(normalized_key)
60
61
  end || {}
61
62
  @pitch = HeadMusic::Pitch.get(reference_pitch_data.fetch(:pitch, DEFAULT_PITCH_NAME))
62
63
  @frequency = reference_pitch_data.fetch(:frequency, DEFAULT_FREQUENCY)
@@ -78,10 +79,12 @@ class HeadMusic::ReferencePitch
78
79
 
79
80
  private
80
81
 
81
- def normalized_name
82
- @normalized_name ||= begin
82
+ def normalized_key
83
+ @normalized_key ||= begin
83
84
  key = HeadMusic::Utilities::HashKey.for(name.to_s.gsub(/\W?(pitch|tuning|tone)/, ''))
84
- ALIASES[key] || key
85
+ HeadMusic::ReferencePitch.aliases.detect do |alias_data|
86
+ HeadMusic::Utilities::HashKey.for(alias_data.name) == key
87
+ end&.key || key
85
88
  end
86
89
  end
87
90
  end
@@ -8,14 +8,20 @@ class HeadMusic::Sign
8
8
 
9
9
  attr_reader :identifier, :cents, :musical_symbol
10
10
 
11
+ delegate :ascii, :unicode, :html_entity, to: :musical_symbol
12
+
13
+ SIGN_DATA = [
14
+ { identifier: :sharp, ascii: '#', unicode: '♯', html_entity: '&#9839;', cents: 100 },
15
+ { identifier: :flat, ascii: 'b', unicode: '♭', html_entity: '&#9837;', cents: -100 },
16
+ { identifier: :natural, ascii: '', unicode: '♮', html_entity: '&#9838;', cents: 0 },
17
+ { identifier: :double_sharp, ascii: 'x', unicode: '𝄪', html_entity: '&#119082;', cents: 200 },
18
+ { identifier: :double_flat, ascii: 'bb', unicode: '𝄫', html_entity: '&#119083;', cents: -200 },
19
+ ].freeze
20
+
21
+ SIGN_IDENTIFIERS = SIGN_DATA.map { |attributes| attributes[:identifier] }.freeze
22
+
11
23
  def self.all
12
- @all ||= [
13
- new(identifier: :sharp, ascii: '#', unicode: '♯', html_entity: '&#9839;', cents: 100),
14
- new(identifier: :flat, ascii: 'b', unicode: '♭', html_entity: '&#9837;', cents: -100),
15
- new(identifier: :natural, ascii: '', unicode: '♮', html_entity: '&#9838;', cents: 0),
16
- new(identifier: :double_sharp, ascii: '##', unicode: '𝄪', html_entity: '&#119082;', cents: 200),
17
- new(identifier: :double_flat, ascii: 'bb', unicode: '𝄫', html_entity: '&#119083;', cents: -200),
18
- ]
24
+ SIGN_DATA.map { |attributes| new(attributes) }
19
25
  end
20
26
 
21
27
  def self.symbols
@@ -57,6 +63,10 @@ class HeadMusic::Sign
57
63
  cents / 100.0
58
64
  end
59
65
 
66
+ SIGN_IDENTIFIERS.each do |key|
67
+ define_method(:"#{key}?") { identifier == key }
68
+ end
69
+
60
70
  def to_s
61
71
  unicode
62
72
  end
@@ -66,14 +76,11 @@ class HeadMusic::Sign
66
76
  cents <=> other.cents
67
77
  end
68
78
 
69
- delegate :ascii, :html_entity, :unicode, to: :musical_symbol
70
-
71
79
  private
72
80
 
73
81
  def initialize(attributes)
74
82
  @identifier = attributes[:identifier]
75
83
  @cents = attributes[:cents]
76
-
77
84
  @musical_symbol = HeadMusic::MusicalSymbol.new(
78
85
  unicode: attributes[:unicode],
79
86
  ascii: attributes[:ascii],
@@ -14,6 +14,7 @@ class HeadMusic::Spelling
14
14
  delegate :to_i, to: :pitch_class_number
15
15
  delegate :series_ascending, :series_descending, to: :letter_name, prefix: true
16
16
  delegate :enharmonic?, to: :enharmonic_equivalence
17
+ delegate :sharp?, :flat?, :double_sharp?, :double_flat?, to: :sign, allow_nil: true
17
18
 
18
19
  def self.get(identifier)
19
20
  return identifier if identifier.is_a?(HeadMusic::Spelling)
@@ -73,14 +74,6 @@ class HeadMusic::Spelling
73
74
  name
74
75
  end
75
76
 
76
- def sharp?
77
- sign && sign == '#'
78
- end
79
-
80
- def flat?
81
- sign && sign == 'b'
82
- end
83
-
84
77
  def ==(other)
85
78
  other = HeadMusic::Spelling.get(other)
86
79
  to_s == other.to_s
@@ -90,6 +83,10 @@ class HeadMusic::Spelling
90
83
  HeadMusic::Scale.get(self, scale_type_name)
91
84
  end
92
85
 
86
+ def natural?
87
+ !sign || sign.natural?
88
+ end
89
+
93
90
  private_class_method :new
94
91
 
95
92
  private
@@ -12,12 +12,9 @@ class HeadMusic::Tuning
12
12
  end
13
13
 
14
14
  def frequency_for(pitch)
15
- pitch = HeadMusic::Pitch.get(pitch) unless pitch.is_a?(HeadMusic::Pitch)
15
+ pitch = HeadMusic::Pitch.get(pitch)
16
16
  reference_pitch_frequency * (2**(1.0 / 12))**(pitch - reference_pitch.pitch).semitones
17
17
  end
18
18
  end
19
19
 
20
20
  # TODO: other tunings
21
- # Create website that hosts videos on theory and history, handy charts, etc.
22
- # one of those charts can be a frequency table in various tunings
23
- # maybe show pythagorean commas and such. or cents sharp or flat relative to either equal temperment or just intonation
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HeadMusic
4
- VERSION = '0.23.4'
4
+ VERSION = '0.24.0'
5
5
  end
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.23.4
4
+ version: 0.24.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: 2020-03-26 00:00:00.000000000 Z
11
+ date: 2020-03-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport