ramekin 0.0.5b
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -0,0 +1,288 @@
|
|
1
|
+
module Ramekin
|
2
|
+
class Renderer
|
3
|
+
include Error::Helpers
|
4
|
+
|
5
|
+
def self.render(fname, track, &b)
|
6
|
+
new(fname, track).render(&b)
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(filename, track)
|
10
|
+
@filename = File.basename(filename)
|
11
|
+
@track = track
|
12
|
+
|
13
|
+
@channel = 0
|
14
|
+
@tick = 0
|
15
|
+
@octave = 4
|
16
|
+
@volume = 0xFF
|
17
|
+
|
18
|
+
# TODO: add @smwxxx special instruments and automatically
|
19
|
+
# optimize out unused sfx samples
|
20
|
+
@instrument_index = {}
|
21
|
+
|
22
|
+
(0...30).each do |i|
|
23
|
+
@instrument_index[i.to_s] = i
|
24
|
+
end
|
25
|
+
|
26
|
+
@instrument_index['smwflute'] = 0
|
27
|
+
@instrument_index['smwstring'] = 1
|
28
|
+
@instrument_index['smwglock'] = 2
|
29
|
+
@instrument_index['smwmarimba'] = 3
|
30
|
+
@instrument_index['smwcello'] = 4
|
31
|
+
@instrument_index['smwsteelguitar'] = 5
|
32
|
+
@instrument_index['smwtrumpet'] = 6
|
33
|
+
@instrument_index['smwsteeldrum'] = 7
|
34
|
+
@instrument_index['smwacousticbass'] = 8
|
35
|
+
@instrument_index['smwpiano'] = 9
|
36
|
+
@instrument_index['smwsnare'] = 10
|
37
|
+
@instrument_index['smwstring2'] = 11
|
38
|
+
@instrument_index['smwbongo'] = 12
|
39
|
+
@instrument_index['smwep'] = 13
|
40
|
+
@instrument_index['smwslapbass'] = 14
|
41
|
+
@instrument_index['smworchhit'] = 15
|
42
|
+
@instrument_index['smwharp'] = 16
|
43
|
+
@instrument_index['smwdistguitar'] = 17
|
44
|
+
@instrument_index['smwkick'] = 21
|
45
|
+
@instrument_index['smwhat'] = 22
|
46
|
+
@instrument_index['smwshaker'] = 23
|
47
|
+
@instrument_index['smwwoodblock'] = 24
|
48
|
+
@instrument_index['smwhiwoodblock'] = 25
|
49
|
+
@instrument_index['smwdrums'] = 28
|
50
|
+
@instrument_index['smwpower'] = 29
|
51
|
+
|
52
|
+
# optional default octaves for instrument switching
|
53
|
+
@default_octaves = Hash.new(4)
|
54
|
+
end
|
55
|
+
|
56
|
+
def render(&b)
|
57
|
+
return render_string unless block_given?
|
58
|
+
|
59
|
+
render_preamble(&b)
|
60
|
+
|
61
|
+
@track.channels.each { |c| render_channel(c, &b) }
|
62
|
+
end
|
63
|
+
|
64
|
+
def render_preamble(&b)
|
65
|
+
m = @track.meta
|
66
|
+
yield "#SPC\n{\n"
|
67
|
+
yield " #title #{m.title.value.inspect}\n" if m.title
|
68
|
+
yield " #author #{m.author.value.inspect}\n" if m.author
|
69
|
+
yield " #game #{m.game.value.inspect}\n" if m.game
|
70
|
+
yield " #comment #{m.comment.value.inspect}\n" if m.comment
|
71
|
+
yield "}\n\n"
|
72
|
+
|
73
|
+
yield "; generated from #{@filename} by Ramekin\n"
|
74
|
+
yield "; https://codeberg.org/jneen/ramekin\n\n"
|
75
|
+
|
76
|
+
# TODO
|
77
|
+
yield "#amk 2\n\n"
|
78
|
+
|
79
|
+
if m.instruments.any?
|
80
|
+
yield "#path #{@filename.chomp('.rmk').inspect}\n"
|
81
|
+
yield "#samples {\n"
|
82
|
+
m.sample_groups.each do |group|
|
83
|
+
yield " ##{group.value}\n"
|
84
|
+
end
|
85
|
+
|
86
|
+
m.instruments.map(&:sample_name).sort.uniq.each do |sample|
|
87
|
+
yield " #{sample.inspect}\n"
|
88
|
+
end
|
89
|
+
yield "}\n\n"
|
90
|
+
|
91
|
+
yield "#instruments {\n"
|
92
|
+
m.instruments.each_with_index do |inst, i|
|
93
|
+
@instrument_index[inst.name.value] = 30 + i
|
94
|
+
@default_octaves[inst.name.value] = inst.octave
|
95
|
+
yield " #{inst.to_amk}\n"
|
96
|
+
end
|
97
|
+
yield "}\n\n"
|
98
|
+
end
|
99
|
+
|
100
|
+
yield "t#{tempo_of(m.tempo)} ; main tempo (0-60)\n" if m.tempo
|
101
|
+
yield "w#{m.volume.value} ; main volume (0-255)\n" if m.volume
|
102
|
+
yield "l16"
|
103
|
+
# binding.pry
|
104
|
+
end
|
105
|
+
|
106
|
+
def tempo_of(el)
|
107
|
+
case el.type
|
108
|
+
when :t then el.value
|
109
|
+
when :bpm then (el.value.to_i * 256 / 625.0).round
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def next!
|
114
|
+
if @peek
|
115
|
+
return @peek.tap { @peek = nil }
|
116
|
+
end
|
117
|
+
|
118
|
+
@current_channel.next
|
119
|
+
end
|
120
|
+
|
121
|
+
def peek
|
122
|
+
@peek ||= @current_channel.next
|
123
|
+
end
|
124
|
+
|
125
|
+
def render_channel(channel, &b)
|
126
|
+
@tick = 0
|
127
|
+
yield "\n\n"
|
128
|
+
@current_channel = channel.each
|
129
|
+
loop do
|
130
|
+
@current = el = next!
|
131
|
+
|
132
|
+
case el
|
133
|
+
when NoteEvent
|
134
|
+
old_tick = @tick
|
135
|
+
@tick += el.ticks
|
136
|
+
if (old_tick-1) / 192 != (@tick-1) / 192
|
137
|
+
yield "\n"
|
138
|
+
end
|
139
|
+
yield el.to_amk(@octave)
|
140
|
+
@octave = el.octave unless el.rest?
|
141
|
+
when MacroDefinition
|
142
|
+
# pass
|
143
|
+
when LegatoStart, LegatoLastNote
|
144
|
+
yield el.to_amk
|
145
|
+
when Instrument
|
146
|
+
yield "@#{@instrument_index[el.name.value]}"
|
147
|
+
when Token
|
148
|
+
render_token(el, &b)
|
149
|
+
else
|
150
|
+
error! "unexpected element type #{el.inspect}"
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def render_token(token, &b)
|
156
|
+
case token.type
|
157
|
+
when :t, :bpm
|
158
|
+
yield "t#{tempo_of(token)}"
|
159
|
+
when :directive
|
160
|
+
render_directive(token, &b)
|
161
|
+
when :channel
|
162
|
+
yield "##{token.value}\n\n"
|
163
|
+
when :hex
|
164
|
+
yield "$#{token.value.downcase} "
|
165
|
+
when :slash
|
166
|
+
yield "\n/\n"
|
167
|
+
# reset tick counter for newlines
|
168
|
+
@tick = 0
|
169
|
+
when :transpose
|
170
|
+
interval = token.value.to_i
|
171
|
+
|
172
|
+
if interval < 0
|
173
|
+
interval = 0x80 - interval
|
174
|
+
end
|
175
|
+
|
176
|
+
yield sprintf("$fa$02$%02x", token.value.to_i)
|
177
|
+
when :instrument
|
178
|
+
case token.value
|
179
|
+
when /\A\d+\z/ then yield "@#{token.value}"
|
180
|
+
else
|
181
|
+
@octave = nil
|
182
|
+
unless @instrument_index.key?(token.value)
|
183
|
+
error! "undeclared instrument @#{token.value}"
|
184
|
+
end
|
185
|
+
|
186
|
+
yield "@#{@instrument_index[token.value]}"
|
187
|
+
end
|
188
|
+
when :v
|
189
|
+
yield "v#{token.values.compact.join(',')}"
|
190
|
+
@volume = token.values[0].to_i
|
191
|
+
when :relv
|
192
|
+
relvol, duration = token.values
|
193
|
+
yield "v#{[@volume + relvol.to_i, duration].compact.join(',')}"
|
194
|
+
when :adsr
|
195
|
+
vals = token.value.split(',').map { |x| x.to_i(16) }
|
196
|
+
error! 'invalid #adsr, expected 4 arguments' unless vals.size == 4
|
197
|
+
|
198
|
+
a, d, s, r = vals
|
199
|
+
error! 'invalid attack (must be 0-F)' unless (0..15).include?(a)
|
200
|
+
error! 'invalid decay (must be 0-7)' unless (0..7).include?(d)
|
201
|
+
error! 'invalid sustain (must be 0-7)' unless (0..7).include?(s)
|
202
|
+
error! 'invalid release (must be 0-1F)' unless (0..31).include?(r)
|
203
|
+
|
204
|
+
yield sprintf("$ed$%02x$%02x", (7-d) * 16 + (15-a), s * 32 + (31-r))
|
205
|
+
when :y
|
206
|
+
yield "y#{token.value}"
|
207
|
+
when :rely
|
208
|
+
token.value =~ /\A([LRC])(\d*)\z/
|
209
|
+
pan = case $1
|
210
|
+
when 'L' then 10 + $2.to_i
|
211
|
+
when 'R' then 10 - $2.to_i
|
212
|
+
when 'C' then 10
|
213
|
+
end
|
214
|
+
|
215
|
+
yield "y#{pan}"
|
216
|
+
|
217
|
+
when :p
|
218
|
+
# it's free aram
|
219
|
+
if token.value == '0,0'
|
220
|
+
yield '$df'
|
221
|
+
else
|
222
|
+
yield "p#{token.value} "
|
223
|
+
end
|
224
|
+
when :superloop
|
225
|
+
yield " [[ "
|
226
|
+
when :loop
|
227
|
+
if token.value
|
228
|
+
yield " (#{token.value})[ "
|
229
|
+
else
|
230
|
+
yield " [ "
|
231
|
+
end
|
232
|
+
when :loop_end
|
233
|
+
yield " ]#{token.value} "
|
234
|
+
when :loop_call
|
235
|
+
yield " (#{token.values[0]})#{token.values[1]} "
|
236
|
+
when :star
|
237
|
+
yield "*#{token.value}"
|
238
|
+
when :superloop_end
|
239
|
+
yield " ]]#{token.value} "
|
240
|
+
when :amp
|
241
|
+
yield '&'
|
242
|
+
|
243
|
+
# triplets are handled in NoteAggregator
|
244
|
+
when :lbrace, :rbrace
|
245
|
+
# pass
|
246
|
+
when :rbrace
|
247
|
+
yield '}'
|
248
|
+
|
249
|
+
else
|
250
|
+
error! "unexpected token type: #{token.type}"
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
def render_directive(token)
|
255
|
+
case token.value
|
256
|
+
when 'SPC'
|
257
|
+
# pass
|
258
|
+
when 'bend'
|
259
|
+
note = next!
|
260
|
+
|
261
|
+
error! '#bend must be followed by a note' unless NoteEvent === note
|
262
|
+
error! 'cannot #bend to a rest' if note.rest?
|
263
|
+
|
264
|
+
ticks = sprintf("%02x", note.ticks)
|
265
|
+
yield "$dd$#{ticks}$#{ticks}#{note.octave_amk}#{note.note.value}"
|
266
|
+
|
267
|
+
case peek
|
268
|
+
when NoteEvent
|
269
|
+
peek.extensions.concat(note.extensions)
|
270
|
+
else
|
271
|
+
yield "^=#{note.ticks}"
|
272
|
+
end
|
273
|
+
|
274
|
+
@octave = note.octave
|
275
|
+
when 'legato'
|
276
|
+
yield '$f4$01'
|
277
|
+
else
|
278
|
+
error! "unexpected directive ##{token.value}"
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
def render_string
|
283
|
+
out = StringIO.new
|
284
|
+
render { |chunk| out << chunk }
|
285
|
+
out.string
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
@@ -0,0 +1,296 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'open-uri'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module Ramekin
|
6
|
+
class APIEndpoint
|
7
|
+
BASE = 'https://www.smwcentral.net/ajax.php'
|
8
|
+
|
9
|
+
def initialize(**params)
|
10
|
+
@params = {}
|
11
|
+
params.each { |k, v| self[k] = v }
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.get!(**params)
|
15
|
+
resp = URI.open(new(**params).to_uri)
|
16
|
+
binding.pry unless resp.status == %w(200 OK)
|
17
|
+
JSON.parse(resp.read)
|
18
|
+
end
|
19
|
+
|
20
|
+
def []=(k, v)
|
21
|
+
case v
|
22
|
+
when Hash
|
23
|
+
v.each do |subk, subv|
|
24
|
+
self["#{k}[#{subk}]"] = subv
|
25
|
+
end
|
26
|
+
when Array
|
27
|
+
v.each_with_index do |subv, i|
|
28
|
+
self["#{k}[#{i}]"] = subv
|
29
|
+
end
|
30
|
+
when String, Numeric
|
31
|
+
@params[k] = v
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_uri
|
36
|
+
URI(BASE).tap do |uri|
|
37
|
+
uri.query = URI.encode_www_form(@params)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_s
|
42
|
+
to_uri.to_s
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
REPO_DIR = File.expand_path('~/.ramekin')
|
47
|
+
FileUtils.mkdir_p(REPO_DIR)
|
48
|
+
|
49
|
+
class SamplePack
|
50
|
+
def self.smwc_each(&b)
|
51
|
+
params = { a: 'getsectionlist', s: 'brrsamples' }
|
52
|
+
page = APIEndpoint.get!(**params)
|
53
|
+
page['data'].each { |e| yield new(e) }
|
54
|
+
|
55
|
+
(2..page['last_page']).each do |n|
|
56
|
+
page = APIEndpoint.get!(n: n, **params)
|
57
|
+
page['data'].each { |e| yield new(e) }
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.find(name)
|
62
|
+
each do |pack|
|
63
|
+
return pack if pack.name == name
|
64
|
+
end
|
65
|
+
|
66
|
+
return nil
|
67
|
+
end
|
68
|
+
|
69
|
+
extend Enumerable
|
70
|
+
def self.each(&b)
|
71
|
+
return enum_for(:each) unless block_given?
|
72
|
+
|
73
|
+
Dir.chdir Ramekin.config.packages_dir do
|
74
|
+
Dir.entries('.').sort.each do |subd|
|
75
|
+
next unless /\A\d+\z/ =~ subd
|
76
|
+
Dir.chdir(subd) do
|
77
|
+
yield new(JSON.load_file(".meta.json"))
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def initialize(data)
|
84
|
+
@meta = data
|
85
|
+
end
|
86
|
+
|
87
|
+
def name
|
88
|
+
@meta['name'] or raise 'no name'
|
89
|
+
end
|
90
|
+
|
91
|
+
def id
|
92
|
+
@meta['id'].to_i or raise 'no id'
|
93
|
+
end
|
94
|
+
|
95
|
+
def url
|
96
|
+
"https://www.smwcentral.net/?p=section&a=details&id=#{id}"
|
97
|
+
end
|
98
|
+
|
99
|
+
def authors
|
100
|
+
(@meta['authors'] || []).map { |a| a && a['name'] }.compact
|
101
|
+
end
|
102
|
+
|
103
|
+
def find(path)
|
104
|
+
path = path.chomp('.brr')
|
105
|
+
File.join(prefix_dir, "#{path}.brr")
|
106
|
+
end
|
107
|
+
|
108
|
+
def tunings_for(path)
|
109
|
+
rel = Pathname.new(find(path)).relative_path_from(dir).to_s
|
110
|
+
(tunings[rel] || []).map(&:last)
|
111
|
+
end
|
112
|
+
|
113
|
+
def download_url
|
114
|
+
@meta['download_url'] or raise 'no download_url'
|
115
|
+
end
|
116
|
+
|
117
|
+
def shortest_prefix(paths)
|
118
|
+
paths = paths.map { |x| x.split('/') }.sort_by(&:size)
|
119
|
+
|
120
|
+
prefix = []
|
121
|
+
loop do
|
122
|
+
break if paths[0].empty?
|
123
|
+
init = paths.map(&:shift).uniq
|
124
|
+
break if init.size > 1
|
125
|
+
prefix << init[0]
|
126
|
+
end
|
127
|
+
|
128
|
+
prefix.join('/')
|
129
|
+
end
|
130
|
+
|
131
|
+
TUNING_RE = %r(
|
132
|
+
"(.*?[.]brr)" \s*
|
133
|
+
[$](\h\h) \s*
|
134
|
+
[$](\h\h) \s*
|
135
|
+
[$](\h\h) \s*
|
136
|
+
[$](\h\h) \s*
|
137
|
+
[$](\h\h)
|
138
|
+
)x
|
139
|
+
|
140
|
+
def find_tunings(name_re)
|
141
|
+
raw = []
|
142
|
+
Dir.glob('**/*.txt').sort.each do |entry|
|
143
|
+
next unless File.basename(entry).downcase =~ name_re
|
144
|
+
|
145
|
+
contents = File.read(entry, encoding: 'binary')
|
146
|
+
|
147
|
+
contents.gsub!(/;.*$/, '')
|
148
|
+
# windows paths...
|
149
|
+
contents.gsub!(/\\/, '/')
|
150
|
+
|
151
|
+
# contents.scan /#path\s*"(.*?)"/ do |(path)|
|
152
|
+
# paths << path
|
153
|
+
# end
|
154
|
+
|
155
|
+
contents.scan TUNING_RE do |(path, *hexes)|
|
156
|
+
raw << [entry, path, hexes]
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
out = Hash.new {|h,k| h[k] = []}
|
161
|
+
raw.each do |tuning_path, name, hexes|
|
162
|
+
out[File.basename(name).downcase] << [tuning_path, name, hexes]
|
163
|
+
end
|
164
|
+
|
165
|
+
out
|
166
|
+
end
|
167
|
+
|
168
|
+
def index_packs
|
169
|
+
paths = []
|
170
|
+
raw_tunings = []
|
171
|
+
|
172
|
+
patterns_tunings = find_tunings(/\A!patterns[.]txt\z/)
|
173
|
+
other_tunings = find_tunings(/\Atuning/)
|
174
|
+
misc_tunings = find_tunings(//)
|
175
|
+
|
176
|
+
@brrs = Dir.glob('**/*.brr')
|
177
|
+
@brrs.sort!
|
178
|
+
@brrs.uniq!
|
179
|
+
@prefix = shortest_prefix(@brrs.map(&File.method(:dirname)))
|
180
|
+
|
181
|
+
@tunings = {}
|
182
|
+
@brrs.each do |brr|
|
183
|
+
base = File.basename(brr).downcase
|
184
|
+
next if base == 'empty.brr'
|
185
|
+
|
186
|
+
candidates = patterns_tunings[base]
|
187
|
+
candidates = other_tunings[base] if candidates.empty?
|
188
|
+
candidates = misc_tunings[base] if candidates.empty?
|
189
|
+
|
190
|
+
candidates = candidates.uniq { |_,_,h| h }
|
191
|
+
|
192
|
+
# if there is more than one candidate, prioritize the one that's closest
|
193
|
+
if candidates.size >= 2
|
194
|
+
candidates.sort_by! { |path, _, _| -shortest_prefix([brr, path]).size }
|
195
|
+
end
|
196
|
+
|
197
|
+
@tunings[brr] = candidates.map { |p, _, h| [p, h] }
|
198
|
+
end
|
199
|
+
|
200
|
+
File.write('.index.json', JSON.dump(
|
201
|
+
prefix: @prefix,
|
202
|
+
brrs: @brrs,
|
203
|
+
tunings: @tunings,
|
204
|
+
))
|
205
|
+
|
206
|
+
@cached_index = nil
|
207
|
+
end
|
208
|
+
|
209
|
+
def cached_index
|
210
|
+
@cached_index ||= JSON.load_file("#{dir}/.index.json")
|
211
|
+
end
|
212
|
+
|
213
|
+
def prefix
|
214
|
+
cached_index['prefix']
|
215
|
+
end
|
216
|
+
|
217
|
+
def unprefix(path)
|
218
|
+
Pathname.new(dir).join(path).relative_path_from(prefix_dir).to_s
|
219
|
+
end
|
220
|
+
|
221
|
+
def unprefixed_brrs
|
222
|
+
brrs.map(&method(:unprefix))
|
223
|
+
end
|
224
|
+
|
225
|
+
def brrs
|
226
|
+
cached_index['brrs']
|
227
|
+
end
|
228
|
+
|
229
|
+
def tunings
|
230
|
+
cached_index['tunings']
|
231
|
+
end
|
232
|
+
|
233
|
+
def prefix_dir
|
234
|
+
File.join(dir, prefix)
|
235
|
+
end
|
236
|
+
|
237
|
+
def self.download_all!
|
238
|
+
smwc_each(&:download)
|
239
|
+
each(&:index_packs)
|
240
|
+
end
|
241
|
+
|
242
|
+
def needs_download?
|
243
|
+
return true unless exists?
|
244
|
+
@meta['time'] > cached_json['time']
|
245
|
+
end
|
246
|
+
|
247
|
+
def cached_json
|
248
|
+
@cached_json ||= JSON.load_file(meta_file)
|
249
|
+
rescue Errno::ENOENT
|
250
|
+
nil
|
251
|
+
end
|
252
|
+
|
253
|
+
def exists?
|
254
|
+
File.exist?(meta_file) && !!cached_json && !!cached_json['id']
|
255
|
+
end
|
256
|
+
|
257
|
+
def meta_file
|
258
|
+
"#{dir}/.meta.json"
|
259
|
+
end
|
260
|
+
|
261
|
+
def dir
|
262
|
+
"#{Ramekin.config.packages_dir}/#{id}"
|
263
|
+
end
|
264
|
+
|
265
|
+
def download
|
266
|
+
unless needs_download?
|
267
|
+
$stderr.puts "skipping: #{name}:#{id}"
|
268
|
+
return
|
269
|
+
end
|
270
|
+
|
271
|
+
download!
|
272
|
+
end
|
273
|
+
|
274
|
+
def download!
|
275
|
+
$stderr.puts "downloading to #{dir}"
|
276
|
+
FileUtils.rm_rf(dir, secure: true)
|
277
|
+
FileUtils.mkdir_p(dir)
|
278
|
+
|
279
|
+
Dir.chdir(dir) do
|
280
|
+
zip = URI.open(download_url).read
|
281
|
+
Zip::File.open_buffer(zip).each do |entry|
|
282
|
+
# look of disapproval
|
283
|
+
next if entry.directory?
|
284
|
+
next if entry.name =~ /__MACOSX/
|
285
|
+
next unless File.expand_path(name).start_with?(File.expand_path('.'))
|
286
|
+
|
287
|
+
$stderr.puts " -> #{entry.name}"
|
288
|
+
FileUtils.mkdir_p(File.dirname(entry.name))
|
289
|
+
entry.extract(entry.name)
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
File.write(meta_file, JSON.dump(@meta))
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
module Ramekin
|
2
|
+
module SPCPlayer
|
3
|
+
def self.make_instance
|
4
|
+
config = Ramekin.config.spc_player
|
5
|
+
unless config == '__auto__'
|
6
|
+
return CustomSPCPlayer.new(File.expand_path(config, HOME))
|
7
|
+
end
|
8
|
+
|
9
|
+
return WindowsSPCPlayer.new if Ramekin.config.windows?
|
10
|
+
NormalSPCPlayer.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.instance(*a)
|
14
|
+
@instance ||= make_instance
|
15
|
+
end
|
16
|
+
|
17
|
+
class SPCPlayer
|
18
|
+
include Util
|
19
|
+
|
20
|
+
def setup_ok?
|
21
|
+
true
|
22
|
+
end
|
23
|
+
|
24
|
+
def setup!
|
25
|
+
# pass
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
class WindowsSPCPlayer < SPCPlayer
|
31
|
+
def initialize
|
32
|
+
@spcplay_dir = "#{HOME}/spcplay"
|
33
|
+
@smwc_url = "https://dl.smwcentral.net/35673/spcplay-2.20.1.8272.zip"
|
34
|
+
end
|
35
|
+
|
36
|
+
def setup_ok?
|
37
|
+
File.exist?("#@spcplay_dir/spcplay.exe")
|
38
|
+
end
|
39
|
+
|
40
|
+
def play(fname)
|
41
|
+
fname = File.expand_path(fname)
|
42
|
+
Dir.chdir(@spcplay_dir) do
|
43
|
+
sys "spcplay.exe", fname
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def setup!
|
48
|
+
return if setup_ok?
|
49
|
+
|
50
|
+
FileUtils.mkdir_p(@spcplay_dir)
|
51
|
+
|
52
|
+
Dir.chdir @spcplay_dir do
|
53
|
+
$stderr.puts "extracting spcplay from #@smwc_url"
|
54
|
+
$stderr.puts "into #$spcplay_dir"
|
55
|
+
zip = URI.open(@smwc_url).read
|
56
|
+
unpack_zip_here(zip) do |entry|
|
57
|
+
$stderr.puts " -> #{entry}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class NormalSPCPlayer < SPCPlayer
|
64
|
+
def play(fname)
|
65
|
+
sys "#@spct_dir/spct", 'play', fname
|
66
|
+
end
|
67
|
+
|
68
|
+
def render(fname, outfile, seconds=nil)
|
69
|
+
args = ["#@spct_dir/spct", 'render', fname, outfile]
|
70
|
+
args.concat(['-s', seconds.to_s]) if seconds
|
71
|
+
sys(*args)
|
72
|
+
end
|
73
|
+
|
74
|
+
def initialize
|
75
|
+
@spct_dir = "#{HOME}/spct"
|
76
|
+
end
|
77
|
+
|
78
|
+
def setup_ok?
|
79
|
+
executable?("#@spct_dir/spct")
|
80
|
+
end
|
81
|
+
|
82
|
+
def setup!
|
83
|
+
return if setup_ok?
|
84
|
+
|
85
|
+
clear_dir(@spct_dir, remake: false)
|
86
|
+
|
87
|
+
Dir.chdir(HOME) do
|
88
|
+
sys git, 'clone', '--recursive', 'https://codeberg.org/jneen/spct'
|
89
|
+
end
|
90
|
+
|
91
|
+
Dir.chdir(@spct_dir) do
|
92
|
+
sys(make)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class CustomSPCPlayer < SPCPlayer
|
98
|
+
def initialize(path)
|
99
|
+
@path = Ramekin.config.expand(path)
|
100
|
+
end
|
101
|
+
|
102
|
+
def render(fname, outfile, seconds=nil)
|
103
|
+
unless File.basename(@path, '.exe') == 'spct'
|
104
|
+
$stderr.puts "WAV rendering is only supported for spct."
|
105
|
+
exit 1
|
106
|
+
end
|
107
|
+
|
108
|
+
args = [@path, 'render', fname, outfile]
|
109
|
+
args.concat(['-s', seconds.to_s]) if seconds
|
110
|
+
sys(*args)
|
111
|
+
end
|
112
|
+
|
113
|
+
def play(fname)
|
114
|
+
if File.basename(@path, '.exe') == 'spct'
|
115
|
+
sys @path, 'play', fname
|
116
|
+
else
|
117
|
+
sys @path, fname
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|