forked from pcl-ru/pcl-ru
-
Notifications
You must be signed in to change notification settings - Fork 0
/
chapter-25.tex
1586 lines (1329 loc) · 109 KB
/
chapter-25.tex
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
\chapter{Практика: разбор ID3}
\label{ch:25}
\thispagestyle{empty}
Имея библиотеку для разбора двоичных данных, вы уже готовы к созданию кода для чтения и
записи в каком-то реальном двоичном формате, например формате тегов ID3. Теги ID3
используются для хранения дополнительной информации в звуковых файлах MP3. Работа с тегами
ID3 будет хорошей проверкой для библиотеки для работы с двоичными данными, потому что
формат ID3~--- это настоящий, используемый на практике формат~--- смесь инженерных
компромиссов и характерных решений, которые тем не менее выполняют своё назначение. На
тот случай, если вы случайно пропустили революцию свободного обмена данными, вот краткий
обзор того, что представляют собой теги ID3 и как они относятся к файлам MP3.
MP3, или звуковой слой 3 для MPEG\pclfootnote{MPEG Audio Layer 3.},~--- это формат для хранения
сжатых звуковых данных, разработанный исследователями из Фраунгоферовского института
ин\-тег\-раль\-ных схем и стандартизованный <<Группой экспертов кино>>\pclfootnote{Moving Picture
Experts Group.}, объединённым комитетом организаций ISO\pclfootnote{International
Organization for Standardization.} и IEC\pclfootnote{International Electrotechnical
Commission.}. Однако формат MP3 сам по себе определяет только то, как хранить звуковые
данные. Это не страшно до тех пор, пока все ваши звуковые файлы обрабатываются каким-то
одним приложением, которое может хранить эти метаданные вне звуковых файлов, сохраняя их
связь со звуковыми файлами. Однако как только люди стали обмениваться файлами MP3 через
Интернет через такие файлообменные системы, как Napster, они быстро обнаружили, что нужно
как-то вставлять метаданные внутрь самих файлов MP3.
Поскольку стандарт MP3 уже был оформлен и значительная часть программного обеспечения и
оборудования уже была написана и спроектирована, причём так, что они знали, как
декодировать существующий формат файлов MP3, то любая схема внедрения информации в файл
MP3 была бы такой, что эта информация вынужденно была бы невидима декодерам файлов MP3. Вот
тут и появился ID3.
Первоначально формат ID3, изобретённый программистом Эриком Кэмпом (Eric Kemp),
представлял собой 128 байт, прилепленных в конце файла MP3, где их не замечало бы
большинство программ, работающих с MP3. Эта информация состояла из четырёх
тридцатибуквенных полей, являвшихся названием песни, названием альбома, именем исполнителя
и комментарием, одного четырёхбайтового поля года и одного однобайтового поля кода жанра
произведения. Кэмп придумал стандартные значения первых 80 кодов жанров. Nullsoft,
производитель программы Winamp, очень популярного MP3-плеера, позже добавил в этот список
ещё что-то около 60 жанров.
Этот формат было легко разбирать, но он был достаточно ограничен. Не было способа
сохранить названия более чем в 30 символов, было ограничение в 256 жанров, и значения
кодов жанров должны были одинаково восприниматься всеми пользователями, использующими
ID3. Не было даже способа сохранить номер дорожки на исходном диске до тех пор, пока
другой программист, Микаэль Мутшлер (Michael Mutschler), не предложил вставлять
номер дорожки в поле комментария, отделяя его от остального комментария нулевым байтом,
так чтобы существующее ПО, использующее ID3, которое предположительно читало бы до
первого нулевого символа в каждом текстовом поле, игнорировало бы его. Версия Кэмпа теперь
называется <<ID3 версия 1>> (ID3v1), а версия Мутшлера~--- <<ID3 версия 1.1>> (ID3v1.1).
Предложения первой версии, как бы ограничены не были, являлись хотя бы частичным
решением проблемы хранения метаданных, так что они были применены многими программами
копирования музыки\pclfootnote{Выдирание (ripping)~--- процесс, при помощи которого аудио-CD
преобразуется в MP3-файл на вашем жёстком диске. В~наши дни большинство таких программ
ещё и автоматически получают информацию о песнях, которые они выдирают, из онлайновых
баз данных, таких так Gracenote (the Compact Disc Database [CDDB]) или FreeDB, которую
они затем встраивают в MP3-файлы и ID3-теги.}, сохранявшими теги ID3 в файлах MP3, и
MP3-плеерами, вытаскивавшими эту информацию из тегов ID3 и показывавшими их пользователю.
Однако к 1998 году все эти ограничения стали совсем уже раздражающими, и новая группа
разработчиков, возглавляемая Мартином Нильсоном (Martin Nilsson), начала работу
над совершенно новой схемой хранения метаданных, которую назвали ID3v2. Формат ID3v2 крайне гибок,
разрешает включать много видов информации практически без ограничения длины. Также он
берёт на вооружение некоторые особенности формата MP3-файла, для того чтобы разместить
теги ID3v2 в начале файла MP3.
Однако разбирать теги в формате ID3v2~--- задача, значительно более сложная, чем теги в
формате версии~1. В~этой главе мы будем использовать библиотеку разбора бинарных данных из
предыдущей главы, для того чтобы разработать код, который сможет читать и писать теги в
формате ID3v2. Ну или, по крайней мере, сделаем какое-то приемлемое начало, поскольку если
ID3v1 достаточно прост, то ID3v2 порой причудлив до невозможности. Реализация всех
закоулков и потаённых уголков спецификации была бы порядочно сложной работой, особенно
если бы вы хотели поддержать все три версии, которые были документированы. На самом деле
вы можете игнорировать многие возможности в этих спецификациях, поскольку они очень редко
используются в <<дикой природе>>. В~качестве закуски вы можете опустить поддержку всей
версии~2.4, поскольку она не была широко воспринята и в основном всего лишь добавляла
некую вовсе не нужную гибкость, по сравнению с версией~2.3. Я сконцентрируюсь на версиях~2.2
и~2.3, потому что обе они широко используются и достаточно сильно отличаются друг от
друга, чтобы сделать нашу работу интересной.
\section{Структура тега ID3v2}
До того, как начать кодировать, вам нужно познакомиться с общей структурой тегов
ID3v2. Каждый тег начинается с заголовка, содержащего информацию о теге в общем. Первые
три байта заголовка содержат строку <<ID3>> в кодировке ISO-8859-1. То есть это байты с
кодами 73, 68 и~51. Затем идут два байта, которые кодируют <<старшую версию>> и ревизию
спецификации ID3, которой тег намеревается соответствовать. Далее идёт один байт, биты
которого интерпретируются как различные флаги. Значение каждого из флагов зависит от
версии спецификации. Некоторые из флагов могут влиять на то, как обрабатывается весь тег
целиком. Байты <<старшей версии>> на самом деле используются для записи младшей версии
спецификации, в то время как ревизия используется для хранения подверсии
спецификации. Таким образом, поле <<старшая версия>> тега, соответствующего спецификации
версии 2.3.0, будет 3. Поле ревизии всег\-да равно нулю, поскольку каждая новая спецификация
ID3v2 увеличивала младшую версию, оставляя подверсию нулём. Значение, хранимое в поле
старшей версии тега, как вы увидите, имеет сильное влияние на то, как надо разбирать всю
оставшуюся часть тега.
Последнее поле в заголовке тега~--- это число, закодированное в четырёх байтах, в каждом из
которых используется лишь по семь бит, содержащее размер всего тега без учёта заголовка.
В~тегах версии 2.3 в заголовке может быть ещё несколько дополнительных полей; всё
остальное~--- это данные, разделённые на фреймы. Разные типы фреймов хранят разные виды
информации: от простого текста вроде названия песни до встроенного изображения. Каждый
фрейм начинается с заголовка, содержащего строковый идентификатор и размер. В~версии 2.3
заголовок фрейма также содержит два байта флагов и~--- при выставленном флаге~---
дополнительный однобайтовый код, указывающий, как закодирован остаток фрейма.
Фреймы~--- идеальный пример помеченных структур данных: чтобы распарсить тело
фрейма, надо прочитать заголовок и использовать идентификатор для определения того, какой вид
данных вы читаете.
Заголовок ID3 не указывает прямо, сколько фреймов в теге,~--- он говорит, насколько тот
большой, но раз фреймы могут быть разной длины, единственным способом узнать количество
фреймов будет прочитать их данные. К тому же размер, записанный в заголовке, может быть
больше, чем реальное количество байт в данных фреймов; после фреймов могут идти нули для
выравнивания под указанный размер. Это позволяет программам изменять тег без
переписывания всего MP3-файла\pclfootnote{Почти все файловые системы предоставляют
возможность перезаписывать существующие байты файла, но немногие~--- если вообще такие
есть~--- дают возможность добавлять или удалять данные в начало или середину файла без
необходимости перезаписать остаток файла. Так как теги ID3 обычно хранятся в начале
файла, чтобы перезаписать тег ID3, не трогая оставшуюся часть файла, вы должны заменить
старый тег новым точно такой же длины. Записывая теги ID3 с некоторым количеством
заполнения, вы получаете лучшие шансы сделать так: если в новом теге будет больше
данных, чем в первоначальном, вы используете меньше заполнителя, а если короче~---
больше.}.
Итак, наши главные задачи: чтение заголовка ID3; определение версии, 2.2 или~2.3; чтение
данных всех фреймов до конца тега или до блока выравнивания.
\section{Определение пакета}
Как и с другими библиотеками, которые мы разработали ранее, тот код, который мы напишем в
этой главе, стоит поместить в отдельный пакет. Нам надо будет обращаться к функциям
из библиотек binary и pathname из глав~\ref{ch:15} и~\ref{ch:24} и надо экспортировать
имена функций, которые составляют API этого пакета. Определим его так:
\begin{myverb}
(defpackage :com.gigamonkeys.id3v2
(:use :common-lisp
:com.gigamonkeys.binary-data
:com.gigamonkeys.pathnames)
(:export
:read-id3
:mp3-p
:id3-p
:album
:composer
:genre
:encoding-program
:artist
:part-of-set
:track
:song
:year
:size
:translated-genre))
\end{myverb}
Как обычно, вы можете, и наверное, вам даже следует, заменить <<com.gigamonkeys>> в имени
пакета на ваш собственный домен.
\section{Типы целых}
Можно начать с определения бинарных типов для чтения и записи некоторых примитивов,
использующихся в формате ID3,
нескольких целочисленных типов разного размера и четырёх видов строк.
ID3 использует беззнаковые целые, закодированные в одном, двух, трёх или четырёх байтах.
Если вы сначала напишете обобщённый двоичный тип \lstinline{unsigned-integer},
который принимает в качестве параметра количество читаемых байтов, то затем
с помощью короткой формы \lstinline{define-binary-type} можно будет определять
конкретные типы. Обобщённый тип \lstinline{unsigned-integer} выглядит следуюшим образом:
\begin{myverb}
(define-binary-type unsigned-integer (bytes)
(:reader (in)
(loop with value = 0
for low-bit downfrom (* 8 (1- bytes)) to 0 by 8 do
(setf (ldb (byte 8 low-bit) value) (read-byte in))
finally (return value)))
(:writer (out value)
(loop for low-bit downfrom (* 8 (1- bytes)) to 0 by 8
do (write-byte (ldb (byte 8 low-bit) value) out))))
\end{myverb}
Теперь можно пользоваться короткой формой define-binary-type для определения типов для
каждого размера целого из формата ID3:
\begin{myverb}
(define-binary-type u1 () (unsigned-integer :bytes 1))
(define-binary-type u2 () (unsigned-integer :bytes 2))
(define-binary-type u3 () (unsigned-integer :bytes 3))
(define-binary-type u4 () (unsigned-integer :bytes 4))
\end{myverb}
Еще один тип, который надо уметь читать и писать,~--- это 28-битное значение из заголовка.
Это размер, закодированный не как обычно~--- количеством бит, кратным~8, таким как~32, а
28-ю, потому что тег ID3 не может содержать байт \lstinline!#xff!, за
которым идут три включённых бита~--- такая последовательность для
MP3-декодеров имеет особое значение. В~принципе, ни одно поле в заголовке ID3 не может содержать такую
последовательность байтов, но если бы размер тега был закодирован обычным беззнаковым
целым, то были бы проблемы. Чтобы исключить такую возможность, размер кодируется в семи
младших битах каждого байта, все старшие всегда нули\pclfootnote{Данные фреймов, идущих за
заголовком ID3, также потенциально могут содержать эту незаконную
последовательность. Это предотвращается использованием специальной схемы, которая
включается при помощи одного из флагов в заголовке тега. Код из этой главы не принимает
в расчёт возможность установки этого флага, он редко используется на практике.}.
Таким образом, оно может быть считано и записано во многом как беззнаковое целое, только
размер байта, который передаётся в LDB, должен быть 7, а не 8. Это сходство наводит на
мысль, что если добавить параметр bits-per-byte к существующему бинарному типу
unsigned-integer, тогда можно определить новый тип id3-tag-size, используя короткую форму
define-binary-type. Новая версия unsigned-integer такая же, как старая, только
bits-per-byte заменяет прописанную везде в старой восьмёрку. Выглядит так:
\begin{myverb}
(define-binary-type unsigned-integer (bytes bits-per-byte)
(:reader (in)
(loop with value = 0
for low-bit downfrom (* bits-per-byte (1- bytes)) to 0 by bits-per-byte do
(setf (ldb (byte bits-per-byte low-bit) value) (read-byte in))
finally (return value)))
(:writer (out value)
(loop for low-bit downfrom (* bits-per-byte (1- bytes)) to 0 by bits-per-byte
do (write-byte (ldb (byte bits-per-byte low-bit) value) out))))
\end{myverb}
Теперь определение id3-tag-size становится тривиальным:
\begin{myverb}
(define-binary-type id3-tag-size () (unsigned-integer :bytes 4 :bits-per-byte 7))
\end{myverb}
Также надо изменить определения u1-u4 для указания, что там 8 бит в байте:
\begin{myverb}
(define-binary-type u1 () (unsigned-integer :bytes 1 :bits-per-byte 8))
(define-binary-type u2 () (unsigned-integer :bytes 2 :bits-per-byte 8))
(define-binary-type u3 () (unsigned-integer :bytes 3 :bits-per-byte 8))
(define-binary-type u4 () (unsigned-integer :bytes 4 :bits-per-byte 8))
\end{myverb}
\section{Типы строк}
Еще один из примитивных типов, который повсеместен в теге ID3,~--- это строка.
В~предыдущей главе мы обсудили некоторые вещи, на которые надо обратить
внимание, когда имеешь дело со строками в бинарных файлах, такие как разница между кодом
знака и кодировкой.
ID3 использует две разные кодировки: ISO 8859-1 и Unicode. ISO 8859-1, также известный как
Latin-1,~--- это 8-битная кодировка, которая дополняет ASCII буквами
из языков Восточной Европы. Другими словами, одни и те же коды от 0 до 127 указывают на
одни и те же знаки ASCII и ISO 8859-1, но ISO 8859-1 содержит также символы с кодами до 255.
Unicode~--- это кодировка, сделанная, чтобы обеспечить кодом практически каждый
знак всех на свете языков. Unicode~--- надмножество ISO 8859-1, так же
как ISO 8859-1~--- надмножество ASCII: коды 0--255 отображаются на одни и те же знаки ISO
8859-1 и Unicode. (Таким образом, Unicode ещё и надмножество ASCII.)
Поскольку ISO 8859-1 является 8-битной кодировкой, она использует один байт на
знак. Для Unicode-строк ID3 использует кодировку UCS-2 с меткой порядка байтов\pclfootnote{В
ID3v2.4 UCS-2 заменили на почти идентичную ей UTF-16 и добавили дополнительные кодировки
UTF-16BE и UTF-8.}. Через пару мгновений я расскажу, что это такое.
Чтение и запись этих двух кодировок не является проблемой~--- это всего лишь вопрос чтения
и записи беззнаковых чисел в разных форматах, и мы только что написали код для этого.
Трюк в том, чтобы перевести эти числовые значения в объекты знаков языка Lisp.
Ваша реализация Lisp, возможно, использует или Unicode, или ISO 8859-1 в качестве внутренней
кодировки. И раз все значения от 0 до 255 отображаются на одни и те же знаки в ISO 8859-1
и Unicode, то можно использовать функции \lstinline{CODE-CHAR} и \lstinline{СHAR-CODE} для их транслирования в обе
кодировки. Однако, если ваш Lisp поддерживает только ISO 8859-1, тогда можно будет
в качестве символов Lisp использовать только первые 255 символов Unicode. Другими словами, в
такой реализации Lisp, если вы попробуете обработать тег ID3, который использует строки
Unicode, и любая из этих строк содержит знак с кодом, большим 255, то вы получите ошибку,
когда попытаетесь перевести этот код в символ Lisp. Пока будем считать, что мы
или используем Lisp, поддерживающий Unicode, или не будем работать с файлами, содержащими
знаки вне досягаемости ISO 8859-1.
Ещё один момент с кодированием строк состоит в том, что необходимо выяснить, какое количество
байт следует интерпретировать как символьные данные.
ID3 использует две стратегии, рассмотренные в предыдущей главе: некоторые
строки заканчиваются нулевым символом, тогда как другие встречаются на позициях, по
которым можно определить количество байт для считывания: или когда строка в том
расположении всегда одной длины, или когда она в конце составной структуры, чей размер
известен. Тем не менее обратите внимание, что количество байт не обязательно совпадает с
количеством знаков в строке.
Складывая все эти варианты вместе, получим, что формат ID3 использует четыре способа
чтения и записи строк: два вида знаков на два вида разграничения строковых данных.
Очевидно, значительная часть логики чтения и записи строк будет полностью совпадать. Так
что можно начать с определения двух бинарных типов: один для чтения строк фиксированной длины
(в знаках) и другой для чтения строк, заканчивающихся заданным символом.
Оба пользуются тем, что тип, передаваемый в \lstinline{read-value} и \lstinline{write-value},~---
это такие же данные; тип читаемого символа можно сделать параметром
этих типов. Этой техникой мы будем пользоваться в данной главе довольно часто.
\begin{myverb}
(define-binary-type generic-string (length character-type)
(:reader (in)
(let ((string (make-string length)))
(dotimes (i length)
(setf (char string i) (read-value character-type in)))
string))
(:writer (out string)
(dotimes (i length)
(write-value character-type out (char string i)))))
(define-binary-type generic-terminated-string (terminator character-type)
(:reader (in)
(with-output-to-string (s)
(loop for char = (read-value character-type in)
until (char= char terminator) do (write-char char s))))
(:writer (out string)
(loop for char across string
do (write-value character-type out char)
finally (write-value character-type out terminator))))
\end{myverb}
С этими типами несложно будет прочитать строки ISO 8859-1. Поскольку \lstinline{character-type},
который передаётся в \lstinline{read-value} и \lstinline{write-value}, должен быть именем бинарного типа, то надо
определить \lstinline{iso-8859-1-char}. Здесь же неплохо организовать проверку корректности читаемых
и записываемых кодов символов.
\begin{myverb}
(define-binary-type iso-8859-1-char ()
(:reader (in)
(let ((code (read-byte in)))
(or (code-char code)
(error "Character code ~d not supported" code))))
(:writer (out char)
(let ((code (char-code char)))
(if (<= 0 code #xff)
(write-byte code out)
(error "Illegal character for iso-8859-1 encoding: character: ~c with code: ~d"
char code)))))
\end{myverb}
Теперь определение строк ISO 8859-1 становится тривиальным:
\begin{myverb}
(define-binary-type iso-8859-1-string (length)
(generic-string :length length :character-type 'iso-8859-1-char))
(define-binary-type iso-8859-1-terminated-string (terminator)
(generic-terminated-string :terminator terminator :character-type 'iso-8859-1-char))
\end{myverb}
Чтение строк UCS-2 лишь немногим сложнее. Трудности возникают из-за того, что можно
кодировать UCS-2 двумя способами: в порядке байтов от старшего к младшему (big-endian) или
от младшего к старшему (little-endian). Поэтому строки UCS-2 начинаются с двух
дополнительных байтов, которые называются меткой порядка байтов, состоящих из числового
значения \lstinline!#xfeff!, закодированных или в порядке big-endian, или в little-endian.
При чтении строки UCS-2 надо прочитать метку порядка байтов, а потом, в зависимости от её
значения, читать знаки в порядке big-endian или в little-endian. Так что понадобятся два
разных типа знаков UCS-2. Но нужна только одна версия кода проверки корректности. Значит,
можно определить параметризованный бинарный тип:
\begin{myverb}
(define-binary-type ucs-2-char (swap)
(:reader (in)
(let ((code (read-value 'u2 in)))
(when swap (setf code (swap-bytes code)))
(or (code-char code) (error "Character code ~d not supported" code))))
(:writer (out char)
(let ((code (char-code char)))
(unless (<= 0 code #xffff)
(error "Illegal character for ucs-2 encoding: ~c with char-code: ~d" char code))
(when swap (setf code (swap-bytes code)))
(write-value 'u2 out code))))
\end{myverb}
\noindent{}где функция \lstinline{swap-bytes} определена ниже, с использованием преимущества функции LDB, с
которой можно делать SETF и, соответственно, ROTATEF.
\begin{myverb}
(defun swap-bytes (code)
(assert (<= code #xffff))
(rotatef (ldb (byte 8 0) code) (ldb (byte 8 8) code))
code)
\end{myverb}
Используя ucs-2-char, определим два типа знаков, которые будут применяться в качестве
аргумента \lstinline{character-type} функций обобщённых строк.
\begin{myverb}
(define-binary-type ucs-2-char-big-endian () (ucs-2-char :swap nil))
(define-binary-type ucs-2-char-little-endian () (ucs-2-char :swap t))
\end{myverb}
Затем нужна функция, которая возвращает тип знаков, которые будут использоваться в
зависимости от метки порядка байтов.
\begin{myverb}
(defun ucs-2-char-type (byte-order-mark)
(ecase byte-order-mark
(#xfeff 'ucs-2-char-big-endian)
(#xfffe 'ucs-2-char-little-endian)))
\end{myverb}
Теперь можно определить оба строковых типа для строк UCS-2,
которые читают метку порядка байтов и определяют, какой вариант знаков UCS-2 передавать в
качестве аргумента \lstinline{character-type} в \lstinline{read-value} и \lstinline{write-value}. Остаётся только учесть,
что надо переводить аргумент length, который дан в байтах, в количество
читаемых знаков, учитывая при этом метку порядка байтов.
\begin{myverb}
(define-binary-type ucs-2-string (length)
(:reader (in)
(let ((byte-order-mark (read-value 'u2 in))
(characters (1- (/ length 2))))
(read-value
'generic-string in
:length characters
:character-type (ucs-2-char-type byte-order-mark))))
(:writer (out string)
(write-value 'u2 out #xfeff)
(write-value
'generic-string out string
:length (length string)
:character-type (ucs-2-char-type #xfeff))))
(define-binary-type ucs-2-terminated-string (terminator)
(:reader (in)
(let ((byte-order-mark (read-value 'u2 in)))
(read-value
'generic-terminated-string in
:terminator terminator
:character-type (ucs-2-char-type byte-order-mark))))
(:writer (out string)
(write-value 'u2 out #xfeff)
(write-value
'generic-terminated-string out string
:terminator terminator
:character-type (ucs-2-char-type #xfeff))))
\end{myverb}
\section{Заголовок тега ID3}
Закончив с основными примитивными типами, мы готовы перейти к более общей картине и начать
определять бинарные классы для представления сначала тега ID3 в целом, а потом и отдельных
фреймов.
Если заглянуть в спецификацию ID3v2.2, то мы увидим, что в основе структуры тега такой заголовок:
\begin{myverb}
ID3/file identifier "ID3"
ID3 version $02 00
ID3 flags %xx000000
ID3 size 4 * %0xxxxxxx
\end{myverb}
%$
за которым идут данные фреймов и выравнивание. Поскольку мы уже определили типы для
чтения и записи всех полей в этом заголовке, определение класса, который сможет читать
заголовок ID3,~--- это всего лишь вопрос их объединения.
\begin{myverb}
(define-binary-class id3-tag ()
((identifier (iso-8859-1-string :length 3))
(major-version u1)
(revision u1)
(flags u1)
(size id3-tag-size)))
\end{myverb}
Если у вас под рукой есть какой-нибудь MP3-файл, вы можете проверить всю эту кучу кода и
заодно посмотреть, какую версию тега ID3 он содержит. Для начала напишем функцию, которая
считывает только что определённый id3-tag из начала файла. Надо понимать тем не менее,
что тег ID3 не обязан находиться в начале файла, хотя в наши дни он почти всегда там.
Чтобы найти тег ID3 где-то ещё в файле, последний можно просканировать в поисках
последовательности байтов 73, 68, 51 (другими словами, это строка <<ID3>>)\pclfootnote{Версия
2.4 формата ID3 также поддерживает размещение похожего окончания в конце тега, что
позволяет проще находить тег, присоединённый к концу файла.}. Правда, сейчас уже,
наверное, можно считать, что файлы начинаются с тегов.
\begin{myverb}
(defun read-id3 (file)
(with-open-file (in file :element-type '(unsigned-byte 8))
(read-value 'id3-tag in)))
\end{myverb}
На основе этой функции можно написать другую, которая получает имя файла и печатает
информацию из заголовка тега вместе с именем файла.
\begin{myverb}
(defun show-tag-header (file)
(with-slots (identifier major-version revision flags size) (read-id3 file)
(format t "~a ~d.~d ~8,'0b ~d bytes -- ~a~%"
identifier major-version revision flags size (enough-namestring file))))
\end{myverb}
Она выдаст примерно следующее:
\begin{myverb}
ID3V2> (show-tag-header "/usr2/mp3/Kitka/Wintersongs/02 Byla Cesta.mp3")
ID3 2.0 00000000 2165 bytes -- Kitka/Wintersongs/02 Byla Cesta.mp3
NIL
\end{myverb}
Конечно, чтобы определить, какая версия ID3 встречается чаще всего в вашей библиотеке,
лучше бы иметь функцию, которая выдаёт сводку по всем MP3-файлам в директории. Такую легко
реализовать с помощью функции walk-directory из главы~\ref{ch:15}. Для начала определим
вспомогательную функцию, которая проверяет, что у файла расширение MP3.
\begin{myverb}
(defun mp3-p (file)
(and
(not (directory-pathname-p file))
(string-equal "mp3" (pathname-type file))))
\end{myverb}
Затем соединим \lstinline{show-tag-header}, \lstinline{mp3-p} с \lstinline{walk-directory}, чтобы
печатать сводку по заголовкам ID3 в файлах в заданном каталоге.
\begin{myverb}
(defun show-tag-headers (dir)
(walk-directory dir #'show-tag-header :test #'mp3-p))
\end{myverb}
Однако, если у вас много MP3-файлов, вы можете пожелать просто посчитать, сколько тегов
ID3 каждой версии у вас в MP3-коллекции. Для получения этой информации можно было бы
написать такую функцию:
\begin{myverb}
(defun count-versions (dir)
(let ((versions (mapcar #'(lambda (x) (cons x 0)) '(2 3 4))))
(flet ((count-version (file)
(incf (cdr (assoc (major-version (read-id3 file)) versions)))))
(walk-directory dir #'count-version :test #'mp3-p))
versions))
\end{myverb}
Другая функция, которая понадобится в главе~\ref{ch:29}, для проверки, что файл
действительно начинается с тега ID3, которую можно определить вот так:
\begin{myverb}
(defun id3-p (file)
(with-open-file (in file :element-type '(unsigned-byte 8))
(string= "ID3" (read-value 'iso-8859-1-string in :length 3))))
\end{myverb}
\section{Фреймы ID3}
Как уже обсуждалось ранее, основная часть тега ID3 разделена на фреймы. Каждый фрейм
имеет структуру, похожую на структуру всего тега. Каждый фрейм начинается с заголовка,
указывающего вид фрейма и размер фрейма в байтах. Структура заголовка фрейма немного
разная у версий 2.2 и 2.3 формата ID3, и так получилось, что нам придётся работать с
обеими формами. Для начала сфокусируемся на разборе версии~2.2.
Заголовок в версии 2.2 состоит из трёх байт, которые кодируют трёхбуквенную ISO 8859-1
строку, за которой идёт трёхбайтовое беззнаковое число, задающее размер
фрейма в байтах без шестибайтового заголовка. Строка указывает тип фрейма, что
определяет, как мы будем разбирать данные. Это как раз та ситуация, для которой мы
определили макрос \lstinline{define-tagged-binary-class}. Мы можем определить помеченный класс,
который читает заголовок фрейма и затем подбирает подходящий конкретный класс, используя
функцию, которая отображает ID на имя класса.
\begin{myverb}
(define-tagged-binary-class id3-frame ()
((id (iso-8859-1-string :length 3))
(size u3))
(:dispatch (find-frame-class id)))
\end{myverb}
Теперь мы готовы начать строить реализацию конкретных классов фреймов. Однако
спецификация определяет достаточно большое количество типов фреймов~--- 63 в версии 2.2 и
еще больше в более поздних версиях. Даже считая типы фреймов, которые имеют общую
структуру, эквивалентными, мы все ещё получим 24 уникальных типа в версии 2.2. Но только
несколько из них используются на практике. Так что, вместо того чтобы сразу приступить к
определению классов для каждого из типа фреймов, вы можете начать с написания обобщенного
класса фреймов, который позволит вам читать фреймы в тег без разбора самих данных. Это
даст вам возможность определить, какие фреймы в самом деле присутствуют в файлах MP3,
которые вы хотите обрабатывать. Вам все равно понадобится этот класс, поскольку
спецификация разрешает включение эксперементальных фреймов, которые вам нужно будет уметь
читать без разбора данных в них.
Так как поле размера из заголовка фрейма точно говорит вам, какова длина фрейма в байтах,
вы можете определить класс \lstinline{generic-frame} (обобщённый фрейм), который расширяет \lstinline{id3-frame} и
добавляет единственное поле, data, которое будет содержать массив байт.
\begin{myverb}
(define-binary-class generic-frame (id3-frame)
((data (raw-bytes :size size))))
\end{myverb}
Тип поля data, \lstinline{raw-bytes}, должен просто содержать массив байт. Вы можете определить его
вот так:
\begin{myverb}
(define-binary-type raw-bytes (size)
(:reader (in)
(let ((buf (make-array size :element-type '(unsigned-byte 8))))
(read-sequence buf in)
buf))
(:writer (out buf)
(write-sequence buf out)))
\end{myverb}
На данный момент нам нужно, чтобы все фреймы читались как \lstinline{generic-frame}, так что можно
определить функцию \lstinline{find-frame-class}, которая используется в выражении \lstinline{:dispatch} в классе
\lstinline{id3-frame}, так чтобы она всегда возвращала \lstinline{generic-frame}, не обращая внимания на
индентификатор фрейма.
\begin{myverb}
(defun find-frame-class (id)
(declare (ignore id))
'generic-frame)
\end{myverb}
Вам придётся модифицицировать \lstinline{id3-tag} так, что он будет читать фреймы после полей
заголовка. Есть только одна маленькая трудность в чтении данных фреймов: несмотря на то
что заголовок тега указывает, каков его размер, в это число включён и заполнитель,
который может идти за данными фреймов. Так как заголовок тега не говорит вам, сколько
фреймов содержит тег, единственный способ определить, что вы натолкнулись на
заполнитель,~--- найти нулевой байт там, где вы ожидали идентификатор фрейма.
Чтобы управиться с этим, можно определить бинарный тип \lstinline{id3-frames}, который будет
ответственен за чтение остатка тега, создание объектов фреймов для представления всех
найденных фреймов и пропуск заполнителя. Этот тип будет принимать как параметр размер
тега, который он сможет использовать, чтобы избежать чтения за концом тега. Но читающему
коду ещё и придётся определять начало заполнителя, который может следовать за данными
фрейма в теге. Вместо того чтобы вызывать \lstinline{read-value} прямо в форме \lstinline{:reader} типа
\lstinline{id3-frames}, лучше использовать функцию \lstinline{read-frame}, определив её так, чтобы она возвращала
\lstinline{NIL}, когда обнаружит заполнитель, иначе возвращая объект \lstinline{id3-frame}, прочитанный через
\lstinline{read-value}. Предпологая, что \lstinline{read-frame} определена так, что она читает только один байт
после конца предыдущего фрейма для обнаружения заполнителя, можно определить бинарный тип
\lstinline{id3-frames} так:
\begin{myverb}
(define-binary-type id3-frames (tag-size)
(:reader (in)
(loop with to-read = tag-size
while (plusp to-read)
for frame = (read-frame in)
while frame
do (decf to-read (+ 6 (size frame)))
collect frame
finally (loop repeat (1- to-read) do (read-byte in))))
(:writer (out frames)
(loop with to-write = tag-size
for frame in frames
do (write-value 'id3-frame out frame)
(decf to-write (+ 6 (size frame)))
finally (loop repeat to-write do (write-byte 0 out)))))
\end{myverb}
Следующим кодом мы добавим слот frames в id3-tag.
\begin{myverb}
(define-binary-class id3-tag ()
((identifier (iso-8859-1-string :length 3))
(major-version u1)
(revision u1)
(flags u1)
(size id3-tag-size)
(frames (id3-frames :tag-size size))))
\end{myverb}
\section{Обнаружение заполнителя тега}
Теперь всё, что осталось доделать,~--- реализовать \lstinline{read-frame}. Это потребует немного
сноровки, так как код, который на самом деле читает байты из потока, лежит на несколько
уровней ниже \lstinline{read-frame}.
То, что вам бы действительно хотелось делать в \lstinline{read-frame},~--- прочитать один байт и,
если он нулевой, вернуть \lstinline{NIL}, в противном случае прочитать фрейм при помощи
\lstinline{read-value}. К несчастью, если вы прочитаете байт в \lstinline{read-frame}, то он не
сможет быть заново прочитан \lstinline{read-value}\footnote{Символьные потоки поддерживают две
функции: \lstinline{peek-char} и \lstinline{unread-char}, каждая из которых помогла бы решить
описанную задачу, но в бинарных потоках эквивалентных функций нет.}\hspace{\footnotenegspace}.
Выходит, это прекрасная возможность использовать систему условий~--- вы можете устроить
проверку на нулевые байты в коде нижнего уровня, читающем поток, и сигнализировать
условие, когда прочитан ноль; \lstinline{read-frame} сможет затем обработать условие, размотав
стек до того, как будут прочитаны следующие байты. В~дополнение к тому, что это аккуратное
решение проблемы обнаружения начала заполнителя тега, это также и пример, как можно
использовать условия для целей, отличных от обработки ошибок.
Можно начать с определения типа условия, который будет сигнализирован кодом нижнего уровня
и обработан кодом верхнего уровня. Этому условию не нужны слоты~--- вам просто нужен
отдельный класс условия, чтобы знать, что другой код не будет сигнализировать или
обрабатывать его.
\begin{myverb}
(define-condition in-padding () ())
\end{myverb}
Затем вам нужно определить бинарный тип, чей \lstinline{:reader} читает данное число байт,
сначала читая один байт и сигнализируя условие \lstinline{in-padding}, если он нулевой, и,
иначе, читая оставшиеся байты как \lstinline{iso-8859-1-string} и соединяя их с первым
прочитанным.
\begin{myverb}
(define-binary-type frame-id (length)
(:reader (in)
(let ((first-byte (read-byte in)))
(when (= first-byte 0) (signal 'in-padding))
(let ((rest (read-value 'iso-8859-1-string in :length (1- length))))
(concatenate
'string (string (code-char first-byte)) rest))))
(:writer (out id)
(write-value 'iso-8859-1-string out id :length length)))
\end{myverb}
Если переопределить \lstinline{id3-frame} так, чтобы тип его слота \lstinline{id} был
\lstinline{frame-id}, а не \lstinline{iso-8859-1-string}, условие будет сигнализировано, когда метод
\lstinline{read-value} класса \lstinline{id3-frame} прочтёт нулевой байт вместо начала фрейма.
\begin{myverb}
(define-tagged-binary-class id3-frame ()
((id (frame-id :length 3))
(size u3))
(:dispatch (find-frame-class id)))
\end{myverb}
Теперь все, что нужно сделать \lstinline{read-frame},~--- это обернуть вызов \lstinline{read-value} в
\lstinline{HANDLER-CASE}, который обработает условие \lstinline{in-padding}, просто вернув
\lstinline{NIL}.
\begin{myverb}
(defun read-frame (in)
(handler-case (read-value 'id3-frame in)
(in-padding () nil)))
\end{myverb}
Определив \lstinline{read-frame}, вы можете прочитать ID3 тег версии 2.2 целиком, представляя
фреймы экземплярами \lstinline{generic-frame}. В~разделе <<Какие фреймы на самом деле нужны?>>
вы проведёте несколько экспериментов в REPL, чтобы определить, какие классы фреймов вам
нужно реализовать. Но сначала давайте добавим поддержку для тегов ID3 версии~2.3.
\section{Поддержка нескольких версий ID3}
На данный момент \lstinline{id3-tag} определён с помощью \lstinline{define-binary-class}, но, если
вы хотите поддерживать различные версии ID3, больше смысла в использовании
\lstinline{define-tagged-binary-class}, который диспетчеризует значение
\lstinline{major-version}. Как выясняется, все версии ID3v2 имеют одну и ту же структуру вплоть
до поля \lstinline{size}. Итак, вы можете определить помеченный бинарный класс, как в следующем
коде, который определяет базовую структуру и потом передаёт управление подходящему
подклассу, специфичному для данной версии:
\begin{myverb}
(define-tagged-binary-class id3-tag ()
((identifier (iso-8859-1-string :length 3))
(major-version u1)
(revision u1)
(flags u1)
(size id3-tag-size))
(:dispatch
(ecase major-version
(2 'id3v2.2-tag)
(3 'id3v2.3-tag))))
\end{myverb}
Теги версий 2.2 и 2.3 различаются в двух местах. Во-первых, заголовок тега версии~2.3
может содержать вплоть до четырёх необязательных дополнительных полей заголовка, что
определяется значениями в поле \lstinline{flags}. Во-вторых, формат фрейма сменился между
версией~2.2 и версией~2.3, что означает, что вам придётся использовать различные классы
для представления фреймов версии~2.2 и фреймов, соответствующих версии~2.3.
Так как новый класс \lstinline{id3-tag} основан на том классе, который вы первоначально
напи\-сали для представления тега версии~2.2, неудивительно, что новый класс
\lstinline{id3v2.2-tag} тривиален, наследуя большую часть слотов от нового класса
\lstinline{id3-tag} и добавляя один недостающий слот, \lstinline{frames}. Так как теги версий~2.2
и~2.3 используют различные форматы фреймов, вам придётся изменить тип \lstinline{id3-frames}
так, чтобы он параметризовался типом фрейма для чтения. Но сейчас предположим, что вы это
сделаете, и добавим аргумент \lstinline{:frame-type} к дескриптору типов \lstinline{id3-frames} так:
\begin{myverb}
(define-binary-class id3v2.2-tag (id3-tag)
((frames (id3-frames :tag-size size :frame-type 'id3v2.2-frame))))
\end{myverb}
Класс \lstinline{id3v2.3-tag} немого более сложен из-за необязательных полей. Первые три из
четырёх необязательных полей добавляются, когда установлен шестой бит в поле
\lstinline{flags}. Они представляют собой четырёхбайтовое целое, указывающее размер
расширенного заголовка, два байта флагов и ещё одно четырёхбайтовое целое, указывающее,
сколько байт заполнителя включено в тег\pclfootnote{Если в теге есть расширенный заголовок,
вы можете использовать это значение, чтобы определить, где должны заканчиваться
данные. Однако, если расширенный заголовок не используется, вам всё равно придётся
использовать старый алгоритм, так что не стоит добавлять код, делающий это
по-другому.}. Четвёртое необязательное поле добавляется, когда установлен пятнадцатый
бит дополнительных флагов заголовка~--- четырёхбайтовая циклическая избыточностная проверка
(CRC) оставшейся части тега.
Библиотека двоичных данных не предоставляет никакой специальной поддержки для
необязательных полей в двоичном классе, но выходит так, что хватает обычных
параметризованных двоичных типов. Вы можете определить тип, параметризованный именем типа
и значением, который указывает, должно ли значение этого типа быть действительно
прочитано или записано.
\begin{myverb}
(define-binary-type optional (type if)
(:reader (in)
(when if (read-value type in)))
(:writer (out value)
(when if (write-value type out value))))
\end{myverb}
Использование \lstinline{if} как имени параметра кажется немного странным в этом коде, но оно
делает дескрипторы необязательных типов волне читаемыми.
\begin{myverb}
(define-binary-class id3v2.3-tag (id3-tag)
((extended-header-size (optional :type 'u4 :if (extended-p flags)))
(extra-flags (optional :type 'u2 :if (extended-p flags)))
(padding-size (optional :type 'u4 :if (extended-p flags)))
(crc (optional :type 'u4 :if (crc-p flags extra-flags)))
(frames (id3-frames :tag-size size :frame-type 'id3v2.3-frame))))
\end{myverb}
\noindent{}где \lstinline{extended-p} и \lstinline{crc-p}~--- вспомогательные функции, которые проверяют
соответствующий бит флагов, переданных им. Чтобы определить, выставлен отдельный бит в
целом числе или нет, можно использовать \lstinline{LOGBITP}, ещё одну жонглирующую битами
функцию. Она принимает индекс и целое и возвращает истину, если указанный бит установлен в
числе.
\begin{myverb}
(defun extended-p (flags) (logbitp 6 flags))
(defun crc-p (flags extra-flags)
(and (extended-p flags) (logbitp 15 extra-flags)))
\end{myverb}
Как и в классе тега версии 2.2, слот \lstinline{frames} определяется с типом \lstinline{id3-frames},
передавая имя типа фрейма как параметр. Вам, однако, придётся сделать незначительные
изменения в \lstinline{id3-frames} и \lstinline{read-frame} для поддержки дополнительного параметра
\lstinline{frame-type}.
\begin{myverb}
(define-binary-type id3-frames (tag-size frame-type)
(:reader (in)
(loop with to-read = tag-size
while (plusp to-read)
for frame = (read-frame frame-type in)
while frame
do (decf to-read (+ (frame-header-size frame) (size frame)))
collect frame
finally (loop repeat (1- to-read) do (read-byte in))))
(:writer (out frames)
(loop with to-write = tag-size
for frame in frames
do (write-value frame-type out frame)
(decf to-write (+ (frame-header-size frame) (size frame)))
finally (loop repeat to-write do (write-byte 0 out)))))
(defun read-frame (frame-type in)
(handler-case (read-value frame-type in)
(in-padding () nil)))
\end{myverb}
Изменения заключены в вызовах \lstinline{read-frame} и \lstinline{write-value}, где вам нужно
передать аргумент \lstinline{frame-type}, и в вычислении размера фрейма, где нужно использовать
функцию \lstinline{frame-header-size}, а не прописать значение~6, так как размер заголовка
изменился между версиями~2.2 и~2.3. Поскольку различие в результате этой функции основано на
классе фрейма, имеет смысл определить обобщённую функцию так:
\begin{myverb}
(defgeneric frame-header-size (frame))
\end{myverb}
Вы определите необходимые методы для этой обобщённой функции в следующем разделе, после
того как определите новые классы фреймов.
\section{Базовые классы для фреймов разных версий}
Раньше вы определили один базовый класс для всех фреймов, но теперь у вас два класса,
\lstinline{id3v2.2-frame} и \lstinline{id3v2.3-frame}. Класс \lstinline{id3v2.2-frame} будет, по сути,
таким же, как и первоначальный класс \lstinline{id3-frame}.
\begin{myverb}
(define-tagged-binary-class id3v2.2-frame ()
((id (frame-id :length 3))
(size u3))
(:dispatch (find-frame-class id)))
\end{myverb}
с другой стороны, \lstinline{id3v2.3-frame} требует больших изменений. Идентификатор
фрейма и поле размера были расширены в версии~2.3 с трёх до четырёх байт каждое, и были
добавлены два байта с флагами. Дополнительно фрейм, как и тег версии~2.3, может содержать
необязательные поля, зависящие от значений трёх флагов фрейма\footnote{Эти флаги не только
контролируют, включены ли необязательные поля, но и могут влиять на оставшуюся часть
тега. В~частности, если установлен седьмой бит флага, данные шифруются. На практике эти
возможности применяются редко, если вообще где-нибудь применяются, так что пока вы
можете просто проигнорировать их. Но к этой задаче вам пришлось бы обратиться, чтобы
качество вашего кода соответствовало промышленным стандартам. Одним простым половинчатым
решением было бы поменять \lstinline{find-frame-class} так, чтобы он принимал второй
аргумент, и передавать ему флаги; если фрейм зашифрован, вы могли бы создать экземпляр
обобщённого фрейма и положить в него данные фрейма.}\hspace{\footnotenegspace}. Держа
эти изменения в уме, вы можете определить базовый класс фрейма версии 2.3 вместе с
несколькими вспомогательными функциями, например так:
\begin{myverb}
(define-tagged-binary-class id3v2.3-frame ()
((id (frame-id :length 4))
(size u4)
(flags u2)
(decompressed-size (optional :type 'u4 :if (frame-compressed-p flags)))
(encryption-scheme (optional :type 'u1 :if (frame-encrypted-p flags)))
(grouping-identity (optional :type 'u1 :if (frame-grouped-p flags))))
(:dispatch (find-frame-class id)))
(defun frame-compressed-p (flags) (logbitp 7 flags))
(defun frame-encrypted-p (flags) (logbitp 6 flags))
(defun frame-grouped-p (flags) (logbitp 5 flags))
\end{myverb}
Определив эти два класса, вы можете реализовать методы обобщённой функции
\lstinline{frame-header-size}.
\begin{myverb}
(defmethod frame-header-size ((frame id3v2.2-frame)) 6)
(defmethod frame-header-size ((frame id3v2.3-frame)) 10)
\end{myverb}
Необязательные поля в фрейме версии 2.3 в этом вычислении не считаются частью заголовка,
так как они уже включены в значение размера фрейма.
\section{Конкретные классы для фреймов разных версий}
При первоначальном определении класс \lstinline{generic-frame} наследовал \lstinline{id3-frame}. Но
сейчас \lstinline{id3-frame} заменён двумя специфичными для версий базовыми классами,
\lstinline{id3v2.2-frame} и \lstinline{id3v2.3-frame}. Так что вам надо определить две новые версии
\lstinline{generic-frame}, по каждой для своего базового класса. Один из способов определить
эти классы таков:
\begin{myverb}
(define-binary-class generic-frame-v2.2 (id3v2.2-frame)
((data (raw-bytes :size size))))
(define-binary-class generic-frame-v2.3 (id3v2.3-frame)
((data (raw-bytes :size size))))
\end{myverb}
Однако немного раздражает то, что эти два класса одинаковы, за исключением их
суперклассов. Это не очень плохо в данном случае, так как здесь только одно дополнительное
поле. Но если вы выберете этот подход для других конкретных классов фреймов, таких,
которые имеют более сложную внутреннюю структуру, идентичную для двух версий ID3,
дублирование будет более раздражающим.
Другой подход, тот, который вам на самом деле следует использовать,~--- определить класс
\lstinline{generic-frame} как <<примесь>> (mixin): класс, который предполагается для
использования как суперкласс с одним из специфичных для версии базовых классов для
получения конкретного, специфичного для версии класса фрейма. В~этом способе только один
хитрый момент: \lstinline{generic-frame} не расширяет любой из базовых классов фрейма, так что
вы не сможете обращаться к слоту \lstinline{size} в определении. Вместо этого вы должны
использовать функцию \lstinline{current-binary-object}, которая обсуждалась в конце предыдущей
части, для доступа к объекту, в процессе чтения или записи которого находитесь, и передать
его в \lstinline{size}. И вам нужно учесть разницу в числе байт полного размера фрейма, которые
будут отложены, если любое из необязательных полей будет включено во фрейм. Так что вы
должны определить обобщённую функцию \lstinline{data-bytes} и методамы, которые делают
правильные действия, и для фреймов версии 2.2, и для версии~2.3.
\begin{myverb}
(define-binary-class generic-frame ()
((data (raw-bytes :size (data-bytes (current-binary-object))))))
(defgeneric data-bytes (frame))
(defmethod data-bytes ((frame id3v2.2-frame))
(size frame))
(defmethod data-bytes ((frame id3v2.3-frame))
(let ((flags (flags frame)))
(- (size frame)
(if (frame-compressed-p flags) 4 0)
(if (frame-encrypted-p flags) 1 0)
(if (frame-grouped-p flags) 1 0))))
\end{myverb}
После этого вы можете определить конкретные классы, которые расширяют один из специфичных
для версий классов, и класс \lstinline{generic-frame} для определения специфичного для версии класса
фрейма.
\begin{myverb}
(define-binary-class generic-frame-v2.2 (id3v2.2-frame generic-frame) ())
(define-binary-class generic-frame-v2.3 (id3v2.3-frame generic-frame) ())
\end{myverb}
Определив эти классы, вы можете переопределить функцию \lstinline{find-frame-class} так, чтобы
она возвращала правильный класс для версии, основываясь на длине идентификатора.
\begin{myverb}
(defun find-frame-class (id)
(ecase (length id)
(3 'generic-frame-v2.2)
(4 'generic-frame-v2.3)))
\end{myverb}
\section{Какие фреймы на самом деле нужны?}
Имея возможность читать теги и версии 2.2, и версии 2.3, используя обобщённые фреймы, вы
готовы начать реализацию классов для представления специфичных фреймов, которые вам
нужны. Однако, перед тем как нырнуть в это, вам следует набрать воздуха и выяснить, какие
фреймы вам на самом деле нужны, так как я уже упомянул ранее, что спецификация ID3
содержит множество фреймов, которые почти никогда не используются. Конечно, то, какие