lygre 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
@@ -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