patternjavascriptMinor
Generating sinusoidal music as WAV output in JavaScript ES6
Viewed 0 times
wavjavascriptoutputgeneratinges6musicsinusoidal
Problem
This is a single isomorphic
I have left thorough comments in the code to help document the usage of the function arguments. There is also an example script at the end utilizing this small library.
I hope to address readability and/or performance, but any other advice is welcome too.
```
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,
// ...
// skipped for brevity
// ...
'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 * sampl
class in ES6, written with the intention of generating a full WAV file given note names and durations in seconds. In JavaScript it can be exported as a Blob and in Node.js it can be exported as a Buffer, to be written to the filesystem.I have left thorough comments in the code to help document the usage of the function arguments. There is also an example script at the end utilizing this small library.
I hope to address readability and/or performance, but any other advice is welcome too.
```
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,
// ...
// skipped for brevity
// ...
'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 * sampl
Solution
Any performance optimization must start with Timeline/JS profiling.
In this case the slowest part is
Let's speed it up 10 times or more by eliminating function calls to DataView's methods in favor of directly accessed unsigned byte view:
8-bit WAV case is simple:
16-bit WAV requires us to implement byte order maintenance manually (see P.S.):
32-bit case is similar.
Eliminate the largest bottlenecks and repeat the profiling tests progressively until satisfied.
P.S. The optimized 16-bit case without inner-loop branching as per the comments:
In this case the slowest part is
typedData getter, ~850ms on i7 CPU with the OP's example. This is an enormously huge time for such a simple operation. Let's speed it up 10 times or more by eliminating function calls to DataView's methods in favor of directly accessed unsigned byte view:
var uint8 = new Uint8Array(buffer);8-bit WAV case is simple:
case 1:
for (i = 0; i < bytes; i++) {
uint8[i] = data[i] * amplitude + 128;
}
break;16-bit WAV requires us to implement byte order maintenance manually (see P.S.):
case 2:
if (this.littleEndian) {
for (i = 0; i > 8;
}
}
} else {
for (i = 0; i > 8;
uint8[i * 2 + 1] = v & 255;
}
}
}
break;32-bit case is similar.
Eliminate the largest bottlenecks and repeat the profiling tests progressively until satisfied.
P.S. The optimized 16-bit case without inner-loop branching as per the comments:
case 2:
if (this.littleEndian) {
for (i = 0; i > 8;
}
} else {
for (i = 0; i > 8;
uint8[i * 2 + 1] = v & 255;
}
}
break;Code Snippets
var uint8 = new Uint8Array(buffer);case 1:
for (i = 0; i < bytes; i++) {
uint8[i] = data[i] * amplitude + 128;
}
break;case 2:
if (this.littleEndian) {
for (i = 0; i < bytes; i++) {
var v = data[i] * amplitude;
if (!v) {
uint8[i * 2] = 0;
uint8[i * 2 + 1] = 0;
} else {
if (v < 0) {
v = 65536 + v;
}
uint8[i * 2] = v & 255;
uint8[i * 2 + 1] = v >> 8;
}
}
} else {
for (i = 0; i < bytes; i++) {
var v = data[i] * amplitude;
if (!v) {
uint8[i * 2] = 0;
uint8[i * 2 + 1] = 0;
} else {
if (v < 0) {
v = 65536 + v;
}
uint8[i * 2] = v >> 8;
uint8[i * 2 + 1] = v & 255;
}
}
}
break;case 2:
if (this.littleEndian) {
for (i = 0; i < bytes; i++) {
var v = (data[i] * amplitude + 65536) & 65535;
uint8[i * 2] = v & 255;
uint8[i * 2 + 1] = v >> 8;
}
} else {
for (i = 0; i < bytes; i++) {
var v = (data[i] * amplitude + 65536) & 65535;
uint8[i * 2] = v >> 8;
uint8[i * 2 + 1] = v & 255;
}
}
break;Context
StackExchange Code Review Q#142443, answer score: 2
Revisions (0)
No revisions yet.