Skip to content

Instantly share code, notes, and snippets.

@Belrestro
Created February 12, 2018 16:02
Show Gist options
  • Save Belrestro/341ba8fab03bb46b1de377dedb923412 to your computer and use it in GitHub Desktop.
Save Belrestro/341ba8fab03bb46b1de377dedb923412 to your computer and use it in GitHub Desktop.
export class Decoder {
getEmptyChunk () {
return {
name: '',
length: 0
};
}
readString (data, offset, length) {
return data.slice(offset, offset + length);
}
readIntL (data, offset, length) {
let value = 0;
for (let i = 0; i < length; i++) {
value = value + ((data.charCodeAt(offset + i) & 0xFF) * Math.pow(2, 8 * i));
}
return value;
}
readChunkHeaderL (data, offset) {
const chunk = this.getEmptyChunk();
chunk.name = this.readString(data, offset, 4);
chunk.length = this.readIntL(data, offset + 4, 4);
return chunk;
}
readIntB (data, offset, length) {
let value = 0;
for (let i = 0; i < length; i++) {
value = value + ((data.charCodeAt(offset + i) & 0xFF) * Math.pow(2, 8 * (length - i - 1)));
}
return value;
}
readChunkHeaderB (data, offset) {
const chunk = this.getEmptyChunk();
chunk.name = this.readString(data, offset, 4);
chunk.length = this.readIntB(data, offset + 4, 4);
return chunk;
}
readFloatB (data, offset) {
let expon = this.readIntB(data, offset, 2);
const range = 1 << 16 - 1;
if (expon >= range) {
expon |= ~(range - 1);
}
let sign = 1;
if (expon < 0) {
sign = -1;
expon += range;
}
const himant = this.readIntB(data, offset + 2, 4);
const lomant = this.readIntB(data, offset + 6, 4);
let value;
if (expon === himant && expon === lomant && lomant === 0) {
value = 0;
} else if (expon === 0x7FFF) {
value = Number.MAX_VALUE;
} else {
expon -= 16383;
value = (himant * 0x100000000 + lomant) * Math.pow(2, expon - 63);
}
return sign * value;
}
}
export class WAVDecoder extends Decoder {
decode (data) {
const decoded: any = {};
let offset = 0;
// Header
let chunk = this.readChunkHeaderL(data, offset);
offset += 8;
if (chunk.name !== 'RIFF') {
console.error('File is not a WAV');
return null;
}
let fileLength = chunk.length;
fileLength += 8;
const wave = this.readString(data, offset, 4);
offset += 4;
if (wave !== 'WAVE') {
console.error('File is not a WAV');
return null;
}
let bytesPerSample;
let numberOfChannels;
let bitDepth;
let sampleRate;
let channels;
while (offset < fileLength) {
chunk = this.readChunkHeaderL(data, offset);
offset += 8;
if (chunk.name === 'fmt ') {
// File encoding
const encoding = this.readIntL(data, offset, 2);
offset += 2;
if (encoding !== 0x0001) {
// Only support PCM
console.error('Cannot decode non-PCM encoded WAV file');
return null;
}
// Number of channels
numberOfChannels = this.readIntL(data, offset, 2);
offset += 2;
// Sample rate
sampleRate = this.readIntL(data, offset, 4);
offset += 4;
// Ignore bytes/sec - 4 bytes
offset += 4;
// Ignore block align - 2 bytes
offset += 2;
// Bit depth
bitDepth = this.readIntL(data, offset, 2);
bytesPerSample = bitDepth / 8;
offset += 2;
} else if (chunk.name === 'data') {
// Data must come after fmt, so we are okay to use it's variables
// here
const length = chunk.length / (bytesPerSample * numberOfChannels);
channels = [];
for (let i = 0; i < numberOfChannels; i++) {
channels.push(new Float32Array(length));
}
for (let i = 0; i < numberOfChannels; i++) {
const channel = channels[i];
for (let j = 0; j < length; j++) {
let index = offset;
index += (j * numberOfChannels + i) * bytesPerSample;
// Sample
let value = this.readIntL(data, index, bytesPerSample);
// Scale range from 0 to 2**bitDepth -> -2**(bitDepth-1) to
// 2**(bitDepth-1)
const range = 1 << bitDepth - 1;
if (value >= range) {
value |= ~(range - 1);
}
// Scale range to -1 to 1
channel[j] = value / range;
}
}
offset += chunk.length;
} else {
offset += chunk.length;
}
}
decoded.sampleRate = sampleRate;
decoded.bitDepth = bitDepth;
decoded.channels = channels;
decoded.length = length;
return decoded;
}
}
export class AIFFDecoder extends Decoder {
decode (data) {
const decoded: any = {};
let offset = 0;
// Header
let chunk = this.readChunkHeaderB(data, offset);
offset += 8;
if (chunk.name !== 'FORM') {
console.error('File is not an AIFF');
return null;
}
let fileLength = chunk.length;
fileLength += 8;
const aiff = this.readString(data, offset, 4);
offset += 4;
if (aiff !== 'AIFF') {
console.error('File is not an AIFF');
return null;
}
let bytesPerSample;
let numberOfChannels;
let bitDepth;
let sampleRate;
let channels;
while (offset < fileLength) {
chunk = this.readChunkHeaderB(data, offset);
offset += 8;
if (chunk.name === 'COMM') {
// Number of channels
const numberOfChannels = this.readIntB(data, offset, 2);
offset += 2;
// Number of samples
const length = this.readIntB(data, offset, 4);
offset += 4;
channels = [];
for (let i = 0; i < numberOfChannels; i++) {
channels.push(new Float32Array(length));
}
// Bit depth
bitDepth = this.readIntB(data, offset, 2);
bytesPerSample = bitDepth / 8;
offset += 2;
// Sample rate
sampleRate = this.readFloatB(data, offset);
offset += 10;
} else if (chunk.name === 'SSND') {
// Data offset
const dataOffset = this.readIntB(data, offset, 4);
offset += 4;
// Ignore block size
offset += 4;
// Skip over data offset
offset += dataOffset;
for (let i = 0; i < numberOfChannels; i++) {
const channel = channels[i];
for (let j = 0; j < length; j++) {
let index = offset;
index += (j * numberOfChannels + i) * bytesPerSample;
// Sample
let value = this.readIntB(data, index, bytesPerSample);
// Scale range from 0 to 2**bitDepth -> -2**(bitDepth-1) to
// 2**(bitDepth-1)
let range = 1 << bitDepth - 1;
if (value >= range) {
value |= ~(range - 1);
}
// Scale range to -1 to 1
channel[j] = value / range;
}
}
offset += chunk.length - dataOffset - 8;
} else {
offset += chunk.length;
}
}
decoded.sampleRate = sampleRate;
decoded.bitDepth = bitDepth;
decoded.channels = channels;
decoded.length = length;
return decoded;
}
}
export class AudioFileRequest {
private url: string;
private extension: string;
private async: boolean = true;
constructor (url, async) {
this.url = url;
this.async = async;
const splitURL = url.split('.');
this.extension = splitURL[splitURL.length - 1].toLowerCase();
}
onSuccess (decoded) {}
onFailure (decoded?) {}
send () {
if (this.extension !== 'wav' &&
this.extension !== 'aiff' &&
this.extension !== 'aif') {
this.onFailure();
return;
}
const request = new XMLHttpRequest();
request.open('GET', this.url, this.async);
request.overrideMimeType('text/plain; charset=x-user-defined');
request.onreadystatechange = ((event) => {
if (request.readyState === 4) {
if (request.status === 200 || request.status === 0) {
this.handleResponse(request.responseText);
} else {
this.onFailure();
}
}
}).bind(this);
request.send(null);
}
handleResponse (data) {
let decoder;
let decoded;
if (this.extension === 'wav') {
decoder = new WAVDecoder();
decoded = decoder.decode(data);
} else if (this.extension === 'aiff' || this.extension === 'aif') {
decoder = new AIFFDecoder();
decoded = decoder.decode(data);
}
this.onSuccess(decoded);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment