musicality 0.3.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/ChangeLog.md +8 -1
  3. data/bin/midify +3 -4
  4. data/examples/composition/auto_counterpoint.rb +53 -0
  5. data/examples/composition/part_generator.rb +51 -0
  6. data/examples/composition/scale_exercise.rb +41 -0
  7. data/examples/{hip.rb → notation/hip.rb} +1 -1
  8. data/examples/{missed_connection.rb → notation/missed_connection.rb} +1 -1
  9. data/examples/{song1.rb → notation/song1.rb} +1 -1
  10. data/examples/{song2.rb → notation/song2.rb} +1 -1
  11. data/lib/musicality.rb +34 -4
  12. data/lib/musicality/composition/generation/counterpoint_generator.rb +153 -0
  13. data/lib/musicality/composition/generation/random_rhythm_generator.rb +39 -0
  14. data/lib/musicality/composition/model/pitch_class.rb +33 -0
  15. data/lib/musicality/composition/model/pitch_classes.rb +22 -0
  16. data/lib/musicality/composition/model/scale.rb +34 -0
  17. data/lib/musicality/composition/model/scale_class.rb +37 -0
  18. data/lib/musicality/composition/model/scale_classes.rb +91 -0
  19. data/lib/musicality/composition/note_generation.rb +31 -0
  20. data/lib/musicality/composition/transposition.rb +8 -0
  21. data/lib/musicality/composition/util/adding_sequence.rb +24 -0
  22. data/lib/musicality/composition/util/biinfinite_sequence.rb +130 -0
  23. data/lib/musicality/composition/util/compound_sequence.rb +44 -0
  24. data/lib/musicality/composition/util/probabilities.rb +20 -0
  25. data/lib/musicality/composition/util/random_sampler.rb +26 -0
  26. data/lib/musicality/composition/util/repeating_sequence.rb +24 -0
  27. data/lib/musicality/errors.rb +2 -0
  28. data/lib/musicality/notation/conversion/score_conversion.rb +1 -1
  29. data/lib/musicality/notation/conversion/score_converter.rb +3 -3
  30. data/lib/musicality/notation/model/link.rb +26 -24
  31. data/lib/musicality/notation/model/links.rb +11 -0
  32. data/lib/musicality/notation/model/note.rb +14 -15
  33. data/lib/musicality/notation/model/part.rb +3 -3
  34. data/lib/musicality/notation/model/pitch.rb +8 -0
  35. data/lib/musicality/notation/model/score.rb +70 -44
  36. data/lib/musicality/notation/model/symbols.rb +22 -0
  37. data/lib/musicality/notation/packing/score_packing.rb +2 -3
  38. data/lib/musicality/notation/parsing/articulation_parsing.rb +4 -4
  39. data/lib/musicality/notation/parsing/articulation_parsing.treetop +2 -2
  40. data/lib/musicality/notation/parsing/link_nodes.rb +2 -14
  41. data/lib/musicality/notation/parsing/link_parsing.rb +9 -107
  42. data/lib/musicality/notation/parsing/link_parsing.treetop +4 -12
  43. data/lib/musicality/notation/parsing/note_node.rb +23 -21
  44. data/lib/musicality/notation/parsing/note_parsing.rb +70 -70
  45. data/lib/musicality/notation/parsing/note_parsing.treetop +6 -3
  46. data/lib/musicality/notation/parsing/pitch_node.rb +4 -2
  47. data/lib/musicality/performance/conversion/score_collator.rb +3 -3
  48. data/lib/musicality/performance/midi/midi_util.rb +13 -6
  49. data/lib/musicality/performance/midi/score_sequencing.rb +17 -0
  50. data/lib/musicality/printing/lilypond/errors.rb +5 -0
  51. data/lib/musicality/printing/lilypond/meter_engraving.rb +11 -0
  52. data/lib/musicality/printing/lilypond/note_engraving.rb +53 -0
  53. data/lib/musicality/printing/lilypond/part_engraver.rb +12 -0
  54. data/lib/musicality/printing/lilypond/pitch_engraving.rb +30 -0
  55. data/lib/musicality/printing/lilypond/score_engraver.rb +78 -0
  56. data/lib/musicality/version.rb +1 -1
  57. data/spec/composition/generation/random_rhythm_generator_spec.rb +50 -0
  58. data/spec/composition/model/pitch_class_spec.rb +75 -0
  59. data/spec/composition/model/pitch_classes_spec.rb +24 -0
  60. data/spec/composition/model/scale_class_spec.rb +98 -0
  61. data/spec/composition/model/scale_spec.rb +110 -0
  62. data/spec/composition/note_generation_spec.rb +113 -0
  63. data/spec/composition/transposition_spec.rb +17 -0
  64. data/spec/composition/util/adding_sequence_spec.rb +176 -0
  65. data/spec/composition/util/compound_sequence_spec.rb +50 -0
  66. data/spec/composition/util/probabilities_spec.rb +39 -0
  67. data/spec/composition/util/random_sampler_spec.rb +47 -0
  68. data/spec/composition/util/repeating_sequence_spec.rb +151 -0
  69. data/spec/notation/conversion/score_conversion_spec.rb +3 -3
  70. data/spec/notation/conversion/score_converter_spec.rb +24 -24
  71. data/spec/notation/model/link_spec.rb +27 -25
  72. data/spec/notation/model/note_spec.rb +9 -6
  73. data/spec/notation/model/pitch_spec.rb +24 -1
  74. data/spec/notation/model/score_spec.rb +57 -16
  75. data/spec/notation/packing/score_packing_spec.rb +134 -206
  76. data/spec/notation/parsing/articulation_parsing_spec.rb +1 -8
  77. data/spec/notation/parsing/convenience_methods_spec.rb +1 -1
  78. data/spec/notation/parsing/link_nodes_spec.rb +3 -4
  79. data/spec/notation/parsing/link_parsing_spec.rb +10 -4
  80. data/spec/notation/parsing/note_node_spec.rb +8 -7
  81. data/spec/notation/parsing/note_parsing_spec.rb +9 -12
  82. data/spec/performance/conversion/score_collator_spec.rb +14 -14
  83. data/spec/performance/midi/midi_util_spec.rb +26 -0
  84. data/spec/performance/midi/score_sequencer_spec.rb +1 -1
  85. metadata +57 -12
  86. data/lib/musicality/notation/model/program.rb +0 -53
  87. data/lib/musicality/notation/packing/program_packing.rb +0 -16
  88. data/spec/notation/model/program_spec.rb +0 -50
  89. 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
- art:articulation?
13
- pitch_links:(first:pitch_link more:("," pl:pitch_link)*)?
14
- acc:accent?
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
- sem = pitch_letter.to_semitone
6
+ modval = 0
7
7
  unless mod.empty?
8
- sem += case mod.text_value
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.segments
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.segments)
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.segments)
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
- MIDI_NOTENUMS = Hash[
13
- (0..127).map do |note_num|
14
- [ p0.transpose(note_num), note_num ]
15
- end
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
- MIDI_NOTENUMS.fetch(pitch.round)
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,5 @@
1
+ module Musicality
2
+
3
+ UnsupportedDurationError = Class.new(RuntimeError)
4
+
5
+ end
@@ -0,0 +1,11 @@
1
+ module Musicality
2
+
3
+ class Meter
4
+ def to_lilypond
5
+ num = beats_per_measure * beat_duration.numerator
6
+ den = beat_duration.denominator
7
+ "#{num}/#{den}"
8
+ end
9
+ end
10
+
11
+ 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,12 @@
1
+ module Musicality
2
+
3
+ class PartEngraver
4
+ def initialize part
5
+ @part = part
6
+ end
7
+
8
+ def to_lilypad
9
+ end
10
+ end
11
+
12
+ 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
@@ -1,3 +1,3 @@
1
1
  module Musicality
2
- VERSION = "0.3.0"
2
+ VERSION = "0.5.0"
3
3
  end
@@ -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