ipf.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. # greaseweazle/image/ipf.py
  2. #
  3. # Written & released by Keir Fraser <keir.xen@gmail.com>
  4. #
  5. # This is free and unencumbered software released into the public domain.
  6. # See the file COPYING for more details, or visit <http://unlicense.org>.
  7. import os, sys
  8. import platform
  9. import ctypes as ct
  10. import itertools as it
  11. from bitarray import bitarray
  12. from greaseweazle.track import MasterTrack, RawTrack
  13. from greaseweazle import error
  14. from .image import Image
  15. class CapsDateTimeExt(ct.Structure):
  16. _pack_ = 1
  17. _fields_ = [
  18. ('year', ct.c_uint),
  19. ('month', ct.c_uint),
  20. ('day', ct.c_uint),
  21. ('hour', ct.c_uint),
  22. ('min', ct.c_uint),
  23. ('sec', ct.c_uint),
  24. ('tick', ct.c_uint)]
  25. class CapsImageInfo(ct.Structure):
  26. platform_name = [
  27. "N/A", "Amiga", "Atari ST", "IBM PC", "Amstrad CPC",
  28. "Spectrum", "Sam Coupe", "Archimedes", "C64", "Atari (8-bit)" ]
  29. _pack_ = 1
  30. _fields_ = [
  31. ('type', ct.c_uint), # image type
  32. ('release', ct.c_uint), # release ID
  33. ('revision', ct.c_uint), # release revision ID
  34. ('mincylinder', ct.c_uint), # lowest cylinder number
  35. ('maxcylinder', ct.c_uint), # highest cylinder number
  36. ('minhead', ct.c_uint), # lowest head number
  37. ('maxhead', ct.c_uint), # highest head number
  38. ('crdt', CapsDateTimeExt), # image creation date.time
  39. ('platform', ct.c_uint * 4)] # intended platform(s)
  40. class CapsTrackInfoT2(ct.Structure):
  41. _pack_ = 1
  42. _fields_ = [
  43. ('type', ct.c_uint), # track type
  44. ('cylinder', ct.c_uint), # cylinder#
  45. ('head', ct.c_uint), # head#
  46. ('sectorcnt', ct.c_uint), # available sectors
  47. ('sectorsize', ct.c_uint), # sector size, unused
  48. ('trackbuf', ct.POINTER(ct.c_ubyte)), # track buffer memory
  49. ('tracklen', ct.c_uint), # track buffer memory length
  50. ('timelen', ct.c_uint), # timing buffer length
  51. ('timebuf', ct.POINTER(ct.c_uint)), # timing buffer
  52. ('overlap', ct.c_int), # overlap position
  53. ('startbit', ct.c_uint), # start position of the decoding
  54. ('wseed', ct.c_uint), # weak bit generator data
  55. ('weakcnt', ct.c_uint)] # number of weak data areas
  56. class CapsSectorInfo(ct.Structure):
  57. _pack_ = 1
  58. _fields_ = [
  59. ('descdatasize', ct.c_uint), # data size in bits from IPF descriptor
  60. ('descgapsize', ct.c_uint), # gap size in bits from IPF descriptor
  61. ('datasize', ct.c_uint), # data size in bits from decoder
  62. ('gapsize', ct.c_uint), # gap size in bits from decoder
  63. ('datastart', ct.c_uint), # data start pos in bits from decoder
  64. ('gapstart', ct.c_uint), # gap start pos in bits from decoder
  65. ('gapsizews0', ct.c_uint), # gap size before write splice
  66. ('gapsizews1', ct.c_uint), # gap size after write splice
  67. ('gapws0mode', ct.c_uint), # gap size mode before write splice
  68. ('gapws1mode', ct.c_uint), # gap size mode after write splice
  69. ('celltype', ct.c_uint), # bitcell type
  70. ('enctype', ct.c_uint)] # encoder type
  71. class CapsDataInfo(ct.Structure):
  72. _pack_ = 1
  73. _fields_ = [
  74. ('type', ct.c_uint), # data type
  75. ('start', ct.c_uint), # start position
  76. ('size', ct.c_uint)] # size in bits
  77. class DI_LOCK:
  78. INDEX = 1<<0
  79. ALIGN = 1<<1
  80. DENVAR = 1<<2
  81. DENAUTO = 1<<3
  82. DENNOISE = 1<<4
  83. NOISE = 1<<5
  84. NOISEREV = 1<<6
  85. MEMREF = 1<<7
  86. UPDATEFD = 1<<8
  87. TYPE = 1<<9
  88. DENALT = 1<<10
  89. OVLBIT = 1<<11
  90. TRKBIT = 1<<12
  91. NOUPDATE = 1<<13
  92. SETWSEED = 1<<14
  93. def_flags = (DENVAR | UPDATEFD | TYPE | OVLBIT | TRKBIT)
  94. class IPFTrack(MasterTrack):
  95. verify_revs = 2
  96. tolerance = 100
  97. @staticmethod
  98. def strong_data(sector, weak):
  99. """Return list of sector data areas excluding weak sections."""
  100. def range_next(i):
  101. s,l = next(i)
  102. return s, s+l
  103. weak_tol = 16 # Skip this number of bits after a weak area
  104. weak_iter = it.chain(weak, [(1<<30,1)])
  105. ws,we = -1,-1
  106. sector_iter = iter(sector)
  107. s,e = range_next(sector_iter)
  108. try:
  109. while True:
  110. while we <= s:
  111. ws,we = range_next(weak_iter)
  112. we += weak_tol
  113. if ws < e:
  114. if s < ws:
  115. yield (s,ws-s)
  116. s = we
  117. else:
  118. yield (s,e-s)
  119. s = e
  120. if s >= e:
  121. s,e = range_next(sector_iter)
  122. except StopIteration:
  123. pass
  124. def verify_track(self, flux):
  125. flux.cue_at_index()
  126. raw = RawTrack(clock = self.time_per_rev/len(self.bits), data = flux)
  127. raw_bits, _ = raw.get_all_data()
  128. for s,l in IPFTrack.strong_data(self.sectors, self.weak):
  129. sector = self.bits[s:s+l]
  130. # Search within an area +/- the pre-defined # bitcells tolerance
  131. raw_area = raw_bits[max(self.splice + s - self.tolerance, 0)
  132. : self.splice + s + l + self.tolerance]
  133. # All we care about is at least one match (this is a bit fuzzy)
  134. if next(raw_area.itersearch(sector), None) is None:
  135. return False
  136. return True
  137. class IPF(Image):
  138. read_only = True
  139. def __init__(self):
  140. self.lib = get_libcaps()
  141. def __del__(self):
  142. try:
  143. self.lib.CAPSUnlockAllTracks(self.iid)
  144. self.lib.CAPSUnlockImage(self.iid)
  145. self.lib.CAPSRemImage(self.iid)
  146. del(self.iid)
  147. except AttributeError:
  148. pass
  149. def __str__(self):
  150. pi = self.pi
  151. s = "IPF Image File:"
  152. s += "\n SPS ID: %04d (rev %d)" % (pi.release, pi.revision)
  153. s += "\n Platform: "
  154. nr_platforms = 0
  155. for p in pi.platform:
  156. if p == 0 and nr_platforms != 0:
  157. break
  158. if nr_platforms > 0:
  159. s += ", "
  160. s += pi.platform_name[p]
  161. nr_platforms += 1
  162. s += ("\n Created: %d/%d/%d %02d:%02d:%02d"
  163. % (pi.crdt.year, pi.crdt.month, pi.crdt.day,
  164. pi.crdt.hour, pi.crdt.min, pi.crdt.sec))
  165. s += ("\n Cyls: %d-%d Heads: %d-%d"
  166. % (pi.mincylinder, pi.maxcylinder, pi.minhead, pi.maxhead))
  167. return s
  168. @classmethod
  169. def from_file(cls, name):
  170. ipf = cls()
  171. ipf.iid = ipf.lib.CAPSAddImage()
  172. error.check(ipf.iid >= 0, "Could not create IPF image container")
  173. cname = ct.c_char_p(name.encode())
  174. res = ipf.lib.CAPSLockImage(ipf.iid, cname)
  175. error.check(res == 0, "Could not open IPF image '%s'" % name)
  176. res = ipf.lib.CAPSLoadImage(ipf.iid, DI_LOCK.def_flags)
  177. error.check(res == 0, "Could not load IPF image '%s'" % name)
  178. ipf.pi = CapsImageInfo()
  179. res = ipf.lib.CAPSGetImageInfo(ct.byref(ipf.pi), ipf.iid)
  180. error.check(res == 0, "Could not get info for IPF '%s'" % name)
  181. print(ipf)
  182. return ipf
  183. def get_track(self, cyl, head):
  184. pi = self.pi
  185. if head < pi.minhead or head > pi.maxhead:
  186. return None
  187. if cyl < pi.mincylinder or cyl > pi.maxcylinder:
  188. return None
  189. ti = CapsTrackInfoT2(2)
  190. res = self.lib.CAPSLockTrack(ct.byref(ti), self.iid,
  191. cyl, head, DI_LOCK.def_flags)
  192. error.check(res == 0, "Could not lock IPF track %d.%d" % (cyl, head))
  193. if not ti.trackbuf:
  194. return None # unformatted/empty
  195. carray_type = ct.c_ubyte * ((ti.tracklen+7)//8)
  196. carray = carray_type.from_address(
  197. ct.addressof(ti.trackbuf.contents))
  198. trackbuf = bitarray(endian='big')
  199. trackbuf.frombytes(bytes(carray))
  200. trackbuf = trackbuf[:ti.tracklen]
  201. data = []
  202. for i in range(ti.sectorcnt):
  203. si = CapsSectorInfo()
  204. res = self.lib.CAPSGetInfo(ct.byref(si), self.iid,
  205. cyl, head, 1, i)
  206. error.check(res == 0, "Couldn't get sector info")
  207. # Adjust the range start to be splice- rather than index-relative
  208. data.append(((si.datastart - ti.overlap) % ti.tracklen,
  209. si.datasize))
  210. weak = []
  211. for i in range(ti.weakcnt):
  212. wi = CapsDataInfo()
  213. res = self.lib.CAPSGetInfo(ct.byref(wi), self.iid,
  214. cyl, head, 2, i)
  215. error.check(res == 0, "Couldn't get weak data info")
  216. # Adjust the range start to be splice- rather than index-relative
  217. weak.append(((wi.start - ti.overlap) % ti.tracklen, wi.size))
  218. timebuf = None
  219. if ti.timebuf:
  220. carray_type = ct.c_uint * ti.timelen
  221. carray = carray_type.from_address(
  222. ct.addressof(ti.timebuf.contents))
  223. # Unpack the per-byte timing info into per-bitcell
  224. timebuf = []
  225. for i in carray:
  226. for j in range(8):
  227. timebuf.append(i)
  228. # Pad the timing info with normal cell lengths as necessary
  229. for j in range(len(carray)*8, ti.tracklen):
  230. timebuf.append(1000)
  231. # Clip the timing info, if necessary.
  232. timebuf = timebuf[:ti.tracklen]
  233. # Rotate the track to start at the splice rather than the index.
  234. if ti.overlap:
  235. trackbuf = trackbuf[ti.overlap:] + trackbuf[:ti.overlap]
  236. if timebuf:
  237. timebuf = timebuf[ti.overlap:] + timebuf[:ti.overlap]
  238. # We don't really have access to the bitrate. It depends on RPM.
  239. # So we assume a rotation rate of 300 RPM (5 rev/sec).
  240. rpm = 300
  241. track = IPFTrack(
  242. bits = trackbuf,
  243. time_per_rev = 60/rpm,
  244. bit_ticks = timebuf,
  245. splice = ti.overlap,
  246. weak = weak)
  247. track.verify = track
  248. track.sectors = data
  249. return track
  250. # Open and initialise the CAPS library.
  251. def open_libcaps():
  252. # Get the OS-dependent list of valid CAPS library names.
  253. _names = []
  254. if platform.system() == "Linux":
  255. _names = [ "libcapsimage.so.5", "libcapsimage.so.5.1",
  256. "libcapsimage.so.4", "libcapsimage.so.4.2",
  257. "libcapsimage.so" ]
  258. elif platform.system() == "Darwin":
  259. _names = [ "CAPSImage.framework/CAPSImage" ]
  260. elif platform.system() == "Windows":
  261. _names = [ "CAPSImg_x64.dll", "CAPSImg.dll" ]
  262. # Get the absolute path to the root Greaseweazle folder.
  263. path = os.path.dirname(os.path.abspath(__file__))
  264. for _ in range(3):
  265. path = os.path.join(path, os.pardir)
  266. path = os.path.normpath(path)
  267. # Create a search list of both relative and absolute library names.
  268. names = []
  269. for name in _names:
  270. names.append(name)
  271. names.append(os.path.join(path, name))
  272. # Walk the search list, trying to open the CAPS library.
  273. for name in names:
  274. try:
  275. lib = ct.cdll.LoadLibrary(name)
  276. break
  277. except:
  278. pass
  279. error.check("lib" in locals(), """\
  280. Could not find SPS/CAPS IPF decode library
  281. For installation instructions please read the wiki:
  282. <https://github.com/keirf/Greaseweazle/wiki/IPF-Images>""")
  283. # We have opened the library. Now initialise it.
  284. res = lib.CAPSInit()
  285. error.check(res == 0, "Failure initialising IPF library '%s'" % name)
  286. return lib
  287. # Get a reference to the CAPS library. Open it if necessary.
  288. def get_libcaps():
  289. global libcaps
  290. if not 'libcaps' in globals():
  291. libcaps = open_libcaps()
  292. return libcaps
  293. # Local variables:
  294. # python-indent: 4
  295. # End: