lygre 0.0.1

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