badline 0.6.1 → 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 +56 -0
- data/CLAUDE.md +3 -1
- data/README.md +132 -247
- 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/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/cartridge/freezer.rb +2 -0
- data/lib/badline/cartridge.rb +3 -0
- data/lib/badline/frontend/app.rb +38 -35
- data/lib/badline/frontend/audio_sink.rb +6 -5
- data/lib/badline/frontend/boot.rb +9 -2
- data/lib/badline/frontend/buttons.rb +184 -0
- 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/filter_view.rb +96 -0
- data/lib/badline/frontend/frame_report.rb +52 -0
- data/lib/badline/frontend/info_view.rb +120 -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 +185 -0
- data/lib/badline/frontend/pause_menu.rb +246 -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/snapshots.rb +1 -1
- data/lib/badline/frontend/sound.rb +0 -1
- data/lib/badline/frontend/timeline.rb +5 -1
- data/lib/badline/frontend/visualizer_view.rb +101 -0
- data/lib/badline/frontend.rb +21 -0
- data/lib/badline/media/disk_set.rb +108 -0
- data/lib/badline/media/queue.rb +158 -0
- data/lib/badline/media.rb +14 -8
- data/lib/badline/options/help.rb +34 -5
- data/lib/badline/options/option.rb +1 -1
- data/lib/badline/options/table.rb +5 -3
- data/lib/badline/options/validation.rb +54 -0
- data/lib/badline/options.rb +65 -46
- 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 +36 -1
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,15 +8,15 @@ 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"),
|
|
16
16
|
Option.new("--version", "Show the version and what built it", section: "Options", build: :native),
|
|
17
17
|
Option.new("--no-autostart", "Boot to READY. instead of running the program",
|
|
18
18
|
section: "Window options", needs: :window),
|
|
19
|
-
Option.new("--
|
|
19
|
+
Option.new("--writable", "Let the machine write to disk images, changing their files",
|
|
20
20
|
section: "Window options", needs: :window),
|
|
21
21
|
Option.new("--sound", "Play the SID through the host's audio device (F10 mutes)",
|
|
22
22
|
section: "Window options", needs: :window),
|
|
@@ -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,11 +55,13 @@ 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
|
|
48
|
-
@
|
|
64
|
+
@writable = false
|
|
49
65
|
@sound = native
|
|
50
66
|
@frames = 0
|
|
51
67
|
@paced = 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,9 +131,11 @@ 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
|
-
def
|
|
138
|
+
def writable? = @writable
|
|
108
139
|
|
|
109
140
|
def sound? = @sound
|
|
110
141
|
|
|
@@ -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)
|
|
@@ -190,7 +238,7 @@ module Badline
|
|
|
190
238
|
def switch(name)
|
|
191
239
|
case name
|
|
192
240
|
when "--no-autostart" then @autostart = false
|
|
193
|
-
when "--
|
|
241
|
+
when "--writable" then @writable = true
|
|
194
242
|
when "--sound" then @sound = true
|
|
195
243
|
when "--no-sound" then @sound = false
|
|
196
244
|
when "--no-vsync" then @vsync = false
|
|
@@ -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
|
|
data/lib/badline/sid/catch_up.rb
CHANGED
|
@@ -96,7 +96,17 @@ module Badline
|
|
|
96
96
|
|
|
97
97
|
def record_sample(span)
|
|
98
98
|
sample = @decimator.push(current_sample, span)
|
|
99
|
-
|
|
99
|
+
return unless sample
|
|
100
|
+
|
|
101
|
+
@samples << sample
|
|
102
|
+
record_voices if @voice_samples
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def record_voices
|
|
106
|
+
voice_samples = @voice_samples
|
|
107
|
+
voice_samples << (@voice1.output >> 7)
|
|
108
|
+
voice_samples << (@voice2.output >> 7)
|
|
109
|
+
voice_samples << (@voice3.output >> 7)
|
|
100
110
|
end
|
|
101
111
|
|
|
102
112
|
def current_sample
|
data/lib/badline/sid/filter.rb
CHANGED
|
@@ -122,6 +122,15 @@ module Badline
|
|
|
122
122
|
reset
|
|
123
123
|
end
|
|
124
124
|
|
|
125
|
+
# The integrators and the registers carry over; the cutoff maps
|
|
126
|
+
# through the new model's curve.
|
|
127
|
+
def model=(model)
|
|
128
|
+
@model = model
|
|
129
|
+
@w0_table = W0.fetch(model)
|
|
130
|
+
@mixer_dc = MIXER_DC.fetch(model)
|
|
131
|
+
@w0 = @w0_table[@cutoff]
|
|
132
|
+
end
|
|
133
|
+
|
|
125
134
|
def reset
|
|
126
135
|
@cutoff = 0x000
|
|
127
136
|
@routing = 0x0
|
data/lib/badline/sid/voice.rb
CHANGED
|
@@ -33,6 +33,12 @@ module Badline
|
|
|
33
33
|
@dc_offset = DC_OFFSET.fetch(model)
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
+
def model=(model)
|
|
37
|
+
@waveform.model = model
|
|
38
|
+
@wave_zero = WAVE_ZERO.fetch(model)
|
|
39
|
+
@dc_offset = DC_OFFSET.fetch(model)
|
|
40
|
+
end
|
|
41
|
+
|
|
36
42
|
def reset!
|
|
37
43
|
@waveform.reset!
|
|
38
44
|
@envelope = Envelope.new
|
data/lib/badline/sid/waveform.rb
CHANGED
|
@@ -54,17 +54,24 @@ module Badline
|
|
|
54
54
|
attr_reader :accumulator, :shift_register, :frequency, :pulse_width, :pulse
|
|
55
55
|
|
|
56
56
|
def initialize(model: :mos6581)
|
|
57
|
-
|
|
57
|
+
use_model(model)
|
|
58
58
|
@tri_saw_delay = model == :mos8580
|
|
59
|
-
@pulse_noise_read = PULSE_NOISE_READ.fetch(model)
|
|
60
|
-
@pulse_noise_write = PULSE_NOISE_WRITE.fetch(model)
|
|
61
|
-
@combined = Combined.tables(model)
|
|
62
|
-
@shift_register_reset_delay = SHIFT_REGISTER_RESET_DELAY.fetch(model)
|
|
63
59
|
@accumulator = POWER_ON_ACCUMULATOR
|
|
64
60
|
@sync_source = @sync_dest = self
|
|
65
61
|
reset!
|
|
66
62
|
end
|
|
67
63
|
|
|
64
|
+
# The running state carries over. A 6581 never delays its shapers, so
|
|
65
|
+
# the 8580's delayed triangle and sawtooth start from the current phase.
|
|
66
|
+
def model=(model)
|
|
67
|
+
use_model(model)
|
|
68
|
+
if model == :mos8580 && !@tri_saw_delay
|
|
69
|
+
@delayed_sawtooth = sawtooth
|
|
70
|
+
@delayed_triangle = triangle
|
|
71
|
+
end
|
|
72
|
+
@tri_saw_delay = model == :mos8580
|
|
73
|
+
end
|
|
74
|
+
|
|
68
75
|
# The RES line clears the registers and reseeds the LFSR, but leaves
|
|
69
76
|
# the accumulator alone (SID/oscinit).
|
|
70
77
|
def reset!
|
|
@@ -190,6 +197,14 @@ module Badline
|
|
|
190
197
|
|
|
191
198
|
private
|
|
192
199
|
|
|
200
|
+
def use_model(model)
|
|
201
|
+
@topbit_feedback = model != :mos8580
|
|
202
|
+
@pulse_noise_read = PULSE_NOISE_READ.fetch(model)
|
|
203
|
+
@pulse_noise_write = PULSE_NOISE_WRITE.fetch(model)
|
|
204
|
+
@combined = Combined.tables(model)
|
|
205
|
+
@shift_register_reset_delay = SHIFT_REGISTER_RESET_DELAY.fetch(model)
|
|
206
|
+
end
|
|
207
|
+
|
|
193
208
|
def shape(selected, saw = sawtooth, tri = triangle(selected))
|
|
194
209
|
case selected
|
|
195
210
|
when 0x1 then tri
|
data/lib/badline/sid.rb
CHANGED
|
@@ -16,7 +16,9 @@ module Badline
|
|
|
16
16
|
# $D41B-$D41C - Voice 3 oscillator/envelope - read only
|
|
17
17
|
# $D41D-$D41F - Unconnected
|
|
18
18
|
#
|
|
19
|
-
# The register file mirrors every 32 bytes up to $D7FF.
|
|
19
|
+
# The register file mirrors every 32 bytes up to $D7FF. A second or
|
|
20
|
+
# third SID, which only the SID player fits, is built `at` another
|
|
21
|
+
# address and decodes just the 32 bytes there.
|
|
20
22
|
#
|
|
21
23
|
# Reading a write-only or unconnected register returns the last byte the
|
|
22
24
|
# SID saw on the data bus. That value fades to $00 as the capacitance
|
|
@@ -55,8 +57,8 @@ module Badline
|
|
|
55
57
|
attr_accessor :pots
|
|
56
58
|
attr_reader :model
|
|
57
59
|
|
|
58
|
-
def initialize(model: :mos6581, pots: nil, filter_chunk: FILTER_CHUNK)
|
|
59
|
-
addressable_at(
|
|
60
|
+
def initialize(model: :mos6581, pots: nil, filter_chunk: FILTER_CHUNK, at: 0xd400)
|
|
61
|
+
addressable_at(at, length: at == 0xd400 ? 2**10 : 2**5)
|
|
60
62
|
@model = model
|
|
61
63
|
@pots = pots
|
|
62
64
|
@bus_ttl_reset = BUS_TTL.fetch(model)
|
|
@@ -67,6 +69,7 @@ module Badline
|
|
|
67
69
|
@deferred_writes = []
|
|
68
70
|
@decimator = nil
|
|
69
71
|
@samples = []
|
|
72
|
+
@voice_samples = nil
|
|
70
73
|
@filter_chunk = filter_chunk
|
|
71
74
|
@voices = Voice.linked(VOICES, model:)
|
|
72
75
|
@voice1, @voice2, @voice3 = @voices
|
|
@@ -74,6 +77,18 @@ module Badline
|
|
|
74
77
|
reset!
|
|
75
78
|
end
|
|
76
79
|
|
|
80
|
+
# Switches the chip, its filter and its voices to the other model's
|
|
81
|
+
# values. The cycles already passed run on the old model first, and the
|
|
82
|
+
# running state carries over.
|
|
83
|
+
def model=(model)
|
|
84
|
+
catch_up
|
|
85
|
+
@model = model
|
|
86
|
+
@bus_ttl_reset = BUS_TTL.fetch(model)
|
|
87
|
+
@filter.model = model
|
|
88
|
+
@voices.each { |voice| voice.model = model }
|
|
89
|
+
update_span_rules
|
|
90
|
+
end
|
|
91
|
+
|
|
77
92
|
# The RES line clears the registers, the data bus, the filter and the
|
|
78
93
|
# voices, all but their accumulators. Recording carries on across it.
|
|
79
94
|
def reset!
|
|
@@ -112,6 +127,23 @@ module Badline
|
|
|
112
127
|
@samples = []
|
|
113
128
|
end
|
|
114
129
|
|
|
130
|
+
# Also keeps each voice's own output, before the filter and the volume,
|
|
131
|
+
# as each recorded sample closes, for #drain_voice_samples.
|
|
132
|
+
def record_voices!
|
|
133
|
+
@voice_samples = [] if @voice_samples.nil?
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# The voices' outputs since the last drain, three to a sample, in voice
|
|
137
|
+
# order, or nothing unless #record_voices! asked for them.
|
|
138
|
+
def drain_voice_samples
|
|
139
|
+
return [] if @voice_samples.nil?
|
|
140
|
+
|
|
141
|
+
catch_up
|
|
142
|
+
samples = @voice_samples
|
|
143
|
+
@voice_samples = []
|
|
144
|
+
samples
|
|
145
|
+
end
|
|
146
|
+
|
|
115
147
|
# The samples recorded since the last drain.
|
|
116
148
|
def drain_samples
|
|
117
149
|
catch_up
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Badline
|
|
4
|
+
module Storage
|
|
5
|
+
# The layout of an HVSC collection: tunes in directories under its root,
|
|
6
|
+
# and databases about them in `DOCUMENTS`. A tune's HVSC path is its path
|
|
7
|
+
# from the root, as `/MUSICIANS/H/Hubbard_Rob/Commando.sid`.
|
|
8
|
+
module HVSC
|
|
9
|
+
class << self
|
|
10
|
+
# The named file in the DOCUMENTS directory of a collection above the
|
|
11
|
+
# tune, or else of the one `$HVSC_BASE` points at.
|
|
12
|
+
def document(tune_path, name)
|
|
13
|
+
candidates(tune_path).map { |dir| File.join(dir, "DOCUMENTS", name) }
|
|
14
|
+
.find { |path| File.file?(path) }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# The tune's HVSC path in the collection that holds the document, or
|
|
18
|
+
# nil when the tune lies outside it.
|
|
19
|
+
def path(tune_path, document)
|
|
20
|
+
root = File.dirname(File.expand_path(document), 2)
|
|
21
|
+
tune = File.expand_path(tune_path)
|
|
22
|
+
tune.start_with?("#{root}/") ? tune[root.length..] : nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def candidates(tune_path)
|
|
28
|
+
dirs = []
|
|
29
|
+
dir = File.dirname(File.expand_path(tune_path))
|
|
30
|
+
until dirs.last == dir
|
|
31
|
+
dirs << dir
|
|
32
|
+
dir = File.dirname(dir)
|
|
33
|
+
end
|
|
34
|
+
base = ENV.fetch("HVSC_BASE", nil)
|
|
35
|
+
dirs << base if base
|
|
36
|
+
dirs
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|