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.
- checksums.yaml +7 -0
- data/.github/workflows/main.yml +18 -0
- data/.gitignore +11 -0
- data/.rubocop.yml +47 -0
- data/.ruby_version +1 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +19 -0
- data/Gemfile.lock +71 -0
- data/LICENSE +21 -0
- data/LICENSE.txt +21 -0
- data/README.md +113 -0
- data/README.rdoc +6 -0
- data/Rakefile +23 -0
- data/bin/bash_scripts/cleanup_listening_exercises.sh +2 -0
- data/bin/bash_scripts/cleanup_singing_exercises.sh +2 -0
- data/bin/bash_scripts/midi_to_mp3.sh +13 -0
- data/bin/console +15 -0
- data/bin/fet +87 -0
- data/bin/setup +8 -0
- data/fet.gemspec +41 -0
- data/fet.rdoc +94 -0
- data/lib/fet/chord_progression.rb +55 -0
- data/lib/fet/cli/generate/listening.rb +20 -0
- data/lib/fet/cli/generate/singing.rb +18 -0
- data/lib/fet/cli/generate/single_note_listening.rb +17 -0
- data/lib/fet/degree.rb +44 -0
- data/lib/fet/degrees.rb +61 -0
- data/lib/fet/exceptions.rb +18 -0
- data/lib/fet/generator/listening.rb +107 -0
- data/lib/fet/generator/singing.rb +89 -0
- data/lib/fet/generator/single_note_listening.rb +67 -0
- data/lib/fet/hardcoded_midi_values.rb +24 -0
- data/lib/fet/instrument_ranges.rb +19 -0
- data/lib/fet/midi_note.rb +40 -0
- data/lib/fet/midilib_interface.rb +134 -0
- data/lib/fet/music_theory.rb +116 -0
- data/lib/fet/note.rb +138 -0
- data/lib/fet/note_validations.rb +58 -0
- data/lib/fet/version.rb +5 -0
- data/lib/fet.rb +10 -0
- metadata +147 -0
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
|
data/lib/fet/version.rb
ADDED
data/lib/fet.rb
ADDED
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: []
|