Skip to content

Instantly share code, notes, and snippets.

@sayjeyhi
Created November 30, 2021 21:37
Show Gist options
  • Save sayjeyhi/158555da01f6dcc969caecdbc3e2a938 to your computer and use it in GitHub Desktop.
Save sayjeyhi/158555da01f6dcc969caecdbc3e2a938 to your computer and use it in GitHub Desktop.
Create a wav, from raw data
'use strict';
export default class WAV {
private readonly header: ArrayBuffer;
private readonly data: Uint8Array;
private view: DataView;
private pointer: number;
private littleEndian: boolean | undefined;
static semitone(note = 'REST') {
// matches occurrence of A through G
// followed by positive or negative integer
// followed by 0 to 2 occurrences of flat or sharp
const re = /^([A-G])(-?\d+)(b{0,2}|#{0,2})$/;
// if semitone is unrecognized, assume REST
if (!re.test(note)) {
return -Infinity;
}
// parse substrings of note
const [, tone, octave, accidental] = note.match(re) || [];
// semitone indexed relative to A4 == 69 for compatibility with MIDI
const tones: Record<string, number> = { C: 0, D: 2, E: 4, F: 5, G: 7, A: 9, B: 11 };
const octaves: Record<string | number, number> = {
'-1': 0,
0: 1,
1: 2,
2: 3,
3: 4,
4: 5,
5: 6,
6: 7,
7: 8,
8: 9,
9: 10,
10: 11
};
const accidentals: Record<string | number, number> = {
bb: -2,
b: -1,
'': 0,
'#': 1,
'##': 2
};
// if semitone is unrecognized, assume REST
if (
tones[tone] === undefined ||
octaves[octave] === undefined ||
accidentals[accidental] === undefined
) {
return -Infinity;
}
// return calculated index
return tones[tone] + octaves[octave] * 12 + accidentals[accidental];
}
static note(semitone = -Infinity) {
const octaves = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const tones = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
const octaveIndex = Math.floor(semitone / 12);
const toneIndex = Math.floor(semitone - octaveIndex * 12);
const octave = octaves[octaveIndex];
const tone = tones[toneIndex];
// by default assume REST
if (octave === undefined || tone === undefined) {
return 'REST';
}
// tone followed by octave followed by accidental
return tone.charAt(0) + octave.toString() + tone.charAt(1);
}
// converts semitone index to frequency in Hz
static frequency(semitone = -Infinity) {
// A4 is 440 Hz, 12 semitones per octave
return 440 * Math.pow(2, (semitone - 69) / 12);
}
constructor(
data: Uint8Array,
numChannels = 1,
sampleRate = 44100,
bitsPerSample = 16,
littleEndian = true
) {
const bytesPerSample = bitsPerSample >>> 3;
// 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 endianness 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 * bytesPerSample;
this.BlockAlign = numChannels * bytesPerSample;
this.BitsPerSample = bitsPerSample;
this.SubChunk2ID = 'data';
this.SubChunk2Size = data.length * bytesPerSample;
}
// internal setter for writing strings as raw bytes to header
setString(str: string, byteLength = str.length, byteOffset = 0) {
for (let 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: number, byteOffset = 0) {
let str = '';
for (let i = 0; i < byteLength; i++) {
str += String.fromCharCode(this.view.getUint8(byteOffset + i));
}
return str;
}
// header property mutators
// 4 bytes at offset of 0 bytes
set ChunkID(str) {
this.setString(str, 4, 0);
}
get ChunkID() {
return this.getString(4, 0);
}
// 4 bytes at offset of 4 bytes
set ChunkSize(uint) {
this.view.setUint32(4, uint, this.littleEndian);
}
get ChunkSize() {
return this.view.getUint32(4, this.littleEndian);
}
// 4 bytes at offset of 8 bytes
set Format(str) {
this.setString(str, 4, 8);
}
get Format() {
return this.getString(4, 8);
}
// 4 bytes at offset of 12 bytes
set SubChunk1ID(str) {
this.setString(str, 4, 12);
}
get SubChunk1ID() {
return this.getString(4, 12);
}
// 4 bytes at offset of 16 bytes
set SubChunk1Size(uint) {
this.view.setUint32(16, uint, this.littleEndian);
}
get SubChunk1Size() {
return this.view.getUint32(16, this.littleEndian);
}
// 2 bytes at offset of 20 bytes
set AudioFormat(uint) {
this.view.setUint16(20, uint, this.littleEndian);
}
get AudioFormat() {
return this.view.getUint16(20, this.littleEndian);
}
// 2 bytes at offset of 22 bytes
set NumChannels(uint) {
this.view.setUint16(22, uint, this.littleEndian);
}
get NumChannels() {
return this.view.getUint16(22, this.littleEndian);
}
// 4 bytes at offset of 24 bytes
set SampleRate(uint) {
this.view.setUint32(24, uint, this.littleEndian);
}
get SampleRate() {
return this.view.getUint32(24, this.littleEndian);
}
// 4 bytes at offset of 28 bytes
set ByteRate(uint) {
this.view.setUint32(28, uint, this.littleEndian);
}
get ByteRate() {
return this.view.getUint32(28, this.littleEndian);
}
// 2 bytes at offset of 32 bytes
set BlockAlign(uint) {
this.view.setUint16(32, uint, this.littleEndian);
}
get BlockAlign() {
return this.view.getUint16(32, this.littleEndian);
}
// 2 bytes at offset of 34 bytes
set BitsPerSample(uint) {
this.view.setUint16(34, uint, this.littleEndian);
}
get BitsPerSample() {
return this.view.getUint16(34, this.littleEndian);
}
// 4 bytes at offset of 36 bytes
set SubChunk2ID(str) {
this.setString(str, 4, 36);
}
get SubChunk2ID() {
return this.getString(4, 36);
}
// 4 bytes at offset of 40 bytes
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() {
const bytesPerSample = this.BitsPerSample >>> 3;
console.log('WAV:bytesPerSample: ', bytesPerSample);
const data = this.data;
const size = this.SubChunk2Size;
console.log('WAV: size: ', size);
const samples = size / bytesPerSample;
console.log('WAV: samples: ', samples);
const buffer = new ArrayBuffer(size);
const uint8 = new Uint8Array(buffer);
// convert signed normalized sound data to typed integer data
// i.e. [-1, 1] -> [INT_MIN, INT_MAX]
const amplitude = Math.pow(2, (bytesPerSample << 3) - 1) - 1;
let i, d;
switch (bytesPerSample) {
case 1:
// endianness 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 3:
// LSB first
if (this.littleEndian) {
for (i = 0; i < samples; i++) {
// [INT24_MIN, INT24_MAX] -> [0, UINT24_MAX]
d = (data[i] * amplitude + 0x1000000) & 0xffffff;
// unwrap inner loop
uint8[i * 3] = d & 0xff;
uint8[i * 3 + 1] = (d >>> 8) & 0xff;
uint8[i * 3 + 2] = d >>> 16;
}
// MSB first
} else {
for (i = 0; i < samples; i++) {
// [INT24_MIN, INT24_MAX] -> [0, UINT24_MAX]
d = (data[i] * amplitude + 0x1000000) & 0xffffff;
// unwrap inner loop
uint8[i * 3] = d >>> 16;
uint8[i * 3 + 1] = (d >>> 8) & 0xff;
uint8[i * 3 + 2] = d & 0xff;
}
}
// eslint-disable-next-line no-fallthrough
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.data], { type: 'audio/wav' });
}
// Node.js-specific
// generates buffer from concatenated typed arrays
toBuffer() {
// return Buffer.concat([Buffer.from(this.header), Buffer.from(this.typedData)]);
const tmp = new Uint8Array(this.header.byteLength + this.typedData.byteLength);
tmp.set(new Uint8Array(this.header), 0);
tmp.set(new Uint8Array(this.typedData), this.header.byteLength);
return tmp;
}
// pointer mutators
// gets time (in seconds) of pointer
tell() {
return this.pointer / this.NumChannels / this.SampleRate;
}
// sets time (in seconds) of pointer
// zero-fills by default
seek(time: number, fill = true) {
const data = this.data;
const sample = Math.round(this.SampleRate * time);
this.pointer = this.NumChannels * sample;
if (fill) {
// zero-fill seek
while (data.length < this.pointer) {
data[data.length] = 0;
}
} else {
this.pointer = data.length;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment