badline 0.2.1 → 0.4.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/.claude/hooks/session-start.sh +57 -0
- data/.claude/settings.json +14 -0
- data/CHANGELOG.md +107 -0
- data/CLAUDE.md +51 -16
- data/CONTRIBUTING.md +95 -3
- data/README.md +119 -51
- data/SECURITY.md +9 -0
- data/doc/pinned-behaviour.md +293 -39
- data/exe/badline-ruby +59 -0
- data/lib/badline/address_bus.rb +28 -14
- data/lib/badline/addressable.rb +1 -1
- data/lib/badline/audio/aiff.rb +8 -3
- data/lib/badline/audio/bare_player.rb +20 -3
- data/lib/badline/audio/cli.rb +17 -12
- data/lib/badline/audio/console.rb +12 -58
- data/lib/badline/audio/machine_player.rb +9 -6
- data/lib/badline/audio/playback.rb +3 -0
- data/lib/badline/audio/renderer.rb +3 -3
- data/lib/badline/audio/sdl_sink.rb +20 -52
- data/lib/badline/audio/stream.rb +117 -0
- data/lib/badline/audio/terminal.rb +90 -0
- data/lib/badline/audio.rb +3 -2
- data/lib/badline/cartridge/easy_flash.rb +6 -2
- data/lib/badline/cartridge/epyx_fastload.rb +1 -1
- data/lib/badline/cartridge/geo_ram.rb +63 -0
- data/lib/badline/cartridge/mach5.rb +1 -1
- data/lib/badline/cartridge/magic_desk.rb +5 -1
- data/lib/badline/cartridge/ocean.rb +5 -2
- data/lib/badline/cartridge/rex_utility.rb +1 -1
- data/lib/badline/cartridge/rgcd.rb +1 -1
- data/lib/badline/cartridge/simons_basic.rb +2 -2
- data/lib/badline/cartridge/westermann.rb +2 -2
- data/lib/badline/cartridge.rb +9 -0
- data/lib/badline/checkpoint.rb +98 -0
- data/lib/badline/cia/interrupt_register.rb +34 -10
- data/lib/badline/cia.rb +19 -10
- data/lib/badline/color_memory.rb +2 -2
- data/lib/badline/computer.rb +34 -14
- data/lib/badline/cpu.rb +2 -2
- data/lib/badline/gui/application.rb +70 -26
- data/lib/badline/gui/gamepads.rb +22 -22
- data/lib/badline/gui/joy_map.rb +1 -1
- data/lib/badline/gui/key_map.rb +6 -1
- data/lib/badline/gui/pane.rb +7 -6
- data/lib/badline/gui/screen_pane.rb +9 -13
- data/lib/badline/gui/window.rb +35 -20
- data/lib/badline/gui.rb +1 -1
- data/lib/badline/input/mouse1351.rb +21 -6
- data/lib/badline/input/paddles.rb +11 -6
- data/lib/badline/kernal_trap/channel.rb +102 -7
- data/lib/badline/kernal_trap/drive/block_commands.rb +108 -0
- data/lib/badline/kernal_trap/drive/channels.rb +53 -0
- data/lib/badline/kernal_trap/drive/memory.rb +109 -0
- data/lib/badline/kernal_trap/drive/parameters.rb +28 -0
- data/lib/badline/kernal_trap/drive/status.rb +3 -0
- data/lib/badline/kernal_trap/drive/write_file.rb +41 -0
- data/lib/badline/kernal_trap/drive/writes.rb +92 -0
- data/lib/badline/kernal_trap/drive.rb +109 -131
- data/lib/badline/kernal_trap/file.rb +6 -14
- data/lib/badline/kernal_trap/load.rb +6 -0
- data/lib/badline/kernal_trap/save.rb +13 -7
- data/lib/badline/keyboard.rb +19 -3
- data/lib/badline/media.rb +44 -13
- data/lib/badline/memory.rb +2 -2
- data/lib/badline/options.rb +209 -0
- data/lib/badline/ram_expansion/banking.rb +30 -0
- data/lib/badline/ram_expansion/plus256k.rb +38 -0
- data/lib/badline/ram_expansion/plus60k.rb +31 -0
- data/lib/badline/ram_expansion/unexpanded.rb +22 -0
- data/lib/badline/ram_expansion.rb +21 -0
- data/lib/badline/region.rb +51 -0
- data/lib/badline/rom.rb +14 -5
- data/lib/badline/sdl/events.rb +47 -0
- data/lib/badline/sdl/functions.rb +73 -0
- data/lib/badline/sdl.rb +67 -0
- data/lib/badline/sid/catch_up.rb +110 -0
- data/lib/badline/sid/decimator.rb +6 -1
- data/lib/badline/sid/filter.rb +5 -5
- data/lib/badline/sid/waveform/fast_forward.rb +10 -10
- data/lib/badline/sid/waveform/noise_writeback.rb +32 -14
- data/lib/badline/sid/waveform.rb +8 -8
- data/lib/badline/sid.rb +19 -93
- data/lib/badline/storage/d64_image.rb +10 -0
- data/lib/badline/storage/d71_image.rb +12 -0
- data/lib/badline/storage/d81_image.rb +9 -0
- data/lib/badline/storage/disk_image/bam.rb +111 -0
- data/lib/badline/storage/disk_image/directory.rb +79 -0
- data/lib/badline/storage/disk_image/writing.rb +110 -0
- data/lib/badline/storage/disk_image.rb +49 -4
- data/lib/badline/storage/host_directory.rb +17 -4
- data/lib/badline/storage/sid_file.rb +36 -2
- data/lib/badline/storage.rb +18 -1
- data/lib/badline/time_of_day.rb +11 -2
- data/lib/badline/version.rb +1 -1
- data/lib/badline/via/control_lines.rb +104 -0
- data/lib/badline/via/interrupt_register.rb +58 -0
- data/lib/badline/via/shift_register.rb +135 -0
- data/lib/badline/via/timer1.rb +82 -0
- data/lib/badline/via/timer2.rb +84 -0
- data/lib/badline/via.rb +262 -0
- data/lib/badline/vic/bank.rb +4 -3
- data/lib/badline/vic/color_patches.rb +6 -3
- data/lib/badline/vic/display_state.rb +4 -0
- data/lib/badline/vic/graphics_shifter.rb +49 -8
- data/lib/badline/vic/registers.rb +19 -27
- data/lib/badline/vic/sequencer.rb +18 -18
- data/lib/badline/vic/sequencer_output.rb +4 -4
- data/lib/badline/vic/sprite/internal_bus.rb +4 -0
- data/lib/badline/vic/sprite/shifter.rb +25 -3
- data/lib/badline/vic/sprite.rb +5 -4
- data/lib/badline/vic/sprites.rb +8 -3
- data/lib/badline/vic.rb +98 -46
- data/lib/badline.rb +5 -0
- data/packaging/homebrew/badline.rb +34 -0
- metadata +37 -20
- data/exe/badline +0 -75
- data/exe/badline-sid +0 -34
- data/lib/badline/audio/options.rb +0 -137
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class SID
|
|
5
|
+
# Running the DSP over the cycles that have passed since it last caught
|
|
6
|
+
# up, in spans that fast-forward the voices and end on a whole cycle.
|
|
7
|
+
module CatchUp
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
# Runs the cycles that have passed, landing each queued write on the
|
|
11
|
+
# cycle the CPU wrote it on.
|
|
12
|
+
def catch_up
|
|
13
|
+
replayed = 0
|
|
14
|
+
i = 0
|
|
15
|
+
while i < @deferred_writes.length
|
|
16
|
+
cycle = @deferred_writes[i]
|
|
17
|
+
run(cycle - replayed)
|
|
18
|
+
replayed = cycle
|
|
19
|
+
apply_write(@deferred_writes[i + 1], @deferred_writes[i + 2])
|
|
20
|
+
i += 3
|
|
21
|
+
end
|
|
22
|
+
run(@pending_cycles - replayed)
|
|
23
|
+
@deferred_writes.clear
|
|
24
|
+
@pending_cycles = 0
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def run(cycles)
|
|
28
|
+
while cycles != 0
|
|
29
|
+
span = span(cycles)
|
|
30
|
+
fast_forward(span - 1) if span > 1
|
|
31
|
+
clock!(span)
|
|
32
|
+
cycles -= span
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The longest stretch that can be fast-forwarded. A combined waveform
|
|
37
|
+
# feeding back into its own oscillator steps cycle by cycle, and a span
|
|
38
|
+
# never runs past an MSB rise that hard-syncs the next voice, so both
|
|
39
|
+
# land on a whole cycle.
|
|
40
|
+
def span(cycles)
|
|
41
|
+
return 1 if @stepped
|
|
42
|
+
|
|
43
|
+
span = @synthesizing ? synthesis_span(cycles) : cycles
|
|
44
|
+
return span unless @synced
|
|
45
|
+
|
|
46
|
+
span = sync_span(@waveform1, span)
|
|
47
|
+
span = sync_span(@waveform2, span)
|
|
48
|
+
sync_span(@waveform3, span)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The filter steps at most @filter_chunk cycles at a time, and never
|
|
52
|
+
# across the end of a recorded sample.
|
|
53
|
+
def synthesis_span(cycles)
|
|
54
|
+
span = [cycles, @filter_chunk].min
|
|
55
|
+
return span unless @decimator
|
|
56
|
+
|
|
57
|
+
[@decimator.cycles_to_close, span].min
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Whether a span has to step, or watch for hard sync, only changes with
|
|
61
|
+
# a voice's control register.
|
|
62
|
+
def update_span_rules
|
|
63
|
+
@stepped = @voices.any? { |voice| voice.waveform.stepped?(@synthesizing) }
|
|
64
|
+
@synced = @voices.any? { |voice| voice.waveform.sync? }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def sync_span(waveform, span)
|
|
68
|
+
return span unless waveform.sync_dest.sync?
|
|
69
|
+
|
|
70
|
+
rise = waveform.cycles_to_msb_rise
|
|
71
|
+
rise && rise < span ? rise : span
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Unrolled over the three voices: the per-cycle block calls cost
|
|
75
|
+
# measurably on the synthesis path.
|
|
76
|
+
def fast_forward(cycles)
|
|
77
|
+
@voice1.fast_forward(cycles)
|
|
78
|
+
@voice2.fast_forward(cycles)
|
|
79
|
+
@voice3.fast_forward(cycles)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# The last cycle of a span, run whole. The filter integrates the span
|
|
83
|
+
# from the voices' output at its end.
|
|
84
|
+
def clock!(span)
|
|
85
|
+
@voice1.cycle!
|
|
86
|
+
@voice2.cycle!
|
|
87
|
+
@voice3.cycle!
|
|
88
|
+
@waveform1.synchronize!
|
|
89
|
+
@waveform2.synchronize!
|
|
90
|
+
@waveform3.synchronize!
|
|
91
|
+
return unless @synthesizing
|
|
92
|
+
|
|
93
|
+
@filter.cycle!(@voices, span)
|
|
94
|
+
record_sample(span) if @decimator
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def record_sample(span)
|
|
98
|
+
sample = @decimator.push(current_sample, span)
|
|
99
|
+
@samples << sample if sample
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def current_sample
|
|
103
|
+
sample = @filter.output / SAMPLE_DIVISOR
|
|
104
|
+
return sample unless sample < -0x8000 || sample > 0x7fff
|
|
105
|
+
|
|
106
|
+
sample.clamp(-0x8000, 0x7fff)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -16,10 +16,11 @@ module Badline
|
|
|
16
16
|
@phase = 0
|
|
17
17
|
@sum = 0
|
|
18
18
|
@count = 0
|
|
19
|
+
@cycles_to_close = window_cycles
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
# Cycles until the current window closes, counting the next one as 1.
|
|
22
|
-
|
|
23
|
+
attr_reader :cycles_to_close
|
|
23
24
|
|
|
24
25
|
# Returns the averaged sample when these cycles close a window, and
|
|
25
26
|
# nil otherwise. They must not run past the window's end.
|
|
@@ -27,14 +28,18 @@ module Badline
|
|
|
27
28
|
@sum += sample * cycles
|
|
28
29
|
@count += cycles
|
|
29
30
|
@phase += @rate * cycles
|
|
31
|
+
@cycles_to_close -= cycles
|
|
30
32
|
return unless @phase >= @clock_hz
|
|
31
33
|
|
|
32
34
|
@phase -= @clock_hz
|
|
35
|
+
@cycles_to_close = window_cycles
|
|
33
36
|
average
|
|
34
37
|
end
|
|
35
38
|
|
|
36
39
|
private
|
|
37
40
|
|
|
41
|
+
def window_cycles = (@clock_hz - @phase + @rate - 1) / @rate
|
|
42
|
+
|
|
38
43
|
def average
|
|
39
44
|
value = @sum.fdiv(@count).round
|
|
40
45
|
@sum = 0
|
data/lib/badline/sid/filter.rb
CHANGED
|
@@ -182,7 +182,7 @@ module Badline
|
|
|
182
182
|
@unfiltered = 0
|
|
183
183
|
route_voice(voice1.output >> 7, 0x1)
|
|
184
184
|
route_voice(voice2.output >> 7, 0x2)
|
|
185
|
-
if @routing
|
|
185
|
+
if @routing & 0x4 != 0
|
|
186
186
|
@input += voice3.output >> 7
|
|
187
187
|
elsif !@voice3_off
|
|
188
188
|
@unfiltered += voice3.output >> 7
|
|
@@ -190,7 +190,7 @@ module Badline
|
|
|
190
190
|
end
|
|
191
191
|
|
|
192
192
|
def route_voice(output, bit)
|
|
193
|
-
if @routing
|
|
193
|
+
if @routing & bit == bit
|
|
194
194
|
@input += output
|
|
195
195
|
else
|
|
196
196
|
@unfiltered += output
|
|
@@ -203,9 +203,9 @@ module Badline
|
|
|
203
203
|
|
|
204
204
|
def filtered
|
|
205
205
|
value = 0
|
|
206
|
-
value += @lowpass if @mode
|
|
207
|
-
value += @bandpass if @mode
|
|
208
|
-
value += @highpass if @mode
|
|
206
|
+
value += @lowpass if @mode & 0x1 != 0
|
|
207
|
+
value += @bandpass if @mode & 0x2 != 0
|
|
208
|
+
value += @highpass if @mode & 0x4 != 0
|
|
209
209
|
value
|
|
210
210
|
end
|
|
211
211
|
end
|
|
@@ -14,11 +14,11 @@ module Badline
|
|
|
14
14
|
if @test
|
|
15
15
|
bleed_shift_register(cycles)
|
|
16
16
|
else
|
|
17
|
-
while @shift_pipeline
|
|
17
|
+
while @shift_pipeline != 0 && cycles != 0
|
|
18
18
|
advance
|
|
19
19
|
cycles -= 1
|
|
20
20
|
end
|
|
21
|
-
skip_ahead(cycles) if cycles
|
|
21
|
+
skip_ahead(cycles) if cycles != 0 && @frequency != 0
|
|
22
22
|
compare_pulse(((@accumulator - @frequency) & 0xffffff) >> 12)
|
|
23
23
|
end
|
|
24
24
|
@stale = true
|
|
@@ -59,9 +59,9 @@ module Badline
|
|
|
59
59
|
previous = @accumulator
|
|
60
60
|
@accumulator += @frequency * cycles
|
|
61
61
|
rises = bit19_rises(previous, @accumulator)
|
|
62
|
-
if rises
|
|
62
|
+
if rises != 0
|
|
63
63
|
@shift_pipeline = pipeline_after(cycles)
|
|
64
|
-
rises -= 1 if @shift_pipeline
|
|
64
|
+
rises -= 1 if @shift_pipeline != 0
|
|
65
65
|
rises.times { shift_noise }
|
|
66
66
|
end
|
|
67
67
|
@accumulator &= 0xffffff
|
|
@@ -74,27 +74,27 @@ module Badline
|
|
|
74
74
|
# How far a rise on one of the last two cycles has come.
|
|
75
75
|
def pipeline_after(cycles)
|
|
76
76
|
last = @accumulator - @frequency
|
|
77
|
-
return 2 if bit19_rises(last, @accumulator)
|
|
78
|
-
return 1 if cycles > 1 && bit19_rises(last - @frequency, last)
|
|
77
|
+
return 2 if bit19_rises(last, @accumulator) != 0
|
|
78
|
+
return 1 if cycles > 1 && bit19_rises(last - @frequency, last) != 0
|
|
79
79
|
|
|
80
80
|
0
|
|
81
81
|
end
|
|
82
82
|
|
|
83
83
|
def drain_floating(cycles)
|
|
84
|
-
return
|
|
84
|
+
return if @floating_ttl.zero?
|
|
85
85
|
|
|
86
86
|
@floating_ttl -= cycles
|
|
87
|
-
return
|
|
87
|
+
return unless @floating_ttl <= 0
|
|
88
88
|
|
|
89
89
|
@floating_ttl = 0
|
|
90
90
|
@floating = 0x000
|
|
91
91
|
end
|
|
92
92
|
|
|
93
93
|
def bleed_shift_register(cycles = 1)
|
|
94
|
-
return
|
|
94
|
+
return if @shift_register_reset.zero?
|
|
95
95
|
|
|
96
96
|
@shift_register_reset -= cycles
|
|
97
|
-
return
|
|
97
|
+
return unless @shift_register_reset <= 0
|
|
98
98
|
|
|
99
99
|
@shift_register_reset = 0
|
|
100
100
|
@shift_register = SHIFT_REGISTER_RESET
|
|
@@ -49,29 +49,47 @@ module Badline
|
|
|
49
49
|
NOISE_TAPS.each { |bit, line| @shift_register &= ~bit if value.nobits?(line) }
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
+
# Setting the test bit starts the bleed, and writes the output of a
|
|
53
|
+
# noise combination back first, from the accumulator it had. Only
|
|
54
|
+
# SID/noiselfsrinit sets the bit onto such a combination from outside
|
|
55
|
+
# it: its $f8/$80 pairs go from noise alone to all four.
|
|
56
|
+
def raise_test
|
|
57
|
+
@shift_register_reset = @shift_register_reset_delay
|
|
58
|
+
return unless @selected.anybits?(0x8) && combined?(@selected) && @shift_pipeline != 1
|
|
59
|
+
|
|
60
|
+
write_shift_register(write_back(@selected, shape(@selected)))
|
|
61
|
+
end
|
|
62
|
+
|
|
52
63
|
# Releasing the test bit finishes the shift it interrupted: the old
|
|
53
64
|
# waveform's output may be written back first, then a bit clocks in
|
|
54
|
-
# over the forced-high bit 22.
|
|
55
|
-
# pulse+noise of the old waveform writes the lowest noise lines low.
|
|
65
|
+
# over the forced-high bit 22.
|
|
56
66
|
def release_test(previous)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
elsif release_writes_back?(previous, @selected)
|
|
60
|
-
write_shift_register(write_back(previous, shape(previous)))
|
|
61
|
-
end
|
|
67
|
+
value = release_write(previous)
|
|
68
|
+
write_shift_register(value) if value
|
|
62
69
|
shift_noise(1)
|
|
63
70
|
end
|
|
64
71
|
|
|
72
|
+
# What the release writes over the taps, or nil. On the 6581, a change
|
|
73
|
+
# that keeps only pulse+noise of the old waveform writes the lowest
|
|
74
|
+
# noise lines low. On the 8580, a change from pulse+noise to all four
|
|
75
|
+
# writes every line low.
|
|
76
|
+
def release_write(previous)
|
|
77
|
+
if @topbit_feedback
|
|
78
|
+
return noise & PULSE_NOISE_RELEASE if previous != 0xc && (previous & @selected) == 0xc
|
|
79
|
+
elsif previous == 0xc && @selected == 0xf
|
|
80
|
+
return 0x000
|
|
81
|
+
end
|
|
82
|
+
write_back(previous, shape(previous)) if release_writes_back?(previous, @selected)
|
|
83
|
+
end
|
|
84
|
+
|
|
65
85
|
# Which waveform changes write the old output back as the test bit
|
|
66
86
|
# falls (SID/wb_testsuite, after libresidfp's do_writeback). Noise has
|
|
67
|
-
# to have been combined before and still be
|
|
68
|
-
#
|
|
69
|
-
# nor
|
|
70
|
-
#
|
|
71
|
-
# back.
|
|
87
|
+
# to have been combined before and still be combined after. Nothing
|
|
88
|
+
# is written back on changing to pulse+noise or from pulse+noise to
|
|
89
|
+
# sawtooth+noise, nor, on the 6581, on trading triangle for sawtooth
|
|
90
|
+
# or back.
|
|
72
91
|
def release_writes_back?(previous, selected)
|
|
73
|
-
return false if previous <= 0x8 || selected
|
|
74
|
-
return previous == 0xf if selected == 0x8
|
|
92
|
+
return false if previous <= 0x8 || selected <= 0x8
|
|
75
93
|
return false if selected == 0xc || (previous == 0xc && selected == 0xa)
|
|
76
94
|
|
|
77
95
|
!(@topbit_feedback && [previous & 0x3, selected & 0x3].sort == [0x1, 0x2])
|
data/lib/badline/sid/waveform.rb
CHANGED
|
@@ -114,10 +114,10 @@ module Badline
|
|
|
114
114
|
@sync = value.anybits?(0x02)
|
|
115
115
|
test = value.anybits?(0x08)
|
|
116
116
|
if test
|
|
117
|
+
raise_test unless @test
|
|
117
118
|
@accumulator = 0x000000
|
|
118
119
|
@pulse = 0xfff
|
|
119
120
|
@shift_pipeline = 0
|
|
120
|
-
@shift_register_reset = @shift_register_reset_delay unless @test
|
|
121
121
|
elsif @test
|
|
122
122
|
release_test(previous)
|
|
123
123
|
end
|
|
@@ -218,7 +218,7 @@ module Badline
|
|
|
218
218
|
def compare_pulse(phase = @accumulator >> 12) = (@pulse = @test || phase >= @pulse_width ? 0xfff : 0x000)
|
|
219
219
|
|
|
220
220
|
# More than one bit set in the selection.
|
|
221
|
-
def combined?(selected) = selected
|
|
221
|
+
def combined?(selected) = selected & (selected - 1) != 0
|
|
222
222
|
|
|
223
223
|
# The one point in the cycle where the waveform selector drives the DAC:
|
|
224
224
|
# either it latches a fresh value, or nothing drives the line and the
|
|
@@ -259,10 +259,10 @@ module Badline
|
|
|
259
259
|
def advance
|
|
260
260
|
previous = @accumulator
|
|
261
261
|
@accumulator = (previous + @frequency) & 0xffffff
|
|
262
|
-
@msb_rising = previous
|
|
263
|
-
if previous
|
|
262
|
+
@msb_rising = @accumulator & ~previous & MSB != 0
|
|
263
|
+
if @accumulator & ~previous & 0x080000 != 0
|
|
264
264
|
@shift_pipeline = 2
|
|
265
|
-
elsif @shift_pipeline
|
|
265
|
+
elsif @shift_pipeline != 0 && (@shift_pipeline -= 1).zero?
|
|
266
266
|
write_shift_register(write_back(@selected, @output)) if release_writes_back?(@selected, @selected)
|
|
267
267
|
shift_noise
|
|
268
268
|
end
|
|
@@ -277,12 +277,12 @@ module Badline
|
|
|
277
277
|
# TriXOR = !Saw & ((!V3 & Ring) ^ bit23), where V3 is the modulating
|
|
278
278
|
# voice's MSB (SID/ringmod).
|
|
279
279
|
def inverted?(selected)
|
|
280
|
-
return false if selected
|
|
280
|
+
return false if selected & 0x2 != 0
|
|
281
281
|
|
|
282
|
-
msb = @accumulator
|
|
282
|
+
msb = @accumulator & MSB != 0
|
|
283
283
|
return msb unless @ring
|
|
284
284
|
|
|
285
|
-
msb
|
|
285
|
+
msb == (@sync_source.accumulator & MSB != 0)
|
|
286
286
|
end
|
|
287
287
|
end
|
|
288
288
|
end
|
data/lib/badline/sid.rb
CHANGED
|
@@ -5,6 +5,7 @@ require "badline/sid/envelope"
|
|
|
5
5
|
require "badline/sid/voice"
|
|
6
6
|
require "badline/sid/filter"
|
|
7
7
|
require "badline/sid/decimator"
|
|
8
|
+
require "badline/sid/catch_up"
|
|
8
9
|
|
|
9
10
|
module Badline
|
|
10
11
|
# SID (Sound Interface Device) chip, in either the 6581 or the 8580
|
|
@@ -31,13 +32,16 @@ module Badline
|
|
|
31
32
|
# DC offset that the RC network on the C64 board filters out.
|
|
32
33
|
class SID
|
|
33
34
|
include Addressable
|
|
35
|
+
include CatchUp
|
|
34
36
|
|
|
35
37
|
VOICES = 3
|
|
36
38
|
|
|
37
39
|
# Cycles the data bus holds a value, measured through SID/bitfade.
|
|
38
40
|
BUS_TTL = { mos6581: 0x1d00, mos8580: 0xa2000 }.freeze
|
|
39
41
|
|
|
40
|
-
# Scales the filter's 20-bit mix down to a signed 16-bit sample
|
|
42
|
+
# Scales the filter's 20-bit mix down to a signed 16-bit sample, as
|
|
43
|
+
# reSID 0.16 does: three voices at full swing and volume 15 span the
|
|
44
|
+
# whole range, on either chip.
|
|
41
45
|
SAMPLE_DIVISOR = ((0xfff * 0xff) >> 7) * 3 * 15 * 2 / (2**16)
|
|
42
46
|
|
|
43
47
|
# Writes the DSP queues between catch-ups. A full queue catches up
|
|
@@ -58,6 +62,7 @@ module Badline
|
|
|
58
62
|
@bus_ttl_reset = BUS_TTL.fetch(model)
|
|
59
63
|
@filter = Filter.new(model:)
|
|
60
64
|
@synthesizing = false
|
|
65
|
+
@stepped = @synced = false
|
|
61
66
|
@pending_cycles = 0
|
|
62
67
|
@deferred_writes = []
|
|
63
68
|
@decimator = nil
|
|
@@ -78,6 +83,7 @@ module Badline
|
|
|
78
83
|
@bus_ttl = 0
|
|
79
84
|
@voices.each(&:reset!)
|
|
80
85
|
@filter.reset
|
|
86
|
+
update_span_rules
|
|
81
87
|
end
|
|
82
88
|
|
|
83
89
|
# The DSP only counts cycles as they pass. Whatever asks for its state
|
|
@@ -90,17 +96,19 @@ module Badline
|
|
|
90
96
|
return if @synthesizing
|
|
91
97
|
|
|
92
98
|
@synthesizing = true
|
|
99
|
+
update_span_rules
|
|
93
100
|
catch_up
|
|
94
101
|
end
|
|
95
102
|
|
|
96
103
|
# Starts collecting the output, averaged down to `rate` samples a
|
|
97
104
|
# second, for #drain_samples. `filter_chunk` overrides the one the SID
|
|
98
|
-
# was built with, for a machine that built its own.
|
|
99
|
-
|
|
105
|
+
# was built with, for a machine that built its own. `clock_hz` is the
|
|
106
|
+
# rate the SID's cycles are taken to run at.
|
|
107
|
+
def record(rate:, filter_chunk: @filter_chunk, clock_hz: TimeOfDay::CLOCK_HZ)
|
|
100
108
|
synthesize!
|
|
101
109
|
catch_up
|
|
102
110
|
@filter_chunk = filter_chunk
|
|
103
|
-
@decimator = Decimator.new(clock_hz
|
|
111
|
+
@decimator = Decimator.new(clock_hz:, rate:)
|
|
104
112
|
@samples = []
|
|
105
113
|
end
|
|
106
114
|
|
|
@@ -152,7 +160,7 @@ module Badline
|
|
|
152
160
|
end
|
|
153
161
|
|
|
154
162
|
def peek(addr)
|
|
155
|
-
case
|
|
163
|
+
case offset_of(addr) % (2**5)
|
|
156
164
|
when 0x19 then latch(pots ? pots.pot_x : 0xff)
|
|
157
165
|
when 0x1a then latch(pots ? pots.pot_y : 0xff)
|
|
158
166
|
when 0x1b then latch(osc3)
|
|
@@ -162,7 +170,7 @@ module Badline
|
|
|
162
170
|
end
|
|
163
171
|
|
|
164
172
|
def poke(addr, value)
|
|
165
|
-
reg =
|
|
173
|
+
reg = offset_of(addr) % (2**5)
|
|
166
174
|
write_register(reg, value) if reg <= 0x18
|
|
167
175
|
latch(value)
|
|
168
176
|
end
|
|
@@ -172,90 +180,6 @@ module Badline
|
|
|
172
180
|
|
|
173
181
|
private
|
|
174
182
|
|
|
175
|
-
# Runs the cycles that have passed, landing each queued write on the
|
|
176
|
-
# cycle the CPU wrote it on.
|
|
177
|
-
def catch_up
|
|
178
|
-
replayed = 0
|
|
179
|
-
@deferred_writes.each do |cycle, reg, value|
|
|
180
|
-
run(cycle - replayed)
|
|
181
|
-
replayed = cycle
|
|
182
|
-
apply_write(reg, value)
|
|
183
|
-
end
|
|
184
|
-
run(@pending_cycles - replayed)
|
|
185
|
-
@deferred_writes.clear
|
|
186
|
-
@pending_cycles = 0
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
def run(cycles)
|
|
190
|
-
while cycles.positive?
|
|
191
|
-
span = span(cycles)
|
|
192
|
-
fast_forward(span - 1) if span > 1
|
|
193
|
-
clock!(span)
|
|
194
|
-
cycles -= span
|
|
195
|
-
end
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
# The longest stretch that can be fast-forwarded. A combined waveform
|
|
199
|
-
# feeding back into its own oscillator steps cycle by cycle, and a span
|
|
200
|
-
# never runs past an MSB rise that hard-syncs the next voice, so both
|
|
201
|
-
# land on a whole cycle.
|
|
202
|
-
def span(cycles)
|
|
203
|
-
synthesizing = @synthesizing
|
|
204
|
-
return 1 if @waveform1.stepped?(synthesizing) || @waveform2.stepped?(synthesizing) ||
|
|
205
|
-
@waveform3.stepped?(synthesizing)
|
|
206
|
-
|
|
207
|
-
span = synthesizing ? synthesis_span(cycles) : cycles
|
|
208
|
-
span = sync_span(@waveform1, span)
|
|
209
|
-
span = sync_span(@waveform2, span)
|
|
210
|
-
sync_span(@waveform3, span)
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
# The filter steps at most @filter_chunk cycles at a time, and never
|
|
214
|
-
# across the end of a recorded sample.
|
|
215
|
-
def synthesis_span(cycles)
|
|
216
|
-
span = [cycles, @filter_chunk].min
|
|
217
|
-
return span unless @decimator
|
|
218
|
-
|
|
219
|
-
[@decimator.cycles_to_close, span].min
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
def sync_span(waveform, span)
|
|
223
|
-
return span unless waveform.sync_dest.sync?
|
|
224
|
-
|
|
225
|
-
rise = waveform.cycles_to_msb_rise
|
|
226
|
-
rise && rise < span ? rise : span
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
# Unrolled over the three voices: the per-cycle block calls cost
|
|
230
|
-
# measurably on the synthesis path.
|
|
231
|
-
def fast_forward(cycles)
|
|
232
|
-
@voice1.fast_forward(cycles)
|
|
233
|
-
@voice2.fast_forward(cycles)
|
|
234
|
-
@voice3.fast_forward(cycles)
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
# The last cycle of a span, run whole. The filter integrates the span
|
|
238
|
-
# from the voices' output at its end.
|
|
239
|
-
def clock!(span)
|
|
240
|
-
@voice1.cycle!
|
|
241
|
-
@voice2.cycle!
|
|
242
|
-
@voice3.cycle!
|
|
243
|
-
@waveform1.synchronize!
|
|
244
|
-
@waveform2.synchronize!
|
|
245
|
-
@waveform3.synchronize!
|
|
246
|
-
return unless @synthesizing
|
|
247
|
-
|
|
248
|
-
@filter.cycle!(@voices, span)
|
|
249
|
-
record_sample(span) if @decimator
|
|
250
|
-
end
|
|
251
|
-
|
|
252
|
-
def record_sample(span)
|
|
253
|
-
sample = @decimator.push(current_sample, span)
|
|
254
|
-
@samples << sample if sample
|
|
255
|
-
end
|
|
256
|
-
|
|
257
|
-
def current_sample = (@filter.output / SAMPLE_DIVISOR).clamp(-0x8000, 0x7fff)
|
|
258
|
-
|
|
259
183
|
def latch(value)
|
|
260
184
|
@bus_ttl = @bus_ttl_reset
|
|
261
185
|
@bus_value = value
|
|
@@ -268,8 +192,8 @@ module Badline
|
|
|
268
192
|
|
|
269
193
|
def write_register(reg, value)
|
|
270
194
|
@registers.poke(reg, value)
|
|
271
|
-
catch_up if @deferred_writes.length == DEFERRED_WRITES
|
|
272
|
-
@deferred_writes
|
|
195
|
+
catch_up if @deferred_writes.length == DEFERRED_WRITES * 3
|
|
196
|
+
@deferred_writes.push(@pending_cycles, reg, value)
|
|
273
197
|
end
|
|
274
198
|
|
|
275
199
|
def apply_write(reg, value)
|
|
@@ -283,7 +207,9 @@ module Badline
|
|
|
283
207
|
when 1 then voice.waveform.frequency_high = value
|
|
284
208
|
when 2 then voice.waveform.pulse_width_low = value
|
|
285
209
|
when 3 then voice.waveform.pulse_width_high = value
|
|
286
|
-
when 4
|
|
210
|
+
when 4
|
|
211
|
+
voice.control = value
|
|
212
|
+
update_span_rules
|
|
287
213
|
when 5 then voice.envelope.attack_decay = value
|
|
288
214
|
when 6 then voice.envelope.sustain_release = value
|
|
289
215
|
end
|
|
@@ -13,6 +13,16 @@ module Badline
|
|
|
13
13
|
|
|
14
14
|
def directory_track = 18
|
|
15
15
|
def directory_sector = 1
|
|
16
|
+
def directory_interleave = 3
|
|
17
|
+
def interleave = 10
|
|
18
|
+
def reserved_tracks = [18]
|
|
19
|
+
|
|
20
|
+
# The BAM in the header block covers the 35 tracks of a standard
|
|
21
|
+
# disk. The layouts that extend it to 40 tracks differ between DOS
|
|
22
|
+
# versions, so the tracks past 35 stay unused.
|
|
23
|
+
def bam_tracks = 1..35
|
|
24
|
+
def bam_count(track) = [18, 0, 4 * track]
|
|
25
|
+
def bam_bitmap(track) = [18, 0, (4 * track) + 1]
|
|
16
26
|
|
|
17
27
|
def sectors_in(track)
|
|
18
28
|
case track
|
|
@@ -9,6 +9,18 @@ module Badline
|
|
|
9
9
|
|
|
10
10
|
def error_tables = ERROR_TABLES
|
|
11
11
|
|
|
12
|
+
def interleave = 6
|
|
13
|
+
def reserved_tracks = [18, 53]
|
|
14
|
+
|
|
15
|
+
# The header block flags a double-sided disk. The second side's free
|
|
16
|
+
# counts follow the first side's BAM there, and its bitmaps take the
|
|
17
|
+
# first block of track 53.
|
|
18
|
+
def bam_tracks = double_sided? ? 1..70 : super
|
|
19
|
+
def bam_count(track) = track > 35 ? [18, 0, 0xdd + track - 36] : super
|
|
20
|
+
def bam_bitmap(track) = track > 35 ? [53, 0, 3 * (track - 36)] : super
|
|
21
|
+
|
|
22
|
+
def double_sided? = @bytes[sector_offset(18, 0) + 3].anybits?(0x80)
|
|
23
|
+
|
|
12
24
|
def sectors_in(track)
|
|
13
25
|
track > 35 ? super(track - 35) : super
|
|
14
26
|
end
|
|
@@ -11,6 +11,15 @@ module Badline
|
|
|
11
11
|
|
|
12
12
|
def directory_track = 40
|
|
13
13
|
def directory_sector = 3
|
|
14
|
+
def directory_interleave = 1
|
|
15
|
+
def interleave = 1
|
|
16
|
+
def reserved_tracks = [40]
|
|
17
|
+
|
|
18
|
+
# Blocks 1 and 2 of track 40 hold the BAM for tracks 1 to 40 and 41
|
|
19
|
+
# to 80, six bytes a track from offset 16.
|
|
20
|
+
def bam_tracks = 1..80
|
|
21
|
+
def bam_count(track) = [40, track > 40 ? 2 : 1, 0x10 + (((track - 1) % 40) * 6)]
|
|
22
|
+
def bam_bitmap(track) = [40, track > 40 ? 2 : 1, 0x11 + (((track - 1) % 40) * 6)]
|
|
14
23
|
|
|
15
24
|
def sectors_in(_track) = 40
|
|
16
25
|
end
|