ramekin 0.0.5b

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