musicality 0.5.1 → 0.6.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 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