musicality 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (141) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.rspec +2 -0
  4. data/.ruby-version +1 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +47 -0
  8. data/Rakefile +65 -0
  9. data/bin/midify +78 -0
  10. data/examples/hip.rb +32 -0
  11. data/examples/missed_connection.rb +26 -0
  12. data/examples/song1.rb +33 -0
  13. data/examples/song2.rb +32 -0
  14. data/lib/musicality/errors.rb +9 -0
  15. data/lib/musicality/notation/conversion/change_conversion.rb +19 -0
  16. data/lib/musicality/notation/conversion/measure_note_map.rb +40 -0
  17. data/lib/musicality/notation/conversion/measured_score_conversion.rb +70 -0
  18. data/lib/musicality/notation/conversion/measured_score_converter.rb +95 -0
  19. data/lib/musicality/notation/conversion/note_time_converter.rb +68 -0
  20. data/lib/musicality/notation/conversion/tempo_conversion.rb +25 -0
  21. data/lib/musicality/notation/conversion/unmeasured_score_conversion.rb +47 -0
  22. data/lib/musicality/notation/conversion/unmeasured_score_converter.rb +64 -0
  23. data/lib/musicality/notation/model/articulations.rb +13 -0
  24. data/lib/musicality/notation/model/change.rb +62 -0
  25. data/lib/musicality/notation/model/dynamics.rb +12 -0
  26. data/lib/musicality/notation/model/link.rb +73 -0
  27. data/lib/musicality/notation/model/meter.rb +54 -0
  28. data/lib/musicality/notation/model/meters.rb +9 -0
  29. data/lib/musicality/notation/model/note.rb +120 -0
  30. data/lib/musicality/notation/model/part.rb +54 -0
  31. data/lib/musicality/notation/model/pitch.rb +163 -0
  32. data/lib/musicality/notation/model/pitches.rb +21 -0
  33. data/lib/musicality/notation/model/program.rb +53 -0
  34. data/lib/musicality/notation/model/score.rb +132 -0
  35. data/lib/musicality/notation/packing/change_packing.rb +46 -0
  36. data/lib/musicality/notation/packing/part_packing.rb +31 -0
  37. data/lib/musicality/notation/packing/program_packing.rb +16 -0
  38. data/lib/musicality/notation/packing/score_packing.rb +108 -0
  39. data/lib/musicality/notation/parsing/articulation_parsing.rb +264 -0
  40. data/lib/musicality/notation/parsing/articulation_parsing.treetop +59 -0
  41. data/lib/musicality/notation/parsing/convenience_methods.rb +74 -0
  42. data/lib/musicality/notation/parsing/duration_nodes.rb +21 -0
  43. data/lib/musicality/notation/parsing/duration_parsing.rb +205 -0
  44. data/lib/musicality/notation/parsing/duration_parsing.treetop +25 -0
  45. data/lib/musicality/notation/parsing/link_nodes.rb +35 -0
  46. data/lib/musicality/notation/parsing/link_parsing.rb +270 -0
  47. data/lib/musicality/notation/parsing/link_parsing.treetop +33 -0
  48. data/lib/musicality/notation/parsing/meter_parsing.rb +190 -0
  49. data/lib/musicality/notation/parsing/meter_parsing.treetop +29 -0
  50. data/lib/musicality/notation/parsing/note_node.rb +40 -0
  51. data/lib/musicality/notation/parsing/note_parsing.rb +229 -0
  52. data/lib/musicality/notation/parsing/note_parsing.treetop +28 -0
  53. data/lib/musicality/notation/parsing/numbers/nonnegative_float_parsing.rb +289 -0
  54. data/lib/musicality/notation/parsing/numbers/nonnegative_float_parsing.treetop +29 -0
  55. data/lib/musicality/notation/parsing/numbers/nonnegative_integer_parsing.rb +64 -0
  56. data/lib/musicality/notation/parsing/numbers/nonnegative_integer_parsing.treetop +17 -0
  57. data/lib/musicality/notation/parsing/numbers/nonnegative_rational_parsing.rb +86 -0
  58. data/lib/musicality/notation/parsing/numbers/nonnegative_rational_parsing.treetop +20 -0
  59. data/lib/musicality/notation/parsing/numbers/positive_float_parsing.rb +503 -0
  60. data/lib/musicality/notation/parsing/numbers/positive_float_parsing.treetop +33 -0
  61. data/lib/musicality/notation/parsing/numbers/positive_integer_parsing.rb +95 -0
  62. data/lib/musicality/notation/parsing/numbers/positive_integer_parsing.treetop +19 -0
  63. data/lib/musicality/notation/parsing/numbers/positive_rational_parsing.rb +84 -0
  64. data/lib/musicality/notation/parsing/numbers/positive_rational_parsing.treetop +19 -0
  65. data/lib/musicality/notation/parsing/parseable.rb +30 -0
  66. data/lib/musicality/notation/parsing/pitch_node.rb +23 -0
  67. data/lib/musicality/notation/parsing/pitch_parsing.rb +448 -0
  68. data/lib/musicality/notation/parsing/pitch_parsing.treetop +52 -0
  69. data/lib/musicality/notation/parsing/segment_parsing.rb +141 -0
  70. data/lib/musicality/notation/parsing/segment_parsing.treetop +23 -0
  71. data/lib/musicality/notation/util/interpolation.rb +16 -0
  72. data/lib/musicality/notation/util/piecewise_function.rb +122 -0
  73. data/lib/musicality/notation/util/value_computer.rb +170 -0
  74. data/lib/musicality/performance/conversion/glissando_converter.rb +34 -0
  75. data/lib/musicality/performance/conversion/note_sequence_extractor.rb +98 -0
  76. data/lib/musicality/performance/conversion/portamento_converter.rb +24 -0
  77. data/lib/musicality/performance/conversion/score_collator.rb +126 -0
  78. data/lib/musicality/performance/midi/midi_events.rb +34 -0
  79. data/lib/musicality/performance/midi/midi_util.rb +31 -0
  80. data/lib/musicality/performance/midi/part_sequencer.rb +123 -0
  81. data/lib/musicality/performance/midi/score_sequencer.rb +45 -0
  82. data/lib/musicality/performance/model/note_attacks.rb +19 -0
  83. data/lib/musicality/performance/model/note_sequence.rb +111 -0
  84. data/lib/musicality/performance/util/note_linker.rb +28 -0
  85. data/lib/musicality/performance/util/optimization.rb +31 -0
  86. data/lib/musicality/validatable.rb +38 -0
  87. data/lib/musicality/version.rb +3 -0
  88. data/lib/musicality.rb +81 -0
  89. data/musicality.gemspec +30 -0
  90. data/spec/musicality_spec.rb +7 -0
  91. data/spec/notation/conversion/change_conversion_spec.rb +40 -0
  92. data/spec/notation/conversion/measure_note_map_spec.rb +73 -0
  93. data/spec/notation/conversion/measured_score_conversion_spec.rb +141 -0
  94. data/spec/notation/conversion/measured_score_converter_spec.rb +329 -0
  95. data/spec/notation/conversion/note_time_converter_spec.rb +81 -0
  96. data/spec/notation/conversion/tempo_conversion_spec.rb +40 -0
  97. data/spec/notation/conversion/unmeasured_score_conversion_spec.rb +71 -0
  98. data/spec/notation/conversion/unmeasured_score_converter_spec.rb +116 -0
  99. data/spec/notation/model/change_spec.rb +90 -0
  100. data/spec/notation/model/link_spec.rb +83 -0
  101. data/spec/notation/model/meter_spec.rb +97 -0
  102. data/spec/notation/model/note_spec.rb +183 -0
  103. data/spec/notation/model/part_spec.rb +69 -0
  104. data/spec/notation/model/pitch_spec.rb +180 -0
  105. data/spec/notation/model/program_spec.rb +50 -0
  106. data/spec/notation/model/score_spec.rb +211 -0
  107. data/spec/notation/packing/change_packing_spec.rb +153 -0
  108. data/spec/notation/packing/part_packing_spec.rb +66 -0
  109. data/spec/notation/packing/program_packing_spec.rb +33 -0
  110. data/spec/notation/packing/score_packing_spec.rb +301 -0
  111. data/spec/notation/parsing/articulation_parsing_spec.rb +23 -0
  112. data/spec/notation/parsing/convenience_methods_spec.rb +99 -0
  113. data/spec/notation/parsing/duration_nodes_spec.rb +83 -0
  114. data/spec/notation/parsing/duration_parsing_spec.rb +70 -0
  115. data/spec/notation/parsing/link_nodes_spec.rb +30 -0
  116. data/spec/notation/parsing/link_parsing_spec.rb +13 -0
  117. data/spec/notation/parsing/meter_parsing_spec.rb +23 -0
  118. data/spec/notation/parsing/note_node_spec.rb +87 -0
  119. data/spec/notation/parsing/note_parsing_spec.rb +46 -0
  120. data/spec/notation/parsing/numbers/nonnegative_float_spec.rb +28 -0
  121. data/spec/notation/parsing/numbers/nonnegative_integer_spec.rb +11 -0
  122. data/spec/notation/parsing/numbers/nonnegative_rational_spec.rb +11 -0
  123. data/spec/notation/parsing/numbers/positive_float_spec.rb +28 -0
  124. data/spec/notation/parsing/numbers/positive_integer_spec.rb +28 -0
  125. data/spec/notation/parsing/numbers/positive_rational_spec.rb +28 -0
  126. data/spec/notation/parsing/pitch_node_spec.rb +38 -0
  127. data/spec/notation/parsing/pitch_parsing_spec.rb +14 -0
  128. data/spec/notation/parsing/segment_parsing_spec.rb +27 -0
  129. data/spec/notation/util/value_computer_spec.rb +146 -0
  130. data/spec/performance/conversion/glissando_converter_spec.rb +93 -0
  131. data/spec/performance/conversion/note_sequence_extractor_spec.rb +230 -0
  132. data/spec/performance/conversion/portamento_converter_spec.rb +91 -0
  133. data/spec/performance/conversion/score_collator_spec.rb +183 -0
  134. data/spec/performance/midi/midi_util_spec.rb +110 -0
  135. data/spec/performance/midi/part_sequencer_spec.rb +40 -0
  136. data/spec/performance/midi/score_sequencer_spec.rb +50 -0
  137. data/spec/performance/model/note_sequence_spec.rb +147 -0
  138. data/spec/performance/util/note_linker_spec.rb +68 -0
  139. data/spec/performance/util/optimization_spec.rb +73 -0
  140. data/spec/spec_helper.rb +43 -0
  141. metadata +323 -0
@@ -0,0 +1,25 @@
1
+ module Musicality
2
+
3
+ class Tempo
4
+ class QNPM
5
+ def self.to_bpm qnpm, beat_dur
6
+ Rational(qnpm,4*beat_dur)
7
+ end
8
+
9
+ def self.to_nps qnpm
10
+ Rational(qnpm,240)
11
+ end
12
+ end
13
+
14
+ class BPM
15
+ def self.to_qnpm bpm, beat_dur
16
+ 4*beat_dur*bpm
17
+ end
18
+
19
+ def self.to_nps bpm, beat_dur
20
+ Rational(bpm*beat_dur,60)
21
+ end
22
+ end
23
+ end
24
+
25
+ end
@@ -0,0 +1,47 @@
1
+ module Musicality
2
+
3
+ class Score
4
+ class Unmeasured < TempoBased
5
+ # Convert to unmeasured score by converting measure-based offsets to
6
+ # note-based offsets, and eliminating the use of meters. Also, tempo is
7
+ # coverted from beats-per-minute to quarter-notes per minute.
8
+ def to_timed tempo_sample_rate
9
+ UnmeasuredScoreConverter.new(self,tempo_sample_rate).convert_score
10
+ end
11
+
12
+ def note_time_map tempo_sample_rate
13
+ tempo_computer = ValueComputer.new(@start_tempo, @tempo_changes)
14
+ ntc = NoteTimeConverter.new(tempo_computer, tempo_sample_rate)
15
+ ntc.note_time_map(note_offsets)
16
+ end
17
+
18
+ def note_offsets
19
+ noffs = Set.new([0.to_r])
20
+
21
+ @tempo_changes.each do |noff,change|
22
+ noffs += change.offsets(noff)
23
+ end
24
+
25
+ @parts.values.each do |part|
26
+ noff = 0.to_r
27
+ part.notes.each do |note|
28
+ noff += note.duration
29
+ noffs.add(noff)
30
+ end
31
+
32
+ part.dynamic_changes.each do |noff,change|
33
+ noffs += change.offsets(noff)
34
+ end
35
+ end
36
+
37
+ @program.segments.each do |seg|
38
+ noffs.add(seg.first)
39
+ noffs.add(seg.last)
40
+ end
41
+
42
+ return noffs.sort
43
+ end
44
+ end
45
+ end
46
+
47
+ end
@@ -0,0 +1,64 @@
1
+ require 'set'
2
+
3
+ module Musicality
4
+
5
+ # Converts unmeasured score to timed score, by converting note-based offsets
6
+ # and durations to time-based, and eliminating the use of tempo.
7
+ class UnmeasuredScoreConverter
8
+
9
+ def initialize score, tempo_sample_rate
10
+ unless score.valid?
11
+ raise NotValidError, "The given score can not be converted because \
12
+ it is invalid, with these errors: #{score.errors}"
13
+ end
14
+
15
+ @score = score
16
+ @note_time_map = score.note_time_map(tempo_sample_rate)
17
+ end
18
+
19
+ def convert_score
20
+ Score::Timed.new(parts: convert_parts, program: convert_program)
21
+ end
22
+
23
+ # Convert note-based offsets & durations to time-based.
24
+ def convert_parts
25
+ Hash[ @score.parts.map do |name,part|
26
+ offset = 0.to_r
27
+
28
+ new_notes = part.notes.map do |note|
29
+ starttime = @note_time_map[offset]
30
+ endtime = @note_time_map[offset + note.duration]
31
+ offset += note.duration
32
+ newnote = note.clone
33
+ newnote.duration = endtime - starttime
34
+ newnote
35
+ end
36
+
37
+ new_dcs = Hash[ part.dynamic_changes.map do |noff,change|
38
+ case change
39
+ when Change::Immediate
40
+ [@note_time_map[noff],change.clone]
41
+ when Change::Gradual
42
+ toff1, toff2, toff3, toff4 = change.offsets(noff).map {|x| @note_time_map[x] }
43
+ [toff2, Change::Gradual.new(change.value,
44
+ toff3-toff2, toff2-toff1, toff4-toff3)]
45
+ end
46
+ end ]
47
+
48
+ [name, Part.new(part.start_dynamic,
49
+ notes: new_notes, dynamic_changes: new_dcs)]
50
+ end]
51
+ end
52
+
53
+ # Convert note-based offsets & durations to time-based.
54
+ def convert_program
55
+ newsegments = @score.program.segments.map do |segment|
56
+ first = @note_time_map[segment.first]
57
+ last = @note_time_map[segment.last]
58
+ first...last
59
+ end
60
+ Program.new(newsegments)
61
+ end
62
+ end
63
+
64
+ end
@@ -0,0 +1,13 @@
1
+ module Musicality
2
+
3
+ module Articulations
4
+ NORMAL = :normal
5
+ SLUR = :slur
6
+ LEGATO = :legato
7
+ TENUTO = :tenuto
8
+ PORTATO = :portato
9
+ STACCATO = :staccato
10
+ STACCATISSIMO = :staccatissimo
11
+ end
12
+
13
+ end
@@ -0,0 +1,62 @@
1
+ module Musicality
2
+
3
+ class Change
4
+ attr_reader :value, :duration
5
+
6
+ def initialize value, duration
7
+ @value = value
8
+ @duration = duration
9
+ end
10
+
11
+ def ==(other)
12
+ self.class == other.class &&
13
+ self.value == other.value &&
14
+ self.duration == other.duration
15
+ end
16
+
17
+ class Immediate < Change
18
+ def initialize value
19
+ super(value,0)
20
+ end
21
+
22
+ def clone
23
+ Immediate.new(@value)
24
+ end
25
+ end
26
+
27
+ class Gradual < Change
28
+ attr_reader :elapsed, :impending, :remaining, :total_duration
29
+
30
+ def initialize value, impending, elapsed=0, remaining=0
31
+ if elapsed < 0
32
+ raise NegativeError, "elapsed (#{elapsed}) is < 0"
33
+ end
34
+
35
+ if impending <= 0
36
+ raise NonPositiveError, "impending (#{impending}) is <= 0"
37
+ end
38
+
39
+ if remaining < 0
40
+ raise NegativeError, "remaining #{remaining} is < 0"
41
+ end
42
+
43
+ @total_duration = elapsed + impending + remaining
44
+ @elapsed = elapsed
45
+ @impending = impending
46
+ @remaining = remaining
47
+ super(value,impending)
48
+ end
49
+
50
+ def ==(other)
51
+ super(other) &&
52
+ @elapsed == other.elapsed &&
53
+ @remaining == other.remaining
54
+ end
55
+
56
+ def clone
57
+ Gradual.new(@value, @impending, @elapsed, @remaining)
58
+ end
59
+ end
60
+ end
61
+
62
+ end
@@ -0,0 +1,12 @@
1
+ module Musicality
2
+ module Dynamics
3
+ PPP = 0.125
4
+ PP = 0.25
5
+ P = 0.375
6
+ MP = 0.5
7
+ MF = 0.625
8
+ F = 0.75
9
+ FF = 0.875
10
+ FFF = 1.0
11
+ end
12
+ end
@@ -0,0 +1,73 @@
1
+ module Musicality
2
+
3
+ # Connect one note pitch to the target pitch of the next note, via slur, legato, etc.
4
+ #
5
+ # @!attribute [rw] target_pitch
6
+ # @return [Pitch] The pitch of the note which is being connected to.
7
+ #
8
+ class Link
9
+ def clone
10
+ Marshal.load(Marshal.dump(self))
11
+ end
12
+
13
+ class Tie < Link
14
+ def initialize; end
15
+
16
+ def ==(other)
17
+ self.class == other.class
18
+ end
19
+
20
+ def transpose diff
21
+ self.clone.transpose! diff
22
+ end
23
+
24
+ def transpose! diff
25
+ return self
26
+ end
27
+
28
+ def to_s; "="; end
29
+ end
30
+
31
+ class TargetedLink < Link
32
+ attr_accessor :target_pitch
33
+
34
+ def initialize target_pitch
35
+ @target_pitch = target_pitch
36
+ end
37
+
38
+ def ==(other)
39
+ self.class == other.class && @target_pitch == other.target_pitch
40
+ end
41
+
42
+ def transpose diff
43
+ self.clone.transpose! diff
44
+ end
45
+
46
+ def transpose! diff
47
+ @target_pitch = @target_pitch.transpose(diff)
48
+ return self
49
+ end
50
+
51
+ def to_s
52
+ link_char + @target_pitch.to_s
53
+ end
54
+ end
55
+
56
+ class Glissando < TargetedLink
57
+ def link_char; "~"; end
58
+ end
59
+
60
+ class Portamento < TargetedLink
61
+ def link_char; "/"; end
62
+ end
63
+
64
+ class Slur < TargetedLink
65
+ def link_char; "="; end
66
+ end
67
+
68
+ class Legato < TargetedLink
69
+ def link_char; "|"; end
70
+ end
71
+ end
72
+
73
+ end
@@ -0,0 +1,54 @@
1
+ module Musicality
2
+
3
+ class Meter
4
+ include Validatable
5
+
6
+ attr_reader :measure_duration, :beat_duration, :beats_per_measure
7
+
8
+ def initialize beats_per_measure, beat_duration
9
+ @beats_per_measure = beats_per_measure
10
+ @beat_duration = beat_duration
11
+ @measure_duration = beats_per_measure * beat_duration
12
+ end
13
+
14
+ def check_methods
15
+ [ :check_beats_per_measure, :check_beat_duration ]
16
+ end
17
+
18
+ def check_beats_per_measure
19
+ unless @beats_per_measure > 0
20
+ raise NonPositiveError, "beats per measure #{@beats_per_measure} is not positive"
21
+ end
22
+
23
+ unless @beats_per_measure.is_a?(Integer)
24
+ raise NonIntegerError, "beats per measure #{@beats_per_measure} is not an integer"
25
+ end
26
+ end
27
+
28
+ def check_beat_duration
29
+ unless @beat_duration > 0
30
+ raise NonPositiveError, "beat duration #{@beat_duration} is not positive"
31
+ end
32
+
33
+ unless @beat_duration > 0
34
+ raise NonRationalError, "beat duration #{@beat_duration} is a rational"
35
+ end
36
+ end
37
+
38
+ def ==(other)
39
+ return (@beats_per_measure == other.beats_per_measure &&
40
+ @beat_duration == other.beat_duration)
41
+ end
42
+
43
+ def to_s
44
+ if beat_duration.numerator == 1
45
+ num = beats_per_measure * beat_duration.numerator
46
+ den = beat_duration.denominator
47
+ "#{num}/#{den}"
48
+ else
49
+ "#{beats_per_measure}*#{beat_duration}"
50
+ end
51
+ end
52
+ end
53
+
54
+ end
@@ -0,0 +1,9 @@
1
+ module Musicality
2
+ module Meters
3
+ TWO_TWO = Meter.new(2,"1/2".to_r)
4
+ TWO_FOUR = Meter.new(2,"1/4".to_r)
5
+ THREE_FOUR = Meter.new(3,"1/4".to_r)
6
+ FOUR_FOUR = Meter.new(4,"1/4".to_r)
7
+ SIX_EIGHT = Meter.new(2,"3/8".to_r)
8
+ end
9
+ end
@@ -0,0 +1,120 @@
1
+ module Musicality
2
+
3
+ require 'set'
4
+
5
+ class Note
6
+ include Validatable
7
+
8
+ attr_reader :pitches, :links
9
+ attr_accessor :articulation, :duration, :accented
10
+
11
+ DEFAULT_ARTICULATION = Articulations::NORMAL
12
+
13
+ def initialize duration, pitches = [], articulation: DEFAULT_ARTICULATION, accented: false, links: {}
14
+ @duration = duration
15
+ if !pitches.is_a? Enumerable
16
+ pitches = [ pitches ]
17
+ end
18
+ @pitches = Set.new(pitches).sort
19
+ @articulation = articulation
20
+ @accented = accented
21
+ @links = links
22
+ end
23
+
24
+ def check_methods
25
+ [ :ensure_positive_duration ]
26
+ end
27
+
28
+ def ensure_positive_duration
29
+ unless @duration > 0
30
+ raise NonPositiveError, "duration #{@duration} is not positive"
31
+ end
32
+ end
33
+
34
+ def == other
35
+ return (@duration == other.duration) &&
36
+ (self.pitches == other.pitches) &&
37
+ (@links.to_a.sort == other.links.to_a.sort) &&
38
+ (@articulation == other.articulation) &&
39
+ (@accented == other.accented)
40
+ end
41
+
42
+ def clone
43
+ Marshal.load(Marshal.dump(self))
44
+ end
45
+
46
+ def transpose diff
47
+ self.clone.transpose! diff
48
+ end
49
+
50
+ def transpose! diff
51
+ @pitches = @pitches.map {|pitch| pitch.transpose(diff) }
52
+ @links = Hash[ @links.map do |k,v|
53
+ [ k.transpose(diff), v.transpose(diff) ]
54
+ end ]
55
+ return self
56
+ end
57
+
58
+ def stretch ratio
59
+ self.clone.stretch! ratio
60
+ end
61
+
62
+ def stretch! ratio
63
+ @duration *= ratio
64
+ return self
65
+ end
66
+
67
+ def to_s
68
+ d = @duration.to_r
69
+ if d.denominator == 1
70
+ dur_str = "#{d.numerator}"
71
+ elsif d.numerator == 1
72
+ dur_str = "/#{d.denominator}"
73
+ else
74
+ dur_str = d.to_s
75
+ end
76
+
77
+ art_str = case @articulation
78
+ when Articulations::SLUR then "="
79
+ when Articulations::LEGATO then "|"
80
+ when Articulations::TENUTO then "_"
81
+ when Articulations::PORTATO then "%"
82
+ when Articulations::STACCATO then "."
83
+ when Articulations::STACCATISSIMO then "'"
84
+ else ""
85
+ end
86
+
87
+ pitch_links_str = @pitches.map do |p|
88
+ if @links.has_key?(p)
89
+ p.to_s + @links[p].to_s
90
+ else
91
+ p.to_s
92
+ end
93
+ end.join(",")
94
+
95
+ acc_str = @accented ? "!" : ""
96
+ return dur_str + art_str + pitch_links_str + acc_str
97
+ end
98
+
99
+ def self.add_note_method(name, dur)
100
+ self.class.send(:define_method,name.to_sym) do |pitches = [], articulation: DEFAULT_ARTICULATION, links: {}, accented: false|
101
+ Note.new(dur, pitches, articulation: articulation, links: links, accented: accented)
102
+ end
103
+ end
104
+
105
+ {
106
+ :sixteenth => Rational(1,16),
107
+ :dotted_SIXTEENTH => Rational(3,32),
108
+ :eighth => Rational(1,8),
109
+ :dotted_eighth => Rational(3,16),
110
+ :quarter => Rational(1,4),
111
+ :dotted_quarter => Rational(3,8),
112
+ :half => Rational(1,2),
113
+ :dotted_half => Rational(3,4),
114
+ :whole => Rational(1),
115
+ }.each do |meth_name, dur|
116
+ add_note_method meth_name, dur
117
+ end
118
+ end
119
+
120
+ end
@@ -0,0 +1,54 @@
1
+ require 'yaml'
2
+
3
+ module Musicality
4
+
5
+ class Part
6
+ include Validatable
7
+
8
+ attr_accessor :start_dynamic, :dynamic_changes, :notes
9
+
10
+ def initialize start_dynamic, notes: [], dynamic_changes: {}
11
+ @notes = notes
12
+ @start_dynamic = start_dynamic
13
+ @dynamic_changes = dynamic_changes
14
+
15
+ yield(self) if block_given?
16
+ end
17
+
18
+ def check_methods
19
+ [:ensure_start_dynamic, :ensure_dynamic_change_values_range ]
20
+ end
21
+
22
+ def validatables
23
+ @notes
24
+ end
25
+
26
+ def clone
27
+ Marshal.load(Marshal.dump(self))
28
+ end
29
+
30
+ def ==(other)
31
+ return (@notes == other.notes) &&
32
+ (@start_dynamic == other.start_dynamic) &&
33
+ (@dynamic_changes == other.dynamic_changes)
34
+ end
35
+
36
+ def duration
37
+ return @notes.inject(0) { |sum, note| sum + note.duration }
38
+ end
39
+
40
+ def ensure_start_dynamic
41
+ unless @start_dynamic.between?(0,1)
42
+ raise RangeError, "start dynamic #{@start_dynamic} is not between 0 and 1"
43
+ end
44
+ end
45
+
46
+ def ensure_dynamic_change_values_range
47
+ outofrange = @dynamic_changes.values.select {|v| !v.value.between?(0,1) }
48
+ if outofrange.any?
49
+ raise RangeError, "dynamic change values #{outofrange} are not between 0 and 1"
50
+ end
51
+ end
52
+ end
53
+
54
+ end
@@ -0,0 +1,163 @@
1
+ module Musicality
2
+
3
+ # Abstraction of a musical pitch. Contains values for octave and semitone.
4
+ #
5
+ # Octaves represent the largest means of differing two pitches. Each
6
+ # octave added will double the ratio. At zero octaves, the ratio is
7
+ # 1.0. At one octave, the ratio will be 2.0. Each semitone is an increment
8
+ # of less-than-power-of-two.
9
+ #
10
+ # Semitones are the primary steps between octaves. The number of
11
+ # semitones per octave is 12.
12
+
13
+ # @author James Tunnell
14
+ #
15
+ # @!attribute [r] octave
16
+ # @return [Fixnum] The pitch octave.
17
+ # @!attribute [r] semitone
18
+ # @return [Fixnum] The pitch semitone.
19
+ #
20
+ class Pitch
21
+ include Comparable
22
+ attr_reader :octave, :semitone, :cent, :total_cents
23
+
24
+ #The default number of semitones per octave is 12, corresponding to
25
+ # the twelve-tone equal temperment tuning system.
26
+ SEMITONES_PER_OCTAVE = 12
27
+ CENTS_PER_SEMITONE = 100
28
+ CENTS_PER_OCTAVE = SEMITONES_PER_OCTAVE * CENTS_PER_SEMITONE
29
+
30
+ # The base ferquency is C0
31
+ BASE_FREQ = 16.351597831287414
32
+
33
+ def initialize octave:0, semitone:0, cent: 0
34
+ raise NonIntegerError, "octave #{octave} is not an integer" unless octave.is_a?(Integer)
35
+ raise NonIntegerError, "semitone #{semitone} is not an integer" unless semitone.is_a?(Integer)
36
+ raise NonIntegerError, "cent #{cent} is not an integer" unless cent.is_a?(Integer)
37
+
38
+ @octave = octave
39
+ @semitone = semitone
40
+ @cent = cent
41
+ @total_cents = (@octave*SEMITONES_PER_OCTAVE + @semitone)*CENTS_PER_SEMITONE + @cent
42
+ balance!
43
+ end
44
+
45
+ # Return the pitch's frequency, which is determined by multiplying the base
46
+ # frequency and the pitch ratio. Base frequency defaults to DEFAULT_BASE_FREQ,
47
+ # but can be set during initialization to something else by specifying the
48
+ # :base_freq key.
49
+ def freq
50
+ return self.ratio() * BASE_FREQ
51
+ end
52
+
53
+ # Calculate the pitch ratio. Raises 2 to the power of the total cent
54
+ # count divided by cents-per-octave.
55
+ # @return [Float] ratio
56
+ def ratio
57
+ 2.0**(@total_cents.to_f / CENTS_PER_OCTAVE)
58
+ end
59
+
60
+ # Override default hash method.
61
+ def hash
62
+ return @total_cents
63
+ end
64
+
65
+ # Compare pitch equality using total semitone
66
+ def ==(other)
67
+ return (self.class == other.class &&
68
+ @total_cents == other.total_cents)
69
+ end
70
+
71
+ def eql?(other)
72
+ self == other
73
+ end
74
+
75
+ # Compare pitches. A higher ratio or total semitone is considered larger.
76
+ # @param [Pitch] other The pitch object to compare.
77
+ def <=> (other)
78
+ @total_cents <=> other.total_cents
79
+ end
80
+
81
+ # rounds to the nearest semitone
82
+ def round
83
+ if @cent == 0
84
+ self.clone
85
+ else
86
+ Pitch.new(semitone: (@total_cents / CENTS_PER_SEMITONE.to_f).round)
87
+ end
88
+ end
89
+
90
+ # diff in (rounded) semitones
91
+ def diff other
92
+ Rational(@total_cents - other.total_cents, CENTS_PER_SEMITONE)
93
+ end
94
+
95
+ def transpose semitones
96
+ Pitch.new(cent: (@total_cents + semitones * CENTS_PER_SEMITONE).round)
97
+ end
98
+
99
+ def total_semitones
100
+ Rational(@total_cents, CENTS_PER_SEMITONE)
101
+ end
102
+
103
+ def self.from_semitones semitones
104
+ Pitch.new(cent: (semitones * CENTS_PER_SEMITONE).round)
105
+ end
106
+
107
+ def clone
108
+ Pitch.new(cent: @total_cents)
109
+ end
110
+
111
+ def to_s(sharpit = false)
112
+ letter = case semitone
113
+ when 0 then "C"
114
+ when 1 then sharpit ? "C#" : "Db"
115
+ when 2 then "D"
116
+ when 3 then sharpit ? "D#" : "Eb"
117
+ when 4 then "E"
118
+ when 5 then "F"
119
+ when 6 then sharpit ? "F#" : "Gb"
120
+ when 7 then "G"
121
+ when 8 then sharpit ? "G#" : "Ab"
122
+ when 9 then "A"
123
+ when 10 then sharpit ? "A#" : "Bb"
124
+ when 11 then "B"
125
+ end
126
+
127
+ if @cent == 0
128
+ return letter + octave.to_s
129
+ elsif @cent > 0
130
+ return letter + octave.to_s + "+" + @cent.to_s
131
+ else
132
+ return letter + octave.to_s + @cent.to_s
133
+ end
134
+ end
135
+
136
+ def self.from_ratio ratio
137
+ raise NonPositiveError, "ratio #{ratio} is not > 0" unless ratio > 0
138
+ x = Math.log2 ratio
139
+ new(cent: (x * CENTS_PER_OCTAVE).round)
140
+ end
141
+
142
+ def self.from_freq freq
143
+ from_ratio(freq / BASE_FREQ)
144
+ end
145
+
146
+ private
147
+
148
+ # Balance out the octave and semitone count.
149
+ def balance!
150
+ centsTotal = @total_cents
151
+
152
+ @octave = centsTotal / CENTS_PER_OCTAVE
153
+ centsTotal -= @octave * CENTS_PER_OCTAVE
154
+
155
+ @semitone = centsTotal / CENTS_PER_SEMITONE
156
+ centsTotal -= @semitone * CENTS_PER_SEMITONE
157
+
158
+ @cent = centsTotal
159
+ return self
160
+ end
161
+ end
162
+
163
+ end