badline 0.8.0 → 0.10.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 (134) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +58 -0
  3. data/CLAUDE.md +6 -4
  4. data/README.md +60 -19
  5. data/doc/library.md +10 -3
  6. data/doc/media.md +11 -2
  7. data/doc/pinned-behaviour.md +199 -9
  8. data/doc/sid-player.md +8 -4
  9. data/doc/snapshots.md +79 -33
  10. data/lib/badline/address_bus/pla.rb +130 -0
  11. data/lib/badline/address_bus/roms.rb +32 -0
  12. data/lib/badline/address_bus.rb +13 -92
  13. data/lib/badline/audio/bare_player.rb +24 -0
  14. data/lib/badline/audio/checkpoints.rb +42 -0
  15. data/lib/badline/audio/jukebox.rb +11 -4
  16. data/lib/badline/audio/machine_player.rb +16 -0
  17. data/lib/badline/audio/renderer.rb +80 -9
  18. data/lib/badline/audio/stereo.rb +3 -5
  19. data/lib/badline/audio.rb +1 -0
  20. data/lib/badline/chrout_trap.rb +4 -3
  21. data/lib/badline/computer/attachments.rb +52 -12
  22. data/lib/badline/computer/saved_state.rb +1 -0
  23. data/lib/badline/computer.rb +50 -33
  24. data/lib/badline/control_ports.rb +10 -3
  25. data/lib/badline/core.rb +51 -0
  26. data/lib/badline/data_path.rb +37 -0
  27. data/lib/badline/datasette.rb +63 -10
  28. data/lib/badline/drive1541/disk.rb +3 -3
  29. data/lib/badline/drive1541/disk_state.rb +14 -5
  30. data/lib/badline/drive1541/serial_port.rb +5 -8
  31. data/lib/badline/drive1541/sleep.rb +5 -6
  32. data/lib/badline/drive1541.rb +3 -3
  33. data/lib/badline/frontend/app.rb +63 -64
  34. data/lib/badline/frontend/boot.rb +34 -4
  35. data/lib/badline/frontend/confirmation.rb +17 -11
  36. data/lib/badline/frontend/controls.rb +12 -6
  37. data/lib/badline/frontend/drive_led.rb +9 -10
  38. data/lib/badline/frontend/frame_rate.rb +4 -4
  39. data/lib/badline/frontend/machine_window.rb +87 -0
  40. data/lib/badline/frontend/menu_dialogs.rb +52 -14
  41. data/lib/badline/frontend/menu_media.rb +70 -16
  42. data/lib/badline/frontend/menu_pages.rb +47 -42
  43. data/lib/badline/frontend/menu_ports.rb +69 -0
  44. data/lib/badline/frontend/name_field.rb +88 -0
  45. data/lib/badline/frontend/pacer.rb +6 -5
  46. data/lib/badline/frontend/pause_menu.rb +52 -37
  47. data/lib/badline/frontend/player_screen.rb +1 -1
  48. data/lib/badline/frontend/player_window.rb +4 -4
  49. data/lib/badline/frontend/screen.rb +39 -37
  50. data/lib/badline/frontend/sid_view.rb +1 -1
  51. data/lib/badline/frontend/snapshots.rb +145 -17
  52. data/lib/badline/frontend/sound.rb +17 -9
  53. data/lib/badline/frontend/timeline.rb +36 -5
  54. data/lib/badline/frontend.rb +3 -0
  55. data/lib/badline/iec_bus.rb +16 -28
  56. data/lib/badline/kernal_trap/drive.rb +3 -0
  57. data/lib/badline/kernal_trap/file.rb +1 -1
  58. data/lib/badline/kernal_trap/layout.rb +112 -0
  59. data/lib/badline/kernal_trap/load.rb +24 -25
  60. data/lib/badline/kernal_trap/routine.rb +17 -5
  61. data/lib/badline/kernal_trap/save.rb +13 -23
  62. data/lib/badline/kernal_trap/serial.rb +38 -24
  63. data/lib/badline/kernal_trap.rb +1 -0
  64. data/lib/badline/keyboard.rb +17 -14
  65. data/lib/badline/machine.rb +60 -0
  66. data/lib/badline/media/disk_list.rb +75 -0
  67. data/lib/badline/media/disk_set.rb +13 -3
  68. data/lib/badline/media/vic20_basic.rb +54 -0
  69. data/lib/badline/media/vic20_cartridge.rb +74 -0
  70. data/lib/badline/media/vic20_media.rb +132 -0
  71. data/lib/badline/media.rb +39 -20
  72. data/lib/badline/model.rb +62 -0
  73. data/lib/badline/options/event.rb +10 -6
  74. data/lib/badline/options/help.rb +25 -13
  75. data/lib/badline/options/table.rb +15 -1
  76. data/lib/badline/options/validation.rb +63 -3
  77. data/lib/badline/options.rb +37 -34
  78. data/lib/badline/region.rb +16 -4
  79. data/lib/badline/reu/ram.rb +27 -0
  80. data/lib/badline/reu/saved_state.rb +27 -0
  81. data/lib/badline/roms/README +37 -0
  82. data/lib/badline/roms/kernal-sx64.rom +0 -0
  83. data/lib/badline/roms/vic20/basic.rom +0 -0
  84. data/lib/badline/roms/vic20/character.rom +0 -0
  85. data/lib/badline/roms/vic20/kernal-ntsc.rom +0 -0
  86. data/lib/badline/roms/vic20/kernal-pal.rom +0 -0
  87. data/lib/badline/sid/decimator.rb +13 -0
  88. data/lib/badline/sid/filter.rb +6 -3
  89. data/lib/badline/sid.rb +19 -0
  90. data/lib/badline/snapshot/container.rb +2 -0
  91. data/lib/badline/snapshot/fields.rb +1 -0
  92. data/lib/badline/snapshot/image.rb +15 -0
  93. data/lib/badline/snapshot/setup.rb +20 -8
  94. data/lib/badline/snapshot/state.rb +15 -7
  95. data/lib/badline/snapshot/vice/main_cpu.rb +1 -1
  96. data/lib/badline/snapshot/vice/peripherals.rb +12 -2
  97. data/lib/badline/snapshot/vice/restore.rb +3 -1
  98. data/lib/badline/snapshot/vice/reu1764.rb +55 -0
  99. data/lib/badline/snapshot/vice/vicii.rb +59 -13
  100. data/lib/badline/snapshot/vice/vicii_import.rb +14 -3
  101. data/lib/badline/snapshot/vice.rb +25 -23
  102. data/lib/badline/snapshot.rb +10 -0
  103. data/lib/badline/storage/crt_file.rb +16 -5
  104. data/lib/badline/storage/disk_image.rb +3 -0
  105. data/lib/badline/storage/host_directory.rb +3 -0
  106. data/lib/badline/storage/t64.rb +10 -4
  107. data/lib/badline/storage/tap.rb +44 -8
  108. data/lib/badline/storage/unavailable.rb +3 -1
  109. data/lib/badline/storage.rb +5 -5
  110. data/lib/badline/timing.rb +12 -0
  111. data/lib/badline/version.rb +1 -1
  112. data/lib/badline/via/shift_register.rb +43 -16
  113. data/lib/badline/vic/bank.rb +53 -16
  114. data/lib/badline/vic/registers.rb +20 -4
  115. data/lib/badline/vic/sprite/timing.rb +5 -9
  116. data/lib/badline/vic/sprite.rb +4 -3
  117. data/lib/badline/vic.rb +34 -15
  118. data/lib/badline/vic20/attachments.rb +103 -0
  119. data/lib/badline/vic20/bus.rb +308 -0
  120. data/lib/badline/vic20/cpu.rb +10 -0
  121. data/lib/badline/vic20/keyboard_via_ports.rb +53 -0
  122. data/lib/badline/vic20/port_wiring.rb +50 -0
  123. data/lib/badline/vic20/saved_state.rb +152 -0
  124. data/lib/badline/vic20/sound/output.rb +83 -0
  125. data/lib/badline/vic20/sound.rb +233 -0
  126. data/lib/badline/vic20/user_via_ports.rb +47 -0
  127. data/lib/badline/vic20/vic/painter.rb +239 -0
  128. data/lib/badline/vic20/vic/saved_state.rb +55 -0
  129. data/lib/badline/vic20/vic/text_window.rb +71 -0
  130. data/lib/badline/vic20/vic.rb +267 -0
  131. data/lib/badline/vic20.rb +220 -0
  132. data/lib/badline.rb +2 -38
  133. metadata +37 -2
  134. data/lib/badline/address_bus/ultimax_pages.rb +0 -38
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Vic20
5
+ MARKER = "VIC20"
6
+
7
+ # The RAM expansion of the machine a State from #snapshot was taken
8
+ # of, a key of Bus::RAM_CONFIGURATIONS.
9
+ def self.setup(state)
10
+ input = Snapshot::StateReader.new(state)
11
+ input.marker(MARKER)
12
+ input.check_stamp
13
+ SavedState.read_ram(input)
14
+ end
15
+
16
+ # A new machine, built as the one a State from #snapshot was taken of,
17
+ # at that state.
18
+ def self.restored(state) = new(ram: setup(state)).apply_state(state)
19
+
20
+ # The whole machine's state for a snapshot, as Computer's: the
21
+ # machine's own latches, the bus with its RAM and cartridge ROM, the
22
+ # VIC with its sound, both VIAs, the CPU, the datasette and its tape,
23
+ # the disk device 8 serves through the traps, and a true 1541. The
24
+ # serial lines go back into the bus from the restored VIAs. What the
25
+ # host holds stays out: the keyboard, the joystick, RESTORE, the traps
26
+ # and callbacks, whether the display renders, whether the sound
27
+ # records, and whether the datasette records.
28
+ module SavedState
29
+ RAM_CONFIGURATIONS = Bus::RAM_CONFIGURATIONS.keys.freeze
30
+
31
+ def self.read_ram(input)
32
+ RAM_CONFIGURATIONS.fetch(input.int)
33
+ rescue IndexError
34
+ raise Snapshot::FormatError, "the state names a VIC-20 RAM expansion badline doesn't know"
35
+ end
36
+
37
+ # How many more on_init handlers the machine a State was taken of had
38
+ # yet to run than this one has. Nil until the machine is restored.
39
+ attr_reader :init_handlers_lost
40
+
41
+ def snapshot
42
+ out = Snapshot::StateWriter.new
43
+ save_state(out)
44
+ out.state
45
+ end
46
+
47
+ # Takes the machine back to a State from #snapshot, trying it on a new
48
+ # machine first, so one that fails leaves this machine as it was.
49
+ # Raises Snapshot::FormatError for a state of another machine or RAM
50
+ # expansion.
51
+ def restore(state)
52
+ check_ram(Vic20.setup(state))
53
+ Vic20.restored(state)
54
+ apply_state(state)
55
+ end
56
+
57
+ # Puts a State into the machine without trying it on a new one first.
58
+ def apply_state(state)
59
+ input = Snapshot::StateReader.new(state)
60
+ load_state(input)
61
+ raise Snapshot::FormatError, "the state goes on past the machine" unless input.finished?
62
+
63
+ self
64
+ end
65
+
66
+ def save_state(out)
67
+ out.marker(MARKER).stamp
68
+ out.int(RAM_CONFIGURATIONS.index(@ram_configuration))
69
+ out.int(@cycles).boolean(@nmi_asserted).boolean(!@pending_keys.nil?)
70
+ out.ints(@pending_keys) if @pending_keys
71
+ out.int(@cycles <= @init_threshold ? @init_handlers.length : 0)
72
+ @bus.save_state(out)
73
+ @vic.save_state(out)
74
+ @sound.save_state(out)
75
+ @via1.save_state(out)
76
+ @via2.save_state(out)
77
+ @cpu.save_state(out)
78
+ @datasette.save_state(out)
79
+ save_trap_drive(out)
80
+ save_drive1541(out)
81
+ end
82
+
83
+ def load_state(input)
84
+ input.marker(MARKER)
85
+ input.check_stamp
86
+ check_ram(SavedState.read_ram(input))
87
+ @cycles = input.int
88
+ @nmi_asserted = input.boolean?
89
+ @pending_keys = nil
90
+ @pending_keys = input.ints if input.boolean?
91
+ @init_handlers_lost = [input.int - @init_handlers.length, 0].max
92
+ @bus.load_state(input)
93
+ @vic.load_state(input)
94
+ @sound.load_state(input)
95
+ @via1.load_state(input)
96
+ @via2.load_state(input)
97
+ @cpu.load_state(input)
98
+ via_written
99
+ @datasette.load_state(input)
100
+ load_trap_drive(input)
101
+ load_drive1541(input)
102
+ end
103
+
104
+ private
105
+
106
+ # The disk device 8 serves through the traps, with its channels.
107
+ def save_trap_drive(out)
108
+ out.boolean(!@drive.nil?)
109
+ return unless @drive
110
+
111
+ @drive.save_state(out)
112
+ @serial_trap.save_state(out)
113
+ @save_trap.save_state(out)
114
+ end
115
+
116
+ def load_trap_drive(input)
117
+ return unmount unless input.boolean?
118
+
119
+ storage = Storage.reopen(input)
120
+ unmount
121
+ mount(storage)
122
+ @drive.load_state(input)
123
+ @serial_trap.load_state(input)
124
+ @save_trap.load_state(input)
125
+ end
126
+
127
+ # A true 1541 on the serial bus, with its device number.
128
+ def save_drive1541(out)
129
+ out.boolean(!@drive1541.nil?)
130
+ return unless @drive1541
131
+
132
+ out.int(@drive1541.device)
133
+ @drive1541.save_state(out)
134
+ end
135
+
136
+ def load_drive1541(input)
137
+ return detach_drive1541 unless input.boolean?
138
+
139
+ device = input.int
140
+ detach_drive1541 if @drive1541 && @drive1541.device != device
141
+ attach_drive1541(Drive1541.new(device:)) unless @drive1541
142
+ @drive1541.load_state(input)
143
+ end
144
+
145
+ def check_ram(ram)
146
+ return if ram == @ram_configuration
147
+
148
+ raise Snapshot::FormatError, "the state is of a VIC-20 with #{ram} RAM, not #{@ram_configuration}"
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Vic20
5
+ class Sound
6
+ # The sound's level averaged over each sample of the audio rate, then
7
+ # through the machine's output stage: a one-pole low-pass at 1,420 Hz
8
+ # and a one-pole high-pass at 158 Hz, both run at the audio rate.
9
+ #
10
+ # The two corners are fitted to xvic's step response: a voice turned
11
+ # on at volume 15 rises and decays back to the line as those two RC
12
+ # stages do, within 6 parts in 11,000.
13
+ class Output
14
+ LOW_PASS_HZ = 1_420
15
+ HIGH_PASS_HZ = 158
16
+
17
+ # Scales the loudest level, four voices high at volume 15, to the
18
+ # top of a signed 16-bit sample.
19
+ SCALE = 32_767.0 / 570
20
+
21
+ attr_reader :samples
22
+
23
+ # `level` is the sound's level as the recording starts, which the
24
+ # stage takes as settled.
25
+ def initialize(clock_hz:, rate:, level:)
26
+ @clock_hz = clock_hz
27
+ @rate = rate
28
+ @remainder = 0
29
+ @window = next_window
30
+ @left = @window
31
+ @sum = 0
32
+ @low = @drift = level.to_f
33
+ @low_pass = 1.0 - Math.exp(-2.0 * Math::PI * LOW_PASS_HZ / rate)
34
+ @high_pass = 1.0 - Math.exp(-2.0 * Math::PI * HIGH_PASS_HZ / rate)
35
+ @samples = []
36
+ end
37
+
38
+ # Adds `cycles` cycles at `level`, closing every sample they finish.
39
+ def hold(level, cycles)
40
+ left = @left
41
+ while cycles >= left
42
+ @sum += level * left
43
+ cycles -= left
44
+ close
45
+ left = @left
46
+ end
47
+ @sum += level * cycles
48
+ @left = left - cycles
49
+ end
50
+
51
+ # The samples closed since the last drain.
52
+ def drain
53
+ samples = @samples
54
+ @samples = []
55
+ samples
56
+ end
57
+
58
+ private
59
+
60
+ def close
61
+ @low += @low_pass * (@sum.fdiv(@window) - @low)
62
+ @drift += @high_pass * (@low - @drift)
63
+ sample = ((@low - @drift) * SCALE).round
64
+ sample = 32_767 if sample > 32_767
65
+ sample = -32_768 if sample < -32_768
66
+ @samples << sample
67
+ @sum = 0
68
+ @window = next_window
69
+ @left = @window
70
+ end
71
+
72
+ # Each window is a whole number of cycles, and they add up to the
73
+ # clock's cycles over a second.
74
+ def next_window
75
+ @remainder += @clock_hz
76
+ cycles = @remainder / @rate
77
+ @remainder -= cycles * @rate
78
+ cycles
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,233 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "badline/vic20/sound/output"
4
+
5
+ module Badline
6
+ class Vic20
7
+ # The VIC-I's sound: three square-wave voices, bass, alto and soprano at
8
+ # $900A-$900C, a noise voice at $900D, and the volume in $900E bits 0-3.
9
+ #
10
+ # Each voice has a 7-bit counter, ticked every 16, 8, 4 or 2 cycles from
11
+ # bass to noise. It counts up from the low 7 bits of its register, and
12
+ # on the tick it would reach 127 it reloads from the register instead.
13
+ # So a voice's counter comes round every 128 - ((value + 1) & 127)
14
+ # ticks, and the register's new value takes hold at the next reload.
15
+ #
16
+ # A tone voice's counter shifts an 8-bit shift register at each reload,
17
+ # bit 7 out and back into bit 0 inverted while bit 7 of the register
18
+ # enables the voice, and a zero while it doesn't. Bit 0 is the voice's
19
+ # output: a square wave of 16 shifts while the voice is on, which a
20
+ # voice turned off shifts out to silence.
21
+ #
22
+ # The noise voice's counter shifts a 16-bit LFSR instead, bit 0 in from
23
+ # the XOR of bits 3, 12, 14 and 15 while the voice is on and a one
24
+ # while it's off. Each time the LFSR's bit 0 rises, it shifts the noise
25
+ # voice's own 8-bit shift register, bit 7 back into bit 0, inverted
26
+ # while the voice is on. Its bit 0 is the voice's output while the voice
27
+ # is on, and it is silent while it's off.
28
+ #
29
+ # The output is the voices that are high, plus a little more, times the
30
+ # volume, so a volume write steps the output even with every voice off.
31
+ #
32
+ # The voices only run while #record collects the output: nothing the
33
+ # CPU reads depends on them. Until then a write only sets the register.
34
+ # While recording, the voices catch up to the current cycle whenever a
35
+ # write lands or the samples are drained, one shift at a time.
36
+ class Sound
37
+ VOICES = 4
38
+ NOISE = 3
39
+
40
+ # The cycles between ticks of each voice's counter.
41
+ DIVIDERS = [16, 8, 4, 2].freeze
42
+
43
+ # What a voice that is high adds to the output's level, and what the
44
+ # output carries with every voice low, before the volume.
45
+ VOICE_LEVEL = 9
46
+ IDLE_LEVEL = 2
47
+
48
+ attr_reader :volume
49
+
50
+ # `clock` answers the machine's cycles, `clock_hz` the rate they run at.
51
+ def initialize(clock, clock_hz)
52
+ @clock = clock
53
+ @clock_hz = clock_hz
54
+ @output = nil
55
+ @time = 0
56
+ @shift_registers = Array.new(VOICES, 0)
57
+ @next_shifts = Array.new(VOICES, 0)
58
+ @frequencies = Array.new(VOICES, 0)
59
+ @enabled = Array.new(VOICES, false)
60
+ power_on!
61
+ end
62
+
63
+ def model = :mos6561
64
+
65
+ # The power-on state: every register clear and the tone voices' shift
66
+ # registers empty. The noise voice's LFSR and shift register start full
67
+ # of ones, as a noise voice left off fills them.
68
+ def power_on!
69
+ now = @clock.cycles
70
+ run_to(now)
71
+ @volume = 0
72
+ VOICES.times do |voice|
73
+ @frequencies[voice] = 0
74
+ @enabled[voice] = false
75
+ @shift_registers[voice] = 0
76
+ divider = DIVIDERS[voice]
77
+ @next_shifts[voice] = ((now / divider) + 128) * divider
78
+ end
79
+ @shift_registers[NOISE] = 0xff
80
+ @lfsr = 0xffff
81
+ @level = level_now
82
+ end
83
+
84
+ # Starts collecting the output, averaged down to `rate` samples a
85
+ # second, for #drain_samples. `clock_hz` is the rate the cycles are
86
+ # taken to run at.
87
+ def record(rate:, clock_hz: @clock_hz)
88
+ now = @clock.cycles
89
+ run_to(now)
90
+ resume(now)
91
+ @output = Output.new(clock_hz:, rate:, level: @level)
92
+ end
93
+
94
+ def recording? = !@output.nil?
95
+
96
+ # The samples recorded since the last drain.
97
+ def drain_samples
98
+ output = @output
99
+ return [] if output.nil?
100
+
101
+ run_to(@clock.cycles)
102
+ output.drain
103
+ end
104
+
105
+ # A write to $900A-$900E, which takes hold from the next cycle.
106
+ def write(register, value)
107
+ run_to(@clock.cycles + 1)
108
+ if register == 0x0e
109
+ @volume = value & 0x0f
110
+ else
111
+ voice = register - 0x0a
112
+ @frequencies[voice] = value & 0x7f
113
+ @enabled[voice] = value >= 0x80
114
+ end
115
+ @level = level_now
116
+ end
117
+
118
+ # The registers and the voices as they last ran, for a snapshot.
119
+ # Recording is the host's, and starts the voices again from the
120
+ # cycle it starts at.
121
+ def save_state(out)
122
+ run_to(@clock.cycles)
123
+ out.marker("VIC20 SOUND").int(@volume).ints(@frequencies).booleans(@enabled)
124
+ out.ints(@shift_registers).ints(@next_shifts).int(@lfsr).int(@level).int(@time)
125
+ end
126
+
127
+ def load_state(input)
128
+ input.marker("VIC20 SOUND")
129
+ @volume = input.int
130
+ input.ints_into(@frequencies)
131
+ input.booleans_into(@enabled)
132
+ input.ints_into(@shift_registers)
133
+ input.ints_into(@next_shifts)
134
+ @lfsr = input.int
135
+ @level = input.int
136
+ @time = input.int
137
+ resume(@clock.cycles) if recording?
138
+ end
139
+
140
+ # A voice's shift register as of the current cycle, while recording.
141
+ def shift_register(voice)
142
+ run_to(@clock.cycles)
143
+ @shift_registers[voice]
144
+ end
145
+
146
+ def lfsr
147
+ run_to(@clock.cycles)
148
+ @lfsr
149
+ end
150
+
151
+ private
152
+
153
+ # Moves each shift that the cycles since the voices last ran have
154
+ # passed to its next turn from `now`, keeping its phase.
155
+ def resume(now)
156
+ @time = now
157
+ VOICES.times do |voice|
158
+ due = @next_shifts[voice]
159
+ next if due >= now
160
+
161
+ interval = period(voice) * DIVIDERS[voice]
162
+ @next_shifts[voice] = due + ((now - due + interval - 1) / interval * interval)
163
+ end
164
+ end
165
+
166
+ # Runs the voices up to the start of cycle `target`, the output holding
167
+ # each level for the cycles it lasts.
168
+ def run_to(target)
169
+ time = @time
170
+ output = @output
171
+ return if output.nil? || target <= time
172
+
173
+ following = next_shift
174
+ while following < target
175
+ output.hold(@level, following - time)
176
+ time = following
177
+ shift_due(time)
178
+ @level = level_now
179
+ following = next_shift
180
+ end
181
+ output.hold(@level, target - time)
182
+ @time = target
183
+ end
184
+
185
+ def next_shift
186
+ shifts = @next_shifts
187
+ following = shifts[0]
188
+ following = shifts[1] if shifts[1] < following
189
+ following = shifts[2] if shifts[2] < following
190
+ following = shifts[3] if shifts[3] < following
191
+ following
192
+ end
193
+
194
+ def shift_due(time)
195
+ shifts = @next_shifts
196
+ VOICES.times do |voice|
197
+ next unless shifts[voice] == time
198
+
199
+ voice == NOISE ? step_noise : shift_tone(voice)
200
+ shifts[voice] = time + (period(voice) * DIVIDERS[voice])
201
+ end
202
+ end
203
+
204
+ def shift_tone(voice)
205
+ bits = @shift_registers[voice]
206
+ fill = @enabled[voice] && bits < 0x80 ? 1 : 0
207
+ @shift_registers[voice] = ((bits << 1) | fill) & 0xff
208
+ end
209
+
210
+ def step_noise
211
+ lfsr = @lfsr
212
+ enabled = @enabled[NOISE]
213
+ fill = enabled ? ((lfsr >> 3) ^ (lfsr >> 12) ^ (lfsr >> 14) ^ (lfsr >> 15)) & 1 : 1
214
+ @lfsr = ((lfsr << 1) | fill) & 0xffff
215
+ return unless fill == 1 && lfsr.even?
216
+
217
+ bits = @shift_registers[NOISE]
218
+ fill = (bits >> 7) ^ (enabled ? 1 : 0)
219
+ @shift_registers[NOISE] = ((bits << 1) | fill) & 0xff
220
+ end
221
+
222
+ # The ticks between two reloads of a voice's counter.
223
+ def period(voice) = 128 - ((@frequencies[voice] + 1) & 0x7f)
224
+
225
+ def level_now
226
+ registers = @shift_registers
227
+ high = (registers[0] & 1) + (registers[1] & 1) + (registers[2] & 1)
228
+ high += registers[NOISE] & 1 if @enabled[NOISE]
229
+ ((high * VOICE_LEVEL) + IDLE_LEVEL) * @volume
230
+ end
231
+ end
232
+ end
233
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class Vic20
5
+ # What VIA 1's ports read from outside the chip. Port A carries the
6
+ # serial bus's CLK and DATA on PA0 and PA1, which read 1 while a line
7
+ # is released, the joystick's up, down and left switches on PA2-PA4,
8
+ # its fire button on PA5, the line the light pen shares, and the
9
+ # cassette sense switch on PA6, low while a key is down on the deck.
10
+ # The right switch is on VIA 2 (see KeyboardVIAPorts). PA7 drives ATN
11
+ # out and reads back what it drives. Port B is the user port, with
12
+ # nothing plugged in.
13
+ #
14
+ # CA2 drives the cassette motor, which runs while CA2 is low, and the
15
+ # RESTORE key pulls CA1 (see Vic20#press_restore).
16
+ class UserVIAPorts
17
+ attr_reader :joystick
18
+
19
+ def initialize(joystick:, serial_bus:, datasette:)
20
+ @joystick = joystick
21
+ @serial_bus = serial_bus
22
+ @datasette = datasette
23
+ end
24
+
25
+ # The joystick's up, down and left bits (0-2) move up two, and its
26
+ # fire bit (4) up one.
27
+ def read_a(_lines)
28
+ bits = @joystick.port_bits
29
+ value = 0x80 | ((bits & 0x07) << 2) | ((bits & 0x10) << 1)
30
+ value |= 0x40 unless @datasette.sense_low?
31
+ value | serial_inputs
32
+ end
33
+
34
+ def read_b(_lines) = 0xff
35
+
36
+ private
37
+
38
+ def serial_inputs
39
+ low = @serial_bus.low_lines
40
+ value = 0x03
41
+ value &= ~0x01 if low.anybits?(IECBus::CLK)
42
+ value &= ~0x02 if low.anybits?(IECBus::DATA)
43
+ value
44
+ end
45
+ end
46
+ end
47
+ end