Last active
November 1, 2016 05:00
-
-
Save patrickroberts/3b065ab94ce5094baacf45ed23e2a16e to your computer and use it in GitHub Desktop.
WAV music synthesizer class
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
'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; | |
}()); |
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
'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