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