wtf_chord 0.3 → 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
- SHA1:
3
- metadata.gz: 0ffe99b5205f9a4cbde1ebdd815a41dc9f8f0836
4
- data.tar.gz: d08a21cf109465b729d171e88e7a3f3631ca95df
2
+ SHA256:
3
+ metadata.gz: 8d764a30038a11df351e6d3d9dfce09084aa89af352d92b36677ed5eac31e425
4
+ data.tar.gz: 0c713ee0f4e3b36720014f56c1c55ca6686981b49e228402d8c2f826463affce
5
5
  SHA512:
6
- metadata.gz: e250bfa2ee065d20208dfca8831b33c216b2a4c2c450fecae75bef2a9b8c69189c2d6fd244dff31c25c00c38f9549d579f42b7ba6fca557509fb5a88460a2290
7
- data.tar.gz: 25a18c48cecc08247ccfa78c6c396d2edd9c4481b0c86f39cf23636fb1b7bf51ba038d15b5e2fd0db8cd89c904aaf5d939cacb1a78233dcd0e7108495e1fd4cc
6
+ metadata.gz: e83f9de2c3913c4206d7d4f424f46f1082fcd16a50f8f19d4d78f2d83f3a66f610d23852ac66b890190f444db14e74d3fc9249514e6ece64626685c4c7434de2
7
+ data.tar.gz: 3ac9226adb773858a51c474c0f1b299e3bf2601ce567d9967c96b297ba4b0895da08c6b50e404221ba636be01d83740fea20b3f5684ebb24a2053b4b81235425
data/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ ## 0.5.0 / 2016-08-17
2
+
3
+ * 2 major features
4
+ * Support chords with bass string mods (like Am/G)
5
+ * Add extra_complexity modifier
6
+
7
+ * 1 bug fixes:
8
+ * Fix fingering equality
9
+
10
+ * Add CHANGELOG.md
data/README.md CHANGED
@@ -31,13 +31,13 @@ Or install it yourself as:
31
31
 
32
32
  Finds fingerings of the requested chord.
33
33
 
34
- v0.2.1
34
+ v0.3.1
35
35
 
36
36
  Options:
37
37
  -h, --help Show command line help
38
38
  --version Show help/version info
39
39
  -n, --amount N Amount of fingering variants to output.
40
- (default: 3)
40
+ (default: 5)
41
41
  -o, --output FORMAT Output format.
42
42
  (default: default)
43
43
  -R, --[no-]rates Output fingering complexity rates
@@ -57,25 +57,25 @@ For example, to print two fingering variants of the `Dm` chord, just run:
57
57
  And you'll get the visual presentation of chords' fingerings:
58
58
 
59
59
  [ × × 0 2 3 1 ]
60
- ––––––––––––––––––
61
- | | | | |
62
- | | | | |
63
- | | | | |
60
+ ——————————————————
61
+ | | | | |
62
+ | | | | |
63
+ | | | | |
64
64
  | | | | | |
65
65
  | | | | | |
66
66
  | | | | | |
67
- ––––––––––––––––––
67
+ ——————————————————
68
68
  D A D F
69
69
 
70
70
  [ × 5 7 7 6 5 ]
71
- ––––––––––––––––––
72
- | | | | V
73
- | | | | |
74
- | | | |
71
+ ——————————————————
72
+ | | | | ◉ ← V
73
+ | | | | |
74
+ | | | |
75
75
  | | | | | |
76
76
  | | | | | |
77
77
  | | | | | |
78
- ––––––––––––––––––
78
+ ——————————————————
79
79
  D A D F A
80
80
 
81
81
  You can get simpler output, using the `--output` option:
@@ -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,28 +1,62 @@
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
6
- attr_reader :pitch, :steps, :notes, :name
8
+ BASS_MATCH = /(?<=[\\\/])[A-H][b#]?(?=$)/
9
+
10
+ attr_reader :pitch, :steps, :notes, :name, :bass
7
11
 
8
12
  def initialize(note, name)
9
13
  @pitch = note.is_a?(Pitch) ? note : WTFChord.note(note)
10
- @name = "#{@pitch.key}#{name}".freeze
14
+ @name = "#{@pitch.key}#{name}"
11
15
  @steps = Array(WTFChord.rules[name])
12
16
  @notes = @steps.map { |dist| (@pitch + dist).note }.tap(&:uniq!)
17
+
18
+ BASS_MATCH.match(@name) do |m|
19
+ @bass = WTFChord.note(m[0]).note
20
+ end
13
21
  end
14
22
 
15
- def inspect
16
- "#{name} (#{@notes.map(&:key) * ' - '})"
23
+ def fingerings(limit = nil, collector: Collectors::Guitar)
24
+ collector.new(self).call[0, limit || 5]
17
25
  end
18
26
 
19
- def fingerings(limit = nil)
20
- limit ||= 5
21
- FingeringsGenerator.new(self).call[0, limit]
27
+ def tone
28
+ bass || original_bass
22
29
  end
23
30
 
24
31
  def third_tone
25
- @third_tone ||= @notes[@steps.index(7) || -1]
32
+ @third_tone ||= @notes[@steps.size > 3 ? 2 : -1]
33
+ end
34
+
35
+ def bass?
36
+ !!@bass
37
+ end
38
+
39
+ def size
40
+ bass? ? -~@notes.size : @notes.size
41
+ end
42
+
43
+ def original_bass
44
+ @notes[0]
45
+ end
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
+
58
+ def inspect
59
+ "#{name} (#{@notes.map(&:key) * ' - '})"
26
60
  end
27
61
  end
28
62
  end
data/lib/wtf_chord/cli.rb CHANGED
@@ -7,14 +7,22 @@ module WTFChord
7
7
  include Methadone::CLILogging
8
8
 
9
9
  main do |name|
10
- chord = WTFChord.chord(name)
11
- fingerings = chord.fingerings(options['amount'])
12
- formatter = WTFChord::Formatter.new(options['output'], options['rates'])
10
+ chord = WTFChord.chord(name)
11
+ formatter = WTFChord::Formatter.new(options['output'], options['rates'])
13
12
 
14
13
  debug { "Output using formatter: #{formatter.formatter.to_s}\n" }
15
14
 
16
- puts chord.inspect, nil
17
- formatter.(*fingerings)
15
+ options['amount'] = 1 if options['output'] == 'piano1'
16
+
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
18
26
  end
19
27
 
20
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
@@ -28,7 +28,7 @@ module WTFChord
28
28
  end
29
29
  end
30
30
 
31
- base_rate
31
+ base_rate + @fingering.extra_complexity
32
32
  end
33
33
 
34
34
  private
@@ -4,15 +4,18 @@ require 'wtf_chord/formatter'
4
4
 
5
5
  module WTFChord
6
6
  class Fingering < Fretboard
7
+ attr_accessor :extra_complexity
8
+
7
9
  def initialize(guitar, fingers = nil)
8
10
  @capo = guitar.capo
9
11
  @strings = guitar.strings.map(&:dup)
12
+ @extra_complexity = 0.0
10
13
  set_fingers(fingers) if fingers.is_a?(Array)
11
14
  yield(self) if block_given?
12
15
  end
13
16
 
14
17
  def code
15
- @code ||= strings.map(&:code).pack("c*")
18
+ strings.map(&:code).pack("c*")
16
19
  end
17
20
 
18
21
  def == other
@@ -23,6 +26,18 @@ module WTFChord
23
26
  end
24
27
  end
25
28
 
29
+ def eql?(other)
30
+ super || self == other
31
+ end
32
+
33
+ def hash
34
+ strings.map(&:code).hash
35
+ end
36
+
37
+ def used_keys
38
+ used_strings.map(&:note).map(&:key).uniq.join
39
+ end
40
+
26
41
  def complexity
27
42
  complexity_counter.rate
28
43
  end
@@ -43,6 +58,14 @@ module WTFChord
43
58
  @strings.reject(&:dead?)
44
59
  end
45
60
 
61
+ def find_used_string_for(note)
62
+ used = used_strings
63
+ if idx = used.index(note)
64
+ string = used[idx]
65
+ yield(strings.index(string), string)
66
+ end
67
+ end
68
+
46
69
  def min_fret
47
70
  holded_strings.min.fret
48
71
  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
@@ -9,19 +11,21 @@ module WTFChord
9
11
  end
10
12
 
11
13
  def call(*fingerings)
12
- formatter.with_rates(@with_rates) do |f|
13
- puts (fingerings.map(&f) * f.separator)
14
- end
14
+ formatter.with_rates(@with_rates) { |f| fingerings.map(&f) }
15
15
  end
16
16
 
17
17
  alias :[] :call
18
18
 
19
+ def separator
20
+ formatter.separator
21
+ end
22
+
19
23
  def formatter
20
- WTFChord::Formatters.const_get(formatter_name)
24
+ Formatters.const_get(formatter_name)
21
25
  end
22
26
 
23
27
  def formatter?
24
- WTFChord::Formatters.const_defined?(formatter_name)
28
+ Formatters.const_defined?(formatter_name)
25
29
  end
26
30
 
27
31
  def formatter_name
@@ -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,6 +33,10 @@ module WTFChord
31
33
  def draw
32
34
  end
33
35
 
36
+ def actual_strings
37
+ strings.reject(&:dead?)
38
+ end
39
+
34
40
  def with_rates?
35
41
  !!@with_rates
36
42
  end
@@ -1,25 +1,28 @@
1
+ require 'wtf_chord/helpers/roman_numbers_helper'
2
+
1
3
  module WTFChord
2
4
  module Formatters
3
5
  class Default < Base
4
6
  OPEN = "|".freeze
5
- HORIZ = "".freeze
7
+ HORIZ = "".freeze
6
8
  SPACE = " ".freeze
7
- BULL = "\u2022".freeze
8
- LATIN = %w(0 I II III IV V VI VII VIII IX X XII XIII).freeze
9
+ BULL = "\u25C9".freeze
9
10
  NEWLINE = "\n".freeze
11
+ COMPLEXITY_FORMAT = " (complexity: %.2f)".freeze
12
+ EMPTY_STRING = Array.new(FRETS, OPEN).freeze
10
13
 
11
- def self.separator
12
- "\n\n".freeze
13
- end
14
+ include RomanNumbersHelper
14
15
 
15
16
  def draw
16
- [
17
- "[ #{head} ]#{capo}",
18
- " #{border}",
19
- *fret_rows,
20
- " #{border}",
21
- " #{string_keys}#{rate}"
22
- ] * NEWLINE
17
+ <<-EOF.gsub(/^\s+\|/, '')
18
+ |[ #{head} ] #{capo}
19
+ | #{border}
20
+ #{fret_rows.map.with_index { |row, index|
21
+ " | #{row} #{roman_min_fret if index == 0}"
22
+ }.join("\n")}
23
+ | #{border}
24
+ | #{string_keys}#{rate}
25
+ EOF
23
26
  end
24
27
 
25
28
  private
@@ -29,11 +32,15 @@ module WTFChord
29
32
  end
30
33
 
31
34
  def capo
32
- " (capo #{to_latin @fret.capo})" if @fret.capo > 0
35
+ "(capo #{romanize(@fret.capo)})" if @fret.capo > 0
36
+ end
37
+
38
+ def roman_min_fret
39
+ " ← #{romanize(min_fret)}" if min_fret > 1
33
40
  end
34
41
 
35
42
  def rate
36
- " (complexity: %.2f)" % @fret.complexity if with_rates?
43
+ COMPLEXITY_FORMAT % @fret.complexity if with_rates?
37
44
  end
38
45
 
39
46
  def border
@@ -41,27 +48,21 @@ module WTFChord
41
48
  end
42
49
 
43
50
  def string_rows
44
- strings.map { |string| draw_string(string.fret) }
51
+ strings.map { |string| draw_string(string) }
45
52
  end
46
53
 
47
54
  def fret_rows
48
- string_rows.transpose.map! { |row| " #{row * SPACE} " }.tap do |rows|
49
- rows[0] << " #{to_latin min_fret}" if min_fret > 1
50
- end
55
+ return to_enum(__method__) unless block_given?
56
+ string_rows.transpose.each { |row| yield(row * SPACE) }
51
57
  end
52
58
 
53
59
  def string_keys
54
60
  strings.map { |s| s.dead? ? SPACE : "%-2s" % s.key } * " "
55
61
  end
56
62
 
57
- def to_latin(num)
58
- LATIN[num]
59
- end
60
-
61
- def draw_string(fret)
62
- Array.new(FRETS, OPEN).tap do |rows|
63
- rows[(fret - min_fret.pred).pred] = BULL if fret.to_i > 0
64
- end
63
+ def draw_string(string)
64
+ EMPTY_STRING.dup.
65
+ tap { |rows| rows[string.fret - min_fret] = BULL if string.holded? }
65
66
  end
66
67
  end
67
68
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "wtf_chord/formatters/piano1"
4
+ require "wtf_chord/keyboard"
5
+
6
+ module WTFChord
7
+ module Formatters
8
+ class Piano < Piano1
9
+ def draw
10
+ [
11
+ pressed_keys.map(&:to_s).join(" - "),
12
+ keyboard_presentation
13
+ ].join("\n")
14
+ end
15
+
16
+ def keyboard_presentation
17
+ KeyboardPresentation.press(*pressed_keys)
18
+ end
19
+ end
20
+ end
21
+ 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
@@ -23,6 +25,14 @@ module WTFChord
23
25
  self
24
26
  end
25
27
 
28
+ def with_capo(capo)
29
+ capo_was = @capo
30
+ set_capo(capo)
31
+ yield
32
+ ensure
33
+ set_capo(capo_was)
34
+ end
35
+
26
36
  def fingers
27
37
  @strings.map { |string| string.dead? ? DEAD : string.fret }
28
38
  end
@@ -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)
@@ -48,11 +53,18 @@ module WTFChord
48
53
  dead? ? -1 : to_i
49
54
  end
50
55
 
56
+ def distance_to(pitch)
57
+ pos = 0
58
+ opened = dup.open
59
+ pos += 1 while (opened + pos) != pitch
60
+ pos
61
+ end
62
+
51
63
  def <=> other
52
64
  return if dead?
53
65
  case other
54
- when Integer then @fret <=> other
55
- when GuitarString then @fret <=> other.fret
66
+ when Integer then @fret <=> other
67
+ when GuitarString then @fret <=> other.fret
56
68
  end
57
69
  end
58
70
 
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WTFChord
4
+ module RomanNumbersHelper
5
+ ROMAN_SYMBOLS ||= ("\u2160".."\u216B").to_a.unshift(nil).freeze
6
+
7
+ def romanize(number)
8
+ ROMAN_SYMBOLS[number]
9
+ end
10
+ end
11
+ end
@@ -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
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "wtf_chord/piano_key"
4
+
5
+ module WTFChord
6
+ class Keyboard
7
+ attr_reader :keys
8
+
9
+ KEYS = %w(C C# D D# E F F# G G# A Bb B)
10
+
11
+ def initialize(octaves = 1..5)
12
+ @keys = octaves.to_a.product(KEYS).map { |(o, k)| PianoKey.new("#{k}#{o}") }
13
+ end
14
+
15
+ def [] idx
16
+ case idx
17
+ when 1..@keys.size then @keys[~-idx]
18
+ end
19
+ end
20
+ end
21
+ 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
@@ -25,7 +25,7 @@ module WTFChord
25
25
 
26
26
  def == other
27
27
  case other
28
- when String then other.casecmp(@key).zero?
28
+ when String then other.casecmp(@key).zero? || aliases.any? { |a| other.casecmp(a.key).zero? }
29
29
  when Integer then other == @position
30
30
  when Note then other.position == @position
31
31
  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
@@ -46,6 +46,10 @@ module WTFChord
46
46
  move -amount
47
47
  end
48
48
 
49
+ def -@
50
+ -to_i
51
+ end
52
+
49
53
  def to_i
50
54
  (@octave * 12) + @note.position
51
55
  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,8 +62,22 @@ module WTFChord
60
62
  Bb|A#
61
63
  B|H
62
64
  )
63
- ScaleArray.build(*chromatic_scale).freeze
65
+ ScaleArray.build(*chromatic_scale)
66
+ end
67
+
68
+ def self.guitar=(guitar)
69
+ Thread.current[:wtf_guitar] = guitar
70
+ end
71
+
72
+ def self.guitar
73
+ Thread.current[:wtf_guitar] ||= Fretboard.new(*%w(E2 A2 D3 G3 B3 E4))
74
+ end
75
+
76
+ def self.piano=(piano)
77
+ Thread.current[:wtf_piano] = piano
64
78
  end
65
79
 
66
- GUITAR ||= Fretboard.new(*%w(E2 A2 D3 G3 B3 E4))
80
+ def self.piano
81
+ Thread.current[:wtf_piano] ||= Keyboard.new
82
+ end
67
83
  end
@@ -1,3 +1,3 @@
1
1
  module WTFChord
2
- VERSION = "0.3"
2
+ VERSION = "0.7.0"
3
3
  end
data/wtf_chord.gemspec CHANGED
@@ -22,7 +22,7 @@ Gem::Specification.new do |spec|
22
22
 
23
23
  spec.add_runtime_dependency "methadone"
24
24
 
25
- spec.add_development_dependency "bundler", ">= 1.11", "< 2"
26
- spec.add_development_dependency "rake", ">= 10.0", "< 12"
25
+ spec.add_development_dependency "bundler"
26
+ spec.add_development_dependency "rake"
27
27
  spec.add_development_dependency "rspec", ">= 3.0", "< 4"
28
28
  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.3'
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: 2016-04-03 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
@@ -30,40 +30,28 @@ dependencies:
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '1.11'
34
- - - "<"
35
- - !ruby/object:Gem::Version
36
- version: '2'
33
+ version: '0'
37
34
  type: :development
38
35
  prerelease: false
39
36
  version_requirements: !ruby/object:Gem::Requirement
40
37
  requirements:
41
38
  - - ">="
42
39
  - !ruby/object:Gem::Version
43
- version: '1.11'
44
- - - "<"
45
- - !ruby/object:Gem::Version
46
- version: '2'
40
+ version: '0'
47
41
  - !ruby/object:Gem::Dependency
48
42
  name: rake
49
43
  requirement: !ruby/object:Gem::Requirement
50
44
  requirements:
51
45
  - - ">="
52
46
  - !ruby/object:Gem::Version
53
- version: '10.0'
54
- - - "<"
55
- - !ruby/object:Gem::Version
56
- version: '12'
47
+ version: '0'
57
48
  type: :development
58
49
  prerelease: false
59
50
  version_requirements: !ruby/object:Gem::Requirement
60
51
  requirements:
61
52
  - - ">="
62
53
  - !ruby/object:Gem::Version
63
- version: '10.0'
64
- - - "<"
65
- - !ruby/object:Gem::Version
66
- version: '12'
54
+ version: '0'
67
55
  - !ruby/object:Gem::Dependency
68
56
  name: rspec
69
57
  requirement: !ruby/object:Gem::Requirement
@@ -95,6 +83,7 @@ files:
95
83
  - ".gitignore"
96
84
  - ".rspec"
97
85
  - ".travis.yml"
86
+ - CHANGELOG.md
98
87
  - Gemfile
99
88
  - README.md
100
89
  - Rakefile
@@ -105,16 +94,26 @@ files:
105
94
  - lib/wtf_chord.yml
106
95
  - lib/wtf_chord/chord.rb
107
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
108
101
  - lib/wtf_chord/complexity_counter.rb
109
102
  - lib/wtf_chord/fingering.rb
110
- - lib/wtf_chord/fingerings_generator.rb
111
103
  - lib/wtf_chord/formatter.rb
112
104
  - lib/wtf_chord/formatters/base.rb
113
105
  - lib/wtf_chord/formatters/default.rb
106
+ - lib/wtf_chord/formatters/piano.rb
107
+ - lib/wtf_chord/formatters/piano1.rb
114
108
  - lib/wtf_chord/formatters/simple.rb
115
109
  - lib/wtf_chord/fretboard.rb
116
110
  - lib/wtf_chord/guitar_string.rb
111
+ - lib/wtf_chord/helpers/roman_numbers_helper.rb
112
+ - lib/wtf_chord/instrument_pitch.rb
113
+ - lib/wtf_chord/keyboard.rb
114
+ - lib/wtf_chord/keyboard_presentation.rb
117
115
  - lib/wtf_chord/note.rb
116
+ - lib/wtf_chord/piano_key.rb
118
117
  - lib/wtf_chord/pitch.rb
119
118
  - lib/wtf_chord/rules.rb
120
119
  - lib/wtf_chord/scale.rb
@@ -138,10 +137,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
138
137
  - !ruby/object:Gem::Version
139
138
  version: '0'
140
139
  requirements: []
141
- rubyforge_project:
142
- rubygems_version: 2.5.1
140
+ rubygems_version: 3.0.3
143
141
  signing_key:
144
142
  specification_version: 4
145
143
  summary: "‘WTF Chord?’ is the Ruby guitar chords generator library."
146
144
  test_files: []
147
- has_rdoc:
@@ -1,85 +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 << variant if filter_variant(variant)
20
- end
21
- end
22
-
23
- fingerings.sort_by!(&:complexity)
24
- end
25
-
26
- private
27
-
28
- def generate(fret_range)
29
- combinations = GUITAR.strings.map.with_index do |s, index|
30
- fret_range.select do |dist|
31
- pitch = s.original + dist
32
- has_note?(pitch)
33
- end.tap do |frets|
34
- frets << nil if frets.size == 0
35
- end
36
- end
37
-
38
- combinations.inject(&:product).each do |fingers|
39
- fingers.flatten!
40
- Fingering.new(GUITAR, fingers) do |variant|
41
- adjust(variant)
42
- yield(variant)
43
- end
44
- end
45
- end
46
-
47
- def filter_variant(variant)
48
- used_strings = variant.used_strings
49
- used_notes = used_strings.map(&:note)
50
- tones_count = notes.map { |n| used_notes.count(n) }
51
-
52
- !fingerings.include?(variant) &&
53
- basetone?(used_strings[0]) &&
54
- (third?(used_strings[1]) || third?(used_strings[2])) &&
55
- all_notes?(used_notes) &&
56
- used_notes.each_cons(2).none? { |(l, r)| l == r } &&
57
- tones_count.all? { |n| tones_count[0] >= n || tones_count[notes.index(third_tone)] >= n } &&
58
- variant.complexity <= 2.25
59
- end
60
-
61
- def adjust(fingering)
62
- while (string = fingering.used_strings[0]) && !basetone?(string)
63
- string.dead if !string.dead?
64
- break if fingering.used_strings.size == 4
65
- end
66
- end
67
-
68
- def all_notes?(used_notes)
69
- notes.all? { |n| used_notes.include?(n) }
70
- end
71
-
72
- def has_note?(value)
73
- value = value.note if !value.is_a?(Note) && value.respond_to?(:note)
74
- notes.include?(value)
75
- end
76
-
77
- def basetone?(string)
78
- string && string.note == notes[0]
79
- end
80
-
81
- def third?(string)
82
- string && (string.note == third_tone)
83
- end
84
- end
85
- end