Parsing a Wave File in JavaScript

icon
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
  1. Headline
  2. Format section
  3. 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
  1. contains the file type - "RIFF"
  2. file size
  3. 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
4Chunk data size contains the number of bytes that contain data about the file
2Compression 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
2Number of channels number of channels, wav file may contain multi-channel recording, for example 5.1
4Sample rate sampling frequency, usually 44100 - CD sampling frequency
4Average bytes per second File bitrate
2Block align 1 frame of sound in which all the channels are located, well, or you can say differently - the sample size
2Significant 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

wave

ps Sorry for the quality and suboptimal code. I’m just learning, besides I tried to write as simple as possible.

Also popular now: