badline 0.14.0 → 0.15.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +23 -0
- data/CLAUDE.md +15 -5
- data/README.md +68 -8
- data/doc/pinned-behaviour.md +145 -0
- data/doc/snapshot-format.md +188 -38
- data/doc/snapshots.md +18 -1
- data/lib/badline/audio/bare_player.rb +4 -8
- data/lib/badline/audio/machine_player.rb +2 -12
- data/lib/badline/c128/banks.rb +1 -3
- data/lib/badline/c128/bus.rb +19 -1
- data/lib/badline/c128/mmu_pages.rb +7 -0
- data/lib/badline/c64/bus/extra_sids.rb +80 -0
- data/lib/badline/c64/bus.rb +1 -0
- data/lib/badline/c64/cartridge/action_replay.rb +6 -2
- data/lib/badline/c64/cartridge/comal80.rb +1 -0
- data/lib/badline/c64/cartridge/dinamic.rb +3 -1
- data/lib/badline/c64/cartridge/final_cartridge3.rb +1 -0
- data/lib/badline/c64/cartridge/fun_play.rb +2 -1
- data/lib/badline/c64/cartridge/game_system.rb +2 -1
- data/lib/badline/c64/cartridge/keyed_state.rb +155 -34
- data/lib/badline/c64/cartridge/magic_desk.rb +2 -1
- data/lib/badline/c64/cartridge/ocean.rb +1 -0
- data/lib/badline/c64/cartridge/pagefox.rb +1 -0
- data/lib/badline/c64/cartridge/rgcd.rb +2 -1
- data/lib/badline/c64/cartridge/super_games.rb +3 -2
- data/lib/badline/c64/cartridge/zaxxon.rb +1 -0
- data/lib/badline/c64/cartridge.rb +3 -3
- data/lib/badline/c64/keyed_modules.rb +6 -2
- data/lib/badline/chips/ted/beam.rb +13 -14
- data/lib/badline/chips/ted/fetches.rb +199 -0
- data/lib/badline/chips/ted/keyed_state.rb +156 -0
- data/lib/badline/chips/ted/modes.rb +134 -0
- data/lib/badline/chips/ted/palette.rb +43 -0
- data/lib/badline/chips/ted/picture.rb +182 -0
- data/lib/badline/chips/ted/sound/output.rb +86 -0
- data/lib/badline/chips/ted/sound.rb +284 -0
- data/lib/badline/chips/ted.rb +53 -6
- data/lib/badline/chips/tia.rb +12 -0
- data/lib/badline/drives/datasette/keyed_state.rb +7 -2
- data/lib/badline/drives/datasette.rb +22 -5
- data/lib/badline/frontend/boot.rb +3 -1
- data/lib/badline/frontend/controls.rb +7 -3
- data/lib/badline/frontend/keys.rb +14 -2
- data/lib/badline/frontend/media_slots.rb +11 -1
- data/lib/badline/frontend/menu/menu_dialogs.rb +2 -1
- data/lib/badline/frontend/menu/menu_media.rb +13 -3
- data/lib/badline/frontend/menu/menu_pages.rb +8 -8
- data/lib/badline/frontend/menu/menu_ports.rb +13 -1
- data/lib/badline/frontend/screen.rb +33 -12
- data/lib/badline/frontend/screens.rb +2 -1
- data/lib/badline/frontend/sound.rb +4 -4
- data/lib/badline/frontend/timeline.rb +19 -6
- data/lib/badline/machine/settling.rb +1 -1
- data/lib/badline/machine.rb +26 -6
- data/lib/badline/media/plus4_media.rb +68 -0
- data/lib/badline/media/program.rb +20 -6
- data/lib/badline/media.rb +33 -12
- data/lib/badline/options/family.rb +1 -0
- data/lib/badline/options/help.rb +30 -6
- data/lib/badline/options/table.rb +15 -2
- data/lib/badline/options/validation.rb +26 -4
- data/lib/badline/options.rb +37 -9
- data/lib/badline/plus4/bus.rb +77 -14
- data/lib/badline/plus4/cassette_sense.rb +21 -0
- data/lib/badline/plus4/keyboard_port.rb +35 -10
- data/lib/badline/plus4/keyed_modules.rb +99 -0
- data/lib/badline/plus4/keyed_parts.rb +143 -0
- data/lib/badline/plus4/keyed_state.rb +41 -0
- data/lib/badline/plus4.rb +92 -16
- data/lib/badline/snapshot/container.rb +2 -0
- data/lib/badline/snapshot/field_doc/markdown.rb +7 -6
- data/lib/badline/snapshot/field_doc.rb +46 -7
- data/lib/badline/snapshot/image.rb +12 -3
- data/lib/badline/snapshot/plus4_setup.rb +39 -0
- data/lib/badline/snapshot.rb +9 -4
- data/lib/badline/storage/g64_image.rb +1 -1
- data/lib/badline/storage/tap.rb +21 -12
- data/lib/badline/support/checkpoint.rb +3 -2
- data/lib/badline/support/plus4_checkpoint.rb +42 -0
- data/lib/badline/version.rb +1 -1
- metadata +15 -1
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Chips
|
|
5
|
+
class TED
|
|
6
|
+
# The picture: a palette index a pixel (Palette), 384 pixels a line
|
|
7
|
+
# and every line of the frame. A line's pixels start at horizontal
|
|
8
|
+
# count 419, so the 40-column window, counts 451-315, runs from pixel
|
|
9
|
+
# 32 to 351, as in xplus4's screenshots.
|
|
10
|
+
#
|
|
11
|
+
# The picture's lines are the scanlines of the frame as they go out,
|
|
12
|
+
# which follow the line count until a write to $FF1C-$FF1D moves it:
|
|
13
|
+
# the frame's scanlines start over where the line count wraps to 0 at
|
|
14
|
+
# the end of the frame, and step with each line otherwise, as in
|
|
15
|
+
# xplus4's screenshots of TED/write-scanline-coarse.
|
|
16
|
+
#
|
|
17
|
+
# A line is painted at column 44, once its last pixel is out, from
|
|
18
|
+
# what the line fetched (Fetches) in the colours of its mode (Modes).
|
|
19
|
+
# A write to a register the picture reads paints the line up to the
|
|
20
|
+
# pixel the write lands on first, so the old value holds to there and
|
|
21
|
+
# the new one from there.
|
|
22
|
+
#
|
|
23
|
+
# Outside the window the border colour ($FF19) shows: above and below
|
|
24
|
+
# it, from the line count reaching line 4 to its reaching line 204 with
|
|
25
|
+
# $FF06 bit 3 set (25 rows), or 8 and 200 (24 rows), so a line count
|
|
26
|
+
# moved past line 4 leaves the frame's window shut;
|
|
27
|
+
# beside it, the 38-column window ($FF07 bit 3 clear) is pixels 39 to
|
|
28
|
+
# 342, as in xplus4's screenshots, where the datasheet's counts would
|
|
29
|
+
# make it 40 to 343. $FF06 bit 4 clear blanks the whole picture to the
|
|
30
|
+
# border colour. A line of the window with no row under way shows the
|
|
31
|
+
# background colour, $FF15.
|
|
32
|
+
#
|
|
33
|
+
# $FF07 bits 0-2 move the characters right by as many pixels, the
|
|
34
|
+
# window's pixels before the first character showing the background.
|
|
35
|
+
module Picture
|
|
36
|
+
WIDTH = 384
|
|
37
|
+
|
|
38
|
+
# The horizontal count of a line's first pixel.
|
|
39
|
+
FIRST_COUNT = 419
|
|
40
|
+
|
|
41
|
+
# The column whose cycle paints the line, and the first whose writes
|
|
42
|
+
# land on the next line.
|
|
43
|
+
PAINT_COLUMN = 44
|
|
44
|
+
NEXT_LINE_COLUMN = 52
|
|
45
|
+
|
|
46
|
+
WINDOW_LEFT = 32
|
|
47
|
+
WINDOW_RIGHT = 352
|
|
48
|
+
NARROW_LEFT = 39
|
|
49
|
+
NARROW_RIGHT = 343
|
|
50
|
+
|
|
51
|
+
# How many dots after its cycle's first a register write takes hold:
|
|
52
|
+
# where xplus4 shows a raster interrupt's colour change, a cell
|
|
53
|
+
# boundary with no horizontal scroll.
|
|
54
|
+
WRITE_DELAY = 19
|
|
55
|
+
|
|
56
|
+
# The picture, a palette index a pixel.
|
|
57
|
+
attr_reader :display
|
|
58
|
+
|
|
59
|
+
# Whether each line has been painted since the front end last
|
|
60
|
+
# cleared them.
|
|
61
|
+
attr_reader :dirty_lines
|
|
62
|
+
|
|
63
|
+
# Whether the TED paints its picture. It fetches either way.
|
|
64
|
+
attr_reader :render
|
|
65
|
+
|
|
66
|
+
def width = WIDTH
|
|
67
|
+
|
|
68
|
+
def height = lines_per_frame
|
|
69
|
+
|
|
70
|
+
def clear_dirty_lines!
|
|
71
|
+
@dirty_lines.fill(false)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def render=(value)
|
|
75
|
+
@render = value
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def reset_picture
|
|
81
|
+
@display = Array.new(WIDTH * 312, 0)
|
|
82
|
+
@dirty_lines = Array.new(312, true)
|
|
83
|
+
@render = true
|
|
84
|
+
@painted = 0
|
|
85
|
+
@scanline = 0
|
|
86
|
+
@window_open = false
|
|
87
|
+
reset_modes
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# The line count steps to +line+: the next scanline, and the
|
|
91
|
+
# window opens or shuts on its first and last lines.
|
|
92
|
+
def step_scanline(line)
|
|
93
|
+
@scanline = (@scanline + 1) % 312
|
|
94
|
+
rows25 = @registers[0x06].anybits?(0x08)
|
|
95
|
+
if line == (rows25 ? 4 : 8)
|
|
96
|
+
@window_open = true
|
|
97
|
+
elsif line == (rows25 ? 204 : 200)
|
|
98
|
+
@window_open = false
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# The line count wraps to 0: the frame's first scanline.
|
|
103
|
+
def end_picture_frame
|
|
104
|
+
@scanline = -1
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# The picture's part of a cycle in +column+.
|
|
108
|
+
def picture_cycle(column)
|
|
109
|
+
return unless column == PAINT_COLUMN
|
|
110
|
+
|
|
111
|
+
paint(WIDTH) if @render
|
|
112
|
+
@painted = 0
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Paints the line up to the pixel a register write lands on.
|
|
116
|
+
def paint_to_write
|
|
117
|
+
return unless @render
|
|
118
|
+
|
|
119
|
+
column = @column
|
|
120
|
+
return if column >= PAINT_COLUMN && column < NEXT_LINE_COLUMN
|
|
121
|
+
|
|
122
|
+
x = ((column * 8) + WRITE_DELAY - FIRST_COUNT) % 456
|
|
123
|
+
x = WIDTH if x > WIDTH
|
|
124
|
+
paint(x)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Paints the line's pixels from where it was left up to +stop+.
|
|
128
|
+
def paint(stop)
|
|
129
|
+
start = @painted
|
|
130
|
+
return if stop <= start
|
|
131
|
+
|
|
132
|
+
@painted = stop
|
|
133
|
+
scanline = @scanline
|
|
134
|
+
@dirty_lines[scanline] = true
|
|
135
|
+
base = scanline * WIDTH
|
|
136
|
+
border = @registers[0x19] & 0x7f
|
|
137
|
+
return fill_span(base, start, stop, border) unless @window_open && @registers[0x06].anybits?(0x10)
|
|
138
|
+
|
|
139
|
+
narrow = !@registers[0x07].anybits?(0x08)
|
|
140
|
+
left = narrow ? NARROW_LEFT : WINDOW_LEFT
|
|
141
|
+
right = narrow ? NARROW_RIGHT : WINDOW_RIGHT
|
|
142
|
+
fill_span(base, start, stop.clamp(..left), border)
|
|
143
|
+
paint_window(base, start.clamp(left..), stop.clamp(..right))
|
|
144
|
+
fill_span(base, start.clamp(right..), stop, border)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def fill_span(base, start, stop, color)
|
|
148
|
+
@display.fill(color, base + start, stop - start) if stop > start
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def paint_window(base, start, stop)
|
|
152
|
+
return if stop <= start
|
|
153
|
+
|
|
154
|
+
background = @registers[0x15] & 0x7f
|
|
155
|
+
return fill_span(base, start, stop, background) unless @pattern_fetches
|
|
156
|
+
|
|
157
|
+
first = WINDOW_LEFT + (@registers[0x07] & 0x07)
|
|
158
|
+
if start < first
|
|
159
|
+
fill_span(base, start, stop.clamp(..first), background)
|
|
160
|
+
start = first
|
|
161
|
+
end
|
|
162
|
+
paint_characters(base, start, stop, first)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Paints the characters' pixels +start+ to +stop+, the first
|
|
166
|
+
# character's first pixel at +first+.
|
|
167
|
+
def paint_characters(base, start, stop, first)
|
|
168
|
+
mode = self.mode
|
|
169
|
+
x = start
|
|
170
|
+
while x < stop
|
|
171
|
+
index = (x - first) >> 3
|
|
172
|
+
origin = first + (index << 3)
|
|
173
|
+
to = (origin + 8).clamp(..stop)
|
|
174
|
+
character_colors(index, mode)
|
|
175
|
+
paint_pixels(base, x, to, origin)
|
|
176
|
+
x = to
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Chips
|
|
5
|
+
class TED
|
|
6
|
+
class Sound
|
|
7
|
+
# The sound's level averaged over each sample of the audio rate,
|
|
8
|
+
# which integrates the SND pin as the board's RC network does, then
|
|
9
|
+
# through the network's coupling capacitor, a one-pole high-pass at
|
|
10
|
+
# 2.7 Hz run at the audio rate.
|
|
11
|
+
#
|
|
12
|
+
# The corner comes from the Plus/4 schematic (310164, sheet 2): SND
|
|
13
|
+
# reaches the video connector through R10 (1K) and C18 (10 uF) into
|
|
14
|
+
# R11 (12K) and R12 (10K) beside Q3's emitter follower. The
|
|
15
|
+
# network's low-pass, C19 (2,700 pF) at about 70 kHz, is far above
|
|
16
|
+
# any audio rate.
|
|
17
|
+
class Output
|
|
18
|
+
HIGH_PASS_HZ = 2.7
|
|
19
|
+
|
|
20
|
+
# Scales the loudest level, both voices high at volume 8, to the
|
|
21
|
+
# top of a signed 16-bit sample.
|
|
22
|
+
SCALE = 32_767.0 / 19_976
|
|
23
|
+
|
|
24
|
+
attr_reader :samples
|
|
25
|
+
|
|
26
|
+
# `level` is the sound's level as the recording starts, which the
|
|
27
|
+
# stage takes as settled.
|
|
28
|
+
def initialize(clock_hz:, rate:, level:)
|
|
29
|
+
@clock_hz = clock_hz
|
|
30
|
+
@rate = rate
|
|
31
|
+
@remainder = 0
|
|
32
|
+
@window = next_window
|
|
33
|
+
@left = @window
|
|
34
|
+
@sum = 0
|
|
35
|
+
@drift = level.to_f
|
|
36
|
+
@high_pass = 1.0 - Math.exp(-2.0 * Math::PI * HIGH_PASS_HZ / rate)
|
|
37
|
+
@samples = []
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Adds `cycles` cycles at `level`, closing every sample they finish.
|
|
41
|
+
def hold_level(level, cycles)
|
|
42
|
+
left = @left
|
|
43
|
+
while cycles >= left
|
|
44
|
+
@sum += level * left
|
|
45
|
+
cycles -= left
|
|
46
|
+
close
|
|
47
|
+
left = @left
|
|
48
|
+
end
|
|
49
|
+
@sum += level * cycles
|
|
50
|
+
@left = left - cycles
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The samples closed since the last drain.
|
|
54
|
+
def drain
|
|
55
|
+
samples = @samples
|
|
56
|
+
@samples = []
|
|
57
|
+
samples
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def close
|
|
63
|
+
average = @sum.fdiv(@window)
|
|
64
|
+
@drift += @high_pass * (average - @drift)
|
|
65
|
+
sample = ((average - @drift) * SCALE).round
|
|
66
|
+
sample = 32_767 if sample > 32_767
|
|
67
|
+
sample = -32_768 if sample < -32_768
|
|
68
|
+
@samples << sample
|
|
69
|
+
@sum = 0
|
|
70
|
+
@window = next_window
|
|
71
|
+
@left = @window
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Each window is a whole number of cycles, and they add up to the
|
|
75
|
+
# clock's cycles over a second.
|
|
76
|
+
def next_window
|
|
77
|
+
@remainder += @clock_hz
|
|
78
|
+
cycles = @remainder / @rate
|
|
79
|
+
@remainder -= cycles * @rate
|
|
80
|
+
cycles
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "badline/chips/ted/sound/output"
|
|
4
|
+
|
|
5
|
+
module Badline
|
|
6
|
+
module Chips
|
|
7
|
+
class TED
|
|
8
|
+
# The TED's sound: two voices, the second a square wave or noise.
|
|
9
|
+
# Voice 1's 10-bit frequency is $FF0E with $FF12 bits 0-1 above it,
|
|
10
|
+
# voice 2's $FF0F with $FF10 bits 0-1. $FF11 holds the volume in bits
|
|
11
|
+
# 0-3 (0 silent, 8 and above the loudest), voice 1 on in bit 4, voice
|
|
12
|
+
# 2's square wave in bit 5 and its noise in bit 6 (the square wins if
|
|
13
|
+
# both are set), and the reload bit in bit 7.
|
|
14
|
+
#
|
|
15
|
+
# Each voice counts once every 4 cycles, and its output toggles every
|
|
16
|
+
# 1023 - x counts for a frequency x, so a square wave of the clock
|
|
17
|
+
# over 8 * (1023 - x). $3FF counts 1,024 between toggles, and $3FE
|
|
18
|
+
# stops the voice after its next toggle, with its output high. A new
|
|
19
|
+
# frequency takes hold at the next toggle. The voices count whether
|
|
20
|
+
# they are on or not.
|
|
21
|
+
#
|
|
22
|
+
# The noise is an 8-bit shift register that shifts at each of voice
|
|
23
|
+
# 2's toggles: bits 1, 4, 5 and 7 XORed go in at bit 0, and bit 1 is
|
|
24
|
+
# the noise's output.
|
|
25
|
+
#
|
|
26
|
+
# While the reload bit is set, the counters hold at their frequencies,
|
|
27
|
+
# the outputs stay high and the noise's register holds all ones, with
|
|
28
|
+
# the noise silent. Writing it clear starts each voice on a full high
|
|
29
|
+
# half-wave. Samples play this way, with the voices held high and the
|
|
30
|
+
# volume as a 9-level D/A converter.
|
|
31
|
+
#
|
|
32
|
+
# The output takes a level from the volume and how many voices are on
|
|
33
|
+
# and high. The levels are measured from xplus4 recordings, whose two
|
|
34
|
+
# voices together come out above twice one.
|
|
35
|
+
#
|
|
36
|
+
# The voices catch up to the current cycle whenever a write lands, the
|
|
37
|
+
# samples are drained or #catch_up! asks, whether or not #record
|
|
38
|
+
# collects the output, so their state doesn't depend on whether the
|
|
39
|
+
# host plays it.
|
|
40
|
+
class Sound
|
|
41
|
+
VOICES = 2
|
|
42
|
+
CYCLES_PER_COUNT = 4
|
|
43
|
+
|
|
44
|
+
# The frequency that stops a voice with its output high.
|
|
45
|
+
HELD = 0x3fe
|
|
46
|
+
|
|
47
|
+
NOISE_TAPS = 0xb2
|
|
48
|
+
NOISE_PERIOD = 255
|
|
49
|
+
|
|
50
|
+
# The output's level for each volume 0-8, with no voice, one voice
|
|
51
|
+
# and both voices high.
|
|
52
|
+
LEVELS = [
|
|
53
|
+
0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
54
|
+
0, 586, 1610, 2634, 3658, 4682, 5706, 6730, 7754,
|
|
55
|
+
0, 1172, 3284, 5526, 7984, 10_658, 13_548, 16_654, 19_976
|
|
56
|
+
].freeze
|
|
57
|
+
|
|
58
|
+
# Far enough ahead that a voice stopped at $3FE, or held by the
|
|
59
|
+
# reload bit, never toggles.
|
|
60
|
+
NEVER = 1 << 60
|
|
61
|
+
|
|
62
|
+
attr_reader :volume, :control
|
|
63
|
+
|
|
64
|
+
# `clock` answers the machine's cycles, `clock_hz` the rate they run at.
|
|
65
|
+
def initialize(clock, clock_hz)
|
|
66
|
+
@clock = clock
|
|
67
|
+
@clock_hz = clock_hz
|
|
68
|
+
@output = nil
|
|
69
|
+
@time = 0
|
|
70
|
+
@frequencies = [0, 0]
|
|
71
|
+
@intervals = [0, 0]
|
|
72
|
+
@phases = [1, 1]
|
|
73
|
+
@next_toggles = [0, 0]
|
|
74
|
+
power_on!
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def model = :mos7360
|
|
78
|
+
|
|
79
|
+
# The power-on state: every register clear, both voices starting on
|
|
80
|
+
# a high half-wave and the noise's register full of ones.
|
|
81
|
+
def power_on!
|
|
82
|
+
now = @clock.cycles
|
|
83
|
+
run_to(now)
|
|
84
|
+
@volume = 0
|
|
85
|
+
@control = 0
|
|
86
|
+
@reloading = false
|
|
87
|
+
@noise = 0xff
|
|
88
|
+
VOICES.times do |voice|
|
|
89
|
+
@frequencies[voice] = 0
|
|
90
|
+
@intervals[voice] = interval_for(0)
|
|
91
|
+
@phases[voice] = 1
|
|
92
|
+
@next_toggles[voice] = now + @intervals[voice]
|
|
93
|
+
end
|
|
94
|
+
@level = level_now
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Starts collecting the output, averaged down to `rate` samples a
|
|
98
|
+
# second, for #drain_samples. `clock_hz` is the rate the cycles are
|
|
99
|
+
# taken to run at.
|
|
100
|
+
def record(rate:, clock_hz: @clock_hz)
|
|
101
|
+
run_to(@clock.cycles)
|
|
102
|
+
@output = Output.new(clock_hz:, rate:, level: @level)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def recording? = !@output.nil?
|
|
106
|
+
|
|
107
|
+
# Runs the voices up to the current cycle.
|
|
108
|
+
def catch_up! = run_to(@clock.cycles)
|
|
109
|
+
|
|
110
|
+
# The samples recorded since the last drain.
|
|
111
|
+
def drain_samples
|
|
112
|
+
output = @output
|
|
113
|
+
return [] if output.nil?
|
|
114
|
+
|
|
115
|
+
run_to(@clock.cycles)
|
|
116
|
+
output.drain
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# A write to $FF0E-$FF12, which takes hold from the next cycle.
|
|
120
|
+
def write(register, value)
|
|
121
|
+
run_to(@clock.cycles + 1)
|
|
122
|
+
case register
|
|
123
|
+
when 0x0e then tune(0, (@frequencies[0] & 0x300) | value)
|
|
124
|
+
when 0x0f then tune(1, (@frequencies[1] & 0x300) | value)
|
|
125
|
+
when 0x10 then tune(1, (@frequencies[1] & 0xff) | ((value & 0x03) << 8))
|
|
126
|
+
when 0x11 then write_control(value)
|
|
127
|
+
when 0x12 then tune(0, (@frequencies[0] & 0xff) | ((value & 0x03) << 8))
|
|
128
|
+
end
|
|
129
|
+
@level = level_now
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def frequency(voice) = @frequencies[voice]
|
|
133
|
+
|
|
134
|
+
def reloading? = @reloading
|
|
135
|
+
|
|
136
|
+
# A voice's square-wave output as of the current cycle, 1 high.
|
|
137
|
+
def phase(voice)
|
|
138
|
+
run_to(@clock.cycles)
|
|
139
|
+
@phases[voice]
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# The noise's shift register as of the current cycle.
|
|
143
|
+
def noise
|
|
144
|
+
run_to(@clock.cycles)
|
|
145
|
+
@noise
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# The output's level as of the current cycle.
|
|
149
|
+
def level
|
|
150
|
+
run_to(@clock.cycles)
|
|
151
|
+
@level
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
private
|
|
155
|
+
|
|
156
|
+
# Runs the voices up to the start of cycle `target`, the output holding
|
|
157
|
+
# each level for the cycles it lasts.
|
|
158
|
+
def run_to(target)
|
|
159
|
+
time = @time
|
|
160
|
+
return if target <= time
|
|
161
|
+
|
|
162
|
+
output = @output
|
|
163
|
+
return skip_to(target) if output.nil?
|
|
164
|
+
|
|
165
|
+
following = next_toggle
|
|
166
|
+
while following < target
|
|
167
|
+
output.hold_level(@level, following - time)
|
|
168
|
+
time = following
|
|
169
|
+
toggle_due(time)
|
|
170
|
+
@level = level_now
|
|
171
|
+
following = next_toggle
|
|
172
|
+
end
|
|
173
|
+
output.hold_level(@level, target - time)
|
|
174
|
+
@time = target
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Moves each voice through its toggles before `target` at once,
|
|
178
|
+
# the noise by as many shifts as voice 2 toggles, less whole turns
|
|
179
|
+
# of its sequence.
|
|
180
|
+
def skip_to(target)
|
|
181
|
+
VOICES.times do |voice|
|
|
182
|
+
due = @next_toggles[voice]
|
|
183
|
+
next if due >= target
|
|
184
|
+
|
|
185
|
+
interval = @intervals[voice]
|
|
186
|
+
toggles = ((target - 1 - due) / interval) + 1
|
|
187
|
+
@next_toggles[voice] = due + (toggles * interval)
|
|
188
|
+
@phases[voice] ^= toggles & 1
|
|
189
|
+
next unless voice == 1
|
|
190
|
+
|
|
191
|
+
(toggles % NOISE_PERIOD).times { shift_noise }
|
|
192
|
+
end
|
|
193
|
+
@level = level_now
|
|
194
|
+
@time = target
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def next_toggle
|
|
198
|
+
toggles = @next_toggles
|
|
199
|
+
following = toggles[0]
|
|
200
|
+
following = toggles[1] if toggles[1] < following
|
|
201
|
+
following
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def toggle_due(time)
|
|
205
|
+
toggles = @next_toggles
|
|
206
|
+
VOICES.times do |voice|
|
|
207
|
+
next unless toggles[voice] == time
|
|
208
|
+
|
|
209
|
+
@phases[voice] ^= 1
|
|
210
|
+
toggles[voice] = time + @intervals[voice]
|
|
211
|
+
shift_noise if voice == 1
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def shift_noise
|
|
216
|
+
noise = @noise
|
|
217
|
+
taps = noise & NOISE_TAPS
|
|
218
|
+
taps ^= taps >> 4
|
|
219
|
+
taps ^= taps >> 2
|
|
220
|
+
taps ^= taps >> 1
|
|
221
|
+
@noise = ((noise << 1) | (taps & 1)) & 0xff
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# The cycles between a voice's toggles at frequency `value`, and
|
|
225
|
+
# never again after the next one at $3FE.
|
|
226
|
+
def interval_for(value)
|
|
227
|
+
return NEVER if value == HELD
|
|
228
|
+
|
|
229
|
+
(((HELD - value) & 0x3ff) + 1) * CYCLES_PER_COUNT
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# A new frequency for `voice`, which takes hold at its next toggle. A
|
|
233
|
+
# voice stopped at $3FE starts counting now.
|
|
234
|
+
def tune(voice, value)
|
|
235
|
+
@frequencies[voice] = value
|
|
236
|
+
@intervals[voice] = interval_for(value)
|
|
237
|
+
return if @reloading || @next_toggles[voice] < NEVER
|
|
238
|
+
|
|
239
|
+
@next_toggles[voice] = @time + @intervals[voice]
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def write_control(value)
|
|
243
|
+
reload = value >= 0x80
|
|
244
|
+
if reload && !@reloading
|
|
245
|
+
hold_voices
|
|
246
|
+
elsif !reload && @reloading
|
|
247
|
+
release_voices
|
|
248
|
+
end
|
|
249
|
+
@reloading = reload
|
|
250
|
+
@control = value
|
|
251
|
+
volume = value & 0x0f
|
|
252
|
+
volume = 8 if volume > 8
|
|
253
|
+
@volume = volume
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def hold_voices
|
|
257
|
+
@phases[0] = 1
|
|
258
|
+
@phases[1] = 1
|
|
259
|
+
@noise = 0xff
|
|
260
|
+
@next_toggles[0] = NEVER
|
|
261
|
+
@next_toggles[1] = NEVER
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def release_voices
|
|
265
|
+
VOICES.times { |voice| @next_toggles[voice] = @time + @intervals[voice] }
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def level_now
|
|
269
|
+
control = @control
|
|
270
|
+
high = 0
|
|
271
|
+
high += 1 if control.anybits?(0x10) && high?(0)
|
|
272
|
+
if control.anybits?(0x20)
|
|
273
|
+
high += 1 if high?(1)
|
|
274
|
+
elsif control.anybits?(0x40) && !@reloading
|
|
275
|
+
high += 1 if @noise.anybits?(0x02) || @frequencies[1] == HELD
|
|
276
|
+
end
|
|
277
|
+
LEVELS[(high * 9) + @volume]
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def high?(voice) = @phases[voice] == 1 || @frequencies[voice] == HELD
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
data/lib/badline/chips/ted.rb
CHANGED
|
@@ -2,14 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
require "badline/chips/ted/timers"
|
|
4
4
|
require "badline/chips/ted/beam"
|
|
5
|
+
require "badline/chips/ted/fetches"
|
|
6
|
+
require "badline/chips/ted/palette"
|
|
7
|
+
require "badline/chips/ted/modes"
|
|
8
|
+
require "badline/chips/ted/picture"
|
|
9
|
+
require "badline/chips/ted/sound"
|
|
10
|
+
require "badline/chips/ted/keyed_state"
|
|
5
11
|
|
|
6
12
|
module Badline
|
|
7
13
|
module Chips
|
|
8
14
|
# The TED of the Commodore 264 series, the 7360 and 8360: its registers
|
|
9
15
|
# at $FF00-$FF1F, the raster, the three timers, the interrupt, the
|
|
10
|
-
# keyboard latch
|
|
11
|
-
#
|
|
12
|
-
# to
|
|
16
|
+
# keyboard latch, the clock it gives the CPU (Beam), what it fetches
|
|
17
|
+
# for the picture (Fetches) and the picture (Picture). Writes to the
|
|
18
|
+
# sound's registers, $FF0E-$FF12, go on to the Sound attached as
|
|
19
|
+
# #sound.
|
|
13
20
|
#
|
|
14
21
|
# The machine clocks it once a single clock cycle, 57 to a line, and
|
|
15
22
|
# asks it then how many cycles the CPU runs in that one (#cpu_slots)
|
|
@@ -23,10 +30,16 @@ module Badline
|
|
|
23
30
|
# and $FF0B the compare's low byte. $FF13 bit 0 reads whether the CPU
|
|
24
31
|
# sees ROM at $8000-$FFFF (#rom_selected=). $FF1C and $FF1D read and
|
|
25
32
|
# set the raster line, and $FF1E reads the horizontal position's top 8
|
|
26
|
-
# bits.
|
|
33
|
+
# bits. $FF1A and $FF1B read and set the character position reload,
|
|
34
|
+
# and $FF1F the row's line in bits 0-2 and in bits 3-6 the blink
|
|
35
|
+
# counter, which counts frames.
|
|
27
36
|
class TED
|
|
28
37
|
include Timers
|
|
29
38
|
include Beam
|
|
39
|
+
include Fetches
|
|
40
|
+
include Palette
|
|
41
|
+
include Modes
|
|
42
|
+
include Picture
|
|
30
43
|
|
|
31
44
|
# The clock a TED is fed, its single clock: the crystal divided by 20
|
|
32
45
|
# on PAL machines and by 16 on NTSC ones (the Plus/4 hardware manual's
|
|
@@ -42,6 +55,10 @@ module Badline
|
|
|
42
55
|
# pen and the three timers.
|
|
43
56
|
SOURCES = 0x5e
|
|
44
57
|
|
|
58
|
+
# The registers the picture reads as it paints: $FF06, $FF07, the
|
|
59
|
+
# cursor and the five colours.
|
|
60
|
+
PICTURE_REGISTERS = 0x03e030c0
|
|
61
|
+
|
|
45
62
|
attr_reader :profile, :interrupt_flags, :interrupt_mask
|
|
46
63
|
|
|
47
64
|
# What answers the keyboard latch: an object with #latch(value).
|
|
@@ -50,10 +67,15 @@ module Badline
|
|
|
50
67
|
# Whether the CPU sees ROM at $8000-$FFFF, which $FF13 bit 0 reads.
|
|
51
68
|
attr_writer :rom_selected
|
|
52
69
|
|
|
70
|
+
# The Sound the sound's registers drive.
|
|
71
|
+
attr_accessor :sound
|
|
72
|
+
|
|
53
73
|
def initialize(profile = PAL)
|
|
54
74
|
@profile = profile
|
|
55
75
|
@keyboard_port = nil
|
|
56
76
|
@rom_selected = true
|
|
77
|
+
@bus = nil
|
|
78
|
+
@sound = nil
|
|
57
79
|
power_on!
|
|
58
80
|
end
|
|
59
81
|
|
|
@@ -63,16 +85,22 @@ module Badline
|
|
|
63
85
|
@interrupt_mask = 0
|
|
64
86
|
@raster_compare = 0
|
|
65
87
|
@key_latch = 0xff
|
|
88
|
+
@blink = 0
|
|
66
89
|
reset_timers
|
|
90
|
+
reset_fetches
|
|
91
|
+
reset_picture
|
|
67
92
|
reset_beam
|
|
68
93
|
end
|
|
69
94
|
|
|
70
|
-
# Counts the timers
|
|
71
|
-
# freeze bit holds them, then decides the
|
|
95
|
+
# Counts the timers, moves the beam on a cycle and makes the cycle's
|
|
96
|
+
# fetches, unless $FF07's freeze bit holds them, then decides the
|
|
97
|
+
# CPU's clock for the cycle.
|
|
72
98
|
def cycle!
|
|
73
99
|
unless frozen?
|
|
74
100
|
count_timers
|
|
75
101
|
move_beam
|
|
102
|
+
fetch(@column)
|
|
103
|
+
picture_cycle(@column)
|
|
76
104
|
end
|
|
77
105
|
decide_clock
|
|
78
106
|
end
|
|
@@ -92,9 +120,12 @@ module Badline
|
|
|
92
120
|
when 0x0a then @interrupt_mask | 0xa0
|
|
93
121
|
when 0x0b then @raster_compare & 0xff
|
|
94
122
|
when 0x13 then (@registers[0x13] & 0xfe) | (@rom_selected ? 1 : 0)
|
|
123
|
+
when 0x1a then 0xfc | (@char_position >> 8)
|
|
124
|
+
when 0x1b then @char_position & 0xff
|
|
95
125
|
when 0x1c then 0xfe | (@line >> 8)
|
|
96
126
|
when 0x1d then @line & 0xff
|
|
97
127
|
when 0x1e then @column << 2
|
|
128
|
+
when 0x1f then 0x80 | (@blink << 3) | @row_line
|
|
98
129
|
else @registers[register]
|
|
99
130
|
end
|
|
100
131
|
end
|
|
@@ -103,6 +134,7 @@ module Badline
|
|
|
103
134
|
register = addr & 0x3f
|
|
104
135
|
return if register >= 0x20
|
|
105
136
|
|
|
137
|
+
paint_to_write if (PICTURE_REGISTERS >> register).anybits?(1)
|
|
106
138
|
@registers[register] = value
|
|
107
139
|
case register
|
|
108
140
|
when 0x00..0x05 then write_timer(register, value)
|
|
@@ -110,8 +142,12 @@ module Badline
|
|
|
110
142
|
when 0x09 then @interrupt_flags &= ~value
|
|
111
143
|
when 0x0a then write_interrupt_mask(value)
|
|
112
144
|
when 0x0b then @raster_compare = (@raster_compare & 0x100) | value
|
|
145
|
+
when 0x0e..0x12 then @sound&.write(register, value)
|
|
146
|
+
when 0x1a then @char_position = (@char_position & 0xff) | ((value & 0x03) << 8)
|
|
147
|
+
when 0x1b then @char_position = (@char_position & 0x300) | value
|
|
113
148
|
when 0x1c then self.line = (@line & 0xff) | ((value & 0x01) << 8)
|
|
114
149
|
when 0x1d then self.line = (@line & 0x100) | value
|
|
150
|
+
when 0x1f then write_row_line(value)
|
|
115
151
|
end
|
|
116
152
|
end
|
|
117
153
|
|
|
@@ -125,6 +161,17 @@ module Badline
|
|
|
125
161
|
irq? ? value | 0x80 : value
|
|
126
162
|
end
|
|
127
163
|
|
|
164
|
+
def write_row_line(value)
|
|
165
|
+
@row_line = value & 0x07
|
|
166
|
+
@blink = (value >> 3) & 0x0f
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# The end of a frame: the fetches' counters start over.
|
|
170
|
+
def end_frame
|
|
171
|
+
end_fetch_frame
|
|
172
|
+
end_picture_frame
|
|
173
|
+
end
|
|
174
|
+
|
|
128
175
|
def write_interrupt_mask(value)
|
|
129
176
|
@interrupt_mask = value & 0x5f
|
|
130
177
|
@raster_compare = (@raster_compare & 0xff) | ((value & 0x01) << 8)
|