badline 0.1.0 → 0.2.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 +167 -0
- data/CLAUDE.md +217 -0
- data/CODE_OF_CONDUCT.md +92 -0
- data/CONTRIBUTING.md +51 -0
- data/README.md +224 -31
- data/doc/pinned-behaviour.md +1117 -0
- data/exe/badline +22 -2
- data/exe/badline-sid +34 -0
- data/lib/badline/address_bus.rb +88 -23
- data/lib/badline/audio/aiff.rb +36 -0
- data/lib/badline/audio/bare_player.rb +129 -0
- data/lib/badline/audio/cli.rb +133 -0
- data/lib/badline/audio/console.rb +75 -0
- data/lib/badline/audio/jukebox.rb +82 -0
- data/lib/badline/audio/machine_player.rb +57 -0
- data/lib/badline/audio/options.rb +137 -0
- data/lib/badline/audio/pcm_writer.rb +57 -0
- data/lib/badline/audio/playback.rb +108 -0
- data/lib/badline/audio/renderer.rb +85 -0
- data/lib/badline/audio/sdl_sink.rb +102 -0
- data/lib/badline/audio/wav.rb +23 -0
- data/lib/badline/audio.rb +14 -0
- data/lib/badline/cartridge/action_replay.rb +111 -0
- data/lib/badline/cartridge/atomic_power.rb +24 -0
- data/lib/badline/cartridge/bank.rb +81 -0
- data/lib/badline/cartridge/comal80.rb +48 -0
- data/lib/badline/cartridge/dinamic.rb +34 -0
- data/lib/badline/cartridge/easy_flash.rb +122 -0
- data/lib/badline/cartridge/epyx_fastload.rb +71 -0
- data/lib/badline/cartridge/final_cartridge3.rb +77 -0
- data/lib/badline/cartridge/flash.rb +265 -0
- data/lib/badline/cartridge/freezer.rb +31 -0
- data/lib/badline/cartridge/fun_play.rb +38 -0
- data/lib/badline/cartridge/g_mod2.rb +62 -0
- data/lib/badline/cartridge/game_system.rb +40 -0
- data/lib/badline/cartridge/kcs_power.rb +72 -0
- data/lib/badline/cartridge/mach5.rb +34 -0
- data/lib/badline/cartridge/magic_desk.rb +6 -3
- data/lib/badline/cartridge/ocean.rb +11 -12
- data/lib/badline/cartridge/pagefox.rb +56 -0
- data/lib/badline/cartridge/retro_replay.rb +276 -0
- data/lib/badline/cartridge/rex_utility.rb +33 -0
- data/lib/badline/cartridge/rgcd.rb +48 -0
- data/lib/badline/cartridge/simons_basic.rb +40 -0
- data/lib/badline/cartridge/standard.rb +3 -19
- data/lib/badline/cartridge/super_games.rb +36 -0
- data/lib/badline/cartridge/westermann.rb +34 -0
- data/lib/badline/cartridge/zaxxon.rb +50 -0
- data/lib/badline/cartridge.rb +131 -15
- data/lib/badline/cia/interrupt_register.rb +88 -0
- data/lib/badline/cia/serial.rb +219 -0
- data/lib/badline/cia/timer.rb +43 -31
- data/lib/badline/cia.rb +109 -74
- data/lib/badline/color_memory.rb +14 -5
- data/lib/badline/computer.rb +68 -8
- data/lib/badline/control_ports.rb +54 -9
- data/lib/badline/cpu/addressing.rb +131 -0
- data/lib/badline/cpu/microcode.rb +171 -0
- data/lib/badline/cpu/operations.rb +88 -0
- data/lib/badline/cpu/stack_operations.rb +82 -0
- data/lib/badline/cpu.rb +108 -167
- data/lib/badline/cycleable.rb +3 -0
- data/lib/badline/datasette.rb +81 -0
- data/lib/badline/debug_register.rb +28 -0
- data/lib/badline/gui/application.rb +79 -18
- data/lib/badline/gui/gamepads.rb +97 -0
- data/lib/badline/gui/joy_map.rb +15 -7
- data/lib/badline/gui.rb +1 -0
- data/lib/badline/input/mouse1351.rb +41 -0
- data/lib/badline/input/paddles.rb +36 -0
- data/lib/badline/input.rb +4 -0
- data/lib/badline/instruction_set/arithmetic.rb +16 -21
- data/lib/badline/instruction_set/bitwise.rb +17 -22
- data/lib/badline/instruction_set/branch.rb +20 -19
- data/lib/badline/instruction_set/flag.rb +7 -7
- data/lib/badline/instruction_set/illegal.rb +14 -22
- data/lib/badline/instruction_set/inc_dec.rb +8 -10
- data/lib/badline/instruction_set/stack.rb +5 -57
- data/lib/badline/instruction_set/transfer.rb +9 -9
- data/lib/badline/instruction_set.rb +9 -31
- data/lib/badline/interrupts.rb +104 -0
- data/lib/badline/joystick.rb +9 -9
- data/lib/badline/kernal_trap/channel.rb +33 -0
- data/lib/badline/kernal_trap/drive.rb +239 -0
- data/lib/badline/kernal_trap/file.rb +4 -24
- data/lib/badline/kernal_trap/load.rb +2 -2
- data/lib/badline/kernal_trap/routine.rb +34 -0
- data/lib/badline/kernal_trap/save.rb +1 -1
- data/lib/badline/kernal_trap/serial.rb +139 -0
- data/lib/badline/kernal_trap.rb +4 -0
- data/lib/badline/keyboard.rb +40 -16
- data/lib/badline/media.rb +48 -10
- data/lib/badline/roms/eapi/LICENSE.md +15 -0
- data/lib/badline/roms/eapi/README +41 -0
- data/lib/badline/roms/eapi/eapi-am29f040-14 +0 -0
- data/lib/badline/sid/decimator.rb +46 -0
- data/lib/badline/sid/envelope.rb +232 -0
- data/lib/badline/sid/filter.rb +213 -0
- data/lib/badline/sid/voice.rb +61 -0
- data/lib/badline/sid/waveform/combined.rb +126 -0
- data/lib/badline/sid/waveform/fast_forward.rb +105 -0
- data/lib/badline/sid/waveform/noise_writeback.rb +82 -0
- data/lib/badline/sid/waveform.rb +289 -0
- data/lib/badline/sid.rb +274 -7
- data/lib/badline/status.rb +32 -27
- data/lib/badline/storage/crt_file.rb +18 -2
- data/lib/badline/storage/disk_image.rb +52 -5
- data/lib/badline/storage/host_directory.rb +18 -4
- data/lib/badline/storage/sid_file.rb +274 -0
- data/lib/badline/storage/song_lengths.rb +65 -0
- data/lib/badline/storage/t64.rb +67 -0
- data/lib/badline/storage/tap.rb +68 -0
- data/lib/badline/storage.rb +19 -0
- data/lib/badline/time_of_day.rb +105 -52
- data/lib/badline/version.rb +1 -1
- data/lib/badline/vic/bank.rb +14 -6
- data/lib/badline/vic/border_mask.rb +76 -0
- data/lib/badline/vic/collisions.rb +104 -0
- data/lib/badline/vic/color_patches.rb +33 -0
- data/lib/badline/vic/display_state.rb +92 -29
- data/lib/badline/vic/graphics_mode.rb +78 -53
- data/lib/badline/vic/graphics_shifter.rb +140 -0
- data/lib/badline/vic/register_log.rb +63 -0
- data/lib/badline/vic/registers.rb +3 -0
- data/lib/badline/vic/sequencer.rb +163 -137
- data/lib/badline/vic/sequencer_output.rb +116 -0
- data/lib/badline/vic/sprite/internal_bus.rb +49 -0
- data/lib/badline/vic/sprite/shifter.rb +129 -0
- data/lib/badline/vic/sprite.rb +275 -65
- data/lib/badline/vic/sprites.rb +220 -58
- data/lib/badline/vic.rb +373 -49
- data/lib/badline.rb +5 -0
- data/vendor/.gitkeep +0 -0
- metadata +103 -4
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class SID
|
|
5
|
+
class Waveform
|
|
6
|
+
# The shapes the chip puts out when more than one waveform is selected.
|
|
7
|
+
#
|
|
8
|
+
# Selecting several shapers connects their outputs to the same twelve
|
|
9
|
+
# DAC lines. Each line settles somewhere between the levels the
|
|
10
|
+
# shapers drive it to, and couples to the lines around it through the
|
|
11
|
+
# resistances between them, so a bit reads high only when enough of
|
|
12
|
+
# its own drive and its neighbours' is high. Modelled here as a
|
|
13
|
+
# threshold on a weighted sum: each selected shaper adds its bit times
|
|
14
|
+
# its strength, a high pulse adds its strength to every line, and the
|
|
15
|
+
# neighbours contribute with one weight per distance below and one per
|
|
16
|
+
# distance above.
|
|
17
|
+
#
|
|
18
|
+
# A low pulse grounds every line.
|
|
19
|
+
#
|
|
20
|
+
# The parameters come from bin/sidwavefit, fitted against OSC3 dumps of
|
|
21
|
+
# real chips in SID/resid-test (oscsample0/1-6581/8580wf30/50/60/70).
|
|
22
|
+
# The tables are built from them once per chip model.
|
|
23
|
+
module Combined
|
|
24
|
+
BITS = 12
|
|
25
|
+
|
|
26
|
+
# Per chip and selection (triangle $1, sawtooth $2, pulse $4): the
|
|
27
|
+
# triangle's strength next to a sawtooth (the sawtooth, or a lone
|
|
28
|
+
# triangle, has strength 1), the pulse's strength, the threshold as
|
|
29
|
+
# a fraction of the full drive, then the weights of the lines one to
|
|
30
|
+
# eleven bits below and one to eleven bits above.
|
|
31
|
+
PARAMETERS = {
|
|
32
|
+
mos6581: {
|
|
33
|
+
0x3 => [0.522, 6.4087, 0.9062,
|
|
34
|
+
0.5304, 0.278, 0.1473, 0.0773, 0.0, 0.0184, 0.0113, 0.0251, 0.0594, 0.2001, 0.2001,
|
|
35
|
+
0.4103, 0.1756, 0.0687, 0.0282, 0.0115, 0.0047, 0.0031, 0.2001, 0.2001, 0.2001, 0.2001],
|
|
36
|
+
0x5 => [5.871, 0.0, 0.7485,
|
|
37
|
+
0.6522, 0.5913, 0.5404, 0.4828, 0.428, 0.361, 0.2955, 0.2453, 0.1558, 0.0501, 0.0016,
|
|
38
|
+
0.7106, 0.6667, 0.6426, 0.6376, 0.5761, 0.6639, 0.7193, 0.4728, 0.4306, 0.392, 0.357],
|
|
39
|
+
0x6 => [3.8329, 4.7751, 0.9565,
|
|
40
|
+
0.815, 0.8302, 0.8204, 0.8107, 0.8511, 0.8416, 0.8322, 0.8728, 0.8636, 0.8545, 0.8954,
|
|
41
|
+
0.8071, 0.7979, 0.7465, 0.7771, 0.7643, 0.7572, 0.7054, 0.4585, 0.4159, 0.3773, 0.3421],
|
|
42
|
+
0x7 => [5.5703, 0.2543, 0.9575,
|
|
43
|
+
0.7177, 0.515, 0.3696, 0.2653, 0.1904, 0.1367, 0.0606, 0.0953, 0.0506, 0.0363, 0.2261,
|
|
44
|
+
0.6347, 0.286, 0.203, 0.0817, 0.0438, 0.1235, 0.2001, 0.2001, 0.2001, 0.2001, 0.2001]
|
|
45
|
+
},
|
|
46
|
+
mos8580: {
|
|
47
|
+
0x3 => [0.2223, 7.7741, 0.8869,
|
|
48
|
+
0.4626, 0.2025, 0.0911, 0.041, 0.0185, 0.0, 0.0008, 0.0, 0.0, 0.0127, 0.0063,
|
|
49
|
+
0.5633, 0.311, 0.2099, 0.0881, 0.0255, 0.0, 0.0431, 0.2002, 0.2002, 0.2002, 0.2002],
|
|
50
|
+
0x5 => [3.1022, 1.1282, 0.9157,
|
|
51
|
+
0.2731, 0.2748, 0.0031, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
|
|
52
|
+
0.6228, 0.7375, 0.6094, 0.6667, 0.4381, 0.4714, 0.3648, 0.2669, 0.2264, 0.2001, 0.2001],
|
|
53
|
+
0x6 => [0.9477, 0.0012, 0.7837,
|
|
54
|
+
0.2565, 0.2289, 0.1095, 0.0805, 0.0282, 0.0281, 0.0, 0.0, 0.0, 0.0, 0.0063,
|
|
55
|
+
0.3382, 0.3131, 0.3129, 0.2898, 0.233, 0.1762, 0.2135, 0.2002, 0.2002, 0.2002, 0.2002],
|
|
56
|
+
0x7 => [1.219, 1.1111, 0.9473,
|
|
57
|
+
0.3829, 0.1466, 0.0577, 0.0215, 0.0082, 0.0032, 0.0, 0.0, 0.0, 0.0, 0.0,
|
|
58
|
+
0.7273, 0.6845, 0.566, 0.4807, 0.6, 0.1954, 0.4651, 0.2194, 0.2002, 0.2002, 0.2002]
|
|
59
|
+
}
|
|
60
|
+
}.freeze
|
|
61
|
+
|
|
62
|
+
# A compiled parameter set: the weight matrix, and per line the sum
|
|
63
|
+
# of its weights and the threshold against the full drive.
|
|
64
|
+
class Network
|
|
65
|
+
attr_reader :triangle_strength, :pulse_strength
|
|
66
|
+
|
|
67
|
+
def initialize(params)
|
|
68
|
+
@triangle_strength, @pulse_strength, threshold = params[0, 3]
|
|
69
|
+
below = [1.0] + params[3, BITS - 1]
|
|
70
|
+
above = [1.0] + params[3 + BITS - 1, BITS - 1]
|
|
71
|
+
@weights = Array.new(BITS) do |i|
|
|
72
|
+
Array.new(BITS) { |j| j <= i ? below[i - j] : above[j - i] }.freeze
|
|
73
|
+
end.freeze
|
|
74
|
+
@norms = @weights.map(&:sum).freeze
|
|
75
|
+
@threshold = threshold
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# `drives` holds [bits, strength] for each selected shaper; a high
|
|
79
|
+
# pulse is passed separately, as it drives every line alike.
|
|
80
|
+
def shape(drives, pulse)
|
|
81
|
+
total = drives.sum { |_, strength| strength }
|
|
82
|
+
total += @pulse_strength if pulse
|
|
83
|
+
out = 0
|
|
84
|
+
BITS.times do |i|
|
|
85
|
+
level = pulse ? @pulse_strength * @norms[i] : 0.0
|
|
86
|
+
weights = @weights[i]
|
|
87
|
+
drives.each do |bits, strength|
|
|
88
|
+
BITS.times { |j| level += weights[j] * strength if bits[j] == 1 }
|
|
89
|
+
end
|
|
90
|
+
out |= 1 << i if level >= @threshold * @norms[i] * total
|
|
91
|
+
end
|
|
92
|
+
out
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
module_function
|
|
97
|
+
|
|
98
|
+
# The pulse-high shapes of the four combinations without noise.
|
|
99
|
+
def tables(model)
|
|
100
|
+
@tables ||= {}
|
|
101
|
+
@tables[model] ||= PARAMETERS.fetch(model).to_h do |selected, params|
|
|
102
|
+
[selected, table(selected, params)]
|
|
103
|
+
end.freeze
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# One combination's shapes, indexed by the phase (the sawtooth), or
|
|
107
|
+
# for triangle and pulse by the triangle. A triangle combined with a
|
|
108
|
+
# sawtooth is never folded, so it is the phase shifted left one bit.
|
|
109
|
+
def table(selected, params)
|
|
110
|
+
network = Network.new(params)
|
|
111
|
+
Array.new(1 << BITS) do |index|
|
|
112
|
+
triangle = selected == 0x5 ? index : (index << 1) & 0xfff
|
|
113
|
+
network.shape(drives(selected, network, index, triangle), selected.anybits?(0x4))
|
|
114
|
+
end.freeze
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def drives(selected, network, saw, triangle)
|
|
118
|
+
drives = []
|
|
119
|
+
drives << [saw, 1.0] if selected.anybits?(0x2)
|
|
120
|
+
drives << [triangle, selected.anybits?(0x2) ? network.triangle_strength : 1.0] if selected.anybits?(0x1)
|
|
121
|
+
drives
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class SID
|
|
5
|
+
class Waveform
|
|
6
|
+
# Running an oscillator many cycles in one step, for the catch-ups that
|
|
7
|
+
# fast-forward the voices between whole cycles.
|
|
8
|
+
module FastForward
|
|
9
|
+
# Runs `cycles` cycles as #cycle! would, for an oscillator that is not
|
|
10
|
+
# feeding back. What those cycles latch onto the output is left to the
|
|
11
|
+
# whole cycle that follows, which latches over it.
|
|
12
|
+
def fast_forward(cycles)
|
|
13
|
+
drain_floating(cycles) if @selected.zero?
|
|
14
|
+
if @test
|
|
15
|
+
bleed_shift_register(cycles)
|
|
16
|
+
else
|
|
17
|
+
while @shift_pipeline.positive? && cycles.positive?
|
|
18
|
+
advance
|
|
19
|
+
cycles -= 1
|
|
20
|
+
end
|
|
21
|
+
skip_ahead(cycles) if cycles.positive?
|
|
22
|
+
compare_pulse(((@accumulator - @frequency) & 0xffffff) >> 12)
|
|
23
|
+
end
|
|
24
|
+
@stale = true
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# The cycle on which the accumulator MSB next rises, counting the next
|
|
28
|
+
# one as 1, or nil if it never will. The test bit leaves the last
|
|
29
|
+
# rise standing for as long as it is held.
|
|
30
|
+
def cycles_to_msb_rise
|
|
31
|
+
return (@msb_rising ? 1 : nil) if @test
|
|
32
|
+
return if @frequency.zero?
|
|
33
|
+
|
|
34
|
+
distance = (MSB - @accumulator) & 0xffffff
|
|
35
|
+
distance = 0x1000000 if distance.zero?
|
|
36
|
+
(distance + @frequency - 1) / @frequency
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# A combined waveform with sawtooth (on the 6581) or noise in the mix
|
|
40
|
+
# writes back into the oscillator every cycle, so it cannot be
|
|
41
|
+
# fast-forwarded. Triangle and pulse alone write nothing back.
|
|
42
|
+
def feedback?
|
|
43
|
+
combined_waveform? && (@selected.anybits?(0x8) || (@topbit_feedback && @selected.anybits?(0x2)))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Whether a catch-up has to run this oscillator a cycle at a time.
|
|
47
|
+
# While synthesizing, every combined waveform does, so the filter
|
|
48
|
+
# integrates it a cycle at a time.
|
|
49
|
+
def stepped?(synthesizing) = synthesizing ? combined_waveform? : feedback?
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def combined_waveform? = !@test && combined?(@selected)
|
|
54
|
+
|
|
55
|
+
# Advances an accumulator with no shift in flight. Each rise of bit 19
|
|
56
|
+
# shifts the LFSR two cycles on, so a rise in the last two cycles
|
|
57
|
+
# leaves its shift pending for the cycles after.
|
|
58
|
+
def skip_ahead(cycles)
|
|
59
|
+
previous = @accumulator
|
|
60
|
+
@accumulator += @frequency * cycles
|
|
61
|
+
rises = bit19_rises(previous, @accumulator)
|
|
62
|
+
if rises.positive?
|
|
63
|
+
@shift_pipeline = pipeline_after(cycles)
|
|
64
|
+
rises -= 1 if @shift_pipeline.positive?
|
|
65
|
+
rises.times { shift_noise }
|
|
66
|
+
end
|
|
67
|
+
@accumulator &= 0xffffff
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Bit 19 rises at most once a cycle, on each odd multiple of 2^19,
|
|
71
|
+
# and at most once in any eight.
|
|
72
|
+
def bit19_rises(from, to) = ((to + 0x80000) >> 20) - ((from + 0x80000) >> 20)
|
|
73
|
+
|
|
74
|
+
# How far a rise on one of the last two cycles has come.
|
|
75
|
+
def pipeline_after(cycles)
|
|
76
|
+
last = @accumulator - @frequency
|
|
77
|
+
return 2 if bit19_rises(last, @accumulator).positive?
|
|
78
|
+
return 1 if cycles > 1 && bit19_rises(last - @frequency, last).positive?
|
|
79
|
+
|
|
80
|
+
0
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def drain_floating(cycles)
|
|
84
|
+
return unless @floating_ttl.positive?
|
|
85
|
+
|
|
86
|
+
@floating_ttl -= cycles
|
|
87
|
+
return if @floating_ttl.positive?
|
|
88
|
+
|
|
89
|
+
@floating_ttl = 0
|
|
90
|
+
@floating = 0x000
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def bleed_shift_register(cycles = 1)
|
|
94
|
+
return unless @shift_register_reset.positive?
|
|
95
|
+
|
|
96
|
+
@shift_register_reset -= cycles
|
|
97
|
+
return if @shift_register_reset.positive?
|
|
98
|
+
|
|
99
|
+
@shift_register_reset = 0
|
|
100
|
+
@shift_register = SHIFT_REGISTER_RESET
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class SID
|
|
5
|
+
class Waveform
|
|
6
|
+
# Noise combined with other waveforms: the output lines write back into
|
|
7
|
+
# the LFSR, and what they write is not always what OSC3 reads.
|
|
8
|
+
module NoiseWriteback
|
|
9
|
+
# The LFSR bits the noise shaper reads, paired with the output bit each
|
|
10
|
+
# one drives (Dag Lem's diagram in SID/noise-reset_new). A combined
|
|
11
|
+
# waveform drives the same lines, so a bit held low there is written
|
|
12
|
+
# back into the register.
|
|
13
|
+
NOISE_TAPS = [[0x100000, 0x800], [0x040000, 0x400], [0x004000, 0x200],
|
|
14
|
+
[0x000800, 0x100], [0x000200, 0x080], [0x000020, 0x040],
|
|
15
|
+
[0x000004, 0x020], [0x000001, 0x010]].freeze
|
|
16
|
+
|
|
17
|
+
# Pulse+noise with the pulse high: the noise lines next to the four
|
|
18
|
+
# below them, which nothing drives, are pulled low. The OSC3 read and
|
|
19
|
+
# the LFSR writeback see the same lines against different thresholds,
|
|
20
|
+
# so they lose a different number of bits. The 8580's pair is the one
|
|
21
|
+
# that reproduces SID/wf12nsr's pulse+noise row (it reads $f8, and the
|
|
22
|
+
# writeback leaves $fc behind), and SID/wb_testsuite's C->9 and C->E
|
|
23
|
+
# rows need that same $fc written at release. On the 6581 the read is
|
|
24
|
+
# the $fc of SID/wf12nsr's readme (VICE bug #1037), and the writeback
|
|
25
|
+
# pulls nothing: wb_testsuite's 8/9/A/B->C rows run pulse+noise and
|
|
26
|
+
# leave the register alone.
|
|
27
|
+
PULSE_NOISE_READ = { mos6581: 0xfc0, mos8580: 0xf80 }.freeze
|
|
28
|
+
PULSE_NOISE_WRITE = { mos6581: 0xff0, mos8580: 0xfc0 }.freeze
|
|
29
|
+
|
|
30
|
+
# What a 6581 writes back when the test bit falls with only pulse+noise
|
|
31
|
+
# common to the old waveform and the new one (SID/wb_testsuite's D->C,
|
|
32
|
+
# E->C, F->C and D->E rows).
|
|
33
|
+
PULSE_NOISE_RELEASE = 0xf80
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# What the lines write into the LFSR, against a lower threshold than
|
|
38
|
+
# OSC3 reads them by: a line left high between two low ones reads high
|
|
39
|
+
# (SID/noisewriteback's noise_writeback_test2) but is written low
|
|
40
|
+
# (SID/wf12nsr's noise+triangle and noise+sawtooth rows).
|
|
41
|
+
def write_back(selected, value)
|
|
42
|
+
return value & ((value << 1) | (value >> 1)) unless selected == 0xc
|
|
43
|
+
return value if value.zero?
|
|
44
|
+
|
|
45
|
+
noise & @pulse_noise_write
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def write_shift_register(value)
|
|
49
|
+
NOISE_TAPS.each { |bit, line| @shift_register &= ~bit if value.nobits?(line) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Releasing the test bit finishes the shift it interrupted: the old
|
|
53
|
+
# waveform's output may be written back first, then a bit clocks in
|
|
54
|
+
# over the forced-high bit 22. On the 6581, a change that keeps only
|
|
55
|
+
# pulse+noise of the old waveform writes the lowest noise lines low.
|
|
56
|
+
def release_test(previous)
|
|
57
|
+
if @topbit_feedback && previous != 0xc && (previous & @selected) == 0xc
|
|
58
|
+
write_shift_register(noise & PULSE_NOISE_RELEASE)
|
|
59
|
+
elsif release_writes_back?(previous, @selected)
|
|
60
|
+
write_shift_register(write_back(previous, shape(previous)))
|
|
61
|
+
end
|
|
62
|
+
shift_noise(1)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Which waveform changes write the old output back as the test bit
|
|
66
|
+
# falls (SID/wb_testsuite, after libresidfp's do_writeback). Noise has
|
|
67
|
+
# to have been combined before and still be selected after. Dropping
|
|
68
|
+
# to noise alone writes nothing back unless all four were selected,
|
|
69
|
+
# nor does changing to pulse+noise or from pulse+noise to
|
|
70
|
+
# sawtooth+noise, nor, on the 6581, trading triangle for sawtooth or
|
|
71
|
+
# back.
|
|
72
|
+
def release_writes_back?(previous, selected)
|
|
73
|
+
return false if previous <= 0x8 || selected < 0x8
|
|
74
|
+
return previous == 0xf if selected == 0x8
|
|
75
|
+
return false if selected == 0xc || (previous == 0xc && selected == 0xa)
|
|
76
|
+
|
|
77
|
+
!(@topbit_feedback && [previous & 0x3, selected & 0x3].sort == [0x1, 0x2])
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "badline/sid/waveform/combined"
|
|
4
|
+
require "badline/sid/waveform/fast_forward"
|
|
5
|
+
require "badline/sid/waveform/noise_writeback"
|
|
6
|
+
|
|
7
|
+
module Badline
|
|
8
|
+
class SID
|
|
9
|
+
# Waveform generator: a 24-bit phase accumulator and the shapers that
|
|
10
|
+
# read it.
|
|
11
|
+
#
|
|
12
|
+
# The accumulator advances by the 16-bit frequency every cycle, so every
|
|
13
|
+
# waveform is a pure function of the phase. Bit 23 drives hard sync and
|
|
14
|
+
# the triangle fold, bit 19 clocks the noise LFSR.
|
|
15
|
+
#
|
|
16
|
+
# Selecting more than one waveform shorts the shapers onto the lines the
|
|
17
|
+
# oscillator itself reads back, so a combined waveform is not only a
|
|
18
|
+
# shape: a low top bit reaches the accumulator MSB through the sawtooth
|
|
19
|
+
# switch, and noise in the mix is written back into the LFSR. The shape
|
|
20
|
+
# itself comes from Combined's model of the lines pulling on each other.
|
|
21
|
+
#
|
|
22
|
+
# The pulse comparator's output reaches the lines a cycle after the
|
|
23
|
+
# accumulator it compared.
|
|
24
|
+
#
|
|
25
|
+
# With nothing selected the DAC input floats, holding the last value
|
|
26
|
+
# driven onto it until the charge drains away.
|
|
27
|
+
class Waveform
|
|
28
|
+
include FastForward
|
|
29
|
+
include NoiseWriteback
|
|
30
|
+
|
|
31
|
+
# All bits high at power up, odd ones stored inverted (SID/oscinit).
|
|
32
|
+
POWER_ON_ACCUMULATOR = 0x555555
|
|
33
|
+
|
|
34
|
+
# Holding the test bit bleeds every LFSR bit high; the power-on state is
|
|
35
|
+
# one shift further on, which is what releasing the bit leaves behind.
|
|
36
|
+
SHIFT_REGISTER_RESET = 0x7fffff
|
|
37
|
+
NOISE_SEED = 0x7ffffe
|
|
38
|
+
|
|
39
|
+
# Cycles the test bit needs to bleed the LFSR high. SID/bitfade's
|
|
40
|
+
# delaynoise measures ~$950000 on a real 8580. Nothing measures the
|
|
41
|
+
# 6581: SID/resid-test's oscsample dumps show it holding its bits
|
|
42
|
+
# through the ~$8800 cycles between runs, and the slow reset
|
|
43
|
+
# (SID/noise-reset_old) allows it a second.
|
|
44
|
+
SHIFT_REGISTER_RESET_DELAY = { mos6581: 0x80000, mos8580: 0x950000 }.freeze
|
|
45
|
+
|
|
46
|
+
# Cycles the floating DAC input holds its charge (SID/osc3-wave0).
|
|
47
|
+
FLOATING_OUTPUT_TTL = 0x4000
|
|
48
|
+
|
|
49
|
+
MSB = 0x800000
|
|
50
|
+
|
|
51
|
+
attr_accessor :sync_source, :sync_dest
|
|
52
|
+
attr_reader :accumulator, :shift_register, :frequency, :pulse_width, :pulse
|
|
53
|
+
|
|
54
|
+
def initialize(model: :mos6581)
|
|
55
|
+
@topbit_feedback = model != :mos8580
|
|
56
|
+
@tri_saw_delay = model == :mos8580
|
|
57
|
+
@pulse_noise_read = PULSE_NOISE_READ.fetch(model)
|
|
58
|
+
@pulse_noise_write = PULSE_NOISE_WRITE.fetch(model)
|
|
59
|
+
@combined = Combined.tables(model)
|
|
60
|
+
@shift_register_reset_delay = SHIFT_REGISTER_RESET_DELAY.fetch(model)
|
|
61
|
+
@accumulator = POWER_ON_ACCUMULATOR
|
|
62
|
+
@sync_source = @sync_dest = self
|
|
63
|
+
reset!
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# The RES line clears the registers and reseeds the LFSR, but leaves
|
|
67
|
+
# the accumulator alone (SID/oscinit).
|
|
68
|
+
def reset!
|
|
69
|
+
@delayed_sawtooth = @delayed_triangle = 0x000
|
|
70
|
+
@shift_register = NOISE_SEED
|
|
71
|
+
@shift_register_reset = 0
|
|
72
|
+
@shift_pipeline = 0
|
|
73
|
+
@frequency = 0x0000
|
|
74
|
+
@pulse_width = 0x000
|
|
75
|
+
@selected = 0x0
|
|
76
|
+
@test = false
|
|
77
|
+
@ring = false
|
|
78
|
+
@sync = false
|
|
79
|
+
@msb_rising = false
|
|
80
|
+
@floating = 0x000
|
|
81
|
+
@floating_ttl = 0
|
|
82
|
+
@pulse = 0xfff
|
|
83
|
+
@output = 0x000
|
|
84
|
+
@stale = true
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def sync? = @sync
|
|
88
|
+
def msb_rising? = @msb_rising
|
|
89
|
+
|
|
90
|
+
def frequency_low=(value)
|
|
91
|
+
@frequency = (@frequency & 0xff00) | value
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def frequency_high=(value)
|
|
95
|
+
@frequency = (value << 8) | (@frequency & 0x00ff)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def pulse_width_low=(value)
|
|
99
|
+
@pulse_width = (@pulse_width & 0xf00) | value
|
|
100
|
+
@stale = true
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def pulse_width_high=(value)
|
|
104
|
+
@pulse_width = ((value & 0x0f) << 8) | (@pulse_width & 0x0ff)
|
|
105
|
+
@stale = true
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# The test bit clears the accumulator and stalls the LFSR half way
|
|
109
|
+
# through a shift, holding bit 22 high while its bits bleed up.
|
|
110
|
+
def control=(value)
|
|
111
|
+
previous = @selected
|
|
112
|
+
@selected = (value >> 4) & 0x0f
|
|
113
|
+
@ring = value.anybits?(0x04)
|
|
114
|
+
@sync = value.anybits?(0x02)
|
|
115
|
+
test = value.anybits?(0x08)
|
|
116
|
+
if test
|
|
117
|
+
@accumulator = 0x000000
|
|
118
|
+
@pulse = 0xfff
|
|
119
|
+
@shift_pipeline = 0
|
|
120
|
+
@shift_register_reset = @shift_register_reset_delay unless @test
|
|
121
|
+
elsif @test
|
|
122
|
+
release_test(previous)
|
|
123
|
+
end
|
|
124
|
+
@test = test
|
|
125
|
+
@stale = true
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def cycle!
|
|
129
|
+
latch_output
|
|
130
|
+
compare_pulse
|
|
131
|
+
@test ? bleed_shift_register : advance
|
|
132
|
+
@stale = true
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Hard sync resets the next voice's accumulator when ours wraps. A
|
|
136
|
+
# source that is itself synced on the cycle its own MSB rises does not
|
|
137
|
+
# pass the sync on.
|
|
138
|
+
def synchronize!
|
|
139
|
+
return unless @msb_rising && @sync_dest.sync? &&
|
|
140
|
+
!(@sync && @sync_source.msb_rising?)
|
|
141
|
+
|
|
142
|
+
@sync_dest.reset_accumulator
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def reset_accumulator
|
|
146
|
+
@accumulator = 0x000000
|
|
147
|
+
@stale = true
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Held for the rest of the cycle once computed, so the filter, OSC3 and
|
|
151
|
+
# the oscillator's own feedback all see one value per cycle.
|
|
152
|
+
def output
|
|
153
|
+
return @output unless @stale
|
|
154
|
+
|
|
155
|
+
@stale = false
|
|
156
|
+
@output = @selected.zero? ? @floating : shape(@selected)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# What OSC3 reads. The 8580 delays the triangle and sawtooth shapers
|
|
160
|
+
# by half a cycle, which OSC3 latches as a whole cycle late; pulse and
|
|
161
|
+
# noise still reach the lines on time.
|
|
162
|
+
def osc3
|
|
163
|
+
return output unless @tri_saw_delay && @selected.anybits?(0x3)
|
|
164
|
+
|
|
165
|
+
shape(@selected, @delayed_sawtooth, @delayed_triangle)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def sawtooth = @accumulator >> 12
|
|
169
|
+
|
|
170
|
+
# The upper half of the ramp is folded back down, shifted left one bit
|
|
171
|
+
# to keep the full 12-bit range.
|
|
172
|
+
def triangle(selected = @selected)
|
|
173
|
+
folded = inverted?(selected) ? @accumulator ^ 0xffffff : @accumulator
|
|
174
|
+
(folded >> 11) & 0xfff
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Eight taps off the 23-bit LFSR, gathered into a 12-bit sample.
|
|
178
|
+
def noise
|
|
179
|
+
((@shift_register & 0x100000) >> 9) |
|
|
180
|
+
((@shift_register & 0x040000) >> 8) |
|
|
181
|
+
((@shift_register & 0x004000) >> 5) |
|
|
182
|
+
((@shift_register & 0x000800) >> 3) |
|
|
183
|
+
((@shift_register & 0x000200) >> 2) |
|
|
184
|
+
((@shift_register & 0x000020) << 1) |
|
|
185
|
+
((@shift_register & 0x000004) << 3) |
|
|
186
|
+
((@shift_register & 0x000001) << 4)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
private
|
|
190
|
+
|
|
191
|
+
def shape(selected, saw = sawtooth, tri = triangle(selected))
|
|
192
|
+
case selected
|
|
193
|
+
when 0x1 then tri
|
|
194
|
+
when 0x2 then saw
|
|
195
|
+
when 0x4 then @pulse
|
|
196
|
+
when 0x8 then noise
|
|
197
|
+
else combined(selected, saw, tri)
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# A low pulse grounds every line. Noise with pulse alone loses its
|
|
202
|
+
# lowest lines; noise with triangle or sawtooth is ANDed over the rest
|
|
203
|
+
# of the mix.
|
|
204
|
+
def combined(selected, saw, tri)
|
|
205
|
+
return 0x000 if selected.anybits?(0x4) && @pulse.zero?
|
|
206
|
+
return noise & @pulse_noise_read if selected == 0xc
|
|
207
|
+
|
|
208
|
+
rest = selected & 0x7
|
|
209
|
+
value = case rest
|
|
210
|
+
when 0x1 then tri
|
|
211
|
+
when 0x2 then saw
|
|
212
|
+
when 0x4 then 0xfff
|
|
213
|
+
else @combined[rest][rest == 0x5 ? tri : saw]
|
|
214
|
+
end
|
|
215
|
+
selected.anybits?(0x8) ? value & noise : value
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def compare_pulse(phase = @accumulator >> 12) = (@pulse = @test || phase >= @pulse_width ? 0xfff : 0x000)
|
|
219
|
+
|
|
220
|
+
# More than one bit set in the selection.
|
|
221
|
+
def combined?(selected) = selected.anybits?(selected - 1)
|
|
222
|
+
|
|
223
|
+
# The one point in the cycle where the waveform selector drives the DAC:
|
|
224
|
+
# either it latches a fresh value, or nothing drives the line and the
|
|
225
|
+
# charge already on it drains a little further.
|
|
226
|
+
def latch_output
|
|
227
|
+
value = output
|
|
228
|
+
if @tri_saw_delay
|
|
229
|
+
@delayed_sawtooth = sawtooth
|
|
230
|
+
@delayed_triangle = triangle
|
|
231
|
+
end
|
|
232
|
+
if @selected.zero?
|
|
233
|
+
drain_floating(1)
|
|
234
|
+
return
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
@floating = value
|
|
238
|
+
@floating_ttl = FLOATING_OUTPUT_TTL
|
|
239
|
+
feed_back(value) if combined?(@selected) && !@test
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# A zero on the shared output lines travels back into the oscillator:
|
|
243
|
+
# through the sawtooth switch it clears the accumulator MSB on the next
|
|
244
|
+
# cycle, and with noise selected it lands in the LFSR, where a bit
|
|
245
|
+
# pulled low can never come back. The 8580 buffers the top bit behind a
|
|
246
|
+
# flip-flop before the sawtooth switch, so only the LFSR sees it. In the
|
|
247
|
+
# first phase of a shift the register bits float, so nothing is pulled
|
|
248
|
+
# down: the output is latched instead, for the second phase to write.
|
|
249
|
+
def feed_back(value)
|
|
250
|
+
@accumulator &= ~MSB if @topbit_feedback && @selected.anybits?(0x2) && value.nobits?(0x800)
|
|
251
|
+
write_shift_register(write_back(@selected, value)) if @selected.anybits?(0x8) && @shift_pipeline != 1
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# A rise of bit 19 starts a shift that takes two more cycles
|
|
255
|
+
# (SID/noisewriteback's noise_writeback_test2, after libresidfp's shift
|
|
256
|
+
# pipeline). The first phase latches the output, and the second writes
|
|
257
|
+
# it over the taps, by the test bit release rule with the waveform left
|
|
258
|
+
# alone, before the register shifts.
|
|
259
|
+
def advance
|
|
260
|
+
previous = @accumulator
|
|
261
|
+
@accumulator = (previous + @frequency) & 0xffffff
|
|
262
|
+
@msb_rising = previous.nobits?(MSB) && @accumulator.anybits?(MSB)
|
|
263
|
+
if previous.nobits?(0x080000) && @accumulator.anybits?(0x080000)
|
|
264
|
+
@shift_pipeline = 2
|
|
265
|
+
elsif @shift_pipeline.positive? && (@shift_pipeline -= 1).zero?
|
|
266
|
+
write_shift_register(write_back(@selected, @output)) if release_writes_back?(@selected, @selected)
|
|
267
|
+
shift_noise
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Bit 0 feeds back bits 22 and 17, with the test bit forcing bit 22 high.
|
|
272
|
+
def shift_noise(forced = 0)
|
|
273
|
+
bit0 = ((@shift_register >> 22) | forced) ^ (@shift_register >> 17)
|
|
274
|
+
@shift_register = ((@shift_register << 1) & 0x7fffff) | (bit0 & 0x1)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# TriXOR = !Saw & ((!V3 & Ring) ^ bit23), where V3 is the modulating
|
|
278
|
+
# voice's MSB (SID/ringmod).
|
|
279
|
+
def inverted?(selected)
|
|
280
|
+
return false if selected.anybits?(0x2)
|
|
281
|
+
|
|
282
|
+
msb = @accumulator.anybits?(MSB)
|
|
283
|
+
return msb unless @ring
|
|
284
|
+
|
|
285
|
+
msb != @sync_source.accumulator.nobits?(MSB)
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
end
|