badline 0.2.1 → 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 +49 -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 +6 -4
- 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 +8 -0
- data/lib/badline/checkpoint.rb +98 -0
- data/lib/badline/cia.rb +4 -2
- data/lib/badline/color_memory.rb +2 -2
- data/lib/badline/computer.rb +11 -6
- data/lib/badline/cpu.rb +2 -2
- data/lib/badline/gui/application.rb +41 -9
- data/lib/badline/kernal_trap/channel.rb +99 -7
- 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.rb +88 -110
- data/lib/badline/kernal_trap/file.rb +0 -5
- data/lib/badline/kernal_trap/load.rb +6 -0
- data/lib/badline/kernal_trap/save.rb +5 -4
- data/lib/badline/media.rb +2 -2
- data/lib/badline/memory.rb +2 -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/storage/disk_image.rb +22 -0
- data/lib/badline/storage/host_directory.rb +15 -3
- data/lib/badline/storage/sid_file.rb +36 -2
- 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 +5 -2
- data/lib/badline.rb +1 -0
- metadata +8 -1
|
@@ -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
|
|
@@ -42,6 +42,28 @@ module Badline
|
|
|
42
42
|
nil
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
+
# The track and sector a file's chain starts at.
|
|
46
|
+
def first_block(name, type: :prg)
|
|
47
|
+
entry = find_entry(name, type)
|
|
48
|
+
[entry[:track], entry[:sector]] if entry
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The bytes of the chain that starts at the block, or nil when the
|
|
52
|
+
# block is outside the image's geometry.
|
|
53
|
+
def read_file_at(track, sector)
|
|
54
|
+
read_chain(track, sector) if block?(track, sector)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The track and sector of the last block in a file's chain.
|
|
58
|
+
def last_block(name, type: :prg)
|
|
59
|
+
entry = find_entry(name, type)
|
|
60
|
+
return unless entry
|
|
61
|
+
|
|
62
|
+
last = nil
|
|
63
|
+
each_sector(entry[:track], entry[:sector]) { |_data, track, sector| last = [track, sector] }
|
|
64
|
+
last
|
|
65
|
+
end
|
|
66
|
+
|
|
45
67
|
# Raw block access for the DOS `U1` command. Returns nil for blocks
|
|
46
68
|
# outside the image's geometry.
|
|
47
69
|
def read_block(track, sector)
|
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
module Badline
|
|
4
4
|
module Storage
|
|
5
5
|
# A host directory served as a drive: .prg files by their host name,
|
|
6
|
-
# .p00 and .t64 entries by their embedded names. A
|
|
7
|
-
#
|
|
6
|
+
# .p00 and .t64 entries by their embedded names. A .prg the host can't
|
|
7
|
+
# read is still listed but fails to read, a .p00 or .t64 whose names
|
|
8
|
+
# can't be read is left out, and a failed write reports false.
|
|
8
9
|
class HostDirectory
|
|
10
|
+
NO_SYNC = 21
|
|
11
|
+
|
|
9
12
|
def initialize(path)
|
|
10
13
|
@path = path
|
|
11
14
|
end
|
|
@@ -15,7 +18,16 @@ module Badline
|
|
|
15
18
|
return unless entry
|
|
16
19
|
|
|
17
20
|
archive = entry[:archive]
|
|
18
|
-
archive ? archive.read_file(entry[:name]) : file_bytes(entry[:file])
|
|
21
|
+
archive ? archive.read_file(entry[:name]) : file_bytes(entry[:file]) || []
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# A listed file the host can't read fails before its first byte, as
|
|
25
|
+
# a 1541 does on a disk it finds no sync on.
|
|
26
|
+
def read_error(name, **)
|
|
27
|
+
entry = find(name)
|
|
28
|
+
return unless entry && entry[:file] && !file_bytes(entry[:file])
|
|
29
|
+
|
|
30
|
+
{ error: NO_SYNC, track: 0, sector: 0, offset: 0 }
|
|
19
31
|
end
|
|
20
32
|
|
|
21
33
|
def write_file(name, bytes)
|
|
@@ -13,6 +13,9 @@ module Badline
|
|
|
13
13
|
|
|
14
14
|
V1_HEADER_SIZE = 0x76
|
|
15
15
|
|
|
16
|
+
# BASIC's RUN handler, where a BASIC tune starts.
|
|
17
|
+
BASIC_RUN = 0xa871
|
|
18
|
+
|
|
16
19
|
# The 6502 stub that starts a tune: call init with the song number,
|
|
17
20
|
# point the KERNAL's IRQ vector at a raster handler that calls play,
|
|
18
21
|
# and spin. Both calls are wrapped in the `$01` save, bank and restore
|
|
@@ -171,6 +174,10 @@ module Badline
|
|
|
171
174
|
# code.
|
|
172
175
|
def mus? = psid? && flags[0] == 1
|
|
173
176
|
|
|
177
|
+
# Flag bit 1 marks an RSID body as a BASIC program, which RUN starts
|
|
178
|
+
# instead of a call to init.
|
|
179
|
+
def basic? = !psid? && flags[1] == 1
|
|
180
|
+
|
|
174
181
|
# A v3 header can place a second SID at `$Dxx0` and a v4 header a
|
|
175
182
|
# third, each given by its middle byte: even, and in `$d420-$d7e0` or
|
|
176
183
|
# `$de00-$dfe0`. Anything else means the SID isn't there.
|
|
@@ -185,16 +192,22 @@ module Badline
|
|
|
185
192
|
# 10 the 8580, 11 either and 00 unknown. Only an 8580-only tune gets one.
|
|
186
193
|
def sid_model = flags[4, 2] == 0b10 ? :mos8580 : :mos6581
|
|
187
194
|
|
|
195
|
+
# Flag bits 2-3 name the video standard the same way: 01 PAL, 10 NTSC,
|
|
196
|
+
# 11 either and 00 unknown. Only an NTSC-only tune gets NTSC.
|
|
197
|
+
def ntsc? = flags[2, 2] == 0b10
|
|
198
|
+
|
|
188
199
|
# HVSC keys its song length database on the whole file, header and all.
|
|
189
200
|
def md5 = @md5 ||= Digest::MD5.hexdigest(@bytes.pack("C*"))
|
|
190
201
|
|
|
191
202
|
# The `$01` value a routine at `address` has to run under, following
|
|
192
203
|
# libsidplayfp's iomap: RAM under BASIC from `$a000`, RAM under both
|
|
193
204
|
# ROMs and no I/O for a routine in the I/O window itself, RAM under the
|
|
194
|
-
# KERNAL from `$e000`.
|
|
205
|
+
# KERNAL from `$e000`. A routine below `$a000` still gets BASIC banked
|
|
206
|
+
# out when the image reaches into it, as VSID does, since it may call
|
|
207
|
+
# into the part that lies there. An RSID tune banks itself and gets nil.
|
|
195
208
|
def bank_for(address)
|
|
196
209
|
return unless psid?
|
|
197
|
-
return 0x37 if address < 0xa000
|
|
210
|
+
return (under_basic? ? 0x36 : 0x37) if address < 0xa000
|
|
198
211
|
return 0x36 if address < 0xd000
|
|
199
212
|
return 0x34 if address < 0xe000
|
|
200
213
|
|
|
@@ -227,6 +240,23 @@ module Badline
|
|
|
227
240
|
Driver.new(self, song: song.clamp(1, songs) - 1, base: driver_address).bytes
|
|
228
241
|
end
|
|
229
242
|
|
|
243
|
+
# What a booted machine needs in RAM besides the image before
|
|
244
|
+
# `boot_command` starts the tune, as address => bytes. A BASIC tune
|
|
245
|
+
# gets the end of its program in VARTAB, as LOAD leaves it, and the
|
|
246
|
+
# song in the A, X and Y that SYS loads from `$030c-$030e`, where the
|
|
247
|
+
# tune reads it with PEEK(780).
|
|
248
|
+
def boot_memory(song: start_song)
|
|
249
|
+
return { driver_address => driver(song:) } unless basic?
|
|
250
|
+
|
|
251
|
+
{ 0x2d => [low_byte(end_address), high_byte(end_address)],
|
|
252
|
+
0x030c => [song.clamp(1, songs) - 1] * 3 }
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def boot_command = basic? ? "run\r" : "sys#{driver_address}\r"
|
|
256
|
+
|
|
257
|
+
# Where the CPU first lands once `boot_command` has started the tune.
|
|
258
|
+
def entry_address = basic? ? BASIC_RUN : driver_address
|
|
259
|
+
|
|
230
260
|
private
|
|
231
261
|
|
|
232
262
|
def truncated?
|
|
@@ -237,6 +267,10 @@ module Badline
|
|
|
237
267
|
@body ||= @bytes[data_offset..] || []
|
|
238
268
|
end
|
|
239
269
|
|
|
270
|
+
def under_basic?
|
|
271
|
+
load_address < 0xc000 && end_address > 0xa000
|
|
272
|
+
end
|
|
273
|
+
|
|
240
274
|
# The tape buffer and the free block below the stack page, neither
|
|
241
275
|
# of which BASIC touches.
|
|
242
276
|
def candidates
|
data/lib/badline/storage.rb
CHANGED
|
@@ -36,8 +36,10 @@ module Badline
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
# CBM-style filename pattern: "*" and "?" wildcards, case-insensitive.
|
|
39
|
+
# The DOS stops comparing at the first "*", so whatever follows it
|
|
40
|
+
# is ignored.
|
|
39
41
|
def matcher(name)
|
|
40
|
-
escaped = Regexp.escape(name.downcase)
|
|
42
|
+
escaped = Regexp.escape(name.downcase.sub(/\*.*/m, "*"))
|
|
41
43
|
.gsub('\*', ".*")
|
|
42
44
|
.gsub('\?', ".")
|
|
43
45
|
Regexp.new("\\A#{escaped}\\z")
|
data/lib/badline/time_of_day.rb
CHANGED
|
@@ -49,6 +49,13 @@ module Badline
|
|
|
49
49
|
@latch[:hours]
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
+
# The clock and alarm as stored, read without latching the clock.
|
|
53
|
+
def registers
|
|
54
|
+
[@clock[:tenths], @clock[:seconds], @clock[:minutes], @clock[:hours],
|
|
55
|
+
@alarm[:tenths], @alarm[:seconds], @alarm[:minutes], @alarm[:hours],
|
|
56
|
+
@stopped ? 1 : 0, @latch ? 1 : 0]
|
|
57
|
+
end
|
|
58
|
+
|
|
52
59
|
def write(field, value, alarm:)
|
|
53
60
|
# The frequency counter is held clear while the clock is stopped, and
|
|
54
61
|
# starts counting again from the write to tenths that restarts it.
|
data/lib/badline/version.rb
CHANGED
|
@@ -9,7 +9,8 @@ module Badline
|
|
|
9
9
|
#
|
|
10
10
|
# The byte a group draws loads at pixel XSCROLL. ECM and BMM take hold at
|
|
11
11
|
# pixel 4 as they rise and at pixel 6 as they fall (the 6569's colour
|
|
12
|
-
# latency)
|
|
12
|
+
# latency), except that BMM falls at pixel 5 out of any mode but hi-res
|
|
13
|
+
# bitmap. MCM takes hold at pixel 4 for the colour lookup but only at
|
|
13
14
|
# pixel 7 for how the register is read, where a rising MCM also resets
|
|
14
15
|
# the multicolour flip-flop. An MCM that falls out of the invalid
|
|
15
16
|
# ECM+MCM mode is a pixel later on both counts: the lookup changes at
|
|
@@ -67,7 +68,7 @@ module Badline
|
|
|
67
68
|
case pixel
|
|
68
69
|
when 0 then end_late_read
|
|
69
70
|
when 4 then step_lookup(mode)
|
|
70
|
-
when 5 then
|
|
71
|
+
when 5 then step_early_fall(mode)
|
|
71
72
|
when 6 then @mode_lookup &= mode | 1
|
|
72
73
|
when 7 then step_read
|
|
73
74
|
end
|
|
@@ -78,6 +79,11 @@ module Badline
|
|
|
78
79
|
@mode_lookup = (@mode_lookup & ~1) | (mode & 1) | (@late_mcm ? 1 : 0) | (mode & ECM_BMM)
|
|
79
80
|
end
|
|
80
81
|
|
|
82
|
+
def step_early_fall(mode)
|
|
83
|
+
@mode_lookup &= ~1 if @late_mcm
|
|
84
|
+
@mode_lookup &= mode | ~BMM unless @mode_lookup == BMM
|
|
85
|
+
end
|
|
86
|
+
|
|
81
87
|
def step_read
|
|
82
88
|
return @late_read = true if @late_mcm
|
|
83
89
|
|