badline 0.6.1 → 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 +37 -0
- data/README.md +125 -46
- data/exe/badline-ruby +9 -1
- 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 +5 -0
- data/lib/badline/audio/aiff.rb +2 -2
- data/lib/badline/audio/bare_player.rb +23 -12
- data/lib/badline/audio/cli.rb +153 -58
- 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 -0
- data/lib/badline/frontend/audio_sink.rb +6 -5
- data/lib/badline/frontend/boot.rb +1 -1
- data/lib/badline/frontend/buttons.rb +90 -0
- data/lib/badline/frontend/filter_view.rb +96 -0
- data/lib/badline/frontend/info_view.rb +120 -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/sid_history.rb +175 -0
- data/lib/badline/frontend/sid_view.rb +140 -0
- data/lib/badline/frontend/visualizer_view.rb +101 -0
- data/lib/badline/frontend.rb +14 -0
- data/lib/badline/media/queue.rb +158 -0
- data/lib/badline/media.rb +13 -8
- data/lib/badline/options/help.rb +34 -5
- data/lib/badline/options/option.rb +1 -1
- data/lib/badline/options/table.rb +4 -2
- data/lib/badline/options/validation.rb +54 -0
- data/lib/badline/options.rb +62 -43
- data/lib/badline/sdl.rb +16 -6
- 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.rb +20 -5
- data/lib/badline/sid.rb +35 -3
- 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
- metadata +24 -1
data/lib/badline/frontend.rb
CHANGED
|
@@ -16,3 +16,17 @@ require "badline/frontend/pad_port"
|
|
|
16
16
|
require "badline/frontend/gamepads"
|
|
17
17
|
require "badline/frontend/app"
|
|
18
18
|
require "badline/frontend/audio_sink"
|
|
19
|
+
require "badline/frontend/painter"
|
|
20
|
+
require "badline/frontend/sid_history"
|
|
21
|
+
require "badline/frontend/sid_view"
|
|
22
|
+
require "badline/frontend/filter_view"
|
|
23
|
+
require "badline/frontend/scope"
|
|
24
|
+
require "badline/frontend/visualizer_view"
|
|
25
|
+
require "badline/frontend/buttons"
|
|
26
|
+
require "badline/frontend/player_state"
|
|
27
|
+
require "badline/frontend/player_header"
|
|
28
|
+
require "badline/frontend/player_footer"
|
|
29
|
+
require "badline/frontend/info_view"
|
|
30
|
+
require "badline/frontend/player_commands"
|
|
31
|
+
require "badline/frontend/player_screen"
|
|
32
|
+
require "badline/frontend/player_window"
|
|
@@ -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
|
|
data/lib/badline/options/help.rb
CHANGED
|
@@ -19,7 +19,7 @@ module Badline
|
|
|
19
19
|
# What --help prints: the usage, then the options this build takes,
|
|
20
20
|
# section by section, laid out as OptionParser did.
|
|
21
21
|
def help
|
|
22
|
-
lines = [banner]
|
|
22
|
+
lines = [@sid_command ? sid_banner : banner]
|
|
23
23
|
SECTIONS.each do |section|
|
|
24
24
|
options = TABLE.select { |option| option.section == section && takes?(option) }
|
|
25
25
|
next if options.empty?
|
|
@@ -40,6 +40,7 @@ module Badline
|
|
|
40
40
|
Usage: #{@program} [options] [media]
|
|
41
41
|
#{@program} --headless [options] tune.sid
|
|
42
42
|
#{@program} [options] tune.sid --audio-out FILE
|
|
43
|
+
#{@program} sid [options] FILE|DIR...
|
|
43
44
|
|
|
44
45
|
Media can be a .prg/.p00 program, a .d64/.d71/.d81 disk image, a
|
|
45
46
|
.g64 disk image for the true 1541, a .t64 tape archive, a .tap
|
|
@@ -48,18 +49,23 @@ module Badline
|
|
|
48
49
|
saves a snapshot and F12 restores it. Device 8 answers through
|
|
49
50
|
traps on the KERNAL's disk routines, unless --true-drive puts an
|
|
50
51
|
emulated 1541 there, which runs its own DOS and reads .d64 and .g64
|
|
51
|
-
images only.
|
|
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.
|
|
52
54
|
|
|
53
55
|
--headless plays a .sid tune on the host's audio device without the
|
|
54
56
|
window, and --audio-out renders it to 16-bit PCM instead. The
|
|
55
57
|
container follows the file's extension, .wav or .aiff. Played on a
|
|
56
|
-
terminal,
|
|
57
|
-
space pauses and q quits.
|
|
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.
|
|
58
63
|
|
|
59
64
|
A .sid file carries no length of its own. Without --seconds the tune is
|
|
60
65
|
looked up by MD5 in HVSC's Songlengths.md5, taken from --songlengths, from
|
|
61
66
|
the DOCUMENTS directory of an HVSC collection above the tune, or from
|
|
62
|
-
$HVSC_BASE. Failing all of those it runs for #{FALLBACK_SECONDS.to_i} seconds
|
|
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}.
|
|
63
69
|
|
|
64
70
|
#{@native ? NATIVE_SPEED : RUBY_SPEED}
|
|
65
71
|
|
|
@@ -74,5 +80,28 @@ module Badline
|
|
|
74
80
|
argument.
|
|
75
81
|
BANNER
|
|
76
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
|
|
77
106
|
end
|
|
78
107
|
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Badline
|
|
4
4
|
class Options
|
|
5
5
|
# One option of the table: its flags and value as the help shows them
|
|
6
|
-
# (`-s, --
|
|
6
|
+
# (`-s, --subtune N`), what it does, the help section it's listed in, the
|
|
7
7
|
# mode it needs (:window, :headless or :either), and the build that
|
|
8
8
|
# takes it (:native, :ruby or :both).
|
|
9
9
|
class Option
|
|
@@ -8,8 +8,8 @@ module Badline
|
|
|
8
8
|
|
|
9
9
|
# Every option of both builds, in the order the help lists them.
|
|
10
10
|
TABLE = [
|
|
11
|
-
Option.new("-s, --
|
|
12
|
-
Option.new("--sid MODEL", "SID to fit: 6581 or 8580 (default: a .sid tune's own, else 6581)",
|
|
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
13
|
section: "Options"),
|
|
14
14
|
Option.new("--disable-jit", "Run without enabling YJIT", section: "Options", build: :ruby),
|
|
15
15
|
Option.new("-h, --help", "Show this help", section: "Options"),
|
|
@@ -44,6 +44,8 @@ module Badline
|
|
|
44
44
|
Option.new("--filter-chunk N", "Filter step in cycles, 1 is exact (default: 4)",
|
|
45
45
|
section: "Options without the window", needs: :headless),
|
|
46
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),
|
|
47
49
|
Option.new("--no-tui", "Play without the interactive display, even on a terminal",
|
|
48
50
|
section: "Options without the window", needs: :headless),
|
|
49
51
|
Option.new("--frames N", "Quit after N frames", section: "Testing options", needs: :window),
|
|
@@ -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
|
data/lib/badline/options.rb
CHANGED
|
@@ -4,33 +4,47 @@ require "badline/options/option"
|
|
|
4
4
|
require "badline/options/event"
|
|
5
5
|
require "badline/options/table"
|
|
6
6
|
require "badline/options/help"
|
|
7
|
+
require "badline/options/validation"
|
|
7
8
|
|
|
8
9
|
module Badline
|
|
9
10
|
# The command line of both builds, badline-ruby and the native badline.
|
|
10
11
|
# Either opens the window for any media, or with --headless or
|
|
11
|
-
# --audio-out plays or renders a .sid tune without one.
|
|
12
|
-
#
|
|
12
|
+
# --audio-out plays or renders a .sid tune without one. `sid`, given
|
|
13
|
+
# first, plays .sid tunes and directories of them in the terminal. The
|
|
14
|
+
# native build adds --no-sound and --version, and badline-ruby adds
|
|
15
|
+
# --disable-jit.
|
|
13
16
|
#
|
|
14
17
|
# TABLE lists the options, and both the parser and the help read it. It
|
|
15
18
|
# parses by hand, inside the subset of Ruby Spinel compiles. Values come
|
|
16
|
-
# as `--
|
|
19
|
+
# as `--subtune 2` or `--subtune=2`, and `--` ends the options. It loads
|
|
17
20
|
# nothing else from badline, so badline-ruby can parse and reject its
|
|
18
21
|
# arguments before paying for the emulator.
|
|
19
22
|
class Options
|
|
23
|
+
include Validation
|
|
24
|
+
|
|
20
25
|
class Error < StandardError; end
|
|
21
26
|
|
|
22
27
|
FALLBACK_SECONDS = 60.0
|
|
23
28
|
|
|
29
|
+
# A tune on the fallback length ends sooner once it has been silent
|
|
30
|
+
# this long.
|
|
31
|
+
SILENCE_SECONDS = 5.0
|
|
32
|
+
|
|
24
33
|
SID_MODELS = { "6581" => :mos6581, "8580" => :mos8580 }.freeze
|
|
25
34
|
|
|
35
|
+
# What `sid` leaves out besides the window's options.
|
|
36
|
+
NOT_FOR_SID = %w[--audio-out].freeze
|
|
37
|
+
|
|
26
38
|
REU_SIZES = %w[128 256 512 1024 2048 4096 8192 16384].freeze
|
|
27
39
|
|
|
28
40
|
# A Float as OptionParser took one: digits with an optional sign,
|
|
29
41
|
# fraction and exponent.
|
|
30
42
|
DECIMAL = /\A[-+]?(\d+(\.\d+)?|\.\d+)([eE][-+]?\d+)?\z/
|
|
31
43
|
|
|
32
|
-
|
|
33
|
-
|
|
44
|
+
# tune_paths holds the media given, which `sid` takes more than one of:
|
|
45
|
+
# tunes and directories of tunes, in the order given.
|
|
46
|
+
attr_reader :program, :media_path, :tune_paths, :subtune, :sid_model, :reu, :frames, :screenshot, :save_snapshot,
|
|
47
|
+
:audio_out, :seconds, :songlengths, :filter_chunk, :timeline
|
|
34
48
|
|
|
35
49
|
def self.parse(argv, native: false) = new(native:).parse(argv)
|
|
36
50
|
|
|
@@ -41,7 +55,9 @@ module Badline
|
|
|
41
55
|
@native = native
|
|
42
56
|
@program = native ? "badline" : "badline-ruby"
|
|
43
57
|
@media_path = nil
|
|
44
|
-
@
|
|
58
|
+
@sid_command = false
|
|
59
|
+
@tune_paths = []
|
|
60
|
+
@subtune = nil
|
|
45
61
|
@sid_model = nil
|
|
46
62
|
@reu = nil
|
|
47
63
|
@autostart = true
|
|
@@ -67,13 +83,16 @@ module Badline
|
|
|
67
83
|
@filter_chunk = nil
|
|
68
84
|
@quiet = false
|
|
69
85
|
@tui = true
|
|
86
|
+
@all_subtunes = false
|
|
70
87
|
@window_only = []
|
|
71
88
|
@headless_only = []
|
|
72
89
|
end
|
|
73
90
|
|
|
74
91
|
def parse(argv)
|
|
75
92
|
args = argv.dup
|
|
93
|
+
sid_command(args) if args.first == "sid"
|
|
76
94
|
argument(args.shift, args) until args.empty?
|
|
95
|
+
play_lone_tune
|
|
77
96
|
validate unless help? || version?
|
|
78
97
|
self
|
|
79
98
|
end
|
|
@@ -82,7 +101,14 @@ module Badline
|
|
|
82
101
|
def snapshot? = !@media_path.nil? && File.extname(@media_path).casecmp?(".vsf")
|
|
83
102
|
|
|
84
103
|
# Plays or renders a .sid tune without the window.
|
|
85
|
-
def headless? = @headless || render?
|
|
104
|
+
def headless? = @headless || render? || @sid_command
|
|
105
|
+
|
|
106
|
+
# Whether `sid` came first, to play the tunes and directories given.
|
|
107
|
+
def sid_command? = @sid_command
|
|
108
|
+
|
|
109
|
+
# Whether `sid` plays in the SID player's window rather than the
|
|
110
|
+
# terminal.
|
|
111
|
+
def player_window? = @sid_command && !@headless
|
|
86
112
|
|
|
87
113
|
def window? = !headless?
|
|
88
114
|
|
|
@@ -94,6 +120,9 @@ module Badline
|
|
|
94
120
|
# How long to play a tune whose length nothing gives.
|
|
95
121
|
def fallback_seconds = FALLBACK_SECONDS
|
|
96
122
|
|
|
123
|
+
# How long a tune playing for fallback_seconds may stay silent.
|
|
124
|
+
def silence_seconds = SILENCE_SECONDS
|
|
125
|
+
|
|
97
126
|
def rate = @rate.nil? ? DEFAULT_RATE : @rate
|
|
98
127
|
|
|
99
128
|
def rate_given? = !@rate.nil?
|
|
@@ -102,6 +131,8 @@ module Badline
|
|
|
102
131
|
|
|
103
132
|
def tui? = @tui
|
|
104
133
|
|
|
134
|
+
def all_subtunes? = @all_subtunes
|
|
135
|
+
|
|
105
136
|
def autostart? = @autostart
|
|
106
137
|
|
|
107
138
|
def read_only? = @read_only
|
|
@@ -126,7 +157,24 @@ module Badline
|
|
|
126
157
|
|
|
127
158
|
private
|
|
128
159
|
|
|
129
|
-
def takes?(option)
|
|
160
|
+
def takes?(option)
|
|
161
|
+
return false if @sid_command && (option.needs == :window || NOT_FOR_SID.include?(option.name))
|
|
162
|
+
|
|
163
|
+
[:both, @native ? :native : :ruby].include?(option.build)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def sid_command(args)
|
|
167
|
+
args.shift
|
|
168
|
+
@sid_command = true
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# A .sid on its own plays in the SID player, as `sid` would, unless an
|
|
172
|
+
# option of the emulator's window asks for the machine.
|
|
173
|
+
def play_lone_tune
|
|
174
|
+
return if @sid_command || headless? || !@window_only.empty? || @media_path.nil?
|
|
175
|
+
|
|
176
|
+
@sid_command = File.extname(@media_path).casecmp?(".sid")
|
|
177
|
+
end
|
|
130
178
|
|
|
131
179
|
def argument(arg, args)
|
|
132
180
|
if arg == "--"
|
|
@@ -172,7 +220,7 @@ module Badline
|
|
|
172
220
|
|
|
173
221
|
def valued_option(name, flag, value)
|
|
174
222
|
case name
|
|
175
|
-
when "--
|
|
223
|
+
when "--subtune" then @subtune = number(flag, value)
|
|
176
224
|
when "--sid" then @sid_model = sid_model_for(value)
|
|
177
225
|
when "--reu" then @reu = reu_size(value)
|
|
178
226
|
when "--frames" then @frames = number(flag, value)
|
|
@@ -203,11 +251,13 @@ module Badline
|
|
|
203
251
|
when "--version" then @version = true
|
|
204
252
|
when "--headless" then @headless = true
|
|
205
253
|
when "--quiet" then @quiet = true
|
|
254
|
+
when "--all-subtunes" then @all_subtunes = true
|
|
206
255
|
else @tui = false
|
|
207
256
|
end
|
|
208
257
|
end
|
|
209
258
|
|
|
210
259
|
def sid_model_for(value)
|
|
260
|
+
return if value == "auto"
|
|
211
261
|
raise Error, "invalid argument: --sid #{value}" unless SID_MODELS.key?(value)
|
|
212
262
|
|
|
213
263
|
SID_MODELS[value]
|
|
@@ -232,41 +282,10 @@ module Badline
|
|
|
232
282
|
end
|
|
233
283
|
|
|
234
284
|
def media_argument(arg)
|
|
235
|
-
raise Error, "unexpected argument: #{arg}" unless @media_path.nil?
|
|
236
|
-
|
|
237
|
-
@media_path = arg
|
|
238
|
-
end
|
|
239
|
-
|
|
240
|
-
def validate
|
|
241
|
-
validate_mode
|
|
242
|
-
validate_numbers
|
|
243
|
-
validate_paths
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
def validate_mode
|
|
247
|
-
if headless?
|
|
248
|
-
raise Error, "#{@window_only.first} needs the window" unless @window_only.empty?
|
|
249
|
-
raise Error, "no tune given" if @media_path.nil?
|
|
250
|
-
raise Error, "not a .sid tune: #{@media_path}" unless File.extname(@media_path).casecmp?(".sid")
|
|
251
|
-
elsif !@headless_only.empty?
|
|
252
|
-
raise Error, "#{@headless_only.first} needs --headless or --audio-out"
|
|
253
|
-
end
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
def validate_numbers
|
|
257
|
-
positive("--song", @song)
|
|
258
|
-
positive("--seconds", @seconds)
|
|
259
|
-
positive("--rate", @rate)
|
|
260
|
-
positive("--filter-chunk", @filter_chunk)
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
def positive(name, value)
|
|
264
|
-
raise Error, "invalid argument: #{name} #{value}" if !value.nil? && value <= 0
|
|
265
|
-
end
|
|
285
|
+
raise Error, "unexpected argument: #{arg}" unless @sid_command || @media_path.nil?
|
|
266
286
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
raise Error, "no such file or directory: #{@songlengths}" unless @songlengths.nil? || File.exist?(@songlengths)
|
|
287
|
+
@media_path = arg if @media_path.nil?
|
|
288
|
+
@tune_paths << arg
|
|
270
289
|
end
|
|
271
290
|
end
|
|
272
291
|
end
|
data/lib/badline/sdl.rb
CHANGED
|
@@ -15,6 +15,7 @@ module Badline
|
|
|
15
15
|
ffi_func :SDL_SetHint, %i[str str], :int
|
|
16
16
|
ffi_func :SDL_CreateWindow, %i[str int int int int uint32], :ptr
|
|
17
17
|
ffi_func :SDL_SetWindowTitle, %i[ptr str], :void
|
|
18
|
+
ffi_func :SDL_SetWindowSize, %i[ptr int int], :void
|
|
18
19
|
ffi_func :SDL_DestroyWindow, [:ptr], :void
|
|
19
20
|
ffi_func :SDL_CreateRenderer, %i[ptr int uint32], :ptr
|
|
20
21
|
ffi_func :SDL_GetWindowDisplayMode, %i[ptr ptr], :int
|
|
@@ -26,6 +27,9 @@ module Badline
|
|
|
26
27
|
ffi_func :SDL_RenderClear, [:ptr], :int
|
|
27
28
|
ffi_func :SDL_SetRenderDrawColor, %i[ptr int int int int], :int
|
|
28
29
|
ffi_func :SDL_RenderFillRect, %i[ptr ptr], :int
|
|
30
|
+
ffi_func :SDL_RenderDrawLines, %i[ptr int_array int], :int
|
|
31
|
+
ffi_func :SDL_SetTextureBlendMode, %i[ptr int], :int
|
|
32
|
+
ffi_func :SDL_SetTextureColorMod, %i[ptr int int int], :int
|
|
29
33
|
ffi_func :SDL_RenderCopy, %i[ptr ptr ptr ptr], :int
|
|
30
34
|
ffi_func :SDL_RenderPresent, [:ptr], :void
|
|
31
35
|
ffi_func :SDL_PollEvent, [:ptr], :int
|
|
@@ -52,9 +56,6 @@ module Badline
|
|
|
52
56
|
ffi_func :SDL_GameControllerGetButton, %i[ptr int], :uint8
|
|
53
57
|
ffi_func :SDL_GameControllerGetAxis, %i[ptr int], :int16
|
|
54
58
|
ffi_func :SDL_ClearQueuedAudio, [:uint32], :void
|
|
55
|
-
ffi_func :SDL_GetCurrentDisplayMode, %i[int ptr], :int
|
|
56
|
-
ffi_func :SDL_CreateRGBSurfaceFrom, %i[ptr int int int int uint32 uint32 uint32 uint32], :ptr
|
|
57
|
-
ffi_func :SDL_CreateTextureFromSurface, %i[ptr ptr], :ptr
|
|
58
59
|
ffi_func :SDL_PushEvent, [:ptr], :int
|
|
59
60
|
ffi_func :SDL_GetKeyName, [:int], :str
|
|
60
61
|
|
|
@@ -69,6 +70,11 @@ module Badline
|
|
|
69
70
|
ffi_read_i32 :event_xrel, 28
|
|
70
71
|
ffi_read_i32 :event_yrel, 32
|
|
71
72
|
ffi_read_u8 :event_button, 16
|
|
73
|
+
# Where the pointer is, in a motion or button event.
|
|
74
|
+
ffi_read_i32 :event_x, 20
|
|
75
|
+
ffi_read_i32 :event_y, 24
|
|
76
|
+
# SDL_DropEvent's file, which the receiver frees.
|
|
77
|
+
ffi_read_ptr :event_file, 8
|
|
72
78
|
|
|
73
79
|
ffi_buffer :rect, 16
|
|
74
80
|
ffi_write_i32 :rect_x, 0
|
|
@@ -77,6 +83,8 @@ module Badline
|
|
|
77
83
|
ffi_write_i32 :rect_h, 12
|
|
78
84
|
# Where the drive LED goes.
|
|
79
85
|
ffi_buffer :led_rect, 16
|
|
86
|
+
# A glyph's place in the SID player's font, drawn at SDL.rect.
|
|
87
|
+
ffi_buffer :glyph_rect, 16
|
|
80
88
|
|
|
81
89
|
# SDL_DisplayMode: Uint32 format; int w, h, refresh_rate; then a pointer.
|
|
82
90
|
ffi_buffer :display_mode, 24
|
|
@@ -104,6 +112,8 @@ module Badline
|
|
|
104
112
|
ffi_const :RENDERER_ACCELERATED, 0x02
|
|
105
113
|
ffi_const :RENDERER_PRESENTVSYNC, 0x04
|
|
106
114
|
ffi_const :PIXELFORMAT_RGB888, 0x16161804
|
|
115
|
+
ffi_const :PIXELFORMAT_ARGB8888, 0x16362004
|
|
116
|
+
ffi_const :BLENDMODE_BLEND, 0x01
|
|
107
117
|
ffi_const :TEXTUREACCESS_STREAMING, 1
|
|
108
118
|
ffi_const :QUIT, 0x100
|
|
109
119
|
ffi_const :KEYDOWN, 0x300
|
|
@@ -114,13 +124,10 @@ module Badline
|
|
|
114
124
|
ffi_const :KMOD_SHIFT, 0x0003
|
|
115
125
|
ffi_const :CONTROLLERDEVICEADDED, 0x653
|
|
116
126
|
ffi_const :CONTROLLERDEVICEREMOVED, 0x654
|
|
117
|
-
ffi_const :CONTROLLERDEVICEREMAPPED, 0x655
|
|
118
127
|
ffi_const :AUDIO_S16LSB, 0x8010
|
|
119
128
|
ffi_const :ALLOW_FREQUENCY_CHANGE, 0x01
|
|
120
129
|
ffi_const :KEY_TAB, 0x09
|
|
121
130
|
ffi_const :KEY_F10, 0x4000_0043
|
|
122
|
-
ffi_const :KEY_F11, 0x4000_0044
|
|
123
|
-
ffi_const :KEY_F12, 0x4000_0045
|
|
124
131
|
end
|
|
125
132
|
|
|
126
133
|
# The few C library calls the native front end makes.
|
|
@@ -128,6 +135,9 @@ module Badline
|
|
|
128
135
|
ffi_func :malloc, [:size_t], :ptr
|
|
129
136
|
ffi_func :free, [:ptr], :void
|
|
130
137
|
ffi_func :poll, %i[ptr size_t int], :int
|
|
138
|
+
# Called with an empty needle, it returns the C string it's given as a
|
|
139
|
+
# String.
|
|
140
|
+
ffi_func :strstr, %i[ptr str], :str
|
|
131
141
|
|
|
132
142
|
ffi_const :POLLIN, 0x01
|
|
133
143
|
|