badline 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +77 -0
- data/CLAUDE.md +10 -9
- data/README.md +135 -42
- data/doc/pinned-behaviour.md +131 -30
- data/exe/badline-ruby +28 -24
- data/lib/badline/address_bus/extra_sids.rb +30 -0
- data/lib/badline/address_bus/sid_slots.rb +42 -0
- data/lib/badline/address_bus.rb +15 -1
- data/lib/badline/audio/aiff.rb +2 -2
- data/lib/badline/audio/bare_player.rb +23 -12
- data/lib/badline/audio/cli.rb +167 -59
- data/lib/badline/audio/console.rb +2 -1
- data/lib/badline/audio/jukebox.rb +121 -36
- data/lib/badline/audio/machine_player.rb +25 -10
- data/lib/badline/audio/pcm_writer.rb +10 -6
- data/lib/badline/audio/playback.rb +6 -5
- data/lib/badline/audio/queued_tune.rb +89 -0
- data/lib/badline/audio/renderer.rb +57 -10
- data/lib/badline/audio/stereo.rb +97 -0
- data/lib/badline/audio/terminal.rb +39 -6
- data/lib/badline/audio/wav.rb +1 -1
- data/lib/badline/audio.rb +2 -3
- data/lib/badline/computer.rb +6 -3
- data/lib/badline/cpu/boundary.rb +54 -0
- data/lib/badline/cpu/core.rb +165 -0
- data/lib/badline/cpu.rb +3 -214
- data/lib/badline/drive1541/cpu.rb +29 -0
- data/lib/badline/drive1541.rb +1 -0
- data/lib/badline/ffi.rb +157 -0
- data/lib/badline/frontend/app.rb +225 -0
- data/lib/badline/frontend/audio_sink.rb +73 -0
- data/lib/badline/frontend/boot.rb +52 -0
- data/lib/badline/frontend/buttons.rb +90 -0
- data/lib/badline/frontend/controls.rb +163 -0
- data/lib/badline/frontend/drive_led.rb +51 -0
- data/lib/badline/frontend/filter_view.rb +96 -0
- data/lib/badline/frontend/frame_rate.rb +83 -0
- data/lib/badline/frontend/gamepads.rb +76 -0
- data/lib/badline/frontend/info_view.rb +120 -0
- data/lib/badline/frontend/keys.rb +48 -0
- data/lib/badline/frontend/pacer.rb +100 -0
- data/lib/badline/frontend/pad_port.rb +61 -0
- data/lib/badline/frontend/painter.rb +175 -0
- data/lib/badline/frontend/player_commands.rb +33 -0
- data/lib/badline/frontend/player_footer.rb +86 -0
- data/lib/badline/frontend/player_header.rb +60 -0
- data/lib/badline/frontend/player_screen.rb +124 -0
- data/lib/badline/frontend/player_state.rb +67 -0
- data/lib/badline/frontend/player_window.rb +228 -0
- data/lib/badline/frontend/scope.rb +42 -0
- data/lib/badline/frontend/screen.rb +65 -0
- data/lib/badline/frontend/screenshot.rb +22 -0
- data/lib/badline/frontend/sid_history.rb +175 -0
- data/lib/badline/frontend/sid_view.rb +140 -0
- data/lib/badline/frontend/snapshots.rb +60 -0
- data/lib/badline/frontend/sound.rb +153 -0
- data/lib/badline/frontend/timeline.rb +171 -0
- data/lib/badline/frontend/visualizer_view.rb +101 -0
- data/lib/badline/frontend.rb +32 -0
- data/lib/badline/media/queue.rb +158 -0
- data/lib/badline/media.rb +13 -8
- data/lib/badline/options/event.rb +94 -0
- data/lib/badline/options/help.rb +107 -0
- data/lib/badline/options/option.rb +29 -0
- data/lib/badline/options/table.rb +64 -0
- data/lib/badline/options/validation.rb +54 -0
- data/lib/badline/options.rb +204 -149
- data/lib/badline/reu/dma/saved_state.rb +50 -0
- data/lib/badline/reu/dma.rb +56 -47
- data/lib/badline/reu/saved_state.rb +2 -1
- data/lib/badline/reu.rb +19 -10
- data/lib/badline/sdl.rb +133 -51
- data/lib/badline/sid/catch_up.rb +11 -1
- data/lib/badline/sid/filter.rb +9 -0
- data/lib/badline/sid/voice.rb +6 -0
- data/lib/badline/sid/waveform/noise_writeback.rb +33 -11
- data/lib/badline/sid/waveform.rb +22 -8
- data/lib/badline/sid.rb +35 -3
- data/lib/badline/snapshot/state.rb +1 -1
- data/lib/badline/storage/hvsc.rb +41 -0
- data/lib/badline/storage/sid_file.rb +45 -25
- data/lib/badline/storage/song_lengths.rb +6 -29
- data/lib/badline/storage/stil/credit.rb +50 -0
- data/lib/badline/storage/stil.rb +126 -0
- data/lib/badline/storage.rb +8 -0
- data/lib/badline/version.rb +1 -1
- data/lib/badline/vic/bank.rb +21 -0
- data/lib/badline/vic.rb +15 -6
- metadata +49 -19
- data/.claude/hooks/session-start.sh +0 -57
- data/.claude/settings.json +0 -14
- data/lib/badline/audio/sdl_sink.rb +0 -70
- data/lib/badline/audio/stream.rb +0 -123
- data/lib/badline/gui/application.rb +0 -243
- data/lib/badline/gui/drive_led_pane.rb +0 -29
- data/lib/badline/gui/gamepads.rb +0 -104
- data/lib/badline/gui/joy_map.rb +0 -27
- data/lib/badline/gui/key_map.rb +0 -39
- data/lib/badline/gui/palette.rb +0 -35
- data/lib/badline/gui/pane.rb +0 -36
- data/lib/badline/gui/screen_pane.rb +0 -46
- data/lib/badline/gui/snapshots.rb +0 -67
- data/lib/badline/gui/window.rb +0 -61
- data/lib/badline/gui.rb +0 -14
- data/lib/badline/sdl/events.rb +0 -49
- data/lib/badline/sdl/functions.rb +0 -74
- data/vendor/.gitkeep +0 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Media
|
|
5
|
+
# An ordered list of media files to go through one at a time, such as
|
|
6
|
+
# the tunes of a player. Each entry is a file and the part of it to
|
|
7
|
+
# start on, counting from 1, out of the parts the file holds: a .sid
|
|
8
|
+
# tune's subtunes, say, or 1 of 1 for media with no parts.
|
|
9
|
+
#
|
|
10
|
+
# #next_entry and #previous_entry step through the entries, and
|
|
11
|
+
# #next_part and #previous_part through the current entry's parts,
|
|
12
|
+
# stopping at its ends. Each returns the entry it moved to, or nil
|
|
13
|
+
# where it stays put. With #loop? the entry steps wrap around at
|
|
14
|
+
# either end, and with #shuffle? they follow a shuffled order that
|
|
15
|
+
# starts from the current entry. #all_parts? decides where #advance
|
|
16
|
+
# goes once a part has played out: on through the entry's parts, or
|
|
17
|
+
# to the next entry. An entry stepped to starts on its first part
|
|
18
|
+
# with #all_parts?, otherwise on its own.
|
|
19
|
+
#
|
|
20
|
+
# `shuffle`, if given, returns the entry indices it's given in a new
|
|
21
|
+
# order in place of Array#shuffle. `build`, if given, turns the paths
|
|
22
|
+
# #add is given into entries.
|
|
23
|
+
class Queue
|
|
24
|
+
# A file and the part of it to start on.
|
|
25
|
+
class Entry
|
|
26
|
+
attr_reader :path, :part, :parts
|
|
27
|
+
|
|
28
|
+
def initialize(path, part: 1, parts: 1)
|
|
29
|
+
@path = path
|
|
30
|
+
@part = part
|
|
31
|
+
@parts = parts
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
attr_reader :part
|
|
36
|
+
|
|
37
|
+
# The files `paths` name, in order: a file as given, and a directory
|
|
38
|
+
# as every file below it with the extension, in path order.
|
|
39
|
+
def self.files(paths, extension)
|
|
40
|
+
paths.flat_map { |path| File.directory?(path) ? below(path, extension).sort : [path] }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.below(directory, extension)
|
|
44
|
+
paths = []
|
|
45
|
+
Dir.children(directory).each do |name|
|
|
46
|
+
path = File.join(directory, name)
|
|
47
|
+
if File.directory?(path)
|
|
48
|
+
paths.concat(below(path, extension))
|
|
49
|
+
elsif File.extname(name).casecmp?(extension)
|
|
50
|
+
paths << path
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
paths
|
|
54
|
+
end
|
|
55
|
+
private_class_method :below
|
|
56
|
+
|
|
57
|
+
def initialize(entries, all_parts: false, shuffle: nil, build: nil)
|
|
58
|
+
@entries = entries
|
|
59
|
+
@shuffler = shuffle
|
|
60
|
+
@builder = build
|
|
61
|
+
@order = (0...entries.size).to_a
|
|
62
|
+
@position = 0
|
|
63
|
+
@part = entries.empty? ? 0 : entries.first.part
|
|
64
|
+
@all_parts = all_parts
|
|
65
|
+
@shuffle = false
|
|
66
|
+
@loop = false
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def size = @entries.size
|
|
70
|
+
|
|
71
|
+
def empty? = @entries.empty?
|
|
72
|
+
|
|
73
|
+
# Where the current entry stands in the order played, from 1.
|
|
74
|
+
def position = empty? ? 0 : @position + 1
|
|
75
|
+
|
|
76
|
+
def entry = empty? ? nil : @entries[@order[@position]]
|
|
77
|
+
|
|
78
|
+
def all_parts? = @all_parts
|
|
79
|
+
|
|
80
|
+
def shuffle? = @shuffle
|
|
81
|
+
|
|
82
|
+
def loop? = @loop
|
|
83
|
+
|
|
84
|
+
# Puts the entries `build` makes of the paths at the end of the order,
|
|
85
|
+
# shuffled or not. An empty queue starts on the first of them.
|
|
86
|
+
def add(paths)
|
|
87
|
+
return if @builder.nil?
|
|
88
|
+
|
|
89
|
+
started = empty?
|
|
90
|
+
first = @entries.size
|
|
91
|
+
@entries.concat(@builder.call(paths))
|
|
92
|
+
@order.concat((first...@entries.size).to_a)
|
|
93
|
+
@part = entry.part if started && !empty?
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def toggle_all_parts = @all_parts = !@all_parts
|
|
97
|
+
|
|
98
|
+
def toggle_loop = @loop = !@loop
|
|
99
|
+
|
|
100
|
+
def toggle_shuffle
|
|
101
|
+
return @shuffle = !@shuffle if empty?
|
|
102
|
+
|
|
103
|
+
current = @order[@position]
|
|
104
|
+
@shuffle = !@shuffle
|
|
105
|
+
@order = (0...size).to_a
|
|
106
|
+
@order = [current] + shuffled(@order - [current]) if @shuffle
|
|
107
|
+
@position = @order.index(current)
|
|
108
|
+
@shuffle
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def next_entry
|
|
112
|
+
return if empty?
|
|
113
|
+
|
|
114
|
+
move(@position + 1)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def previous_entry
|
|
118
|
+
return if empty?
|
|
119
|
+
|
|
120
|
+
move(@position - 1)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def next_part
|
|
124
|
+
return if empty? || @part >= entry.parts
|
|
125
|
+
|
|
126
|
+
@part += 1
|
|
127
|
+
entry
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def previous_part
|
|
131
|
+
return if empty? || @part <= 1
|
|
132
|
+
|
|
133
|
+
@part -= 1
|
|
134
|
+
entry
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Where to go once the current part has played to its end: the
|
|
138
|
+
# entry's next part with #all_parts?, otherwise the next entry.
|
|
139
|
+
def advance
|
|
140
|
+
return next_part if @all_parts && !empty? && @part < entry.parts
|
|
141
|
+
|
|
142
|
+
next_entry
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
private
|
|
146
|
+
|
|
147
|
+
def move(position)
|
|
148
|
+
return unless position.between?(0, size - 1) || @loop
|
|
149
|
+
|
|
150
|
+
@position = position % size
|
|
151
|
+
@part = @all_parts ? 1 : entry.part
|
|
152
|
+
entry
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def shuffled(indices) = @shuffler ? @shuffler.call(indices) : indices.shuffle
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
data/lib/badline/media.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "badline/media/true_drive"
|
|
4
|
+
require "badline/media/queue"
|
|
4
5
|
|
|
5
6
|
module Badline
|
|
6
7
|
module Media
|
|
@@ -25,7 +26,7 @@ module Badline
|
|
|
25
26
|
# A .g64 plugs in a true drive as device 8 (TrueDrive), and with one
|
|
26
27
|
# there, a .d64 goes into it instead of the KERNAL traps too. The
|
|
27
28
|
# autostart then loads through it.
|
|
28
|
-
def attach(computer, path, autostart: true,
|
|
29
|
+
def attach(computer, path, autostart: true, subtune: nil, **options)
|
|
29
30
|
if TrueDrive.takes?(computer, path)
|
|
30
31
|
attach_true_drive(computer, path, options.fetch(:disk, {}), autostart:)
|
|
31
32
|
elsif File.directory?(path)
|
|
@@ -34,7 +35,7 @@ module Badline
|
|
|
34
35
|
elsif File.extname(path).downcase == ".crt"
|
|
35
36
|
attach_cartridge(computer, path, options.fetch(:cartridge, {}))
|
|
36
37
|
elsif File.extname(path).downcase == ".sid"
|
|
37
|
-
attach_sid(computer, path, autostart:,
|
|
38
|
+
attach_sid(computer, path, autostart:, subtune:)
|
|
38
39
|
elsif File.extname(path).downcase == ".tap"
|
|
39
40
|
attach_tape(computer, path, autostart:)
|
|
40
41
|
elsif MOUNT_TYPES.key?(File.extname(path).downcase)
|
|
@@ -91,18 +92,22 @@ module Badline
|
|
|
91
92
|
"Attached cartridge #{path}"
|
|
92
93
|
end
|
|
93
94
|
|
|
94
|
-
def attach_sid(computer, path, autostart:,
|
|
95
|
+
def attach_sid(computer, path, autostart:, subtune:)
|
|
95
96
|
tune = Storage::SIDFile.new(path)
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
if tune.sids > 1
|
|
98
|
+
raise Storage::SIDFile::FormatError, "Written for #{tune.sids} SIDs, which only the SID player plays"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
subtune = (subtune || tune.start_subtune).clamp(1, tune.subtunes)
|
|
102
|
+
computer.on_init { start_tune(computer, tune, autostart:, subtune:) }
|
|
98
103
|
title = tune.name.empty? ? path : tune.name
|
|
99
|
-
title += " (
|
|
104
|
+
title += " (subtune #{subtune})" if tune.subtunes > 1
|
|
100
105
|
autostart ? "Playing #{title}" : "Loaded #{title}"
|
|
101
106
|
end
|
|
102
107
|
|
|
103
|
-
def start_tune(computer, tune, autostart:,
|
|
108
|
+
def start_tune(computer, tune, autostart:, subtune:)
|
|
104
109
|
computer.ram.write(tune.load_address, tune.data)
|
|
105
|
-
tune.boot_memory(
|
|
110
|
+
tune.boot_memory(subtune:).each { |address, bytes| computer.ram.write(address, bytes) }
|
|
106
111
|
computer.type_text(tune.boot_command) if autostart
|
|
107
112
|
end
|
|
108
113
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Options
|
|
5
|
+
# One event of --at or --script: what to do, with its argument, once
|
|
6
|
+
# `frame` frames have run. An event given at several frames becomes
|
|
7
|
+
# one Event per frame.
|
|
8
|
+
class Event
|
|
9
|
+
# The actions, and whether each takes an argument.
|
|
10
|
+
ACTIONS = {
|
|
11
|
+
"key" => true, "type" => true, "insert" => true, "eject" => true, "screenshot" => true,
|
|
12
|
+
"reset" => false, "freeze" => false, "quit" => false
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
# The frames a key or the freeze button is held for.
|
|
16
|
+
HOLD = 5
|
|
17
|
+
|
|
18
|
+
EJECTABLE = %w[disk tape cartridge].freeze
|
|
19
|
+
|
|
20
|
+
# What insert takes: disk images, tapes and cartridges, or a
|
|
21
|
+
# directory to mount as device 8.
|
|
22
|
+
INSERTABLE = %w[.d64 .d71 .d81 .g64 .t64 .tap .crt].freeze
|
|
23
|
+
|
|
24
|
+
attr_reader :frame, :action, :argument
|
|
25
|
+
|
|
26
|
+
def initialize(frame, action, argument)
|
|
27
|
+
@frame = frame
|
|
28
|
+
@action = action
|
|
29
|
+
@argument = argument
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# The events of --at's value, or of --script's file.
|
|
33
|
+
def self.given(flag, value)
|
|
34
|
+
return script(value) if flag == "--script"
|
|
35
|
+
|
|
36
|
+
events = parse(value)
|
|
37
|
+
raise Error, "invalid argument: #{flag} #{value}" if events.empty?
|
|
38
|
+
|
|
39
|
+
events
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The events of a --script file, one a line in --at's form. Blank
|
|
43
|
+
# lines and lines starting with # are skipped.
|
|
44
|
+
def self.script(path)
|
|
45
|
+
raise Error, "no such file: #{path}" unless File.file?(path)
|
|
46
|
+
|
|
47
|
+
events = []
|
|
48
|
+
File.foreach(path) do |line|
|
|
49
|
+
text = line.strip
|
|
50
|
+
events.concat(given("--script #{path}:", text)) unless text.empty? || text.start_with?("#")
|
|
51
|
+
end
|
|
52
|
+
events
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# The events of `FRAME[,FRAME...]:ACTION[=ARGUMENT]`, or an empty list
|
|
56
|
+
# when the text isn't one.
|
|
57
|
+
def self.parse(text)
|
|
58
|
+
colon = text.index(":")
|
|
59
|
+
numbers = colon.nil? ? [] : frames(text[0, colon])
|
|
60
|
+
return [] if numbers.empty?
|
|
61
|
+
|
|
62
|
+
event = text[colon + 1, text.size - colon - 1]
|
|
63
|
+
equals = event.index("=")
|
|
64
|
+
action = equals.nil? ? event : event[0, equals]
|
|
65
|
+
argument = equals.nil? ? "" : event[equals + 1, event.size - equals - 1]
|
|
66
|
+
return [] unless valid?(action, argument)
|
|
67
|
+
|
|
68
|
+
numbers.map { |frame| new(frame, action, argument) }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# The frame numbers of `FRAME[,FRAME...]`, or none unless each is a
|
|
72
|
+
# positive number.
|
|
73
|
+
def self.frames(text)
|
|
74
|
+
numbers = text.split(",")
|
|
75
|
+
return [] unless !numbers.empty? && numbers.all? { |number| number.match?(/\A\d+\z/) && number.to_i.positive? }
|
|
76
|
+
|
|
77
|
+
numbers.map(&:to_i)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def self.valid?(action, argument)
|
|
81
|
+
return false unless ACTIONS.key?(action)
|
|
82
|
+
return argument.empty? unless ACTIONS[action]
|
|
83
|
+
return EJECTABLE.include?(argument) if action == "eject"
|
|
84
|
+
return insertable?(argument) if action == "insert"
|
|
85
|
+
|
|
86
|
+
!argument.empty?
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def self.insertable?(path)
|
|
90
|
+
File.directory?(path) || (File.exist?(path) && INSERTABLE.include?(File.extname(path).downcase))
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Options
|
|
5
|
+
# How fast the headless player runs, which the builds' help tells apart.
|
|
6
|
+
NATIVE_SPEED = <<~TEXT.chomp
|
|
7
|
+
Without the window, PSID tunes run on a bare CPU and SID, and RSID
|
|
8
|
+
tunes boot the whole machine. --filter-chunk 1 runs the filter
|
|
9
|
+
cycle by cycle, which is exact but slower.
|
|
10
|
+
TEXT
|
|
11
|
+
|
|
12
|
+
RUBY_SPEED = <<~TEXT.chomp
|
|
13
|
+
Without the window, PSID tunes run on a bare CPU and SID, faster than
|
|
14
|
+
real time. RSID tunes boot the whole machine and run at about half
|
|
15
|
+
real time, so they stutter when played. --filter-chunk 1 runs the
|
|
16
|
+
filter cycle by cycle, which is exact but takes about twice as long.
|
|
17
|
+
TEXT
|
|
18
|
+
|
|
19
|
+
# What --help prints: the usage, then the options this build takes,
|
|
20
|
+
# section by section, laid out as OptionParser did.
|
|
21
|
+
def help
|
|
22
|
+
lines = [@sid_command ? sid_banner : banner]
|
|
23
|
+
SECTIONS.each do |section|
|
|
24
|
+
options = TABLE.select { |option| option.section == section && takes?(option) }
|
|
25
|
+
next if options.empty?
|
|
26
|
+
|
|
27
|
+
lines << "" if lines.size > 1
|
|
28
|
+
lines << "#{section}:"
|
|
29
|
+
options.each { |option| lines << " #{option.usage.ljust(32)} #{describe(option)}" }
|
|
30
|
+
end
|
|
31
|
+
"#{lines.join("\n")}\n"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def describe(option) = @native && option.name == "--sound" ? "#{option.text} (default)" : option.text
|
|
37
|
+
|
|
38
|
+
def banner
|
|
39
|
+
<<~BANNER
|
|
40
|
+
Usage: #{@program} [options] [media]
|
|
41
|
+
#{@program} --headless [options] tune.sid
|
|
42
|
+
#{@program} [options] tune.sid --audio-out FILE
|
|
43
|
+
#{@program} sid [options] FILE|DIR...
|
|
44
|
+
|
|
45
|
+
Media can be a .prg/.p00 program, a .d64/.d71/.d81 disk image, a
|
|
46
|
+
.g64 disk image for the true 1541, a .t64 tape archive, a .tap
|
|
47
|
+
tape, a .crt cartridge, a .sid tune, a .vsf snapshot, or a directory
|
|
48
|
+
to mount as device 8. It opens in the emulator window, where F11
|
|
49
|
+
saves a snapshot and F12 restores it. Device 8 answers through
|
|
50
|
+
traps on the KERNAL's disk routines, unless --true-drive puts an
|
|
51
|
+
emulated 1541 there, which runs its own DOS and reads .d64 and .g64
|
|
52
|
+
images only. A .sid tune opens in the SID player, as `sid` plays it,
|
|
53
|
+
unless an option of the emulator's window asks for the machine.
|
|
54
|
+
|
|
55
|
+
--headless plays a .sid tune on the host's audio device without the
|
|
56
|
+
window, and --audio-out renders it to 16-bit PCM instead. The
|
|
57
|
+
container follows the file's extension, .wav or .aiff. Played on a
|
|
58
|
+
terminal, → and ← step between the tune's subtunes, n and p between
|
|
59
|
+
tunes, , and . seek, space pauses and q quits. s shuffles the tunes,
|
|
60
|
+
l loops them, and a, or --all-subtunes, plays on through each tune's
|
|
61
|
+
subtunes instead of just the one. `#{@program} sid` plays many tunes, and whole
|
|
62
|
+
directories of them, the same way.
|
|
63
|
+
|
|
64
|
+
A .sid file carries no length of its own. Without --seconds the tune is
|
|
65
|
+
looked up by MD5 in HVSC's Songlengths.md5, taken from --songlengths, from
|
|
66
|
+
the DOCUMENTS directory of an HVSC collection above the tune, or from
|
|
67
|
+
$HVSC_BASE. Failing all of those it runs for #{FALLBACK_SECONDS.to_i} seconds, or
|
|
68
|
+
until it has been silent for #{SILENCE_SECONDS.to_i}.
|
|
69
|
+
|
|
70
|
+
#{@native ? NATIVE_SPEED : RUBY_SPEED}
|
|
71
|
+
|
|
72
|
+
--at and --script run events once a number of frames has run, as
|
|
73
|
+
in --at 3500:key=space or --at 250,500:screenshot=shot%05d.bmp.
|
|
74
|
+
key=NAME presses a C64 key (space, return, a, f1, run_stop, restore)
|
|
75
|
+
or a joystick's direction or fire (joy1-up, joy2-fire) for #{Event::HOLD}
|
|
76
|
+
frames. type=TEXT types it, with \\n for RETURN. insert=FILE swaps
|
|
77
|
+
in a disk, tape or cartridge, and eject=disk, tape or cartridge
|
|
78
|
+
takes one out. screenshot=FILE saves the frame as a .bmp, with the
|
|
79
|
+
frame's number in place of %d. reset, freeze and quit take no
|
|
80
|
+
argument.
|
|
81
|
+
BANNER
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def sid_banner
|
|
85
|
+
<<~BANNER
|
|
86
|
+
Usage: #{@program} sid [options] FILE|DIR...
|
|
87
|
+
|
|
88
|
+
Plays .sid tunes in the SID player's window, or in the terminal with
|
|
89
|
+
--headless, one after another in the order given. A directory adds
|
|
90
|
+
every .sid tune below it, in path order. → and ← step between a
|
|
91
|
+
tune's subtunes, n and p between tunes, , and . seek 10 seconds back
|
|
92
|
+
and forward, space pauses and q quits. s shuffles the tunes, l loops
|
|
93
|
+
them, and a, or --all-subtunes, plays on through each tune's subtunes
|
|
94
|
+
instead of just the one. The window has buttons for all of these,
|
|
95
|
+
and for switching the SID between the tune's own, the 6581 and the
|
|
96
|
+
8580. --subtune picks the first tune's subtune, and each tune after
|
|
97
|
+
it starts on its own. Without tunes, the window opens with an empty
|
|
98
|
+
queue, and .sid files or folders dropped on the window join it.
|
|
99
|
+
|
|
100
|
+
Each tune gets the SID its header names unless --sid says otherwise,
|
|
101
|
+
and its length from HVSC's Songlengths.md5 as #{@program} --help
|
|
102
|
+
describes. A tune written for 2 or 3 SIDs plays on the one badline
|
|
103
|
+
emulates, with a notice.
|
|
104
|
+
BANNER
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Options
|
|
5
|
+
# One option of the table: its flags and value as the help shows them
|
|
6
|
+
# (`-s, --subtune N`), what it does, the help section it's listed in, the
|
|
7
|
+
# mode it needs (:window, :headless or :either), and the build that
|
|
8
|
+
# takes it (:native, :ruby or :both).
|
|
9
|
+
class Option
|
|
10
|
+
attr_reader :usage, :name, :short, :value, :text, :section, :needs, :build
|
|
11
|
+
|
|
12
|
+
def initialize(usage, text, section:, needs: :either, build: :both)
|
|
13
|
+
@usage = usage.start_with?("--") ? " #{usage}" : usage
|
|
14
|
+
words = usage.split
|
|
15
|
+
@short = words.size > 1 && words[1].start_with?("--") ? words[0].delete_suffix(",") : ""
|
|
16
|
+
@name = @short.empty? ? words[0] : words[1]
|
|
17
|
+
@value = words.last.start_with?("-") ? "" : words.last
|
|
18
|
+
@text = text
|
|
19
|
+
@section = section
|
|
20
|
+
@needs = needs
|
|
21
|
+
@build = build
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def valued? = !@value.empty?
|
|
25
|
+
|
|
26
|
+
def named?(flag) = flag == @name || (!@short.empty? && flag == @short)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Options
|
|
5
|
+
DEFAULT_RATE = 44_100
|
|
6
|
+
|
|
7
|
+
SECTIONS = ["Options", "Window options", "Options without the window", "Testing options"].freeze
|
|
8
|
+
|
|
9
|
+
# Every option of both builds, in the order the help lists them.
|
|
10
|
+
TABLE = [
|
|
11
|
+
Option.new("-s, --subtune N", "Subtune of a .sid, from 1 (default: the tune's own)", section: "Options"),
|
|
12
|
+
Option.new("--sid MODEL", "SID to fit: auto, 6581 or 8580 (default: auto, a .sid tune's own, else 6581)",
|
|
13
|
+
section: "Options"),
|
|
14
|
+
Option.new("--disable-jit", "Run without enabling YJIT", section: "Options", build: :ruby),
|
|
15
|
+
Option.new("-h, --help", "Show this help", section: "Options"),
|
|
16
|
+
Option.new("--version", "Show the version and what built it", section: "Options", build: :native),
|
|
17
|
+
Option.new("--no-autostart", "Boot to READY. instead of running the program",
|
|
18
|
+
section: "Window options", needs: :window),
|
|
19
|
+
Option.new("--read-only", "Mount a disk image write-protected, leaving its file unchanged",
|
|
20
|
+
section: "Window options", needs: :window),
|
|
21
|
+
Option.new("--sound", "Play the SID through the host's audio device (F10 mutes)",
|
|
22
|
+
section: "Window options", needs: :window),
|
|
23
|
+
Option.new("--no-sound", "Don't play the SID", section: "Window options", needs: :window, build: :native),
|
|
24
|
+
Option.new("--true-drive", "Put a true 1541 on device 8 instead of the KERNAL traps",
|
|
25
|
+
section: "Window options", needs: :window),
|
|
26
|
+
Option.new("--reu SIZE", "Plug in an REU of SIZE K: 128, 256, 512 (a 1750) or up to 16384",
|
|
27
|
+
section: "Window options", needs: :window),
|
|
28
|
+
Option.new("--ntsc", "Run an NTSC C64, with the 6567R8 VIC-II, instead of a PAL one",
|
|
29
|
+
section: "Window options", needs: :window),
|
|
30
|
+
Option.new("--no-vsync", "Pace frames by the timer or the sound instead of the display",
|
|
31
|
+
section: "Window options", needs: :window),
|
|
32
|
+
Option.new("--verbose", "Print the display, sound and gamepad setup and the frame timing",
|
|
33
|
+
section: "Window options", needs: :window),
|
|
34
|
+
Option.new("--headless", "Play a .sid tune in the terminal instead of the window",
|
|
35
|
+
section: "Options without the window"),
|
|
36
|
+
Option.new("--audio-out FILE", "Render a .sid tune to a .wav or .aiff file",
|
|
37
|
+
section: "Options without the window"),
|
|
38
|
+
Option.new("--seconds N", "Length to play (default: the tune's own)",
|
|
39
|
+
section: "Options without the window", needs: :headless),
|
|
40
|
+
Option.new("--songlengths PATH", "HVSC Songlengths.md5 to take the length from",
|
|
41
|
+
section: "Options without the window", needs: :headless),
|
|
42
|
+
Option.new("--rate HZ", "Sample rate (default: #{DEFAULT_RATE}, or the audio device's own)",
|
|
43
|
+
section: "Options without the window", needs: :headless),
|
|
44
|
+
Option.new("--filter-chunk N", "Filter step in cycles, 1 is exact (default: 4)",
|
|
45
|
+
section: "Options without the window", needs: :headless),
|
|
46
|
+
Option.new("--quiet", "Don't report progress", section: "Options without the window", needs: :headless),
|
|
47
|
+
Option.new("--all-subtunes", "Play on through each tune's subtunes, not just the one",
|
|
48
|
+
section: "Options without the window", needs: :headless),
|
|
49
|
+
Option.new("--no-tui", "Play without the interactive display, even on a terminal",
|
|
50
|
+
section: "Options without the window", needs: :headless),
|
|
51
|
+
Option.new("--frames N", "Quit after N frames", section: "Testing options", needs: :window),
|
|
52
|
+
Option.new("--unpaced", "Run as fast as it can, without vsync or pacing",
|
|
53
|
+
section: "Testing options", needs: :window),
|
|
54
|
+
Option.new("--screenshot FILE", "Save the last frame as a .bmp",
|
|
55
|
+
section: "Testing options", needs: :window),
|
|
56
|
+
Option.new("--save-snapshot FILE", "Save the machine as a .vsf snapshot after the last frame",
|
|
57
|
+
section: "Testing options", needs: :window),
|
|
58
|
+
Option.new("--at FRAME:EVENT", "Run EVENT once FRAME frames have run (see above)",
|
|
59
|
+
section: "Testing options", needs: :window),
|
|
60
|
+
Option.new("--script FILE", "Run the events in FILE, one FRAME:EVENT a line",
|
|
61
|
+
section: "Testing options", needs: :window)
|
|
62
|
+
].freeze
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
class Options
|
|
5
|
+
# The checks Options#parse makes once every argument has been read:
|
|
6
|
+
# the options fit the mode, the numbers are positive and the files
|
|
7
|
+
# exist.
|
|
8
|
+
module Validation
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def validate
|
|
12
|
+
validate_mode
|
|
13
|
+
validate_numbers
|
|
14
|
+
validate_paths
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def validate_mode
|
|
18
|
+
if headless?
|
|
19
|
+
raise Error, "#{@window_only.first} needs the window" unless @window_only.empty?
|
|
20
|
+
raise Error, "no tune given" if tune_paths.empty? && !player_window?
|
|
21
|
+
|
|
22
|
+
tune_paths.each { |path| validate_tune(path) }
|
|
23
|
+
elsif !@headless_only.empty?
|
|
24
|
+
raise Error, "#{@headless_only.first} needs --headless or --audio-out"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# `sid` takes directories as well as tunes.
|
|
29
|
+
def validate_tune(path)
|
|
30
|
+
return if !File.exist?(path) || File.extname(path).casecmp?(".sid")
|
|
31
|
+
return if @sid_command && File.directory?(path)
|
|
32
|
+
|
|
33
|
+
raise Error, "not a .sid tune: #{path}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def validate_numbers
|
|
37
|
+
positive("--subtune", @subtune)
|
|
38
|
+
positive("--seconds", @seconds)
|
|
39
|
+
positive("--rate", @rate)
|
|
40
|
+
positive("--filter-chunk", @filter_chunk)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def positive(name, value)
|
|
44
|
+
raise Error, "invalid argument: #{name} #{value}" if !value.nil? && value <= 0
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def validate_paths
|
|
48
|
+
missing = tune_paths.find { |path| !File.exist?(path) }
|
|
49
|
+
raise Error, "no such file or directory: #{missing}" unless missing.nil?
|
|
50
|
+
raise Error, "no such file or directory: #{@songlengths}" unless @songlengths.nil? || File.exist?(@songlengths)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|