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.
Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/hooks/session-start.sh +57 -0
  3. data/.claude/settings.json +14 -0
  4. data/CHANGELOG.md +107 -0
  5. data/CLAUDE.md +51 -16
  6. data/CONTRIBUTING.md +95 -3
  7. data/README.md +119 -51
  8. data/SECURITY.md +9 -0
  9. data/doc/pinned-behaviour.md +293 -39
  10. data/exe/badline-ruby +59 -0
  11. data/lib/badline/address_bus.rb +28 -14
  12. data/lib/badline/addressable.rb +1 -1
  13. data/lib/badline/audio/aiff.rb +8 -3
  14. data/lib/badline/audio/bare_player.rb +20 -3
  15. data/lib/badline/audio/cli.rb +17 -12
  16. data/lib/badline/audio/console.rb +12 -58
  17. data/lib/badline/audio/machine_player.rb +9 -6
  18. data/lib/badline/audio/playback.rb +3 -0
  19. data/lib/badline/audio/renderer.rb +3 -3
  20. data/lib/badline/audio/sdl_sink.rb +20 -52
  21. data/lib/badline/audio/stream.rb +117 -0
  22. data/lib/badline/audio/terminal.rb +90 -0
  23. data/lib/badline/audio.rb +3 -2
  24. data/lib/badline/cartridge/easy_flash.rb +6 -2
  25. data/lib/badline/cartridge/epyx_fastload.rb +1 -1
  26. data/lib/badline/cartridge/geo_ram.rb +63 -0
  27. data/lib/badline/cartridge/mach5.rb +1 -1
  28. data/lib/badline/cartridge/magic_desk.rb +5 -1
  29. data/lib/badline/cartridge/ocean.rb +5 -2
  30. data/lib/badline/cartridge/rex_utility.rb +1 -1
  31. data/lib/badline/cartridge/rgcd.rb +1 -1
  32. data/lib/badline/cartridge/simons_basic.rb +2 -2
  33. data/lib/badline/cartridge/westermann.rb +2 -2
  34. data/lib/badline/cartridge.rb +9 -0
  35. data/lib/badline/checkpoint.rb +98 -0
  36. data/lib/badline/cia/interrupt_register.rb +34 -10
  37. data/lib/badline/cia.rb +19 -10
  38. data/lib/badline/color_memory.rb +2 -2
  39. data/lib/badline/computer.rb +34 -14
  40. data/lib/badline/cpu.rb +2 -2
  41. data/lib/badline/gui/application.rb +70 -26
  42. data/lib/badline/gui/gamepads.rb +22 -22
  43. data/lib/badline/gui/joy_map.rb +1 -1
  44. data/lib/badline/gui/key_map.rb +6 -1
  45. data/lib/badline/gui/pane.rb +7 -6
  46. data/lib/badline/gui/screen_pane.rb +9 -13
  47. data/lib/badline/gui/window.rb +35 -20
  48. data/lib/badline/gui.rb +1 -1
  49. data/lib/badline/input/mouse1351.rb +21 -6
  50. data/lib/badline/input/paddles.rb +11 -6
  51. data/lib/badline/kernal_trap/channel.rb +102 -7
  52. data/lib/badline/kernal_trap/drive/block_commands.rb +108 -0
  53. data/lib/badline/kernal_trap/drive/channels.rb +53 -0
  54. data/lib/badline/kernal_trap/drive/memory.rb +109 -0
  55. data/lib/badline/kernal_trap/drive/parameters.rb +28 -0
  56. data/lib/badline/kernal_trap/drive/status.rb +3 -0
  57. data/lib/badline/kernal_trap/drive/write_file.rb +41 -0
  58. data/lib/badline/kernal_trap/drive/writes.rb +92 -0
  59. data/lib/badline/kernal_trap/drive.rb +109 -131
  60. data/lib/badline/kernal_trap/file.rb +6 -14
  61. data/lib/badline/kernal_trap/load.rb +6 -0
  62. data/lib/badline/kernal_trap/save.rb +13 -7
  63. data/lib/badline/keyboard.rb +19 -3
  64. data/lib/badline/media.rb +44 -13
  65. data/lib/badline/memory.rb +2 -2
  66. data/lib/badline/options.rb +209 -0
  67. data/lib/badline/ram_expansion/banking.rb +30 -0
  68. data/lib/badline/ram_expansion/plus256k.rb +38 -0
  69. data/lib/badline/ram_expansion/plus60k.rb +31 -0
  70. data/lib/badline/ram_expansion/unexpanded.rb +22 -0
  71. data/lib/badline/ram_expansion.rb +21 -0
  72. data/lib/badline/region.rb +51 -0
  73. data/lib/badline/rom.rb +14 -5
  74. data/lib/badline/sdl/events.rb +47 -0
  75. data/lib/badline/sdl/functions.rb +73 -0
  76. data/lib/badline/sdl.rb +67 -0
  77. data/lib/badline/sid/catch_up.rb +110 -0
  78. data/lib/badline/sid/decimator.rb +6 -1
  79. data/lib/badline/sid/filter.rb +5 -5
  80. data/lib/badline/sid/waveform/fast_forward.rb +10 -10
  81. data/lib/badline/sid/waveform/noise_writeback.rb +32 -14
  82. data/lib/badline/sid/waveform.rb +8 -8
  83. data/lib/badline/sid.rb +19 -93
  84. data/lib/badline/storage/d64_image.rb +10 -0
  85. data/lib/badline/storage/d71_image.rb +12 -0
  86. data/lib/badline/storage/d81_image.rb +9 -0
  87. data/lib/badline/storage/disk_image/bam.rb +111 -0
  88. data/lib/badline/storage/disk_image/directory.rb +79 -0
  89. data/lib/badline/storage/disk_image/writing.rb +110 -0
  90. data/lib/badline/storage/disk_image.rb +49 -4
  91. data/lib/badline/storage/host_directory.rb +17 -4
  92. data/lib/badline/storage/sid_file.rb +36 -2
  93. data/lib/badline/storage.rb +18 -1
  94. data/lib/badline/time_of_day.rb +11 -2
  95. data/lib/badline/version.rb +1 -1
  96. data/lib/badline/via/control_lines.rb +104 -0
  97. data/lib/badline/via/interrupt_register.rb +58 -0
  98. data/lib/badline/via/shift_register.rb +135 -0
  99. data/lib/badline/via/timer1.rb +82 -0
  100. data/lib/badline/via/timer2.rb +84 -0
  101. data/lib/badline/via.rb +262 -0
  102. data/lib/badline/vic/bank.rb +4 -3
  103. data/lib/badline/vic/color_patches.rb +6 -3
  104. data/lib/badline/vic/display_state.rb +4 -0
  105. data/lib/badline/vic/graphics_shifter.rb +49 -8
  106. data/lib/badline/vic/registers.rb +19 -27
  107. data/lib/badline/vic/sequencer.rb +18 -18
  108. data/lib/badline/vic/sequencer_output.rb +4 -4
  109. data/lib/badline/vic/sprite/internal_bus.rb +4 -0
  110. data/lib/badline/vic/sprite/shifter.rb +25 -3
  111. data/lib/badline/vic/sprite.rb +5 -4
  112. data/lib/badline/vic/sprites.rb +8 -3
  113. data/lib/badline/vic.rb +98 -46
  114. data/lib/badline.rb +5 -0
  115. data/packaging/homebrew/badline.rb +34 -0
  116. metadata +37 -20
  117. data/exe/badline +0 -75
  118. data/exe/badline-sid +0 -34
  119. 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
- def cycles_to_close = (@clock_hz - @phase + @rate - 1) / @rate
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
@@ -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.anybits?(0x4)
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.anybits?(bit)
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.anybits?(0x1)
207
- value += @bandpass if @mode.anybits?(0x2)
208
- value += @highpass if @mode.anybits?(0x4)
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.positive? && cycles.positive?
17
+ while @shift_pipeline != 0 && cycles != 0
18
18
  advance
19
19
  cycles -= 1
20
20
  end
21
- skip_ahead(cycles) if cycles.positive?
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.positive?
62
+ if rises != 0
63
63
  @shift_pipeline = pipeline_after(cycles)
64
- rises -= 1 if @shift_pipeline.positive?
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).positive?
78
- return 1 if cycles > 1 && bit19_rises(last - @frequency, last).positive?
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 unless @floating_ttl.positive?
84
+ return if @floating_ttl.zero?
85
85
 
86
86
  @floating_ttl -= cycles
87
- return if @floating_ttl.positive?
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 unless @shift_register_reset.positive?
94
+ return if @shift_register_reset.zero?
95
95
 
96
96
  @shift_register_reset -= cycles
97
- return if @shift_register_reset.positive?
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. On the 6581, a change that keeps only
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
- if @topbit_feedback && previous != 0xc && (previous & @selected) == 0xc
58
- write_shift_register(noise & PULSE_NOISE_RELEASE)
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 selected after. Dropping
68
- # to noise alone writes nothing back unless all four were selected,
69
- # nor does changing to pulse+noise or from pulse+noise to
70
- # sawtooth+noise, nor, on the 6581, trading triangle for sawtooth or
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 < 0x8
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])
@@ -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.anybits?(selected - 1)
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.nobits?(MSB) && @accumulator.anybits?(MSB)
263
- if previous.nobits?(0x080000) && @accumulator.anybits?(0x080000)
262
+ @msb_rising = @accumulator & ~previous & MSB != 0
263
+ if @accumulator & ~previous & 0x080000 != 0
264
264
  @shift_pipeline = 2
265
- elsif @shift_pipeline.positive? && (@shift_pipeline -= 1).zero?
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.anybits?(0x2)
280
+ return false if selected & 0x2 != 0
281
281
 
282
- msb = @accumulator.anybits?(MSB)
282
+ msb = @accumulator & MSB != 0
283
283
  return msb unless @ring
284
284
 
285
- msb != @sync_source.accumulator.nobits?(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
- def record(rate:, filter_chunk: @filter_chunk)
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: TimeOfDay::CLOCK_HZ, rate:)
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 index(addr) % (2**5)
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 = index(addr) % (2**5)
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 << [@pending_cycles, reg, value]
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 then voice.control = value
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