ramekin 0.0.5b
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 +7 -0
- data/README.md +277 -0
- data/gembin/ramekin +16 -0
- data/lib/ramekin/amk_runner/sample_groups.rb +65 -0
- data/lib/ramekin/amk_runner.rb +123 -0
- data/lib/ramekin/amk_setup.rb +116 -0
- data/lib/ramekin/channel_separator.rb +40 -0
- data/lib/ramekin/cli.rb +367 -0
- data/lib/ramekin/config.rb +126 -0
- data/lib/ramekin/element.rb +24 -0
- data/lib/ramekin/errors.rb +57 -0
- data/lib/ramekin/legato.rb +142 -0
- data/lib/ramekin/macros.rb +101 -0
- data/lib/ramekin/meta.rb +227 -0
- data/lib/ramekin/note_aggregator.rb +252 -0
- data/lib/ramekin/processor.rb +78 -0
- data/lib/ramekin/renderer.rb +288 -0
- data/lib/ramekin/sample_pack.rb +296 -0
- data/lib/ramekin/spc_player.rb +122 -0
- data/lib/ramekin/tokenizer.rb +287 -0
- data/lib/ramekin/util.rb +120 -0
- data/lib/ramekin/volume.rb +16 -0
- data/lib/ramekin.rb +19 -0
- metadata +122 -0
data/lib/ramekin/cli.rb
ADDED
@@ -0,0 +1,367 @@
|
|
1
|
+
module Ramekin
|
2
|
+
class CLI
|
3
|
+
include Util
|
4
|
+
|
5
|
+
def self.main(*argv)
|
6
|
+
new.main(*argv)
|
7
|
+
end
|
8
|
+
|
9
|
+
def tempfiles
|
10
|
+
@tempfiles ||= []
|
11
|
+
end
|
12
|
+
|
13
|
+
def tempfile(fname)
|
14
|
+
dir = Dir.mktmpdir
|
15
|
+
tempfiles << dir
|
16
|
+
"#{dir}/#{fname}"
|
17
|
+
end
|
18
|
+
|
19
|
+
def usage
|
20
|
+
$stderr.puts "Ramekin version #{RAMEKIN_VERSION}"
|
21
|
+
$stderr.puts ""
|
22
|
+
$stderr.puts "usage: ramekin [command] [flags]"
|
23
|
+
$stderr.puts " (default command: compile)"
|
24
|
+
$stderr.puts ""
|
25
|
+
$stderr.puts "ramekin compile -i filename.rmk [flags]"
|
26
|
+
$stderr.puts " flags:"
|
27
|
+
$stderr.puts " -h --help"
|
28
|
+
$stderr.puts " display this message"
|
29
|
+
$stderr.puts " -i filename.rmk"
|
30
|
+
$stderr.puts " set the input file"
|
31
|
+
$stderr.puts " --txt filename.txt"
|
32
|
+
$stderr.puts " output amk txt to this file"
|
33
|
+
$stderr.puts " --spc filename.spc"
|
34
|
+
$stderr.puts " attempt to invoke AddmusicK to output an SPC"
|
35
|
+
$stderr.puts " requires ADDMUSICK_DIR and ASAR_COMMAND"
|
36
|
+
$stderr.puts " to be set."
|
37
|
+
$stderr.puts " --play"
|
38
|
+
$stderr.puts " play the generated SPC file. will automatically"
|
39
|
+
$stderr.puts " set up an SPC player if none is detected."
|
40
|
+
default_text = if Ramekin.config.windows?
|
41
|
+
'(installs spcplay.exe, which can render WAVs)'
|
42
|
+
else
|
43
|
+
'(compiles and installs spct - requires git,cmake,make)'
|
44
|
+
end
|
45
|
+
$stderr.puts " #{default_text}"
|
46
|
+
unless Ramekin.config.windows?
|
47
|
+
$stderr.puts " --wav filename.wav"
|
48
|
+
$stderr.puts " --wav filename.wav:N"
|
49
|
+
$stderr.puts " render N seconds of the file to WAV. Requires spct."
|
50
|
+
end
|
51
|
+
$stderr.puts ""
|
52
|
+
$stderr.puts "ramekin package [flags]"
|
53
|
+
$stderr.puts " flags:"
|
54
|
+
$stderr.puts " --update"
|
55
|
+
$stderr.puts " download BRR packages from SMW Central."
|
56
|
+
$stderr.puts " --search [text]"
|
57
|
+
$stderr.puts " search for BRR filenames."
|
58
|
+
$stderr.puts " --list [text]"
|
59
|
+
$stderr.puts " list all samples in packages matching [text]."
|
60
|
+
$stderr.puts ""
|
61
|
+
$stderr.puts "ramekin setup [flags]"
|
62
|
+
$stderr.puts " flags:"
|
63
|
+
$stderr.puts " --amk"
|
64
|
+
$stderr.puts " downloads and sets up AddMusicK and asar."
|
65
|
+
unless Ramekin.config.windows?
|
66
|
+
$stderr.puts " (requires cmake and make to be available)"
|
67
|
+
end
|
68
|
+
$stderr.puts " --packages"
|
69
|
+
$stderr.puts " downloads packages from SMW Central."
|
70
|
+
$stderr.puts " (equivalent to package --update)"
|
71
|
+
$stderr.puts " --spc"
|
72
|
+
$stderr.puts " downloads and sets up a default SPC player."
|
73
|
+
$stderr.puts " --all (default)"
|
74
|
+
$stderr.puts " runs all of the above setup steps"
|
75
|
+
$stderr.puts " --force"
|
76
|
+
$stderr.puts " deletes and redownloads files instead of skipping."
|
77
|
+
end
|
78
|
+
|
79
|
+
def usage!(message)
|
80
|
+
$stderr.puts(message)
|
81
|
+
$stderr.puts
|
82
|
+
usage
|
83
|
+
|
84
|
+
exit 1
|
85
|
+
end
|
86
|
+
|
87
|
+
def fail!(message)
|
88
|
+
$stderr.puts(message)
|
89
|
+
$stderr.puts
|
90
|
+
|
91
|
+
exit 1
|
92
|
+
end
|
93
|
+
|
94
|
+
def compile(*argv)
|
95
|
+
@force = false
|
96
|
+
@play = false
|
97
|
+
|
98
|
+
while (h = argv.shift)
|
99
|
+
case h
|
100
|
+
when '--version', '-V'
|
101
|
+
puts RAMEKIN_VERSION
|
102
|
+
return 0
|
103
|
+
when '--help', '-help', '-h', '-?'
|
104
|
+
usage
|
105
|
+
return 0
|
106
|
+
when '--txt'
|
107
|
+
@output_txt = argv.shift or usage! 'missing filename after --txt'
|
108
|
+
when '--spc'
|
109
|
+
@output_spc = argv.shift or usage! 'missing filename after --spc'
|
110
|
+
when '--package'
|
111
|
+
@output_package = argv.shift or usage! 'missing dirname after --package'
|
112
|
+
when '--play'
|
113
|
+
@play = true
|
114
|
+
when '--wav'
|
115
|
+
arg = argv.shift or usage! 'missing wav file after --render'
|
116
|
+
@wav_file, @wav_seconds = arg.split(':')
|
117
|
+
@wav_seconds ||= ENV['SECONDS'] || 60
|
118
|
+
@wav_seconds = @wav_seconds.to_i or usage! 'invalid number of seconds'
|
119
|
+
when '--force', '-f'
|
120
|
+
@force = true
|
121
|
+
when '-i'
|
122
|
+
@infile = argv.shift or usage! 'missing filename after -i'
|
123
|
+
when '-v', '--verbose'
|
124
|
+
@verbose = true
|
125
|
+
when /\A-/
|
126
|
+
usage! "unknown argument #{h}"
|
127
|
+
else
|
128
|
+
@infile = h
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# if we want to play or render wav, we have to make sure we
|
133
|
+
# have a path to render the SPC file to.
|
134
|
+
if @play || @wav_file
|
135
|
+
if @output_package
|
136
|
+
@play_spc = "#@output_package/#{File.basename(@infile)}.spc"
|
137
|
+
elsif @output_spc
|
138
|
+
@play_spc = @output_spc
|
139
|
+
else
|
140
|
+
@output_spc = @play_spc = tempfile("#{File.basename(@infile)}.spc")
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
usage! 'missing infile (use -i)' if @infile.nil?
|
145
|
+
@infile = File.expand_path(@infile)
|
146
|
+
fail! "can't find a file at #{@infile}" unless File.exist?(@infile)
|
147
|
+
|
148
|
+
source = begin
|
149
|
+
File.read(@infile)
|
150
|
+
rescue
|
151
|
+
fail! "couldn't read the file at #{@infile}"
|
152
|
+
end
|
153
|
+
|
154
|
+
tokens = Tokenizer.tokenize(source).to_a
|
155
|
+
|
156
|
+
outer_chain = Processor.compose(
|
157
|
+
MacroExpander,
|
158
|
+
ScanForL,
|
159
|
+
)
|
160
|
+
|
161
|
+
expanded = outer_chain.call(tokens)
|
162
|
+
|
163
|
+
channels = ChannelSeparator.parse(expanded)
|
164
|
+
|
165
|
+
inner_chain = Processor.compose(
|
166
|
+
NoteAggregator,
|
167
|
+
RestAggregator,
|
168
|
+
Legato,
|
169
|
+
Inspector,
|
170
|
+
)
|
171
|
+
|
172
|
+
channels.channels.map! do |c|
|
173
|
+
next if c.nil?
|
174
|
+
|
175
|
+
inner_chain.call(c)
|
176
|
+
end
|
177
|
+
|
178
|
+
|
179
|
+
txt = Renderer.render(@infile, channels)
|
180
|
+
|
181
|
+
if Error.any?
|
182
|
+
puts "errors:"
|
183
|
+
puts
|
184
|
+
Error.all.each do |error|
|
185
|
+
puts error.present(source)
|
186
|
+
end
|
187
|
+
|
188
|
+
exit 1
|
189
|
+
end
|
190
|
+
|
191
|
+
unless @output_txt || @output_package || @output_spc
|
192
|
+
warn <<~WARN
|
193
|
+
no output specified. use either:
|
194
|
+
--txt my_cool_file.txt
|
195
|
+
--spc my_cool_file.spc
|
196
|
+
--package my_cool_output_dir
|
197
|
+
--play
|
198
|
+
--wav my_cool_file.wav
|
199
|
+
WARN
|
200
|
+
end
|
201
|
+
|
202
|
+
if @output_txt
|
203
|
+
File.write(@output_txt, txt)
|
204
|
+
end
|
205
|
+
|
206
|
+
if @output_spc || @output_package
|
207
|
+
runner = AMKRunner.new(@infile, channels.meta, txt)
|
208
|
+
runner.compile
|
209
|
+
end
|
210
|
+
|
211
|
+
FileUtils.cp(runner.spc_file, @output_spc) if @output_spc
|
212
|
+
if @output_package
|
213
|
+
clear_dir(@output_package, force: @force) or exit 1
|
214
|
+
runner.export(@output_package)
|
215
|
+
|
216
|
+
$stderr.puts <<~MSG
|
217
|
+
Package directory generated at #{@output_package}
|
218
|
+
MSG
|
219
|
+
|
220
|
+
$stderr.puts <<~MSG unless channels.meta.readme
|
221
|
+
Please remember to edit README.txt!
|
222
|
+
MSG
|
223
|
+
end
|
224
|
+
|
225
|
+
if @wav_file
|
226
|
+
SPCPlayer.instance.setup!
|
227
|
+
SPCPlayer.instance.render(@play_spc, @wav_file, @wav_seconds)
|
228
|
+
end
|
229
|
+
|
230
|
+
if @play
|
231
|
+
SPCPlayer.instance.setup!
|
232
|
+
SPCPlayer.instance.play(@play_spc)
|
233
|
+
end
|
234
|
+
|
235
|
+
if @verbose
|
236
|
+
aggregated.each do |a|
|
237
|
+
puts a.inspect
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
tempfiles.each { |path| FileUtils.rm_r(path) if path && !path.empty? }
|
242
|
+
end
|
243
|
+
|
244
|
+
def package(*argv)
|
245
|
+
@mode = nil
|
246
|
+
@update = false
|
247
|
+
@search = nil
|
248
|
+
while (h = argv.shift)
|
249
|
+
case h
|
250
|
+
when '--update'
|
251
|
+
@update = true
|
252
|
+
when '--search'
|
253
|
+
@mode = :search
|
254
|
+
@term = argv.shift || ''
|
255
|
+
when '--list'
|
256
|
+
@mode = :list
|
257
|
+
@term = argv.shift || ''
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
if @update
|
262
|
+
$stderr.puts "=== updating packages... ==="
|
263
|
+
SamplePack.download_all!
|
264
|
+
end
|
265
|
+
|
266
|
+
case @mode
|
267
|
+
when :search
|
268
|
+
search(@term, list_matched: false, search_brr: true)
|
269
|
+
when :list
|
270
|
+
search(@term, list_matched: true, search_brr: false)
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
def setup(*argv)
|
275
|
+
@force = false
|
276
|
+
@setup_packages = false
|
277
|
+
@setup_amk = false
|
278
|
+
@default_action = true
|
279
|
+
while (h = argv.shift)
|
280
|
+
case h
|
281
|
+
when '--all'
|
282
|
+
@setup_packages = true
|
283
|
+
@setup_amk = true
|
284
|
+
@setup_spc = true
|
285
|
+
@default_action = false
|
286
|
+
when '--amk'
|
287
|
+
@setup_amk = true
|
288
|
+
@default_action = false
|
289
|
+
when '--packages'
|
290
|
+
@setup_packages = true
|
291
|
+
@default_action = false
|
292
|
+
when '--spc'
|
293
|
+
@setup_spc = true
|
294
|
+
@default_action = false
|
295
|
+
when '--force'
|
296
|
+
@force = true
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
if @default_action
|
301
|
+
@setup_packages = true
|
302
|
+
@setup_amk = true
|
303
|
+
end
|
304
|
+
|
305
|
+
if @setup_amk
|
306
|
+
AMKSetup.setup!(force: @force)
|
307
|
+
end
|
308
|
+
|
309
|
+
if @setup_spc
|
310
|
+
SPCPlayer.instance.setup!
|
311
|
+
end
|
312
|
+
|
313
|
+
if @setup_packages
|
314
|
+
SamplePack.download_all!
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
def search(term, list_matched: false, search_brr: false)
|
319
|
+
term = term.downcase
|
320
|
+
|
321
|
+
SamplePack.each do |package|
|
322
|
+
matched_pack = package.name.downcase.include?(term)
|
323
|
+
|
324
|
+
matched_brr = package.unprefixed_brrs.select do |brr|
|
325
|
+
brr.downcase.include?(term)
|
326
|
+
end if search_brr
|
327
|
+
|
328
|
+
if search_brr && matched_brr.any?
|
329
|
+
puts "#pack #{package.name.inspect}"
|
330
|
+
dump_brrs(package, matched_brr)
|
331
|
+
elsif matched_pack
|
332
|
+
puts "#pack #{package.name.inspect}"
|
333
|
+
if list_matched
|
334
|
+
dump_brrs(package, package.unprefixed_brrs)
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
def dump_brrs(pack, brrs)
|
341
|
+
brrs.each do |brr|
|
342
|
+
tunings = pack.tunings_for(brr)
|
343
|
+
out = " #{brr.inspect}"
|
344
|
+
out << " (#{tunings.size} tunings)" if tunings.size != 1
|
345
|
+
puts out
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
def main(*argv)
|
350
|
+
if argv.empty?
|
351
|
+
usage
|
352
|
+
return 0
|
353
|
+
end
|
354
|
+
|
355
|
+
case argv[0]
|
356
|
+
when 'compile'
|
357
|
+
compile(*argv[1..])
|
358
|
+
when 'package'
|
359
|
+
package(*argv[1..])
|
360
|
+
when 'setup'
|
361
|
+
setup(*argv[1..])
|
362
|
+
else
|
363
|
+
compile(*argv)
|
364
|
+
end
|
365
|
+
end
|
366
|
+
end
|
367
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Ramekin
|
4
|
+
HOME = File.expand_path(ENV['RAMEKIN_HOME'] || RAMEKIN_HOME)
|
5
|
+
|
6
|
+
FileUtils.mkdir_p(HOME)
|
7
|
+
|
8
|
+
Dir.chdir(HOME) do
|
9
|
+
if RbConfig::CONFIG['SOEXT'] == 'dll'
|
10
|
+
default_player_action = 'automatically download and run spcplay.exe from SMW Central'
|
11
|
+
else
|
12
|
+
default_player_action = 'automatically download, compile and run spct from jneen\'s codeberg'
|
13
|
+
end
|
14
|
+
|
15
|
+
File.write('config.yml', <<~YML) unless File.exist?('config.yml')
|
16
|
+
# This file configures various options for Ramekin.
|
17
|
+
# Lines beginning with # are comments.
|
18
|
+
# For each option, the default value is shown.
|
19
|
+
|
20
|
+
# File paths are relative to the directory this file is in,
|
21
|
+
# or the environment variable RAMEKIN_HOME.
|
22
|
+
|
23
|
+
# Override where Ramekin should find / install AddmusicK
|
24
|
+
# also overrideable with env var RAMEKIN_AMK_DIR
|
25
|
+
# amk_dir: ./addmusick
|
26
|
+
|
27
|
+
# Override where Ramekin should find / install asar
|
28
|
+
# also overrideable with env var RAMEKIN_ASAR_DIR
|
29
|
+
# asar_dir: ./asar
|
30
|
+
|
31
|
+
# Override where Ramekin stores packages from SMW Central
|
32
|
+
# also overrideable with env var RAMEKIN_PACKAGES_DIR
|
33
|
+
# packages_dir: ./packages
|
34
|
+
|
35
|
+
# Use a custom command for playing SPC files.
|
36
|
+
# Either a command line string, or the special setting __auto__,
|
37
|
+
# which will #{default_player_action}.
|
38
|
+
#
|
39
|
+
# also overrideable with env var RAMEKIN_SPC_PLAYER
|
40
|
+
#
|
41
|
+
# spc_player: __auto__
|
42
|
+
# spc_player: C:\\Users\\jneen\\Desktop\\spcplay\\spcplay.exe
|
43
|
+
# spc_player: /path/to/bin/spct
|
44
|
+
|
45
|
+
YML
|
46
|
+
end
|
47
|
+
|
48
|
+
class Config
|
49
|
+
def initialize(file)
|
50
|
+
@raw = YAML.load_file(file) || {}
|
51
|
+
end
|
52
|
+
|
53
|
+
def lib_ext
|
54
|
+
RbConfig::CONFIG['SOEXT']
|
55
|
+
end
|
56
|
+
|
57
|
+
def windows?
|
58
|
+
lib_ext == 'dll'
|
59
|
+
end
|
60
|
+
|
61
|
+
def macos?
|
62
|
+
lib_ext == 'dylib'
|
63
|
+
end
|
64
|
+
|
65
|
+
def linux?
|
66
|
+
lib_ext == 'so'
|
67
|
+
end
|
68
|
+
|
69
|
+
def raw_option(upname, downname, default)
|
70
|
+
ENV[upname] || @raw[downname] || Object.const_get(upname)
|
71
|
+
end
|
72
|
+
|
73
|
+
def option(upname, downname, default)
|
74
|
+
expand(raw_option(upname, downname, default))
|
75
|
+
end
|
76
|
+
|
77
|
+
def amk_dir
|
78
|
+
option('RAMEKIN_AMK_DIR', 'amk_dir', 'addmusick')
|
79
|
+
end
|
80
|
+
|
81
|
+
def asar_dir
|
82
|
+
option('RAMEKIN_ASAR_DIR', 'asar_dir', 'asar')
|
83
|
+
end
|
84
|
+
|
85
|
+
def spc_player
|
86
|
+
raw_option('RAMEKIN_SPC_PLAYER', 'spc_player', '__auto__')
|
87
|
+
end
|
88
|
+
|
89
|
+
def asar_lib
|
90
|
+
try_asar_lib or begin
|
91
|
+
$stderr.puts "can't find asar lib. did you compile it?"
|
92
|
+
exit 1
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def try_asar_lib
|
97
|
+
return nil unless Dir.exist?(asar_dir)
|
98
|
+
|
99
|
+
path = Dir.chdir(asar_dir) do
|
100
|
+
Dir.glob('asar/lib/*').find do |f|
|
101
|
+
next unless File.file?(f)
|
102
|
+
next if File.symlink?(f)
|
103
|
+
f.end_with?(lib_ext)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
path && File.expand_path(path, asar_dir)
|
108
|
+
end
|
109
|
+
|
110
|
+
def packages_dir
|
111
|
+
expand(ENV['RAMEKIN_PACKAGES_DIR'] || @raw['packages_dir'] || 'packages')
|
112
|
+
end
|
113
|
+
|
114
|
+
def expand(path)
|
115
|
+
File.expand_path(path, HOME)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def self.config
|
120
|
+
@config ||= Config.new("#{HOME}/config.yml")
|
121
|
+
end
|
122
|
+
|
123
|
+
FileUtils.mkdir_p(config.packages_dir)
|
124
|
+
FileUtils.mkdir_p(config.asar_dir) unless config.windows?
|
125
|
+
FileUtils.mkdir_p(config.amk_dir)
|
126
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Ramekin
|
2
|
+
class Element
|
3
|
+
def self.make(components, *a)
|
4
|
+
new(*a).tap { |e| e.components = components }
|
5
|
+
end
|
6
|
+
|
7
|
+
attr_accessor :components
|
8
|
+
attr_accessor :start_tick
|
9
|
+
attr_accessor :channel
|
10
|
+
|
11
|
+
attr_writer :macro_stack
|
12
|
+
def macro_stack
|
13
|
+
@macro_stack ||= []
|
14
|
+
end
|
15
|
+
|
16
|
+
def start
|
17
|
+
@components.first.start
|
18
|
+
end
|
19
|
+
|
20
|
+
def fin
|
21
|
+
@components.last.fin
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Ramekin
|
2
|
+
class Error
|
3
|
+
def self.all
|
4
|
+
@all ||= []
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.any?
|
8
|
+
all.size > 0
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.last
|
12
|
+
all.last
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.error!(e, nesting: 0)
|
16
|
+
all << e
|
17
|
+
|
18
|
+
if ENV['RAMEKIN_DEBUG']
|
19
|
+
if nesting >= 0
|
20
|
+
Pry.config.hooks.add_hook(:before_session, :ramekin) do |output, binding, pry|
|
21
|
+
Pry.config.hooks.delete_hook(:before_session, :ramekin)
|
22
|
+
pry.run_command("up #{nesting + 1}")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
binding.pry
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize(start, fin, message)
|
31
|
+
@start = start
|
32
|
+
@range = LocRange.between(start, fin)
|
33
|
+
@message = message
|
34
|
+
end
|
35
|
+
|
36
|
+
def present(orig)
|
37
|
+
out = []
|
38
|
+
out << "line #{@range.start.line}, col #{@range.start.col}:"
|
39
|
+
out << " ---"
|
40
|
+
out << " #{orig[@range.pos_range]}"
|
41
|
+
out << " ---"
|
42
|
+
out << " #{@message}"
|
43
|
+
end
|
44
|
+
|
45
|
+
module Helpers
|
46
|
+
private
|
47
|
+
def error!(message, el: nil, nesting: 0)
|
48
|
+
el ||= @current
|
49
|
+
Error.error!(Error.new(el, el, message), nesting: nesting + 1)
|
50
|
+
end
|
51
|
+
|
52
|
+
def default_error_location
|
53
|
+
@current
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
module Ramekin
|
2
|
+
class LegatoStart < Element
|
3
|
+
def to_amk
|
4
|
+
'$f4$01'
|
5
|
+
end
|
6
|
+
|
7
|
+
def inspect
|
8
|
+
'legato-start'
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class NoteWrapper < NoteEvent
|
13
|
+
def initialize(note)
|
14
|
+
@note = note
|
15
|
+
end
|
16
|
+
|
17
|
+
def rest?
|
18
|
+
@note.rest?
|
19
|
+
end
|
20
|
+
|
21
|
+
def octave_amk(*a)
|
22
|
+
@note.octave_amk(*a)
|
23
|
+
end
|
24
|
+
|
25
|
+
def note_name
|
26
|
+
@note.note_name
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class LegatoLastNote < NoteWrapper
|
31
|
+
def ticks
|
32
|
+
@note.ticks / 2
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_amk(current_octave=nil)
|
36
|
+
post_ticks = @note.ticks - ticks
|
37
|
+
post_len = KNOWN_LENGTHS.fetch(post_ticks) { "=#{post_ticks}" }
|
38
|
+
"#{octave_amk(current_octave)}#{note_name}#{length_amk}$f4$01^#{post_len}"
|
39
|
+
end
|
40
|
+
|
41
|
+
def inspect
|
42
|
+
"legato-end(#{@note.inspect})"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class Legato < Processor
|
47
|
+
def initialize
|
48
|
+
@in_legato = false
|
49
|
+
@seen_legato = false
|
50
|
+
end
|
51
|
+
|
52
|
+
def flush_legato!(&b)
|
53
|
+
non_notes = []
|
54
|
+
non_notes << buffer.pop while buffer.any? && !buffer.last.is_a?(NoteEvent)
|
55
|
+
|
56
|
+
return if buffer.empty?
|
57
|
+
return flush!(&b) if buffer.size == 1
|
58
|
+
|
59
|
+
yield LegatoStart.new
|
60
|
+
last = buffer.pop
|
61
|
+
error! "legato marks must follow a note", el: last unless NoteEvent === last
|
62
|
+
buffer.each(&b)
|
63
|
+
yield LegatoLastNote.new(last)
|
64
|
+
buffer.clear
|
65
|
+
ensure
|
66
|
+
non_notes.reverse_each(&b)
|
67
|
+
end
|
68
|
+
|
69
|
+
TIEABLE_EVENTS = %i(
|
70
|
+
amp
|
71
|
+
y
|
72
|
+
rely
|
73
|
+
v
|
74
|
+
relv
|
75
|
+
o
|
76
|
+
p
|
77
|
+
hex
|
78
|
+
octave
|
79
|
+
transpose
|
80
|
+
adsr
|
81
|
+
bpm
|
82
|
+
)
|
83
|
+
def call(&b)
|
84
|
+
return enum_for(:process) unless block_given?
|
85
|
+
|
86
|
+
each do |el|
|
87
|
+
if @in_legato
|
88
|
+
if NoteEvent === el
|
89
|
+
if @seen_legato
|
90
|
+
@seen_legato = false
|
91
|
+
buffer << el
|
92
|
+
else
|
93
|
+
@in_legato = false
|
94
|
+
@seen_legato = false
|
95
|
+
flush_legato!(&b)
|
96
|
+
buffer!(el, &b)
|
97
|
+
end
|
98
|
+
elsif Token === el && el.type == :amp
|
99
|
+
@seen_legato = true
|
100
|
+
buffer << el
|
101
|
+
elsif Token === el && el.type == :legato_tie
|
102
|
+
@seen_legato = true
|
103
|
+
elsif Token === el && TIEABLE_EVENTS.include?(el.type)
|
104
|
+
buffer << el
|
105
|
+
else
|
106
|
+
@in_legato = false
|
107
|
+
@seen_legato = false
|
108
|
+
flush_legato!(&b)
|
109
|
+
yield el
|
110
|
+
end
|
111
|
+
else
|
112
|
+
if NoteEvent === el
|
113
|
+
flush!(&b)
|
114
|
+
buffer!(el, &b)
|
115
|
+
elsif Token === el && TIEABLE_EVENTS.include?(el.type)
|
116
|
+
buffer << el
|
117
|
+
elsif Token === el && el.type == :legato_tie
|
118
|
+
@seen_legato = true
|
119
|
+
@in_legato = true
|
120
|
+
else
|
121
|
+
flush!(&b)
|
122
|
+
yield el
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
if @in_legato
|
128
|
+
flush_legato!(&b)
|
129
|
+
else
|
130
|
+
flush!(&b)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def buffer!(note, &b)
|
135
|
+
if note.rest?
|
136
|
+
yield note
|
137
|
+
else
|
138
|
+
buffer << note
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|