wtf_chord 0.6.0 → 0.7.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 +4 -4
- data/README.md +22 -0
- data/lib/wtf_chord.rb +5 -0
- data/lib/wtf_chord/chord.rb +25 -4
- data/lib/wtf_chord/cli.rb +10 -3
- data/lib/wtf_chord/collectors.rb +10 -0
- data/lib/wtf_chord/collectors/generic.rb +33 -0
- data/lib/wtf_chord/collectors/guitar.rb +116 -0
- data/lib/wtf_chord/collectors/piano.rb +36 -0
- data/lib/wtf_chord/fingering.rb +4 -0
- data/lib/wtf_chord/formatter.rb +2 -0
- data/lib/wtf_chord/formatters/base.rb +5 -3
- data/lib/wtf_chord/formatters/piano.rb +7 -11
- data/lib/wtf_chord/formatters/piano1.rb +46 -0
- data/lib/wtf_chord/fretboard.rb +4 -2
- data/lib/wtf_chord/guitar_string.rb +11 -6
- data/lib/wtf_chord/instrument_pitch.rb +53 -0
- data/lib/wtf_chord/keyboard.rb +9 -36
- data/lib/wtf_chord/keyboard_presentation.rb +90 -0
- data/lib/wtf_chord/piano_key.rb +19 -0
- data/lib/wtf_chord/scale.rb +15 -5
- data/lib/wtf_chord/version.rb +1 -1
- metadata +10 -3
- data/lib/wtf_chord/fingerings_generator.rb +0 -120
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8d764a30038a11df351e6d3d9dfce09084aa89af352d92b36677ed5eac31e425
|
4
|
+
data.tar.gz: 0c713ee0f4e3b36720014f56c1c55ca6686981b49e228402d8c2f826463affce
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e83f9de2c3913c4206d7d4f424f46f1082fcd16a50f8f19d4d78f2d83f3a66f610d23852ac66b890190f444db14e74d3fc9249514e6ece64626685c4c7434de2
|
7
|
+
data.tar.gz: 3ac9226adb773858a51c474c0f1b299e3bf2601ce567d9967c96b297ba4b0895da08c6b50e404221ba636be01d83740fea20b3f5684ebb24a2053b4b81235425
|
data/README.md
CHANGED
@@ -91,6 +91,28 @@ You can get simpler output, using the `--output` option:
|
|
91
91
|
[ 10 8 7 7 6 10 ]
|
92
92
|
[ 10 8 7 7 10 10 ]
|
93
93
|
|
94
|
+
Starting with 0.7.0, we have the very nice piano formatting:
|
95
|
+
|
96
|
+
$ wtfchord Fdim -n 2 --output=piano
|
97
|
+
F1 - G♯1 - B1
|
98
|
+
┌─┬─┬┬─┬─┬─┬─┬┬─┬┬─┬─┐
|
99
|
+
│ │ ││ │ │ │ ││█││ │ │
|
100
|
+
│ └┬┘└┬┘ │ └┬┘└┬┘└┬┘ │
|
101
|
+
│ │ │ │▐▌│ │ │▐▌│
|
102
|
+
└──┴──┴──┴──┴──┴──┴──┘
|
103
|
+
↑
|
104
|
+
Ⅰ
|
105
|
+
|
106
|
+
|
107
|
+
G♯1 - B1 - F2
|
108
|
+
┌─┬─┬┬─┬─┬─┬─┬┬─┬┬─┬─┬─┬─┬┬─┬─┬─┬─┬┬─┬┬─┬─┐
|
109
|
+
│ │ ││ │ │ │ ││█││ │ │ │ ││ │ │ │ ││ ││ │ │
|
110
|
+
│ └┬┘└┬┘ │ └┬┘└┬┘└┬┘ │ └┬┘└┬┘ │ └┬┘└┬┘└┬┘ │
|
111
|
+
│ │ │ │ │ │ │▐▌│ │ │ │▐▌│ │ │ │
|
112
|
+
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
|
113
|
+
↑ ↑
|
114
|
+
Ⅰ Ⅱ
|
115
|
+
|
94
116
|
## Development
|
95
117
|
|
96
118
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. Run `bundle exec wtf_chord` to use the gem in this directory, ignoring other installed copies of this gem.
|
data/lib/wtf_chord.rb
CHANGED
@@ -1,8 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "wtf_chord/scale"
|
2
4
|
require "wtf_chord/rules"
|
3
5
|
require "wtf_chord/version"
|
4
6
|
|
5
7
|
module WTFChord
|
8
|
+
autoload :Fretboard, "wtf_chord/fretboard"
|
9
|
+
autoload :Keyboard, "wtf_chord/keyboard"
|
10
|
+
|
6
11
|
DEFAULTS = {
|
7
12
|
:rules_file => File.expand_path("../wtf_chord.yml", __FILE__)
|
8
13
|
}.freeze
|
data/lib/wtf_chord/chord.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'wtf_chord/fingering'
|
2
|
-
require 'wtf_chord/
|
4
|
+
require 'wtf_chord/collectors'
|
3
5
|
|
4
6
|
module WTFChord
|
5
7
|
class Chord
|
@@ -9,7 +11,7 @@ module WTFChord
|
|
9
11
|
|
10
12
|
def initialize(note, name)
|
11
13
|
@pitch = note.is_a?(Pitch) ? note : WTFChord.note(note)
|
12
|
-
@name = "#{@pitch.key}#{name}"
|
14
|
+
@name = "#{@pitch.key}#{name}"
|
13
15
|
@steps = Array(WTFChord.rules[name])
|
14
16
|
@notes = @steps.map { |dist| (@pitch + dist).note }.tap(&:uniq!)
|
15
17
|
|
@@ -18,8 +20,12 @@ module WTFChord
|
|
18
20
|
end
|
19
21
|
end
|
20
22
|
|
21
|
-
def fingerings(limit = nil)
|
22
|
-
|
23
|
+
def fingerings(limit = nil, collector: Collectors::Guitar)
|
24
|
+
collector.new(self).call[0, limit || 5]
|
25
|
+
end
|
26
|
+
|
27
|
+
def tone
|
28
|
+
bass || original_bass
|
23
29
|
end
|
24
30
|
|
25
31
|
def third_tone
|
@@ -30,10 +36,25 @@ module WTFChord
|
|
30
36
|
!!@bass
|
31
37
|
end
|
32
38
|
|
39
|
+
def size
|
40
|
+
bass? ? -~@notes.size : @notes.size
|
41
|
+
end
|
42
|
+
|
33
43
|
def original_bass
|
34
44
|
@notes[0]
|
35
45
|
end
|
36
46
|
|
47
|
+
def === other
|
48
|
+
case other
|
49
|
+
when InstrumentPitch
|
50
|
+
self === other.note
|
51
|
+
when Note
|
52
|
+
notes.include?(other) || (bass? && bass == other)
|
53
|
+
else
|
54
|
+
super
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
37
58
|
def inspect
|
38
59
|
"#{name} (#{@notes.map(&:key) * ' - '})"
|
39
60
|
end
|
data/lib/wtf_chord/cli.rb
CHANGED
@@ -12,10 +12,17 @@ module WTFChord
|
|
12
12
|
|
13
13
|
debug { "Output using formatter: #{formatter.formatter.to_s}\n" }
|
14
14
|
|
15
|
-
options['amount'] = 1 if options['output'] == '
|
15
|
+
options['amount'] = 1 if options['output'] == 'piano1'
|
16
16
|
|
17
|
-
|
18
|
-
|
17
|
+
collector =
|
18
|
+
case formatter.name when 'piano', 'piano1'
|
19
|
+
Collectors::Piano
|
20
|
+
else
|
21
|
+
Collectors::Guitar
|
22
|
+
end
|
23
|
+
|
24
|
+
fingerings = chord.fingerings(options['amount'], collector: collector)
|
25
|
+
puts formatter[*fingerings].uniq * formatter.separator
|
19
26
|
end
|
20
27
|
|
21
28
|
version VERSION
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WTFChord
|
4
|
+
class Chord
|
5
|
+
end
|
6
|
+
|
7
|
+
module Collectors
|
8
|
+
class Generic < DelegateClass(Chord)
|
9
|
+
singleton_class.attr_accessor :max_dist
|
10
|
+
|
11
|
+
def fingerings
|
12
|
+
@fingerings ||= []
|
13
|
+
end
|
14
|
+
|
15
|
+
def call
|
16
|
+
reset!
|
17
|
+
collect!
|
18
|
+
end
|
19
|
+
|
20
|
+
def reset!
|
21
|
+
fingerings.clear
|
22
|
+
end
|
23
|
+
|
24
|
+
def collect!
|
25
|
+
raise NotImplementedError
|
26
|
+
end
|
27
|
+
|
28
|
+
def max_dist
|
29
|
+
self.class.max_dist
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WTFChord
|
4
|
+
module Collectors
|
5
|
+
class Guitar < Generic
|
6
|
+
self.max_dist = 5
|
7
|
+
|
8
|
+
MAX_FRET = 7
|
9
|
+
|
10
|
+
def collect!
|
11
|
+
(0...MAX_FRET).each do |from|
|
12
|
+
to = from + max_dist
|
13
|
+
generate(from...to) do |variant|
|
14
|
+
fingerings << set_bass(variant) if filter_variant(variant)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
fingerings.uniq!
|
19
|
+
fingerings.sort_by!(&:complexity)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def generate(fret_range)
|
25
|
+
combinations = WTFChord.guitar.strings.map.with_index do |s, index|
|
26
|
+
fret_range.
|
27
|
+
select { |dist| pitch = s.original + dist; has_note?(pitch) }.
|
28
|
+
tap { |frets| frets << nil if frets.size == 0 }
|
29
|
+
end
|
30
|
+
|
31
|
+
combinations.inject(&:product).each do |fingers|
|
32
|
+
fingers.flatten!
|
33
|
+
|
34
|
+
Fingering.new(WTFChord.guitar, fingers) do |variant|
|
35
|
+
adjust(variant)
|
36
|
+
yield(variant)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def filter_variant(variant)
|
42
|
+
used_strings = variant.used_strings
|
43
|
+
used_notes = used_strings.map(&:note)
|
44
|
+
tones_count = notes.map { |n| used_notes.count(n) }
|
45
|
+
|
46
|
+
!fingerings.include?(variant) &&
|
47
|
+
basetone?(used_strings[0]) &&
|
48
|
+
(
|
49
|
+
(third?(used_strings[1]) || third?(used_strings[2])) ||
|
50
|
+
(last?(used_strings[1]) || last?(used_strings[2]))
|
51
|
+
) &&
|
52
|
+
all_notes?(used_notes) &&
|
53
|
+
used_notes.each_cons(2).none? { |(l, r)| l == r } &&
|
54
|
+
tones_count.all? { |n| tones_count[0] >= n || tones_count[notes.index(third_tone)] >= n } &&
|
55
|
+
variant.complexity <= 2.25
|
56
|
+
end
|
57
|
+
|
58
|
+
def adjust(fingering)
|
59
|
+
while (string = fingering.used_strings[0]) && !basetone?(string)
|
60
|
+
string.dead if !string.dead?
|
61
|
+
break if fingering.used_strings.size == 4
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def set_bass(variant)
|
66
|
+
if bass?
|
67
|
+
variant.find_used_string_for(original_bass) do |idx, bass_string|
|
68
|
+
try_set_bass_on(variant, idx) or begin
|
69
|
+
4.times do |i|
|
70
|
+
next if i == idx
|
71
|
+
if try_set_bass_on(variant, i)
|
72
|
+
bass_string.dead
|
73
|
+
break
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
variant
|
81
|
+
end
|
82
|
+
|
83
|
+
def try_set_bass_on(variant, idx)
|
84
|
+
bass_string = variant.strings[idx].dup
|
85
|
+
distance = bass_string.distance_to(bass)
|
86
|
+
|
87
|
+
if distance >= 0 && distance < 5 && (distance - variant.min_fret) > -3
|
88
|
+
variant.extra_complexity += 0.5 if distance < variant.min_fret
|
89
|
+
variant.strings[idx].hold_on(distance)
|
90
|
+
true
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def all_notes?(used_notes)
|
95
|
+
notes.all? { |n| used_notes.include?(n) }
|
96
|
+
end
|
97
|
+
|
98
|
+
def has_note?(value)
|
99
|
+
value = value.note if !value.is_a?(Note) && value.respond_to?(:note)
|
100
|
+
notes.include?(value)
|
101
|
+
end
|
102
|
+
|
103
|
+
def basetone?(string)
|
104
|
+
string && string.note == notes[0]
|
105
|
+
end
|
106
|
+
|
107
|
+
def third?(string)
|
108
|
+
string && (string.note == third_tone)
|
109
|
+
end
|
110
|
+
|
111
|
+
def last?(string)
|
112
|
+
string && (string.note == notes[-1])
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WTFChord
|
4
|
+
module Collectors
|
5
|
+
class Piano < Generic
|
6
|
+
self.max_dist = 12
|
7
|
+
MAX_OCTAVE = 5
|
8
|
+
|
9
|
+
CheckDist = -> ((a, b)) { (a.to_i - b.to_i).abs <= max_dist }
|
10
|
+
|
11
|
+
FilterProc = -> (actual_size, notes) {
|
12
|
+
notes.uniq(&:key).size == actual_size && notes.sort.each_cons(2).all?(&CheckDist)
|
13
|
+
}.curry(2).freeze
|
14
|
+
|
15
|
+
Distances = -> (notes) {
|
16
|
+
notes.each_cons(2).map { |(a, b)| (a.to_i - b.to_i).abs }
|
17
|
+
}
|
18
|
+
|
19
|
+
FirstTone = -> (tone, notes) {
|
20
|
+
Distances[notes]
|
21
|
+
}.curry(2).freeze
|
22
|
+
|
23
|
+
def collect!
|
24
|
+
@keyboard = Keyboard.new(1..MAX_OCTAVE)
|
25
|
+
|
26
|
+
@fingerings = @keyboard.keys.
|
27
|
+
grep(__getobj__).
|
28
|
+
combination(size).
|
29
|
+
filter(&FilterProc[size])
|
30
|
+
@fingerings.uniq! { |x| x.map(&:key) }
|
31
|
+
@fingerings.sort_by!(&FirstTone[tone])
|
32
|
+
@fingerings
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/wtf_chord/fingering.rb
CHANGED
data/lib/wtf_chord/formatter.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'forwardable'
|
2
4
|
|
3
5
|
module WTFChord
|
@@ -9,7 +11,7 @@ module WTFChord
|
|
9
11
|
def_delegators :@fret, :strings
|
10
12
|
|
11
13
|
def self.separator
|
12
|
-
"\n"
|
14
|
+
"\n"
|
13
15
|
end
|
14
16
|
|
15
17
|
def self.to_proc
|
@@ -31,8 +33,8 @@ module WTFChord
|
|
31
33
|
def draw
|
32
34
|
end
|
33
35
|
|
34
|
-
def
|
35
|
-
strings.reject(&:dead?)
|
36
|
+
def actual_strings
|
37
|
+
strings.reject(&:dead?)
|
36
38
|
end
|
37
39
|
|
38
40
|
def with_rates?
|
@@ -1,24 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "wtf_chord/formatters/piano1"
|
3
4
|
require "wtf_chord/keyboard"
|
4
5
|
|
5
6
|
module WTFChord
|
6
7
|
module Formatters
|
7
|
-
class Piano <
|
8
|
+
class Piano < Piano1
|
8
9
|
def draw
|
9
10
|
[
|
10
|
-
|
11
|
-
|
12
|
-
].join("\n
|
11
|
+
pressed_keys.map(&:to_s).join(" - "),
|
12
|
+
keyboard_presentation
|
13
|
+
].join("\n")
|
13
14
|
end
|
14
15
|
|
15
|
-
def
|
16
|
-
|
17
|
-
unless positions.include?(string.note.position)
|
18
|
-
positions << string.note.position
|
19
|
-
notes << string.note
|
20
|
-
end
|
21
|
-
end[1]
|
16
|
+
def keyboard_presentation
|
17
|
+
KeyboardPresentation.press(*pressed_keys)
|
22
18
|
end
|
23
19
|
end
|
24
20
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "wtf_chord/keyboard_presentation"
|
4
|
+
|
5
|
+
module WTFChord
|
6
|
+
module Formatters
|
7
|
+
class Piano1
|
8
|
+
def self.separator
|
9
|
+
"\n"
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.to_proc
|
13
|
+
proc { |keys| new(keys).draw }
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.with_rates(*)
|
17
|
+
yield(self)
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :pressed_keys
|
21
|
+
|
22
|
+
def initialize(keys)
|
23
|
+
@pressed_keys = keys
|
24
|
+
end
|
25
|
+
|
26
|
+
def draw
|
27
|
+
[
|
28
|
+
unique_notes.sort.join(" - "),
|
29
|
+
keyboard_presentation
|
30
|
+
].join("\n\n")
|
31
|
+
end
|
32
|
+
|
33
|
+
def actual_notes
|
34
|
+
actual_strings.map(&:note)
|
35
|
+
end
|
36
|
+
|
37
|
+
def unique_notes
|
38
|
+
actual_notes.uniq(&:position)
|
39
|
+
end
|
40
|
+
|
41
|
+
def keyboard_presentation
|
42
|
+
KeyboardPresentation.press(*unique_notes.map(&:position))
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/wtf_chord/fretboard.rb
CHANGED
@@ -1,14 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'wtf_chord/guitar_string'
|
2
4
|
|
3
5
|
module WTFChord
|
4
6
|
class Fretboard
|
5
|
-
DEAD = "×"
|
7
|
+
DEAD = "×"
|
6
8
|
|
7
9
|
attr_reader :strings, :capo
|
8
10
|
|
9
11
|
def initialize(*strings)
|
10
12
|
@capo = 0
|
11
|
-
@strings = strings.map
|
13
|
+
@strings = strings.map { |string| GuitarString.new(string, @capo) }
|
12
14
|
end
|
13
15
|
|
14
16
|
def [] idx
|
@@ -1,14 +1,19 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'wtf_chord/instrument_pitch'
|
2
4
|
|
3
5
|
module WTFChord
|
4
|
-
class GuitarString <
|
6
|
+
class GuitarString < InstrumentPitch
|
5
7
|
attr_reader :capo, :fret, :original
|
6
8
|
|
7
9
|
def initialize(note, capo = 0)
|
8
|
-
@original = WTFChord.note(note)
|
9
10
|
@fret = nil
|
10
11
|
@capo = capo
|
11
|
-
super(
|
12
|
+
super(note)
|
13
|
+
end
|
14
|
+
|
15
|
+
def init_pitch
|
16
|
+
@original + capo
|
12
17
|
end
|
13
18
|
|
14
19
|
def initialize_dup(other)
|
@@ -58,8 +63,8 @@ module WTFChord
|
|
58
63
|
def <=> other
|
59
64
|
return if dead?
|
60
65
|
case other
|
61
|
-
when Integer
|
62
|
-
when GuitarString
|
66
|
+
when Integer then @fret <=> other
|
67
|
+
when GuitarString then @fret <=> other.fret
|
63
68
|
end
|
64
69
|
end
|
65
70
|
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'wtf_chord/pitch'
|
4
|
+
|
5
|
+
module WTFChord
|
6
|
+
class InstrumentPitch < DelegateClass(Pitch)
|
7
|
+
attr_reader :interval, :original
|
8
|
+
|
9
|
+
def initialize(note)
|
10
|
+
@original = WTFChord.note(note)
|
11
|
+
super(init_pitch)
|
12
|
+
end
|
13
|
+
|
14
|
+
def init_pitch
|
15
|
+
@original
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize_dup(other)
|
19
|
+
super
|
20
|
+
__setobj__(@original.dup)
|
21
|
+
end
|
22
|
+
|
23
|
+
def dead
|
24
|
+
end
|
25
|
+
|
26
|
+
def dead?
|
27
|
+
raise NotImplementedError
|
28
|
+
end
|
29
|
+
|
30
|
+
def holded?
|
31
|
+
raise NotImplementedError
|
32
|
+
end
|
33
|
+
|
34
|
+
def distance_to(pitch)
|
35
|
+
pos = 0
|
36
|
+
pos += 1 while (original + pos) != pitch
|
37
|
+
pos
|
38
|
+
end
|
39
|
+
|
40
|
+
def code
|
41
|
+
to_i
|
42
|
+
end
|
43
|
+
|
44
|
+
def <=> other
|
45
|
+
case other
|
46
|
+
when Integer
|
47
|
+
to_i <=> other
|
48
|
+
when InstrumentPitch
|
49
|
+
to_i <=> other.to_i
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/lib/wtf_chord/keyboard.rb
CHANGED
@@ -1,47 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "wtf_chord/piano_key"
|
4
|
+
|
3
5
|
module WTFChord
|
4
6
|
class Keyboard
|
5
|
-
|
6
|
-
┌─┬─┬┬─┬─┬─┬─┬┬─┬┬─┬─┐
|
7
|
-
│ │ ││ │ │ │ ││ ││ │ │
|
8
|
-
│ └┬┘└┬┘ │ └┬┘└┬┘└┬┘ │
|
9
|
-
│ │ │ │ │ │ │ │
|
10
|
-
└──┴──┴──┴──┴──┴──┴──┘
|
11
|
-
EOF
|
7
|
+
attr_reader :keys
|
12
8
|
|
13
|
-
|
14
|
-
class Key
|
15
|
-
singleton_class.attr_accessor :select_symbol
|
16
|
-
singleton_class.alias_method :[], :new
|
9
|
+
KEYS = %w(C C# D D# E F F# G G# A Bb B)
|
17
10
|
|
18
|
-
|
19
|
-
|
20
|
-
frame
|
21
|
-
end
|
11
|
+
def initialize(octaves = 1..5)
|
12
|
+
@keys = octaves.to_a.product(KEYS).map { |(o, k)| PianoKey.new("#{k}#{o}") }
|
22
13
|
end
|
23
14
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
KEYS = {
|
28
|
-
1 => W[70...72],
|
29
|
-
2 => B[26...27],
|
30
|
-
3 => W[73...75],
|
31
|
-
4 => B[29...30],
|
32
|
-
5 => W[76...78],
|
33
|
-
6 => W[79...81],
|
34
|
-
7 => B[35...36],
|
35
|
-
8 => W[82...84],
|
36
|
-
9 => B[38...39],
|
37
|
-
10 => W[85...87],
|
38
|
-
11 => B[41...42],
|
39
|
-
12 => W[88...90]
|
40
|
-
}
|
41
|
-
|
42
|
-
def self.press(*positions)
|
43
|
-
positions.each_with_object(FRAME.dup) do |pos, frame|
|
44
|
-
KEYS.fetch(pos).select(frame)
|
15
|
+
def [] idx
|
16
|
+
case idx
|
17
|
+
when 1..@keys.size then @keys[~-idx]
|
45
18
|
end
|
46
19
|
end
|
47
20
|
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'wtf_chord/helpers/roman_numbers_helper'
|
4
|
+
|
5
|
+
module WTFChord
|
6
|
+
class KeyboardPresentation
|
7
|
+
FRAME = <<~EOF
|
8
|
+
┌─┬─┬┬─┬─┬─┬─┬┬─┬┬─┬─┐┬
|
9
|
+
│ │ ││ │ │ │ ││ ││ │ ││
|
10
|
+
│ └┬┘└┬┘ │ └┬┘└┬┘└┬┘ ││
|
11
|
+
│ │ │ │ │ │ │ ││
|
12
|
+
└──┴──┴──┴──┴──┴──┴──┘┴
|
13
|
+
↑ |
|
14
|
+
%s |
|
15
|
+
|
|
16
|
+
EOF
|
17
|
+
|
18
|
+
include RomanNumbersHelper
|
19
|
+
|
20
|
+
KeyStruct = Struct.new(:index) do
|
21
|
+
singleton_class.attr_accessor :mark
|
22
|
+
singleton_class.alias_method :[], :new
|
23
|
+
|
24
|
+
def call(frame)
|
25
|
+
frame[index, mark.length] = mark
|
26
|
+
end
|
27
|
+
|
28
|
+
private def mark
|
29
|
+
self.class.mark
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
Key = -> (mark) { Class.new(KeyStruct).tap { |c| c.mark = mark } }
|
34
|
+
W, B = Key["▐▌"], Key["█"]
|
35
|
+
|
36
|
+
KEYS = [W[73], B[27], W[76], B[30], W[79], W[82], B[36], W[85], B[39], W[88], B[42], W[91]].
|
37
|
+
map.with_index(1).to_h.invert.freeze
|
38
|
+
|
39
|
+
def self.press(*positions)
|
40
|
+
if positions.all? { |v| Integer === v }
|
41
|
+
new.press(*positions)
|
42
|
+
else
|
43
|
+
positions.uniq(&:key).group_by(&:octave).inject(nil) do |a, (octave, strings)|
|
44
|
+
pos = strings.map { |s| s.note.position }
|
45
|
+
kb = new(octave).press(*pos)
|
46
|
+
a.nil? ? kb : a + kb
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
attr_reader :frame
|
52
|
+
|
53
|
+
def initialize(octave = 1)
|
54
|
+
@octave = octave
|
55
|
+
@frame = FRAME.dup % romanize(octave)
|
56
|
+
end
|
57
|
+
|
58
|
+
def +@
|
59
|
+
shift!
|
60
|
+
self
|
61
|
+
end
|
62
|
+
|
63
|
+
def +(other)
|
64
|
+
pop!
|
65
|
+
@frame.replace(@frame.lines(chomp: true).zip((+other).frame.lines).map(&:join).join)
|
66
|
+
self
|
67
|
+
end
|
68
|
+
|
69
|
+
def press(*positions)
|
70
|
+
positions.each { |pos| KEYS.fetch(pos).(@frame) }
|
71
|
+
self
|
72
|
+
end
|
73
|
+
|
74
|
+
def to_s
|
75
|
+
finalize!
|
76
|
+
end
|
77
|
+
|
78
|
+
def finalize!
|
79
|
+
@frame.gsub("|", " ").gsub(/[^\s](?=\n)/, "")
|
80
|
+
end
|
81
|
+
|
82
|
+
def pop!
|
83
|
+
@frame.replace(@frame.lines.each { |line| line[-3, 1] = "" }.join)
|
84
|
+
end
|
85
|
+
|
86
|
+
def shift!
|
87
|
+
@frame.replace(@frame.lines.each { |line| line[0, 1] = "" }.join)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "wtf_chord/instrument_pitch"
|
4
|
+
|
5
|
+
module WTFChord
|
6
|
+
class PianoKey < InstrumentPitch
|
7
|
+
def pressed?
|
8
|
+
!!@pressed
|
9
|
+
end
|
10
|
+
|
11
|
+
def dead?
|
12
|
+
!pressed?
|
13
|
+
end
|
14
|
+
|
15
|
+
def press!
|
16
|
+
@pressed = true
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/wtf_chord/scale.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'wtf_chord/note'
|
2
4
|
require 'wtf_chord/pitch'
|
3
5
|
require 'wtf_chord/fretboard'
|
@@ -30,9 +32,9 @@ module WTFChord
|
|
30
32
|
|
31
33
|
private_constant :ScaleArray
|
32
34
|
|
33
|
-
FLAT ||= "b"
|
34
|
-
SHARP ||= "#"
|
35
|
-
SIGNS ||= { FLAT => "\u266D"
|
35
|
+
FLAT ||= "b"
|
36
|
+
SHARP ||= "#"
|
37
|
+
SIGNS ||= { FLAT => "\u266D", SHARP => "\u266F" }.freeze
|
36
38
|
|
37
39
|
DIATONIC = {
|
38
40
|
"C" => "Do",
|
@@ -43,7 +45,7 @@ module WTFChord
|
|
43
45
|
"A" => "La",
|
44
46
|
"B" => "Si",
|
45
47
|
"H" => "Si"
|
46
|
-
}
|
48
|
+
}
|
47
49
|
|
48
50
|
SCALE ||= begin
|
49
51
|
chromatic_scale = %W(
|
@@ -60,7 +62,7 @@ module WTFChord
|
|
60
62
|
Bb|A#
|
61
63
|
B|H
|
62
64
|
)
|
63
|
-
ScaleArray.build(*chromatic_scale)
|
65
|
+
ScaleArray.build(*chromatic_scale)
|
64
66
|
end
|
65
67
|
|
66
68
|
def self.guitar=(guitar)
|
@@ -70,4 +72,12 @@ module WTFChord
|
|
70
72
|
def self.guitar
|
71
73
|
Thread.current[:wtf_guitar] ||= Fretboard.new(*%w(E2 A2 D3 G3 B3 E4))
|
72
74
|
end
|
75
|
+
|
76
|
+
def self.piano=(piano)
|
77
|
+
Thread.current[:wtf_piano] = piano
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.piano
|
81
|
+
Thread.current[:wtf_piano] ||= Keyboard.new
|
82
|
+
end
|
73
83
|
end
|
data/lib/wtf_chord/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: wtf_chord
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Anton
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-03-
|
11
|
+
date: 2021-03-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: methadone
|
@@ -94,19 +94,26 @@ files:
|
|
94
94
|
- lib/wtf_chord.yml
|
95
95
|
- lib/wtf_chord/chord.rb
|
96
96
|
- lib/wtf_chord/cli.rb
|
97
|
+
- lib/wtf_chord/collectors.rb
|
98
|
+
- lib/wtf_chord/collectors/generic.rb
|
99
|
+
- lib/wtf_chord/collectors/guitar.rb
|
100
|
+
- lib/wtf_chord/collectors/piano.rb
|
97
101
|
- lib/wtf_chord/complexity_counter.rb
|
98
102
|
- lib/wtf_chord/fingering.rb
|
99
|
-
- lib/wtf_chord/fingerings_generator.rb
|
100
103
|
- lib/wtf_chord/formatter.rb
|
101
104
|
- lib/wtf_chord/formatters/base.rb
|
102
105
|
- lib/wtf_chord/formatters/default.rb
|
103
106
|
- lib/wtf_chord/formatters/piano.rb
|
107
|
+
- lib/wtf_chord/formatters/piano1.rb
|
104
108
|
- lib/wtf_chord/formatters/simple.rb
|
105
109
|
- lib/wtf_chord/fretboard.rb
|
106
110
|
- lib/wtf_chord/guitar_string.rb
|
107
111
|
- lib/wtf_chord/helpers/roman_numbers_helper.rb
|
112
|
+
- lib/wtf_chord/instrument_pitch.rb
|
108
113
|
- lib/wtf_chord/keyboard.rb
|
114
|
+
- lib/wtf_chord/keyboard_presentation.rb
|
109
115
|
- lib/wtf_chord/note.rb
|
116
|
+
- lib/wtf_chord/piano_key.rb
|
110
117
|
- lib/wtf_chord/pitch.rb
|
111
118
|
- lib/wtf_chord/rules.rb
|
112
119
|
- lib/wtf_chord/scale.rb
|
@@ -1,120 +0,0 @@
|
|
1
|
-
module WTFChord
|
2
|
-
class Chord
|
3
|
-
end
|
4
|
-
|
5
|
-
class FingeringsGenerator < DelegateClass(Chord)
|
6
|
-
MAX_DIST = 5
|
7
|
-
MAX_FRET = 7
|
8
|
-
|
9
|
-
def fingerings
|
10
|
-
@fingerings ||= []
|
11
|
-
end
|
12
|
-
|
13
|
-
def call
|
14
|
-
fingerings.clear
|
15
|
-
|
16
|
-
(0...MAX_FRET).each do |from|
|
17
|
-
to = from + MAX_DIST
|
18
|
-
generate(from...to) do |variant|
|
19
|
-
fingerings << set_bass(variant) if filter_variant(variant)
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
fingerings.uniq!
|
24
|
-
fingerings.sort_by!(&:complexity)
|
25
|
-
end
|
26
|
-
|
27
|
-
private
|
28
|
-
|
29
|
-
def generate(fret_range)
|
30
|
-
combinations = WTFChord.guitar.strings.map.with_index do |s, index|
|
31
|
-
fret_range.
|
32
|
-
select { |dist| pitch = s.original + dist; has_note?(pitch) }.
|
33
|
-
tap { |frets| frets << nil if frets.size == 0 }
|
34
|
-
end
|
35
|
-
|
36
|
-
combinations.inject(&:product).each do |fingers|
|
37
|
-
fingers.flatten!
|
38
|
-
|
39
|
-
Fingering.new(WTFChord.guitar, fingers) do |variant|
|
40
|
-
adjust(variant)
|
41
|
-
yield(variant)
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
def filter_variant(variant)
|
47
|
-
used_strings = variant.used_strings
|
48
|
-
used_notes = used_strings.map(&:note)
|
49
|
-
tones_count = notes.map { |n| used_notes.count(n) }
|
50
|
-
|
51
|
-
!fingerings.include?(variant) &&
|
52
|
-
basetone?(used_strings[0]) &&
|
53
|
-
(
|
54
|
-
(third?(used_strings[1]) || third?(used_strings[2])) ||
|
55
|
-
(last?(used_strings[1]) || last?(used_strings[2]))
|
56
|
-
) &&
|
57
|
-
all_notes?(used_notes) &&
|
58
|
-
used_notes.each_cons(2).none? { |(l, r)| l == r } &&
|
59
|
-
tones_count.all? { |n| tones_count[0] >= n || tones_count[notes.index(third_tone)] >= n } &&
|
60
|
-
variant.complexity <= 2.25
|
61
|
-
end
|
62
|
-
|
63
|
-
def adjust(fingering)
|
64
|
-
while (string = fingering.used_strings[0]) && !basetone?(string)
|
65
|
-
string.dead if !string.dead?
|
66
|
-
break if fingering.used_strings.size == 4
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
def set_bass(variant)
|
71
|
-
if bass?
|
72
|
-
variant.find_used_string_for(original_bass) do |idx, bass_string|
|
73
|
-
try_set_bass_on(variant, idx) or begin
|
74
|
-
4.times do |i|
|
75
|
-
next if i == idx
|
76
|
-
if try_set_bass_on(variant, i)
|
77
|
-
bass_string.dead
|
78
|
-
break
|
79
|
-
end
|
80
|
-
end
|
81
|
-
end
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
variant
|
86
|
-
end
|
87
|
-
|
88
|
-
def try_set_bass_on(variant, idx)
|
89
|
-
bass_string = variant.strings[idx].dup
|
90
|
-
distance = bass_string.distance_to(bass)
|
91
|
-
|
92
|
-
if distance >= 0 && distance < 5 && (distance - variant.min_fret) > -3
|
93
|
-
variant.extra_complexity += 0.5 if distance < variant.min_fret
|
94
|
-
variant.strings[idx].hold_on(distance)
|
95
|
-
true
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
def all_notes?(used_notes)
|
100
|
-
notes.all? { |n| used_notes.include?(n) }
|
101
|
-
end
|
102
|
-
|
103
|
-
def has_note?(value)
|
104
|
-
value = value.note if !value.is_a?(Note) && value.respond_to?(:note)
|
105
|
-
notes.include?(value)
|
106
|
-
end
|
107
|
-
|
108
|
-
def basetone?(string)
|
109
|
-
string && string.note == notes[0]
|
110
|
-
end
|
111
|
-
|
112
|
-
def third?(string)
|
113
|
-
string && (string.note == third_tone)
|
114
|
-
end
|
115
|
-
|
116
|
-
def last?(string)
|
117
|
-
string && (string.note == notes[-1])
|
118
|
-
end
|
119
|
-
end
|
120
|
-
end
|