fet 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Fet
|
4
|
+
module Generator
|
5
|
+
# Class that generates MIDI files for the singing exercises
|
6
|
+
class Singing
|
7
|
+
def initialize(tempo:, pause:, directory_prefix: "")
|
8
|
+
self.tempo = tempo
|
9
|
+
self.pause = pause
|
10
|
+
self.midi_range = HIGH_SINGING_OCTAVE_RANGE
|
11
|
+
self.directory_prefix = directory_prefix
|
12
|
+
end
|
13
|
+
|
14
|
+
def generate
|
15
|
+
generate_major
|
16
|
+
generate_minor
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
attr_accessor :tempo, :pause, :midi_range, :directory_prefix
|
22
|
+
|
23
|
+
def generate_major
|
24
|
+
MusicTheory::MAJOR_KEYS.each do |root_note_name|
|
25
|
+
root_midi_value = MAJOR_ROOT_MIDI_VALUES[root_note_name]
|
26
|
+
midi_range.each do |note_midi_value|
|
27
|
+
create_midi_file("major", root_note_name, root_midi_value, note_midi_value)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def generate_minor
|
33
|
+
MusicTheory::MINOR_KEYS.each do |root_note_name|
|
34
|
+
root_midi_value = MINOR_ROOT_MIDI_VALUES[root_note_name]
|
35
|
+
midi_range.each do |note_midi_value|
|
36
|
+
create_midi_file("minor", root_note_name, root_midi_value, note_midi_value)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def create_midi_file(key_type, root_note_name, root_midi_value, note_midi_value)
|
42
|
+
progression = Fet::ChordProgression.new(offset: root_midi_value, template_type: key_type).with_offset
|
43
|
+
Fet::MidilibInterface.new(
|
44
|
+
tempo: tempo, progression: progression, notes: [note_midi_value],
|
45
|
+
info: generate_midi_info(key_type, root_note_name, root_midi_value, note_midi_value),
|
46
|
+
filename: full_filename(key_type, root_note_name, root_midi_value, note_midi_value),
|
47
|
+
).create_singing_midi_file(pause)
|
48
|
+
end
|
49
|
+
|
50
|
+
def generate_midi_info(key_type, root_note_name, root_midi_value, note_midi_value)
|
51
|
+
note_name = note_name(root_note_name, root_midi_value, note_midi_value)
|
52
|
+
note_octave_value = Fet::MidiNote.new(note_midi_value).octave_number
|
53
|
+
result = [
|
54
|
+
"Key: [#{root_note_name} #{key_type}]",
|
55
|
+
"Degree: [#{degree_name(root_note_name, root_midi_value, note_midi_value)}]",
|
56
|
+
"Note: [#{note_name}#{note_octave_value}]",
|
57
|
+
]
|
58
|
+
return result.join(" ")
|
59
|
+
end
|
60
|
+
|
61
|
+
def full_filename(key_type, root_note_name, root_midi_value, note_midi_value)
|
62
|
+
result = File.join(*[directory_prefix, "singing", key_type].reject(&:empty?))
|
63
|
+
filename = root_note_name # note, e.g. Db
|
64
|
+
filename += key_type == "major" ? "M" : "m" # type of note, M or m
|
65
|
+
filename += "_" # delimiter
|
66
|
+
filename += degree_name(root_note_name, root_midi_value, note_midi_value) # degree, e.g. b7
|
67
|
+
filename += ".mid" # extension
|
68
|
+
return File.join(result, filename)
|
69
|
+
end
|
70
|
+
|
71
|
+
def root_degrees_instance(root_note_name, root_midi_value)
|
72
|
+
root_octave_value = Fet::MidiNote.new(root_midi_value).octave_number
|
73
|
+
return Fet::Degrees.new(root_name: root_note_name, octave_value: root_octave_value)
|
74
|
+
end
|
75
|
+
|
76
|
+
def degree_name(root_note_name, root_midi_value, note_midi_value)
|
77
|
+
degrees_instance = root_degrees_instance(root_note_name, root_midi_value)
|
78
|
+
return degrees_instance.degree_names_of_midi_value(note_midi_value).last
|
79
|
+
end
|
80
|
+
|
81
|
+
def note_name(root_note_name, root_midi_value, note_midi_value)
|
82
|
+
degrees_instance = root_degrees_instance(root_note_name, root_midi_value)
|
83
|
+
return degrees_instance.note_name_of_degree(
|
84
|
+
degree_name(root_note_name, root_midi_value, note_midi_value),
|
85
|
+
)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Fet
|
4
|
+
module Generator
|
5
|
+
# Class that generates MIDI files for the single note listening exercises
|
6
|
+
class SingleNoteListening
|
7
|
+
def initialize(tempo:, directory_prefix: "")
|
8
|
+
self.tempo = tempo
|
9
|
+
self.note = Fet::Note.new("C")
|
10
|
+
self.octave_value = 4
|
11
|
+
self.midi_value = MidiNote.from_note(note.full_note, octave_value).midi_value
|
12
|
+
self.directory_prefix = directory_prefix
|
13
|
+
end
|
14
|
+
|
15
|
+
def generate
|
16
|
+
MusicTheory::MAJOR_KEYS.each do |root_note_name|
|
17
|
+
root_midi_value = MAJOR_ROOT_MIDI_VALUES[root_note_name]
|
18
|
+
create_midi_file("major", root_note_name, root_midi_value)
|
19
|
+
end
|
20
|
+
|
21
|
+
MusicTheory::MINOR_KEYS.each do |root_note_name|
|
22
|
+
root_midi_value = MINOR_ROOT_MIDI_VALUES[root_note_name]
|
23
|
+
create_midi_file("minor", root_note_name, root_midi_value)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_accessor :tempo, :note, :octave_value, :midi_value, :directory_prefix
|
30
|
+
|
31
|
+
def create_midi_file(key_type, root_note_name, root_midi_value)
|
32
|
+
progression = Fet::ChordProgression.new(offset: root_midi_value, template_type: key_type).with_offset
|
33
|
+
Fet::MidilibInterface.new(
|
34
|
+
tempo: tempo, progression: progression, notes: [midi_value],
|
35
|
+
info: generate_midi_info(key_type, root_note_name, root_midi_value),
|
36
|
+
filename: full_filename(key_type, root_note_name, root_midi_value),
|
37
|
+
).create_listening_midi_file
|
38
|
+
end
|
39
|
+
|
40
|
+
def generate_midi_info(key_type, root_note_name, root_midi_value)
|
41
|
+
result = [
|
42
|
+
"Key: [#{root_note_name} #{key_type}]",
|
43
|
+
"Degree: [#{degree_name(root_note_name, root_midi_value)}]",
|
44
|
+
"Note: [#{note.full_note}#{octave_value}]",
|
45
|
+
]
|
46
|
+
return result.join(" ")
|
47
|
+
end
|
48
|
+
|
49
|
+
def full_filename(key_type, root_note_name, root_midi_value)
|
50
|
+
result = File.join(*[directory_prefix, "listening_single_note", key_type].reject(&:empty?))
|
51
|
+
filename = root_note_name # note, e.g. Db
|
52
|
+
filename += key_type == "major" ? "M" : "m" # type of note, M or m
|
53
|
+
filename += "_" # delimiter
|
54
|
+
filename += degree_name(root_note_name, root_midi_value) # degree, e.g. b7
|
55
|
+
filename += ".mid" # extension
|
56
|
+
return File.join(result, filename)
|
57
|
+
end
|
58
|
+
|
59
|
+
def degree_name(root_note_name, root_midi_value)
|
60
|
+
root_octave_value = Fet::MidiNote.new(root_midi_value).octave_number
|
61
|
+
degrees_instance = Fet::Degrees.new(root_name: root_note_name, octave_value: root_octave_value)
|
62
|
+
degree_name = degrees_instance.degree_names_of_midi_value(midi_value).last
|
63
|
+
return degree_name
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "midi_note"
|
4
|
+
|
5
|
+
module Fet
|
6
|
+
MAJOR_ROOT_MIDI_VALUES = {
|
7
|
+
"C#" => MidiNote.from_note("C#", 4).midi_value,
|
8
|
+
"F#" => MidiNote.from_note("F#", 3).midi_value,
|
9
|
+
"B" => MidiNote.from_note("B", 3).midi_value,
|
10
|
+
"E" => MidiNote.from_note("E", 3).midi_value,
|
11
|
+
"A" => MidiNote.from_note("A", 3).midi_value,
|
12
|
+
"D" => MidiNote.from_note("D", 4).midi_value,
|
13
|
+
"G" => MidiNote.from_note("G", 3).midi_value,
|
14
|
+
"C" => MidiNote.from_note("C", 4).midi_value,
|
15
|
+
"F" => MidiNote.from_note("F", 3).midi_value,
|
16
|
+
"Bb" => MidiNote.from_note("Bb", 3).midi_value,
|
17
|
+
"Eb" => MidiNote.from_note("Eb", 4).midi_value,
|
18
|
+
"Ab" => MidiNote.from_note("Ab", 3).midi_value,
|
19
|
+
"Db" => MidiNote.from_note("Db", 4).midi_value,
|
20
|
+
"Gb" => MidiNote.from_note("Gb", 3).midi_value,
|
21
|
+
"Cb" => MidiNote.from_note("Cb", 3).midi_value,
|
22
|
+
}.deep_freeze
|
23
|
+
MINOR_ROOT_MIDI_VALUES = MusicTheory::MINOR_KEYS.zip(MAJOR_ROOT_MIDI_VALUES.values.map { |i| i - 3 }).to_h.deep_freeze
|
24
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "midi_note"
|
4
|
+
|
5
|
+
module Fet
|
6
|
+
PIANO_RANGE = (MidiNote.from_note("A", 0).midi_value..MidiNote.from_note("C", 8).midi_value).to_a.deep_freeze
|
7
|
+
REDUCED_BY_OCTAVE_PIANO_RANGE = (MidiNote.from_note("A", 1).midi_value..MidiNote.from_note("C", 7).midi_value).to_a.deep_freeze
|
8
|
+
GUITAR_RANGE = (MidiNote.from_note("E", 2).midi_value..MidiNote.from_note("E", 6).midi_value).to_a.deep_freeze
|
9
|
+
# Ranges according to Wikipedia:
|
10
|
+
# Bass: E2 - E4
|
11
|
+
# Baritone: G2 - G4
|
12
|
+
# Tenor: C3 - C5
|
13
|
+
# Countertenor: E3 - E5
|
14
|
+
# Contralto: F3 - F5
|
15
|
+
# Mezzo-soprano: A3 - A5
|
16
|
+
# Soprano: C4 - C6
|
17
|
+
LOW_SINGING_OCTAVE_RANGE = (MidiNote.from_note("D", 3).midi_value...MidiNote.from_note("D", 4).midi_value).to_a.deep_freeze
|
18
|
+
HIGH_SINGING_OCTAVE_RANGE = (MidiNote.from_note("D", 4).midi_value...MidiNote.from_note("D", 5).midi_value).to_a.deep_freeze
|
19
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "music_theory"
|
4
|
+
|
5
|
+
module Fet
|
6
|
+
# Class that handles MIDI values that represent notes
|
7
|
+
class MidiNote
|
8
|
+
attr_reader :midi_value
|
9
|
+
|
10
|
+
def initialize(midi_value)
|
11
|
+
self.midi_value = midi_value
|
12
|
+
validate_midi_value!
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.from_note(note_name, octave_number)
|
16
|
+
midi_value_for_c = 12 * (1 + octave_number)
|
17
|
+
midi_value = midi_value_for_c + MusicTheory.semitones_from_c(note_name)
|
18
|
+
return new(midi_value)
|
19
|
+
end
|
20
|
+
|
21
|
+
def octave_number
|
22
|
+
return (midi_value - 12) / 12
|
23
|
+
end
|
24
|
+
|
25
|
+
# NOTE: This is not strictly correct because e.g. the midi value of 63 can be D#4 or Eb4, which affects
|
26
|
+
# what degree it actually is. However, without additional information, this is good enough.
|
27
|
+
def degree(root_midi_value)
|
28
|
+
return (midi_value - root_midi_value) % 12
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
attr_writer :midi_value
|
34
|
+
|
35
|
+
def validate_midi_value!
|
36
|
+
# In MIDI, the minimum note value is 0 (which is C(-1)) and the maximum note value is 127 (which is G(9))
|
37
|
+
raise InvalidMidiNote.new(midi_value) if midi_value.negative? || midi_value > 127
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "midilib/sequence"
|
4
|
+
require "midilib/consts"
|
5
|
+
|
6
|
+
module Fet
|
7
|
+
# Interface with the midilib library to generate MIDI files
|
8
|
+
class MidilibInterface
|
9
|
+
def initialize(tempo:, progression:, notes:, info:, filename:)
|
10
|
+
self.tempo = tempo
|
11
|
+
self.progression = progression
|
12
|
+
self.notes = notes
|
13
|
+
self.info = info
|
14
|
+
self.filename = filename
|
15
|
+
self.sequence = MIDI::Sequence.new
|
16
|
+
self.track = generate_instrument_track
|
17
|
+
end
|
18
|
+
|
19
|
+
def create_listening_midi_file
|
20
|
+
# Play the chord progression
|
21
|
+
set_progression_on_track
|
22
|
+
|
23
|
+
add_rest(2 * quarter_note_length)
|
24
|
+
play_notes_as_chord(notes, quarter_note_length)
|
25
|
+
|
26
|
+
add_rest(6 * quarter_note_length)
|
27
|
+
play_notes_sequentially(notes, quarter_note_length)
|
28
|
+
|
29
|
+
write_sequence_to_file
|
30
|
+
end
|
31
|
+
|
32
|
+
def create_singing_midi_file(sleep_duration)
|
33
|
+
# Play the chord progression
|
34
|
+
set_progression_on_track
|
35
|
+
|
36
|
+
# Play the note after waiting for a specified amount of time
|
37
|
+
add_seconds_of_rest(sleep_duration) do
|
38
|
+
play_notes_sequentially(notes, quarter_note_length)
|
39
|
+
end
|
40
|
+
|
41
|
+
write_sequence_to_file
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
attr_accessor :tempo, :progression, :notes, :info, :filename, :sequence, :track
|
47
|
+
|
48
|
+
def write_sequence_to_file
|
49
|
+
directory_name = File.dirname(filename)
|
50
|
+
FileUtils.mkdir_p(directory_name)
|
51
|
+
File.open(filename, "wb") { |file| sequence.write(file) }
|
52
|
+
end
|
53
|
+
|
54
|
+
def add_seconds_of_rest(seconds)
|
55
|
+
# Change tempo to 60 so that sleep_duration quarter notes corresponds to number of seconds
|
56
|
+
with_temporary_tempo_change(60) do
|
57
|
+
# Sleep for the requested duration
|
58
|
+
add_rest(seconds * quarter_note_length)
|
59
|
+
yield
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def play_notes_as_chord(the_notes, rest_after_chord)
|
64
|
+
the_notes.each do |note|
|
65
|
+
track.events << MIDI::NoteOn.new(0, note, 127, 0) # track number, note, volume, time to add
|
66
|
+
end
|
67
|
+
|
68
|
+
the_notes.each.with_index do |note, index|
|
69
|
+
time_interval = index.zero? ? rest_after_chord : 0
|
70
|
+
track.events << MIDI::NoteOff.new(0, note, 127, time_interval)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def play_notes_sequentially(the_notes, rest_between_notes)
|
75
|
+
the_notes.each do |note|
|
76
|
+
track.events << MIDI::NoteOn.new(0, note, 127, 0) # track number, note, volume, time to add
|
77
|
+
track.events << MIDI::NoteOff.new(0, note, 127, rest_between_notes)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def add_rest(duration)
|
82
|
+
track.events << MIDI::NoteOff.new(0, 0, 0, duration)
|
83
|
+
end
|
84
|
+
|
85
|
+
def with_temporary_tempo_change(new_tempo)
|
86
|
+
change_tempo(new_tempo)
|
87
|
+
yield
|
88
|
+
change_tempo(tempo)
|
89
|
+
end
|
90
|
+
|
91
|
+
def change_tempo(new_tempo)
|
92
|
+
track.events << MIDI::Tempo.new(MIDI::Tempo.bpm_to_mpq(new_tempo))
|
93
|
+
end
|
94
|
+
|
95
|
+
def generate_instrument_track
|
96
|
+
create_info_track
|
97
|
+
return create_instrument_track
|
98
|
+
end
|
99
|
+
|
100
|
+
def set_progression_on_track
|
101
|
+
# Create the progression
|
102
|
+
progression.each do |chord|
|
103
|
+
play_notes_as_chord(chord, quarter_note_length)
|
104
|
+
end
|
105
|
+
|
106
|
+
return track
|
107
|
+
end
|
108
|
+
|
109
|
+
def create_info_track
|
110
|
+
# Create a first track for the sequence. This holds tempo events and meta info.
|
111
|
+
track = MIDI::Track.new(sequence)
|
112
|
+
sequence.tracks << track
|
113
|
+
track.events << MIDI::Tempo.new(MIDI::Tempo.bpm_to_mpq(tempo))
|
114
|
+
track.events << MIDI::MetaEvent.new(MIDI::META_SEQ_NAME, info)
|
115
|
+
return track
|
116
|
+
end
|
117
|
+
|
118
|
+
def create_instrument_track
|
119
|
+
# Create a track to hold the notes. Add it to the sequence.
|
120
|
+
track = MIDI::Track.new(sequence)
|
121
|
+
sequence.tracks << track
|
122
|
+
track.name = info
|
123
|
+
track.instrument = MIDI::GM_PATCH_NAMES[0] # This is the piano patch
|
124
|
+
track.events << MIDI::ProgramChange.new(0, 1, 0) # Specify instrument as 2nd argument - see consts in midilib
|
125
|
+
return track
|
126
|
+
end
|
127
|
+
|
128
|
+
def quarter_note_length
|
129
|
+
# NOTE: magic constant will freeze strings, but apparently midilib needs "quarter" to NOT be frozen,
|
130
|
+
# so calling dup on it will generate a non-frozen version
|
131
|
+
return sequence.note_to_delta("quarter".dup)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "note"
|
4
|
+
|
5
|
+
module Fet
|
6
|
+
# Module in charge of handling music theory concepts
|
7
|
+
module MusicTheory
|
8
|
+
# All 12 degrees
|
9
|
+
DEGREES = ["1", "b2", "2", "b3", "3", "4", "b5", "5", "b6", "6", "b7", "7"].deep_freeze
|
10
|
+
|
11
|
+
# All 15 keys - 7 sharps...0 accidentals...7 flats.
|
12
|
+
MAJOR_KEYS = ["C#", "F#", "B", "E", "A", "D", "G", "C", "F", "Bb", "Eb", "Ab", "Db", "Gb", "Cb"].deep_freeze
|
13
|
+
MINOR_KEYS = ["A#", "D#", "G#", "C#", "F#", "B", "E", "A", "D", "G", "C", "F", "Bb", "Eb", "Ab"].deep_freeze
|
14
|
+
|
15
|
+
MODES_IN_ORDER_OF_DARKNESS = [
|
16
|
+
["lydian"],
|
17
|
+
["major", "ionian"],
|
18
|
+
["mixolydian"],
|
19
|
+
["dorian"],
|
20
|
+
["minor", "aeolian"],
|
21
|
+
["phrygian"],
|
22
|
+
["locrian"],
|
23
|
+
].deep_freeze
|
24
|
+
|
25
|
+
DEGREES_OF_MODE = {
|
26
|
+
["lydian"] => ["1", "2", "3", "#4", "5", "6", "7"],
|
27
|
+
["major", "ionian"] => ["1", "2", "3", "4", "5", "6", "7"],
|
28
|
+
["mixolydian"] => ["1", "2", "3", "4", "5", "6", "b7"],
|
29
|
+
["dorian"] => ["1", "2", "b3", "4", "5", "6", "b7"],
|
30
|
+
["minor", "aeolian"] => ["1", "2", "b3", "4", "5", "b6", "b7"],
|
31
|
+
["phrygian"] => ["1", "b2", "b3", "4", "5", "b6", "b7"],
|
32
|
+
["locrian"] => ["1", "b2", "b3", "4", "b5", "b6", "b7"],
|
33
|
+
}.deep_freeze
|
34
|
+
|
35
|
+
SEMITONES_FROM_C = {
|
36
|
+
"C" => 0,
|
37
|
+
"D" => 2,
|
38
|
+
"E" => 4,
|
39
|
+
"F" => 5,
|
40
|
+
"G" => 7,
|
41
|
+
"A" => 9,
|
42
|
+
"B" => 11,
|
43
|
+
}.deep_freeze
|
44
|
+
|
45
|
+
# NOTE: returns value from 0 to 11
|
46
|
+
def self.semitones_from_c(note_name)
|
47
|
+
note = Note.new(note_name)
|
48
|
+
return (SEMITONES_FROM_C[note.natural_note] + note.accidental_to_semitone_offset) % 12
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.degrees_of_mode(mode_name)
|
52
|
+
mode_info = DEGREES_OF_MODE.detect { |mode_names, _| mode_names.include?(mode_name) }
|
53
|
+
raise InvalidModeName.new(mode_name) if mode_info.nil?
|
54
|
+
|
55
|
+
return mode_info[1]
|
56
|
+
end
|
57
|
+
|
58
|
+
CIRCLE_OF_FIFTHS_WITHOUT_ACCIDENTALS = ["F", "C", "G", "D", "A", "E", "B"].deep_freeze
|
59
|
+
CIRCLE_OF_FIFTHS = [
|
60
|
+
*CIRCLE_OF_FIFTHS_WITHOUT_ACCIDENTALS.map { |note| Note.new(note).flattened_note.flattened_note.flattened_note.full_note },
|
61
|
+
*CIRCLE_OF_FIFTHS_WITHOUT_ACCIDENTALS.map { |note| Note.new(note).flattened_note.flattened_note.full_note },
|
62
|
+
*CIRCLE_OF_FIFTHS_WITHOUT_ACCIDENTALS.map { |note| Note.new(note).flattened_note.full_note },
|
63
|
+
*CIRCLE_OF_FIFTHS_WITHOUT_ACCIDENTALS,
|
64
|
+
*CIRCLE_OF_FIFTHS_WITHOUT_ACCIDENTALS.map { |note| Note.new(note).sharpened_note.full_note },
|
65
|
+
*CIRCLE_OF_FIFTHS_WITHOUT_ACCIDENTALS.map { |note| Note.new(note).sharpened_note.sharpened_note.full_note },
|
66
|
+
*CIRCLE_OF_FIFTHS_WITHOUT_ACCIDENTALS.map { |note| Note.new(note).sharpened_note.sharpened_note.sharpened_note.full_note },
|
67
|
+
].deep_freeze
|
68
|
+
|
69
|
+
ORDERED_NATURAL_NOTES = CIRCLE_OF_FIFTHS_WITHOUT_ACCIDENTALS.sort.deep_freeze
|
70
|
+
SEMITONES_TO_NEXT_NATURAL_NOTE = {
|
71
|
+
"A" => 2,
|
72
|
+
"B" => 1,
|
73
|
+
"C" => 2,
|
74
|
+
"D" => 2,
|
75
|
+
"E" => 1,
|
76
|
+
"F" => 2,
|
77
|
+
"G" => 2,
|
78
|
+
}.deep_freeze
|
79
|
+
|
80
|
+
# A aeolian -> ["A", "B", "C", "D", "E", "F", "G"]
|
81
|
+
def self.notes_of_mode(note_name, mode_name)
|
82
|
+
relative_major_note_name = relative_major(note_name, mode_name)
|
83
|
+
|
84
|
+
index = CIRCLE_OF_FIFTHS.index(relative_major_note_name)
|
85
|
+
result = CIRCLE_OF_FIFTHS[(index - 1)..(index + 5)]
|
86
|
+
raise UnsupportedRootName.new(note_name) unless result.size == 7
|
87
|
+
|
88
|
+
result = result.sort
|
89
|
+
return result.rotate(result.index(note_name))
|
90
|
+
end
|
91
|
+
|
92
|
+
# A aeolian -> C
|
93
|
+
def self.relative_major(note_name, mode_name)
|
94
|
+
note = Note.new(note_name)
|
95
|
+
index = CIRCLE_OF_FIFTHS.index(note.full_note)
|
96
|
+
raise UnsupportedRootName.new(note_name) if index.nil?
|
97
|
+
|
98
|
+
result_index = index - mode_offset_from_major(mode_name)
|
99
|
+
raise UnsupportedRootName.new(note_name) if result_index.negative?
|
100
|
+
|
101
|
+
result = CIRCLE_OF_FIFTHS[index - mode_offset_from_major(mode_name)]
|
102
|
+
return result
|
103
|
+
end
|
104
|
+
|
105
|
+
class << self
|
106
|
+
private
|
107
|
+
|
108
|
+
def mode_offset_from_major(mode_name)
|
109
|
+
MODES_IN_ORDER_OF_DARKNESS.each.with_index do |mode_names, index|
|
110
|
+
return (index - 1) if mode_names.include?(mode_name)
|
111
|
+
end
|
112
|
+
raise InvalidModeName.new(mode_name)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|