Pull to refresh

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

Reading time 6 min
Views 7K
icon
Сделано под вдохновением этого топика.
Обычный JavaScript, к которому все привыкли, не даёт средств работы ни с файловой системой, ни с двоичными данными, поэтому все описанное ниже будет про node.js.

Wav файл

Wave — это формат для оцифрованных аудио — данных. В нем используется стандартная RIFF структура. Данные можно условно разделить на 3 части
  1. Заголовок
  2. Секция формата
  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);
//Полученный размер файла всегда на 8 байт меньше чем тот, что говорит нам ОС.


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


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»


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


Секция формата данных идет сразу же после заголовка, начинается она с ключевого слова «fmt»


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


Далее идут параметры файла.

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


Далее программы которые создают 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;
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;
}


Благодаря библиотеке 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="' + 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);




Итог

wave

p.s. Извините за качество и неоптимальность кода. Я только учусь, к тому же старался писать максимально просто.
Tags:
Hubs:
+48
Comments 25
Comments Comments 25

Articles