prep.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. """
  2. gameduino.prep - for graphics and sound preparation
  3. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  4. The prep module provides utilities for
  5. preparing Gameduino media: images and sound.
  6. These utilities can be used, for example, to take image files
  7. and encode them so that the Gameduino can display them as backgrounds
  8. or sprites.
  9. ..
  10. This module defines mnemonics for the sprite palette select field:
  11. +-----------------------+-------+
  12. | PALETTE256A | 0 |
  13. +-----------------------+-------+
  14. | PALETTE256B | 1 |
  15. +-----------------------+-------+
  16. | PALETTE256C | 2 |
  17. +-----------------------+-------+
  18. | PALETTE256D | 3 |
  19. +-----------------------+-------+
  20. | PALETTE16A_BITS0123 | 4 |
  21. +-----------------------+-------+
  22. | PALETTE16A_BITS4567 | 6 |
  23. +-----------------------+-------+
  24. | PALETTE16B_BITS0123 | 5 |
  25. +-----------------------+-------+
  26. | PALETTE16B_BITS4567 | 7 |
  27. +-----------------------+-------+
  28. | PALETTE4A_BITS01 | 8 |
  29. +-----------------------+-------+
  30. | PALETTE4A_BITS23 | 10 |
  31. +-----------------------+-------+
  32. | PALETTE4A_BITS45 | 12 |
  33. +-----------------------+-------+
  34. | PALETTE4A_BITS67 | 14 |
  35. +-----------------------+-------+
  36. | PALETTE4B_BITS01 | 9 |
  37. +-----------------------+-------+
  38. | PALETTE4B_BITS23 | 11 |
  39. +-----------------------+-------+
  40. | PALETTE4B_BITS45 | 13 |
  41. +-----------------------+-------+
  42. | PALETTE4B_BITS67 | 15 |
  43. +-----------------------+-------+
  44. The module defines these constants for use as the ``palset`` argument to :meth:`ImageRAM.addsprites`:
  45. +-------------+-------------------------+
  46. | PALETTE256A | 256-color palette A |
  47. +-------------+-------------------------+
  48. | PALETTE256B | 256-color palette B |
  49. +-------------+-------------------------+
  50. | PALETTE256C | 256-color palette C |
  51. +-------------+-------------------------+
  52. | PALETTE256D | 256-color palette D |
  53. +-------------+-------------------------+
  54. | PALETTE4A | Four-color palette A |
  55. +-------------+-------------------------+
  56. | PALETTE4B | Four-color palette B |
  57. +-------------+-------------------------+
  58. | PALETTE16A | Sixteen-color palette A |
  59. +-------------+-------------------------+
  60. | PALETTE16B | Sixteen-color palette B |
  61. +-------------+-------------------------+
  62. """
  63. PALETTE256A = [0]
  64. PALETTE256B = [1]
  65. PALETTE256C = [2]
  66. PALETTE256D = [3]
  67. PALETTE4A_BITS01 = (0x8 + (0 << 1))
  68. PALETTE4A_BITS23 = (0x8 + (1 << 1))
  69. PALETTE4A_BITS45 = (0x8 + (2 << 1))
  70. PALETTE4A_BITS67 = (0x8 + (3 << 1))
  71. PALETTE4A = (PALETTE4A_BITS01, PALETTE4A_BITS23, PALETTE4A_BITS45, PALETTE4A_BITS67)
  72. PALETTE4B_BITS01 = (0x8 + (0 << 1) + 1)
  73. PALETTE4B_BITS23 = (0x8 + (1 << 1) + 1)
  74. PALETTE4B_BITS45 = (0x8 + (2 << 1) + 1)
  75. PALETTE4B_BITS67 = (0x8 + (3 << 1) + 1)
  76. PALETTE4B = (PALETTE4B_BITS01, PALETTE4B_BITS23, PALETTE4B_BITS45, PALETTE4B_BITS67)
  77. PALETTE16A_BITS0123 = (0x4 + (0 << 1))
  78. PALETTE16A_BITS4567 = (0x4 + (1 << 1))
  79. PALETTE16A = (PALETTE16A_BITS0123, PALETTE16A_BITS4567)
  80. PALETTE16B_BITS0123 = (0x4 + (0 << 1) + 1)
  81. PALETTE16B_BITS4567 = (0x4 + (1 << 1) + 1)
  82. PALETTE16B = (PALETTE16B_BITS0123, PALETTE16B_BITS4567)
  83. from array import array
  84. import Image
  85. def dump(hh, name, data):
  86. """
  87. Writes data to a header file for use in an Arduino Sketch.
  88. :param hh: destination header file
  89. :type hh: :class:`file`
  90. :param name: the name of the object, as it will appear in the header file
  91. :type name: string
  92. :param data: the data to be dumped
  93. :type data: :class:`array.array`
  94. """
  95. print >>hh, "static PROGMEM prog_uchar %s[] = {" % name
  96. bb = array('B', data.tostring())
  97. for i in range(0, len(bb), 16):
  98. if (i & 0xff) == 0:
  99. print >>hh
  100. for c in bb[i:i+16]:
  101. print >>hh, "0x%02x, " % c,
  102. print >>hh
  103. print >>hh, "};"
  104. def rgbpal(imdata):
  105. # For RGBA imdata, return list of (r,g,b) triples and the palette
  106. li = array('B', imdata).tolist()
  107. rgbas = zip(li[0::4], li[1::4], li[2::4], li[3::4])
  108. palette = list(set(rgbas))
  109. return (rgbas, palette)
  110. def getch(im, x, y):
  111. # return the RGBA data for the 8x8 character at (x, y) in im
  112. # if the 8x8 RGB contains more than 4 colors, quantize it using
  113. # `scolorq <http://www.cs.berkeley.edu/~dcoetzee/downloads/scolorq/>`_.
  114. sub88 = im.crop((x, y, x + 8, y + 8))
  115. sub88d = sub88.tostring()
  116. (_, pal) = rgbpal(sub88d)
  117. if len(pal) > 4:
  118. return sub88.convert('RGB').convert('P', palette=Image.ADAPTIVE, colors=4).convert("RGBA")
  119. else:
  120. return sub88
  121. def rgb555(r, g, b):
  122. return ((r / 8) << 10) + ((g / 8) << 5) + (b / 8)
  123. def rgba1555(r, g, b, a):
  124. return ((a < 128) << 15) + ((r / 8) << 10) + ((g / 8) << 5) + (b / 8)
  125. def encodech(imdata):
  126. """
  127. imdata is 8x8x4 RGBA character, string of length 256
  128. return the pixel and palette data for it as
  129. :class:`array.array` of type 'B' and 'H' respectively.
  130. """
  131. assert len(imdata) == (4 * 8 * 8)
  132. (rgbs, palette) = rgbpal(imdata)
  133. indices = [palette.index(c) for c in rgbs]
  134. indices_b = ""
  135. for i in range(0, len(indices), 4):
  136. c = ((indices[i] << 6) +
  137. (indices[i + 1] << 4) +
  138. (indices[i + 2] << 2) +
  139. (indices[i + 3]))
  140. indices_b += (chr(c))
  141. palette = (palette + ([(0,0,0,255)] * 4))[:4] # unused palette entries: opaque black
  142. ph = array('H', [rgba1555(*p) for p in palette])
  143. return (indices_b, ph)
  144. def getpal(im):
  145. """ im is a paletted image. Return its palette as a Gameduino sprite palette
  146. in an :class:`array.array` of type 'H'. This form can be used directly with :func:`dump`::
  147. import gameduino.prep as gdprep
  148. ...
  149. gdprep.dump(hfile, "paletteA", gdprep.getpal(im))
  150. """
  151. ncol = ord(max(im.tostring())) + 1
  152. ncol = min([c for c in [4,16,256] if c >= ncol])
  153. lut = im.resize((ncol, 1))
  154. lut.putdata(range(ncol))
  155. palstr = lut.convert("RGB").tostring()
  156. rgbs = zip(*(array('B', palstr[i::3]) for i in range(3)))
  157. rgb555 = [(((r / 8) << 10) | ((g / 8) << 5) | (b / 8)) for (r,g,b) in rgbs]
  158. if 'transparency' in im.info:
  159. rgb555[im.info['transparency']] = 0x8000
  160. return array('H', rgb555)
  161. def encode(im):
  162. """
  163. Convert a PIL image to a Gameduino character background image.
  164. :param im: A Python Imaging Library image
  165. :rtype: tuple of data for (picture, character, font) all :class:`array.array`.
  166. The image must have dimensions that are multiples of 8.
  167. If any character cell contains more than four colors, then the cell's pixel are quantized to four colors before encoding.
  168. If the image requires more than 256 unique character cells, this function throws exception OverflowError.
  169. The tuple returned contains three pieces of data:
  170. * picture - the bytes representing the character cells. For input image sized (w, h) this array has size (w/8)*(h/8). Type of this array is 'B' (unsigned byte)
  171. * character - the glyphs for all used 8x8 characters. One character is 16 bytes. Type of this array is 'B' (unsigned byte)
  172. * palette - the 4-color palettes for all used 8x8 characters. One character is 8 bytes. Type of this array is 'H' (unsigned short)
  173. To display the image, load these three arrays into Gameduino memory. For example,
  174. to encode a single image and
  175. write its data to a header file ``titlescreen.h``::
  176. import gameduino.prep as gdprep
  177. (dpic, dchr, dpal) = gdprep.encode(Image.open("titlescreen.png"))
  178. hdr = open("titlescreen.h", "w")
  179. gdprep.dump(hdr, "titlescreen_pic", dpic)
  180. gdprep.dump(hdr, "titlescreen_chr", dchr)
  181. gdprep.dump(hdr, "titlescreen_pal", dpal)
  182. and to display the image on the screen, an Arduino sketch might do::
  183. #include "titlescreen.h"
  184. void setup()
  185. {
  186. ...
  187. GD.copy(RAM_PIC, titlescreen_pic, sizeof(titlescreen_pic));
  188. GD.copy(RAM_CHR, titlescreen_chr, sizeof(titlescreen_chr));
  189. GD.copy(RAM_PAL, titlescreen_pal, sizeof(titlescreen_pal));
  190. """
  191. if im.mode != "RGBA":
  192. im = im.convert("RGBA")
  193. charset = {} # dict that maps 8x8 images to byte charcodes
  194. picture = [] # 64x64 byte picture RAM
  195. for y in range(0, im.size[1], 8):
  196. for x in range(0, im.size[0], 8):
  197. iglyph = getch(im, x, y)
  198. glyph = iglyph.tostring()
  199. if not glyph in charset:
  200. if len(charset) == 256:
  201. raise OverflowError
  202. charset[glyph] = len(charset)
  203. picture.append(charset[glyph])
  204. picd = array('B', picture)
  205. cd = array('B', [0] * 16 * len(charset))
  206. pd = array('H', [0] * 4 * len(charset))
  207. for d,i in charset.items():
  208. for y in range(8):
  209. (char, pal) = encodech(d)
  210. cd[16 * i:16 * (i+1)] = array('B', char)
  211. pd[4 * i:4 * (i+1)] = pal
  212. return (picd, cd, pd)
  213. def preview(picd, cd, pd):
  214. preview = Image.new("RGB", im.size)
  215. preview.paste(iglyph, (x, y))
  216. return preview
  217. def glom(sizes):
  218. """ Returns a master size and a list of crop/paste coordinates """
  219. mw = max(w for (w,h) in sizes)
  220. mh = sum(h for (w,h) in sizes)
  221. y = 0
  222. r = []
  223. for (w,h) in sizes:
  224. r.append((0, y, w, y + h))
  225. y += h
  226. return ((mw,mh), r)
  227. def palettize(im, ncol):
  228. """ Given an input image or list of images, convert to a palettized version using at most ``ncol`` colors.
  229. This function preserves transparency: if the input(s) have transparency then the returned
  230. image(s) have ``.info['transparency']`` set to the transparent color.
  231. If ``im`` is a single image, returns a single image. If ``im`` is a list of images, returns a list of images.
  232. """
  233. assert ncol in (4, 16, 256)
  234. if isinstance(im, list):
  235. # For a list of images, paste them all into a single image,
  236. # palettize the single image, then return cropped subimages
  237. for i in im:
  238. i.load()
  239. (ms, lpos) = glom([i.size for i in im])
  240. master = Image.new(im[0].mode, ms)
  241. for i,ps in zip(im, lpos):
  242. master.paste(i, ps)
  243. master = palettize(master, ncol)
  244. ims = [master.crop(ps) for ps in lpos]
  245. for i in ims:
  246. i.info = master.info
  247. return ims
  248. else:
  249. im.load()
  250. if im.mode == 'P':
  251. if ord(max(im.tostring())) < ncol:
  252. return im # already done
  253. if 'transparency' in im.info:
  254. im = im.convert("RGBA")
  255. else:
  256. im = im.convert("RGB")
  257. assert im.mode in ("RGBA", "RGB")
  258. if im.mode == "RGB":
  259. return im.convert('P', palette=Image.ADAPTIVE, colors=ncol)
  260. else:
  261. alpha = im.split()[3]
  262. mask = Image.eval(alpha, lambda a: 255 if a <= 128 else 0)
  263. im.paste((0,0,0), mask)
  264. im = im.convert('RGB').convert('P', palette=Image.ADAPTIVE, colors = (ncol - 1))
  265. im.paste(ncol - 1, mask)
  266. im.info['transparency'] = ncol - 1
  267. return im
  268. def isnonblank(im):
  269. assert im.mode == 'P'
  270. if 'transparency' in im.info:
  271. transparent = im.info['transparency']
  272. (w,h) = im.size
  273. colors = set([im.getpixel((i, j)) for i in range(w) for j in range(h)])
  274. return colors != set([transparent])
  275. else:
  276. return True
  277. class ImageRAM(object):
  278. """
  279. The ImageRAM object simplifies loading of the Gameduino's 16K sprite image RAM.
  280. A caller adds sprite images to the ImageRAM, and finally obtains a memory image
  281. using :meth:`ImageRAM.used`.
  282. """
  283. def __init__(self, hh):
  284. self.hh = hh
  285. self.data = array('B', [0] * 16384)
  286. self.nxtpage = 0 # next available page
  287. self.nxtbit = 0 # next available bit
  288. def __bump(self, b):
  289. self.nxtbit += b
  290. if 8 == self.nxtbit:
  291. self.nxtbit = 0
  292. self.nxtpage += 1
  293. def add(self, page, size):
  294. """
  295. Add a sprite image to the ImageRAM
  296. :param page: image data, a list length 256
  297. :param size: size of data elements, either 4, 16 or 256
  298. :rtype: Returns a tuple (image, pal)
  299. This method adds the data in ``page`` to the ImageRAM, and the returns the assigned location. ``image`` is the
  300. sprite image 0-63 containing the data, and ``pal`` is the palette bit select for the data.
  301. For a 4-color image, ``pal`` is 0-3, for 16-color image ``pal`` is 0-1 and for a 256-color image ``pal`` is 0.
  302. The ``image`` and ``pal`` values may be used to display the sprite using :cpp:func:`GD::sprite`.
  303. If the data would cause the ImageRAM to increase beyond 16K, this method throws exception OverflowError.
  304. """
  305. assert size in (4,16,256)
  306. assert max(page) < size, "%d colors allowed, but page contains %d" % (size, max(page))
  307. assert len(page) == 256
  308. bits = {4:2, 16:4, 256:8}[size]
  309. while (self.nxtbit % bits) != 0:
  310. self.__bump(2)
  311. if self.nxtpage == 64:
  312. raise OverflowError
  313. if size == 4:
  314. pal = self.nxtbit / 2
  315. elif size == 16:
  316. pal = self.nxtbit / 4
  317. else:
  318. pal = 0
  319. pg = self.nxtpage
  320. for i in range(256):
  321. self.data[256 * self.nxtpage + i] |= (page[i] << self.nxtbit)
  322. self.__bump(bits)
  323. return (pg, pal)
  324. def addsprites(self, name, size, im, palset = PALETTE256A, center = (0,0)):
  325. """
  326. Extract multiple sprite frames from a source image, and generate the code to draw them.
  327. :param name: name of the sprite set; used to name the generated ``draw_`` function
  328. :param size: size of each sprite frame (width, height)
  329. :param im: source image, mode must be 'P' - paletted
  330. :param palset: palette set to use for the sprite, one of PALETTE256A-D, PALETTE16A-B, PALETTE4A-B
  331. :param center: the center pixel of the sprite image. Default is (0,0) meaning top left pixel.
  332. Given a sequence of sprite frames in ``im``, this method extracts their data and adds it to the ImageRAM.
  333. In addition, it writes the code to draw the sprite to the ImageRAM's header file. For example::
  334. import gameduino.prep as gdprep
  335. ir = gdprep.ImageRAM(open("hdr.h", "w"))
  336. rock0 = gdprep.palettize(Image.open("rock0r.png"), 16)
  337. ir.addsprites("rock0", (16, 16), rock0, gdprep.PALETTE16A, center = (8,8))
  338. would extract the four 16x16 frames from the ``rock0r.png`` image:
  339. .. image:: rock0r.png
  340. and write the following code to ``hdr.h``::
  341. #define ROCK0_FRAMES 4
  342. static void draw_rock0(int x, int y, byte anim, byte rot, byte jk = 0) {
  343. ...
  344. }
  345. For more more examples, see the :ref:`asteroids` demo game.
  346. """
  347. def get16x16(sheet, x, y):
  348. return sheet.crop((16*x, 16*y, 16*(x+1), 16*(y+1)))
  349. def walktile(im, size):
  350. for y in range(0, im.size[1], size[1]):
  351. for x in range(0, im.size[0], size[0]):
  352. yield im.crop((x, y, x + size[0], y + size[1]))
  353. tiles = list(walktile(im, size))
  354. print >>self.hh, "#define %s_FRAMES %d" % (name.upper(), len(tiles))
  355. animtype = ["byte", "int"][len(tiles) > 255]
  356. print >>self.hh, """static void draw_%s(int x, int y, %s anim, byte rot, byte jk = 0) {\n switch (anim) {""" % (name, animtype)
  357. if palset == PALETTE256A:
  358. ncolors = 256
  359. elif palset == PALETTE256B:
  360. ncolors = 256
  361. elif palset == PALETTE256C:
  362. ncolors = 256
  363. elif palset == PALETTE256D:
  364. ncolors = 256
  365. elif palset == PALETTE4A:
  366. ncolors = 4
  367. elif palset == PALETTE4B:
  368. ncolors = 4
  369. elif palset == PALETTE16A:
  370. ncolors = 16
  371. elif palset == PALETTE16B:
  372. ncolors = 16
  373. else:
  374. highest = ord(max(im.tostring()))
  375. ncolors = min([c for c in [4,16,256] if (highest < c)])
  376. for spr,spriteimage in enumerate(tiles):
  377. loads = []
  378. for y in range((size[1] + 15) / 16):
  379. for x in range((size[0] + 15) / 16):
  380. t = get16x16(spriteimage, x, y)
  381. t.info = im.info # workaround: PIL does not copy .info when cropping
  382. if isnonblank(t):
  383. (page, palsel) = self.add(array('B', t.tostring()), ncolors)
  384. loads += [" GD.xsprite(x, y, %d, %d, %d, %d, rot, jk);" % (x * 16 - center[0], y * 16 - center[1], page, palset[palsel])]
  385. if loads:
  386. print >>self.hh, " case %d:" % spr
  387. print >>self.hh, "\n".join(loads)
  388. print >>self.hh, " break;"
  389. print >>self.hh, """ }\n}\n"""
  390. def used(self):
  391. """
  392. Return the contents of the ImageRAM, as an :class:`array.array` of type 'B'.
  393. The size of the array depends on the amount of data added, up to a limit
  394. of 16K.
  395. """
  396. if self.nxtbit == 0:
  397. past = self.nxtpage
  398. else:
  399. past = self.nxtpage + 1
  400. return array('B', self.data[:256*past])
  401. import math
  402. def spectrum(specfile, cutoff = 64, volume = 255):
  403. """
  404. Read an Audacity spectrum file and return a list of (frequency, amplitude)
  405. pairs, loudest first.
  406. :param cutoff: length of the list of returned pairs
  407. :param volume: total volume of the returned pairs
  408. :rtype: list of tuples (frequency, amplitude) where frequency is a floating-point frequency in Hz, and amplitude in an integer amplitude.
  409. This function can be used to create voice profiles for instruments
  410. and sounds. For example to load a choir sound, previously saved
  411. as ``choir.txt``::
  412. for (i, (f, a)) in enumerate(spectrum("choir.txt")):
  413. gd.voice(i, 0, int(4 * f), a, a)
  414. """
  415. snd = [[float(t) for t in l.split()] for l in open(specfile) if not "Freq" in l]
  416. snd = [(f,db) for (f,db) in snd if 40 < f < 8192]
  417. snd = sorted(snd, reverse=True, key=lambda t:t[1])
  418. top = snd[:cutoff]
  419. amps = [(f,math.pow(2, .1 * db)) for (f, db) in top]
  420. samps = sum([a for (f,a) in amps])
  421. return [(f, int(volume * a / samps)) for (f, a) in amps]
  422. __all__ = [ "encode", "dump", "palettize", "getpal", "ImageRAM", "spectrum", ]