Приходилось ли вам когда-нибудь ненароком причинить себе или другим физический вред из-за ошибок в коде? Мне – да.
Примерно год назад я работал со сгенерированными файлами в формате WAV, их было несколько тысяч. Я пытался снабдить их метками, рассортировать по папкам, создавал метаданные. В процессе я прослушал несколько штук, и, к моей досаде, оказалось, что все они начинаются с довольно продолжительной тишины. Это сильно раздражало, особенно когда прослушиваешь серию файлов подряд и постоянно спотыкаешься о паузы перед воспроизведением каждого следующего. Прекрасно, значит, еще и с этим придется что-то делать.
Я уже потратил сколько-то времени на поиск решений для удаления тишины из файлов, когда меня вдруг осенило: это ведь WAV! Данные в файлах формата WAV обычно представляют собой PCM-аудио, то есть каждое значение в файле задает амплитуду звука в некоторый момент времени. Соответственно, если у нас там действительно полная тишина, а не белый шум, то в файле этой тишине должны соответствовать сплошные нули, так ведь?
Так и есть. Ну что ж, значит, дело проще, чем казалось. Достаточно просто прочитать файлы, найти место, где заканчиваются эти нули, и убрать соответствующий фрагмент.
Сначала мне нужно было поближе познакомиться с форматом WAV, чтобы понять, как работать с такими файлами и управлять данными внутри них. Я подобрал несколько источников; одним из самых полезных оказалась старая страница со stanford.edu (сайт сейчас уже недоступен, но, к счастью, сохранился на Wayback Machine). Там была очень доходчивая диаграмма:
![](https://habrastorage.org/webt/jo/nw/7a/jonw7aityn_bym-wxpznkqpqnag.gif)
Итак, структура файла WAV представляется довольно простой: сначала заголовок объемом в 44 байта, а дальше уже собственно данные. С этой информацией уже можно было приступать к коду. Требовалось только пропустить первые 44 байта, убрать последовательность из нулей в начале секции с данными, а всё остальное отправить на воспроизведение в исходном виде. Хотя не могу не добавить, что в другом источнике мне попались такие сведения:
Ну, я решил, что ничего страшного: программу я писал на C, так что за безопасность можно было особенно не переживать.
Код был несложным, уложился меньше чем в сотню строк. По сути, он перебирал весь файл по байту, за исключением первых сорока четырех, и считал идущие подряд нули. Как только попадалось что-нибудь, что нулём не являлось, программа останавливалась, сохраняла соответствующий индекс, и начинала читать файл сначала. На этот раз она пропускала всё, что предшествует индексу (не считая заголовка), и выводила все остальные байты по стандартной схеме.
Целиком код приводить незачем, но вот та часть, которая будет нас интересовать:
Всё круто, всё просто. Пора тестировать. Я запустил программу на одном из файлов с особенно продолжительной паузой.
Проверил, что выдаёт xxd для testfile1.nosilence.wav. Отлично, никаких нулей в начале. Значит, сработало. Чтобы окончательно убедиться, открою-ка я по-быстрому файл в аудиопроигрывателе.
![](https://habrastorage.org/webt/vi/a2/uh/via2uhrktdhlnwf1lqpn8gj3lqc.gif)
Источник
В ту же секунду мне по ушам ударил самый мощный статический шум, который я в жизни слышал. Чуть не вывалившись из кресла, я отчаянно пытался стащить с себя наушники. Помню, дело было посреди ночи, и собака прибежала проверить, что там со мной.
В ушах всё еще звенело, а я сидел и пытался осмыслить свои опрометчивые решения.
А вы заметили третью ошибку в коде, который я приводил выше? Подсказка: смотрите на комментарий. Я рассчитал переменную index как индекс последнего байта, представляющего собой нули. А значит, за вычетом 44 байтов заголовка, теперь мы воспроизводим только то, что следует за индексом или накладывается на него. index у нас стоит на последнем нуле в серии, то есть мы включаем один лишний нулевой байт в секцию с данными.
Это можно исправить следующим образом:
Теперь в выдаче нет лишних нулей, и если воспроизвести файл, ничего страшного не случится. Я всё починил… Но стоп.
В файлах WAV у нас PCM-аудио, а нули в аудиоданных такого рода соответствуют полной тишине. Так разве этот лишний байт не должен быть совершенно бесшумным? Почему он оказался таким громким и таким статическим?
Для начала давайте сравним нормальный аудиофайл с монстром, которого я создал, при помощи Audacity:
![](https://habrastorage.org/r/w1560/webt/te/uk/od/teukodhtpkrj50lcpudy73bqxkm.png)
Угадали, где монстр? Да, это тот самый, у которого амплитуда стабильно вывернута чуть ли не максимум. Почему так?
Я вернулся к источникам, которые отобрал, и попытался разобраться, как ошибка на единицу могла привести к подобному взрыву амплитуды. Я знал, что в моих файлах сэмпл содержит 16 бит, а канала два (стерео), поэтому стал искать соответствующую информацию. Вот что говорилось в разделе про 16-битное стерео PCM-аудио:
«Минимально достаточное количество байтов для хранения заданного размера» — формулировка здесь излишне запутанная. i соответствует числу битов, которые содержатся в сэмпле. В нашем случае их шестнадцать. Соответственно, если у нас есть некое значение длиной в 16 битов, само собой, храниться оно будет в двух байтах. А дальше важный момент: наименее значимый из байтов располагается в хранилище первым. Вот оно.
Взгляните на график, который я сделал, чтобы показать, что привело к возникновению такого сильного сигнала:
![](https://habrastorage.org/r/w1560/webt/mi/mc/yv/mimcyvxwvpbxjziql3j1gp48spo.png)
В верхней части показан мой файл-монстр, в котором я случайно оставил лишний байт с нулями. В каждом из трёх сэмплов — s1, s2 и s3 – по два байта, причем второй более значимый. Следовательно, при переводе этих пар байтов в десятичную форму мы получаем очень высокую амплитуду.
В то же время, в нижней части видно, что, если убрать нулевой байт, сэмплы читаются так, как и должны, и значения в аудиофайле укладываются в рамки разумного.
Выходит, если бы у меня было 8-битное аудио, тогда пропущенный лишний байт не вызвал бы никаких проблем. Но оно было 16-битным, и в итоге, я сдвинул всю последовательность в сэмплах, так что наименее значимый байт стал читаться как наиболее значимый.
Примерно год назад я работал со сгенерированными файлами в формате WAV, их было несколько тысяч. Я пытался снабдить их метками, рассортировать по папкам, создавал метаданные. В процессе я прослушал несколько штук, и, к моей досаде, оказалось, что все они начинаются с довольно продолжительной тишины. Это сильно раздражало, особенно когда прослушиваешь серию файлов подряд и постоянно спотыкаешься о паузы перед воспроизведением каждого следующего. Прекрасно, значит, еще и с этим придется что-то делать.
Я уже потратил сколько-то времени на поиск решений для удаления тишины из файлов, когда меня вдруг осенило: это ведь WAV! Данные в файлах формата WAV обычно представляют собой PCM-аудио, то есть каждое значение в файле задает амплитуду звука в некоторый момент времени. Соответственно, если у нас там действительно полная тишина, а не белый шум, то в файле этой тишине должны соответствовать сплошные нули, так ведь?
$ xxd testfile1.wav | head -n 100
00000000: 5249 4646 64b9 0e00 5741 5645 666d 7420 RIFFd...WAVEfmt
00000010: 1000 0000 0100 0200 44ac 0000 10b1 0200 ........D.......
00000020: 0400 1000 6461 7461 40b9 0e00 0000 0000 ....data@.......
00000030: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000040: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000050: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000060: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000070: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000080: 0000 0000 0000 0000 0000 0000 0000 0000 ................
# ... and a lot more zeros below
Так и есть. Ну что ж, значит, дело проще, чем казалось. Достаточно просто прочитать файлы, найти место, где заканчиваются эти нули, и убрать соответствующий фрагмент.
Как читаются файлы WAV
Сначала мне нужно было поближе познакомиться с форматом WAV, чтобы понять, как работать с такими файлами и управлять данными внутри них. Я подобрал несколько источников; одним из самых полезных оказалась старая страница со stanford.edu (сайт сейчас уже недоступен, но, к счастью, сохранился на Wayback Machine). Там была очень доходчивая диаграмма:
![](https://habrastorage.org/webt/jo/nw/7a/jonw7aityn_bym-wxpznkqpqnag.gif)
Итак, структура файла WAV представляется довольно простой: сначала заголовок объемом в 44 байта, а дальше уже собственно данные. С этой информацией уже можно было приступать к коду. Требовалось только пропустить первые 44 байта, убрать последовательность из нулей в начале секции с данными, а всё остальное отправить на воспроизведение в исходном виде. Хотя не могу не добавить, что в другом источнике мне попались такие сведения:
«Некоторые программы предполагают (и это очень наивно с их стороны), что вводная часть в заголовке всегда занимает ровно 44 байта (как говорится в таблице выше) и что всё остальное содержимое файла составляют только и исключительно аудиоданные. Делать подобные предположения небезопасно».
Ну, я решил, что ничего страшного: программу я писал на C, так что за безопасность можно было особенно не переживать.
Код
Код был несложным, уложился меньше чем в сотню строк. По сути, он перебирал весь файл по байту, за исключением первых сорока четырех, и считал идущие подряд нули. Как только попадалось что-нибудь, что нулём не являлось, программа останавливалась, сохраняла соответствующий индекс, и начинала читать файл сначала. На этот раз она пропускала всё, что предшествует индексу (не считая заголовка), и выводила все остальные байты по стандартной схеме.
Целиком код приводить незачем, но вот та часть, которая будет нас интересовать:
// index was calculated above to be the index of
// the last consecutive zero byte
FILE *f = fopen(argv[1], "rb");
int ind = 0;
int current_byte;
while ((current_byte = fgetc(f)) != EOF) {
if (ind < 44 || ind >= index) {
fputc(current_byte, stdout);
}
ind += 1;
}
fclose(f);
Всё круто, всё просто. Пора тестировать. Я запустил программу на одном из файлов с особенно продолжительной паузой.
./strip_audio testfile1.wav > testfile1.nosilence.wav
Проверил, что выдаёт xxd для testfile1.nosilence.wav. Отлично, никаких нулей в начале. Значит, сработало. Чтобы окончательно убедиться, открою-ка я по-быстрому файл в аудиопроигрывателе.
![](https://habrastorage.org/webt/vi/a2/uh/via2uhrktdhlnwf1lqpn8gj3lqc.gif)
Источник
В ту же секунду мне по ушам ударил самый мощный статический шум, который я в жизни слышал. Чуть не вывалившись из кресла, я отчаянно пытался стащить с себя наушники. Помню, дело было посреди ночи, и собака прибежала проверить, что там со мной.
Где я ошибся?
В ушах всё еще звенело, а я сидел и пытался осмыслить свои опрометчивые решения.
- Ошибка №1: надо было убавить звук.
- Ошибка №2: не надо было сидеть в наушниках.
- Ошибка №3: неучтённая единица.
А вы заметили третью ошибку в коде, который я приводил выше? Подсказка: смотрите на комментарий. Я рассчитал переменную index как индекс последнего байта, представляющего собой нули. А значит, за вычетом 44 байтов заголовка, теперь мы воспроизводим только то, что следует за индексом или накладывается на него. index у нас стоит на последнем нуле в серии, то есть мы включаем один лишний нулевой байт в секцию с данными.
Это можно исправить следующим образом:
// replaced >= with just >
if (ind < 44 || ind > index) {
fputc(current_byte, stdout);
}
Теперь в выдаче нет лишних нулей, и если воспроизвести файл, ничего страшного не случится. Я всё починил… Но стоп.
В файлах WAV у нас PCM-аудио, а нули в аудиоданных такого рода соответствуют полной тишине. Так разве этот лишний байт не должен быть совершенно бесшумным? Почему он оказался таким громким и таким статическим?
Для начала давайте сравним нормальный аудиофайл с монстром, которого я создал, при помощи Audacity:
![](https://habrastorage.org/webt/te/uk/od/teukodhtpkrj50lcpudy73bqxkm.png)
Угадали, где монстр? Да, это тот самый, у которого амплитуда стабильно вывернута чуть ли не максимум. Почему так?
Как читаются аудиосэмплы
Я вернулся к источникам, которые отобрал, и попытался разобраться, как ошибка на единицу могла привести к подобному взрыву амплитуды. Я знал, что в моих файлах сэмпл содержит 16 бит, а канала два (стерео), поэтому стал искать соответствующую информацию. Вот что говорилось в разделе про 16-битное стерео PCM-аудио:
«Каждый сэмпл содержится в целом числе i, которое представляет минимально достаточное количество байтов для хранения заданного размера сэмпла. Наименее значимый из байтов располагается в хранилище первым».
«Минимально достаточное количество байтов для хранения заданного размера» — формулировка здесь излишне запутанная. i соответствует числу битов, которые содержатся в сэмпле. В нашем случае их шестнадцать. Соответственно, если у нас есть некое значение длиной в 16 битов, само собой, храниться оно будет в двух байтах. А дальше важный момент: наименее значимый из байтов располагается в хранилище первым. Вот оно.
Взгляните на график, который я сделал, чтобы показать, что привело к возникновению такого сильного сигнала:
![](https://habrastorage.org/webt/mi/mc/yv/mimcyvxwvpbxjziql3j1gp48spo.png)
В верхней части показан мой файл-монстр, в котором я случайно оставил лишний байт с нулями. В каждом из трёх сэмплов — s1, s2 и s3 – по два байта, причем второй более значимый. Следовательно, при переводе этих пар байтов в десятичную форму мы получаем очень высокую амплитуду.
В то же время, в нижней части видно, что, если убрать нулевой байт, сэмплы читаются так, как и должны, и значения в аудиофайле укладываются в рамки разумного.
Выходит, если бы у меня было 8-битное аудио, тогда пропущенный лишний байт не вызвал бы никаких проблем. Но оно было 16-битным, и в итоге, я сдвинул всю последовательность в сэмплах, так что наименее значимый байт стал читаться как наиболее значимый.
Выводы
- Проверяйте звуковую волну аудиофайла, прежде чем врубать его на максимальной громкости
- Читайте (и принимайте к сведенью) спецификации
- Учитывайте, как код может повлиять на вас и других людей