badline 0.16.1 → 0.18.0

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 (256) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +213 -0
  3. data/CLAUDE.md +11 -7
  4. data/CONTRIBUTING.md +7 -1
  5. data/README.md +265 -26
  6. data/doc/media.md +5 -2
  7. data/doc/pinned-behaviour.md +189 -0
  8. data/doc/snapshot-format.md +616 -74
  9. data/doc/snapshots.md +192 -24
  10. data/exe/badline-ruby +5 -0
  11. data/lib/badline/audio/terminal.rb +34 -2
  12. data/lib/badline/c128/keyed_state.rb +2 -0
  13. data/lib/badline/c128.rb +1 -0
  14. data/lib/badline/c64/keyed_state.rb +2 -0
  15. data/lib/badline/c64.rb +1 -0
  16. data/lib/badline/cbm2/acia.rb +10 -0
  17. data/lib/badline/cbm2/bus.rb +263 -0
  18. data/lib/badline/cbm2/cpu.rb +58 -0
  19. data/lib/badline/cbm2/crtc_video.rb +133 -0
  20. data/lib/badline/cbm2/ieee_drive.rb +18 -0
  21. data/lib/badline/cbm2/ieee_layout.rb +69 -0
  22. data/lib/badline/cbm2/ieee_ports.rb +104 -0
  23. data/lib/badline/cbm2/ieee_traps.rb +81 -0
  24. data/lib/badline/cbm2/keyboard_ports.rb +60 -0
  25. data/lib/badline/cbm2/keyed_modules.rb +67 -0
  26. data/lib/badline/cbm2/keyed_state.rb +95 -0
  27. data/lib/badline/cbm2/model.rb +44 -0
  28. data/lib/badline/cbm2/p500/control_ports.rb +80 -0
  29. data/lib/badline/cbm2/p500/ieee_layout.rb +41 -0
  30. data/lib/badline/cbm2/p500/keyed_state.rb +85 -0
  31. data/lib/badline/cbm2/p500/model.rb +39 -0
  32. data/lib/badline/cbm2/p500/pages.rb +57 -0
  33. data/lib/badline/cbm2/p500.rb +264 -0
  34. data/lib/badline/cbm2/sid_page.rb +19 -0
  35. data/lib/badline/cbm2.rb +270 -0
  36. data/lib/badline/{plus4 → chips}/acia/frames.rb +1 -1
  37. data/lib/badline/chips/acia/keyed_state.rb +47 -0
  38. data/lib/badline/chips/acia.rb +307 -0
  39. data/lib/badline/chips/cia/timer.rb +2 -2
  40. data/lib/badline/chips/cpu/instruction_set.rb +1 -1
  41. data/lib/badline/chips/crtc.rb +3 -2
  42. data/lib/badline/chips/m6809/arithmetic.rb +140 -0
  43. data/lib/badline/chips/m6809/bus_steps.rb +223 -0
  44. data/lib/badline/chips/m6809/core.rb +149 -0
  45. data/lib/badline/chips/m6809/indexed.rb +82 -0
  46. data/lib/badline/chips/m6809/interrupts.rb +159 -0
  47. data/lib/badline/chips/m6809/keyed_state.rb +147 -0
  48. data/lib/badline/chips/m6809/opcodes.rb +159 -0
  49. data/lib/badline/chips/m6809/operations.rb +258 -0
  50. data/lib/badline/chips/m6809/plans.rb +143 -0
  51. data/lib/badline/chips/m6809/registers.rb +82 -0
  52. data/lib/badline/chips/m6809/stack.rb +140 -0
  53. data/lib/badline/chips/m6809.rb +13 -0
  54. data/lib/badline/chips/riot/fast_forward.rb +72 -0
  55. data/lib/badline/chips/riot/keyed_parts.rb +69 -0
  56. data/lib/badline/chips/riot/timer.rb +91 -0
  57. data/lib/badline/chips/riot.rb +162 -0
  58. data/lib/badline/chips/rriot/keyed_state.rb +30 -0
  59. data/lib/badline/chips/rriot.rb +134 -0
  60. data/lib/badline/chips/sid/filter.rb +4 -4
  61. data/lib/badline/chips/sid/waveform.rb +6 -6
  62. data/lib/badline/chips/tpi/keyed_state.rb +38 -0
  63. data/lib/badline/chips/tpi.rb +233 -0
  64. data/lib/badline/chips/vic/bank.rb +60 -5
  65. data/lib/badline/chips/vic/sprite.rb +4 -3
  66. data/lib/badline/chips/vic_i/keyed_parts.rb +7 -2
  67. data/lib/badline/chips/vic_i.rb +84 -21
  68. data/lib/badline/data_path.rb +3 -3
  69. data/lib/badline/drives/datasette/keyed_state.rb +9 -1
  70. data/lib/badline/drives/datasette.rb +27 -3
  71. data/lib/badline/drives/ieee_models.rb +54 -0
  72. data/lib/badline/drives/model1001.rb +37 -0
  73. data/lib/badline/drives/model2031.rb +6 -0
  74. data/lib/badline/drives/model4040/disk.rb +62 -0
  75. data/lib/badline/drives/model4040/ip_bus.rb +46 -0
  76. data/lib/badline/drives/model4040/keyed_state.rb +57 -0
  77. data/lib/badline/drives/model4040/mechanism.rb +192 -0
  78. data/lib/badline/drives/model4040/parts.rb +20 -0
  79. data/lib/badline/drives/model4040.rb +179 -0
  80. data/lib/badline/drives/model8050/board.rb +77 -0
  81. data/lib/badline/drives/model8050/disk.rb +163 -0
  82. data/lib/badline/drives/model8050/dos_board.rb +91 -0
  83. data/lib/badline/drives/model8050/fdc_bus.rb +102 -0
  84. data/lib/badline/drives/model8050/idle.rb +181 -0
  85. data/lib/badline/drives/model8050/ieee_port.rb +147 -0
  86. data/lib/badline/drives/model8050/ip_bus.rb +93 -0
  87. data/lib/badline/drives/model8050/keyed_state.rb +163 -0
  88. data/lib/badline/drives/model8050/mechanism.rb +230 -0
  89. data/lib/badline/drives/model8050/parts.rb +37 -0
  90. data/lib/badline/drives/model8050.rb +131 -0
  91. data/lib/badline/drives/model9060/idle.rb +149 -0
  92. data/lib/badline/drives/model9060/keyed_state.rb +123 -0
  93. data/lib/badline/drives/model9060/parts.rb +98 -0
  94. data/lib/badline/drives/model9060/sasi_bus.rb +88 -0
  95. data/lib/badline/drives/model9060/sasi_commands.rb +168 -0
  96. data/lib/badline/drives/model9060/sasi_target.rb +161 -0
  97. data/lib/badline/drives/model9060.rb +195 -0
  98. data/lib/badline/frontend/app.rb +18 -3
  99. data/lib/badline/frontend/boot.rb +4 -3
  100. data/lib/badline/frontend/controls.rb +107 -15
  101. data/lib/badline/frontend/gamepads.rb +3 -3
  102. data/lib/badline/frontend/keys.rb +97 -2
  103. data/lib/badline/frontend/kim1_panel.rb +208 -0
  104. data/lib/badline/frontend/media_slots.rb +8 -1
  105. data/lib/badline/frontend/menu/drive_types.rb +56 -0
  106. data/lib/badline/frontend/menu/menu_dialogs.rb +61 -14
  107. data/lib/badline/frontend/menu/menu_media.rb +47 -14
  108. data/lib/badline/frontend/menu/menu_pages.rb +113 -4
  109. data/lib/badline/frontend/menu/menu_ports.rb +77 -4
  110. data/lib/badline/frontend/menu/pause_menu.rb +6 -2
  111. data/lib/badline/frontend/pet_keyboard.rb +45 -7
  112. data/lib/badline/frontend/screen.rb +1 -1
  113. data/lib/badline/frontend/screens.rb +40 -2
  114. data/lib/badline/frontend/teletype_pane.rb +152 -0
  115. data/lib/badline/frontend/timeline.rb +49 -11
  116. data/lib/badline/frontend.rb +3 -0
  117. data/lib/badline/kernal_trap/dos/disk_commands.rb +80 -0
  118. data/lib/badline/kernal_trap/dos/status.rb +3 -0
  119. data/lib/badline/kernal_trap/dos.rb +18 -5
  120. data/lib/badline/kernal_trap/ieee.rb +4 -2
  121. data/lib/badline/kernal_trap/ieee_layout.rb +95 -43
  122. data/lib/badline/kim1/bus.rb +117 -0
  123. data/lib/badline/kim1/cpu.rb +25 -0
  124. data/lib/badline/kim1/host_terminal.rb +74 -0
  125. data/lib/badline/kim1/key_entry.rb +74 -0
  126. data/lib/badline/kim1/keyed_modules.rb +53 -0
  127. data/lib/badline/kim1/keyed_state.rb +133 -0
  128. data/lib/badline/kim1/panel.rb +176 -0
  129. data/lib/badline/kim1/rriots.rb +28 -0
  130. data/lib/badline/kim1/sound.rb +29 -0
  131. data/lib/badline/kim1/tape.rb +116 -0
  132. data/lib/badline/kim1/teletype.rb +193 -0
  133. data/lib/badline/kim1.rb +190 -0
  134. data/lib/badline/machine/ieee_drives.rb +171 -0
  135. data/lib/badline/machine/keyboard_buffer.rb +20 -3
  136. data/lib/badline/machine/line_sound.rb +122 -0
  137. data/lib/badline/machine/settling.rb +4 -0
  138. data/lib/badline/machine.rb +62 -14
  139. data/lib/badline/media/cbm2_media.rb +29 -0
  140. data/lib/badline/media/extensions.rb +8 -4
  141. data/lib/badline/media/kim1_media.rb +150 -0
  142. data/lib/badline/media/pet_media.rb +3 -1
  143. data/lib/badline/media/program.rb +6 -3
  144. data/lib/badline/media/true_drive.rb +51 -25
  145. data/lib/badline/media.rb +23 -8
  146. data/lib/badline/options/family.rb +108 -6
  147. data/lib/badline/options/help.rb +58 -12
  148. data/lib/badline/options/table.rb +60 -14
  149. data/lib/badline/options/validation.rb +71 -11
  150. data/lib/badline/options.rb +46 -13
  151. data/lib/badline/pet/bus.rb +16 -2
  152. data/lib/badline/pet/cassettes.rb +22 -1
  153. data/lib/badline/pet/ieee_drive.rb +5 -64
  154. data/lib/badline/pet/ieee_ports.rb +7 -0
  155. data/lib/badline/pet/keyboard_ports.rb +9 -2
  156. data/lib/badline/pet/keyed_state.rb +4 -0
  157. data/lib/badline/pet/model.rb +11 -2
  158. data/lib/badline/pet/rom_sets.rb +1 -0
  159. data/lib/badline/pet/sound.rb +5 -105
  160. data/lib/badline/pet/superpet/acia.rb +12 -0
  161. data/lib/badline/pet/superpet/board.rb +221 -0
  162. data/lib/badline/pet/superpet/dongle.rb +81 -0
  163. data/lib/badline/pet/superpet/keyed_modules.rb +63 -0
  164. data/lib/badline/pet/superpet/keyed_parts.rb +76 -0
  165. data/lib/badline/pet/superpet/m6809.rb +32 -0
  166. data/lib/badline/pet/superpet/waterloo_traps.rb +199 -0
  167. data/lib/badline/pet/superpet.rb +216 -0
  168. data/lib/badline/pet.rb +33 -6
  169. data/lib/badline/plus4/acia.rb +4 -298
  170. data/lib/badline/plus4/keyed_parts.rb +0 -44
  171. data/lib/badline/plus4/keyed_state.rb +2 -0
  172. data/lib/badline/plus4.rb +2 -0
  173. data/lib/badline/roms/1001/fdc-251257-02a.rom +0 -0
  174. data/lib/badline/roms/4040/dos-1.2.rom +0 -0
  175. data/lib/badline/roms/4040/dos-2.1-rev.rom +0 -0
  176. data/lib/badline/roms/4040/dos-2.1.rom +0 -0
  177. data/lib/badline/roms/4040/fdc-901466-02.rom +0 -0
  178. data/lib/badline/roms/4040/fdc-901466-04.rom +0 -0
  179. data/lib/badline/roms/8050/dos-2.7.rom +0 -0
  180. data/lib/badline/roms/8050/fdc-901869-01.rom +0 -0
  181. data/lib/badline/roms/8050/fdc-901885-04.rom +0 -0
  182. data/lib/badline/roms/9060/dos-3.0.rom +0 -0
  183. data/lib/badline/roms/9060/sasi-300515-revb.rom +0 -0
  184. data/lib/badline/roms/README +277 -0
  185. data/lib/badline/roms/cbm2/basic128-02b.rom +0 -0
  186. data/lib/badline/roms/cbm2/basic128.rom +0 -0
  187. data/lib/badline/roms/cbm2/basic256.rom +0 -0
  188. data/lib/badline/roms/cbm2/characters-600.rom +0 -0
  189. data/lib/badline/roms/cbm2/characters-700.rom +0 -0
  190. data/lib/badline/roms/cbm2/kernal-03b.rom +0 -0
  191. data/lib/badline/roms/cbm2/kernal-04a.rom +0 -0
  192. data/lib/badline/roms/cbm2/p500-basic.rom +0 -0
  193. data/lib/badline/roms/cbm2/p500-kernal.rom +0 -0
  194. data/lib/badline/roms/kim1/6530-002.rom +0 -0
  195. data/lib/badline/roms/kim1/6530-003.rom +0 -0
  196. data/lib/badline/roms/pet/characters-superpet.rom +0 -0
  197. data/lib/badline/roms/pet/superpet-a000.rom +0 -0
  198. data/lib/badline/roms/pet/superpet-c000.rom +0 -0
  199. data/lib/badline/roms/pet/superpet-e000-50hz.rom +0 -0
  200. data/lib/badline/roms/pet/superpet-e000.rom +0 -0
  201. data/lib/badline/snapshot/cbm2_setup.rb +53 -0
  202. data/lib/badline/snapshot/container.rb +9 -2
  203. data/lib/badline/snapshot/field_doc/markdown.rb +4 -2
  204. data/lib/badline/snapshot/field_doc.rb +59 -3
  205. data/lib/badline/snapshot/image.rb +65 -9
  206. data/lib/badline/snapshot/kim1_setup.rb +28 -0
  207. data/lib/badline/snapshot/pet_setup.rb +27 -11
  208. data/lib/badline/snapshot/storage_setup.rb +4 -1
  209. data/lib/badline/snapshot/vic20_setup.rb +11 -7
  210. data/lib/badline/snapshot/vice/cbm2.rb +93 -0
  211. data/lib/badline/snapshot/vice/cbm2_acia.rb +75 -0
  212. data/lib/badline/snapshot/vice/cbm2_crtc.rb +127 -0
  213. data/lib/badline/snapshot/vice/cbm2_mains.rb +49 -0
  214. data/lib/badline/snapshot/vice/cbm2_mem.rb +75 -0
  215. data/lib/badline/snapshot/vice/cbm2_restore.rb +204 -0
  216. data/lib/badline/snapshot/vice/cbm2_tpi.rb +58 -0
  217. data/lib/badline/snapshot/vice/cias.rb +13 -9
  218. data/lib/badline/snapshot/vice/p500.rb +96 -0
  219. data/lib/badline/snapshot/vice/p500_restore.rb +130 -0
  220. data/lib/badline/snapshot/vice/p500_vicii.rb +166 -0
  221. data/lib/badline/snapshot/vice/pet.rb +20 -10
  222. data/lib/badline/snapshot/vice/pet_cpu.rb +42 -5
  223. data/lib/badline/snapshot/vice/pet_crtc.rb +25 -17
  224. data/lib/badline/snapshot/vice/pet_drive.rb +180 -0
  225. data/lib/badline/snapshot/vice/pet_drive8050.rb +336 -0
  226. data/lib/badline/snapshot/vice/pet_mem.rb +49 -5
  227. data/lib/badline/snapshot/vice/pet_restore.rb +100 -7
  228. data/lib/badline/snapshot/vice/pet_via.rb +6 -5
  229. data/lib/badline/snapshot/vice/sid_registers.rb +2 -1
  230. data/lib/badline/snapshot/vice/vicii_import.rb +10 -6
  231. data/lib/badline/snapshot/vice.rb +3 -0
  232. data/lib/badline/snapshot.rb +15 -3
  233. data/lib/badline/status.rb +30 -30
  234. data/lib/badline/storage/crt_file.rb +7 -1
  235. data/lib/badline/storage/d64_image.rb +7 -0
  236. data/lib/badline/storage/d67_image.rb +55 -0
  237. data/lib/badline/storage/d80_image.rb +17 -1
  238. data/lib/badline/storage/d81_image.rb +14 -0
  239. data/lib/badline/storage/d90_image.rb +180 -0
  240. data/lib/badline/storage/disk_image/bam.rb +18 -3
  241. data/lib/badline/storage/disk_image/formatting.rb +69 -0
  242. data/lib/badline/storage/disk_image/writing.rb +37 -2
  243. data/lib/badline/storage/disk_image.rb +2 -0
  244. data/lib/badline/storage/kim_tape.rb +254 -0
  245. data/lib/badline/storage/papertape.rb +90 -0
  246. data/lib/badline/storage.rb +5 -0
  247. data/lib/badline/support/cbm2_checkpoint.rb +48 -0
  248. data/lib/badline/support/kim1_checkpoint.rb +41 -0
  249. data/lib/badline/support/p500_checkpoint.rb +35 -0
  250. data/lib/badline/support/superpet_checkpoint.rb +35 -0
  251. data/lib/badline/version.rb +1 -1
  252. data/lib/badline/vic20/keyed_modules.rb +6 -3
  253. data/lib/badline/vic20/keyed_state.rb +6 -4
  254. data/lib/badline/vic20.rb +26 -12
  255. data/lib/badline.rb +2 -0
  256. metadata +148 -2
@@ -71,17 +71,32 @@ module Badline
71
71
  end
72
72
 
73
73
  # A chain's first block goes on the free track nearest the
74
- # directory. Each block after it goes the interleave further round
75
- # the same track, or on the next track out once that one is full.
74
+ # directory, from its sector 0. Each block after it looks from the
75
+ # interleave further round the track (#next_sector), on the same
76
+ # track or, once that one is full, from the same sector on the next
77
+ # track out.
76
78
  def next_block(track = nil, sector = nil)
77
79
  candidates = track ? [track, *tracks_after(track)] : allocation_order
80
+ start = track ? next_sector(track, sector) : 0
78
81
  candidates.each do |candidate|
79
- free = free_sector(candidate, candidate == track ? sector + interleave : 0)
82
+ free = free_sector(candidate, start)
80
83
  return [candidate, free] if free
81
84
  end
82
85
  nil
83
86
  end
84
87
 
88
+ # The interleave on from the sector. Past the track's last sector
89
+ # it comes round again one short, but never below sector 0, as
90
+ # the 2031, 8050, 8250 and 1581 DOS do.
91
+ def next_sector(track, sector)
92
+ start = sector + interleave
93
+ count = sectors_in(track)
94
+ return start if start < count
95
+
96
+ start -= count
97
+ start.positive? ? start - 1 : start
98
+ end
99
+
85
100
  def free_sector(track, first)
86
101
  count = sectors_in(track)
87
102
  (0...count).map { |i| (first + i) % count }.find { |s| block_free?(track, s) }
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Storage
5
+ class DiskImage
6
+ # The DOS's N command. With an ID it formats the disk: every block of
7
+ # the tracks the BAM covers is cleared, and the header, the BAM and an
8
+ # empty directory are written. Without one it only clears the disk:
9
+ # the header takes the new name and keeps its ID and whatever else it
10
+ # held, the BAM marks every block free, and the directory's first
11
+ # block is emptied. Either way the header, the BAM's blocks and the
12
+ # first directory block stay in use. Each format says where these go.
13
+ module Formatting
14
+ # The disk's ID, as the header holds it.
15
+ def disk_id = sector_at(*header_block)[header_name + Catalog::HEADER_ID, 2]
16
+
17
+ # Formats the disk under the name, or with no ID clears it.
18
+ def format(name, id = nil)
19
+ update_image do
20
+ full = !id.nil?
21
+ put_sector(*header_block, []) if full
22
+ id = (id || disk_id).first(2)
23
+ write_header(name, id)
24
+ clear_blocks if full
25
+ headers = bam_headers(id)
26
+ headers.each { |track, sector, bytes| write_at(track, sector, 0, bytes) }
27
+ put_sector(directory_track, directory_sector, [0, 0xff])
28
+ bam_tracks.each { |track| free_track(track) }
29
+ [header_block, *headers.map { |track, sector, _| [track, sector] }, [directory_track, directory_sector]]
30
+ .each { |block| mark(*block, false) }
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ # Every block the BAM covers but the header.
37
+ def clear_blocks
38
+ bam_tracks.each do |track|
39
+ (0...sectors_in(track)).each do |sector|
40
+ put_sector(track, sector, []) unless [track, sector] == header_block
41
+ end
42
+ end
43
+ end
44
+
45
+ # The link to the next block, the format's letter, the name, the ID
46
+ # and the DOS type, each field after the name set off with shifted
47
+ # spaces.
48
+ def write_header(name, id)
49
+ write_at(*header_block, 0, [*header_link, dos_type.getbyte(1), 0])
50
+ write_at(*header_block, header_name,
51
+ encode_name(name) + [NAME_PADDING, NAME_PADDING] + id + [NAME_PADDING] +
52
+ dos_type.bytes + Array.new(header_tail, NAME_PADDING))
53
+ end
54
+
55
+ # A free track counts its sectors and sets a bit for each.
56
+ def free_track(track)
57
+ count = sectors_in(track)
58
+ bits = (1 << count) - 1
59
+ @bytes[count_offset(track)] = count
60
+ bitmap_size.times { |i| @bytes[bitmap_offset(track) + i] = (bits >> (8 * i)) & 0xff }
61
+ end
62
+
63
+ def write_at(track, sector, offset, bytes)
64
+ @bytes[sector_offset(track, sector) + offset, bytes.length] = bytes
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -51,6 +51,39 @@ module Badline
51
51
  files.length
52
52
  end
53
53
 
54
+ # Gives a file a new name in its directory entry. A missing file
55
+ # fails as FILE NOT FOUND, and a new name already on the disk as
56
+ # FILE EXISTS.
57
+ def rename(old_name, new_name)
58
+ entry = existing_entry(old_name)
59
+ raise WriteError, WriteError::FILE_EXISTS if entry_named(new_name)
60
+
61
+ update_image { @bytes[entry[:offset] + 5, Directory::NAME_LENGTH] = encode_name(new_name) }
62
+ end
63
+
64
+ # Writes a new file holding the files named, one after another, as
65
+ # the type of the first. A missing file fails as FILE NOT FOUND, and
66
+ # a new name already on the disk as FILE EXISTS.
67
+ def copy(new_name, names)
68
+ sources = names.map { |name| existing_entry(name) }
69
+ raise WriteError, WriteError::FILE_EXISTS if entry_named(new_name)
70
+
71
+ bytes = sources.flat_map { |entry| read_chain(entry[:track], entry[:sector]) }
72
+ update_image { add_file(new_name, bytes, sources.first[:type]) }
73
+ end
74
+
75
+ # Adds the files named to the end of a SEQ file. A missing file fails
76
+ # as FILE NOT FOUND, and a file of another type as FILE TYPE
77
+ # MISMATCH.
78
+ def concat(name, names)
79
+ entry = existing_entry(name)
80
+ sources = names.map { |source| existing_entry(source) }
81
+ raise WriteError, WriteError::FILE_TYPE_MISMATCH unless entry[:type] == :seq
82
+
83
+ bytes = [entry, *sources].flat_map { |source| read_chain(source[:track], source[:sector]) }
84
+ update_image { rewrite(entry, bytes, :seq) }
85
+ end
86
+
54
87
  # Raw block access for the DOS `U2` command. A block that the error
55
88
  # table marks bad reads cleanly once written.
56
89
  def write_block(track, sector, data)
@@ -81,6 +114,8 @@ module Badline
81
114
 
82
115
  private
83
116
 
117
+ def existing_entry(name) = entry_named(name) || raise(WriteError, WriteError::FILE_NOT_FOUND)
118
+
84
119
  def update_image
85
120
  raise WriteError, WriteError::WRITE_PROTECT_ON unless writable?
86
121
 
@@ -116,8 +151,8 @@ module Badline
116
151
  end
117
152
 
118
153
  def put_sector(track, sector, data)
119
- block = data.first(SECTOR_SIZE)
120
- @bytes[sector_offset(track, sector), SECTOR_SIZE] = block + Array.new(SECTOR_SIZE - block.length, 0)
154
+ offset = sector_offset(track, sector)
155
+ SECTOR_SIZE.times { |i| @bytes[offset + i] = data[i] || 0 }
121
156
  @errors[track_offset(track) + sector] = 1 if @errors
122
157
  end
123
158
  end
@@ -3,6 +3,7 @@
3
3
  require "badline/storage/disk_image/bam"
4
4
  require "badline/storage/disk_image/catalog"
5
5
  require "badline/storage/disk_image/directory"
6
+ require "badline/storage/disk_image/formatting"
6
7
  require "badline/storage/disk_image/writing"
7
8
 
8
9
  module Badline
@@ -15,6 +16,7 @@ module Badline
15
16
  include Bam
16
17
  include Catalog
17
18
  include Directory
19
+ include Formatting
18
20
  include Writing
19
21
 
20
22
  SECTOR_SIZE = 256
@@ -0,0 +1,254 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Storage
5
+ # A KIM-1 cassette, held as the times its signal crosses zero, in
6
+ # microseconds from the start of the tape, which are the KIM-1's
7
+ # cycles. The KIM-1 writes the tape as a square wave on U2's PB7, each
8
+ # bit 3,700 Hz then 2,400 Hz, and reads it back through a 565 PLL and a
9
+ # 311 comparator, which hold PB7 high through the 2,400 Hz tone and low
10
+ # through the 3,700 Hz one (User Manual, appendix E). edges gives the
11
+ # times PB7 changes as the tape plays.
12
+ #
13
+ # A tape reads from a .wav file, 8-bit or 16-bit PCM, its first
14
+ # channel only, and writes one back as a square wave.
15
+ class KIMTape
16
+ class FormatError < StandardError; end
17
+
18
+ # A period longer than this many microseconds is the 2,400 Hz tone:
19
+ # the split between the tones, at the geometric mean of the two.
20
+ SPLIT = 336
21
+
22
+ # The time constants, in seconds, of the detector's DC level and of
23
+ # its envelope, which sets the hysteresis.
24
+ DC_TIME = 0.002
25
+ ENVELOPE_TIME = 0.005
26
+
27
+ # The signal crosses the DC level once it swings this fraction of the
28
+ # envelope past it, and not for an envelope smaller than SILENCE, of
29
+ # full scale.
30
+ HYSTERESIS = 0.3
31
+ SILENCE = 0.03
32
+
33
+ # A gap this many microseconds long between crossings plays as
34
+ # silence in the .wav a tape writes.
35
+ GAP = 2_000
36
+
37
+ # The file the tape reads from and writes back to, or "" for none.
38
+ attr_reader :path, :crossings
39
+
40
+ # The tape in the .wav file at `path`.
41
+ def self.read(path) = new(path, crossings: from_wav(File.binread(path).bytes))
42
+
43
+ # A blank tape at `path`, written to the host file.
44
+ def self.create(path)
45
+ tape = new(path)
46
+ tape.save
47
+ tape
48
+ end
49
+
50
+ # The zero crossings of the .wav file in `bytes`.
51
+ def self.from_wav(bytes)
52
+ raise FormatError, "Not a RIFF WAVE file" unless bytes[0, 4] == "RIFF".bytes && bytes[8, 4] == "WAVE".bytes
53
+
54
+ format = nil
55
+ pos = 12
56
+ while pos + 8 <= bytes.length
57
+ id = bytes[pos, 4].pack("C*")
58
+ size = little_endian(bytes, pos + 4, 4)
59
+ format = bytes[pos + 8, 16] if id == "fmt "
60
+ return Detector.new(Format.new(format)).crossings(bytes, pos + 8, size) if id == "data" && format
61
+
62
+ pos += 8 + size + (size & 1)
63
+ end
64
+ raise FormatError, "No PCM data in the WAVE file"
65
+ end
66
+
67
+ def self.little_endian(bytes, pos, count)
68
+ value = 0
69
+ count.times { |i| value |= bytes[pos + i].to_i << (8 * i) }
70
+ value
71
+ end
72
+
73
+ def initialize(path = "", crossings: [])
74
+ @path = path
75
+ @crossings = crossings
76
+ end
77
+
78
+ # The time of the last crossing.
79
+ def duration = @crossings.empty? ? 0 : @crossings.last
80
+
81
+ # Erases the tape from `time` on, as a deck recording from there
82
+ # does.
83
+ def erase_from(time)
84
+ @crossings.pop while !@crossings.empty? && @crossings.last >= time
85
+ end
86
+
87
+ # Records a crossing at `time`, after the last one.
88
+ def record(time)
89
+ @crossings << time
90
+ end
91
+
92
+ # The times PB7 changes as the tape plays, from high at its start: to
93
+ # low where a period is the 3,700 Hz tone and back to high where one
94
+ # is the 2,400 Hz tone. Each period runs over two crossings, and two
95
+ # in a row must agree before PB7 follows, so that one crossing out of
96
+ # place doesn't split a bit. The period that straddles the change
97
+ # lies between the tones, so PB7 changes half way through it.
98
+ def edges
99
+ crossings = @crossings
100
+ edges = []
101
+ high = true
102
+ disagree = 0
103
+ i = 2
104
+ while i < crossings.length
105
+ low_tone = crossings[i] - crossings[i - 2] > SPLIT
106
+ if low_tone == high
107
+ disagree = 0
108
+ else
109
+ disagree += 1
110
+ if disagree == 2
111
+ high = low_tone
112
+ edges << ((crossings[i - 3] + crossings[i - 2]) / 2)
113
+ disagree = 0
114
+ end
115
+ end
116
+ i += 1
117
+ end
118
+ edges
119
+ end
120
+
121
+ # Writes the tape back to its host file.
122
+ def save
123
+ write_wav(@path)
124
+ end
125
+
126
+ # Writes the tape to `path` as a .wav file, a square wave of `bits`
127
+ # (8 or 16) at `rate` samples a second.
128
+ def write_wav(path, rate: 44_100, bits: 8)
129
+ samples = square_wave(rate, bits == 8 ? 96 : 24_000)
130
+ bytes = bits == 8 ? samples.map { |sample| sample + 128 } : samples.flat_map { |s| [s & 0xff, (s >> 8) & 0xff] }
131
+ File.binwrite(path, (wav_header(rate, bits, bytes.length) + bytes).pack("C*"))
132
+ end
133
+
134
+ private
135
+
136
+ # The tape as samples of `amplitude`: silent up to the first
137
+ # crossing, after a GAP and at the end, and otherwise each crossing
138
+ # turning the wave over.
139
+ def square_wave(rate, amplitude)
140
+ samples = []
141
+ crossings = @crossings
142
+ count = crossings.empty? ? 0 : (((crossings.last + GAP) * rate) / 1_000_000) + 1
143
+ index = 0
144
+ level = -amplitude
145
+ count.times do |n|
146
+ time = (n * 1_000_000) / rate
147
+ while index < crossings.length && crossings[index] <= time
148
+ level = -level
149
+ index += 1
150
+ end
151
+ samples << (index.zero? || time - crossings[index - 1] > GAP ? 0 : level)
152
+ end
153
+ samples
154
+ end
155
+
156
+ def wav_header(rate, bits, size)
157
+ bytes_per_sample = bits / 8
158
+ "RIFF".bytes + le(36 + size, 4) + "WAVEfmt ".bytes + le(16, 4) + le(1, 2) + le(1, 2) +
159
+ le(rate, 4) + le(rate * bytes_per_sample, 4) + le(bytes_per_sample, 2) + le(bits, 2) +
160
+ "data".bytes + le(size, 4)
161
+ end
162
+
163
+ def le(value, count) = Array.new(count) { |i| (value >> (8 * i)) & 0xff }
164
+
165
+ # A WAVE file's fmt chunk: PCM, 8-bit unsigned or 16-bit signed.
166
+ class Format
167
+ attr_reader :channels, :rate, :bits
168
+
169
+ def initialize(chunk)
170
+ tag = KIMTape.little_endian(chunk, 0, 2)
171
+ @channels = KIMTape.little_endian(chunk, 2, 2)
172
+ @rate = KIMTape.little_endian(chunk, 4, 4)
173
+ @bits = KIMTape.little_endian(chunk, 14, 2)
174
+ return if [1, 0xfffe].include?(tag) && [8, 16].include?(@bits) && @channels.positive?
175
+
176
+ raise FormatError, "Only 8-bit and 16-bit PCM WAVE files are supported"
177
+ end
178
+
179
+ def frame_size = @channels * @bits / 8
180
+
181
+ # The first channel's sample at `pos`, from -1 to 1.
182
+ def level_at(bytes, pos)
183
+ return (bytes[pos] - 128) / 128.0 if @bits == 8
184
+
185
+ value = bytes[pos] | (bytes[pos + 1] << 8)
186
+ (value >= 0x8000 ? value - 0x10000 : value) / 32_768.0
187
+ end
188
+ end
189
+
190
+ # Finds where a recording crosses zero. A cassette's signal wanders
191
+ # off its centre and fades, so the detector follows its DC level and
192
+ # its envelope, and counts a crossing only once the signal swings past
193
+ # the DC level by HYSTERESIS of the envelope, timing it where it
194
+ # crossed the DC level, between two samples.
195
+ class Detector
196
+ def initialize(format)
197
+ @format = format
198
+ rate = format.rate
199
+ @dc_step = 1.0 / (rate * DC_TIME)
200
+ @decay = Math.exp(-1.0 / (rate * ENVELOPE_TIME))
201
+ @scale = 1_000_000.0 / rate
202
+ @dc = @envelope = @previous = @rose = @fell = 0.0
203
+ @high = false
204
+ @found = []
205
+ end
206
+
207
+ # The crossings, in microseconds, of the samples in `bytes` from
208
+ # `pos`, `size` bytes of them.
209
+ def crossings(bytes, pos, size)
210
+ format = @format
211
+ frame = format.frame_size
212
+ last = [pos + size, bytes.length].min - frame
213
+ n = 0
214
+ while pos <= last
215
+ follow(format.level_at(bytes, pos), n)
216
+ pos += frame
217
+ n += 1
218
+ end
219
+ @found
220
+ end
221
+
222
+ private
223
+
224
+ # Takes the sample `sample`, the `index`th.
225
+ def follow(sample, index)
226
+ @dc += (sample - @dc) * @dc_step
227
+ level = sample - @dc
228
+ @envelope *= @decay
229
+ @envelope = level.abs if level.abs > @envelope
230
+ previous = @previous
231
+ if previous <= 0 && level.positive?
232
+ @rose = index - 1 + (previous / (previous - level))
233
+ elsif previous >= 0 && level.negative?
234
+ @fell = index - 1 + (previous / (previous - level))
235
+ end
236
+ @previous = level
237
+ swing(level) if @envelope > SILENCE
238
+ end
239
+
240
+ # Turns over at a `level` far enough past the DC level.
241
+ def swing(level)
242
+ swing = @envelope * HYSTERESIS
243
+ if !@high && level > swing
244
+ @high = true
245
+ @found << (@rose * @scale).round
246
+ elsif @high && level < -swing
247
+ @high = false
248
+ @found << (@fell * @scale).round
249
+ end
250
+ end
251
+ end
252
+ end
253
+ end
254
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Storage
5
+ # A MOS papertape, the .ptp or .pap the KIM-1 monitor's Q command
6
+ # punches and its L command reads (User Manual, appendix F). Each
7
+ # record is a semicolon, then in hex its count of data bytes, its
8
+ # address, high byte first, the bytes and a 16-bit checksum, the sum
9
+ # of every byte before it, high byte first. The last record has a
10
+ # count of 0, the number of records before it where the address goes,
11
+ # and its checksum, which the monitor checks without counting the
12
+ # records. What comes between records, a line's end, NULs
13
+ # and the XOFF that ends a dump, the monitor skips, as this does.
14
+ class Papertape
15
+ class FormatError < StandardError; end
16
+
17
+ # One record's data: its bytes and where they go.
18
+ class Record
19
+ attr_reader :address, :bytes
20
+
21
+ def initialize(address, bytes)
22
+ @address = address
23
+ @bytes = bytes
24
+ end
25
+ end
26
+
27
+ HEX = "0123456789ABCDEFabcdef"
28
+
29
+ # The Records, in the tape's order.
30
+ attr_reader :records
31
+
32
+ # The tape's text from its start to the last record's end, which a
33
+ # teletype sends for the L command.
34
+ attr_reader :text
35
+
36
+ def self.read(path) = new(File.binread(path))
37
+
38
+ def initialize(text)
39
+ @records = []
40
+ @text = text.b
41
+ @position = 0
42
+ read_records
43
+ end
44
+
45
+ # Where the first record goes.
46
+ def start_address = @records.first.address
47
+
48
+ private
49
+
50
+ def read_records
51
+ loop do
52
+ start = @text.index(";", @position)
53
+ raise FormatError, "no last record" if start.nil?
54
+
55
+ @position = start + 1
56
+ count = byte
57
+ address = word
58
+ data = Array.new(count) { byte }
59
+ check(count, address, data)
60
+ return finish if count.zero?
61
+
62
+ @records << Record.new(address, data)
63
+ end
64
+ end
65
+
66
+ def check(count, address, data)
67
+ sum = (count + (address >> 8) + (address & 0xff) + data.sum) & 0xffff
68
+ raise FormatError, "bad checksum in the record at $#{address.to_s(16).rjust(4, '0')}" unless word == sum
69
+ end
70
+
71
+ def finish
72
+ raise FormatError, "no records" if @records.empty?
73
+
74
+ @text = @text[0, @position]
75
+ end
76
+
77
+ def byte
78
+ digits = @text[@position, 2].to_s
79
+ unless digits.size == 2 && HEX.include?(digits[0]) && HEX.include?(digits[1])
80
+ raise FormatError, "not a hex byte at offset #{@position}"
81
+ end
82
+
83
+ @position += 2
84
+ digits.to_i(16)
85
+ end
86
+
87
+ def word = (byte << 8) | byte
88
+ end
89
+ end
90
+ end
@@ -5,13 +5,17 @@ require "badline/storage/p00"
5
5
  require "badline/storage/host_directory"
6
6
  require "badline/storage/disk_image"
7
7
  require "badline/storage/d64_image"
8
+ require "badline/storage/d67_image"
8
9
  require "badline/storage/d71_image"
9
10
  require "badline/storage/d81_image"
10
11
  require "badline/storage/d80_image"
11
12
  require "badline/storage/d82_image"
13
+ require "badline/storage/d90_image"
12
14
  require "badline/storage/g64_image"
13
15
  require "badline/storage/t64"
14
16
  require "badline/storage/tap"
17
+ require "badline/storage/papertape"
18
+ require "badline/storage/kim_tape"
15
19
  require "badline/storage/crt_file"
16
20
  require "badline/storage/unavailable"
17
21
 
@@ -24,6 +28,7 @@ module Badline
24
28
  WRITE_PROTECT_ON = 26
25
29
  FILE_NOT_FOUND = 62
26
30
  FILE_EXISTS = 63
31
+ FILE_TYPE_MISMATCH = 64
27
32
  DISK_FULL = 72
28
33
 
29
34
  attr_reader :code
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Support
5
+ # A Checkpoint of a CBM-II: its CPU with both segment registers, the
6
+ # 2K of segment 15's RAM, the RAM board's segments, the screen RAM, the
7
+ # picture, the CRTC's registers and counters, the CIA, the ACIA's
8
+ # registers and both TPIs.
9
+ module CBM2Checkpoint
10
+ COMPONENTS = %w[cpu ram segments screen display crtc cia acia tpi1 tpi2].freeze
11
+
12
+ def self.take(machine)
13
+ bus = machine.bus
14
+ Checkpoint.new(machine.cycles, [
15
+ Checkpoint.fnv1a(Checkpoint.cpu_state(machine.cpu) +
16
+ [bus.execution_segment, bus.indirect_segment]),
17
+ Checkpoint.fnv1a(Checkpoint.ram(bus.ram, 0x800)),
18
+ Checkpoint.fnv1a(segments(bus)),
19
+ Checkpoint.fnv1a(Array.new(0x800) { |offset| bus.screen.peek(0xd000 + offset) }),
20
+ Checkpoint.fnv1a(machine.video.display),
21
+ Checkpoint.fnv1a(crtc_state(machine.crtc)),
22
+ Checkpoint.fnv1a(Checkpoint.cia_state(machine.cia)),
23
+ Checkpoint.fnv1a(acia_state(machine.acia)),
24
+ Checkpoint.fnv1a(tpi_state(machine.tpi1)),
25
+ Checkpoint.fnv1a(tpi_state(machine.tpi2))
26
+ ], COMPONENTS)
27
+ end
28
+
29
+ def self.parse(line) = Checkpoint.parse(line, COMPONENTS)
30
+
31
+ # Every byte of the segments the RAM board fills, segment by segment.
32
+ def self.segments(bus)
33
+ bytes = []
34
+ bus.segment_ram.each { |memory| bytes.concat(Checkpoint.ram(memory, 0x10000)) if memory }
35
+ bytes
36
+ end
37
+
38
+ def self.crtc_state(crtc) = crtc.registers + [crtc.column, crtc.line, crtc.row]
39
+
40
+ def self.acia_state(acia) = [acia.command, acia.control, acia.status]
41
+
42
+ def self.tpi_state(tpi)
43
+ [tpi.port_a_output, tpi.port_b_output, tpi.port_c_output, tpi.latches, tpi.mask, tpi.control,
44
+ tpi.irq? ? 1 : 0, *tpi.stack]
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Support
5
+ # A Checkpoint of a KIM-1: its CPU, the 1K of RAM, each 6530's RAM,
6
+ # ports and timer, the panel's held key and what its digits show, and
7
+ # what the teletype has printed and its line's level.
8
+ module KIM1Checkpoint
9
+ COMPONENTS = %w[cpu ram u2 u3 panel tty].freeze
10
+
11
+ def self.take(machine)
12
+ ram = machine.ram
13
+ Checkpoint.new(machine.cycles, [
14
+ Checkpoint.fnv1a(Checkpoint.cpu_state(machine.cpu)),
15
+ Checkpoint.fnv1a(Checkpoint.ram(ram, ram.length)),
16
+ Checkpoint.fnv1a(rriot_state(machine.u2)),
17
+ Checkpoint.fnv1a(rriot_state(machine.u3)),
18
+ Checkpoint.fnv1a(panel_state(machine.panel)),
19
+ Checkpoint.fnv1a(teletype_state(machine.teletype))
20
+ ], COMPONENTS)
21
+ end
22
+
23
+ def self.parse(line) = Checkpoint.parse(line, COMPONENTS)
24
+
25
+ def self.rriot_state(rriot) = rriot.idle_state.map { |value| bit(value) } + [rriot.timer.counter]
26
+
27
+ def self.panel_state(panel)
28
+ held = panel.held
29
+ [held.nil? ? -1 : KIM1::Panel::KEYS.index(held), *panel.display]
30
+ end
31
+
32
+ def self.teletype_state(teletype) = teletype.output.bytes + [teletype.line_high? ? 1 : 0]
33
+
34
+ def self.bit(value)
35
+ return value if value.is_a?(Integer)
36
+
37
+ value ? 1 : 0
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Support
5
+ # A Checkpoint of a P500: its CPU with both segment registers, the 2K
6
+ # of segment 15's RAM, segments 0 and 1, the static video matrix, the
7
+ # colour RAM's nibbles, the picture, the VIC's registers and beam, the
8
+ # SID's registers, the CIA, the ACIA's registers and both TPIs, the
9
+ # last three as CBM2Checkpoint digests them.
10
+ module P500Checkpoint
11
+ COMPONENTS = %w[cpu ram segments screen color_ram display vic sid cia acia tpi1 tpi2].freeze
12
+
13
+ def self.take(machine)
14
+ bus = machine.bus
15
+ Checkpoint.new(machine.cycles, [
16
+ Checkpoint.fnv1a(Checkpoint.cpu_state(machine.cpu) +
17
+ [bus.execution_segment, bus.indirect_segment]),
18
+ Checkpoint.fnv1a(Checkpoint.ram(bus.ram, 0x800)),
19
+ Checkpoint.fnv1a(CBM2Checkpoint.segments(bus)),
20
+ Checkpoint.fnv1a(Array.new(0x400) { |offset| bus.screen.peek(0xd000 + offset) }),
21
+ Checkpoint.fnv1a(Checkpoint.color_ram(machine.color_ram, 0xd400)),
22
+ Checkpoint.fnv1a(machine.vic.display),
23
+ Checkpoint.fnv1a(Checkpoint.vic_state(machine.vic)),
24
+ Checkpoint.fnv1a(Array.new(0x20) { |reg| machine.sid.register(reg) }),
25
+ Checkpoint.fnv1a(Checkpoint.cia_state(machine.cia)),
26
+ Checkpoint.fnv1a(CBM2Checkpoint.acia_state(machine.acia)),
27
+ Checkpoint.fnv1a(CBM2Checkpoint.tpi_state(machine.tpi1)),
28
+ Checkpoint.fnv1a(CBM2Checkpoint.tpi_state(machine.tpi2))
29
+ ], COMPONENTS)
30
+ end
31
+
32
+ def self.parse(line) = Checkpoint.parse(line, COMPONENTS)
33
+ end
34
+ end
35
+ end