Created
February 12, 2018 16:02
-
-
Save Belrestro/341ba8fab03bb46b1de377dedb923412 to your computer and use it in GitHub Desktop.
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
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