musicality 0.3.0 → 0.5.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 +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
|