Разбор Wave файла на JavaScript

Обычный JavaScript, к которому все привыкли, не даёт средств работы ни с файловой системой, ни с двоичными данными, поэтому все описанное ниже будет про node.js.

Wav файл

Wave — это формат для оцифрованных аудио — данных. В нем используется стандартная RIFF структура. Данные можно условно разделить на 3 части
  • Заголовок
  • Секция формата
  • Данные

Сам разбор файла

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


Подключаем необходимые нам модули, node-canvas нам нужен для рисования волны wave файла.

var path = '/my/files/TH.wav' ;// Путь к файлу.
var wave = {}; // создаём объект в который будем помещать все полученные данные


fs.readFile(path, function (err, data) {
if (err) throw err; // Считываем файл и помещаем его содержимое в переменную data


Итак начем разбор файла.

Заголовок

Первая часть самая простая. Её можно так же поделить на 3 кусока по 4 байта

  1. содержит тип файла — «RIFF»
  2. размер файла
  3. содержит метку «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);

Тут есть одна тонкость — по умолчанию считанные байты переводятся в 10 систему, что создаёт дополнительные неудобства, поэтому введем функцию addByte, которая будет добавлять отсутствующие биты в начале байта.

function addByte(byt)
{
while(8!=byt.length)
{
byt='0'+byt;
}
return byt;
}


Полученный размер файла всегда на 8 байт меньше чем тот, что говорит нам ОС.

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»

Секция формата данных

Идет сразу же после заголовка, начинается она с ключевого слова «fmt»

var text ='';
for (var i=j;i<j+4;i++)
{ 
text += String.fromCharCode(data[i]);
//text += data[i].toString(16);
}
j=i; 


Далее идут параметры файла. К сожелению, не вижу в редакторе возможности создания таблицы, поэтому буду объяснять на пальцах делать списком.

  1. 4 байта — Chunk Data Size — содержит кол-во байт, в которых содержатся дополнительные и не обязательные данные о файле
  2. 2 байта — Compression code — содердится код, который указывается на наличие коспрессии файла (wav файл может содержать звук пережатый даже MPEG, но эти возможности не используются), чаще всего там будет 1, что значит PCM/uncompressed, т.е. никакого сжатия нет
  3. 2 байта — Number of channels — количество каналов, wav файл может содержать многоканальную музыку, но чаще всего все таки там 2 канала
  4. 4 байта — Sample rate — частота сэмплирования, обычно 44100 — частота сэмплирования CD диска
  5. 4 байта — Average bytes per second — Битрейт файла
  6. 2 байта — Block align — 1 фрейм звука, в котором находятся все каналы, ну или можно сказать по другому — размер выборки
  7. 2 байта — Significant bits per sample — кол бит (!!!) для кодирования фрэйма одного канала
  8. Дальше программы которые создают wav файлы могут записывать сюда всё что угодно, зачастую понятное потом только этим программам


// 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;
console.log(j+' Extra format bytes - ' +parseInt(text,2)+' bytes'); 
wave.extra_format_bytes = parseInt(text,2);

//end fmt


Данные

Поскольку количество дополнительных полей в секции fmt мало предсказуемо (в поле extra_bytes_format зачастую не отражается реальная ситуация), проще всего найти ключевое слово «data» наощупь.

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 байта после ключевого слова должны содержать размер данных
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);


Теперь мы можем получать сами данные, все необходимое мы получили выше.
здесь я с рассмотрю классические пример на 2 канала, потому что другие варианты встречаются очень редко.
//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;
}



Основные данные мы получили. Спецификации позволяют сохранять в wave — файл ещё кучу разных данных, но на практике это обычно не используется.

Благодаря библиотеке node.js — canvas-node мы можем нарисовать волны.

Рисуем волны

Работать с библиотекой можно также как и с обычным canvas'-ом в браузере
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="http://' + canvas.toDataURL() + '"/>
');
//левый канал готов

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="http://' + canvas2.toDataURL() + '"/>
');

// правый канал готов

res.end();
}).listen(8000);



Итог
image

p.s. Извините за качество кода. Я только учусь, к тому же старался писать максимально просто.


0 комментариев

Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.