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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6b17613170273223d591f45e88733cf89449d8b872088496c7c569a7e053f4e0
4
- data.tar.gz: 10460d448a476b45fd1ea0c96b6b418458f7c123b83f9bc8f8d45593e6fd1b36
3
+ metadata.gz: 8d764a30038a11df351e6d3d9dfce09084aa89af352d92b36677ed5eac31e425
4
+ data.tar.gz: 0c713ee0f4e3b36720014f56c1c55ca6686981b49e228402d8c2f826463affce
5
5
  SHA512:
6
- metadata.gz: 37dc9a3b6f74eeb4615f8ff897982c0a4dff8577316415ce50cd9278968298d2c9a18d24ca885fde1f599c919b6547884983ab38e68f5ae49e33f6671436e8a9
7
- data.tar.gz: 8d326d7047187a13e778af13928a3e5ac847e56c57109817feab09b17d00a29c2d56a33c9d98d23946ccde52fa5cc65e158f14ee71e59470bdb57ec319514ccb
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
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'wtf_chord/fingering'
2
- require 'wtf_chord/fingerings_generator'
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}".freeze
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
- FingeringsGenerator.new(self).call[0, limit || 5]
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'] == 'piano'
15
+ options['amount'] = 1 if options['output'] == 'piano1'
16
16
 
17
- puts chord.inspect, nil
18
- puts formatter[*chord.fingerings(options['amount'])] * formatter.separator
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,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "wtf_chord/collectors/generic"
4
+
5
+ module WTFChord
6
+ module Collectors
7
+ autoload :Guitar, "wtf_chord/collectors/guitar"
8
+ autoload :Piano, "wtf_chord/collectors/piano"
9
+ end
10
+ end
@@ -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
@@ -34,6 +34,10 @@ module WTFChord
34
34
  strings.map(&:code).hash
35
35
  end
36
36
 
37
+ def used_keys
38
+ used_strings.map(&:note).map(&:key).uniq.join
39
+ end
40
+
37
41
  def complexity
38
42
  complexity_counter.rate
39
43
  end
@@ -2,6 +2,8 @@ require 'wtf_chord/formatters/base'
2
2
 
3
3
  module WTFChord
4
4
  class Formatter
5
+ attr_reader :name
6
+
5
7
  def initialize(name, with_rates = false)
6
8
  @name = name
7
9
  @with_rates = with_rates
@@ -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".freeze
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 keys
35
- strings.reject(&:dead?).map(&:key)
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 < Base
8
+ class Piano < Piano1
8
9
  def draw
9
10
  [
10
- unique_notes.sort.join(" - "),
11
- Keyboard.press(*unique_notes.map(&:position))
12
- ].join("\n\n")
11
+ pressed_keys.map(&:to_s).join(" - "),
12
+ keyboard_presentation
13
+ ].join("\n")
13
14
  end
14
15
 
15
- def unique_notes
16
- strings.reject(&:dead?).each_with_object([[], []]) do |string, (positions, notes)|
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
@@ -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 = "×".freeze
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! { |string| GuitarString.new(string, @capo) }
13
+ @strings = strings.map { |string| GuitarString.new(string, @capo) }
12
14
  end
13
15
 
14
16
  def [] idx
@@ -1,14 +1,19 @@
1
- require 'wtf_chord/pitch'
1
+ # frozen_string_literal: true
2
+
3
+ require 'wtf_chord/instrument_pitch'
2
4
 
3
5
  module WTFChord
4
- class GuitarString < DelegateClass(WTFChord::Pitch)
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(@original + capo)
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 then @fret <=> other
62
- when GuitarString then @fret <=> other.fret
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
@@ -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
- FRAME = <<~EOF
6
- ┌─┬─┬┬─┬─┬─┬─┬┬─┬┬─┬─┐
7
- │ │ ││ │ │ │ ││ ││ │ │
8
- │ └┬┘└┬┘ │ └┬┘└┬┘└┬┘ │
9
- │ │ │ │ │ │ │ │
10
- └──┴──┴──┴──┴──┴──┴──┘
11
- EOF
7
+ attr_reader :keys
12
8
 
13
- Key = Struct.new(:offset)
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
- def select(frame)
19
- frame[offset] = self.class.select_symbol
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
- W = Class.new(Key) { self.select_symbol = "▐▌" }
25
- B = Class.new(Key) { self.select_symbol = "█" }
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
@@ -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".freeze
34
- SHARP ||= "#".freeze
35
- SIGNS ||= { FLAT => "\u266D".freeze, SHARP => "\u266F".freeze }.freeze
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
- }.freeze
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).freeze
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
@@ -1,3 +1,3 @@
1
1
  module WTFChord
2
- VERSION = "0.6.0"
2
+ VERSION = "0.7.0"
3
3
  end
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.6.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-28 00:00:00.000000000 Z
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