xt(t)));var o=wt(it.main.frequency),s=wt(it.main.velocity),a=wt(e),l=vt.now()+.15;vt.play(i,o,s,l,l+a),Ct(i,o,s,a)}}function yt(){var t=[{type:it.carrier.type,freq:[],gain:[{a:it.carrier.attack,h:it.carrier.hold,d:it.carrier.decay,s:it.carrier.sustain,r:it.carrier.release}]}];if(it.distort1.folder.expanded){var e=it.distort1.type,n=it.distort1.argument;t[0].effect=`${e}-${n}`}if(it.mods1.folder.expanded&&t[0].freq.push({w:it.mods1.delay,t:it.mods1.jumpmult,f:it.mods1.jumpadd,a:0,p:it.mods1.sweep,q:it.mods1.sweeptime,x:it.mods1.repeat}),it.mods2.folder.expanded&&t[0].freq.push({w:it.mods2.delay,t:it.mods2.jumpmult,f:it.mods2.jumpadd,a:0,p:it.mods2.sweep,q:it.mods2.sweeptime,x:it.mods2.repeat}),it.tremolo.folder.expanded&&t[0].freq.push({type:it.tremolo.type,freq:it.tremolo.frequency,gain:{t:it.tremolo.depth}}),it.vibrato.folder.expanded&&t[0].gain.push({type:it.vibrato.type,freq:it.vibrato.frequency,gain:{t:it.vibrato.depth,r:it.carrier.release,z:0}}),it.FM.folder.expanded&&t[0].freq.push({type:it.FM.type,freq:{t:it.FM.freqmult,f:it.FM.freqadd},gain:{t:it.FM.gainmult,a:it.FM.attack,d:it.FM.decay,s:it.FM.sustain,r:it.FM.release}}),it.distort2.folder.expanded){var r=it.distort2.type,i=it.distort2.argument;t.push(`${r}-${i}`)}return it.effect1.folder.expanded&&t.push({type:it.effect1.type,freq:{t:it.effect1.freqmult,p:it.effect1.sweep,q:it.effect1.sweeptime},Q:it.effect1.q}),it.effect2.folder.expanded&&t.push({type:it.effect2.type,freq:{t:it.effect2.freqmult,p:it.effect2.sweep,q:it.effect2.sweeptime},Q:it.effect2.q}),t}function wt(t){var e=t<0?-1:1;return(t=Math.abs(t))<.002?0:t>20?e*Math.round(t):t>2?e*Math.round(10*t)/10:t>.2?e*Math.round(100*t)/100:e*Math.round(1e3*t)/1e3}function xt(t){Object.keys(t).forEach((e=>{Array.isArray(t[e])&&1===t[e].length&&(t[e]=t[e][0]),"number"==typeof t[e]&&(t[e]=wt(t[e])),"object"==typeof t[e]&&(0===Object.keys(t[e]).length?delete t[e]:xt(t[e]))}))}var jt=Y(".code");function Ct(t,e,n,r){var i=t.map((t=>function(t){Et(t);var e=JSON.stringify(t);return(e=(e=e.replace(/"([^"]*)":/g,"$1:")).replace(/,gain/g,", gain")).replace(/,freq/g,", freq")}(t)));jt.value="",jt.value+="// import Gen from 'wasgen'\n",jt.value+="// var gen = new Gen()\n",jt.value+="gen.play([\n",i.forEach((t=>{jt.value+=` ${t},\n`})),jt.value+=`], ${e}, ${n}, gen.now(), gen.now() + ${r})\n`}function Et(t){for(var e in t)"object"==typeof t[e]&&Et(t[e]),"number"==typeof t[e]&&(t[e]=Math.round(1e3*t[e])/1e3)}var Pt,kt=Y(".render");kt.onclick=async()=>{if(OfflineAudioContext){var t=it.carrier.duration+it.carrier.attack+it.carrier.hold,e=t+5*it.carrier.release+.01,r=new OfflineAudioContext(2,44100*e|0,44100),i=new X(r,r.destination,!1,!0),o=yt();o.forEach((t=>xt(t)));var s=wt(it.main.frequency),a=wt(it.main.velocity),l=wt(t),u=5/44100;Ct(o,s,a,l),/crush/.test(JSON.stringify(o))&&await(100,new Promise((t=>setTimeout(t,100)))),i.play(o,s,a,u,u+l);var c=await r.startRendering();Pt||(Pt=document.createElement("a"),document.body.appendChild(Pt),Pt.style.display="none");var p=n(562)(c),d=new window.Blob([new DataView(p)],{type:"audio/wav"}),m=window.URL.createObjectURL(d);Pt.href=m,Pt.download="audio.wav",Pt.click(),window.URL.revokeObjectURL(m),i.dispose()}else kt.textContent="Not supported in your browser, sorry!"}})()})();
--------------------------------------------------------------------------------
/docs/demo.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | var $ = document.querySelector.bind(document)
4 | var Tweakpane = require('tweakpane')
5 | import './presets'
6 |
7 | import packageData from '../package.json'
8 | var version = packageData.version
9 | $('#ver').innerHTML = `v${version}`
10 |
11 | // @ts-ignore - ??
12 | export var pane1 = new Tweakpane({ container: $('.settings1') })
13 | // @ts-ignore - ??
14 | export var pane2 = new Tweakpane({ container: $('.settings2') })
15 | export var params = {}
16 |
17 | export var demo = {
18 | beginEditingParams,
19 | finishEditingParams,
20 | }
21 |
22 |
23 | /*
24 | *
25 | * constants
26 | *
27 | */
28 |
29 | var types = {
30 | sine: 'sine',
31 | triangle: 'triangle',
32 | square: 'square',
33 | sawtooth: 'sawtooth',
34 | 'pulse (10%)': 'p10',
35 | 'pulse (25%)': 'p25',
36 | 'pulse (40%)': 'p40',
37 | 'harmonics(1, 0, 1)': 'w909',
38 | 'harmonics(1, 1, 1)': 'w999',
39 | 'harmonics(1, 0.6, 0.3)': 'w963',
40 | 'white noise': 'n0',
41 | 'pink noise': 'np',
42 | 'brown noise': 'nb',
43 | 'metallic noise': 'n1',
44 | }
45 |
46 | var effectTypes = {
47 | lowpass: 'lowpass',
48 | highpass: 'highpass',
49 | bandpass: 'bandpass',
50 | notch: 'notch',
51 | }
52 |
53 |
54 |
55 |
56 |
57 | /*
58 | *
59 | *
60 | *
61 | * UI setup
62 | *
63 | *
64 | *
65 | */
66 |
67 |
68 | var f
69 | var o
70 |
71 | o = params.main = {}
72 | f = o.folder = pane1.addFolder({ title: 'MAIN' })
73 | addNumeric(f, o, 'velocity', 1, 0, 1)
74 | addNumeric(f, o, 'frequency', 440, 100, 8000, true, 1)
75 |
76 |
77 | o = params.carrier = {}
78 | f = o.folder = pane1.addFolder({ title: 'CARRIER SIGNAL' })
79 | addPulldown(f, o, 'type', types, 'triangle')
80 | addNumeric(f, o, 'attack', 0.1, 0, 5, true)
81 | addNumeric(f, o, 'hold', 0, 0, 5, true)
82 | addNumeric(f, o, 'sustain', 0.8, 0, 1, false)
83 | addNumeric(f, o, 'decay', 0.1, 0, 5, true)
84 | addNumeric(f, o, 'duration', 0.1, 0, 5, true)
85 | addNumeric(f, o, 'release', 0.1, 0, 5, true)
86 |
87 |
88 | o = params.distort1 = {}
89 | f = o.folder = pane1.addFolder({ title: 'carrier distortion' })
90 | addPulldown(f, o, 'type', {
91 | bitcrush: 'crush',
92 | clip: 'shape-clip',
93 | boost: 'shape-boost',
94 | fold: 'shape-fold',
95 | thin: 'shape-thin',
96 | fat: 'shape-fat',
97 | })
98 | addNumeric(f, o, 'argument', 5, 1, 20, false, 1)
99 | o.folder.expanded = false
100 |
101 |
102 |
103 | o = params.mods1 = {}
104 | f = o.folder = pane1.addFolder({ title: 'freq mods 1' })
105 | addNumeric(f, o, 'delay', 0.1, 0.01, 2, true, 0.01)
106 | addNumeric(f, o, 'jump mult', 1, 0.5, 2, true, 0.05)
107 | addNumeric(f, o, 'jump add', 0, -200, 200, false, 10)
108 | addNumeric(f, o, 'sweep', 1, 0.1, 10, true, 0.05)
109 | addNumeric(f, o, 'sweep time', 0.25, 0.01, 2, true, 0.01)
110 | addNumeric(f, o, 'repeat', 1, 1, 20, false, 1)
111 | o.folder.expanded = false
112 |
113 |
114 |
115 | o = params.mods2 = {}
116 | f = o.folder = pane1.addFolder({ title: 'freq mods 2' })
117 | addNumeric(f, o, 'delay', 0.1, 0.01, 2, true, 0.01)
118 | addNumeric(f, o, 'jump mult', 1, 0.5, 2, true, 0.05)
119 | addNumeric(f, o, 'jump add', 0, -200, 200, false, 10)
120 | addNumeric(f, o, 'sweep', 1, 0.1, 10, true, 0.05)
121 | addNumeric(f, o, 'sweep time', 0.25, 0.01, 2, true, 0.01)
122 | addNumeric(f, o, 'repeat', 1, 1, 20, false, 1)
123 | o.folder.expanded = false
124 |
125 |
126 |
127 |
128 |
129 |
130 | o = params.FM = {}
131 | f = o.folder = pane2.addFolder({ title: 'FM signal' })
132 | addNumeric(f, o, 'freq mult', 1, 0.1, 10, true)
133 | addNumeric(f, o, 'freq add', 0, -10, 10, false, 0.1)
134 | addPulldown(f, o, 'type', types)
135 | addNumeric(f, o, 'gain mult', 1, 0.1, 10, true)
136 | addNumeric(f, o, 'attack', 0.1, 0, 5, true)
137 | addNumeric(f, o, 'decay', 0.1, 0, 5, true)
138 | addNumeric(f, o, 'sustain', 0.8, 0, 1)
139 | addNumeric(f, o, 'release', 0.1, 0, 5, true)
140 | o.folder.expanded = false
141 |
142 |
143 |
144 | o = params.tremolo = {}
145 | f = o.folder = pane2.addFolder({ title: 'tremolo' })
146 | addPulldown(f, o, 'type', types)
147 | addNumeric(f, o, 'depth', 0.05, 0.01, 2, true)
148 | addNumeric(f, o, 'frequency', 10, 0.1, 100, true)
149 | o.folder.expanded = false
150 |
151 |
152 |
153 | o = params.vibrato = {}
154 | f = o.folder = pane2.addFolder({ title: 'vibrato' })
155 | addPulldown(f, o, 'type', types)
156 | addNumeric(f, o, 'depth', 0.2, 0.01, 2, true)
157 | addNumeric(f, o, 'frequency', 10, 0.1, 100, true)
158 | o.folder.expanded = false
159 |
160 |
161 |
162 |
163 |
164 | o = params.effect1 = {}
165 | f = o.folder = pane2.addFolder({ title: 'effect 1' })
166 | addPulldown(f, o, 'type', effectTypes)
167 | addNumeric(f, o, 'freq mult', 1, 0.1, 10, true)
168 | addNumeric(f, o, 'sweep', 1, 0.1, 10, true)
169 | addNumeric(f, o, 'sweep time', 0.2, 0.01, 2, true)
170 | addNumeric(f, o, 'Q', 1, 0.2, 10, true, 0.1)
171 | o.folder.expanded = false
172 |
173 |
174 | o = params.effect2 = {}
175 | f = o.folder = pane2.addFolder({ title: 'effect 2' })
176 | addPulldown(f, o, 'type', effectTypes)
177 | addNumeric(f, o, 'freq mult', 1, 0.1, 10, true)
178 | addNumeric(f, o, 'sweep', 1, 0.1, 10, true)
179 | addNumeric(f, o, 'sweep time', 0.2, 0.01, 2, true)
180 | addNumeric(f, o, 'Q', 1, 0.2, 10, true, 0.1)
181 | o.folder.expanded = false
182 |
183 |
184 |
185 |
186 | o = params.distort2 = {}
187 | f = o.folder = pane2.addFolder({ title: 'post distortion' })
188 | addPulldown(f, o, 'type', {
189 | bitcrush: 'crush',
190 | clip: 'shape-clip',
191 | boost: 'shape-boost',
192 | fold: 'shape-fold',
193 | thin: 'shape-thin',
194 | fat: 'shape-fat',
195 | })
196 | addNumeric(f, o, 'argument', 5, 1, 20, false, 1)
197 | o.folder.expanded = false
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 | /*
223 | *
224 | * helpers to simplify adding params
225 | *
226 | */
227 |
228 | function setKeyPrefix(obj) {
229 | if (obj.keyPrefix) return
230 | for (var s in params) if (params[s] === obj) obj.keyPrefix = s
231 | }
232 |
233 | function addPulldown(folder, obj, name, options, value) {
234 | setKeyPrefix(obj)
235 | var label = name
236 | name = name.replace(/\s+/g, '').toLowerCase()
237 | var presetKey = obj.keyPrefix + '_' + name
238 | obj[name] = value || options[Object.keys(options)[0]]
239 | folder.addInput(obj, name, { options, label, presetKey })
240 | }
241 |
242 | function addNumeric(folder, obj, name, val, min, max, logScale, step) {
243 | setKeyPrefix(obj)
244 | var label = name
245 | name = name.replace(/\s+/g, '').toLowerCase()
246 | var presetKey = obj.keyPrefix + '_' + name
247 |
248 | if (logScale) {
249 | var absmin = 0.001
250 | if (val < absmin) val = absmin
251 | if (min < absmin) min = absmin
252 | step = step || min
253 | } else {
254 | step = step || min || 0.01
255 | }
256 | obj[name] = val
257 |
258 | // cache-bust so it updates later
259 | if (logScale) obj[name] = min
260 |
261 | var input = folder.addInput(obj, name, { min, max, step, label, presetKey })
262 |
263 | if (logScale) {
264 | // deeply deeply hackish
265 | // log-ify the slider view
266 | var controller = input.controller.controller
267 | controller.view_.sliderInputView_.update = function () {
268 | var v = Math.log(this.value.rawValue)
269 | var min = Math.log(this.minValue_)
270 | var max = Math.log(this.maxValue_)
271 | var p = 100 * (v - min) / (max - min)
272 | this.innerElem_.style.width = p + '%'
273 | }
274 | // log-ify the slider inputs
275 | controller.sliderIc_.ptHandler_.computePosition_ = function (x, y) {
276 | var rect = this.element.getBoundingClientRect()
277 | var f = x / rect.width
278 | var val = Math.pow(max, f) * Math.pow(min, 1 - f)
279 | var px = (val - min) / (max - min)
280 | var py = y / rect.height
281 | return { px, py }
282 | }
283 | // rework the number formatting
284 | controller.view_.textInputView_.formatter_.format = function (val) {
285 | return fmt(val)
286 | }
287 | // undo cache-bust
288 | obj[name] = val
289 | }
290 | }
291 |
292 | // update all display-only log scale parameters
293 | function refreshPanes() {
294 | pane1.refresh()
295 | pane2.refresh()
296 | }
297 |
298 | // params are all set up now, so refresh the log values
299 | refreshPanes()
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 | /*
309 | *
310 | * this bit is hairy, due to how much
311 | * chrome hates pages that use webaudio
312 | *
313 | */
314 |
315 | var ignoreParamEvents = false
316 |
317 | function beginEditingParams() {
318 | ignoreParamEvents = true
319 | }
320 |
321 | function finishEditingParams() {
322 | refreshPanes()
323 | playSound()
324 | ignoreParamEvents = false
325 | }
326 |
327 |
328 | // general event for all param changes
329 | function onParamChange() {
330 | if (ignoreParamEvents) return
331 | refreshPanes()
332 | playSound()
333 | }
334 | pane1.on('change', onParamChange)
335 | pane2.on('change', onParamChange)
336 |
337 |
338 | // also play sound on keypresses
339 | window.onkeydown = (ev) => {
340 | if (ev.metaKey) return
341 | if (ev.key === 'Tab') return
342 | if (ev.key === 'Shift') return
343 | var focused = document.activeElement
344 | var focType = (focused && focused['type']) || ''
345 | if (/text/.test(focType)) return
346 |
347 | var freq = params.main.frequency
348 | if (ev.key === ' ') {
349 | // don't scroll on space
350 | ev.preventDefault()
351 | } else {
352 | var chars = 'zxcvbnmasdfghjklqwertyuiop'
353 | var i = chars.indexOf(ev.key.toLowerCase())
354 | if (i < 0) return
355 | var scale = [0, 2, 4, 5, 7, 9, 11]
356 | var note = 55
357 | while (i >= scale.length) {
358 | note += 12
359 | i -= scale.length
360 | }
361 | note += scale[i]
362 | freq = 440 * Math.pow(2, (note - 69) / 12)
363 | }
364 |
365 | // the following implicitly triggers a sound
366 | beginEditingParams()
367 | params.main.frequency = freq
368 | finishEditingParams()
369 | }
370 |
371 |
372 |
373 |
374 |
375 |
376 |
377 |
378 |
379 |
380 |
381 |
382 |
383 |
384 | /*
385 | *
386 | *
387 | *
388 | * Program creation and sound playback
389 | *
390 | *
391 | *
392 | */
393 |
394 |
395 | import Generator from '../../wasgen'
396 | // import Generator from 'wasgen'
397 |
398 | var nextSoundCutoff = -1000
399 | var gen, ctx
400 |
401 | function playSound() {
402 | $('.hint').style.display = 'none'
403 |
404 | if (!gen) {
405 | ctx = new (window.AudioContext || window['webkitAudioContext'])()
406 | var dest = ctx.createGain()
407 | dest.connect(ctx.destination)
408 | gen = new Generator(ctx, dest)
409 | window['gen'] = gen // so that playback code will run in console
410 | window['ctx'] = ctx
411 | $('#ver').innerHTML += ` -- wasgen v${gen.version}`
412 | }
413 | if (ctx.state !== 'running') ctx.resume()
414 |
415 | // overall play duration
416 | var duration = params.carrier.duration
417 | + params.carrier.attack
418 | + params.carrier.hold
419 |
420 | // debounce
421 | var t = performance.now()
422 | if (t < nextSoundCutoff) return
423 | var wait = Math.min(Math.max(0.25, duration / 2), 1.5)
424 | nextSoundCutoff = t + wait * 1000
425 |
426 | // build program from params
427 | var program = buildProgram()
428 | program.forEach(prog => formatProgramObject(prog))
429 |
430 | // play!
431 | var freq = fmt(params.main.frequency)
432 | var vel = fmt(params.main.velocity)
433 | var dur = fmt(duration)
434 | var now = gen.now() + 0.15
435 | gen.play(program, freq, vel, now, now + dur)
436 | writeCode(program, freq, vel, dur)
437 | }
438 |
439 |
440 | function buildProgram() {
441 |
442 | /** @type {*} */
443 | var program = [{
444 | type: params.carrier.type,
445 | freq: [],
446 | gain: [{
447 | a: params.carrier.attack,
448 | h: params.carrier.hold,
449 | d: params.carrier.decay,
450 | s: params.carrier.sustain,
451 | r: params.carrier.release,
452 | }],
453 | }]
454 |
455 |
456 | if (params.distort1.folder.expanded) {
457 | var d1 = params.distort1.type
458 | var d1a = params.distort1.argument
459 | program[0].effect = `${d1}-${d1a}`
460 | }
461 |
462 |
463 | if (params.mods1.folder.expanded) {
464 | program[0].freq.push({
465 | w: params.mods1.delay,
466 | t: params.mods1.jumpmult,
467 | f: params.mods1.jumpadd,
468 | a: 0,
469 | p: params.mods1.sweep,
470 | q: params.mods1.sweeptime,
471 | x: params.mods1.repeat,
472 | })
473 | }
474 |
475 |
476 | if (params.mods2.folder.expanded) {
477 | program[0].freq.push({
478 | w: params.mods2.delay,
479 | t: params.mods2.jumpmult,
480 | f: params.mods2.jumpadd,
481 | a: 0,
482 | p: params.mods2.sweep,
483 | q: params.mods2.sweeptime,
484 | x: params.mods2.repeat,
485 | })
486 | }
487 |
488 |
489 |
490 | if (params.tremolo.folder.expanded) {
491 | program[0].freq.push({
492 | type: params.tremolo.type,
493 | freq: params.tremolo.frequency,
494 | gain: { t: params.tremolo.depth },
495 | })
496 | }
497 |
498 | if (params.vibrato.folder.expanded) {
499 | program[0].gain.push({
500 | type: params.vibrato.type,
501 | freq: params.vibrato.frequency,
502 | gain: {
503 | t: params.vibrato.depth,
504 | r: params.carrier.release,
505 | z: 0,
506 | },
507 | })
508 | }
509 |
510 | if (params.FM.folder.expanded) {
511 | program[0].freq.push({
512 | type: params.FM.type,
513 | freq: {
514 | t: params.FM.freqmult,
515 | f: params.FM.freqadd,
516 | },
517 | gain: {
518 | t: params.FM.gainmult,
519 | a: params.FM.attack,
520 | d: params.FM.decay,
521 | s: params.FM.sustain,
522 | r: params.FM.release,
523 | },
524 | })
525 | }
526 |
527 | if (params.distort2.folder.expanded) {
528 | var d2 = params.distort2.type
529 | var d2a = params.distort2.argument
530 | program.push(`${d2}-${d2a}`)
531 | }
532 |
533 | if (params.effect1.folder.expanded) {
534 | program.push({
535 | type: params.effect1.type,
536 | freq: {
537 | t: params.effect1.freqmult,
538 | p: params.effect1.sweep,
539 | q: params.effect1.sweeptime,
540 | },
541 | Q: params.effect1.q,
542 | })
543 | }
544 | if (params.effect2.folder.expanded) {
545 | program.push({
546 | type: params.effect2.type,
547 | freq: {
548 | t: params.effect2.freqmult,
549 | p: params.effect2.sweep,
550 | q: params.effect2.sweeptime,
551 | },
552 | Q: params.effect2.q,
553 | })
554 | }
555 | return program
556 | }
557 |
558 |
559 | function fmt(num) {
560 | var sign = (num < 0) ? -1 : 1
561 | num = Math.abs(num)
562 | if (num < 0.002) return 0
563 | if (num > 20) return sign * Math.round(num)
564 | if (num > 2) return sign * Math.round(num * 10) / 10
565 | if (num > 0.2) return sign * Math.round(num * 100) / 100
566 | return sign * Math.round(num * 1000) / 1000
567 | }
568 |
569 | function formatProgramObject(obj) {
570 | Object.keys(obj).forEach(s => {
571 | if (Array.isArray(obj[s])) {
572 | if (obj[s].length === 1) obj[s] = obj[s][0]
573 | }
574 | if (typeof obj[s] === 'number') {
575 | obj[s] = fmt(obj[s])
576 | }
577 | if (typeof obj[s] === 'object') {
578 | if (Object.keys(obj[s]).length === 0) {
579 | delete obj[s]
580 | } else {
581 | formatProgramObject(obj[s])
582 | }
583 | }
584 | })
585 | }
586 |
587 |
588 |
589 |
590 |
591 |
592 |
593 |
594 | /*
595 | *
596 | *
597 | *
598 | * writing out playback code
599 | *
600 | *
601 | *
602 | */
603 |
604 | var textfield = $('.code')
605 |
606 | function writeCode(program, freq, vel, dur) {
607 | var progstrs = program.map(o => stringify(o))
608 |
609 | textfield.value = ``
610 | textfield.value += `// import Gen from 'wasgen'\n`
611 | textfield.value += `// var gen = new Gen()\n`
612 | textfield.value += `gen.play([\n`
613 | progstrs.forEach(str => { textfield.value += ` ${str},\n` })
614 | textfield.value += `], ${freq}, ${vel}, gen.now(), gen.now() + ${dur})\n`
615 | }
616 |
617 | function stringify(obj) {
618 | roundNums(obj)
619 | var s = JSON.stringify(obj)
620 | s = s.replace(/"([^"]*)":/g, '$1:')
621 | s = s.replace(/,gain/g, ', gain')
622 | s = s.replace(/,freq/g, ', freq')
623 | return s
624 | }
625 |
626 | function roundNums(obj) {
627 | for (var key in obj) {
628 | if (typeof obj[key] === 'object') roundNums(obj[key])
629 | if (typeof obj[key] === 'number') obj[key] = Math.round(obj[key] * 1000) / 1000
630 | }
631 | }
632 |
633 |
634 |
635 |
636 |
637 | /*
638 | *
639 | *
640 | *
641 | * experimenting with rendering -> offlineContext -> WAV
642 | *
643 | *
644 | *
645 | */
646 |
647 | var renderBut = $('.render')
648 | var anchor
649 | renderBut.onclick = async () => {
650 | if (!OfflineAudioContext) {
651 | renderBut.textContent = 'Not supported in your browser, sorry!'
652 | return
653 | }
654 |
655 | // play and overall duration
656 | var playDur = params.carrier.duration
657 | + params.carrier.attack
658 | + params.carrier.hold
659 | var totalDur = playDur
660 | + params.carrier.release * 5 // magic number!
661 | + 0.01 // arbitrarily pad the end a tad
662 | var rate = 44100
663 | var samples = (totalDur * rate) | 0
664 | var ctx = new OfflineAudioContext(2, samples, rate)
665 | var gen = new Generator(ctx, ctx.destination, false, true)
666 |
667 | // build program from params
668 | var program = buildProgram()
669 | program.forEach(prog => formatProgramObject(prog))
670 |
671 | // play args
672 | var freq = fmt(params.main.frequency)
673 | var vel = fmt(params.main.velocity)
674 | var dur = fmt(playDur)
675 | var now = 5 / 44100
676 | writeCode(program, freq, vel, dur)
677 |
678 | // pausing here lets wasgen init bitcrusher audioworklet..
679 | if (/crush/.test(JSON.stringify(program))) await sleep(100)
680 |
681 | // play logic
682 | gen.play(program, freq, vel, now, now + dur)
683 | var buffer = await ctx.startRendering()
684 |
685 | // yoink: https://github.com/Jam3/audiobuffer-to-wav/blob/master/demo/index.js
686 | if (!anchor) {
687 | anchor = document.createElement('a')
688 | document.body.appendChild(anchor)
689 | anchor.style.display = 'none'
690 | }
691 |
692 | var bufferToWav = require('audiobuffer-to-wav')
693 | var wav = bufferToWav(buffer)
694 | var blob = new window.Blob([new DataView(wav)], {
695 | type: 'audio/wav'
696 | })
697 |
698 | var url = window.URL.createObjectURL(blob)
699 | anchor.href = url
700 | anchor.download = 'audio.wav'
701 | anchor.click()
702 | window.URL.revokeObjectURL(url)
703 |
704 | gen.dispose()
705 | }
706 |
707 | function sleep(ms) {
708 | return new Promise(resolve => setTimeout(resolve, ms))
709 | }
710 |
711 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | wafxr
8 |
101 |
102 |
103 |
104 |
105 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 | Playback code:
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
143 |
144 |
145 |
146 |
147 |
--------------------------------------------------------------------------------
/docs/presets.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | import { demo, pane1, pane2, params } from './demo'
4 | var $ = document.querySelector.bind(document)
5 |
6 | function rand(a, b) { return a + (Math.random() * (b - a)) }
7 | function rint(a, b) { return Math.floor(rand(a, b)) }
8 | function rlog(a, b) { return Math.pow(2, rand(Math.log2(a), Math.log2(b))) }
9 | function choose() { return (arguments[(Math.random() * arguments.length) | 0]) }
10 |
11 |
12 |
13 |
14 |
15 |
16 | /*
17 | *
18 | * SETUP AND ABSTRACTIONS
19 | *
20 | */
21 |
22 | var basePresets = []
23 | setTimeout(() => {
24 | // do this after main module initializes
25 | basePresets = [pane1.exportPreset(), pane2.exportPreset()]
26 | delete basePresets[0].main_velocity
27 | delete basePresets[0].main_frequency
28 | }, 1)
29 |
30 |
31 |
32 | // expand folders needed for a given preset
33 | function setFolders(args = []) {
34 | for (var s in params) {
35 | var exp = false
36 | if (s === 'main' || s === 'carrier') exp = true
37 | if (args.includes(s)) exp = true
38 | params[s].folder.expanded = exp
39 | }
40 | }
41 |
42 | // abstraction to init/apply/finish a preset
43 | function applyPreset(applyPresetFn) {
44 | demo.beginEditingParams()
45 | // reset everything to defaults, before applying a preset
46 | pane1.importPreset(basePresets[0])
47 | pane2.importPreset(basePresets[1])
48 | var result = applyPresetFn()
49 | if (result.frequency) params.main.frequency = result.frequency
50 | setFolders(result.folders)
51 | demo.finishEditingParams()
52 | }
53 |
54 |
55 | // actual handlers
56 | var addHandler = (name, fn) => {
57 | $('.' + name).addEventListener('click', ev => applyPreset(fn))
58 | }
59 |
60 | addHandler('reset', resetPreset)
61 | addHandler('jump', jumpPreset)
62 | addHandler('coin', coinPreset)
63 | addHandler('expl', explosionPreset)
64 | addHandler('laser', laserPreset)
65 | addHandler('ouch', ouchPreset)
66 | addHandler('power', powerPreset)
67 | addHandler('ui', uiPreset)
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | /*
80 | *
81 | * PRESET IMPLEMENTATIONS
82 | *
83 | */
84 |
85 |
86 | function resetPreset() {
87 | var folders = []
88 | var frequency = 440
89 | return { folders, frequency }
90 | }
91 |
92 |
93 |
94 | function jumpPreset() {
95 | var folders = []
96 | var frequency = rlog(200, 800)
97 | params.carrier.type = choose('triangle', 'w909', 'w999')
98 | params.carrier.duration = rand(0.05, 0.2)
99 | params.carrier.sustain = rand(0.2, 0.7)
100 | params.carrier.release = rand(0.05, 0.15)
101 | // sweeps and jumps and effects
102 | folders.push('mods1')
103 | params.mods1.delay = 0
104 | params.mods1.sweep = rand(1.1, 2.5)
105 | params.mods1.sweeptime = rand(0.1, 0.3)
106 | if (rint(0, 2)) {
107 | folders.push('mods2')
108 | params.mods2.delay = rand(0.03, 0.1)
109 | params.mods2.jumpmult = 1 + rand(0.1, 0.3) * choose(1, -1)
110 | }
111 | return { folders, frequency }
112 | }
113 |
114 |
115 |
116 |
117 |
118 |
119 | function coinPreset() {
120 | var folders = []
121 | var frequency = rlog(120, 1600)
122 | params.carrier.type = choose('sine', 'triangle', 'w909', 'w999')
123 | params.carrier.duration = rand(0.05, 0.1)
124 | params.carrier.sustain = rand(0.1, 0.5)
125 | params.carrier.release = rand(0.05, 0.25)
126 | // sweeps and jumps and effects
127 | if (rint(0, 4)) {
128 | folders.push('mods1')
129 | params.mods1.delay = rand(0.02, 0.1)
130 | params.mods1.jumpmult = rand(1.1, 1.5)
131 | if (rint(0, 1.7)) {
132 | folders.push('mods2')
133 | params.mods2.delay = rand(0.02, 0.1)
134 | params.mods2.jumpmult = rand(1.1, 1.5)
135 | }
136 | }
137 | return { folders, frequency }
138 | }
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 | function explosionPreset() {
147 | var folders = []
148 | var frequency = rlog(100, 1000)
149 | params.carrier.duration = rand(0.1, 0.2)
150 | params.carrier.sustain = rand(0.1, 0.5)
151 | params.carrier.release = rand(0.2, 0.3)
152 | folders.push('distort1')
153 | params.distort1.type = choose('crush', 'shape-boost')
154 | params.distort1.argument = rint(1, 9)
155 | if (rint(0, 2)) {
156 | params.carrier.type = choose('sine', 'triangle')
157 | folders.push('FM')
158 | params.FM.type = choose('n0', 'np', 'nb')
159 | params.FM.multiplier = rand(3, 10)
160 | params.FM.release = 5
161 | // sweeps and jumps and effects
162 | folders.push('mods1')
163 | params.mods1.sweep = rand(0.2, 0.9)
164 | params.mods1.sweeptime = params.carrier.release
165 | } else {
166 | params.carrier.type = choose('n0', 'np', 'nb')
167 | folders.push('effect1')
168 | params.effect1.type = choose('bandpass', 'lowpass')
169 | var low = /low/.test(params.effect1.type)
170 | params.effect1.freqmult = low ? rand(3, 8) : rand(1, 1.5)
171 | params.effect1.sweep = rand(0.1, 0.5)
172 | params.effect1.sweeptime = rlog(0.01, 0.2)
173 | params.effect1.q = rand(0.5, 5)
174 | if (rint(0, 2)) {
175 | folders.push('tremolo')
176 | params.tremolo.depth = rand(0.1, 0.5)
177 | params.tremolo.frequency = rlog(5, 60)
178 | }
179 | }
180 | if (rint(0, 2)) {
181 | folders.push('vibrato')
182 | params.vibrato.depth = rand(0.05, 0.2)
183 | params.vibrato.frequency = rlog(5, 60)
184 | }
185 | return { folders, frequency }
186 | }
187 |
188 |
189 |
190 | function laserPreset() {
191 | var folders = []
192 | var frequency = rlog(300, 1500)
193 | params.carrier.type = choose('sine', 'triangle', 'sawtooth', 'p25', 'p40')
194 | params.carrier.duration = rand(0.05, 0.15)
195 | params.carrier.sustain = rand(0.2, 0.6)
196 | params.carrier.release = rand(0.02, 0.1)
197 | // sweeps and jumps and effects
198 | folders.push('mods1')
199 | params.mods1.sweep = rlog(0.25, 4)
200 | params.mods1.sweeptime = rand(0.02, 0.08)
201 | if (rint(0, 2)) {
202 | folders.push('vibrato')
203 | params.vibrato.depth = rand(0.1, 0.5)
204 | params.vibrato.frequency = rlog(5, 80)
205 | }
206 | return { folders, frequency }
207 | }
208 |
209 |
210 |
211 |
212 |
213 | function ouchPreset() {
214 | var folders = []
215 | var frequency = rand(400, 1000)
216 | params.carrier.type = choose('n0', 'np', 'nb')
217 | params.carrier.duration = rlog(0.03, 0.06)
218 | params.carrier.attack = rlog(0.01, 0.05)
219 | params.carrier.sustain = rand(0.2, 0.6)
220 | params.carrier.release = rlog(0.01, 0.1)
221 | params.carrier.decay = rlog(0.01, 0.1)
222 | if (rint(0, 3)) {
223 | folders.push('distort1')
224 | params.distort1.type = choose('crush', 'shape-boost')
225 | params.distort1.argument = rint(2, 7)
226 | }
227 | folders.push('effect1')
228 | params.effect1.type = 'bandpass'
229 | params.effect1.freqmult = rand(1, 2)
230 | params.effect1.sweep = rand(0.25, 0.8)
231 | params.effect1.sweeptime = rlog(0.01, 0.05)
232 | params.effect1.q = rand(0.5, 3)
233 | return { folders, frequency }
234 | }
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 | function powerPreset() {
243 | var folders = []
244 | var frequency = rlog(400, 1000)
245 | params.carrier.type = choose('sine', 'square', 'triangle', 'sawtooth', 'w909')
246 | params.carrier.duration = rand(0.05, 0.1)
247 | params.carrier.sustain = rand(0.2, 0.6)
248 | params.carrier.release = rand(0.05, 0.2)
249 | // sweeps and jumps and effects
250 | folders.push('mods1')
251 | if (rint(0, 2)) {
252 | params.mods1.sweep = rand(1, 1.7)
253 | params.mods1.sweeptime = rand(0.05, 0.2)
254 | } else {
255 | params.mods1.sweep = rand(0.7, 0.9)
256 | params.mods1.sweeptime = rand(0.05, 0.2)
257 | params.mods1.delay = rand(0.05, 0.2)
258 | params.mods1.jumpadd = rand(50, 150)
259 | params.mods1.repeat = 30
260 | }
261 | if (rint(0, 2)) {
262 | folders.push('tremolo')
263 | params.tremolo.type = 'square'
264 | params.tremolo.depth = rlog(0.025, 0.5)
265 | params.tremolo.frequency = rlog(10, 50)
266 | }
267 | return { folders, frequency }
268 | }
269 |
270 |
271 |
272 |
273 |
274 | function uiPreset() {
275 | var folders = []
276 | var frequency = rlog(150, 3000)
277 | params.carrier.type = choose('sine', 'square', 'triangle', 'sawtooth', 'p25', 'w909', 'n0', 'n1')
278 | params.carrier.duration = rand(0.01, 0.05)
279 | params.carrier.attack = rand(0.01, 0.05)
280 | params.carrier.sustain = rand(0.2, 0.6)
281 | params.carrier.release = rand(0.01, 0.05)
282 | params.carrier.decay = rlog(0.01, 0.1)
283 | // sweeps and jumps and effects
284 | if (rint(0, 2)) {
285 | folders.push('mods1')
286 | params.mods1.sweep = rand(0.5, 1.5)
287 | params.mods1.sweeptime = rand(0.01, 0.5)
288 | }
289 | return { folders, frequency }
290 | }
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
--------------------------------------------------------------------------------
/docs/webpack.config.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | var path = require('path')
4 | var entryPath = path.resolve('.', 'demo.js')
5 | var buildPath = path.resolve('.')
6 |
7 |
8 | module.exports = (env) => ({
9 |
10 | mode: (() => {
11 | return (env && env.prod) ?
12 | 'production' : 'development'
13 | })(),
14 |
15 | entry: entryPath,
16 |
17 | output: {
18 | path: buildPath,
19 | filename: 'bundle.js',
20 | },
21 | stats: "minimal",
22 | devServer: {
23 | static: buildPath,
24 | host: "0.0.0.0",
25 | },
26 | })
27 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | // inputs
3 | "include": [
4 | "docs/demo.js",
5 | ],
6 | "compilerOptions": {
7 | // output
8 | "outDir": "./dist",
9 | "target": "es5",
10 | "rootDir": ".",
11 | "allowJs": true,
12 | "checkJs": true,
13 | "resolveJsonModule": true,
14 | "esModuleInterop": true,
15 | "lib": [
16 | "DOM",
17 | "DOM.Iterable",
18 | "ES2018"
19 | ],
20 | },
21 | }
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "wafxr",
3 | "version": "0.18.2",
4 | "lockfileVersion": 2,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "wafxr",
9 | "version": "0.18.2",
10 | "license": "ISC",
11 | "dependencies": {
12 | "wasgen": "^0.18.2"
13 | },
14 | "devDependencies": {
15 | "audiobuffer-to-wav": "^1.0.0",
16 | "tweakpane": "^1.3.4"
17 | }
18 | },
19 | "node_modules/audiobuffer-to-wav": {
20 | "version": "1.0.0",
21 | "resolved": "https://registry.npmjs.org/audiobuffer-to-wav/-/audiobuffer-to-wav-1.0.0.tgz",
22 | "integrity": "sha1-1bQyJxRV5/7laxEc0PjWINf54QU=",
23 | "dev": true
24 | },
25 | "node_modules/param-enveloper": {
26 | "version": "0.3.0",
27 | "resolved": "https://registry.npmjs.org/param-enveloper/-/param-enveloper-0.3.0.tgz",
28 | "integrity": "sha512-m6bijy1QhP5xctlcD4BVM+QCKeu3LaHdnP5fYj56J91s28jtF2Y7c+YExVBFxAyb/LO0sfCK95Ugt69E0ea/5w=="
29 | },
30 | "node_modules/tweakpane": {
31 | "version": "1.6.1",
32 | "resolved": "https://registry.npmjs.org/tweakpane/-/tweakpane-1.6.1.tgz",
33 | "integrity": "sha512-uvaD2SSfAInM2Zk/tCh7j2Ri5GYzKVBmQI7hD1cEKe2HVUT7QWflxZfZbIRCeD4C0odK5PLrCvCq90YjrOMFyg==",
34 | "dev": true
35 | },
36 | "node_modules/wasgen": {
37 | "version": "0.18.2",
38 | "resolved": "https://registry.npmjs.org/wasgen/-/wasgen-0.18.2.tgz",
39 | "integrity": "sha512-xuxm2cSJikciXC9HSJh8C+k5XVxBXGx9EERdafSmg6l+AJG9iNH35fkUO5IJagMRDCChHBJUlhy1/+c6zTmAJA==",
40 | "dependencies": {
41 | "param-enveloper": "^0.3.0"
42 | }
43 | }
44 | },
45 | "dependencies": {
46 | "audiobuffer-to-wav": {
47 | "version": "1.0.0",
48 | "resolved": "https://registry.npmjs.org/audiobuffer-to-wav/-/audiobuffer-to-wav-1.0.0.tgz",
49 | "integrity": "sha1-1bQyJxRV5/7laxEc0PjWINf54QU=",
50 | "dev": true
51 | },
52 | "param-enveloper": {
53 | "version": "0.3.0",
54 | "resolved": "https://registry.npmjs.org/param-enveloper/-/param-enveloper-0.3.0.tgz",
55 | "integrity": "sha512-m6bijy1QhP5xctlcD4BVM+QCKeu3LaHdnP5fYj56J91s28jtF2Y7c+YExVBFxAyb/LO0sfCK95Ugt69E0ea/5w=="
56 | },
57 | "tweakpane": {
58 | "version": "1.6.1",
59 | "resolved": "https://registry.npmjs.org/tweakpane/-/tweakpane-1.6.1.tgz",
60 | "integrity": "sha512-uvaD2SSfAInM2Zk/tCh7j2Ri5GYzKVBmQI7hD1cEKe2HVUT7QWflxZfZbIRCeD4C0odK5PLrCvCq90YjrOMFyg==",
61 | "dev": true
62 | },
63 | "wasgen": {
64 | "version": "0.18.2",
65 | "resolved": "https://registry.npmjs.org/wasgen/-/wasgen-0.18.2.tgz",
66 | "integrity": "sha512-xuxm2cSJikciXC9HSJh8C+k5XVxBXGx9EERdafSmg6l+AJG9iNH35fkUO5IJagMRDCChHBJUlhy1/+c6zTmAJA==",
67 | "requires": {
68 | "param-enveloper": "^0.3.0"
69 | }
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "wafxr",
3 | "version": "0.18.2",
4 | "description": "webaudio sound effects",
5 | "main": "src/wafxr.js",
6 | "files": [
7 | "/src"
8 | ],
9 | "scripts": {
10 | "start": "cd docs && webpack-dev-server",
11 | "build": "cd docs && webpack --env prod"
12 | },
13 | "author": "Andy Hall",
14 | "keywords": [
15 | "webaudio",
16 | "sound effects",
17 | "js"
18 | ],
19 | "license": "ISC",
20 | "repository": "github:fenomas/wafxr",
21 | "dependencies": {
22 | "wasgen": "^0.18.2"
23 | },
24 | "devDependencies": {
25 | "audiobuffer-to-wav": "^1.0.0",
26 | "tweakpane": "^1.3.4"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/wafxr.js:
--------------------------------------------------------------------------------
1 |
2 | /*
3 | *
4 | * now generates sounds with `wasgen` library
5 | * export it, as a convenience for any legacy code
6 | * using this lib as a dependency
7 | *
8 | */
9 |
10 | import { gen } from 'wasgen'
11 |
12 | export default gen
13 |
14 |
15 |
--------------------------------------------------------------------------------