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.
Files changed (140) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +185 -0
  3. data/CLAUDE.md +217 -0
  4. data/CODE_OF_CONDUCT.md +92 -0
  5. data/CONTRIBUTING.md +51 -0
  6. data/README.md +224 -31
  7. data/doc/pinned-behaviour.md +1117 -0
  8. data/exe/badline +22 -2
  9. data/exe/badline-sid +34 -0
  10. data/lib/badline/address_bus.rb +96 -25
  11. data/lib/badline/audio/aiff.rb +36 -0
  12. data/lib/badline/audio/bare_player.rb +129 -0
  13. data/lib/badline/audio/cli.rb +133 -0
  14. data/lib/badline/audio/console.rb +75 -0
  15. data/lib/badline/audio/jukebox.rb +82 -0
  16. data/lib/badline/audio/machine_player.rb +57 -0
  17. data/lib/badline/audio/options.rb +137 -0
  18. data/lib/badline/audio/pcm_writer.rb +57 -0
  19. data/lib/badline/audio/playback.rb +108 -0
  20. data/lib/badline/audio/renderer.rb +85 -0
  21. data/lib/badline/audio/sdl_sink.rb +102 -0
  22. data/lib/badline/audio/wav.rb +23 -0
  23. data/lib/badline/audio.rb +14 -0
  24. data/lib/badline/cartridge/action_replay.rb +111 -0
  25. data/lib/badline/cartridge/atomic_power.rb +24 -0
  26. data/lib/badline/cartridge/bank.rb +81 -0
  27. data/lib/badline/cartridge/comal80.rb +48 -0
  28. data/lib/badline/cartridge/dinamic.rb +34 -0
  29. data/lib/badline/cartridge/easy_flash.rb +122 -0
  30. data/lib/badline/cartridge/epyx_fastload.rb +71 -0
  31. data/lib/badline/cartridge/final_cartridge3.rb +77 -0
  32. data/lib/badline/cartridge/flash.rb +265 -0
  33. data/lib/badline/cartridge/freezer.rb +31 -0
  34. data/lib/badline/cartridge/fun_play.rb +38 -0
  35. data/lib/badline/cartridge/g_mod2.rb +62 -0
  36. data/lib/badline/cartridge/game_system.rb +40 -0
  37. data/lib/badline/cartridge/kcs_power.rb +72 -0
  38. data/lib/badline/cartridge/mach5.rb +34 -0
  39. data/lib/badline/cartridge/magic_desk.rb +6 -3
  40. data/lib/badline/cartridge/ocean.rb +11 -12
  41. data/lib/badline/cartridge/pagefox.rb +56 -0
  42. data/lib/badline/cartridge/retro_replay.rb +276 -0
  43. data/lib/badline/cartridge/rex_utility.rb +33 -0
  44. data/lib/badline/cartridge/rgcd.rb +48 -0
  45. data/lib/badline/cartridge/simons_basic.rb +40 -0
  46. data/lib/badline/cartridge/standard.rb +3 -19
  47. data/lib/badline/cartridge/super_games.rb +36 -0
  48. data/lib/badline/cartridge/westermann.rb +34 -0
  49. data/lib/badline/cartridge/zaxxon.rb +50 -0
  50. data/lib/badline/cartridge.rb +132 -16
  51. data/lib/badline/cia/interrupt_register.rb +102 -0
  52. data/lib/badline/cia/serial.rb +219 -0
  53. data/lib/badline/cia/timer.rb +43 -31
  54. data/lib/badline/cia.rb +138 -88
  55. data/lib/badline/color_memory.rb +14 -5
  56. data/lib/badline/computer.rb +97 -12
  57. data/lib/badline/control_ports.rb +54 -9
  58. data/lib/badline/cpu/addressing.rb +131 -0
  59. data/lib/badline/cpu/microcode.rb +169 -0
  60. data/lib/badline/cpu/operations.rb +88 -0
  61. data/lib/badline/cpu/stack_operations.rb +82 -0
  62. data/lib/badline/cpu.rb +109 -168
  63. data/lib/badline/cycleable.rb +3 -0
  64. data/lib/badline/datasette.rb +81 -0
  65. data/lib/badline/debug_register.rb +28 -0
  66. data/lib/badline/gui/application.rb +79 -18
  67. data/lib/badline/gui/gamepads.rb +97 -0
  68. data/lib/badline/gui/joy_map.rb +15 -7
  69. data/lib/badline/gui.rb +1 -0
  70. data/lib/badline/input/mouse1351.rb +41 -0
  71. data/lib/badline/input/paddles.rb +36 -0
  72. data/lib/badline/input.rb +4 -0
  73. data/lib/badline/instruction_set/arithmetic.rb +16 -21
  74. data/lib/badline/instruction_set/bitwise.rb +17 -22
  75. data/lib/badline/instruction_set/branch.rb +20 -19
  76. data/lib/badline/instruction_set/flag.rb +7 -7
  77. data/lib/badline/instruction_set/illegal.rb +14 -22
  78. data/lib/badline/instruction_set/inc_dec.rb +8 -10
  79. data/lib/badline/instruction_set/stack.rb +5 -57
  80. data/lib/badline/instruction_set/transfer.rb +9 -9
  81. data/lib/badline/instruction_set.rb +9 -31
  82. data/lib/badline/interrupts.rb +104 -0
  83. data/lib/badline/joystick.rb +9 -9
  84. data/lib/badline/kernal_trap/channel.rb +51 -0
  85. data/lib/badline/kernal_trap/drive/status.rb +49 -0
  86. data/lib/badline/kernal_trap/drive.rb +277 -0
  87. data/lib/badline/kernal_trap/file.rb +5 -25
  88. data/lib/badline/kernal_trap/load.rb +86 -24
  89. data/lib/badline/kernal_trap/routine.rb +51 -0
  90. data/lib/badline/kernal_trap/save.rb +46 -14
  91. data/lib/badline/kernal_trap/serial.rb +171 -0
  92. data/lib/badline/kernal_trap.rb +4 -0
  93. data/lib/badline/keyboard.rb +40 -16
  94. data/lib/badline/media.rb +48 -10
  95. data/lib/badline/memory.rb +5 -0
  96. data/lib/badline/roms/eapi/LICENSE.md +15 -0
  97. data/lib/badline/roms/eapi/README +41 -0
  98. data/lib/badline/roms/eapi/eapi-am29f040-14 +0 -0
  99. data/lib/badline/sid/decimator.rb +46 -0
  100. data/lib/badline/sid/envelope.rb +232 -0
  101. data/lib/badline/sid/filter.rb +213 -0
  102. data/lib/badline/sid/voice.rb +61 -0
  103. data/lib/badline/sid/waveform/combined.rb +126 -0
  104. data/lib/badline/sid/waveform/fast_forward.rb +105 -0
  105. data/lib/badline/sid/waveform/noise_writeback.rb +82 -0
  106. data/lib/badline/sid/waveform.rb +289 -0
  107. data/lib/badline/sid.rb +274 -7
  108. data/lib/badline/status.rb +251 -23
  109. data/lib/badline/storage/crt_file.rb +18 -2
  110. data/lib/badline/storage/d64_image.rb +6 -0
  111. data/lib/badline/storage/d71_image.rb +4 -0
  112. data/lib/badline/storage/d81_image.rb +4 -0
  113. data/lib/badline/storage/disk_image.rb +99 -13
  114. data/lib/badline/storage/host_directory.rb +30 -4
  115. data/lib/badline/storage/sid_file.rb +274 -0
  116. data/lib/badline/storage/song_lengths.rb +65 -0
  117. data/lib/badline/storage/t64.rb +67 -0
  118. data/lib/badline/storage/tap.rb +68 -0
  119. data/lib/badline/storage.rb +19 -0
  120. data/lib/badline/time_of_day.rb +105 -52
  121. data/lib/badline/version.rb +1 -1
  122. data/lib/badline/vic/bank.rb +14 -6
  123. data/lib/badline/vic/border_mask.rb +76 -0
  124. data/lib/badline/vic/collisions.rb +104 -0
  125. data/lib/badline/vic/color_patches.rb +33 -0
  126. data/lib/badline/vic/display_state.rb +92 -29
  127. data/lib/badline/vic/graphics_mode.rb +78 -53
  128. data/lib/badline/vic/graphics_shifter.rb +140 -0
  129. data/lib/badline/vic/register_log.rb +63 -0
  130. data/lib/badline/vic/registers.rb +3 -0
  131. data/lib/badline/vic/sequencer.rb +163 -137
  132. data/lib/badline/vic/sequencer_output.rb +116 -0
  133. data/lib/badline/vic/sprite/internal_bus.rb +49 -0
  134. data/lib/badline/vic/sprite/shifter.rb +129 -0
  135. data/lib/badline/vic/sprite.rb +275 -65
  136. data/lib/badline/vic/sprites.rb +220 -58
  137. data/lib/badline/vic.rb +389 -55
  138. data/lib/badline.rb +5 -0
  139. data/vendor/.gitkeep +0 -0
  140. 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