Skip to content

Instantly share code, notes, and snippets.

@patrickroberts
Last active November 1, 2016 05:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save patrickroberts/3b065ab94ce5094baacf45ed23e2a16e to your computer and use it in GitHub Desktop.
Save patrickroberts/3b065ab94ce5094baacf45ed23e2a16e to your computer and use it in GitHub Desktop.
WAV music synthesizer class
'use strict';
(function () {
WAV.frequency = function frequency(note) {
var map = {
'REST': 0,
'A0': 27.5,
'A0#': 29.135,
'B0b': 29.135,
'B0': 30.868,
'C1b': 30.868,
'C1': 32.703,
'C1#': 34.648,
'D1b': 34.648,
'D1': 36.708,
'D1#': 38.891,
'E1b': 38.891,
'E1': 41.203,
'F1b': 41.203,
'F1': 43.654,
'F1#': 46.249,
'G1b': 46.249,
'G1': 48.999,
'G1#': 51.913,
'A1b': 51.913,
'A1': 55,
'A1#': 58.27,
'B1b': 58.27,
'B1': 61.736,
'C2b': 61.736,
'C2': 65.406,
'C2#': 69.296,
'D2b': 69.296,
'D2': 73.416,
'D2#': 77.782,
'E2b': 77.782,
'E2': 82.406,
'F2b': 82.406,
'F2': 87.308,
'F2#': 92.498,
'G2b': 92.498,
'G2': 97.998,
'G2#': 103.826,
'A2b': 103.826,
'A2': 110,
'A2#': 116.54,
'B2b': 116.54,
'B2': 123.472,
'C3b': 123.472,
'C3': 130.812,
'C3#': 138.592,
'D3b': 138.592,
'D3': 146.832,
'D3#': 155.564,
'E3b': 155.564,
'E3': 164.812,
'F3b': 164.812,
'F3': 174.616,
'F3#': 184.996,
'G3b': 184.996,
'G3': 195.996,
'G3#': 207.652,
'A3b': 207.652,
'A3': 220,
'A3#': 233.08,
'B3b': 233.08,
'B3': 246.944,
'C4b': 246.944,
'C4': 261.624,
'C4#': 277.184,
'D4b': 277.184,
'D4': 293.664,
'D4#': 311.128,
'E4b': 311.128,
'E4': 329.624,
'F4b': 329.624,
'F4': 349.232,
'F4#': 369.992,
'G4b': 369.992,
'G4': 391.992,
'G4#': 415.304,
'A4b': 415.304,
'A4': 440,
'A4#': 466.16,
'B4b': 466.16,
'B4': 493.888,
'C5b': 493.888,
'C5': 523.248,
'C5#': 554.368,
'D5b': 554.368,
'D5': 587.328,
'D5#': 622.256,
'E5b': 622.256,
'E5': 659.248,
'F5b': 659.248,
'F5': 698.464,
'F5#': 739.984,
'G5b': 739.984,
'G5': 783.984,
'G5#': 830.608,
'A5b': 830.608,
'A5': 880,
'A5#': 932.32,
'B5b': 932.32,
'B5': 987.776,
'C6b': 987.776,
'C6': 1046.496,
'C6#': 1108.736,
'D6b': 1108.736,
'D6': 1174.656,
'D6#': 1244.512,
'E6b': 1244.512,
'E6': 1318.496,
'F6b': 1318.496,
'F6': 1396.928,
'F6#': 1479.968,
'G6b': 1479.968,
'G6': 1567.968,
'G6#': 1661.216,
'A6b': 1661.216,
'A6': 1760,
'A6#': 1864.64,
'B6b': 1864.64,
'B6': 1975.552,
'C7b': 1975.552,
'C7': 2092.992,
'C7#': 2217.472,
'D7b': 2217.472,
'D7': 2349.312,
'D7#': 2489.024,
'E7b': 2489.024,
'E7': 2636.992,
'F7b': 2636.992,
'F7': 2793.856,
'F7#': 2959.936,
'G7b': 2959.936,
'G7': 3135.936,
'G7#': 3322.432,
'A7b': 3322.432,
'A7': 3520,
'A7#': 3729.28,
'B7b': 3729.28,
'B7': 3951.104,
'C8b': 3951.104,
'C8': 4185.984
};
return map[note];
};
function WAV() {
var numChannels = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 1;
var sampleRate = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 44100;
var data = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [];
var bitsPerSample = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 16;
var littleEndian = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : true;
// WAV header is always 44 bytes
this.header = new ArrayBuffer(44);
// flexible container for reading / writing raw bytes in header
this.view = new DataView(this.header);
// leave sound data as non typed array for more flexibility
this.data = data;
// initialize as non-configurable because it
// causes script to freeze when using parsed
// chunk sizes with wrong endianess assumed
Object.defineProperty(this, 'littleEndian', {
configurable: false,
enumerable: true,
value: littleEndian,
writable: false
});
// initial write index in data array
this.pointer = 0;
// WAV header properties
this.ChunkID = littleEndian ? 'RIFF' : 'RIFX';
this.ChunkSize = this.header.byteLength - 8;
this.Format = 'WAVE';
this.SubChunk1ID = 'fmt ';
this.SubChunk1Size = 16;
this.AudioFormat = 1;
this.NumChannels = numChannels;
this.SampleRate = sampleRate;
this.ByteRate = numChannels * sampleRate * bitsPerSample >>> 3;
this.BlockAlign = numChannels * bitsPerSample >>> 3;
this.BitsPerSample = bitsPerSample;
this.SubChunk2ID = 'data';
this.SubChunk2Size = data.length * bitsPerSample >>> 3;
}
// internal setter for writing strings as raw bytes to header
WAV.prototype.setString = function setString(str) {
var byteLength = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : str.length;
var byteOffset = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0;
for (var i = 0; i < byteLength; i++) {
this.view.setUint8(byteOffset + i, str.charCodeAt(i));
}
};
// internal getter for reading raw bytes as strings from header
WAV.prototype.getString = function getString(byteLength) {
var byteOffset = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
for (var i = 0, str = ''; i < byteLength; i++) {
str += String.fromCharCode(this.view.getUint8(byteOffset + i));
}
return str;
};
// binary container outputs
// browser-specific
// generates blob from concatenated typed arrays
WAV.prototype.toBlob = function toBlob() {
return new Blob([this.header, this.typedData], { type: 'audio/wav' });
};
// Node.js-specific
// generates buffer from concatenated typed arrays
WAV.prototype.toBuffer = function toBuffer() {
return Buffer.concat([Buffer.from(this.header), Buffer.from(this.typedData)]);
};
// sound data mutators
// writes the specified note to the sound data
// for amount of time in seconds
// at given normalized amplitude
// to channels listed (or all by default)
// adds to existing data by default
// and does not reset write index after operation by default
WAV.prototype.addNote = function addNote(_ref) {
var note = _ref.note,
time = _ref.time;
var amplitude = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1;
var channels = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [];
var blend = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true;
var reset = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
// creating local references to properties
var data = this.data;
var numChannels = this.NumChannels;
var sampleRate = this.SampleRate;
var bitsPerSample = this.BitsPerSample;
var i;
// by default write to all channels
if (channels.length === 0) {
for (i = 0; i < numChannels; i++) {
channels[i] = i;
}
}
// inline .indexOf() function calls to array references
var skipChannel = [];
for (i = 0; i < numChannels; i++) {
skipChannel[i] = channels.indexOf(i) === -1;
}
// calculating properties of given note
var frequency = WAV.frequency(note) * Math.PI * 2 / sampleRate;
var period = Math.PI * 2 / frequency;
// amount of blocks to be written
var blocksOut = Math.round(sampleRate * time);
// reduces sound artifacts by stopping at last block
// where sine wave is approximately 0
var nonZero = Math.round(blocksOut / period) * period;
// index of start and stop samples
var start = this.pointer;
var stop = data.length;
// determines amount of blocks to be updated
var blocksIn = Math.min(Math.floor((stop - start) / numChannels), blocksOut);
// i = index of each sample block
i = 0;
// j = index of each channel in block
// d = sample data value
var j, d;
// update existing data
if (blend && frequency > 0) {
for (i = 0; i < blocksIn; i++) {
for (j = 0; j < channels.length; j++) {
d = i > nonZero ? 0 : amplitude * Math.sin(frequency * i);
data[start + i * numChannels + channels[j]] += d;
}
}
} else if (blend) {
i = blocksIn;
}
// append or overwrite data
for (; i < blocksOut; i++) {
for (j = 0; j < numChannels; j++) {
d = frequency === 0 || i > nonZero || skipChannel[j] ? 0 : amplitude * Math.sin(frequency * i);
data[start + i * numChannels + j] = d;
}
}
// update header properties
var end = Math.max(start + blocksOut * numChannels, stop) * bitsPerSample >>> 3;
this.ChunkSize = end + this.header.byteLength - 8;
this.SubChunk2Size = end;
if (!reset) {
// move write index to end of written data
this.pointer = start + i * numChannels;
}
};
// adds specified notes in series
// each playing for time * relativeDuration seconds
// followed by a time * (1 - relativeDuration) second rest
WAV.prototype.addProgression = function addProgression(notes) {
var amplitude = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1;
var channels = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [];
var blend = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true;
var reset = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
var relativeDuration = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : 1;
var start = this.pointer;
for (var i = 0, secs, rest; i < notes.length; i++) {
var _notes$i = notes[i],
note = _notes$i.note,
time = _notes$i.time;
if (relativeDuration === 1 || note === 'REST') {
this.addNote(notes[i], amplitude, channels, blend, false);
} else {
secs = time * relativeDuration;
rest = time - secs;
this.addNote({ note: note, time: secs }, amplitude, channels, blend, false);
this.addNote({ note: 'REST', time: rest }, amplitude, channels, blend, false);
}
}
if (reset) {
this.pointer = start;
}
};
// adds specified notes in parallel
// playing for time * relativeDuration seconds
// followed by a time * (1 - relativeDuration) second rest
WAV.prototype.addChord = function addChord(notes) {
var amplitude = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1 / notes.length;
var channels = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [];
var blend = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true;
var reset = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
var relativeDuration = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : 1;
var start = this.pointer;
for (var i = 0, secs, rest; i < notes.length; i++) {
// reset pointer manually at the beginning of each iteration
this.pointer = start;
var _notes$i2 = notes[i],
note = _notes$i2.note,
time = _notes$i2.time;
if (relativeDuration === 1 || note === 'REST') {
this.addNote(notes[i], time, amplitude, channels, blend, false);
} else {
secs = time * relativeDuration;
rest = time - secs;
this.addNote({ note: note, time: secs }, amplitude, channels, blend, false);
this.addNote({ note: 'REST', time: rest }, amplitude, channels, blend, false);
}
// after 1st iteration, notes must blend since they are in parallel
blend = true;
}
if (reset) {
this.pointer = start;
}
};
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
// header property mutators
_createClass(WAV, [{
key: 'ChunkID',
set: function set(str) {
this.setString(str, 4, 0);
},
get: function get() {
return this.getString(4, 0);
}
}, {
key: 'ChunkSize',
set: function set(uint) {
this.view.setUint32(4, uint, this.littleEndian);
},
get: function get() {
return this.view.getUint32(4, this.littleEndian);
}
}, {
key: 'Format',
set: function set(str) {
this.setString(str, 4, 8);
},
get: function get() {
return this.getString(4, 8);
}
}, {
key: 'SubChunk1ID',
set: function set(str) {
this.setString(str, 4, 12);
},
get: function get() {
return this.getString(4, 12);
}
}, {
key: 'SubChunk1Size',
set: function set(uint) {
this.view.setUint32(16, uint, this.littleEndian);
},
get: function get() {
return this.view.getUint32(16, this.littleEndian);
}
}, {
key: 'AudioFormat',
set: function set(uint) {
this.view.setUint16(20, uint, this.littleEndian);
},
get: function get() {
return this.view.getUint16(20, this.littleEndian);
}
}, {
key: 'NumChannels',
set: function set(uint) {
this.view.setUint16(22, uint, this.littleEndian);
},
get: function get() {
return this.view.getUint16(22, this.littleEndian);
}
}, {
key: 'SampleRate',
set: function set(uint) {
this.view.setUint32(24, uint, this.littleEndian);
},
get: function get() {
return this.view.getUint32(24, this.littleEndian);
}
}, {
key: 'ByteRate',
set: function set(uint) {
this.view.setUint32(28, uint, this.littleEndian);
},
get: function get() {
return this.view.getUint32(28, this.littleEndian);
}
}, {
key: 'BlockAlign',
set: function set(uint) {
this.view.setUint16(32, uint, this.littleEndian);
},
get: function get() {
return this.view.getUint16(32, this.littleEndian);
}
}, {
key: 'BitsPerSample',
set: function set(uint) {
this.view.setUint16(34, uint, this.littleEndian);
},
get: function get() {
return this.view.getUint16(34, this.littleEndian);
}
}, {
key: 'SubChunk2ID',
set: function set(str) {
this.setString(str, 4, 36);
},
get: function get() {
return this.getString(4, 36);
}
}, {
key: 'SubChunk2Size',
set: function set(uint) {
this.view.setUint32(40, uint, this.littleEndian);
},
get: function get() {
return this.view.getUint32(40, this.littleEndian);
}
// internal getter for sound data as
// typed array based on header properties
}, {
key: 'typedData',
get: function get() {
var bytesPerSample = this.BitsPerSample >>> 3;
var data = this.data;
var size = this.SubChunk2Size;
var samples = size / bytesPerSample;
var buffer = new ArrayBuffer(size);
var uint8 = new Uint8Array(buffer);
// convert signed normalized sound data to typed integer data
// i.e. [-1, 1] -> [INT_MIN, INT_MAX]
var amplitude = (1 << (bytesPerSample << 3) - 1) - 1;
var i, d;
switch (bytesPerSample) {
case 1:
// endianess not relevant for 8-bit encoding
for (i = 0; i < samples; i++) {
// convert by adding 0x80 instead of 0x100
// WAV uses unsigned data for 8-bit encoding
// [INT8_MIN, INT8_MAX] -> [0, UINT8_MAX]
uint8[i] = data[i] * amplitude + 0x80 & 0xFF;
}
break;
case 2:
// LSB first
if (this.littleEndian) {
for (i = 0; i < samples; i++) {
// [INT16_MIN, INT16_MAX] -> [0, UINT16_MAX]
d = data[i] * amplitude + 0x10000 & 0xFFFF;
// unwrap inner loop
uint8[i * 2] = d & 0xFF;
uint8[i * 2 + 1] = d >>> 8;
}
// MSB first
} else {
for (i = 0; i < samples; i++) {
// [INT16_MIN, INT16_MAX] -> [0, UINT16_MAX]
d = data[i] * amplitude + 0x10000 & 0xFFFF;
// unwrap inner loop
uint8[i * 2] = d >>> 8;
uint8[i * 2 + 1] = d & 0xFF;
}
}
break;
case 4:
// LSB first
if (this.littleEndian) {
for (i = 0; i < samples; i++) {
// [INT32_MIN, INT32_MAX] -> [0, UINT32_MAX]
d(data[i] * amplitude + 0x100000000) & 0xFFFFFFFF;
// unwrap inner loop
uint8[i * 4] = d & 0xFF;
uint8[i * 4 + 1] = d >>> 8 & 0xFF;
uint8[i * 4 + 2] = d >>> 16 & 0xFF;
uint8[i * 4 + 3] = d >>> 24;
}
// MSB first
} else {
for (i = 0; i < samples; i++) {
// [INT32_MIN, INT32_MAX] -> [0, UINT32_MAX]
d(data[i] * amplitude + 0x100000000) & 0xFFFFFFFF;
// unwrap inner loop
uint8[i * 4] = d >>> 24;
uint8[i * 4 + 1] = d >>> 16 & 0xFF;
uint8[i * 4 + 2] = d >>> 8 & 0xFF;
uint8[i * 4 + 3] = d & 0xFF;
}
}
}
return buffer;
}
}]);
// expose non-singleton constructor
window.WAV = WAV;
}());
'use strict';
(function () {
// WAV class
class WAV {
static frequency(note) {
const map = {
'REST': 0,
'A0': 27.5,
'A0#': 29.135,
'B0b': 29.135,
'B0': 30.868,
'C1b': 30.868,
'C1': 32.703,
'C1#': 34.648,
'D1b': 34.648,
'D1': 36.708,
'D1#': 38.891,
'E1b': 38.891,
'E1': 41.203,
'F1b': 41.203,
'F1': 43.654,
'F1#': 46.249,
'G1b': 46.249,
'G1': 48.999,
'G1#': 51.913,
'A1b': 51.913,
'A1': 55,
'A1#': 58.27,
'B1b': 58.27,
'B1': 61.736,
'C2b': 61.736,
'C2': 65.406,
'C2#': 69.296,
'D2b': 69.296,
'D2': 73.416,
'D2#': 77.782,
'E2b': 77.782,
'E2': 82.406,
'F2b': 82.406,
'F2': 87.308,
'F2#': 92.498,
'G2b': 92.498,
'G2': 97.998,
'G2#': 103.826,
'A2b': 103.826,
'A2': 110,
'A2#': 116.54,
'B2b': 116.54,
'B2': 123.472,
'C3b': 123.472,
'C3': 130.812,
'C3#': 138.592,
'D3b': 138.592,
'D3': 146.832,
'D3#': 155.564,
'E3b': 155.564,
'E3': 164.812,
'F3b': 164.812,
'F3': 174.616,
'F3#': 184.996,
'G3b': 184.996,
'G3': 195.996,
'G3#': 207.652,
'A3b': 207.652,
'A3': 220,
'A3#': 233.08,
'B3b': 233.08,
'B3': 246.944,
'C4b': 246.944,
'C4': 261.624,
'C4#': 277.184,
'D4b': 277.184,
'D4': 293.664,
'D4#': 311.128,
'E4b': 311.128,
'E4': 329.624,
'F4b': 329.624,
'F4': 349.232,
'F4#': 369.992,
'G4b': 369.992,
'G4': 391.992,
'G4#': 415.304,
'A4b': 415.304,
'A4': 440,
'A4#': 466.16,
'B4b': 466.16,
'B4': 493.888,
'C5b': 493.888,
'C5': 523.248,
'C5#': 554.368,
'D5b': 554.368,
'D5': 587.328,
'D5#': 622.256,
'E5b': 622.256,
'E5': 659.248,
'F5b': 659.248,
'F5': 698.464,
'F5#': 739.984,
'G5b': 739.984,
'G5': 783.984,
'G5#': 830.608,
'A5b': 830.608,
'A5': 880,
'A5#': 932.32,
'B5b': 932.32,
'B5': 987.776,
'C6b': 987.776,
'C6': 1046.496,
'C6#': 1108.736,
'D6b': 1108.736,
'D6': 1174.656,
'D6#': 1244.512,
'E6b': 1244.512,
'E6': 1318.496,
'F6b': 1318.496,
'F6': 1396.928,
'F6#': 1479.968,
'G6b': 1479.968,
'G6': 1567.968,
'G6#': 1661.216,
'A6b': 1661.216,
'A6': 1760,
'A6#': 1864.64,
'B6b': 1864.64,
'B6': 1975.552,
'C7b': 1975.552,
'C7': 2092.992,
'C7#': 2217.472,
'D7b': 2217.472,
'D7': 2349.312,
'D7#': 2489.024,
'E7b': 2489.024,
'E7': 2636.992,
'F7b': 2636.992,
'F7': 2793.856,
'F7#': 2959.936,
'G7b': 2959.936,
'G7': 3135.936,
'G7#': 3322.432,
'A7b': 3322.432,
'A7': 3520,
'A7#': 3729.28,
'B7b': 3729.28,
'B7': 3951.104,
'C8b': 3951.104,
'C8': 4185.984
};
return map[note];
}
constructor(numChannels = 1, sampleRate = 44100, data = [], bitsPerSample = 16, littleEndian = true) {
// WAV header is always 44 bytes
this.header = new ArrayBuffer(44);
// flexible container for reading / writing raw bytes in header
this.view = new DataView(this.header);
// leave sound data as non typed array for more flexibility
this.data = data;
// initialize as non-configurable because it
// causes script to freeze when using parsed
// chunk sizes with wrong endianess assumed
Object.defineProperty(this, 'littleEndian', {
configurable: false,
enumerable: true,
value: littleEndian,
writable: false
});
// initial write index in data array
this.pointer = 0;
// WAV header properties
this.ChunkID = littleEndian ? 'RIFF' : 'RIFX';
this.ChunkSize = this.header.byteLength - 8;
this.Format = 'WAVE';
this.SubChunk1ID = 'fmt ';
this.SubChunk1Size = 16;
this.AudioFormat = 1;
this.NumChannels = numChannels;
this.SampleRate = sampleRate;
this.ByteRate = numChannels * sampleRate * bitsPerSample >>> 3;
this.BlockAlign = numChannels * bitsPerSample >>> 3;
this.BitsPerSample = bitsPerSample
this.SubChunk2ID = 'data';
this.SubChunk2Size = data.length * bitsPerSample >>> 3;
}
// internal setter for writing strings as raw bytes to header
setString(str, byteLength = str.length, byteOffset = 0) {
for (var i = 0; i < byteLength; i++) {
this.view.setUint8(byteOffset + i, str.charCodeAt(i));
}
}
// internal getter for reading raw bytes as strings from header
getString(byteLength, byteOffset = 0) {
for (var i = 0, str = ''; i < byteLength; i++) {
str += String.fromCharCode(this.view.getUint8(byteOffset + i));
}
return str;
}
// header property mutators
set ChunkID(str) {
this.setString(str, 4, 0);
}
get ChunkID() {
return this.getString(4, 0);
}
set ChunkSize(uint) {
this.view.setUint32(4, uint, this.littleEndian);
}
get ChunkSize() {
return this.view.getUint32(4, this.littleEndian);
}
set Format(str) {
this.setString(str, 4, 8);
}
get Format() {
return this.getString(4, 8);
}
set SubChunk1ID(str) {
this.setString(str, 4, 12);
}
get SubChunk1ID() {
return this.getString(4, 12);
}
set SubChunk1Size(uint) {
this.view.setUint32(16, uint, this.littleEndian);
}
get SubChunk1Size() {
return this.view.getUint32(16, this.littleEndian);
}
set AudioFormat(uint) {
this.view.setUint16(20, uint, this.littleEndian);
}
get AudioFormat() {
return this.view.getUint16(20, this.littleEndian);
}
set NumChannels(uint) {
this.view.setUint16(22, uint, this.littleEndian);
}
get NumChannels() {
return this.view.getUint16(22, this.littleEndian);
}
set SampleRate(uint) {
this.view.setUint32(24, uint, this.littleEndian);
}
get SampleRate() {
return this.view.getUint32(24, this.littleEndian);
}
set ByteRate(uint) {
this.view.setUint32(28, uint, this.littleEndian);
}
get ByteRate() {
return this.view.getUint32(28, this.littleEndian);
}
set BlockAlign(uint) {
this.view.setUint16(32, uint, this.littleEndian);
}
get BlockAlign() {
return this.view.getUint16(32, this.littleEndian);
}
set BitsPerSample(uint) {
this.view.setUint16(34, uint, this.littleEndian);
}
get BitsPerSample() {
return this.view.getUint16(34, this.littleEndian);
}
set SubChunk2ID(str) {
this.setString(str, 4, 36);
}
get SubChunk2ID() {
return this.getString(4, 36);
}
set SubChunk2Size(uint) {
this.view.setUint32(40, uint, this.littleEndian);
}
get SubChunk2Size() {
return this.view.getUint32(40, this.littleEndian);
}
// internal getter for sound data as
// typed array based on header properties
get typedData() {
var bytesPerSample = this.BitsPerSample >>> 3;
var data = this.data;
var size = this.SubChunk2Size;
var samples = size / bytesPerSample;
var buffer = new ArrayBuffer(size);
var uint8 = new Uint8Array(buffer);
// convert signed normalized sound data to typed integer data
// i.e. [-1, 1] -> [INT_MIN, INT_MAX]
var amplitude = (1 << ((bytesPerSample << 3) - 1)) - 1;
var i, d;
switch (bytesPerSample) {
case 1:
// endianess not relevant for 8-bit encoding
for (i = 0; i < samples; i++) {
// convert by adding 0x80 instead of 0x100
// WAV uses unsigned data for 8-bit encoding
// [INT8_MIN, INT8_MAX] -> [0, UINT8_MAX]
uint8[i] = (data[i] * amplitude + 0x80) & 0xFF;
}
break;
case 2:
// LSB first
if (this.littleEndian) {
for (i = 0; i < samples; i++) {
// [INT16_MIN, INT16_MAX] -> [0, UINT16_MAX]
d = (data[i] * amplitude + 0x10000) & 0xFFFF;
// unwrap inner loop
uint8[i * 2 ] = (d ) & 0xFF;
uint8[i * 2 + 1] = (d >>> 8);
}
// MSB first
} else {
for (i = 0; i < samples; i++) {
// [INT16_MIN, INT16_MAX] -> [0, UINT16_MAX]
d = (data[i] * amplitude + 0x10000) & 0xFFFF;
// unwrap inner loop
uint8[i * 2 ] = (d >>> 8);
uint8[i * 2 + 1] = (d ) & 0xFF;
}
}
break;
case 4:
// LSB first
if (this.littleEndian) {
for (i = 0; i < samples; i++) {
// [INT32_MIN, INT32_MAX] -> [0, UINT32_MAX]
d (data[i] * amplitude + 0x100000000) & 0xFFFFFFFF;
// unwrap inner loop
uint8[i * 4 ] = (d ) & 0xFF;
uint8[i * 4 + 1] = (d >>> 8 ) & 0xFF;
uint8[i * 4 + 2] = (d >>> 16) & 0xFF;
uint8[i * 4 + 3] = (d >>> 24);
}
// MSB first
} else {
for (i = 0; i < samples; i++) {
// [INT32_MIN, INT32_MAX] -> [0, UINT32_MAX]
d (data[i] * amplitude + 0x100000000) & 0xFFFFFFFF;
// unwrap inner loop
uint8[i * 4 ] = (d >>> 24);
uint8[i * 4 + 1] = (d >>> 16) & 0xFF;
uint8[i * 4 + 2] = (d >>> 8 ) & 0xFF;
uint8[i * 4 + 3] = (d ) & 0xFF;
}
}
}
return buffer;
}
// binary container outputs
// browser-specific
// generates blob from concatenated typed arrays
toBlob() {
return new Blob([this.header, this.typedData], {type: 'audio/wav'});
}
// Node.js-specific
// generates buffer from concatenated typed arrays
toBuffer() {
return Buffer.concat([Buffer.from(this.header), Buffer.from(this.typedData)]);
}
// sound data mutators
// writes the specified note to the sound data
// for amount of time in seconds
// at given normalized amplitude
// to channels listed (or all by default)
// adds to existing data by default
// and does not reset write index after operation by default
addNote({note, time}, amplitude = 1, channels = [], blend = true, reset = false) {
// creating local references to properties
var data = this.data;
var numChannels = this.NumChannels;
var sampleRate = this.SampleRate;
var bitsPerSample = this.BitsPerSample;
var i;
// by default write to all channels
if (channels.length === 0) {
for (i = 0; i < numChannels; i++) {
channels[i] = i;
}
}
// inline .indexOf() function calls to array references
var skipChannel = [];
for (i = 0; i < numChannels; i++) {
skipChannel[i] = (channels.indexOf(i) === -1);
}
// calculating properties of given note
var frequency = WAV.frequency(note) * Math.PI * 2 / sampleRate;
var period = Math.PI * 2 / frequency;
// amount of blocks to be written
var blocksOut = Math.round(sampleRate * time);
// reduces sound artifacts by stopping at last block
// where sine wave is approximately 0
var nonZero = Math.round(blocksOut / period) * period;
// index of start and stop samples
var start = this.pointer;
var stop = data.length;
// determines amount of blocks to be updated
var blocksIn = Math.min(Math.floor((stop - start) / numChannels), blocksOut);
// i = index of each sample block
i = 0;
// j = index of each channel in block
// d = sample data value
var j, d;
// update existing data
if (blend && frequency > 0) {
for (i = 0; i < blocksIn; i++) {
for (j = 0; j < channels.length; j++) {
d = (i > nonZero) ? 0 : amplitude * Math.sin(frequency * i);
data[start + i * numChannels + channels[j]] += d;
}
}
} else if (blend) {
i = blocksIn;
}
// append or overwrite data
for (; i < blocksOut; i++) {
for (j = 0; j < numChannels; j++) {
d = (frequency === 0 || i > nonZero || skipChannel[j]) ? 0 : amplitude * Math.sin(frequency * i);
data[start + i * numChannels + j] = d;
}
}
// update header properties
var end = Math.max(start + blocksOut * numChannels, stop) * bitsPerSample >>> 3;
this.ChunkSize = end + this.header.byteLength - 8;
this.SubChunk2Size = end;
if (!reset) {
// move write index to end of written data
this.pointer = start + i * numChannels;
}
}
// adds specified notes in series
// each playing for time * relativeDuration seconds
// followed by a time * (1 - relativeDuration) second rest
addProgression(notes, amplitude = 1, channels = [], blend = true, reset = false, relativeDuration = 1) {
var start = this.pointer;
for (var i = 0, secs, rest; i < notes.length; i++) {
let {note, time} = notes[i];
if (relativeDuration === 1 || note === 'REST') {
this.addNote(notes[i], amplitude, channels, blend, false);
} else {
secs = time * relativeDuration;
rest = time - secs;
this.addNote({note, time: secs}, amplitude, channels, blend, false);
this.addNote({note: 'REST', time: rest}, amplitude, channels, blend, false);
}
}
if (reset) {
this.pointer = start;
}
}
// adds specified notes in parallel
// playing for time * relativeDuration seconds
// followed by a time * (1 - relativeDuration) second rest
addChord(notes, amplitude = 1 / notes.length, channels = [], blend = true, reset = false, relativeDuration = 1) {
var start = this.pointer;
for (var i = 0, secs, rest; i < notes.length; i++) {
// reset pointer manually at the beginning of each iteration
this.pointer = start;
let {note, time} = notes[i];
if (relativeDuration === 1 || note === 'REST') {
this.addNote(notes[i], time, amplitude, channels, blend, false);
} else {
secs = time * relativeDuration;
rest = time - secs;
this.addNote({note, time: secs}, amplitude, channels, blend, false);
this.addNote({note: 'REST', time: rest}, amplitude, channels, blend, false);
}
// after 1st iteration, notes must blend since they are in parallel
blend = true;
}
if (reset) {
this.pointer = start;
}
}
}
// expose non-singleton constructor
window.WAV = WAV;
}());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment