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,123 @@
1
+ module Musicality
2
+
3
+ class PartSequencer
4
+ def initialize part, dynamics_sample_rate: 50, cents_per_step: 10
5
+ replace_portamento_with_glissando(part.notes)
6
+
7
+ extractor = NoteSequenceExtractor.new(part.notes, cents_per_step)
8
+ note_sequences = extractor.extract_sequences
9
+ note_events = gather_note_events(note_sequences)
10
+
11
+ dynamic_events = gather_dynamic_events(part.start_dynamic,
12
+ part.dynamic_changes, dynamics_sample_rate)
13
+
14
+ @events = (note_events + dynamic_events).sort
15
+ end
16
+
17
+ def make_midi_track midi_sequence, part_name, channel, ppqn, program
18
+ track = begin_track(midi_sequence, part_name, channel, program)
19
+
20
+ prev_offset = 0
21
+ @events.each do |offset, event|
22
+ if offset == prev_offset
23
+ delta = 0
24
+ else
25
+ delta = MidiUtil.delta(offset - prev_offset, ppqn)
26
+ end
27
+
28
+ track.events << case event
29
+ when MidiEvent::NoteOn
30
+ vel = MidiUtil.note_velocity(event.accented)
31
+ MIDI::NoteOn.new(channel, event.notenum, vel, delta)
32
+ when MidiEvent::NoteOff
33
+ MIDI::NoteOff.new(channel, event.notenum, 127, delta)
34
+ when MidiEvent::Expression
35
+ MIDI::Controller.new(channel, MIDI::CC_EXPRESSION_CONTROLLER, event.volume, delta)
36
+ end
37
+
38
+ prev_offset = offset
39
+ end
40
+ return track
41
+ end
42
+
43
+ private
44
+
45
+ def replace_portamento_with_glissando notes
46
+ notes.each do |note|
47
+ note.links.each do |pitch,link|
48
+ if link.is_a? Link::Portamento
49
+ note.links[pitch] = Link::Glissando.new(link.target_pitch)
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ def gather_note_events note_sequences
56
+ note_events = []
57
+ note_sequences.each do |note_seq|
58
+ pitches = note_seq.pitches.sort
59
+ pitches.each_index do |i|
60
+ offset, pitch = pitches[i]
61
+
62
+ accented = false
63
+ if note_seq.attacks.has_key?(offset)
64
+ accented = note_seq.attacks[offset].accented?
65
+ end
66
+
67
+ note_num = MidiUtil.pitch_to_notenum(pitch)
68
+ on_at = offset
69
+ off_at = (i < (pitches.size - 1)) ? pitches[i+1][0] : note_seq.stop
70
+
71
+ note_events.push [on_at, MidiEvent::NoteOn.new(note_num, accented)]
72
+ note_events.push [off_at, MidiEvent::NoteOff.new(note_num)]
73
+ end
74
+ end
75
+ return note_events
76
+ end
77
+
78
+ def gather_dynamic_events start_dyn, dyn_changes, sample_rate
79
+ dynamic_events = []
80
+
81
+ dyn_comp = ValueComputer.new(start_dyn,dyn_changes)
82
+ finish = 0
83
+ if dyn_changes.any?
84
+ finish, change = dyn_changes.max
85
+ if change.is_a? Change::Gradual
86
+ finish += change.duration
87
+ end
88
+ end
89
+ samples = dyn_comp.sample(0, finish, sample_rate)
90
+
91
+ prev = nil
92
+ samples.each_index do |i|
93
+ sample = samples[i]
94
+ unless sample == prev
95
+ offset = Rational(i,sample_rate)
96
+ volume = MidiUtil.dynamic_to_volume(sample)
97
+ dynamic_events.push [offset, MidiEvent::Expression.new(volume)]
98
+ end
99
+ prev = sample
100
+ end
101
+
102
+ return dynamic_events
103
+ end
104
+
105
+ def begin_track midi_sequence, part_name, channel, program
106
+ # Track to hold part notes
107
+ track = MIDI::Track.new(midi_sequence)
108
+
109
+ # Name the track and instrument
110
+ track.name = part_name
111
+ track.instrument = MIDI::GM_PATCH_NAMES[program]
112
+
113
+ # Add a volume controller event (optional).
114
+ track.events << MIDI::Controller.new(channel, MIDI::CC_VOLUME, 127)
115
+
116
+ # Change to particular instrument sound
117
+ track.events << MIDI::ProgramChange.new(channel, program)
118
+
119
+ return track
120
+ end
121
+ end
122
+
123
+ end
@@ -0,0 +1,45 @@
1
+ module Musicality
2
+
3
+ class ScoreSequencer
4
+ def initialize score
5
+ unless score.is_a?(Score::Timed)
6
+ raise ArgumentError, "The given score is not a Score::Timed. \
7
+ Convert it first using MeasureScoreConverter or UnmeasuredScoreConverter."
8
+ end
9
+
10
+ @parts = score.collated? ? score.parts : ScoreCollator.new(score).collate_parts
11
+
12
+ # part names should all be strings, because 1) a midi track name needs to
13
+ # be a string and 2) the instrument map used to map part names to MIDI
14
+ # program numbers will use part name strings as keys.
15
+ @parts = Hash[ @parts.map {|k,v| [k.to_s,v] } ]
16
+ end
17
+
18
+ USEC_PER_QUARTER_SEC = 250000
19
+
20
+ def make_midi_seq instr_map = {}
21
+ seq = MIDI::Sequence.new()
22
+
23
+ # first track for the sequence holds time sig and tempo events
24
+ track0 = MIDI::Track.new(seq)
25
+ seq.tracks << track0
26
+ track0.events << MIDI::Tempo.new(USEC_PER_QUARTER_SEC)
27
+ track0.events << MIDI::MetaEvent.new(MIDI::META_SEQ_NAME, 'Sequence Name')
28
+
29
+ channel = 0
30
+ @parts.each do |part_name,part|
31
+ program = 1
32
+ if instr_map.has_key?(part_name)
33
+ program = instr_map[part_name]
34
+ end
35
+
36
+ pseq = PartSequencer.new(part)
37
+ seq.tracks << pseq.make_midi_track(seq, part_name, channel, seq.ppqn, program)
38
+ channel += 1
39
+ end
40
+
41
+ return seq
42
+ end
43
+ end
44
+
45
+ end
@@ -0,0 +1,19 @@
1
+ module Musicality
2
+
3
+ require 'singleton'
4
+ class AccentedAttack
5
+ include Singleton
6
+
7
+ def accented?; return true; end
8
+ end
9
+
10
+ class UnaccentedAttack
11
+ include Singleton
12
+
13
+ def accented?; return false; end
14
+ end
15
+
16
+ ACCENTED = AccentedAttack.instance
17
+ UNACCENTED = UnaccentedAttack.instance
18
+
19
+ end
@@ -0,0 +1,111 @@
1
+ module Musicality
2
+
3
+ SlurredElement = Struct.new(:duration, :pitch, :accented) do
4
+ def slurred?; true; end
5
+ def articulation; Articulations::NORMAL; end
6
+ def accented?; accented; end
7
+ end
8
+
9
+ LegatoElement = Struct.new(:duration, :pitch, :accented) do
10
+ def slurred?; false; end
11
+ def articulation; Articulations::NORMAL; end
12
+ def accented?; accented; end
13
+ end
14
+
15
+ FinalElement = Struct.new(:duration, :pitch, :accented, :articulation) do
16
+ def slurred?; false; end
17
+ def accented?; accented; end
18
+ end
19
+
20
+ class NoteSequence
21
+ def self.adjust_duration duration, articulation
22
+ x = duration
23
+ y = Math.log2(x)
24
+
25
+ case articulation
26
+ when Articulations::TENUTO
27
+ x
28
+ when Articulations::PORTATO
29
+ x / (1 + 2**(y-1))
30
+ when Articulations::STACCATO
31
+ x / (1 + 2**(y))
32
+ when Articulations::STACCATISSIMO
33
+ x / (1 + 2**(y+1))
34
+ else
35
+ x - (1/16.0)*(1/(1+2**(-y)))
36
+ end
37
+ end
38
+
39
+ attr_reader :start, :stop, :pitches, :attacks
40
+ def initialize start, stop, pitches, attacks
41
+ if start >= stop
42
+ raise ArgumentError, "start #{start} is not less than stop #{stop}"
43
+ end
44
+
45
+ if pitches.empty?
46
+ raise ArgumentError, "no pitches given (at least one pitch is required at start offset)"
47
+ end
48
+
49
+ unless pitches.has_key?(start)
50
+ raise ArgumentError, "no start pitch given"
51
+ end
52
+
53
+ pitches.keys.each do |offset|
54
+ unless offset.between?(start,stop)
55
+ raise ArgumentError, "pitch offset #{offset} is not between start #{start} and stop #{stop}"
56
+ end
57
+ end
58
+
59
+ if attacks.empty?
60
+ raise ArgumentError, "no attacks given (at least one is required at start offset)"
61
+ end
62
+
63
+ unless attacks.has_key?(start)
64
+ raise ArgumentError, "no start attack given"
65
+ end
66
+
67
+ attacks.keys.each do |offset|
68
+ unless offset.between?(start,stop)
69
+ raise ArgumentError, "attack offset #{offset} is not between start #{start} and stop #{stop}"
70
+ end
71
+ end
72
+
73
+ @start, @stop = start, stop
74
+ @pitches, @attacks = pitches, attacks
75
+ end
76
+
77
+ def self.from_elements offset, elements
78
+ pitches = {}
79
+ attacks = {}
80
+ start = offset
81
+
82
+ if elements.empty?
83
+ raise ArgumentError, "no elements given"
84
+ end
85
+
86
+ last = elements.last
87
+ skip_attack = false
88
+ elements.each do |el|
89
+ if skip_attack
90
+ unless pitches.max[1] == el.pitch
91
+ pitches[offset] = el.pitch
92
+ end
93
+ else
94
+ pitches[offset] = el.pitch
95
+ attacks[offset] = el.accented ? ACCENTED : UNACCENTED
96
+ end
97
+ skip_attack = el.slurred?
98
+
99
+ unless el.equal?(last)
100
+ offset += el.duration
101
+ end
102
+ end
103
+ stop = offset + NoteSequence.adjust_duration(last.duration, last.articulation)
104
+
105
+ new(start, stop, pitches, attacks)
106
+ end
107
+
108
+ def duration; @stop - @start; end
109
+ end
110
+
111
+ end
@@ -0,0 +1,28 @@
1
+ module Musicality
2
+
3
+ class NoteLinker
4
+ def self.find_unlinked_pitches note
5
+ linked = Set.new(note.pitches) & note.links.keys
6
+ (Set.new(note.pitches) - linked).to_a
7
+ end
8
+
9
+ def self.find_untargeted_pitches note, next_note
10
+ linked = Set.new(note.pitches) & note.links.keys
11
+ targeted = Set.new(linked.map {|p| note.links[p].target_pitch })
12
+ (Set.new(next_note.pitches) - targeted).to_a
13
+ end
14
+
15
+ def self.figure_links note, next_note
16
+ unlinked = find_unlinked_pitches(note)
17
+ untargeted = find_untargeted_pitches(note, next_note)
18
+ Optimization.linking(unlinked, untargeted)
19
+ end
20
+
21
+ def self.fully_link note, next_note, link_class
22
+ figure_links(note,next_note).each do |pitch,tgt_pitch|
23
+ note.links[pitch] = link_class.new(tgt_pitch)
24
+ end
25
+ end
26
+ end
27
+
28
+ end
@@ -0,0 +1,31 @@
1
+ module Musicality
2
+
3
+ module Optimization
4
+ def self.linking unlinked, untargeted
5
+ n = [unlinked.size, untargeted.size].min
6
+
7
+ bestsol = nil
8
+ bestscore = Float::INFINITY
9
+ unlinked.combination(n).each do |comb|
10
+ untargeted.permutation(n).each do |perm|
11
+ score = 0
12
+ n.times do |i|
13
+ score += perm[i].diff(comb[i]).abs
14
+ end
15
+
16
+ if score < bestscore
17
+ bestsol = [ comb, perm ]
18
+ bestscore = score
19
+ end
20
+ end
21
+ end
22
+
23
+ solution = {}
24
+ n.times do |i|
25
+ solution[ bestsol[0][i] ] = bestsol[1][i]
26
+ end
27
+ return solution
28
+ end
29
+ end
30
+
31
+ end
@@ -0,0 +1,38 @@
1
+ # assumes that @checks is defined as an array of no-arg lambdas, each
2
+ # lambda raising an error (with useful msg) when check fails
3
+ module Validatable
4
+ attr_reader :errors
5
+
6
+ def check_methods; []; end
7
+ def validatables; []; end
8
+
9
+ def validate
10
+ @errors = []
11
+
12
+
13
+ check_methods.each do |check_method|
14
+ begin
15
+ send(check_method)
16
+ rescue StandardError => e
17
+ @errors.push e
18
+ end
19
+ end
20
+
21
+ validatables.each do |v|
22
+ if v.respond_to?(:validate)
23
+ @errors += v.validate
24
+ end
25
+ end
26
+
27
+ return @errors
28
+ end
29
+
30
+ def valid?
31
+ self.validate
32
+ @errors.empty?
33
+ end
34
+
35
+ def invalid?
36
+ !self.valid?
37
+ end
38
+ end
@@ -0,0 +1,3 @@
1
+ module Musicality
2
+ VERSION = "0.1.0"
3
+ end
data/lib/musicality.rb ADDED
@@ -0,0 +1,81 @@
1
+ # basic core classes
2
+ require 'musicality/version'
3
+ require 'musicality/validatable'
4
+ require 'musicality/errors'
5
+
6
+ #
7
+ # Notation
8
+ #
9
+
10
+ require 'musicality/notation/model/pitch'
11
+ require 'musicality/notation/model/pitches'
12
+ require 'musicality/notation/model/link'
13
+ require 'musicality/notation/model/articulations'
14
+ require 'musicality/notation/model/note'
15
+ require 'musicality/notation/model/dynamics'
16
+ require 'musicality/notation/model/change'
17
+ require 'musicality/notation/model/part'
18
+ require 'musicality/notation/model/program'
19
+ require 'musicality/notation/model/meter'
20
+ require 'musicality/notation/model/meters'
21
+ require 'musicality/notation/model/score'
22
+
23
+ require 'treetop'
24
+ require 'musicality/notation/parsing/numbers/nonnegative_integer_parsing'
25
+ require 'musicality/notation/parsing/numbers/positive_integer_parsing'
26
+ require 'musicality/notation/parsing/numbers/positive_float_parsing'
27
+ require 'musicality/notation/parsing/numbers/positive_rational_parsing'
28
+ require 'musicality/notation/parsing/numbers/nonnegative_float_parsing'
29
+ require 'musicality/notation/parsing/numbers/nonnegative_rational_parsing'
30
+ require 'musicality/notation/parsing/pitch_parsing'
31
+ require 'musicality/notation/parsing/pitch_node'
32
+ require 'musicality/notation/parsing/duration_parsing'
33
+ require 'musicality/notation/parsing/duration_nodes'
34
+ require 'musicality/notation/parsing/articulation_parsing'
35
+ require 'musicality/notation/parsing/link_parsing'
36
+ require 'musicality/notation/parsing/link_nodes'
37
+ require 'musicality/notation/parsing/note_parsing'
38
+ require 'musicality/notation/parsing/note_node'
39
+ require 'musicality/notation/parsing/meter_parsing'
40
+ require 'musicality/notation/parsing/segment_parsing'
41
+ require 'musicality/notation/parsing/parseable'
42
+ require 'musicality/notation/parsing/convenience_methods'
43
+
44
+ require 'musicality/notation/packing/change_packing'
45
+ require 'musicality/notation/packing/part_packing'
46
+ require 'musicality/notation/packing/program_packing'
47
+ require 'musicality/notation/packing/score_packing'
48
+
49
+ require 'musicality/notation/util/interpolation'
50
+ require 'musicality/notation/util/piecewise_function'
51
+ require 'musicality/notation/util/value_computer'
52
+
53
+ require 'musicality/notation/conversion/tempo_conversion'
54
+ require 'musicality/notation/conversion/change_conversion'
55
+ require 'musicality/notation/conversion/note_time_converter'
56
+ require 'musicality/notation/conversion/unmeasured_score_converter'
57
+ require 'musicality/notation/conversion/unmeasured_score_conversion'
58
+ require 'musicality/notation/conversion/measure_note_map'
59
+ require 'musicality/notation/conversion/measured_score_converter'
60
+ require 'musicality/notation/conversion/measured_score_conversion'
61
+
62
+ #
63
+ # Performance
64
+ #
65
+
66
+ require 'musicality/performance/model/note_attacks'
67
+ require 'musicality/performance/model/note_sequence'
68
+
69
+ require 'musicality/performance/util/optimization'
70
+ require 'musicality/performance/util/note_linker'
71
+
72
+ require 'musicality/performance/conversion/glissando_converter'
73
+ require 'musicality/performance/conversion/portamento_converter'
74
+ require 'musicality/performance/conversion/note_sequence_extractor'
75
+ require 'musicality/performance/conversion/score_collator'
76
+
77
+ require 'midilib'
78
+ require 'musicality/performance/midi/midi_util'
79
+ require 'musicality/performance/midi/midi_events'
80
+ require 'musicality/performance/midi/part_sequencer'
81
+ require 'musicality/performance/midi/score_sequencer'
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'musicality/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "musicality"
8
+ spec.version = Musicality::VERSION
9
+ spec.authors = ["James Tunnell"]
10
+ spec.email = ["jamestunnell@gmail.com"]
11
+ spec.summary = %q{Music notation, composition, and performance}
12
+ spec.description = %q{The library is based around an abstract representation for music notation. \
13
+ From here, functions are built up to make composing elaborate pieces in this notation representation more manageable. \
14
+ Finally, music performance is supported by providing translation to common formats, like MIDI. }
15
+ spec.homepage = "https://github.com/jamestunnell/musicality"
16
+ spec.license = "MIT"
17
+
18
+ spec.files = `git ls-files -z`.split("\x0")
19
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
20
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.7"
24
+ spec.add_development_dependency "rake", "~> 10.0"
25
+ spec.add_development_dependency "rspec", "~> 2.9"
26
+
27
+ spec.add_dependency "treetop", "~> 1.5"
28
+ spec.add_dependency 'midilib', '~> 2.0'
29
+ spec.add_dependency 'docopt', '~> 0.5'
30
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ describe Musicality do
4
+ it "should have a VERSION constant" do
5
+ subject.const_get('VERSION').should_not be_empty
6
+ end
7
+ end
@@ -0,0 +1,40 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
2
+
3
+ describe Change::Immediate do
4
+ describe '#offsets' do
5
+ it 'should return array with just the given base offset' do
6
+ c = Change::Immediate.new(12)
7
+ c.offsets(44).should eq([44])
8
+ end
9
+ end
10
+ end
11
+
12
+ describe Change::Gradual do
13
+ describe '#offsets' do
14
+ before :all do
15
+ @change = Change::Gradual.new(100,1.5,0.5,0.25)
16
+ @base = 25.5
17
+ @offsets = @change.offsets(@base)
18
+ end
19
+
20
+ it 'should return array with 4 elements' do
21
+ @offsets.size.should eq(4)
22
+ end
23
+
24
+ it 'should include the given base offset' do
25
+ @offsets.should include(@base)
26
+ end
27
+
28
+ it 'should include the base offset - elapsed' do
29
+ @offsets.should include(@base - @change.elapsed)
30
+ end
31
+
32
+ it 'should include the base offset + impending' do
33
+ @offsets.should include(@base + @change.impending)
34
+ end
35
+
36
+ it 'should include the base offset + impending + remaining' do
37
+ @offsets.should include(@base + @change.impending + @change.remaining)
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,73 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
2
+
3
+ describe 'Conversion.measure_note_map' do
4
+ before :all do
5
+ mdurs = Hash[ [[0, (3/4)], [1, (1/2)], [3, (3/4)]] ]
6
+ @moffs = [ 0, 1, 3, 4, 5, 6, 7, 8, 11, 14, 17, 20, (45/2)]
7
+ @mnoff_map = Conversion.measure_note_map(@moffs,mdurs)
8
+ end
9
+
10
+ it 'should return a Hash' do
11
+ @mnoff_map.should be_a Hash
12
+ end
13
+
14
+ it 'should have same size as array returned by #measure_offsets' do
15
+ @mnoff_map.size.should eq(@moffs.size)
16
+ end
17
+
18
+ it 'should have a key for each offset in the array returned by #measure_offsets' do
19
+ @mnoff_map.keys.sort.should eq(@moffs)
20
+ end
21
+
22
+ context 'single measure duration at 0' do
23
+ it 'should mutiply all measure offsets by start measure duration' do
24
+ [TWO_FOUR,SIX_EIGHT,FOUR_FOUR,THREE_FOUR].each do |meter|
25
+ mdur = meter.measure_duration
26
+ mdurs = { 0 => mdur }
27
+ tgt = @moffs.map {|moff| moff * mdur}
28
+ Conversion.measure_note_map(@moffs,mdurs).values.sort.should eq(tgt)
29
+ end
30
+ end
31
+ end
32
+
33
+ context '1 meter change' do
34
+ before :all do
35
+ @first_mc_off = 3
36
+ @start_meter = THREE_FOUR
37
+ @new_meter = TWO_FOUR
38
+ @score = Score::Measured.new(@start_meter, 120,
39
+ meter_changes: { @first_mc_off => Change::Immediate.new(@new_meter) },
40
+ tempo_changes: {
41
+ "1/2".to_r => Change::Gradual.new(100,1),
42
+ 2 => Change::Immediate.new(120),
43
+ 3 => Change::Immediate.new(100),
44
+ 3.1 => Change::Gradual.new(100,1),
45
+ 5 => Change::Immediate.new(120),
46
+ 6 => Change::Immediate.new(100),
47
+ }
48
+ )
49
+ @moffs = @score.measure_offsets
50
+ @mdurs = @score.measure_durations
51
+ @mnoff_map = Conversion.measure_note_map(@moffs,@mdurs)
52
+ end
53
+
54
+ it 'should mutiply all measure offsets that occur on or before 1st meter change offset by start measure duration' do
55
+ moffs = @moffs.select{ |x| x <= @first_mc_off }
56
+ tgt = moffs.map do |moff|
57
+ moff * @start_meter.measure_duration
58
+ end.sort
59
+ src = @mnoff_map.select {|k,v| k <= @first_mc_off }
60
+ src.values.sort.should eq(tgt)
61
+ end
62
+
63
+ it 'should, for any measure offsets occurring after 1st meter change offset, add 1st_meter_change_offset * 1st_measure_duration to \
64
+ new_measure_duration * (offset - 1st_meter_change_offset)' do
65
+ moffs = @moffs.select{ |x| x > @first_mc_off }
66
+ tgt = moffs.map do |moff|
67
+ @first_mc_off * @start_meter.measure_duration + (moff - @first_mc_off) * @new_meter.measure_duration
68
+ end.sort
69
+ src = @mnoff_map.select {|k,v| k > @first_mc_off }
70
+ src.values.sort.should eq(tgt)
71
+ end
72
+ end
73
+ end