head_music 0.1.1 → 0.1.5

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
  SHA1:
3
- metadata.gz: 161df8f1795f4e70adcdc8d7298cdbf1dd2af984
4
- data.tar.gz: 621aac81ec102840a879186df55eb9aeed040b8b
3
+ metadata.gz: 8379f9a907ea8dd73018b1cd9a8da6a055510735
4
+ data.tar.gz: f3de473d170a910e26a5b4318232a5a9d7eb4258
5
5
  SHA512:
6
- metadata.gz: 947631876b0c2c74a807515783a22783836838d75d2a21eab2f66e8863a5b25d5849e92874885007a99dd0596acacb5c795ad0ffc153d47cac616322ec566fef
7
- data.tar.gz: 9ff5a2e01209ccd7f515d9d152f49159a0f18518f7bfb17e289609cb519395b96e7ae4399dd321346b3b971f05ae970d2095fb4556ca1bb666b36d268f46fe2c
6
+ metadata.gz: 05c161185607fd4f4ac48b5ad9389ee6d15fda24deaed1bf1a8f4e88b7140615ef60cdf695266c6e6a714cf66dc9db3a5f47e056c7c6096a9c0fde6e0d5fb0fa
7
+ data.tar.gz: f7142b9a29669c3a885c6ee9edf68330c9912ff8e71622efb1d95f7c3f314dea05dc2443c5f1c092988ceb236ba7a7768ac28a0ba43651c5954085595e57b295
@@ -10,15 +10,15 @@ class HeadMusic::Accidental
10
10
 
11
11
  def self.get(identifier)
12
12
  @accidentals ||= {}
13
- @accidentals[identifier] ||= for_symbol(identifier) || for_interval(identifier)
13
+ for_symbol(identifier) || for_interval(identifier)
14
14
  end
15
15
 
16
16
  def self.for_symbol(identifier)
17
- new(identifier) if ACCIDENTAL_SEMITONES.keys.include?(identifier)
17
+ @accidentals[identifier.to_s] ||= new(identifier.to_s) if ACCIDENTAL_SEMITONES.keys.include?(identifier.to_s)
18
18
  end
19
19
 
20
20
  def self.for_interval(semitones)
21
- ACCIDENTAL_SEMITONES.key(semitones.to_i)
21
+ @accidentals[semitones.to_i] ||= ACCIDENTAL_SEMITONES.key(semitones.to_i)
22
22
  end
23
23
 
24
24
  def initialize(string)
@@ -9,7 +9,7 @@ class HeadMusic::Interval
9
9
 
10
10
  def self.get(semitones)
11
11
  @intervals_memo ||= {}
12
- @intervals_memo[semitones] ||= new(semitones)
12
+ @intervals_memo[semitones.to_i] ||= new(semitones.to_i)
13
13
  end
14
14
 
15
15
  def self.named(name)
@@ -7,9 +7,9 @@ class HeadMusic::KeySignature
7
7
 
8
8
  delegate :pitch_class, to: :tonic_spelling, prefix: :tonic
9
9
 
10
- def initialize(tonic_spelling, scale_type = :major)
10
+ def initialize(tonic_spelling, scale_type = nil)
11
11
  @tonic_spelling = tonic_spelling
12
- @scale_type = scale_type
12
+ @scale_type = scale_type || :major
13
13
  end
14
14
 
15
15
  def sharps
@@ -19,12 +19,12 @@ class HeadMusic::Letter
19
19
 
20
20
  def self.get(identifier)
21
21
  @letters ||= {}
22
- @letters[identifier] ||= from_name(identifier) || from_pitch_class(identifier)
22
+ from_name(identifier) || from_pitch_class(identifier)
23
23
  end
24
24
 
25
25
  def self.from_name(name)
26
26
  name = name.to_s.first.upcase
27
- new(name) if NAMES.include?(name)
27
+ @letters[name] ||= new(name) if NAMES.include?(name)
28
28
  end
29
29
 
30
30
  def self.from_pitch_class(pitch_class)
@@ -33,7 +33,7 @@ class HeadMusic::Letter
33
33
  pitch_class = pitch_class.to_i % 12
34
34
  name = NAMES.detect { |name| pitch_class == NATURAL_PITCH_CLASS_NUMBERS[name] }
35
35
  name ||= HeadMusic::PitchClass::PREFERRED_SPELLINGS[pitch_class].first
36
- return new(name) if NAMES.include?(name)
36
+ @letters[name] ||= new(name) if NAMES.include?(name)
37
37
  end
38
38
 
39
39
  attr_reader :name
@@ -0,0 +1,42 @@
1
+ class HeadMusic::Octave
2
+ include Comparable
3
+
4
+ DEFAULT = 4
5
+
6
+ MATCHER = /(-?\d+)$/
7
+
8
+ def self.get(identifier)
9
+ @octaves ||= {}
10
+ from_number(identifier) || from_name(identifier) || default
11
+ end
12
+
13
+ def self.from_number(identifier)
14
+ return nil unless identifier.to_s == identifier.to_i.to_s
15
+ return nil unless (-2..12).include?(identifier.to_i)
16
+ @octaves[identifier.to_i] ||= new(identifier.to_i)
17
+ end
18
+
19
+ def self.from_name(string)
20
+ if string.to_s.match(HeadMusic::Spelling::MATCHER)
21
+ _letter, _accidental, octave_string = string.to_s.match(HeadMusic::Spelling::MATCHER).captures
22
+ @octaves[octave_string.to_i] ||= new(octave_string.to_i) if octave_string
23
+ end
24
+ end
25
+
26
+ def self.default
27
+ @octaves[DEFAULT] ||= new(DEFAULT)
28
+ end
29
+
30
+ attr_reader :number
31
+ delegate :to_i, :to_s, to: :number
32
+
33
+ def initialize(number)
34
+ @number ||= number
35
+ end
36
+
37
+ def <=>(other)
38
+ self.to_i <=> other.to_i
39
+ end
40
+
41
+ private_class_method :new
42
+ end
@@ -13,16 +13,21 @@ class HeadMusic::Pitch
13
13
 
14
14
  def self.from_name(name)
15
15
  return nil unless name == name.to_s
16
- spelling = HeadMusic::Spelling.get(name)
17
- octave = name.scan(/-?\d+$/).first.to_i
18
- new(spelling, octave) if spelling && (-1..9).include?(octave)
16
+ fetch_or_create(HeadMusic::Spelling.get(name), HeadMusic::Octave.get(name).to_i)
19
17
  end
20
18
 
21
19
  def self.from_number(number)
22
20
  return nil unless number == number.to_i
23
21
  spelling = HeadMusic::Spelling.from_number(number)
24
22
  octave = (number.to_i / 12) - 1
25
- new(spelling, octave) if spelling && (-1..9).include?(octave)
23
+ fetch_or_create(spelling, octave)
24
+ end
25
+
26
+ def self.fetch_or_create(spelling, octave)
27
+ if spelling && (-1..9).include?(octave)
28
+ key = [spelling, octave].join
29
+ @pitches[key] ||= new(spelling, octave)
30
+ end
26
31
  end
27
32
 
28
33
  def initialize(spelling, octave)
@@ -38,6 +43,8 @@ class HeadMusic::Pitch
38
43
  (octave + 1) * 12 + pitch_class.to_i
39
44
  end
40
45
 
46
+ alias_method :midi, :midi_note_number
47
+
41
48
  def to_s
42
49
  name
43
50
  end
@@ -3,9 +3,10 @@ class HeadMusic::PitchClass
3
3
 
4
4
  PREFERRED_SPELLINGS = %w[C C# D Eb E F F# G Ab A Bb B]
5
5
 
6
- def self.get(number)
6
+ def self.get(identifier)
7
7
  @pitch_classes ||= {}
8
- number = number.to_i % 12
8
+ number = Spelling.get(identifier).pitch_class.to_i if Spelling.match(identifier)
9
+ number ||= identifier.to_i % 12
9
10
  @pitch_classes[number] ||= new(number)
10
11
  end
11
12
 
@@ -14,7 +15,7 @@ class HeadMusic::PitchClass
14
15
  end
15
16
 
16
17
  def initialize(pitch_class_or_midi_number)
17
- @number = pitch_class_or_midi_number % 12
18
+ @number = pitch_class_or_midi_number.to_i % 12
18
19
  end
19
20
 
20
21
  def to_i
@@ -0,0 +1,22 @@
1
+ class HeadMusic::Quality
2
+ QUALITY_NAMES = %w[perfect major minor diminished augmented].map(&:to_sym)
3
+
4
+ def self.get(identifier)
5
+ @qualities ||= {}
6
+ identifier = identifier.to_s.to_sym
7
+ @qualities[identifier] ||= new(identifier) if QUALITY_NAMES.include?(identifier)
8
+ end
9
+
10
+ attr_reader :name
11
+ delegate :to_s, to: :name
12
+
13
+ def initialize(name)
14
+ @name = name
15
+ end
16
+
17
+ def ==(other)
18
+ self.to_s == other.to_s
19
+ end
20
+
21
+ private_class_method :new
22
+ end
@@ -0,0 +1,83 @@
1
+ class HeadMusic::ScaleType
2
+ H = 1 # whole step
3
+ W = 2 # half step
4
+ WH = W + H # augmented second
5
+
6
+ # Modal
7
+ I = [W, W, H, W, W, W, H]
8
+ II = I.rotate
9
+ III = I.rotate(2)
10
+ IV = I.rotate(3)
11
+ V = I.rotate(4)
12
+ VI = I.rotate(5)
13
+ VII = I.rotate(6)
14
+
15
+ # Tonal
16
+ HARMONIC_MINOR = [W, H, W, W, H, WH, H]
17
+ MELODIC_MINOR_ASCENDING = [W, H, W, W, W, W, H]
18
+
19
+ CHROMATIC = [H, H, H, H, H, H, H, H, H, H, H, H]
20
+ MINOR_PENTATONIC = [3, 2, 2, 3, 2]
21
+ MAJOR_PENTATONIC = MINOR_PENTATONIC.rotate
22
+
23
+ MODE_NAMES = {
24
+ i: [:ionian, :major, :maj],
25
+ ii: [:dorian],
26
+ iii: [:phrygian],
27
+ iv: [:lydian],
28
+ v: [:mixolydian],
29
+ vi: [:aeolian, :minor, :natural_minor, :min],
30
+ vii: [:locrian],
31
+ }
32
+ SCALE_TYPES = {}
33
+ MODE_NAMES.each do |roman_numeral, aliases|
34
+ intervals = { ascending: const_get(roman_numeral.upcase) }
35
+ SCALE_TYPES[roman_numeral] = intervals
36
+ aliases.each do |name|
37
+ SCALE_TYPES[name.to_sym] = intervals
38
+ end
39
+ end
40
+ SCALE_TYPES[:harmonic_minor] = { ascending: HARMONIC_MINOR }
41
+ SCALE_TYPES[:melodic_minor] = { ascending: MELODIC_MINOR_ASCENDING, descending: VI.reverse }
42
+
43
+ SCALE_TYPES[:chromatic] = { ascending: CHROMATIC }
44
+
45
+ SCALE_TYPES[:minor_pentatonic] = { ascending: MINOR_PENTATONIC }
46
+ SCALE_TYPES[:major_pentatonic] = { ascending: MAJOR_PENTATONIC }
47
+
48
+ SCALE_TYPES[:octatonic] = { ascending: [W, H, W, H, W, H, W, H] }
49
+ SCALE_TYPES[:whole_tone] = { ascending: [W, W, W, W, W, W] }
50
+ SCALE_TYPES[:monotonic] = { ascending: [12] }
51
+
52
+ class << self
53
+ SCALE_TYPES.keys.each do |name|
54
+ define_method(name) do
55
+ self.get(name)
56
+ end
57
+ end
58
+ end
59
+
60
+ def self.get(name)
61
+ @scale_types ||= {}
62
+ name = name.to_s.to_sym
63
+ intervals = SCALE_TYPES[name]
64
+ @scale_types[name] ||= new(name, intervals[:ascending], intervals[:descending])
65
+ end
66
+
67
+ attr_reader :name, :ascending_intervals, :descending_intervals
68
+ delegate :to_s, to: :name
69
+
70
+ def initialize(name, ascending_intervals, descending_intervals = nil)
71
+ @name = name
72
+ @ascending_intervals = ascending_intervals
73
+ @descending_intervals = descending_intervals || ascending_intervals.reverse
74
+ end
75
+
76
+ def ==(other)
77
+ self.state == other.state
78
+ end
79
+
80
+ def state
81
+ [ascending_intervals, descending_intervals]
82
+ end
83
+ end
@@ -1,24 +1,30 @@
1
- # A Spelling is a pitch class with a letter and an accidental
1
+ # A Spelling is a pitch class with a letter and possibly an accidental
2
2
 
3
3
  class HeadMusic::Spelling
4
+ MATCHER = /^\s*([A-G])([b#]*)(\-?\d+)?\s*$/
5
+
6
+ attr_reader :pitch_class
4
7
  attr_reader :letter
5
8
  attr_reader :accidental
6
- attr_reader :pitch_class
7
9
 
8
- SPELLING_MATCHER = /^\s*([A-G])([b#]*)(\-?\d+)?\s*$/
10
+ delegate :number, to: :pitch_class, prefix: true
9
11
 
10
12
  def self.get(identifier)
11
13
  @spellings ||= {}
12
- @spellings[identifier] ||= from_name(identifier) || from_number(identifier)
14
+ from_name(identifier) || from_number(identifier)
15
+ end
16
+
17
+ def self.match(string)
18
+ string.to_s.match(MATCHER)
13
19
  end
14
20
 
15
21
  def self.from_name(name)
16
- return nil unless name == name.to_s
17
- match = name.to_s.match(SPELLING_MATCHER)
18
- if match
19
- letter_name, accidental_string, _octave = match.captures
22
+ if match(name)
23
+ letter_name, accidental_string, _octave = match(name).captures
20
24
  letter = HeadMusic::Letter.get(letter_name)
21
- new(letter, HeadMusic::Accidental.get(accidental_string)) if letter
25
+ return nil unless letter
26
+ accidental = HeadMusic::Accidental.get(accidental_string)
27
+ fetch_or_create(letter, accidental)
22
28
  end
23
29
  end
24
30
 
@@ -26,10 +32,13 @@ class HeadMusic::Spelling
26
32
  return nil unless number == number.to_i
27
33
  pitch_class_number = number.to_i % 12
28
34
  letter = HeadMusic::Letter.from_pitch_class(pitch_class_number)
29
- if letter.pitch_class != pitch_class_number
30
- accidental = HeadMusic::Accidental.for_interval(pitch_class_number - letter.pitch_class.to_i)
31
- end
32
- new(letter, accidental)
35
+ accidental = HeadMusic::Accidental.for_interval(pitch_class_number - letter.pitch_class.to_i)
36
+ fetch_or_create(letter, accidental)
37
+ end
38
+
39
+ def self.fetch_or_create(letter, accidental)
40
+ key = [letter, accidental].join
41
+ @spellings[key] ||= new(letter, accidental)
33
42
  end
34
43
 
35
44
  def initialize(letter, accidental = nil)
@@ -1,3 +1,3 @@
1
1
  module HeadMusic
2
- VERSION = "0.1.1"
2
+ VERSION = "0.1.5"
3
3
  end
data/lib/head_music.rb CHANGED
@@ -8,9 +8,12 @@ require "head_music/circle"
8
8
  require "head_music/interval"
9
9
  require "head_music/key_signature"
10
10
  require "head_music/letter"
11
+ require "head_music/octave"
11
12
  require "head_music/pitch_class"
12
13
  require "head_music/pitch"
14
+ require "head_music/quality"
13
15
  require "head_music/scale"
16
+ require "head_music/scale_type"
14
17
  require "head_music/spelling"
15
18
 
16
19
  module HeadMusic
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.1.1
4
+ version: 0.1.5
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-02-17 00:00:00.000000000 Z
11
+ date: 2017-02-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -105,9 +105,12 @@ files:
105
105
  - lib/head_music/interval.rb
106
106
  - lib/head_music/key_signature.rb
107
107
  - lib/head_music/letter.rb
108
+ - lib/head_music/octave.rb
108
109
  - lib/head_music/pitch.rb
109
110
  - lib/head_music/pitch_class.rb
111
+ - lib/head_music/quality.rb
110
112
  - lib/head_music/scale.rb
113
+ - lib/head_music/scale_type.rb
111
114
  - lib/head_music/spelling.rb
112
115
  - lib/head_music/version.rb
113
116
  homepage: https://github.com/roberthead/head_music