gen_id3_testdata.sh (5384B)
1 #!/bin/sh 2 # Regenerate src/plugins/testdata/id3_test.mp3 for test_id3.c. 3 # 4 # Debian packages needed: python3, python3-mutagen, lame. 5 # 6 # The audio is a one second 440 Hz sine at 44100 Hz, mono, written as a 7 # WAV by python and encoded by LAME so that the file carries a real 8 # Xing/LAME tag -- that tag is the point of the exercise, and ffmpeg 9 # writes its own ("Lavf lame") rather than LAME's, which would not 10 # exercise the same fields. 11 # 12 # Everything is deterministic: no timestamps, no random data. Note 13 # that the LAME encoder version string ("LAME3.100") and the settings 14 # summary derived from the LAME tag are asserted on literally in 15 # test_id3.c; if you regenerate this with a different LAME release, 16 # those two expectations have to be updated to match. 17 # 18 # The tag set is ID3v2.4 on purpose. It exercises all four text 19 # encodings in one file (TIT2 as UTF-8, TPE1 as UTF-16 with a byte 20 # order mark, TALB as ISO-8859-1, the rest as written), and the USLT 21 # and APIC frames are both longer than 127 bytes, which is where the 22 # synchsafe frame size of 2.4 first differs from the plain integer of 23 # 2.3. A hand-written ID3v1 tag follows the audio; its comment and 24 # year fields are the only ones the plugin should use, since the ID3v2 25 # tag covers everything else. 26 # 27 # This file is part of libextractor; the generated data is CC0. 28 set -e 29 30 out="$(dirname "$0")/../src/plugins/testdata" 31 tmp="$(mktemp -d)" 32 trap 'rm -rf "$tmp"' 0 33 34 python3 - "$tmp" <<'EOF' 35 import math 36 import os 37 import struct 38 import sys 39 import wave 40 41 tmp = sys.argv[1] 42 w = wave.open(os.path.join(tmp, 'tone.wav'), 'wb') 43 w.setnchannels(1) 44 w.setsampwidth(2) 45 w.setframerate(44100) 46 w.writeframes(b''.join(struct.pack('<h', int(12000 * math.sin(2 * math.pi 47 * 440 * i 48 / 44100))) 49 for i in range(44100))) 50 w.close() 51 EOF 52 53 lame -V 5 --vbr-new -m m -q 2 --quiet "$tmp/tone.wav" "$tmp/id3_test.mp3" 54 55 python3 - "$tmp" <<'EOF' 56 import os 57 import struct 58 import sys 59 import zlib 60 61 from mutagen.id3 import (ID3, APIC, COMM, PRIV, TALB, TBPM, TCOM, TCON, TCOP, 62 TDRC, TENC, TFLT, TIT2, TLAN, TMED, TOWN, TPE1, TPE3, 63 TPOS, TPUB, TRCK, TSSE, TXXX, UFID, USLT, WOAF, WOAR) 64 65 tmp = sys.argv[1] 66 path = os.path.join(tmp, 'id3_test.mp3') 67 68 69 def png(side): 70 """A deterministic side x side RGB PNG, roughly 300 bytes.""" 71 raw = b''.join(b'\x00' + bytes(((x * 7) % 256, (y * 11) % 256, 72 ((x + y) * 3) % 256)[c] 73 for x in range(side) for c in range(3)) 74 for y in range(side)) 75 76 def chunk(tag, data): 77 c = tag + data 78 return (struct.pack('>I', len(data)) + c 79 + struct.pack('>I', zlib.crc32(c) & 0xFFFFFFFF)) 80 81 return (b'\x89PNG\r\n\x1a\n' 82 + chunk(b'IHDR', struct.pack('>IIBBBBB', side, side, 8, 2, 0, 0, 0)) 83 + chunk(b'IDAT', zlib.compress(raw, 9)) 84 + chunk(b'IEND', b'')) 85 86 87 lyrics = ('One two three four five six seven eight nine ten. ' 88 'This lyric frame exists only to be longer than one hundred ' 89 'and twenty seven bytes, so that the synchsafe frame size of ' 90 'ID3v2.4 differs from a plain big endian integer here.') 91 92 tags = ID3() 93 tags.add(TIT2(encoding=3, text=['Grüße aus Köln'])) 94 tags.add(TPE1(encoding=1, text=['Bœuf Trio'])) 95 tags.add(TALB(encoding=0, text=['Café Sessions'])) 96 tags.add(TCON(encoding=0, text=['(52)'])) 97 tags.add(TRCK(encoding=0, text=['3/12'])) 98 tags.add(TPOS(encoding=0, text=['1/2'])) 99 tags.add(TDRC(encoding=0, text=['2024-03-14'])) 100 tags.add(TCOM(encoding=0, text=['Ada Lovelace'])) 101 tags.add(TPE3(encoding=0, text=['Grace Hopper'])) 102 tags.add(TPUB(encoding=0, text=['GNU Records'])) 103 tags.add(TCOP(encoding=0, text=['2026 GNU libextractor'])) 104 tags.add(TLAN(encoding=0, text=['deu'])) 105 tags.add(TBPM(encoding=0, text=['128'])) 106 tags.add(TENC(encoding=0, text=['GNU libextractor test rig'])) 107 tags.add(TSSE(encoding=0, text=['LAME 3.100 -V 5 --vbr-new'])) 108 tags.add(TOWN(encoding=0, text=['licensee@example.org'])) 109 tags.add(TFLT(encoding=0, text=['MPG/3'])) 110 tags.add(TMED(encoding=0, text=['DIG/A/T'])) 111 tags.add(USLT(encoding=3, lang='eng', desc='', text=lyrics)) 112 tags.add(UFID(owner='http://musicbrainz.org', 113 data=b'9f1a3e6c-0000-4000-8000-0123456789ab')) 114 tags.add(WOAF(url='https://www.gnu.org/software/libextractor/')) 115 tags.add(WOAR(url='https://www.gnu.org/')) 116 tags.add(PRIV(owner='com.apple.iTunes', data=b'\x00\x01\x02\x03')) 117 tags.add(TXXX(encoding=0, desc='apID', text=['buyer@example.org'])) 118 tags.add(APIC(encoding=0, mime='image/png', type=3, desc='Cover', 119 data=png(8))) 120 tags.save(path, v2_version=4, v1=0) 121 122 # An ID3v1 tag written by hand, so that its fields are visibly 123 # different from the ID3v2 ones: only the comment and the year should 124 # ever reach the caller, because the ID3v2 tag covers everything else. 125 def field(s, n): 126 return s.encode('latin-1')[:n].ljust(n, b'\x00') 127 128 129 v1 = (b'TAG' + field('ID3v1 Title', 30) + field('ID3v1 Artist', 30) 130 + field('ID3v1 Album', 30) + field('1999', 4) 131 + field('ID3v1 comment field', 30) + bytes([52])) 132 assert len(v1) == 128 133 with open(path, 'ab') as f: 134 f.write(v1) 135 EOF 136 137 cp "$tmp/id3_test.mp3" "$out/id3_test.mp3" 138 ls -l "$out/id3_test.mp3"