lygre 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/grely.rb +37 -0
- data/lib/grely.rb +13 -0
- data/lib/lygre/gabcpitchreader.rb +134 -0
- data/lib/lygre/gabcscore.rb +88 -0
- data/lib/lygre/gabcsemantics.rb +138 -0
- data/lib/lygre/lilypondconvertor.rb +165 -0
- data/spec/gabcgrammar_spec.rb +222 -0
- data/spec/gabcparser_spec.rb +44 -0
- data/spec/gabcpitchreader_spec.rb +116 -0
- data/spec/gabcscore_spec.rb +143 -0
- data/spec/lilypondconvertor_spec.rb +144 -0
- data/spec/matchers.rb +20 -0
- data/spec/spec_helper.rb +37 -0
- metadata +125 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 526592f951e4b589a6c55ddcbdee18ef7316fe81
|
4
|
+
data.tar.gz: 41375a347f0b43ebd94eb7d48b7eec8136b06870
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: be30c1c2c467349f386da0be4838753acd35b0b7769420a6752b36cd48046ed72c9bf71dd38077500016cd162d7621b0bfaa787da2d31107c01a3c879ba1caf1
|
7
|
+
data.tar.gz: 61b8761522c20c47a2c41d445a255fd1dfb344fbced567bee020b0aefe6f3c8c526f79beb6df06b0858a478861f7c927b95366df1d4512321aea9c2160202f18
|
data/bin/grely.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
#!/bin/env ruby
|
2
|
+
|
3
|
+
# grely
|
4
|
+
|
5
|
+
# one day it will perform
|
6
|
+
# conversion gregorio (gabc) -> lilypond
|
7
|
+
|
8
|
+
# now it only
|
9
|
+
# says if it is able to parse the given input file
|
10
|
+
|
11
|
+
require 'grely'
|
12
|
+
|
13
|
+
parser = GabcParser.new
|
14
|
+
|
15
|
+
if ARGV.size >= 1 then
|
16
|
+
inputf = ARGV[0]
|
17
|
+
rf = File.open inputf
|
18
|
+
else
|
19
|
+
rf = STDIN
|
20
|
+
end
|
21
|
+
|
22
|
+
input = rf.read
|
23
|
+
|
24
|
+
result = parser.parse(input)
|
25
|
+
|
26
|
+
if result then
|
27
|
+
STDERR.puts 'grely thinks this is a valid gabc file.'
|
28
|
+
puts LilypondConvertor.new(cadenza: true).convert result.create_score
|
29
|
+
exit 0
|
30
|
+
else
|
31
|
+
STDERR.puts 'grely considers the input invalid gabc:'
|
32
|
+
STDERR.puts
|
33
|
+
STDERR.puts "'#{parser.failure_reason}' on line #{parser.failure_line} column #{parser.failure_column}:"
|
34
|
+
STDERR.puts input.split("\n")[parser.failure_line-1]
|
35
|
+
STDERR.puts (" " * parser.failure_column) + "^"
|
36
|
+
exit 1
|
37
|
+
end
|
data/lib/grely.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# libraries needed for the gregorio -> lilypond conversion
|
2
|
+
|
3
|
+
%w{
|
4
|
+
gabcscore
|
5
|
+
gabcsemantics
|
6
|
+
gabcpitchreader
|
7
|
+
lilypondconvertor
|
8
|
+
}.each {|f| require_relative File.join('lygre', f)}
|
9
|
+
|
10
|
+
# gabc parser
|
11
|
+
require 'polyglot'
|
12
|
+
require 'treetop'
|
13
|
+
Treetop.load File.expand_path('../lib/lygre/gabcgrammar', File.dirname(__FILE__))
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'rb-music-theory'
|
4
|
+
|
5
|
+
# responsible for converting the 'visual pitch' information
|
6
|
+
# contained in gabc to absolute musical pitch
|
7
|
+
class GabcPitchReader
|
8
|
+
CLEFS = {:c => "c''", :f => "f'"}
|
9
|
+
CLEF_POSITIONS = 1..4
|
10
|
+
|
11
|
+
def initialize(clef=:c, clef_position=4)
|
12
|
+
unless CLEFS.include? clef
|
13
|
+
raise ArgumentError.new "#{clef} is not a valid clef"
|
14
|
+
end
|
15
|
+
unless CLEF_POSITIONS.include? clef_position
|
16
|
+
raise ArgumentError.new "#{clef_position} is not a valid clef position"
|
17
|
+
end
|
18
|
+
|
19
|
+
@clef = clef
|
20
|
+
@clef_position = clef_position
|
21
|
+
|
22
|
+
init_base
|
23
|
+
end
|
24
|
+
|
25
|
+
attr_reader :clef, :clef_position, :base
|
26
|
+
|
27
|
+
# gets a gabc visual pitch, returns a RbMusicTheory::Note
|
28
|
+
def pitch(visual_note)
|
29
|
+
hnote = visual_note.to_s.ord - 'a'.ord # steps from a - the lowest writable gabc note
|
30
|
+
return @base.diatonic_steps(hnote)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def init_base
|
36
|
+
steps = -1 * (3 + # a is 3 steps under the first line
|
37
|
+
2 * (@clef_position - 1)) # one step for each line and space
|
38
|
+
@base = NoteFactory.create_note(CLEFS[@clef]).diatonic_steps(steps)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# an interface to create RBMusicTheory::Notes using lilypond notation
|
43
|
+
class NoteFactory
|
44
|
+
class << self
|
45
|
+
|
46
|
+
# notesym is a String - absolute pitch in the lilypond format.
|
47
|
+
# (currently alterations are not supported, as they are not necessary
|
48
|
+
# for our dealing with Gregorian chant.)
|
49
|
+
# returns a coresponding RBMusicTheory::Note
|
50
|
+
def create(notesym)
|
51
|
+
unless notesym =~ /^[a-g]('+|,+)?$/i
|
52
|
+
raise ArgumentError.new('#{notesym} is not a valid lilypond absolute pitch')
|
53
|
+
end
|
54
|
+
|
55
|
+
note = notesym[0]
|
56
|
+
octaves = notesym[1..-1]
|
57
|
+
|
58
|
+
n = RBMusicTheory::Note.new note.upcase
|
59
|
+
sign = 0
|
60
|
+
base_octave_shift = -1 # Note.new('C') returns c'=60, not c=48
|
61
|
+
if octaves then
|
62
|
+
sign = (octaves[0] == ',' ? -1 : 1)
|
63
|
+
octave_shift = (octaves.size * sign) + base_octave_shift
|
64
|
+
else
|
65
|
+
octave_shift = base_octave_shift
|
66
|
+
end
|
67
|
+
n += octave_shift * RBMusicTheory::NoteInterval.octave.value # strangely, NoteInterval cannot be multiplied
|
68
|
+
|
69
|
+
return n
|
70
|
+
end
|
71
|
+
|
72
|
+
alias :create_note :create
|
73
|
+
alias :[] :create
|
74
|
+
|
75
|
+
# returns a lilypond absolute pitch for the given RbMusicTheory::Note
|
76
|
+
#
|
77
|
+
# this method doesn't fit well in a *factory*, but
|
78
|
+
# #create translates lilypond pitch to Note and #lily_abs_pitch
|
79
|
+
# does the reverse translation, so maybe just the class should be renamed
|
80
|
+
def lily_abs_pitch(note)
|
81
|
+
octave_shifts = ''
|
82
|
+
octave_diff = note.value - create('c').value
|
83
|
+
|
84
|
+
octave_value = RBMusicTheory::NoteInterval.octave.value
|
85
|
+
octave_shift = octave_diff.abs / octave_value
|
86
|
+
if octave_diff < 0 and (octave_diff.abs % octave_value) > 0 then
|
87
|
+
octave_shift += 1
|
88
|
+
end
|
89
|
+
|
90
|
+
octave_signs = (octave_diff >= 0 ? "'" : ",") * octave_shift
|
91
|
+
note.name.downcase + octave_signs
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# monkey-patch Note to add step arithmetics
|
97
|
+
module RBMusicTheory
|
98
|
+
|
99
|
+
class Note
|
100
|
+
|
101
|
+
def diatonic_steps(steps, scale=nil)
|
102
|
+
if scale.nil? then
|
103
|
+
scale = self.class.new('C').major_scale
|
104
|
+
end
|
105
|
+
|
106
|
+
deg = self.degree_in(scale)
|
107
|
+
|
108
|
+
return scale.degree(deg + steps)
|
109
|
+
end
|
110
|
+
|
111
|
+
# note's degree in a scale
|
112
|
+
def degree_in(scale)
|
113
|
+
degree = scale.note_names.index(self.name)
|
114
|
+
if degree.nil? then
|
115
|
+
raise ArgumentError.new("#{name} is not a member of #{scale.note_names} scale")
|
116
|
+
end
|
117
|
+
degree += 1 # degrees start with 1
|
118
|
+
|
119
|
+
in_base_octave = scale.degree(degree)
|
120
|
+
octave_steps = scale.note_names.size
|
121
|
+
octave_value = RBMusicTheory::NoteInterval.octave.value
|
122
|
+
|
123
|
+
value_difference = self.value - in_base_octave.value
|
124
|
+
octave_difference = value_difference.abs / octave_value
|
125
|
+
if value_difference < 0 then
|
126
|
+
octave_difference *= -1
|
127
|
+
end
|
128
|
+
|
129
|
+
degree += octave_difference * octave_steps
|
130
|
+
|
131
|
+
return degree
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
# todo: wrap in a module
|
4
|
+
|
5
|
+
# initialized by arguments or by assignment in a block;
|
6
|
+
# frozen thereafter
|
7
|
+
class Immutable
|
8
|
+
def initialize(args={})
|
9
|
+
args.each_pair do |k,v|
|
10
|
+
writer = (k.to_s + '=').to_sym
|
11
|
+
self.send(writer, v)
|
12
|
+
end
|
13
|
+
|
14
|
+
yield self if block_given?
|
15
|
+
|
16
|
+
freeze
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# clean and easy to use music data produced by
|
21
|
+
# Gabc::ScoreNode#create_score
|
22
|
+
# from the syntax tree created by GabcParser
|
23
|
+
class GabcScore < Immutable
|
24
|
+
|
25
|
+
# header fields as Hash
|
26
|
+
attr_accessor :header
|
27
|
+
|
28
|
+
# music information as GabcMusic
|
29
|
+
attr_accessor :music
|
30
|
+
end
|
31
|
+
|
32
|
+
class GabcMusic < Immutable
|
33
|
+
|
34
|
+
attr_accessor :clef
|
35
|
+
|
36
|
+
# Array of GabcWords
|
37
|
+
attr_accessor :words
|
38
|
+
end
|
39
|
+
|
40
|
+
class GabcClef < Immutable
|
41
|
+
|
42
|
+
# 'c' or 'f'
|
43
|
+
attr_accessor :pitch
|
44
|
+
|
45
|
+
# Integer 1...4
|
46
|
+
attr_accessor :line
|
47
|
+
|
48
|
+
# Boolean
|
49
|
+
attr_accessor :bemol
|
50
|
+
|
51
|
+
def to_s
|
52
|
+
"#{pitch}#{bemol ? 'b' : ''}#{line}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# collection of syllables
|
57
|
+
class GabcWord < Array
|
58
|
+
|
59
|
+
def initialize(*args)
|
60
|
+
super(*args)
|
61
|
+
freeze
|
62
|
+
end
|
63
|
+
|
64
|
+
alias_method :each_syllable, :each
|
65
|
+
end
|
66
|
+
|
67
|
+
class GabcSyllable < Immutable
|
68
|
+
|
69
|
+
# String; may be empty
|
70
|
+
attr_accessor :lyrics
|
71
|
+
|
72
|
+
# Array of GabcNotes and other objects; may be empty
|
73
|
+
attr_accessor :notes
|
74
|
+
end
|
75
|
+
|
76
|
+
class GabcNote < Immutable
|
77
|
+
|
78
|
+
attr_accessor :pitch
|
79
|
+
attr_accessor :shape
|
80
|
+
attr_accessor :initio_debilis
|
81
|
+
attr_accessor :rhythmic_signs
|
82
|
+
attr_accessor :accent
|
83
|
+
end
|
84
|
+
|
85
|
+
class GabcDivisio < Immutable
|
86
|
+
|
87
|
+
attr_accessor :type
|
88
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
# classes containing semantics of various nodes
|
4
|
+
# instantiated by the GabcParser
|
5
|
+
|
6
|
+
require 'treetop'
|
7
|
+
require_relative 'gabcscore'
|
8
|
+
|
9
|
+
# monkey-patch SyntaxNode
|
10
|
+
# to add a useful traversal method
|
11
|
+
class Treetop::Runtime::SyntaxNode
|
12
|
+
|
13
|
+
def each_element
|
14
|
+
return if elements.nil?
|
15
|
+
elements.each {|e| yield e }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
module Gabc
|
20
|
+
|
21
|
+
SyntaxNode = Treetop::Runtime::SyntaxNode
|
22
|
+
|
23
|
+
# root node
|
24
|
+
class ScoreNode < SyntaxNode
|
25
|
+
|
26
|
+
# creates and returns a GabcScore from the syntax tree
|
27
|
+
def create_score
|
28
|
+
return GabcScore.new do |s|
|
29
|
+
s.header = header.to_hash
|
30
|
+
s.music = body.create_music
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
module HeaderNode
|
36
|
+
|
37
|
+
def to_hash
|
38
|
+
r = {}
|
39
|
+
|
40
|
+
each_element do |lvl1|
|
41
|
+
lvl1.each_element do |lvl2|
|
42
|
+
lvl2.each_element do |field|
|
43
|
+
if field.is_a? HeaderFieldNode then
|
44
|
+
r[field.field_id.text_value] = field.field_value.text_value
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
return r
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class HeaderFieldNode < SyntaxNode
|
55
|
+
end
|
56
|
+
|
57
|
+
class BodyNode < SyntaxNode
|
58
|
+
|
59
|
+
def create_music
|
60
|
+
GabcMusic.new do |m|
|
61
|
+
|
62
|
+
clef = elements.find {|e| e.respond_to? :clef_symbol }
|
63
|
+
if clef != nil then
|
64
|
+
m.clef = GabcClef.new(pitch: clef.clef_symbol.text_value.to_sym,
|
65
|
+
line: clef.line_number.text_value.to_i,
|
66
|
+
bemol: (clef.bemol.text_value == 'b'))
|
67
|
+
end
|
68
|
+
|
69
|
+
words = []
|
70
|
+
each_element do |ele|
|
71
|
+
if ele.is_a? WordNode then
|
72
|
+
words << ele.create_word
|
73
|
+
else
|
74
|
+
ele.each_element do |elel|
|
75
|
+
elel.each_element do |elelel|
|
76
|
+
if elelel.is_a? WordNode then
|
77
|
+
words << elelel.create_word
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
m.words = words
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
module WordNode
|
90
|
+
|
91
|
+
def create_word
|
92
|
+
w = []
|
93
|
+
|
94
|
+
each_element do |ele|
|
95
|
+
next unless ele.is_a? SyllableNode
|
96
|
+
w << GabcSyllable.new do |s|
|
97
|
+
s.lyrics = ele.lyrics.text_value
|
98
|
+
s.notes = collect_notes ele
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
return GabcWord.new w
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
# recursively collects notes from a node
|
108
|
+
def collect_notes(node, arr=[])
|
109
|
+
node.each_element do |ele|
|
110
|
+
if ele.is_a? NoteNode then
|
111
|
+
arr << GabcNote.new do |n|
|
112
|
+
n.pitch = ele.note_pitch.text_value.downcase.to_sym
|
113
|
+
end
|
114
|
+
elsif ele.is_a? DivisioNode then
|
115
|
+
arr << GabcDivisio.new do |d|
|
116
|
+
d.type = ele.text_value
|
117
|
+
end
|
118
|
+
else
|
119
|
+
collect_notes ele, arr
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
return arr
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
module SyllableNode
|
128
|
+
|
129
|
+
end
|
130
|
+
|
131
|
+
class NoteNode < SyntaxNode
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
module DivisioNode
|
136
|
+
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
class LilypondConvertor
|
4
|
+
|
5
|
+
# true - print if given; false - ignore; 'always' - print even if empty
|
6
|
+
DEFAULT_SETTINGS = {
|
7
|
+
version: true,
|
8
|
+
notes: true,
|
9
|
+
lyrics: true,
|
10
|
+
header: true,
|
11
|
+
cadenza: false
|
12
|
+
}
|
13
|
+
|
14
|
+
DEFAULT_CLEF = GabcClef.new(pitch: :c, line: 4, bemol: false)
|
15
|
+
|
16
|
+
# maps gabc divisiones to lilypond bars
|
17
|
+
BARS = {
|
18
|
+
':' => '\bar "|"',
|
19
|
+
';' => '\bar "|"',
|
20
|
+
'::' => '\bar "||"',
|
21
|
+
',' => '\bar "\'"'
|
22
|
+
}
|
23
|
+
|
24
|
+
def initialize(settings={})
|
25
|
+
@settings = DEFAULT_SETTINGS.dup.update(settings)
|
26
|
+
|
27
|
+
# todo: make it possible to freely choose absolute c _pitch_
|
28
|
+
@c_pitch = NoteFactory["c''"]
|
29
|
+
|
30
|
+
@lily_scale = [:c, :d, :e, :f, :g, :a, :b]
|
31
|
+
@gabc_lines = ['', :d, :f, :h, :j]
|
32
|
+
end
|
33
|
+
|
34
|
+
# converts GabcScore to Lilypond source
|
35
|
+
def convert(score)
|
36
|
+
header = score.header.keys.sort.collect do |k|
|
37
|
+
" #{k} = \"#{score.header[k]}\""
|
38
|
+
end.join "\n"
|
39
|
+
|
40
|
+
notes = []
|
41
|
+
lyrics = []
|
42
|
+
|
43
|
+
clef = score.music.clef
|
44
|
+
if clef == nil then
|
45
|
+
clef = DEFAULT_CLEF
|
46
|
+
end
|
47
|
+
@gabc_reader = GabcPitchReader.new clef.pitch, clef.line
|
48
|
+
|
49
|
+
score.music.words.each do |word|
|
50
|
+
notes << word_notes(word, clef)
|
51
|
+
lyrics << word_lyrics(word)
|
52
|
+
end
|
53
|
+
|
54
|
+
r = ''
|
55
|
+
|
56
|
+
r += "\\version \"2.16.0\"\n\n" if @settings[:version]
|
57
|
+
r += "\\score {\n"
|
58
|
+
|
59
|
+
if @settings[:notes] and
|
60
|
+
(notes.size > 0 or @settings[:notes] == 'always') then
|
61
|
+
r += " \\absolute {\n"
|
62
|
+
|
63
|
+
if @settings[:cadenza] then
|
64
|
+
r += " \\cadenzaOn\n"
|
65
|
+
end
|
66
|
+
|
67
|
+
r += " #{notes.join(" ")}\n" +
|
68
|
+
" }\n"
|
69
|
+
end
|
70
|
+
|
71
|
+
if @settings[:lyrics] and
|
72
|
+
(lyrics.size > 0 or @settings[:lyrics] == 'always') then
|
73
|
+
r += " \\addlyrics {\n" +
|
74
|
+
" #{lyrics.join(" ")}\n" +
|
75
|
+
" }\n"
|
76
|
+
end
|
77
|
+
|
78
|
+
if @settings[:header] and
|
79
|
+
(header.size > 0 or @settings[:header] == 'always') then
|
80
|
+
r += " \\header {\n" +
|
81
|
+
" #{header}\n" +
|
82
|
+
" }\n"
|
83
|
+
end
|
84
|
+
|
85
|
+
r += "}\n"
|
86
|
+
|
87
|
+
return r
|
88
|
+
end
|
89
|
+
|
90
|
+
# returns the output of #convert 'minimized', with whitespace reduced
|
91
|
+
# and normalized (useful for testing)
|
92
|
+
def convert_min(score)
|
93
|
+
convert(score).gsub(/\s+/, ' ').strip
|
94
|
+
end
|
95
|
+
|
96
|
+
# makes a melisma from a group of notes
|
97
|
+
def melisma(notes)
|
98
|
+
notes[0] = (notes[0].to_s + '(').to_sym
|
99
|
+
notes[-1] = (notes[-1].to_s + ')').to_sym
|
100
|
+
return notes
|
101
|
+
end
|
102
|
+
|
103
|
+
def word_notes(word, clef)
|
104
|
+
r = []
|
105
|
+
word.each_syllable do |syl|
|
106
|
+
notes = syl.notes
|
107
|
+
|
108
|
+
if notes.empty? then
|
109
|
+
r << 's'
|
110
|
+
else
|
111
|
+
sylnotes = notes.collect do |n|
|
112
|
+
if n.is_a? GabcNote then
|
113
|
+
NoteFactory.lily_abs_pitch(@gabc_reader.pitch(n.pitch))
|
114
|
+
|
115
|
+
elsif n.is_a? GabcDivisio then
|
116
|
+
divisio = n.type
|
117
|
+
unless BARS.has_key? divisio
|
118
|
+
raise RuntimeError.new "Unhandled bar type '#{n.type}'"
|
119
|
+
end
|
120
|
+
|
121
|
+
BARS[divisio].dup
|
122
|
+
|
123
|
+
else
|
124
|
+
raise RuntimeError.new "Unknown music content #{n}"
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
if notes.size >= 2 then
|
129
|
+
sylnotes = melisma sylnotes
|
130
|
+
end
|
131
|
+
r += sylnotes
|
132
|
+
end
|
133
|
+
end
|
134
|
+
return r.join ' '
|
135
|
+
end
|
136
|
+
|
137
|
+
def word_lyrics(word)
|
138
|
+
word.collect do |syll|
|
139
|
+
l = syll.lyrics
|
140
|
+
|
141
|
+
if syll.lyrics.start_with? '*' then
|
142
|
+
l = '"' + syll.lyrics + '"'
|
143
|
+
end
|
144
|
+
|
145
|
+
if syll.lyrics.include? '<' then
|
146
|
+
l = syll.lyrics.gsub(/<i>([^<]+)<\/i>/) do |m|
|
147
|
+
'\italic{' + $1 + '}'
|
148
|
+
end
|
149
|
+
l = '\markup{'+l+'}'
|
150
|
+
end
|
151
|
+
|
152
|
+
if syll.notes.size == 1 and
|
153
|
+
syll.notes.first.is_a? GabcDivisio and
|
154
|
+
syll.lyrics.size > 0 then
|
155
|
+
|
156
|
+
unless l.start_with? '\markup'
|
157
|
+
l = '\markup{'+l+'}'
|
158
|
+
end
|
159
|
+
l = '\set stanza = '+l
|
160
|
+
end
|
161
|
+
|
162
|
+
l
|
163
|
+
end.join ' -- '
|
164
|
+
end
|
165
|
+
end
|