diskstack 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. diskstack/__init__.py +5 -0
  2. diskstack/_vendor/__init__.py +0 -0
  3. diskstack/_vendor/greaseweazle/COPYING +24 -0
  4. diskstack/_vendor/greaseweazle/__init__.py +2 -0
  5. diskstack/_vendor/greaseweazle/codec/__init__.py +0 -0
  6. diskstack/_vendor/greaseweazle/codec/amiga/__init__.py +0 -0
  7. diskstack/_vendor/greaseweazle/codec/amiga/amigados.py +221 -0
  8. diskstack/_vendor/greaseweazle/codec/apple2/__init__.py +0 -0
  9. diskstack/_vendor/greaseweazle/codec/apple2/apple2_gcr.py +223 -0
  10. diskstack/_vendor/greaseweazle/codec/bitcell.py +117 -0
  11. diskstack/_vendor/greaseweazle/codec/codec.py +362 -0
  12. diskstack/_vendor/greaseweazle/codec/commodore/__init__.py +0 -0
  13. diskstack/_vendor/greaseweazle/codec/commodore/c64_gcr.py +229 -0
  14. diskstack/_vendor/greaseweazle/codec/datageneral/__init__.py +0 -0
  15. diskstack/_vendor/greaseweazle/codec/datageneral/datageneral.py +211 -0
  16. diskstack/_vendor/greaseweazle/codec/hp/__init__.py +0 -0
  17. diskstack/_vendor/greaseweazle/codec/hp/hp_mmfm.py +258 -0
  18. diskstack/_vendor/greaseweazle/codec/ibm/__init__.py +0 -0
  19. diskstack/_vendor/greaseweazle/codec/ibm/ibm.py +1005 -0
  20. diskstack/_vendor/greaseweazle/codec/macintosh/__init__.py +0 -0
  21. diskstack/_vendor/greaseweazle/codec/macintosh/mac_gcr.py +238 -0
  22. diskstack/_vendor/greaseweazle/codec/micropolis/__init__.py +0 -0
  23. diskstack/_vendor/greaseweazle/codec/micropolis/micropolis.py +233 -0
  24. diskstack/_vendor/greaseweazle/codec/northstar/__init__.py +0 -0
  25. diskstack/_vendor/greaseweazle/codec/northstar/northstar.py +210 -0
  26. diskstack/_vendor/greaseweazle/data/__init__.py +0 -0
  27. diskstack/_vendor/greaseweazle/data/diskdefs.cfg +81 -0
  28. diskstack/_vendor/greaseweazle/data/diskdefs_acorn.cfg +119 -0
  29. diskstack/_vendor/greaseweazle/data/diskdefs_akai.cfg +27 -0
  30. diskstack/_vendor/greaseweazle/data/diskdefs_amiga.cfg +17 -0
  31. diskstack/_vendor/greaseweazle/data/diskdefs_apple2.cfg +31 -0
  32. diskstack/_vendor/greaseweazle/data/diskdefs_apricot.cfg +25 -0
  33. diskstack/_vendor/greaseweazle/data/diskdefs_atari.cfg +33 -0
  34. diskstack/_vendor/greaseweazle/data/diskdefs_atarist.cfg +76 -0
  35. diskstack/_vendor/greaseweazle/data/diskdefs_coco.cfg +98 -0
  36. diskstack/_vendor/greaseweazle/data/diskdefs_commodore.cfg +127 -0
  37. diskstack/_vendor/greaseweazle/data/diskdefs_datageneral.cfg +8 -0
  38. diskstack/_vendor/greaseweazle/data/diskdefs_dec.cfg +37 -0
  39. diskstack/_vendor/greaseweazle/data/diskdefs_dragon.cfg +61 -0
  40. diskstack/_vendor/greaseweazle/data/diskdefs_eagle.cfg +25 -0
  41. diskstack/_vendor/greaseweazle/data/diskdefs_ensoniq.cfg +39 -0
  42. diskstack/_vendor/greaseweazle/data/diskdefs_epson.cfg +95 -0
  43. diskstack/_vendor/greaseweazle/data/diskdefs_gem.cfg +15 -0
  44. diskstack/_vendor/greaseweazle/data/diskdefs_hp.cfg +25 -0
  45. diskstack/_vendor/greaseweazle/data/diskdefs_ibm.cfg +140 -0
  46. diskstack/_vendor/greaseweazle/data/diskdefs_kaypro.cfg +58 -0
  47. diskstack/_vendor/greaseweazle/data/diskdefs_luxor.cfg +96 -0
  48. diskstack/_vendor/greaseweazle/data/diskdefs_mac.cfg +71 -0
  49. diskstack/_vendor/greaseweazle/data/diskdefs_micropolis.cfg +73 -0
  50. diskstack/_vendor/greaseweazle/data/diskdefs_mm1.cfg +69 -0
  51. diskstack/_vendor/greaseweazle/data/diskdefs_msx.cfg +45 -0
  52. diskstack/_vendor/greaseweazle/data/diskdefs_northstar.cfg +37 -0
  53. diskstack/_vendor/greaseweazle/data/diskdefs_occ1.cfg +23 -0
  54. diskstack/_vendor/greaseweazle/data/diskdefs_olivetti.cfg +18 -0
  55. diskstack/_vendor/greaseweazle/data/diskdefs_pc98.cfg +69 -0
  56. diskstack/_vendor/greaseweazle/data/diskdefs_raw.cfg +25 -0
  57. diskstack/_vendor/greaseweazle/data/diskdefs_rm.cfg +98 -0
  58. diskstack/_vendor/greaseweazle/data/diskdefs_sci.cfg +15 -0
  59. diskstack/_vendor/greaseweazle/data/diskdefs_sega.cfg +13 -0
  60. diskstack/_vendor/greaseweazle/data/diskdefs_sharp.cfg +13 -0
  61. diskstack/_vendor/greaseweazle/data/diskdefs_thomson.cfg +78 -0
  62. diskstack/_vendor/greaseweazle/data/diskdefs_tsc.cfg +81 -0
  63. diskstack/_vendor/greaseweazle/data/diskdefs_xerox.cfg +59 -0
  64. diskstack/_vendor/greaseweazle/data/diskdefs_zx.cfg +245 -0
  65. diskstack/_vendor/greaseweazle/error.py +19 -0
  66. diskstack/_vendor/greaseweazle/flux.py +315 -0
  67. diskstack/_vendor/greaseweazle/image/__init__.py +0 -0
  68. diskstack/_vendor/greaseweazle/image/a2r.py +133 -0
  69. diskstack/_vendor/greaseweazle/image/acorn.py +28 -0
  70. diskstack/_vendor/greaseweazle/image/adf.py +15 -0
  71. diskstack/_vendor/greaseweazle/image/apple2.py +18 -0
  72. diskstack/_vendor/greaseweazle/image/apridisk.py +107 -0
  73. diskstack/_vendor/greaseweazle/image/caps.py +464 -0
  74. diskstack/_vendor/greaseweazle/image/d64.py +51 -0
  75. diskstack/_vendor/greaseweazle/image/d81.py +28 -0
  76. diskstack/_vendor/greaseweazle/image/d88.py +211 -0
  77. diskstack/_vendor/greaseweazle/image/dcp.py +45 -0
  78. diskstack/_vendor/greaseweazle/image/dim.py +53 -0
  79. diskstack/_vendor/greaseweazle/image/dmk.py +166 -0
  80. diskstack/_vendor/greaseweazle/image/dsk.py +31 -0
  81. diskstack/_vendor/greaseweazle/image/edsk.py +591 -0
  82. diskstack/_vendor/greaseweazle/image/fd.py +23 -0
  83. diskstack/_vendor/greaseweazle/image/fdi.py +41 -0
  84. diskstack/_vendor/greaseweazle/image/hdm.py +15 -0
  85. diskstack/_vendor/greaseweazle/image/hfe.py +742 -0
  86. diskstack/_vendor/greaseweazle/image/image.py +130 -0
  87. diskstack/_vendor/greaseweazle/image/imd.py +201 -0
  88. diskstack/_vendor/greaseweazle/image/img.py +111 -0
  89. diskstack/_vendor/greaseweazle/image/kryoflux.py +304 -0
  90. diskstack/_vendor/greaseweazle/image/mgt.py +15 -0
  91. diskstack/_vendor/greaseweazle/image/msa.py +153 -0
  92. diskstack/_vendor/greaseweazle/image/nfd.py +113 -0
  93. diskstack/_vendor/greaseweazle/image/nsi.py +47 -0
  94. diskstack/_vendor/greaseweazle/image/scp.py +471 -0
  95. diskstack/_vendor/greaseweazle/image/sf7.py +15 -0
  96. diskstack/_vendor/greaseweazle/image/sharp2d.py +15 -0
  97. diskstack/_vendor/greaseweazle/image/td0.py +141 -0
  98. diskstack/_vendor/greaseweazle/image/xdf.py +15 -0
  99. diskstack/_vendor/greaseweazle/optimised/__init__.py +22 -0
  100. diskstack/_vendor/greaseweazle/tools/__init__.py +0 -0
  101. diskstack/_vendor/greaseweazle/tools/util.py +133 -0
  102. diskstack/_vendor/greaseweazle/track.py +494 -0
  103. diskstack/cache.py +210 -0
  104. diskstack/candidates.py +396 -0
  105. diskstack/cli.py +348 -0
  106. diskstack/errors.py +5 -0
  107. diskstack/filler.py +29 -0
  108. diskstack/formats.py +381 -0
  109. diskstack/parallel.py +73 -0
  110. diskstack/report.py +353 -0
  111. diskstack/stack.py +317 -0
  112. diskstack-1.0.0.dist-info/METADATA +329 -0
  113. diskstack-1.0.0.dist-info/RECORD +119 -0
  114. diskstack-1.0.0.dist-info/WHEEL +5 -0
  115. diskstack-1.0.0.dist-info/entry_points.txt +2 -0
  116. diskstack-1.0.0.dist-info/licenses/LICENSE +21 -0
  117. diskstack-1.0.0.dist-info/licenses/NOTICE +55 -0
  118. diskstack-1.0.0.dist-info/licenses/diskstack/_vendor/greaseweazle/COPYING +24 -0
  119. diskstack-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,133 @@
1
+ # Extracted from greaseweazle/tools/util.py by tools/vendor.py.
2
+ # The rest of that module drives the USB hardware and needs pyserial.
3
+ from __future__ import annotations
4
+ from typing import List, Optional
5
+ import re
6
+ import itertools as it
7
+ from diskstack._vendor.greaseweazle import error
8
+
9
+
10
+ def columnify(strings, columns=80, sep=2):
11
+ max_len = max(len(s) for s in strings) + sep
12
+ per_row = max(1, columns // max_len)
13
+ return '\n'.join(map(lambda row: (f'{{:{max_len}}}'*per_row).format(*row),
14
+ it.zip_longest(*[iter(strings)]*per_row,
15
+ fillvalue='')))
16
+
17
+
18
+ def range_str(l):
19
+ if len(l) == 0:
20
+ return '<none>'
21
+ p, str = None, ''
22
+ for i in l:
23
+ if p is not None and i == p[1]+1:
24
+ p = p[0], i
25
+ continue
26
+ if p is not None:
27
+ str += ('%d,' % p[0]) if p[0] == p[1] else ('%d-%d,' % p)
28
+ p = (i,i)
29
+ if p is not None:
30
+ str += ('%d' % p[0]) if p[0] == p[1] else ('%d-%d' % p)
31
+ return str
32
+
33
+ class TrackSet:
34
+
35
+ class TrackIter:
36
+ """Iterate over a TrackSet in physical <cyl,head> order."""
37
+ def __init__(self, ts):
38
+ l = []
39
+ for c in ts.cyls:
40
+ for h in ts.heads:
41
+ pc, ph = ts.ch_to_pch(c, h)
42
+ l.append((pc, ph, c, h))
43
+ l.sort()
44
+ self.l = iter(l)
45
+ def __iter__(self):
46
+ return self
47
+ def __next__(self):
48
+ (self.physical_cyl, self.physical_head,
49
+ self.cyl, self.head) = next(self.l)
50
+ return self
51
+
52
+ def __init__(self, trackspec):
53
+ self.cyls = list()
54
+ self.heads = list()
55
+ self.h_off = [0]*2
56
+ self.step = 1
57
+ self.hswap = False
58
+ self.trackspec = ''
59
+ self.update_from_trackspec(trackspec)
60
+
61
+ def ch_to_pch(self, c, h):
62
+ pc = c//-self.step if self.step < 0 else c*self.step
63
+ pc += self.h_off[h]
64
+ ph = 1-h if self.hswap else h
65
+ return pc, ph
66
+
67
+ def update_from_trackspec(self, trackspec):
68
+ """Update a TrackSet based on a trackspec."""
69
+ self.trackspec += trackspec
70
+ for x in trackspec.split(':'):
71
+ if x == 'hswap':
72
+ self.hswap = True
73
+ continue
74
+ k,v = x.split('=')
75
+ if k == 'c':
76
+ cyls = set()
77
+ for crange in v.split(','):
78
+ m = re.match(r'(\d+)(-(\d+)(/(\d+))?)?$', crange)
79
+ if m is None: raise ValueError()
80
+ if m.group(3) is None:
81
+ s,e,step = int(m.group(1)), int(m.group(1)), 1
82
+ else:
83
+ s,e,step = int(m.group(1)), int(m.group(3)), 1
84
+ if m.group(5) is not None:
85
+ step = int(m.group(5))
86
+ for c in range(s, e+1, step):
87
+ cyls.add(c)
88
+ self.cyls = sorted(cyls)
89
+ elif k == 'h':
90
+ heads = [False]*2
91
+ for hrange in v.split(','):
92
+ m = re.match(r'([01])(-([01]))?$', hrange)
93
+ if m is None: raise ValueError()
94
+ if m.group(3) is None:
95
+ s,e = int(m.group(1)), int(m.group(1))
96
+ else:
97
+ s,e = int(m.group(1)), int(m.group(3))
98
+ for h in range(s, e+1):
99
+ heads[h] = True
100
+ self.heads = []
101
+ for h in range(len(heads)):
102
+ if heads[h]: self.heads.append(h)
103
+ elif re.match(r'h[01].off$', k):
104
+ h = int(re.match(r'h([01]).off$', k).group(1))
105
+ m = re.match(r'([+-]\d+)$', v)
106
+ if m is None: raise ValueError()
107
+ self.h_off[h] = int(m.group(1))
108
+ elif k == 'step':
109
+ m = re.match(r'1/(\d+)$', v)
110
+ self.step = -int(m.group(1)) if m is not None else int(v)
111
+ else:
112
+ raise ValueError()
113
+
114
+ def __str__(self):
115
+ s = 'c=%s' % range_str(self.cyls)
116
+ s += ':h=%s' % range_str(self.heads)
117
+ for i in range(len(self.h_off)):
118
+ x = self.h_off[i]
119
+ if x != 0:
120
+ s += ':h%d.off=%s%d' % (i, '+' if x >= 0 else '', x)
121
+ if self.step != 1:
122
+ s += ':step=' + (('1/%d' % -self.step) if self.step < 0
123
+ else ('%d' % self.step))
124
+ if self.hswap: s += ':hswap'
125
+ return s
126
+
127
+ def __iter__(self):
128
+ return self.TrackIter(self)
129
+
130
+ def __contains__(self, key):
131
+ c, h = key
132
+ return c in self.cyls and h in self.heads
133
+
@@ -0,0 +1,494 @@
1
+ # greaseweazle/track.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
+
8
+ from typing import Any, List, Optional, Tuple, Union, Protocol
9
+ import binascii
10
+ import itertools as it
11
+ from bitarray import bitarray
12
+ from diskstack._vendor.greaseweazle.flux import Flux, WriteoutFlux
13
+ from diskstack._vendor.greaseweazle import optimised
14
+
15
+ class PLL:
16
+ def __init__(self, pllspec: str):
17
+ self.period_adj_pct = 5
18
+ self.phase_adj_pct = 60
19
+ self.lowpass_thresh = None
20
+ for x in pllspec.split(':'):
21
+ k,v = x.split('=')
22
+ if k == 'period':
23
+ self.period_adj_pct = int(v)
24
+ elif k == 'phase':
25
+ self.phase_adj_pct = int(v)
26
+ elif k == 'lowpass':
27
+ self.lowpass_thresh = float(v)/1e6
28
+ else:
29
+ raise ValueError()
30
+ def __str__(self) -> str:
31
+ s = ("PLL: period_adj=%d%% phase_adj=%d%%"
32
+ % (self.period_adj_pct, self.phase_adj_pct))
33
+ if self.lowpass_thresh is not None:
34
+ s += (" lowpass_thresh=%.2f" % (self.lowpass_thresh*1e6))
35
+ return s
36
+
37
+ plls = [
38
+ # Default: An aggressive PLL which will quickly sync to extreme
39
+ # bit timings. For example: long tracks and variable-rate tracks.
40
+ PLL('period=5:phase=60'),
41
+ # Fallback: A conservative PLL which is good at ignoring noise in
42
+ # otherwise fairly well-behaved tracks. For example: high-frequency
43
+ # noise caused by dirt and mould on an old disk.
44
+ PLL('period=1:phase=10')
45
+ ]
46
+
47
+ # Precompensation to apply to a MasterTrack for writeout.
48
+ class Precomp:
49
+ MFM = 0
50
+ FM = 1
51
+ GCR = 2
52
+ TYPESTRING = [ 'MFM', 'FM', 'GCR' ]
53
+ def __str__(self) -> str:
54
+ return "Precomp: %s, %dns" % (Precomp.TYPESTRING[self.type], self.ns)
55
+ def __init__(self, type: int, ns: float):
56
+ self.type = type
57
+ self.ns = ns
58
+ def apply(self, bits: bitarray, bit_ticks: List[float],
59
+ scale: float) -> None:
60
+ t = self.ns * scale
61
+ if self.type == Precomp.MFM:
62
+ for i in bits.search(bitarray('10100', endian='big')):
63
+ bit_ticks[i+2] -= t
64
+ bit_ticks[i+3] += t
65
+ for i in bits.search(bitarray('00101', endian='big')):
66
+ bit_ticks[i+2] += t
67
+ bit_ticks[i+3] -= t
68
+ # This is primarily for GCR and FM which permit adjacent 1s (and
69
+ # have correspondingly slower bit times). However it may be useful
70
+ # for illegal MFM sequences too, especially on Amiga (custom syncwords,
71
+ # 4us-bitcell tracks). Normal MFM should not trigger these patterns.
72
+ for i in bits.search(bitarray('110', endian='big')):
73
+ bit_ticks[i+1] -= t
74
+ bit_ticks[i+2] += t
75
+ for i in bits.search(bitarray('011', endian='big')):
76
+ bit_ticks[i+1] += t
77
+ bit_ticks[i+2] -= t
78
+
79
+ class HasVerify(Protocol):
80
+ verify_revs: float
81
+ def verify_track(self, flux: Flux) -> bool:
82
+ ...
83
+
84
+ # A pristine representation of a track, from a codec and/or a perfect image.
85
+ class MasterTrack:
86
+
87
+ # Verify capability may be added "ad hoc" to a MasterTrack object
88
+ verify: Optional[HasVerify] = None
89
+
90
+ @property
91
+ def bitrate(self) -> float:
92
+ return len(self.bits) / self.time_per_rev
93
+
94
+ def scale(self, factor: float):
95
+ """Scale up index timing by specified factor."""
96
+ self.time_per_rev *= factor
97
+
98
+ # bits: Track bitcell data, aligned to the write splice (bitarray or bytes)
99
+ # time_per_rev: Time per revolution, in seconds (float)
100
+ # bit_ticks: Per-bitcell time values, in unitless 'ticks'
101
+ # splice: Location of the track splice, in bitcells, after the index
102
+ # weak: List of (start, length) weak ranges
103
+ # hardsector_bits: Optional list of hard-sector lengths, in bitcells
104
+ def __init__(
105
+ self,
106
+ bits: Union[bitarray, bytes],
107
+ time_per_rev: float,
108
+ bit_ticks: Optional[List[float]] = None,
109
+ splice: int = 0,
110
+ weak: List[Tuple[int,int]] = [],
111
+ hardsector_bits: Optional[List[int]] = None
112
+ ) -> None:
113
+ self.bits: bitarray
114
+ if isinstance(bits, bytes):
115
+ self.bits = bitarray(endian='big')
116
+ self.bits.frombytes(bits)
117
+ else:
118
+ self.bits = bits
119
+ self.time_per_rev = time_per_rev
120
+ self.bit_ticks = bit_ticks
121
+ self.splice = splice
122
+ self.weak = weak
123
+ self.precomp: Optional[Precomp] = None
124
+ self.force_random_weak = True
125
+ self.hardsector_bits = hardsector_bits
126
+
127
+ def __str__(self) -> str:
128
+ s = "\nMaster Track: splice @ %d\n" % self.splice
129
+ s += (" %d bits, %.1f kbit/s"
130
+ % (len(self.bits), self.bitrate/1000))
131
+ if self.bit_ticks:
132
+ s += " (variable)"
133
+ s += ("\n %.1f ms / rev (%.1f rpm)"
134
+ % (self.time_per_rev * 1000, 60 / self.time_per_rev))
135
+ if len(self.weak) > 0:
136
+ s += "\n %d weak range" % len(self.weak)
137
+ if len(self.weak) > 1: s += "s"
138
+ s += ": " + ", ".join(str(n) for _,n in self.weak) + " bits"
139
+ #s += str(binascii.hexlify(self.bits.tobytes()))
140
+ return s
141
+
142
+ def summary_string(self) -> str:
143
+ s = ('Bitcells (%d bits, %.1f kbit/s, %.1f rpm'
144
+ % (len(self.bits), self.bitrate/1000, 60 / self.time_per_rev))
145
+ if self.bit_ticks:
146
+ s += ', variable'
147
+ if len(self.weak) > 0:
148
+ s += ', weak'
149
+ s += ')'
150
+ return s
151
+
152
+ def reverse(self) -> None:
153
+ bitlen = len(self.bits)
154
+ if bitlen == 0: return
155
+ self.bits.reverse()
156
+ if self.bit_ticks is not None:
157
+ self.bit_ticks.reverse()
158
+ self.splice = -self.splice % bitlen
159
+ self.weak = list(map(lambda x: (-x[0] % bitlen, x[1]), self.weak))
160
+ if self.hardsector_bits is not None:
161
+ self.hardsector_bits.reverse()
162
+
163
+ def flux(self, revs: Optional[int] = None) -> Flux:
164
+ flux = self._flux(for_writeout=False, cue_at_index=True, revs=revs)
165
+ assert isinstance(flux, Flux)
166
+ return flux
167
+
168
+ def flux_for_writeout(self, cue_at_index) -> WriteoutFlux:
169
+ wflux = self._flux(for_writeout=True, cue_at_index=cue_at_index)
170
+ assert isinstance(wflux, WriteoutFlux)
171
+ return wflux
172
+
173
+ def _flux(self,
174
+ for_writeout: bool,
175
+ cue_at_index: bool,
176
+ revs: Optional[int] = None
177
+ ) -> Union[Flux, WriteoutFlux]:
178
+
179
+ # We're going to mess with the track data, so take a copy.
180
+ bits = self.bits.copy()
181
+ bitlen = len(bits)
182
+
183
+ # Also copy the bit_ticks array (or create a dummy one), and remember
184
+ # the total ticks that it contains.
185
+ bit_ticks = self.bit_ticks.copy() if self.bit_ticks else [1] * bitlen
186
+ ticks_to_index = sum(bit_ticks)
187
+
188
+ # Weak regions need special processing for correct flux representation.
189
+ for s, n in self.weak:
190
+ if n < 2: continue # Too short to reliably weaken
191
+ e = s + n
192
+ assert 0 <= s < e <= bitlen
193
+ pattern = bitarray(endian="big")
194
+ if n < 400 or self.force_random_weak:
195
+ # Short weak regions are written with no flux transitions.
196
+ # Actually we insert a flux transition every 32 bitcells, else
197
+ # we risk triggering Greaseweazle's No Flux Area generator.
198
+ pattern.frombytes(b"\x80\x00\x00\x00")
199
+ bits[s:e] = (pattern * (n//32+1))[:n]
200
+ else:
201
+ # Long weak regions we present a fuzzy clock bit in an
202
+ # otherwise normal byte (16 bits MFM). The byte may be
203
+ # interpreted as
204
+ # MFM 0001001010100101 = 12A5 = byte 0x43, or
205
+ # MFM 0001001010010101 = 1295 = byte 0x47
206
+ pattern.frombytes(b"\x12\xA5")
207
+ bits[s:e] = (pattern * (n//16+1))[:n]
208
+ for i in range(0, n-10, 16):
209
+ x, y = bit_ticks[s+i+10], bit_ticks[s+i+11]
210
+ bit_ticks[s+i+10], bit_ticks[s+i+11] = x+y*0.5, y*0.5
211
+ # To prevent corrupting a preceding sync word by effectively
212
+ # starting the weak region early, we start with a 1 if we just
213
+ # clocked out a 0.
214
+ bits[s] = not bits[s-1]
215
+ # Similarly modify the last bit of the weak region.
216
+ bits[e-1] = not(bits[e-2] or bits[e % bitlen])
217
+
218
+ if cue_at_index:
219
+ # Rotate data to start at the index.
220
+ index = -self.splice % bitlen
221
+ if index != 0:
222
+ bits = bits[index:] + bits[:index]
223
+ bit_ticks = bit_ticks[index:] + bit_ticks[:index]
224
+ splice_at_index = index < 4 or bitlen - index < 4
225
+ else:
226
+ assert for_writeout
227
+ splice_at_index = False
228
+
229
+ if not for_writeout:
230
+ # Do not extend the track for reliable writeout to disk.
231
+ pass
232
+ elif not cue_at_index:
233
+ # We write the track wherever it may fall (uncued).
234
+ # We stretch the track with extra header gap bytes, in case the
235
+ # drive spins slow and we need more length to create an overlap.
236
+ # Thus if the drive spins slow, the track gets a longer header.
237
+ pos = 4
238
+ # We stretch by 10 percent, which is way more than enough.
239
+ rep = bitlen // (10 * 32)
240
+ bit_ticks = bit_ticks[pos:pos+32] * rep + bit_ticks[pos:]
241
+ bits = bits[pos:pos+32] * rep + bits[pos:]
242
+ elif splice_at_index:
243
+ # Splice is at the index (or within a few bitcells of it).
244
+ # We stretch the track with extra footer gap bytes, in case the
245
+ # drive motor spins slower than expected and we need more filler
246
+ # to get us to the index pulse (where the write will terminate).
247
+ # Thus if the drive spins slow, the track gets a longer footer.
248
+ pos = (self.splice - 4) % bitlen
249
+ # We stretch by 10 percent, which is way more than enough.
250
+ rep = bitlen // (10 * 32)
251
+ bit_ticks = bit_ticks[:pos] + bit_ticks[pos-32:pos] * rep
252
+ bits = bits[:pos] + bits[pos-32:pos] * rep
253
+ else:
254
+ # Splice is not at the index. We will write more than one
255
+ # revolution, and terminate the second revolution at the splice.
256
+ # For the first revolution we repeat the track header *backwards*
257
+ # to the very start of the write. This is in case the drive motor
258
+ # spins slower than expected and the write ends before the original
259
+ # splice position.
260
+ # Thus if the drive spins slow, the track gets a longer header.
261
+ bit_ticks += bit_ticks[:self.splice-4]
262
+ bits += bits[:self.splice-4]
263
+ pos = self.splice+4
264
+ fill_pattern = bits[pos:pos+32]
265
+ while pos >= 32:
266
+ pos -= 32
267
+ bits[pos:pos+32] = fill_pattern
268
+
269
+ if for_writeout and self.precomp is not None:
270
+ self.precomp.apply(bits, bit_ticks,
271
+ ticks_to_index / (self.time_per_rev*1e9))
272
+
273
+ # Convert the stretched track data into flux.
274
+ bit_ticks_i = iter(bit_ticks)
275
+ flux_list = []
276
+ flux_ticks: float = 0
277
+ for bit in bits:
278
+ flux_ticks += next(bit_ticks_i)
279
+ if bit:
280
+ flux_list.append(flux_ticks)
281
+ flux_ticks = 0
282
+
283
+ # Package up WriteoutFlux.
284
+ if for_writeout:
285
+ if flux_ticks:
286
+ flux_list.append(flux_ticks)
287
+ return WriteoutFlux(
288
+ ticks_to_index, flux_list,
289
+ sample_freq = ticks_to_index / self.time_per_rev,
290
+ index_cued = cue_at_index,
291
+ terminate_at_index = splice_at_index)
292
+
293
+ # Package up Flux.
294
+ index_list = [ticks_to_index]
295
+ if revs is None:
296
+ # Emit two revolutions if track data crosses the index.
297
+ revs = 1 if splice_at_index else 2
298
+ assert revs is not None and revs > 0
299
+ if revs > 1:
300
+ l = flux_list
301
+ for i in range(revs-1):
302
+ flux_list = l + [flux_ticks+flux_list[0]] + flux_list[1:]
303
+ index_list *= revs
304
+ flux = Flux(index_list, flux_list,
305
+ sample_freq = ticks_to_index / self.time_per_rev,
306
+ index_cued = True)
307
+ flux.splice = sum(bit_ticks[:self.splice])
308
+ return flux
309
+
310
+ class PLLRevolution:
311
+ def __init__(self, nr_bits: int,
312
+ hardsector_bits: Optional[List[int]] = None) -> None:
313
+ self.nr_bits = nr_bits
314
+ self.hardsector_bits = hardsector_bits
315
+
316
+ # Track data generated from flux.
317
+ class PLLTrack:
318
+
319
+ # clock: Expected time per raw bitcell, in seconds (float)
320
+ # data: Flux object, or a form convertible to a Flux object
321
+ # time_per_rev: Expected time per revolution, in seconds (optional, float)
322
+ # lowpass_thresh: Merge short fluxes with adjacent fluxes (optional, float)
323
+ def __init__(self, clock: float, data, time_per_rev=None, pll=None,
324
+ lowpass_thresh=None):
325
+ self.clock = clock
326
+ self.time_per_rev = time_per_rev
327
+ self.clock_max_adj = 0.10
328
+ if pll is None: pll = plls[0]
329
+ self.pll_period_adj = pll.period_adj_pct / 100
330
+ self.pll_phase_adj = pll.phase_adj_pct / 100
331
+ self.lowpass_thresh = (lowpass_thresh if pll.lowpass_thresh is None
332
+ else pll.lowpass_thresh)
333
+ self.bitarray = bitarray(endian='big')
334
+ self.timearray: List[float] = []
335
+ self.revolutions: List[PLLRevolution] = []
336
+ self.import_flux_data(data)
337
+
338
+
339
+ def __str__(self) -> str:
340
+ s = "\nRaw Track: %d revolutions\n" % len(self.revolutions)
341
+ for rev in range(len(self.revolutions)):
342
+ b, _ = self.get_revolution(rev)
343
+ s += "Revolution %u (%u bits): " % (rev, len(b))
344
+ s += str(binascii.hexlify(b.tobytes())) + "\n"
345
+ b = self.bitarray[sum([x.nr_bits for x in self.revolutions]):]
346
+ s += "Tail (%u bits): " % (len(b))
347
+ s += str(binascii.hexlify(b.tobytes())) + "\n"
348
+ return s[:-1]
349
+
350
+
351
+ def get_revolution(self, nr) -> Tuple[bitarray, List[float]]:
352
+ start = sum([x.nr_bits for x in self.revolutions[:nr]])
353
+ end = start + self.revolutions[nr].nr_bits
354
+ return self.bitarray[start:end], self.timearray[start:end]
355
+
356
+
357
+ def get_all_data(self) -> Tuple[bitarray, List[float]]:
358
+ return self.bitarray, self.timearray
359
+
360
+
361
+ def import_flux_data(self, data) -> None:
362
+
363
+ flux = data.flux()
364
+ freq = flux.sample_freq
365
+ if self.time_per_rev is not None:
366
+ # Adjust the raw flux to have the expected time per revolution.
367
+ freq *= flux.time_per_rev / self.time_per_rev
368
+
369
+ clock = self.clock
370
+ clock_min = self.clock * (1 - self.clock_max_adj)
371
+ clock_max = self.clock * (1 + self.clock_max_adj)
372
+
373
+ index_iter = it.chain(iter(map(lambda x: x/freq, flux.index_list)),
374
+ [float('inf')])
375
+
376
+ if self.lowpass_thresh is not None:
377
+ # Short fluxes below the threshold are merged together, and with
378
+ # adjacent fluxes. The scenario discussed in issue #325 is that
379
+ # a long flux (>=10us) gets a single short flux-reversal pair
380
+ # inserted in the middle by drive electronics. So all we really
381
+ # need to filter in practice is:
382
+ # (Long),(Short),(Long) -> (Long+Short+Long)
383
+ # Sequences of short pulses have not been seen in real disk dumps.
384
+ flux_list, thresh = [0.0], self.lowpass_thresh
385
+ flux_iter = iter(flux.list)
386
+ while (x := next(flux_iter, None)) is not None:
387
+ if x/freq <= thresh:
388
+ y = next(flux_iter, 0.0)
389
+ # Merge a group-of-three centred on shortest of {x, y}.
390
+ if y <= x:
391
+ flux_list.append(x + y + next(flux_iter, 0.0))
392
+ else:
393
+ flux_list[-1] += x + y
394
+ else:
395
+ flux_list.append(x)
396
+ else:
397
+ flux_list = flux.list
398
+
399
+ # Make sure there's enough time in the flux list to cover all
400
+ # revolutions by appending a "large enough" final flux value.
401
+ tail = max(0, sum(flux.index_list) - sum(flux_list) + clock*freq*2)
402
+ flux_iter = it.chain(flux_list, [tail])
403
+
404
+ revolutions: List[int] = []
405
+ try:
406
+ optimised.flux_to_bitcells(
407
+ self.bitarray, self.timearray, revolutions,
408
+ index_iter, flux_iter,
409
+ freq, clock, clock_min, clock_max,
410
+ self.pll_period_adj, self.pll_phase_adj)
411
+ except AttributeError:
412
+ flux_to_bitcells(
413
+ self.bitarray, self.timearray, revolutions,
414
+ index_iter, flux_iter,
415
+ freq, clock, clock_min, clock_max,
416
+ self.pll_period_adj, self.pll_phase_adj)
417
+
418
+ hardsector_bits = None
419
+ for i, nr_bits in enumerate(revolutions):
420
+ if flux.sector_list is not None:
421
+ start = sum(revolutions[:i])
422
+ cell_sum = it.accumulate(self.timearray[start:start+nr_bits])
423
+ hardsector_bits = []
424
+ for sector_end in it.accumulate(map(lambda x: x/freq,
425
+ flux.sector_list[i])):
426
+ nbits = 0
427
+ try:
428
+ while next(cell_sum) < sector_end:
429
+ nbits += 1
430
+ nbits += 1
431
+ except StopIteration:
432
+ pass
433
+ hardsector_bits.append(nbits)
434
+ self.revolutions.append(PLLRevolution(nr_bits, hardsector_bits))
435
+
436
+
437
+ def flux_to_bitcells(bit_array, time_array, revolutions,
438
+ index_iter, flux_iter,
439
+ freq, clock_centre, clock_min, clock_max,
440
+ pll_period_adj, pll_phase_adj) -> None:
441
+
442
+ nbits = 0
443
+ ticks = 0.0
444
+ clock = clock_centre
445
+ to_index = next(index_iter)
446
+
447
+ for x in flux_iter:
448
+
449
+ # Gather enough ticks to generate at least one bitcell.
450
+ ticks += x / freq
451
+ if ticks < clock/2:
452
+ continue
453
+
454
+ # Clock out zero or more 0s, followed by a 1.
455
+ zeros = 0
456
+ while True:
457
+ ticks -= clock
458
+ if ticks < clock/2:
459
+ break
460
+ zeros += 1
461
+ bit_array.append(False)
462
+ bit_array.append(True)
463
+
464
+ # PLL: Adjust clock window position according to phase mismatch.
465
+ new_ticks = ticks * (1 - pll_phase_adj)
466
+
467
+ # Distribute the clock adjustment across all bits we just emitted.
468
+ _clock = clock + (ticks - new_ticks) / (zeros + 1)
469
+ for i in range(zeros + 1):
470
+ # Check if we cross the index mark.
471
+ to_index -= _clock
472
+ if to_index < 0:
473
+ revolutions.append(nbits)
474
+ nbits = 0
475
+ to_index += next(index_iter)
476
+ # Emit bit time.
477
+ nbits += 1
478
+ time_array.append(_clock)
479
+
480
+ # PLL: Adjust clock frequency according to phase mismatch.
481
+ if zeros <= 3:
482
+ # In sync: adjust clock by a fraction of the phase mismatch.
483
+ clock += ticks * pll_period_adj
484
+ else:
485
+ # Out of sync: adjust clock towards centre.
486
+ clock += (clock_centre - clock) * pll_period_adj
487
+ # Clamp the clock's adjustment range.
488
+ clock = min(max(clock, clock_min), clock_max)
489
+
490
+ ticks = new_ticks
491
+
492
+ # Local variables:
493
+ # python-indent: 4
494
+ # End: