badline 0.7.0 → 0.8.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 +19 -0
- data/CLAUDE.md +3 -1
- data/README.md +96 -290
- data/doc/library.md +62 -0
- data/doc/media.md +69 -0
- data/doc/sid-player.md +132 -0
- data/doc/snapshots.md +69 -0
- data/lib/badline/cartridge/freezer.rb +2 -0
- data/lib/badline/cartridge.rb +3 -0
- data/lib/badline/frontend/app.rb +38 -35
- data/lib/badline/frontend/boot.rb +8 -1
- data/lib/badline/frontend/buttons.rb +101 -7
- data/lib/badline/frontend/confirmation.rb +50 -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 +122 -0
- data/lib/badline/frontend/menu_media.rb +128 -0
- data/lib/badline/frontend/menu_pages.rb +198 -0
- data/lib/badline/frontend/painter.rb +10 -0
- data/lib/badline/frontend/pause_menu.rb +246 -0
- data/lib/badline/frontend/snapshots.rb +1 -1
- data/lib/badline/frontend/sound.rb +0 -1
- data/lib/badline/frontend/timeline.rb +5 -1
- data/lib/badline/frontend.rb +7 -0
- data/lib/badline/media/disk_set.rb +108 -0
- data/lib/badline/media.rb +1 -0
- data/lib/badline/options/table.rb +1 -1
- data/lib/badline/options.rb +3 -3
- data/lib/badline/version.rb +1 -1
- metadata +13 -1
data/doc/media.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
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, found by name in its folder, which the pause menu's
|
|
63
|
+
PREVIOUS DISK and NEXT DISK step through.
|
|
64
|
+
|
|
65
|
+
## The light pen
|
|
66
|
+
|
|
67
|
+
Control port 1's fire line is also the VIC-II's light pen input, so
|
|
68
|
+
joystick 1's fire button and the 1351's left button in port 1 latch the
|
|
69
|
+
light pen registers.
|
data/doc/sid-player.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
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`, `--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: a seek forward runs on silently from where the
|
|
90
|
+
subtune is, and a seek back plays it again from its start up to the
|
|
91
|
+
point asked for. Space pauses and `q` quits. The status line shows which
|
|
92
|
+
modes are on, and they last until the player quits.
|
|
93
|
+
|
|
94
|
+
`--no-tui`, or output that isn't a terminal, gives plain progress output
|
|
95
|
+
instead and plays through the queue without the keys.
|
|
96
|
+
|
|
97
|
+
## Rendering
|
|
98
|
+
|
|
99
|
+
`--audio-out` renders one subtune to a 16-bit stereo PCM file. The
|
|
100
|
+
file's extension picks the format, `.wav` or `.aiff`.
|
|
101
|
+
|
|
102
|
+
## Lengths and STIL
|
|
103
|
+
|
|
104
|
+
A `.sid` file doesn't store its length, so the player looks the tune up
|
|
105
|
+
by MD5 in HVSC's `Songlengths.md5`. It finds the database through
|
|
106
|
+
`--songlengths`, in a `DOCUMENTS` directory in any of the tune's parent
|
|
107
|
+
directories (the layout of an HVSC collection), or under
|
|
108
|
+
`$HVSC_BASE/DOCUMENTS`. Without a database or `--seconds` it runs for 60
|
|
109
|
+
seconds, but a subtune that falls silent for 5 seconds before then ends
|
|
110
|
+
there, when played and when rendered alike. Silent means the output
|
|
111
|
+
holds within 16 steps of one level, since a 6581 idles at a DC offset
|
|
112
|
+
rather than at zero. A subtune with a known length plays to its length
|
|
113
|
+
whatever it sounds like.
|
|
114
|
+
|
|
115
|
+
A tune in an HVSC collection also gets its entry in HVSC's `STIL.txt`,
|
|
116
|
+
found the same way: comments, covers, and subtune names and composers.
|
|
117
|
+
The tune's own fields follow its header, and each subtune's print as it
|
|
118
|
+
starts.
|
|
119
|
+
|
|
120
|
+
## Speed
|
|
121
|
+
|
|
122
|
+
`badline` plays PSID and RSID tunes in real time. In `badline-ruby`,
|
|
123
|
+
PSID tunes run on a CPU and RAM with only the SID clocked, at about
|
|
124
|
+
twice real time, so they play smoothly, but RSID tunes set up their own
|
|
125
|
+
interrupts, so they boot a full C64 first and run at about half real
|
|
126
|
+
time. They render fine but stutter when played, and `badline-ruby` says
|
|
127
|
+
so when it falls behind. Both builds run the same code, so they render
|
|
128
|
+
the same file sample for sample. See
|
|
129
|
+
[native/README.md](../native/README.md#without-the-window).
|
|
130
|
+
|
|
131
|
+
The filter steps four cycles at a time; `--filter-chunk 1` steps it
|
|
132
|
+
every cycle, which is exact and takes about twice as long.
|
data/doc/snapshots.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Snapshots
|
|
2
|
+
|
|
3
|
+
In the window, `F11` saves the whole machine to a new
|
|
4
|
+
`badline-<date>-<time>.vsf` in the working directory, and `F12` goes
|
|
5
|
+
back to the snapshot last saved or opened, in a new machine built as the
|
|
6
|
+
saved one was. Both executables do this, and both open a `.vsf` given as
|
|
7
|
+
the media. A snapshot that fails to open leaves the machine running as
|
|
8
|
+
it was.
|
|
9
|
+
|
|
10
|
+
## From Ruby
|
|
11
|
+
|
|
12
|
+
`computer.save_snapshot(path)` saves, `computer.restore_snapshot(path)`
|
|
13
|
+
takes a machine back to a snapshot, and `Badline::Snapshot.load(path)`
|
|
14
|
+
builds a new machine as the saved one was built and restores it.
|
|
15
|
+
`computer.snapshot` and `computer.restore(state)` do the same in memory,
|
|
16
|
+
without a file. A restore that fails leaves the machine as it was.
|
|
17
|
+
|
|
18
|
+
## What a snapshot holds
|
|
19
|
+
|
|
20
|
+
A snapshot holds the machine as it was on the cycle it was saved: the
|
|
21
|
+
chips down to the instruction step and the pixel pipeline, the RAM and
|
|
22
|
+
any +60K or +256K expansion, the cartridge with its RAM and flash, a
|
|
23
|
+
GEO-RAM, a true 1541 with its RAM, its VIAs and the disk under the head,
|
|
24
|
+
a disk or directory mounted through the traps with its open channels,
|
|
25
|
+
and the tape with its place on it. A restored machine runs on exactly
|
|
26
|
+
as the saved one would have.
|
|
27
|
+
|
|
28
|
+
A directory, a tape and a true drive's disk image open again from their
|
|
29
|
+
paths when the snapshot is restored, the disk with its tracks as the
|
|
30
|
+
drive last saw them. A disk image mounted through the traps comes back
|
|
31
|
+
with its contents from the snapshot. What the host holds stays the
|
|
32
|
+
host's: the keyboard, the joysticks, the mouse and paddles, sound, and
|
|
33
|
+
blocks given to `on_init` that hadn't run yet, which a restore reports.
|
|
34
|
+
|
|
35
|
+
A snapshot only restores in the badline version that wrote it, into a
|
|
36
|
+
machine with the same chip models, RAM expansion and REU.
|
|
37
|
+
`computer.snapshot` holds an REU's RAM, registers and transfer too, but
|
|
38
|
+
a machine with an REU doesn't save to a file yet, as badline doesn't
|
|
39
|
+
write VICE's REU module. Nor does an NTSC machine, as badline doesn't
|
|
40
|
+
write an NTSC VIC-II in VICE's terms yet.
|
|
41
|
+
|
|
42
|
+
## VICE
|
|
43
|
+
|
|
44
|
+
Snapshots use VICE's `.vsf` format. badline writes VICE's modules for
|
|
45
|
+
the CPU, RAM and CPU port, both CIAs, the SID and the VIC-II, plus the
|
|
46
|
+
ones x64sc needs to open the file, with nothing attached to the
|
|
47
|
+
cartridge, tape or user ports and no true drive. A `BADLINE` module,
|
|
48
|
+
which VICE skips, holds the whole machine, and badline restores its own
|
|
49
|
+
snapshots from it.
|
|
50
|
+
|
|
51
|
+
x64sc 3.10 opens badline's snapshots, taken at the end of the
|
|
52
|
+
instruction the CPU was in, as long as its VIC-II model matches
|
|
53
|
+
(`-model c64` for the default machine). It keeps its own drives and
|
|
54
|
+
leaves out the cartridge, the expansions and the tape.
|
|
55
|
+
|
|
56
|
+
Snapshots x64sc saves open in badline through the same modules: the CPU
|
|
57
|
+
at its instruction boundary, RAM, the CPU port, the CIAs' registers,
|
|
58
|
+
timers and clocks, the SID's registers and reSID voice state, and the
|
|
59
|
+
VIC-II's registers, beam position, counters and colour RAM. The VIC-II's
|
|
60
|
+
pixel pipeline starts empty. badline reads the modules x64sc 3.7 to 3.10
|
|
61
|
+
write, and VICE's development versions' `MAINC64CPU` and `VIC-IISC`. A
|
|
62
|
+
module version it doesn't know is left out, or fails the restore for the
|
|
63
|
+
CPU and RAM. It reports the modules it leaves out, such as the 1541
|
|
64
|
+
drives, the cartridge, the datasette and the keyboard, and carries on
|
|
65
|
+
without them. A snapshot of an NTSC machine fails, as badline reads only
|
|
66
|
+
VICE's PAL VIC-II. Restored into a running machine, a VICE snapshot
|
|
67
|
+
fails for a machine built another way, such as a C64C's snapshot in a
|
|
68
|
+
C64, and otherwise takes the cartridge out and switches the machine off
|
|
69
|
+
and on.
|
data/lib/badline/cartridge.rb
CHANGED
data/lib/badline/frontend/app.rb
CHANGED
|
@@ -7,8 +7,8 @@ module Badline
|
|
|
7
7
|
# changed lines, present and wait. Pacer decides the cycles and the wait.
|
|
8
8
|
class App
|
|
9
9
|
SCALE = 2
|
|
10
|
+
DROPFILE = 0x1000
|
|
10
11
|
TITLE = "Badline"
|
|
11
|
-
STAGES = %w[events emulate audio blit present wait].freeze
|
|
12
12
|
|
|
13
13
|
# Takes the frame limit, the pacing, the snapshot path, the sound and
|
|
14
14
|
# the verbosity from Options, and runs the timeline's events.
|
|
@@ -22,11 +22,11 @@ module Badline
|
|
|
22
22
|
@screen = Screen.new(computer.vic)
|
|
23
23
|
@led = DriveLed.for(computer)
|
|
24
24
|
@controls = Controls.new(computer)
|
|
25
|
-
@spent = Array.new(STAGES.size, 0.0)
|
|
26
|
-
@slowest = 0.0
|
|
27
25
|
open_window
|
|
28
26
|
@sound = Sound.new(computer.sid, options.sound?, @verbose)
|
|
29
27
|
@gamepads = Gamepads.new(computer, @verbose)
|
|
28
|
+
@frame_report = FrameReport.new(@sound)
|
|
29
|
+
@menu = PauseMenu.new(Painter.new(@renderer), options)
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def run
|
|
@@ -34,8 +34,7 @@ module Badline
|
|
|
34
34
|
@running = true
|
|
35
35
|
@started = @reported = now
|
|
36
36
|
@pacer.start(@started)
|
|
37
|
-
@
|
|
38
|
-
frame while @running
|
|
37
|
+
@menu.open? ? paused_frame : frame while @running
|
|
39
38
|
@snapshots.finish
|
|
40
39
|
@computer.drive1541&.flush
|
|
41
40
|
@gamepads.close
|
|
@@ -64,9 +63,7 @@ module Badline
|
|
|
64
63
|
end
|
|
65
64
|
|
|
66
65
|
def finish_frame(stamps)
|
|
67
|
-
|
|
68
|
-
took = stamps[-2] - stamps.first
|
|
69
|
-
@slowest = took if took > @slowest
|
|
66
|
+
@frame_report.add(stamps) if @verbose
|
|
70
67
|
@frames += 1
|
|
71
68
|
@timeline.run(@computer, @frames)
|
|
72
69
|
@running = false if @frames == @frame_limit || @timeline.quit?(@frames)
|
|
@@ -95,8 +92,6 @@ module Badline
|
|
|
95
92
|
@texture = SDL.SDL_CreateTexture(
|
|
96
93
|
@renderer, SDL::PIXELFORMAT_RGB888, SDL::TEXTUREACCESS_STREAMING, Screen::WIDTH, Screen::HEIGHT
|
|
97
94
|
)
|
|
98
|
-
SDL.rect_w(SDL.rect, Screen::WIDTH)
|
|
99
|
-
SDL.rect_h(SDL.rect, Screen::HEIGHT)
|
|
100
95
|
end
|
|
101
96
|
|
|
102
97
|
def close_window
|
|
@@ -107,7 +102,7 @@ module Badline
|
|
|
107
102
|
end
|
|
108
103
|
|
|
109
104
|
def handle_events
|
|
110
|
-
handle_event(SDL.event_type(SDL.event)) while SDL.SDL_PollEvent(SDL.event) != 0
|
|
105
|
+
handle_event(SDL.event_type(SDL.event)) while !@menu.open? && SDL.SDL_PollEvent(SDL.event) != 0
|
|
111
106
|
end
|
|
112
107
|
|
|
113
108
|
def handle_event(type)
|
|
@@ -122,6 +117,7 @@ module Badline
|
|
|
122
117
|
@controls.mouse_button(SDL.event_button(SDL.event), type == SDL::MOUSEBUTTONDOWN)
|
|
123
118
|
when SDL::CONTROLLERDEVICEADDED, SDL::CONTROLLERDEVICEREMOVED
|
|
124
119
|
@gamepads.rescan
|
|
120
|
+
when DROPFILE then resume(false) unless @menu.drop(@computer, @controls, @sound)
|
|
125
121
|
end
|
|
126
122
|
end
|
|
127
123
|
|
|
@@ -145,20 +141,45 @@ module Badline
|
|
|
145
141
|
@controls.computer = computer
|
|
146
142
|
@gamepads.computer = computer
|
|
147
143
|
@sound.sid = computer.sid
|
|
144
|
+
@snapshots.computer = computer
|
|
148
145
|
end
|
|
149
146
|
|
|
150
147
|
def handle_toggle(scancode)
|
|
151
148
|
if scancode == Keys::TAB
|
|
152
|
-
@controls.
|
|
153
|
-
SDL.SDL_SetRelativeMouseMode(@controls.pot_device? ? 1 : 0)
|
|
149
|
+
@controls.toggle_keys
|
|
154
150
|
elsif scancode == Keys::F9
|
|
155
|
-
|
|
151
|
+
return open_menu
|
|
156
152
|
else
|
|
157
153
|
@sound.toggle_mute
|
|
158
154
|
end
|
|
159
155
|
update_title
|
|
160
156
|
end
|
|
161
157
|
|
|
158
|
+
def open_menu = @menu.show(@computer, @controls, @sound)
|
|
159
|
+
|
|
160
|
+
# Closes the menu and runs the machine on from where it stood,
|
|
161
|
+
# pressing the cartridge's freeze button if asked.
|
|
162
|
+
def resume(freeze)
|
|
163
|
+
@menu.close
|
|
164
|
+
SDL.SDL_SetRelativeMouseMode(@controls.pot_device? ? 1 : 0)
|
|
165
|
+
@timeline.press_freeze(@computer, @frames) if freeze
|
|
166
|
+
@reported = now
|
|
167
|
+
@pacer.start(@reported)
|
|
168
|
+
update_title
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Runs a frame of the menu, which has the keys and the mouse while the
|
|
172
|
+
# machine stands still.
|
|
173
|
+
def paused_frame
|
|
174
|
+
action = @menu.frame(@renderer, @texture, @controls)
|
|
175
|
+
if action == :quit
|
|
176
|
+
@running = false
|
|
177
|
+
elsif !action.nil?
|
|
178
|
+
swap(@menu.computer) if action == :swap
|
|
179
|
+
resume(action == :freeze)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
162
183
|
def update_title
|
|
163
184
|
title = TITLE
|
|
164
185
|
title += " [#{@controls.tag}]" unless @controls.tag.empty?
|
|
@@ -183,12 +204,12 @@ module Badline
|
|
|
183
204
|
|
|
184
205
|
def upload
|
|
185
206
|
@screen.update
|
|
186
|
-
SDL.SDL_UpdateTexture(@texture,
|
|
207
|
+
SDL.SDL_UpdateTexture(@texture, nil, @screen.pixels, Screen::ROW_BYTES)
|
|
187
208
|
end
|
|
188
209
|
|
|
189
210
|
def draw
|
|
190
211
|
SDL.SDL_RenderClear(@renderer)
|
|
191
|
-
SDL.SDL_RenderCopy(@renderer, @texture,
|
|
212
|
+
SDL.SDL_RenderCopy(@renderer, @texture, nil, nil)
|
|
192
213
|
@led&.draw(@renderer)
|
|
193
214
|
@timeline.screenshots(@frames + 1).each { |path| Screenshot.write(@renderer, path) }
|
|
194
215
|
SDL.SDL_RenderPresent(@renderer)
|
|
@@ -197,28 +218,10 @@ module Badline
|
|
|
197
218
|
def report(at)
|
|
198
219
|
@pacer.check(50, at - @reported, at)
|
|
199
220
|
@pacer.measure(50, at - @reported)
|
|
200
|
-
|
|
201
|
-
@spent = Array.new(STAGES.size, 0.0)
|
|
202
|
-
@slowest = 0.0
|
|
221
|
+
@frame_report.show(at - @reported) if @verbose
|
|
203
222
|
@reported = at
|
|
204
223
|
end
|
|
205
224
|
|
|
206
|
-
def report_frames(at)
|
|
207
|
-
fps = 50 / (at - @reported)
|
|
208
|
-
stages = STAGES.each_with_index.map { |name, stage| "#{name} #{(@spent[stage] * 20).round(2)}" }
|
|
209
|
-
puts "#{fps.round(1)} fps, per frame ms: #{stages.join(' ')}, slowest work #{(@slowest * 1000).round(2)}"
|
|
210
|
-
report_sound(at) if @sound.on?
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
def report_sound(at)
|
|
214
|
-
sound = @sound
|
|
215
|
-
rate = (sound.queued - @reported_samples) / (at - @reported)
|
|
216
|
-
queue = sound.high.zero? ? "empty" : "#{(sound.low * 1000).round(1)}-#{(sound.high * 1000).round(1)} ms"
|
|
217
|
-
puts " sound #{rate.round} samples/s, queue #{queue}, #{sound.underruns} underruns, #{sound.dropped} dropped"
|
|
218
|
-
sound.reset_levels
|
|
219
|
-
@reported_samples = sound.queued
|
|
220
|
-
end
|
|
221
|
-
|
|
222
225
|
def now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
223
226
|
end
|
|
224
227
|
end
|
|
@@ -39,12 +39,19 @@ module Badline
|
|
|
39
39
|
return computer
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
start(options, media, options.writable?)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# A new machine built from the options, with `media`, if not nil,
|
|
46
|
+
# attached and started as the command line does, its disk writable if
|
|
47
|
+
# `writable`.
|
|
48
|
+
def self.start(options, media, writable)
|
|
42
49
|
computer = Computer.new(sid_model: options.sid_model || Media.sid_model(media), reu: options.reu,
|
|
43
50
|
region: options.ntsc? ? Region::NTSC : Region::PAL)
|
|
44
51
|
Media::TrueDrive.plug(computer) if options.true_drive?
|
|
45
52
|
unless media.nil?
|
|
46
53
|
puts Media.attach(computer, media, autostart: options.autostart?, subtune: options.subtune,
|
|
47
|
-
disk: { read_only:
|
|
54
|
+
disk: { read_only: !writable })
|
|
48
55
|
end
|
|
49
56
|
computer
|
|
50
57
|
end
|
|
@@ -5,27 +5,43 @@ module Badline
|
|
|
5
5
|
# The SID player's buttons: each is drawn where it goes on every frame,
|
|
6
6
|
# and remembers its place, so a click finds the action under it. The
|
|
7
7
|
# one under the pointer lights up, and one that's on is drawn reversed.
|
|
8
|
+
# They take the player's colours unless given others: the text, the text
|
|
9
|
+
# under the pointer, the background and, if any, a fill behind each
|
|
10
|
+
# button.
|
|
8
11
|
class Buttons
|
|
9
12
|
PAD = 2
|
|
10
13
|
HEIGHT = Painter::GLYPH + (PAD * 2)
|
|
11
14
|
|
|
12
|
-
def initialize(painter)
|
|
15
|
+
def initialize(painter, colors = [SIDView::TEXT, SIDView::BRIGHT, PlayerScreen::BACKGROUND])
|
|
13
16
|
@painter = painter
|
|
17
|
+
@text = colors[0]
|
|
18
|
+
@bright = colors[1]
|
|
19
|
+
@back = colors[2]
|
|
20
|
+
@fill = colors.size > 3 ? colors[3] : -1
|
|
14
21
|
@lefts = []
|
|
15
22
|
@tops = []
|
|
16
23
|
@widths = []
|
|
17
24
|
@heights = []
|
|
18
25
|
@actions = []
|
|
26
|
+
@focusable = []
|
|
27
|
+
@toggles = {}
|
|
19
28
|
@pointer_x = -1
|
|
20
29
|
@pointer_y = -1
|
|
30
|
+
@focus = nil
|
|
21
31
|
end
|
|
22
32
|
|
|
33
|
+
# The action of the button the keys have moved to, which lights up as
|
|
34
|
+
# the one under the pointer does, or nil.
|
|
35
|
+
attr_accessor :focus
|
|
36
|
+
|
|
23
37
|
def forget
|
|
24
38
|
@lefts.clear
|
|
25
39
|
@tops.clear
|
|
26
40
|
@widths.clear
|
|
27
41
|
@heights.clear
|
|
28
42
|
@actions.clear
|
|
43
|
+
@focusable.clear
|
|
44
|
+
@toggles.clear
|
|
29
45
|
end
|
|
30
46
|
|
|
31
47
|
def point(left, top)
|
|
@@ -38,11 +54,54 @@ module Badline
|
|
|
38
54
|
def text(left, top, label, action, on: false)
|
|
39
55
|
width = Painter.width(label) + (PAD * 2)
|
|
40
56
|
place(left, top, width, HEIGHT, action)
|
|
41
|
-
@painter.box(left, top, width, HEIGHT,
|
|
42
|
-
@painter.
|
|
57
|
+
@painter.box(left, top, width, HEIGHT, @fill) if !on && @fill >= 0
|
|
58
|
+
@painter.box(left, top, width, HEIGHT, action == @focus ? @bright : @text) if on
|
|
59
|
+
@painter.text(left + PAD, top + PAD, label, on ? @back : color(@actions.size - 1))
|
|
43
60
|
width
|
|
44
61
|
end
|
|
45
62
|
|
|
63
|
+
# Draws a button as #text does, without the fill.
|
|
64
|
+
def plain(left, top, label, action, on: false)
|
|
65
|
+
fill = @fill
|
|
66
|
+
@fill = -1
|
|
67
|
+
width = text(left, top, label, action, on:)
|
|
68
|
+
@fill = fill
|
|
69
|
+
width
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Draws a button as wide as `box`, its left and top and width, with
|
|
73
|
+
# `label` at its left and `value`, if any, at its right, as a menu's
|
|
74
|
+
# row.
|
|
75
|
+
def row(box, label, action, value = "")
|
|
76
|
+
left = box[0]
|
|
77
|
+
top = box[1]
|
|
78
|
+
width = box[2]
|
|
79
|
+
place(left, top, width, HEIGHT, action)
|
|
80
|
+
@painter.box(left, top, width, HEIGHT, @fill) if @fill >= 0
|
|
81
|
+
color = color(@actions.size - 1)
|
|
82
|
+
@painter.text(left + PAD, top + PAD, label, color)
|
|
83
|
+
@painter.text(left + width - PAD - Painter.width(value), top + PAD, value, color) unless value.empty?
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Draws a row as #row does, with its choices at its right, `choices`
|
|
87
|
+
# holding their labels, their actions and the index of the one on.
|
|
88
|
+
# A click picks a choice, and pressing the row, `action`, picks the
|
|
89
|
+
# next, which #resolve gives.
|
|
90
|
+
def toggle(box, label, action, choices)
|
|
91
|
+
row(box, label, action)
|
|
92
|
+
labels = choices[0]
|
|
93
|
+
@toggles[action] = choices[1][(choices[2] + 1) % labels.size]
|
|
94
|
+
right = box[0] + box[2] - PAD
|
|
95
|
+
index = labels.size - 1
|
|
96
|
+
while index >= 0
|
|
97
|
+
width = Painter.width(labels[index]) + (PAD * 2)
|
|
98
|
+
right -= width
|
|
99
|
+
choice(right, box[1], labels[index], choices[1][index], index == choices[2])
|
|
100
|
+
right -= 2
|
|
101
|
+
index -= 1
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
46
105
|
# Draws an icon button `scale` times the size of a glyph, and returns
|
|
47
106
|
# its width.
|
|
48
107
|
def icon(left, top, name, action, scale: 1)
|
|
@@ -58,17 +117,42 @@ module Badline
|
|
|
58
117
|
place(box[0], box[1], box[2], box[3], action)
|
|
59
118
|
end
|
|
60
119
|
|
|
61
|
-
# The action of the button at `left`, `top`, or nil
|
|
120
|
+
# The action of the button at `left`, `top`, or nil: a toggle's
|
|
121
|
+
# choice before the row it sits on.
|
|
62
122
|
def action_at(left, top)
|
|
123
|
+
found = nil
|
|
63
124
|
index = 0
|
|
64
125
|
while index < @actions.size
|
|
65
|
-
|
|
126
|
+
if inside?(index, left, top)
|
|
127
|
+
return @actions[index] unless @focusable[index]
|
|
66
128
|
|
|
129
|
+
found ||= @actions[index]
|
|
130
|
+
end
|
|
67
131
|
index += 1
|
|
68
132
|
end
|
|
69
|
-
|
|
133
|
+
found
|
|
70
134
|
end
|
|
71
135
|
|
|
136
|
+
# Moves the focus `delta` buttons on, among those drawn from the
|
|
137
|
+
# `first` on, stopping at either end.
|
|
138
|
+
def shift(delta, first)
|
|
139
|
+
return if @actions.size <= first
|
|
140
|
+
|
|
141
|
+
focusable = []
|
|
142
|
+
(first...@actions.size).each { |index| focusable << @actions[index] if @focusable[index] }
|
|
143
|
+
return if focusable.empty?
|
|
144
|
+
|
|
145
|
+
from = focusable.index(@focus)
|
|
146
|
+
@focus = focusable[from.nil? ? 0 : (from + delta).clamp(0, focusable.size - 1)]
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# The action pressing a button stands for: for a toggle's row, its
|
|
150
|
+
# next choice's.
|
|
151
|
+
def resolve(action) = @toggles.fetch(action, action)
|
|
152
|
+
|
|
153
|
+
# The action of the button drawn `index`th, or nil.
|
|
154
|
+
def action(index) = @actions[index]
|
|
155
|
+
|
|
72
156
|
private
|
|
73
157
|
|
|
74
158
|
def place(left, top, width, height, action)
|
|
@@ -77,9 +161,19 @@ module Badline
|
|
|
77
161
|
@widths << width
|
|
78
162
|
@heights << height
|
|
79
163
|
@actions << action
|
|
164
|
+
@focusable << true
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# A choice of a toggle, which a click picks but the keys pass by.
|
|
168
|
+
def choice(left, top, label, action, on)
|
|
169
|
+
width = Painter.width(label) + (PAD * 2)
|
|
170
|
+
place(left, top, width, HEIGHT, action)
|
|
171
|
+
@focusable[-1] = false
|
|
172
|
+
@painter.box(left, top, width, HEIGHT, @text) if on
|
|
173
|
+
@painter.text(left + PAD, top + PAD, label, on ? @back : color(@actions.size - 1))
|
|
80
174
|
end
|
|
81
175
|
|
|
82
|
-
def color(index) = inside?(index, @pointer_x, @pointer_y) ?
|
|
176
|
+
def color(index) = inside?(index, @pointer_x, @pointer_y) || @actions[index] == @focus ? @bright : @text
|
|
83
177
|
|
|
84
178
|
def inside?(index, left, top)
|
|
85
179
|
left >= @lefts[index] && left < @lefts[index] + @widths[index] &&
|