musicality 0.1.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.
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