Работа с двоичными аудиофайлами в C

Прошло очень много времени с тех пор, как я программировал, но у меня никогда не было возможности читать из двоичного файла. В основном я работал с файлами, содержащими текстовые данные. В прошлом месяце я решил поработать над одним двоичным файлом, чтобы создать что-то интересное.

В составе группы у нас был фронтенд и бэкенд этого мини-проекта.

Звуковые файлы формы волны

Аудиофайлы Waveform — это двоичные файлы, в которых хранится аудиоинформация. В отличие от MP3 или других аудиоформатов, этот формат не сжимает никакой информации, что упрощает работу с ним. Как новичок, имеющий дело с нетекстовыми форматами файлов, я решил, что это отличный вариант для использования в нашем проекте.

Как хранятся данные?

Как мы уже знаем, вся информация хранится в виде комбинации нулей и единиц. Незнакомой аудитории это кажется невозможным для чтения. Однако, привыкнув к «структуре», это не так сложно, как может показаться.

Каждый двоичный файл имеет раздел «заголовок» и раздел «данные». Заголовок описывает правила файлов заголовков. В случае файла .wav различные поля заголовка указывают важные атрибуты файла, такие как количество битов, составляющих сэмпл, длину данных и количество каналов в аудио (обычно 2 канала — левый и правый).

Чтение заголовков из бинарного файла

Моя первая задача — прочитать эту информацию заголовка. Чтобы узнать, какие биты указывают на какой атрибут и его значение, мы обратимся к таблице, такой как ниже:

Примечание. После тщательной отладки это может иметь место лишь иногда. Были некоторые экземпляры файлов .wav, информация заголовка которых превышала 44 байта. А пока предположим, что заголовок всегда имеет длину 44 байта.

На основе приведенной выше таблицы заголовков мы можем создать структуру, которая выглядит следующим образом. В этой статье я буду использовать C для демонстрации своего кода.

typedef struct {
  char riff[4];
  uint32_t file_size;
  char format[4];
  char riff_format[4];
  uint32_t format_size;
  uint16_t audio_format;
  uint16_t num_channels;
  uint32_t sample_rate;
  uint32_t byte_rate;
  uint16_t block_align;
  uint16_t bits_per_sample;
  char data[4];
  uint32_t data_size;
} wav_header_t;

И легко прочитать данные с помощью простого вызова функции

wav_header_t* header = malloc(sizeof(wave_header_t));

// read the size of wave_header_t 1 time and store it sequentially into
// the address given by header*
fread(header, sizeof(wav_header_t), 1, wav_file);

Чтение данных из бинарного файла

После того, как вы прочитали все заголовки, мы можем понять доступную информацию для чтения данных. Из таблицы обратите внимание, что байты 37–40 должны говорить «данные», что отмечает начало раздела «данные» файла. Следующие 4 байта сообщают мне размер раздела данных. Используя эти данные, я могу легко прочитать данные из файла сигнала.

wave_header_t* header; // initialised and read
uint8_t* audio_data = malloc(header->data_size);

fread(audio_data, header->data_size, 1, wav_file);

Какой смысл имеют байты данных?

В заголовках у нас есть поле bits_per_sample. Предположим, что это 24. Каждые 24 бита в этом файле сигнала составляют семпл. Сэмпл — это крошечная единица времени в аудиофайле. Вы можете использовать поля в заголовке для расчета периода одного семпла. Последующие 24 бита — это последующие сэмплы в аудиофайле. Эта последовательность 24-битных чисел составляет «волну».

Последовательность чисел, визуализированная на графике, выглядит чем-то более сложным, чем картинка выше.

Волна представляет собой суперпозицию нескольких волн разной частоты и соответствующих им амплитуд (объемов).

Нам потребуется разбить эту волну на более мелкие волны, составляющие исходную волну, и записать частоты и их амплитуду. Но как нам взять эту волну и разделить ее на частоты? Преобразование Фурье!

Использование библиотеки FFTW3

Преобразование Фурье (ПФ) — это именно тот инструмент, который мы можем использовать для достижения желаемой цели. Быстрое преобразование Фурье (БПФ) — это известный алгоритм в области компьютерных наук, который может принимать массив чисел и выводить результат выполнения преобразования Фурье над числами. В FFTW3 есть замечательные руководства по его использованию.

#include <fftw3.h>

fftw_complex* apply_fft(...) {
  fftw_complex* in = fftw_malloc(100); // size of input array
  fftw_complex* out = fftw_malloc(100); // size of output array

  // using a specific type of FT: 1 dimensional Discrete FT
  fftw_plan* plan = fftw_plan_dft_1d(...); 

  // populate the "in" variable 
  fftw_execute(plan);

  // now the "out" variable has the result of FFT on "in"
  // cleanup the memory that is no longer in use
  return out;
}

Заключение

Чтение двоичных файлов ничем не отличается от чтения текстовых файлов. Важно найти правильную информацию о том, как устроен файл и как можно интерпретировать формат закодированных в нем данных. Сложность программы нельзя узнать, не изучив ее и не реализовав на практике.

Надеюсь, вам понравилось читать мою статью и вы узнали что-то новое. Спасибо! Люблю то, что я делаю?

Want to connect?

My GitHub profile.
My Portfolio website.