101 lines
5.2 KiB
Python
101 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Original synthetic MP3+G test media. Regeneration requires ffmpeg; playback does not."""
|
|
from pathlib import Path
|
|
import wave,struct,math,subprocess,zipfile,shutil
|
|
root=Path(__file__).resolve().parents[1]/'Fixtures';root.mkdir(exist_ok=True)
|
|
font={
|
|
'A':['01110','10001','10001','11111','10001','10001','10001'],
|
|
'C':['01111','10000','10000','10000','10000','10000','01111'],
|
|
'D':['11110','10001','10001','10001','10001','10001','11110'],
|
|
'E':['11111','10000','10000','11110','10000','10000','11111'],
|
|
'F':['11111','10000','10000','11110','10000','10000','10000'],
|
|
'G':['01111','10000','10000','10111','10001','10001','01111'],
|
|
'H':['10001','10001','10001','11111','10001','10001','10001'],
|
|
'I':['11111','00100','00100','00100','00100','00100','11111'],
|
|
'J':['00111','00010','00010','00010','10010','10010','01100'],
|
|
'K':['10001','10010','10100','11000','10100','10010','10001'],
|
|
'L':['10000','10000','10000','10000','10000','10000','11111'],
|
|
'M':['10001','11011','10101','10101','10001','10001','10001'],
|
|
'N':['10001','11001','10101','10011','10001','10001','10001'],
|
|
'O':['01110','10001','10001','10001','10001','10001','01110'],
|
|
'P':['11110','10001','10001','11110','10000','10000','10000'],
|
|
'R':['11110','10001','10001','11110','10100','10010','10001'],
|
|
'S':['01111','10000','10000','01110','00001','00001','11110'],
|
|
'T':['11111','00100','00100','00100','00100','00100','00100'],
|
|
'U':['10001','10001','10001','10001','10001','10001','01110'],
|
|
'V':['10001','10001','10001','10001','10001','01010','00100'],
|
|
'W':['10001','10001','10001','10101','10101','11011','10001'],
|
|
'Y':['10001','10001','01010','00100','00100','00100','00100'],
|
|
'3':['11110','00001','00001','01110','00001','00001','11110'],
|
|
' ':['00000']*7,
|
|
}
|
|
def packet(inst,data):return bytes([9,inst,0,0]+(data+[0]*16)[:16]+[0]*4)
|
|
packets=[bytes(24) for _ in range(6000)]
|
|
def put(index,seq):
|
|
for p in seq:
|
|
assert packets[index]==bytes(24),'overlapping instructions'
|
|
packets[index]=p;index+=1
|
|
return index
|
|
colors=[0x013,0xfff,0xfd0,0x0cf]+[0]*12
|
|
def palette(offset):
|
|
data=[]
|
|
for c in colors[offset:offset+8]:data.extend([(c>>6)&63,c&63])
|
|
return packet(30 if offset==0 else 31,data)
|
|
put(0,[palette(0),palette(8),packet(1,[0,0]),packet(2,[0])])
|
|
def frame(lines):
|
|
pixels=[[0]*300 for _ in range(216)]
|
|
for line,(text,y) in enumerate(lines):
|
|
x=(300-len(text)*12)//2
|
|
for char in text:
|
|
for gy,row in enumerate(font[char]):
|
|
for gx,bit in enumerate(row):
|
|
if bit=='1':
|
|
for dy in range(2):
|
|
for dx in range(2):pixels[y+gy*2+dy][x+gx*2+dx]=1
|
|
x+=12
|
|
return pixels
|
|
def tiles(pixels,xor=False):
|
|
result=[]
|
|
for row in range(1,17):
|
|
for col in range(1,49):
|
|
bits=[]
|
|
for y in range(12):
|
|
v=0
|
|
for x in range(6):v=(v<<1)|pixels[row*12+y][col*6+x]
|
|
bits.append(v)
|
|
if any(bits):result.append(packet(38 if xor else 6,[0,3 if xor else 1,row,col]+bits))
|
|
return result
|
|
scenes=[[("KJAMS CLONE",66),("MP3 PLUS G TEST",114)],[("ONE TWO THREE FOUR",66),("SING ALONG",114)],[("WITH THE TEST TONE",66),("PLAY PAUSE SEEK",114)],[("MP3 PLUS G TEST",66),("TEST COMPLETE",114)]]
|
|
for index,lines in enumerate(scenes):
|
|
start=index*1500+(5 if index==0 else 0)
|
|
pixels=frame(lines)
|
|
put(start,([packet(1,[0,0])] if index else [])+tiles(pixels))
|
|
# Highlight each word after the initial white text has been drawn.
|
|
for line,(text,y) in enumerate(lines):
|
|
left=(300-len(text)*12)//2
|
|
words=text.split();cursor=0
|
|
for wi,word in enumerate(words):
|
|
mask=[[0]*300 for _ in range(216)]
|
|
for py in range(y,y+14):
|
|
for px in range(left+cursor*12,left+(cursor+len(word))*12):mask[py][px]=pixels[py][px]
|
|
put(index*1500+240+line*600+wi*100,tiles(mask,True))
|
|
cursor+=len(word)+1
|
|
(root/'TestTrack.cdg').write_bytes(b''.join(packets))
|
|
rate=44100;duration=20;notes=[261.63,329.63,392.0,523.25,392.0,329.63,293.66,261.63]
|
|
with wave.open(str(root/'TestTrack.wav'),'w') as out:
|
|
out.setparams((1,2,rate,0,'NONE','not compressed'))
|
|
for beat in range(duration*2):
|
|
freq=notes[beat%len(notes)]
|
|
data=bytearray()
|
|
for i in range(rate//2):
|
|
t=i/rate;fade=min(1,t/0.015,(0.5-t)/0.04)
|
|
data.extend(struct.pack('<h',int(3000*max(0,fade)*math.sin(2*math.pi*freq*t))))
|
|
out.writeframes(data)
|
|
ffmpeg=shutil.which('ffmpeg')
|
|
if not ffmpeg:raise SystemExit('ffmpeg is needed only to regenerate the included MP3 test file')
|
|
subprocess.run([ffmpeg,'-y','-loglevel','error','-i',str(root/'TestTrack.wav'),'-codec:a','libmp3lame','-q:a','5','-metadata','title=MP3 Plus G Test','-metadata','artist=KJamsClone Test Generator','-metadata','album=Original Test Media',str(root/'TestTrack.mp3')],check=True)
|
|
with zipfile.ZipFile(root/'TestTrack.zip','w',zipfile.ZIP_DEFLATED) as z:
|
|
for name in ['TestTrack.mp3','TestTrack.cdg']:z.write(root/name,name)
|
|
(root/'TEST-MEDIA.txt').write_text('Original synthetic 20-second low-volume tone melody and original test text. No vocals or commercial song. Free to use. Import TestTrack.zip, or keep TestTrack.mp3 and TestTrack.cdg together and import the MP3. CDG has timed white-to-yellow word highlighting. Test pause, seek, tempo and key changes. Demo.* remains the original 4-second regression fixture. Unsafe.zip is a deliberately invalid traversal test fixture, not playable media.\n')
|
|
print('Created original TestTrack.cdg, TestTrack.mp3, TestTrack.zip and WAV source.')
|