HiveBrain v1.2.0
Get Started
← Back to all entries
patternjavascriptMinor

Generating sinusoidal music as WAV output in JavaScript ES6

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
wavjavascriptoutputgeneratinges6musicsinusoidal

Problem

This is a single isomorphic 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 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.