fet 0.2.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.
data/lib/fet/note.rb ADDED
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "note_validations"
4
+
5
+ module Fet
6
+ # Class responsible for parsing and validating musical notes from a string, e.g. "Eb"
7
+ class Note
8
+ include NoteValidations
9
+
10
+ ACCIDENTAL_TO_SEMITONES_MAP = {
11
+ "b" => -1,
12
+ "#" => 1,
13
+ "x" => 2,
14
+ }.deep_freeze
15
+
16
+ # TODO: this class could also potentially handle the octave number - note that e.g. Bb,B,Bx... are in octave 3 while Cb,C,C# are in octave 4
17
+ attr_accessor :full_note,
18
+ :natural_note,
19
+ :accidental
20
+
21
+ def initialize(note)
22
+ self.full_note = note
23
+ validate_full_note!
24
+
25
+ self.natural_note = note[0]
26
+ validate_natural_note!
27
+
28
+ self.accidental = note[1..]
29
+ validate_accidental!
30
+ end
31
+
32
+ def self.accidental_from_semitone_offset(semitone_offset)
33
+ return "" if semitone_offset.zero?
34
+ return "b" * -semitone_offset if semitone_offset.negative?
35
+
36
+ number_of_hashes = (semitone_offset % 2).zero? ? 0 : 1
37
+ number_of_xs = semitone_offset / 2
38
+ return "#" * number_of_hashes + "x" * number_of_xs
39
+ end
40
+
41
+ # NOTE: performs the following conversions:
42
+ # Fbb -> Fb -> F -> F# ->Fx -> F#x -> Fxx
43
+ def sharpened_note
44
+ note_as_string = case
45
+ when accidental.start_with?("b")
46
+ "#{natural_note}#{accidental[1..]}"
47
+ when accidental.start_with?("#")
48
+ "#{natural_note}x#{accidental[1..]}"
49
+ else
50
+ "#{natural_note}##{accidental}"
51
+ end
52
+
53
+ return Note.new(note_as_string)
54
+ end
55
+
56
+ # NOTE: performs the following conversions:
57
+ # Fxx -> F#x -> Fx -> F# -> F -> Fb -> Fbb
58
+ def flattened_note
59
+ note_as_string = case
60
+ when accidental.start_with?("x")
61
+ "#{natural_note}##{accidental[1..]}"
62
+ when accidental.start_with?("#")
63
+ "#{natural_note}#{accidental[1..]}"
64
+ else
65
+ "#{natural_note}#{accidental}b"
66
+ end
67
+
68
+ return Note.new(note_as_string)
69
+ end
70
+
71
+ # NOTE: normalizing the note means:
72
+ # - converting the natural note + accidentals such that the remaining accidental is either "b", "", or "#", or
73
+ # - if a note name is provided, then convert the accidental such that the natural note matches the pitch of the original
74
+ def normalized_note
75
+ remaining_semitones = accidental_to_semitone_offset
76
+ next_note = remaining_semitones.positive? ? next_natural_note : previous_natural_note
77
+ next_note_offset = remaining_semitones.positive? ? semitone_offset_to_next_natural_note : semitone_offset_to_previous_natural_note
78
+ return Note.new(full_note) if next_note_offset.abs > remaining_semitones.abs
79
+
80
+ return Note.new("#{next_note}#{self.class.accidental_from_semitone_offset(remaining_semitones - next_note_offset)}").normalized_note
81
+ end
82
+
83
+ # NOTE: Note.new("E").change_natural_note("D") -> Note.new("Dx")
84
+ # def change_natural_note(new_natural_note)
85
+ # semitone_offset = accidental_to_semitone_offset
86
+ # return Note.new("#{natural_note}#{self.class.accidental_from_semitone_offset(semitone_offset)}") if new_natural_note == natural_note
87
+ # end
88
+
89
+ def degree(root_name)
90
+ notes_array = Fet::MusicTheory.notes_of_mode(root_name, "major")
91
+ index = notes_array.index { |note| Note.new(note).natural_note == natural_note }
92
+
93
+ degree = index + 1
94
+ degree_note = Note.new(notes_array[index])
95
+
96
+ accidental_difference = accidental_to_semitone_offset - degree_note.accidental_to_semitone_offset
97
+ return "#{self.class.accidental_from_semitone_offset(accidental_difference)}#{degree}"
98
+ end
99
+
100
+ def accidental_to_semitone_offset
101
+ return accidental.chars.map { |char| ACCIDENTAL_TO_SEMITONES_MAP[char] }.sum
102
+ end
103
+
104
+ def natural?
105
+ return accidental.chars.empty?
106
+ end
107
+
108
+ def flattened?
109
+ return accidental.chars.include?("b")
110
+ end
111
+
112
+ def sharpened?
113
+ return accidental.chars.include?("#") || accidental.chars.include?("x")
114
+ end
115
+
116
+ private
117
+
118
+ def next_natural_note
119
+ index = Fet::MusicTheory::ORDERED_NATURAL_NOTES.index(natural_note)
120
+ index = (index + 1) % Fet::MusicTheory::ORDERED_NATURAL_NOTES.size
121
+ return Fet::MusicTheory::ORDERED_NATURAL_NOTES[index]
122
+ end
123
+
124
+ def previous_natural_note
125
+ index = Fet::MusicTheory::ORDERED_NATURAL_NOTES.index(natural_note)
126
+ index = (index - 1) % Fet::MusicTheory::ORDERED_NATURAL_NOTES.size
127
+ return Fet::MusicTheory::ORDERED_NATURAL_NOTES[index]
128
+ end
129
+
130
+ def semitone_offset_to_next_natural_note
131
+ return Fet::MusicTheory::SEMITONES_TO_NEXT_NATURAL_NOTE[natural_note]
132
+ end
133
+
134
+ def semitone_offset_to_previous_natural_note
135
+ return -Fet::MusicTheory::SEMITONES_TO_NEXT_NATURAL_NOTE[previous_natural_note]
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fet
4
+ # Module holding validations for the Note class
5
+ module NoteValidations
6
+ private
7
+
8
+ def validate_full_note!
9
+ raise InvalidNote.new(full_note) unless full_note.is_a?(String)
10
+ end
11
+
12
+ def validate_natural_note!
13
+ raise InvalidNote.new(full_note) unless natural_note.is_a?(String)
14
+ raise InvalidNote.new(full_note) unless MusicTheory::CIRCLE_OF_FIFTHS_WITHOUT_ACCIDENTALS.include?(natural_note)
15
+ end
16
+
17
+ def validate_accidental!
18
+ raise InvalidNote.new(full_note) unless valid_accidental_type?
19
+ return if accidental.empty?
20
+
21
+ raise InvalidNote.new(full_note) unless valid_accidental_logic?
22
+ end
23
+
24
+ def valid_accidental_type?
25
+ return accidental.is_a?(String)
26
+ end
27
+
28
+ def valid_accidental_logic?
29
+ return false unless valid_accidental_characters?
30
+ return false if can_be_flat_accidental? && !valid_flat_accidental?
31
+ return false if can_be_sharp_accidental? && !valid_sharp_accidental?
32
+
33
+ return true
34
+ end
35
+
36
+ def valid_accidental_characters?
37
+ return accidental.chars.uniq.all? { |char| ["b", "#", "x"].include?(char) }
38
+ end
39
+
40
+ def can_be_flat_accidental?
41
+ return accidental.chars.uniq.include?("b")
42
+ end
43
+
44
+ def valid_flat_accidental?
45
+ return accidental.chars.uniq.all? { |char| ["b"].include?(char) } # there can only be "b" characters
46
+ end
47
+
48
+ def can_be_sharp_accidental?
49
+ return accidental.chars.uniq.include?("#") || accidental.chars.uniq.include?("x")
50
+ end
51
+
52
+ def valid_sharp_accidental?
53
+ return accidental.chars.uniq.all? { |char| ["#", "x"].include?(char) } && # only allow "#" and "x" characters
54
+ [0, 1].include?(accidental.chars.select { |char| char == "#" }.size) && # there can be at most one "#" character
55
+ accidental.chars.sort.join == accidental # the "#" character must come before any "x" characters
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fet
4
+ VERSION = "0.2.0"
5
+ end
data/lib/fet.rb ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ice_nine"
4
+ require "ice_nine/core_ext/object"
5
+
6
+ Dir["#{__dir__}/fet/**/*.rb"].each { |file| require_relative(file.delete_prefix("#{__dir__}/")) }
7
+
8
+ # Base Gem module
9
+ module Fet
10
+ end
metadata ADDED
@@ -0,0 +1,147 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fet
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Dimitrios Lisenko
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-09-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: gli
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.20'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 2.20.1
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '2.20'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 2.20.1
33
+ - !ruby/object:Gem::Dependency
34
+ name: ice_nine
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: 0.11.2
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: 0.11.2
47
+ - !ruby/object:Gem::Dependency
48
+ name: midilib
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '2.0'
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: 2.0.5
57
+ type: :runtime
58
+ prerelease: false
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - "~>"
62
+ - !ruby/object:Gem::Version
63
+ version: '2.0'
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: 2.0.5
67
+ description: Teaches your ear to recognize notes based on their function in a key.
68
+ email:
69
+ - dimitrioslisenko@gmail.com
70
+ executables:
71
+ - fet
72
+ extensions: []
73
+ extra_rdoc_files:
74
+ - README.rdoc
75
+ - fet.rdoc
76
+ files:
77
+ - ".github/workflows/main.yml"
78
+ - ".gitignore"
79
+ - ".rubocop.yml"
80
+ - ".ruby_version"
81
+ - CHANGELOG.md
82
+ - Gemfile
83
+ - Gemfile.lock
84
+ - LICENSE
85
+ - LICENSE.txt
86
+ - README.md
87
+ - README.rdoc
88
+ - Rakefile
89
+ - bin/bash_scripts/cleanup_listening_exercises.sh
90
+ - bin/bash_scripts/cleanup_singing_exercises.sh
91
+ - bin/bash_scripts/midi_to_mp3.sh
92
+ - bin/console
93
+ - bin/fet
94
+ - bin/setup
95
+ - fet.gemspec
96
+ - fet.rdoc
97
+ - lib/fet.rb
98
+ - lib/fet/chord_progression.rb
99
+ - lib/fet/cli/generate/listening.rb
100
+ - lib/fet/cli/generate/singing.rb
101
+ - lib/fet/cli/generate/single_note_listening.rb
102
+ - lib/fet/degree.rb
103
+ - lib/fet/degrees.rb
104
+ - lib/fet/exceptions.rb
105
+ - lib/fet/generator/listening.rb
106
+ - lib/fet/generator/singing.rb
107
+ - lib/fet/generator/single_note_listening.rb
108
+ - lib/fet/hardcoded_midi_values.rb
109
+ - lib/fet/instrument_ranges.rb
110
+ - lib/fet/midi_note.rb
111
+ - lib/fet/midilib_interface.rb
112
+ - lib/fet/music_theory.rb
113
+ - lib/fet/note.rb
114
+ - lib/fet/note_validations.rb
115
+ - lib/fet/version.rb
116
+ homepage: https://github.com/DimitriosLisenko/fet
117
+ licenses:
118
+ - MIT
119
+ metadata:
120
+ homepage_uri: https://github.com/DimitriosLisenko/fet
121
+ source_code_uri: https://github.com/DimitriosLisenko/fet
122
+ changelog_uri: https://github.com/DimitriosLisenko/fet/blob/master/CHANGELOG.md
123
+ post_install_message:
124
+ rdoc_options:
125
+ - "--title"
126
+ - fet
127
+ - "--main"
128
+ - README.rdoc
129
+ - "-ri"
130
+ require_paths:
131
+ - lib
132
+ required_ruby_version: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: 3.0.0
137
+ required_rubygems_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ requirements: []
143
+ rubygems_version: 3.2.22
144
+ signing_key:
145
+ specification_version: 4
146
+ summary: A functional ear trainer.
147
+ test_files: []