Skip to content

Commit f6094e0

Browse files
committed
test: add MP3 fixtures for multi-value TXXX frames
Four fixtures covering null-separated multi-value TXXX fields (2-value, 3-value, empty-slot, and single-value regression guard) plus the Python script that generates them. Signed-off-by: Rouzax <GitHub@mgdn.nl>
1 parent 10a7a75 commit f6094e0

5 files changed

Lines changed: 87 additions & 0 deletions

File tree

t/generate_txxx_fixtures.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
#!/usr/bin/env python3
2+
"""Generate MP3 test fixtures for Audio::Scan TXXX multi-value tests.
3+
4+
Uses a valid MPEG frame extracted from an existing fixture as the audio
5+
payload, then hand-builds ID3v2.4 tags with null-separated TXXX values
6+
so we get exact byte-level control over the multi-value encoding.
7+
"""
8+
import struct, os
9+
10+
OUTDIR = "/home/martijn/github/Audio-Scan/t/mp3"
11+
# Use an existing valid MP3 as the audio base
12+
BASE_MP3 = "/home/martijn/github/Audio-Scan/t/mp3/no-tags-mp1l3.mp3"
13+
14+
15+
def get_audio_payload():
16+
"""Read raw MPEG audio frames from an existing no-tags fixture."""
17+
with open(BASE_MP3, 'rb') as f:
18+
return f.read()
19+
20+
21+
def syncsafe_encode(size):
22+
"""Encode an integer as a 4-byte syncsafe integer (ID3v2.4 spec)."""
23+
return (
24+
((size & 0x0FE00000) << 3) |
25+
((size & 0x001FC000) << 2) |
26+
((size & 0x00003F80) << 1) |
27+
(size & 0x0000007F)
28+
)
29+
30+
31+
def make_txxx_frame(desc, values, encoding=3):
32+
"""Build an ID3v2.4 TXXX frame with null-separated values.
33+
encoding: 0=Latin1, 3=UTF-8
34+
"""
35+
if encoding == 3:
36+
sep = b'\x00'
37+
desc_bytes = desc.encode('utf-8') + b'\x00'
38+
val_bytes = sep.join(v.encode('utf-8') for v in values)
39+
else:
40+
sep = b'\x00'
41+
desc_bytes = desc.encode('latin-1') + b'\x00'
42+
val_bytes = sep.join(v.encode('latin-1') for v in values)
43+
44+
data = bytes([encoding]) + desc_bytes + val_bytes
45+
size = len(data)
46+
frame = b'TXXX' + struct.pack('>I', syncsafe_encode(size)) + b'\x00\x00' + data
47+
return frame
48+
49+
50+
def make_id3v2_tag(frames):
51+
"""Wrap frames in an ID3v2.4 tag header."""
52+
body = b''.join(frames)
53+
size = len(body)
54+
header = b'ID3' + b'\x04\x00' + b'\x00' + struct.pack('>I', syncsafe_encode(size))
55+
return header + body
56+
57+
58+
def write_fixture(name, frames):
59+
tag = make_id3v2_tag(frames)
60+
audio = get_audio_payload()
61+
path = os.path.join(OUTDIR, name)
62+
with open(path, 'wb') as f:
63+
f.write(tag + audio)
64+
print(f"Wrote {path} ({len(tag) + len(audio)} bytes)")
65+
66+
67+
# Fixture 1: Two-value TXXX (ALBUMARTISTS)
68+
write_fixture("v2.4-txxx-multivalue.mp3", [
69+
make_txxx_frame("ALBUMARTISTS", ["Artist1", "Artist2"]),
70+
make_txxx_frame("ARTISTS", ["TrackArtist1", "TrackArtist2"]),
71+
])
72+
73+
# Fixture 2: Three-value TXXX
74+
write_fixture("v2.4-txxx-multivalue-3.mp3", [
75+
make_txxx_frame("ALBUMARTISTS", ["Artist1", "Artist2", "Artist3"]),
76+
])
77+
78+
# Fixture 3: Multi-value with empty slot (tagger bug simulation)
79+
write_fixture("v2.4-txxx-multivalue-empty.mp3", [
80+
make_txxx_frame("ALBUMARTISTS", ["Artist1", "", "Artist3"]),
81+
])
82+
83+
# Fixture 4: Single-value TXXX (regression guard)
84+
write_fixture("v2.4-txxx-single.mp3", [
85+
make_txxx_frame("ALBUMARTISTS", ["OnlyArtist"]),
86+
make_txxx_frame("USER FRAME", ["SingleValue"]),
87+
])

t/mp3/v2.4-txxx-multivalue-3.mp3

4.27 KB
Binary file not shown.
4.27 KB
Binary file not shown.

t/mp3/v2.4-txxx-multivalue.mp3

4.31 KB
Binary file not shown.

t/mp3/v2.4-txxx-single.mp3

4.29 KB
Binary file not shown.

0 commit comments

Comments
 (0)