ramekin 0.0.5b

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