musicality 0.3.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/ChangeLog.md +8 -1
- data/bin/midify +3 -4
- data/examples/composition/auto_counterpoint.rb +53 -0
- data/examples/composition/part_generator.rb +51 -0
- data/examples/composition/scale_exercise.rb +41 -0
- data/examples/{hip.rb → notation/hip.rb} +1 -1
- data/examples/{missed_connection.rb → notation/missed_connection.rb} +1 -1
- data/examples/{song1.rb → notation/song1.rb} +1 -1
- data/examples/{song2.rb → notation/song2.rb} +1 -1
- data/lib/musicality.rb +34 -4
- data/lib/musicality/composition/generation/counterpoint_generator.rb +153 -0
- data/lib/musicality/composition/generation/random_rhythm_generator.rb +39 -0
- data/lib/musicality/composition/model/pitch_class.rb +33 -0
- data/lib/musicality/composition/model/pitch_classes.rb +22 -0
- data/lib/musicality/composition/model/scale.rb +34 -0
- data/lib/musicality/composition/model/scale_class.rb +37 -0
- data/lib/musicality/composition/model/scale_classes.rb +91 -0
- data/lib/musicality/composition/note_generation.rb +31 -0
- data/lib/musicality/composition/transposition.rb +8 -0
- data/lib/musicality/composition/util/adding_sequence.rb +24 -0
- data/lib/musicality/composition/util/biinfinite_sequence.rb +130 -0
- data/lib/musicality/composition/util/compound_sequence.rb +44 -0
- data/lib/musicality/composition/util/probabilities.rb +20 -0
- data/lib/musicality/composition/util/random_sampler.rb +26 -0
- data/lib/musicality/composition/util/repeating_sequence.rb +24 -0
- data/lib/musicality/errors.rb +2 -0
- data/lib/musicality/notation/conversion/score_conversion.rb +1 -1
- data/lib/musicality/notation/conversion/score_converter.rb +3 -3
- data/lib/musicality/notation/model/link.rb +26 -24
- data/lib/musicality/notation/model/links.rb +11 -0
- data/lib/musicality/notation/model/note.rb +14 -15
- data/lib/musicality/notation/model/part.rb +3 -3
- data/lib/musicality/notation/model/pitch.rb +8 -0
- data/lib/musicality/notation/model/score.rb +70 -44
- data/lib/musicality/notation/model/symbols.rb +22 -0
- data/lib/musicality/notation/packing/score_packing.rb +2 -3
- data/lib/musicality/notation/parsing/articulation_parsing.rb +4 -4
- data/lib/musicality/notation/parsing/articulation_parsing.treetop +2 -2
- data/lib/musicality/notation/parsing/link_nodes.rb +2 -14
- data/lib/musicality/notation/parsing/link_parsing.rb +9 -107
- data/lib/musicality/notation/parsing/link_parsing.treetop +4 -12
- data/lib/musicality/notation/parsing/note_node.rb +23 -21
- data/lib/musicality/notation/parsing/note_parsing.rb +70 -70
- data/lib/musicality/notation/parsing/note_parsing.treetop +6 -3
- data/lib/musicality/notation/parsing/pitch_node.rb +4 -2
- data/lib/musicality/performance/conversion/score_collator.rb +3 -3
- data/lib/musicality/performance/midi/midi_util.rb +13 -6
- data/lib/musicality/performance/midi/score_sequencing.rb +17 -0
- data/lib/musicality/printing/lilypond/errors.rb +5 -0
- data/lib/musicality/printing/lilypond/meter_engraving.rb +11 -0
- data/lib/musicality/printing/lilypond/note_engraving.rb +53 -0
- data/lib/musicality/printing/lilypond/part_engraver.rb +12 -0
- data/lib/musicality/printing/lilypond/pitch_engraving.rb +30 -0
- data/lib/musicality/printing/lilypond/score_engraver.rb +78 -0
- data/lib/musicality/version.rb +1 -1
- data/spec/composition/generation/random_rhythm_generator_spec.rb +50 -0
- data/spec/composition/model/pitch_class_spec.rb +75 -0
- data/spec/composition/model/pitch_classes_spec.rb +24 -0
- data/spec/composition/model/scale_class_spec.rb +98 -0
- data/spec/composition/model/scale_spec.rb +110 -0
- data/spec/composition/note_generation_spec.rb +113 -0
- data/spec/composition/transposition_spec.rb +17 -0
- data/spec/composition/util/adding_sequence_spec.rb +176 -0
- data/spec/composition/util/compound_sequence_spec.rb +50 -0
- data/spec/composition/util/probabilities_spec.rb +39 -0
- data/spec/composition/util/random_sampler_spec.rb +47 -0
- data/spec/composition/util/repeating_sequence_spec.rb +151 -0
- data/spec/notation/conversion/score_conversion_spec.rb +3 -3
- data/spec/notation/conversion/score_converter_spec.rb +24 -24
- data/spec/notation/model/link_spec.rb +27 -25
- data/spec/notation/model/note_spec.rb +9 -6
- data/spec/notation/model/pitch_spec.rb +24 -1
- data/spec/notation/model/score_spec.rb +57 -16
- data/spec/notation/packing/score_packing_spec.rb +134 -206
- data/spec/notation/parsing/articulation_parsing_spec.rb +1 -8
- data/spec/notation/parsing/convenience_methods_spec.rb +1 -1
- data/spec/notation/parsing/link_nodes_spec.rb +3 -4
- data/spec/notation/parsing/link_parsing_spec.rb +10 -4
- data/spec/notation/parsing/note_node_spec.rb +8 -7
- data/spec/notation/parsing/note_parsing_spec.rb +9 -12
- data/spec/performance/conversion/score_collator_spec.rb +14 -14
- data/spec/performance/midi/midi_util_spec.rb +26 -0
- data/spec/performance/midi/score_sequencer_spec.rb +1 -1
- metadata +57 -12
- data/lib/musicality/notation/model/program.rb +0 -53
- data/lib/musicality/notation/packing/program_packing.rb +0 -16
- data/spec/notation/model/program_spec.rb +0 -50
- data/spec/notation/packing/program_packing_spec.rb +0 -33
@@ -9,9 +9,12 @@ grammar Note
|
|
9
9
|
|
10
10
|
rule note
|
11
11
|
duration
|
12
|
-
|
13
|
-
|
14
|
-
|
12
|
+
more:(
|
13
|
+
first_pl:pitch_link
|
14
|
+
more_pl:("," pl:pitch_link)*
|
15
|
+
art:articulation?
|
16
|
+
acc:accent?
|
17
|
+
)?
|
15
18
|
<NoteNode>
|
16
19
|
end
|
17
20
|
|
@@ -3,13 +3,15 @@ module Parsing
|
|
3
3
|
class PitchNode < Treetop::Runtime::SyntaxNode
|
4
4
|
def to_pitch
|
5
5
|
|
6
|
-
|
6
|
+
modval = 0
|
7
7
|
unless mod.empty?
|
8
|
-
|
8
|
+
modval = case mod.text_value
|
9
9
|
when "#" then 1
|
10
10
|
when "b" then -1
|
11
11
|
end
|
12
12
|
end
|
13
|
+
sem = (pitch_letter.to_semitone + modval) % Musicality::Pitch::SEMITONES_PER_OCTAVE
|
14
|
+
|
13
15
|
oct = octave.to_i
|
14
16
|
ncents = 0
|
15
17
|
unless cents.empty?
|
@@ -13,7 +13,7 @@ class ScoreCollator
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def collate_parts
|
16
|
-
segments = @score.program
|
16
|
+
segments = @score.program
|
17
17
|
|
18
18
|
Hash[
|
19
19
|
@score.parts.map do |name, part|
|
@@ -29,12 +29,12 @@ class ScoreCollator
|
|
29
29
|
|
30
30
|
def collate_tempo_changes
|
31
31
|
collate_changes(@score.start_tempo,
|
32
|
-
@score.tempo_changes, @score.program
|
32
|
+
@score.tempo_changes, @score.program)
|
33
33
|
end
|
34
34
|
|
35
35
|
def collate_meter_changes
|
36
36
|
collate_changes(@score.start_meter,
|
37
|
-
@score.meter_changes, @score.program
|
37
|
+
@score.meter_changes, @score.program)
|
38
38
|
end
|
39
39
|
|
40
40
|
private
|
@@ -9,14 +9,21 @@ class MidiUtil
|
|
9
9
|
end
|
10
10
|
|
11
11
|
p0 = Pitch.new(octave:-1,semitone:0)
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
12
|
+
PITCH_TO_NOTENUM = {}
|
13
|
+
NOTENUM_TO_PITCH = {}
|
14
|
+
|
15
|
+
(0..127).each do |note_num|
|
16
|
+
pitch = p0.transpose(note_num)
|
17
|
+
PITCH_TO_NOTENUM[pitch] = note_num
|
18
|
+
NOTENUM_TO_PITCH[note_num] = pitch
|
19
|
+
end
|
17
20
|
|
18
21
|
def self.pitch_to_notenum pitch
|
19
|
-
|
22
|
+
PITCH_TO_NOTENUM.fetch(pitch.round)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.notenum_to_pitch notenum
|
26
|
+
NOTENUM_TO_PITCH.fetch(notenum)
|
20
27
|
end
|
21
28
|
|
22
29
|
def self.dynamic_to_volume dynamic
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Musicality
|
2
|
+
|
3
|
+
class Score
|
4
|
+
class Timed < Score
|
5
|
+
def to_midi_seq instr_map = {}
|
6
|
+
ScoreSequencer.new(self).make_midi_seq(instr_map)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class TempoBased < Score
|
11
|
+
def to_midi_seq tempo_sample_rate, instr_map = {}
|
12
|
+
to_timed(tempo_sample_rate).to_midi(instr_map)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Musicality
|
2
|
+
|
3
|
+
class Note
|
4
|
+
def to_lilypond sharpit = false
|
5
|
+
dur_strs = []
|
6
|
+
d = duration
|
7
|
+
|
8
|
+
if d > 1
|
9
|
+
dur_strs += ["1"]*d.to_i
|
10
|
+
d -= d.to_i
|
11
|
+
end
|
12
|
+
|
13
|
+
if d > 0
|
14
|
+
n = Math.log2(d)
|
15
|
+
if n < -7
|
16
|
+
raise UnsupportedDurationError, "Note duration #{d} is too short for Lilypond"
|
17
|
+
end
|
18
|
+
|
19
|
+
if n.to_i == n # duration is exact power of two
|
20
|
+
dur_strs.push (2**(-n)).to_i.to_s
|
21
|
+
else # duration may be dotted
|
22
|
+
undotted_duration = d/1.5
|
23
|
+
n = Math.log2(undotted_duration)
|
24
|
+
|
25
|
+
if n.to_i == n # duration (undotted) is exact power of two
|
26
|
+
if n < -7
|
27
|
+
raise UnsupportedDurationError, "Undotted note duration #{undotted_duration} is too short for Lilypond"
|
28
|
+
end
|
29
|
+
|
30
|
+
dur_strs.push (2**(-n)).to_i.to_s + "."
|
31
|
+
else
|
32
|
+
raise UnsupportedDurationError, "Leftover note duration #{d}is not power-of-two, and is not dotted power-of-two"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
if pitches.any?
|
38
|
+
if pitches.size == 1
|
39
|
+
p_str = pitches.first.to_lilypond
|
40
|
+
else
|
41
|
+
p_str = "<" + pitches.map {|p| p.to_lilypond}.join(" ") + ">"
|
42
|
+
end
|
43
|
+
join_str = "~ "
|
44
|
+
else
|
45
|
+
p_str = "r"
|
46
|
+
join_str = " "
|
47
|
+
end
|
48
|
+
|
49
|
+
return dur_strs.map {|dur_str| p_str + dur_str }.join(join_str)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Musicality
|
2
|
+
|
3
|
+
class Pitch
|
4
|
+
def to_lilypond sharpit = false
|
5
|
+
output = case semitone
|
6
|
+
when 0 then "c"
|
7
|
+
when 1 then sharpit ? "cis" : "des"
|
8
|
+
when 2 then "d"
|
9
|
+
when 3 then sharpit ? "dis" : "ees"
|
10
|
+
when 4 then "e"
|
11
|
+
when 5 then "f"
|
12
|
+
when 6 then sharpit ? "fis" : "ges"
|
13
|
+
when 7 then "g"
|
14
|
+
when 8 then sharpit ? "gis" : "aes"
|
15
|
+
when 9 then "a"
|
16
|
+
when 10 then sharpit ? "ais" : "bes"
|
17
|
+
when 11 then "b"
|
18
|
+
end
|
19
|
+
|
20
|
+
if octave > 3
|
21
|
+
output += "'"*(octave - 3)
|
22
|
+
elsif octave < 3
|
23
|
+
output += ","*(3 - octave)
|
24
|
+
end
|
25
|
+
|
26
|
+
return output
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Musicality
|
2
|
+
|
3
|
+
class ScoreEngraver
|
4
|
+
LILYPOND_VERSION = "2.18.2"
|
5
|
+
MAX_LINE_LEN = 76
|
6
|
+
|
7
|
+
def initialize score
|
8
|
+
case score
|
9
|
+
when Score::Measured
|
10
|
+
@start_meter = score.start_meter
|
11
|
+
when Score::Unmeasured
|
12
|
+
@start_meter = Meters::FOUR_FOUR
|
13
|
+
else
|
14
|
+
raise TypeError, "Only tempo-based score support Lilypond conversion"
|
15
|
+
end
|
16
|
+
|
17
|
+
@parts = score.collated? ? score.parts : ScoreCollator.new(score).collate_parts
|
18
|
+
end
|
19
|
+
|
20
|
+
def make_lilypond part_names = nil
|
21
|
+
part_names ||= @parts.keys
|
22
|
+
output = "\\version \"#{LILYPOND_VERSION}\"\n{\n <<\n"
|
23
|
+
master = true
|
24
|
+
part_names.each do |part_name|
|
25
|
+
part = @parts[part_name]
|
26
|
+
|
27
|
+
clef = ScoreEngraver.best_clef(part.notes)
|
28
|
+
output += " \\new Staff {\n"
|
29
|
+
output += " \\clef #{clef}\n"
|
30
|
+
if(master)
|
31
|
+
output += " \\time #{@start_meter.to_lilypond}\n"
|
32
|
+
end
|
33
|
+
|
34
|
+
line = ""
|
35
|
+
part.notes.each_index do |i|
|
36
|
+
note = part.notes[i]
|
37
|
+
begin
|
38
|
+
str = note.to_lilypond
|
39
|
+
rescue UnsupportedDurationError => e
|
40
|
+
binding.pry
|
41
|
+
end
|
42
|
+
|
43
|
+
if (line.size + str.size) > MAX_LINE_LEN
|
44
|
+
output += " " + line
|
45
|
+
line = ""
|
46
|
+
end
|
47
|
+
line += str
|
48
|
+
end
|
49
|
+
output += " " + line
|
50
|
+
|
51
|
+
output += " }\n"
|
52
|
+
|
53
|
+
master = false if master
|
54
|
+
end
|
55
|
+
output += " >>\n}\n"
|
56
|
+
return output
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.best_clef notes
|
60
|
+
ranges = { "treble" => Pitches::C4..Pitches::A5,
|
61
|
+
"bass" => Pitches::E2..Pitches::C4,
|
62
|
+
"tenor" => Pitches::B2..Pitches::G4 }
|
63
|
+
range_scores = { "treble" => 0, "bass" => 0, "tenor" => 0 }
|
64
|
+
notes.each do |n|
|
65
|
+
n.pitches.each do |p|
|
66
|
+
ranges.each do |name,range|
|
67
|
+
if p >= range.min && p <= range.max
|
68
|
+
range_scores[name] += n.duration
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
range_score = range_scores.max_by {|range,score| score}
|
74
|
+
return range_score[0]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
data/lib/musicality/version.rb
CHANGED
@@ -0,0 +1,50 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
|
2
|
+
|
3
|
+
describe RandomRhythmGenerator do
|
4
|
+
describe '#initialize' do
|
5
|
+
context 'given probabilities that do not add up to 1' do
|
6
|
+
it 'should raise ArgumentError' do
|
7
|
+
[{ 1 => 0.4, 2 => 0.59 },
|
8
|
+
{ 1/2.to_r => 0.5, 1/4.to_r => 0.5001 }].each do |durs_w_probs|
|
9
|
+
expect do
|
10
|
+
RandomRhythmGenerator.new(durs_w_probs)
|
11
|
+
end.to raise_error(ArgumentError)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
before :all do
|
18
|
+
@rrgs = [
|
19
|
+
{ 1/8.to_r => 0.25, 1/4.to_r => 0.5, 1/2.to_r => 0.25 },
|
20
|
+
{ 1/6.to_r => 0.25, 1/4.to_r => 0.25, 1/3.to_r => 0.25, 1/12.to_r => 0.25 }
|
21
|
+
].map {|durs_w_probs| RandomRhythmGenerator.new(durs_w_probs) }
|
22
|
+
end
|
23
|
+
|
24
|
+
describe '#random_rhythm' do
|
25
|
+
it 'should return durations that add to given total dur' do
|
26
|
+
@rrgs.each do |rrg|
|
27
|
+
[3,1,1/2.to_r,5/8.to_r,15/16.to_r].each do |total_dur|
|
28
|
+
20.times do
|
29
|
+
rhythm = rrg.random_rhythm(total_dur)
|
30
|
+
rhythm.inject(0,:+).should eq(total_dur)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe '#random_dur' do
|
38
|
+
it 'should return a random duration, according to the probabilities given at initialization' do
|
39
|
+
@rrgs.each do |rrg|
|
40
|
+
counts = Hash[ rrg.durations.map {|dur| [dur,0] } ]
|
41
|
+
1000.times { counts[rrg.random_dur] += 1 }
|
42
|
+
rrg.durations.each_with_index do |dur,i|
|
43
|
+
count = counts[dur]
|
44
|
+
tgt_prob = rrg.probabilities[i]
|
45
|
+
(count / 1000.to_f).should be_within(0.05).of(tgt_prob)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
|
2
|
+
|
3
|
+
include PitchClasses
|
4
|
+
|
5
|
+
describe PitchClass do
|
6
|
+
it 'should define the MOD constant' do
|
7
|
+
PitchClass.constants.should include(:MOD)
|
8
|
+
end
|
9
|
+
|
10
|
+
describe '.from_i' do
|
11
|
+
it 'should return the given integer % PitchClass::MOD' do
|
12
|
+
PitchClass.from_i(-1).should eq(11)
|
13
|
+
PitchClass.from_i(12).should eq(0)
|
14
|
+
PitchClass.from_i(2).should eq(2)
|
15
|
+
PitchClass.from_i(16).should eq(4)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'should add the #to_pc method to the Fixnum class' do
|
20
|
+
5.methods.should include(:to_pc)
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'should add the #to_pc method to the Pitch class' do
|
24
|
+
Pitch.new.methods.should include(:to_pc)
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'should add the #to_pcs method to Enumerable classes, like Array' do
|
28
|
+
[1,2,3].methods.should include(:to_pcs)
|
29
|
+
end
|
30
|
+
|
31
|
+
describe 'Pitch#to_pc' do
|
32
|
+
it 'should send semitone through PitchClass.from_i' do
|
33
|
+
[ C4, D3, E5, G5,
|
34
|
+
Pitch.new(semitone: 4),
|
35
|
+
Pitch.new(semitone: 13),
|
36
|
+
].each do |pitch|
|
37
|
+
pitch.to_pc.should eq(PitchClass.from_i(pitch.semitone))
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe 'Fixnum#to_pc' do
|
43
|
+
it 'should pass self to PitchClass.from_i' do
|
44
|
+
[-1,12,2,16].each do |i|
|
45
|
+
i.to_pc.should eq(PitchClass.from_i(i))
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe '.invert' do
|
51
|
+
before :all do
|
52
|
+
@cases = {
|
53
|
+
C => C,
|
54
|
+
Db => B,
|
55
|
+
D => Bb,
|
56
|
+
Eb => A,
|
57
|
+
E => Ab,
|
58
|
+
F => G,
|
59
|
+
Gb => Gb
|
60
|
+
}
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'should produce a pitch class' do
|
64
|
+
@cases.each do |input_pc, output_pc|
|
65
|
+
PitchClass.invert(input_pc).should eq(output_pc)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'should produce a pitch class that when inverted again produces the original pitch class' do
|
70
|
+
@cases.each do |input_pc, output_pc|
|
71
|
+
PitchClass.invert(output_pc).should eq(input_pc)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
|
2
|
+
|
3
|
+
pc_syms = [:C, :Db, :D, :Eb, :E, :F, :Gb, :G, :Ab, :A, :Bb, :B]
|
4
|
+
|
5
|
+
describe PitchClasses do
|
6
|
+
it 'should include pitch-class constants for C, Db, D, ...' do
|
7
|
+
pc_syms.each do |sym|
|
8
|
+
PitchClasses.constants.should include(sym)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
|
14
|
+
describe 'PITCH_CLASSES' do
|
15
|
+
it 'should be in the Musicality module namespace' do
|
16
|
+
Musicality.constants.should include(:PITCH_CLASSES)
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'should have each constant value in PitchClasses' do
|
20
|
+
PitchClasses.constants.each do |sym|
|
21
|
+
PITCH_CLASSES.should include(PitchClasses.const_get(sym))
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|