head_music 0.23.4 → 0.24.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
  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