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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +67 -0
  3. data/CLAUDE.md +6 -0
  4. data/README.md +24 -6
  5. data/doc/pinned-behaviour.md +18 -6
  6. data/exe/badline +5 -2
  7. data/lib/badline/address_bus.rb +14 -6
  8. data/lib/badline/addressable.rb +1 -1
  9. data/lib/badline/audio/bare_player.rb +20 -3
  10. data/lib/badline/audio/machine_player.rb +9 -6
  11. data/lib/badline/audio/renderer.rb +3 -3
  12. data/lib/badline/audio/stream.rb +117 -0
  13. data/lib/badline/audio.rb +1 -0
  14. data/lib/badline/cartridge/easy_flash.rb +6 -2
  15. data/lib/badline/cartridge/epyx_fastload.rb +1 -1
  16. data/lib/badline/cartridge/mach5.rb +1 -1
  17. data/lib/badline/cartridge/magic_desk.rb +5 -1
  18. data/lib/badline/cartridge/ocean.rb +5 -2
  19. data/lib/badline/cartridge/rex_utility.rb +1 -1
  20. data/lib/badline/cartridge/rgcd.rb +1 -1
  21. data/lib/badline/cartridge/simons_basic.rb +2 -2
  22. data/lib/badline/cartridge/westermann.rb +2 -2
  23. data/lib/badline/cartridge.rb +16 -8
  24. data/lib/badline/checkpoint.rb +98 -0
  25. data/lib/badline/cia/interrupt_register.rb +20 -6
  26. data/lib/badline/cia.rb +36 -19
  27. data/lib/badline/color_memory.rb +2 -2
  28. data/lib/badline/computer.rb +41 -11
  29. data/lib/badline/cpu/microcode.rb +41 -43
  30. data/lib/badline/cpu.rb +3 -3
  31. data/lib/badline/gui/application.rb +41 -9
  32. data/lib/badline/kernal_trap/channel.rb +118 -8
  33. data/lib/badline/kernal_trap/drive/block_commands.rb +79 -0
  34. data/lib/badline/kernal_trap/drive/channels.rb +53 -0
  35. data/lib/badline/kernal_trap/drive/memory.rb +109 -0
  36. data/lib/badline/kernal_trap/drive/parameters.rb +28 -0
  37. data/lib/badline/kernal_trap/drive/status.rb +49 -0
  38. data/lib/badline/kernal_trap/drive.rb +148 -132
  39. data/lib/badline/kernal_trap/file.rb +0 -5
  40. data/lib/badline/kernal_trap/load.rb +93 -25
  41. data/lib/badline/kernal_trap/routine.rb +17 -0
  42. data/lib/badline/kernal_trap/save.rb +48 -15
  43. data/lib/badline/kernal_trap/serial.rb +34 -2
  44. data/lib/badline/media.rb +2 -2
  45. data/lib/badline/memory.rb +7 -2
  46. data/lib/badline/rom.rb +14 -5
  47. data/lib/badline/sid/catch_up.rb +110 -0
  48. data/lib/badline/sid/decimator.rb +6 -1
  49. data/lib/badline/sid/filter.rb +5 -5
  50. data/lib/badline/sid/waveform/fast_forward.rb +10 -10
  51. data/lib/badline/sid/waveform.rb +7 -7
  52. data/lib/badline/sid.rb +16 -92
  53. data/lib/badline/status.rb +255 -32
  54. data/lib/badline/storage/d64_image.rb +6 -0
  55. data/lib/badline/storage/d71_image.rb +4 -0
  56. data/lib/badline/storage/d81_image.rb +4 -0
  57. data/lib/badline/storage/disk_image.rb +78 -17
  58. data/lib/badline/storage/host_directory.rb +26 -2
  59. data/lib/badline/storage/sid_file.rb +37 -3
  60. data/lib/badline/storage.rb +3 -1
  61. data/lib/badline/time_of_day.rb +7 -0
  62. data/lib/badline/version.rb +1 -1
  63. data/lib/badline/vic/graphics_shifter.rb +8 -2
  64. data/lib/badline/vic/registers.rb +16 -27
  65. data/lib/badline/vic.rb +23 -10
  66. data/lib/badline.rb +1 -0
  67. 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) { send(routine) if kernal? }
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
- return_to_caller
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(tune.driver_address, tune.driver(song:))
63
- computer.type_text("sys#{tune.driver_address}\r") if autostart
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:)
@@ -10,11 +10,11 @@ module Badline
10
10
  end
11
11
 
12
12
  def peek(addr)
13
- @storage[index(addr)]
13
+ @storage[offset_of(addr)]
14
14
  end
15
15
 
16
16
  def poke(addr, value)
17
- @storage[index(addr)] = value
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 = File.read(file_path(filename)).bytes
18
+ data = read(filename)
8
19
  new(data, length: data.length, start: start)
9
20
  end
10
21
 
11
- private
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
- 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
@@ -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,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
- def record(rate:, filter_chunk: @filter_chunk)
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: TimeOfDay::CLOCK_HZ, rate:)
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 index(addr) % (2**5)
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 = index(addr) % (2**5)
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 << [@pending_cycles, reg, value]
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 then voice.control = value
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