📜 ⬆️ ⬇️

Parsing the Wave File in JavaScript

icon
Made under the inspiration of this topic.
Regular JavaScript, which everyone is used to, does not provide the means to work with the file system or with 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 not usually used.

Parsing the file itself

var http = require('http'); var fs = require('fs'); var sys= require('sys') var Canvas = require('canvas'); 

We connect the modules we need, we need the node-canvas 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 

Begin parsing the file ...
')
Headline

The first part is the easiest. It can also be divided into 3 pieces of 4 bytes
  1. contains 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 - by default, the read bytes are transferred to the 10th system, which creates additional inconveniences, so we introduce the addByte function, which will add the missing bits at the beginning of a 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 come the file options.

size, bytetitledescription
fourChunk Data Sizecontains the number of bytes that contain data about the file
2Compression codeThere is a code that indicates the presence of file compression (a wav file may contain a sound that has been compressed 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 channelsnumber of channels, a wav file may contain a multi-channel recording, for example, 5.1
fourSample ratesampling frequency, usually 44100 - CD sampling frequency
fourAverage bytes per secondFile bitrate
2Block align1 frame of sound in which all the channels are located, well, or you can say differently - the sample size
2Significant bits per samplethe number of bits (!) for encoding the frame of one channel


Further, programs that create wav files can write anything here, which is often understandable only 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 often does not reflect in the extra_bytes_format field), the easiest way is to find the keyword “data” 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 must contain 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 get the data itself, we got everything we needed above. In this topic, I will consider a classic example for 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 - the canvas-node, we can draw waves.

Drawing waves


You can work with the library as well as with the usual 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('<img src="' + canvas.toDataURL() + '" /><br/>'); //   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('<img src="' + canvas2.toDataURL() + '" /><br/>'); //    res.end(); }).listen(8000); 


Total

wave

ps Sorry for the quality and non-optimality of the code. I'm just learning, besides trying to write as simply as possible.

Source: https://habr.com/ru/post/113611/


All Articles