badline 0.1.0 → 0.2.1

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 (140) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +185 -0
  3. data/CLAUDE.md +217 -0
  4. data/CODE_OF_CONDUCT.md +92 -0
  5. data/CONTRIBUTING.md +51 -0
  6. data/README.md +224 -31
  7. data/doc/pinned-behaviour.md +1117 -0
  8. data/exe/badline +22 -2
  9. data/exe/badline-sid +34 -0
  10. data/lib/badline/address_bus.rb +96 -25
  11. data/lib/badline/audio/aiff.rb +36 -0
  12. data/lib/badline/audio/bare_player.rb +129 -0
  13. data/lib/badline/audio/cli.rb +133 -0
  14. data/lib/badline/audio/console.rb +75 -0
  15. data/lib/badline/audio/jukebox.rb +82 -0
  16. data/lib/badline/audio/machine_player.rb +57 -0
  17. data/lib/badline/audio/options.rb +137 -0
  18. data/lib/badline/audio/pcm_writer.rb +57 -0
  19. data/lib/badline/audio/playback.rb +108 -0
  20. data/lib/badline/audio/renderer.rb +85 -0
  21. data/lib/badline/audio/sdl_sink.rb +102 -0
  22. data/lib/badline/audio/wav.rb +23 -0
  23. data/lib/badline/audio.rb +14 -0
  24. data/lib/badline/cartridge/action_replay.rb +111 -0
  25. data/lib/badline/cartridge/atomic_power.rb +24 -0
  26. data/lib/badline/cartridge/bank.rb +81 -0
  27. data/lib/badline/cartridge/comal80.rb +48 -0
  28. data/lib/badline/cartridge/dinamic.rb +34 -0
  29. data/lib/badline/cartridge/easy_flash.rb +122 -0
  30. data/lib/badline/cartridge/epyx_fastload.rb +71 -0
  31. data/lib/badline/cartridge/final_cartridge3.rb +77 -0
  32. data/lib/badline/cartridge/flash.rb +265 -0
  33. data/lib/badline/cartridge/freezer.rb +31 -0
  34. data/lib/badline/cartridge/fun_play.rb +38 -0
  35. data/lib/badline/cartridge/g_mod2.rb +62 -0
  36. data/lib/badline/cartridge/game_system.rb +40 -0
  37. data/lib/badline/cartridge/kcs_power.rb +72 -0
  38. data/lib/badline/cartridge/mach5.rb +34 -0
  39. data/lib/badline/cartridge/magic_desk.rb +6 -3
  40. data/lib/badline/cartridge/ocean.rb +11 -12
  41. data/lib/badline/cartridge/pagefox.rb +56 -0
  42. data/lib/badline/cartridge/retro_replay.rb +276 -0
  43. data/lib/badline/cartridge/rex_utility.rb +33 -0
  44. data/lib/badline/cartridge/rgcd.rb +48 -0
  45. data/lib/badline/cartridge/simons_basic.rb +40 -0
  46. data/lib/badline/cartridge/standard.rb +3 -19
  47. data/lib/badline/cartridge/super_games.rb +36 -0
  48. data/lib/badline/cartridge/westermann.rb +34 -0
  49. data/lib/badline/cartridge/zaxxon.rb +50 -0
  50. data/lib/badline/cartridge.rb +132 -16
  51. data/lib/badline/cia/interrupt_register.rb +102 -0
  52. data/lib/badline/cia/serial.rb +219 -0
  53. data/lib/badline/cia/timer.rb +43 -31
  54. data/lib/badline/cia.rb +138 -88
  55. data/lib/badline/color_memory.rb +14 -5
  56. data/lib/badline/computer.rb +97 -12
  57. data/lib/badline/control_ports.rb +54 -9
  58. data/lib/badline/cpu/addressing.rb +131 -0
  59. data/lib/badline/cpu/microcode.rb +169 -0
  60. data/lib/badline/cpu/operations.rb +88 -0
  61. data/lib/badline/cpu/stack_operations.rb +82 -0
  62. data/lib/badline/cpu.rb +109 -168
  63. data/lib/badline/cycleable.rb +3 -0
  64. data/lib/badline/datasette.rb +81 -0
  65. data/lib/badline/debug_register.rb +28 -0
  66. data/lib/badline/gui/application.rb +79 -18
  67. data/lib/badline/gui/gamepads.rb +97 -0
  68. data/lib/badline/gui/joy_map.rb +15 -7
  69. data/lib/badline/gui.rb +1 -0
  70. data/lib/badline/input/mouse1351.rb +41 -0
  71. data/lib/badline/input/paddles.rb +36 -0
  72. data/lib/badline/input.rb +4 -0
  73. data/lib/badline/instruction_set/arithmetic.rb +16 -21
  74. data/lib/badline/instruction_set/bitwise.rb +17 -22
  75. data/lib/badline/instruction_set/branch.rb +20 -19
  76. data/lib/badline/instruction_set/flag.rb +7 -7
  77. data/lib/badline/instruction_set/illegal.rb +14 -22
  78. data/lib/badline/instruction_set/inc_dec.rb +8 -10
  79. data/lib/badline/instruction_set/stack.rb +5 -57
  80. data/lib/badline/instruction_set/transfer.rb +9 -9
  81. data/lib/badline/instruction_set.rb +9 -31
  82. data/lib/badline/interrupts.rb +104 -0
  83. data/lib/badline/joystick.rb +9 -9
  84. data/lib/badline/kernal_trap/channel.rb +51 -0
  85. data/lib/badline/kernal_trap/drive/status.rb +49 -0
  86. data/lib/badline/kernal_trap/drive.rb +277 -0
  87. data/lib/badline/kernal_trap/file.rb +5 -25
  88. data/lib/badline/kernal_trap/load.rb +86 -24
  89. data/lib/badline/kernal_trap/routine.rb +51 -0
  90. data/lib/badline/kernal_trap/save.rb +46 -14
  91. data/lib/badline/kernal_trap/serial.rb +171 -0
  92. data/lib/badline/kernal_trap.rb +4 -0
  93. data/lib/badline/keyboard.rb +40 -16
  94. data/lib/badline/media.rb +48 -10
  95. data/lib/badline/memory.rb +5 -0
  96. data/lib/badline/roms/eapi/LICENSE.md +15 -0
  97. data/lib/badline/roms/eapi/README +41 -0
  98. data/lib/badline/roms/eapi/eapi-am29f040-14 +0 -0
  99. data/lib/badline/sid/decimator.rb +46 -0
  100. data/lib/badline/sid/envelope.rb +232 -0
  101. data/lib/badline/sid/filter.rb +213 -0
  102. data/lib/badline/sid/voice.rb +61 -0
  103. data/lib/badline/sid/waveform/combined.rb +126 -0
  104. data/lib/badline/sid/waveform/fast_forward.rb +105 -0
  105. data/lib/badline/sid/waveform/noise_writeback.rb +82 -0
  106. data/lib/badline/sid/waveform.rb +289 -0
  107. data/lib/badline/sid.rb +274 -7
  108. data/lib/badline/status.rb +251 -23
  109. data/lib/badline/storage/crt_file.rb +18 -2
  110. data/lib/badline/storage/d64_image.rb +6 -0
  111. data/lib/badline/storage/d71_image.rb +4 -0
  112. data/lib/badline/storage/d81_image.rb +4 -0
  113. data/lib/badline/storage/disk_image.rb +99 -13
  114. data/lib/badline/storage/host_directory.rb +30 -4
  115. data/lib/badline/storage/sid_file.rb +274 -0
  116. data/lib/badline/storage/song_lengths.rb +65 -0
  117. data/lib/badline/storage/t64.rb +67 -0
  118. data/lib/badline/storage/tap.rb +68 -0
  119. data/lib/badline/storage.rb +19 -0
  120. data/lib/badline/time_of_day.rb +105 -52
  121. data/lib/badline/version.rb +1 -1
  122. data/lib/badline/vic/bank.rb +14 -6
  123. data/lib/badline/vic/border_mask.rb +76 -0
  124. data/lib/badline/vic/collisions.rb +104 -0
  125. data/lib/badline/vic/color_patches.rb +33 -0
  126. data/lib/badline/vic/display_state.rb +92 -29
  127. data/lib/badline/vic/graphics_mode.rb +78 -53
  128. data/lib/badline/vic/graphics_shifter.rb +140 -0
  129. data/lib/badline/vic/register_log.rb +63 -0
  130. data/lib/badline/vic/registers.rb +3 -0
  131. data/lib/badline/vic/sequencer.rb +163 -137
  132. data/lib/badline/vic/sequencer_output.rb +116 -0
  133. data/lib/badline/vic/sprite/internal_bus.rb +49 -0
  134. data/lib/badline/vic/sprite/shifter.rb +129 -0
  135. data/lib/badline/vic/sprite.rb +275 -65
  136. data/lib/badline/vic/sprites.rb +220 -58
  137. data/lib/badline/vic.rb +389 -55
  138. data/lib/badline.rb +5 -0
  139. data/vendor/.gitkeep +0 -0
  140. metadata +104 -4
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module KernalTrap
5
+ # PC traps on the KERNAL's serial bus primitives. Frames addressed to
6
+ # device 8 go to a virtual drive instead of out over the IEC lines, so
7
+ # OPEN/CHKIN/CHRIN reach a CBM DOS, and so do the block reads that
8
+ # loaders make through them. Other devices fall through to the ROM.
9
+ class Serial < Routine
10
+ ROUTINES = {
11
+ 0xed09 => :talk,
12
+ 0xed0c => :listen,
13
+ 0xedb9 => :second,
14
+ 0xedc7 => :tksa,
15
+ 0xeddd => :ciout,
16
+ 0xedef => :untalk,
17
+ 0xedfe => :unlisten,
18
+ 0xee13 => :acptr
19
+ }.freeze
20
+
21
+ # Frame type in the high nibble of the secondary address
22
+ OPEN = 0xf0
23
+ CLOSE = 0xe0
24
+
25
+ # ST bits at $90
26
+ DEVICE_NOT_PRESENT = 0x80
27
+ EOI = 0x40
28
+ READ_TIMEOUT = 0x02
29
+
30
+ NO_DATA = [0x0d, EOI | READ_TIMEOUT].freeze
31
+
32
+ def initialize(cpu:, bus:, drive:)
33
+ super(cpu:, bus:)
34
+ @drive = drive
35
+ @listening = false
36
+ @talking = false
37
+ @listen_channel = nil
38
+ @talk_channel = nil
39
+ @frame = OPEN
40
+ @buffer = []
41
+ end
42
+
43
+ def install
44
+ ROUTINES.each do |address, routine|
45
+ @cpu.install_trap(address) { call_routine(routine) if kernal? }
46
+ end
47
+ self
48
+ end
49
+
50
+ private
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
+
65
+ # Addressing the bus starts a new frame. The channel number arrives
66
+ # in the secondary address that follows.
67
+ def talk
68
+ @talk_channel = nil
69
+ @talking = @cpu.a == DEVICE
70
+ return_to_caller if @talking
71
+ end
72
+
73
+ def listen
74
+ @listen_channel = nil
75
+ @listening = @cpu.a == DEVICE
76
+ return_to_caller if @listening
77
+ end
78
+
79
+ def second
80
+ return unless @listening
81
+
82
+ @frame = @cpu.a & 0xf0
83
+ @listen_channel = @cpu.a & 0x0f
84
+ @buffer = []
85
+ return_to_caller
86
+ end
87
+
88
+ def tksa
89
+ return unless @talking
90
+
91
+ @talk_channel = @cpu.a & 0x0f
92
+ return_to_caller
93
+ end
94
+
95
+ def ciout
96
+ return unless @listening
97
+
98
+ @buffer << @cpu.a
99
+ update_status(DEVICE_NOT_PRESENT) unless accepted?
100
+ @cpu.status.carry = false
101
+ return_to_caller
102
+ end
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
+
111
+ def unlisten
112
+ return unless @listening
113
+
114
+ @listening = false
115
+ deliver_frame
116
+ release_bus
117
+ end
118
+
119
+ def untalk
120
+ return unless @talking
121
+
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?
133
+ return_to_caller
134
+ end
135
+
136
+ def acptr
137
+ return unless @talking
138
+
139
+ byte, status = received
140
+ @cpu.a = byte
141
+ update_status(status)
142
+ @cpu.status.carry = false
143
+ return_to_caller
144
+ end
145
+
146
+ def deliver_frame
147
+ return unless @listen_channel
148
+
149
+ case @frame
150
+ when OPEN then @drive.open(@listen_channel, @buffer.pack("C*"))
151
+ when CLOSE then @drive.close(@listen_channel)
152
+ else @drive.write(@listen_channel, @buffer)
153
+ end
154
+ @buffer = []
155
+ end
156
+
157
+ def received
158
+ byte, eoi = @drive.read(@talk_channel)
159
+ return NO_DATA unless byte
160
+
161
+ [byte, eoi ? EOI : 0]
162
+ end
163
+
164
+ def update_status(bits)
165
+ return if bits.zero?
166
+
167
+ @bus.poke(0x90, @bus.peek(0x90) | bits)
168
+ end
169
+ end
170
+ end
171
+ end
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "badline/kernal_trap/routine"
4
+ require "badline/kernal_trap/channel"
5
+ require "badline/kernal_trap/drive"
3
6
  require "badline/kernal_trap/file"
4
7
  require "badline/kernal_trap/load"
5
8
  require "badline/kernal_trap/save"
9
+ require "badline/kernal_trap/serial"
@@ -1,6 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Badline
4
+ # The 8x8 key matrix spanning CIA 1's two ports: rows on port A, columns on
5
+ # port B.
6
+ #
7
+ # A pressed key shorts its row line to its column line, so whichever side is
8
+ # pulled low drags the other down with it. Scanning works in either
9
+ # direction, and chains of keys sharing a line propagate the pulldown -- the
10
+ # ghost keys real hardware reports.
4
11
  class Keyboard
5
12
  include IntegerHelper
6
13
 
@@ -8,9 +15,10 @@ module Badline
8
15
 
9
16
  def initialize
10
17
  @keys = []
18
+ @row_masks = nil
11
19
 
12
20
  @matrix = [
13
- %i[delete return cursor_h f7 f1 f3 cursor_v],
21
+ %i[delete return cursor_h f7 f1 f3 f5 cursor_v],
14
22
  %i[3 w a 4 z s e lshift],
15
23
  %i[5 r d 6 c f t x],
16
24
  %i[7 y g 8 b h u v],
@@ -22,35 +30,51 @@ module Badline
22
30
  end
23
31
 
24
32
  def press(key)
25
- @keys << key if valid_key?(key)
33
+ return unless valid_key?(key)
34
+
35
+ @keys << key
36
+ @row_masks = nil
26
37
  end
27
38
 
28
- def read_a(_port_a, _port_b)
29
- 0xff
39
+ def release(key)
40
+ @keys.reject! { |k| k == key }
41
+ @row_masks = nil
30
42
  end
31
43
 
32
- def read_b(port_a, _port_b)
33
- return 0xff unless keys.any?
44
+ # Settles the row (port A) and column (port B) lines against each other
45
+ # until no key contact pulls a line low any more, and returns both.
46
+ def scan(rows, cols)
47
+ return [rows, cols] if keys.empty?
34
48
 
35
- output = 0xff
49
+ loop do
50
+ settled_rows = rows
51
+ settled_cols = cols
36
52
 
37
- matrix.each_with_index do |row, a|
38
- next unless port_a[a].zero?
53
+ row_masks.each_with_index do |mask, row|
54
+ bit = 1 << row
55
+ next if mask.zero? || (rows.allbits?(bit) && cols.allbits?(mask))
39
56
 
40
- row.each_with_index do |key, b|
41
- output -= (1 << b) if keys.include?(key)
57
+ rows &= ~bit
58
+ cols &= ~mask
42
59
  end
43
- end
44
60
 
45
- output
61
+ return [rows, cols] if rows == settled_rows && cols == settled_cols
62
+ end
46
63
  end
47
64
 
48
- def release(key)
49
- @keys.reject! { |k| k == key }
50
- end
65
+ def read_a(port_a, port_b) = scan(port_a, port_b).first
66
+
67
+ def read_b(port_a, port_b) = scan(port_a, port_b).last
51
68
 
52
69
  private
53
70
 
71
+ # Columns held down per row, as a bit mask of port B lines.
72
+ def row_masks
73
+ @row_masks ||= matrix.map do |row|
74
+ row.each_with_index.sum { |key, column| keys.include?(key) ? 1 << column : 0 }
75
+ end
76
+ end
77
+
54
78
  def valid_key?(key)
55
79
  matrix.flatten.include?(key)
56
80
  end
data/lib/badline/media.rb CHANGED
@@ -3,37 +3,75 @@
3
3
  module Badline
4
4
  module Media
5
5
  AUTOSTART = %(lO"*",8,1\rrun\r)
6
+ TAPE_AUTOSTART = %(lO\rrun\r)
6
7
  BASIC_START = 0x0801
7
8
 
8
- DISK_TYPES = {
9
+ MOUNT_TYPES = {
9
10
  ".d64" => Storage::D64Image,
10
11
  ".d71" => Storage::D71Image,
11
- ".d81" => Storage::D81Image
12
+ ".d81" => Storage::D81Image,
13
+ ".t64" => Storage::T64
12
14
  }.freeze
13
15
 
14
16
  class << self
15
- def attach(computer, path, autostart: true)
17
+ # `cartridge` sets the jumpers of a .crt cartridge that has them, such
18
+ # as `{ flash_jumper: true }` for the Retro Replay's flash mode.
19
+ def attach(computer, path, autostart: true, song: nil, cartridge: {})
16
20
  if File.directory?(path)
17
21
  computer.mount(Storage::HostDirectory.new(path))
18
22
  "Mounted #{path} as device 8"
19
23
  elsif File.extname(path).downcase == ".crt"
20
- attach_cartridge(computer, path)
21
- elsif (image = DISK_TYPES[File.extname(path).downcase])
22
- attach_disk(computer, image.new(path), path, autostart:)
24
+ attach_cartridge(computer, path, cartridge)
25
+ elsif File.extname(path).downcase == ".sid"
26
+ attach_sid(computer, path, autostart:, song:)
27
+ elsif File.extname(path).downcase == ".tap"
28
+ attach_tape(computer, path, autostart:)
29
+ elsif (storage = MOUNT_TYPES[File.extname(path).downcase])
30
+ attach_storage(computer, storage.new(path), path, autostart:)
23
31
  else
24
32
  attach_prg(computer, path, autostart:)
25
33
  end
26
34
  end
27
35
 
36
+ # The SID a machine for `path` should be built with. A .sid tune names
37
+ # its own; everything else gets the 6581.
38
+ def sid_model(path)
39
+ return :mos6581 unless path && File.extname(path).downcase == ".sid"
40
+
41
+ Storage::SIDFile.new(path).sid_model
42
+ end
43
+
28
44
  private
29
45
 
30
- def attach_cartridge(computer, path)
31
- computer.attach_cartridge(Cartridge.from_file(path))
46
+ def attach_cartridge(computer, path, options)
47
+ computer.attach_cartridge(Cartridge.from_file(path, **options))
32
48
  "Attached cartridge #{path}"
33
49
  end
34
50
 
35
- def attach_disk(computer, image, path, autostart:)
36
- computer.mount(image)
51
+ def attach_sid(computer, path, autostart:, song:)
52
+ tune = Storage::SIDFile.new(path)
53
+ song = (song || tune.start_song).clamp(1, tune.songs)
54
+ computer.on_init { start_tune(computer, tune, autostart:, song:) }
55
+ title = tune.name.empty? ? path : tune.name
56
+ title += " (song #{song})" if tune.songs > 1
57
+ autostart ? "Playing #{title}" : "Loaded #{title}"
58
+ end
59
+
60
+ def start_tune(computer, tune, autostart:, song:)
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
64
+ end
65
+
66
+ def attach_tape(computer, path, autostart:)
67
+ computer.datasette.insert(Storage::TAP.new(path))
68
+ computer.datasette.play!
69
+ computer.type_text(TAPE_AUTOSTART) if autostart
70
+ "Inserted #{path} in the datasette"
71
+ end
72
+
73
+ def attach_storage(computer, storage, path, autostart:)
74
+ computer.mount(storage)
37
75
  computer.type_text(AUTOSTART) if autostart
38
76
  "Mounted #{path} as device 8"
39
77
  end
@@ -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
@@ -0,0 +1,15 @@
1
+ This software is provided 'as-is', without any express or implied
2
+ warranty. In no event will the authors be held liable for any damages
3
+ arising from the use of this software.
4
+
5
+ Permission is granted to anyone to use this software for any purpose,
6
+ including commercial applications, and to alter it and redistribute it
7
+ freely, subject to the following restrictions:
8
+
9
+ 1. The origin of this software must not be misrepresented; you must not
10
+ claim that you wrote the original software. If you use this software
11
+ in a product, an acknowledgment in the product documentation would be
12
+ appreciated but is not required.
13
+ 2. Altered source versions must be plainly marked as such, and must not be
14
+ misrepresented as being the original software.
15
+ 3. This notice may not be removed or altered from any source distribution.
@@ -0,0 +1,41 @@
1
+ EasyFlash EAPI for the Am29F040
2
+ ===============================
3
+
4
+ eapi-am29f040-14 is the EasyFlash flash driver (EAPI) for the AMD Am29F040,
5
+ version 1.4, by Thomas 'skoe' Giesel. It is third-party software, not part
6
+ of badline, and is distributed under the zlib licence in LICENSE.md.
7
+
8
+ It is the unaltered output of assembling the upstream source, a C64 PRG
9
+ (a two-byte load address, $C000, then 768 bytes):
10
+
11
+ Source: https://gitlab.com/easyflash/eapi
12
+ Revision: 1b4ea15d44974d18f6f82156b935f59d2f85596c
13
+ File: eapi-am29f040.s (includes eapi_defs.s)
14
+ Built: acme -l out/eapi-am29f040-14.labels -o out/eapi-am29f040-14 eapi-am29f040.s
15
+ (the repository's Makefile rule, with ACME 0.96.4)
16
+ SHA-256: 4c32d425d7b0a1f659e007bccad5c43603a93236974ef19743b2eefb812ab9ed
17
+
18
+ The source file's notice:
19
+
20
+ (c) 2009-2010 Thomas 'skoe' Giesel
21
+
22
+ This software is provided 'as-is', without any express or implied
23
+ warranty. In no event will the authors be held liable for any damages
24
+ arising from the use of this software.
25
+
26
+ Permission is granted to anyone to use this software for any purpose,
27
+ including commercial applications, and to alter it and redistribute it
28
+ freely, subject to the following restrictions:
29
+
30
+ 1. The origin of this software must not be misrepresented; you must not
31
+ claim that you wrote the original software. If you use this software
32
+ in a product, an acknowledgment in the product documentation would be
33
+ appreciated but is not required.
34
+ 2. Altered source versions must be plainly marked as such, and must not be
35
+ misrepresented as being the original software.
36
+ 3. This notice may not be removed or altered from any source distribution.
37
+
38
+ When an EasyFlash image carries an EAPI, as its "eapi" signature at $B800
39
+ in bank 0 of ROMH shows, badline replaces those 768 bytes with this driver
40
+ on attach, the way VICE does, so the image drives the flash chip badline
41
+ emulates.
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ class SID
5
+ # Reduces the SID's one-sample-per-cycle stream to an audio rate by
6
+ # averaging each window. Point sampling would be cheaper, but the pulse
7
+ # and noise waveforms carry enough energy above the output Nyquist to
8
+ # alias audibly.
9
+ #
10
+ # A sample can stand for several cycles at once, when the DSP has run
11
+ # them as one step.
12
+ class Decimator
13
+ def initialize(clock_hz:, rate:)
14
+ @clock_hz = clock_hz
15
+ @rate = rate
16
+ @phase = 0
17
+ @sum = 0
18
+ @count = 0
19
+ end
20
+
21
+ # Cycles until the current window closes, counting the next one as 1.
22
+ def cycles_to_close = (@clock_hz - @phase + @rate - 1) / @rate
23
+
24
+ # Returns the averaged sample when these cycles close a window, and
25
+ # nil otherwise. They must not run past the window's end.
26
+ def push(sample, cycles = 1)
27
+ @sum += sample * cycles
28
+ @count += cycles
29
+ @phase += @rate * cycles
30
+ return unless @phase >= @clock_hz
31
+
32
+ @phase -= @clock_hz
33
+ average
34
+ end
35
+
36
+ private
37
+
38
+ def average
39
+ value = @sum.fdiv(@count).round
40
+ @sum = 0
41
+ @count = 0
42
+ value
43
+ end
44
+ end
45
+ end
46
+ end