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.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +47 -0
- data/Rakefile +65 -0
- data/bin/midify +78 -0
- data/examples/hip.rb +32 -0
- data/examples/missed_connection.rb +26 -0
- data/examples/song1.rb +33 -0
- data/examples/song2.rb +32 -0
- data/lib/musicality/errors.rb +9 -0
- data/lib/musicality/notation/conversion/change_conversion.rb +19 -0
- data/lib/musicality/notation/conversion/measure_note_map.rb +40 -0
- data/lib/musicality/notation/conversion/measured_score_conversion.rb +70 -0
- data/lib/musicality/notation/conversion/measured_score_converter.rb +95 -0
- data/lib/musicality/notation/conversion/note_time_converter.rb +68 -0
- data/lib/musicality/notation/conversion/tempo_conversion.rb +25 -0
- data/lib/musicality/notation/conversion/unmeasured_score_conversion.rb +47 -0
- data/lib/musicality/notation/conversion/unmeasured_score_converter.rb +64 -0
- data/lib/musicality/notation/model/articulations.rb +13 -0
- data/lib/musicality/notation/model/change.rb +62 -0
- data/lib/musicality/notation/model/dynamics.rb +12 -0
- data/lib/musicality/notation/model/link.rb +73 -0
- data/lib/musicality/notation/model/meter.rb +54 -0
- data/lib/musicality/notation/model/meters.rb +9 -0
- data/lib/musicality/notation/model/note.rb +120 -0
- data/lib/musicality/notation/model/part.rb +54 -0
- data/lib/musicality/notation/model/pitch.rb +163 -0
- data/lib/musicality/notation/model/pitches.rb +21 -0
- data/lib/musicality/notation/model/program.rb +53 -0
- data/lib/musicality/notation/model/score.rb +132 -0
- data/lib/musicality/notation/packing/change_packing.rb +46 -0
- data/lib/musicality/notation/packing/part_packing.rb +31 -0
- data/lib/musicality/notation/packing/program_packing.rb +16 -0
- data/lib/musicality/notation/packing/score_packing.rb +108 -0
- data/lib/musicality/notation/parsing/articulation_parsing.rb +264 -0
- data/lib/musicality/notation/parsing/articulation_parsing.treetop +59 -0
- data/lib/musicality/notation/parsing/convenience_methods.rb +74 -0
- data/lib/musicality/notation/parsing/duration_nodes.rb +21 -0
- data/lib/musicality/notation/parsing/duration_parsing.rb +205 -0
- data/lib/musicality/notation/parsing/duration_parsing.treetop +25 -0
- data/lib/musicality/notation/parsing/link_nodes.rb +35 -0
- data/lib/musicality/notation/parsing/link_parsing.rb +270 -0
- data/lib/musicality/notation/parsing/link_parsing.treetop +33 -0
- data/lib/musicality/notation/parsing/meter_parsing.rb +190 -0
- data/lib/musicality/notation/parsing/meter_parsing.treetop +29 -0
- data/lib/musicality/notation/parsing/note_node.rb +40 -0
- data/lib/musicality/notation/parsing/note_parsing.rb +229 -0
- data/lib/musicality/notation/parsing/note_parsing.treetop +28 -0
- data/lib/musicality/notation/parsing/numbers/nonnegative_float_parsing.rb +289 -0
- data/lib/musicality/notation/parsing/numbers/nonnegative_float_parsing.treetop +29 -0
- data/lib/musicality/notation/parsing/numbers/nonnegative_integer_parsing.rb +64 -0
- data/lib/musicality/notation/parsing/numbers/nonnegative_integer_parsing.treetop +17 -0
- data/lib/musicality/notation/parsing/numbers/nonnegative_rational_parsing.rb +86 -0
- data/lib/musicality/notation/parsing/numbers/nonnegative_rational_parsing.treetop +20 -0
- data/lib/musicality/notation/parsing/numbers/positive_float_parsing.rb +503 -0
- data/lib/musicality/notation/parsing/numbers/positive_float_parsing.treetop +33 -0
- data/lib/musicality/notation/parsing/numbers/positive_integer_parsing.rb +95 -0
- data/lib/musicality/notation/parsing/numbers/positive_integer_parsing.treetop +19 -0
- data/lib/musicality/notation/parsing/numbers/positive_rational_parsing.rb +84 -0
- data/lib/musicality/notation/parsing/numbers/positive_rational_parsing.treetop +19 -0
- data/lib/musicality/notation/parsing/parseable.rb +30 -0
- data/lib/musicality/notation/parsing/pitch_node.rb +23 -0
- data/lib/musicality/notation/parsing/pitch_parsing.rb +448 -0
- data/lib/musicality/notation/parsing/pitch_parsing.treetop +52 -0
- data/lib/musicality/notation/parsing/segment_parsing.rb +141 -0
- data/lib/musicality/notation/parsing/segment_parsing.treetop +23 -0
- data/lib/musicality/notation/util/interpolation.rb +16 -0
- data/lib/musicality/notation/util/piecewise_function.rb +122 -0
- data/lib/musicality/notation/util/value_computer.rb +170 -0
- data/lib/musicality/performance/conversion/glissando_converter.rb +34 -0
- data/lib/musicality/performance/conversion/note_sequence_extractor.rb +98 -0
- data/lib/musicality/performance/conversion/portamento_converter.rb +24 -0
- data/lib/musicality/performance/conversion/score_collator.rb +126 -0
- data/lib/musicality/performance/midi/midi_events.rb +34 -0
- data/lib/musicality/performance/midi/midi_util.rb +31 -0
- data/lib/musicality/performance/midi/part_sequencer.rb +123 -0
- data/lib/musicality/performance/midi/score_sequencer.rb +45 -0
- data/lib/musicality/performance/model/note_attacks.rb +19 -0
- data/lib/musicality/performance/model/note_sequence.rb +111 -0
- data/lib/musicality/performance/util/note_linker.rb +28 -0
- data/lib/musicality/performance/util/optimization.rb +31 -0
- data/lib/musicality/validatable.rb +38 -0
- data/lib/musicality/version.rb +3 -0
- data/lib/musicality.rb +81 -0
- data/musicality.gemspec +30 -0
- data/spec/musicality_spec.rb +7 -0
- data/spec/notation/conversion/change_conversion_spec.rb +40 -0
- data/spec/notation/conversion/measure_note_map_spec.rb +73 -0
- data/spec/notation/conversion/measured_score_conversion_spec.rb +141 -0
- data/spec/notation/conversion/measured_score_converter_spec.rb +329 -0
- data/spec/notation/conversion/note_time_converter_spec.rb +81 -0
- data/spec/notation/conversion/tempo_conversion_spec.rb +40 -0
- data/spec/notation/conversion/unmeasured_score_conversion_spec.rb +71 -0
- data/spec/notation/conversion/unmeasured_score_converter_spec.rb +116 -0
- data/spec/notation/model/change_spec.rb +90 -0
- data/spec/notation/model/link_spec.rb +83 -0
- data/spec/notation/model/meter_spec.rb +97 -0
- data/spec/notation/model/note_spec.rb +183 -0
- data/spec/notation/model/part_spec.rb +69 -0
- data/spec/notation/model/pitch_spec.rb +180 -0
- data/spec/notation/model/program_spec.rb +50 -0
- data/spec/notation/model/score_spec.rb +211 -0
- data/spec/notation/packing/change_packing_spec.rb +153 -0
- data/spec/notation/packing/part_packing_spec.rb +66 -0
- data/spec/notation/packing/program_packing_spec.rb +33 -0
- data/spec/notation/packing/score_packing_spec.rb +301 -0
- data/spec/notation/parsing/articulation_parsing_spec.rb +23 -0
- data/spec/notation/parsing/convenience_methods_spec.rb +99 -0
- data/spec/notation/parsing/duration_nodes_spec.rb +83 -0
- data/spec/notation/parsing/duration_parsing_spec.rb +70 -0
- data/spec/notation/parsing/link_nodes_spec.rb +30 -0
- data/spec/notation/parsing/link_parsing_spec.rb +13 -0
- data/spec/notation/parsing/meter_parsing_spec.rb +23 -0
- data/spec/notation/parsing/note_node_spec.rb +87 -0
- data/spec/notation/parsing/note_parsing_spec.rb +46 -0
- data/spec/notation/parsing/numbers/nonnegative_float_spec.rb +28 -0
- data/spec/notation/parsing/numbers/nonnegative_integer_spec.rb +11 -0
- data/spec/notation/parsing/numbers/nonnegative_rational_spec.rb +11 -0
- data/spec/notation/parsing/numbers/positive_float_spec.rb +28 -0
- data/spec/notation/parsing/numbers/positive_integer_spec.rb +28 -0
- data/spec/notation/parsing/numbers/positive_rational_spec.rb +28 -0
- data/spec/notation/parsing/pitch_node_spec.rb +38 -0
- data/spec/notation/parsing/pitch_parsing_spec.rb +14 -0
- data/spec/notation/parsing/segment_parsing_spec.rb +27 -0
- data/spec/notation/util/value_computer_spec.rb +146 -0
- data/spec/performance/conversion/glissando_converter_spec.rb +93 -0
- data/spec/performance/conversion/note_sequence_extractor_spec.rb +230 -0
- data/spec/performance/conversion/portamento_converter_spec.rb +91 -0
- data/spec/performance/conversion/score_collator_spec.rb +183 -0
- data/spec/performance/midi/midi_util_spec.rb +110 -0
- data/spec/performance/midi/part_sequencer_spec.rb +40 -0
- data/spec/performance/midi/score_sequencer_spec.rb +50 -0
- data/spec/performance/model/note_sequence_spec.rb +147 -0
- data/spec/performance/util/note_linker_spec.rb +68 -0
- data/spec/performance/util/optimization_spec.rb +73 -0
- data/spec/spec_helper.rb +43 -0
- 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
|
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'
|
data/musicality.gemspec
ADDED
@@ -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,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
|