badline 0.1.0 → 0.2.1
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 +185 -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 +96 -25
- 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 +132 -16
- data/lib/badline/cia/interrupt_register.rb +102 -0
- data/lib/badline/cia/serial.rb +219 -0
- data/lib/badline/cia/timer.rb +43 -31
- data/lib/badline/cia.rb +138 -88
- data/lib/badline/color_memory.rb +14 -5
- data/lib/badline/computer.rb +97 -12
- data/lib/badline/control_ports.rb +54 -9
- data/lib/badline/cpu/addressing.rb +131 -0
- data/lib/badline/cpu/microcode.rb +169 -0
- data/lib/badline/cpu/operations.rb +88 -0
- data/lib/badline/cpu/stack_operations.rb +82 -0
- data/lib/badline/cpu.rb +109 -168
- 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 +51 -0
- data/lib/badline/kernal_trap/drive/status.rb +49 -0
- data/lib/badline/kernal_trap/drive.rb +277 -0
- data/lib/badline/kernal_trap/file.rb +5 -25
- data/lib/badline/kernal_trap/load.rb +86 -24
- data/lib/badline/kernal_trap/routine.rb +51 -0
- data/lib/badline/kernal_trap/save.rb +46 -14
- data/lib/badline/kernal_trap/serial.rb +171 -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/memory.rb +5 -0
- 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 +251 -23
- data/lib/badline/storage/crt_file.rb +18 -2
- data/lib/badline/storage/d64_image.rb +6 -0
- data/lib/badline/storage/d71_image.rb +4 -0
- data/lib/badline/storage/d81_image.rb +4 -0
- data/lib/badline/storage/disk_image.rb +99 -13
- data/lib/badline/storage/host_directory.rb +30 -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 +389 -55
- data/lib/badline.rb +5 -0
- data/vendor/.gitkeep +0 -0
- metadata +104 -4
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class SID
|
|
5
|
+
# ADSR envelope generator.
|
|
6
|
+
#
|
|
7
|
+
# A 15-bit rate counter divides the 1 MHz clock down to one envelope step
|
|
8
|
+
# per period. Attack steps linearly; decay and release run through a
|
|
9
|
+
# second, level-dependent divider that approximates an exponential
|
|
10
|
+
# curve.
|
|
11
|
+
#
|
|
12
|
+
# Each stage runs a cycle or more behind the one feeding it, after
|
|
13
|
+
# reSID 1.0's single-cycle pipeline: the rate counter resets the cycle
|
|
14
|
+
# after it matches, the envelope counter steps two cycles after that,
|
|
15
|
+
# and a gate edge only switches the rate over on its second cycle.
|
|
16
|
+
class Envelope
|
|
17
|
+
# Rate counter comparison values for each ADSR nibble. The counter
|
|
18
|
+
# takes a cycle to reset, so the period is one cycle longer (2ms to
|
|
19
|
+
# 8s over the attack range).
|
|
20
|
+
PERIODS = [8, 31, 62, 94, 148, 219, 266, 312,
|
|
21
|
+
391, 976, 1953, 3125, 3906, 11_719, 19_531, 31_250].freeze
|
|
22
|
+
|
|
23
|
+
# Envelope levels where the exponential divider changes, and the
|
|
24
|
+
# divider it changes to.
|
|
25
|
+
EXPONENTIAL_PERIODS = { 0xff => 1, 0x5d => 2, 0x36 => 4, 0x1a => 8,
|
|
26
|
+
0x0e => 16, 0x06 => 30, 0x00 => 1 }.freeze
|
|
27
|
+
|
|
28
|
+
attr_reader :state, :counter, :env3
|
|
29
|
+
|
|
30
|
+
def initialize
|
|
31
|
+
@attack = @decay = @sustain = @release = 0x0
|
|
32
|
+
@gate = false
|
|
33
|
+
@state = @next_state = :release
|
|
34
|
+
@counter = @env3 = 0x00
|
|
35
|
+
@rate_counter = 0
|
|
36
|
+
@rate_period = PERIODS[0]
|
|
37
|
+
@reset_rate_counter = false
|
|
38
|
+
@exponential_counter = 0
|
|
39
|
+
@exponential_period = 1
|
|
40
|
+
@state_pipeline = @envelope_pipeline = @exponential_pipeline = 0
|
|
41
|
+
@hold_zero = true
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def output = @counter
|
|
45
|
+
|
|
46
|
+
# The gate edge switches the state over two cycles. Rising, it runs
|
|
47
|
+
# the decay rate for the first of them. The rate counter is left
|
|
48
|
+
# running, so the first step lands somewhere inside the current
|
|
49
|
+
# period.
|
|
50
|
+
def control=(value)
|
|
51
|
+
gate = value.anybits?(0x01)
|
|
52
|
+
return if gate == @gate
|
|
53
|
+
|
|
54
|
+
@gate = gate
|
|
55
|
+
gate ? gate_on : gate_off
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def attack_decay=(value)
|
|
59
|
+
@attack = (value >> 4) & 0x0f
|
|
60
|
+
@decay = value & 0x0f
|
|
61
|
+
@rate_period = PERIODS[@attack] if @state == :attack
|
|
62
|
+
@rate_period = PERIODS[@decay] if @state == :decay_sustain
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def sustain_release=(value)
|
|
66
|
+
@sustain = (value >> 4) & 0x0f
|
|
67
|
+
@release = value & 0x0f
|
|
68
|
+
@rate_period = PERIODS[@release] if @state == :release
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# ENV3 reads the counter as it stood at the start of the cycle.
|
|
72
|
+
#
|
|
73
|
+
# Lowering the rate period below the current counter sends it the long
|
|
74
|
+
# way round through 2^15 before the envelope can step, the ADSR delay
|
|
75
|
+
# bug that hard restarts rely on. Counting from 0x7fff wraps to 1.
|
|
76
|
+
def cycle!
|
|
77
|
+
@env3 = @counter
|
|
78
|
+
run_pipeline if pipelined?
|
|
79
|
+
return @reset_rate_counter = true if @rate_counter == @rate_period
|
|
80
|
+
|
|
81
|
+
@rate_counter += 1
|
|
82
|
+
@rate_counter = 1 if @rate_counter == 0x8000
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Runs `cycles` cycles as #cycle! would. Between steps only the rate
|
|
86
|
+
# counter moves, so it jumps from one match to the next and the cycles
|
|
87
|
+
# around each step run whole. Frozen at zero, a period changes
|
|
88
|
+
# nothing, so whole periods are counted off at once.
|
|
89
|
+
def fast_forward(cycles)
|
|
90
|
+
while cycles.positive?
|
|
91
|
+
if pipelined?
|
|
92
|
+
cycle!
|
|
93
|
+
cycles -= 1
|
|
94
|
+
next
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
@env3 = @counter
|
|
98
|
+
idle = cycles_to_match
|
|
99
|
+
return advance_rate_counter(cycles) if cycles < idle
|
|
100
|
+
|
|
101
|
+
advance_rate_counter(idle)
|
|
102
|
+
cycles = skip_frozen_periods(cycles - idle)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
def land_step
|
|
109
|
+
@envelope_pipeline -= 1
|
|
110
|
+
step if @envelope_pipeline.zero? && !@hold_zero
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def gate_on
|
|
114
|
+
@next_state = :attack
|
|
115
|
+
@state = :decay_sustain
|
|
116
|
+
@rate_period = PERIODS[@decay]
|
|
117
|
+
@state_pipeline = 2
|
|
118
|
+
if @reset_rate_counter || @exponential_pipeline == 2
|
|
119
|
+
@envelope_pipeline = @exponential_period == 1 || @exponential_pipeline == 2 ? 2 : 4
|
|
120
|
+
elsif @exponential_pipeline == 1
|
|
121
|
+
@state_pipeline = 3
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def gate_off
|
|
126
|
+
@next_state = :release
|
|
127
|
+
@state_pipeline = @envelope_pipeline.positive? ? 3 : 2
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def state_change
|
|
131
|
+
@state_pipeline -= 1
|
|
132
|
+
if @next_state == :attack
|
|
133
|
+
enter_attack if @state_pipeline.zero?
|
|
134
|
+
elsif (@state == :attack && @state_pipeline.zero?) ||
|
|
135
|
+
(@state == :decay_sustain && @state_pipeline == 1)
|
|
136
|
+
@state = :release
|
|
137
|
+
@rate_period = PERIODS[@release]
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def enter_attack
|
|
142
|
+
@state = :attack
|
|
143
|
+
@rate_period = PERIODS[@attack]
|
|
144
|
+
@hold_zero = false
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# The first attack step also resets the exponential divider.
|
|
148
|
+
def reset_rate_counter
|
|
149
|
+
@rate_counter = 0
|
|
150
|
+
@reset_rate_counter = false
|
|
151
|
+
if @state == :attack
|
|
152
|
+
@exponential_counter = 0
|
|
153
|
+
@envelope_pipeline = 2
|
|
154
|
+
elsif !@hold_zero && (@exponential_counter += 1) == @exponential_period
|
|
155
|
+
@exponential_pipeline = @exponential_period == 1 ? 1 : 2
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def exponential_step
|
|
160
|
+
@exponential_counter = 0
|
|
161
|
+
return unless @state == :release || (@state == :decay_sustain && @counter != sustain_level)
|
|
162
|
+
|
|
163
|
+
@envelope_pipeline = 1
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def pipelined?
|
|
167
|
+
@reset_rate_counter || (@state_pipeline | @envelope_pipeline | @exponential_pipeline) != 0
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def run_pipeline
|
|
171
|
+
state_change if @state_pipeline.positive?
|
|
172
|
+
land_step if @envelope_pipeline.positive?
|
|
173
|
+
if @exponential_pipeline.positive? && (@exponential_pipeline -= 1).zero?
|
|
174
|
+
exponential_step
|
|
175
|
+
elsif @reset_rate_counter
|
|
176
|
+
reset_rate_counter
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Cycles that only count before the rate counter matches its period.
|
|
181
|
+
def cycles_to_match
|
|
182
|
+
return @rate_period - @rate_counter if @rate_counter <= @rate_period
|
|
183
|
+
|
|
184
|
+
0x7fff - @rate_counter + @rate_period
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def advance_rate_counter(cycles)
|
|
188
|
+
@rate_counter += cycles
|
|
189
|
+
@rate_counter -= 0x7fff if @rate_counter > 0x7fff
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# With the counter on its period, a frozen envelope comes back to the
|
|
193
|
+
# same state every period plus one cycles. Otherwise the next cycle
|
|
194
|
+
# starts a step.
|
|
195
|
+
def skip_frozen_periods(cycles)
|
|
196
|
+
cycles %= @rate_period + 1 if @hold_zero && @state != :attack
|
|
197
|
+
return cycles unless cycles.positive?
|
|
198
|
+
|
|
199
|
+
cycle!
|
|
200
|
+
cycles - 1
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def step
|
|
204
|
+
if @state == :attack
|
|
205
|
+
attack_step
|
|
206
|
+
else
|
|
207
|
+
@counter = (@counter - 1) & 0xff
|
|
208
|
+
end
|
|
209
|
+
set_exponential_period
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def attack_step
|
|
213
|
+
@counter = (@counter + 1) & 0xff
|
|
214
|
+
return unless @counter == 0xff
|
|
215
|
+
|
|
216
|
+
@state = :decay_sustain
|
|
217
|
+
@rate_period = PERIODS[@decay]
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def sustain_level = (@sustain << 4) | @sustain
|
|
221
|
+
|
|
222
|
+
# Reaching zero freezes the envelope until the gate is raised again.
|
|
223
|
+
def set_exponential_period
|
|
224
|
+
period = EXPONENTIAL_PERIODS[@counter]
|
|
225
|
+
return unless period
|
|
226
|
+
|
|
227
|
+
@exponential_period = period
|
|
228
|
+
@hold_zero = true if @counter.zero?
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class SID
|
|
5
|
+
# The SID's multimode filter: a state-variable design integrating low-,
|
|
6
|
+
# band- and high-pass in parallel, with the mixer and master volume on
|
|
7
|
+
# its output, and the C64 board's own RC network after that.
|
|
8
|
+
#
|
|
9
|
+
# Voices are routed through it or around it per the RES/FILT register,
|
|
10
|
+
# and the outputs selected by MODE/VOL are summed back together with the
|
|
11
|
+
# unfiltered ones.
|
|
12
|
+
class Filter
|
|
13
|
+
# Cutoff frequency in Hz at each chip's breakpoints. The 6581 curve was
|
|
14
|
+
# sampled from a real chip by reSID and bends sharply around $300; the
|
|
15
|
+
# 8580's is close to a straight line from 0 to 12.5 kHz. The curve
|
|
16
|
+
# between breakpoints is interpolated linearly; reSID fits a spline, so
|
|
17
|
+
# the two differ by a few Hz mid-segment.
|
|
18
|
+
CUTOFF_POINTS = {
|
|
19
|
+
mos6581: [[0, 220], [128, 230], [256, 250], [384, 300],
|
|
20
|
+
[512, 420], [640, 780], [768, 1600], [832, 2300],
|
|
21
|
+
[896, 3200], [960, 4300], [1024, 5000], [1088, 5400],
|
|
22
|
+
[1152, 5800], [1216, 6000], [1280, 6200], [1344, 6400],
|
|
23
|
+
[1408, 6600], [1472, 6800], [1536, 7000], [1600, 7200],
|
|
24
|
+
[1664, 7400], [1728, 7600], [1792, 7800], [1856, 8000],
|
|
25
|
+
[1920, 8200], [1984, 8400], [2047, 8600]].freeze,
|
|
26
|
+
mos8580: [[0, 0], [128, 800], [256, 1600], [384, 2500],
|
|
27
|
+
[512, 3300], [640, 4100], [768, 4800], [896, 5600],
|
|
28
|
+
[1024, 6500], [1152, 7500], [1280, 8400], [1408, 9200],
|
|
29
|
+
[1536, 9800], [1664, 10_500], [1792, 11_000],
|
|
30
|
+
[1920, 11_700], [2047, 12_500]].freeze
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
# 2^20 / 1 MHz, so one cycle of integration is a plain shift.
|
|
34
|
+
SCALE = (2**20) / 1_000_000.0
|
|
35
|
+
|
|
36
|
+
# The integrator goes unstable above ~16 kHz at a one-cycle step.
|
|
37
|
+
W0_MAX = (2 * Math::PI * 16_000 * SCALE).to_i
|
|
38
|
+
|
|
39
|
+
# A step of several cycles scales w0 by their count. reSID keeps such
|
|
40
|
+
# a step stable by capping the cutoff at 4 kHz over 8 cycles; this is
|
|
41
|
+
# the same bound on the product, so a single cycle is never capped.
|
|
42
|
+
W0_STEP_MAX = (2 * Math::PI * 4_000 * 8 * SCALE).to_i
|
|
43
|
+
|
|
44
|
+
def self.build_w0(points)
|
|
45
|
+
table = Array.new(2048)
|
|
46
|
+
points.each_cons(2) do |(fc0, hz0), (fc1, hz1)|
|
|
47
|
+
(fc0..fc1).each do |register|
|
|
48
|
+
hz = hz0 + (((hz1 - hz0) * (register - fc0)).to_f / (fc1 - fc0))
|
|
49
|
+
table[register] = [(2 * Math::PI * hz * SCALE).to_i, W0_MAX].min
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
table.freeze
|
|
53
|
+
end
|
|
54
|
+
private_class_method :build_w0
|
|
55
|
+
|
|
56
|
+
W0 = CUTOFF_POINTS.transform_values { |points| build_w0(points) }.freeze
|
|
57
|
+
|
|
58
|
+
# 1024/Q, with Q sweeping from 0.707 up to 1.707 across the register.
|
|
59
|
+
RESONANCE = (0x0..0xf).map { |res| (1024.0 / (0.707 + (res / 15.0))).to_i }.freeze
|
|
60
|
+
|
|
61
|
+
# The mixer's own input DC offset, derived by reSID from the 6581's
|
|
62
|
+
# zero-volume and full-volume output levels. The 8580 has none.
|
|
63
|
+
MIXER_DC = { mos6581: -((0xfff * 0xff) / 18) >> 7, mos8580: 0 }.freeze
|
|
64
|
+
|
|
65
|
+
# Fractional bits carried in the integrators. At low cutoffs a cycle's
|
|
66
|
+
# step is only a few units, which truncating to whole units would
|
|
67
|
+
# swallow, and always toward minus infinity.
|
|
68
|
+
FRACTION = 12
|
|
69
|
+
|
|
70
|
+
# The RC network between the SID's audio pin and the C64's output jack:
|
|
71
|
+
# a 10kΩ/1000pF low-pass at ~16 kHz, then a 1kΩ/10µF high-pass at
|
|
72
|
+
# ~16 Hz that strips the 6581's DC offset off the mix.
|
|
73
|
+
class External
|
|
74
|
+
W0_LOWPASS = (100_000 * SCALE).round
|
|
75
|
+
W0_HIGHPASS = (100 * SCALE).round
|
|
76
|
+
|
|
77
|
+
# The high-pass step is W0_HIGHPASS/2^20 of the gap, so in whole
|
|
78
|
+
# units it stalls on any gap under ~10k, as reSID's does. Sixteen
|
|
79
|
+
# fractional bits bring that under one unit.
|
|
80
|
+
FRACTION = 16
|
|
81
|
+
|
|
82
|
+
# Its input arrives with the filter's fractional bits still on.
|
|
83
|
+
INPUT_SHIFT = FRACTION - Filter::FRACTION
|
|
84
|
+
|
|
85
|
+
attr_reader :output
|
|
86
|
+
|
|
87
|
+
def initialize
|
|
88
|
+
@lowpass = 0
|
|
89
|
+
@highpass = 0
|
|
90
|
+
@output = 0
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Takes the input in 1/2^Filter::FRACTION units, and reads the
|
|
94
|
+
# output out in whole ones.
|
|
95
|
+
def cycle!(input, cycles = 1)
|
|
96
|
+
delta_lowpass = ((W0_LOWPASS >> 8) * cycles * ((input << INPUT_SHIFT) - @lowpass)) >> 12
|
|
97
|
+
delta_highpass = (W0_HIGHPASS * cycles * (@lowpass - @highpass)) >> 20
|
|
98
|
+
@output = (@lowpass - @highpass) >> FRACTION
|
|
99
|
+
@lowpass += delta_lowpass
|
|
100
|
+
@highpass += delta_highpass
|
|
101
|
+
@output
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
attr_reader :model, :cutoff, :routing, :mode, :volume
|
|
106
|
+
|
|
107
|
+
def initialize(model: :mos6581)
|
|
108
|
+
@model = model
|
|
109
|
+
@w0_table = W0.fetch(model)
|
|
110
|
+
@mixer_dc = MIXER_DC.fetch(model)
|
|
111
|
+
@external = External.new
|
|
112
|
+
reset
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def reset
|
|
116
|
+
@cutoff = 0x000
|
|
117
|
+
@routing = 0x0
|
|
118
|
+
@mode = 0x0
|
|
119
|
+
@volume = 0x0
|
|
120
|
+
@voice3_off = false
|
|
121
|
+
@w0 = @w0_table[0]
|
|
122
|
+
@resonance = RESONANCE[0]
|
|
123
|
+
@lowpass = @bandpass = @highpass = 0
|
|
124
|
+
@input = @unfiltered = 0
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def write(reg, value)
|
|
128
|
+
case reg
|
|
129
|
+
when 0x15 then self.cutoff = (@cutoff & 0x7f8) | (value & 0x07)
|
|
130
|
+
when 0x16 then self.cutoff = (value << 3) | (@cutoff & 0x007)
|
|
131
|
+
when 0x17 then write_resonance(value)
|
|
132
|
+
when 0x18 then write_mode_volume(value)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def cutoff=(value)
|
|
137
|
+
@cutoff = value & 0x7ff
|
|
138
|
+
@w0 = @w0_table[@cutoff]
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def voice3_off? = @voice3_off
|
|
142
|
+
|
|
143
|
+
def lowpass = @lowpass >> FRACTION
|
|
144
|
+
def bandpass = @bandpass >> FRACTION
|
|
145
|
+
def highpass = @highpass >> FRACTION
|
|
146
|
+
|
|
147
|
+
# Integrates `cycles` cycles in one step, from the voices' output at
|
|
148
|
+
# the end of them.
|
|
149
|
+
def cycle!(voices, cycles = 1)
|
|
150
|
+
route(voices)
|
|
151
|
+
w0 = @w0 * cycles
|
|
152
|
+
w0 = W0_STEP_MAX if w0 > W0_STEP_MAX
|
|
153
|
+
@bandpass -= (w0 * @highpass) >> 20
|
|
154
|
+
@lowpass -= (w0 * @bandpass) >> 20
|
|
155
|
+
@highpass = ((@bandpass * @resonance) >> 10) - @lowpass - (@input << FRACTION)
|
|
156
|
+
@external.cycle!(fine_mix, cycles)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# The SID's own audio pin, before the board's RC network.
|
|
160
|
+
def mix = fine_mix >> FRACTION
|
|
161
|
+
|
|
162
|
+
def output = @external.output
|
|
163
|
+
|
|
164
|
+
private
|
|
165
|
+
|
|
166
|
+
def write_resonance(value)
|
|
167
|
+
@routing = value & 0x0f
|
|
168
|
+
@resonance = RESONANCE[(value >> 4) & 0x0f]
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def write_mode_volume(value)
|
|
172
|
+
@volume = value & 0x0f
|
|
173
|
+
@mode = (value >> 4) & 0x07
|
|
174
|
+
@voice3_off = value.anybits?(0x80)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Unrolled over the three voices, like SID#clock!. Voice 3 is silenced
|
|
178
|
+
# by MODE/VOL bit 7 only while it bypasses the filter.
|
|
179
|
+
def route(voices)
|
|
180
|
+
voice1, voice2, voice3 = voices
|
|
181
|
+
@input = 0
|
|
182
|
+
@unfiltered = 0
|
|
183
|
+
route_voice(voice1.output >> 7, 0x1)
|
|
184
|
+
route_voice(voice2.output >> 7, 0x2)
|
|
185
|
+
if @routing.anybits?(0x4)
|
|
186
|
+
@input += voice3.output >> 7
|
|
187
|
+
elsif !@voice3_off
|
|
188
|
+
@unfiltered += voice3.output >> 7
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def route_voice(output, bit)
|
|
193
|
+
if @routing.anybits?(bit)
|
|
194
|
+
@input += output
|
|
195
|
+
else
|
|
196
|
+
@unfiltered += output
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# The mix with the integrators' fractional bits kept, so the RC
|
|
201
|
+
# network isn't fed the filter's output floored to whole units.
|
|
202
|
+
def fine_mix = (((@unfiltered + @mixer_dc) << FRACTION) + filtered) * @volume
|
|
203
|
+
|
|
204
|
+
def filtered
|
|
205
|
+
value = 0
|
|
206
|
+
value += @lowpass if @mode.anybits?(0x1)
|
|
207
|
+
value += @bandpass if @mode.anybits?(0x2)
|
|
208
|
+
value += @highpass if @mode.anybits?(0x4)
|
|
209
|
+
value
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class SID
|
|
5
|
+
# One of the three voices: a waveform generator amplitude-modulated by
|
|
6
|
+
# its envelope.
|
|
7
|
+
#
|
|
8
|
+
# The 6581 waveform DAC swings around a midpoint well above ground, so
|
|
9
|
+
# even a silent voice carries a large DC offset into the mixer. The
|
|
10
|
+
# 8580's is centred and carries none.
|
|
11
|
+
class Voice
|
|
12
|
+
WAVE_ZERO = { mos6581: 0x380, mos8580: 0x800 }.freeze
|
|
13
|
+
DC_OFFSET = { mos6581: 0x800 * 0xff, mos8580: 0 }.freeze
|
|
14
|
+
|
|
15
|
+
attr_reader :waveform, :envelope
|
|
16
|
+
|
|
17
|
+
# Each voice hard-syncs and ring-modulates against the previous one,
|
|
18
|
+
# wrapping from the first back round to the last.
|
|
19
|
+
def self.linked(count, model:)
|
|
20
|
+
voices = Array.new(count) { new(model:) }
|
|
21
|
+
waveforms = voices.map(&:waveform)
|
|
22
|
+
waveforms.each_with_index do |waveform, i|
|
|
23
|
+
waveform.sync_source = waveforms[i - 1]
|
|
24
|
+
waveform.sync_dest = waveforms[(i + 1) % count]
|
|
25
|
+
end
|
|
26
|
+
voices
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def initialize(model: :mos6581)
|
|
30
|
+
@waveform = Waveform.new(model:)
|
|
31
|
+
@envelope = Envelope.new
|
|
32
|
+
@wave_zero = WAVE_ZERO.fetch(model)
|
|
33
|
+
@dc_offset = DC_OFFSET.fetch(model)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def reset!
|
|
37
|
+
@waveform.reset!
|
|
38
|
+
@envelope = Envelope.new
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def control=(value)
|
|
42
|
+
@waveform.control = value
|
|
43
|
+
@envelope.control = value
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def cycle!
|
|
47
|
+
@envelope.cycle!
|
|
48
|
+
@waveform.cycle!
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def fast_forward(cycles)
|
|
52
|
+
@envelope.fast_forward(cycles)
|
|
53
|
+
@waveform.fast_forward(cycles)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def output
|
|
57
|
+
((@waveform.output - @wave_zero) * @envelope.output) + @dc_offset
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -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
|