head_music 0.6.4 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -2,3 +2,8 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in head_music.gemspec
4
4
  gemspec
5
+
6
+ group :test do
7
+ gem "simplecov"
8
+ gem "codeclimate-test-reporter", "~> 1.0.0"
9
+ end
data/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # HeadMusic
2
2
 
3
+ ![Build status](https://circleci.com/gh/roberthead/head_music.svg?style=shield&circle-token=8b39a6f5e809e9baa321e0f13aa06c70c6511794)
4
+ [![Code Climate](https://codeclimate.com/github/roberthead/head_music/badges/gpa.svg)](https://codeclimate.com/github/roberthead/head_music)
5
+ [![Test Coverage](https://codeclimate.com/github/roberthead/head_music/badges/coverage.svg)](https://codeclimate.com/github/roberthead/head_music/coverage)
6
+
3
7
  The *head_music* ruby gem models the elements of western music theory, such as note names, scales, key signatures, intervals, and chords.
4
8
 
5
9
  ## Installation
data/circle.yml CHANGED
@@ -6,4 +6,4 @@ machine:
6
6
  # Version of ruby to use
7
7
  ruby:
8
8
  version:
9
- 2.3.0
9
+ 2.3.1
data/head_music.gemspec CHANGED
@@ -21,7 +21,7 @@ Gem::Specification.new do |spec|
21
21
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
22
  spec.require_paths = ["lib"]
23
23
 
24
- spec.required_ruby_version = '~> 2.3'
24
+ spec.required_ruby_version = '~> 2.3.1'
25
25
 
26
26
  spec.add_runtime_dependency "activesupport", "~> 5.0"
27
27
  spec.add_runtime_dependency "humanize", "~> 1.3"
@@ -0,0 +1,29 @@
1
+ class HeadMusic::Composition
2
+ attr_reader :name, :key_signature, :meter, :measures
3
+
4
+ def initialize(name:, key_signature: nil, meter: nil)
5
+ ensure_attributes(name, key_signature, meter)
6
+ add_measure
7
+ end
8
+
9
+ def add_measure
10
+ add_measures(1)
11
+ end
12
+
13
+ def add_measures(number)
14
+ @measures ||= []
15
+ number.times do
16
+ @measures << HeadMusic::Measure.new(self)
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def ensure_attributes(name, key_signature, meter)
23
+ @name = name
24
+ @key_signature = HeadMusic::KeySignature.get(key_signature) if key_signature
25
+ @key_signature ||= HeadMusic::KeySignature.default
26
+ @meter = HeadMusic::Meter.get(meter) if meter
27
+ @meter ||= HeadMusic::Meter.default
28
+ end
29
+ end
@@ -1,15 +1,28 @@
1
1
  class HeadMusic::KeySignature
2
2
  attr_reader :tonic_spelling
3
- attr_reader :scale_type
3
+ attr_reader :quality_name
4
4
 
5
5
  SHARPS = %w{F# C# G# D# A# E# B#}
6
6
  FLATS = %w{Bb Eb Ab Db Gb Cb Fb}
7
7
 
8
+ def self.default
9
+ @default ||= new('C', :major)
10
+ end
11
+
12
+ def self.get(identifier)
13
+ return identifier if identifier.is_a?(HeadMusic::KeySignature)
14
+ @key_signatures ||= {}
15
+ tonic_spelling, quality_name = identifier.split(/\s/)
16
+ hash_key = HeadMusic::Utilities::HashKey.for(identifier)
17
+ @key_signatures[hash_key] ||= new(tonic_spelling, quality_name)
18
+ end
19
+
8
20
  delegate :pitch_class, to: :tonic_spelling, prefix: :tonic
21
+ delegate :to_s, to: :name
9
22
 
10
- def initialize(tonic_spelling, scale_type = nil)
23
+ def initialize(tonic_spelling, quality_name = nil)
11
24
  @tonic_spelling = tonic_spelling
12
- @scale_type = scale_type || :major
25
+ @quality_name = quality_name || :major
13
26
  end
14
27
 
15
28
  def sharps
@@ -21,11 +34,11 @@ class HeadMusic::KeySignature
21
34
  end
22
35
 
23
36
  def num_sharps
24
- (HeadMusic::Circle.of_fifths.index(tonic_pitch_class) - scale_type_adjustment) % 12
37
+ (HeadMusic::Circle.of_fifths.index(tonic_pitch_class) - quality_name_adjustment) % 12
25
38
  end
26
39
 
27
40
  def num_flats
28
- (HeadMusic::Circle.of_fourths.index(tonic_pitch_class) + scale_type_adjustment) % 12
41
+ (HeadMusic::Circle.of_fourths.index(tonic_pitch_class) + quality_name_adjustment) % 12
29
42
  end
30
43
 
31
44
  def sharps_or_flats
@@ -34,18 +47,26 @@ class HeadMusic::KeySignature
34
47
  num_sharps <= num_flats ? sharps : flats
35
48
  end
36
49
 
50
+ def name
51
+ [tonic_spelling.to_s, quality_name.to_s].join(' ')
52
+ end
53
+
54
+ def ==(other)
55
+ self.to_s == other.to_s
56
+ end
57
+
37
58
  private
38
59
 
39
- def scale_type_adjustment
40
- scale_type == :minor ? 3 : 0
60
+ def quality_name_adjustment
61
+ quality_name == :minor ? 3 : 0
41
62
  end
42
63
 
43
64
  def major?
44
- @scale_type.to_sym == :major
65
+ @quality_name.to_sym == :major
45
66
  end
46
67
 
47
68
  def minor?
48
- @scale_type.to_sym == :minor
69
+ @quality_name.to_sym == :minor
49
70
  end
50
71
 
51
72
  def relative_major_pitch_class
@@ -0,0 +1,14 @@
1
+ class HeadMusic::Measure
2
+ attr_reader :composition
3
+
4
+ delegate :key_signature, :meter, to: :composition
5
+
6
+ def initialize(composition)
7
+ @composition = composition
8
+ end
9
+
10
+ # TODO: encapsulate key changes and meter changes
11
+ # Assume the key and meter of the previous measure
12
+ # all the way back to the first measure,
13
+ # which defaults to the key and meter of the composition
14
+ end
@@ -8,8 +8,22 @@ class HeadMusic::Meter
8
8
 
9
9
  def self.get(identifier)
10
10
  identifer = identifer.to_s
11
- identifier = NAMED[identifier.to_sym] || identifier
12
- new(*identifier.split('/').map(&:to_i))
11
+ hash_key = HeadMusic::Utilities::HashKey.for(identifier)
12
+ time_signature_string = NAMED[hash_key] || identifier
13
+ @meters ||= {}
14
+ @meters[hash_key] ||= new(*time_signature_string.split('/').map(&:to_i))
15
+ end
16
+
17
+ def self.default
18
+ get('4/4')
19
+ end
20
+
21
+ def self.common_time
22
+ get(:common_time)
23
+ end
24
+
25
+ def self.cut_time
26
+ get(:cut_time)
13
27
  end
14
28
 
15
29
  def initialize(top_number, bottom_number)
@@ -40,6 +54,43 @@ class HeadMusic::Meter
40
54
  compound? ? top_number / 3 : top_number
41
55
  end
42
56
 
57
+ def counts_per_measure
58
+ top_number
59
+ end
60
+
61
+ def beat_strength(count, tick: 0)
62
+ return 100 if count == 1 && tick == 0
63
+ return 80 if strong_counts.include?(count) && tick == 0
64
+ return 60 if tick == 0
65
+ return 40 if strong_ticks.include?(tick)
66
+ 20
67
+ end
68
+
69
+ def ticks_per_count
70
+ @ticks_per_count ||= count_unit.ticks
71
+ end
72
+
73
+ def strong_ticks
74
+ @strong_ticks ||=
75
+ [2,3,4].map do |sixths|
76
+ ticks_per_count * (sixths / 6.0)
77
+ end
78
+ end
79
+
80
+ def count_unit
81
+ HeadMusic::RhythmicUnit.for_denominator_value(bottom_number)
82
+ end
83
+
84
+ def beat_unit
85
+ @beat_unit ||=
86
+ if compound?
87
+ unit = HeadMusic::RhythmicUnit.for_denominator_value(bottom_number / 2)
88
+ HeadMusic::RhythmicValue.new(unit, dots: 1)
89
+ else
90
+ HeadMusic::RhythmicValue.new(count_unit)
91
+ end
92
+ end
93
+
43
94
  def to_s
44
95
  [top_number, bottom_number].join('/')
45
96
  end
@@ -47,4 +98,18 @@ class HeadMusic::Meter
47
98
  def ==(other)
48
99
  to_s == other.to_s
49
100
  end
101
+
102
+ def strong_counts
103
+ @strong_counts ||= begin
104
+ (1..counts_per_measure).select do |count|
105
+ count == 1 ||
106
+ count == counts_per_measure / 2.0 + 1 ||
107
+ (
108
+ counts_per_measure % 3 == 0 &&
109
+ counts_per_measure > 6 &&
110
+ count % 3 == 1
111
+ )
112
+ end
113
+ end
114
+ end
50
115
  end
@@ -0,0 +1,61 @@
1
+ class HeadMusic::Position
2
+ attr_reader :composition, :measure_number, :count, :tick
3
+ delegate :to_s, to: :code
4
+ delegate :meter, to: :composition
5
+
6
+ def initialize(composition, code_or_measure, count = nil, tick = nil)
7
+ if code_or_measure.is_a?(String) && code_or_measure =~ /\D/
8
+ ensure_state(composition, *code_or_measure.split(/\D+/))
9
+ else
10
+ ensure_state(composition, code_or_measure, count, tick)
11
+ end
12
+ end
13
+
14
+ def code
15
+ [measure_number, count, tick].join(':')
16
+ end
17
+
18
+ def state
19
+ [composition.name, code].join(' ')
20
+ end
21
+
22
+ def ==(other)
23
+ self.state == other.state
24
+ end
25
+
26
+ private
27
+
28
+ def ensure_state(composition, measure_number, count, tick)
29
+ @composition = composition
30
+ @measure_number = measure_number.to_i
31
+ @count = (count || 1).to_i
32
+ @tick = (tick || 0).to_i
33
+ roll_over_units
34
+ end
35
+
36
+ def roll_over_units
37
+ roll_over_ticks
38
+ roll_over_counts
39
+ end
40
+
41
+ def roll_over_ticks
42
+ while @tick > meter.ticks_per_count
43
+ @tick -= meter.ticks_per_count.to_i
44
+ @count += 1
45
+ end
46
+ end
47
+
48
+ def roll_over_counts
49
+ while @count > meter.counts_per_measure
50
+ @count -= meter.counts_per_measure
51
+ @measure_number += 1
52
+ end
53
+ end
54
+ end
55
+
56
+ # In Logic Pro X, the 'beat' is determined by the denominator, even if compound.
57
+ # Logic then divides the beat into 'divisions' that are a sixteenth in length.
58
+ # Each division is then divided into 240 ticks (960 PPQN / 4 sixteenths-per-quarter)
59
+
60
+ # Tempo specifies the beat unit, usually the traditional beat unit in the case of compound meters,
61
+ # so 6/8 would specify [dotted-quarter] = 132 (or whatever).
@@ -1,29 +1,40 @@
1
1
  class HeadMusic::RhythmicUnit
2
2
  MULTIPLES = ['whole', 'double whole', 'longa', 'maxima']
3
- DIVISIONS = ['whole', 'half', 'quarter', 'eighth', 'sixteenth', 'thirty-second', 'sixty-fourth', 'hundred twenty-eighth', 'two hundred fifty-sixth']
3
+ FRACTIONS = ['whole', 'half', 'quarter', 'eighth', 'sixteenth', 'thirty-second', 'sixty-fourth', 'hundred twenty-eighth', 'two hundred fifty-sixth']
4
4
 
5
5
  BRITISH_MULTIPLE_NAMES = %w[semibreve breve longa maxima]
6
6
  BRITISH_DIVISION_NAMES = %w[semibreve minim crotchet quaver semiquaver demisemiquaver hemidemisemiquaver semihemidemisemiquaver demisemihemidemisemiquaver]
7
7
 
8
+ PPQN = PULSES_PER_QUARTER_NOTE = 960
9
+
8
10
  def self.get(name)
9
11
  @rhythmic_units ||= {}
10
- @rhythmic_units[name.to_s] ||= new(name.to_s)
12
+ hash_key = HeadMusic::Utilities::HashKey.for(name)
13
+ @rhythmic_units[hash_key] ||= new(name.to_s)
11
14
  end
12
15
  singleton_class.send(:alias_method, :[], :get)
13
16
 
17
+ def self.for_denominator_value(denominator)
18
+ get(FRACTIONS[Math.log2(denominator).to_i])
19
+ end
20
+
14
21
  attr_reader :name, :numerator, :denominator
15
22
  delegate :to_s, to: :name
16
23
 
17
24
  def initialize(canonical_name)
18
25
  @name ||= canonical_name
19
26
  @numerator ||= MULTIPLES.include?(name) ? 2**MULTIPLES.index(name) : 1
20
- @denominator ||= DIVISIONS.include?(name) ? 2**DIVISIONS.index(name) : 1
27
+ @denominator ||= FRACTIONS.include?(name) ? 2**FRACTIONS.index(name) : 1
21
28
  end
22
29
 
23
30
  def relative_value
24
31
  @numerator.to_f / @denominator
25
32
  end
26
33
 
34
+ def ticks
35
+ PPQN * 4 * relative_value
36
+ end
37
+
27
38
  def notehead
28
39
  case relative_value
29
40
  when 8
@@ -40,7 +51,7 @@ class HeadMusic::RhythmicUnit
40
51
  end
41
52
 
42
53
  def flags
43
- DIVISIONS.include?(name) ? [DIVISIONS.index(name) - 2, 0].max : 0
54
+ FRACTIONS.include?(name) ? [FRACTIONS.index(name) - 2, 0].max : 0
44
55
  end
45
56
 
46
57
  def has_stem?
@@ -50,8 +61,8 @@ class HeadMusic::RhythmicUnit
50
61
  def british_name
51
62
  if MULTIPLES.include?(name)
52
63
  BRITISH_MULTIPLE_NAMES[MULTIPLES.index(name)]
53
- elsif DIVISIONS.include?(name)
54
- BRITISH_DIVISION_NAMES[DIVISIONS.index(name)]
64
+ elsif FRACTIONS.include?(name)
65
+ BRITISH_DIVISION_NAMES[FRACTIONS.index(name)]
55
66
  end
56
67
  end
57
68
 
@@ -1,9 +1,8 @@
1
1
  class HeadMusic::RhythmicValue
2
- PPQN = PULSES_PER_QUARTER_NOTE = 960
3
-
4
2
  attr_reader :unit, :dots, :tied_value
5
3
 
6
4
  delegate :name, to: :unit, prefix: true
5
+ delegate :to_s, to: :name
7
6
 
8
7
  def initialize(unit, dots: nil, tied_value: nil)
9
8
  @unit = HeadMusic::RhythmicUnit.get(unit)
@@ -28,7 +27,7 @@ class HeadMusic::RhythmicValue
28
27
  end
29
28
 
30
29
  def ticks
31
- PPQN * 4 * total_value
30
+ HeadMusic::RhythmicUnit::PPQN * 4 * total_value
32
31
  end
33
32
 
34
33
  def per_whole
@@ -57,4 +56,8 @@ class HeadMusic::RhythmicValue
57
56
  single_value_name
58
57
  end
59
58
  end
59
+
60
+ def ==(other)
61
+ to_s == other.to_s
62
+ end
60
63
  end
@@ -1,8 +1,7 @@
1
1
  class HeadMusic::Scale
2
2
  def self.get(root_pitch, scale_type_name = nil)
3
3
  root_pitch = HeadMusic::Pitch.get(root_pitch)
4
- scale_type_name ||= :major
5
- scale_type ||= HeadMusic::ScaleType.get(scale_type_name)
4
+ scale_type ||= HeadMusic::ScaleType.get(scale_type_name || :major)
6
5
  @scales ||= {}
7
6
  @scales[root_pitch.to_s] ||= {}
8
7
  @scales[root_pitch.to_s][scale_type.name] ||= new(root_pitch, scale_type)
@@ -18,23 +17,19 @@ class HeadMusic::Scale
18
17
  def pitches(direction: :ascending, octaves: 1)
19
18
  @pitches ||= {}
20
19
  @pitches[direction] ||= {}
21
- @pitches[direction][octaves] ||= begin
22
- letter_name_cycle = root_pitch.letter_name_cycle
23
- semitones_from_root = 0
24
- [root_pitch].tap do |pitches|
25
- if [:ascending, :both].include?(direction)
26
- (1..octaves).each do |i|
27
- scale_type.ascending_intervals.each_with_index do |semitones, i|
28
- semitones_from_root += semitones
29
- pitches << pitch_for_step(i+1, semitones_from_root, :ascending)
30
- end
31
- end
32
- end
33
- if [:descending, :both].include?(direction)
20
+ @pitches[direction][octaves] ||= determine_scale_pitches(direction, octaves)
21
+ end
22
+
23
+ def determine_scale_pitches(direction, octaves)
24
+ letter_name_cycle = root_pitch.letter_name_cycle
25
+ semitones_from_root = 0
26
+ [root_pitch].tap do |pitches|
27
+ [:ascending, :descending].each do |single_direction|
28
+ if [single_direction, :both].include?(direction)
34
29
  (1..octaves).each do |i|
35
- scale_type.descending_intervals.each_with_index do |semitones, i|
36
- semitones_from_root -= semitones
37
- pitches << pitch_for_step(i+1, semitones_from_root, :descending)
30
+ direction_intervals(single_direction).each_with_index do |semitones, i|
31
+ semitones_from_root += semitones * direction_sign(single_direction)
32
+ pitches << pitch_for_step(i+1, semitones_from_root, single_direction)
38
33
  end
39
34
  end
40
35
  end
@@ -42,6 +37,14 @@ class HeadMusic::Scale
42
37
  end
43
38
  end
44
39
 
40
+ def direction_sign(direction)
41
+ direction == :descending ? -1 : 1
42
+ end
43
+
44
+ def direction_intervals(direction)
45
+ scale_type.send("#{direction}_intervals")
46
+ end
47
+
45
48
  def spellings(direction: :ascending, octaves: 1)
46
49
  pitches(direction: direction, octaves: octaves).map(&:spelling).map(&:to_s)
47
50
  end
@@ -1,3 +1,3 @@
1
1
  module HeadMusic
2
- VERSION = "0.6.4"
2
+ VERSION = "0.7.0"
3
3
  end
data/lib/head_music.rb CHANGED
@@ -14,11 +14,14 @@ require 'head_music/instrument'
14
14
  require 'head_music/interval'
15
15
  require 'head_music/key_signature'
16
16
  require 'head_music/letter_name'
17
+ require 'head_music/composition'
18
+ require 'head_music/measure'
17
19
  require 'head_music/meter'
18
20
  require 'head_music/note'
19
21
  require 'head_music/octave'
20
22
  require 'head_music/pitch_class'
21
23
  require 'head_music/pitch'
24
+ require 'head_music/position'
22
25
  require 'head_music/quality'
23
26
  require 'head_music/rhythmic_unit'
24
27
  require 'head_music/rhythmic_value'
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.6.4
4
+ version: 0.7.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-02-26 00:00:00.000000000 Z
11
+ date: 2017-03-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -104,6 +104,7 @@ extra_rdoc_files: []
104
104
  files:
105
105
  - ".gitignore"
106
106
  - ".rspec"
107
+ - ".rubocop.yml"
107
108
  - ".travis.yml"
108
109
  - CODE_OF_CONDUCT.md
109
110
  - Gemfile
@@ -118,6 +119,7 @@ files:
118
119
  - lib/head_music/accidental.rb
119
120
  - lib/head_music/circle.rb
120
121
  - lib/head_music/clef.rb
122
+ - lib/head_music/composition.rb
121
123
  - lib/head_music/consonance.rb
122
124
  - lib/head_music/functional_interval.rb
123
125
  - lib/head_music/grand_staff.rb
@@ -125,11 +127,13 @@ files:
125
127
  - lib/head_music/interval.rb
126
128
  - lib/head_music/key_signature.rb
127
129
  - lib/head_music/letter_name.rb
130
+ - lib/head_music/measure.rb
128
131
  - lib/head_music/meter.rb
129
132
  - lib/head_music/note.rb
130
133
  - lib/head_music/octave.rb
131
134
  - lib/head_music/pitch.rb
132
135
  - lib/head_music/pitch_class.rb
136
+ - lib/head_music/position.rb
133
137
  - lib/head_music/quality.rb
134
138
  - lib/head_music/rhythmic_unit.rb
135
139
  - lib/head_music/rhythmic_value.rb
@@ -151,7 +155,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
151
155
  requirements:
152
156
  - - "~>"
153
157
  - !ruby/object:Gem::Version
154
- version: '2.3'
158
+ version: 2.3.1
155
159
  required_rubygems_version: !ruby/object:Gem::Requirement
156
160
  requirements:
157
161
  - - ">="