Parsing a Wave File in JavaScript
Made under the inspiration of this topic.
Normal JavaScript, which everyone is used to, does not provide tools for working with either the file system or binary data, so everything described below will be about node.js.
Wav file
Wave is a format for digitized audio data. It uses a standard RIFF structure. Data can be divided into 3 parts
- Headline
- Format section
- Data
There may be more data, but this is usually not used.
File parsing itself
var http = require('http');
var fs = require('fs');
var sys= require('sys')
var Canvas = require('canvas');
We connect the modules we need, node-canvas we need to draw the wave of the wave file.
var path = '/my/files/TH.wav'; // Путь к файлу.
var wave = {}; // создаём объект в который будем помещать все полученные данные
fs.readFile(path, function (err, data) {
if (err) throw err; // Считываем файл и помещаем его содержимое в переменную data
Getting the file parsing ...
Headline
The first part is the simplest. It can also be divided into 3 pieces of 4 bytes
- contains the file type - "RIFF"
- file size
- contains the label "wave"
var text = '';
var j = 0
for (var i = j; i < j + 4; i++) {
text += String.fromCharCode(data[i]);
}
j = i;
wave.type = text;
// получили тип - «RIFF»
var text = '';
for (var i = j; i < j + 4; i++) {
var byt = data[i].toString(2);
if (byt.length != 8) {
byt = addByte(byt)
}
text = byt + text;
}
j = i;
wave.size = parseInt(text, 2);
//Полученный размер файла всегда на 8 байт меньше чем тот, что говорит нам ОС.
There is one subtlety here - by default, the read bytes are transferred to the 10th system, which creates additional inconvenience, so we introduce the addByte function, which will add the missing bits at the beginning of the byte.
function addByte(byt) {
while (8 != byt.length) {
byt = '0' + byt;
}
return byt;
}
var text = '';
for (var i = j; i < j + 4; i++) {
text += String.fromCharCode(data[i]);
}
j = i;
console.log(j + ' Label -' + text);
wave.label = text;
//Метка «wave»
Data Format Section
The data format section goes right after the header, it starts with the keyword “fmt”
var text = '';
for (var i = j; i < j + 4; i++) {
text += String.fromCharCode(data[i]);
//text += data[i].toString(16);
}
j = i;
Next are the file options.
size, byte | title | description |
---|---|---|
4 | Chunk data size | contains the number of bytes that contain data about the file |
2 | Compression code | contains code that indicates the presence of file compression (a wav file may contain sound pinched even by MPEG, but these features are not used), most often there will be 1, which means PCM / uncompressed, i.e. no compression |
2 | Number of channels | number of channels, wav file may contain multi-channel recording, for example 5.1 |
4 | Sample rate | sampling frequency, usually 44100 - CD sampling frequency |
4 | Average bytes per second | File bitrate |
2 | Block align | 1 frame of sound in which all the channels are located, well, or you can say differently - the sample size |
2 | Significant bits per sample | the number of bits (!) for encoding the frame of one channel |
Further, programs that create wav files can write anything here, often only later understood by these programs.
// extra bytes fmt
var text = '';
for (var i = j; i < j + 4; i++) {
var byt = data[i].toString(2);
if (byt.length != 8) {
byt = addByte(byt)
}
text = byt + text;
}
j = i;
wave.extra_bytes_fmt = parseInt(text, 2);
//Compression code
var text = '';
for (var i = j; i < j + 2; i++) {
var byt = data[i].toString(2);
if (byt.length != 8) {
byt = addByte(byt)
}
text = byt + text;
}
j = i;
var compression = '';
switch (parseInt(text, 2)) {
case 0:
compression = 'Unknown';
break;
case 1:
compression = 'PCM/uncompressed';
break;
case 2:
compression = 'Microsoft ADPCM';
break;
case 6:
compression = 'ITU G.711 a-law';
break;
case 7:
compression = 'ITU G.711 µ-law';
break;
case 17:
compression = 'IMA ADPCM';
break;
case 20:
compression = 'ITU G.723 ADPCM (Yamaha)';
break;
case 49:
compression = 'GSM 6.10';
break;
case 64:
compression = 'ITU G.721 ADPCM';
break;
case 80:
compression = 'MPEG';
break;
case 65536:
compression = 'Experimental';
break;
default:
compression = 'Other';
break;
}
wave.compression = compression;
//Number of channels
var text = '';
for (var i = j; i < j + 2; i++) {
var byt = data[i].toString(2);
if (byt.length != 8) {
byt = addByte(byt)
}
text = byt + text;
}
j = i;
console.log(j + ' Number of channels - ' + parseInt(text, 2));
wave.number_of_channels = parseInt(text, 2);
//Sample rate
var text = '';
for (var i = j; i < j + 4; i++) {
var byt = data[i].toString(2);
if (byt.length != 8) {
byt = addByte(byt)
}
text = byt + text;
}
j = i;
console.log(j + ' Sample rate - ' + parseInt(text, 2) + ' hz ');
wave.sample_rate = parseInt(text, 2);
//Average bytes per second
var text = '';
for (var i = j; i < j + 4; i++) {
var byt = data[i].toString(2);
if (byt.length != 8) {
byt = addByte(byt)
}
text = byt + text;
}
j = i;
wave.average_bytes_per_second = parseInt(text, 2) * 8 / 1000;
// переводим в гораздо более родные и понятные кбит/с
//Block align
var text = '';
for (var i = j; i < j + 2; i++) {
var byt = data[i].toString(2);
if (byt.length != 8) {
byt = addByte(byt)
}
text = byt + text;
}
j = i;
wave.block_align = parseInt(text, 2);
//Significant bits per sample
var text = '';
for (var i = j; i < j + 2; i++) {
var byt = data[i].toString(2);
if (byt.length != 8) {
byt = addByte(byt)
}
text = byt + text;
}
j = i;
wave.significant_bits_per_sample = parseInt(text, 2);
//Extra format bytes
var text = '';
for (var i = j; i < j + 2; i++) {
var byt = data[i].toString(2);
if (byt.length != 8) {
byt = addByte(byt)
}
text = byt + text;
}
j = i;
wave.extra_format_bytes = parseInt(text, 2);
//end fmt
Data
Since the number of additional fields in the fmt section is not very predictable (the actual situation is often not reflected in the extra_bytes_format field), it is easiest to find the “data” keyword by touch.
while (!(text == 'data' || j == wave.size)) {
text = String.fromCharCode(data[j]) + String.fromCharCode(data[j + 1]) + String.fromCharCode(data[j + 2]) + String.fromCharCode(data[j + 3]);
j++;
}
wave.data_position = j;
4 bytes after the keyword should contain the data size
var text = '';
for (var i = j; i < j + 4; i++) {
var byt = data[i].toString(2);
if (byt.length != 8) {
byt = addByte(byt)
}
text = byt + text;
}
j = i;
wave.chunk_size = parseInt(text, 2);
Now we can receive the data ourselves, all the necessary we got above. In this topic, I will consider a classic example of 2 channels, because other options are very rare.
//sound
wave.lc = [];
wave.rc = [];
var k = 16; /* поскольку в несжатом очень много данных - мы будем брать не все данные, а через каждые k байтов*/
wave.n = wave.block_align * k;
while (j < wave.size) {
var text = '';
for (var i = j; i < j + wave.block_align; i++) {
var byt = data[i].toString(2);
if (byt.length != 8) {
byt = addByte(byt)
}
text = text + byt;
}
var s1 = text.slice(0, text.length / 2);
if (s1[0] == 1) {
s1 = -(parseInt(text.slice(1, text.length / 2), 2))
} else {
s1 = parseInt(text.slice(0, text.length / 2), 2)
}
var s2 = text.slice(text.length / 2, text.length);
if (s2[0] == 1) {
s2 = -(parseInt(text.slice(text.length / 2 + 1, text.length), 2))
} else {
s2 = parseInt(text.slice(text.length / 2, text.length), 2)
} /*если на 1 фрейм приходится 8 бит, то байт беззнаковый, если больше (16,24, 32… ), первый бит байта будет знаком */
wave.lc.push(s1);
wave.rc.push(s2);
j = i;
j += wave.n;
}
Thanks to the node.js library - canvas-node, we can draw waves.
Draw the waves
You can work with the library as well as with a regular canvas in the browser
var canvas = new Canvas(900, 300);
var ctx = canvas.getContext('2d');
var canvas2 = new Canvas(900, 300);
var ctx2 = canvas2.getContext('2d');
ctx.strokeStyle = 'rgba(0,187,255,1)';
ctx.beginPath();
ctx.moveTo(0, 150);
ctx2.strokeStyle = 'rgba(0,187,255,1)';
ctx2.beginPath();
ctx2.moveTo(0, 150);
wave.k = 900 / wave.lc.length;
wave.l = 300 / Math.pow(2, wave.significant_bits_per_sample);
// эти параметры необходимы для того чтобы полученная волна корректно умещалась на нашем холсте размером 900 на 300
var q = Math.pow(2, wave.significant_bits_per_sample) / 2;
/* Поскольку node.js у меня крутится на виртуалке с FreeBSD, то чтобы посмотреть результат поднимем маленький сервер*/
var web = http.createServer(function (req, res) {
res.writeHead(200, {
'Content-Type': 'text/html'
});
for (var i = 1; i < wave.lc.length; i++) {
if (wave.lc[i] > 0) {
var y = 150 + Math.floor(wave.lc[i] * wave.l)
} else {
var y = 150 + Math.floor((-q - wave.lc[i]) * wave.l)
}
if (wave.lc[i] == 0) y = 150
ctx.lineTo(Math.floor(i * wave.k), y);
}
ctx.stroke();
res.write('
');
//левый канал готов
for (var i = 1; i < wave.rc.length; i++) {
if (wave.rc[i] > 0) {
var y = 150 + Math.floor(wave.rc[i] * wave.l)
} else {
var y = 150 + Math.floor((-q - wave.rc[i]) * wave.l)
}
if (wave.rc[i] == 0) y = 150
ctx2.lineTo(Math.floor(i * wave.k), y);
}
ctx2.stroke();
res.write('
');
// правый канал готов
res.end();
}).listen(8000);
Total
ps Sorry for the quality and suboptimal code. I’m just learning, besides I tried to write as simple as possible.