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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +77 -0
- data/CLAUDE.md +10 -9
- data/README.md +135 -42
- data/doc/pinned-behaviour.md +131 -30
- data/exe/badline-ruby +28 -24
- data/lib/badline/address_bus/extra_sids.rb +30 -0
- data/lib/badline/address_bus/sid_slots.rb +42 -0
- data/lib/badline/address_bus.rb +15 -1
- data/lib/badline/audio/aiff.rb +2 -2
- data/lib/badline/audio/bare_player.rb +23 -12
- data/lib/badline/audio/cli.rb +167 -59
- data/lib/badline/audio/console.rb +2 -1
- data/lib/badline/audio/jukebox.rb +121 -36
- data/lib/badline/audio/machine_player.rb +25 -10
- data/lib/badline/audio/pcm_writer.rb +10 -6
- data/lib/badline/audio/playback.rb +6 -5
- data/lib/badline/audio/queued_tune.rb +89 -0
- data/lib/badline/audio/renderer.rb +57 -10
- data/lib/badline/audio/stereo.rb +97 -0
- data/lib/badline/audio/terminal.rb +39 -6
- data/lib/badline/audio/wav.rb +1 -1
- data/lib/badline/audio.rb +2 -3
- data/lib/badline/computer.rb +6 -3
- data/lib/badline/cpu/boundary.rb +54 -0
- data/lib/badline/cpu/core.rb +165 -0
- data/lib/badline/cpu.rb +3 -214
- data/lib/badline/drive1541/cpu.rb +29 -0
- data/lib/badline/drive1541.rb +1 -0
- data/lib/badline/ffi.rb +157 -0
- data/lib/badline/frontend/app.rb +225 -0
- data/lib/badline/frontend/audio_sink.rb +73 -0
- data/lib/badline/frontend/boot.rb +52 -0
- data/lib/badline/frontend/buttons.rb +90 -0
- data/lib/badline/frontend/controls.rb +163 -0
- data/lib/badline/frontend/drive_led.rb +51 -0
- data/lib/badline/frontend/filter_view.rb +96 -0
- data/lib/badline/frontend/frame_rate.rb +83 -0
- data/lib/badline/frontend/gamepads.rb +76 -0
- data/lib/badline/frontend/info_view.rb +120 -0
- data/lib/badline/frontend/keys.rb +48 -0
- data/lib/badline/frontend/pacer.rb +100 -0
- data/lib/badline/frontend/pad_port.rb +61 -0
- data/lib/badline/frontend/painter.rb +175 -0
- data/lib/badline/frontend/player_commands.rb +33 -0
- data/lib/badline/frontend/player_footer.rb +86 -0
- data/lib/badline/frontend/player_header.rb +60 -0
- data/lib/badline/frontend/player_screen.rb +124 -0
- data/lib/badline/frontend/player_state.rb +67 -0
- data/lib/badline/frontend/player_window.rb +228 -0
- data/lib/badline/frontend/scope.rb +42 -0
- data/lib/badline/frontend/screen.rb +65 -0
- data/lib/badline/frontend/screenshot.rb +22 -0
- data/lib/badline/frontend/sid_history.rb +175 -0
- data/lib/badline/frontend/sid_view.rb +140 -0
- data/lib/badline/frontend/snapshots.rb +60 -0
- data/lib/badline/frontend/sound.rb +153 -0
- data/lib/badline/frontend/timeline.rb +171 -0
- data/lib/badline/frontend/visualizer_view.rb +101 -0
- data/lib/badline/frontend.rb +32 -0
- data/lib/badline/media/queue.rb +158 -0
- data/lib/badline/media.rb +13 -8
- data/lib/badline/options/event.rb +94 -0
- data/lib/badline/options/help.rb +107 -0
- data/lib/badline/options/option.rb +29 -0
- data/lib/badline/options/table.rb +64 -0
- data/lib/badline/options/validation.rb +54 -0
- data/lib/badline/options.rb +204 -149
- data/lib/badline/reu/dma/saved_state.rb +50 -0
- data/lib/badline/reu/dma.rb +56 -47
- data/lib/badline/reu/saved_state.rb +2 -1
- data/lib/badline/reu.rb +19 -10
- data/lib/badline/sdl.rb +133 -51
- data/lib/badline/sid/catch_up.rb +11 -1
- data/lib/badline/sid/filter.rb +9 -0
- data/lib/badline/sid/voice.rb +6 -0
- data/lib/badline/sid/waveform/noise_writeback.rb +33 -11
- data/lib/badline/sid/waveform.rb +22 -8
- data/lib/badline/sid.rb +35 -3
- data/lib/badline/snapshot/state.rb +1 -1
- data/lib/badline/storage/hvsc.rb +41 -0
- data/lib/badline/storage/sid_file.rb +45 -25
- data/lib/badline/storage/song_lengths.rb +6 -29
- data/lib/badline/storage/stil/credit.rb +50 -0
- data/lib/badline/storage/stil.rb +126 -0
- data/lib/badline/storage.rb +8 -0
- data/lib/badline/version.rb +1 -1
- data/lib/badline/vic/bank.rb +21 -0
- data/lib/badline/vic.rb +15 -6
- metadata +49 -19
- data/.claude/hooks/session-start.sh +0 -57
- data/.claude/settings.json +0 -14
- data/lib/badline/audio/sdl_sink.rb +0 -70
- data/lib/badline/audio/stream.rb +0 -123
- data/lib/badline/gui/application.rb +0 -243
- data/lib/badline/gui/drive_led_pane.rb +0 -29
- data/lib/badline/gui/gamepads.rb +0 -104
- data/lib/badline/gui/joy_map.rb +0 -27
- data/lib/badline/gui/key_map.rb +0 -39
- data/lib/badline/gui/palette.rb +0 -35
- data/lib/badline/gui/pane.rb +0 -36
- data/lib/badline/gui/screen_pane.rb +0 -46
- data/lib/badline/gui/snapshots.rb +0 -67
- data/lib/badline/gui/window.rb +0 -61
- data/lib/badline/gui.rb +0 -14
- data/lib/badline/sdl/events.rb +0 -49
- data/lib/badline/sdl/functions.rb +0 -74
- 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
|
|
110
|
-
#
|
|
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
|
|
162
|
-
#
|
|
163
|
-
# CPU
|
|
164
|
-
|
|
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
|
|
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
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
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
|
-
|
|
10
|
+
ffi_lib "SDL2"
|
|
16
11
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
]
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
93
|
+
ffi_buffer :output_w, 4
|
|
94
|
+
ffi_buffer :output_h, 4
|
|
95
|
+
ffi_read_i32 :read_i32, 0
|
|
47
96
|
|
|
48
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
58
|
-
def self.check_pointer(pointer)
|
|
59
|
-
raise Error, error if pointer.null?
|
|
142
|
+
ffi_const :POLLIN, 0x01
|
|
60
143
|
|
|
61
|
-
|
|
62
|
-
|
|
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"
|
data/lib/badline/sid/catch_up.rb
CHANGED
|
@@ -96,7 +96,17 @@ module Badline
|
|
|
96
96
|
|
|
97
97
|
def record_sample(span)
|
|
98
98
|
sample = @decimator.push(current_sample, span)
|
|
99
|
-
|
|
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
|
data/lib/badline/sid/filter.rb
CHANGED
|
@@ -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
|
data/lib/badline/sid/voice.rb
CHANGED
|
@@ -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.
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
# leave the register alone.
|
|
27
|
-
|
|
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
|
|
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
|
-
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
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(
|
|
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
|
data/lib/badline/sid/waveform.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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
|