Last active
September 3, 2019 18:44
-
-
Save mtmckenna/9b7f794195dcec264a2f27945a387f77 to your computer and use it in GitHub Desktop.
SoundEffect class that wraps jsfxr + uses web audio API instead of the HTML5 audio tag
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const AudioContext = window.AudioContext || window.webkitAudioContext; | |
let context = new AudioContext(); | |
let mute = false; | |
export default class SoundEffect { | |
constructor(data) { | |
this.ended = true; | |
context.decodeAudioData(jsfxr(data), (buffer) => { | |
this.buffer = buffer; | |
}); | |
} | |
play() { | |
if (!this.buffer || mute) return; | |
this.source = context.createBufferSource(); | |
this.source.buffer = this.buffer; | |
this.source.connect(context.destination); | |
this.source.start(0); | |
this.ended = false; | |
this.source.addEventListener("ended", () => this.ended = true ); | |
} | |
} | |
/** | |
* SfxrParams | |
* | |
* Copyright 2010 Thomas Vian | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
* | |
* @author Thomas Vian | |
*/ | |
/** @constructor */ | |
function SfxrParams() { | |
//-------------------------------------------------------------------------- | |
// | |
// Settings String Methods | |
// | |
//-------------------------------------------------------------------------- | |
/** | |
* Parses a settings array into the parameters | |
* @param array Array of the settings values, where elements 0 - 23 are | |
* a: waveType | |
* b: attackTime | |
* c: sustainTime | |
* d: sustainPunch | |
* e: decayTime | |
* f: startFrequency | |
* g: minFrequency | |
* h: slide | |
* i: deltaSlide | |
* j: vibratoDepth | |
* k: vibratoSpeed | |
* l: changeAmount | |
* m: changeSpeed | |
* n: squareDuty | |
* o: dutySweep | |
* p: repeatSpeed | |
* q: phaserOffset | |
* r: phaserSweep | |
* s: lpFilterCutoff | |
* t: lpFilterCutoffSweep | |
* u: lpFilterResonance | |
* v: hpFilterCutoff | |
* w: hpFilterCutoffSweep | |
* x: masterVolume | |
* @return If the string successfully parsed | |
*/ | |
this.setSettings = function(values) | |
{ | |
for ( var i = 0; i < 24; i++ ) | |
{ | |
this[String.fromCharCode( 97 + i )] = values[i] || 0; | |
} | |
// I moved this here from the reset(true) function | |
if (this['c'] < .01) { | |
this['c'] = .01; | |
} | |
var totalTime = this['b'] + this['c'] + this['e']; | |
if (totalTime < .18) { | |
var multiplier = .18 / totalTime; | |
this['b'] *= multiplier; | |
this['c'] *= multiplier; | |
this['e'] *= multiplier; | |
} | |
} | |
} | |
/** | |
* SfxrSynth | |
* | |
* Copyright 2010 Thomas Vian | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
* | |
* @author Thomas Vian | |
*/ | |
/** @constructor */ | |
function SfxrSynth() { | |
// All variables are kept alive through function closures | |
//-------------------------------------------------------------------------- | |
// | |
// Sound Parameters | |
// | |
//-------------------------------------------------------------------------- | |
this._params = new SfxrParams(); // Params instance | |
//-------------------------------------------------------------------------- | |
// | |
// Synth Variables | |
// | |
//-------------------------------------------------------------------------- | |
var _envelopeLength0, // Length of the attack stage | |
_envelopeLength1, // Length of the sustain stage | |
_envelopeLength2, // Length of the decay stage | |
_period, // Period of the wave | |
_maxPeriod, // Maximum period before sound stops (from minFrequency) | |
_slide, // Note slide | |
_deltaSlide, // Change in slide | |
_changeAmount, // Amount to change the note by | |
_changeTime, // Counter for the note change | |
_changeLimit, // Once the time reaches this limit, the note changes | |
_squareDuty, // Offset of center switching point in the square wave | |
_dutySweep; // Amount to change the duty by | |
//-------------------------------------------------------------------------- | |
// | |
// Synth Methods | |
// | |
//-------------------------------------------------------------------------- | |
/** | |
* Resets the runing variables from the params | |
* Used once at the start (total reset) and for the repeat effect (partial reset) | |
*/ | |
this.reset = function() { | |
// Shorter reference | |
var p = this._params; | |
_period = 100 / (p['f'] * p['f'] + .001); | |
_maxPeriod = 100 / (p['g'] * p['g'] + .001); | |
_slide = 1 - p['h'] * p['h'] * p['h'] * .01; | |
_deltaSlide = -p['i'] * p['i'] * p['i'] * .000001; | |
if (!p['a']) { | |
_squareDuty = .5 - p['n'] / 2; | |
_dutySweep = -p['o'] * .00005; | |
} | |
_changeAmount = 1 + p['l'] * p['l'] * (p['l'] > 0 ? -.9 : 10); | |
_changeTime = 0; | |
_changeLimit = p['m'] == 1 ? 0 : (1 - p['m']) * (1 - p['m']) * 20000 + 32; | |
} | |
// I split the reset() function into two functions for better readability | |
this.totalReset = function() { | |
this.reset(); | |
// Shorter reference | |
var p = this._params; | |
// Calculating the length is all that remained here, everything else moved somewhere | |
_envelopeLength0 = p['b'] * p['b'] * 100000; | |
_envelopeLength1 = p['c'] * p['c'] * 100000; | |
_envelopeLength2 = p['e'] * p['e'] * 100000 + 12; | |
// Full length of the volume envelop (and therefore sound) | |
// Make sure the length can be divided by 3 so we will not need the padding "==" after base64 encode | |
return ((_envelopeLength0 + _envelopeLength1 + _envelopeLength2) / 3 | 0) * 3; | |
} | |
/** | |
* Writes the wave to the supplied buffer ByteArray | |
* @param buffer A ByteArray to write the wave to | |
* @return If the wave is finished | |
*/ | |
this.synthWave = function(buffer, length) { | |
// Shorter reference | |
var p = this._params; | |
// If the filters are active | |
var _filters = p['s'] != 1 || p['v'], | |
// Cutoff multiplier which adjusts the amount the wave position can move | |
_hpFilterCutoff = p['v'] * p['v'] * .1, | |
// Speed of the high-pass cutoff multiplier | |
_hpFilterDeltaCutoff = 1 + p['w'] * .0003, | |
// Cutoff multiplier which adjusts the amount the wave position can move | |
_lpFilterCutoff = p['s'] * p['s'] * p['s'] * .1, | |
// Speed of the low-pass cutoff multiplier | |
_lpFilterDeltaCutoff = 1 + p['t'] * .0001, | |
// If the low pass filter is active | |
_lpFilterOn = p['s'] != 1, | |
// masterVolume * masterVolume (for quick calculations) | |
_masterVolume = p['x'] * p['x'], | |
// Minimum frequency before stopping | |
_minFreqency = p['g'], | |
// If the phaser is active | |
_phaser = p['q'] || p['r'], | |
// Change in phase offset | |
_phaserDeltaOffset = p['r'] * p['r'] * p['r'] * .2, | |
// Phase offset for phaser effect | |
_phaserOffset = p['q'] * p['q'] * (p['q'] < 0 ? -1020 : 1020), | |
// Once the time reaches this limit, some of the iables are reset | |
_repeatLimit = p['p'] ? ((1 - p['p']) * (1 - p['p']) * 20000 | 0) + 32 : 0, | |
// The punch factor (louder at begining of sustain) | |
_sustainPunch = p['d'], | |
// Amount to change the period of the wave by at the peak of the vibrato wave | |
_vibratoAmplitude = p['j'] / 2, | |
// Speed at which the vibrato phase moves | |
_vibratoSpeed = p['k'] * p['k'] * .01, | |
// The type of wave to generate | |
_waveType = p['a']; | |
var _envelopeLength = _envelopeLength0, // Length of the current envelope stage | |
_envelopeOverLength0 = 1 / _envelopeLength0, // (for quick calculations) | |
_envelopeOverLength1 = 1 / _envelopeLength1, // (for quick calculations) | |
_envelopeOverLength2 = 1 / _envelopeLength2; // (for quick calculations) | |
// Damping muliplier which restricts how fast the wave position can move | |
var _lpFilterDamping = 5 / (1 + p['u'] * p['u'] * 20) * (.01 + _lpFilterCutoff); | |
if (_lpFilterDamping > .8) { | |
_lpFilterDamping = .8; | |
} | |
_lpFilterDamping = 1 - _lpFilterDamping; | |
var _finished = false, // If the sound has finished | |
_envelopeStage = 0, // Current stage of the envelope (attack, sustain, decay, end) | |
_envelopeTime = 0, // Current time through current enelope stage | |
_envelopeVolume = 0, // Current volume of the envelope | |
_hpFilterPos = 0, // Adjusted wave position after high-pass filter | |
_lpFilterDeltaPos = 0, // Change in low-pass wave position, as allowed by the cutoff and damping | |
_lpFilterOldPos, // Previous low-pass wave position | |
_lpFilterPos = 0, // Adjusted wave position after low-pass filter | |
_periodTemp, // Period modified by vibrato | |
_phase = 0, // Phase through the wave | |
_phaserInt, // Integer phaser offset, for bit maths | |
_phaserPos = 0, // Position through the phaser buffer | |
_pos, // Phase expresed as a Number from 0-1, used for fast sin approx | |
_repeatTime = 0, // Counter for the repeats | |
_sample, // Sub-sample calculated 8 times per actual sample, averaged out to get the super sample | |
_superSample, // Actual sample writen to the wave | |
_vibratoPhase = 0; // Phase through the vibrato sine wave | |
// Buffer of wave values used to create the out of phase second wave | |
var _phaserBuffer = new Array(1024), | |
// Buffer of random values used to generate noise | |
_noiseBuffer = new Array(32); | |
for (var i = _phaserBuffer.length; i--; ) { | |
_phaserBuffer[i] = 0; | |
} | |
for (var i = _noiseBuffer.length; i--; ) { | |
_noiseBuffer[i] = Math.random() * 2 - 1; | |
} | |
for (var i = 0; i < length; i++) { | |
if (_finished) { | |
return i; | |
} | |
// Repeats every _repeatLimit times, partially resetting the sound parameters | |
if (_repeatLimit) { | |
if (++_repeatTime >= _repeatLimit) { | |
_repeatTime = 0; | |
this.reset(); | |
} | |
} | |
// If _changeLimit is reached, shifts the pitch | |
if (_changeLimit) { | |
if (++_changeTime >= _changeLimit) { | |
_changeLimit = 0; | |
_period *= _changeAmount; | |
} | |
} | |
// Acccelerate and apply slide | |
_slide += _deltaSlide; | |
_period *= _slide; | |
// Checks for frequency getting too low, and stops the sound if a minFrequency was set | |
if (_period > _maxPeriod) { | |
_period = _maxPeriod; | |
if (_minFreqency > 0) { | |
_finished = true; | |
} | |
} | |
_periodTemp = _period; | |
// Applies the vibrato effect | |
if (_vibratoAmplitude > 0) { | |
_vibratoPhase += _vibratoSpeed; | |
_periodTemp *= 1 + Math.sin(_vibratoPhase) * _vibratoAmplitude; | |
} | |
_periodTemp |= 0; | |
if (_periodTemp < 8) { | |
_periodTemp = 8; | |
} | |
// Sweeps the square duty | |
if (!_waveType) { | |
_squareDuty += _dutySweep; | |
if (_squareDuty < 0) { | |
_squareDuty = 0; | |
} else if (_squareDuty > .5) { | |
_squareDuty = .5; | |
} | |
} | |
// Moves through the different stages of the volume envelope | |
if (++_envelopeTime > _envelopeLength) { | |
_envelopeTime = 0; | |
switch (++_envelopeStage) { | |
case 1: | |
_envelopeLength = _envelopeLength1; | |
break; | |
case 2: | |
_envelopeLength = _envelopeLength2; | |
} | |
} | |
// Sets the volume based on the position in the envelope | |
switch (_envelopeStage) { | |
case 0: | |
_envelopeVolume = _envelopeTime * _envelopeOverLength0; | |
break; | |
case 1: | |
_envelopeVolume = 1 + (1 - _envelopeTime * _envelopeOverLength1) * 2 * _sustainPunch; | |
break; | |
case 2: | |
_envelopeVolume = 1 - _envelopeTime * _envelopeOverLength2; | |
break; | |
case 3: | |
_envelopeVolume = 0; | |
_finished = true; | |
} | |
// Moves the phaser offset | |
if (_phaser) { | |
_phaserOffset += _phaserDeltaOffset; | |
_phaserInt = _phaserOffset | 0; | |
if (_phaserInt < 0) { | |
_phaserInt = -_phaserInt; | |
} else if (_phaserInt > 1023) { | |
_phaserInt = 1023; | |
} | |
} | |
// Moves the high-pass filter cutoff | |
if (_filters && _hpFilterDeltaCutoff) { | |
_hpFilterCutoff *= _hpFilterDeltaCutoff; | |
if (_hpFilterCutoff < .00001) { | |
_hpFilterCutoff = .00001; | |
} else if (_hpFilterCutoff > .1) { | |
_hpFilterCutoff = .1; | |
} | |
} | |
_superSample = 0; | |
for (var j = 8; j--; ) { | |
// Cycles through the period | |
_phase++; | |
if (_phase >= _periodTemp) { | |
_phase %= _periodTemp; | |
// Generates new random noise for this period | |
if (_waveType == 3) { | |
for (var n = _noiseBuffer.length; n--; ) { | |
_noiseBuffer[n] = Math.random() * 2 - 1; | |
} | |
} | |
} | |
// Gets the sample from the oscillator | |
switch (_waveType) { | |
case 0: // Square wave | |
_sample = ((_phase / _periodTemp) < _squareDuty) ? .5 : -.5; | |
break; | |
case 1: // Saw wave | |
_sample = 1 - _phase / _periodTemp * 2; | |
break; | |
case 2: // Sine wave (fast and accurate approx) | |
_pos = _phase / _periodTemp; | |
_pos = (_pos > .5 ? _pos - 1 : _pos) * 6.28318531; | |
_sample = 1.27323954 * _pos + .405284735 * _pos * _pos * (_pos < 0 ? 1 : -1); | |
_sample = .225 * ((_sample < 0 ? -1 : 1) * _sample * _sample - _sample) + _sample; | |
break; | |
case 3: // Noise | |
_sample = _noiseBuffer[Math.abs(_phase * 32 / _periodTemp | 0)]; | |
} | |
// Applies the low and high pass filters | |
if (_filters) { | |
_lpFilterOldPos = _lpFilterPos; | |
_lpFilterCutoff *= _lpFilterDeltaCutoff; | |
if (_lpFilterCutoff < 0) { | |
_lpFilterCutoff = 0; | |
} else if (_lpFilterCutoff > .1) { | |
_lpFilterCutoff = .1; | |
} | |
if (_lpFilterOn) { | |
_lpFilterDeltaPos += (_sample - _lpFilterPos) * _lpFilterCutoff; | |
_lpFilterDeltaPos *= _lpFilterDamping; | |
} else { | |
_lpFilterPos = _sample; | |
_lpFilterDeltaPos = 0; | |
} | |
_lpFilterPos += _lpFilterDeltaPos; | |
_hpFilterPos += _lpFilterPos - _lpFilterOldPos; | |
_hpFilterPos *= 1 - _hpFilterCutoff; | |
_sample = _hpFilterPos; | |
} | |
// Applies the phaser effect | |
if (_phaser) { | |
_phaserBuffer[_phaserPos % 1024] = _sample; | |
_sample += _phaserBuffer[(_phaserPos - _phaserInt + 1024) % 1024]; | |
_phaserPos++; | |
} | |
_superSample += _sample; | |
} | |
// Averages out the super samples and applies volumes | |
_superSample *= .125 * _envelopeVolume * _masterVolume; | |
// Clipping if too loud | |
buffer[i] = _superSample >= 1 ? 32767 : _superSample <= -1 ? -32768 : _superSample * 32767 | 0; | |
} | |
return length; | |
} | |
} | |
// Adapted from http://codebase.es/riffwave/ | |
var synth = new SfxrSynth(); | |
// Export for the Closure Compiler | |
const jsfxr = function(settings) { | |
// Initialize SfxrParams | |
synth._params.setSettings(settings); | |
// Synthesize Wave | |
var envelopeFullLength = synth.totalReset(); | |
var data = new Uint8Array(((envelopeFullLength + 1) / 2 | 0) * 4 + 44); | |
var used = synth.synthWave(new Uint16Array(data.buffer, 44), envelopeFullLength) * 2; | |
var dv = new Uint32Array(data.buffer, 0, 44); | |
// Initialize header | |
dv[0] = 0x46464952; // "RIFF" | |
dv[1] = used + 36; // put total size here | |
dv[2] = 0x45564157; // "WAVE" | |
dv[3] = 0x20746D66; // "fmt " | |
dv[4] = 0x00000010; // size of the following | |
dv[5] = 0x00010001; // Mono: 1 channel, PCM format | |
dv[6] = 0x0000AC44; // 44,100 samples per second | |
dv[7] = 0x00015888; // byte rate: two bytes per sample | |
dv[8] = 0x00100002; // 16 bits per sample, aligned on every two bytes | |
dv[9] = 0x61746164; // "data" | |
dv[10] = used; // put number of samples here | |
return dv.buffer; | |
// Base64 encoding written by me, @maettig | |
// used += 44; | |
// var i = 0, | |
// base64Characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', | |
// output = 'data:audio/wav;base64,'; | |
// for (; i < used; i += 3) | |
// { | |
// var a = data[i] << 16 | data[i + 1] << 8 | data[i + 2]; | |
// output += base64Characters[a >> 18] + base64Characters[a >> 12 & 63] + base64Characters[a >> 6 & 63] + base64Characters[a & 63]; | |
// } | |
// return output; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment