badline 0.6.0 → 0.7.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 (108) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +77 -0
  3. data/CLAUDE.md +10 -9
  4. data/README.md +135 -42
  5. data/doc/pinned-behaviour.md +131 -30
  6. data/exe/badline-ruby +28 -24
  7. data/lib/badline/address_bus/extra_sids.rb +30 -0
  8. data/lib/badline/address_bus/sid_slots.rb +42 -0
  9. data/lib/badline/address_bus.rb +15 -1
  10. data/lib/badline/audio/aiff.rb +2 -2
  11. data/lib/badline/audio/bare_player.rb +23 -12
  12. data/lib/badline/audio/cli.rb +167 -59
  13. data/lib/badline/audio/console.rb +2 -1
  14. data/lib/badline/audio/jukebox.rb +121 -36
  15. data/lib/badline/audio/machine_player.rb +25 -10
  16. data/lib/badline/audio/pcm_writer.rb +10 -6
  17. data/lib/badline/audio/playback.rb +6 -5
  18. data/lib/badline/audio/queued_tune.rb +89 -0
  19. data/lib/badline/audio/renderer.rb +57 -10
  20. data/lib/badline/audio/stereo.rb +97 -0
  21. data/lib/badline/audio/terminal.rb +39 -6
  22. data/lib/badline/audio/wav.rb +1 -1
  23. data/lib/badline/audio.rb +2 -3
  24. data/lib/badline/computer.rb +6 -3
  25. data/lib/badline/cpu/boundary.rb +54 -0
  26. data/lib/badline/cpu/core.rb +165 -0
  27. data/lib/badline/cpu.rb +3 -214
  28. data/lib/badline/drive1541/cpu.rb +29 -0
  29. data/lib/badline/drive1541.rb +1 -0
  30. data/lib/badline/ffi.rb +157 -0
  31. data/lib/badline/frontend/app.rb +225 -0
  32. data/lib/badline/frontend/audio_sink.rb +73 -0
  33. data/lib/badline/frontend/boot.rb +52 -0
  34. data/lib/badline/frontend/buttons.rb +90 -0
  35. data/lib/badline/frontend/controls.rb +163 -0
  36. data/lib/badline/frontend/drive_led.rb +51 -0
  37. data/lib/badline/frontend/filter_view.rb +96 -0
  38. data/lib/badline/frontend/frame_rate.rb +83 -0
  39. data/lib/badline/frontend/gamepads.rb +76 -0
  40. data/lib/badline/frontend/info_view.rb +120 -0
  41. data/lib/badline/frontend/keys.rb +48 -0
  42. data/lib/badline/frontend/pacer.rb +100 -0
  43. data/lib/badline/frontend/pad_port.rb +61 -0
  44. data/lib/badline/frontend/painter.rb +175 -0
  45. data/lib/badline/frontend/player_commands.rb +33 -0
  46. data/lib/badline/frontend/player_footer.rb +86 -0
  47. data/lib/badline/frontend/player_header.rb +60 -0
  48. data/lib/badline/frontend/player_screen.rb +124 -0
  49. data/lib/badline/frontend/player_state.rb +67 -0
  50. data/lib/badline/frontend/player_window.rb +228 -0
  51. data/lib/badline/frontend/scope.rb +42 -0
  52. data/lib/badline/frontend/screen.rb +65 -0
  53. data/lib/badline/frontend/screenshot.rb +22 -0
  54. data/lib/badline/frontend/sid_history.rb +175 -0
  55. data/lib/badline/frontend/sid_view.rb +140 -0
  56. data/lib/badline/frontend/snapshots.rb +60 -0
  57. data/lib/badline/frontend/sound.rb +153 -0
  58. data/lib/badline/frontend/timeline.rb +171 -0
  59. data/lib/badline/frontend/visualizer_view.rb +101 -0
  60. data/lib/badline/frontend.rb +32 -0
  61. data/lib/badline/media/queue.rb +158 -0
  62. data/lib/badline/media.rb +13 -8
  63. data/lib/badline/options/event.rb +94 -0
  64. data/lib/badline/options/help.rb +107 -0
  65. data/lib/badline/options/option.rb +29 -0
  66. data/lib/badline/options/table.rb +64 -0
  67. data/lib/badline/options/validation.rb +54 -0
  68. data/lib/badline/options.rb +204 -149
  69. data/lib/badline/reu/dma/saved_state.rb +50 -0
  70. data/lib/badline/reu/dma.rb +56 -47
  71. data/lib/badline/reu/saved_state.rb +2 -1
  72. data/lib/badline/reu.rb +19 -10
  73. data/lib/badline/sdl.rb +133 -51
  74. data/lib/badline/sid/catch_up.rb +11 -1
  75. data/lib/badline/sid/filter.rb +9 -0
  76. data/lib/badline/sid/voice.rb +6 -0
  77. data/lib/badline/sid/waveform/noise_writeback.rb +33 -11
  78. data/lib/badline/sid/waveform.rb +22 -8
  79. data/lib/badline/sid.rb +35 -3
  80. data/lib/badline/snapshot/state.rb +1 -1
  81. data/lib/badline/storage/hvsc.rb +41 -0
  82. data/lib/badline/storage/sid_file.rb +45 -25
  83. data/lib/badline/storage/song_lengths.rb +6 -29
  84. data/lib/badline/storage/stil/credit.rb +50 -0
  85. data/lib/badline/storage/stil.rb +126 -0
  86. data/lib/badline/storage.rb +8 -0
  87. data/lib/badline/version.rb +1 -1
  88. data/lib/badline/vic/bank.rb +21 -0
  89. data/lib/badline/vic.rb +15 -6
  90. metadata +49 -19
  91. data/.claude/hooks/session-start.sh +0 -57
  92. data/.claude/settings.json +0 -14
  93. data/lib/badline/audio/sdl_sink.rb +0 -70
  94. data/lib/badline/audio/stream.rb +0 -123
  95. data/lib/badline/gui/application.rb +0 -243
  96. data/lib/badline/gui/drive_led_pane.rb +0 -29
  97. data/lib/badline/gui/gamepads.rb +0 -104
  98. data/lib/badline/gui/joy_map.rb +0 -27
  99. data/lib/badline/gui/key_map.rb +0 -39
  100. data/lib/badline/gui/palette.rb +0 -35
  101. data/lib/badline/gui/pane.rb +0 -36
  102. data/lib/badline/gui/screen_pane.rb +0 -46
  103. data/lib/badline/gui/snapshots.rb +0 -67
  104. data/lib/badline/gui/window.rb +0 -61
  105. data/lib/badline/gui.rb +0 -14
  106. data/lib/badline/sdl/events.rb +0 -49
  107. data/lib/badline/sdl/functions.rb +0 -74
  108. data/vendor/.gitkeep +0 -0
data/lib/badline/reu.rb CHANGED
@@ -106,9 +106,8 @@ module Badline
106
106
  # From the moment the REC asks for the bus until it hands it back.
107
107
  def dma? = @requested || @running
108
108
 
109
- # Whether the REC held the bus on the last cycle clocked. The CPU keeps
110
- # it while the REC waits to start, and has it back from the cycle the
111
- # REC lets go.
109
+ # Whether the REC ran a transfer on the last cycle clocked, which it
110
+ # goes on doing until the cycle it lets go of the bus.
112
111
  def holds_bus? = @running
113
112
 
114
113
  def reset!
@@ -120,7 +119,7 @@ module Badline
120
119
  @length = @length_start = 0xffff
121
120
  @interrupt_mask = 0x1f
122
121
  @address_control = 0x3f
123
- @armed = @requested = @running = false
122
+ @armed = @requested = @running = @ba_was_low = false
124
123
  interrupt(false)
125
124
  end
126
125
 
@@ -158,16 +157,20 @@ module Badline
158
157
  request_bus
159
158
  end
160
159
 
161
- # Clocks the REC for a cycle, given the VIC's BA line and whether the
162
- # CPU writes on it. A requested transfer starts on the first cycle the
163
- # CPU reads with BA high.
164
- def dma_cycle!(ba_low, cpu_writing)
160
+ # Clocks the REC for a cycle, given the VIC's BA line. A requested
161
+ # transfer starts on the first cycle with BA high as the REC sees it,
162
+ # even one the CPU writes on. Pinned by REU/rmw-trigger. A fetch
163
+ # requested on the cycle before BA falls writes its first byte on the
164
+ # first BA-low cycle as well, and again when it starts. Pinned by
165
+ # REU/badoublewrite.
166
+ def dma_cycle!(ba_low)
167
+ late = ba_low && @vic.reu_ba_late?
165
168
  if @requested
166
- return if cpu_writing || ba_low
169
+ return hold_off if ba_low && !late
167
170
 
168
171
  start_transfer
169
172
  end
170
- @dma.cycle!(ba_low)
173
+ @dma.cycle!(ba_low, late, ba_low && @vic.reu_ba_handed_on?)
171
174
  end_transfer unless @dma.holds_bus?
172
175
  end
173
176
 
@@ -239,9 +242,15 @@ module Badline
239
242
 
240
243
  def request_bus
241
244
  @requested = true
245
+ @ba_was_low = @vic.ba_low?
242
246
  @on_dma&.call
243
247
  end
244
248
 
249
+ def hold_off
250
+ @dma.write_ahead(@command & TRANSFER_TYPE, @c64, @expansion | (@bank << 16)) unless @ba_was_low
251
+ @ba_was_low = true
252
+ end
253
+
245
254
  def start_transfer
246
255
  @requested = false
247
256
  @running = true
data/lib/badline/sdl.rb CHANGED
@@ -1,67 +1,149 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "fiddle"
4
-
5
3
  module Badline
6
- # The libSDL2 calls the window, the gamepads and the audio sink make,
7
- # bound through Fiddle, so nothing has to compile against SDL at install
8
- # time. Only the library itself has to be present.
9
- #
10
- # Each function is a constant named after its C name without the prefix:
11
- # SDL::RenderClear.call(renderer) calls SDL_RenderClear. They're built
12
- # with need_gvl, which skips releasing Ruby's lock around the call: none
13
- # of these block for long, and releasing it costs more than most of them.
4
+ # libSDL2 through Spinel's FFI DSL, the one binding both builds use. The
5
+ # native build compiles the declarations, and finds the library's
6
+ # directory (see NativeBuild.sdl2_flags). badline-ruby loads badline/ffi
7
+ # first, which implements them over Fiddle, so only the library itself
8
+ # has to be present.
14
9
  module SDL
15
- class Error < StandardError; end
10
+ ffi_lib "SDL2"
16
11
 
17
- # Where package managers put the shared library. dlopen searches the
18
- # system paths for the bare names, but not Homebrew's.
19
- LIBRARIES = %w[
20
- libSDL2-2.0.so.0 libSDL2.so
21
- libSDL2-2.0.0.dylib /opt/homebrew/lib/libSDL2-2.0.0.dylib /usr/local/lib/libSDL2-2.0.0.dylib
22
- SDL2.dll
23
- ].freeze
12
+ ffi_func :SDL_Init, [:uint32], :int
13
+ ffi_func :SDL_Quit, [], :void
14
+ ffi_func :SDL_GetError, [], :str
15
+ ffi_func :SDL_SetHint, %i[str str], :int
16
+ ffi_func :SDL_CreateWindow, %i[str int int int int uint32], :ptr
17
+ ffi_func :SDL_SetWindowTitle, %i[ptr str], :void
18
+ ffi_func :SDL_SetWindowSize, %i[ptr int int], :void
19
+ ffi_func :SDL_DestroyWindow, [:ptr], :void
20
+ ffi_func :SDL_CreateRenderer, %i[ptr int uint32], :ptr
21
+ ffi_func :SDL_GetWindowDisplayMode, %i[ptr ptr], :int
22
+ ffi_func :SDL_DestroyRenderer, [:ptr], :void
23
+ ffi_func :SDL_RenderSetLogicalSize, %i[ptr int int], :int
24
+ ffi_func :SDL_CreateTexture, %i[ptr uint32 int int int], :ptr
25
+ ffi_func :SDL_DestroyTexture, [:ptr], :void
26
+ ffi_func :SDL_UpdateTexture, %i[ptr ptr int_array int], :int
27
+ ffi_func :SDL_RenderClear, [:ptr], :int
28
+ ffi_func :SDL_SetRenderDrawColor, %i[ptr int int int int], :int
29
+ ffi_func :SDL_RenderFillRect, %i[ptr ptr], :int
30
+ ffi_func :SDL_RenderDrawLines, %i[ptr int_array int], :int
31
+ ffi_func :SDL_SetTextureBlendMode, %i[ptr int], :int
32
+ ffi_func :SDL_SetTextureColorMod, %i[ptr int int int], :int
33
+ ffi_func :SDL_RenderCopy, %i[ptr ptr ptr ptr], :int
34
+ ffi_func :SDL_RenderPresent, [:ptr], :void
35
+ ffi_func :SDL_PollEvent, [:ptr], :int
36
+ ffi_func :SDL_SetRelativeMouseMode, [:int], :int
37
+ ffi_func :SDL_Delay, [:uint32], :void
38
+ ffi_func :SDL_GetRendererOutputSize, %i[ptr ptr ptr], :int
39
+ ffi_func :SDL_RenderReadPixels, %i[ptr ptr uint32 ptr int], :int
40
+ ffi_func :SDL_CreateRGBSurfaceWithFormatFrom, %i[ptr int int int int uint32], :ptr
41
+ ffi_func :SDL_FreeSurface, [:ptr], :void
42
+ ffi_func :SDL_RWFromFile, %i[str str], :ptr
43
+ ffi_func :SDL_SaveBMP_RW, %i[ptr ptr int], :int
44
+ ffi_func :SDL_InitSubSystem, [:uint32], :int
45
+ ffi_func :SDL_QuitSubSystem, [:uint32], :void
46
+ ffi_func :SDL_OpenAudioDevice, %i[ptr int ptr ptr int], :uint32
47
+ ffi_func :SDL_CloseAudioDevice, [:uint32], :void
48
+ ffi_func :SDL_PauseAudioDevice, %i[uint32 int], :void
49
+ ffi_func :SDL_QueueAudio, %i[uint32 buffer_in uint32], :int
50
+ ffi_func :SDL_GetQueuedAudioSize, [:uint32], :uint32
51
+ ffi_func :SDL_NumJoysticks, [], :int
52
+ ffi_func :SDL_IsGameController, [:int], :int
53
+ ffi_func :SDL_GameControllerOpen, [:int], :ptr
54
+ ffi_func :SDL_GameControllerClose, [:ptr], :void
55
+ ffi_func :SDL_GameControllerName, [:ptr], :str
56
+ ffi_func :SDL_GameControllerGetButton, %i[ptr int], :uint8
57
+ ffi_func :SDL_GameControllerGetAxis, %i[ptr int], :int16
58
+ ffi_func :SDL_ClearQueuedAudio, [:uint32], :void
59
+ ffi_func :SDL_PushEvent, [:ptr], :int
60
+ ffi_func :SDL_GetKeyName, [:int], :str
24
61
 
25
- def self.open_library
26
- LIBRARIES.each do |name|
27
- return Fiddle.dlopen(name)
28
- rescue Fiddle::DLError
29
- next
30
- end
31
- raise Error, "can't find libSDL2 (tried #{LIBRARIES.join(', ')})"
32
- end
62
+ ffi_buffer :event, 56
63
+ ffi_read_u32 :event_type, 0
64
+ ffi_read_u8 :event_repeat, 13
65
+ ffi_read_i32 :event_scancode, 16
66
+ ffi_read_i32 :event_sym, 20
67
+ ffi_read_u16 :event_mod, 24
68
+ # SDL_MouseMotionEvent's xrel and yrel, and SDL_MouseButtonEvent's
69
+ # button.
70
+ ffi_read_i32 :event_xrel, 28
71
+ ffi_read_i32 :event_yrel, 32
72
+ ffi_read_u8 :event_button, 16
73
+ # Where the pointer is, in a motion or button event.
74
+ ffi_read_i32 :event_x, 20
75
+ ffi_read_i32 :event_y, 24
76
+ # SDL_DropEvent's file, which the receiver frees.
77
+ ffi_read_ptr :event_file, 8
33
78
 
34
- LIBRARY = open_library
79
+ ffi_buffer :rect, 16
80
+ ffi_write_i32 :rect_x, 0
81
+ ffi_write_i32 :rect_y, 4
82
+ ffi_write_i32 :rect_w, 8
83
+ ffi_write_i32 :rect_h, 12
84
+ # Where the drive LED goes.
85
+ ffi_buffer :led_rect, 16
86
+ # A glyph's place in the SID player's font, drawn at SDL.rect.
87
+ ffi_buffer :glyph_rect, 16
35
88
 
36
- VOID = Fiddle::TYPE_VOID
37
- INT = Fiddle::TYPE_INT
38
- UINT8 = Fiddle::TYPE_UINT8_T
39
- INT16 = Fiddle::TYPE_INT16_T
40
- UINT32 = Fiddle::TYPE_UINT32_T
41
- POINTER = Fiddle::TYPE_VOIDP
42
- STRING = Fiddle::TYPE_CONST_STRING
89
+ # SDL_DisplayMode: Uint32 format; int w, h, refresh_rate; then a pointer.
90
+ ffi_buffer :display_mode, 24
91
+ ffi_read_i32 :mode_refresh, 12
43
92
 
44
- def self.function(name, arguments, result)
45
- Fiddle::Function.new(LIBRARY[name], arguments, result, name:, need_gvl: true)
46
- end
93
+ ffi_buffer :output_w, 4
94
+ ffi_buffer :output_h, 4
95
+ ffi_read_i32 :read_i32, 0
47
96
 
48
- def self.error = GetError.call
97
+ # SDL_AudioSpec: int freq; Uint16 format; Uint8 channels, silence;
98
+ # Uint16 samples, padding; Uint32 size; then two pointers.
99
+ ffi_buffer :wanted, 32
100
+ ffi_buffer :obtained, 32
101
+ ffi_write_i32 :spec_freq, 0
102
+ ffi_write_u16 :spec_format, 4
103
+ ffi_write_u8 :spec_channels, 6
104
+ ffi_write_u16 :spec_samples, 8
49
105
 
50
- # A negative result from SDL is an error, and SDL_GetError says which.
51
- def self.check(result)
52
- raise Error, error if result.negative?
106
+ ffi_const :INIT_AUDIO, 0x10
107
+ ffi_const :INIT_VIDEO, 0x20
108
+ ffi_const :INIT_GAMECONTROLLER, 0x2000
109
+ ffi_const :INIT_EVENTS, 0x4000
110
+ ffi_const :WINDOWPOS_CENTERED, 0x2fff0000
111
+ ffi_const :WINDOW_RESIZABLE, 0x20
112
+ ffi_const :RENDERER_ACCELERATED, 0x02
113
+ ffi_const :RENDERER_PRESENTVSYNC, 0x04
114
+ ffi_const :PIXELFORMAT_RGB888, 0x16161804
115
+ ffi_const :PIXELFORMAT_ARGB8888, 0x16362004
116
+ ffi_const :BLENDMODE_BLEND, 0x01
117
+ ffi_const :TEXTUREACCESS_STREAMING, 1
118
+ ffi_const :QUIT, 0x100
119
+ ffi_const :KEYDOWN, 0x300
120
+ ffi_const :KEYUP, 0x301
121
+ ffi_const :MOUSEMOTION, 0x400
122
+ ffi_const :MOUSEBUTTONDOWN, 0x401
123
+ ffi_const :MOUSEBUTTONUP, 0x402
124
+ ffi_const :KMOD_SHIFT, 0x0003
125
+ ffi_const :CONTROLLERDEVICEADDED, 0x653
126
+ ffi_const :CONTROLLERDEVICEREMOVED, 0x654
127
+ ffi_const :AUDIO_S16LSB, 0x8010
128
+ ffi_const :ALLOW_FREQUENCY_CHANGE, 0x01
129
+ ffi_const :KEY_TAB, 0x09
130
+ ffi_const :KEY_F10, 0x4000_0043
131
+ end
53
132
 
54
- result
55
- end
133
+ # The few C library calls the native front end makes.
134
+ module LibC
135
+ ffi_func :malloc, [:size_t], :ptr
136
+ ffi_func :free, [:ptr], :void
137
+ ffi_func :poll, %i[ptr size_t int], :int
138
+ # Called with an empty needle, it returns the C string it's given as a
139
+ # String.
140
+ ffi_func :strstr, %i[ptr str], :str
56
141
 
57
- # So is a null handle.
58
- def self.check_pointer(pointer)
59
- raise Error, error if pointer.null?
142
+ ffi_const :POLLIN, 0x01
60
143
 
61
- pointer
62
- end
144
+ # struct pollfd: int fd; short events, revents.
145
+ ffi_buffer :pollfd, 8
146
+ ffi_write_i32 :pollfd_fd, 0
147
+ ffi_write_i16 :pollfd_events, 4
63
148
  end
64
149
  end
65
-
66
- require "badline/sdl/functions"
67
- require "badline/sdl/events"
@@ -96,7 +96,17 @@ module Badline
96
96
 
97
97
  def record_sample(span)
98
98
  sample = @decimator.push(current_sample, span)
99
- @samples << sample if sample
99
+ return unless sample
100
+
101
+ @samples << sample
102
+ record_voices if @voice_samples
103
+ end
104
+
105
+ def record_voices
106
+ voice_samples = @voice_samples
107
+ voice_samples << (@voice1.output >> 7)
108
+ voice_samples << (@voice2.output >> 7)
109
+ voice_samples << (@voice3.output >> 7)
100
110
  end
101
111
 
102
112
  def current_sample
@@ -122,6 +122,15 @@ module Badline
122
122
  reset
123
123
  end
124
124
 
125
+ # The integrators and the registers carry over; the cutoff maps
126
+ # through the new model's curve.
127
+ def model=(model)
128
+ @model = model
129
+ @w0_table = W0.fetch(model)
130
+ @mixer_dc = MIXER_DC.fetch(model)
131
+ @w0 = @w0_table[@cutoff]
132
+ end
133
+
125
134
  def reset
126
135
  @cutoff = 0x000
127
136
  @routing = 0x0
@@ -33,6 +33,12 @@ module Badline
33
33
  @dc_offset = DC_OFFSET.fetch(model)
34
34
  end
35
35
 
36
+ def model=(model)
37
+ @waveform.model = model
38
+ @wave_zero = WAVE_ZERO.fetch(model)
39
+ @dc_offset = DC_OFFSET.fetch(model)
40
+ end
41
+
36
42
  def reset!
37
43
  @waveform.reset!
38
44
  @envelope = Envelope.new
@@ -20,11 +20,12 @@ module Badline
20
20
  # so they lose a different number of bits. The 8580's pair is the one
21
21
  # that reproduces SID/wf12nsr's pulse+noise row (it reads $f8, and the
22
22
  # writeback leaves $fc behind), and SID/wb_testsuite's C->9 and C->E
23
- # rows need that same $fc written at release. On the 6581 the read is
24
- # the $fc of SID/wf12nsr's readme (VICE bug #1037), and the writeback
25
- # pulls nothing: wb_testsuite's 8/9/A/B->C rows run pulse+noise and
26
- # leave the register alone.
27
- PULSE_NOISE_READ = { mos6581: 0xfc0, mos8580: 0xf80 }.freeze
23
+ # rows need that same $fc written at release. The 6581 reads $00 of a
24
+ # full register (SID/wf12nsr's pulse+noise row) and pulls nothing
25
+ # while the register holds: wb_testsuite's 8->C and C->C rows run
26
+ # pulse+noise and leave the register alone. Its lines only land
27
+ # part way through a shift (#write_back_shift).
28
+ PULSE_NOISE_READ = { mos6581: 0x000, mos8580: 0xf80 }.freeze
28
29
  PULSE_NOISE_WRITE = { mos6581: 0xff0, mos8580: 0xfc0 }.freeze
29
30
 
30
31
  # What a 6581 writes back when the test bit falls with only pulse+noise
@@ -40,24 +41,45 @@ module Badline
40
41
  # (SID/wf12nsr's noise+triangle and noise+sawtooth rows).
41
42
  def write_back(selected, value)
42
43
  return value & ((value << 1) | (value >> 1)) unless selected == 0xc
43
- return value if value.zero?
44
+ return 0x000 if @pulse.zero?
44
45
 
45
46
  noise & @pulse_noise_write
46
47
  end
47
48
 
49
+ # What the lines write while the register is part way through a
50
+ # shift, where the 6581's pulse+noise writes its output as read.
51
+ def write_back_mid_shift(selected, value)
52
+ return value if pulse_noise_mid_shift?(selected)
53
+
54
+ write_back(selected, value)
55
+ end
56
+
57
+ def pulse_noise_mid_shift?(selected) = @topbit_feedback && selected == 0xc
58
+
48
59
  def write_shift_register(value)
49
60
  NOISE_TAPS.each { |bit, line| @shift_register &= ~bit if value.nobits?(line) }
50
61
  end
51
62
 
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.
63
+ # The second phase of a shift writes back by the test bit release
64
+ # rule with the waveform left alone. The 6581's pulse+noise, which
65
+ # that rule leaves out, writes its lines too (SID/wf12nsr's
66
+ # pulse+noise row).
67
+ def write_back_shift
68
+ return unless pulse_noise_mid_shift?(@selected) || release_writes_back?(@selected, @selected)
69
+
70
+ write_shift_register(write_back_mid_shift(@selected, @output))
71
+ end
72
+
73
+ # Setting the test bit starts the bleed, and stalls the register part
74
+ # way through a shift, where a noise combination writes its output
75
+ # back first, from the accumulator it had. SID/noiselfsrinit's
76
+ # $f8/$80 pairs set the bit onto all four from noise alone, and
77
+ # SID/wf12nsr's pulse+noise row sets it onto pulse+noise.
56
78
  def raise_test
57
79
  @shift_register_reset = @shift_register_reset_delay
58
80
  return unless @selected.anybits?(0x8) && combined?(@selected) && @shift_pipeline != 1
59
81
 
60
- write_shift_register(write_back(@selected, shape(@selected)))
82
+ write_shift_register(write_back_mid_shift(@selected, shape(@selected)))
61
83
  end
62
84
 
63
85
  # Releasing the test bit finishes the shift it interrupted: the old
@@ -54,17 +54,24 @@ module Badline
54
54
  attr_reader :accumulator, :shift_register, :frequency, :pulse_width, :pulse
55
55
 
56
56
  def initialize(model: :mos6581)
57
- @topbit_feedback = model != :mos8580
57
+ use_model(model)
58
58
  @tri_saw_delay = model == :mos8580
59
- @pulse_noise_read = PULSE_NOISE_READ.fetch(model)
60
- @pulse_noise_write = PULSE_NOISE_WRITE.fetch(model)
61
- @combined = Combined.tables(model)
62
- @shift_register_reset_delay = SHIFT_REGISTER_RESET_DELAY.fetch(model)
63
59
  @accumulator = POWER_ON_ACCUMULATOR
64
60
  @sync_source = @sync_dest = self
65
61
  reset!
66
62
  end
67
63
 
64
+ # The running state carries over. A 6581 never delays its shapers, so
65
+ # the 8580's delayed triangle and sawtooth start from the current phase.
66
+ def model=(model)
67
+ use_model(model)
68
+ if model == :mos8580 && !@tri_saw_delay
69
+ @delayed_sawtooth = sawtooth
70
+ @delayed_triangle = triangle
71
+ end
72
+ @tri_saw_delay = model == :mos8580
73
+ end
74
+
68
75
  # The RES line clears the registers and reseeds the LFSR, but leaves
69
76
  # the accumulator alone (SID/oscinit).
70
77
  def reset!
@@ -190,6 +197,14 @@ module Badline
190
197
 
191
198
  private
192
199
 
200
+ def use_model(model)
201
+ @topbit_feedback = model != :mos8580
202
+ @pulse_noise_read = PULSE_NOISE_READ.fetch(model)
203
+ @pulse_noise_write = PULSE_NOISE_WRITE.fetch(model)
204
+ @combined = Combined.tables(model)
205
+ @shift_register_reset_delay = SHIFT_REGISTER_RESET_DELAY.fetch(model)
206
+ end
207
+
193
208
  def shape(selected, saw = sawtooth, tri = triangle(selected))
194
209
  case selected
195
210
  when 0x1 then tri
@@ -256,8 +271,7 @@ module Badline
256
271
  # A rise of bit 19 starts a shift that takes two more cycles
257
272
  # (SID/noisewriteback's noise_writeback_test2, after libresidfp's shift
258
273
  # pipeline). The first phase latches the output, and the second writes
259
- # it over the taps, by the test bit release rule with the waveform left
260
- # alone, before the register shifts.
274
+ # it over the taps before the register shifts.
261
275
  def advance
262
276
  previous = @accumulator
263
277
  @accumulator = (previous + @frequency) & 0xffffff
@@ -265,7 +279,7 @@ module Badline
265
279
  if @accumulator & ~previous & 0x080000 != 0
266
280
  @shift_pipeline = 2
267
281
  elsif @shift_pipeline != 0 && (@shift_pipeline -= 1).zero?
268
- write_shift_register(write_back(@selected, @output)) if release_writes_back?(@selected, @selected)
282
+ write_back_shift
269
283
  shift_noise
270
284
  end
271
285
  end
data/lib/badline/sid.rb CHANGED
@@ -16,7 +16,9 @@ module Badline
16
16
  # $D41B-$D41C - Voice 3 oscillator/envelope - read only
17
17
  # $D41D-$D41F - Unconnected
18
18
  #
19
- # The register file mirrors every 32 bytes up to $D7FF.
19
+ # The register file mirrors every 32 bytes up to $D7FF. A second or
20
+ # third SID, which only the SID player fits, is built `at` another
21
+ # address and decodes just the 32 bytes there.
20
22
  #
21
23
  # Reading a write-only or unconnected register returns the last byte the
22
24
  # SID saw on the data bus. That value fades to $00 as the capacitance
@@ -55,8 +57,8 @@ module Badline
55
57
  attr_accessor :pots
56
58
  attr_reader :model
57
59
 
58
- def initialize(model: :mos6581, pots: nil, filter_chunk: FILTER_CHUNK)
59
- addressable_at(0xd400, length: 2**10)
60
+ def initialize(model: :mos6581, pots: nil, filter_chunk: FILTER_CHUNK, at: 0xd400)
61
+ addressable_at(at, length: at == 0xd400 ? 2**10 : 2**5)
60
62
  @model = model
61
63
  @pots = pots
62
64
  @bus_ttl_reset = BUS_TTL.fetch(model)
@@ -67,6 +69,7 @@ module Badline
67
69
  @deferred_writes = []
68
70
  @decimator = nil
69
71
  @samples = []
72
+ @voice_samples = nil
70
73
  @filter_chunk = filter_chunk
71
74
  @voices = Voice.linked(VOICES, model:)
72
75
  @voice1, @voice2, @voice3 = @voices
@@ -74,6 +77,18 @@ module Badline
74
77
  reset!
75
78
  end
76
79
 
80
+ # Switches the chip, its filter and its voices to the other model's
81
+ # values. The cycles already passed run on the old model first, and the
82
+ # running state carries over.
83
+ def model=(model)
84
+ catch_up
85
+ @model = model
86
+ @bus_ttl_reset = BUS_TTL.fetch(model)
87
+ @filter.model = model
88
+ @voices.each { |voice| voice.model = model }
89
+ update_span_rules
90
+ end
91
+
77
92
  # The RES line clears the registers, the data bus, the filter and the
78
93
  # voices, all but their accumulators. Recording carries on across it.
79
94
  def reset!
@@ -112,6 +127,23 @@ module Badline
112
127
  @samples = []
113
128
  end
114
129
 
130
+ # Also keeps each voice's own output, before the filter and the volume,
131
+ # as each recorded sample closes, for #drain_voice_samples.
132
+ def record_voices!
133
+ @voice_samples = [] if @voice_samples.nil?
134
+ end
135
+
136
+ # The voices' outputs since the last drain, three to a sample, in voice
137
+ # order, or nothing unless #record_voices! asked for them.
138
+ def drain_voice_samples
139
+ return [] if @voice_samples.nil?
140
+
141
+ catch_up
142
+ samples = @voice_samples
143
+ @voice_samples = []
144
+ samples
145
+ end
146
+
115
147
  # The samples recorded since the last drain.
116
148
  def drain_samples
117
149
  catch_up
@@ -14,7 +14,7 @@ module Badline
14
14
  # The layout the chips write their fields in. It goes up whenever a
15
15
  # save_state writes something else, so a State in another layout
16
16
  # fails before anything is read into a machine.
17
- SCHEMA = 1
17
+ SCHEMA = 2
18
18
 
19
19
  attr_reader :values, :strings
20
20
 
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Badline
4
+ module Storage
5
+ # The layout of an HVSC collection: tunes in directories under its root,
6
+ # and databases about them in `DOCUMENTS`. A tune's HVSC path is its path
7
+ # from the root, as `/MUSICIANS/H/Hubbard_Rob/Commando.sid`.
8
+ module HVSC
9
+ class << self
10
+ # The named file in the DOCUMENTS directory of a collection above the
11
+ # tune, or else of the one `$HVSC_BASE` points at.
12
+ def document(tune_path, name)
13
+ candidates(tune_path).map { |dir| File.join(dir, "DOCUMENTS", name) }
14
+ .find { |path| File.file?(path) }
15
+ end
16
+
17
+ # The tune's HVSC path in the collection that holds the document, or
18
+ # nil when the tune lies outside it.
19
+ def path(tune_path, document)
20
+ root = File.dirname(File.expand_path(document), 2)
21
+ tune = File.expand_path(tune_path)
22
+ tune.start_with?("#{root}/") ? tune[root.length..] : nil
23
+ end
24
+
25
+ private
26
+
27
+ def candidates(tune_path)
28
+ dirs = []
29
+ dir = File.dirname(File.expand_path(tune_path))
30
+ until dirs.last == dir
31
+ dirs << dir
32
+ dir = File.dirname(dir)
33
+ end
34
+ base = ENV.fetch("HVSC_BASE", nil)
35
+ dirs << base if base
36
+ dirs
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end