musique 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 49ee2d5eebc0c30d67c830b2e8fe5abaf97e91b9
4
+ data.tar.gz: 5850e2bdb290e8eddf82336f6aa897849455feaa
5
+ SHA512:
6
+ metadata.gz: 467fbe00501716a9b449ae782d1ea925884449e879d9508107fa0c820954a76a2a5397e24bb5035aba7b7f9d77df3136c20bb3a64020a6e6f8495d0f1dbae859
7
+ data.tar.gz: fe2d69a49b2c4c31189dc5a84024f5b3388a8dd13a25648e6347dcbefa54ff993f1baeac03a0cb02757475d7e2358919593fa51491d5895450a61a36f89b141e
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014-ω Janko Marohnić
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,100 @@
1
+ # Musique
2
+
3
+ Musique is a gem for manipulating with musical constructs, such as notes,
4
+ chords and intervals.
5
+
6
+ Installation
7
+ ------------
8
+
9
+ ```sh
10
+ $ gem install musique
11
+ ```
12
+
13
+ Usage
14
+ -----
15
+
16
+ ### `Music::Note`
17
+
18
+ ```rb
19
+ require "musique"
20
+
21
+ include Music
22
+
23
+ note = Note.new("C#1")
24
+ note.letter #=> "C"
25
+ note.accidental #=> "#"
26
+ note.octave #=> 1
27
+
28
+ # Comparison
29
+ Note.new("C1") < Note.new("E1") #=> true
30
+ Note.new("C#1") == Note.new("D♭1") #=> true
31
+
32
+ # Transposing
33
+ major_third = Interval.new(3, :major)
34
+ Note.new("C1").transpose_up(major_third).name #=> "E1"
35
+ Note.new("E1").transpose_down(major_third).name #=> "C1"
36
+
37
+ # Difference
38
+ Note.new("C2") - Note.new("C1") #=> #<Music::Interval @number=8, @quality=:perfect>
39
+ ```
40
+
41
+ ### `Music::Chord`
42
+
43
+ ```rb
44
+ require "musique"
45
+
46
+ include Music
47
+
48
+ chord = Chord.new("C#7")
49
+ chord.root.name #=> "C#"
50
+ chord.kind #=> "7"
51
+
52
+ # Notes
53
+ Chord.new("C").notes.map(&:name) #=> ["C", "E", "G"]
54
+ Chord.new("Cm7").notes.map(&:name) #=> ["C", "E♭", "G", "B♭"]
55
+
56
+ # Transposing
57
+ major_third = Interval.new(3, :major)
58
+ Chord.new("C").transpose_up(major_third).notes.map(&:name) #=> ["E", "G#", "B"]
59
+ ```
60
+
61
+ Into `Chord.new(...)` you should be able to pass in any chord name. For example,
62
+ `"Cm"` and `"Cmin"` both represent the "C minor" chord, and both can be passed in.
63
+
64
+ If you come across a chord name that doesn't work, please open an issue,
65
+ I would like to know about it.
66
+
67
+ ### `Music::Interval`
68
+
69
+ ```rb
70
+ require "musique"
71
+
72
+ include Music
73
+
74
+ interval = Interval.new(3, :minor)
75
+ interval.number #=> 3
76
+ interval.quality #=> :minor
77
+
78
+ # Comparison
79
+ Interval.new(3, :minor) > Interval.new(2, :major) #=> true
80
+ Interval.new(3, :minor) == Interval.new(2, :augmented) #=> true
81
+
82
+ # Kinds
83
+ Interval.new(6, :major).consonance? #=> true
84
+ Interval.new(6, :major).perfect_consonance? #=> false (perfect consonances are 1, 4, and 5)
85
+ Interval.new(6, :major).imperfect_consonance? #=> true
86
+ Interval.new(6, :major).dissonance? #=> false
87
+
88
+ # Size
89
+ Interval.new(3, :major).size #=> 4 (semitones)
90
+ ```
91
+
92
+ Social
93
+ ------
94
+
95
+ You can follow me on Twitter, I'm [@m_janko](http://twitter.com/m_janko).
96
+
97
+ License
98
+ -------
99
+
100
+ This project is released under the [MIT license](/LICENSE).
@@ -0,0 +1,108 @@
1
+ # encoding: utf-8
2
+
3
+ module Music
4
+
5
+ class Chord
6
+
7
+ RULES = {
8
+ "5" => ["1", "major 3", "5"],
9
+ "m5" => ["1", "minor 3", "5"],
10
+
11
+ "7" => ["1", "major 3", "5", "major 7"],
12
+ "m7" => ["1", "minor 3", "5", "minor 7"],
13
+ }
14
+
15
+ REGEXP = /
16
+ (?<root> [CDEFGAB][#♭b]? )
17
+ (?<kind> m?\d* )
18
+ /x
19
+
20
+ ##
21
+ # @return [Music::Note]
22
+ # @example
23
+ # Chord.new("C").root #=> #<Music::Note @letter="C">
24
+ #
25
+ attr_reader :root
26
+ ##
27
+ #
28
+ # @return [String]
29
+ # @example
30
+ # Chord.new("C7").kind #=> "7"
31
+ #
32
+ attr_reader :kind
33
+
34
+ ##
35
+ # @param name [String] Name of the chord, in any form.
36
+ # @example
37
+ # Chord.new("Cm") == Chord.new("Cmin") #=> true
38
+ #
39
+ def initialize(name)
40
+ unless match = name.match(/^#{REGEXP}$/)
41
+ raise ArgumentError, "invalid chord format: #{name}"
42
+ end
43
+
44
+ @root = Note.new(match[:root])
45
+
46
+ @kind = match[:kind]
47
+ @kind << "5" if @kind !~ /\d+/
48
+ end
49
+
50
+ def name
51
+ [root, kind].join
52
+ end
53
+
54
+ ##
55
+ # Compares the names.
56
+ #
57
+ def ==(other)
58
+ self.name == other.name
59
+ end
60
+
61
+ ##
62
+ # @return [Array<Music::Note>]
63
+ #
64
+ def notes
65
+ RULES[kind].map do |interval_name|
66
+ quality = interval_name[/^[a-z]+/] || "perfect"
67
+ number = interval_name[/\d+$/]
68
+ interval = Interval.new(number.to_i, quality.to_sym)
69
+
70
+ root.transpose_by(interval)
71
+ end
72
+ end
73
+
74
+ ##
75
+ # @param interval [Music::Interval]
76
+ # @return [Music::Note]
77
+ # @example
78
+ # major_third = Interval.new(3, :major)
79
+ # Chord.new("C7").transpose_by(major_third) == Note.new("E7") #=> true
80
+ # Note.new("E7").transpose_by(-major_third) == Note.new("C7") #=> true
81
+ #
82
+ def transpose_by(interval)
83
+ Chord.new(root.transpose_by(interval).name + kind)
84
+ end
85
+
86
+ ##
87
+ # @param (see #transpose_by)
88
+ # @return (see #transpose_by)
89
+ #
90
+ # @see #transpose_by
91
+ #
92
+ def transpose_up(interval)
93
+ transpose_by(interval)
94
+ end
95
+
96
+ ##
97
+ # @param (see #transpose_by)
98
+ # @return (see #transpose_by)
99
+ #
100
+ # @see #transpose_by
101
+ #
102
+ def transpose_down(interval)
103
+ transpose_by(-interval)
104
+ end
105
+
106
+ end
107
+
108
+ end
@@ -0,0 +1,173 @@
1
+ module Music
2
+
3
+ class Interval
4
+
5
+ include Comparable
6
+
7
+ ##
8
+ # @private
9
+ #
10
+ SIZES = {
11
+ 1 => [0],
12
+ 2 => [1, 2],
13
+ 3 => [3, 4],
14
+ 4 => [5],
15
+ 5 => [7],
16
+ 6 => [8, 9],
17
+ 7 => [10, 11],
18
+ 8 => [12],
19
+ }
20
+
21
+ ##
22
+ # @private
23
+ #
24
+ OCTAVE = 8
25
+
26
+ QUALITIES = [:minor, :major, :perfect, :augmented, :diminished]
27
+
28
+ ##
29
+ # @return [Integer]
30
+ #
31
+ attr_reader :number
32
+ ##
33
+ # @return [Symbol]
34
+ #
35
+ attr_reader :quality
36
+ ##
37
+ # Indicates if the interval is positive or negative.
38
+ #
39
+ # @return [1, -1]
40
+ #
41
+ attr_reader :direction
42
+
43
+ ##
44
+ # @param number [Integer]
45
+ # @param quality [Symbol] See the {QUALITIES} constant for the list of
46
+ # possible values.
47
+ #
48
+ # @example
49
+ # Interval.new(3, :major) # a major third
50
+ # Interval.new(5, :perfect) # a perfect fifth
51
+ #
52
+ def initialize(number, quality)
53
+ if not QUALITIES.include?(quality)
54
+ raise ArgumentError, "invalid interval quality: #{quality}"
55
+ elsif [1, 4, 5].include?((number.abs - 1) % 7 + 1) and [:major, :minor].include?(quality)
56
+ raise ArgumentError, "interval #{number} doesn't have quality \"#{quality}\""
57
+ end
58
+
59
+ @number, @quality = number.abs, quality
60
+ @direction = number >= 0 ? +1 : -1
61
+ end
62
+
63
+ ##
64
+ # Perfect consonants are perfect unisons, fourths and fifths (and their
65
+ # compound intervals).
66
+ #
67
+ # @return [Boolean]
68
+ #
69
+ def perfect_consonance?
70
+ [1, 4, 5].include?(normalized_number) and [:perfect].include?(quality)
71
+ end
72
+
73
+ ##
74
+ # Imperfect consonants are minor/major thirds and sixths (and their compound
75
+ # intervals).
76
+ #
77
+ # @return [Boolean]
78
+ #
79
+ def imperfect_consonance?
80
+ [3, 6].include?(normalized_number) and [:minor, :major].include?(quality)
81
+ end
82
+
83
+ ##
84
+ # Consonances are perfect and imperfect consonances together.
85
+ #
86
+ # @return [Boolean]
87
+ #
88
+ # @see #perfect_consonance?
89
+ # @see #imperfect_consonance?
90
+ #
91
+ def consonance?
92
+ perfect_consonance? or imperfect_consonance?
93
+ end
94
+
95
+ ##
96
+ # Disonances are all intervals that are not consonances.
97
+ #
98
+ # @return [Boolean]
99
+ #
100
+ # @see #consonance?
101
+ #
102
+ def dissonance?
103
+ not consonance?
104
+ end
105
+
106
+ ##
107
+ # Returns the same inteval with changed direction.
108
+ #
109
+ # @return [Music::Interval]
110
+ #
111
+ def -@
112
+ Interval.new((-1 * direction) * number, quality)
113
+ end
114
+
115
+ ##
116
+ # Returns the number of semitones that interval covers.
117
+ #
118
+ # @return [Integer]
119
+ #
120
+ def size
121
+ result = 0
122
+ current_number = number
123
+
124
+ while current_number >= OCTAVE
125
+ result += SIZES[OCTAVE][0]
126
+ current_number -= OCTAVE - 1
127
+ end
128
+
129
+ sizes = SIZES.fetch(current_number)
130
+ result +=
131
+ case quality
132
+ when :diminished then sizes.first - 1
133
+ when :minor then sizes.first
134
+ when :perfect then sizes[0]
135
+ when :major then sizes.last
136
+ when :augmented then sizes.last + 1
137
+ end
138
+
139
+ result * direction
140
+ end
141
+
142
+ ##
143
+ # Compares intervals by their size.
144
+ #
145
+ # @param other [Music::Interval]
146
+ # @example
147
+ # Interval.new(3, :major) > Interval.new(4, :perfect) #=> true
148
+ # Interval.new(3, :major) == Interval.new(4, :diminished) #=> true
149
+ #
150
+ def <=>(other)
151
+ self.size.abs <=> other.size.abs
152
+ end
153
+
154
+ ##
155
+ # The actual diatonic difference. For example, seconds are numbered with
156
+ # `2`, but the actual diatonic difference between two notes that
157
+ # are in a second is `1`.
158
+ #
159
+ # @return [Integer]
160
+ #
161
+ def diff
162
+ (number - 1) * direction
163
+ end
164
+
165
+ private
166
+
167
+ def normalized_number
168
+ (number - 1) % 7 + 1
169
+ end
170
+
171
+ end
172
+
173
+ end
data/lib/music/note.rb ADDED
@@ -0,0 +1,173 @@
1
+ # encoding: utf-8
2
+
3
+ module Music
4
+
5
+ class Note
6
+
7
+ include Comparable
8
+
9
+ CHROMATIC_SCALE = %w[C C/D D D/E E F F/G G G/A A A/B B]
10
+ DIATONIC_SCALE = %w[C D E F G A B]
11
+
12
+ ACCIDENTALS = %w[# ♭]
13
+
14
+ REGEXP = /
15
+ (?<letter> [CDEFGAB] )
16
+ (?<accidental> [#♭b]? )
17
+ (?<octave> (-?\d+)? )
18
+ /x
19
+
20
+ ##
21
+ # C, D, E, F, G, A or B.
22
+ #
23
+ # @return [String]
24
+ #
25
+ attr_reader :letter
26
+ ##
27
+ # \# or ♭.
28
+ #
29
+ # @return [String]
30
+ #
31
+ attr_reader :accidental
32
+ ##
33
+ # An integer.
34
+ #
35
+ # @return [Integer]
36
+ #
37
+ attr_reader :octave
38
+
39
+ ##
40
+ # @param name [String] Consists of a letter (C, D, E, F, G, A, B),
41
+ # accidental (# or ♭) and octave (any integer).
42
+ #
43
+ # @example
44
+ # Note.new("C")
45
+ # Note.new("C#")
46
+ # Note.new("C♭") #=> or Note.new("Cb")
47
+ #
48
+ # Note.new("C1")
49
+ # Note.new("B-1")
50
+ #
51
+ def initialize(name)
52
+ unless match = name.match(/^#{REGEXP}$/)
53
+ raise ArgumentError, "invalid note name: #{name} (example: C#1)"
54
+ end
55
+
56
+ @letter = match[:letter]
57
+ @accidental = match[:accidental].sub("b", "♭") unless match[:accidental].empty?
58
+ @octave = match[:octave].to_i unless match[:octave].empty?
59
+ end
60
+
61
+ def name
62
+ [letter, accidental, octave].join
63
+ end
64
+ alias to_s name
65
+
66
+ ##
67
+ # Compares notes by their pitch.
68
+ #
69
+ # @param other [Music::Note]
70
+ # @example
71
+ # Note.new("C#") == Note.new("D♭") #=> true
72
+ # Note.new("C") < Note.new("D") #=> true
73
+ # Note.new("C2") > Note.new("C1") #=> true
74
+ #
75
+ def <=>(other)
76
+ self.pitch <=> other.pitch
77
+ end
78
+
79
+ ##
80
+ # @param interval [Music::Interval]
81
+ # @return [Music::Note]
82
+ # @example
83
+ # major_third = Interval.new(3, :major)
84
+ # Note.new("C").transpose_by(major_third) == Note.new("E") #=> true
85
+ # Note.new("E").transpose_by(-major_third) == Note.new("C") #=> true
86
+ #
87
+ def transpose_by(interval)
88
+ transposed_pitch = pitch + interval.size
89
+ transposed_pitch %= CHROMATIC_SCALE.size if octave.nil?
90
+
91
+ transposed_diatonic_idx = (diatonic_idx + interval.diff) % DIATONIC_SCALE.size
92
+ transposed_letter = DIATONIC_SCALE.fetch(transposed_diatonic_idx)
93
+
94
+ transposed_octave = octave + (diatonic_idx + interval.diff) / DIATONIC_SCALE.size if octave
95
+
96
+ transposed_accidental = ACCIDENTALS.find do |accidental|
97
+ note = Note.new [transposed_letter, accidental, transposed_octave].join
98
+ note.pitch == transposed_pitch
99
+ end
100
+
101
+ Note.new [transposed_letter, transposed_accidental, transposed_octave].join
102
+ end
103
+
104
+ ##
105
+ # @param (see #transpose_by)
106
+ # @return (see #transpose_by)
107
+ #
108
+ # @see #transpose_by
109
+ #
110
+ def transpose_up(interval)
111
+ transpose_by(interval)
112
+ end
113
+
114
+ ##
115
+ # @param (see #transpose_by)
116
+ # @return (see #transpose_by)
117
+ #
118
+ # @see #transpose_by
119
+ #
120
+ def transpose_down(interval)
121
+ transpose_by(-interval)
122
+ end
123
+
124
+ ##
125
+ # @param other [Music::Note]
126
+ # @return [Music::Interval]
127
+ # @example
128
+ # Note.new("E") - Note.new("C") #=> #<Music::Interval @number=3, @quality=:major>
129
+ #
130
+ def -(other)
131
+ number = (self.diatonic_idx - other.diatonic_idx).abs + 1
132
+ number += (self.octave - other.octave).abs * DIATONIC_SCALE.size unless octave.nil?
133
+
134
+ distance = (self.pitch - other.pitch).abs
135
+ quality = Interval::QUALITIES.find do |quality|
136
+ begin; Interval.new(number, quality).size == distance; rescue ArgumentError; end
137
+ end
138
+
139
+ interval = Interval.new(number, quality)
140
+ self >= other ? interval : -interval
141
+ end
142
+
143
+ protected
144
+
145
+ ##
146
+ # Internal integer representation on the note.
147
+ #
148
+ # @return [Integer]
149
+ #
150
+ def pitch
151
+ result = chromatic_idx + (octave.to_i * CHROMATIC_SCALE.size)
152
+ result %= CHROMATIC_SCALE.size if octave.nil?
153
+ result
154
+ end
155
+
156
+ ##
157
+ # @return [Integer] An integer from 0 to 6
158
+ #
159
+ def diatonic_idx
160
+ DIATONIC_SCALE.index(letter)
161
+ end
162
+
163
+ ##
164
+ # @return [Integer] An integer from 0 to 11
165
+ #
166
+ def chromatic_idx
167
+ accidental_effect = {"#" => +1, "♭" => -1}.fetch(accidental, 0)
168
+ CHROMATIC_SCALE.index(letter) + accidental_effect
169
+ end
170
+
171
+ end
172
+
173
+ end
data/lib/music.rb ADDED
@@ -0,0 +1,6 @@
1
+ module Music
2
+ autoload :Note, "music/note"
3
+ autoload :Interval, "music/interval"
4
+ autoload :Chord, "music/chord"
5
+ autoload :Scale, "music/scale"
6
+ end
data/lib/musique.rb ADDED
@@ -0,0 +1 @@
1
+ require "music"
metadata ADDED
@@ -0,0 +1,52 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: musique
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Janko Marohnić
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-01-19 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: This gem is an object-oriented wrapper for the Flickr API.
14
+ email:
15
+ - janko.marohnic@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - LICENSE
21
+ - README.md
22
+ - lib/music.rb
23
+ - lib/music/chord.rb
24
+ - lib/music/interval.rb
25
+ - lib/music/note.rb
26
+ - lib/musique.rb
27
+ homepage: http://janko-m.github.com/flickr-objects
28
+ licenses:
29
+ - MIT
30
+ metadata: {}
31
+ post_install_message:
32
+ rdoc_options: []
33
+ require_paths:
34
+ - lib
35
+ required_ruby_version: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 1.9.2
40
+ required_rubygems_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
45
+ requirements: []
46
+ rubyforge_project:
47
+ rubygems_version: 2.2.0
48
+ signing_key:
49
+ specification_version: 4
50
+ summary: This gem is an object-oriented wrapper for the Flickr API.
51
+ test_files: []
52
+ has_rdoc: