badline 0.7.0 → 0.9.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 +48 -0
- data/CLAUDE.md +3 -1
- data/README.md +104 -292
- data/doc/library.md +69 -0
- data/doc/media.md +78 -0
- data/doc/pinned-behaviour.md +53 -9
- data/doc/sid-player.md +136 -0
- data/doc/snapshots.md +102 -0
- data/lib/badline/address_bus/roms.rb +32 -0
- data/lib/badline/address_bus.rb +4 -4
- data/lib/badline/audio/bare_player.rb +24 -0
- data/lib/badline/audio/checkpoints.rb +42 -0
- data/lib/badline/audio/jukebox.rb +11 -4
- data/lib/badline/audio/machine_player.rb +16 -0
- data/lib/badline/audio/renderer.rb +80 -9
- data/lib/badline/audio/stereo.rb +3 -5
- data/lib/badline/audio.rb +1 -0
- data/lib/badline/cartridge/freezer.rb +2 -0
- data/lib/badline/cartridge.rb +3 -0
- data/lib/badline/computer/attachments.rb +5 -5
- data/lib/badline/computer.rb +12 -10
- data/lib/badline/core.rb +48 -0
- data/lib/badline/data_path.rb +37 -0
- data/lib/badline/datasette.rb +24 -9
- data/lib/badline/drive1541/disk.rb +3 -3
- data/lib/badline/drive1541/disk_state.rb +14 -5
- data/lib/badline/frontend/app.rb +41 -37
- data/lib/badline/frontend/boot.rb +16 -4
- data/lib/badline/frontend/buttons.rb +101 -7
- data/lib/badline/frontend/confirmation.rb +56 -0
- data/lib/badline/frontend/controls.rb +44 -32
- data/lib/badline/frontend/file_browser.rb +225 -0
- data/lib/badline/frontend/frame_report.rb +52 -0
- data/lib/badline/frontend/menu_dialogs.rb +160 -0
- data/lib/badline/frontend/menu_media.rb +157 -0
- data/lib/badline/frontend/menu_pages.rb +220 -0
- data/lib/badline/frontend/name_field.rb +88 -0
- data/lib/badline/frontend/painter.rb +10 -0
- data/lib/badline/frontend/pause_menu.rb +264 -0
- data/lib/badline/frontend/player_window.rb +3 -3
- data/lib/badline/frontend/snapshots.rb +146 -18
- data/lib/badline/frontend/sound.rb +11 -6
- data/lib/badline/frontend/timeline.rb +5 -1
- data/lib/badline/frontend.rb +8 -0
- data/lib/badline/kernal_trap/load.rb +10 -0
- data/lib/badline/kernal_trap/routine.rb +28 -0
- data/lib/badline/kernal_trap/serial.rb +19 -7
- data/lib/badline/media/disk_list.rb +75 -0
- data/lib/badline/media/disk_set.rb +118 -0
- data/lib/badline/media.rb +9 -3
- data/lib/badline/model.rb +62 -0
- data/lib/badline/options/event.rb +5 -3
- data/lib/badline/options/help.rb +16 -12
- data/lib/badline/options/table.rb +6 -2
- data/lib/badline/options/validation.rb +9 -0
- data/lib/badline/options.rb +10 -7
- data/lib/badline/region.rb +16 -4
- data/lib/badline/reu/ram.rb +27 -0
- data/lib/badline/reu/saved_state.rb +27 -0
- data/lib/badline/roms/README +10 -0
- data/lib/badline/roms/kernal-sx64.rom +0 -0
- data/lib/badline/sid/decimator.rb +13 -0
- data/lib/badline/sid/filter.rb +6 -3
- data/lib/badline/sid.rb +19 -0
- data/lib/badline/snapshot/fields.rb +1 -0
- data/lib/badline/snapshot/setup.rb +20 -8
- data/lib/badline/snapshot/state.rb +14 -7
- data/lib/badline/snapshot/vice/main_cpu.rb +1 -1
- data/lib/badline/snapshot/vice/peripherals.rb +12 -2
- data/lib/badline/snapshot/vice/restore.rb +3 -1
- data/lib/badline/snapshot/vice/reu1764.rb +55 -0
- data/lib/badline/snapshot/vice/vicii.rb +59 -13
- data/lib/badline/snapshot/vice/vicii_import.rb +14 -3
- data/lib/badline/snapshot/vice.rb +25 -23
- data/lib/badline/storage/t64.rb +7 -4
- data/lib/badline/storage/tap.rb +7 -5
- data/lib/badline/storage/unavailable.rb +1 -1
- data/lib/badline/storage.rb +5 -5
- data/lib/badline/version.rb +1 -1
- data/lib/badline/vic/sprite/timing.rb +5 -9
- data/lib/badline/vic/sprite.rb +4 -3
- data/lib/badline.rb +2 -38
- metadata +22 -1
data/doc/media.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Media
|
|
2
|
+
|
|
3
|
+
The [README](../README.md#media) lists the formats badline takes. This
|
|
4
|
+
page covers how device 8 and the true 1541 work, and how to attach media
|
|
5
|
+
from Ruby.
|
|
6
|
+
|
|
7
|
+
## Device 8 without a drive
|
|
8
|
+
|
|
9
|
+
Apart from a `.g64`, which only a true drive can read, there is no 1541
|
|
10
|
+
unless `--true-drive` asks for one. Device 8 works by trapping the
|
|
11
|
+
KERNAL's `LOAD` and `SAVE` routines and its serial bus primitives, so
|
|
12
|
+
files open by name through `OPEN` and `CHRIN` as well. `LOAD"$",8` lists
|
|
13
|
+
the directory of any medium mounted there, as a 1541 does.
|
|
14
|
+
|
|
15
|
+
The command channel answers `I`, `B-P` and `U1` block reads, which
|
|
16
|
+
covers loaders that read blocks directly. On a writable disk image it
|
|
17
|
+
also takes `SAVE` (with `@0:` to replace a file), files opened for
|
|
18
|
+
writing or appending, `S` to scratch, `U2` and `B-W` block writes, and
|
|
19
|
+
`B-A` and `B-F`. It doesn't rename, copy, format or validate disks.
|
|
20
|
+
Loaders that upload their own code to the drive with `M-W` and `M-E`,
|
|
21
|
+
and copy protection that reads raw GCR, won't work there, but do on a
|
|
22
|
+
true drive.
|
|
23
|
+
|
|
24
|
+
A directory mounted as device 8 serves the `.prg` and `.p00` files in it
|
|
25
|
+
and the contents of any `.t64`, and `SAVE` writes a new `.prg`.
|
|
26
|
+
|
|
27
|
+
## The true 1541
|
|
28
|
+
|
|
29
|
+
`--true-drive` puts an emulated 1541 on device 8 instead, running its
|
|
30
|
+
own DOS ROM on its own 6502 and talking to the machine over the serial
|
|
31
|
+
bus. It reads `.d64` and `.g64` images, which it autostarts with the
|
|
32
|
+
same `LOAD"*",8,1` and `RUN`, but not `.d71`, `.d81` or `.t64` images
|
|
33
|
+
or directories. Loading runs at the speed of a real 1541, and the
|
|
34
|
+
drive's red LED lights in the bottom right corner of the border.
|
|
35
|
+
|
|
36
|
+
A `.g64` holds the disk's raw GCR, half tracks and all, so copy
|
|
37
|
+
protection and fast loaders that read it work.
|
|
38
|
+
|
|
39
|
+
## Write protection
|
|
40
|
+
|
|
41
|
+
Disks given on the command line, inserted from the pause menu or by an
|
|
42
|
+
`--at` insert go in write-protected, so the drive reports
|
|
43
|
+
`26,WRITE PROTECT ON` for any write and the image file stays as it was.
|
|
44
|
+
`--writable`, or WRITABLE on the pause menu's Drive page, lets writes go
|
|
45
|
+
straight back to the image file. An image the host can't write, and a
|
|
46
|
+
`.t64`, stay write-protected.
|
|
47
|
+
|
|
48
|
+
## From Ruby
|
|
49
|
+
|
|
50
|
+
`Badline::Media.attach(computer, path)` attaches any medium the
|
|
51
|
+
command line takes, and autostarts it unless given `autostart: false`.
|
|
52
|
+
`Badline::Media.insert_disk(computer, path)` swaps the disk image or
|
|
53
|
+
directory in device 8 while the machine runs, for software that asks for
|
|
54
|
+
another disk. Both write to disk images unless told otherwise:
|
|
55
|
+
`insert_disk` takes `read_only: true`, and `attach` takes
|
|
56
|
+
`disk: { read_only: true }`, as the test harnesses under `bin/` do. A
|
|
57
|
+
`.g64` goes in the true 1541, and takes out a disk mounted through the
|
|
58
|
+
traps, so `LOAD` and `SAVE` reach the 1541 too. With the 1541 in device
|
|
59
|
+
8, a `.d64` goes in its drive as well.
|
|
60
|
+
|
|
61
|
+
`Badline::Media::DiskSet.around(path)` lists the disks of the set a disk
|
|
62
|
+
image belongs to, which the pause menu's PREVIOUS DISK and NEXT DISK step
|
|
63
|
+
through. An `.m3u` or `.vfl` list in the disk's folder that names it
|
|
64
|
+
gives the set. Without one, the set is found by name in the folder.
|
|
65
|
+
Given a list, it returns the disks the list names.
|
|
66
|
+
|
|
67
|
+
A list names one disk image a line, as a path relative to the list or an
|
|
68
|
+
absolute one. Blank lines and lines starting with `#` are skipped, and
|
|
69
|
+
entries that aren't there are left out. In a VICE `.vfl` flip list, only
|
|
70
|
+
the entries after a `UNIT 8` line, or before any `UNIT` line, count.
|
|
71
|
+
`Media.attach` and the command line take a list as media and attach its
|
|
72
|
+
first disk.
|
|
73
|
+
|
|
74
|
+
## The light pen
|
|
75
|
+
|
|
76
|
+
Control port 1's fire line is also the VIC-II's light pen input, so
|
|
77
|
+
joystick 1's fire button and the 1351's left button in port 1 latch the
|
|
78
|
+
light pen registers.
|
data/doc/pinned-behaviour.md
CHANGED
|
@@ -26,6 +26,7 @@ only catches the rows that happen to move.
|
|
|
26
26
|
- [VIC light pen](#vic-light-pen)
|
|
27
27
|
- [VIC-II 8565](#vic-ii-8565)
|
|
28
28
|
- [VIC-II NTSC](#vic-ii-ntsc)
|
|
29
|
+
- [VIC-II 6572 (Drean)](#vic-ii-6572-drean)
|
|
29
30
|
- [CIA 6526 timer pipeline](#cia-6526-timer-pipeline)
|
|
30
31
|
- [CIA 6526A interrupt register](#cia-6526a-interrupt-register)
|
|
31
32
|
- [CIA serial shift register](#cia-serial-shift-register)
|
|
@@ -293,7 +294,9 @@ only catches the rows that happen to move.
|
|
|
293
294
|
early in the next one for 3–7.
|
|
294
295
|
- A sprite still shifting at K loses the rest of its row. Its output
|
|
295
296
|
holds its last pixel from K through **K+6**, then goes dark.
|
|
296
|
-
- A comparator hit in K..K+11 shows nothing.
|
|
297
|
+
- A comparator hit in K..K+11 shows nothing. Where K falls in the
|
|
298
|
+
last 11 pixels of the line, as sprite 3's does on a 65-cycle line,
|
|
299
|
+
the twelve run on into the next line's first pixels.
|
|
297
300
|
- A hit from K+12 on shows the row the reload brought. For sprites 0–2
|
|
298
301
|
that is the *next* line's row, a line early. For sprites 3–7 it is the
|
|
299
302
|
current row, and a hit ahead of their K shows the previous line's row.
|
|
@@ -307,6 +310,11 @@ only catches the rows that happen to move.
|
|
|
307
310
|
`spritex/demusinterruptus` and `spritegap2` pass on it too.
|
|
308
311
|
- Spec guard: *the reload* in
|
|
309
312
|
[`vic/sprite_spec.rb`](../spec/badline/vic/sprite_spec.rb).
|
|
313
|
+
- The run into the next line is pinned by `spritescan_drean`'s 6572
|
|
314
|
+
dump, where sprite 3 shows nothing for X = $193 to $19e, the twelve
|
|
315
|
+
pixels from K = 515 on a 520-pixel line. Without it a hit at $198 to
|
|
316
|
+
$19e collides. Spec guard: *the reload near the end of a 65-cycle
|
|
317
|
+
line*. The `testbench-ntsc` sprite rows pass either way.
|
|
310
318
|
- The hold's length is pinned by `spritefetchbug/test-136-2a`, whose
|
|
311
319
|
X-expanded sprite 0 sits at X = $136 and is still shifting at 459. Its
|
|
312
320
|
reference holds the last pixel through 465. Holding it for one pixel
|
|
@@ -941,14 +949,22 @@ region moves the sprites. The positions live in `Region::Profile` and
|
|
|
941
949
|
- Spec guard: *sprite DMA cycle stealing on NTSC* and *#phi1_data on
|
|
942
950
|
NTSC* in [`vic_spec.rb`](../spec/badline/vic_spec.rb), and
|
|
943
951
|
[`timing_spec.rb`](../spec/badline/vic/sprite/timing_spec.rb).
|
|
944
|
-
- **Display compare
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
+
- **Display compare.** The 6567R8 turns a sprite's display on and off in
|
|
953
|
+
cycle 59 (`sprite_display_cycle`), where the 6567R56A keeps the 6569's
|
|
954
|
+
cycle 58. This isn't pinned yet: moving the 6567R8's to 58 or the
|
|
955
|
+
6567R56A's to 59 fails no row of those subsets.
|
|
956
|
+
- **X counter.** The 6567R8's X counter spends 8 pixels more on a line
|
|
957
|
+
(`x_hold`): after $187 it runs over $180-$187 a second time, then on
|
|
958
|
+
from $188, so it reaches $1ff on a 520-pixel line. The 6567R56A's runs
|
|
959
|
+
straight through 512 pixels.
|
|
960
|
+
- Pinned for the 6572, which shares the line, by `spritescan_drean`'s
|
|
961
|
+
dump (see [VIC-II 6572 (Drean)](#vic-ii-6572-drean)).
|
|
962
|
+
- The 6567R8's own lightpen dump (`split-tests/lightpen`'s
|
|
963
|
+
`dump6567.prg`, run by hand on `--model ntsc`) agrees: the sample the
|
|
964
|
+
test takes 8 pixels into the hold reads $c2 ($180-$181). Reading
|
|
965
|
+
$184-$187 there instead, as badline did before, gives $c4. The 6572's
|
|
966
|
+
`dump6572.prg` has the same values. The `testbench-ntsc` rows pass
|
|
967
|
+
either way.
|
|
952
968
|
- **Blanking and crop.** The blanked lines are the ones VICE's NTSC view
|
|
953
969
|
leaves out, 12-27 on the 6567R8 and 13-27 on the 6567R56A, so the
|
|
954
970
|
`testbench-ntsc` screenshots, VICE's 247 lines from line 28 running on
|
|
@@ -970,6 +986,34 @@ What the NTSC references don't settle, and so what stays as it is:
|
|
|
970
986
|
readme says it was matched against screenshots, and the one 6567R8
|
|
971
987
|
photograph in the testprogs is of revision R01.
|
|
972
988
|
|
|
989
|
+
## VIC-II 6572 (Drean)
|
|
990
|
+
|
|
991
|
+
The 6572 of the Drean C64 (`Region::DREAN`) runs the 6567R8's line, 65
|
|
992
|
+
cycles with its sprite fetches in cycle 59, its display compare in cycle
|
|
993
|
+
59 and its X counter running over $180-$187 twice, on PAL-N's 312 lines at
|
|
994
|
+
1,023,440 Hz. The oracle is `split-tests/spritescan/spritescan_drean.prg`,
|
|
995
|
+
which checks itself against its `dump6572.bin`, from a real 6572R1. The
|
|
996
|
+
testlist comments that row out, and `testbench-drean`
|
|
997
|
+
(`bin/testbench --drean`) runs it.
|
|
998
|
+
|
|
999
|
+
- Pinned by that dump: every byte the test compares matches, and the
|
|
1000
|
+
test passes.
|
|
1001
|
+
- The X counter's second run over $180-$187 is pinned by sprite 1,
|
|
1002
|
+
pattern C (a one-pixel sprite against one shifted a pixel left), which
|
|
1003
|
+
collides for X = $180 to $186 on the 6572. Those are inside sprite 1's
|
|
1004
|
+
twelve dead pixels (K = 483), so the sprite only shows from the second
|
|
1005
|
+
time the counter reads its X. Repeating $184-$187 in the hold instead
|
|
1006
|
+
misses $180-$183 (4 bytes), repeating $180-$183 misses $184-$186 (3),
|
|
1007
|
+
and repeating $17c-$183 or $184-$18b moves 18 bytes. Spec guard: the
|
|
1008
|
+
6567R8 and 6572 examples in
|
|
1009
|
+
[`timing_spec.rb`](../spec/badline/vic/sprite/timing_spec.rb).
|
|
1010
|
+
- The display compare isn't pinned: the dump matches with it in cycle
|
|
1011
|
+
58 as well as 59.
|
|
1012
|
+
- Not matched: `spritegap3`'s 6572 dump has sprite 0 at X = 0 miss the
|
|
1013
|
+
sprite at X = 1 for every pair it is in, where badline, like the
|
|
1014
|
+
6567R8 dump, has them collide. The test still passes, because it
|
|
1015
|
+
accepts the 6567R8's dump too.
|
|
1016
|
+
|
|
973
1017
|
## CIA 6526 timer pipeline
|
|
974
1018
|
|
|
975
1019
|
- Start and stop go through two stages. A force load lands one tick after
|
data/doc/sid-player.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# The SID player
|
|
2
|
+
|
|
3
|
+
The [README](../README.md#playing-and-rendering-sid-tunes) shows how to
|
|
4
|
+
start it. This page covers the player in detail.
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
badline sid ~/C64Music/MUSICIANS/H/Hubbard_Rob # play every tune below a directory
|
|
8
|
+
badline sid tune.sid other.sid # play a queue of tunes
|
|
9
|
+
badline --headless tune.sid # play, length from HVSC
|
|
10
|
+
badline --headless -s 3 tune.sid # play the third subtune
|
|
11
|
+
badline --headless --all-subtunes tune.sid # play on through every subtune
|
|
12
|
+
badline --seconds 180 tune.sid --audio-out out.aiff
|
|
13
|
+
badline -s 3 --rate 48000 tune.sid --audio-out out.wav
|
|
14
|
+
badline --headless --sid 8580 tune.sid
|
|
15
|
+
badline --filter-chunk 1 tune.sid --audio-out out.wav # exact filter, slower
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## The queue
|
|
19
|
+
|
|
20
|
+
`badline sid FILE|DIR...` plays a queue of tunes in the SID player's
|
|
21
|
+
window, in the order given, and a directory adds every `.sid` tune below
|
|
22
|
+
it in path order. `badline sid --headless` plays the queue in the
|
|
23
|
+
terminal instead. `badline sid` on its own opens the window with an
|
|
24
|
+
empty queue. Dropping `.sid` files or folders on the window adds them
|
|
25
|
+
to the end of the queue, and an empty queue starts playing them. A file
|
|
26
|
+
that isn't a tune is skipped.
|
|
27
|
+
|
|
28
|
+
`badline tune.sid` plays the tune in the SID player too, unless an
|
|
29
|
+
option of the emulator's window, such as `--ntsc` or `--reu`, asks for
|
|
30
|
+
the machine: then a tune for one SID runs on the emulated C64, started
|
|
31
|
+
through a small driver after boot.
|
|
32
|
+
|
|
33
|
+
`--subtune` (or `-s`) picks the first tune's subtune, counting from 1 as
|
|
34
|
+
HVSC does, and defaults to the tune's own start subtune. Each tune plays
|
|
35
|
+
that one subtune, and when it ends the player goes on to the next tune,
|
|
36
|
+
stopping after the last. `a`, or `--all-subtunes`, turns on playing all
|
|
37
|
+
subtunes, so that a subtune that ends goes on to the tune's next
|
|
38
|
+
subtune, and a tune stepped to starts on its first subtune.
|
|
39
|
+
|
|
40
|
+
`--sid auto`, the default, fits each of a tune's SIDs the model its
|
|
41
|
+
header names. A tune written for 2 or 3 SIDs plays on as many, at the
|
|
42
|
+
addresses its header gives, in stereo: SID 1 on the left, SID 2 on the
|
|
43
|
+
right and SID 3 in the centre. A tune on one SID plays the same on both
|
|
44
|
+
channels.
|
|
45
|
+
|
|
46
|
+
Playback asks the device for 44.1 kHz and takes whatever rate it
|
|
47
|
+
offers, unless `--rate` says otherwise. Ctrl-C stops it. The emulator
|
|
48
|
+
window's own options, `--no-autostart`, `--writable`, `--sound`,
|
|
49
|
+
`--true-drive`, `--reu`, `--model`, `--ntsc` and `--verbose`, don't apply.
|
|
50
|
+
|
|
51
|
+
## The window
|
|
52
|
+
|
|
53
|
+
The window is worked with the mouse. Its header shows the tune's name,
|
|
54
|
+
author and release, the tune the subtune covers at the moment when
|
|
55
|
+
HVSC's STIL credits one, following the times STIL gives, and buttons
|
|
56
|
+
that switch between three views and between the tune's own SID model,
|
|
57
|
+
the 6581 and the 8580, which changes the chips playing on the spot. Its
|
|
58
|
+
footer shows the time played on a bar you can click to seek, and buttons
|
|
59
|
+
that pause, step between tunes and between a tune's subtunes, and turn
|
|
60
|
+
shuffle, looping and all subtunes on and off.
|
|
61
|
+
|
|
62
|
+
- The visualizer shows each voice's note, and how far off it is in
|
|
63
|
+
cents, over a scope of the voice's output, and the mixed output below
|
|
64
|
+
them. A tune on more than one SID gets a row of voices for each SID,
|
|
65
|
+
and the mix splits into its left and right.
|
|
66
|
+
- The SID view shows one SID at a time, and for a tune on more than one,
|
|
67
|
+
a row of SID 1, 2 and 3 buttons at its top picks which. It shows each
|
|
68
|
+
voice's output, its control bits, pulse width, envelope settings, and
|
|
69
|
+
the envelope's level and stage, and the filter's modes, cutoff,
|
|
70
|
+
resonance, volume and its response on the chip playing.
|
|
71
|
+
- The INFO view shows the tune's STIL entry and the subtune's, with a
|
|
72
|
+
scroll bar, the mouse wheel or the up and down keys for the long ones.
|
|
73
|
+
|
|
74
|
+
The terminal's keys work in the window too, along with Tab to switch
|
|
75
|
+
views and `c` to step through the SID models.
|
|
76
|
+
|
|
77
|
+
## The terminal
|
|
78
|
+
|
|
79
|
+
Played on a terminal, `--headless` and `sid` show each tune's name,
|
|
80
|
+
author and release as it starts, then the tune's place in the queue, the
|
|
81
|
+
subtune number and the time played against the subtune's length.
|
|
82
|
+
`--headless` queues just the one tune.
|
|
83
|
+
|
|
84
|
+
→ and ← step to the tune's next and previous subtune, stopping at its
|
|
85
|
+
first and last. `n` and `p` step to the next and previous tune. `s`
|
|
86
|
+
turns shuffle on and off, which plays the queue's tunes in a random
|
|
87
|
+
order, and `l` turns looping on and off, so that the end of the queue
|
|
88
|
+
goes on to its start. `,` and `.` seek 10 seconds back and forward
|
|
89
|
+
within the subtune. As a subtune plays, the player saves its state every
|
|
90
|
+
10 seconds. A seek back runs silently from the latest of those saved
|
|
91
|
+
states at or before the point asked for, or from the subtune's start
|
|
92
|
+
before the first. A seek forward runs on silently from where the
|
|
93
|
+
subtune is, or from a saved state further on, when an earlier seek back
|
|
94
|
+
left one there. The saved states last until the tune or subtune
|
|
95
|
+
changes. Space pauses and `q` quits. The status line shows which
|
|
96
|
+
modes are on, and they last until the player quits.
|
|
97
|
+
|
|
98
|
+
`--no-tui`, or output that isn't a terminal, gives plain progress output
|
|
99
|
+
instead and plays through the queue without the keys.
|
|
100
|
+
|
|
101
|
+
## Rendering
|
|
102
|
+
|
|
103
|
+
`--audio-out` renders one subtune to a 16-bit stereo PCM file. The
|
|
104
|
+
file's extension picks the format, `.wav` or `.aiff`.
|
|
105
|
+
|
|
106
|
+
## Lengths and STIL
|
|
107
|
+
|
|
108
|
+
A `.sid` file doesn't store its length, so the player looks the tune up
|
|
109
|
+
by MD5 in HVSC's `Songlengths.md5`. It finds the database through
|
|
110
|
+
`--songlengths`, in a `DOCUMENTS` directory in any of the tune's parent
|
|
111
|
+
directories (the layout of an HVSC collection), or under
|
|
112
|
+
`$HVSC_BASE/DOCUMENTS`. Without a database or `--seconds` it runs for 60
|
|
113
|
+
seconds, but a subtune that falls silent for 5 seconds before then ends
|
|
114
|
+
there, when played and when rendered alike. Silent means the output
|
|
115
|
+
holds within 16 steps of one level, since a 6581 idles at a DC offset
|
|
116
|
+
rather than at zero. A subtune with a known length plays to its length
|
|
117
|
+
whatever it sounds like.
|
|
118
|
+
|
|
119
|
+
A tune in an HVSC collection also gets its entry in HVSC's `STIL.txt`,
|
|
120
|
+
found the same way: comments, covers, and subtune names and composers.
|
|
121
|
+
The tune's own fields follow its header, and each subtune's print as it
|
|
122
|
+
starts.
|
|
123
|
+
|
|
124
|
+
## Speed
|
|
125
|
+
|
|
126
|
+
`badline` plays PSID and RSID tunes in real time. In `badline-ruby`,
|
|
127
|
+
PSID tunes run on a CPU and RAM with only the SID clocked, at about
|
|
128
|
+
twice real time, so they play smoothly, but RSID tunes set up their own
|
|
129
|
+
interrupts, so they boot a full C64 first and run at about half real
|
|
130
|
+
time. They render fine but stutter when played, and `badline-ruby` says
|
|
131
|
+
so when it falls behind. Both builds run the same code, so they render
|
|
132
|
+
the same file sample for sample. See
|
|
133
|
+
[native/README.md](../native/README.md#without-the-window).
|
|
134
|
+
|
|
135
|
+
The filter steps four cycles at a time; `--filter-chunk 1` steps it
|
|
136
|
+
every cycle, which is exact and takes about twice as long.
|
data/doc/snapshots.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Snapshots
|
|
2
|
+
|
|
3
|
+
In the window, `F11` quicksaves the whole machine to one of five slots,
|
|
4
|
+
`quicksave-1.vsf` to `quicksave-5.vsf` in the `quicksaves` folder of
|
|
5
|
+
badline's data folder. It fills the empty slots first, then replaces the
|
|
6
|
+
one saved longest ago. `F12` restores the newest quicksave, or a named
|
|
7
|
+
save in the data folder's `saves` folder if one is newer, in a new
|
|
8
|
+
machine built as the saved one was. Quicksaves stay after badline quits,
|
|
9
|
+
so `F12` finds them the next time it starts. What's printed on a save or
|
|
10
|
+
a restore names the slot and the file.
|
|
11
|
+
|
|
12
|
+
A run without `--frames` also autosaves every 6,000 frames the machine
|
|
13
|
+
has run, two minutes on PAL, to `autosave-1.vsf` to `autosave-3.vsf` in
|
|
14
|
+
the `autosaves` folder, replacing the oldest. Time in the pause menu
|
|
15
|
+
doesn't count, nothing is printed, and `F12` never picks an autosave. An
|
|
16
|
+
autosave that fails warns once and stops autosaving.
|
|
17
|
+
|
|
18
|
+
The pause menu's Snapshots page quicksaves, saves under a name in the
|
|
19
|
+
`saves` folder and loads a named save. The name starts as the game's,
|
|
20
|
+
from its cartridge, disk or tape, or BASIC, with the next free number,
|
|
21
|
+
and saving over a name already taken asks first. Below those, the
|
|
22
|
+
newest quicksaves and autosaves, with the time each was saved, load
|
|
23
|
+
with a press.
|
|
24
|
+
|
|
25
|
+
The data folder is `~/Library/Application Support/badline` on macOS,
|
|
26
|
+
and `$XDG_DATA_HOME/badline` or `~/.local/share/badline` elsewhere.
|
|
27
|
+
`BADLINE_DATA_PATH` puts it somewhere else.
|
|
28
|
+
|
|
29
|
+
Both executables do this, and both open a `.vsf` given as the media,
|
|
30
|
+
in a machine built as the saved one was, whatever `--model` says.
|
|
31
|
+
`--save-snapshot FILE` saves one to `FILE` after the last frame. A
|
|
32
|
+
snapshot that fails to open leaves the machine running as it was.
|
|
33
|
+
|
|
34
|
+
## From Ruby
|
|
35
|
+
|
|
36
|
+
`computer.save_snapshot(path)` saves, `computer.restore_snapshot(path)`
|
|
37
|
+
takes a machine back to a snapshot, and `Badline::Snapshot.load(path)`
|
|
38
|
+
builds a new machine as the saved one was built and restores it.
|
|
39
|
+
`computer.snapshot` and `computer.restore(state)` do the same in memory,
|
|
40
|
+
without a file. A restore that fails leaves the machine as it was.
|
|
41
|
+
|
|
42
|
+
## What a snapshot holds
|
|
43
|
+
|
|
44
|
+
A snapshot holds the machine as it was on the cycle it was saved: the
|
|
45
|
+
chips down to the instruction step and the pixel pipeline, the RAM and
|
|
46
|
+
any +60K or +256K expansion, the cartridge with its RAM and flash, a
|
|
47
|
+
GEO-RAM, a true 1541 with its RAM, its VIAs and the disk under the head,
|
|
48
|
+
a disk or directory mounted through the traps with its open channels,
|
|
49
|
+
and the tape with its place on it. A restored machine runs on exactly
|
|
50
|
+
as the saved one would have.
|
|
51
|
+
|
|
52
|
+
A tape, a `.t64` archive and a disk image mounted through the traps come
|
|
53
|
+
back with their contents from the snapshot, so they need no file to
|
|
54
|
+
restore. A true drive's disk opens again from its image's path, with its
|
|
55
|
+
tracks as the drive last saw them. When that file is gone, the disk
|
|
56
|
+
comes back from the tracks alone, write-protected. A directory mounted
|
|
57
|
+
as device 8 opens again from its path. What the host holds stays the
|
|
58
|
+
host's: the keyboard, the joysticks, the mouse and paddles, sound, and
|
|
59
|
+
blocks given to `on_init` that hadn't run yet, which a restore reports.
|
|
60
|
+
|
|
61
|
+
Every model saves this way, PAL, NTSC, old NTSC and Drean alike, and so
|
|
62
|
+
does a machine with an REU, with its RAM, its registers and a transfer
|
|
63
|
+
part way through. A snapshot only restores in the badline version that
|
|
64
|
+
wrote it, into a machine with the same chip models, region, RAM
|
|
65
|
+
expansion, REU, KERNAL and datasette. One saved before badline recorded
|
|
66
|
+
the KERNAL and the datasette restores as a machine with the C64's KERNAL
|
|
67
|
+
and a datasette.
|
|
68
|
+
|
|
69
|
+
## VICE
|
|
70
|
+
|
|
71
|
+
Snapshots use VICE's `.vsf` format. badline writes VICE's modules for
|
|
72
|
+
the CPU, RAM and CPU port, both CIAs, the SID and the VIC-II, and an
|
|
73
|
+
REU's `REU1764`, plus the ones x64sc needs to open the file, with
|
|
74
|
+
nothing else attached to the cartridge port, nothing on the tape or user
|
|
75
|
+
ports and no true drive. The VIC-II module names the chip as VICE does:
|
|
76
|
+
the 6569 or 8565 on PAL, the 6567R8 or 8562 on NTSC, the 6567R56A on old
|
|
77
|
+
NTSC and the 6572 on PAL-N. A `BADLINE` module, which VICE skips, holds
|
|
78
|
+
the whole machine, and badline restores its own snapshots from it.
|
|
79
|
+
|
|
80
|
+
x64sc 3.10 opens badline's snapshots, taken at the end of the
|
|
81
|
+
instruction the CPU was in, and of an REU transfer under way, as long
|
|
82
|
+
as its VIC-II model matches: `-model c64`, `c64c`, `ntsc`, `newntsc`,
|
|
83
|
+
`oldntsc` or `drean`, as `--model` names the machine. It takes the REU
|
|
84
|
+
from the snapshot, keeps its own drives and leaves out the cartridge,
|
|
85
|
+
the other expansions and the tape. A program x64sc runs on from badline's
|
|
86
|
+
snapshot reads the raster as badline does, cycle for cycle.
|
|
87
|
+
|
|
88
|
+
Snapshots x64sc saves open in badline through the same modules: the CPU
|
|
89
|
+
at its instruction boundary, RAM, the CPU port, the CIAs' registers,
|
|
90
|
+
timers and clocks, the SID's registers and reSID voice state, the
|
|
91
|
+
VIC-II's registers, beam position, counters and colour RAM, and an
|
|
92
|
+
REU's size, registers and RAM. The VIC-II's model builds the machine's
|
|
93
|
+
region, and the 6569R1 runs as the 6569. The VIC-II's pixel pipeline
|
|
94
|
+
starts empty. badline reads the modules x64sc 3.7 to 3.10 write, and
|
|
95
|
+
VICE's development versions' `MAINC64CPU` and `VIC-IISC`. A module
|
|
96
|
+
version it doesn't know is left out, or fails the restore for the CPU
|
|
97
|
+
and RAM. It reports the modules it leaves out, such as the 1541 drives,
|
|
98
|
+
the cartridge port, the datasette and the keyboard, and carries on
|
|
99
|
+
without them. Restored into a running machine, a VICE snapshot
|
|
100
|
+
fails for a machine built another way, such as a C64C's snapshot in a
|
|
101
|
+
C64, and otherwise takes the cartridge out and switches the machine off
|
|
102
|
+
and on.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class AddressBus
|
|
5
|
+
# The BASIC, character and KERNAL ROMs, and which KERNAL is fitted.
|
|
6
|
+
module ROMs
|
|
7
|
+
# The KERNAL ROMs a machine can be fitted with, by name: the C64's
|
|
8
|
+
# and the SX-64's.
|
|
9
|
+
KERNALS = { c64: "kernal.rom", sx64: "kernal-sx64.rom" }.freeze
|
|
10
|
+
|
|
11
|
+
attr_reader :basic_rom, :character_rom, :kernal_rom, :kernal
|
|
12
|
+
|
|
13
|
+
# Fits the KERNAL ROM KERNALS names `name`, in place of the one before.
|
|
14
|
+
def kernal=(name)
|
|
15
|
+
raise ArgumentError, "no KERNAL named #{name}" unless KERNALS.key?(name)
|
|
16
|
+
|
|
17
|
+
@kernal = name
|
|
18
|
+
@kernal_rom = ROM.load(KERNALS[name], 0xe000)
|
|
19
|
+
update_overlays!
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def load_roms
|
|
25
|
+
@basic_rom = ROM.load("basic.rom", 0xa000)
|
|
26
|
+
@character_rom = ROM.load("character.rom", 0xd000)
|
|
27
|
+
@kernal = :c64
|
|
28
|
+
@kernal_rom = ROM.load(KERNALS[:c64], 0xe000)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
data/lib/badline/address_bus.rb
CHANGED
|
@@ -4,6 +4,7 @@ require "badline/address_bus/saved_state"
|
|
|
4
4
|
require "badline/address_bus/sid_slots"
|
|
5
5
|
require "badline/address_bus/extra_sids"
|
|
6
6
|
require "badline/address_bus/ultimax_pages"
|
|
7
|
+
require "badline/address_bus/roms"
|
|
7
8
|
|
|
8
9
|
module Badline
|
|
9
10
|
# Memory layout:
|
|
@@ -35,6 +36,7 @@ module Badline
|
|
|
35
36
|
include Addressable
|
|
36
37
|
include UltimaxPages
|
|
37
38
|
include ExtraSIDs
|
|
39
|
+
include ROMs
|
|
38
40
|
|
|
39
41
|
# I/O 1 and 2, and the Ultimax holes, with nothing on the bus. A read
|
|
40
42
|
# picks up the byte the VIC fetched in the preceding phi1 half-cycle,
|
|
@@ -58,7 +60,7 @@ module Badline
|
|
|
58
60
|
# See doc/pinned-behaviour.md.
|
|
59
61
|
RAM_POWER_ON = Array.new(2**16) { |addr| (((addr + 2) / 4) ^ (addr / 0x4000)).odd? ? 0xff : 0x00 }.freeze
|
|
60
62
|
|
|
61
|
-
attr_reader :io_port, :ram,
|
|
63
|
+
attr_reader :io_port, :ram,
|
|
62
64
|
:vic, :sid, :color_ram, :cia1, :cia2, :keyboard, :joystick1, :joystick2,
|
|
63
65
|
:control_ports, :cartridge, :ultimax, :phi1_ultimax, :datasette, :region, :video_ram, :reu
|
|
64
66
|
|
|
@@ -71,9 +73,7 @@ module Badline
|
|
|
71
73
|
@cartridge = @reu = nil
|
|
72
74
|
@debug_register = nil
|
|
73
75
|
|
|
74
|
-
|
|
75
|
-
@character_rom = ROM.load("character.rom", 0xd000)
|
|
76
|
-
@kernal_rom = ROM.load("kernal.rom", 0xe000)
|
|
76
|
+
load_roms
|
|
77
77
|
|
|
78
78
|
@keyboard = Keyboard.new
|
|
79
79
|
@joystick1 = Joystick.new
|
|
@@ -74,6 +74,30 @@ module Badline
|
|
|
74
74
|
settle
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
+
# The rig between frames: the bus and its chips, the extra SIDs, the
|
|
78
|
+
# CPU, and the call waiting for the idle loop.
|
|
79
|
+
def save_state(out)
|
|
80
|
+
out.marker("BARE PLAYER")
|
|
81
|
+
@bus.save_state(out)
|
|
82
|
+
@extras.each { |sid| sid.save_state(out) }
|
|
83
|
+
@cpu.save_state(out)
|
|
84
|
+
out.optional_int(@pending).int(@argument).boolean(@idle)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Takes a rig that hasn't started to where #save_state found one, in
|
|
88
|
+
# place of #start.
|
|
89
|
+
def load_state(input)
|
|
90
|
+
input.marker("BARE PLAYER")
|
|
91
|
+
@bus.load_state(input)
|
|
92
|
+
@extras.each { |sid| sid.load_state(input) }
|
|
93
|
+
@cpu.load_state(input)
|
|
94
|
+
@pending = input.optional_int
|
|
95
|
+
@argument = input.int
|
|
96
|
+
@idle = input.boolean?
|
|
97
|
+
@stereo.synthesize!
|
|
98
|
+
install_dispatch
|
|
99
|
+
end
|
|
100
|
+
|
|
77
101
|
# Advances one call of play, or `budget` cycles if that is shorter,
|
|
78
102
|
# then yields whatever the SIDs recorded over it, in stereo. Returns
|
|
79
103
|
# the cycles advanced.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Audio
|
|
5
|
+
# The states a subtune's renderer saved as it played, at the first
|
|
6
|
+
# frame end past every `every` seconds, so that a seek starts from the
|
|
7
|
+
# latest one at or before where it goes rather than from the subtune's
|
|
8
|
+
# start. Whoever plays the subtune keeps them across its renderers, and
|
|
9
|
+
# drops them for another subtune.
|
|
10
|
+
class Checkpoints
|
|
11
|
+
EVERY = 10.0
|
|
12
|
+
|
|
13
|
+
def initialize(every: EVERY)
|
|
14
|
+
@every = every
|
|
15
|
+
@cycles = []
|
|
16
|
+
@states = []
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Whether the frame ending `cycles` into the subtune is the first past
|
|
20
|
+
# the next mark.
|
|
21
|
+
def due?(cycles, clock_hz) = cycles >= ((@cycles.length + 1) * @every * clock_hz).round
|
|
22
|
+
|
|
23
|
+
def keep(cycles, state)
|
|
24
|
+
@cycles << cycles
|
|
25
|
+
@states << state
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# The state saved latest at or before `cycles`, or nil.
|
|
29
|
+
def latest(cycles)
|
|
30
|
+
index = @cycles.rindex { |at| at <= cycles }
|
|
31
|
+
index.nil? ? nil : @states[index]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# The cycles into the subtune of the state saved latest at or before
|
|
35
|
+
# `cycles`, or 0 for the start.
|
|
36
|
+
def reach(cycles)
|
|
37
|
+
index = @cycles.rindex { |at| at <= cycles }
|
|
38
|
+
index.nil? ? 0 : @cycles[index]
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -7,9 +7,11 @@ module Badline
|
|
|
7
7
|
# turn the queue's shuffle, loop and all-subtunes modes on and off, and
|
|
8
8
|
# quit, and the status line follows along. Seeking runs the subtune
|
|
9
9
|
# silently up to where it was asked to go: on from where it is to go
|
|
10
|
-
# forward, and from the
|
|
11
|
-
#
|
|
12
|
-
# the
|
|
10
|
+
# forward, and from the latest of its Checkpoints at or before there to
|
|
11
|
+
# go back, or to go forward when that checkpoint is further on than
|
|
12
|
+
# where the subtune is. A subtune that plays to its end moves on as
|
|
13
|
+
# Media::Queue#advance says. It stops at the end of the queue, on q, or
|
|
14
|
+
# on Ctrl-C.
|
|
13
15
|
#
|
|
14
16
|
# `renderer` builds the renderer for an entry's subtune at the sink's
|
|
15
17
|
# rate, or returns nil for an entry that can't play, which is skipped
|
|
@@ -34,6 +36,9 @@ module Badline
|
|
|
34
36
|
@backward = false
|
|
35
37
|
@below_at = -BELOW_NOTE
|
|
36
38
|
@from = 0.0
|
|
39
|
+
@entry = nil
|
|
40
|
+
@subtune = 0
|
|
41
|
+
@checkpoints = Checkpoints.new
|
|
37
42
|
end
|
|
38
43
|
|
|
39
44
|
# Returns the result of the last subtune's Playback#play, or :unplayable
|
|
@@ -77,6 +82,7 @@ module Badline
|
|
|
77
82
|
end
|
|
78
83
|
|
|
79
84
|
def play(entry, subtune)
|
|
85
|
+
@checkpoints = Checkpoints.new unless entry.equal?(@entry) && subtune == @subtune
|
|
80
86
|
@entry = entry
|
|
81
87
|
@subtune = subtune
|
|
82
88
|
@elapsed = 0.0
|
|
@@ -84,6 +90,7 @@ module Badline
|
|
|
84
90
|
return :unplayable if renderer.nil?
|
|
85
91
|
|
|
86
92
|
renderer.from = @from
|
|
93
|
+
renderer.checkpoints = @checkpoints
|
|
87
94
|
@streaming = renderer
|
|
88
95
|
@from = 0.0
|
|
89
96
|
@backward = false
|
|
@@ -127,7 +134,7 @@ module Badline
|
|
|
127
134
|
|
|
128
135
|
def seek(seconds)
|
|
129
136
|
target = seconds.clamp(0.0, [@length.call(@entry, @subtune) - 1.0, 0.0].max)
|
|
130
|
-
return @streaming.from = target if target > @elapsed
|
|
137
|
+
return @streaming.from = target if target > @elapsed && !@streaming.checkpoint_ahead?(target)
|
|
131
138
|
|
|
132
139
|
@from = target
|
|
133
140
|
skip
|
|
@@ -39,6 +39,22 @@ module Badline
|
|
|
39
39
|
|
|
40
40
|
def clock_hz = @computer.region.clock_hz
|
|
41
41
|
|
|
42
|
+
# The whole machine, as Computer#snapshot takes it, and the extra SIDs,
|
|
43
|
+
# once it plays.
|
|
44
|
+
def save_state(out)
|
|
45
|
+
@computer.save_state(out)
|
|
46
|
+
@extras.each { |sid| sid.save_state(out) }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Takes a machine that hasn't started to where #save_state found one,
|
|
50
|
+
# in place of #start.
|
|
51
|
+
def load_state(input)
|
|
52
|
+
@computer.load_state(input)
|
|
53
|
+
@extras.each { |sid| sid.load_state(input) }
|
|
54
|
+
@stereo.synthesize!
|
|
55
|
+
@started = true
|
|
56
|
+
end
|
|
57
|
+
|
|
42
58
|
# Advances one frame of the machine, or `budget` cycles if that is
|
|
43
59
|
# shorter, then yields whatever the SIDs recorded over it, in stereo.
|
|
44
60
|
# Returns the cycles advanced.
|