stretto 0.6.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.markdown +0 -0
- data/README.markdown +67 -0
- data/Rakefile +9 -0
- data/lib/stretto.rb +14 -0
- data/lib/stretto/grammar/channel_pressure_grammar.treetop +9 -0
- data/lib/stretto/grammar/chord_grammar.treetop +60 -0
- data/lib/stretto/grammar/controller_change_grammar.treetop +9 -0
- data/lib/stretto/grammar/duration_grammar.treetop +60 -0
- data/lib/stretto/grammar/grammar_helper.rb +6 -0
- data/lib/stretto/grammar/harmonic_chord_grammar.treetop +15 -0
- data/lib/stretto/grammar/harmony_grammar.treetop +15 -0
- data/lib/stretto/grammar/instrument_grammar.treetop +9 -0
- data/lib/stretto/grammar/key_signature_grammar.treetop +10 -0
- data/lib/stretto/grammar/layer_change_grammar.treetop +9 -0
- data/lib/stretto/grammar/measure_grammar.treetop +7 -0
- data/lib/stretto/grammar/note_grammar.treetop +32 -0
- data/lib/stretto/grammar/pitch_bend_grammar.treetop +7 -0
- data/lib/stretto/grammar/polyphonic_pressure_grammar.treetop +9 -0
- data/lib/stretto/grammar/rest_grammar.treetop +9 -0
- data/lib/stretto/grammar/stretto_grammar.treetop +62 -0
- data/lib/stretto/grammar/tempo_grammar.treetop +9 -0
- data/lib/stretto/grammar/timing_grammar.treetop +9 -0
- data/lib/stretto/grammar/tokens/attack_decay_token.rb +42 -0
- data/lib/stretto/grammar/tokens/chord_token.rb +67 -0
- data/lib/stretto/grammar/tokens/controller_change_token.rb +26 -0
- data/lib/stretto/grammar/tokens/duration_token.rb +55 -0
- data/lib/stretto/grammar/tokens/harmonic_chord_token.rb +25 -0
- data/lib/stretto/grammar/tokens/harmony_with_melody_token.rb +66 -0
- data/lib/stretto/grammar/tokens/hash_token.rb +15 -0
- data/lib/stretto/grammar/tokens/key_signature_token.rb +30 -0
- data/lib/stretto/grammar/tokens/measure_token.rb +21 -0
- data/lib/stretto/grammar/tokens/modifier_token.rb +99 -0
- data/lib/stretto/grammar/tokens/note_string_token.rb +106 -0
- data/lib/stretto/grammar/tokens/note_token.rb +25 -0
- data/lib/stretto/grammar/tokens/pattern_token.rb +26 -0
- data/lib/stretto/grammar/tokens/polyphonic_pressure_token.rb +28 -0
- data/lib/stretto/grammar/tokens/rest_token.rb +21 -0
- data/lib/stretto/grammar/tokens/value_token.rb +28 -0
- data/lib/stretto/grammar/tokens/variable_definition_token.rb +30 -0
- data/lib/stretto/grammar/value_grammar.treetop +45 -0
- data/lib/stretto/grammar/variable_grammar.treetop +10 -0
- data/lib/stretto/grammar/voice_change_grammar.treetop +9 -0
- data/lib/stretto/music_elements/channel_pressure.rb +44 -0
- data/lib/stretto/music_elements/chord.rb +194 -0
- data/lib/stretto/music_elements/controller_change.rb +49 -0
- data/lib/stretto/music_elements/harmonic_chord.rb +56 -0
- data/lib/stretto/music_elements/harmony.rb +37 -0
- data/lib/stretto/music_elements/instrument.rb +57 -0
- data/lib/stretto/music_elements/key_signature.rb +110 -0
- data/lib/stretto/music_elements/layer.rb +22 -0
- data/lib/stretto/music_elements/layer_change.rb +39 -0
- data/lib/stretto/music_elements/measure.rb +38 -0
- data/lib/stretto/music_elements/melody.rb +72 -0
- data/lib/stretto/music_elements/modifiers/attack_decay.rb +55 -0
- data/lib/stretto/music_elements/modifiers/chord_intervals.rb +44 -0
- data/lib/stretto/music_elements/modifiers/duration.rb +180 -0
- data/lib/stretto/music_elements/modifiers/value.rb +172 -0
- data/lib/stretto/music_elements/modifiers/variables.rb +365 -0
- data/lib/stretto/music_elements/music_element.rb +88 -0
- data/lib/stretto/music_elements/note.rb +282 -0
- data/lib/stretto/music_elements/pattern.rb +100 -0
- data/lib/stretto/music_elements/pitch_bend.rb +46 -0
- data/lib/stretto/music_elements/polyphonic_pressure.rb +62 -0
- data/lib/stretto/music_elements/rest.rb +26 -0
- data/lib/stretto/music_elements/tempo.rb +42 -0
- data/lib/stretto/music_elements/timing.rb +33 -0
- data/lib/stretto/music_elements/variable.rb +49 -0
- data/lib/stretto/music_elements/voice.rb +49 -0
- data/lib/stretto/music_elements/voice_change.rb +39 -0
- data/lib/stretto/parsers/exceptions.rb +11 -0
- data/lib/stretto/parsers/parser.rb +89 -0
- data/lib/stretto/renderers/midiator/player.rb +200 -0
- data/lib/stretto/util/jfugue_format_parser.rb +20 -0
- data/lib/stretto/util/node.rb +5 -0
- data/lib/stretto/util/utils.rb +41 -0
- data/lib/stretto/version.rb +3 -0
- metadata +203 -0
@@ -0,0 +1,106 @@
|
|
1
|
+
module Stretto
|
2
|
+
module Tokens
|
3
|
+
|
4
|
+
|
5
|
+
# Token from parsing a note pitch, when an explicit pitch is given.
|
6
|
+
#
|
7
|
+
# @example "[60]". "[SOME_NOTE]"
|
8
|
+
module NotePitchToken
|
9
|
+
|
10
|
+
# Always return nil
|
11
|
+
# @return nil
|
12
|
+
def accidental
|
13
|
+
nil
|
14
|
+
end
|
15
|
+
|
16
|
+
# Always return nil
|
17
|
+
# @return nil
|
18
|
+
def octave
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
|
22
|
+
# Always return nil
|
23
|
+
# @return nil
|
24
|
+
def key
|
25
|
+
nil
|
26
|
+
end
|
27
|
+
|
28
|
+
# Wraps the value into a Value object, either a numeric or a variable value
|
29
|
+
# @return [Value] The wrapped value for the pitch
|
30
|
+
def pitch
|
31
|
+
klass = if _pitch.is_numeric?
|
32
|
+
Value::NumericValue
|
33
|
+
else
|
34
|
+
Value::VariableValue
|
35
|
+
end
|
36
|
+
Value.new(klass.new(_pitch.text_value))
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
#------------------------------------------------------------------
|
41
|
+
|
42
|
+
# Token result from parsing the part of a note that contains key, and optional
|
43
|
+
# accidental and octave values
|
44
|
+
#
|
45
|
+
# @example "C#7"
|
46
|
+
module NoteKeyAccidentalOctaveToken
|
47
|
+
|
48
|
+
# @return [String] The key of the note ('A' through 'G')
|
49
|
+
# @example "C"
|
50
|
+
def key
|
51
|
+
note_key.key.text_value
|
52
|
+
end
|
53
|
+
|
54
|
+
# @return [String, nil] The accidental of the note, if present (one of 'bb', 'b', 'n', '#', '##')
|
55
|
+
# @example "#"
|
56
|
+
def accidental
|
57
|
+
note_key.accidental.text_value if note_key.accidental and note_key.accidental.text_value.present?
|
58
|
+
end
|
59
|
+
|
60
|
+
# Always return nil, as this is used to differentiate a note with a given pitch from one with key,
|
61
|
+
# octave and accidental whose pitch has to be calculated
|
62
|
+
# @return [nil]
|
63
|
+
def pitch
|
64
|
+
nil
|
65
|
+
end
|
66
|
+
|
67
|
+
# @return [String, nil] The octave of the note, if present
|
68
|
+
# @example "5"
|
69
|
+
def octave
|
70
|
+
_octave.text_value if _octave and _octave.text_value.present?
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
#------------------------------------------------------------------
|
75
|
+
|
76
|
+
# Include this module to include note string functionality, that is, to provide
|
77
|
+
# key, accidental, octave and/or pitch
|
78
|
+
module WithNoteStringToken
|
79
|
+
|
80
|
+
# @return (see NoteKeyAccidentalOctaveToken#octave)
|
81
|
+
# @return (see NotePitchToken#octave)
|
82
|
+
def octave
|
83
|
+
note_string.octave
|
84
|
+
end
|
85
|
+
|
86
|
+
# @return (see NoteKeyAccidentalOctaveToken#accidental)
|
87
|
+
# @return (see NotePitchToken#accidental)
|
88
|
+
def accidental
|
89
|
+
note_string.accidental
|
90
|
+
end
|
91
|
+
|
92
|
+
# @return (see NoteKeyAccidentalOctaveToken#key)
|
93
|
+
# @return (see NotePitchToken#key)
|
94
|
+
def key
|
95
|
+
note_string.key
|
96
|
+
end
|
97
|
+
|
98
|
+
# @return (see NoteKeyAccidentalOctaveToken#pitch)
|
99
|
+
# @return (see NotePitchToken#pitch)
|
100
|
+
def pitch
|
101
|
+
note_string.pitch
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'duration_token')
|
2
|
+
require File.join(File.dirname(__FILE__), 'note_string_token')
|
3
|
+
require File.join(File.dirname(__FILE__), 'attack_decay_token')
|
4
|
+
require File.join(File.dirname(__FILE__), '../../music_elements/note')
|
5
|
+
|
6
|
+
module Stretto
|
7
|
+
module Tokens
|
8
|
+
|
9
|
+
# Token result from parsing a note element. It includes the note string, attack, decay and duration
|
10
|
+
#
|
11
|
+
# @example "C", "F#6", "[60]w.", "Gb/3.0a100d100"
|
12
|
+
class NoteToken < HashToken
|
13
|
+
|
14
|
+
include WithDurationToken
|
15
|
+
include WithNoteStringToken
|
16
|
+
include WithAttackDecayToken
|
17
|
+
|
18
|
+
# @return [MusicElements::Note] The constructed Note element
|
19
|
+
def to_stretto(pattern = nil)
|
20
|
+
Stretto::MusicElements::Note.new(self, pattern)
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Stretto
|
2
|
+
module Tokens
|
3
|
+
|
4
|
+
# This is the token that parses a pattern, that is, a collection of music elements
|
5
|
+
#
|
6
|
+
# @example "T[ALLEGRO] I[PIANO] | C C D E | Rw. | Dmin"
|
7
|
+
module PatternToken
|
8
|
+
|
9
|
+
# @return [Array(MusicElements::MusicElement)] An array containing all the elements of
|
10
|
+
# the pattern
|
11
|
+
#-
|
12
|
+
# TODO: This should return a Pattern object directly
|
13
|
+
def to_stretto(pattern = nil)
|
14
|
+
unless head.text_value.empty?
|
15
|
+
[head.to_stretto(pattern)] + more_elements.elements.map do |element|
|
16
|
+
element.music_element.to_stretto(pattern)
|
17
|
+
end
|
18
|
+
else
|
19
|
+
[]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '../../music_elements/polyphonic_pressure')
|
2
|
+
|
3
|
+
module Stretto
|
4
|
+
module Tokens
|
5
|
+
|
6
|
+
# Token from parsing a polyphonic pressure element
|
7
|
+
#
|
8
|
+
# @example "*80,100"
|
9
|
+
class PolyphonicPressureToken < HashToken
|
10
|
+
|
11
|
+
# @return [MusicElements::PolyphonicPressure] The PolyphonicPressure element constructed
|
12
|
+
def to_stretto(pattern = nil)
|
13
|
+
Stretto::MusicElements::PolyphonicPressure.new(self, pattern)
|
14
|
+
end
|
15
|
+
|
16
|
+
# @return [Value] Value of the pitch to apply pressure
|
17
|
+
def pitch
|
18
|
+
Stretto::Value.new(__pitch.wrap)
|
19
|
+
end
|
20
|
+
|
21
|
+
# @return [Value] Value of the pressure applied
|
22
|
+
def value
|
23
|
+
Stretto::Value.new(__value.wrap)
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'duration_token')
|
2
|
+
require File.join(File.dirname(__FILE__), '../../music_elements/rest')
|
3
|
+
|
4
|
+
module Stretto
|
5
|
+
module Tokens
|
6
|
+
|
7
|
+
# Token from parsing a rest element
|
8
|
+
#
|
9
|
+
# @example "R", "Rq"
|
10
|
+
class RestToken < HashToken
|
11
|
+
|
12
|
+
include WithDurationToken
|
13
|
+
|
14
|
+
# @return [MusicElement::Rest] The Rest element constructed
|
15
|
+
def to_stretto(pattern = nil)
|
16
|
+
Stretto::MusicElements::Rest.new(self, pattern)
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Stretto
|
2
|
+
module Tokens
|
3
|
+
|
4
|
+
# Represents a numeric value (either decimal or integer) parsed from a music string
|
5
|
+
# @example "10.5"
|
6
|
+
# @see VariableToken
|
7
|
+
module NumericToken
|
8
|
+
|
9
|
+
# @return [Value::NumericValue] A NumericValue with the parsed text as a float number
|
10
|
+
def wrap
|
11
|
+
Stretto::Value::NumericValue.new(text_value.to_f)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Represents a variable value parsed from a music string
|
16
|
+
# @example "[SOME_VARIABLE]"
|
17
|
+
# @see NumericToken
|
18
|
+
module VariableToken
|
19
|
+
|
20
|
+
# @return [Value::VariableValue] A VariableValue with the parsed text as the name of
|
21
|
+
# the variable it references
|
22
|
+
def wrap
|
23
|
+
Stretto::Value::VariableValue.new(name.text_value)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '../../music_elements/variable')
|
2
|
+
|
3
|
+
module Stretto
|
4
|
+
module Tokens
|
5
|
+
|
6
|
+
# Token from parsing a variable definition
|
7
|
+
#
|
8
|
+
# @example "$MY_VAR=80", "$OTHER_VAR=[SOME_NOTE_VAR]"
|
9
|
+
class VariableDefinitionToken < HashToken
|
10
|
+
|
11
|
+
# @return [MusicElements::Variable] The constructed Variable element
|
12
|
+
def to_stretto(pattern = nil)
|
13
|
+
Stretto::MusicElements::Variable.new(self, pattern)
|
14
|
+
end
|
15
|
+
|
16
|
+
# @return [String] The name of the variable
|
17
|
+
def name
|
18
|
+
__name.text_value
|
19
|
+
end
|
20
|
+
|
21
|
+
# @return [Value] The value of the variable, a Value object
|
22
|
+
# wrapping either a numeric or another variable value
|
23
|
+
def value
|
24
|
+
Stretto::Value.new(__value.wrap)
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
grammar ValueGrammar
|
2
|
+
|
3
|
+
rule variable_or_integer
|
4
|
+
variable <Stretto::Tokens::VariableToken>
|
5
|
+
/
|
6
|
+
integer <Stretto::Tokens::NumericToken>
|
7
|
+
end
|
8
|
+
|
9
|
+
rule variable_or_decimal
|
10
|
+
variable <Stretto::Tokens::VariableToken>
|
11
|
+
/
|
12
|
+
decimal <Stretto::Tokens::NumericToken>
|
13
|
+
end
|
14
|
+
|
15
|
+
rule integer
|
16
|
+
"0" / [1-9] [0-9]*
|
17
|
+
end
|
18
|
+
|
19
|
+
rule decimal
|
20
|
+
integer ('.' [0-9]+)?
|
21
|
+
end
|
22
|
+
|
23
|
+
rule variable
|
24
|
+
'[' name:variable_name ']'
|
25
|
+
end
|
26
|
+
|
27
|
+
rule variable_name
|
28
|
+
[A-Za-z_] [A-Za-z0-9_]+
|
29
|
+
end
|
30
|
+
|
31
|
+
rule integer_or_variable_name
|
32
|
+
integer {
|
33
|
+
def is_numeric?
|
34
|
+
true
|
35
|
+
end
|
36
|
+
}
|
37
|
+
/
|
38
|
+
variable_name {
|
39
|
+
def is_numeric?
|
40
|
+
false
|
41
|
+
end
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
grammar VariableGrammar
|
2
|
+
|
3
|
+
include ValueGrammar
|
4
|
+
|
5
|
+
rule variable_definition
|
6
|
+
# It can be set to a integer also, but decimal handles that case
|
7
|
+
'$' __name:variable_name '=' __value:variable_or_decimal <Stretto::Tokens::VariableDefinitionToken>
|
8
|
+
end
|
9
|
+
|
10
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'music_element')
|
2
|
+
|
3
|
+
module Stretto
|
4
|
+
module MusicElements
|
5
|
+
|
6
|
+
# ChannelPressure sends a MIDI message similar to applying pressure to an
|
7
|
+
# electronic keyboard. This event applies pressure to the whole channel, and its
|
8
|
+
# value can rango from 0 to 127
|
9
|
+
class ChannelPressure < MusicElement
|
10
|
+
|
11
|
+
MAX_CHANNEL_PRESSURE_VALUE = 127
|
12
|
+
|
13
|
+
def initialize(string_or_options, pattern = nil)
|
14
|
+
token = case string_or_options
|
15
|
+
when String then Stretto::Parser.parse_channel_pressure!(string_or_options)
|
16
|
+
else string_or_options
|
17
|
+
end
|
18
|
+
super(token[:text_value], pattern)
|
19
|
+
@original_value = token[:value]
|
20
|
+
end
|
21
|
+
|
22
|
+
# Sets value and validates in range
|
23
|
+
def value=(value)
|
24
|
+
if value < 0 or value > MAX_CHANNEL_PRESSURE_VALUE
|
25
|
+
raise Exceptions::ValueOutOfBoundsException.new("Channel pressure should be in range 0..#{MAX_CHANNEL_PRESSURE_VALUE}")
|
26
|
+
end
|
27
|
+
@value = value
|
28
|
+
end
|
29
|
+
|
30
|
+
def value
|
31
|
+
@value || @original_value.to_i(@pattern)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# @private
|
37
|
+
def substitute_variables!
|
38
|
+
self.value = value
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,194 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'music_element')
|
2
|
+
require File.join(File.dirname(__FILE__), 'modifiers/attack_decay')
|
3
|
+
require File.join(File.dirname(__FILE__), 'modifiers/chord_intervals')
|
4
|
+
require 'forwardable'
|
5
|
+
|
6
|
+
module Stretto
|
7
|
+
module MusicElements
|
8
|
+
|
9
|
+
# A chord indicates a group of notes that can be played together.
|
10
|
+
#
|
11
|
+
# The most common chord is a named chord. This is indicated by a note string
|
12
|
+
# (see {Note}) and the name of the chord. For example, chord C major is
|
13
|
+
# represented by +Cmaj+, and it will consist on the notes C, E and G (for definition,
|
14
|
+
# according to the musical theory).
|
15
|
+
#
|
16
|
+
# The chord can also indicate a duration (see {Duration}) and attack and decay (see
|
17
|
+
# {AttackDecay}), which have to be inserted after the named chord. For example, the chord
|
18
|
+
# +C#5maj7wa80d100+ has a base note of +C#5+, is the major 7th chord (+maj7+) with a
|
19
|
+
# whole duration (+w+), and has attack 80 and decay 100 (+a80d100+). The default octave
|
20
|
+
# for chords is 3
|
21
|
+
#
|
22
|
+
# For a list of supported chords see the JFugue reference guide, or the source code
|
23
|
+
# for {CHORD_INTERVALS}
|
24
|
+
#
|
25
|
+
# A chord can be inverted; that is, some of its notes is raised or lowered an octave, also
|
26
|
+
# called as the voicing of a chord. Chord inversions can be specified by two ways.
|
27
|
+
# The first one is to indicate with the symbol +^+ the number of notes that will be raised
|
28
|
+
# a whole octave (for example, the chord +C5maj+ will have notes C5, E5 and G5, its
|
29
|
+
# inversion +C5maj^+ will have E5, G5 and E6, and +C5maj^^+ notes G5, C6 and E6). The other
|
30
|
+
# way is to indicate explicitely the pivot note to invert from (the note +C5maj^E5+ is
|
31
|
+
# equivalent to the first inversion). It will raise an error if a chord is tried to
|
32
|
+
# be inverted by a note that is not part of the chord.
|
33
|
+
#
|
34
|
+
# This class can also hold non-regular chords, called harmonic chords. See {HarmonicChord}
|
35
|
+
class Chord < MusicElement
|
36
|
+
|
37
|
+
include Duration
|
38
|
+
include AttackDecay
|
39
|
+
|
40
|
+
DEFAULT_OCTAVE = 3
|
41
|
+
|
42
|
+
attr_reader :original_named_chord, :named_chord
|
43
|
+
attr_reader :key_signature
|
44
|
+
attr_accessor :instrument
|
45
|
+
|
46
|
+
extend Forwardable
|
47
|
+
def_delegators :@base_note, :original_accidental, :accidental,
|
48
|
+
:original_pitch, :pitch,
|
49
|
+
:original_key, :key,
|
50
|
+
:original_octave, :octave,
|
51
|
+
:original_attack, :attack,
|
52
|
+
:original_decay, :decay
|
53
|
+
|
54
|
+
def initialize(string_or_options, pattern = nil)
|
55
|
+
token = case string_or_options
|
56
|
+
when String then Stretto::Parser.parse_chord!(string_or_options)
|
57
|
+
else string_or_options
|
58
|
+
end
|
59
|
+
super(token[:text_value], pattern)
|
60
|
+
unless @notes = token[:notes]
|
61
|
+
build_duration_from_token(token[:duration])
|
62
|
+
@original_base_note = token[:base_note]
|
63
|
+
@original_named_chord = token[:named_chord]
|
64
|
+
@named_chord = @original_named_chord.downcase
|
65
|
+
@original_inversions = token[:inversions]
|
66
|
+
@base_note = base_note
|
67
|
+
build_attack_and_decay(token[:attack], token[:decay])
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# @return [Array(MusicElements::Note)] The array of notes generated by this chord
|
72
|
+
def notes
|
73
|
+
unless @notes
|
74
|
+
build_chord_notes(@named_chord)
|
75
|
+
build_inversions
|
76
|
+
end
|
77
|
+
@notes
|
78
|
+
end
|
79
|
+
|
80
|
+
# Returns the base note of the chord, that is, the note the chord was specified with.
|
81
|
+
#
|
82
|
+
# Note that this is not necessarily equal to the first note in the chord. If it has
|
83
|
+
# inversions, the base note will be kept as the original note, but the
|
84
|
+
# +notes+ construct will be effectively transposed.
|
85
|
+
def base_note
|
86
|
+
@base_note || build_base_note(@original_base_note)
|
87
|
+
end
|
88
|
+
|
89
|
+
# @return [Boolean] True if all the notes of the chord are equal
|
90
|
+
# @see MusicElements::Note#==
|
91
|
+
def ==(other)
|
92
|
+
notes && notes == other.notes
|
93
|
+
end
|
94
|
+
|
95
|
+
# @return [Number] The number of inversions of the chord
|
96
|
+
# @example
|
97
|
+
# Chord.new("Cmaj^^").inversions # => 2
|
98
|
+
def inversions
|
99
|
+
build_inversions unless @inversions
|
100
|
+
@inversions
|
101
|
+
end
|
102
|
+
|
103
|
+
# @return [MusicElements::Note] The pivot note in which do the inversion, if specified
|
104
|
+
# @example
|
105
|
+
# Chord.new("Cmaj^E").pivot_note # => Note<@key="E">
|
106
|
+
def pivot_note
|
107
|
+
build_inversions unless @pivot_note
|
108
|
+
@pivot_note
|
109
|
+
end
|
110
|
+
|
111
|
+
# Assigns the key signature for the chord, and for all of its notes.
|
112
|
+
#
|
113
|
+
# @see MusicElements::Note#key_signature=
|
114
|
+
def key_signature=(key_signature)
|
115
|
+
@key_signature = key_signature
|
116
|
+
if @base_note
|
117
|
+
@base_note.key_signature = key_signature
|
118
|
+
build_chord_notes(@named_chord)
|
119
|
+
build_inversions
|
120
|
+
end
|
121
|
+
@notes.each{ |note| note.pattern = @pattern }
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
# Builds the base_note according to the parsed token
|
127
|
+
def build_base_note(base_note_options)
|
128
|
+
Note.new({
|
129
|
+
:text_value => base_note_options[:text_value],
|
130
|
+
:octave => base_note_options[:octave] || DEFAULT_OCTAVE,
|
131
|
+
:accidental => base_note_options[:accidental],
|
132
|
+
:key => base_note_options[:key],
|
133
|
+
:pitch => base_note_options[:pitch],
|
134
|
+
:attack => base_note_options[:attack],
|
135
|
+
:decay => base_note_options[:decay],
|
136
|
+
:duration => base_note_options[:duration] },
|
137
|
+
@pattern
|
138
|
+
)
|
139
|
+
end
|
140
|
+
|
141
|
+
# When a named chord is specified, returns an array of the notes consisting
|
142
|
+
# on the base note plus the note with the intervals applied
|
143
|
+
def build_chord_notes(named_chord)
|
144
|
+
@named_chord = named_chord
|
145
|
+
intervals = CHORD_INTERVALS[@named_chord]
|
146
|
+
@notes = [@base_note] + intervals.map{|interval| @base_note + interval}
|
147
|
+
end
|
148
|
+
|
149
|
+
# Builds either implicit (+^+) or explicit (+^+_note_) inversions
|
150
|
+
def build_inversions
|
151
|
+
if @original_inversions
|
152
|
+
@inversions = @original_inversions[:inversions]
|
153
|
+
pivot_note = @original_inversions[:pivot_note]
|
154
|
+
if pivot_note
|
155
|
+
@pivot_note = Note.new({
|
156
|
+
:text_value => pivot_note.text_value,
|
157
|
+
:key => pivot_note.key,
|
158
|
+
:pitch => pivot_note.pitch,
|
159
|
+
:accidental => pivot_note.accidental,
|
160
|
+
:octave => pivot_note.octave || DEFAULT_OCTAVE,
|
161
|
+
:duration => @original_duration_token,
|
162
|
+
:attack => @original_attack,
|
163
|
+
:decay => @original_decay
|
164
|
+
}, @pattern)
|
165
|
+
@pivot_note.pattern = @pattern
|
166
|
+
end
|
167
|
+
else
|
168
|
+
@inversions = 0
|
169
|
+
end
|
170
|
+
raise Exceptions::ChordInversionsException.new("Number of inversions (#{@inversions}) is greater than chord size (#{notes.size})") if @inversions >= notes.size
|
171
|
+
|
172
|
+
notes.each{ |note| note.pattern = @pattern }
|
173
|
+
if @pivot_note
|
174
|
+
actual_pivot = notes.index(notes.find { |note| @pivot_note.pitch == note.pitch }) # TODO: Dropped notes.index(&block) for compatibility with <1.8.7
|
175
|
+
raise Exceptions::ChordInversionsException.new("Note #{@pivot_note.original_string}(#{@pivot_note.pitch}) does not belong to chord #{@original_string}") unless actual_pivot
|
176
|
+
actual_pivot.times { notes << notes.shift + 12 }
|
177
|
+
else
|
178
|
+
@inversions.times { notes << notes.shift + 12 }
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
|
183
|
+
# @private
|
184
|
+
# @see MusicElements::MusicElement#substitute_variables!
|
185
|
+
def substitute_variables!
|
186
|
+
@base_note.pattern = @pattern
|
187
|
+
build_chord_notes(@named_chord)
|
188
|
+
build_inversions
|
189
|
+
end
|
190
|
+
|
191
|
+
end
|
192
|
+
|
193
|
+
end
|
194
|
+
end
|