Blog. Just Blog

Парсинг метаданных MP3-файлов на Ассемблере

Версия для печати Добавить в Избранное Отправить на E-Mail | Категория: Образ мышления: Assembler | Автор: ManHunter
Парсинг метаданных MP3-файлов на Ассемблере
Парсинг метаданных MP3-файлов на Ассемблере

В одной из предыдущих статей я рассказал, как можно получить данные из различных тегов MP3-файлов. Но, как выяснилось, системная реализация не в состоянии корректно обработать некоторые строковые данные. Например, название исполнителя "To/Die/For" обрезается до строки "To". Это связано с особенностями стандарта ID3v2.3, в котором символ слеша является служебным и используется для разделения нескольких значений. При этом в заголовке MP3-файла версия тегов может быть обозначена как ID3v2.4, в которой это ограничение снято, но система все равно будет обрабатывать данные по более старому стандарту. И вот опять, если хочешь сделать что-то хорошо, сделай это сам.

Устаревшие стандарты контейнеров ID3v1.0 и ID3v1.1 сложности в парсинге не представляют, там все размеры данных фиксированные и расположены по фиксированным смещениям. Несмотря на устаревание, в большинстве случаев редакторы тегов дублируют основные метаданные в ID3v1.0. Для получения данных из этого контейнера сперва читаем 128 байт от конца файла, а затем проверяем трехбайтовый заголовок, он должен быть равен строке "TAG". Данные тегов следуют прямо за ним. Размер строк для названия композиции, артиста и альбома неизменный и составляет 30 байт, все строки в кодировке Windows-1251. Читать их можно следующим образом:
  1.         ; ESI -> указатель на текущий тег
  2.         ; EDI -> указатель на буфер-приемник данных
  3. load_string_v1:
  4.         ; Загрузка строкового тега
  5.         push    esi
  6.         push    edi
  7.  
  8.         mov     edi,buff
  9.         invoke  RtlZeroMemory,edi,31
  10.         mov     ecx,30
  11.         rep     movsb
  12.  
  13.         ; Обрезать хвостовые пробелы
  14.         mov     esi,buff+31
  15. @@:
  16.         dec     esi
  17.         cmp     esi,buff
  18.         jbe     @f
  19.         cmp     byte[esi],0
  20.         je      @b
  21.         cmp     byte[esi],' '
  22.         je      @b
  23.         mov     byte[esi+1],0
  24. @@:
  25.         pop     edi
  26.         invoke  MultiByteToWideChar,1251,0,buff,30,0,0
  27.         invoke  MultiByteToWideChar,1251,0,buff,-1,edi,eax
  28.  
  29.         pop     esi
  30.         retn
Обратите внимание, что к строкам применяется условная команда rtrim, то есть обрезка пробелов с хвоста. Это нужно для того, чтобы не забивать строку мусором и оставить только значащие символы.

На смену ID3v1 пришел новый формат контейнера ID3v2. Заголовок контейнера ID3v2 любой версии состоит из 10 байт. Первые 3 символа - сигнатура "ID3", затем байт номера версии, после него два байта флагов, значения которых можно игнорировать. Длина содержимого контейнера ID3v2 представлена в виде 4-байтного значения, следующего за сигнатурой и флагами. Но не просто так. У каждого байта старший бит равен 0, а итоговое значение собирается из четырех 7-битных блоков. Такая рептилоидная логика кажется странной, но вот так решили разработчики стандарта. Для преобразования данных с длиной можно использовать следующий код:
  1.         ; ESI -> указатель на 4 байта с длиной
  2.         xor     ebx,ebx
  3.         mov     ecx,4
  4. @@:
  5.         shl     ebx,7
  6.         lodsb
  7.         or      bl,al
  8.         loop    @b
  9.         ; EBX -> декодированное значение длины
Разбор контейнеров ID3v2 начнем с общепринятого формата ID3v2.3 и современного ID3v2.4. В подавляющем большинстве случаев контейнер с тегами располагается в самом начале MP3-файла. Согласно спецификации, ID3v2.4 может быть записан и в конец файла, но опять же, на практике я подобного не встречал. Я даже не нашел никакой информации, каким образом программы должны загружать данные неизвестного размера, находящиеся в конце файла. Редакторы тегов, стараясь соблюсти максимальную совместимость с плеерами, все равно записывают метаданные в начало файла. Поэтому я буду считать, что контейнер с тегами находится или в начале файла, или отсутствует.

После заголовка контейнера следуют теги со своими подзаголовками, однако формат тегов в разных версиях отличается. В ID3v2.3 и ID3v2.4 заголовок каждого тега состоит из 10 байт. Первые 4 байта - название тега, которое может состоять из символов 'A'-'Z' и '0'-'9', затем 4-байтное значение длины данных тега, а затем 2 байта флагов, которые тоже можно игнорировать.

Может показаться, что парсинг таких структурированных данных не вызывает сложностей, но это только на первый взгляд. Проблема номер один. Размер контейнера может не совпадать с суммарным размером всех имеющихся в нем тегов. Это происходит по причине, что некоторые программы резервируют какое-то место для возможности дальнейшего редактирования тегов, чтобы лишний раз не перезаписывать весь файл целиком. Проблема номер два, точнее даже не проблема, а полный пипец. По существующему стандарту длина данных в четырехбайтном поле должна быть записана в виде уже упомянутых выше 7-битных байтов. Но, как выяснилось на практике, некоторые программы записывают длину в виде полноценного Big-endian DWORD'а. Причем этим грешит даже хваленый iTunes. Для небольших объемов данных это не имеет значения, но как только размер превышает 7Fh байт, преобразование длины вернет неправильный результат. Стандарты? Нет, не слышали.

Для решения первой проблемы надо проверять название текущего тега. В принципе, достаточно его соответствия формату. Если хоть один из четырех символов не проходит проверку, значит парсинг надо останавливать, так как ни к чему хорошему он уже не приведет. Со второй проблемой возни побольше. Единственный способ вычислить правильную длину данных в случае превышения ее 7Fh - это проверять данные, которые окажутся после перехода к следующему тегу с учетом вычисленной длины текущего тега. Как минимум название следующего тега должно соответствовать установленному формату, а новое смещение не должно выходить за границы контейнера. В зависимости от результатов этих проверок, или берется преобразованная длина, или длина рассматривается как Big-endian DWORD. Код для обработки стремительно распухает:
  1.         ; EBX -> текущий указатель на текущий тег
  2.         ; ESI -> указатель на размер данных
  3.         ; [sMem] -> размер контейнера
  4.         ; [pMem] -> указатель на начало контейнера
  5. ;---------------------------------------------
  6. ; Декодировать 7-битные байты ID3v2.3 и ID3v2.4
  7. ;---------------------------------------------
  8. decode_length3:
  9.         push    edx
  10.         push    eax
  11.         push    ebx
  12.  
  13.         xor     ebx,ebx
  14.         mov     ecx,4
  15. @@:
  16.         shl     ebx,7
  17.         lodsb
  18.         or      bl,al
  19.         loop    @b
  20.         mov     ecx,ebx
  21.  
  22.         ; Исправляем косяк длины тегов
  23.         cmp     ecx,7Fh
  24.         jbe     decode_done3
  25.  
  26.         pop     ebx
  27.         push    ebx
  28.  
  29.         ; Следующий указатель выходит за границы блока ID3?
  30.         mov     edx,ebx
  31.         add     edx,ecx
  32.         add     edx,10
  33.         cmp     edx,[sMem]
  34.         jbe     @f
  35. decode_fix3:
  36.         ; Длина в виде обычного DWORD'а
  37.         mov     ecx,dword[esi-4]
  38.         bswap   ecx
  39.         jmp     decode_done3
  40. @@:
  41.         add     edx,[pMem]
  42.  
  43.         ; Проверить 4 байта по следующему указателю
  44.         xor     ebx,ebx
  45. decode_check3:
  46.         mov     al,byte[edx+ebx]
  47.         cmp     al,'0'
  48.         jb      decode_fix3
  49.         cmp     al,'9'
  50.         jbe     @f
  51.         cmp     al,'A'
  52.         jb      decode_fix3
  53.         cmp     al,'Z'
  54.         ja      decode_fix3
  55. @@:
  56.         inc     ebx
  57.         cmp     ebx,4
  58.         jb      decode_check3
  59.  
  60. decode_done3:
  61.         pop     ebx
  62.         pop     eax
  63.         pop     edx
  64.  
  65.         ; ECX -> размер данных
В каких-то исключительных случаях даже для невалидной длины по новому смещению могут оказаться данные, внешне похожие на правильные. Вероятность такой ситуации ничтожно мала, так что ей можно пренебречь. Но если уж вам хочется сделать абсолютно пуленепробиваемую программу, то проверки надо усилить, например, добавив обработку флагов следующего тега, проверить его длину, проверить еще один следующий тег на основании этой длины, проверить вхождение названия тега в список поддерживаемых последним стандартом, или что-то подобное.

Размер данных у нас есть, теперь переходим к загрузке строковых значений из тегов. Строки могут быть как юникодные в кодировке UTF-8 или UTF-16, так и в обычной кодировке Windows-1251. Кодировка строки определяется первым байтом строковых данных: 0 - Windows-1251, 1 - UTF-16, 3 - UTF-8. Других значений я не нашел. Строке в кодировке UTF-16 дополнительно предшествует двухбайтный маркер BOM: 0xFF, 0xFE. При обработке строк этот маркер желательно тоже пропускать. Соответственно, начиная со второго или четвертого байта находится непосредственно сама строка. Определив кодировку, преобразуем строку в юникод или грузим как есть.
  1. ;---------------------------------------------
  2. ; Загрузка строкового тега
  3. ;---------------------------------------------
  4.         ; EBX -> указатель на текущий тег
  5.         ; ESI -> указатель на текущий тег
  6.         ; EDI -> указатель на буфер-приемник данных
  7. load_string_v23:
  8.         mov     esi,eax
  9.         add     esi,4
  10.         stdcall decode_length3
  11.         add     esi,2
  12.         add     ebx,10
  13.         add     ebx,ecx
  14.         cmp     byte [esi],1
  15.         je      load_unicode
  16.         cmp     byte [esi],3
  17.         je      load_utf8
  18. load_1251:
  19.         ; Строка в Windows-1251
  20.         inc     esi
  21.         dec     ecx
  22.         push    ecx
  23.         push    edi
  24.         push    ecx
  25.         invoke  RtlZeroMemory,buff,BUFF_SIZE*2
  26.         mov     edi,buff
  27.         pop     ecx
  28.         rep     movsb
  29.         pop     edi
  30.         pop     ecx
  31.         invoke  MultiByteToWideChar,1251,0,buff,ecx,0,0
  32.         invoke  MultiByteToWideChar,1251,0,buff,-1,edi,eax
  33.         jmp     load_done
  34. load_utf8:
  35.         ; Строка в UTF-8
  36.         inc     esi
  37.         dec     ecx
  38.         push    ecx
  39.         push    edi
  40.         push    ecx
  41.         invoke  RtlZeroMemory,buff,BUFF_SIZE*2
  42.         mov     edi,buff
  43.         pop     ecx
  44.         rep     movsb
  45.         pop     edi
  46.         pop     ecx
  47.         invoke  MultiByteToWideChar,CP_UTF8,0,buff,ecx,0,0
  48.         invoke  MultiByteToWideChar,CP_UTF8,0,buff,-1,edi,eax
  49.         jmp     load_done
  50. load_unicode:
  51.         ; Строка в Unicode
  52.         add     esi,3
  53.         sub     ecx,3
  54.         rep     movsb
  55. load_done:
Теперь парсинг сводится к поиску нужных тегов по их названиям или к выводу всех имеющихся метаданных, все зависит от поставленной задачи. К сожалению, таким способом не получится узнать продолжительность трека, для этого придется парсить уже структуру музыкальных фреймов. Кстати, несмотря на удобство использования, системные средства тоже не всегда возвращают правильное значение продолжительности трека. Но это уже совсем другая история.
  1.         ; Парсинг тегов
  2.         xor     ebx,ebx
  3. loc_parse_id3v23:
  4.         ; Достигнут конец ID3?
  5.         cmp     ebx,[sMem]
  6.         jae     loc_done
  7.         mov     eax,ebx
  8.         add     eax,[pMem]
  9.  
  10.         ; Проверить валидность названия тега
  11.         xor     ecx,ecx
  12. loc_check_v23:
  13.         mov     dl,byte[eax+ecx]
  14.         cmp     dl,'0'
  15.         jb      loc_done
  16.         cmp     dl,'9'
  17.         jbe     @f
  18.         cmp     dl,'A'
  19.         jb      loc_done
  20.         cmp     dl,'Z'
  21.         ja      loc_done
  22. @@:
  23.         inc     ecx
  24.         cmp     ecx,4
  25.         jb      loc_check_v23
  26.  
  27.         ; Album
  28.         cmp     dword[eax],'TALB'
  29.         jne     @f
  30.         mov     edi,album
  31.         stdcall load_string_v23
  32.         jmp     loc_parse_id3v23
  33. @@:
  34.         ; Artist
  35.         cmp     dword[eax],'TPE1'
  36.         jne     @f
  37.         mov     edi,artist
  38.         stdcall load_string_v23
  39.         jmp     loc_parse_id3v23
  40. @@:
  41.         ; Title
  42.         cmp     dword[eax],'TIT2'
  43.         jne     @f
  44.         mov     edi,title
  45.         stdcall load_string_v23
  46.         jmp     loc_parse_id3v23
  47. @@:
  48.         ; Следующий тег
  49.         mov     esi,eax
  50.         add     esi,4
  51.         stdcall decode_length3
  52.         add     ebx,10
  53.         add     ebx,ecx
  54.         jmp     loc_parse_id3v23
  55. loc_done:
Первая версия контейнера ID3v2.2, несмотря на былую популярность, безнадежно устарела, мне с трудом удалось найти в своей обширной музыкальной коллекции несколько файлов с ним, чтобы провести тестирование. Контейнер с тегами ID3v2.2 всегда располагается в самом начале MP3-файла. Как и в случае с ID3v2.3 и ID3v2.4, после заголовка идут теги, однако формат тегов тут другой. Длина заголовка каждого тега равняется 6 байтам. Первые 3 байта отводятся на название тега, затем три байта на его размер и после заголовка уже идут данные. Поскольку в описании ничего не сказано про формат хранения длины, а файлов для экспериментов у меня не так много, то рискну предположить, что длина тут тоже в 7-битных байтах. Немного модифицировав предыдущий алгоритм определения длины данных, получаем следующий код:
  1. ;---------------------------------------------
  2. ; Декодировать 7-битные байты ID3v2.2
  3. ;---------------------------------------------
  4.         ; EBX -> текущий указатель на текущий тег
  5.         ; ESI -> указатель на размер данных
  6.         ; [sMem] -> размер контейнера
  7.         ; [pMem] -> указатель на начало контейнера
  8. decode_length2:
  9.         push    edx
  10.         push    eax
  11.         push    ebx
  12.  
  13.         xor     ebx,ebx
  14.         mov     ecx,3
  15. @@:
  16.         shl     ebx,7
  17.         lodsb
  18.         or      bl,al
  19.         loop    @b
  20.         mov     ecx,ebx
  21.  
  22.         ; Исправляем косяк длины тегов
  23.         cmp     ecx,7Fh
  24.         jbe     decode_done2
  25.  
  26.         pop     ebx
  27.         push    ebx
  28.  
  29.         ; Следующий указатель выходит за границы блока ID3?
  30.         mov     edx,ebx
  31.         add     edx,ecx
  32.         add     edx,6
  33.         cmp     edx,[sMem]
  34.         jbe     decode_no_fix2
  35. decode_fix2:
  36.         ; Длина в виде обычного DWORD'а
  37.         sub     esi,3
  38.         xor     ebx,ebx
  39.         mov     ecx,3
  40. @@:
  41.         shl     ebx,8
  42.         lodsb
  43.         or      bl,al
  44.         loop    @b
  45.         mov     ecx,ebx
  46.         jmp     decode_done2
  47.  
  48. decode_no_fix2:
  49.         add     edx,[pMem]
  50.  
  51.         ; Проверить 3 байта по следующему указателю
  52.         xor     ebx,ebx
  53. decode_check2:
  54.         mov     al,byte[edx+ebx]
  55.         cmp     al,'0'
  56.         jb      decode_fix2
  57.         cmp     al,'9'
  58.         jbe     @f
  59.         cmp     al,'A'
  60.         jb      decode_fix2
  61.         cmp     al,'Z'
  62.         ja      decode_fix2
  63. @@:
  64.         inc     ebx
  65.         cmp     ebx,3
  66.         jb      decode_check2
  67.  
  68. decode_done2:
  69.         pop     ebx
  70.         pop     eax
  71.         pop     edx
  72.  
  73.         ; ECX -> размер данных
Для небольших размеров данных приведенный код работает безупречно, а на случай выхода за предельное значение 7Fh проверяется тег по новому смещению, как это было сделано в предыдущем варианте. Хуже точно не будет. Теперь у нас есть размер данных и есть тег, определяющий эти данные. Перед парсингом содержимого контейнера надо выяснить по поводу кодировок. В ID3v2.2 строки могут быть или в Windows-1251, или в UTF-16, других вариантов не предусмотрено. Кодировка определяется первым байтом строки: 0 - Windows-1251, 1 - UTF-16 с BOM. Так же, как и в современных версиях, при обработке строки пропускаем первый байт и, в случае UTF-16, дополнительно два байта BOM. Чтобы не плодить сущностей, тут используется обращение к коду из предыдущего загрузчика строк.
  1. ;---------------------------------------------
  2. ; Загрузка строкового тега ID3v2.2
  3. ;---------------------------------------------
  4. load_string_v22:
  5.         mov     esi,eax
  6.         add     esi,3
  7.         stdcall decode_length2
  8.         add     ebx,6
  9.         add     ebx,ecx
  10.         cmp     byte [esi],1
  11.         ; Загрузить как UTF-16
  12.         je      load_unicode
  13.         ; Загрузить как Windows-1251
  14.         jmp     load_1251
Ну и сам парсинг. Он мало отличается от обработки контейнеров ID3v2.3 и ID3v2.4, за исключением того, что трехбайтное название тега проверяется по кусочкам.
  1.         ; Парсинг тегов
  2.         xor     ebx,ebx
  3. loc_parse_id3v22:
  4.         ; Достигнут конец ID3?
  5.         cmp     ebx,[sMem]
  6.         jae     loc_done
  7.         mov     eax,ebx
  8.         add     eax,[pMem]
  9.  
  10.         ; Проверить валидность названия тега
  11.         xor     ecx,ecx
  12. loc_check_v22:
  13.         mov     dl,byte[eax+ecx]
  14.         cmp     dl,'0'
  15.         jb      loc_done
  16.         cmp     dl,'9'
  17.         jbe     @f
  18.         cmp     dl,'A'
  19.         jb      loc_done
  20.         cmp     dl,'Z'
  21.         ja      loc_done
  22. @@:
  23.         inc     ecx
  24.         cmp     ecx,3
  25.         jb      loc_check_v22
  26.  
  27.         ; Album
  28.         cmp     word[eax],'TA'
  29.         jne     @f
  30.         cmp     byte[eax+2],'L'
  31.         jne     @f
  32.         mov     edi,album
  33.         stdcall load_string_v22
  34.         jmp     loc_parse_id3v22
  35. @@:
  36.         ; Artist
  37.         cmp     word[eax],'TP'
  38.         jne     @f
  39.         cmp     byte[eax+2],'1'
  40.         jne     @f
  41.         mov     edi,artist
  42.         stdcall load_string_v22
  43.         jmp     loc_parse_id3v22
  44. @@:
  45.         ; Title
  46.         cmp     word[eax],'TT'
  47.         jne     @f
  48.         cmp     byte[eax+2],'2'
  49.         jne     @f
  50.         mov     edi,title
  51.         stdcall load_string_v22
  52.         jmp     loc_parse_id3v22
  53. @@:
  54.         ; Следующий тег
  55.         mov     esi,eax
  56.         add     esi,3
  57.         stdcall decode_length2
  58.         add     ebx,6
  59.         add     ebx,ecx
  60.         jmp     loc_parse_id3v22
  61.  
  62. loc_done:
Приведенных примеров будет достаточно, чтобы парсить метаданные не только для MP3-файлов, но и для других медиа-форматов, которые поддерживают стандарт ID3, но не могут быть обработаны средствами системы. Также если вы планируете обеспечивать совместимость ваших программ с Windows более ранних версий, чем Vista, то без ручного парсинга вам в принципе не обойтись.

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

Пример программы с исходным текстом (FASM)Пример программы с исходным текстом (FASM)

Parse.Metadata.Demo.zip (313,213 bytes)


Поделиться ссылкой ВКонтакте Поделиться ссылкой на Facebook Поделиться ссылкой на LiveJournal Поделиться ссылкой в Мой Круг Добавить в Мой мир Добавить на ЛиРу (Liveinternet) Добавить в закладки Memori Добавить в закладки Google
Просмотров: 394 | Комментариев: 5

Комментарии

Отзывы посетителей сайта о статье
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 то чего вдруг вспомнил? Стал попадаться на глаза у серьезных людей, в серьезных программах. И пришла мысль разобраться получше и сравнить с Питоном.
ManHunter (28.02.2022 в 11:32):
ЦитатаТема парсинга является, на мой взгляд, чуть ли не основой всего программирования.

Я бы сказал, что не именно парсинг, а обработка данных.

ЦитатаА можно что-то подобное сделать на javascript? Имеется в виду - парсинг метаданных MP3-файлов.

Можно, чо нет-то. Работа с файлами на JS вся расписана в предыдущих статьях, структура id3 расписана здесь, как загружать байты/дворды на JS - это тоже все есть.
ASMiral (27.02.2022 в 20:15):
ManHunter, спасибо за интересную тему/статью. Прям, не статья, а бриллиант, как метко подметили в предыдущей вашей статье на эту тему(Работа с метаданными MP3-файлов на Ассемблере). Тема парсинга является, на мой взгляд, чуть ли не основой всего программирования. Наверное нет ни одной более-менее серьезной программы, где бы не использовался парсинг.

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

А что это у вас там за хитрая папочка apiw? Что-то связанное с юникодом? У меня только API. Убрал последнюю букву - скомпилировалось и вроде, все работает. :)

P.S. Раньше как-то не обращал внимания, что у вас в блоге есть темы по javascript. Прям ностальгия. :) Я ж программированием и реверсингом увлекся благодаря javascript. С рекламой все пытался бороться. :) Я это все к чему? А можно что-то подобное сделать на javascript? Имеется в виду - парсинг метаданных MP3-файлов. Меня интересует сложно это будет или не очень? Опыт у меня в таких делах вообще нулевой, а у вас и статья про парсинг на javascript имеется. Правда там с фотографиями все связано, но я думаю, что принцип похожий должен быть. В общем, если не трудно, просветите.

Добавить комментарий

Заполните форму для добавления комментария
Имя*:
Текст комментария (не более 2000 символов)*:

*Все поля обязательны для заполнения.
Комментарии, содержащие рекламу, ненормативную лексику, оскорбления и т.п., а также флуд и сообщения не по теме, будут удаляться. Нарушителям может быть заблокирован доступ к сайту.
Наверх
Powered by PCL's Speckled Band Engine 0.2 RC3
© ManHunter / PCL, 2008-2022
При использовании материалов ссылка на сайт обязательна
Время генерации: 0.07 сек. / MySQL: 2 (0.0055 сек.) / Память: 4.75 Mb
Наверх