Меломаны и просто любители хорошей музыки порой сталкиваются с необходимостью автоматической конвертации и аннотирования аудиофайлаов своей музыкальной коллекции. В этой статье рассмотрим способ автоматической конвертации и добавления аннотации к аудиофайлам с помощью инструмента FFmpeg и сценариев CMD.
Автоматическая конвертация аудиофайлов в Windows
Существует множество различных программ для конвертации аудиофайлов и редактирования тегов, но почти все из них — оболочки утилит командной строки вокруг одного‑двух проектов с открытым исходным кодом, в которых как раз и реализована вся необходимая функциональность. Как правило, в основе лежат несколько утилит командной строки.
Итак. У нас есть музыкальные альбомы, в виде наборов аудиофайлов. Необходимо добавить к данным аудиофайлам изображение обложки альбома и описание из текстового файла, а также есть желание конвертировать эти файлы в формат MP3 с определенными параметрами аудиоданных. Для решения этой задачи попробуем использовать утилиту FFmpeg и язык сценариев CMD.EXE.
FFmpeg для автоматической конвертации файлов
В начале я сомневался в идее применения FFmpeg, но мои сомнения были развеяны этой статьей. Идея в том, что теги указываются утилите FFmpeg в виде параметров:
1 |
-metadata НазваниеТега="ЗначениеТега" |
Кроме того, в статье есть пример использования файла txt в качестве источника тегов. К моему сожалению, для реализации этого надо для каждого аудифайла создавать отдельные текстовые файлы с тегами, тогда как намного удобнее все сведения о содержимом музыкального альбома держать в одном текстовом файле.
Указывать полный путь к исполняемому файлу FFmpeg в командной строке крайне неудобно. Как правило во время установки программы предлагается включить путь в системную переменную окружения PATH, но лично я не люблю ее засорять. Вместо этого рекомендую поместить в папку, уже присутствующую в данной переменной, командный файл ffmpeg.cmd такого содержания:
1 |
@"%ProgramFiles%\ImageMagick-7.1.0-Q8\ffmpeg.exe" %* |
Символ @ подавляет эхо‑печать командной строки, а вместо сочетания %* при выполнении будут подставлены все параметры, переданные сценарию ffmpeg.cmd. Таким образом, использовать FFmpeg из командной строки для конвертации файла SRC\sample.wav в TGT\sample.mp3 можно следующим образом:
1 |
ffmpeg.cmd -i SRC\sample.wav TGT\sample.mp3 |
Можно также использовать FFmpeg для того чтобы добавить к MP3-файлу изображением обложки.Изображение обложки — для MP3-файла видеопоток и, для слкеивания его с аудиоданными, воспользуемся командой вида:
1 |
ffmpeg -i SRC\audio.mp3 -i SRC\cover.jpg -map 0 -map 1 -id3v2_version 3 -metadata:s:v title="Album cover" TGT\audio.mp3 |
С помощью параметра -map источники данных из входных файлов отображаются в потоки выходного файла:
- Первый опция определяет первый поток, ее параметр говорит о том, что надо использовать первый указанный с помощью опции -i источник, в приведенном примере это аудиоданные из файла SRC\audio.mp3.
- Вторая опция определяет второй поток, ее параметр 1 говорит, что надо использовать второй указанный с помощью опции -i источник — изображение из файла SRC\cover.jpg.
По умолчанию FFmpeg автоматически кодирует потоки в соответствии со своими предустановками так, чтобы они отвечали формату выходного файла (определяется по расширению). Например, аудио она конвертирует кодеком LAME с частотой дискретизации 44 100 Гц и скоростью потока 128 Кбит/с, а изображение преобразует в формат PNG.
Если манипуляции с обложкой еще можно простить, то описанные действия со звуком меломаны могут воспринять как личное оскорбление. К счастью, можно запретить программе выполнять какие бы то ни было преобразования, указав в командной строке опцию -c copy. Или запретить преобразовывать только аудио, конкретизировав опцию так: -c:a copy.
Опция -id3v2_version 3 требует использовать версию 2.3 тегов ID3. Без нее записанное в выходной файл изображение не будет отыскиваться и демонстрироваться проигрывателями. Наоборот, опция -metadata:s:v title=»Album cover», назначающая потоку ([b]s[/b]tream) с видеоданными ([b]v[/b]ideo) атрибут title со значением «Album cover», служит только декоративной цели.
Теги ID3 должны назначаться контейнеру MP3 в целом, а не отдельному содержащемуся в нем потоку. Поэтому для них опция -metadata используется без дополнительных спецификаторов.
В моей аудиоколлекции наряду с музыкальными произведениями обнаружились аудиоподкасты, записанные с высоким качеством. Восприятие этих материалов на слух не страдает, если понизить частоту дискретизации (sample rate) до 22 кГц и свести стереозвук в монодорожку. Конкретизировать параметры преобразования аудиопотока программе FFmpeg можно с помощью таких опций:
1 |
... -c:a libmp3lame -ar 22050 -ac 1 ... |
Опция -c предписывает выполнять обработку аудиопотока кодеком Lame MP3, опция -ar задает частоту дискретизации аудиопотока 22 050 Гц, а опция -ac требует свести звук в одну дорожку. После такого преобразования размер MP3-файлов значительно уменьшился. Допустимые значения частоты дискретизации в порядке убывания качества (Гц): 48 000, 44 100, 32 000, 24 000, 22 050, 16 000, 11 025, 8000.
Управлять качеством звукового потока можно и с помощью опции, позволяющей задать максимальную скорость передачи данных (bitrate). Например, для ограничения скорости до 96 Кбит/с надо указать: -b:a 96k.
Возможности CMD-сценариев
Язык CMD-сценариев базируется на крайне ограниченном языке пакетных batch-файлов, который изначально использовался только для выполнения одной и той же линейной последовательности команд операционной системы. Но со временем инструкция FOR была расширена для того, чтобы с ее помощью можно было обрабатывать текстовые файлы, выполнять синтаксический разбор строк и присваивать переменным сценария значения на основе обнаруженных фрагментов.
Подробную справку о возможностях инструкции FOR выгрузит в текстовый файл help-for.txt такая последовательность команд:
123 CHCP 1251HELP FOR > help-for.txtCHCP 866Команда CHCP устанавливает кодировку символов, которая используется в сеансе командной строки. По умолчанию действует кодировка CP866 (DOS), с которой не умеет работать штатный текстовый редактор «Блокнот». Чтобы читать полученный текстовый файл в блокноте, надо перед формированием файла установить кодировку CP1251 (Windows). В результате изучать документацию можно с помощью команды
1 notepad.exe help-for.txt
Разберем принцип работы языка сценариев с текстовыми файлами на следующем примере. Предположим, у нас есть CSV-файл employees.csv со следующим содержимым в кодировке CP1251:
1 2 3 4 5 6 |
# ШТАТНОЕ РАСПИСАНИЕ # ООО «Админ на час» # ТабельныйНомер;Фамилия;Имя;Отчество;Должность;Оклад 1;Иванов;Иван;Иванович;директор;300 2;Петров;Петр;Петрович;главбух;200 3;Сидоров;Сидор;Сидорович;сисадмин;100 |
Применим к нему команду test-for.cmd employees.csv, где файл test-for.cmd содержит следующий сценарий:
1 2 3 4 |
@ECHO OFF CHCP 1251 > NUL: FOR /F "eol=# tokens=2,3,5 delims=;" %%I IN (%1) DO ECHO %%J %%I - %%K CHCP 866 > NUL: |
Ключ /F инструкции FOR заставляет ее обрабатывать содержащиеся в текстовом файле строки с именем, переданным в качестве первого параметра командной строки, в соответствии с описанием структуры текстового файла, которое заключено в кавычки. Элемент eol этого описания задает символ (он может быть только один), которым начинается однострочный комментарий (в приведенном примере это символ решетки #). Такой выбор позволяет исключить из обработки первые три строки файла employees.csv. Пустые строки в обработке тоже не участвуют, поэтому и его четвертая строка будет пропущена.
В элементе tokens через запятую перечисляются порядковые номера интересующих разработчика информационных полей, значения которых разделяются в обрабатываемой строке символами‑разделителями, указанными в элементе delims. В приведенном примере это второе, третье и пятое поле, что соответствует фамилии, имени и должности. В качестве разделителя будет использован символ точка с запятой, но при необходимости в элементе delims можно указать один за другим сразу несколько символов‑разделителей.
Несколько идущих подряд разделителей считаются одним, а стоящий в самом начале строки разделитель игнорируется. Последнее свойство инструкции FOR позволит организовать хранение и обработку двух списков в одном текстовом файле.
После описания структуры в инструкции FOR указывается имя переменной, которой присвоится значение первого поля обработанной строки. В приведенном примере фамилия будет записана в переменную %%I. Значения остальных полей будут присвоены переменным, следующим после указанной в алфавитном порядке. В рассматриваемом примере фамилия и должность попадут в переменные %%J и %%K соответственно.
На каждой итерации цикла FOR переменные инициализируются значениями соответствующих полей, после чего они могут использоваться в команде, которая записывается в инструкции FOR после слова DO. В приведенном примере это команда ECHO, печатающая строку вида «Имя Фамилия — Должность». В результате обработки файла employees.csv на экран будут выведены строки:
1 2 3 |
Иван Иванов - директор Петр Петров - главбух Сидор Сидоров - сисадмин |
Команды CHCP в сценарии test-for.cmd нужны только для того, чтобы корректно отображался текст в Windows-кодировке, а конструкции … > NUL: использованы для подавления вывода их информационных сообщений «Текущая кодовая страница такая-то».
Модернизации подвергся и механизм вызова подпрограмм. Если в BAT-сценариях инструкция CALL позволяла только передать управление находящемуся в другом файле BAT-сценарию с последующим возвратом, то в CMD-сценариях появилась возможность временной передачи управления внутри выполняющегося сценария. К сожалению, после перехода по метке отсутствует возможность досрочного возврата в точку вызова, то есть подпрограммой считается код от метки до конца файла.
Работу подпрограммы рассмотрим на исправленном сценарии, который не только выводит информацию о сотрудниках, но и подсчитывает фонд заработной платы.
1 2 3 4 5 6 7 8 9 10 11 |
@ECHO OFF SET TOTAL=0 CHCP 1251 > NUL: FOR /F "eol=# tokens=2,3,5,6 delims=;" %%I IN (%1) DO CALL :CALCULATE %%I %%J %%K %%L ECHO *** Фонд зарплаты: %TOTAL% *** CHCP 866 > NUL: SET TOTAL= GOTO :EOF :CALCULATE ECHO %2 %1 - %3 (%4) SET /A TOTAL=%TOTAL%+%4 |
Поскольку теперь на каждой итерации цикла требуется выполнять не одну команду, а несколько, то удобно в операторе FOR заменить команду печати ECHO командой вызова подпрограммы CALL. После слова CALL указывается метка строки, с которой начинается подпрограмма (в приведенном примере это :CALCULATE), а дальше следуют аргументы, значения которых в подпрограмме будут присвоены параметрам %1, %2, %3 и так далее по количеству аргументов. В приведенном примере их четыре: Фамилия, Имя, Должность и Оклад.
В подпрограмме :CALCULATE выполняются все необходимые для обработки полей информационной строки действия. Во‑первых, команда ECHO печатает информацию о сотруднике, дополненную величиной его оклада. Во‑вторых, инструкция SET (тоже модернизированная и дополненная признаком выполнения арифметических операций /A) увеличивает значение переменной TOTAL, инициализированной нулевым значением, на величину оклада сотрудника.
warning
Как бы тебе ни хотелось украсить листинг сценария, ни в коем случае не вставляй пробелы в инструкции SET между именем переменной, знаком равенства и присваиваемым значением. В этой инструкции каждый пробел имеет значение!
Когда цикл FOR завершит обработку информационных строк файла со штатным расписанием, следующей командой ECHO будет распечатан общий фонд зарплаты, после чего команда SET удалит переменную TOTAL. Одна из важнейших ролей в этом заключительном блоке отведена команде безусловного перехода GOTO, которая служит барьером между основной программой и подпрограммой. Она передает управление на виртуальный конец сценария, которому соответствует предопределенная метка :EOF, в результате чего сценарий завершает свою работу.
Разработка прототипа командного сценария
Теперь, когда возможности FFmpeg и языка командных сценариев прояснились, можно приступать к решению нашей задачи — разработке сценария для конвертации и назначения ID3-тегов аудиофайлам из коллекции. Для начала решим, как будет храниться информация о тегах.
Аудиофайлы обычно существуют не сами по себе, а сгруппированы в альбомы, которые издатели записывают на один информационный носитель: компакт‑диск, виниловую пластинку и прочее. Поэтому в одном информационном текстовом файле будем хранить теги для одного такого альбома. Если проанализировать множество ID3-тегов, то можно выделить в нем две категории: теги, которые относятся ко всему альбому в целом (например, название альбома и исполнитель альбома), и теги, которые относятся к отдельному треку (номер трека на носителе, название трека, исполнитель трека).
Чтобы не дублировать для каждого аудиофайла «альбомные» теги, запишем их один раз в заголовке информационного файла. Теги для отдельных треков удобно хранить в строках текстового файла с разделителями. В качестве разделителя будем использовать символ, встретить который в значениях тегов маловероятно. Я выбрал символ тильда ~. А для альбомных тегов, которые естественно представлять в виде пар «ТЕГ=ЗНАЧЕНИЕ», было решено использовать в качестве разделителя знак равенства =.
Вырисовывается следующая структура информационного файла:
1 2 3 4 5 |
ALBUM=Название альбома ARTIST=Исполнитель альбома 1~Название первого трека~Исполнитель первого трека 2~Название второго трека~Исполнитель второго трека ... |
Ориентируясь на разработанную структуру, набросаем прототип командного сценария, который будет ее использовать для обработки аудиофайлов.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
@ECHO OFF REM =================== REM Основная программа. REM =================== CHCP 65001 > NUL: SET ID3TAGS=SRC\id3tags.txt SET ALBUM_TITLE= SET ALBUM_ARTIST= FOR /F "eol=~ tokens=1,2* delims==" %%I IN (%ID3TAGS%) DO CALL :PROCESSING header %%I "%%J" ECHO Альбом......... %ALBUM_TITLE% ECHO Исполнитель.... %ALBUM_ARTIST% FOR /F "eol== tokens=1,2,3 delims=~" %%I IN (%ID3TAGS%) DO CALL :PROCESSING body %%I "%%J" "%%K" SET ALBUM_TITLE= SET ALBUM_ARTIST= CHCP 866 > NUL: GOTO :EOF REM ============================== REM Подпрограмма обработки записи. REM ============================== :PROCESSING IF "%1"=="header" GOTO :HEADER IF "%1"=="body" GOTO :BODY ECHO *** Ошибка: неизвестный способ обработки "%1". *** GOTO :EOF REM ------------------------------------ REM Обработка записи из заголовка файла. REM ------------------------------------ :HEADER SHIFT IF "%1"=="ALBUM" SET ALBUM_TITLE=%2 IF "%1"=="ARTIST" SET ALBUM_ARTIST=%2 GOTO :EOF REM ------------------------------- REM Обработка записи из тела файла. REM ------------------------------- :BODY SHIFT ECHO DEBUG: %1 %2 %3 |
Поскольку для тегов предпочтительно использовать кодировку Unicode, то все текстовые файлы, как со сценарием, так и со значениями тегов, должны быть сохранены в формате UTF-8. В соответствии с этим для сеанса командной строки устанавливается кодовая страница 65001. Предполагается, что исходные аудиофайлы будут храниться в подкаталоге SRC, а преобразованные файлы — записываться в подкаталог TGT текущего каталога. Информация о тегах будет храниться вместе с исходными файлами в текстовом файле SRC\id3tags.txt описанной выше структуры.
При сохранении текстовых файлов в формате UTF-8 обращай внимание, чтобы они не предварялись маркером последовательности байтов BOM. Командный процессор не умеет его правильно обрабатывать, в результате чего он не распознает первую команду @ECHO OFF и при работе сценария на экран выводится много «мусора». В крайнем случае можно сделать первую строку сценария пустой, но и тогда его вывод будет начинаться с сообщения:
12 "я╗┐" не является внутренней или внешнейкомандой, исполняемой программой или пакетным файлом.
Поскольку в информационном файле присутствуют два блока разной структуры, он будет обрабатываться в два прохода двумя циклами FOR. Первый цикл присвоит значения альбомных тегов переменным окружения сценария с тем, чтобы их можно было в дальнейшем использовать для каждого аудиофайла. Второй цикл на каждой итерации будет запускать FFmpeg для обработки аудиофайла.
И здесь нужно найти метод, который позволит различать информационные строки первого и второго типов. Это можно сделать разными способами. Я решил использовать символ — разделитель полей одного блока в качестве признака комментария для другого блока и в начало каждой информационной строки добавить символ‑разделитель, после чего структура информационного файла приняла такой вид:
1 2 3 4 5 |
=ALBUM=Название альбома =ARTIST=Исполнитель альбома ~1~Название первого трека~Исполнитель первого трека ~2~Название второго трека~Исполнитель второго трека ... |
После этого первый цикл воспринимает строки второго блока как комментарии, а второй цикл так же относится к строкам первого блока.
Средства работы с подпрограммами в языке CMD-сценариев довольно ограниченны. Из‑за отсутствия инструкции досрочного возврата из подпрограммы пришлось моделировать в одной подпрограмме работу нескольких виртуальных подпрограмм. Для этого через аргумент инструкции CALL передается имя виртуальной подпрограммы («header» или «body»), которое анализируется в начале настоящей подпрограммы (:PROCESSING). После этого выполняется переход к блоку инструкций вызванной виртуальной подпрограммы (на метку :HEADER или :BODY). Чтобы исключить ставшее ненужным имя подпрограммы из списка полученных параметров, виртуальные подпрограммы начинаются с инструкции SHIFT.
Если сейчас выполнить приведенный прототип сценария, то на экране отобразятся следующие строки:
1 2 3 4 |
Альбом......... "Название альбома" Исполнитель.... "Исполнитель альбома" DEBUG: 1 "Название первого трека" "Исполнитель первого трека" DEBUG: 2 "Название второго трека" "Исполнитель второго трека" |
Значит, разбор текстового файла со значениями тегов работает так, как задумано, и можно переходить к реализации обработки аудиофайлов с помощью FFmpeg.
Сценарий пакетной обработки аудиофайлов
Для выполнения реальных операций над файлами надо в прототипе сценария заменить отладочный вывод ECHO DEBUG… на блок инструкций, которые эти операции будут выполнять. Я оформил этот блок следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
SET MP3FN=%1.mp3 IF NOT EXIST SRC\%MP3FN% SET MP3FN=0%1.mp3 IF NOT EXIST SRC\%MP3FN% SET MP3FN=00%1.mp3 IF NOT EXIST SRC\%MP3FN% GOTO :BODY_FNF SET SRCFN="SRC\%MP3FN%" SET TGTFN="TGT\%MP3FN%" ECHO Конвертация файла %SRCFN% -^> %TGTFN% %FFMPEG% -i %SRCFN% -i %COVER% -map 0 -map 1 -c:a libmp3lame -ar %FQ% -b:a %BR% -ac %AC% ^ -f mp3 -id3v2_version 3 -map_metadata -1 ^ -metadata album=%ALBUM_TITLE% -metadata album_artist=%ALBUM_ARTIST% ^ -metadata track="%1" -metadata title=%2 -metadata artist=%3 ^ -metadata:s:v title="Album cover" %TGTFN% GOTO :EOF :BODY_FNF ECHO *** Ошибка: не найден файл для трека №%1. *** |
Короткое имя файла по его порядковому номеру формируется и записывается в переменную MP3FN первой инструкцией SET. В моих коллекциях аудиофайлы хранятся под своими порядковыми номерами в альбомах: 01.mp3, 02.mp3, 03.mp3, …, 10.mp3, 11.mp3… В сценарии для универсальности я предположил, что номер в имени файла может не содержать ведущего нуля: 1.mp3, 2.mp3 и так далее. Чтобы автоматически обрабатывались файлы вплоть до трехразрядных номеров, дальше следуют инструкции IF, добавляющие ведущие нули к имени файла, если таковой отсутствует в папке SRC. Если отсутствует файл и с трехразрядным номером, то будет выдано сообщение об ошибке.
В случае успеха в переменные SRCFN и TGTFN записываются полные имена входного и выходного аудиофайлов, значения которых используются в командной строке FFmpeg. Командная строка получилась довольно длинной, и, чтобы разбить ее на части, пришлось воспользоваться символом экранирования перевода строки, роль которого в командной строке Windows играет не обратная наклонная черта \, как можно было бы подумать, а крышка ^.
Параметры командной строки FFmpeg должны быть понятны из предыдущих примеров. Разве что может возникнуть резонный вопрос, откуда возьмутся значения переменных: имя файла с изображением обложки %COVER%, частота дискретизации %FQ%, скорость потока %BR% и количество аудиодорожек %AC%. Действительно, их надо определить в начале сценария, дополнив блок инициализации переменных окружения строками
1 2 3 4 5 |
SET FFMPEG="%ProgramFiles%\ImageMagick-7.1.0-Q8\ffmpeg.exe" SET COVER=SRC\cover.jpg SET FQ=22050 SET BR=32k SET AC=1 |
Получившийся сценарий я применил для обработки аудиозаписей лекций цикла «Восток (Лекторий ВШЭ)», стереофайлы которых с частотой дискретизации 44 100 Гц при скорости потока 128 Кбит занимали на диске 877 Мбайт. Объем преобразованных файлов уменьшился до 219 Мбайт (то есть примерно в четыре раза), а тембр речи лекторов субъективно даже улучшился.
Доработка сценария
По большому счету поставленную в этой статье задачу уже можно считать решенной. Однако, как известно, начав программировать (пусть даже на языке сценариев), потом очень трудно остановиться. В описании тегов ID3 можно обнаружить, что допускается нумерация трека в формате «i/n», где i — порядковый номер трека, а n — общее количество треков в альбоме. Почему бы не воспользоваться арифметическими способностями инструкции SET и не подсчитать это количество, чтобы формировать тег номера в расширенном формате?
Конечно, это потребует еще одной переменной для подсчета треков и дополнительного прохода по информационному файлу с помощью инструкции FOR с вызовом подпрограммы, выполняющей инкрементирование значения переменной для каждой обнаруженной записи об аудиофайле.
Добавим в блок инициализации переменных строку
1 |
SET TOTAL=0 |
А после первой инструкции FOR, выполняющей инициализацию альбомных переменных, добавим строку для подсчета треков:
1 |
FOR /F "eol== tokens=1 delims=~" %%I IN (%ID3TAGS%) DO SET /A TOTAL=TOTAL+1 |
Теперь можно расширить блок печати информации об обрабатываемом альбоме строкой
1 |
ECHO Треков......... %TOTAL% |
Осталось в командной строке FFmpeg заменить назначения ID3-тега с номером трека на -metadata track=»%1/%TOTAL%». С этого момента сценарий начнет аннотировать MP3-файлы номером трека в расширенном формате.
Здесь можно загрузить исходные данные и код, использованные в статье.
Заключение
В результате проделанной работы мы получили удобный инструмент для пакетной обработки аудиофайлов. Если принять во внимание, что FFmpeg умеет еще и обрабатывать видеофайлы и файлы с изображениями, а сценарий автоматизации без особых проблем адаптируется для решения других подобных задач, то инструмент этот можно назвать универсальным. При этом графическая оболочка не скрывает от нас возможностей базовой утилиты, а для программирования достаточно штатного текстового редактора операционной системы.