badline 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +49 -0
- data/CLAUDE.md +6 -0
- data/README.md +24 -6
- data/doc/pinned-behaviour.md +18 -6
- data/exe/badline +5 -2
- data/lib/badline/address_bus.rb +6 -4
- data/lib/badline/addressable.rb +1 -1
- data/lib/badline/audio/bare_player.rb +20 -3
- data/lib/badline/audio/machine_player.rb +9 -6
- data/lib/badline/audio/renderer.rb +3 -3
- data/lib/badline/audio/stream.rb +117 -0
- data/lib/badline/audio.rb +1 -0
- data/lib/badline/cartridge/easy_flash.rb +6 -2
- data/lib/badline/cartridge/epyx_fastload.rb +1 -1
- data/lib/badline/cartridge/mach5.rb +1 -1
- data/lib/badline/cartridge/magic_desk.rb +5 -1
- data/lib/badline/cartridge/ocean.rb +5 -2
- data/lib/badline/cartridge/rex_utility.rb +1 -1
- data/lib/badline/cartridge/rgcd.rb +1 -1
- data/lib/badline/cartridge/simons_basic.rb +2 -2
- data/lib/badline/cartridge/westermann.rb +2 -2
- data/lib/badline/cartridge.rb +8 -0
- data/lib/badline/checkpoint.rb +98 -0
- data/lib/badline/cia.rb +4 -2
- data/lib/badline/color_memory.rb +2 -2
- data/lib/badline/computer.rb +11 -6
- data/lib/badline/cpu.rb +2 -2
- data/lib/badline/gui/application.rb +41 -9
- data/lib/badline/kernal_trap/channel.rb +99 -7
- data/lib/badline/kernal_trap/drive/block_commands.rb +79 -0
- data/lib/badline/kernal_trap/drive/channels.rb +53 -0
- data/lib/badline/kernal_trap/drive/memory.rb +109 -0
- data/lib/badline/kernal_trap/drive/parameters.rb +28 -0
- data/lib/badline/kernal_trap/drive.rb +88 -110
- data/lib/badline/kernal_trap/file.rb +0 -5
- data/lib/badline/kernal_trap/load.rb +6 -0
- data/lib/badline/kernal_trap/save.rb +5 -4
- data/lib/badline/media.rb +2 -2
- data/lib/badline/memory.rb +2 -2
- data/lib/badline/rom.rb +14 -5
- data/lib/badline/sid/catch_up.rb +110 -0
- data/lib/badline/sid/decimator.rb +6 -1
- data/lib/badline/sid/filter.rb +5 -5
- data/lib/badline/sid/waveform/fast_forward.rb +10 -10
- data/lib/badline/sid/waveform.rb +7 -7
- data/lib/badline/sid.rb +16 -92
- data/lib/badline/storage/disk_image.rb +22 -0
- data/lib/badline/storage/host_directory.rb +15 -3
- data/lib/badline/storage/sid_file.rb +36 -2
- data/lib/badline/storage.rb +3 -1
- data/lib/badline/time_of_day.rb +7 -0
- data/lib/badline/version.rb +1 -1
- data/lib/badline/vic/graphics_shifter.rb +8 -2
- data/lib/badline/vic/registers.rb +16 -27
- data/lib/badline/vic.rb +5 -2
- data/lib/badline.rb +1 -0
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9348bef9831893e0aefa4d6965e1ea99a7252fb9f8f71e311f6b93b849ee1faa
|
|
4
|
+
data.tar.gz: '01029a95df00b30c92120e7f91c2e6dc9af247a927c41e64296a6285abdb9267'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8944a5873ed411c9ab156a5cedf537f8d7db1b0a06625c567a24b2b33f05c8ac6b20b45edbbaf7049b91a72da7bc1a5fc35b0c3f24e7029fda86a671315bfef5
|
|
7
|
+
data.tar.gz: bb10fd47802aadab93ed86916ce8de6a2d17e4163ecf723a0a6d4b45085eefadd2905e2eabbfbba39530f7d0e7d3be18f0e059a17bc6c71fb69aa592ea7204a3
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.0](https://github.com/elektronaut/badline/compare/v0.2.1...v0.3.0) (2026-09-24)
|
|
4
|
+
|
|
5
|
+
Sound is the headline: `badline --sound` plays the SID in the emulator window and paces emulation to the audio device. It is off by default, because the emulator still runs below real time on CRuby and the sound stutters there. This release also brings the virtual drive closer to a real 1541 (buffer channels in drive RAM, DOS command parsing, the BAM in buffer 4, `LOAD"*"`), cartridge banking fixes, and `Badline::Checkpoint` with `bin/machine_diff` for checking that a change leaves emulation alone.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* add Badline::Checkpoint, a per-component digest of machine state ([183e164](https://github.com/elektronaut/badline/commit/183e16438b4b1bfae5c16b10f29f2b08d0a6b297))
|
|
12
|
+
* add bin/machine_diff, a differential run harness ([58f695a](https://github.com/elektronaut/badline/commit/58f695a32096fe61d7395fcec9a76d8d84b5e440))
|
|
13
|
+
* load ROMs from a configurable Badline.rom_path ([4fa12c8](https://github.com/elektronaut/badline/commit/4fa12c8e55004eebe0b1a14e80f03713306e3acf))
|
|
14
|
+
* pace the window to the audio device while sound plays ([031b001](https://github.com/elektronaut/badline/commit/031b00196ee9a881740047c726231ffc0400d03d))
|
|
15
|
+
* play the SID in the emulator window with --sound ([4cbd211](https://github.com/elektronaut/badline/commit/4cbd211ca986834e3d8fcbafdabc5586c87ec950))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* answer a buffer channel's first read with its buffer number ([851e0d2](https://github.com/elektronaut/badline/commit/851e0d2bb71db51cc36607da90b4f7891b32bced))
|
|
21
|
+
* bank BASIC out for PSID routines whose image reaches under it ([9c5bf57](https://github.com/elektronaut/badline/commit/9c5bf57646ca8de8687679accdca26ac4dd68931))
|
|
22
|
+
* bank sparse and odd-sized cartridge images like the hardware ([1560f63](https://github.com/elektronaut/badline/commit/1560f63678a254bfa53e8915c35851dab00dc27b))
|
|
23
|
+
* drop the carriage return that ends a file name, as the 1541 does ([3ca3d51](https://github.com/elektronaut/badline/commit/3ca3d518808cf87ab0e14a3b312e972883fc4699))
|
|
24
|
+
* fail a block write at the block it names ([4b9664c](https://github.com/elektronaut/badline/commit/4b9664ca02bd0026334ffee01d4f6a5cfc354f73))
|
|
25
|
+
* fail an append open on a write-protected disk ([b5c7baf](https://github.com/elektronaut/badline/commit/b5c7bafed7f189c3e7a0074d27f0b76e8ffb3b56))
|
|
26
|
+
* fail an unreadable host file with a READ ERROR ([8b3d8f4](https://github.com/elektronaut/badline/commit/8b3d8f4eeae28a4ef61ef72e76dcfc02cefb738f))
|
|
27
|
+
* fill a buffer channel with the bytes written to it ([36c3660](https://github.com/elektronaut/badline/commit/36c3660e1c170ea058b80d3a0b10ec019aa84058))
|
|
28
|
+
* give single-bank mappers erased ROM when the image has no chip ([07e5bf3](https://github.com/elektronaut/badline/commit/07e5bf39b26e022f814c99b0d9226cecf3114400))
|
|
29
|
+
* ignore whatever follows a star in a DOS file name ([b2f8831](https://github.com/elektronaut/badline/commit/b2f8831b193e496d19c03388fe0613b9f9c9abb3))
|
|
30
|
+
* keep the drive's buffer channels in its RAM ([9efefe4](https://github.com/elektronaut/badline/commit/9efefe44520067db27496ea2af85468228c6579f))
|
|
31
|
+
* keep the drive's RAM when the disk changes ([2c07d0e](https://github.com/elektronaut/badline/commit/2c07d0eb73120dfe1eda69a5fc8ad43a31d83ef2))
|
|
32
|
+
* leave secondary address $60 in $B9 after a trapped LOAD ([d7366a8](https://github.com/elektronaut/badline/commit/d7366a8aaf0fe13d239035178751fc8f783d7a30))
|
|
33
|
+
* mask Ocean and Magic Desk bank selection to the ROM size ([e6d36a0](https://github.com/elektronaut/badline/commit/e6d36a004583d18e225e4bb5cfccdd69380d523e))
|
|
34
|
+
* play NTSC PSID tunes on an NTSC clock on the bare rig ([006e04b](https://github.com/elektronaut/badline/commit/006e04b03985ef5d2fe57bab6c862bbe97d65f50))
|
|
35
|
+
* read DOS command parameters the way the 1541 does ([db5b4fc](https://github.com/elektronaut/badline/commit/db5b4fc06b82f5423453ee988d30f407a8565ed1))
|
|
36
|
+
* read open bus from a cartridge's empty ROM socket ([fee013d](https://github.com/elektronaut/badline/commit/fee013dcff2a062b75713b268422171e79a50375))
|
|
37
|
+
* read the BAM into buffer 4 when the drive initializes a disk ([c1320a5](https://github.com/elektronaut/badline/commit/c1320a5d2f7d9fcd24e800fcacaf07ba8b07b889))
|
|
38
|
+
* reopen the last loaded file on a LOAD of "*" ([64b3991](https://github.com/elektronaut/badline/commit/64b3991b9e1c91190e9b47e44238b93e0ea74850))
|
|
39
|
+
* report host SAVE and read failures on the drive's error channel ([039b6da](https://github.com/elektronaut/badline/commit/039b6da1779397e56ebf6cc25effe656ca85c1f6))
|
|
40
|
+
* reset the drive with the machine, as the serial bus's RESET line does ([4044db6](https://github.com/elektronaut/badline/commit/4044db6ae1bd241381c2d7651cf2a44b1ae8a61a))
|
|
41
|
+
* RUN BASIC RSID tunes instead of calling their first byte ([d9a51c2](https://github.com/elektronaut/badline/commit/d9a51c2eebd5092e97da576c438a647079458e78))
|
|
42
|
+
* show a BMM falling out of multicolour bitmap or ECM+BMM at pixel 5 ([988a0c6](https://github.com/elektronaut/badline/commit/988a0c60e40c720e8e1cd7acf0506107902a1ccb))
|
|
43
|
+
* **vic:** power on with every register zero, as VICE does ([d5bf757](https://github.com/elektronaut/badline/commit/d5bf75756d2edfdf3b6bf40214615262b79958a6))
|
|
44
|
+
* wrap a buffer channel's reads back to its second byte, as the DOS does ([9c9f0ed](https://github.com/elektronaut/badline/commit/9c9f0edb7b92b580bb2cb71668e5fa07fa8e3bd9))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
### Performance Improvements
|
|
48
|
+
|
|
49
|
+
* cut the SID's per-span catch-up overhead ([f50ede8](https://github.com/elektronaut/badline/commit/f50ede8472389c28c1dde70909794ef69ae77238))
|
|
50
|
+
* type the hot core for Spinel with RBS seeds ([90d31b0](https://github.com/elektronaut/badline/commit/90d31b02331982e216284f2ac533f5a6aa934bf9))
|
|
51
|
+
|
|
3
52
|
## [0.2.1](https://github.com/elektronaut/badline/compare/v0.2.0...v0.2.1) (2026-09-23)
|
|
4
53
|
|
|
5
54
|
|
data/CLAUDE.md
CHANGED
|
@@ -52,6 +52,12 @@ timer test takes about 25 minutes.
|
|
|
52
52
|
where that time goes. `test/baselines/README.md`
|
|
53
53
|
documents the baseline format, the suites and how rows are compared.
|
|
54
54
|
|
|
55
|
+
`bin/machine_diff <scenario|media> --against <rev>` checks that a change
|
|
56
|
+
leaves emulation alone. It runs the scenario on this tree and on `<rev>`'s
|
|
57
|
+
`lib/` side by side, digests the CPU, RAM, VIC, CIAs and SID every million
|
|
58
|
+
cycles, and names the first component that differs. Use it for refactors
|
|
59
|
+
and speed work.
|
|
60
|
+
|
|
55
61
|
## Driving the emulator headlessly
|
|
56
62
|
|
|
57
63
|
`exe/badline <media>` opens the SDL window, which is no use for checking
|
data/README.md
CHANGED
|
@@ -10,9 +10,9 @@ are modelled at the cycle level.
|
|
|
10
10
|
|
|
11
11
|
It runs programs, disk and tape images, cartridges and SID tunes, and
|
|
12
12
|
the SDL2 front end supports the keyboard, joysticks, game controllers,
|
|
13
|
-
paddles and a 1351 mouse.
|
|
14
|
-
|
|
15
|
-
details.
|
|
13
|
+
paddles and a 1351 mouse. Emulation runs slower than a real C64, so
|
|
14
|
+
live sound, which is off by default, stutters. See
|
|
15
|
+
[What's emulated](#whats-emulated) for the details.
|
|
16
16
|
|
|
17
17
|
## Requirements
|
|
18
18
|
|
|
@@ -54,6 +54,21 @@ you can type the `LOAD` yourself. `--song N` picks a subtune of a
|
|
|
54
54
|
without YJIT, which is otherwise switched on at startup.
|
|
55
55
|
`badline --help` lists the options.
|
|
56
56
|
|
|
57
|
+
The KERNAL, BASIC and character ROMs come with the gem. To run other
|
|
58
|
+
images, such as a patched KERNAL, point `BADLINE_ROM_PATH` at a
|
|
59
|
+
directory that holds `kernal.rom`, `basic.rom` and `character.rom`,
|
|
60
|
+
plus `eapi/eapi-am29f040-14` if you attach EasyFlash cartridges. From
|
|
61
|
+
Ruby, `Badline.rom_path = dir` does the same before a
|
|
62
|
+
`Badline::Computer` is built, and `nil` restores the bundled set.
|
|
63
|
+
|
|
64
|
+
`--sound` plays the SID through the host's audio device, and `F10`
|
|
65
|
+
mutes and unmutes it. Sound is off by default. While it plays, the
|
|
66
|
+
audio device sets the pace instead of the display, so the machine never
|
|
67
|
+
runs ahead of the sound or drifts behind it. The whole machine runs
|
|
68
|
+
below real time, though, so the sound stutters: it plays in bursts with
|
|
69
|
+
silent gaps between them, at the right pitch, and never slows the
|
|
70
|
+
emulation down.
|
|
71
|
+
|
|
57
72
|
## Media
|
|
58
73
|
|
|
59
74
|
| Format | Handling |
|
|
@@ -160,6 +175,9 @@ Control port 1's fire line is also the VIC-II's light pen input, so
|
|
|
160
175
|
joystick 1's fire button and the 1351's left button in port 1 latch the
|
|
161
176
|
light pen registers.
|
|
162
177
|
|
|
178
|
+
With `--sound`, `F10` mutes and unmutes the sound, and the window
|
|
179
|
+
title shows `[MUTED]` while it's off.
|
|
180
|
+
|
|
163
181
|
## What's emulated
|
|
164
182
|
|
|
165
183
|
- **6510**: every opcode, documented and undocumented, with per-cycle
|
|
@@ -202,9 +220,9 @@ light pen registers.
|
|
|
202
220
|
|
|
203
221
|
Known gaps:
|
|
204
222
|
|
|
205
|
-
-
|
|
206
|
-
|
|
207
|
-
|
|
223
|
+
- Live audio in the emulator window stutters, because the whole machine
|
|
224
|
+
runs below real time. `badline-sid` plays PSID tunes smoothly on
|
|
225
|
+
their own.
|
|
208
226
|
- No drive emulation, so fast loaders and anything else that runs code
|
|
209
227
|
on the drive won't work (see [Media](#media)). Disk images are
|
|
210
228
|
read-only.
|
data/doc/pinned-behaviour.md
CHANGED
|
@@ -650,12 +650,24 @@ VICE x64sc's `vicii_fetch_graphics` and `draw_graphics8` for the 6569.
|
|
|
650
650
|
- Pinned by `modesplit` (48 → 1222 px painting whole groups, 428 with
|
|
651
651
|
MCM read at pixel 4), the `videomode` rows and `vicii_reg_timing`
|
|
652
652
|
(pass → 274).
|
|
653
|
-
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
653
|
+
- A falling BMM shows at pixel 5 instead of 6 unless it leaves hi-res
|
|
654
|
+
bitmap: out of multicolour bitmap, and out of the invalid ECM+BMM
|
|
655
|
+
modes, whether ECM falls with it or stays.
|
|
656
|
+
- Pinned by `modesplit` (48 px → pass), whose first section drops BMM
|
|
657
|
+
out of ECM+BMM, by the E+B row of `vicii_reg_timing-a5` and `-ff`
|
|
658
|
+
(7 px each → pass), which drops ECM and BMM together, and by
|
|
659
|
+
`videomode-y` (1 px → pass), which drops it out of multicolour
|
|
660
|
+
bitmap. The hi-res exception is pinned by `videomode2` and the BMM
|
|
661
|
+
row of `vicii_reg_timing` (pass → 1 and 7 px without it).
|
|
662
|
+
- Two references disagree, and both are marked unsafe in the
|
|
663
|
+
testlist: `videomode-v` drops BMM out of multicolour bitmap and
|
|
664
|
+
`videomode-w` out of ECM+BMM, and both want pixel 6 (+1 px each).
|
|
665
|
+
The readme says these delays vary with the chip and its
|
|
666
|
+
temperature.
|
|
667
|
+
- Spec guard: *takes a BMM falling out of hi-res bitmap at pixel 6*,
|
|
668
|
+
*out of ECM+BMM at pixel 5* and *out of multicolour bitmap at
|
|
669
|
+
pixel 5* in
|
|
670
|
+
[`vic/graphics_shifter_spec.rb`](../spec/badline/vic/graphics_shifter_spec.rb).
|
|
659
671
|
- An MCM that falls out of the invalid ECM+MCM text mode is a pixel
|
|
660
672
|
later on both counts. The lookup stays black through pixel 4 and
|
|
661
673
|
changes at pixel 5, and the pairs are read through pixel 7, with
|
data/exe/badline
CHANGED
|
@@ -6,7 +6,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
|
6
6
|
|
|
7
7
|
require "optparse"
|
|
8
8
|
|
|
9
|
-
options = { autostart: true, jit: true }
|
|
9
|
+
options = { autostart: true, jit: true, sound: false }
|
|
10
10
|
|
|
11
11
|
parser = OptionParser.new do |opts|
|
|
12
12
|
opts.banner = <<~BANNER
|
|
@@ -32,6 +32,8 @@ parser = OptionParser.new do |opts|
|
|
|
32
32
|
options[:sid_model] = model
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
opts.on("--sound", "Play the SID through the host's audio device (F10 mutes)") { options[:sound] = true }
|
|
36
|
+
|
|
35
37
|
opts.on("--disable-jit", "Run without enabling YJIT") do
|
|
36
38
|
options[:jit] = false
|
|
37
39
|
end
|
|
@@ -64,7 +66,8 @@ require "badline/gui"
|
|
|
64
66
|
|
|
65
67
|
begin
|
|
66
68
|
application = Badline::GUI::Application.new(media_path:, autostart: options[:autostart],
|
|
67
|
-
song: options[:song], sid_model: options[:sid_model]
|
|
69
|
+
song: options[:song], sid_model: options[:sid_model],
|
|
70
|
+
sound: options[:sound])
|
|
68
71
|
rescue Badline::Storage::SIDFile::FormatError, Badline::Storage::T64::FormatError,
|
|
69
72
|
Badline::Storage::TAP::FormatError, Badline::Storage::CRTFile::FormatError,
|
|
70
73
|
Badline::Cartridge::UnsupportedTypeError => e
|
data/lib/badline/address_bus.rb
CHANGED
|
@@ -180,10 +180,12 @@ module Badline
|
|
|
180
180
|
end
|
|
181
181
|
end
|
|
182
182
|
|
|
183
|
+
# EXROM and GAME select the cartridge whether or not it has a chip
|
|
184
|
+
# there, and an empty socket leaves the bus floating.
|
|
183
185
|
def map_rom_overlays
|
|
184
|
-
@read_pages.fill(@cartridge.roml, 0x80, 0x20) if roml?
|
|
186
|
+
@read_pages.fill(@cartridge.roml || @open_bus, 0x80, 0x20) if roml?
|
|
185
187
|
if romh?
|
|
186
|
-
@read_pages.fill(@cartridge.romh, 0xa0, 0x20)
|
|
188
|
+
@read_pages.fill(@cartridge.romh || @open_bus, 0xa0, 0x20)
|
|
187
189
|
elsif basic?
|
|
188
190
|
@read_pages.fill(basic_rom, 0xa0, 0x20)
|
|
189
191
|
end
|
|
@@ -254,11 +256,11 @@ module Badline
|
|
|
254
256
|
end
|
|
255
257
|
|
|
256
258
|
def roml?
|
|
257
|
-
@cartridge&.
|
|
259
|
+
@cartridge&.exrom&.zero? && io_port.kernal? && io_port.basic?
|
|
258
260
|
end
|
|
259
261
|
|
|
260
262
|
def romh?
|
|
261
|
-
@cartridge&.
|
|
263
|
+
@cartridge&.exrom&.zero? && @cartridge.game.zero? && io_port.kernal?
|
|
262
264
|
end
|
|
263
265
|
|
|
264
266
|
# In 16K mode LORAM alone leaves $d000 as RAM, where it still maps I/O.
|
data/lib/badline/addressable.rb
CHANGED
|
@@ -8,6 +8,10 @@ module Badline
|
|
|
8
8
|
# which is what makes this path about three times faster than the whole
|
|
9
9
|
# machine.
|
|
10
10
|
#
|
|
11
|
+
# Nothing on the rig ties it to PAL, so an NTSC tune gets an NTSC
|
|
12
|
+
# machine: 60 Hz frames, the NTSC KERNAL's timer and `$02a6`, and a
|
|
13
|
+
# clock of 1022727 Hz for the SID's output.
|
|
14
|
+
#
|
|
11
15
|
# Calls go through a six-byte stub whose JSR operand the dispatch trap
|
|
12
16
|
# rewrites:
|
|
13
17
|
#
|
|
@@ -23,6 +27,11 @@ module Badline
|
|
|
23
27
|
# 312 raster lines of 63 cycles.
|
|
24
28
|
FRAME_CYCLES = 19_656
|
|
25
29
|
|
|
30
|
+
# 263 raster lines of 65 cycles.
|
|
31
|
+
NTSC_FRAME_CYCLES = 17_095
|
|
32
|
+
|
|
33
|
+
NTSC_CLOCK_HZ = 1_022_727
|
|
34
|
+
|
|
26
35
|
# A tune's init routine is free to unpack itself, but not forever.
|
|
27
36
|
INIT_LIMIT = 10_000_000
|
|
28
37
|
|
|
@@ -30,6 +39,11 @@ module Badline
|
|
|
30
39
|
# second.
|
|
31
40
|
KERNAL_TIMER = 0x4025
|
|
32
41
|
|
|
42
|
+
NTSC_KERNAL_TIMER = 0x4295
|
|
43
|
+
|
|
44
|
+
# The KERNAL's PAL/NTSC flag, 1 for PAL.
|
|
45
|
+
VIDEO_STANDARD = 0x02a6
|
|
46
|
+
|
|
33
47
|
attr_reader :sid
|
|
34
48
|
|
|
35
49
|
def initialize(tune, song: nil, sid_model: tune.sid_model)
|
|
@@ -39,7 +53,8 @@ module Badline
|
|
|
39
53
|
# The CPU port as the KERNAL leaves it, which a PSID tune expects.
|
|
40
54
|
@bus.poke(0x00, 0x2f)
|
|
41
55
|
@bus.poke(0x01, 0x37)
|
|
42
|
-
@bus.
|
|
56
|
+
@bus.poke(VIDEO_STANDARD, tune.ntsc? ? 0 : 1)
|
|
57
|
+
@bus.cia1.timer_a_latch = tune.ntsc? ? NTSC_KERNAL_TIMER : KERNAL_TIMER
|
|
43
58
|
@cpu = CPU.new(@bus)
|
|
44
59
|
@sid = @bus.sid
|
|
45
60
|
@idle = false
|
|
@@ -67,12 +82,14 @@ module Badline
|
|
|
67
82
|
cycles
|
|
68
83
|
end
|
|
69
84
|
|
|
85
|
+
def clock_hz = @tune.ntsc? ? NTSC_CLOCK_HZ : TimeOfDay::CLOCK_HZ
|
|
86
|
+
|
|
70
87
|
private
|
|
71
88
|
|
|
72
|
-
# A
|
|
89
|
+
# A video frame, or for a CIA-timed song one period of CIA 1 timer A,
|
|
73
90
|
# which init and play are both free to reprogram.
|
|
74
91
|
def period
|
|
75
|
-
return FRAME_CYCLES unless @tune.cia_timed?(@song + 1)
|
|
92
|
+
return (@tune.ntsc? ? NTSC_FRAME_CYCLES : FRAME_CYCLES) unless @tune.cia_timed?(@song + 1)
|
|
76
93
|
|
|
77
94
|
@bus.cia1.timer_a_latch + 1
|
|
78
95
|
end
|
|
@@ -4,8 +4,8 @@ module Badline
|
|
|
4
4
|
module Audio
|
|
5
5
|
# Runs a tune on the whole machine, for the RSID tunes that install their
|
|
6
6
|
# own interrupts and expect a booted C64 underneath. The tune goes in
|
|
7
|
-
#
|
|
8
|
-
# when the CPU
|
|
7
|
+
# the way `Media` puts it in: BASIC SYSes the driver stub, or RUNs a
|
|
8
|
+
# BASIC tune. Rendering starts when the CPU gets there.
|
|
9
9
|
class MachinePlayer
|
|
10
10
|
FRAME_CYCLES = BarePlayer::FRAME_CYCLES
|
|
11
11
|
|
|
@@ -22,12 +22,15 @@ module Badline
|
|
|
22
22
|
|
|
23
23
|
def start
|
|
24
24
|
@computer.on_init { inject }
|
|
25
|
-
@computer.cpu.install_trap(@tune.
|
|
25
|
+
@computer.cpu.install_trap(@tune.entry_address) { begin_playing }
|
|
26
26
|
@computer.cycle! until @started || @computer.cycles > START_LIMIT
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def sid = @computer.sid
|
|
30
30
|
|
|
31
|
+
# The machine is PAL whatever the tune asks for.
|
|
32
|
+
def clock_hz = TimeOfDay::CLOCK_HZ
|
|
33
|
+
|
|
31
34
|
# Advances one PAL frame, or `budget` cycles if that is shorter, then
|
|
32
35
|
# yields whatever the SID recorded over it. Returns the cycles advanced.
|
|
33
36
|
def frame(budget, &)
|
|
@@ -41,13 +44,13 @@ module Badline
|
|
|
41
44
|
|
|
42
45
|
def inject
|
|
43
46
|
@computer.ram.write(@tune.load_address, @tune.data)
|
|
44
|
-
@
|
|
45
|
-
@computer.type_text(
|
|
47
|
+
@tune.boot_memory(song: @song).each { |address, bytes| @computer.ram.write(address, bytes) }
|
|
48
|
+
@computer.type_text(@tune.boot_command)
|
|
46
49
|
@injected = true
|
|
47
50
|
end
|
|
48
51
|
|
|
49
52
|
def begin_playing
|
|
50
|
-
return
|
|
53
|
+
return if @started || !@injected
|
|
51
54
|
|
|
52
55
|
@computer.sid.synthesize!
|
|
53
56
|
@started = true
|
|
@@ -60,7 +60,7 @@ module Badline
|
|
|
60
60
|
# The cycles it takes to close exactly `total_samples` windows; the SID
|
|
61
61
|
# records floor(cycles * rate / clock) of them.
|
|
62
62
|
def total_cycles
|
|
63
|
-
((total_samples *
|
|
63
|
+
((total_samples * player.clock_hz) + @rate - 1) / @rate
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
def container_for(path)
|
|
@@ -71,13 +71,13 @@ module Badline
|
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
def each_frame
|
|
74
|
-
player.sid.record(rate: @rate, filter_chunk: @filter_chunk)
|
|
74
|
+
player.sid.record(rate: @rate, filter_chunk: @filter_chunk, clock_hz: player.clock_hz)
|
|
75
75
|
total = total_cycles
|
|
76
76
|
remaining = total
|
|
77
77
|
while remaining.positive?
|
|
78
78
|
samples = []
|
|
79
79
|
remaining -= player.frame(remaining) { |sample| samples << sample }
|
|
80
|
-
yield samples, (total - remaining).fdiv(
|
|
80
|
+
yield samples, (total - remaining).fdiv(player.clock_hz)
|
|
81
81
|
end
|
|
82
82
|
end
|
|
83
83
|
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Audio
|
|
5
|
+
# Plays a running machine's SID through a sink, for the window: #feed
|
|
6
|
+
# queues whatever the SID recorded since the last call, once a frame,
|
|
7
|
+
# and #pace holds the machine to the device's clock.
|
|
8
|
+
#
|
|
9
|
+
# The device starts once the queue holds `ahead` seconds. Below real time
|
|
10
|
+
# the queue runs dry, and the device then stops until it holds `ahead`
|
|
11
|
+
# again, so the sound stutters with silent gaps rather than slowing
|
|
12
|
+
# down; `on_underrun` hears about the first time. A frame that would
|
|
13
|
+
# take the queue past `limit` seconds, as an unpaced machine above real
|
|
14
|
+
# time makes, is dropped whole.
|
|
15
|
+
class Stream
|
|
16
|
+
AHEAD = 0.08
|
|
17
|
+
LIMIT = 0.25
|
|
18
|
+
|
|
19
|
+
attr_reader :underruns, :dropped
|
|
20
|
+
|
|
21
|
+
# What #pace reads the time from and sleeps on: #now and #sleep.
|
|
22
|
+
attr_writer :clock
|
|
23
|
+
|
|
24
|
+
def initialize(sink, sid, ahead: AHEAD, limit: LIMIT, on_underrun: nil)
|
|
25
|
+
@sink = sink
|
|
26
|
+
@sid = sid
|
|
27
|
+
@ahead = ahead
|
|
28
|
+
@limit = limit
|
|
29
|
+
@on_underrun = on_underrun
|
|
30
|
+
@clock = Clock.new
|
|
31
|
+
@next_frame = nil
|
|
32
|
+
@started = false
|
|
33
|
+
@muted = false
|
|
34
|
+
@underruns = 0
|
|
35
|
+
@dropped = 0
|
|
36
|
+
sid.record(rate: sink.rate)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def rate = @sink.rate
|
|
40
|
+
|
|
41
|
+
def muted? = @muted
|
|
42
|
+
|
|
43
|
+
def playing? = @started
|
|
44
|
+
|
|
45
|
+
# Muting drops the queue; unmuting fills it to `ahead` again before
|
|
46
|
+
# the device restarts.
|
|
47
|
+
def toggle_mute
|
|
48
|
+
@muted = !@muted
|
|
49
|
+
return unless @muted
|
|
50
|
+
|
|
51
|
+
halt
|
|
52
|
+
@sink.clear
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def feed
|
|
56
|
+
samples = @sid.drain_samples
|
|
57
|
+
return if @muted || samples.empty?
|
|
58
|
+
|
|
59
|
+
queued = @sink.queued_seconds
|
|
60
|
+
return @dropped += samples.size if queued + samples.size.fdiv(rate) > @limit
|
|
61
|
+
|
|
62
|
+
underrun! if @started && queued.zero?
|
|
63
|
+
@sink.queue(samples)
|
|
64
|
+
start if @sink.queued_seconds >= @ahead
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Waits out the frame just run. While the device plays, that is until
|
|
68
|
+
# it has played the queue down to `ahead`, so the machine runs at the
|
|
69
|
+
# device's rate and the queue neither drifts dry nor grows. Muted or
|
|
70
|
+
# filling up, the clock paces frames `seconds` apart instead.
|
|
71
|
+
def pace(seconds)
|
|
72
|
+
return wait_for_device if playing?
|
|
73
|
+
|
|
74
|
+
now = @clock.now
|
|
75
|
+
@next_frame = now if @next_frame.nil? || @next_frame < now
|
|
76
|
+
@clock.sleep(@next_frame - now)
|
|
77
|
+
@next_frame += seconds
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def close = @sink.close
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def start
|
|
85
|
+
return if @started
|
|
86
|
+
|
|
87
|
+
@started = true
|
|
88
|
+
@sink.start
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def wait_for_device
|
|
92
|
+
@next_frame = nil
|
|
93
|
+
while (excess = @sink.queued_seconds - @ahead).positive?
|
|
94
|
+
@clock.sleep([excess, 0.001].max)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def halt
|
|
99
|
+
@started = false
|
|
100
|
+
@sink.pause
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def underrun!
|
|
104
|
+
@underruns += 1
|
|
105
|
+
@on_underrun&.call if @underruns == 1
|
|
106
|
+
halt
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# The host's monotonic clock.
|
|
110
|
+
class Clock
|
|
111
|
+
def now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
112
|
+
|
|
113
|
+
def sleep(seconds) = Kernel.sleep(seconds)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
data/lib/badline/audio.rb
CHANGED
|
@@ -9,6 +9,7 @@ require "badline/audio/machine_player"
|
|
|
9
9
|
require "badline/audio/renderer"
|
|
10
10
|
require "badline/audio/sdl_sink"
|
|
11
11
|
require "badline/audio/playback"
|
|
12
|
+
require "badline/audio/stream"
|
|
12
13
|
require "badline/audio/console"
|
|
13
14
|
require "badline/audio/jukebox"
|
|
14
15
|
require "badline/audio/cli"
|
|
@@ -18,7 +18,10 @@ module Badline
|
|
|
18
18
|
class EasyFlash < Cartridge
|
|
19
19
|
BANKS = 64
|
|
20
20
|
EAPI_OFFSET = 0x1800
|
|
21
|
-
|
|
21
|
+
|
|
22
|
+
def self.eapi
|
|
23
|
+
@eapi ||= ROM.read("eapi/eapi-am29f040-14").drop(2).freeze
|
|
24
|
+
end
|
|
22
25
|
|
|
23
26
|
attr_reader :low_flash, :high_flash
|
|
24
27
|
|
|
@@ -110,7 +113,8 @@ module Badline
|
|
|
110
113
|
def install_eapi(high)
|
|
111
114
|
return unless high[EAPI_OFFSET, 4] == "eapi".bytes
|
|
112
115
|
|
|
113
|
-
|
|
116
|
+
eapi = EasyFlash.eapi
|
|
117
|
+
high[EAPI_OFFSET, eapi.length] = eapi
|
|
114
118
|
end
|
|
115
119
|
|
|
116
120
|
def place(flash_data, offset, bytes)
|
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
4
|
class Cartridge
|
|
5
|
+
# Magic Desk: up to 128 8K banks. A write to I/O 1 selects the bank from
|
|
6
|
+
# bits 0-6, masked to the size of the ROM and at least four banks, and
|
|
7
|
+
# switches the ROM out with bit 7.
|
|
5
8
|
class MagicDesk < Cartridge
|
|
6
9
|
def poke(addr, value)
|
|
7
10
|
return if addr > 0xdeff
|
|
8
11
|
|
|
9
12
|
@exrom = value.anybits?(0x80) ? 1 : 0
|
|
10
|
-
@roml = @banks
|
|
13
|
+
@roml = bank(@banks, value & @bank_mask)
|
|
11
14
|
changed!
|
|
12
15
|
end
|
|
13
16
|
|
|
@@ -19,6 +22,7 @@ module Badline
|
|
|
19
22
|
|
|
20
23
|
def install_chips(chips)
|
|
21
24
|
@banks = banks_from(chips).first
|
|
25
|
+
@bank_mask = (bank_mask(@banks) | 0x03) & 0x7f
|
|
22
26
|
reset
|
|
23
27
|
end
|
|
24
28
|
end
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
module Badline
|
|
4
4
|
class Cartridge
|
|
5
|
+
# Ocean: up to 64 8K banks. A write to I/O 1 selects the bank from bits
|
|
6
|
+
# 0-5, masked to the size of the ROM.
|
|
5
7
|
class Ocean < Cartridge
|
|
6
8
|
def poke(addr, value)
|
|
7
9
|
return if addr > 0xdeff
|
|
8
10
|
|
|
9
|
-
select_bank(
|
|
11
|
+
select_bank(value & @bank_mask)
|
|
10
12
|
changed!
|
|
11
13
|
end
|
|
12
14
|
|
|
@@ -19,12 +21,13 @@ module Badline
|
|
|
19
21
|
|
|
20
22
|
# In 16K mode the selected bank shows through ROMH as well.
|
|
21
23
|
def select_bank(number)
|
|
22
|
-
@roml = @banks
|
|
24
|
+
@roml = bank(@banks, number)
|
|
23
25
|
@romh = @roml if game.zero?
|
|
24
26
|
end
|
|
25
27
|
|
|
26
28
|
def install_chips(chips)
|
|
27
29
|
@banks = banks_from(chips).first
|
|
30
|
+
@bank_mask = bank_mask(@banks) & 0x3f
|
|
28
31
|
reset
|
|
29
32
|
end
|
|
30
33
|
end
|