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