musicality 0.5.1 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2b88f653838ad19299cc067b8b849a521cf7ca95
4
- data.tar.gz: 1be51ce70f8a1c873ef95cd7887d63db02f50ccf
3
+ metadata.gz: a6974b6669fd75f17166b07bf7a92e5e76298143
4
+ data.tar.gz: e8d4366d647aa439d3d63a93103578b355461292
5
5
  SHA512:
6
- metadata.gz: f4c6cc3c5383cf641734519e7bdb32969426252914d74f80649bf2d01fdc697dda211d73b31040550918688eb998a033c634b5b5413737a653780948fc1f396e
7
- data.tar.gz: 7b5be121f5efa1881a309dbcda6209718428ee2ab8ebf73593d3345724b18b9d1983e3c0af40161e752f8aa988343129c49d24a414880b661ba4c076ec5ca702
6
+ metadata.gz: 231e327998bfbb3320ced367f4f71694e15b48f10380c42e3788868678c1c6c69f63001b3e43b3231ae48ef566db62ab52580c45e9b67eebccfd6d1951f72d5b
7
+ data.tar.gz: 673824837e850617754f9ea648e135516c27300b68d8d57568cf53ab5cee539d883f43d354e24eb3c4eccacd426fac9a64559f72d1b81e236353f64d136fca97
data/ChangeLog.md CHANGED
@@ -1,3 +1,8 @@
1
+ ### 0.6.0 / 2015-04-28
2
+ * Add Score DSL
3
+ * Add composition convenience methods `e`, `q`, and `dq` for generating eighth, quarter, and dotted quarter notes
4
+ * Add part selection and titling to Lilypond score engraver
5
+
1
6
  ### 0.5.0 / 2015-04-27
2
7
  * Alter note syntax
3
8
  * shift articulation placement to after pitches
@@ -0,0 +1,30 @@
1
+ module Musicality
2
+
3
+ class ScoreDSL
4
+ def self.load fname
5
+ include Musicality
6
+ include Pitches
7
+ include Articulations
8
+ include Meters
9
+ include Dynamics
10
+
11
+ dsl = ScoreDSL.new
12
+ dsl.instance_eval(File.read(fname), fname)
13
+ dsl
14
+ end
15
+
16
+ def initialize
17
+ @score = nil
18
+ end
19
+
20
+ def score start_meter, start_tempo, &block
21
+ @score = Score::Measured.new(start_meter,start_tempo)
22
+ @score.instance_eval(&block)
23
+ end
24
+
25
+ def score_yaml
26
+ @score.pack.to_yaml
27
+ end
28
+ end
29
+
30
+ end
@@ -0,0 +1,77 @@
1
+ module Musicality
2
+
3
+ class Score
4
+ def section title, &block
5
+ a = duration
6
+ self.instance_eval(&block)
7
+ b = duration
8
+ @sections[title] = a...b
9
+ end
10
+
11
+ def repeat arg
12
+ case arg
13
+ when Range
14
+ program.push arg
15
+ when String,Symbol
16
+ program.push @sections.fetch(arg)
17
+ else
18
+ raise ArgumentError, "Arg is not a Range, String, or Symbol"
19
+ end
20
+ end
21
+
22
+ DEFAULT_START_DYNAMIC = Dynamics::MF
23
+ def notes part_notes
24
+ raise ArgumentError, "No part notes given" if part_notes.empty?
25
+
26
+ durs = part_notes.values.map {|notes| notes.map {|n| n.duration }.inject(0,:+) }
27
+ durs_uniq = durs.uniq
28
+ raise DurationMismatchError, "New part note durations do not all match" if durs_uniq.size > 1
29
+ dur = durs_uniq.first
30
+ raise NonPositiveError, "Part note durations are not positive" if dur <= 0
31
+
32
+ a = self.duration
33
+ starting_part_dur = self.max_part_duration
34
+ part_notes.each do |part,notes|
35
+ unless parts.has_key? part
36
+ parts[part] = Part.new DEFAULT_START_DYNAMIC
37
+ if starting_part_dur > 0
38
+ parts[part].notes.push Note.new(starting_part_dur)
39
+ end
40
+ end
41
+ parts[part].notes += notes
42
+ end
43
+ (parts.keys - part_notes.keys).each do |part|
44
+ parts[part].notes.push Note.new(dur)
45
+ end
46
+
47
+ b = self.duration
48
+ program.push a...b
49
+ a...b
50
+ end
51
+
52
+ def dynamic_change new_dynamic, transition_dur: 0, offset: 0
53
+ if transition_dur == 0
54
+ tempo_changes[self.duration + offset] = Change::Immediate.new(new_tempo)
55
+ else
56
+ tempo_changes[self.duration + offset] = Change::Gradual.linear(new_tempo, transition_dur)
57
+ end
58
+ end
59
+
60
+ class TempoBased < Score
61
+ def tempo_change new_tempo, transition_dur: 0, offset: 0
62
+ if transition_dur == 0
63
+ tempo_changes[self.duration + offset] = Change::Immediate.new(new_tempo)
64
+ else
65
+ tempo_changes[self.duration + offset] = Change::Gradual.linear(new_tempo, transition_dur)
66
+ end
67
+ end
68
+ end
69
+
70
+ class Measured < Score::TempoBased
71
+ def meter_change new_meter, offset: 0
72
+ meter_changes[self.duration + offset] = Change::Immediate.new(new_meter)
73
+ end
74
+ end
75
+ end
76
+
77
+ end
@@ -28,4 +28,19 @@ def make_notes rhythm, pitch_groups
28
28
  end
29
29
  module_function :make_notes
30
30
 
31
+ def e(pitch_groups)
32
+ pitch_groups.map {|pg| Note.eighth(pg) }
33
+ end
34
+ module_function :e
35
+
36
+ def q(pitch_groups)
37
+ pitch_groups.map {|pg| Note.quarter(pg) }
38
+ end
39
+ module_function :q
40
+
41
+ def dq(pitch_groups)
42
+ pitch_groups.map {|pg| Note.dotted_quarter(pg) }
43
+ end
44
+ module_function :dq
45
+
31
46
  end
@@ -9,4 +9,6 @@ module Musicality
9
9
  class NotValidError < StandardError; end
10
10
  class DomainError < StandardError; end
11
11
  class EmptyError < StandardError; end
12
+ class DurationMismatchError < StandardError; end
13
+
12
14
  end
@@ -2,11 +2,14 @@ module Musicality
2
2
 
3
3
  class Score
4
4
  include Validatable
5
- attr_accessor :parts, :program
5
+ attr_accessor :parts, :program, :title, :composer
6
6
 
7
- def initialize parts: {}, program: []
7
+ def initialize parts: {}, program: [], title: nil, composer: nil, sections: {}
8
8
  @parts = parts
9
9
  @program = program
10
+ @title = title
11
+ @composer = composer
12
+ @sections = sections
10
13
  yield(self) if block_given?
11
14
  end
12
15
 
@@ -21,7 +24,7 @@ class Score
21
24
  return @parts == other.parts && @program == other.program
22
25
  end
23
26
 
24
- def duration
27
+ def max_part_duration
25
28
  @parts.map {|name,part| part.duration }.max || 0.to_r
26
29
  end
27
30
 
@@ -31,17 +34,19 @@ class Score
31
34
 
32
35
  class Timed < Score
33
36
  def seconds_long
34
- self.duration
37
+ max_part_duration
35
38
  end
39
+ alias duration seconds_long
36
40
  end
37
41
 
38
42
  class TempoBased < Score
39
43
  attr_accessor :start_tempo, :tempo_changes
40
44
 
41
- def initialize start_tempo, tempo_changes: {}, parts: {}, program: []
45
+ # See Score#initialize for remaining kwargs
46
+ def initialize start_tempo, tempo_changes: {}, **kwargs
42
47
  @start_tempo = start_tempo
43
48
  @tempo_changes = tempo_changes
44
- super(parts: parts, program: program)
49
+ super(**kwargs)
45
50
  end
46
51
 
47
52
  def check_methods
@@ -54,7 +59,7 @@ class Score
54
59
  end
55
60
 
56
61
  def notes_long
57
- self.duration
62
+ max_part_duration
58
63
  end
59
64
 
60
65
  private
@@ -81,6 +86,7 @@ class Score
81
86
  # Tempo-based score without meter, bar lines, or fixed pulse (beat).
82
87
  # Offsets are note-based, and tempo values are in quarter-notes-per-minute.
83
88
  class Unmeasured < Score::TempoBased
89
+ alias duration notes_long
84
90
  end
85
91
 
86
92
  # Tempo-based score with meter, bar lines, and a fixed pulse (beat).
@@ -88,12 +94,12 @@ class Score
88
94
  class Measured < Score::TempoBased
89
95
  attr_accessor :start_meter, :meter_changes
90
96
 
91
- def initialize start_meter, start_tempo, meter_changes: {}, tempo_changes: {}, parts: {}, program: []
97
+ # See Score::TempoBased#initialize for remaining kwargs
98
+ def initialize start_meter, start_tempo, meter_changes: {}, **kwargs
92
99
  @start_meter = start_meter
93
100
  @meter_changes = meter_changes
94
101
 
95
- super(start_tempo, tempo_changes: tempo_changes,
96
- program: program, parts: parts)
102
+ super(start_tempo, **kwargs)
97
103
  end
98
104
 
99
105
  def check_methods
@@ -129,6 +135,8 @@ class Score
129
135
  end
130
136
  return moff_prev + Rational(noff_end - noff_prev, mdur_prev)
131
137
  end
138
+
139
+ alias duration measures_long
132
140
 
133
141
  private
134
142
 
@@ -7,67 +7,79 @@ class Score
7
7
  end
8
8
 
9
9
  def self.unpack packing
10
- score = Score.unpack_common(packing)
11
- new(parts: score.parts, program: score.program)
10
+ new(**Score.unpack_common(packing))
12
11
  end
13
12
  end
14
13
 
15
14
  class TempoBased < Score
16
15
  def pack
17
- packed_tcs = Hash[ tempo_changes.map do |offset,change|
18
- [offset,change.pack]
19
- end ]
20
-
21
16
  pack_common.merge("start_tempo" => @start_tempo,
22
- "tempo_changes" => packed_tcs)
17
+ "tempo_changes" => pack_tempo_changes)
23
18
  end
24
19
 
20
+ def pack_tempo_changes
21
+ Hash[ tempo_changes.map do |offset,change|
22
+ [offset,change.pack]
23
+ end ]
24
+ end
25
+
26
+ def self.unpack_tempo_changes packing
27
+ Hash[ packing["tempo_changes"].map do |k,v|
28
+ [k, Change.unpack(v) ]
29
+ end ]
30
+ end
31
+
25
32
  def self.unpack packing
26
- score = Score.unpack_common(packing)
27
-
28
33
  unpacked_tcs = Hash[ packing["tempo_changes"].map do |k,v|
29
34
  [k, Change.unpack(v) ]
30
35
  end ]
31
-
36
+
37
+ Score.unpack_common(packing)
32
38
  new(packing["start_tempo"],
33
39
  tempo_changes: unpacked_tcs,
34
- program: score.program,
35
- parts: score.parts
36
- )
37
- end
40
+ **unpacked)
41
+ end
38
42
  end
39
43
 
40
44
  class Unmeasured < TempoBased
41
- def pack
42
- super()
43
- end
44
-
45
45
  def self.unpack packing
46
- score = superclass.unpack(packing)
47
- new(score.start_tempo, program: score.program,
48
- tempo_changes: score.tempo_changes, parts: score.parts)
46
+ new(packing["start_tempo"],
47
+ tempo_changes: unpack_tempo_changes(packing),
48
+ **Score.unpack_common(packing))
49
49
  end
50
50
  end
51
51
 
52
52
  class Measured < TempoBased
53
53
  def pack
54
- return super().merge("start_meter" => start_meter.to_s,
55
- "meter_changes" => Hash[ meter_changes.map do |off,change|
56
- [off,change.pack(:with => :to_s)]
57
- end ]
58
- )
54
+ return super().merge("start_meter" => pack_start_meter,
55
+ "meter_changes" => pack_meter_changes)
56
+ end
57
+
58
+ def pack_meter_changes
59
+ Hash[ meter_changes.map do |off,change|
60
+ [off,change.pack(:with => :to_s)]
61
+ end ]
62
+ end
63
+
64
+ def pack_start_meter
65
+ start_meter.to_s
59
66
  end
60
67
 
61
- def self.unpack packing
62
- score = superclass.unpack(packing)
63
- unpacked_start_meter = Meter.parse(packing["start_meter"])
64
- unpacked_mcs = Hash[ packing["meter_changes"].map do |off,p|
68
+ def self.unpack_meter_changes packing
69
+ Hash[ packing["meter_changes"].map do |off,p|
65
70
  [off, Change.unpack(p, :with => :to_meter) ]
66
71
  end ]
67
-
68
- new(unpacked_start_meter, score.start_tempo,
69
- parts: score.parts, program: score.program,
70
- meter_changes: unpacked_mcs, tempo_changes: score.tempo_changes)
72
+ end
73
+
74
+ def self.unpack_start_meter packing
75
+ Meter.parse(packing["start_meter"])
76
+ end
77
+
78
+ def self.unpack packing
79
+ new(unpack_start_meter(packing), packing["start_tempo"],
80
+ tempo_changes: unpack_tempo_changes(packing),
81
+ meter_changes: unpack_meter_changes(packing),
82
+ **Score.unpack_common(packing))
71
83
  end
72
84
  end
73
85
 
@@ -89,6 +101,8 @@ class Score
89
101
  { "type" => self.class.to_s.split("::")[-1],
90
102
  "program" => packed_prog,
91
103
  "parts" => packed_parts,
104
+ "title" => @title,
105
+ "composer" => @composer
92
106
  }
93
107
  end
94
108
 
@@ -98,9 +112,11 @@ class Score
98
112
  end ]
99
113
  unpacked_prog = packing["program"].map {|str| Segment.parse(str) }
100
114
 
101
- new(program: unpacked_prog,
102
- parts: unpacked_parts
103
- )
115
+ { :program => unpacked_prog,
116
+ :parts => unpacked_parts,
117
+ :title => packing["title"],
118
+ :composer => packing["composer"]
119
+ }
104
120
  end
105
121
  end
106
122
 
@@ -15,41 +15,68 @@ class ScoreEngraver
15
15
  end
16
16
 
17
17
  @parts = score.collated? ? score.parts : ScoreCollator.new(score).collate_parts
18
+ @title = score.title
19
+ @composer = score.composer
18
20
  end
19
21
 
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
22
+ # Generate a Lilypond header for the score
23
+ def header
24
+ output = "\\version \"#{LILYPOND_VERSION}\"\n"
25
+ output += "\\header {\n"
26
+ if @title
27
+ output += " title = \"#{@title}\"\n"
28
+ end
29
+ if @composer
30
+ output += " composer = \"#{@composer}\"\n"
31
+ end
32
+ output += "}\n"
33
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
34
+ return output
35
+ end
42
36
 
43
- if (line.size + str.size) > MAX_LINE_LEN
44
- output += " " + line
45
- line = ""
46
- end
47
- line += str
37
+ # Generate a Lilypond staff for the given part
38
+ def staff part, part_title, master: false
39
+ clef = ScoreEngraver.best_clef(part.notes)
40
+ output = " \\new Staff {\n"
41
+ output += " \\set Staff.instrumentName = \\markup { \"#{part_title}\" }\n"
42
+ output += " \\clef #{clef}\n"
43
+ if(master)
44
+ output += " \\time #{@start_meter.to_lilypond}\n"
45
+ end
46
+
47
+ line = ""
48
+ part.notes.each_index do |i|
49
+ note = part.notes[i]
50
+ begin
51
+ str = note.to_lilypond + " "
52
+ rescue UnsupportedDurationError => e
53
+ binding.pry
48
54
  end
49
- output += " " + line
50
55
 
51
- output += " }\n"
52
-
56
+ if (line.size + str.size) > MAX_LINE_LEN
57
+ output += " " + line
58
+ line = ""
59
+ end
60
+ line += str
61
+ end
62
+ output += " " + line
63
+
64
+ output += " }\n"
65
+ end
66
+
67
+ # @param [Hash] part_names A hash for titling parts differently than their names
68
+ # @param [Array] selected_parts The names of parts selected for engraving
69
+ def make_lilypond selected_parts: @parts.keys, part_titles: {}
70
+ (selected_parts - part_titles.keys).each do |part_name|
71
+ part_titles[part_name] = part_name
72
+ end
73
+ output = header
74
+ output += "{\n <<\n"
75
+ master = true
76
+ selected_parts.each do |part_name|
77
+ part = @parts[part_name]
78
+ part_title = part_titles[part_name]
79
+ output += staff part, part_title, master: master
53
80
  master = false if master
54
81
  end
55
82
  output += " >>\n}\n"
@@ -1,3 +1,3 @@
1
1
  module Musicality
2
- VERSION = "0.5.1"
2
+ VERSION = "0.6.0"
3
3
  end
data/lib/musicality.rb CHANGED
@@ -82,6 +82,9 @@ require 'musicality/composition/transposition'
82
82
  require 'musicality/composition/generation/counterpoint_generator'
83
83
  require 'musicality/composition/generation/random_rhythm_generator'
84
84
 
85
+ require 'musicality/composition/dsl/score_methods'
86
+ require 'musicality/composition/dsl/score_dsl'
87
+
85
88
  #
86
89
  # Performance
87
90
  #
@@ -104,6 +107,10 @@ require 'musicality/performance/midi/part_sequencer'
104
107
  require 'musicality/performance/midi/score_sequencer'
105
108
  require 'musicality/performance/midi/score_sequencing'
106
109
 
110
+ #
111
+ # Printing
112
+ #
113
+
107
114
  require 'musicality/printing/lilypond/errors'
108
115
  require 'musicality/printing/lilypond/pitch_engraving'
109
116
  require 'musicality/printing/lilypond/note_engraving'
@@ -33,10 +33,10 @@ describe Score do
33
33
  end
34
34
  end
35
35
 
36
- describe '#duration' do
36
+ describe '#max_part_duration' do
37
37
  context 'no parts' do
38
38
  it 'should return 0' do
39
- Score.new.duration.should eq(0)
39
+ Score.new.max_part_duration.should eq(0)
40
40
  end
41
41
  end
42
42
 
@@ -44,7 +44,16 @@ describe Score do
44
44
  it 'should return the part duration' do
45
45
  Score.new(parts: {"part1" => Part.new(Dynamics::PP,
46
46
  notes: "/4 /4 /2 1".to_notes)
47
- }).duration.should eq(2)
47
+ }).max_part_duration.should eq(2)
48
+ end
49
+ end
50
+
51
+ context 'two parts' do
52
+ it 'should return the part duration of the longer part' do
53
+ Score.new(parts: {"part1" => Part.new(Dynamics::PP,
54
+ notes: "/4 /4 /2 1".to_notes), "part2" => Part.new(Dynamics::MP,
55
+ notes: "4".to_notes)
56
+ }).max_part_duration.should eq(4)
48
57
  end
49
58
  end
50
59
  end
@@ -1,6 +1,9 @@
1
1
  require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
2
2
 
3
3
  measured_score = Score::Measured.new(FOUR_FOUR,120) do |s|
4
+ s.title = "The best song ever"
5
+ s.composer = "James T."
6
+
4
7
  s.meter_changes[1] = Change::Immediate.new(THREE_FOUR)
5
8
  s.meter_changes[7] = Change::Immediate.new(SIX_EIGHT)
6
9
 
@@ -28,11 +31,15 @@ end
28
31
  unmeasured_score = Score::Unmeasured.new(30) do |s|
29
32
  s.program = measured_score.program
30
33
  s.parts = measured_score.parts
34
+ s.title = measured_score.title
35
+ s.composer = measured_score.composer
31
36
  end
32
37
 
33
38
  timed_score = Score::Timed.new do |s|
34
39
  s.program = measured_score.program
35
40
  s.parts = measured_score.parts
41
+ s.title = measured_score.title
42
+ s.composer = measured_score.composer
36
43
  end
37
44
 
38
45
  [ measured_score, unmeasured_score, timed_score ].each do |score|
@@ -72,6 +79,16 @@ end
72
79
  @h['parts'][name].should eq packing
73
80
  end
74
81
  end
82
+
83
+ it 'should add title to packing' do
84
+ @h.should have_key "title"
85
+ @h["title"].should eq @score.title
86
+ end
87
+
88
+ it 'should add composer to packing' do
89
+ @h.should have_key "composer"
90
+ @h["composer"].should eq @score.composer
91
+ end
75
92
  end
76
93
 
77
94
  describe '.unpack' do
@@ -90,6 +107,14 @@ end
90
107
  it 'should successfuly unpack the program' do
91
108
  @score2.program.should eq @score.program
92
109
  end
110
+
111
+ it 'should successfuly unpack the title' do
112
+ @score2.title.should eq @score.title
113
+ end
114
+
115
+ it 'should successfuly unpack the composer' do
116
+ @score2.composer.should eq @score.composer
117
+ end
93
118
  end
94
119
  end
95
120
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: musicality
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Tunnell
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-04-28 00:00:00.000000000 Z
11
+ date: 2015-04-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -137,6 +137,8 @@ files:
137
137
  - examples/notation/song1.rb
138
138
  - examples/notation/song2.rb
139
139
  - lib/musicality.rb
140
+ - lib/musicality/composition/dsl/score_dsl.rb
141
+ - lib/musicality/composition/dsl/score_methods.rb
140
142
  - lib/musicality/composition/generation/counterpoint_generator.rb
141
143
  - lib/musicality/composition/generation/random_rhythm_generator.rb
142
144
  - lib/musicality/composition/model/pitch_class.rb