picotune 0.0.2
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/lib/picotune.rb +542 -0
- metadata +57 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: df51a0bf3802fd2ddba38cf31f7bfdc939b6d718a5c7c224880a07c9ac8af936
|
4
|
+
data.tar.gz: 6e3ab3ac81fc7b8636dcd99256af65f044eff8ed3372e1574c27df7c644db28e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1b056316506eaba70a5b34180d9ba1ebbc2c12eb21ccee6de8949646dcbed964f6cb903b6a36d6954c9e17f32d072ab02243e6e133443c7232599e572c41ba99
|
7
|
+
data.tar.gz: 4af2a9c00d2950b26385b55687f8ce0ac542f2bc75c0e802bd9e7cfbe332b0313302f9ffc56551ef47f6046d5075a2065347c0257c0a16e86a614010ad9d2769
|
data/lib/picotune.rb
ADDED
@@ -0,0 +1,542 @@
|
|
1
|
+
require 'wavefile'
|
2
|
+
|
3
|
+
class PicoTune
|
4
|
+
SAMPLE_RATE = 11468
|
5
|
+
TONE_CONSTANT = 1.059463
|
6
|
+
FREQUENCIES = {
|
7
|
+
C: 32.70,
|
8
|
+
'C#': 34.65,
|
9
|
+
'Db': 34.65,
|
10
|
+
D: 36.71,
|
11
|
+
'D#': 38.89,
|
12
|
+
'Eb': 38.89,
|
13
|
+
E: 41.20,
|
14
|
+
F: 43.65,
|
15
|
+
'F#': 46.25,
|
16
|
+
'Gb': 46.25,
|
17
|
+
G: 49.0,
|
18
|
+
'G#': 51.91,
|
19
|
+
'Ab': 51.91,
|
20
|
+
A: 55.00,
|
21
|
+
'A#': 58.27,
|
22
|
+
'Bb': 58.27,
|
23
|
+
B: 61.74
|
24
|
+
}
|
25
|
+
|
26
|
+
def initialize filename
|
27
|
+
@filename = filename
|
28
|
+
@assembler = Assembler.new filename
|
29
|
+
@tune = @assembler.assemble
|
30
|
+
end
|
31
|
+
|
32
|
+
def wav
|
33
|
+
@tune.wav
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class PicoTune::Sample
|
38
|
+
attr_reader :left, :right
|
39
|
+
|
40
|
+
def to_a
|
41
|
+
[@left, @right]
|
42
|
+
end
|
43
|
+
|
44
|
+
def initialize left = 0.0, right = 0.0
|
45
|
+
@left, @right = left.to_f, right.to_f
|
46
|
+
end
|
47
|
+
|
48
|
+
def add sample
|
49
|
+
@left += sample.left
|
50
|
+
@right += sample.right
|
51
|
+
|
52
|
+
# "foldback" instead of hard clipping, sounds cool
|
53
|
+
if @left > 1.0
|
54
|
+
@left = 1.0 - (@left - 1.0) # ex: 1.0 - (1.2 - 1.0) => 1.0 - 0.2 => 0.8
|
55
|
+
elsif @left < -1.0
|
56
|
+
@left = -1.0 - (@left + 1.0) # ex: -1.0 - (-1.2 + 1.0) => -1.0 - -0.2 => -0.8
|
57
|
+
end
|
58
|
+
|
59
|
+
if @right > 1.0
|
60
|
+
@right = 1.0 - (@right - 1.0)
|
61
|
+
elsif @right < -1.0
|
62
|
+
@right = -1.0 - (@right + 1.0)
|
63
|
+
end
|
64
|
+
|
65
|
+
self # return self to chain ops EX: sample.add(sample).add(sample) etc
|
66
|
+
end
|
67
|
+
|
68
|
+
def modify_left operator, modifier
|
69
|
+
@left = @left.send operator, modifier
|
70
|
+
end
|
71
|
+
|
72
|
+
def modify_right operator, modifier
|
73
|
+
@right = @right.send operator, modifier
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class PicoTune::WaveSample
|
78
|
+
def initialize tone, samples_per_wave, multiplier = nil
|
79
|
+
@tone = tone
|
80
|
+
@samples_per_wave = samples_per_wave
|
81
|
+
@multiplier = multiplier
|
82
|
+
end
|
83
|
+
|
84
|
+
def sample index
|
85
|
+
value = case @tone
|
86
|
+
when 'square'
|
87
|
+
square index
|
88
|
+
when 'sine'
|
89
|
+
sine index
|
90
|
+
when 'triangle'
|
91
|
+
triangle index
|
92
|
+
when 'noise'
|
93
|
+
noise index
|
94
|
+
when 'saw'
|
95
|
+
saw index
|
96
|
+
else
|
97
|
+
sine index
|
98
|
+
end
|
99
|
+
|
100
|
+
PicoTune::Sample.new value, value
|
101
|
+
end
|
102
|
+
|
103
|
+
def sine index
|
104
|
+
Math.sin(index / (@samples_per_wave / (Math::PI * 2))) * (@multiplier || 0.5)
|
105
|
+
end
|
106
|
+
|
107
|
+
def saw index
|
108
|
+
interval = @samples_per_wave / 2
|
109
|
+
half_interval = interval / 2
|
110
|
+
percent = ((index + half_interval) % interval) / interval.to_f
|
111
|
+
((0.6 * percent) - 0.3) * (@multiplier || 0.5)
|
112
|
+
end
|
113
|
+
|
114
|
+
def square index
|
115
|
+
(index <= @samples_per_wave / 2 ? 1.0 : -1.0) * (@multiplier || 0.25)
|
116
|
+
end
|
117
|
+
|
118
|
+
def noise index
|
119
|
+
value = sine index
|
120
|
+
rand = Random.rand - 0.5
|
121
|
+
value * rand * (@multiplier || 0.5)
|
122
|
+
end
|
123
|
+
|
124
|
+
def triangle index
|
125
|
+
half = @samples_per_wave / 2
|
126
|
+
quarter = @samples_per_wave / 4
|
127
|
+
ramp = 1.0 / quarter
|
128
|
+
m = @multiplier || 0.5
|
129
|
+
|
130
|
+
if index <= half
|
131
|
+
if index <= quarter
|
132
|
+
index * ramp * m
|
133
|
+
else
|
134
|
+
(half - index) * ramp * m
|
135
|
+
end
|
136
|
+
else
|
137
|
+
if index <= half + quarter
|
138
|
+
-((index - half) * ramp) * m
|
139
|
+
else
|
140
|
+
-((@samples_per_wave - index) * ramp) * m
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
class PicoTune::Tune
|
147
|
+
attr_reader :name, :sequence, :phrases
|
148
|
+
|
149
|
+
def initialize name, sequence, phrases
|
150
|
+
@name = name
|
151
|
+
@sequence = sequence
|
152
|
+
@phrases = phrases
|
153
|
+
end
|
154
|
+
|
155
|
+
def buffer
|
156
|
+
@buffer ||= begin
|
157
|
+
tune_buffer_length = @sequence.reduce(0) do |acc, phrase_name|
|
158
|
+
acc + @phrases.find { |p| p.name == phrase_name }.buffer_size
|
159
|
+
end
|
160
|
+
|
161
|
+
offset = 0
|
162
|
+
samples = Array.new(tune_buffer_length) { PicoTune::Sample.new }
|
163
|
+
|
164
|
+
@sequence.each do |phrase_name|
|
165
|
+
phrase = @phrases.find { |p| p.name == phrase_name }
|
166
|
+
|
167
|
+
phrase.buffer.each_with_index do |phrase_sample, index|
|
168
|
+
tune_sample = samples[offset + index] || PicoTune::Sample.new
|
169
|
+
tune_sample.add phrase_sample
|
170
|
+
samples[offset + index] = tune_sample
|
171
|
+
end
|
172
|
+
|
173
|
+
offset += phrase.buffer_size
|
174
|
+
end
|
175
|
+
|
176
|
+
samples
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def wav
|
181
|
+
@wav ||= begin
|
182
|
+
wav_format = WaveFile::Format.new :stereo, :float, PicoTune::SAMPLE_RATE
|
183
|
+
wav_buffer = WaveFile::Buffer.new buffer.map(&:to_a), wav_format
|
184
|
+
name = "#{@name}.wav"
|
185
|
+
|
186
|
+
WaveFile::Writer.new(name, WaveFile::Format.new(:stereo, :pcm_8, PicoTune::SAMPLE_RATE)) do |writer|
|
187
|
+
writer.write(wav_buffer)
|
188
|
+
end
|
189
|
+
|
190
|
+
name
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
class PicoTune::Phrase
|
196
|
+
attr_reader :name, :tempo, :beats, :subbeats, :melodies
|
197
|
+
|
198
|
+
def initialize name, tempo, beats, subbeats, melodies
|
199
|
+
@name = name
|
200
|
+
@tempo = tempo.to_i
|
201
|
+
@beats = beats.to_i
|
202
|
+
@subbeats = subbeats.to_i
|
203
|
+
@melodies = melodies
|
204
|
+
end
|
205
|
+
|
206
|
+
def seconds_per_beat
|
207
|
+
60.0 / @tempo
|
208
|
+
end
|
209
|
+
|
210
|
+
def seconds_per_measure
|
211
|
+
seconds_per_beat * @beats
|
212
|
+
end
|
213
|
+
|
214
|
+
def buffer_size
|
215
|
+
(seconds_per_measure * PicoTune::SAMPLE_RATE).to_i
|
216
|
+
end
|
217
|
+
|
218
|
+
def buffer
|
219
|
+
@buffer ||= begin
|
220
|
+
samples = Array.new(buffer_size) { PicoTune::Sample.new }
|
221
|
+
|
222
|
+
@melodies.each do |melody|
|
223
|
+
temp = Array.new(buffer_size) { PicoTune::Sample.new } if melody.instrument.reverb?
|
224
|
+
sub_buffer_size = buffer_size / (@beats * @subbeats)
|
225
|
+
last_step_number = -1
|
226
|
+
carry_over = 0
|
227
|
+
|
228
|
+
melody.pattern.steps.each_with_index do |note, step_number|
|
229
|
+
unless note == '.'
|
230
|
+
buffer_pointer = step_number * sub_buffer_size
|
231
|
+
local_index = 0
|
232
|
+
wave_index = 0
|
233
|
+
length_offset = (1 - melody.instrument.length_value) * sub_buffer_size
|
234
|
+
|
235
|
+
if step_number == last_step_number + 1
|
236
|
+
local_index = carry_over
|
237
|
+
end
|
238
|
+
|
239
|
+
carry_over = 0
|
240
|
+
|
241
|
+
while local_index + length_offset < sub_buffer_size || !wave_index.zero?
|
242
|
+
current_sample = (temp ? temp : samples)[buffer_pointer + local_index] || PicoTune::Sample.new
|
243
|
+
|
244
|
+
new_sample = melody.instrument.wave wave_index, note
|
245
|
+
|
246
|
+
current_sample.add new_sample
|
247
|
+
|
248
|
+
(temp ? temp : samples)[buffer_pointer + local_index] = current_sample
|
249
|
+
|
250
|
+
wave_index += 1
|
251
|
+
local_index += 1
|
252
|
+
last_step_number = step_number
|
253
|
+
carry_over += 1 if local_index + length_offset >= sub_buffer_size
|
254
|
+
wave_index = 0 if wave_index >= melody.instrument.samples_per_wave(note)
|
255
|
+
end
|
256
|
+
|
257
|
+
if melody.instrument.reverb?
|
258
|
+
i = 0
|
259
|
+
while i < temp.size
|
260
|
+
if i + melody.instrument.reverb_offset < temp.size
|
261
|
+
verb_sample = temp[i + melody.instrument.reverb_offset]
|
262
|
+
verb_sample.modify_left :+, temp[i].left * melody.instrument.decay
|
263
|
+
verb_sample.modify_right :+, temp[i].right * melody.instrument.decay
|
264
|
+
|
265
|
+
temp[i + melody.instrument.reverb_offset] = verb_sample
|
266
|
+
end
|
267
|
+
|
268
|
+
samples[i] = samples[i].add temp[i]
|
269
|
+
|
270
|
+
i += 1
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
samples
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
class PicoTune::Instrument
|
283
|
+
attr_reader :name, :tone, :length, :volume, :pan, :reverb
|
284
|
+
|
285
|
+
def initialize name, tone = 0, length = 'full', volume = 'full', pan = 'center', reverb = 'none'
|
286
|
+
@name = name
|
287
|
+
@tone = tone
|
288
|
+
@length = length
|
289
|
+
@volume = volume
|
290
|
+
@pan = pan
|
291
|
+
@reverb = reverb
|
292
|
+
end
|
293
|
+
|
294
|
+
def length_value
|
295
|
+
case @length
|
296
|
+
when 'none'
|
297
|
+
0.0
|
298
|
+
when 'quarter'
|
299
|
+
0.25
|
300
|
+
when 'half'
|
301
|
+
0.5
|
302
|
+
when 'threequarters'
|
303
|
+
0.75
|
304
|
+
when 'full'
|
305
|
+
1.0
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
def volume_value
|
310
|
+
case @volume
|
311
|
+
when 'none'
|
312
|
+
0.0
|
313
|
+
when 'quarter'
|
314
|
+
0.25
|
315
|
+
when 'half'
|
316
|
+
0.5
|
317
|
+
when 'threequarters'
|
318
|
+
0.75
|
319
|
+
when 'full'
|
320
|
+
1.0
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
def pan_value
|
325
|
+
case @pan
|
326
|
+
when 'left'
|
327
|
+
0
|
328
|
+
when 'centerleft'
|
329
|
+
1
|
330
|
+
when 'center'
|
331
|
+
2
|
332
|
+
when 'centerright'
|
333
|
+
3
|
334
|
+
when 'right'
|
335
|
+
4
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
def delay
|
340
|
+
@reverb == 'none' ? 0.0 : 0.1
|
341
|
+
end
|
342
|
+
|
343
|
+
def decay
|
344
|
+
case @reverb
|
345
|
+
when 'none'
|
346
|
+
0.0
|
347
|
+
when 'some'
|
348
|
+
0.25
|
349
|
+
when 'more'
|
350
|
+
0.5
|
351
|
+
when 'lots'
|
352
|
+
0.75
|
353
|
+
else
|
354
|
+
0.0
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
def reverb_offset
|
359
|
+
(PicoTune::SAMPLE_RATE * delay).floor
|
360
|
+
end
|
361
|
+
|
362
|
+
def reverb?
|
363
|
+
%w(some more lots).include? @reverb
|
364
|
+
end
|
365
|
+
|
366
|
+
def wave wave_index, note
|
367
|
+
frequency = frequency_for_note note
|
368
|
+
samples_per_wave = (PicoTune::SAMPLE_RATE / frequency).ceil
|
369
|
+
sample = PicoTune::WaveSample.new(@tone, samples_per_wave).sample wave_index
|
370
|
+
sample.modify_left :*, volume_value * (1 - pan_value / 4.0)
|
371
|
+
sample.modify_right :*, volume_value * (pan_value / 4.0)
|
372
|
+
sample
|
373
|
+
end
|
374
|
+
|
375
|
+
def samples_per_wave note
|
376
|
+
frequency = frequency_for_note note
|
377
|
+
(PicoTune::SAMPLE_RATE / frequency).ceil end
|
378
|
+
|
379
|
+
def frequency_for_note note
|
380
|
+
parts = note.split ''
|
381
|
+
octave = parts.pop.to_i
|
382
|
+
name = parts.join.to_sym
|
383
|
+
freq = PicoTune::FREQUENCIES[name]
|
384
|
+
|
385
|
+
raise "Bad note: #{name} from #{note}. Valid note names are <C, C# or Db, D, D# or Eb, E, F, F# or Gb, G, G# or Ab, A, A# or Bb, B>" unless freq
|
386
|
+
raise "Bad octave: #{octave} from #{note}. Valid octave number is 1..8" unless (1..8).include?(octave)
|
387
|
+
|
388
|
+
octave_shift = PicoTune::TONE_CONSTANT ** 12
|
389
|
+
(octave - 1).times { freq = freq * octave_shift }
|
390
|
+
|
391
|
+
freq
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
class PicoTune::Melody
|
396
|
+
attr_reader :instrument, :pattern
|
397
|
+
|
398
|
+
def initialize instrument, pattern
|
399
|
+
raise 'nil instrument' if instrument.nil?
|
400
|
+
raise 'nil pattern' if pattern.nil?
|
401
|
+
|
402
|
+
@instrument = instrument
|
403
|
+
@pattern = pattern
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
class PicoTune::Pattern
|
408
|
+
attr_reader :name, :steps
|
409
|
+
|
410
|
+
def initialize name, steps
|
411
|
+
@name = name
|
412
|
+
@steps = steps
|
413
|
+
end
|
414
|
+
end
|
415
|
+
|
416
|
+
class PicoTune::Assembler
|
417
|
+
attr_reader :phrases, :instruments, :patterns
|
418
|
+
|
419
|
+
def initialize file
|
420
|
+
@file = file
|
421
|
+
@parser = PicoTune::Parser.new file
|
422
|
+
end
|
423
|
+
|
424
|
+
def assemble
|
425
|
+
patterns = []
|
426
|
+
phrases = []
|
427
|
+
|
428
|
+
list = @parser.parse
|
429
|
+
|
430
|
+
instruments = list.select { |item| item['type'] == 'instrument' }.map do |item|
|
431
|
+
PicoTune::Instrument.new(
|
432
|
+
item['name'],
|
433
|
+
item['tone'],
|
434
|
+
item['length'],
|
435
|
+
item['volume'],
|
436
|
+
item['pan'],
|
437
|
+
item['reverb']
|
438
|
+
)
|
439
|
+
end
|
440
|
+
|
441
|
+
patterns = list.select { |item| item['type'] == 'pattern' }.map do |item|
|
442
|
+
PicoTune::Pattern.new item['name'], item['list']
|
443
|
+
end
|
444
|
+
|
445
|
+
phrases = list.select { |item| item['type'] == 'phrase' }.map do |item|
|
446
|
+
melodies = item['melodies'].map do |m|
|
447
|
+
instrument = instruments.find { |i| i.name == m[0] }
|
448
|
+
pattern = patterns.find { |p| p.name == m[1] }
|
449
|
+
|
450
|
+
PicoTune::Melody.new instrument, pattern
|
451
|
+
end
|
452
|
+
|
453
|
+
PicoTune::Phrase.new(
|
454
|
+
item['name'],
|
455
|
+
item['tempo'],
|
456
|
+
item['beats'],
|
457
|
+
item['subbeats'],
|
458
|
+
melodies
|
459
|
+
)
|
460
|
+
end
|
461
|
+
|
462
|
+
sequence = list.find { |item| item['type'] == 'sequence' }
|
463
|
+
sequence['list'].each do |phrase_name|
|
464
|
+
raise "undefined phrase \"#{phrase_name}\" in sequence" unless phrases.find { |p| p.name == phrase_name }
|
465
|
+
end
|
466
|
+
|
467
|
+
|
468
|
+
tune = list.find { |item| item['type'] == 'tune' }
|
469
|
+
raise 'undefined tune name' unless tune['name']
|
470
|
+
|
471
|
+
PicoTune::Tune.new tune['name'], sequence['list'], phrases
|
472
|
+
end
|
473
|
+
end
|
474
|
+
|
475
|
+
class PicoTune::Parser
|
476
|
+
def initialize file
|
477
|
+
@lines = File.open(file).readlines.map(&:strip)
|
478
|
+
@keywords = ['tune ', 'sequence', 'instrument ', 'phrase ', 'pattern ']
|
479
|
+
end
|
480
|
+
|
481
|
+
def parse
|
482
|
+
i = 0
|
483
|
+
bag = []
|
484
|
+
collecting_melodies = false
|
485
|
+
|
486
|
+
while i < @lines.length
|
487
|
+
line = @lines[i]
|
488
|
+
|
489
|
+
if line.start_with? *@keywords
|
490
|
+
collecting_melodies = false
|
491
|
+
parts = line.split ' '
|
492
|
+
item = {}
|
493
|
+
|
494
|
+
item['type'] = parts[0]
|
495
|
+
|
496
|
+
if parts[0] == 'sequence'
|
497
|
+
item['list'] = parts[1..-1]
|
498
|
+
elsif parts[0] == 'pattern'
|
499
|
+
item['name'] = parts[1]
|
500
|
+
item['list'] = pattern_steps(parts[2])
|
501
|
+
else
|
502
|
+
item['name'] = parts[1]
|
503
|
+
end
|
504
|
+
|
505
|
+
bag << item
|
506
|
+
elsif line.length > 0
|
507
|
+
parts = line.split ' '
|
508
|
+
|
509
|
+
if bag.last['type'] == 'phrase' && parts[0] == 'melodies'
|
510
|
+
collecting_melodies = true
|
511
|
+
bag.last['melodies'] = []
|
512
|
+
elsif collecting_melodies
|
513
|
+
bag.last['melodies'].push parts
|
514
|
+
else
|
515
|
+
bag.last[parts[0]] = parts[1]
|
516
|
+
end
|
517
|
+
end
|
518
|
+
|
519
|
+
i += 1
|
520
|
+
end
|
521
|
+
|
522
|
+
bag
|
523
|
+
end
|
524
|
+
|
525
|
+
def pattern_steps pattern
|
526
|
+
p = pattern.split(/([a-zA-Z][#b]?\d)/, -1)
|
527
|
+
.map { |b| b.split(/(\.|-)/, -1) }
|
528
|
+
.flatten
|
529
|
+
.delete_if { |b| b.length.zero? }
|
530
|
+
|
531
|
+
i = 0
|
532
|
+
while i < p.length
|
533
|
+
if p[i] == '-'
|
534
|
+
p[i] = p[i - 1]
|
535
|
+
end
|
536
|
+
|
537
|
+
i += 1
|
538
|
+
end
|
539
|
+
|
540
|
+
p
|
541
|
+
end
|
542
|
+
end
|
metadata
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: picotune
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Zachary Schroeder
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-10-13 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: wavefile
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.1.1
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.1.1
|
27
|
+
description: Use a text file with a simple DSL to generate a musical (maybe) wav file.
|
28
|
+
email: schroza@gmail.com
|
29
|
+
executables: []
|
30
|
+
extensions: []
|
31
|
+
extra_rdoc_files: []
|
32
|
+
files:
|
33
|
+
- lib/picotune.rb
|
34
|
+
homepage: https://rubygems.org/gems/picotune
|
35
|
+
licenses:
|
36
|
+
- MIT
|
37
|
+
metadata: {}
|
38
|
+
post_install_message:
|
39
|
+
rdoc_options: []
|
40
|
+
require_paths:
|
41
|
+
- lib
|
42
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: '0'
|
52
|
+
requirements: []
|
53
|
+
rubygems_version: 3.2.3
|
54
|
+
signing_key:
|
55
|
+
specification_version: 4
|
56
|
+
summary: Text file -> wav file. Make tiny tunes!
|
57
|
+
test_files: []
|