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.
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