Парсинг метаданных MP3-файлов на Ассемблере
Парсинг метаданных MP3-файлов на Ассемблере
В одной из предыдущих статей я рассказал, как можно получить данные из различных тегов MP3-файлов. Но, как выяснилось, системная реализация не в состоянии корректно обработать некоторые строковые данные. Например, название исполнителя "To/Die/For" обрезается до строки "To". Это связано с особенностями стандарта ID3v2.3, в котором символ слеша является служебным и используется для разделения нескольких значений. При этом в заголовке MP3-файла версия тегов может быть обозначена как ID3v2.4, в которой это ограничение снято, но система все равно будет обрабатывать данные по более старому стандарту. И вот опять, если хочешь сделать что-то хорошо, сделай это сам.
Устаревшие стандарты контейнеров ID3v1.0 и ID3v1.1 сложности в парсинге не представляют, там все размеры данных фиксированные и расположены по фиксированным смещениям. Несмотря на устаревание, в большинстве случаев редакторы тегов дублируют основные метаданные в ID3v1.0. Для получения данных из этого контейнера сперва читаем 128 байт от конца файла, а затем проверяем трехбайтовый заголовок, он должен быть равен строке "TAG". Данные тегов следуют прямо за ним. Размер строк для названия композиции, артиста и альбома неизменный и составляет 30 байт, все строки в кодировке Windows-1251. Читать их можно следующим образом:
Code (Assembler) : Убрать нумерацию
- ; ESI -> указатель на текущий тег
- ; EDI -> указатель на буфер-приемник данных
- load_string_v1:
- ; Загрузка строкового тега
- push esi
- push edi
- mov edi,buff
- invoke RtlZeroMemory,edi,31
- mov ecx,30
- rep movsb
- ; Обрезать хвостовые пробелы
- mov esi,buff+31
- @@:
- dec esi
- cmp esi,buff
- jbe @f
- cmp byte[esi],0
- je @b
- cmp byte[esi],' '
- je @b
- mov byte[esi+1],0
- @@:
- pop edi
- invoke MultiByteToWideChar,1251,0,buff,30,0,0
- invoke MultiByteToWideChar,1251,0,buff,-1,edi,eax
- pop esi
- retn
На смену ID3v1 пришел новый формат контейнера ID3v2. Заголовок контейнера ID3v2 любой версии состоит из 10 байт. Первые 3 символа - сигнатура "ID3", затем байт номера версии, после него два байта флагов, значения которых можно игнорировать. Длина содержимого контейнера ID3v2 представлена в виде 4-байтного значения, следующего за сигнатурой и флагами. Но не просто так. У каждого байта старший бит равен 0, а итоговое значение собирается из четырех 7-битных блоков. Такая рептилоидная логика кажется странной, но вот так решили разработчики стандарта. Для преобразования данных с длиной можно использовать следующий код:
Code (Assembler) : Убрать нумерацию
- ; ESI -> указатель на 4 байта с длиной
- xor ebx,ebx
- mov ecx,4
- @@:
- shl ebx,7
- lodsb
- or bl,al
- loop @b
- ; EBX -> декодированное значение длины
После заголовка контейнера следуют теги со своими подзаголовками, однако формат тегов в разных версиях отличается. В ID3v2.3 и ID3v2.4 заголовок каждого тега состоит из 10 байт. Первые 4 байта - название тега, которое может состоять из символов 'A'-'Z' и '0'-'9', затем 4-байтное значение длины данных тега, а затем 2 байта флагов, которые тоже можно игнорировать.
Может показаться, что парсинг таких структурированных данных не вызывает сложностей, но это только на первый взгляд. Проблема номер один. Размер контейнера может не совпадать с суммарным размером всех имеющихся в нем тегов. Это происходит по причине, что некоторые программы резервируют какое-то место для возможности дальнейшего редактирования тегов, чтобы лишний раз не перезаписывать весь файл целиком. Проблема номер два, точнее даже не проблема, а полный пипец. По существующему стандарту длина данных в четырехбайтном поле должна быть записана в виде уже упомянутых выше 7-битных байтов. Но, как выяснилось на практике, некоторые программы записывают длину в виде полноценного Big-endian DWORD'а. Причем этим грешит даже хваленый iTunes. Для небольших объемов данных это не имеет значения, но как только размер превышает 7Fh байт, преобразование длины вернет неправильный результат. Стандарты? Нет, не слышали.
Для решения первой проблемы надо проверять название текущего тега. В принципе, достаточно его соответствия формату. Если хоть один из четырех символов не проходит проверку, значит парсинг надо останавливать, так как ни к чему хорошему он уже не приведет. Со второй проблемой возни побольше. Единственный способ вычислить правильную длину данных в случае превышения ее 7Fh - это проверять данные, которые окажутся после перехода к следующему тегу с учетом вычисленной длины текущего тега. Как минимум название следующего тега должно соответствовать установленному формату, а новое смещение не должно выходить за границы контейнера. В зависимости от результатов этих проверок, или берется преобразованная длина, или длина рассматривается как Big-endian DWORD. Код для обработки стремительно распухает:
Code (Assembler) : Убрать нумерацию
- ; EBX -> текущий указатель на текущий тег
- ; ESI -> указатель на размер данных
- ; [sMem] -> размер контейнера
- ; [pMem] -> указатель на начало контейнера
- ;---------------------------------------------
- ; Декодировать 7-битные байты ID3v2.3 и ID3v2.4
- ;---------------------------------------------
- decode_length3:
- push edx
- push eax
- push ebx
- xor ebx,ebx
- mov ecx,4
- @@:
- shl ebx,7
- lodsb
- or bl,al
- loop @b
- mov ecx,ebx
- ; Исправляем косяк длины тегов
- cmp ecx,7Fh
- jbe decode_done3
- pop ebx
- push ebx
- ; Следующий указатель выходит за границы блока ID3?
- mov edx,ebx
- add edx,ecx
- add edx,10
- cmp edx,[sMem]
- jbe @f
- decode_fix3:
- ; Длина в виде обычного DWORD'а
- mov ecx,dword[esi-4]
- bswap ecx
- jmp decode_done3
- @@:
- add edx,[pMem]
- ; Проверить 4 байта по следующему указателю
- xor ebx,ebx
- decode_check3:
- mov al,byte[edx+ebx]
- cmp al,'0'
- jb decode_fix3
- cmp al,'9'
- jbe @f
- cmp al,'A'
- jb decode_fix3
- cmp al,'Z'
- ja decode_fix3
- @@:
- inc ebx
- cmp ebx,4
- jb decode_check3
- decode_done3:
- pop ebx
- pop eax
- pop edx
- ; ECX -> размер данных
Размер данных у нас есть, теперь переходим к загрузке строковых значений из тегов. Строки могут быть как юникодные в кодировке UTF-8 или UTF-16, так и в обычной кодировке Windows-1251. Кодировка строки определяется первым байтом строковых данных: 0 - Windows-1251, 1 - UTF-16, 3 - UTF-8. Других значений я не нашел. Строке в кодировке UTF-16 дополнительно предшествует двухбайтный маркер BOM: 0xFF, 0xFE. При обработке строк этот маркер желательно тоже пропускать. Соответственно, начиная со второго или четвертого байта находится непосредственно сама строка. Определив кодировку, преобразуем строку в юникод или грузим как есть.
Code (Assembler) : Убрать нумерацию
- ;---------------------------------------------
- ; Загрузка строкового тега
- ;---------------------------------------------
- ; EBX -> указатель на текущий тег
- ; ESI -> указатель на текущий тег
- ; EDI -> указатель на буфер-приемник данных
- load_string_v23:
- mov esi,eax
- add esi,4
- stdcall decode_length3
- add esi,2
- add ebx,10
- add ebx,ecx
- cmp byte [esi],1
- je load_unicode
- cmp byte [esi],3
- je load_utf8
- load_1251:
- ; Строка в Windows-1251
- inc esi
- dec ecx
- push ecx
- push edi
- push ecx
- invoke RtlZeroMemory,buff,BUFF_SIZE*2
- mov edi,buff
- pop ecx
- rep movsb
- pop edi
- pop ecx
- invoke MultiByteToWideChar,1251,0,buff,ecx,0,0
- invoke MultiByteToWideChar,1251,0,buff,-1,edi,eax
- jmp load_done
- load_utf8:
- ; Строка в UTF-8
- inc esi
- dec ecx
- push ecx
- push edi
- push ecx
- invoke RtlZeroMemory,buff,BUFF_SIZE*2
- mov edi,buff
- pop ecx
- rep movsb
- pop edi
- pop ecx
- invoke MultiByteToWideChar,CP_UTF8,0,buff,ecx,0,0
- invoke MultiByteToWideChar,CP_UTF8,0,buff,-1,edi,eax
- jmp load_done
- load_unicode:
- ; Строка в Unicode
- add esi,3
- sub ecx,3
- rep movsb
- load_done:
Code (Assembler) : Убрать нумерацию
- ; Парсинг тегов
- xor ebx,ebx
- loc_parse_id3v23:
- ; Достигнут конец ID3?
- cmp ebx,[sMem]
- jae loc_done
- mov eax,ebx
- add eax,[pMem]
- ; Проверить валидность названия тега
- xor ecx,ecx
- loc_check_v23:
- mov dl,byte[eax+ecx]
- cmp dl,'0'
- jb loc_done
- cmp dl,'9'
- jbe @f
- cmp dl,'A'
- jb loc_done
- cmp dl,'Z'
- ja loc_done
- @@:
- inc ecx
- cmp ecx,4
- jb loc_check_v23
- ; Album
- cmp dword[eax],'TALB'
- jne @f
- mov edi,album
- stdcall load_string_v23
- jmp loc_parse_id3v23
- @@:
- ; Artist
- cmp dword[eax],'TPE1'
- jne @f
- mov edi,artist
- stdcall load_string_v23
- jmp loc_parse_id3v23
- @@:
- ; Title
- cmp dword[eax],'TIT2'
- jne @f
- mov edi,title
- stdcall load_string_v23
- jmp loc_parse_id3v23
- @@:
- ; Следующий тег
- mov esi,eax
- add esi,4
- stdcall decode_length3
- add ebx,10
- add ebx,ecx
- jmp loc_parse_id3v23
- loc_done:
Code (Assembler) : Убрать нумерацию
- ;---------------------------------------------
- ; Декодировать 7-битные байты ID3v2.2
- ;---------------------------------------------
- ; EBX -> текущий указатель на текущий тег
- ; ESI -> указатель на размер данных
- ; [sMem] -> размер контейнера
- ; [pMem] -> указатель на начало контейнера
- decode_length2:
- push edx
- push eax
- push ebx
- xor ebx,ebx
- mov ecx,3
- @@:
- shl ebx,7
- lodsb
- or bl,al
- loop @b
- mov ecx,ebx
- ; Исправляем косяк длины тегов
- cmp ecx,7Fh
- jbe decode_done2
- pop ebx
- push ebx
- ; Следующий указатель выходит за границы блока ID3?
- mov edx,ebx
- add edx,ecx
- add edx,6
- cmp edx,[sMem]
- jbe decode_no_fix2
- decode_fix2:
- ; Длина в виде обычного DWORD'а
- sub esi,3
- xor ebx,ebx
- mov ecx,3
- @@:
- shl ebx,8
- lodsb
- or bl,al
- loop @b
- mov ecx,ebx
- jmp decode_done2
- decode_no_fix2:
- add edx,[pMem]
- ; Проверить 3 байта по следующему указателю
- xor ebx,ebx
- decode_check2:
- mov al,byte[edx+ebx]
- cmp al,'0'
- jb decode_fix2
- cmp al,'9'
- jbe @f
- cmp al,'A'
- jb decode_fix2
- cmp al,'Z'
- ja decode_fix2
- @@:
- inc ebx
- cmp ebx,3
- jb decode_check2
- decode_done2:
- pop ebx
- pop eax
- pop edx
- ; ECX -> размер данных
Code (Assembler) : Убрать нумерацию
- ;---------------------------------------------
- ; Загрузка строкового тега ID3v2.2
- ;---------------------------------------------
- load_string_v22:
- mov esi,eax
- add esi,3
- stdcall decode_length2
- add ebx,6
- add ebx,ecx
- cmp byte [esi],1
- ; Загрузить как UTF-16
- je load_unicode
- ; Загрузить как Windows-1251
- jmp load_1251
Code (Assembler) : Убрать нумерацию
- ; Парсинг тегов
- xor ebx,ebx
- loc_parse_id3v22:
- ; Достигнут конец ID3?
- cmp ebx,[sMem]
- jae loc_done
- mov eax,ebx
- add eax,[pMem]
- ; Проверить валидность названия тега
- xor ecx,ecx
- loc_check_v22:
- mov dl,byte[eax+ecx]
- cmp dl,'0'
- jb loc_done
- cmp dl,'9'
- jbe @f
- cmp dl,'A'
- jb loc_done
- cmp dl,'Z'
- ja loc_done
- @@:
- inc ecx
- cmp ecx,3
- jb loc_check_v22
- ; Album
- cmp word[eax],'TA'
- jne @f
- cmp byte[eax+2],'L'
- jne @f
- mov edi,album
- stdcall load_string_v22
- jmp loc_parse_id3v22
- @@:
- ; Artist
- cmp word[eax],'TP'
- jne @f
- cmp byte[eax+2],'1'
- jne @f
- mov edi,artist
- stdcall load_string_v22
- jmp loc_parse_id3v22
- @@:
- ; Title
- cmp word[eax],'TT'
- jne @f
- cmp byte[eax+2],'2'
- jne @f
- mov edi,title
- stdcall load_string_v22
- jmp loc_parse_id3v22
- @@:
- ; Следующий тег
- mov esi,eax
- add esi,3
- stdcall decode_length2
- add ebx,6
- add ebx,ecx
- jmp loc_parse_id3v22
- loc_done:
UPD. Как показала практика, аудиофайлы в формате AAC (Advanced Audio Coding) имеют точно такую же структуру тегов, так что эти файлы можно обрабатывать вообще без изменения кода.
В приложении примеры программ с исходными текстами, которыя самостоятельно парсят и выводят данные из тегов MP3-файла и AAC-файла. Приоритет отдается данным из ID3v2, недостающие значения по возможности подгружаются из ID3v1.
Просмотров: 1319 | Комментариев: 11
Метки: Assembler, мультимедиа
Внимание! Статья опубликована больше года назад, информация могла устареть!
Комментарии
Отзывы посетителей сайта о статье
ManHunter
(25.11.2022 в 14:01):
Потому что за такую множественность надо изначально дрючить телеграфным столбом. Есть же нормальный формат для записей, где тянули несколько участников, такое встречается постоянно:
PERFORMER=а (feat. дэ & е)
LYRICIST=жэ, и
PERFORMER=а (feat. дэ & е)
LYRICIST=жэ, и
Лестер Глючный
(25.11.2022 в 13:50):
Но ведь тогда может получится так, что записи окажутся разроненными:
PERFORMER=а
TUNETITLE=бэ
RELEASEMONTH=вэ
RELEASEYEAR=гэ
PERFORMER=дэ
PERFORMER=е
ALBUM=ё
LYRICIST=жэ
COMPOSER=зэ
LYRICIST=и?
Да, мне тоже по началу нравилось такое разделение, когда в винампе встретилось (Vorbis?), но, как видно, оно удобно не всегда (т.е. фактически, ещё и сортировать придётся).
Если уж разделять так, то в программе (проигрывателе/редакторе меток) с «фиксированными для этих меток полями» лучше банальным "ENTER"`ом разделять множественные значения (что и приведёт к генерации 0x000D и 0x000A).
PERFORMER=а
TUNETITLE=бэ
RELEASEMONTH=вэ
RELEASEYEAR=гэ
PERFORMER=дэ
PERFORMER=е
ALBUM=ё
LYRICIST=жэ
COMPOSER=зэ
LYRICIST=и?
Да, мне тоже по началу нравилось такое разделение, когда в винампе встретилось (Vorbis?), но, как видно, оно удобно не всегда (т.е. фактически, ещё и сортировать придётся).
Если уж разделять так, то в программе (проигрывателе/редакторе меток) с «фиксированными для этих меток полями» лучше банальным "ENTER"`ом разделять множественные значения (что и приведёт к генерации 0x000D и 0x000A).
ManHunter
(20.11.2022 в 14:38):
Немного не соглашусь. Стандартом должна быть одна запись - одно значение, без какого-либо искусственного разделения, которое в любой момент может оказаться невалидным. Как там Илон Маск своего отпрыска назвал?
Лестер Глючный
(20.11.2022 в 14:33):
За разделение множественных значений слешем/точкой с запятой (боже упаси) "стандартизатору" ID3 влупить бы клавиатурой по зубам!
Реальным стандартом должны являться 0x000D, 0x000A, в крайнем случае 0x0009.
Реальным стандартом должны являться 0x000D, 0x000A, в крайнем случае 0x0009.
ManHunter
(12.11.2022 в 00:15):
Добавил пример для формата AAC
ASMiral
(02.03.2022 в 17:27):
Да, видимо, мое последнее сообщение было расценено как излишняя навязчивость. Хотел как лучше, а получилось... :)
ManHunter
(01.03.2022 в 01:11):
Если бы интересовало что-то еще, я бы об этом писал на сайте. Нет публикаций = нет интереса.
ASMiral
(28.02.2022 в 20:41):
ManHunter, я тут чё удумал-то? :) Услышал тут от бабушек возле подъезда незнакомое слово - коллаборация. Стал искать что оно обозначает в толковом словаре и пришел к выводу, что это именно то, чего нам с вами не хватает. :) Но для начала хотел задать несколько наводящих вопросов:
1. Насколько вы дружны с Питоном? Интересен ли он вам как язык или не особо?
2. Как вы относитесь к языкам C/C++? Вы с ними хоть немного соприкасаетесь или они вас, вообще, не заботят? Потому как, семья, нехватка свободного времени и т.д и т.п.
P.S. Я про JS то чего вдруг вспомнил? Стал попадаться на глаза у серьезных людей, в серьезных программах. И пришла мысль разобраться получше и сравнить с Питоном.
1. Насколько вы дружны с Питоном? Интересен ли он вам как язык или не особо?
2. Как вы относитесь к языкам C/C++? Вы с ними хоть немного соприкасаетесь или они вас, вообще, не заботят? Потому как, семья, нехватка свободного времени и т.д и т.п.
P.S. Я про JS то чего вдруг вспомнил? Стал попадаться на глаза у серьезных людей, в серьезных программах. И пришла мысль разобраться получше и сравнить с Питоном.
ManHunter
(28.02.2022 в 11:32):
Я бы сказал, что не именно парсинг, а обработка данных.
Можно, чо нет-то. Работа с файлами на JS вся расписана в предыдущих статьях, структура id3 расписана здесь, как загружать байты/дворды на JS - это тоже все есть.
ASMiral
(27.02.2022 в 20:15):
ManHunter, спасибо за интересную тему/статью. Прям, не статья, а бриллиант, как метко подметили в предыдущей вашей статье на эту тему(Работа с метаданными MP3-файлов на Ассемблере). Тема парсинга является, на мой взгляд, чуть ли не основой всего программирования. Наверное нет ни одной более-менее серьезной программы, где бы не использовался парсинг.
Главное пример получился очень удачным, в плане понимания работы кода. Несмотря на множество разных сравнений и скачков. В общем, пробежался по исходнику, скомпилировал - все, вроде, работает и, пока, вроде, все понятно.
А что это у вас там за хитрая папочка apiw? Что-то связанное с юникодом? У меня только API. Убрал последнюю букву - скомпилировалось и вроде, все работает. :)
P.S. Раньше как-то не обращал внимания, что у вас в блоге есть темы по javascript. Прям ностальгия. :) Я ж программированием и реверсингом увлекся благодаря javascript. С рекламой все пытался бороться. :) Я это все к чему? А можно что-то подобное сделать на javascript? Имеется в виду - парсинг метаданных MP3-файлов. Меня интересует сложно это будет или не очень? Опыт у меня в таких делах вообще нулевой, а у вас и статья про парсинг на javascript имеется. Правда там с фотографиями все связано, но я думаю, что принцип похожий должен быть. В общем, если не трудно, просветите.
Главное пример получился очень удачным, в плане понимания работы кода. Несмотря на множество разных сравнений и скачков. В общем, пробежался по исходнику, скомпилировал - все, вроде, работает и, пока, вроде, все понятно.
А что это у вас там за хитрая папочка apiw? Что-то связанное с юникодом? У меня только API. Убрал последнюю букву - скомпилировалось и вроде, все работает. :)
P.S. Раньше как-то не обращал внимания, что у вас в блоге есть темы по javascript. Прям ностальгия. :) Я ж программированием и реверсингом увлекся благодаря javascript. С рекламой все пытался бороться. :) Я это все к чему? А можно что-то подобное сделать на javascript? Имеется в виду - парсинг метаданных MP3-файлов. Меня интересует сложно это будет или не очень? Опыт у меня в таких делах вообще нулевой, а у вас и статья про парсинг на javascript имеется. Правда там с фотографиями все связано, но я думаю, что принцип похожий должен быть. В общем, если не трудно, просветите.
Добавить комментарий
Заполните форму для добавления комментария
А есть ещё “упущенные артикли”, “Имя Фамилия vs Фамилия Имя” — тут уже сложнее понять идентичность… Вроде как бы и есть "пользовательские" поля, но кто их использует?… А вставлять новую строку в такое – мало кому в голову придёт.
Если всем плевать на сортировку, и всегда искать кого-то в фонотеке только по конкретному свойству (Advanced Query Search) — ну, придётся вспоминать и остальные буквы, а не только первые две (как обычно бывает)… А раз так, тогда проще уж вообще без тегов обходится, и всю инфу вписывать в имя файла — с Everything быстрее найдётся (с ним банально пробелом разделяю «забытые буквы»)… а MAX_PATH… вроде не должно волновать invoke`ющих Nt…|Zw…
Мне бы ещё вообще возможность указывать локализированные названия, т.е. не только на родном, но и "интернациональном", или вообще даже тех, на которые были переведены другими (напр. японском, французском), иногда даже вместе с текстом!