badline 0.1.0 → 0.2.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 +167 -0
- data/CLAUDE.md +217 -0
- data/CODE_OF_CONDUCT.md +92 -0
- data/CONTRIBUTING.md +51 -0
- data/README.md +224 -31
- data/doc/pinned-behaviour.md +1117 -0
- data/exe/badline +22 -2
- data/exe/badline-sid +34 -0
- data/lib/badline/address_bus.rb +88 -23
- data/lib/badline/audio/aiff.rb +36 -0
- data/lib/badline/audio/bare_player.rb +129 -0
- data/lib/badline/audio/cli.rb +133 -0
- data/lib/badline/audio/console.rb +75 -0
- data/lib/badline/audio/jukebox.rb +82 -0
- data/lib/badline/audio/machine_player.rb +57 -0
- data/lib/badline/audio/options.rb +137 -0
- data/lib/badline/audio/pcm_writer.rb +57 -0
- data/lib/badline/audio/playback.rb +108 -0
- data/lib/badline/audio/renderer.rb +85 -0
- data/lib/badline/audio/sdl_sink.rb +102 -0
- data/lib/badline/audio/wav.rb +23 -0
- data/lib/badline/audio.rb +14 -0
- data/lib/badline/cartridge/action_replay.rb +111 -0
- data/lib/badline/cartridge/atomic_power.rb +24 -0
- data/lib/badline/cartridge/bank.rb +81 -0
- data/lib/badline/cartridge/comal80.rb +48 -0
- data/lib/badline/cartridge/dinamic.rb +34 -0
- data/lib/badline/cartridge/easy_flash.rb +122 -0
- data/lib/badline/cartridge/epyx_fastload.rb +71 -0
- data/lib/badline/cartridge/final_cartridge3.rb +77 -0
- data/lib/badline/cartridge/flash.rb +265 -0
- data/lib/badline/cartridge/freezer.rb +31 -0
- data/lib/badline/cartridge/fun_play.rb +38 -0
- data/lib/badline/cartridge/g_mod2.rb +62 -0
- data/lib/badline/cartridge/game_system.rb +40 -0
- data/lib/badline/cartridge/kcs_power.rb +72 -0
- data/lib/badline/cartridge/mach5.rb +34 -0
- data/lib/badline/cartridge/magic_desk.rb +6 -3
- data/lib/badline/cartridge/ocean.rb +11 -12
- data/lib/badline/cartridge/pagefox.rb +56 -0
- data/lib/badline/cartridge/retro_replay.rb +276 -0
- data/lib/badline/cartridge/rex_utility.rb +33 -0
- data/lib/badline/cartridge/rgcd.rb +48 -0
- data/lib/badline/cartridge/simons_basic.rb +40 -0
- data/lib/badline/cartridge/standard.rb +3 -19
- data/lib/badline/cartridge/super_games.rb +36 -0
- data/lib/badline/cartridge/westermann.rb +34 -0
- data/lib/badline/cartridge/zaxxon.rb +50 -0
- data/lib/badline/cartridge.rb +131 -15
- data/lib/badline/cia/interrupt_register.rb +88 -0
- data/lib/badline/cia/serial.rb +219 -0
- data/lib/badline/cia/timer.rb +43 -31
- data/lib/badline/cia.rb +109 -74
- data/lib/badline/color_memory.rb +14 -5
- data/lib/badline/computer.rb +68 -8
- data/lib/badline/control_ports.rb +54 -9
- data/lib/badline/cpu/addressing.rb +131 -0
- data/lib/badline/cpu/microcode.rb +171 -0
- data/lib/badline/cpu/operations.rb +88 -0
- data/lib/badline/cpu/stack_operations.rb +82 -0
- data/lib/badline/cpu.rb +108 -167
- data/lib/badline/cycleable.rb +3 -0
- data/lib/badline/datasette.rb +81 -0
- data/lib/badline/debug_register.rb +28 -0
- data/lib/badline/gui/application.rb +79 -18
- data/lib/badline/gui/gamepads.rb +97 -0
- data/lib/badline/gui/joy_map.rb +15 -7
- data/lib/badline/gui.rb +1 -0
- data/lib/badline/input/mouse1351.rb +41 -0
- data/lib/badline/input/paddles.rb +36 -0
- data/lib/badline/input.rb +4 -0
- data/lib/badline/instruction_set/arithmetic.rb +16 -21
- data/lib/badline/instruction_set/bitwise.rb +17 -22
- data/lib/badline/instruction_set/branch.rb +20 -19
- data/lib/badline/instruction_set/flag.rb +7 -7
- data/lib/badline/instruction_set/illegal.rb +14 -22
- data/lib/badline/instruction_set/inc_dec.rb +8 -10
- data/lib/badline/instruction_set/stack.rb +5 -57
- data/lib/badline/instruction_set/transfer.rb +9 -9
- data/lib/badline/instruction_set.rb +9 -31
- data/lib/badline/interrupts.rb +104 -0
- data/lib/badline/joystick.rb +9 -9
- data/lib/badline/kernal_trap/channel.rb +33 -0
- data/lib/badline/kernal_trap/drive.rb +239 -0
- data/lib/badline/kernal_trap/file.rb +4 -24
- data/lib/badline/kernal_trap/load.rb +2 -2
- data/lib/badline/kernal_trap/routine.rb +34 -0
- data/lib/badline/kernal_trap/save.rb +1 -1
- data/lib/badline/kernal_trap/serial.rb +139 -0
- data/lib/badline/kernal_trap.rb +4 -0
- data/lib/badline/keyboard.rb +40 -16
- data/lib/badline/media.rb +48 -10
- data/lib/badline/roms/eapi/LICENSE.md +15 -0
- data/lib/badline/roms/eapi/README +41 -0
- data/lib/badline/roms/eapi/eapi-am29f040-14 +0 -0
- data/lib/badline/sid/decimator.rb +46 -0
- data/lib/badline/sid/envelope.rb +232 -0
- data/lib/badline/sid/filter.rb +213 -0
- data/lib/badline/sid/voice.rb +61 -0
- data/lib/badline/sid/waveform/combined.rb +126 -0
- data/lib/badline/sid/waveform/fast_forward.rb +105 -0
- data/lib/badline/sid/waveform/noise_writeback.rb +82 -0
- data/lib/badline/sid/waveform.rb +289 -0
- data/lib/badline/sid.rb +274 -7
- data/lib/badline/status.rb +32 -27
- data/lib/badline/storage/crt_file.rb +18 -2
- data/lib/badline/storage/disk_image.rb +52 -5
- data/lib/badline/storage/host_directory.rb +18 -4
- data/lib/badline/storage/sid_file.rb +274 -0
- data/lib/badline/storage/song_lengths.rb +65 -0
- data/lib/badline/storage/t64.rb +67 -0
- data/lib/badline/storage/tap.rb +68 -0
- data/lib/badline/storage.rb +19 -0
- data/lib/badline/time_of_day.rb +105 -52
- data/lib/badline/version.rb +1 -1
- data/lib/badline/vic/bank.rb +14 -6
- data/lib/badline/vic/border_mask.rb +76 -0
- data/lib/badline/vic/collisions.rb +104 -0
- data/lib/badline/vic/color_patches.rb +33 -0
- data/lib/badline/vic/display_state.rb +92 -29
- data/lib/badline/vic/graphics_mode.rb +78 -53
- data/lib/badline/vic/graphics_shifter.rb +140 -0
- data/lib/badline/vic/register_log.rb +63 -0
- data/lib/badline/vic/registers.rb +3 -0
- data/lib/badline/vic/sequencer.rb +163 -137
- data/lib/badline/vic/sequencer_output.rb +116 -0
- data/lib/badline/vic/sprite/internal_bus.rb +49 -0
- data/lib/badline/vic/sprite/shifter.rb +129 -0
- data/lib/badline/vic/sprite.rb +275 -65
- data/lib/badline/vic/sprites.rb +220 -58
- data/lib/badline/vic.rb +373 -49
- data/lib/badline.rb +5 -0
- data/vendor/.gitkeep +0 -0
- metadata +103 -4
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
|
-
|
|
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
|
-
|
|
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
|
|
22
|
-
|
|
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
|
|
36
|
-
|
|
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
|
|
@@ -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.
|
|
Binary file
|
|
@@ -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
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class SID
|
|
5
|
+
# ADSR envelope generator.
|
|
6
|
+
#
|
|
7
|
+
# A 15-bit rate counter divides the 1 MHz clock down to one envelope step
|
|
8
|
+
# per period. Attack steps linearly; decay and release run through a
|
|
9
|
+
# second, level-dependent divider that approximates an exponential
|
|
10
|
+
# curve.
|
|
11
|
+
#
|
|
12
|
+
# Each stage runs a cycle or more behind the one feeding it, after
|
|
13
|
+
# reSID 1.0's single-cycle pipeline: the rate counter resets the cycle
|
|
14
|
+
# after it matches, the envelope counter steps two cycles after that,
|
|
15
|
+
# and a gate edge only switches the rate over on its second cycle.
|
|
16
|
+
class Envelope
|
|
17
|
+
# Rate counter comparison values for each ADSR nibble. The counter
|
|
18
|
+
# takes a cycle to reset, so the period is one cycle longer (2ms to
|
|
19
|
+
# 8s over the attack range).
|
|
20
|
+
PERIODS = [8, 31, 62, 94, 148, 219, 266, 312,
|
|
21
|
+
391, 976, 1953, 3125, 3906, 11_719, 19_531, 31_250].freeze
|
|
22
|
+
|
|
23
|
+
# Envelope levels where the exponential divider changes, and the
|
|
24
|
+
# divider it changes to.
|
|
25
|
+
EXPONENTIAL_PERIODS = { 0xff => 1, 0x5d => 2, 0x36 => 4, 0x1a => 8,
|
|
26
|
+
0x0e => 16, 0x06 => 30, 0x00 => 1 }.freeze
|
|
27
|
+
|
|
28
|
+
attr_reader :state, :counter, :env3
|
|
29
|
+
|
|
30
|
+
def initialize
|
|
31
|
+
@attack = @decay = @sustain = @release = 0x0
|
|
32
|
+
@gate = false
|
|
33
|
+
@state = @next_state = :release
|
|
34
|
+
@counter = @env3 = 0x00
|
|
35
|
+
@rate_counter = 0
|
|
36
|
+
@rate_period = PERIODS[0]
|
|
37
|
+
@reset_rate_counter = false
|
|
38
|
+
@exponential_counter = 0
|
|
39
|
+
@exponential_period = 1
|
|
40
|
+
@state_pipeline = @envelope_pipeline = @exponential_pipeline = 0
|
|
41
|
+
@hold_zero = true
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def output = @counter
|
|
45
|
+
|
|
46
|
+
# The gate edge switches the state over two cycles. Rising, it runs
|
|
47
|
+
# the decay rate for the first of them. The rate counter is left
|
|
48
|
+
# running, so the first step lands somewhere inside the current
|
|
49
|
+
# period.
|
|
50
|
+
def control=(value)
|
|
51
|
+
gate = value.anybits?(0x01)
|
|
52
|
+
return if gate == @gate
|
|
53
|
+
|
|
54
|
+
@gate = gate
|
|
55
|
+
gate ? gate_on : gate_off
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def attack_decay=(value)
|
|
59
|
+
@attack = (value >> 4) & 0x0f
|
|
60
|
+
@decay = value & 0x0f
|
|
61
|
+
@rate_period = PERIODS[@attack] if @state == :attack
|
|
62
|
+
@rate_period = PERIODS[@decay] if @state == :decay_sustain
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def sustain_release=(value)
|
|
66
|
+
@sustain = (value >> 4) & 0x0f
|
|
67
|
+
@release = value & 0x0f
|
|
68
|
+
@rate_period = PERIODS[@release] if @state == :release
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# ENV3 reads the counter as it stood at the start of the cycle.
|
|
72
|
+
#
|
|
73
|
+
# Lowering the rate period below the current counter sends it the long
|
|
74
|
+
# way round through 2^15 before the envelope can step, the ADSR delay
|
|
75
|
+
# bug that hard restarts rely on. Counting from 0x7fff wraps to 1.
|
|
76
|
+
def cycle!
|
|
77
|
+
@env3 = @counter
|
|
78
|
+
run_pipeline if pipelined?
|
|
79
|
+
return @reset_rate_counter = true if @rate_counter == @rate_period
|
|
80
|
+
|
|
81
|
+
@rate_counter += 1
|
|
82
|
+
@rate_counter = 1 if @rate_counter == 0x8000
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Runs `cycles` cycles as #cycle! would. Between steps only the rate
|
|
86
|
+
# counter moves, so it jumps from one match to the next and the cycles
|
|
87
|
+
# around each step run whole. Frozen at zero, a period changes
|
|
88
|
+
# nothing, so whole periods are counted off at once.
|
|
89
|
+
def fast_forward(cycles)
|
|
90
|
+
while cycles.positive?
|
|
91
|
+
if pipelined?
|
|
92
|
+
cycle!
|
|
93
|
+
cycles -= 1
|
|
94
|
+
next
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
@env3 = @counter
|
|
98
|
+
idle = cycles_to_match
|
|
99
|
+
return advance_rate_counter(cycles) if cycles < idle
|
|
100
|
+
|
|
101
|
+
advance_rate_counter(idle)
|
|
102
|
+
cycles = skip_frozen_periods(cycles - idle)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
def land_step
|
|
109
|
+
@envelope_pipeline -= 1
|
|
110
|
+
step if @envelope_pipeline.zero? && !@hold_zero
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def gate_on
|
|
114
|
+
@next_state = :attack
|
|
115
|
+
@state = :decay_sustain
|
|
116
|
+
@rate_period = PERIODS[@decay]
|
|
117
|
+
@state_pipeline = 2
|
|
118
|
+
if @reset_rate_counter || @exponential_pipeline == 2
|
|
119
|
+
@envelope_pipeline = @exponential_period == 1 || @exponential_pipeline == 2 ? 2 : 4
|
|
120
|
+
elsif @exponential_pipeline == 1
|
|
121
|
+
@state_pipeline = 3
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def gate_off
|
|
126
|
+
@next_state = :release
|
|
127
|
+
@state_pipeline = @envelope_pipeline.positive? ? 3 : 2
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def state_change
|
|
131
|
+
@state_pipeline -= 1
|
|
132
|
+
if @next_state == :attack
|
|
133
|
+
enter_attack if @state_pipeline.zero?
|
|
134
|
+
elsif (@state == :attack && @state_pipeline.zero?) ||
|
|
135
|
+
(@state == :decay_sustain && @state_pipeline == 1)
|
|
136
|
+
@state = :release
|
|
137
|
+
@rate_period = PERIODS[@release]
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def enter_attack
|
|
142
|
+
@state = :attack
|
|
143
|
+
@rate_period = PERIODS[@attack]
|
|
144
|
+
@hold_zero = false
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# The first attack step also resets the exponential divider.
|
|
148
|
+
def reset_rate_counter
|
|
149
|
+
@rate_counter = 0
|
|
150
|
+
@reset_rate_counter = false
|
|
151
|
+
if @state == :attack
|
|
152
|
+
@exponential_counter = 0
|
|
153
|
+
@envelope_pipeline = 2
|
|
154
|
+
elsif !@hold_zero && (@exponential_counter += 1) == @exponential_period
|
|
155
|
+
@exponential_pipeline = @exponential_period == 1 ? 1 : 2
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def exponential_step
|
|
160
|
+
@exponential_counter = 0
|
|
161
|
+
return unless @state == :release || (@state == :decay_sustain && @counter != sustain_level)
|
|
162
|
+
|
|
163
|
+
@envelope_pipeline = 1
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def pipelined?
|
|
167
|
+
@reset_rate_counter || (@state_pipeline | @envelope_pipeline | @exponential_pipeline) != 0
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def run_pipeline
|
|
171
|
+
state_change if @state_pipeline.positive?
|
|
172
|
+
land_step if @envelope_pipeline.positive?
|
|
173
|
+
if @exponential_pipeline.positive? && (@exponential_pipeline -= 1).zero?
|
|
174
|
+
exponential_step
|
|
175
|
+
elsif @reset_rate_counter
|
|
176
|
+
reset_rate_counter
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Cycles that only count before the rate counter matches its period.
|
|
181
|
+
def cycles_to_match
|
|
182
|
+
return @rate_period - @rate_counter if @rate_counter <= @rate_period
|
|
183
|
+
|
|
184
|
+
0x7fff - @rate_counter + @rate_period
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def advance_rate_counter(cycles)
|
|
188
|
+
@rate_counter += cycles
|
|
189
|
+
@rate_counter -= 0x7fff if @rate_counter > 0x7fff
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# With the counter on its period, a frozen envelope comes back to the
|
|
193
|
+
# same state every period plus one cycles. Otherwise the next cycle
|
|
194
|
+
# starts a step.
|
|
195
|
+
def skip_frozen_periods(cycles)
|
|
196
|
+
cycles %= @rate_period + 1 if @hold_zero && @state != :attack
|
|
197
|
+
return cycles unless cycles.positive?
|
|
198
|
+
|
|
199
|
+
cycle!
|
|
200
|
+
cycles - 1
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def step
|
|
204
|
+
if @state == :attack
|
|
205
|
+
attack_step
|
|
206
|
+
else
|
|
207
|
+
@counter = (@counter - 1) & 0xff
|
|
208
|
+
end
|
|
209
|
+
set_exponential_period
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def attack_step
|
|
213
|
+
@counter = (@counter + 1) & 0xff
|
|
214
|
+
return unless @counter == 0xff
|
|
215
|
+
|
|
216
|
+
@state = :decay_sustain
|
|
217
|
+
@rate_period = PERIODS[@decay]
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def sustain_level = (@sustain << 4) | @sustain
|
|
221
|
+
|
|
222
|
+
# Reaching zero freezes the envelope until the gate is raised again.
|
|
223
|
+
def set_exponential_period
|
|
224
|
+
period = EXPONENTIAL_PERIODS[@counter]
|
|
225
|
+
return unless period
|
|
226
|
+
|
|
227
|
+
@exponential_period = period
|
|
228
|
+
@hold_zero = true if @counter.zero?
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class SID
|
|
5
|
+
# The SID's multimode filter: a state-variable design integrating low-,
|
|
6
|
+
# band- and high-pass in parallel, with the mixer and master volume on
|
|
7
|
+
# its output, and the C64 board's own RC network after that.
|
|
8
|
+
#
|
|
9
|
+
# Voices are routed through it or around it per the RES/FILT register,
|
|
10
|
+
# and the outputs selected by MODE/VOL are summed back together with the
|
|
11
|
+
# unfiltered ones.
|
|
12
|
+
class Filter
|
|
13
|
+
# Cutoff frequency in Hz at each chip's breakpoints. The 6581 curve was
|
|
14
|
+
# sampled from a real chip by reSID and bends sharply around $300; the
|
|
15
|
+
# 8580's is close to a straight line from 0 to 12.5 kHz. The curve
|
|
16
|
+
# between breakpoints is interpolated linearly; reSID fits a spline, so
|
|
17
|
+
# the two differ by a few Hz mid-segment.
|
|
18
|
+
CUTOFF_POINTS = {
|
|
19
|
+
mos6581: [[0, 220], [128, 230], [256, 250], [384, 300],
|
|
20
|
+
[512, 420], [640, 780], [768, 1600], [832, 2300],
|
|
21
|
+
[896, 3200], [960, 4300], [1024, 5000], [1088, 5400],
|
|
22
|
+
[1152, 5800], [1216, 6000], [1280, 6200], [1344, 6400],
|
|
23
|
+
[1408, 6600], [1472, 6800], [1536, 7000], [1600, 7200],
|
|
24
|
+
[1664, 7400], [1728, 7600], [1792, 7800], [1856, 8000],
|
|
25
|
+
[1920, 8200], [1984, 8400], [2047, 8600]].freeze,
|
|
26
|
+
mos8580: [[0, 0], [128, 800], [256, 1600], [384, 2500],
|
|
27
|
+
[512, 3300], [640, 4100], [768, 4800], [896, 5600],
|
|
28
|
+
[1024, 6500], [1152, 7500], [1280, 8400], [1408, 9200],
|
|
29
|
+
[1536, 9800], [1664, 10_500], [1792, 11_000],
|
|
30
|
+
[1920, 11_700], [2047, 12_500]].freeze
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
# 2^20 / 1 MHz, so one cycle of integration is a plain shift.
|
|
34
|
+
SCALE = (2**20) / 1_000_000.0
|
|
35
|
+
|
|
36
|
+
# The integrator goes unstable above ~16 kHz at a one-cycle step.
|
|
37
|
+
W0_MAX = (2 * Math::PI * 16_000 * SCALE).to_i
|
|
38
|
+
|
|
39
|
+
# A step of several cycles scales w0 by their count. reSID keeps such
|
|
40
|
+
# a step stable by capping the cutoff at 4 kHz over 8 cycles; this is
|
|
41
|
+
# the same bound on the product, so a single cycle is never capped.
|
|
42
|
+
W0_STEP_MAX = (2 * Math::PI * 4_000 * 8 * SCALE).to_i
|
|
43
|
+
|
|
44
|
+
def self.build_w0(points)
|
|
45
|
+
table = Array.new(2048)
|
|
46
|
+
points.each_cons(2) do |(fc0, hz0), (fc1, hz1)|
|
|
47
|
+
(fc0..fc1).each do |register|
|
|
48
|
+
hz = hz0 + (((hz1 - hz0) * (register - fc0)).to_f / (fc1 - fc0))
|
|
49
|
+
table[register] = [(2 * Math::PI * hz * SCALE).to_i, W0_MAX].min
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
table.freeze
|
|
53
|
+
end
|
|
54
|
+
private_class_method :build_w0
|
|
55
|
+
|
|
56
|
+
W0 = CUTOFF_POINTS.transform_values { |points| build_w0(points) }.freeze
|
|
57
|
+
|
|
58
|
+
# 1024/Q, with Q sweeping from 0.707 up to 1.707 across the register.
|
|
59
|
+
RESONANCE = (0x0..0xf).map { |res| (1024.0 / (0.707 + (res / 15.0))).to_i }.freeze
|
|
60
|
+
|
|
61
|
+
# The mixer's own input DC offset, derived by reSID from the 6581's
|
|
62
|
+
# zero-volume and full-volume output levels. The 8580 has none.
|
|
63
|
+
MIXER_DC = { mos6581: -((0xfff * 0xff) / 18) >> 7, mos8580: 0 }.freeze
|
|
64
|
+
|
|
65
|
+
# Fractional bits carried in the integrators. At low cutoffs a cycle's
|
|
66
|
+
# step is only a few units, which truncating to whole units would
|
|
67
|
+
# swallow, and always toward minus infinity.
|
|
68
|
+
FRACTION = 12
|
|
69
|
+
|
|
70
|
+
# The RC network between the SID's audio pin and the C64's output jack:
|
|
71
|
+
# a 10kΩ/1000pF low-pass at ~16 kHz, then a 1kΩ/10µF high-pass at
|
|
72
|
+
# ~16 Hz that strips the 6581's DC offset off the mix.
|
|
73
|
+
class External
|
|
74
|
+
W0_LOWPASS = (100_000 * SCALE).round
|
|
75
|
+
W0_HIGHPASS = (100 * SCALE).round
|
|
76
|
+
|
|
77
|
+
# The high-pass step is W0_HIGHPASS/2^20 of the gap, so in whole
|
|
78
|
+
# units it stalls on any gap under ~10k, as reSID's does. Sixteen
|
|
79
|
+
# fractional bits bring that under one unit.
|
|
80
|
+
FRACTION = 16
|
|
81
|
+
|
|
82
|
+
# Its input arrives with the filter's fractional bits still on.
|
|
83
|
+
INPUT_SHIFT = FRACTION - Filter::FRACTION
|
|
84
|
+
|
|
85
|
+
attr_reader :output
|
|
86
|
+
|
|
87
|
+
def initialize
|
|
88
|
+
@lowpass = 0
|
|
89
|
+
@highpass = 0
|
|
90
|
+
@output = 0
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Takes the input in 1/2^Filter::FRACTION units, and reads the
|
|
94
|
+
# output out in whole ones.
|
|
95
|
+
def cycle!(input, cycles = 1)
|
|
96
|
+
delta_lowpass = ((W0_LOWPASS >> 8) * cycles * ((input << INPUT_SHIFT) - @lowpass)) >> 12
|
|
97
|
+
delta_highpass = (W0_HIGHPASS * cycles * (@lowpass - @highpass)) >> 20
|
|
98
|
+
@output = (@lowpass - @highpass) >> FRACTION
|
|
99
|
+
@lowpass += delta_lowpass
|
|
100
|
+
@highpass += delta_highpass
|
|
101
|
+
@output
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
attr_reader :model, :cutoff, :routing, :mode, :volume
|
|
106
|
+
|
|
107
|
+
def initialize(model: :mos6581)
|
|
108
|
+
@model = model
|
|
109
|
+
@w0_table = W0.fetch(model)
|
|
110
|
+
@mixer_dc = MIXER_DC.fetch(model)
|
|
111
|
+
@external = External.new
|
|
112
|
+
reset
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def reset
|
|
116
|
+
@cutoff = 0x000
|
|
117
|
+
@routing = 0x0
|
|
118
|
+
@mode = 0x0
|
|
119
|
+
@volume = 0x0
|
|
120
|
+
@voice3_off = false
|
|
121
|
+
@w0 = @w0_table[0]
|
|
122
|
+
@resonance = RESONANCE[0]
|
|
123
|
+
@lowpass = @bandpass = @highpass = 0
|
|
124
|
+
@input = @unfiltered = 0
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def write(reg, value)
|
|
128
|
+
case reg
|
|
129
|
+
when 0x15 then self.cutoff = (@cutoff & 0x7f8) | (value & 0x07)
|
|
130
|
+
when 0x16 then self.cutoff = (value << 3) | (@cutoff & 0x007)
|
|
131
|
+
when 0x17 then write_resonance(value)
|
|
132
|
+
when 0x18 then write_mode_volume(value)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def cutoff=(value)
|
|
137
|
+
@cutoff = value & 0x7ff
|
|
138
|
+
@w0 = @w0_table[@cutoff]
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def voice3_off? = @voice3_off
|
|
142
|
+
|
|
143
|
+
def lowpass = @lowpass >> FRACTION
|
|
144
|
+
def bandpass = @bandpass >> FRACTION
|
|
145
|
+
def highpass = @highpass >> FRACTION
|
|
146
|
+
|
|
147
|
+
# Integrates `cycles` cycles in one step, from the voices' output at
|
|
148
|
+
# the end of them.
|
|
149
|
+
def cycle!(voices, cycles = 1)
|
|
150
|
+
route(voices)
|
|
151
|
+
w0 = @w0 * cycles
|
|
152
|
+
w0 = W0_STEP_MAX if w0 > W0_STEP_MAX
|
|
153
|
+
@bandpass -= (w0 * @highpass) >> 20
|
|
154
|
+
@lowpass -= (w0 * @bandpass) >> 20
|
|
155
|
+
@highpass = ((@bandpass * @resonance) >> 10) - @lowpass - (@input << FRACTION)
|
|
156
|
+
@external.cycle!(fine_mix, cycles)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# The SID's own audio pin, before the board's RC network.
|
|
160
|
+
def mix = fine_mix >> FRACTION
|
|
161
|
+
|
|
162
|
+
def output = @external.output
|
|
163
|
+
|
|
164
|
+
private
|
|
165
|
+
|
|
166
|
+
def write_resonance(value)
|
|
167
|
+
@routing = value & 0x0f
|
|
168
|
+
@resonance = RESONANCE[(value >> 4) & 0x0f]
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def write_mode_volume(value)
|
|
172
|
+
@volume = value & 0x0f
|
|
173
|
+
@mode = (value >> 4) & 0x07
|
|
174
|
+
@voice3_off = value.anybits?(0x80)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Unrolled over the three voices, like SID#clock!. Voice 3 is silenced
|
|
178
|
+
# by MODE/VOL bit 7 only while it bypasses the filter.
|
|
179
|
+
def route(voices)
|
|
180
|
+
voice1, voice2, voice3 = voices
|
|
181
|
+
@input = 0
|
|
182
|
+
@unfiltered = 0
|
|
183
|
+
route_voice(voice1.output >> 7, 0x1)
|
|
184
|
+
route_voice(voice2.output >> 7, 0x2)
|
|
185
|
+
if @routing.anybits?(0x4)
|
|
186
|
+
@input += voice3.output >> 7
|
|
187
|
+
elsif !@voice3_off
|
|
188
|
+
@unfiltered += voice3.output >> 7
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def route_voice(output, bit)
|
|
193
|
+
if @routing.anybits?(bit)
|
|
194
|
+
@input += output
|
|
195
|
+
else
|
|
196
|
+
@unfiltered += output
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# The mix with the integrators' fractional bits kept, so the RC
|
|
201
|
+
# network isn't fed the filter's output floored to whole units.
|
|
202
|
+
def fine_mix = (((@unfiltered + @mixer_dc) << FRACTION) + filtered) * @volume
|
|
203
|
+
|
|
204
|
+
def filtered
|
|
205
|
+
value = 0
|
|
206
|
+
value += @lowpass if @mode.anybits?(0x1)
|
|
207
|
+
value += @bandpass if @mode.anybits?(0x2)
|
|
208
|
+
value += @highpass if @mode.anybits?(0x4)
|
|
209
|
+
value
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class SID
|
|
5
|
+
# One of the three voices: a waveform generator amplitude-modulated by
|
|
6
|
+
# its envelope.
|
|
7
|
+
#
|
|
8
|
+
# The 6581 waveform DAC swings around a midpoint well above ground, so
|
|
9
|
+
# even a silent voice carries a large DC offset into the mixer. The
|
|
10
|
+
# 8580's is centred and carries none.
|
|
11
|
+
class Voice
|
|
12
|
+
WAVE_ZERO = { mos6581: 0x380, mos8580: 0x800 }.freeze
|
|
13
|
+
DC_OFFSET = { mos6581: 0x800 * 0xff, mos8580: 0 }.freeze
|
|
14
|
+
|
|
15
|
+
attr_reader :waveform, :envelope
|
|
16
|
+
|
|
17
|
+
# Each voice hard-syncs and ring-modulates against the previous one,
|
|
18
|
+
# wrapping from the first back round to the last.
|
|
19
|
+
def self.linked(count, model:)
|
|
20
|
+
voices = Array.new(count) { new(model:) }
|
|
21
|
+
waveforms = voices.map(&:waveform)
|
|
22
|
+
waveforms.each_with_index do |waveform, i|
|
|
23
|
+
waveform.sync_source = waveforms[i - 1]
|
|
24
|
+
waveform.sync_dest = waveforms[(i + 1) % count]
|
|
25
|
+
end
|
|
26
|
+
voices
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def initialize(model: :mos6581)
|
|
30
|
+
@waveform = Waveform.new(model:)
|
|
31
|
+
@envelope = Envelope.new
|
|
32
|
+
@wave_zero = WAVE_ZERO.fetch(model)
|
|
33
|
+
@dc_offset = DC_OFFSET.fetch(model)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def reset!
|
|
37
|
+
@waveform.reset!
|
|
38
|
+
@envelope = Envelope.new
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def control=(value)
|
|
42
|
+
@waveform.control = value
|
|
43
|
+
@envelope.control = value
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def cycle!
|
|
47
|
+
@envelope.cycle!
|
|
48
|
+
@waveform.cycle!
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def fast_forward(cycles)
|
|
52
|
+
@envelope.fast_forward(cycles)
|
|
53
|
+
@waveform.fast_forward(cycles)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def output
|
|
57
|
+
((@waveform.output - @wave_zero) * @envelope.output) + @dc_offset
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|