badline 0.2.0 → 0.3.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 +67 -0
- data/CLAUDE.md +6 -0
- data/README.md +24 -6
- data/doc/pinned-behaviour.md +18 -6
- data/exe/badline +5 -2
- data/lib/badline/address_bus.rb +14 -6
- data/lib/badline/addressable.rb +1 -1
- data/lib/badline/audio/bare_player.rb +20 -3
- data/lib/badline/audio/machine_player.rb +9 -6
- data/lib/badline/audio/renderer.rb +3 -3
- data/lib/badline/audio/stream.rb +117 -0
- data/lib/badline/audio.rb +1 -0
- data/lib/badline/cartridge/easy_flash.rb +6 -2
- data/lib/badline/cartridge/epyx_fastload.rb +1 -1
- 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 +16 -8
- data/lib/badline/checkpoint.rb +98 -0
- data/lib/badline/cia/interrupt_register.rb +20 -6
- data/lib/badline/cia.rb +36 -19
- data/lib/badline/color_memory.rb +2 -2
- data/lib/badline/computer.rb +41 -11
- data/lib/badline/cpu/microcode.rb +41 -43
- data/lib/badline/cpu.rb +3 -3
- data/lib/badline/gui/application.rb +41 -9
- data/lib/badline/kernal_trap/channel.rb +118 -8
- data/lib/badline/kernal_trap/drive/block_commands.rb +79 -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 +49 -0
- data/lib/badline/kernal_trap/drive.rb +148 -132
- data/lib/badline/kernal_trap/file.rb +0 -5
- data/lib/badline/kernal_trap/load.rb +93 -25
- data/lib/badline/kernal_trap/routine.rb +17 -0
- data/lib/badline/kernal_trap/save.rb +48 -15
- data/lib/badline/kernal_trap/serial.rb +34 -2
- data/lib/badline/media.rb +2 -2
- data/lib/badline/memory.rb +7 -2
- data/lib/badline/rom.rb +14 -5
- 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.rb +7 -7
- data/lib/badline/sid.rb +16 -92
- data/lib/badline/status.rb +255 -32
- 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 +78 -17
- data/lib/badline/storage/host_directory.rb +26 -2
- data/lib/badline/storage/sid_file.rb +37 -3
- data/lib/badline/storage.rb +3 -1
- data/lib/badline/time_of_day.rb +7 -0
- data/lib/badline/version.rb +1 -1
- data/lib/badline/vic/graphics_shifter.rb +8 -2
- data/lib/badline/vic/registers.rb +16 -27
- data/lib/badline/vic.rb +23 -10
- data/lib/badline.rb +1 -0
- metadata +9 -1
|
@@ -23,6 +23,7 @@ module Badline
|
|
|
23
23
|
CLOSE = 0xe0
|
|
24
24
|
|
|
25
25
|
# ST bits at $90
|
|
26
|
+
DEVICE_NOT_PRESENT = 0x80
|
|
26
27
|
EOI = 0x40
|
|
27
28
|
READ_TIMEOUT = 0x02
|
|
28
29
|
|
|
@@ -41,13 +42,26 @@ module Badline
|
|
|
41
42
|
|
|
42
43
|
def install
|
|
43
44
|
ROUTINES.each do |address, routine|
|
|
44
|
-
@cpu.install_trap(address) {
|
|
45
|
+
@cpu.install_trap(address) { call_routine(routine) if kernal? }
|
|
45
46
|
end
|
|
46
47
|
self
|
|
47
48
|
end
|
|
48
49
|
|
|
49
50
|
private
|
|
50
51
|
|
|
52
|
+
def call_routine(routine)
|
|
53
|
+
case routine
|
|
54
|
+
when :talk then talk
|
|
55
|
+
when :listen then listen
|
|
56
|
+
when :second then second
|
|
57
|
+
when :tksa then tksa
|
|
58
|
+
when :ciout then ciout
|
|
59
|
+
when :untalk then untalk
|
|
60
|
+
when :unlisten then unlisten
|
|
61
|
+
when :acptr then acptr
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
51
65
|
# Addressing the bus starts a new frame. The channel number arrives
|
|
52
66
|
# in the secondary address that follows.
|
|
53
67
|
def talk
|
|
@@ -82,22 +96,40 @@ module Badline
|
|
|
82
96
|
return unless @listening
|
|
83
97
|
|
|
84
98
|
@buffer << @cpu.a
|
|
99
|
+
update_status(DEVICE_NOT_PRESENT) unless accepted?
|
|
85
100
|
@cpu.status.carry = false
|
|
86
101
|
return_to_caller
|
|
87
102
|
end
|
|
88
103
|
|
|
104
|
+
# The drive acknowledges every byte of an open or close frame, but
|
|
105
|
+
# takes data only on a channel it has open. Otherwise nothing holds
|
|
106
|
+
# the data line when a byte starts, so the KERNAL finds no device.
|
|
107
|
+
def accepted?
|
|
108
|
+
[OPEN, CLOSE].include?(@frame) || @drive.listening?(@listen_channel)
|
|
109
|
+
end
|
|
110
|
+
|
|
89
111
|
def unlisten
|
|
90
112
|
return unless @listening
|
|
91
113
|
|
|
92
114
|
@listening = false
|
|
93
115
|
deliver_frame
|
|
94
|
-
|
|
116
|
+
release_bus
|
|
95
117
|
end
|
|
96
118
|
|
|
97
119
|
def untalk
|
|
98
120
|
return unless @talking
|
|
99
121
|
|
|
100
122
|
@talking = false
|
|
123
|
+
release_bus
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# UNLSN and UNTLK end by releasing ATN, the clock and the data line on
|
|
127
|
+
# CIA 2's port A, and leave the port's last read in the accumulator.
|
|
128
|
+
def release_bus
|
|
129
|
+
@bus.poke(0xdd00, @bus.peek(0xdd00) & 0xc7)
|
|
130
|
+
@cpu.a = @bus.peek(0xdd00) & 0xdf
|
|
131
|
+
@cpu.status.negative = @cpu.a.anybits?(0x80)
|
|
132
|
+
@cpu.status.zero = @cpu.a.zero?
|
|
101
133
|
return_to_caller
|
|
102
134
|
end
|
|
103
135
|
|
data/lib/badline/media.rb
CHANGED
|
@@ -59,8 +59,8 @@ module Badline
|
|
|
59
59
|
|
|
60
60
|
def start_tune(computer, tune, autostart:, song:)
|
|
61
61
|
computer.ram.write(tune.load_address, tune.data)
|
|
62
|
-
computer.ram.write(
|
|
63
|
-
computer.type_text(
|
|
62
|
+
tune.boot_memory(song:).each { |address, bytes| computer.ram.write(address, bytes) }
|
|
63
|
+
computer.type_text(tune.boot_command) if autostart
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
def attach_tape(computer, path, autostart:)
|
data/lib/badline/memory.rb
CHANGED
|
@@ -10,11 +10,11 @@ module Badline
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def peek(addr)
|
|
13
|
-
@storage[
|
|
13
|
+
@storage[offset_of(addr)]
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def poke(addr, value)
|
|
17
|
-
@storage[
|
|
17
|
+
@storage[offset_of(addr)] = value
|
|
18
18
|
value
|
|
19
19
|
end
|
|
20
20
|
|
|
@@ -26,6 +26,11 @@ module Badline
|
|
|
26
26
|
Array(bytes).each_with_index { |b, i| poke(addr + i, b) }
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
# Refills the whole memory as if newly built with `initial`
|
|
30
|
+
def clear!(initial = [])
|
|
31
|
+
@storage.replace(zero_fill(initial))
|
|
32
|
+
end
|
|
33
|
+
|
|
29
34
|
private
|
|
30
35
|
|
|
31
36
|
def blank_value
|
data/lib/badline/rom.rb
CHANGED
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
|
+
class << self
|
|
5
|
+
attr_writer :rom_path
|
|
6
|
+
|
|
7
|
+
# The directory ROM images and other bundled binaries load from.
|
|
8
|
+
# Defaults to $BADLINE_ROM_PATH, then to the roms directory next to this
|
|
9
|
+
# file. Assigning nil restores the default.
|
|
10
|
+
def rom_path
|
|
11
|
+
@rom_path ||= ENV.fetch("BADLINE_ROM_PATH", File.expand_path("roms", __dir__))
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
4
15
|
class ROM < Memory
|
|
5
16
|
class << self
|
|
6
17
|
def load(filename, start = 0x0)
|
|
7
|
-
data =
|
|
18
|
+
data = read(filename)
|
|
8
19
|
new(data, length: data.length, start: start)
|
|
9
20
|
end
|
|
10
21
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def file_path(filename)
|
|
14
|
-
File.join(File.dirname(__FILE__), "roms", filename)
|
|
22
|
+
def read(filename)
|
|
23
|
+
File.binread(File.join(Badline.rom_path, filename)).bytes
|
|
15
24
|
end
|
|
16
25
|
end
|
|
17
26
|
|
|
@@ -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
|
data/lib/badline/sid/waveform.rb
CHANGED
|
@@ -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,6 +32,7 @@ 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
|
|
|
@@ -58,6 +60,7 @@ module Badline
|
|
|
58
60
|
@bus_ttl_reset = BUS_TTL.fetch(model)
|
|
59
61
|
@filter = Filter.new(model:)
|
|
60
62
|
@synthesizing = false
|
|
63
|
+
@stepped = @synced = false
|
|
61
64
|
@pending_cycles = 0
|
|
62
65
|
@deferred_writes = []
|
|
63
66
|
@decimator = nil
|
|
@@ -78,6 +81,7 @@ module Badline
|
|
|
78
81
|
@bus_ttl = 0
|
|
79
82
|
@voices.each(&:reset!)
|
|
80
83
|
@filter.reset
|
|
84
|
+
update_span_rules
|
|
81
85
|
end
|
|
82
86
|
|
|
83
87
|
# The DSP only counts cycles as they pass. Whatever asks for its state
|
|
@@ -90,17 +94,19 @@ module Badline
|
|
|
90
94
|
return if @synthesizing
|
|
91
95
|
|
|
92
96
|
@synthesizing = true
|
|
97
|
+
update_span_rules
|
|
93
98
|
catch_up
|
|
94
99
|
end
|
|
95
100
|
|
|
96
101
|
# Starts collecting the output, averaged down to `rate` samples a
|
|
97
102
|
# second, for #drain_samples. `filter_chunk` overrides the one the SID
|
|
98
|
-
# was built with, for a machine that built its own.
|
|
99
|
-
|
|
103
|
+
# was built with, for a machine that built its own. `clock_hz` is the
|
|
104
|
+
# rate the SID's cycles are taken to run at.
|
|
105
|
+
def record(rate:, filter_chunk: @filter_chunk, clock_hz: TimeOfDay::CLOCK_HZ)
|
|
100
106
|
synthesize!
|
|
101
107
|
catch_up
|
|
102
108
|
@filter_chunk = filter_chunk
|
|
103
|
-
@decimator = Decimator.new(clock_hz
|
|
109
|
+
@decimator = Decimator.new(clock_hz:, rate:)
|
|
104
110
|
@samples = []
|
|
105
111
|
end
|
|
106
112
|
|
|
@@ -152,7 +158,7 @@ module Badline
|
|
|
152
158
|
end
|
|
153
159
|
|
|
154
160
|
def peek(addr)
|
|
155
|
-
case
|
|
161
|
+
case offset_of(addr) % (2**5)
|
|
156
162
|
when 0x19 then latch(pots ? pots.pot_x : 0xff)
|
|
157
163
|
when 0x1a then latch(pots ? pots.pot_y : 0xff)
|
|
158
164
|
when 0x1b then latch(osc3)
|
|
@@ -162,7 +168,7 @@ module Badline
|
|
|
162
168
|
end
|
|
163
169
|
|
|
164
170
|
def poke(addr, value)
|
|
165
|
-
reg =
|
|
171
|
+
reg = offset_of(addr) % (2**5)
|
|
166
172
|
write_register(reg, value) if reg <= 0x18
|
|
167
173
|
latch(value)
|
|
168
174
|
end
|
|
@@ -172,90 +178,6 @@ module Badline
|
|
|
172
178
|
|
|
173
179
|
private
|
|
174
180
|
|
|
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
181
|
def latch(value)
|
|
260
182
|
@bus_ttl = @bus_ttl_reset
|
|
261
183
|
@bus_value = value
|
|
@@ -268,8 +190,8 @@ module Badline
|
|
|
268
190
|
|
|
269
191
|
def write_register(reg, value)
|
|
270
192
|
@registers.poke(reg, value)
|
|
271
|
-
catch_up if @deferred_writes.length == DEFERRED_WRITES
|
|
272
|
-
@deferred_writes
|
|
193
|
+
catch_up if @deferred_writes.length == DEFERRED_WRITES * 3
|
|
194
|
+
@deferred_writes.push(@pending_cycles, reg, value)
|
|
273
195
|
end
|
|
274
196
|
|
|
275
197
|
def apply_write(reg, value)
|
|
@@ -283,7 +205,9 @@ module Badline
|
|
|
283
205
|
when 1 then voice.waveform.frequency_high = value
|
|
284
206
|
when 2 then voice.waveform.pulse_width_low = value
|
|
285
207
|
when 3 then voice.waveform.pulse_width_high = value
|
|
286
|
-
when 4
|
|
208
|
+
when 4
|
|
209
|
+
voice.control = value
|
|
210
|
+
update_span_rules
|
|
287
211
|
when 5 then voice.envelope.attack_decay = value
|
|
288
212
|
when 6 then voice.envelope.sustain_release = value
|
|
289
213
|
end
|